Skip to content

feat(downsample): add resample-bounding-box pipeline (metadata-only, C++/TS/Python) - #26

Closed
thewtex wants to merge 4 commits into
mainfrom
resample-bounding-box
Closed

feat(downsample): add resample-bounding-box pipeline (metadata-only, C++/TS/Python)#26
thewtex wants to merge 4 commits into
mainfrom
resample-bounding-box

Conversation

@thewtex

@thewtex thewtex commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a new resample-bounding-box pipeline to the @itk-wasm/downsample / itkwasm-downsample package. Given a spatial transform, a fixed image, and a moving image, it computes the padded sub-region of the moving image that a caller must fetch in order to resample the fixed image's grid through that transform — using image metadata only (size, spacing, origin, direction). No pixel buffer is ever dereferenced, so both images may be passed with empty data.

The pipeline is implemented once in C++ and shipped across all three runtimes — C++ (WASI/native CTest), TypeScript (browser + Node), and Python (WASI + Emscripten) — producing identical results.

Why

Resampling a fixed grid into a moving image only ever reads moving-image samples inside the transformed footprint of the fixed grid. When the moving image is large, remote, or chunked, materializing all of it just to resample a small overlapping region is wasteful. This pipeline answers "which moving-image indices will the resample actually read?" from arithmetic on metadata alone, so a caller can fetch only that block (or skip non-overlapping tiles entirely) before moving any real pixels. It complements the existing resampling pipelines in this package and the affine-transform tooling used to build the transforms fed into it.

What changed

New pipeline (C++)

  • resample-bounding-box.cxx — pipeline entry point; parses transform, fixed, moving, bounding-box (output JSON) and a --padding option, and serializes the result.
  • resampleBoundingBox.h — the metadata-only bounding-box computation.
  • resampleReadInputTransform.h — generic transform reading/dimension peeking.
  • resample-bounding-box-test.cxx + resample-bounding-box-generate-inputs.cxx — CTest coverage and a self-contained test-input generator.
  • CMakeLists.txt wired up for the new targets.

Language bindings & tests

  • TypeScript: resampleBoundingBox (browser + Node) with generated options/result interfaces, exported from the package index; Node tests and a browser demo-app controller.
  • Python: resample_bounding_box (sync + async) for WASI and Emscripten packages, with WASI pytest coverage.

Documentation

  • docs/resample-bounding-box.md — motivation, inputs, the algorithm, the output JSON schema, edge cases, and intended downstream use.

Core itk-wasm bindgen improvements (needed by this pipeline, but general fixes)

  • Falsy numeric options are now forwarded. TypeScript and Python binding generators previously used a truthiness guard, which silently dropped a valid 0 for a numeric option (e.g. --padding 0). They now use a presence check (typeof !== "undefined" / is not None) for numeric options while keeping the truthiness guard for TEXT/BOOL.
  • New opt-in itk-wasm.bindgen-exclude config in package.json so build-only helper executables (here, the resample-bounding-box-generate-inputs test-input generator) are not emitted as public language bindings.

