Skip to content

feat(impact-reg): accept a displacement field written as an OME-Zarr store - #68

Merged
vboussot merged 7 commits into
mainfrom
feat/omezarr-rfc5-impact-reg
Jul 28, 2026
Merged

feat(impact-reg): accept a displacement field written as an OME-Zarr store#68
vboussot merged 7 commits into
mainfrom
feat/omezarr-rfc5-impact-reg

Conversation

@vboussot

@vboussot vboussot commented Jul 28, 2026

Copy link
Copy Markdown
Member

Stacked on #67 — review that one first; this branch targets it, not main.

Why

#67 lets KonfAI write a DVF as an NGFF RFC-5 vector field. The orchestrator could not then consume one: every step after inference assumed an ITK image literally called DVF.mha — discovery, the copy beside the results, the ensemble average, and the Transform.h5 export.

A preset emitting a store failed on:

FileNotFoundError: Preset inference did not produce 'DVF.mha' under /tmp/...

with the store sitting in the directory it had just listed, because Path("DVF.ome.zarr").stem is "DVF.ome".

What

  • Discovery by name, not by filename, so either form is found.
  • _copy_output keeps the form the preset produced (file → copy2, store → copytree), and replaces an existing output of either kind so a re-run overwrites cleanly.
  • _write_displacement_field writes a derived (averaged) field in the form its members arrived in, so an ensemble does not silently change format.
  • Reading goes through konfai.utils.ITK.read_displacement_field, which opens an ITK image or a store.

That reader lives in konfai, not here, deliberately: reading a field as if it were an ordinary image is a failure that does not announce itself — the component axis looks like any other, so indexing it returns a third of the field, and a registration that is wrong without looking wrong. One place should know how to open either form.

Tests

apps/impact_reg/tests/unit/test_displacement_field_io.py — parametrised over .mha and .ome.zarr: discovery finds either, the copy preserves the form and overwrites a store, an averaged field stays a declared field, and the transform reads back identically from both.

The last one is asserted on a mapped point, not on the array: a field read with its component axis mishandled keeps a plausible shape, and only applying it exposes that.

34 orchestrator unit tests pass; ruff, ruff format, mypy, bandit clean.

Summary by CodeRabbit

  • New Features
    • Added generic handling of displacement-field outputs across SimpleITK image files and OME-Zarr stores.
  • Bug Fixes
    • Improved discovery/copying of displacement-field outputs while preserving their original representation form.
    • Fixed ensemble DVF averaging and transform generation for consistent results across formats.
    • Ensured overwriting an OME-Zarr store refreshes cached reads; standardized OME-Zarr geometry/attribute handling.
  • Tests
    • Added unit coverage for round-tripping, output discovery, overwrite behavior, and correct errors when OME-Zarr displacement-field support isn’t available.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds unified displacement-field loading for ITK files and NGFF OME-Zarr stores, updates registration and uncertainty workflows to preserve and process either representation, and centralizes OME-Zarr metadata and cache handling. Tests cover discovery, copying, replacement, validation, and transform equivalence.

Changes

Displacement-field I/O

Layer / File(s) Summary
OME-Zarr metadata, cache handling, and field reader
konfai/utils/dataset.py, konfai/utils/ome_zarr.py, konfai/utils/ITK.py, tests/unit/test_imaging_formats.py, tests/unit/test_omezarr_displacement_field.py
OME-Zarr attributes and cache clearing are centralized, displacement-field writes retain Zarr v3 validation, and read_displacement_field loads vector fields from files or RFC-5 stores.
Format-agnostic registration pipeline
apps/impact_reg/impact_reg_konfai/impact_reg.py
Preset outputs are discovered by stem, copied in their original form, averaged through the unified reader, and used for transforms and uncertainty processing.
Representation and transform validation
apps/impact_reg/tests/unit/test_displacement_field_io.py
Tests validate discovery, copying, store replacement, cache behavior, displacement metadata, and equivalent transforms across file and store representations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Preset
  participant ImpactReg
  participant read_displacement_field
  participant OMEZarr
  Preset->>ImpactReg: produce Moved and DVF by stem
  ImpactReg->>OMEZarr: preserve or copy output representation
  ImpactReg->>read_displacement_field: read DVF file or store
  read_displacement_field-->>ImpactReg: SimpleITK vector field
  ImpactReg->>OMEZarr: write compatible averaged DVF
  ImpactReg->>ImpactReg: write Transform.h5 from dvf_out
