TrueEBSD when the map and the images are in different frames edit page

Authors: Vivian Tong; Stefan Olovsjö, Seco Tools AB, R&D Materials and Technology, 737 82 Fagersta, Sweden. Contact: vivian.tong@extern.tu-freiberg.de

Sometimes the EBSD map and the SEM images are not stored the same way round. Rotate the stage between two acquisitions, load a map from software that writes its scan lines in the other direction, or import images taken in a separate session, and the two no longer agree about which way the sample points.

TrueEBSD will not guess at this, because guessing wrong does not fail loudly — the matching still runs, still fits, and hands back a finished-looking result that is quietly nonsense. So it asks, and this page shows how to answer.

The short WC-Co page is the ordinary case, where everything was collected in one session and there is nothing to say. Read that one first; this page is the same workflow with one extra step.

This script needs the TrueEBSD toolbox, which is not part of MTEX and is distributed separately under Apache-2.0: https://github.com/vtvivian/mtex-trueebsd

addpath(genpath('<path to mtex-trueEbsd>'))

The data

The same cut-down WC-Co dataset as the short page: an EBSD map and four SEM images of the same area.

No plotting convention is set here. A convention decides how things are drawn; it has no bearing on how they are stored, and it is the storage that matters when two pictures are compared pixel by pixel.

mtexdata trueEbsdWCCoSmall

img = ebsd.opt.trueEbsdImgs;
img.fsdB3  = rescale(imboxfilt(img.fsdB3,3));
img.fsdT3  = rescale(imboxfilt(img.fsdT3,3));
img.fsdT1  = rescale(imboxfilt(img.fsdT1,3));
img.fsdT10 = rescale(imboxfilt(img.fsdT10,3));
ebsd = EBSDsquare (y↓→x)
 
 Phase  Orientations     Mineral         Color  Symmetry  Crystal reference frame
     0    89 (0.72%)  notIndexed          none                                   
     1   11180 (91%)         W C  LightSkyBlue      -6m2        X||a*, Y||b, Z||c
     2    991 (8.1%)      Co-fcc  DarkSeaGreen      m-3m                         
     3    28 (0.23%)      Co-hcp     Goldenrod     6/mmm        X||a*, Y||b, Z||c
 
 Properties: bc, bs, bands, MAD, quality, oldId
 Scan unit : um
 X x Y x Z : [10 → 30] x [8 → 23] x [0 → 0]
 Normal vector: (0,0,1)
 Square grid  :96 x 128
 source: trueEbsdWCCo, centre half of the field of view, coarsened 4x
 madeBy: trueEBSD tools/makeWCCoSmall.m

Turn the map, to have something to fix

To get a map and images that disagree, turn the map through 90°. This is what a stage rotation between acquisitions would leave you with: the map now shows the specimen at right angles to what the detectors saw.

rot90z = rotation.byAxisAngle(zvector,90*degree);
ebsdRot = gridify(rotate(ebsd,rot90z),'force');

Build the sequence

Exactly as on the short page. Nothing here mentions the rotation — as far as this code knows, these are just five pictures of one sample.

imgList = createArray(5,1,'distortedImg');
imgList(1) = distortedImg('bc','shift-drift', ebsdRot, 'name','bcImg', ...
  'highContrast',1, 'edgePadWidth',3);
imgList(2) = distortedImg(img.fsdB3, 'true',  'dxy', img.pixSzImg, 'name','fsdB3',  'highContrast',1, 'edgePadWidth',5);
imgList(3) = distortedImg(img.fsdT3, 'shift', 'dxy', img.pixSzImg, 'name','fsdT3',  'highContrast',1, 'edgePadWidth',5);
imgList(4) = distortedImg(img.fsdT1, 'tilt',  'dxy', img.pixSzImg, 'name','fsdT1',  'highContrast',1, 'edgePadWidth',5);
imgList(5) = distortedImg(img.fsdT10,'true',  'dxy', img.pixSzImg, 'name','fsdT10', 'highContrast',1, 'edgePadWidth',3);

Plotting the sequence shows the problem at once: the EBSD map is on its side relative to the four images.

plot(imgList)

Fix it by telling TrueEBSD how the map is oriented. Each entry is drawn the way its own frame says it should be, so changing the map's frame changes what you see — turn it until the plots agree with each other. Here the map needs 'x↑→y'; the images keep the default.

This is the part only you can supply. It is a fact about how the data was collected, and no amount of looking at the numbers will recover it.

ebsdFrame = imgList(1).frame;
ebsdFrame.how2plot = 'x↑→y';
plot(imgList)

All five now agree on screen, which is the assertion TrueEBSD needs. 'byScreenAlignment' means exactly that: I have plotted these and they are the same way up. transformReferenceFrame takes that statement and restates all five in one frame, so they can be compared.

imgList = imgList.transformReferenceFrame(ebsdFrame, 'byScreenAlignment')
imgList = distortedImg (x↑→y)
 
            image    pixel  frame   distortion  contrast                 EBSD
 1       96 x 128  0.16 µm   x↑→y  shift-drift      high  W C, Co-fcc, Co-hcp
 2  192 x 256 x 3  0.08 µm   x↑→y         true      high                    -
 3      192 x 256  0.08 µm   x↑→y        shift      high                    -
 4      192 x 256  0.08 µm   x↑→y         tilt      high                    -
 5      192 x 256  0.08 µm   x↑→y         true      high                    -

