Repository navigation
BUG: Fix wrong output and invalid input handling in 19 filters - #1765
imikejackson merged 18 commits into
Conversation
There was a problem hiding this comment.
PR 1765 Code Review: BUG: Fix wrong output and invalid input handling in 19 filters
Reviewed commit
6b43dc5(imikejackson, 13 commits, compared against merge basec008bccondevelop). Coverage: all 69 changed files. I read every source, header and doc diff in full and checked each fix against thedevelopcode. For the 17 test files, I listed every added test case and read the CropEdgeGeometry and CopyDataObject tests in full. I did not read the other 15 test files line by line.
Findings
-
[Low] ReadImageStack still rejects a physical Z crop whose maximum is exactly the upper bound
File:src/Plugins/ImageProcessing/src/ImageProcessing/Filters/Algorithms/ReadImageStackCropping.hpp:46(ComputeCroppedZRange→ComputeCroppedZDimension)
This PR makes a crop maximum oforigin + dims × spacingvalid for ReadImage, CropImageGeometry, ReadZeissTxm, ReadNIfTI and ReadMha, and ReadImageStack's X/Y crops get the fix through ReadImage. The Z axis doesn't. With 3 files, origin 0 and spacing 1,zBoundPhysical[1] = 3.0passes the range check atImageStackCropping.hpp:82. Thenfloor(3.0) = 3 >= zDimfails with -23525. So in one filter, X/Y accept the upper bound and Z rejects it. This bug predates the PR, but the PR fixes the same bug everywhere else. Fix: clampzMaxIndextozDim - 1inComputeCroppedZDimension, the wayImageGeometryCropnow does, and add aCrop_Physical_Z_MaxAtBoundtest. Any other caller of that shared helper gets the same fix. -
[Low] The new location checks also reject Feature Ids that have the right size but sit in another Attribute Matrix
Files:src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeLargestCrossSectionsFilter.cpp:122,src/Plugins/SimplnxCore/src/SimplnxCore/Filters/SurfaceNetsFilter.cpp:161,:168
The bug was reading past the end of a short DataArray. The fix rejects any DataArray whose parent isn't the geometry's Cell Data Attribute Matrix. That also rejects a Feature Ids array with exactlygetNumberOfCells()tuples stored in another Attribute Matrix of the same shape, which worked ondevelop. The PR body states this behavior change, so it's a design question rather than a bug: comparing the tuple count withimageGeom.getNumberOfCells()would prevent the out-of-bounds read without breaking those layouts. Also, -3711 and -56350 are each used for two different conditions ("no Cell Data" and "wrong parent"). Separate codes would make the failure tests more specific. -
[Low] No test covers the CopyDataObject fix when "Use New Parent" is on
File:src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CopyDataObjectFilter.cpp:132
Ondevelop, the created paths were made by replacing the target name in the path string. With Use New Parent on, that kept the old parent (Group/A_copy/child), but the object is copied toNewParent/A_copy/child. UsingnewDataPathinrebasefixes that case too, but the new test only checks the suffix case under the same parent. Add a section withk_UseNewParent_Key = trueand a group child, and check that the declared paths are under the new parent. -
[Nit] Anonymous namespace in a header
File:src/Plugins/ImageProcessing/src/ImageProcessing/Filters/Algorithms/ReadImageStackCropping.hpp:6
ZRangeandComputeCroppedZRangeare defined in an unnamed namespace inside a header that two .cpp files include, so each file gets its own copy. This is the CERT DCL59-CPP pattern. Put them innx::core(the function is alreadyinline). The header also relies on other includes forstd::floorandfmt::format. Include<cmath>,<fmt/format.h>andsimplnx/Common/Result.hppdirectly. -
[Nit] Redundant comparison in the output-range check
File:src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RegularGridSampleSurfaceMesh.cpp:419
minLabel > outputMax || maxLabel > outputMax:minLabel <= maxLabel, so the first comparison never decides the result. UsemaxLabel > outputMax.
C++ checks
- Memory/lifetime: no issues. The
CropEdgeGeometryparallel tasks borrowsourceIndices, andtaskRunner.wait()returns before that vector is destroyed.NeighborList::setValuetakes a copy of the vector, so duplicated rows don't share storage with the source. - Thread safety: no issues. In
CopyAttributeArrays, StringArray and NeighborList data is copied on the calling thread while worker tasks write other arrays. In RegularGridSampleSurfaceMesh, the converted slice is written while the existing mutex is held. - Efficiency: no issues worth raising. RegularGridSampleSurfaceMesh allocates one converted buffer per slice, and only for a custom output type.
- Formatting: clang-format 19.1.1 with the root
.clang-format, dry run on all 50 changed C/C++ files: pass. CI'sclang_format_prcheck also passes.
Documentation
No issues found. The 19 doc changes match the code. For example, the -50503 clamp warning that ReadImage now passes through exists in ImageGeometryCrop.cpp:172-180, and the Silhouette text on the cluster-0 mean matches both algorithms.
Missing or failed evidence
- I listed every added test case in 15 of the 17 test files and checked that it targets the stated defect, but I didn't read those test bodies line by line. CI passing confirms they run and pass. I didn't confirm locally that each one fails on
develop; that comes from the PR description. - No local build or test run.
Confirmed correct
Each defect below is confirmed against the develop code:
DataPath::rebase/attemptRename: ondevelop,attemptRenameoverwrote onlyoldPath.getLength()components. A shorternewPathread past its end, and a longer one was cut short.rebasecompares whole path components (Edge Geometry≠Edge Geometry Ids) and handles prefixes of any depth. Each call site's.value()can't fail, because child paths always start with the source path.- Child renaming by substring (7 places):
StringUtilities::replacereplaced every occurrence in the full path string, soCell Feature DatabecameCell Feature Cropped. All 7 call sites now userebase. - Crop at the upper bound:
floor((max − origin)/spacing)equalsdimswhen max is exactly the upper bound, which tripped thexMax > numCells − 1error (-5553). Clamping withstd::minfixes it, and a value past the bound still goes to the existing clamp and warning branch. - ReadImage minimum below the origin: on
develop, execute computed a negative voxel index and failed with -2002, while preflight clamped. Execute now clamps to 0 as well. - ReadImageStack Z range: on
develop, execute converted physical Z through the destination geometry, which is already cropped and offset, so any crop not starting at slice 0 mapped to the wrong slice. Preflight and execute now use the sameComputeCroppedZRange. The Z origin shift is replaced by a postprocessed override, which runs as a deferredUpdateImageGeomAction(ReadImageStackFilter.cpp:395-400). - ComputeFeatureBounds: the
currentOffset--before resizing dropped the last box, and edge Feature Ids held the box index. The bounds buffer is now sized by the Feature Data Attribute Matrix, which the Split and Unified output loops already iterate over. - Silhouette Direct: the distance table had
numClusters + 1columns but was indexed by raw id, so sparse ids wrote past its end. The dense mapping matches the Scanline path. Empty clusters (ids found only in masked-out tuples) are left out of both the division and the minimum. Cluster 0 is now divided by its count, which matches the Scanline change. - RegularGridSampleSurfaceMesh:
getDataRefAs<DataArray<T>>used the Face Labels type, so it threwbad_castwhen the output type was different. The range check runs before workers are scheduled, and negative labels are clamped to 0 before the toggle logic. - QuickSurfaceMesh/SurfaceNets Direct: the selected feature DataArrays are now checked against the largest Feature Id, matching the Scanline path. The removed preflight variables were unused. A negative
Max Distancegavestd::clampa low bound above its high bound, which is undefined behavior; preflight now rejects it. - WriteASCIIData:
PrintDataSetsToMultipleFilesnames each file<dir>/<targetName><ext>(OStreamUtilities.cpp:557), so two arrays with the same name wrote the same file. - CropEdgeGeometry: on
develop, there was one clip point per outside vertex, so a second crossing reused the first clip point. The(vertex, clipPoint)key gives each crossing its own vertex, extra copies are added after the originals, and theelsebranch never readssrcVerticesfor a duplicate. Ondevelop, StringArray and NeighborList preflight failed atdynamic_cast<const IDataArray&>withbad_cast. - CI: passes on macOS, both Ubuntu toolchains, Windows, and clang-format.
Verdict
APPROVE: every fix addresses a confirmed defect on develop and has a matching regression test. The findings are a related upper-bound case the PR doesn't cover, one design question, one untested branch, and two nits; none of them blocks merging.
* attemptRename copied newPath.m_Path[i] for each index of oldPath. When the new prefix was shorter it read past its end, and when it was longer it dropped the extra components, so it was correct only for prefixes of the same depth. It now delegates to rebase() * Add DataPath::rebase(oldPrefix, newPrefix), which returns the path with the leading oldPrefix replaced by newPrefix, comparing whole components (a child "Edge Geometry Ids" does not start with "Edge Geometry"). It returns std::nullopt when the path does not start with oldPrefix * Add tests for rebase and for attemptRename with prefixes of different depth Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a filter copied the other children of a geometry (or of a copied DataGroup) to a new location, it built each copied DataPath by replacing every occurrence of the source name in the whole path string. A child whose name contains the source name was renamed (Geom Feature Data under Geom became Geom Out Feature Data), and a parent DataGroup whose name contains it was renamed too, so the copy failed with -5102. Each site now uses DataPath::rebase(), which replaces the leading path components only. * CE-2: CropEdgeGeometry * RotateSampleRefFrame * RemoveFlaggedVertices * PadImageGeometry * CopyDataObject (the declared created paths of the copied children) * ImageGeometryCrop (CropImageGeometry and the image readers that crop) * ImageGeometryResample (ResampleImageGeom and ReadImageStack) * Add a test for each filter and a sentence to each filter doc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* RI-3: a physical crop whose maximum equals the upper bound of the Image Geometry (origin + dimensions x spacing) computed a maximum index equal to the number of cells, so preflight failed with -5553, while a slightly larger maximum was clamped with only a warning. The shared crop helper now clamps the computed maximum index to dimensions - 1 * The helper is shared, so this fixes ReadImage, CropImageGeometry, ReadZeissTxm, ReadNIfTI and ReadMha. Add a test for each caller and a doc sentence to each filter doc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* RI-1: a physical crop whose minimum is below the image origin passed preflight (the shared crop helper clamps it to the first cell), then execute computed a negative start index and failed with -2002. Execute now uses the same minimum rule as preflight on X, Y and Z * RI-1b: preflight discarded the warnings of the shared crop helper, so the -50503 "clamped" warning never reached the user. They are now returned with the preflight result * RI-2: "Put Input Origin at the Center of Geometry" centers the Image Geometry on (0, 0, 0) and does not use the Origin value. The behavior does not change; the parameter is renamed "Center Geometry on (0, 0, 0)", its help text and the filter doc say what it does, and a test pins it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* RIS-1: execute looked up the physical Z crop in the already-cropped Image Geometry, so every physical Z crop that did not start at the first slice failed with -64512/-64513. Preflight and execute now compute the slice range of the full stack with one shared helper, ComputeCroppedZRange (ReadImageStackCropping.hpp) * RIS-2: a Z crop did not shift the Z origin, so the volume was placed too low. The Z origin now moves to the first kept slice, as X/Y crops and ReadImage already do * RIS-3: Convert to Grayscale on an image DataArray with 1 or 2 components was not rejected (the parameter help says it is) and read neighboring values or past the end of the buffer. Preflight now returns -23507 * Document the Z origin shift and add tests for each defect Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* FB-1: with Create Bounding Box Geometries on, the counter of written boxes was decremented before the Edge Geometry was resized, so the last bounding box was always dropped (one feature gave an empty Edge Geometry). The decrement is removed in Direct and Scanline * FB-2: the Edge Feature Ids DataArray held the box position 0, 1, 2... instead of the Feature Id of the feature each box bounds * FB-3: the internal bounds buffer was sized by the largest Feature Id, but the output loops ran over the Feature Data Attribute Matrix tuple count, which the doc allows to be larger, so the extra tuples were read past the end of the buffer. The buffer is now sized by the Feature Data tuple count, and the extra tuples are written as NaN (this also fixes minor FB-4) * CS-5: the Feature Ids parameter now declares a component shape of 1, because the algorithms read one Feature Id per element * Document the edge geometry contents and NaN tuples; add tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LCS-1: preflight did not check that the Feature Ids DataArray is a
child of the Image Geometry's Cell Data Attribute Matrix. The parameter
accepts a DataArray from any Attribute Matrix, so one with fewer tuples
than the number of cells was read past its end and the values read were
used as an index for a write. Preflight now returns -3711 when the Feature
Ids parent is not the geometry's Cell Data. An Attribute Matrix only
accepts children with its tuple count, so this also guarantees the
{Z,Y,X} tuple shape that the Scanline path reads (minor LCS-3)
* The existing test fixtures now attach their Cell Data to the Image
Geometry
* Document the requirement and add a test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* SIL-1: the Direct algorithm sized its distance table by the number of
distinct Cluster Ids but indexed it by the raw id, so sparse ids such as
{3, 7} or a negative id wrote past the end of the table. Direct now maps
Cluster Ids to dense indices (0 stays 0), as Scanline does, and rejects
negative ids with -54081
* SIL-2: with Use Mask on, preflight did not check that the Mask Array and
Cluster Ids have the same number of tuples as the Attribute Array to
Silhouette. Preflight now returns -8977
* SIL-3: for points in cluster 0, the mean distance to the point's own
cluster was a sum, so their scores were wrong. Both algorithms now
divide by the cluster size. Empty clusters cannot be the competing
cluster
* CS-9, CS-10: the Mask Array and Cluster Ids parameters now declare a
component shape of 1, because both algorithms read one value per tuple
* Document cluster 0 scoring and non-contiguous ids; add tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* RG-1: with Use Custom Output Type on, any Output Type for Feature Ids that differed from the Face Labels DataType made execute fail with bad_cast (-2). The write now dispatches on the output DataType and converts each slice when the types differ * RG-1 (D-A6): a Face Labels value that does not fit in the chosen output type is an execute error, -11801, that names the value range and the type. Preflight cannot see the values * Use Custom Output Type now sits directly above Output Type for Feature Ids and is linked to it, so the output type is shown only when the bool is on. Parameter keys do not change * RG-2: Face Labels of -1, the exterior label that QuickSurfaceMesh and SurfaceNets write, were copied into the Feature Ids, so the rest of a row of cells became -1. A label below 0 is now treated as 0 * Document both rules and add tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* QSM-2: nothing checked that each selected feature DataArray has more tuples than the largest Feature Id. The Direct algorithm read past the end of a too-short feature DataArray and returned success, while Scanline returned -62073, so the two algorithms disagreed. Direct now makes the same check and returns -62073. The check depends on the Feature Id values, so it runs at execute * Remove the misleading preflight comment that promised this check, and the unused variable next to it * Document the requirement and add a test that runs both algorithms Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* WA-1: in Multiple Files mode, two selected DataArrays with the same name in different parents (for example /A/Data and /B/Data) were written to the same output file. The second file overwrote the first and the filter reported success. Preflight now returns -51003 when two selected DataArrays have the same name, ignoring case, because some file systems do not distinguish case * Document the requirement and add a test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* SN-1: the Direct algorithm did not check that each selected feature DataArray has more tuples than the largest Feature Id, so a too-short DataArray was read past its end. Direct now makes the same check as Scanline and returns -62073 * SN-2: preflight did not check that Cell Feature Ids and the selected cell DataArrays are children of the Image Geometry's Cell Data Attribute Matrix, so a DataArray with fewer tuples than cells could be selected and read past its end. Preflight now returns -56350 (Cell Feature Ids, or no Cell Data Attribute Matrix) and -56351 (selected cell DataArrays) * SN-4: with Apply smoothing operations on, a Max Distance from Voxel Center below 0 was undefined behavior (std::clamp with lo > hi) and a negative Relaxation Iterations was accepted silently. Preflight now returns -56352 and -56353 * Remove the unused preflight variable that suggested the SN-1 check * Document the requirements and add tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t data * CE-1: in Interpolate mode, when one vertex outside the crop bounds was shared by two edges that cross the bounds at different points, both edges got the same clipped vertex, so one edge was bent off its line. The outside vertex is now duplicated, one copy for each distinct clip point, and its vertex data is copied to each duplicate * CE-3: a StringArray or NeighborList in the Vertex or Edge Attribute Matrix threw bad_cast out of preflight, which crashed the application. Preflight now creates these arrays, and execute copies them with the kept vertices and edges, using the same gather list as the DataArrays * Document both behaviors and add tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6b43dc5 to
d17b0e9
Compare
* A physical Z crop maximum equal to origin + files x spacing passed the range check (-23523), but floor() then gave a slice index equal to the file count, which failed with -23525. The X and Y axes and the other image readers already accept this value. ComputeCroppedZDimension now clamps the maximum slice index to the last slice, the same way ImageGeometryCrop does, and -23525 is no longer reachable * Add Crop_Physical_Z_MaxAtBound test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ComputeLargestCrossSections and SurfaceNets rejected any Feature Ids (or SurfaceNets selected cell DataArray) that was not in the Image Geometry's Cell Data Attribute Matrix. That also rejected correctly sized DataArrays in another Attribute Matrix, which worked on develop * Preflight now checks that the tuple count equals the number of cells, which still prevents the read past the end of a short DataArray. The Image Geometry no longer needs an assigned Cell Data Attribute Matrix * Each error code now has one meaning: -3711 (Feature Ids tuple count), -56350 (Cell Feature Ids tuple count), -56351 (selected cell DataArray tuple count). The error messages give both counts * Add tests for too-short DataArrays and for correctly sized DataArrays in another Attribute Matrix; update the docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Copy a DataGroup with a child to a new parent and check that the declared and copied paths are under the new parent, not the old one Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ZRange and ComputeCroppedZRange are in a header, so put them in nx::core instead of an unnamed namespace (CERT DCL59-CPP) * Include <cmath>, <fmt/format.h> and Result.hpp directly Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h range check * minLabel <= maxLabel, so only maxLabel > outputMax decides the result Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add IsChildOfAttributeMatrix(dataStructure, arrayPath, attributeMatrix) to DataArrayUtilities. It compares DataObject ids, so it is correct when the Attribute Matrix can be reached through more than one parent path, and it returns false when the path has no parent * ComputeLargestCrossSections (-3711) and SurfaceNets (-56350, -56351) keep the tuple-count checks merged in BlueQuartzSoftware#1765, which accept correctly sized DataArrays from another Attribute Matrix, so they do not use the helper. RegularGridSampleSurfaceMesh uses it in a later commit * Add unit tests for the helper Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
This change fixes defects that a code review found in filters that do not have a V&V report. Each defect makes a filter give wrong output, read or write past the end of an array, crash, or fail with a valid configuration. Each fix has a regression test that fails on
develop. There is one commit for each filter or shared utility. No test data archive is added or changed. 70 files changed, +2612 / −318 lines.Behavior changes
Data/Cell Feature Datacopied toCroppednow givesCropped/Cell Feature Data, notCropped/Cell Feature Cropped. A saved pipeline that refers to the old, incorrect name must be updated.================================================================================
================================================================================
src/simplnx/DataStructure/DataPath.hpp/.cppsrc/simplnx/Utilities/ImageProcessing/ImageGeometryCrop.cppsrc/simplnx/Utilities/ImageProcessing/ImageGeometryResample.cppDataPath::rebase(oldPrefix, newPrefix)replaces the leading components of a path. It compares whole components, so the childEdge Geometry Idsdoes not start withEdge Geometry.DataPath::attemptRenamenow uses it. Ondevelop,attemptRenameis correct only when the old and new prefixes have the same depth. Pipeline rename tracking uses only prefixes of the same depth, so its behavior does not change.DataPath::rebase. The places are CropEdgeGeometry, RotateSampleRefFrame, RemoveFlaggedVertices, PadImageGeometry, CopyDataObject,ImageGeometryCropandImageGeometryResample. Ondevelop, a parent DataGroup whose name contains the geometry name is renamed too, so the copy fails with −5102.develop, it fails with −5553, while a slightly larger value is clamped. This fixes ReadImage, CropImageGeometry, ReadZeissTxm, ReadNIfTI and ReadMha.src/simplnx/Utilities/ImageIO/ImageStackCropping.hpp: the same fix for the Z axis of an image stack. A physical Z crop maximum equal to origin + number of files × spacing is accepted. Ondevelop, it fails with −23525.================================================================================
2. Image readers
src/Plugins/ImageProcessing/src/ImageProcessing/Filters/develop, execute fails with −2002. Preflight now returns the warnings of the crop helper, so the −50503 "clamped" warning reaches the user.develop, a physical Z crop that does not start at the first slice fails at execute. A Z crop also moves the Z origin to the first kept slice, as X and Y crops do. Convert to Grayscale on an image with 1 or 2 components fails in preflight (−23507). Ondevelop, it reads neighboring values or reads past the end of the buffer.================================================================================
3. SimplnxCore filters
develop, the last box is lost. Each edge has the Feature Id of its feature, not the position of the box. The internal bounds buffer is sized by the Feature Data Attribute Matrix, so extra tuples are written as NaN instead of read past the end of the buffer.develop, a shorter DataArray is read past its end, and the values read are used as an index for a write. A correctly sized DataArray in another Attribute Matrix is still accepted.develop, both write past the end of a table. With Use Mask on, preflight checks that the mask, the Cluster Ids and the input DataArray have the same number of tuples (−8977). Points in cluster 0 use the mean distance to their own cluster, not the sum.develop, it fails withbad_cast. A Face Labels value that does not fit in the output type is an execute error (−11801). A Face Label below 0 is treated as 0. Ondevelop, the −1 exterior label that QuickSurfaceMesh and SurfaceNets write is copied into the output, so the rest of the row becomes −1.develop, Direct reads past the end of the DataArray and returns success.develop, a shorter DataArray is read past its end. With smoothing on, a negative Max Distance from Voxel Center (−56352) or Relaxation Iterations (−56353) fails in preflight. Ondevelop, the negative distance is undefined behavior instd::clamp.develop, two selected DataArrays with the same name in Multiple Files mode write the same file, and the second one overwrites the first.develop, both edges use one clip point, so one edge moves off its line. StringArray and NeighborList data in the Vertex and Edge Attribute Matrices are copied with the kept vertices and edges. Ondevelop, they make preflight throwbad_cast.Test plan
developfor the reason that its commit message gives.develop).