Loading

Possibly related PRs

Poem

A bunny found a field in a store,
Then read it from a file once more.
DVFs hopped through every form,
Averaged fields stayed true and warm.
“Transform!” cried Bun, with joyful glee—
“The same mapped points for you and me!”

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is on-topic but does not follow the repository template or include required sections like Related issues, Type of change, and Checklist. Rewrite the PR description to match the template headings and add Related issues, Type of change, testing details, checklist items, and breaking-change notes if needed.
Docstring Coverage ⚠️ Warning Docstring coverage is 79.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: accepting OME-Zarr displacement fields.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/omezarr-rfc5-impact-reg

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/impact_reg/impact_reg_konfai/impact_reg.py`:
- Around line 109-116: The displacement-field OME-Zarr round-trip must preserve
the fixed-grid direction matrix alongside origin and spacing. In
apps/impact_reg/impact_reg_konfai/impact_reg.py lines 109-116, update
write_ome_zarr to persist Origin, Spacing, and Direction through the OME-Zarr
attributes contract; in konfai/utils/ITK.py lines 69-72, restore all three
attributes onto the SimpleITK image, including SetDirection(), before
constructing transforms. Update
apps/impact_reg/tests/unit/test_displacement_field_io.py lines 45-50 to use a
non-identity direction matrix and assert it round-trips.
- Around line 247-249: Update uncertainty() to replace both sitk.ReadImage calls
with read_displacement_field(), using the preserved ensemble paths so .ome.zarr
displacement fields are supported. Add a regression test covering uncertainty
calculation with OME-Zarr ensemble members.

In `@konfai/utils/ITK.py`:
- Line 63: Replace the full-volume `read_ome_zarr_data_slice` call in the
surrounding ITK data-processing flow with lazy patch/stream processing; avoid
constructing all-axis `slice(None)` requests or retaining the complete C[Z]YX
field in memory. Use the existing `can_stream_patch` and `read_data_slice`
mechanisms where applicable, processing and writing each patch incrementally
while preserving the current metadata and output behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 57a9d0fa-11ed-4fbc-88c3-6b1a7f063f09

📥 Commits

Reviewing files that changed from the base of the PR and between e82fbe1 and a946985.

📒 Files selected for processing (3)
  • apps/impact_reg/impact_reg_konfai/impact_reg.py
  • apps/impact_reg/tests/unit/test_displacement_field_io.py
  • konfai/utils/ITK.py

Comment thread apps/impact_reg/impact_reg_konfai/impact_reg.py Outdated
Comment thread apps/impact_reg/impact_reg_konfai/impact_reg.py
Comment thread konfai/utils/ITK.py Outdated
@vboussot
vboussot force-pushed the feat/omezarr-rfc5-impact-reg branch from a946985 to f7b5514 Compare July 28, 2026 14:32
@vboussot
vboussot force-pushed the feat/omezarr-rfc5-displacement branch from 92571f0 to 8d75f72 Compare July 28, 2026 15:10
@vboussot
vboussot force-pushed the feat/omezarr-rfc5-impact-reg branch from f7b5514 to df5c577 Compare July 28, 2026 15:10
Base automatically changed from feat/omezarr-rfc5-displacement to main July 28, 2026 15:46
@vboussot
vboussot force-pushed the feat/omezarr-rfc5-impact-reg branch from df5c577 to 2a1d105 Compare July 28, 2026 15:50
vboussot added 2 commits July 28, 2026 17:52
…store

Every step after inference assumed the field was an ITK image called DVF.mha:
discovery, the copy beside the results, the ensemble average, and the Transform.h5
export. A preset emitting an RFC-5 store therefore failed on "Preset inference did
not produce 'DVF.mha'" -- with the store sitting in the directory just listed,
because Path("DVF.ome.zarr").stem is "DVF.ome".

Outputs are now located by NAME and copied in whatever form the preset produced,
a derived (averaged) field is written in the form its members came in, and reading
goes through konfai.utils.ITK.read_displacement_field, which opens either an ITK
image or a store. That reader lives in konfai rather than here: reading a field as
if it were an ordinary image is a failure that does not announce itself -- the
component axis looks like any other, so indexing it returns a third of the field
and a registration that is wrong without looking wrong -- and one place should
know how to open either form.
…OME-Zarr

The OME-Zarr write dropped the field's direction matrix and the reader never
restored it, so a non-identity fixed-grid direction came back a geometrically
wrong transform -- silently, the very failure the store typing is meant to end.
Both sides now go through image_to_data / data_to_image, the encoding the
Dataset backend already uses, so geometry (Origin, Spacing, Direction)
round-trips and neither side re-derives it. The shared geometry builder is
lifted out of the Dataset reader into ome_zarr_attributes().

- uncertainty() reads its ensemble members through read_displacement_field too,
  so an .ome.zarr DVF no longer fails on a raw sitk.ReadImage
- _average_displacement accumulates a running sum (two fields at peak, not the
  whole ensemble stacked)
- the round-trip test uses a non-identity Direction, and the suite skips where
  zarr v3 is unavailable (Python 3.10)
@vboussot
vboussot force-pushed the feat/omezarr-rfc5-impact-reg branch from 2a1d105 to 53f58f6 Compare July 28, 2026 15:53
vboussot added 2 commits July 28, 2026 17:56
The guard that turns an unwritable RFC-5 store into a named requirement was
asserted nowhere: on zarr 3 the branch is never taken, and on zarr 2 the
displacement suite skips wholesale, so it went unverified on every leg of the
matrix. Covered beside _require_zarr, whose capability guard is tested the same
way, so it runs on 3.10 too -- moving the check below ngff-zarr's writer now
fails the suite instead of silently restoring the opaque ValueError.

The DIRECTION constant called a rotation matrix "anisotropic" (the spacing is
what is anisotropic) and restated the helper docstring beside it.
The OME-Zarr reader memoises NGFF metadata per store path, on the standing
assumption that whatever writes a store invalidates it. _copy_output writes one
by copying, so a second registration into the same output directory hit the memo
and paired the copy's voxels with the replaced store's axes and geometry: a
shape mismatch surfaces as a reshape ValueError naming neither the cache nor the
store, and equal shapes surface as nothing at all -- the field simply sampled in
the wrong place.

The invalidation the write paths already performed is now clear_ome_zarr_cache(),
which _copy_output calls after replacing a store, and which says what callers
outside the module owe it.

The regression test uses a store with no konfai sidecar, as a third-party RFC-5
producer emits: konfai's own sidecar is read outside the memo and masks the
staleness. Removing the invalidation fails it on geometry, not on a crash.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/unit/test_omezarr_displacement_field.py (1)

36-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Scope the Zarr v3 skip to RFC-5-specific tests.

The module-wide mark also skips the plain-image, missing-store, and affine-rejection tests, even though those paths do not require RFC-5 writing. This removes useful coverage on supported Zarr 2 environments; apply the skip only to RFC-5 tests or split the test modules.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_omezarr_displacement_field.py` around lines 36 - 43, Remove
the module-wide skipif based on _zarr_v3_available and apply that marker only to
the RFC-5 displacement-field tests. Keep plain-image, missing-store, and
affine-rejection tests runnable on Zarr 2 environments, either by decorating the
RFC-5-specific test functions or separating them into a dedicated module.
apps/impact_reg/impact_reg_konfai/impact_reg.py (1)

