Skip to content

Add Open Resolution Level command and Pyramidal dataset lifecycle management#90

Merged
stefanhahmann merged 63 commits into
mainfrom
dataset-lifecycle-tobi
Jun 11, 2026
Merged

Add Open Resolution Level command and Pyramidal dataset lifecycle management#90
stefanhahmann merged 63 commits into
mainfrom
dataset-lifecycle-tobi

Conversation

@stefanhahmann

@stefanhahmann stefanhahmann commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Replaces PR #80

Resolves Issue #41
Resolves Issue #79
Resolves Issue #14

Summary

  • Adds OpenResolutionLevelCommand (menu: Plugins > OME-Zarr > Open Resolution Level…) to open a specific resolution level of an OME-Zarr multi-resolution dataset as a new ImageJ dataset
  • Adds per-level dataset access (asPyramidalDataset(int)) and NonExistingResolutionLevelException in the pyramid API
  • Introduces the Pyramidal interface (implemented by PyramidalDataset and PyramidalBdvDataset)
  • Replaces BdvHandleService with PyramidalService, which tracks the most recently focused Pyramidal window (BDV or IJ)
  • PyramidalPreprocessor auto-resolves @Parameter Pyramidal command inputs from the active window
  • OpenInBDVCommand creates a distinct PyramidalBdvDataset for each BDV opened, sharing the same underlying Pyramidal5DImageData (pixel data loaded only once)
  • Stores axis calibration per resolution level in PyramidContents / AxisCalibration; physical extent (pixels × scale) is verified to be identical across all resolution levels
  • Selecting resolution level via the menu is not yet part of this PR to not make it too big.
  • Showing dimensions (XYZCT) during the resolution level selection is also not yet part of this PR to not make it too big.

Test plan

  • Run mvn test — all existing and new tests pass

Open in IJ → Open Resolution Level

  • Drag-and-drop an OME-Zarr onto Fiji → opens as IJ2 dataset (1 dataset in DatasetService, 1 pyramidal in PyramidalService)
  • Run Plugins > OME-Zarr > Open Resolution Level… with the IJ2 dataset active → dialog shows resolution choices, selecting a level opens a new IJ2 window (2 datasets total); both share the same pyramid data

Open in BDV → Open Resolution Level

  • Drag-and-drop an OME-Zarr onto Fiji → open in BDV (0 dataset in DatasetService, 1 pyramidal in PyramidalService)
  • With the BDV window focused, run Open Resolution Level… without an active IJ2 display → command falls back to the focused BDV dataset, opens the selected level as a new IJ2 window (2 pyramidals in total, 1 dataset)

Open in IJ → Open in BDV

  • Drag-and-drop → opens as IJ2 dataset (1 dataset)
  • Run Plugins > OME-Zarr > Open in BDV with the IJ2 dataset active → BDV window opens (2 pyramidals in total, 1 dataset); BDV is a distinct PyramidalBdv sharing the same underlying pyramid data as the IJ2 dataset

Open in BDV → Open in BDV again

  • Drag-and-drop → open in BDV (1 pyramidal)
  • With BDV window focused, run Open in BDV again (no active IJ2 display) → command falls back to focused BDV dataset, a second BDV window opens (2 pyramidals, 0 datasets in total); both BDV share the same underlying pyramid data

Open dataset A in IJ → Open dataset B in BDV → Open resolution level

  • Drag-and-drop → open in IJ (1 pyramidal, 1 dataset)
  • Drag-and-drop → open in BDV (2 pyramidals, 1 dataset)
  • With BDV window active run Open Resolution Level, select resolution level 1; The resolution level 1 of the dataset open in the BDV window should be shown.

Error cases

  • Run Open Resolution Level… or Open in BDV with no image open → command cancels with a "No image is currently open" message dialog
  • Close the BDV window, then run Open in BDV → same cancellation message

Jython Macro for testing

#@ DatasetService ds
#@ PyramidalService ps

print("----")
for dimg in ds.getDatasets():
	print("image '"+dimg.getImgPlus().getName()+"' (id: "+str(id(dimg))+\
	") of width x height: "+str(dimg.getWidth())+'x'+str(dimg.getHeight()))
for pyramidal in ps.getPyramidals():
	print("pyramidal '"+pyramidal.getPyramidName()+"' (id: "+str(id(pyramidal))+")")
