cellects 0.1.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. cellects/__init__.py +0 -0
  2. cellects/__main__.py +49 -0
  3. cellects/config/__init__.py +0 -0
  4. cellects/config/all_vars_dict.py +154 -0
  5. cellects/core/__init__.py +0 -0
  6. cellects/core/cellects_paths.py +30 -0
  7. cellects/core/cellects_threads.py +1464 -0
  8. cellects/core/motion_analysis.py +1931 -0
  9. cellects/core/one_image_analysis.py +1065 -0
  10. cellects/core/one_video_per_blob.py +679 -0
  11. cellects/core/program_organizer.py +1347 -0
  12. cellects/core/script_based_run.py +154 -0
  13. cellects/gui/__init__.py +0 -0
  14. cellects/gui/advanced_parameters.py +1258 -0
  15. cellects/gui/cellects.py +189 -0
  16. cellects/gui/custom_widgets.py +789 -0
  17. cellects/gui/first_window.py +449 -0
  18. cellects/gui/if_several_folders_window.py +239 -0
  19. cellects/gui/image_analysis_window.py +1909 -0
  20. cellects/gui/required_output.py +232 -0
  21. cellects/gui/video_analysis_window.py +656 -0
  22. cellects/icons/__init__.py +0 -0
  23. cellects/icons/cellects_icon.icns +0 -0
  24. cellects/icons/cellects_icon.ico +0 -0
  25. cellects/image_analysis/__init__.py +0 -0
  26. cellects/image_analysis/cell_leaving_detection.py +54 -0
  27. cellects/image_analysis/cluster_flux_study.py +102 -0
  28. cellects/image_analysis/extract_exif.py +61 -0
  29. cellects/image_analysis/fractal_analysis.py +184 -0
  30. cellects/image_analysis/fractal_functions.py +108 -0
  31. cellects/image_analysis/image_segmentation.py +272 -0
  32. cellects/image_analysis/morphological_operations.py +867 -0
  33. cellects/image_analysis/network_functions.py +1244 -0
  34. cellects/image_analysis/one_image_analysis_threads.py +289 -0
  35. cellects/image_analysis/progressively_add_distant_shapes.py +246 -0
  36. cellects/image_analysis/shape_descriptors.py +981 -0
  37. cellects/utils/__init__.py +0 -0
  38. cellects/utils/formulas.py +881 -0
  39. cellects/utils/load_display_save.py +1016 -0
  40. cellects/utils/utilitarian.py +516 -0
  41. cellects-0.1.0.dev1.dist-info/LICENSE.odt +0 -0
  42. cellects-0.1.0.dev1.dist-info/METADATA +131 -0
  43. cellects-0.1.0.dev1.dist-info/RECORD +46 -0
  44. cellects-0.1.0.dev1.dist-info/WHEEL +5 -0
  45. cellects-0.1.0.dev1.dist-info/entry_points.txt +2 -0
  46. cellects-0.1.0.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1065 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ This script contains the OneImageAnalysis class
