- 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 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 image into memory, reading from the file only when needed. Some metadata is read and
and exposed as attributes to the Imread object (TODO: structure data in OME format). and stored in an ome structure. Additionally, it can automatically calculate an affine
Additionally, it can automatically calculate an affine transform that corrects for transform that corrects for chromatic abberrations etc. and apply it on the fly to the image.
chromatic abberrations etc. and apply it on the fly to the image.
Currently supports imagej tif files, czi files, micromanager tif sequences and anything Currently supports imagej tif files, czi files, micromanager tif sequences and anything
bioformats can handle. bioformats can handle.
@@ -13,13 +12,8 @@ bioformats can handle.
pip install ndbioimage@git+https://github.com/wimpomp/ndbioimage.git pip install ndbioimage@git+https://github.com/wimpomp/ndbioimage.git
### With bioformats (if java is properly installed) Optionally:
https://downloads.openmicroscopy.org/bio-formats/latest/artifacts/bioformats_package.jar
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
## Usage ## Usage
@@ -27,34 +21,34 @@ bioformats can handle.
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from ndbioimage import imread from ndbioimage import Imread
with imread('image_file.tif', axes='ctxy', dtype=int) as im: with Imread('image_file.tif', axes='ctxy', dtype=int) as im:
plt.imshow(im[2, 1]) plt.imshow(im[2, 1])
- Showing some image metadata - Showing some image metadata
from ndbioimage import imread from ndbioimage import Imread
from pprint import pprint from pprint import pprint
with imread('image_file.tif') as im: with Imread('image_file.tif') as im:
pprint(im) pprint(im)
- Slicing the image without loading the image into memory - Slicing the image without loading the image into memory
from ndbioimage import imread from ndbioimage import Imread
with imread('image_file.tif', axes='cztxy') as im: with Imread('image_file.tif', axes='cztxy') as im:
sliced_im = im[1, :, :, 100:200, 100:200] 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 - Converting (part) of the image to a numpy ndarray
from ndbioimage import imread from ndbioimage import Imread
import numpy as np 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]) array = np.asarray(im[0, 0])
## Adding more formats ## 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: subclass Imread and are required to implement the following methods:
- staticmethod _can_open(path): return True if path can be opened by this reader - 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, - property ome: reads metadata from file and adds them to an OME object imported
- the shape of the data in the file needs to be set as self.shape = (X, Y, C, Z, T) from the ome-types library
- other attributes like pxsize, acquisitiontime and title can be set here as well
- \_\_frame__(self, c, z, t): return the frame at channel=c, z-slice=z, time=t from the file - \_\_frame__(self, c, z, t): return the frame at channel=c, z-slice=z, time=t from the file
Optional methods: Optional methods:
@@ -78,5 +71,5 @@ Optional fields:
for example: any file handles for example: any file handles
# TODO # TODO
- structure the metadata in OME format tree - more image formats
- re-implement transforms - 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: from pathlib import Path
import javabridge
import bioformats
try:
class JVM: class JVM:
""" There can be only one java virtual machine per python process, """ There can be only one java virtual machine per python process,
so this is a singleton class to manage the jvm. so this is a singleton class to manage the jvm.
@@ -9,30 +8,43 @@ try:
_instance = None _instance = None
vm_started = False vm_started = False
vm_killed = False vm_killed = False
success = True
def __new__(cls, *args): def __new__(cls, *args):
if cls._instance is None: if cls._instance is None:
cls._instance = object.__new__(cls) cls._instance = object.__new__(cls)
return cls._instance return cls._instance
def start_vm(self): def __init__(self, classpath=None):
if not self.vm_started and not self.vm_killed: if not self.vm_started and not self.vm_killed:
javabridge.start_vm(class_path=bioformats.JARS, run_headless=True) if classpath is None:
outputstream = javabridge.make_instance('java/io/ByteArrayOutputStream', "()V") classpath = [str(Path(__file__).parent / 'jars' / '*')]
printstream = javabridge.make_instance('java/io/PrintStream', "(Ljava/io/OutputStream;)V", outputstream)
javabridge.static_call('Ljava/lang/System;', "setOut", "(Ljava/io/PrintStream;)V", printstream) import jpype
javabridge.static_call('Ljava/lang/System;', "setErr", "(Ljava/io/PrintStream;)V", printstream) 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 self.vm_started = True
log4j = javabridge.JClassWrapper("loci.common.Log4jTools") self.image_reader = ImageReader
log4j.enableLogging() self.channel_separator = ChannelSeparator
log4j.setRootLevel("ERROR") self.format_tools = FormatTools
self.metadata_tools = MetadataTools
if self.vm_killed: if self.vm_killed:
raise Exception('The JVM was killed before, and cannot be restarted in this Python process.') raise Exception('The JVM was killed before, and cannot be restarted in this Python process.')
def kill_vm(self): 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_started = False
self.vm_killed = True self.vm_killed = True
except ImportError: except ImportError:
JVM = None JVM = None

