- read metadata into ome structure

- pytest
- use pathlib
- series as part of the path: path/PosN
- summary only shows some available metadata
- allow dict in Imread[dict(c=c, z=z, t=t)]
- bfread in different process so the user can start another jvm
- deal with multiple images (series/positions) in czi files
- use jpype instead of javabridge/bioformats
- poetry for install
This commit is contained in:
Wim Pomp
2023-06-29 14:23:03 +02:00
parent 33ddb845ae
commit 506b449f4d
15 changed files with 1268 additions and 920 deletions

View File

View File

@@ -1,10 +1,9 @@
# ndbioimage
# ndbioimage - Work in progress
Exposes (bio) images as a numpy ndarray like object, but without loading the whole
image into memory, reading from the file only when needed. Some metadata is read
and exposed as attributes to the Imread object (TODO: structure data in OME format).
Additionally, it can automatically calculate an affine transform that corrects for
chromatic abberrations etc. and apply it on the fly to the image.
Exposes (bio) images as a numpy ndarray-like-object, but without loading the whole
image into memory, reading from the file only when needed. Some metadata is read and
and stored in an ome structure. Additionally, it can automatically calculate an affine
transform that corrects for chromatic abberrations etc. and apply it on the fly to the image.
Currently supports imagej tif files, czi files, micromanager tif sequences and anything
bioformats can handle.
@@ -13,13 +12,8 @@ bioformats can handle.
pip install ndbioimage@git+https://github.com/wimpomp/ndbioimage.git
### With bioformats (if java is properly installed)
pip install ndbioimage[bioformats]@git+https://github.com/wimpomp/ndbioimage.git
### With affine transforms (only for python 3.8, 3.9 and 3.10)
pip install ndbioimage[transforms]@git+https://github.com/wimpomp/ndbioimage.git
Optionally:
https://downloads.openmicroscopy.org/bio-formats/latest/artifacts/bioformats_package.jar
## Usage
@@ -27,34 +21,34 @@ bioformats can handle.
import matplotlib.pyplot as plt
from ndbioimage import imread
with imread('image_file.tif', axes='ctxy', dtype=int) as im:
from ndbioimage import Imread
with Imread('image_file.tif', axes='ctxy', dtype=int) as im:
plt.imshow(im[2, 1])
- Showing some image metadata
from ndbioimage import imread
from ndbioimage import Imread
from pprint import pprint
with imread('image_file.tif') as im:
with Imread('image_file.tif') as im:
pprint(im)
- Slicing the image without loading the image into memory
from ndbioimage import imread
with imread('image_file.tif', axes='cztxy') as im:
from ndbioimage import Imread
with Imread('image_file.tif', axes='cztxy') as im:
sliced_im = im[1, :, :, 100:200, 100:200]
sliced_im is an instance of imread which will load any image data from file only when needed
sliced_im is an instance of Imread which will load any image data from file only when needed
- Converting (part) of the image to a numpy ndarray
from ndbioimage import imread
from ndbioimage import Imread
import numpy as np
with imread('image_file.tif', axes='cztxy') as im:
with Imread('image_file.tif', axes='cztxy') as im:
array = np.asarray(im[0, 0])
## Adding more formats
@@ -63,9 +57,8 @@ automatically recognize it and use it to open the appropriate file format. Image
subclass Imread and are required to implement the following methods:
- staticmethod _can_open(path): return True if path can be opened by this reader
- \_\_metadata__(self): reads metadata from file and adds them to self as attributes,
- the shape of the data in the file needs to be set as self.shape = (X, Y, C, Z, T)
- other attributes like pxsize, acquisitiontime and title can be set here as well
- property ome: reads metadata from file and adds them to an OME object imported
from the ome-types library
- \_\_frame__(self, c, z, t): return the frame at channel=c, z-slice=z, time=t from the file
Optional methods:
@@ -78,5 +71,5 @@ Optional fields:
for example: any file handles
# TODO
- structure the metadata in OME format tree
- more image formats
- re-implement transforms

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
__version__ = '2022.7.0'
__git_commit_hash__ = 'unknown'

View File

