feat(downsample): add resample-bounding-box pipeline (metadata-only, C++/TS/Python) - #1549
Merged
thewtex merged 9 commits intoJul 7, 2026
Merged
Conversation
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>
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
force-pushed
the
resample-bounding-box
branch
from
July 3, 2026 15:10
3117fa6 to
c20b089
Compare
Member
Author
|
@vboussot please take a look |
…o build The resample-bounding-box browser demo controller imports readTransform from @itk-wasm/transform-io, but the package was never declared as a dependency of @itk-wasm/downsample. The production `vite build` (build:demo) therefore failed in CI with "Rollup failed to resolve import @itk-wasm/transform-io", breaking the downsample Node.js Tests job at the build:gen:typescript step. Add @itk-wasm/transform-io (workspace:^) to the TypeScript package's devDependencies alongside the other sibling io packages, and record the corresponding entry in pnpm-lock.yaml. The demo build now resolves the import (vite build: 716 modules transformed, previously failed at 13). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Python bindings are generated by scanning the WASI build, which -- unlike
the Emscripten build that TypeScript bindgen scans -- contains the CTest-only
executables built under `if(NOT EMSCRIPTEN)`: both resample-bounding-box-
generate-inputs AND resample-bounding-box-test. Only the former was listed in
`itk-wasm.bindgen-exclude`, so bindgen tried to extract an interface JSON from
the resample-bounding-box-test unit-test binary, which is not an itk-wasm
pipeline. That yields an empty interface object, and package-dunder-init.js
then calls snakeCase(interfaceJson.name) on undefined, crashing the
`bindgen:python` step of the downsample Python CI job:
TypeError: Cannot read properties of undefined (reading 'replaceAll')
at snakeCase (.../bindgen/snake-case.js:2:20)
Add resample-bounding-box-test to bindgen-exclude, and expand the CMakeLists
comment to note that the WASI build (scanned by Python bindgen) contains these
binaries, so any CTest-only executable must be excluded there too.
Verified by running `itk-wasm -b wasi-build bindgen --interface python` against
the local WASI build: it now exits 0 and the generated itkwasm_downsample
__init__.py imports only the six real pipelines (no *_test / *_generate_inputs
modules); the committed Python snapshot is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The resample-bounding-box browser demo controller imports readTransform from @itk-wasm/transform-io, so the production `vite build` (build:demo) must be able to resolve that package's built entry. A prior commit declared @itk-wasm/transform-io as a devDependency of the @itk-wasm/downsample TypeScript package, which fixed the "Rollup failed to resolve import" error -- but exposed the next one: vite then failed with [commonjs--resolver] Failed to resolve entry for package "@itk-wasm/transform-io". The package may have incorrect main/module/exports specified in its package.json. i.e. the package resolves, but its dist/ was never built in CI. The downsample Node.js job builds `pnpm --filter "@itk-wasm/downsample-build..." build:gen: typescript`; the sibling io packages get built only because their *-build packages are devDependencies of @itk-wasm/downsample-build (image-io-build, compare-images-build). transform-io-build was missing from that list, so transform-io never entered the build closure and its dist was absent when the demo bundled. Add @itk-wasm/transform-io-build (workspace:^) to @itk-wasm/downsample-build's devDependencies (and the matching pnpm-lock.yaml importer entry) so transform- io's build:gen:typescript runs -- topologically before the downsample demo build -- and its dist is present. Verified: `pnpm install` reports the lockfile is up to date, and @itk-wasm/transform-io-build now appears in the `@itk-wasm/downsample-build...` filter closure with a build:gen:typescript script. transform-io is imported only by the browser demo (not the node tests, which build the transform inline), so this fully covers the failing step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ample-bounding-box ResampleBoundingBoxComputer sampled every fixed-grid boundary pixel (all faces and edges) for every transform. That is only necessary for NONLINEAR transforms, where an interior edge pixel can map outside the hull of the transformed corners. For a LINEAR transform (translation, rigid, similarity, affine) the image of the fixed rectangle is convex, so its axis-aligned bound is already attained at the 2^Dimension transformed corners -- 4 in 2D, 8 in 3D, 16 in 4D -- independent of image size. Detect linearity with itk::Transform::IsLinear() (i.e. GetTransformCategory() == Linear), which dispatches polymorphically through the abstract base the pipeline already holds, and take a corners-only fast path in that case; keep the full boundary walk for nonlinear transforms. Both paths feed the identical min/max accumulation, so the computed region is unchanged for linear transforms -- the existing 2D/3D translation and 2D affine-rotation assertions (and the TypeScript and Python suites, which use only linear transforms) pass byte-for-byte, now via the corners. The corner enumeration is a bitmask over Dimension bits (bit d selects the low or high index on axis d); a size-1 axis harmlessly duplicates corners. The degenerate zero-size-axis early return is unchanged and still precedes path selection. Tests and docs: - Add a `usedLinearCornerPath` diagnostic to ResampleBoundingBoxResult; repurpose the existing numberOfBoundaryPoints assertions to confirm the linear cases now sample exactly 2^Dimension corners while producing the same region. - Add test2DNonlinearBoundary with a custom nonlinear "bulge" transform whose right-edge midpoint maps beyond the transformed corners, asserting the full-boundary path is taken (56 samples) and that its +x extent exceeds the corners-only bound -- locking in that the boundary walk still triggers, and matters, exactly when the transform is nonlinear. - Switch testInstanceReuse to the nonlinear transform so the reused, variable-size boundary storage is actually exercised (the linear path is a fixed 2^Dimension). - Update docs/resample-bounding-box.md to describe both paths. Verified natively against ITK 5.4 (-Wall -Wextra -Werror, clean): all unit checks pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ze instead of rapidjson
Replace the hand-built rapidjson DOM in resample-bounding-box.cxx with glaze
(glz::write_json). Two small structs -- CornersJSON and BoundingBox -- mirror the
output schema, and glaze serializes their public members by name in declaration
order via compile-time reflection, so the ~55 lines of Value/AddMember/PushBack
boilerplate (and three lambdas) collapse to a struct literal plus one write call.
Write errors are reported through the same CLI::Error / pipeline.exit() path the
file already uses for the dimension-dispatch error. glaze is already a dependency
of this translation unit (resampleReadInputTransform.h uses it) and reaches the
include path transitively via the WebAssemblyInterface ITK module, so no
CMakeLists change is needed.
Two behaviors worth noting:
- Linkage: the serialization structs are placed in a NAMED namespace, not an
anonymous one. glaze's member-name reflection instantiates a helper over the
type, which the (emscripten/clang) toolchain rejects for a type with internal
linkage ("its type does not have linkage"). A named namespace gives external
linkage while still scoping the generic type names.
- Number format: glaze writes the shortest round-trippable form, so an
integer-valued double serializes without a trailing ".0" (e.g. 19, not 19.0).
The JSON value is identical and every binding parses it as a float. All test
suites compare numeric values, not text: the C++ unit test uses the in-memory
ResampleBoundingBoxResult (no JSON), and the TypeScript (closeTo / deepEqual)
and Python (pytest.approx / ==) suites parse then compare -- and the integer
index arrays (paddedStartIndex, paddedSize) serialize identically to before.
Non-integer corners keep full double precision. docs/resample-bounding-box.md
worked example updated to the actual compact output, with a note.
Verified against the real glaze 2.9.5 in the itkwasm images: the pipeline
compiles and links under emscripten, and a WASI build passes all 7
resample-bounding-box CTests. The emitted JSON for the 2D-translation, 3D-
translation, and 2D-affine-rotation cases matches the expected regions (e.g.
2D translation -> {"paddedStartIndex":[19,24],"paddedSize":[33,33],...}).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thewtex
merged commit Jul 7, 2026
f53aaf9
into
InsightSoftwareConsortium:main
68 of 69 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new
resample-bounding-boxpipeline to the@itk-wasm/downsample/itkwasm-downsamplepackage. Given a spatialtransform, afixedimage, and amovingimage, 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 emptydata.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; parsestransform,fixed,moving,bounding-box(output JSON) and a--paddingoption, 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.txtwired up for the new targets.Language bindings & tests
resampleBoundingBox(browser + Node) with generated options/result interfaces, exported from the package index; Node tests and a browser demo-app controller.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-wasmbindgen improvements (needed by this pipeline, but general fixes)0for 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 forTEXT/BOOL.itk-wasm.bindgen-excludeconfig inpackage.jsonso build-only helper executables (here, theresample-bounding-box-generate-inputstest-input generator) are not emitted as public language bindings.Implementation details
itk::wasm::SupportInputTransformTypes, because that helper deserializes the transform input (aTransformList/ JSON array) as a single transform object and throws for every in-memory transform.--help/--interface-json/--versiondispatch to a default 2D functor so bindgen can extract the interface.itk::Transformbase at double precision — lossless for float32/float64 inputs and robust to.iwtscalar-type detection that can misreport a float64 transform as float32.2^Ncorners) 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(default1, 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 unpaddedcornersare 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
--padding 0.