|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +## from: https://forum.image.sc/t/trouble-generating-ome-tiffs-in-the-right-shape/89191/7 |
| 4 | + |
| 5 | +import argparse |
| 6 | +from xml.etree import ElementTree |
| 7 | +from tifffile import TiffFile, TiffWriter |
| 8 | +from xml.dom import minidom |
| 9 | + |
| 10 | +def tiles(series): |
| 11 | + # yield raw tiles from all pages in TIFF series |
| 12 | + fh = series.parent.filehandle |
| 13 | + for page in series: |
| 14 | + for offset, bytecount in zip(page.dataoffsets, page.databytecounts): |
| 15 | + fh.seek(offset) |
| 16 | + yield fh.read(bytecount) |
| 17 | + |
| 18 | + |
| 19 | +def main(): |
| 20 | + parser = argparse.ArgumentParser(description="Convert HALO/Indica tif to OME tif") |
| 21 | + parser.add_argument("-o", "--output", type=str, required=True, help="Output .ome.tif file") |
| 22 | + parser.add_argument("-i", "--image", type=str, required=True, help="Input .tif image") |
| 23 | + |
| 24 | + args = parser.parse_args() |
| 25 | + |
| 26 | + with TiffFile(args.image) as tif: |
| 27 | + print(tif) |
| 28 | + |
| 29 | + tree = ElementTree.fromstring(tif.pages.first.description) |
| 30 | + channel_names = [ |
| 31 | + channel.attrib['name'] for channel in tree.iter('channel') |
| 32 | + ] |
| 33 | + |
| 34 | + objective_value = float(tree.find('.//objective').attrib['value']) |
| 35 | + |
| 36 | + with TiffWriter( |
| 37 | + args.output, bigtiff=True, ome=True, byteorder=tif.byteorder |
| 38 | + ) as ome: |
| 39 | + for series in tif.series: |
| 40 | + print("SERIES:", series) |
| 41 | + print(" series.axes:", series.axes, "series.shape:", series.shape) |
| 42 | + assert series.axes == 'IYX' |
| 43 | + assert series.shape[0] == len(channel_names) |
| 44 | + |
| 45 | + for i, level in enumerate(series.levels): |
| 46 | + page = level.keyframe |
| 47 | + print(" LEVEL", i, "level.shape:", level.shape, "page.keyframe.shape:", getattr(page, 'shape', None)) |
| 48 | + print(" page.imagewidth:", getattr(page, 'imagewidth', None), |
| 49 | + "page.imagelength:", getattr(page, 'imagelength', None)) |
| 50 | + print(" page.tile:", getattr(page, 'tile', None)) |
| 51 | + print(" TileWidth/TileLength tags:", |
| 52 | + page.tags.get('TileWidth'), page.tags.get('TileLength')) |
| 53 | + |
| 54 | + assert page.is_tiled |
| 55 | + |
| 56 | + # --- compute rows/cols and samples confidently from page tags --- |
| 57 | + rows = getattr(page, 'imagelength', None) |
| 58 | + cols = getattr(page, 'imagewidth', None) |
| 59 | + # number of channels/samples (fall back to level.shape if unsure) |
| 60 | + samples = series.shape[0] if series.shape else (level.shape[0] if len(level.shape) == 3 else 1) |
| 61 | + |
| 62 | + # If level.shape looks like (rows, cols, samples) convert to (samples, rows, cols) |
| 63 | + if len(level.shape) == 3: |
| 64 | + a, b, c = level.shape |
| 65 | + if (a == rows and b == cols and c == samples): |
| 66 | + inferred_shape = (samples, rows, cols) |
| 67 | + elif (a == samples and b == rows and c == cols): |
| 68 | + inferred_shape = (a, b, c) # already (samples, rows, cols) |
| 69 | + else: |
| 70 | + # fallback to explicit (samples, rows, cols) |
| 71 | + inferred_shape = (samples, rows, cols) |
| 72 | + else: |
| 73 | + inferred_shape = (rows, cols) |
| 74 | + |
| 75 | + # get tile dims from tags if present (TileWidth, TileLength) |
| 76 | + try: |
| 77 | + tile_w = page.tags['TileWidth'].value |
| 78 | + tile_l = page.tags['TileLength'].value |
| 79 | + tile = (tile_w, tile_l) |
| 80 | + except Exception: |
| 81 | + tile = page.tile or None |
| 82 | + |
| 83 | + if i == 0: |
| 84 | + # base-level metadata construction (unchanged) |
| 85 | + if len(series.levels) > 1: |
| 86 | + subifds = len(series.levels) - 1 |
| 87 | + else: |
| 88 | + subifds = None |
| 89 | + resx, resy = page.get_resolution('micrometer') |
| 90 | + |
| 91 | + # Correct resolution by objective magnification |
| 92 | + resx = resx / objective_value |
| 93 | + resy = resy / objective_value |
| 94 | + |
| 95 | + metadata = { |
| 96 | + 'axes': 'CYX', |
| 97 | + 'PhysicalSizeX': resx, |
| 98 | + 'PhysicalSizeXUnit': 'µm', |
| 99 | + 'PhysicalSizeY': resy, |
| 100 | + 'PhysicalSizeYUnit': 'µm', |
| 101 | + 'Channel': {'Name': channel_names}, |
| 102 | + } |
| 103 | + else: |
| 104 | + subifds = None |
| 105 | + metadata = None |
| 106 | + |
| 107 | + dtype = 'float32' if level.dtype == 'uint32' else level.dtype |
| 108 | + |
| 109 | + print(" -> writing with shape:", inferred_shape, "tile:", tile) |
| 110 | + |
| 111 | + ome.write( |
| 112 | + tiles(level), |
| 113 | + shape=inferred_shape, |
| 114 | + dtype=dtype, |
| 115 | + photometric=page.photometric, |
| 116 | + compression=page.compression, |
| 117 | + resolution=page.resolution, |
| 118 | + resolutionunit=page.resolutionunit, |
| 119 | + tile=tile, |
| 120 | + subifds=subifds, |
| 121 | + metadata=metadata, |
| 122 | + ) |
| 123 | + |
| 124 | + return |
| 125 | + |
| 126 | + |
| 127 | +if __name__ == "__main__": |
| 128 | + main() |
0 commit comments