- 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:
0
ndbioimage/.gitignore → .gitignore
vendored
0
ndbioimage/.gitignore → .gitignore
vendored
45
README.md
45
README.md
@@ -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
@@ -1,2 +0,0 @@
|
|||||||
__version__ = '2022.7.0'
|
|
||||||
__git_commit_hash__ = 'unknown'
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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__)]
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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']]
|
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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)])
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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
37
pyproject.toml
Normal 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"
|
||||||
48
setup.py
48
setup.py
@@ -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
10
tests/test_open.py
Normal 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())
|
||||||
Reference in New Issue
Block a user