EBSD data is measured on a regular grid, so a grain boundary comes out of calcGrains as a staircase of pixel edges. Every segment runs along one of the grid axes, whatever direction the boundary actually has - which means the boundary is too long, its direction is quantized to a few values, and its curvature is meaningless.
smoothBoundary repairs this.
mtexdata csl
[grains, ebsd] = ebsd.calcGrains('minPixel',3);
grainsSmooth = smoothBoundary(grains,5);
plot(grainsSmooth,grainsSmooth.meanOrientation,'micronbar','off')ebsd = EBSD (y↓→x)
Phase Orientations Mineral Color Symmetry Crystal reference frame
0 5 (0.0032%) notIndexed none
-1 154107 (100%) iron LightSkyBlue m-3m
Properties: ci, error, iq
Scan unit : um
X x Y x Z : [0 → 511] x [0 → 300] x [0 → 0]
Normal vector: (0,0,1)
This page is about using it. How it works, and the full list of algorithms it can use, is in Smoothing Algorithms.
What it does
Three things, in this order: the staircase is removed, the boundary is resampled at even spacing, and only then is it smoothed. The first two are not cosmetic - smoothing a staircase directly just makes a finer staircase.
Seen on a few grains, against the measured boundary in grey:
plot(grains.boundary,'linewidth',4,'linecolor','LightGray','micronbar','off')
hold on
plot(grainsSmooth.boundary,'linewidth',2,'linecolor','Fuchsia')
hold off
axis([313 353 140 156])
The staircase overestimates the length of a boundary running at 45 degree by sqrt(2), so the total boundary length drops by something of that order
lenRaw = sum(grains.boundary('indexed').segLength);
lenSmooth = sum(grainsSmooth.boundary('indexed').segLength);
fprintf(['total boundary length: %.0f %s measured, %.0f %s smoothed' ...
' - %.0f%% shorter\n'], lenRaw, grains.scanUnit, lenSmooth, ...
grains.scanUnit, 100*(1-lenSmooth/lenRaw))total boundary length: 19419 um measured, 15648 um smoothed - 19% shorterand the distribution of boundary directions stops being a pair of spikes at 0 and 90 degree
figure
subplot(1,2,1)
histogram(grains.boundary('indexed').direction, ...
'weights',grains.boundary('indexed').segLength,180)
subplot(1,2,2)
histogram(grainsSmooth.boundary('indexed').direction, ...
'weights',grainsSmooth.boundary('indexed').segLength,180)
How much to smooth
The second argument is the number of smoothing iterations. More of it means a smoother boundary, and the scatter around the grid directions keeps falling - but see the warning about shrinkage below before turning it up.
iter = [1 5 10 25];
color = copper(length(iter)+1);
plot(grains.boundary,'linewidth',1,'linecolor','LightGray','micronbar','off')
for i = 1:length(iter)
hold on
plot(smoothBoundary(grains,iter(i)).boundary('i','i'), ...
'linewidth',2,'linecolor',color(i,:))
end
hold off
axis([313 353 140 156])
Which algorithm - the short version
The smoothing step itself can be done in several ways, and the choice is made by passing a boundaryFilter. For practical work there are two that matter.
Use the default if the smoothed boundary is for plotting, or for measuring directions and lengths. It is a Laplacian, it is fast, and it is what smoothBoundary(grains,5) selects without being asked.
Use taubinFilter if grain areas or shape parameters matter. A Laplacian shrinks - every iteration pulls a convex region inwards and nothing bounds how far. Taubin follows each smoothing pass by a slightly larger unshrinking one, which stops the drift.
ref = smoothBoundary(grains,0); % simplified and resampled, not smoothed
A0 = ref.area;
big = A0 > 10*median(grains.boundary.segLength)^2;
A0 = A0(big);
aL = smoothBoundary(grains,25).area; aL = aL(big);
aT = smoothBoundary(grains,taubinFilter(25)).area; aT = aT(big);
fprintf('grain area after 25 iterations\n')
fprintf([' laplaceFilter (default) %+6.2f%% on average,' ...
' %+6.1f%% for the worst grain\n'], ...
100*mean((aL-A0)./A0), 100*min((aL-A0)./A0))
fprintf([' taubinFilter %+6.2f%% on average,' ...
' %+6.1f%% for the worst grain\n'], ...
100*mean((aT-A0)./A0), 100*min((aT-A0)./A0))grain area after 25 iterations
laplaceFilter (default) -2.43% on average, -59.0% for the worst grain
taubinFilter -0.09% on average, -6.9% for the worst grainOn a small grain the difference is visible - the Laplacian in magenta cuts inside the measured staircase, Taubin in blue follows it.
A = grains.area;
[~,id] = min(abs(A - 30));
c = grains(id).centroid;
plot(grains.boundary,'linewidth',4,'linecolor','LightGray','micronbar','off')
hold on
plot(smoothBoundary(grains,25).boundary,'linewidth',2.5,'linecolor','Fuchsia')
plot(smoothBoundary(grains,taubinFilter(25)).boundary, ...
'linewidth',2.5,'linecolor','DodgerBlue')
hold off
axis([c.x-12 c.x+12 c.y-12 c.y+12])
There is a third option worth knowing about. curvatureFilter replaces the iteration count by a length - the wavelength that gets damped to half amplitude. Detail finer than it is removed, detail coarser survives. Since it is a length it means the same thing whatever step size the map was measured at, so results stay comparable between scans.
F = curvatureFilter;
F.smoothingLength = 4; % in the units of the map, here um
plot(grains.boundary,'linewidth',4,'linecolor','LightGray','micronbar','off')
hold on
plot(smoothBoundary(grains,F).boundary,'linewidth',2.5,'linecolor','Orange')
hold off
axis([313 353 140 156])
huberFilter exists for faceted materials - it keeps genuine corners sharp instead of rounding them off. It is not a good default, see Smoothing Algorithms.
When to switch the first two steps off
Removing the staircase and resampling change the number of boundary segments, and a resampled segment no longer runs between one specific pair of pixels. So wherever gB.ebsdId is read per segment - to look up the orientations on either side of a boundary, for instance - both steps have to be off
grainsPlain = smoothBoundary(grains,5,'noSimplify','noRefine');
fprintf(['boundary segments: %d measured, %d after smoothing, ' ...
'%d with both steps off\n'], length(grains.boundary), ...
length(grainsSmooth.boundary), length(grainsPlain.boundary))boundary segments: 21269 measured, 18452 after smoothing, 21269 with both steps offOnly the last keeps one segment per pixel edge, and therefore one valid ebsdId pair per segment.
Triple points
Triple and quadruple points are held fixed, so which grains touch, and where, never changes. Pass 'moveTriplePoints' to let them move as well. Be careful with it: it is what allows a small grain to shrink away.