104-123: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two independent implementations of the same OME-Zarr displacement-field write contract. Both sites capture Origin/Spacing/Direction and call write_ome_zarr(..., displacement_field=True), but they do it separately — the same kind of divergence that caused the earlier "dropped Direction" critical bug this PR fixes.

  • apps/impact_reg/impact_reg_konfai/impact_reg.py#L104-L123: promote _write_displacement_field into konfai/utils/ITK.py as a public write_displacement_field(field, dest), symmetric to the existing read_displacement_field, and call it here.
  • konfai/utils/dataset.py#L1363-L1392: reuse the same helper's core serialization (Origin/Spacing/Direction capture) inside the sitk.Transform branch instead of maintaining a parallel path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/impact_reg/impact_reg_konfai/impact_reg.py` around lines 104 - 123,
Promote _write_displacement_field into public
konfai.utils.ITK.write_displacement_field, symmetric with
read_displacement_field, and replace the local implementation in
apps/impact_reg/impact_reg_konfai/impact_reg.py:104-123 with that helper. In
konfai/utils/dataset.py:1363-1392, update the sitk.Transform branch to reuse the
helper’s shared OME-Zarr serialization, preserving Origin, Spacing, Direction,
and displacement_field=True rather than maintaining a parallel implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/impact_reg/impact_reg_konfai/impact_reg.py`:
- Around line 251-266: Clear existing outputs with the same logical stem before
writing current results in the single-preset and ensemble branches. Update the
cleanup around _copy_output and _write_displacement_field so stale DVF, Moved,
and per-preset ensemble files with different suffixes are removed while
preserving unrelated outputs, then write the current-format files as before.

---

Nitpick comments:
In `@apps/impact_reg/impact_reg_konfai/impact_reg.py`:
- Around line 104-123: Promote _write_displacement_field into public
konfai.utils.ITK.write_displacement_field, symmetric with
read_displacement_field, and replace the local implementation in
apps/impact_reg/impact_reg_konfai/impact_reg.py:104-123 with that helper. In
konfai/utils/dataset.py:1363-1392, update the sitk.Transform branch to reuse the
helper’s shared OME-Zarr serialization, preserving Origin, Spacing, Direction,
and displacement_field=True rather than maintaining a parallel implementation.

In `@tests/unit/test_omezarr_displacement_field.py`:
- Around line 36-43: Remove the module-wide skipif based on _zarr_v3_available
and apply that marker only to the RFC-5 displacement-field tests. Keep
plain-image, missing-store, and affine-rejection tests runnable on Zarr 2
environments, either by decorating the RFC-5-specific test functions or
separating them into a dedicated module.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b4213770-e03d-4744-ae3e-a6a3dc7bf5bb

📥 Commits

Reviewing files that changed from the base of the PR and between f7b5514 and 2a1d105.

📒 Files selected for processing (7)
  • apps/impact_reg/impact_reg_konfai/impact_reg.py
  • apps/impact_reg/tests/unit/test_displacement_field_io.py
  • konfai/utils/ITK.py
  • konfai/utils/dataset.py
  • konfai/utils/ome_zarr.py
  • tests/unit/test_imaging_formats.py
  • tests/unit/test_omezarr_displacement_field.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • konfai/utils/ITK.py
  • apps/impact_reg/tests/unit/test_displacement_field_io.py

Comment thread apps/impact_reg/impact_reg_konfai/impact_reg.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@konfai/utils/dataset.py`:
- Around line 356-362: Update the geometry parsing around the scale and
translation mappings to validate that each metadata array has the same length as
axes; raise DatasetManagerError when either length mismatches, before
constructing the mappings. Remove the per-axis 1.0/0.0 fallbacks in the Spacing
and Origin construction so inconsistent NGFF geometry is rejected rather than
fabricated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 44273b1e-26d3-4fff-a278-2365c6b35c03

