-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathimagecollection.py
461 lines (363 loc) · 14.1 KB
/
imagecollection.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
"""
Modified version of skimage.io.collections that handles array shapes/slicing.
Data structures to hold collections of images, with optional caching.
"""
from __future__ import with_statement
import os
from glob import glob
import re
from copy import copy
import numpy as np
import six
from PIL import Image
from skimage.external.tifffile import TiffFile
__all__ = ['MultiImage', 'ImageCollection', 'concatenate_images',
'imread_collection_wrapper']
def concatenate_images(ic):
"""Concatenate all images in the image collection into an array.
Parameters
----------
ic: an iterable of images (including ImageCollection and MultiImage)
The images to be concatenated.
Returns
-------
ar : np.ndarray
An array having one more dimension than the images in `ic`.
See Also
--------
ImageCollection.concatenate, MultiImage.concatenate
Raises
------
ValueError
If images in `ic` don't have identical shapes.
"""
all_images = [img[np.newaxis, ...] for img in ic]
try:
ar = np.concatenate(all_images)
except ValueError:
raise ValueError('Image dimensions must agree.')
return ar
def alphanumeric_key(s):
"""Convert string to list of strings and ints that gives intuitive sorting.
Parameters
----------
s: string
Returns
-------
k: a list of strings and ints
Examples
--------
>>> alphanumeric_key('z23a')
['z', 23, 'a']
>>> filenames = ['f9.10.png', 'e10.png', 'f9.9.png', 'f10.10.png',
... 'f10.9.png']
>>> sorted(filenames)
['e10.png', 'f10.10.png', 'f10.9.png', 'f9.10.png', 'f9.9.png']
>>> sorted(filenames, key=alphanumeric_key)
['e10.png', 'f9.9.png', 'f9.10.png', 'f10.9.png', 'f10.10.png']
"""
k = [int(c) if c.isdigit() else c for c in re.split('([0-9]+)', s)]
return k
class ImageCollection(object):
"""Load and manage a collection of image files.
Note that files are always stored in alphabetical order. Also note that
slicing returns a new ImageCollection, *not* a view into the data.
Parameters
----------
load_pattern : str or list
Pattern glob or filenames to load. The path can be absolute or
relative. Multiple patterns should be separated by os.pathsep,
e.g. '/tmp/work/*.png:/tmp/other/*.jpg'. Also see
implementation notes below.
conserve_memory : bool, optional
If True, never keep more than one in memory at a specific
time. Otherwise, images will be cached once they are loaded.
Other parameters
----------------
load_func : callable
``imread`` by default. See notes below.
Attributes
----------
files : list of str
If a glob string is given for `load_pattern`, this attribute
stores the expanded file list. Otherwise, this is simply
equal to `load_pattern`.
Notes
-----
ImageCollection can be modified to load images from an arbitrary
source by specifying a combination of `load_pattern` and
`load_func`. For an ImageCollection ``ic``, ``ic[5]`` uses
``load_func(file_pattern[5])`` to load the image.
Imagine, for example, an ImageCollection that loads every tenth
frame from a video file::
class AVILoader:
video_file = 'myvideo.avi'
def __call__(self, frame):
return video_read(self.video_file, frame)
avi_load = AVILoader()
frames = range(0, 1000, 10) # 0, 10, 20, ...
ic = ImageCollection(frames, load_func=avi_load)
x = ic[5] # calls avi_load(frames[5]) or equivalently avi_load(50)
Another use of ``load_func`` would be to convert all images to ``uint8``::
def imread_convert(f):
return imread(f).astype(np.uint8)
ic = ImageCollection('/tmp/*.png', load_func=imread_convert)
For files with multiple images, the images will be flattened into a list
and added to the list of available images. In this case, ``load_func``
should accept the keyword argument ``img_num``.
Examples
--------
>>> import skimage.io as io
>>> from skimage import data_dir
>>> coll = io.ImageCollection(data_dir + '/chess*.png')
>>> len(coll)
2
>>> coll[0].shape
(200, 200)
>>> ic = io.ImageCollection('/tmp/work/*.png:/tmp/other/*.jpg')
"""
def __init__(self, load_pattern, conserve_memory=True, load_func=None,
**load_func_kwargs):
"""Load and manage a collection of images."""
if isinstance(load_pattern, six.string_types):
load_pattern = load_pattern.split(os.pathsep)
self._files = []
for pattern in load_pattern:
self._files.extend(glob(pattern))
self._files = sorted(self._files, key=alphanumeric_key)
self._numframes = self._find_images()
else:
self._files = load_pattern
self._numframes = len(self._files)
self._frame_index = None
if conserve_memory:
memory_slots = 1
else:
memory_slots = self._numframes
self._conserve_memory = conserve_memory
self._cached = None
if load_func is None:
from skimage.io._io import imread
self.load_func = imread
else:
self.load_func = load_func
self.load_func_kwargs = load_func_kwargs
# ----- start bbue (Thu Nov 2 17:22:25 2017) ------------------------------
# added shape variable
self.shape = (self._numframes,None)
try:
# grab an image to set the shape variable
_img = self.load_func(self._files[0],**self.load_func_kwargs)
self.shape = tuple([self._numframes]+list(_img.shape))
except:
pass
# ----- end bbue (Thu Nov 2 17:22:25 2017) --------------------------------
self.data = np.empty(memory_slots, dtype=object)
@property
def files(self):
return self._files
@property
def conserve_memory(self):
return self._conserve_memory
def _find_images(self):
index = []
for fname in self._files:
if fname.lower().endswith(('.tiff', '.tif')):
with open(fname, 'rb') as f:
img = TiffFile(f)
index += [(fname, i) for i in range(len(img.pages))]
else:
try:
im = Image.open(fname)
im.seek(0)
except (IOError, OSError):
continue
i = 0
while True:
try:
im.seek(i)
except EOFError:
break
index.append((fname, i))
i += 1
if hasattr(im, 'fp') and im.fp:
im.fp.close()
self._frame_index = index
return len(index)
def __getitem__(self, n):
"""Return selected image(s) in the collection.
Loading is done on demand.
Parameters
----------
n : int or slice
The image number to be returned, or a slice selecting the images
and ordering to be returned in a new ImageCollection.
Returns
-------
img : ndarray or ImageCollection.
The `n`-th image in the collection, or a new ImageCollection with
the selected images.
"""
if hasattr(n, '__index__'):
n = n.__index__()
if hasattr(n, '__len__'):
if hasattr(n, 'shape'):
n = n.tolist()
if type(n) not in [int, slice, list]:
raise TypeError('slicing must be with an int or slice object')
if type(n) is int:
n = self._check_imgnum(n)
idx = n % len(self.data)
if ((self.conserve_memory and n != self._cached) or
(self.data[idx] is None)):
kwargs = self.load_func_kwargs
if self._frame_index:
fname, img_num = self._frame_index[n]
if img_num is not None:
kwargs['img_num'] = img_num
try:
self.data[idx] = self.load_func(fname, **kwargs)
# Account for functions that do not accept an img_num kwarg
except TypeError as e:
if "unexpected keyword argument 'img_num'" in str(e):
del kwargs['img_num']
self.data[idx] = self.load_func(fname, **kwargs)
else:
raise
else:
self.data[idx] = self.load_func(self.files[n], **kwargs)
self._cached = n
return self.data[idx]
else:
# A slice object was provided, so create a new ImageCollection
# object. Any loaded image data in the original ImageCollection
# will be copied by reference to the new object. Image data
# loaded after this creation is not linked.
if type(n) == slice:
fidx = range(self._numframes)[n]
elif type(n) == list:
fidx = [int(ni) for ni in n]
new_ic = copy(self)
if self._frame_index:
new_ic._files = [self._frame_index[i][0] for i in fidx]
new_ic._frame_index = [self._frame_index[i] for i in fidx]
else:
new_ic._files = [self._files[i] for i in fidx]
new_ic._numframes = len(fidx)
if self.conserve_memory:
if self._cached in fidx:
new_ic._cached = fidx.index(self._cached)
new_ic.data = np.copy(self.data)
else:
new_ic.data = np.empty(1, dtype=object)
else:
new_ic.data = self.data[fidx]
# ----- start bbue (Thu Nov 2 17:22:25 2017) ------------------------------
# added shape variable
new_ic.shape = tuple([new_ic._numframes]+list(self.shape[1:]))
# ----- end bbue (Thu Nov 2 17:22:25 2017) ------------------------------
return new_ic
def _check_imgnum(self, n):
"""Check that the given image number is valid."""
num = self._numframes
if -num <= n < num:
n = n % num
else:
raise IndexError("There are only %s images in the collection"
% num)
return n
def __iter__(self):
"""Iterate over the images."""
for i in range(len(self)):
yield self[i]
def __len__(self):
"""Number of images in collection."""
return self._numframes
def __str__(self):
return str(self.files)
def reload(self, n=None):
"""Clear the image cache.
Parameters
----------
n : None or int
Clear the cache for this image only. By default, the
entire cache is erased.
"""
self.data = np.empty_like(self.data)
def concatenate(self):
"""Concatenate all images in the collection into an array.
Returns
-------
ar : np.ndarray
An array having one more dimension than the images in `self`.
See Also
--------
concatenate_images
Raises
------
ValueError
If images in the `ImageCollection` don't have identical shapes.
"""
return concatenate_images(self)
def imread_collection_wrapper(imread):
def imread_collection(load_pattern, conserve_memory=True):
"""Return an `ImageCollection` from files matching the given pattern.
Note that files are always stored in alphabetical order. Also note that
slicing returns a new ImageCollection, *not* a view into the data.
See `skimage.io.ImageCollection` for details.
Parameters
----------
load_pattern : str or list
Pattern glob or filenames to load. The path can be absolute or
relative. Multiple patterns should be separated by a colon,
e.g. '/tmp/work/*.png:/tmp/other/*.jpg'. Also see
implementation notes below.
conserve_memory : bool, optional
If True, never keep more than one in memory at a specific
time. Otherwise, images will be cached once they are loaded.
"""
return ImageCollection(load_pattern, conserve_memory=conserve_memory,
load_func=imread)
return imread_collection
class MultiImage(ImageCollection):
"""A class containing a single multi-frame image.
Parameters
----------
filename : str
The complete path to the image file.
conserve_memory : bool, optional
Whether to conserve memory by only caching a single frame. Default is
True.
Notes
-----
If ``conserve_memory=True`` the memory footprint can be reduced, however
the performance can be affected because frames have to be read from file
more often.
The last accessed frame is cached, all other frames will have to be read
from file.
The current implementation makes use of ``tifffile`` for Tiff files and
PIL otherwise.
Examples
--------
>>> from skimage import data_dir
>>> img = MultiImage(data_dir + '/multipage.tif') # doctest: +SKIP
>>> len(img) # doctest: +SKIP
2
>>> for frame in img: # doctest: +SKIP
... print(frame.shape) # doctest: +SKIP
(15, 10)
(15, 10)
"""
def __init__(self, filename, conserve_memory=True, dtype=None,
**imread_kwargs):
"""Load a multi-img."""
from skimage.io._io import imread
def load_func(fname, **kwargs):
kwargs.setdefault('dtype', dtype)
return imread(fname, **kwargs)
self._filename = filename
super(MultiImage, self).__init__(filename, conserve_memory,
load_func=load_func, **imread_kwargs)
@property
def filename(self):
return self._filename