print("----")

stefanhahmann and others added 30 commits May 27, 2026 15:12
Introduces the method signature for opening a specific pyramid level as
an ImageJ dataset. Index 0 is the highest resolution; each increment is
the next coarser level.

Per the intended design, all datasets opened from the same pyramid
(across IJ and BDV) will share the same Pyramidal5DImageData instance,
so cachedCellImgs / volatileImgs are never loaded more than once.

The body currently throws UnsupportedOperationException; it will be
filled in as part of the resolution-level dataset lifecycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exposes the backing Pyramidal5DImageData reference held by a
PyramidalDataset. This accessor is needed both for the upcoming
"open specific resolution in IJ" command and for verifying in tests that multiple open datasets share
the same underlying pyramid instance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two tests documenting the expected dataset-lifecycle behaviour
when opening a multi-resolution OME-Zarr in ImageJ and BigDataViewer:

Scenario – open each resolution level in IJ, then BDV twice:
  - 2 openIJWithImage(level) calls → 2 datasets in DatasetService
  - each openBDVWithImage() call → one additional dataset
  - all datasets must be backed by the exact same Pyramidal5DImageData
    instance (verified via assertSame on getPyramidData())

Scenario – open in BDV first, then a specific level in IJ:
  - openBDVWithImage() → 1 dataset
  - openIJWithImage(1) → 2nd dataset, sharing the same
    Pyramidal5DImageData as the BDV dataset

Both tests currently error with UnsupportedOperationException because
openIJWithImage(int) is not yet implemented.

Test dataset: 5d_dataset_v4.ome.zarr (2 levels:
  level 0 = [x=64,y=64,z=16,c=3,t=4],
  level 1 = [x=32,y=32,z=8,c=3,t=4])

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously PyramidContents.axes held calibration only for the resolution
level 0, so opening any other level would apply the wrong physical scales.
Replace the single AxisCalibration[] with AxisCalibration[][] (one entry
per level) in PyramidContents and update both backends to build the full
per-level array during load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add NonExistingResolutionLevelException (RuntimeException) for when a
requested resolution level index is out of range. Add a bounds check in
Pyramidal5DImageDataImpl.asPyramidalDataset(int) and asDataset(int) that
throws this exception, and expose asPyramidalDataset(int) on the
Pyramidal5DImageData interface.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- All three public openers (openIJWithImage, openIJWithImage(int),
  openBDVWithImage) and the test-facing openImage now delegate to a
  shared openPyramidImage that handles the three common exceptions
  (MultiImageDatasetException, NotAMultiscaleImageException,
  IllegalArgumentException/JsonSyntaxException).
- Each caller adds only its own specific catch on top:
  NoMatchingResolutionException for the no-arg IJ and BDV openers,
  NonExistingResolutionLevelException for openIJWithImage(int).
- getPyramid() now folds preferredWidth from settings so the cached
  pyramid is built with the correct initial resolution level for all
  callers; openMultiScaleImage() is removed.
- Remove the unused message parameter from the test-facing openImage().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Confirm that all datasets backed by the same resolution level of the same pyramid wrap the identical CachedCellImg object, so that chunks loaded for one view are served from cache to any other view at the
same level — not loaded twice. Also asserts that different resolution levels use distinct CachedCellImg instances, as expected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ialog

Implements a DynamicCommand that populates a dropdown with one entry per
resolution level of the active PyramidalDataset, then opens the chosen
level as a new dataset backed by the same pyramid data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers: cancel on null dataset, cancel on non-pyramidal dataset,
choice list population, opening the correct resolution level,
and shared pyramid data identity across levels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Provides a minimal 16x16 v0.5 dataset with exactly one resolution level,
used to verify title-suffix behaviour that differs from multi-resolution
pyramids. Includes creation script, conda.yml, and README.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Multi-resolution images get an (R) suffix in the dataset name so the
title in ImageJ and BDV windows signals that other resolution levels
are available. Single-resolution images are left unchanged. A null or
empty base name produces just (R) rather than (null) (R).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…acking

BdvHandleService had a mix of concerns: it managed BdvStackSource handles
(openNewBdv, addToLastOrInNewBdv) and was later intended to track BDV window focus for command resolution.
The stack-source API was not yet wired up in production and is currently not needed.

