Skip to content

fix(downsample): shift output origin to coarse-grid pixel centers - #1556

Merged
thewtex merged 5 commits into
InsightSoftwareConsortium:mainfrom
thewtex:downsample-translation
Jul 21, 2026
Merged

fix(downsample): shift output origin to coarse-grid pixel centers#1556
thewtex merged 5 commits into
InsightSoftwareConsortium:mainfrom
thewtex:downsample-translation

Conversation

@thewtex

@thewtex thewtex commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the output-origin defect in the downsample and downsample-label-image pipelines reported in #1409. Both pipelines resampled onto a coarser grid but kept the input origin unchanged, so the downsampled image was misaligned by half a coarse pixel relative to the physically correct grid — and relative to downsample-bin-shrink, which was already correct.

The problem

Under ITK's pixel-center convention, a pixel's origin is the physical location of its center. When you downsample by a factor k, each output pixel covers k input pixels, so its center sits at the physical location of continuous input index (k − 1) / 2. The old code ignored this:

outputOrigin[i] = inputOrigin[i] + cropRadiusValue * inputSpacing[i];

This kept the origin at the first input pixel's center (plus any crop offset) with no half-pixel shift and no awareness of the image direction matrix. itk::BinShrinkImageFilter (used by downsample-bin-shrink) already does the right thing, which is why the two pipelines disagreed.

The fix

At all three affected sites — downsample.cxx scalar path, downsample.cxx vector-image path, and downsample-label-image.cxx — the origin is now computed through a continuous index so ITK applies the direction matrix for us:

// Origin lands at the center of the first bin, matching itk::BinShrinkImageFilter
outputOriginIndex[i] = inputStartIndex[i] + cropRadiusValue + (shrinkFactors[i] - 1.0) / 2.0;
...
inputImage->TransformContinuousIndexToPhysicalPoint(outputOriginIndex, outputOrigin);

Key points:

  • Half-pixel shift. The (shrinkFactors[i] - 1.0) / 2.0 term moves the origin to the center of the first coarse bin — the (k − 1) / 2 convention BinShrinkImageFilter uses.
  • Direction-aware. Building an itk::ContinuousIndex and calling TransformContinuousIndexToPhysicalPoint routes the shift through the image direction matrix, instead of the old per-axis arithmetic that silently assumed an identity direction.
  • Crop radius composes correctly. The existing crop offset is folded into the same continuous index, so cropRadius and the origin shift now stack (e.g. cropRadius = [8, 8] with shrinkFactors = [2, 2] → origin [8.5, 8.5]).
  • downsample-bin-shrink.cxx was already correct and is unchanged; the fix makes downsample match it exactly for the same inputs.

Verification & tests