📥 Commits

Reviewing files that changed from the base of the PR and between 2a1d105 and 0f0f781.

📒 Files selected for processing (7)
  • apps/impact_reg/impact_reg_konfai/impact_reg.py
  • apps/impact_reg/tests/unit/test_displacement_field_io.py
  • konfai/utils/ITK.py
  • konfai/utils/dataset.py
  • konfai/utils/ome_zarr.py
  • tests/unit/test_imaging_formats.py
  • tests/unit/test_omezarr_displacement_field.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • konfai/utils/ITK.py
  • tests/unit/test_imaging_formats.py
  • apps/impact_reg/impact_reg_konfai/impact_reg.py
  • konfai/utils/ome_zarr.py

Comment thread konfai/utils/dataset.py
Comment on lines +356 to +362
scale = dict(zip(axes, metadata.get("scale", []), strict=False))
translation = dict(zip(axes, metadata.get("translation", []), strict=False))
spatial_axes = [axis for axis in ("x", "y", "z") if axis in axes]
if "Spacing" not in attributes:
attributes["Spacing"] = np.asarray([scale.get(axis, 1.0) for axis in spatial_axes])
if "Origin" not in attributes:
attributes["Origin"] = np.asarray([translation.get(axis, 0.0) for axis in spatial_axes])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect the relevant region.
ast-grep outline konfai/utils/dataset.py --view expanded || true
echo '---'
sed -n '300,420p' konfai/utils/dataset.py | cat -n

