{ "cells": [ { "cell_type": "markdown", "metadata": { "nbsphinx": "hidden" }, "source": [ "This notebook is part of the `kikuchipy` documentation https://kikuchipy.org.\n", "Links to the documentation won't work from the notebook." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Reference frames\n", "\n", "## Sample-detector geometry\n", "\n", "The figure below shows the [sample reference frame](#detector-sample-geometry)\n", "and the [detector reference frame](#detector-coordinates) used in kikuchipy, all\n", "of which are right handed. In short, the sample reference frame is the one used\n", "by EDAX TSL, RD-TD-ND, while the pattern center is defined as in the Bruker\n", "software." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **(a)** (lower left), a schematic of the microscope chamber shows the\n", "definition of the sample reference frame, RD-TD-ND. The\n", "$x_{euler}-y_{euler}-z_{euler}$ crystal reference frame used by Bruker is shown\n", "for reference. An EBSD pattern on the detector screen is viewed from *behind*\n", "the screen *towards* the sample. The inset **(b)** shows the detector and sample\n", "normals viewed from above, and the azimuthal angle $\\omega$ which is defined as\n", "the sample tilt angle round the RD axis. **(c)** shows how the EBSD map appears\n", "within the data collection software, with the sample reference frame and the\n", "scanning reference frame, $x_{scan}-y_{scan}-z_{scan}$, attached. Note the\n", "$180^{\\circ}$ rotation of the map about ND. **(d)** shows the relationship\n", "between the sample reference frame and the detector reference frame,\n", "$x_{detector}-y_{detector}-z_{detector}$, with the projection center\n", "highlighted. The detector tilt $\\theta$ and sample tilt $\\sigma$, in this case\n", "$10^{\\circ}$ and $70^{\\circ}$, respectively, are also shown." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The above figure shows the EBSD pattern in the\n", "[sample reference frame figure](#detector-sample-geometry) (a) as viewed from\n", "behind the screen towards the sample (left), with the detector reference frame\n", "the same as in (d) with its origin (0, 0) in the upper left pixel. The detector\n", "pixels' gnomonic coordinates can be described with a calibrated projection\n", "center (PC) (right), with the gnomonic reference frame origin (0, 0) in ($PC_x,\n", "PC_y$). The circles indicate the angular distance from the PC in steps of\n", "$10^{\\circ}$." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The EBSD detector\n", "\n", "All relevant parameters for the sample-detector geometry are stored in an\n", "[kikuchipy.detectors.EBSDDetector](../reference.rst#kikuchipy.detectors.EBSDDetector)\n", "instance. Let's first import necessary libraries and a small Nickel EBSD test\n", "data set" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exchange inline for notebook or qt5 (from pyqt) for interactive plotting\n", "%matplotlib inline\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import kikuchipy as kp\n", "\n", "\n", "s = kp.data.nickel_ebsd_small() # Use kp.load(\"data.h5\") to load your own data\n", "s" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Then we can define a detector with the same parameters as the one used to\n", "acquire the small Nickel data set" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "detector = kp.detectors.EBSDDetector(\n", " shape=s.axes_manager.signal_shape[::-1],\n", " pc=[0.421, 0.779, 0.505],\n", " convention=\"tsl\",\n", " px_size=70, # microns\n", " binning=8,\n", " tilt=0,\n", " sample_tilt=70\n", ")\n", "detector" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "detector.pc_tsl()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The projection/pattern center (PC) is stored internally in the Bruker\n", "convention:\n", "- PCx is measured from the left border of the detector in fractions of detector\n", " width.\n", "- PCy is measured from the top border of the detector in fractions of detector\n", " height.\n", "- PCz is the distance from the detector scintillator to the sample divided by\n", " pattern height.\n", "\n", "Above, the PC was passed in the EDAX TSL convention. Passing the PC in the\n", "Bruker, Oxford, or EMsoft v4 or v5 convention is also supported. Likewise, the\n", "PC can be returned in all conventions via\n", "[EBSDDetector.pc_emsoft()](../reference.rst#kikuchipy.detectors.EBSDDetector.pc_emsoft)\n", "and similar. Conversions between conventions are implemented as described in\n", "Jackson et al. (2019). The\n", "unbinned pixel size $\\delta$, binning factor $b$ and number of pixel rows $s_y$\n", "and columns $s_x$ are needed to convert a PC between the EMsoft and Bruker\n", "conventions:\n", "\n", "- EDAX TSL or Oxford to Bruker\n", "\n", "$$\n", "[PC_x, PC_y, PC_z] = [x^*, 1 - y^*, z^*].\n", "$$\n", "\n", "- EMsoft to Bruker, with $v = -1$ for EMsoft v5 and $+1$ for v4\n", "\n", "$$\n", "[PC_x, PC_y, PC_z] = \\left[\n", "\\frac{1}{2} + v\\frac{x_{pc}}{s_x b},\n", "\\frac{1}{2} - \\frac{y_{pc}}{s_y b},\n", "\\frac{L}{s_y \\delta b}\n", "\\right].\n", "$$\n", "\n", "The detector can be plotted to show whether the average PC is placed as\n", "expected using\n", "[EBSDDetector.plot()](../reference.rst#kikuchipy.detectors.EBSDDetector.plot) (see\n", "its docstring for a complete explanation of its parameters)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "nbsphinx-thumbnail": { "tooltip": "Definitions of the sample and projection/pattern center reference frames" }, "tags": [ "nbsphinx-thumbnail" ] }, "outputs": [], "source": [ "detector.plot(pattern=s.inav[0, 0].data)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This will produce a figure similar to the left panel in the\n", "[detector coordinates figure](#detector-coordinates) above, without the arrows\n", "and colored labels.\n", "\n", "Multiple PCs with a 1D or 2D navigation shape can be passed to the `pc`\n", "parameter upon initialization, or can be set directly. This gives the detector\n", "a navigation shape (not to be confused with the detector shape) and a navigation\n", "dimension (maximum of two)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "detector.pc = np.ones([3, 4, 3]) * [0.421, 0.779, 0.505]\n", "detector.navigation_shape" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "detector.navigation_dimension" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "detector.pc = detector.pc[0, 0]\n", "detector.navigation_shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "Note\n", "\n", "The offset and scale of HyperSpy’s `axes_manager` is fixed for a signal,\n", "meaning that we cannot let the PC vary with scan position if we want to\n", "calibrate the EBSD detector via the `axes_manager`. The need for a varying\n", "PC was the main motivation behind the `EBSDDetector` class.\n", " \n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The right panel in the [detector coordinates figure](#detector-coordinates)\n", "above shows the detector plotted in the gnomonic projection using\n", "[EBSDDetector.plot()](../reference.rst#kikuchipy.detectors.EBSDDetector.plot). We\n", "assign 2D gnomonic coordinates ($x_g, y_g$) in a gnomonic projection plane\n", "parallel to the detector screen to a 3D point ($x_d, y_d, z_d$) in the detector\n", "frame as\n", "\n", "$$\n", "x_g = \\frac{x_d}{z_d}, \\qquad y_g = \\frac{y_d}{z_d}.\n", "$$\n", "\n", "The detector bounds and pixel scale in this projection, per navigation point,\n", "are stored with the detector" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "detector.bounds" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "detector.gnomonic_bounds" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "detector.x_range" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "detector.r_max # Largest radial distance to PC" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Projection center calibration" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The gnomonic projection (pattern) center (PC) of an EBSD detector can be\n", "estimated by the \"moving-screen\" technique\n", "Hjelen et al.. The technique relies\n", "on the assumption that the beam normal, shown in the\n", "[top figure (d)](#detector-sample-geometry) above, is normal to the detector\n", "screen as well as the incoming electron beam, and will therefore intersect the\n", "screen at a position independent of the detector distance (DD). To find this\n", "position, we need two EBSD patterns acquired with a stationary beam but with a\n", "known difference $\\Delta z$ in DD, say 5 mm.\n", "\n", "First, the goal is to find the pattern position which does not shift between the\n", "two camera positions, ($PC_x$, $PC_y$). This point can be estimated in fractions\n", "of screen width and height, respectively, by selecting the same pattern features\n", "in both patterns. The two points of each pattern feature can then be used to\n", "form a straight line, and two or more such lines should intersect at ($PC_x$,\n", "$PC_y$).\n", "\n", "Second, the DD ($PC_z$) can be estimated from the same points. After finding\n", "the distances $L_{in}$ and $L_{out}$ between two points (features) in both\n", "patterns (in = operating position, out = 5 mm from operating position), the DD\n", "can be found from the relation\n", "\n", "$$\n", "\\mathrm{DD} = \\frac{\\Delta z}{L_{out}/L_{in} - 1},\n", "$$\n", "\n", "where DD is given in the same unit as the known camera distance difference. If\n", "also the detector pixel size $\\delta$ is known (e.g. 46 mm / 508 px), $PC_z$ can\n", "be given in the fraction of the detector screen height\n", "\n", "$$\n", "PC_z = \\frac{\\mathrm{DD}}{N_r \\delta b},\n", "$$\n", "\n", "where $N_r$ is the number of detector rows and $b$ is the binning factor.\n", "\n", "Let's find an estimate of the PC from two single crystal Silicon EBSD patterns,\n", "which are included in the [kikuchipy.data](../reference.rst#data) module" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s_in = kp.data.silicon_ebsd_moving_screen_in(allow_download=True)\n", "s_in.remove_static_background()\n", "s_in.remove_dynamic_background()\n", "\n", "s_out5mm = kp.data.silicon_ebsd_moving_screen_out5mm(allow_download=True)\n", "s_out5mm.remove_static_background()\n", "s_out5mm.remove_dynamic_background()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As a first approximation, we can find the detector pixel positions of the same\n", "features in both patterns by plotting them and noting the upper right\n", "coordianates provided by Matplotlib when plotting with an interactive backend\n", "(e.g. qt5 or notebook) and hovering over image pixels" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(ncols=2, sharex=True, sharey=True, figsize=(20, 10))\n", "ax[0].imshow(s_in.data, cmap=\"gray\")\n", "_ = ax[1].imshow(s_out5mm.data, cmap=\"gray\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For this example we choose the positions of three zone axes. The PC calibration\n", "is performed by creating an instance of the\n", "[PCCalibrationMovingScreen](../reference.rst#kikuchipy.detectors.PCCalibrationMovingScreen)\n", "class" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cal = kp.detectors.PCCalibrationMovingScreen(\n", " pattern_in=s_in.data,\n", " pattern_out=s_out5mm.data,\n", " points_in=[(109, 131), (390, 139), (246, 232)],\n", " points_out=[(77, 146), (424, 156), (246, 269)],\n", " delta_z=5,\n", " px_size=None, # Default\n", " convention=\"tsl\", # Default\n", ")\n", "cal" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that ($PC_x$, $PC_y$) = (0.5123, 0.8606), while DD = 21.7 mm. To get\n", "$PC_z$ in fractions of detector height, we have to provide the detector pixel\n", "size $\\delta$ upon initialization, or set it directly and recalculate the PC" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cal.px_size = 46 / 508 # mm/px\n", "cal" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can visualize the estimation by using the (opinionated) convenience method\n", "[PCCalibrationMovingScreen.plot()](../reference.rst#kikuchipy.detectors.PCCalibrationMovingScreen.plot)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cal.plot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As expected, the three lines in the right figure meet at a more or less the same\n", "position. We can replot the three images and zoom in on the PC to see how close\n", "they are to each other. We will use two standard deviations of all $PC_x$\n", "estimates as the axis limits (scaled with pattern shape)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# PCy defined from top to bottom, otherwise \"tsl\", defined from bottom to top\n", "cal.convention = \"bruker\"\n", "pcx, pcy, _ = cal.pc\n", "two_std = 2 * np.std(cal.pcx_all, axis=0)\n", "\n", "fig, ax = cal.plot(return_fig_ax=True)\n", "ax[2].set_xlim([cal.ncols * (pcx - two_std), cal.ncols * (pcx + two_std)])\n", "_ = ax[2].set_ylim([cal.nrows * ( pcy - two_std), cal.nrows * (pcy + two_std)])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, we can use this PC estimate along with the orientation of the Si\n", "crystal, as determined by Hough indexing with a commercial software, to see how\n", "good the estimate is, by performing a\n", "[geometrical EBSD simulation](geometrical_ebsd_simulations.ipynb) of positions of\n", "Kikuchi band centres and zone axes from the five $\\{hkl\\}$ families $\\{111\\}$,\n", "$\\{200\\}$, $\\{220\\}$, $\\{222\\}$, and $\\{311\\}$" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from diffsims.crystallography import ReciprocalLatticePoint\n", "from orix import crystal_map, quaternion\n", "\n", "\n", "# Create simulation generator from a detector and crystal phase and orientation\n", "detector = kp.detectors.EBSDDetector(\n", " shape=cal.shape, pc=cal.pc, sample_tilt=70, convention=cal.convention\n", ")\n", "phase = crystal_map.Phase(space_group=227)\n", "r = quaternion.Rotation.from_euler(np.deg2rad([133.3, 88.7, 177.8]))\n", "simgen = kp.generators.EBSDSimulationGenerator(\n", " detector=detector, phase=phase, rotations=r\n", ")\n", "simgen.navigation_shape = s_in.axes_manager.navigation_shape\n", "\n", "# Specify which plane families for which to simulate bands and zone axes\n", "rlp = ReciprocalLatticePoint(\n", " phase=phase, hkl=[[1, 1, 1], [2, 0, 0], [2, 2, 0], [2, 2, 2], [3, 1, 1]]\n", ").symmetrise() # Symmetrise to get all symmetrically equivalent planes\n", "simgeo = simgen.geometrical_simulation(rlp)\n", "\n", "#del s_in.metadata.Markers # Uncomment this if we want to re-add markers\n", "s_in.add_marker(\n", " marker=simgeo.as_markers(),\n", " plot_marker=False,\n", " permanent=True\n", ")\n", "s_in.plot(navigator=None, colorbar=False, axes_off=True, title=\"\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The PC is not perfect, but the estimate might be good enough for a further PC\n", "and/or orientation refinement." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.10" } }, "nbformat": 4, "nbformat_minor": 4 }