Skip to content

perf(sizeshape): scatter-based spatial moments + inertia, replacing regionprops einsum (~1.6x)#77

Draft
timtreis wants to merge 3 commits into
mainfrom
perf/sizeshape-moments-scatter
Draft

perf(sizeshape): scatter-based spatial moments + inertia, replacing regionprops einsum (~1.6x)#77
timtreis wants to merge 3 commits into
mainfrom
perf/sizeshape-moments-scatter

Conversation

@timtreis

@timtreis timtreis commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

What

get_sizeshape's moment features came from regionprops_table's moments/moments_central/moments_normalized/moments_hu plus inertia_tensor/inertia_tensor_eigvals — all computed per region with an einsum routine whose contraction path is re-derived for every object. cProfile on a 1080²/142-object tile showed this moment machinery dominated the call (~120 ms of 352 ms, ~40 ms of it pure einsum_path overhead).

This computes the whole set in one label-scatter pass (primitives/_moments.py): raw spatial moments in each object's local bbox frame, central moments by direct centred summation, then normalized + the 7 Hu invariants. The 2D inertia tensor + eigenvalues are derived from the same central moments (inertia_2d), so for 2D regionprops now computes no moments at all — its per-region einsum is gone entirely.

Performance

get_sizeshape: 352 → 220 ms (1.6×) on the large tile (median of 11). Removing inertia from regionprops alone saves ~41 ms (regionprops 147→106 ms); the moment scatter is the rest. No new dependencies.

Fidelity

  • Raw spatial moments: bit-exact vs regionprops (integer-coordinate sums).
  • Central / normalized / Hu / inertia / eigvals: ~1e-13 relative (moments reach ~1e8 magnitude). Inertia tensor + eigvals match skimage to ~1e-12.
  • Object order (ascending label) and the NaN convention for normalized moments (p+q<2) match regionprops. Eigenvalues descending, as skimage.
  • 2D only; the 3D path is untouched (still on regionprops).

Tests

test/test_sizeshape_moments.py: raw moments bit-exact; central/normalized/Hu and inertia/eigvals vs regionprops at rtol=1e-7 across multi-object, non-contiguous labels, edge-touching, single-pixel (degenerate), empty, and end-to-end get_sizeshape wiring. Full test_core_measurements green; ruff + CI-style mypy clean.

Reuse hook

spatial_moments_2d + inertia_2d live in primitives/ so the planned numba sizeshape lane can reuse them.

@timtreis timtreis force-pushed the perf/sizeshape-moments-scatter branch 3 times, most recently from 1f77175 to 010232e Compare June 6, 2026 19:01
@timtreis timtreis changed the title perf(sizeshape): scatter-based spatial moments, replacing regionprops einsum (~1.4x) perf(sizeshape): scatter-based spatial moments + inertia, replacing regionprops einsum (~1.6x) Jun 6, 2026
…egionprops einsum (~1.6x)

`get_sizeshape`'s moment features came from `regionprops_table`'s `moments` /
`moments_central` / `moments_normalized` / `moments_hu` columns plus the
`inertia_tensor` / `inertia_tensor_eigvals`, all computed per-region with an
`einsum` routine whose contraction path is re-derived for every object. On a
1080^2 / 142-object tile cProfile showed this moment machinery dominated the
call (~120 ms of 352 ms, ~40 ms of it pure `einsum_path` overhead).

Moments are plain reductions, so compute the whole set in one label-scatter pass
(`primitives/_moments.py`): raw spatial moments in each object's local bbox
frame, central moments by direct centred summation, then normalized + the 7 Hu
invariants. The 2D inertia tensor and its eigenvalues are derived from the same
central moments (`inertia_2d`), so for 2D regionprops now computes *no* moments
at all — its per-region einsum is gone entirely.

Raw spatial moments are bit-exact vs regionprops; the centroid-dependent
matrices and inertia match to ~1e-13 relative (moments reach ~1e8 magnitude).
Object order (ascending label) and the NaN convention for normalized moments
(p+q<2) match regionprops. 2D only; the 3D path is unchanged.