echo '--- nearby references ---'
rg -n "ome_zarr_attributes|Spacing|Origin|scale|translation|strict=False|DatasetManagerError" konfai/utils/dataset.py

Repository: fideus-labs/KonfAI

Length of output: 16868


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect any tests or callers that constrain OME-Zarr geometry handling.
rg -n "ome_zarr_attributes|Spacing|Origin|translation|scale|OME-Zarr|ngff|zarr" konfai tests .

Repository: fideus-labs/KonfAI

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the OME-Zarr reader/writer path around metadata creation and consumption.
sed -n '1260,1355p' konfai/utils/dataset.py | cat -n
echo '---'
sed -n '1,260p' konfai/utils/ome_zarr.py | cat -n
echo '---'
sed -n '260,420p' konfai/utils/ome_zarr.py | cat -n

Repository: fideus-labs/KonfAI

Length of output: 26623


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Probe how ome_zarr_attributes behaves for malformed metadata.
python3 - <<'PY'
from pathlib import Path
import importlib.util
import sys

path = Path("konfai/utils/dataset.py")
spec = importlib.util.spec_from_file_location("dataset_mod", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)

cases = [
    {
        "name": "short scale",
        "metadata": {"axes": ["c", "z", "y", "x"], "scale": [1.0, 2.0], "translation": [0.0, 3.0, 4.0, 5.0]},
    },
    {
        "name": "long translation",
        "metadata": {"axes": ["c", "z", "y", "x"], "scale": [1.0, 2.0, 3.0, 4.0], "translation": [0.0, 3.0, 4.0, 5.0, 6.0]},
    },
    {
        "name": "missing z scale",
        "metadata": {"axes": ["c", "z", "y", "x"], "scale": [1.0, 0.5, 0.6], "translation": [0.0, 1.0, 2.0, 3.0]},
    },
]
for case in cases:
    attrs = mod.ome_zarr_attributes(case["metadata"])
    print(case["name"])
    print("  axes:", attrs["OMEAxes"].tolist())
    print("  spacing:", attrs.get_np_array("Spacing").tolist())
    print("  origin:", attrs.get_np_array("Origin").tolist())
PY

Repository: fideus-labs/KonfAI

Length of output: 505


