Source code for data_processing.image_analysis.hexagonal_mesh

import numpy as np
from scipy.spatial import Delaunay
from scipy.cluster.vq import kmeans, vq
from skimage.filters import threshold_multiotsu
import cv2

from data_processing.image_analysis.base_image_analyzer import ImageAnalysisTemplate
from data_processing.image_analysis.analysis_registry import register_class
from data_processing.image_analysis.pixel_stage_converter import z_normal


[docs] @register_class class HexagonalMesh(ImageAnalysisTemplate): """ Class utilizing the multi-Otsu algorhitm combined with the Delaunay algorithm for finding the nods and midpoints on hexagonal fluorescent mesh. """
[docs] def get_mesh_nodes(self): """ Function for finding the nods of fluorescent mesh by multi-otsu thresholding. :return: numpy array of mesh node centroids in pixel coordinates """ # Blur blurred = cv2.GaussianBlur(self.image, (7, 7), 0) # Multi-Otsu thresholds = threshold_multiotsu(blurred, classes=3) t_high = thresholds[1] _, thresh = cv2.threshold(blurred, t_high, 255, cv2.THRESH_BINARY) # Morphology thresholded = cv2.morphologyEx( thresh, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8) ) # Connected components num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(thresholded) return centroids
[docs] def get_delaunay_edges(self, points): """ Compute unique edges from Delaunay triangulation of given points. :return: numpy array of index pairs defining edges between points """ tri = Delaunay(points) triangles = tri.simplices edges = set() for triangle in triangles: for i in range(3): edge = tuple(sorted([triangle[i], triangle[(i + 1) % 3]])) edges.add(edge) return np.array(list(edges))
[docs] def filter_edges_by_distance(self, points, edges, n_clusters=3, remove_outliers=False): """ Group edges by length using clustering, optionally removing long-distance outliers. :return: lists of edges grouped by distance, cluster centers, and edge cluster labels """ # Calculating average distances edge_distances = [] for edge in edges: dist = np.linalg.norm(points[edge[0]] - points[edge[1]]) edge_distances.append(dist) edge_distances = np.array(edge_distances).reshape(-1, 1) edges = np.array(edges) if remove_outliers: # Calculating IQR and upper limit q75, q25 = np.percentile(edge_distances, [75, 25]) iqr = q75 - q25 upper_bound = q75 + 1.5 * iqr # Creating a mask for outlayers mask = (edge_distances <= upper_bound).flatten() edge_distances = edge_distances[mask] edges = edges[mask] if len(edge_distances) < n_clusters: n_clusters = len(edge_distances) # Finding clusters in lengths if n_clusters > 0: cluster_centers, _ = kmeans(edge_distances, n_clusters) cluster_labels, _ = vq(edge_distances, cluster_centers) else: return [], np.array([]), np.array([]) # Grouping edges into clasters clustered_edges = [[] for _ in range(n_clusters)] for i, edge in enumerate(edges): cluster_idx = cluster_labels[i] clustered_edges[cluster_idx].append(edge) # sorting clasters according to the distances sorted_indices = np.argsort(cluster_centers.flatten()) clustered_edges = [clustered_edges[i] for i in sorted_indices] cluster_centers = cluster_centers[sorted_indices] return clustered_edges, cluster_centers, cluster_labels
[docs] def find_midpoints_and_centroids(self, points, n_clusters=3, remove_outliers=True): """ Find edge midpoints and polygon centroids based on clustered Delaunay edges. :return: numpy array of edge midpoints and array of polygon centroids """ points = np.array(points) # 1. Finding the edges between points edges = self.get_delaunay_edges(points) # 2. Grouping edges by the length clustered_edges, cluster_centers, cluster_labels = self.filter_edges_by_distance(points, edges, n_clusters, remove_outliers) edge_midpoints = [] for edges_group in clustered_edges: if len(edges_group) > 0: midpoints = np.mean(points[edges_group], axis=1) edge_midpoints.append(midpoints) if len(clustered_edges) > 0: longest_edges = clustered_edges[-1] polygon_centroids = np.mean(points[longest_edges], axis=1) else: polygon_centroids = np.array([]) if len(edge_midpoints) > 0: return edge_midpoints[0], polygon_centroids else: return np.array([]), polygon_centroids
[docs] def get_measurement_points(self): """ Generate measurement points from mesh nodes, edge midpoints, and polygon centroids, and convert them to stage coordinates. :return: lists of dictionaries with the founded objects properties and their positions in the pixels coordinates and in the stage coordinates in um """ nodes = self.get_mesh_nodes() edge_midpoints, polygon_centroids = self.find_midpoints_and_centroids(nodes) measurement_points = [] groups = [ (nodes, "node"), (edge_midpoints, "edge midpoint"), (polygon_centroids, "polygon centroid"), ] for group, point_type in groups: for pos in group: measurement_points.append({ "position": pos, "type": point_type }) transformed_points = self.pixel_converter.convert_points(measurement_points, xy_mode='normal', z_strategy=z_normal) return measurement_points, transformed_points