Skip to content

Add resample pipeline to @itk-wasm/downsample (transform + selectable interpolator) - #27

Closed
thewtex wants to merge 7 commits into
mainfrom
resample-pipeline
Closed

Add resample pipeline to @itk-wasm/downsample (transform + selectable interpolator)#27
thewtex wants to merge 7 commits into
mainfrom
resample-pipeline

Conversation

@thewtex

@thewtex thewtex commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a new resample pipeline to @itk-wasm/downsample / itkwasm-downsample. It wraps itk::ResampleImageFilter to resample a moving image onto a reference image's grid, with an optional transform and a selectable interpolator, across the full ITK-Wasm target matrix (WASI, Node, browser/Emscripten, and Python sync + async / WASI + Emscripten).

This was built in five phases: a working C++ prototype, VectorImage support + interpolator ctest coverage, regenerated TypeScript/Python bindings, test data + independent baselines, and the Node/browser/Python test suites.

What changed

C++ pipeline — packages/downsample/resample.cxx (new)

  • Positional input (moving image), reference-image (geometry-only — an empty pixel buffer is accepted, since only origin/spacing/direction/size are read), and output.
  • -t,--transform optional transform mapping output-grid points into moving-image space (defaults to identity).
  • -i,--interpolator selectable from six methods: linear (default), nearest_neighbor, label_image, b_spline, windowed_sinc, gaussian.
  • Supports 2D/3D/4D images over all scalar pixel types and itk::VectorImage (multi-component) types.

Build — packages/downsample/CMakeLists.txt

  • Registers the resample executable and adds the required ITK modules (ITKImageFunction, ITKTransform) plus native transform IO (ITKIOTransformInsightLegacy, ITKIOTransformHDF5).
  • Adds self-contained ctests: a baseline resample smoke test plus one per interpolator (identity resample of cthead1.png), and a label_image test on 2th_cthead1.png — all needing no new test data.

Bindings — regenerated TypeScript + Python

  • TypeScript: resample / resampleNode with ResampleOptions (transform?: TransformList, interpolator?: string) and ResampleResult, exported from the package entry points.
  • Python: sync + async resample across the wasi and emscripten sub-packages.

Test data & baselines

  • New independent baselines and refreshed dam test-data-hash in package.json / test:data:download.

Tests

  • Node: ava tests for the resample bindings.
  • Browser: a demo-app controller + sample-input loader and a Playwright resample.spec.ts.
  • Python: WASI test_resample.py.
  • Adds @itk-wasm/transform-io (TS devDep) and itkwasm-transform-io (pixi) so transform inputs can be exercised in tests.

Why

The downsample package could shrink images but had no general way to map an image onto an arbitrary output grid with a transform and a chosen interpolation method — the standard "resample onto a reference geometry" operation. This adds that as a first-class, fully-bound pipeline.

Implementation notes

  • Transform type: the option is read as itk::wasm::InputTransform<itk::AffineTransform<double, N>> rather than the abstract itk::Transform<double, N, N>. The abstract base won't compile — the memory-IO reader calls TTransform::New(), and itk::Transform has no itkNewMacro. The concrete double-precision AffineTransform is-a Transform<double, N, N> and still feeds SetTransform polymorphically.
  • Geometry-only reference: the filter uses SetReferenceImage(...) + UseReferenceImageOn(), so the reference image contributes only its geometry.
  • VectorImage path: ResampleImageFilter has no native multi-component support, so (mirroring downsample.cxx) each component is extracted (VectorIndexSelectionCastImageFilter), resampled through shared wiring, then recomposed (ComposeImageFilter). Per-component filters are kept alive until a single final compose update, because itk::DataObject only holds a weak pointer back to its producer.
  • Shared wiring: reference-geometry / transform / interpolator setup lives in one MakeResampleFilter<TImage> helper reused by both the scalar and per-component vector paths.

@thewtex thewtex changed the title resample pipeline Add resample pipeline to @itk-wasm/downsample (transform + selectable interpolator) Jul 3, 2026
thewtex and others added 7 commits July 3, 2026 11:01
Add packages/downsample/resample.cxx wrapping itk::ResampleImageFilter as a
WASI/Wasm pipeline: a moving image, a reference-image whose geometry defines the
output grid (metadata-only/empty pixel buffer supported), an optional
--transform, and a selectable --interpolator (linear, nearest_neighbor,
label_image, b_spline, windowed_sinc, gaussian) for 2D/3D/4D scalar images. A
shared SelectInterpolator<TImage>() helper returns the common
itk::InterpolateImageFunction<ImageType, double>::Pointer.

