ChristopherMarais commited on
Commit
c05d309
1 Parent(s): 8ef1088

Upload Ambrosia.py

Browse files
Files changed (1) hide show
  1. Ambrosia.py +296 -0
Ambrosia.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLASS:
2
+ # pre_process_image
3
+ # METHODS:
4
+ # __init__
5
+ # INPUT:
6
+ # image_dir = (str) a full path to an image with multiple beetles and possibly a size reference circle
7
+ # manual_thresh_buffer (float) {optional} this is a manual way to control the binarizxing threshold.
8
+ # use this when beetles are broken up into multiple images\
9
+ # inputs should range from -1 to 1. higehr vlaues include lighter colors into the blobs and lower values reduce blob size
10
+ # OUTPUT(ATTRIBUTES):
11
+ # image_dir = (str) the same directory as is given as an input to the iamge that is being processed
12
+ # image = (np.array) the original compound image
13
+ # grey_image = (np.array) the original compound image in greyscale
14
+ # bw_image = (np.array) the original image in binary black and white
15
+ # inv_bw_image = (np.array) the original image inverted black and white binary
16
+ # clear_inv_bw_image = (np.array) the inverted black and white binary original image with all components touching the border removed
17
+ # segment
18
+ # INPUT:
19
+ # cluster_num = (int) {default=2} the number of clusters used for kmeans to pick only the cluster with alrgest blobs
20
+ # image_edge_buffer = (int) {default=50} number of pixels to add to box borders
21
+ # OUTPUT(ATTRIBUTES):
22
+ # cluster_num = (int) the same as the input
23
+ # image_edge_buffer = (int) the same as the input
24
+ # labeled_image = (np.array) the original compound image that is labelled
25
+ # max_kmeans_label = (int) the label of the cluster with the largest object/blob
26
+ # image_selected_df = (pd.DataFrame) a dataframe with columns describing each segmented image:
27
+ # 'centroid' = centre of the image
28
+ # 'bbox-0' = border 0
29
+ # 'bbox-1' = border 1
30
+ # 'bbox-2' = border 2
31
+ # 'bbox-3' = border 3
32
+ # 'orientation' = angle of image segment
33
+ # 'axis_major_length'
34
+ # 'axis_minor_length'
35
+ # 'area'
36
+ # 'area_filled'
37
+ # image_properties_df = (pd.DataFrame) similar to the image_selected_df, but inlcudes all the artefacts that are picked up
38
+ # col_image_lst = (list) a list with all the segmented images in color
39
+ # inv_bw_image_lst = (list) a list with all the segmented images in inverted binary black and white
40
+ # image_segment_count = (int) number of segmented images extracted from the compound image
41
+ # detect_outlier
42
+ # INPUT:
43
+ # None
44
+ # OUTPUT(ATTRIBUTES):
45
+ # image_array = (np.array) an array of the list of color segemented images (number of images, (R,G,B))
46
+ # r_ar_lst = (list) a list of arrays with flattened images red values
47
+ # g_ar_lst = (list) a list of arrays with flattened images green values
48
+ # b_ar_lst = (list) a list of arrays with flattened images blue values
49
+ # all_ar_lst = (list) a list of arrays with flattened images all red, green, and blue values
50
+ # px_dens_dist = (np.array) frequency distribution at 0-255 of all the values for each pixel
51
+ # corr_coef = (np.array) a square array of length equal to the number of segmented images showing the spearman correlation bewteen images
52
+ # corr_pval = (np.array) the pvalues associatedwith each correlation
53
+ # corr_coef_sum = (np.array) the sum of the correlations across each iamge compared to all others
54
+ # outlier_idx = (int) the index of the image with the lowest spearman correlation sum
55
+ # outlier_val = (float) the lowest sum correlation value
56
+ # outlier_col_image = (np.array) the color image of what is detected as the outlier
57
+ # outlier_inv_bw_image = (np.array) the inverted black on white image of the outlier segmented image
58
+ # outlier_bw_image = (np.array) the white on black image of the outlier segmented image
59
+ # image_selected_df = (pd.DataFrame) an updated dataframe that contains the circle identification data
60
+ # estimate_size
61
+ # INPUT:
62
+ # known_radius = (int) {default=1} the radius of the reference circle (shoudl be approximately the same size as the specimens to work best)
63
+ # canny_sigma = (int) {default=5} this describes how strict the cleaning border is for identifying the circle to place over the reference circle
64
+ # outlier_idx = (int) {default should be self.outlier_idx} change this when the circle is falsely detected
65
+ # OUTPUT(ATTRIBUTES):
66
+ # outlier_bw_image = (np.array) an updated version of the outlier iamge with a clean circle clear of artifacts
67
+ # outlier_idx = (int) same as the input
68
+ # clean_inv_bw_image_lst = (list) a list of cleaned white on black images no blobs touching hte border
69
+ # image_selected_df = (pd.DataFrame) an update to the dataframe of metadata containing pixel counts and relative area in mm^2 of all segmented images
70
+ # *black and white is white on black
71
+
72
+ # import requirements
73
+ import os
74
+ os.environ["OMP_NUM_THREADS"] = '1' #use this line on windows machines to avoid memory leaks
75
+ import numpy as np
76
+ import pandas as pd
77
+ from math import ceil
78
+ from skimage import io
79
+ from skimage.filters import threshold_otsu
80
+ from skimage.color import rgb2gray
81
+ from skimage.segmentation import clear_border
82
+ from skimage.measure import label, regionprops_table
83
+ from skimage.transform import hough_circle, hough_circle_peaks
84
+ from skimage.feature import canny
85
+ from skimage.draw import disk
86
+ from sklearn.cluster import KMeans
87
+ from scipy.stats import spearmanr
88
+
89
+ class pre_process_image:
90
+ # initialize image to be segmented from path
91
+ def __init__(self, image=None, image_dir=None, manual_thresh_buffer=0):
92
+ if image_dir is not None:
93
+ self.image_dir = image_dir.replace('\\','/') # full directory path to image
94
+ self.image = io.imread(image_dir) # read image from directory
95
+ elif image is not None:
96
+ self.image = image
97
+ else:
98
+ print("No image given to function")
99
+ self.grey_image = rgb2gray(self.image) #convert image to greyscale
100
+ self.bw_image = self.grey_image > threshold_otsu(self.grey_image) + manual_thresh_buffer # binarize image to be black & white
101
+ self.inv_bw_image = np.invert(self.bw_image) # invert black and white image
102
+ self.clear_inv_bw_image = clear_border(self.inv_bw_image) # remove anything touching image border
103
+
104
+ # segment the image into smaller images
105
+ def segment(self, cluster_num=2, image_edge_buffer=50):
106
+ self.cluster_num = cluster_num
107
+ self.image_edge_buffer = image_edge_buffer
108
+ self.labeled_image = label(self.clear_inv_bw_image) #label image
109
+ image_properties_df = pd.DataFrame( # get the properties of each image used to segment blobs in image
110
+ regionprops_table(
111
+ self.labeled_image,
112
+ properties=('centroid',
113
+ 'bbox',
114
+ 'orientation',
115
+ 'axis_major_length',
116
+ 'axis_minor_length',
117
+ 'area',
118
+ 'area_filled')
119
+ )
120
+ )
121
+ # cluster boxes of blobs by size
122
+ kmean_result = KMeans(n_clusters=cluster_num, n_init='auto').fit(
123
+ np.array(
124
+ image_properties_df[['axis_major_length', 'axis_minor_length']]
125
+ )
126
+ )
127
+ image_properties_df['kmeans_label'] = kmean_result.labels_
128
+ # keep only the largest cluster (ball bearing needs to be a similar size as the beetles)
129
+ self.max_kmeans_label = int(image_properties_df.kmeans_label[image_properties_df['area'] == image_properties_df['area'].max()])
130
+ image_selected_df = image_properties_df[image_properties_df['kmeans_label']==self.max_kmeans_label]
131
+ self.image_properties_df = image_properties_df
132
+ # enlarge the boxes around blobs with buffer
133
+ coord_df = image_selected_df.loc[:,['bbox-0','bbox-1','bbox-2','bbox-3']].copy()
134
+ coord_df = coord_df.reset_index(drop = True)
135
+ image_selected_df = image_selected_df.reset_index(drop = True)
136
+ coord_df.loc[:,['bbox-0','bbox-1']] = coord_df.loc[:,['bbox-0','bbox-1']]-self.image_edge_buffer
137
+ coord_df.loc[:,['bbox-2','bbox-3']] = coord_df.loc[:,['bbox-2','bbox-3']]+self.image_edge_buffer
138
+ image_selected_df.loc[:,['bbox-0','bbox-1','bbox-2','bbox-3']] = coord_df.loc[:,['bbox-0','bbox-1','bbox-2','bbox-3']]
139
+ # limit boundaries to the initial image size without this the iamge size bugs out when the boundaries are negative and it removes the image
140
+ mask = image_selected_df[['bbox-0','bbox-1','bbox-2','bbox-3']]>=0
141
+ image_selected_df[['bbox-0','bbox-1','bbox-2','bbox-3']] = image_selected_df[['bbox-0','bbox-1','bbox-2','bbox-3']].where(mask, other=0)
142
+ self.image_selected_df = image_selected_df
143
+ # crop blobs from image based on box sizes and add to list
144
+ col_image_lst = []
145
+ inv_bw_image_lst = []
146
+ for i in range(len(image_selected_df)):
147
+ coord_i = image_selected_df.iloc[i]
148
+ # color images
149
+ crop_img = self.image[int(coord_i['bbox-0']):int(coord_i['bbox-2']), int(coord_i['bbox-1']):int(coord_i['bbox-3'])]
150
+ col_image_lst.append(crop_img)
151
+ # inverted black and white images
152
+ crop_bw_img = self.inv_bw_image[int(coord_i['bbox-0']):int(coord_i['bbox-2']), int(coord_i['bbox-1']):int(coord_i['bbox-3'])]
153
+ inv_bw_image_lst.append(crop_bw_img)
154
+
155
+ #clear all images that are empty
156
+ # col_image_lst = [x for x in col_image_lst if x.shape[0] != 0]
157
+ # inv_bw_image_lst = [x for x in inv_bw_image_lst if x.shape[0] != 0]
158
+
159
+ self.col_image_lst = col_image_lst
160
+ self.inv_bw_image_lst = inv_bw_image_lst
161
+ self.image_segment_count = len(col_image_lst)
162
+
163
+ def detect_outlier(self):
164
+ # convert list to numpy array
165
+ self.image_array = np.copy(np.array(self.col_image_lst, dtype='object'))
166
+ # initialize lists to store data in
167
+ r_ar_lst = []
168
+ g_ar_lst = []
169
+ b_ar_lst = []
170
+ all_ar_lst = []
171
+ for l in range(self.image_segment_count):
172
+ # flatten arrays
173
+ img_var = self.image_array[l]
174
+ r_ar = img_var[:,:,0].flatten() # red
175
+ g_ar = img_var[:,:,1].flatten() # green
176
+ b_ar = img_var[:,:,2].flatten() # blue
177
+ all_ar = img_var.flatten() # all
178
+ # collect data in lists
179
+ r_ar_lst.append(r_ar)
180
+ g_ar_lst.append(g_ar)
181
+ b_ar_lst.append(b_ar)
182
+ all_ar_lst.append(all_ar)
183
+ self.r_ar_lst = r_ar_lst
184
+ self.g_ar_lst = g_ar_lst
185
+ self.b_ar_lst = b_ar_lst
186
+ self.all_ar_lst = all_ar_lst
187
+ # get frequency of values at each rgb value(0-255)
188
+ values_array = all_ar_lst # use all, but can use any color
189
+ temp_dist_ar = np.zeros(shape=(255, self.image_segment_count))
190
+ for i in range(self.image_segment_count):
191
+ unique, counts = np.unique(values_array[i], return_counts=True)
192
+ temp_dict = dict(zip(unique, counts))
193
+ for j in temp_dict.keys():
194
+ temp_dist_ar[j-1][i] = temp_dict[j]
195
+ self.px_dens_dist = temp_dist_ar
196
+ # calculate the spearman correlation of distributions between images
197
+ # use spearman because it is a non-parametric measures
198
+ # use the sum of the correlation coefficients to identify the outlier image
199
+ corr_ar = np.array(spearmanr(temp_dist_ar, axis=0))
200
+ corr_coef_ar = corr_ar[0,:,:]
201
+ corr_pval_ar = corr_ar[1,:,:]
202
+ corr_sum_ar = corr_coef_ar.sum(axis=0)
203
+ self.corr_coef = corr_coef_ar
204
+ self.corr_pval = corr_pval_ar
205
+ self.corr_coef_sum = corr_sum_ar
206
+ self.outlier_idx = corr_sum_ar.argmin()
207
+ self.outlier_val = corr_sum_ar.min()
208
+ self.outlier_col_image = self.col_image_lst[self.outlier_idx]
209
+ self.outlier_inv_bw_image = self.inv_bw_image_lst[self.outlier_idx]
210
+ self.outlier_bw_image = np.invert(self.outlier_inv_bw_image)
211
+ # update metadata dataframe
212
+ self.image_selected_df['circle_class'] = 'non_circle'
213
+ self.image_selected_df.loc[self.outlier_idx, 'circle_class'] = 'circle'
214
+
215
+ def estimate_size(self, outlier_idx, known_radius=1, canny_sigma=5):
216
+ for i in range(len(self.corr_coef_sum)):
217
+ # add appropriate data to dataframe when circle not detected at all
218
+ if i == (len(self.corr_coef_sum)-1):
219
+ self.outlier_idx = None
220
+ self.outlier_val = None
221
+ self.outlier_col_image = None
222
+ self.outlier_inv_bw_image = None
223
+ self.outlier_bw_image = None
224
+ # update metadata dataframe
225
+ self.image_selected_df['circle_class'] = 'non_circle'
226
+ self.image_selected_df['real_area'] = 0
227
+ clean_inv_bw_image_lst = []
228
+ for inv_bw_image in self.inv_bw_image_lst:
229
+ # bw_image = np.invert(inv_bw_image)
230
+ clean_inv_bw_image = clear_border(inv_bw_image)
231
+ clean_inv_bw_image_lst.append(clean_inv_bw_image)
232
+ px_count_lst = []
233
+ for bw_img in clean_inv_bw_image_lst:
234
+ unique_px_count = np.unique(bw_img, return_counts=True)
235
+ px_dict = dict(zip(list(unique_px_count[0]), list(unique_px_count[1])))
236
+ if len(px_dict) == 1:
237
+ px_count = 0
238
+ else:
239
+ px_count = px_dict[True]
240
+ px_count_lst.append(px_count)
241
+ self.image_selected_df['pixel_count'] = px_count_lst
242
+ print("Circle could not be found: "+str(self.image_dir))
243
+ else:
244
+ try:
245
+ self.outlier_idx = np.argsort(self.corr_coef_sum)[i]
246
+ self.outlier_val = np.sort(self.corr_coef_sum)[i]
247
+ self.outlier_col_image = self.col_image_lst[self.outlier_idx]
248
+ self.outlier_inv_bw_image = self.inv_bw_image_lst[self.outlier_idx]
249
+ self.outlier_bw_image = np.invert(self.outlier_inv_bw_image)
250
+ # update metadata dataframe
251
+ self.image_selected_df['circle_class'] = 'non_circle'
252
+ self.image_selected_df.loc[self.outlier_idx, 'circle_class'] = 'circle'
253
+ outlier_inv_bw_image = np.invert(self.outlier_bw_image)
254
+ # remove the border touching blobs of all b&w images
255
+ clean_inv_bw_image_lst = []
256
+ for inv_bw_image in self.inv_bw_image_lst:
257
+ # bw_image = np.invert(inv_bw_image)
258
+ clean_inv_bw_image = clear_border(inv_bw_image)
259
+ clean_inv_bw_image_lst.append(clean_inv_bw_image)
260
+ # default is the image detected with detect_outlier
261
+ # change outlier_bw_image if this is not the ball bearing
262
+ edges = canny(self.outlier_bw_image, sigma=canny_sigma)
263
+ # Detect radius
264
+ max_r = int((max(outlier_inv_bw_image.shape)/2) + (self.image_edge_buffer/2)) # max radius
265
+ min_r = int((max_r-self.image_edge_buffer) - (self.image_edge_buffer/2)) # min radius
266
+ hough_radii = np.arange(min_r, max_r, 10)
267
+ hough_res = hough_circle(edges, hough_radii)
268
+ # Select the most prominent circle
269
+ accums, cx, cy, radii = hough_circle_peaks(hough_res, hough_radii, total_num_peaks=1)
270
+ circy, circx = disk((cy[0], cx[0]), radii[0])
271
+ # change the outlier image to fill in the circle
272
+ outlier_inv_bw_image[circy, circx] = True # this index error occurs when the outlier object circle does not fit into the image
273
+
274
+ self.outlier_inv_bw_image = clear_border(outlier_inv_bw_image)
275
+ clean_inv_bw_image_lst[self.outlier_idx] = self.outlier_inv_bw_image
276
+ self.clean_inv_bw_image_lst = clean_inv_bw_image_lst
277
+ # get the area of the ball bearing based on the known radius
278
+ circle_area = np.pi*(known_radius**2)
279
+ px_count_lst = []
280
+ for bw_img in clean_inv_bw_image_lst:
281
+ px_count = np.unique(bw_img, return_counts=True)[1][1] # this index error occurs when the outlier object touches the edge of the image (forces recalculation of outlier)
282
+ px_count_lst.append(px_count)
283
+ self.image_selected_df['pixel_count'] = px_count_lst
284
+ circle_px_count = px_count_lst[self.outlier_idx]
285
+ area_ar = (np.array(px_count_lst)/circle_px_count)*circle_area
286
+ self.image_selected_df['real_area'] = area_ar
287
+
288
+ break
289
+
290
+ except IndexError:
291
+ print('Updating circle classification for image: '+ str(self.image_dir))
292
+
293
+ else:
294
+ print("No circle was found to estimate beetle size")
295
+
296
+ # add a section at line 219 that labels all area as 0 and all circle_class as non_circle when the least outlying object is considered.