Vehicle-Anti-Theft-Face-Rec.../venv/Lib/site-packages/imageio/plugins/_tifffile.py

10183 lines
359 KiB
Python
Raw Normal View History

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# tifffile.py
# Copyright (c) 2008-2018, Christoph Gohlke
# Copyright (c) 2008-2018, The Regents of the University of California
# Produced at the Laboratory for Fluorescence Dynamics
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of any
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""Read image and meta data from (bio) TIFF(R) files. Save numpy arrays as TIFF.
Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, STK, LSM, NIH,
SGI, ImageJ, MicroManager, FluoView, ScanImage, SEQ, GEL, and GeoTIFF files.
Tifffile is not a general-purpose TIFF library.
Only a subset of the TIFF specification is supported, mainly uncompressed and
losslessly compressed 1, 8, 16, 32 and 64 bit integer, 16, 32 and 64-bit float,
grayscale and RGB(A) images, which are commonly used in scientific imaging.
Specifically, reading slices of image data, image trees defined via SubIFDs,
CCITT and OJPEG compression, chroma subsampling without JPEG compression,
or IPTC and XMP metadata are not implemented.
TIFF(R), the tagged Image File Format, is a trademark and under control of
Adobe Systems Incorporated. BigTIFF allows for files greater than 4 GB.
STK, LSM, FluoView, SGI, SEQ, GEL, and OME-TIFF, are custom extensions
defined by Molecular Devices (Universal Imaging Corporation), Carl Zeiss
MicroImaging, Olympus, Silicon Graphics International, Media Cybernetics,
Molecular Dynamics, and the Open Microscopy Environment consortium
respectively.
For command line usage run C{python -m tifffile --help}
:Author:
`Christoph Gohlke <https://www.lfd.uci.edu/~gohlke/>`_
:Organization:
Laboratory for Fluorescence Dynamics, University of California, Irvine
:Version: 2018.06.15
Requirements
------------
* `CPython 3.6 64-bit <https://www.python.org>`_
* `Numpy 1.14 <http://www.numpy.org>`_
* `Matplotlib 2.2 <https://www.matplotlib.org>`_ (optional for plotting)
* `Tifffile.c 2018.02.10 <https://www.lfd.uci.edu/~gohlke/>`_
(recommended for faster decoding of PackBits and LZW encoded strings)
* `Tifffile_geodb.py 2018.02.10 <https://www.lfd.uci.edu/~gohlke/>`_
(optional enums for GeoTIFF metadata)
* Python 2 requires 'futures', 'enum34', 'pathlib'.
Revisions
---------
2018.06.15
Pass 2680 tests.
Towards reading JPEG and other compressions via imagecodecs package (WIP).
Add function to validate TIFF using 'jhove -m TIFF-hul'.
Save bool arrays as bilevel TIFF.
Accept pathlib.Path as filenames.
Move 'software' argument from TiffWriter __init__ to save.
Raise DOS limit to 16 TB.
Lazy load lzma and zstd compressors and decompressors.
Add option to save IJMetadata tags.
Return correct number of pages for truncated series (bug fix).
Move EXIF tags to TIFF.TAG as per TIFF/EP standard.
2018.02.18
Pass 2293 tests.
Always save RowsPerStrip and Resolution tags as required by TIFF standard.
Do not use badly typed ImageDescription.
Coherce bad ASCII string tags to bytes.
Tuning of __str__ functions.
Fix reading 'undefined' tag values (bug fix).
Read and write ZSTD compressed data.
Use hexdump to print byte strings.
Determine TIFF byte order from data dtype in imsave.
Add option to specify RowsPerStrip for compressed strips.
Allow memory map of arrays with non-native byte order.
Attempt to handle ScanImage <= 5.1 files.
Restore TiffPageSeries.pages sequence interface.
Use numpy.frombuffer instead of fromstring to read from binary data.
Parse GeoTIFF metadata.
Add option to apply horizontal differencing before compression.
Towards reading PerkinElmer QPTIFF (no test files).
Do not index out of bounds data in tifffile.c unpackbits and decodelzw.
2017.09.29 (tentative)
Many backwards incompatible changes improving speed and resource usage:
Pass 2268 tests.
Add detail argument to __str__ function. Remove info functions.
Fix potential issue correcting offsets of large LSM files with positions.
Remove TiffFile sequence interface; use TiffFile.pages instead.
Do not make tag values available as TiffPage attributes.
Use str (not bytes) type for tag and metadata strings (WIP).
Use documented standard tag and value names (WIP).
Use enums for some documented TIFF tag values.
Remove 'memmap' and 'tmpfile' options; use out='memmap' instead.
Add option to specify output in asarray functions.
Add option to concurrently decode image strips or tiles using threads.
Add TiffPage.asrgb function (WIP).
Do not apply colormap in asarray.
Remove 'colormapped', 'rgbonly', and 'scale_mdgel' options from asarray.
Consolidate metadata in TiffFile _metadata functions.
Remove non-tag metadata properties from TiffPage.
Add function to convert LSM to tiled BIN files.
Align image data in file.
Make TiffPage.dtype a numpy.dtype.
Add 'ndim' and 'size' properties to TiffPage and TiffPageSeries.
Allow imsave to write non-BigTIFF files up to ~4 GB.
Only read one page for shaped series if possible.
Add memmap function to create memory-mapped array stored in TIFF file.
Add option to save empty arrays to TIFF files.
Add option to save truncated TIFF files.
Allow single tile images to be saved contiguously.
Add optional movie mode for files with uniform pages.
Lazy load pages.
Use lightweight TiffFrame for IFDs sharing properties with key TiffPage.
Move module constants to 'TIFF' namespace (speed up module import).
Remove 'fastij' option from TiffFile.
Remove 'pages' parameter from TiffFile.
Remove TIFFfile alias.
Deprecate Python 2.
Require enum34 and futures packages on Python 2.7.
Remove Record class and return all metadata as dict instead.
Add functions to parse STK, MetaSeries, ScanImage, SVS, Pilatus metadata.
Read tags from EXIF and GPS IFDs.
Use pformat for tag and metadata values.
Fix reading some UIC tags (bug fix).
Do not modify input array in imshow (bug fix).
Fix Python implementation of unpack_ints.
2017.05.23
Pass 1961 tests.
Write correct number of SampleFormat values (bug fix).
Use Adobe deflate code to write ZIP compressed files.
Add option to pass tag values as packed binary data for writing.
Defer tag validation to attribute access.
Use property instead of lazyattr decorator for simple expressions.
2017.03.17
Write IFDs and tag values on word boundaries.
Read ScanImage metadata.
Remove is_rgb and is_indexed attributes from TiffFile.
Create files used by doctests.
2017.01.12
Read Zeiss SEM metadata.
Read OME-TIFF with invalid references to external files.
Rewrite C LZW decoder (5x faster).
Read corrupted LSM files missing EOI code in LZW stream.
2017.01.01
Add option to append images to existing TIFF files.
Read files without pages.
Read S-FEG and Helios NanoLab tags created by FEI software.
Allow saving Color Filter Array (CFA) images.
Add info functions returning more information about TiffFile and TiffPage.
Add option to read specific pages only.
Remove maxpages argument (backwards incompatible).
Remove test_tifffile function.
2016.10.28
Pass 1944 tests.
Improve detection of ImageJ hyperstacks.
Read TVIPS metadata created by EM-MENU (by Marco Oster).
Add option to disable using OME-XML metadata.
Allow non-integer range attributes in modulo tags (by Stuart Berg).
2016.06.21
Do not always memmap contiguous data in page series.
2016.05.13
Add option to specify resolution unit.
Write grayscale images with extra samples when planarconfig is specified.
Do not write RGB color images with 2 samples.
Reorder TiffWriter.save keyword arguments (backwards incompatible).
2016.04.18
Pass 1932 tests.
TiffWriter, imread, and imsave accept open binary file streams.
2016.04.13
Correctly handle reversed fill order in 2 and 4 bps images (bug fix).
Implement reverse_bitorder in C.
2016.03.18
Fix saving additional ImageJ metadata.
2016.02.22
Pass 1920 tests.
Write 8 bytes double tag values using offset if necessary (bug fix).
Add option to disable writing second image description tag.
Detect tags with incorrect counts.
Disable color mapping for LSM.
2015.11.13
Read LSM 6 mosaics.
Add option to specify directory of memory-mapped files.
Add command line options to specify vmin and vmax values for colormapping.
2015.10.06
New helper function to apply colormaps.
Renamed is_palette attributes to is_indexed (backwards incompatible).
Color-mapped samples are now contiguous (backwards incompatible).
Do not color-map ImageJ hyperstacks (backwards incompatible).
Towards reading Leica SCN.
2015.09.25
Read images with reversed bit order (FillOrder is LSB2MSB).
2015.09.21
Read RGB OME-TIFF.
Warn about malformed OME-XML.
2015.09.16
Detect some corrupted ImageJ metadata.
Better axes labels for 'shaped' files.
Do not create TiffTag for default values.
Chroma subsampling is not supported.
Memory-map data in TiffPageSeries if possible (optional).
2015.08.17
Pass 1906 tests.
Write ImageJ hyperstacks (optional).
Read and write LZMA compressed data.
Specify datetime when saving (optional).
Save tiled and color-mapped images (optional).
Ignore void bytecounts and offsets if possible.
Ignore bogus image_depth tag created by ISS Vista software.
Decode floating point horizontal differencing (not tiled).
Save image data contiguously if possible.
Only read first IFD from ImageJ files if possible.
Read ImageJ 'raw' format (files larger than 4 GB).
TiffPageSeries class for pages with compatible shape and data type.
Try to read incomplete tiles.
Open file dialog if no filename is passed on command line.
Ignore errors when decoding OME-XML.
Rename decoder functions (backwards incompatible).
2014.08.24
TiffWriter class for incremental writing images.
Simplify examples.
2014.08.19
Add memmap function to FileHandle.
Add function to determine if image data in TiffPage is memory-mappable.
Do not close files if multifile_close parameter is False.
2014.08.10
Pass 1730 tests.
Return all extrasamples by default (backwards incompatible).
Read data from series of pages into memory-mapped array (optional).
Squeeze OME dimensions (backwards incompatible).
Workaround missing EOI code in strips.
Support image and tile depth tags (SGI extension).
Better handling of STK/UIC tags (backwards incompatible).
Disable color mapping for STK.
Julian to datetime converter.
TIFF ASCII type may be NULL separated.
Unwrap strip offsets for LSM files greater than 4 GB.
Correct strip byte counts in compressed LSM files.
Skip missing files in OME series.
Read embedded TIFF files.
2014.02.05
Save rational numbers as type 5 (bug fix).
2013.12.20
Keep other files in OME multi-file series closed.
FileHandle class to abstract binary file handle.
Disable color mapping for bad OME-TIFF produced by bio-formats.
Read bad OME-XML produced by ImageJ when cropping.
2013.11.03
Allow zlib compress data in imsave function (optional).
Memory-map contiguous image data (optional).
2013.10.28
Read MicroManager metadata and little-endian ImageJ tag.
Save extra tags in imsave function.
Save tags in ascending order by code (bug fix).
2012.10.18
Accept file like objects (read from OIB files).
2012.08.21
Rename TIFFfile to TiffFile and TIFFpage to TiffPage.
TiffSequence class for reading sequence of TIFF files.
Read UltraQuant tags.
Allow float numbers as resolution in imsave function.
2012.08.03
Read MD GEL tags and NIH Image header.
2012.07.25
Read ImageJ tags.
...
Notes
-----
The API is not stable yet and might change between revisions.
Tested on little-endian platforms only.
Other Python packages and modules for reading (bio) scientific TIFF files:
* `python-bioformats <https://github.com/CellProfiler/python-bioformats>`_
* `Imread <https://github.com/luispedro/imread>`_
* `PyLibTiff <https://github.com/pearu/pylibtiff>`_
* `ITK <https://www.itk.org>`_
* `PyLSM <https://launchpad.net/pylsm>`_
* `PyMca.TiffIO.py <https://github.com/vasole/pymca>`_ (same as fabio.TiffIO)
* `BioImageXD.Readers <http://www.bioimagexd.net/>`_
* `Cellcognition.io <http://cellcognition.org/>`_
* `pymimage <https://github.com/ardoi/pymimage>`_
* `pytiff <https://github.com/FZJ-INM1-BDA/pytiff>`_
Acknowledgements
----------------
* Egor Zindy, University of Manchester, for lsm_scan_info specifics.
* Wim Lewis for a bug fix and some LSM functions.
* Hadrien Mary for help on reading MicroManager files.
* Christian Kliche for help writing tiled and color-mapped files.
References
----------
1) TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated.
http://partners.adobe.com/public/developer/tiff/
2) TIFF File Format FAQ. http://www.awaresystems.be/imaging/tiff/faq.html
3) MetaMorph Stack (STK) Image File Format.
http://support.meta.moleculardevices.com/docs/t10243.pdf
4) Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010).
Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011
5) The OME-TIFF format.
http://www.openmicroscopy.org/site/support/file-formats/ome-tiff
6) UltraQuant(r) Version 6.0 for Windows Start-Up Guide.
http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf
7) Micro-Manager File Formats.
http://www.micro-manager.org/wiki/Micro-Manager_File_Formats
8) Tags for TIFF and Related Specifications. Digital Preservation.
http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml
9) ScanImage BigTiff Specification - ScanImage 2016.
http://scanimage.vidriotechnologies.com/display/SI2016/
ScanImage+BigTiff+Specification
10) CIPA DC-008-2016: Exchangeable image file format for digital still cameras:
Exif Version 2.31.
http://www.cipa.jp/std/documents/e/DC-008-Translation-2016-E.pdf
Examples
--------
>>> # write numpy array to TIFF file
>>> data = numpy.random.rand(4, 301, 219)
>>> imsave('temp.tif', data, photometric='minisblack')
>>> # read numpy array from TIFF file
>>> image = imread('temp.tif')
>>> numpy.testing.assert_array_equal(image, data)
>>> # iterate over pages and tags in TIFF file
>>> with TiffFile('temp.tif') as tif:
... images = tif.asarray()
... for page in tif.pages:
... for tag in page.tags.values():
... _ = tag.name, tag.value
... image = page.asarray()
"""
from __future__ import division, print_function
import sys
import os
import io
import re
import glob
import math
import zlib
import time
import json
import enum
import struct
import pathlib
import warnings
import binascii
import tempfile
import datetime
import threading
import collections
import multiprocessing
import concurrent.futures
import numpy
# delay imports: mmap, pprint, fractions, xml, tkinter, matplotlib, lzma, zstd,
# subprocess
__version__ = '2018.06.15'
__docformat__ = 'restructuredtext en'
__all__ = (
'imsave', 'imread', 'imshow', 'memmap',
'TiffFile', 'TiffWriter', 'TiffSequence',
# utility functions used by oiffile or czifile
'FileHandle', 'lazyattr', 'natural_sorted', 'decode_lzw', 'stripnull',
'create_output', 'repeat_nd', 'format_size', 'product', 'xml2dict')
def imread(files, **kwargs):
"""Return image data from TIFF file(s) as numpy array.
Refer to the TiffFile class and member functions for documentation.
Parameters
----------
files : str, binary stream, or sequence
File name, seekable binary stream, glob pattern, or sequence of
file names.
kwargs : dict
Parameters 'multifile' and 'is_ome' are passed to the TiffFile class.
The 'pattern' parameter is passed to the TiffSequence class.
Other parameters are passed to the asarray functions.
The first image series is returned if no arguments are provided.
Examples
--------
>>> # get image from first page
>>> imsave('temp.tif', numpy.random.rand(3, 4, 301, 219))
>>> im = imread('temp.tif', key=0)
>>> im.shape
(4, 301, 219)
>>> # get images from sequence of files
>>> ims = imread(['temp.tif', 'temp.tif'])
>>> ims.shape
(2, 3, 4, 301, 219)
"""
kwargs_file = parse_kwargs(kwargs, 'multifile', 'is_ome')
kwargs_seq = parse_kwargs(kwargs, 'pattern')
if isinstance(files, basestring) and any(i in files for i in '?*'):
files = glob.glob(files)
if not files:
raise ValueError('no files found')
if not hasattr(files, 'seek') and len(files) == 1:
files = files[0]
if isinstance(files, basestring) or hasattr(files, 'seek'):
with TiffFile(files, **kwargs_file) as tif:
return tif.asarray(**kwargs)
else:
with TiffSequence(files, **kwargs_seq) as imseq:
return imseq.asarray(**kwargs)
def imsave(file, data=None, shape=None, dtype=None, bigsize=2**32-2**25,
**kwargs):
"""Write numpy array to TIFF file.
Refer to the TiffWriter class and member functions for documentation.
Parameters
----------
file : str or binary stream
File name or writable binary stream, such as an open file or BytesIO.
data : array_like
Input image. The last dimensions are assumed to be image depth,
height, width, and samples.
If None, an empty array of the specified shape and dtype is
saved to file.
Unless 'byteorder' is specified in 'kwargs', the TIFF file byte order
is determined from the data's dtype or the dtype argument.
shape : tuple
If 'data' is None, shape of an empty array to save to the file.
dtype : numpy.dtype
If 'data' is None, data-type of an empty array to save to the file.
bigsize : int
Create a BigTIFF file if the size of data in bytes is larger than
this threshold and 'imagej' or 'truncate' are not enabled.
By default, the threshold is 4 GB minus 32 MB reserved for metadata.
Use the 'bigtiff' parameter to explicitly specify the type of
file created.
kwargs : dict
Parameters 'append', 'byteorder', 'bigtiff', and 'imagej', are passed
to TiffWriter(). Other parameters are passed to TiffWriter.save().
Returns
-------
If the image data are written contiguously, return offset and bytecount
of image data in the file.
Examples
--------
>>> # save a RGB image
>>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
>>> imsave('temp.tif', data, photometric='rgb')
>>> # save a random array and metadata, using compression
>>> data = numpy.random.rand(2, 5, 3, 301, 219)
>>> imsave('temp.tif', data, compress=6, metadata={'axes': 'TZCYX'})
"""
tifargs = parse_kwargs(kwargs, 'append', 'bigtiff', 'byteorder', 'imagej')
if data is None:
size = product(shape) * numpy.dtype(dtype).itemsize
byteorder = numpy.dtype(dtype).byteorder
else:
try:
size = data.nbytes
byteorder = data.dtype.byteorder
except Exception:
size = 0
byteorder = None
if size > bigsize and 'bigtiff' not in tifargs and not (
tifargs.get('imagej', False) or tifargs.get('truncate', False)):
tifargs['bigtiff'] = True
if 'byteorder' not in tifargs:
tifargs['byteorder'] = byteorder
with TiffWriter(file, **tifargs) as tif:
return tif.save(data, shape, dtype, **kwargs)
def memmap(filename, shape=None, dtype=None, page=None, series=0, mode='r+',
**kwargs):
"""Return memory-mapped numpy array stored in TIFF file.
Memory-mapping requires data stored in native byte order, without tiling,
compression, predictors, etc.
If 'shape' and 'dtype' are provided, existing files will be overwritten or
appended to depending on the 'append' parameter.
Otherwise the image data of a specified page or series in an existing
file will be memory-mapped. By default, the image data of the first page
series is memory-mapped.
Call flush() to write any changes in the array to the file.
Raise ValueError if the image data in the file is not memory-mappable.
Parameters
----------
filename : str
Name of the TIFF file which stores the array.
shape : tuple
Shape of the empty array.
dtype : numpy.dtype
Data-type of the empty array.
page : int
Index of the page which image data to memory-map.
series : int
Index of the page series which image data to memory-map.
mode : {'r+', 'r', 'c'}, optional
The file open mode. Default is to open existing file for reading and
writing ('r+').
kwargs : dict
Additional parameters passed to imsave() or TiffFile().
Examples
--------
>>> # create an empty TIFF file and write to memory-mapped image
>>> im = memmap('temp.tif', shape=(256, 256), dtype='float32')
>>> im[255, 255] = 1.0
>>> im.flush()
>>> im.shape, im.dtype
((256, 256), dtype('float32'))
>>> del im
>>> # memory-map image data in a TIFF file
>>> im = memmap('temp.tif', page=0)
>>> im[255, 255]
1.0
"""
if shape is not None and dtype is not None:
# create a new, empty array
kwargs.update(data=None, shape=shape, dtype=dtype, returnoffset=True,
align=TIFF.ALLOCATIONGRANULARITY)
result = imsave(filename, **kwargs)
if result is None:
# TODO: fail before creating file or writing data
raise ValueError('image data are not memory-mappable')
offset = result[0]
else:
# use existing file
with TiffFile(filename, **kwargs) as tif:
if page is not None:
page = tif.pages[page]
if not page.is_memmappable:
raise ValueError('image data are not memory-mappable')
offset, _ = page.is_contiguous
shape = page.shape
dtype = page.dtype
else:
series = tif.series[series]
if series.offset is None:
raise ValueError('image data are not memory-mappable')
shape = series.shape
dtype = series.dtype
offset = series.offset
dtype = tif.byteorder + dtype.char
return numpy.memmap(filename, dtype, mode, offset, shape, 'C')
class lazyattr(object):
"""Attribute whose value is computed on first access."""
# TODO: help() doesn't work
__slots__ = ('func',)
def __init__(self, func):
self.func = func
# self.__name__ = func.__name__
# self.__doc__ = func.__doc__
# self.lock = threading.RLock()
def __get__(self, instance, owner):
# with self.lock:
if instance is None:
return self
try:
value = self.func(instance)
except AttributeError as e:
raise RuntimeError(e)
if value is NotImplemented:
return getattr(super(owner, instance), self.func.__name__)
setattr(instance, self.func.__name__, value)
return value
class TiffWriter(object):
"""Write numpy arrays to TIFF file.
TiffWriter instances must be closed using the 'close' method, which is
automatically called when using the 'with' context manager.
TiffWriter's main purpose is saving nD numpy array's as TIFF,
not to create any possible TIFF format. Specifically, JPEG compression,
SubIFDs, ExifIFD, or GPSIFD tags are not supported.
Examples
--------
>>> # successively append images to BigTIFF file
>>> data = numpy.random.rand(2, 5, 3, 301, 219)
>>> with TiffWriter('temp.tif', bigtiff=True) as tif:
... for i in range(data.shape[0]):
... tif.save(data[i], compress=6, photometric='minisblack')
"""
def __init__(self, file, bigtiff=False, byteorder=None, append=False,
imagej=False):
"""Open a TIFF file for writing.
An empty TIFF file is created if the file does not exist, else the
file is overwritten with an empty TIFF file unless 'append'
is true. Use bigtiff=True when creating files larger than 4 GB.
Parameters
----------
file : str, binary stream, or FileHandle
File name or writable binary stream, such as an open file
or BytesIO.
bigtiff : bool
If True, the BigTIFF format is used.
byteorder : {'<', '>', '=', '|'}
The endianness of the data in the file.
By default, this is the system's native byte order.
append : bool
If True and 'file' is an existing standard TIFF file, image data
and tags are appended to the file.
Appending data may corrupt specifically formatted TIFF files
such as LSM, STK, ImageJ, NIH, or FluoView.
imagej : bool
If True, write an ImageJ hyperstack compatible file.
This format can handle data types uint8, uint16, or float32 and
data shapes up to 6 dimensions in TZCYXS order.
RGB images (S=3 or S=4) must be uint8.
ImageJ's default byte order is big-endian but this implementation
uses the system's native byte order by default.
ImageJ does not support BigTIFF format or LZMA compression.
The ImageJ file format is undocumented.
"""
if append:
# determine if file is an existing TIFF file that can be extended
try:
with FileHandle(file, mode='rb', size=0) as fh:
pos = fh.tell()
try:
with TiffFile(fh) as tif:
if (append != 'force' and
any(getattr(tif, 'is_'+a) for a in (
'lsm', 'stk', 'imagej', 'nih',
'fluoview', 'micromanager'))):
raise ValueError('file contains metadata')
byteorder = tif.byteorder
bigtiff = tif.is_bigtiff
self._ifdoffset = tif.pages.next_page_offset
except Exception as e:
raise ValueError('cannot append to file: %s' % str(e))
finally:
fh.seek(pos)
except (IOError, FileNotFoundError):
append = False
if byteorder in (None, '=', '|'):
byteorder = '<' if sys.byteorder == 'little' else '>'
elif byteorder not in ('<', '>'):
raise ValueError('invalid byteorder %s' % byteorder)
if imagej and bigtiff:
warnings.warn('writing incompatible BigTIFF ImageJ')
self._byteorder = byteorder
self._imagej = bool(imagej)
self._truncate = False
self._metadata = None
self._colormap = None
self._descriptionoffset = 0
self._descriptionlen = 0
self._descriptionlenoffset = 0
self._tags = None
self._shape = None # normalized shape of data in consecutive pages
self._datashape = None # shape of data in consecutive pages
self._datadtype = None # data type
self._dataoffset = None # offset to data
self._databytecounts = None # byte counts per plane
self._tagoffsets = None # strip or tile offset tag code
if bigtiff:
self._bigtiff = True
self._offsetsize = 8
self._tagsize = 20
self._tagnoformat = 'Q'
self._offsetformat = 'Q'
self._valueformat = '8s'
else:
self._bigtiff = False
self._offsetsize = 4
self._tagsize = 12
self._tagnoformat = 'H'
self._offsetformat = 'I'
self._valueformat = '4s'
if append:
self._fh = FileHandle(file, mode='r+b', size=0)
self._fh.seek(0, 2)
else:
self._fh = FileHandle(file, mode='wb', size=0)
self._fh.write({'<': b'II', '>': b'MM'}[byteorder])
if bigtiff:
self._fh.write(struct.pack(byteorder+'HHH', 43, 8, 0))
else:
self._fh.write(struct.pack(byteorder+'H', 42))
# first IFD
self._ifdoffset = self._fh.tell()
self._fh.write(struct.pack(byteorder+self._offsetformat, 0))
def save(self, data=None, shape=None, dtype=None, returnoffset=False,
photometric=None, planarconfig=None, tile=None, contiguous=True,
align=16, truncate=False, compress=0, rowsperstrip=None,
predictor=False, colormap=None, description=None,
datetime=None, resolution=None, software='tifffile.py',
metadata={}, ijmetadata=None, extratags=()):
"""Write numpy array and tags to TIFF file.
The data shape's last dimensions are assumed to be image depth,
height (length), width, and samples.
If a colormap is provided, the data's dtype must be uint8 or uint16
and the data values are indices into the last dimension of the
colormap.
If 'shape' and 'dtype' are specified, an empty array is saved.
This option cannot be used with compression or multiple tiles.
Image data are written uncompressed in one strip per plane by default.
Dimensions larger than 2 to 4 (depending on photometric mode, planar
configuration, and SGI mode) are flattened and saved as separate pages.
The SampleFormat and BitsPerSample tags are derived from the data type.
Parameters
----------
data : numpy.ndarray or None
Input image array.
shape : tuple or None
Shape of the empty array to save. Used only if 'data' is None.
dtype : numpy.dtype or None
Data-type of the empty array to save. Used only if 'data' is None.
returnoffset : bool
If True and the image data in the file is memory-mappable, return
the offset and number of bytes of the image data in the file.
photometric : {'MINISBLACK', 'MINISWHITE', 'RGB', 'PALETTE', 'CFA'}
The color space of the image data.
By default, this setting is inferred from the data shape and the
value of colormap.
For CFA images, DNG tags must be specified in 'extratags'.
planarconfig : {'CONTIG', 'SEPARATE'}
Specifies if samples are stored contiguous or in separate planes.
By default, this setting is inferred from the data shape.
If this parameter is set, extra samples are used to store grayscale
images.
'CONTIG': last dimension contains samples.
'SEPARATE': third last dimension contains samples.
tile : tuple of int
The shape (depth, length, width) of image tiles to write.
If None (default), image data are written in strips.
The tile length and width must be a multiple of 16.
If the tile depth is provided, the SGI ImageDepth and TileDepth
tags are used to save volume data.
Unless a single tile is used, tiles cannot be used to write
contiguous files.
Few software can read the SGI format, e.g. MeVisLab.
contiguous : bool
If True (default) and the data and parameters are compatible with
previous ones, if any, the image data are stored contiguously after
the previous one. Parameters 'photometric' and 'planarconfig'
are ignored. Parameters 'description', datetime', and 'extratags'
are written to the first page of a contiguous series only.
align : int
Byte boundary on which to align the image data in the file.
Default 16. Use mmap.ALLOCATIONGRANULARITY for memory-mapped data.
Following contiguous writes are not aligned.
truncate : bool
If True, only write the first page including shape metadata if
possible (uncompressed, contiguous, not tiled).
Other TIFF readers will only be able to read part of the data.
compress : int or 'LZMA', 'ZSTD'
Values from 0 to 9 controlling the level of zlib compression.
If 0 (default), data are written uncompressed.
Compression cannot be used to write contiguous files.
If 'LZMA' or 'ZSTD', LZMA or ZSTD compression is used, which is
not available on all platforms.
rowsperstrip : int
The number of rows per strip used for compression.
Uncompressed data are written in one strip per plane.
predictor : bool
If True, apply horizontal differencing to integer type images
before compression.
colormap : numpy.ndarray
RGB color values for the corresponding data value.
Must be of shape (3, 2**(data.itemsize*8)) and dtype uint16.
description : str
The subject of the image. Must be 7-bit ASCII. Cannot be used with
the ImageJ format. Saved with the first page only.
datetime : datetime
Date and time of image creation in '%Y:%m:%d %H:%M:%S' format.
If None (default), the current date and time is used.
Saved with the first page only.
resolution : (float, float[, str]) or ((int, int), (int, int)[, str])
X and Y resolutions in pixels per resolution unit as float or
rational numbers. A third, optional parameter specifies the
resolution unit, which must be None (default for ImageJ),
'INCH' (default), or 'CENTIMETER'.
software : str
Name of the software used to create the file. Must be 7-bit ASCII.
Saved with the first page only.
metadata : dict
Additional meta data to be saved along with shape information
in JSON or ImageJ formats in an ImageDescription tag.
If None, do not write a second ImageDescription tag.
Strings must be 7-bit ASCII. Saved with the first page only.
ijmetadata : dict
Additional meta data to be saved in application specific
IJMetadata and IJMetadataByteCounts tags. Refer to the
imagej_metadata_tags function for valid keys and values.
Saved with the first page only.
extratags : sequence of tuples
Additional tags as [(code, dtype, count, value, writeonce)].
code : int
The TIFF tag Id.
dtype : str
Data type of items in 'value' in Python struct format.
One of B, s, H, I, 2I, b, h, i, 2i, f, d, Q, or q.
count : int
Number of data values. Not used for string or byte string
values.
value : sequence
'Count' values compatible with 'dtype'.
Byte strings must contain count values of dtype packed as
binary data.
writeonce : bool
If True, the tag is written to the first page only.
"""
# TODO: refactor this function
fh = self._fh
byteorder = self._byteorder
if data is None:
if compress:
raise ValueError('cannot save compressed empty file')
datashape = shape
datadtype = numpy.dtype(dtype).newbyteorder(byteorder)
datadtypechar = datadtype.char
else:
data = numpy.asarray(data, byteorder+data.dtype.char, 'C')
if data.size == 0:
raise ValueError('cannot save empty array')
datashape = data.shape
datadtype = data.dtype
datadtypechar = data.dtype.char
returnoffset = returnoffset and datadtype.isnative
bilevel = datadtypechar == '?'
if bilevel:
index = -1 if datashape[-1] > 1 else -2
datasize = product(datashape[:index])
if datashape[index] % 8:
datasize *= datashape[index] // 8 + 1
else:
datasize *= datashape[index] // 8
else:
datasize = product(datashape) * datadtype.itemsize
# just append contiguous data if possible
self._truncate = bool(truncate)
if self._datashape:
if (not contiguous
or self._datashape[1:] != datashape
or self._datadtype != datadtype
or (compress and self._tags)
or tile
or not numpy.array_equal(colormap, self._colormap)):
# incompatible shape, dtype, compression mode, or colormap
self._write_remaining_pages()
self._write_image_description()
self._truncate = False
self._descriptionoffset = 0
self._descriptionlenoffset = 0
self._datashape = None
self._colormap = None
if self._imagej:
raise ValueError(
'ImageJ does not support non-contiguous data')
else:
# consecutive mode
self._datashape = (self._datashape[0] + 1,) + datashape
if not compress:
# write contiguous data, write IFDs/tags later
offset = fh.tell()
if data is None:
fh.write_empty(datasize)
else:
fh.write_array(data)
if returnoffset:
return offset, datasize
return
input_shape = datashape
tagnoformat = self._tagnoformat
valueformat = self._valueformat
offsetformat = self._offsetformat
offsetsize = self._offsetsize
tagsize = self._tagsize
MINISBLACK = TIFF.PHOTOMETRIC.MINISBLACK
RGB = TIFF.PHOTOMETRIC.RGB
CFA = TIFF.PHOTOMETRIC.CFA
PALETTE = TIFF.PHOTOMETRIC.PALETTE
CONTIG = TIFF.PLANARCONFIG.CONTIG
SEPARATE = TIFF.PLANARCONFIG.SEPARATE
# parse input
if photometric is not None:
photometric = enumarg(TIFF.PHOTOMETRIC, photometric)
if planarconfig:
planarconfig = enumarg(TIFF.PLANARCONFIG, planarconfig)
if not compress:
compress = False
compresstag = 1
predictor = False
else:
if isinstance(compress, (tuple, list)):
compress, compresslevel = compress
elif isinstance(compress, int):
compress, compresslevel = 'ADOBE_DEFLATE', int(compress)
if not 0 <= compresslevel <= 9:
raise ValueError('invalid compression level %s' % compress)
else:
compresslevel = None
compress = compress.upper()
compresstag = enumarg(TIFF.COMPRESSION, compress)
# prepare ImageJ format
if self._imagej:
if compress in ('LZMA', 'ZSTD'):
raise ValueError(
'ImageJ cannot handle LZMA or ZSTD compression')
if description:
warnings.warn('not writing description to ImageJ file')
description = None
volume = False
if datadtypechar not in 'BHhf':
raise ValueError(
'ImageJ does not support data type %s' % datadtypechar)
ijrgb = photometric == RGB if photometric else None
if datadtypechar not in 'B':
ijrgb = False
ijshape = imagej_shape(datashape, ijrgb)
if ijshape[-1] in (3, 4):
photometric = RGB
if datadtypechar not in 'B':
raise ValueError('ImageJ does not support data type %s '
'for RGB' % datadtypechar)
elif photometric is None:
photometric = MINISBLACK
planarconfig = None
if planarconfig == SEPARATE:
raise ValueError('ImageJ does not support planar images')
else:
planarconfig = CONTIG if ijrgb else None
# define compress function
if compress:
if compresslevel is None:
compressor, compresslevel = TIFF.COMPESSORS[compresstag]
else:
compressor, _ = TIFF.COMPESSORS[compresstag]
compresslevel = int(compresslevel)
if predictor:
if datadtype.kind not in 'iu':
raise ValueError(
'prediction not implemented for %s' % datadtype)
def compress(data, level=compresslevel):
# horizontal differencing
diff = numpy.diff(data, axis=-2)
data = numpy.insert(diff, 0, data[..., 0, :], axis=-2)
return compressor(data, level)
else:
def compress(data, level=compresslevel):
return compressor(data, level)
# verify colormap and indices
if colormap is not None:
if datadtypechar not in 'BH':
raise ValueError('invalid data dtype for palette mode')
colormap = numpy.asarray(colormap, dtype=byteorder+'H')
if colormap.shape != (3, 2**(datadtype.itemsize * 8)):
raise ValueError('invalid color map shape')
self._colormap = colormap
# verify tile shape
if tile:
tile = tuple(int(i) for i in tile[:3])
volume = len(tile) == 3
if (len(tile) < 2 or tile[-1] % 16 or tile[-2] % 16 or
any(i < 1 for i in tile)):
raise ValueError('invalid tile shape')
else:
tile = ()
volume = False
# normalize data shape to 5D or 6D, depending on volume:
# (pages, planar_samples, [depth,] height, width, contig_samples)
datashape = reshape_nd(datashape, 3 if photometric == RGB else 2)
shape = datashape
ndim = len(datashape)
samplesperpixel = 1
extrasamples = 0
if volume and ndim < 3:
volume = False
if colormap is not None:
photometric = PALETTE
planarconfig = None
if photometric is None:
photometric = MINISBLACK
if bilevel:
photometric = TIFF.PHOTOMETRIC.MINISWHITE
elif planarconfig == CONTIG:
if ndim > 2 and shape[-1] in (3, 4):
photometric = RGB
elif planarconfig == SEPARATE:
if volume and ndim > 3 and shape[-4] in (3, 4):
photometric = RGB
elif ndim > 2 and shape[-3] in (3, 4):
photometric = RGB
elif ndim > 2 and shape[-1] in (3, 4):
photometric = RGB
elif self._imagej:
photometric = MINISBLACK
elif volume and ndim > 3 and shape[-4] in (3, 4):
photometric = RGB
elif ndim > 2 and shape[-3] in (3, 4):
photometric = RGB
if planarconfig and len(shape) <= (3 if volume else 2):
planarconfig = None
photometric = MINISBLACK
if photometric == RGB:
if len(shape) < 3:
raise ValueError('not a RGB(A) image')
if len(shape) < 4:
volume = False
if planarconfig is None:
if shape[-1] in (3, 4):
planarconfig = CONTIG
elif shape[-4 if volume else -3] in (3, 4):
planarconfig = SEPARATE
elif shape[-1] > shape[-4 if volume else -3]:
planarconfig = SEPARATE
else:
planarconfig = CONTIG
if planarconfig == CONTIG:
datashape = (-1, 1) + shape[(-4 if volume else -3):]
samplesperpixel = datashape[-1]
else:
datashape = (-1,) + shape[(-4 if volume else -3):] + (1,)
samplesperpixel = datashape[1]
if samplesperpixel > 3:
extrasamples = samplesperpixel - 3
elif photometric == CFA:
if len(shape) != 2:
raise ValueError('invalid CFA image')
volume = False
planarconfig = None
datashape = (-1, 1) + shape[-2:] + (1,)
if 50706 not in (et[0] for et in extratags):
raise ValueError('must specify DNG tags for CFA image')
elif planarconfig and len(shape) > (3 if volume else 2):
if planarconfig == CONTIG:
datashape = (-1, 1) + shape[(-4 if volume else -3):]
samplesperpixel = datashape[-1]
else:
datashape = (-1,) + shape[(-4 if volume else -3):] + (1,)
samplesperpixel = datashape[1]
extrasamples = samplesperpixel - 1
else:
planarconfig = None
# remove trailing 1s
while len(shape) > 2 and shape[-1] == 1:
shape = shape[:-1]
if len(shape) < 3:
volume = False
datashape = (-1, 1) + shape[(-3 if volume else -2):] + (1,)
# normalize shape to 6D
assert len(datashape) in (5, 6)
if len(datashape) == 5:
datashape = datashape[:2] + (1,) + datashape[2:]
if datashape[0] == -1:
s0 = product(input_shape) // product(datashape[1:])
datashape = (s0,) + datashape[1:]
shape = datashape
if data is not None:
data = data.reshape(shape)
if tile and not volume:
tile = (1, tile[-2], tile[-1])
if photometric == PALETTE:
if (samplesperpixel != 1 or extrasamples or
shape[1] != 1 or shape[-1] != 1):
raise ValueError('invalid data shape for palette mode')
if photometric == RGB and samplesperpixel == 2:
raise ValueError('not a RGB image (samplesperpixel=2)')
if bilevel:
if compress:
raise ValueError('cannot save compressed bilevel image')
if tile:
raise ValueError('cannot save tiled bilevel image')
if photometric not in (0, 1):
raise ValueError('cannot save bilevel image as %s' %
str(photometric))
datashape = list(datashape)
if datashape[-2] % 8:
datashape[-2] = datashape[-2] // 8 + 1
else:
datashape[-2] = datashape[-2] // 8
datashape = tuple(datashape)
assert datasize == product(datashape)
if data is not None:
data = numpy.packbits(data, axis=-2)
assert datashape[-2] == data.shape[-2]
bytestr = bytes if sys.version[0] == '2' else (
lambda x: bytes(x, 'ascii') if isinstance(x, str) else x)
tags = [] # list of (code, ifdentry, ifdvalue, writeonce)
strip_or_tile = 'Tile' if tile else 'Strip'
tagbytecounts = TIFF.TAG_NAMES[strip_or_tile + 'ByteCounts']
tag_offsets = TIFF.TAG_NAMES[strip_or_tile + 'Offsets']
self._tagoffsets = tag_offsets
def pack(fmt, *val):
return struct.pack(byteorder+fmt, *val)
def addtag(code, dtype, count, value, writeonce=False):
# Compute ifdentry & ifdvalue bytes from code, dtype, count, value
# Append (code, ifdentry, ifdvalue, writeonce) to tags list
code = int(TIFF.TAG_NAMES.get(code, code))
try:
tifftype = TIFF.DATA_DTYPES[dtype]
except KeyError:
raise ValueError('unknown dtype %s' % dtype)
rawcount = count
if dtype == 's':
# strings
value = bytestr(value) + b'\0'
count = rawcount = len(value)
rawcount = value.find(b'\0\0')
if rawcount < 0:
rawcount = count
else:
rawcount += 1 # length of string without buffer
value = (value,)
elif isinstance(value, bytes):
# packed binary data
dtsize = struct.calcsize(dtype)
if len(value) % dtsize:
raise ValueError('invalid packed binary data')
count = len(value) // dtsize
if len(dtype) > 1:
count *= int(dtype[:-1])
dtype = dtype[-1]
ifdentry = [pack('HH', code, tifftype),
pack(offsetformat, rawcount)]
ifdvalue = None
if struct.calcsize(dtype) * count <= offsetsize:
# value(s) can be written directly
if isinstance(value, bytes):
ifdentry.append(pack(valueformat, value))
elif count == 1:
if isinstance(value, (tuple, list, numpy.ndarray)):
value = value[0]
ifdentry.append(pack(valueformat, pack(dtype, value)))
else:
ifdentry.append(pack(valueformat,
pack(str(count)+dtype, *value)))
else:
# use offset to value(s)
ifdentry.append(pack(offsetformat, 0))
if isinstance(value, bytes):
ifdvalue = value
elif isinstance(value, numpy.ndarray):
assert value.size == count
assert value.dtype.char == dtype
ifdvalue = value.tostring()
elif isinstance(value, (tuple, list)):
ifdvalue = pack(str(count)+dtype, *value)
else:
ifdvalue = pack(dtype, value)
tags.append((code, b''.join(ifdentry), ifdvalue, writeonce))
def rational(arg, max_denominator=1000000):
""""Return nominator and denominator from float or two integers."""
from fractions import Fraction # delayed import
try:
f = Fraction.from_float(arg)
except TypeError:
f = Fraction(arg[0], arg[1])
f = f.limit_denominator(max_denominator)
return f.numerator, f.denominator
if description:
# user provided description
addtag('ImageDescription', 's', 0, description, writeonce=True)
# write shape and metadata to ImageDescription
self._metadata = {} if not metadata else metadata.copy()
if self._imagej:
description = imagej_description(
input_shape, shape[-1] in (3, 4), self._colormap is not None,
**self._metadata)
elif metadata or metadata == {}:
if self._truncate:
self._metadata.update(truncated=True)
description = json_description(input_shape, **self._metadata)
else:
description = None
if description:
# add 64 bytes buffer
# the image description might be updated later with the final shape
description = str2bytes(description, 'ascii')
description += b'\0'*64
self._descriptionlen = len(description)
addtag('ImageDescription', 's', 0, description, writeonce=True)
if software:
addtag('Software', 's', 0, software, writeonce=True)
if datetime is None:
datetime = self._now()
addtag('DateTime', 's', 0, datetime.strftime('%Y:%m:%d %H:%M:%S'),
writeonce=True)
addtag('Compression', 'H', 1, compresstag)
if predictor:
addtag('Predictor', 'H', 1, 2)
addtag('ImageWidth', 'I', 1, shape[-2])
addtag('ImageLength', 'I', 1, shape[-3])
if tile:
addtag('TileWidth', 'I', 1, tile[-1])
addtag('TileLength', 'I', 1, tile[-2])
if tile[0] > 1:
addtag('ImageDepth', 'I', 1, shape[-4])
addtag('TileDepth', 'I', 1, tile[0])
addtag('NewSubfileType', 'I', 1, 0)
if not bilevel:
sampleformat = {'u': 1, 'i': 2, 'f': 3, 'c': 6}[datadtype.kind]
addtag('SampleFormat', 'H', samplesperpixel,
(sampleformat,) * samplesperpixel)
addtag('PhotometricInterpretation', 'H', 1, photometric.value)
if colormap is not None:
addtag('ColorMap', 'H', colormap.size, colormap)
addtag('SamplesPerPixel', 'H', 1, samplesperpixel)
if bilevel:
pass
elif planarconfig and samplesperpixel > 1:
addtag('PlanarConfiguration', 'H', 1, planarconfig.value)
addtag('BitsPerSample', 'H', samplesperpixel,
(datadtype.itemsize * 8,) * samplesperpixel)
else:
addtag('BitsPerSample', 'H', 1, datadtype.itemsize * 8)
if extrasamples:
if photometric == RGB and extrasamples == 1:
addtag('ExtraSamples', 'H', 1, 1) # associated alpha channel
else:
addtag('ExtraSamples', 'H', extrasamples, (0,) * extrasamples)
if resolution is not None:
addtag('XResolution', '2I', 1, rational(resolution[0]))
addtag('YResolution', '2I', 1, rational(resolution[1]))
if len(resolution) > 2:
unit = resolution[2]
unit = 1 if unit is None else enumarg(TIFF.RESUNIT, unit)
elif self._imagej:
unit = 1
else:
unit = 2
addtag('ResolutionUnit', 'H', 1, unit)
elif not self._imagej:
addtag('XResolution', '2I', 1, (1, 1))
addtag('YResolution', '2I', 1, (1, 1))
addtag('ResolutionUnit', 'H', 1, 1)
if ijmetadata:
for t in imagej_metadata_tags(ijmetadata, byteorder):
addtag(*t)
contiguous = not compress
if tile:
# one chunk per tile per plane
tiles = ((shape[2] + tile[0] - 1) // tile[0],
(shape[3] + tile[1] - 1) // tile[1],
(shape[4] + tile[2] - 1) // tile[2])
numtiles = product(tiles) * shape[1]
stripbytecounts = [
product(tile) * shape[-1] * datadtype.itemsize] * numtiles
addtag(tagbytecounts, offsetformat, numtiles, stripbytecounts)
addtag(tag_offsets, offsetformat, numtiles, [0] * numtiles)
contiguous = contiguous and product(tiles) == 1
if not contiguous:
# allocate tile buffer
chunk = numpy.empty(tile + (shape[-1],), dtype=datadtype)
elif contiguous:
# one strip per plane
if bilevel:
stripbytecounts = [product(datashape[2:])] * shape[1]
else:
stripbytecounts = [
product(datashape[2:]) * datadtype.itemsize] * shape[1]
addtag(tagbytecounts, offsetformat, shape[1], stripbytecounts)
addtag(tag_offsets, offsetformat, shape[1], [0] * shape[1])
addtag('RowsPerStrip', 'I', 1, shape[-3])
else:
# compress rowsperstrip or ~64 KB chunks
rowsize = product(shape[-2:]) * datadtype.itemsize
if rowsperstrip is None:
rowsperstrip = 65536 // rowsize
if rowsperstrip < 1:
rowsperstrip = 1
elif rowsperstrip > shape[-3]:
rowsperstrip = shape[-3]
addtag('RowsPerStrip', 'I', 1, rowsperstrip)
numstrips = (shape[-3] + rowsperstrip - 1) // rowsperstrip
numstrips *= shape[1]
stripbytecounts = [0] * numstrips
addtag(tagbytecounts, offsetformat, numstrips, [0] * numstrips)
addtag(tag_offsets, offsetformat, numstrips, [0] * numstrips)
if data is None and not contiguous:
raise ValueError('cannot write non-contiguous empty file')
# add extra tags from user
for t in extratags:
addtag(*t)
# TODO: check TIFFReadDirectoryCheckOrder warning in files containing
# multiple tags of same code
# the entries in an IFD must be sorted in ascending order by tag code
tags = sorted(tags, key=lambda x: x[0])
if not (self._bigtiff or self._imagej) and (
fh.tell() + datasize > 2**31-1):
raise ValueError('data too large for standard TIFF file')
# if not compressed or multi-tiled, write the first IFD and then
# all data contiguously; else, write all IFDs and data interleaved
for pageindex in range(1 if contiguous else shape[0]):
# update pointer at ifd_offset
pos = fh.tell()
if pos % 2:
# location of IFD must begin on a word boundary
fh.write(b'\0')
pos += 1
fh.seek(self._ifdoffset)
fh.write(pack(offsetformat, pos))
fh.seek(pos)
# write ifdentries
fh.write(pack(tagnoformat, len(tags)))
tag_offset = fh.tell()
fh.write(b''.join(t[1] for t in tags))
self._ifdoffset = fh.tell()
fh.write(pack(offsetformat, 0)) # offset to next IFD
# write tag values and patch offsets in ifdentries, if necessary
for tagindex, tag in enumerate(tags):
if tag[2]:
pos = fh.tell()
if pos % 2:
# tag value is expected to begin on word boundary
fh.write(b'\0')
pos += 1
fh.seek(tag_offset + tagindex*tagsize + offsetsize + 4)
fh.write(pack(offsetformat, pos))
fh.seek(pos)
if tag[0] == tag_offsets:
stripoffsetsoffset = pos
elif tag[0] == tagbytecounts:
strip_bytecounts_offset = pos
elif tag[0] == 270 and tag[2].endswith(b'\0\0\0\0'):
# image description buffer
self._descriptionoffset = pos
self._descriptionlenoffset = (
tag_offset + tagindex * tagsize + 4)
fh.write(tag[2])
# write image data
data_offset = fh.tell()
skip = align - data_offset % align
fh.seek(skip, 1)
data_offset += skip
if contiguous:
if data is None:
fh.write_empty(datasize)
else:
fh.write_array(data)
elif tile:
if data is None:
fh.write_empty(numtiles * stripbytecounts[0])
else:
stripindex = 0
for plane in data[pageindex]:
for tz in range(tiles[0]):
for ty in range(tiles[1]):
for tx in range(tiles[2]):
c0 = min(tile[0], shape[2] - tz*tile[0])
c1 = min(tile[1], shape[3] - ty*tile[1])
c2 = min(tile[2], shape[4] - tx*tile[2])
chunk[c0:, c1:, c2:] = 0
chunk[:c0, :c1, :c2] = plane[
tz*tile[0]:tz*tile[0]+c0,
ty*tile[1]:ty*tile[1]+c1,
tx*tile[2]:tx*tile[2]+c2]
if compress:
t = compress(chunk)
fh.write(t)
stripbytecounts[stripindex] = len(t)
stripindex += 1
else:
fh.write_array(chunk)
fh.flush()
elif compress:
# write one strip per rowsperstrip
assert data.shape[2] == 1 # not handling depth
numstrips = (shape[-3] + rowsperstrip - 1) // rowsperstrip
stripindex = 0
for plane in data[pageindex]:
for i in range(numstrips):
strip = plane[0, i*rowsperstrip: (i+1)*rowsperstrip]
strip = compress(strip)
fh.write(strip)
stripbytecounts[stripindex] = len(strip)
stripindex += 1
# update strip/tile offsets and bytecounts if necessary
pos = fh.tell()
for tagindex, tag in enumerate(tags):
if tag[0] == tag_offsets: # strip/tile offsets
if tag[2]:
fh.seek(stripoffsetsoffset)
strip_offset = data_offset
for size in stripbytecounts:
fh.write(pack(offsetformat, strip_offset))
strip_offset += size
else:
fh.seek(tag_offset + tagindex*tagsize + offsetsize + 4)
fh.write(pack(offsetformat, data_offset))
elif tag[0] == tagbytecounts: # strip/tile bytecounts
if compress:
if tag[2]:
fh.seek(strip_bytecounts_offset)
for size in stripbytecounts:
fh.write(pack(offsetformat, size))
else:
fh.seek(tag_offset + tagindex*tagsize +
offsetsize + 4)
fh.write(pack(offsetformat, stripbytecounts[0]))
break
fh.seek(pos)
fh.flush()
# remove tags that should be written only once
if pageindex == 0:
tags = [tag for tag in tags if not tag[-1]]
self._shape = shape
self._datashape = (1,) + input_shape
self._datadtype = datadtype
self._dataoffset = data_offset
self._databytecounts = stripbytecounts
if contiguous:
# write remaining IFDs/tags later
self._tags = tags
# return offset and size of image data
if returnoffset:
return data_offset, sum(stripbytecounts)
def _write_remaining_pages(self):
"""Write outstanding IFDs and tags to file."""
if not self._tags or self._truncate:
return
fh = self._fh
fhpos = fh.tell()
if fhpos % 2:
fh.write(b'\0')
fhpos += 1
byteorder = self._byteorder
offsetformat = self._offsetformat
offsetsize = self._offsetsize
tagnoformat = self._tagnoformat
tagsize = self._tagsize
dataoffset = self._dataoffset
pagedatasize = sum(self._databytecounts)
pageno = self._shape[0] * self._datashape[0] - 1
def pack(fmt, *val):
return struct.pack(byteorder+fmt, *val)
# construct template IFD in memory
# need to patch offsets to next IFD and data before writing to disk
ifd = io.BytesIO()
ifd.write(pack(tagnoformat, len(self._tags)))
tagoffset = ifd.tell()
ifd.write(b''.join(t[1] for t in self._tags))
ifdoffset = ifd.tell()
ifd.write(pack(offsetformat, 0)) # offset to next IFD
# tag values
for tagindex, tag in enumerate(self._tags):
offset2value = tagoffset + tagindex*tagsize + offsetsize + 4
if tag[2]:
pos = ifd.tell()
if pos % 2: # tag value is expected to begin on word boundary
ifd.write(b'\0')
pos += 1
ifd.seek(offset2value)
try:
ifd.write(pack(offsetformat, pos + fhpos))
except Exception: # struct.error
if self._imagej:
warnings.warn('truncating ImageJ file')
self._truncate = True
return
raise ValueError('data too large for non-BigTIFF file')
ifd.seek(pos)
ifd.write(tag[2])
if tag[0] == self._tagoffsets:
# save strip/tile offsets for later updates
stripoffset2offset = offset2value
stripoffset2value = pos
elif tag[0] == self._tagoffsets:
# save strip/tile offsets for later updates
stripoffset2offset = None
stripoffset2value = offset2value
# size to word boundary
if ifd.tell() % 2:
ifd.write(b'\0')
# check if all IFDs fit in file
pos = fh.tell()
if not self._bigtiff and pos + ifd.tell() * pageno > 2**32 - 256:
if self._imagej:
warnings.warn('truncating ImageJ file')
self._truncate = True
return
raise ValueError('data too large for non-BigTIFF file')
# TODO: assemble IFD chain in memory
for _ in range(pageno):
# update pointer at IFD offset
pos = fh.tell()
fh.seek(self._ifdoffset)
fh.write(pack(offsetformat, pos))
fh.seek(pos)
self._ifdoffset = pos + ifdoffset
# update strip/tile offsets in IFD
dataoffset += pagedatasize # offset to image data
if stripoffset2offset is None:
ifd.seek(stripoffset2value)
ifd.write(pack(offsetformat, dataoffset))
else:
ifd.seek(stripoffset2offset)
ifd.write(pack(offsetformat, pos + stripoffset2value))
ifd.seek(stripoffset2value)
stripoffset = dataoffset
for size in self._databytecounts:
ifd.write(pack(offsetformat, stripoffset))
stripoffset += size
# write IFD entry
fh.write(ifd.getvalue())
self._tags = None
self._datadtype = None
self._dataoffset = None
self._databytecounts = None
# do not reset _shape or _data_shape
def _write_image_description(self):
"""Write meta data to ImageDescription tag."""
if (not self._datashape or self._datashape[0] == 1 or
self._descriptionoffset <= 0):
return
colormapped = self._colormap is not None
if self._imagej:
isrgb = self._shape[-1] in (3, 4)
description = imagej_description(
self._datashape, isrgb, colormapped, **self._metadata)
else:
description = json_description(self._datashape, **self._metadata)
# rewrite description and its length to file
description = description.encode('utf-8')
description = description[:self._descriptionlen-1]
pos = self._fh.tell()
self._fh.seek(self._descriptionoffset)
self._fh.write(description)
self._fh.seek(self._descriptionlenoffset)
self._fh.write(struct.pack(self._byteorder+self._offsetformat,
len(description)+1))
self._fh.seek(pos)
self._descriptionoffset = 0
self._descriptionlenoffset = 0
self._descriptionlen = 0
def _now(self):
"""Return current date and time."""
return datetime.datetime.now()
def close(self):
"""Write remaining pages and close file handle."""
if not self._truncate:
self._write_remaining_pages()
self._write_image_description()
self._fh.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
class TiffFile(object):
"""Read image and metadata from TIFF file.
TiffFile instances must be closed using the 'close' method, which is
automatically called when using the 'with' context manager.
Attributes
----------
pages : TiffPages
Sequence of TIFF pages in file.
series : list of TiffPageSeries
Sequences of closely related TIFF pages. These are computed
from OME, LSM, ImageJ, etc. metadata or based on similarity
of page properties such as shape, dtype, and compression.
byteorder : '>', '<'
The endianness of data in the file.
'>': big-endian (Motorola).
'>': little-endian (Intel).
is_flag : bool
If True, file is of a certain format.
Flags are: bigtiff, movie, shaped, ome, imagej, stk, lsm, fluoview,
nih, vista, 'micromanager, metaseries, mdgel, mediacy, tvips, fei,
sem, scn, svs, scanimage, andor, epics, pilatus, qptiff.
All attributes are read-only.
Examples
--------
>>> # read image array from TIFF file
>>> imsave('temp.tif', numpy.random.rand(5, 301, 219))
>>> with TiffFile('temp.tif') as tif:
... data = tif.asarray()
>>> data.shape
(5, 301, 219)
"""
def __init__(self, arg, name=None, offset=None, size=None,
multifile=True, movie=None, **kwargs):
"""Initialize instance from file.
Parameters
----------
arg : str or open file
Name of file or open file object.
The file objects are closed in TiffFile.close().
name : str
Optional name of file in case 'arg' is a file handle.
offset : int
Optional start position of embedded file. By default, this is
the current file position.
size : int
Optional size of embedded file. By default, this is the number
of bytes from the 'offset' to the end of the file.
multifile : bool
If True (default), series may include pages from multiple files.
Currently applies to OME-TIFF only.
movie : bool
If True, assume that later pages differ from first page only by
data offsets and byte counts. Significantly increases speed and
reduces memory usage when reading movies with thousands of pages.
Enabling this for non-movie files will result in data corruption
or crashes. Python 3 only.
kwargs : bool
'is_ome': If False, disable processing of OME-XML metadata.
"""
if 'fastij' in kwargs:
del kwargs['fastij']
raise DeprecationWarning('the fastij option will be removed')
for key, value in kwargs.items():
if key[:3] == 'is_' and key[3:] in TIFF.FILE_FLAGS:
if value is not None and not value:
setattr(self, key, bool(value))
else:
raise TypeError('unexpected keyword argument: %s' % key)
fh = FileHandle(arg, mode='rb', name=name, offset=offset, size=size)
self._fh = fh
self._multifile = bool(multifile)
self._files = {fh.name: self} # cache of TiffFiles
try:
fh.seek(0)
try:
byteorder = {b'II': '<', b'MM': '>'}[fh.read(2)]
except KeyError:
raise ValueError('not a TIFF file')
sys_byteorder = {'big': '>', 'little': '<'}[sys.byteorder]
self.isnative = byteorder == sys_byteorder
version = struct.unpack(byteorder+'H', fh.read(2))[0]
if version == 43:
# BigTiff
self.is_bigtiff = True
offsetsize, zero = struct.unpack(byteorder+'HH', fh.read(4))
if zero or offsetsize != 8:
raise ValueError('invalid BigTIFF file')
self.byteorder = byteorder
self.offsetsize = 8
self.offsetformat = byteorder+'Q'
self.tagnosize = 8
self.tagnoformat = byteorder+'Q'
self.tagsize = 20
self.tagformat1 = byteorder+'HH'
self.tagformat2 = byteorder+'Q8s'
elif version == 42:
self.is_bigtiff = False
self.byteorder = byteorder
self.offsetsize = 4
self.offsetformat = byteorder+'I'
self.tagnosize = 2
self.tagnoformat = byteorder+'H'
self.tagsize = 12
self.tagformat1 = byteorder+'HH'
self.tagformat2 = byteorder+'I4s'
else:
raise ValueError('invalid TIFF file')
# file handle is at offset to offset to first page
self.pages = TiffPages(self)
if self.is_lsm and (self.filehandle.size >= 2**32 or
self.pages[0].compression != 1 or
self.pages[1].compression != 1):
self._lsm_load_pages()
self._lsm_fix_strip_offsets()
self._lsm_fix_strip_bytecounts()
elif movie:
self.pages.useframes = True
except Exception:
fh.close()
raise
@property
def filehandle(self):
"""Return file handle."""
return self._fh
@property
def filename(self):
"""Return name of file handle."""
return self._fh.name
@lazyattr
def fstat(self):
"""Return status of file handle as stat_result object."""
try:
return os.fstat(self._fh.fileno())
except Exception: # io.UnsupportedOperation
return None
def close(self):
"""Close open file handle(s)."""
for tif in self._files.values():
tif.filehandle.close()
self._files = {}
def asarray(self, key=None, series=None, out=None, validate=True,
maxworkers=1):
"""Return image data from multiple TIFF pages as numpy array.
By default, the data from the first series is returned.
Parameters
----------
key : int, slice, or sequence of page indices
Defines which pages to return as array.
series : int or TiffPageSeries
Defines which series of pages to return as array.
out : numpy.ndarray, str, or file-like object; optional
Buffer where image data will be saved.
If None (default), a new array will be created.
If numpy.ndarray, a writable array of compatible dtype and shape.
If 'memmap', directly memory-map the image data in the TIFF file
if possible; else create a memory-mapped array in a temporary file.
If str or open file, the file name or file object used to
create a memory-map to an array stored in a binary file on disk.
validate : bool
If True (default), validate various tags.
Passed to TiffPage.asarray().
maxworkers : int
Maximum number of threads to concurrently get data from pages.
Default is 1. If None, up to half the CPU cores are used.
Reading data from file is limited to a single thread.
Using multiple threads can significantly speed up this function
if the bottleneck is decoding compressed data, e.g. in case of
large LZW compressed LSM files.
If the bottleneck is I/O or pure Python code, using multiple
threads might be detrimental.
"""
if not self.pages:
return numpy.array([])
if key is None and series is None:
series = 0
if series is not None:
try:
series = self.series[series]
except (KeyError, TypeError):
pass
pages = series._pages
else:
pages = self.pages
if key is None:
pass
elif isinstance(key, inttypes):
pages = [pages[key]]
elif isinstance(key, slice):
pages = pages[key]
elif isinstance(key, collections.Iterable):
pages = [pages[k] for k in key]
else:
raise TypeError('key must be an int, slice, or sequence')
if not pages:
raise ValueError('no pages selected')
if self.is_nih:
result = stack_pages(pages, out=out, maxworkers=maxworkers,
squeeze=False)
elif key is None and series and series.offset:
typecode = self.byteorder + series.dtype.char
if out == 'memmap' and pages[0].is_memmappable:
result = self.filehandle.memmap_array(
typecode, series.shape, series.offset)
else:
if out is not None:
out = create_output(out, series.shape, series.dtype)
self.filehandle.seek(series.offset)
result = self.filehandle.read_array(
typecode, product(series.shape), out=out, native=True)
elif len(pages) == 1:
result = pages[0].asarray(out=out, validate=validate)
else:
result = stack_pages(pages, out=out, maxworkers=maxworkers)
if result is None:
return
if key is None:
try:
result.shape = series.shape
except ValueError:
try:
warnings.warn('failed to reshape %s to %s' % (
result.shape, series.shape))
# try series of expected shapes
result.shape = (-1,) + series.shape
except ValueError:
# revert to generic shape
result.shape = (-1,) + pages[0].shape
elif len(pages) == 1:
result.shape = pages[0].shape
else:
result.shape = (-1,) + pages[0].shape
return result
@lazyattr
def series(self):
"""Return related pages as TiffPageSeries.
Side effect: after calling this function, TiffFile.pages might contain
TiffPage and TiffFrame instances.
"""
if not self.pages:
return []
useframes = self.pages.useframes
keyframe = self.pages.keyframe
series = []
for name in 'ome imagej lsm fluoview nih mdgel shaped'.split():
if getattr(self, 'is_' + name, False):
series = getattr(self, '_%s_series' % name)()
break
self.pages.useframes = useframes
self.pages.keyframe = keyframe
if not series:
series = self._generic_series()
# remove empty series, e.g. in MD Gel files
series = [s for s in series if sum(s.shape) > 0]
for i, s in enumerate(series):
s.index = i
return series
def _generic_series(self):
"""Return image series in file."""
if self.pages.useframes:
# movie mode
page = self.pages[0]
shape = page.shape
axes = page.axes
if len(self.pages) > 1:
shape = (len(self.pages),) + shape
axes = 'I' + axes
return [TiffPageSeries(self.pages[:], shape, page.dtype, axes,
stype='movie')]
self.pages.clear(False)
self.pages.load()
result = []
keys = []
series = {}
compressions = TIFF.DECOMPESSORS
for page in self.pages:
if not page.shape:
continue
key = page.shape + (page.axes, page.compression in compressions)
if key in series:
series[key].append(page)
else:
keys.append(key)
series[key] = [page]
for key in keys:
pages = series[key]
page = pages[0]
shape = page.shape
axes = page.axes
if len(pages) > 1:
shape = (len(pages),) + shape
axes = 'I' + axes
result.append(TiffPageSeries(pages, shape, page.dtype, axes,
stype='Generic'))
return result
def _shaped_series(self):
"""Return image series in "shaped" file."""
pages = self.pages
pages.useframes = True
lenpages = len(pages)
def append_series(series, pages, axes, shape, reshape, name,
truncated):
page = pages[0]
if not axes:
shape = page.shape
axes = page.axes
if len(pages) > 1:
shape = (len(pages),) + shape
axes = 'Q' + axes
size = product(shape)
resize = product(reshape)
if page.is_contiguous and resize > size and resize % size == 0:
if truncated is None:
truncated = True
axes = 'Q' + axes
shape = (resize // size,) + shape
try:
axes = reshape_axes(axes, shape, reshape)
shape = reshape
except ValueError as e:
warnings.warn(str(e))
series.append(
TiffPageSeries(pages, shape, page.dtype, axes, name=name,
stype='Shaped', truncated=truncated))
keyframe = axes = shape = reshape = name = None
series = []
index = 0
while True:
if index >= lenpages:
break
# new keyframe; start of new series
pages.keyframe = index
keyframe = pages[index]
if not keyframe.is_shaped:
warnings.warn('invalid shape metadata or corrupted file')
return
# read metadata
axes = None
shape = None
metadata = json_description_metadata(keyframe.is_shaped)
name = metadata.get('name', '')
reshape = metadata['shape']
truncated = metadata.get('truncated', None)
if 'axes' in metadata:
axes = metadata['axes']
if len(axes) == len(reshape):
shape = reshape
else:
axes = ''
warnings.warn('axes do not match shape')
# skip pages if possible
spages = [keyframe]
size = product(reshape)
npages, mod = divmod(size, product(keyframe.shape))
if mod:
warnings.warn('series shape does not match page shape')
return
if 1 < npages <= lenpages - index:
size *= keyframe._dtype.itemsize
if truncated:
npages = 1
elif (keyframe.is_final and
keyframe.offset + size < pages[index+1].offset):
truncated = False
else:
# need to read all pages for series
truncated = False
for j in range(index+1, index+npages):
page = pages[j]
page.keyframe = keyframe
spages.append(page)
append_series(series, spages, axes, shape, reshape, name,
truncated)
index += npages
return series
def _imagej_series(self):
"""Return image series in ImageJ file."""
# ImageJ's dimension order is always TZCYXS
# TODO: fix loading of color, composite, or palette images
self.pages.useframes = True
self.pages.keyframe = 0
ij = self.imagej_metadata
pages = self.pages
page = pages[0]
def is_hyperstack():
# ImageJ hyperstack store all image metadata in the first page and
# image data are stored contiguously before the second page, if any
if not page.is_final:
return False
images = ij.get('images', 0)
if images <= 1:
return False
offset, count = page.is_contiguous
if (count != product(page.shape) * page.bitspersample // 8
or offset + count*images > self.filehandle.size):
raise ValueError()
# check that next page is stored after data
if len(pages) > 1 and offset + count*images > pages[1].offset:
return False
return True
try:
hyperstack = is_hyperstack()
except ValueError:
warnings.warn('invalid ImageJ metadata or corrupted file')
return
if hyperstack:
# no need to read other pages
pages = [page]
else:
self.pages.load()
shape = []
axes = []
if 'frames' in ij:
shape.append(ij['frames'])
axes.append('T')
if 'slices' in ij:
shape.append(ij['slices'])
axes.append('Z')
if 'channels' in ij and not (page.photometric == 2 and not
ij.get('hyperstack', False)):
shape.append(ij['channels'])
axes.append('C')
remain = ij.get('images', len(pages))//(product(shape) if shape else 1)
if remain > 1:
shape.append(remain)
axes.append('I')
if page.axes[0] == 'I':
# contiguous multiple images
shape.extend(page.shape[1:])
axes.extend(page.axes[1:])
elif page.axes[:2] == 'SI':
# color-mapped contiguous multiple images
shape = page.shape[0:1] + tuple(shape) + page.shape[2:]
axes = list(page.axes[0]) + axes + list(page.axes[2:])
else:
shape.extend(page.shape)
axes.extend(page.axes)
truncated = (
hyperstack and len(self.pages) == 1 and
page.is_contiguous[1] != product(shape) * page.bitspersample // 8)
return [TiffPageSeries(pages, shape, page.dtype, axes, stype='ImageJ',
truncated=truncated)]
def _fluoview_series(self):
"""Return image series in FluoView file."""
self.pages.useframes = True
self.pages.keyframe = 0
self.pages.load()
mm = self.fluoview_metadata
mmhd = list(reversed(mm['Dimensions']))
axes = ''.join(TIFF.MM_DIMENSIONS.get(i[0].upper(), 'Q')
for i in mmhd if i[1] > 1)
shape = tuple(int(i[1]) for i in mmhd if i[1] > 1)
return [TiffPageSeries(self.pages, shape, self.pages[0].dtype, axes,
name=mm['ImageName'], stype='FluoView')]
def _mdgel_series(self):
"""Return image series in MD Gel file."""
# only a single page, scaled according to metadata in second page
self.pages.useframes = False
self.pages.keyframe = 0
self.pages.load()
md = self.mdgel_metadata
if md['FileTag'] in (2, 128):
dtype = numpy.dtype('float32')
scale = md['ScalePixel']
scale = scale[0] / scale[1] # rational
if md['FileTag'] == 2:
# squary root data format
def transform(a):
return a.astype('float32')**2 * scale
else:
def transform(a):
return a.astype('float32') * scale
else:
transform = None
page = self.pages[0]
return [TiffPageSeries([page], page.shape, dtype, page.axes,
transform=transform, stype='MDGel')]
def _nih_series(self):
"""Return image series in NIH file."""
self.pages.useframes = True
self.pages.keyframe = 0
self.pages.load()
page0 = self.pages[0]
if len(self.pages) == 1:
shape = page0.shape
axes = page0.axes
else:
shape = (len(self.pages),) + page0.shape
axes = 'I' + page0.axes
return [
TiffPageSeries(self.pages, shape, page0.dtype, axes, stype='NIH')]
def _ome_series(self):
"""Return image series in OME-TIFF file(s)."""
from xml.etree import cElementTree as etree # delayed import
omexml = self.pages[0].description
try:
root = etree.fromstring(omexml)
except etree.ParseError as e:
# TODO: test badly encoded OME-XML
warnings.warn('ome-xml: %s' % e)
try:
# might work on Python 2
omexml = omexml.decode('utf-8', 'ignore').encode('utf-8')
root = etree.fromstring(omexml)
except Exception:
return
self.pages.useframes = True
self.pages.keyframe = 0
self.pages.load()
uuid = root.attrib.get('UUID', None)
self._files = {uuid: self}
dirname = self._fh.dirname
modulo = {}
series = []
for element in root:
if element.tag.endswith('BinaryOnly'):
# TODO: load OME-XML from master or companion file
warnings.warn('ome-xml: not an ome-tiff master file')
break
if element.tag.endswith('StructuredAnnotations'):
for annot in element:
if not annot.attrib.get('Namespace',
'').endswith('modulo'):
continue
for value in annot:
for modul in value:
for along in modul:
if not along.tag[:-1].endswith('Along'):
continue
axis = along.tag[-1]
newaxis = along.attrib.get('Type', 'other')
newaxis = TIFF.AXES_LABELS[newaxis]
if 'Start' in along.attrib:
step = float(along.attrib.get('Step', 1))
start = float(along.attrib['Start'])
stop = float(along.attrib['End']) + step
labels = numpy.arange(start, stop, step)
else:
labels = [label.text for label in along
if label.tag.endswith('Label')]
modulo[axis] = (newaxis, labels)
if not element.tag.endswith('Image'):
continue
attr = element.attrib
name = attr.get('Name', None)
for pixels in element:
if not pixels.tag.endswith('Pixels'):
continue
attr = pixels.attrib
dtype = attr.get('PixelType', None)
axes = ''.join(reversed(attr['DimensionOrder']))
shape = list(int(attr['Size'+ax]) for ax in axes)
size = product(shape[:-2])
ifds = None
spp = 1 # samples per pixel
# FIXME: this implementation assumes the last two
# dimensions are stored in tiff pages (shape[:-2]).
# Apparently that is not always the case.
for data in pixels:
if data.tag.endswith('Channel'):
attr = data.attrib
if ifds is None:
spp = int(attr.get('SamplesPerPixel', spp))
ifds = [None] * (size // spp)
elif int(attr.get('SamplesPerPixel', 1)) != spp:
raise ValueError(
"cannot handle differing SamplesPerPixel")
continue
if ifds is None:
ifds = [None] * (size // spp)
if not data.tag.endswith('TiffData'):
continue
attr = data.attrib
ifd = int(attr.get('IFD', 0))
num = int(attr.get('NumPlanes', 1 if 'IFD' in attr else 0))
num = int(attr.get('PlaneCount', num))
idx = [int(attr.get('First'+ax, 0)) for ax in axes[:-2]]
try:
idx = numpy.ravel_multi_index(idx, shape[:-2])
except ValueError:
# ImageJ produces invalid ome-xml when cropping
warnings.warn('ome-xml: invalid TiffData index')
continue
for uuid in data:
if not uuid.tag.endswith('UUID'):
continue
if uuid.text not in self._files:
if not self._multifile:
# abort reading multifile OME series
# and fall back to generic series
return []
fname = uuid.attrib['FileName']
try:
tif = TiffFile(os.path.join(dirname, fname))
tif.pages.useframes = True
tif.pages.keyframe = 0
tif.pages.load()
except (IOError, FileNotFoundError, ValueError):
warnings.warn(
"ome-xml: failed to read '%s'" % fname)
break
self._files[uuid.text] = tif
tif.close()
pages = self._files[uuid.text].pages
try:
for i in range(num if num else len(pages)):
ifds[idx + i] = pages[ifd + i]
except IndexError:
warnings.warn('ome-xml: index out of range')
# only process first UUID
break
else:
pages = self.pages
try:
for i in range(num if num else len(pages)):
ifds[idx + i] = pages[ifd + i]
except IndexError:
warnings.warn('ome-xml: index out of range')
if all(i is None for i in ifds):
# skip images without data
continue
# set a keyframe on all IFDs
keyframe = None
for i in ifds:
# try find a TiffPage
if i and i == i.keyframe:
keyframe = i
break
if not keyframe:
# reload a TiffPage from file
for i, keyframe in enumerate(ifds):
if keyframe:
keyframe.parent.pages.keyframe = keyframe.index
keyframe = keyframe.parent.pages[keyframe.index]
ifds[i] = keyframe
break
for i in ifds:
if i is not None:
i.keyframe = keyframe
dtype = keyframe.dtype
series.append(
TiffPageSeries(ifds, shape, dtype, axes, parent=self,
name=name, stype='OME'))
for serie in series:
shape = list(serie.shape)
for axis, (newaxis, labels) in modulo.items():
i = serie.axes.index(axis)
size = len(labels)
if shape[i] == size:
serie.axes = serie.axes.replace(axis, newaxis, 1)
else:
shape[i] //= size
shape.insert(i+1, size)
serie.axes = serie.axes.replace(axis, axis+newaxis, 1)
serie.shape = tuple(shape)
# squeeze dimensions
for serie in series:
serie.shape, serie.axes = squeeze_axes(serie.shape, serie.axes)
return series
def _lsm_series(self):
"""Return main image series in LSM file. Skip thumbnails."""
lsmi = self.lsm_metadata
axes = TIFF.CZ_LSMINFO_SCANTYPE[lsmi['ScanType']]
if self.pages[0].photometric == 2: # RGB; more than one channel
axes = axes.replace('C', '').replace('XY', 'XYC')
if lsmi.get('DimensionP', 0) > 1:
axes += 'P'
if lsmi.get('DimensionM', 0) > 1:
axes += 'M'
axes = axes[::-1]
shape = tuple(int(lsmi[TIFF.CZ_LSMINFO_DIMENSIONS[i]]) for i in axes)
name = lsmi.get('Name', '')
self.pages.keyframe = 0
pages = self.pages[::2]
dtype = pages[0].dtype
series = [TiffPageSeries(pages, shape, dtype, axes, name=name,
stype='LSM')]
if self.pages[1].is_reduced:
self.pages.keyframe = 1
pages = self.pages[1::2]
dtype = pages[0].dtype
cp, i = 1, 0
while cp < len(pages) and i < len(shape)-2:
cp *= shape[i]
i += 1
shape = shape[:i] + pages[0].shape
axes = axes[:i] + 'CYX'
series.append(TiffPageSeries(pages, shape, dtype, axes, name=name,
stype='LSMreduced'))
return series
def _lsm_load_pages(self):
"""Load all pages from LSM file."""
self.pages.cache = True
self.pages.useframes = True
# second series: thumbnails
self.pages.keyframe = 1
keyframe = self.pages[1]
for page in self.pages[1::2]:
page.keyframe = keyframe
# first series: data
self.pages.keyframe = 0
keyframe = self.pages[0]
for page in self.pages[::2]:
page.keyframe = keyframe
def _lsm_fix_strip_offsets(self):
"""Unwrap strip offsets for LSM files greater than 4 GB.
Each series and position require separate unwrapping (undocumented).
"""
if self.filehandle.size < 2**32:
return
pages = self.pages
npages = len(pages)
series = self.series[0]
axes = series.axes
# find positions
positions = 1
for i in 0, 1:
if series.axes[i] in 'PM':
positions *= series.shape[i]
# make time axis first
if positions > 1:
ntimes = 0
for i in 1, 2:
if axes[i] == 'T':
ntimes = series.shape[i]
break
if ntimes:
div, mod = divmod(npages, 2*positions*ntimes)
assert mod == 0
shape = (positions, ntimes, div, 2)
indices = numpy.arange(product(shape)).reshape(shape)
indices = numpy.moveaxis(indices, 1, 0)
else:
indices = numpy.arange(npages).reshape(-1, 2)
# images of reduced page might be stored first
if pages[0].dataoffsets[0] > pages[1].dataoffsets[0]:
indices = indices[..., ::-1]
# unwrap offsets
wrap = 0
previousoffset = 0
for i in indices.flat:
page = pages[i]
dataoffsets = []
for currentoffset in page.dataoffsets:
if currentoffset < previousoffset:
wrap += 2**32
dataoffsets.append(currentoffset + wrap)
previousoffset = currentoffset
page.dataoffsets = tuple(dataoffsets)
def _lsm_fix_strip_bytecounts(self):
"""Set databytecounts to size of compressed data.
The StripByteCounts tag in LSM files contains the number of bytes
for the uncompressed data.
"""
pages = self.pages
if pages[0].compression == 1:
return
# sort pages by first strip offset
pages = sorted(pages, key=lambda p: p.dataoffsets[0])
npages = len(pages) - 1
for i, page in enumerate(pages):
if page.index % 2:
continue
offsets = page.dataoffsets
bytecounts = page.databytecounts
if i < npages:
lastoffset = pages[i+1].dataoffsets[0]
else:
# LZW compressed strips might be longer than uncompressed
lastoffset = min(offsets[-1] + 2*bytecounts[-1], self._fh.size)
offsets = offsets + (lastoffset,)
page.databytecounts = tuple(offsets[j+1] - offsets[j]
for j in range(len(bytecounts)))
def __getattr__(self, name):
"""Return 'is_flag' attributes from first page."""
if name[3:] in TIFF.FILE_FLAGS:
if not self.pages:
return False
value = bool(getattr(self.pages[0], name))
setattr(self, name, value)
return value
raise AttributeError("'%s' object has no attribute '%s'" %
(self.__class__.__name__, name))
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def __str__(self, detail=0, width=79):
"""Return string containing information about file.
The detail parameter specifies the level of detail returned:
0: file only.
1: all series, first page of series and its tags.
2: large tag values and file metadata.
3: all pages.
"""
info = [
"TiffFile '%s'",
format_size(self._fh.size),
{'<': 'LittleEndian', '>': 'BigEndian'}[self.byteorder]]
if self.is_bigtiff:
info.append('BigTiff')
info.append('|'.join(f.upper() for f in self.flags))
if len(self.pages) > 1:
info.append('%i Pages' % len(self.pages))
if len(self.series) > 1:
info.append('%i Series' % len(self.series))
if len(self._files) > 1:
info.append('%i Files' % (len(self._files)))
info = ' '.join(info)
info = info.replace(' ', ' ').replace(' ', ' ')
info = info % snipstr(self._fh.name, max(12, width+2-len(info)))
if detail <= 0:
return info
info = [info]
info.append('\n'.join(str(s) for s in self.series))
if detail >= 3:
info.extend((TiffPage.__str__(p, detail=detail, width=width)
for p in self.pages
if p is not None))
else:
info.extend((TiffPage.__str__(s.pages[0], detail=detail,
width=width)
for s in self.series
if s.pages[0] is not None))
if detail >= 2:
for name in sorted(self.flags):
if hasattr(self, name + '_metadata'):
m = getattr(self, name + '_metadata')
if m:
info.append(
'%s_METADATA\n%s' % (name.upper(),
pformat(m, width=width,
height=detail*12)))
return '\n\n'.join(info).replace('\n\n\n', '\n\n')
@lazyattr
def flags(self):
"""Return set of file flags."""
return set(name.lower() for name in sorted(TIFF.FILE_FLAGS)
if getattr(self, 'is_' + name))
@lazyattr
def is_mdgel(self):
"""File has MD Gel format."""
try:
return self.pages[0].is_mdgel or self.pages[1].is_mdgel
except IndexError:
return False
@property
def is_movie(self):
"""Return if file is a movie."""
return self.pages.useframes
@lazyattr
def shaped_metadata(self):
"""Return Tifffile metadata from JSON descriptions as dicts."""
if not self.is_shaped:
return
return tuple(json_description_metadata(s.pages[0].is_shaped)
for s in self.series if s.stype.lower() == 'shaped')
@lazyattr
def ome_metadata(self):
"""Return OME XML as dict."""
# TODO: remove this or return XML?
if not self.is_ome:
return
return xml2dict(self.pages[0].description)['OME']
@lazyattr
def qptiff_metadata(self):
"""Return PerkinElmer-QPI-ImageDescription XML element as dict."""
if not self.is_qptiff:
return
root = 'PerkinElmer-QPI-ImageDescription'
xml = self.pages[0].description.replace(' ' + root + ' ', root)
return xml2dict(xml)[root]
@lazyattr
def lsm_metadata(self):
"""Return LSM metadata from CZ_LSMINFO tag as dict."""
if not self.is_lsm:
return
return self.pages[0].tags['CZ_LSMINFO'].value
@lazyattr
def stk_metadata(self):
"""Return STK metadata from UIC tags as dict."""
if not self.is_stk:
return
page = self.pages[0]
tags = page.tags
result = {}
result['NumberPlanes'] = tags['UIC2tag'].count
if page.description:
result['PlaneDescriptions'] = page.description.split('\0')
# result['plane_descriptions'] = stk_description_metadata(
# page.image_description)
if 'UIC1tag' in tags:
result.update(tags['UIC1tag'].value)
if 'UIC3tag' in tags:
result.update(tags['UIC3tag'].value) # wavelengths
if 'UIC4tag' in tags:
result.update(tags['UIC4tag'].value) # override uic1 tags
uic2tag = tags['UIC2tag'].value
result['ZDistance'] = uic2tag['ZDistance']
result['TimeCreated'] = uic2tag['TimeCreated']
result['TimeModified'] = uic2tag['TimeModified']
try:
result['DatetimeCreated'] = numpy.array(
[julian_datetime(*dt) for dt in
zip(uic2tag['DateCreated'], uic2tag['TimeCreated'])],
dtype='datetime64[ns]')
result['DatetimeModified'] = numpy.array(
[julian_datetime(*dt) for dt in
zip(uic2tag['DateModified'], uic2tag['TimeModified'])],
dtype='datetime64[ns]')
except ValueError as e:
warnings.warn('stk_metadata: %s' % e)
return result
@lazyattr
def imagej_metadata(self):
"""Return consolidated ImageJ metadata as dict."""
if not self.is_imagej:
return
page = self.pages[0]
result = imagej_description_metadata(page.is_imagej)
if 'IJMetadata' in page.tags:
try:
result.update(page.tags['IJMetadata'].value)
except Exception:
pass
return result
@lazyattr
def fluoview_metadata(self):
"""Return consolidated FluoView metadata as dict."""
if not self.is_fluoview:
return
result = {}
page = self.pages[0]
result.update(page.tags['MM_Header'].value)
# TODO: read stamps from all pages
result['Stamp'] = page.tags['MM_Stamp'].value
# skip parsing image description; not reliable
# try:
# t = fluoview_description_metadata(page.image_description)
# if t is not None:
# result['ImageDescription'] = t
# except Exception as e:
# warnings.warn(
# "failed to read FluoView image description: %s" % e)
return result
@lazyattr
def nih_metadata(self):
"""Return NIH Image metadata from NIHImageHeader tag as dict."""
if not self.is_nih:
return
return self.pages[0].tags['NIHImageHeader'].value
@lazyattr
def fei_metadata(self):
"""Return FEI metadata from SFEG or HELIOS tags as dict."""
if not self.is_fei:
return
tags = self.pages[0].tags
if 'FEI_SFEG' in tags:
return tags['FEI_SFEG'].value
if 'FEI_HELIOS' in tags:
return tags['FEI_HELIOS'].value
@lazyattr
def sem_metadata(self):
"""Return SEM metadata from CZ_SEM tag as dict."""
if not self.is_sem:
return
return self.pages[0].tags['CZ_SEM'].value
@lazyattr
def mdgel_metadata(self):
"""Return consolidated metadata from MD GEL tags as dict."""
for page in self.pages[:2]:
if 'MDFileTag' in page.tags:
tags = page.tags
break
else:
return
result = {}
for code in range(33445, 33453):
name = TIFF.TAGS[code]
if name not in tags:
continue
result[name[2:]] = tags[name].value
return result
@lazyattr
def andor_metadata(self):
"""Return Andor tags as dict."""
return self.pages[0].andor_tags
@lazyattr
def epics_metadata(self):
"""Return EPICS areaDetector tags as dict."""
return self.pages[0].epics_tags
@lazyattr
def tvips_metadata(self):
"""Return TVIPS tag as dict."""
if not self.is_tvips:
return
return self.pages[0].tags['TVIPS'].value
@lazyattr
def metaseries_metadata(self):
"""Return MetaSeries metadata from image description as dict."""
if not self.is_metaseries:
return
return metaseries_description_metadata(self.pages[0].description)
@lazyattr
def pilatus_metadata(self):
"""Return Pilatus metadata from image description as dict."""
if not self.is_pilatus:
return
return pilatus_description_metadata(self.pages[0].description)
@lazyattr
def micromanager_metadata(self):
"""Return consolidated MicroManager metadata as dict."""
if not self.is_micromanager:
return
# from file header
result = read_micromanager_metadata(self._fh)
# from tag
result.update(self.pages[0].tags['MicroManagerMetadata'].value)
return result
@lazyattr
def scanimage_metadata(self):
"""Return ScanImage non-varying frame and ROI metadata as dict."""
if not self.is_scanimage:
return
result = {}
try:
framedata, roidata = read_scanimage_metadata(self._fh)
result['FrameData'] = framedata
result.update(roidata)
except ValueError:
pass
# TODO: scanimage_artist_metadata
try:
result['Description'] = scanimage_description_metadata(
self.pages[0].description)
except Exception as e:
warnings.warn('scanimage_description_metadata failed: %s' % e)
return result
@property
def geotiff_metadata(self):
"""Return GeoTIFF metadata from first page as dict."""
if not self.is_geotiff:
return
return self.pages[0].geotiff_tags
class TiffPages(object):
"""Sequence of TIFF image file directories."""
def __init__(self, parent):
"""Initialize instance from file. Read first TiffPage from file.
The file position must be at an offset to an offset to a TiffPage.
"""
self.parent = parent
self.pages = [] # cache of TiffPages, TiffFrames, or their offsets
self.complete = False # True if offsets to all pages were read
self._tiffpage = TiffPage # class for reading tiff pages
self._keyframe = None
self._cache = True
# read offset to first page
fh = parent.filehandle
self._nextpageoffset = fh.tell()
offset = struct.unpack(parent.offsetformat,
fh.read(parent.offsetsize))[0]
if offset == 0:
# warnings.warn('file contains no pages')
self.complete = True
return
if offset >= fh.size:
warnings.warn('invalid page offset (%i)' % offset)
self.complete = True
return
# always read and cache first page
fh.seek(offset)
page = TiffPage(parent, index=0)
self.pages.append(page)
self._keyframe = page
@property
def cache(self):
"""Return if pages/frames are currenly being cached."""
return self._cache
@cache.setter
def cache(self, value):
"""Enable or disable caching of pages/frames. Clear cache if False."""
value = bool(value)
if self._cache and not value:
self.clear()
self._cache = value
@property
def useframes(self):
"""Return if currently using TiffFrame (True) or TiffPage (False)."""
return self._tiffpage == TiffFrame and TiffFrame is not TiffPage
@useframes.setter
def useframes(self, value):
"""Set to use TiffFrame (True) or TiffPage (False)."""
self._tiffpage = TiffFrame if value else TiffPage
@property
def keyframe(self):
"""Return index of current keyframe."""
return self._keyframe.index
@keyframe.setter
def keyframe(self, index):
"""Set current keyframe. Load TiffPage from file if necessary."""
if self._keyframe.index == index:
return
if self.complete or 0 <= index < len(self.pages):
page = self.pages[index]
if isinstance(page, TiffPage):
self._keyframe = page
return
elif isinstance(page, TiffFrame):
# remove existing frame
self.pages[index] = page.offset
# load TiffPage from file
useframes = self.useframes
self._tiffpage = TiffPage
self._keyframe = self[index]
self.useframes = useframes
@property
def next_page_offset(self):
"""Return offset where offset to a new page can be stored."""
if not self.complete:
self._seek(-1)
return self._nextpageoffset
def load(self):
"""Read all remaining pages from file."""
fh = self.parent.filehandle
keyframe = self._keyframe
pages = self.pages
if not self.complete:
self._seek(-1)
for i, page in enumerate(pages):
if isinstance(page, inttypes):
fh.seek(page)
page = self._tiffpage(self.parent, index=i, keyframe=keyframe)
pages[i] = page
def clear(self, fully=True):
"""Delete all but first page from cache. Set keyframe to first page."""
pages = self.pages
if not self._cache or len(pages) < 1:
return
self._keyframe = pages[0]
if fully:
# delete all but first TiffPage/TiffFrame
for i, page in enumerate(pages[1:]):
if not isinstance(page, inttypes):
pages[i+1] = page.offset
elif TiffFrame is not TiffPage:
# delete only TiffFrames
for i, page in enumerate(pages):
if isinstance(page, TiffFrame):
pages[i] = page.offset
def _seek(self, index, maxpages=2**22):
"""Seek file to offset of specified page."""
pages = self.pages
if not pages:
return
fh = self.parent.filehandle
if fh.closed:
raise RuntimeError('FileHandle is closed')
if self.complete or 0 <= index < len(pages):
page = pages[index]
offset = page if isinstance(page, inttypes) else page.offset
fh.seek(offset)
return
offsetformat = self.parent.offsetformat
offsetsize = self.parent.offsetsize
tagnoformat = self.parent.tagnoformat
tagnosize = self.parent.tagnosize
tagsize = self.parent.tagsize
unpack = struct.unpack
page = pages[-1]
offset = page if isinstance(page, inttypes) else page.offset
while len(pages) < maxpages:
# read offsets to pages from file until index is reached
fh.seek(offset)
# skip tags
try:
tagno = unpack(tagnoformat, fh.read(tagnosize))[0]
if tagno > 4096:
raise ValueError('suspicious number of tags')
except Exception:
warnings.warn('corrupted tag list at offset %i' % offset)
del pages[-1]
self.complete = True
break
self._nextpageoffset = offset + tagnosize + tagno * tagsize
fh.seek(self._nextpageoffset)
# read offset to next page
offset = unpack(offsetformat, fh.read(offsetsize))[0]
if offset == 0:
self.complete = True
break
if offset >= fh.size:
warnings.warn('invalid page offset (%i)' % offset)
self.complete = True
break
pages.append(offset)
if 0 <= index < len(pages):
break
if index >= len(pages):
raise IndexError('list index out of range')
page = pages[index]
fh.seek(page if isinstance(page, inttypes) else page.offset)
def __bool__(self):
"""Return True if file contains any pages."""
return len(self.pages) > 0
def __len__(self):
"""Return number of pages in file."""
if not self.complete:
self._seek(-1)
return len(self.pages)
def __getitem__(self, key):
"""Return specified page(s) from cache or file."""
pages = self.pages
if not pages:
raise IndexError('list index out of range')
if key == 0:
return pages[key]
if isinstance(key, slice):
start, stop, _ = key.indices(2**31-1)
if not self.complete and max(stop, start) > len(pages):
self._seek(-1)
return [self[i] for i in range(*key.indices(len(pages)))]
if self.complete and key >= len(pages):
raise IndexError('list index out of range')
try:
page = pages[key]
except IndexError:
page = 0
if not isinstance(page, inttypes):
return page
self._seek(key)
page = self._tiffpage(self.parent, index=key, keyframe=self._keyframe)
if self._cache:
pages[key] = page
return page
def __iter__(self):
"""Return iterator over all pages."""
i = 0
while True:
try:
yield self[i]
i += 1
except IndexError:
break
class TiffPage(object):
"""TIFF image file directory (IFD).
Attributes
----------
index : int
Index of page in file.
dtype : numpy.dtype or None
Data type (native byte order) of the image in IFD.
shape : tuple
Dimensions of the image in IFD.
axes : str
Axes label codes:
'X' width, 'Y' height, 'S' sample, 'I' image series|page|plane,
'Z' depth, 'C' color|em-wavelength|channel, 'E' ex-wavelength|lambda,
'T' time, 'R' region|tile, 'A' angle, 'P' phase, 'H' lifetime,
'L' exposure, 'V' event, 'Q' unknown, '_' missing
tags : dict
Dictionary of tags in IFD. {tag.name: TiffTag}
colormap : numpy.ndarray
Color look up table, if exists.
All attributes are read-only.
Notes
-----
The internal, normalized '_shape' attribute is 6 dimensional:
0 : number planes/images (stk, ij).
1 : planar samplesperpixel.
2 : imagedepth Z (sgi).
3 : imagelength Y.
4 : imagewidth X.
5 : contig samplesperpixel.
"""
# default properties; will be updated from tags
imagewidth = 0
imagelength = 0
imagedepth = 1
tilewidth = 0
tilelength = 0
tiledepth = 1
bitspersample = 1
samplesperpixel = 1
sampleformat = 1
rowsperstrip = 2**32-1
compression = 1
planarconfig = 1
fillorder = 1
photometric = 0
predictor = 1
extrasamples = 1
colormap = None
software = ''
description = ''
description1 = ''
def __init__(self, parent, index, keyframe=None):
"""Initialize instance from file.
The file handle position must be at offset to a valid IFD.
"""
self.parent = parent
self.index = index
self.shape = ()
self._shape = ()
self.dtype = None
self._dtype = None
self.axes = ''
self.tags = {}
self.dataoffsets = ()
self.databytecounts = ()
# read TIFF IFD structure and its tags from file
fh = parent.filehandle
self.offset = fh.tell() # offset to this IFD
try:
tagno = struct.unpack(parent.tagnoformat,
fh.read(parent.tagnosize))[0]
if tagno > 4096:
raise ValueError('suspicious number of tags')
except Exception:
raise ValueError('corrupted tag list at offset %i' % self.offset)
tagsize = parent.tagsize
data = fh.read(tagsize * tagno)
tags = self.tags
index = -tagsize
for _ in range(tagno):
index += tagsize
try:
tag = TiffTag(self.parent, data[index:index+tagsize])
except TiffTag.Error as e:
warnings.warn(str(e))
continue
tagname = tag.name
if tagname not in tags:
name = tagname
tags[name] = tag
else:
# some files contain multiple tags with same code
# e.g. MicroManager files contain two ImageDescription tags
i = 1
while True:
name = '%s%i' % (tagname, i)
if name not in tags:
tags[name] = tag
break
name = TIFF.TAG_ATTRIBUTES.get(name, '')
if name:
if (name[:3] in 'sof des' and not isinstance(tag.value, str)):
pass # wrong string type for software, description
else:
setattr(self, name, tag.value)
if not tags:
return # found in FIBICS
# consolidate private tags; remove them from self.tags
if self.is_andor:
self.andor_tags
elif self.is_epics:
self.epics_tags
if self.is_lsm or (self.index and self.parent.is_lsm):
# correct non standard LSM bitspersample tags
self.tags['BitsPerSample']._fix_lsm_bitspersample(self)
if self.is_vista or (self.index and self.parent.is_vista):
# ISS Vista writes wrong ImageDepth tag
self.imagedepth = 1
if self.is_stk and 'UIC1tag' in tags and not tags['UIC1tag'].value:
# read UIC1tag now that plane count is known
uic1tag = tags['UIC1tag']
fh.seek(uic1tag.valueoffset)
tags['UIC1tag'].value = read_uic1tag(
fh, self.parent.byteorder, uic1tag.dtype,
uic1tag.count, None, tags['UIC2tag'].count)
if 'IJMetadata' in tags:
# decode IJMetadata tag
try:
tags['IJMetadata'].value = imagej_metadata(
tags['IJMetadata'].value,
tags['IJMetadataByteCounts'].value,
self.parent.byteorder)
except Exception as e:
warnings.warn(str(e))
if 'BitsPerSample' in tags:
tag = tags['BitsPerSample']
if tag.count == 1:
self.bitspersample = tag.value
else:
# LSM might list more items than samplesperpixel
value = tag.value[:self.samplesperpixel]
if any((v-value[0] for v in value)):
self.bitspersample = value
else:
self.bitspersample = value[0]
if 'SampleFormat' in tags:
tag = tags['SampleFormat']
if tag.count == 1:
self.sampleformat = tag.value
else:
value = tag.value[:self.samplesperpixel]
if any((v-value[0] for v in value)):
self.sampleformat = value
else:
self.sampleformat = value[0]
if 'ImageLength' in tags:
if 'RowsPerStrip' not in tags or tags['RowsPerStrip'].count > 1:
self.rowsperstrip = self.imagelength
# self.stripsperimage = int(math.floor(
# float(self.imagelength + self.rowsperstrip - 1) /
# self.rowsperstrip))
# determine dtype
dtype = self.sampleformat, self.bitspersample
dtype = TIFF.SAMPLE_DTYPES.get(dtype, None)
if dtype is not None:
dtype = numpy.dtype(dtype)
self.dtype = self._dtype = dtype
# determine shape of data
imagelength = self.imagelength
imagewidth = self.imagewidth
imagedepth = self.imagedepth
samplesperpixel = self.samplesperpixel
if self.is_stk:
assert self.imagedepth == 1
uictag = tags['UIC2tag'].value
planes = tags['UIC2tag'].count
if self.planarconfig == 1:
self._shape = (
planes, 1, 1, imagelength, imagewidth, samplesperpixel)
if samplesperpixel == 1:
self.shape = (planes, imagelength, imagewidth)
self.axes = 'YX'
else:
self.shape = (
planes, imagelength, imagewidth, samplesperpixel)
self.axes = 'YXS'
else:
self._shape = (
planes, samplesperpixel, 1, imagelength, imagewidth, 1)
if samplesperpixel == 1:
self.shape = (planes, imagelength, imagewidth)
self.axes = 'YX'
else:
self.shape = (
planes, samplesperpixel, imagelength, imagewidth)
self.axes = 'SYX'
# detect type of series
if planes == 1:
self.shape = self.shape[1:]
elif numpy.all(uictag['ZDistance'] != 0):
self.axes = 'Z' + self.axes
elif numpy.all(numpy.diff(uictag['TimeCreated']) != 0):
self.axes = 'T' + self.axes
else:
self.axes = 'I' + self.axes
elif self.photometric == 2 or samplesperpixel > 1: # PHOTOMETRIC.RGB
if self.planarconfig == 1:
self._shape = (
1, 1, imagedepth, imagelength, imagewidth, samplesperpixel)
if imagedepth == 1:
self.shape = (imagelength, imagewidth, samplesperpixel)
self.axes = 'YXS'
else:
self.shape = (
imagedepth, imagelength, imagewidth, samplesperpixel)
self.axes = 'ZYXS'
else:
self._shape = (1, samplesperpixel, imagedepth,
imagelength, imagewidth, 1)
if imagedepth == 1:
self.shape = (samplesperpixel, imagelength, imagewidth)
self.axes = 'SYX'
else:
self.shape = (
samplesperpixel, imagedepth, imagelength, imagewidth)
self.axes = 'SZYX'
else:
self._shape = (1, 1, imagedepth, imagelength, imagewidth, 1)
if imagedepth == 1:
self.shape = (imagelength, imagewidth)
self.axes = 'YX'
else:
self.shape = (imagedepth, imagelength, imagewidth)
self.axes = 'ZYX'
# dataoffsets and databytecounts
if 'TileOffsets' in tags:
self.dataoffsets = tags['TileOffsets'].value
elif 'StripOffsets' in tags:
self.dataoffsets = tags['StripOffsets'].value
else:
self.dataoffsets = (0,)
if 'TileByteCounts' in tags:
self.databytecounts = tags['TileByteCounts'].value
elif 'StripByteCounts' in tags:
self.databytecounts = tags['StripByteCounts'].value
else:
self.databytecounts = (
product(self.shape) * (self.bitspersample // 8),)
if self.compression != 1:
warnings.warn('required ByteCounts tag is missing')
assert len(self.shape) == len(self.axes)
def asarray(self, out=None, squeeze=True, lock=None, reopen=True,
maxsize=2**44, validate=True):
"""Read image data from file and return as numpy array.
Raise ValueError if format is unsupported.
Parameters
----------
out : numpy.ndarray, str, or file-like object; optional
Buffer where image data will be saved.
If None (default), a new array will be created.
If numpy.ndarray, a writable array of compatible dtype and shape.
If 'memmap', directly memory-map the image data in the TIFF file
if possible; else create a memory-mapped array in a temporary file.
If str or open file, the file name or file object used to
create a memory-map to an array stored in a binary file on disk.
squeeze : bool
If True, all length-1 dimensions (except X and Y) are
squeezed out from the array.
If False, the shape of the returned array might be different from
the page.shape.
lock : {RLock, NullContext}
A reentrant lock used to syncronize reads from file.
If None (default), the lock of the parent's filehandle is used.
reopen : bool
If True (default) and the parent file handle is closed, the file
is temporarily re-opened and closed if no exception occurs.
maxsize: int or None
Maximum size of data before a ValueError is raised.
Can be used to catch DOS. Default: 16 TB.
validate : bool
If True (default), validate various parameters.
If None, only validate parameters and return None.
"""
self_ = self
self = self.keyframe # self or keyframe
if not self._shape or product(self._shape) == 0:
return
tags = self.tags
if validate or validate is None:
if maxsize and product(self._shape) > maxsize:
raise ValueError('data are too large %s' % str(self._shape))
if self.dtype is None:
raise ValueError('data type not supported: %s%i' % (
self.sampleformat, self.bitspersample))
if self.compression not in TIFF.DECOMPESSORS:
raise ValueError(
'cannot decompress %s' % self.compression.name)
if 'SampleFormat' in tags:
tag = tags['SampleFormat']
if tag.count != 1 and any((i-tag.value[0] for i in tag.value)):
raise ValueError(
'sample formats do not match %s' % tag.value)
if self.is_chroma_subsampled and (self.compression != 7 or
self.planarconfig == 2):
raise NotImplementedError('chroma subsampling not supported')
if validate is None:
return
fh = self_.parent.filehandle
lock = fh.lock if lock is None else lock
with lock:
closed = fh.closed
if closed:
if reopen:
fh.open()
else:
raise IOError('file handle is closed')
dtype = self._dtype
shape = self._shape
imagewidth = self.imagewidth
imagelength = self.imagelength
imagedepth = self.imagedepth
bitspersample = self.bitspersample
typecode = self.parent.byteorder + dtype.char
lsb2msb = self.fillorder == 2
offsets, bytecounts = self_.offsets_bytecounts
istiled = self.is_tiled
if istiled:
tilewidth = self.tilewidth
tilelength = self.tilelength
tiledepth = self.tiledepth
tw = (imagewidth + tilewidth - 1) // tilewidth
tl = (imagelength + tilelength - 1) // tilelength
td = (imagedepth + tiledepth - 1) // tiledepth
shape = (shape[0], shape[1],
td*tiledepth, tl*tilelength, tw*tilewidth, shape[-1])
tileshape = (tiledepth, tilelength, tilewidth, shape[-1])
runlen = tilewidth
else:
runlen = imagewidth
if self.planarconfig == 1:
runlen *= self.samplesperpixel
if out == 'memmap' and self.is_memmappable:
with lock:
result = fh.memmap_array(typecode, shape, offset=offsets[0])
elif self.is_contiguous:
if out is not None:
out = create_output(out, shape, dtype)
with lock:
fh.seek(offsets[0])
result = fh.read_array(typecode, product(shape), out=out)
if out is None and not result.dtype.isnative:
# swap byte order and dtype without copy
result.byteswap(True)
result = result.newbyteorder()
if lsb2msb:
reverse_bitorder(result)
else:
result = create_output(out, shape, dtype)
decompress = TIFF.DECOMPESSORS[self.compression]
if self.compression == 7: # COMPRESSION.JPEG
if bitspersample not in (8, 12):
raise ValueError(
'unsupported JPEG precision %i' % bitspersample)
if 'JPEGTables' in tags:
table = tags['JPEGTables'].value
else:
table = b''
unpack = identityfunc
colorspace = TIFF.PHOTOMETRIC(self.photometric).name
def decompress(x, func=decompress, table=table,
bitspersample=bitspersample,
colorspace=colorspace):
return func(x, table, bitspersample,
colorspace).reshape(-1)
elif bitspersample in (8, 16, 32, 64, 128):
if (bitspersample * runlen) % 8:
raise ValueError('data and sample size mismatch')
def unpack(x, typecode=typecode):
if self.predictor == 3: # PREDICTOR.FLOATINGPOINT
# the floating point horizontal differencing decoder
# needs the raw byte order
typecode = dtype.char
try:
# read only numpy array
return numpy.frombuffer(x, typecode)
except ValueError:
# strips may be missing EOI
# warnings.warn('unpack: %s' % e)
xlen = ((len(x) // (bitspersample // 8)) *
(bitspersample // 8))
return numpy.frombuffer(x[:xlen], typecode)
elif isinstance(bitspersample, tuple):
def unpack(x, typecode=typecode, bitspersample=bitspersample):
return unpack_rgb(x, typecode, bitspersample)
else:
def unpack(x, typecode=typecode, bitspersample=bitspersample,
runlen=runlen):
return unpack_ints(x, typecode, bitspersample, runlen)
if istiled:
writable = None
tw, tl, td, pl = 0, 0, 0, 0
for tile in buffered_read(fh, lock, offsets, bytecounts):
if lsb2msb:
tile = reverse_bitorder(tile)
tile = decompress(tile)
tile = unpack(tile)
try:
tile.shape = tileshape
except ValueError:
# incomplete tiles; see gdal issue #1179
warnings.warn('invalid tile data')
t = numpy.zeros(tileshape, dtype).reshape(-1)
s = min(tile.size, t.size)
t[:s] = tile[:s]
tile = t.reshape(tileshape)
if self.predictor == 2: # PREDICTOR.HORIZONTAL
if writable is None:
writable = tile.flags['WRITEABLE']
if writable:
numpy.cumsum(tile, axis=-2, dtype=dtype, out=tile)
else:
tile = numpy.cumsum(tile, axis=-2, dtype=dtype)
elif self.predictor == 3: # PREDICTOR.FLOATINGPOINT
raise NotImplementedError()
result[0, pl, td:td+tiledepth,
tl:tl+tilelength, tw:tw+tilewidth, :] = tile
del tile
tw += tilewidth
if tw >= shape[4]:
tw, tl = 0, tl + tilelength
if tl >= shape[3]:
tl, td = 0, td + tiledepth
if td >= shape[2]:
td, pl = 0, pl + 1
result = result[..., :imagedepth, :imagelength, :imagewidth, :]
else:
strip_size = self.rowsperstrip * self.imagewidth
if self.planarconfig == 1:
strip_size *= self.samplesperpixel
result = result.reshape(-1)
index = 0
for strip in buffered_read(fh, lock, offsets, bytecounts):
if lsb2msb:
strip = reverse_bitorder(strip)
strip = decompress(strip)
strip = unpack(strip)
size = min(result.size, strip.size, strip_size,
result.size - index)
result[index:index+size] = strip[:size]
del strip
index += size
result.shape = self._shape
if self.predictor != 1 and not (istiled and not self.is_contiguous):
if self.parent.is_lsm and self.compression == 1:
pass # work around bug in LSM510 software
elif self.predictor == 2: # PREDICTOR.HORIZONTAL
numpy.cumsum(result, axis=-2, dtype=dtype, out=result)
elif self.predictor == 3: # PREDICTOR.FLOATINGPOINT
result = decode_floats(result)
if squeeze:
try:
result.shape = self.shape
except ValueError:
warnings.warn('failed to reshape from %s to %s' % (
str(result.shape), str(self.shape)))
if closed:
# TODO: file should remain open if an exception occurred above
fh.close()
return result
def asrgb(self, uint8=False, alpha=None, colormap=None,
dmin=None, dmax=None, *args, **kwargs):
"""Return image data as RGB(A).
Work in progress.
"""
data = self.asarray(*args, **kwargs)
self = self.keyframe # self or keyframe
photometric = self.photometric
PHOTOMETRIC = TIFF.PHOTOMETRIC
if photometric == PHOTOMETRIC.PALETTE:
colormap = self.colormap
if (colormap.shape[1] < 2**self.bitspersample or
self.dtype.char not in 'BH'):
raise ValueError('cannot apply colormap')
if uint8:
if colormap.max() > 255:
colormap >>= 8
colormap = colormap.astype('uint8')
if 'S' in self.axes:
data = data[..., 0] if self.planarconfig == 1 else data[0]
data = apply_colormap(data, colormap)
elif photometric == PHOTOMETRIC.RGB:
if 'ExtraSamples' in self.tags:
if alpha is None:
alpha = TIFF.EXTRASAMPLE
extrasamples = self.extrasamples
if self.tags['ExtraSamples'].count == 1:
extrasamples = (extrasamples,)
for i, exs in enumerate(extrasamples):
if exs in alpha:
if self.planarconfig == 1:
data = data[..., [0, 1, 2, 3+i]]
else:
data = data[:, [0, 1, 2, 3+i]]
break
else:
if self.planarconfig == 1:
data = data[..., :3]
else:
data = data[:, :3]
# TODO: convert to uint8?
elif photometric == PHOTOMETRIC.MINISBLACK:
raise NotImplementedError()
elif photometric == PHOTOMETRIC.MINISWHITE:
raise NotImplementedError()
elif photometric == PHOTOMETRIC.SEPARATED:
raise NotImplementedError()
else:
raise NotImplementedError()
return data
def aspage(self):
return self
@property
def keyframe(self):
return self
@keyframe.setter
def keyframe(self, index):
return
@lazyattr
def offsets_bytecounts(self):
"""Return simplified offsets and bytecounts."""
if self.is_contiguous:
offset, byte_count = self.is_contiguous
return [offset], [byte_count]
return clean_offsets_counts(self.dataoffsets, self.databytecounts)
@lazyattr
def is_contiguous(self):
"""Return offset and size of contiguous data, else None.
Excludes prediction and fill_order.
"""
if (self.compression != 1
or self.bitspersample not in (8, 16, 32, 64)):
return
if 'TileWidth' in self.tags:
if (self.imagewidth != self.tilewidth or
self.imagelength % self.tilelength or
self.tilewidth % 16 or self.tilelength % 16):
return
if ('ImageDepth' in self.tags and 'TileDepth' in self.tags and
(self.imagelength != self.tilelength or
self.imagedepth % self.tiledepth)):
return
offsets = self.dataoffsets
bytecounts = self.databytecounts
if len(offsets) == 1:
return offsets[0], bytecounts[0]
if self.is_stk or all((offsets[i] + bytecounts[i] == offsets[i+1] or
bytecounts[i+1] == 0) # no data/ignore offset
for i in range(len(offsets)-1)):
return offsets[0], sum(bytecounts)
@lazyattr
def is_final(self):
"""Return if page's image data are stored in final form.
Excludes byte-swapping.
"""
return (self.is_contiguous and self.fillorder == 1 and
self.predictor == 1 and not self.is_chroma_subsampled)
@lazyattr
def is_memmappable(self):
"""Return if page's image data in file can be memory-mapped."""
return (self.parent.filehandle.is_file and self.is_final and
# (self.bitspersample == 8 or self.parent.isnative) and
self.is_contiguous[0] % self.dtype.itemsize == 0) # aligned?
def __str__(self, detail=0, width=79):
"""Return string containing information about page."""
if self.keyframe != self:
return TiffFrame.__str__(self, detail)
attr = ''
for name in ('memmappable', 'final', 'contiguous'):
attr = getattr(self, 'is_'+name)
if attr:
attr = name.upper()
break
info = ' '.join(s for s in (
'x'.join(str(i) for i in self.shape),
'%s%s' % (TIFF.SAMPLEFORMAT(self.sampleformat).name,
self.bitspersample),
'|'.join(i for i in (
TIFF.PHOTOMETRIC(self.photometric).name,
'TILED' if self.is_tiled else '',
self.compression.name if self.compression != 1 else '',
self.planarconfig.name if self.planarconfig != 1 else '',
self.predictor.name if self.predictor != 1 else '',
self.fillorder.name if self.fillorder != 1 else '')
if i),
attr,
'|'.join((f.upper() for f in self.flags))
) if s)
info = 'TiffPage %i @%i %s' % (self.index, self.offset, info)
if detail <= 0:
return info
info = [info]
tags = self.tags
tlines = []
vlines = []
for tag in sorted(tags.values(), key=lambda x: x.code):
value = tag.__str__(width=width+1)
tlines.append(value[:width].strip())
if detail > 1 and len(value) > width:
name = tag.name.upper()
if detail <= 2 and ('COUNTS' in name or 'OFFSETS' in name):
value = pformat(tag.value, width=width, height=detail*4)
else:
value = pformat(tag.value, width=width, height=detail*12)
vlines.append('%s\n%s' % (tag.name, value))
info.append('\n'.join(tlines))
if detail > 1:
info.append('\n\n'.join(vlines))
if detail > 3:
try:
info.append('DATA\n%s' % pformat(
self.asarray(), width=width, height=detail*8))
except Exception:
pass
return '\n\n'.join(info)
@lazyattr
def flags(self):
"""Return set of flags."""
return set((name.lower() for name in sorted(TIFF.FILE_FLAGS)
if getattr(self, 'is_' + name)))
@property
def ndim(self):
"""Return number of array dimensions."""
return len(self.shape)
@property
def size(self):
"""Return number of elements in array."""
return product(self.shape)
@lazyattr
def andor_tags(self):
"""Return consolidated metadata from Andor tags as dict.
Remove Andor tags from self.tags.
"""
if not self.is_andor:
return
tags = self.tags
result = {'Id': tags['AndorId'].value}
for tag in list(self.tags.values()):
code = tag.code
if not 4864 < code < 5031:
continue
value = tag.value
name = tag.name[5:] if len(tag.name) > 5 else tag.name
result[name] = value
del tags[tag.name]
return result
@lazyattr
def epics_tags(self):
"""Return consolidated metadata from EPICS areaDetector tags as dict.
Remove areaDetector tags from self.tags.
"""
if not self.is_epics:
return
result = {}
tags = self.tags
for tag in list(self.tags.values()):
code = tag.code
if not 65000 <= code < 65500:
continue
value = tag.value
if code == 65000:
result['timeStamp'] = datetime.datetime.fromtimestamp(
float(value))
elif code == 65001:
result['uniqueID'] = int(value)
elif code == 65002:
result['epicsTSSec'] = int(value)
elif code == 65003:
result['epicsTSNsec'] = int(value)
else:
key, value = value.split(':', 1)
result[key] = astype(value)
del tags[tag.name]
return result
@lazyattr
def geotiff_tags(self):
"""Return consolidated metadata from GeoTIFF tags as dict."""
if not self.is_geotiff:
return
tags = self.tags
gkd = tags['GeoKeyDirectoryTag'].value
if gkd[0] != 1:
warnings.warn('invalid GeoKeyDirectoryTag')
return {}
result = {
'KeyDirectoryVersion': gkd[0],
'KeyRevision': gkd[1],
'KeyRevisionMinor': gkd[2],
# 'NumberOfKeys': gkd[3],
}
# deltags = ['GeoKeyDirectoryTag']
geokeys = TIFF.GEO_KEYS
geocodes = TIFF.GEO_CODES
for index in range(gkd[3]):
keyid, tagid, count, offset = gkd[4 + index * 4: index * 4 + 8]
keyid = geokeys.get(keyid, keyid)
if tagid == 0:
value = offset
else:
tagname = TIFF.TAGS[tagid]
# deltags.append(tagname)
value = tags[tagname].value[offset: offset + count]
if tagid == 34737 and count > 1 and value[-1] == '|':
value = value[:-1]
value = value if count > 1 else value[0]
if keyid in geocodes:
try:
value = geocodes[keyid](value)
except Exception:
pass
result[keyid] = value
if 'IntergraphMatrixTag' in tags:
value = tags['IntergraphMatrixTag'].value
value = numpy.array(value)
if len(value) == 16:
value = value.reshape((4, 4)).tolist()
result['IntergraphMatrix'] = value
if 'ModelPixelScaleTag' in tags:
value = numpy.array(tags['ModelPixelScaleTag'].value).tolist()
result['ModelPixelScale'] = value
if 'ModelTiepointTag' in tags:
value = tags['ModelTiepointTag'].value
value = numpy.array(value).reshape((-1, 6)).squeeze().tolist()
result['ModelTiepoint'] = value
if 'ModelTransformationTag' in tags:
value = tags['ModelTransformationTag'].value
value = numpy.array(value).reshape((4, 4)).tolist()
result['ModelTransformation'] = value
elif False:
# if 'ModelPixelScaleTag' in tags and 'ModelTiepointTag' in tags:
sx, sy, sz = tags['ModelPixelScaleTag'].value
tiepoints = tags['ModelTiepointTag'].value
transforms = []
for tp in range(0, len(tiepoints), 6):
i, j, k, x, y, z = tiepoints[tp:tp+6]
transforms.append([
[sx, 0.0, 0.0, x - i * sx],
[0.0, -sy, 0.0, y + j * sy],
[0.0, 0.0, sz, z - k * sz],
[0.0, 0.0, 0.0, 1.0]])
if len(tiepoints) == 6:
transforms = transforms[0]
result['ModelTransformation'] = transforms
if 'RPCCoefficientTag' in tags:
rpcc = tags['RPCCoefficientTag'].value
result['RPCCoefficient'] = {
'ERR_BIAS': rpcc[0],
'ERR_RAND': rpcc[1],
'LINE_OFF': rpcc[2],
'SAMP_OFF': rpcc[3],
'LAT_OFF': rpcc[4],
'LONG_OFF': rpcc[5],
'HEIGHT_OFF': rpcc[6],
'LINE_SCALE': rpcc[7],
'SAMP_SCALE': rpcc[8],
'LAT_SCALE': rpcc[9],
'LONG_SCALE': rpcc[10],
'HEIGHT_SCALE': rpcc[11],
'LINE_NUM_COEFF': rpcc[12:33],
'LINE_DEN_COEFF ': rpcc[33:53],
'SAMP_NUM_COEFF': rpcc[53:73],
'SAMP_DEN_COEFF': rpcc[73:]}
return result
@property
def is_tiled(self):
"""Page contains tiled image."""
return 'TileWidth' in self.tags
@property
def is_reduced(self):
"""Page is reduced image of another image."""
return ('NewSubfileType' in self.tags and
self.tags['NewSubfileType'].value & 1)
@property
def is_chroma_subsampled(self):
"""Page contains chroma subsampled image."""
return ('YCbCrSubSampling' in self.tags and
self.tags['YCbCrSubSampling'].value != (1, 1))
@lazyattr
def is_imagej(self):
"""Return ImageJ description if exists, else None."""
for description in (self.description, self.description1):
if not description:
return
if description[:7] == 'ImageJ=':
return description
@lazyattr
def is_shaped(self):
"""Return description containing array shape if exists, else None."""
for description in (self.description, self.description1):
if not description:
return
if description[:1] == '{' and '"shape":' in description:
return description
if description[:6] == 'shape=':
return description
@property
def is_mdgel(self):
"""Page contains MDFileTag tag."""
return 'MDFileTag' in self.tags
@property
def is_mediacy(self):
"""Page contains Media Cybernetics Id tag."""
return ('MC_Id' in self.tags and
self.tags['MC_Id'].value[:7] == b'MC TIFF')
@property
def is_stk(self):
"""Page contains UIC2Tag tag."""
return 'UIC2tag' in self.tags
@property
def is_lsm(self):
"""Page contains CZ_LSMINFO tag."""
return 'CZ_LSMINFO' in self.tags
@property
def is_fluoview(self):
"""Page contains FluoView MM_STAMP tag."""
return 'MM_Stamp' in self.tags
@property
def is_nih(self):
"""Page contains NIH image header."""
return 'NIHImageHeader' in self.tags
@property
def is_sgi(self):
"""Page contains SGI image and tile depth tags."""
return 'ImageDepth' in self.tags and 'TileDepth' in self.tags
@property
def is_vista(self):
"""Software tag is 'ISS Vista'."""
return self.software == 'ISS Vista'
@property
def is_metaseries(self):
"""Page contains MDS MetaSeries metadata in ImageDescription tag."""
if self.index > 1 or self.software != 'MetaSeries':
return False
d = self.description
return d.startswith('<MetaData>') and d.endswith('</MetaData>')
@property
def is_ome(self):
"""Page contains OME-XML in ImageDescription tag."""
if self.index > 1 or not self.description:
return False
d = self.description
return d[:14] == '<?xml version=' and d[-6:] == '</OME>'
@property
def is_scn(self):
"""Page contains Leica SCN XML in ImageDescription tag."""
if self.index > 1 or not self.description:
return False
d = self.description
return d[:14] == '<?xml version=' and d[-6:] == '</scn>'
@property
def is_micromanager(self):
"""Page contains Micro-Manager metadata."""
return 'MicroManagerMetadata' in self.tags
@property
def is_andor(self):
"""Page contains Andor Technology tags."""
return 'AndorId' in self.tags
@property
def is_pilatus(self):
"""Page contains Pilatus tags."""
return (self.software[:8] == 'TVX TIFF' and
self.description[:2] == '# ')
@property
def is_epics(self):
"""Page contains EPICS areaDetector tags."""
return (self.description == 'EPICS areaDetector' or
self.software == 'EPICS areaDetector')
@property
def is_tvips(self):
"""Page contains TVIPS metadata."""
return 'TVIPS' in self.tags
@property
def is_fei(self):
"""Page contains SFEG or HELIOS metadata."""
return 'FEI_SFEG' in self.tags or 'FEI_HELIOS' in self.tags
@property
def is_sem(self):
"""Page contains Zeiss SEM metadata."""
return 'CZ_SEM' in self.tags
@property
def is_svs(self):
"""Page contains Aperio metadata."""
return self.description[:20] == 'Aperio Image Library'
@property
def is_scanimage(self):
"""Page contains ScanImage metadata."""
return (self.description[:12] == 'state.config' or
self.software[:22] == 'SI.LINE_FORMAT_VERSION' or
'scanimage.SI.' in self.description[-256:])
@property
def is_qptiff(self):
"""Page contains PerkinElmer tissue images metadata."""
# The ImageDescription tag contains XML with a top-level
# <PerkinElmer-QPI-ImageDescription> element
return self.software[:15] == 'PerkinElmer-QPI'
@property
def is_geotiff(self):
"""Page contains GeoTIFF metadata."""
return 'GeoKeyDirectoryTag' in self.tags
class TiffFrame(object):
"""Lightweight TIFF image file directory (IFD).
Only a limited number of tag values are read from file, e.g. StripOffsets,
and StripByteCounts. Other tag values are assumed to be identical with a
specified TiffPage instance, the keyframe.
TiffFrame is intended to reduce resource usage and speed up reading data
from file, not for introspection of metadata.
Not compatible with Python 2.
"""
__slots__ = ('keyframe', 'parent', 'index', 'offset',
'dataoffsets', 'databytecounts')
is_mdgel = False
tags = {}
def __init__(self, parent, index, keyframe):
"""Read specified tags from file.
The file handle position must be at the offset to a valid IFD.
"""
self.keyframe = keyframe
self.parent = parent
self.index = index
self.dataoffsets = None
self.databytecounts = None
unpack = struct.unpack
fh = parent.filehandle
self.offset = fh.tell()
try:
tagno = unpack(parent.tagnoformat, fh.read(parent.tagnosize))[0]
if tagno > 4096:
raise ValueError('suspicious number of tags')
except Exception:
raise ValueError('corrupted page list at offset %i' % self.offset)
# tags = {}
tagcodes = {273, 279, 324, 325} # TIFF.FRAME_TAGS
tagsize = parent.tagsize
codeformat = parent.tagformat1[:2]
data = fh.read(tagsize * tagno)
index = -tagsize
for _ in range(tagno):
index += tagsize
code = unpack(codeformat, data[index:index+2])[0]
if code not in tagcodes:
continue
try:
tag = TiffTag(parent, data[index:index+tagsize])
except TiffTag.Error as e:
warnings.warn(str(e))
continue
if code == 273 or code == 324:
setattr(self, 'dataoffsets', tag.value)
elif code == 279 or code == 325:
setattr(self, 'databytecounts', tag.value)
# elif code == 270:
# tagname = tag.name
# if tagname not in tags:
# tags[tagname] = bytes2str(tag.value)
# elif 'ImageDescription1' not in tags:
# tags['ImageDescription1'] = bytes2str(tag.value)
# else:
# tags[tag.name] = tag.value
def aspage(self):
"""Return TiffPage from file."""
self.parent.filehandle.seek(self.offset)
return TiffPage(self.parent, index=self.index, keyframe=None)
def asarray(self, *args, **kwargs):
"""Read image data from file and return as numpy array."""
# TODO: fix TypeError on Python 2
# "TypeError: unbound method asarray() must be called with TiffPage
# instance as first argument (got TiffFrame instance instead)"
kwargs['validate'] = False
return TiffPage.asarray(self, *args, **kwargs)
def asrgb(self, *args, **kwargs):
"""Read image data from file and return RGB image as numpy array."""
kwargs['validate'] = False
return TiffPage.asrgb(self, *args, **kwargs)
@property
def offsets_bytecounts(self):
"""Return simplified offsets and bytecounts."""
if self.keyframe.is_contiguous:
return self.dataoffsets[:1], self.keyframe.is_contiguous[1:]
return clean_offsets_counts(self.dataoffsets, self.databytecounts)
@property
def is_contiguous(self):
"""Return offset and size of contiguous data, else None."""
if self.keyframe.is_contiguous:
return self.dataoffsets[0], self.keyframe.is_contiguous[1]
@property
def is_memmappable(self):
"""Return if page's image data in file can be memory-mapped."""
return self.keyframe.is_memmappable
def __getattr__(self, name):
"""Return attribute from keyframe."""
if name in TIFF.FRAME_ATTRS:
return getattr(self.keyframe, name)
# this error could be raised because an AttributeError was
# raised inside a @property function
raise AttributeError("'%s' object has no attribute '%s'" %
(self.__class__.__name__, name))
def __str__(self, detail=0):
"""Return string containing information about frame."""
info = ' '.join(s for s in (
'x'.join(str(i) for i in self.shape),
str(self.dtype)))
return 'TiffFrame %i @%i %s' % (self.index, self.offset, info)
class TiffTag(object):
"""TIFF tag structure.
Attributes
----------
name : string
Name of tag.
code : int
Decimal code of tag.
dtype : str
Datatype of tag data. One of TIFF DATA_FORMATS.
count : int
Number of values.
value : various types
Tag data as Python object.
ImageSourceData : int
Location of value in file.
All attributes are read-only.
"""
__slots__ = ('code', 'count', 'dtype', 'value', 'valueoffset')
class Error(Exception):
pass
def __init__(self, parent, tagheader, **kwargs):
"""Initialize instance from tag header."""
fh = parent.filehandle
byteorder = parent.byteorder
unpack = struct.unpack
offsetsize = parent.offsetsize
self.valueoffset = fh.tell() + offsetsize + 4
code, type_ = unpack(parent.tagformat1, tagheader[:4])
count, value = unpack(parent.tagformat2, tagheader[4:])
try:
dtype = TIFF.DATA_FORMATS[type_]
except KeyError:
raise TiffTag.Error('unknown tag data type %i' % type_)
fmt = '%s%i%s' % (byteorder, count * int(dtype[0]), dtype[1])
size = struct.calcsize(fmt)
if size > offsetsize or code in TIFF.TAG_READERS:
self.valueoffset = offset = unpack(parent.offsetformat, value)[0]
if offset < 8 or offset > fh.size - size:
raise TiffTag.Error('invalid tag value offset')
# if offset % 2:
# warnings.warn('tag value does not begin on word boundary')
fh.seek(offset)
if code in TIFF.TAG_READERS:
readfunc = TIFF.TAG_READERS[code]
value = readfunc(fh, byteorder, dtype, count, offsetsize)
elif type_ == 7 or (count > 1 and dtype[-1] == 'B'):
value = read_bytes(fh, byteorder, dtype, count, offsetsize)
elif code in TIFF.TAGS or dtype[-1] == 's':
value = unpack(fmt, fh.read(size))
else:
value = read_numpy(fh, byteorder, dtype, count, offsetsize)
elif dtype[-1] == 'B' or type_ == 7:
value = value[:size]
else:
value = unpack(fmt, value[:size])
process = (code not in TIFF.TAG_READERS and code not in TIFF.TAG_TUPLE
and type_ != 7)
if process and dtype[-1] == 's' and isinstance(value[0], bytes):
# TIFF ASCII fields can contain multiple strings,
# each terminated with a NUL
value = value[0]
try:
value = bytes2str(stripascii(value).strip())
except UnicodeDecodeError:
warnings.warn('tag %i: coercing invalid ASCII to bytes' % code)
dtype = '1B'
else:
if code in TIFF.TAG_ENUM:
t = TIFF.TAG_ENUM[code]
try:
value = tuple(t(v) for v in value)
except ValueError as e:
warnings.warn(str(e))
if process:
if len(value) == 1:
value = value[0]
self.code = code
self.dtype = dtype
self.count = count
self.value = value
@property
def name(self):
return TIFF.TAGS.get(self.code, str(self.code))
def _fix_lsm_bitspersample(self, parent):
"""Correct LSM bitspersample tag.
Old LSM writers may use a separate region for two 16-bit values,
although they fit into the tag value element of the tag.
"""
if self.code == 258 and self.count == 2:
# TODO: test this case; need example file
warnings.warn('correcting LSM bitspersample tag')
tof = parent.offsetformat[parent.offsetsize]
self.valueoffset = struct.unpack(tof, self._value)[0]
parent.filehandle.seek(self.valueoffset)
self.value = struct.unpack('<HH', parent.filehandle.read(4))
def __str__(self, detail=0, width=79):
"""Return string containing information about tag."""
height = 1 if detail <= 0 else 8 * detail
tcode = '%i%s' % (self.count * int(self.dtype[0]), self.dtype[1])
line = 'TiffTag %i %s %s @%i ' % (
self.code, self.name, tcode, self.valueoffset)[:width]
if self.code in TIFF.TAG_ENUM:
if self.count == 1:
value = TIFF.TAG_ENUM[self.code](self.value).name
else:
value = pformat(tuple(v.name for v in self.value))
else:
value = pformat(self.value, width=width, height=height)
if detail <= 0:
line += value
line = line[:width]
else:
line += '\n' + value
return line
class TiffPageSeries(object):
"""Series of TIFF pages with compatible shape and data type.
Attributes
----------
pages : list of TiffPage
Sequence of TiffPages in series.
dtype : numpy.dtype
Data type (native byte order) of the image array in series.
shape : tuple
Dimensions of the image array in series.
axes : str
Labels of axes in shape. See TiffPage.axes.
offset : int or None
Position of image data in file if memory-mappable, else None.
"""
def __init__(self, pages, shape, dtype, axes, parent=None, name=None,
transform=None, stype=None, truncated=False):
"""Initialize instance."""
self.index = 0
self._pages = pages # might contain only first of contiguous pages
self.shape = tuple(shape)
self.axes = ''.join(axes)
self.dtype = numpy.dtype(dtype)
self.stype = stype if stype else ''
self.name = name if name else ''
self.transform = transform
if parent:
self.parent = parent
elif pages:
self.parent = pages[0].parent
else:
self.parent = None
if len(pages) == 1 and not truncated:
self._len = int(product(self.shape) // product(pages[0].shape))
else:
self._len = len(pages)
def asarray(self, out=None):
"""Return image data from series of TIFF pages as numpy array."""
if self.parent:
result = self.parent.asarray(series=self, out=out)
if self.transform is not None:
result = self.transform(result)
return result
@lazyattr
def offset(self):
"""Return offset to series data in file, if any."""
if not self._pages:
return
pos = 0
for page in self._pages:
if page is None:
return
if not page.is_final:
return
if not pos:
pos = page.is_contiguous[0] + page.is_contiguous[1]
continue
if pos != page.is_contiguous[0]:
return
pos += page.is_contiguous[1]
page = self._pages[0]
offset = page.is_contiguous[0]
if (page.is_imagej or page.is_shaped) and len(self._pages) == 1:
# truncated files
return offset
if pos == offset + product(self.shape) * self.dtype.itemsize:
return offset
@property
def ndim(self):
"""Return number of array dimensions."""
return len(self.shape)
@property
def size(self):
"""Return number of elements in array."""
return int(product(self.shape))
@property
def pages(self):
"""Return sequence of all pages in series."""
# a workaround to keep the old interface working
return self
def __len__(self):
"""Return number of TiffPages in series."""
return self._len
def __getitem__(self, key):
"""Return specified TiffPage."""
if len(self._pages) == 1 and 0 < key < self._len:
index = self._pages[0].index
return self.parent.pages[index + key]
return self._pages[key]
def __iter__(self):
"""Return iterator over TiffPages in series."""
if len(self._pages) == self._len:
for page in self._pages:
yield page
else:
pages = self.parent.pages
index = self._pages[0].index
for i in range(self._len):
yield pages[index + i]
def __str__(self):
"""Return string with information about series."""
s = ' '.join(s for s in (
snipstr("'%s'" % self.name, 20) if self.name else '',
'x'.join(str(i) for i in self.shape),
str(self.dtype),
self.axes,
self.stype,
'%i Pages' % len(self.pages),
('Offset=%i' % self.offset) if self.offset else '') if s)
return 'TiffPageSeries %i %s' % (self.index, s)
class TiffSequence(object):
"""Sequence of TIFF files.
The image data in all files must match shape, dtype, etc.
Attributes
----------
files : list
List of file names.
shape : tuple
Shape of image sequence. Excludes shape of image array.
axes : str
Labels of axes in shape.
Examples
--------
>>> # read image stack from sequence of TIFF files
>>> imsave('temp_C001T001.tif', numpy.random.rand(64, 64))
>>> imsave('temp_C001T002.tif', numpy.random.rand(64, 64))
>>> tifs = TiffSequence('temp_C001*.tif')
>>> tifs.shape
(1, 2)
>>> tifs.axes
'CT'
>>> data = tifs.asarray()
>>> data.shape
(1, 2, 64, 64)
"""
_patterns = {
'axes': r"""
# matches Olympus OIF and Leica TIFF series
_?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))
_?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
_?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
_?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
_?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
_?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
_?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
"""}
class ParseError(Exception):
pass
def __init__(self, files, imread=TiffFile, pattern='axes',
*args, **kwargs):
"""Initialize instance from multiple files.
Parameters
----------
files : str, pathlib.Path, or sequence thereof
Glob pattern or sequence of file names.
Binary streams are not supported.
imread : function or class
Image read function or class with asarray function returning numpy
array from single file.
pattern : str
Regular expression pattern that matches axes names and sequence
indices in file names.
By default, the pattern matches Olympus OIF and Leica TIFF series.
"""
if isinstance(files, pathlib.Path):
files = str(files)
if isinstance(files, basestring):
files = natural_sorted(glob.glob(files))
files = list(files)
if not files:
raise ValueError('no files found')
if isinstance(files[0], pathlib.Path):
files = [str(pathlib.Path(f)) for f in files]
elif not isinstance(files[0], basestring):
raise ValueError('not a file name')
self.files = files
if hasattr(imread, 'asarray'):
# redefine imread
_imread = imread
def imread(fname, *args, **kwargs):
with _imread(fname) as im:
return im.asarray(*args, **kwargs)
self.imread = imread
self.pattern = self._patterns.get(pattern, pattern)
try:
self._parse()
if not self.axes:
self.axes = 'I'
except self.ParseError:
self.axes = 'I'
self.shape = (len(files),)
self._startindex = (0,)
self._indices = tuple((i,) for i in range(len(files)))
def __str__(self):
"""Return string with information about image sequence."""
return '\n'.join([
self.files[0],
' size: %i' % len(self.files),
' axes: %s' % self.axes,
' shape: %s' % str(self.shape)])
def __len__(self):
return len(self.files)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def close(self):
pass
def asarray(self, out=None, *args, **kwargs):
"""Read image data from all files and return as numpy array.
The args and kwargs parameters are passed to the imread function.
Raise IndexError or ValueError if image shapes do not match.
"""
im = self.imread(self.files[0], *args, **kwargs)
shape = self.shape + im.shape
result = create_output(out, shape, dtype=im.dtype)
result = result.reshape(-1, *im.shape)
for index, fname in zip(self._indices, self.files):
index = [i-j for i, j in zip(index, self._startindex)]
index = numpy.ravel_multi_index(index, self.shape)
im = self.imread(fname, *args, **kwargs)
result[index] = im
result.shape = shape
return result
def _parse(self):
"""Get axes and shape from file names."""
if not self.pattern:
raise self.ParseError('invalid pattern')
pattern = re.compile(self.pattern, re.IGNORECASE | re.VERBOSE)
matches = pattern.findall(self.files[0])
if not matches:
raise self.ParseError('pattern does not match file names')
matches = matches[-1]
if len(matches) % 2:
raise self.ParseError('pattern does not match axis name and index')
axes = ''.join(m for m in matches[::2] if m)
if not axes:
raise self.ParseError('pattern does not match file names')
indices = []
for fname in self.files:
matches = pattern.findall(fname)[-1]
if axes != ''.join(m for m in matches[::2] if m):
raise ValueError('axes do not match within the image sequence')
indices.append([int(m) for m in matches[1::2] if m])
shape = tuple(numpy.max(indices, axis=0))
startindex = tuple(numpy.min(indices, axis=0))
shape = tuple(i-j+1 for i, j in zip(shape, startindex))
if product(shape) != len(self.files):
warnings.warn('files are missing. Missing data are zeroed')
self.axes = axes.upper()
self.shape = shape
self._indices = indices
self._startindex = startindex
class FileHandle(object):
"""Binary file handle.
A limited, special purpose file handler that can:
* handle embedded files (for CZI within CZI files)
* re-open closed files (for multi-file formats, such as OME-TIFF)
* read and write numpy arrays and records from file like objects
Only 'rb' and 'wb' modes are supported. Concurrently reading and writing
of the same stream is untested.
When initialized from another file handle, do not use it unless this
FileHandle is closed.
Attributes
----------
name : str
Name of the file.
path : str
Absolute path to file.
size : int
Size of file in bytes.
is_file : bool
If True, file has a filno and can be memory-mapped.
All attributes are read-only.
"""
__slots__ = ('_fh', '_file', '_mode', '_name', '_dir', '_lock',
'_offset', '_size', '_close', 'is_file')
def __init__(self, file, mode='rb', name=None, offset=None, size=None):
"""Initialize file handle from file name or another file handle.
Parameters
----------
file : str, pathlib.Path, binary stream, or FileHandle
File name or seekable binary stream, such as an open file
or BytesIO.
mode : str
File open mode in case 'file' is a file name. Must be 'rb' or 'wb'.
name : str
Optional name of file in case 'file' is a binary stream.
offset : int
Optional start position of embedded file. By default, this is
the current file position.
size : int
Optional size of embedded file. By default, this is the number
of bytes from the 'offset' to the end of the file.
"""
self._file = file
self._fh = None
self._mode = mode
self._name = name
self._dir = ''
self._offset = offset
self._size = size
self._close = True
self.is_file = False
self._lock = NullContext()
self.open()
def open(self):
"""Open or re-open file."""
if self._fh:
return # file is open
if isinstance(self._file, pathlib.Path):
self._file = str(self._file)
if isinstance(self._file, basestring):
# file name
self._file = os.path.realpath(self._file)
self._dir, self._name = os.path.split(self._file)
self._fh = open(self._file, self._mode)
self._close = True
if self._offset is None:
self._offset = 0
elif isinstance(self._file, FileHandle):
# FileHandle
self._fh = self._file._fh
if self._offset is None:
self._offset = 0
self._offset += self._file._offset
self._close = False
if not self._name:
if self._offset:
name, ext = os.path.splitext(self._file._name)
self._name = '%s@%i%s' % (name, self._offset, ext)
else:
self._name = self._file._name
if self._mode and self._mode != self._file._mode:
raise ValueError('FileHandle has wrong mode')
self._mode = self._file._mode
self._dir = self._file._dir
elif hasattr(self._file, 'seek'):
# binary stream: open file, BytesIO
try:
self._file.tell()
except Exception:
raise ValueError('binary stream is not seekable')
self._fh = self._file
if self._offset is None:
self._offset = self._file.tell()
self._close = False
if not self._name:
try:
self._dir, self._name = os.path.split(self._fh.name)
except AttributeError:
self._name = 'Unnamed binary stream'
try:
self._mode = self._fh.mode
except AttributeError:
pass
else:
raise ValueError('The first parameter must be a file name, '
'seekable binary stream, or FileHandle')
if self._offset:
self._fh.seek(self._offset)
if self._size is None:
pos = self._fh.tell()
self._fh.seek(self._offset, 2)
self._size = self._fh.tell()
self._fh.seek(pos)
try:
self._fh.fileno()
self.is_file = True
except Exception:
self.is_file = False
def read(self, size=-1):
"""Read 'size' bytes from file, or until EOF is reached."""
if size < 0 and self._offset:
size = self._size
return self._fh.read(size)
def write(self, bytestring):
"""Write bytestring to file."""
return self._fh.write(bytestring)
def flush(self):
"""Flush write buffers if applicable."""
return self._fh.flush()
def memmap_array(self, dtype, shape, offset=0, mode='r', order='C'):
"""Return numpy.memmap of data stored in file."""
if not self.is_file:
raise ValueError('Cannot memory-map file without fileno')
return numpy.memmap(self._fh, dtype=dtype, mode=mode,
offset=self._offset + offset,
shape=shape, order=order)
def read_array(self, dtype, count=-1, sep='', chunksize=2**25, out=None,
native=False):
"""Return numpy array from file.
Work around numpy issue #2230, "numpy.fromfile does not accept
StringIO object" https://github.com/numpy/numpy/issues/2230.
"""
fh = self._fh
dtype = numpy.dtype(dtype)
size = self._size if count < 0 else count * dtype.itemsize
if out is None:
try:
result = numpy.fromfile(fh, dtype, count, sep)
except IOError:
# ByteIO
data = fh.read(size)
result = numpy.frombuffer(data, dtype, count).copy()
if native and not result.dtype.isnative:
# swap byte order and dtype without copy
result.byteswap(True)
result = result.newbyteorder()
return result
# Read data from file in chunks and copy to output array
shape = out.shape
size = min(out.nbytes, size)
out = out.reshape(-1)
index = 0
while size > 0:
data = fh.read(min(chunksize, size))
datasize = len(data)
if datasize == 0:
break
size -= datasize
data = numpy.frombuffer(data, dtype)
out[index:index+data.size] = data
index += data.size
if hasattr(out, 'flush'):
out.flush()
return out.reshape(shape)
def read_record(self, dtype, shape=1, byteorder=None):
"""Return numpy record from file."""
rec = numpy.rec
try:
record = rec.fromfile(self._fh, dtype, shape, byteorder=byteorder)
except Exception:
dtype = numpy.dtype(dtype)
if shape is None:
shape = self._size // dtype.itemsize
size = product(sequence(shape)) * dtype.itemsize
data = self._fh.read(size)
record = rec.fromstring(data, dtype, shape, byteorder=byteorder)
return record[0] if shape == 1 else record
def write_empty(self, size):
"""Append size bytes to file. Position must be at end of file."""
if size < 1:
return
self._fh.seek(size-1, 1)
self._fh.write(b'\x00')
def write_array(self, data):
"""Write numpy array to binary file."""
try:
data.tofile(self._fh)
except Exception:
# BytesIO
self._fh.write(data.tostring())
def tell(self):
"""Return file's current position."""
return self._fh.tell() - self._offset
def seek(self, offset, whence=0):
"""Set file's current position."""
if self._offset:
if whence == 0:
self._fh.seek(self._offset + offset, whence)
return
elif whence == 2 and self._size > 0:
self._fh.seek(self._offset + self._size + offset, 0)
return
self._fh.seek(offset, whence)
def close(self):
"""Close file."""
if self._close and self._fh:
self._fh.close()
self._fh = None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def __getattr__(self, name):
"""Return attribute from underlying file object."""
if self._offset:
warnings.warn(
"FileHandle: '%s' not implemented for embedded files" % name)
return getattr(self._fh, name)
@property
def name(self):
return self._name
@property
def dirname(self):
return self._dir
@property
def path(self):
return os.path.join(self._dir, self._name)
@property
def size(self):
return self._size
@property
def closed(self):
return self._fh is None
@property
def lock(self):
return self._lock
@lock.setter
def lock(self, value):
self._lock = threading.RLock() if value else NullContext()
class NullContext(object):
"""Null context manager.
>>> with NullContext():
... pass
"""
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
pass
class OpenFileCache(object):
"""Keep files open."""
__slots__ = ('files', 'past', 'lock', 'size')
def __init__(self, size, lock=None):
"""Initialize open file cache."""
self.past = [] # FIFO of opened files
self.files = {} # refcounts of opened files
self.lock = NullContext() if lock is None else lock
self.size = int(size)
def open(self, filehandle):
"""Re-open file if necessary."""
with self.lock:
if filehandle in self.files:
self.files[filehandle] += 1
elif filehandle.closed:
filehandle.open()
self.files[filehandle] = 1
self.past.append(filehandle)
def close(self, filehandle):
"""Close openend file if no longer used."""
with self.lock:
if filehandle in self.files:
self.files[filehandle] -= 1
# trim the file cache
index = 0
size = len(self.past)
while size > self.size and index < size:
filehandle = self.past[index]
if self.files[filehandle] == 0:
filehandle.close()
del self.files[filehandle]
del self.past[index]
size -= 1
else:
index += 1
def clear(self):
"""Close all opened files if not in use."""
with self.lock:
for filehandle, refcount in list(self.files.items()):
if refcount == 0:
filehandle.close()
del self.files[filehandle]
del self.past[self.past.index(filehandle)]
class LazyConst(object):
"""Class whose attributes are computed on first access from its methods."""
def __init__(self, cls):
self._cls = cls
self.__doc__ = getattr(cls, '__doc__')
def __getattr__(self, name):
func = getattr(self._cls, name)
if not callable(func):
return func
try:
value = func()
except TypeError:
# Python 2 unbound method
value = func.__func__()
setattr(self, name, value)
return value
@LazyConst
class TIFF(object):
"""Namespace for module constants."""
def TAGS():
# TIFF tag codes and names from TIFF6, TIFF/EP, EXIF, and other specs
return {
11: 'ProcessingSoftware',
254: 'NewSubfileType',
255: 'SubfileType',
256: 'ImageWidth',
257: 'ImageLength',
258: 'BitsPerSample',
259: 'Compression',
262: 'PhotometricInterpretation',
263: 'Thresholding',
264: 'CellWidth',
265: 'CellLength',
266: 'FillOrder',
269: 'DocumentName',
270: 'ImageDescription',
271: 'Make',
272: 'Model',
273: 'StripOffsets',
274: 'Orientation',
277: 'SamplesPerPixel',
278: 'RowsPerStrip',
279: 'StripByteCounts',
280: 'MinSampleValue',
281: 'MaxSampleValue',
282: 'XResolution',
283: 'YResolution',
284: 'PlanarConfiguration',
285: 'PageName',
286: 'XPosition',
287: 'YPosition',
288: 'FreeOffsets',
289: 'FreeByteCounts',
290: 'GrayResponseUnit',
291: 'GrayResponseCurve',
292: 'T4Options',
293: 'T6Options',
296: 'ResolutionUnit',
297: 'PageNumber',
300: 'ColorResponseUnit',
301: 'TransferFunction',
305: 'Software',
306: 'DateTime',
315: 'Artist',
316: 'HostComputer',
317: 'Predictor',
318: 'WhitePoint',
319: 'PrimaryChromaticities',
320: 'ColorMap',
321: 'HalftoneHints',
322: 'TileWidth',
323: 'TileLength',
324: 'TileOffsets',
325: 'TileByteCounts',
326: 'BadFaxLines',
327: 'CleanFaxData',
328: 'ConsecutiveBadFaxLines',
330: 'SubIFDs',
332: 'InkSet',
333: 'InkNames',
334: 'NumberOfInks',
336: 'DotRange',
337: 'TargetPrinter',
338: 'ExtraSamples',
339: 'SampleFormat',
340: 'SMinSampleValue',
341: 'SMaxSampleValue',
342: 'TransferRange',
343: 'ClipPath',
344: 'XClipPathUnits',
345: 'YClipPathUnits',
346: 'Indexed',
347: 'JPEGTables',
351: 'OPIProxy',
400: 'GlobalParametersIFD',
401: 'ProfileType',
402: 'FaxProfile',
403: 'CodingMethods',
404: 'VersionYear',
405: 'ModeNumber',
433: 'Decode',
434: 'DefaultImageColor',
435: 'T82Options',
437: 'JPEGTables_', # 347
512: 'JPEGProc',
513: 'JPEGInterchangeFormat',
514: 'JPEGInterchangeFormatLength',
515: 'JPEGRestartInterval',
517: 'JPEGLosslessPredictors',
518: 'JPEGPointTransforms',
519: 'JPEGQTables',
520: 'JPEGDCTables',
521: 'JPEGACTables',
529: 'YCbCrCoefficients',
530: 'YCbCrSubSampling',
531: 'YCbCrPositioning',
532: 'ReferenceBlackWhite',
559: 'StripRowCounts',
700: 'XMP', # XMLPacket
769: 'GDIGamma', # GDI+
770: 'ICCProfileDescriptor', # GDI+
771: 'SRGBRenderingIntent', # GDI+
800: 'ImageTitle', # GDI+
999: 'USPTO_Miscellaneous',
4864: 'AndorId', # TODO: Andor Technology 4864 - 5030
4869: 'AndorTemperature',
4876: 'AndorExposureTime',
4878: 'AndorKineticCycleTime',
4879: 'AndorAccumulations',
4881: 'AndorAcquisitionCycleTime',
4882: 'AndorReadoutTime',
4884: 'AndorPhotonCounting',
4885: 'AndorEmDacLevel',
4890: 'AndorFrames',
4896: 'AndorHorizontalFlip',
4897: 'AndorVerticalFlip',
4898: 'AndorClockwise',
4899: 'AndorCounterClockwise',
4904: 'AndorVerticalClockVoltage',
4905: 'AndorVerticalShiftSpeed',
4907: 'AndorPreAmpSetting',
4908: 'AndorCameraSerial',
4911: 'AndorActualTemperature',
4912: 'AndorBaselineClamp',
4913: 'AndorPrescans',
4914: 'AndorModel',
4915: 'AndorChipSizeX',
4916: 'AndorChipSizeY',
4944: 'AndorBaselineOffset',
4966: 'AndorSoftwareVersion',
18246: 'Rating',
18247: 'XP_DIP_XML',
18248: 'StitchInfo',
18249: 'RatingPercent',
20481: 'ResolutionXUnit', # GDI+
20482: 'ResolutionYUnit', # GDI+
20483: 'ResolutionXLengthUnit', # GDI+
20484: 'ResolutionYLengthUnit', # GDI+
20485: 'PrintFlags', # GDI+
20486: 'PrintFlagsVersion', # GDI+
20487: 'PrintFlagsCrop', # GDI+
20488: 'PrintFlagsBleedWidth', # GDI+
20489: 'PrintFlagsBleedWidthScale', # GDI+
20490: 'HalftoneLPI', # GDI+
20491: 'HalftoneLPIUnit', # GDI+
20492: 'HalftoneDegree', # GDI+
20493: 'HalftoneShape', # GDI+
20494: 'HalftoneMisc', # GDI+
20495: 'HalftoneScreen', # GDI+
20496: 'JPEGQuality', # GDI+
20497: 'GridSize', # GDI+
20498: 'ThumbnailFormat', # GDI+
20499: 'ThumbnailWidth', # GDI+
20500: 'ThumbnailHeight', # GDI+
20501: 'ThumbnailColorDepth', # GDI+
20502: 'ThumbnailPlanes', # GDI+
20503: 'ThumbnailRawBytes', # GDI+
20504: 'ThumbnailSize', # GDI+
20505: 'ThumbnailCompressedSize', # GDI+
20506: 'ColorTransferFunction', # GDI+
20507: 'ThumbnailData',
20512: 'ThumbnailImageWidth', # GDI+
20513: 'ThumbnailImageHeight', # GDI+
20514: 'ThumbnailBitsPerSample', # GDI+
20515: 'ThumbnailCompression',
20516: 'ThumbnailPhotometricInterp', # GDI+
20517: 'ThumbnailImageDescription', # GDI+
20518: 'ThumbnailEquipMake', # GDI+
20519: 'ThumbnailEquipModel', # GDI+
20520: 'ThumbnailStripOffsets', # GDI+
20521: 'ThumbnailOrientation', # GDI+
20522: 'ThumbnailSamplesPerPixel', # GDI+
20523: 'ThumbnailRowsPerStrip', # GDI+
20524: 'ThumbnailStripBytesCount', # GDI+
20525: 'ThumbnailResolutionX',
20526: 'ThumbnailResolutionY',
20527: 'ThumbnailPlanarConfig', # GDI+
20528: 'ThumbnailResolutionUnit',
20529: 'ThumbnailTransferFunction',
20530: 'ThumbnailSoftwareUsed', # GDI+
20531: 'ThumbnailDateTime', # GDI+
20532: 'ThumbnailArtist', # GDI+
20533: 'ThumbnailWhitePoint', # GDI+
20534: 'ThumbnailPrimaryChromaticities', # GDI+
20535: 'ThumbnailYCbCrCoefficients', # GDI+
20536: 'ThumbnailYCbCrSubsampling', # GDI+
20537: 'ThumbnailYCbCrPositioning',
20538: 'ThumbnailRefBlackWhite', # GDI+
20539: 'ThumbnailCopyRight', # GDI+
20545: 'InteroperabilityIndex',
20546: 'InteroperabilityVersion',
20624: 'LuminanceTable',
20625: 'ChrominanceTable',
20736: 'FrameDelay', # GDI+
20737: 'LoopCount', # GDI+
20738: 'GlobalPalette', # GDI+
20739: 'IndexBackground', # GDI+
20740: 'IndexTransparent', # GDI+
20752: 'PixelUnit', # GDI+
20753: 'PixelPerUnitX', # GDI+
20754: 'PixelPerUnitY', # GDI+
20755: 'PaletteHistogram', # GDI+
28672: 'SonyRawFileType', # Sony ARW
28722: 'VignettingCorrParams', # Sony ARW
28725: 'ChromaticAberrationCorrParams', # Sony ARW
28727: 'DistortionCorrParams', # Sony ARW
# Private tags >= 32768
32781: 'ImageID',
32931: 'WangTag1',
32932: 'WangAnnotation',
32933: 'WangTag3',
32934: 'WangTag4',
32953: 'ImageReferencePoints',
32954: 'RegionXformTackPoint',
32955: 'WarpQuadrilateral',
32956: 'AffineTransformMat',
32995: 'Matteing',
32996: 'DataType',
32997: 'ImageDepth',
32998: 'TileDepth',
33300: 'ImageFullWidth',
33301: 'ImageFullLength',
33302: 'TextureFormat',
33303: 'TextureWrapModes',
33304: 'FieldOfViewCotangent',
33305: 'MatrixWorldToScreen',
33306: 'MatrixWorldToCamera',
33405: 'Model2',
33421: 'CFARepeatPatternDim',
33422: 'CFAPattern',
33423: 'BatteryLevel',
33424: 'KodakIFD',
33434: 'ExposureTime',
33437: 'FNumber',
33432: 'Copyright',
33445: 'MDFileTag',
33446: 'MDScalePixel',
33447: 'MDColorTable',
33448: 'MDLabName',
33449: 'MDSampleInfo',
33450: 'MDPrepDate',
33451: 'MDPrepTime',
33452: 'MDFileUnits',
33550: 'ModelPixelScaleTag',
33589: 'AdventScale',
33590: 'AdventRevision',
33628: 'UIC1tag', # Metamorph Universal Imaging Corp STK
33629: 'UIC2tag',
33630: 'UIC3tag',
33631: 'UIC4tag',
33723: 'IPTCNAA',
33858: 'ExtendedTagsOffset', # DEFF points IFD with private tags
33918: 'IntergraphPacketData', # INGRPacketDataTag
33919: 'IntergraphFlagRegisters', # INGRFlagRegisters
33920: 'IntergraphMatrixTag', # IrasBTransformationMatrix
33921: 'INGRReserved',
33922: 'ModelTiepointTag',
33923: 'LeicaMagic',
34016: 'Site',
34017: 'ColorSequence',
34018: 'IT8Header',
34019: 'RasterPadding',
34020: 'BitsPerRunLength',
34021: 'BitsPerExtendedRunLength',
34022: 'ColorTable',
34023: 'ImageColorIndicator',
34024: 'BackgroundColorIndicator',
34025: 'ImageColorValue',
34026: 'BackgroundColorValue',
34027: 'PixelIntensityRange',
34028: 'TransparencyIndicator',
34029: 'ColorCharacterization',
34030: 'HCUsage',
34031: 'TrapIndicator',
34032: 'CMYKEquivalent',
34118: 'CZ_SEM', # Zeiss SEM
34152: 'AFCP_IPTC',
34232: 'PixelMagicJBIGOptions',
34263: 'JPLCartoIFD',
34122: 'IPLAB', # number of images
34264: 'ModelTransformationTag',
34306: 'WB_GRGBLevels', # Leaf MOS
34310: 'LeafData',
34361: 'MM_Header',
34362: 'MM_Stamp',
34363: 'MM_Unknown',
34377: 'ImageResources', # Photoshop
34386: 'MM_UserBlock',
34412: 'CZ_LSMINFO',
34665: 'ExifTag',
34675: 'InterColorProfile', # ICCProfile
34680: 'FEI_SFEG', #
34682: 'FEI_HELIOS', #
34683: 'FEI_TITAN', #
34687: 'FXExtensions',
34688: 'MultiProfiles',
34689: 'SharedData',
34690: 'T88Options',
34710: 'MarCCD', # offset to MarCCD header
34732: 'ImageLayer',
34735: 'GeoKeyDirectoryTag',
34736: 'GeoDoubleParamsTag',
34737: 'GeoAsciiParamsTag',
34750: 'JBIGOptions',
34821: 'PIXTIFF', # ? Pixel Translations Inc
34850: 'ExposureProgram',
34852: 'SpectralSensitivity',
34853: 'GPSTag', # GPSIFD
34855: 'ISOSpeedRatings',
34856: 'OECF',
34857: 'Interlace',
34858: 'TimeZoneOffset',
34859: 'SelfTimerMode',
34864: 'SensitivityType',
34865: 'StandardOutputSensitivity',
34866: 'RecommendedExposureIndex',
34867: 'ISOSpeed',
34868: 'ISOSpeedLatitudeyyy',
34869: 'ISOSpeedLatitudezzz',
34908: 'HylaFAXFaxRecvParams',
34909: 'HylaFAXFaxSubAddress',
34910: 'HylaFAXFaxRecvTime',
34911: 'FaxDcs',
34929: 'FedexEDR',
34954: 'LeafSubIFD',
34959: 'Aphelion1',
34960: 'Aphelion2',
34961: 'AphelionInternal', # ADCIS
36864: 'ExifVersion',
36867: 'DateTimeOriginal',
36868: 'DateTimeDigitized',
36873: 'GooglePlusUploadCode',
36880: 'OffsetTime',
36881: 'OffsetTimeOriginal',
36882: 'OffsetTimeDigitized',
# TODO: Pilatus/CHESS/TV6 36864..37120 conflicting with Exif tags
# 36864: 'TVX ?',
# 36865: 'TVX_NumExposure',
# 36866: 'TVX_NumBackground',
# 36867: 'TVX_ExposureTime',
# 36868: 'TVX_BackgroundTime',
# 36870: 'TVX ?',
# 36873: 'TVX_SubBpp',
# 36874: 'TVX_SubWide',
# 36875: 'TVX_SubHigh',
# 36876: 'TVX_BlackLevel',
# 36877: 'TVX_DarkCurrent',
# 36878: 'TVX_ReadNoise',
# 36879: 'TVX_DarkCurrentNoise',
# 36880: 'TVX_BeamMonitor',
# 37120: 'TVX_UserVariables', # A/D values
37121: 'ComponentsConfiguration',
37122: 'CompressedBitsPerPixel',
37377: 'ShutterSpeedValue',
37378: 'ApertureValue',
37379: 'BrightnessValue',
37380: 'ExposureBiasValue',
37381: 'MaxApertureValue',
37382: 'SubjectDistance',
37383: 'MeteringMode',
37384: 'LightSource',
37385: 'Flash',
37386: 'FocalLength',
37387: 'FlashEnergy_', # 37387
37388: 'SpatialFrequencyResponse_', # 37388
37389: 'Noise',
37390: 'FocalPlaneXResolution',
37391: 'FocalPlaneYResolution',
37392: 'FocalPlaneResolutionUnit',
37393: 'ImageNumber',
37394: 'SecurityClassification',
37395: 'ImageHistory',
37396: 'SubjectLocation',
37397: 'ExposureIndex',
37398: 'TIFFEPStandardID',
37399: 'SensingMethod',
37434: 'CIP3DataFile',
37435: 'CIP3Sheet',
37436: 'CIP3Side',
37439: 'StoNits',
37500: 'MakerNote',
37510: 'UserComment',
37520: 'SubsecTime',
37521: 'SubsecTimeOriginal',
37522: 'SubsecTimeDigitized',
37679: 'MODIText', # Microsoft Office Document Imaging
37680: 'MODIOLEPropertySetStorage',
37681: 'MODIPositioning',
37706: 'TVIPS', # offset to TemData structure
37707: 'TVIPS1',
37708: 'TVIPS2', # same TemData structure as undefined
37724: 'ImageSourceData', # Photoshop
37888: 'Temperature',
37889: 'Humidity',
37890: 'Pressure',
37891: 'WaterDepth',
37892: 'Acceleration',
37893: 'CameraElevationAngle',
40001: 'MC_IpWinScal', # Media Cybernetics
40100: 'MC_IdOld',
40965: 'InteroperabilityTag', # InteropOffset
40091: 'XPTitle',
40092: 'XPComment',
40093: 'XPAuthor',
40094: 'XPKeywords',
40095: 'XPSubject',
40960: 'FlashpixVersion',
40961: 'ColorSpace',
40962: 'PixelXDimension',
40963: 'PixelYDimension',
40964: 'RelatedSoundFile',
40976: 'SamsungRawPointersOffset',
40977: 'SamsungRawPointersLength',
41217: 'SamsungRawByteOrder',
41218: 'SamsungRawUnknown',
41483: 'FlashEnergy',
41484: 'SpatialFrequencyResponse',
41485: 'Noise_', # 37389
41486: 'FocalPlaneXResolution_', # 37390
41487: 'FocalPlaneYResolution_', # 37391
41488: 'FocalPlaneResolutionUnit_', # 37392
41489: 'ImageNumber_', # 37393
41490: 'SecurityClassification_', # 37394
41491: 'ImageHistory_', # 37395
41492: 'SubjectLocation_', # 37395
41493: 'ExposureIndex_ ', # 37397
41494: 'TIFF-EPStandardID',
41495: 'SensingMethod_', # 37399
41728: 'FileSource',
41729: 'SceneType',
41730: 'CFAPattern_', # 33422
41985: 'CustomRendered',
41986: 'ExposureMode',
41987: 'WhiteBalance',
41988: 'DigitalZoomRatio',
41989: 'FocalLengthIn35mmFilm',
41990: 'SceneCaptureType',
41991: 'GainControl',
41992: 'Contrast',
41993: 'Saturation',
41994: 'Sharpness',
41995: 'DeviceSettingDescription',
41996: 'SubjectDistanceRange',
42016: 'ImageUniqueID',
42032: 'CameraOwnerName',
42033: 'BodySerialNumber',
42034: 'LensSpecification',
42035: 'LensMake',
42036: 'LensModel',
42037: 'LensSerialNumber',
42112: 'GDAL_METADATA',
42113: 'GDAL_NODATA',
42240: 'Gamma',
43314: 'NIHImageHeader',
44992: 'ExpandSoftware',
44993: 'ExpandLens',
44994: 'ExpandFilm',
44995: 'ExpandFilterLens',
44996: 'ExpandScanner',
44997: 'ExpandFlashLamp',
48129: 'PixelFormat', # HDP and WDP
48130: 'Transformation',
48131: 'Uncompressed',
48132: 'ImageType',
48256: 'ImageWidth_', # 256
48257: 'ImageHeight_',
48258: 'WidthResolution',
48259: 'HeightResolution',
48320: 'ImageOffset',
48321: 'ImageByteCount',
48322: 'AlphaOffset',
48323: 'AlphaByteCount',
48324: 'ImageDataDiscard',
48325: 'AlphaDataDiscard',
50215: 'OceScanjobDescription',
50216: 'OceApplicationSelector',
50217: 'OceIdentificationNumber',
50218: 'OceImageLogicCharacteristics',
50255: 'Annotations',
50288: 'MC_Id', # Media Cybernetics
50289: 'MC_XYPosition',
50290: 'MC_ZPosition',
50291: 'MC_XYCalibration',
50292: 'MC_LensCharacteristics',
50293: 'MC_ChannelName',
50294: 'MC_ExcitationWavelength',
50295: 'MC_TimeStamp',
50296: 'MC_FrameProperties',
50341: 'PrintImageMatching',
50495: 'PCO_RAW', # TODO: PCO CamWare
50547: 'OriginalFileName',
50560: 'USPTO_OriginalContentType', # US Patent Office
50561: 'USPTO_RotationCode',
50656: 'CR2CFAPattern',
50706: 'DNGVersion', # DNG 50706 .. 51112
50707: 'DNGBackwardVersion',
50708: 'UniqueCameraModel',
50709: 'LocalizedCameraModel',
50710: 'CFAPlaneColor',
50711: 'CFALayout',
50712: 'LinearizationTable',
50713: 'BlackLevelRepeatDim',
50714: 'BlackLevel',
50715: 'BlackLevelDeltaH',
50716: 'BlackLevelDeltaV',
50717: 'WhiteLevel',
50718: 'DefaultScale',
50719: 'DefaultCropOrigin',
50720: 'DefaultCropSize',
50721: 'ColorMatrix1',
50722: 'ColorMatrix2',
50723: 'CameraCalibration1',
50724: 'CameraCalibration2',
50725: 'ReductionMatrix1',
50726: 'ReductionMatrix2',
50727: 'AnalogBalance',
50728: 'AsShotNeutral',
50729: 'AsShotWhiteXY',
50730: 'BaselineExposure',
50731: 'BaselineNoise',
50732: 'BaselineSharpness',
50733: 'BayerGreenSplit',
50734: 'LinearResponseLimit',
50735: 'CameraSerialNumber',
50736: 'LensInfo',
50737: 'ChromaBlurRadius',
50738: 'AntiAliasStrength',
50739: 'ShadowScale',
50740: 'DNGPrivateData',
50741: 'MakerNoteSafety',
50752: 'RawImageSegmentation',
50778: 'CalibrationIlluminant1',
50779: 'CalibrationIlluminant2',
50780: 'BestQualityScale',
50781: 'RawDataUniqueID',
50784: 'AliasLayerMetadata',
50827: 'OriginalRawFileName',
50828: 'OriginalRawFileData',
50829: 'ActiveArea',
50830: 'MaskedAreas',
50831: 'AsShotICCProfile',
50832: 'AsShotPreProfileMatrix',
50833: 'CurrentICCProfile',
50834: 'CurrentPreProfileMatrix',
50838: 'IJMetadataByteCounts',
50839: 'IJMetadata',
50844: 'RPCCoefficientTag',
50879: 'ColorimetricReference',
50885: 'SRawType',
50898: 'PanasonicTitle',
50899: 'PanasonicTitle2',
50931: 'CameraCalibrationSignature',
50932: 'ProfileCalibrationSignature',
50933: 'ProfileIFD',
50934: 'AsShotProfileName',
50935: 'NoiseReductionApplied',
50936: 'ProfileName',
50937: 'ProfileHueSatMapDims',
50938: 'ProfileHueSatMapData1',
50939: 'ProfileHueSatMapData2',
50940: 'ProfileToneCurve',
50941: 'ProfileEmbedPolicy',
50942: 'ProfileCopyright',
50964: 'ForwardMatrix1',
50965: 'ForwardMatrix2',
50966: 'PreviewApplicationName',
50967: 'PreviewApplicationVersion',
50968: 'PreviewSettingsName',
50969: 'PreviewSettingsDigest',
50970: 'PreviewColorSpace',
50971: 'PreviewDateTime',
50972: 'RawImageDigest',
50973: 'OriginalRawFileDigest',
50974: 'SubTileBlockSize',
50975: 'RowInterleaveFactor',
50981: 'ProfileLookTableDims',
50982: 'ProfileLookTableData',
51008: 'OpcodeList1',
51009: 'OpcodeList2',
51022: 'OpcodeList3',
51023: 'FibicsXML', #
51041: 'NoiseProfile',
51043: 'TimeCodes',
51044: 'FrameRate',
51058: 'TStop',
51081: 'ReelName',
51089: 'OriginalDefaultFinalSize',
51090: 'OriginalBestQualitySize',
51091: 'OriginalDefaultCropSize',
51105: 'CameraLabel',
51107: 'ProfileHueSatMapEncoding',
51108: 'ProfileLookTableEncoding',
51109: 'BaselineExposureOffset',
51110: 'DefaultBlackRender',
51111: 'NewRawImageDigest',
51112: 'RawToPreviewGain',
51125: 'DefaultUserCrop',
51123: 'MicroManagerMetadata',
59932: 'Padding',
59933: 'OffsetSchema',
# Reusable Tags 65000-65535
# 65000: Dimap_Document XML
# 65000-65112: Photoshop Camera RAW EXIF tags
# 65000: 'OwnerName',
# 65001: 'SerialNumber',
# 65002: 'Lens',
# 65024: 'KDC_IFD',
# 65100: 'RawFile',
# 65101: 'Converter',
# 65102: 'WhiteBalance',
# 65105: 'Exposure',
# 65106: 'Shadows',
# 65107: 'Brightness',
# 65108: 'Contrast',
# 65109: 'Saturation',
# 65110: 'Sharpness',
# 65111: 'Smoothness',
# 65112: 'MoireFilter',
65200: 'FlexXML', #
65563: 'PerSample',
}
def TAG_NAMES():
return {v: c for c, v in TIFF.TAGS.items()}
def TAG_READERS():
# Map TIFF tag codes to import functions
return {
320: read_colormap,
# 700: read_bytes, # read_utf8,
# 34377: read_bytes,
33723: read_bytes,
# 34675: read_bytes,
33628: read_uic1tag, # Universal Imaging Corp STK
33629: read_uic2tag,
33630: read_uic3tag,
33631: read_uic4tag,
34118: read_cz_sem, # Carl Zeiss SEM
34361: read_mm_header, # Olympus FluoView
34362: read_mm_stamp,
34363: read_numpy, # MM_Unknown
34386: read_numpy, # MM_UserBlock
34412: read_cz_lsminfo, # Carl Zeiss LSM
34680: read_fei_metadata, # S-FEG
34682: read_fei_metadata, # Helios NanoLab
37706: read_tvips_header, # TVIPS EMMENU
37724: read_bytes, # ImageSourceData
33923: read_bytes, # read_leica_magic
43314: read_nih_image_header,
# 40001: read_bytes,
40100: read_bytes,
50288: read_bytes,
50296: read_bytes,
50839: read_bytes,
51123: read_json,
34665: read_exif_ifd,
34853: read_gps_ifd,
40965: read_interoperability_ifd,
}
def TAG_TUPLE():
# Tags whose values must be stored as tuples
return frozenset((273, 279, 324, 325, 530, 531, 34736))
def TAG_ATTRIBUTES():
# Map tag codes to TiffPage attribute names
return {
'ImageWidth': 'imagewidth',
'ImageLength': 'imagelength',
'BitsPerSample': 'bitspersample',
'Compression': 'compression',
'PlanarConfiguration': 'planarconfig',
'FillOrder': 'fillorder',
'PhotometricInterpretation': 'photometric',
'ColorMap': 'colormap',
'ImageDescription': 'description',
'ImageDescription1': 'description1',
'SamplesPerPixel': 'samplesperpixel',
'RowsPerStrip': 'rowsperstrip',
'Software': 'software',
'Predictor': 'predictor',
'TileWidth': 'tilewidth',
'TileLength': 'tilelength',
'ExtraSamples': 'extrasamples',
'SampleFormat': 'sampleformat',
'ImageDepth': 'imagedepth',
'TileDepth': 'tiledepth',
}
def TAG_ENUM():
return {
# 254: TIFF.FILETYPE,
255: TIFF.OFILETYPE,
259: TIFF.COMPRESSION,
262: TIFF.PHOTOMETRIC,
263: TIFF.THRESHHOLD,
266: TIFF.FILLORDER,
274: TIFF.ORIENTATION,
284: TIFF.PLANARCONFIG,
290: TIFF.GRAYRESPONSEUNIT,
# 292: TIFF.GROUP3OPT,
# 293: TIFF.GROUP4OPT,
296: TIFF.RESUNIT,
300: TIFF.COLORRESPONSEUNIT,
317: TIFF.PREDICTOR,
338: TIFF.EXTRASAMPLE,
339: TIFF.SAMPLEFORMAT,
# 512: TIFF.JPEGPROC,
# 531: TIFF.YCBCRPOSITION,
}
def FILETYPE():
class FILETYPE(enum.IntFlag):
# Python 3.6 only
UNDEFINED = 0
REDUCEDIMAGE = 1
PAGE = 2
MASK = 4
return FILETYPE
def OFILETYPE():
class OFILETYPE(enum.IntEnum):
UNDEFINED = 0
IMAGE = 1
REDUCEDIMAGE = 2
PAGE = 3
return OFILETYPE
def COMPRESSION():
class COMPRESSION(enum.IntEnum):
NONE = 1 # Uncompressed
CCITTRLE = 2 # CCITT 1D
CCITT_T4 = 3 # 'T4/Group 3 Fax',
CCITT_T6 = 4 # 'T6/Group 4 Fax',
LZW = 5
OJPEG = 6 # old-style JPEG
JPEG = 7
ADOBE_DEFLATE = 8
JBIG_BW = 9
JBIG_COLOR = 10
JPEG_99 = 99
KODAK_262 = 262
NEXT = 32766
SONY_ARW = 32767
PACKED_RAW = 32769
SAMSUNG_SRW = 32770
CCIRLEW = 32771
SAMSUNG_SRW2 = 32772
PACKBITS = 32773
THUNDERSCAN = 32809
IT8CTPAD = 32895
IT8LW = 32896
IT8MP = 32897
IT8BL = 32898
PIXARFILM = 32908
PIXARLOG = 32909
DEFLATE = 32946
DCS = 32947
APERIO_JP2000_YCBC = 33003 # Leica Aperio
APERIO_JP2000_RGB = 33005 # Leica Aperio
JBIG = 34661
SGILOG = 34676
SGILOG24 = 34677
JPEG2000 = 34712
NIKON_NEF = 34713
JBIG2 = 34715
MDI_BINARY = 34718 # 'Microsoft Document Imaging
MDI_PROGRESSIVE = 34719 # 'Microsoft Document Imaging
MDI_VECTOR = 34720 # 'Microsoft Document Imaging
JPEG_LOSSY = 34892
LZMA = 34925
ZSTD = 34926
OPS_PNG = 34933 # Objective Pathology Services
OPS_JPEGXR = 34934 # Objective Pathology Services
PIXTIFF = 50013
KODAK_DCR = 65000
PENTAX_PEF = 65535
# def __bool__(self): return self != 1 # Python 3.6 only
return COMPRESSION
def PHOTOMETRIC():
class PHOTOMETRIC(enum.IntEnum):
MINISWHITE = 0
MINISBLACK = 1
RGB = 2
PALETTE = 3
MASK = 4
SEPARATED = 5 # CMYK
YCBCR = 6
CIELAB = 8
ICCLAB = 9
ITULAB = 10
CFA = 32803 # Color Filter Array
LOGL = 32844
LOGLUV = 32845
LINEAR_RAW = 34892
return PHOTOMETRIC
def THRESHHOLD():
class THRESHHOLD(enum.IntEnum):
BILEVEL = 1
HALFTONE = 2
ERRORDIFFUSE = 3
return THRESHHOLD
def FILLORDER():
class FILLORDER(enum.IntEnum):
MSB2LSB = 1
LSB2MSB = 2
return FILLORDER
def ORIENTATION():
class ORIENTATION(enum.IntEnum):
TOPLEFT = 1
TOPRIGHT = 2
BOTRIGHT = 3
BOTLEFT = 4
LEFTTOP = 5
RIGHTTOP = 6
RIGHTBOT = 7
LEFTBOT = 8
return ORIENTATION
def PLANARCONFIG():
class PLANARCONFIG(enum.IntEnum):
CONTIG = 1
SEPARATE = 2
return PLANARCONFIG
def GRAYRESPONSEUNIT():
class GRAYRESPONSEUNIT(enum.IntEnum):
_10S = 1
_100S = 2
_1000S = 3
_10000S = 4
_100000S = 5
return GRAYRESPONSEUNIT
def GROUP4OPT():
class GROUP4OPT(enum.IntEnum):
UNCOMPRESSED = 2
return GROUP4OPT
def RESUNIT():
class RESUNIT(enum.IntEnum):
NONE = 1
INCH = 2
CENTIMETER = 3
# def __bool__(self): return self != 1 # Python 3.6 only
return RESUNIT
def COLORRESPONSEUNIT():
class COLORRESPONSEUNIT(enum.IntEnum):
_10S = 1
_100S = 2
_1000S = 3
_10000S = 4
_100000S = 5
return COLORRESPONSEUNIT
def PREDICTOR():
class PREDICTOR(enum.IntEnum):
NONE = 1
HORIZONTAL = 2
FLOATINGPOINT = 3
# def __bool__(self): return self != 1 # Python 3.6 only
return PREDICTOR
def EXTRASAMPLE():
class EXTRASAMPLE(enum.IntEnum):
UNSPECIFIED = 0
ASSOCALPHA = 1
UNASSALPHA = 2
return EXTRASAMPLE
def SAMPLEFORMAT():
class SAMPLEFORMAT(enum.IntEnum):
UINT = 1
INT = 2
IEEEFP = 3
VOID = 4
COMPLEXINT = 5
COMPLEXIEEEFP = 6
return SAMPLEFORMAT
def DATATYPES():
class DATATYPES(enum.IntEnum):
NOTYPE = 0
BYTE = 1
ASCII = 2
SHORT = 3
LONG = 4
RATIONAL = 5
SBYTE = 6
UNDEFINED = 7
SSHORT = 8
SLONG = 9
SRATIONAL = 10
FLOAT = 11
DOUBLE = 12
IFD = 13
UNICODE = 14
COMPLEX = 15
LONG8 = 16
SLONG8 = 17
IFD8 = 18
return DATATYPES
def DATA_FORMATS():
# Map TIFF DATATYPES to Python struct formats
return {
1: '1B', # BYTE 8-bit unsigned integer.
2: '1s', # ASCII 8-bit byte that contains a 7-bit ASCII code;
# the last byte must be NULL (binary zero).
3: '1H', # SHORT 16-bit (2-byte) unsigned integer
4: '1I', # LONG 32-bit (4-byte) unsigned integer.
5: '2I', # RATIONAL Two LONGs: the first represents the numerator
# of a fraction; the second, the denominator.
6: '1b', # SBYTE An 8-bit signed (twos-complement) integer.
7: '1B', # UNDEFINED An 8-bit byte that may contain anything,
# depending on the definition of the field.
8: '1h', # SSHORT A 16-bit (2-byte) signed (twos-complement)
# integer.
9: '1i', # SLONG A 32-bit (4-byte) signed (twos-complement)
# integer.
10: '2i', # SRATIONAL Two SLONGs: the first represents the
# numerator of a fraction, the second the denominator.
11: '1f', # FLOAT Single precision (4-byte) IEEE format.
12: '1d', # DOUBLE Double precision (8-byte) IEEE format.
13: '1I', # IFD unsigned 4 byte IFD offset.
# 14: '', # UNICODE
# 15: '', # COMPLEX
16: '1Q', # LONG8 unsigned 8 byte integer (BigTiff)
17: '1q', # SLONG8 signed 8 byte integer (BigTiff)
18: '1Q', # IFD8 unsigned 8 byte IFD offset (BigTiff)
}
def DATA_DTYPES():
# Map numpy dtypes to TIFF DATATYPES
return {'B': 1, 's': 2, 'H': 3, 'I': 4, '2I': 5, 'b': 6,
'h': 8, 'i': 9, '2i': 10, 'f': 11, 'd': 12, 'Q': 16, 'q': 17}
def SAMPLE_DTYPES():
# Map TIFF SampleFormats and BitsPerSample to numpy dtype
return {
(1, 1): '?', # bitmap
(1, 2): 'B',
(1, 3): 'B',
(1, 4): 'B',
(1, 5): 'B',
(1, 6): 'B',
(1, 7): 'B',
(1, 8): 'B',
(1, 9): 'H',
(1, 10): 'H',
(1, 11): 'H',
(1, 12): 'H',
(1, 13): 'H',
(1, 14): 'H',
(1, 15): 'H',
(1, 16): 'H',
(1, 17): 'I',
(1, 18): 'I',
(1, 19): 'I',
(1, 20): 'I',
(1, 21): 'I',
(1, 22): 'I',
(1, 23): 'I',
(1, 24): 'I',
(1, 25): 'I',
(1, 26): 'I',
(1, 27): 'I',
(1, 28): 'I',
(1, 29): 'I',
(1, 30): 'I',
(1, 31): 'I',
(1, 32): 'I',
(1, 64): 'Q',
(2, 8): 'b',
(2, 16): 'h',
(2, 32): 'i',
(2, 64): 'q',
(3, 16): 'e',
(3, 32): 'f',
(3, 64): 'd',
(6, 64): 'F',
(6, 128): 'D',
(1, (5, 6, 5)): 'B',
}
def COMPESSORS():
# Map COMPRESSION to compress functions and default compression levels
class Compressors(object):
"""Delay import compressor functions."""
def __init__(self):
self._compressors = {8: (zlib.compress, 6),
32946: (zlib.compress, 6)}
def __getitem__(self, key):
if key in self._compressors:
return self._compressors[key]
if key == 34925:
try:
import lzma # delayed import
except ImportError:
try:
import backports.lzma as lzma # delayed import
except ImportError:
raise KeyError
def lzma_compress(x, level):
return lzma.compress(x)
self._compressors[key] = lzma_compress, 0
return lzma_compress, 0
if key == 34926:
try:
import zstd # delayed import
except ImportError:
raise KeyError
self._compressors[key] = zstd.compress, 9
return zstd.compress, 9
raise KeyError
def __contains__(self, key):
try:
self[key]
return True
except KeyError:
return False
return Compressors()
def DECOMPESSORS():
# Map COMPRESSION to decompress functions
class Decompressors(object):
"""Delay import decompressor functions."""
def __init__(self):
self._decompressors = {None: identityfunc,
1: identityfunc,
5: decode_lzw,
8: zlib.decompress,
32773: decode_packbits,
32946: zlib.decompress}
def __getitem__(self, key):
if key in self._decompressors:
return self._decompressors[key]
if key == 7:
try:
from imagecodecs import jpeg, jpeg_12
except ImportError:
raise KeyError
def decode_jpeg(x, table, bps, colorspace=None):
if bps == 8:
return jpeg.decode_jpeg(x, table, colorspace)
elif bps == 12:
return jpeg_12.decode_jpeg_12(x, table, colorspace)
else:
raise ValueError('bitspersample not supported')
self._decompressors[key] = decode_jpeg
return decode_jpeg
if key == 34925:
try:
import lzma # delayed import
except ImportError:
try:
import backports.lzma as lzma # delayed import
except ImportError:
raise KeyError
self._decompressors[key] = lzma.decompress
return lzma.decompress
if key == 34926:
try:
import zstd # delayed import
except ImportError:
raise KeyError
self._decompressors[key] = zstd.decompress
return zstd.decompress
raise KeyError
def __contains__(self, item):
try:
self[item]
return True
except KeyError:
return False
return Decompressors()
def FRAME_ATTRS():
# Attributes that a TiffFrame shares with its keyframe
return set('shape ndim size dtype axes is_final'.split())
def FILE_FLAGS():
# TiffFile and TiffPage 'is_\*' attributes
exclude = set('reduced final memmappable contiguous tiled '
'chroma_subsampled'.split())
return set(a[3:] for a in dir(TiffPage)
if a[:3] == 'is_' and a[3:] not in exclude)
def FILE_EXTENSIONS():
# TIFF file extensions
return tuple('tif tiff ome.tif lsm stk qptiff pcoraw '
'gel seq svs bif tf8 tf2 btf'.split())
def FILEOPEN_FILTER():
# String for use in Windows File Open box
return [('%s files' % ext.upper(), '*.%s' % ext)
for ext in TIFF.FILE_EXTENSIONS] + [('allfiles', '*')]
def AXES_LABELS():
# TODO: is there a standard for character axes labels?
axes = {
'X': 'width',
'Y': 'height',
'Z': 'depth',
'S': 'sample', # rgb(a)
'I': 'series', # general sequence, plane, page, IFD
'T': 'time',
'C': 'channel', # color, emission wavelength
'A': 'angle',
'P': 'phase', # formerly F # P is Position in LSM!
'R': 'tile', # region, point, mosaic
'H': 'lifetime', # histogram
'E': 'lambda', # excitation wavelength
'L': 'exposure', # lux
'V': 'event',
'Q': 'other',
'M': 'mosaic', # LSM 6
}
axes.update(dict((v, k) for k, v in axes.items()))
return axes
def ANDOR_TAGS():
# Andor Technology tags #4864 - 5030
return set(range(4864, 5030))
def EXIF_TAGS():
tags = {
# 65000 - 65112 Photoshop Camera RAW EXIF tags
65000: 'OwnerName',
65001: 'SerialNumber',
65002: 'Lens',
65100: 'RawFile',
65101: 'Converter',
65102: 'WhiteBalance',
65105: 'Exposure',
65106: 'Shadows',
65107: 'Brightness',
65108: 'Contrast',
65109: 'Saturation',
65110: 'Sharpness',
65111: 'Smoothness',
65112: 'MoireFilter',
}
tags.update(TIFF.TAGS)
return tags
def GPS_TAGS():
return {
0: 'GPSVersionID',
1: 'GPSLatitudeRef',
2: 'GPSLatitude',
3: 'GPSLongitudeRef',
4: 'GPSLongitude',
5: 'GPSAltitudeRef',
6: 'GPSAltitude',
7: 'GPSTimeStamp',
8: 'GPSSatellites',
9: 'GPSStatus',
10: 'GPSMeasureMode',
11: 'GPSDOP',
12: 'GPSSpeedRef',
13: 'GPSSpeed',
14: 'GPSTrackRef',
15: 'GPSTrack',
16: 'GPSImgDirectionRef',
17: 'GPSImgDirection',
18: 'GPSMapDatum',
19: 'GPSDestLatitudeRef',
20: 'GPSDestLatitude',
21: 'GPSDestLongitudeRef',
22: 'GPSDestLongitude',
23: 'GPSDestBearingRef',
24: 'GPSDestBearing',
25: 'GPSDestDistanceRef',
26: 'GPSDestDistance',
27: 'GPSProcessingMethod',
28: 'GPSAreaInformation',
29: 'GPSDateStamp',
30: 'GPSDifferential',
31: 'GPSHPositioningError',
}
def IOP_TAGS():
return {
1: 'InteroperabilityIndex',
2: 'InteroperabilityVersion',
4096: 'RelatedImageFileFormat',
4097: 'RelatedImageWidth',
4098: 'RelatedImageLength',
}
def GEO_KEYS():
return {
1024: 'GTModelTypeGeoKey',
1025: 'GTRasterTypeGeoKey',
1026: 'GTCitationGeoKey',
2048: 'GeographicTypeGeoKey',
2049: 'GeogCitationGeoKey',
2050: 'GeogGeodeticDatumGeoKey',
2051: 'GeogPrimeMeridianGeoKey',
2052: 'GeogLinearUnitsGeoKey',
2053: 'GeogLinearUnitSizeGeoKey',
2054: 'GeogAngularUnitsGeoKey',
2055: 'GeogAngularUnitsSizeGeoKey',
2056: 'GeogEllipsoidGeoKey',
2057: 'GeogSemiMajorAxisGeoKey',
2058: 'GeogSemiMinorAxisGeoKey',
2059: 'GeogInvFlatteningGeoKey',
2060: 'GeogAzimuthUnitsGeoKey',
2061: 'GeogPrimeMeridianLongGeoKey',
2062: 'GeogTOWGS84GeoKey',
3059: 'ProjLinearUnitsInterpCorrectGeoKey', # GDAL
3072: 'ProjectedCSTypeGeoKey',
3073: 'PCSCitationGeoKey',
3074: 'ProjectionGeoKey',
3075: 'ProjCoordTransGeoKey',
3076: 'ProjLinearUnitsGeoKey',
3077: 'ProjLinearUnitSizeGeoKey',
3078: 'ProjStdParallel1GeoKey',
3079: 'ProjStdParallel2GeoKey',
3080: 'ProjNatOriginLongGeoKey',
3081: 'ProjNatOriginLatGeoKey',
3082: 'ProjFalseEastingGeoKey',
3083: 'ProjFalseNorthingGeoKey',
3084: 'ProjFalseOriginLongGeoKey',
3085: 'ProjFalseOriginLatGeoKey',
3086: 'ProjFalseOriginEastingGeoKey',
3087: 'ProjFalseOriginNorthingGeoKey',
3088: 'ProjCenterLongGeoKey',
3089: 'ProjCenterLatGeoKey',
3090: 'ProjCenterEastingGeoKey',
3091: 'ProjFalseOriginNorthingGeoKey',
3092: 'ProjScaleAtNatOriginGeoKey',
3093: 'ProjScaleAtCenterGeoKey',
3094: 'ProjAzimuthAngleGeoKey',
3095: 'ProjStraightVertPoleLongGeoKey',
3096: 'ProjRectifiedGridAngleGeoKey',
4096: 'VerticalCSTypeGeoKey',
4097: 'VerticalCitationGeoKey',
4098: 'VerticalDatumGeoKey',
4099: 'VerticalUnitsGeoKey',
}
def GEO_CODES():
try:
from .tifffile_geodb import GEO_CODES # delayed import
except (ImportError, ValueError):
try:
from tifffile_geodb import GEO_CODES # delayed import
except (ImportError, ValueError):
GEO_CODES = {}
return GEO_CODES
def CZ_LSMINFO():
return [
('MagicNumber', 'u4'),
('StructureSize', 'i4'),
('DimensionX', 'i4'),
('DimensionY', 'i4'),
('DimensionZ', 'i4'),
('DimensionChannels', 'i4'),
('DimensionTime', 'i4'),
('DataType', 'i4'), # DATATYPES
('ThumbnailX', 'i4'),
('ThumbnailY', 'i4'),
('VoxelSizeX', 'f8'),
('VoxelSizeY', 'f8'),
('VoxelSizeZ', 'f8'),
('OriginX', 'f8'),
('OriginY', 'f8'),
('OriginZ', 'f8'),
('ScanType', 'u2'),
('SpectralScan', 'u2'),
('TypeOfData', 'u4'), # TYPEOFDATA
('OffsetVectorOverlay', 'u4'),
('OffsetInputLut', 'u4'),
('OffsetOutputLut', 'u4'),
('OffsetChannelColors', 'u4'),
('TimeIntervall', 'f8'),
('OffsetChannelDataTypes', 'u4'),
('OffsetScanInformation', 'u4'), # SCANINFO
('OffsetKsData', 'u4'),
('OffsetTimeStamps', 'u4'),
('OffsetEventList', 'u4'),
('OffsetRoi', 'u4'),
('OffsetBleachRoi', 'u4'),
('OffsetNextRecording', 'u4'),
# LSM 2.0 ends here
('DisplayAspectX', 'f8'),
('DisplayAspectY', 'f8'),
('DisplayAspectZ', 'f8'),
('DisplayAspectTime', 'f8'),
('OffsetMeanOfRoisOverlay', 'u4'),
('OffsetTopoIsolineOverlay', 'u4'),
('OffsetTopoProfileOverlay', 'u4'),
('OffsetLinescanOverlay', 'u4'),
('ToolbarFlags', 'u4'),
('OffsetChannelWavelength', 'u4'),
('OffsetChannelFactors', 'u4'),
('ObjectiveSphereCorrection', 'f8'),
('OffsetUnmixParameters', 'u4'),
# LSM 3.2, 4.0 end here
('OffsetAcquisitionParameters', 'u4'),
('OffsetCharacteristics', 'u4'),
('OffsetPalette', 'u4'),
('TimeDifferenceX', 'f8'),
('TimeDifferenceY', 'f8'),
('TimeDifferenceZ', 'f8'),
('InternalUse1', 'u4'),
('DimensionP', 'i4'),
('DimensionM', 'i4'),
('DimensionsReserved', '16i4'),
('OffsetTilePositions', 'u4'),
('', '9u4'), # Reserved
('OffsetPositions', 'u4'),
# ('', '21u4'), # must be 0
]
def CZ_LSMINFO_READERS():
# Import functions for CZ_LSMINFO sub-records
# TODO: read more CZ_LSMINFO sub-records
return {
'ScanInformation': read_lsm_scaninfo,
'TimeStamps': read_lsm_timestamps,
'EventList': read_lsm_eventlist,
'ChannelColors': read_lsm_channelcolors,
'Positions': read_lsm_floatpairs,
'TilePositions': read_lsm_floatpairs,
'VectorOverlay': None,
'InputLut': None,
'OutputLut': None,
'TimeIntervall': None,
'ChannelDataTypes': None,
'KsData': None,
'Roi': None,
'BleachRoi': None,
'NextRecording': None,
'MeanOfRoisOverlay': None,
'TopoIsolineOverlay': None,
'TopoProfileOverlay': None,
'ChannelWavelength': None,
'SphereCorrection': None,
'ChannelFactors': None,
'UnmixParameters': None,
'AcquisitionParameters': None,
'Characteristics': None,
}
def CZ_LSMINFO_SCANTYPE():
# Map CZ_LSMINFO.ScanType to dimension order
return {
0: 'XYZCT', # 'Stack' normal x-y-z-scan
1: 'XYZCT', # 'Z-Scan' x-z-plane Y=1
2: 'XYZCT', # 'Line'
3: 'XYTCZ', # 'Time Series Plane' time series x-y XYCTZ ? Z=1
4: 'XYZTC', # 'Time Series z-Scan' time series x-z
5: 'XYTCZ', # 'Time Series Mean-of-ROIs'
6: 'XYZTC', # 'Time Series Stack' time series x-y-z
7: 'XYCTZ', # Spline Scan
8: 'XYCZT', # Spline Plane x-z
9: 'XYTCZ', # Time Series Spline Plane x-z
10: 'XYZCT', # 'Time Series Point' point mode
}
def CZ_LSMINFO_DIMENSIONS():
# Map dimension codes to CZ_LSMINFO attribute
return {
'X': 'DimensionX',
'Y': 'DimensionY',
'Z': 'DimensionZ',
'C': 'DimensionChannels',
'T': 'DimensionTime',
'P': 'DimensionP',
'M': 'DimensionM',
}
def CZ_LSMINFO_DATATYPES():
# Description of CZ_LSMINFO.DataType
return {
0: 'varying data types',
1: '8 bit unsigned integer',
2: '12 bit unsigned integer',
5: '32 bit float',
}
def CZ_LSMINFO_TYPEOFDATA():
# Description of CZ_LSMINFO.TypeOfData
return {
0: 'Original scan data',
1: 'Calculated data',
2: '3D reconstruction',
3: 'Topography height map',
}
def CZ_LSMINFO_SCANINFO_ARRAYS():
return {
0x20000000: 'Tracks',
0x30000000: 'Lasers',
0x60000000: 'DetectionChannels',
0x80000000: 'IlluminationChannels',
0xa0000000: 'BeamSplitters',
0xc0000000: 'DataChannels',
0x11000000: 'Timers',
0x13000000: 'Markers',
}
def CZ_LSMINFO_SCANINFO_STRUCTS():
return {
# 0x10000000: 'Recording',
0x40000000: 'Track',
0x50000000: 'Laser',
0x70000000: 'DetectionChannel',
0x90000000: 'IlluminationChannel',
0xb0000000: 'BeamSplitter',
0xd0000000: 'DataChannel',
0x12000000: 'Timer',
0x14000000: 'Marker',
}
def CZ_LSMINFO_SCANINFO_ATTRIBUTES():
return {
# Recording
0x10000001: 'Name',
0x10000002: 'Description',
0x10000003: 'Notes',
0x10000004: 'Objective',
0x10000005: 'ProcessingSummary',
0x10000006: 'SpecialScanMode',
0x10000007: 'ScanType',
0x10000008: 'ScanMode',
0x10000009: 'NumberOfStacks',
0x1000000a: 'LinesPerPlane',
0x1000000b: 'SamplesPerLine',
0x1000000c: 'PlanesPerVolume',
0x1000000d: 'ImagesWidth',
0x1000000e: 'ImagesHeight',
0x1000000f: 'ImagesNumberPlanes',
0x10000010: 'ImagesNumberStacks',
0x10000011: 'ImagesNumberChannels',
0x10000012: 'LinscanXySize',
0x10000013: 'ScanDirection',
0x10000014: 'TimeSeries',
0x10000015: 'OriginalScanData',
0x10000016: 'ZoomX',
0x10000017: 'ZoomY',
0x10000018: 'ZoomZ',
0x10000019: 'Sample0X',
0x1000001a: 'Sample0Y',
0x1000001b: 'Sample0Z',
0x1000001c: 'SampleSpacing',
0x1000001d: 'LineSpacing',
0x1000001e: 'PlaneSpacing',
0x1000001f: 'PlaneWidth',
0x10000020: 'PlaneHeight',
0x10000021: 'VolumeDepth',
0x10000023: 'Nutation',
0x10000034: 'Rotation',
0x10000035: 'Precession',
0x10000036: 'Sample0time',
0x10000037: 'StartScanTriggerIn',
0x10000038: 'StartScanTriggerOut',
0x10000039: 'StartScanEvent',
0x10000040: 'StartScanTime',
0x10000041: 'StopScanTriggerIn',
0x10000042: 'StopScanTriggerOut',
0x10000043: 'StopScanEvent',
0x10000044: 'StopScanTime',
0x10000045: 'UseRois',
0x10000046: 'UseReducedMemoryRois',
0x10000047: 'User',
0x10000048: 'UseBcCorrection',
0x10000049: 'PositionBcCorrection1',
0x10000050: 'PositionBcCorrection2',
0x10000051: 'InterpolationY',
0x10000052: 'CameraBinning',
0x10000053: 'CameraSupersampling',
0x10000054: 'CameraFrameWidth',
0x10000055: 'CameraFrameHeight',
0x10000056: 'CameraOffsetX',
0x10000057: 'CameraOffsetY',
0x10000059: 'RtBinning',
0x1000005a: 'RtFrameWidth',
0x1000005b: 'RtFrameHeight',
0x1000005c: 'RtRegionWidth',
0x1000005d: 'RtRegionHeight',
0x1000005e: 'RtOffsetX',
0x1000005f: 'RtOffsetY',
0x10000060: 'RtZoom',
0x10000061: 'RtLinePeriod',
0x10000062: 'Prescan',
0x10000063: 'ScanDirectionZ',
# Track
0x40000001: 'MultiplexType', # 0 After Line; 1 After Frame
0x40000002: 'MultiplexOrder',
0x40000003: 'SamplingMode', # 0 Sample; 1 Line Avg; 2 Frame Avg
0x40000004: 'SamplingMethod', # 1 Mean; 2 Sum
0x40000005: 'SamplingNumber',
0x40000006: 'Acquire',
0x40000007: 'SampleObservationTime',
0x4000000b: 'TimeBetweenStacks',
0x4000000c: 'Name',
0x4000000d: 'Collimator1Name',
0x4000000e: 'Collimator1Position',
0x4000000f: 'Collimator2Name',
0x40000010: 'Collimator2Position',
0x40000011: 'IsBleachTrack',
0x40000012: 'IsBleachAfterScanNumber',
0x40000013: 'BleachScanNumber',
0x40000014: 'TriggerIn',
0x40000015: 'TriggerOut',
0x40000016: 'IsRatioTrack',
0x40000017: 'BleachCount',
0x40000018: 'SpiCenterWavelength',
0x40000019: 'PixelTime',
0x40000021: 'CondensorFrontlens',
0x40000023: 'FieldStopValue',
0x40000024: 'IdCondensorAperture',
0x40000025: 'CondensorAperture',
0x40000026: 'IdCondensorRevolver',
0x40000027: 'CondensorFilter',
0x40000028: 'IdTransmissionFilter1',
0x40000029: 'IdTransmission1',
0x40000030: 'IdTransmissionFilter2',
0x40000031: 'IdTransmission2',
0x40000032: 'RepeatBleach',
0x40000033: 'EnableSpotBleachPos',
0x40000034: 'SpotBleachPosx',
0x40000035: 'SpotBleachPosy',
0x40000036: 'SpotBleachPosz',
0x40000037: 'IdTubelens',
0x40000038: 'IdTubelensPosition',
0x40000039: 'TransmittedLight',
0x4000003a: 'ReflectedLight',
0x4000003b: 'SimultanGrabAndBleach',
0x4000003c: 'BleachPixelTime',
# Laser
0x50000001: 'Name',
0x50000002: 'Acquire',
0x50000003: 'Power',
# DetectionChannel
0x70000001: 'IntegrationMode',
0x70000002: 'SpecialMode',
0x70000003: 'DetectorGainFirst',
0x70000004: 'DetectorGainLast',
0x70000005: 'AmplifierGainFirst',
0x70000006: 'AmplifierGainLast',
0x70000007: 'AmplifierOffsFirst',
0x70000008: 'AmplifierOffsLast',
0x70000009: 'PinholeDiameter',
0x7000000a: 'CountingTrigger',
0x7000000b: 'Acquire',
0x7000000c: 'PointDetectorName',
0x7000000d: 'AmplifierName',
0x7000000e: 'PinholeName',
0x7000000f: 'FilterSetName',
0x70000010: 'FilterName',
0x70000013: 'IntegratorName',
0x70000014: 'ChannelName',
0x70000015: 'DetectorGainBc1',
0x70000016: 'DetectorGainBc2',
0x70000017: 'AmplifierGainBc1',
0x70000018: 'AmplifierGainBc2',
0x70000019: 'AmplifierOffsetBc1',
0x70000020: 'AmplifierOffsetBc2',
0x70000021: 'SpectralScanChannels',
0x70000022: 'SpiWavelengthStart',
0x70000023: 'SpiWavelengthStop',
0x70000026: 'DyeName',
0x70000027: 'DyeFolder',
# IlluminationChannel
0x90000001: 'Name',
0x90000002: 'Power',
0x90000003: 'Wavelength',
0x90000004: 'Aquire',
0x90000005: 'DetchannelName',
0x90000006: 'PowerBc1',
0x90000007: 'PowerBc2',
# BeamSplitter
0xb0000001: 'FilterSet',
0xb0000002: 'Filter',
0xb0000003: 'Name',
# DataChannel
0xd0000001: 'Name',
0xd0000003: 'Acquire',
0xd0000004: 'Color',
0xd0000005: 'SampleType',
0xd0000006: 'BitsPerSample',
0xd0000007: 'RatioType',
0xd0000008: 'RatioTrack1',
0xd0000009: 'RatioTrack2',
0xd000000a: 'RatioChannel1',
0xd000000b: 'RatioChannel2',
0xd000000c: 'RatioConst1',
0xd000000d: 'RatioConst2',
0xd000000e: 'RatioConst3',
0xd000000f: 'RatioConst4',
0xd0000010: 'RatioConst5',
0xd0000011: 'RatioConst6',
0xd0000012: 'RatioFirstImages1',
0xd0000013: 'RatioFirstImages2',
0xd0000014: 'DyeName',
0xd0000015: 'DyeFolder',
0xd0000016: 'Spectrum',
0xd0000017: 'Acquire',
# Timer
0x12000001: 'Name',
0x12000002: 'Description',
0x12000003: 'Interval',
0x12000004: 'TriggerIn',
0x12000005: 'TriggerOut',
0x12000006: 'ActivationTime',
0x12000007: 'ActivationNumber',
# Marker
0x14000001: 'Name',
0x14000002: 'Description',
0x14000003: 'TriggerIn',
0x14000004: 'TriggerOut',
}
def NIH_IMAGE_HEADER():
return [
('FileID', 'a8'),
('nLines', 'i2'),
('PixelsPerLine', 'i2'),
('Version', 'i2'),
('OldLutMode', 'i2'),
('OldnColors', 'i2'),
('Colors', 'u1', (3, 32)),
('OldColorStart', 'i2'),
('ColorWidth', 'i2'),
('ExtraColors', 'u2', (6, 3)),
('nExtraColors', 'i2'),
('ForegroundIndex', 'i2'),
('BackgroundIndex', 'i2'),
('XScale', 'f8'),
('Unused2', 'i2'),
('Unused3', 'i2'),
('UnitsID', 'i2'), # NIH_UNITS_TYPE
('p1', [('x', 'i2'), ('y', 'i2')]),
('p2', [('x', 'i2'), ('y', 'i2')]),
('CurveFitType', 'i2'), # NIH_CURVEFIT_TYPE
('nCoefficients', 'i2'),
('Coeff', 'f8', 6),
('UMsize', 'u1'),
('UM', 'a15'),
('UnusedBoolean', 'u1'),
('BinaryPic', 'b1'),
('SliceStart', 'i2'),
('SliceEnd', 'i2'),
('ScaleMagnification', 'f4'),
('nSlices', 'i2'),
('SliceSpacing', 'f4'),
('CurrentSlice', 'i2'),
('FrameInterval', 'f4'),
('PixelAspectRatio', 'f4'),
('ColorStart', 'i2'),
('ColorEnd', 'i2'),
('nColors', 'i2'),
('Fill1', '3u2'),
('Fill2', '3u2'),
('Table', 'u1'), # NIH_COLORTABLE_TYPE
('LutMode', 'u1'), # NIH_LUTMODE_TYPE
('InvertedTable', 'b1'),
('ZeroClip', 'b1'),
('XUnitSize', 'u1'),
('XUnit', 'a11'),
('StackType', 'i2'), # NIH_STACKTYPE_TYPE
# ('UnusedBytes', 'u1', 200)
]
def NIH_COLORTABLE_TYPE():
return ('CustomTable', 'AppleDefault', 'Pseudo20', 'Pseudo32',
'Rainbow', 'Fire1', 'Fire2', 'Ice', 'Grays', 'Spectrum')
def NIH_LUTMODE_TYPE():
return ('PseudoColor', 'OldAppleDefault', 'OldSpectrum', 'GrayScale',
'ColorLut', 'CustomGrayscale')
def NIH_CURVEFIT_TYPE():
return ('StraightLine', 'Poly2', 'Poly3', 'Poly4', 'Poly5', 'ExpoFit',
'PowerFit', 'LogFit', 'RodbardFit', 'SpareFit1',
'Uncalibrated', 'UncalibratedOD')
def NIH_UNITS_TYPE():
return ('Nanometers', 'Micrometers', 'Millimeters', 'Centimeters',
'Meters', 'Kilometers', 'Inches', 'Feet', 'Miles', 'Pixels',
'OtherUnits')
def NIH_STACKTYPE_TYPE():
return ('VolumeStack', 'RGBStack', 'MovieStack', 'HSVStack')
def TVIPS_HEADER_V1():
# TVIPS TemData structure from EMMENU Help file
return [
('Version', 'i4'),
('CommentV1', 'a80'),
('HighTension', 'i4'),
('SphericalAberration', 'i4'),
('IlluminationAperture', 'i4'),
('Magnification', 'i4'),
('PostMagnification', 'i4'),
('FocalLength', 'i4'),
('Defocus', 'i4'),
('Astigmatism', 'i4'),
('AstigmatismDirection', 'i4'),
('BiprismVoltage', 'i4'),
('SpecimenTiltAngle', 'i4'),
('SpecimenTiltDirection', 'i4'),
('IlluminationTiltDirection', 'i4'),
('IlluminationTiltAngle', 'i4'),
('ImageMode', 'i4'),
('EnergySpread', 'i4'),
('ChromaticAberration', 'i4'),
('ShutterType', 'i4'),
('DefocusSpread', 'i4'),
('CcdNumber', 'i4'),
('CcdSize', 'i4'),
('OffsetXV1', 'i4'),
('OffsetYV1', 'i4'),
('PhysicalPixelSize', 'i4'),
('Binning', 'i4'),
('ReadoutSpeed', 'i4'),
('GainV1', 'i4'),
('SensitivityV1', 'i4'),
('ExposureTimeV1', 'i4'),
('FlatCorrected', 'i4'),
('DeadPxCorrected', 'i4'),
('ImageMean', 'i4'),
('ImageStd', 'i4'),
('DisplacementX', 'i4'),
('DisplacementY', 'i4'),
('DateV1', 'i4'),
('TimeV1', 'i4'),
('ImageMin', 'i4'),
('ImageMax', 'i4'),
('ImageStatisticsQuality', 'i4'),
]
def TVIPS_HEADER_V2():
return [
('ImageName', 'V160'), # utf16
('ImageFolder', 'V160'),
('ImageSizeX', 'i4'),
('ImageSizeY', 'i4'),
('ImageSizeZ', 'i4'),
('ImageSizeE', 'i4'),
('ImageDataType', 'i4'),
('Date', 'i4'),
('Time', 'i4'),
('Comment', 'V1024'),
('ImageHistory', 'V1024'),
('Scaling', '16f4'),
('ImageStatistics', '16c16'),
('ImageType', 'i4'),
('ImageDisplaType', 'i4'),
('PixelSizeX', 'f4'), # distance between two px in x, [nm]
('PixelSizeY', 'f4'), # distance between two px in y, [nm]
('ImageDistanceZ', 'f4'),
('ImageDistanceE', 'f4'),
('ImageMisc', '32f4'),
('TemType', 'V160'),
('TemHighTension', 'f4'),
('TemAberrations', '32f4'),
('TemEnergy', '32f4'),
('TemMode', 'i4'),
('TemMagnification', 'f4'),
('TemMagnificationCorrection', 'f4'),
('PostMagnification', 'f4'),
('TemStageType', 'i4'),
('TemStagePosition', '5f4'), # x, y, z, a, b
('TemImageShift', '2f4'),
('TemBeamShift', '2f4'),
('TemBeamTilt', '2f4'),
('TilingParameters', '7f4'), # 0: tiling? 1:x 2:y 3: max x
# 4: max y 5: overlap x 6: overlap y
('TemIllumination', '3f4'), # 0: spotsize 1: intensity
('TemShutter', 'i4'),
('TemMisc', '32f4'),
('CameraType', 'V160'),
('PhysicalPixelSizeX', 'f4'),
('PhysicalPixelSizeY', 'f4'),
('OffsetX', 'i4'),
('OffsetY', 'i4'),
('BinningX', 'i4'),
('BinningY', 'i4'),
('ExposureTime', 'f4'),
('Gain', 'f4'),
('ReadoutRate', 'f4'),
('FlatfieldDescription', 'V160'),
('Sensitivity', 'f4'),
('Dose', 'f4'),
('CamMisc', '32f4'),
('FeiMicroscopeInformation', 'V1024'),
('FeiSpecimenInformation', 'V1024'),
('Magic', 'u4'),
]
def MM_HEADER():
# Olympus FluoView MM_Header
MM_DIMENSION = [
('Name', 'a16'),
('Size', 'i4'),
('Origin', 'f8'),
('Resolution', 'f8'),
('Unit', 'a64')]
return [
('HeaderFlag', 'i2'),
('ImageType', 'u1'),
('ImageName', 'a257'),
('OffsetData', 'u4'),
('PaletteSize', 'i4'),
('OffsetPalette0', 'u4'),
('OffsetPalette1', 'u4'),
('CommentSize', 'i4'),
('OffsetComment', 'u4'),
('Dimensions', MM_DIMENSION, 10),
('OffsetPosition', 'u4'),
('MapType', 'i2'),
('MapMin', 'f8'),
('MapMax', 'f8'),
('MinValue', 'f8'),
('MaxValue', 'f8'),
('OffsetMap', 'u4'),
('Gamma', 'f8'),
('Offset', 'f8'),
('GrayChannel', MM_DIMENSION),
('OffsetThumbnail', 'u4'),
('VoiceField', 'i4'),
('OffsetVoiceField', 'u4'),
]
def MM_DIMENSIONS():
# Map FluoView MM_Header.Dimensions to axes characters
return {
'X': 'X',
'Y': 'Y',
'Z': 'Z',
'T': 'T',
'CH': 'C',
'WAVELENGTH': 'C',
'TIME': 'T',
'XY': 'R',
'EVENT': 'V',
'EXPOSURE': 'L',
}
def UIC_TAGS():
# Map Universal Imaging Corporation MetaMorph internal tag ids to
# name and type
from fractions import Fraction # delayed import
return [
('AutoScale', int),
('MinScale', int),
('MaxScale', int),
('SpatialCalibration', int),
('XCalibration', Fraction),
('YCalibration', Fraction),
('CalibrationUnits', str),
('Name', str),
('ThreshState', int),
('ThreshStateRed', int),
('tagid_10', None), # undefined
('ThreshStateGreen', int),
('ThreshStateBlue', int),
('ThreshStateLo', int),
('ThreshStateHi', int),
('Zoom', int),
('CreateTime', julian_datetime),
('LastSavedTime', julian_datetime),
('currentBuffer', int),
('grayFit', None),
('grayPointCount', None),
('grayX', Fraction),
('grayY', Fraction),
('grayMin', Fraction),
('grayMax', Fraction),
('grayUnitName', str),
('StandardLUT', int),
('wavelength', int),
('StagePosition', '(%i,2,2)u4'), # N xy positions as fract
('CameraChipOffset', '(%i,2,2)u4'), # N xy offsets as fract
('OverlayMask', None),
('OverlayCompress', None),
('Overlay', None),
('SpecialOverlayMask', None),
('SpecialOverlayCompress', None),
('SpecialOverlay', None),
('ImageProperty', read_uic_image_property),
('StageLabel', '%ip'), # N str
('AutoScaleLoInfo', Fraction),
('AutoScaleHiInfo', Fraction),
('AbsoluteZ', '(%i,2)u4'), # N fractions
('AbsoluteZValid', '(%i,)u4'), # N long
('Gamma', 'I'), # 'I' uses offset
('GammaRed', 'I'),
('GammaGreen', 'I'),
('GammaBlue', 'I'),
('CameraBin', '2I'),
('NewLUT', int),
('ImagePropertyEx', None),
('PlaneProperty', int),
('UserLutTable', '(256,3)u1'),
('RedAutoScaleInfo', int),
('RedAutoScaleLoInfo', Fraction),
('RedAutoScaleHiInfo', Fraction),
('RedMinScaleInfo', int),
('RedMaxScaleInfo', int),
('GreenAutoScaleInfo', int),
('GreenAutoScaleLoInfo', Fraction),
('GreenAutoScaleHiInfo', Fraction),
('GreenMinScaleInfo', int),
('GreenMaxScaleInfo', int),
('BlueAutoScaleInfo', int),
('BlueAutoScaleLoInfo', Fraction),
('BlueAutoScaleHiInfo', Fraction),
('BlueMinScaleInfo', int),
('BlueMaxScaleInfo', int),
# ('OverlayPlaneColor', read_uic_overlay_plane_color),
]
def PILATUS_HEADER():
# PILATUS CBF Header Specification, Version 1.4
# Map key to [value_indices], type
return {
'Detector': ([slice(1, None)], str),
'Pixel_size': ([1, 4], float),
'Silicon': ([3], float),
'Exposure_time': ([1], float),
'Exposure_period': ([1], float),
'Tau': ([1], float),
'Count_cutoff': ([1], int),
'Threshold_setting': ([1], float),
'Gain_setting': ([1, 2], str),
'N_excluded_pixels': ([1], int),
'Excluded_pixels': ([1], str),
'Flat_field': ([1], str),
'Trim_file': ([1], str),
'Image_path': ([1], str),
# optional
'Wavelength': ([1], float),
'Energy_range': ([1, 2], float),
'Detector_distance': ([1], float),
'Detector_Voffset': ([1], float),
'Beam_xy': ([1, 2], float),
'Flux': ([1], str),
'Filter_transmission': ([1], float),
'Start_angle': ([1], float),
'Angle_increment': ([1], float),
'Detector_2theta': ([1], float),
'Polarization': ([1], float),
'Alpha': ([1], float),
'Kappa': ([1], float),
'Phi': ([1], float),
'Phi_increment': ([1], float),
'Chi': ([1], float),
'Chi_increment': ([1], float),
'Oscillation_axis': ([slice(1, None)], str),
'N_oscillations': ([1], int),
'Start_position': ([1], float),
'Position_increment': ([1], float),
'Shutter_time': ([1], float),
'Omega': ([1], float),
'Omega_increment': ([1], float)
}
def REVERSE_BITORDER_BYTES():
# Bytes with reversed bitorder
return (
b'\x00\x80@\xc0 \xa0`\xe0\x10\x90P\xd00\xb0p\xf0\x08\x88H\xc8('
b'\xa8h\xe8\x18\x98X\xd88\xb8x\xf8\x04\x84D\xc4$\xa4d\xe4\x14'
b'\x94T\xd44\xb4t\xf4\x0c\x8cL\xcc,\xacl\xec\x1c\x9c\\\xdc<\xbc|'
b'\xfc\x02\x82B\xc2"\xa2b\xe2\x12\x92R\xd22\xb2r\xf2\n\x8aJ\xca*'
b'\xaaj\xea\x1a\x9aZ\xda:\xbaz\xfa\x06\x86F\xc6&\xa6f\xe6\x16'
b'\x96V\xd66\xb6v\xf6\x0e\x8eN\xce.\xaen\xee\x1e\x9e^\xde>\xbe~'
b'\xfe\x01\x81A\xc1!\xa1a\xe1\x11\x91Q\xd11\xb1q\xf1\t\x89I\xc9)'
b'\xa9i\xe9\x19\x99Y\xd99\xb9y\xf9\x05\x85E\xc5%\xa5e\xe5\x15'
b'\x95U\xd55\xb5u\xf5\r\x8dM\xcd-\xadm\xed\x1d\x9d]\xdd=\xbd}'
b'\xfd\x03\x83C\xc3#\xa3c\xe3\x13\x93S\xd33\xb3s\xf3\x0b\x8bK'
b'\xcb+\xabk\xeb\x1b\x9b[\xdb;\xbb{\xfb\x07\x87G\xc7\'\xa7g\xe7'
b'\x17\x97W\xd77\xb7w\xf7\x0f\x8fO\xcf/\xafo\xef\x1f\x9f_'
b'\xdf?\xbf\x7f\xff')
def REVERSE_BITORDER_ARRAY():
# Numpy array of bytes with reversed bitorder
return numpy.frombuffer(TIFF.REVERSE_BITORDER_BYTES, dtype='uint8')
def ALLOCATIONGRANULARITY():
# alignment for writing contiguous data to TIFF
import mmap # delayed import
return mmap.ALLOCATIONGRANULARITY
def read_tags(fh, byteorder, offsetsize, tagnames,
customtags=None, maxifds=None):
"""Read tags from chain of IFDs and return as list of dicts.
The file handle position must be at a valid IFD header.
"""
if offsetsize == 4:
offsetformat = byteorder+'I'
tagnosize = 2
tagnoformat = byteorder+'H'
tagsize = 12
tagformat1 = byteorder+'HH'
tagformat2 = byteorder+'I4s'
elif offsetsize == 8:
offsetformat = byteorder+'Q'
tagnosize = 8
tagnoformat = byteorder+'Q'
tagsize = 20
tagformat1 = byteorder+'HH'
tagformat2 = byteorder+'Q8s'
else:
raise ValueError('invalid offset size')
if customtags is None:
customtags = {}
if maxifds is None:
maxifds = 2**32
result = []
unpack = struct.unpack
offset = fh.tell()
while len(result) < maxifds:
# loop over IFDs
try:
tagno = unpack(tagnoformat, fh.read(tagnosize))[0]
if tagno > 4096:
raise ValueError('suspicious number of tags')
except Exception:
warnings.warn('corrupted tag list at offset %i' % offset)
break
tags = {}
data = fh.read(tagsize * tagno)
pos = fh.tell()
index = 0
for _ in range(tagno):
code, type_ = unpack(tagformat1, data[index:index+4])
count, value = unpack(tagformat2, data[index+4:index+tagsize])
index += tagsize
name = tagnames.get(code, str(code))
try:
dtype = TIFF.DATA_FORMATS[type_]
except KeyError:
raise TiffTag.Error('unknown tag data type %i' % type_)
fmt = '%s%i%s' % (byteorder, count * int(dtype[0]), dtype[1])
size = struct.calcsize(fmt)
if size > offsetsize or code in customtags:
offset = unpack(offsetformat, value)[0]
if offset < 8 or offset > fh.size - size:
raise TiffTag.Error('invalid tag value offset %i' % offset)
fh.seek(offset)
if code in customtags:
readfunc = customtags[code][1]
value = readfunc(fh, byteorder, dtype, count, offsetsize)
elif type_ == 7 or (count > 1 and dtype[-1] == 'B'):
value = read_bytes(fh, byteorder, dtype, count, offsetsize)
elif code in tagnames or dtype[-1] == 's':
value = unpack(fmt, fh.read(size))
else:
value = read_numpy(fh, byteorder, dtype, count, offsetsize)
elif dtype[-1] == 'B' or type_ == 7:
value = value[:size]
else:
value = unpack(fmt, value[:size])
if code not in customtags and code not in TIFF.TAG_TUPLE:
if len(value) == 1:
value = value[0]
if type_ != 7 and dtype[-1] == 's' and isinstance(value, bytes):
# TIFF ASCII fields can contain multiple strings,
# each terminated with a NUL
try:
value = bytes2str(stripascii(value).strip())
except UnicodeDecodeError:
warnings.warn(
'tag %i: coercing invalid ASCII to bytes' % code)
tags[name] = value
result.append(tags)
# read offset to next page
fh.seek(pos)
offset = unpack(offsetformat, fh.read(offsetsize))[0]
if offset == 0:
break
if offset >= fh.size:
warnings.warn('invalid page offset %i' % offset)
break
fh.seek(offset)
if result and maxifds == 1:
result = result[0]
return result
def read_exif_ifd(fh, byteorder, dtype, count, offsetsize):
"""Read EXIF tags from file and return as dict."""
exif = read_tags(fh, byteorder, offsetsize, TIFF.EXIF_TAGS, maxifds=1)
for name in ('ExifVersion', 'FlashpixVersion'):
try:
exif[name] = bytes2str(exif[name])
except Exception:
pass
if 'UserComment' in exif:
idcode = exif['UserComment'][:8]
try:
if idcode == b'ASCII\x00\x00\x00':
exif['UserComment'] = bytes2str(exif['UserComment'][8:])
elif idcode == b'UNICODE\x00':
exif['UserComment'] = exif['UserComment'][8:].decode('utf-16')
except Exception:
pass
return exif
def read_gps_ifd(fh, byteorder, dtype, count, offsetsize):
"""Read GPS tags from file and return as dict."""
return read_tags(fh, byteorder, offsetsize, TIFF.GPS_TAGS, maxifds=1)
def read_interoperability_ifd(fh, byteorder, dtype, count, offsetsize):
"""Read Interoperability tags from file and return as dict."""
tag_names = {1: 'InteroperabilityIndex'}
return read_tags(fh, byteorder, offsetsize, tag_names, maxifds=1)
def read_bytes(fh, byteorder, dtype, count, offsetsize):
"""Read tag data from file and return as byte string."""
dtype = 'B' if dtype[-1] == 's' else byteorder+dtype[-1]
count *= numpy.dtype(dtype).itemsize
data = fh.read(count)
if len(data) != count:
warnings.warn('failed to read all bytes: %i, %i' % (len(data), count))
return data
def read_utf8(fh, byteorder, dtype, count, offsetsize):
"""Read tag data from file and return as unicode string."""
return fh.read(count).decode('utf-8')
def read_numpy(fh, byteorder, dtype, count, offsetsize):
"""Read tag data from file and return as numpy array."""
dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1]
return fh.read_array(dtype, count)
def read_colormap(fh, byteorder, dtype, count, offsetsize):
"""Read ColorMap data from file and return as numpy array."""
cmap = fh.read_array(byteorder+dtype[-1], count)
cmap.shape = (3, -1)
return cmap
def read_json(fh, byteorder, dtype, count, offsetsize):
"""Read JSON tag data from file and return as object."""
data = fh.read(count)
try:
return json.loads(unicode(stripnull(data), 'utf-8'))
except ValueError:
warnings.warn("invalid JSON '%s'" % data)
def read_mm_header(fh, byteorder, dtype, count, offsetsize):
"""Read FluoView mm_header tag from file and return as dict."""
mmh = fh.read_record(TIFF.MM_HEADER, byteorder=byteorder)
mmh = recarray2dict(mmh)
mmh['Dimensions'] = [
(bytes2str(d[0]).strip(), d[1], d[2], d[3], bytes2str(d[4]).strip())
for d in mmh['Dimensions']]
d = mmh['GrayChannel']
mmh['GrayChannel'] = (
bytes2str(d[0]).strip(), d[1], d[2], d[3], bytes2str(d[4]).strip())
return mmh
def read_mm_stamp(fh, byteorder, dtype, count, offsetsize):
"""Read FluoView mm_stamp tag from file and return as numpy.ndarray."""
return fh.read_array(byteorder+'f8', 8)
def read_uic1tag(fh, byteorder, dtype, count, offsetsize, planecount=None):
"""Read MetaMorph STK UIC1Tag from file and return as dict.
Return empty dictionary if planecount is unknown.
"""
assert dtype in ('2I', '1I') and byteorder == '<'
result = {}
if dtype == '2I':
# pre MetaMorph 2.5 (not tested)
values = fh.read_array('<u4', 2*count).reshape(count, 2)
result = {'ZDistance': values[:, 0] / values[:, 1]}
elif planecount:
for _ in range(count):
tagid = struct.unpack('<I', fh.read(4))[0]
if tagid in (28, 29, 37, 40, 41):
# silently skip unexpected tags
fh.read(4)
continue
name, value = read_uic_tag(fh, tagid, planecount, offset=True)
result[name] = value
return result
def read_uic2tag(fh, byteorder, dtype, planecount, offsetsize):
"""Read MetaMorph STK UIC2Tag from file and return as dict."""
assert dtype == '2I' and byteorder == '<'
values = fh.read_array('<u4', 6*planecount).reshape(planecount, 6)
return {
'ZDistance': values[:, 0] / values[:, 1],
'DateCreated': values[:, 2], # julian days
'TimeCreated': values[:, 3], # milliseconds
'DateModified': values[:, 4], # julian days
'TimeModified': values[:, 5]} # milliseconds
def read_uic3tag(fh, byteorder, dtype, planecount, offsetsize):
"""Read MetaMorph STK UIC3Tag from file and return as dict."""
assert dtype == '2I' and byteorder == '<'
values = fh.read_array('<u4', 2*planecount).reshape(planecount, 2)
return {'Wavelengths': values[:, 0] / values[:, 1]}
def read_uic4tag(fh, byteorder, dtype, planecount, offsetsize):
"""Read MetaMorph STK UIC4Tag from file and return as dict."""
assert dtype == '1I' and byteorder == '<'
result = {}
while True:
tagid = struct.unpack('<H', fh.read(2))[0]
if tagid == 0:
break
name, value = read_uic_tag(fh, tagid, planecount, offset=False)
result[name] = value
return result
def read_uic_tag(fh, tagid, planecount, offset):
"""Read a single UIC tag value from file and return tag name and value.
UIC1Tags use an offset.
"""
def read_int(count=1):
value = struct.unpack('<%iI' % count, fh.read(4*count))
return value[0] if count == 1 else value
try:
name, dtype = TIFF.UIC_TAGS[tagid]
except IndexError:
# unknown tag
return '_TagId%i' % tagid, read_int()
Fraction = TIFF.UIC_TAGS[4][1]
if offset:
pos = fh.tell()
if dtype not in (int, None):
off = read_int()
if off < 8:
if dtype is str:
return name, ''
warnings.warn("invalid offset for uic tag '%s': %i" %
(name, off))
return name, off
fh.seek(off)
if dtype is None:
# skip
name = '_' + name
value = read_int()
elif dtype is int:
# int
value = read_int()
elif dtype is Fraction:
# fraction
value = read_int(2)
value = value[0] / value[1]
elif dtype is julian_datetime:
# datetime
value = julian_datetime(*read_int(2))
elif dtype is read_uic_image_property:
# ImagePropertyEx
value = read_uic_image_property(fh)
elif dtype is str:
# pascal string
size = read_int()
if 0 <= size < 2**10:
value = struct.unpack('%is' % size, fh.read(size))[0][:-1]
value = bytes2str(stripnull(value))
elif offset:
value = ''
warnings.warn("corrupt string in uic tag '%s'" % name)
else:
raise ValueError('invalid string size: %i' % size)
elif dtype == '%ip':
# sequence of pascal strings
value = []
for _ in range(planecount):
size = read_int()
if 0 <= size < 2**10:
string = struct.unpack('%is' % size, fh.read(size))[0][:-1]
string = bytes2str(stripnull(string))
value.append(string)
elif offset:
warnings.warn("corrupt string in uic tag '%s'" % name)
else:
raise ValueError('invalid string size: %i' % size)
else:
# struct or numpy type
dtype = '<' + dtype
if '%i' in dtype:
dtype = dtype % planecount
if '(' in dtype:
# numpy type
value = fh.read_array(dtype, 1)[0]
if value.shape[-1] == 2:
# assume fractions
value = value[..., 0] / value[..., 1]
else:
# struct format
value = struct.unpack(dtype, fh.read(struct.calcsize(dtype)))
if len(value) == 1:
value = value[0]
if offset:
fh.seek(pos + 4)
return name, value
def read_uic_image_property(fh):
"""Read UIC ImagePropertyEx tag from file and return as dict."""
# TODO: test this
size = struct.unpack('B', fh.read(1))[0]
name = struct.unpack('%is' % size, fh.read(size))[0][:-1]
flags, prop = struct.unpack('<IB', fh.read(5))
if prop == 1:
value = struct.unpack('II', fh.read(8))
value = value[0] / value[1]
else:
size = struct.unpack('B', fh.read(1))[0]
value = struct.unpack('%is' % size, fh.read(size))[0]
return dict(name=name, flags=flags, value=value)
def read_cz_lsminfo(fh, byteorder, dtype, count, offsetsize):
"""Read CZ_LSMINFO tag from file and return as dict."""
assert byteorder == '<'
magic_number, structure_size = struct.unpack('<II', fh.read(8))
if magic_number not in (50350412, 67127628):
raise ValueError('invalid CZ_LSMINFO structure')
fh.seek(-8, 1)
if structure_size < numpy.dtype(TIFF.CZ_LSMINFO).itemsize:
# adjust structure according to structure_size
lsminfo = []
size = 0
for name, dtype in TIFF.CZ_LSMINFO:
size += numpy.dtype(dtype).itemsize
if size > structure_size:
break
lsminfo.append((name, dtype))
else:
lsminfo = TIFF.CZ_LSMINFO
lsminfo = fh.read_record(lsminfo, byteorder=byteorder)
lsminfo = recarray2dict(lsminfo)
# read LSM info subrecords at offsets
for name, reader in TIFF.CZ_LSMINFO_READERS.items():
if reader is None:
continue
offset = lsminfo.get('Offset' + name, 0)
if offset < 8:
continue
fh.seek(offset)
try:
lsminfo[name] = reader(fh)
except ValueError:
pass
return lsminfo
def read_lsm_floatpairs(fh):
"""Read LSM sequence of float pairs from file and return as list."""
size = struct.unpack('<i', fh.read(4))[0]
return fh.read_array('<2f8', count=size)
def read_lsm_positions(fh):
"""Read LSM positions from file and return as list."""
size = struct.unpack('<I', fh.read(4))[0]
return fh.read_array('<2f8', count=size)
def read_lsm_timestamps(fh):
"""Read LSM time stamps from file and return as list."""
size, count = struct.unpack('<ii', fh.read(8))
if size != (8 + 8 * count):
warnings.warn('invalid LSM TimeStamps block')
return []
# return struct.unpack('<%dd' % count, fh.read(8*count))
return fh.read_array('<f8', count=count)
def read_lsm_eventlist(fh):
"""Read LSM events from file and return as list of (time, type, text)."""
count = struct.unpack('<II', fh.read(8))[1]
events = []
while count > 0:
esize, etime, etype = struct.unpack('<IdI', fh.read(16))
etext = bytes2str(stripnull(fh.read(esize - 16)))
events.append((etime, etype, etext))
count -= 1
return events
def read_lsm_channelcolors(fh):
"""Read LSM ChannelColors structure from file and return as dict."""
result = {'Mono': False, 'Colors': [], 'ColorNames': []}
pos = fh.tell()
(size, ncolors, nnames,
coffset, noffset, mono) = struct.unpack('<IIIIII', fh.read(24))
if ncolors != nnames:
warnings.warn('invalid LSM ChannelColors structure')
return result
result['Mono'] = bool(mono)
# Colors
fh.seek(pos + coffset)
colors = fh.read_array('uint8', count=ncolors*4).reshape((ncolors, 4))
result['Colors'] = colors.tolist()
# ColorNames
fh.seek(pos + noffset)
buffer = fh.read(size - noffset)
names = []
while len(buffer) > 4:
size = struct.unpack('<I', buffer[:4])[0]
names.append(bytes2str(buffer[4:3+size]))
buffer = buffer[4+size:]
result['ColorNames'] = names
return result
def read_lsm_scaninfo(fh):
"""Read LSM ScanInfo structure from file and return as dict."""
block = {}
blocks = [block]
unpack = struct.unpack
if struct.unpack('<I', fh.read(4))[0] != 0x10000000:
# not a Recording sub block
warnings.warn('invalid LSM ScanInfo structure')
return block
fh.read(8)
while True:
entry, dtype, size = unpack('<III', fh.read(12))
if dtype == 2:
# ascii
value = bytes2str(stripnull(fh.read(size)))
elif dtype == 4:
# long
value = unpack('<i', fh.read(4))[0]
elif dtype == 5:
# rational
value = unpack('<d', fh.read(8))[0]
else:
value = 0
if entry in TIFF.CZ_LSMINFO_SCANINFO_ARRAYS:
blocks.append(block)
name = TIFF.CZ_LSMINFO_SCANINFO_ARRAYS[entry]
newobj = []
block[name] = newobj
block = newobj
elif entry in TIFF.CZ_LSMINFO_SCANINFO_STRUCTS:
blocks.append(block)
newobj = {}
block.append(newobj)
block = newobj
elif entry in TIFF.CZ_LSMINFO_SCANINFO_ATTRIBUTES:
name = TIFF.CZ_LSMINFO_SCANINFO_ATTRIBUTES[entry]
block[name] = value
elif entry == 0xffffffff:
# end sub block
block = blocks.pop()
else:
# unknown entry
block['Entry0x%x' % entry] = value
if not blocks:
break
return block
def read_tvips_header(fh, byteorder, dtype, count, offsetsize):
"""Read TVIPS EM-MENU headers and return as dict."""
result = {}
header = fh.read_record(TIFF.TVIPS_HEADER_V1, byteorder=byteorder)
for name, typestr in TIFF.TVIPS_HEADER_V1:
result[name] = header[name].tolist()
if header['Version'] == 2:
header = fh.read_record(TIFF.TVIPS_HEADER_V2, byteorder=byteorder)
if header['Magic'] != int(0xaaaaaaaa):
warnings.warn('invalid TVIPS v2 magic number')
return {}
# decode utf16 strings
for name, typestr in TIFF.TVIPS_HEADER_V2:
if typestr.startswith('V'):
s = header[name].tostring().decode('utf16', errors='ignore')
result[name] = stripnull(s, null='\0')
else:
result[name] = header[name].tolist()
# convert nm to m
for axis in 'XY':
header['PhysicalPixelSize' + axis] /= 1e9
header['PixelSize' + axis] /= 1e9
elif header.version != 1:
warnings.warn('unknown TVIPS header version')
return {}
return result
def read_fei_metadata(fh, byteorder, dtype, count, offsetsize):
"""Read FEI SFEG/HELIOS headers and return as dict."""
result = {}
section = {}
data = bytes2str(fh.read(count))
for line in data.splitlines():
line = line.strip()
if line.startswith('['):
section = {}
result[line[1:-1]] = section
continue
try:
key, value = line.split('=')
except ValueError:
continue
section[key] = astype(value)
return result
def read_cz_sem(fh, byteorder, dtype, count, offsetsize):
"""Read Zeiss SEM tag and return as dict."""
result = {'': ()}
key = None
data = bytes2str(fh.read(count))
for line in data.splitlines():
if line.isupper():
key = line.lower()
elif key:
try:
name, value = line.split('=')
except ValueError:
continue
value = value.strip()
unit = ''
try:
v, u = value.split()
number = astype(v, (int, float))
if number != v:
value = number
unit = u
except Exception:
number = astype(value, (int, float))
if number != value:
value = number
if value in ('No', 'Off'):
value = False
elif value in ('Yes', 'On'):
value = True
result[key] = (name.strip(), value)
if unit:
result[key] += (unit,)
key = None
else:
result[''] += (astype(line, (int, float)),)
return result
def read_nih_image_header(fh, byteorder, dtype, count, offsetsize):
"""Read NIH_IMAGE_HEADER tag from file and return as dict."""
a = fh.read_record(TIFF.NIH_IMAGE_HEADER, byteorder=byteorder)
a = a.newbyteorder(byteorder)
a = recarray2dict(a)
a['XUnit'] = a['XUnit'][:a['XUnitSize']]
a['UM'] = a['UM'][:a['UMsize']]
return a
def read_scanimage_metadata(fh):
"""Read ScanImage BigTIFF v3 static and ROI metadata from open file.
Return non-varying frame data as dict and ROI group data as JSON.
The settings can be used to read image data and metadata without parsing
the TIFF file.
Raise ValueError if file does not contain valid ScanImage v3 metadata.
"""
fh.seek(0)
try:
byteorder, version = struct.unpack('<2sH', fh.read(4))
if byteorder != b'II' or version != 43:
raise Exception
fh.seek(16)
magic, version, size0, size1 = struct.unpack('<IIII', fh.read(16))
if magic != 117637889 or version != 3:
raise Exception
except Exception:
raise ValueError('not a ScanImage BigTIFF v3 file')
frame_data = matlabstr2py(bytes2str(fh.read(size0)[:-1]))
roi_data = read_json(fh, '<', None, size1, None) if size1 > 1 else {}
return frame_data, roi_data
def read_micromanager_metadata(fh):
"""Read MicroManager non-TIFF settings from open file and return as dict.
The settings can be used to read image data without parsing the TIFF file.
Raise ValueError if the file does not contain valid MicroManager metadata.
"""
fh.seek(0)
try:
byteorder = {b'II': '<', b'MM': '>'}[fh.read(2)]
except IndexError:
raise ValueError('not a MicroManager TIFF file')
result = {}
fh.seek(8)
(index_header, index_offset, display_header, display_offset,
comments_header, comments_offset, summary_header, summary_length
) = struct.unpack(byteorder + 'IIIIIIII', fh.read(32))
if summary_header != 2355492:
raise ValueError('invalid MicroManager summary header')
result['Summary'] = read_json(fh, byteorder, None, summary_length, None)
if index_header != 54773648:
raise ValueError('invalid MicroManager index header')
fh.seek(index_offset)
header, count = struct.unpack(byteorder + 'II', fh.read(8))
if header != 3453623:
raise ValueError('invalid MicroManager index header')
data = struct.unpack(byteorder + 'IIIII'*count, fh.read(20*count))
result['IndexMap'] = {'Channel': data[::5],
'Slice': data[1::5],
'Frame': data[2::5],
'Position': data[3::5],
'Offset': data[4::5]}
if display_header != 483765892:
raise ValueError('invalid MicroManager display header')
fh.seek(display_offset)
header, count = struct.unpack(byteorder + 'II', fh.read(8))
if header != 347834724:
raise ValueError('invalid MicroManager display header')
result['DisplaySettings'] = read_json(fh, byteorder, None, count, None)
if comments_header != 99384722:
raise ValueError('invalid MicroManager comments header')
fh.seek(comments_offset)
header, count = struct.unpack(byteorder + 'II', fh.read(8))
if header != 84720485:
raise ValueError('invalid MicroManager comments header')
result['Comments'] = read_json(fh, byteorder, None, count, None)
return result
def read_metaseries_catalog(fh):
"""Read MetaSeries non-TIFF hint catalog from file.
Raise ValueError if the file does not contain a valid hint catalog.
"""
# TODO: implement read_metaseries_catalog
raise NotImplementedError()
def imagej_metadata_tags(metadata, byteorder):
"""Return IJMetadata and IJMetadataByteCounts tags from metadata dict.
The tags can be passed to the TiffWriter.save function as extratags.
The metadata dict may contain the following keys and values:
Info : str
Human-readable information as string.
Labels : sequence of str
Human-readable labels for each channel.
Ranges : sequence of doubles
Lower and upper values for each channel.
LUTs : sequence of (3, 256) uint8 ndarrays
Color palettes for each channel.
Plot : bytes
Undocumented ImageJ internal format.
ROI: bytes
Undocumented ImageJ internal region of interest format.
Overlays : bytes
Undocumented ImageJ internal format.
"""
header = [{'>': b'IJIJ', '<': b'JIJI'}[byteorder]]
bytecounts = [0]
body = []
def _string(data, byteorder):
return data.encode('utf-16' + {'>': 'be', '<': 'le'}[byteorder])
def _doubles(data, byteorder):
return struct.pack(byteorder+('d' * len(data)), *data)
def _ndarray(data, byteorder):
return data.tobytes()
def _bytes(data, byteorder):
return data
metadata_types = (
('Info', b'info', 1, _string),
('Labels', b'labl', None, _string),
('Ranges', b'rang', 1, _doubles),
('LUTs', b'luts', None, _ndarray),
('Plot', b'plot', 1, _bytes),
('ROI', b'roi ', 1, _bytes),
('Overlays', b'over', None, _bytes))
for key, mtype, count, func in metadata_types:
if key.lower() in metadata:
key = key.lower()
elif key not in metadata:
continue
if byteorder == '<':
mtype = mtype[::-1]
values = metadata[key]
if count is None:
count = len(values)
else:
values = [values]
header.append(mtype + struct.pack(byteorder+'I', count))
for value in values:
data = func(value, byteorder)
body.append(data)
bytecounts.append(len(data))
if not body:
return ()
body = b''.join(body)
header = b''.join(header)
data = header + body
bytecounts[0] = len(header)
bytecounts = struct.pack(byteorder+('I' * len(bytecounts)), *bytecounts)
return ((50839, 'B', len(data), data, True),
(50838, 'I', len(bytecounts)//4, bytecounts, True))
def imagej_metadata(data, bytecounts, byteorder):
"""Return IJMetadata tag value as dict.
The 'Info' string can have multiple formats, e.g. OIF or ScanImage,
that might be parsed into dicts using the matlabstr2py or
oiffile.SettingsFile functions.
"""
def _string(data, byteorder):
return data.decode('utf-16' + {'>': 'be', '<': 'le'}[byteorder])
def _doubles(data, byteorder):
return struct.unpack(byteorder+('d' * (len(data) // 8)), data)
def _lut(data, byteorder):
return numpy.frombuffer(data, 'uint8').reshape(-1, 256)
def _bytes(data, byteorder):
return data
metadata_types = { # big-endian
b'info': ('Info', _string),
b'labl': ('Labels', _string),
b'rang': ('Ranges', _doubles),
b'luts': ('LUTs', _lut),
b'plot': ('Plots', _bytes),
b'roi ': ('ROI', _bytes),
b'over': ('Overlays', _bytes)}
metadata_types.update( # little-endian
dict((k[::-1], v) for k, v in metadata_types.items()))
if not bytecounts:
raise ValueError('no ImageJ metadata')
if data[:4] not in (b'IJIJ', b'JIJI'):
raise ValueError('invalid ImageJ metadata')
header_size = bytecounts[0]
if header_size < 12 or header_size > 804:
raise ValueError('invalid ImageJ metadata header size')
ntypes = (header_size - 4) // 8
header = struct.unpack(byteorder+'4sI'*ntypes, data[4:4+ntypes*8])
pos = 4 + ntypes * 8
counter = 0
result = {}
for mtype, count in zip(header[::2], header[1::2]):
values = []
name, func = metadata_types.get(mtype, (bytes2str(mtype), read_bytes))
for _ in range(count):
counter += 1
pos1 = pos + bytecounts[counter]
values.append(func(data[pos:pos1], byteorder))
pos = pos1
result[name.strip()] = values[0] if count == 1 else values
return result
def imagej_description_metadata(description):
"""Return metatata from ImageJ image description as dict.
Raise ValueError if not a valid ImageJ description.
>>> description = 'ImageJ=1.11a\\nimages=510\\nhyperstack=true\\n'
>>> imagej_description_metadata(description) # doctest: +SKIP
{'ImageJ': '1.11a', 'images': 510, 'hyperstack': True}
"""
def _bool(val):
return {'true': True, 'false': False}[val.lower()]
result = {}
for line in description.splitlines():
try:
key, val = line.split('=')
except Exception:
continue
key = key.strip()
val = val.strip()
for dtype in (int, float, _bool):
try:
val = dtype(val)
break
except Exception:
pass
result[key] = val
if 'ImageJ' not in result:
raise ValueError('not a ImageJ image description')
return result
def imagej_description(shape, rgb=None, colormaped=False, version='1.11a',
hyperstack=None, mode=None, loop=None, **kwargs):
"""Return ImageJ image description from data shape.
ImageJ can handle up to 6 dimensions in order TZCYXS.
>>> imagej_description((51, 5, 2, 196, 171)) # doctest: +SKIP
ImageJ=1.11a
images=510
channels=2
slices=5
frames=51
hyperstack=true
mode=grayscale
loop=false
"""
if colormaped:
raise NotImplementedError('ImageJ colormapping not supported')
shape = imagej_shape(shape, rgb=rgb)
rgb = shape[-1] in (3, 4)
result = ['ImageJ=%s' % version]
append = []
result.append('images=%i' % product(shape[:-3]))
if hyperstack is None:
hyperstack = True
append.append('hyperstack=true')
else:
append.append('hyperstack=%s' % bool(hyperstack))
if shape[2] > 1:
result.append('channels=%i' % shape[2])
if mode is None and not rgb:
mode = 'grayscale'
if hyperstack and mode:
append.append('mode=%s' % mode)
if shape[1] > 1:
result.append('slices=%i' % shape[1])
if shape[0] > 1:
result.append('frames=%i' % shape[0])
if loop is None:
append.append('loop=false')
if loop is not None:
append.append('loop=%s' % bool(loop))
for key, value in kwargs.items():
append.append('%s=%s' % (key.lower(), value))
return '\n'.join(result + append + [''])
def imagej_shape(shape, rgb=None):
"""Return shape normalized to 6D ImageJ hyperstack TZCYXS.
Raise ValueError if not a valid ImageJ hyperstack shape.
>>> imagej_shape((2, 3, 4, 5, 3), False)
(2, 3, 4, 5, 3, 1)
"""
shape = tuple(int(i) for i in shape)
ndim = len(shape)
if 1 > ndim > 6:
raise ValueError('invalid ImageJ hyperstack: not 2 to 6 dimensional')
if rgb is None:
rgb = shape[-1] in (3, 4) and ndim > 2
if rgb and shape[-1] not in (3, 4):
raise ValueError('invalid ImageJ hyperstack: not a RGB image')
if not rgb and ndim == 6 and shape[-1] != 1:
raise ValueError('invalid ImageJ hyperstack: not a non-RGB image')
if rgb or shape[-1] == 1:
return (1, ) * (6 - ndim) + shape
return (1, ) * (5 - ndim) + shape + (1,)
def json_description(shape, **metadata):
"""Return JSON image description from data shape and other meta data.
Return UTF-8 encoded JSON.
>>> json_description((256, 256, 3), axes='YXS') # doctest: +SKIP
b'{"shape": [256, 256, 3], "axes": "YXS"}'
"""
metadata.update(shape=shape)
return json.dumps(metadata) # .encode('utf-8')
def json_description_metadata(description):
"""Return metatata from JSON formated image description as dict.
Raise ValuError if description is of unknown format.
>>> description = '{"shape": [256, 256, 3], "axes": "YXS"}'
>>> json_description_metadata(description) # doctest: +SKIP
{'shape': [256, 256, 3], 'axes': 'YXS'}
>>> json_description_metadata('shape=(256, 256, 3)')
{'shape': (256, 256, 3)}
"""
if description[:6] == 'shape=':
# old style 'shaped' description; not JSON
shape = tuple(int(i) for i in description[7:-1].split(','))
return dict(shape=shape)
if description[:1] == '{' and description[-1:] == '}':
# JSON description
return json.loads(description)
raise ValueError('invalid JSON image description', description)
def fluoview_description_metadata(description, ignoresections=None):
"""Return metatata from FluoView image description as dict.
The FluoView image description format is unspecified. Expect failures.
>>> descr = ('[Intensity Mapping]\\nMap Ch0: Range=00000 to 02047\\n'
... '[Intensity Mapping End]')
>>> fluoview_description_metadata(descr)
{'Intensity Mapping': {'Map Ch0: Range': '00000 to 02047'}}
"""
if not description.startswith('['):
raise ValueError('invalid FluoView image description')
if ignoresections is None:
ignoresections = {'Region Info (Fields)', 'Protocol Description'}
result = {}
sections = [result]
comment = False
for line in description.splitlines():
if not comment:
line = line.strip()
if not line:
continue
if line[0] == '[':
if line[-5:] == ' End]':
# close section
del sections[-1]
section = sections[-1]
name = line[1:-5]
if comment:
section[name] = '\n'.join(section[name])
if name[:4] == 'LUT ':
a = numpy.array(section[name], dtype='uint8')
a.shape = -1, 3
section[name] = a
continue
# new section
comment = False
name = line[1:-1]
if name[:4] == 'LUT ':
section = []
elif name in ignoresections:
section = []
comment = True
else:
section = {}
sections.append(section)
result[name] = section
continue
# add entry
if comment:
section.append(line)
continue
line = line.split('=', 1)
if len(line) == 1:
section[line[0].strip()] = None
continue
key, value = line
if key[:4] == 'RGB ':
section.extend(int(rgb) for rgb in value.split())
else:
section[key.strip()] = astype(value.strip())
return result
def pilatus_description_metadata(description):
"""Return metatata from Pilatus image description as dict.
Return metadata from Pilatus pixel array detectors by Dectris, created
by camserver or TVX software.
>>> pilatus_description_metadata('# Pixel_size 172e-6 m x 172e-6 m')
{'Pixel_size': (0.000172, 0.000172)}
"""
result = {}
if not description.startswith('# '):
return result
for c in '#:=,()':
description = description.replace(c, ' ')
for line in description.split('\n'):
if line[:2] != ' ':
continue
line = line.split()
name = line[0]
if line[0] not in TIFF.PILATUS_HEADER:
try:
result['DateTime'] = datetime.datetime.strptime(
' '.join(line), '%Y-%m-%dT%H %M %S.%f')
except Exception:
result[name] = ' '.join(line[1:])
continue
indices, dtype = TIFF.PILATUS_HEADER[line[0]]
if isinstance(indices[0], slice):
# assumes one slice
values = line[indices[0]]
else:
values = [line[i] for i in indices]
if dtype is float and values[0] == 'not':
values = ['NaN']
values = tuple(dtype(v) for v in values)
if dtype == str:
values = ' '.join(values)
elif len(values) == 1:
values = values[0]
result[name] = values
return result
def svs_description_metadata(description):
"""Return metatata from Aperio image description as dict.
The Aperio image description format is unspecified. Expect failures.
>>> svs_description_metadata('Aperio Image Library v1.0')
{'Aperio Image Library': 'v1.0'}
"""
if not description.startswith('Aperio Image Library '):
raise ValueError('invalid Aperio image description')
result = {}
lines = description.split('\n')
key, value = lines[0].strip().rsplit(None, 1) # 'Aperio Image Library'
result[key.strip()] = value.strip()
if len(lines) == 1:
return result
items = lines[1].split('|')
result[''] = items[0].strip() # TODO: parse this?
for item in items[1:]:
key, value = item.split(' = ')
result[key.strip()] = astype(value.strip())
return result
def stk_description_metadata(description):
"""Return metadata from MetaMorph image description as list of dict.
The MetaMorph image description format is unspecified. Expect failures.
"""
description = description.strip()
if not description:
return []
try:
description = bytes2str(description)
except UnicodeDecodeError:
warnings.warn('failed to parse MetaMorph image description')
return []
result = []
for plane in description.split('\x00'):
d = {}
for line in plane.split('\r\n'):
line = line.split(':', 1)
if len(line) > 1:
name, value = line
d[name.strip()] = astype(value.strip())
else:
value = line[0].strip()
if value:
if '' in d:
d[''].append(value)
else:
d[''] = [value]
result.append(d)
return result
def metaseries_description_metadata(description):
"""Return metatata from MetaSeries image description as dict."""
if not description.startswith('<MetaData>'):
raise ValueError('invalid MetaSeries image description')
from xml.etree import cElementTree as etree # delayed import
root = etree.fromstring(description)
types = {'float': float, 'int': int,
'bool': lambda x: asbool(x, 'on', 'off')}
def parse(root, result):
# recursive
for child in root:
attrib = child.attrib
if not attrib:
result[child.tag] = parse(child, {})
continue
if 'id' in attrib:
i = attrib['id']
t = attrib['type']
v = attrib['value']
if t in types:
result[i] = types[t](v)
else:
result[i] = v
return result
adict = parse(root, {})
if 'Description' in adict:
adict['Description'] = adict['Description'].replace('&#13;&#10;', '\n')
return adict
def scanimage_description_metadata(description):
"""Return metatata from ScanImage image description as dict."""
return matlabstr2py(description)
def scanimage_artist_metadata(artist):
"""Return metatata from ScanImage artist tag as dict."""
try:
return json.loads(artist)
except ValueError:
warnings.warn("invalid JSON '%s'" % artist)
def _replace_by(module_function, package=__package__, warn=None, prefix='_'):
"""Try replace decorated function by module.function."""
return lambda f: f # imageio: just use what's in here
def _warn(e, warn):
if warn is None:
warn = '\n Functionality might be degraded or be slow.\n'
elif warn is True:
warn = ''
elif not warn:
return
warnings.warn('%s%s' % (e, warn))
try:
from importlib import import_module
except ImportError as e:
_warn(e, warn)
return identityfunc
def decorate(func, module_function=module_function, warn=warn):
module, function = module_function.split('.')
try:
if package:
module = import_module('.' + module, package=package)
else:
module = import_module(module)
except Exception as e:
_warn(e, warn)
return func
try:
func, oldfunc = getattr(module, function), func
except Exception as e:
_warn(e, warn)
return func
globals()[prefix + func.__name__] = oldfunc
return func
return decorate
def decode_floats(data):
"""Decode floating point horizontal differencing.
The TIFF predictor type 3 reorders the bytes of the image values and
applies horizontal byte differencing to improve compression of floating
point images. The ordering of interleaved color channels is preserved.
Parameters
----------
data : numpy.ndarray
The image to be decoded. The dtype must be a floating point.
The shape must include the number of contiguous samples per pixel
even if 1.
"""
shape = data.shape
dtype = data.dtype
if len(shape) < 3:
raise ValueError('invalid data shape')
if dtype.char not in 'dfe':
raise ValueError('not a floating point image')
littleendian = data.dtype.byteorder == '<' or (
sys.byteorder == 'little' and data.dtype.byteorder == '=')
# undo horizontal byte differencing
data = data.view('uint8')
data.shape = shape[:-2] + (-1,) + shape[-1:]
numpy.cumsum(data, axis=-2, dtype='uint8', out=data)
# reorder bytes
if littleendian:
data.shape = shape[:-2] + (-1,) + shape[-2:]
data = numpy.swapaxes(data, -3, -2)
data = numpy.swapaxes(data, -2, -1)
data = data[..., ::-1]
# back to float
data = numpy.ascontiguousarray(data)
data = data.view(dtype)
data.shape = shape
return data
@_replace_by('_tifffile.decode_packbits')
def decode_packbits(encoded):
"""Decompress PackBits encoded byte string.
PackBits is a simple byte-oriented run-length compression scheme.
"""
func = ord if sys.version[0] == '2' else identityfunc
result = []
result_extend = result.extend
i = 0
try:
while True:
n = func(encoded[i]) + 1
i += 1
if n < 129:
result_extend(encoded[i:i+n])
i += n
elif n > 129:
result_extend(encoded[i:i+1] * (258-n))
i += 1
except IndexError:
pass
return b''.join(result) if sys.version[0] == '2' else bytes(result)
@_replace_by('_tifffile.decode_lzw')
def decode_lzw(encoded):
"""Decompress LZW (Lempel-Ziv-Welch) encoded TIFF strip (byte string).
The strip must begin with a CLEAR code and end with an EOI code.
This implementation of the LZW decoding algorithm is described in (1) and
is not compatible with old style LZW compressed files like quad-lzw.tif.
"""
len_encoded = len(encoded)
bitcount_max = len_encoded * 8
unpack = struct.unpack
if sys.version[0] == '2':
newtable = [chr(i) for i in range(256)]
else:
newtable = [bytes([i]) for i in range(256)]
newtable.extend((0, 0))
def next_code():
"""Return integer of 'bitw' bits at 'bitcount' position in encoded."""
start = bitcount // 8
s = encoded[start:start+4]
try:
code = unpack('>I', s)[0]
except Exception:
code = unpack('>I', s + b'\x00'*(4-len(s)))[0]
code <<= bitcount % 8
code &= mask
return code >> shr
switchbitch = { # code: bit-width, shr-bits, bit-mask
255: (9, 23, int(9*'1'+'0'*23, 2)),
511: (10, 22, int(10*'1'+'0'*22, 2)),
1023: (11, 21, int(11*'1'+'0'*21, 2)),
2047: (12, 20, int(12*'1'+'0'*20, 2)), }
bitw, shr, mask = switchbitch[255]
bitcount = 0
if len_encoded < 4:
raise ValueError('strip must be at least 4 characters long')
if next_code() != 256:
raise ValueError('strip must begin with CLEAR code')
code = 0
oldcode = 0
result = []
result_append = result.append
while True:
code = next_code() # ~5% faster when inlining this function
bitcount += bitw
if code == 257 or bitcount >= bitcount_max: # EOI
break
if code == 256: # CLEAR
table = newtable[:]
table_append = table.append
lentable = 258
bitw, shr, mask = switchbitch[255]
code = next_code()
bitcount += bitw
if code == 257: # EOI
break
result_append(table[code])
else:
if code < lentable:
decoded = table[code]
newcode = table[oldcode] + decoded[:1]
else:
newcode = table[oldcode]
newcode += newcode[:1]
decoded = newcode
result_append(decoded)
table_append(newcode)
lentable += 1
oldcode = code
if lentable in switchbitch:
bitw, shr, mask = switchbitch[lentable]
if code != 257:
warnings.warn('unexpected end of LZW stream (code %i)' % code)
return b''.join(result)
@_replace_by('_tifffile.unpack_ints')
def unpack_ints(data, dtype, itemsize, runlen=0):
"""Decompress byte string to array of integers of any bit size <= 32.
This Python implementation is slow and only handles itemsizes 1, 2, 4, 8,
16, 32, and 64.
Parameters
----------
data : byte str
Data to decompress.
dtype : numpy.dtype or str
A numpy boolean or integer type.
itemsize : int
Number of bits per integer.
runlen : int
Number of consecutive integers, after which to start at next byte.
Examples
--------
>>> unpack_ints(b'a', 'B', 1)
array([0, 1, 1, 0, 0, 0, 0, 1], dtype=uint8)
>>> unpack_ints(b'ab', 'B', 2)
array([1, 2, 0, 1, 1, 2, 0, 2], dtype=uint8)
"""
if itemsize == 1: # bitarray
data = numpy.frombuffer(data, '|B')
data = numpy.unpackbits(data)
if runlen % 8:
data = data.reshape(-1, runlen + (8 - runlen % 8))
data = data[:, :runlen].reshape(-1)
return data.astype(dtype)
dtype = numpy.dtype(dtype)
if itemsize in (8, 16, 32, 64):
return numpy.frombuffer(data, dtype)
if itemsize not in (1, 2, 4, 8, 16, 32):
raise ValueError('itemsize not supported: %i' % itemsize)
if dtype.kind not in 'biu':
raise ValueError('invalid dtype')
itembytes = next(i for i in (1, 2, 4, 8) if 8 * i >= itemsize)
if itembytes != dtype.itemsize:
raise ValueError('dtype.itemsize too small')
if runlen == 0:
runlen = (8 * len(data)) // itemsize
skipbits = runlen * itemsize % 8
if skipbits:
skipbits = 8 - skipbits
shrbits = itembytes*8 - itemsize
bitmask = int(itemsize*'1'+'0'*shrbits, 2)
dtypestr = '>' + dtype.char # dtype always big-endian?
unpack = struct.unpack
size = runlen * (len(data)*8 // (runlen*itemsize + skipbits))
result = numpy.empty((size,), dtype)
bitcount = 0
for i in range(size):
start = bitcount // 8
s = data[start:start+itembytes]
try:
code = unpack(dtypestr, s)[0]
except Exception:
code = unpack(dtypestr, s + b'\x00'*(itembytes-len(s)))[0]
code <<= bitcount % 8
code &= bitmask
result[i] = code >> shrbits
bitcount += itemsize
if (i+1) % runlen == 0:
bitcount += skipbits
return result
def unpack_rgb(data, dtype='<B', bitspersample=(5, 6, 5), rescale=True):
"""Return array from byte string containing packed samples.
Use to unpack RGB565 or RGB555 to RGB888 format.
Parameters
----------
data : byte str
The data to be decoded. Samples in each pixel are stored consecutively.
Pixels are aligned to 8, 16, or 32 bit boundaries.
dtype : numpy.dtype
The sample data type. The byteorder applies also to the data stream.
bitspersample : tuple
Number of bits for each sample in a pixel.
rescale : bool
Upscale samples to the number of bits in dtype.
Returns
-------
result : ndarray
Flattened array of unpacked samples of native dtype.
Examples
--------
>>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff)
>>> print(unpack_rgb(data, '<B', (5, 6, 5), False))
[ 1 1 1 31 63 31]
>>> print(unpack_rgb(data, '<B', (5, 6, 5)))
[ 8 4 8 255 255 255]
>>> print(unpack_rgb(data, '<B', (5, 5, 5)))
[ 16 8 8 255 255 255]
"""
dtype = numpy.dtype(dtype)
bits = int(numpy.sum(bitspersample))
if not (bits <= 32 and all(i <= dtype.itemsize*8 for i in bitspersample)):
raise ValueError('sample size not supported: %s' % str(bitspersample))
dt = next(i for i in 'BHI' if numpy.dtype(i).itemsize*8 >= bits)
data = numpy.frombuffer(data, dtype.byteorder+dt)
result = numpy.empty((data.size, len(bitspersample)), dtype.char)
for i, bps in enumerate(bitspersample):
t = data >> int(numpy.sum(bitspersample[i+1:]))
t &= int('0b'+'1'*bps, 2)
if rescale:
o = ((dtype.itemsize * 8) // bps + 1) * bps
if o > data.dtype.itemsize * 8:
t = t.astype('I')
t *= (2**o - 1) // (2**bps - 1)
t //= 2**(o - (dtype.itemsize * 8))
result[:, i] = t
return result.reshape(-1)
@_replace_by('_tifffile.reverse_bitorder')
def reverse_bitorder(data):
"""Reverse bits in each byte of byte string or numpy array.
Decode data where pixels with lower column values are stored in the
lower-order bits of the bytes (FillOrder is LSB2MSB).
Parameters
----------
data : byte string or ndarray
The data to be bit reversed. If byte string, a new bit-reversed byte
string is returned. Numpy arrays are bit-reversed in-place.
Examples
--------
>>> reverse_bitorder(b'\\x01\\x64')
b'\\x80&'
>>> data = numpy.array([1, 666], dtype='uint16')
>>> reverse_bitorder(data)
>>> data
array([ 128, 16473], dtype=uint16)
"""
try:
view = data.view('uint8')
numpy.take(TIFF.REVERSE_BITORDER_ARRAY, view, out=view)
except AttributeError:
return data.translate(TIFF.REVERSE_BITORDER_BYTES)
except ValueError:
raise NotImplementedError('slices of arrays not supported')
def apply_colormap(image, colormap, contig=True):
"""Return palette-colored image.
The image values are used to index the colormap on axis 1. The returned
image is of shape image.shape+colormap.shape[0] and dtype colormap.dtype.
Parameters
----------
image : numpy.ndarray
Indexes into the colormap.
colormap : numpy.ndarray
RGB lookup table aka palette of shape (3, 2**bits_per_sample).
contig : bool
If True, return a contiguous array.
Examples
--------
>>> image = numpy.arange(256, dtype='uint8')
>>> colormap = numpy.vstack([image, image, image]).astype('uint16') * 256
>>> apply_colormap(image, colormap)[-1]
array([65280, 65280, 65280], dtype=uint16)
"""
image = numpy.take(colormap, image, axis=1)
image = numpy.rollaxis(image, 0, image.ndim)
if contig:
image = numpy.ascontiguousarray(image)
return image
def reorient(image, orientation):
"""Return reoriented view of image array.
Parameters
----------
image : numpy.ndarray
Non-squeezed output of asarray() functions.
Axes -3 and -2 must be image length and width respectively.
orientation : int or str
One of TIFF.ORIENTATION names or values.
"""
ORIENTATION = TIFF.ORIENTATION
orientation = enumarg(ORIENTATION, orientation)
if orientation == ORIENTATION.TOPLEFT:
return image
elif orientation == ORIENTATION.TOPRIGHT:
return image[..., ::-1, :]
elif orientation == ORIENTATION.BOTLEFT:
return image[..., ::-1, :, :]
elif orientation == ORIENTATION.BOTRIGHT:
return image[..., ::-1, ::-1, :]
elif orientation == ORIENTATION.LEFTTOP:
return numpy.swapaxes(image, -3, -2)
elif orientation == ORIENTATION.RIGHTTOP:
return numpy.swapaxes(image, -3, -2)[..., ::-1, :]
elif orientation == ORIENTATION.RIGHTBOT:
return numpy.swapaxes(image, -3, -2)[..., ::-1, :, :]
elif orientation == ORIENTATION.LEFTBOT:
return numpy.swapaxes(image, -3, -2)[..., ::-1, ::-1, :]
def repeat_nd(a, repeats):
"""Return read-only view into input array with elements repeated.
Zoom nD image by integer factors using nearest neighbor interpolation
(box filter).
Parameters
----------
a : array_like
Input array.
repeats : sequence of int
The number of repetitions to apply along each dimension of input array.
Example
-------
>>> repeat_nd([[1, 2], [3, 4]], (2, 2))
array([[1, 1, 2, 2],
[1, 1, 2, 2],
[3, 3, 4, 4],
[3, 3, 4, 4]])
"""
a = numpy.asarray(a)
reshape = []
shape = []
strides = []
for i, j, k in zip(a.strides, a.shape, repeats):
shape.extend((j, k))
strides.extend((i, 0))
reshape.append(j * k)
return numpy.lib.stride_tricks.as_strided(
a, shape, strides, writeable=False).reshape(reshape)
def reshape_nd(data_or_shape, ndim):
"""Return image array or shape with at least ndim dimensions.
Prepend 1s to image shape as necessary.
>>> reshape_nd(numpy.empty(0), 1).shape
(0,)
>>> reshape_nd(numpy.empty(1), 2).shape
(1, 1)
>>> reshape_nd(numpy.empty((2, 3)), 3).shape
(1, 2, 3)
>>> reshape_nd(numpy.empty((3, 4, 5)), 3).shape
(3, 4, 5)
>>> reshape_nd((2, 3), 3)
(1, 2, 3)
"""
is_shape = isinstance(data_or_shape, tuple)
shape = data_or_shape if is_shape else data_or_shape.shape
if len(shape) >= ndim:
return data_or_shape
shape = (1,) * (ndim - len(shape)) + shape
return shape if is_shape else data_or_shape.reshape(shape)
def squeeze_axes(shape, axes, skip='XY'):
"""Return shape and axes with single-dimensional entries removed.
Remove unused dimensions unless their axes are listed in 'skip'.
>>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC')
((5, 2, 1), 'TYX')
"""
if len(shape) != len(axes):
raise ValueError('dimensions of axes and shape do not match')
shape, axes = zip(*(i for i in zip(shape, axes)
if i[0] > 1 or i[1] in skip))
return tuple(shape), ''.join(axes)
def transpose_axes(image, axes, asaxes='CTZYX'):
"""Return image with its axes permuted to match specified axes.
A view is returned if possible.
>>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape
(5, 2, 1, 3, 4)
"""
for ax in axes:
if ax not in asaxes:
raise ValueError('unknown axis %s' % ax)
# add missing axes to image
shape = image.shape
for ax in reversed(asaxes):
if ax not in axes:
axes = ax + axes
shape = (1,) + shape
image = image.reshape(shape)
# transpose axes
image = image.transpose([axes.index(ax) for ax in asaxes])
return image
def reshape_axes(axes, shape, newshape, unknown='Q'):
"""Return axes matching new shape.
Unknown dimensions are labelled 'Q'.
>>> reshape_axes('YXS', (219, 301, 1), (219, 301))
'YX'
>>> reshape_axes('IYX', (12, 219, 301), (3, 4, 219, 1, 301, 1))
'QQYQXQ'
"""
shape = tuple(shape)
newshape = tuple(newshape)
if len(axes) != len(shape):
raise ValueError('axes do not match shape')
size = product(shape)
newsize = product(newshape)
if size != newsize:
raise ValueError('cannot reshape %s to %s' % (shape, newshape))
if not axes or not newshape:
return ''
lendiff = max(0, len(shape) - len(newshape))
if lendiff:
newshape = newshape + (1,) * lendiff
i = len(shape)-1
prodns = 1
prods = 1
result = []
for ns in newshape[::-1]:
prodns *= ns
while i > 0 and shape[i] == 1 and ns != 1:
i -= 1
if ns == shape[i] and prodns == prods*shape[i]:
prods *= shape[i]
result.append(axes[i])
i -= 1
else:
result.append(unknown)
return ''.join(reversed(result[lendiff:]))
def stack_pages(pages, out=None, maxworkers=1, *args, **kwargs):
"""Read data from sequence of TiffPage and stack them vertically.
Additional parameters are passsed to the TiffPage.asarray function.
"""
npages = len(pages)
if npages == 0:
raise ValueError('no pages')
if npages == 1:
return pages[0].asarray(out=out, *args, **kwargs)
page0 = next(p for p in pages if p is not None)
page0.asarray(validate=None) # ThreadPoolExecutor swallows exceptions
shape = (npages,) + page0.keyframe.shape
dtype = page0.keyframe.dtype
out = create_output(out, shape, dtype)
if maxworkers is None:
maxworkers = multiprocessing.cpu_count() // 2
page0.parent.filehandle.lock = maxworkers > 1
filecache = OpenFileCache(size=max(4, maxworkers),
lock=page0.parent.filehandle.lock)
def func(page, index, out=out, filecache=filecache,
args=args, kwargs=kwargs):
"""Read, decode, and copy page data."""
if page is not None:
filecache.open(page.parent.filehandle)
out[index] = page.asarray(lock=filecache.lock, reopen=False,
validate=False, *args, **kwargs)
filecache.close(page.parent.filehandle)
if maxworkers < 2:
for i, page in enumerate(pages):
func(page, i)
else:
with concurrent.futures.ThreadPoolExecutor(maxworkers) as executor:
executor.map(func, pages, range(npages))
filecache.clear()
page0.parent.filehandle.lock = None
return out
def clean_offsets_counts(offsets, counts):
"""Return cleaned offsets and byte counts.
Remove zero offsets and counts. Use to sanitize _offsets and _bytecounts
tag values for strips or tiles.
"""
offsets = list(offsets)
counts = list(counts)
assert len(offsets) == len(counts)
j = 0
for i, (o, b) in enumerate(zip(offsets, counts)):
if o > 0 and b > 0:
if i > j:
offsets[j] = o
counts[j] = b
j += 1
elif b > 0 and o <= 0:
raise ValueError('invalid offset')
else:
warnings.warn('empty byte count')
if j == 0:
j = 1
return offsets[:j], counts[:j]
def buffered_read(fh, lock, offsets, bytecounts, buffersize=2**26):
"""Return iterator over blocks read from file."""
length = len(offsets)
i = 0
while i < length:
data = []
with lock:
size = 0
while size < buffersize and i < length:
fh.seek(offsets[i])
bytecount = bytecounts[i]
data.append(fh.read(bytecount))
size += bytecount
i += 1
for block in data:
yield block
def create_output(out, shape, dtype, mode='w+', suffix='.memmap'):
"""Return numpy array where image data of shape and dtype can be copied.
The 'out' parameter may have the following values or types:
None
An empty array of shape and dtype is created and returned.
numpy.ndarray
An existing writable array of compatible dtype and shape. A view of
the same array is returned after verification.
'memmap' or 'memmap:tempdir'
A memory-map to an array stored in a temporary binary file on disk
is created and returned.
str or open file
The file name or file object used to create a memory-map to an array
stored in a binary file on disk. The created memory-mapped array is
returned.
"""
if out is None:
return numpy.zeros(shape, dtype)
if isinstance(out, str) and out[:6] == 'memmap':
tempdir = out[7:] if len(out) > 7 else None
with tempfile.NamedTemporaryFile(dir=tempdir, suffix=suffix) as fh:
return numpy.memmap(fh, shape=shape, dtype=dtype, mode=mode)
if isinstance(out, numpy.ndarray):
if product(shape) != product(out.shape):
raise ValueError('incompatible output shape')
if not numpy.can_cast(dtype, out.dtype):
raise ValueError('incompatible output dtype')
return out.reshape(shape)
if isinstance(out, pathlib.Path):
out = str(out)
return numpy.memmap(out, shape=shape, dtype=dtype, mode=mode)
def matlabstr2py(string):
"""Return Python object from Matlab string representation.
Return str, bool, int, float, list (Matlab arrays or cells), or
dict (Matlab structures) types.
Use to access ScanImage metadata.
>>> matlabstr2py('1')
1
>>> matlabstr2py("['x y z' true false; 1 2.0 -3e4; NaN Inf @class]")
[['x y z', True, False], [1, 2.0, -30000.0], [nan, inf, '@class']]
>>> d = matlabstr2py("SI.hChannels.channelType = {'stripe' 'stripe'}\\n"
... "SI.hChannels.channelsActive = 2")
>>> d['SI.hChannels.channelType']
['stripe', 'stripe']
"""
# TODO: handle invalid input
# TODO: review unboxing of multidimensional arrays
def lex(s):
# return sequence of tokens from matlab string representation
tokens = ['[']
while True:
t, i = next_token(s)
if t is None:
break
if t == ';':
tokens.extend((']', '['))
elif t == '[':
tokens.extend(('[', '['))
elif t == ']':
tokens.extend((']', ']'))
else:
tokens.append(t)
s = s[i:]
tokens.append(']')
return tokens
def next_token(s):
# return next token in matlab string
length = len(s)
if length == 0:
return None, 0
i = 0
while i < length and s[i] == ' ':
i += 1
if i == length:
return None, i
if s[i] in '{[;]}':
return s[i], i + 1
if s[i] == "'":
j = i + 1
while j < length and s[j] != "'":
j += 1
return s[i: j+1], j + 1
if s[i] == '<':
j = i + 1
while j < length and s[j] != '>':
j += 1
return s[i: j+1], j + 1
j = i
while j < length and s[j] not in ' {[;]}':
j += 1
return s[i:j], j
def value(s, fail=False):
# return Python value of token
s = s.strip()
if not s:
return s
if len(s) == 1:
try:
return int(s)
except Exception:
if fail:
raise ValueError()
return s
if s[0] == "'":
if fail and s[-1] != "'" or "'" in s[1:-1]:
raise ValueError()
return s[1:-1]
if s[0] == '<':
if fail and s[-1] != '>' or '<' in s[1:-1]:
raise ValueError()
return s
if fail and any(i in s for i in " ';[]{}"):
raise ValueError()
if s[0] == '@':
return s
if s in ('true', 'True'):
return True
if s in ('false', 'False'):
return False
if s[:6] == 'zeros(':
return numpy.zeros([int(i) for i in s[6:-1].split(',')]).tolist()
if s[:5] == 'ones(':
return numpy.ones([int(i) for i in s[5:-1].split(',')]).tolist()
if '.' in s or 'e' in s:
try:
return float(s)
except Exception:
pass
try:
return int(s)
except Exception:
pass
try:
return float(s) # nan, inf
except Exception:
if fail:
raise ValueError()
return s
def parse(s):
# return Python value from string representation of Matlab value
s = s.strip()
try:
return value(s, fail=True)
except ValueError:
pass
result = add2 = []
levels = [add2]
for t in lex(s):
if t in '[{':
add2 = []
levels.append(add2)
elif t in ']}':
x = levels.pop()
if len(x) == 1 and isinstance(x[0], (list, str)):
x = x[0]
add2 = levels[-1]
add2.append(x)
else:
add2.append(value(t))
if len(result) == 1 and isinstance(result[0], (list, str)):
result = result[0]
return result
if '\r' in string or '\n' in string:
# structure
d = {}
for line in string.splitlines():
line = line.strip()
if not line or line[0] == '%':
continue
k, v = line.split('=', 1)
k = k.strip()
if any(c in k for c in " ';[]{}<>"):
continue
d[k] = parse(v)
return d
return parse(string)
def stripnull(string, null=b'\x00'):
"""Return string truncated at first null character.
Clean NULL terminated C strings. For unicode strings use null='\\0'.
>>> stripnull(b'string\\x00')
b'string'
>>> stripnull('string\\x00', null='\\0')
'string'
"""
i = string.find(null)
return string if (i < 0) else string[:i]
def stripascii(string):
"""Return string truncated at last byte that is 7-bit ASCII.
Clean NULL separated and terminated TIFF strings.
>>> stripascii(b'string\\x00string\\n\\x01\\x00')
b'string\\x00string\\n'
>>> stripascii(b'\\x00')
b''
"""
# TODO: pythonize this
i = len(string)
while i:
i -= 1
if 8 < byte2int(string[i]) < 127:
break
else:
i = -1
return string[:i+1]
def asbool(value, true=(b'true', u'true'), false=(b'false', u'false')):
"""Return string as bool if possible, else raise TypeError.
>>> asbool(b' False ')
False
"""
value = value.strip().lower()
if value in true: # might raise UnicodeWarning/BytesWarning
return True
if value in false:
return False
raise TypeError()
def astype(value, types=None):
"""Return argument as one of types if possible.
>>> astype('42')
42
>>> astype('3.14')
3.14
>>> astype('True')
True
>>> astype(b'Neee-Wom')
'Neee-Wom'
"""
if types is None:
types = int, float, asbool, bytes2str
for typ in types:
try:
return typ(value)
except (ValueError, AttributeError, TypeError, UnicodeEncodeError):
pass
return value
def format_size(size, threshold=1536):
"""Return file size as string from byte size.
>>> format_size(1234)
'1234 B'
>>> format_size(12345678901)
'11.50 GiB'
"""
if size < threshold:
return "%i B" % size
for unit in ('KiB', 'MiB', 'GiB', 'TiB', 'PiB'):
size /= 1024.0
if size < threshold:
return "%.2f %s" % (size, unit)
def identityfunc(arg):
"""Single argument identity function.
>>> identityfunc('arg')
'arg'
"""
return arg
def nullfunc(*args, **kwargs):
"""Null function.
>>> nullfunc('arg', kwarg='kwarg')
"""
return
def sequence(value):
"""Return tuple containing value if value is not a sequence.
>>> sequence(1)
(1,)
>>> sequence([1])
[1]
"""
try:
len(value)
return value
except TypeError:
return (value,)
def product(iterable):
"""Return product of sequence of numbers.
Equivalent of functools.reduce(operator.mul, iterable, 1).
Multiplying numpy integers might overflow.
>>> product([2**8, 2**30])
274877906944
>>> product([])
1
"""
prod = 1
for i in iterable:
prod *= i
return prod
def natural_sorted(iterable):
"""Return human sorted list of strings.
E.g. for sorting file names.
>>> natural_sorted(['f1', 'f2', 'f10'])
['f1', 'f2', 'f10']
"""
def sortkey(x):
return [(int(c) if c.isdigit() else c) for c in re.split(numbers, x)]
numbers = re.compile(r'(\d+)')
return sorted(iterable, key=sortkey)
def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)):
"""Return datetime object from timestamp in Excel serial format.
Convert LSM time stamps.
>>> excel_datetime(40237.029999999795)
datetime.datetime(2010, 2, 28, 0, 43, 11, 999982)
"""
return epoch + datetime.timedelta(timestamp)
def julian_datetime(julianday, milisecond=0):
"""Return datetime from days since 1/1/4713 BC and ms since midnight.
Convert Julian dates according to MetaMorph.
>>> julian_datetime(2451576, 54362783)
datetime.datetime(2000, 2, 2, 15, 6, 2, 783)
"""
if julianday <= 1721423:
# no datetime before year 1
return None
a = julianday + 1
if a > 2299160:
alpha = math.trunc((a - 1867216.25) / 36524.25)
a += 1 + alpha - alpha // 4
b = a + (1524 if a > 1721423 else 1158)
c = math.trunc((b - 122.1) / 365.25)
d = math.trunc(365.25 * c)
e = math.trunc((b - d) / 30.6001)
day = b - d - math.trunc(30.6001 * e)
month = e - (1 if e < 13.5 else 13)
year = c - (4716 if month > 2.5 else 4715)
hour, milisecond = divmod(milisecond, 1000 * 60 * 60)
minute, milisecond = divmod(milisecond, 1000 * 60)
second, milisecond = divmod(milisecond, 1000)
return datetime.datetime(year, month, day,
hour, minute, second, milisecond)
def byteorder_isnative(byteorder):
"""Return if byteorder matches the system's byteorder.
>>> byteorder_isnative('=')
True
"""
if byteorder == '=' or byteorder == sys.byteorder:
return True
keys = {'big': '>', 'little': '<'}
return keys.get(byteorder, byteorder) == keys[sys.byteorder]
def recarray2dict(recarray):
"""Return numpy.recarray as dict."""
# TODO: subarrays
result = {}
for descr, value in zip(recarray.dtype.descr, recarray):
name, dtype = descr[:2]
if dtype[1] == 'S':
value = bytes2str(stripnull(value))
elif value.ndim < 2:
value = value.tolist()
result[name] = value
return result
def xml2dict(xml, sanitize=True, prefix=None):
"""Return XML as dict.
>>> xml2dict('<?xml version="1.0" ?><root attr="name"><key>1</key></root>')
{'root': {'key': 1, 'attr': 'name'}}
"""
from xml.etree import cElementTree as etree # delayed import
at = tx = ''
if prefix:
at, tx = prefix
def astype(value):
# return value as int, float, bool, or str
for t in (int, float, asbool):
try:
return t(value)
except Exception:
pass
return value
def etree2dict(t):
# adapted from https://stackoverflow.com/a/10077069/453463
key = t.tag
if sanitize:
key = key.rsplit('}', 1)[-1]
d = {key: {} if t.attrib else None}
children = list(t)
if children:
dd = collections.defaultdict(list)
for dc in map(etree2dict, children):
for k, v in dc.items():
dd[k].append(astype(v))
d = {key: {k: astype(v[0]) if len(v) == 1 else astype(v)
for k, v in dd.items()}}
if t.attrib:
d[key].update((at + k, astype(v)) for k, v in t.attrib.items())
if t.text:
text = t.text.strip()
if children or t.attrib:
if text:
d[key][tx + 'value'] = astype(text)
else:
d[key] = astype(text)
return d
return etree2dict(etree.fromstring(xml))
def hexdump(bytestr, width=75, height=24, snipat=-2, modulo=2, ellipsis='...'):
"""Return hexdump representation of byte string.
>>> hexdump(binascii.unhexlify('49492a00080000000e00fe0004000100'))
'49 49 2a 00 08 00 00 00 0e 00 fe 00 04 00 01 00 II*.............'
"""
size = len(bytestr)
if size < 1 or width < 2 or height < 1:
return ''
if height == 1:
addr = b''
bytesperline = min(modulo * (((width - len(addr)) // 4) // modulo),
size)
if bytesperline < 1:
return ''
nlines = 1
else:
addr = b'%%0%ix: ' % len(b'%x' % size)
bytesperline = min(modulo * (((width - len(addr % 1)) // 4) // modulo),
size)
if bytesperline < 1:
return ''
width = 3*bytesperline + len(addr % 1)
nlines = (size - 1) // bytesperline + 1
if snipat is None or snipat == 1:
snipat = height
elif 0 < abs(snipat) < 1:
snipat = int(math.floor(height * snipat))
if snipat < 0:
snipat += height
if height == 1 or nlines == 1:
blocks = [(0, bytestr[:bytesperline])]
addr = b''
height = 1
width = 3 * bytesperline
elif height is None or nlines <= height:
blocks = [(0, bytestr)]
elif snipat <= 0:
start = bytesperline * (nlines - height)
blocks = [(start, bytestr[start:])] # (start, None)
elif snipat >= height or height < 3:
end = bytesperline * height
blocks = [(0, bytestr[:end])] # (end, None)
else:
end1 = bytesperline * snipat
end2 = bytesperline * (height - snipat - 1)
blocks = [(0, bytestr[:end1]),
(size-end1-end2, None),
(size-end2, bytestr[size-end2:])]
ellipsis = str2bytes(ellipsis)
result = []
for start, bytestr in blocks:
if bytestr is None:
result.append(ellipsis) # 'skip %i bytes' % start)
continue
hexstr = binascii.hexlify(bytestr)
strstr = re.sub(br'[^\x20-\x7f]', b'.', bytestr)
for i in range(0, len(bytestr), bytesperline):
h = hexstr[2*i:2*i+bytesperline*2]
r = (addr % (i + start)) if height > 1 else addr
r += b' '.join(h[i:i+2] for i in range(0, 2*bytesperline, 2))
r += b' ' * (width - len(r))
r += strstr[i:i+bytesperline]
result.append(r)
result = b'\n'.join(result)
if sys.version_info[0] == 3:
result = result.decode('ascii')
return result
def isprintable(string):
"""Return if all characters in string are printable.
>>> isprintable('abc')
True
>>> isprintable(b'\01')
False
"""
string = string.strip()
if len(string) < 1:
return True
if sys.version_info[0] == 3:
try:
return string.isprintable()
except Exception:
pass
try:
return string.decode('utf-8').isprintable()
except Exception:
pass
else:
if string.isalnum():
return True
printable = ('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRST'
'UVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c')
return all(c in printable for c in string)
def clean_whitespace(string, compact=False):
"""Return string with compressed whitespace."""
for a, b in (('\r\n', '\n'), ('\r', '\n'), ('\n\n', '\n'),
('\t', ' '), (' ', ' ')):
string = string.replace(a, b)
if compact:
for a, b in (('\n', ' '), ('[ ', '['),
(' ', ' '), (' ', ' '), (' ', ' ')):
string = string.replace(a, b)
return string.strip()
def pformat_xml(xml):
"""Return pretty formatted XML."""
try:
import lxml.etree as etree # delayed import
if not isinstance(xml, bytes):
xml = xml.encode('utf-8')
xml = etree.parse(io.BytesIO(xml))
xml = etree.tostring(xml, pretty_print=True, xml_declaration=True,
encoding=xml.docinfo.encoding)
xml = bytes2str(xml)
except Exception:
if isinstance(xml, bytes):
xml = bytes2str(xml)
xml = xml.replace('><', '>\n<')
return xml.replace(' ', ' ').replace('\t', ' ')
def pformat(arg, width=79, height=24, compact=True):
"""Return pretty formatted representation of object as string.
Whitespace might be altered.
"""
if height is None or height < 1:
height = 1024
if width is None or width < 1:
width = 256
npopt = numpy.get_printoptions()
numpy.set_printoptions(threshold=100, linewidth=width)
if isinstance(arg, basestring):
if arg[:5].lower() in ('<?xml', b'<?xml'):
if height == 1:
arg = arg[:4*width]
else:
arg = pformat_xml(arg)
elif isinstance(arg, bytes):
if isprintable(arg):
arg = bytes2str(arg)
arg = clean_whitespace(arg)
else:
numpy.set_printoptions(**npopt)
return hexdump(arg, width=width, height=height, modulo=1)
arg = arg.rstrip()
elif isinstance(arg, numpy.record):
arg = arg.pprint()
else:
import pprint # delayed import
compact = {} if sys.version_info[0] == 2 else dict(compact=compact)
arg = pprint.pformat(arg, width=width, **compact)
numpy.set_printoptions(**npopt)
if height == 1:
arg = clean_whitespace(arg, compact=True)
return arg[:width]
argl = list(arg.splitlines())
if len(argl) > height:
arg = '\n'.join(argl[:height//2] + ['...'] + argl[-height//2:])
return arg
def snipstr(string, width=79, snipat=0.5, ellipsis='...'):
"""Return string cut to specified length.
>>> snipstr('abcdefghijklmnop', 8)
'abc...op'
"""
if ellipsis is None:
if isinstance(string, bytes):
ellipsis = b'...'
else:
ellipsis = u'\u2026' # does not print on win-py3.5
esize = len(ellipsis)
splitlines = string.splitlines()
# TODO: finish and test multiline snip
result = []
for line in splitlines:
if line is None:
result.append(ellipsis)
continue
linelen = len(line)
if linelen <= width:
result.append(string)
continue
split = snipat
if split is None or split == 1:
split = linelen
elif 0 < abs(split) < 1:
split = int(math.floor(linelen * split))
if split < 0:
split += linelen
if split < 0:
split = 0
if esize == 0 or width < esize + 1:
if split <= 0:
result.append(string[-width:])
else:
result.append(string[:width])
elif split <= 0:
result.append(ellipsis + string[esize-width:])
elif split >= linelen or width < esize + 4:
result.append(string[:width-esize] + ellipsis)
else:
splitlen = linelen - width + esize
end1 = split - splitlen // 2
end2 = end1 + splitlen
result.append(string[:end1] + ellipsis + string[end2:])
if isinstance(string, bytes):
return b'\n'.join(result)
else:
return '\n'.join(result)
def enumarg(enum, arg):
"""Return enum member from its name or value.
>>> enumarg(TIFF.PHOTOMETRIC, 2)
<PHOTOMETRIC.RGB: 2>
>>> enumarg(TIFF.PHOTOMETRIC, 'RGB')
<PHOTOMETRIC.RGB: 2>
"""
try:
return enum(arg)
except Exception:
try:
return enum[arg.upper()]
except Exception:
raise ValueError('invalid argument %s' % arg)
def parse_kwargs(kwargs, *keys, **keyvalues):
"""Return dict with keys from keys|keyvals and values from kwargs|keyvals.
Existing keys are deleted from kwargs.
>>> kwargs = {'one': 1, 'two': 2, 'four': 4}
>>> kwargs2 = parse_kwargs(kwargs, 'two', 'three', four=None, five=5)
>>> kwargs == {'one': 1}
True
>>> kwargs2 == {'two': 2, 'four': 4, 'five': 5}
True
"""
result = {}
for key in keys:
if key in kwargs:
result[key] = kwargs[key]
del kwargs[key]
for key, value in keyvalues.items():
if key in kwargs:
result[key] = kwargs[key]
del kwargs[key]
else:
result[key] = value
return result
def update_kwargs(kwargs, **keyvalues):
"""Update dict with keys and values if keys do not already exist.
>>> kwargs = {'one': 1, }
>>> update_kwargs(kwargs, one=None, two=2)
>>> kwargs == {'one': 1, 'two': 2}
True
"""
for key, value in keyvalues.items():
if key not in kwargs:
kwargs[key] = value
def validate_jhove(filename, jhove='jhove', ignore=('More than 50 IFDs',)):
"""Validate TIFF file using jhove -m TIFF-hul.
Raise ValueError if jhove outputs an error message unless the message
contains one of the strings in 'ignore'.
JHOVE does not support bigtiff or more than 50 IFDs.
See `JHOVE TIFF-hul Module <http://jhove.sourceforge.net/tiff-hul.html>`_
"""
import subprocess # noqa: delayed import
out = subprocess.check_output([jhove, filename, '-m', 'TIFF-hul'])
if b'ErrorMessage: ' in out:
for line in out.splitlines():
line = line.strip()
if line.startswith(b'ErrorMessage: '):
error = line[14:].decode('utf8')
for i in ignore:
if i in error:
break
else:
raise ValueError(error)
break
def lsm2bin(lsmfile, binfile=None, tile=(256, 256), verbose=True):
"""Convert [MP]TZCYX LSM file to series of BIN files.
One BIN file containing 'ZCYX' data are created for each position, time,
and tile. The position, time, and tile indices are encoded at the end
of the filenames.
"""
verbose = print_ if verbose else nullfunc
if binfile is None:
binfile = lsmfile
elif binfile.lower() == 'none':
binfile = None
if binfile:
binfile += '_(z%ic%iy%ix%i)_m%%ip%%it%%03iy%%ix%%i.bin'
verbose('\nOpening LSM file... ', end='', flush=True)
start_time = time.time()
with TiffFile(lsmfile) as lsm:
if not lsm.is_lsm:
verbose('\n', lsm, flush=True)
raise ValueError('not a LSM file')
series = lsm.series[0] # first series contains the image data
shape = series.shape
axes = series.axes
dtype = series.dtype
size = product(shape) * dtype.itemsize
verbose('%.3f s' % (time.time() - start_time))
# verbose(lsm, flush=True)
verbose('Image\n axes: %s\n shape: %s\n dtype: %s\n size: %s'
% (axes, shape, dtype, format_size(size)), flush=True)
if not series.axes.endswith('TZCYX'):
raise ValueError('not a *TZCYX LSM file')
verbose('Copying image from LSM to BIN files', end='', flush=True)
start_time = time.time()
tiles = shape[-2] // tile[-2], shape[-1] // tile[-1]
if binfile:
binfile = binfile % (shape[-4], shape[-3], tile[0], tile[1])
shape = (1,) * (7-len(shape)) + shape
# cache for ZCYX stacks and output files
data = numpy.empty(shape[3:], dtype=dtype)
out = numpy.empty((shape[-4], shape[-3], tile[0], tile[1]),
dtype=dtype)
# iterate over Tiff pages containing data
pages = iter(series.pages)
for m in range(shape[0]): # mosaic axis
for p in range(shape[1]): # position axis
for t in range(shape[2]): # time axis
for z in range(shape[3]): # z slices
data[z] = next(pages).asarray()
for y in range(tiles[0]): # tile y
for x in range(tiles[1]): # tile x
out[:] = data[...,
y*tile[0]:(y+1)*tile[0],
x*tile[1]:(x+1)*tile[1]]
if binfile:
out.tofile(binfile % (m, p, t, y, x))
verbose('.', end='', flush=True)
verbose(' %.3f s' % (time.time() - start_time))
def imshow(data, title=None, vmin=0, vmax=None, cmap=None, bitspersample=None,
photometric='RGB', interpolation=None, dpi=96, figure=None,
subplot=111, maxdim=32768, **kwargs):
"""Plot n-dimensional images using matplotlib.pyplot.
Return figure, subplot and plot axis.
Requires pyplot already imported C{from matplotlib import pyplot}.
Parameters
----------
bitspersample : int or None
Number of bits per channel in integer RGB images.
photometric : {'MINISWHITE', 'MINISBLACK', 'RGB', or 'PALETTE'}
The color space of the image data.
title : str
Window and subplot title.
figure : matplotlib.figure.Figure (optional).
Matplotlib to use for plotting.
subplot : int
A matplotlib.pyplot.subplot axis.
maxdim : int
maximum image width and length.
kwargs : optional
Arguments for matplotlib.pyplot.imshow.
"""
isrgb = photometric in ('RGB',) # 'PALETTE', 'YCBCR'
if data.dtype.kind == 'b':
isrgb = False
if isrgb and not (data.shape[-1] in (3, 4) or (
data.ndim > 2 and data.shape[-3] in (3, 4))):
isrgb = False
photometric = 'MINISBLACK'
data = data.squeeze()
if photometric in ('MINISWHITE', 'MINISBLACK', None):
data = reshape_nd(data, 2)
else:
data = reshape_nd(data, 3)
dims = data.ndim
if dims < 2:
raise ValueError('not an image')
elif dims == 2:
dims = 0
isrgb = False
else:
if isrgb and data.shape[-3] in (3, 4):
data = numpy.swapaxes(data, -3, -2)
data = numpy.swapaxes(data, -2, -1)
elif not isrgb and (data.shape[-1] < data.shape[-2] // 8 and
data.shape[-1] < data.shape[-3] // 8 and
data.shape[-1] < 5):
data = numpy.swapaxes(data, -3, -1)
data = numpy.swapaxes(data, -2, -1)
isrgb = isrgb and data.shape[-1] in (3, 4)
dims -= 3 if isrgb else 2
if isrgb:
data = data[..., :maxdim, :maxdim, :maxdim]
else:
data = data[..., :maxdim, :maxdim]
if photometric == 'PALETTE' and isrgb:
datamax = data.max()
if datamax > 255:
data = data >> 8 # possible precision loss
data = data.astype('B')
elif data.dtype.kind in 'ui':
if not (isrgb and data.dtype.itemsize <= 1) or bitspersample is None:
try:
bitspersample = int(math.ceil(math.log(data.max(), 2)))
except Exception:
bitspersample = data.dtype.itemsize * 8
elif not isinstance(bitspersample, inttypes):
# bitspersample can be tuple, e.g. (5, 6, 5)
bitspersample = data.dtype.itemsize * 8
datamax = 2**bitspersample
if isrgb:
if bitspersample < 8:
data = data << (8 - bitspersample)
elif bitspersample > 8:
data = data >> (bitspersample - 8) # precision loss
data = data.astype('B')
elif data.dtype.kind == 'f':
datamax = data.max()
if isrgb and datamax > 1.0:
if data.dtype.char == 'd':
data = data.astype('f')
data /= datamax
else:
data = data / datamax
elif data.dtype.kind == 'b':
datamax = 1
elif data.dtype.kind == 'c':
data = numpy.absolute(data)
datamax = data.max()
if not isrgb:
if vmax is None:
vmax = datamax
if vmin is None:
if data.dtype.kind == 'i':
dtmin = numpy.iinfo(data.dtype).min
vmin = numpy.min(data)
if vmin == dtmin:
vmin = numpy.min(data > dtmin)
if data.dtype.kind == 'f':
dtmin = numpy.finfo(data.dtype).min
vmin = numpy.min(data)
if vmin == dtmin:
vmin = numpy.min(data > dtmin)
else:
vmin = 0
pyplot = sys.modules['matplotlib.pyplot']
if figure is None:
pyplot.rc('font', family='sans-serif', weight='normal', size=8)
figure = pyplot.figure(dpi=dpi, figsize=(10.3, 6.3), frameon=True,
facecolor='1.0', edgecolor='w')
try:
figure.canvas.manager.window.title(title)
except Exception:
pass
size = len(title.splitlines()) if title else 1
pyplot.subplots_adjust(bottom=0.03*(dims+2), top=0.98-size*0.03,
left=0.1, right=0.95, hspace=0.05, wspace=0.0)
subplot = pyplot.subplot(subplot)
if title:
try:
title = unicode(title, 'Windows-1252')
except TypeError:
pass
pyplot.title(title, size=11)
if cmap is None:
if data.dtype.char == '?':
cmap = 'gray'
elif data.dtype.kind in 'buf' or vmin == 0:
cmap = 'viridis'
else:
cmap = 'coolwarm'
if photometric == 'MINISWHITE':
cmap += '_r'
image = pyplot.imshow(numpy.atleast_2d(data[(0,) * dims].squeeze()),
vmin=vmin, vmax=vmax, cmap=cmap,
interpolation=interpolation, **kwargs)
if not isrgb:
pyplot.colorbar() # panchor=(0.55, 0.5), fraction=0.05
def format_coord(x, y):
# callback function to format coordinate display in toolbar
x = int(x + 0.5)
y = int(y + 0.5)
try:
if dims:
return '%s @ %s [%4i, %4i]' % (
curaxdat[1][y, x], current, y, x)
return '%s @ [%4i, %4i]' % (data[y, x], y, x)
except IndexError:
return ''
def none(event):
return ''
subplot.format_coord = format_coord
image.get_cursor_data = none
image.format_cursor_data = none
if dims:
current = list((0,) * dims)
curaxdat = [0, data[tuple(current)].squeeze()]
sliders = [pyplot.Slider(
pyplot.axes([0.125, 0.03*(axis+1), 0.725, 0.025]),
'Dimension %i' % axis, 0, data.shape[axis]-1, 0, facecolor='0.5',
valfmt='%%.0f [%i]' % data.shape[axis]) for axis in range(dims)]
for slider in sliders:
slider.drawon = False
def set_image(current, sliders=sliders, data=data):
# change image and redraw canvas
curaxdat[1] = data[tuple(current)].squeeze()
image.set_data(curaxdat[1])
for ctrl, index in zip(sliders, current):
ctrl.eventson = False
ctrl.set_val(index)
ctrl.eventson = True
figure.canvas.draw()
def on_changed(index, axis, data=data, current=current):
# callback function for slider change event
index = int(round(index))
curaxdat[0] = axis
if index == current[axis]:
return
if index >= data.shape[axis]:
index = 0
elif index < 0:
index = data.shape[axis] - 1
current[axis] = index
set_image(current)
def on_keypressed(event, data=data, current=current):
# callback function for key press event
key = event.key
axis = curaxdat[0]
if str(key) in '0123456789':
on_changed(key, axis)
elif key == 'right':
on_changed(current[axis] + 1, axis)
elif key == 'left':
on_changed(current[axis] - 1, axis)
elif key == 'up':
curaxdat[0] = 0 if axis == len(data.shape)-1 else axis + 1
elif key == 'down':
curaxdat[0] = len(data.shape)-1 if axis == 0 else axis - 1
elif key == 'end':
on_changed(data.shape[axis] - 1, axis)
elif key == 'home':
on_changed(0, axis)
figure.canvas.mpl_connect('key_press_event', on_keypressed)
for axis, ctrl in enumerate(sliders):
ctrl.on_changed(lambda k, a=axis: on_changed(k, a))
return figure, subplot, image
def _app_show():
"""Block the GUI. For use as skimage plugin."""
pyplot = sys.modules['matplotlib.pyplot']
pyplot.show()
def askopenfilename(**kwargs):
"""Return file name(s) from Tkinter's file open dialog."""
try:
from Tkinter import Tk
import tkFileDialog as filedialog
except ImportError:
from tkinter import Tk, filedialog
root = Tk()
root.withdraw()
root.update()
filenames = filedialog.askopenfilename(**kwargs)
root.destroy()
return filenames
def main(argv=None):
"""Command line usage main function."""
if float(sys.version[0:3]) < 2.7:
print('This script requires Python version 2.7 or better.')
print('This is Python version %s' % sys.version)
return 0
if argv is None:
argv = sys.argv
import optparse # TODO: use argparse
parser = optparse.OptionParser(
usage='usage: %prog [options] path',
description='Display image data in TIFF files.',
version='%%prog %s' % __version__)
opt = parser.add_option
opt('-p', '--page', dest='page', type='int', default=-1,
help='display single page')
opt('-s', '--series', dest='series', type='int', default=-1,
help='display series of pages of same shape')
opt('--nomultifile', dest='nomultifile', action='store_true',
default=False, help='do not read OME series from multiple files')
opt('--noplots', dest='noplots', type='int', default=8,
help='maximum number of plots')
opt('--interpol', dest='interpol', metavar='INTERPOL', default='bilinear',
help='image interpolation method')
opt('--dpi', dest='dpi', type='int', default=96,
help='plot resolution')
opt('--vmin', dest='vmin', type='int', default=None,
help='minimum value for colormapping')
opt('--vmax', dest='vmax', type='int', default=None,
help='maximum value for colormapping')
opt('--debug', dest='debug', action='store_true', default=False,
help='raise exception on failures')
opt('--doctest', dest='doctest', action='store_true', default=False,
help='runs the docstring examples')
opt('-v', '--detail', dest='detail', type='int', default=2)
opt('-q', '--quiet', dest='quiet', action='store_true')
settings, path = parser.parse_args()
path = ' '.join(path)
if settings.doctest:
import doctest
doctest.testmod(optionflags=doctest.ELLIPSIS)
return 0
if not path:
path = askopenfilename(title='Select a TIFF file',
filetypes=TIFF.FILEOPEN_FILTER)
if not path:
parser.error('No file specified')
if any(i in path for i in '?*'):
path = glob.glob(path)
if not path:
print('no files match the pattern')
return 0
# TODO: handle image sequences
path = path[0]
if not settings.quiet:
print('\nReading file structure...', end=' ')
start = time.time()
try:
tif = TiffFile(path, multifile=not settings.nomultifile)
except Exception as e:
if settings.debug:
raise
else:
print('\n', e)
sys.exit(0)
if not settings.quiet:
print('%.3f ms' % ((time.time()-start) * 1e3))
if tif.is_ome:
settings.norgb = True
images = []
if settings.noplots > 0:
if not settings.quiet:
print('Reading image data... ', end=' ')
def notnone(x):
return next(i for i in x if i is not None)
start = time.time()
try:
if settings.page >= 0:
images = [(tif.asarray(key=settings.page),
tif[settings.page], None)]
elif settings.series >= 0:
images = [(tif.asarray(series=settings.series),
notnone(tif.series[settings.series]._pages),
tif.series[settings.series])]
else:
images = []
for i, s in enumerate(tif.series[:settings.noplots]):
try:
images.append((tif.asarray(series=i),
notnone(s._pages),
tif.series[i]))
except ValueError as e:
images.append((None, notnone(s.pages), None))
if settings.debug:
raise
else:
print('\nSeries %i failed: %s... ' % (i, e),
end='')
if not settings.quiet:
print('%.3f ms' % ((time.time()-start) * 1e3))
except Exception as e:
if settings.debug:
raise
else:
print(e)
if not settings.quiet:
print()
print(TiffFile.__str__(tif, detail=int(settings.detail)))
print()
tif.close()
if images and settings.noplots > 0:
try:
import matplotlib
matplotlib.use('TkAgg')
from matplotlib import pyplot
except ImportError as e:
warnings.warn('failed to import matplotlib.\n%s' % e)
else:
for img, page, series in images:
if img is None:
continue
vmin, vmax = settings.vmin, settings.vmax
if 'GDAL_NODATA' in page.tags:
try:
vmin = numpy.min(
img[img > float(page.tags['GDAL_NODATA'].value)])
except ValueError:
pass
if tif.is_stk:
try:
vmin = tif.stk_metadata['MinScale']
vmax = tif.stk_metadata['MaxScale']
except KeyError:
pass
else:
if vmax <= vmin:
vmin, vmax = settings.vmin, settings.vmax
if series:
title = '%s\n%s\n%s' % (str(tif), str(page), str(series))
else:
title = '%s\n %s' % (str(tif), str(page))
photometric = 'MINISBLACK'
if page.photometric not in (3,):
photometric = TIFF.PHOTOMETRIC(page.photometric).name
imshow(img, title=title, vmin=vmin, vmax=vmax,
bitspersample=page.bitspersample,
photometric=photometric,
interpolation=settings.interpol,
dpi=settings.dpi)
pyplot.show()
if sys.version_info[0] == 2:
inttypes = int, long # noqa
def print_(*args, **kwargs):
"""Print function with flush support."""
flush = kwargs.pop('flush', False)
print(*args, **kwargs)
if flush:
sys.stdout.flush()
def bytes2str(b, encoding=None, errors=None):
"""Return string from bytes."""
return b
def str2bytes(s, encoding=None):
"""Return bytes from string."""
return s
def byte2int(b):
"""Return value of byte as int."""
return ord(b)
class FileNotFoundError(IOError):
pass
TiffFrame = TiffPage # noqa
else:
inttypes = int
basestring = str, bytes
unicode = str
print_ = print
def bytes2str(b, encoding=None, errors='strict'):
"""Return unicode string from encoded bytes."""
if encoding is not None:
return b.decode(encoding, errors)
try:
return b.decode('utf-8', errors)
except UnicodeDecodeError:
return b.decode('cp1252', errors)
def str2bytes(s, encoding='cp1252'):
"""Return bytes from unicode string."""
return s.encode(encoding)
def byte2int(b):
"""Return value of byte as int."""
return b
if __name__ == '__main__':
sys.exit(main())