Register resample in CMakeLists.txt (foreach list, ITKImageFunction + ITKTransform
components, and native ITKIOTransformInsightLegacy/HDF5) and add a self-contained
ctest that resamples the existing cthead1.png with the default identity transform.

The transform option uses InputTransform<AffineTransform<double, ImageDimension>>
(concrete, double precision) rather than the abstract itk::Transform base, which
cannot compile through the memory-IO reader's TTransform::New().

Verified: pnpm build:wasi (exit 0) and pnpm test:wasi (6/6 pass, incl. resample).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ase 02)

Add an itk::VectorImage specialization to resample.cxx that resamples each
component independently (VectorIndexSelectionCast -> ResampleImageFilter ->
ComposeImageFilter), so every interpolator works with multi-component pixels,
and register the five VariableLengthVector pixel types in SupportInputImageTypes.

Deduplicate the scalar and vector paths behind a shared MakeResampleFilter<T>
helper (reference-geometry/transform/interpolator wiring) alongside the existing
SelectInterpolator<T>. Per-component filters are held alive until a single final
ComposeImageFilter update because DataObject::m_Source is a WeakPointer.

Add ctests exercising each interpolator (linear, nearest_neighbor, b_spline,
windowed_sinc, gaussian) on cthead1.png plus a label_image test on
2th_cthead1.png. WASI build is clean; all 12 ctests pass. Vector-path runtime
validation is deferred to the node/python memory-IO tests in a later phase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regenerate the language bindings for the resample pipeline from the
compiled interface metadata (build:emscripten + bindgen:typescript,
build:wasi + bindgen:python).

TypeScript (packages/downsample/typescript/src/):
- Add resample.ts, resample-node.ts, resample-options.ts,
  resample-node-options.ts, resample-result.ts, resample-node-result.ts.
- Re-export resample / ResampleOptions / ResampleResult (+ Node variants)
  from index-only.ts and index-node-only.ts; add the TransformList type
  re-export to index-common.ts.
- Regenerated demo-app scaffolding (resample-controller.ts,
  resample-load-sample-inputs.ts, index.html).

Python:
- Add resample.py (wasi + dispatch) and resample_async.py (emscripten +
  dispatch); each package __init__ exports the symbol.
- Re-embed the emscripten js_package.py bundle; add the wasi test stub.

The wrappers expose the moving Image + reference Image inputs, an optional
transform marshaled as an InterfaceTypes.TransformList memory-IO input
(like affine-ops), and an interpolator string option with the six-value
CLI::IsMember enum. No resample.cxx change was needed and the
@itk-wasm/downsample TS package type-checks cleanly. See
.maestro/.../Working/resample-design-notes.md section 9 for the verified
option shape and a flagged pre-existing core-bindgen quirk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h (Phase 04)

Assemble resample test inputs and independent (itk-generated) baselines for the
@itk-wasm/downsample resample pipeline, pack them with dam, and record the new
content hash.

Inputs (test/data/input/, distributed via test/data.tar.gz):
- cthead1-resample-reference.nrrd     scalar reference, different size/spacing/origin
- apple-resample-reference.mha        3-component reference for the vector path
- cthead1-resample-reference-metadata-only.json  geometry-only sidecar (empty-buffer path)
- cthead1-resample-transform.h5       non-identity affine (rotation + translation)

Baselines (test/data/baseline/): cthead1-resample-linear.nrrd,
cthead1-resample-nearest-neighbor.nrrd, apple-resample-linear.mha. Generated with
native itk.ResampleImageFilter wired identically to resample.cxx's MakeResampleFilter,
so they are independent (not self-referential). Verified bit-identical (pxMaxAbs=0)
against the actual resample.wasi.wasm memory-IO output for scalar linear /
nearest_neighbor, the metadata-only empty-buffer reference, and the vector case.