Reject inconsistent NGFF geometry instead of defaulting it. zip(..., strict=False) will silently drop axes when scale or translation is shorter than axes, and the later .get(..., 1.0/0.0) fallback fabricates plausible geometry. Validate the lengths and raise DatasetManagerError on mismatch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@konfai/utils/dataset.py` around lines 356 - 362, Update the geometry parsing
around the scale and translation mappings to validate that each metadata array
has the same length as axes; raise DatasetManagerError when either length
mismatches, before constructing the mappings. Remove the per-axis 1.0/0.0
fallbacks in the Spacing and Origin construction so inconsistent NGFF geometry
is rejected rather than fabricated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@konfai/utils/ITK.py`:
- Around line 53-65: The directory-loading branch in the ITK image conversion
flow must validate the OME-Zarr RFC-5 declaration before reading or converting
data. Use the existing is_displacement_field metadata check and raise
TransformError for non-displacement stores before
get_ome_zarr_info/read_ome_zarr_data_slice or data_to_image execute, then add a
regression test covering a normal three-channel OME-Zarr directory.
- Around line 41-65: Redesign read_displacement_field so OME-Zarr inputs are
processed lazily in patches or streamed chunks instead of requesting the full
C[Z]YX volume via read_ome_zarr_data_slice or retaining full-volume copies in
data_to_image and sitk.Cast. Reuse the repository’s
can_stream_patch/read_data_slice mechanisms where applicable, while preserving
the existing SimpleITK-file path and resulting displacement-field geometry and
pixel type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 98b4c57b-2537-43d7-b014-822c0f3da0d3

📥 Commits

Reviewing files that changed from the base of the PR and between d15c1ae and 0f0f781.

📒 Files selected for processing (7)
  • apps/impact_reg/impact_reg_konfai/impact_reg.py
  • apps/impact_reg/tests/unit/test_displacement_field_io.py
  • konfai/utils/ITK.py
  • konfai/utils/dataset.py
  • konfai/utils/ome_zarr.py
  • tests/unit/test_imaging_formats.py
  • tests/unit/test_omezarr_displacement_field.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/unit/test_imaging_formats.py
  • apps/impact_reg/tests/unit/test_displacement_field_io.py
  • konfai/utils/dataset.py
  • apps/impact_reg/impact_reg_konfai/impact_reg.py

Comment thread konfai/utils/ITK.py
Comment on lines +41 to +65
def read_displacement_field(path: str | Path) -> sitk.Image:
"""A displacement field, from an ITK image file OR an NGFF RFC-5 OME-Zarr store.

A field written as a store cannot go through ``sitk.ReadImage``, and reading it as an ordinary
image is worse than failing: the component axis looks like any other, so indexing it yields one
third of the field and a registration that is wrong without being obviously wrong. This is the one
place that knows how to open either form, so no caller has to decide.

The result is ``sitkVectorFloat64`` -- what ``DisplacementFieldTransform`` requires.
"""
_require_simpleitk()
path = Path(path)
if not path.is_dir():
return sitk.ReadImage(str(path), sitk.sitkVectorFloat64)

from konfai.utils.dataset import data_to_image, ome_zarr_attributes
from konfai.utils.ome_zarr import get_ome_zarr_info, read_ome_zarr_data_slice

axes = get_ome_zarr_info(path)["axes"]
n_axes = 1 + sum(axis in axes for axis in ("z", "y", "x")) # channel-first C[Z]YX
data, metadata = read_ome_zarr_data_slice(path, tuple(slice(None) for _ in range(n_axes)))
# Origin, Spacing and Direction together: NGFF scale/translation alone cannot express the
# direction matrix, so the geometry comes from the konfai sidecar through data_to_image.
field = data_to_image(data, ome_zarr_attributes(metadata))
return sitk.Cast(field, sitk.sitkVectorFloat64)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major

Do not materialize the entire OME-Zarr field.

Line 61 requests every voxel and then retains additional full-volume buffers through data_to_image and sitk.Cast, which can OOM on clinical DVFs. Redesign this boundary around lazy patch/stream processing rather than constructing the complete C[Z]YX field.

