<jupyter_start><jupyter_text>IntroductionThe goal of this tutorial is to teach users how to import and perform basic levelling of SPM images using Nanoscopy.The first step to working with an SPM image is to import it. Nanoscopy currently relies on other packages to load the SPM image data from files. When the data has been loaded from a file, Nanoscopy stores the data for future use in a special data container, called "SPMImage", which is defined in the Nanoscopy package itself. This data container stores the different data channels contained in the SPM image file, as well as important metadata from the file. Importing dataSPM data importing in Nanoscopy makes use of the "io" module in the "spm" subpackage. This module contains a convenience function which will attempt to load the data from its file based on the file extension as long as the extension is one that is recognized by the importing algorithm. The function is called "read". The read function can take either the path of a file to be loaded or the path to a directory of files to be loaded. Regardless, the read function always returns a list of SPMImage items, with one item per file loaded.Let's load an example data file, which happens to be an STM image produced by Nanonis control hardware (which uses the SXM file extension):<jupyter_code># Specify the file to be imported
filepath_sxm = '../ExampleDataFiles/STM_Au-111_Flat.sxm'

Image = nano.spm.io.read(filepath_sxm)[0]<jupyter_output>Processing ../ExampleDataFiles/STM_Au-111_Flat.sxm...DONE<jupyter_text>In the code above, we included a "[0]" after the read function to select the first (and only) item in the list returned by read. Thus, "Image" is an SPMImage class object rather than a list of SPMImage objects. Accessing DataThe SPMImage class has several properties. The first property "data", which is a dictionary containing the image data channels. The substructure of the data dictionary is:* First level: Dictionary of channel names.  Use channel name string to select channel.  Example: ['Z']* Second level: List of images. Use index to select desired image. Example: [0] * Third level: Numpy array containing the image data for the specified image.Let's show an image of one of the heightmap channels from the imported image:<jupyter_code>plt.imshow(Image.data['Z'][0])<jupyter_output><empty_output><jupyter_text>The SPMImage class also has a "traces" property which contains records of the scan direction (up or down) "trace" direction (forward or backward). This data is structured the same way as the data property, so using the same arguments as for data will give the corresponding scan info for the selected image. See below for an example.Importantly, the SPMImage class also stores any metadata that was detected in the file. There are two ways to access the metadata: in raw form and in a parsed form. The raw metadata, as acquired by the relevant importing function (which are currently provided by external packages), is stored in the "headers" property as a dictionary. There is usually quite a bit of metadata, so we won't show an example of this here.The second way to access metadata is to use the "params" property. This is a dictionary which contains a limited selection of metadata which represents what is likely to be the most useful metadata for analysis. An example is shown below.<jupyter_code>print('Trace info for image shown above: ')
print(Image.traces['Z'][0])

print('\nParsed metadata for the image shown above:')
print(Image.params)<jupyter_output>Trace info for image shown above: 
{'direction': 'down', 'trace': 'forward'}

Parsed metadata for the image shown above:
{'bias': 1.0, 'setpoint_value': 1e-09, 'setpoint_unit': 'A', 'width': 4e-08, 'height': 4e-08, 'channels': ['Current', '(A);Z', '(m)'], 'scan_direction': 'down', 'date_time': datetime.datetime(2021, 2, 25, 11, 36, 19)}<jupyter_text>Performing some basic image levellingIt is very common for SPM images to need some pre-processing before they can be analyzed. One example of such pre-processing is image levelling. This can be useful to remove, for example, a large scale height gradient due to sample tilt for a sample which should be relatively flat. Since image data is stored as Numpy arrays, a user can use any appropriate strategy to accomplish the levelling processes.One way to perform image levelling is to use the SPIEPy package, which contains methods for standard plane levelling and 2D polynomial background subtraction among others. Let's see an example of leveling the above image using a plane fit.<jupyter_code># Define shorthand variable for the first image in the Z channel
im_raw = Image.data['Z'][0]

# Convert Numpy array image to SPIEPy image format.
im = spiepy.Im()
im.data = im_raw

# Use SPIEPy to perform standard plane levelling.
im_planeflat, _ = spiepy.flatten_xy(im)

