Skip to content

ENH: Standardize 2D Image Handling#1590

Merged
imikejackson merged 9 commits into
BlueQuartzSoftware:developfrom
nyoungbq:enh/standardize_2d_image_handling
Apr 24, 2026
Merged

ENH: Standardize 2D Image Handling#1590
imikejackson merged 9 commits into
BlueQuartzSoftware:developfrom
nyoungbq:enh/standardize_2d_image_handling

Conversation

@nyoungbq
Copy link
Copy Markdown
Contributor

Naming Conventions

Naming of variables should descriptive where needed. Loop Control Variables can use i if warranted. Most of these conventions are enforced through the clang-tidy and clang-format configuration files. See the file simplnx/docs/Code_Style_Guide.md for a more in depth explanation.

Filter Checklist

The help file simplnx/docs/Porting_Filters.md has documentation to help you port or write new filters. At the top is a nice checklist of items that should be noted when porting a filter.

Unit Testing

The idea of unit testing is to test the filter for proper execution and error handling. How many variations on a unit test each filter needs is entirely dependent on what the filter is doing. Generally, the variations can fall into a few categories:

  • 1 Unit test to test output from the filter against known exemplar set of data
  • 1 Unit test to test invalid input code paths that are specific to a filter. Don't test that a DataPath does not exist since that test is already performed as part of the SelectDataArrayAction.

Code Cleanup

  • No commented out code (rare exceptions to this is allowed..)
  • No API changes were made (or the changes have been approved)
  • No major design changes were made (or the changes have been approved)
  • Added test (or behavior not changed)
  • Updated API documentation (or API not changed)
  • Added license to new files (if any)
  • Added example pipelines that use the filter
  • Classes and methods are properly documented

@nyoungbq nyoungbq requested a review from imikejackson April 15, 2026 14:25
@imikejackson
Copy link
Copy Markdown
Contributor

imikejackson commented Apr 16, 2026

Code Review

What this PR does

Multi-purpose refactor of how SIMPLNX handles 2D/1D ImageGeometry cases (where one or more dimensions = 1):

  1. New compile-time dimensionality dispatch in NeighborUtilities.hpp — a VoxelNeighbors<ImageDimensionState<...>> template provides per-shape (3D / Empty-X 2D / Empty-Y 2D / Empty-Z 2D / 1D / Single voxel) neighbor counts and offsets. ProcessCorners, ProcessEdges, ProcessFaces template helpers iterate the right boundary cells per shape via if constexpr branches.
  2. Refactors ComputeFeatureNeighbors and IdentifySample to use the new dispatch — old hand-rolled Is1DImageDimsState/Is2DImageDimsState logic is replaced by templated helpers.
  3. Removes the "fake spacing = 1.0" auto-fix in ImageGeom::findElementSizes and ComputeFeatureSizes::ProcessImageGeom. Users must now explicitly set spacing[empty_dim] = 1.0 (documented in Geometry.rst).
  4. ComputeFeatureNeighbors doc note advising users to set spacing intentionally for 2D shared-surface-area math.
  5. Modernizes loop control in IdentifySample: nested (z,y,x) loops with throttled progress messages and modulo-decoded indices.
  6. API breakage: k_NegativeXNeighbor/etc. constants moved out of the nx::core namespace into VoxelNeighbors<T>:: static members. initializeFaceNeighborOffsets/initializeFaceNeighborInternalIdx/computeValidFaceNeighbors/computeFaceSurfaceAreas changed from returning std::array<…, 6> to returning std::vector<…>.

