1# -*- coding: utf-8 -*-
2# imageio is distributed under the terms of the (new) BSD License.
3
4import re
5import warnings
6from numbers import Number
7from pathlib import Path
8from typing import Dict
9
10import numpy as np
11
12from imageio.core.legacy_plugin_wrapper import LegacyPlugin
13from imageio.core.util import Array
14from imageio.core.v3_plugin_api import PluginV3
15
16from . import formats
17from .config import known_extensions, known_plugins
18from .core import RETURN_BYTES
19from .core.imopen import imopen
20
21MEMTEST_DEFAULT_MIM = "256MB"
22MEMTEST_DEFAULT_MVOL = "1GB"
23
24
25mem_re = re.compile(r"^(\d+\.?\d*)\s*([kKMGTPEZY]?i?)B?$")
26sizes = {"": 1, None: 1}
27for i, si in enumerate([""] + list("kMGTPEZY")):
28 sizes[si] = 1000**i
29 if si:
30 sizes[si.upper() + "i"] = 1024**i
31
32
33def to_nbytes(arg, default=None):
34 if not arg:
35 arg = float("inf")
36
37 if arg is True:
38 arg = default
39
40 if isinstance(arg, Number):
41 return arg
42
43 match = mem_re.match(arg)
44 if match is None:
45 raise ValueError(
46 "Memory size could not be parsed "
47 "(is your capitalisation correct?): {}".format(arg)
48 )
49
50 num, unit = match.groups()
51
52 try:
53 return float(num) * sizes[unit]
54 except KeyError: # pragma: no cover
55 # Note: I don't think we can reach this
56 raise ValueError(
57 "Memory size unit not recognised "
58 "(is your capitalisation correct?): {}".format(unit)
59 )
60
61
62def help(name=None):
63 """help(name=None)
64
65 Print the documentation of the format specified by name, or a list
66 of supported formats if name is omitted.
67
68 Parameters
69 ----------
70 name : str
71 Can be the name of a format, a filename extension, or a full
72 filename. See also the :doc:`formats page <../formats/index>`.
73 """
74 if not name:
75 print(formats)
76 else:
77 print(formats[name])
78
79
80def decypher_format_arg(format_name: str) -> Dict[str, str]:
81 """Split format into plugin and format
82
83 The V2 API aliases plugins and supported formats. This function
84 splits these so that they can be fed separately to `iio.imopen`.
85
86 """
87
88 plugin = None
89 extension = None
90
91 if format_name is None:
92 pass # nothing to do
93 elif Path(format_name).suffix.lower() in known_extensions:
94 extension = Path(format_name).suffix.lower()
95 elif format_name in known_plugins:
96 plugin = format_name
97 elif format_name.upper() in known_plugins:
98 plugin = format_name.upper()
99 elif format_name.lower() in known_extensions:
100 extension = format_name.lower()
101 elif "." + format_name.lower() in known_extensions:
102 extension = "." + format_name.lower()
103 else:
104 raise IndexError(f"No format known by name `{plugin}`.")
105
106 return {"plugin": plugin, "extension": extension}
107
108
109class LegacyReader:
110 def __init__(self, plugin_instance: PluginV3, **kwargs):
111 self.instance = plugin_instance
112 self.last_index = 0
113 self.closed = False
114
115 if (
116 type(self.instance).__name__ == "PillowPlugin"
117 and kwargs.get("pilmode") is not None
118 ):
119 kwargs["mode"] = kwargs["pilmode"]
120 del kwargs["pilmode"]
121
122 self.read_args = kwargs
123
124 def close(self):
125 if not self.closed:
126 self.instance.close()
127 self.closed = True
128
129 def __enter__(self):
130 return self
131
132 def __exit__(self, type, value, traceback):
133 self.close()
134
135 def __del__(self):
136 self.close()
137
138 @property
139 def request(self):
140 return self.instance.request
141
142 @property
143 def format(self):
144 raise TypeError("V3 Plugins don't have a format.")
145
146 def get_length(self):
147 return self.instance.properties(index=...).n_images
148
149 def get_data(self, index):
150 self.last_index = index
151 img = self.instance.read(index=index, **self.read_args)
152 metadata = self.instance.metadata(index=index, exclude_applied=False)
153 return Array(img, metadata)
154
155 def get_next_data(self):
156 return self.get_data(self.last_index + 1)
157
158 def set_image_index(self, index):
159 self.last_index = index - 1
160
161 def get_meta_data(self, index=None):
162 return self.instance.metadata(index=index, exclude_applied=False)
163
164 def iter_data(self):
165 for idx, img in enumerate(self.instance.iter()):
166 metadata = self.instance.metadata(index=idx, exclude_applied=False)
167 yield Array(img, metadata)
168
169 def __iter__(self):
170 return self.iter_data()
171
172 def __len__(self):
173 return self.get_length()
174
175
176class LegacyWriter:
177 def __init__(self, plugin_instance: PluginV3, **kwargs):
178 self.instance = plugin_instance
179 self.last_index = 0
180 self.closed = False
181
182 if type(self.instance).__name__ == "PillowPlugin" and "pilmode" in kwargs:
183 kwargs["mode"] = kwargs["pilmode"]
184 del kwargs["pilmode"]
185
186 self.write_args = kwargs
187
188 def close(self):
189 if not self.closed:
190 self.instance.close()
191 self.closed = True
192
193 def __enter__(self):
194 return self
195
196 def __exit__(self, type, value, traceback):
197 self.close()
198
199 def __del__(self):
200 self.close()
201
202 @property
203 def request(self):
204 return self.instance.request
205
206 @property
207 def format(self):
208 raise TypeError("V3 Plugins don't have a format.")
209
210 def append_data(self, im, meta=None):
211 # TODO: write metadata in the future; there is currently no
212 # generic way to do this with v3 plugins :(
213 if meta is not None:
214 warnings.warn(
215 "V3 Plugins currently don't have a uniform way to"
216 " write metadata, so any metadata is ignored."
217 )
218
219 # total_meta = dict()
220 # if meta is None:
221 # meta = {}
222 # if hasattr(im, "meta") and isinstance(im.meta, dict):
223 # total_meta.update(im.meta)
224 # total_meta.update(meta)
225
226 return self.instance.write(im, **self.write_args)
227
228 def set_meta_data(self, meta):
229 # TODO: write metadata
230 raise NotImplementedError(
231 "V3 Plugins don't have a uniform way to write metadata (yet)."
232 )
233
234
235def is_batch(ndimage):
236 if isinstance(ndimage, (list, tuple)):
237 return True
238
239 ndimage = np.asarray(ndimage)
240 if ndimage.ndim <= 2:
241 return False
242 elif ndimage.ndim == 3 and ndimage.shape[2] < 5:
243 return False
244
245 return True
246
247
248def is_volume(ndimage):
249 ndimage = np.asarray(ndimage)
250 if not is_batch(ndimage):
251 return False
252
253 if ndimage.ndim == 3 and ndimage.shape[2] >= 5:
254 return True
255 elif ndimage.ndim == 4 and ndimage.shape[3] < 5:
256 return True
257 else:
258 return False
259
260
261# Base functions that return a reader/writer
262
263
264def get_reader(uri, format=None, mode="?", **kwargs):
265 """get_reader(uri, format=None, mode='?', **kwargs)
266
267 Returns a :class:`.Reader` object which can be used to read data
268 and meta data from the specified file.
269
270 Parameters
271 ----------
272 uri : {str, pathlib.Path, bytes, file}
273 The resource to load the image from, e.g. a filename, pathlib.Path,
274 http address or file object, see the docs for more info.
275 format : str
276 The format to use to read the file. By default imageio selects
277 the appropriate for you based on the filename and its contents.
278 mode : {'i', 'I', 'v', 'V', '?'}
279 Used to give the reader a hint on what the user expects (default "?"):
280 "i" for an image, "I" for multiple images, "v" for a volume,
281 "V" for multiple volumes, "?" for don't care.
282 kwargs : ...
283 Further keyword arguments are passed to the reader. See :func:`.help`
284 to see what arguments are available for a particular format.
285 """
286
287 imopen_args = decypher_format_arg(format)
288 imopen_args["legacy_mode"] = True
289
290 image_file = imopen(uri, "r" + mode, **imopen_args)
291
292 if isinstance(image_file, LegacyPlugin):
293 return image_file.legacy_get_reader(**kwargs)
294 else:
295 return LegacyReader(image_file, **kwargs)
296
297
298def get_writer(uri, format=None, mode="?", **kwargs):
299 """get_writer(uri, format=None, mode='?', **kwargs)
300
301 Returns a :class:`.Writer` object which can be used to write data
302 and meta data to the specified file.
303
304 Parameters
305 ----------
306 uri : {str, pathlib.Path, file}
307 The resource to write the image to, e.g. a filename, pathlib.Path
308 or file object, see the docs for more info.
309 format : str
310 The format to use to write the file. By default imageio selects
311 the appropriate for you based on the filename.
312 mode : {'i', 'I', 'v', 'V', '?'}
313 Used to give the writer a hint on what the user expects (default '?'):
314 "i" for an image, "I" for multiple images, "v" for a volume,
315 "V" for multiple volumes, "?" for don't care.
316 kwargs : ...
317 Further keyword arguments are passed to the writer. See :func:`.help`
318 to see what arguments are available for a particular format.
319 """
320
321 imopen_args = decypher_format_arg(format)
322 imopen_args["legacy_mode"] = True
323
324 image_file = imopen(uri, "w" + mode, **imopen_args)
325 if isinstance(image_file, LegacyPlugin):
326 return image_file.legacy_get_writer(**kwargs)
327 else:
328 return LegacyWriter(image_file, **kwargs)
329
330
331# Images
332
333
334def imread(uri, format=None, **kwargs):
335 """imread(uri, format=None, **kwargs)
336
337 Reads an image from the specified file. Returns a numpy array, which
338 comes with a dict of meta data at its 'meta' attribute.
339
340 Note that the image data is returned as-is, and may not always have
341 a dtype of uint8 (and thus may differ from what e.g. PIL returns).
342
343 Parameters
344 ----------
345 uri : {str, pathlib.Path, bytes, file}
346 The resource to load the image from, e.g. a filename, pathlib.Path,
347 http address or file object, see the docs for more info.
348 format : str
349 The format to use to read the file. By default imageio selects
350 the appropriate for you based on the filename and its contents.
351 kwargs : ...
352 Further keyword arguments are passed to the reader. See :func:`.help`
353 to see what arguments are available for a particular format.
354 """
355
356 imopen_args = decypher_format_arg(format)
357 imopen_args["legacy_mode"] = True
358
359 with imopen(uri, "ri", **imopen_args) as file:
360 result = file.read(index=0, **kwargs)
361
362 return result
363
364
365def imwrite(uri, im, format=None, **kwargs):
366 """imwrite(uri, im, format=None, **kwargs)
367
368 Write an image to the specified file.
369
370 Parameters
371 ----------
372 uri : {str, pathlib.Path, file}
373 The resource to write the image to, e.g. a filename, pathlib.Path
374 or file object, see the docs for more info.
375 im : numpy.ndarray
376 The image data. Must be NxM, NxMx3 or NxMx4.
377 format : str
378 The format to use to write the file. By default imageio selects
379 the appropriate for you based on the filename and its contents.
380 kwargs : ...
381 Further keyword arguments are passed to the writer. See :func:`.help`
382 to see what arguments are available for a particular format.
383 """
384
385 # Test image
386 imt = type(im)
387 im = np.asarray(im)
388 if not np.issubdtype(im.dtype, np.number):
389 raise ValueError("Image is not numeric, but {}.".format(imt.__name__))
390
391 if is_batch(im) or im.ndim < 2:
392 raise ValueError("Image must be 2D (grayscale, RGB, or RGBA).")
393
394 imopen_args = decypher_format_arg(format)
395 imopen_args["legacy_mode"] = True
396 with imopen(uri, "wi", **imopen_args) as file:
397 return file.write(im, **kwargs)
398
399
400# Multiple images
401
402
403def mimread(uri, format=None, memtest=MEMTEST_DEFAULT_MIM, **kwargs):
404 """mimread(uri, format=None, memtest="256MB", **kwargs)
405
406 Reads multiple images from the specified file. Returns a list of
407 numpy arrays, each with a dict of meta data at its 'meta' attribute.
408
409 Parameters
410 ----------
411 uri : {str, pathlib.Path, bytes, file}
412 The resource to load the images from, e.g. a filename,pathlib.Path,
413 http address or file object, see the docs for more info.
414 format : str
415 The format to use to read the file. By default imageio selects
416 the appropriate for you based on the filename and its contents.
417 memtest : {bool, int, float, str}
418 If truthy, this function will raise an error if the resulting
419 list of images consumes greater than the amount of memory specified.
420 This is to protect the system from using so much memory that it needs
421 to resort to swapping, and thereby stall the computer. E.g.
422 ``mimread('hunger_games.avi')``.
423
424 If the argument is a number, that will be used as the threshold number
425 of bytes.
426
427 If the argument is a string, it will be interpreted as a number of bytes with
428 SI/IEC prefixed units (e.g. '1kB', '250MiB', '80.3YB').
429
430 - Units are case sensitive
431 - k, M etc. represent a 1000-fold change, where Ki, Mi etc. represent 1024-fold
432 - The "B" is optional, but if present, must be capitalised
433
434 If the argument is True, the default will be used, for compatibility reasons.
435
436 Default: '256MB'
437 kwargs : ...
438 Further keyword arguments are passed to the reader. See :func:`.help`
439 to see what arguments are available for a particular format.
440 """
441
442 # used for mimread and mvolread
443 nbyte_limit = to_nbytes(memtest, MEMTEST_DEFAULT_MIM)
444
445 images = list()
446 nbytes = 0
447
448 imopen_args = decypher_format_arg(format)
449 imopen_args["legacy_mode"] = True
450 with imopen(uri, "rI", **imopen_args) as file:
451 for image in file.iter(**kwargs):
452 images.append(image)
453 nbytes += image.nbytes
454 if nbytes > nbyte_limit:
455 raise RuntimeError(
456 "imageio.mimread() has read over {}B of "
457 "image data.\nStopped to avoid memory problems."
458 " Use imageio.get_reader(), increase threshold, or memtest=False".format(
459 int(nbyte_limit)
460 )
461 )
462
463 if len(images) == 1 and is_batch(images[0]):
464 images = [*images[0]]
465
466 return images
467
468
469def mimwrite(uri, ims, format=None, **kwargs):
470 """mimwrite(uri, ims, format=None, **kwargs)
471
472 Write multiple images to the specified file.
473
474 Parameters
475 ----------
476 uri : {str, pathlib.Path, file}
477 The resource to write the images to, e.g. a filename, pathlib.Path
478 or file object, see the docs for more info.
479 ims : sequence of numpy arrays
480 The image data. Each array must be NxM, NxMx3 or NxMx4.
481 format : str
482 The format to use to read the file. By default imageio selects
483 the appropriate for you based on the filename and its contents.
484 kwargs : ...
485 Further keyword arguments are passed to the writer. See :func:`.help`
486 to see what arguments are available for a particular format.
487 """
488
489 if not is_batch(ims):
490 raise ValueError("Image data must be a sequence of ndimages.")
491
492 imopen_args = decypher_format_arg(format)
493 imopen_args["legacy_mode"] = True
494 with imopen(uri, "wI", **imopen_args) as file:
495 return file.write(ims, is_batch=True, **kwargs)
496
497
498# Volumes
499
500
501def volread(uri, format=None, **kwargs):
502 """volread(uri, format=None, **kwargs)
503
504 Reads a volume from the specified file. Returns a numpy array, which
505 comes with a dict of meta data at its 'meta' attribute.
506
507 Parameters
508 ----------
509 uri : {str, pathlib.Path, bytes, file}
510 The resource to load the volume from, e.g. a filename, pathlib.Path,
511 http address or file object, see the docs for more info.
512 format : str
513 The format to use to read the file. By default imageio selects
514 the appropriate for you based on the filename and its contents.
515 kwargs : ...
516 Further keyword arguments are passed to the reader. See :func:`.help`
517 to see what arguments are available for a particular format.
518 """
519
520 imopen_args = decypher_format_arg(format)
521 imopen_args["legacy_mode"] = True
522 with imopen(uri, "rv", **imopen_args) as file:
523 return file.read(index=0, **kwargs)
524
525
526def volwrite(uri, im, format=None, **kwargs):
527 """volwrite(uri, vol, format=None, **kwargs)
528
529 Write a volume to the specified file.
530
531 Parameters
532 ----------
533 uri : {str, pathlib.Path, file}
534 The resource to write the image to, e.g. a filename, pathlib.Path
535 or file object, see the docs for more info.
536 vol : numpy.ndarray
537 The image data. Must be NxMxL (or NxMxLxK if each voxel is a tuple).
538 format : str
539 The format to use to read the file. By default imageio selects
540 the appropriate for you based on the filename and its contents.
541 kwargs : ...
542 Further keyword arguments are passed to the writer. See :func:`.help`
543 to see what arguments are available for a particular format.
544 """
545
546 # Test image
547 im = np.asarray(im)
548 if not is_volume(im):
549 raise ValueError("Image must be 3D, or 4D if each voxel is a tuple.")
550
551 imopen_args = decypher_format_arg(format)
552 imopen_args["legacy_mode"] = True
553
554 with imopen(uri, "wv", **imopen_args) as file:
555 return file.write(im, is_batch=False, **kwargs)
556
557
558# Multiple volumes
559
560
561def mvolread(uri, format=None, memtest=MEMTEST_DEFAULT_MVOL, **kwargs):
562 """mvolread(uri, format=None, memtest='1GB', **kwargs)
563
564 Reads multiple volumes from the specified file. Returns a list of
565 numpy arrays, each with a dict of meta data at its 'meta' attribute.
566
567 Parameters
568 ----------
569 uri : {str, pathlib.Path, bytes, file}
570 The resource to load the volumes from, e.g. a filename, pathlib.Path,
571 http address or file object, see the docs for more info.
572 format : str
573 The format to use to read the file. By default imageio selects
574 the appropriate for you based on the filename and its contents.
575 memtest : {bool, int, float, str}
576 If truthy, this function will raise an error if the resulting
577 list of images consumes greater than the amount of memory specified.
578 This is to protect the system from using so much memory that it needs
579 to resort to swapping, and thereby stall the computer. E.g.
580 ``mimread('hunger_games.avi')``.
581
582 If the argument is a number, that will be used as the threshold number
583 of bytes.
584
585 If the argument is a string, it will be interpreted as a number of bytes with
586 SI/IEC prefixed units (e.g. '1kB', '250MiB', '80.3YB').
587
588 - Units are case sensitive
589 - k, M etc. represent a 1000-fold change, where Ki, Mi etc. represent 1024-fold
590 - The "B" is optional, but if present, must be capitalised
591
592 If the argument is True, the default will be used, for compatibility reasons.
593
594 Default: '1GB'
595 kwargs : ...
596 Further keyword arguments are passed to the reader. See :func:`.help`
597 to see what arguments are available for a particular format.
598 """
599
600 # used for mimread and mvolread
601 nbyte_limit = to_nbytes(memtest, MEMTEST_DEFAULT_MVOL)
602
603 images = list()
604 nbytes = 0
605 imopen_args = decypher_format_arg(format)
606 imopen_args["legacy_mode"] = True
607 with imopen(uri, "rV", **imopen_args) as file:
608 for image in file.iter(**kwargs):
609 images.append(image)
610 nbytes += image.nbytes
611 if nbytes > nbyte_limit:
612 raise RuntimeError(
613 "imageio.mimread() has read over {}B of "
614 "image data.\nStopped to avoid memory problems."
615 " Use imageio.get_reader(), increase threshold, or memtest=False".format(
616 int(nbyte_limit)
617 )
618 )
619
620 return images
621
622
623def mvolwrite(uri, ims, format=None, **kwargs):
624 """mvolwrite(uri, vols, format=None, **kwargs)
625
626 Write multiple volumes to the specified file.
627
628 Parameters
629 ----------
630 uri : {str, pathlib.Path, file}
631 The resource to write the volumes to, e.g. a filename, pathlib.Path
632 or file object, see the docs for more info.
633 ims : sequence of numpy arrays
634 The image data. Each array must be NxMxL (or NxMxLxK if each
635 voxel is a tuple).
636 format : str
637 The format to use to read the file. By default imageio selects
638 the appropriate for you based on the filename and its contents.
639 kwargs : ...
640 Further keyword arguments are passed to the writer. See :func:`.help`
641 to see what arguments are available for a particular format.
642 """
643
644 for im in ims:
645 if not is_volume(im):
646 raise ValueError("Image must be 3D, or 4D if each voxel is a tuple.")
647
648 imopen_args = decypher_format_arg(format)
649 imopen_args["legacy_mode"] = True
650 with imopen(uri, "wV", **imopen_args) as file:
651 return file.write(ims, is_batch=True, **kwargs)
652
653
654# aliases
655read = get_reader
656save = get_writer
657imsave = imwrite
658mimsave = mimwrite
659volsave = volwrite
660mvolsave = mvolwrite
661
662__all__ = [
663 "imread",
664 "mimread",
665 "volread",
666 "mvolread",
667 "imwrite",
668 "mimwrite",
669 "volwrite",
670 "mvolwrite",
671 # misc
672 "help",
673 "get_reader",
674 "get_writer",
675 "RETURN_BYTES",
676]