From 86a34472b120f5dde94070032d2df94871a17e5f Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Tue, 11 Mar 2025 16:57:18 +0100 Subject: [PATCH 01/64] Add method for associating label images using 3DImageJSuite --- src/imcflibs/imagej/labelimage.py | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/imcflibs/imagej/labelimage.py b/src/imcflibs/imagej/labelimage.py index 5d800b48..6391fe60 100644 --- a/src/imcflibs/imagej/labelimage.py +++ b/src/imcflibs/imagej/labelimage.py @@ -97,6 +97,51 @@ def relate_label_images(label_image_ref, label_image_to_relate): return ImageCalculator.run(label_image_ref, imp_dup, "Multiply create") +def associate_label_images_3d(outer_label_imp, inner_label_imp): + """ + Associate two label images. + + Uses the 3D Association plugin from the 3DImageJSuite. + + Parameters + ---------- + outer_label_imp : ij.ImagePlus + The outer label image + inner_label_imp : ij.ImagePlus + The inner label image + + Returns + ------- + related_inner_imp : ij.ImagePlus + The related inner label image + """ + + outer_label_imp.show() + inner_label_imp.show() + + outer_title = outer_label_imp.getTitle() + inner_title = inner_label_imp.getTitle() + + IJ.run( + "3D Association", + "image_a=" + + outer_title + + " " + + "image_b=" + + inner_title + + " " + + "method=Colocalisation min=1 max=0.000", + ) + + related_inner_imp = IJ.getImage() + + outer_label_imp.hide() + inner_label_imp.hide() + related_inner_imp.hide() + + return related_inner_imp + + def filter_objects(label_image, table, string, min_val, max_val): """Filter labels based on specific min and max values. From 97084b62abdfb4cac1afec2318e200a21e7410c5 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Tue, 11 Mar 2025 17:01:08 +0100 Subject: [PATCH 02/64] Fix the filtering of objects to use the calibrated method --- src/imcflibs/imagej/labelimage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/imcflibs/imagej/labelimage.py b/src/imcflibs/imagej/labelimage.py index 6391fe60..9a29183b 100644 --- a/src/imcflibs/imagej/labelimage.py +++ b/src/imcflibs/imagej/labelimage.py @@ -227,11 +227,11 @@ def binary_to_label(imp, title, min_thresh=1, min_vol=None, max_vol=None): # Set the minimum size for labeling if provided if min_vol: - labeler.setMinSize(min_vol) + labeler.setMinSizeCalibrated(min_vol) # Set the maximum size for labeling if provided if max_vol: - labeler.setMaxSize(max_vol) + labeler.setMinSizeCalibrated(max_vol) # Get the labeled image seg = labeler.getLabels(img) From 408131ccd06627f470f51bf7159515ca2dc0efae Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Tue, 11 Mar 2025 17:01:28 +0100 Subject: [PATCH 03/64] Use the morpholibj package to 2D dilate labels --- src/imcflibs/imagej/labelimage.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/imcflibs/imagej/labelimage.py b/src/imcflibs/imagej/labelimage.py index 9a29183b..1b996424 100644 --- a/src/imcflibs/imagej/labelimage.py +++ b/src/imcflibs/imagej/labelimage.py @@ -274,17 +274,7 @@ def dilate_labels_2d(imp, dilation_radius): current_imp = Duplicator().run(imp, 1, 1, i, imp.getNSlices(), 1, 1) # Perform a dilation of the labels in the current slice - IJ.run( - current_imp, - "Label Morphological Filters", - "operation=Dilation radius=" + str(dilation_radius) + " from_any_label", - ) - - # Get the dilated labels - dilated_labels_imp = IJ.getImage() - - # Hide the dilated labels to avoid visual clutter - dilated_labels_imp.hide() + dilated_labels_imp = li.dilateLabels(current_imp, dilation_radius) # Append the dilated labels to the list dilated_labels_list.append(dilated_labels_imp) From c542b19c5727530b21eed8a973674e6da00ed338 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Tue, 11 Mar 2025 17:01:59 +0100 Subject: [PATCH 04/64] Format imports --- src/imcflibs/imagej/labelimage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imcflibs/imagej/labelimage.py b/src/imcflibs/imagej/labelimage.py index 1b996424..b68c4de8 100644 --- a/src/imcflibs/imagej/labelimage.py +++ b/src/imcflibs/imagej/labelimage.py @@ -2,7 +2,7 @@ """Functions to work with ImageJ label images.""" -from ij import IJ, ImagePlus, Prefs, ImageStack +from ij import IJ, ImagePlus, ImageStack, Prefs from ij.plugin import Duplicator, ImageCalculator from ij.plugin.filter import ThresholdToSelection from ij.process import FloatProcessor, ImageProcessor From 886613a606ab9dc4ca891e22bff48a2694e4909f Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Tue, 11 Mar 2025 17:02:27 +0100 Subject: [PATCH 05/64] Add method to write results to CSV --- src/imcflibs/imagej/misc.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 10994a92..b0a0154c 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -4,6 +4,7 @@ import time import smtplib import os +import csv from ij import IJ # pylint: disable-msg=import-error from ij.plugin import ImageCalculator @@ -410,3 +411,30 @@ def get_threshold_value_from_method(imp, method, ops): threshold_value = int(round(threshold_value.get())) return threshold_value + + +def write_results(out_file, content): + """ + Write the results to a csv file. + + Parameters + ---------- + out_file : str + Path to the output file. + content : list of OrderedDict + List of dictionaries representing the results. + + """ + + # Check if the output file exists + if not os.path.exists(out_file): + # If the file does not exist, create it and write the header + with open(out_file, "wb") as f: + dict_writer = csv.DictWriter(f, content[0].keys(), delimiter=";") + dict_writer.writeheader() + dict_writer.writerows(content) + else: + # If the file exists, append the results + with open(out_file, "ab") as f: + dict_writer = csv.DictWriter(f, content[0].keys(), delimiter=";") + dict_writer.writerows(content) From 831d771af57e90627c83520d359202227879c3d5 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Tue, 11 Mar 2025 17:03:02 +0100 Subject: [PATCH 06/64] Add methods for 3D Maxima Finder and 3D Watershed --- src/imcflibs/imagej/objects3d.py | 96 ++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/imcflibs/imagej/objects3d.py b/src/imcflibs/imagej/objects3d.py index 9bcbd384..c1000d8d 100644 --- a/src/imcflibs/imagej/objects3d.py +++ b/src/imcflibs/imagej/objects3d.py @@ -1,6 +1,10 @@ +from de.mpicbg.scf.imgtools.image.create.image import ImageCreationUtilities +from de.mpicbg.scf.imgtools.image.create.labelmap import WatershedLabeling from ij import IJ from mcib3d.geom import Objects3DPopulation from mcib3d.image3d import ImageHandler, ImageLabeller +from mcib3d.image3d.processing import MaximaFinder +from net.imglib2.img import ImagePlusAdapter def population3d_to_imgplus(imp, population): @@ -138,3 +142,95 @@ def get_objects_within_intensity(obj_pop, imp, min_intensity, max_intensity): # Return the new population with the filtered objects return Objects3DPopulation(objects_within_intensity) + + +def maxima_finder_3D(imageplus, min_threshold=0, noise=100, rxy=1.5, rz=1.5): + """ + Find local maxima in a 3D image. + + This function identifies local maxima in a 3D image using a specified minimum threshold and noise level. + The radii for the maxima detection can be set independently for the x/y and z dimensions. + + Parameters + ---------- + imageplus : ij.ImagePlus + The input 3D image in which to find local maxima. + min_threshold : int, optional + The minimum intensity threshold for maxima detection. Default is 0. + noise : int, optional + The noise tolerance level for maxima detection. Default is 100. + rxy : float, optional + The radius for maxima detection in the x and y dimensions. Default is 1.5. + rz : float, optional + The radius for maxima detection in the z dimension. Default is 1.5. + + Returns + ------- + ij.ImagePlus + An ImagePlus object containing the detected maxima as peaks. + """ + # Wrap the input ImagePlus into an ImageHandler + img = ImageHandler.wrap(imageplus) + + # Duplicate the image and apply a threshold cut-off + thresholded = img.duplicate() + thresholded.thresholdCut(min_threshold, False, True) + + # Initialize the MaximaFinder with the thresholded image and noise level + maxima_finder = MaximaFinder(thresholded, noise) + + # Set the radii for maxima detection in x/y and z dimensions + maxima_finder.setRadii(rxy, rz) + + # Retrieve the image peaks as an ImageHandler + img_peaks = maxima_finder.getImagePeaks() + + # Convert the ImageHandler peaks to an ImagePlus + imp_peaks = img_peaks.getImagePlus() + + # Set the calibration of the peaks image to match the input image + imp_peaks.setCalibration(imageplus.getCalibration()) + + # Set the title of the peaks image + imp_peaks.setTitle("Peaks") + + return imp_peaks + + +def seeded_watershed(imp_binary, imp_peaks, threshold=10): + """ + Perform a seeded watershed segmentation on a binary image using seed points. + + This function applies a watershed segmentation to a binary image using seed points provided in another image. + An optional threshold can be specified to control the segmentation process. + + Parameters + ---------- + imp_binary : ij.ImagePlus + The binary image to segment. + imp_peaks : ij.ImagePlus + The image containing the seed points for the watershed segmentation. + threshold : float, optional + The threshold value to use for the segmentation. Default is 10. + + Returns + ------- + ij.ImagePlus + The segmented image with labels. + """ + + img = ImagePlusAdapter.convertFloat(imp_binary) + img_seed = ImagePlusAdapter.convertFloat(imp_peaks).copy() + + if threshold: + watersheded_result = WatershedLabeling.watershed(img, img_seed, threshold) + else: + watersheded_result = WatershedLabeling.watershed(img, img_seed) + + return ImageCreationUtilities.convertImgToImagePlus( + watersheded_result, + "Label image", + "", + imp_binary.getDimensions(), + imp_binary.getCalibration(), + ) From 4dc45044d63289aeb94bf9efae6ad87dfe2c4b20 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Tue, 11 Mar 2025 17:04:33 +0100 Subject: [PATCH 07/64] Add processing library to do basic methods Add one to apply a filter, to do rolling ball background subtraction and thresholding --- src/imcflibs/imagej/processing.py | 135 ++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/imcflibs/imagej/processing.py diff --git a/src/imcflibs/imagej/processing.py b/src/imcflibs/imagej/processing.py new file mode 100644 index 00000000..de34a348 --- /dev/null +++ b/src/imcflibs/imagej/processing.py @@ -0,0 +1,135 @@ +from ij import IJ + +from ..log import LOG as log + +def apply_filter(imp, filter_method, filter_radius, do_3D=False): + """ + Make a specific filter followed by a threshold method of choice + + Parameters + ---------- + imp : ImagePlus + Input ImagePlus to filter and threshold + filter_method : str + Name of the filter method to use. Must be one of: + - Median + - Mean + - Gaussian Blur + - Minimum + - Maximum + filter_radius : int + Radius of the filter filter to use + do_3d : bool, optional + If set to True, will do a 3D filtering, by default False + + + Returns + ------- + ij.ImagePlus + Filtered ImagePlus + """ + log.info("Applying filter %s with radius %d" % (filter_method, filter_radius)) + + if filter_method not in ["Median", "Mean", "Gaussian Blur", "Minimum", "Maximum"]: + raise ValueError( + "filter_method must be one of: Median, Mean, Gaussian Blur, Minimum, Maximum" + ) + + if do_3d: + filter = filter_method + " 3D..." + else: + filter = filter_method + "..." + + options = ( + "sigma=" if filter_method == "Gaussian Blur" else "radius=" + + str(filter_radius) + + " stack" + ) + + log.debug("Filter: <%s> with options <%s>" % (filter, options)) + + imageplus = imp.duplicate() + IJ.run(imageplus, filter, options) + + return imageplus + +def apply_background_subtraction(imp, rolling_ball_radius, do_3D=False): + """ + Perform background subtraction using a rolling ball method + + Parameters + ---------- + imp : ij.ImagePlus + Input ImagePlus to filter and threshold + rolling_ball_radius : int + Radius of the rolling ball filter to use + do_3d : bool, optional + If set to True, will do a 3D filtering, by default False + + Returns + ------- + ij.ImagePlus + Filtered ImagePlus + """ + log.info("Applying rolling ball with radius %d" % rolling_ball_radius) + + options = ( + "rolling=" + str(rolling_ball_radius) + + " stack" if do_3D else "" + ) + + log.debug("Background subtraction options: %s" % options) + + imageplus = imp.duplicate() + IJ.run(imageplus, "Substract Background...", options) + + return imageplus + +def apply_threshold(imp, threshold_method): + """ + Apply a threshold method to the input ImagePlus + + Parameters + ---------- + imp : ij.ImagePlus + Input ImagePlus to filter and threshold + threshold_method : str + Name of the threshold method to use + do_3d : bool, optional + If set to True, the automatic threshold will be done on a 3D stack, by default True + + Returns + ------- + ij.ImagePlus + Thresholded ImagePlus + """ + + log.info("Applying threshold method %s" % threshold_method) + + imageplus = imp.duplicate() + + auto_threshold_options = ( + threshold_method + + " " + + "dark" + + " " + + "stack" if do_3D else "" + ) + + log.debug("Auto threshold options: %s" % auto_threshold_options) + + IJ.setAutoThreshold(imageplus, auto_threshold_options) + + convert_to_binary_options = ( + "method=" + threshold_method + + " " + + "background=Dark" + + " " + + "black" + ) + + log.debug("Convert to binary options: %s" % convert_to_binary_options) + + IJ.run(imageplus, "Convert to Mask", convert_to_binary_options) + + return imageplus \ No newline at end of file From 6fa4d699d8aadcc843f27a9b60f81d76f880f5e0 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Thu, 13 Mar 2025 16:06:01 +0100 Subject: [PATCH 08/64] Add methods to get different metadata using bioformats --- src/imcflibs/imagej/bioformats.py | 249 ++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) diff --git a/src/imcflibs/imagej/bioformats.py b/src/imcflibs/imagej/bioformats.py index 60865845..154e7f2f 100644 --- a/src/imcflibs/imagej/bioformats.py +++ b/src/imcflibs/imagej/bioformats.py @@ -289,3 +289,252 @@ def write_bf_memoryfile(path_to_file): reader = Memoizer(ImageReader()) reader.setId(path_to_file) reader.close() + + +def get_metadata_from_image(path_to_image): + """Extract metadata from an image file using Bio-Formats. + + This function reads an image file using the Bio-Formats library and extracts + various metadata properties including physical dimensions, pixel dimensions, + and other image characteristics. + + Parameters + ---------- + path_to_image : str or pathlib.Path + Path to the image file from which metadata should be extracted. + + Returns + ------- + dict + A dictionary containing the following metadata: + - unit_width : float + Physical width of a pixel. + - unit_height : float + Physical height of a pixel. + - unit_depth : float + Physical depth of a voxel. + - pixel_width : int + Width of the image in pixels. + - pixel_height : int + Height of the image in pixels. + - slice_count : int + Number of Z-slices. + - channel_count : int + Number of channels. + - timepoints_count : int + Number of timepoints. + - dimension_order : str + Order of dimensions in the image (e.g., 'XYZCT'). + - pixel_type : str + Data type of the pixel values. + """ + reader = ImageReader() + ome_meta = MetadataTools.createOMEXMLMetadata() + reader.setMetadataStore(ome_meta) + reader.setId(str(path_to_image)) + + phys_size_x = ome_meta.getPixelsPhysicalSizeX(0) + phys_size_y = ome_meta.getPixelsPhysicalSizeY(0) + phys_size_z = ome_meta.getPixelsPhysicalSizeZ(0) + pixel_size_x = ome_meta.getPixelsSizeX(0) + pixel_size_y = ome_meta.getPixelsSizeY(0) + pixel_size_z = ome_meta.getPixelsSizeZ(0) + channel_count = ome_meta.getPixelsSizeC(0) + timepoints_count = ome_meta.getPixelsSizeT(0) + dimension_order = ome_meta.getPixelsDimensionOrder(0) + pixel_type = ome_meta.getPixelsType(0) + + image_calibration = { + "unit_width": phys_size_x.value(), + "unit_height": phys_size_y.value(), + "unit_depth": phys_size_z.value(), + "pixel_width": pixel_size_x.getNumberValue(), + "pixel_height": pixel_size_y.getNumberValue(), + "slice_count": pixel_size_z.getNumberValue(), + "channel_count": channel_count.getNumberValue(), + "timepoints_count": timepoints_count.getNumberValue(), + "dimension_order": dimension_order, + "pixel_type": pixel_type, + } + + reader.close() + + return image_calibration + + +def get_stage_coordinates_from_ome_metadata(source, imagenames): + """Get the stage coordinates and calibration from the ome-xml for a given list of images + + Parameters + ---------- + source : str + Path to the images + imagenames : list of str + List of images filenames + + Returns + ------- + tuple + Contains + dimensions : int + Number of dimensions (2D or 3D) + stage_coordinates_x : list + The absolute stage x-coordinated from ome-xml metadata + stage_coordinates_y : list + The absolute stage y-coordinated from ome-xml metadata + stage_coordinates_z : list + The absolute stage z-coordinated from ome-xml metadata + relative_coordinates_x : list + The relative stage x-coordinates in px + relative_coordinates_y : list + The relative stage y-coordinates in px + relative_coordinates_z : list + The relative stage z-coordinates in px + image_calibration : list + x,y,z image calibration in unit/px + calibration_unit : str + Image calibration unit + image_dimensions_czt : list + Number of images in dimensions c,z,t + series_names : list of str + Names of all series contained in the files + max_size : list of int + Maximum size across all files in dimensions x,y,z + """ + + # open an array to store the abosolute stage coordinates from metadata + stage_coordinates_x = [] + stage_coordinates_y = [] + stage_coordinates_z = [] + series_names = [] + + for counter, image in enumerate(imagenames): + # parse metadata + reader = ImageReader() + reader.setFlattenedResolutions(False) + omeMeta = MetadataTools.createOMEXMLMetadata() + reader.setMetadataStore(omeMeta) + reader.setId(source + str(image)) + series_count = reader.getSeriesCount() + + # get hyperstack dimensions from the first image + if counter == 0: + frame_size_x = reader.getSizeX() + frame_size_y = reader.getSizeY() + frame_size_z = reader.getSizeZ() + frame_size_c = reader.getSizeC() + frame_size_t = reader.getSizeT() + + # note the dimensions + if frame_size_z == 1: + dimensions = 2 + if frame_size_z > 1: + dimensions = 3 + + # get the physical calibration for the first image series + physSizeX = omeMeta.getPixelsPhysicalSizeX(0) + physSizeY = omeMeta.getPixelsPhysicalSizeY(0) + physSizeZ = omeMeta.getPixelsPhysicalSizeZ(0) + + # workaround to get the z-interval if physSizeZ.value() returns None. + z_interval = 1 + if physSizeZ is not None: + z_interval = physSizeZ.value() + + if frame_size_z > 1 and physSizeZ is None: + print("no z calibration found, trying to recover") + first_plane = omeMeta.getPlanePositionZ(0, 0) + next_plane_imagenumber = frame_size_c + frame_size_t - 1 + second_plane = omeMeta.getPlanePositionZ(0, next_plane_imagenumber) + z_interval = abs(abs(first_plane.value()) - abs(second_plane.value())) + print("z-interval seems to be: " + str(z_interval)) + + # create an image calibration + image_calibration = [physSizeX.value(), physSizeY.value(), z_interval] + calibration_unit = physSizeX.unit().getSymbol() + image_dimensions_czt = [frame_size_c, frame_size_z, frame_size_t] + + reader.close() + + for series in range(series_count): + if omeMeta.getImageName(series) == "macro image": + continue + + if series_count > 1 and not str(image).endswith(".vsi"): + series_names.append(omeMeta.getImageName(series)) + else: + series_names.append(str(image)) + # get the plane position in calibrated units + current_position_x = omeMeta.getPlanePositionX(series, 0) + current_position_y = omeMeta.getPlanePositionY(series, 0) + current_position_z = omeMeta.getPlanePositionZ(series, 0) + + physSizeX_max = ( + physSizeX.value() + if physSizeX.value() >= omeMeta.getPixelsPhysicalSizeX(series).value() + else omeMeta.getPixelsPhysicalSizeX(series).value() + ) + physSizeY_max = ( + physSizeY.value() + if physSizeY.value() >= omeMeta.getPixelsPhysicalSizeY(series).value() + else omeMeta.getPixelsPhysicalSizeY(series).value() + ) + if omeMeta.getPixelsPhysicalSizeZ(series): + physSizeZ_max = ( + physSizeZ.value() + if physSizeZ.value() + >= omeMeta.getPixelsPhysicalSizeZ(series).value() + else omeMeta.getPixelsPhysicalSizeZ(series).value() + ) + + else: + physSizeZ_max = 1.0 + + # get the absolute stage positions and store them + pos_x = current_position_x.value() + pos_y = current_position_y.value() + + if current_position_z is None: + print("the z-position is missing in the ome-xml metadata.") + pos_z = 1.0 + else: + pos_z = current_position_z.value() + + stage_coordinates_x.append(pos_x) + stage_coordinates_y.append(pos_y) + stage_coordinates_z.append(pos_z) + + max_size = [physSizeX_max, physSizeY_max, physSizeZ_max] + + # calculate the store the relative stage movements in px (for the grid/collection stitcher) + relative_coordinates_x_px = [] + relative_coordinates_y_px = [] + relative_coordinates_z_px = [] + + for i in range(len(stage_coordinates_x)): + rel_pos_x = ( + stage_coordinates_x[i] - stage_coordinates_x[0] + ) / physSizeX.value() + rel_pos_y = ( + stage_coordinates_y[i] - stage_coordinates_y[0] + ) / physSizeY.value() + rel_pos_z = (stage_coordinates_z[i] - stage_coordinates_z[0]) / z_interval + + relative_coordinates_x_px.append(rel_pos_x) + relative_coordinates_y_px.append(rel_pos_y) + relative_coordinates_z_px.append(rel_pos_z) + + return ( + dimensions, + stage_coordinates_x, + stage_coordinates_y, + stage_coordinates_z, + relative_coordinates_x_px, + relative_coordinates_y_px, + relative_coordinates_z_px, + image_calibration, + calibration_unit, + image_dimensions_czt, + series_names, + max_size, + ) From 0bdd98ec5bdc44eb6be66ad19905f6338eb3bbfc Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Thu, 13 Mar 2025 16:07:29 +0100 Subject: [PATCH 09/64] Update method to support NaN (discarding them) and rounding results --- src/imcflibs/imagej/misc.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index b0a0154c..363a8e8f 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -97,24 +97,31 @@ def percentage(part, whole): return 100 * float(part) / float(whole) -def calculate_mean_and_stdv(float_values): +def calculate_mean_and_stdv(values_list, round_decimals=0): """Calculate mean and standard deviation from a list of floats. Parameters ---------- - float_values : list of float - List containing float numbers. + values_list : list of int,float + List containing numbers. + round_decimals : int, optional + Rounding decimal to use for the result, by default 0 Returns ------- tuple of (float, float) Mean and standard deviation of the input list. """ - mean = sum(float_values) / len(float_values) + filtered_list = filter(None, values_list) + + try: + mean = round(sum(filtered_list) / len(filtered_list), round_decimals) + except ZeroDivisionError: + mean = 0 tot = 0.0 - for x in float_values: + for x in filtered_list: tot = tot + (x - mean) ** 2 - return [mean, (tot / (len(float_values))) ** 0.5] + return [mean, (tot / (len(filtered_list))) ** 0.5] def find_focus(imp): From 34d5880db0e8f268a69e4357fce4fc2f110369ef Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Thu, 13 Mar 2025 16:07:58 +0100 Subject: [PATCH 10/64] Add method to save in different file format This method could be improved to be used in the stitching script --- src/imcflibs/imagej/misc.py | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 363a8e8f..446bc4b8 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -445,3 +445,71 @@ def write_results(out_file, content): with open(out_file, "ab") as f: dict_writer = csv.DictWriter(f, content[0].keys(), delimiter=";") dict_writer.writerows(content) +def save_as(imageplus, extension, out_dir, series, pad_number, split_channels): + """Function to save an image + + Parameters + ---------- + imageplus : ImagePlus + ImagePlus to save + extension : str + Extension to use for the output + out_dir : str + Path for the output + series : int + Series to open + pad_number : int + Number of 0 to use for padding + split_channels : bool + Bool to split or not the channels + """ + + out_ext = {} + out_ext["ImageJ-TIF"] = ".tif" + out_ext["ICS-1"] = ".ids" + out_ext["ICS-2"] = ".ics" + out_ext["OME-TIFF"] = ".ome.tif" + out_ext["CellH5"] = ".ch5" + out_ext["BMP"] = ".bmp" + + imp_to_use = [] + dir_to_save = [] + + if split_channels: + for channel in range(1, imageplus.getNChannels() + 1): + imp_to_use.append( + Duplicator().run( + imageplus, + channel, + channel, + 1, + imageplus.getNSlices(), + 1, + imageplus.getNFrames(), + ) + ) + dir_to_save.append(os.path.join(out_dir, "C" + str(channel))) + else: + imp_to_use.append(imageplus) + dir_to_save.append(out_dir) + + for index, current_imp in enumerate(imp_to_use): + basename = imageplus.getShortTitle() + + out_path = os.path.join( + dir_to_save[index], basename + "_series_" + str(series).zfill(pad_number) + ) + + if extension == "ImageJ-TIF": + check_folder(dir_to_save[index]) + IJ.saveAs(current_imp, "Tiff", out_path + ".tif") + + elif extension == "BMP": + out_folder = os.path.join(out_dir, basename + os.path.sep) + check_folder(out_folder) + StackWriter.save(current_imp, out_folder, "format=bmp") + + else: + bf.export(current_imp, out_path + out_ext[extension]) + + current_imp.close() From 15bc0c4521fbaff3cc3be0b1ba52e5b94c366dd2 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Thu, 13 Mar 2025 16:10:17 +0100 Subject: [PATCH 11/64] Add method to pad a string --- src/imcflibs/imagej/misc.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 446bc4b8..0dffc03f 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -513,3 +513,26 @@ def save_as(imageplus, extension, out_dir, series, pad_number, split_channels): bf.export(current_imp, out_path + out_ext[extension]) current_imp.close() +def pad_number(index, pad_length=2): + """Pad a number with leading zeros to a specified length. + + Parameters + ---------- + index : int or str + The number to be padded + pad_length : int, optional + The total length of the resulting string after padding, by default 2 + + Returns + ------- + str + The padded number as a string + + Examples + -------- + >>> pad_number(7) + '07' + >>> pad_number(42, 4) + '0042' + """ + return str(index).zfill(pad_length) From 618908834280e4c813b69ae7885a0354c1f4edd2 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Thu, 13 Mar 2025 16:12:57 +0100 Subject: [PATCH 12/64] Add methods to find imaris and convert an image to IMS --- src/imcflibs/imagej/misc.py | 65 +++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 0dffc03f..07c7e1d2 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -536,3 +536,68 @@ def pad_number(index, pad_length=2): '0042' """ return str(index).zfill(pad_length) + + +def locate_latest_imaris(paths_to_check=None): + """Find paths to latest installed Imaris or ImarisFileConverter version. + + Parameters + ---------- + paths_to_check: list of str, optional + A list of paths that should be used to look for the installations, by default + `None` which will fall back to the standard installation locations of Bitplane. + + Returns + ------- + str + Full path to the most recent (as in "version number") ImarisFileConverter + or Imaris installation folder with the latter one having priority. + Will be empty if nothing is found. + """ + + if not paths_to_check: + paths_to_check = [ + r"C:\Program Files\Bitplane\ImarisFileConverter ", + r"C:\Program Files\Bitplane\Imaris ", + ] + + imaris_paths = [""] + + for check in paths_to_check: + hits = glob.glob(check + "*") + imaris_paths += sorted( + hits, key=lambda x: float(x.replace(check, "").replace(".", "")) + ) + + return imaris_paths[-1] + + +def convert_to_imaris(path_to_image): + """Convert a given file to Imaris5 .ims using ImarisConvert.exe via subprocess. + + Parameters + ---------- + path_to_image : str + Absolute path to the input image file. + + Notes + ----- + The function handles special case for .ids files by converting them to .ics before + processing. It uses the latest installed Imaris application to perform the conversion. + """ + + path_root, file_extension = os.path.splitext(path_to_image) + if file_extension == ".ids": + file_extension = ".ics" + path_to_image = path_root + file_extension + + imaris_path = locate_latest_imaris() + + command = 'ImarisConvert.exe -i "%s" -of Imaris5 -o "%s"' % ( + path_to_image, + path_to_image.replace(file_extension, ".ims"), + ) + print("\n%s" % command) + IJ.log("Converting to Imaris5 .ims...") + subprocess.call(command, shell=True, cwd=imaris_path) + IJ.log("Conversion to .ims is finished") From 272df1f0009d412a40fadea8f00b19a50ee5e2cd Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Thu, 13 Mar 2025 16:13:18 +0100 Subject: [PATCH 13/64] Add missing imports and formatting --- src/imcflibs/imagej/misc.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 07c7e1d2..396a54e3 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -1,16 +1,18 @@ """Miscellaneous ImageJ related functions, mostly convenience wrappers.""" +import csv +import os +import smtplib +import subprocess import sys import time -import smtplib -import os -import csv from ij import IJ # pylint: disable-msg=import-error -from ij.plugin import ImageCalculator +from ij.plugin import Duplicator, ImageCalculator, StackWriter -from . import prefs from ..log import LOG as log +from . import bioformats as bf +from . import prefs def show_status(msg): @@ -445,6 +447,8 @@ def write_results(out_file, content): with open(out_file, "ab") as f: dict_writer = csv.DictWriter(f, content[0].keys(), delimiter=";") dict_writer.writerows(content) + + def save_as(imageplus, extension, out_dir, series, pad_number, split_channels): """Function to save an image @@ -513,6 +517,8 @@ def save_as(imageplus, extension, out_dir, series, pad_number, split_channels): bf.export(current_imp, out_path + out_ext[extension]) current_imp.close() + + def pad_number(index, pad_length=2): """Pad a number with leading zeros to a specified length. From c1a933250bf1a40510d3c6526493e5438d5772f0 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Thu, 13 Mar 2025 16:13:54 +0100 Subject: [PATCH 14/64] Add methods to get different metadata from OMERO --- src/imcflibs/imagej/omerotools.py | 76 +++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index 8b5a3e07..0968021f 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -204,3 +204,79 @@ def find_dataset(client, dataset_id): """ # Fetch the dataset from the OMERO server using the provided dataset ID return client.getDataset(Long(dataset_id)) +def get_acquisition_metadata_from_imageid(user_client, image_wpr): + """Get acquisition metadata from OMERO based on an image ID + + Parameters + ---------- + user_client : fr.igred.omero.Client + Client used for login to OMERO + image_wpr : fr.igred.omero.repositor.ImageWrapper + Wrapper to the image for the ROIs + + Returns + ------- + tuple of (int, int, str, int) + List of info about the acquisition + """ + ctx = user_client.getCtx() + instrument_data = ( + user_client.getGateway() + .getMetadataService(ctx) + .loadInstrument(image_wpr.asDataObject().getInstrumentId()) + ) + objective_data = instrument_data.copyObjective().get(0) + if objective_data.getNominalMagnification() is None: + obj_mag = 0 + else: + obj_mag = objective_data.getNominalMagnification().getValue() + if objective_data.getLensNA() is None: + obj_na = 0 + else: + obj_na = objective_data.getLensNA().getValue() + if image_wpr.getAcquisitionDate() is None: + if image_wpr.asDataObject().getFormat() == "ZeissCZI": + field = "Information|Document|CreationDate" + date_field = get_info_from_original_metadata(user_client, image_wpr, field) + acq_date = date_field.split("T")[0] + acq_date_number = int(acq_date.replace("-", "")) + else: + acq_date = "NA" + acq_date_number = 0 + + else: + sdf = SimpleDateFormat("yyyy-MM-dd") + acq_date = sdf.format( + image_wpr.getAcquisitionDate() + ) # image_wpr.getAcquisitionDate() + acq_date_number = int(acq_date.replace("-", "")) + + return obj_mag, obj_na, acq_date, acq_date_number + + +def get_info_from_original_metadata(user_client, image_wpr, field): + """Recovers information from the original metadata + + In some cases, some information aren't parsed correctly by BF and have to + get recovered directly from the original metadata. This gets the value + based on the field string. + + Parameters + ---------- + user_client : fr.igred.omero.Client + Client used for login to OMERO + image_id : int + ID of the image to look. + field : str + Field to look for in the original metadata. Needs to be found beforehand. + + Returns + ------- + str + Value of the field + """ + omr = OriginalMetadataRequest(Long(image_wpr.getId())) + cmd = user_client.getGateway().submit(user_client.getCtx(), omr) + rsp = cmd.loop(5, 500) + gm = rsp.globalMetadata + return gm.get(field).getValue() From 05e6a2efa6d91bad6ba570b6a1f2163330d74f98 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Thu, 13 Mar 2025 16:14:08 +0100 Subject: [PATCH 15/64] Add method to delete annotation from OMERO --- src/imcflibs/imagej/omerotools.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index 0968021f..2fc83508 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -187,6 +187,21 @@ def add_annotation(client, repository_wpr, annotations, header): repository_wpr.addMapAnnotation(client, map_annotation_wpr) +def delete_annotation(user_client, repository_wpr): + """Delete annotations linked to object + + Parameters + ---------- + user_client : fr.igred.omero.Client + Client used for login to OMERO + repository_wpr : fr.igred.omero.repositor.GenericRepositoryObjectWrapper + Wrapper to the object for the anotation + + """ + kv_pairs = repository_wpr.getMapAnnotations(user_client) + user_client.delete(kv_pairs) + + def find_dataset(client, dataset_id): """Retrieve a dataset (wrapper) from the OMERO server. From fae0930d4efa08819bbda977c3ccb4d23ccc34dd Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Thu, 13 Mar 2025 16:14:25 +0100 Subject: [PATCH 16/64] Add methods to save an OMERO.table --- src/imcflibs/imagej/omerotools.py | 49 +++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index 2fc83508..f83414cd 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -295,3 +295,52 @@ def get_info_from_original_metadata(user_client, image_wpr, field): rsp = cmd.loop(5, 500) gm = rsp.globalMetadata return gm.get(field).getValue() + + +def create_table_columns(headings): + """Create the table headings from the ImageJ results table + + Parameters + ---------- + headings : list(str) + List of columns names + + Returns + ------- + list(omero.gateway.model.TableDataColumn) + List of columns formatted to be uploaded to OMERO + """ + table_columns = [] + # populate the headings + for h in range(len(headings)): + heading = headings.keys()[h] + type = headings.values()[h] + # OMERO.tables queries don't handle whitespace well + heading = heading.replace(" ", "_") + # title_heading = ["Slice", "Label"] + table_columns.append(TableDataColumn(heading, h, type)) + # table_columns.append(TableDataColumn("Image", size, ImageData)) + return table_columns + + +def upload_array_as_omero_table(user_client, table_title, data, columns, image_wpr): + """Upload a table to OMERO plus from a list of lists + + Parameters + ---------- + user_client : fr.igred.omero.Client + Client used for login to OMERO + data : list(list()) + List of lists of results to upload + columns : list(str) + List of columns names + image_wpr : fr.igred.omero.repositor.ImageWrapper + Wrapper to the image to be uploaded + """ + dataset_wpr = image_wpr.getDatasets(user_client)[0] + + table_columns = create_table_columns(columns) + table_data = TableData(table_columns, data) + table_wpr = TableWrapper(table_data) + table_wpr.setName(table_title) + dataset_wpr.addTable(user_client, table_wpr) From de5c84a009dc6c0b116a8bcff33a3cb52ce97e80 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Thu, 13 Mar 2025 16:14:37 +0100 Subject: [PATCH 17/64] Add method to save IJ-ROIs to OMERO --- src/imcflibs/imagej/omerotools.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index f83414cd..3ac17754 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -344,3 +344,24 @@ def upload_array_as_omero_table(user_client, table_title, data, columns, image_w table_wpr = TableWrapper(table_data) table_wpr.setName(table_title) dataset_wpr.addTable(user_client, table_wpr) + + +def save_rois_to_omero(user_client, image_wpr, rm): + """Save ROIs to OMERO linked to the image + + Parameters + ---------- + user_client : fr.igred.omero.Client + Client used for login to OMERO + image_wpr : fr.igred.omero.repositor.ImageWrapper + Wrapper to the image for the ROIs + rm : ij.plugin.frame.RoiManager + ROI Manager containing the ROIs + + """ + rois_list = rm.getRoisAsArray() + rois_arraylist = ArrayList(len(rois_list)) + for roi in rois_list: + rois_arraylist.add(roi) + rois_to_upload = ROIWrapper.fromImageJ(rois_arraylist) + image_wpr.saveROIs(user_client, rois_to_upload) From 16dd9616bea40112f14f6104aaf8de77beee4769 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Thu, 13 Mar 2025 16:16:37 +0100 Subject: [PATCH 18/64] Add missing imports and formatting --- src/imcflibs/imagej/omerotools.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index 3ac17754..dc8d51ce 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -8,11 +8,14 @@ [simple-omero-client]: https://github.com/GReD-Clermont/simple-omero-client """ -from java.lang import Long - - from fr.igred.omero import Client -from fr.igred.omero.annotations import MapAnnotationWrapper +from fr.igred.omero.annotations import MapAnnotationWrapper, TableWrapper +from fr.igred.omero.roi import ROIWrapper +from java.lang import Long +from java.text import SimpleDateFormat +from java.util import ArrayList +from omero.cmd import OriginalMetadataRequest +from omero.gateway.model import TableData, TableDataColumn def parse_url(client, omero_str): @@ -219,6 +222,8 @@ def find_dataset(client, dataset_id): """ # Fetch the dataset from the OMERO server using the provided dataset ID return client.getDataset(Long(dataset_id)) + + def get_acquisition_metadata_from_imageid(user_client, image_wpr): """Get acquisition metadata from OMERO based on an image ID From 04a9f847202e6efbadb96ebf47d7fc6b171d8c99 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Thu, 13 Mar 2025 16:16:46 +0100 Subject: [PATCH 19/64] Formatting --- src/imcflibs/pathtools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imcflibs/pathtools.py b/src/imcflibs/pathtools.py index 7053b2ae..db65e3a4 100644 --- a/src/imcflibs/pathtools.py +++ b/src/imcflibs/pathtools.py @@ -2,8 +2,8 @@ import os.path import platform -from os import sep import re +from os import sep from . import strtools from .log import LOG as log From 8a041dedd95e51e6ab936579dba81d14fd92b653 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Wed, 19 Mar 2025 09:42:52 +0100 Subject: [PATCH 20/64] Use `imp` instead of `imageplus` --- src/imcflibs/imagej/misc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 396a54e3..a0a0eb3c 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -454,7 +454,7 @@ def save_as(imageplus, extension, out_dir, series, pad_number, split_channels): Parameters ---------- - imageplus : ImagePlus + imp : ij.ImagePlus ImagePlus to save extension : str Extension to use for the output @@ -480,16 +480,16 @@ def save_as(imageplus, extension, out_dir, series, pad_number, split_channels): dir_to_save = [] if split_channels: - for channel in range(1, imageplus.getNChannels() + 1): + for channel in range(1, imp.getNChannels() + 1): imp_to_use.append( Duplicator().run( - imageplus, + imp, channel, channel, 1, - imageplus.getNSlices(), + imp.getNSlices(), 1, - imageplus.getNFrames(), + imp.getNFrames(), ) ) dir_to_save.append(os.path.join(out_dir, "C" + str(channel))) @@ -498,7 +498,7 @@ def save_as(imageplus, extension, out_dir, series, pad_number, split_channels): dir_to_save.append(out_dir) for index, current_imp in enumerate(imp_to_use): - basename = imageplus.getShortTitle() + basename = imp.getShortTitle() out_path = os.path.join( dir_to_save[index], basename + "_series_" + str(series).zfill(pad_number) From 309360fa98041b2ace1627f518ea120a327d3fe0 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Wed, 19 Mar 2025 09:43:14 +0100 Subject: [PATCH 21/64] Improve method name and docstring --- src/imcflibs/imagej/misc.py | 59 +++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index a0a0eb3c..20a11caf 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -449,23 +449,64 @@ def write_results(out_file, content): dict_writer.writerows(content) -def save_as(imageplus, extension, out_dir, series, pad_number, split_channels): - """Function to save an image +def save_image_with_extension( + imp, extension, out_dir, series, pad_number, split_channels +): + """Save an ImagePlus object in the specified format. + + This function provides flexible options for saving ImageJ images in various + formats with customizable naming conventions. It supports different + Bio-Formats compatible formats as well as ImageJ-native formats, and can + handle multi-channel images by either saving them as a single file or + splitting channels into separate files. + + The function automatically creates necessary directories and uses consistent + naming patterns with series numbers. For split channels, separate + subdirectories are created for each channel (C1, C2, etc.). Parameters ---------- imp : ij.ImagePlus - ImagePlus to save - extension : str - Extension to use for the output + ImagePlus object to save. + extension : {'ImageJ-TIF', 'ICS-1', 'ICS-2', 'OME-TIFF', 'CellH5', 'BMP'} + Output format to use: - ImageJ-TIF: Saves as ImageJ TIFF format (.tif) - + ICS-1: Saves as ICS version 1 format (.ids) - ICS-2: Saves as ICS + version 2 format (.ics) - OME-TIFF: Saves as OME-TIFF format (.ome.tif) + - CellH5: Saves as CellH5 format (.ch5) - BMP: Saves as BMP format (one + file per slice) out_dir : str - Path for the output + Directory path where the image(s) will be saved. series : int - Series to open + Series number to append to the filename. pad_number : int - Number of 0 to use for padding + Number of digits to use when zero-padding the series number. split_channels : bool - Bool to split or not the channels + If True, splits channels and saves them individually in separate folders + named "C1", "C2", etc. inside out_dir. If False, saves all channels in a + single file. + + Notes + ----- + - For "ImageJ-TIF" format, uses IJ.saveAs function + - For "BMP" format, saves images using StackWriter.save with one BMP file + per slice in a subfolder named after the original image + - For all other formats, uses Bio-Formats exporter (bf.export) + - The original ImagePlus is not modified, but any duplicate images created + for channel splitting are closed after saving + - Metadata is preserved when using Bio-Formats exporters + + Examples + -------- + Save a multichannel image as OME-TIFF without splitting channels: + + >>> save_image_with_extension(imp, "OME-TIFF", "/output/path", 1, 3, False) + # Saves as: /output/path/image_title_series_001.ome.tif + + Save with channels split: + + >>> save_image_with_extension(imp, "OME-TIFF", "/output/path", 1, 3, True) + # Saves as: /output/path/C1/image_title_series_001.ome.tif + # /output/path/C2/image_title_series_001.ome.tif """ out_ext = {} From 170ac0c9ddef95d1912e42a7b371de06d9ce18b4 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Wed, 19 Mar 2025 09:43:36 +0100 Subject: [PATCH 22/64] Improve method name and docstring --- src/imcflibs/imagej/misc.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 20a11caf..96c5f6b8 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -422,17 +422,41 @@ def get_threshold_value_from_method(imp, method, ops): return threshold_value -def write_results(out_file, content): - """ - Write the results to a csv file. +def write_ordereddict_to_csv(out_file, content): + """Write data from a list of OrderedDicts to a CSV file. + + This function writes data to a CSV file, preserving the order of columns + as defined in the OrderedDict objects. If the output file doesn't exist, + it creates a new file with a header row. If the file exists, it appends + the data without repeating the header. Parameters ---------- out_file : str - Path to the output file. + Path to the output CSV file. content : list of OrderedDict - List of dictionaries representing the results. + List of OrderedDict objects representing the data rows to be written. + All dictionaries should have the same keys. + Examples + -------- + >>> from collections import OrderedDict + >>> results = [ + ... OrderedDict([('id', 1), ('name', 'Sample A'), ('value', 42.5)]), + ... OrderedDict([('id', 2), ('name', 'Sample B'), ('value', 37.2)]) + ... ] + >>> write_ordereddict_to_csv('results.csv', results) + + The resulting CSV file will contain: + id;name;value + 1;Sample A;42.5 + 2;Sample B;37.2 + + Notes + ----- + - Uses semicolon (;) as delimiter + - When appending to an existing file, assumes the column structure matches + - Opens files in binary mode for compatibility """ # Check if the output file exists From cca38ba4d8955e87ed9e23bd83b0263f4c2e948a Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Wed, 19 Mar 2025 09:43:46 +0100 Subject: [PATCH 23/64] Add missing import --- src/imcflibs/imagej/misc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 96c5f6b8..47935fd9 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -1,6 +1,7 @@ """Miscellaneous ImageJ related functions, mostly convenience wrappers.""" import csv +import glob import os import smtplib import subprocess From e8589000916924af414c44cfc304f711beb99b8f Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Wed, 19 Mar 2025 09:44:35 +0100 Subject: [PATCH 24/64] Change method to use the one added to pathtools --- src/imcflibs/imagej/misc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 47935fd9..59445455 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -11,6 +11,7 @@ from ij import IJ # pylint: disable-msg=import-error from ij.plugin import Duplicator, ImageCalculator, StackWriter +from .. import pathtools from ..log import LOG as log from . import bioformats as bf from . import prefs @@ -571,12 +572,12 @@ def save_image_with_extension( ) if extension == "ImageJ-TIF": - check_folder(dir_to_save[index]) + pathtools.create_directory(dir_to_save[index]) IJ.saveAs(current_imp, "Tiff", out_path + ".tif") elif extension == "BMP": out_folder = os.path.join(out_dir, basename + os.path.sep) - check_folder(out_folder) + pathtools.create_directory(out_folder) StackWriter.save(current_imp, out_folder, "format=bmp") else: From b42cec32098b386a3569052333de335c4b42916c Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Wed, 19 Mar 2025 09:44:45 +0100 Subject: [PATCH 25/64] Formatting --- src/imcflibs/imagej/misc.py | 62 ++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 59445455..b5dfdc03 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -43,7 +43,9 @@ def show_progress(cur, final): ----- `ij.IJ.showProgress` internally increments the given `cur` value by 1. """ - log.info("Progress: %s / %s (%s)", cur + 1, final, (1.0 + cur) / final) + log.info( + "Progress: %s / %s (%s)", cur + 1, final, (1.0 + cur) / final + ) IJ.showProgress(cur, final) @@ -80,7 +82,9 @@ def elapsed_time_since(start, end=None): hours, rem = divmod(end - start, 3600) minutes, seconds = divmod(rem, 60) - return "{:0>2}:{:0>2}:{:05.2f}".format(int(hours), int(minutes), seconds) + return "{:0>2}:{:0>2}:{:05.2f}".format( + int(hours), int(minutes), seconds + ) def percentage(part, whole): @@ -119,7 +123,9 @@ def calculate_mean_and_stdv(values_list, round_decimals=0): filtered_list = filter(None, values_list) try: - mean = round(sum(filtered_list) / len(filtered_list), round_decimals) + mean = round( + sum(filtered_list) / len(filtered_list), round_decimals + ) except ZeroDivisionError: mean = 0 tot = 0.0 @@ -160,7 +166,9 @@ def find_focus(imp): # Check if more than 1 channel # FUTURE Could be improved for multi channel if imp_dimensions[2] != 1: - sys.exit("Image has more than one channel, please reduce dimensionality") + sys.exit( + "Image has more than one channel, please reduce dimensionality" + ) # Loop through each time point for plane in range(1, imp_dimensions[4] + 1): @@ -176,7 +184,9 @@ def find_focus(imp): # pix_array = pix_array*pix_array sumpix_array = sum(pix_array) - var = sumpix_array / (imp_dimensions[0] * imp_dimensions[1] * mean) + var = sumpix_array / ( + imp_dimensions[0] * imp_dimensions[1] * mean + ) if var > norm_var: norm_var = var @@ -205,10 +215,14 @@ def send_mail(job_name, recipient, filename, total_execution_time): # Ensure the sender and server are configured from Prefs if not sender: - log.info("Sender email is not configured. Please check IJ_Prefs.txt.") + log.info( + "Sender email is not configured. Please check IJ_Prefs.txt." + ) return if not server: - log.info("SMTP server is not configured. Please check IJ_Prefs.txt.") + log.info( + "SMTP server is not configured. Please check IJ_Prefs.txt." + ) return # Ensure the recipient is provided @@ -284,8 +298,18 @@ def timed_log(message, as_string=False): Message to print """ if as_string: - return time.strftime("%H:%M:%S", time.localtime()) + ": " + message + " " - IJ.log(time.strftime("%H:%M:%S", time.localtime()) + ": " + message + " ") + return ( + time.strftime("%H:%M:%S", time.localtime()) + + ": " + + message + + " " + ) + IJ.log( + time.strftime("%H:%M:%S", time.localtime()) + + ": " + + message + + " " + ) def get_free_memory(): @@ -465,13 +489,17 @@ def write_ordereddict_to_csv(out_file, content): if not os.path.exists(out_file): # If the file does not exist, create it and write the header with open(out_file, "wb") as f: - dict_writer = csv.DictWriter(f, content[0].keys(), delimiter=";") + dict_writer = csv.DictWriter( + f, content[0].keys(), delimiter=";" + ) dict_writer.writeheader() dict_writer.writerows(content) else: # If the file exists, append the results with open(out_file, "ab") as f: - dict_writer = csv.DictWriter(f, content[0].keys(), delimiter=";") + dict_writer = csv.DictWriter( + f, content[0].keys(), delimiter=";" + ) dict_writer.writerows(content) @@ -559,16 +587,19 @@ def save_image_with_extension( imp.getNFrames(), ) ) - dir_to_save.append(os.path.join(out_dir, "C" + str(channel))) + dir_to_save.append( + os.path.join(out_dir, "C" + str(channel)) + ) else: - imp_to_use.append(imageplus) + imp_to_use.append(imp) dir_to_save.append(out_dir) for index, current_imp in enumerate(imp_to_use): basename = imp.getShortTitle() out_path = os.path.join( - dir_to_save[index], basename + "_series_" + str(series).zfill(pad_number) + dir_to_save[index], + basename + "_series_" + str(series).zfill(pad_number), ) if extension == "ImageJ-TIF": @@ -639,7 +670,8 @@ def locate_latest_imaris(paths_to_check=None): for check in paths_to_check: hits = glob.glob(check + "*") imaris_paths += sorted( - hits, key=lambda x: float(x.replace(check, "").replace(".", "")) + hits, + key=lambda x: float(x.replace(check, "").replace(".", "")), ) return imaris_paths[-1] From 184a9438ff176d538cce5006aa1aab2e40ce0ba7 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Wed, 19 Mar 2025 10:43:32 +0100 Subject: [PATCH 26/64] Update the formatting --- src/imcflibs/imagej/misc.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index b5dfdc03..a6f9fc3e 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -523,11 +523,13 @@ def save_image_with_extension( imp : ij.ImagePlus ImagePlus object to save. extension : {'ImageJ-TIF', 'ICS-1', 'ICS-2', 'OME-TIFF', 'CellH5', 'BMP'} - Output format to use: - ImageJ-TIF: Saves as ImageJ TIFF format (.tif) - - ICS-1: Saves as ICS version 1 format (.ids) - ICS-2: Saves as ICS - version 2 format (.ics) - OME-TIFF: Saves as OME-TIFF format (.ome.tif) - - CellH5: Saves as CellH5 format (.ch5) - BMP: Saves as BMP format (one - file per slice) + Output format to use: + - ImageJ-TIF: Saves as ImageJ TIFF format (.tif) + - ICS-1: Saves as ICS version 1 format (.ids) + - ICS-2: Saves as ICS version 2 format (.ics) + - OME-TIFF: Saves as OME-TIFF format (.ome.tif) + - CellH5: Saves as CellH5 format (.ch5) + - BMP: Saves as BMP format (one file per slice) out_dir : str Directory path where the image(s) will be saved. series : int From 54ee5f9d76f3246f0c928c7fa5b0956ebdcd0b83 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Wed, 19 Mar 2025 14:55:54 +0100 Subject: [PATCH 27/64] Format docstring's Returns `dict` That's an attempt to use a format that would render the dict description in the docstring in a way so it's easily readable in VS Code as well as through the pdoc HTML. --- src/imcflibs/imagej/bioformats.py | 33 ++++++++++++------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/imcflibs/imagej/bioformats.py b/src/imcflibs/imagej/bioformats.py index 0eced928..83d19728 100644 --- a/src/imcflibs/imagej/bioformats.py +++ b/src/imcflibs/imagej/bioformats.py @@ -307,26 +307,19 @@ def get_metadata_from_image(path_to_image): ------- dict A dictionary containing the following metadata: - - unit_width : float - Physical width of a pixel. - - unit_height : float - Physical height of a pixel. - - unit_depth : float - Physical depth of a voxel. - - pixel_width : int - Width of the image in pixels. - - pixel_height : int - Height of the image in pixels. - - slice_count : int - Number of Z-slices. - - channel_count : int - Number of channels. - - timepoints_count : int - Number of timepoints. - - dimension_order : str - Order of dimensions in the image (e.g., 'XYZCT'). - - pixel_type : str - Data type of the pixel values. + + { + unit_width : float, # physical width of a pixel + unit_height : float, # physical height of a pixel + unit_depth : float, # physical depth of a voxel + pixel_width : int, # width of the image in pixels + pixel_height : int, # height of the image in pixels + slice_count : int, # number of Z-slices + channel_count : int, # number of channels + timepoints_count : int, # number of timepoints + dimension_order : str, # order of dimensions, e.g. "XYZCT" + pixel_type : str, # data type of the pixel values + } """ reader = ImageReader() ome_meta = MetadataTools.createOMEXMLMetadata() From 88ee3a864e2217f2ad3c429cc180c7420e6fa33b Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Wed, 19 Mar 2025 14:58:47 +0100 Subject: [PATCH 28/64] Add back `create_directory` with better docstring --- src/imcflibs/pathtools.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/imcflibs/pathtools.py b/src/imcflibs/pathtools.py index a5fba12e..956c8997 100644 --- a/src/imcflibs/pathtools.py +++ b/src/imcflibs/pathtools.py @@ -179,7 +179,9 @@ def jython_fiji_exists(path): return False -def listdir_matching(path, suffix, fullpath=False, sort=False, regex=False): +def listdir_matching( + path, suffix, fullpath=False, sort=False, regex=False +): """Get a list of files in a directory matching a given suffix. Parameters @@ -295,9 +297,13 @@ def derive_out_dir(in_dir, out_dir): """ if out_dir.upper() in ["-", "NONE"]: out_dir = in_dir - log.info("No output directory given, using input dir [%s].", out_dir) + log.info( + "No output directory given, using input dir [%s].", out_dir + ) else: - log.info("Using directory [%s] for results and temp files.", out_dir) + log.info( + "Using directory [%s] for results and temp files.", out_dir + ) return out_dir @@ -357,6 +363,27 @@ def folder_size(source): return total_size +def create_directory(new_path): + """Create a new directory at the specified path. + + This function first checks if the directory already exists and only + attempts to create it if it doesn't exist. + + Parameters + ---------- + new_path : str + Path where the new directory should be created. + + Notes + ----- + This approach is used as a workaround for Python 2.7 which doesn't + have the exist_ok' parameter in os.makedirs(). + """ + + if not os.path.exists(new_path): + os.makedirs(new_path) + + # pylint: disable-msg=C0103 # we use the variable name 'exists' in its common spelling (lowercase), so # removing this workaround will be straightforward at a later point From 74668385346fb6619d8c8af0272fa930ce65d3c1 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Wed, 19 Mar 2025 15:02:15 +0100 Subject: [PATCH 29/64] Fix linting --- src/imcflibs/imagej/bdv.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/imcflibs/imagej/bdv.py b/src/imcflibs/imagej/bdv.py index 7dfa1325..4d7f6825 100644 --- a/src/imcflibs/imagej/bdv.py +++ b/src/imcflibs/imagej/bdv.py @@ -199,7 +199,7 @@ def process_angle(self, value, range_end=None): Contains the end of the range, by default None. Notes: - ------ + ----- Previous function name : angle_select(). """ @@ -225,7 +225,7 @@ def process_channel(self, value, range_end=None): Contains the end of the range, by default None. Notes: - ------ + ----- Previous function name : channel_select(). """ @@ -251,7 +251,7 @@ def process_illumination(self, value, range_end=None): Contains the end of the range, by default None. Notes: - ------ + ----- Previous function name : illumination_select(). """ @@ -277,7 +277,7 @@ def process_tile(self, value, range_end=None): Contains the end of the range, by default None. Notes: - ------ + ----- Previous function name : tile_select(). """ @@ -303,7 +303,7 @@ def process_timepoint(self, value, range_end=None): Contains the end of the range, by default None. Notes: - ------ + ----- Previous function name : timepoint_select(). """ From 5bea0adc37583eeef84e8074211361223db2d503 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Wed, 19 Mar 2025 15:02:27 +0100 Subject: [PATCH 30/64] Formatting --- src/imcflibs/imagej/bdv.py | 161 ++++++++++++++++++++++++++++--------- 1 file changed, 121 insertions(+), 40 deletions(-) diff --git a/src/imcflibs/imagej/bdv.py b/src/imcflibs/imagej/bdv.py index 4d7f6825..b3d0f2d2 100644 --- a/src/imcflibs/imagej/bdv.py +++ b/src/imcflibs/imagej/bdv.py @@ -130,7 +130,9 @@ def reference_channel(self, value): """ # channel = int(value) - 1 # will raise a ValueError if cast fails self._use_channel = "channels=[use Channel %s]" % int(value) - log.debug("New reference channel setting: %s", self._use_channel) + log.debug( + "New reference channel setting: %s", self._use_channel + ) def reference_illumination(self, value): """Set the reference illumination when using *Expert Grouping Options*. @@ -146,8 +148,13 @@ def reference_illumination(self, value): value : int or int-like The illumination number to use for the grouping. """ - self._use_illumination = "illuminations=[use Illumination %s]" % value - log.debug("New reference illumination setting: %s", self._use_illumination) + self._use_illumination = ( + "illuminations=[use Illumination %s]" % value + ) + log.debug( + "New reference illumination setting: %s", + self._use_illumination, + ) def reference_tile(self, value): """Set the reference tile when using *Expert Grouping Options*. @@ -181,7 +188,9 @@ def reference_timepoint(self, value): The timepoint number to use for the grouping. """ self._use_timepoint = "timepoints=[use Timepoint %s]" % value - log.debug("New reference timepoint setting: %s", self._use_timepoint) + log.debug( + "New reference timepoint setting: %s", self._use_timepoint + ) ### process-X methods @@ -403,16 +412,22 @@ def fmt_acitt_options(self, input="process"): """ input_type = ["process", "resave"] if input not in input_type: - raise ValueError("Invalue input type. Expected one of: %s" % input_type) + raise ValueError( + "Invalue input type. Expected one of: %s" % input_type + ) parameters = [ input + "_angle=" + self._angle_processing_option, input + "_channel=" + self._channel_processing_option, - input + "_illumination=" + self._illumination_processing_option, + input + + "_illumination=" + + self._illumination_processing_option, input + "_tile=" + self._tile_processing_option, input + "_timepoint=" + self._timepoint_processing_option, ] parameter_string = " ".join(parameters).strip() - log.debug("Formatted 'process_X' options: <%s>", parameter_string) + log.debug( + "Formatted 'process_X' options: <%s>", parameter_string + ) return parameter_string + " " def fmt_acitt_selectors(self): @@ -432,12 +447,16 @@ def fmt_acitt_selectors(self): parameters = [ self._angle_select if self._angle_select else "", self._channel_select if self._channel_select else "", - self._illumination_select if self._illumination_select else "", + self._illumination_select + if self._illumination_select + else "", self._tile_select if self._tile_select else "", self._timepoint_select if self._timepoint_select else "", ] parameter_string = " ".join(parameters).strip() - log.debug("Formatted 'processing_X' selectors: <%s>", parameter_string) + log.debug( + "Formatted 'processing_X' selectors: <%s>", parameter_string + ) return parameter_string + " " def fmt_how_to_treat(self): @@ -455,7 +474,9 @@ def fmt_how_to_treat(self): "how_to_treat_timepoints=" + self._treat_timepoints, ] parameter_string = " ".join(parameters).strip() - log.debug("Formatted 'how_to_treat_X' options: <%s>", parameter_string) + log.debug( + "Formatted 'how_to_treat_X' options: <%s>", parameter_string + ) return parameter_string + " " def fmt_use_acitt(self): @@ -470,13 +491,22 @@ def fmt_use_acitt(self): """ parameters = [ self._use_angle if self._treat_angles == "group" else "", - self._use_channel if self._treat_channels == "group" else "", - self._use_illumination if self._treat_illuminations == "group" else "", + self._use_channel + if self._treat_channels == "group" + else "", + self._use_illumination + if self._treat_illuminations == "group" + else "", self._use_tile if self._treat_tiles == "group" else "", - self._use_timepoint if self._treat_timepoints == "group" else "", + self._use_timepoint + if self._treat_timepoints == "group" + else "", ] parameter_string = " ".join(parameters).strip() - log.debug("Formatted expert grouping 'use' options: <%s>", parameter_string) + log.debug( + "Formatted expert grouping 'use' options: <%s>", + parameter_string, + ) return parameter_string + " " @@ -514,7 +544,9 @@ class DefinitionOptions(object): def __init__(self): self._angle_definition = SINGLE_FILE % "angle" self._channel_definition = MULTI_SINGLE_FILE % "channel" - self._illumination_definition = SINGLE_FILE % "illumination direction" + self._illumination_definition = ( + SINGLE_FILE % "illumination direction" + ) self._tile_definition = MULTI_MULTI_FILE % "tile" self._timepoint_definition = SINGLE_FILE % "time-point" @@ -535,7 +567,9 @@ def check_definition_option(self, value): "multi_single", "multi_multi", ]: - raise ValueError("Value must be one of single, multi_multi or multi_single") + raise ValueError( + "Value must be one of single, multi_multi or multi_single" + ) return { "single": SINGLE_FILE, @@ -553,7 +587,9 @@ def set_angle_definition(self, value): """ choices = self.check_definition_option(value) self._angle_definition = choices[value] % "angle" - log.debug("New 'angle_definition' setting: %s", self._angle_definition) + log.debug( + "New 'angle_definition' setting: %s", self._angle_definition + ) def set_channel_definition(self, value): """Set the value for the channel definition @@ -565,7 +601,10 @@ def set_channel_definition(self, value): """ choices = self.check_definition_option(value) self._channel_definition = choices[value] % "channel" - log.debug("New 'channel_definition' setting: %s", self._channel_definition) + log.debug( + "New 'channel_definition' setting: %s", + self._channel_definition, + ) def set_illumination_definition(self, value): """Set the value for the illumination definition @@ -576,9 +615,12 @@ def set_illumination_definition(self, value): One of `single`, `multi_single` or `multi_multi`. """ choices = self.check_definition_option(value) - self._illumination_definition = choices[value] % "illumination direction" + self._illumination_definition = ( + choices[value] % "illumination direction" + ) log.debug( - "New 'illumination_definition' setting: %s", self._illumination_definition + "New 'illumination_definition' setting: %s", + self._illumination_definition, ) def set_tile_definition(self, value): @@ -591,7 +633,9 @@ def set_tile_definition(self, value): """ choices = self.check_definition_option(value) self._tile_definition = choices[value] % "tile" - log.debug("New 'tile_definition' setting: %s", self._tile_definition) + log.debug( + "New 'tile_definition' setting: %s", self._tile_definition + ) def set_timepoint_definition(self, value): """Set the value for the time_point_definition @@ -603,7 +647,10 @@ def set_timepoint_definition(self, value): """ choices = self.check_definition_option(value) self._timepoint_definition = choices[value] % "time-point" - log.debug("New 'timepoint_definition' setting: %s", self._timepoint_definition) + log.debug( + "New 'timepoint_definition' setting: %s", + self._timepoint_definition, + ) def fmt_acitt_options(self): """Format Angle / Channel / Illumination / Tile / Timepoint options. @@ -624,7 +671,9 @@ def fmt_acitt_options(self): "multiple_timepoints=" + self._timepoint_definition, ] parameter_string = " ".join(parameters).strip() - log.debug("Formatted 'multiple_X' options: <%s>", parameter_string) + log.debug( + "Formatted 'multiple_X' options: <%s>", parameter_string + ) return parameter_string + " " @@ -648,10 +697,14 @@ def check_processing_input(value, range_end): value = [value] # Check if all the elements of the value list are of the same type if not all(isinstance(x, type(value[0])) for x in value): - raise TypeError("Invalid input type. All the values should be of the same type") + raise TypeError( + "Invalid input type. All the values should be of the same type" + ) if type(range_end) is int: if type(value[0]) is not int: - raise TypeError("Invalid input type. Expected an int for the range start") + raise TypeError( + "Invalid input type. Expected an int for the range start" + ) elif len(value) != 1: raise ValueError( "Invalid input type. Expected a single number for the range start" @@ -689,7 +742,13 @@ def get_processing_settings(dimension, selection, value, range_end): if selection == "single": processing_option = SINGLE % dimension - dimension_select = "processing_" + dimension + "=[" + dimension + " %s]" % value + dimension_select = ( + "processing_" + + dimension + + "=[" + + dimension + + " %s]" % value + ) if selection == "multiple": processing_option = MULTIPLE % dimension @@ -732,7 +791,9 @@ def backup_xml_files(source_directory, subfolder_name): pathtools.create_directory(xml_backup_directory) backup_subfolder = xml_backup_directory + "/%s" % (subfolder_name) pathtools.create_directory(backup_subfolder) - all_xml_files = pathtools.listdir_matching(source_directory, ".*\\.xml", regex=True) + all_xml_files = pathtools.listdir_matching( + source_directory, ".*\\.xml", regex=True + ) os.chdir(source_directory) for xml_file in all_xml_files: shutil.copy2(xml_file, backup_subfolder) @@ -792,7 +853,9 @@ def define_dataset_auto( dataset_save_path = result_folder if subsampling_factors: subsampling_factors = ( - "manual_mipmap_setup subsampling_factors=" + subsampling_factors + " " + "manual_mipmap_setup subsampling_factors=" + + subsampling_factors + + " " ) else: subsampling_factors = "" @@ -959,7 +1022,9 @@ def resave_as_h5( split_hdf5 = "" if subsampling_factors: - subsampling_factors = "subsampling_factors=" + subsampling_factors + " " + subsampling_factors = ( + "subsampling_factors=" + subsampling_factors + " " + ) else: subsampling_factors = " " if hdf5_chunk_sizes: @@ -1049,10 +1114,13 @@ def phase_correlation_pairwise_shifts_calculation( file_info = pathtools.parse_path(project_path) if downsampling_xyz != "": - downsampling = "downsample_in_x=%s downsample_in_y=%s downsample_in_z=%s " % ( - downsampling_xyz[0], - downsampling_xyz[1], - downsampling_xyz[2], + downsampling = ( + "downsample_in_x=%s downsample_in_y=%s downsample_in_z=%s " + % ( + downsampling_xyz[0], + downsampling_xyz[1], + downsampling_xyz[2], + ) ) else: downsampling = "" @@ -1076,7 +1144,9 @@ def phase_correlation_pairwise_shifts_calculation( log.debug("Calculate pairwise shifts options: <%s>", options) IJ.run("Calculate pairwise shifts ...", str(options)) - backup_xml_files(file_info["path"], "phase_correlation_shift_calculation") + backup_xml_files( + file_info["path"], "phase_correlation_shift_calculation" + ) def filter_pairwise_shifts( @@ -1188,7 +1258,9 @@ def optimize_and_apply_shifts( + processing_opts.fmt_how_to_treat() ) - log.debug("Optimization and shifts application options: <%s>", options) + log.debug( + "Optimization and shifts application options: <%s>", options + ) IJ.run("Optimize globally and apply shifts ...", str(options)) backup_xml_files(file_info["path"], "optimize_and_apply_shifts") @@ -1366,8 +1438,12 @@ def duplicate_transformations( target = "[All Channels]" source = str(channel_source - 1) if tile_source: - tile_apply = "apply_to_tile=[Single tile (Select from List)] " - tile_process = "processing_tile=[tile " + str(tile_source) + "] " + tile_apply = ( + "apply_to_tile=[Single tile (Select from List)] " + ) + tile_process = ( + "processing_tile=[tile " + str(tile_source) + "] " + ) else: tile_apply = "apply_to_tile=[All tiles] " elif transformation_type == "tile": @@ -1375,9 +1451,13 @@ def duplicate_transformations( target = "[All Tiles]" source = str(tile_source) if channel_source: - chnl_apply = "apply_to_channel=[Single channel (Select from List)] " + chnl_apply = ( + "apply_to_channel=[Single channel (Select from List)] " + ) chnl_process = ( - "processing_channel=[channel " + str(channel_source - 1) + "] " + "processing_channel=[channel " + + str(channel_source - 1) + + "] " ) else: chnl_apply = "apply_to_channel=[All channels] " @@ -1413,7 +1493,8 @@ def duplicate_transformations( IJ.run("Duplicate Transformations", str(options)) backup_xml_files( - file_info["path"], "duplicate_transformation_" + transformation_type + file_info["path"], + "duplicate_transformation_" + transformation_type, ) From 0e81c37d5d3d3902b8c5211efde931a21f3d162f Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Wed, 19 Mar 2025 15:09:01 +0100 Subject: [PATCH 31/64] Fix linting issues --- src/imcflibs/imagej/bdv.py | 52 +++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/imcflibs/imagej/bdv.py b/src/imcflibs/imagej/bdv.py index b3d0f2d2..0f1a42a4 100644 --- a/src/imcflibs/imagej/bdv.py +++ b/src/imcflibs/imagej/bdv.py @@ -207,7 +207,7 @@ def process_angle(self, value, range_end=None): range_end : int, optional Contains the end of the range, by default None. - Notes: + Notes ----- Previous function name : angle_select(). """ @@ -233,7 +233,7 @@ def process_channel(self, value, range_end=None): range_end : int, optional Contains the end of the range, by default None. - Notes: + Notes ----- Previous function name : channel_select(). """ @@ -259,7 +259,7 @@ def process_illumination(self, value, range_end=None): range_end : int, optional Contains the end of the range, by default None. - Notes: + Notes ----- Previous function name : illumination_select(). """ @@ -285,7 +285,7 @@ def process_tile(self, value, range_end=None): range_end : int, optional Contains the end of the range, by default None. - Notes: + Notes ----- Previous function name : tile_select(). """ @@ -311,7 +311,7 @@ def process_timepoint(self, value, range_end=None): range_end : int, optional Contains the end of the range, by default None. - Notes: + Notes ----- Previous function name : timepoint_select(). """ @@ -578,7 +578,7 @@ def check_definition_option(self, value): } def set_angle_definition(self, value): - """Set the value for the angle definition + """Set the value for the angle definition. Parameters ---------- @@ -592,7 +592,7 @@ def set_angle_definition(self, value): ) def set_channel_definition(self, value): - """Set the value for the channel definition + """Set the value for the channel definition. Parameters ---------- @@ -607,7 +607,7 @@ def set_channel_definition(self, value): ) def set_illumination_definition(self, value): - """Set the value for the illumination definition + """Set the value for the illumination definition. Parameters ---------- @@ -624,7 +624,7 @@ def set_illumination_definition(self, value): ) def set_tile_definition(self, value): - """Set the value for the tile_definition + """Set the value for the tile_definition. Parameters ---------- @@ -638,7 +638,7 @@ def set_tile_definition(self, value): ) def set_timepoint_definition(self, value): - """Set the value for the time_point_definition + """Set the value for the time_point_definition. Parameters ---------- @@ -688,6 +688,7 @@ def check_processing_input(value, range_end): Contains the list of input dimensions, the first input dimension of a range or a single channel range_end : int or None Contains the end of the range if need be + Returns ------- str @@ -809,8 +810,8 @@ def define_dataset_auto( subsampling_factors=None, hdf5_chunk_sizes=None, ): - """Will run the corresponding "Define Dataset" using the "Auto-Loader" - option. + """Define a dataset using the Autoloader or Multi-View loader. + If the series is tiles, will run "Define Dataset...", otherwise will run "Define Multi-View Dataset...". @@ -993,8 +994,10 @@ def resave_as_h5( XML input file. output_h5_file_path : str Export path for the output file including the `.xml `extension. - timepoints : str, optional - The timepoints that should be exported, by default `All Timepoints`. + processing_opts : imcflibs.imagej.bdv.ProcessingOptions, optional + The `ProcessingOptions` object defining parameters for the run. Will + fall back to the defaults defined in the corresponding class if the + parameter is `None` or skipped. timepoints_per_partition : int, optional How many timepoints to export per partition, by default `1`. use_deflate_compression : bool, optional @@ -1279,10 +1282,10 @@ def detect_interest_points( ---------- project_path : str Path to the `.xml` project. - process_timepoint : str, optional - Timepoint to be processed, by default `All Timepoints`. - process_channel : str, optional - Channel to be processed, by default `All channels`. + processing_opts : imcflibs.imagej.bdv.ProcessingOptions, optional + The `ProcessingOptions` object defining parameters for the run. Will + fall back to the defaults defined in the corresponding class if the + parameter is `None` or skipped. sigma : float, optional Minimum sigma for interest points detection, by default `1.8`. threshold : float, optional @@ -1337,14 +1340,11 @@ def interest_points_registration( ---------- project_path : str Path to the `.xml` project. - process_timepoint : str, optional - Timepoint to be processed, by default `All Timepoints`. - process_channel : str, optional - Channels to be used for performing the registration. By default, all - channels are taken into account, however this behavior could be - undesirable if only one channel is adequate (e.g. beads or other useful - fiducials). To restrict registration to a specific channel, provide the - channel name using this parameter. By default `All channels`. + processing_opts : imcflibs.imagej.bdv.ProcessingOptions, optional + The `ProcessingOptions` object defining parameters for the run. Will + fall back to the defaults defined in the corresponding class if the + parameter is `None` or skipped. This controls which angles, channels, + illuminations, tiles and timepoints are processed. rigid_timepoints : bool, optional If set to `True` each timepoint will be considered as a rigid unit (useful e.g. if spatial registration has already been performed before). From 1b222d019d928db7f4a64f1bc7f658217e449258 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Wed, 19 Mar 2025 18:02:21 +0100 Subject: [PATCH 32/64] Update docstring of write_ordereddict_to_csv() By indenting the example content block it renders nicely in VS Code and pdoc, plus it's also fairly readable in plain-text. --- src/imcflibs/imagej/misc.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index d5558eab..0a62d925 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -470,16 +470,17 @@ def write_ordereddict_to_csv(out_file, content): ... ] >>> write_ordereddict_to_csv('results.csv', results) - The resulting CSV file will contain: - id;name;value - 1;Sample A;42.5 - 2;Sample B;37.2 + The resulting CSV file will have the following content: + + id;name;value + 1;Sample A;42.5 + 2;Sample B;37.2 Notes ----- - - Uses semicolon (;) as delimiter - - When appending to an existing file, assumes the column structure matches - - Opens files in binary mode for compatibility + - Uses the semicolon charachter (`;`) as delimiter. + - When appending to an existing file, the column structure has to match. + - Output file is opened in binary mode for compatibility. """ # Check if the output file exists From f9375c5b98251e9998912f164819ce3a9a9621ca Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Wed, 19 Mar 2025 21:28:02 +0100 Subject: [PATCH 33/64] Rework docstring --- src/imcflibs/imagej/misc.py | 39 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 0a62d925..f67b1e9b 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -521,13 +521,7 @@ def save_image_with_extension( imp : ij.ImagePlus ImagePlus object to save. extension : {'ImageJ-TIF', 'ICS-1', 'ICS-2', 'OME-TIFF', 'CellH5', 'BMP'} - Output format to use: - - ImageJ-TIF: Saves as ImageJ TIFF format (.tif) - - ICS-1: Saves as ICS version 1 format (.ids) - - ICS-2: Saves as ICS version 2 format (.ics) - - OME-TIFF: Saves as OME-TIFF format (.ome.tif) - - CellH5: Saves as CellH5 format (.ch5) - - BMP: Saves as BMP format (one file per slice) + Output format to use, see Notes section below for details. out_dir : str Directory path where the image(s) will be saved. series : int @@ -535,32 +529,37 @@ def save_image_with_extension( pad_number : int Number of digits to use when zero-padding the series number. split_channels : bool - If True, splits channels and saves them individually in separate folders - named "C1", "C2", etc. inside out_dir. If False, saves all channels in a + If True, split channels and save them individually in separate folders + named "C1", "C2", etc. inside out_dir. If False, save all channels in a single file. Notes ----- - - For "ImageJ-TIF" format, uses IJ.saveAs function - - For "BMP" format, saves images using StackWriter.save with one BMP file - per slice in a subfolder named after the original image - - For all other formats, uses Bio-Formats exporter (bf.export) - - The original ImagePlus is not modified, but any duplicate images created - for channel splitting are closed after saving - - Metadata is preserved when using Bio-Formats exporters + Depending on the value of the `extension` parameter, one of the following + output formats and saving strategies will be used: + - Bio-Formats based formats will be produced by calling `bf.export()`, note + that these formats will preserve metadata (which is **not** the case for + the other formats using different saving strategies): + - `ICS-1`: Save as ICS version 1 format (a pair of `.ics` and `.ids`). + - `ICS-2`: Save as ICS version 2 format (single `.ics` file). + - `OME-TIFF`: Save in OME-TIFF format (`.ome.tif`). + - `CellH5`: Save as CellH5 format (`.ch5`). + - `ImageJ-TIF`: Save in ImageJ TIFF format (`.tif`) using `IJ.saveAs()`. + - `BMP`: Save in BMP format using `StackWriter.save()`, producing one `.bmp` + per slice in a subfolder named after the original image. Examples -------- Save a multichannel image as OME-TIFF without splitting channels: >>> save_image_with_extension(imp, "OME-TIFF", "/output/path", 1, 3, False) - # Saves as: /output/path/image_title_series_001.ome.tif + # resulting file: /output/path/image_title_series_001.ome.tif - Save with channels split: + Save with channel splitting: >>> save_image_with_extension(imp, "OME-TIFF", "/output/path", 1, 3, True) - # Saves as: /output/path/C1/image_title_series_001.ome.tif - # /output/path/C2/image_title_series_001.ome.tif + # resulting files: /output/path/C1/image_title_series_001.ome.tif + # /output/path/C2/image_title_series_001.ome.tif """ out_ext = {} From e99dcdb3a67461922ceea6c2e7e89a7c46b934d2 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Wed, 19 Mar 2025 21:59:31 +0100 Subject: [PATCH 34/64] Update docstring of locate_latest_imaris() Details of the functionality are preferably explained in the first section of the docstring, as this is what another person will read first. --- src/imcflibs/imagej/misc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index f67b1e9b..3127120b 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -644,6 +644,10 @@ def pad_number(index, pad_length=2): def locate_latest_imaris(paths_to_check=None): """Find paths to latest installed Imaris or ImarisFileConverter version. + Identify the full path to the most recent (as in "version number") + ImarisFileConverter or Imaris installation folder with the latter one having + priority. In case nothing is found, an empty string is returned. + Parameters ---------- paths_to_check: list of str, optional @@ -653,11 +657,7 @@ def locate_latest_imaris(paths_to_check=None): Returns ------- str - Full path to the most recent (as in "version number") ImarisFileConverter - or Imaris installation folder with the latter one having priority. - Will be empty if nothing is found. """ - if not paths_to_check: paths_to_check = [ r"C:\Program Files\Bitplane\ImarisFileConverter ", From d2a49913d11a7f515fde4458c889039bb14f7fea Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Wed, 19 Mar 2025 22:41:25 +0100 Subject: [PATCH 35/64] Improve docstring --- src/imcflibs/imagej/omerotools.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index f41580a7..ec2cf5d1 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -275,11 +275,11 @@ def get_acquisition_metadata_from_imageid(user_client, image_wpr): def get_info_from_original_metadata(user_client, image_wpr, field): - """Recovers information from the original metadata + """Retrieve information from the original metadata (as opposed to OME-MD). - In some cases, some information aren't parsed correctly by BF and have to - get recovered directly from the original metadata. This gets the value - based on the field string. + In some cases not all information is parsed correctly by BF and has to be + recovered / identified directly from the *original* metadata. This function + extracts the corresponding value based on the field identifier. Parameters ---------- From 6eb32db147cc1ec786a2492e805332b94b2a7e0b Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Wed, 19 Mar 2025 22:45:53 +0100 Subject: [PATCH 36/64] Improve docstring --- src/imcflibs/imagej/omerotools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index ec2cf5d1..824d1c6a 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -303,17 +303,17 @@ def get_info_from_original_metadata(user_client, image_wpr, field): def create_table_columns(headings): - """Create the table headings from the ImageJ results table + """Create OMERO table headings from an ImageJ results table. Parameters ---------- headings : list(str) - List of columns names + List of columns names. Returns ------- list(omero.gateway.model.TableDataColumn) - List of columns formatted to be uploaded to OMERO + List of columns formatted to be uploaded to OMERO. """ table_columns = [] # populate the headings From bfbe58599258d631d4d19e29a7a2164a5776c8ec Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Wed, 19 Mar 2025 23:00:38 +0100 Subject: [PATCH 37/64] Shorten docstring --- src/imcflibs/pathtools.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/imcflibs/pathtools.py b/src/imcflibs/pathtools.py index 956c8997..bd8965e5 100644 --- a/src/imcflibs/pathtools.py +++ b/src/imcflibs/pathtools.py @@ -366,18 +366,13 @@ def folder_size(source): def create_directory(new_path): """Create a new directory at the specified path. - This function first checks if the directory already exists and only - attempts to create it if it doesn't exist. + This is a workaround for Python 2.7 where `os.makedirs()` is lacking + the `exist_ok` parameter that is present in Python 3.2 and newer. Parameters ---------- new_path : str Path where the new directory should be created. - - Notes - ----- - This approach is used as a workaround for Python 2.7 which doesn't - have the exist_ok' parameter in os.makedirs(). """ if not os.path.exists(new_path): From 18131a87fd6301f9ab4e48cb7a57e02459127de1 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Thu, 20 Mar 2025 13:35:37 +0100 Subject: [PATCH 38/64] Update dependency for mocks to 0.8.0.a0 --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8b95696a..03015af8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -145,13 +145,13 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "imcf-fiji-mocks" -version = "0.7.0" +version = "0.8.0a0" description = "Mocks collection for Fiji-Python. Zero functional code." optional = false python-versions = ">=2.7" files = [ - {file = "imcf_fiji_mocks-0.7.0-py2.py3-none-any.whl", hash = "sha256:643c3d4cc916d1573f1f3490885e37822c11c40c09b6a1e06c2fc561c8aeb20e"}, - {file = "imcf_fiji_mocks-0.7.0.tar.gz", hash = "sha256:65eab1974629ef72bbe7b8d76e81e067d5fb55b65aab8fb7e0f24755e0f7ac6b"}, + {file = "imcf_fiji_mocks-0.8.0a0-py2.py3-none-any.whl", hash = "sha256:d4841b32725a2b81790e1051da264bfb72b7e275c23d87a04053462420184f78"}, + {file = "imcf_fiji_mocks-0.8.0a0.tar.gz", hash = "sha256:d00ca538bf49dde5386d148a7b71c68cbee0397edc6ce4ce2547f7f26c803478"}, ] [[package]] @@ -520,4 +520,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.10" -content-hash = "010a8923c68fb6e367da60506e1937f570cd37633e42c44ba312012be4aa2172" +content-hash = "26f88742f5c3e83b8a39f299398e5784b3689404e83870520d80db6aaa91d6ff" diff --git a/pyproject.toml b/pyproject.toml index 0ab51dd3..dda9e34b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ version = "0.0.0" # - or: python = ">=3.10" [tool.poetry.dependencies] -imcf-fiji-mocks = ">=0.7.0" +imcf-fiji-mocks = ">=0.8.0.a0" python = ">=2.7" python-micrometa = "^15.2.2" sjlogging = ">=0.5.2" From b520c2836be3d96d973f28a0c6fc676a4418f8ed Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Mon, 24 Mar 2025 14:24:10 +0100 Subject: [PATCH 39/64] Revert "Fix linting issues" This reverts commit 0e81c37d5d3d3902b8c5211efde931a21f3d162f. --- src/imcflibs/imagej/bdv.py | 52 +++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/imcflibs/imagej/bdv.py b/src/imcflibs/imagej/bdv.py index 0f1a42a4..b3d0f2d2 100644 --- a/src/imcflibs/imagej/bdv.py +++ b/src/imcflibs/imagej/bdv.py @@ -207,7 +207,7 @@ def process_angle(self, value, range_end=None): range_end : int, optional Contains the end of the range, by default None. - Notes + Notes: ----- Previous function name : angle_select(). """ @@ -233,7 +233,7 @@ def process_channel(self, value, range_end=None): range_end : int, optional Contains the end of the range, by default None. - Notes + Notes: ----- Previous function name : channel_select(). """ @@ -259,7 +259,7 @@ def process_illumination(self, value, range_end=None): range_end : int, optional Contains the end of the range, by default None. - Notes + Notes: ----- Previous function name : illumination_select(). """ @@ -285,7 +285,7 @@ def process_tile(self, value, range_end=None): range_end : int, optional Contains the end of the range, by default None. - Notes + Notes: ----- Previous function name : tile_select(). """ @@ -311,7 +311,7 @@ def process_timepoint(self, value, range_end=None): range_end : int, optional Contains the end of the range, by default None. - Notes + Notes: ----- Previous function name : timepoint_select(). """ @@ -578,7 +578,7 @@ def check_definition_option(self, value): } def set_angle_definition(self, value): - """Set the value for the angle definition. + """Set the value for the angle definition Parameters ---------- @@ -592,7 +592,7 @@ def set_angle_definition(self, value): ) def set_channel_definition(self, value): - """Set the value for the channel definition. + """Set the value for the channel definition Parameters ---------- @@ -607,7 +607,7 @@ def set_channel_definition(self, value): ) def set_illumination_definition(self, value): - """Set the value for the illumination definition. + """Set the value for the illumination definition Parameters ---------- @@ -624,7 +624,7 @@ def set_illumination_definition(self, value): ) def set_tile_definition(self, value): - """Set the value for the tile_definition. + """Set the value for the tile_definition Parameters ---------- @@ -638,7 +638,7 @@ def set_tile_definition(self, value): ) def set_timepoint_definition(self, value): - """Set the value for the time_point_definition. + """Set the value for the time_point_definition Parameters ---------- @@ -688,7 +688,6 @@ def check_processing_input(value, range_end): Contains the list of input dimensions, the first input dimension of a range or a single channel range_end : int or None Contains the end of the range if need be - Returns ------- str @@ -810,8 +809,8 @@ def define_dataset_auto( subsampling_factors=None, hdf5_chunk_sizes=None, ): - """Define a dataset using the Autoloader or Multi-View loader. - + """Will run the corresponding "Define Dataset" using the "Auto-Loader" + option. If the series is tiles, will run "Define Dataset...", otherwise will run "Define Multi-View Dataset...". @@ -994,10 +993,8 @@ def resave_as_h5( XML input file. output_h5_file_path : str Export path for the output file including the `.xml `extension. - processing_opts : imcflibs.imagej.bdv.ProcessingOptions, optional - The `ProcessingOptions` object defining parameters for the run. Will - fall back to the defaults defined in the corresponding class if the - parameter is `None` or skipped. + timepoints : str, optional + The timepoints that should be exported, by default `All Timepoints`. timepoints_per_partition : int, optional How many timepoints to export per partition, by default `1`. use_deflate_compression : bool, optional @@ -1282,10 +1279,10 @@ def detect_interest_points( ---------- project_path : str Path to the `.xml` project. - processing_opts : imcflibs.imagej.bdv.ProcessingOptions, optional - The `ProcessingOptions` object defining parameters for the run. Will - fall back to the defaults defined in the corresponding class if the - parameter is `None` or skipped. + process_timepoint : str, optional + Timepoint to be processed, by default `All Timepoints`. + process_channel : str, optional + Channel to be processed, by default `All channels`. sigma : float, optional Minimum sigma for interest points detection, by default `1.8`. threshold : float, optional @@ -1340,11 +1337,14 @@ def interest_points_registration( ---------- project_path : str Path to the `.xml` project. - processing_opts : imcflibs.imagej.bdv.ProcessingOptions, optional - The `ProcessingOptions` object defining parameters for the run. Will - fall back to the defaults defined in the corresponding class if the - parameter is `None` or skipped. This controls which angles, channels, - illuminations, tiles and timepoints are processed. + process_timepoint : str, optional + Timepoint to be processed, by default `All Timepoints`. + process_channel : str, optional + Channels to be used for performing the registration. By default, all + channels are taken into account, however this behavior could be + undesirable if only one channel is adequate (e.g. beads or other useful + fiducials). To restrict registration to a specific channel, provide the + channel name using this parameter. By default `All channels`. rigid_timepoints : bool, optional If set to `True` each timepoint will be considered as a rigid unit (useful e.g. if spatial registration has already been performed before). From 2b6a8d8162989005d28c9af63f3450c9afdc8e64 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Mon, 24 Mar 2025 14:24:21 +0100 Subject: [PATCH 40/64] Revert "Formatting" This reverts commit 5bea0adc37583eeef84e8074211361223db2d503. --- src/imcflibs/imagej/bdv.py | 161 +++++++++---------------------------- 1 file changed, 40 insertions(+), 121 deletions(-) diff --git a/src/imcflibs/imagej/bdv.py b/src/imcflibs/imagej/bdv.py index b3d0f2d2..4d7f6825 100644 --- a/src/imcflibs/imagej/bdv.py +++ b/src/imcflibs/imagej/bdv.py @@ -130,9 +130,7 @@ def reference_channel(self, value): """ # channel = int(value) - 1 # will raise a ValueError if cast fails self._use_channel = "channels=[use Channel %s]" % int(value) - log.debug( - "New reference channel setting: %s", self._use_channel - ) + log.debug("New reference channel setting: %s", self._use_channel) def reference_illumination(self, value): """Set the reference illumination when using *Expert Grouping Options*. @@ -148,13 +146,8 @@ def reference_illumination(self, value): value : int or int-like The illumination number to use for the grouping. """ - self._use_illumination = ( - "illuminations=[use Illumination %s]" % value - ) - log.debug( - "New reference illumination setting: %s", - self._use_illumination, - ) + self._use_illumination = "illuminations=[use Illumination %s]" % value + log.debug("New reference illumination setting: %s", self._use_illumination) def reference_tile(self, value): """Set the reference tile when using *Expert Grouping Options*. @@ -188,9 +181,7 @@ def reference_timepoint(self, value): The timepoint number to use for the grouping. """ self._use_timepoint = "timepoints=[use Timepoint %s]" % value - log.debug( - "New reference timepoint setting: %s", self._use_timepoint - ) + log.debug("New reference timepoint setting: %s", self._use_timepoint) ### process-X methods @@ -412,22 +403,16 @@ def fmt_acitt_options(self, input="process"): """ input_type = ["process", "resave"] if input not in input_type: - raise ValueError( - "Invalue input type. Expected one of: %s" % input_type - ) + raise ValueError("Invalue input type. Expected one of: %s" % input_type) parameters = [ input + "_angle=" + self._angle_processing_option, input + "_channel=" + self._channel_processing_option, - input - + "_illumination=" - + self._illumination_processing_option, + input + "_illumination=" + self._illumination_processing_option, input + "_tile=" + self._tile_processing_option, input + "_timepoint=" + self._timepoint_processing_option, ] parameter_string = " ".join(parameters).strip() - log.debug( - "Formatted 'process_X' options: <%s>", parameter_string - ) + log.debug("Formatted 'process_X' options: <%s>", parameter_string) return parameter_string + " " def fmt_acitt_selectors(self): @@ -447,16 +432,12 @@ def fmt_acitt_selectors(self): parameters = [ self._angle_select if self._angle_select else "", self._channel_select if self._channel_select else "", - self._illumination_select - if self._illumination_select - else "", + self._illumination_select if self._illumination_select else "", self._tile_select if self._tile_select else "", self._timepoint_select if self._timepoint_select else "", ] parameter_string = " ".join(parameters).strip() - log.debug( - "Formatted 'processing_X' selectors: <%s>", parameter_string - ) + log.debug("Formatted 'processing_X' selectors: <%s>", parameter_string) return parameter_string + " " def fmt_how_to_treat(self): @@ -474,9 +455,7 @@ def fmt_how_to_treat(self): "how_to_treat_timepoints=" + self._treat_timepoints, ] parameter_string = " ".join(parameters).strip() - log.debug( - "Formatted 'how_to_treat_X' options: <%s>", parameter_string - ) + log.debug("Formatted 'how_to_treat_X' options: <%s>", parameter_string) return parameter_string + " " def fmt_use_acitt(self): @@ -491,22 +470,13 @@ def fmt_use_acitt(self): """ parameters = [ self._use_angle if self._treat_angles == "group" else "", - self._use_channel - if self._treat_channels == "group" - else "", - self._use_illumination - if self._treat_illuminations == "group" - else "", + self._use_channel if self._treat_channels == "group" else "", + self._use_illumination if self._treat_illuminations == "group" else "", self._use_tile if self._treat_tiles == "group" else "", - self._use_timepoint - if self._treat_timepoints == "group" - else "", + self._use_timepoint if self._treat_timepoints == "group" else "", ] parameter_string = " ".join(parameters).strip() - log.debug( - "Formatted expert grouping 'use' options: <%s>", - parameter_string, - ) + log.debug("Formatted expert grouping 'use' options: <%s>", parameter_string) return parameter_string + " " @@ -544,9 +514,7 @@ class DefinitionOptions(object): def __init__(self): self._angle_definition = SINGLE_FILE % "angle" self._channel_definition = MULTI_SINGLE_FILE % "channel" - self._illumination_definition = ( - SINGLE_FILE % "illumination direction" - ) + self._illumination_definition = SINGLE_FILE % "illumination direction" self._tile_definition = MULTI_MULTI_FILE % "tile" self._timepoint_definition = SINGLE_FILE % "time-point" @@ -567,9 +535,7 @@ def check_definition_option(self, value): "multi_single", "multi_multi", ]: - raise ValueError( - "Value must be one of single, multi_multi or multi_single" - ) + raise ValueError("Value must be one of single, multi_multi or multi_single") return { "single": SINGLE_FILE, @@ -587,9 +553,7 @@ def set_angle_definition(self, value): """ choices = self.check_definition_option(value) self._angle_definition = choices[value] % "angle" - log.debug( - "New 'angle_definition' setting: %s", self._angle_definition - ) + log.debug("New 'angle_definition' setting: %s", self._angle_definition) def set_channel_definition(self, value): """Set the value for the channel definition @@ -601,10 +565,7 @@ def set_channel_definition(self, value): """ choices = self.check_definition_option(value) self._channel_definition = choices[value] % "channel" - log.debug( - "New 'channel_definition' setting: %s", - self._channel_definition, - ) + log.debug("New 'channel_definition' setting: %s", self._channel_definition) def set_illumination_definition(self, value): """Set the value for the illumination definition @@ -615,12 +576,9 @@ def set_illumination_definition(self, value): One of `single`, `multi_single` or `multi_multi`. """ choices = self.check_definition_option(value) - self._illumination_definition = ( - choices[value] % "illumination direction" - ) + self._illumination_definition = choices[value] % "illumination direction" log.debug( - "New 'illumination_definition' setting: %s", - self._illumination_definition, + "New 'illumination_definition' setting: %s", self._illumination_definition ) def set_tile_definition(self, value): @@ -633,9 +591,7 @@ def set_tile_definition(self, value): """ choices = self.check_definition_option(value) self._tile_definition = choices[value] % "tile" - log.debug( - "New 'tile_definition' setting: %s", self._tile_definition - ) + log.debug("New 'tile_definition' setting: %s", self._tile_definition) def set_timepoint_definition(self, value): """Set the value for the time_point_definition @@ -647,10 +603,7 @@ def set_timepoint_definition(self, value): """ choices = self.check_definition_option(value) self._timepoint_definition = choices[value] % "time-point" - log.debug( - "New 'timepoint_definition' setting: %s", - self._timepoint_definition, - ) + log.debug("New 'timepoint_definition' setting: %s", self._timepoint_definition) def fmt_acitt_options(self): """Format Angle / Channel / Illumination / Tile / Timepoint options. @@ -671,9 +624,7 @@ def fmt_acitt_options(self): "multiple_timepoints=" + self._timepoint_definition, ] parameter_string = " ".join(parameters).strip() - log.debug( - "Formatted 'multiple_X' options: <%s>", parameter_string - ) + log.debug("Formatted 'multiple_X' options: <%s>", parameter_string) return parameter_string + " " @@ -697,14 +648,10 @@ def check_processing_input(value, range_end): value = [value] # Check if all the elements of the value list are of the same type if not all(isinstance(x, type(value[0])) for x in value): - raise TypeError( - "Invalid input type. All the values should be of the same type" - ) + raise TypeError("Invalid input type. All the values should be of the same type") if type(range_end) is int: if type(value[0]) is not int: - raise TypeError( - "Invalid input type. Expected an int for the range start" - ) + raise TypeError("Invalid input type. Expected an int for the range start") elif len(value) != 1: raise ValueError( "Invalid input type. Expected a single number for the range start" @@ -742,13 +689,7 @@ def get_processing_settings(dimension, selection, value, range_end): if selection == "single": processing_option = SINGLE % dimension - dimension_select = ( - "processing_" - + dimension - + "=[" - + dimension - + " %s]" % value - ) + dimension_select = "processing_" + dimension + "=[" + dimension + " %s]" % value if selection == "multiple": processing_option = MULTIPLE % dimension @@ -791,9 +732,7 @@ def backup_xml_files(source_directory, subfolder_name): pathtools.create_directory(xml_backup_directory) backup_subfolder = xml_backup_directory + "/%s" % (subfolder_name) pathtools.create_directory(backup_subfolder) - all_xml_files = pathtools.listdir_matching( - source_directory, ".*\\.xml", regex=True - ) + all_xml_files = pathtools.listdir_matching(source_directory, ".*\\.xml", regex=True) os.chdir(source_directory) for xml_file in all_xml_files: shutil.copy2(xml_file, backup_subfolder) @@ -853,9 +792,7 @@ def define_dataset_auto( dataset_save_path = result_folder if subsampling_factors: subsampling_factors = ( - "manual_mipmap_setup subsampling_factors=" - + subsampling_factors - + " " + "manual_mipmap_setup subsampling_factors=" + subsampling_factors + " " ) else: subsampling_factors = "" @@ -1022,9 +959,7 @@ def resave_as_h5( split_hdf5 = "" if subsampling_factors: - subsampling_factors = ( - "subsampling_factors=" + subsampling_factors + " " - ) + subsampling_factors = "subsampling_factors=" + subsampling_factors + " " else: subsampling_factors = " " if hdf5_chunk_sizes: @@ -1114,13 +1049,10 @@ def phase_correlation_pairwise_shifts_calculation( file_info = pathtools.parse_path(project_path) if downsampling_xyz != "": - downsampling = ( - "downsample_in_x=%s downsample_in_y=%s downsample_in_z=%s " - % ( - downsampling_xyz[0], - downsampling_xyz[1], - downsampling_xyz[2], - ) + downsampling = "downsample_in_x=%s downsample_in_y=%s downsample_in_z=%s " % ( + downsampling_xyz[0], + downsampling_xyz[1], + downsampling_xyz[2], ) else: downsampling = "" @@ -1144,9 +1076,7 @@ def phase_correlation_pairwise_shifts_calculation( log.debug("Calculate pairwise shifts options: <%s>", options) IJ.run("Calculate pairwise shifts ...", str(options)) - backup_xml_files( - file_info["path"], "phase_correlation_shift_calculation" - ) + backup_xml_files(file_info["path"], "phase_correlation_shift_calculation") def filter_pairwise_shifts( @@ -1258,9 +1188,7 @@ def optimize_and_apply_shifts( + processing_opts.fmt_how_to_treat() ) - log.debug( - "Optimization and shifts application options: <%s>", options - ) + log.debug("Optimization and shifts application options: <%s>", options) IJ.run("Optimize globally and apply shifts ...", str(options)) backup_xml_files(file_info["path"], "optimize_and_apply_shifts") @@ -1438,12 +1366,8 @@ def duplicate_transformations( target = "[All Channels]" source = str(channel_source - 1) if tile_source: - tile_apply = ( - "apply_to_tile=[Single tile (Select from List)] " - ) - tile_process = ( - "processing_tile=[tile " + str(tile_source) + "] " - ) + tile_apply = "apply_to_tile=[Single tile (Select from List)] " + tile_process = "processing_tile=[tile " + str(tile_source) + "] " else: tile_apply = "apply_to_tile=[All tiles] " elif transformation_type == "tile": @@ -1451,13 +1375,9 @@ def duplicate_transformations( target = "[All Tiles]" source = str(tile_source) if channel_source: - chnl_apply = ( - "apply_to_channel=[Single channel (Select from List)] " - ) + chnl_apply = "apply_to_channel=[Single channel (Select from List)] " chnl_process = ( - "processing_channel=[channel " - + str(channel_source - 1) - + "] " + "processing_channel=[channel " + str(channel_source - 1) + "] " ) else: chnl_apply = "apply_to_channel=[All channels] " @@ -1493,8 +1413,7 @@ def duplicate_transformations( IJ.run("Duplicate Transformations", str(options)) backup_xml_files( - file_info["path"], - "duplicate_transformation_" + transformation_type, + file_info["path"], "duplicate_transformation_" + transformation_type ) From 82b86833336fcffa791f766f5ba5041743c8aa14 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Mon, 24 Mar 2025 14:24:40 +0100 Subject: [PATCH 41/64] Revert "Fix linting" This reverts commit 74668385346fb6619d8c8af0272fa930ce65d3c1. --- src/imcflibs/imagej/bdv.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/imcflibs/imagej/bdv.py b/src/imcflibs/imagej/bdv.py index 4d7f6825..7dfa1325 100644 --- a/src/imcflibs/imagej/bdv.py +++ b/src/imcflibs/imagej/bdv.py @@ -199,7 +199,7 @@ def process_angle(self, value, range_end=None): Contains the end of the range, by default None. Notes: - ----- + ------ Previous function name : angle_select(). """ @@ -225,7 +225,7 @@ def process_channel(self, value, range_end=None): Contains the end of the range, by default None. Notes: - ----- + ------ Previous function name : channel_select(). """ @@ -251,7 +251,7 @@ def process_illumination(self, value, range_end=None): Contains the end of the range, by default None. Notes: - ----- + ------ Previous function name : illumination_select(). """ @@ -277,7 +277,7 @@ def process_tile(self, value, range_end=None): Contains the end of the range, by default None. Notes: - ----- + ------ Previous function name : tile_select(). """ @@ -303,7 +303,7 @@ def process_timepoint(self, value, range_end=None): Contains the end of the range, by default None. Notes: - ----- + ------ Previous function name : timepoint_select(). """ From ebb141379a8d2a35eefb4bf666a852d106f2b58c Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Mon, 24 Mar 2025 16:00:59 +0100 Subject: [PATCH 42/64] Rename functions --- src/imcflibs/imagej/bioformats.py | 4 ++-- src/imcflibs/imagej/labelimage.py | 4 ++-- src/imcflibs/imagej/misc.py | 6 +++--- src/imcflibs/imagej/objects3d.py | 2 +- src/imcflibs/imagej/omerotools.py | 3 ++- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/imcflibs/imagej/bioformats.py b/src/imcflibs/imagej/bioformats.py index 83d19728..80a4b9ee 100644 --- a/src/imcflibs/imagej/bioformats.py +++ b/src/imcflibs/imagej/bioformats.py @@ -291,7 +291,7 @@ def write_bf_memoryfile(path_to_file): reader.close() -def get_metadata_from_image(path_to_image): +def get_metadata_from_file(path_to_image): """Extract metadata from an image file using Bio-Formats. This function reads an image file using the Bio-Formats library and extracts @@ -355,7 +355,7 @@ def get_metadata_from_image(path_to_image): return image_calibration -def get_stage_coordinates_from_ome_metadata(source, imagenames): +def get_stage_coords(source, filenames): """Get the stage coordinates and calibration from the ome-xml for a given list of images Parameters diff --git a/src/imcflibs/imagej/labelimage.py b/src/imcflibs/imagej/labelimage.py index b68c4de8..767161ae 100644 --- a/src/imcflibs/imagej/labelimage.py +++ b/src/imcflibs/imagej/labelimage.py @@ -67,7 +67,7 @@ def label_image_to_roi_list(label_image, low_thresh=None): return roi_list, max_value -def relate_label_images(label_image_ref, label_image_to_relate): +def cookie_cut_labels(label_image_ref, label_image_to_relate): """Relate label images, giving the same label to objects belonging together. ❗ NOTE: Won't work with touching labels ❗ @@ -97,7 +97,7 @@ def relate_label_images(label_image_ref, label_image_to_relate): return ImageCalculator.run(label_image_ref, imp_dup, "Multiply create") -def associate_label_images_3d(outer_label_imp, inner_label_imp): +def relate_label_images(outer_label_imp, inner_label_imp): """ Associate two label images. diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 3127120b..6256f692 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -501,8 +501,8 @@ def write_ordereddict_to_csv(out_file, content): dict_writer.writerows(content) -def save_image_with_extension( - imp, extension, out_dir, series, pad_number, split_channels +def save_image_in_format( + imp, format, out_dir, series, pad_number, split_channels ): """Save an ImagePlus object in the specified format. @@ -676,7 +676,7 @@ def locate_latest_imaris(paths_to_check=None): return imaris_paths[-1] -def convert_to_imaris(path_to_image): +def run_imarisconvert(file_path): """Convert a given file to Imaris5 .ims using ImarisConvert.exe via subprocess. Parameters diff --git a/src/imcflibs/imagej/objects3d.py b/src/imcflibs/imagej/objects3d.py index 845e304c..17d0d083 100644 --- a/src/imcflibs/imagej/objects3d.py +++ b/src/imcflibs/imagej/objects3d.py @@ -151,7 +151,7 @@ def get_objects_within_intensity(obj_pop, imp, min_intensity, max_intensity): return Objects3DPopulation(objects_within_intensity) -def maxima_finder_3D(imageplus, min_threshold=0, noise=100, rxy=1.5, rz=1.5): +def maxima_finder_3d(imp, min_threshold=0, noise=100, rxy=1.5, rz=1.5): """ Find local maxima in a 3D image. diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index 824d1c6a..a646258c 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -190,7 +190,7 @@ def add_annotation(client, repository_wpr, annotations, header): repository_wpr.addMapAnnotation(client, map_annotation_wpr) -def delete_annotation(user_client, repository_wpr): +def delete_keyvalue_annotations(user_client, object_wrapper): """Delete annotations linked to object Parameters @@ -223,6 +223,7 @@ def find_dataset(client, dataset_id): # Fetch the dataset from the OMERO server using the provided dataset ID return client.getDataset(Long(dataset_id)) + def get_acquisition_metadata(user_client, image_wpr): def get_acquisition_metadata_from_imageid(user_client, image_wpr): """Get acquisition metadata from OMERO based on an image ID From b59b59729f787cad41d1fa63e611ab9d0d2097ea Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Mon, 24 Mar 2025 16:02:17 +0100 Subject: [PATCH 43/64] Change returns to use dictionaries instead --- src/imcflibs/imagej/bioformats.py | 66 +++++++++---------- src/imcflibs/imagej/omerotools.py | 102 +++++++++++++++++------------- 2 files changed, 90 insertions(+), 78 deletions(-) diff --git a/src/imcflibs/imagej/bioformats.py b/src/imcflibs/imagej/bioformats.py index 80a4b9ee..0f6f2a99 100644 --- a/src/imcflibs/imagej/bioformats.py +++ b/src/imcflibs/imagej/bioformats.py @@ -356,42 +356,42 @@ def get_metadata_from_file(path_to_image): def get_stage_coords(source, filenames): - """Get the stage coordinates and calibration from the ome-xml for a given list of images + """Get stage coordinates and calibration for a given list of images. Parameters ---------- source : str Path to the images - imagenames : list of str + filenames : list of str List of images filenames Returns ------- - tuple - Contains - dimensions : int + dict + A dictionary containing the following metadata: + - `dimensions` : int Number of dimensions (2D or 3D) - stage_coordinates_x : list - The absolute stage x-coordinated from ome-xml metadata - stage_coordinates_y : list - The absolute stage y-coordinated from ome-xml metadata - stage_coordinates_z : list - The absolute stage z-coordinated from ome-xml metadata - relative_coordinates_x : list + - `stage_coordinates_x` : list + The absolute stage x-coordinated + - `stage_coordinates_y` : list + The absolute stage y-coordinated + - `stage_coordinates_z` : list + The absolute stage z-coordinated + - `relative_coordinates_x` : list The relative stage x-coordinates in px - relative_coordinates_y : list + - `relative_coordinates_y` : list The relative stage y-coordinates in px - relative_coordinates_z : list + - `relative_coordinates_z` : list The relative stage z-coordinates in px - image_calibration : list + - `image_calibration` : list x,y,z image calibration in unit/px - calibration_unit : str + - `calibration_unit` : str Image calibration unit - image_dimensions_czt : list + - `image_dimensions_czt` : list Number of images in dimensions c,z,t - series_names : list of str + - `series_names` : list of str Names of all series contained in the files - max_size : list of int + - `max_size` : list of int Maximum size across all files in dimensions x,y,z """ @@ -517,17 +517,17 @@ def get_stage_coords(source, filenames): relative_coordinates_y_px.append(rel_pos_y) relative_coordinates_z_px.append(rel_pos_z) - return ( - dimensions, - stage_coordinates_x, - stage_coordinates_y, - stage_coordinates_z, - relative_coordinates_x_px, - relative_coordinates_y_px, - relative_coordinates_z_px, - image_calibration, - calibration_unit, - image_dimensions_czt, - series_names, - max_size, - ) + return { + "dimensions": dimensions, + "stage_coordinates_x": stage_coordinates_x, + "stage_coordinates_y": stage_coordinates_y, + "stage_coordinates_z": stage_coordinates_z, + "relative_coordinates_x": relative_coordinates_x_px, + "relative_coordinates_y": relative_coordinates_y_px, + "relative_coordinates_z": relative_coordinates_z_px, + "image_calibration": image_calibration, + "calibration_unit": calibration_unit, + "image_dimensions_czt": image_dimensions_czt, + "series_names": series_names, + "max_size": max_size, + } diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index a646258c..f365b73d 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -224,55 +224,67 @@ def find_dataset(client, dataset_id): return client.getDataset(Long(dataset_id)) def get_acquisition_metadata(user_client, image_wpr): + """Get acquisition metadata from OMERO based on an image ID. + + Parameters + ---------- + user_client : fr.igred.omero.Client + Client used for login to OMERO + image_wpr : fr.igred.omero.repositor.ImageWrapper + Wrapper to the image for the metadata + + Returns + ------- + dict + Dictionary containing acquisition metadata: + - `objective_magnification`: Objective magnification + - `objective_na`: Objective NA + - `acquisition_date`: Acquisition date + - `acquisition_date_number`: Acquisition date as a number + """ + ctx = user_client.getCtx() + instrument_data = ( + user_client.getGateway() + .getMetadataService(ctx) + .loadInstrument(image_wpr.asDataObject().getInstrumentId()) + ) + objective_data = instrument_data.copyObjective().get(0) + metadata = {} -def get_acquisition_metadata_from_imageid(user_client, image_wpr): - """Get acquisition metadata from OMERO based on an image ID - - Parameters - ---------- - user_client : fr.igred.omero.Client - Client used for login to OMERO - image_wpr : fr.igred.omero.repositor.ImageWrapper - Wrapper to the image for the ROIs + metadata["objective_magnification"] = ( + objective_data.getNominalMagnification().getValue() + if objective_data.getNominalMagnification() is not None + else 0 + ) + metadata["objective_na"] = ( + objective_data.getLensNA().getValue() + if objective_data.getLensNA() is not None + else 0 + ) - Returns - ------- - tuple of (int, int, str, int) - List of info about the acquisition - """ - ctx = user_client.getCtx() - instrument_data = ( - user_client.getGateway() - .getMetadataService(ctx) - .loadInstrument(image_wpr.asDataObject().getInstrumentId()) - ) - objective_data = instrument_data.copyObjective().get(0) - if objective_data.getNominalMagnification() is None: - obj_mag = 0 - else: - obj_mag = objective_data.getNominalMagnification().getValue() - if objective_data.getLensNA() is None: - obj_na = 0 - else: - obj_na = objective_data.getLensNA().getValue() - if image_wpr.getAcquisitionDate() is None: - if image_wpr.asDataObject().getFormat() == "ZeissCZI": - field = "Information|Document|CreationDate" - date_field = get_info_from_original_metadata(user_client, image_wpr, field) - acq_date = date_field.split("T")[0] - acq_date_number = int(acq_date.replace("-", "")) + if image_wpr.getAcquisitionDate() is None: + if image_wpr.asDataObject().getFormat() == "ZeissCZI": + field = "Information|Document|CreationDate" + date_field = get_info_from_original_metadata( + user_client, image_wpr, field + ) + metadata["acquisition_date"] = date_field.split("T")[0] + metadata["acquisition_date_number"] = int( + metadata["acquisition_date"].replace("-", "") + ) + else: + metadata["acquisition_date"] = "NA" + metadata["acquisition_date_number"] = 0 else: - acq_date = "NA" - acq_date_number = 0 - - else: - sdf = SimpleDateFormat("yyyy-MM-dd") - acq_date = sdf.format( - image_wpr.getAcquisitionDate() - ) # image_wpr.getAcquisitionDate() - acq_date_number = int(acq_date.replace("-", "")) + sdf = SimpleDateFormat("yyyy-MM-dd") + metadata["acquisition_date"] = sdf.format( + image_wpr.getAcquisitionDate() + ) + metadata["acquisition_date_number"] = int( + metadata["acquisition_date"].replace("-", "") + ) - return obj_mag, obj_na, acq_date, acq_date_number + return metadata def get_info_from_original_metadata(user_client, image_wpr, field): From 98b9ca8136b75a3177ad4829fff1bfccfe7caa1e Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Mon, 24 Mar 2025 16:04:37 +0100 Subject: [PATCH 44/64] Change input variable name --- src/imcflibs/imagej/bioformats.py | 2 +- src/imcflibs/imagej/misc.py | 25 +++++++++++-------------- src/imcflibs/imagej/objects3d.py | 6 +++--- src/imcflibs/imagej/omerotools.py | 10 +++++----- src/imcflibs/imagej/processing.py | 8 ++++---- 5 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/imcflibs/imagej/bioformats.py b/src/imcflibs/imagej/bioformats.py index 0f6f2a99..8d874c11 100644 --- a/src/imcflibs/imagej/bioformats.py +++ b/src/imcflibs/imagej/bioformats.py @@ -401,7 +401,7 @@ def get_stage_coords(source, filenames): stage_coordinates_z = [] series_names = [] - for counter, image in enumerate(imagenames): + for counter, image in enumerate(filenames): # parse metadata reader = ImageReader() reader.setFlattenedResolutions(False) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 6256f692..3dbfce13 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -520,7 +520,7 @@ def save_image_in_format( ---------- imp : ij.ImagePlus ImagePlus object to save. - extension : {'ImageJ-TIF', 'ICS-1', 'ICS-2', 'OME-TIFF', 'CellH5', 'BMP'} + format : {'ImageJ-TIF', 'ICS-1', 'ICS-2', 'OME-TIFF', 'CellH5', 'BMP'} Output format to use, see Notes section below for details. out_dir : str Directory path where the image(s) will be saved. @@ -535,7 +535,7 @@ def save_image_in_format( Notes ----- - Depending on the value of the `extension` parameter, one of the following + Depending on the value of the `format` parameter, one of the following output formats and saving strategies will be used: - Bio-Formats based formats will be produced by calling `bf.export()`, note that these formats will preserve metadata (which is **not** the case for @@ -601,17 +601,17 @@ def save_image_in_format( basename + "_series_" + str(series).zfill(pad_number), ) - if extension == "ImageJ-TIF": + if format == "ImageJ-TIF": pathtools.create_directory(dir_to_save[index]) IJ.saveAs(current_imp, "Tiff", out_path + ".tif") - elif extension == "BMP": + elif format == "BMP": out_folder = os.path.join(out_dir, basename + os.path.sep) pathtools.create_directory(out_folder) StackWriter.save(current_imp, out_folder, "format=bmp") else: - bf.export(current_imp, out_path + out_ext[extension]) + bf.export(current_imp, out_path + out_ext[format]) current_imp.close() @@ -681,25 +681,22 @@ def run_imarisconvert(file_path): Parameters ---------- - path_to_image : str + file_path : str Absolute path to the input image file. - Notes - ----- - The function handles special case for .ids files by converting them to .ics before - processing. It uses the latest installed Imaris application to perform the conversion. + """ - path_root, file_extension = os.path.splitext(path_to_image) + path_root, file_extension = os.path.splitext(file_path) if file_extension == ".ids": file_extension = ".ics" - path_to_image = path_root + file_extension + file_path = path_root + file_extension imaris_path = locate_latest_imaris() command = 'ImarisConvert.exe -i "%s" -of Imaris5 -o "%s"' % ( - path_to_image, - path_to_image.replace(file_extension, ".ims"), + file_path, + file_path.replace(file_extension, ".ims"), ) print("\n%s" % command) IJ.log("Converting to Imaris5 .ims...") diff --git a/src/imcflibs/imagej/objects3d.py b/src/imcflibs/imagej/objects3d.py index 17d0d083..c3122fe3 100644 --- a/src/imcflibs/imagej/objects3d.py +++ b/src/imcflibs/imagej/objects3d.py @@ -160,7 +160,7 @@ def maxima_finder_3d(imp, min_threshold=0, noise=100, rxy=1.5, rz=1.5): Parameters ---------- - imageplus : ij.ImagePlus + imp : ij.ImagePlus The input 3D image in which to find local maxima. min_threshold : int, optional The minimum intensity threshold for maxima detection. Default is 0. @@ -177,7 +177,7 @@ def maxima_finder_3d(imp, min_threshold=0, noise=100, rxy=1.5, rz=1.5): An ImagePlus object containing the detected maxima as peaks. """ # Wrap the input ImagePlus into an ImageHandler - img = ImageHandler.wrap(imageplus) + img = ImageHandler.wrap(imp) # Duplicate the image and apply a threshold cut-off thresholded = img.duplicate() @@ -196,7 +196,7 @@ def maxima_finder_3d(imp, min_threshold=0, noise=100, rxy=1.5, rz=1.5): imp_peaks = img_peaks.getImagePlus() # Set the calibration of the peaks image to match the input image - imp_peaks.setCalibration(imageplus.getCalibration()) + imp_peaks.setCalibration(imp.getCalibration()) # Set the title of the peaks image imp_peaks.setTitle("Peaks") diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index f365b73d..f6eb3d21 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -191,17 +191,17 @@ def add_annotation(client, repository_wpr, annotations, header): def delete_keyvalue_annotations(user_client, object_wrapper): - """Delete annotations linked to object + """Delete annotations linked to object. Parameters ---------- user_client : fr.igred.omero.Client Client used for login to OMERO - repository_wpr : fr.igred.omero.repositor.GenericRepositoryObjectWrapper + object_wrapper : fr.igred.omero.repositor.GenericRepositoryObjectWrapper Wrapper to the object for the anotation """ - kv_pairs = repository_wpr.getMapAnnotations(user_client) + kv_pairs = object_wrapper.getMapAnnotations(user_client) user_client.delete(kv_pairs) @@ -298,8 +298,8 @@ def get_info_from_original_metadata(user_client, image_wpr, field): ---------- user_client : fr.igred.omero.Client Client used for login to OMERO - image_id : int - ID of the image to look. + image_wpr : fr.igred.omero.repositor.ImageWrapper + Wrapper to the image field : str Field to look for in the original metadata. Needs to be found beforehand. diff --git a/src/imcflibs/imagej/processing.py b/src/imcflibs/imagej/processing.py index de34a348..af503d64 100644 --- a/src/imcflibs/imagej/processing.py +++ b/src/imcflibs/imagej/processing.py @@ -3,7 +3,7 @@ from ..log import LOG as log def apply_filter(imp, filter_method, filter_radius, do_3D=False): - """ +def apply_filter(imp, filter_method, filter_radius, do_3d=False): Make a specific filter followed by a threshold method of choice Parameters @@ -54,7 +54,7 @@ def apply_filter(imp, filter_method, filter_radius, do_3D=False): return imageplus def apply_background_subtraction(imp, rolling_ball_radius, do_3D=False): - """ +def apply_background_subtraction(imp, rolling_ball_radius, do_3d=False): Perform background subtraction using a rolling ball method Parameters @@ -75,7 +75,7 @@ def apply_background_subtraction(imp, rolling_ball_radius, do_3D=False): options = ( "rolling=" + str(rolling_ball_radius) - + " stack" if do_3D else "" + if do_3d ) log.debug("Background subtraction options: %s" % options) @@ -86,7 +86,7 @@ def apply_background_subtraction(imp, rolling_ball_radius, do_3D=False): return imageplus def apply_threshold(imp, threshold_method): - """ +def apply_threshold(imp, threshold_method, do_3d=True): Apply a threshold method to the input ImagePlus Parameters From 14d2efc0a8fe36403b570cb7c0faab10494683bc Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Mon, 24 Mar 2025 16:09:04 +0100 Subject: [PATCH 45/64] Use logging instead --- src/imcflibs/imagej/bioformats.py | 5 +++-- src/imcflibs/imagej/misc.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/imcflibs/imagej/bioformats.py b/src/imcflibs/imagej/bioformats.py index 8d874c11..72aa867e 100644 --- a/src/imcflibs/imagej/bioformats.py +++ b/src/imcflibs/imagej/bioformats.py @@ -435,12 +435,13 @@ def get_stage_coords(source, filenames): z_interval = physSizeZ.value() if frame_size_z > 1 and physSizeZ is None: - print("no z calibration found, trying to recover") + log.debug("no z calibration found, trying to recover") first_plane = omeMeta.getPlanePositionZ(0, 0) next_plane_imagenumber = frame_size_c + frame_size_t - 1 second_plane = omeMeta.getPlanePositionZ(0, next_plane_imagenumber) z_interval = abs(abs(first_plane.value()) - abs(second_plane.value())) print("z-interval seems to be: " + str(z_interval)) + log.debug("z-interval seems to be: " + str(z_interval)) # create an image calibration image_calibration = [physSizeX.value(), physSizeY.value(), z_interval] @@ -488,7 +489,7 @@ def get_stage_coords(source, filenames): pos_y = current_position_y.value() if current_position_z is None: - print("the z-position is missing in the ome-xml metadata.") + log.debug( pos_z = 1.0 else: pos_z = current_position_z.value() diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 3dbfce13..e3774e83 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -698,7 +698,7 @@ def run_imarisconvert(file_path): file_path, file_path.replace(file_extension, ".ims"), ) - print("\n%s" % command) + log.debug("\n%s" % command) IJ.log("Converting to Imaris5 .ims...") subprocess.call(command, shell=True, cwd=imaris_path) IJ.log("Conversion to .ims is finished") From 0045e6594483270dcf33f3d4a016a37cec108753 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Mon, 24 Mar 2025 16:12:15 +0100 Subject: [PATCH 46/64] Format using Ruff --- src/imcflibs/imagej/bioformats.py | 41 ++++++++++++++++++------ src/imcflibs/imagej/labelimage.py | 24 ++++++++++---- src/imcflibs/imagej/misc.py | 7 +++- src/imcflibs/imagej/objects3d.py | 31 ++++++++++++------ src/imcflibs/imagej/omerotools.py | 43 ++++++++++++++++++------- src/imcflibs/imagej/processing.py | 53 +++++++++++++++++++------------ 6 files changed, 140 insertions(+), 59 deletions(-) diff --git a/src/imcflibs/imagej/bioformats.py b/src/imcflibs/imagej/bioformats.py index 72aa867e..9d7181c5 100644 --- a/src/imcflibs/imagej/bioformats.py +++ b/src/imcflibs/imagej/bioformats.py @@ -159,7 +159,9 @@ def export(imp, filename, overwrite=False): log.debug("Detected calibration unit: %s", unit) except Exception as err: log.error("Unable to detect spatial unit: %s", err) - raise RuntimeError("Error detecting image calibration: %s" % err) + raise RuntimeError( + "Error detecting image calibration: %s" % err + ) if unit == "pixel" and (suffix == "ics" or suffix == "ids"): log.warn( "Forcing unit to be 'm' instead of 'pixel' to avoid " @@ -176,7 +178,9 @@ def export(imp, filename, overwrite=False): log.debug("Exporting finished.") -def export_using_orig_name(imp, path, orig_name, tag, suffix, overwrite=False): +def export_using_orig_name( + imp, path, orig_name, tag, suffix, overwrite=False +): """Export an image to a given path, deriving the name from the input file. The input filename is stripped to its pure file name, without any path or @@ -438,15 +442,26 @@ def get_stage_coords(source, filenames): log.debug("no z calibration found, trying to recover") first_plane = omeMeta.getPlanePositionZ(0, 0) next_plane_imagenumber = frame_size_c + frame_size_t - 1 - second_plane = omeMeta.getPlanePositionZ(0, next_plane_imagenumber) - z_interval = abs(abs(first_plane.value()) - abs(second_plane.value())) - print("z-interval seems to be: " + str(z_interval)) + second_plane = omeMeta.getPlanePositionZ( + 0, next_plane_imagenumber + ) + z_interval = abs( + abs(first_plane.value()) - abs(second_plane.value()) + ) log.debug("z-interval seems to be: " + str(z_interval)) # create an image calibration - image_calibration = [physSizeX.value(), physSizeY.value(), z_interval] + image_calibration = [ + physSizeX.value(), + physSizeY.value(), + z_interval, + ] calibration_unit = physSizeX.unit().getSymbol() - image_dimensions_czt = [frame_size_c, frame_size_z, frame_size_t] + image_dimensions_czt = [ + frame_size_c, + frame_size_z, + frame_size_t, + ] reader.close() @@ -465,12 +480,14 @@ def get_stage_coords(source, filenames): physSizeX_max = ( physSizeX.value() - if physSizeX.value() >= omeMeta.getPixelsPhysicalSizeX(series).value() + if physSizeX.value() + >= omeMeta.getPixelsPhysicalSizeX(series).value() else omeMeta.getPixelsPhysicalSizeX(series).value() ) physSizeY_max = ( physSizeY.value() - if physSizeY.value() >= omeMeta.getPixelsPhysicalSizeY(series).value() + if physSizeY.value() + >= omeMeta.getPixelsPhysicalSizeY(series).value() else omeMeta.getPixelsPhysicalSizeY(series).value() ) if omeMeta.getPixelsPhysicalSizeZ(series): @@ -490,6 +507,8 @@ def get_stage_coords(source, filenames): if current_position_z is None: log.debug( + "the z-position is missing in the ome-xml metadata." + ) pos_z = 1.0 else: pos_z = current_position_z.value() @@ -512,7 +531,9 @@ def get_stage_coords(source, filenames): rel_pos_y = ( stage_coordinates_y[i] - stage_coordinates_y[0] ) / physSizeY.value() - rel_pos_z = (stage_coordinates_z[i] - stage_coordinates_z[0]) / z_interval + rel_pos_z = ( + stage_coordinates_z[i] - stage_coordinates_z[0] + ) / z_interval relative_coordinates_x_px.append(rel_pos_x) relative_coordinates_y_px.append(rel_pos_y) diff --git a/src/imcflibs/imagej/labelimage.py b/src/imcflibs/imagej/labelimage.py index 767161ae..0d32c0a5 100644 --- a/src/imcflibs/imagej/labelimage.py +++ b/src/imcflibs/imagej/labelimage.py @@ -31,7 +31,9 @@ def label_image_to_roi_list(label_image, low_thresh=None): max_value = 0 for slice in range(1, label_image.getNSlices() + 1): - label_image_slice = Duplicator().run(label_image, 1, 1, slice, slice, 1, 1) + label_image_slice = Duplicator().run( + label_image, 1, 1, slice, slice, 1, 1 + ) image_processor = label_image_slice.getProcessor() pixels = image_processor.getFloatArray() @@ -57,7 +59,9 @@ def label_image_to_roi_list(label_image, low_thresh=None): elif value == 0: continue # print(value) - float_processor.setThreshold(value, value, ImageProcessor.NO_LUT_UPDATE) + float_processor.setThreshold( + value, value, ImageProcessor.NO_LUT_UPDATE + ) roi = ThresholdToSelection.run(img_float_copy) roi.setName(str(value)) roi.setPosition(slice) @@ -94,7 +98,9 @@ def cookie_cut_labels(label_image_ref, label_image_to_relate): Prefs.blackBackground = True IJ.run(imp_dup, "Convert to Mask", "") IJ.run(imp_dup, "Divide...", "value=255") - return ImageCalculator.run(label_image_ref, imp_dup, "Multiply create") + return ImageCalculator.run( + label_image_ref, imp_dup, "Multiply create" + ) def relate_label_images(outer_label_imp, inner_label_imp): @@ -190,7 +196,9 @@ def measure_objects_size_shape_2d(label_image): return regions.process(label_image) -def binary_to_label(imp, title, min_thresh=1, min_vol=None, max_vol=None): +def binary_to_label( + imp, title, min_thresh=1, min_vol=None, max_vol=None +): """Segment a binary image to get a label image (2D/3D). Works on: 2D and 3D binary data. @@ -271,10 +279,14 @@ def dilate_labels_2d(imp, dilation_radius): # Iterate over each slice of the input ImagePlus for i in range(1, imp.getNSlices() + 1): # Duplicate the current slice - current_imp = Duplicator().run(imp, 1, 1, i, imp.getNSlices(), 1, 1) + current_imp = Duplicator().run( + imp, 1, 1, i, imp.getNSlices(), 1, 1 + ) # Perform a dilation of the labels in the current slice - dilated_labels_imp = li.dilateLabels(current_imp, dilation_radius) + dilated_labels_imp = li.dilateLabels( + current_imp, dilation_radius + ) # Append the dilated labels to the list dilated_labels_list.append(dilated_labels_imp) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index e3774e83..80f072ac 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -303,7 +303,12 @@ def timed_log(message, as_string=False): Flag to request the formatted string to be returned instead of printing it to the log. By default False. """ - formatted = time.strftime("%H:%M:%S", time.localtime()) + ": " + message + " " + formatted = ( + time.strftime("%H:%M:%S", time.localtime()) + + ": " + + message + + " " + ) if as_string: return formatted IJ.log(formatted) diff --git a/src/imcflibs/imagej/objects3d.py b/src/imcflibs/imagej/objects3d.py index c3122fe3..df2aecf8 100644 --- a/src/imcflibs/imagej/objects3d.py +++ b/src/imcflibs/imagej/objects3d.py @@ -5,8 +5,12 @@ [mcib3d]: https://mcib3d.frama.io/3d-suite-imagej/ """ -from de.mpicbg.scf.imgtools.image.create.image import ImageCreationUtilities -from de.mpicbg.scf.imgtools.image.create.labelmap import WatershedLabeling +from de.mpicbg.scf.imgtools.image.create.image import ( + ImageCreationUtilities, +) +from de.mpicbg.scf.imgtools.image.create.labelmap import ( + WatershedLabeling, +) from ij import IJ from mcib3d.geom import Objects3DPopulation from mcib3d.image3d import ImageHandler, ImageLabeller @@ -71,7 +75,9 @@ def imgplus_to_population3d(imp): return Objects3DPopulation(img) -def segment_3d_image(imp, title=None, min_thresh=1, min_vol=None, max_vol=None): +def segment_3d_image( + imp, title=None, min_thresh=1, min_vol=None, max_vol=None +): """Segment a 3D binary image to get a labelled stack. Parameters @@ -117,7 +123,9 @@ def segment_3d_image(imp, title=None, min_thresh=1, min_vol=None, max_vol=None): return seg.getImagePlus() -def get_objects_within_intensity(obj_pop, imp, min_intensity, max_intensity): +def get_objects_within_intensity( + obj_pop, imp, min_intensity, max_intensity +): """Filter a population for objects within the given intensity range. Parameters @@ -144,7 +152,10 @@ def get_objects_within_intensity(obj_pop, imp, min_intensity, max_intensity): # Calculate the mean intensity of the object mean_intensity = obj.getPixMeanValue(ImageHandler.wrap(imp)) # Check if the object is within the specified intensity range - if mean_intensity >= min_intensity and mean_intensity < max_intensity: + if ( + mean_intensity >= min_intensity + and mean_intensity < max_intensity + ): objects_within_intensity.append(obj) # Return the new population with the filtered objects @@ -152,8 +163,7 @@ def get_objects_within_intensity(obj_pop, imp, min_intensity, max_intensity): def maxima_finder_3d(imp, min_threshold=0, noise=100, rxy=1.5, rz=1.5): - """ - Find local maxima in a 3D image. + """Find local maxima in a 3D image. This function identifies local maxima in a 3D image using a specified minimum threshold and noise level. The radii for the maxima detection can be set independently for the x/y and z dimensions. @@ -205,8 +215,7 @@ def maxima_finder_3d(imp, min_threshold=0, noise=100, rxy=1.5, rz=1.5): def seeded_watershed(imp_binary, imp_peaks, threshold=10): - """ - Perform a seeded watershed segmentation on a binary image using seed points. + """Perform a seeded watershed segmentation on a binary image using seed points. This function applies a watershed segmentation to a binary image using seed points provided in another image. An optional threshold can be specified to control the segmentation process. @@ -230,7 +239,9 @@ def seeded_watershed(imp_binary, imp_peaks, threshold=10): img_seed = ImagePlusAdapter.convertFloat(imp_peaks).copy() if threshold: - watersheded_result = WatershedLabeling.watershed(img, img_seed, threshold) + watersheded_result = WatershedLabeling.watershed( + img, img_seed, threshold + ) else: watersheded_result = WatershedLabeling.watershed(img, img_seed) diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index f6eb3d21..92e48fc1 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -9,7 +9,10 @@ """ from fr.igred.omero import Client -from fr.igred.omero.annotations import MapAnnotationWrapper, TableWrapper +from fr.igred.omero.annotations import ( + MapAnnotationWrapper, + TableWrapper, +) from fr.igred.omero.roi import ROIWrapper from java.lang import Long from java.text import SimpleDateFormat @@ -54,23 +57,33 @@ def parse_url(client, omero_str): [ image for image in client.getDataset( - Long(part.split("dataset-")[1].split("/")[0]) + Long( + part.split("dataset-")[1].split( + "/" + )[0] + ) ).getImages() ] ) - dataset_id = Long(part.split("dataset-")[1].split("/")[0]) + dataset_id = Long( + part.split("dataset-")[1].split("/")[0] + ) dataset_ids.append(dataset_id) else: image_ids.extend( [ image for image in client.getDataset( - Long(omero_str.split("dataset-")[1].split("/")[0]) + Long( + omero_str.split("dataset-")[1].split("/")[0] + ) ).getImages() ] ) # If there is only one dataset - dataset_id = Long(omero_str.split("dataset-")[1].split("/")[0]) + dataset_id = Long( + omero_str.split("dataset-")[1].split("/")[0] + ) dataset_ids.append(dataset_id) # Get the images from the dataset @@ -84,10 +97,15 @@ def parse_url(client, omero_str): elif "image-" in omero_str: image_ids = omero_str.split("image-") image_ids.pop(0) - image_ids = [s.split("%")[0].replace("|", "") for s in image_ids] + image_ids = [ + s.split("%")[0].replace("|", "") for s in image_ids + ] else: image_ids = ( - [s.split("%")[0].replace("|", "") for s in omero_str.split("image-")[1:]] + [ + s.split("%")[0].replace("|", "") + for s in omero_str.split("image-")[1:] + ] if "image-" in omero_str else omero_str.split(",") ) @@ -166,7 +184,9 @@ def upload_image_to_omero(user_client, path, dataset_id): Long ID of the uploaded image """ - return user_client.getDataset(Long(dataset_id)).importImage(user_client, path)[0] + return user_client.getDataset(Long(dataset_id)).importImage( + user_client, path + )[0] def add_annotation(client, repository_wpr, annotations, header): @@ -341,8 +361,9 @@ def create_table_columns(headings): return table_columns -def upload_array_as_omero_table(user_client, table_title, data, columns, image_wpr): - """Upload a table to OMERO plus from a list of lists +def upload_array_as_omero_table( + user_client, table_title, data, columns, image_wpr +): Parameters ---------- @@ -365,7 +386,7 @@ def upload_array_as_omero_table(user_client, table_title, data, columns, image_w def save_rois_to_omero(user_client, image_wpr, rm): - """Save ROIs to OMERO linked to the image + """Save ROIs to OMERO linked to the image. Parameters ---------- diff --git a/src/imcflibs/imagej/processing.py b/src/imcflibs/imagej/processing.py index af503d64..83dc6257 100644 --- a/src/imcflibs/imagej/processing.py +++ b/src/imcflibs/imagej/processing.py @@ -2,9 +2,9 @@ from ..log import LOG as log -def apply_filter(imp, filter_method, filter_radius, do_3D=False): + def apply_filter(imp, filter_method, filter_radius, do_3d=False): - Make a specific filter followed by a threshold method of choice + """Make a specific filter followed by a threshold method of choice. Parameters ---------- @@ -28,9 +28,18 @@ def apply_filter(imp, filter_method, filter_radius, do_3d=False): ij.ImagePlus Filtered ImagePlus """ - log.info("Applying filter %s with radius %d" % (filter_method, filter_radius)) + log.info( + "Applying filter %s with radius %d" + % (filter_method, filter_radius) + ) - if filter_method not in ["Median", "Mean", "Gaussian Blur", "Minimum", "Maximum"]: + if filter_method not in [ + "Median", + "Mean", + "Gaussian Blur", + "Minimum", + "Maximum", + ]: raise ValueError( "filter_method must be one of: Median, Mean, Gaussian Blur, Minimum, Maximum" ) @@ -41,9 +50,9 @@ def apply_filter(imp, filter_method, filter_radius, do_3d=False): filter = filter_method + "..." options = ( - "sigma=" if filter_method == "Gaussian Blur" else "radius=" - + str(filter_radius) - + " stack" + "sigma=" + if filter_method == "Gaussian Blur" + else "radius=" + str(filter_radius) + " stack" ) log.debug("Filter: <%s> with options <%s>" % (filter, options)) @@ -53,9 +62,9 @@ def apply_filter(imp, filter_method, filter_radius, do_3d=False): return imageplus -def apply_background_subtraction(imp, rolling_ball_radius, do_3D=False): + def apply_background_subtraction(imp, rolling_ball_radius, do_3d=False): - Perform background subtraction using a rolling ball method + """Perform background subtraction using a rolling ball method. Parameters ---------- @@ -71,11 +80,14 @@ def apply_background_subtraction(imp, rolling_ball_radius, do_3d=False): ij.ImagePlus Filtered ImagePlus """ - log.info("Applying rolling ball with radius %d" % rolling_ball_radius) + log.info( + "Applying rolling ball with radius %d" % rolling_ball_radius + ) options = ( - "rolling=" + str(rolling_ball_radius) + "rolling=" + str(rolling_ball_radius) + " stack" if do_3d + else "" ) log.debug("Background subtraction options: %s" % options) @@ -85,9 +97,9 @@ def apply_background_subtraction(imp, rolling_ball_radius, do_3d=False): return imageplus -def apply_threshold(imp, threshold_method): + def apply_threshold(imp, threshold_method, do_3d=True): - Apply a threshold method to the input ImagePlus + """Apply a threshold method to the input ImagePlus. Parameters ---------- @@ -109,11 +121,7 @@ def apply_threshold(imp, threshold_method, do_3d=True): imageplus = imp.duplicate() auto_threshold_options = ( - threshold_method - + " " - + "dark" - + " " - + "stack" if do_3D else "" + threshold_method + " " + "dark" + " " + "stack" if do_3D else "" ) log.debug("Auto threshold options: %s" % auto_threshold_options) @@ -121,15 +129,18 @@ def apply_threshold(imp, threshold_method, do_3d=True): IJ.setAutoThreshold(imageplus, auto_threshold_options) convert_to_binary_options = ( - "method=" + threshold_method + "method=" + + threshold_method + " " + "background=Dark" + " " + "black" ) - log.debug("Convert to binary options: %s" % convert_to_binary_options) + log.debug( + "Convert to binary options: %s" % convert_to_binary_options + ) IJ.run(imageplus, "Convert to Mask", convert_to_binary_options) - return imageplus \ No newline at end of file + return imageplus From 0ff343341d2568979844a501b00c361b6eaa3999 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Mon, 24 Mar 2025 16:13:18 +0100 Subject: [PATCH 47/64] Add comments and notes to docstring, fix missing inputs --- src/imcflibs/imagej/labelimage.py | 13 ++++++--- src/imcflibs/imagej/misc.py | 44 ++++++++++++++++++++++++------- src/imcflibs/imagej/omerotools.py | 36 +++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/imcflibs/imagej/labelimage.py b/src/imcflibs/imagej/labelimage.py index 0d32c0a5..74991b91 100644 --- a/src/imcflibs/imagej/labelimage.py +++ b/src/imcflibs/imagej/labelimage.py @@ -104,10 +104,17 @@ def cookie_cut_labels(label_image_ref, label_image_to_relate): def relate_label_images(outer_label_imp, inner_label_imp): - """ - Associate two label images. + """Relate label images, giving the same label to objects belonging together. + + Given two label images, this function will create a new label image + with the same labels as the reference image, but with the objects + of the second image using the 3D Association plugin from the + 3DImageJSuite. - Uses the 3D Association plugin from the 3DImageJSuite. + Notes + ----- + Unlike `cookie_cut_labels`, this should work with touching labels by + using MereoTopology algorithms. Parameters ---------- diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 80f072ac..a38da080 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -453,10 +453,13 @@ def get_threshold_value_from_method(imp, method, ops): def write_ordereddict_to_csv(out_file, content): """Write data from a list of OrderedDicts to a CSV file. - This function writes data to a CSV file, preserving the order of columns - as defined in the OrderedDict objects. If the output file doesn't exist, - it creates a new file with a header row. If the file exists, it appends - the data without repeating the header. + In order to save the results of an analysis, looping over files, + making an OrderedDict help structure the data. If a list is made + containing all these OrderedDict, this function will write the data to + a CSV file, preserving the order of columns as defined in the + OrderedDict objects. If the output file doesn't exist, it creates a + new file with a header row. If the file exists, it appends the data + without repeating the header. Parameters ---------- @@ -464,7 +467,7 @@ def write_ordereddict_to_csv(out_file, content): Path to the output CSV file. content : list of OrderedDict List of OrderedDict objects representing the data rows to be written. - All dictionaries should have the same keys. + All dictionaries must have the same keys. Examples -------- @@ -477,9 +480,22 @@ def write_ordereddict_to_csv(out_file, content): The resulting CSV file will have the following content: - id;name;value - 1;Sample A;42.5 - 2;Sample B;37.2 + id;name;value + 1;Sample A;42.5 + 2;Sample B;37.2 + + >>> results = [] + >>> for i in range(1, 3): + ... results.append(OrderedDict([('id', i), ('name', f'Sample {chr(64+i)}'), ('value', 30 + i*7.5)])) + >>> write_ordereddict_to_csv('results.csv', results) + + The resulting CSV file will have the following content: + + + id;name;value + 1;Sample A;37.5 + 2;Sample B;45.0 + 3;Sample C;52.5 Notes ----- @@ -682,7 +698,17 @@ def locate_latest_imaris(paths_to_check=None): def run_imarisconvert(file_path): - """Convert a given file to Imaris5 .ims using ImarisConvert.exe via subprocess. + """Convert a given file to Imaris format using ImarisConvert. + + Convert the input image file to Imaris format (Imaris5) using the + ImarisConvert utility. The function uses the latest installed Imaris + application to perform the conversion using subprocess. + + Notes + ----- + The function handles special case for .ids files by converting them + to .ics before processing. It uses the latest installed Imaris + application to perform the conversion. Parameters ---------- diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index 92e48fc1..833f74e3 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -364,18 +364,54 @@ def create_table_columns(headings): def upload_array_as_omero_table( user_client, table_title, data, columns, image_wpr ): + """Upload a table to OMERO from a list of lists. Parameters ---------- user_client : fr.igred.omero.Client Client used for login to OMERO + table_title : str + Title of the table to be uploaded. data : list(list()) List of lists of results to upload columns : list(str) List of columns names image_wpr : fr.igred.omero.repositor.ImageWrapper Wrapper to the image to be uploaded + + Examples + -------- + >>> from fr.igred.omero import Client + >>> from java.lang import String, Double, Long + >>> + >>> # Connect to OMERO + >>> client = Client() + >>> client.connect("omero.example.org", 4064, "username", "password") + >>> + >>> # Get an image + >>> image_id = 123456 + >>> image_wpr = client.getImage(Long(image_id)) + >>> + >>> # Prepare column definitions (name-type pairs) + >>> columns = { + ... "Row_ID": Long, + ... "Cell_Area": Double, + ... "Cell_Type": String + ... } + >>> + >>> # Prepare data (list of rows, each row is a list of values) + >>> data = [ + ... [1, 250.5, "Neuron"], + ... [2, 180.2, "Astrocyte"], + ... [3, 310.7, "Neuron"] + ... ] + >>> + >>> # Upload the table + >>> upload_array_as_omero_table( + ... client, "Cell Measurements", data, columns, image_wpr + ... ) """ + dataset_wpr = image_wpr.getDatasets(user_client)[0] table_columns = create_table_columns(columns) From 74ffe426199c8bfeec033eca3c26faa4d0fe2b31 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Mon, 24 Mar 2025 16:13:43 +0100 Subject: [PATCH 48/64] Change output to inform if command has failed --- src/imcflibs/imagej/misc.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index a38da080..9a8ec48a 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -731,5 +731,8 @@ def run_imarisconvert(file_path): ) log.debug("\n%s" % command) IJ.log("Converting to Imaris5 .ims...") - subprocess.call(command, shell=True, cwd=imaris_path) - IJ.log("Conversion to .ims is finished") + result = subprocess.call(command, shell=True, cwd=imaris_path) + if result == 0: + IJ.log("Conversion to .ims is finished") + else: + IJ.log("Conversion failed with error code: %d" % result) From a094dc928e7cf4b466128b3dc7b5cf27e017678a Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Mon, 24 Mar 2025 16:13:59 +0100 Subject: [PATCH 49/64] Change method to return faster in case of empty list --- src/imcflibs/imagej/misc.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 9a8ec48a..2563936a 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -120,18 +120,20 @@ def calculate_mean_and_stdv(values_list, round_decimals=0): tuple of (float, float) Mean and standard deviation of the input list. """ - filtered_list = filter(None, values_list) + filtered_list = [x for x in values_list if x is not None] - try: - mean = round( - sum(filtered_list) / len(filtered_list), round_decimals - ) - except ZeroDivisionError: - mean = 0 - tot = 0.0 - for x in filtered_list: - tot = tot + (x - mean) ** 2 - return [mean, (tot / (len(filtered_list))) ** 0.5] + if not filtered_list: + return 0, 0 + + mean = round( + sum(filtered_list) / len(filtered_list), round_decimals + ) + variance = sum((x - mean) ** 2 for x in filtered_list) / len( + filtered_list + ) + std_dev = round(variance**0.5, round_decimals) + + return mean, std_dev def find_focus(imp): From 25ee6ed1b01a5b978a48624ee4b22f08aa783772 Mon Sep 17 00:00:00 2001 From: Laurent Guerard Date: Mon, 24 Mar 2025 16:30:13 +0100 Subject: [PATCH 50/64] Add module docstring for ImageJ processing utilities --- src/imcflibs/imagej/processing.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/imcflibs/imagej/processing.py b/src/imcflibs/imagej/processing.py index 83dc6257..00a718ef 100644 --- a/src/imcflibs/imagej/processing.py +++ b/src/imcflibs/imagej/processing.py @@ -1,3 +1,9 @@ +"""ImageJ processing utilities for filtering and thresholding images. + +This module provides functions to apply various image processing operations +using ImageJ, including filters, background subtraction, and thresholding. +""" + from ij import IJ from ..log import LOG as log From 00dc6413819de0f23ecf5ab02702cb45caeea450 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 15:20:47 +0100 Subject: [PATCH 51/64] Move minor implementation detail into code comment Unless this makes a real difference from the perspective of _using_ the function, it doesn't need to be in the docstring. --- src/imcflibs/imagej/misc.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 2563936a..74873be4 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -704,22 +704,15 @@ def run_imarisconvert(file_path): Convert the input image file to Imaris format (Imaris5) using the ImarisConvert utility. The function uses the latest installed Imaris - application to perform the conversion using subprocess. - - Notes - ----- - The function handles special case for .ids files by converting them - to .ics before processing. It uses the latest installed Imaris - application to perform the conversion. + application to perform the conversion via `subprocess.call()`. Parameters ---------- file_path : str Absolute path to the input image file. - - """ - + # in case the given file has the suffix `.ids` (meaning it is part of an + # ICS-1 `.ics`+`.ids` pair), point ImarisConvert to the `.ics` file instead: path_root, file_extension = os.path.splitext(file_path) if file_extension == ".ids": file_extension = ".ics" @@ -735,6 +728,6 @@ def run_imarisconvert(file_path): IJ.log("Converting to Imaris5 .ims...") result = subprocess.call(command, shell=True, cwd=imaris_path) if result == 0: - IJ.log("Conversion to .ims is finished") + IJ.log("Conversion to .ims is finished.") else: IJ.log("Conversion failed with error code: %d" % result) From 51248a486923e9a94bedc4e5f2477ce73418aede Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 15:24:35 +0100 Subject: [PATCH 52/64] Docstring cleanups --- src/imcflibs/imagej/omerotools.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index 833f74e3..a84a60e3 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -216,9 +216,9 @@ def delete_keyvalue_annotations(user_client, object_wrapper): Parameters ---------- user_client : fr.igred.omero.Client - Client used for login to OMERO + Client used for login to OMERO. object_wrapper : fr.igred.omero.repositor.GenericRepositoryObjectWrapper - Wrapper to the object for the anotation + Wrapper to the object for the anotation. """ kv_pairs = object_wrapper.getMapAnnotations(user_client) @@ -256,11 +256,13 @@ def get_acquisition_metadata(user_client, image_wpr): Returns ------- dict - Dictionary containing acquisition metadata: - - `objective_magnification`: Objective magnification - - `objective_na`: Objective NA - - `acquisition_date`: Acquisition date - - `acquisition_date_number`: Acquisition date as a number + + { + objective_magnification : float, + objective_na : float, + acquisition_date : str, + acquisition_date_number : str, + } """ ctx = user_client.getCtx() instrument_data = ( From d81e682a4ee3f7fe277ed583ad1d106c7fb1a687 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 15:24:49 +0100 Subject: [PATCH 53/64] Shorten docstring example --- src/imcflibs/imagej/omerotools.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index a84a60e3..ef2290f2 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -385,30 +385,24 @@ def upload_array_as_omero_table( -------- >>> from fr.igred.omero import Client >>> from java.lang import String, Double, Long - >>> - >>> # Connect to OMERO - >>> client = Client() + ... + >>> client = Client() # connect to OMERO >>> client.connect("omero.example.org", 4064, "username", "password") - >>> - >>> # Get an image - >>> image_id = 123456 - >>> image_wpr = client.getImage(Long(image_id)) - >>> - >>> # Prepare column definitions (name-type pairs) - >>> columns = { + ... + >>> image_wpr = client.getImage(Long(123456)) # get an image + ... + >>> columns = { # prepare column definitions (name-type pairs) ... "Row_ID": Long, ... "Cell_Area": Double, - ... "Cell_Type": String + ... "Cell_Type": String, ... } - >>> - >>> # Prepare data (list of rows, each row is a list of values) - >>> data = [ + ... + >>> data = [ # prepare data (list of rows, each row is a list of values) ... [1, 250.5, "Neuron"], ... [2, 180.2, "Astrocyte"], - ... [3, 310.7, "Neuron"] + ... [3, 310.7, "Neuron"], ... ] - >>> - >>> # Upload the table + ... >>> upload_array_as_omero_table( ... client, "Cell Measurements", data, columns, image_wpr ... ) From 6b641bb21b6176489ae3d3c014b096bde671efef Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 15:42:21 +0100 Subject: [PATCH 54/64] Indent to show as a fixed-width block --- src/imcflibs/imagej/misc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 74873be4..d07e8af9 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -482,9 +482,9 @@ def write_ordereddict_to_csv(out_file, content): The resulting CSV file will have the following content: - id;name;value - 1;Sample A;42.5 - 2;Sample B;37.2 + id;name;value + 1;Sample A;42.5 + 2;Sample B;37.2 >>> results = [] >>> for i in range(1, 3): From 51f2cec2ec16fa4bb63c4b57f2978adec7e12f80 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 15:45:47 +0100 Subject: [PATCH 55/64] One example should be enough As the 2nd example was not using the function in a different way, there is no real need for it. If it's worth having the example with the `range()`, `append` etc., we should make it the only one (and also make it valid Python 2.7 syntax, i.e. no f-strings). --- src/imcflibs/imagej/misc.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index d07e8af9..2b4dca9e 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -486,19 +486,6 @@ def write_ordereddict_to_csv(out_file, content): 1;Sample A;42.5 2;Sample B;37.2 - >>> results = [] - >>> for i in range(1, 3): - ... results.append(OrderedDict([('id', i), ('name', f'Sample {chr(64+i)}'), ('value', 30 + i*7.5)])) - >>> write_ordereddict_to_csv('results.csv', results) - - The resulting CSV file will have the following content: - - - id;name;value - 1;Sample A;37.5 - 2;Sample B;45.0 - 3;Sample C;52.5 - Notes ----- - Uses the semicolon charachter (`;`) as delimiter. From a65fe410d5f9fe2bfa670a6c5bcc276778828ae8 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 15:46:30 +0100 Subject: [PATCH 56/64] Add empty line to fix docstring rendering --- src/imcflibs/pathtools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/imcflibs/pathtools.py b/src/imcflibs/pathtools.py index bd8965e5..7f738b6d 100644 --- a/src/imcflibs/pathtools.py +++ b/src/imcflibs/pathtools.py @@ -38,6 +38,7 @@ def parse_path(path, prefix=""): dict The parsed (and possibly combined) path split into its components, with the following keys: + - `orig` : The full string as passed into this function (possibly combined with the prefix in case one was specified). - `full` : The same as `orig` with separators adjusted to the current From c35e312e37c41ea477885a7da4f125b844c27161 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 15:49:24 +0100 Subject: [PATCH 57/64] Run black formatting (restore default line length of 88) --- src/imcflibs/imagej/bioformats.py | 30 +++++----------- src/imcflibs/imagej/labelimage.py | 24 ++++--------- src/imcflibs/imagej/misc.py | 57 ++++++++----------------------- src/imcflibs/imagej/objects3d.py | 17 +++------ src/imcflibs/imagej/omerotools.py | 39 +++++---------------- src/imcflibs/imagej/processing.py | 26 +++----------- src/imcflibs/imagej/shading.py | 6 ++-- src/imcflibs/pathtools.py | 12 ++----- 8 files changed, 51 insertions(+), 160 deletions(-) diff --git a/src/imcflibs/imagej/bioformats.py b/src/imcflibs/imagej/bioformats.py index 9d7181c5..45e1b84d 100644 --- a/src/imcflibs/imagej/bioformats.py +++ b/src/imcflibs/imagej/bioformats.py @@ -159,9 +159,7 @@ def export(imp, filename, overwrite=False): log.debug("Detected calibration unit: %s", unit) except Exception as err: log.error("Unable to detect spatial unit: %s", err) - raise RuntimeError( - "Error detecting image calibration: %s" % err - ) + raise RuntimeError("Error detecting image calibration: %s" % err) if unit == "pixel" and (suffix == "ics" or suffix == "ids"): log.warn( "Forcing unit to be 'm' instead of 'pixel' to avoid " @@ -178,9 +176,7 @@ def export(imp, filename, overwrite=False): log.debug("Exporting finished.") -def export_using_orig_name( - imp, path, orig_name, tag, suffix, overwrite=False -): +def export_using_orig_name(imp, path, orig_name, tag, suffix, overwrite=False): """Export an image to a given path, deriving the name from the input file. The input filename is stripped to its pure file name, without any path or @@ -442,12 +438,8 @@ def get_stage_coords(source, filenames): log.debug("no z calibration found, trying to recover") first_plane = omeMeta.getPlanePositionZ(0, 0) next_plane_imagenumber = frame_size_c + frame_size_t - 1 - second_plane = omeMeta.getPlanePositionZ( - 0, next_plane_imagenumber - ) - z_interval = abs( - abs(first_plane.value()) - abs(second_plane.value()) - ) + second_plane = omeMeta.getPlanePositionZ(0, next_plane_imagenumber) + z_interval = abs(abs(first_plane.value()) - abs(second_plane.value())) log.debug("z-interval seems to be: " + str(z_interval)) # create an image calibration @@ -480,14 +472,12 @@ def get_stage_coords(source, filenames): physSizeX_max = ( physSizeX.value() - if physSizeX.value() - >= omeMeta.getPixelsPhysicalSizeX(series).value() + if physSizeX.value() >= omeMeta.getPixelsPhysicalSizeX(series).value() else omeMeta.getPixelsPhysicalSizeX(series).value() ) physSizeY_max = ( physSizeY.value() - if physSizeY.value() - >= omeMeta.getPixelsPhysicalSizeY(series).value() + if physSizeY.value() >= omeMeta.getPixelsPhysicalSizeY(series).value() else omeMeta.getPixelsPhysicalSizeY(series).value() ) if omeMeta.getPixelsPhysicalSizeZ(series): @@ -506,9 +496,7 @@ def get_stage_coords(source, filenames): pos_y = current_position_y.value() if current_position_z is None: - log.debug( - "the z-position is missing in the ome-xml metadata." - ) + log.debug("the z-position is missing in the ome-xml metadata.") pos_z = 1.0 else: pos_z = current_position_z.value() @@ -531,9 +519,7 @@ def get_stage_coords(source, filenames): rel_pos_y = ( stage_coordinates_y[i] - stage_coordinates_y[0] ) / physSizeY.value() - rel_pos_z = ( - stage_coordinates_z[i] - stage_coordinates_z[0] - ) / z_interval + rel_pos_z = (stage_coordinates_z[i] - stage_coordinates_z[0]) / z_interval relative_coordinates_x_px.append(rel_pos_x) relative_coordinates_y_px.append(rel_pos_y) diff --git a/src/imcflibs/imagej/labelimage.py b/src/imcflibs/imagej/labelimage.py index 74991b91..470f5107 100644 --- a/src/imcflibs/imagej/labelimage.py +++ b/src/imcflibs/imagej/labelimage.py @@ -31,9 +31,7 @@ def label_image_to_roi_list(label_image, low_thresh=None): max_value = 0 for slice in range(1, label_image.getNSlices() + 1): - label_image_slice = Duplicator().run( - label_image, 1, 1, slice, slice, 1, 1 - ) + label_image_slice = Duplicator().run(label_image, 1, 1, slice, slice, 1, 1) image_processor = label_image_slice.getProcessor() pixels = image_processor.getFloatArray() @@ -59,9 +57,7 @@ def label_image_to_roi_list(label_image, low_thresh=None): elif value == 0: continue # print(value) - float_processor.setThreshold( - value, value, ImageProcessor.NO_LUT_UPDATE - ) + float_processor.setThreshold(value, value, ImageProcessor.NO_LUT_UPDATE) roi = ThresholdToSelection.run(img_float_copy) roi.setName(str(value)) roi.setPosition(slice) @@ -98,9 +94,7 @@ def cookie_cut_labels(label_image_ref, label_image_to_relate): Prefs.blackBackground = True IJ.run(imp_dup, "Convert to Mask", "") IJ.run(imp_dup, "Divide...", "value=255") - return ImageCalculator.run( - label_image_ref, imp_dup, "Multiply create" - ) + return ImageCalculator.run(label_image_ref, imp_dup, "Multiply create") def relate_label_images(outer_label_imp, inner_label_imp): @@ -203,9 +197,7 @@ def measure_objects_size_shape_2d(label_image): return regions.process(label_image) -def binary_to_label( - imp, title, min_thresh=1, min_vol=None, max_vol=None -): +def binary_to_label(imp, title, min_thresh=1, min_vol=None, max_vol=None): """Segment a binary image to get a label image (2D/3D). Works on: 2D and 3D binary data. @@ -286,14 +278,10 @@ def dilate_labels_2d(imp, dilation_radius): # Iterate over each slice of the input ImagePlus for i in range(1, imp.getNSlices() + 1): # Duplicate the current slice - current_imp = Duplicator().run( - imp, 1, 1, i, imp.getNSlices(), 1, 1 - ) + current_imp = Duplicator().run(imp, 1, 1, i, imp.getNSlices(), 1, 1) # Perform a dilation of the labels in the current slice - dilated_labels_imp = li.dilateLabels( - current_imp, dilation_radius - ) + dilated_labels_imp = li.dilateLabels(current_imp, dilation_radius) # Append the dilated labels to the list dilated_labels_list.append(dilated_labels_imp) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 2b4dca9e..467fd9bb 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -43,9 +43,7 @@ def show_progress(cur, final): ----- `ij.IJ.showProgress` internally increments the given `cur` value by 1. """ - log.info( - "Progress: %s / %s (%s)", cur + 1, final, (1.0 + cur) / final - ) + log.info("Progress: %s / %s (%s)", cur + 1, final, (1.0 + cur) / final) IJ.showProgress(cur, final) @@ -82,9 +80,7 @@ def elapsed_time_since(start, end=None): hours, rem = divmod(end - start, 3600) minutes, seconds = divmod(rem, 60) - return "{:0>2}:{:0>2}:{:05.2f}".format( - int(hours), int(minutes), seconds - ) + return "{:0>2}:{:0>2}:{:05.2f}".format(int(hours), int(minutes), seconds) def percentage(part, whole): @@ -125,13 +121,9 @@ def calculate_mean_and_stdv(values_list, round_decimals=0): if not filtered_list: return 0, 0 - mean = round( - sum(filtered_list) / len(filtered_list), round_decimals - ) - variance = sum((x - mean) ** 2 for x in filtered_list) / len( - filtered_list - ) - std_dev = round(variance**0.5, round_decimals) + mean = round(sum(filtered_list) / len(filtered_list), round_decimals) + variance = sum((x - mean) ** 2 for x in filtered_list) / len(filtered_list) + std_dev = round(variance ** 0.5, round_decimals) return mean, std_dev @@ -168,9 +160,7 @@ def find_focus(imp): # Check if more than 1 channel # FUTURE Could be improved for multi channel if imp_dimensions[2] != 1: - sys.exit( - "Image has more than one channel, please reduce dimensionality" - ) + sys.exit("Image has more than one channel, please reduce dimensionality") # Loop through each time point for plane in range(1, imp_dimensions[4] + 1): @@ -186,9 +176,7 @@ def find_focus(imp): # pix_array = pix_array*pix_array sumpix_array = sum(pix_array) - var = sumpix_array / ( - imp_dimensions[0] * imp_dimensions[1] * mean - ) + var = sumpix_array / (imp_dimensions[0] * imp_dimensions[1] * mean) if var > norm_var: norm_var = var @@ -217,14 +205,10 @@ def send_mail(job_name, recipient, filename, total_execution_time): # Ensure the sender and server are configured from Prefs if not sender: - log.info( - "Sender email is not configured. Please check IJ_Prefs.txt." - ) + log.info("Sender email is not configured. Please check IJ_Prefs.txt.") return if not server: - log.info( - "SMTP server is not configured. Please check IJ_Prefs.txt." - ) + log.info("SMTP server is not configured. Please check IJ_Prefs.txt.") return # Ensure the recipient is provided @@ -305,12 +289,7 @@ def timed_log(message, as_string=False): Flag to request the formatted string to be returned instead of printing it to the log. By default False. """ - formatted = ( - time.strftime("%H:%M:%S", time.localtime()) - + ": " - + message - + " " - ) + formatted = time.strftime("%H:%M:%S", time.localtime()) + ": " + message + " " if as_string: return formatted IJ.log(formatted) @@ -497,23 +476,17 @@ def write_ordereddict_to_csv(out_file, content): if not os.path.exists(out_file): # If the file does not exist, create it and write the header with open(out_file, "wb") as f: - dict_writer = csv.DictWriter( - f, content[0].keys(), delimiter=";" - ) + dict_writer = csv.DictWriter(f, content[0].keys(), delimiter=";") dict_writer.writeheader() dict_writer.writerows(content) else: # If the file exists, append the results with open(out_file, "ab") as f: - dict_writer = csv.DictWriter( - f, content[0].keys(), delimiter=";" - ) + dict_writer = csv.DictWriter(f, content[0].keys(), delimiter=";") dict_writer.writerows(content) -def save_image_in_format( - imp, format, out_dir, series, pad_number, split_channels -): +def save_image_in_format(imp, format, out_dir, series, pad_number, split_channels): """Save an ImagePlus object in the specified format. This function provides flexible options for saving ImageJ images in various @@ -596,9 +569,7 @@ def save_image_in_format( imp.getNFrames(), ) ) - dir_to_save.append( - os.path.join(out_dir, "C" + str(channel)) - ) + dir_to_save.append(os.path.join(out_dir, "C" + str(channel))) else: imp_to_use.append(imp) dir_to_save.append(out_dir) diff --git a/src/imcflibs/imagej/objects3d.py b/src/imcflibs/imagej/objects3d.py index df2aecf8..8388e8c2 100644 --- a/src/imcflibs/imagej/objects3d.py +++ b/src/imcflibs/imagej/objects3d.py @@ -75,9 +75,7 @@ def imgplus_to_population3d(imp): return Objects3DPopulation(img) -def segment_3d_image( - imp, title=None, min_thresh=1, min_vol=None, max_vol=None -): +def segment_3d_image(imp, title=None, min_thresh=1, min_vol=None, max_vol=None): """Segment a 3D binary image to get a labelled stack. Parameters @@ -123,9 +121,7 @@ def segment_3d_image( return seg.getImagePlus() -def get_objects_within_intensity( - obj_pop, imp, min_intensity, max_intensity -): +def get_objects_within_intensity(obj_pop, imp, min_intensity, max_intensity): """Filter a population for objects within the given intensity range. Parameters @@ -152,10 +148,7 @@ def get_objects_within_intensity( # Calculate the mean intensity of the object mean_intensity = obj.getPixMeanValue(ImageHandler.wrap(imp)) # Check if the object is within the specified intensity range - if ( - mean_intensity >= min_intensity - and mean_intensity < max_intensity - ): + if mean_intensity >= min_intensity and mean_intensity < max_intensity: objects_within_intensity.append(obj) # Return the new population with the filtered objects @@ -239,9 +232,7 @@ def seeded_watershed(imp_binary, imp_peaks, threshold=10): img_seed = ImagePlusAdapter.convertFloat(imp_peaks).copy() if threshold: - watersheded_result = WatershedLabeling.watershed( - img, img_seed, threshold - ) + watersheded_result = WatershedLabeling.watershed(img, img_seed, threshold) else: watersheded_result = WatershedLabeling.watershed(img, img_seed) diff --git a/src/imcflibs/imagej/omerotools.py b/src/imcflibs/imagej/omerotools.py index ef2290f2..2e2c4325 100644 --- a/src/imcflibs/imagej/omerotools.py +++ b/src/imcflibs/imagej/omerotools.py @@ -57,33 +57,23 @@ def parse_url(client, omero_str): [ image for image in client.getDataset( - Long( - part.split("dataset-")[1].split( - "/" - )[0] - ) + Long(part.split("dataset-")[1].split("/")[0]) ).getImages() ] ) - dataset_id = Long( - part.split("dataset-")[1].split("/")[0] - ) + dataset_id = Long(part.split("dataset-")[1].split("/")[0]) dataset_ids.append(dataset_id) else: image_ids.extend( [ image for image in client.getDataset( - Long( - omero_str.split("dataset-")[1].split("/")[0] - ) + Long(omero_str.split("dataset-")[1].split("/")[0]) ).getImages() ] ) # If there is only one dataset - dataset_id = Long( - omero_str.split("dataset-")[1].split("/")[0] - ) + dataset_id = Long(omero_str.split("dataset-")[1].split("/")[0]) dataset_ids.append(dataset_id) # Get the images from the dataset @@ -97,15 +87,10 @@ def parse_url(client, omero_str): elif "image-" in omero_str: image_ids = omero_str.split("image-") image_ids.pop(0) - image_ids = [ - s.split("%")[0].replace("|", "") for s in image_ids - ] + image_ids = [s.split("%")[0].replace("|", "") for s in image_ids] else: image_ids = ( - [ - s.split("%")[0].replace("|", "") - for s in omero_str.split("image-")[1:] - ] + [s.split("%")[0].replace("|", "") for s in omero_str.split("image-")[1:]] if "image-" in omero_str else omero_str.split(",") ) @@ -184,9 +169,7 @@ def upload_image_to_omero(user_client, path, dataset_id): Long ID of the uploaded image """ - return user_client.getDataset(Long(dataset_id)).importImage( - user_client, path - )[0] + return user_client.getDataset(Long(dataset_id)).importImage(user_client, path)[0] def add_annotation(client, repository_wpr, annotations, header): @@ -299,9 +282,7 @@ def get_acquisition_metadata(user_client, image_wpr): metadata["acquisition_date_number"] = 0 else: sdf = SimpleDateFormat("yyyy-MM-dd") - metadata["acquisition_date"] = sdf.format( - image_wpr.getAcquisitionDate() - ) + metadata["acquisition_date"] = sdf.format(image_wpr.getAcquisitionDate()) metadata["acquisition_date_number"] = int( metadata["acquisition_date"].replace("-", "") ) @@ -363,9 +344,7 @@ def create_table_columns(headings): return table_columns -def upload_array_as_omero_table( - user_client, table_title, data, columns, image_wpr -): +def upload_array_as_omero_table(user_client, table_title, data, columns, image_wpr): """Upload a table to OMERO from a list of lists. Parameters diff --git a/src/imcflibs/imagej/processing.py b/src/imcflibs/imagej/processing.py index 00a718ef..c50e9c5f 100644 --- a/src/imcflibs/imagej/processing.py +++ b/src/imcflibs/imagej/processing.py @@ -34,10 +34,7 @@ def apply_filter(imp, filter_method, filter_radius, do_3d=False): ij.ImagePlus Filtered ImagePlus """ - log.info( - "Applying filter %s with radius %d" - % (filter_method, filter_radius) - ) + log.info("Applying filter %s with radius %d" % (filter_method, filter_radius)) if filter_method not in [ "Median", @@ -86,15 +83,9 @@ def apply_background_subtraction(imp, rolling_ball_radius, do_3d=False): ij.ImagePlus Filtered ImagePlus """ - log.info( - "Applying rolling ball with radius %d" % rolling_ball_radius - ) + log.info("Applying rolling ball with radius %d" % rolling_ball_radius) - options = ( - "rolling=" + str(rolling_ball_radius) + " stack" - if do_3d - else "" - ) + options = "rolling=" + str(rolling_ball_radius) + " stack" if do_3d else "" log.debug("Background subtraction options: %s" % options) @@ -135,17 +126,10 @@ def apply_threshold(imp, threshold_method, do_3d=True): IJ.setAutoThreshold(imageplus, auto_threshold_options) convert_to_binary_options = ( - "method=" - + threshold_method - + " " - + "background=Dark" - + " " - + "black" + "method=" + threshold_method + " " + "background=Dark" + " " + "black" ) - log.debug( - "Convert to binary options: %s" % convert_to_binary_options - ) + log.debug("Convert to binary options: %s" % convert_to_binary_options) IJ.run(imageplus, "Convert to Mask", convert_to_binary_options) diff --git a/src/imcflibs/imagej/shading.py b/src/imcflibs/imagej/shading.py index 02a11af1..1e3a62fc 100644 --- a/src/imcflibs/imagej/shading.py +++ b/src/imcflibs/imagej/shading.py @@ -182,6 +182,7 @@ def process_files(files, outpath, model_file, fmt): if model: model.close() + def simple_flatfield_correction(imp, sigma=20.0): """Perform a simple flatfield correction to a given ImagePlus stack. @@ -205,10 +206,7 @@ def simple_flatfield_correction(imp, sigma=20.0): # Normalize image to the highest value of original (requires 32-bit image) IJ.run(flatfield, "32-bit", "") - IJ.run( - flatfield, - "Divide...", - "value=" + str(stats.max)) + IJ.run(flatfield, "Divide...", "value=" + str(stats.max)) ic = ImageCalculator() flatfield_corrected = ic.run("Divide create", imp, flatfield) diff --git a/src/imcflibs/pathtools.py b/src/imcflibs/pathtools.py index 7f738b6d..166a5186 100644 --- a/src/imcflibs/pathtools.py +++ b/src/imcflibs/pathtools.py @@ -180,9 +180,7 @@ def jython_fiji_exists(path): return False -def listdir_matching( - path, suffix, fullpath=False, sort=False, regex=False -): +def listdir_matching(path, suffix, fullpath=False, sort=False, regex=False): """Get a list of files in a directory matching a given suffix. Parameters @@ -298,13 +296,9 @@ def derive_out_dir(in_dir, out_dir): """ if out_dir.upper() in ["-", "NONE"]: out_dir = in_dir - log.info( - "No output directory given, using input dir [%s].", out_dir - ) + log.info("No output directory given, using input dir [%s].", out_dir) else: - log.info( - "Using directory [%s] for results and temp files.", out_dir - ) + log.info("Using directory [%s] for results and temp files.", out_dir) return out_dir From caa1370ca6a6f8b07174d82e8e0631e20d097089 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 15:59:59 +0100 Subject: [PATCH 58/64] Notes section comes before Examples https://numpydoc.readthedocs.io/en/latest/format.html#sections --- src/imcflibs/imagej/misc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 467fd9bb..453f6791 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -450,6 +450,12 @@ def write_ordereddict_to_csv(out_file, content): List of OrderedDict objects representing the data rows to be written. All dictionaries must have the same keys. + Notes + ----- + - Uses the semicolon charachter (`;`) as delimiter. + - When appending to an existing file, the column structure has to match. + - Output file is opened in binary mode for compatibility. + Examples -------- >>> from collections import OrderedDict @@ -464,12 +470,6 @@ def write_ordereddict_to_csv(out_file, content): id;name;value 1;Sample A;42.5 2;Sample B;37.2 - - Notes - ----- - - Uses the semicolon charachter (`;`) as delimiter. - - When appending to an existing file, the column structure has to match. - - Output file is opened in binary mode for compatibility. """ # Check if the output file exists From 99ab0f0815cbdce58be166cd516cab35acc2c20a Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 16:01:25 +0100 Subject: [PATCH 59/64] Minor updates on Notes section --- src/imcflibs/imagej/misc.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 453f6791..5718ff1f 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -452,9 +452,10 @@ def write_ordereddict_to_csv(out_file, content): Notes ----- - - Uses the semicolon charachter (`;`) as delimiter. - - When appending to an existing file, the column structure has to match. - - Output file is opened in binary mode for compatibility. + - The CSV file will use the semicolon charachter (`;`) as delimiter. + - When appending to an existing file, the column structure has to match. No + sanity checking is being done on this by the function! + - The output file is opened in binary mode for compatibility. Examples -------- From 07f3064dc923c7cd46d234eaeaa578ccedd71678 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 16:02:03 +0100 Subject: [PATCH 60/64] Update function details --- src/imcflibs/imagej/misc.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/imcflibs/imagej/misc.py b/src/imcflibs/imagej/misc.py index 5718ff1f..704abbff 100644 --- a/src/imcflibs/imagej/misc.py +++ b/src/imcflibs/imagej/misc.py @@ -434,13 +434,13 @@ def get_threshold_value_from_method(imp, method, ops): def write_ordereddict_to_csv(out_file, content): """Write data from a list of OrderedDicts to a CSV file. - In order to save the results of an analysis, looping over files, - making an OrderedDict help structure the data. If a list is made - containing all these OrderedDict, this function will write the data to - a CSV file, preserving the order of columns as defined in the - OrderedDict objects. If the output file doesn't exist, it creates a - new file with a header row. If the file exists, it appends the data - without repeating the header. + When performing measurements in an analysis that is e.g. looping over + multiple files, it's useful to keep the results in `OrderedDict` objects, + e.g. one per analyzed file / dataset. This function can be used to create a + CSV file (or append to an existing one) from a list of `OrderedDict`s. The + structure inside the dicts is entirely up to the calling code (i.e. it's not + related to ImageJ's *Results* window or such), the only requirement is + type-consistency among all the `OrderedDict`s provided to the function. Parameters ---------- From 433308686721fa0cfafc1d7dcdaf40f380e5c339 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 16:15:27 +0100 Subject: [PATCH 61/64] Place Notes section after Returns https://numpydoc.readthedocs.io/en/latest/format.html#sections --- src/imcflibs/imagej/labelimage.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/imcflibs/imagej/labelimage.py b/src/imcflibs/imagej/labelimage.py index 470f5107..22b5dc37 100644 --- a/src/imcflibs/imagej/labelimage.py +++ b/src/imcflibs/imagej/labelimage.py @@ -105,11 +105,6 @@ def relate_label_images(outer_label_imp, inner_label_imp): of the second image using the 3D Association plugin from the 3DImageJSuite. - Notes - ----- - Unlike `cookie_cut_labels`, this should work with touching labels by - using MereoTopology algorithms. - Parameters ---------- outer_label_imp : ij.ImagePlus @@ -121,6 +116,11 @@ def relate_label_images(outer_label_imp, inner_label_imp): ------- related_inner_imp : ij.ImagePlus The related inner label image + + Notes + ----- + Unlike `cookie_cut_labels`, this should work with touching labels by + using MereoTopology algorithms. """ outer_label_imp.show() From ec158ff901ca3c1b2a395935dc57c0caa2e636df Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 16:15:45 +0100 Subject: [PATCH 62/64] Docstring conventions --- src/imcflibs/imagej/labelimage.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/imcflibs/imagej/labelimage.py b/src/imcflibs/imagej/labelimage.py index 22b5dc37..15481e14 100644 --- a/src/imcflibs/imagej/labelimage.py +++ b/src/imcflibs/imagej/labelimage.py @@ -100,27 +100,26 @@ def cookie_cut_labels(label_image_ref, label_image_to_relate): def relate_label_images(outer_label_imp, inner_label_imp): """Relate label images, giving the same label to objects belonging together. - Given two label images, this function will create a new label image - with the same labels as the reference image, but with the objects - of the second image using the 3D Association plugin from the - 3DImageJSuite. + Given two label images, this function will create a new label image with the + same labels as the reference image, but with the objects of the second image + using the 3D Association plugin from the 3DImageJSuite. Parameters ---------- outer_label_imp : ij.ImagePlus - The outer label image + The outer label image. inner_label_imp : ij.ImagePlus - The inner label image + The inner label image. Returns ------- related_inner_imp : ij.ImagePlus - The related inner label image + The related inner label image. Notes ----- - Unlike `cookie_cut_labels`, this should work with touching labels by - using MereoTopology algorithms. + Unlike `cookie_cut_labels`, this should work with touching labels by using + MereoTopology algorithms. """ outer_label_imp.show() From 047d913e9bac4d5922c68219986e3ad63b55d1cd Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 16:24:55 +0100 Subject: [PATCH 63/64] Docstring conventions --- src/imcflibs/imagej/bioformats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/imcflibs/imagej/bioformats.py b/src/imcflibs/imagej/bioformats.py index 45e1b84d..5c3816f3 100644 --- a/src/imcflibs/imagej/bioformats.py +++ b/src/imcflibs/imagej/bioformats.py @@ -361,9 +361,9 @@ def get_stage_coords(source, filenames): Parameters ---------- source : str - Path to the images + Path to the images. filenames : list of str - List of images filenames + List of images filenames. Returns ------- From 98cfaffb65cb5d4b62bd45dbf15ba7a0b3457942 Mon Sep 17 00:00:00 2001 From: Niko Ehrenfeuchter Date: Tue, 25 Mar 2025 16:34:06 +0100 Subject: [PATCH 64/64] Reformat dict in Returns section Tries to address the issue discussed in https://github.com/imcf/python-imcflibs/pull/49#discussion_r2003514256 --- src/imcflibs/imagej/bioformats.py | 40 ++++++++++++------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/src/imcflibs/imagej/bioformats.py b/src/imcflibs/imagej/bioformats.py index 5c3816f3..758cc072 100644 --- a/src/imcflibs/imagej/bioformats.py +++ b/src/imcflibs/imagej/bioformats.py @@ -368,31 +368,21 @@ def get_stage_coords(source, filenames): Returns ------- dict - A dictionary containing the following metadata: - - `dimensions` : int - Number of dimensions (2D or 3D) - - `stage_coordinates_x` : list - The absolute stage x-coordinated - - `stage_coordinates_y` : list - The absolute stage y-coordinated - - `stage_coordinates_z` : list - The absolute stage z-coordinated - - `relative_coordinates_x` : list - The relative stage x-coordinates in px - - `relative_coordinates_y` : list - The relative stage y-coordinates in px - - `relative_coordinates_z` : list - The relative stage z-coordinates in px - - `image_calibration` : list - x,y,z image calibration in unit/px - - `calibration_unit` : str - Image calibration unit - - `image_dimensions_czt` : list - Number of images in dimensions c,z,t - - `series_names` : list of str - Names of all series contained in the files - - `max_size` : list of int - Maximum size across all files in dimensions x,y,z + + { + dimensions : int, # number of dimensions (2D or 3D) + stage_coordinates_x : list, # absolute stage x-coordinated + stage_coordinates_y : list, # absolute stage y-coordinated + stage_coordinates_z : list, # absolute stage z-coordinated + relative_coordinates_x : list, # relative stage x-coordinates in px + relative_coordinates_y : list, # relative stage y-coordinates in px + relative_coordinates_z : list, # relative stage z-coordinates in px + image_calibration : list, # x,y,z image calibration in unit/px + calibration_unit : str, # image calibration unit + image_dimensions_czt : list, # number of images in dimensions c,z,t + series_names : list of str, # names of all series in the files + max_size : list of int, # max size (x/y/z) across all files + } """ # open an array to store the abosolute stage coordinates from metadata