55
66
77from ulc_mm_package .image_processing .focus_metrics import downsample_image
8- from ulc_mm_package .image_processing .processing_constants import (
9- RBC_THUMBNAIL_PATH ,
10- CELLS_FOUND_THRESHOLD ,
11- MIN_POINTS_ABOVE_THRESH ,
12- )
13-
14- RBC_THUMBNAIL = cv2 .imread (RBC_THUMBNAIL_PATH , 0 )
15-
16-
17- def get_correlation_map (
18- img : np .ndarray ,
19- template_img : np .ndarray = RBC_THUMBNAIL ,
20- downsample_factor : int = 20 ,
21- ) -> np .ndarray :
22- """Downsamples the image and returns the 2D cross-correlation map.
23-
24- Side note
25- ----------
26- The values generated by the cross-correlation map (using TM_CCOEFF)
27- are sensitive to the dimensions of the image and template. As such, the threshold
28- used is somewhat specific to the particular downsampling factor used.
29-
30- TM_CCOEFF_NORMED (which normalizes the values to lie within 0-1) is not used
31- because we want to do a comparison of how well the template matches _between_ images.
32-
33- This function has been tested for robustness with a downsample_factor=10
34- and the current template image (image_processing/thumbnail.png).
35-
36- Parameters
37- ----------
38- img: np.ndarray
39- Base image
40- template_img: np.ndarray
41- Image to use as a template for the cross-correlation
42- downsample_factor: int=10
43-
44- Returns
45- -------
46- np.ndarray:
47- Map of the 2D cross-correlation values
48- """
49-
50- img_ds = downsample_image (img , downsample_factor )
51- return cv2 .matchTemplate (img_ds , template_img , cv2 .TM_CCOEFF )
528
539
5410class LowDensity (Exception ):
@@ -60,80 +16,47 @@ class NoCellsFound(Exception):
6016
6117
6218class CellFinder :
63- def __init__ (
64- self , template_path : str = RBC_THUMBNAIL_PATH , downsample_factor : int = 10
65- ):
66- self .thumbnail = downsample_image (
67- cv2 .imread (template_path , 0 ), downsample_factor
68- )
19+ def __init__ (self , downsample_factor : int = 10 , lookback_window : int = 3 ):
6920 self .downsample_factor = downsample_factor
21+ self .lookback_window = lookback_window
7022 self .motor_pos : List [int ] = []
71- self .confidences : List [float ] = []
72- self .maps : List [np .ndarray ] = []
23+ self .sds : List [float ] = []
7324
7425 def add_image (self , motor_pos : int , img : np .ndarray ) -> None :
75- """Check for cells for the given image, store the result + motor position the image was taken at."""
26+ """Calculate the standard deviation of the given image (after downsmapling), store the result + motor position the image was taken at."""
27+
28+ img_ds = downsample_image (img , self .downsample_factor )
29+ sd = np .std (img_ds )
7630
7731 self .motor_pos .append (motor_pos )
78- xcorr_map = get_correlation_map (img , self .thumbnail , self .downsample_factor )
79- self .confidences .append (np .max (xcorr_map ))
80- self .maps .append (xcorr_map )
32+ self .sds .append (sd )
8133
8234 def get_cells_found_position (self ) -> Optional [int ]:
83- """Check if the cross-correlation value exceeds the threshold for cell detection and there are
84- a sufficient number of points above the cells found threshold.
85-
86- Returns
87- -------
88- int
89- Motor position if cells were found
90- Exceptions
91- ----------
92- NoCellsFound
93- Raised if the value of the maximum cross correlation value from the given images
94- does not exceed a threshold.
95- """
96-
97- argmax = np .argmax (self .confidences )
98- max_val = self .confidences [argmax ]
99-
100- if max_val >= CELLS_FOUND_THRESHOLD :
101- return self .motor_pos [np .argmax (self .confidences )]
35+ """Find the motor position whose image SD stands out compared to the rest.
10236
103- raise NoCellsFound (
104- "None of the images at any of the motor positions had a maximum cross-correlation exceeding the CELLS_FOUND threshold"
105- )
37+ Use the median-absolute-deviation and a monotonic decrease over a given lookback
38+ period to assess whether the peak a) has objects in it and b) is prominent relative to
39+ the surrounding measurements.
40+ """
41+ sds = np .asarray (self .sds , dtype = float )
42+ if sds .size > self .lookback_window :
43+ argmax = int (np .argmax (sds ))
44+ max_val = float (sds [argmax ])
10645
107- def sufficient_points_above_thresh (self , xcorr_map : np .ndarray ) -> bool :
108- """Check if a sufficient number of points are above the cells found threshold
109- in the xcorr map.
46+ median = np .median (sds )
47+ mad = np .median (np .abs (sds - median ))
11048
111- Parameters
112- ----------
113- xcorr_map: np.ndarray
49+ k = 1.4826 # Scale factor for normally distributed data
50+ robust_z = (max_val - median ) / (k * mad )
11451
115- Returns
116- -------
117- bool
118- """
52+ is_monotonically_decreasing = np .all (
53+ np .sign (np .diff (sds [- self .lookback_window :])) == - 1
54+ )
55+ if is_monotonically_decreasing and robust_z >= 3.0 :
56+ return self .motor_pos [argmax ]
11957
120- points = np .argwhere (xcorr_map >= CELLS_FOUND_THRESHOLD )
121- return len (points ) > MIN_POINTS_ABOVE_THRESH
58+ raise NoCellsFound ("No image stands out as containing cells" )
12259
12360 def reset (self ) -> None :
12461 self .motor_pos = []
125- self .confidences = []
126-
127- def find_cells_cross_corr (self , img : np .ndarray ) -> float :
128- """Returns the max value of the correlation between the RBC thumbnail and the given image (downsampled)
129-
130- Returns
131- -------
132- float:
133- Max value of the 2D cross-correlation
134- """
135-
136- cross_corr_map = get_correlation_map (
137- img , self .thumbnail , self .downsample_factor
138- )
139- return np .max (cross_corr_map )
62+ self .sds = []
0 commit comments