1#! /usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tifffile.py
4
5# Copyright (c) 2008-2018, Christoph Gohlke
6# Copyright (c) 2008-2018, The Regents of the University of California
7# Produced at the Laboratory for Fluorescence Dynamics
8# All rights reserved.
9#
10# Redistribution and use in source and binary forms, with or without
11# modification, are permitted provided that the following conditions are met:
12#
13# * Redistributions of source code must retain the above copyright
14# notice, this list of conditions and the following disclaimer.
15# * Redistributions in binary form must reproduce the above copyright
16# notice, this list of conditions and the following disclaimer in the
17# documentation and/or other materials provided with the distribution.
18# * Neither the name of the copyright holders nor the names of any
19# contributors may be used to endorse or promote products derived
20# from this software without specific prior written permission.
21#
22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32# POSSIBILITY OF SUCH DAMAGE.
33
34"""Read image and meta data from (bio) TIFF(R) files. Save numpy arrays as TIFF.
35
36Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, STK, LSM, NIH,
37SGI, ImageJ, MicroManager, FluoView, ScanImage, SEQ, GEL, and GeoTIFF files.
38
39Tifffile is not a general-purpose TIFF library.
40Only a subset of the TIFF specification is supported, mainly uncompressed and
41losslessly compressed 1, 8, 16, 32 and 64 bit integer, 16, 32 and 64-bit float,
42grayscale and RGB(A) images, which are commonly used in scientific imaging.
43Specifically, reading slices of image data, image trees defined via SubIFDs,
44CCITT and OJPEG compression, chroma subsampling without JPEG compression,
45or IPTC and XMP metadata are not implemented.
46
47TIFF(R), the tagged Image File Format, is a trademark and under control of
48Adobe Systems Incorporated. BigTIFF allows for files greater than 4 GB.
49STK, LSM, FluoView, SGI, SEQ, GEL, and OME-TIFF, are custom extensions
50defined by Molecular Devices (Universal Imaging Corporation), Carl Zeiss
51MicroImaging, Olympus, Silicon Graphics International, Media Cybernetics,
52Molecular Dynamics, and the Open Microscopy Environment consortium
53respectively.
54
55For command line usage run C{python -m tifffile --help}
56
57:Author:
58 `Christoph Gohlke <https://www.lfd.uci.edu/~gohlke/>`_
59
60:Organization:
61 Laboratory for Fluorescence Dynamics, University of California, Irvine
62
63:Version: 2018.06.15
64
65Requirements
66------------
67* `CPython 3.6 64-bit <https://www.python.org>`_
68* `Numpy 1.14 <http://www.numpy.org>`_
69* `Matplotlib 2.2 <https://www.matplotlib.org>`_ (optional for plotting)
70* `Tifffile.c 2018.02.10 <https://www.lfd.uci.edu/~gohlke/>`_
71 (recommended for faster decoding of PackBits and LZW encoded strings)
72* `Tifffile_geodb.py 2018.02.10 <https://www.lfd.uci.edu/~gohlke/>`_
73 (optional enums for GeoTIFF metadata)
74* Python 2 requires 'futures', 'enum34', 'pathlib'.
75
76Revisions
77---------
782018.06.15
79 Pass 2680 tests.
80 Towards reading JPEG and other compressions via imagecodecs package (WIP).
81 Add function to validate TIFF using 'jhove -m TIFF-hul'.
82 Save bool arrays as bilevel TIFF.
83 Accept pathlib.Path as filenames.
84 Move 'software' argument from TiffWriter __init__ to save.
85 Raise DOS limit to 16 TB.
86 Lazy load lzma and zstd compressors and decompressors.
87 Add option to save IJMetadata tags.
88 Return correct number of pages for truncated series (bug fix).
89 Move EXIF tags to TIFF.TAG as per TIFF/EP standard.
902018.02.18
91 Pass 2293 tests.
92 Always save RowsPerStrip and Resolution tags as required by TIFF standard.
93 Do not use badly typed ImageDescription.
94 Coherce bad ASCII string tags to bytes.
95 Tuning of __str__ functions.
96 Fix reading 'undefined' tag values (bug fix).
97 Read and write ZSTD compressed data.
98 Use hexdump to print byte strings.
99 Determine TIFF byte order from data dtype in imsave.
100 Add option to specify RowsPerStrip for compressed strips.
101 Allow memory map of arrays with non-native byte order.
102 Attempt to handle ScanImage <= 5.1 files.
103 Restore TiffPageSeries.pages sequence interface.
104 Use numpy.frombuffer instead of fromstring to read from binary data.
105 Parse GeoTIFF metadata.
106 Add option to apply horizontal differencing before compression.
107 Towards reading PerkinElmer QPTIFF (no test files).
108 Do not index out of bounds data in tifffile.c unpackbits and decodelzw.
1092017.09.29 (tentative)
110 Many backwards incompatible changes improving speed and resource usage:
111 Pass 2268 tests.
112 Add detail argument to __str__ function. Remove info functions.
113 Fix potential issue correcting offsets of large LSM files with positions.
114 Remove TiffFile sequence interface; use TiffFile.pages instead.
115 Do not make tag values available as TiffPage attributes.
116 Use str (not bytes) type for tag and metadata strings (WIP).
117 Use documented standard tag and value names (WIP).
118 Use enums for some documented TIFF tag values.
119 Remove 'memmap' and 'tmpfile' options; use out='memmap' instead.
120 Add option to specify output in asarray functions.
121 Add option to concurrently decode image strips or tiles using threads.
122 Add TiffPage.asrgb function (WIP).
123 Do not apply colormap in asarray.
124 Remove 'colormapped', 'rgbonly', and 'scale_mdgel' options from asarray.
125 Consolidate metadata in TiffFile _metadata functions.
126 Remove non-tag metadata properties from TiffPage.
127 Add function to convert LSM to tiled BIN files.
128 Align image data in file.
129 Make TiffPage.dtype a numpy.dtype.
130 Add 'ndim' and 'size' properties to TiffPage and TiffPageSeries.
131 Allow imsave to write non-BigTIFF files up to ~4 GB.
132 Only read one page for shaped series if possible.
133 Add memmap function to create memory-mapped array stored in TIFF file.
134 Add option to save empty arrays to TIFF files.
135 Add option to save truncated TIFF files.
136 Allow single tile images to be saved contiguously.
137 Add optional movie mode for files with uniform pages.
138 Lazy load pages.
139 Use lightweight TiffFrame for IFDs sharing properties with key TiffPage.
140 Move module constants to 'TIFF' namespace (speed up module import).
141 Remove 'fastij' option from TiffFile.
142 Remove 'pages' parameter from TiffFile.
143 Remove TIFFfile alias.
144 Deprecate Python 2.
145 Require enum34 and futures packages on Python 2.7.
146 Remove Record class and return all metadata as dict instead.
147 Add functions to parse STK, MetaSeries, ScanImage, SVS, Pilatus metadata.
148 Read tags from EXIF and GPS IFDs.
149 Use pformat for tag and metadata values.
150 Fix reading some UIC tags (bug fix).
151 Do not modify input array in imshow (bug fix).
152 Fix Python implementation of unpack_ints.
1532017.05.23
154 Pass 1961 tests.
155 Write correct number of SampleFormat values (bug fix).
156 Use Adobe deflate code to write ZIP compressed files.
157 Add option to pass tag values as packed binary data for writing.
158 Defer tag validation to attribute access.
159 Use property instead of lazyattr decorator for simple expressions.
1602017.03.17
161 Write IFDs and tag values on word boundaries.
162 Read ScanImage metadata.
163 Remove is_rgb and is_indexed attributes from TiffFile.
164 Create files used by doctests.
1652017.01.12
166 Read Zeiss SEM metadata.
167 Read OME-TIFF with invalid references to external files.
168 Rewrite C LZW decoder (5x faster).
169 Read corrupted LSM files missing EOI code in LZW stream.
1702017.01.01
171 Add option to append images to existing TIFF files.
172 Read files without pages.
173 Read S-FEG and Helios NanoLab tags created by FEI software.
174 Allow saving Color Filter Array (CFA) images.
175 Add info functions returning more information about TiffFile and TiffPage.
176 Add option to read specific pages only.
177 Remove maxpages argument (backwards incompatible).
178 Remove test_tifffile function.
1792016.10.28
180 Pass 1944 tests.
181 Improve detection of ImageJ hyperstacks.
182 Read TVIPS metadata created by EM-MENU (by Marco Oster).
183 Add option to disable using OME-XML metadata.
184 Allow non-integer range attributes in modulo tags (by Stuart Berg).
1852016.06.21
186 Do not always memmap contiguous data in page series.
1872016.05.13
188 Add option to specify resolution unit.
189 Write grayscale images with extra samples when planarconfig is specified.
190 Do not write RGB color images with 2 samples.
191 Reorder TiffWriter.save keyword arguments (backwards incompatible).
1922016.04.18
193 Pass 1932 tests.
194 TiffWriter, imread, and imsave accept open binary file streams.
1952016.04.13
196 Correctly handle reversed fill order in 2 and 4 bps images (bug fix).
197 Implement reverse_bitorder in C.
1982016.03.18
199 Fix saving additional ImageJ metadata.
2002016.02.22
201 Pass 1920 tests.
202 Write 8 bytes double tag values using offset if necessary (bug fix).
203 Add option to disable writing second image description tag.
204 Detect tags with incorrect counts.
205 Disable color mapping for LSM.
2062015.11.13
207 Read LSM 6 mosaics.
208 Add option to specify directory of memory-mapped files.
209 Add command line options to specify vmin and vmax values for colormapping.
2102015.10.06
211 New helper function to apply colormaps.
212 Renamed is_palette attributes to is_indexed (backwards incompatible).
213 Color-mapped samples are now contiguous (backwards incompatible).
214 Do not color-map ImageJ hyperstacks (backwards incompatible).
215 Towards reading Leica SCN.
2162015.09.25
217 Read images with reversed bit order (FillOrder is LSB2MSB).
2182015.09.21
219 Read RGB OME-TIFF.
220 Warn about malformed OME-XML.
2212015.09.16
222 Detect some corrupted ImageJ metadata.
223 Better axes labels for 'shaped' files.
224 Do not create TiffTag for default values.
225 Chroma subsampling is not supported.
226 Memory-map data in TiffPageSeries if possible (optional).
2272015.08.17
228 Pass 1906 tests.
229 Write ImageJ hyperstacks (optional).
230 Read and write LZMA compressed data.
231 Specify datetime when saving (optional).
232 Save tiled and color-mapped images (optional).
233 Ignore void bytecounts and offsets if possible.
234 Ignore bogus image_depth tag created by ISS Vista software.
235 Decode floating point horizontal differencing (not tiled).
236 Save image data contiguously if possible.
237 Only read first IFD from ImageJ files if possible.
238 Read ImageJ 'raw' format (files larger than 4 GB).
239 TiffPageSeries class for pages with compatible shape and data type.
240 Try to read incomplete tiles.
241 Open file dialog if no filename is passed on command line.
242 Ignore errors when decoding OME-XML.
243 Rename decoder functions (backwards incompatible).
2442014.08.24
245 TiffWriter class for incremental writing images.
246 Simplify examples.
2472014.08.19
248 Add memmap function to FileHandle.
249 Add function to determine if image data in TiffPage is memory-mappable.
250 Do not close files if multifile_close parameter is False.
2512014.08.10
252 Pass 1730 tests.
253 Return all extrasamples by default (backwards incompatible).
254 Read data from series of pages into memory-mapped array (optional).
255 Squeeze OME dimensions (backwards incompatible).
256 Workaround missing EOI code in strips.
257 Support image and tile depth tags (SGI extension).
258 Better handling of STK/UIC tags (backwards incompatible).
259 Disable color mapping for STK.
260 Julian to datetime converter.
261 TIFF ASCII type may be NULL separated.
262 Unwrap strip offsets for LSM files greater than 4 GB.
263 Correct strip byte counts in compressed LSM files.
264 Skip missing files in OME series.
265 Read embedded TIFF files.
2662014.02.05
267 Save rational numbers as type 5 (bug fix).
2682013.12.20
269 Keep other files in OME multi-file series closed.
270 FileHandle class to abstract binary file handle.
271 Disable color mapping for bad OME-TIFF produced by bio-formats.
272 Read bad OME-XML produced by ImageJ when cropping.
2732013.11.03
274 Allow zlib compress data in imsave function (optional).
275 Memory-map contiguous image data (optional).
2762013.10.28
277 Read MicroManager metadata and little-endian ImageJ tag.
278 Save extra tags in imsave function.
279 Save tags in ascending order by code (bug fix).
2802012.10.18
281 Accept file like objects (read from OIB files).
2822012.08.21
283 Rename TIFFfile to TiffFile and TIFFpage to TiffPage.
284 TiffSequence class for reading sequence of TIFF files.
285 Read UltraQuant tags.
286 Allow float numbers as resolution in imsave function.
2872012.08.03
288 Read MD GEL tags and NIH Image header.
2892012.07.25
290 Read ImageJ tags.
291 ...
292
293Notes
294-----
295The API is not stable yet and might change between revisions.
296
297Tested on little-endian platforms only.
298
299Other Python packages and modules for reading (bio) scientific TIFF files:
300
301* `python-bioformats <https://github.com/CellProfiler/python-bioformats>`_
302* `Imread <https://github.com/luispedro/imread>`_
303* `PyLibTiff <https://github.com/pearu/pylibtiff>`_
304* `ITK <https://www.itk.org>`_
305* `PyLSM <https://launchpad.net/pylsm>`_
306* `PyMca.TiffIO.py <https://github.com/vasole/pymca>`_ (same as fabio.TiffIO)
307* `BioImageXD.Readers <http://www.bioimagexd.net/>`_
308* `Cellcognition.io <http://cellcognition.org/>`_
309* `pymimage <https://github.com/ardoi/pymimage>`_
310* `pytiff <https://github.com/FZJ-INM1-BDA/pytiff>`_
311
312Acknowledgements
313----------------
314* Egor Zindy, University of Manchester, for lsm_scan_info specifics.
315* Wim Lewis for a bug fix and some LSM functions.
316* Hadrien Mary for help on reading MicroManager files.
317* Christian Kliche for help writing tiled and color-mapped files.
318
319References
320----------
3211) TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated.
322 http://partners.adobe.com/public/developer/tiff/
3232) TIFF File Format FAQ. http://www.awaresystems.be/imaging/tiff/faq.html
3243) MetaMorph Stack (STK) Image File Format.
325 http://support.meta.moleculardevices.com/docs/t10243.pdf
3264) Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010).
327 Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011
3285) The OME-TIFF format.
329 http://www.openmicroscopy.org/site/support/file-formats/ome-tiff
3306) UltraQuant(r) Version 6.0 for Windows Start-Up Guide.
331 http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf
3327) Micro-Manager File Formats.
333 http://www.micro-manager.org/wiki/Micro-Manager_File_Formats
3348) Tags for TIFF and Related Specifications. Digital Preservation.
335 http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml
3369) ScanImage BigTiff Specification - ScanImage 2016.
337 http://scanimage.vidriotechnologies.com/display/SI2016/
338 ScanImage+BigTiff+Specification
33910) CIPA DC-008-2016: Exchangeable image file format for digital still cameras:
340 Exif Version 2.31.
341 http://www.cipa.jp/std/documents/e/DC-008-Translation-2016-E.pdf
342
343Examples
344--------
345>>> # write numpy array to TIFF file
346>>> data = numpy.random.rand(4, 301, 219)
347>>> imsave('temp.tif', data, photometric='minisblack')
348
349>>> # read numpy array from TIFF file
350>>> image = imread('temp.tif')
351>>> numpy.testing.assert_array_equal(image, data)
352
353>>> # iterate over pages and tags in TIFF file
354>>> with TiffFile('temp.tif') as tif:
355... images = tif.asarray()
356... for page in tif.pages:
357... for tag in page.tags.values():
358... _ = tag.name, tag.value
359... image = page.asarray()
360
361"""
362
363from __future__ import division, print_function
364
365import sys
366import os
367import io
368import re
369import glob
370import math
371import zlib
372import time
373import json
374import enum
375import struct
376import pathlib
377import warnings
378import binascii
379import tempfile
380import datetime
381import threading
382import collections
383import multiprocessing
384import concurrent.futures
385
386import numpy
387
388# delay imports: mmap, pprint, fractions, xml, tkinter, matplotlib, lzma, zstd,
389# subprocess
390
391__version__ = "2018.06.15"
392__docformat__ = "restructuredtext en"
393__all__ = (
394 "imsave",
395 "imread",
396 "imshow",
397 "memmap",
398 "TiffFile",
399 "TiffWriter",
400 "TiffSequence",
401 # utility functions used by oiffile or czifile
402 "FileHandle",
403 "lazyattr",
404 "natural_sorted",
405 "decode_lzw",
406 "stripnull",
407 "create_output",
408 "repeat_nd",
409 "format_size",
410 "product",
411 "xml2dict",
412)
413
414
415def imread(files, **kwargs):
416 """Return image data from TIFF file(s) as numpy array.
417
418 Refer to the TiffFile class and member functions for documentation.
419
420 Parameters
421 ----------
422 files : str, binary stream, or sequence
423 File name, seekable binary stream, glob pattern, or sequence of
424 file names.
425 kwargs : dict
426 Parameters 'multifile' and 'is_ome' are passed to the TiffFile class.
427 The 'pattern' parameter is passed to the TiffSequence class.
428 Other parameters are passed to the asarray functions.
429 The first image series is returned if no arguments are provided.
430
431 Examples
432 --------
433 >>> # get image from first page
434 >>> imsave('temp.tif', numpy.random.rand(3, 4, 301, 219))
435 >>> im = imread('temp.tif', key=0)
436 >>> im.shape
437 (4, 301, 219)
438
439 >>> # get images from sequence of files
440 >>> ims = imread(['temp.tif', 'temp.tif'])
441 >>> ims.shape
442 (2, 3, 4, 301, 219)
443
444 """
445 kwargs_file = parse_kwargs(kwargs, "multifile", "is_ome")
446 kwargs_seq = parse_kwargs(kwargs, "pattern")
447
448 if isinstance(files, basestring) and any(i in files for i in "?*"):
449 files = glob.glob(files)
450 if not files:
451 raise ValueError("no files found")
452 if not hasattr(files, "seek") and len(files) == 1:
453 files = files[0]
454
455 if isinstance(files, basestring) or hasattr(files, "seek"):
456 with TiffFile(files, **kwargs_file) as tif:
457 return tif.asarray(**kwargs)
458 else:
459 with TiffSequence(files, **kwargs_seq) as imseq:
460 return imseq.asarray(**kwargs)
461
462
463def imsave(file, data=None, shape=None, dtype=None, bigsize=2**32 - 2**25, **kwargs):
464 """Write numpy array to TIFF file.
465
466 Refer to the TiffWriter class and member functions for documentation.
467
468 Parameters
469 ----------
470 file : str or binary stream
471 File name or writable binary stream, such as an open file or BytesIO.
472 data : array_like
473 Input image. The last dimensions are assumed to be image depth,
474 height, width, and samples.
475 If None, an empty array of the specified shape and dtype is
476 saved to file.
477 Unless 'byteorder' is specified in 'kwargs', the TIFF file byte order
478 is determined from the data's dtype or the dtype argument.
479 shape : tuple
480 If 'data' is None, shape of an empty array to save to the file.
481 dtype : numpy.dtype
482 If 'data' is None, data-type of an empty array to save to the file.
483 bigsize : int
484 Create a BigTIFF file if the size of data in bytes is larger than
485 this threshold and 'imagej' or 'truncate' are not enabled.
486 By default, the threshold is 4 GB minus 32 MB reserved for metadata.
487 Use the 'bigtiff' parameter to explicitly specify the type of
488 file created.
489 kwargs : dict
490 Parameters 'append', 'byteorder', 'bigtiff', and 'imagej', are passed
491 to TiffWriter(). Other parameters are passed to TiffWriter.save().
492
493 Returns
494 -------
495 If the image data are written contiguously, return offset and bytecount
496 of image data in the file.
497
498 Examples
499 --------
500 >>> # save a RGB image
501 >>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
502 >>> imsave('temp.tif', data, photometric='rgb')
503
504 >>> # save a random array and metadata, using compression
505 >>> data = numpy.random.rand(2, 5, 3, 301, 219)
506 >>> imsave('temp.tif', data, compress=6, metadata={'axes': 'TZCYX'})
507
508 """
509 tifargs = parse_kwargs(kwargs, "append", "bigtiff", "byteorder", "imagej")
510 if data is None:
511 size = product(shape) * numpy.dtype(dtype).itemsize
512 byteorder = numpy.dtype(dtype).byteorder
513 else:
514 try:
515 size = data.nbytes
516 byteorder = data.dtype.byteorder
517 except Exception:
518 size = 0
519 byteorder = None
520 if (
521 size > bigsize
522 and "bigtiff" not in tifargs
523 and not (tifargs.get("imagej", False) or tifargs.get("truncate", False))
524 ):
525 tifargs["bigtiff"] = True
526 if "byteorder" not in tifargs:
527 tifargs["byteorder"] = byteorder
528
529 with TiffWriter(file, **tifargs) as tif:
530 return tif.save(data, shape, dtype, **kwargs)
531
532
533def memmap(filename, shape=None, dtype=None, page=None, series=0, mode="r+", **kwargs):
534 """Return memory-mapped numpy array stored in TIFF file.
535
536 Memory-mapping requires data stored in native byte order, without tiling,
537 compression, predictors, etc.
538 If 'shape' and 'dtype' are provided, existing files will be overwritten or
539 appended to depending on the 'append' parameter.
540 Otherwise the image data of a specified page or series in an existing
541 file will be memory-mapped. By default, the image data of the first page
542 series is memory-mapped.
543 Call flush() to write any changes in the array to the file.
544 Raise ValueError if the image data in the file is not memory-mappable.
545
546 Parameters
547 ----------
548 filename : str
549 Name of the TIFF file which stores the array.
550 shape : tuple
551 Shape of the empty array.
552 dtype : numpy.dtype
553 Data-type of the empty array.
554 page : int
555 Index of the page which image data to memory-map.
556 series : int
557 Index of the page series which image data to memory-map.
558 mode : {'r+', 'r', 'c'}, optional
559 The file open mode. Default is to open existing file for reading and
560 writing ('r+').
561 kwargs : dict
562 Additional parameters passed to imsave() or TiffFile().
563
564 Examples
565 --------
566 >>> # create an empty TIFF file and write to memory-mapped image
567 >>> im = memmap('temp.tif', shape=(256, 256), dtype='float32')
568 >>> im[255, 255] = 1.0
569 >>> im.flush()
570 >>> im.shape, im.dtype
571 ((256, 256), dtype('float32'))
572 >>> del im
573
574 >>> # memory-map image data in a TIFF file
575 >>> im = memmap('temp.tif', page=0)
576 >>> im[255, 255]
577 1.0
578
579 """
580 if shape is not None and dtype is not None:
581 # create a new, empty array
582 kwargs.update(
583 data=None,
584 shape=shape,
585 dtype=dtype,
586 returnoffset=True,
587 align=TIFF.ALLOCATIONGRANULARITY,
588 )
589 result = imsave(filename, **kwargs)
590 if result is None:
591 # TODO: fail before creating file or writing data
592 raise ValueError("image data are not memory-mappable")
593 offset = result[0]
594 else:
595 # use existing file
596 with TiffFile(filename, **kwargs) as tif:
597 if page is not None:
598 page = tif.pages[page]
599 if not page.is_memmappable:
600 raise ValueError("image data are not memory-mappable")
601 offset, _ = page.is_contiguous
602 shape = page.shape
603 dtype = page.dtype
604 else:
605 series = tif.series[series]
606 if series.offset is None:
607 raise ValueError("image data are not memory-mappable")
608 shape = series.shape
609 dtype = series.dtype
610 offset = series.offset
611 dtype = tif.byteorder + dtype.char
612 return numpy.memmap(filename, dtype, mode, offset, shape, "C")
613
614
615class lazyattr(object):
616 """Attribute whose value is computed on first access."""
617
618 # TODO: help() doesn't work
619 __slots__ = ("func",)
620
621 def __init__(self, func):
622 self.func = func
623 # self.__name__ = func.__name__
624 # self.__doc__ = func.__doc__
625 # self.lock = threading.RLock()
626
627 def __get__(self, instance, owner):
628 # with self.lock:
629 if instance is None:
630 return self
631 try:
632 value = self.func(instance)
633 except AttributeError as e:
634 raise RuntimeError(e)
635 if value is NotImplemented:
636 return getattr(super(owner, instance), self.func.__name__)
637 setattr(instance, self.func.__name__, value)
638 return value
639
640
641class TiffWriter(object):
642 """Write numpy arrays to TIFF file.
643
644 TiffWriter instances must be closed using the 'close' method, which is
645 automatically called when using the 'with' context manager.
646
647 TiffWriter's main purpose is saving nD numpy array's as TIFF,
648 not to create any possible TIFF format. Specifically, JPEG compression,
649 SubIFDs, ExifIFD, or GPSIFD tags are not supported.
650
651 Examples
652 --------
653 >>> # successively append images to BigTIFF file
654 >>> data = numpy.random.rand(2, 5, 3, 301, 219)
655 >>> with TiffWriter('temp.tif', bigtiff=True) as tif:
656 ... for i in range(data.shape[0]):
657 ... tif.save(data[i], compress=6, photometric='minisblack')
658
659 """
660
661 def __init__(self, file, bigtiff=False, byteorder=None, append=False, imagej=False):
662 """Open a TIFF file for writing.
663
664 An empty TIFF file is created if the file does not exist, else the
665 file is overwritten with an empty TIFF file unless 'append'
666 is true. Use bigtiff=True when creating files larger than 4 GB.
667
668 Parameters
669 ----------
670 file : str, binary stream, or FileHandle
671 File name or writable binary stream, such as an open file
672 or BytesIO.
673 bigtiff : bool
674 If True, the BigTIFF format is used.
675 byteorder : {'<', '>', '=', '|'}
676 The endianness of the data in the file.
677 By default, this is the system's native byte order.
678 append : bool
679 If True and 'file' is an existing standard TIFF file, image data
680 and tags are appended to the file.
681 Appending data may corrupt specifically formatted TIFF files
682 such as LSM, STK, ImageJ, NIH, or FluoView.
683 imagej : bool
684 If True, write an ImageJ hyperstack compatible file.
685 This format can handle data types uint8, uint16, or float32 and
686 data shapes up to 6 dimensions in TZCYXS order.
687 RGB images (S=3 or S=4) must be uint8.
688 ImageJ's default byte order is big-endian but this implementation
689 uses the system's native byte order by default.
690 ImageJ does not support BigTIFF format or LZMA compression.
691 The ImageJ file format is undocumented.
692
693 """
694 if append:
695 # determine if file is an existing TIFF file that can be extended
696 try:
697 with FileHandle(file, mode="rb", size=0) as fh:
698 pos = fh.tell()
699 try:
700 with TiffFile(fh) as tif:
701 if append != "force" and any(
702 getattr(tif, "is_" + a)
703 for a in (
704 "lsm",
705 "stk",
706 "imagej",
707 "nih",
708 "fluoview",
709 "micromanager",
710 )
711 ):
712 raise ValueError("file contains metadata")
713 byteorder = tif.byteorder
714 bigtiff = tif.is_bigtiff
715 self._ifdoffset = tif.pages.next_page_offset
716 except Exception as e:
717 raise ValueError("cannot append to file: %s" % str(e))
718 finally:
719 fh.seek(pos)
720 except (IOError, FileNotFoundError):
721 append = False
722
723 if byteorder in (None, "=", "|"):
724 byteorder = "<" if sys.byteorder == "little" else ">"
725 elif byteorder not in ("<", ">"):
726 raise ValueError("invalid byteorder %s" % byteorder)
727 if imagej and bigtiff:
728 warnings.warn("writing incompatible BigTIFF ImageJ")
729
730 self._byteorder = byteorder
731 self._imagej = bool(imagej)
732 self._truncate = False
733 self._metadata = None
734 self._colormap = None
735
736 self._descriptionoffset = 0
737 self._descriptionlen = 0
738 self._descriptionlenoffset = 0
739 self._tags = None
740 self._shape = None # normalized shape of data in consecutive pages
741 self._datashape = None # shape of data in consecutive pages
742 self._datadtype = None # data type
743 self._dataoffset = None # offset to data
744 self._databytecounts = None # byte counts per plane
745 self._tagoffsets = None # strip or tile offset tag code
746
747 if bigtiff:
748 self._bigtiff = True
749 self._offsetsize = 8
750 self._tagsize = 20
751 self._tagnoformat = "Q"
752 self._offsetformat = "Q"
753 self._valueformat = "8s"
754 else:
755 self._bigtiff = False
756 self._offsetsize = 4
757 self._tagsize = 12
758 self._tagnoformat = "H"
759 self._offsetformat = "I"
760 self._valueformat = "4s"
761
762 if append:
763 self._fh = FileHandle(file, mode="r+b", size=0)
764 self._fh.seek(0, 2)
765 else:
766 self._fh = FileHandle(file, mode="wb", size=0)
767 self._fh.write({"<": b"II", ">": b"MM"}[byteorder])
768 if bigtiff:
769 self._fh.write(struct.pack(byteorder + "HHH", 43, 8, 0))
770 else:
771 self._fh.write(struct.pack(byteorder + "H", 42))
772 # first IFD
773 self._ifdoffset = self._fh.tell()
774 self._fh.write(struct.pack(byteorder + self._offsetformat, 0))
775
776 def save(
777 self,
778 data=None,
779 shape=None,
780 dtype=None,
781 returnoffset=False,
782 photometric=None,
783 planarconfig=None,
784 tile=None,
785 contiguous=True,
786 align=16,
787 truncate=False,
788 compress=0,
789 rowsperstrip=None,
790 predictor=False,
791 colormap=None,
792 description=None,
793 datetime=None,
794 resolution=None,
795 software="tifffile.py",
796 metadata={},
797 ijmetadata=None,
798 extratags=(),
799 ):
800 """Write numpy array and tags to TIFF file.
801
802 The data shape's last dimensions are assumed to be image depth,
803 height (length), width, and samples.
804 If a colormap is provided, the data's dtype must be uint8 or uint16
805 and the data values are indices into the last dimension of the
806 colormap.
807 If 'shape' and 'dtype' are specified, an empty array is saved.
808 This option cannot be used with compression or multiple tiles.
809 Image data are written uncompressed in one strip per plane by default.
810 Dimensions larger than 2 to 4 (depending on photometric mode, planar
811 configuration, and SGI mode) are flattened and saved as separate pages.
812 The SampleFormat and BitsPerSample tags are derived from the data type.
813
814 Parameters
815 ----------
816 data : numpy.ndarray or None
817 Input image array.
818 shape : tuple or None
819 Shape of the empty array to save. Used only if 'data' is None.
820 dtype : numpy.dtype or None
821 Data-type of the empty array to save. Used only if 'data' is None.
822 returnoffset : bool
823 If True and the image data in the file is memory-mappable, return
824 the offset and number of bytes of the image data in the file.
825 photometric : {'MINISBLACK', 'MINISWHITE', 'RGB', 'PALETTE', 'CFA'}
826 The color space of the image data.
827 By default, this setting is inferred from the data shape and the
828 value of colormap.
829 For CFA images, DNG tags must be specified in 'extratags'.
830 planarconfig : {'CONTIG', 'SEPARATE'}
831 Specifies if samples are stored contiguous or in separate planes.
832 By default, this setting is inferred from the data shape.
833 If this parameter is set, extra samples are used to store grayscale
834 images.
835 'CONTIG': last dimension contains samples.
836 'SEPARATE': third last dimension contains samples.
837 tile : tuple of int
838 The shape (depth, length, width) of image tiles to write.
839 If None (default), image data are written in strips.
840 The tile length and width must be a multiple of 16.
841 If the tile depth is provided, the SGI ImageDepth and TileDepth
842 tags are used to save volume data.
843 Unless a single tile is used, tiles cannot be used to write
844 contiguous files.
845 Few software can read the SGI format, e.g. MeVisLab.
846 contiguous : bool
847 If True (default) and the data and parameters are compatible with
848 previous ones, if any, the image data are stored contiguously after
849 the previous one. Parameters 'photometric' and 'planarconfig'
850 are ignored. Parameters 'description', datetime', and 'extratags'
851 are written to the first page of a contiguous series only.
852 align : int
853 Byte boundary on which to align the image data in the file.
854 Default 16. Use mmap.ALLOCATIONGRANULARITY for memory-mapped data.
855 Following contiguous writes are not aligned.
856 truncate : bool
857 If True, only write the first page including shape metadata if
858 possible (uncompressed, contiguous, not tiled).
859 Other TIFF readers will only be able to read part of the data.
860 compress : int or 'LZMA', 'ZSTD'
861 Values from 0 to 9 controlling the level of zlib compression.
862 If 0 (default), data are written uncompressed.
863 Compression cannot be used to write contiguous files.
864 If 'LZMA' or 'ZSTD', LZMA or ZSTD compression is used, which is
865 not available on all platforms.
866 rowsperstrip : int
867 The number of rows per strip used for compression.
868 Uncompressed data are written in one strip per plane.
869 predictor : bool
870 If True, apply horizontal differencing to integer type images
871 before compression.
872 colormap : numpy.ndarray
873 RGB color values for the corresponding data value.
874 Must be of shape (3, 2**(data.itemsize*8)) and dtype uint16.
875 description : str
876 The subject of the image. Must be 7-bit ASCII. Cannot be used with
877 the ImageJ format. Saved with the first page only.
878 datetime : datetime
879 Date and time of image creation in '%Y:%m:%d %H:%M:%S' format.
880 If None (default), the current date and time is used.
881 Saved with the first page only.
882 resolution : (float, float[, str]) or ((int, int), (int, int)[, str])
883 X and Y resolutions in pixels per resolution unit as float or
884 rational numbers. A third, optional parameter specifies the
885 resolution unit, which must be None (default for ImageJ),
886 'INCH' (default), or 'CENTIMETER'.
887 software : str
888 Name of the software used to create the file. Must be 7-bit ASCII.
889 Saved with the first page only.
890 metadata : dict
891 Additional meta data to be saved along with shape information
892 in JSON or ImageJ formats in an ImageDescription tag.
893 If None, do not write a second ImageDescription tag.
894 Strings must be 7-bit ASCII. Saved with the first page only.
895 ijmetadata : dict
896 Additional meta data to be saved in application specific
897 IJMetadata and IJMetadataByteCounts tags. Refer to the
898 imagej_metadata_tags function for valid keys and values.
899 Saved with the first page only.
900 extratags : sequence of tuples
901 Additional tags as [(code, dtype, count, value, writeonce)].
902
903 code : int
904 The TIFF tag Id.
905 dtype : str
906 Data type of items in 'value' in Python struct format.
907 One of B, s, H, I, 2I, b, h, i, 2i, f, d, Q, or q.
908 count : int
909 Number of data values. Not used for string or byte string
910 values.
911 value : sequence
912 'Count' values compatible with 'dtype'.
913 Byte strings must contain count values of dtype packed as
914 binary data.
915 writeonce : bool
916 If True, the tag is written to the first page only.
917
918 """
919 # TODO: refactor this function
920 fh = self._fh
921 byteorder = self._byteorder
922
923 if data is None:
924 if compress:
925 raise ValueError("cannot save compressed empty file")
926 datashape = shape
927 datadtype = numpy.dtype(dtype).newbyteorder(byteorder)
928 datadtypechar = datadtype.char
929 else:
930 data = numpy.asarray(data, byteorder + data.dtype.char, "C")
931 if data.size == 0:
932 raise ValueError("cannot save empty array")
933 datashape = data.shape
934 datadtype = data.dtype
935 datadtypechar = data.dtype.char
936
937 returnoffset = returnoffset and datadtype.isnative
938 bilevel = datadtypechar == "?"
939 if bilevel:
940 index = -1 if datashape[-1] > 1 else -2
941 datasize = product(datashape[:index])
942 if datashape[index] % 8:
943 datasize *= datashape[index] // 8 + 1
944 else:
945 datasize *= datashape[index] // 8
946 else:
947 datasize = product(datashape) * datadtype.itemsize
948
949 # just append contiguous data if possible
950 self._truncate = bool(truncate)
951 if self._datashape:
952 if (
953 not contiguous
954 or self._datashape[1:] != datashape
955 or self._datadtype != datadtype
956 or (compress and self._tags)
957 or tile
958 or not numpy.array_equal(colormap, self._colormap)
959 ):
960 # incompatible shape, dtype, compression mode, or colormap
961 self._write_remaining_pages()
962 self._write_image_description()
963 self._truncate = False
964 self._descriptionoffset = 0
965 self._descriptionlenoffset = 0
966 self._datashape = None
967 self._colormap = None
968 if self._imagej:
969 raise ValueError("ImageJ does not support non-contiguous data")
970 else:
971 # consecutive mode
972 self._datashape = (self._datashape[0] + 1,) + datashape
973 if not compress:
974 # write contiguous data, write IFDs/tags later
975 offset = fh.tell()
976 if data is None:
977 fh.write_empty(datasize)
978 else:
979 fh.write_array(data)
980 if returnoffset:
981 return offset, datasize
982 return
983
984 input_shape = datashape
985 tagnoformat = self._tagnoformat
986 valueformat = self._valueformat
987 offsetformat = self._offsetformat
988 offsetsize = self._offsetsize
989 tagsize = self._tagsize
990
991 MINISBLACK = TIFF.PHOTOMETRIC.MINISBLACK
992 RGB = TIFF.PHOTOMETRIC.RGB
993 CFA = TIFF.PHOTOMETRIC.CFA
994 PALETTE = TIFF.PHOTOMETRIC.PALETTE
995 CONTIG = TIFF.PLANARCONFIG.CONTIG
996 SEPARATE = TIFF.PLANARCONFIG.SEPARATE
997
998 # parse input
999 if photometric is not None:
1000 photometric = enumarg(TIFF.PHOTOMETRIC, photometric)
1001 if planarconfig:
1002 planarconfig = enumarg(TIFF.PLANARCONFIG, planarconfig)
1003 if not compress:
1004 compress = False
1005 compresstag = 1
1006 predictor = False
1007 else:
1008 if isinstance(compress, (tuple, list)):
1009 compress, compresslevel = compress
1010 elif isinstance(compress, int):
1011 compress, compresslevel = "ADOBE_DEFLATE", int(compress)
1012 if not 0 <= compresslevel <= 9:
1013 raise ValueError("invalid compression level %s" % compress)
1014 else:
1015 compresslevel = None
1016 compress = compress.upper()
1017 compresstag = enumarg(TIFF.COMPRESSION, compress)
1018
1019 # prepare ImageJ format
1020 if self._imagej:
1021 if compress in ("LZMA", "ZSTD"):
1022 raise ValueError("ImageJ cannot handle LZMA or ZSTD compression")
1023 if description:
1024 warnings.warn("not writing description to ImageJ file")
1025 description = None
1026 volume = False
1027 if datadtypechar not in "BHhf":
1028 raise ValueError("ImageJ does not support data type %s" % datadtypechar)
1029 ijrgb = photometric == RGB if photometric else None
1030 if datadtypechar not in "B":
1031 ijrgb = False
1032 ijshape = imagej_shape(datashape, ijrgb)
1033 if ijshape[-1] in (3, 4):
1034 photometric = RGB
1035 if datadtypechar not in "B":
1036 raise ValueError(
1037 "ImageJ does not support data type %s "
1038 "for RGB" % datadtypechar
1039 )
1040 elif photometric is None:
1041 photometric = MINISBLACK
1042 planarconfig = None
1043 if planarconfig == SEPARATE:
1044 raise ValueError("ImageJ does not support planar images")
1045 else:
1046 planarconfig = CONTIG if ijrgb else None
1047
1048 # define compress function
1049 if compress:
1050 if compresslevel is None:
1051 compressor, compresslevel = TIFF.COMPESSORS[compresstag]
1052 else:
1053 compressor, _ = TIFF.COMPESSORS[compresstag]
1054 compresslevel = int(compresslevel)
1055 if predictor:
1056 if datadtype.kind not in "iu":
1057 raise ValueError("prediction not implemented for %s" % datadtype)
1058
1059 def compress(data, level=compresslevel):
1060 # horizontal differencing
1061 diff = numpy.diff(data, axis=-2)
1062 data = numpy.insert(diff, 0, data[..., 0, :], axis=-2)
1063 return compressor(data, level)
1064
1065 else:
1066
1067 def compress(data, level=compresslevel):
1068 return compressor(data, level)
1069
1070 # verify colormap and indices
1071 if colormap is not None:
1072 if datadtypechar not in "BH":
1073 raise ValueError("invalid data dtype for palette mode")
1074 colormap = numpy.asarray(colormap, dtype=byteorder + "H")
1075 if colormap.shape != (3, 2 ** (datadtype.itemsize * 8)):
1076 raise ValueError("invalid color map shape")
1077 self._colormap = colormap
1078
1079 # verify tile shape
1080 if tile:
1081 tile = tuple(int(i) for i in tile[:3])
1082 volume = len(tile) == 3
1083 if (
1084 len(tile) < 2
1085 or tile[-1] % 16
1086 or tile[-2] % 16
1087 or any(i < 1 for i in tile)
1088 ):
1089 raise ValueError("invalid tile shape")
1090 else:
1091 tile = ()
1092 volume = False
1093
1094 # normalize data shape to 5D or 6D, depending on volume:
1095 # (pages, planar_samples, [depth,] height, width, contig_samples)
1096 datashape = reshape_nd(datashape, 3 if photometric == RGB else 2)
1097 shape = datashape
1098 ndim = len(datashape)
1099
1100 samplesperpixel = 1
1101 extrasamples = 0
1102 if volume and ndim < 3:
1103 volume = False
1104 if colormap is not None:
1105 photometric = PALETTE
1106 planarconfig = None
1107 if photometric is None:
1108 photometric = MINISBLACK
1109 if bilevel:
1110 photometric = TIFF.PHOTOMETRIC.MINISWHITE
1111 elif planarconfig == CONTIG:
1112 if ndim > 2 and shape[-1] in (3, 4):
1113 photometric = RGB
1114 elif planarconfig == SEPARATE:
1115 if volume and ndim > 3 and shape[-4] in (3, 4):
1116 photometric = RGB
1117 elif ndim > 2 and shape[-3] in (3, 4):
1118 photometric = RGB
1119 elif ndim > 2 and shape[-1] in (3, 4):
1120 photometric = RGB
1121 elif self._imagej:
1122 photometric = MINISBLACK
1123 elif volume and ndim > 3 and shape[-4] in (3, 4):
1124 photometric = RGB
1125 elif ndim > 2 and shape[-3] in (3, 4):
1126 photometric = RGB
1127 if planarconfig and len(shape) <= (3 if volume else 2):
1128 planarconfig = None
1129 photometric = MINISBLACK
1130 if photometric == RGB:
1131 if len(shape) < 3:
1132 raise ValueError("not a RGB(A) image")
1133 if len(shape) < 4:
1134 volume = False
1135 if planarconfig is None:
1136 if shape[-1] in (3, 4):
1137 planarconfig = CONTIG
1138 elif shape[-4 if volume else -3] in (3, 4):
1139 planarconfig = SEPARATE
1140 elif shape[-1] > shape[-4 if volume else -3]:
1141 planarconfig = SEPARATE
1142 else:
1143 planarconfig = CONTIG
1144 if planarconfig == CONTIG:
1145 datashape = (-1, 1) + shape[(-4 if volume else -3) :]
1146 samplesperpixel = datashape[-1]
1147 else:
1148 datashape = (-1,) + shape[(-4 if volume else -3) :] + (1,)
1149 samplesperpixel = datashape[1]
1150 if samplesperpixel > 3:
1151 extrasamples = samplesperpixel - 3
1152 elif photometric == CFA:
1153 if len(shape) != 2:
1154 raise ValueError("invalid CFA image")
1155 volume = False
1156 planarconfig = None
1157 datashape = (-1, 1) + shape[-2:] + (1,)
1158 if 50706 not in (et[0] for et in extratags):
1159 raise ValueError("must specify DNG tags for CFA image")
1160 elif planarconfig and len(shape) > (3 if volume else 2):
1161 if planarconfig == CONTIG:
1162 datashape = (-1, 1) + shape[(-4 if volume else -3) :]
1163 samplesperpixel = datashape[-1]
1164 else:
1165 datashape = (-1,) + shape[(-4 if volume else -3) :] + (1,)
1166 samplesperpixel = datashape[1]
1167 extrasamples = samplesperpixel - 1
1168 else:
1169 planarconfig = None
1170 # remove trailing 1s
1171 while len(shape) > 2 and shape[-1] == 1:
1172 shape = shape[:-1]
1173 if len(shape) < 3:
1174 volume = False
1175 datashape = (-1, 1) + shape[(-3 if volume else -2) :] + (1,)
1176
1177 # normalize shape to 6D
1178 assert len(datashape) in (5, 6)
1179 if len(datashape) == 5:
1180 datashape = datashape[:2] + (1,) + datashape[2:]
1181 if datashape[0] == -1:
1182 s0 = product(input_shape) // product(datashape[1:])
1183 datashape = (s0,) + datashape[1:]
1184 shape = datashape
1185 if data is not None:
1186 data = data.reshape(shape)
1187
1188 if tile and not volume:
1189 tile = (1, tile[-2], tile[-1])
1190
1191 if photometric == PALETTE:
1192 if samplesperpixel != 1 or extrasamples or shape[1] != 1 or shape[-1] != 1:
1193 raise ValueError("invalid data shape for palette mode")
1194
1195 if photometric == RGB and samplesperpixel == 2:
1196 raise ValueError("not a RGB image (samplesperpixel=2)")
1197
1198 if bilevel:
1199 if compress:
1200 raise ValueError("cannot save compressed bilevel image")
1201 if tile:
1202 raise ValueError("cannot save tiled bilevel image")
1203 if photometric not in (0, 1):
1204 raise ValueError("cannot save bilevel image as %s" % str(photometric))
1205 datashape = list(datashape)
1206 if datashape[-2] % 8:
1207 datashape[-2] = datashape[-2] // 8 + 1
1208 else:
1209 datashape[-2] = datashape[-2] // 8
1210 datashape = tuple(datashape)
1211 assert datasize == product(datashape)
1212 if data is not None:
1213 data = numpy.packbits(data, axis=-2)
1214 assert datashape[-2] == data.shape[-2]
1215
1216 bytestr = (
1217 bytes
1218 if sys.version[0] == "2"
1219 else (lambda x: bytes(x, "ascii") if isinstance(x, str) else x)
1220 )
1221 tags = [] # list of (code, ifdentry, ifdvalue, writeonce)
1222
1223 strip_or_tile = "Tile" if tile else "Strip"
1224 tagbytecounts = TIFF.TAG_NAMES[strip_or_tile + "ByteCounts"]
1225 tag_offsets = TIFF.TAG_NAMES[strip_or_tile + "Offsets"]
1226 self._tagoffsets = tag_offsets
1227
1228 def pack(fmt, *val):
1229 return struct.pack(byteorder + fmt, *val)
1230
1231 def addtag(code, dtype, count, value, writeonce=False):
1232 # Compute ifdentry & ifdvalue bytes from code, dtype, count, value
1233 # Append (code, ifdentry, ifdvalue, writeonce) to tags list
1234 code = int(TIFF.TAG_NAMES.get(code, code))
1235 try:
1236 tifftype = TIFF.DATA_DTYPES[dtype]
1237 except KeyError:
1238 raise ValueError("unknown dtype %s" % dtype)
1239 rawcount = count
1240
1241 if dtype == "s":
1242 # strings
1243 value = bytestr(value) + b"\0"
1244 count = rawcount = len(value)
1245 rawcount = value.find(b"\0\0")
1246 if rawcount < 0:
1247 rawcount = count
1248 else:
1249 rawcount += 1 # length of string without buffer
1250 value = (value,)
1251 elif isinstance(value, bytes):
1252 # packed binary data
1253 dtsize = struct.calcsize(dtype)
1254 if len(value) % dtsize:
1255 raise ValueError("invalid packed binary data")
1256 count = len(value) // dtsize
1257 if len(dtype) > 1:
1258 count *= int(dtype[:-1])
1259 dtype = dtype[-1]
1260 ifdentry = [pack("HH", code, tifftype), pack(offsetformat, rawcount)]
1261 ifdvalue = None
1262 if struct.calcsize(dtype) * count <= offsetsize:
1263 # value(s) can be written directly
1264 if isinstance(value, bytes):
1265 ifdentry.append(pack(valueformat, value))
1266 elif count == 1:
1267 if isinstance(value, (tuple, list, numpy.ndarray)):
1268 value = value[0]
1269 ifdentry.append(pack(valueformat, pack(dtype, value)))
1270 else:
1271 ifdentry.append(pack(valueformat, pack(str(count) + dtype, *value)))
1272 else:
1273 # use offset to value(s)
1274 ifdentry.append(pack(offsetformat, 0))
1275 if isinstance(value, bytes):
1276 ifdvalue = value
1277 elif isinstance(value, numpy.ndarray):
1278 assert value.size == count
1279 assert value.dtype.char == dtype
1280 ifdvalue = value.tostring()
1281 elif isinstance(value, (tuple, list)):
1282 ifdvalue = pack(str(count) + dtype, *value)
1283 else:
1284 ifdvalue = pack(dtype, value)
1285 tags.append((code, b"".join(ifdentry), ifdvalue, writeonce))
1286
1287 def rational(arg, max_denominator=1000000):
1288 """ "Return nominator and denominator from float or two integers."""
1289 from fractions import Fraction # delayed import
1290
1291 try:
1292 f = Fraction.from_float(arg)
1293 except TypeError:
1294 f = Fraction(arg[0], arg[1])
1295 f = f.limit_denominator(max_denominator)
1296 return f.numerator, f.denominator
1297
1298 if description:
1299 # user provided description
1300 addtag("ImageDescription", "s", 0, description, writeonce=True)
1301
1302 # write shape and metadata to ImageDescription
1303 self._metadata = {} if not metadata else metadata.copy()
1304 if self._imagej:
1305 description = imagej_description(
1306 input_shape,
1307 shape[-1] in (3, 4),
1308 self._colormap is not None,
1309 **self._metadata
1310 )
1311 elif metadata or metadata == {}:
1312 if self._truncate:
1313 self._metadata.update(truncated=True)
1314 description = json_description(input_shape, **self._metadata)
1315 else:
1316 description = None
1317 if description:
1318 # add 64 bytes buffer
1319 # the image description might be updated later with the final shape
1320 description = str2bytes(description, "ascii")
1321 description += b"\0" * 64
1322 self._descriptionlen = len(description)
1323 addtag("ImageDescription", "s", 0, description, writeonce=True)
1324
1325 if software:
1326 addtag("Software", "s", 0, software, writeonce=True)
1327 if datetime is None:
1328 datetime = self._now()
1329 addtag(
1330 "DateTime", "s", 0, datetime.strftime("%Y:%m:%d %H:%M:%S"), writeonce=True
1331 )
1332 addtag("Compression", "H", 1, compresstag)
1333 if predictor:
1334 addtag("Predictor", "H", 1, 2)
1335 addtag("ImageWidth", "I", 1, shape[-2])
1336 addtag("ImageLength", "I", 1, shape[-3])
1337 if tile:
1338 addtag("TileWidth", "I", 1, tile[-1])
1339 addtag("TileLength", "I", 1, tile[-2])
1340 if tile[0] > 1:
1341 addtag("ImageDepth", "I", 1, shape[-4])
1342 addtag("TileDepth", "I", 1, tile[0])
1343 addtag("NewSubfileType", "I", 1, 0)
1344 if not bilevel:
1345 sampleformat = {"u": 1, "i": 2, "f": 3, "c": 6}[datadtype.kind]
1346 addtag(
1347 "SampleFormat", "H", samplesperpixel, (sampleformat,) * samplesperpixel
1348 )
1349 addtag("PhotometricInterpretation", "H", 1, photometric.value)
1350 if colormap is not None:
1351 addtag("ColorMap", "H", colormap.size, colormap)
1352 addtag("SamplesPerPixel", "H", 1, samplesperpixel)
1353 if bilevel:
1354 pass
1355 elif planarconfig and samplesperpixel > 1:
1356 addtag("PlanarConfiguration", "H", 1, planarconfig.value)
1357 addtag(
1358 "BitsPerSample",
1359 "H",
1360 samplesperpixel,
1361 (datadtype.itemsize * 8,) * samplesperpixel,
1362 )
1363 else:
1364 addtag("BitsPerSample", "H", 1, datadtype.itemsize * 8)
1365 if extrasamples:
1366 if photometric == RGB and extrasamples == 1:
1367 addtag("ExtraSamples", "H", 1, 1) # associated alpha channel
1368 else:
1369 addtag("ExtraSamples", "H", extrasamples, (0,) * extrasamples)
1370 if resolution is not None:
1371 addtag("XResolution", "2I", 1, rational(resolution[0]))
1372 addtag("YResolution", "2I", 1, rational(resolution[1]))
1373 if len(resolution) > 2:
1374 unit = resolution[2]
1375 unit = 1 if unit is None else enumarg(TIFF.RESUNIT, unit)
1376 elif self._imagej:
1377 unit = 1
1378 else:
1379 unit = 2
1380 addtag("ResolutionUnit", "H", 1, unit)
1381 elif not self._imagej:
1382 addtag("XResolution", "2I", 1, (1, 1))
1383 addtag("YResolution", "2I", 1, (1, 1))
1384 addtag("ResolutionUnit", "H", 1, 1)
1385 if ijmetadata:
1386 for t in imagej_metadata_tags(ijmetadata, byteorder):
1387 addtag(*t)
1388
1389 contiguous = not compress
1390 if tile:
1391 # one chunk per tile per plane
1392 tiles = (
1393 (shape[2] + tile[0] - 1) // tile[0],
1394 (shape[3] + tile[1] - 1) // tile[1],
1395 (shape[4] + tile[2] - 1) // tile[2],
1396 )
1397 numtiles = product(tiles) * shape[1]
1398 stripbytecounts = [
1399 product(tile) * shape[-1] * datadtype.itemsize
1400 ] * numtiles
1401 addtag(tagbytecounts, offsetformat, numtiles, stripbytecounts)
1402 addtag(tag_offsets, offsetformat, numtiles, [0] * numtiles)
1403 contiguous = contiguous and product(tiles) == 1
1404 if not contiguous:
1405 # allocate tile buffer
1406 chunk = numpy.empty(tile + (shape[-1],), dtype=datadtype)
1407 elif contiguous:
1408 # one strip per plane
1409 if bilevel:
1410 stripbytecounts = [product(datashape[2:])] * shape[1]
1411 else:
1412 stripbytecounts = [product(datashape[2:]) * datadtype.itemsize] * shape[
1413 1
1414 ]
1415 addtag(tagbytecounts, offsetformat, shape[1], stripbytecounts)
1416 addtag(tag_offsets, offsetformat, shape[1], [0] * shape[1])
1417 addtag("RowsPerStrip", "I", 1, shape[-3])
1418 else:
1419 # compress rowsperstrip or ~64 KB chunks
1420 rowsize = product(shape[-2:]) * datadtype.itemsize
1421 if rowsperstrip is None:
1422 rowsperstrip = 65536 // rowsize
1423 if rowsperstrip < 1:
1424 rowsperstrip = 1
1425 elif rowsperstrip > shape[-3]:
1426 rowsperstrip = shape[-3]
1427 addtag("RowsPerStrip", "I", 1, rowsperstrip)
1428
1429 numstrips = (shape[-3] + rowsperstrip - 1) // rowsperstrip
1430 numstrips *= shape[1]
1431 stripbytecounts = [0] * numstrips
1432 addtag(tagbytecounts, offsetformat, numstrips, [0] * numstrips)
1433 addtag(tag_offsets, offsetformat, numstrips, [0] * numstrips)
1434
1435 if data is None and not contiguous:
1436 raise ValueError("cannot write non-contiguous empty file")
1437
1438 # add extra tags from user
1439 for t in extratags:
1440 addtag(*t)
1441
1442 # TODO: check TIFFReadDirectoryCheckOrder warning in files containing
1443 # multiple tags of same code
1444 # the entries in an IFD must be sorted in ascending order by tag code
1445 tags = sorted(tags, key=lambda x: x[0])
1446
1447 if not (self._bigtiff or self._imagej) and (fh.tell() + datasize > 2**31 - 1):
1448 raise ValueError("data too large for standard TIFF file")
1449
1450 # if not compressed or multi-tiled, write the first IFD and then
1451 # all data contiguously; else, write all IFDs and data interleaved
1452 for pageindex in range(1 if contiguous else shape[0]):
1453 # update pointer at ifd_offset
1454 pos = fh.tell()
1455 if pos % 2:
1456 # location of IFD must begin on a word boundary
1457 fh.write(b"\0")
1458 pos += 1
1459 fh.seek(self._ifdoffset)
1460 fh.write(pack(offsetformat, pos))
1461 fh.seek(pos)
1462
1463 # write ifdentries
1464 fh.write(pack(tagnoformat, len(tags)))
1465 tag_offset = fh.tell()
1466 fh.write(b"".join(t[1] for t in tags))
1467 self._ifdoffset = fh.tell()
1468 fh.write(pack(offsetformat, 0)) # offset to next IFD
1469
1470 # write tag values and patch offsets in ifdentries, if necessary
1471 for tagindex, tag in enumerate(tags):
1472 if tag[2]:
1473 pos = fh.tell()
1474 if pos % 2:
1475 # tag value is expected to begin on word boundary
1476 fh.write(b"\0")
1477 pos += 1
1478 fh.seek(tag_offset + tagindex * tagsize + offsetsize + 4)
1479 fh.write(pack(offsetformat, pos))
1480 fh.seek(pos)
1481 if tag[0] == tag_offsets:
1482 stripoffsetsoffset = pos
1483 elif tag[0] == tagbytecounts:
1484 strip_bytecounts_offset = pos
1485 elif tag[0] == 270 and tag[2].endswith(b"\0\0\0\0"):
1486 # image description buffer
1487 self._descriptionoffset = pos
1488 self._descriptionlenoffset = tag_offset + tagindex * tagsize + 4
1489 fh.write(tag[2])
1490
1491 # write image data
1492 data_offset = fh.tell()
1493 skip = align - data_offset % align
1494 fh.seek(skip, 1)
1495 data_offset += skip
1496 if contiguous:
1497 if data is None:
1498 fh.write_empty(datasize)
1499 else:
1500 fh.write_array(data)
1501 elif tile:
1502 if data is None:
1503 fh.write_empty(numtiles * stripbytecounts[0])
1504 else:
1505 stripindex = 0
1506 for plane in data[pageindex]:
1507 for tz in range(tiles[0]):
1508 for ty in range(tiles[1]):
1509 for tx in range(tiles[2]):
1510 c0 = min(tile[0], shape[2] - tz * tile[0])
1511 c1 = min(tile[1], shape[3] - ty * tile[1])
1512 c2 = min(tile[2], shape[4] - tx * tile[2])
1513 chunk[c0:, c1:, c2:] = 0
1514 chunk[:c0, :c1, :c2] = plane[
1515 tz * tile[0] : tz * tile[0] + c0,
1516 ty * tile[1] : ty * tile[1] + c1,
1517 tx * tile[2] : tx * tile[2] + c2,
1518 ]
1519 if compress:
1520 t = compress(chunk)
1521 fh.write(t)
1522 stripbytecounts[stripindex] = len(t)
1523 stripindex += 1
1524 else:
1525 fh.write_array(chunk)
1526 fh.flush()
1527 elif compress:
1528 # write one strip per rowsperstrip
1529 assert data.shape[2] == 1 # not handling depth
1530 numstrips = (shape[-3] + rowsperstrip - 1) // rowsperstrip
1531 stripindex = 0
1532 for plane in data[pageindex]:
1533 for i in range(numstrips):
1534 strip = plane[0, i * rowsperstrip : (i + 1) * rowsperstrip]
1535 strip = compress(strip)
1536 fh.write(strip)
1537 stripbytecounts[stripindex] = len(strip)
1538 stripindex += 1
1539
1540 # update strip/tile offsets and bytecounts if necessary
1541 pos = fh.tell()
1542 for tagindex, tag in enumerate(tags):
1543 if tag[0] == tag_offsets: # strip/tile offsets
1544 if tag[2]:
1545 fh.seek(stripoffsetsoffset)
1546 strip_offset = data_offset
1547 for size in stripbytecounts:
1548 fh.write(pack(offsetformat, strip_offset))
1549 strip_offset += size
1550 else:
1551 fh.seek(tag_offset + tagindex * tagsize + offsetsize + 4)
1552 fh.write(pack(offsetformat, data_offset))
1553 elif tag[0] == tagbytecounts: # strip/tile bytecounts
1554 if compress:
1555 if tag[2]:
1556 fh.seek(strip_bytecounts_offset)
1557 for size in stripbytecounts:
1558 fh.write(pack(offsetformat, size))
1559 else:
1560 fh.seek(tag_offset + tagindex * tagsize + offsetsize + 4)
1561 fh.write(pack(offsetformat, stripbytecounts[0]))
1562 break
1563 fh.seek(pos)
1564 fh.flush()
1565
1566 # remove tags that should be written only once
1567 if pageindex == 0:
1568 tags = [tag for tag in tags if not tag[-1]]
1569
1570 self._shape = shape
1571 self._datashape = (1,) + input_shape
1572 self._datadtype = datadtype
1573 self._dataoffset = data_offset
1574 self._databytecounts = stripbytecounts
1575
1576 if contiguous:
1577 # write remaining IFDs/tags later
1578 self._tags = tags
1579 # return offset and size of image data
1580 if returnoffset:
1581 return data_offset, sum(stripbytecounts)
1582
1583 def _write_remaining_pages(self):
1584 """Write outstanding IFDs and tags to file."""
1585 if not self._tags or self._truncate:
1586 return
1587
1588 fh = self._fh
1589 fhpos = fh.tell()
1590 if fhpos % 2:
1591 fh.write(b"\0")
1592 fhpos += 1
1593 byteorder = self._byteorder
1594 offsetformat = self._offsetformat
1595 offsetsize = self._offsetsize
1596 tagnoformat = self._tagnoformat
1597 tagsize = self._tagsize
1598 dataoffset = self._dataoffset
1599 pagedatasize = sum(self._databytecounts)
1600 pageno = self._shape[0] * self._datashape[0] - 1
1601
1602 def pack(fmt, *val):
1603 return struct.pack(byteorder + fmt, *val)
1604
1605 # construct template IFD in memory
1606 # need to patch offsets to next IFD and data before writing to disk
1607 ifd = io.BytesIO()
1608 ifd.write(pack(tagnoformat, len(self._tags)))
1609 tagoffset = ifd.tell()
1610 ifd.write(b"".join(t[1] for t in self._tags))
1611 ifdoffset = ifd.tell()
1612 ifd.write(pack(offsetformat, 0)) # offset to next IFD
1613 # tag values
1614 for tagindex, tag in enumerate(self._tags):
1615 offset2value = tagoffset + tagindex * tagsize + offsetsize + 4
1616 if tag[2]:
1617 pos = ifd.tell()
1618 if pos % 2: # tag value is expected to begin on word boundary
1619 ifd.write(b"\0")
1620 pos += 1
1621 ifd.seek(offset2value)
1622 try:
1623 ifd.write(pack(offsetformat, pos + fhpos))
1624 except Exception: # struct.error
1625 if self._imagej:
1626 warnings.warn("truncating ImageJ file")
1627 self._truncate = True
1628 return
1629 raise ValueError("data too large for non-BigTIFF file")
1630 ifd.seek(pos)
1631 ifd.write(tag[2])
1632 if tag[0] == self._tagoffsets:
1633 # save strip/tile offsets for later updates
1634 stripoffset2offset = offset2value
1635 stripoffset2value = pos
1636 elif tag[0] == self._tagoffsets:
1637 # save strip/tile offsets for later updates
1638 stripoffset2offset = None
1639 stripoffset2value = offset2value
1640 # size to word boundary
1641 if ifd.tell() % 2:
1642 ifd.write(b"\0")
1643
1644 # check if all IFDs fit in file
1645 pos = fh.tell()
1646 if not self._bigtiff and pos + ifd.tell() * pageno > 2**32 - 256:
1647 if self._imagej:
1648 warnings.warn("truncating ImageJ file")
1649 self._truncate = True
1650 return
1651 raise ValueError("data too large for non-BigTIFF file")
1652
1653 # TODO: assemble IFD chain in memory
1654 for _ in range(pageno):
1655 # update pointer at IFD offset
1656 pos = fh.tell()
1657 fh.seek(self._ifdoffset)
1658 fh.write(pack(offsetformat, pos))
1659 fh.seek(pos)
1660 self._ifdoffset = pos + ifdoffset
1661 # update strip/tile offsets in IFD
1662 dataoffset += pagedatasize # offset to image data
1663 if stripoffset2offset is None:
1664 ifd.seek(stripoffset2value)
1665 ifd.write(pack(offsetformat, dataoffset))
1666 else:
1667 ifd.seek(stripoffset2offset)
1668 ifd.write(pack(offsetformat, pos + stripoffset2value))
1669 ifd.seek(stripoffset2value)
1670 stripoffset = dataoffset
1671 for size in self._databytecounts:
1672 ifd.write(pack(offsetformat, stripoffset))
1673 stripoffset += size
1674 # write IFD entry
1675 fh.write(ifd.getvalue())
1676
1677 self._tags = None
1678 self._datadtype = None
1679 self._dataoffset = None
1680 self._databytecounts = None
1681 # do not reset _shape or _data_shape
1682
1683 def _write_image_description(self):
1684 """Write meta data to ImageDescription tag."""
1685 if (
1686 not self._datashape
1687 or self._datashape[0] == 1
1688 or self._descriptionoffset <= 0
1689 ):
1690 return
1691
1692 colormapped = self._colormap is not None
1693 if self._imagej:
1694 isrgb = self._shape[-1] in (3, 4)
1695 description = imagej_description(
1696 self._datashape, isrgb, colormapped, **self._metadata
1697 )
1698 else:
1699 description = json_description(self._datashape, **self._metadata)
1700
1701 # rewrite description and its length to file
1702 description = description.encode("utf-8")
1703 description = description[: self._descriptionlen - 1]
1704 pos = self._fh.tell()
1705 self._fh.seek(self._descriptionoffset)
1706 self._fh.write(description)
1707 self._fh.seek(self._descriptionlenoffset)
1708 self._fh.write(
1709 struct.pack(self._byteorder + self._offsetformat, len(description) + 1)
1710 )
1711 self._fh.seek(pos)
1712
1713 self._descriptionoffset = 0
1714 self._descriptionlenoffset = 0
1715 self._descriptionlen = 0
1716
1717 def _now(self):
1718 """Return current date and time."""
1719 return datetime.datetime.now()
1720
1721 def close(self):
1722 """Write remaining pages and close file handle."""
1723 if not self._truncate:
1724 self._write_remaining_pages()
1725 self._write_image_description()
1726 self._fh.close()
1727
1728 def __enter__(self):
1729 return self
1730
1731 def __exit__(self, exc_type, exc_value, traceback):
1732 self.close()
1733
1734
1735class TiffFile(object):
1736 """Read image and metadata from TIFF file.
1737
1738 TiffFile instances must be closed using the 'close' method, which is
1739 automatically called when using the 'with' context manager.
1740
1741 Attributes
1742 ----------
1743 pages : TiffPages
1744 Sequence of TIFF pages in file.
1745 series : list of TiffPageSeries
1746 Sequences of closely related TIFF pages. These are computed
1747 from OME, LSM, ImageJ, etc. metadata or based on similarity
1748 of page properties such as shape, dtype, and compression.
1749 byteorder : '>', '<'
1750 The endianness of data in the file.
1751 '>': big-endian (Motorola).
1752 '>': little-endian (Intel).
1753 is_flag : bool
1754 If True, file is of a certain format.
1755 Flags are: bigtiff, movie, shaped, ome, imagej, stk, lsm, fluoview,
1756 nih, vista, 'micromanager, metaseries, mdgel, mediacy, tvips, fei,
1757 sem, scn, svs, scanimage, andor, epics, pilatus, qptiff.
1758
1759 All attributes are read-only.
1760
1761 Examples
1762 --------
1763 >>> # read image array from TIFF file
1764 >>> imsave('temp.tif', numpy.random.rand(5, 301, 219))
1765 >>> with TiffFile('temp.tif') as tif:
1766 ... data = tif.asarray()
1767 >>> data.shape
1768 (5, 301, 219)
1769
1770 """
1771
1772 def __init__(
1773 self,
1774 arg,
1775 name=None,
1776 offset=None,
1777 size=None,
1778 multifile=True,
1779 movie=None,
1780 **kwargs
1781 ):
1782 """Initialize instance from file.
1783
1784 Parameters
1785 ----------
1786 arg : str or open file
1787 Name of file or open file object.
1788 The file objects are closed in TiffFile.close().
1789 name : str
1790 Optional name of file in case 'arg' is a file handle.
1791 offset : int
1792 Optional start position of embedded file. By default, this is
1793 the current file position.
1794 size : int
1795 Optional size of embedded file. By default, this is the number
1796 of bytes from the 'offset' to the end of the file.
1797 multifile : bool
1798 If True (default), series may include pages from multiple files.
1799 Currently applies to OME-TIFF only.
1800 movie : bool
1801 If True, assume that later pages differ from first page only by
1802 data offsets and byte counts. Significantly increases speed and
1803 reduces memory usage when reading movies with thousands of pages.
1804 Enabling this for non-movie files will result in data corruption
1805 or crashes. Python 3 only.
1806 kwargs : bool
1807 'is_ome': If False, disable processing of OME-XML metadata.
1808
1809 """
1810 if "fastij" in kwargs:
1811 del kwargs["fastij"]
1812 raise DeprecationWarning("the fastij option will be removed")
1813 for key, value in kwargs.items():
1814 if key[:3] == "is_" and key[3:] in TIFF.FILE_FLAGS:
1815 if value is not None and not value:
1816 setattr(self, key, bool(value))
1817 else:
1818 raise TypeError("unexpected keyword argument: %s" % key)
1819
1820 fh = FileHandle(arg, mode="rb", name=name, offset=offset, size=size)
1821 self._fh = fh
1822 self._multifile = bool(multifile)
1823 self._files = {fh.name: self} # cache of TiffFiles
1824 try:
1825 fh.seek(0)
1826 try:
1827 byteorder = {b"II": "<", b"MM": ">"}[fh.read(2)]
1828 except KeyError:
1829 raise ValueError("not a TIFF file")
1830 sys_byteorder = {"big": ">", "little": "<"}[sys.byteorder]
1831 self.isnative = byteorder == sys_byteorder
1832
1833 version = struct.unpack(byteorder + "H", fh.read(2))[0]
1834 if version == 43:
1835 # BigTiff
1836 self.is_bigtiff = True
1837 offsetsize, zero = struct.unpack(byteorder + "HH", fh.read(4))
1838 if zero or offsetsize != 8:
1839 raise ValueError("invalid BigTIFF file")
1840 self.byteorder = byteorder
1841 self.offsetsize = 8
1842 self.offsetformat = byteorder + "Q"
1843 self.tagnosize = 8
1844 self.tagnoformat = byteorder + "Q"
1845 self.tagsize = 20
1846 self.tagformat1 = byteorder + "HH"
1847 self.tagformat2 = byteorder + "Q8s"
1848 elif version == 42:
1849 self.is_bigtiff = False
1850 self.byteorder = byteorder
1851 self.offsetsize = 4
1852 self.offsetformat = byteorder + "I"
1853 self.tagnosize = 2
1854 self.tagnoformat = byteorder + "H"
1855 self.tagsize = 12
1856 self.tagformat1 = byteorder + "HH"
1857 self.tagformat2 = byteorder + "I4s"
1858 else:
1859 raise ValueError("invalid TIFF file")
1860
1861 # file handle is at offset to offset to first page
1862 self.pages = TiffPages(self)
1863
1864 if self.is_lsm and (
1865 self.filehandle.size >= 2**32
1866 or self.pages[0].compression != 1
1867 or self.pages[1].compression != 1
1868 ):
1869 self._lsm_load_pages()
1870 self._lsm_fix_strip_offsets()
1871 self._lsm_fix_strip_bytecounts()
1872 elif movie:
1873 self.pages.useframes = True
1874
1875 except Exception:
1876 fh.close()
1877 raise
1878
1879 @property
1880 def filehandle(self):
1881 """Return file handle."""
1882 return self._fh
1883
1884 @property
1885 def filename(self):
1886 """Return name of file handle."""
1887 return self._fh.name
1888
1889 @lazyattr
1890 def fstat(self):
1891 """Return status of file handle as stat_result object."""
1892 try:
1893 return os.fstat(self._fh.fileno())
1894 except Exception: # io.UnsupportedOperation
1895 return None
1896
1897 def close(self):
1898 """Close open file handle(s)."""
1899 for tif in self._files.values():
1900 tif.filehandle.close()
1901 self._files = {}
1902
1903 def asarray(self, key=None, series=None, out=None, validate=True, maxworkers=1):
1904 """Return image data from multiple TIFF pages as numpy array.
1905
1906 By default, the data from the first series is returned.
1907
1908 Parameters
1909 ----------
1910 key : int, slice, or sequence of page indices
1911 Defines which pages to return as array.
1912 series : int or TiffPageSeries
1913 Defines which series of pages to return as array.
1914 out : numpy.ndarray, str, or file-like object; optional
1915 Buffer where image data will be saved.
1916 If None (default), a new array will be created.
1917 If numpy.ndarray, a writable array of compatible dtype and shape.
1918 If 'memmap', directly memory-map the image data in the TIFF file
1919 if possible; else create a memory-mapped array in a temporary file.
1920 If str or open file, the file name or file object used to
1921 create a memory-map to an array stored in a binary file on disk.
1922 validate : bool
1923 If True (default), validate various tags.
1924 Passed to TiffPage.asarray().
1925 maxworkers : int
1926 Maximum number of threads to concurrently get data from pages.
1927 Default is 1. If None, up to half the CPU cores are used.
1928 Reading data from file is limited to a single thread.
1929 Using multiple threads can significantly speed up this function
1930 if the bottleneck is decoding compressed data, e.g. in case of
1931 large LZW compressed LSM files.
1932 If the bottleneck is I/O or pure Python code, using multiple
1933 threads might be detrimental.
1934
1935 """
1936 if not self.pages:
1937 return numpy.array([])
1938 if key is None and series is None:
1939 series = 0
1940 if series is not None:
1941 try:
1942 series = self.series[series]
1943 except (KeyError, TypeError):
1944 pass
1945 pages = series._pages
1946 else:
1947 pages = self.pages
1948
1949 if key is None:
1950 pass
1951 elif isinstance(key, inttypes):
1952 pages = [pages[key]]
1953 elif isinstance(key, slice):
1954 pages = pages[key]
1955 elif isinstance(key, collections.Iterable):
1956 pages = [pages[k] for k in key]
1957 else:
1958 raise TypeError("key must be an int, slice, or sequence")
1959
1960 if not pages:
1961 raise ValueError("no pages selected")
1962
1963 if self.is_nih:
1964 result = stack_pages(pages, out=out, maxworkers=maxworkers, squeeze=False)
1965 elif key is None and series and series.offset:
1966 typecode = self.byteorder + series.dtype.char
1967 if out == "memmap" and pages[0].is_memmappable:
1968 result = self.filehandle.memmap_array(
1969 typecode, series.shape, series.offset
1970 )
1971 else:
1972 if out is not None:
1973 out = create_output(out, series.shape, series.dtype)
1974 self.filehandle.seek(series.offset)
1975 result = self.filehandle.read_array(
1976 typecode, product(series.shape), out=out, native=True
1977 )
1978 elif len(pages) == 1:
1979 result = pages[0].asarray(out=out, validate=validate)
1980 else:
1981 result = stack_pages(pages, out=out, maxworkers=maxworkers)
1982
1983 if result is None:
1984 return
1985
1986 if key is None:
1987 try:
1988 result.shape = series.shape
1989 except ValueError:
1990 try:
1991 warnings.warn(
1992 "failed to reshape %s to %s" % (result.shape, series.shape)
1993 )
1994 # try series of expected shapes
1995 result.shape = (-1,) + series.shape
1996 except ValueError:
1997 # revert to generic shape
1998 result.shape = (-1,) + pages[0].shape
1999 elif len(pages) == 1:
2000 result.shape = pages[0].shape
2001 else:
2002 result.shape = (-1,) + pages[0].shape
2003 return result
2004
2005 @lazyattr
2006 def series(self):
2007 """Return related pages as TiffPageSeries.
2008
2009 Side effect: after calling this function, TiffFile.pages might contain
2010 TiffPage and TiffFrame instances.
2011
2012 """
2013 if not self.pages:
2014 return []
2015
2016 useframes = self.pages.useframes
2017 keyframe = self.pages.keyframe
2018 series = []
2019 for name in "ome imagej lsm fluoview nih mdgel shaped".split():
2020 if getattr(self, "is_" + name, False):
2021 series = getattr(self, "_%s_series" % name)()
2022 break
2023 self.pages.useframes = useframes
2024 self.pages.keyframe = keyframe
2025 if not series:
2026 series = self._generic_series()
2027
2028 # remove empty series, e.g. in MD Gel files
2029 series = [s for s in series if sum(s.shape) > 0]
2030
2031 for i, s in enumerate(series):
2032 s.index = i
2033 return series
2034
2035 def _generic_series(self):
2036 """Return image series in file."""
2037 if self.pages.useframes:
2038 # movie mode
2039 page = self.pages[0]
2040 shape = page.shape
2041 axes = page.axes
2042 if len(self.pages) > 1:
2043 shape = (len(self.pages),) + shape
2044 axes = "I" + axes
2045 return [
2046 TiffPageSeries(self.pages[:], shape, page.dtype, axes, stype="movie")
2047 ]
2048
2049 self.pages.clear(False)
2050 self.pages.load()
2051 result = []
2052 keys = []
2053 series = {}
2054 compressions = TIFF.DECOMPESSORS
2055 for page in self.pages:
2056 if not page.shape:
2057 continue
2058 key = page.shape + (page.axes, page.compression in compressions)
2059 if key in series:
2060 series[key].append(page)
2061 else:
2062 keys.append(key)
2063 series[key] = [page]
2064 for key in keys:
2065 pages = series[key]
2066 page = pages[0]
2067 shape = page.shape
2068 axes = page.axes
2069 if len(pages) > 1:
2070 shape = (len(pages),) + shape
2071 axes = "I" + axes
2072 result.append(
2073 TiffPageSeries(pages, shape, page.dtype, axes, stype="Generic")
2074 )
2075
2076 return result
2077
2078 def _shaped_series(self):
2079 """Return image series in "shaped" file."""
2080 pages = self.pages
2081 pages.useframes = True
2082 lenpages = len(pages)
2083
2084 def append_series(series, pages, axes, shape, reshape, name, truncated):
2085 page = pages[0]
2086 if not axes:
2087 shape = page.shape
2088 axes = page.axes
2089 if len(pages) > 1:
2090 shape = (len(pages),) + shape
2091 axes = "Q" + axes
2092 size = product(shape)
2093 resize = product(reshape)
2094 if page.is_contiguous and resize > size and resize % size == 0:
2095 if truncated is None:
2096 truncated = True
2097 axes = "Q" + axes
2098 shape = (resize // size,) + shape
2099 try:
2100 axes = reshape_axes(axes, shape, reshape)
2101 shape = reshape
2102 except ValueError as e:
2103 warnings.warn(str(e))
2104 series.append(
2105 TiffPageSeries(
2106 pages,
2107 shape,
2108 page.dtype,
2109 axes,
2110 name=name,
2111 stype="Shaped",
2112 truncated=truncated,
2113 )
2114 )
2115
2116 keyframe = axes = shape = reshape = name = None
2117 series = []
2118 index = 0
2119 while True:
2120 if index >= lenpages:
2121 break
2122 # new keyframe; start of new series
2123 pages.keyframe = index
2124 keyframe = pages[index]
2125 if not keyframe.is_shaped:
2126 warnings.warn("invalid shape metadata or corrupted file")
2127 return
2128 # read metadata
2129 axes = None
2130 shape = None
2131 metadata = json_description_metadata(keyframe.is_shaped)
2132 name = metadata.get("name", "")
2133 reshape = metadata["shape"]
2134 truncated = metadata.get("truncated", None)
2135 if "axes" in metadata:
2136 axes = metadata["axes"]
2137 if len(axes) == len(reshape):
2138 shape = reshape
2139 else:
2140 axes = ""
2141 warnings.warn("axes do not match shape")
2142 # skip pages if possible
2143 spages = [keyframe]
2144 size = product(reshape)
2145 npages, mod = divmod(size, product(keyframe.shape))
2146 if mod:
2147 warnings.warn("series shape does not match page shape")
2148 return
2149 if 1 < npages <= lenpages - index:
2150 size *= keyframe._dtype.itemsize
2151 if truncated:
2152 npages = 1
2153 elif (
2154 keyframe.is_final
2155 and keyframe.offset + size < pages[index + 1].offset
2156 ):
2157 truncated = False
2158 else:
2159 # need to read all pages for series
2160 truncated = False
2161 for j in range(index + 1, index + npages):
2162 page = pages[j]
2163 page.keyframe = keyframe
2164 spages.append(page)
2165 append_series(series, spages, axes, shape, reshape, name, truncated)
2166 index += npages
2167
2168 return series
2169
2170 def _imagej_series(self):
2171 """Return image series in ImageJ file."""
2172 # ImageJ's dimension order is always TZCYXS
2173 # TODO: fix loading of color, composite, or palette images
2174 self.pages.useframes = True
2175 self.pages.keyframe = 0
2176
2177 ij = self.imagej_metadata
2178 pages = self.pages
2179 page = pages[0]
2180
2181 def is_hyperstack():
2182 # ImageJ hyperstack store all image metadata in the first page and
2183 # image data are stored contiguously before the second page, if any
2184 if not page.is_final:
2185 return False
2186 images = ij.get("images", 0)
2187 if images <= 1:
2188 return False
2189 offset, count = page.is_contiguous
2190 if (
2191 count != product(page.shape) * page.bitspersample // 8
2192 or offset + count * images > self.filehandle.size
2193 ):
2194 raise ValueError()
2195 # check that next page is stored after data
2196 if len(pages) > 1 and offset + count * images > pages[1].offset:
2197 return False
2198 return True
2199
2200 try:
2201 hyperstack = is_hyperstack()
2202 except ValueError:
2203 warnings.warn("invalid ImageJ metadata or corrupted file")
2204 return
2205 if hyperstack:
2206 # no need to read other pages
2207 pages = [page]
2208 else:
2209 self.pages.load()
2210
2211 shape = []
2212 axes = []
2213 if "frames" in ij:
2214 shape.append(ij["frames"])
2215 axes.append("T")
2216 if "slices" in ij:
2217 shape.append(ij["slices"])
2218 axes.append("Z")
2219 if "channels" in ij and not (
2220 page.photometric == 2 and not ij.get("hyperstack", False)
2221 ):
2222 shape.append(ij["channels"])
2223 axes.append("C")
2224 remain = ij.get("images", len(pages)) // (product(shape) if shape else 1)
2225 if remain > 1:
2226 shape.append(remain)
2227 axes.append("I")
2228 if page.axes[0] == "I":
2229 # contiguous multiple images
2230 shape.extend(page.shape[1:])
2231 axes.extend(page.axes[1:])
2232 elif page.axes[:2] == "SI":
2233 # color-mapped contiguous multiple images
2234 shape = page.shape[0:1] + tuple(shape) + page.shape[2:]
2235 axes = list(page.axes[0]) + axes + list(page.axes[2:])
2236 else:
2237 shape.extend(page.shape)
2238 axes.extend(page.axes)
2239
2240 truncated = (
2241 hyperstack
2242 and len(self.pages) == 1
2243 and page.is_contiguous[1] != product(shape) * page.bitspersample // 8
2244 )
2245
2246 return [
2247 TiffPageSeries(
2248 pages, shape, page.dtype, axes, stype="ImageJ", truncated=truncated
2249 )
2250 ]
2251
2252 def _fluoview_series(self):
2253 """Return image series in FluoView file."""
2254 self.pages.useframes = True
2255 self.pages.keyframe = 0
2256 self.pages.load()
2257 mm = self.fluoview_metadata
2258 mmhd = list(reversed(mm["Dimensions"]))
2259 axes = "".join(
2260 TIFF.MM_DIMENSIONS.get(i[0].upper(), "Q") for i in mmhd if i[1] > 1
2261 )
2262 shape = tuple(int(i[1]) for i in mmhd if i[1] > 1)
2263 return [
2264 TiffPageSeries(
2265 self.pages,
2266 shape,
2267 self.pages[0].dtype,
2268 axes,
2269 name=mm["ImageName"],
2270 stype="FluoView",
2271 )
2272 ]
2273
2274 def _mdgel_series(self):
2275 """Return image series in MD Gel file."""
2276 # only a single page, scaled according to metadata in second page
2277 self.pages.useframes = False
2278 self.pages.keyframe = 0
2279 self.pages.load()
2280 md = self.mdgel_metadata
2281 if md["FileTag"] in (2, 128):
2282 dtype = numpy.dtype("float32")
2283 scale = md["ScalePixel"]
2284 scale = scale[0] / scale[1] # rational
2285 if md["FileTag"] == 2:
2286 # squary root data format
2287 def transform(a):
2288 return a.astype("float32") ** 2 * scale
2289
2290 else:
2291
2292 def transform(a):
2293 return a.astype("float32") * scale
2294
2295 else:
2296 transform = None
2297 page = self.pages[0]
2298 return [
2299 TiffPageSeries(
2300 [page], page.shape, dtype, page.axes, transform=transform, stype="MDGel"
2301 )
2302 ]
2303
2304 def _nih_series(self):
2305 """Return image series in NIH file."""
2306 self.pages.useframes = True
2307 self.pages.keyframe = 0
2308 self.pages.load()
2309 page0 = self.pages[0]
2310 if len(self.pages) == 1:
2311 shape = page0.shape
2312 axes = page0.axes
2313 else:
2314 shape = (len(self.pages),) + page0.shape
2315 axes = "I" + page0.axes
2316 return [TiffPageSeries(self.pages, shape, page0.dtype, axes, stype="NIH")]
2317
2318 def _ome_series(self):
2319 """Return image series in OME-TIFF file(s)."""
2320 from xml.etree import cElementTree as etree # delayed import
2321
2322 omexml = self.pages[0].description
2323 try:
2324 root = etree.fromstring(omexml)
2325 except etree.ParseError as e:
2326 # TODO: test badly encoded OME-XML
2327 warnings.warn("ome-xml: %s" % e)
2328 try:
2329 # might work on Python 2
2330 omexml = omexml.decode("utf-8", "ignore").encode("utf-8")
2331 root = etree.fromstring(omexml)
2332 except Exception:
2333 return
2334
2335 self.pages.useframes = True
2336 self.pages.keyframe = 0
2337 self.pages.load()
2338
2339 uuid = root.attrib.get("UUID", None)
2340 self._files = {uuid: self}
2341 dirname = self._fh.dirname
2342 modulo = {}
2343 series = []
2344 for element in root:
2345 if element.tag.endswith("BinaryOnly"):
2346 # TODO: load OME-XML from master or companion file
2347 warnings.warn("ome-xml: not an ome-tiff master file")
2348 break
2349 if element.tag.endswith("StructuredAnnotations"):
2350 for annot in element:
2351 if not annot.attrib.get("Namespace", "").endswith("modulo"):
2352 continue
2353 for value in annot:
2354 for modul in value:
2355 for along in modul:
2356 if not along.tag[:-1].endswith("Along"):
2357 continue
2358 axis = along.tag[-1]
2359 newaxis = along.attrib.get("Type", "other")
2360 newaxis = TIFF.AXES_LABELS[newaxis]
2361 if "Start" in along.attrib:
2362 step = float(along.attrib.get("Step", 1))
2363 start = float(along.attrib["Start"])
2364 stop = float(along.attrib["End"]) + step
2365 labels = numpy.arange(start, stop, step)
2366 else:
2367 labels = [
2368 label.text
2369 for label in along
2370 if label.tag.endswith("Label")
2371 ]
2372 modulo[axis] = (newaxis, labels)
2373
2374 if not element.tag.endswith("Image"):
2375 continue
2376
2377 attr = element.attrib
2378 name = attr.get("Name", None)
2379
2380 for pixels in element:
2381 if not pixels.tag.endswith("Pixels"):
2382 continue
2383 attr = pixels.attrib
2384 dtype = attr.get("PixelType", None)
2385 axes = "".join(reversed(attr["DimensionOrder"]))
2386 shape = list(int(attr["Size" + ax]) for ax in axes)
2387 size = product(shape[:-2])
2388 ifds = None
2389 spp = 1 # samples per pixel
2390 # FIXME: this implementation assumes the last two
2391 # dimensions are stored in tiff pages (shape[:-2]).
2392 # Apparently that is not always the case.
2393 for data in pixels:
2394 if data.tag.endswith("Channel"):
2395 attr = data.attrib
2396 if ifds is None:
2397 spp = int(attr.get("SamplesPerPixel", spp))
2398 ifds = [None] * (size // spp)
2399 elif int(attr.get("SamplesPerPixel", 1)) != spp:
2400 raise ValueError("cannot handle differing SamplesPerPixel")
2401 continue
2402 if ifds is None:
2403 ifds = [None] * (size // spp)
2404 if not data.tag.endswith("TiffData"):
2405 continue
2406 attr = data.attrib
2407 ifd = int(attr.get("IFD", 0))
2408 num = int(attr.get("NumPlanes", 1 if "IFD" in attr else 0))
2409 num = int(attr.get("PlaneCount", num))
2410 idx = [int(attr.get("First" + ax, 0)) for ax in axes[:-2]]
2411 try:
2412 idx = numpy.ravel_multi_index(idx, shape[:-2])
2413 except ValueError:
2414 # ImageJ produces invalid ome-xml when cropping
2415 warnings.warn("ome-xml: invalid TiffData index")
2416 continue
2417 for uuid in data:
2418 if not uuid.tag.endswith("UUID"):
2419 continue
2420 if uuid.text not in self._files:
2421 if not self._multifile:
2422 # abort reading multifile OME series
2423 # and fall back to generic series
2424 return []
2425 fname = uuid.attrib["FileName"]
2426 try:
2427 tif = TiffFile(os.path.join(dirname, fname))
2428 tif.pages.useframes = True
2429 tif.pages.keyframe = 0
2430 tif.pages.load()
2431 except (IOError, FileNotFoundError, ValueError):
2432 warnings.warn("ome-xml: failed to read '%s'" % fname)
2433 break
2434 self._files[uuid.text] = tif
2435 tif.close()
2436 pages = self._files[uuid.text].pages
2437 try:
2438 for i in range(num if num else len(pages)):
2439 ifds[idx + i] = pages[ifd + i]
2440 except IndexError:
2441 warnings.warn("ome-xml: index out of range")
2442 # only process first UUID
2443 break
2444 else:
2445 pages = self.pages
2446 try:
2447 for i in range(num if num else len(pages)):
2448 ifds[idx + i] = pages[ifd + i]
2449 except IndexError:
2450 warnings.warn("ome-xml: index out of range")
2451
2452 if all(i is None for i in ifds):
2453 # skip images without data
2454 continue
2455
2456 # set a keyframe on all IFDs
2457 keyframe = None
2458 for i in ifds:
2459 # try find a TiffPage
2460 if i and i == i.keyframe:
2461 keyframe = i
2462 break
2463 if not keyframe:
2464 # reload a TiffPage from file
2465 for i, keyframe in enumerate(ifds):
2466 if keyframe:
2467 keyframe.parent.pages.keyframe = keyframe.index
2468 keyframe = keyframe.parent.pages[keyframe.index]
2469 ifds[i] = keyframe
2470 break
2471 for i in ifds:
2472 if i is not None:
2473 i.keyframe = keyframe
2474
2475 dtype = keyframe.dtype
2476 series.append(
2477 TiffPageSeries(
2478 ifds, shape, dtype, axes, parent=self, name=name, stype="OME"
2479 )
2480 )
2481 for serie in series:
2482 shape = list(serie.shape)
2483 for axis, (newaxis, labels) in modulo.items():
2484 i = serie.axes.index(axis)
2485 size = len(labels)
2486 if shape[i] == size:
2487 serie.axes = serie.axes.replace(axis, newaxis, 1)
2488 else:
2489 shape[i] //= size
2490 shape.insert(i + 1, size)
2491 serie.axes = serie.axes.replace(axis, axis + newaxis, 1)
2492 serie.shape = tuple(shape)
2493 # squeeze dimensions
2494 for serie in series:
2495 serie.shape, serie.axes = squeeze_axes(serie.shape, serie.axes)
2496 return series
2497
2498 def _lsm_series(self):
2499 """Return main image series in LSM file. Skip thumbnails."""
2500 lsmi = self.lsm_metadata
2501 axes = TIFF.CZ_LSMINFO_SCANTYPE[lsmi["ScanType"]]
2502 if self.pages[0].photometric == 2: # RGB; more than one channel
2503 axes = axes.replace("C", "").replace("XY", "XYC")
2504 if lsmi.get("DimensionP", 0) > 1:
2505 axes += "P"
2506 if lsmi.get("DimensionM", 0) > 1:
2507 axes += "M"
2508 axes = axes[::-1]
2509 shape = tuple(int(lsmi[TIFF.CZ_LSMINFO_DIMENSIONS[i]]) for i in axes)
2510 name = lsmi.get("Name", "")
2511 self.pages.keyframe = 0
2512 pages = self.pages[::2]
2513 dtype = pages[0].dtype
2514 series = [TiffPageSeries(pages, shape, dtype, axes, name=name, stype="LSM")]
2515
2516 if self.pages[1].is_reduced:
2517 self.pages.keyframe = 1
2518 pages = self.pages[1::2]
2519 dtype = pages[0].dtype
2520 cp, i = 1, 0
2521 while cp < len(pages) and i < len(shape) - 2:
2522 cp *= shape[i]
2523 i += 1
2524 shape = shape[:i] + pages[0].shape
2525 axes = axes[:i] + "CYX"
2526 series.append(
2527 TiffPageSeries(pages, shape, dtype, axes, name=name, stype="LSMreduced")
2528 )
2529
2530 return series
2531
2532 def _lsm_load_pages(self):
2533 """Load all pages from LSM file."""
2534 self.pages.cache = True
2535 self.pages.useframes = True
2536 # second series: thumbnails
2537 self.pages.keyframe = 1
2538 keyframe = self.pages[1]
2539 for page in self.pages[1::2]:
2540 page.keyframe = keyframe
2541 # first series: data
2542 self.pages.keyframe = 0
2543 keyframe = self.pages[0]
2544 for page in self.pages[::2]:
2545 page.keyframe = keyframe
2546
2547 def _lsm_fix_strip_offsets(self):
2548 """Unwrap strip offsets for LSM files greater than 4 GB.
2549
2550 Each series and position require separate unwrapping (undocumented).
2551
2552 """
2553 if self.filehandle.size < 2**32:
2554 return
2555
2556 pages = self.pages
2557 npages = len(pages)
2558 series = self.series[0]
2559 axes = series.axes
2560
2561 # find positions
2562 positions = 1
2563 for i in 0, 1:
2564 if series.axes[i] in "PM":
2565 positions *= series.shape[i]
2566
2567 # make time axis first
2568 if positions > 1:
2569 ntimes = 0
2570 for i in 1, 2:
2571 if axes[i] == "T":
2572 ntimes = series.shape[i]
2573 break
2574 if ntimes:
2575 div, mod = divmod(npages, 2 * positions * ntimes)
2576 assert mod == 0
2577 shape = (positions, ntimes, div, 2)
2578 indices = numpy.arange(product(shape)).reshape(shape)
2579 indices = numpy.moveaxis(indices, 1, 0)
2580 else:
2581 indices = numpy.arange(npages).reshape(-1, 2)
2582
2583 # images of reduced page might be stored first
2584 if pages[0].dataoffsets[0] > pages[1].dataoffsets[0]:
2585 indices = indices[..., ::-1]
2586
2587 # unwrap offsets
2588 wrap = 0
2589 previousoffset = 0
2590 for i in indices.flat:
2591 page = pages[i]
2592 dataoffsets = []
2593 for currentoffset in page.dataoffsets:
2594 if currentoffset < previousoffset:
2595 wrap += 2**32
2596 dataoffsets.append(currentoffset + wrap)
2597 previousoffset = currentoffset
2598 page.dataoffsets = tuple(dataoffsets)
2599
2600 def _lsm_fix_strip_bytecounts(self):
2601 """Set databytecounts to size of compressed data.
2602
2603 The StripByteCounts tag in LSM files contains the number of bytes
2604 for the uncompressed data.
2605
2606 """
2607 pages = self.pages
2608 if pages[0].compression == 1:
2609 return
2610 # sort pages by first strip offset
2611 pages = sorted(pages, key=lambda p: p.dataoffsets[0])
2612 npages = len(pages) - 1
2613 for i, page in enumerate(pages):
2614 if page.index % 2:
2615 continue
2616 offsets = page.dataoffsets
2617 bytecounts = page.databytecounts
2618 if i < npages:
2619 lastoffset = pages[i + 1].dataoffsets[0]
2620 else:
2621 # LZW compressed strips might be longer than uncompressed
2622 lastoffset = min(offsets[-1] + 2 * bytecounts[-1], self._fh.size)
2623 offsets = offsets + (lastoffset,)
2624 page.databytecounts = tuple(
2625 offsets[j + 1] - offsets[j] for j in range(len(bytecounts))
2626 )
2627
2628 def __getattr__(self, name):
2629 """Return 'is_flag' attributes from first page."""
2630 if name[3:] in TIFF.FILE_FLAGS:
2631 if not self.pages:
2632 return False
2633 value = bool(getattr(self.pages[0], name))
2634 setattr(self, name, value)
2635 return value
2636 raise AttributeError(
2637 "'%s' object has no attribute '%s'" % (self.__class__.__name__, name)
2638 )
2639
2640 def __enter__(self):
2641 return self
2642
2643 def __exit__(self, exc_type, exc_value, traceback):
2644 self.close()
2645
2646 def __str__(self, detail=0, width=79):
2647 """Return string containing information about file.
2648
2649 The detail parameter specifies the level of detail returned:
2650
2651 0: file only.
2652 1: all series, first page of series and its tags.
2653 2: large tag values and file metadata.
2654 3: all pages.
2655
2656 """
2657 info = [
2658 "TiffFile '%s'",
2659 format_size(self._fh.size),
2660 {"<": "LittleEndian", ">": "BigEndian"}[self.byteorder],
2661 ]
2662 if self.is_bigtiff:
2663 info.append("BigTiff")
2664 info.append("|".join(f.upper() for f in self.flags))
2665 if len(self.pages) > 1:
2666 info.append("%i Pages" % len(self.pages))
2667 if len(self.series) > 1:
2668 info.append("%i Series" % len(self.series))
2669 if len(self._files) > 1:
2670 info.append("%i Files" % (len(self._files)))
2671 info = " ".join(info)
2672 info = info.replace(" ", " ").replace(" ", " ")
2673 info = info % snipstr(self._fh.name, max(12, width + 2 - len(info)))
2674 if detail <= 0:
2675 return info
2676 info = [info]
2677 info.append("\n".join(str(s) for s in self.series))
2678 if detail >= 3:
2679 info.extend(
2680 (
2681 TiffPage.__str__(p, detail=detail, width=width)
2682 for p in self.pages
2683 if p is not None
2684 )
2685 )
2686 else:
2687 info.extend(
2688 (
2689 TiffPage.__str__(s.pages[0], detail=detail, width=width)
2690 for s in self.series
2691 if s.pages[0] is not None
2692 )
2693 )
2694 if detail >= 2:
2695 for name in sorted(self.flags):
2696 if hasattr(self, name + "_metadata"):
2697 m = getattr(self, name + "_metadata")
2698 if m:
2699 info.append(
2700 "%s_METADATA\n%s"
2701 % (
2702 name.upper(),
2703 pformat(m, width=width, height=detail * 12),
2704 )
2705 )
2706 return "\n\n".join(info).replace("\n\n\n", "\n\n")
2707
2708 @lazyattr
2709 def flags(self):
2710 """Return set of file flags."""
2711 return set(
2712 name.lower()
2713 for name in sorted(TIFF.FILE_FLAGS)
2714 if getattr(self, "is_" + name)
2715 )
2716
2717 @lazyattr
2718 def is_mdgel(self):
2719 """File has MD Gel format."""
2720 try:
2721 return self.pages[0].is_mdgel or self.pages[1].is_mdgel
2722 except IndexError:
2723 return False
2724
2725 @property
2726 def is_movie(self):
2727 """Return if file is a movie."""
2728 return self.pages.useframes
2729
2730 @lazyattr
2731 def shaped_metadata(self):
2732 """Return Tifffile metadata from JSON descriptions as dicts."""
2733 if not self.is_shaped:
2734 return
2735 return tuple(
2736 json_description_metadata(s.pages[0].is_shaped)
2737 for s in self.series
2738 if s.stype.lower() == "shaped"
2739 )
2740
2741 @lazyattr
2742 def ome_metadata(self):
2743 """Return OME XML as dict."""
2744 # TODO: remove this or return XML?
2745 if not self.is_ome:
2746 return
2747 return xml2dict(self.pages[0].description)["OME"]
2748
2749 @lazyattr
2750 def qptiff_metadata(self):
2751 """Return PerkinElmer-QPI-ImageDescription XML element as dict."""
2752 if not self.is_qptiff:
2753 return
2754 root = "PerkinElmer-QPI-ImageDescription"
2755 xml = self.pages[0].description.replace(" " + root + " ", root)
2756 return xml2dict(xml)[root]
2757
2758 @lazyattr
2759 def lsm_metadata(self):
2760 """Return LSM metadata from CZ_LSMINFO tag as dict."""
2761 if not self.is_lsm:
2762 return
2763 return self.pages[0].tags["CZ_LSMINFO"].value
2764
2765 @lazyattr
2766 def stk_metadata(self):
2767 """Return STK metadata from UIC tags as dict."""
2768 if not self.is_stk:
2769 return
2770 page = self.pages[0]
2771 tags = page.tags
2772 result = {}
2773 result["NumberPlanes"] = tags["UIC2tag"].count
2774 if page.description:
2775 result["PlaneDescriptions"] = page.description.split("\0")
2776 # result['plane_descriptions'] = stk_description_metadata(
2777 # page.image_description)
2778 if "UIC1tag" in tags:
2779 result.update(tags["UIC1tag"].value)
2780 if "UIC3tag" in tags:
2781 result.update(tags["UIC3tag"].value) # wavelengths
2782 if "UIC4tag" in tags:
2783 result.update(tags["UIC4tag"].value) # override uic1 tags
2784 uic2tag = tags["UIC2tag"].value
2785 result["ZDistance"] = uic2tag["ZDistance"]
2786 result["TimeCreated"] = uic2tag["TimeCreated"]
2787 result["TimeModified"] = uic2tag["TimeModified"]
2788 try:
2789 result["DatetimeCreated"] = numpy.array(
2790 [
2791 julian_datetime(*dt)
2792 for dt in zip(uic2tag["DateCreated"], uic2tag["TimeCreated"])
2793 ],
2794 dtype="datetime64[ns]",
2795 )
2796 result["DatetimeModified"] = numpy.array(
2797 [
2798 julian_datetime(*dt)
2799 for dt in zip(uic2tag["DateModified"], uic2tag["TimeModified"])
2800 ],
2801 dtype="datetime64[ns]",
2802 )
2803 except ValueError as e:
2804 warnings.warn("stk_metadata: %s" % e)
2805 return result
2806
2807 @lazyattr
2808 def imagej_metadata(self):
2809 """Return consolidated ImageJ metadata as dict."""
2810 if not self.is_imagej:
2811 return
2812 page = self.pages[0]
2813 result = imagej_description_metadata(page.is_imagej)
2814 if "IJMetadata" in page.tags:
2815 try:
2816 result.update(page.tags["IJMetadata"].value)
2817 except Exception:
2818 pass
2819 return result
2820
2821 @lazyattr
2822 def fluoview_metadata(self):
2823 """Return consolidated FluoView metadata as dict."""
2824 if not self.is_fluoview:
2825 return
2826 result = {}
2827 page = self.pages[0]
2828 result.update(page.tags["MM_Header"].value)
2829 # TODO: read stamps from all pages
2830 result["Stamp"] = page.tags["MM_Stamp"].value
2831 # skip parsing image description; not reliable
2832 # try:
2833 # t = fluoview_description_metadata(page.image_description)
2834 # if t is not None:
2835 # result['ImageDescription'] = t
2836 # except Exception as e:
2837 # warnings.warn(
2838 # "failed to read FluoView image description: %s" % e)
2839 return result
2840
2841 @lazyattr
2842 def nih_metadata(self):
2843 """Return NIH Image metadata from NIHImageHeader tag as dict."""
2844 if not self.is_nih:
2845 return
2846 return self.pages[0].tags["NIHImageHeader"].value
2847
2848 @lazyattr
2849 def fei_metadata(self):
2850 """Return FEI metadata from SFEG or HELIOS tags as dict."""
2851 if not self.is_fei:
2852 return
2853 tags = self.pages[0].tags
2854 if "FEI_SFEG" in tags:
2855 return tags["FEI_SFEG"].value
2856 if "FEI_HELIOS" in tags:
2857 return tags["FEI_HELIOS"].value
2858
2859 @lazyattr
2860 def sem_metadata(self):
2861 """Return SEM metadata from CZ_SEM tag as dict."""
2862 if not self.is_sem:
2863 return
2864 return self.pages[0].tags["CZ_SEM"].value
2865
2866 @lazyattr
2867 def mdgel_metadata(self):
2868 """Return consolidated metadata from MD GEL tags as dict."""
2869 for page in self.pages[:2]:
2870 if "MDFileTag" in page.tags:
2871 tags = page.tags
2872 break
2873 else:
2874 return
2875 result = {}
2876 for code in range(33445, 33453):
2877 name = TIFF.TAGS[code]
2878 if name not in tags:
2879 continue
2880 result[name[2:]] = tags[name].value
2881 return result
2882
2883 @lazyattr
2884 def andor_metadata(self):
2885 """Return Andor tags as dict."""
2886 return self.pages[0].andor_tags
2887
2888 @lazyattr
2889 def epics_metadata(self):
2890 """Return EPICS areaDetector tags as dict."""
2891 return self.pages[0].epics_tags
2892
2893 @lazyattr
2894 def tvips_metadata(self):
2895 """Return TVIPS tag as dict."""
2896 if not self.is_tvips:
2897 return
2898 return self.pages[0].tags["TVIPS"].value
2899
2900 @lazyattr
2901 def metaseries_metadata(self):
2902 """Return MetaSeries metadata from image description as dict."""
2903 if not self.is_metaseries:
2904 return
2905 return metaseries_description_metadata(self.pages[0].description)
2906
2907 @lazyattr
2908 def pilatus_metadata(self):
2909 """Return Pilatus metadata from image description as dict."""
2910 if not self.is_pilatus:
2911 return
2912 return pilatus_description_metadata(self.pages[0].description)
2913
2914 @lazyattr
2915 def micromanager_metadata(self):
2916 """Return consolidated MicroManager metadata as dict."""
2917 if not self.is_micromanager:
2918 return
2919 # from file header
2920 result = read_micromanager_metadata(self._fh)
2921 # from tag
2922 result.update(self.pages[0].tags["MicroManagerMetadata"].value)
2923 return result
2924
2925 @lazyattr
2926 def scanimage_metadata(self):
2927 """Return ScanImage non-varying frame and ROI metadata as dict."""
2928 if not self.is_scanimage:
2929 return
2930 result = {}
2931 try:
2932 framedata, roidata = read_scanimage_metadata(self._fh)
2933 result["FrameData"] = framedata
2934 result.update(roidata)
2935 except ValueError:
2936 pass
2937 # TODO: scanimage_artist_metadata
2938 try:
2939 result["Description"] = scanimage_description_metadata(
2940 self.pages[0].description
2941 )
2942 except Exception as e:
2943 warnings.warn("scanimage_description_metadata failed: %s" % e)
2944 return result
2945
2946 @property
2947 def geotiff_metadata(self):
2948 """Return GeoTIFF metadata from first page as dict."""
2949 if not self.is_geotiff:
2950 return
2951 return self.pages[0].geotiff_tags
2952
2953
2954class TiffPages(object):
2955 """Sequence of TIFF image file directories."""
2956
2957 def __init__(self, parent):
2958 """Initialize instance from file. Read first TiffPage from file.
2959
2960 The file position must be at an offset to an offset to a TiffPage.
2961
2962 """
2963 self.parent = parent
2964 self.pages = [] # cache of TiffPages, TiffFrames, or their offsets
2965 self.complete = False # True if offsets to all pages were read
2966 self._tiffpage = TiffPage # class for reading tiff pages
2967 self._keyframe = None
2968 self._cache = True
2969
2970 # read offset to first page
2971 fh = parent.filehandle
2972 self._nextpageoffset = fh.tell()
2973 offset = struct.unpack(parent.offsetformat, fh.read(parent.offsetsize))[0]
2974
2975 if offset == 0:
2976 # warnings.warn('file contains no pages')
2977 self.complete = True
2978 return
2979 if offset >= fh.size:
2980 warnings.warn("invalid page offset (%i)" % offset)
2981 self.complete = True
2982 return
2983
2984 # always read and cache first page
2985 fh.seek(offset)
2986 page = TiffPage(parent, index=0)
2987 self.pages.append(page)
2988 self._keyframe = page
2989
2990 @property
2991 def cache(self):
2992 """Return if pages/frames are currently being cached."""
2993 return self._cache
2994
2995 @cache.setter
2996 def cache(self, value):
2997 """Enable or disable caching of pages/frames. Clear cache if False."""
2998 value = bool(value)
2999 if self._cache and not value:
3000 self.clear()
3001 self._cache = value
3002
3003 @property
3004 def useframes(self):
3005 """Return if currently using TiffFrame (True) or TiffPage (False)."""
3006 return self._tiffpage == TiffFrame and TiffFrame is not TiffPage
3007
3008 @useframes.setter
3009 def useframes(self, value):
3010 """Set to use TiffFrame (True) or TiffPage (False)."""
3011 self._tiffpage = TiffFrame if value else TiffPage
3012
3013 @property
3014 def keyframe(self):
3015 """Return index of current keyframe."""
3016 return self._keyframe.index
3017
3018 @keyframe.setter
3019 def keyframe(self, index):
3020 """Set current keyframe. Load TiffPage from file if necessary."""
3021 if self._keyframe.index == index:
3022 return
3023 if self.complete or 0 <= index < len(self.pages):
3024 page = self.pages[index]
3025 if isinstance(page, TiffPage):
3026 self._keyframe = page
3027 return
3028 elif isinstance(page, TiffFrame):
3029 # remove existing frame
3030 self.pages[index] = page.offset
3031 # load TiffPage from file
3032 useframes = self.useframes
3033 self._tiffpage = TiffPage
3034 self._keyframe = self[index]
3035 self.useframes = useframes
3036
3037 @property
3038 def next_page_offset(self):
3039 """Return offset where offset to a new page can be stored."""
3040 if not self.complete:
3041 self._seek(-1)
3042 return self._nextpageoffset
3043
3044 def load(self):
3045 """Read all remaining pages from file."""
3046 fh = self.parent.filehandle
3047 keyframe = self._keyframe
3048 pages = self.pages
3049 if not self.complete:
3050 self._seek(-1)
3051 for i, page in enumerate(pages):
3052 if isinstance(page, inttypes):
3053 fh.seek(page)
3054 page = self._tiffpage(self.parent, index=i, keyframe=keyframe)
3055 pages[i] = page
3056
3057 def clear(self, fully=True):
3058 """Delete all but first page from cache. Set keyframe to first page."""
3059 pages = self.pages
3060 if not self._cache or len(pages) < 1:
3061 return
3062 self._keyframe = pages[0]
3063 if fully:
3064 # delete all but first TiffPage/TiffFrame
3065 for i, page in enumerate(pages[1:]):
3066 if not isinstance(page, inttypes):
3067 pages[i + 1] = page.offset
3068 elif TiffFrame is not TiffPage:
3069 # delete only TiffFrames
3070 for i, page in enumerate(pages):
3071 if isinstance(page, TiffFrame):
3072 pages[i] = page.offset
3073
3074 def _seek(self, index, maxpages=2**22):
3075 """Seek file to offset of specified page."""
3076 pages = self.pages
3077 if not pages:
3078 return
3079
3080 fh = self.parent.filehandle
3081 if fh.closed:
3082 raise RuntimeError("FileHandle is closed")
3083
3084 if self.complete or 0 <= index < len(pages):
3085 page = pages[index]
3086 offset = page if isinstance(page, inttypes) else page.offset
3087 fh.seek(offset)
3088 return
3089
3090 offsetformat = self.parent.offsetformat
3091 offsetsize = self.parent.offsetsize
3092 tagnoformat = self.parent.tagnoformat
3093 tagnosize = self.parent.tagnosize
3094 tagsize = self.parent.tagsize
3095 unpack = struct.unpack
3096
3097 page = pages[-1]
3098 offset = page if isinstance(page, inttypes) else page.offset
3099
3100 while len(pages) < maxpages:
3101 # read offsets to pages from file until index is reached
3102 fh.seek(offset)
3103 # skip tags
3104 try:
3105 tagno = unpack(tagnoformat, fh.read(tagnosize))[0]
3106 if tagno > 4096:
3107 raise ValueError("suspicious number of tags")
3108 except Exception:
3109 warnings.warn("corrupted tag list at offset %i" % offset)
3110 del pages[-1]
3111 self.complete = True
3112 break
3113 self._nextpageoffset = offset + tagnosize + tagno * tagsize
3114 fh.seek(self._nextpageoffset)
3115
3116 # read offset to next page
3117 offset = unpack(offsetformat, fh.read(offsetsize))[0]
3118 if offset == 0:
3119 self.complete = True
3120 break
3121 if offset >= fh.size:
3122 warnings.warn("invalid page offset (%i)" % offset)
3123 self.complete = True
3124 break
3125
3126 pages.append(offset)
3127 if 0 <= index < len(pages):
3128 break
3129
3130 if index >= len(pages):
3131 raise IndexError("list index out of range")
3132
3133 page = pages[index]
3134 fh.seek(page if isinstance(page, inttypes) else page.offset)
3135
3136 def __bool__(self):
3137 """Return True if file contains any pages."""
3138 return len(self.pages) > 0
3139
3140 def __len__(self):
3141 """Return number of pages in file."""
3142 if not self.complete:
3143 self._seek(-1)
3144 return len(self.pages)
3145
3146 def __getitem__(self, key):
3147 """Return specified page(s) from cache or file."""
3148 pages = self.pages
3149 if not pages:
3150 raise IndexError("list index out of range")
3151 if key == 0:
3152 return pages[key]
3153
3154 if isinstance(key, slice):
3155 start, stop, _ = key.indices(2**31 - 1)
3156 if not self.complete and max(stop, start) > len(pages):
3157 self._seek(-1)
3158 return [self[i] for i in range(*key.indices(len(pages)))]
3159
3160 if self.complete and key >= len(pages):
3161 raise IndexError("list index out of range")
3162
3163 try:
3164 page = pages[key]
3165 except IndexError:
3166 page = 0
3167 if not isinstance(page, inttypes):
3168 return page
3169
3170 self._seek(key)
3171 page = self._tiffpage(self.parent, index=key, keyframe=self._keyframe)
3172 if self._cache:
3173 pages[key] = page
3174 return page
3175
3176 def __iter__(self):
3177 """Return iterator over all pages."""
3178 i = 0
3179 while True:
3180 try:
3181 yield self[i]
3182 i += 1
3183 except IndexError:
3184 break
3185
3186
3187class TiffPage(object):
3188 """TIFF image file directory (IFD).
3189
3190 Attributes
3191 ----------
3192 index : int
3193 Index of page in file.
3194 dtype : numpy.dtype or None
3195 Data type (native byte order) of the image in IFD.
3196 shape : tuple
3197 Dimensions of the image in IFD.
3198 axes : str
3199 Axes label codes:
3200 'X' width, 'Y' height, 'S' sample, 'I' image series|page|plane,
3201 'Z' depth, 'C' color|em-wavelength|channel, 'E' ex-wavelength|lambda,
3202 'T' time, 'R' region|tile, 'A' angle, 'P' phase, 'H' lifetime,
3203 'L' exposure, 'V' event, 'Q' unknown, '_' missing
3204 tags : dict
3205 Dictionary of tags in IFD. {tag.name: TiffTag}
3206 colormap : numpy.ndarray
3207 Color look up table, if exists.
3208
3209 All attributes are read-only.
3210
3211 Notes
3212 -----
3213 The internal, normalized '_shape' attribute is 6 dimensional:
3214
3215 0 : number planes/images (stk, ij).
3216 1 : planar samplesperpixel.
3217 2 : imagedepth Z (sgi).
3218 3 : imagelength Y.
3219 4 : imagewidth X.
3220 5 : contig samplesperpixel.
3221
3222 """
3223
3224 # default properties; will be updated from tags
3225 imagewidth = 0
3226 imagelength = 0
3227 imagedepth = 1
3228 tilewidth = 0
3229 tilelength = 0
3230 tiledepth = 1
3231 bitspersample = 1
3232 samplesperpixel = 1
3233 sampleformat = 1
3234 rowsperstrip = 2**32 - 1
3235 compression = 1
3236 planarconfig = 1
3237 fillorder = 1
3238 photometric = 0
3239 predictor = 1
3240 extrasamples = 1
3241 colormap = None
3242 software = ""
3243 description = ""
3244 description1 = ""
3245
3246 def __init__(self, parent, index, keyframe=None):
3247 """Initialize instance from file.
3248
3249 The file handle position must be at offset to a valid IFD.
3250
3251 """
3252 self.parent = parent
3253 self.index = index
3254 self.shape = ()
3255 self._shape = ()
3256 self.dtype = None
3257 self._dtype = None
3258 self.axes = ""
3259 self.tags = {}
3260
3261 self.dataoffsets = ()
3262 self.databytecounts = ()
3263
3264 # read TIFF IFD structure and its tags from file
3265 fh = parent.filehandle
3266 self.offset = fh.tell() # offset to this IFD
3267 try:
3268 tagno = struct.unpack(parent.tagnoformat, fh.read(parent.tagnosize))[0]
3269 if tagno > 4096:
3270 raise ValueError("suspicious number of tags")
3271 except Exception:
3272 raise ValueError("corrupted tag list at offset %i" % self.offset)
3273
3274 tagsize = parent.tagsize
3275 data = fh.read(tagsize * tagno)
3276 tags = self.tags
3277 index = -tagsize
3278 for _ in range(tagno):
3279 index += tagsize
3280 try:
3281 tag = TiffTag(self.parent, data[index : index + tagsize])
3282 except TiffTag.Error as e:
3283 warnings.warn(str(e))
3284 continue
3285 tagname = tag.name
3286 if tagname not in tags:
3287 name = tagname
3288 tags[name] = tag
3289 else:
3290 # some files contain multiple tags with same code
3291 # e.g. MicroManager files contain two ImageDescription tags
3292 i = 1
3293 while True:
3294 name = "%s%i" % (tagname, i)
3295 if name not in tags:
3296 tags[name] = tag
3297 break
3298 name = TIFF.TAG_ATTRIBUTES.get(name, "")
3299 if name:
3300 if name[:3] in "sof des" and not isinstance(tag.value, str):
3301 pass # wrong string type for software, description
3302 else:
3303 setattr(self, name, tag.value)
3304
3305 if not tags:
3306 return # found in FIBICS
3307
3308 # consolidate private tags; remove them from self.tags
3309 if self.is_andor:
3310 self.andor_tags
3311 elif self.is_epics:
3312 self.epics_tags
3313
3314 if self.is_lsm or (self.index and self.parent.is_lsm):
3315 # correct non standard LSM bitspersample tags
3316 self.tags["BitsPerSample"]._fix_lsm_bitspersample(self)
3317
3318 if self.is_vista or (self.index and self.parent.is_vista):
3319 # ISS Vista writes wrong ImageDepth tag
3320 self.imagedepth = 1
3321
3322 if self.is_stk and "UIC1tag" in tags and not tags["UIC1tag"].value:
3323 # read UIC1tag now that plane count is known
3324 uic1tag = tags["UIC1tag"]
3325 fh.seek(uic1tag.valueoffset)
3326 tags["UIC1tag"].value = read_uic1tag(
3327 fh,
3328 self.parent.byteorder,
3329 uic1tag.dtype,
3330 uic1tag.count,
3331 None,
3332 tags["UIC2tag"].count,
3333 )
3334
3335 if "IJMetadata" in tags:
3336 # decode IJMetadata tag
3337 try:
3338 tags["IJMetadata"].value = imagej_metadata(
3339 tags["IJMetadata"].value,
3340 tags["IJMetadataByteCounts"].value,
3341 self.parent.byteorder,
3342 )
3343 except Exception as e:
3344 warnings.warn(str(e))
3345
3346 if "BitsPerSample" in tags:
3347 tag = tags["BitsPerSample"]
3348 if tag.count == 1:
3349 self.bitspersample = tag.value
3350 else:
3351 # LSM might list more items than samplesperpixel
3352 value = tag.value[: self.samplesperpixel]
3353 if any((v - value[0] for v in value)):
3354 self.bitspersample = value
3355 else:
3356 self.bitspersample = value[0]
3357
3358 if "SampleFormat" in tags:
3359 tag = tags["SampleFormat"]
3360 if tag.count == 1:
3361 self.sampleformat = tag.value
3362 else:
3363 value = tag.value[: self.samplesperpixel]
3364 if any((v - value[0] for v in value)):
3365 self.sampleformat = value
3366 else:
3367 self.sampleformat = value[0]
3368
3369 if "ImageLength" in tags:
3370 if "RowsPerStrip" not in tags or tags["RowsPerStrip"].count > 1:
3371 self.rowsperstrip = self.imagelength
3372 # self.stripsperimage = int(math.floor(
3373 # float(self.imagelength + self.rowsperstrip - 1) /
3374 # self.rowsperstrip))
3375
3376 # determine dtype
3377 dtype = self.sampleformat, self.bitspersample
3378 dtype = TIFF.SAMPLE_DTYPES.get(dtype, None)
3379 if dtype is not None:
3380 dtype = numpy.dtype(dtype)
3381 self.dtype = self._dtype = dtype
3382
3383 # determine shape of data
3384 imagelength = self.imagelength
3385 imagewidth = self.imagewidth
3386 imagedepth = self.imagedepth
3387 samplesperpixel = self.samplesperpixel
3388
3389 if self.is_stk:
3390 assert self.imagedepth == 1
3391 uictag = tags["UIC2tag"].value
3392 planes = tags["UIC2tag"].count
3393 if self.planarconfig == 1:
3394 self._shape = (planes, 1, 1, imagelength, imagewidth, samplesperpixel)
3395 if samplesperpixel == 1:
3396 self.shape = (planes, imagelength, imagewidth)
3397 self.axes = "YX"
3398 else:
3399 self.shape = (planes, imagelength, imagewidth, samplesperpixel)
3400 self.axes = "YXS"
3401 else:
3402 self._shape = (planes, samplesperpixel, 1, imagelength, imagewidth, 1)
3403 if samplesperpixel == 1:
3404 self.shape = (planes, imagelength, imagewidth)
3405 self.axes = "YX"
3406 else:
3407 self.shape = (planes, samplesperpixel, imagelength, imagewidth)
3408 self.axes = "SYX"
3409 # detect type of series
3410 if planes == 1:
3411 self.shape = self.shape[1:]
3412 elif numpy.all(uictag["ZDistance"] != 0):
3413 self.axes = "Z" + self.axes
3414 elif numpy.all(numpy.diff(uictag["TimeCreated"]) != 0):
3415 self.axes = "T" + self.axes
3416 else:
3417 self.axes = "I" + self.axes
3418 elif self.photometric == 2 or samplesperpixel > 1: # PHOTOMETRIC.RGB
3419 if self.planarconfig == 1:
3420 self._shape = (
3421 1,
3422 1,
3423 imagedepth,
3424 imagelength,
3425 imagewidth,
3426 samplesperpixel,
3427 )
3428 if imagedepth == 1:
3429 self.shape = (imagelength, imagewidth, samplesperpixel)
3430 self.axes = "YXS"
3431 else:
3432 self.shape = (imagedepth, imagelength, imagewidth, samplesperpixel)
3433 self.axes = "ZYXS"
3434 else:
3435 self._shape = (
3436 1,
3437 samplesperpixel,
3438 imagedepth,
3439 imagelength,
3440 imagewidth,
3441 1,
3442 )
3443 if imagedepth == 1:
3444 self.shape = (samplesperpixel, imagelength, imagewidth)
3445 self.axes = "SYX"
3446 else:
3447 self.shape = (samplesperpixel, imagedepth, imagelength, imagewidth)
3448 self.axes = "SZYX"
3449 else:
3450 self._shape = (1, 1, imagedepth, imagelength, imagewidth, 1)
3451 if imagedepth == 1:
3452 self.shape = (imagelength, imagewidth)
3453 self.axes = "YX"
3454 else:
3455 self.shape = (imagedepth, imagelength, imagewidth)
3456 self.axes = "ZYX"
3457
3458 # dataoffsets and databytecounts
3459 if "TileOffsets" in tags:
3460 self.dataoffsets = tags["TileOffsets"].value
3461 elif "StripOffsets" in tags:
3462 self.dataoffsets = tags["StripOffsets"].value
3463 else:
3464 self.dataoffsets = (0,)
3465
3466 if "TileByteCounts" in tags:
3467 self.databytecounts = tags["TileByteCounts"].value
3468 elif "StripByteCounts" in tags:
3469 self.databytecounts = tags["StripByteCounts"].value
3470 else:
3471 self.databytecounts = (product(self.shape) * (self.bitspersample // 8),)
3472 if self.compression != 1:
3473 warnings.warn("required ByteCounts tag is missing")
3474
3475 assert len(self.shape) == len(self.axes)
3476
3477 def asarray(
3478 self,
3479 out=None,
3480 squeeze=True,
3481 lock=None,
3482 reopen=True,
3483 maxsize=2**44,
3484 validate=True,
3485 ):
3486 """Read image data from file and return as numpy array.
3487
3488 Raise ValueError if format is unsupported.
3489
3490 Parameters
3491 ----------
3492 out : numpy.ndarray, str, or file-like object; optional
3493 Buffer where image data will be saved.
3494 If None (default), a new array will be created.
3495 If numpy.ndarray, a writable array of compatible dtype and shape.
3496 If 'memmap', directly memory-map the image data in the TIFF file
3497 if possible; else create a memory-mapped array in a temporary file.
3498 If str or open file, the file name or file object used to
3499 create a memory-map to an array stored in a binary file on disk.
3500 squeeze : bool
3501 If True, all length-1 dimensions (except X and Y) are
3502 squeezed out from the array.
3503 If False, the shape of the returned array might be different from
3504 the page.shape.
3505 lock : {RLock, NullContext}
3506 A reentrant lock used to synchronize reads from file.
3507 If None (default), the lock of the parent's filehandle is used.
3508 reopen : bool
3509 If True (default) and the parent file handle is closed, the file
3510 is temporarily re-opened and closed if no exception occurs.
3511 maxsize: int or None
3512 Maximum size of data before a ValueError is raised.
3513 Can be used to catch DOS. Default: 16 TB.
3514 validate : bool
3515 If True (default), validate various parameters.
3516 If None, only validate parameters and return None.
3517
3518 """
3519 self_ = self
3520 self = self.keyframe # self or keyframe
3521
3522 if not self._shape or product(self._shape) == 0:
3523 return
3524
3525 tags = self.tags
3526
3527 if validate or validate is None:
3528 if maxsize and product(self._shape) > maxsize:
3529 raise ValueError("data are too large %s" % str(self._shape))
3530 if self.dtype is None:
3531 raise ValueError(
3532 "data type not supported: %s%i"
3533 % (self.sampleformat, self.bitspersample)
3534 )
3535 if self.compression not in TIFF.DECOMPESSORS:
3536 raise ValueError("cannot decompress %s" % self.compression.name)
3537 if "SampleFormat" in tags:
3538 tag = tags["SampleFormat"]
3539 if tag.count != 1 and any((i - tag.value[0] for i in tag.value)):
3540 raise ValueError("sample formats do not match %s" % tag.value)
3541 if self.is_chroma_subsampled and (
3542 self.compression != 7 or self.planarconfig == 2
3543 ):
3544 raise NotImplementedError("chroma subsampling not supported")
3545 if validate is None:
3546 return
3547
3548 fh = self_.parent.filehandle
3549 lock = fh.lock if lock is None else lock
3550 with lock:
3551 closed = fh.closed
3552 if closed:
3553 if reopen:
3554 fh.open()
3555 else:
3556 raise IOError("file handle is closed")
3557
3558 dtype = self._dtype
3559 shape = self._shape
3560 imagewidth = self.imagewidth
3561 imagelength = self.imagelength
3562 imagedepth = self.imagedepth
3563 bitspersample = self.bitspersample
3564 typecode = self.parent.byteorder + dtype.char
3565 lsb2msb = self.fillorder == 2
3566 offsets, bytecounts = self_.offsets_bytecounts
3567 istiled = self.is_tiled
3568
3569 if istiled:
3570 tilewidth = self.tilewidth
3571 tilelength = self.tilelength
3572 tiledepth = self.tiledepth
3573 tw = (imagewidth + tilewidth - 1) // tilewidth
3574 tl = (imagelength + tilelength - 1) // tilelength
3575 td = (imagedepth + tiledepth - 1) // tiledepth
3576 shape = (
3577 shape[0],
3578 shape[1],
3579 td * tiledepth,
3580 tl * tilelength,
3581 tw * tilewidth,
3582 shape[-1],
3583 )
3584 tileshape = (tiledepth, tilelength, tilewidth, shape[-1])
3585 runlen = tilewidth
3586 else:
3587 runlen = imagewidth
3588
3589 if self.planarconfig == 1:
3590 runlen *= self.samplesperpixel
3591
3592 if out == "memmap" and self.is_memmappable:
3593 with lock:
3594 result = fh.memmap_array(typecode, shape, offset=offsets[0])
3595 elif self.is_contiguous:
3596 if out is not None:
3597 out = create_output(out, shape, dtype)
3598 with lock:
3599 fh.seek(offsets[0])
3600 result = fh.read_array(typecode, product(shape), out=out)
3601 if out is None and not result.dtype.isnative:
3602 # swap byte order and dtype without copy
3603 result.byteswap(True)
3604 result = result.newbyteorder()
3605 if lsb2msb:
3606 reverse_bitorder(result)
3607 else:
3608 result = create_output(out, shape, dtype)
3609
3610 decompress = TIFF.DECOMPESSORS[self.compression]
3611
3612 if self.compression == 7: # COMPRESSION.JPEG
3613 if bitspersample not in (8, 12):
3614 raise ValueError("unsupported JPEG precision %i" % bitspersample)
3615 if "JPEGTables" in tags:
3616 table = tags["JPEGTables"].value
3617 else:
3618 table = b""
3619 unpack = identityfunc
3620 colorspace = TIFF.PHOTOMETRIC(self.photometric).name
3621
3622 def decompress(
3623 x,
3624 func=decompress,
3625 table=table,
3626 bitspersample=bitspersample,
3627 colorspace=colorspace,
3628 ):
3629 return func(x, table, bitspersample, colorspace).reshape(-1)
3630
3631 elif bitspersample in (8, 16, 32, 64, 128):
3632 if (bitspersample * runlen) % 8:
3633 raise ValueError("data and sample size mismatch")
3634
3635 def unpack(x, typecode=typecode):
3636 if self.predictor == 3: # PREDICTOR.FLOATINGPOINT
3637 # the floating point horizontal differencing decoder
3638 # needs the raw byte order
3639 typecode = dtype.char
3640 try:
3641 # read only numpy array
3642 return numpy.frombuffer(x, typecode)
3643 except ValueError:
3644 # strips may be missing EOI
3645 # warnings.warn('unpack: %s' % e)
3646 xlen = (len(x) // (bitspersample // 8)) * (bitspersample // 8)
3647 return numpy.frombuffer(x[:xlen], typecode)
3648
3649 elif isinstance(bitspersample, tuple):
3650
3651 def unpack(x, typecode=typecode, bitspersample=bitspersample):
3652 return unpack_rgb(x, typecode, bitspersample)
3653
3654 else:
3655
3656 def unpack(
3657 x, typecode=typecode, bitspersample=bitspersample, runlen=runlen
3658 ):
3659 return unpack_ints(x, typecode, bitspersample, runlen)
3660
3661 if istiled:
3662 writable = None
3663 tw, tl, td, pl = 0, 0, 0, 0
3664 for tile in buffered_read(fh, lock, offsets, bytecounts):
3665 if lsb2msb:
3666 tile = reverse_bitorder(tile)
3667 tile = decompress(tile)
3668 tile = unpack(tile)
3669 try:
3670 tile.shape = tileshape
3671 except ValueError:
3672 # incomplete tiles; see gdal issue #1179
3673 warnings.warn("invalid tile data")
3674 t = numpy.zeros(tileshape, dtype).reshape(-1)
3675 s = min(tile.size, t.size)
3676 t[:s] = tile[:s]
3677 tile = t.reshape(tileshape)
3678 if self.predictor == 2: # PREDICTOR.HORIZONTAL
3679 if writable is None:
3680 writable = tile.flags["WRITEABLE"]
3681 if writable:
3682 numpy.cumsum(tile, axis=-2, dtype=dtype, out=tile)
3683 else:
3684 tile = numpy.cumsum(tile, axis=-2, dtype=dtype)
3685 elif self.predictor == 3: # PREDICTOR.FLOATINGPOINT
3686 raise NotImplementedError()
3687 result[
3688 0,
3689 pl,
3690 td : td + tiledepth,
3691 tl : tl + tilelength,
3692 tw : tw + tilewidth,
3693 :,
3694 ] = tile
3695 del tile
3696 tw += tilewidth
3697 if tw >= shape[4]:
3698 tw, tl = 0, tl + tilelength
3699 if tl >= shape[3]:
3700 tl, td = 0, td + tiledepth
3701 if td >= shape[2]:
3702 td, pl = 0, pl + 1
3703 result = result[..., :imagedepth, :imagelength, :imagewidth, :]
3704 else:
3705 strip_size = self.rowsperstrip * self.imagewidth
3706 if self.planarconfig == 1:
3707 strip_size *= self.samplesperpixel
3708 result = result.reshape(-1)
3709 index = 0
3710 for strip in buffered_read(fh, lock, offsets, bytecounts):
3711 if lsb2msb:
3712 strip = reverse_bitorder(strip)
3713 strip = decompress(strip)
3714 strip = unpack(strip)
3715 size = min(result.size, strip.size, strip_size, result.size - index)
3716 result[index : index + size] = strip[:size]
3717 del strip
3718 index += size
3719
3720 result.shape = self._shape
3721
3722 if self.predictor != 1 and not (istiled and not self.is_contiguous):
3723 if self.parent.is_lsm and self.compression == 1:
3724 pass # work around bug in LSM510 software
3725 elif self.predictor == 2: # PREDICTOR.HORIZONTAL
3726 numpy.cumsum(result, axis=-2, dtype=dtype, out=result)
3727 elif self.predictor == 3: # PREDICTOR.FLOATINGPOINT
3728 result = decode_floats(result)
3729
3730 if squeeze:
3731 try:
3732 result.shape = self.shape
3733 except ValueError:
3734 warnings.warn(
3735 "failed to reshape from %s to %s"
3736 % (str(result.shape), str(self.shape))
3737 )
3738
3739 if closed:
3740 # TODO: file should remain open if an exception occurred above
3741 fh.close()
3742 return result
3743
3744 def asrgb(
3745 self,
3746 uint8=False,
3747 alpha=None,
3748 colormap=None,
3749 dmin=None,
3750 dmax=None,
3751 *args,
3752 **kwargs
3753 ):
3754 """Return image data as RGB(A).
3755
3756 Work in progress.
3757
3758 """
3759 data = self.asarray(*args, **kwargs)
3760 self = self.keyframe # self or keyframe
3761 photometric = self.photometric
3762 PHOTOMETRIC = TIFF.PHOTOMETRIC
3763
3764 if photometric == PHOTOMETRIC.PALETTE:
3765 colormap = self.colormap
3766 if colormap.shape[1] < 2**self.bitspersample or self.dtype.char not in "BH":
3767 raise ValueError("cannot apply colormap")
3768 if uint8:
3769 if colormap.max() > 255:
3770 colormap >>= 8
3771 colormap = colormap.astype("uint8")
3772 if "S" in self.axes:
3773 data = data[..., 0] if self.planarconfig == 1 else data[0]
3774 data = apply_colormap(data, colormap)
3775
3776 elif photometric == PHOTOMETRIC.RGB:
3777 if "ExtraSamples" in self.tags:
3778 if alpha is None:
3779 alpha = TIFF.EXTRASAMPLE
3780 extrasamples = self.extrasamples
3781 if self.tags["ExtraSamples"].count == 1:
3782 extrasamples = (extrasamples,)
3783 for i, exs in enumerate(extrasamples):
3784 if exs in alpha:
3785 if self.planarconfig == 1:
3786 data = data[..., [0, 1, 2, 3 + i]]
3787 else:
3788 data = data[:, [0, 1, 2, 3 + i]]
3789 break
3790 else:
3791 if self.planarconfig == 1:
3792 data = data[..., :3]
3793 else:
3794 data = data[:, :3]
3795 # TODO: convert to uint8?
3796
3797 elif photometric == PHOTOMETRIC.MINISBLACK:
3798 raise NotImplementedError()
3799 elif photometric == PHOTOMETRIC.MINISWHITE:
3800 raise NotImplementedError()
3801 elif photometric == PHOTOMETRIC.SEPARATED:
3802 raise NotImplementedError()
3803 else:
3804 raise NotImplementedError()
3805 return data
3806
3807 def aspage(self):
3808 return self
3809
3810 @property
3811 def keyframe(self):
3812 return self
3813
3814 @keyframe.setter
3815 def keyframe(self, index):
3816 return
3817
3818 @lazyattr
3819 def offsets_bytecounts(self):
3820 """Return simplified offsets and bytecounts."""
3821 if self.is_contiguous:
3822 offset, byte_count = self.is_contiguous
3823 return [offset], [byte_count]
3824 return clean_offsets_counts(self.dataoffsets, self.databytecounts)
3825
3826 @lazyattr
3827 def is_contiguous(self):
3828 """Return offset and size of contiguous data, else None.
3829
3830 Excludes prediction and fill_order.
3831
3832 """
3833 if self.compression != 1 or self.bitspersample not in (8, 16, 32, 64):
3834 return
3835 if "TileWidth" in self.tags:
3836 if (
3837 self.imagewidth != self.tilewidth
3838 or self.imagelength % self.tilelength
3839 or self.tilewidth % 16
3840 or self.tilelength % 16
3841 ):
3842 return
3843 if (
3844 "ImageDepth" in self.tags
3845 and "TileDepth" in self.tags
3846 and (
3847 self.imagelength != self.tilelength
3848 or self.imagedepth % self.tiledepth
3849 )
3850 ):
3851 return
3852
3853 offsets = self.dataoffsets
3854 bytecounts = self.databytecounts
3855 if len(offsets) == 1:
3856 return offsets[0], bytecounts[0]
3857 if self.is_stk or all(
3858 (
3859 offsets[i] + bytecounts[i] == offsets[i + 1] or bytecounts[i + 1] == 0
3860 ) # no data/ignore offset
3861 for i in range(len(offsets) - 1)
3862 ):
3863 return offsets[0], sum(bytecounts)
3864
3865 @lazyattr
3866 def is_final(self):
3867 """Return if page's image data are stored in final form.
3868
3869 Excludes byte-swapping.
3870
3871 """
3872 return (
3873 self.is_contiguous
3874 and self.fillorder == 1
3875 and self.predictor == 1
3876 and not self.is_chroma_subsampled
3877 )
3878
3879 @lazyattr
3880 def is_memmappable(self):
3881 """Return if page's image data in file can be memory-mapped."""
3882 return (
3883 self.parent.filehandle.is_file
3884 and self.is_final
3885 and
3886 # (self.bitspersample == 8 or self.parent.isnative) and
3887 self.is_contiguous[0] % self.dtype.itemsize == 0
3888 ) # aligned?
3889
3890 def __str__(self, detail=0, width=79):
3891 """Return string containing information about page."""
3892 if self.keyframe != self:
3893 return TiffFrame.__str__(self, detail)
3894 attr = ""
3895 for name in ("memmappable", "final", "contiguous"):
3896 attr = getattr(self, "is_" + name)
3897 if attr:
3898 attr = name.upper()
3899 break
3900 info = " ".join(
3901 s
3902 for s in (
3903 "x".join(str(i) for i in self.shape),
3904 "%s%s"
3905 % (TIFF.SAMPLEFORMAT(self.sampleformat).name, self.bitspersample),
3906 "|".join(
3907 i
3908 for i in (
3909 TIFF.PHOTOMETRIC(self.photometric).name,
3910 "TILED" if self.is_tiled else "",
3911 self.compression.name if self.compression != 1 else "",
3912 self.planarconfig.name if self.planarconfig != 1 else "",
3913 self.predictor.name if self.predictor != 1 else "",
3914 self.fillorder.name if self.fillorder != 1 else "",
3915 )
3916 if i
3917 ),
3918 attr,
3919 "|".join((f.upper() for f in self.flags)),
3920 )
3921 if s
3922 )
3923 info = "TiffPage %i @%i %s" % (self.index, self.offset, info)
3924 if detail <= 0:
3925 return info
3926 info = [info]
3927 tags = self.tags
3928 tlines = []
3929 vlines = []
3930 for tag in sorted(tags.values(), key=lambda x: x.code):
3931 value = tag.__str__(width=width + 1)
3932 tlines.append(value[:width].strip())
3933 if detail > 1 and len(value) > width:
3934 name = tag.name.upper()
3935 if detail <= 2 and ("COUNTS" in name or "OFFSETS" in name):
3936 value = pformat(tag.value, width=width, height=detail * 4)
3937 else:
3938 value = pformat(tag.value, width=width, height=detail * 12)
3939 vlines.append("%s\n%s" % (tag.name, value))
3940 info.append("\n".join(tlines))
3941 if detail > 1:
3942 info.append("\n\n".join(vlines))
3943 if detail > 3:
3944 try:
3945 info.append(
3946 "DATA\n%s" % pformat(self.asarray(), width=width, height=detail * 8)
3947 )
3948 except Exception:
3949 pass
3950 return "\n\n".join(info)
3951
3952 @lazyattr
3953 def flags(self):
3954 """Return set of flags."""
3955 return set(
3956 (
3957 name.lower()
3958 for name in sorted(TIFF.FILE_FLAGS)
3959 if getattr(self, "is_" + name)
3960 )
3961 )
3962
3963 @property
3964 def ndim(self):
3965 """Return number of array dimensions."""
3966 return len(self.shape)
3967
3968 @property
3969 def size(self):
3970 """Return number of elements in array."""
3971 return product(self.shape)
3972
3973 @lazyattr
3974 def andor_tags(self):
3975 """Return consolidated metadata from Andor tags as dict.
3976
3977 Remove Andor tags from self.tags.
3978
3979 """
3980 if not self.is_andor:
3981 return
3982 tags = self.tags
3983 result = {"Id": tags["AndorId"].value}
3984 for tag in list(self.tags.values()):
3985 code = tag.code
3986 if not 4864 < code < 5031:
3987 continue
3988 value = tag.value
3989 name = tag.name[5:] if len(tag.name) > 5 else tag.name
3990 result[name] = value
3991 del tags[tag.name]
3992 return result
3993
3994 @lazyattr
3995 def epics_tags(self):
3996 """Return consolidated metadata from EPICS areaDetector tags as dict.
3997
3998 Remove areaDetector tags from self.tags.
3999
4000 """
4001 if not self.is_epics:
4002 return
4003 result = {}
4004 tags = self.tags
4005 for tag in list(self.tags.values()):
4006 code = tag.code
4007 if not 65000 <= code < 65500:
4008 continue
4009 value = tag.value
4010 if code == 65000:
4011 result["timeStamp"] = datetime.datetime.fromtimestamp(float(value))
4012 elif code == 65001:
4013 result["uniqueID"] = int(value)
4014 elif code == 65002:
4015 result["epicsTSSec"] = int(value)
4016 elif code == 65003:
4017 result["epicsTSNsec"] = int(value)
4018 else:
4019 key, value = value.split(":", 1)
4020 result[key] = astype(value)
4021 del tags[tag.name]
4022 return result
4023
4024 @lazyattr
4025 def geotiff_tags(self):
4026 """Return consolidated metadata from GeoTIFF tags as dict."""
4027 if not self.is_geotiff:
4028 return
4029 tags = self.tags
4030
4031 gkd = tags["GeoKeyDirectoryTag"].value
4032 if gkd[0] != 1:
4033 warnings.warn("invalid GeoKeyDirectoryTag")
4034 return {}
4035
4036 result = {
4037 "KeyDirectoryVersion": gkd[0],
4038 "KeyRevision": gkd[1],
4039 "KeyRevisionMinor": gkd[2],
4040 # 'NumberOfKeys': gkd[3],
4041 }
4042 # deltags = ['GeoKeyDirectoryTag']
4043 geokeys = TIFF.GEO_KEYS
4044 geocodes = TIFF.GEO_CODES
4045 for index in range(gkd[3]):
4046 keyid, tagid, count, offset = gkd[4 + index * 4 : index * 4 + 8]
4047 keyid = geokeys.get(keyid, keyid)
4048 if tagid == 0:
4049 value = offset
4050 else:
4051 tagname = TIFF.TAGS[tagid]
4052 # deltags.append(tagname)
4053 value = tags[tagname].value[offset : offset + count]
4054 if tagid == 34737 and count > 1 and value[-1] == "|":
4055 value = value[:-1]
4056 value = value if count > 1 else value[0]
4057 if keyid in geocodes:
4058 try:
4059 value = geocodes[keyid](value)
4060 except Exception:
4061 pass
4062 result[keyid] = value
4063
4064 if "IntergraphMatrixTag" in tags:
4065 value = tags["IntergraphMatrixTag"].value
4066 value = numpy.array(value)
4067 if len(value) == 16:
4068 value = value.reshape((4, 4)).tolist()
4069 result["IntergraphMatrix"] = value
4070 if "ModelPixelScaleTag" in tags:
4071 value = numpy.array(tags["ModelPixelScaleTag"].value).tolist()
4072 result["ModelPixelScale"] = value
4073 if "ModelTiepointTag" in tags:
4074 value = tags["ModelTiepointTag"].value
4075 value = numpy.array(value).reshape((-1, 6)).squeeze().tolist()
4076 result["ModelTiepoint"] = value
4077 if "ModelTransformationTag" in tags:
4078 value = tags["ModelTransformationTag"].value
4079 value = numpy.array(value).reshape((4, 4)).tolist()
4080 result["ModelTransformation"] = value
4081 elif False:
4082 # if 'ModelPixelScaleTag' in tags and 'ModelTiepointTag' in tags:
4083 sx, sy, sz = tags["ModelPixelScaleTag"].value
4084 tiepoints = tags["ModelTiepointTag"].value
4085 transforms = []
4086 for tp in range(0, len(tiepoints), 6):
4087 i, j, k, x, y, z = tiepoints[tp : tp + 6]
4088 transforms.append(
4089 [
4090 [sx, 0.0, 0.0, x - i * sx],
4091 [0.0, -sy, 0.0, y + j * sy],
4092 [0.0, 0.0, sz, z - k * sz],
4093 [0.0, 0.0, 0.0, 1.0],
4094 ]
4095 )
4096 if len(tiepoints) == 6:
4097 transforms = transforms[0]
4098 result["ModelTransformation"] = transforms
4099
4100 if "RPCCoefficientTag" in tags:
4101 rpcc = tags["RPCCoefficientTag"].value
4102 result["RPCCoefficient"] = {
4103 "ERR_BIAS": rpcc[0],
4104 "ERR_RAND": rpcc[1],
4105 "LINE_OFF": rpcc[2],
4106 "SAMP_OFF": rpcc[3],
4107 "LAT_OFF": rpcc[4],
4108 "LONG_OFF": rpcc[5],
4109 "HEIGHT_OFF": rpcc[6],
4110 "LINE_SCALE": rpcc[7],
4111 "SAMP_SCALE": rpcc[8],
4112 "LAT_SCALE": rpcc[9],
4113 "LONG_SCALE": rpcc[10],
4114 "HEIGHT_SCALE": rpcc[11],
4115 "LINE_NUM_COEFF": rpcc[12:33],
4116 "LINE_DEN_COEFF ": rpcc[33:53],
4117 "SAMP_NUM_COEFF": rpcc[53:73],
4118 "SAMP_DEN_COEFF": rpcc[73:],
4119 }
4120
4121 return result
4122
4123 @property
4124 def is_tiled(self):
4125 """Page contains tiled image."""
4126 return "TileWidth" in self.tags
4127
4128 @property
4129 def is_reduced(self):
4130 """Page is reduced image of another image."""
4131 return "NewSubfileType" in self.tags and self.tags["NewSubfileType"].value & 1
4132
4133 @property
4134 def is_chroma_subsampled(self):
4135 """Page contains chroma subsampled image."""
4136 return "YCbCrSubSampling" in self.tags and self.tags[
4137 "YCbCrSubSampling"
4138 ].value != (1, 1)
4139
4140 @lazyattr
4141 def is_imagej(self):
4142 """Return ImageJ description if exists, else None."""
4143 for description in (self.description, self.description1):
4144 if not description:
4145 return
4146 if description[:7] == "ImageJ=":
4147 return description
4148
4149 @lazyattr
4150 def is_shaped(self):
4151 """Return description containing array shape if exists, else None."""
4152 for description in (self.description, self.description1):
4153 if not description:
4154 return
4155 if description[:1] == "{" and '"shape":' in description:
4156 return description
4157 if description[:6] == "shape=":
4158 return description
4159
4160 @property
4161 def is_mdgel(self):
4162 """Page contains MDFileTag tag."""
4163 return "MDFileTag" in self.tags
4164
4165 @property
4166 def is_mediacy(self):
4167 """Page contains Media Cybernetics Id tag."""
4168 return "MC_Id" in self.tags and self.tags["MC_Id"].value[:7] == b"MC TIFF"
4169
4170 @property
4171 def is_stk(self):
4172 """Page contains UIC2Tag tag."""
4173 return "UIC2tag" in self.tags
4174
4175 @property
4176 def is_lsm(self):
4177 """Page contains CZ_LSMINFO tag."""
4178 return "CZ_LSMINFO" in self.tags
4179
4180 @property
4181 def is_fluoview(self):
4182 """Page contains FluoView MM_STAMP tag."""
4183 return "MM_Stamp" in self.tags
4184
4185 @property
4186 def is_nih(self):
4187 """Page contains NIH image header."""
4188 return "NIHImageHeader" in self.tags
4189
4190 @property
4191 def is_sgi(self):
4192 """Page contains SGI image and tile depth tags."""
4193 return "ImageDepth" in self.tags and "TileDepth" in self.tags
4194
4195 @property
4196 def is_vista(self):
4197 """Software tag is 'ISS Vista'."""
4198 return self.software == "ISS Vista"
4199
4200 @property
4201 def is_metaseries(self):
4202 """Page contains MDS MetaSeries metadata in ImageDescription tag."""
4203 if self.index > 1 or self.software != "MetaSeries":
4204 return False
4205 d = self.description
4206 return d.startswith("<MetaData>") and d.endswith("</MetaData>")
4207
4208 @property
4209 def is_ome(self):
4210 """Page contains OME-XML in ImageDescription tag."""
4211 if self.index > 1 or not self.description:
4212 return False
4213 d = self.description
4214 return d[:14] == "<?xml version=" and d[-6:] == "</OME>"
4215
4216 @property
4217 def is_scn(self):
4218 """Page contains Leica SCN XML in ImageDescription tag."""
4219 if self.index > 1 or not self.description:
4220 return False
4221 d = self.description
4222 return d[:14] == "<?xml version=" and d[-6:] == "</scn>"
4223
4224 @property
4225 def is_micromanager(self):
4226 """Page contains Micro-Manager metadata."""
4227 return "MicroManagerMetadata" in self.tags
4228
4229 @property
4230 def is_andor(self):
4231 """Page contains Andor Technology tags."""
4232 return "AndorId" in self.tags
4233
4234 @property
4235 def is_pilatus(self):
4236 """Page contains Pilatus tags."""
4237 return self.software[:8] == "TVX TIFF" and self.description[:2] == "# "
4238
4239 @property
4240 def is_epics(self):
4241 """Page contains EPICS areaDetector tags."""
4242 return (
4243 self.description == "EPICS areaDetector"
4244 or self.software == "EPICS areaDetector"
4245 )
4246
4247 @property
4248 def is_tvips(self):
4249 """Page contains TVIPS metadata."""
4250 return "TVIPS" in self.tags
4251
4252 @property
4253 def is_fei(self):
4254 """Page contains SFEG or HELIOS metadata."""
4255 return "FEI_SFEG" in self.tags or "FEI_HELIOS" in self.tags
4256
4257 @property
4258 def is_sem(self):
4259 """Page contains Zeiss SEM metadata."""
4260 return "CZ_SEM" in self.tags
4261
4262 @property
4263 def is_svs(self):
4264 """Page contains Aperio metadata."""
4265 return self.description[:20] == "Aperio Image Library"
4266
4267 @property
4268 def is_scanimage(self):
4269 """Page contains ScanImage metadata."""
4270 return (
4271 self.description[:12] == "state.config"
4272 or self.software[:22] == "SI.LINE_FORMAT_VERSION"
4273 or "scanimage.SI." in self.description[-256:]
4274 )
4275
4276 @property
4277 def is_qptiff(self):
4278 """Page contains PerkinElmer tissue images metadata."""
4279 # The ImageDescription tag contains XML with a top-level
4280 # <PerkinElmer-QPI-ImageDescription> element
4281 return self.software[:15] == "PerkinElmer-QPI"
4282
4283 @property
4284 def is_geotiff(self):
4285 """Page contains GeoTIFF metadata."""
4286 return "GeoKeyDirectoryTag" in self.tags
4287
4288
4289class TiffFrame(object):
4290 """Lightweight TIFF image file directory (IFD).
4291
4292 Only a limited number of tag values are read from file, e.g. StripOffsets,
4293 and StripByteCounts. Other tag values are assumed to be identical with a
4294 specified TiffPage instance, the keyframe.
4295
4296 TiffFrame is intended to reduce resource usage and speed up reading data
4297 from file, not for introspection of metadata.
4298
4299 Not compatible with Python 2.
4300
4301 """
4302
4303 __slots__ = (
4304 "keyframe",
4305 "parent",
4306 "index",
4307 "offset",
4308 "dataoffsets",
4309 "databytecounts",
4310 )
4311
4312 is_mdgel = False
4313 tags = {}
4314
4315 def __init__(self, parent, index, keyframe):
4316 """Read specified tags from file.
4317
4318 The file handle position must be at the offset to a valid IFD.
4319
4320 """
4321 self.keyframe = keyframe
4322 self.parent = parent
4323 self.index = index
4324 self.dataoffsets = None
4325 self.databytecounts = None
4326
4327 unpack = struct.unpack
4328 fh = parent.filehandle
4329 self.offset = fh.tell()
4330 try:
4331 tagno = unpack(parent.tagnoformat, fh.read(parent.tagnosize))[0]
4332 if tagno > 4096:
4333 raise ValueError("suspicious number of tags")
4334 except Exception:
4335 raise ValueError("corrupted page list at offset %i" % self.offset)
4336
4337 # tags = {}
4338 tagcodes = {273, 279, 324, 325} # TIFF.FRAME_TAGS
4339 tagsize = parent.tagsize
4340 codeformat = parent.tagformat1[:2]
4341
4342 data = fh.read(tagsize * tagno)
4343 index = -tagsize
4344 for _ in range(tagno):
4345 index += tagsize
4346 code = unpack(codeformat, data[index : index + 2])[0]
4347 if code not in tagcodes:
4348 continue
4349 try:
4350 tag = TiffTag(parent, data[index : index + tagsize])
4351 except TiffTag.Error as e:
4352 warnings.warn(str(e))
4353 continue
4354 if code == 273 or code == 324:
4355 setattr(self, "dataoffsets", tag.value)
4356 elif code == 279 or code == 325:
4357 setattr(self, "databytecounts", tag.value)
4358 # elif code == 270:
4359 # tagname = tag.name
4360 # if tagname not in tags:
4361 # tags[tagname] = bytes2str(tag.value)
4362 # elif 'ImageDescription1' not in tags:
4363 # tags['ImageDescription1'] = bytes2str(tag.value)
4364 # else:
4365 # tags[tag.name] = tag.value
4366
4367 def aspage(self):
4368 """Return TiffPage from file."""
4369 self.parent.filehandle.seek(self.offset)
4370 return TiffPage(self.parent, index=self.index, keyframe=None)
4371
4372 def asarray(self, *args, **kwargs):
4373 """Read image data from file and return as numpy array."""
4374 # TODO: fix TypeError on Python 2
4375 # "TypeError: unbound method asarray() must be called with TiffPage
4376 # instance as first argument (got TiffFrame instance instead)"
4377 kwargs["validate"] = False
4378 return TiffPage.asarray(self, *args, **kwargs)
4379
4380 def asrgb(self, *args, **kwargs):
4381 """Read image data from file and return RGB image as numpy array."""
4382 kwargs["validate"] = False
4383 return TiffPage.asrgb(self, *args, **kwargs)
4384
4385 @property
4386 def offsets_bytecounts(self):
4387 """Return simplified offsets and bytecounts."""
4388 if self.keyframe.is_contiguous:
4389 return self.dataoffsets[:1], self.keyframe.is_contiguous[1:]
4390 return clean_offsets_counts(self.dataoffsets, self.databytecounts)
4391
4392 @property
4393 def is_contiguous(self):
4394 """Return offset and size of contiguous data, else None."""
4395 if self.keyframe.is_contiguous:
4396 return self.dataoffsets[0], self.keyframe.is_contiguous[1]
4397
4398 @property
4399 def is_memmappable(self):
4400 """Return if page's image data in file can be memory-mapped."""
4401 return self.keyframe.is_memmappable
4402
4403 def __getattr__(self, name):
4404 """Return attribute from keyframe."""
4405 if name in TIFF.FRAME_ATTRS:
4406 return getattr(self.keyframe, name)
4407 # this error could be raised because an AttributeError was
4408 # raised inside a @property function
4409 raise AttributeError(
4410 "'%s' object has no attribute '%s'" % (self.__class__.__name__, name)
4411 )
4412
4413 def __str__(self, detail=0):
4414 """Return string containing information about frame."""
4415 info = " ".join(
4416 s for s in ("x".join(str(i) for i in self.shape), str(self.dtype))
4417 )
4418 return "TiffFrame %i @%i %s" % (self.index, self.offset, info)
4419
4420
4421class TiffTag(object):
4422 """TIFF tag structure.
4423
4424 Attributes
4425 ----------
4426 name : string
4427 Name of tag.
4428 code : int
4429 Decimal code of tag.
4430 dtype : str
4431 Datatype of tag data. One of TIFF DATA_FORMATS.
4432 count : int
4433 Number of values.
4434 value : various types
4435 Tag data as Python object.
4436 ImageSourceData : int
4437 Location of value in file.
4438
4439 All attributes are read-only.
4440
4441 """
4442
4443 __slots__ = ("code", "count", "dtype", "value", "valueoffset")
4444
4445 class Error(Exception):
4446 pass
4447
4448 def __init__(self, parent, tagheader, **kwargs):
4449 """Initialize instance from tag header."""
4450 fh = parent.filehandle
4451 byteorder = parent.byteorder
4452 unpack = struct.unpack
4453 offsetsize = parent.offsetsize
4454
4455 self.valueoffset = fh.tell() + offsetsize + 4
4456 code, type_ = unpack(parent.tagformat1, tagheader[:4])
4457 count, value = unpack(parent.tagformat2, tagheader[4:])
4458
4459 try:
4460 dtype = TIFF.DATA_FORMATS[type_]
4461 except KeyError:
4462 raise TiffTag.Error("unknown tag data type %i" % type_)
4463
4464 fmt = "%s%i%s" % (byteorder, count * int(dtype[0]), dtype[1])
4465 size = struct.calcsize(fmt)
4466 if size > offsetsize or code in TIFF.TAG_READERS:
4467 self.valueoffset = offset = unpack(parent.offsetformat, value)[0]
4468 if offset < 8 or offset > fh.size - size:
4469 raise TiffTag.Error("invalid tag value offset")
4470 # if offset % 2:
4471 # warnings.warn('tag value does not begin on word boundary')
4472 fh.seek(offset)
4473 if code in TIFF.TAG_READERS:
4474 readfunc = TIFF.TAG_READERS[code]
4475 value = readfunc(fh, byteorder, dtype, count, offsetsize)
4476 elif type_ == 7 or (count > 1 and dtype[-1] == "B"):
4477 value = read_bytes(fh, byteorder, dtype, count, offsetsize)
4478 elif code in TIFF.TAGS or dtype[-1] == "s":
4479 value = unpack(fmt, fh.read(size))
4480 else:
4481 value = read_numpy(fh, byteorder, dtype, count, offsetsize)
4482 elif dtype[-1] == "B" or type_ == 7:
4483 value = value[:size]
4484 else:
4485 value = unpack(fmt, value[:size])
4486
4487 process = (
4488 code not in TIFF.TAG_READERS and code not in TIFF.TAG_TUPLE and type_ != 7
4489 )
4490 if process and dtype[-1] == "s" and isinstance(value[0], bytes):
4491 # TIFF ASCII fields can contain multiple strings,
4492 # each terminated with a NUL
4493 value = value[0]
4494 try:
4495 value = bytes2str(stripascii(value).strip())
4496 except UnicodeDecodeError:
4497 warnings.warn("tag %i: coercing invalid ASCII to bytes" % code)
4498 dtype = "1B"
4499 else:
4500 if code in TIFF.TAG_ENUM:
4501 t = TIFF.TAG_ENUM[code]
4502 try:
4503 value = tuple(t(v) for v in value)
4504 except ValueError as e:
4505 warnings.warn(str(e))
4506 if process:
4507 if len(value) == 1:
4508 value = value[0]
4509
4510 self.code = code
4511 self.dtype = dtype
4512 self.count = count
4513 self.value = value
4514
4515 @property
4516 def name(self):
4517 return TIFF.TAGS.get(self.code, str(self.code))
4518
4519 def _fix_lsm_bitspersample(self, parent):
4520 """Correct LSM bitspersample tag.
4521
4522 Old LSM writers may use a separate region for two 16-bit values,
4523 although they fit into the tag value element of the tag.
4524
4525 """
4526 if self.code == 258 and self.count == 2:
4527 # TODO: test this case; need example file
4528 warnings.warn("correcting LSM bitspersample tag")
4529 tof = parent.offsetformat[parent.offsetsize]
4530 self.valueoffset = struct.unpack(tof, self._value)[0]
4531 parent.filehandle.seek(self.valueoffset)
4532 self.value = struct.unpack("<HH", parent.filehandle.read(4))
4533
4534 def __str__(self, detail=0, width=79):
4535 """Return string containing information about tag."""
4536 height = 1 if detail <= 0 else 8 * detail
4537 tcode = "%i%s" % (self.count * int(self.dtype[0]), self.dtype[1])
4538 line = (
4539 "TiffTag %i %s %s @%i "
4540 % (self.code, self.name, tcode, self.valueoffset)[:width]
4541 )
4542
4543 if self.code in TIFF.TAG_ENUM:
4544 if self.count == 1:
4545 value = TIFF.TAG_ENUM[self.code](self.value).name
4546 else:
4547 value = pformat(tuple(v.name for v in self.value))
4548 else:
4549 value = pformat(self.value, width=width, height=height)
4550
4551 if detail <= 0:
4552 line += value
4553 line = line[:width]
4554 else:
4555 line += "\n" + value
4556 return line
4557
4558
4559# Added to produce cleaner exceptions if tifffile unexpectedly fails to open the
4560# file. See this comment (and the following) for details:
4561# https://github.com/imageio/imageio/commit/bdbe699bbcda4223b0b6bd4d7474f84bbe34af09#r64068747
4562class TiffFileError(ValueError):
4563 pass
4564
4565
4566class TiffPageSeries(object):
4567 """Series of TIFF pages with compatible shape and data type.
4568
4569 Attributes
4570 ----------
4571 pages : list of TiffPage
4572 Sequence of TiffPages in series.
4573 dtype : numpy.dtype
4574 Data type (native byte order) of the image array in series.
4575 shape : tuple
4576 Dimensions of the image array in series.
4577 axes : str
4578 Labels of axes in shape. See TiffPage.axes.
4579 offset : int or None
4580 Position of image data in file if memory-mappable, else None.
4581
4582 """
4583
4584 def __init__(
4585 self,
4586 pages,
4587 shape,
4588 dtype,
4589 axes,
4590 parent=None,
4591 name=None,
4592 transform=None,
4593 stype=None,
4594 truncated=False,
4595 ):
4596 """Initialize instance."""
4597 self.index = 0
4598 self._pages = pages # might contain only first of contiguous pages
4599 self.shape = tuple(shape)
4600 self.axes = "".join(axes)
4601 self.dtype = numpy.dtype(dtype)
4602 self.stype = stype if stype else ""
4603 self.name = name if name else ""
4604 self.transform = transform
4605 if parent:
4606 self.parent = parent
4607 elif pages:
4608 self.parent = pages[0].parent
4609 else:
4610 self.parent = None
4611 if len(pages) == 1 and not truncated:
4612 self._len = int(product(self.shape) // product(pages[0].shape))
4613 else:
4614 self._len = len(pages)
4615
4616 def asarray(self, out=None):
4617 """Return image data from series of TIFF pages as numpy array."""
4618 if self.parent:
4619 result = self.parent.asarray(series=self, out=out)
4620 if self.transform is not None:
4621 result = self.transform(result)
4622 return result
4623
4624 @lazyattr
4625 def offset(self):
4626 """Return offset to series data in file, if any."""
4627 if not self._pages:
4628 return
4629
4630 pos = 0
4631 for page in self._pages:
4632 if page is None:
4633 return
4634 if not page.is_final:
4635 return
4636 if not pos:
4637 pos = page.is_contiguous[0] + page.is_contiguous[1]
4638 continue
4639 if pos != page.is_contiguous[0]:
4640 return
4641 pos += page.is_contiguous[1]
4642
4643 page = self._pages[0]
4644 offset = page.is_contiguous[0]
4645 if (page.is_imagej or page.is_shaped) and len(self._pages) == 1:
4646 # truncated files
4647 return offset
4648 if pos == offset + product(self.shape) * self.dtype.itemsize:
4649 return offset
4650
4651 @property
4652 def ndim(self):
4653 """Return number of array dimensions."""
4654 return len(self.shape)
4655
4656 @property
4657 def size(self):
4658 """Return number of elements in array."""
4659 return int(product(self.shape))
4660
4661 @property
4662 def pages(self):
4663 """Return sequence of all pages in series."""
4664 # a workaround to keep the old interface working
4665 return self
4666
4667 def __len__(self):
4668 """Return number of TiffPages in series."""
4669 return self._len
4670
4671 def __getitem__(self, key):
4672 """Return specified TiffPage."""
4673 if len(self._pages) == 1 and 0 < key < self._len:
4674 index = self._pages[0].index
4675 return self.parent.pages[index + key]
4676 return self._pages[key]
4677
4678 def __iter__(self):
4679 """Return iterator over TiffPages in series."""
4680 if len(self._pages) == self._len:
4681 for page in self._pages:
4682 yield page
4683 else:
4684 pages = self.parent.pages
4685 index = self._pages[0].index
4686 for i in range(self._len):
4687 yield pages[index + i]
4688
4689 def __str__(self):
4690 """Return string with information about series."""
4691 s = " ".join(
4692 s
4693 for s in (
4694 snipstr("'%s'" % self.name, 20) if self.name else "",
4695 "x".join(str(i) for i in self.shape),
4696 str(self.dtype),
4697 self.axes,
4698 self.stype,
4699 "%i Pages" % len(self.pages),
4700 ("Offset=%i" % self.offset) if self.offset else "",
4701 )
4702 if s
4703 )
4704 return "TiffPageSeries %i %s" % (self.index, s)
4705
4706
4707class TiffSequence(object):
4708 """Sequence of TIFF files.
4709
4710 The image data in all files must match shape, dtype, etc.
4711
4712 Attributes
4713 ----------
4714 files : list
4715 List of file names.
4716 shape : tuple
4717 Shape of image sequence. Excludes shape of image array.
4718 axes : str
4719 Labels of axes in shape.
4720
4721 Examples
4722 --------
4723 >>> # read image stack from sequence of TIFF files
4724 >>> imsave('temp_C001T001.tif', numpy.random.rand(64, 64))
4725 >>> imsave('temp_C001T002.tif', numpy.random.rand(64, 64))
4726 >>> tifs = TiffSequence('temp_C001*.tif')
4727 >>> tifs.shape
4728 (1, 2)
4729 >>> tifs.axes
4730 'CT'
4731 >>> data = tifs.asarray()
4732 >>> data.shape
4733 (1, 2, 64, 64)
4734
4735 """
4736
4737 _patterns = {
4738 "axes": r"""
4739 # matches Olympus OIF and Leica TIFF series
4740 _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))
4741 _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
4742 _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
4743 _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
4744 _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
4745 _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
4746 _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
4747 """
4748 }
4749
4750 class ParseError(Exception):
4751 pass
4752
4753 def __init__(self, files, imread=TiffFile, pattern="axes", *args, **kwargs):
4754 """Initialize instance from multiple files.
4755
4756 Parameters
4757 ----------
4758 files : str, pathlib.Path, or sequence thereof
4759 Glob pattern or sequence of file names.
4760 Binary streams are not supported.
4761 imread : function or class
4762 Image read function or class with asarray function returning numpy
4763 array from single file.
4764 pattern : str
4765 Regular expression pattern that matches axes names and sequence
4766 indices in file names.
4767 By default, the pattern matches Olympus OIF and Leica TIFF series.
4768
4769 """
4770 if isinstance(files, pathlib.Path):
4771 files = str(files)
4772 if isinstance(files, basestring):
4773 files = natural_sorted(glob.glob(files))
4774 files = list(files)
4775 if not files:
4776 raise ValueError("no files found")
4777 if isinstance(files[0], pathlib.Path):
4778 files = [str(pathlib.Path(f)) for f in files]
4779 elif not isinstance(files[0], basestring):
4780 raise ValueError("not a file name")
4781 self.files = files
4782
4783 if hasattr(imread, "asarray"):
4784 # redefine imread
4785 _imread = imread
4786
4787 def imread(fname, *args, **kwargs):
4788 with _imread(fname) as im:
4789 return im.asarray(*args, **kwargs)
4790
4791 self.imread = imread
4792
4793 self.pattern = self._patterns.get(pattern, pattern)
4794 try:
4795 self._parse()
4796 if not self.axes:
4797 self.axes = "I"
4798 except self.ParseError:
4799 self.axes = "I"
4800 self.shape = (len(files),)
4801 self._startindex = (0,)
4802 self._indices = tuple((i,) for i in range(len(files)))
4803
4804 def __str__(self):
4805 """Return string with information about image sequence."""
4806 return "\n".join(
4807 [
4808 self.files[0],
4809 " size: %i" % len(self.files),
4810 " axes: %s" % self.axes,
4811 " shape: %s" % str(self.shape),
4812 ]
4813 )
4814
4815 def __len__(self):
4816 return len(self.files)
4817
4818 def __enter__(self):
4819 return self
4820
4821 def __exit__(self, exc_type, exc_value, traceback):
4822 self.close()
4823
4824 def close(self):
4825 pass
4826
4827 def asarray(self, out=None, *args, **kwargs):
4828 """Read image data from all files and return as numpy array.
4829
4830 The args and kwargs parameters are passed to the imread function.
4831
4832 Raise IndexError or ValueError if image shapes do not match.
4833
4834 """
4835 im = self.imread(self.files[0], *args, **kwargs)
4836 shape = self.shape + im.shape
4837 result = create_output(out, shape, dtype=im.dtype)
4838 result = result.reshape(-1, *im.shape)
4839 for index, fname in zip(self._indices, self.files):
4840 index = [i - j for i, j in zip(index, self._startindex)]
4841 index = numpy.ravel_multi_index(index, self.shape)
4842 im = self.imread(fname, *args, **kwargs)
4843 result[index] = im
4844 result.shape = shape
4845 return result
4846
4847 def _parse(self):
4848 """Get axes and shape from file names."""
4849 if not self.pattern:
4850 raise self.ParseError("invalid pattern")
4851 pattern = re.compile(self.pattern, re.IGNORECASE | re.VERBOSE)
4852 matches = pattern.findall(self.files[0])
4853 if not matches:
4854 raise self.ParseError("pattern does not match file names")
4855 matches = matches[-1]
4856 if len(matches) % 2:
4857 raise self.ParseError("pattern does not match axis name and index")
4858 axes = "".join(m for m in matches[::2] if m)
4859 if not axes:
4860 raise self.ParseError("pattern does not match file names")
4861
4862 indices = []
4863 for fname in self.files:
4864 matches = pattern.findall(fname)[-1]
4865 if axes != "".join(m for m in matches[::2] if m):
4866 raise ValueError("axes do not match within the image sequence")
4867 indices.append([int(m) for m in matches[1::2] if m])
4868 shape = tuple(numpy.max(indices, axis=0))
4869 startindex = tuple(numpy.min(indices, axis=0))
4870 shape = tuple(i - j + 1 for i, j in zip(shape, startindex))
4871 if product(shape) != len(self.files):
4872 warnings.warn("files are missing. Missing data are zeroed")
4873
4874 self.axes = axes.upper()
4875 self.shape = shape
4876 self._indices = indices
4877 self._startindex = startindex
4878
4879
4880class FileHandle(object):
4881 """Binary file handle.
4882
4883 A limited, special purpose file handler that can:
4884
4885 * handle embedded files (for CZI within CZI files)
4886 * re-open closed files (for multi-file formats, such as OME-TIFF)
4887 * read and write numpy arrays and records from file like objects
4888
4889 Only 'rb' and 'wb' modes are supported. Concurrently reading and writing
4890 of the same stream is untested.
4891
4892 When initialized from another file handle, do not use it unless this
4893 FileHandle is closed.
4894
4895 Attributes
4896 ----------
4897 name : str
4898 Name of the file.
4899 path : str
4900 Absolute path to file.
4901 size : int
4902 Size of file in bytes.
4903 is_file : bool
4904 If True, file has a filno and can be memory-mapped.
4905
4906 All attributes are read-only.
4907
4908 """
4909
4910 __slots__ = (
4911 "_fh",
4912 "_file",
4913 "_mode",
4914 "_name",
4915 "_dir",
4916 "_lock",
4917 "_offset",
4918 "_size",
4919 "_close",
4920 "is_file",
4921 )
4922
4923 def __init__(self, file, mode="rb", name=None, offset=None, size=None):
4924 """Initialize file handle from file name or another file handle.
4925
4926 Parameters
4927 ----------
4928 file : str, pathlib.Path, binary stream, or FileHandle
4929 File name or seekable binary stream, such as an open file
4930 or BytesIO.
4931 mode : str
4932 File open mode in case 'file' is a file name. Must be 'rb' or 'wb'.
4933 name : str
4934 Optional name of file in case 'file' is a binary stream.
4935 offset : int
4936 Optional start position of embedded file. By default, this is
4937 the current file position.
4938 size : int
4939 Optional size of embedded file. By default, this is the number
4940 of bytes from the 'offset' to the end of the file.
4941
4942 """
4943 self._file = file
4944 self._fh = None
4945 self._mode = mode
4946 self._name = name
4947 self._dir = ""
4948 self._offset = offset
4949 self._size = size
4950 self._close = True
4951 self.is_file = False
4952 self._lock = NullContext()
4953 self.open()
4954
4955 def open(self):
4956 """Open or re-open file."""
4957 if self._fh:
4958 return # file is open
4959
4960 if isinstance(self._file, pathlib.Path):
4961 self._file = str(self._file)
4962 if isinstance(self._file, basestring):
4963 # file name
4964 self._file = os.path.realpath(self._file)
4965 self._dir, self._name = os.path.split(self._file)
4966 self._fh = open(self._file, self._mode)
4967 self._close = True
4968 if self._offset is None:
4969 self._offset = 0
4970 elif isinstance(self._file, FileHandle):
4971 # FileHandle
4972 self._fh = self._file._fh
4973 if self._offset is None:
4974 self._offset = 0
4975 self._offset += self._file._offset
4976 self._close = False
4977 if not self._name:
4978 if self._offset:
4979 name, ext = os.path.splitext(self._file._name)
4980 self._name = "%s@%i%s" % (name, self._offset, ext)
4981 else:
4982 self._name = self._file._name
4983 if self._mode and self._mode != self._file._mode:
4984 raise ValueError("FileHandle has wrong mode")
4985 self._mode = self._file._mode
4986 self._dir = self._file._dir
4987 elif hasattr(self._file, "seek"):
4988 # binary stream: open file, BytesIO
4989 try:
4990 self._file.tell()
4991 except Exception:
4992 raise ValueError("binary stream is not seekable")
4993 self._fh = self._file
4994 if self._offset is None:
4995 self._offset = self._file.tell()
4996 self._close = False
4997 if not self._name:
4998 try:
4999 self._dir, self._name = os.path.split(self._fh.name)
5000 except AttributeError:
5001 self._name = "Unnamed binary stream"
5002 try:
5003 self._mode = self._fh.mode
5004 except AttributeError:
5005 pass
5006 else:
5007 raise ValueError(
5008 "The first parameter must be a file name, "
5009 "seekable binary stream, or FileHandle"
5010 )
5011
5012 if self._offset:
5013 self._fh.seek(self._offset)
5014
5015 if self._size is None:
5016 pos = self._fh.tell()
5017 self._fh.seek(self._offset, 2)
5018 self._size = self._fh.tell()
5019 self._fh.seek(pos)
5020
5021 try:
5022 self._fh.fileno()
5023 self.is_file = True
5024 except Exception:
5025 self.is_file = False
5026
5027 def read(self, size=-1):
5028 """Read 'size' bytes from file, or until EOF is reached."""
5029 if size < 0 and self._offset:
5030 size = self._size
5031 return self._fh.read(size)
5032
5033 def write(self, bytestring):
5034 """Write bytestring to file."""
5035 return self._fh.write(bytestring)
5036
5037 def flush(self):
5038 """Flush write buffers if applicable."""
5039 return self._fh.flush()
5040
5041 def memmap_array(self, dtype, shape, offset=0, mode="r", order="C"):
5042 """Return numpy.memmap of data stored in file."""
5043 if not self.is_file:
5044 raise ValueError("Cannot memory-map file without fileno")
5045 return numpy.memmap(
5046 self._fh,
5047 dtype=dtype,
5048 mode=mode,
5049 offset=self._offset + offset,
5050 shape=shape,
5051 order=order,
5052 )
5053
5054 def read_array(
5055 self, dtype, count=-1, sep="", chunksize=2**25, out=None, native=False
5056 ):
5057 """Return numpy array from file.
5058
5059 Work around numpy issue #2230, "numpy.fromfile does not accept
5060 StringIO object" https://github.com/numpy/numpy/issues/2230.
5061
5062 """
5063 fh = self._fh
5064 dtype = numpy.dtype(dtype)
5065 size = self._size if count < 0 else count * dtype.itemsize
5066
5067 if out is None:
5068 try:
5069 result = numpy.fromfile(fh, dtype, count, sep)
5070 except IOError:
5071 # ByteIO
5072 data = fh.read(size)
5073 result = numpy.frombuffer(data, dtype, count).copy()
5074 if native and not result.dtype.isnative:
5075 # swap byte order and dtype without copy
5076 result.byteswap(True)
5077 result = result.newbyteorder()
5078 return result
5079
5080 # Read data from file in chunks and copy to output array
5081 shape = out.shape
5082 size = min(out.nbytes, size)
5083 out = out.reshape(-1)
5084 index = 0
5085 while size > 0:
5086 data = fh.read(min(chunksize, size))
5087 datasize = len(data)
5088 if datasize == 0:
5089 break
5090 size -= datasize
5091 data = numpy.frombuffer(data, dtype)
5092 out[index : index + data.size] = data
5093 index += data.size
5094
5095 if hasattr(out, "flush"):
5096 out.flush()
5097 return out.reshape(shape)
5098
5099 def read_record(self, dtype, shape=1, byteorder=None):
5100 """Return numpy record from file."""
5101 rec = numpy.rec
5102 try:
5103 record = rec.fromfile(self._fh, dtype, shape, byteorder=byteorder)
5104 except Exception:
5105 dtype = numpy.dtype(dtype)
5106 if shape is None:
5107 shape = self._size // dtype.itemsize
5108 size = product(sequence(shape)) * dtype.itemsize
5109 data = self._fh.read(size)
5110 record = rec.fromstring(data, dtype, shape, byteorder=byteorder)
5111 return record[0] if shape == 1 else record
5112
5113 def write_empty(self, size):
5114 """Append size bytes to file. Position must be at end of file."""
5115 if size < 1:
5116 return
5117 self._fh.seek(size - 1, 1)
5118 self._fh.write(b"\x00")
5119
5120 def write_array(self, data):
5121 """Write numpy array to binary file."""
5122 try:
5123 data.tofile(self._fh)
5124 except Exception:
5125 # BytesIO
5126 self._fh.write(data.tostring())
5127
5128 def tell(self):
5129 """Return file's current position."""
5130 return self._fh.tell() - self._offset
5131
5132 def seek(self, offset, whence=0):
5133 """Set file's current position."""
5134 if self._offset:
5135 if whence == 0:
5136 self._fh.seek(self._offset + offset, whence)
5137 return
5138 elif whence == 2 and self._size > 0:
5139 self._fh.seek(self._offset + self._size + offset, 0)
5140 return
5141 self._fh.seek(offset, whence)
5142
5143 def close(self):
5144 """Close file."""
5145 if self._close and self._fh:
5146 self._fh.close()
5147 self._fh = None
5148
5149 def __enter__(self):
5150 return self
5151
5152 def __exit__(self, exc_type, exc_value, traceback):
5153 self.close()
5154
5155 def __getattr__(self, name):
5156 """Return attribute from underlying file object."""
5157 if self._offset:
5158 warnings.warn("FileHandle: '%s' not implemented for embedded files" % name)
5159 return getattr(self._fh, name)
5160
5161 @property
5162 def name(self):
5163 return self._name
5164
5165 @property
5166 def dirname(self):
5167 return self._dir
5168
5169 @property
5170 def path(self):
5171 return os.path.join(self._dir, self._name)
5172
5173 @property
5174 def size(self):
5175 return self._size
5176
5177 @property
5178 def closed(self):
5179 return self._fh is None
5180
5181 @property
5182 def lock(self):
5183 return self._lock
5184
5185 @lock.setter
5186 def lock(self, value):
5187 self._lock = threading.RLock() if value else NullContext()
5188
5189
5190class NullContext(object):
5191 """Null context manager.
5192
5193 >>> with NullContext():
5194 ... pass
5195
5196 """
5197
5198 def __enter__(self):
5199 return self
5200
5201 def __exit__(self, exc_type, exc_value, traceback):
5202 pass
5203
5204
5205class OpenFileCache(object):
5206 """Keep files open."""
5207
5208 __slots__ = ("files", "past", "lock", "size")
5209
5210 def __init__(self, size, lock=None):
5211 """Initialize open file cache."""
5212 self.past = [] # FIFO of opened files
5213 self.files = {} # refcounts of opened files
5214 self.lock = NullContext() if lock is None else lock
5215 self.size = int(size)
5216
5217 def open(self, filehandle):
5218 """Re-open file if necessary."""
5219 with self.lock:
5220 if filehandle in self.files:
5221 self.files[filehandle] += 1
5222 elif filehandle.closed:
5223 filehandle.open()
5224 self.files[filehandle] = 1
5225 self.past.append(filehandle)
5226
5227 def close(self, filehandle):
5228 """Close opened file if no longer used."""
5229 with self.lock:
5230 if filehandle in self.files:
5231 self.files[filehandle] -= 1
5232 # trim the file cache
5233 index = 0
5234 size = len(self.past)
5235 while size > self.size and index < size:
5236 filehandle = self.past[index]
5237 if self.files[filehandle] == 0:
5238 filehandle.close()
5239 del self.files[filehandle]
5240 del self.past[index]
5241 size -= 1
5242 else:
5243 index += 1
5244
5245 def clear(self):
5246 """Close all opened files if not in use."""
5247 with self.lock:
5248 for filehandle, refcount in list(self.files.items()):
5249 if refcount == 0:
5250 filehandle.close()
5251 del self.files[filehandle]
5252 del self.past[self.past.index(filehandle)]
5253
5254
5255class LazyConst(object):
5256 """Class whose attributes are computed on first access from its methods."""
5257
5258 def __init__(self, cls):
5259 self._cls = cls
5260 self.__doc__ = getattr(cls, "__doc__")
5261
5262 def __getattr__(self, name):
5263 func = getattr(self._cls, name)
5264 if not callable(func):
5265 return func
5266 try:
5267 value = func()
5268 except TypeError:
5269 # Python 2 unbound method
5270 value = func.__func__()
5271 setattr(self, name, value)
5272 return value
5273
5274
5275@LazyConst
5276class TIFF(object):
5277 """Namespace for module constants."""
5278
5279 def TAGS():
5280 # TIFF tag codes and names from TIFF6, TIFF/EP, EXIF, and other specs
5281 return {
5282 11: "ProcessingSoftware",
5283 254: "NewSubfileType",
5284 255: "SubfileType",
5285 256: "ImageWidth",
5286 257: "ImageLength",
5287 258: "BitsPerSample",
5288 259: "Compression",
5289 262: "PhotometricInterpretation",
5290 263: "Thresholding",
5291 264: "CellWidth",
5292 265: "CellLength",
5293 266: "FillOrder",
5294 269: "DocumentName",
5295 270: "ImageDescription",
5296 271: "Make",
5297 272: "Model",
5298 273: "StripOffsets",
5299 274: "Orientation",
5300 277: "SamplesPerPixel",
5301 278: "RowsPerStrip",
5302 279: "StripByteCounts",
5303 280: "MinSampleValue",
5304 281: "MaxSampleValue",
5305 282: "XResolution",
5306 283: "YResolution",
5307 284: "PlanarConfiguration",
5308 285: "PageName",
5309 286: "XPosition",
5310 287: "YPosition",
5311 288: "FreeOffsets",
5312 289: "FreeByteCounts",
5313 290: "GrayResponseUnit",
5314 291: "GrayResponseCurve",
5315 292: "T4Options",
5316 293: "T6Options",
5317 296: "ResolutionUnit",
5318 297: "PageNumber",
5319 300: "ColorResponseUnit",
5320 301: "TransferFunction",
5321 305: "Software",
5322 306: "DateTime",
5323 315: "Artist",
5324 316: "HostComputer",
5325 317: "Predictor",
5326 318: "WhitePoint",
5327 319: "PrimaryChromaticities",
5328 320: "ColorMap",
5329 321: "HalftoneHints",
5330 322: "TileWidth",
5331 323: "TileLength",
5332 324: "TileOffsets",
5333 325: "TileByteCounts",
5334 326: "BadFaxLines",
5335 327: "CleanFaxData",
5336 328: "ConsecutiveBadFaxLines",
5337 330: "SubIFDs",
5338 332: "InkSet",
5339 333: "InkNames",
5340 334: "NumberOfInks",
5341 336: "DotRange",
5342 337: "TargetPrinter",
5343 338: "ExtraSamples",
5344 339: "SampleFormat",
5345 340: "SMinSampleValue",
5346 341: "SMaxSampleValue",
5347 342: "TransferRange",
5348 343: "ClipPath",
5349 344: "XClipPathUnits",
5350 345: "YClipPathUnits",
5351 346: "Indexed",
5352 347: "JPEGTables",
5353 351: "OPIProxy",
5354 400: "GlobalParametersIFD",
5355 401: "ProfileType",
5356 402: "FaxProfile",
5357 403: "CodingMethods",
5358 404: "VersionYear",
5359 405: "ModeNumber",
5360 433: "Decode",
5361 434: "DefaultImageColor",
5362 435: "T82Options",
5363 437: "JPEGTables_", # 347
5364 512: "JPEGProc",
5365 513: "JPEGInterchangeFormat",
5366 514: "JPEGInterchangeFormatLength",
5367 515: "JPEGRestartInterval",
5368 517: "JPEGLosslessPredictors",
5369 518: "JPEGPointTransforms",
5370 519: "JPEGQTables",
5371 520: "JPEGDCTables",
5372 521: "JPEGACTables",
5373 529: "YCbCrCoefficients",
5374 530: "YCbCrSubSampling",
5375 531: "YCbCrPositioning",
5376 532: "ReferenceBlackWhite",
5377 559: "StripRowCounts",
5378 700: "XMP", # XMLPacket
5379 769: "GDIGamma", # GDI+
5380 770: "ICCProfileDescriptor", # GDI+
5381 771: "SRGBRenderingIntent", # GDI+
5382 800: "ImageTitle", # GDI+
5383 999: "USPTO_Miscellaneous",
5384 4864: "AndorId", # TODO: Andor Technology 4864 - 5030
5385 4869: "AndorTemperature",
5386 4876: "AndorExposureTime",
5387 4878: "AndorKineticCycleTime",
5388 4879: "AndorAccumulations",
5389 4881: "AndorAcquisitionCycleTime",
5390 4882: "AndorReadoutTime",
5391 4884: "AndorPhotonCounting",
5392 4885: "AndorEmDacLevel",
5393 4890: "AndorFrames",
5394 4896: "AndorHorizontalFlip",
5395 4897: "AndorVerticalFlip",
5396 4898: "AndorClockwise",
5397 4899: "AndorCounterClockwise",
5398 4904: "AndorVerticalClockVoltage",
5399 4905: "AndorVerticalShiftSpeed",
5400 4907: "AndorPreAmpSetting",
5401 4908: "AndorCameraSerial",
5402 4911: "AndorActualTemperature",
5403 4912: "AndorBaselineClamp",
5404 4913: "AndorPrescans",
5405 4914: "AndorModel",
5406 4915: "AndorChipSizeX",
5407 4916: "AndorChipSizeY",
5408 4944: "AndorBaselineOffset",
5409 4966: "AndorSoftwareVersion",
5410 18246: "Rating",
5411 18247: "XP_DIP_XML",
5412 18248: "StitchInfo",
5413 18249: "RatingPercent",
5414 20481: "ResolutionXUnit", # GDI+
5415 20482: "ResolutionYUnit", # GDI+
5416 20483: "ResolutionXLengthUnit", # GDI+
5417 20484: "ResolutionYLengthUnit", # GDI+
5418 20485: "PrintFlags", # GDI+
5419 20486: "PrintFlagsVersion", # GDI+
5420 20487: "PrintFlagsCrop", # GDI+
5421 20488: "PrintFlagsBleedWidth", # GDI+
5422 20489: "PrintFlagsBleedWidthScale", # GDI+
5423 20490: "HalftoneLPI", # GDI+
5424 20491: "HalftoneLPIUnit", # GDI+
5425 20492: "HalftoneDegree", # GDI+
5426 20493: "HalftoneShape", # GDI+
5427 20494: "HalftoneMisc", # GDI+
5428 20495: "HalftoneScreen", # GDI+
5429 20496: "JPEGQuality", # GDI+
5430 20497: "GridSize", # GDI+
5431 20498: "ThumbnailFormat", # GDI+
5432 20499: "ThumbnailWidth", # GDI+
5433 20500: "ThumbnailHeight", # GDI+
5434 20501: "ThumbnailColorDepth", # GDI+
5435 20502: "ThumbnailPlanes", # GDI+
5436 20503: "ThumbnailRawBytes", # GDI+
5437 20504: "ThumbnailSize", # GDI+
5438 20505: "ThumbnailCompressedSize", # GDI+
5439 20506: "ColorTransferFunction", # GDI+
5440 20507: "ThumbnailData",
5441 20512: "ThumbnailImageWidth", # GDI+
5442 20513: "ThumbnailImageHeight", # GDI+
5443 20514: "ThumbnailBitsPerSample", # GDI+
5444 20515: "ThumbnailCompression",
5445 20516: "ThumbnailPhotometricInterp", # GDI+
5446 20517: "ThumbnailImageDescription", # GDI+
5447 20518: "ThumbnailEquipMake", # GDI+
5448 20519: "ThumbnailEquipModel", # GDI+
5449 20520: "ThumbnailStripOffsets", # GDI+
5450 20521: "ThumbnailOrientation", # GDI+
5451 20522: "ThumbnailSamplesPerPixel", # GDI+
5452 20523: "ThumbnailRowsPerStrip", # GDI+
5453 20524: "ThumbnailStripBytesCount", # GDI+
5454 20525: "ThumbnailResolutionX",
5455 20526: "ThumbnailResolutionY",
5456 20527: "ThumbnailPlanarConfig", # GDI+
5457 20528: "ThumbnailResolutionUnit",
5458 20529: "ThumbnailTransferFunction",
5459 20530: "ThumbnailSoftwareUsed", # GDI+
5460 20531: "ThumbnailDateTime", # GDI+
5461 20532: "ThumbnailArtist", # GDI+
5462 20533: "ThumbnailWhitePoint", # GDI+
5463 20534: "ThumbnailPrimaryChromaticities", # GDI+
5464 20535: "ThumbnailYCbCrCoefficients", # GDI+
5465 20536: "ThumbnailYCbCrSubsampling", # GDI+
5466 20537: "ThumbnailYCbCrPositioning",
5467 20538: "ThumbnailRefBlackWhite", # GDI+
5468 20539: "ThumbnailCopyRight", # GDI+
5469 20545: "InteroperabilityIndex",
5470 20546: "InteroperabilityVersion",
5471 20624: "LuminanceTable",
5472 20625: "ChrominanceTable",
5473 20736: "FrameDelay", # GDI+
5474 20737: "LoopCount", # GDI+
5475 20738: "GlobalPalette", # GDI+
5476 20739: "IndexBackground", # GDI+
5477 20740: "IndexTransparent", # GDI+
5478 20752: "PixelUnit", # GDI+
5479 20753: "PixelPerUnitX", # GDI+
5480 20754: "PixelPerUnitY", # GDI+
5481 20755: "PaletteHistogram", # GDI+
5482 28672: "SonyRawFileType", # Sony ARW
5483 28722: "VignettingCorrParams", # Sony ARW
5484 28725: "ChromaticAberrationCorrParams", # Sony ARW
5485 28727: "DistortionCorrParams", # Sony ARW
5486 # Private tags >= 32768
5487 32781: "ImageID",
5488 32931: "WangTag1",
5489 32932: "WangAnnotation",
5490 32933: "WangTag3",
5491 32934: "WangTag4",
5492 32953: "ImageReferencePoints",
5493 32954: "RegionXformTackPoint",
5494 32955: "WarpQuadrilateral",
5495 32956: "AffineTransformMat",
5496 32995: "Matteing",
5497 32996: "DataType",
5498 32997: "ImageDepth",
5499 32998: "TileDepth",
5500 33300: "ImageFullWidth",
5501 33301: "ImageFullLength",
5502 33302: "TextureFormat",
5503 33303: "TextureWrapModes",
5504 33304: "FieldOfViewCotangent",
5505 33305: "MatrixWorldToScreen",
5506 33306: "MatrixWorldToCamera",
5507 33405: "Model2",
5508 33421: "CFARepeatPatternDim",
5509 33422: "CFAPattern",
5510 33423: "BatteryLevel",
5511 33424: "KodakIFD",
5512 33434: "ExposureTime",
5513 33437: "FNumber",
5514 33432: "Copyright",
5515 33445: "MDFileTag",
5516 33446: "MDScalePixel",
5517 33447: "MDColorTable",
5518 33448: "MDLabName",
5519 33449: "MDSampleInfo",
5520 33450: "MDPrepDate",
5521 33451: "MDPrepTime",
5522 33452: "MDFileUnits",
5523 33550: "ModelPixelScaleTag",
5524 33589: "AdventScale",
5525 33590: "AdventRevision",
5526 33628: "UIC1tag", # Metamorph Universal Imaging Corp STK
5527 33629: "UIC2tag",
5528 33630: "UIC3tag",
5529 33631: "UIC4tag",
5530 33723: "IPTCNAA",
5531 33858: "ExtendedTagsOffset", # DEFF points IFD with private tags
5532 33918: "IntergraphPacketData", # INGRPacketDataTag
5533 33919: "IntergraphFlagRegisters", # INGRFlagRegisters
5534 33920: "IntergraphMatrixTag", # IrasBTransformationMatrix
5535 33921: "INGRReserved",
5536 33922: "ModelTiepointTag",
5537 33923: "LeicaMagic",
5538 34016: "Site",
5539 34017: "ColorSequence",
5540 34018: "IT8Header",
5541 34019: "RasterPadding",
5542 34020: "BitsPerRunLength",
5543 34021: "BitsPerExtendedRunLength",
5544 34022: "ColorTable",
5545 34023: "ImageColorIndicator",
5546 34024: "BackgroundColorIndicator",
5547 34025: "ImageColorValue",
5548 34026: "BackgroundColorValue",
5549 34027: "PixelIntensityRange",
5550 34028: "TransparencyIndicator",
5551 34029: "ColorCharacterization",
5552 34030: "HCUsage",
5553 34031: "TrapIndicator",
5554 34032: "CMYKEquivalent",
5555 34118: "CZ_SEM", # Zeiss SEM
5556 34152: "AFCP_IPTC",
5557 34232: "PixelMagicJBIGOptions",
5558 34263: "JPLCartoIFD",
5559 34122: "IPLAB", # number of images
5560 34264: "ModelTransformationTag",
5561 34306: "WB_GRGBLevels", # Leaf MOS
5562 34310: "LeafData",
5563 34361: "MM_Header",
5564 34362: "MM_Stamp",
5565 34363: "MM_Unknown",
5566 34377: "ImageResources", # Photoshop
5567 34386: "MM_UserBlock",
5568 34412: "CZ_LSMINFO",
5569 34665: "ExifTag",
5570 34675: "InterColorProfile", # ICCProfile
5571 34680: "FEI_SFEG", #
5572 34682: "FEI_HELIOS", #
5573 34683: "FEI_TITAN", #
5574 34687: "FXExtensions",
5575 34688: "MultiProfiles",
5576 34689: "SharedData",
5577 34690: "T88Options",
5578 34710: "MarCCD", # offset to MarCCD header
5579 34732: "ImageLayer",
5580 34735: "GeoKeyDirectoryTag",
5581 34736: "GeoDoubleParamsTag",
5582 34737: "GeoAsciiParamsTag",
5583 34750: "JBIGOptions",
5584 34821: "PIXTIFF", # ? Pixel Translations Inc
5585 34850: "ExposureProgram",
5586 34852: "SpectralSensitivity",
5587 34853: "GPSTag", # GPSIFD
5588 34855: "ISOSpeedRatings",
5589 34856: "OECF",
5590 34857: "Interlace",
5591 34858: "TimeZoneOffset",
5592 34859: "SelfTimerMode",
5593 34864: "SensitivityType",
5594 34865: "StandardOutputSensitivity",
5595 34866: "RecommendedExposureIndex",
5596 34867: "ISOSpeed",
5597 34868: "ISOSpeedLatitudeyyy",
5598 34869: "ISOSpeedLatitudezzz",
5599 34908: "HylaFAXFaxRecvParams",
5600 34909: "HylaFAXFaxSubAddress",
5601 34910: "HylaFAXFaxRecvTime",
5602 34911: "FaxDcs",
5603 34929: "FedexEDR",
5604 34954: "LeafSubIFD",
5605 34959: "Aphelion1",
5606 34960: "Aphelion2",
5607 34961: "AphelionInternal", # ADCIS
5608 36864: "ExifVersion",
5609 36867: "DateTimeOriginal",
5610 36868: "DateTimeDigitized",
5611 36873: "GooglePlusUploadCode",
5612 36880: "OffsetTime",
5613 36881: "OffsetTimeOriginal",
5614 36882: "OffsetTimeDigitized",
5615 # TODO: Pilatus/CHESS/TV6 36864..37120 conflicting with Exif tags
5616 # 36864: 'TVX ?',
5617 # 36865: 'TVX_NumExposure',
5618 # 36866: 'TVX_NumBackground',
5619 # 36867: 'TVX_ExposureTime',
5620 # 36868: 'TVX_BackgroundTime',
5621 # 36870: 'TVX ?',
5622 # 36873: 'TVX_SubBpp',
5623 # 36874: 'TVX_SubWide',
5624 # 36875: 'TVX_SubHigh',
5625 # 36876: 'TVX_BlackLevel',
5626 # 36877: 'TVX_DarkCurrent',
5627 # 36878: 'TVX_ReadNoise',
5628 # 36879: 'TVX_DarkCurrentNoise',
5629 # 36880: 'TVX_BeamMonitor',
5630 # 37120: 'TVX_UserVariables', # A/D values
5631 37121: "ComponentsConfiguration",
5632 37122: "CompressedBitsPerPixel",
5633 37377: "ShutterSpeedValue",
5634 37378: "ApertureValue",
5635 37379: "BrightnessValue",
5636 37380: "ExposureBiasValue",
5637 37381: "MaxApertureValue",
5638 37382: "SubjectDistance",
5639 37383: "MeteringMode",
5640 37384: "LightSource",
5641 37385: "Flash",
5642 37386: "FocalLength",
5643 37387: "FlashEnergy_", # 37387
5644 37388: "SpatialFrequencyResponse_", # 37388
5645 37389: "Noise",
5646 37390: "FocalPlaneXResolution",
5647 37391: "FocalPlaneYResolution",
5648 37392: "FocalPlaneResolutionUnit",
5649 37393: "ImageNumber",
5650 37394: "SecurityClassification",
5651 37395: "ImageHistory",
5652 37396: "SubjectLocation",
5653 37397: "ExposureIndex",
5654 37398: "TIFFEPStandardID",
5655 37399: "SensingMethod",
5656 37434: "CIP3DataFile",
5657 37435: "CIP3Sheet",
5658 37436: "CIP3Side",
5659 37439: "StoNits",
5660 37500: "MakerNote",
5661 37510: "UserComment",
5662 37520: "SubsecTime",
5663 37521: "SubsecTimeOriginal",
5664 37522: "SubsecTimeDigitized",
5665 37679: "MODIText", # Microsoft Office Document Imaging
5666 37680: "MODIOLEPropertySetStorage",
5667 37681: "MODIPositioning",
5668 37706: "TVIPS", # offset to TemData structure
5669 37707: "TVIPS1",
5670 37708: "TVIPS2", # same TemData structure as undefined
5671 37724: "ImageSourceData", # Photoshop
5672 37888: "Temperature",
5673 37889: "Humidity",
5674 37890: "Pressure",
5675 37891: "WaterDepth",
5676 37892: "Acceleration",
5677 37893: "CameraElevationAngle",
5678 40001: "MC_IpWinScal", # Media Cybernetics
5679 40100: "MC_IdOld",
5680 40965: "InteroperabilityTag", # InteropOffset
5681 40091: "XPTitle",
5682 40092: "XPComment",
5683 40093: "XPAuthor",
5684 40094: "XPKeywords",
5685 40095: "XPSubject",
5686 40960: "FlashpixVersion",
5687 40961: "ColorSpace",
5688 40962: "PixelXDimension",
5689 40963: "PixelYDimension",
5690 40964: "RelatedSoundFile",
5691 40976: "SamsungRawPointersOffset",
5692 40977: "SamsungRawPointersLength",
5693 41217: "SamsungRawByteOrder",
5694 41218: "SamsungRawUnknown",
5695 41483: "FlashEnergy",
5696 41484: "SpatialFrequencyResponse",
5697 41485: "Noise_", # 37389
5698 41486: "FocalPlaneXResolution_", # 37390
5699 41487: "FocalPlaneYResolution_", # 37391
5700 41488: "FocalPlaneResolutionUnit_", # 37392
5701 41489: "ImageNumber_", # 37393
5702 41490: "SecurityClassification_", # 37394
5703 41491: "ImageHistory_", # 37395
5704 41492: "SubjectLocation_", # 37395
5705 41493: "ExposureIndex_ ", # 37397
5706 41494: "TIFF-EPStandardID",
5707 41495: "SensingMethod_", # 37399
5708 41728: "FileSource",
5709 41729: "SceneType",
5710 41730: "CFAPattern_", # 33422
5711 41985: "CustomRendered",
5712 41986: "ExposureMode",
5713 41987: "WhiteBalance",
5714 41988: "DigitalZoomRatio",
5715 41989: "FocalLengthIn35mmFilm",
5716 41990: "SceneCaptureType",
5717 41991: "GainControl",
5718 41992: "Contrast",
5719 41993: "Saturation",
5720 41994: "Sharpness",
5721 41995: "DeviceSettingDescription",
5722 41996: "SubjectDistanceRange",
5723 42016: "ImageUniqueID",
5724 42032: "CameraOwnerName",
5725 42033: "BodySerialNumber",
5726 42034: "LensSpecification",
5727 42035: "LensMake",
5728 42036: "LensModel",
5729 42037: "LensSerialNumber",
5730 42112: "GDAL_METADATA",
5731 42113: "GDAL_NODATA",
5732 42240: "Gamma",
5733 43314: "NIHImageHeader",
5734 44992: "ExpandSoftware",
5735 44993: "ExpandLens",
5736 44994: "ExpandFilm",
5737 44995: "ExpandFilterLens",
5738 44996: "ExpandScanner",
5739 44997: "ExpandFlashLamp",
5740 48129: "PixelFormat", # HDP and WDP
5741 48130: "Transformation",
5742 48131: "Uncompressed",
5743 48132: "ImageType",
5744 48256: "ImageWidth_", # 256
5745 48257: "ImageHeight_",
5746 48258: "WidthResolution",
5747 48259: "HeightResolution",
5748 48320: "ImageOffset",
5749 48321: "ImageByteCount",
5750 48322: "AlphaOffset",
5751 48323: "AlphaByteCount",
5752 48324: "ImageDataDiscard",
5753 48325: "AlphaDataDiscard",
5754 50215: "OceScanjobDescription",
5755 50216: "OceApplicationSelector",
5756 50217: "OceIdentificationNumber",
5757 50218: "OceImageLogicCharacteristics",
5758 50255: "Annotations",
5759 50288: "MC_Id", # Media Cybernetics
5760 50289: "MC_XYPosition",
5761 50290: "MC_ZPosition",
5762 50291: "MC_XYCalibration",
5763 50292: "MC_LensCharacteristics",
5764 50293: "MC_ChannelName",
5765 50294: "MC_ExcitationWavelength",
5766 50295: "MC_TimeStamp",
5767 50296: "MC_FrameProperties",
5768 50341: "PrintImageMatching",
5769 50495: "PCO_RAW", # TODO: PCO CamWare
5770 50547: "OriginalFileName",
5771 50560: "USPTO_OriginalContentType", # US Patent Office
5772 50561: "USPTO_RotationCode",
5773 50656: "CR2CFAPattern",
5774 50706: "DNGVersion", # DNG 50706 .. 51112
5775 50707: "DNGBackwardVersion",
5776 50708: "UniqueCameraModel",
5777 50709: "LocalizedCameraModel",
5778 50710: "CFAPlaneColor",
5779 50711: "CFALayout",
5780 50712: "LinearizationTable",
5781 50713: "BlackLevelRepeatDim",
5782 50714: "BlackLevel",
5783 50715: "BlackLevelDeltaH",
5784 50716: "BlackLevelDeltaV",
5785 50717: "WhiteLevel",
5786 50718: "DefaultScale",
5787 50719: "DefaultCropOrigin",
5788 50720: "DefaultCropSize",
5789 50721: "ColorMatrix1",
5790 50722: "ColorMatrix2",
5791 50723: "CameraCalibration1",
5792 50724: "CameraCalibration2",
5793 50725: "ReductionMatrix1",
5794 50726: "ReductionMatrix2",
5795 50727: "AnalogBalance",
5796 50728: "AsShotNeutral",
5797 50729: "AsShotWhiteXY",
5798 50730: "BaselineExposure",
5799 50731: "BaselineNoise",
5800 50732: "BaselineSharpness",
5801 50733: "BayerGreenSplit",
5802 50734: "LinearResponseLimit",
5803 50735: "CameraSerialNumber",
5804 50736: "LensInfo",
5805 50737: "ChromaBlurRadius",
5806 50738: "AntiAliasStrength",
5807 50739: "ShadowScale",
5808 50740: "DNGPrivateData",
5809 50741: "MakerNoteSafety",
5810 50752: "RawImageSegmentation",
5811 50778: "CalibrationIlluminant1",
5812 50779: "CalibrationIlluminant2",
5813 50780: "BestQualityScale",
5814 50781: "RawDataUniqueID",
5815 50784: "AliasLayerMetadata",
5816 50827: "OriginalRawFileName",
5817 50828: "OriginalRawFileData",
5818 50829: "ActiveArea",
5819 50830: "MaskedAreas",
5820 50831: "AsShotICCProfile",
5821 50832: "AsShotPreProfileMatrix",
5822 50833: "CurrentICCProfile",
5823 50834: "CurrentPreProfileMatrix",
5824 50838: "IJMetadataByteCounts",
5825 50839: "IJMetadata",
5826 50844: "RPCCoefficientTag",
5827 50879: "ColorimetricReference",
5828 50885: "SRawType",
5829 50898: "PanasonicTitle",
5830 50899: "PanasonicTitle2",
5831 50931: "CameraCalibrationSignature",
5832 50932: "ProfileCalibrationSignature",
5833 50933: "ProfileIFD",
5834 50934: "AsShotProfileName",
5835 50935: "NoiseReductionApplied",
5836 50936: "ProfileName",
5837 50937: "ProfileHueSatMapDims",
5838 50938: "ProfileHueSatMapData1",
5839 50939: "ProfileHueSatMapData2",
5840 50940: "ProfileToneCurve",
5841 50941: "ProfileEmbedPolicy",
5842 50942: "ProfileCopyright",
5843 50964: "ForwardMatrix1",
5844 50965: "ForwardMatrix2",
5845 50966: "PreviewApplicationName",
5846 50967: "PreviewApplicationVersion",
5847 50968: "PreviewSettingsName",
5848 50969: "PreviewSettingsDigest",
5849 50970: "PreviewColorSpace",
5850 50971: "PreviewDateTime",
5851 50972: "RawImageDigest",
5852 50973: "OriginalRawFileDigest",
5853 50974: "SubTileBlockSize",
5854 50975: "RowInterleaveFactor",
5855 50981: "ProfileLookTableDims",
5856 50982: "ProfileLookTableData",
5857 51008: "OpcodeList1",
5858 51009: "OpcodeList2",
5859 51022: "OpcodeList3",
5860 51023: "FibicsXML", #
5861 51041: "NoiseProfile",
5862 51043: "TimeCodes",
5863 51044: "FrameRate",
5864 51058: "TStop",
5865 51081: "ReelName",
5866 51089: "OriginalDefaultFinalSize",
5867 51090: "OriginalBestQualitySize",
5868 51091: "OriginalDefaultCropSize",
5869 51105: "CameraLabel",
5870 51107: "ProfileHueSatMapEncoding",
5871 51108: "ProfileLookTableEncoding",
5872 51109: "BaselineExposureOffset",
5873 51110: "DefaultBlackRender",
5874 51111: "NewRawImageDigest",
5875 51112: "RawToPreviewGain",
5876 51125: "DefaultUserCrop",
5877 51123: "MicroManagerMetadata",
5878 59932: "Padding",
5879 59933: "OffsetSchema",
5880 # Reusable Tags 65000-65535
5881 # 65000: Dimap_Document XML
5882 # 65000-65112: Photoshop Camera RAW EXIF tags
5883 # 65000: 'OwnerName',
5884 # 65001: 'SerialNumber',
5885 # 65002: 'Lens',
5886 # 65024: 'KDC_IFD',
5887 # 65100: 'RawFile',
5888 # 65101: 'Converter',
5889 # 65102: 'WhiteBalance',
5890 # 65105: 'Exposure',
5891 # 65106: 'Shadows',
5892 # 65107: 'Brightness',
5893 # 65108: 'Contrast',
5894 # 65109: 'Saturation',
5895 # 65110: 'Sharpness',
5896 # 65111: 'Smoothness',
5897 # 65112: 'MoireFilter',
5898 65200: "FlexXML", #
5899 65563: "PerSample",
5900 }
5901
5902 def TAG_NAMES():
5903 return {v: c for c, v in TIFF.TAGS.items()}
5904
5905 def TAG_READERS():
5906 # Map TIFF tag codes to import functions
5907 return {
5908 320: read_colormap,
5909 # 700: read_bytes, # read_utf8,
5910 # 34377: read_bytes,
5911 33723: read_bytes,
5912 # 34675: read_bytes,
5913 33628: read_uic1tag, # Universal Imaging Corp STK
5914 33629: read_uic2tag,
5915 33630: read_uic3tag,
5916 33631: read_uic4tag,
5917 34118: read_cz_sem, # Carl Zeiss SEM
5918 34361: read_mm_header, # Olympus FluoView
5919 34362: read_mm_stamp,
5920 34363: read_numpy, # MM_Unknown
5921 34386: read_numpy, # MM_UserBlock
5922 34412: read_cz_lsminfo, # Carl Zeiss LSM
5923 34680: read_fei_metadata, # S-FEG
5924 34682: read_fei_metadata, # Helios NanoLab
5925 37706: read_tvips_header, # TVIPS EMMENU
5926 37724: read_bytes, # ImageSourceData
5927 33923: read_bytes, # read_leica_magic
5928 43314: read_nih_image_header,
5929 # 40001: read_bytes,
5930 40100: read_bytes,
5931 50288: read_bytes,
5932 50296: read_bytes,
5933 50839: read_bytes,
5934 51123: read_json,
5935 34665: read_exif_ifd,
5936 34853: read_gps_ifd,
5937 40965: read_interoperability_ifd,
5938 }
5939
5940 def TAG_TUPLE():
5941 # Tags whose values must be stored as tuples
5942 return frozenset((273, 279, 324, 325, 530, 531, 34736))
5943
5944 def TAG_ATTRIBUTES():
5945 # Map tag codes to TiffPage attribute names
5946 return {
5947 "ImageWidth": "imagewidth",
5948 "ImageLength": "imagelength",
5949 "BitsPerSample": "bitspersample",
5950 "Compression": "compression",
5951 "PlanarConfiguration": "planarconfig",
5952 "FillOrder": "fillorder",
5953 "PhotometricInterpretation": "photometric",
5954 "ColorMap": "colormap",
5955 "ImageDescription": "description",
5956 "ImageDescription1": "description1",
5957 "SamplesPerPixel": "samplesperpixel",
5958 "RowsPerStrip": "rowsperstrip",
5959 "Software": "software",
5960 "Predictor": "predictor",
5961 "TileWidth": "tilewidth",
5962 "TileLength": "tilelength",
5963 "ExtraSamples": "extrasamples",
5964 "SampleFormat": "sampleformat",
5965 "ImageDepth": "imagedepth",
5966 "TileDepth": "tiledepth",
5967 }
5968
5969 def TAG_ENUM():
5970 return {
5971 # 254: TIFF.FILETYPE,
5972 255: TIFF.OFILETYPE,
5973 259: TIFF.COMPRESSION,
5974 262: TIFF.PHOTOMETRIC,
5975 263: TIFF.THRESHHOLD,
5976 266: TIFF.FILLORDER,
5977 274: TIFF.ORIENTATION,
5978 284: TIFF.PLANARCONFIG,
5979 290: TIFF.GRAYRESPONSEUNIT,
5980 # 292: TIFF.GROUP3OPT,
5981 # 293: TIFF.GROUP4OPT,
5982 296: TIFF.RESUNIT,
5983 300: TIFF.COLORRESPONSEUNIT,
5984 317: TIFF.PREDICTOR,
5985 338: TIFF.EXTRASAMPLE,
5986 339: TIFF.SAMPLEFORMAT,
5987 # 512: TIFF.JPEGPROC,
5988 # 531: TIFF.YCBCRPOSITION,
5989 }
5990
5991 def FILETYPE():
5992 class FILETYPE(enum.IntFlag):
5993 # Python 3.6 only
5994 UNDEFINED = 0
5995 REDUCEDIMAGE = 1
5996 PAGE = 2
5997 MASK = 4
5998
5999 return FILETYPE
6000
6001 def OFILETYPE():
6002 class OFILETYPE(enum.IntEnum):
6003 UNDEFINED = 0
6004 IMAGE = 1
6005 REDUCEDIMAGE = 2
6006 PAGE = 3
6007
6008 return OFILETYPE
6009
6010 def COMPRESSION():
6011 class COMPRESSION(enum.IntEnum):
6012 NONE = 1 # Uncompressed
6013 CCITTRLE = 2 # CCITT 1D
6014 CCITT_T4 = 3 # 'T4/Group 3 Fax',
6015 CCITT_T6 = 4 # 'T6/Group 4 Fax',
6016 LZW = 5
6017 OJPEG = 6 # old-style JPEG
6018 JPEG = 7
6019 ADOBE_DEFLATE = 8
6020 JBIG_BW = 9
6021 JBIG_COLOR = 10
6022 JPEG_99 = 99
6023 KODAK_262 = 262
6024 NEXT = 32766
6025 SONY_ARW = 32767
6026 PACKED_RAW = 32769
6027 SAMSUNG_SRW = 32770
6028 CCIRLEW = 32771
6029 SAMSUNG_SRW2 = 32772
6030 PACKBITS = 32773
6031 THUNDERSCAN = 32809
6032 IT8CTPAD = 32895
6033 IT8LW = 32896
6034 IT8MP = 32897
6035 IT8BL = 32898
6036 PIXARFILM = 32908
6037 PIXARLOG = 32909
6038 DEFLATE = 32946
6039 DCS = 32947
6040 APERIO_JP2000_YCBC = 33003 # Leica Aperio
6041 APERIO_JP2000_RGB = 33005 # Leica Aperio
6042 JBIG = 34661
6043 SGILOG = 34676
6044 SGILOG24 = 34677
6045 JPEG2000 = 34712
6046 NIKON_NEF = 34713
6047 JBIG2 = 34715
6048 MDI_BINARY = 34718 # 'Microsoft Document Imaging
6049 MDI_PROGRESSIVE = 34719 # 'Microsoft Document Imaging
6050 MDI_VECTOR = 34720 # 'Microsoft Document Imaging
6051 JPEG_LOSSY = 34892
6052 LZMA = 34925
6053 ZSTD = 34926
6054 OPS_PNG = 34933 # Objective Pathology Services
6055 OPS_JPEGXR = 34934 # Objective Pathology Services
6056 PIXTIFF = 50013
6057 KODAK_DCR = 65000
6058 PENTAX_PEF = 65535
6059 # def __bool__(self): return self != 1 # Python 3.6 only
6060
6061 return COMPRESSION
6062
6063 def PHOTOMETRIC():
6064 class PHOTOMETRIC(enum.IntEnum):
6065 MINISWHITE = 0
6066 MINISBLACK = 1
6067 RGB = 2
6068 PALETTE = 3
6069 MASK = 4
6070 SEPARATED = 5 # CMYK
6071 YCBCR = 6
6072 CIELAB = 8
6073 ICCLAB = 9
6074 ITULAB = 10
6075 CFA = 32803 # Color Filter Array
6076 LOGL = 32844
6077 LOGLUV = 32845
6078 LINEAR_RAW = 34892
6079
6080 return PHOTOMETRIC
6081
6082 def THRESHHOLD():
6083 class THRESHHOLD(enum.IntEnum):
6084 BILEVEL = 1
6085 HALFTONE = 2
6086 ERRORDIFFUSE = 3
6087
6088 return THRESHHOLD
6089
6090 def FILLORDER():
6091 class FILLORDER(enum.IntEnum):
6092 MSB2LSB = 1
6093 LSB2MSB = 2
6094
6095 return FILLORDER
6096
6097 def ORIENTATION():
6098 class ORIENTATION(enum.IntEnum):
6099 TOPLEFT = 1
6100 TOPRIGHT = 2
6101 BOTRIGHT = 3
6102 BOTLEFT = 4
6103 LEFTTOP = 5
6104 RIGHTTOP = 6
6105 RIGHTBOT = 7
6106 LEFTBOT = 8
6107
6108 return ORIENTATION
6109
6110 def PLANARCONFIG():
6111 class PLANARCONFIG(enum.IntEnum):
6112 CONTIG = 1
6113 SEPARATE = 2
6114
6115 return PLANARCONFIG
6116
6117 def GRAYRESPONSEUNIT():
6118 class GRAYRESPONSEUNIT(enum.IntEnum):
6119 _10S = 1
6120 _100S = 2
6121 _1000S = 3
6122 _10000S = 4
6123 _100000S = 5
6124
6125 return GRAYRESPONSEUNIT
6126
6127 def GROUP4OPT():
6128 class GROUP4OPT(enum.IntEnum):
6129 UNCOMPRESSED = 2
6130
6131 return GROUP4OPT
6132
6133 def RESUNIT():
6134 class RESUNIT(enum.IntEnum):
6135 NONE = 1
6136 INCH = 2
6137 CENTIMETER = 3
6138 # def __bool__(self): return self != 1 # Python 3.6 only
6139
6140 return RESUNIT
6141
6142 def COLORRESPONSEUNIT():
6143 class COLORRESPONSEUNIT(enum.IntEnum):
6144 _10S = 1
6145 _100S = 2
6146 _1000S = 3
6147 _10000S = 4
6148 _100000S = 5
6149
6150 return COLORRESPONSEUNIT
6151
6152 def PREDICTOR():
6153 class PREDICTOR(enum.IntEnum):
6154 NONE = 1
6155 HORIZONTAL = 2
6156 FLOATINGPOINT = 3
6157 # def __bool__(self): return self != 1 # Python 3.6 only
6158
6159 return PREDICTOR
6160
6161 def EXTRASAMPLE():
6162 class EXTRASAMPLE(enum.IntEnum):
6163 UNSPECIFIED = 0
6164 ASSOCALPHA = 1
6165 UNASSALPHA = 2
6166
6167 return EXTRASAMPLE
6168
6169 def SAMPLEFORMAT():
6170 class SAMPLEFORMAT(enum.IntEnum):
6171 UINT = 1
6172 INT = 2
6173 IEEEFP = 3
6174 VOID = 4
6175 COMPLEXINT = 5
6176 COMPLEXIEEEFP = 6
6177
6178 return SAMPLEFORMAT
6179
6180 def DATATYPES():
6181 class DATATYPES(enum.IntEnum):
6182 NOTYPE = 0
6183 BYTE = 1
6184 ASCII = 2
6185 SHORT = 3
6186 LONG = 4
6187 RATIONAL = 5
6188 SBYTE = 6
6189 UNDEFINED = 7
6190 SSHORT = 8
6191 SLONG = 9
6192 SRATIONAL = 10
6193 FLOAT = 11
6194 DOUBLE = 12
6195 IFD = 13
6196 UNICODE = 14
6197 COMPLEX = 15
6198 LONG8 = 16
6199 SLONG8 = 17
6200 IFD8 = 18
6201
6202 return DATATYPES
6203
6204 def DATA_FORMATS():
6205 # Map TIFF DATATYPES to Python struct formats
6206 return {
6207 1: "1B", # BYTE 8-bit unsigned integer.
6208 2: "1s", # ASCII 8-bit byte that contains a 7-bit ASCII code;
6209 # the last byte must be NULL (binary zero).
6210 3: "1H", # SHORT 16-bit (2-byte) unsigned integer
6211 4: "1I", # LONG 32-bit (4-byte) unsigned integer.
6212 5: "2I", # RATIONAL Two LONGs: the first represents the numerator
6213 # of a fraction; the second, the denominator.
6214 6: "1b", # SBYTE An 8-bit signed (twos-complement) integer.
6215 7: "1B", # UNDEFINED An 8-bit byte that may contain anything,
6216 # depending on the definition of the field.
6217 8: "1h", # SSHORT A 16-bit (2-byte) signed (twos-complement)
6218 # integer.
6219 9: "1i", # SLONG A 32-bit (4-byte) signed (twos-complement)
6220 # integer.
6221 10: "2i", # SRATIONAL Two SLONGs: the first represents the
6222 # numerator of a fraction, the second the denominator.
6223 11: "1f", # FLOAT Single precision (4-byte) IEEE format.
6224 12: "1d", # DOUBLE Double precision (8-byte) IEEE format.
6225 13: "1I", # IFD unsigned 4 byte IFD offset.
6226 # 14: '', # UNICODE
6227 # 15: '', # COMPLEX
6228 16: "1Q", # LONG8 unsigned 8 byte integer (BigTiff)
6229 17: "1q", # SLONG8 signed 8 byte integer (BigTiff)
6230 18: "1Q", # IFD8 unsigned 8 byte IFD offset (BigTiff)
6231 }
6232
6233 def DATA_DTYPES():
6234 # Map numpy dtypes to TIFF DATATYPES
6235 return {
6236 "B": 1,
6237 "s": 2,
6238 "H": 3,
6239 "I": 4,
6240 "2I": 5,
6241 "b": 6,
6242 "h": 8,
6243 "i": 9,
6244 "2i": 10,
6245 "f": 11,
6246 "d": 12,
6247 "Q": 16,
6248 "q": 17,
6249 }
6250
6251 def SAMPLE_DTYPES():
6252 # Map TIFF SampleFormats and BitsPerSample to numpy dtype
6253 return {
6254 (1, 1): "?", # bitmap
6255 (1, 2): "B",
6256 (1, 3): "B",
6257 (1, 4): "B",
6258 (1, 5): "B",
6259 (1, 6): "B",
6260 (1, 7): "B",
6261 (1, 8): "B",
6262 (1, 9): "H",
6263 (1, 10): "H",
6264 (1, 11): "H",
6265 (1, 12): "H",
6266 (1, 13): "H",
6267 (1, 14): "H",
6268 (1, 15): "H",
6269 (1, 16): "H",
6270 (1, 17): "I",
6271 (1, 18): "I",
6272 (1, 19): "I",
6273 (1, 20): "I",
6274 (1, 21): "I",
6275 (1, 22): "I",
6276 (1, 23): "I",
6277 (1, 24): "I",
6278 (1, 25): "I",
6279 (1, 26): "I",
6280 (1, 27): "I",
6281 (1, 28): "I",
6282 (1, 29): "I",
6283 (1, 30): "I",
6284 (1, 31): "I",
6285 (1, 32): "I",
6286 (1, 64): "Q",
6287 (2, 8): "b",
6288 (2, 16): "h",
6289 (2, 32): "i",
6290 (2, 64): "q",
6291 (3, 16): "e",
6292 (3, 32): "f",
6293 (3, 64): "d",
6294 (6, 64): "F",
6295 (6, 128): "D",
6296 (1, (5, 6, 5)): "B",
6297 }
6298
6299 def COMPESSORS():
6300 # Map COMPRESSION to compress functions and default compression levels
6301
6302 class Compressors(object):
6303 """Delay import compressor functions."""
6304
6305 def __init__(self):
6306 self._compressors = {8: (zlib.compress, 6), 32946: (zlib.compress, 6)}
6307
6308 def __getitem__(self, key):
6309 if key in self._compressors:
6310 return self._compressors[key]
6311
6312 if key == 34925:
6313 try:
6314 import lzma # delayed import
6315 except ImportError:
6316 try:
6317 import backports.lzma as lzma # delayed import
6318 except ImportError:
6319 raise KeyError
6320
6321 def lzma_compress(x, level):
6322 return lzma.compress(x)
6323
6324 self._compressors[key] = lzma_compress, 0
6325 return lzma_compress, 0
6326
6327 if key == 34926:
6328 try:
6329 import zstd # delayed import
6330 except ImportError:
6331 raise KeyError
6332 self._compressors[key] = zstd.compress, 9
6333 return zstd.compress, 9
6334
6335 raise KeyError
6336
6337 def __contains__(self, key):
6338 try:
6339 self[key]
6340 return True
6341 except KeyError:
6342 return False
6343
6344 return Compressors()
6345
6346 def DECOMPESSORS():
6347 # Map COMPRESSION to decompress functions
6348
6349 class Decompressors(object):
6350 """Delay import decompressor functions."""
6351
6352 def __init__(self):
6353 self._decompressors = {
6354 None: identityfunc,
6355 1: identityfunc,
6356 5: decode_lzw,
6357 8: zlib.decompress,
6358 32773: decode_packbits,
6359 32946: zlib.decompress,
6360 }
6361
6362 def __getitem__(self, key):
6363 if key in self._decompressors:
6364 return self._decompressors[key]
6365
6366 if key == 7:
6367 try:
6368 from imagecodecs import jpeg, jpeg_12
6369 except ImportError:
6370 raise KeyError
6371
6372 def decode_jpeg(x, table, bps, colorspace=None):
6373 if bps == 8:
6374 return jpeg.decode_jpeg(x, table, colorspace)
6375 elif bps == 12:
6376 return jpeg_12.decode_jpeg_12(x, table, colorspace)
6377 else:
6378 raise ValueError("bitspersample not supported")
6379
6380 self._decompressors[key] = decode_jpeg
6381 return decode_jpeg
6382
6383 if key == 34925:
6384 try:
6385 import lzma # delayed import
6386 except ImportError:
6387 try:
6388 import backports.lzma as lzma # delayed import
6389 except ImportError:
6390 raise KeyError
6391 self._decompressors[key] = lzma.decompress
6392 return lzma.decompress
6393
6394 if key == 34926:
6395 try:
6396 import zstd # delayed import
6397 except ImportError:
6398 raise KeyError
6399 self._decompressors[key] = zstd.decompress
6400 return zstd.decompress
6401 raise KeyError
6402
6403 def __contains__(self, item):
6404 try:
6405 self[item]
6406 return True
6407 except KeyError:
6408 return False
6409
6410 return Decompressors()
6411
6412 def FRAME_ATTRS():
6413 # Attributes that a TiffFrame shares with its keyframe
6414 return set("shape ndim size dtype axes is_final".split())
6415
6416 def FILE_FLAGS():
6417 # TiffFile and TiffPage 'is_\*' attributes
6418 exclude = set(
6419 "reduced final memmappable contiguous tiled " "chroma_subsampled".split()
6420 )
6421 return set(
6422 a[3:] for a in dir(TiffPage) if a[:3] == "is_" and a[3:] not in exclude
6423 )
6424
6425 def FILE_EXTENSIONS():
6426 # TIFF file extensions
6427 return tuple(
6428 "tif tiff ome.tif lsm stk qptiff pcoraw "
6429 "gel seq svs bif tf8 tf2 btf".split()
6430 )
6431
6432 def FILEOPEN_FILTER():
6433 # String for use in Windows File Open box
6434 return [
6435 ("%s files" % ext.upper(), "*.%s" % ext) for ext in TIFF.FILE_EXTENSIONS
6436 ] + [("allfiles", "*")]
6437
6438 def AXES_LABELS():
6439 # TODO: is there a standard for character axes labels?
6440 axes = {
6441 "X": "width",
6442 "Y": "height",
6443 "Z": "depth",
6444 "S": "sample", # rgb(a)
6445 "I": "series", # general sequence, plane, page, IFD
6446 "T": "time",
6447 "C": "channel", # color, emission wavelength
6448 "A": "angle",
6449 "P": "phase", # formerly F # P is Position in LSM!
6450 "R": "tile", # region, point, mosaic
6451 "H": "lifetime", # histogram
6452 "E": "lambda", # excitation wavelength
6453 "L": "exposure", # lux
6454 "V": "event",
6455 "Q": "other",
6456 "M": "mosaic", # LSM 6
6457 }
6458 axes.update(dict((v, k) for k, v in axes.items()))
6459 return axes
6460
6461 def ANDOR_TAGS():
6462 # Andor Technology tags #4864 - 5030
6463 return set(range(4864, 5030))
6464
6465 def EXIF_TAGS():
6466 tags = {
6467 # 65000 - 65112 Photoshop Camera RAW EXIF tags
6468 65000: "OwnerName",
6469 65001: "SerialNumber",
6470 65002: "Lens",
6471 65100: "RawFile",
6472 65101: "Converter",
6473 65102: "WhiteBalance",
6474 65105: "Exposure",
6475 65106: "Shadows",
6476 65107: "Brightness",
6477 65108: "Contrast",
6478 65109: "Saturation",
6479 65110: "Sharpness",
6480 65111: "Smoothness",
6481 65112: "MoireFilter",
6482 }
6483 tags.update(TIFF.TAGS)
6484 return tags
6485
6486 def GPS_TAGS():
6487 return {
6488 0: "GPSVersionID",
6489 1: "GPSLatitudeRef",
6490 2: "GPSLatitude",
6491 3: "GPSLongitudeRef",
6492 4: "GPSLongitude",
6493 5: "GPSAltitudeRef",
6494 6: "GPSAltitude",
6495 7: "GPSTimeStamp",
6496 8: "GPSSatellites",
6497 9: "GPSStatus",
6498 10: "GPSMeasureMode",
6499 11: "GPSDOP",
6500 12: "GPSSpeedRef",
6501 13: "GPSSpeed",
6502 14: "GPSTrackRef",
6503 15: "GPSTrack",
6504 16: "GPSImgDirectionRef",
6505 17: "GPSImgDirection",
6506 18: "GPSMapDatum",
6507 19: "GPSDestLatitudeRef",
6508 20: "GPSDestLatitude",
6509 21: "GPSDestLongitudeRef",
6510 22: "GPSDestLongitude",
6511 23: "GPSDestBearingRef",
6512 24: "GPSDestBearing",
6513 25: "GPSDestDistanceRef",
6514 26: "GPSDestDistance",
6515 27: "GPSProcessingMethod",
6516 28: "GPSAreaInformation",
6517 29: "GPSDateStamp",
6518 30: "GPSDifferential",
6519 31: "GPSHPositioningError",
6520 }
6521
6522 def IOP_TAGS():
6523 return {
6524 1: "InteroperabilityIndex",
6525 2: "InteroperabilityVersion",
6526 4096: "RelatedImageFileFormat",
6527 4097: "RelatedImageWidth",
6528 4098: "RelatedImageLength",
6529 }
6530
6531 def GEO_KEYS():
6532 return {
6533 1024: "GTModelTypeGeoKey",
6534 1025: "GTRasterTypeGeoKey",
6535 1026: "GTCitationGeoKey",
6536 2048: "GeographicTypeGeoKey",
6537 2049: "GeogCitationGeoKey",
6538 2050: "GeogGeodeticDatumGeoKey",
6539 2051: "GeogPrimeMeridianGeoKey",
6540 2052: "GeogLinearUnitsGeoKey",
6541 2053: "GeogLinearUnitSizeGeoKey",
6542 2054: "GeogAngularUnitsGeoKey",
6543 2055: "GeogAngularUnitsSizeGeoKey",
6544 2056: "GeogEllipsoidGeoKey",
6545 2057: "GeogSemiMajorAxisGeoKey",
6546 2058: "GeogSemiMinorAxisGeoKey",
6547 2059: "GeogInvFlatteningGeoKey",
6548 2060: "GeogAzimuthUnitsGeoKey",
6549 2061: "GeogPrimeMeridianLongGeoKey",
6550 2062: "GeogTOWGS84GeoKey",
6551 3059: "ProjLinearUnitsInterpCorrectGeoKey", # GDAL
6552 3072: "ProjectedCSTypeGeoKey",
6553 3073: "PCSCitationGeoKey",
6554 3074: "ProjectionGeoKey",
6555 3075: "ProjCoordTransGeoKey",
6556 3076: "ProjLinearUnitsGeoKey",
6557 3077: "ProjLinearUnitSizeGeoKey",
6558 3078: "ProjStdParallel1GeoKey",
6559 3079: "ProjStdParallel2GeoKey",
6560 3080: "ProjNatOriginLongGeoKey",
6561 3081: "ProjNatOriginLatGeoKey",
6562 3082: "ProjFalseEastingGeoKey",
6563 3083: "ProjFalseNorthingGeoKey",
6564 3084: "ProjFalseOriginLongGeoKey",
6565 3085: "ProjFalseOriginLatGeoKey",
6566 3086: "ProjFalseOriginEastingGeoKey",
6567 3087: "ProjFalseOriginNorthingGeoKey",
6568 3088: "ProjCenterLongGeoKey",
6569 3089: "ProjCenterLatGeoKey",
6570 3090: "ProjCenterEastingGeoKey",
6571 3091: "ProjFalseOriginNorthingGeoKey",
6572 3092: "ProjScaleAtNatOriginGeoKey",
6573 3093: "ProjScaleAtCenterGeoKey",
6574 3094: "ProjAzimuthAngleGeoKey",
6575 3095: "ProjStraightVertPoleLongGeoKey",
6576 3096: "ProjRectifiedGridAngleGeoKey",
6577 4096: "VerticalCSTypeGeoKey",
6578 4097: "VerticalCitationGeoKey",
6579 4098: "VerticalDatumGeoKey",
6580 4099: "VerticalUnitsGeoKey",
6581 }
6582
6583 def GEO_CODES():
6584 try:
6585 from .tifffile_geodb import GEO_CODES # delayed import
6586 except (ImportError, ValueError):
6587 try:
6588 from tifffile_geodb import GEO_CODES # delayed import
6589 except (ImportError, ValueError):
6590 GEO_CODES = {}
6591 return GEO_CODES
6592
6593 def CZ_LSMINFO():
6594 return [
6595 ("MagicNumber", "u4"),
6596 ("StructureSize", "i4"),
6597 ("DimensionX", "i4"),
6598 ("DimensionY", "i4"),
6599 ("DimensionZ", "i4"),
6600 ("DimensionChannels", "i4"),
6601 ("DimensionTime", "i4"),
6602 ("DataType", "i4"), # DATATYPES
6603 ("ThumbnailX", "i4"),
6604 ("ThumbnailY", "i4"),
6605 ("VoxelSizeX", "f8"),
6606 ("VoxelSizeY", "f8"),
6607 ("VoxelSizeZ", "f8"),
6608 ("OriginX", "f8"),
6609 ("OriginY", "f8"),
6610 ("OriginZ", "f8"),
6611 ("ScanType", "u2"),
6612 ("SpectralScan", "u2"),
6613 ("TypeOfData", "u4"), # TYPEOFDATA
6614 ("OffsetVectorOverlay", "u4"),
6615 ("OffsetInputLut", "u4"),
6616 ("OffsetOutputLut", "u4"),
6617 ("OffsetChannelColors", "u4"),
6618 ("TimeIntervall", "f8"),
6619 ("OffsetChannelDataTypes", "u4"),
6620 ("OffsetScanInformation", "u4"), # SCANINFO
6621 ("OffsetKsData", "u4"),
6622 ("OffsetTimeStamps", "u4"),
6623 ("OffsetEventList", "u4"),
6624 ("OffsetRoi", "u4"),
6625 ("OffsetBleachRoi", "u4"),
6626 ("OffsetNextRecording", "u4"),
6627 # LSM 2.0 ends here
6628 ("DisplayAspectX", "f8"),
6629 ("DisplayAspectY", "f8"),
6630 ("DisplayAspectZ", "f8"),
6631 ("DisplayAspectTime", "f8"),
6632 ("OffsetMeanOfRoisOverlay", "u4"),
6633 ("OffsetTopoIsolineOverlay", "u4"),
6634 ("OffsetTopoProfileOverlay", "u4"),
6635 ("OffsetLinescanOverlay", "u4"),
6636 ("ToolbarFlags", "u4"),
6637 ("OffsetChannelWavelength", "u4"),
6638 ("OffsetChannelFactors", "u4"),
6639 ("ObjectiveSphereCorrection", "f8"),
6640 ("OffsetUnmixParameters", "u4"),
6641 # LSM 3.2, 4.0 end here
6642 ("OffsetAcquisitionParameters", "u4"),
6643 ("OffsetCharacteristics", "u4"),
6644 ("OffsetPalette", "u4"),
6645 ("TimeDifferenceX", "f8"),
6646 ("TimeDifferenceY", "f8"),
6647 ("TimeDifferenceZ", "f8"),
6648 ("InternalUse1", "u4"),
6649 ("DimensionP", "i4"),
6650 ("DimensionM", "i4"),
6651 ("DimensionsReserved", "16i4"),
6652 ("OffsetTilePositions", "u4"),
6653 ("", "9u4"), # Reserved
6654 ("OffsetPositions", "u4"),
6655 # ('', '21u4'), # must be 0
6656 ]
6657
6658 def CZ_LSMINFO_READERS():
6659 # Import functions for CZ_LSMINFO sub-records
6660 # TODO: read more CZ_LSMINFO sub-records
6661 return {
6662 "ScanInformation": read_lsm_scaninfo,
6663 "TimeStamps": read_lsm_timestamps,
6664 "EventList": read_lsm_eventlist,
6665 "ChannelColors": read_lsm_channelcolors,
6666 "Positions": read_lsm_floatpairs,
6667 "TilePositions": read_lsm_floatpairs,
6668 "VectorOverlay": None,
6669 "InputLut": None,
6670 "OutputLut": None,
6671 "TimeIntervall": None,
6672 "ChannelDataTypes": None,
6673 "KsData": None,
6674 "Roi": None,
6675 "BleachRoi": None,
6676 "NextRecording": None,
6677 "MeanOfRoisOverlay": None,
6678 "TopoIsolineOverlay": None,
6679 "TopoProfileOverlay": None,
6680 "ChannelWavelength": None,
6681 "SphereCorrection": None,
6682 "ChannelFactors": None,
6683 "UnmixParameters": None,
6684 "AcquisitionParameters": None,
6685 "Characteristics": None,
6686 }
6687
6688 def CZ_LSMINFO_SCANTYPE():
6689 # Map CZ_LSMINFO.ScanType to dimension order
6690 return {
6691 0: "XYZCT", # 'Stack' normal x-y-z-scan
6692 1: "XYZCT", # 'Z-Scan' x-z-plane Y=1
6693 2: "XYZCT", # 'Line'
6694 3: "XYTCZ", # 'Time Series Plane' time series x-y XYCTZ ? Z=1
6695 4: "XYZTC", # 'Time Series z-Scan' time series x-z
6696 5: "XYTCZ", # 'Time Series Mean-of-ROIs'
6697 6: "XYZTC", # 'Time Series Stack' time series x-y-z
6698 7: "XYCTZ", # Spline Scan
6699 8: "XYCZT", # Spline Plane x-z
6700 9: "XYTCZ", # Time Series Spline Plane x-z
6701 10: "XYZCT", # 'Time Series Point' point mode
6702 }
6703
6704 def CZ_LSMINFO_DIMENSIONS():
6705 # Map dimension codes to CZ_LSMINFO attribute
6706 return {
6707 "X": "DimensionX",
6708 "Y": "DimensionY",
6709 "Z": "DimensionZ",
6710 "C": "DimensionChannels",
6711 "T": "DimensionTime",
6712 "P": "DimensionP",
6713 "M": "DimensionM",
6714 }
6715
6716 def CZ_LSMINFO_DATATYPES():
6717 # Description of CZ_LSMINFO.DataType
6718 return {
6719 0: "varying data types",
6720 1: "8 bit unsigned integer",
6721 2: "12 bit unsigned integer",
6722 5: "32 bit float",
6723 }
6724
6725 def CZ_LSMINFO_TYPEOFDATA():
6726 # Description of CZ_LSMINFO.TypeOfData
6727 return {
6728 0: "Original scan data",
6729 1: "Calculated data",
6730 2: "3D reconstruction",
6731 3: "Topography height map",
6732 }
6733
6734 def CZ_LSMINFO_SCANINFO_ARRAYS():
6735 return {
6736 0x20000000: "Tracks",
6737 0x30000000: "Lasers",
6738 0x60000000: "DetectionChannels",
6739 0x80000000: "IlluminationChannels",
6740 0xA0000000: "BeamSplitters",
6741 0xC0000000: "DataChannels",
6742 0x11000000: "Timers",
6743 0x13000000: "Markers",
6744 }
6745
6746 def CZ_LSMINFO_SCANINFO_STRUCTS():
6747 return {
6748 # 0x10000000: 'Recording',
6749 0x40000000: "Track",
6750 0x50000000: "Laser",
6751 0x70000000: "DetectionChannel",
6752 0x90000000: "IlluminationChannel",
6753 0xB0000000: "BeamSplitter",
6754 0xD0000000: "DataChannel",
6755 0x12000000: "Timer",
6756 0x14000000: "Marker",
6757 }
6758
6759 def CZ_LSMINFO_SCANINFO_ATTRIBUTES():
6760 return {
6761 # Recording
6762 0x10000001: "Name",
6763 0x10000002: "Description",
6764 0x10000003: "Notes",
6765 0x10000004: "Objective",
6766 0x10000005: "ProcessingSummary",
6767 0x10000006: "SpecialScanMode",
6768 0x10000007: "ScanType",
6769 0x10000008: "ScanMode",
6770 0x10000009: "NumberOfStacks",
6771 0x1000000A: "LinesPerPlane",
6772 0x1000000B: "SamplesPerLine",
6773 0x1000000C: "PlanesPerVolume",
6774 0x1000000D: "ImagesWidth",
6775 0x1000000E: "ImagesHeight",
6776 0x1000000F: "ImagesNumberPlanes",
6777 0x10000010: "ImagesNumberStacks",
6778 0x10000011: "ImagesNumberChannels",
6779 0x10000012: "LinscanXySize",
6780 0x10000013: "ScanDirection",
6781 0x10000014: "TimeSeries",
6782 0x10000015: "OriginalScanData",
6783 0x10000016: "ZoomX",
6784 0x10000017: "ZoomY",
6785 0x10000018: "ZoomZ",
6786 0x10000019: "Sample0X",
6787 0x1000001A: "Sample0Y",
6788 0x1000001B: "Sample0Z",
6789 0x1000001C: "SampleSpacing",
6790 0x1000001D: "LineSpacing",
6791 0x1000001E: "PlaneSpacing",
6792 0x1000001F: "PlaneWidth",
6793 0x10000020: "PlaneHeight",
6794 0x10000021: "VolumeDepth",
6795 0x10000023: "Nutation",
6796 0x10000034: "Rotation",
6797 0x10000035: "Precession",
6798 0x10000036: "Sample0time",
6799 0x10000037: "StartScanTriggerIn",
6800 0x10000038: "StartScanTriggerOut",
6801 0x10000039: "StartScanEvent",
6802 0x10000040: "StartScanTime",
6803 0x10000041: "StopScanTriggerIn",
6804 0x10000042: "StopScanTriggerOut",
6805 0x10000043: "StopScanEvent",
6806 0x10000044: "StopScanTime",
6807 0x10000045: "UseRois",
6808 0x10000046: "UseReducedMemoryRois",
6809 0x10000047: "User",
6810 0x10000048: "UseBcCorrection",
6811 0x10000049: "PositionBcCorrection1",
6812 0x10000050: "PositionBcCorrection2",
6813 0x10000051: "InterpolationY",
6814 0x10000052: "CameraBinning",
6815 0x10000053: "CameraSupersampling",
6816 0x10000054: "CameraFrameWidth",
6817 0x10000055: "CameraFrameHeight",
6818 0x10000056: "CameraOffsetX",
6819 0x10000057: "CameraOffsetY",
6820 0x10000059: "RtBinning",
6821 0x1000005A: "RtFrameWidth",
6822 0x1000005B: "RtFrameHeight",
6823 0x1000005C: "RtRegionWidth",
6824 0x1000005D: "RtRegionHeight",
6825 0x1000005E: "RtOffsetX",
6826 0x1000005F: "RtOffsetY",
6827 0x10000060: "RtZoom",
6828 0x10000061: "RtLinePeriod",
6829 0x10000062: "Prescan",
6830 0x10000063: "ScanDirectionZ",
6831 # Track
6832 0x40000001: "MultiplexType", # 0 After Line; 1 After Frame
6833 0x40000002: "MultiplexOrder",
6834 0x40000003: "SamplingMode", # 0 Sample; 1 Line Avg; 2 Frame Avg
6835 0x40000004: "SamplingMethod", # 1 Mean; 2 Sum
6836 0x40000005: "SamplingNumber",
6837 0x40000006: "Acquire",
6838 0x40000007: "SampleObservationTime",
6839 0x4000000B: "TimeBetweenStacks",
6840 0x4000000C: "Name",
6841 0x4000000D: "Collimator1Name",
6842 0x4000000E: "Collimator1Position",
6843 0x4000000F: "Collimator2Name",
6844 0x40000010: "Collimator2Position",
6845 0x40000011: "IsBleachTrack",
6846 0x40000012: "IsBleachAfterScanNumber",
6847 0x40000013: "BleachScanNumber",
6848 0x40000014: "TriggerIn",
6849 0x40000015: "TriggerOut",
6850 0x40000016: "IsRatioTrack",
6851 0x40000017: "BleachCount",
6852 0x40000018: "SpiCenterWavelength",
6853 0x40000019: "PixelTime",
6854 0x40000021: "CondensorFrontlens",
6855 0x40000023: "FieldStopValue",
6856 0x40000024: "IdCondensorAperture",
6857 0x40000025: "CondensorAperture",
6858 0x40000026: "IdCondensorRevolver",
6859 0x40000027: "CondensorFilter",
6860 0x40000028: "IdTransmissionFilter1",
6861 0x40000029: "IdTransmission1",
6862 0x40000030: "IdTransmissionFilter2",
6863 0x40000031: "IdTransmission2",
6864 0x40000032: "RepeatBleach",
6865 0x40000033: "EnableSpotBleachPos",
6866 0x40000034: "SpotBleachPosx",
6867 0x40000035: "SpotBleachPosy",
6868 0x40000036: "SpotBleachPosz",
6869 0x40000037: "IdTubelens",
6870 0x40000038: "IdTubelensPosition",
6871 0x40000039: "TransmittedLight",
6872 0x4000003A: "ReflectedLight",
6873 0x4000003B: "SimultanGrabAndBleach",
6874 0x4000003C: "BleachPixelTime",
6875 # Laser
6876 0x50000001: "Name",
6877 0x50000002: "Acquire",
6878 0x50000003: "Power",
6879 # DetectionChannel
6880 0x70000001: "IntegrationMode",
6881 0x70000002: "SpecialMode",
6882 0x70000003: "DetectorGainFirst",
6883 0x70000004: "DetectorGainLast",
6884 0x70000005: "AmplifierGainFirst",
6885 0x70000006: "AmplifierGainLast",
6886 0x70000007: "AmplifierOffsFirst",
6887 0x70000008: "AmplifierOffsLast",
6888 0x70000009: "PinholeDiameter",
6889 0x7000000A: "CountingTrigger",
6890 0x7000000B: "Acquire",
6891 0x7000000C: "PointDetectorName",
6892 0x7000000D: "AmplifierName",
6893 0x7000000E: "PinholeName",
6894 0x7000000F: "FilterSetName",
6895 0x70000010: "FilterName",
6896 0x70000013: "IntegratorName",
6897 0x70000014: "ChannelName",
6898 0x70000015: "DetectorGainBc1",
6899 0x70000016: "DetectorGainBc2",
6900 0x70000017: "AmplifierGainBc1",
6901 0x70000018: "AmplifierGainBc2",
6902 0x70000019: "AmplifierOffsetBc1",
6903 0x70000020: "AmplifierOffsetBc2",
6904 0x70000021: "SpectralScanChannels",
6905 0x70000022: "SpiWavelengthStart",
6906 0x70000023: "SpiWavelengthStop",
6907 0x70000026: "DyeName",
6908 0x70000027: "DyeFolder",
6909 # IlluminationChannel
6910 0x90000001: "Name",
6911 0x90000002: "Power",
6912 0x90000003: "Wavelength",
6913 0x90000004: "Aquire",
6914 0x90000005: "DetchannelName",
6915 0x90000006: "PowerBc1",
6916 0x90000007: "PowerBc2",
6917 # BeamSplitter
6918 0xB0000001: "FilterSet",
6919 0xB0000002: "Filter",
6920 0xB0000003: "Name",
6921 # DataChannel
6922 0xD0000001: "Name",
6923 0xD0000003: "Acquire",
6924 0xD0000004: "Color",
6925 0xD0000005: "SampleType",
6926 0xD0000006: "BitsPerSample",
6927 0xD0000007: "RatioType",
6928 0xD0000008: "RatioTrack1",
6929 0xD0000009: "RatioTrack2",
6930 0xD000000A: "RatioChannel1",
6931 0xD000000B: "RatioChannel2",
6932 0xD000000C: "RatioConst1",
6933 0xD000000D: "RatioConst2",
6934 0xD000000E: "RatioConst3",
6935 0xD000000F: "RatioConst4",
6936 0xD0000010: "RatioConst5",
6937 0xD0000011: "RatioConst6",
6938 0xD0000012: "RatioFirstImages1",
6939 0xD0000013: "RatioFirstImages2",
6940 0xD0000014: "DyeName",
6941 0xD0000015: "DyeFolder",
6942 0xD0000016: "Spectrum",
6943 0xD0000017: "Acquire",
6944 # Timer
6945 0x12000001: "Name",
6946 0x12000002: "Description",
6947 0x12000003: "Interval",
6948 0x12000004: "TriggerIn",
6949 0x12000005: "TriggerOut",
6950 0x12000006: "ActivationTime",
6951 0x12000007: "ActivationNumber",
6952 # Marker
6953 0x14000001: "Name",
6954 0x14000002: "Description",
6955 0x14000003: "TriggerIn",
6956 0x14000004: "TriggerOut",
6957 }
6958
6959 def NIH_IMAGE_HEADER():
6960 return [
6961 ("FileID", "a8"),
6962 ("nLines", "i2"),
6963 ("PixelsPerLine", "i2"),
6964 ("Version", "i2"),
6965 ("OldLutMode", "i2"),
6966 ("OldnColors", "i2"),
6967 ("Colors", "u1", (3, 32)),
6968 ("OldColorStart", "i2"),
6969 ("ColorWidth", "i2"),
6970 ("ExtraColors", "u2", (6, 3)),
6971 ("nExtraColors", "i2"),
6972 ("ForegroundIndex", "i2"),
6973 ("BackgroundIndex", "i2"),
6974 ("XScale", "f8"),
6975 ("Unused2", "i2"),
6976 ("Unused3", "i2"),
6977 ("UnitsID", "i2"), # NIH_UNITS_TYPE
6978 ("p1", [("x", "i2"), ("y", "i2")]),
6979 ("p2", [("x", "i2"), ("y", "i2")]),
6980 ("CurveFitType", "i2"), # NIH_CURVEFIT_TYPE
6981 ("nCoefficients", "i2"),
6982 ("Coeff", "f8", 6),
6983 ("UMsize", "u1"),
6984 ("UM", "a15"),
6985 ("UnusedBoolean", "u1"),
6986 ("BinaryPic", "b1"),
6987 ("SliceStart", "i2"),
6988 ("SliceEnd", "i2"),
6989 ("ScaleMagnification", "f4"),
6990 ("nSlices", "i2"),
6991 ("SliceSpacing", "f4"),
6992 ("CurrentSlice", "i2"),
6993 ("FrameInterval", "f4"),
6994 ("PixelAspectRatio", "f4"),
6995 ("ColorStart", "i2"),
6996 ("ColorEnd", "i2"),
6997 ("nColors", "i2"),
6998 ("Fill1", "3u2"),
6999 ("Fill2", "3u2"),
7000 ("Table", "u1"), # NIH_COLORTABLE_TYPE
7001 ("LutMode", "u1"), # NIH_LUTMODE_TYPE
7002 ("InvertedTable", "b1"),
7003 ("ZeroClip", "b1"),
7004 ("XUnitSize", "u1"),
7005 ("XUnit", "a11"),
7006 ("StackType", "i2"), # NIH_STACKTYPE_TYPE
7007 # ('UnusedBytes', 'u1', 200)
7008 ]
7009
7010 def NIH_COLORTABLE_TYPE():
7011 return (
7012 "CustomTable",
7013 "AppleDefault",
7014 "Pseudo20",
7015 "Pseudo32",
7016 "Rainbow",
7017 "Fire1",
7018 "Fire2",
7019 "Ice",
7020 "Grays",
7021 "Spectrum",
7022 )
7023
7024 def NIH_LUTMODE_TYPE():
7025 return (
7026 "PseudoColor",
7027 "OldAppleDefault",
7028 "OldSpectrum",
7029 "GrayScale",
7030 "ColorLut",
7031 "CustomGrayscale",
7032 )
7033
7034 def NIH_CURVEFIT_TYPE():
7035 return (
7036 "StraightLine",
7037 "Poly2",
7038 "Poly3",
7039 "Poly4",
7040 "Poly5",
7041 "ExpoFit",
7042 "PowerFit",
7043 "LogFit",
7044 "RodbardFit",
7045 "SpareFit1",
7046 "Uncalibrated",
7047 "UncalibratedOD",
7048 )
7049
7050 def NIH_UNITS_TYPE():
7051 return (
7052 "Nanometers",
7053 "Micrometers",
7054 "Millimeters",
7055 "Centimeters",
7056 "Meters",
7057 "Kilometers",
7058 "Inches",
7059 "Feet",
7060 "Miles",
7061 "Pixels",
7062 "OtherUnits",
7063 )
7064
7065 def NIH_STACKTYPE_TYPE():
7066 return ("VolumeStack", "RGBStack", "MovieStack", "HSVStack")
7067
7068 def TVIPS_HEADER_V1():
7069 # TVIPS TemData structure from EMMENU Help file
7070 return [
7071 ("Version", "i4"),
7072 ("CommentV1", "a80"),
7073 ("HighTension", "i4"),
7074 ("SphericalAberration", "i4"),
7075 ("IlluminationAperture", "i4"),
7076 ("Magnification", "i4"),
7077 ("PostMagnification", "i4"),
7078 ("FocalLength", "i4"),
7079 ("Defocus", "i4"),
7080 ("Astigmatism", "i4"),
7081 ("AstigmatismDirection", "i4"),
7082 ("BiprismVoltage", "i4"),
7083 ("SpecimenTiltAngle", "i4"),
7084 ("SpecimenTiltDirection", "i4"),
7085 ("IlluminationTiltDirection", "i4"),
7086 ("IlluminationTiltAngle", "i4"),
7087 ("ImageMode", "i4"),
7088 ("EnergySpread", "i4"),
7089 ("ChromaticAberration", "i4"),
7090 ("ShutterType", "i4"),
7091 ("DefocusSpread", "i4"),
7092 ("CcdNumber", "i4"),
7093 ("CcdSize", "i4"),
7094 ("OffsetXV1", "i4"),
7095 ("OffsetYV1", "i4"),
7096 ("PhysicalPixelSize", "i4"),
7097 ("Binning", "i4"),
7098 ("ReadoutSpeed", "i4"),
7099 ("GainV1", "i4"),
7100 ("SensitivityV1", "i4"),
7101 ("ExposureTimeV1", "i4"),
7102 ("FlatCorrected", "i4"),
7103 ("DeadPxCorrected", "i4"),
7104 ("ImageMean", "i4"),
7105 ("ImageStd", "i4"),
7106 ("DisplacementX", "i4"),
7107 ("DisplacementY", "i4"),
7108 ("DateV1", "i4"),
7109 ("TimeV1", "i4"),
7110 ("ImageMin", "i4"),
7111 ("ImageMax", "i4"),
7112 ("ImageStatisticsQuality", "i4"),
7113 ]
7114
7115 def TVIPS_HEADER_V2():
7116 return [
7117 ("ImageName", "V160"), # utf16
7118 ("ImageFolder", "V160"),
7119 ("ImageSizeX", "i4"),
7120 ("ImageSizeY", "i4"),
7121 ("ImageSizeZ", "i4"),
7122 ("ImageSizeE", "i4"),
7123 ("ImageDataType", "i4"),
7124 ("Date", "i4"),
7125 ("Time", "i4"),
7126 ("Comment", "V1024"),
7127 ("ImageHistory", "V1024"),
7128 ("Scaling", "16f4"),
7129 ("ImageStatistics", "16c16"),
7130 ("ImageType", "i4"),
7131 ("ImageDisplaType", "i4"),
7132 ("PixelSizeX", "f4"), # distance between two px in x, [nm]
7133 ("PixelSizeY", "f4"), # distance between two px in y, [nm]
7134 ("ImageDistanceZ", "f4"),
7135 ("ImageDistanceE", "f4"),
7136 ("ImageMisc", "32f4"),
7137 ("TemType", "V160"),
7138 ("TemHighTension", "f4"),
7139 ("TemAberrations", "32f4"),
7140 ("TemEnergy", "32f4"),
7141 ("TemMode", "i4"),
7142 ("TemMagnification", "f4"),
7143 ("TemMagnificationCorrection", "f4"),
7144 ("PostMagnification", "f4"),
7145 ("TemStageType", "i4"),
7146 ("TemStagePosition", "5f4"), # x, y, z, a, b
7147 ("TemImageShift", "2f4"),
7148 ("TemBeamShift", "2f4"),
7149 ("TemBeamTilt", "2f4"),
7150 ("TilingParameters", "7f4"), # 0: tiling? 1:x 2:y 3: max x
7151 # 4: max y 5: overlap x 6: overlap y
7152 ("TemIllumination", "3f4"), # 0: spotsize 1: intensity
7153 ("TemShutter", "i4"),
7154 ("TemMisc", "32f4"),
7155 ("CameraType", "V160"),
7156 ("PhysicalPixelSizeX", "f4"),
7157 ("PhysicalPixelSizeY", "f4"),
7158 ("OffsetX", "i4"),
7159 ("OffsetY", "i4"),
7160 ("BinningX", "i4"),
7161 ("BinningY", "i4"),
7162 ("ExposureTime", "f4"),
7163 ("Gain", "f4"),
7164 ("ReadoutRate", "f4"),
7165 ("FlatfieldDescription", "V160"),
7166 ("Sensitivity", "f4"),
7167 ("Dose", "f4"),
7168 ("CamMisc", "32f4"),
7169 ("FeiMicroscopeInformation", "V1024"),
7170 ("FeiSpecimenInformation", "V1024"),
7171 ("Magic", "u4"),
7172 ]
7173
7174 def MM_HEADER():
7175 # Olympus FluoView MM_Header
7176 MM_DIMENSION = [
7177 ("Name", "a16"),
7178 ("Size", "i4"),
7179 ("Origin", "f8"),
7180 ("Resolution", "f8"),
7181 ("Unit", "a64"),
7182 ]
7183 return [
7184 ("HeaderFlag", "i2"),
7185 ("ImageType", "u1"),
7186 ("ImageName", "a257"),
7187 ("OffsetData", "u4"),
7188 ("PaletteSize", "i4"),
7189 ("OffsetPalette0", "u4"),
7190 ("OffsetPalette1", "u4"),
7191 ("CommentSize", "i4"),
7192 ("OffsetComment", "u4"),
7193 ("Dimensions", MM_DIMENSION, 10),
7194 ("OffsetPosition", "u4"),
7195 ("MapType", "i2"),
7196 ("MapMin", "f8"),
7197 ("MapMax", "f8"),
7198 ("MinValue", "f8"),
7199 ("MaxValue", "f8"),
7200 ("OffsetMap", "u4"),
7201 ("Gamma", "f8"),
7202 ("Offset", "f8"),
7203 ("GrayChannel", MM_DIMENSION),
7204 ("OffsetThumbnail", "u4"),
7205 ("VoiceField", "i4"),
7206 ("OffsetVoiceField", "u4"),
7207 ]
7208
7209 def MM_DIMENSIONS():
7210 # Map FluoView MM_Header.Dimensions to axes characters
7211 return {
7212 "X": "X",
7213 "Y": "Y",
7214 "Z": "Z",
7215 "T": "T",
7216 "CH": "C",
7217 "WAVELENGTH": "C",
7218 "TIME": "T",
7219 "XY": "R",
7220 "EVENT": "V",
7221 "EXPOSURE": "L",
7222 }
7223
7224 def UIC_TAGS():
7225 # Map Universal Imaging Corporation MetaMorph internal tag ids to
7226 # name and type
7227 from fractions import Fraction # delayed import
7228
7229 return [
7230 ("AutoScale", int),
7231 ("MinScale", int),
7232 ("MaxScale", int),
7233 ("SpatialCalibration", int),
7234 ("XCalibration", Fraction),
7235 ("YCalibration", Fraction),
7236 ("CalibrationUnits", str),
7237 ("Name", str),
7238 ("ThreshState", int),
7239 ("ThreshStateRed", int),
7240 ("tagid_10", None), # undefined
7241 ("ThreshStateGreen", int),
7242 ("ThreshStateBlue", int),
7243 ("ThreshStateLo", int),
7244 ("ThreshStateHi", int),
7245 ("Zoom", int),
7246 ("CreateTime", julian_datetime),
7247 ("LastSavedTime", julian_datetime),
7248 ("currentBuffer", int),
7249 ("grayFit", None),
7250 ("grayPointCount", None),
7251 ("grayX", Fraction),
7252 ("grayY", Fraction),
7253 ("grayMin", Fraction),
7254 ("grayMax", Fraction),
7255 ("grayUnitName", str),
7256 ("StandardLUT", int),
7257 ("wavelength", int),
7258 ("StagePosition", "(%i,2,2)u4"), # N xy positions as fract
7259 ("CameraChipOffset", "(%i,2,2)u4"), # N xy offsets as fract
7260 ("OverlayMask", None),
7261 ("OverlayCompress", None),
7262 ("Overlay", None),
7263 ("SpecialOverlayMask", None),
7264 ("SpecialOverlayCompress", None),
7265 ("SpecialOverlay", None),
7266 ("ImageProperty", read_uic_image_property),
7267 ("StageLabel", "%ip"), # N str
7268 ("AutoScaleLoInfo", Fraction),
7269 ("AutoScaleHiInfo", Fraction),
7270 ("AbsoluteZ", "(%i,2)u4"), # N fractions
7271 ("AbsoluteZValid", "(%i,)u4"), # N long
7272 ("Gamma", "I"), # 'I' uses offset
7273 ("GammaRed", "I"),
7274 ("GammaGreen", "I"),
7275 ("GammaBlue", "I"),
7276 ("CameraBin", "2I"),
7277 ("NewLUT", int),
7278 ("ImagePropertyEx", None),
7279 ("PlaneProperty", int),
7280 ("UserLutTable", "(256,3)u1"),
7281 ("RedAutoScaleInfo", int),
7282 ("RedAutoScaleLoInfo", Fraction),
7283 ("RedAutoScaleHiInfo", Fraction),
7284 ("RedMinScaleInfo", int),
7285 ("RedMaxScaleInfo", int),
7286 ("GreenAutoScaleInfo", int),
7287 ("GreenAutoScaleLoInfo", Fraction),
7288 ("GreenAutoScaleHiInfo", Fraction),
7289 ("GreenMinScaleInfo", int),
7290 ("GreenMaxScaleInfo", int),
7291 ("BlueAutoScaleInfo", int),
7292 ("BlueAutoScaleLoInfo", Fraction),
7293 ("BlueAutoScaleHiInfo", Fraction),
7294 ("BlueMinScaleInfo", int),
7295 ("BlueMaxScaleInfo", int),
7296 # ('OverlayPlaneColor', read_uic_overlay_plane_color),
7297 ]
7298
7299 def PILATUS_HEADER():
7300 # PILATUS CBF Header Specification, Version 1.4
7301 # Map key to [value_indices], type
7302 return {
7303 "Detector": ([slice(1, None)], str),
7304 "Pixel_size": ([1, 4], float),
7305 "Silicon": ([3], float),
7306 "Exposure_time": ([1], float),
7307 "Exposure_period": ([1], float),
7308 "Tau": ([1], float),
7309 "Count_cutoff": ([1], int),
7310 "Threshold_setting": ([1], float),
7311 "Gain_setting": ([1, 2], str),
7312 "N_excluded_pixels": ([1], int),
7313 "Excluded_pixels": ([1], str),
7314 "Flat_field": ([1], str),
7315 "Trim_file": ([1], str),
7316 "Image_path": ([1], str),
7317 # optional
7318 "Wavelength": ([1], float),
7319 "Energy_range": ([1, 2], float),
7320 "Detector_distance": ([1], float),
7321 "Detector_Voffset": ([1], float),
7322 "Beam_xy": ([1, 2], float),
7323 "Flux": ([1], str),
7324 "Filter_transmission": ([1], float),
7325 "Start_angle": ([1], float),
7326 "Angle_increment": ([1], float),
7327 "Detector_2theta": ([1], float),
7328 "Polarization": ([1], float),
7329 "Alpha": ([1], float),
7330 "Kappa": ([1], float),
7331 "Phi": ([1], float),
7332 "Phi_increment": ([1], float),
7333 "Chi": ([1], float),
7334 "Chi_increment": ([1], float),
7335 "Oscillation_axis": ([slice(1, None)], str),
7336 "N_oscillations": ([1], int),
7337 "Start_position": ([1], float),
7338 "Position_increment": ([1], float),
7339 "Shutter_time": ([1], float),
7340 "Omega": ([1], float),
7341 "Omega_increment": ([1], float),
7342 }
7343
7344 def REVERSE_BITORDER_BYTES():
7345 # Bytes with reversed bitorder
7346 return (
7347 b"\x00\x80@\xc0 \xa0`\xe0\x10\x90P\xd00\xb0p\xf0\x08\x88H\xc8("
7348 b"\xa8h\xe8\x18\x98X\xd88\xb8x\xf8\x04\x84D\xc4$\xa4d\xe4\x14"
7349 b"\x94T\xd44\xb4t\xf4\x0c\x8cL\xcc,\xacl\xec\x1c\x9c\\\xdc<\xbc|"
7350 b'\xfc\x02\x82B\xc2"\xa2b\xe2\x12\x92R\xd22\xb2r\xf2\n\x8aJ\xca*'
7351 b"\xaaj\xea\x1a\x9aZ\xda:\xbaz\xfa\x06\x86F\xc6&\xa6f\xe6\x16"
7352 b"\x96V\xd66\xb6v\xf6\x0e\x8eN\xce.\xaen\xee\x1e\x9e^\xde>\xbe~"
7353 b"\xfe\x01\x81A\xc1!\xa1a\xe1\x11\x91Q\xd11\xb1q\xf1\t\x89I\xc9)"
7354 b"\xa9i\xe9\x19\x99Y\xd99\xb9y\xf9\x05\x85E\xc5%\xa5e\xe5\x15"
7355 b"\x95U\xd55\xb5u\xf5\r\x8dM\xcd-\xadm\xed\x1d\x9d]\xdd=\xbd}"
7356 b"\xfd\x03\x83C\xc3#\xa3c\xe3\x13\x93S\xd33\xb3s\xf3\x0b\x8bK"
7357 b"\xcb+\xabk\xeb\x1b\x9b[\xdb;\xbb{\xfb\x07\x87G\xc7'\xa7g\xe7"
7358 b"\x17\x97W\xd77\xb7w\xf7\x0f\x8fO\xcf/\xafo\xef\x1f\x9f_"
7359 b"\xdf?\xbf\x7f\xff"
7360 )
7361
7362 def REVERSE_BITORDER_ARRAY():
7363 # Numpy array of bytes with reversed bitorder
7364 return numpy.frombuffer(TIFF.REVERSE_BITORDER_BYTES, dtype="uint8")
7365
7366 def ALLOCATIONGRANULARITY():
7367 # alignment for writing contiguous data to TIFF
7368 import mmap # delayed import
7369
7370 return mmap.ALLOCATIONGRANULARITY
7371
7372
7373def read_tags(fh, byteorder, offsetsize, tagnames, customtags=None, maxifds=None):
7374 """Read tags from chain of IFDs and return as list of dicts.
7375
7376 The file handle position must be at a valid IFD header.
7377
7378 """
7379 if offsetsize == 4:
7380 offsetformat = byteorder + "I"
7381 tagnosize = 2
7382 tagnoformat = byteorder + "H"
7383 tagsize = 12
7384 tagformat1 = byteorder + "HH"
7385 tagformat2 = byteorder + "I4s"
7386 elif offsetsize == 8:
7387 offsetformat = byteorder + "Q"
7388 tagnosize = 8
7389 tagnoformat = byteorder + "Q"
7390 tagsize = 20
7391 tagformat1 = byteorder + "HH"
7392 tagformat2 = byteorder + "Q8s"
7393 else:
7394 raise ValueError("invalid offset size")
7395
7396 if customtags is None:
7397 customtags = {}
7398 if maxifds is None:
7399 maxifds = 2**32
7400
7401 result = []
7402 unpack = struct.unpack
7403 offset = fh.tell()
7404 while len(result) < maxifds:
7405 # loop over IFDs
7406 try:
7407 tagno = unpack(tagnoformat, fh.read(tagnosize))[0]
7408 if tagno > 4096:
7409 raise ValueError("suspicious number of tags")
7410 except Exception:
7411 warnings.warn("corrupted tag list at offset %i" % offset)
7412 break
7413
7414 tags = {}
7415 data = fh.read(tagsize * tagno)
7416 pos = fh.tell()
7417 index = 0
7418 for _ in range(tagno):
7419 code, type_ = unpack(tagformat1, data[index : index + 4])
7420 count, value = unpack(tagformat2, data[index + 4 : index + tagsize])
7421 index += tagsize
7422 name = tagnames.get(code, str(code))
7423 try:
7424 dtype = TIFF.DATA_FORMATS[type_]
7425 except KeyError:
7426 raise TiffTag.Error("unknown tag data type %i" % type_)
7427
7428 fmt = "%s%i%s" % (byteorder, count * int(dtype[0]), dtype[1])
7429 size = struct.calcsize(fmt)
7430 if size > offsetsize or code in customtags:
7431 offset = unpack(offsetformat, value)[0]
7432 if offset < 8 or offset > fh.size - size:
7433 raise TiffTag.Error("invalid tag value offset %i" % offset)
7434 fh.seek(offset)
7435 if code in customtags:
7436 readfunc = customtags[code][1]
7437 value = readfunc(fh, byteorder, dtype, count, offsetsize)
7438 elif type_ == 7 or (count > 1 and dtype[-1] == "B"):
7439 value = read_bytes(fh, byteorder, dtype, count, offsetsize)
7440 elif code in tagnames or dtype[-1] == "s":
7441 value = unpack(fmt, fh.read(size))
7442 else:
7443 value = read_numpy(fh, byteorder, dtype, count, offsetsize)
7444 elif dtype[-1] == "B" or type_ == 7:
7445 value = value[:size]
7446 else:
7447 value = unpack(fmt, value[:size])
7448
7449 if code not in customtags and code not in TIFF.TAG_TUPLE:
7450 if len(value) == 1:
7451 value = value[0]
7452 if type_ != 7 and dtype[-1] == "s" and isinstance(value, bytes):
7453 # TIFF ASCII fields can contain multiple strings,
7454 # each terminated with a NUL
7455 try:
7456 value = bytes2str(stripascii(value).strip())
7457 except UnicodeDecodeError:
7458 warnings.warn("tag %i: coercing invalid ASCII to bytes" % code)
7459
7460 tags[name] = value
7461
7462 result.append(tags)
7463 # read offset to next page
7464 fh.seek(pos)
7465 offset = unpack(offsetformat, fh.read(offsetsize))[0]
7466 if offset == 0:
7467 break
7468 if offset >= fh.size:
7469 warnings.warn("invalid page offset %i" % offset)
7470 break
7471 fh.seek(offset)
7472
7473 if result and maxifds == 1:
7474 result = result[0]
7475 return result
7476
7477
7478def read_exif_ifd(fh, byteorder, dtype, count, offsetsize):
7479 """Read EXIF tags from file and return as dict."""
7480 exif = read_tags(fh, byteorder, offsetsize, TIFF.EXIF_TAGS, maxifds=1)
7481 for name in ("ExifVersion", "FlashpixVersion"):
7482 try:
7483 exif[name] = bytes2str(exif[name])
7484 except Exception:
7485 pass
7486 if "UserComment" in exif:
7487 idcode = exif["UserComment"][:8]
7488 try:
7489 if idcode == b"ASCII\x00\x00\x00":
7490 exif["UserComment"] = bytes2str(exif["UserComment"][8:])
7491 elif idcode == b"UNICODE\x00":
7492 exif["UserComment"] = exif["UserComment"][8:].decode("utf-16")
7493 except Exception:
7494 pass
7495 return exif
7496
7497
7498def read_gps_ifd(fh, byteorder, dtype, count, offsetsize):
7499 """Read GPS tags from file and return as dict."""
7500 return read_tags(fh, byteorder, offsetsize, TIFF.GPS_TAGS, maxifds=1)
7501
7502
7503def read_interoperability_ifd(fh, byteorder, dtype, count, offsetsize):
7504 """Read Interoperability tags from file and return as dict."""
7505 tag_names = {1: "InteroperabilityIndex"}
7506 return read_tags(fh, byteorder, offsetsize, tag_names, maxifds=1)
7507
7508
7509def read_bytes(fh, byteorder, dtype, count, offsetsize):
7510 """Read tag data from file and return as byte string."""
7511 dtype = "B" if dtype[-1] == "s" else byteorder + dtype[-1]
7512 count *= numpy.dtype(dtype).itemsize
7513 data = fh.read(count)
7514 if len(data) != count:
7515 warnings.warn("failed to read all bytes: %i, %i" % (len(data), count))
7516 return data
7517
7518
7519def read_utf8(fh, byteorder, dtype, count, offsetsize):
7520 """Read tag data from file and return as unicode string."""
7521 return fh.read(count).decode("utf-8")
7522
7523
7524def read_numpy(fh, byteorder, dtype, count, offsetsize):
7525 """Read tag data from file and return as numpy array."""
7526 dtype = "b" if dtype[-1] == "s" else byteorder + dtype[-1]
7527 return fh.read_array(dtype, count)
7528
7529
7530def read_colormap(fh, byteorder, dtype, count, offsetsize):
7531 """Read ColorMap data from file and return as numpy array."""
7532 cmap = fh.read_array(byteorder + dtype[-1], count)
7533 cmap.shape = (3, -1)
7534 return cmap
7535
7536
7537def read_json(fh, byteorder, dtype, count, offsetsize):
7538 """Read JSON tag data from file and return as object."""
7539 data = fh.read(count)
7540 try:
7541 return json.loads(unicode(stripnull(data), "utf-8"))
7542 except ValueError:
7543 warnings.warn("invalid JSON '%s'" % data)
7544
7545
7546def read_mm_header(fh, byteorder, dtype, count, offsetsize):
7547 """Read FluoView mm_header tag from file and return as dict."""
7548 mmh = fh.read_record(TIFF.MM_HEADER, byteorder=byteorder)
7549 mmh = recarray2dict(mmh)
7550 mmh["Dimensions"] = [
7551 (bytes2str(d[0]).strip(), d[1], d[2], d[3], bytes2str(d[4]).strip())
7552 for d in mmh["Dimensions"]
7553 ]
7554 d = mmh["GrayChannel"]
7555 mmh["GrayChannel"] = (
7556 bytes2str(d[0]).strip(),
7557 d[1],
7558 d[2],
7559 d[3],
7560 bytes2str(d[4]).strip(),
7561 )
7562 return mmh
7563
7564
7565def read_mm_stamp(fh, byteorder, dtype, count, offsetsize):
7566 """Read FluoView mm_stamp tag from file and return as numpy.ndarray."""
7567 return fh.read_array(byteorder + "f8", 8)
7568
7569
7570def read_uic1tag(fh, byteorder, dtype, count, offsetsize, planecount=None):
7571 """Read MetaMorph STK UIC1Tag from file and return as dict.
7572
7573 Return empty dictionary if planecount is unknown.
7574
7575 """
7576 assert dtype in ("2I", "1I") and byteorder == "<"
7577 result = {}
7578 if dtype == "2I":
7579 # pre MetaMorph 2.5 (not tested)
7580 values = fh.read_array("<u4", 2 * count).reshape(count, 2)
7581 result = {"ZDistance": values[:, 0] / values[:, 1]}
7582 elif planecount:
7583 for _ in range(count):
7584 tagid = struct.unpack("<I", fh.read(4))[0]
7585 if tagid in (28, 29, 37, 40, 41):
7586 # silently skip unexpected tags
7587 fh.read(4)
7588 continue
7589 name, value = read_uic_tag(fh, tagid, planecount, offset=True)
7590 result[name] = value
7591 return result
7592
7593
7594def read_uic2tag(fh, byteorder, dtype, planecount, offsetsize):
7595 """Read MetaMorph STK UIC2Tag from file and return as dict."""
7596 assert dtype == "2I" and byteorder == "<"
7597 values = fh.read_array("<u4", 6 * planecount).reshape(planecount, 6)
7598 return {
7599 "ZDistance": values[:, 0] / values[:, 1],
7600 "DateCreated": values[:, 2], # julian days
7601 "TimeCreated": values[:, 3], # milliseconds
7602 "DateModified": values[:, 4], # julian days
7603 "TimeModified": values[:, 5],
7604 } # milliseconds
7605
7606
7607def read_uic3tag(fh, byteorder, dtype, planecount, offsetsize):
7608 """Read MetaMorph STK UIC3Tag from file and return as dict."""
7609 assert dtype == "2I" and byteorder == "<"
7610 values = fh.read_array("<u4", 2 * planecount).reshape(planecount, 2)
7611 return {"Wavelengths": values[:, 0] / values[:, 1]}
7612
7613
7614def read_uic4tag(fh, byteorder, dtype, planecount, offsetsize):
7615 """Read MetaMorph STK UIC4Tag from file and return as dict."""
7616 assert dtype == "1I" and byteorder == "<"
7617 result = {}
7618 while True:
7619 tagid = struct.unpack("<H", fh.read(2))[0]
7620 if tagid == 0:
7621 break
7622 name, value = read_uic_tag(fh, tagid, planecount, offset=False)
7623 result[name] = value
7624 return result
7625
7626
7627def read_uic_tag(fh, tagid, planecount, offset):
7628 """Read a single UIC tag value from file and return tag name and value.
7629
7630 UIC1Tags use an offset.
7631
7632 """
7633
7634 def read_int(count=1):
7635 value = struct.unpack("<%iI" % count, fh.read(4 * count))
7636 return value[0] if count == 1 else value
7637
7638 try:
7639 name, dtype = TIFF.UIC_TAGS[tagid]
7640 except IndexError:
7641 # unknown tag
7642 return "_TagId%i" % tagid, read_int()
7643
7644 Fraction = TIFF.UIC_TAGS[4][1]
7645
7646 if offset:
7647 pos = fh.tell()
7648 if dtype not in (int, None):
7649 off = read_int()
7650 if off < 8:
7651 if dtype is str:
7652 return name, ""
7653 warnings.warn("invalid offset for uic tag '%s': %i" % (name, off))
7654 return name, off
7655 fh.seek(off)
7656
7657 if dtype is None:
7658 # skip
7659 name = "_" + name
7660 value = read_int()
7661 elif dtype is int:
7662 # int
7663 value = read_int()
7664 elif dtype is Fraction:
7665 # fraction
7666 value = read_int(2)
7667 value = value[0] / value[1]
7668 elif dtype is julian_datetime:
7669 # datetime
7670 value = julian_datetime(*read_int(2))
7671 elif dtype is read_uic_image_property:
7672 # ImagePropertyEx
7673 value = read_uic_image_property(fh)
7674 elif dtype is str:
7675 # pascal string
7676 size = read_int()
7677 if 0 <= size < 2**10:
7678 value = struct.unpack("%is" % size, fh.read(size))[0][:-1]
7679 value = bytes2str(stripnull(value))
7680 elif offset:
7681 value = ""
7682 warnings.warn("corrupt string in uic tag '%s'" % name)
7683 else:
7684 raise ValueError("invalid string size: %i" % size)
7685 elif dtype == "%ip":
7686 # sequence of pascal strings
7687 value = []
7688 for _ in range(planecount):
7689 size = read_int()
7690 if 0 <= size < 2**10:
7691 string = struct.unpack("%is" % size, fh.read(size))[0][:-1]
7692 string = bytes2str(stripnull(string))
7693 value.append(string)
7694 elif offset:
7695 warnings.warn("corrupt string in uic tag '%s'" % name)
7696 else:
7697 raise ValueError("invalid string size: %i" % size)
7698 else:
7699 # struct or numpy type
7700 dtype = "<" + dtype
7701 if "%i" in dtype:
7702 dtype = dtype % planecount
7703 if "(" in dtype:
7704 # numpy type
7705 value = fh.read_array(dtype, 1)[0]
7706 if value.shape[-1] == 2:
7707 # assume fractions
7708 value = value[..., 0] / value[..., 1]
7709 else:
7710 # struct format
7711 value = struct.unpack(dtype, fh.read(struct.calcsize(dtype)))
7712 if len(value) == 1:
7713 value = value[0]
7714
7715 if offset:
7716 fh.seek(pos + 4)
7717
7718 return name, value
7719
7720
7721def read_uic_image_property(fh):
7722 """Read UIC ImagePropertyEx tag from file and return as dict."""
7723 # TODO: test this
7724 size = struct.unpack("B", fh.read(1))[0]
7725 name = struct.unpack("%is" % size, fh.read(size))[0][:-1]
7726 flags, prop = struct.unpack("<IB", fh.read(5))
7727 if prop == 1:
7728 value = struct.unpack("II", fh.read(8))
7729 value = value[0] / value[1]
7730 else:
7731 size = struct.unpack("B", fh.read(1))[0]
7732 value = struct.unpack("%is" % size, fh.read(size))[0]
7733 return dict(name=name, flags=flags, value=value)
7734
7735
7736def read_cz_lsminfo(fh, byteorder, dtype, count, offsetsize):
7737 """Read CZ_LSMINFO tag from file and return as dict."""
7738 assert byteorder == "<"
7739 magic_number, structure_size = struct.unpack("<II", fh.read(8))
7740 if magic_number not in (50350412, 67127628):
7741 raise ValueError("invalid CZ_LSMINFO structure")
7742 fh.seek(-8, 1)
7743
7744 if structure_size < numpy.dtype(TIFF.CZ_LSMINFO).itemsize:
7745 # adjust structure according to structure_size
7746 lsminfo = []
7747 size = 0
7748 for name, dtype in TIFF.CZ_LSMINFO:
7749 size += numpy.dtype(dtype).itemsize
7750 if size > structure_size:
7751 break
7752 lsminfo.append((name, dtype))
7753 else:
7754 lsminfo = TIFF.CZ_LSMINFO
7755
7756 lsminfo = fh.read_record(lsminfo, byteorder=byteorder)
7757 lsminfo = recarray2dict(lsminfo)
7758
7759 # read LSM info subrecords at offsets
7760 for name, reader in TIFF.CZ_LSMINFO_READERS.items():
7761 if reader is None:
7762 continue
7763 offset = lsminfo.get("Offset" + name, 0)
7764 if offset < 8:
7765 continue
7766 fh.seek(offset)
7767 try:
7768 lsminfo[name] = reader(fh)
7769 except ValueError:
7770 pass
7771 return lsminfo
7772
7773
7774def read_lsm_floatpairs(fh):
7775 """Read LSM sequence of float pairs from file and return as list."""
7776 size = struct.unpack("<i", fh.read(4))[0]
7777 return fh.read_array("<2f8", count=size)
7778
7779
7780def read_lsm_positions(fh):
7781 """Read LSM positions from file and return as list."""
7782 size = struct.unpack("<I", fh.read(4))[0]
7783 return fh.read_array("<2f8", count=size)
7784
7785
7786def read_lsm_timestamps(fh):
7787 """Read LSM time stamps from file and return as list."""
7788 size, count = struct.unpack("<ii", fh.read(8))
7789 if size != (8 + 8 * count):
7790 warnings.warn("invalid LSM TimeStamps block")
7791 return []
7792 # return struct.unpack('<%dd' % count, fh.read(8*count))
7793 return fh.read_array("<f8", count=count)
7794
7795
7796def read_lsm_eventlist(fh):
7797 """Read LSM events from file and return as list of (time, type, text)."""
7798 count = struct.unpack("<II", fh.read(8))[1]
7799 events = []
7800 while count > 0:
7801 esize, etime, etype = struct.unpack("<IdI", fh.read(16))
7802 etext = bytes2str(stripnull(fh.read(esize - 16)))
7803 events.append((etime, etype, etext))
7804 count -= 1
7805 return events
7806
7807
7808def read_lsm_channelcolors(fh):
7809 """Read LSM ChannelColors structure from file and return as dict."""
7810 result = {"Mono": False, "Colors": [], "ColorNames": []}
7811 pos = fh.tell()
7812 (size, ncolors, nnames, coffset, noffset, mono) = struct.unpack(
7813 "<IIIIII", fh.read(24)
7814 )
7815 if ncolors != nnames:
7816 warnings.warn("invalid LSM ChannelColors structure")
7817 return result
7818 result["Mono"] = bool(mono)
7819 # Colors
7820 fh.seek(pos + coffset)
7821 colors = fh.read_array("uint8", count=ncolors * 4).reshape((ncolors, 4))
7822 result["Colors"] = colors.tolist()
7823 # ColorNames
7824 fh.seek(pos + noffset)
7825 buffer = fh.read(size - noffset)
7826 names = []
7827 while len(buffer) > 4:
7828 size = struct.unpack("<I", buffer[:4])[0]
7829 names.append(bytes2str(buffer[4 : 3 + size]))
7830 buffer = buffer[4 + size :]
7831 result["ColorNames"] = names
7832 return result
7833
7834
7835def read_lsm_scaninfo(fh):
7836 """Read LSM ScanInfo structure from file and return as dict."""
7837 block = {}
7838 blocks = [block]
7839 unpack = struct.unpack
7840 if struct.unpack("<I", fh.read(4))[0] != 0x10000000:
7841 # not a Recording sub block
7842 warnings.warn("invalid LSM ScanInfo structure")
7843 return block
7844 fh.read(8)
7845 while True:
7846 entry, dtype, size = unpack("<III", fh.read(12))
7847 if dtype == 2:
7848 # ascii
7849 value = bytes2str(stripnull(fh.read(size)))
7850 elif dtype == 4:
7851 # long
7852 value = unpack("<i", fh.read(4))[0]
7853 elif dtype == 5:
7854 # rational
7855 value = unpack("<d", fh.read(8))[0]
7856 else:
7857 value = 0
7858 if entry in TIFF.CZ_LSMINFO_SCANINFO_ARRAYS:
7859 blocks.append(block)
7860 name = TIFF.CZ_LSMINFO_SCANINFO_ARRAYS[entry]
7861 newobj = []
7862 block[name] = newobj
7863 block = newobj
7864 elif entry in TIFF.CZ_LSMINFO_SCANINFO_STRUCTS:
7865 blocks.append(block)
7866 newobj = {}
7867 block.append(newobj)
7868 block = newobj
7869 elif entry in TIFF.CZ_LSMINFO_SCANINFO_ATTRIBUTES:
7870 name = TIFF.CZ_LSMINFO_SCANINFO_ATTRIBUTES[entry]
7871 block[name] = value
7872 elif entry == 0xFFFFFFFF:
7873 # end sub block
7874 block = blocks.pop()
7875 else:
7876 # unknown entry
7877 block["Entry0x%x" % entry] = value
7878 if not blocks:
7879 break
7880 return block
7881
7882
7883def read_tvips_header(fh, byteorder, dtype, count, offsetsize):
7884 """Read TVIPS EM-MENU headers and return as dict."""
7885 result = {}
7886 header = fh.read_record(TIFF.TVIPS_HEADER_V1, byteorder=byteorder)
7887 for name, typestr in TIFF.TVIPS_HEADER_V1:
7888 result[name] = header[name].tolist()
7889 if header["Version"] == 2:
7890 header = fh.read_record(TIFF.TVIPS_HEADER_V2, byteorder=byteorder)
7891 if header["Magic"] != int(0xAAAAAAAA):
7892 warnings.warn("invalid TVIPS v2 magic number")
7893 return {}
7894 # decode utf16 strings
7895 for name, typestr in TIFF.TVIPS_HEADER_V2:
7896 if typestr.startswith("V"):
7897 s = header[name].tostring().decode("utf16", errors="ignore")
7898 result[name] = stripnull(s, null="\0")
7899 else:
7900 result[name] = header[name].tolist()
7901 # convert nm to m
7902 for axis in "XY":
7903 header["PhysicalPixelSize" + axis] /= 1e9
7904 header["PixelSize" + axis] /= 1e9
7905 elif header.version != 1:
7906 warnings.warn("unknown TVIPS header version")
7907 return {}
7908 return result
7909
7910
7911def read_fei_metadata(fh, byteorder, dtype, count, offsetsize):
7912 """Read FEI SFEG/HELIOS headers and return as dict."""
7913 result = {}
7914 section = {}
7915 data = bytes2str(fh.read(count))
7916 for line in data.splitlines():
7917 line = line.strip()
7918 if line.startswith("["):
7919 section = {}
7920 result[line[1:-1]] = section
7921 continue
7922 try:
7923 key, value = line.split("=")
7924 except ValueError:
7925 continue
7926 section[key] = astype(value)
7927 return result
7928
7929
7930def read_cz_sem(fh, byteorder, dtype, count, offsetsize):
7931 """Read Zeiss SEM tag and return as dict."""
7932 result = {"": ()}
7933 key = None
7934 data = bytes2str(fh.read(count))
7935 for line in data.splitlines():
7936 if line.isupper():
7937 key = line.lower()
7938 elif key:
7939 try:
7940 name, value = line.split("=")
7941 except ValueError:
7942 continue
7943 value = value.strip()
7944 unit = ""
7945 try:
7946 v, u = value.split()
7947 number = astype(v, (int, float))
7948 if number != v:
7949 value = number
7950 unit = u
7951 except Exception:
7952 number = astype(value, (int, float))
7953 if number != value:
7954 value = number
7955 if value in ("No", "Off"):
7956 value = False
7957 elif value in ("Yes", "On"):
7958 value = True
7959 result[key] = (name.strip(), value)
7960 if unit:
7961 result[key] += (unit,)
7962 key = None
7963 else:
7964 result[""] += (astype(line, (int, float)),)
7965 return result
7966
7967
7968def read_nih_image_header(fh, byteorder, dtype, count, offsetsize):
7969 """Read NIH_IMAGE_HEADER tag from file and return as dict."""
7970 a = fh.read_record(TIFF.NIH_IMAGE_HEADER, byteorder=byteorder)
7971 a = a.newbyteorder(byteorder)
7972 a = recarray2dict(a)
7973 a["XUnit"] = a["XUnit"][: a["XUnitSize"]]
7974 a["UM"] = a["UM"][: a["UMsize"]]
7975 return a
7976
7977
7978def read_scanimage_metadata(fh):
7979 """Read ScanImage BigTIFF v3 static and ROI metadata from open file.
7980
7981 Return non-varying frame data as dict and ROI group data as JSON.
7982
7983 The settings can be used to read image data and metadata without parsing
7984 the TIFF file.
7985
7986 Raise ValueError if file does not contain valid ScanImage v3 metadata.
7987
7988 """
7989 fh.seek(0)
7990 try:
7991 byteorder, version = struct.unpack("<2sH", fh.read(4))
7992 if byteorder != b"II" or version != 43:
7993 raise Exception
7994 fh.seek(16)
7995 magic, version, size0, size1 = struct.unpack("<IIII", fh.read(16))
7996 if magic != 117637889 or version != 3:
7997 raise Exception
7998 except Exception:
7999 raise ValueError("not a ScanImage BigTIFF v3 file")
8000
8001 frame_data = matlabstr2py(bytes2str(fh.read(size0)[:-1]))
8002 roi_data = read_json(fh, "<", None, size1, None) if size1 > 1 else {}
8003 return frame_data, roi_data
8004
8005
8006def read_micromanager_metadata(fh):
8007 """Read MicroManager non-TIFF settings from open file and return as dict.
8008
8009 The settings can be used to read image data without parsing the TIFF file.
8010
8011 Raise ValueError if the file does not contain valid MicroManager metadata.
8012
8013 """
8014 fh.seek(0)
8015 try:
8016 byteorder = {b"II": "<", b"MM": ">"}[fh.read(2)]
8017 except IndexError:
8018 raise ValueError("not a MicroManager TIFF file")
8019
8020 result = {}
8021 fh.seek(8)
8022 (
8023 index_header,
8024 index_offset,
8025 display_header,
8026 display_offset,
8027 comments_header,
8028 comments_offset,
8029 summary_header,
8030 summary_length,
8031 ) = struct.unpack(byteorder + "IIIIIIII", fh.read(32))
8032
8033 if summary_header != 2355492:
8034 raise ValueError("invalid MicroManager summary header")
8035 result["Summary"] = read_json(fh, byteorder, None, summary_length, None)
8036
8037 if index_header != 54773648:
8038 raise ValueError("invalid MicroManager index header")
8039 fh.seek(index_offset)
8040 header, count = struct.unpack(byteorder + "II", fh.read(8))
8041 if header != 3453623:
8042 raise ValueError("invalid MicroManager index header")
8043 data = struct.unpack(byteorder + "IIIII" * count, fh.read(20 * count))
8044 result["IndexMap"] = {
8045 "Channel": data[::5],
8046 "Slice": data[1::5],
8047 "Frame": data[2::5],
8048 "Position": data[3::5],
8049 "Offset": data[4::5],
8050 }
8051
8052 if display_header != 483765892:
8053 raise ValueError("invalid MicroManager display header")
8054 fh.seek(display_offset)
8055 header, count = struct.unpack(byteorder + "II", fh.read(8))
8056 if header != 347834724:
8057 raise ValueError("invalid MicroManager display header")
8058 result["DisplaySettings"] = read_json(fh, byteorder, None, count, None)
8059
8060 if comments_header != 99384722:
8061 raise ValueError("invalid MicroManager comments header")
8062 fh.seek(comments_offset)
8063 header, count = struct.unpack(byteorder + "II", fh.read(8))
8064 if header != 84720485:
8065 raise ValueError("invalid MicroManager comments header")
8066 result["Comments"] = read_json(fh, byteorder, None, count, None)
8067
8068 return result
8069
8070
8071def read_metaseries_catalog(fh):
8072 """Read MetaSeries non-TIFF hint catalog from file.
8073
8074 Raise ValueError if the file does not contain a valid hint catalog.
8075
8076 """
8077 # TODO: implement read_metaseries_catalog
8078 raise NotImplementedError()
8079
8080
8081def imagej_metadata_tags(metadata, byteorder):
8082 """Return IJMetadata and IJMetadataByteCounts tags from metadata dict.
8083
8084 The tags can be passed to the TiffWriter.save function as extratags.
8085
8086 The metadata dict may contain the following keys and values:
8087
8088 Info : str
8089 Human-readable information as string.
8090 Labels : sequence of str
8091 Human-readable labels for each channel.
8092 Ranges : sequence of doubles
8093 Lower and upper values for each channel.
8094 LUTs : sequence of (3, 256) uint8 ndarrays
8095 Color palettes for each channel.
8096 Plot : bytes
8097 Undocumented ImageJ internal format.
8098 ROI: bytes
8099 Undocumented ImageJ internal region of interest format.
8100 Overlays : bytes
8101 Undocumented ImageJ internal format.
8102
8103 """
8104 header = [{">": b"IJIJ", "<": b"JIJI"}[byteorder]]
8105 bytecounts = [0]
8106 body = []
8107
8108 def _string(data, byteorder):
8109 return data.encode("utf-16" + {">": "be", "<": "le"}[byteorder])
8110
8111 def _doubles(data, byteorder):
8112 return struct.pack(byteorder + ("d" * len(data)), *data)
8113
8114 def _ndarray(data, byteorder):
8115 return data.tobytes()
8116
8117 def _bytes(data, byteorder):
8118 return data
8119
8120 metadata_types = (
8121 ("Info", b"info", 1, _string),
8122 ("Labels", b"labl", None, _string),
8123 ("Ranges", b"rang", 1, _doubles),
8124 ("LUTs", b"luts", None, _ndarray),
8125 ("Plot", b"plot", 1, _bytes),
8126 ("ROI", b"roi ", 1, _bytes),
8127 ("Overlays", b"over", None, _bytes),
8128 )
8129
8130 for key, mtype, count, func in metadata_types:
8131 if key.lower() in metadata:
8132 key = key.lower()
8133 elif key not in metadata:
8134 continue
8135 if byteorder == "<":
8136 mtype = mtype[::-1]
8137 values = metadata[key]
8138 if count is None:
8139 count = len(values)
8140 else:
8141 values = [values]
8142 header.append(mtype + struct.pack(byteorder + "I", count))
8143 for value in values:
8144 data = func(value, byteorder)
8145 body.append(data)
8146 bytecounts.append(len(data))
8147
8148 if not body:
8149 return ()
8150 body = b"".join(body)
8151 header = b"".join(header)
8152 data = header + body
8153 bytecounts[0] = len(header)
8154 bytecounts = struct.pack(byteorder + ("I" * len(bytecounts)), *bytecounts)
8155 return (
8156 (50839, "B", len(data), data, True),
8157 (50838, "I", len(bytecounts) // 4, bytecounts, True),
8158 )
8159
8160
8161def imagej_metadata(data, bytecounts, byteorder):
8162 """Return IJMetadata tag value as dict.
8163
8164 The 'Info' string can have multiple formats, e.g. OIF or ScanImage,
8165 that might be parsed into dicts using the matlabstr2py or
8166 oiffile.SettingsFile functions.
8167
8168 """
8169
8170 def _string(data, byteorder):
8171 return data.decode("utf-16" + {">": "be", "<": "le"}[byteorder])
8172
8173 def _doubles(data, byteorder):
8174 return struct.unpack(byteorder + ("d" * (len(data) // 8)), data)
8175
8176 def _lut(data, byteorder):
8177 return numpy.frombuffer(data, "uint8").reshape(-1, 256)
8178
8179 def _bytes(data, byteorder):
8180 return data
8181
8182 metadata_types = { # big-endian
8183 b"info": ("Info", _string),
8184 b"labl": ("Labels", _string),
8185 b"rang": ("Ranges", _doubles),
8186 b"luts": ("LUTs", _lut),
8187 b"plot": ("Plots", _bytes),
8188 b"roi ": ("ROI", _bytes),
8189 b"over": ("Overlays", _bytes),
8190 }
8191 metadata_types.update( # little-endian
8192 dict((k[::-1], v) for k, v in metadata_types.items())
8193 )
8194
8195 if not bytecounts:
8196 raise ValueError("no ImageJ metadata")
8197
8198 if data[:4] not in (b"IJIJ", b"JIJI"):
8199 raise ValueError("invalid ImageJ metadata")
8200
8201 header_size = bytecounts[0]
8202 if header_size < 12 or header_size > 804:
8203 raise ValueError("invalid ImageJ metadata header size")
8204
8205 ntypes = (header_size - 4) // 8
8206 header = struct.unpack(byteorder + "4sI" * ntypes, data[4 : 4 + ntypes * 8])
8207 pos = 4 + ntypes * 8
8208 counter = 0
8209 result = {}
8210 for mtype, count in zip(header[::2], header[1::2]):
8211 values = []
8212 name, func = metadata_types.get(mtype, (bytes2str(mtype), read_bytes))
8213 for _ in range(count):
8214 counter += 1
8215 pos1 = pos + bytecounts[counter]
8216 values.append(func(data[pos:pos1], byteorder))
8217 pos = pos1
8218 result[name.strip()] = values[0] if count == 1 else values
8219 return result
8220
8221
8222def imagej_description_metadata(description):
8223 """Return metatata from ImageJ image description as dict.
8224
8225 Raise ValueError if not a valid ImageJ description.
8226
8227 >>> description = 'ImageJ=1.11a\\nimages=510\\nhyperstack=true\\n'
8228 >>> imagej_description_metadata(description) # doctest: +SKIP
8229 {'ImageJ': '1.11a', 'images': 510, 'hyperstack': True}
8230
8231 """
8232
8233 def _bool(val):
8234 return {"true": True, "false": False}[val.lower()]
8235
8236 result = {}
8237 for line in description.splitlines():
8238 try:
8239 key, val = line.split("=")
8240 except Exception:
8241 continue
8242 key = key.strip()
8243 val = val.strip()
8244 for dtype in (int, float, _bool):
8245 try:
8246 val = dtype(val)
8247 break
8248 except Exception:
8249 pass
8250 result[key] = val
8251
8252 if "ImageJ" not in result:
8253 raise ValueError("not a ImageJ image description")
8254 return result
8255
8256
8257def imagej_description(
8258 shape,
8259 rgb=None,
8260 colormaped=False,
8261 version="1.11a",
8262 hyperstack=None,
8263 mode=None,
8264 loop=None,
8265 **kwargs
8266):
8267 """Return ImageJ image description from data shape.
8268
8269 ImageJ can handle up to 6 dimensions in order TZCYXS.
8270
8271 >>> imagej_description((51, 5, 2, 196, 171)) # doctest: +SKIP
8272 ImageJ=1.11a
8273 images=510
8274 channels=2
8275 slices=5
8276 frames=51
8277 hyperstack=true
8278 mode=grayscale
8279 loop=false
8280
8281 """
8282 if colormaped:
8283 raise NotImplementedError("ImageJ colormapping not supported")
8284 shape = imagej_shape(shape, rgb=rgb)
8285 rgb = shape[-1] in (3, 4)
8286
8287 result = ["ImageJ=%s" % version]
8288 append = []
8289 result.append("images=%i" % product(shape[:-3]))
8290 if hyperstack is None:
8291 hyperstack = True
8292 append.append("hyperstack=true")
8293 else:
8294 append.append("hyperstack=%s" % bool(hyperstack))
8295 if shape[2] > 1:
8296 result.append("channels=%i" % shape[2])
8297 if mode is None and not rgb:
8298 mode = "grayscale"
8299 if hyperstack and mode:
8300 append.append("mode=%s" % mode)
8301 if shape[1] > 1:
8302 result.append("slices=%i" % shape[1])
8303 if shape[0] > 1:
8304 result.append("frames=%i" % shape[0])
8305 if loop is None:
8306 append.append("loop=false")
8307 if loop is not None:
8308 append.append("loop=%s" % bool(loop))
8309 for key, value in kwargs.items():
8310 append.append("%s=%s" % (key.lower(), value))
8311
8312 return "\n".join(result + append + [""])
8313
8314
8315def imagej_shape(shape, rgb=None):
8316 """Return shape normalized to 6D ImageJ hyperstack TZCYXS.
8317
8318 Raise ValueError if not a valid ImageJ hyperstack shape.
8319
8320 >>> imagej_shape((2, 3, 4, 5, 3), False)
8321 (2, 3, 4, 5, 3, 1)
8322
8323 """
8324 shape = tuple(int(i) for i in shape)
8325 ndim = len(shape)
8326 if 1 > ndim > 6:
8327 raise ValueError("invalid ImageJ hyperstack: not 2 to 6 dimensional")
8328 if rgb is None:
8329 rgb = shape[-1] in (3, 4) and ndim > 2
8330 if rgb and shape[-1] not in (3, 4):
8331 raise ValueError("invalid ImageJ hyperstack: not a RGB image")
8332 if not rgb and ndim == 6 and shape[-1] != 1:
8333 raise ValueError("invalid ImageJ hyperstack: not a non-RGB image")
8334 if rgb or shape[-1] == 1:
8335 return (1,) * (6 - ndim) + shape
8336 return (1,) * (5 - ndim) + shape + (1,)
8337
8338
8339def json_description(shape, **metadata):
8340 """Return JSON image description from data shape and other meta data.
8341
8342 Return UTF-8 encoded JSON.
8343
8344 >>> json_description((256, 256, 3), axes='YXS') # doctest: +SKIP
8345 b'{"shape": [256, 256, 3], "axes": "YXS"}'
8346
8347 """
8348 metadata.update(shape=shape)
8349 return json.dumps(metadata) # .encode('utf-8')
8350
8351
8352def json_description_metadata(description):
8353 """Return metatata from JSON formatted image description as dict.
8354
8355 Raise ValuError if description is of unknown format.
8356
8357 >>> description = '{"shape": [256, 256, 3], "axes": "YXS"}'
8358 >>> json_description_metadata(description) # doctest: +SKIP
8359 {'shape': [256, 256, 3], 'axes': 'YXS'}
8360 >>> json_description_metadata('shape=(256, 256, 3)')
8361 {'shape': (256, 256, 3)}
8362
8363 """
8364 if description[:6] == "shape=":
8365 # old style 'shaped' description; not JSON
8366 shape = tuple(int(i) for i in description[7:-1].split(","))
8367 return dict(shape=shape)
8368 if description[:1] == "{" and description[-1:] == "}":
8369 # JSON description
8370 return json.loads(description)
8371 raise ValueError("invalid JSON image description", description)
8372
8373
8374def fluoview_description_metadata(description, ignoresections=None):
8375 """Return metatata from FluoView image description as dict.
8376
8377 The FluoView image description format is unspecified. Expect failures.
8378
8379 >>> descr = ('[Intensity Mapping]\\nMap Ch0: Range=00000 to 02047\\n'
8380 ... '[Intensity Mapping End]')
8381 >>> fluoview_description_metadata(descr)
8382 {'Intensity Mapping': {'Map Ch0: Range': '00000 to 02047'}}
8383
8384 """
8385 if not description.startswith("["):
8386 raise ValueError("invalid FluoView image description")
8387 if ignoresections is None:
8388 ignoresections = {"Region Info (Fields)", "Protocol Description"}
8389
8390 result = {}
8391 sections = [result]
8392 comment = False
8393 for line in description.splitlines():
8394 if not comment:
8395 line = line.strip()
8396 if not line:
8397 continue
8398 if line[0] == "[":
8399 if line[-5:] == " End]":
8400 # close section
8401 del sections[-1]
8402 section = sections[-1]
8403 name = line[1:-5]
8404 if comment:
8405 section[name] = "\n".join(section[name])
8406 if name[:4] == "LUT ":
8407 a = numpy.array(section[name], dtype="uint8")
8408 a.shape = -1, 3
8409 section[name] = a
8410 continue
8411 # new section
8412 comment = False
8413 name = line[1:-1]
8414 if name[:4] == "LUT ":
8415 section = []
8416 elif name in ignoresections:
8417 section = []
8418 comment = True
8419 else:
8420 section = {}
8421 sections.append(section)
8422 result[name] = section
8423 continue
8424 # add entry
8425 if comment:
8426 section.append(line)
8427 continue
8428 line = line.split("=", 1)
8429 if len(line) == 1:
8430 section[line[0].strip()] = None
8431 continue
8432 key, value = line
8433 if key[:4] == "RGB ":
8434 section.extend(int(rgb) for rgb in value.split())
8435 else:
8436 section[key.strip()] = astype(value.strip())
8437 return result
8438
8439
8440def pilatus_description_metadata(description):
8441 """Return metatata from Pilatus image description as dict.
8442
8443 Return metadata from Pilatus pixel array detectors by Dectris, created
8444 by camserver or TVX software.
8445
8446 >>> pilatus_description_metadata('# Pixel_size 172e-6 m x 172e-6 m')
8447 {'Pixel_size': (0.000172, 0.000172)}
8448
8449 """
8450 result = {}
8451 if not description.startswith("# "):
8452 return result
8453 for c in "#:=,()":
8454 description = description.replace(c, " ")
8455 for line in description.split("\n"):
8456 if line[:2] != " ":
8457 continue
8458 line = line.split()
8459 name = line[0]
8460 if line[0] not in TIFF.PILATUS_HEADER:
8461 try:
8462 result["DateTime"] = datetime.datetime.strptime(
8463 " ".join(line), "%Y-%m-%dT%H %M %S.%f"
8464 )
8465 except Exception:
8466 result[name] = " ".join(line[1:])
8467 continue
8468 indices, dtype = TIFF.PILATUS_HEADER[line[0]]
8469 if isinstance(indices[0], slice):
8470 # assumes one slice
8471 values = line[indices[0]]
8472 else:
8473 values = [line[i] for i in indices]
8474 if dtype is float and values[0] == "not":
8475 values = ["NaN"]
8476 values = tuple(dtype(v) for v in values)
8477 if dtype == str:
8478 values = " ".join(values)
8479 elif len(values) == 1:
8480 values = values[0]
8481 result[name] = values
8482 return result
8483
8484
8485def svs_description_metadata(description):
8486 """Return metatata from Aperio image description as dict.
8487
8488 The Aperio image description format is unspecified. Expect failures.
8489
8490 >>> svs_description_metadata('Aperio Image Library v1.0')
8491 {'Aperio Image Library': 'v1.0'}
8492
8493 """
8494 if not description.startswith("Aperio Image Library "):
8495 raise ValueError("invalid Aperio image description")
8496 result = {}
8497 lines = description.split("\n")
8498 key, value = lines[0].strip().rsplit(None, 1) # 'Aperio Image Library'
8499 result[key.strip()] = value.strip()
8500 if len(lines) == 1:
8501 return result
8502 items = lines[1].split("|")
8503 result[""] = items[0].strip() # TODO: parse this?
8504 for item in items[1:]:
8505 key, value = item.split(" = ")
8506 result[key.strip()] = astype(value.strip())
8507 return result
8508
8509
8510def stk_description_metadata(description):
8511 """Return metadata from MetaMorph image description as list of dict.
8512
8513 The MetaMorph image description format is unspecified. Expect failures.
8514
8515 """
8516 description = description.strip()
8517 if not description:
8518 return []
8519 try:
8520 description = bytes2str(description)
8521 except UnicodeDecodeError:
8522 warnings.warn("failed to parse MetaMorph image description")
8523 return []
8524 result = []
8525 for plane in description.split("\x00"):
8526 d = {}
8527 for line in plane.split("\r\n"):
8528 line = line.split(":", 1)
8529 if len(line) > 1:
8530 name, value = line
8531 d[name.strip()] = astype(value.strip())
8532 else:
8533 value = line[0].strip()
8534 if value:
8535 if "" in d:
8536 d[""].append(value)
8537 else:
8538 d[""] = [value]
8539 result.append(d)
8540 return result
8541
8542
8543def metaseries_description_metadata(description):
8544 """Return metatata from MetaSeries image description as dict."""
8545 if not description.startswith("<MetaData>"):
8546 raise ValueError("invalid MetaSeries image description")
8547
8548 from xml.etree import cElementTree as etree # delayed import
8549
8550 root = etree.fromstring(description)
8551 types = {"float": float, "int": int, "bool": lambda x: asbool(x, "on", "off")}
8552
8553 def parse(root, result):
8554 # recursive
8555 for child in root:
8556 attrib = child.attrib
8557 if not attrib:
8558 result[child.tag] = parse(child, {})
8559 continue
8560 if "id" in attrib:
8561 i = attrib["id"]
8562 t = attrib["type"]
8563 v = attrib["value"]
8564 if t in types:
8565 result[i] = types[t](v)
8566 else:
8567 result[i] = v
8568 return result
8569
8570 adict = parse(root, {})
8571 if "Description" in adict:
8572 adict["Description"] = adict["Description"].replace(" ", "\n")
8573 return adict
8574
8575
8576def scanimage_description_metadata(description):
8577 """Return metatata from ScanImage image description as dict."""
8578 return matlabstr2py(description)
8579
8580
8581def scanimage_artist_metadata(artist):
8582 """Return metatata from ScanImage artist tag as dict."""
8583 try:
8584 return json.loads(artist)
8585 except ValueError:
8586 warnings.warn("invalid JSON '%s'" % artist)
8587
8588
8589def _replace_by(module_function, package=__package__, warn=None, prefix="_"):
8590 """Try replace decorated function by module.function."""
8591 return lambda f: f # imageio: just use what's in here
8592
8593 def _warn(e, warn):
8594 if warn is None:
8595 warn = "\n Functionality might be degraded or be slow.\n"
8596 elif warn is True:
8597 warn = ""
8598 elif not warn:
8599 return
8600 warnings.warn("%s%s" % (e, warn))
8601
8602 try:
8603 from importlib import import_module
8604 except ImportError as e:
8605 _warn(e, warn)
8606 return identityfunc
8607
8608 def decorate(func, module_function=module_function, warn=warn):
8609 module, function = module_function.split(".")
8610 try:
8611 if package:
8612 module = import_module("." + module, package=package)
8613 else:
8614 module = import_module(module)
8615 except Exception as e:
8616 _warn(e, warn)
8617 return func
8618 try:
8619 func, oldfunc = getattr(module, function), func
8620 except Exception as e:
8621 _warn(e, warn)
8622 return func
8623 globals()[prefix + func.__name__] = oldfunc
8624 return func
8625
8626 return decorate
8627
8628
8629def decode_floats(data):
8630 """Decode floating point horizontal differencing.
8631
8632 The TIFF predictor type 3 reorders the bytes of the image values and
8633 applies horizontal byte differencing to improve compression of floating
8634 point images. The ordering of interleaved color channels is preserved.
8635
8636 Parameters
8637 ----------
8638 data : numpy.ndarray
8639 The image to be decoded. The dtype must be a floating point.
8640 The shape must include the number of contiguous samples per pixel
8641 even if 1.
8642
8643 """
8644 shape = data.shape
8645 dtype = data.dtype
8646 if len(shape) < 3:
8647 raise ValueError("invalid data shape")
8648 if dtype.char not in "dfe":
8649 raise ValueError("not a floating point image")
8650 littleendian = data.dtype.byteorder == "<" or (
8651 sys.byteorder == "little" and data.dtype.byteorder == "="
8652 )
8653 # undo horizontal byte differencing
8654 data = data.view("uint8")
8655 data.shape = shape[:-2] + (-1,) + shape[-1:]
8656 numpy.cumsum(data, axis=-2, dtype="uint8", out=data)
8657 # reorder bytes
8658 if littleendian:
8659 data.shape = shape[:-2] + (-1,) + shape[-2:]
8660 data = numpy.swapaxes(data, -3, -2)
8661 data = numpy.swapaxes(data, -2, -1)
8662 data = data[..., ::-1]
8663 # back to float
8664 data = numpy.ascontiguousarray(data)
8665 data = data.view(dtype)
8666 data.shape = shape
8667 return data
8668
8669
8670@_replace_by("_tifffile.decode_packbits")
8671def decode_packbits(encoded):
8672 """Decompress PackBits encoded byte string.
8673
8674 PackBits is a simple byte-oriented run-length compression scheme.
8675
8676 """
8677 func = ord if sys.version[0] == "2" else identityfunc
8678 result = []
8679 result_extend = result.extend
8680 i = 0
8681 try:
8682 while True:
8683 n = func(encoded[i]) + 1
8684 i += 1
8685 if n < 129:
8686 result_extend(encoded[i : i + n])
8687 i += n
8688 elif n > 129:
8689 result_extend(encoded[i : i + 1] * (258 - n))
8690 i += 1
8691 except IndexError:
8692 pass
8693 return b"".join(result) if sys.version[0] == "2" else bytes(result)
8694
8695
8696@_replace_by("_tifffile.decode_lzw")
8697def decode_lzw(encoded):
8698 """Decompress LZW (Lempel-Ziv-Welch) encoded TIFF strip (byte string).
8699
8700 The strip must begin with a CLEAR code and end with an EOI code.
8701
8702 This implementation of the LZW decoding algorithm is described in (1) and
8703 is not compatible with old style LZW compressed files like quad-lzw.tif.
8704
8705 """
8706 len_encoded = len(encoded)
8707 bitcount_max = len_encoded * 8
8708 unpack = struct.unpack
8709
8710 if sys.version[0] == "2":
8711 newtable = [chr(i) for i in range(256)]
8712 else:
8713 newtable = [bytes([i]) for i in range(256)]
8714 newtable.extend((0, 0))
8715
8716 def next_code():
8717 """Return integer of 'bitw' bits at 'bitcount' position in encoded."""
8718 start = bitcount // 8
8719 s = encoded[start : start + 4]
8720 try:
8721 code = unpack(">I", s)[0]
8722 except Exception:
8723 code = unpack(">I", s + b"\x00" * (4 - len(s)))[0]
8724 code <<= bitcount % 8
8725 code &= mask
8726 return code >> shr
8727
8728 switchbitch = { # code: bit-width, shr-bits, bit-mask
8729 255: (9, 23, int(9 * "1" + "0" * 23, 2)),
8730 511: (10, 22, int(10 * "1" + "0" * 22, 2)),
8731 1023: (11, 21, int(11 * "1" + "0" * 21, 2)),
8732 2047: (12, 20, int(12 * "1" + "0" * 20, 2)),
8733 }
8734 bitw, shr, mask = switchbitch[255]
8735 bitcount = 0
8736
8737 if len_encoded < 4:
8738 raise ValueError("strip must be at least 4 characters long")
8739
8740 if next_code() != 256:
8741 raise ValueError("strip must begin with CLEAR code")
8742
8743 code = 0
8744 oldcode = 0
8745 result = []
8746 result_append = result.append
8747 while True:
8748 code = next_code() # ~5% faster when inlining this function
8749 bitcount += bitw
8750 if code == 257 or bitcount >= bitcount_max: # EOI
8751 break
8752 if code == 256: # CLEAR
8753 table = newtable[:]
8754 table_append = table.append
8755 lentable = 258
8756 bitw, shr, mask = switchbitch[255]
8757 code = next_code()
8758 bitcount += bitw
8759 if code == 257: # EOI
8760 break
8761 result_append(table[code])
8762 else:
8763 if code < lentable:
8764 decoded = table[code]
8765 newcode = table[oldcode] + decoded[:1]
8766 else:
8767 newcode = table[oldcode]
8768 newcode += newcode[:1]
8769 decoded = newcode
8770 result_append(decoded)
8771 table_append(newcode)
8772 lentable += 1
8773 oldcode = code
8774 if lentable in switchbitch:
8775 bitw, shr, mask = switchbitch[lentable]
8776
8777 if code != 257:
8778 warnings.warn("unexpected end of LZW stream (code %i)" % code)
8779
8780 return b"".join(result)
8781
8782
8783@_replace_by("_tifffile.unpack_ints")
8784def unpack_ints(data, dtype, itemsize, runlen=0):
8785 """Decompress byte string to array of integers of any bit size <= 32.
8786
8787 This Python implementation is slow and only handles itemsizes 1, 2, 4, 8,
8788 16, 32, and 64.
8789
8790 Parameters
8791 ----------
8792 data : byte str
8793 Data to decompress.
8794 dtype : numpy.dtype or str
8795 A numpy boolean or integer type.
8796 itemsize : int
8797 Number of bits per integer.
8798 runlen : int
8799 Number of consecutive integers, after which to start at next byte.
8800
8801 Examples
8802 --------
8803 >>> unpack_ints(b'a', 'B', 1)
8804 array([0, 1, 1, 0, 0, 0, 0, 1], dtype=uint8)
8805 >>> unpack_ints(b'ab', 'B', 2)
8806 array([1, 2, 0, 1, 1, 2, 0, 2], dtype=uint8)
8807
8808 """
8809 if itemsize == 1: # bitarray
8810 data = numpy.frombuffer(data, "|B")
8811 data = numpy.unpackbits(data)
8812 if runlen % 8:
8813 data = data.reshape(-1, runlen + (8 - runlen % 8))
8814 data = data[:, :runlen].reshape(-1)
8815 return data.astype(dtype)
8816
8817 dtype = numpy.dtype(dtype)
8818 if itemsize in (8, 16, 32, 64):
8819 return numpy.frombuffer(data, dtype)
8820 if itemsize not in (1, 2, 4, 8, 16, 32):
8821 raise ValueError("itemsize not supported: %i" % itemsize)
8822 if dtype.kind not in "biu":
8823 raise ValueError("invalid dtype")
8824
8825 itembytes = next(i for i in (1, 2, 4, 8) if 8 * i >= itemsize)
8826 if itembytes != dtype.itemsize:
8827 raise ValueError("dtype.itemsize too small")
8828 if runlen == 0:
8829 runlen = (8 * len(data)) // itemsize
8830 skipbits = runlen * itemsize % 8
8831 if skipbits:
8832 skipbits = 8 - skipbits
8833 shrbits = itembytes * 8 - itemsize
8834 bitmask = int(itemsize * "1" + "0" * shrbits, 2)
8835 dtypestr = ">" + dtype.char # dtype always big-endian?
8836
8837 unpack = struct.unpack
8838 size = runlen * (len(data) * 8 // (runlen * itemsize + skipbits))
8839 result = numpy.empty((size,), dtype)
8840 bitcount = 0
8841 for i in range(size):
8842 start = bitcount // 8
8843 s = data[start : start + itembytes]
8844 try:
8845 code = unpack(dtypestr, s)[0]
8846 except Exception:
8847 code = unpack(dtypestr, s + b"\x00" * (itembytes - len(s)))[0]
8848 code <<= bitcount % 8
8849 code &= bitmask
8850 result[i] = code >> shrbits
8851 bitcount += itemsize
8852 if (i + 1) % runlen == 0:
8853 bitcount += skipbits
8854 return result
8855
8856
8857def unpack_rgb(data, dtype="<B", bitspersample=(5, 6, 5), rescale=True):
8858 """Return array from byte string containing packed samples.
8859
8860 Use to unpack RGB565 or RGB555 to RGB888 format.
8861
8862 Parameters
8863 ----------
8864 data : byte str
8865 The data to be decoded. Samples in each pixel are stored consecutively.
8866 Pixels are aligned to 8, 16, or 32 bit boundaries.
8867 dtype : numpy.dtype
8868 The sample data type. The byteorder applies also to the data stream.
8869 bitspersample : tuple
8870 Number of bits for each sample in a pixel.
8871 rescale : bool
8872 Upscale samples to the number of bits in dtype.
8873
8874 Returns
8875 -------
8876 result : ndarray
8877 Flattened array of unpacked samples of native dtype.
8878
8879 Examples
8880 --------
8881 >>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff)
8882 >>> print(unpack_rgb(data, '<B', (5, 6, 5), False))
8883 [ 1 1 1 31 63 31]
8884 >>> print(unpack_rgb(data, '<B', (5, 6, 5)))
8885 [ 8 4 8 255 255 255]
8886 >>> print(unpack_rgb(data, '<B', (5, 5, 5)))
8887 [ 16 8 8 255 255 255]
8888
8889 """
8890 dtype = numpy.dtype(dtype)
8891 bits = int(numpy.sum(bitspersample))
8892 if not (bits <= 32 and all(i <= dtype.itemsize * 8 for i in bitspersample)):
8893 raise ValueError("sample size not supported: %s" % str(bitspersample))
8894 dt = next(i for i in "BHI" if numpy.dtype(i).itemsize * 8 >= bits)
8895 data = numpy.frombuffer(data, dtype.byteorder + dt)
8896 result = numpy.empty((data.size, len(bitspersample)), dtype.char)
8897 for i, bps in enumerate(bitspersample):
8898 t = data >> int(numpy.sum(bitspersample[i + 1 :]))
8899 t &= int("0b" + "1" * bps, 2)
8900 if rescale:
8901 o = ((dtype.itemsize * 8) // bps + 1) * bps
8902 if o > data.dtype.itemsize * 8:
8903 t = t.astype("I")
8904 t *= (2**o - 1) // (2**bps - 1)
8905 t //= 2 ** (o - (dtype.itemsize * 8))
8906 result[:, i] = t
8907 return result.reshape(-1)
8908
8909
8910@_replace_by("_tifffile.reverse_bitorder")
8911def reverse_bitorder(data):
8912 """Reverse bits in each byte of byte string or numpy array.
8913
8914 Decode data where pixels with lower column values are stored in the
8915 lower-order bits of the bytes (FillOrder is LSB2MSB).
8916
8917 Parameters
8918 ----------
8919 data : byte string or ndarray
8920 The data to be bit reversed. If byte string, a new bit-reversed byte
8921 string is returned. Numpy arrays are bit-reversed in-place.
8922
8923 Examples
8924 --------
8925 >>> reverse_bitorder(b'\\x01\\x64')
8926 b'\\x80&'
8927 >>> data = numpy.array([1, 666], dtype='uint16')
8928 >>> reverse_bitorder(data)
8929 >>> data
8930 array([ 128, 16473], dtype=uint16)
8931
8932 """
8933 try:
8934 view = data.view("uint8")
8935 numpy.take(TIFF.REVERSE_BITORDER_ARRAY, view, out=view)
8936 except AttributeError:
8937 return data.translate(TIFF.REVERSE_BITORDER_BYTES)
8938 except ValueError:
8939 raise NotImplementedError("slices of arrays not supported")
8940
8941
8942def apply_colormap(image, colormap, contig=True):
8943 """Return palette-colored image.
8944
8945 The image values are used to index the colormap on axis 1. The returned
8946 image is of shape image.shape+colormap.shape[0] and dtype colormap.dtype.
8947
8948 Parameters
8949 ----------
8950 image : numpy.ndarray
8951 Indexes into the colormap.
8952 colormap : numpy.ndarray
8953 RGB lookup table aka palette of shape (3, 2**bits_per_sample).
8954 contig : bool
8955 If True, return a contiguous array.
8956
8957 Examples
8958 --------
8959 >>> image = numpy.arange(256, dtype='uint8')
8960 >>> colormap = numpy.vstack([image, image, image]).astype('uint16') * 256
8961 >>> apply_colormap(image, colormap)[-1]
8962 array([65280, 65280, 65280], dtype=uint16)
8963
8964 """
8965 image = numpy.take(colormap, image, axis=1)
8966 image = numpy.rollaxis(image, 0, image.ndim)
8967 if contig:
8968 image = numpy.ascontiguousarray(image)
8969 return image
8970
8971
8972def reorient(image, orientation):
8973 """Return reoriented view of image array.
8974
8975 Parameters
8976 ----------
8977 image : numpy.ndarray
8978 Non-squeezed output of asarray() functions.
8979 Axes -3 and -2 must be image length and width respectively.
8980 orientation : int or str
8981 One of TIFF.ORIENTATION names or values.
8982
8983 """
8984 ORIENTATION = TIFF.ORIENTATION
8985 orientation = enumarg(ORIENTATION, orientation)
8986
8987 if orientation == ORIENTATION.TOPLEFT:
8988 return image
8989 elif orientation == ORIENTATION.TOPRIGHT:
8990 return image[..., ::-1, :]
8991 elif orientation == ORIENTATION.BOTLEFT:
8992 return image[..., ::-1, :, :]
8993 elif orientation == ORIENTATION.BOTRIGHT:
8994 return image[..., ::-1, ::-1, :]
8995 elif orientation == ORIENTATION.LEFTTOP:
8996 return numpy.swapaxes(image, -3, -2)
8997 elif orientation == ORIENTATION.RIGHTTOP:
8998 return numpy.swapaxes(image, -3, -2)[..., ::-1, :]
8999 elif orientation == ORIENTATION.RIGHTBOT:
9000 return numpy.swapaxes(image, -3, -2)[..., ::-1, :, :]
9001 elif orientation == ORIENTATION.LEFTBOT:
9002 return numpy.swapaxes(image, -3, -2)[..., ::-1, ::-1, :]
9003
9004
9005def repeat_nd(a, repeats):
9006 """Return read-only view into input array with elements repeated.
9007
9008 Zoom nD image by integer factors using nearest neighbor interpolation
9009 (box filter).
9010
9011 Parameters
9012 ----------
9013 a : array_like
9014 Input array.
9015 repeats : sequence of int
9016 The number of repetitions to apply along each dimension of input array.
9017
9018 Example
9019 -------
9020 >>> repeat_nd([[1, 2], [3, 4]], (2, 2))
9021 array([[1, 1, 2, 2],
9022 [1, 1, 2, 2],
9023 [3, 3, 4, 4],
9024 [3, 3, 4, 4]])
9025
9026 """
9027 a = numpy.asarray(a)
9028 reshape = []
9029 shape = []
9030 strides = []
9031 for i, j, k in zip(a.strides, a.shape, repeats):
9032 shape.extend((j, k))
9033 strides.extend((i, 0))
9034 reshape.append(j * k)
9035 return numpy.lib.stride_tricks.as_strided(
9036 a, shape, strides, writeable=False
9037 ).reshape(reshape)
9038
9039
9040def reshape_nd(data_or_shape, ndim):
9041 """Return image array or shape with at least ndim dimensions.
9042
9043 Prepend 1s to image shape as necessary.
9044
9045 >>> reshape_nd(numpy.empty(0), 1).shape
9046 (0,)
9047 >>> reshape_nd(numpy.empty(1), 2).shape
9048 (1, 1)
9049 >>> reshape_nd(numpy.empty((2, 3)), 3).shape
9050 (1, 2, 3)
9051 >>> reshape_nd(numpy.empty((3, 4, 5)), 3).shape
9052 (3, 4, 5)
9053 >>> reshape_nd((2, 3), 3)
9054 (1, 2, 3)
9055
9056 """
9057 is_shape = isinstance(data_or_shape, tuple)
9058 shape = data_or_shape if is_shape else data_or_shape.shape
9059 if len(shape) >= ndim:
9060 return data_or_shape
9061 shape = (1,) * (ndim - len(shape)) + shape
9062 return shape if is_shape else data_or_shape.reshape(shape)
9063
9064
9065def squeeze_axes(shape, axes, skip="XY"):
9066 """Return shape and axes with single-dimensional entries removed.
9067
9068 Remove unused dimensions unless their axes are listed in 'skip'.
9069
9070 >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC')
9071 ((5, 2, 1), 'TYX')
9072
9073 """
9074 if len(shape) != len(axes):
9075 raise ValueError("dimensions of axes and shape do not match")
9076 shape, axes = zip(*(i for i in zip(shape, axes) if i[0] > 1 or i[1] in skip))
9077 return tuple(shape), "".join(axes)
9078
9079
9080def transpose_axes(image, axes, asaxes="CTZYX"):
9081 """Return image with its axes permuted to match specified axes.
9082
9083 A view is returned if possible.
9084
9085 >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape
9086 (5, 2, 1, 3, 4)
9087
9088 """
9089 for ax in axes:
9090 if ax not in asaxes:
9091 raise ValueError("unknown axis %s" % ax)
9092 # add missing axes to image
9093 shape = image.shape
9094 for ax in reversed(asaxes):
9095 if ax not in axes:
9096 axes = ax + axes
9097 shape = (1,) + shape
9098 image = image.reshape(shape)
9099 # transpose axes
9100 image = image.transpose([axes.index(ax) for ax in asaxes])
9101 return image
9102
9103
9104def reshape_axes(axes, shape, newshape, unknown="Q"):
9105 """Return axes matching new shape.
9106
9107 Unknown dimensions are labelled 'Q'.
9108
9109 >>> reshape_axes('YXS', (219, 301, 1), (219, 301))
9110 'YX'
9111 >>> reshape_axes('IYX', (12, 219, 301), (3, 4, 219, 1, 301, 1))
9112 'QQYQXQ'
9113
9114 """
9115 shape = tuple(shape)
9116 newshape = tuple(newshape)
9117 if len(axes) != len(shape):
9118 raise ValueError("axes do not match shape")
9119
9120 size = product(shape)
9121 newsize = product(newshape)
9122 if size != newsize:
9123 raise ValueError("cannot reshape %s to %s" % (shape, newshape))
9124 if not axes or not newshape:
9125 return ""
9126
9127 lendiff = max(0, len(shape) - len(newshape))
9128 if lendiff:
9129 newshape = newshape + (1,) * lendiff
9130
9131 i = len(shape) - 1
9132 prodns = 1
9133 prods = 1
9134 result = []
9135 for ns in newshape[::-1]:
9136 prodns *= ns
9137 while i > 0 and shape[i] == 1 and ns != 1:
9138 i -= 1
9139 if ns == shape[i] and prodns == prods * shape[i]:
9140 prods *= shape[i]
9141 result.append(axes[i])
9142 i -= 1
9143 else:
9144 result.append(unknown)
9145
9146 return "".join(reversed(result[lendiff:]))
9147
9148
9149def stack_pages(pages, out=None, maxworkers=1, *args, **kwargs):
9150 """Read data from sequence of TiffPage and stack them vertically.
9151
9152 Additional parameters are passed to the TiffPage.asarray function.
9153
9154 """
9155 npages = len(pages)
9156 if npages == 0:
9157 raise ValueError("no pages")
9158
9159 if npages == 1:
9160 return pages[0].asarray(out=out, *args, **kwargs)
9161
9162 page0 = next(p for p in pages if p is not None)
9163 page0.asarray(validate=None) # ThreadPoolExecutor swallows exceptions
9164 shape = (npages,) + page0.keyframe.shape
9165 dtype = page0.keyframe.dtype
9166 out = create_output(out, shape, dtype)
9167
9168 if maxworkers is None:
9169 maxworkers = multiprocessing.cpu_count() // 2
9170 page0.parent.filehandle.lock = maxworkers > 1
9171
9172 filecache = OpenFileCache(
9173 size=max(4, maxworkers), lock=page0.parent.filehandle.lock
9174 )
9175
9176 def func(page, index, out=out, filecache=filecache, args=args, kwargs=kwargs):
9177 """Read, decode, and copy page data."""
9178 if page is not None:
9179 filecache.open(page.parent.filehandle)
9180 out[index] = page.asarray(
9181 lock=filecache.lock, reopen=False, validate=False, *args, **kwargs
9182 )
9183 filecache.close(page.parent.filehandle)
9184
9185 if maxworkers < 2:
9186 for i, page in enumerate(pages):
9187 func(page, i)
9188 else:
9189 with concurrent.futures.ThreadPoolExecutor(maxworkers) as executor:
9190 executor.map(func, pages, range(npages))
9191
9192 filecache.clear()
9193 page0.parent.filehandle.lock = None
9194
9195 return out
9196
9197
9198def clean_offsets_counts(offsets, counts):
9199 """Return cleaned offsets and byte counts.
9200
9201 Remove zero offsets and counts. Use to sanitize _offsets and _bytecounts
9202 tag values for strips or tiles.
9203
9204 """
9205 offsets = list(offsets)
9206 counts = list(counts)
9207 assert len(offsets) == len(counts)
9208 j = 0
9209 for i, (o, b) in enumerate(zip(offsets, counts)):
9210 if o > 0 and b > 0:
9211 if i > j:
9212 offsets[j] = o
9213 counts[j] = b
9214 j += 1
9215 elif b > 0 and o <= 0:
9216 raise ValueError("invalid offset")
9217 else:
9218 warnings.warn("empty byte count")
9219 if j == 0:
9220 j = 1
9221 return offsets[:j], counts[:j]
9222
9223
9224def buffered_read(fh, lock, offsets, bytecounts, buffersize=2**26):
9225 """Return iterator over blocks read from file."""
9226 length = len(offsets)
9227 i = 0
9228 while i < length:
9229 data = []
9230 with lock:
9231 size = 0
9232 while size < buffersize and i < length:
9233 fh.seek(offsets[i])
9234 bytecount = bytecounts[i]
9235 data.append(fh.read(bytecount))
9236 size += bytecount
9237 i += 1
9238 for block in data:
9239 yield block
9240
9241
9242def create_output(out, shape, dtype, mode="w+", suffix=".memmap"):
9243 """Return numpy array where image data of shape and dtype can be copied.
9244
9245 The 'out' parameter may have the following values or types:
9246
9247 None
9248 An empty array of shape and dtype is created and returned.
9249 numpy.ndarray
9250 An existing writable array of compatible dtype and shape. A view of
9251 the same array is returned after verification.
9252 'memmap' or 'memmap:tempdir'
9253 A memory-map to an array stored in a temporary binary file on disk
9254 is created and returned.
9255 str or open file
9256 The file name or file object used to create a memory-map to an array
9257 stored in a binary file on disk. The created memory-mapped array is
9258 returned.
9259
9260 """
9261 if out is None:
9262 return numpy.zeros(shape, dtype)
9263 if isinstance(out, str) and out[:6] == "memmap":
9264 tempdir = out[7:] if len(out) > 7 else None
9265 with tempfile.NamedTemporaryFile(dir=tempdir, suffix=suffix) as fh:
9266 return numpy.memmap(fh, shape=shape, dtype=dtype, mode=mode)
9267 if isinstance(out, numpy.ndarray):
9268 if product(shape) != product(out.shape):
9269 raise ValueError("incompatible output shape")
9270 if not numpy.can_cast(dtype, out.dtype):
9271 raise ValueError("incompatible output dtype")
9272 return out.reshape(shape)
9273 if isinstance(out, pathlib.Path):
9274 out = str(out)
9275 return numpy.memmap(out, shape=shape, dtype=dtype, mode=mode)
9276
9277
9278def matlabstr2py(string):
9279 """Return Python object from Matlab string representation.
9280
9281 Return str, bool, int, float, list (Matlab arrays or cells), or
9282 dict (Matlab structures) types.
9283
9284 Use to access ScanImage metadata.
9285
9286 >>> matlabstr2py('1')
9287 1
9288 >>> matlabstr2py("['x y z' true false; 1 2.0 -3e4; NaN Inf @class]")
9289 [['x y z', True, False], [1, 2.0, -30000.0], [nan, inf, '@class']]
9290 >>> d = matlabstr2py("SI.hChannels.channelType = {'stripe' 'stripe'}\\n"
9291 ... "SI.hChannels.channelsActive = 2")
9292 >>> d['SI.hChannels.channelType']
9293 ['stripe', 'stripe']
9294
9295 """
9296 # TODO: handle invalid input
9297 # TODO: review unboxing of multidimensional arrays
9298
9299 def lex(s):
9300 # return sequence of tokens from matlab string representation
9301 tokens = ["["]
9302 while True:
9303 t, i = next_token(s)
9304 if t is None:
9305 break
9306 if t == ";":
9307 tokens.extend(("]", "["))
9308 elif t == "[":
9309 tokens.extend(("[", "["))
9310 elif t == "]":
9311 tokens.extend(("]", "]"))
9312 else:
9313 tokens.append(t)
9314 s = s[i:]
9315 tokens.append("]")
9316 return tokens
9317
9318 def next_token(s):
9319 # return next token in matlab string
9320 length = len(s)
9321 if length == 0:
9322 return None, 0
9323 i = 0
9324 while i < length and s[i] == " ":
9325 i += 1
9326 if i == length:
9327 return None, i
9328 if s[i] in "{[;]}":
9329 return s[i], i + 1
9330 if s[i] == "'":
9331 j = i + 1
9332 while j < length and s[j] != "'":
9333 j += 1
9334 return s[i : j + 1], j + 1
9335 if s[i] == "<":
9336 j = i + 1
9337 while j < length and s[j] != ">":
9338 j += 1
9339 return s[i : j + 1], j + 1
9340 j = i
9341 while j < length and s[j] not in " {[;]}":
9342 j += 1
9343 return s[i:j], j
9344
9345 def value(s, fail=False):
9346 # return Python value of token
9347 s = s.strip()
9348 if not s:
9349 return s
9350 if len(s) == 1:
9351 try:
9352 return int(s)
9353 except Exception:
9354 if fail:
9355 raise ValueError()
9356 return s
9357 if s[0] == "'":
9358 if fail and s[-1] != "'" or "'" in s[1:-1]:
9359 raise ValueError()
9360 return s[1:-1]
9361 if s[0] == "<":
9362 if fail and s[-1] != ">" or "<" in s[1:-1]:
9363 raise ValueError()
9364 return s
9365 if fail and any(i in s for i in " ';[]{}"):
9366 raise ValueError()
9367 if s[0] == "@":
9368 return s
9369 if s in ("true", "True"):
9370 return True
9371 if s in ("false", "False"):
9372 return False
9373 if s[:6] == "zeros(":
9374 return numpy.zeros([int(i) for i in s[6:-1].split(",")]).tolist()
9375 if s[:5] == "ones(":
9376 return numpy.ones([int(i) for i in s[5:-1].split(",")]).tolist()
9377 if "." in s or "e" in s:
9378 try:
9379 return float(s)
9380 except Exception:
9381 pass
9382 try:
9383 return int(s)
9384 except Exception:
9385 pass
9386 try:
9387 return float(s) # nan, inf
9388 except Exception:
9389 if fail:
9390 raise ValueError()
9391 return s
9392
9393 def parse(s):
9394 # return Python value from string representation of Matlab value
9395 s = s.strip()
9396 try:
9397 return value(s, fail=True)
9398 except ValueError:
9399 pass
9400 result = add2 = []
9401 levels = [add2]
9402 for t in lex(s):
9403 if t in "[{":
9404 add2 = []
9405 levels.append(add2)
9406 elif t in "]}":
9407 x = levels.pop()
9408 if len(x) == 1 and isinstance(x[0], (list, str)):
9409 x = x[0]
9410 add2 = levels[-1]
9411 add2.append(x)
9412 else:
9413 add2.append(value(t))
9414 if len(result) == 1 and isinstance(result[0], (list, str)):
9415 result = result[0]
9416 return result
9417
9418 if "\r" in string or "\n" in string:
9419 # structure
9420 d = {}
9421 for line in string.splitlines():
9422 line = line.strip()
9423 if not line or line[0] == "%":
9424 continue
9425 k, v = line.split("=", 1)
9426 k = k.strip()
9427 if any(c in k for c in " ';[]{}<>"):
9428 continue
9429 d[k] = parse(v)
9430 return d
9431 return parse(string)
9432
9433
9434def stripnull(string, null=b"\x00"):
9435 """Return string truncated at first null character.
9436
9437 Clean NULL terminated C strings. For unicode strings use null='\\0'.
9438
9439 >>> stripnull(b'string\\x00')
9440 b'string'
9441 >>> stripnull('string\\x00', null='\\0')
9442 'string'
9443
9444 """
9445 i = string.find(null)
9446 return string if (i < 0) else string[:i]
9447
9448
9449def stripascii(string):
9450 """Return string truncated at last byte that is 7-bit ASCII.
9451
9452 Clean NULL separated and terminated TIFF strings.
9453
9454 >>> stripascii(b'string\\x00string\\n\\x01\\x00')
9455 b'string\\x00string\\n'
9456 >>> stripascii(b'\\x00')
9457 b''
9458
9459 """
9460 # TODO: pythonize this
9461 i = len(string)
9462 while i:
9463 i -= 1
9464 if 8 < byte2int(string[i]) < 127:
9465 break
9466 else:
9467 i = -1
9468 return string[: i + 1]
9469
9470
9471def asbool(value, true=(b"true", "true"), false=(b"false", "false")):
9472 """Return string as bool if possible, else raise TypeError.
9473
9474 >>> asbool(b' False ')
9475 False
9476
9477 """
9478 value = value.strip().lower()
9479 if value in true: # might raise UnicodeWarning/BytesWarning
9480 return True
9481 if value in false:
9482 return False
9483 raise TypeError()
9484
9485
9486def astype(value, types=None):
9487 """Return argument as one of types if possible.
9488
9489 >>> astype('42')
9490 42
9491 >>> astype('3.14')
9492 3.14
9493 >>> astype('True')
9494 True
9495 >>> astype(b'Neee-Wom')
9496 'Neee-Wom'
9497
9498 """
9499 if types is None:
9500 types = int, float, asbool, bytes2str
9501 for typ in types:
9502 try:
9503 return typ(value)
9504 except (ValueError, AttributeError, TypeError, UnicodeEncodeError):
9505 pass
9506 return value
9507
9508
9509def format_size(size, threshold=1536):
9510 """Return file size as string from byte size.
9511
9512 >>> format_size(1234)
9513 '1234 B'
9514 >>> format_size(12345678901)
9515 '11.50 GiB'
9516
9517 """
9518 if size < threshold:
9519 return "%i B" % size
9520 for unit in ("KiB", "MiB", "GiB", "TiB", "PiB"):
9521 size /= 1024.0
9522 if size < threshold:
9523 return "%.2f %s" % (size, unit)
9524
9525
9526def identityfunc(arg):
9527 """Single argument identity function.
9528
9529 >>> identityfunc('arg')
9530 'arg'
9531
9532 """
9533 return arg
9534
9535
9536def nullfunc(*args, **kwargs):
9537 """Null function.
9538
9539 >>> nullfunc('arg', kwarg='kwarg')
9540
9541 """
9542 return
9543
9544
9545def sequence(value):
9546 """Return tuple containing value if value is not a sequence.
9547
9548 >>> sequence(1)
9549 (1,)
9550 >>> sequence([1])
9551 [1]
9552
9553 """
9554 try:
9555 len(value)
9556 return value
9557 except TypeError:
9558 return (value,)
9559
9560
9561def product(iterable):
9562 """Return product of sequence of numbers.
9563
9564 Equivalent of functools.reduce(operator.mul, iterable, 1).
9565 Multiplying numpy integers might overflow.
9566
9567 >>> product([2**8, 2**30])
9568 274877906944
9569 >>> product([])
9570 1
9571
9572 """
9573 prod = 1
9574 for i in iterable:
9575 prod *= i
9576 return prod
9577
9578
9579def natural_sorted(iterable):
9580 """Return human sorted list of strings.
9581
9582 E.g. for sorting file names.
9583
9584 >>> natural_sorted(['f1', 'f2', 'f10'])
9585 ['f1', 'f2', 'f10']
9586
9587 """
9588
9589 def sortkey(x):
9590 return [(int(c) if c.isdigit() else c) for c in re.split(numbers, x)]
9591
9592 numbers = re.compile(r"(\d+)")
9593 return sorted(iterable, key=sortkey)
9594
9595
9596def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)):
9597 """Return datetime object from timestamp in Excel serial format.
9598
9599 Convert LSM time stamps.
9600
9601 >>> excel_datetime(40237.029999999795)
9602 datetime.datetime(2010, 2, 28, 0, 43, 11, 999982)
9603
9604 """
9605 return epoch + datetime.timedelta(timestamp)
9606
9607
9608def julian_datetime(julianday, milisecond=0):
9609 """Return datetime from days since 1/1/4713 BC and ms since midnight.
9610
9611 Convert Julian dates according to MetaMorph.
9612
9613 >>> julian_datetime(2451576, 54362783)
9614 datetime.datetime(2000, 2, 2, 15, 6, 2, 783)
9615
9616 """
9617 if julianday <= 1721423:
9618 # no datetime before year 1
9619 return None
9620
9621 a = julianday + 1
9622 if a > 2299160:
9623 alpha = math.trunc((a - 1867216.25) / 36524.25)
9624 a += 1 + alpha - alpha // 4
9625 b = a + (1524 if a > 1721423 else 1158)
9626 c = math.trunc((b - 122.1) / 365.25)
9627 d = math.trunc(365.25 * c)
9628 e = math.trunc((b - d) / 30.6001)
9629
9630 day = b - d - math.trunc(30.6001 * e)
9631 month = e - (1 if e < 13.5 else 13)
9632 year = c - (4716 if month > 2.5 else 4715)
9633
9634 hour, milisecond = divmod(milisecond, 1000 * 60 * 60)
9635 minute, milisecond = divmod(milisecond, 1000 * 60)
9636 second, milisecond = divmod(milisecond, 1000)
9637
9638 return datetime.datetime(year, month, day, hour, minute, second, milisecond)
9639
9640
9641def byteorder_isnative(byteorder):
9642 """Return if byteorder matches the system's byteorder.
9643
9644 >>> byteorder_isnative('=')
9645 True
9646
9647 """
9648 if byteorder == "=" or byteorder == sys.byteorder:
9649 return True
9650 keys = {"big": ">", "little": "<"}
9651 return keys.get(byteorder, byteorder) == keys[sys.byteorder]
9652
9653
9654def recarray2dict(recarray):
9655 """Return numpy.recarray as dict."""
9656 # TODO: subarrays
9657 result = {}
9658 for descr, value in zip(recarray.dtype.descr, recarray):
9659 name, dtype = descr[:2]
9660 if dtype[1] == "S":
9661 value = bytes2str(stripnull(value))
9662 elif value.ndim < 2:
9663 value = value.tolist()
9664 result[name] = value
9665 return result
9666
9667
9668def xml2dict(xml, sanitize=True, prefix=None):
9669 """Return XML as dict.
9670
9671 >>> xml2dict('<?xml version="1.0" ?><root attr="name"><key>1</key></root>')
9672 {'root': {'key': 1, 'attr': 'name'}}
9673
9674 """
9675 from xml.etree import cElementTree as etree # delayed import
9676
9677 at = tx = ""
9678 if prefix:
9679 at, tx = prefix
9680
9681 def astype(value):
9682 # return value as int, float, bool, or str
9683 for t in (int, float, asbool):
9684 try:
9685 return t(value)
9686 except Exception:
9687 pass
9688 return value
9689
9690 def etree2dict(t):
9691 # adapted from https://stackoverflow.com/a/10077069/453463
9692 key = t.tag
9693 if sanitize:
9694 key = key.rsplit("}", 1)[-1]
9695 d = {key: {} if t.attrib else None}
9696 children = list(t)
9697 if children:
9698 dd = collections.defaultdict(list)
9699 for dc in map(etree2dict, children):
9700 for k, v in dc.items():
9701 dd[k].append(astype(v))
9702 d = {
9703 key: {
9704 k: astype(v[0]) if len(v) == 1 else astype(v) for k, v in dd.items()
9705 }
9706 }
9707 if t.attrib:
9708 d[key].update((at + k, astype(v)) for k, v in t.attrib.items())
9709 if t.text:
9710 text = t.text.strip()
9711 if children or t.attrib:
9712 if text:
9713 d[key][tx + "value"] = astype(text)
9714 else:
9715 d[key] = astype(text)
9716 return d
9717
9718 return etree2dict(etree.fromstring(xml))
9719
9720
9721def hexdump(bytestr, width=75, height=24, snipat=-2, modulo=2, ellipsis="..."):
9722 """Return hexdump representation of byte string.
9723
9724 >>> hexdump(binascii.unhexlify('49492a00080000000e00fe0004000100'))
9725 '49 49 2a 00 08 00 00 00 0e 00 fe 00 04 00 01 00 II*.............'
9726
9727 """
9728 size = len(bytestr)
9729 if size < 1 or width < 2 or height < 1:
9730 return ""
9731 if height == 1:
9732 addr = b""
9733 bytesperline = min(modulo * (((width - len(addr)) // 4) // modulo), size)
9734 if bytesperline < 1:
9735 return ""
9736 nlines = 1
9737 else:
9738 addr = b"%%0%ix: " % len(b"%x" % size)
9739 bytesperline = min(modulo * (((width - len(addr % 1)) // 4) // modulo), size)
9740 if bytesperline < 1:
9741 return ""
9742 width = 3 * bytesperline + len(addr % 1)
9743 nlines = (size - 1) // bytesperline + 1
9744
9745 if snipat is None or snipat == 1:
9746 snipat = height
9747 elif 0 < abs(snipat) < 1:
9748 snipat = int(math.floor(height * snipat))
9749 if snipat < 0:
9750 snipat += height
9751
9752 if height == 1 or nlines == 1:
9753 blocks = [(0, bytestr[:bytesperline])]
9754 addr = b""
9755 height = 1
9756 width = 3 * bytesperline
9757 elif height is None or nlines <= height:
9758 blocks = [(0, bytestr)]
9759 elif snipat <= 0:
9760 start = bytesperline * (nlines - height)
9761 blocks = [(start, bytestr[start:])] # (start, None)
9762 elif snipat >= height or height < 3:
9763 end = bytesperline * height
9764 blocks = [(0, bytestr[:end])] # (end, None)
9765 else:
9766 end1 = bytesperline * snipat
9767 end2 = bytesperline * (height - snipat - 1)
9768 blocks = [
9769 (0, bytestr[:end1]),
9770 (size - end1 - end2, None),
9771 (size - end2, bytestr[size - end2 :]),
9772 ]
9773
9774 ellipsis = str2bytes(ellipsis)
9775 result = []
9776 for start, bytestr in blocks:
9777 if bytestr is None:
9778 result.append(ellipsis) # 'skip %i bytes' % start)
9779 continue
9780 hexstr = binascii.hexlify(bytestr)
9781 strstr = re.sub(rb"[^\x20-\x7f]", b".", bytestr)
9782 for i in range(0, len(bytestr), bytesperline):
9783 h = hexstr[2 * i : 2 * i + bytesperline * 2]
9784 r = (addr % (i + start)) if height > 1 else addr
9785 r += b" ".join(h[i : i + 2] for i in range(0, 2 * bytesperline, 2))
9786 r += b" " * (width - len(r))
9787 r += strstr[i : i + bytesperline]
9788 result.append(r)
9789 result = b"\n".join(result)
9790 if sys.version_info[0] == 3:
9791 result = result.decode("ascii")
9792 return result
9793
9794
9795def isprintable(string):
9796 """Return if all characters in string are printable.
9797
9798 >>> isprintable('abc')
9799 True
9800 >>> isprintable(b'\01')
9801 False
9802
9803 """
9804 string = string.strip()
9805 if len(string) < 1:
9806 return True
9807 if sys.version_info[0] == 3:
9808 try:
9809 return string.isprintable()
9810 except Exception:
9811 pass
9812 try:
9813 return string.decode("utf-8").isprintable()
9814 except Exception:
9815 pass
9816 else:
9817 if string.isalnum():
9818 return True
9819 printable = (
9820 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRST"
9821 "UVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c"
9822 )
9823 return all(c in printable for c in string)
9824
9825
9826def clean_whitespace(string, compact=False):
9827 """Return string with compressed whitespace."""
9828 for a, b in (
9829 ("\r\n", "\n"),
9830 ("\r", "\n"),
9831 ("\n\n", "\n"),
9832 ("\t", " "),
9833 (" ", " "),
9834 ):
9835 string = string.replace(a, b)
9836 if compact:
9837 for a, b in (("\n", " "), ("[ ", "["), (" ", " "), (" ", " "), (" ", " ")):
9838 string = string.replace(a, b)
9839 return string.strip()
9840
9841
9842def pformat_xml(xml):
9843 """Return pretty formatted XML."""
9844 try:
9845 import lxml.etree as etree # delayed import
9846
9847 if not isinstance(xml, bytes):
9848 xml = xml.encode("utf-8")
9849 xml = etree.parse(io.BytesIO(xml))
9850 xml = etree.tostring(
9851 xml, pretty_print=True, xml_declaration=True, encoding=xml.docinfo.encoding
9852 )
9853 xml = bytes2str(xml)
9854 except Exception:
9855 if isinstance(xml, bytes):
9856 xml = bytes2str(xml)
9857 xml = xml.replace("><", ">\n<")
9858 return xml.replace(" ", " ").replace("\t", " ")
9859
9860
9861def pformat(arg, width=79, height=24, compact=True):
9862 """Return pretty formatted representation of object as string.
9863
9864 Whitespace might be altered.
9865
9866 """
9867 if height is None or height < 1:
9868 height = 1024
9869 if width is None or width < 1:
9870 width = 256
9871
9872 npopt = numpy.get_printoptions()
9873 numpy.set_printoptions(threshold=100, linewidth=width)
9874
9875 if isinstance(arg, basestring):
9876 if arg[:5].lower() in ("<?xml", b"<?xml"):
9877 if height == 1:
9878 arg = arg[: 4 * width]
9879 else:
9880 arg = pformat_xml(arg)
9881 elif isinstance(arg, bytes):
9882 if isprintable(arg):
9883 arg = bytes2str(arg)
9884 arg = clean_whitespace(arg)
9885 else:
9886 numpy.set_printoptions(**npopt)
9887 return hexdump(arg, width=width, height=height, modulo=1)
9888 arg = arg.rstrip()
9889 elif isinstance(arg, numpy.record):
9890 arg = arg.pprint()
9891 else:
9892 import pprint # delayed import
9893
9894 compact = {} if sys.version_info[0] == 2 else dict(compact=compact)
9895 arg = pprint.pformat(arg, width=width, **compact)
9896
9897 numpy.set_printoptions(**npopt)
9898
9899 if height == 1:
9900 arg = clean_whitespace(arg, compact=True)
9901 return arg[:width]
9902
9903 argl = list(arg.splitlines())
9904 if len(argl) > height:
9905 arg = "\n".join(argl[: height // 2] + ["..."] + argl[-height // 2 :])
9906 return arg
9907
9908
9909def snipstr(string, width=79, snipat=0.5, ellipsis="..."):
9910 """Return string cut to specified length.
9911
9912 >>> snipstr('abcdefghijklmnop', 8)
9913 'abc...op'
9914
9915 """
9916 if ellipsis is None:
9917 if isinstance(string, bytes):
9918 ellipsis = b"..."
9919 else:
9920 ellipsis = "\u2026" # does not print on win-py3.5
9921 esize = len(ellipsis)
9922
9923 splitlines = string.splitlines()
9924 # TODO: finish and test multiline snip
9925
9926 result = []
9927 for line in splitlines:
9928 if line is None:
9929 result.append(ellipsis)
9930 continue
9931 linelen = len(line)
9932 if linelen <= width:
9933 result.append(string)
9934 continue
9935
9936 split = snipat
9937 if split is None or split == 1:
9938 split = linelen
9939 elif 0 < abs(split) < 1:
9940 split = int(math.floor(linelen * split))
9941 if split < 0:
9942 split += linelen
9943 if split < 0:
9944 split = 0
9945
9946 if esize == 0 or width < esize + 1:
9947 if split <= 0:
9948 result.append(string[-width:])
9949 else:
9950 result.append(string[:width])
9951 elif split <= 0:
9952 result.append(ellipsis + string[esize - width :])
9953 elif split >= linelen or width < esize + 4:
9954 result.append(string[: width - esize] + ellipsis)
9955 else:
9956 splitlen = linelen - width + esize
9957 end1 = split - splitlen // 2
9958 end2 = end1 + splitlen
9959 result.append(string[:end1] + ellipsis + string[end2:])
9960
9961 if isinstance(string, bytes):
9962 return b"\n".join(result)
9963 else:
9964 return "\n".join(result)
9965
9966
9967def enumarg(enum, arg):
9968 """Return enum member from its name or value.
9969
9970 >>> enumarg(TIFF.PHOTOMETRIC, 2)
9971 <PHOTOMETRIC.RGB: 2>
9972 >>> enumarg(TIFF.PHOTOMETRIC, 'RGB')
9973 <PHOTOMETRIC.RGB: 2>
9974
9975 """
9976 try:
9977 return enum(arg)
9978 except Exception:
9979 try:
9980 return enum[arg.upper()]
9981 except Exception:
9982 raise ValueError("invalid argument %s" % arg)
9983
9984
9985def parse_kwargs(kwargs, *keys, **keyvalues):
9986 """Return dict with keys from keys|keyvals and values from kwargs|keyvals.
9987
9988 Existing keys are deleted from kwargs.
9989
9990 >>> kwargs = {'one': 1, 'two': 2, 'four': 4}
9991 >>> kwargs2 = parse_kwargs(kwargs, 'two', 'three', four=None, five=5)
9992 >>> kwargs == {'one': 1}
9993 True
9994 >>> kwargs2 == {'two': 2, 'four': 4, 'five': 5}
9995 True
9996
9997 """
9998 result = {}
9999 for key in keys:
10000 if key in kwargs:
10001 result[key] = kwargs[key]
10002 del kwargs[key]
10003 for key, value in keyvalues.items():
10004 if key in kwargs:
10005 result[key] = kwargs[key]
10006 del kwargs[key]
10007 else:
10008 result[key] = value
10009 return result
10010
10011
10012def update_kwargs(kwargs, **keyvalues):
10013 """Update dict with keys and values if keys do not already exist.
10014
10015 >>> kwargs = {'one': 1, }
10016 >>> update_kwargs(kwargs, one=None, two=2)
10017 >>> kwargs == {'one': 1, 'two': 2}
10018 True
10019
10020 """
10021 for key, value in keyvalues.items():
10022 if key not in kwargs:
10023 kwargs[key] = value
10024
10025
10026def validate_jhove(filename, jhove="jhove", ignore=("More than 50 IFDs",)):
10027 """Validate TIFF file using jhove -m TIFF-hul.
10028
10029 Raise ValueError if jhove outputs an error message unless the message
10030 contains one of the strings in 'ignore'.
10031
10032 JHOVE does not support bigtiff or more than 50 IFDs.
10033
10034 See `JHOVE TIFF-hul Module <http://jhove.sourceforge.net/tiff-hul.html>`_
10035
10036 """
10037 import subprocess # noqa: delayed import
10038
10039 out = subprocess.check_output([jhove, filename, "-m", "TIFF-hul"])
10040 if b"ErrorMessage: " in out:
10041 for line in out.splitlines():
10042 line = line.strip()
10043 if line.startswith(b"ErrorMessage: "):
10044 error = line[14:].decode("utf8")
10045 for i in ignore:
10046 if i in error:
10047 break
10048 else:
10049 raise ValueError(error)
10050 break
10051
10052
10053def lsm2bin(lsmfile, binfile=None, tile=(256, 256), verbose=True):
10054 """Convert [MP]TZCYX LSM file to series of BIN files.
10055
10056 One BIN file containing 'ZCYX' data are created for each position, time,
10057 and tile. The position, time, and tile indices are encoded at the end
10058 of the filenames.
10059
10060 """
10061 verbose = print_ if verbose else nullfunc
10062
10063 if binfile is None:
10064 binfile = lsmfile
10065 elif binfile.lower() == "none":
10066 binfile = None
10067 if binfile:
10068 binfile += "_(z%ic%iy%ix%i)_m%%ip%%it%%03iy%%ix%%i.bin"
10069
10070 verbose("\nOpening LSM file... ", end="", flush=True)
10071 start_time = time.time()
10072
10073 with TiffFile(lsmfile) as lsm:
10074 if not lsm.is_lsm:
10075 verbose("\n", lsm, flush=True)
10076 raise ValueError("not a LSM file")
10077 series = lsm.series[0] # first series contains the image data
10078 shape = series.shape
10079 axes = series.axes
10080 dtype = series.dtype
10081 size = product(shape) * dtype.itemsize
10082
10083 verbose("%.3f s" % (time.time() - start_time))
10084 # verbose(lsm, flush=True)
10085 verbose(
10086 "Image\n axes: %s\n shape: %s\n dtype: %s\n size: %s"
10087 % (axes, shape, dtype, format_size(size)),
10088 flush=True,
10089 )
10090 if not series.axes.endswith("TZCYX"):
10091 raise ValueError("not a *TZCYX LSM file")
10092
10093 verbose("Copying image from LSM to BIN files", end="", flush=True)
10094 start_time = time.time()
10095 tiles = shape[-2] // tile[-2], shape[-1] // tile[-1]
10096 if binfile:
10097 binfile = binfile % (shape[-4], shape[-3], tile[0], tile[1])
10098 shape = (1,) * (7 - len(shape)) + shape
10099 # cache for ZCYX stacks and output files
10100 data = numpy.empty(shape[3:], dtype=dtype)
10101 out = numpy.empty((shape[-4], shape[-3], tile[0], tile[1]), dtype=dtype)
10102 # iterate over Tiff pages containing data
10103 pages = iter(series.pages)
10104 for m in range(shape[0]): # mosaic axis
10105 for p in range(shape[1]): # position axis
10106 for t in range(shape[2]): # time axis
10107 for z in range(shape[3]): # z slices
10108 data[z] = next(pages).asarray()
10109 for y in range(tiles[0]): # tile y
10110 for x in range(tiles[1]): # tile x
10111 out[:] = data[
10112 ...,
10113 y * tile[0] : (y + 1) * tile[0],
10114 x * tile[1] : (x + 1) * tile[1],
10115 ]
10116 if binfile:
10117 out.tofile(binfile % (m, p, t, y, x))
10118 verbose(".", end="", flush=True)
10119 verbose(" %.3f s" % (time.time() - start_time))
10120
10121
10122def imshow(
10123 data,
10124 title=None,
10125 vmin=0,
10126 vmax=None,
10127 cmap=None,
10128 bitspersample=None,
10129 photometric="RGB",
10130 interpolation=None,
10131 dpi=96,
10132 figure=None,
10133 subplot=111,
10134 maxdim=32768,
10135 **kwargs
10136):
10137 """Plot n-dimensional images using matplotlib.pyplot.
10138
10139 Return figure, subplot and plot axis.
10140 Requires pyplot already imported C{from matplotlib import pyplot}.
10141
10142 Parameters
10143 ----------
10144 bitspersample : int or None
10145 Number of bits per channel in integer RGB images.
10146 photometric : {'MINISWHITE', 'MINISBLACK', 'RGB', or 'PALETTE'}
10147 The color space of the image data.
10148 title : str
10149 Window and subplot title.
10150 figure : matplotlib.figure.Figure (optional).
10151 Matplotlib to use for plotting.
10152 subplot : int
10153 A matplotlib.pyplot.subplot axis.
10154 maxdim : int
10155 maximum image width and length.
10156 kwargs : optional
10157 Arguments for matplotlib.pyplot.imshow.
10158
10159 """
10160 isrgb = photometric in ("RGB",) # 'PALETTE', 'YCBCR'
10161 if data.dtype.kind == "b":
10162 isrgb = False
10163 if isrgb and not (
10164 data.shape[-1] in (3, 4) or (data.ndim > 2 and data.shape[-3] in (3, 4))
10165 ):
10166 isrgb = False
10167 photometric = "MINISBLACK"
10168
10169 data = data.squeeze()
10170 if photometric in ("MINISWHITE", "MINISBLACK", None):
10171 data = reshape_nd(data, 2)
10172 else:
10173 data = reshape_nd(data, 3)
10174
10175 dims = data.ndim
10176 if dims < 2:
10177 raise ValueError("not an image")
10178 elif dims == 2:
10179 dims = 0
10180 isrgb = False
10181 else:
10182 if isrgb and data.shape[-3] in (3, 4):
10183 data = numpy.swapaxes(data, -3, -2)
10184 data = numpy.swapaxes(data, -2, -1)
10185 elif not isrgb and (
10186 data.shape[-1] < data.shape[-2] // 8
10187 and data.shape[-1] < data.shape[-3] // 8
10188 and data.shape[-1] < 5
10189 ):
10190 data = numpy.swapaxes(data, -3, -1)
10191 data = numpy.swapaxes(data, -2, -1)
10192 isrgb = isrgb and data.shape[-1] in (3, 4)
10193 dims -= 3 if isrgb else 2
10194
10195 if isrgb:
10196 data = data[..., :maxdim, :maxdim, :maxdim]
10197 else:
10198 data = data[..., :maxdim, :maxdim]
10199
10200 if photometric == "PALETTE" and isrgb:
10201 datamax = data.max()
10202 if datamax > 255:
10203 data = data >> 8 # possible precision loss
10204 data = data.astype("B")
10205 elif data.dtype.kind in "ui":
10206 if not (isrgb and data.dtype.itemsize <= 1) or bitspersample is None:
10207 try:
10208 bitspersample = int(math.ceil(math.log(data.max(), 2)))
10209 except Exception:
10210 bitspersample = data.dtype.itemsize * 8
10211 elif not isinstance(bitspersample, inttypes):
10212 # bitspersample can be tuple, e.g. (5, 6, 5)
10213 bitspersample = data.dtype.itemsize * 8
10214 datamax = 2**bitspersample
10215 if isrgb:
10216 if bitspersample < 8:
10217 data = data << (8 - bitspersample)
10218 elif bitspersample > 8:
10219 data = data >> (bitspersample - 8) # precision loss
10220 data = data.astype("B")
10221 elif data.dtype.kind == "f":
10222 datamax = data.max()
10223 if isrgb and datamax > 1.0:
10224 if data.dtype.char == "d":
10225 data = data.astype("f")
10226 data /= datamax
10227 else:
10228 data = data / datamax
10229 elif data.dtype.kind == "b":
10230 datamax = 1
10231 elif data.dtype.kind == "c":
10232 data = numpy.absolute(data)
10233 datamax = data.max()
10234
10235 if not isrgb:
10236 if vmax is None:
10237 vmax = datamax
10238 if vmin is None:
10239 if data.dtype.kind == "i":
10240 dtmin = numpy.iinfo(data.dtype).min
10241 vmin = numpy.min(data)
10242 if vmin == dtmin:
10243 vmin = numpy.min(data > dtmin)
10244 if data.dtype.kind == "f":
10245 dtmin = numpy.finfo(data.dtype).min
10246 vmin = numpy.min(data)
10247 if vmin == dtmin:
10248 vmin = numpy.min(data > dtmin)
10249 else:
10250 vmin = 0
10251
10252 pyplot = sys.modules["matplotlib.pyplot"]
10253
10254 if figure is None:
10255 pyplot.rc("font", family="sans-serif", weight="normal", size=8)
10256 figure = pyplot.figure(
10257 dpi=dpi, figsize=(10.3, 6.3), frameon=True, facecolor="1.0", edgecolor="w"
10258 )
10259 try:
10260 figure.canvas.manager.window.title(title)
10261 except Exception:
10262 pass
10263 size = len(title.splitlines()) if title else 1
10264 pyplot.subplots_adjust(
10265 bottom=0.03 * (dims + 2),
10266 top=0.98 - size * 0.03,
10267 left=0.1,
10268 right=0.95,
10269 hspace=0.05,
10270 wspace=0.0,
10271 )
10272 subplot = pyplot.subplot(subplot)
10273
10274 if title:
10275 try:
10276 title = unicode(title, "Windows-1252")
10277 except TypeError:
10278 pass
10279 pyplot.title(title, size=11)
10280
10281 if cmap is None:
10282 if data.dtype.char == "?":
10283 cmap = "gray"
10284 elif data.dtype.kind in "buf" or vmin == 0:
10285 cmap = "viridis"
10286 else:
10287 cmap = "coolwarm"
10288 if photometric == "MINISWHITE":
10289 cmap += "_r"
10290
10291 image = pyplot.imshow(
10292 numpy.atleast_2d(data[(0,) * dims].squeeze()),
10293 vmin=vmin,
10294 vmax=vmax,
10295 cmap=cmap,
10296 interpolation=interpolation,
10297 **kwargs
10298 )
10299
10300 if not isrgb:
10301 pyplot.colorbar() # panchor=(0.55, 0.5), fraction=0.05
10302
10303 def format_coord(x, y):
10304 # callback function to format coordinate display in toolbar
10305 x = int(x + 0.5)
10306 y = int(y + 0.5)
10307 try:
10308 if dims:
10309 return "%s @ %s [%4i, %4i]" % (curaxdat[1][y, x], current, y, x)
10310 return "%s @ [%4i, %4i]" % (data[y, x], y, x)
10311 except IndexError:
10312 return ""
10313
10314 def none(event):
10315 return ""
10316
10317 subplot.format_coord = format_coord
10318 image.get_cursor_data = none
10319 image.format_cursor_data = none
10320
10321 if dims:
10322 current = list((0,) * dims)
10323 curaxdat = [0, data[tuple(current)].squeeze()]
10324 sliders = [
10325 pyplot.Slider(
10326 pyplot.axes([0.125, 0.03 * (axis + 1), 0.725, 0.025]),
10327 "Dimension %i" % axis,
10328 0,
10329 data.shape[axis] - 1,
10330 0,
10331 facecolor="0.5",
10332 valfmt="%%.0f [%i]" % data.shape[axis],
10333 )
10334 for axis in range(dims)
10335 ]
10336 for slider in sliders:
10337 slider.drawon = False
10338
10339 def set_image(current, sliders=sliders, data=data):
10340 # change image and redraw canvas
10341 curaxdat[1] = data[tuple(current)].squeeze()
10342 image.set_data(curaxdat[1])
10343 for ctrl, index in zip(sliders, current):
10344 ctrl.eventson = False
10345 ctrl.set_val(index)
10346 ctrl.eventson = True
10347 figure.canvas.draw()
10348
10349 def on_changed(index, axis, data=data, current=current):
10350 # callback function for slider change event
10351 index = int(round(index))
10352 curaxdat[0] = axis
10353 if index == current[axis]:
10354 return
10355 if index >= data.shape[axis]:
10356 index = 0
10357 elif index < 0:
10358 index = data.shape[axis] - 1
10359 current[axis] = index
10360 set_image(current)
10361
10362 def on_keypressed(event, data=data, current=current):
10363 # callback function for key press event
10364 key = event.key
10365 axis = curaxdat[0]
10366 if str(key) in "0123456789":
10367 on_changed(key, axis)
10368 elif key == "right":
10369 on_changed(current[axis] + 1, axis)
10370 elif key == "left":
10371 on_changed(current[axis] - 1, axis)
10372 elif key == "up":
10373 curaxdat[0] = 0 if axis == len(data.shape) - 1 else axis + 1
10374 elif key == "down":
10375 curaxdat[0] = len(data.shape) - 1 if axis == 0 else axis - 1
10376 elif key == "end":
10377 on_changed(data.shape[axis] - 1, axis)
10378 elif key == "home":
10379 on_changed(0, axis)
10380
10381 figure.canvas.mpl_connect("key_press_event", on_keypressed)
10382 for axis, ctrl in enumerate(sliders):
10383 ctrl.on_changed(lambda k, a=axis: on_changed(k, a))
10384
10385 return figure, subplot, image
10386
10387
10388def _app_show():
10389 """Block the GUI. For use as skimage plugin."""
10390 pyplot = sys.modules["matplotlib.pyplot"]
10391 pyplot.show()
10392
10393
10394def askopenfilename(**kwargs):
10395 """Return file name(s) from Tkinter's file open dialog."""
10396 try:
10397 from Tkinter import Tk
10398 import tkFileDialog as filedialog
10399 except ImportError:
10400 from tkinter import Tk, filedialog
10401 root = Tk()
10402 root.withdraw()
10403 root.update()
10404 filenames = filedialog.askopenfilename(**kwargs)
10405 root.destroy()
10406 return filenames
10407
10408
10409def main(argv=None):
10410 """Command line usage main function."""
10411 if float(sys.version[0:3]) < 2.7:
10412 print("This script requires Python version 2.7 or better.")
10413 print("This is Python version %s" % sys.version)
10414 return 0
10415 if argv is None:
10416 argv = sys.argv
10417
10418 import optparse # TODO: use argparse
10419
10420 parser = optparse.OptionParser(
10421 usage="usage: %prog [options] path",
10422 description="Display image data in TIFF files.",
10423 version="%%prog %s" % __version__,
10424 )
10425 opt = parser.add_option
10426 opt("-p", "--page", dest="page", type="int", default=-1, help="display single page")
10427 opt(
10428 "-s",
10429 "--series",
10430 dest="series",
10431 type="int",
10432 default=-1,
10433 help="display series of pages of same shape",
10434 )
10435 opt(
10436 "--nomultifile",
10437 dest="nomultifile",
10438 action="store_true",
10439 default=False,
10440 help="do not read OME series from multiple files",
10441 )
10442 opt(
10443 "--noplots",
10444 dest="noplots",
10445 type="int",
10446 default=8,
10447 help="maximum number of plots",
10448 )
10449 opt(
10450 "--interpol",
10451 dest="interpol",
10452 metavar="INTERPOL",
10453 default="bilinear",
10454 help="image interpolation method",
10455 )
10456 opt("--dpi", dest="dpi", type="int", default=96, help="plot resolution")
10457 opt(
10458 "--vmin",
10459 dest="vmin",
10460 type="int",
10461 default=None,
10462 help="minimum value for colormapping",
10463 )
10464 opt(
10465 "--vmax",
10466 dest="vmax",
10467 type="int",
10468 default=None,
10469 help="maximum value for colormapping",
10470 )
10471 opt(
10472 "--debug",
10473 dest="debug",
10474 action="store_true",
10475 default=False,
10476 help="raise exception on failures",
10477 )
10478 opt(
10479 "--doctest",
10480 dest="doctest",
10481 action="store_true",
10482 default=False,
10483 help="runs the docstring examples",
10484 )
10485 opt("-v", "--detail", dest="detail", type="int", default=2)
10486 opt("-q", "--quiet", dest="quiet", action="store_true")
10487
10488 settings, path = parser.parse_args()
10489 path = " ".join(path)
10490
10491 if settings.doctest:
10492 import doctest
10493
10494 doctest.testmod(optionflags=doctest.ELLIPSIS)
10495 return 0
10496 if not path:
10497 path = askopenfilename(
10498 title="Select a TIFF file", filetypes=TIFF.FILEOPEN_FILTER
10499 )
10500 if not path:
10501 parser.error("No file specified")
10502
10503 if any(i in path for i in "?*"):
10504 path = glob.glob(path)
10505 if not path:
10506 print("no files match the pattern")
10507 return 0
10508 # TODO: handle image sequences
10509 path = path[0]
10510
10511 if not settings.quiet:
10512 print("\nReading file structure...", end=" ")
10513 start = time.time()
10514 try:
10515 tif = TiffFile(path, multifile=not settings.nomultifile)
10516 except Exception as e:
10517 if settings.debug:
10518 raise
10519 else:
10520 print("\n", e)
10521 sys.exit(0)
10522 if not settings.quiet:
10523 print("%.3f ms" % ((time.time() - start) * 1e3))
10524
10525 if tif.is_ome:
10526 settings.norgb = True
10527
10528 images = []
10529 if settings.noplots > 0:
10530 if not settings.quiet:
10531 print("Reading image data... ", end=" ")
10532
10533 def notnone(x):
10534 return next(i for i in x if i is not None)
10535
10536 start = time.time()
10537 try:
10538 if settings.page >= 0:
10539 images = [(tif.asarray(key=settings.page), tif[settings.page], None)]
10540 elif settings.series >= 0:
10541 images = [
10542 (
10543 tif.asarray(series=settings.series),
10544 notnone(tif.series[settings.series]._pages),
10545 tif.series[settings.series],
10546 )
10547 ]
10548 else:
10549 images = []
10550 for i, s in enumerate(tif.series[: settings.noplots]):
10551 try:
10552 images.append(
10553 (tif.asarray(series=i), notnone(s._pages), tif.series[i])
10554 )
10555 except ValueError as e:
10556 images.append((None, notnone(s.pages), None))
10557 if settings.debug:
10558 raise
10559 else:
10560 print("\nSeries %i failed: %s... " % (i, e), end="")
10561 if not settings.quiet:
10562 print("%.3f ms" % ((time.time() - start) * 1e3))
10563 except Exception as e:
10564 if settings.debug:
10565 raise
10566 else:
10567 print(e)
10568
10569 if not settings.quiet:
10570 print()
10571 print(TiffFile.__str__(tif, detail=int(settings.detail)))
10572 print()
10573 tif.close()
10574
10575 if images and settings.noplots > 0:
10576 try:
10577 import matplotlib
10578
10579 matplotlib.use("TkAgg")
10580 from matplotlib import pyplot
10581 except ImportError as e:
10582 warnings.warn("failed to import matplotlib.\n%s" % e)
10583 else:
10584 for img, page, series in images:
10585 if img is None:
10586 continue
10587 vmin, vmax = settings.vmin, settings.vmax
10588 if "GDAL_NODATA" in page.tags:
10589 try:
10590 vmin = numpy.min(
10591 img[img > float(page.tags["GDAL_NODATA"].value)]
10592 )
10593 except ValueError:
10594 pass
10595 if tif.is_stk:
10596 try:
10597 vmin = tif.stk_metadata["MinScale"]
10598 vmax = tif.stk_metadata["MaxScale"]
10599 except KeyError:
10600 pass
10601 else:
10602 if vmax <= vmin:
10603 vmin, vmax = settings.vmin, settings.vmax
10604 if series:
10605 title = "%s\n%s\n%s" % (str(tif), str(page), str(series))
10606 else:
10607 title = "%s\n %s" % (str(tif), str(page))
10608 photometric = "MINISBLACK"
10609 if page.photometric not in (3,):
10610 photometric = TIFF.PHOTOMETRIC(page.photometric).name
10611 imshow(
10612 img,
10613 title=title,
10614 vmin=vmin,
10615 vmax=vmax,
10616 bitspersample=page.bitspersample,
10617 photometric=photometric,
10618 interpolation=settings.interpol,
10619 dpi=settings.dpi,
10620 )
10621 pyplot.show()
10622
10623
10624if sys.version_info[0] == 2:
10625 inttypes = int, long # noqa
10626
10627 def print_(*args, **kwargs):
10628 """Print function with flush support."""
10629 flush = kwargs.pop("flush", False)
10630 print(*args, **kwargs)
10631 if flush:
10632 sys.stdout.flush()
10633
10634 def bytes2str(b, encoding=None, errors=None):
10635 """Return string from bytes."""
10636 return b
10637
10638 def str2bytes(s, encoding=None):
10639 """Return bytes from string."""
10640 return s
10641
10642 def byte2int(b):
10643 """Return value of byte as int."""
10644 return ord(b)
10645
10646 class FileNotFoundError(IOError):
10647 pass
10648
10649 TiffFrame = TiffPage # noqa
10650else:
10651 inttypes = int
10652 basestring = str, bytes
10653 unicode = str
10654 print_ = print
10655
10656 def bytes2str(b, encoding=None, errors="strict"):
10657 """Return unicode string from encoded bytes."""
10658 if encoding is not None:
10659 return b.decode(encoding, errors)
10660 try:
10661 return b.decode("utf-8", errors)
10662 except UnicodeDecodeError:
10663 return b.decode("cp1252", errors)
10664
10665 def str2bytes(s, encoding="cp1252"):
10666 """Return bytes from unicode string."""
10667 return s.encode(encoding)
10668
10669 def byte2int(b):
10670 """Return value of byte as int."""
10671 return b
10672
10673
10674if __name__ == "__main__":
10675 sys.exit(main())