def czi2tif(czifile, tiffile=None, squeeze=True, verbose=True, **kwargs): """Convert CZI file to memory-mappable TIFF file. To read the image data from the created TIFF file: Read the 'StripOffsets' and 'ImageDescription' tags from the first TIFF page. Get the 'dtype' and 'shape' attributes from the ImageDescription string using a JSON decoder. Memory-map 'product(shape) * sizeof(dtype)' bytes in the file starting at StripOffsets[0]. Cast the mapped bytes to an array of 'dtype' and 'shape'. """ verbose = print_ if verbose else lambda *a, **b: None if tiffile is None: tiffile = czifile + '.tif' elif tiffile.lower() == 'none': tiffile = None verbose("\nOpening CZI file... ", end='', flush=True) start_time = time.time() with CziFile(czifile) as czi: if squeeze: shape, axes = squeeze_axes(czi.shape, czi.axes, '') else: shape = czi.shape axes = czi.axes dtype = str(czi.dtype) size = product(shape) * czi.dtype.itemsize verbose("%.3f s" % (time.time() - start_time)) verbose("Image\n axes: %s\n shape: %s\n dtype: %s\n size: %s" % (axes, shape, dtype, format_size(size)), flush=True) if not tiffile: verbose("Copying image from CZI file to RAM... ", end='', flush=True) start_time = time.time() czi.asarray(order=0) else: verbose("Creating empty TIF file... ", end='', flush=True) start_time = time.time() if 'software' not in kwargs: kwargs['software'] = 'czi2tif' metadata = kwargs.pop('metadata', {}) metadata.update(axes=axes, dtype=dtype) data = memmap(tiffile, shape=shape, dtype=dtype, metadata=metadata, **kwargs) data = data.reshape(czi.shape) verbose("%.3f s" % (time.time() - start_time)) verbose("Copying image from CZI to TIF file... ", end='', flush=True) start_time = time.time() czi.asarray(order=0, out=data) verbose("%.3f s" % (time.time() - start_time), flush=True)
def asarray(self, out=None, squeeze=True, lock=None, reopen=True, maxsize=None, maxworkers=None, validate=True, loc=None, size=None): """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 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 (default), 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 synchronize seeks and 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 Maximum size of data before a ValueError is raised. Can be used to catch DOS. Default: 16 TB. maxworkers : int or None Maximum number of threads to concurrently decode compressed segments. If None (default), up to half the CPU cores are used. See remarks in TiffFile.asarray. validate : bool If True (default), validate various parameters. If None, only validate parameters and return None. Returns ------- numpy.ndarray Numpy array of decompressed, depredicted, and unpacked image data read from Strip/Tile Offsets/ByteCounts, formatted according to shape and dtype metadata found in tags and parameters. Photometric conversion, pre-multiplied alpha, orientation, and colorimetry corrections are not applied. Specifically, CMYK images are not converted to RGB, MinIsWhite images are not inverted, and color palettes are not applied. An exception are YCbCr JPEG compressed images, which will be converted to RGB. """ # properties from TiffPage or TiffFrame fh = self.parent.filehandle byteorder = self.parent.tiff.byteorder offsets, bytecounts = self._offsetscounts self_ = self self = self.keyframe # self or keyframe if not self._shape or tifffile.product(self._shape) == 0: return None tags = self.tags if validate or validate is None: if maxsize is None: maxsize = 2**44 if maxsize and tifffile.product(self._shape) > maxsize: raise ValueError('TiffPage %i: data are too large %s' % (self.index, str(self._shape))) if self.dtype is None: raise ValueError( 'TiffPage %i: data type not supported: %s%i' % (self.index, self.sampleformat, self.bitspersample)) if self.compression not in tifffile.TIFF.DECOMPESSORS: raise ValueError('TiffPage %i: cannot decompress %s' % (self.index, 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( 'TiffPage %i: sample formats do not match %s' % (self.index, tag.value)) if self.is_subsampled and (self.compression not in (6, 7) or self.planarconfig == 2): raise NotImplementedError( 'TiffPage %i: chroma subsampling not supported' % self.index) if validate is None: return None lock = fh.lock if lock is None else lock with lock: closed = fh.closed if closed: if reopen: fh.open() else: raise IOError('TiffPage %i: file handle is closed' % self.index) dtype = self._dtype shape = self._shape imagewidth = self.imagewidth imagelength = self.imagelength imagedepth = self.imagedepth bitspersample = self.bitspersample typecode = byteorder + dtype.char lsb2msb = self.fillorder == 2 istiled = self.is_tiled result_offset = None 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 tiledshape = (td, tl, tw) tileshape = (tiledepth, tilelength, tilewidth, shape[-1]) runlen = tilewidth else: runlen = imagewidth if self.planarconfig == 1: runlen *= self.samplesperpixel if isinstance(out, str) and out == 'memmap' and self.is_memmappable: # direct memory map array in file with lock: result = fh.memmap_array(typecode, shape, offset=offsets[0]) elif self.is_contiguous: # read contiguous bytes to array if out is not None: out = tifffile.create_output(out, shape, dtype) with lock: fh.seek(offsets[0]) result = fh.read_array(typecode, tifffile.product(shape), out=out) if lsb2msb: tifffile.bitorder_decode(result, out=result) else: # decompress, unpack,... individual strips or tiles if loc is not None and size is not None and istiled: result = np.zeros(shape=(shape[0], shape[1], shape[2], size[1], size[0], shape[5]), dtype=dtype) result_offset = (0, 0, 0, loc[1], loc[0], 0) else: result = tifffile.create_output(out, shape, dtype) decompress = tifffile.TIFF.DECOMPESSORS[self.compression] if self.compression in (6, 7): # COMPRESSION.JPEG colorspace = None outcolorspace = None jpegtables = None if lsb2msb: tifffile.log_warning('TiffPage %i: disabling LSB2MSB for JPEG', self.index) lsb2msb = False if 'JPEGTables' in tags: # load JPEGTables from TiffFrame jpegtables = self_._gettags({347}, lock=lock)[0][1].value # TODO: obtain table from OJPEG tags # elif ('JPEGInterchangeFormat' in tags and # 'JPEGInterchangeFormatLength' in tags and # tags['JPEGInterchangeFormat'].value != offsets[0]): # fh.seek(tags['JPEGInterchangeFormat'].value) # fh.read(tags['JPEGInterchangeFormatLength'].value) if 'ExtraSamples' in tags: pass elif self.photometric == 6: # YCBCR -> RGB outcolorspace = 'RGB' elif self.photometric == 2: if self.planarconfig == 1: colorspace = outcolorspace = 'RGB' else: outcolorspace = tifffile.TIFF.PHOTOMETRIC( self.photometric).name if istiled: heightwidth = tilelength, tilewidth else: heightwidth = imagelength, imagewidth def decompress(data, bitspersample=bitspersample, jpegtables=jpegtables, colorspace=colorspace, outcolorspace=outcolorspace, shape=heightwidth, out=None, _decompress=decompress): return _decompress(data, bitspersample, jpegtables, colorspace, outcolorspace, shape, out) def unpack(data): return data.reshape(-1) elif bitspersample in (8, 16, 32, 64, 128): if (bitspersample * runlen) % 8: raise ValueError('TiffPage %i: data and sample size mismatch' % self.index) if self.predictor == 3: # PREDICTOR.FLOATINGPOINT # the floating-point horizontal differencing decoder # needs the raw byte order typecode = dtype.char def unpack(data, typecode=typecode, out=None): try: # read only numpy array return numpy.frombuffer(data, typecode) except ValueError: # strips may be missing EOI # log_warning('TiffPage.asarray: ...') bps = bitspersample // 8 xlen = (len(data) // bps) * bps return numpy.frombuffer(data[:xlen], typecode) elif isinstance(bitspersample, tuple): def unpack(data, out=None): return tifffile.unpack_rgb(data, typecode, bitspersample) else: def unpack(data, out=None): return tifffile.packints_decode(data, typecode, bitspersample, runlen) # TODO: store decode function for future use # TODO: unify tile and strip decoding if istiled: unpredict = tifffile.TIFF.UNPREDICTORS[self.predictor] def decode(tile, tileindex, tileshape=tileshape, tiledshape=tiledshape, lsb2msb=lsb2msb, decompress=decompress, unpack=unpack, unpredict=unpredict, nodata=self.nodata, out=result[0]): return tile_decode(tile, tileindex, tileshape, tiledshape, lsb2msb, decompress, unpack, unpredict, nodata, out, offset=result_offset[1:] if result_offset is not None else None, total_shape=shape[1:]) use = get_used_tiles(len(offsets), tileshape, tiledshape, loc, size) bytecounts = [b for i, b in enumerate(bytecounts) if i in use] offsets = [b for i, b in enumerate(offsets) if i in use] tileiter = fh.read_segments(offsets, bytecounts, lock) if self.compression == 1 or len(offsets) < 3: maxworkers = 1 elif maxworkers is None or maxworkers < 1: import multiprocessing maxworkers = max(multiprocessing.cpu_count() // 2, 1) if maxworkers < 2: for i, tile in enumerate(tileiter): decode(tile, use[i]) else: # decode first tile un-threaded to catch exceptions decode(next(tileiter), use[0]) with ThreadPoolExecutor(maxworkers) as executor: executor.map(decode, tileiter, use[1:]) else: stripsize = self.rowsperstrip * self.imagewidth if self.planarconfig == 1: stripsize *= self.samplesperpixel outsize = stripsize * self.dtype.itemsize result = result.reshape(-1) index = 0 for strip in fh.read_segments(offsets, bytecounts, lock): if strip is None: result[index:index + stripsize] = self.nodata index += stripsize continue if lsb2msb: strip = tifffile.bitorder_decode(strip, out=strip) strip = decompress(strip, out=outsize) strip = unpack(strip) size = min(result.size, strip.size, stripsize, 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): unpredict = tifffile.TIFF.UNPREDICTORS[self.predictor] result = unpredict(result, axis=-2, out=result) if squeeze: try: if result_offset is not None: result = np.asarray(result)[0, 0, 0] else: result.shape = self.shape except ValueError: tifffile.log_warning('TiffPage %i: failed to reshape %s to %s', self.index, result.shape, self.shape) if closed: # TODO: file should remain open if an exception occurred above fh.close() return result
def bin2cmap( binfiles: Sequence[PathLike] | str, shape: tuple[int, ...], dtype: numpy.dtype, offset: int = 0, cmapfile: PathLike | None = None, fail: bool = True, **kwargs, ) -> None: r"""Convert series of SimFCS BIN files to Chimera MAP file. SimFCS BIN files contain homogeneous data of any type and shape, stored C-contiguously in little endian order. A common format is: shape=(-1, 256, 256), dtype='uint16'. TODO: Support generic strides, storage order, and byteorder. Parameters ---------- binfiles : str or sequence of str List of BIN file names or file pattern, e.g. '\*.bin' shape : tuple of 3 int Shape of data in BIN files in ZYX order, e.g. (32, 256, 256). dtype : numpy dtype Type of data in the BIN files, e.g. 'uint16'. offset : int, optional Number of bytes to skip at beginning of BIN file (default: 0). cmapfile : str, optional Name of the output CMAP file. If None (default), the name is derived from the first BIN file. fail : bool, optional If True (default), raise error when reading invalid BIN files. **kwargs Additional parameters passed to the CmapFile.addmap function, e.g. verbose, step, origin, cell_angles, rotation_axis, rotation_angle, subsample, chunks, and compression. """ binfiles_list = parse_files(binfiles) validate_shape(shape, 3) shape = tuple(shape) dtype = numpy.dtype(dtype) count = product(shape) if count < 0: count = -1 if not cmapfile: cmapfile = os.fspath(binfiles_list[0]) + '.cmap' verbose = kwargs.get('verbose', False) if verbose: print(f"Creating '{cmapfile}'", flush=True) with CmapFile(cmapfile, 'w') as cmap: for binfile in binfiles_list: if verbose: print('+', os.path.basename(binfile), end=' ', flush=True) try: with open(binfile, 'rb') as fh: fh.seek(offset) data = numpy.fromfile(fh, dtype=dtype, count=count) data.shape = shape shape = data.shape except Exception: if fail: raise if verbose: print('failed!', end='', flush=True) continue cmap.addmap(data, name=os.path.basename(binfile), **kwargs) if verbose: print(flush=True)