From 40a4893e312bcaa771ecaa99764791bb92054de7 Mon Sep 17 00:00:00 2001 From: Kai Schleicher Date: Wed, 28 Oct 2026 16:33:14 +0100 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=90=9B=20Import=20the=20BIOP=20fusion?= =?UTF-8?q?=20command=20only=20in=20fuse=5Fdataset=5Fbdvp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit imcflibs.imagej.bdv could not be imported in a Fiji without the PTBIOP update site (ImportError: No module named scijava), although only fuse_dataset_bdvp needs the BIOP command. Co-Authored-By: Claude Opus 5.5 --- src/imcflibs/imagej/bdv.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/imcflibs/imagej/bdv.py b/src/imcflibs/imagej/bdv.py index 63815b27..ba80f6e2 100644 --- a/src/imcflibs/imagej/bdv.py +++ b/src/imcflibs/imagej/bdv.py @@ -13,9 +13,6 @@ import shutil import sys -from ch.epfl.biop.scijava.command.spimdata import ( - FuseBigStitcherDatasetIntoOMETiffCommand, -) from ij import IJ from java.io import File, FileInputStream, InputStreamReader from javax.xml.parsers import DocumentBuilderFactory @@ -1710,6 +1707,12 @@ def fuse_dataset_bdvp( ... use_interpolation=False, ... ) """ + # imported here: needs the PTBIOP update site, the other functions of this + # module do not + from ch.epfl.biop.scijava.command.spimdata import ( + FuseBigStitcherDatasetIntoOMETiffCommand, + ) + file_info = pathtools.parse_path(project_path) if not result_path: From 14de45c20f34a19a065e2eace5cc6af62cc91cd5 Mon Sep 17 00:00:00 2001 From: Kai Schleicher Date: Wed, 28 Oct 2026 16:33:14 +0100 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=90=9B=E2=9C=A8=20Match=20the=20bdv?= =?UTF-8?q?=20wrappers=20to=20multiview-reconstruction=209=20/=20BigStitch?= =?UTF-8?q?er=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checked against the dialogs of multiview-reconstruction 9.0.11 and BigStitcher 3.0.8: - run MVR / BigStitcher commands by plugin class (run_command): the legacy SPIM_Registration plugin shipped with Fiji registers several of the same command names, and IJ.run may start it instead - dialog keys and choices: RANSAC (minmal_number_of_inliers, "Normal (10,000)", advanced options), search radius only with limit_search_radius, timepoint spelling, illumination key of the manual definition, pre-align, fusion export keys; HDF5 export (removed in MVR 9) raises, TIFF is the default - new optional parameters, defaults keep the previous behaviour: registration (model, regularisation, grouping, point selection, descriptor values, search radius, RANSAC, strategy, fixed views), stitching optimisation (strategy, fixed groups), detection (label, downsampling, intensity range, grouping), fusion (bounding box, non-rigid, N5 / OME-ZARR options, file names), backup_xml for the stitching steps - new helpers fmt_ransac_options and export_choice Co-Authored-By: Claude Opus 5.5 --- src/imcflibs/imagej/bdv.py | 611 ++++++++++++++++++++++++++++++++----- 1 file changed, 532 insertions(+), 79 deletions(-) diff --git a/src/imcflibs/imagej/bdv.py b/src/imcflibs/imagej/bdv.py index ba80f6e2..090098c4 100644 --- a/src/imcflibs/imagej/bdv.py +++ b/src/imcflibs/imagej/bdv.py @@ -37,6 +37,65 @@ MULTI_MULTI_FILE = "[YES (one file per %s)]" """@private""" +PLUGIN_CLASSES = { + "Apply Transformations": "net.preibisch.mvrecon.fiji.plugin.Apply_Transformation", + "Define Multi-View Dataset": "net.preibisch.mvrecon.fiji.plugin.Define_Multi_View_Dataset", + "Detect Interest Points for Registration": ( + "net.preibisch.mvrecon.fiji.plugin.Interest_Point_Detection" + ), + "Duplicate Transformations": "net.preibisch.mvrecon.fiji.plugin.Duplicate_Transformation", + "Image Fusion": "net.preibisch.mvrecon.fiji.plugin.Image_Fusion", + "Register Dataset based on Interest Points": ( + "net.preibisch.mvrecon.fiji.plugin.Interest_Point_Registration" + ), + "Calculate pairwise shifts ...": "net.preibisch.stitcher.plugin.Calculate_Pairwise_Shifts", + "Filter pairwise shifts ...": "net.preibisch.stitcher.plugin.Filter_Pairwise_Shifts", + "Optimize globally and apply shifts ...": ( + "net.preibisch.stitcher.plugin.Global_Optimization_Stitching" + ), +} +"""@private +Plugin classes of the MVR / BigStitcher commands (multiview-reconstruction +9.x, BigStitcher 3.x).""" + + +def run_command(command, options): + """Run an MVR / BigStitcher command by its plugin class. + + `IJ.run()` looks a command up by its menu name. The legacy + `SPIM_Registration` plugin, which some Fiji installations still ship, + registers several of the same names (e.g. "Register Dataset based on + Interest Points", "Detect Interest Points for Registration", "Apply + Transformations"), so `IJ.run()` may start the old plugin instead. This + function starts the class from `PLUGIN_CLASSES` and hands over the macro + options the same way `IJ.run()` does. Commands without a known class (and + environments without `IJ.runPlugIn`, e.g. the test mocks) use `IJ.run()`. + + Parameters + ---------- + command : str + The menu command, e.g. `Image Fusion`. + options : str + The macro options string. + """ + class_name = PLUGIN_CLASSES.get(command) + if class_name is None or not hasattr(IJ, "runPlugIn"): + IJ.run(command, str(options)) + return + + from ij import Macro + from java.lang import Thread + + thread = Thread.currentThread() + # ImageJ only hands macro options to threads named "Run$_..." + if not thread.getName().startswith("Run$_"): + thread.setName("Run$_" + thread.getName()) + Macro.setOptions(thread, str(options)) + try: + IJ.runPlugIn(command, class_name, "") + finally: + Macro.setOptions(thread, None) + class ProcessingOptions(object): """Helper to store processing options and generate parameter strings. @@ -97,7 +156,8 @@ def __init__(self): self._use_channel = "channels=[Average Channels]" self._use_illumination = "illuminations=[Average Illuminations]" self._use_tile = "tiles=[Average Tiles]" - self._use_timepoint = "timepoints=[Average Timepoints]" + # BigStitcher writes "TimePoint" (from the class name `TimePoint`) + self._use_timepoint = "timepoints=[Average TimePoints]" # 'treat_*' values are: "group", "compare" or "[treat individually]" self._treat_angles = "[treat individually]" @@ -193,7 +253,7 @@ def reference_timepoint(self, value): value : int or int-like The timepoint number to use for the grouping. """ - self._use_timepoint = "timepoints=[use Timepoint %s]" % value + self._use_timepoint = "timepoints=[use TimePoint %s]" % value log.debug("New reference timepoint setting: %s", self._use_timepoint) ### process-X methods @@ -511,7 +571,7 @@ class DefinitionOptions(object): >>> opts.fmt_acitt_options() ... multiple_angles=[NO (one angle)] ... multiple_channels=[YES (all channels in one file)] - ... multiple_illuminations_directions=[NO (one illumination direction)] + ... _____multiple_illumination_directions=[NO (one illumination direction)] ... multiple_tiles=[YES (all tiles in one file)] ... multiple_timepoints=[NO (one time-point)] """ @@ -648,7 +708,7 @@ def fmt_acitt_options(self): """Format Angle / Channel / Illumination / Tile / Timepoint options. Build a string providing the `multiple_angles`, `multiple_channels`, - `multiple_illuminations_directions`, `multiple_tiles` and + `_____multiple_illumination_directions`, `multiple_tiles` and `multiple_timepoints` options that can be used in a BDV-related `IJ.run` call. @@ -659,7 +719,8 @@ def fmt_acitt_options(self): parameters = [ "multiple_angles=" + self._angle_definition, "multiple_channels=" + self._channel_definition, - "multiple_illuminations_directions=" + self._illumination_definition, + # dialog label "_____Multiple_illumination_directions" + "_____multiple_illumination_directions=" + self._illumination_definition, "multiple_tiles=" + self._tile_definition, "multiple_timepoints=" + self._timepoint_definition, ] @@ -748,19 +809,23 @@ def get_processing_settings(dimension, selection, value, range_end): % (selection, ", ".join(valid_selections)) ) + # the dialogs name timepoints "Timepoint" (capitalised), all other + # dimensions by their lower-case attribute name + label = "Timepoint" if dimension == "timepoint" else dimension + if selection == "single": - processing_option = SINGLE % dimension - dimension_select = "processing_" + dimension + "=[" + dimension + " %s]" % value + processing_option = SINGLE % label + dimension_select = "processing_" + dimension + "=[" + label + " %s]" % value if selection == "multiple": - processing_option = MULTIPLE % dimension + processing_option = MULTIPLE % label dimension_list = "" for dimension_name in value: dimension_list += dimension + "_%s " % dimension_name dimension_select = dimension_list.rstrip() if selection == "range": - processing_option = RANGE % dimension + processing_option = RANGE % label dimension_select = ( "process_following_" + dimension @@ -912,7 +977,7 @@ def define_dataset_auto( log.debug(options) - IJ.run("Define Multi-View Dataset", str(options)) + run_command("Define Multi-View Dataset", options) def define_dataset_manual( @@ -975,7 +1040,7 @@ def define_dataset_manual( ) log.debug("Manual dataset definition options: <%s>", options) - IJ.run("Define Multi-View Dataset", str(options)) + run_command("Define Multi-View Dataset", options) def resave_as_h5( @@ -1097,6 +1162,7 @@ def phase_correlation_pairwise_shifts_calculation( project_path, processing_opts=None, downsampling_xyz="", + backup_xml=True, ): """Calculate pairwise shifts using Phase Correlation. @@ -1111,6 +1177,8 @@ def phase_correlation_pairwise_shifts_calculation( downsampling_xyz : list of int, optional Downsampling factors in X, Y and Z, for example `[4,4,4]`. By default empty which will result in BigStitcher choosing the factors. + backup_xml : bool, optional + Copy the XML files to `xml-backup/` afterwards (default `True`). """ if not processing_opts: @@ -1144,9 +1212,10 @@ def phase_correlation_pairwise_shifts_calculation( ) log.debug("Calculate pairwise shifts options: <%s>", options) - IJ.run("Calculate pairwise shifts ...", str(options)) + run_command("Calculate pairwise shifts ...", options) - backup_xml_files(file_info["path"], "phase_correlation_shift_calculation") + if backup_xml: + backup_xml_files(file_info["path"], "phase_correlation_shift_calculation") def filter_pairwise_shifts( @@ -1155,6 +1224,7 @@ def filter_pairwise_shifts( max_r=1, max_shift_xyz="", max_displacement="", + backup_xml=True, ): """Filter the pairwise shifts based on different thresholds. @@ -1172,6 +1242,8 @@ def filter_pairwise_shifts( max_displacement : int, optional Maximal displacement to keep. By default empty, meaning no filtering based on the displacement will be applied. + backup_xml : bool, optional + Copy the XML files to `xml-backup/` afterwards (default `True`). """ file_info = pathtools.parse_path(project_path) @@ -1206,9 +1278,10 @@ def filter_pairwise_shifts( ) log.debug("Filter pairwise options: <%s>", options) - IJ.run("Filter pairwise shifts ...", str(options)) + run_command("Filter pairwise shifts ...", options) - backup_xml_files(file_info["path"], "filter_pairwise_shifts") + if backup_xml: + backup_xml_files(file_info["path"], "filter_pairwise_shifts") def optimize_and_apply_shifts( @@ -1216,6 +1289,11 @@ def optimize_and_apply_shifts( processing_opts=None, relative_error=2.5, absolute_error=3.5, + strategy="Two-Round using Metadata to align unconnected Tiles and " + + "iterative dropping of bad links", + fix_groups=None, + pre_align=True, + backup_xml=True, ): """Optimize the shifts and apply them to the dataset. @@ -1231,6 +1309,22 @@ def optimize_and_apply_shifts( Relative alignment error (in px) to accept, by default `2.5`. absolute_error : float, optional Absolute alignment error (in px) to accept, by default `3.5`. + strategy : str, optional + The global optimization strategy (without brackets), by default + `Two-Round using Metadata to align unconnected Tiles and iterative + dropping of bad links`. Note: the plain variant is spelled `Two-Round + using metadata to align unconnected Tiles` (lower-case "m"). + fix_groups : list of str, optional + Groups to fix, as in the checkbox labels `Fix_Group_` cut at the + first space, e.g. `["0-0"]` if every view is its own group or + `["0-0,"]` if view 0 shares a group (`0-0, 0-24`). Keys that do not + exist are ignored, so both variants can be given. By default `None` + (BigStitcher fixes nothing and does not fall back to a default view). + pre_align : bool, optional + Pre-align the tiles before the optimization, by default `True` (MVR + <= 7 always did; MVR 9 only if the `pre-align` key is given). + backup_xml : bool, optional + Copy the XML files to `xml-backup/` afterwards (default `True`). """ if not processing_opts: @@ -1238,6 +1332,8 @@ def optimize_and_apply_shifts( file_info = pathtools.parse_path(project_path) + fix = "".join("fix_group_%s " % group for group in (fix_groups or [])) + options = ( "select=[" + project_path @@ -1251,17 +1347,21 @@ def optimize_and_apply_shifts( + "absolute=" + str(absolute_error) + " " - + "global_optimization_strategy=[Two-Round using Metadata to align unconnected " - + "Tiles and iterative dropping of bad links] " + + "global_optimization_strategy=[" + + strategy + + "] " + "show_expert_grouping_options " + processing_opts.fmt_use_acitt() + processing_opts.fmt_how_to_treat() + + fix + + ("pre-align " if pre_align else "") ) log.debug("Optimization and shifts application options: <%s>", options) - IJ.run("Optimize globally and apply shifts ...", str(options)) + run_command("Optimize globally and apply shifts ...", options) - backup_xml_files(file_info["path"], "optimize_and_apply_shifts") + if backup_xml: + backup_xml_files(file_info["path"], "optimize_and_apply_shifts") def detect_interest_points( @@ -1270,6 +1370,13 @@ def detect_interest_points( sigma=1.8, threshold=0.008, maximum_number=3000, + label="beads", + downsample_xy=8, + downsample_z=2, + min_intensity=None, + max_intensity=None, + group_tiles=True, + group_illuminations=False, ): """Run the "Detect Interest Points" command for registration. @@ -1285,13 +1392,43 @@ def detect_interest_points( Minimum sigma for interest points detection, by default `1.8`. threshold : float, optional Threshold value for the interest point detection, by default `0.008`. + Relative to the intensity range (`min_intensity` .. `max_intensity`, + by default the min .. max of each view). maximum_number : int, optional Maximum number of interest points to use, by default `3000`. + label : str, optional + Label of the interest points, by default `beads`. + downsample_xy : int, optional + Downsampling in X and Y before the detection (1, 2, 4 or 8), by + default `8`. + downsample_z : int, optional + Downsampling in Z before the detection (1, 2, 4 or 8), by default `2`. + min_intensity, max_intensity : float, optional + Fixed intensity range for all views (`set_minimal_and_maximal_intensity`). + By default `None`: each view is normalised to its own min .. max, which + makes the threshold mean different things in bright and dim views. + group_tiles : bool, optional + Combine the tiles of a view into one detection, by default `True`. + group_illuminations : bool, optional + Combine the illuminations of a view, by default `False`. """ if not processing_opts: processing_opts = ProcessingOptions() + if min_intensity is not None and max_intensity is not None: + intensity = ( + "set_minimal_and_maximal_intensity " + + "minimal_intensity=" + + str(min_intensity) + + " " + + "maximal_intensity=" + + str(max_intensity) + + " " + ) + else: + intensity = "" + options = ( "select=[" + project_path @@ -1299,13 +1436,20 @@ def detect_interest_points( + processing_opts.fmt_acitt_options() + processing_opts.fmt_acitt_selectors() + "type_of_interest_point_detection=Difference-of-Gaussian " - + "label_interest_points=beads " + + "label_interest_points=" + + label + + " " + "limit_amount_of_detections " - + "group_tiles " + + ("group_tiles " if group_tiles else "") + + ("group_illuminations " if group_illuminations else "") + "subpixel_localization=[3-dimensional quadratic fit] " + "interest_point_specification=[Advanced ...] " - + "downsample_xy=8x " - + "downsample_z=2x " + + "downsample_xy=" + + str(downsample_xy) + + "x " + + "downsample_z=" + + str(downsample_z) + + "x " + "sigma=" + str(sigma) + " " @@ -1313,6 +1457,7 @@ def detect_interest_points( + str(threshold) + " " + "find_maxima " + + intensity + "maximum_number=" + str(maximum_number) + " " @@ -1321,16 +1466,93 @@ def detect_interest_points( ) log.debug("Interest points detection options: <%s>", options) - IJ.run("Detect Interest Points for Registration", str(options)) + run_command("Detect Interest Points for Registration", options) + + +def fmt_ransac_options( + max_error=5, + min_inliers=None, + iterations="Normal (10,000)", + min_inlier_ratio=None, + max_trust=None, +): + """Format the RANSAC options of the interest point registration. + + Valid for multiview-reconstruction 9.x. MVR 7 used an `inlier_factor` + instead of an absolute number of inliers and the iteration choices "Fast", + "Normal", ... without the counts. + + Parameters + ---------- + max_error : float, optional + Allowed error for RANSAC in px, by default `5`. + min_inliers : int, optional + Minimal number of inliers per link (`minmal_number_of_inliers`, typo + as in MVR). By default `None`, i.e. MVR's default (12). + iterations : str, optional + One of `Fast (1,000)`, `Normal (10,000)` (default), `Thorough (10^5)`, + `Very thorough (10^6)`, `Ridiculous (10^7)`. + min_inlier_ratio : float, optional + Minimal inlier ratio (advanced RANSAC dialog). By default `None`, i.e. + MVR's default (0.05; MVR 7 used 0.1). + max_trust : float, optional + Maximal trust, a factor of the median residual (advanced RANSAC + dialog). By default `None`, i.e. MVR's default (3; MVR 7 used 4). + + Returns + ------- + str + """ + options = ( + "allowed_error_for_ransac=" + + str(max_error) + + " " + + "ransac_iterations=[" + + iterations + + "] " + ) + if min_inliers is not None: + options += "minmal_number_of_inliers=" + str(min_inliers) + " " + if min_inlier_ratio is not None or max_trust is not None: + options += "advanced_ransac_options filter_ransac " + if min_inlier_ratio is not None: + options += "minimal_inlier_ratio=" + str(min_inlier_ratio) + " " + if max_trust is not None: + options += "maximal_trust=" + str(max_trust) + " " + return options def interest_points_registration( project_path, processing_opts=None, rigid_timepoints=False, + label="beads", + transformation="Affine", + regularize_with="Affine", + regularization_lambda=0.1, + group_tiles=True, + group_illuminations=True, + group_channels=True, + merge_distance=5, + view_selection="Only compare overlapping views (according to current " + + "transformations)", + point_selection="Compare all interest point of overlapping views", + number_of_neighbors=3, + redundancy=1, + significance=3, + search_radius=None, + ransac_options=None, + strategy="Two-Round: Handle unconnected tiles, remove wrong links RELAXED " + + "(5.0x / 7.0px)", + fix_views=None, + pre_align=True, ): """Run the "Register Dataset based on Interest Points" command. + Uses the "Precise descriptor-based (translation invariant)" algorithm. The + defaults reproduce the earlier behaviour of this function (Affine, + regularised with Affine, all groups, RELAXED). + Parameters ---------- project_path : str @@ -1344,6 +1566,48 @@ def interest_points_registration( If set to `True` each timepoint will be considered as a rigid unit (useful e.g. if spatial registration has already been performed before). By default `False`. + label : str, optional + Label of the interest points, by default `beads`. + transformation : str, optional + `Translation`, `Rigid` or `Affine` (default). + regularize_with : str or None, optional + Model to regularize with (`Translation`, `Rigid`, `Affine`), by default + `Affine`; `None` for no regularization. + regularization_lambda : float, optional + Weight of the regularization model, by default `0.1`. + group_tiles, group_illuminations, group_channels : bool, optional + Combine the interest points of the tiles / illuminations / channels of + a view into one virtual view, by default `True`. + merge_distance : float, optional + Distance (px) below which grouped points are merged, by default `5`. + view_selection : str, optional + Which views to compare (choice without brackets), by default only + overlapping views. + point_selection : str, optional + Which points to compare (choice without brackets), by default all + points of overlapping views; `Only compare interest points that overlap + between views (according to current transformations)` restricts them + to the overlaps. + number_of_neighbors, redundancy, significance : int, optional + Descriptor parameters, by default `3`, `1` and `3`. + search_radius : float, optional + Only pair descriptors whose points are within this distance (px) under + the current transformations (`limit_search_radius`). By default `None` + (no limit). + ransac_options : str, optional + RANSAC options from `fmt_ransac_options()`, by default + `fmt_ransac_options()` (MVR defaults, 10,000 iterations). + strategy : str, optional + The global optimization strategy (without brackets), by default + `Two-Round: Handle unconnected tiles, remove wrong links RELAXED (5.0x + / 7.0px)`. + fix_views : str, optional + The `fix_views` choice, e.g. `Fix first view`, together with `Do not + map back (use this if views are fixed)`. By default `None` (dialog + default; with a single timepoint that is `Fix first view`). + pre_align : bool, optional + Pre-align the views before the global optimization, by default `True` + (MVR <= 7 always did; MVR 9 only if the `pre-align` key is given). """ if not processing_opts: @@ -1354,6 +1618,51 @@ def interest_points_registration( else: rigid_timepoints_arg = " " + if ransac_options is None: + ransac_options = fmt_ransac_options() + + if regularize_with: + regularize = ( + "regularize_model " + + "model_to_regularize_with=" + + regularize_with + + " " + + "lambda=" + + str(regularization_lambda) + + " " + ) + else: + regularize = "" + + grouping = ( + ("group_tiles " if group_tiles else "") + + ("group_illuminations " if group_illuminations else "") + + ("group_channels " if group_channels else "") + ) + if grouping: + grouping += ( + "interestpoint_grouping=[" + + "Group interest points (simply combine all in one virtual view)] " + + "interest=" + + str(merge_distance) + + " " + ) + + if search_radius is not None: + radius = "limit_search_radius search_radius=" + str(search_radius) + " " + else: + radius = "" + + if fix_views: + fix = ( + "fix_views=[" + + fix_views + + "] " + + "map_back_views=[Do not map back (use this if views are fixed)] " + ) + else: + fix = "" + options = ( "select=[" + project_path @@ -1364,36 +1673,42 @@ def interest_points_registration( + "registration_over_time=[" + "Match against one reference timepoint (no global optimization)] " + "registration_in_between_views=[" - + "Only compare overlapping views (according to current transformations)] " - + "interest_point_inclusion=[Compare all interest point of overlapping views] " - + "interest_points=beads " - + "group_tiles " - + "group_illuminations " - + "group_channels " + + view_selection + + "] " + + "interest_point_inclusion=[" + + point_selection + + "] " + + "interest_points=" + + label + + " " + + grouping + "reference=1 " + rigid_timepoints_arg - + "transformation=Affine " - + "regularize_model " - + "model_to_regularize_with=Affine " - + "lambda=0.10 " - + "number_of_neighbors=3 " - + "redundancy=1 " - + "significance=3 " - + "search_radius=100 " - + "allowed_error_for_ransac=5 " - + "inlier_factor=3 " - + "ransac_iterations=Normal " + + fix + + "transformation=" + + transformation + + " " + + regularize + + "number_of_neighbors=" + + str(number_of_neighbors) + + " " + + "redundancy=" + + str(redundancy) + + " " + + "significance=" + + str(significance) + + " " + + radius + + ransac_options + "global_optimization_strategy=[" - + "Two-Round: Handle unconnected tiles, " - + "remove wrong links RELAXED (5.0x / 7.0px)] " - + "interestpoint_grouping=[" - + "Group interest points (simply combine all in one virtual view)] " - + "interest=5" + + strategy + + "] " + + ("pre-align" if pre_align else "") ) log.debug("Interest points registration options: <%s>", options) # register using interest points - IJ.run("Register Dataset based on Interest Points", options) + run_command("Register Dataset based on Interest Points", options) def duplicate_transformations( @@ -1485,7 +1800,7 @@ def duplicate_transformations( ) log.debug("Transformation duplication options: <%s>", options) - IJ.run("Duplicate Transformations", str(options)) + run_command("Duplicate Transformations", options) backup_xml_files( file_info["path"], @@ -1493,6 +1808,36 @@ def duplicate_transformations( ) +def export_choice(export): + """Get the `export=` value of "OME-ZARR/N5/HDF5 export using N5-API". + + Choices of multiview-reconstruction 9.x: `N5`, `OME-ZARR v3`, `OME-ZARR v2 + (deprecated)` and `HDF5 (currently not supported)`. + + Parameters + ---------- + export : {`N5`, `OME-ZARR`} + The format; `OME-ZARR` is written as OME-ZARR v3. + + Returns + ------- + str + The choice in brackets, e.g. `[OME-ZARR v3]`. + + Raises + ------ + ValueError + For HDF5 (not available in MVR 9.x) and unknown formats. + """ + choices = {"N5": "[N5]", "OME-ZARR": "[OME-ZARR v3]"} + if export not in choices: + raise ValueError( + "No export option for %s in MVR 9.x (HDF5 is not available, use N5 " + "or OME-ZARR)" % export + ) + return choices[export] + + def fuse_dataset( project_path, processing_opts=None, @@ -1501,15 +1846,35 @@ def fuse_dataset( interpolation="[Linear Interpolation]", pixel_type="[16-bit unsigned integer]", fusion_type="Avg, Blending", - export="HDF5", + export="TIFF", compression="Zstandard", + bounding_box="All Views", + preserve_original=True, + non_rigid_label=None, + non_rigid_alpha=1.0, + non_rigid_cpd=10, + filename_addition=None, + output_basename=None, + block_size=(128, 128, 64), + block_size_factor=(2, 2, 2), + subsampling_factors="", + sharding=True, + single_container=True, ): """Call BigStitcher's "Fuse Dataset" command. Wrapper to `BigStitcher > Batch Processing > Fuse Dataset`. Depending on the export type, inputs are different and therefore will - distribute inputs differently. + distribute inputs differently. TIFF stacks are written uncompressed (one + file per timepoint and channel). N5 and OME-ZARR always get a + multi-resolution pyramid and a BDV/BigStitcher XML. + + Note: MVR keeps the last values of its dialogs for the rest of the Fiji + session, so an option missing from the macro string is not MVR's default + but the value of the previous fusion. Compression and block sizes are + therefore always passed (defaults = MVR's initial values); only the pyramid + levels can be left to MVR, which proposes them for each fused image. Parameters ---------- @@ -1528,12 +1893,62 @@ def fuse_dataset( Interpolation to use during fusion, by default `[Linear Interpolation]`. pixel_type : str, optional Pixel type to use during fusion, by default `[16-bit unsigned integer]`. - export : str, optional - Format of the output fused image, by default `HDF5`. + export : {`TIFF`, `N5`, `OME-ZARR`}, optional + Format of the output fused image, by default `TIFF` (uncompressed; + MVR's "Save as (compressed) TIFF stacks" never compresses). `N5` and + `OME-ZARR` (OME-ZARR v3) write chunked containers in parallel. `HDF5` + is not available in MVR 9.x and raises a `ValueError`. fusion_type : str, optional Type of fusion algorithm to use, by default `Avg, Blending`. compression : str, optional - Compression method to use when exporting as HDF5, by default `Zstandard`. + N5 / OME-ZARR only: one of `Lz4`, `Gzip`, `Zstandard`, `Blosc`, + `Bzip2`, `Xz`, `Raw (no compression)`. By default `Zstandard` (MVR's + default). + bounding_box : str, optional + Name of the bounding box to fuse (without brackets), by default `All + Views`. + preserve_original : bool, optional + Keep the anisotropy of the input (z in planes) at downsampling 1, by + default `True`. Ignored by MVR for downsampling > 1. + non_rigid_label : str, optional + Interest point label for a non-rigid fusion (moving least squares on + the stored correspondences). By default `None` (rigid). MVR fuses + views with fewer than 12 correspondences with their affine model. Note + (MVR 7.0.7 - 9.0.11): the non-rigid fusion ignores the minimum of the + bounding box (fuses world 0 .. max), and the label is only found if + every fused view has correspondences of it. + non_rigid_alpha : float, optional + Alpha of the moving least squares, by default `1.0`. + non_rigid_cpd : int, optional + Control point distance in output voxels, by default `10`. + filename_addition : str, optional + TIFF only: prefix of the file names (`_fused_tp__ch_.tif`). + By default `None`, i.e. the project's base name. + output_basename : str, optional + N5 / OME-ZARR only: base name of the container and its XML. By + default `None`, i.e. `_fused`. + block_size : list of int, optional + N5 / OME-ZARR only: block (chunk) size in X, Y and Z, by default + `(128, 128, 64)` (MVR's default). N5 writes one file per block. + block_size_factor : list of int, optional + N5 / OME-ZARR only: compute block size as a factor of the block size in + X, Y and Z; with OME-ZARR sharding also the shard size (one file per + shard). By default `(2, 2, 2)` (MVR's default). + subsampling_factors : str, optional + N5 / OME-ZARR only: downsampling factors of the pyramid levels, e.g. + `{ {1,1,1}, {2,2,1}, {4,4,2} }`. By default `""`, i.e. the levels + BigStitcher proposes for the size of the fused image (data dependent). + sharding : bool, optional + OME-ZARR only: group blocks into shards (fewer files), by default + `True` (BigStitcher's default). + single_container : bool, optional + OME-ZARR only: store channels and timepoints in a single OME-ZARR + container, by default `True` (BigStitcher's default). + + Raises + ------ + ValueError + If `export` is `HDF5` or unknown. """ if processing_opts is None: @@ -1545,12 +1960,31 @@ def fuse_dataset( # if not os.path.exists(result_path): # os.makedirs(result_path) + if non_rigid_label: + non_rigid = ( + "interest_points_for_non_rigid=[" + + non_rigid_label + + "] " + + "non_rigid_advanced_parameters " + + "alpha=" + + str(non_rigid_alpha) + + " " + + "control_point_distance=" + + str(non_rigid_cpd) + + " " + ) + else: + non_rigid = "interest_points_for_non_rigid=[-= Disable Non-Rigid =-] " + options = ( "select=[" + project_path + "] " + processing_opts.fmt_acitt_options() - + "bounding_box=[All Views] " + + processing_opts.fmt_acitt_selectors() + + "bounding_box=[" + + bounding_box + + "] " + "downsampling=" + str(downsampling) + " " @@ -1563,12 +1997,14 @@ def fuse_dataset( + "pixel_type=" + pixel_type + " " - + "interest_points_for_non_rigid=[-= Disable Non-Rigid =-] " - + "preserve_original " + + non_rigid + + ("preserve_original " if preserve_original else "") + "produce=[Each timepoint & channel] " ) if export == "TIFF": + if filename_addition is None: + filename_addition = file_info["basename"] options = ( options + "fused_image=[Save as (compressed) TIFF stacks] " @@ -1577,44 +2013,61 @@ def fuse_dataset( + result_path + "/.] " + "filename_addition=[" - + file_info["basename"] + + filename_addition + "]" ) - elif export == "HDF5": - h5_fused_path = pathtools.join2( - result_path, file_info["basename"] + "_fused.h5" - ) - xml_fused_path = pathtools.join2( - result_path, file_info["basename"] + "_fused.xml" - ) + elif export in ("N5", "OME-ZARR"): + if output_basename is None: + output_basename = file_info["basename"] + "_fused" + extension = {"N5": ".n5", "OME-ZARR": ".zarr"}[export] + path_key = {"N5": "n5_dataset_path", "OME-ZARR": "zarr_dataset_path"}[export] + data_path = pathtools.join2(result_path, output_basename + extension) + xml_fused_path = pathtools.join2(result_path, output_basename + ".xml") options = ( options + "fused_image=[OME-ZARR/N5/HDF5 export using N5-API] " + "define_input=[Auto-load from input data (values shown below)] " - + "export=HDF5 " - + "compression=" - + compression + + "export=" + + export_choice(export) + " " - + "create " - + "create_0 " - + "hdf5_file=[" - + h5_fused_path + + "create_a_bdv/bigstitcher " + + "create_multi-resolution " + + path_key + + "=[" + + data_path + "] " + "xml_output_file=[" + xml_fused_path + "] " - + "show_advanced_block_size_options " - + "block_size_x=128 " - + "block_size_y=128 " - + "block_size_z=64 " - + "block_size_factor_x=1 " - + "block_size_factor_y=1 " - + "block_size_factor_z=1" ) + options = options + "compression=[" + compression + "] " + if export == "OME-ZARR": + # checkboxes missing from a macro are off, so the defaults are + # passed explicitly + options = ( + options + + ("store_channels " if single_container else "") + + ("enable_sharding " if sharding else "") + ) + options = options + ( + "show_advanced_block_size_options " + + "block_size_x=%s block_size_y=%s block_size_z=%s " % tuple(block_size) + + "block_size_factor_x=%s block_size_factor_y=%s block_size_factor_z=%s " + % tuple(block_size_factor) + ) + if subsampling_factors: + options = options + "subsampling_factors=[" + subsampling_factors + "] " + elif export == "HDF5": + raise ValueError( + "HDF5 export is not available in multiview-reconstruction 9.x, use " + "TIFF, N5 or OME-ZARR" + ) + else: + raise ValueError("Unknown export format: %s" % export) log.debug("Dataset fusion options: <%s>", options) - IJ.run("Image Fusion", str(options)) + run_command("Image Fusion", options) def fuse_dataset_bdvp( From 89ed7fed8092a4cd3eb81230b338abfc4cc1febf Mon Sep 17 00:00:00 2001 From: Kai Schleicher Date: Wed, 28 Oct 2026 16:33:14 +0100 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9C=85=20Test=20the=20bdv=20wrappers=20a?= =?UTF-8?q?gainst=20the=20MVR=209=20/=20BigStitcher=203=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- tests/bdv/test_definitionoptions.py | 8 +- tests/bdv/test_mvr_wrappers.py | 326 ++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+), 4 deletions(-) create mode 100644 tests/bdv/test_mvr_wrappers.py diff --git a/tests/bdv/test_definitionoptions.py b/tests/bdv/test_definitionoptions.py index 74d4ce99..54802448 100644 --- a/tests/bdv/test_definitionoptions.py +++ b/tests/bdv/test_definitionoptions.py @@ -10,7 +10,7 @@ def test_defaults(): acitt_options = ( "multiple_angles=[NO (one angle)] " "multiple_channels=[YES (all channels in one file)] " - "multiple_illuminations_directions=[NO (one illumination direction)] " + "_____multiple_illumination_directions=[NO (one illumination direction)] " "multiple_tiles=[YES (one file per tile)] " "multiple_timepoints=[NO (one time-point)] " ) @@ -40,7 +40,7 @@ def test__multiple_timepoints_files(): acitt_options = ( "multiple_angles=[NO (one angle)] " "multiple_channels=[YES (all channels in one file)] " - "multiple_illuminations_directions=[NO (one illumination direction)] " + "_____multiple_illumination_directions=[NO (one illumination direction)] " "multiple_tiles=[YES (one file per tile)] " "multiple_timepoints=[YES (one file per time-point)] " ) @@ -57,7 +57,7 @@ def test__multiple_channels_files_multiple_timepoints(): acitt_options = ( "multiple_angles=[NO (one angle)] " "multiple_channels=[YES (one file per channel)] " - "multiple_illuminations_directions=[NO (one illumination direction)] " + "_____multiple_illumination_directions=[NO (one illumination direction)] " "multiple_tiles=[YES (one file per tile)] " "multiple_timepoints=[YES (all time-points in one file)] " ) @@ -75,7 +75,7 @@ def test_single_tile_multiple_angles_files(): acitt_options = ( "multiple_angles=[YES (one file per angle)] " "multiple_channels=[YES (all channels in one file)] " - "multiple_illuminations_directions=[NO (one illumination direction)] " + "_____multiple_illumination_directions=[NO (one illumination direction)] " "multiple_tiles=[NO (one tile)] " "multiple_timepoints=[NO (one time-point)] " ) diff --git a/tests/bdv/test_mvr_wrappers.py b/tests/bdv/test_mvr_wrappers.py new file mode 100644 index 00000000..799ba61f --- /dev/null +++ b/tests/bdv/test_mvr_wrappers.py @@ -0,0 +1,326 @@ +"""Tests for the MVR / BigStitcher command wrappers in the BDV module. + +The expected strings use the dialog keys and choices of multiview-reconstruction +9.0.11 and BigStitcher 3.0.8. +""" + +import logging + +import pytest + +from imcflibs.imagej import bdv + +XML = "/data/project/dataset.xml" +PROCESS_ALL = ( + "process_angle=[All angles] " + + "process_channel=[All channels] " + + "process_illumination=[All illuminations] " + + "process_tile=[All tiles] " + + "process_timepoint=[All Timepoints] " +) + + +def norm(text): + """Collapse runs of white space (ImageJ ignores them in macro options).""" + return " ".join(text.split()) + + +def last_call(caplog): + """Return the logged (mock) `IJ.run` call, white space collapsed.""" + return norm(caplog.messages[-1]) + + +def ij_run(cmd, options): + """Format a (mock) `IJ.run` call as logged by `imcf-fiji-mocks`.""" + return norm("IJ.run(cmd=[%s], params=[%s])" % (cmd, options)) + + +def test_timepoint_selection_is_capitalised(): + """The dialogs name timepoints "Timepoint", other dimensions lower-case.""" + opts = bdv.ProcessingOptions() + opts.process_timepoint(2) + opts.process_channel(1) + assert "process_timepoint=[Single Timepoint (Select from List)]" in opts.fmt_acitt_options() + assert "process_channel=[Single channel (Select from List)]" in opts.fmt_acitt_options() + assert norm(opts.fmt_acitt_selectors()) == ( + "processing_channel=[channel 1] processing_timepoint=[Timepoint 2]" + ) + + +def test_timepoint_grouping_choices(): + """BigStitcher's grouping choices are spelled "TimePoint(s)".""" + opts = bdv.ProcessingOptions() + opts.treat_timepoints("group") + assert "timepoints=[Average TimePoints]" in opts.fmt_use_acitt() + opts.reference_timepoint(0) + assert "timepoints=[use TimePoint 0]" in opts.fmt_use_acitt() + + +def test_fmt_ransac_options_defaults(): + """Default RANSAC options: MVR defaults, iterations with their count.""" + assert bdv.fmt_ransac_options() == ( + "allowed_error_for_ransac=5 ransac_iterations=[Normal (10,000)] " + ) + + +def test_fmt_ransac_options_all(): + """Inliers, inlier ratio and maximal trust (advanced RANSAC dialog).""" + assert bdv.fmt_ransac_options(min_inliers=3, min_inlier_ratio=0.1, max_trust=4) == ( + "allowed_error_for_ransac=5 ransac_iterations=[Normal (10,000)] " + + "minmal_number_of_inliers=3 " + + "advanced_ransac_options filter_ransac " + + "minimal_inlier_ratio=0.1 " + + "maximal_trust=4 " + ) + + +def test_optimize_and_apply_shifts(caplog): + """Fix groups, strategy and pre-align of the stitching optimization.""" + caplog.set_level(logging.WARNING) + opts = bdv.ProcessingOptions() + opts.treat_channels("compare") + bdv.optimize_and_apply_shifts( + XML, + processing_opts=opts, + relative_error=5.0, + absolute_error=7.0, + fix_groups=["0-0", "0-0,"], + backup_xml=False, + ) + expected = ( + "select=[" + XML + "] " + + PROCESS_ALL + + " " + + "relative=5.0 " + + "absolute=7.0 " + + "global_optimization_strategy=[Two-Round using Metadata to align " + + "unconnected Tiles and iterative dropping of bad links] " + + "show_expert_grouping_options " + + "illuminations=[Average Illuminations] " + + "how_to_treat_angles=[treat individually] " + + "how_to_treat_channels=compare " + + "how_to_treat_illuminations=group " + + "how_to_treat_tiles=compare " + + "how_to_treat_timepoints=[treat individually] " + + "fix_group_0-0 fix_group_0-0, " + + "pre-align " + ) + assert last_call(caplog) == ij_run("Optimize globally and apply shifts ...", expected) + + +def test_interest_points_registration_translation(caplog): + """Translation per view, no grouping, limited search radius, fixed view.""" + caplog.set_level(logging.WARNING) + bdv.interest_points_registration( + XML, + transformation="Translation", + regularize_with=None, + group_tiles=False, + group_illuminations=False, + group_channels=False, + point_selection="Only compare interest points that overlap between " + + "views (according to current transformations)", + redundancy=2, + search_radius=120, + ransac_options=bdv.fmt_ransac_options(min_inliers=3), + strategy="Two-Round: Handle unconnected tiles, remove wrong links " + + "STRICT (2.5x / 3.5px)", + fix_views="Fix first view", + ) + expected = ( + "select=[" + XML + "] " + + PROCESS_ALL + + " " + + "registration_algorithm=[Precise descriptor-based (translation invariant)] " + + "registration_over_time=[Match against one reference timepoint " + + "(no global optimization)] " + + "registration_in_between_views=[Only compare overlapping views " + + "(according to current transformations)] " + + "interest_point_inclusion=[Only compare interest points that overlap " + + "between views (according to current transformations)] " + + "interest_points=beads " + + "reference=1 " + + " " + + "fix_views=[Fix first view] " + + "map_back_views=[Do not map back (use this if views are fixed)] " + + "transformation=Translation " + + "number_of_neighbors=3 " + + "redundancy=2 " + + "significance=3 " + + "limit_search_radius search_radius=120 " + + "allowed_error_for_ransac=5 ransac_iterations=[Normal (10,000)] " + + "minmal_number_of_inliers=3 " + + "global_optimization_strategy=[Two-Round: Handle unconnected tiles, " + + "remove wrong links STRICT (2.5x / 3.5px)] " + + "pre-align" + ) + assert last_call(caplog) == ij_run("Register Dataset based on Interest Points", expected) + + +def test_interest_points_registration_grouped_channels(caplog): + """Channels grouped into one virtual view, Rigid regularised with Rigid.""" + caplog.set_level(logging.WARNING) + bdv.interest_points_registration( + XML, + transformation="Rigid", + regularize_with="Rigid", + group_tiles=False, + group_illuminations=False, + pre_align=False, + ) + call = last_call(caplog) + assert "transformation=Rigid regularize_model model_to_regularize_with=Rigid lambda=0.1 " in call + assert ( + "group_channels interestpoint_grouping=[Group interest points (simply " + + "combine all in one virtual view)] interest=5 " + ) in call + assert "group_tiles" not in call + assert "pre-align" not in call + assert "limit_search_radius" not in call + + +def test_detect_interest_points_intensity_range(caplog): + """Fixed intensity range, own downsampling and label, no grouping.""" + caplog.set_level(logging.WARNING) + bdv.detect_interest_points( + XML, + threshold=0.1, + maximum_number=20000, + label="cells", + downsample_xy=4, + downsample_z=1, + min_intensity=290, + max_intensity=4000, + group_tiles=False, + ) + call = last_call(caplog) + assert "label_interest_points=cells " in call + assert "downsample_xy=4x downsample_z=1x " in call + assert "set_minimal_and_maximal_intensity minimal_intensity=290 maximal_intensity=4000 " in call + assert "group_tiles" not in call + assert call.startswith("IJ.run(cmd=[Detect Interest Points for Registration]") + + +def test_fuse_dataset_tiff(caplog): + """TIFF fusion with a bounding box, non-rigid and a file name prefix.""" + caplog.set_level(logging.WARNING) + bdv.fuse_dataset( + XML, + result_path="/data/fused", + downsampling=29, + export="TIFF", + bounding_box="My Bounding Box", + preserve_original=False, + non_rigid_label="beads_split", + non_rigid_cpd=2, + filename_addition="ds29", + ) + expected = ( + "select=[" + XML + "] " + + PROCESS_ALL + + " " + + "bounding_box=[My Bounding Box] " + + "downsampling=29 " + + "interpolation=[Linear Interpolation] " + + "fusion_type=[Avg, Blending] " + + "pixel_type=[16-bit unsigned integer] " + + "interest_points_for_non_rigid=[beads_split] " + + "non_rigid_advanced_parameters alpha=1.0 control_point_distance=2 " + + "produce=[Each timepoint & channel] " + + "fused_image=[Save as (compressed) TIFF stacks] " + + "define_input=[Auto-load from input data (values shown below)] " + + "output_file_directory=[/data/fused/.] " + + "filename_addition=[ds29]" + ) + assert last_call(caplog) == ij_run("Image Fusion", expected) + + +def test_export_choice(): + """Export choices of MVR 9.x; HDF5 is not available.""" + assert bdv.export_choice("N5") == "[N5]" + assert bdv.export_choice("OME-ZARR") == "[OME-ZARR v3]" + with pytest.raises(ValueError): + bdv.export_choice("HDF5") + + +def test_fuse_dataset_default_tiff(caplog): + """The default export is (uncompressed) TIFF named after the project.""" + caplog.set_level(logging.WARNING) + bdv.fuse_dataset(XML, result_path="/data/fused") + call = last_call(caplog) + assert "fused_image=[Save as (compressed) TIFF stacks] " in call + assert "filename_addition=[dataset]" in call + assert "compression" not in call + + +def test_fuse_dataset_n5(caplog): + """N5 export: always a BDV/BigStitcher XML and a pyramid, MVR defaults.""" + caplog.set_level(logging.WARNING) + bdv.fuse_dataset(XML, result_path="/data/fused", export="N5", output_basename="out") + call = last_call(caplog) + assert "export=[N5] " in call + assert "create_a_bdv/bigstitcher create_multi-resolution " in call + assert "n5_dataset_path=[/data/fused/out.n5] " in call + assert "xml_output_file=[/data/fused/out.xml] " in call + assert "interest_points_for_non_rigid=[-= Disable Non-Rigid =-] " in call + assert "preserve_original " in call + # MVR's initial defaults are always passed (the dialogs keep the values of + # the previous call); the pyramid levels are left to MVR + assert "compression=[Zstandard] " in call + assert ( + "show_advanced_block_size_options " + + "block_size_x=128 block_size_y=128 block_size_z=64 " + + "block_size_factor_x=2 block_size_factor_y=2 block_size_factor_z=2 " + ) in call + assert "subsampling_factors" not in call + assert "sharding" not in call + + +def test_fuse_dataset_n5_options(caplog): + """N5 export with compression, block sizes and pyramid levels.""" + caplog.set_level(logging.WARNING) + bdv.fuse_dataset( + XML, + result_path="/data/fused", + export="N5", + compression="Raw (no compression)", + block_size=[64, 64, 32], + block_size_factor=[4, 4, 2], + subsampling_factors="{ {1,1,1}, {2,2,1} }", + ) + call = last_call(caplog) + assert "compression=[Raw (no compression)] " in call + assert ( + "show_advanced_block_size_options " + + "block_size_x=64 block_size_y=64 block_size_z=32 " + + "block_size_factor_x=4 block_size_factor_y=4 block_size_factor_z=2 " + ) in call + assert "subsampling_factors=[{ {1,1,1}, {2,2,1} }]" in call + + +def test_fuse_dataset_ome_zarr(caplog): + """OME-ZARR v3: sharding and a single container unless switched off.""" + caplog.set_level(logging.WARNING) + bdv.fuse_dataset(XML, result_path="/data/fused", export="OME-ZARR") + call = last_call(caplog) + assert "export=[OME-ZARR v3] " in call + assert "zarr_dataset_path=[/data/fused/dataset_fused.zarr] " in call + assert "store_channels enable_sharding " in call + bdv.fuse_dataset( + XML, result_path="/data/fused", export="OME-ZARR", sharding=False, single_container=False + ) + call = last_call(caplog) + assert "store_channels" not in call + assert "enable_sharding" not in call + + +def test_fuse_dataset_hdf5(): + """HDF5 export is not available in MVR 9.x.""" + with pytest.raises(ValueError): + bdv.fuse_dataset(XML, export="HDF5") + + +def test_fuse_dataset_unknown_export(): + """Unknown export formats are rejected.""" + with pytest.raises(ValueError): + bdv.fuse_dataset(XML, export="PNG") From f8b0a610fd14728d5cc1e53f4b7433bee0e1d29f Mon Sep 17 00:00:00 2001 From: Kai Schleicher Date: Wed, 28 Oct 2026 16:33:14 +0100 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=9D=20Changelog:=20bdv=20wrappers?= =?UTF-8?q?=20for=20multiview-reconstruction=209=20/=20BigStitcher=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2647f020..8daf2b58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,16 @@ multiple modules. For detailed information, please refer to the * `imcflibs.imagej.bdv.read_metadata_from_xml` allows reading metadata back from the xml of a H5/XML couple. +* `imcflibs.imagej.bdv.run_command` runs an MVR / BigStitcher command by its + plugin class instead of its menu name. The legacy `SPIM_Registration` + plugin registers several of the same command names, so `IJ.run()` could + start the old plugin instead (e.g. a `VerifyError` in "Register Dataset + based on Interest Points"). All MVR / BigStitcher wrappers use it. +* `imcflibs.imagej.bdv.fmt_ransac_options` formats the RANSAC options of the + interest point registration (minimal number of inliers, iterations, + inlier ratio, maximal trust) for multiview-reconstruction 9.x. +* `imcflibs.imagej.bdv.export_choice` gives the `export=` choice of the + N5-API fusion export of MVR 9.x (`[N5]`, `[OME-ZARR v3]`). #### New functions in `imcflibs.imagej.misc` @@ -58,6 +68,39 @@ multiple modules. For detailed information, please refer to the * `imcflibs.imagej.bdv.resave_as_h5`: use the correct processing options object and change the ImageJ command to "Resave as HDF5 (local)" for better accuracy. +* Match the dialogs of multiview-reconstruction 9.x and BigStitcher 3.x + (checked against the 9.0.11 / 3.0.8 jars): + * `imcflibs.imagej.bdv.ProcessingOptions`: timepoints are spelled + "Timepoint" (`[Single Timepoint (Select from List)]`, `[Timepoint N]`) + and "TimePoints" in the grouping choices (`[Average TimePoints]`). + * `imcflibs.imagej.bdv.DefinitionOptions`: illumination key + `_____multiple_illumination_directions`. + * `imcflibs.imagej.bdv.interest_points_registration`: RANSAC keys + `minmal_number_of_inliers` (replaces `inlier_factor`) and + `ransac_iterations=[Normal (10,000)]`; the search radius only applies with + `limit_search_radius`. New optional parameters: label, transformation and + regularization, grouping, view / point selection, descriptor parameters, + search radius, RANSAC options, strategy, fixed views, pre-alignment. + * `imcflibs.imagej.bdv.optimize_and_apply_shifts`: new optional `strategy`, + `fix_groups` and `pre_align` (MVR 9 only pre-aligns with the + `pre-align` key; default on, as MVR 7 always did). + * `imcflibs.imagej.bdv.detect_interest_points`: new optional label, + downsampling, fixed intensity range and grouping. + * `imcflibs.imagej.bdv.fuse_dataset`: the default export is now TIFF + (uncompressed; HDF5 export is not available in MVR 9.x and raises a + `ValueError`). N5 and OME-ZARR (v3) always create a BDV/BigStitcher XML + and a multi-resolution pyramid (keys `create_a_bdv/bigstitcher` and + `create_multi-resolution`, were `create` / `create_0`); optional + compression, block sizes, pyramid levels, sharding and single-container + settings. Compression and block sizes are always passed (MVR's initial + defaults), since MVR keeps the values of the previous fusion of the + session; the pyramid levels default to MVR's proposal. Processing + selectors are passed on; new optional bounding box, `preserve_original`, non-rigid + fusion, TIFF `filename_addition` and output base name. + * `phase_correlation_pairwise_shifts_calculation`, `filter_pairwise_shifts` + and `optimize_and_apply_shifts`: optional `backup_xml` (default `True`). +* `imcflibs.imagej.bdv` can be imported without the PTBIOP update site: the + BIOP command is only imported in `fuse_dataset_bdvp`. #### `imcflibs.imagej.bioformats` From 12e77cee3b1562aa0d0af0612c01749be474a59a Mon Sep 17 00:00:00 2001 From: Kai Schleicher Date: Wed, 28 Oct 2026 17:50:29 +0100 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=93=9D=20Reference=20issues=20#158,?= =?UTF-8?q?=20#159=20and=20#160?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 +++--- src/imcflibs/imagej/bdv.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8daf2b58..26c38b65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ multiple modules. For detailed information, please refer to the plugin class instead of its menu name. The legacy `SPIM_Registration` plugin registers several of the same command names, so `IJ.run()` could start the old plugin instead (e.g. a `VerifyError` in "Register Dataset - based on Interest Points"). All MVR / BigStitcher wrappers use it. + based on Interest Points"). All MVR / BigStitcher wrappers use it (#158). * `imcflibs.imagej.bdv.fmt_ransac_options` formats the RANSAC options of the interest point registration (minimal number of inliers, iterations, inlier ratio, maximal trust) for multiview-reconstruction 9.x. @@ -69,7 +69,7 @@ multiple modules. For detailed information, please refer to the object and change the ImageJ command to "Resave as HDF5 (local)" for better accuracy. * Match the dialogs of multiview-reconstruction 9.x and BigStitcher 3.x - (checked against the 9.0.11 / 3.0.8 jars): + (checked against the 9.0.11 / 3.0.8 jars, #159): * `imcflibs.imagej.bdv.ProcessingOptions`: timepoints are spelled "Timepoint" (`[Single Timepoint (Select from List)]`, `[Timepoint N]`) and "TimePoints" in the grouping choices (`[Average TimePoints]`). @@ -94,7 +94,7 @@ multiple modules. For detailed information, please refer to the compression, block sizes, pyramid levels, sharding and single-container settings. Compression and block sizes are always passed (MVR's initial defaults), since MVR keeps the values of the previous fusion of the - session; the pyramid levels default to MVR's proposal. Processing + session (#160); the pyramid levels default to MVR's proposal. Processing selectors are passed on; new optional bounding box, `preserve_original`, non-rigid fusion, TIFF `filename_addition` and output base name. * `phase_correlation_pairwise_shifts_calculation`, `filter_pairwise_shifts` diff --git a/src/imcflibs/imagej/bdv.py b/src/imcflibs/imagej/bdv.py index 090098c4..f88c18f2 100644 --- a/src/imcflibs/imagej/bdv.py +++ b/src/imcflibs/imagej/bdv.py @@ -70,6 +70,7 @@ def run_command(command, options): function starts the class from `PLUGIN_CLASSES` and hands over the macro options the same way `IJ.run()` does. Commands without a known class (and environments without `IJ.runPlugIn`, e.g. the test mocks) use `IJ.run()`. + See https://github.com/imcf/python-imcflibs/issues/158 for the details. Parameters ----------