# Tutorial

This tutorial will run through how to load a FIB/SEM image stack, identify two fiducials, track them from slice to slice to see how they change, and get a measurement of

## Getting Started

1. Download the image files for the tutorial and extract them to a folder. They can be found here (719 MB download).

2. Install the dependencies and the `fibtracking` module following the Installation instructions.

3. Fire up a Python (or Jupyter or Notebook) instance, and import the module (along with `matplotlib` if you are using Jupyter):

       >>> %matplotlib qt4
       >>> from fibtracking import fibtracking as ft

## Loading and Reviewing Images

1. Run the following code to load the images into memory. The `readimages()` method will pop-up a window asking for the directory containing the images. Click OK once you have chosen.

       >>> images = ft.readimages()
           Reading 308 images from C:\tmp\TiffImages:

2. The `fibtracking` module provides a simple interactive image browser using OpenCV tools. It can be invoked using the `image_browser()` method, and controlled using the keyboard shortcuts included in the title bar. Options are provided for labeling the image stack with the image index, as well as making the images smaller for easier viewing.

       >>> ft.image_browser(images,
       ...                  downsize=True,
       ...                  labelimages=True)

3. Similarly, there is an `export_video()` method that will use `OpenCV`‘s video export capability to save the image stack as an animated video. This method is somewhat brittle and the codec support is different on every machine (depending on how OpenCV was compiled), but I’ve been able to get it working on both Windows and Linux systems.

   Call the method giving the image list, a file name, and the frame rate (among some other options, similar to `image_browser()`): If no value is given for `fourcc`, it should open a window asking for which codec to use. On Windows, the Microsoft Video 1 codec seems to work best. On Linux, any supported codec should work.

       >>> fibtracking.export_video(im_list=images,
       ...                          filename='test_video.avi',
       ...                          fps=20,
       ...                          labelimages=False)

   The method will also suggest an `ffmpeg` call that can be used to reduce the resulting file size (for sharing with others). This results in an output like the following:

## Tracking Fiducials

1. Importantly, the module needs to know how big a pixel is in the horizontal dimension. As I have experience with FEI and Tescan FIB/SEM systems, support for reading this value from those files is implemented. For FEI, the value is read directly from the image. For Tescan, it is read from the header file that is saved with every outputted image:

       >>> pixWidth = ft.get_tescan_pixel_width('slice-00001-png.hdr')
       ... # this image is not provided in the dataset, and is just for illustration purposes
       >>> print(pixWidth)
           2.4002e-08

       >>> pixWidth = ft.get_fei_pixel_width('EBeam - SliceImage - 001.tif')
       >>> print(pixWidth)
           2.13166e-08

2. With the pixel width known, we can track fiducials now using the `trackfiducials()` method. See the documentation for details. Most important is the `im_number` parameter. It controls what image is displayed for selecting the fiducial (useful if the fiducials do not appear in the first few slices of your stack).

       >>> widths = ft.trackfiducials(pix_width=pixWidth,
       ...                            im_list=images,
       ...                            im_number=20,
       ...                            plot_fiducials=True
       ...                            fid_num_to_plot=25,
       ...                            per_row=5)

3. This method will prompt for a directory to read if no `im_list` is given. Otherwise, it pops up some instruction boxes that explain what you should do at each step:

   1. The first step is to crop the data to the area to where you expect to see the fiducials. Since the fiducials should always be in the same general location, reducing the search area drastically speeds the fiducial recognition step. Keep in mind that the fiducials usually move upwards over the course of the acquisition, so make sure to select a large enough box to include them all. Press `Enter` after selecting the rectangle:

   2. In the next two dialogs, select a small box around the two fiducials to track.

   3. During operation, a progress bar will be shown to display how long the tracking is expected to take:

          >>> widths = ft.trackfiducials(pix_width=pixWidth,
          ...                            im_list=images,
          ...                            im_number=20,
          ...                            plot_fiducials=True
          ...                            fid_num_to_plot=25,
          ...                            per_row=5)
              Pixel width is 2.13166e-08m
              Box coordinates are(x1, y1), (x2, y2): (470, 356), (1718, 870)
              Tracking fiducials:  41%|████      | 126/308 [00:08<00:12, 30.5 it/s]

   4. If requested, plots showing where the fiducial was located in a range of images will be shown. If any of these are obviously not what you selected as a fiducial, there was some sort of problem with the recognition process and you should probably try again:

## Visualizing Fiducial Tracking Data

1. After running the last step, the `widths` variable should contain a list of distances (in whatever units `pixWidth` was given in) between the two fiducials recognized on each slice. These should get smaller as the list progresses, representing the fiducials getting closer together.

   A helper function is available to plot the this data:

       >>> f = ft.plot_fid_distances(widths)

   As can be observed, the first few slices are not particularly evenly spaced, but the milling reaches an equilibrium point and the profile eventually becomes linear around slice 70, meaning that the slice thicknesses are effectively equal.

   The `plot_fid_distances()` can also take a parameter `plot_diff=True`, which will include a plot of the differences in fiducial widths between each slice. Due to the discrete nature of the data, this plot is not the most useful (and could probably use some improvements). Regardless, the output looks like this:

   >     >>> f = ft.plot_fid_distances(widths, plot_diff=True)

## Calculating Slice Thicknesses

### Global Slice Thickness

1. Oftentimes, rather than an individual thickness value for each slice, the most relevant information is a global average slice thickness, and the range over which it is valid. The `fit_fid_distances()` method will help find this information.

   >     >>> ft.fit_fid_distances(widths,
   >     ...                      start_image=110,
   >     ...                      fid_line_angle=40,
   >     ...                      reject_m=None,
   >     ...                      plot_results=True)
   >         Average fiducial delta_x is -33.39 nm
   >         Fiducial delta_x offset is 18718.21 nm
   >         Average slice thickness is 19.90 nm

   This method will plot the widths (similar to `plot_fid_distances()`), but will also fit a linear function to the data, and calculate an average slice thickness, based off of the `fid_line_angle` value provided. If the `reject_m` parameter is provided, the method will attempt to remove outlier data points (useful if the fiducial recognition was not perfect) before fitting, in order to give a more accurate value. The `start_image` and `end_image` parameters can be used to tailor the range of slices used for fitting (useful for ignoring the first N slices that are not in equilibrium yet). If the `plot_results` flag is set, an output like the following will be produced:

   In this dataset, we called for a nominal slice thickness of 20nm, and we can observe that on average, we were very close to that. In further processing, we can trim the first 75 slices (or so) off of our dataset to ensure we have uniform slice thicknesses, and set the thickness to 19.9nm.

### Individual Slice Thicknesses

1. Although it has not been as useful as the Global Slice Thickness calculation, originally the methods were written to calculate individual slice thicknesses, and then trying to fit that data. As mentioned previously, the discrete nature of the data makes this information not terribly useful, but this is how you’d do it if you desire:

       >>> thicknesses = ft.slice_thickness(widths,
       ...                                  fid_line_angle=40,
       ...                                  input_units='m',
       ...                                  output_units='nm')[0]

   The `slice_thickness()` method returns three values: the list of thicknesses, the mean, and the standard deviation, so we use the `[0]` syntax to extract just the list of thickness values. There is a `plot_thicknesses()` method to smooth this data and plot the results for exploration as well. The method has a great number of options for tailoring the output, and experimenting with these will be left as an exercise to the reader. A simple call:

       >>> ft.plot_thicknesses(thicknesses, legend_loc='upper left')

   Produces output like the following. Included in this plot are some basic statistics (mean and standard deviation), as well as a LOWESS regression that helps show how the slice thickness is changing on average:

   Again, if we crop out the first slices where there was no equilibrium, the results of the fit improve significantly:

       >>> ft.plot_thicknesses(thicknesses[80:], legend_loc='upper left')

## Saving/Importing Results

1. For later or further analysis, the results of these methods can be easily saved to or imported from `.csv` files using the `save_output()` and `import_csv_data()` methods.

   To save the fiducial widths and slice thickness results:

       >>> ft.save_output(widths,
       ...                fname='fiducial_widths.csv',
       ...                collabels=['Slice', 'Fiducial widths (m)'])
           2016-04-06 12:12:04
           Wrote output to fiducial_widths.csv
       >>> ft.save_output(thicknesses,
       ...                fname='thicknesses.csv',
       ...                collabels=['Slice','Thickness (nm)'])
           2016-04-06 12:13:42
           Wrote output to thicknesses.csv

   To import this data back into the program:

       >>> widths = ft.import_csv_data('fiducial_widths.csv')
       >>> thicknesses = ft.import_csv_data('thicknesses.csv')