As per coding guidelines, “Never load a full imaging volume into RAM; use lazy, patch-based, or streaming access such as can_stream_patch and read_data_slice.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@konfai/utils/ITK.py` around lines 41 - 65, Redesign read_displacement_field
so OME-Zarr inputs are processed lazily in patches or streamed chunks instead of
requesting the full C[Z]YX volume via read_ome_zarr_data_slice or retaining
full-volume copies in data_to_image and sitk.Cast. Reuse the repository’s
can_stream_patch/read_data_slice mechanisms where applicable, while preserving
the existing SimpleITK-file path and resulting displacement-field geometry and
pixel type.

Source: Coding guidelines

Comment thread konfai/utils/ITK.py
…ut per stem

Two holes the RFC-5 declaration was meant to close but nobody consulted.

read_displacement_field treated any directory as a field without asking the
store what it held, so an ordinary three-channel OME-Zarr image became a working
DisplacementFieldTransform -- a registration to nowhere that never failed. It
now reads the declaration it was built to trust and raises TransformError.

_copy_output removed only the exact path it was about to write, so a re-run
emitting the other form left DVF.mha beside DVF.ome.zarr. Discovery is by stem
and takes the first match, which sorts to the stale one. Clearing every output
of the stem is shared with the ensemble branch through _output_path, and is
matched on a literal prefix so a preset name never reads as a glob pattern.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
apps/impact_reg/impact_reg_konfai/impact_reg.py (1)

281-282: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Clear stale Moved.* outputs in the ensemble path.

Line 281 cleans only DVF.*; the subsequent hard-coded Moved.mha write leaves a prior Moved.ome.zarr behind. This still violates the same-stem replacement contract on a single-preset-to-ensemble rerun.

Proposed fix
                     dvf_out = _output_path(case_out, "DVF", "".join(dvf_paths[0].suffixes))
                     _write_displacement_field(avg_dvf, dvf_out)
                     transform = sitk.DisplacementFieldTransform(sitk.Cast(avg_dvf, sitk.sitkVectorFloat64))
                     moving = sitk.ReadImage(str(moving_image))
+                    moved_out = _output_path(case_out, "Moved", ".mha")
                     sitk.WriteImage(
                         sitk.Resample(moving, avg_dvf, transform, sitk.sitkLinear, 0.0, moving.GetPixelID()),
-                        str(case_out / "Moved.mha"),
+                        str(moved_out),
                     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/impact_reg/impact_reg_konfai/impact_reg.py` around lines 281 - 282,
Update the ensemble output handling around _write_displacement_field to remove
stale Moved.* files before writing the hard-coded Moved.mha output, ensuring
prior Moved.ome.zarr or other same-stem artifacts are cleared on reruns while
preserving the existing DVF cleanup behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@apps/impact_reg/impact_reg_konfai/impact_reg.py`:
- Around line 281-282: Update the ensemble output handling around
_write_displacement_field to remove stale Moved.* files before writing the
hard-coded Moved.mha output, ensuring prior Moved.ome.zarr or other same-stem
artifacts are cleared on reruns while preserving the existing DVF cleanup
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7b8f049a-493a-4c68-af42-0de937672ecb

📥 Commits

Reviewing files that changed from the base of the PR and between 0f0f781 and c88d7fc.

📒 Files selected for processing (3)
  • apps/impact_reg/impact_reg_konfai/impact_reg.py
  • apps/impact_reg/tests/unit/test_displacement_field_io.py
  • konfai/utils/ITK.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • konfai/utils/ITK.py
  • apps/impact_reg/tests/unit/test_displacement_field_io.py

vboussot added 2 commits July 28, 2026 18:34
The RFC-5 displacement-field work raised the floor to ngff-zarr>=0.38 in
pyproject.toml, but the lock beside it still pinned 0.37.0 — a lock that does
not satisfy its own manifest. Anyone installing from it had to re-solve, so
the environment stopped being the reproducible one it exists to guarantee.

Relocked to 0.40.0 and the transitive versions that came with it.
`pixi lock --check` is clean.
The ensemble branch writes Moved.mha verbatim, so a preceding single-preset run
whose model emitted a store left Moved.ome.zarr beside it -- the same-stem
contract the DVF side had just been given. It goes through _output_path as well.

Covered on the helper rather than on one caller, since both writers lean on it,
including the assertion that a stem is matched whole: MovedMask.mha shares a
prefix with Moved and must survive.
@vboussot
vboussot merged commit 04c3269 into main Jul 28, 2026
32 of 34 checks passed
@vboussot
vboussot deleted the feat/omezarr-rfc5-impact-reg branch July 28, 2026 18:17
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