@@ -1,7 +1,6 @@
try:
import javabridge
import bioformats
from pathlib import Path
try:
class JVM:
""" There can be only one java virtual machine per python process,
so this is a singleton class to manage the jvm.
@@ -9,30 +8,43 @@ try:
_instance = None
vm_started = False
vm_killed = False
success = True
def __new__(cls, *args):
if cls._instance is None:
cls._instance = object.__new__(cls)
return cls._instance
def start_vm(self):
def __init__(self, classpath=None):
if not self.vm_started and not self.vm_killed:
javabridge.start_vm(class_path=bioformats.JARS, run_headless=True)
outputstream = javabridge.make_instance('java/io/ByteArrayOutputStream', "()V")
printstream = javabridge.make_instance('java/io/PrintStream', "(Ljava/io/OutputStream;)V", outputstream)
javabridge.static_call('Ljava/lang/System;', "setOut", "(Ljava/io/PrintStream;)V", printstream)
javabridge.static_call('Ljava/lang/System;', "setErr", "(Ljava/io/PrintStream;)V", printstream)
if classpath is None:
classpath = [str(Path(__file__).parent / 'jars' / '*')]
import jpype
jpype.startJVM(classpath=classpath)
import jpype.imports
from loci.common import DebugTools
from loci.formats import ImageReader
from loci.formats import ChannelSeparator
from loci.formats import FormatTools
from loci.formats import MetadataTools
DebugTools.setRootLevel("ERROR")
self.vm_started = True
log4j = javabridge.JClassWrapper("loci.common.Log4jTools")
log4j.enableLogging()
log4j.setRootLevel("ERROR")
self.image_reader = ImageReader
self.channel_separator = ChannelSeparator
self.format_tools = FormatTools
self.metadata_tools = MetadataTools
if self.vm_killed:
raise Exception('The JVM was killed before, and cannot be restarted in this Python process.')
def kill_vm(self):
javabridge.kill_vm()
if self.vm_started and not self.vm_killed:
import jpype
jpype.shutdownJVM()
self.vm_started = False
self.vm_killed = True
except ImportError:
JVM = None

View File

@@ -1,3 +1,2 @@
import os
__all__ = [os.path.splitext(os.path.basename(file))[0] for file in os.listdir(os.path.dirname(__file__))
if file.endswith('.py') and not file == os.path.basename(__file__)]
from pathlib import Path
__all__ = [file.stem for file in Path(__file__).parent.iterdir() if file.suffix == ".py" and not file == Path(__file__)]

View File

@@ -1,12 +1,172 @@
from ndbioimage import Imread, XmlData, JVM
import os
import multiprocessing
import numpy as np
import untangle
from abc import ABC
from ndbioimage import Imread, JVM
from multiprocessing import queues
from traceback import print_exc
from urllib import request
from pathlib import Path
if JVM is not None:
import bioformats
class Reader(Imread):
class JVMReader:
def __init__(self, path, series):
bf_jar = Path(__file__).parent.parent / 'jars' / 'bioformats_package.jar'
if not bf_jar.exists():
print('Downloading bioformats_package.jar.')
url = 'https://downloads.openmicroscopy.org/bio-formats/latest/artifacts/bioformats_package.jar'
bf_jar.write_bytes(request.urlopen(url).read())
mp = multiprocessing.get_context('spawn')
self.path = path
self.series = series
self.queue_in = mp.Queue()
self.queue_out = mp.Queue()
self.queue_error = mp.Queue()
self.done = mp.Event()
self.process = mp.Process(target=self.run)
self.process.start()
self.is_alive = True
def close(self):
if self.is_alive:
self.done.set()
while not self.queue_in.empty():
self.queue_in.get()
self.queue_in.close()
self.queue_in.join_thread()
while not self.queue_out.empty():
print(self.queue_out.get())
self.queue_out.close()
self.process.join()
self.process.close()
self.is_alive = False
def frame(self, c, z, t):
self.queue_in.put((c, z, t))
return self.queue_out.get()
def run(self):
""" Read planes from the image reader file.
adapted from python-bioformats/bioformats/formatreader.py
"""
jvm = JVM()
reader = jvm.image_reader()
ome_meta = jvm.metadata_tools.createOMEXMLMetadata()
reader.setMetadataStore(ome_meta)
reader.setId(str(self.path))
reader.setSeries(self.series)
open_bytes_func = reader.openBytes
width, height = int(reader.getSizeX()), int(reader.getSizeY())
pixel_type = reader.getPixelType()
little_endian = reader.isLittleEndian()
if pixel_type == jvm.format_tools.INT8:
dtype = np.int8
elif pixel_type == jvm.format_tools.UINT8:
dtype = np.uint8
elif pixel_type == jvm.format_tools.UINT16:
dtype = '<u2' if little_endian else '>u2'
elif pixel_type == jvm.format_tools.INT16:
dtype = '<i2' if little_endian else '>i2'
elif pixel_type == jvm.format_tools.UINT32:
dtype = '<u4' if little_endian else '>u4'
elif pixel_type == jvm.format_tools.INT32:
dtype = '<i4' if little_endian else '>i4'
elif pixel_type == jvm.format_tools.FLOAT:
dtype = '<f4' if little_endian else '>f4'
elif pixel_type == jvm.format_tools.DOUBLE:
dtype = '<f8' if little_endian else '>f8'
else:
dtype = None
try:
while not self.done.is_set():
try:
c, z, t = self.queue_in.get(True, 0.02)
if reader.isRGB() and reader.isInterleaved():
index = reader.getIndex(z, 0, t)
image = np.frombuffer(open_bytes_func(index), dtype)
image.shape = (height, width, reader.getSizeC())
if image.shape[2] > 3:
image = image[:, :, :3]
elif c is not None and reader.getRGBChannelCount() == 1:
index = reader.getIndex(z, c, t)
image = np.frombuffer(open_bytes_func(index), dtype)
image.shape = (height, width)
elif reader.getRGBChannelCount() > 1:
n_planes = reader.getRGBChannelCount()
rdr = jvm.channel_separator(reader)
planes = [np.frombuffer(rdr.openBytes(rdr.getIndex(z, i, t)), dtype) for i in range(n_planes)]
if len(planes) > 3:
planes = planes[:3]
elif len(planes) < 3:
# > 1 and < 3 means must be 2
# see issue #775
planes.append(np.zeros(planes[0].shape, planes[0].dtype))
image = np.dstack(planes)
image.shape = (height, width, 3)
del rdr
elif reader.getSizeC() > 1:
images = [np.frombuffer(open_bytes_func(reader.getIndex(z, i, t)), dtype)
for i in range(reader.getSizeC())]
image = np.dstack(images)
image.shape = (height, width, reader.getSizeC())
# if not channel_names is None:
# metadata = MetadataRetrieve(self.metadata)
# for i in range(self.reader.getSizeC()):
# index = self.reader.getIndex(z, 0, t)
# channel_name = metadata.getChannelName(index, i)
# if channel_name is None:
# channel_name = metadata.getChannelID(index, i)
# channel_names.append(channel_name)
elif reader.isIndexed():
#
# The image data is indexes into a color lookup-table
# But sometimes the table is the identity table and just generates
# a monochrome RGB image
#
index = reader.getIndex(z, 0, t)
image = np.frombuffer(open_bytes_func(index), dtype)
if pixel_type in (jvm.format_tools.INT16, jvm.format_tools.UINT16):
lut = reader.get16BitLookupTable()
if lut is not None:
lut = np.array(lut)
# lut = np.array(
# [env.get_short_array_elements(d)
# for d in env.get_object_array_elements(lut)]) \
# .transpose()
else:
lut = reader.get8BitLookupTable()
if lut is not None:
lut = np.array(lut)
# lut = np.array(
# [env.get_byte_array_elements(d)
# for d in env.get_object_array_elements(lut)]) \
# .transpose()
image.shape = (height, width)
if (lut is not None) and not np.all(lut == np.arange(lut.shape[0])[:, np.newaxis]):
image = lut[image, :]
else:
index = reader.getIndex(z, 0, t)
image = np.frombuffer(open_bytes_func(index), dtype)
image.shape = (height, width)
if image.ndim == 3:
self.queue_out.put(image[..., c])
else:
self.queue_out.put(image)
except queues.Empty:
continue
except (Exception,):
print_exc()
self.queue_out.put(np.zeros((32, 32)))
finally:
jvm.kill_vm()
class Reader(Imread, ABC):
""" This class is used as a last resort, when we don't have another way to open the file. We don't like it
because it requires the java vm.
"""
@@ -15,75 +175,13 @@ if JVM is not None:
@staticmethod
def _can_open(path):
return True
return not path.is_dir()
def open(self):
self.jvm = JVM()
self.jvm.start_vm()
self.key = np.random.randint(1e9)
self.reader = bioformats.get_image_reader(self.key, self.path)
self.reader = JVMReader(self.path, self.series)
def __metadata__(self):
s = self.reader.rdr.getSeriesCount()
if self.series >= s:
print('Series {} does not exist.'.format(self.series))
self.reader.rdr.setSeries(self.series)
X = self.reader.rdr.getSizeX()
Y = self.reader.rdr.getSizeY()
C = self.reader.rdr.getSizeC()
Z = self.reader.rdr.getSizeZ()
T = self.reader.rdr.getSizeT()
self.shape = (X, Y, C, Z, T)
omexml = bioformats.get_omexml_metadata(self.path)
self.metadata = XmlData(untangle.parse(omexml))
image = list(self.metadata.search_all('Image').values())
if len(image) and self.series in image[0]:
image = XmlData(image[0][self.series])
else:
image = self.metadata
unit = lambda u: 10 ** {'nm': 9, 'µm': 6, 'um': 6, 'mm': 3, 'm': 0}[u]
pxsizeunit = image.search('PhysicalSizeXUnit')[0]
pxsize = image.search('PhysicalSizeX')[0]
if pxsize is not None:
self.pxsize = pxsize / unit(pxsizeunit) * 1e6
if self.zstack:
deltazunit = image.search('PhysicalSizeZUnit')[0]
deltaz = image.search('PhysicalSizeZ')[0]
if deltaz is not None:
self.deltaz = deltaz / unit(deltazunit) * 1e6
if self.path.endswith('.lif'):
self.title = os.path.splitext(os.path.basename(self.path))[0]
self.exposuretime = self.metadata.re_search(r'WideFieldChannelInfo\|ExposureTime', self.exposuretime)
if self.timeseries:
self.settimeinterval = \
self.metadata.re_search(r'ATLCameraSettingDefinition\|CycleTime', self.settimeinterval * 1e3)[
0] / 1000
if not self.settimeinterval:
self.settimeinterval = self.exposuretime[0]
self.pxsizecam = self.metadata.re_search(r'ATLCameraSettingDefinition\|TheoCamSensorPixelSizeX',
self.pxsizecam)
self.objective = self.metadata.re_search(r'ATLCameraSettingDefinition\|ObjectiveName', 'none')[0]
self.magnification = \
self.metadata.re_search(r'ATLCameraSettingDefinition\|Magnification', self.magnification)[0]
elif self.path.endswith('.ims'):
self.magnification = self.metadata.search('LensPower', 100)[0]
self.NA = self.metadata.search('NumericalAperture', 1.47)[0]
self.title = self.metadata.search('Name', self.title)
self.binning = self.metadata.search('BinningX', 1)[0]
def __frame__(self, *args):
frame = self.reader.read(*args, rescale=False).astype('float')
if frame.ndim == 3:
return frame[..., args[0]]
else:
return frame
def __frame__(self, c, z, t):
return self.reader.frame(c, z, t)
def close(self):
bioformats.release_image_reader(self.key)
self.reader.close()

View File

@@ -1,115 +1,432 @@
from ndbioimage import Imread, XmlData, tolist
import czifile
import untangle
import numpy as np
import re
from lxml import etree
from ome_types import model
from abc import ABC
from ndbioimage import Imread
from functools import cached_property
from itertools import product
from pathlib import Path
class Reader(Imread):
class Reader(Imread, ABC):
priority = 0
do_not_pickle = 'reader', 'filedict', 'extrametadata'
do_not_pickle = 'reader', 'filedict'
@staticmethod
def _can_open(path):
return isinstance(path, str) and path.endswith('.czi')
return isinstance(path, Path) and path.suffix == '.czi'
def open(self):
self.reader = czifile.CziFile(self.path)
filedict = {}
for directory_entry in self.reader.filtered_subblock_directory:
idx = self.get_index(directory_entry, self.reader.start)
if 'S' not in self.reader.axes or self.series in range(*idx[self.reader.axes.index('S')]):
for c in range(*idx[self.reader.axes.index('C')]):
for z in range(*idx[self.reader.axes.index('Z')]):
for t in range(*idx[self.reader.axes.index('T')]):
if (c, z, t) in filedict:
filedict[(c, z, t)].append(directory_entry)
filedict[c, z, t].append(directory_entry)
else:
filedict[(c, z, t)] = [directory_entry]
filedict[c, z, t] = [directory_entry]
self.filedict = filedict
def close(self):
self.reader.close()
def __metadata__(self):
# TODO: make sure frame function still works when a subblock has data from more than one frame
self.shape = tuple([self.reader.shape[self.reader.axes.index(directory_entry)] for directory_entry in 'XYCZT'])
self.metadata = XmlData(untangle.parse(self.reader.metadata()))
image = [i for i in self.metadata.search_all('Image').values() if i]
if len(image) and self.series in image[0]:
image = XmlData(image[0][self.series])
@cached_property
def ome(self):
xml = self.reader.metadata()
attachments = {i.attachment_entry.name: i.attachment_entry.data_segment()
for i in self.reader.attachments()}
tree = etree.fromstring(xml)
metadata = tree.find("Metadata")
version = metadata.find("Version")
if version is not None:
version = version.text
else:
image = self.metadata
version = metadata.find("Experiment").attrib["Version"]
pxsize = image.search('ScalingX')[0]
if pxsize is not None:
self.pxsize = pxsize * 1e6
if self.zstack:
deltaz = image.search('ScalingZ')[0]
if deltaz is not None:
self.deltaz = deltaz * 1e6
if version == '1.0':
return self.ome_10(tree, attachments)
elif version == '1.2':
return self.ome_12(tree, attachments)
self.title = self.metadata.re_search(('Information', 'Document', 'Name'), self.title)[0]
self.acquisitiondate = self.metadata.re_search(('Information', 'Document', 'CreationDate'),
self.acquisitiondate)[0]
self.exposuretime = self.metadata.re_search(('TrackSetup', 'CameraIntegrationTime'), self.exposuretime)
if self.timeseries:
self.settimeinterval = self.metadata.re_search(('Interval', 'TimeSpan', 'Value'),
self.settimeinterval * 1e3)[0] / 1000
if not self.settimeinterval:
self.settimeinterval = self.exposuretime[0]
self.pxsizecam = self.metadata.re_search(('AcquisitionModeSetup', 'PixelPeriod'), self.pxsizecam)
self.magnification = self.metadata.re_search('NominalMagnification', self.magnification)[0]
attenuators = self.metadata.search_all('Attenuator')
self.laserwavelengths = [[1e9 * float(i['Wavelength']) for i in tolist(attenuator)]
for attenuator in attenuators.values()]
self.laserpowers = [[float(i['Transmission']) for i in tolist(attenuator)]
for attenuator in attenuators.values()]
self.collimator = self.metadata.re_search(('Collimator', 'Position'))
detector = self.metadata.search(('Instrument', 'Detector'))
self.gain = [int(i.get('AmplificationGain', 1)) for i in detector]
self.powermode = self.metadata.re_search(('TrackSetup', 'FWFOVPosition'))[0]
optovar = self.metadata.re_search(('TrackSetup', 'TubeLensPosition'), '1x')
self.optovar = []
for o in optovar:
a = re.search(r'\d?\d*[,.]?\d+(?=x$)', o)
if hasattr(a, 'group'):
self.optovar.append(float(a.group(0).replace(',', '.')))
self.pcf = [2 ** self.metadata.re_search(('Image', 'ComponentBitCount'), 14)[0] / float(i)
for i in self.metadata.re_search(('Channel', 'PhotonConversionFactor'), 1)]
self.binning = self.metadata.re_search(('AcquisitionModeSetup', 'CameraBinning'), 1)[0]
self.objective = self.metadata.re_search(('AcquisitionModeSetup', 'Objective'))[0]
self.NA = self.metadata.re_search(('Instrument', 'Objective', 'LensNA'))[0]
self.filter = self.metadata.re_search(('TrackSetup', 'BeamSplitter', 'Filter'))[0]
self.tirfangle = [50 * i for i in self.metadata.re_search(('TrackSetup', 'TirfAngle'), 0)]
self.frameoffset = [self.metadata.re_search(('AcquisitionModeSetup', 'CameraFrameOffsetX'))[0],
self.metadata.re_search(('AcquisitionModeSetup', 'CameraFrameOffsetY'))[0]]
self.cnamelist = [c['DetectorSettings']['Detector']['Id'] for c in
self.metadata['ImageDocument']['Metadata']['Information']['Image'].search('Channel')]
def ome_12(self, tree, attachments):
def text(item, default=""):
return default if item is None else item.text
ome = model.OME()
metadata = tree.find("Metadata")
information = metadata.find("Information")
display_setting = metadata.find("DisplaySetting")
ome.experimenters = [model.Experimenter(id="Experimenter:0",
user_name=information.find("Document").find("UserName").text)]
instrument = information.find("Instrument")
for _ in instrument.find("Microscopes"):
ome.instruments.append(model.Instrument())
for detector in instrument.find("Detectors"):
try:
self.track, self.detector = zip(*[[int(i) for i in re.findall(r'\d', c)] for c in self.cnamelist])
detector_type = model.detector.Type(text(detector.find("Type")).upper() or "")
except ValueError:
self.track = tuple(range(len(self.cnamelist)))
self.detector = (0,) * len(self.cnamelist)
detector_type = model.detector.Type.OTHER
ome.instruments[0].detectors.append(
model.Detector(
id=detector.attrib["Id"].replace(' ', ''), model=text(detector.find("Manufacturer").find("Model")),
type=detector_type
))
for objective in instrument.find("Objectives"):
ome.instruments[0].objectives.append(
model.Objective(
id=objective.attrib["Id"],
model=text(objective.find("Manufacturer").find("Model")),
immersion=text(objective.find("Immersion")),
lens_na=float(text(objective.find("LensNA"))),
nominal_magnification=float(text(objective.find("NominalMagnification")))))
for tubelens in instrument.find("TubeLenses"):
ome.instruments[0].objectives.append(
model.Objective(
id=f'Objective:{tubelens.attrib["Id"]}',
model=tubelens.attrib["Name"],
nominal_magnification=1.0)) # TODO: nominal_magnification
for light_source in instrument.find("LightSources"):
if light_source.find("LightSourceType").find("Laser") is not None:
ome.instruments[0].light_source_group.append(
model.Laser(
id=f'LightSource:{light_source.attrib["Id"]}',
power=float(text(light_source.find("Power"))),
wavelength=float(light_source.attrib["Id"][-3:])))
x_min = min([f.start[f.axes.index('X')] for f in self.filedict[0, 0, 0]])
y_min = min([f.start[f.axes.index('Y')] for f in self.filedict[0, 0, 0]])
x_max = max([f.start[f.axes.index('X')] + f.shape[f.axes.index('X')] for f in self.filedict[0, 0, 0]])
y_max = max([f.start[f.axes.index('Y')] + f.shape[f.axes.index('Y')] for f in self.filedict[0, 0, 0]])
size_x = x_max - x_min
size_y = y_max - y_min
size_c, size_z, size_t = [self.reader.shape[self.reader.axes.index(directory_entry)]
for directory_entry in 'CZT']
image = information.find("Image")
pixel_type = text(image.find("PixelType"), "Gray16")
if pixel_type.startswith("Gray"):
pixel_type = "uint" + pixel_type[4:]
objective_settings = image.find("ObjectiveSettings")
scenes = image.find("Dimensions").find("S").find("Scenes")
center_position = [float(pos) for pos in text(scenes[0].find("CenterPosition")).split(',')]
um = model.simple_types.UnitsLength.MICROMETER
nm = model.simple_types.UnitsLength.NANOMETER
ome.images.append(
model.Image(
id="Image:0",
name=f'{text(information.find("Document").find("Name"))} #1',
pixels=model.Pixels(
id="Pixels:0", size_x=size_x, size_y=size_y,
size_c=size_c, size_z=size_z, size_t=size_t,
dimension_order="XYCZT", type=pixel_type,
significant_bits=int(text(image.find("ComponentBitCount"))),
big_endian=False, interleaved=False, metadata_only=True),
experimenter_ref=model.ExperimenterRef(id='Experimenter:0'),
instrument_ref=model.InstrumentRef(id='Instrument:0'),
objective_settings=model.ObjectiveSettings(
id=objective_settings.find("ObjectiveRef").attrib["Id"],
medium=text(objective_settings.find("Medium")),
refractive_index=float(text(objective_settings.find("RefractiveIndex")))),
stage_label=model.StageLabel(
name=f"Scene position #0",
x=center_position[0], x_unit=um,
y=center_position[1], y_unit=um)))
for distance in metadata.find("Scaling").find("Items"):
if distance.attrib["Id"] == "X":
ome.images[0].pixels.physical_size_x = float(text(distance.find("Value"))) * 1e6
elif distance.attrib["Id"] == "Y":
ome.images[0].pixels.physical_size_y = float(text(distance.find("Value"))) * 1e6
elif size_z > 1 and distance.attrib["Id"] == "Z":
ome.images[0].pixels.physical_size_z = float(text(distance.find("Value"))) * 1e6
channels_im = {channel.attrib["Id"]: channel for channel in image.find("Dimensions").find("Channels")}
channels_ds = {channel.attrib["Id"]: channel for channel in display_setting.find("Channels")}
for idx, (key, channel) in enumerate(channels_im.items()):
detector_settings = channel.find("DetectorSettings")
laser_scan_info = channel.find("LaserScanInfo")
detector = detector_settings.find("Detector")
try:
binning = model.simple_types.Binning(text(detector_settings.find("Binning")))
except ValueError:
binning = model.simple_types.Binning.OTHER
light_sources_settings = channel.find("LightSourcesSettings")
# no space in ome for multiple lightsources simultaneously
light_source_settings = light_sources_settings[0]
light_source_settings = model.LightSourceSettings(
id="LightSource:" + "_".join([light_source_settings.find("LightSource").attrib["Id"]
for light_source_settings in light_sources_settings]),
attenuation=float(text(light_source_settings.find("Attenuation"))),
wavelength=float(text(light_source_settings.find("Wavelength"))),
wavelength_unit=nm)
ome.images[0].pixels.channels.append(
model.Channel(
id=f"Channel:0:{idx}",
name=channel.attrib["Name"],
acquisition_mode=text(channel.find("AcquisitionMode")),
color=model.simple_types.Color(text(channels_ds[channel.attrib["Id"]].find("Color"))),
detector_settings=model.DetectorSettings(
id=detector.attrib["Id"].replace(" ", ""),
binning=binning),
emission_wavelength=text(channel.find("EmissionWavelength")),
excitation_wavelength=text(channel.find("ExcitationWavelength")),
# filter_set_ref=model.FilterSetRef(id=ome.instruments[0].filter_sets[filterset_idx].id),
illumination_type=text(channel.find("IlluminationType")),
light_source_settings=light_source_settings,
samples_per_pixel=int(text(laser_scan_info.find("Averaging")))))
exposure_times = [float(text(channel.find("LaserScanInfo").find("FrameTime"))) for channel in
channels_im.values()]
delta_ts = attachments['TimeStamps'].data()
for t, z, c in product(range(size_t), range(size_z), range(size_c)):
ome.images[0].pixels.planes.append(
model.Plane(the_c=c, the_z=z, the_t=t, delta_t=delta_ts[t], exposure_time=exposure_times[c]))
idx = 0
for layer in metadata.find("Layers"):
rectangle = layer.find("Elements").find("Rectangle")
if rectangle is not None:
geometry = rectangle.find("Geometry")
roi = model.ROI(id=f"ROI:{idx}", description=text(layer.find("Usage")))
roi.union.append(
model.Rectangle(
id='Shape:0:0',
height=float(text(geometry.find("Height"))),
width=float(text(geometry.find("Width"))),
x=float(text(geometry.find("Left"))),
y=float(text(geometry.find("Top")))))
ome.rois.append(roi)
ome.images[0].roi_ref.append(model.ROIRef(id=f"ROI:{idx}"))
idx += 1
return ome
def ome_10(self, tree, attachments):
def text(item, default=""):
return default if item is None else item.text
ome = model.OME()
metadata = tree.find("Metadata")
information = metadata.find("Information")
display_setting = metadata.find("DisplaySetting")
experiment = metadata.find("Experiment")
acquisition_block = experiment.find("ExperimentBlocks").find("AcquisitionBlock")
ome.experimenters = [model.Experimenter(id="Experimenter:0",
user_name=information.find("User").find("DisplayName").text)]
instrument = information.find("Instrument")
ome.instruments.append(model.Instrument(id=instrument.attrib["Id"]))
for detector in instrument.find("Detectors"):
try:
detector_type = model.detector.Type(text(detector.find("Type")).upper() or "")
except ValueError:
detector_type = model.detector.Type.OTHER
ome.instruments[0].detectors.append(
model.Detector(
id=detector.attrib["Id"], model=text(detector.find("Manufacturer").find("Model")),
amplification_gain=float(text(detector.find("AmplificationGain"))),
gain=float(text(detector.find("Gain"))), zoom=float(text(detector.find("Zoom"))),
type=detector_type
))
for objective in instrument.find("Objectives"):
ome.instruments[0].objectives.append(
model.Objective(
id=objective.attrib["Id"],
model=text(objective.find("Manufacturer").find("Model")),
immersion=text(objective.find("Immersion")),
lens_na=float(text(objective.find("LensNA"))),
nominal_magnification=float(text(objective.find("NominalMagnification")))))
for light_source in instrument.find("LightSources"):
if light_source.find("LightSourceType").find("Laser") is not None:
ome.instruments[0].light_source_group.append(
model.Laser(
id=light_source.attrib["Id"],
model=text(light_source.find("Manufacturer").find("Model")),
power=float(text(light_source.find("Power"))),
wavelength=float(
text(light_source.find("LightSourceType").find("Laser").find("Wavelength")))))
multi_track_setup = acquisition_block.find("MultiTrackSetup")
for idx, tube_lens in enumerate(set(text(track_setup.find("TubeLensPosition"))
for track_setup in multi_track_setup)):
ome.instruments[0].objectives.append(
model.Objective(id=f"Objective:Tubelens:{idx}", model=tube_lens,
nominal_magnification=float(
re.findall(r'\d+[,.]\d*', tube_lens)[0].replace(',', '.'))
))
for idx, filter_ in enumerate(set(text(beam_splitter.find("Filter"))
for track_setup in multi_track_setup
for beam_splitter in track_setup.find("BeamSplitters"))):
ome.instruments[0].filter_sets.append(
model.FilterSet(id=f"FilterSet:{idx}", model=filter_)
)
for idx, collimator in enumerate(set(text(track_setup.find("FWFOVPosition"))
for track_setup in multi_track_setup)):
ome.instruments[0].filters.append(model.Filter(id=f"Filter:Collimator:{idx}", model=collimator))
x_min = min([f.start[f.axes.index('X')] for f in self.filedict[0, 0, 0]])
y_min = min([f.start[f.axes.index('Y')] for f in self.filedict[0, 0, 0]])
x_max = max([f.start[f.axes.index('X')] + f.shape[f.axes.index('X')] for f in self.filedict[0, 0, 0]])
y_max = max([f.start[f.axes.index('Y')] + f.shape[f.axes.index('Y')] for f in self.filedict[0, 0, 0]])
size_x = x_max - x_min
size_y = y_max - y_min
size_c, size_z, size_t = [self.reader.shape[self.reader.axes.index(directory_entry)]
for directory_entry in 'CZT']
image = information.find("Image")
pixel_type = text(image.find("PixelType"), "Gray16")
if pixel_type.startswith("Gray"):
pixel_type = "uint" + pixel_type[4:]
objective_settings = image.find("ObjectiveSettings")
scenes = image.find("Dimensions").find("S").find("Scenes")
positions = scenes[0].find("Positions")[0]
um = model.simple_types.UnitsLength.MICROMETER
nm = model.simple_types.UnitsLength.NANOMETER
ome.images.append(
model.Image(
id="Image:0",
name=f'{text(information.find("Document").find("Name"))} #1',
pixels=model.Pixels(
id="Pixels:0", size_x=size_x, size_y=size_y,
size_c=size_c, size_z=size_z, size_t=size_t,
dimension_order="XYCZT", type=pixel_type,
significant_bits=int(text(image.find("ComponentBitCount"))),
big_endian=False, interleaved=False, metadata_only=True),
experimenter_ref=model.ExperimenterRef(id='Experimenter:0'),
instrument_ref=model.InstrumentRef(id='Instrument:0'),
objective_settings=model.ObjectiveSettings(
id=objective_settings.find("ObjectiveRef").attrib["Id"],
medium=text(objective_settings.find("Medium")),
refractive_index=float(text(objective_settings.find("RefractiveIndex")))),
stage_label=model.StageLabel(
name=f"Scene position #0",
x=float(positions.attrib["X"]), x_unit=um,
y=float(positions.attrib["Y"]), y_unit=um,
z=float(positions.attrib["Z"]), z_unit=um)))
for distance in metadata.find("Scaling").find("Items"):
if distance.attrib["Id"] == "X":
ome.images[0].pixels.physical_size_x = float(text(distance.find("Value"))) * 1e6
elif distance.attrib["Id"] == "Y":
ome.images[0].pixels.physical_size_y = float(text(distance.find("Value"))) * 1e6
elif size_z > 1 and distance.attrib["Id"] == "Z":
ome.images[0].pixels.physical_size_z = float(text(distance.find("Value"))) * 1e6
channels_im = {channel.attrib["Id"]: channel for channel in image.find("Dimensions").find("Channels")}
channels_ds = {channel.attrib["Id"]: channel for channel in display_setting.find("Channels")}
channels_ts = {detector.attrib["Id"]: track_setup
for track_setup in
experiment.find("ExperimentBlocks").find("AcquisitionBlock").find("MultiTrackSetup")
for detector in track_setup.find("Detectors")}
for idx, (key, channel) in enumerate(channels_im.items()):
detector_settings = channel.find("DetectorSettings")
laser_scan_info = channel.find("LaserScanInfo")
detector = detector_settings.find("Detector")
try:
binning = model.simple_types.Binning(text(detector_settings.find("Binning")))
except ValueError:
binning = model.simple_types.Binning.OTHER
filterset = text(channels_ts[key].find("BeamSplitters")[0].find("Filter"))
filterset_idx = [filterset.model for filterset in ome.instruments[0].filter_sets].index(filterset)
light_sources_settings = channel.find("LightSourcesSettings")
# no space in ome for multiple lightsources simultaneously
light_source_settings = light_sources_settings[0]
light_source_settings = model.LightSourceSettings(
id="_".join([light_source_settings.find("LightSource").attrib["Id"]
for light_source_settings in light_sources_settings]),
attenuation=float(text(light_source_settings.find("Attenuation"))),
wavelength=float(text(light_source_settings.find("Wavelength"))),
wavelength_unit=nm)
ome.images[0].pixels.channels.append(
model.Channel(
id=f"Channel:0:{idx}",
name=channel.attrib["Name"],
acquisition_mode=text(channel.find("AcquisitionMode")),
color=model.simple_types.Color(text(channels_ds[channel.attrib["Id"]].find("Color"))),
detector_settings=model.DetectorSettings(id=detector.attrib["Id"], binning=binning),
emission_wavelength=text(channel.find("EmissionWavelength")),
excitation_wavelength=text(channel.find("ExcitationWavelength")),
filter_set_ref=model.FilterSetRef(id=ome.instruments[0].filter_sets[filterset_idx].id),
illumination_type=text(channel.find("IlluminationType")),
light_source_settings=light_source_settings,
samples_per_pixel=int(text(laser_scan_info.find("Averaging")))))
exposure_times = [float(text(channel.find("LaserScanInfo").find("FrameTime"))) for channel in
channels_im.values()]
delta_ts = attachments['TimeStamps'].data()
for t, z, c in product(range(size_t), range(size_z), range(size_c)):
ome.images[0].pixels.planes.append(
model.Plane(the_c=c, the_z=z, the_t=t, delta_t=delta_ts[t],
exposure_time=exposure_times[c],
position_x=float(positions.attrib["X"]), position_x_unit=um,
position_y=float(positions.attrib["Y"]), position_y_unit=um,
position_z=float(positions.attrib["Z"]), position_z_unit=um))
idx = 0
for layer in metadata.find("Layers"):
rectangle = layer.find("Elements").find("Rectangle")
if rectangle is not None:
geometry = rectangle.find("Geometry")
roi = model.ROI(id=f"ROI:{idx}", description=text(layer.find("Usage")))
roi.union.append(
model.Rectangle(
id='Shape:0:0',
height=float(text(geometry.find("Height"))),
width=float(text(geometry.find("Width"))),
x=float(text(geometry.find("Left"))),
y=float(text(geometry.find("Top")))))
ome.rois.append(roi)
ome.images[0].roi_ref.append(model.ROIRef(id=f"ROI:{idx}"))
idx += 1
return ome
def __frame__(self, c=0, z=0, t=0):
f = np.zeros(self.file_shape[:2], self.dtype)
for directory_entry in self.filedict[(c, z, t)]:
f = np.zeros(self.shape['xy'], self.dtype)
directory_entries = self.filedict[c, z, t]
x_min = min([f.start[f.axes.index('X')] for f in directory_entries])
y_min = min([f.start[f.axes.index('Y')] for f in directory_entries])
xy_min = {'X': x_min, 'Y': y_min}
for directory_entry in directory_entries:
subblock = directory_entry.data_segment()
tile = subblock.data(resize=True, order=0)
index = [slice(i - j, i - j + k) for i, j, k in
zip(directory_entry.start, self.reader.start, tile.shape)]
index = tuple([index[self.reader.axes.index(i)] for i in 'XY'])
axes_min = [xy_min.get(ax, 0) for ax in directory_entry.axes]
index = [slice(i - j - m, i - j + k)
for i, j, k, m in zip(directory_entry.start, self.reader.start, tile.shape, axes_min)]
index = tuple(index[self.reader.axes.index(i)] for i in 'XY')
f[index] = tile.squeeze()
return f
@staticmethod
def get_index(directory_entry, start):
return [(i - j, i - j + k) for i, j, k in zip(directory_entry.start, start, directory_entry.shape)]
@cached_property
def timeval(self):
tval = np.unique(list(filter(lambda x: x.attachment_entry.filename.startswith('TimeStamp'),
self.reader.attachments()))[0].data())
return sorted(tval[tval > 0])[:self.shape['t']]

View File

@@ -1,29 +1,55 @@
from ndbioimage import Imread
import numpy as np
from ome_types import model
from functools import cached_property
from abc import ABC
from ndbioimage import Imread
from itertools import product
class Reader(Imread):
class Reader(Imread, ABC):
priority = 20
do_not_pickle = 'ome'
do_not_copy = 'ome'
@staticmethod
def _can_open(path):
return isinstance(path, np.ndarray) and 1 <= path.ndim <= 5
def __metadata__(self):
self.base = np.array(self.path, ndmin=5)
self.title = self.path = 'numpy array'
self.axes = self.axes[:self.base.ndim]
self.shape = self.base.shape
self.acquisitiondate = 'now'
@cached_property
def ome(self):
def shape(size_x=1, size_y=1, size_c=1, size_z=1, size_t=1):
return size_x, size_y, size_c, size_z, size_t
size_x, size_y, size_c, size_z, size_t = shape(*self.array.shape)
try:
pixel_type = model.simple_types.PixelType(self.array.dtype.name)
except ValueError:
if self.array.dtype.name.startswith('int'):
pixel_type = model.simple_types.PixelType('int32')
else:
pixel_type = model.simple_types.PixelType('float')
ome = model.OME()
ome.instruments.append(model.Instrument())
ome.images.append(
model.Image(
pixels=model.Pixels(
size_c=size_c, size_z=size_z, size_t=size_t, size_x=size_x, size_y=size_y,
dimension_order="XYCZT", type=pixel_type,
objective_settings=model.ObjectiveSettings(id="Objective:0"))))
for c, z, t in product(range(size_c), range(size_z), range(size_t)):
ome.images[0].pixels.planes.append(model.Plane(the_c=c, the_z=z, the_t=t, delta_t=0))
return ome
def open(self):
self.array = np.array(self.path)
self.path = 'numpy array'
def __frame__(self, c, z, t):
xyczt = (slice(None), slice(None), c, z, t)
in_idx = tuple(xyczt['xyczt'.find(i)] for i in self.axes)
frame = self.base[in_idx]
# xyczt = (slice(None), slice(None), c, z, t)
# in_idx = tuple(xyczt['xyczt'.find(i)] for i in self.axes)
# print(f'{in_idx = }')
frame = self.array[:, :, c, z, t]
if self.axes.find('y') < self.axes.find('x'):
return frame.T
else:
return frame
def __str__(self):
return self.path

View File

@@ -1,85 +1,110 @@
from ndbioimage import Imread, XmlData
import os
from abc import ABC
import tifffile
import yaml
import json
import re
from ndbioimage import Imread
from pathlib import Path
from functools import cached_property
from ome_types import model
from itertools import product
from datetime import datetime
class Reader(Imread):
class Reader(Imread, ABC):
priority = 10
@staticmethod
def _can_open(path):
return isinstance(path, str) and os.path.splitext(path)[1] == ''
return isinstance(path, Path) and path.suffix == ""
def __metadata__(self):
filelist = sorted([file for file in os.listdir(self.path) if re.search(r'^img_\d{3,}.*\d{3,}.*\.tif$', file)])
def get_metadata(self, c, z, t):
with tifffile.TiffFile(self.filedict[c, z, t]) as tif:
return {key: yaml.safe_load(value) for key, value in tif.pages[0].tags[50839].value.items()}
try:
with tifffile.TiffFile(os.path.join(self.path, filelist[0])) as tif:
self.metadata = XmlData({key: yaml.safe_load(value)
for key, value in tif.pages[0].tags[50839].value.items()})
except Exception: # fallback
with open(os.path.join(self.path, 'metadata.txt'), 'r') as metadatafile:
self.metadata = XmlData(json.loads(metadatafile.read()))
@cached_property
def ome(self):
ome = model.OME()
metadata = self.get_metadata(0, 0, 0)
ome.experimenters.append(
model.Experimenter(id="Experimenter:0", user_name=metadata["Info"]["Summary"]["UserName"]))
objective_str = metadata["Info"]["ZeissObjectiveTurret-Label"]
ome.instruments.append(model.Instrument())
ome.instruments[0].objectives.append(
model.Objective(
id="Objective:0", manufacturer="Zeiss", model=objective_str,
nominal_magnification=float(re.findall(r"(\d+)x", objective_str)[0]),
lens_na=float(re.findall(r"/(\d\.\d+)", objective_str)[0]),
immersion=model.objective.Immersion.OIL if 'oil' in objective_str.lower() else None))
tubelens_str = metadata["Info"]["ZeissOptovar-Label"]
ome.instruments[0].objectives.append(
model.Objective(
id="Objective:Tubelens:0", manufacturer="Zeiss", model=tubelens_str,
nominal_magnification=float(re.findall(r"\d?\d*[,.]?\d+(?=x$)", tubelens_str)[0].replace(",", "."))))
ome.instruments[0].detectors.append(
model.Detector(
id="Detector:0", amplification_gain=100))
ome.instruments[0].filter_sets.append(
model.FilterSet(id='FilterSet:0', model=metadata["Info"]["ZeissReflectorTurret-Label"]))
pxsize = metadata["Info"]["PixelSizeUm"]
pxsize_cam = 6.5 if 'Hamamatsu' in metadata["Info"]["Core-Camera"] else None
if pxsize == 0:
pxsize = pxsize_cam / ome.instruments[0].objectives[0].nominal_magnification
pixel_type = metadata["Info"]["PixelType"].lower()
if pixel_type.startswith("gray"):
pixel_type = "uint" + pixel_type[4:]
else:
pixel_type = "uint16" # assume
size_c, size_z, size_t = [max(i) + 1 for i in zip(*self.filedict.keys())]
t0 = datetime.strptime(metadata["Info"]["Time"], "%Y-%m-%d %H:%M:%S %z")
ome.images.append(
model.Image(
pixels=model.Pixels(
size_c=size_c, size_z=size_z, size_t=size_t,
size_x=metadata['Info']['Width'], size_y=metadata['Info']['Height'],
dimension_order="XYCZT", type=pixel_type, physical_size_x=pxsize, physical_size_y=pxsize,
physical_size_z=metadata["Info"]["Summary"]["z-step_um"]),
objective_settings=model.ObjectiveSettings(id="Objective:0")))
for c, z, t in product(range(size_c), range(size_z), range(size_t)):
ome.images[0].pixels.planes.append(
model.Plane(
the_c=c, the_z=z, the_t=t, exposure_time=metadata["Info"]["Exposure-ms"] / 1000,
delta_t=(datetime.strptime(self.get_metadata(c, z, t)["Info"]["Time"],
"%Y-%m-%d %H:%M:%S %z") - t0).seconds))
# compare channel names from metadata with filenames
cnamelist = self.metadata.search('ChNames')
cnamelist = [c for c in cnamelist if any([c in f for f in filelist])]
pattern_c = re.compile(r"img_\d{3,}_(.*)_\d{3,}$")
for c in range(size_c):
ome.images[0].pixels.channels.append(
model.Channel(
id=f"Channel:{c}", name=pattern_c.findall(self.filedict[c, 0, 0].stem)[0],
detector_settings=model.DetectorSettings(
id="Detector:0", binning=metadata["Info"]["Hamamatsu_sCMOS-Binning"]),
filter_set_ref=model.FilterSetRef(id='FilterSet:0')))
return ome
self.filedict = {}
maxc = 0
maxz = 0
maxt = 0
for file in filelist:
T = re.search(r'(?<=img_)\d{3,}', file)
Z = re.search(r'\d{3,}(?=\.tif$)', file)
C = file[T.end() + 1:Z.start() - 1]
t = int(T.group(0))
z = int(Z.group(0))
if C in cnamelist:
c = cnamelist.index(C)
def open(self):
if not self.path.name.startswith("Pos"):
path = self.path / f"Pos{self.series}"
else:
c = len(cnamelist)
cnamelist.append(C)
path = self.path
self.filedict[(c, z, t)] = file
if c > maxc:
maxc = c
if z > maxz:
maxz = z
if t > maxt:
maxt = t
self.cnamelist = [str(cname) for cname in cnamelist]
filelist = sorted([file for file in path.iterdir() if re.search(r'^img_\d{3,}.*\d{3,}.*\.tif$', file.name)])
with tifffile.TiffFile(self.path / filelist[0]) as tif:
metadata = {key: yaml.safe_load(value) for key, value in tif.pages[0].tags[50839].value.items()}
X = self.metadata.search('Width')[0]
Y = self.metadata.search('Height')[0]
self.shape = (int(X), int(Y), maxc + 1, maxz + 1, maxt + 1)
# compare channel names from metadata with filenames
cnamelist = metadata["Info"]["Summary"]["ChNames"]
cnamelist = [c for c in cnamelist if any([c in f.name for f in filelist])]
self.pxsize = self.metadata.re_search(r'(?i)pixelsize_?um', 0)[0]
if self.zstack:
self.deltaz = self.metadata.re_search(r'(?i)z-step_?um', 0)[0]
if self.timeseries:
self.settimeinterval = self.metadata.re_search(r'(?i)interval_?ms', 0)[0] / 1000
if 'Hamamatsu' in self.metadata.search('Core-Camera', '')[0]:
self.pxsizecam = 6.5
self.title = self.metadata.search('Prefix')[0]
self.acquisitiondate = self.metadata.search('Time')[0]
self.exposuretime = [i / 1000 for i in self.metadata.search('Exposure-ms')]
self.objective = self.metadata.search('ZeissObjectiveTurret-Label')[0]
self.optovar = []
for o in self.metadata.search('ZeissOptovar-Label'):
a = re.search(r'\d?\d*[,.]?\d+(?=x$)', o)
if hasattr(a, 'group'):
self.optovar.append(float(a.group(0).replace(',', '.')))
if self.pxsize == 0:
self.magnification = int(re.findall(r'(\d+)x', self.objective)[0]) * self.optovar[0]
self.pxsize = self.pxsizecam / self.magnification
else:
self.magnification = self.pxsizecam / self.pxsize
self.pcf = self.shape[2] * self.metadata.re_search(r'(?i)conversion\sfactor\scoeff', 1)
self.filter = self.metadata.search('ZeissReflectorTurret-Label', self.filter)[0]
pattern_t = re.compile(r"img_(\d{3,})")
pattern_c = re.compile(r"img_\d{3,}_(.*)_\d{3,}$")
pattern_z = re.compile(r"(\d{3,})$")
self.filedict = {(int(pattern_t.findall(file.stem)[0]),
int(pattern_z.findall(file.stem)[0]),
cnamelist.index(pattern_c.findall(file.stem)[0])): file for file in filelist}
def __frame__(self, c=0, z=0, t=0):
return tifffile.imread(os.path.join(self.path, self.filedict[(c, z, t)]))
return tifffile.imread(self.path / self.filedict[(c, z, t)])

View File

@@ -1,50 +1,73 @@
from ndbioimage import Imread, XmlData
from abc import ABC
from ndbioimage import Imread
import numpy as np
import tifffile
import yaml
from functools import cached_property
from ome_types import model
from pathlib import Path
from itertools import product
class Reader(Imread):
class Reader(Imread, ABC):
priority = 0
do_not_pickle = 'reader'
@staticmethod
def _can_open(path):
if isinstance(path, str) and (path.endswith('.tif') or path.endswith('.tiff')):
if isinstance(path, Path) and path.suffix in ('.tif', '.tiff'):
with tifffile.TiffFile(path) as tif:
return tif.is_imagej
else:
return False
@cached_property
def ome(self):
metadata = {key: yaml.safe_load(value) if isinstance(value, str) else value
for key, value in self.reader.imagej_metadata.items()}
page = self.reader.pages[0]
self.p_ndim = page.ndim
size_x = page.imagelength
size_y = page.imagewidth
if self.p_ndim == 3:
size_c = page.samplesperpixel
self.p_transpose = [i for i in [page.axes.find(j) for j in 'SYX'] if i >= 0]
size_t = metadata.get('frames', 1) # // C
else:
size_c = metadata.get('channels', 1)
size_t = metadata.get('frames', 1)
size_z = metadata.get('slices', 1)
if 282 in page.tags and 296 in page.tags and page.tags[296].value == 1:
f = page.tags[282].value
pxsize = f[1] / f[0]
else:
pxsize = None
ome = model.OME()
ome.instruments.append(model.Instrument(id='Instrument:0'))
ome.instruments[0].objectives.append(model.Objective(id='Objective:0'))
ome.images.append(
model.Image(
id='Image:0',
pixels=model.Pixels(
id='Pixels:0',
size_c=size_c, size_z=size_z, size_t=size_t, size_x=size_x, size_y=size_y,
dimension_order="XYCZT", type=page.dtype.name, physical_size_x=pxsize, physical_size_y=pxsize),
objective_settings=model.ObjectiveSettings(id="Objective:0")))
for c, z, t in product(range(size_c), range(size_z), range(size_t)):
ome.images[0].pixels.planes.append(model.Plane(the_c=c, the_z=z, the_t=t, delta_t=0))
return ome
def open(self):
self.reader = tifffile.TiffFile(self.path)
def close(self):
self.reader.close()
def __metadata__(self):
self.metadata = XmlData({key: yaml.safe_load(value) if isinstance(value, str) else value
for key, value in self.reader.imagej_metadata.items()})
P = self.reader.pages[0]
self.pndim = P.ndim
X = P.imagelength
Y = P.imagewidth
if self.pndim == 3:
C = P.samplesperpixel
self.transpose = [i for i in [P.axes.find(j) for j in 'SYX'] if i >= 0]
T = self.metadata.get('frames', 1) # // C
else:
C = self.metadata.get('channels', 1)
T = self.metadata.get('frames', 1)
Z = self.metadata.get('slices', 1)
self.shape = (X, Y, C, Z, T)
if 282 in P.tags and 296 in P.tags and P.tags[296].value == 1:
f = P.tags[282].value
self.pxsize = f[1] / f[0]
# TODO: more metadata
def __frame__(self, c, z, t):
if self.pndim == 3:
return np.transpose(self.reader.asarray(z + t * self.shape[3]), self.transpose)[c]
if self.p_ndim == 3:
return np.transpose(self.reader.asarray(z + t * self.file_shape[3]), self.p_transpose)[c]
else:
return self.reader.asarray(c + z * self.shape[2] + t * self.shape[2] * self.shape[3])
return self.reader.asarray(c + z * self.file_shape[2] + t * self.file_shape[2] * self.file_shape[3])

View File

@@ -1,21 +1,19 @@
import yaml
import os
import re
import numpy as np
from copy import deepcopy
from collections import OrderedDict
from pathlib import Path
try:
# best if SimpleElastix is installed: https://simpleelastix.readthedocs.io/GettingStarted.html
import SimpleITK as sitk
installed = True
except ImportError:
installed = False
sitk = None
try:
pp = True
from pandas import DataFrame, Series
except ImportError:
pp = False
DataFrame, Series = None, None
if hasattr(yaml, 'full_load'):
@@ -24,42 +22,52 @@ else:
yamlload = yaml.load
class Transforms(OrderedDict):
class Transforms(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args[1:], **kwargs)
self.default = Transform()
if len(args):
self.load(args[0])
def __mul__(self, other):
new = Transforms()
if isinstance(other, Transforms):
for key0, value0 in self.items():
for key1, value1 in other.items():
new[key0 + key1] = value0 * value1
return new
elif other is None:
return self
else:
for key in self.keys():
new[key] = self[key] * other
return new
def asdict(self):
return {f'{key[0]:.0f}:{key[1]:.0f}': value.asdict() for key, value in self.items()}
return {':'.join(str(i).replace('\\', '\\\\').replace(':', r'\:') for i in key): value.asdict()
for key, value in self.items()}
def load(self, file):
if isinstance(file, dict):
d = file
else:
if not file[-3:] == 'yml':
file += '.yml'
with open(file, 'r') as f:
with open(file.with_suffix(".yml"), 'r') as f:
d = yamlload(f)
pattern = re.compile(r'[^\\]:')
for key, value in d.items():
self[tuple([int(k) for k in key.split(':')])] = Transform(value)
self[tuple(i.replace(r'\:', ':').replace('\\\\', '\\') for i in pattern.split(key))] = Transform(value)
def __call__(self, channel, time, tracks, detectors):
track, detector = tracks[channel], detectors[channel]
if (track, detector) in self:
return self[track, detector]
elif (0, detector) in self:
return self[0, detector]
else:
return Transform()
def __missing__(self, key):
return self.default
def __reduce__(self):
return self.__class__, (self.asdict(),)
def __getstate__(self):
return self.__dict__
def __setstate__(self, state):
self.__dict__.update(state)
def save(self, file):
if not file[-3:] == 'yml':
file += '.yml'
with open(file, 'w') as f:
with open(file.with_suffix(".yml"), 'w') as f:
yaml.safe_dump(self.asdict(), f, default_flow_style=None)
def copy(self):
@@ -68,6 +76,7 @@ class Transforms(OrderedDict):
def adapt(self, origin, shape):
for value in self.values():
value.adapt(origin, shape)
self.default.adapt(origin, shape)
@property
def inverse(self):
@@ -76,15 +85,20 @@ class Transforms(OrderedDict):
inverse[key] = value.inverse
return inverse
@property
def ndim(self):
return len(list(self.keys())[0])
class Transform:
def __init__(self, *args):
if not installed:
raise ImportError('SimpleElastix is not installed: https://simpleelastix.readthedocs.io/GettingStarted.html')
self.transform = sitk.ReadTransform(os.path.join(os.path.dirname(__file__), 'transform.txt'))
self.dparameters = (0, 0, 0, 0, 0, 0)
self.shape = (512, 512)
self.origin = (255.5, 255.5)
if sitk is None:
raise ImportError('SimpleElastix is not installed: '
'https://simpleelastix.readthedocs.io/GettingStarted.html')
self.transform = sitk.ReadTransform(str(Path(__file__).parent / 'transform.txt'))
self.dparameters = 0, 0, 0, 0, 0, 0
self.shape = 512, 512
self.origin = 255.5, 255.5
if len(args) == 1: # load from file or dict
if isinstance(args[0], np.ndarray):
self.matrix = args[0]
@@ -92,7 +106,7 @@ class Transform:
self.load(*args)
elif len(args) > 1: # make new transform using fixed and moving image
self.register(*args)
self._last = None
self._last, self._inverse = None, None
def __mul__(self, other): # TODO: take care of dmatrix
result = self.copy()
@@ -114,13 +128,13 @@ class Transform:
return deepcopy(self)
@staticmethod
def castImage(im):
def cast_image(im):
if not isinstance(im, sitk.Image):
im = sitk.GetImageFromArray(im)
return im
@staticmethod
def castArray(im):
def cast_array(im):
if isinstance(im, sitk.Image):
im = sitk.GetArrayFromImage(im)
return im
@@ -190,7 +204,7 @@ class Transform:
dtype = im.dtype
im = im.astype('float')
intp = sitk.sitkBSpline if np.issubdtype(dtype, np.floating) else sitk.sitkNearestNeighbor
return self.castArray(sitk.Resample(self.castImage(im), self.transform, intp, default)).astype(dtype)
return self.cast_array(sitk.Resample(self.cast_image(im), self.transform, intp, default)).astype(dtype)
def coords(self, array, columns=None):
""" Transform coordinates in 2 column numpy array,
@@ -198,7 +212,7 @@ class Transform:
"""
if self.is_unity():
return array.copy()
elif pp and isinstance(array, (DataFrame, Series)):
elif DataFrame is not None and isinstance(array, (DataFrame, Series)):
columns = columns or ['x', 'y']
array = array.copy()
if isinstance(array, DataFrame):
@@ -239,7 +253,7 @@ class Transform:
"""
kind = kind or 'affine'
self.shape = fix.shape
fix, mov = self.castImage(fix), self.castImage(mov)
fix, mov = self.cast_image(fix), self.cast_image(mov)
# TODO: implement RigidTransform
tfilter = sitk.ElastixImageFilter()
tfilter.LogToConsoleOff()