The new BdvFocusService tracks which PyramidalDataset is in the currently
focused BDV window via a WindowFocusListener registered in BdvUtils. This
lets commands resolve the correct dataset when the @parameter injection via
DisplayService does not find an active ImageDisplay (which is the case for
BDV windows, which are plain Swing frames rather than SciJava displays).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the command is invoked from a BDV context, @parameter Dataset injection
returns null because BDV windows are not SciJava ImageDisplays. Injecting
BdvFocusService and calling resolveDataset() in initialize() covers this
case by falling back to the most recently focused BDV window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two new tests verify that opening a resolution level via the command shares
the same Pyramidal5DImageData as the source dataset, both for the BDV
(simulated via BdvFocusService#notifyWindowFocused) and IJ2 code paths.

BdvHandleService reflects the previously existing class 1:1 and was now moved to the test resources for further reference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two tests capture the expected behaviour that invoking the command always
produces a second, distinct PyramidalDataset sharing the same
Pyramidal5DImageData as the source:

- runAfterIj2OpenCreatesSecondBdvDataset: source was opened in IJ2, dataset
  is set directly on the command
- runAfterBdvOpenCreatesSecondBdvDataset: source was opened in BDV (simulated
  via BdvFocusService), dataset is null and must be resolved via the service

Both tests currently fail: the first because the command opens the same
dataset object in BDV rather than creating a new wrapper, the second because
the command does not fall back to BdvFocusService when dataset is null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the command called showBdvAndRegisterDataset with the same
PyramidalDataset object that was already registered (from an earlier IJ2
or BDV open). Because incrementReferences() only fires DataCreatedEvent on
the 0→1 transition, the second open never appeared as a separate entry in
DatasetService.

The fix creates a new PyramidalDataset wrapper via
getPyramidal5DImageData().asPyramidalDataset() before passing it to BDV,
so each "Open in BDV" produces a fresh registration with a refcount of its
own. Also adds BdvFocusService fallback in run() so the command can resolve
the active dataset when invoked from a BDV context (where @parameter Dataset
injection returns null).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract registerDatasetLifecycle and registerFocusTracking as private
helpers to clarify the two concerns in showBdvAndRegisterDataset.
Also renames the bdvFocusService parameter (was bdvHandleService) and
updates the stale javadoc that still referenced the old name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes Dataset as a @parameter so SciJava's input harvester never sees it
and therefore never shows a chooser dialog when no ImageJ display is active.
The active dataset is now resolved explicitly in run(): first via
ImageDisplayService (FIJI case), then via BdvFocusService (BDV-only case).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the single logService.error call with two distinct IJ.error dialogs:
one when no image is open at all, and one when the active image is not an
OME-Zarr dataset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Streamline reference counting and window-close event notification in `registerDatasetLifecycle`.
- Move focus notifications to lifecycle registration and consolidate `WindowFocusListener` in `registerFocusTracking`
stefanhahmann and others added 18 commits June 11, 2026 15:08
Adjust imports and types throughout the test suite: `PyramidalDataset<?>` casts
become `PyramidalDataset`, `Function<PyramidalDataset<?>,…>` becomes
`Function<PyramidalDataset,…>`, and command tests construct and inject
`Pyramidal` objects directly instead of routing through dataset resolution
helpers.

Co-authored-by: tpietzsch <tobias.pietzsch@gmail.com>
Co-authored-by: tpietzsch <tobias.pietzsch@gmail.com>
Co-authored-by: tpietzsch <tobias.pietzsch@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…and also update javadocs of the new `PyramidalService`
…al5DImageDataImpl` to use `AbstractContextual`.
@stefanhahmann stefanhahmann force-pushed the dataset-lifecycle-tobi branch 2 times, most recently from edc1922 to b76fd9b Compare June 11, 2026 13:13
@stefanhahmann stefanhahmann force-pushed the dataset-lifecycle-tobi branch from b76fd9b to 9d88ea1 Compare June 11, 2026 13:14
@sonarqubecloud

Copy link
Copy Markdown

@stefanhahmann stefanhahmann merged commit b7a1b46 into main Jun 11, 2026
6 checks passed
@stefanhahmann stefanhahmann deleted the dataset-lifecycle-tobi branch June 11, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant