Skip to content

Commit a625e56

Browse files
committed
VV: Group Microtexture Regions fully V&V'ed
1 parent f8b56d0 commit a625e56

14 files changed

Lines changed: 1767 additions & 0 deletions

src/Plugins/OrientationAnalysis/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ set(FilterList
6666
CreateEnsembleInfoFilter
6767
EBSDSegmentFeaturesFilter
6868
EbsdToH5EbsdFilter
69+
GroupMicroTextureRegionsFilter
6970
MergeTwinsFilter
7071
NeighborOrientationCorrelationFilter
7172
ReadAngDataFilter
@@ -204,6 +205,7 @@ set(filter_algorithms
204205
CreateEnsembleInfo
205206
EBSDSegmentFeatures
206207
EbsdToH5Ebsd
208+
GroupMicroTextureRegions
207209
MergeTwins
208210
NeighborOrientationCorrelation
209211
ReadAngData
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Group MicroTexture Regions
2+
3+
## Group (Subgroup)
4+
5+
Reconstruction Filters (Grouping)
6+
7+
## Description
8+
9+
This Filter groups neighboring **Features** that have c-axes aligned within a user-defined tolerance. The algorithm for grouping the **Features** is analogous to the algorithm for segmenting the **Features** — only the average orientation of the **Features** is used instead of the orientations of the individual **Cells**, and the criterion for grouping only considers the alignment of the c-axes. The user can specify a tolerance for how closely aligned the c-axes must be for neighbor **Features** to be grouped.
10+
11+
NOTE: This filter is intended for use with *Hexagonal* materials. While the c-axis is actually just referring to the <001> direction and thus will operate on any symmetry, the utility of grouping by <001> alignment is likely only important/useful in materials with anisotropy in that direction (like materials with *Hexagonal* symmetry). Features whose phase resolves to anything other than *Hexagonal_High* are silently left ungrouped.
12+
13+
### Randomization of Parent Ids
14+
15+
By default the filter assigns parent ids deterministically in the order features are picked as BFS seeds, so identical inputs produce identical parent ids. Set **Randomize Parent Ids** to true to randomly permute the assigned parent ids (useful when feeding the output straight into a color-mapped visualization where adjacent groups should not share the same color by accident). For reproducible randomization, enable **Use Seed for Random Generation** and supply a **Seed** value; the seed actually used is also written to a top-level array (default name `_Group_MicroTexture_Regions_Seed_Value_`) so the run can be replayed.
16+
17+
% Auto generated parameter table will be inserted here
18+
19+
## References
20+
21+
## Example Pipelines
22+
23+
## License & Copyright
24+
25+
Please see the description file distributed with this **Plugin**
26+
27+
## DREAM3D-NX Help
28+
29+
If you need help, need to file a bug report or want to request a new feature, please head over to the [DREAM3DNX-Issues](https://github.com/BlueQuartzSoftware/DREAM3DNX-Issues/discussions) GitHub site where the community of DREAM3D-NX users can help answer your questions.
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
#include "GroupMicroTextureRegions.hpp"
2+
3+
#include "simplnx/Common/Constants.hpp"
4+
#include "simplnx/DataStructure/DataArray.hpp"
5+
#include "simplnx/DataStructure/NeighborList.hpp"
6+
#include "simplnx/Utilities/Math/GeometryMath.hpp"
7+
#include "simplnx/Utilities/MessageHelper.hpp"
8+
9+
#include "EbsdLib/LaueOps/LaueOps.h"
10+
11+
#include <random>
12+
13+
using namespace nx::core;
14+
15+
// -----------------------------------------------------------------------------
16+
GroupMicroTextureRegions::GroupMicroTextureRegions(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel,
17+
GroupMicroTextureRegionsInputValues* inputValues)
18+
: m_DataStructure(dataStructure)
19+
, m_InputValues(inputValues)
20+
, m_ShouldCancel(shouldCancel)
21+
, m_MessageHandler(mesgHandler)
22+
, m_FeaturePhases(m_DataStructure.getDataRefAs<Int32Array>(m_InputValues->FeaturePhasesArrayPath))
23+
, m_FeatureParentIds(m_DataStructure.getDataRefAs<Int32Array>(m_InputValues->FeatureParentIdsArrayName))
24+
, m_CrystalStructures(m_DataStructure.getDataRefAs<UInt32Array>(m_InputValues->CrystalStructuresArrayPath))
25+
, m_AvgQuats(m_DataStructure.getDataRefAs<Float32Array>(m_InputValues->AvgQuatsArrayPath))
26+
, m_Volumes(m_DataStructure.getDataRefAs<Float32Array>(m_InputValues->VolumesArrayPath))
27+
{
28+
}
29+
30+
// -----------------------------------------------------------------------------
31+
GroupMicroTextureRegions::~GroupMicroTextureRegions() noexcept = default;
32+
33+
// -----------------------------------------------------------------------------
34+
const std::atomic_bool& GroupMicroTextureRegions::getCancel()
35+
{
36+
return m_ShouldCancel;
37+
}
38+
39+
// -----------------------------------------------------------------------------
40+
void GroupMicroTextureRegions::randomizeParentIds(usize totalPoints, usize totalParentIds)
41+
{
42+
// Shuffle parent IDs in [1, totalParentIds-1] via Fisher-Yates with the same
43+
// RNG state already seeded by operator(). Parent ID 0 is reserved (unassigned)
44+
// and is excluded from the shuffle so cells with no parent stay at 0.
45+
auto& cellParentIds = m_DataStructure.getDataRefAs<Int32Array>(m_InputValues->CellParentIdsArrayName);
46+
47+
std::vector<int32> shuffle(totalParentIds);
48+
for(usize i = 0; i < totalParentIds; i++)
49+
{
50+
shuffle[i] = static_cast<int32>(i);
51+
}
52+
53+
std::uniform_int_distribution<usize> intDist(1, totalParentIds - 1);
54+
for(usize i = 1; i < totalParentIds; i++)
55+
{
56+
usize r = intDist(m_Generator);
57+
std::swap(shuffle[i], shuffle[r]);
58+
}
59+
60+
// Remap feature parent IDs first so cell parent IDs can index through the new mapping
61+
const usize numFeatures = m_FeatureParentIds.getNumberOfTuples();
62+
for(usize f = 0; f < numFeatures; f++)
63+
{
64+
const int32 oldId = m_FeatureParentIds[f];
65+
if(oldId >= 0 && static_cast<usize>(oldId) < totalParentIds)
66+
{
67+
m_FeatureParentIds[f] = shuffle[oldId];
68+
}
69+
}
70+
71+
// Re-derive cell parent IDs from the shuffled feature parent IDs
72+
auto& featureIds = m_DataStructure.getDataRefAs<Int32Array>(m_InputValues->FeatureIdsArrayPath);
73+
for(usize k = 0; k < totalPoints; k++)
74+
{
75+
cellParentIds[k] = m_FeatureParentIds[featureIds[k]];
76+
}
77+
}
78+
79+
// -----------------------------------------------------------------------------
80+
Result<> GroupMicroTextureRegions::execute()
81+
{
82+
MessageHelper messageHelper(m_MessageHandler);
83+
ThrottledMessenger throttledMessenger = messageHelper.createThrottledMessenger();
84+
85+
NeighborList<int32>& featureNeighborListRef = m_DataStructure.getDataRefAs<NeighborList<int32>>(m_InputValues->ContiguousNeighborListArrayPath);
86+
NeighborList<int32>* nonContigNeighListPtr = nullptr;
87+
if(m_InputValues->UseNonContiguousNeighbors)
88+
{
89+
nonContigNeighListPtr = m_DataStructure.getDataAs<NeighborList<int32>>(m_InputValues->NonContiguousNeighborListArrayPath);
90+
if(nullptr == nonContigNeighListPtr)
91+
{
92+
return MakeErrorResult(-99345, "There was an error getting the Non-contiguous neighborlist from the DataStructure");
93+
}
94+
}
95+
96+
std::vector<int32> groupList;
97+
98+
int32 parentCount = 0;
99+
int32 featureSeed = 0;
100+
int32 list1size = 0;
101+
int32 list2size = 0;
102+
103+
while(featureSeed >= 0)
104+
{
105+
parentCount++;
106+
featureSeed = getSeed(parentCount);
107+
if(featureSeed < 0)
108+
{
109+
continue;
110+
}
111+
112+
groupList.clear();
113+
groupList.push_back(featureSeed);
114+
for(std::vector<int32>::size_type j = 0; j < groupList.size(); j++)
115+
{
116+
const int32 firstFeature = groupList[j];
117+
list1size = static_cast<int32>(featureNeighborListRef[firstFeature].size());
118+
if(m_InputValues->UseNonContiguousNeighbors)
119+
{
120+
list2size = nonContigNeighListPtr->getListSize(firstFeature);
121+
}
122+
// Walk contiguous neighbors (k=0) then optional non-contiguous neighbors (k=1)
123+
for(int32 k = 0; k < 2; k++)
124+
{
125+
const int32 listSize = (k == 0) ? list1size : list2size;
126+
for(int32 l = 0; l < listSize; l++)
127+
{
128+
int32 neigh = -1;
129+
if(k == 0)
130+
{
131+
neigh = featureNeighborListRef[firstFeature][l];
132+
}
133+
else if(k == 1 && m_InputValues->UseNonContiguousNeighbors)
134+
{
135+
bool ok = false;
136+
neigh = nonContigNeighListPtr->getValue(firstFeature, l, ok);
137+
}
138+
if(neigh >= 0 && neigh != firstFeature)
139+
{
140+
if(determineGrouping(firstFeature, neigh, parentCount))
141+
{
142+
groupList.push_back(neigh);
143+
}
144+
}
145+
}
146+
}
147+
}
148+
149+
throttledMessenger.sendThrottledMessage([&]() { return fmt::format("Parent Count: {}", parentCount); });
150+
}
151+
return {};
152+
}
153+
154+
// -----------------------------------------------------------------------------
155+
Result<> GroupMicroTextureRegions::operator()()
156+
{
157+
MessageHelper messageHelper(m_MessageHandler);
158+
159+
m_Generator = std::mt19937_64(m_InputValues->SeedValue);
160+
m_Distribution = std::uniform_real_distribution<float32>(0.0f, 1.0f);
161+
162+
// Initialize Data
163+
m_AvgCAxes[0] = 0.0f;
164+
m_AvgCAxes[1] = 0.0f;
165+
m_AvgCAxes[2] = 0.0f;
166+
m_FeatureParentIds.fill(-1);
167+
168+
// Execute the main grouping algorithm
169+
messageHelper.sendMessage(fmt::format("Start Grouping....."));
170+
171+
// Execute the grouping algorithm
172+
Result<> result = execute();
173+
if(result.invalid())
174+
{
175+
return result;
176+
}
177+
178+
// handle active array resize
179+
if(m_NumTuples < 2)
180+
{
181+
return MakeErrorResult(-87000, fmt::format("The number of grouped Features was {} which means no grouped features were detected. A grouping value may be set too high", m_NumTuples));
182+
}
183+
m_DataStructure.getDataRefAs<AttributeMatrix>(m_InputValues->NewCellFeatureAttributeMatrixName).resizeTuples(ShapeType{m_NumTuples});
184+
185+
auto& cellParentIds = m_DataStructure.getDataRefAs<Int32Array>(m_InputValues->CellParentIdsArrayName);
186+
auto& featureIds = m_DataStructure.getDataRefAs<Int32Array>(m_InputValues->FeatureIdsArrayPath);
187+
const usize totalPoints = featureIds.getNumberOfTuples();
188+
for(usize k = 0; k < totalPoints; k++)
189+
{
190+
cellParentIds[k] = m_FeatureParentIds[featureIds[k]];
191+
}
192+
193+
if(m_InputValues->RandomizeParentIds)
194+
{
195+
messageHelper.sendMessage(fmt::format("Randomizing Parent Ids"));
196+
randomizeParentIds(totalPoints, m_NumTuples);
197+
}
198+
199+
return {};
200+
}
201+
202+
// -----------------------------------------------------------------------------
203+
int GroupMicroTextureRegions::getSeed(int32 newFid)
204+
{
205+
usize numFeatures = m_FeaturePhases.getNumberOfTuples();
206+
207+
int32 featureIdSeed = -1;
208+
209+
// Precalculate some constants
210+
const int32 totalFMinus1 = static_cast<int32>(numFeatures) - 1;
211+
212+
usize counter = 0;
213+
// This section finds a feature id that has not been grouped yet. It starts by
214+
// randomly selecting a feature id between 0 and numFeatures-1. We then start
215+
// looping. If the initial random value is valid then we exit the loop after
216+
// a single iteration. If that feature has already been grouped, then we add one
217+
// to the `randFeature` value and try again. If we get to the end of the range of
218+
// featureIds then the algorithm will loop back to featureId = 0 and start incrementing
219+
// from there. This is reasonably efficient as we only generate random numbers
220+
// as needed.
221+
auto randFeature = static_cast<int32>(m_Distribution(m_Generator) * static_cast<float32>(totalFMinus1));
222+
while(featureIdSeed == -1 && counter < numFeatures)
223+
{
224+
if(randFeature > totalFMinus1)
225+
{
226+
randFeature = randFeature - numFeatures;
227+
}
228+
if(m_FeatureParentIds.getValue(randFeature) == -1)
229+
{
230+
featureIdSeed = randFeature;
231+
}
232+
randFeature++;
233+
counter++;
234+
}
235+
236+
// // Used for debugging and demonstration
237+
// if(newFid == 1)
238+
// {
239+
// auto& centroids = m_DataStructure.getDataRefAs<Float32Array>(m_InputValues->VolumesArrayPath.replaceName("Centroids"));
240+
// std::ofstream fout ("/tmp/GroupMicroTextureInitialVoxelSeeds.txt", std::ios_base::out | std::ios_base::app);
241+
// fout << fmt::format("Feature Parent Id: {} | X: {}, Y: {}\n", voxelSeed, centroids.getComponent(voxelSeed, 0), centroids.getComponent(voxelSeed, 1));
242+
// }
243+
244+
if(featureIdSeed >= 0)
245+
{
246+
m_FeatureParentIds[featureIdSeed] = newFid;
247+
m_NumTuples = newFid + 1;
248+
249+
if(m_InputValues->UseRunningAverage)
250+
{
251+
usize index = featureIdSeed * 4;
252+
// Get the orientation matrix (which is passive) and then transpose it to make it active transform
253+
ebsdlib::Matrix3X3F g1t = ebsdlib::Quaternion<float32>(m_AvgQuats.getValue(index + 0), m_AvgQuats.getValue(index + 1), m_AvgQuats.getValue(index + 2), m_AvgQuats.getValue(index + 3))
254+
.toOrientationMatrix()
255+
.toGMatrix()
256+
.transpose();
257+
ebsdlib::Matrix3X1F cAxis(0.0f, 0.0f, 1.0f);
258+
// normalize so that the dot product can be taken below without
259+
// dividing by the magnitudes (they would be 1)
260+
const ebsdlib::Matrix3X1F c1 = (g1t * cAxis).normalize();
261+
262+
m_AvgCAxes = c1 * m_Volumes.getValue(featureIdSeed);
263+
}
264+
}
265+
266+
return featureIdSeed;
267+
}
268+
269+
// -----------------------------------------------------------------------------
270+
bool GroupMicroTextureRegions::determineGrouping(int32 referenceFeature, int32 neighborFeature, int32 newFid)
271+
{
272+
const int32 neighborParentId = m_FeatureParentIds.getValue(neighborFeature);
273+
const int32 referenceFeaturePhase = m_FeaturePhases.getValue(referenceFeature);
274+
const int32 neighborFeaturePhase = m_FeaturePhases.getValue(neighborFeature);
275+
276+
if(neighborParentId == -1 && referenceFeaturePhase > 0 && neighborFeaturePhase > 0)
277+
{
278+
ebsdlib::Matrix3X1F c1 = {0.0f, 0.0f, 0.0f};
279+
ebsdlib::Matrix3X1F cAxis(0.0f, 0.0f, 1.0f);
280+
281+
if(!m_InputValues->UseRunningAverage)
282+
{
283+
const usize index = referenceFeature * 4;
284+
// Get the orientation matrix (which is passive) and then transpose it to make it active transform
285+
// transpose the g matrix so when c-axis is multiplied by it,
286+
// it will give the sample direction that the c-axis is along
287+
ebsdlib::Matrix3X3F g1t = ebsdlib::Quaternion<float32>(m_AvgQuats.getValue(index + 0), m_AvgQuats.getValue(index + 1), m_AvgQuats.getValue(index + 2), m_AvgQuats.getValue(index + 3))
288+
.toOrientationMatrix()
289+
.toGMatrix()
290+
.transpose();
291+
c1 = (g1t * cAxis).normalize();
292+
}
293+
uint32 phase1 = m_CrystalStructures.getValue(referenceFeaturePhase);
294+
uint32 phase2 = m_CrystalStructures.getValue(neighborFeaturePhase);
295+
if(phase1 == phase2 && (phase1 == ebsdlib::CrystalStructure::Hexagonal_High))
296+
{
297+
const usize index = neighborFeature * 4;
298+
// Get the orientation matrix (which is passive) and then transpose it to make it active transform
299+
// transpose the g matrix so when c-axis is multiplied by it,
300+
// it will give the sample direction that the c-axis is along
301+
ebsdlib::Matrix3X3F g2t = ebsdlib::Quaternion<float32>(m_AvgQuats.getValue(index + 0), m_AvgQuats.getValue(index + 1), m_AvgQuats.getValue(index + 2), m_AvgQuats.getValue(index + 3))
302+
.toOrientationMatrix()
303+
.toGMatrix()
304+
.transpose();
305+
ebsdlib::Matrix3X1F c2 = (g2t * cAxis).normalize();
306+
307+
float32 w;
308+
if(m_InputValues->UseRunningAverage)
309+
{
310+
w = m_AvgCAxes.cosTheta(c2);
311+
}
312+
else
313+
{
314+
w = c1.cosTheta(c2);
315+
}
316+
w = std::acos(std::clamp(w, -1.0f, 1.0f));
317+
318+
// Convert user defined tolerance to radians.
319+
float32 cAxisToleranceRad = m_InputValues->CAxisTolerance * nx::core::Constants::k_PiF / 180.0f;
320+
if(w <= cAxisToleranceRad || (nx::core::Constants::k_PiD - w) <= cAxisToleranceRad)
321+
{
322+
m_FeatureParentIds.setValue(neighborFeature, newFid);
323+
if(m_InputValues->UseRunningAverage)
324+
{
325+
c2 = c2 * m_Volumes.getValue(neighborFeature);
326+
m_AvgCAxes = m_AvgCAxes + c2;
327+
}
328+
return true;
329+
}
330+
}
331+
}
332+
return false;
333+
}

0 commit comments

Comments
 (0)