37
pyproject.toml Normal file
View File

@@ -0,0 +1,37 @@
[tool.poetry]
name = "ndbioimage"
version = "2023.6.0"
description = "Bio image reading, metadata and some affine registration."
authors = ["W. Pomp <w.pomp@nki.nl>"]
license = "GPLv3"
readme = "README.md"
keywords = ["bioformats", "imread", "numpy", "metadata"]
include = ["transform.txt"]
repository = "https://github.com/wimpomp/ndbioimage"
[tool.poetry.dependencies]
python = "^3.8"
numpy = "*"
pandas = "*"
tifffile = "*"
czifile = "*"
tiffwrite = "*"
ome-types = "*"
pint = "*"
tqdm = "*"
lxml = "*"
pyyaml = "*"
parfor = "*"
JPype1 = "*"
SimpleITK-SimpleElastix = "*"
pytest = { version = "*", optional = true }
[tool.poetry.extras]
test = ["pytest-xdist"]
[tool.poetry.scripts]
ndbioimage = "ndbioimage:main"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -1,48 +0,0 @@
import setuptools
import platform
import os
version = '2022.7.1'
if platform.system().lower() == 'linux':
import pkg_resources
pkg_resources.require(['pip >= 20.3'])
with open('README.md', 'r') as fh:
long_description = fh.read()
with open(os.path.join(os.path.dirname(__file__), 'ndbioimage', '_version.py'), 'w') as f:
f.write(f"__version__ = '{version}'\n")
try:
with open(os.path.join(os.path.dirname(__file__), '.git', 'refs', 'heads', 'master')) as h:
f.write("__git_commit_hash__ = '{}'\n".format(h.read().rstrip('\n')))
except Exception:
f.write(f"__git_commit_hash__ = 'unknown'\n")
setuptools.setup(
name='ndbioimage',
version=version,
author='Wim Pomp',
author_email='w.pomp@nki.nl',
description='Bio image reading, metadata and some affine registration.',
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/wimpomp/ndbioimage',
packages=['ndbioimage', 'ndbioimage.readers'],
classifiers=[
'Programming Language :: Python :: 3',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Operating System :: OS Independent',
],
python_requires='>=3.7',
install_requires=['untangle', 'pandas', 'psutil', 'numpy', 'tqdm', 'tifffile', 'czifile', 'pyyaml', 'dill',
'tiffwrite'],
extras_require={'transforms': 'SimpleITK-SimpleElastix',
'bioformats': ['python-javabridge', 'python-bioformats']},
tests_require=['pytest-xdist'],
entry_points={'console_scripts': ['ndbioimage=ndbioimage.ndbioimage:main']},
package_data={'': ['transform.txt']},
include_package_data=True,
)

10
tests/test_open.py Normal file
View File

@@ -0,0 +1,10 @@
import pytest
from pathlib import Path
from ndbioimage import Imread
@pytest.mark.parametrize("file",
[file for file in (Path(__file__).parent / 'files').iterdir() if file.suffix != '.pzl'])
def test_open(file):
with Imread(file) as im:
print(im[dict(c=0, z=0, t=0)].mean())