Measured: get_sizeshape 352 -> 220 ms (1.6x), large tile. No new deps.

`spatial_moments_2d` / `inertia_2d` live in `primitives/` so the planned numba
sizeshape lane can reuse them. Adds golden tests vs regionprops (raw bit-exact;
central/normalized/Hu/inertia across multi / non-contiguous / edge-touching /
single-pixel / empty + end-to-end get_sizeshape wiring).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@timtreis timtreis force-pushed the perf/sizeshape-moments-scatter branch from 010232e to 303cd48 Compare June 6, 2026 19:07
timtreis added a commit that referenced this pull request Jun 6, 2026
…apper

Address /code-review notes on the sizeshape lane:

- Extract `moment_feature_dict` into `primitives/_moments.py` as the single
  source of truth for the 53 `calculate_advanced` moment/inertia feature
  names and the (p,q) orders exposed. The numba `_sizeshape_2d` now calls it
  instead of building those keys with inline f-string loops, removing the
  duplication of the numpy assembly and the f-string/constant drift risk.
  The numpy `get_sizeshape` adopts the same helper when #77's moment rewrite
  rebases onto this lane (the sizeshape golden test cross-checks the keys vs
  the numpy F_* constants meanwhile).
- Clarify the wrapper: `pixels` is accepted only for dispatch-signature
  parity and is unused (sizeshape is purely geometric, like the numpy
  backend); pass `masks` to `to_bzyx` for axis normalisation and drop the
  computed-then-discarded `_pixels_zyx`.
- Add an end-to-end empty-mask test exercising the full dict assembly with
  zero objects (the kernels had empty tests; the wrapper assembly did not).

25 tests pass (incl. new empty case); ruff clean.

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

The previous commit stripped the explicit moment columns from the 2D
regionprops request but kept axis_major_length / axis_minor_length /
eccentricity / orientation — and regionprops derives THOSE from
moments_central, so it still ran the per-region moment einsum. The added
scatter was therefore pure overhead and 2D get_sizeshape regressed
(~198ms baseline -> ~216ms).

Apply "option B" (already used by the numba sizeshape backend): derive
all four from the scatter's central moments via a new
`axes_eccentricity_orientation` helper (built on the existing
`inertia_2d`), so the 2D regionprops call requests NOTHING moment-related
and never runs the einsum. The scatter (raw/central/normalized/hu) is now
computed once for 2D and reused by both the moment features and the
axis/ecc/orientation features. 3D keeps axis lengths on regionprops
(3D moments out of scope).

large 1080^2/142obj: regionprops 208->139ms/call, get_sizeshape
~216 -> ~178ms (now below the 198ms pre-opt baseline). Bit-exact vs
regionprops to 1e-9 incl. degenerate (single-pixel, line) — new
`test_axes_match_*` golden tests added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@timtreis timtreis added the numpy label Jun 9, 2026
… label LUT

Review-driven cleanups to the scatter-based get_sizeshape, all verified bit-exact
vs the PyPI 0.1.19 release (column order + values):

- inertia_2d clips eigenvalues to >=0 like skimage, fixing NaN MinorAxisLength /
  eccentricity>1 on thin/oblique objects (~4% of thin lines triggered it).
- spatial_moments_2d takes the per-object bbox origin from label_to_idx_lut's
  find_objects pass (new return_bbox=) instead of a 1<<31 sentinel + minimum.at;
  gains an `advanced` flag so the normalized/Hu moments are skipped when the
  caller won't emit them.
- get_sizeshape computes inertia_2d once (shared by the axes derivation and the
  tensor features) and assembles the 53 advanced moment features via a new
  moment_feature_dict (grouped order matching 0.1.19); drops the redundant
  whole-image numpy.unique for the object count.
- _moment_matrix powers driven by _ORDER; honest docstring on raw-moment
  divergence (round-off, grows with object size; not "bit-exact").
- Regression tests: pin the 0.1.19 key order; thin-object clip vs skimage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant