- make cache size configurable

This commit is contained in:
Wim Pomp
2025-01-08 13:16:34 +01:00
parent e6d5ec0928
commit f75e1925b8
6 changed files with 53 additions and 41 deletions

View File

@@ -68,8 +68,8 @@ with Imread('image_file.tif', axes='cztyx') as im:
### Command line
```ndbioimage --help```: show help
```ndbioimage image```: show metadata about image
```ndbioimage image {name}.tif -r```: copy image into image.tif (replacing {name} with image), while registering channels
```ndbioimage image image.mp4 -C cyan lime red``` copy image into image.mp4 (z will be max projected), make channel colors cyan lime and red
```ndbioimage image -w {name}.tif -r```: copy image into image.tif (replacing {name} with image), while registering channels
```ndbioimage image -w image.mp4 -C cyan lime red``` copy image into image.mp4 (z will be max projected), make channel colors cyan lime and red
## Adding more formats
Readers for image formats subclass AbstractReader. When an image reader is imported, Imread will

View File

@@ -65,18 +65,18 @@ class DequeDict(OrderedDict):
self.maxlen = maxlen
super().__init__(*args, **kwargs)
def __truncate__(self) -> None:
def __setitem__(self, *args: Any, **kwargs: Any) -> None:
super().__setitem__(*args, **kwargs)
self.truncate()
def truncate(self) -> None:
if self.maxlen is not None:
while len(self) > self.maxlen:
self.popitem(False)
def __setitem__(self, *args: Any, **kwargs: Any) -> None:
super().__setitem__(*args, **kwargs)
self.__truncate__()
def update(self, *args: Any, **kwargs: Any) -> None:
super().update(*args, **kwargs)
self.__truncate__()
super().update(*args, **kwargs) # type: ignore
self.truncate()
def find(obj: Sequence[Any], **kwargs: Any) -> Any:
@@ -249,6 +249,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
tirfangle: Optional[list[float]]
gain: Optional[list[float]]
pcf: Optional[list[float]]
path: Path
__frame__: Callable[[int, int, int], np.ndarray]
@staticmethod
@@ -331,8 +332,9 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
if hasattr(self, 'close'):
self.close()
def __getitem__(self, n: int | Sequence[int] | slice | type(Ellipsis) |
dict[str, int | Sequence[int] | slice | type(Ellipsis)]) -> Number | Imread | np.ndarray:
def __getitem__(self, n: int | Sequence[int] | Sequence[slice] | slice | type(Ellipsis) |
dict[str, int | Sequence[int] | Sequence[slice] | slice | type(Ellipsis)]
) -> Number | Imread | np.ndarray:
""" slice like a numpy array but return an Imread instance """
if self.isclosed:
raise OSError('file is closed')
@@ -380,7 +382,8 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
return new
def __getstate__(self) -> dict[str: Any]:
return {key: value for key, value in self.__dict__.items() if key not in self.do_not_pickle}
return ({key: value for key, value in self.__dict__.items() if key not in self.do_not_pickle} |
{'cache_size': self.cache.maxlen})
def __len__(self) -> int:
return self.shape[0]
@@ -390,10 +393,10 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
def __setstate__(self, state: dict[str, Any]) -> None:
""" What happens during unpickling """
self.__dict__.update(state)
self.__dict__.update({key: value for key, value in state.items() if key != 'cache_size'})
if isinstance(self, AbstractReader):
self.open()
self.cache = DequeDict(16)
self.cache = DequeDict(state.get('cache_size', 16))
def __str__(self) -> str:
return str(self.path)
@@ -509,7 +512,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
yxczt = (slice(None), slice(None)) + idx
in_idx = tuple(yxczt['yxczt'.find(i)] for i in self.axes)
w = where if where is None or isinstance(where, bool) else where[in_idx]
initials = [fun(np.asarray(ffun(self[in_idx])), initial=initial, where=w)
initials = [fun(np.asarray(ffun(self[in_idx])), initial=initial, where=w) # type: ignore
for fun, ffun, initial in zip(funs, ffuns, initials)]
res = cfun(*initials)
res = (np.round(res) if dtype.kind in 'ui' else res).astype(p.sub('', dtype.name))
@@ -543,7 +546,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
out_idx = tuple(yxczt['yxczt'.find(i)] for i in out_axes)
in_idx = tuple(yxczt['yxczt'.find(i)] for i in self.axes)
w = where if where is None or isinstance(where, bool) else where[in_idx]
res = cfun(*[fun(ffun(self[in_idx]), frame_ax, initial=initial, where=w)
res = cfun(*[fun(ffun(self[in_idx]), frame_ax, initial=initial, where=w) # type: ignore
for fun, ffun, initial in zip(funs, ffuns, initials)])
out[out_idx] = (np.round(res) if out.dtype.kind in 'ui' else res).astype(p.sub('', dtype.name))
else:
@@ -556,12 +559,12 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
if idx['czt'.find(axis_str)] == 0:
w = where if where is None or isinstance(where, bool) else (where[in_idx],)
for tmp, fun, ffun, initial in zip(tmps, funs, ffuns, initials):
tmp[out_idx] = fun((ffun(self[in_idx]),), 0, initial=initial, where=w)
tmp[out_idx] = fun((ffun(self[in_idx]),), 0, initial=initial, where=w) # type: ignore
else:
w = where if where is None or isinstance(where, bool) else \
(np.ones_like(where[in_idx]), where[in_idx])
for tmp, fun, ffun in zip(tmps, funs, ffuns):
tmp[out_idx] = fun((tmp[out_idx], ffun(self[in_idx])), 0, where=w)
tmp[out_idx] = fun((tmp[out_idx], ffun(self[in_idx])), 0, where=w) # type: ignore
out[...] = (np.round(cfun(*tmps)) if out.dtype.kind in 'ui' else
cfun(*tmps)).astype(p.sub('', dtype.name))
return out
@@ -606,7 +609,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
@property
def size(self) -> int:
return np.prod(self.shape)
return np.prod(self.shape) # type: ignore
@property
def shape(self) -> Shape:
@@ -665,7 +668,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
@property
def T(self) -> Imread: # noqa
return self.transpose()
return self.transpose() # type: ignore
@cached_property
def timeseries(self) -> bool:
@@ -782,7 +785,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
@wraps(np.ndarray.std)
def std(self, axis=None, dtype=None, out=None, ddof=0, keepdims=None, *, where=True):
return self.var(axis, dtype, out, ddof, keepdims, where=where, std=True)
return self.var(axis, dtype, out, ddof, keepdims, where=where, std=True) # type: ignore
@wraps(np.ndarray.sum)
def sum(self, axis=None, dtype=None, out=None, keepdims=False, initial=None, where=True, **_):
@@ -845,7 +848,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
for i, e in zip('yxczt', (y, x, c, z, t)))
d = np.empty((len(y), len(x), len(c), len(z), len(t)), self.dtype)
for (ci, cj), (zi, zj), (ti, tj) in product(enumerate(c), enumerate(z), enumerate(t)):
d[:, :, ci, zi, ti] = self.frame(cj, zj, tj)[y][:, x]
d[:, :, ci, zi, ti] = self.frame(cj, zj, tj)[y][:, x] # type: ignore
return d
def copy(self) -> View:
@@ -865,14 +868,15 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
# cache last n (default 16) frames in memory for speed (~250x faster)
key = (c, z, t, self.transform, self.frame_decorator)
if key in self.cache:
if self.cache.maxlen and key in self.cache:
self.cache.move_to_end(key)
f = self.cache[key]
else:
f = self.transform[self.channel_names[c], t].frame(self.__frame__(c, z, t))
if self.frame_decorator is not None:
f = self.frame_decorator(self, f, c, z, t)
self.cache[key] = f
if self.cache.maxlen:
self.cache[key] = f
if self.dtype is not None:
return f.copy().astype(self.dtype)
else:
@@ -942,7 +946,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
for plane in image.pixels.planes if plane.the_c == 0 and plane.the_t == 0])
i = np.argsort(z[:, 1])
image.pixels.physical_size_z = np.nanmean(np.true_divide(*np.diff(z[i], axis=0).T)) * 1e6
image.pixels.physical_size_z_unit = 'µm'
image.pixels.physical_size_z_unit = 'µm' # type: ignore
except Exception: # noqa
pass
return ome
@@ -1015,7 +1019,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
frame = np.dstack([255 * frame * i for i in color])
return np.clip(np.round(frame), 0, 255).astype('uint8')
ab = list(zip(*[get_ab(i) for i in self.transpose('cztyx')]))
ab = list(zip(*[get_ab(i) for i in self.transpose('cztyx')])) # type: ignore
colors = colors or ('r', 'g', 'b')[:self.shape['c']] + max(0, self.shape['c'] - 3) * ('w',)
brightnesses = brightnesses or (1,) * self.shape['c']
scale = scale or 1
@@ -1027,7 +1031,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
outputdict={'-vcodec': 'libx264', '-preset': 'veryslow', '-pix_fmt': 'yuv420p', '-r': '7',
'-vf': f'setpts={25 / 7}*PTS,scale={shape_x}:{shape_y}:flags=neighbor'}
) as movie:
im = self.transpose('tzcyx')
im = self.transpose('tzcyx') # type: ignore
for ti in tqdm(t, desc='Saving movie', disable=not bar):
movie.writeFrame(np.max([cframe(yx, c, a, b / s, scale)
for yx, a, b, c, s in zip(im[ti].max('z'), *ab, colors, brightnesses)], 0))
@@ -1061,7 +1065,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
n[i] = (n[i],)
shape = [len(i) for i in n]
with TransformTiff(self, fname.with_suffix('.tif'), pixel_type,
with TransformTiff(self, fname.with_suffix('.tif'), dtype=pixel_type,
pxsize=self.pxsize_um, deltaz=self.deltaz_um, **kwargs) as tif:
for i, m in tqdm(zip(product(*[range(s) for s in shape]), product(*n)), # noqa
total=np.prod(shape), desc='Saving tiff', disable=not bar):
@@ -1106,6 +1110,11 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
view.transform.adapt(view.frameoffset, view.shape.yxczt, view.channel_names)
return view
def set_cache_size(self, cache_size: int) -> None:
assert isinstance(cache_size, int) and cache_size >= 0
self.cache.maxlen = cache_size
self.cache.truncate()
@staticmethod
def split_path_series(path: Path | str) -> tuple[Path, int]:
if isinstance(path, str):
@@ -1320,12 +1329,13 @@ def main() -> None:
parser = ArgumentParser(description='Display info and save as tif')
parser.add_argument('-v', '--version', action='version', version=__version__)
parser.add_argument('file', help='image_file', type=str, nargs='*')
parser.add_argument('-w', '--write', help='path to tif/movie out, {folder}, {name} and {ext} take this from file in', type=str, default=None)
parser.add_argument('-w', '--write', help='path to tif/movie out, {folder}, {name} and {ext} take this from file in',
type=str, default=None)
parser.add_argument('-o', '--extract_ome', help='extract ome to xml file', action='store_true')
parser.add_argument('-r', '--register', help='register channels', action='store_true')
parser.add_argument('-c', '--channel', help='channel', type=int, default=None)
parser.add_argument('-z', '--zslice', help='z-slice', type=int, default=None)
parser.add_argument('-t', '--time', help='time', type=str, default=None)
parser.add_argument('-t', '--time', help='time (frames) in python slicing notation', type=str, default=None)
parser.add_argument('-s', '--split', help='split channels', action='store_true')
parser.add_argument('-f', '--force', help='force overwrite', action='store_true')
parser.add_argument('-C', '--movie-colors', help='colors for channels in movie', type=str, nargs='*')

View File

@@ -332,7 +332,7 @@ class OmeParse:
model.Objective(
id=objective.attrib['Id'],
model=self.text(objective.find('Manufacturer').find('Model')),
immersion=self.text(objective.find('Immersion')),
immersion=self.text(objective.find('Immersion')), # type: ignore
lens_na=float(self.text(objective.find('LensNA'))),
nominal_magnification=float(self.text(objective.find('NominalMagnification')))))
@@ -410,14 +410,14 @@ class OmeParse:
pixels=model.Pixels(
id='Pixels:0', size_x=self.size_x, size_y=self.size_y,
size_c=self.size_c, size_z=self.size_z, size_t=self.size_t,
dimension_order='XYCZT', type=pixel_type,
dimension_order='XYCZT', type=pixel_type, # type: ignore
significant_bits=int(self.text(image.find('ComponentBitCount'))),
big_endian=False, interleaved=False, metadata_only=True),
big_endian=False, interleaved=False, metadata_only=True), # type: ignore
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=self.text(objective_settings.find('Medium')),
medium=self.text(objective_settings.find('Medium')), # type: ignore
refractive_index=float(self.text(objective_settings.find('RefractiveIndex')))),
stage_label=model.StageLabel(
name=f'Scene position #0',
@@ -492,13 +492,13 @@ class OmeParse:
model.Channel(
id=f'Channel:{idx}',
name=channel.attrib['Name'],
acquisition_mode=self.text(channel.find('AcquisitionMode')),
acquisition_mode=self.text(channel.find('AcquisitionMode')), # type: ignore
color=model.Color(self.text(self.channels_ds[channel.attrib['Id']].find('Color'), 'white')),
detector_settings=model.DetectorSettings(id=detector.attrib['Id'], binning=binning),
# emission_wavelength=text(channel.find('EmissionWavelength')), # TODO: fix
excitation_wavelength=light_source_settings.wavelength,
filter_set_ref=model.FilterSetRef(id=self.ome.instruments[0].filter_sets[filterset_idx].id),
illumination_type=self.text(channel.find('IlluminationType')),
illumination_type=self.text(channel.find('IlluminationType')), # type: ignore
light_source_settings=light_source_settings,
samples_per_pixel=int(self.text(laser_scan_info.find('Averaging')))))
elif self.version in ('1.1', '1.2'):
@@ -543,7 +543,7 @@ class OmeParse:
model.Channel(
id=f'Channel:{idx}',
name=channel.attrib['Name'],
acquisition_mode=self.text(channel.find('AcquisitionMode')).replace(
acquisition_mode=self.text(channel.find('AcquisitionMode')).replace( # type: ignore
'SingleMoleculeLocalisation', 'SingleMoleculeImaging'),
color=color,
detector_settings=model.DetectorSettings(
@@ -553,7 +553,7 @@ class OmeParse:
excitation_wavelength=self.try_default(float, None,
self.text(channel.find('ExcitationWavelength'))),
# filter_set_ref=model.FilterSetRef(id=ome.instruments[0].filter_sets[filterset_idx].id),
illumination_type=self.text(channel.find('IlluminationType')),
illumination_type=self.text(channel.find('IlluminationType')), # type: ignore
light_source_settings=light_source_settings,
samples_per_pixel=samples_per_pixel))

View File

@@ -92,7 +92,8 @@ class Reader(AbstractReader, ABC):
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,
dimension_order='XYCZT', # type: ignore
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')))

View File

@@ -61,7 +61,8 @@ class Reader(AbstractReader, ABC):
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=dtype, physical_size_x=pxsize, physical_size_y=pxsize),
dimension_order='XYCZT', type=dtype, # type: ignore
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=interval_t * t))

View File

@@ -16,7 +16,7 @@ numpy = ">=1.20.0"
pandas = "*"
tifffile = "*"
czifile = "2019.7.2"
tiffwrite = ">=2024.10.4"
tiffwrite = ">=2025.1.0"
ome-types = ">=0.4.0"
pint = "*"
tqdm = "*"