package.json: bump itk-wasm.test-data-hash, the test-data-urls entry, and both hash
occurrences in test:data:download to the new dam CID
(bafkreiesjpg3sjqkyjb77djrffdvznjjsgvudd2y44iqjrfgnd45uyyprq). The repacked
test/data.tar.gz still needs uploading to pinata at the new CID for CI/other machines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add packages/downsample/typescript/test/node/resample-test.js with four ava
cases exercising resampleNode against the Phase 04 packed inputs and independent
baselines (full node suite: 9/9 pass, no regressions):

- linear + transform: cthead1.png onto cthead1-resample-reference.nrrd with the
  cthead1-resample-transform.h5 affine (read via @itk-wasm/transform-io
  readTransformNode -> Affine/float64/2->2) vs cthead1-resample-linear.nrrd.
- nearest_neighbor + transform: same inputs vs cthead1-resample-nearest-neighbor.nrrd.
- label_image: 2th_cthead1.png as both moving and reference (identity), compared to
  the input itself. itk 5.4.6's wheel lacks LabelImageGenericInterpolateImageFunction
  (GenericLabelInterpolator remote module) so no independent label baseline exists;
  a grid-aligned identity resample is pixel-exact for the label interpolator, mirroring
  the C++ resample-label-image ctest. No new packed baseline / dam re-pack needed.
- VectorImage: apple.jpg + apple-resample-reference.mha with imageType.pixelType
  reassigned to VariableLengthVector (required for the vector dispatch, as
  test_downsample.py's vector test does) vs apple-resample-linear.mha.

Add @itk-wasm/transform-io as a downsample TS devDependency (workspace link) for the
transform reader; pnpm-lock.yaml records the link. common.js is unchanged -- the tests
use its existing testInputPath/testBaselinePath directory exports with inline path.join,
matching every existing node test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ase 05)

Wire the resample function into the browser demo-app, mirroring the
downsample controllers:

- resample-load-sample-inputs.ts: replace the bindgen `export default null`
  stub with a real loader that fetches cthead1.png from the published input
  CID and resamples it onto its own grid (identity, interpolator: linear).
  Self-contained from the single published input; the Phase-04 reference/
  transform files are only in the local data.tar.gz (unpublished raw-tarball
  CID), so they can't be fetched via IPFS. Transform left to its identity
  default; the controller still wires the interactive transform-upload path.
- index.ts: register resample-controller.js (the bindgen-generated controller
  already wires moving/reference/transform/interpolator -> resample; kept as-is
  per the generated-file convention). The resample tab/panel already exist in
  index.html from Phase 03 bindgen.
- vite.config.js: serve @itk-wasm/transform-io pipelines so readTransform's
  hdf5-read-transform wasm loads in-browser (image-io/mesh-io already copied).

Validated: `pnpm build:demo` bundles cleanly (716 modules); dev server serves
/pipelines/resample.*, /pipelines/hdf5-read-transform.*, and downsample.* at
HTTP 200 (no regression).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(Phase 05)

Completes the resample deliverable — validated C++ -> TypeScript (Node + browser)
-> Python (wasi). No binding/pipeline fixes were needed.

- typescript/test/browser/resample.spec.ts: playwright spec modeled on
  downsample.spec.ts — uploads cthead1.png as both moving + reference (identity
  resample), sets the interpolator, runs, and asserts the output renders.
- python/itkwasm-downsample-wasi/test/test_resample.py: 4 real wasi cases
  (linear+transform, nearest_neighbor+transform, label_image identity,
  VectorImage) compared to the Phase-04 baselines. Reads the .h5 transform via
  itkwasm-transform-io. Placed in test/ (the hand-written suite, next to the real
  test_downsample.py); tests/ (bindgen stubs) is left untouched. Emscripten/
  dispatch stay bindgen stubs, matching every other downsample function.
- pixi.toml/pixi.lock: add itkwasm-transform-io >=1.1.0,<2 (only new dep; images
  via itkwasm-image-io, comparison via itkwasm-compare-images). Lock re-resolved
  (also bumped format v6->v7); consistent with the manifest and CI-safe.

Full matrix green: Node ava 9/9, Python wasi 16 passed, browser playwright 3/3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewtex
thewtex force-pushed the resample-pipeline branch from 97ca6b9 to b552528 Compare July 3, 2026 15:01
@thewtex

thewtex commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Superseded by the upstream PR against InsightSoftwareConsortium/ITK-Wasm: InsightSoftwareConsortium#1548

@thewtex thewtex closed this Jul 3, 2026
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