@@ -48,6 +48,8 @@ def gridscan3d(
4848 data : tuple [np .ndarray , ...],
4949 threshold : float = 0.25 ,
5050 threshold_absolute : float = 0 ,
51+ threshold_msp : float = 0.25 ,
52+ threshold_msp_absolute : float = 3 ,
5153 plot : bool = False ,
5254 sample_id : int | None = None ,
5355 multipin_sample_ids : dict [int , int ] = {},
@@ -56,6 +58,14 @@ def gridscan3d(
5658 """
5759 3D gridscan analysis from 2 x 2D perpendicular gridscans.
5860
61+ Fitting routine finds each separate region of continuously connected data, filters
62+ out any regions which do not have a voxel with intensity > threshold_absolute. Then,
63+ for each of these regions, a relative threshold of the max voxel intensity for that
64+ region is applied, with the aim to separate/disconnect peaks of intensity within the
65+ region. Each continuously connected sub_region within the region after applying the
66+ threshold is then found and is stored as grid_Scan result. The purpose of this second
67+ step is to try and locate the individual centres of crystals that are close together.
68+
5969 Assumption: X is along the rotation axis, Y, Z are perpendicular
6070
6171 - spot find on images at -45°, +45° -> 2 x 2D array of spot finding metrics
@@ -68,16 +78,21 @@ def gridscan3d(
6878
6979 Args:
7080 data: A tuple of spot counts from 2 orthogonal 2D gridscans
71- threshold: mask out values less than this fraction of the maximum data value
72- in the reconstructed 3d grid
73- threshold_absolute: mask out values less than this absolute value in the
74- original grids
81+ threshold: Mask out values less than this fraction of the maximum data value
82+ in an identified region of the reconstructed 3d grid. Does not apply to
83+ multi-sample pins.
84+ threshold_absolute: filter out identified regions of continuous signal where the
85+ max value is less than this absolute value.
86+ threshold_msp: Applies only to multi-sample pins. Mask out values less than this
87+ fraction of the maximum data value in each continuous region of the reconstructed
88+ 3d grid.
89+ threshold_msp_absolute: Applied instead of threshold_absolute for multi-sample pins.
7590 plot: Show interactive debug plots of the grid scan analysis (default=False)
7691 sample_id: The sample id attributed to the grid_scan. This will usually be the
7792 sample in sublocation 1 in the case of multi-sample pins.
7893 multipin_sample_ids: Sample_ids for all samples on a multi-sample pin.
79- A dictionary of sample ids with the corresponding sub-locations in the
80- pin as keys.
94+ A dictionary of sample ids with the corresponding sub-locations in the
95+ pin as keys.
8196 well_limits: The lower and upper limits of the x-coordinate for each well in
8297 the multi-sample pin in units of grid scan boxes.
8398
@@ -88,62 +103,81 @@ def gridscan3d(
88103 assert data [0 ].ndim == 2
89104 assert data [1 ].ndim == 2
90105
91- # mask out grid scans to reduce impact of noisy pixels / edge effects
106+ # Use alternative thresholds for multi-sample pins
107+ if multipin_sample_ids :
108+ threshold = threshold_msp
109+ threshold_absolute = threshold_msp_absolute
110+
111+ # Apply absolute threshold to screen out noise.
92112 data [0 ][data [0 ] < threshold_absolute ] = 0
93113 data [1 ][data [1 ] < threshold_absolute ] = 0
94114
95115 reconstructed_3d = data [0 ][:, :, np .newaxis ] * data [1 ][:, np .newaxis , :]
96116 logger .debug (data [0 ].shape )
97117 logger .debug (data [1 ].shape )
98118 logger .debug (reconstructed_3d .shape )
99- max_idx = tuple (
100- int (r [0 ]) for r in np .where (reconstructed_3d == reconstructed_3d .max ())
101- )
102- max_count = int (reconstructed_3d [max_idx ])
103119
104- thresholded = (reconstructed_3d >= threshold * max_count ) * reconstructed_3d
105- # Count corner-corner contacts as a contiguous region
120+ # Count corner-corner contacts as a continuous region
106121 structure = np .ones ((3 , 3 , 3 ))
107- labels , n_regions = scipy .ndimage .label (thresholded , structure = structure )
108- logger .info (f"Found { n_regions } distinct regions" )
122+ # For multi-sample pins first find separate continuous regions in the 3d grid
123+ if multipin_sample_ids :
124+ # Find all continuous regions
125+ labels , n_regions = scipy .ndimage .label (reconstructed_3d , structure = structure )
126+ logger .info (f"Found { n_regions } distinct regions" )
109127
110- object_slices = scipy .ndimage .find_objects (labels )
128+ else :
129+ # For single sample pins, treat the entire grid as a single region
130+ labels = np .ones_like (reconstructed_3d , dtype = int )
131+ n_regions = 1
111132
112133 results : list [GridScan3DResult ] = []
113- for index in range (1 , n_regions + 1 ):
114- com = tuple (
115- c + 0.5
116- for c in scipy .ndimage .center_of_mass (
117- thresholded , labels = labels , index = index
118- )
134+ # Loop over each continuous region, apply a relative threshold then find sub-regions
135+ for label in range (1 , n_regions + 1 ):
136+ labelled_data = (labels == label ) * reconstructed_3d
137+ # Apply relative threshold to filter out edge effects and to separate out multiple centres in a single region.
138+ thresholded = (labelled_data >= threshold * labelled_data .max ()) * labelled_data
139+ sub_labels , n_sub_regions = scipy .ndimage .label (
140+ thresholded , structure = structure
119141 )
120- max_voxel = tuple (
121- int (i )
122- for i in scipy .ndimage .maximum_position (
123- thresholded , labels = labels , index = index
142+ logger .debug (f"For label { label } , { n_sub_regions } sub regions found" )
143+ object_slices = scipy .ndimage .find_objects (sub_labels )
144+
145+ for sub_label in range (1 , n_sub_regions + 1 ):
146+ com = tuple (
147+ c + 0.5
148+ for c in scipy .ndimage .center_of_mass (
149+ thresholded , labels = sub_labels , index = sub_label
150+ )
151+ )
152+ max_voxel = tuple (
153+ int (i )
154+ for i in scipy .ndimage .maximum_position (
155+ thresholded , labels = sub_labels , index = sub_label
156+ )
157+ )
158+ max_count = int (thresholded [max_voxel ])
159+ n_voxels = np .count_nonzero (sub_labels == sub_label )
160+ total_count = int (
161+ scipy .ndimage .sum_labels (
162+ thresholded , labels = sub_labels , index = sub_label
163+ )
164+ )
165+ x , y , z = object_slices [sub_label - 1 ]
166+ bounding_box = ((x .start , y .start , z .start ), (x .stop , y .stop , z .stop ))
167+ tagged_sample_id = tag_sample_id (
168+ sample_id , multipin_sample_ids , well_limits , com [0 ]
124169 )
125- )
126- max_count = int (thresholded [max_voxel ])
127- n_voxels = np .count_nonzero (labels == index )
128- total_count = int (
129- scipy .ndimage .sum_labels (thresholded , labels = labels , index = index )
130- )
131- x , y , z = object_slices [index - 1 ]
132- bounding_box = ((x .start , y .start , z .start ), (x .stop , y .stop , z .stop ))
133- tagged_sample_id = tag_sample_id (
134- sample_id , multipin_sample_ids , well_limits , com [0 ]
135- )
136170
137- result = GridScan3DResult (
138- centre_of_mass = com ,
139- max_voxel = max_voxel ,
140- max_count = max_count ,
141- n_voxels = n_voxels ,
142- total_count = total_count ,
143- bounding_box = bounding_box ,
144- sample_id = tagged_sample_id ,
145- )
146- results .append (result )
171+ result = GridScan3DResult (
172+ centre_of_mass = com ,
173+ max_voxel = max_voxel ,
174+ max_count = max_count ,
175+ n_voxels = n_voxels ,
176+ total_count = total_count ,
177+ bounding_box = bounding_box ,
178+ sample_id = tagged_sample_id ,
179+ )
180+ results .append (result )
147181
148182 if plot :
149183 plot_gridscan3d_results (data , reconstructed_3d , results )
0 commit comments