From f820e99879e1b8ef7f453a83fc6ef28dd440a5ad Mon Sep 17 00:00:00 2001 From: Aidan Campbell Date: Fri, 13 Sep 2024 11:59:32 +0200 Subject: [PATCH] dark_spot_counter.py Performs HyperSpy find_peaks_log() and then applies circle markers to the peak positions on the Signal2D. Secondly, interactive marker placement and removal is enabled by mouse clicking inside the plot. Either of the functionalities can be used independently. Using the signal scale and the number of markers, the density of dark spots is calculated and applied to the signal metadata. --- lumispy/utils/signals.py | 160 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/lumispy/utils/signals.py b/lumispy/utils/signals.py index 8467b3eac..d5dab0bf8 100644 --- a/lumispy/utils/signals.py +++ b/lumispy/utils/signals.py @@ -79,3 +79,163 @@ def _interpolate_signal(axis_array, index, **kwargs): raise ValueError("The parmeter `signal_axis` must be a HyperSpy Axis object.") return com_val + + + + + + +import warnings +import matplotlib.pyplot as plt + +from hyperspy.drawing._markers.circles import Circles +from matplotlib import get_backend + +def dark_spot_counter( + image, + log_algorithm=True, + max_sigma=30, + min_sigma=12, + num_sigma=5, + threshold=0.1, + overlap=0.5, + log_scale=False, + exclude_border=True, + invert_img=True, + click_tolerance=0.06, + r=0.15, + color='g', + linewidth=2 +): + """ + Interactive tool for detecting and marking dark spots in an image using + the Laplacian of Gaussian (LoG) method, with optional manual adjustments + via mouse clicks. Updates image metadata to reflect the number and density + of detected dark spots. + + Parameters + ---------- + image : hyperspy Signal2D object + The image to be processed and annotated. + log_algorithm : bool, default True + Whether to use the Laplacian of Gaussian (LoG) algorithm for dark spot detection. + max_sigma : float, default 30 + The maximum sigma value for the LoG filter, determining the largest scale of detection. + min_sigma : float, default 12 + The minimum sigma value for the LoG filter, determining the smallest scale of detection. + num_sigma : int, default 5 + The number of sigma values to use for the LoG filter. + threshold : float, default 0.1 + The threshold for detecting dark spots. + overlap : float, default 0.5 + The overlap parameter for the LoG filter, defining the minimum overlap between detected spots. + log_scale : bool, default False + Whether to use logarithmic scale in the LoG filter. + exclude_border : bool, default True + Whether to exclude dark spots near the borders of the image. + invert_img : bool, default True + Whether to invert the image before applying the LoG filter. + click_tolerance : float, default 0.06 + The tolerance for detecting clicks when adding or removing markers. + r : float, default 0.15 + The radius of the markers used to display detected dark spots. + color : str, default 'g' + The color of the markers used to display detected dark spots. + linewidth : float, default 2 + The linewidth of the edges of the markers used to display detected dark spots. + + Returns + ------- + None + Updates the image with marked dark spots and modifies the metadata to + include the number and density of detected dark spots. + """ + + if get_backend() != 'widget': + warnings.warn("You are not using the matplotlib widget backend. Interactive features may not work optimally with this setting.") + + if image.axes_manager.as_dictionary()['axis-0']['units'] is None: + warnings.warn("The provided signal does not possess units in its axes_manager; therefore, dark spot density calculations may be inaccurate. Supported units include: 'µm', 'nm'") + + # Determine conversion factor for physical area calculation + factor = 1 + if image.axes_manager[0].units == 'µm': + factor = 1e-6 + elif image.axes_manager[0].units == 'nm': + factor = 1e-9 + + # Calculate physical area of image in metres for density calculation + h = image.axes_manager[0].size * image.axes_manager[0].scale * factor + w = image.axes_manager[1].size * image.axes_manager[1].scale * factor + + def update_density(): + """Update density in image metadata.""" + N = len(image.metadata.Markers.as_dictionary()['Circles']['kwargs']['offsets']) + density = "{:.3e}".format(N / (h * w * 1e4)) # Calculate density in cm^-2 + image.metadata.set_item('Dark_spots.number', N) + image.metadata.set_item('Dark_spots.density', density) + image.metadata.set_item('Dark_spots.density_units', 'cm$^{-2}$') + plt.title(r'$\rho_{\text{spots}} = \text{' + density + r'} \text{ cm}^{-2}$') + + # Initialize marker collection from existing or create a new one + if image.metadata.has_item('Markers'): + marker = Circles(**image.metadata.Markers.Circles.kwargs) + del image.metadata.Markers.Circles # Remove to avoid duplication + else: + marker = Circles( + offsets=np.empty((0, 2)), + sizes=np.array([r]), + edgecolor=color, + linewidth=linewidth + ) + + # Perform Laplacian of Gaussian algorithm + if log_algorithm: + from hyperspy.utils.peakfinders2D import find_peaks_log + + image_log = (image.map(np.max, inplace=False) - image) if invert_img else image + + blobs = find_peaks_log( + image_log.data, + min_sigma, + max_sigma, + num_sigma, + threshold, + overlap, + log_scale, + exclude_border + ) + + y = blobs[:, 0] * image.axes_manager[1].scale + x = blobs[:, 1] * image.axes_manager[0].scale + + marker.add_items(offsets=np.stack((x, y), axis=1), sizes=np.array([])) + image.add_marker(marker, permanent=True) + update_density() + else: + image.plot() + + # Event handlers for mouse clicks + def click1(event): + if event.inaxes is not None: + index = (np.array([], dtype=np.int64),) + + if len(marker.get_current_kwargs()['offsets']) > 0: + index = np.where(np.all(np.isclose(marker.get_current_kwargs()['offsets'], [event.xdata, event.ydata], atol=click_tolerance), axis=1)) + + if len(index[0]) == 1: + marker.remove_items(indices=index[0][0]) + + if len(index[0]) == 0: + marker.add_items(offsets=np.array([[event.xdata, event.ydata]]), sizes=np.array([])) + + image.add_marker(marker, permanent=True) + update_density() + + def click2(event): + if event.inaxes is not None: + update_density() + + plt.connect('button_press_event', click1) + plt.connect('button_release_event', click2) +