Implementation details

  • Metadata-only contract. Only size/spacing/origin/direction are read; the fixed and moving pixel buffers are never dereferenced.
  • Dimension dispatch. Supports transforms at dimension 2, 3, and 4. The pipeline dispatches on the transform dimension itself rather than via itk::wasm::SupportInputTransformTypes, because that helper deserializes the transform input (a TransformList / JSON array) as a single transform object and throws for every in-memory transform. --help/--interface-json/--version dispatch to a default 2D functor so bindgen can extract the interface.
  • Double-precision math. The transform is read into the abstract itk::Transform base at double precision — lossless for float32/float64 inputs and robust to .iwt scalar-type detection that can misreport a float64 transform as float32.
  • Full-boundary sampling, not just corners. Every boundary pixel of the fixed grid (all faces/edges, not just the 2^N corners) is transformed to a moving-image continuous index; interior pixels are skipped efficiently. For affine transforms the transformed rectangle is convex so corners would suffice, but for nonlinear transforms an interior edge pixel can map outside the corner hull — sampling the full boundary avoids under-bounding the region.
  • Padding & edge cases. --padding (default 1, covering linear interpolation's one-neighbor read) expands the integer region symmetrically per side; sizes are clamped to ≥ 0; a degenerate fixed image with a zero-length axis yields an empty all-zero region. The unpadded corners are the tight transformed-point extremes regardless of padding.

Output JSON

{
  "paddedStartIndex": [int, ...],
  "paddedSize": [uint, ...],
  "paddedCorners": { "min": [double, ...], "max": [double, ...] },
  "corners":       { "min": [double, ...], "max": [double, ...] }
}

Test plan

  • C++ CTest asserts region results and the boundary-pixel count (guarding against a regression to corners-only sampling), covering 2D/3D translation and a 2D affine rotation with non-axis-aligned corners.
  • Node and Python tests assert identical results to the C++ pipeline across the same cases, including the metadata-only (empty-buffer) contract and --padding 0.

@thewtex thewtex changed the title resample bounding box feat(downsample): add resample-bounding-box pipeline (metadata-only, C++/TS/Python) Jul 3, 2026
thewtex and others added 4 commits July 3, 2026 11:02
Add a metadata-only ITK-Wasm pipeline that computes the padded
moving-image region needed to resample a fixed image grid through a
spatial transform, emitting a JSON bounding box.

- resampleBoundingBox.h: reusable ResampleBoundingBoxComputer<TTransform>
  (enumerates all fixed-grid boundary pixels, accumulates tight physical
  and moving continuous-index min/max, pads outward). Metadata only.
- resample-bounding-box.cxx: thin WASM wrapper dispatching over
  float/double x dims 2/3/4; emits paddedStartIndex/paddedSize/
  paddedCorners/corners JSON via rapidjson.
- resample-bounding-box-generate-inputs.cxx: self-contained generator of
  fixed/moving .iwi metadata images and a translation .iwt.
- CMakeLists.txt: ITKTransform/ITKTransformIO, new targets, and
  DEPENDS-linked CTests (generate inputs, then run the pipeline).

Verified end-to-end under pnpm build:wasi / CTest (wasmtime): a (10,5)
translation with padding 1 yields the hand-checked bounding box.

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

Generate and validate the TypeScript/JavaScript bindings for the
resample-bounding-box pipeline, proving it is callable from Node with
metadata-only images (empty pixel buffers) and a transform.

- Bindings: resampleBoundingBoxNode(transform, fixed, moving, { padding })
  and the browser resampleBoundingBox(...); result field renamed from the
  generic "output" to "boundingBox" at the source (the .cxx option name).
- Exclude the test-only resample-bounding-box-generate-inputs generator from
  the Emscripten build (CMake `if(NOT EMSCRIPTEN)`) so bindgen does not expose
  it; it is still built for the WASI/native C++ CTest.
- resampleReadInputTransform.h: read a spatial transform generically into the
  abstract itk::Transform base via the ITK object factory, from the wasm memory
  store under --memory-io or the filesystem otherwise (always double precision).
- Dispatch on transform dimension in main() instead of SupportInputTransformTypes,
  whose memory-IO type detection mis-parses a TransformList (JSON array) as a
  single transform and throws for every in-memory transform.
- bindgen: forward falsy-but-valid scalar options (e.g. padding: 0) by using a
  presence check instead of truthiness in function-module.js.
- Node tests for padding 1 and padding 0 (region shrinks one pixel per side),
  both with empty-data metadata-only images; auto-discovered by the ava glob.
- Browser demo controller wired into the demo-app.

Verified: `pnpm test:node` passes the new tests; `pnpm test:wasi` still passes
all 7 C++ CTests (no regression to the filesystem read path).

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

Generate the itkwasm-downsample Python bindings (wasi, emscripten, dispatch)
for the resample-bounding-box pipeline and add a WASI pytest validating the
padded moving-image region for a 2D translation with metadata-only images.

Framework changes:
- cli/bindgen.js: honor an opt-in `itk-wasm.bindgen-exclude` list so build-only
  helper executables (the resample-bounding-box-generate-inputs CTest fixture,
  which must be built under WASI) are not emitted as public language bindings.
- python bindgen (wasi/emscripten function modules): forward numeric scalar
  options with a presence check (`is not None`) instead of truthiness, so a
  valid `padding=0` is no longer dropped to the C++ default of 1 (the Python
  analogue of the Phase-02 TypeScript falsy-0 fix).

pnpm test:python:wasi -> 13 passed (2 new: padding=1 exact region, padding=0
shrinks one pixel per side).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… and docs for resample-bounding-box

Phase 04 of the resample-bounding-box pipeline: multi-dimension coverage,
robustness, and a documentation artifact. C++ 12/12 CTests, Node 8/8, and
Python wasi 14/14 all pass, with identical regions across the three surfaces.

resampleBoundingBox.h (hardening):
- Guard a degenerate fixed image (any zero-length axis): report an empty
  region instead of floor/ceil-ing sentinel min/max into garbage indices.
- Make boundary-point storage reuse-safe via clear()+reserve()+push_back so
  the vector holds exactly the current call's points -- no stale points leak
  when one ResampleBoundingBoxComputer instance is reused across differing
  boundary counts (shrinking then growing).
- Add a diagnostic (unserialized) numberOfBoundaryPoints so tests can confirm
  full-boundary (not corners-only) sampling. paddedSize stays clamped to >= 0.

resample-bounding-box-generate-inputs.cxx:
- Add a --case selector (2d-translation default, 3d-translation, 2d-rotation)
  emitting each self-contained fixed/moving/transform set; the rotation uses
  an itk::AffineTransform (cos .8/sin .6 about center) -> non-axis-aligned
  corners.

resample-bounding-box-test.cxx (new): in-process unit test asserting the exact
region for the 2D/3D translation and 2D rotation cases at double precision,
plus the hardening (padding symmetry, padding-independent corners, degenerate
axis, instance reuse vs fresh). Built/CTest-run only outside Emscripten.

CMakeLists.txt: 3D and rotation integration CTests (generate -> run, with
DEPENDS) and the unit-test CTest.

TypeScript + Python: parametrized case tables (2D/3D translation, 2D affine
rotation) with dimension-generic image/transform helpers and an affine helper;
assert regions identical to the C++ results, including the padding-0 padded
corners (which equal the tight corners only when those fall on grid lines).

docs/resample-bounding-box.md (new): structured reference (YAML front matter +
[[downsample]]/[[affine-ops]] wiki-links) documenting the inputs, algorithm,
exact JSON schema, and intended downstream sub-region fetch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewtex
thewtex force-pushed the resample-bounding-box branch from 0aa848d to 3117fa6 Compare July 3, 2026 15:02
@thewtex

thewtex commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

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

@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