Run the workflow

From here nothing is different. The sequence is in one frame, so the rest is the ordinary workflow — see the short page for what each step does.

job = trueEbsd(imgList);
job.pixelSizeMatch(0)

nRows = size(job.resizedList(1).img,1);
nCols = size(job.resizedList(1).img,2);

for n = 1:numel(job.resizedList)
  for m = 1:numel(job.resizedList(n).setXCF)
    job.resizedList(n).setXCF(m).ROISize  = 64;
    job.resizedList(n).setXCF(m).NumROI.x = 16;
    job.resizedList(n).setXCF(m).NumROI.y = round(16 * nRows/nCols);
  end
end

job.resizedList(5).setXCF(1).ROISize = 0;
for n = 3:5
  job.resizedList(n).setXCF(1).xcfImg = 'img';
end

job.calcDistortion('fitErr')
using default pixel size of 0.079501 um, minimum from imgList
 
ans = trueEbsd (pixel size matched)
 
            image   distortion  shift  residual
 1      192 x 256  shift-drift      -         -
 2  192 x 256 x 3         true      -         -
 3      192 x 256        shift      -         -
 4      192 x 256         tilt      -         -
 5      192 x 256         true      -         -
 
 common grid: 192 x 256 at 0.08 µm
 

 fitting the distortion across 5 maps, 4 hops

  hop   distortion    model               ROI      shift   residual
  2→1   shift-drift   poly11            64 px    3.73 px
                      linearinterp      64 px    1.83 px
                      after fitting                         1.61 px
  3→2   true          not correlated        -    0.00 px
                      not fitted                            1.52 px
  4→3   shift         poly11            64 px    3.18 px
Warning: Iteration limit reached for robust fitting. 
                      after fitting                         0.33 px
  5→4   tilt          projective        64 px    3.63 px
                      poly11            64 px    0.44 px
                      poly22            64 px    0.31 px
                      after fitting                         0.31 px

 
ans = trueEbsd (shifts calculated)
 
            image   distortion    shift  residual
 1      192 x 256  shift-drift     1 px    1.6 px
 2  192 x 256 x 3         true     0 px    1.5 px
 3      192 x 256        shift   3.2 px   0.33 px
 4      192 x 256         tilt  0.34 px   0.31 px
 5      192 x 256         true        -         -
 
 common grid: 192 x 256 at 0.08 µm
job.undistort

plot(job.undistortedList,'TrueEBSD image sequence after alignment')
ans = trueEbsd (undistorted)
 
            image   distortion    shift  residual
 1      192 x 256  shift-drift     1 px    1.6 px
 2  192 x 256 x 3         true     0 px    1.5 px
 3      192 x 256        shift   3.2 px   0.33 px
 4      192 x 256         tilt  0.34 px   0.31 px
 5      192 x 256         true        -         -
 
 common grid: 192 x 256 at 0.08 µm

Check the result

The aligned images are properties of the map, exactly as on the short page.

The map and the images come out the same way round and the same shape. That is the point of having said which frame everything is in: gridify takes a layout, so the map is rebuilt in the frame you asked for rather than in MTEX's standard one, and ebsd.bc can then be put beside an aligned image pixel for pixel with nothing in between.

ebsdOut = job.undistortedList(1).ebsd;

fprintf('aligned image: %d x %d\n', size(job.undistortedList(1).img,1), size(job.undistortedList(1).img,2));
fprintf('EBSD object  : %d x %d\n', size(ebsdOut,1), size(ebsdOut,2));

figure
nextAxis
plot(ebsdOut('W C'), ebsdOut('W C').orientations, 'coordinates','on')
title('Undistorted MTEX EBSD map (WC IPF out of screen)','Color','k')

nextAxis
im = ebsdOut.fsdT1;
if size(im,3) > 1, im = mean(im,3); end
plot(ebsdOut, im, 'coordinates','on')
mtexColorMap gray
title('Undistorted fsdT1','Color','k')
aligned image: 192 x 256
EBSD object  : 192 x 256

The real test is whether the alignment worked. Compare the map's band contrast against the image aligned to it — they should track each other closely, and they do, just as well as on the short page where no rotation was involved. Saying which way round the map was cost nothing in quality; it only made an otherwise impossible case possible.

a = double(ebsdOut.bc);
b = double(job.undistortedList(1).img);
k = isfinite(a) & isfinite(b) & a > 0;
r = corrcoef(a(k),b(k));
fprintf('corr(ebsd.bc, aligned image) = %.4f\n', r(1,2));
corr(ebsd.bc, aligned image) = 0.9309

Finish

Everything this page added to the short workflow was two lines: turning the map's frame until the plots agreed, and one call to transformReferenceFrame to say so. The sequence, the distortions, the box settings and the result are identical.

If your map and images were collected the same way up you need none of it. Reach for this when the plots disagree, or when building the job stops with trueEbsd:frameMismatch — which is TrueEBSD telling you it has spotted the problem rather than guessing past it.