# Compare the levelled and unlevelled images.
figa , (ax1a , ax2a) =  plt.subplots(1, 2)
ax1a.imshow(im_raw)#, cmap = spiepy.NANOMAP, origin = 'lower')
ax1a.set_title('Original Image')
ax2a.imshow(im_planeflat.data)#, cmap = spiepy.NANOMAP, origin = 'lower')
ax2a.set_title('Plane-Flattened')<jupyter_output><empty_output><jupyter_text>Notice that the regions between the stripes in the image are more uniformly-colored after plane fitting than they were before. This is what we would want to see for this image and it means that the plane flattening was a decent choice for levelling this image.Not all images can be reasonably levelled using a standard plane fit. For example, if there are multiple terraces in the image a plane flattening routine will actually add extra tilt to the image (since the image isn't really a plane). Another example is if there is a very prominent, localized feature in the image (a blob of contaminants, for example). In this case, a standard plane flattening will introduce streaks into the image around the feature. For handling these cases, among others, more advanced techniques are required.Let's see an example of this by loading a new image. The new image is the same material (Au(111)) as the one we loaded previously, but it has multiple terraces. We will also perform a standard plane fit for demonstration purposes.<jupyter_code># Specify the file to be imported
filepath_sxm2 = '../ExampleDataFiles/STM_Au-111_Terraced.sxm'
Image2 = nano.spm.io.read(filepath_sxm2)[0]
im2_raw = Image2.data['Z'][0]

# Convert Numpy array image to SPIEPy image format.
im2 = spiepy.Im()
im2.data = im2_raw

# Use SPIEPy to perform standard plane levelling.
im2_planeflat, _ = spiepy.flatten_xy(im2)

# Compare the levelled and unlevelled images.
figb , (ax1b , ax2b) =  plt.subplots(1, 2)
ax1b.imshow(im2_raw)#, cmap = spiepy.NANOMAP, origin = 'lower')
ax1b.set_title('Original Image')
ax2b.imshow(im2_planeflat.data)#, cmap = spiepy.NANOMAP, origin = 'lower')
ax2b.set_title('Plane-Flattened')<jupyter_output>Processing ../ExampleDataFiles/STM_Au-111_Terraced.sxm...DONE<jupyter_text>Notice that the striped pattern is more visible in the plane-flattened image. Additionally, the color of each terrace is more uniform in the direction parallel to the step edges separating the terraces. However, we expect each terrace to be uniformly colored except for the striped pattern, so a standard plane flattening isn't sufficient.Oftentimes, advanced image levelling requires the use of masks. Masks are arrays of pixels which highlight one or more region in an image for consideration in processing. In the case of image levelling, masked regions can either be ignored by levelling algorithms or the levelling algorithm can be made to level the image using only the masked region. The convention adopted by both SPIEPY and the NanoscoPy package is that masked regions will be ignored by levelling routines.Let's see an example where we mask the lower right portion of the image and then try to plane flatten. This serves as a demonstration only, and it won't give good results.<jupyter_code># Create a blank mask with the same dimensions as the image. Blank arrays contain all zeros.
mask = np.zeros_like(im2_raw)

# Set elements of the mask in the bottom right quadrant to have a value of 1. Pixels with non-zero values are considered masked. 
image_shape = np.shape(im2_raw)
for row in range(int(image_shape[0]/2),image_shape[0]):
    for column in range(int(image_shape[1]/2),image_shape[1]):
        mask[row,column] = 1

# Create a masked version of the original image. This uses masked arrays from Numpy.
masked_image = np.ma.array(im2_raw, mask = mask)

# Perform a plane level while ignoring the masked region.
im2 = spiepy.Im()
im2.data = im2_raw
im2_maskflat, _ = spiepy.flatten_xy(im2, mask)

# Show the images
# Compare the levelled and unlevelled images.
figc , (ax1c , ax2c, ax3c) =  plt.subplots(1, 3)
ax1c.imshow(im2_raw)#, cmap = spiepy.NANOMAP, origin = 'lower')
ax1c.set_title('Original Image')
ax2c.imshow(masked_image)#, cmap = spiepy.NANOMAP, origin = 'lower')
ax2c.set_title('Masked')
ax3c.imshow(im2_maskflat.data)#, cmap = spiepy.NANOMAP, origin = 'lower')
ax3c.set_title('Masked and Plane-Flattened')<jupyter_output><empty_output><jupyter_text>Nanoscopy has several tools to assist in performing image levelling. These tools are found in the "process" module in the "spm" sub-package. These tools are written to make use of functionality from the SPIEPy package. One function in the process module, called terrace_level, can be used to handle images which have multiple flat terraces. The idea is that the algorithm determines what transforms are required to level each terrace individually, then applies the average of the transforms to level the image as a whole. For this to work, a mask indicating the locations of the step edges is needed. The terrace_level function will attempt to locate the steps edges and then level the image (using SPIEPy for both tasks). Let's see it in action.<jupyter_code># Perform terrace leveling using the convenience function.
im2_terraced = nano.spm.process.terrace_level(im2_raw)

# Generate the step edge mask for visualization purposes. This isn't typically required when the terrace_level function is used.
mask_step = nano.spm.process.create_mask(im2_planeflat.data, mask_method = 'step')

# Generate a masked image for visualization purposes.
masked_image = np.ma.array(im2_raw, mask = mask_step)

# Compare the levelled and unlevelled images.
figd , (ax1d , ax2d) =  plt.subplots(1, 2)
ax1d.imshow(masked_image)#, cmap = spiepy.NANOMAP, origin = 'lower')
ax1d.set_title('Original Image - Masked')
ax2d.imshow(im2_terraced)#, cmap = spiepy.NANOMAP, origin = 'lower')
ax2d.set_title('Terrace Levelled')<jupyter_output><empty_output><jupyter_text>Notice that the terrace-levelled image above shows not only the striped features we expect, but each terrace is essentially uniformly-colored (except for the stripes) as we expect. This means that terrace levelling was a decent choice for leveling this image. Bonus: Height HistogramsNanoscopy also has a function to generate a histogram from the pixel values from an image. This function is found in the "analysis" module of the "spm" sub-package. Since we've been dealing with a heightmap image, we can get a height histogram from the terrace-levelled image above. In the histogram below, we see that there are four peaks which are evenly-spaced. This is what we expect to see, since there are four terraces (each peak represents one terrace).<jupyter_code># Calculate histogram data
bin_centers, counts, bin_width = nano.spm.analysis.make_histogram(im2_terraced , num_bins = 100)

# Make a histogram with the data also plotted as a line.
plt.bar(bin_centers, counts , width = bin_width)
plt.plot(bin_centers , counts , 'g')<jupyter_output><empty_output><jupyter_text>Bonus 2: Cell Shading<jupyter_code># Specify the file to be imported
filepath_sxm3 = '../ExampleDataFiles/STM_TaAs2_029.sxm'
Image3 = nano.spm.io.read(filepath_sxm3)[0]
im3_raw = Image3.data['Z'][0]

# Perform a plane level while ignoring the masked region.
im3 = spiepy.Im()
im3.data = im3_raw
im3_flat, _ = spiepy.flatten_xy(im3)

regions, peaks = spiepy.locate_regions(im3_flat, sigma = 2)

figf , (ax1f , ax2f) =  plt.subplots(1, 2)
ax1f.imshow(im3_flat.data)#, cmap = spiepy.NANOMAP, origin = 'lower')
ax1f.set_title('Original Image')
ax2f.imshow(im3_flat.data)#regions.data)#, cmap = spiepy.NANOMAP, origin = 'lower')
ax2f.scatter(peaks[0],peaks[1])
ax2f.set_title('Regions')
print(len(peaks))<jupyter_output>2
{'fraction_non_alphanumeric': 0.0737146065613219, 'fraction_numerical': 0.012432822651800754, 'mean_word_length': 5.157037037037037, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 33, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '418467', 'license': 'MIT', 'n_tokens_mistral': 3771, 'n_tokens_neox': 3589, 'n_words': 1644, 'path': 'Examples/Import and Level Image Using SPMImage Class.ipynb', 'repo_name': 'darianSmalley/NanoscoPy', 'size': '12467'}