View File

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

View File

@@ -1,89 +1,187 @@
from ndbioimage import Imread, XmlData, JVM import multiprocessing
import os
import numpy as np 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:
""" This class is used as a last resort, when we don't have another way to open the file. We don't like it def __init__(self, path, series):
because it requires the java vm. 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
""" """
priority = 99 # panic and open with BioFormats jvm = JVM()
do_not_pickle = 'reader', 'key', 'jvm' reader = jvm.image_reader()
ome_meta = jvm.metadata_tools.createOMEXMLMetadata()
reader.setMetadataStore(ome_meta)
reader.setId(str(self.path))
reader.setSeries(self.series)
@staticmethod open_bytes_func = reader.openBytes
def _can_open(path): width, height = int(reader.getSizeX()), int(reader.getSizeY())
return True
def open(self): pixel_type = reader.getPixelType()
self.jvm = JVM() little_endian = reader.isLittleEndian()
self.jvm.start_vm()
self.key = np.random.randint(1e9)
self.reader = bioformats.get_image_reader(self.key, self.path)
def __metadata__(self): if pixel_type == jvm.format_tools.INT8:
s = self.reader.rdr.getSeriesCount() dtype = np.int8
if self.series >= s: elif pixel_type == jvm.format_tools.UINT8:
print('Series {} does not exist.'.format(self.series)) dtype = np.uint8
self.reader.rdr.setSeries(self.series) 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
X = self.reader.rdr.getSizeX() try:
Y = self.reader.rdr.getSizeY() while not self.done.is_set():
C = self.reader.rdr.getSizeC() try:
Z = self.reader.rdr.getSizeZ() c, z, t = self.queue_in.get(True, 0.02)
T = self.reader.rdr.getSizeT() if reader.isRGB() and reader.isInterleaved():
self.shape = (X, Y, C, Z, T) 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)
omexml = bioformats.get_omexml_metadata(self.path) if image.ndim == 3:
self.metadata = XmlData(untangle.parse(omexml)) 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()
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] 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.
"""
priority = 99 # panic and open with BioFormats
do_not_pickle = 'reader', 'key', 'jvm'
pxsizeunit = image.search('PhysicalSizeXUnit')[0] @staticmethod
pxsize = image.search('PhysicalSizeX')[0] def _can_open(path):
if pxsize is not None: return not path.is_dir()
self.pxsize = pxsize / unit(pxsizeunit) * 1e6
if self.zstack: def open(self):
deltazunit = image.search('PhysicalSizeZUnit')[0] self.reader = JVMReader(self.path, self.series)
deltaz = image.search('PhysicalSizeZ')[0]
if deltaz is not None:
self.deltaz = deltaz / unit(deltazunit) * 1e6
if self.path.endswith('.lif'): def __frame__(self, c, z, t):
self.title = os.path.splitext(os.path.basename(self.path))[0] return self.reader.frame(c, z, t)
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): def close(self):
frame = self.reader.read(*args, rescale=False).astype('float') self.reader.close()
if frame.ndim == 3:
return frame[..., args[0]]
else:
return frame
def close(self):
bioformats.release_image_reader(self.key)

View File

