Skip to content

Commit fcfc58a

Browse files
ENH: Add slice-by-slice processing option to Isolate Largest Feature. (#1169)
Signed-off-by: Joey Kleingers <[email protected]>
1 parent 27a6089 commit fcfc58a

File tree

3 files changed

+237
-17
lines changed

3 files changed

+237
-17
lines changed

src/Plugins/SimplnxCore/docs/IdentifySampleFilter.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ If *Fill Holes* is set to *true*:
1717

1818
*Note:* if there are in fact "holes" in the sample, then this **Filter** will "close" them (if *Fill Holes* is set to true) by calling all the **Cells** "inside" the sample *good*. If the user wants to reidentify those holes, then reuse the threshold **Filter** with the criteria of *GoodVoxels = 1* and whatever original criteria identified the "holes", as this will limit applying those original criteria to within the sample and not the outer border region.
1919

20+
*Additional Note:* Only completely water-tight, internal holes within the sample are addressed when *Fill Holes* is enabled. To fill in a contiguous group of good cells that includes holes located along the outer edge of the sample, try enabling *Process Data Slice-By-Slice*. For each slice of the chosen plane, this will search for the largest contiguous set of *good* **Cells**, set all other *good* **Cells** to be *bad* **Cells**, and (if *Fill Holes* is enabled) fill all water-tight holes PER SLICE instead of the whole 3D volume at once. This option can be used to allow non water-tight holes to be filled without also accidentally filling the surrounding overscan area.
21+
2022
| Name | Description |
2123
|------|-------------|
2224
|![Small IN100 IPF Map](Images/Small_IN100.png) | Good dataset to use this filter |

src/Plugins/SimplnxCore/src/SimplnxCore/Filters/IdentifySampleFilter.cpp

Lines changed: 233 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "simplnx/DataStructure/Geometry/ImageGeom.hpp"
55
#include "simplnx/Parameters/ArraySelectionParameter.hpp"
66
#include "simplnx/Parameters/BoolParameter.hpp"
7+
#include "simplnx/Parameters/ChoicesParameter.hpp"
78
#include "simplnx/Parameters/DataGroupSelectionParameter.hpp"
89
#include "simplnx/Parameters/GeometrySelectionParameter.hpp"
910
#include "simplnx/Utilities/FilterUtilities.hpp"
@@ -46,11 +47,8 @@ struct IdentifySampleFunctor
4647
std::vector<bool> checked(totalPoints, false);
4748
std::vector<bool> sample(totalPoints, false);
4849
int64 biggestBlock = 0;
49-
usize count = 0;
50-
int32 good = 0;
50+
5151
int64 neighbor = 0;
52-
int64 column = 0, row = 0, plane = 0;
53-
int64 index = 0;
5452

5553
// In this loop over the data we are finding the biggest contiguous set of GoodVoxels and calling that the 'sample' All GoodVoxels that do not touch the 'sample'
5654
// are flipped to be called 'bad' voxels or 'not sample'
@@ -70,16 +68,16 @@ struct IdentifySampleFunctor
7068
if(!checked[i] && goodVoxels.getValue(i))
7169
{
7270
currentVList.push_back(i);
73-
count = 0;
71+
usize count = 0;
7472
while(count < currentVList.size())
7573
{
76-
index = currentVList[count];
77-
column = index % xp;
78-
row = (index / xp) % yp;
79-
plane = index / (xp * yp);
74+
int64 index = currentVList[count];
75+
int64 column = index % xp;
76+
int64 row = (index / xp) % yp;
77+
int64 plane = index / (xp * yp);
8078
for(int32 j = 0; j < 6; j++)
8179
{
82-
good = 1;
80+
int32 good = 1;
8381
neighbor = index + neighborPoints[j];
8482
if(j == 0 && plane == 0)
8583
{
@@ -156,21 +154,21 @@ struct IdentifySampleFunctor
156154
if(!checked[i] && !goodVoxels.getValue(i))
157155
{
158156
currentVList.push_back(i);
159-
count = 0;
157+
usize count = 0;
160158
touchesBoundary = false;
161159
while(count < currentVList.size())
162160
{
163-
index = currentVList[count];
164-
column = index % xp;
165-
row = (index / xp) % yp;
166-
plane = index / (xp * yp);
161+
int64 index = currentVList[count];
162+
int64 column = index % xp;
163+
int64 row = (index / xp) % yp;
164+
int64 plane = index / (xp * yp);
167165
if(column == 0 || column == (xp - 1) || row == 0 || row == (yp - 1) || plane == 0 || plane == (zp - 1))
168166
{
169167
touchesBoundary = true;
170168
}
171169
for(int32 j = 0; j < 6; j++)
172170
{
173-
good = 1;
171+
int32 good = 1;
174172
neighbor = index + neighborPoints[j];
175173
if(j == 0 && plane == 0)
176174
{
@@ -218,6 +216,200 @@ struct IdentifySampleFunctor
218216
checked.clear();
219217
}
220218
};
219+
220+
struct IdentifySampleSliceBySliceFunctor
221+
{
222+
enum class Plane
223+
{
224+
XY,
225+
XZ,
226+
YZ
227+
};
228+
229+
template <typename T>
230+
void operator()(const ImageGeom* imageGeom, IDataArray* goodVoxelsPtr, bool fillHoles, Plane plane)
231+
{
232+
auto& goodVoxels = goodVoxelsPtr->template getIDataStoreRefAs<AbstractDataStore<T>>();
233+
234+
SizeVec3 uDims = imageGeom->getDimensions();
235+
const int64 dimX = static_cast<int64>(uDims[0]);
236+
const int64 dimY = static_cast<int64>(uDims[1]);
237+
const int64 dimZ = static_cast<int64>(uDims[2]);
238+
239+
int64 planeDim1, planeDim2, fixedDim;
240+
int64 stride1, stride2, fixedStride;
241+
242+
switch(plane)
243+
{
244+
case Plane::XY:
245+
planeDim1 = dimX;
246+
planeDim2 = dimY;
247+
fixedDim = dimZ;
248+
stride1 = 1;
249+
stride2 = dimX;
250+
fixedStride = dimX * dimY;
251+
break;
252+
253+
case Plane::XZ:
254+
planeDim1 = dimX;
255+
planeDim2 = dimZ;
256+
fixedDim = dimY;
257+
stride1 = 1;
258+
stride2 = dimX * dimY;
259+
fixedStride = dimX;
260+
break;
261+
262+
case Plane::YZ:
263+
planeDim1 = dimY;
264+
planeDim2 = dimZ;
265+
fixedDim = dimX;
266+
stride1 = dimX;
267+
stride2 = dimX * dimY;
268+
fixedStride = 1;
269+
break;
270+
}
271+
272+
for(int64 fixedIdx = 0; fixedIdx < fixedDim; ++fixedIdx) // Process each slice
273+
{
274+
std::vector<bool> checked(planeDim1 * planeDim2, false);
275+
std::vector<bool> sample(planeDim1 * planeDim2, false);
276+
std::vector<int64> currentVList;
277+
int64 biggestBlock = 0;
278+
279+
// Identify the largest contiguous set of good voxels in the slice
280+
for(int64 p2 = 0; p2 < planeDim2; ++p2)
281+
{
282+
for(int64 p1 = 0; p1 < planeDim1; ++p1)
283+
{
284+
int64 planeIndex = p2 * planeDim1 + p1;
285+
int64 globalIndex = fixedIdx * fixedStride + p2 * stride2 + p1 * stride1;
286+
287+
if(!checked[planeIndex] && goodVoxels.getValue(globalIndex))
288+
{
289+
currentVList.push_back(planeIndex);
290+
int64 count = 0;
291+
292+
while(count < currentVList.size())
293+
{
294+
int64 localIdx = currentVList[count];
295+
int64 localP1 = localIdx % planeDim1;
296+
int64 localP2 = localIdx / planeDim1;
297+
298+
for(int j = 0; j < 4; ++j)
299+
{
300+
int64 dp1[4] = {0, 0, -1, 1};
301+
int64 dp2[4] = {-1, 1, 0, 0};
302+
303+
int64 neighborP1 = localP1 + dp1[j];
304+
int64 neighborP2 = localP2 + dp2[j];
305+
306+
if(neighborP1 >= 0 && neighborP1 < planeDim1 && neighborP2 >= 0 && neighborP2 < planeDim2)
307+
{
308+
int64 neighborIdx = neighborP2 * planeDim1 + neighborP1;
309+
int64 globalNeighborIdx = fixedIdx * fixedStride + neighborP2 * stride2 + neighborP1 * stride1;
310+
311+
if(!checked[neighborIdx] && goodVoxels.getValue(globalNeighborIdx))
312+
{
313+
currentVList.push_back(neighborIdx);
314+
checked[neighborIdx] = true;
315+
}
316+
}
317+
}
318+
count++;
319+
}
320+
321+
if(static_cast<int64>(currentVList.size()) > biggestBlock)
322+
{
323+
biggestBlock = currentVList.size();
324+
sample.assign(planeDim1 * planeDim2, false);
325+
for(int64 idx : currentVList)
326+
{
327+
sample[idx] = true;
328+
}
329+
}
330+
currentVList.clear();
331+
}
332+
}
333+
}
334+
335+
for(int64 p2 = 0; p2 < planeDim2; ++p2)
336+
{
337+
for(int64 p1 = 0; p1 < planeDim1; ++p1)
338+
{
339+
int64 planeIndex = p2 * planeDim1 + p1;
340+
int64 globalIndex = fixedIdx * fixedStride + p2 * stride2 + p1 * stride1;
341+
342+
if(!sample[planeIndex])
343+
{
344+
goodVoxels.setValue(globalIndex, false);
345+
}
346+
}
347+
}
348+
349+
if(fillHoles)
350+
{
351+
for(int64 p2 = 0; p2 < planeDim2; ++p2)
352+
{
353+
for(int64 p1 = 0; p1 < planeDim1; ++p1)
354+
{
355+
int64 planeIndex = p2 * planeDim1 + p1;
356+
int64 globalIndex = fixedIdx * fixedStride + p2 * stride2 + p1 * stride1;
357+
358+
if(!checked[planeIndex] && !goodVoxels.getValue(globalIndex))
359+
{
360+
currentVList.push_back(planeIndex);
361+
int64 count = 0;
362+
bool touchesBoundary = false;
363+
364+
while(count < currentVList.size())
365+
{
366+
int64 localIdx = currentVList[count];
367+
int64 localP1 = localIdx % planeDim1;
368+
int64 localP2 = localIdx / planeDim1;
369+
370+
if(localP1 == 0 || localP1 == planeDim1 - 1 || localP2 == 0 || localP2 == planeDim2 - 1)
371+
{
372+
touchesBoundary = true;
373+
}
374+
375+
for(int j = 0; j < 4; ++j)
376+
{
377+
int64 dp1[4] = {0, 0, -1, 1};
378+
int64 dp2[4] = {-1, 1, 0, 0};
379+
380+
int64 neighborP1 = localP1 + dp1[j];
381+
int64 neighborP2 = localP2 + dp2[j];
382+
383+
if(neighborP1 >= 0 && neighborP1 < planeDim1 && neighborP2 >= 0 && neighborP2 < planeDim2)
384+
{
385+
int64 neighborIdx = neighborP2 * planeDim1 + neighborP1;
386+
int64 globalNeighborIdx = fixedIdx * fixedStride + neighborP2 * stride2 + neighborP1 * stride1;
387+
388+
if(!checked[neighborIdx] && !goodVoxels.getValue(globalNeighborIdx))
389+
{
390+
currentVList.push_back(neighborIdx);
391+
checked[neighborIdx] = true;
392+
}
393+
}
394+
}
395+
count++;
396+
}
397+
398+
if(!touchesBoundary)
399+
{
400+
for(int64 idx : currentVList)
401+
{
402+
goodVoxels.setValue(fixedIdx * fixedStride + idx, true);
403+
}
404+
}
405+
currentVList.clear();
406+
}
407+
}
408+
}
409+
}
410+
}
411+
}
412+
};
221413
} // namespace
222414

223415
//------------------------------------------------------------------------------
@@ -257,11 +449,26 @@ Parameters IdentifySampleFilter::parameters() const
257449

258450
params.insertSeparator(Parameters::Separator{"Input Parameter(s)"});
259451
params.insert(std::make_unique<BoolParameter>(k_FillHoles_Key, "Fill Holes in Largest Feature", "Whether to fill holes within sample after it is identified", true));
452+
params.insertLinkableParameter(std::make_unique<BoolParameter>(k_SliceBySlice_Key, "Process Data Slice-By-Slice",
453+
"Whether to identify the largest sample (and optionally fill holes) slice-by-slice. This option is useful if you have a sample that "
454+
"is not water-tight and the holes open up to the overscan section, or if you have holes that sit on a boundary. The original "
455+
"algorithm will not fill holes that have these characteristics, only holes that are completely enclosed by the sample and "
456+
"water-tight. If you have holes that are not water-tight or sit on a boundary, choose this option and then pick the plane that will "
457+
"allow the holes to be water-tight on each slice of that plane.",
458+
false));
459+
params.insert(
460+
std::make_unique<ChoicesParameter>(k_SliceBySlicePlane_Key, "Slice-By-Slice Plane",
461+
"Set the plane that the data will be processed slice-by-slice. For example, if you pick the XY plane, the data will be processed in the Z direction.", 0,
462+
ChoicesParameter::Choices{"XY", "XZ", "YZ"}));
463+
260464
params.insert(std::make_unique<GeometrySelectionParameter>(k_SelectedImageGeometryPath_Key, "Image Geometry", "DataPath to the target ImageGeom", DataPath(),
261465
GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Image}));
262466
params.insert(std::make_unique<ArraySelectionParameter>(k_MaskArrayPath_Key, "Mask Array", "DataPath to the mask array defining what is sample and what is not", DataPath(),
263467
ArraySelectionParameter::AllowedTypes{nx::core::DataType::boolean, nx::core::DataType::uint8},
264468
ArraySelectionParameter::AllowedComponentShapes{{1}}));
469+
470+
params.linkParameters(k_SliceBySlice_Key, k_SliceBySlicePlane_Key, true);
471+
265472
return params;
266473
}
267474

@@ -301,13 +508,22 @@ Result<> IdentifySampleFilter::executeImpl(DataStructure& dataStructure, const A
301508
const std::atomic_bool& shouldCancel) const
302509
{
303510
const auto fillHoles = args.value<bool>(k_FillHoles_Key);
511+
const auto sliceBySlice = args.value<bool>(k_SliceBySlice_Key);
512+
const auto sliceBySlicePlane = static_cast<IdentifySampleSliceBySliceFunctor::Plane>(args.value<ChoicesParameter::ValueType>(k_SliceBySlicePlane_Key));
304513
const auto imageGeomPath = args.value<DataPath>(k_SelectedImageGeometryPath_Key);
305514
const auto goodVoxelsArrayPath = args.value<DataPath>(k_MaskArrayPath_Key);
306515

307516
auto* inputData = dataStructure.getDataAs<IDataArray>(goodVoxelsArrayPath);
308517
const auto* imageGeom = dataStructure.getDataAs<ImageGeom>(imageGeomPath);
309518

310-
ExecuteDataFunction(IdentifySampleFunctor{}, inputData->getDataType(), imageGeom, inputData, fillHoles);
519+
if(sliceBySlice)
520+
{
521+
ExecuteDataFunction(IdentifySampleSliceBySliceFunctor{}, inputData->getDataType(), imageGeom, inputData, fillHoles, sliceBySlicePlane);
522+
}
523+
else
524+
{
525+
ExecuteDataFunction(IdentifySampleFunctor{}, inputData->getDataType(), imageGeom, inputData, fillHoles);
526+
}
311527

312528
return {};
313529
}

src/Plugins/SimplnxCore/src/SimplnxCore/Filters/IdentifySampleFilter.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class SIMPLNXCORE_EXPORT IdentifySampleFilter : public IFilter
2222

2323
// Parameter Keys
2424
static inline constexpr StringLiteral k_FillHoles_Key = "fill_holes";
25+
static inline constexpr StringLiteral k_SliceBySlice_Key = "slice_by_slice";
26+
static inline constexpr StringLiteral k_SliceBySlicePlane_Key = "slice_by_slice_plane_index";
2527
static inline constexpr StringLiteral k_SelectedImageGeometryPath_Key = "input_image_geometry_path";
2628
static inline constexpr StringLiteral k_MaskArrayPath_Key = "mask_array_path";
2729

0 commit comments

Comments
 (0)