In-situ 4D STEM maximum spot mapping
Python Script
Produce a series of colorful maps of crystalline regions from an in-situ 4D STEM dataset open in DigitalMicrograph. This uses a simple algorithm that finds the location and intensity of the maximum pixel in each pattern. The resulting in-situ video of color maps automatically syncs with the original data in DigitalMicrograph. While the results look like orientation maps, the script does not perform proper orientation mapping.
Preview
'''
This script produces maps of crystalline regions from an in-situ 4D STEM dataset in DigitalMicrograph
Run the script with the in-situ 4D STEM dataset window front-most in the software
Then place a picker-tool on the 4D dataset so a diffration pattern is shown.
Next place a circular ROI around the region in the center that should be
ignored when finding the brightest pixel in each diffraction pattern.
The ROI must be circular, not oval, or an error will be returned.
This requires a module FFTArrayAnalysis, which requires scipy, skimage, matplotlib, and tqdm
The module is part of a package which can be installed with pip:
pip install BenMillerScripts
Code written by Ben Miller. Last Updated July 2024
'''
import os, sys
from datetime import datetime
start_time = datetime.now()
import tkinter as tk
from tkinter import *
sys.argv.extend(['-a', ' '])
import tkinter.filedialog as tkfd
import numpy as np
from benmillerscripts import FFTArrayAnalysis as FAA
from pathlib import PureWindowsPath
import scipy
import tqdm
#XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
#User-Set Parameters
FAA.maskC_width = 0 #Pixels to mask on the center vertical and horizontal
map_var = 'theta' #Variable to Map in the colormap ('theta' or 'radius')
#theta maps the angle of the brightest FFT spot
#radius maps the spacing of the brightest FFT spot
scale = 1 #Optional Scaling factor (should be a positive integer)
#This scales the data up in real space, but down in diffraction space
FAA.pre_filter = False #Optionally filter the 4DSTEM dataset with a median filter
gaussian_blur = True #Optionally filter the 4DSTEM dataset with a gaussian blur
FAA.median_size = (1,1,3,3)
showcube = False #Optionally show the datacube that is analyzed (after cropping, scaling, and masking)
show_grey_maps = False #Optionally show the 2 maps that are used to generate the color map
subtract_mean_bg = False #Optionally subtract the mean diffraction pattern from every pattern
draw_color_scale = True #Optionally produce a circular color scale
find_RGB_scale_max = False #Find the approximate max over the whole dataset (this will process a subset of the frames)
RGB_scale_max = 535000 #The maximum found using the find_RGB_scale_max option
numframes = None #number of in-situ frames to process. Set to None to get the total number from the director image and process the whole dataset
step = 1 # set to >1 to process only a fraction of the frames
GUI_Progress_Bar = True
#XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
#Use TQDM??
if GUI_Progress_Bar:
from tqdm.gui import tqdm
import matplotlib.pyplot as plt
#If we want to find a reasonable RGB scale maximum to use for this script, we can just process a subset of the data
if find_RGB_scale_max:
step = step*5
RGB_scale_max = None
if not DM.OkCancelDialog("Finding Colorscale Max is on (find_RGB_scale_max = True)... the script will only process 1 out of "+str(step)+" frames."): exit()
def GetSelectedFiles():
"""
Function to Get List of File Locations for Files Selected with a File Dialog
Returns:
listOfFiles (list of strings) list of files selected by user
"""
sys.argv.extend(['-a', ' '])
root = tk.Tk()
root.withdraw() #use to hide tkinter window
currdir = os.getcwd()
listOfFiles = list(tkfd.askopenfilenames(parent=root,title='Choose DM4 File'))
if len(listOfFiles)>0:
os.chdir(os.path.dirname(listOfFiles[0]))
root.destroy()
return listOfFiles
def GetNthFrame(director_image_filepath,director_image=None, N=0):
"""
Function to get the Nth frame from an in-situ dataset saved with a
director-image and raw-file(s) format.
"""
global j
try:
raw_filepath = director_image_filepath[:-4]+".raw"
raw_length = os.path.getsize(raw_filepath)
if j != 0:
raw_filepath = os.path.splitext(raw_filepath)[0]+".raw_"+str(j)
except:
print("No raw file found for: " +director_image_filepath)
if director_image is None:
director_image = DM.OpenImage(director_image_filepath)
director_data = director_image.GetNumArray()
data_type = director_data.dtype
pixels_per_frame = np.abs(np.prod(director_data.shape,dtype='uint'))
bytes_per_frame = director_data.nbytes
read_start = N*bytes_per_frame - j*raw_length
if read_start + bytes_per_frame > raw_length:
j=j+1
count_1 = int((raw_length-read_start)/bytes_per_frame*pixels_per_frame)
count_2 = pixels_per_frame-count_1
array_1 = np.fromfile(raw_filepath, dtype=data_type, count=count_1, sep='', offset=read_start)
raw_filepath = os.path.splitext(raw_filepath)[0]+".raw_"+str(j)
array_2 = np.fromfile(raw_filepath, dtype=data_type, count=count_2, sep='', offset=0)
array = np.concatenate([array_1,array_2])
else:
array = np.fromfile(raw_filepath, dtype=data_type, count=pixels_per_frame, sep='', offset=read_start)
array = array.reshape(director_data.shape)
return array
def get_pickerROI_num():
'''
Function to get a picker-tool index number from the front-most data in GMS
returns: ROI_num integer index for an region of interest (ROI) in a GMS image
'''
#DM Script Embedded in this Python Code
#The script gets the picker tool number from the front-most image
# and puts it in a global tag that Python can access
dm='imagedocument imdoc = GetFrontImageDocument()' +'\n'
dm+='imageDisplay disp = imdoc.ImageDocumentGetImageModeDisplay()' +'\n'
dm+='number nR = disp.ImageDisplayCountROIs()' +'\n'
dm+='if(nR==0) Result("No Picker Tool Present on 4D STEM Dataset")' +'\n'
dm+='if(nR>1) Result("Warning, Multiple Picker Tools Present on 4D STEM Dataset, Using Number "+disp.ImageDisplayGetROI(0).ROIGetLabel())' +'\n'
dm+='number ROI_num = disp.ImageDisplayGetROI(0).ROIGetLabel().val()' +'\n'
dm+='GetPersistentTagGroup().TagGroupSetTagAsLong("Python_temp:out:ROInum",ROI_num)' +'\n'
#Run DM Script
try: DM.ExecuteScriptString(dm)
except: print("No Picker Tool Found on Front-Most Image"); exit()
#Get # from Tags
(b,ROI_num) = DM.GetPersistentTagGroup().GetTagAsUInt32('Python_temp:out:ROInum')
return ROI_num
def get_picker_data_and_ROI(image):
'''
Function to get a diffraction pattern from the linked picker-tool data and a rectangular ROI within it
Accepts: image the original DM image containing a 4DSTEM dataset- this should have a picker-tool placed on it, be front-most in GMS,
returns: (top,left,bottom,right) the coordinates (in pixels) of the rectangular ROI position
diff_data numpy array with the picker tool data (a diffraction pattern)
'''
#make sure this image is front-most
image.ShowImage()
#check that this is a 4D dataset
if image.GetNumDimensions() != 4: DM.OkDialog("Front-Most Image is not a 4D Dataset...\n\nAborting Script") ; exit()
#Get picker tool diffraction data
Diff_Name = "("+str(get_pickerROI_num())+") Diffraction of "+image.GetName()
diff_img = DM.FindImageByName( Diff_Name )
diff_data = diff_img.GetNumArray()
sy,sx = diff_data.shape
if ( diff_img is None ):
print( 'No image "', Diff_Name, '" was found' )
#Get ROI coordinates
imageDisplay = diff_img.GetImageDisplay(0)
roi = imageDisplay.GetROI(0)
try:
if roi.IsRectangle:
top,left,bottom,right = np.array(roi.GetRectangle(),dtype='int32')
x = (right+left)//2
y = (bottom+top)//2
r = (right-left)/2
if roi.IsCircle():
x,y,r = np.array(roi.GetCircle(),dtype='int32')
except: print("Problem Reading ROI on the Picker Tool Image... Script Aborted. Please Place a Circular ROI around the Central Spot"); exit()
del diff_img, image
return (x,y,r),diff_data
def DM_interaction_wait_dialog(instructions_str, title_str='DM_Interaction_Dialog',button1_str='Continue',button2_str='Cancel' ):
'''
Function to Create a tkinter dialog that enables the user to interact with GMS
while the script is paused, and then resume the script after they are done
Accepts:
instructions_str string giving instructions to the user about what actions are expected in GMS
title_str optional string for the dialog window title
button1_str optional string to change the continue button label text
button1_str optional string to change the cancel button label text
Returns: Nothing
'''
root = Tk()
root.attributes('-topmost', 'true')
root.title(title_str)
def go_on():
root.destroy()
def cancel():
print("Script Aborted by User")
root.destroy()
sys.exit()
Label(root, text=instructions_str).grid(row=0, sticky=W)
Button(root, text=button1_str, command=go_on).grid(column=0, row=1, sticky=W, pady=4)
Button(root, text=button2_str, command=cancel).grid(column=1, row=1, sticky=W, pady=4)
mainloop()
def process_4D_data(image_4D,raw_datacube=None):
"""
Function to process a 4D datacube, outputing only an RGB array
This expects a DM image, not a numpy array, but a numpy array can be passed in
with the optional raw_datacube parameter
"""
if raw_datacube is None:
raw_datacube = image_4D.GetNumArray()
(x,y,r), picker_diff= get_picker_data_and_ROI(image_4D)
sx,sy = picker_diff.shape
center = (x,y)
FAA.maskP = r*2/sx*100
try:
if r == 0:
raise ValueError("ROI must be a circle (with radius defined in control panel on the left of DM) , not an oval.")
except:
DM.OkDialog("ROI must be a circle (with radius defined in control panel on the left of DM) , not an oval.")
if GUI_Progress_Bar: plt.close('all')
name = image_4D.GetName()
origin, x_scale, scale_unit = image_4D.GetDimensionCalibration(2, 0)
#Optionally Scale Data (up in real space, down in diff space)
if scale > 1:
data_s = FAA.STEMx_Crop_to_Center_Square(raw_datacube, center)
datasquare = np.copy(FAA.Upscale_4D_STEM(data_s, scale))
else:
datasquare = np.copy(FAA.STEMx_Crop_to_Center_Square(raw_datacube, center))
#Optionally Subtract Mean Pattern from Data
if subtract_mean_bg:
avg_diffraction = np.mean(datasquare,axis=(0,1))
data_BGSub = datasquare-avg_diffraction
data_BGSub[data_BGSub<0] = 0
data_input = data_BGSub
else:
data_input = datasquare
sigma =(0,0,2,2)
if gaussian_blur: data_input = scipy.ndimage.gaussian_filter(data_input, sigma,truncate=2)
#Process Dataset
(RGB_im,direction_image,intensity_image,spacing_image,diffractogram_max) = FAA.STEMx_Process_Cube(image_4D,map_var, im_data = data_input, show_cube = showcube,RGB_scale_max=RGB_scale_max)
DM_RGB = FAA.ShowRGB(RGB_im, name)
DM.DoEvents()
return RGB_im
def find_colorscale_max(showplot=True):
'''
Function to use values stored by the FAA module to determine the best
colorscale maximum to use during future runs of this script
'''
if showplot:
high_im = DM.CreateImage(np.asarray(FAA.high_lims))
high_im.ShowImage()
del high_im
#Output the suggested maximum
high_limit = np.max(np.asarray(FAA.high_lims))
print("Max spot intensity over dataset is %s" %high_limit)
print("Use this value for the RGB_scale_max parameter")
def mean_max_4D(image_4D,raw_datacube=None):
if raw_datacube is None:
raw_datacube = image_4D.GetNumArray()
mean = np.mean(raw_datacube,axis =(0,1))
max = np.max(raw_datacube,axis =(0,1))
return mean, max
director_image = DM.GetFrontImage()
#Get the file path of the 4D STEM dataset open in DM (this needs a one-line DM script)
dm='GetPersistentTagGroup().TagGroupSetTagAsString("Python_temp:out:FrontFileLocation",GetFrontImageDocument().ImageDocumentGetCurrentFile())'
DM.ExecuteScriptString(dm)
(b,filepath) = DM.GetPersistentTagGroup().GetTagAsString('Python_temp:out:FrontFileLocation')
if os.path.isfile(filepath):
print(filepath)
else:
print("Front image could not be found on disk. Has it been saved?")
#director_file = GetSelectedFiles()[0]
director_file = filepath
folder,file = os.path.split(director_file)
output_file = os.path.join(folder,"IS Processing",file)
output_file2 = os.path.join(folder,"IS Processing Mean-Max",file)
if not os.path.exists(os.path.dirname(output_file)):
os.makedirs(os.path.dirname(output_file))
if not os.path.exists(os.path.dirname(output_file2)):
os.makedirs(os.path.dirname(output_file2))
DM_interaction_wait_dialog("Put a Picker tool on the 4D Dataset and then an oval ROI around the central region of the diffration pattern to be masked out")
if numframes is None:
returnVal, numframes = director_image.GetTagGroup().GetTagAsLong('In-situ:Recorded:# Frames')
raw_out_path = PureWindowsPath(os.path.join(folder,output_file)).with_suffix(".raw")
if os.path.exists(raw_out_path):
message = str(raw_out_path) + " Already exists\n Overwrite existing processed data?"
if not DM.OkCancelDialog(message): exit()
print("Over-writing existing processed data")
os.remove(raw_out_path)
j=0
with open(raw_out_path, "ab") as raw_file:
for i in tqdm(range(0,numframes,step)):
if i>0: DM.DeleteImage(result)
print("Processing frame %s of %s " %(i,numframes))
data = GetNthFrame(director_file,director_image=director_image,N=i)
RGB_im = process_4D_data(director_image,raw_datacube=data)
result = DM.GetFrontImage()
DM.DoEvents()
raw_file.write(RGB_im.tobytes())
result_2D = DM.GetFrontImage()
dm = "InSituTags_CopyRecordTags(FindImageByID("+str(director_image.GetID())+"),FindImageByID("+str(result_2D.GetID())+"))"
dm += "\n FindImageByID("+str(result_2D.GetID())+").IMDSetFunctionInSituDirector()"
dm += "\n InSituTags_SetRawDataInfo(FindImageByID("+str(result_2D.GetID())+"),FindImageByID("+str(result_2D.GetID())+"))"
DM.ExecuteScriptString(dm)
DM.DoEvents()
path_2D = str(raw_out_path.with_suffix(".dm4"))
print(path_2D)
DM.GetFrontImage().SaveImage(path_2D)
print("Saved 2D Results")
#Must close and re-open in-situ director image (using an image document) to get the In-Situ Player to sync it
DM.DeleteImage(result_2D)
print("Opening 2D Director Image\n")
DM.DoEvents()
doc = DM.NewImageDocumentFromFile(path_2D)
doc.Show()
if GUI_Progress_Bar: plt.close('all')
find_colorscale_max(showplot=find_RGB_scale_max)
#Optionally Produce Circular Color Scale
if draw_color_scale: FAA.DrawColorScale()
print("\nStart time was %s" %start_time.time())
print("End time was %s" %datetime.now().time())
print("Processing took %s minutes" %((datetime.now()-start_time ).total_seconds()/60))
print("\nDone")