Critical Issues

  • 🛑 Wrong stride values in initializeFaceNeighborOffsets for 2D cases — silent correctness bug. src/simplnx/Utilities/NeighborUtilities.hpp:244-269:

    if constexpr(std::is_same_v<ImageDimensionStateT, EmptyXImage2D>)
    {
      return {-dims[2], -1, 1, dims[2]};   // ❌ should be ±dims[1]
    }
    if constexpr(std::is_same_v<ImageDimensionStateT, EmptyYImage2D>)
    {
      return {-dims[2], -1, 1, dims[2]};   // ❌ should be ±dims[0]
    }
    if constexpr(std::is_same_v<ImageDimensionStateT, EmptyZImage2D>)
    {
      return {-dims[1], -1, 1, dims[1]};   // ❌ should be ±dims[0]
    }

    The stored linear index is always z*dims[0]*dims[1] + y*dims[0] + x. With dims[2]=1 (EmptyZImage2D), the Y-neighbor stride is dims[0], not dims[1]. Same problem in the other two empty-2D cases. These offsets are consumed by IdentifySample::operator() (line 36) and ComputeFeatureNeighbors::operator() (line 36) for any non-3D image. Square 2D images (dims[0] == dims[1]) hide the bug; non-square 2D images will compute wrong neighbors.

  • 🛑 Self-include in NeighborUtilities.hppsrc/simplnx/Utilities/NeighborUtilities.hpp:3:

    #include "NeighborUtilities.hpp"   // self-include, almost certainly accidental

    #pragma once saves it from infinite recursion, but this is clearly a mistake — likely a typo for a different header.

  • 🛑 Performance regression: std::arraystd::vector in hot loops. Original computeValidFaceNeighbors, initializeFaceNeighborOffsets, initializeFaceNeighborInternalIdx returned stack-allocated std::array<…, 6>. They now return heap-allocated std::vector<…> (with std::vector<bool>'s packed-bit gymnastics for the flags). FillBadData::phaseFourIterativeFill calls computeValidFaceNeighbors twice per voxel inside an iterative outer loop — on a 512³ volume that's ~268M heap allocations per iteration. Recommended fix: keep std::array<…, MaxFaces> plus a count field, or take an output buffer by reference, or have the templated specializations return a fixed-size std::array whose size is VoxelNeighbors<T>::k_FaceNeighborCount (statically known). Lifting the call out of FillBadData's inner loop is also worth doing — the validity vector is recomputed twice on the same (xIdx,yIdx,zIdx).

  • ⚠️ Precision loss in computeFaceSurfaceAreassrc/simplnx/Utilities/NeighborUtilities.hpp:322-326:

    std::vector<float64> computeFaceSurfaceAreas(...)
    {
      const auto zFace = static_cast<float32>(spacing[0] * spacing[1]);  // float64*float64 → float32
      ...
      return {zFace, ...};   // float32 implicitly widened back to float64
    }

    Returns float64 but truncates intermediates to float32. Drop the static_cast<float32> so the math stays in double precision.


Other Issues

  • Inconsistent bounds-checking in IdentifySamplesrc/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySample.cpp:157-160:

    if(!checked.at(neighborPoint) && !goodVoxels.getValue(neighborPoint))
    {
      currentVList.push_back(neighborPoint);
      checked[neighborPoint] = true;

    .at() does bounds-checking and throws — every other access in the same loop uses []. Looks like a leftover debug aid. Switch to [] (or use .at() everywhere if genuinely needed).

  • Breaking behavioral change in ImageGeom::findElementSizessrc/simplnx/DataStructure/Geometry/ImageGeom.cpp:160-164. Previous code coerced spacing of any empty dim to 1.0 implicitly and errored if 2+ dims were empty. Both protections removed:

    • 2D images that previously had stored z-spacing = 0.5 will compute element sizes 0.5× smaller than before.
    • 1D images (2 empty dims) now silently produce a wrong "volume" instead of erroring.

    The existing test had to change setSpacing(20.2f, 0.1f, 67777.1f)setSpacing(20.2f, 0.1f, 1.0f) to keep passing. This is a silent break for any user with existing .dream3d files. Recommend either keeping the dimensional check that errors if >1 empty dim, or documenting this in the Image Geometry release notes more prominently than the python Geometry.rst.

  • API break — global neighbor-index constants are gone. Free constants k_NegativeZNeighbor, ..., k_FaceNeighborCount removed from nx::core namespace, replaced by VoxelNeighbors<Image3D>::k_NegativeZNeighbor etc. Downstream plugins (SimplnxReview, FileStore, Synthetic) and DREAM3DNX were not updated in this PR. Anyone using these constants will hit a compile break. Confirm with downstream maintainers, or keep deprecated inline constexpr aliases at namespace scope.

  • Naming/structure cruft from squashed iteration. ImageDimensionalUtilities sub-namespace was first added as a separate header (ImageDimensionalUtilities.hpp), then deleted, then re-added as a sub-namespace inside NeighborUtilities.hpp. Final state works, but consider renaming the inner namespace to something like nx::core::detail::VoxelTraversal. Also ProcessVoxels template is duplicated between ComputeFeatureNeighbors.cpp and IdentifySample.cpp — extract it.

  • Documentation typos in wrapping/python/docs/source/Geometry.rst: dimesnional → dimensional, accomodate → accommodate, peice → piece, caluclation → calculation, evalute → evaluate, counter-intuitative → counter-intuitive.

  • Use the detail::ImageDimensionality concept consistently. IdentifySample.cpp:13 declares template <typename ImageDimsStateT> with no constraint — could use the new concept like ComputeFeatureNeighbors.cpp:16.


Test Coverage

  • Add hand-built 2D tests for IdentifySample and ComputeFeatureNeighbors covering each EmptyX/Y/Z dispatch with non-square dims (e.g. 3×4×1, 1×3×4). The current IdentifySampleTest.cpp only loads pre-generated exemplar .dream3d files and would not catch the wrong-stride bug noted above.
  • Add a regression test for the ImageGeom::findElementSizes semantic change — at minimum, a test that confirms a 1D image (2 empty dims) errors or has a documented behavior.
  • Add tests for ImageDimensionalUtilities::ProcessCorners/Edges/Faces themselves (e.g. recording which (x,y,z) indices the lambda is invoked with, for each dimensionality state).

Suggested merge order

  1. Block on the wrong 2D strides + add 2D unit tests that prove the fix.
  2. Trivial fixes: self-include, static_cast<float32>, .at() inconsistency.
  3. Quantify the heap-allocation regression with one of the existing benchmarks; revert to std::array if it shows up.
  4. Decide on the findElementSizes semantic break — at least add a release note.

Overall this is a worthwhile direction (compile-time dispatch over runtime branches is cleaner than the previous hand-rolled corner/edge enumeration), but the correctness of the EmptyXYZImage2D neighbor offsets and the API/semantic breakage need to be settled before this is safe to merge.

Copy link
Copy Markdown
Contributor

@imikejackson imikejackson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments

@nyoungbq nyoungbq requested a review from imikejackson April 16, 2026 18:24
@imikejackson imikejackson enabled auto-merge (squash) April 24, 2026 01:49
nyoungbq and others added 9 commits April 23, 2026 21:50
- Patch bug in 2D Identify Sample Handling
- Optimize Identify Sample Fill Hole Algorithm
- Update Neighbors Docs
Locks in the contract for the 2D/1D dimensionality dispatch added in this
branch. The tests exercise non-square dims so a regression of the wrong-stride
bug on EmptyX/Y/Z 2D cases would fail here without any pipeline plumbing.

Covers for every ImageDimensionState:
- VoxelNeighbors<T>::k_FaceNeighborCount
- initializeFaceNeighborInternalIdx<T>() ordering
- initializeFaceNeighborOffsets<T>(dims) with non-square dims
- computeValidFaceNeighbors<T>(x,y,z,dims) at corners, edges, interior
- computeFaceSurfaceAreas<T>(spacing) precision and ordering
- ProcessCorners/Edges/Faces<T>(fn, dims) visitation pattern

Also adds the missing <vector> include to NeighborUtilities.hpp so it is
standalone-compilable.

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

The pre-existing 2D tests for both filters used 5x5x1 layouts, which have
dims[0] == dims[1] and therefore mask any wrong row-stride in the
initializeFaceNeighborOffsets Empty2D dispatches.

Add three new cases per filter, one for each EmptyX/Y/Z 2D dimensionality,
using a 3x2 (and 3x4 for IdentifySample) layout with hand-computed expected
outputs. In ComputeFeatureNeighbors the buggy stride would miss one of the
three boundary faces and yield 2 * area instead of the expected 3 * area; in
IdentifySample the buggy stride would either merge the two connected
components or run off the end of the mask buffer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 2D-handling refactor removed the implicit empty-dim spacing coercion and
replaced it with an explicit error on non-positive spacing. These tests
capture the new contract so any future regression is caught by the core
geometry test suite rather than by a pipeline-level integration test.

Covers:
- 3D with valid positive spacing computes volume = prod(spacing)
- 2D 'piece of paper' example from Geometry.rst, with the spacing=1.0
  override pattern the docs recommend for flat-surface analyses
- non-positive spacing on any axis returns error -1530
- 1D image with two empty axes produces element sizes of the expected length

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@imikejackson imikejackson force-pushed the enh/standardize_2d_image_handling branch from f97183e to 5f778d6 Compare April 24, 2026 01:52
@imikejackson imikejackson merged commit e76ad50 into BlueQuartzSoftware:develop Apr 24, 2026
6 checks passed
@imikejackson imikejackson deleted the enh/standardize_2d_image_handling branch April 24, 2026 12:40
joeykleingers added a commit to joeykleingers/simplnx that referenced this pull request Apr 27, 2026
…x bool-mask bulk I/O

Three logically related changes that finish reconciling the rebased
branch with Nathan Young's PR BlueQuartzSoftware#1590 (ENH: Standardize 2D Image Handling)
and fix one resulting OOC perf cliff:

1. Wholesale port of PR BlueQuartzSoftware#1590's two algorithm rewrites into the renamed
   in-core dispatch variants:
   - ComputeFeatureNeighborsDirect.cpp gets Nathan's templated
     ComputeFeatureNeighborsFunctor<ImageDimensionStateT> and ProcessVoxels
     dispatcher in place of the OOC-commit-era custom in-core logic.
   - IdentifySampleBFS.cpp gets Nathan's templated IdentifySampleFunctor
     plus the corresponding ProcessVoxels dispatch.
   The Scanline OOC variant of ComputeFeatureNeighbors is updated to
   reference the namespaced VoxelNeighbors<Image3D>:: constants while
   preserving its Z-slice rolling-window bulk-I/O structure.

2. Reapply PR BlueQuartzSoftware#1590's constexpr/const cleanups across the algorithm
   files where the rebase took --theirs (the OOC commit version) at the
   2aa00ee conflict and dropped Nathan's small adjustments:
     SimplnxCore: ComputeBoundaryCellsDirect, ErodeDilateBadData,
       ErodeDilateCoordinationNumber, ErodeDilateMask,
       ReplaceElementAttributesWithNeighborValues,
       RequireMinimumSizeFeatures
     OrientationAnalysis: BadDataNeighborOrientationCheckWorklist,
       NeighborOrientationCorrelation
   The pattern is uniform: promote the inlined `6` neighbor-array sizes
   to use VoxelNeighbors<Image3D>::k_FaceNeighborCount via a local
   k_NumFaceNeighbors alias, make neighborVoxelIndexOffsets const,
   make faceNeighborInternalIdx constexpr, make isValidFaceNeighbor
   const where it is not mutated, drop the now-unused DataGroup.hpp
   include, and const-ify NeighborOrientationCorrelation's orientationOps.
   ComputeFeatureNeighborsFilter.md picks up Nathan's all-dimension
   note about user-set spacing for shared surface area calculation.

3. Fix a per-element OOC fallback in BadDataNeighborOrientationCheckScanline
   that was triggered whenever the input mask was a BoolArray rather
   than a UInt8Array. The previous code routed bool masks through
   maskCompare->isTrue / maskCompare->setValue per voxel per Z-slice,
   causing chunk thrashing under chunked OOC storage. The Small_IN100
   pipeline test (a 189x201x117 volume with a bool mask produced by
   MultiThresholdObjects) ran in 4.7 s on simplnx-Rel but 3+ minutes
   on simplnx-ooc-Rel. AbstractDataStore<bool> already exposes
   copyIntoBuffer/copyFromBuffer just like AbstractDataStore<uint8>;
   the comment claiming otherwise was stale. Resolve a typed
   AbstractDataStore<bool>* alongside the existing uint8 store pointer
   and route both load and write-back through bulk I/O, with a small
   per-slice std::unique_ptr<bool[]> scratch buffer bridging between
   the algorithm's uint8 slice buffers and the bool data store's typed
   bulk API. With this change Small_IN100 OOC drops to 4.6 s
   (~1.6x in-core, in line with normal OOC overhead).

Tests updated:
  - IdentifySampleTest.cpp adopts Nathan's PR BlueQuartzSoftware#1590 hand-built 2D Empty
    Z/Y/X Non-Square regression tests plus the parameterized
    identify_sample_v2 exemplar test and the SIMPL Backwards Compatibility
    test, all wrapped with the OOC dual-path pattern (ForceOocAlgorithmGuard
    + GENERATE(from_range(k_ForceOocTestValues))). The pre-existing
    200x200x200 large-scale OOC validation test is retained.

Verified: simplnx-Rel and simplnx-ooc-Rel preset builds both clean.
All 43 affected-filter tests pass on simplnx-Rel; all 86 affected-filter
tests pass on simplnx-ooc-Rel (regex covering ComputeFeatureNeighbors,
IdentifySample, BadDataNeighborOrientation, ComputeBoundaryCells,
ErodeDilate*, NeighborOrientationCorrelation,
ReplaceElementAttributesWithNeighborValues, RequireMinimumSizeFeatures).
imikejackson added a commit that referenced this pull request May 4, 2026
Batch D covers c-axis misalignments and neighbor-correlation
filters. All reports remain DRAFT pending developer review of the
tentative Algorithm Relationship and Oracle classifications.

* ComputeFeatureNeighborCAxisMisalignmentsFilter — DIVISOR BUG
  REPLICATED VERBATIM from sibling
  ComputeFeatureNeighborMisorientationsFilter:
  hexNeighborListSize is reassigned on line 111 of the algorithm,
  clobbering the hexNeighborListSize-- decrement on line 150 and
  producing wrong per-feature AvgCAxisMisalignments whenever any
  non-hex neighbor exists. Production-relevant because
  EBSD_Hexagonal_Data_Analysis.d3dpipeline ships with
  find_avg_misals: true. Existing test exemplar is hex-only so it
  cannot trigger the bug. PR #1467 was OEM-reviewed but preserved
  the bug because the review covered parameters, not divisor logic.
* ComputeFeatureNeighborsFilter — PR #1569 fixed an explicit "Major
  bug in calculation of Shared Surface Area List (Present in 6.5)";
  the shared 6_6_stats_test_v2.tar.gz SSA values are confirmed
  bad-from-legacy and are intentionally skipped in the
  Legacy:SmallIn100 test. PR #1590 fixed an in-house row-stride
  bug in the EmptyZ/EmptyY/EmptyX dispatchers that had been masked
  by all 5x5x1 fixtures having dims[0] == dims[1]. 32 hand-derived
  inline test cases now cover 0D/1D/2D/3D x empty-axis x optional
  flag combinations.
* BadDataNeighborOrientationCheckFilter — PR #1499 (REV) is the
  model V&V case in the audit: explicit iteration-guard bug fix
  paired with 28 algorithmic test cases (~1700 lines) using
  hand-derived expectedMask arrays as a de facto Class-1
  analytical oracle. PR #1590 made the 6-face neighbor logic
  2D-aware via NeighborUtilities.
* NeighborOrientationCorrelationFilter — currentLevel > Level
  strict inequality means Level=6 is a no-op (surprising
  semantics); only one end-to-end algorithmic test exists; PR
  #1472 introduced a float-to-double widening that may drift vs
  legacy.

Cross-cutting:
- The divisor-bug pattern is now confirmed as a TWO-FILTER copy-
  paste pattern, not isolated. Screen remaining "average across
  neighbors" filters for the same shape (NeighborList::size()
  reassigned inside the inner loop, clobbering an earlier --).
- Two AUDIT-CONFIRMED legacy 6.5 defects (PR #1569 SSA, PR #1499
  iteration guard) — first instances where legacy is provably
  wrong rather than SIMPLNX merely diverging.
- The model V&V pattern is hand-derived expected* arrays inline in
  test source. Filters with one happy-path exemplar test are
  measurably weaker.

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
joeykleingers added a commit to joeykleingers/simplnx that referenced this pull request May 5, 2026
…x bool-mask bulk I/O

Three logically related changes that finish reconciling the rebased
branch with Nathan Young's PR BlueQuartzSoftware#1590 (ENH: Standardize 2D Image Handling)
and fix one resulting OOC perf cliff:

1. Wholesale port of PR BlueQuartzSoftware#1590's two algorithm rewrites into the renamed
   in-core dispatch variants:
   - ComputeFeatureNeighborsDirect.cpp gets Nathan's templated
     ComputeFeatureNeighborsFunctor<ImageDimensionStateT> and ProcessVoxels
     dispatcher in place of the OOC-commit-era custom in-core logic.
   - IdentifySampleBFS.cpp gets Nathan's templated IdentifySampleFunctor
     plus the corresponding ProcessVoxels dispatch.
   The Scanline OOC variant of ComputeFeatureNeighbors is updated to
   reference the namespaced VoxelNeighbors<Image3D>:: constants while
   preserving its Z-slice rolling-window bulk-I/O structure.

2. Reapply PR BlueQuartzSoftware#1590's constexpr/const cleanups across the algorithm
   files where the rebase took --theirs (the OOC commit version) at the
   2aa00ee conflict and dropped Nathan's small adjustments:
     SimplnxCore: ComputeBoundaryCellsDirect, ErodeDilateBadData,
       ErodeDilateCoordinationNumber, ErodeDilateMask,
       ReplaceElementAttributesWithNeighborValues,
       RequireMinimumSizeFeatures
     OrientationAnalysis: BadDataNeighborOrientationCheckWorklist,
       NeighborOrientationCorrelation
   The pattern is uniform: promote the inlined `6` neighbor-array sizes
   to use VoxelNeighbors<Image3D>::k_FaceNeighborCount via a local
   k_NumFaceNeighbors alias, make neighborVoxelIndexOffsets const,
   make faceNeighborInternalIdx constexpr, make isValidFaceNeighbor
   const where it is not mutated, drop the now-unused DataGroup.hpp
   include, and const-ify NeighborOrientationCorrelation's orientationOps.
   ComputeFeatureNeighborsFilter.md picks up Nathan's all-dimension
   note about user-set spacing for shared surface area calculation.

3. Fix a per-element OOC fallback in BadDataNeighborOrientationCheckScanline
   that was triggered whenever the input mask was a BoolArray rather
   than a UInt8Array. The previous code routed bool masks through
   maskCompare->isTrue / maskCompare->setValue per voxel per Z-slice,
   causing chunk thrashing under chunked OOC storage. The Small_IN100
   pipeline test (a 189x201x117 volume with a bool mask produced by
   MultiThresholdObjects) ran in 4.7 s on simplnx-Rel but 3+ minutes
   on simplnx-ooc-Rel. AbstractDataStore<bool> already exposes
   copyIntoBuffer/copyFromBuffer just like AbstractDataStore<uint8>;
   the comment claiming otherwise was stale. Resolve a typed
   AbstractDataStore<bool>* alongside the existing uint8 store pointer
   and route both load and write-back through bulk I/O, with a small
   per-slice std::unique_ptr<bool[]> scratch buffer bridging between
   the algorithm's uint8 slice buffers and the bool data store's typed
   bulk API. With this change Small_IN100 OOC drops to 4.6 s
   (~1.6x in-core, in line with normal OOC overhead).

Tests updated:
  - IdentifySampleTest.cpp adopts Nathan's PR BlueQuartzSoftware#1590 hand-built 2D Empty
    Z/Y/X Non-Square regression tests plus the parameterized
    identify_sample_v2 exemplar test and the SIMPL Backwards Compatibility
    test, all wrapped with the OOC dual-path pattern (ForceOocAlgorithmGuard
    + GENERATE(from_range(k_ForceOocTestValues))). The pre-existing
    200x200x200 large-scale OOC validation test is retained.

Verified: simplnx-Rel and simplnx-ooc-Rel preset builds both clean.
All 43 affected-filter tests pass on simplnx-Rel; all 86 affected-filter
tests pass on simplnx-ooc-Rel (regex covering ComputeFeatureNeighbors,
IdentifySample, BadDataNeighborOrientation, ComputeBoundaryCells,
ErodeDilate*, NeighborOrientationCorrelation,
ReplaceElementAttributesWithNeighborValues, RequireMinimumSizeFeatures).
joeykleingers added a commit to joeykleingers/simplnx that referenced this pull request May 5, 2026
…x bool-mask bulk I/O

Three logically related changes that finish reconciling the rebased
branch with Nathan Young's PR BlueQuartzSoftware#1590 (ENH: Standardize 2D Image Handling)
and fix one resulting OOC perf cliff:

1. Wholesale port of PR BlueQuartzSoftware#1590's two algorithm rewrites into the renamed
   in-core dispatch variants:
   - ComputeFeatureNeighborsDirect.cpp gets Nathan's templated
     ComputeFeatureNeighborsFunctor<ImageDimensionStateT> and ProcessVoxels
     dispatcher in place of the OOC-commit-era custom in-core logic.
   - IdentifySampleBFS.cpp gets Nathan's templated IdentifySampleFunctor
     plus the corresponding ProcessVoxels dispatch.
   The Scanline OOC variant of ComputeFeatureNeighbors is updated to
   reference the namespaced VoxelNeighbors<Image3D>:: constants while
   preserving its Z-slice rolling-window bulk-I/O structure.

2. Reapply PR BlueQuartzSoftware#1590's constexpr/const cleanups across the algorithm
   files where the rebase took --theirs (the OOC commit version) at the
   2aa00ee conflict and dropped Nathan's small adjustments:
     SimplnxCore: ComputeBoundaryCellsDirect, ErodeDilateBadData,
       ErodeDilateCoordinationNumber, ErodeDilateMask,
       ReplaceElementAttributesWithNeighborValues,
       RequireMinimumSizeFeatures
     OrientationAnalysis: BadDataNeighborOrientationCheckWorklist,
       NeighborOrientationCorrelation
   The pattern is uniform: promote the inlined `6` neighbor-array sizes
   to use VoxelNeighbors<Image3D>::k_FaceNeighborCount via a local
   k_NumFaceNeighbors alias, make neighborVoxelIndexOffsets const,
   make faceNeighborInternalIdx constexpr, make isValidFaceNeighbor
   const where it is not mutated, drop the now-unused DataGroup.hpp
   include, and const-ify NeighborOrientationCorrelation's orientationOps.
   ComputeFeatureNeighborsFilter.md picks up Nathan's all-dimension
   note about user-set spacing for shared surface area calculation.

3. Fix a per-element OOC fallback in BadDataNeighborOrientationCheckScanline
   that was triggered whenever the input mask was a BoolArray rather
   than a UInt8Array. The previous code routed bool masks through
   maskCompare->isTrue / maskCompare->setValue per voxel per Z-slice,
   causing chunk thrashing under chunked OOC storage. The Small_IN100
   pipeline test (a 189x201x117 volume with a bool mask produced by
   MultiThresholdObjects) ran in 4.7 s on simplnx-Rel but 3+ minutes
   on simplnx-ooc-Rel. AbstractDataStore<bool> already exposes
   copyIntoBuffer/copyFromBuffer just like AbstractDataStore<uint8>;
   the comment claiming otherwise was stale. Resolve a typed
   AbstractDataStore<bool>* alongside the existing uint8 store pointer
   and route both load and write-back through bulk I/O, with a small
   per-slice std::unique_ptr<bool[]> scratch buffer bridging between
   the algorithm's uint8 slice buffers and the bool data store's typed
   bulk API. With this change Small_IN100 OOC drops to 4.6 s
   (~1.6x in-core, in line with normal OOC overhead).

Tests updated:
  - IdentifySampleTest.cpp adopts Nathan's PR BlueQuartzSoftware#1590 hand-built 2D Empty
    Z/Y/X Non-Square regression tests plus the parameterized
    identify_sample_v2 exemplar test and the SIMPL Backwards Compatibility
    test, all wrapped with the OOC dual-path pattern (ForceOocAlgorithmGuard
    + GENERATE(from_range(k_ForceOocTestValues))). The pre-existing
    200x200x200 large-scale OOC validation test is retained.

Verified: simplnx-Rel and simplnx-ooc-Rel preset builds both clean.
All 43 affected-filter tests pass on simplnx-Rel; all 86 affected-filter
tests pass on simplnx-ooc-Rel (regex covering ComputeFeatureNeighbors,
IdentifySample, BadDataNeighborOrientation, ComputeBoundaryCells,
ErodeDilate*, NeighborOrientationCorrelation,
ReplaceElementAttributesWithNeighborValues, RequireMinimumSizeFeatures).
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.

2 participants