Skip to content

Commit bf97721

Browse files
authored
Merge pull request #49 from lguerard/coding-session/2025-03-11
Follow-ups from the coding session of 2025-03-11 Yay, thanks for all the improvements! πŸš€
2 parents 1ea92fd + 8cef7bf commit bf97721

8 files changed

Lines changed: 939 additions & 54 deletions

File tree

β€Žsrc/imcflibs/imagej/bioformats.pyβ€Ž

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,243 @@ def write_bf_memoryfile(path_to_file):
289289
reader = Memoizer(ImageReader())
290290
reader.setId(path_to_file)
291291
reader.close()
292+
293+
294+
def get_metadata_from_file(path_to_image):
295+
"""Extract metadata from an image file using Bio-Formats.
296+
297+
This function reads an image file using the Bio-Formats library and extracts
298+
various metadata properties including physical dimensions, pixel dimensions,
299+
and other image characteristics.
300+
301+
Parameters
302+
----------
303+
path_to_image : str or pathlib.Path
304+
Path to the image file from which metadata should be extracted.
305+
306+
Returns
307+
-------
308+
dict
309+
A dictionary containing the following metadata:
310+
311+
{
312+
unit_width : float, # physical width of a pixel
313+
unit_height : float, # physical height of a pixel
314+
unit_depth : float, # physical depth of a voxel
315+
pixel_width : int, # width of the image in pixels
316+
pixel_height : int, # height of the image in pixels
317+
slice_count : int, # number of Z-slices
318+
channel_count : int, # number of channels
319+
timepoints_count : int, # number of timepoints
320+
dimension_order : str, # order of dimensions, e.g. "XYZCT"
321+
pixel_type : str, # data type of the pixel values
322+
}
323+
"""
324+
reader = ImageReader()
325+
ome_meta = MetadataTools.createOMEXMLMetadata()
326+
reader.setMetadataStore(ome_meta)
327+
reader.setId(str(path_to_image))
328+
329+
phys_size_x = ome_meta.getPixelsPhysicalSizeX(0)
330+
phys_size_y = ome_meta.getPixelsPhysicalSizeY(0)
331+
phys_size_z = ome_meta.getPixelsPhysicalSizeZ(0)
332+
pixel_size_x = ome_meta.getPixelsSizeX(0)
333+
pixel_size_y = ome_meta.getPixelsSizeY(0)
334+
pixel_size_z = ome_meta.getPixelsSizeZ(0)
335+
channel_count = ome_meta.getPixelsSizeC(0)
336+
timepoints_count = ome_meta.getPixelsSizeT(0)
337+
dimension_order = ome_meta.getPixelsDimensionOrder(0)
338+
pixel_type = ome_meta.getPixelsType(0)
339+
340+
image_calibration = {
341+
"unit_width": phys_size_x.value(),
342+
"unit_height": phys_size_y.value(),
343+
"unit_depth": phys_size_z.value(),
344+
"pixel_width": pixel_size_x.getNumberValue(),
345+
"pixel_height": pixel_size_y.getNumberValue(),
346+
"slice_count": pixel_size_z.getNumberValue(),
347+
"channel_count": channel_count.getNumberValue(),
348+
"timepoints_count": timepoints_count.getNumberValue(),
349+
"dimension_order": dimension_order,
350+
"pixel_type": pixel_type,
351+
}
352+
353+
reader.close()
354+
355+
return image_calibration
356+
357+
358+
def get_stage_coords(source, filenames):
359+
"""Get stage coordinates and calibration for a given list of images.
360+
361+
Parameters
362+
----------
363+
source : str
364+
Path to the images.
365+
filenames : list of str
366+
List of images filenames.
367+
368+
Returns
369+
-------
370+
dict
371+
372+
{
373+
dimensions : int, # number of dimensions (2D or 3D)
374+
stage_coordinates_x : list, # absolute stage x-coordinated
375+
stage_coordinates_y : list, # absolute stage y-coordinated
376+
stage_coordinates_z : list, # absolute stage z-coordinated
377+
relative_coordinates_x : list, # relative stage x-coordinates in px
378+
relative_coordinates_y : list, # relative stage y-coordinates in px
379+
relative_coordinates_z : list, # relative stage z-coordinates in px
380+
image_calibration : list, # x,y,z image calibration in unit/px
381+
calibration_unit : str, # image calibration unit
382+
image_dimensions_czt : list, # number of images in dimensions c,z,t
383+
series_names : list of str, # names of all series in the files
384+
max_size : list of int, # max size (x/y/z) across all files
385+
}
386+
"""
387+
388+
# open an array to store the abosolute stage coordinates from metadata
389+
stage_coordinates_x = []
390+
stage_coordinates_y = []
391+
stage_coordinates_z = []
392+
series_names = []
393+
394+
for counter, image in enumerate(filenames):
395+
# parse metadata
396+
reader = ImageReader()
397+
reader.setFlattenedResolutions(False)
398+
omeMeta = MetadataTools.createOMEXMLMetadata()
399+
reader.setMetadataStore(omeMeta)
400+
reader.setId(source + str(image))
401+
series_count = reader.getSeriesCount()
402+
403+
# get hyperstack dimensions from the first image
404+
if counter == 0:
405+
frame_size_x = reader.getSizeX()
406+
frame_size_y = reader.getSizeY()
407+
frame_size_z = reader.getSizeZ()
408+
frame_size_c = reader.getSizeC()
409+
frame_size_t = reader.getSizeT()
410+
411+
# note the dimensions
412+
if frame_size_z == 1:
413+
dimensions = 2
414+
if frame_size_z > 1:
415+
dimensions = 3
416+
417+
# get the physical calibration for the first image series
418+
physSizeX = omeMeta.getPixelsPhysicalSizeX(0)
419+
physSizeY = omeMeta.getPixelsPhysicalSizeY(0)
420+
physSizeZ = omeMeta.getPixelsPhysicalSizeZ(0)
421+
422+
# workaround to get the z-interval if physSizeZ.value() returns None.
423+
z_interval = 1
424+
if physSizeZ is not None:
425+
z_interval = physSizeZ.value()
426+
427+
if frame_size_z > 1 and physSizeZ is None:
428+
log.debug("no z calibration found, trying to recover")
429+
first_plane = omeMeta.getPlanePositionZ(0, 0)
430+
next_plane_imagenumber = frame_size_c + frame_size_t - 1
431+
second_plane = omeMeta.getPlanePositionZ(0, next_plane_imagenumber)
432+
z_interval = abs(abs(first_plane.value()) - abs(second_plane.value()))
433+
log.debug("z-interval seems to be: " + str(z_interval))
434+
435+
# create an image calibration
436+
image_calibration = [
437+
physSizeX.value(),
438+
physSizeY.value(),
439+
z_interval,
440+
]
441+
calibration_unit = physSizeX.unit().getSymbol()
442+
image_dimensions_czt = [
443+
frame_size_c,
444+
frame_size_z,
445+
frame_size_t,
446+
]
447+
448+
reader.close()
449+
450+
for series in range(series_count):
451+
if omeMeta.getImageName(series) == "macro image":
452+
continue
453+
454+
if series_count > 1 and not str(image).endswith(".vsi"):
455+
series_names.append(omeMeta.getImageName(series))
456+
else:
457+
series_names.append(str(image))
458+
# get the plane position in calibrated units
459+
current_position_x = omeMeta.getPlanePositionX(series, 0)
460+
current_position_y = omeMeta.getPlanePositionY(series, 0)
461+
current_position_z = omeMeta.getPlanePositionZ(series, 0)
462+
463+
physSizeX_max = (
464+
physSizeX.value()
465+
if physSizeX.value() >= omeMeta.getPixelsPhysicalSizeX(series).value()
466+
else omeMeta.getPixelsPhysicalSizeX(series).value()
467+
)
468+
physSizeY_max = (
469+
physSizeY.value()
470+
if physSizeY.value() >= omeMeta.getPixelsPhysicalSizeY(series).value()
471+
else omeMeta.getPixelsPhysicalSizeY(series).value()
472+
)
473+
if omeMeta.getPixelsPhysicalSizeZ(series):
474+
physSizeZ_max = (
475+
physSizeZ.value()
476+
if physSizeZ.value()
477+
>= omeMeta.getPixelsPhysicalSizeZ(series).value()
478+
else omeMeta.getPixelsPhysicalSizeZ(series).value()
479+
)
480+
481+
else:
482+
physSizeZ_max = 1.0
483+
484+
# get the absolute stage positions and store them
485+
pos_x = current_position_x.value()
486+
pos_y = current_position_y.value()
487+
488+
if current_position_z is None:
489+
log.debug("the z-position is missing in the ome-xml metadata.")
490+
pos_z = 1.0
491+
else:
492+
pos_z = current_position_z.value()
493+
494+
stage_coordinates_x.append(pos_x)
495+
stage_coordinates_y.append(pos_y)
496+
stage_coordinates_z.append(pos_z)
497+
498+
max_size = [physSizeX_max, physSizeY_max, physSizeZ_max]
499+
500+
# calculate the store the relative stage movements in px (for the grid/collection stitcher)
501+
relative_coordinates_x_px = []
502+
relative_coordinates_y_px = []
503+
relative_coordinates_z_px = []
504+
505+
for i in range(len(stage_coordinates_x)):
506+
rel_pos_x = (
507+
stage_coordinates_x[i] - stage_coordinates_x[0]
508+
) / physSizeX.value()
509+
rel_pos_y = (
510+
stage_coordinates_y[i] - stage_coordinates_y[0]
511+
) / physSizeY.value()
512+
rel_pos_z = (stage_coordinates_z[i] - stage_coordinates_z[0]) / z_interval
513+
514+
relative_coordinates_x_px.append(rel_pos_x)
515+
relative_coordinates_y_px.append(rel_pos_y)
516+
relative_coordinates_z_px.append(rel_pos_z)
517+
518+
return {
519+
"dimensions": dimensions,
520+
"stage_coordinates_x": stage_coordinates_x,
521+
"stage_coordinates_y": stage_coordinates_y,
522+
"stage_coordinates_z": stage_coordinates_z,
523+
"relative_coordinates_x": relative_coordinates_x_px,
524+
"relative_coordinates_y": relative_coordinates_y_px,
525+
"relative_coordinates_z": relative_coordinates_z_px,
526+
"image_calibration": image_calibration,
527+
"calibration_unit": calibration_unit,
528+
"image_dimensions_czt": image_dimensions_czt,
529+
"series_names": series_names,
530+
"max_size": max_size,
531+
}