@@ -1,115 +1,432 @@
from ndbioimage import Imread, XmlData, tolist
import czifile import czifile
import untangle
import numpy as np import numpy as np
import re 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 functools import cached_property
from itertools import product
from pathlib import Path
class Reader(Imread): class Reader(Imread, ABC):
priority = 0 priority = 0
do_not_pickle = 'reader', 'filedict', 'extrametadata' do_not_pickle = 'reader', 'filedict'
@staticmethod @staticmethod
def _can_open(path): def _can_open(path):
return isinstance(path, str) and path.endswith('.czi') return isinstance(path, Path) and path.suffix == '.czi'
def open(self): def open(self):
self.reader = czifile.CziFile(self.path) self.reader = czifile.CziFile(self.path)
filedict = {} filedict = {}
for directory_entry in self.reader.filtered_subblock_directory: for directory_entry in self.reader.filtered_subblock_directory:
idx = self.get_index(directory_entry, self.reader.start) idx = self.get_index(directory_entry, self.reader.start)
for c in range(*idx[self.reader.axes.index('C')]): if 'S' not in self.reader.axes or self.series in range(*idx[self.reader.axes.index('S')]):
for z in range(*idx[self.reader.axes.index('Z')]): for c in range(*idx[self.reader.axes.index('C')]):
for t in range(*idx[self.reader.axes.index('T')]): for z in range(*idx[self.reader.axes.index('Z')]):
if (c, z, t) in filedict: for t in range(*idx[self.reader.axes.index('T')]):
filedict[(c, z, t)].append(directory_entry) if (c, z, t) in filedict:
else: filedict[c, z, t].append(directory_entry)
filedict[(c, z, t)] = [directory_entry] else:
filedict[c, z, t] = [directory_entry]
self.filedict = filedict self.filedict = filedict
def close(self): def close(self):
self.reader.close() self.reader.close()
def __metadata__(self): @cached_property
# TODO: make sure frame function still works when a subblock has data from more than one frame def ome(self):
self.shape = tuple([self.reader.shape[self.reader.axes.index(directory_entry)] for directory_entry in 'XYCZT']) xml = self.reader.metadata()
self.metadata = XmlData(untangle.parse(self.reader.metadata())) attachments = {i.attachment_entry.name: i.attachment_entry.data_segment()
for i in self.reader.attachments()}
image = [i for i in self.metadata.search_all('Image').values() if i] tree = etree.fromstring(xml)
if len(image) and self.series in image[0]: metadata = tree.find("Metadata")
image = XmlData(image[0][self.series]) version = metadata.find("Version")
if version is not None:
version = version.text
else: else:
image = self.metadata version = metadata.find("Experiment").attrib["Version"]
pxsize = image.search('ScalingX')[0] if version == '1.0':
if pxsize is not None: return self.ome_10(tree, attachments)
self.pxsize = pxsize * 1e6 elif version == '1.2':
if self.zstack: return self.ome_12(tree, attachments)
deltaz = image.search('ScalingZ')[0]
if deltaz is not None:
self.deltaz = deltaz * 1e6
self.title = self.metadata.re_search(('Information', 'Document', 'Name'), self.title)[0] def ome_12(self, tree, attachments):
self.acquisitiondate = self.metadata.re_search(('Information', 'Document', 'CreationDate'), def text(item, default=""):
self.acquisitiondate)[0] return default if item is None else item.text
self.exposuretime = self.metadata.re_search(('TrackSetup', 'CameraIntegrationTime'), self.exposuretime)
if self.timeseries: ome = model.OME()
self.settimeinterval = self.metadata.re_search(('Interval', 'TimeSpan', 'Value'),
self.settimeinterval * 1e3)[0] / 1000 metadata = tree.find("Metadata")
if not self.settimeinterval:
self.settimeinterval = self.exposuretime[0] information = metadata.find("Information")
self.pxsizecam = self.metadata.re_search(('AcquisitionModeSetup', 'PixelPeriod'), self.pxsizecam) display_setting = metadata.find("DisplaySetting")
self.magnification = self.metadata.re_search('NominalMagnification', self.magnification)[0] ome.experimenters = [model.Experimenter(id="Experimenter:0",
attenuators = self.metadata.search_all('Attenuator') user_name=information.find("Document").find("UserName").text)]
self.laserwavelengths = [[1e9 * float(i['Wavelength']) for i in tolist(attenuator)]
for attenuator in attenuators.values()] instrument = information.find("Instrument")
self.laserpowers = [[float(i['Transmission']) for i in tolist(attenuator)] for _ in instrument.find("Microscopes"):
for attenuator in attenuators.values()] ome.instruments.append(model.Instrument())
self.collimator = self.metadata.re_search(('Collimator', 'Position'))
detector = self.metadata.search(('Instrument', 'Detector')) for detector in instrument.find("Detectors"):
self.gain = [int(i.get('AmplificationGain', 1)) for i in detector] try:
self.powermode = self.metadata.re_search(('TrackSetup', 'FWFOVPosition'))[0] detector_type = model.detector.Type(text(detector.find("Type")).upper() or "")
optovar = self.metadata.re_search(('TrackSetup', 'TubeLensPosition'), '1x') except ValueError:
self.optovar = [] detector_type = model.detector.Type.OTHER
for o in optovar:
a = re.search(r'\d?\d*[,.]?\d+(?=x$)', o) ome.instruments[0].detectors.append(
if hasattr(a, 'group'): model.Detector(
self.optovar.append(float(a.group(0).replace(',', '.'))) id=detector.attrib["Id"].replace(' ', ''), model=text(detector.find("Manufacturer").find("Model")),
self.pcf = [2 ** self.metadata.re_search(('Image', 'ComponentBitCount'), 14)[0] / float(i) type=detector_type
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] for objective in instrument.find("Objectives"):
self.NA = self.metadata.re_search(('Instrument', 'Objective', 'LensNA'))[0] ome.instruments[0].objectives.append(
self.filter = self.metadata.re_search(('TrackSetup', 'BeamSplitter', 'Filter'))[0] model.Objective(
self.tirfangle = [50 * i for i in self.metadata.re_search(('TrackSetup', 'TirfAngle'), 0)] id=objective.attrib["Id"],
self.frameoffset = [self.metadata.re_search(('AcquisitionModeSetup', 'CameraFrameOffsetX'))[0], model=text(objective.find("Manufacturer").find("Model")),
self.metadata.re_search(('AcquisitionModeSetup', 'CameraFrameOffsetY'))[0]] immersion=text(objective.find("Immersion")),
self.cnamelist = [c['DetectorSettings']['Detector']['Id'] for c in lens_na=float(text(objective.find("LensNA"))),
self.metadata['ImageDocument']['Metadata']['Information']['Image'].search('Channel')] nominal_magnification=float(text(objective.find("NominalMagnification")))))
try:
self.track, self.detector = zip(*[[int(i) for i in re.findall(r'\d', c)] for c in self.cnamelist]) for tubelens in instrument.find("TubeLenses"):
except ValueError: ome.instruments[0].objectives.append(
self.track = tuple(range(len(self.cnamelist))) model.Objective(
self.detector = (0,) * len(self.cnamelist) 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): def __frame__(self, c=0, z=0, t=0):
f = np.zeros(self.file_shape[:2], self.dtype) f = np.zeros(self.shape['xy'], self.dtype)
for directory_entry in self.filedict[(c, z, t)]: 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() subblock = directory_entry.data_segment()
tile = subblock.data(resize=True, order=0) tile = subblock.data(resize=True, order=0)
index = [slice(i - j, i - j + k) for i, j, k in axes_min = [xy_min.get(ax, 0) for ax in directory_entry.axes]
zip(directory_entry.start, self.reader.start, tile.shape)] index = [slice(i - j - m, i - j + k)
index = tuple([index[self.reader.axes.index(i)] for i in 'XY']) 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() f[index] = tile.squeeze()
return f return f
@staticmethod @staticmethod
def get_index(directory_entry, start): 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)] 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 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 priority = 20
do_not_pickle = 'ome'
do_not_copy = 'ome'
@staticmethod @staticmethod
def _can_open(path): def _can_open(path):
return isinstance(path, np.ndarray) and 1 <= path.ndim <= 5 return isinstance(path, np.ndarray) and 1 <= path.ndim <= 5
def __metadata__(self): @cached_property
self.base = np.array(self.path, ndmin=5) def ome(self):
self.title = self.path = 'numpy array' def shape(size_x=1, size_y=1, size_c=1, size_z=1, size_t=1):
self.axes = self.axes[:self.base.ndim] return size_x, size_y, size_c, size_z, size_t
self.shape = self.base.shape size_x, size_y, size_c, size_z, size_t = shape(*self.array.shape)
self.acquisitiondate = 'now' 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): def __frame__(self, c, z, t):
xyczt = (slice(None), slice(None), c, z, t) # xyczt = (slice(None), slice(None), c, z, t)
in_idx = tuple(xyczt['xyczt'.find(i)] for i in self.axes) # in_idx = tuple(xyczt['xyczt'.find(i)] for i in self.axes)
frame = self.base[in_idx] # print(f'{in_idx = }')
frame = self.array[:, :, c, z, t]
if self.axes.find('y') < self.axes.find('x'): if self.axes.find('y') < self.axes.find('x'):
return frame.T return frame.T
else: else:
return frame return frame
def __str__(self):
return self.path

View File

@@ -1,85 +1,110 @@
from ndbioimage import Imread, XmlData from abc import ABC
import os
import tifffile import tifffile
import yaml import yaml
import json
import re 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 priority = 10
@staticmethod @staticmethod
def _can_open(path): def _can_open(path):
return isinstance(path, str) and os.path.splitext(path)[1] == '' return isinstance(path, Path) and path.suffix == ""
def __metadata__(self): def get_metadata(self, c, z, t):
filelist = sorted([file for file in os.listdir(self.path) if re.search(r'^img_\d{3,}.*\d{3,}.*\.tif$', file)]) 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: @cached_property
with tifffile.TiffFile(os.path.join(self.path, filelist[0])) as tif: def ome(self):
self.metadata = XmlData({key: yaml.safe_load(value) ome = model.OME()
for key, value in tif.pages[0].tags[50839].value.items()}) metadata = self.get_metadata(0, 0, 0)
except Exception: # fallback ome.experimenters.append(
with open(os.path.join(self.path, 'metadata.txt'), 'r') as metadatafile: model.Experimenter(id="Experimenter:0", user_name=metadata["Info"]["Summary"]["UserName"]))
self.metadata = XmlData(json.loads(metadatafile.read())) 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 # compare channel names from metadata with filenames
cnamelist = self.metadata.search('ChNames') pattern_c = re.compile(r"img_\d{3,}_(.*)_\d{3,}$")
cnamelist = [c for c in cnamelist if any([c in f for f in filelist])] 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 = {} def open(self):
maxc = 0 if not self.path.name.startswith("Pos"):
maxz = 0 path = self.path / f"Pos{self.series}"
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)
else:
c = len(cnamelist)
cnamelist.append(C)
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]
X = self.metadata.search('Width')[0]
Y = self.metadata.search('Height')[0]
self.shape = (int(X), int(Y), maxc + 1, maxz + 1, maxt + 1)
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: else:
self.magnification = self.pxsizecam / self.pxsize path = self.path
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] 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()}
# 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])]
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): 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 numpy as np
import tifffile import tifffile
import yaml 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 priority = 0
do_not_pickle = 'reader' do_not_pickle = 'reader'
@staticmethod @staticmethod
def _can_open(path): 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: with tifffile.TiffFile(path) as tif:
return tif.is_imagej return tif.is_imagej
else: else:
return False 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): def open(self):
self.reader = tifffile.TiffFile(self.path) self.reader = tifffile.TiffFile(self.path)
def close(self): def close(self):
self.reader.close() 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): def __frame__(self, c, z, t):
if self.pndim == 3: if self.p_ndim == 3:
return np.transpose(self.reader.asarray(z + t * self.shape[3]), self.transpose)[c] return np.transpose(self.reader.asarray(z + t * self.file_shape[3]), self.p_transpose)[c]
else: 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 yaml
import os import re
import numpy as np import numpy as np
from copy import deepcopy from copy import deepcopy
from collections import OrderedDict from pathlib import Path
try: try:
# best if SimpleElastix is installed: https://simpleelastix.readthedocs.io/GettingStarted.html # best if SimpleElastix is installed: https://simpleelastix.readthedocs.io/GettingStarted.html
import SimpleITK as sitk import SimpleITK as sitk
installed = True
except ImportError: except ImportError:
installed = False sitk = None
try: try:
pp = True
from pandas import DataFrame, Series from pandas import DataFrame, Series
except ImportError: except ImportError:
pp = False DataFrame, Series = None, None
if hasattr(yaml, 'full_load'): if hasattr(yaml, 'full_load'):
@@ -24,42 +22,52 @@ else:
yamlload = yaml.load yamlload = yaml.load
class Transforms(OrderedDict): class Transforms(dict):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args[1:], **kwargs) super().__init__(*args[1:], **kwargs)
self.default = Transform()
if len(args): if len(args):
self.load(args[0]) 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): 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): def load(self, file):
if isinstance(file, dict): if isinstance(file, dict):
d = file d = file
else: else:
if not file[-3:] == 'yml': with open(file.with_suffix(".yml"), 'r') as f:
file += '.yml'
with open(file, 'r') as f:
d = yamlload(f) d = yamlload(f)
pattern = re.compile(r'[^\\]:')
for key, value in d.items(): 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): def __missing__(self, key):
track, detector = tracks[channel], detectors[channel] return self.default
if (track, detector) in self:
return self[track, detector]
elif (0, detector) in self:
return self[0, detector]
else:
return Transform()
def __reduce__(self): def __getstate__(self):
return self.__class__, (self.asdict(),) return self.__dict__
def __setstate__(self, state):
self.__dict__.update(state)
def save(self, file): def save(self, file):
if not file[-3:] == 'yml': with open(file.with_suffix(".yml"), 'w') as f:
file += '.yml'
with open(file, 'w') as f:
yaml.safe_dump(self.asdict(), f, default_flow_style=None) yaml.safe_dump(self.asdict(), f, default_flow_style=None)
def copy(self): def copy(self):
@@ -68,6 +76,7 @@ class Transforms(OrderedDict):
def adapt(self, origin, shape): def adapt(self, origin, shape):
for value in self.values(): for value in self.values():
value.adapt(origin, shape) value.adapt(origin, shape)
self.default.adapt(origin, shape)
@property @property
def inverse(self): def inverse(self):
@@ -76,15 +85,20 @@ class Transforms(OrderedDict):
inverse[key] = value.inverse inverse[key] = value.inverse
return inverse return inverse
@property
def ndim(self):
return len(list(self.keys())[0])
class Transform: class Transform:
def __init__(self, *args): def __init__(self, *args):
if not installed: if sitk is None:
raise ImportError('SimpleElastix is not installed: https://simpleelastix.readthedocs.io/GettingStarted.html') raise ImportError('SimpleElastix is not installed: '
self.transform = sitk.ReadTransform(os.path.join(os.path.dirname(__file__), 'transform.txt')) 'https://simpleelastix.readthedocs.io/GettingStarted.html')
self.dparameters = (0, 0, 0, 0, 0, 0) self.transform = sitk.ReadTransform(str(Path(__file__).parent / 'transform.txt'))
self.shape = (512, 512) self.dparameters = 0, 0, 0, 0, 0, 0
self.origin = (255.5, 255.5) self.shape = 512, 512
self.origin = 255.5, 255.5
if len(args) == 1: # load from file or dict if len(args) == 1: # load from file or dict
if isinstance(args[0], np.ndarray): if isinstance(args[0], np.ndarray):
self.matrix = args[0] self.matrix = args[0]
@@ -92,7 +106,7 @@ class Transform:
self.load(*args) self.load(*args)
elif len(args) > 1: # make new transform using fixed and moving image elif len(args) > 1: # make new transform using fixed and moving image
self.register(*args) self.register(*args)
self._last = None self._last, self._inverse = None, None
def __mul__(self, other): # TODO: take care of dmatrix def __mul__(self, other): # TODO: take care of dmatrix
result = self.copy() result = self.copy()
@@ -114,13 +128,13 @@ class Transform:
return deepcopy(self) return deepcopy(self)
@staticmethod @staticmethod
def castImage(im): def cast_image(im):
if not isinstance(im, sitk.Image): if not isinstance(im, sitk.Image):
im = sitk.GetImageFromArray(im) im = sitk.GetImageFromArray(im)
return im return im
@staticmethod @staticmethod
def castArray(im): def cast_array(im):
if isinstance(im, sitk.Image): if isinstance(im, sitk.Image):
im = sitk.GetArrayFromImage(im) im = sitk.GetArrayFromImage(im)
return im return im
@@ -190,7 +204,7 @@ class Transform:
dtype = im.dtype dtype = im.dtype
im = im.astype('float') im = im.astype('float')
intp = sitk.sitkBSpline if np.issubdtype(dtype, np.floating) else sitk.sitkNearestNeighbor 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): def coords(self, array, columns=None):
""" Transform coordinates in 2 column numpy array, """ Transform coordinates in 2 column numpy array,
@@ -198,7 +212,7 @@ class Transform:
""" """
if self.is_unity(): if self.is_unity():
return array.copy() 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'] columns = columns or ['x', 'y']
array = array.copy() array = array.copy()
if isinstance(array, DataFrame): if isinstance(array, DataFrame):
@@ -239,7 +253,7 @@ class Transform:
""" """
kind = kind or 'affine' kind = kind or 'affine'
self.shape = fix.shape 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 # TODO: implement RigidTransform
tfilter = sitk.ElastixImageFilter() tfilter = sitk.ElastixImageFilter()
tfilter.LogToConsoleOff() 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())