Skip to content

Commit afc4257

Browse files
committed
DOC: Expand ComputeFeatureSizes algorithm documentation
Rewrite the Algorithm section to fully teach the filter: * State what the three output arrays (NumElements, Volume, EquivalentDiameter) represent and show the spherical/circular diameter formulas * Image Geometry path: explain the uniform-voxel-volume shortcut that lets the filter skip per-voxel volume computations, then walk the 256K-tuple chunked count pass and the per-feature output pass; cover the 2D fallback rules and the two-empty-dimensions preflight error * RectGrid path: contrast with the Image case, describe the lockstep FeatureIds + elementSizes chunked read, and explain why Kahan summation is needed to avoid float32 rounding error on billion-voxel volumes * Justify the 256K chunk size choice based on HDF5 chunk-lookup overhead vs. L2 cache residency * Summarize memory footprint Signed-off-by: Joey Kleingers <joey.kleingers@bluequartz.net>
1 parent b0b55fa commit afc4257

1 file changed

Lines changed: 50 additions & 4 deletions

File tree

src/Plugins/SimplnxCore/docs/ComputeFeatureSizesFilter.md

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,59 @@ During the computation of the **Feature** sizes, the size of each individual **E
1818

1919
## Algorithm
2020

21-
For **Image Geometry**, the algorithm counts voxels per feature, then multiplies by the uniform voxel volume (product of spacings) to obtain volumes. For 2D image geometries (one dimension equals 1), areas and equivalent circular diameters are computed instead.
21+
### What the filter computes
2222

23-
For **Rectilinear Grid Geometry**, where each cell can have a different volume, the algorithm reads both the Feature IDs and element sizes in lockstep and uses Kahan summation to accurately accumulate per-feature volumes.
23+
Each **Feature** occupies some number of **Cells** (voxels) in the input grid. The filter produces three arrays indexed by Feature ID:
2424

25-
### Performance
25+
- **NumElements** — the number of voxels belonging to each feature (int32).
26+
- **Volume** (or **Area** in 2D) — the physical volume (area) of each feature (float32).
27+
- **EquivalentDiameter** — the diameter of the sphere (or circle in 2D) that would have the same volume (area) as the feature (float32).
2628

27-
This filter is optimized for out-of-core (OOC) data storage. The Feature IDs array (and element sizes for Rectilinear Grid) is read in fixed-size chunks (64K tuples) via `copyIntoBuffer()`. Per-feature voxel counts and volumes are accumulated in plain `std::vector` buffers. This chunked approach reduces the number of OOC I/O operations from O(total_voxels) to O(total_voxels / 64K).
29+
Equivalent spherical diameter `d` is computed from volume `V` using `V = (4π/3)·r³`, giving `d = 2·(V / (4π/3))^(1/3)`. The 2D equivalent circular diameter uses `A = π·r²`, giving `d = 2·(A/π)^(1/2)`.
30+
31+
### Image Geometry path
32+
33+
An **Image Geometry** has uniform voxel spacing, so the volume of every voxel is the same: `V_voxel = dx · dy · dz`. The filter exploits this to skip per-voxel volume computations:
34+
35+
1. **Count voxels per feature.** Stream the per-cell Feature IDs array in 256K-tuple chunks. For each chunk:
36+
- Bulk-read the chunk via `copyIntoBuffer()` into a 1 MB RAM buffer.
37+
- Loop over the buffer, incrementing `featureVoxelCounts[id]` for each voxel's Feature ID.
38+
- The counter array is indexed by feature (thousands of entries, ~8 bytes each) and easily fits in L2 cache, so increments cost a few cycles.
39+
2. **Compute per-feature outputs** in a single pass over the feature-level arrays:
40+
- `NumElements[f] = featureVoxelCounts[f]`
41+
- `Volume[f] = featureVoxelCounts[f] * V_voxel`
42+
- `EquivalentDiameter[f] = 2·cbrt(Volume[f] / (4π/3))`
43+
44+
A 2D fallback (when any one of the three dimensions equals 1) computes area and **Equivalent Circular Diameter** using `2·sqrt(Area/π)` instead. The filter errors out in preflight if two or more dimensions equal 1, because the intended orientation for the 1D/2D scaling is ambiguous in that case.
45+
46+
### Rectilinear Grid Geometry path
47+
48+
A **Rectilinear Grid** has per-voxel spacing, so cell volumes vary. The filter can't use the uniform-spacing shortcut and must sum each feature's member voxel volumes explicitly:
49+
50+
1. **Cache element sizes** via the geometry's `findElementSizes()` helper (stored as a cell-level array).
51+
2. **Count voxels and sum volumes in lockstep.** Stream both the Feature IDs array and the element sizes array in 256K-tuple chunks (~2 MB working set total):
52+
- Bulk-read matching chunks of Feature IDs and element sizes.
53+
- For each voxel in the chunks:
54+
- Increment `featureVoxelCounts[id]`.
55+
- Accumulate `featureVolumes[id] += elementSize[i]` using **Kahan summation** (tracks per-feature compensator terms to correct floating-point rounding error). Kahan is necessary because summing billions of float32 volumes in native precision would lose low-order bits, especially for large features.
56+
3. **Compute per-feature outputs** using `featureVolumes[f]` directly (no post-multiplication).
57+
58+
If `Save Element Sizes` is off, the element sizes array created in step 1 is deleted at the end to save memory.
59+
60+
### Why 256K chunks
61+
62+
The voxel-counting pass is I/O-bound on OOC-backed Feature IDs. Each `copyIntoBuffer()` call carries fixed HDF5 chunk-lookup overhead; the compute (a single indexed increment) is memory-bandwidth-bound on a counter that easily fits in L2. On a 2 B-voxel volume, 256 K-tuple chunks reduce the call count from ~30 K (at the old 64 K chunk) to ~7.5 K while keeping per-chunk memory at a bounded 1 MB. Larger chunks yield diminishing returns because they no longer align with any HDF5 chunk shape.
63+
64+
### Memory footprint
65+
66+
Peak working memory:
67+
68+
- `featureVoxelCounts``numFeatures × 8 B` (thousands of features → tens of KB).
69+
- `featureVolumes`, `featureCompensators` — same size (RectGrid only).
70+
- Feature IDs chunk buffer — 1 MB.
71+
- Element sizes chunk buffer — 1 MB (RectGrid only).
72+
73+
All feature-level allocations are O(features), which is inherently small (thousands); chunk buffers are O(1) in dataset size. Billion-voxel volumes run without pressure on RAM.
2874

2975
## Image Geometry Additional Considerations
3076

0 commit comments

Comments
 (0)