β€Žsrc/imcflibs/imagej/labelimage.pyβ€Ž

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
"""Functions to work with ImageJ label images."""
44

5-
from ij import IJ, ImagePlus, Prefs, ImageStack
5+
from ij import IJ, ImagePlus, ImageStack, Prefs
66
from ij.plugin import Duplicator, ImageCalculator
77
from ij.plugin.filter import ThresholdToSelection
88
from ij.process import FloatProcessor, ImageProcessor
@@ -67,7 +67,7 @@ def label_image_to_roi_list(label_image, low_thresh=None):
6767
return roi_list, max_value
6868

6969

70-
def relate_label_images(label_image_ref, label_image_to_relate):
70+
def cookie_cut_labels(label_image_ref, label_image_to_relate):
7171
"""Relate label images, giving the same label to objects belonging together.
7272
7373
❗ NOTE: Won't work with touching labels ❗
@@ -97,6 +97,57 @@ def relate_label_images(label_image_ref, label_image_to_relate):
9797
return ImageCalculator.run(label_image_ref, imp_dup, "Multiply create")
9898

9999

100+
def relate_label_images(outer_label_imp, inner_label_imp):
101+
"""Relate label images, giving the same label to objects belonging together.
102+
103+
Given two label images, this function will create a new label image with the
104+
same labels as the reference image, but with the objects of the second image
105+
using the 3D Association plugin from the 3DImageJSuite.
106+
107+
Parameters
108+
----------
109+
outer_label_imp : ij.ImagePlus
110+
The outer label image.
111+
inner_label_imp : ij.ImagePlus
112+
The inner label image.
113+
114+
Returns
115+
-------
116+
related_inner_imp : ij.ImagePlus
117+
The related inner label image.
118+
119+
Notes
120+
-----
121+
Unlike `cookie_cut_labels`, this should work with touching labels by using
122+
MereoTopology algorithms.
123+
"""
124+
125+
outer_label_imp.show()
126+
inner_label_imp.show()
127+
128+
outer_title = outer_label_imp.getTitle()
129+
inner_title = inner_label_imp.getTitle()
130+
131+
IJ.run(
132+
"3D Association",
133+
"image_a="
134+
+ outer_title
135+
+ " "
136+
+ "image_b="
137+
+ inner_title
138+
+ " "
139+
+ "method=Colocalisation min=1 max=0.000",
140+
)
141+
142+
related_inner_imp = IJ.getImage()
143+
144+
outer_label_imp.hide()
145+
inner_label_imp.hide()
146+
related_inner_imp.hide()
147+
148+
return related_inner_imp
149+
150+
100151
def filter_objects(label_image, table, string, min_val, max_val):
101152
"""Filter labels based on specific min and max values.
102153
@@ -182,11 +233,11 @@ def binary_to_label(imp, title, min_thresh=1, min_vol=None, max_vol=None):
182233

183234
# Set the minimum size for labeling if provided
184235
if min_vol:
185-
labeler.setMinSize(min_vol)
236+
labeler.setMinSizeCalibrated(min_vol)
186237

187238
# Set the maximum size for labeling if provided
188239
if max_vol:
189-
labeler.setMaxSize(max_vol)
240+
labeler.setMinSizeCalibrated(max_vol)
190241

191242
# Get the labeled image
192243
seg = labeler.getLabels(img)
@@ -229,17 +280,7 @@ def dilate_labels_2d(imp, dilation_radius):
229280
current_imp = Duplicator().run(imp, 1, 1, i, imp.getNSlices(), 1, 1)
230281

231282
# Perform a dilation of the labels in the current slice
232-
IJ.run(
233-
current_imp,
234-
"Label Morphological Filters",
235-
"operation=Dilation radius=" + str(dilation_radius) + " from_any_label",
236-
)
237-
238-
# Get the dilated labels
239-
dilated_labels_imp = IJ.getImage()
240-
241-
# Hide the dilated labels to avoid visual clutter
242-
dilated_labels_imp.hide()
283+
dilated_labels_imp = li.dilateLabels(current_imp, dilation_radius)
243284

244285
# Append the dilated labels to the list
245286
dilated_labels_list.append(dilated_labels_imp)

0 commit comments

Comments
Β (0)