4
+ OneImageAnalysis is a class containing many tools to analyze one image
5
+
6
+ An image can be coded in different color spaces, such as RGB, HSV, etc. These color spaces code the color of each pixel as three numbers, ranging from 0 to 255. Our aim is to find a combination of these three numbers that provides a single intensity value for each pixel, and which maximizes the contrast between the organism and the background. To increase the flexibility of our algorithm, we use more than one color space to look for these combinations. In particular, we use the RGB, LAB, HSV, LUV, HLS and YUV color spaces. What we call a color space combination is a transformation combining several channels of one or more color spaces.
7
+ To find the optimal color space combination, Cellects uses one image (which we will call “seed image”). The software selects by default the first image of the sequence as seed image, but the user can select a different image where the cells are more visible.
8
+ Cellects has a fully automatic algorithm to select a good color space combination, which proceeds in four steps:
9
+
10
+ First, it screens every channel of every color space. For instance, it converts the image into grayscale using the second channel of the color space HSV, and segments that grayscale image using Otsu thresholding. Once a binary image is computed from every channel, Cellects only keep the channels for which the number of connected components is lower than 10000, and the total area detected is higher than 100 pixels but lower than 0.75 times the total size of the image. By doing so, we eliminate the channels that produce the most noise.
11
+
12
+ In the second step, Cellects uses all the channels that pass the first filter and tests all possible pairwise combinations. Cellects combines channels by summing their intensities and re-scaling the result between 0 and 255. It then performs the segmentation on these combinations, and filters them with the same criteria as in the first step.
13
+
14
+ The third step uses the previously selected channels and combinations that produce the highest and lowest detected surface to make logical operations between them. It applies the AND operator between the two results having the highest surface, and the OR operator between the two results having the lowest surface. It thus generates another two candidate segmentations, which are added to the ones obtained in the previous steps.
15
+
16
+ In the fourth step, Cellects works under the assumption that the image contains multiple similar arenas containing a collection of objects with similar size and shape, and keeps the segmentations whose standard error of the area is smaller than ten times the smallest area standard error across all segmentations. To account for cases in which the experimental setup induces segmentation errors in one particular direction, Cellects also keeps the segmentation with minimal width standard error across all segmentations, and the one with minimal height standard error across all segmentations. All retained segmentations are shown to the user, who can then select the best one.
17
+
18
+ As an optional step, Cellects can refine the choice of color space combination, using the last image of the sequence instead of the seed image. In order to increase the diversity of combinations explored, this optional analysis is performed in a different way than for the seed image. Also, this refining can use information from the segmentation of the seed frame and from the geometry of the arenas to rank the quality of the segmentation emerging from each color space combination. To generate these combinations, Cellects follows four steps.
19
+ The first step is identical to the first step of the previously described automatic algorithm (in section 1) and starts by screening every possible channel and color space.
20
+
21
+ The second step aims to find combinations that consider many channels, rather than those with only one or two. To do that, it creates combinations that consist of the sum of all channels except one. It then filters these combinations in the same way as for the previous step. Then, all surviving combinations are retained, and also undergo the same process in which one more channel is excluded, and the process continues until reaching single-channel combinations. This process thus creates new combinations that include any number of channels.
22
+
23
+ The third step filters these segmentations, keeping those that fulfill the following criteria: (1) The number of connected components is higher than the number of arenas and lower than 10000. (2) The detected area covers less than 99% of the image. (2) Less than 1% of the detected area falls outside the arenas. (4) Each connected component of the detected area covers less than 75% of the image.
24
+
25
+ Finally, the fourth step ranks the remaining segmentations using the following criteria: If the user labeled any areas as “cell”, the ranking will reflect the amount of cell pixels in common between the segmentation and the user labels. If the user did not label any areas as cells but labeled areas as background, the ranking will reflect the number of background pixels in common. Otherwise, the ranking will reflect the number of pixels in common with the segmentation of the first image.
26
+
27
+
28
+ """
29
+
30
+ import logging
31
+ import os
32
+ from copy import deepcopy
33
+ import numpy as np
34
+ import cv2 # named opencv-python
35
+ import multiprocessing.pool as mp
36
+ from numba.typed import List as TList
37
+ from numba.typed import Dict as TDict
38
+ from cellects.image_analysis.morphological_operations import cross_33, Ellipse
39
+ from cellects.image_analysis.image_segmentation import get_color_spaces, combine_color_spaces, otsu_thresholding, get_otsu_threshold
40
+ from cellects.image_analysis.one_image_analysis_threads import SaveCombinationThread, ProcessFirstImage
41
+ from cellects.utils.formulas import bracket_to_uint8_image_contrast
42
+
43
+
44
+ class OneImageAnalysis:
45
+ """
46
+ This class takes a 3D matrix (2 space and 1 color [BGR] dimensions),
47
+ Its methods allow image
48
+ - conversion to any bgr/hsv/lab channels
49
+ - croping
50
+ - rotating
51
+ - filtering using some of the mainly used techniques:
52
+ - Gaussian, Median, Bilateral, Laplacian, Mexican hat
53
+ - segmenting using thresholds or kmeans
54
+ - shape selection according to horizontal size or shape ('circle' vs 'quadrilateral')
55
+
56
+ ps: A viewing method displays the image before and after the most advanced modification made in instance
57
+ """
58
+ def __init__(self, image):
59
+ self.image = image
60
+ if len(self.image.shape) == 2:
61
+ self.already_greyscale = True
62
+ else:
63
+ self.already_greyscale = False
64
+ self.image2 = None
65
+ self.binary_image2 = None
66
+ self.drift_correction_already_adjusted: bool = False
67
+ # Create empty variables to fill in the following functions
68
+ self.binary_image = np.zeros(self.image.shape[:2], dtype=np.uint8)
69
+ self.previous_binary_image = None
70
+ self.validated_shapes = np.zeros(self.image.shape[:2], dtype=np.uint8)
71
+ self.centroids = 0
72
+ self.shape_number = 0
73
+ self.concomp_stats = 0
74
+ self.y_boundaries = None
75
+ self.x_boundaries = None
76
+ self.crop_coord = None
77
+ self.cropped: bool = False
78
+ self.subtract_background = None
79
+ self.subtract_background2 = None
80
+ self.im_combinations = None
81
+ self.bgr = image
82
+ self.colorspace_list = TList(("bgr", "lab", "hsv", "luv", "hls", "yuv"))
83
+ self.spot_shapes = None
84
+ self.all_c_spaces = TDict()
85
+ self.hsv = None
86
+ self.hls = None
87
+ self.lab = None
88
+ self.luv = None
89
+ self.yuv = None
90
+ """
91
+ I/ Image modification for segmentation through thresholding
92
+ This part contain methods to convert, visualize, filter and threshold one image.
93
+ """
94
+
95
+ def generate_color_space_combination(self, c_spaces, first_dict, second_dict, background=None, background2=None):
96
+
97
+ # logging.info(f"Generate the color space combination {first_dict}")
98
+ self.all_c_spaces = get_color_spaces(self.bgr, c_spaces)
99
+ self.image = combine_color_spaces(first_dict, self.all_c_spaces, background)
100
+ if len(second_dict) > 0:
101
+ logging.info(f"Coupled with the color space combination {second_dict}")
102
+ self.image2 = combine_color_spaces(second_dict, self.all_c_spaces, background2)
103
+
104
+ def convert_and_segment(self, c_space_dict, color_number=2, biomask=None,
105
+ backmask=None, subtract_background=None, subtract_background2=None, grid_segmentation=False,
106
+ lighter_background=None, side_length=20, step=5, int_variation_thresh=None, mask=None):
107
+
108
+ if self.already_greyscale:
109
+ self.segmentation(logical='None', color_number=2, biomask=biomask, backmask=backmask,
110
+ grid_segmentation=grid_segmentation, lighter_background=lighter_background,
111
+ side_length=side_length, step=step, int_variation_thresh=int_variation_thresh, mask=mask)
112
+ else:
113
+ if len(self.all_c_spaces) == 0:
114
+ self.all_c_spaces = get_color_spaces(self.bgr)
115
+ # if c_space_dict['logical'] != 'None':
116
+ first_dict = TDict()
117
+ second_dict = TDict()
118
+ for k, v in c_space_dict.items():
119
+ if k != 'logical' and v.sum() > 0:
120
+ if k[-1] != '2':
121
+ first_dict[k] = v
122
+ else:
123
+ second_dict[k[:-1]] = v
124
+ logging.info(first_dict)
125
+ self.image = combine_color_spaces(first_dict, self.all_c_spaces, subtract_background)
126
+ if len(second_dict) > 0:
127
+ self.image2 = combine_color_spaces(second_dict, self.all_c_spaces, subtract_background2)
128
+ self.segmentation(logical=c_space_dict['logical'], color_number=color_number, biomask=biomask,
129
+ backmask=backmask, grid_segmentation=grid_segmentation,
130
+ lighter_background=lighter_background, side_length=side_length, step=step,
131
+ int_variation_thresh=int_variation_thresh, mask=mask)
132
+
133
+ else:
134
+
135
+ self.segmentation(logical='None', color_number=color_number, biomask=biomask,
136
+ backmask=backmask, grid_segmentation=grid_segmentation,
137
+ lighter_background=lighter_background, side_length=side_length, step=step,
138
+ int_variation_thresh=int_variation_thresh, mask=mask)
139
+
140
+
141
+ def segmentation(self, logical='None', color_number=2, biomask=None, backmask=None, bio_label=None, bio_label2=None, grid_segmentation=False, lighter_background=None, side_length=20, step=5, int_variation_thresh=None, mask=None):
142
+ if (color_number > 2):
143
+ self.kmeans(color_number, biomask, backmask, logical, bio_label, bio_label2)
144
+ elif grid_segmentation:
145
+ if lighter_background is None:
146
+ self.binary_image = otsu_thresholding(self.image)
147
+ lighter_background = self.binary_image.sum() > (self.binary_image.size / 2)
148
+ if int_variation_thresh is None:
149
+ int_variation_thresh =100 - (np.ptp(self.image) * 90 / 255)
150
+ self.grid_segmentation(lighter_background, side_length, step, int_variation_thresh, mask)
151
+ else:
152
+ # logging.info("Segment the image using Otsu thresholding")
153
+ self.binary_image = otsu_thresholding(self.image)
154
+ if self.previous_binary_image is not None:
155
+ if (self.binary_image * (1 - self.previous_binary_image)).sum() > (self.binary_image * self.previous_binary_image).sum():
156
+ # Ones of the binary image have more in common with the background than with the specimen
157
+ self.binary_image = 1 - self.binary_image
158
+ # self.binary_image = self.correct_with_previous_binary_image(self.binary_image.copy())
159
+
160
+ if logical != 'None':
161
+ # logging.info("Segment the image using Otsu thresholding")
162
+ self.binary_image2 = otsu_thresholding(self.image2)
163
+ if self.previous_binary_image is not None:
164
+ if (self.binary_image2 * (1 - self.previous_binary_image)).sum() > (
165
+ self.binary_image2 * self.previous_binary_image).sum():
166
+ self.binary_image2 = 1 - self.binary_image2
167
+ # self.binary_image2 = self.correct_with_previous_binary_image(self.binary_image2.copy())
168
+
169
+ if logical != 'None':
170
+ if logical == 'Or':
171
+ self.binary_image = np.logical_or(self.binary_image, self.binary_image2)
172
+ elif logical == 'And':
173
+ self.binary_image = np.logical_and(self.binary_image, self.binary_image2)
174
+ elif logical == 'Xor':
175
+ self.binary_image = np.logical_xor(self.binary_image, self.binary_image2)
176
+ self.binary_image = self.binary_image.astype(np.uint8)
177
+
178
+
179
+ def correct_with_previous_binary_image(self, binary_image):
180
+ # If binary image is more than twenty times bigger or smaller than the previous binary image:
181
+ # otsu thresholding failed, we use a threshold of 127 instead
182
+ if binary_image.sum() > self.previous_binary_image.sum() * 20 or binary_image.sum() < self.previous_binary_image.sum() * 0.05:
183
+ binary_adaptive = cv2.adaptiveThreshold(bracket_to_uint8_image_contrast(self.image), 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
184
+ # from skimage import filters
185
+ # threshold_value = filters.threshold_li(self.image)
186
+ # binary_image = self.image >= threshold_value
187
+ binary_image = self.image >= 127
188
+ # And again, make sure than these pixels are shared with the previous binary image
189
+ if (binary_image * (1 - self.previous_binary_image)).sum() > (binary_image * self.previous_binary_image).sum():
190
+ binary_image = 1 - binary_image
191
+ return binary_image.astype(np.uint8)
192
+
193
+
194
+ def get_largest_shape(self):
195
+ shape_number, shapes, stats, centroids = cv2.connectedComponentsWithStats(self.binary_image)
196
+ sorted_area = np.sort(stats[1:, 4])
197
+ self.validated_shapes = np.zeros(self.binary_image.shape, dtype=np.uint8)
198
+ self.validated_shapes[np.nonzero(shapes == np.nonzero(stats[:, 4] == sorted_area[-1])[0])] = 1
199
+
200
+ def generate_subtract_background(self, c_space_dict):
201
+ logging.info("Generate background using the generate_subtract_background method of OneImageAnalysis class")
202
+ if len(self.all_c_spaces) == 0 and not self.already_greyscale:
203
+ self.all_c_spaces = get_color_spaces(self.bgr)
204
+ self.convert_and_segment(c_space_dict, grid_segmentation=False)
205
+ # self.image = generate_color_space_combination(c_space_dict, self.all_c_spaces)
206
+ disk_size = int(np.floor(np.sqrt(np.min(self.bgr.shape[:2])) / 2))
207
+ disk = np.uint8(Ellipse((disk_size, disk_size)).create())
208
+ self.subtract_background = cv2.morphologyEx(self.image, cv2.MORPH_OPEN, disk)
209
+ if self.image2 is not None:
210
+ self.subtract_background2 = cv2.morphologyEx(self.image2, cv2.MORPH_OPEN, disk)
211
+
212
+ def check_if_image_border_attest_drift_correction(self):
213
+ t = np.all(self.binary_image[0, :])
214
+ b = np.all(self.binary_image[-1, :])
215
+ l = np.all(self.binary_image[:, 0])
216
+ r = np.all(self.binary_image[:, -1])
217
+ if (t and b) or (t and r) or (t and l) or (t and r) or (b and l) or (b and r) or (l and r):
218
+ cc_nb, shapes = cv2.connectedComponents(self.binary_image)
219
+ if cc_nb == 2:
220
+ return True
221
+ else:
222
+ return False
223
+ else:
224
+ return False
225
+
226
+ def adjust_to_drift_correction(self, logical):
227
+ if not self.drift_correction_already_adjusted:
228
+ self.drift_correction_already_adjusted = True
229
+
230
+ mask = cv2.dilate(self.binary_image, kernel=cross_33)
231
+ mask -= self.binary_image
232
+ mask = np.nonzero(mask)
233
+
234
+ drift_correction = np.mean(self.image[mask[0], mask[1]])
235
+ self.image[np.nonzero(self.binary_image)] = drift_correction
236
+ threshold = get_otsu_threshold(self.image)
237
+ binary = (self.image > threshold)
238
+ # while np.any(binary * self.binary_image) and threshold > 1: #binary.sum() > self.binary_image.sum()
239
+ # threshold -= 1
240
+ # binary1 = (self.image > threshold)
241
+ # binary2 = np.logical_not(binary1)
242
+ # if binary1.sum() < binary2.sum():
243
+ # binary = binary1
244
+ # else:
245
+ # binary = binary2
246
+ self.binary_image = binary.astype(np.uint8)
247
+
248
+ if self.image2 is not None:
249
+ drift_correction2 = np.mean(self.image2[mask[0], mask[1]])
250
+ self.image2[np.nonzero(self.binary_image)] = drift_correction2
251
+ threshold = get_otsu_threshold(self.image2)
252
+ binary1 = (self.image2 > threshold)
253
+ binary2 = np.logical_not(binary1)
254
+ if binary1.sum() < binary2.sum():
255
+ binary = binary1
256
+ else:
257
+ binary = binary2
258
+ while np.any(binary * self.binary_image2) and threshold > 1: # binary.sum() > self.binary_image.sum()
259
+ threshold -= 1
260
+ binary1 = (self.image2 > threshold)
261
+ binary2 = np.logical_not(binary1)
262
+ if binary1.sum() < binary2.sum():
263
+ binary = binary1
264
+ else:
265
+ binary = binary2
266
+ self.binary_image2 = binary.astype(np.uint8)
267
+ if logical == 'Or':
268
+ self.binary_image = np.logical_or(self.binary_image, self.binary_image2)
269
+ elif logical == 'And':
270
+ self.binary_image = np.logical_and(self.binary_image, self.binary_image2)
271
+ elif logical == 'Xor':
272
+ self.binary_image = np.logical_xor(self.binary_image, self.binary_image2)
273
+ self.binary_image = self.binary_image.astype(np.uint8)
274
+
275
+ def set_spot_shapes_and_size_confint(self, spot_shape):
276
+ self.spot_size_confints = np.arange(0.75, 0.00, - 0.05)# np.concatenate((np.arange(0.75, 0.00, - 0.05), np.arange(0.05, 0.00, -0.005)))#
277
+ if spot_shape is None:
278
+ self.spot_shapes = np.tile(["circle", "rectangle"], len(self.spot_size_confints))
279
+ self.spot_size_confints = np.repeat(self.spot_size_confints, 2)
280
+ else:
281
+ self.spot_shapes = np.repeat(spot_shape, len(self.spot_size_confints))
282
+
283
+ def find_first_im_csc(self, sample_number=None, several_blob_per_arena=True, spot_shape=None, spot_size=None, kmeans_clust_nb=None, biomask=None, backmask=None, color_space_dictionaries=None, carefully=False):
284
+ logging.info(f"Prepare color space lists, dictionaries and matrices")
285
+ if len(self.all_c_spaces) == 0:
286
+ self.all_c_spaces = get_color_spaces(self.bgr)
287
+ if color_space_dictionaries is None:
288
+ if carefully:
289
+ colorspace_list = ["bgr", "lab", "hsv", "luv", "hls", "yuv"]
290
+ else:
291
+ colorspace_list = ["lab", "hsv"]
292
+ color_space_dictionaries = TList()
293
+ for i, c_space in enumerate(colorspace_list):
294
+ for i in np.arange(3):
295
+ channels = np.array((0, 0, 0), dtype=np.int8)
296
+ channels[i] = 1
297
+ csc_dict = TDict()
298
+ csc_dict[c_space] = channels
299
+ color_space_dictionaries.append(csc_dict)
300
+
301
+ # if not several_blob_per_arena:
302
+ self.set_spot_shapes_and_size_confint(spot_shape)
303
+
304
+ self.combination_features = np.zeros((len(color_space_dictionaries) + 50, 11), dtype=np.uint32)
305
+ # ["c1", "c2", "c3", "unaltered_cc_nb", "concomp_nb", "total_area", "width_std", "height_std", "centrodist_std", "biosum", "backsum"]
306
+ unaltered_cc_nb, cc_nb, area, width_std, height_std, area_std, biosum, backsum = 3, 4, 5, 6, 7, 8, 9, 10
307
+ self.saved_images_list = TList()
308
+ self.converted_images_list = TList()
309
+ self.saved_color_space_list = list()
310
+ self.saved_csc_nb = 0
311
+ self.save_combination_thread = SaveCombinationThread(self)
312
+ get_one_channel_result = True
313
+ combine_channels = False
314
+
315
+ for csc_dict in color_space_dictionaries:
316
+ logging.info(f"Try detection with each color space channel, one by one. Currently analyzing {csc_dict}")
317
+ list_args = [self, get_one_channel_result, combine_channels, csc_dict, several_blob_per_arena,
318
+ sample_number, spot_size, kmeans_clust_nb, biomask, backmask, None]
319
+ ProcessFirstImage(list_args)
320
+ # logging.info(csc_dict)
321
+
322
+ if sample_number is not None and carefully:
323
+ # tic = default_timer()
324
+ # Try to add csc together
325
+ # possibilities = np.arange(len(self.saved_color_space_list))
326
+ possibilities = []
327
+ if self.saved_csc_nb > 6:
328
+ different_color_spaces = np.unique(self.saved_color_space_list)
329
+ for color_space in different_color_spaces:
330
+ csc_idx = np.nonzero(np.isin(self.saved_color_space_list, color_space))[0]
331
+ possibilities.append(csc_idx[0] + np.argmin(self.combination_features[csc_idx, area_std]))
332
+ if len(possibilities) < 6:
333
+ remaining_possibilities = np.arange(len(self.saved_color_space_list))
334
+ remaining_possibilities = remaining_possibilities[np.logical_not(np.isin(remaining_possibilities, possibilities))]
335
+ while len(possibilities) < 6:
336
+ new_possibility = np.argmin(self.combination_features[remaining_possibilities, area_std])
337
+ possibilities.append(new_possibility)
338
+ remaining_possibilities = remaining_possibilities[remaining_possibilities != new_possibility]
339
+
340
+
341
+ pool = mp.ThreadPool(processes=os.cpu_count() - 1)
342
+ get_one_channel_result = False
343
+ combine_channels = True
344
+ list_args = [[self, get_one_channel_result, combine_channels, i, several_blob_per_arena, sample_number,
345
+ spot_size, kmeans_clust_nb, biomask, backmask, possibilities] for i in possibilities]
346
+ for process_i in pool.imap_unordered(ProcessFirstImage, list_args):
347
+ pass
348
+
349
+ # Get the most and the least covered images and the 2 best biomask and backmask scores
350
+ # To try combinations of those
351
+ if self.saved_csc_nb > 1:
352
+ coverage = np.argsort(self.combination_features[:self.saved_csc_nb, area])
353
+ most1 = coverage[-1]; most2 = coverage[-2]
354
+ least1 = coverage[0]; least2 = coverage[1]
355
+ if biomask is not None:
356
+ bio_sort = np.argsort(self.combination_features[:self.saved_csc_nb, biosum])
357
+ bio1 = bio_sort[-1]; bio2 = bio_sort[-2]
358
+ if backmask is not None:
359
+ back_sort = np.argsort(self.combination_features[:self.saved_csc_nb, backsum])
360
+ back1 = back_sort[-1]; back2 = back_sort[-2]
361
+
362
+ # Try a logical And between the most covered images
363
+ # Should only need one instanciation
364
+ process_i = ProcessFirstImage(
365
+ [self, False, False, None, several_blob_per_arena, sample_number, spot_size, kmeans_clust_nb, biomask, backmask, None])
366
+ process_i.binary_image = np.logical_and(self.saved_images_list[most1], self.saved_images_list[most2]).astype(np.uint8)
367
+ process_i.image = self.converted_images_list[most1]
368
+ process_i.process_binary_image()
369
+ process_i.csc_dict = {list(self.saved_color_space_list[most1].keys())[0]: self.combination_features[most1, :3],
370
+ "logical": "And",
371
+ list(self.saved_color_space_list[most2].keys())[0] + "2": self.combination_features[most2, :3]}
372
+ process_i.unaltered_concomp_nb = np.min(self.combination_features[(most1, most2), unaltered_cc_nb])
373
+ process_i.total_area = process_i.binary_image.sum()
374
+ self.save_combination_features(process_i)
375
+ process_i.image = self.converted_images_list[least1]
376
+ process_i.binary_image = np.logical_or(self.saved_images_list[least1], self.saved_images_list[least2]).astype(np.uint8)
377
+ process_i.process_binary_image()
378
+ process_i.csc_dict = {list(self.saved_color_space_list[least1].keys())[0]: self.combination_features[least1, :3],
379
+ "logical": "Or",
380
+ list(self.saved_color_space_list[least2].keys())[0] + "2": self.combination_features[least2, :3]}
381
+ process_i.unaltered_concomp_nb = np.max(self.combination_features[(least1, least2), unaltered_cc_nb])
382
+ process_i.total_area = process_i.binary_image.sum()
383
+ self.save_combination_features(process_i)
384
+
385
+ # self.save_combination_features(csc_dict, unaltered_concomp_nb, self.binary_image.sum(), biomask, backmask)
386
+
387
+ # If most images are very low in biosum or backsum, try to mix them together to improve that score
388
+ # Do a logical And between the two best biomasks
389
+ if biomask is not None:
390
+ if not np.all(np.isin((bio1, bio2), (most1, most2))):
391
+ process_i.image = self.converted_images_list[bio1]
392
+ process_i.binary_image = np.logical_and(self.saved_images_list[bio1], self.saved_images_list[bio2]).astype(
393
+ np.uint8)
394
+ process_i.process_binary_image()
395
+ process_i.csc_dict = {list(self.saved_color_space_list[bio1].keys())[0]: self.combination_features[bio1, :3],
396
+ "logical": "And",
397
+ list(self.saved_color_space_list[bio2].keys())[0] + "2": self.combination_features[bio2,:3]}
398
+ process_i.unaltered_concomp_nb = np.min(self.combination_features[(bio1, bio2), unaltered_cc_nb])
399
+ process_i.total_area = process_i.binary_image.sum()
400
+
401
+ self.save_combination_features(process_i)
402
+
403
+ # Do a logical And between the two best backmask
404
+ if backmask is not None:
405
+
406
+ if not np.all(np.isin((back1, back2), (most1, most2))):
407
+ process_i.image = self.converted_images_list[back1]
408
+ process_i.binary_image = np.logical_and(self.saved_images_list[back1], self.saved_images_list[back2]).astype(
409
+ np.uint8)
410
+ process_i.process_binary_image()
411
+ process_i.csc_dict = {list(self.saved_color_space_list[back1].keys())[0]: self.combination_features[back1, :3],
412
+ "logical": "And",
413
+ list(self.saved_color_space_list[back2].keys())[0] + "2": self.combination_features[back2,:3]}
414
+ process_i.unaltered_concomp_nb = np.min(self.combination_features[(back1, back2), unaltered_cc_nb])
415
+ process_i.total_area = process_i.binary_image.sum()
416
+ self.save_combination_features(process_i)
417
+ # Do a logical Or between the best biomask and the best backmask
418
+ if biomask is not None and backmask is not None:
419
+ if not np.all(np.isin((bio1, back1), (least1, least2))):
420
+ process_i.image = self.converted_images_list[bio1]
421
+ process_i.binary_image = np.logical_and(self.saved_images_list[bio1], self.saved_images_list[back1]).astype(
422
+ np.uint8)
423
+ process_i.process_binary_image()
424
+ process_i.csc_dict = {list(self.saved_color_space_list[bio1].keys())[0]: self.combination_features[bio1, :3],
425
+ "logical": "Or",
426
+ list(self.saved_color_space_list[back1].keys())[0] + "2": self.combination_features[back1, :3]}
427
+ process_i.unaltered_concomp_nb = np.max(self.combination_features[(bio1, back1), unaltered_cc_nb])
428
+ # self.save_combination_features(csc_dict, unaltered_concomp_nb, self.binary_image.sum(), biomask,
429
+ # backmask)
430
+ process_i.total_area = self.binary_image.sum()
431
+ self.save_combination_features(process_i)
432
+
433
+ if self.save_combination_thread.is_alive():
434
+ self.save_combination_thread.join()
435
+ self.combination_features = self.combination_features[:self.saved_csc_nb, :]
436
+ # Only keep the row that filled conditions
437
+ # Save all combinations if they fulfill the following conditions:
438
+ # - Their conncomp number is lower than 3 times the smaller conncomp number.
439
+ # - OR The minimal area variations
440
+ # - OR The minimal width variations
441
+ # - OR The minimal height variations
442
+ # - AND/OR their segmentation fits with biomask and backmask
443
+ width_std_fit = self.combination_features[:, width_std] == np.min(self.combination_features[:, width_std])
444
+ height_std_fit = self.combination_features[:, height_std] == np.min(self.combination_features[:, height_std])
445
+ area_std_fit = self.combination_features[:, area_std] < np.min(self.combination_features[:, area_std]) * 10
446
+ fit = np.logical_or(np.logical_or(width_std_fit, height_std_fit), area_std_fit)
447
+ biomask_fit = np.ones(self.saved_csc_nb, dtype=bool)
448
+ backmask_fit = np.ones(self.saved_csc_nb, dtype=bool)
449
+ if biomask is not None or backmask is not None:
450
+ if biomask is not None:
451
+ biomask_fit = self.combination_features[:, biosum] > 0.9 * len(biomask[0])
452
+ if backmask is not None:
453
+ backmask_fit = self.combination_features[:, backsum] > 0.9 * len(backmask[0])
454
+ # First test a logical OR between the precedent options and the mask fits.
455
+ fit = np.logical_or(fit, np.logical_and(biomask_fit, backmask_fit))
456
+ # If this is not stringent enough, use a logical AND and increase progressively the proportion of pixels that
457
+ # must match the biomask and the backmask
458
+ if np.sum(fit) > 5:
459
+ to_add = 0
460
+ while np.sum(fit) > 5 and to_add <= 0.25:
461
+ if biomask is not None:
462
+ biomask_fit = self.combination_features[:, biosum] > (0.75 + to_add) * len(biomask[0])
463
+ if backmask is not None:
464
+ backmask_fit = self.combination_features[:, backsum] > (0.75 + to_add) * len(backmask[0])
465
+ test_fit = np.logical_and(fit, np.logical_and(biomask_fit, backmask_fit))
466
+ if np.sum(test_fit) != 0:
467
+ fit = test_fit
468
+ to_add += 0.05
469
+ else:
470
+ self.combination_features = self.combination_features[:self.saved_csc_nb, :]
471
+ fit = np.array([True])
472
+ # If saved_csc_nb is too low, try bool operators to mix them together to fill holes for instance
473
+ # Order the table according to the number of shapes that have been removed by filters
474
+ # cc_efficiency_order = np.argsort(self.combination_features[:, unaltered_cc_nb] - self.combination_features[:, cc_nb])
475
+ cc_efficiency_order = np.argsort(self.combination_features[:, area_std])
476
+ # Save and return a dictionnary containing the selected color space combinations
477
+ # and their corresponding binary images
478
+
479
+ # first_im_combinations = [i for i in np.arange(fit.sum())]
480
+ self.im_combinations = []
481
+ for saved_csc in cc_efficiency_order:
482
+ if fit[saved_csc]:
483
+ self.im_combinations.append({})
484
+ # self.im_combinations.append({})
485
+ # self.im_combinations[len(self.im_combinations) - 1]["csc"] = self.saved_color_space_list[saved_csc]
486
+ self.im_combinations[len(self.im_combinations) - 1]["csc"] = {}
487
+ self.im_combinations[len(self.im_combinations) - 1]["csc"]['logical'] = 'None'
488
+ for k, v in self.saved_color_space_list[saved_csc].items():
489
+ self.im_combinations[len(self.im_combinations) - 1]["csc"][k] = v
490
+ # self.im_combinations[len(self.im_combinations) - 1]["csc"] = {list(self.saved_color_space_list[saved_csc])[0]: self.combination_features[saved_csc, :3]}
491
+
492
+ if backmask is not None:
493
+ shape_number, shapes = cv2.connectedComponents(self.saved_images_list[saved_csc], connectivity=8)
494
+ if np.any(shapes[backmask]):
495
+ shapes[np.isin(shapes, np.unique(shapes[backmask]))] = 0
496
+ self.saved_images_list[saved_csc] = (shapes > 0).astype(np.uint8)
497
+ if biomask is not None:
498
+ self.saved_images_list[saved_csc][biomask] = 1
499
+ if backmask is not None or biomask is not None:
500
+ self.combination_features[saved_csc, cc_nb], shapes = cv2.connectedComponents(self.saved_images_list[saved_csc], connectivity=8)
501
+ self.combination_features[saved_csc, cc_nb] -= 1
502
+ self.im_combinations[len(self.im_combinations) - 1]["binary_image"] = self.saved_images_list[saved_csc]
503
+ self.im_combinations[len(self.im_combinations) - 1]["shape_number"] = self.combination_features[saved_csc, cc_nb]
504
+ self.im_combinations[len(self.im_combinations) - 1]["converted_image"] = self.converted_images_list[saved_csc]
505
+
506
+ # logging.info(default_timer()-tic)
507
+ self.saved_color_space_list = []
508
+ self.saved_images_list = None
509
+ self.converted_images_list = None
510
+ self.combination_features = None
511
+
512
+ # def save_combination_features(self, process_i):
513
+ # if self.save_combination_thread.is_alive():
514
+ # self.save_combination_thread.join()
515
+ # self.save_combination_thread = SaveCombinationThread(self)
516
+ # self.save_combination_thread.process_i = process_i
517
+ # self.save_combination_thread.start()
518
+
519
+ def save_combination_features(self, process_i):
520
+ self.saved_images_list.append(process_i.validated_shapes)
521
+ self.converted_images_list.append(np.round(process_i.image).astype(np.uint8))
522
+ self.saved_color_space_list.append(process_i.csc_dict)
523
+ self.combination_features[self.saved_csc_nb, :3] = list(process_i.csc_dict.values())[0]
524
+ self.combination_features[
525
+ self.saved_csc_nb, 3] = process_i.unaltered_concomp_nb - 1 # unaltered_cc_nb
526
+ self.combination_features[self.saved_csc_nb, 4] = process_i.shape_number # cc_nb
527
+ self.combination_features[self.saved_csc_nb, 5] = process_i.total_area # area
528
+ self.combination_features[self.saved_csc_nb, 6] = np.std(process_i.stats[1:, 2]) # width_std
529
+ self.combination_features[self.saved_csc_nb, 7] = np.std(process_i.stats[1:, 3]) # height_std
530
+ self.combination_features[self.saved_csc_nb, 8] = np.std(process_i.stats[1:, 4]) # area_std
531
+ if process_i.biomask is not None:
532
+ self.combination_features[self.saved_csc_nb, 9] = np.sum(
533
+ process_i.validated_shapes[process_i.biomask[0], process_i.biomask[1]])
534
+ if process_i.backmask is not None:
535
+ self.combination_features[self.saved_csc_nb, 10] = np.sum(
536
+ (1 - process_i.validated_shapes)[process_i.backmask[0], process_i.backmask[1]])
537
+ self.saved_csc_nb += 1
538
+
539
+ def update_current_images(self, current_combination_id):
540
+ self.image = self.im_combinations[current_combination_id]["converted_image"]
541
+ self.validated_shapes = self.im_combinations[current_combination_id]["binary_image"]
542
+
543
+ def find_last_im_csc(self, concomp_nb, total_surfarea, max_shape_size, out_of_arenas=None, ref_image=None,
544
+ subtract_background=None, kmeans_clust_nb=None, biomask=None, backmask=None,
545
+ color_space_dictionaries=None, carefully=False):
546
+ if len(self.all_c_spaces) == 0:
547
+ self.all_c_spaces = get_color_spaces(self.bgr)
548
+ if color_space_dictionaries is None:
549
+ if carefully:
550
+ colorspace_list = TList(("bgr", "lab", "hsv", "luv", "hls", "yuv"))
551
+ else:
552
+ colorspace_list = TList(("lab", "hsv"))
553
+ color_space_dictionaries = TList()
554
+ for i, c_space in enumerate(colorspace_list):
555
+ for i in np.arange(3):
556
+ channels = np.array((0, 0, 0), dtype=np.int8)
557
+ channels[i] = 1
558
+ csc_dict = TDict()
559
+ csc_dict[c_space] = channels
560
+ color_space_dictionaries.append(csc_dict)
561
+ if ref_image is not None:
562
+ ref_image = cv2.dilate(ref_image, cross_33)
563
+ else:
564
+ ref_image = np.ones(self.bgr.shape[:2], dtype=np.uint8)
565
+ if out_of_arenas is not None:
566
+ out_of_arenas_threshold = 0.01 * out_of_arenas.sum()
567
+ else:
568
+ out_of_arenas = np.zeros(self.bgr.shape[:2], dtype=np.uint8)
569
+ out_of_arenas_threshold = 1
570
+ self.combination_features = np.zeros((len(color_space_dictionaries) + 50, 9), dtype=np.uint32)
571
+ cc_nb_idx, area_idx, out_of_arenas_idx, surf_in_common_idx, biosum_idx, backsum_idx = 3, 4, 5, 6, 7, 8
572
+ self.saved_images_list = TList()
573
+ self.converted_images_list = TList()
574
+ self.saved_color_space_list = list()
575
+ self.saved_csc_nb = 0
576
+ self.save_combination_thread = SaveCombinationThread(self)
577
+
578
+ # One channel processing
579
+ potentials = TDict()
580
+ for csc_dict in color_space_dictionaries:
581
+ self.image = combine_color_spaces(csc_dict, self.all_c_spaces, subtract_background)
582
+ # self.generate_color_space_combination(c_space_dict, subtract_background)
583
+ if kmeans_clust_nb is not None and (biomask is not None or backmask is not None):
584
+ self.kmeans(kmeans_clust_nb, biomask, backmask)
585
+ else:
586
+ self.binary_image = otsu_thresholding(self.image)
587
+ surf = np.sum(self.binary_image)
588
+ if surf < total_surfarea:
589
+ # nb, shapes = cv2.connectedComponents(oia.binary_image)
590
+ nb, shapes = cv2.connectedComponents(self.binary_image)
591
+ # outside_pixels = np.sum(oia.binary_image * out_of_arenas)
592
+ outside_pixels = np.sum(self.binary_image * out_of_arenas)
593
+ if outside_pixels < out_of_arenas_threshold:
594
+ if (nb > concomp_nb[0]) and (nb < concomp_nb[1]):
595
+ # in_common = np.sum(ref_image * oia.binary_image)
596
+ in_common = np.sum(ref_image * self.binary_image)
597
+ if in_common > 0:
598
+ nb, shapes, stats, centroids = cv2.connectedComponentsWithStats(self.binary_image)
599
+ nb -= 1
600
+ if np.all(np.sort(stats[:, 4])[:-1] < max_shape_size):
601
+ # oia.viewing()
602
+ c_space = list(csc_dict.keys())[0]
603
+ self.converted_images_list.append(self.image)
604
+ self.saved_images_list.append(self.binary_image)
605
+ self.saved_color_space_list.append(csc_dict)
606
+ self.combination_features[self.saved_csc_nb, :3] = csc_dict[c_space]
607
+ self.combination_features[self.saved_csc_nb, cc_nb_idx] = nb
608
+ self.combination_features[self.saved_csc_nb, area_idx] = surf
609
+ self.combination_features[self.saved_csc_nb, out_of_arenas_idx] = outside_pixels
610
+ self.combination_features[self.saved_csc_nb, surf_in_common_idx] = in_common
611
+ if biomask is not None:
612
+ self.combination_features[self.saved_csc_nb, biosum_idx] = np.sum(
613
+ self.binary_image[biomask[0], biomask[1]])
614
+ if backmask is not None:
615
+ self.combination_features[self.saved_csc_nb, backsum_idx] = np.sum(
616
+ (1 - self.binary_image)[backmask[0], backmask[1]])
617
+ if np.isin(c_space, list(potentials.keys())):
618
+ potentials[c_space] += csc_dict[c_space]
619
+ else:
620
+ potentials[c_space] = csc_dict[c_space]
621
+ self.saved_csc_nb += 1
622
+ if len(potentials) > 0:
623
+ # All combination processing
624
+
625
+ # Add a combination of all selected channels :
626
+ self.saved_color_space_list.append(potentials)
627
+ # all_potential_combinations.append(potentials)
628
+ self.image = combine_color_spaces(potentials, self.all_c_spaces, subtract_background)
629
+ # self.generate_color_space_combination(potentials, subtract_background)
630
+ if kmeans_clust_nb is not None and (biomask is not None or backmask is not None):
631
+ self.kmeans(kmeans_clust_nb, biomask, backmask)
632
+ else:
633
+ self.binary_image = otsu_thresholding(self.image)
634
+ # self.thresholding()
635
+ surf = self.binary_image.sum()
636
+ nb, shapes = cv2.connectedComponents(self.binary_image)
637
+ nb -= 1
638
+ outside_pixels = np.sum(self.binary_image * out_of_arenas)
639
+ in_common = np.sum(ref_image * self.binary_image)
640
+ self.converted_images_list.append(self.image)
641
+ self.saved_images_list.append(self.binary_image)
642
+ self.saved_color_space_list.append(potentials)
643
+ self.combination_features[self.saved_csc_nb, :3] = list(potentials.values())[0]
644
+ self.combination_features[self.saved_csc_nb, cc_nb_idx] = nb
645
+ self.combination_features[self.saved_csc_nb, area_idx] = surf
646
+ self.combination_features[self.saved_csc_nb, out_of_arenas_idx] = outside_pixels
647
+ self.combination_features[self.saved_csc_nb, surf_in_common_idx] = in_common
648
+ if biomask is not None:
649
+ self.combination_features[self.saved_csc_nb, biosum_idx] = np.sum(
650
+ self.binary_image[biomask[0], biomask[1]])
651
+ if backmask is not None:
652
+ self.combination_features[self.saved_csc_nb, backsum_idx] = np.sum(
653
+ (1 - self.binary_image)[backmask[0], backmask[1]])
654
+ self.saved_csc_nb += 1
655
+ # current = {"total_area": surf, "concomp_nb": nb, "out_of_arenas": outside_pixels,
656
+ # "surf_in_common": in_common}
657
+ # combination_features = combination_features.append(current, ignore_index=True)
658
+
659
+ # All combination processing
660
+ # Try to remove color space one by one
661
+ i = 0
662
+ original_length = len(potentials)
663
+ while np.logical_and(len(potentials) > 1, i < original_length // 2):
664
+ color_space_to_remove = TList()
665
+ # The while loop until one col space remains or the removal of one implies a strong enough area change
666
+ previous_c_space = list(potentials.keys())[-1]
667
+ for c_space in potentials.keys():
668
+ try_potentials = potentials.copy()
669
+ try_potentials.pop(c_space)
670
+ if i > 0:
671
+ try_potentials.pop(previous_c_space)
672
+ self.image = combine_color_spaces(try_potentials, self.all_c_spaces, subtract_background)
673
+ # self.generate_color_space_combination(try_potentials, subtract_background)
674
+ if kmeans_clust_nb is not None and (biomask is not None or backmask is not None):
675
+ self.kmeans(kmeans_clust_nb, biomask, backmask)
676
+ else:
677
+ self.binary_image = otsu_thresholding(self.image)
678
+ # self.thresholding()
679
+ surf = np.sum(self.binary_image)
680
+ if surf < total_surfarea:
681
+ nb, shapes = cv2.connectedComponents(self.binary_image)
682
+ outside_pixels = np.sum(self.binary_image * out_of_arenas)
683
+ if outside_pixels < out_of_arenas_threshold:
684
+ if (nb > concomp_nb[0]) and (nb < concomp_nb[1]):
685
+ in_common = np.sum(ref_image * self.binary_image)
686
+ if in_common > 0:
687
+ nb, shapes, stats, centroids = cv2.connectedComponentsWithStats(self.binary_image)
688
+ nb -= 1
689
+ if np.all(np.sort(stats[:, 4])[:-1] < max_shape_size):
690
+ # If a color space remove fits in the requirements, we store its values
691
+ self.converted_images_list.append(self.image)
692
+ self.saved_images_list.append(self.binary_image)
693
+ self.saved_color_space_list.append(try_potentials)
694
+ self.combination_features[self.saved_csc_nb, cc_nb_idx] = nb
695
+ self.combination_features[self.saved_csc_nb, area_idx] = surf
696
+ self.combination_features[self.saved_csc_nb, out_of_arenas_idx] = outside_pixels
697
+ self.combination_features[self.saved_csc_nb, surf_in_common_idx] = in_common
698
+ if biomask is not None:
699
+ self.combination_features[self.saved_csc_nb, biosum_idx] = np.sum(
700
+ self.binary_image[biomask[0], biomask[1]])
701
+ if backmask is not None:
702
+ self.combination_features[self.saved_csc_nb, backsum_idx] = np.sum(
703
+ (1 - self.binary_image)[backmask[0], backmask[1]])
704
+ self.saved_csc_nb += 1
705
+ # all_potential_combinations.append(try_potentials)
706
+ # current = {"total_area": surf, "concomp_nb": nb, "out_of_arenas": outside_pixels,
707
+ # "surf_in_common": in_common}
708
+ # combination_features = combination_features.append(current, ignore_index=True)
709
+ color_space_to_remove.append(c_space)
710
+ if i > 0:
711
+ color_space_to_remove.append(previous_c_space)
712
+ # If it does not (if it did not pass every "if" layers), we definitely remove that color space
713
+ previous_c_space = c_space
714
+ color_space_to_remove = np.unique(color_space_to_remove)
715
+ for remove_col_space in color_space_to_remove:
716
+ potentials.pop(remove_col_space)
717
+ i += 1
718
+ if np.logical_and(len(potentials) > 0, i > 1):
719
+ self.converted_images_list.append(self.image)
720
+ self.saved_images_list.append(self.binary_image)
721
+ self.saved_color_space_list.append(potentials)
722
+ self.combination_features[self.saved_csc_nb, :3] = list(potentials.values())[0]
723
+ self.combination_features[self.saved_csc_nb, cc_nb_idx] = nb
724
+ self.combination_features[self.saved_csc_nb, area_idx] = surf
725
+ self.combination_features[self.saved_csc_nb, out_of_arenas_idx] = outside_pixels
726
+ self.combination_features[self.saved_csc_nb, surf_in_common_idx] = in_common
727
+ if biomask is not None:
728
+ self.combination_features[self.saved_csc_nb, biosum_idx] = np.sum(
729
+ self.binary_image[biomask[0], biomask[1]])
730
+ if backmask is not None:
731
+ self.combination_features[self.saved_csc_nb, backsum_idx] = np.sum(
732
+ (1 - self.binary_image)[backmask[0], backmask[1]])
733
+ self.saved_csc_nb += 1
734
+ # all_potential_combinations.append(potentials)
735
+ # current = {"total_area": surf, "concomp_nb": nb, "out_of_arenas": outside_pixels,
736
+ # "surf_in_common": in_common}
737
+ # combination_features = combination_features.append(current, ignore_index=True)
738
+
739
+ self.combination_features = self.combination_features[:self.saved_csc_nb, :]
740
+ # Among all potentials, select the best one, according to criterion decreasing in importance
741
+ # a = combination_features.sort_values(by=["surf_in_common"], ascending=False)
742
+ # self.channel_combination = all_potential_combinations[a[:1].index[0]]
743
+ cc_efficiency_order = np.argsort(self.combination_features[:, surf_in_common_idx])
744
+
745
+ # Save and return a dictionnary containing the selected color space combinations
746
+ # and their corresponding binary images
747
+
748
+ self.im_combinations = []
749
+ for saved_csc in cc_efficiency_order:
750
+ if len(self.saved_color_space_list[saved_csc]) > 0:
751
+ self.im_combinations.append({})
752
+ self.im_combinations[len(self.im_combinations) - 1]["csc"] = {}
753
+ self.im_combinations[len(self.im_combinations) - 1]["csc"]['logical'] = 'None'
754
+ for k, v in self.saved_color_space_list[saved_csc].items():
755
+ self.im_combinations[len(self.im_combinations) - 1]["csc"][k] = v
756
+ self.im_combinations[len(self.im_combinations) - 1]["binary_image"] = self.saved_images_list[saved_csc]
757
+ self.im_combinations[len(self.im_combinations) - 1]["converted_image"] = np.round(self.converted_images_list[
758
+ saved_csc]).astype(np.uint8)
759
+ self.saved_color_space_list = []
760
+ self.saved_images_list = None
761
+ self.converted_images_list = None
762
+ self.combination_features = None
763
+
764
+ """
765
+ Thresholding is a very simple and fast segmentation method. Kmeans can be implemented in a function bellow
766
+ """
767
+ def thresholding(self, luminosity_threshold=None, lighter_background=None):
768
+ if luminosity_threshold is not None:
769
+ binarymg = np.zeros(self.image.shape, dtype=np.uint8)
770
+ if lighter_background:
771
+ binarymg[self.image < luminosity_threshold] = 1
772
+ else:
773
+ binarymg[self.image > luminosity_threshold] = 1
774
+ else:
775
+ ret, binarymg = cv2.threshold(self.image, 0, 1, cv2.THRESH_OTSU)
776
+ #binarymg = binarymg - 1
777
+ # Make sure that blobs are 1 and background is 0
778
+ if np.sum(binarymg) > np.sum(1 - binarymg):
779
+ binarymg = 1 - binarymg
780
+ self.binary_image = binarymg
781
+
782
+ def kmeans(self, cluster_number, biomask=None, backmask=None, logical='None', bio_label=None, bio_label2=None):
783
+ image = self.image.reshape((-1, 1))
784
+ image = np.float32(image)
785
+ criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
786
+ compactness, label, center = cv2.kmeans(image, cluster_number, None, criteria, attempts=10, flags=cv2.KMEANS_RANDOM_CENTERS)
787
+ kmeans_image = np.uint8(label.flatten().reshape(self.image.shape[:2]))
788
+ sum_per_label = np.zeros(cluster_number)
789
+ self.binary_image = np.zeros(self.image.shape[:2], np.uint8)
790
+ if self.previous_binary_image is not None:
791
+ binary_images = []
792
+ image_scores = np.zeros(cluster_number, np.uint64)
793
+ for i in range(cluster_number):
794
+ binary_image_i = np.zeros(self.image.shape[:2], np.uint8)
795
+ binary_image_i[np.nonzero(kmeans_image == i)] = 1
796
+ image_scores[i] = (binary_image_i * self.previous_binary_image).sum()
797
+ binary_images.append(binary_image_i)
798
+ self.binary_image[np.nonzero(kmeans_image == np.argmax(image_scores))] = 1
799
+ elif bio_label is not None:
800
+ self.binary_image[np.nonzero(kmeans_image == bio_label)] = 1
801
+ self.bio_label = bio_label
802
+ else:
803
+ if biomask is not None:
804
+ all_labels = kmeans_image[biomask[0], biomask[1]]
805
+ for i in range(cluster_number):
806
+ sum_per_label[i] = (all_labels == i).sum()
807
+ self.bio_label = np.nonzero(sum_per_label == np.max(sum_per_label))
808
+ elif backmask is not None:
809
+ all_labels = kmeans_image[backmask[0], backmask[1]]
810
+ for i in range(cluster_number):
811
+ sum_per_label[i] = (all_labels == i).sum()
812
+ self.bio_label = np.nonzero(sum_per_label == np.min(sum_per_label))
813
+ else:
814
+ for i in range(cluster_number):
815
+ sum_per_label[i] = (kmeans_image == i).sum()
816
+ self.bio_label = np.nonzero(sum_per_label == np.min(sum_per_label))
817
+ self.binary_image[np.nonzero(kmeans_image == self.bio_label)] = 1
818
+
819
+ if logical != 'None':
820
+ image = self.image2.reshape((-1, 1))
821
+ image = np.float32(image)
822
+ criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
823
+ compactness, label, center = cv2.kmeans(image, cluster_number, None, criteria, attempts=10,
824
+ flags=cv2.KMEANS_RANDOM_CENTERS)
825
+ kmeans_image = np.uint8(label.flatten().reshape(self.image.shape[:2]))
826
+ sum_per_label = np.zeros(cluster_number)
827
+ self.binary_image2 = np.zeros(self.image.shape[:2], np.uint8)
828
+ if self.previous_binary_image is not None:
829
+ binary_images = []
830
+ image_scores = np.zeros(cluster_number, np.uint64)
831
+ for i in range(cluster_number):
832
+ binary_image_i = np.zeros(self.image.shape[:2], np.uint8)
833
+ binary_image_i[np.nonzero(kmeans_image == i)] = 1
834
+ image_scores[i] = (binary_image_i * self.previous_binary_image).sum()
835
+ binary_images.append(binary_image_i)
836
+ self.binary_image2[np.nonzero(kmeans_image == np.argmax(image_scores))] = 1
837
+ elif bio_label2 is not None:
838
+ self.binary_image2[np.nonzero(kmeans_image == bio_label2)] = 1
839
+ self.bio_label2 = bio_label2
840
+ else:
841
+ if biomask is not None:
842
+ all_labels = kmeans_image[biomask[0], biomask[1]]
843
+ for i in range(cluster_number):
844
+ sum_per_label[i] = (all_labels == i).sum()
845
+ self.bio_label2 = np.nonzero(sum_per_label == np.max(sum_per_label))
846
+ elif backmask is not None:
847
+ all_labels = kmeans_image[backmask[0], backmask[1]]
848
+ for i in range(cluster_number):
849
+ sum_per_label[i] = (all_labels == i).sum()
850
+ self.bio_label2 = np.nonzero(sum_per_label == np.min(sum_per_label))
851
+ else:
852
+ for i in range(cluster_number):
853
+ sum_per_label[i] = (kmeans_image == i).sum()
854
+ self.bio_label2 = np.nonzero(sum_per_label == np.min(sum_per_label))
855
+ self.binary_image2[np.nonzero(kmeans_image == self.bio_label2)] = 1
856
+
857
+ def binarize_k_means_product(self, grey_idx):
858
+ binarization = np.zeros_like(self.binary_image)
859
+ binarization[np.nonzero(self.binary_image == grey_idx)] = 1
860
+ self.binary_image = binarization
861
+
862
+ def grid_segmentation(self, lighter_background, side_length=8, step=2, int_variation_thresh=20, mask=None):
863
+ """
864
+ Segment small squares of the images to detect local intensity valleys
865
+ This method segment the image locally using otsu thresholding on a rolling window
866
+ :param side_length: The size of the window to detect the blobs
867
+ :type side_length: uint8
868
+ :param step:
869
+ :type step: uint8
870
+ :return:
871
+ """
872
+ if len(self.image.shape) == 3:
873
+ print("Image is not Grayscale")
874
+ if mask is None:
875
+ min_y = 0
876
+ min_x = 0
877
+ y_size = self.image.shape[0]
878
+ x_size = self.image.shape[1]
879
+ max_y = y_size + 1
880
+ max_x = x_size + 1
881
+ mask = np.ones_like(self.image)
882
+ else:
883
+ y, x = np.nonzero(mask)
884
+ min_y = np.min(y)
885
+ if (min_y - 20) >= 0:
886
+ min_y -= 20
887
+ else:
888
+ min_y = 0
889
+ max_y = np.max(y) + 1
890
+ if (max_y + 20) < mask.shape[0]:
891
+ max_y += 20
892
+ else:
893
+ max_y = mask.shape[0] - 1
894
+ min_x = np.min(x)
895
+ if (min_x - 20) >= 0:
896
+ min_x -= 20
897
+ else:
898
+ min_x = 0
899
+ max_x = np.max(x) + 1
900
+ if (max_x + 20) < mask.shape[1]:
901
+ max_x += 20
902
+ else:
903
+ max_x = mask.shape[1] - 1
904
+ y_size = max_y - min_y
905
+ x_size = max_x - min_x
906
+ grid_image = np.zeros((y_size, x_size), np.uint64)
907
+ homogeneities = np.zeros((y_size, x_size), np.uint64)
908
+ cropped_mask = mask[min_y:max_y, min_x:max_x]
909
+ cropped_image = self.image[min_y:max_y, min_x:max_x]
910
+ # will be more efficient if it only loops over a zoom on self.mask == 1
911
+ for to_add in np.arange(0, side_length, step):
912
+ y_windows = np.arange(0, y_size, side_length)
913
+ x_windows = np.arange(0, x_size, side_length)
914
+ y_windows += to_add
915
+ x_windows += to_add
916
+ for y_start in y_windows:
917
+ # y_start = 4
918
+ if y_start < self.image.shape[0]:
919
+ y_end = y_start + side_length
920
+ if y_end < self.image.shape[0]:
921
+ for x_start in x_windows:
922
+ if x_start < self.image.shape[1]:
923
+ x_end = x_start + side_length
924
+ if x_end < self.image.shape[1]:
925
+ if np.any(cropped_mask[y_start:y_end, x_start:x_end]):
926
+ potential_detection = cropped_image[y_start:y_end, x_start:x_end]
927
+ if np.any(potential_detection):
928
+ if np.ptp(potential_detection[np.nonzero(potential_detection)]) < int_variation_thresh:
929
+ homogeneities[y_start:y_end, x_start:x_end] += 1
930
+
931
+ threshold = get_otsu_threshold(potential_detection)
932
+ if lighter_background:
933
+ net_coord = np.nonzero(potential_detection < threshold)
934
+ else:
935
+ net_coord = np.nonzero(potential_detection > threshold)
936
+ grid_image[y_start + net_coord[0], x_start + net_coord[1]] += 1
937
+
938
+ self.binary_image = np.zeros(self.image.shape, np.uint8)
939
+ self.binary_image[min_y:max_y, min_x:max_x] = (grid_image >= (side_length // step)).astype(np.uint8)
940
+ self.binary_image[min_y:max_y, min_x:max_x][homogeneities >= (((side_length // step) // 2) + 1)] = 0
941
+
942
+
943
+ """
944
+ III/ Use validated shapes to exclude from analysis the image parts that are far from them
945
+ i.e. detect projected shape boundaries over both axis and determine crop coordinates
946
+ """
947
+ def get_crop_coordinates(self, are_zigzag=None):
948
+ logging.info("Project the image on the y axis to detect rows of arenas")
949
+ self.y_boundaries, y_max_sum = self.projection_to_get_peaks_boundaries(axis=1)
950
+ logging.info("Project the image on the x axis to detect columns of arenas")
951
+ self.x_boundaries, x_max_sum = self.projection_to_get_peaks_boundaries(axis=0)
952
+ logging.info("Get crop coordinates using the get_crop_coordinates method of OneImageAnalysis class")
953
+ row_number = len(np.nonzero(self.y_boundaries)[0]) // 2
954
+ col_number = len(np.nonzero(self.x_boundaries)[0]) // 2
955
+ if (x_max_sum / col_number) * 2 < (y_max_sum / row_number):
956
+ are_zigzag = "columns"
957
+ elif (x_max_sum / col_number) > (y_max_sum / row_number) * 2:
958
+ are_zigzag = "rows"
959
+ else:
960
+ are_zigzag = None
961
+ # here automatically determine if are zigzag
962
+ x_boundary_number = (self.x_boundaries == 1).sum()
963
+ if x_boundary_number > 1:
964
+ if x_boundary_number < 4:
965
+ x_interval = np.absolute(np.max(np.diff(np.where(self.x_boundaries == 1)[0]))) // 2
966
+ else:
967
+ if are_zigzag == "columns":
968
+ x_interval = np.absolute(np.max(np.diff(np.where(self.x_boundaries == 1)[0][::2]))) // 2
969
+ else:
970
+ x_interval = np.absolute(np.max(np.diff(np.where(self.x_boundaries == 1)[0]))) // 2
971
+ cx_min = np.where(self.x_boundaries == - 1)[0][0] - x_interval.astype(int)
972
+ cx_max = np.where(self.x_boundaries == 1)[0][col_number - 1] + x_interval.astype(int)
973
+ if cx_min < 0: cx_min = 0
974
+ if cx_max > len(self.x_boundaries): cx_max = len(self.x_boundaries) - 1
975
+ else:
976
+ cx_min = 0
977
+ cx_max = len(self.x_boundaries) - 1
978
+
979
+ y_boundary_number = (self.y_boundaries == 1).sum()
980
+ if y_boundary_number > 1:
981
+ if y_boundary_number < 4:
982
+ y_interval = np.absolute(np.max(np.diff(np.where(self.y_boundaries == 1)[0]))) // 2
983
+ else:
984
+ if are_zigzag == "rows":
985
+ y_interval = np.absolute(np.max(np.diff(np.where(self.y_boundaries == 1)[0][::2]))) // 2
986
+ else:
987
+ y_interval = np.absolute(np.max(np.diff(np.where(self.y_boundaries == 1)[0]))) // 2
988
+ cy_min = np.where(self.y_boundaries == - 1)[0][0] - y_interval.astype(int)
989
+ cy_max = np.where(self.y_boundaries == 1)[0][row_number - 1] + y_interval.astype(int)
990
+ if cy_min < 0: cy_min = 0
991
+ if cy_max > len(self.y_boundaries): cy_max = len(self.y_boundaries) - 1
992
+ else:
993
+ cy_min = 0
994
+ cy_max = len(self.y_boundaries) - 1
995
+
996
+ self.crop_coord = [cy_min, cy_max, cx_min, cx_max]
997
+ return are_zigzag
998
+ # plt.imshow(self.image)
999
+ #plt.scatter(cx_min,cy_min)
1000
+ #plt.scatter(cx_max, cy_max)
1001
+
1002
+ def projection_to_get_peaks_boundaries(self, axis):
1003
+ sums = np.sum(self.validated_shapes, axis)
1004
+ slopes = np.greater(sums, 0)
1005
+ slopes = np.append(0, np.diff(slopes))
1006
+ coord = np.nonzero(slopes)[0]
1007
+ for ci in np.arange(len(coord)):
1008
+ if ci % 2 == 0:
1009
+ slopes[coord[ci]] = - 1
1010
+ return slopes, sums.max()
1011
+
1012
+ def jackknife_cutting(self, changes):
1013
+ """
1014
+ This function compare the mean distance between each 1 in a vector of 0.
1015
+ Since a few irregular intervals affect less the median that the mean,
1016
+ It try to remove each 1, one by one to see if it reduce enough the difference between mean and median.
1017
+ If the standard error of that difference is higher than 2,
1018
+ we remove each point whose removal decrease that difference by half of the median of these differences.
1019
+ i.e. differences between jackkniffed means and original median of the distance between each 1.
1020
+ """
1021
+ indices = np.nonzero(changes)[0]
1022
+ indices_to_remove = np.zeros(len(indices), dtype=bool)
1023
+ # To test the impact of a removal, changes must contain at least four 1.
1024
+ if len(indices) > 3:
1025
+ jackknifed_mean = np.zeros(np.sum(changes == 1))
1026
+ for dot_i in np.arange(len(indices)):
1027
+ steep = changes == 1
1028
+ steep[indices[dot_i]] = False
1029
+ new_indices = np.where(steep == 1)[0]
1030
+ if dot_i != 0:
1031
+ new_indices[dot_i:] = indices[(dot_i + 1):] - (indices[dot_i] - indices[dot_i - 1])
1032
+ jackknifed_mean[dot_i] = np.mean(np.diff(new_indices))
1033
+ improving_cuts = np.absolute(jackknifed_mean - np.median(np.diff(indices)))
1034
+ if np.std(improving_cuts) > 2:
1035
+ improving_cuts = np.argwhere(improving_cuts < 0.5 * np.median(improving_cuts))
1036
+ indices_to_remove[improving_cuts] = 1
1037
+ return indices_to_remove
1038
+
1039
+ def automatically_crop(self, crop_coord):
1040
+ if not self.cropped:
1041
+ logging.info("Crop using the automatically_crop method of OneImageAnalysis class")
1042
+ self.cropped = True
1043
+ self.image = self.image[crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3], ...]
1044
+ self.bgr = deepcopy(self.bgr[crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3], ...])
1045
+ if len(self.all_c_spaces) > 0:
1046
+ self.all_c_spaces = get_color_spaces(self.bgr)
1047
+ if self.im_combinations is not None:
1048
+ for i in np.arange(len(self.im_combinations)):
1049
+ self.im_combinations[i]["binary_image"] = self.im_combinations[i]["binary_image"][crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3]]
1050
+ self.im_combinations[i]["converted_image"] = self.im_combinations[i]["converted_image"][crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3]]
1051
+ self.binary_image = self.binary_image[crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3]]
1052
+ if self.image2 is not None:
1053
+ self.image2 = self.image2[crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3], ...]
1054
+ if self.binary_image2 is not None:
1055
+ self.binary_image2 = self.binary_image2[crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3], ...]
1056
+ if self.subtract_background is not None:
1057
+ self.subtract_background = self.subtract_background[crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3], ...]
1058
+ if self.subtract_background2 is not None:
1059
+ self.subtract_background2 = self.subtract_background2[crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3], ...]
1060
+ self.validated_shapes = self.validated_shapes[crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3]]
1061
+
1062
+ self.y_boundaries, y_max_sum = self.projection_to_get_peaks_boundaries(axis=1)
1063
+ self.x_boundaries, x_max_sum = self.projection_to_get_peaks_boundaries(axis=0)
1064
+
1065
+