Added regression tests (Node + Python, WASI and emscripten) that pin the corrected grid convention so it cannot silently regress:

  • downsample(cthead1, shrinkFactors=[2, 2]) → origin [0.5, 0.5], spacing [2, 2], size [128, 128]
  • downsample origin/spacing/size now exactly equal downsample-bin-shrink for the same input and factors (directly pins the downsample pipeline should result in a shift in origin #1409 convention)
  • cropRadius = [8, 8] composes with the shift → origin [8.5, 8.5], size [120, 120]
  • vector-image path asserts the half-spacing shift per axis

Baselines were regenerated for the corrected origins and the test-data hash bumped accordingly. The package version is bumped 1.8.1 → 1.9.0 (behavior change to the output geometry).

Fixes #1409

thewtex and others added 3 commits July 14, 2026 18:00
The downsample and downsample-label-image pipelines kept the input
origin (plus an axis-aligned crop-radius offset), so the coarse-grid
pixel centers did not coincide with the centers of the fine-pixel
neighborhoods they aggregate. Under ITK's pixel-center convention,
downsampling by factor k must shift the origin by
(k - 1) / 2 * inputSpacing along each axis, applied through the
direction matrix.

Compute the output origin as the physical point of the continuous input
index inputStartIndex + cropRadius + (k - 1) / 2 via
TransformContinuousIndexToPhysicalPoint, composing the half-pixel shift
with the existing crop-radius offset. This also makes the crop-radius
offset direction-aware; previously it was added axis-by-axis directly
to the origin, ignoring the direction matrix.

The convention matches itk::BinShrinkImageFilter, which places the
output origin at the physical point of continuous input index
(shrinkFactor - 1) / 2, so downsample-bin-shrink needed no change, and
downsample with cropRadius = 0 now produces an origin identical to
downsample-bin-shrink for the same factors.

Fixes InsightSoftwareConsortium#1409

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ines

The origin fix changes both the metadata and the pixel values of the
downsample and downsample-label-image outputs, so the affected
content-addressed baselines were regenerated with the fixed pipelines:

- cthead1-downsample.nrrd (origin [0, 0] -> [0.5, 0.5])
- apple-downsample.mha (origin shifted by 0.5 * the fractional input
  spacing per axis, to [0.17638888888888887, 0.17638888888888887])
- 2th_cthead1-downsample-label-image.nrrd (origin [0, 0] -> [0.5, 0.5])

The downsample-bin-shrink and resample* baselines are byte-identical to
before; those pipelines are unchanged. The re-packed test/data.tar.gz
has content ID

  bafkreie3b4xdvf4cqykvagruaivwtb3oi55hwt4egzlhppu2o3hw2eudhi

referenced from package.json in test-data-hash, test-data-urls, and the
test:data:download script.

New regression assertions in the Node ava and Python pytest suites pin
the grid convention from issue InsightSoftwareConsortium#1409. The baseline tests now also
assert the output origin (approximately [0.5, 0.5] for the unit-spacing
inputs, and computed from the input metadata for apple.jpg). New tests
assert that downsample and downsample-bin-shrink produce identical
origin, spacing, and size, pinning the itk::BinShrinkImageFilter
convention as the contract, and that a cropRadius of [8, 8] composes
with the half-pixel shift (origin [8.5, 8.5], size [120, 120] with
shrink factors [2, 2]). Node suite: 23/23 pass; Python WASI suite:
33/33 pass.

test/data.tar.gz is not yet pinned: a maintainer still needs to upload
it to the IPFS pinning service (itk.mypinata.cloud). Until then, CI and
other machines cannot download this package's test data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Minor version bump for the user-visible behavior change in the
downsample and downsample-label-image pipelines: the output origin now
shifts to the coarse-grid pixel centers, matching ITK's pixel-center
convention and itk::BinShrinkImageFilter (issue InsightSoftwareConsortium#1409).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thewtex thewtex changed the title downsample translation fix(downsample): shift output origin to coarse-grid pixel centers Jul 15, 2026
thewtex and others added 2 commits July 15, 2026 16:58
The new test data tarball CID introduced with the origin-shift baselines
was never pinned to the itk.mypinata.cloud gateway, so test:data:download
failed with HTTP 403 across the WASI build-test job, the Python Wasm
matrix, and Node.js test jobs.

Upload the tarball as a test-data release asset, following the
transform-io convention, and list the GitHub release URL first with the
Pinata gateway URL retained as a fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The emscripten pyodide tests fetched pipeline modules from the jsDelivr
CDN at the package version baked into js_package.py. On a version bump
that version is not yet published to npm, so every pipeline fetch 404s
and CI fails, a chicken-and-egg between publish and merge.

Build the emscripten pipelines in test:python:emscripten, copy them
into the pytest-pyodide dist directory, and point
js_package.config.pipelines_base_url at the test server so the tests
exercise the pipelines built from the branch instead of the published
package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thewtex
thewtex merged commit 4283212 into InsightSoftwareConsortium:main Jul 21, 2026
67 checks passed
@thewtex
thewtex deleted the downsample-translation branch July 21, 2026 00:59
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.

downsample pipeline should result in a shift in origin

1 participant