cellects 0.2.7__py3-none-any.whl → 0.3.0__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.
- cellects/core/cellects_threads.py +46 -184
- cellects/core/motion_analysis.py +74 -45
- cellects/core/one_image_analysis.py +233 -528
- cellects/core/program_organizer.py +157 -98
- cellects/core/script_based_run.py +13 -23
- cellects/gui/first_window.py +7 -4
- cellects/gui/image_analysis_window.py +45 -35
- cellects/gui/ui_strings.py +3 -2
- cellects/image_analysis/image_segmentation.py +21 -77
- cellects/image_analysis/morphological_operations.py +9 -13
- cellects/image_analysis/one_image_analysis_threads.py +312 -182
- cellects/image_analysis/shape_descriptors.py +1068 -1067
- cellects/utils/formulas.py +3 -1
- cellects/utils/load_display_save.py +1 -1
- {cellects-0.2.7.dist-info → cellects-0.3.0.dist-info}/METADATA +1 -1
- {cellects-0.2.7.dist-info → cellects-0.3.0.dist-info}/RECORD +20 -20
- {cellects-0.2.7.dist-info → cellects-0.3.0.dist-info}/LICENSE +0 -0
- {cellects-0.2.7.dist-info → cellects-0.3.0.dist-info}/WHEEL +0 -0
- {cellects-0.2.7.dist-info → cellects-0.3.0.dist-info}/entry_points.txt +0 -0
- {cellects-0.2.7.dist-info → cellects-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -21,18 +21,62 @@ from copy import deepcopy
|
|
|
21
21
|
import numpy as np
|
|
22
22
|
import cv2 # named opencv-python
|
|
23
23
|
import multiprocessing.pool as mp
|
|
24
|
-
from numba.typed import List
|
|
25
|
-
from numba.typed import Dict
|
|
24
|
+
from numba.typed import List
|
|
25
|
+
from numba.typed import Dict
|
|
26
26
|
from numpy.typing import NDArray
|
|
27
27
|
from typing import Tuple
|
|
28
|
+
import pandas as pd
|
|
29
|
+
from scipy.stats import rankdata
|
|
28
30
|
from skimage.measure import perimeter
|
|
29
31
|
from cellects.image_analysis.morphological_operations import cross_33, create_ellipse, spot_size_coefficients
|
|
30
|
-
from cellects.image_analysis.image_segmentation import generate_color_space_combination, get_color_spaces, extract_first_pc, combine_color_spaces, apply_filter, otsu_thresholding, get_otsu_threshold, kmeans, windowed_thresholding
|
|
31
|
-
from cellects.image_analysis.one_image_analysis_threads import
|
|
32
|
+
from cellects.image_analysis.image_segmentation import generate_color_space_combination, get_color_spaces, filter_dict, extract_first_pc, combine_color_spaces, apply_filter, otsu_thresholding, get_otsu_threshold, kmeans, windowed_thresholding
|
|
33
|
+
from cellects.image_analysis.one_image_analysis_threads import ProcessImage
|
|
32
34
|
from cellects.image_analysis.network_functions import NetworkDetection
|
|
33
35
|
from cellects.utils.formulas import bracket_to_uint8_image_contrast
|
|
34
36
|
from cellects.utils.utilitarian import split_dict, translate_dict
|
|
35
37
|
|
|
38
|
+
def init_params():
|
|
39
|
+
params = {}
|
|
40
|
+
# User set variables:
|
|
41
|
+
params['is_first_image']: bool = False
|
|
42
|
+
params['several_blob_per_arena']: bool = True
|
|
43
|
+
params['blob_nb']: int = None
|
|
44
|
+
params['blob_shape']: str = None
|
|
45
|
+
params['blob_size']: int = None
|
|
46
|
+
params['kmeans_clust_nb']: int = None
|
|
47
|
+
params['arenas_mask']: NDArray = None
|
|
48
|
+
params['ref_image']: NDArray = None
|
|
49
|
+
params['bio_mask']: Tuple = None
|
|
50
|
+
params['back_mask']: Tuple = None
|
|
51
|
+
params['filter_spec']: dict = {'filter1_type': "", 'filter1_param': [.5, 1.], 'filter2_type': "", 'filter2_param': [.5, 1.]}
|
|
52
|
+
# Computed before OneImageAnalysis usage:
|
|
53
|
+
|
|
54
|
+
# Computed in OneImageAnalysis usage:
|
|
55
|
+
params['con_comp_extent']: list = None
|
|
56
|
+
params['max_blob_size']: int = None
|
|
57
|
+
params['total_surface_area']: int = None
|
|
58
|
+
params['out_of_arenas_mask']: NDArray = None
|
|
59
|
+
params['subtract_background']: NDArray = None
|
|
60
|
+
params['are_zigzag']: str = None
|
|
61
|
+
return params
|
|
62
|
+
|
|
63
|
+
def make_one_dict_per_channel():
|
|
64
|
+
colorspace_list = ["bgr", "lab", "hsv", "luv", "hls", "yuv"]
|
|
65
|
+
one_dict_per_channel = List()
|
|
66
|
+
channels = np.array((1, 1, 1), dtype=np.int8)
|
|
67
|
+
csc_dict = Dict()
|
|
68
|
+
csc_dict["bgr"] = channels
|
|
69
|
+
one_dict_per_channel.append(csc_dict)
|
|
70
|
+
for c_space in colorspace_list:
|
|
71
|
+
for j in np.arange(3):
|
|
72
|
+
channels = np.array((0, 0, 0), dtype=np.int8)
|
|
73
|
+
channels[j] = 1
|
|
74
|
+
csc_dict = Dict()
|
|
75
|
+
csc_dict[c_space] = channels
|
|
76
|
+
one_dict_per_channel.append(csc_dict)
|
|
77
|
+
return one_dict_per_channel
|
|
78
|
+
|
|
79
|
+
one_dict_per_channel = make_one_dict_per_channel()
|
|
36
80
|
|
|
37
81
|
class OneImageAnalysis:
|
|
38
82
|
"""
|
|
@@ -48,7 +92,7 @@ class OneImageAnalysis:
|
|
|
48
92
|
|
|
49
93
|
ps: A viewing method displays the image before and after the most advanced modification made in instance
|
|
50
94
|
"""
|
|
51
|
-
def __init__(self, image, shape_number=
|
|
95
|
+
def __init__(self, image, shape_number: int=1):
|
|
52
96
|
self.image = image
|
|
53
97
|
if len(self.image.shape) == 2:
|
|
54
98
|
self.already_greyscale = True
|
|
@@ -72,9 +116,9 @@ class OneImageAnalysis:
|
|
|
72
116
|
self.subtract_background2 = None
|
|
73
117
|
self.im_combinations = None
|
|
74
118
|
self.bgr = image
|
|
75
|
-
self.colorspace_list =
|
|
119
|
+
self.colorspace_list = List(("bgr", "lab", "hsv", "luv", "hls", "yuv"))
|
|
76
120
|
self.spot_shapes = None
|
|
77
|
-
self.all_c_spaces =
|
|
121
|
+
self.all_c_spaces = Dict()
|
|
78
122
|
self.hsv = None
|
|
79
123
|
self.hls = None
|
|
80
124
|
self.lab = None
|
|
@@ -86,8 +130,8 @@ class OneImageAnalysis:
|
|
|
86
130
|
self.drift_mask_coord = None
|
|
87
131
|
self.saved_csc_nb = 0
|
|
88
132
|
|
|
89
|
-
def convert_and_segment(self, c_space_dict: dict, color_number=2,
|
|
90
|
-
|
|
133
|
+
def convert_and_segment(self, c_space_dict: dict, color_number=2, bio_mask: NDArray[np.uint8]=None,
|
|
134
|
+
back_mask: NDArray[np.uint8]=None, subtract_background: NDArray=None,
|
|
91
135
|
subtract_background2: NDArray=None, rolling_window_segmentation: dict=None,
|
|
92
136
|
lighter_background: bool=None,
|
|
93
137
|
allowed_window: NDArray=None, filter_spec: dict=None):
|
|
@@ -102,8 +146,8 @@ class OneImageAnalysis:
|
|
|
102
146
|
|
|
103
147
|
- `c_space_dict` (dict): Dictionary containing color spaces.
|
|
104
148
|
- `color_number` (int, optional): Number of colors to use in segmentation. Defaults to 2.
|
|
105
|
-
- `
|
|
106
|
-
- `
|
|
149
|
+
- `bio_mask` (NDArray[np.uint8], optional): Biomask for segmentation. Defaults to None.
|
|
150
|
+
- `back_mask` (NDArray[np.uint8], optional): Backmask for segmentation. Defaults to None.
|
|
107
151
|
- `subtract_background` (NDArray, optional): Background to subtract. Defaults to None.
|
|
108
152
|
- `subtract_background2` (NDArray, optional): Second background to subtract. Defaults to None.
|
|
109
153
|
- rolling_window_segmentation (dict, optional): Flag for grid segmentation. Defaults to None.
|
|
@@ -123,13 +167,13 @@ class OneImageAnalysis:
|
|
|
123
167
|
if len(all_c_spaces) > len(self.all_c_spaces):
|
|
124
168
|
self.all_c_spaces = all_c_spaces
|
|
125
169
|
|
|
126
|
-
self.segmentation(logical=c_space_dict['logical'], color_number=color_number,
|
|
127
|
-
|
|
170
|
+
self.segmentation(logical=c_space_dict['logical'], color_number=color_number, bio_mask=bio_mask,
|
|
171
|
+
back_mask=back_mask, rolling_window_segmentation=rolling_window_segmentation,
|
|
128
172
|
lighter_background=lighter_background, allowed_window=allowed_window, filter_spec=filter_spec)
|
|
129
173
|
|
|
130
174
|
|
|
131
|
-
def segmentation(self, logical: str='None', color_number: int=2,
|
|
132
|
-
|
|
175
|
+
def segmentation(self, logical: str='None', color_number: int=2, bio_mask: NDArray[np.uint8]=None,
|
|
176
|
+
back_mask: NDArray[np.uint8]=None, bio_label=None, bio_label2=None,
|
|
133
177
|
rolling_window_segmentation: dict=None, lighter_background: bool=None, allowed_window: Tuple=None,
|
|
134
178
|
filter_spec: dict=None):
|
|
135
179
|
"""
|
|
@@ -140,8 +184,8 @@ class OneImageAnalysis:
|
|
|
140
184
|
Options are 'Or', 'And', 'Xor'. Default is 'None'.
|
|
141
185
|
color_number (int): Number of colors to use in segmentation. Must be greater than 2
|
|
142
186
|
for kmeans clustering. Default is 2.
|
|
143
|
-
|
|
144
|
-
|
|
187
|
+
bio_mask (NDArray[np.uint8]): Binary mask for biological areas. Default is None.
|
|
188
|
+
back_mask (NDArray[np.uint8]): Binary mask for background areas. Default is None.
|
|
145
189
|
bio_label (Any): Label for biological features. Default is None.
|
|
146
190
|
bio_label2 (Any): Secondary label for biological features. Default is None.
|
|
147
191
|
rolling_window_segmentation (dict): Whether to perform grid segmentation. Default is None.
|
|
@@ -169,7 +213,7 @@ class OneImageAnalysis:
|
|
|
169
213
|
|
|
170
214
|
# 3. Do one of the three segmentation algorithms: kmeans, otsu, windowed
|
|
171
215
|
if color_number > 2:
|
|
172
|
-
binary_image, binary_image2, self.bio_label, self.bio_label2 = kmeans(greyscale, greyscale2, color_number,
|
|
216
|
+
binary_image, binary_image2, self.bio_label, self.bio_label2 = kmeans(greyscale, greyscale2, color_number, bio_mask, back_mask, logical, bio_label, bio_label2)
|
|
173
217
|
elif rolling_window_segmentation is not None and rolling_window_segmentation['do']:
|
|
174
218
|
binary_image = windowed_thresholding(greyscale, lighter_background, rolling_window_segmentation['side_len'],
|
|
175
219
|
rolling_window_segmentation['step'], rolling_window_segmentation['min_int_var'])
|
|
@@ -247,8 +291,8 @@ class OneImageAnalysis:
|
|
|
247
291
|
# self.adjust_to_drift_correction(c_space_dict['logical'])
|
|
248
292
|
self.check_if_image_border_attest_drift_correction()
|
|
249
293
|
self.convert_and_segment(c_space_dict, rolling_window_segmentation=None, allowed_window=self.drift_mask_coord)
|
|
250
|
-
disk_size =
|
|
251
|
-
disk = create_ellipse(disk_size, disk_size).astype(np.uint8)
|
|
294
|
+
disk_size = int(np.floor(np.sqrt(np.min(self.bgr.shape[:2])) / 2))
|
|
295
|
+
disk = create_ellipse(disk_size, disk_size, min_size=3).astype(np.uint8)
|
|
252
296
|
self.subtract_background = cv2.morphologyEx(self.image, cv2.MORPH_OPEN, disk)
|
|
253
297
|
if self.image2 is not None:
|
|
254
298
|
self.subtract_background2 = cv2.morphologyEx(self.image2, cv2.MORPH_OPEN, disk)
|
|
@@ -339,253 +383,169 @@ class OneImageAnalysis:
|
|
|
339
383
|
self.binary_image = np.logical_xor(self.binary_image, self.binary_image2)
|
|
340
384
|
self.binary_image = self.binary_image.astype(np.uint8)
|
|
341
385
|
|
|
342
|
-
def
|
|
343
|
-
spot_size=None, kmeans_clust_nb: int=None, biomask: NDArray[np.uint8]=None,
|
|
344
|
-
backmask: NDArray[np.uint8]=None, color_space_dictionaries: TList=None, basic: bool=True):
|
|
345
|
-
"""
|
|
346
|
-
Prepare color space lists, dictionaries and matrices.
|
|
347
|
-
|
|
348
|
-
Args:
|
|
349
|
-
sample_number: An integer representing the sample number. Defaults to None.
|
|
350
|
-
several_blob_per_arena: A boolean indicating whether there are several blobs per arena. Defaults to True.
|
|
351
|
-
spot_shape: A string representing the shape of the spot. Defaults to None.
|
|
352
|
-
spot_size: An integer representing the size of the spot. Defaults to None.
|
|
353
|
-
kmeans_clust_nb: An integer representing the number of clusters for K-means. Defaults to None.
|
|
354
|
-
biomask: A 2D numpy array of type np.uint8 representing the bio mask. Defaults to None.
|
|
355
|
-
backmask: A 2D numpy array of type np.uint8 representing the background mask. Defaults to None.
|
|
356
|
-
color_space_dictionaries: A list of dictionaries containing color space information. Defaults to None.
|
|
357
|
-
basic: A boolean indicating whether to process the data basic. Defaults to True.
|
|
358
|
-
|
|
359
|
-
Note:
|
|
360
|
-
This method processes the input data to find the first image that matches certain criteria, using various color spaces and masks.
|
|
361
|
-
|
|
362
|
-
"""
|
|
363
|
-
logging.info(f"Start automatic detection of the first image")
|
|
386
|
+
def init_combinations_lists(self):
|
|
364
387
|
self.im_combinations = []
|
|
365
|
-
self.saved_images_list =
|
|
366
|
-
self.converted_images_list =
|
|
388
|
+
self.saved_images_list = List()
|
|
389
|
+
self.converted_images_list = List()
|
|
367
390
|
self.saved_color_space_list = list()
|
|
368
391
|
self.saved_csc_nb = 0
|
|
369
392
|
|
|
393
|
+
def find_color_space_combinations(self, params: dict=None, only_bgr: bool = False):
|
|
394
|
+
logging.info(f"Start automatic finding of color space combinations...")
|
|
395
|
+
self.init_combinations_lists()
|
|
370
396
|
if self.image.any():
|
|
371
|
-
|
|
372
|
-
if
|
|
373
|
-
|
|
374
|
-
|
|
397
|
+
# 1. Set all params
|
|
398
|
+
if params is None:
|
|
399
|
+
params = init_params()
|
|
400
|
+
if params['arenas_mask'] is not None:
|
|
401
|
+
params['out_of_arenas_mask'] = 1 - params['arenas_mask']
|
|
402
|
+
if params['ref_image'] is not None:
|
|
403
|
+
params['ref_image'] = cv2.dilate(params['ref_image'], cross_33)
|
|
404
|
+
if params['several_blob_per_arena']:
|
|
405
|
+
params['con_comp_extent'] = [1, self.binary_image.size // 50]
|
|
406
|
+
else:
|
|
407
|
+
params['con_comp_extent'] = [params['blob_nb'], np.max((params['blob_nb'], self.binary_image.size // 100))]
|
|
408
|
+
im_size = self.image.shape[0] * self.image.shape[1]
|
|
409
|
+
|
|
410
|
+
if not params['several_blob_per_arena'] and params['blob_nb'] is not None and params['blob_nb'] > 1 and params['are_zigzag'] is not None:
|
|
411
|
+
if params['are_zigzag'] == "columns":
|
|
412
|
+
inter_dist = np.mean(np.diff(np.nonzero(self.y_boundaries)))
|
|
413
|
+
elif params['are_zigzag'] == "rows":
|
|
414
|
+
inter_dist = np.mean(np.diff(np.nonzero(self.x_boundaries)))
|
|
375
415
|
else:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
color_space_dictionaries.append(csc_dict)
|
|
385
|
-
|
|
386
|
-
self.combination_features = np.zeros((len(color_space_dictionaries) + 50, 11), dtype=np.uint32)
|
|
387
|
-
unaltered_cc_nb, cc_nb, area, width_std, height_std, area_std, biosum, backsum = 3, 4, 5, 6, 7, 8, 9, 10
|
|
388
|
-
self.save_combination_thread = SaveCombinationThread(self)
|
|
389
|
-
get_one_channel_result = True
|
|
390
|
-
combine_channels = False
|
|
391
|
-
logging.info(f"Try detection with each available color space channel, one by one.")
|
|
392
|
-
for csc_dict in color_space_dictionaries:
|
|
393
|
-
list_args = [self, get_one_channel_result, combine_channels, csc_dict, several_blob_per_arena,
|
|
394
|
-
sample_number, spot_size, spot_shape, kmeans_clust_nb, biomask, backmask, None]
|
|
395
|
-
ProcessFirstImage(list_args)
|
|
396
|
-
|
|
397
|
-
if sample_number is not None and basic:
|
|
398
|
-
# Try to add csc together
|
|
399
|
-
possibilities = []
|
|
400
|
-
if self.saved_csc_nb > 6:
|
|
401
|
-
different_color_spaces = np.unique(self.saved_color_space_list)
|
|
402
|
-
for color_space in different_color_spaces:
|
|
403
|
-
csc_idx = np.nonzero(np.isin(self.saved_color_space_list, color_space))[0]
|
|
404
|
-
possibilities.append(csc_idx[0] + np.argmin(self.combination_features[csc_idx, area_std]))
|
|
405
|
-
if len(possibilities) <= 6:
|
|
406
|
-
remaining_possibilities = np.arange(len(self.saved_color_space_list))
|
|
407
|
-
remaining_possibilities = remaining_possibilities[np.logical_not(np.isin(remaining_possibilities, possibilities))]
|
|
408
|
-
while len(possibilities) <= 6:
|
|
409
|
-
new_possibility = np.argmin(self.combination_features[remaining_possibilities, area_std])
|
|
410
|
-
possibilities.append(new_possibility)
|
|
411
|
-
remaining_possibilities = remaining_possibilities[remaining_possibilities != new_possibility]
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
pool = mp.ThreadPool(processes=os.cpu_count() - 1)
|
|
415
|
-
get_one_channel_result = False
|
|
416
|
-
combine_channels = True
|
|
417
|
-
list_args = [[self, get_one_channel_result, combine_channels, i, several_blob_per_arena, sample_number,
|
|
418
|
-
spot_size, spot_shape, kmeans_clust_nb, biomask, backmask, possibilities] for i in possibilities]
|
|
419
|
-
for process_i in pool.imap_unordered(ProcessFirstImage, list_args):
|
|
420
|
-
pass
|
|
421
|
-
|
|
422
|
-
# Get the most and the least covered images and the 2 best biomask and backmask scores
|
|
423
|
-
# To try combinations of those
|
|
424
|
-
if self.saved_csc_nb <= 1:
|
|
425
|
-
csc_dict = {'bgr': np.array((1, 1, 1))}
|
|
426
|
-
list_args = [self, False, False, csc_dict, several_blob_per_arena,
|
|
427
|
-
sample_number, spot_size, spot_shape, kmeans_clust_nb, biomask, backmask, None]
|
|
428
|
-
process_i = ProcessFirstImage(list_args)
|
|
429
|
-
process_i.image = self.bgr.mean(axis=-1)
|
|
430
|
-
process_i.binary_image = otsu_thresholding(process_i.image)
|
|
431
|
-
process_i.csc_dict = csc_dict
|
|
432
|
-
process_i.total_area = process_i.binary_image.sum()
|
|
433
|
-
process_i.process_binary_image()
|
|
434
|
-
process_i.unaltered_concomp_nb, shapes = cv2.connectedComponents(process_i.validated_shapes)
|
|
435
|
-
self.save_combination_features(process_i)
|
|
436
|
-
self.combination_features = self.combination_features[:self.saved_csc_nb, :]
|
|
437
|
-
fit = np.array([True])
|
|
416
|
+
dist1 = np.mean(np.diff(np.nonzero(self.y_boundaries)))
|
|
417
|
+
dist2 = np.mean(np.diff(np.nonzero(self.x_boundaries)))
|
|
418
|
+
inter_dist = np.max(dist1, dist2)
|
|
419
|
+
if params['blob_shape'] == "rectangle":
|
|
420
|
+
params['max_blob_size'] = np.square(2 * inter_dist)
|
|
421
|
+
else:
|
|
422
|
+
params['max_blob_size'] = np.pi * np.square(inter_dist)
|
|
423
|
+
params['total_surface_area'] = params['max_blob_size'] * self.sample_number
|
|
438
424
|
else:
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
least1 = coverage[0]; least2 = coverage[1]
|
|
442
|
-
if biomask is not None:
|
|
443
|
-
bio_sort = np.argsort(self.combination_features[:self.saved_csc_nb, biosum])
|
|
444
|
-
bio1 = bio_sort[-1]; bio2 = bio_sort[-2]
|
|
445
|
-
if backmask is not None:
|
|
446
|
-
back_sort = np.argsort(self.combination_features[:self.saved_csc_nb, backsum])
|
|
447
|
-
back1 = back_sort[-1]; back2 = back_sort[-2]
|
|
448
|
-
|
|
449
|
-
# Try a logical And between the most covered images
|
|
450
|
-
# Should only need one instanciation
|
|
451
|
-
process_i = ProcessFirstImage(
|
|
452
|
-
[self, False, False, None, several_blob_per_arena, sample_number, spot_size, spot_shape, kmeans_clust_nb, biomask, backmask, None])
|
|
453
|
-
process_i.binary_image = np.logical_and(self.saved_images_list[most1], self.saved_images_list[most2]).astype(np.uint8)
|
|
454
|
-
process_i.image = self.converted_images_list[most1]
|
|
455
|
-
process_i.process_binary_image()
|
|
456
|
-
process_i.csc_dict = {list(self.saved_color_space_list[most1].keys())[0]: self.combination_features[most1, :3],
|
|
457
|
-
"logical": "And",
|
|
458
|
-
list(self.saved_color_space_list[most2].keys())[0] + "2": self.combination_features[most2, :3]}
|
|
459
|
-
process_i.unaltered_concomp_nb = np.min(self.combination_features[(most1, most2), unaltered_cc_nb])
|
|
460
|
-
process_i.total_area = process_i.binary_image.sum()
|
|
461
|
-
self.save_combination_features(process_i)
|
|
462
|
-
process_i.image = self.converted_images_list[least1]
|
|
463
|
-
process_i.binary_image = np.logical_or(self.saved_images_list[least1], self.saved_images_list[least2]).astype(np.uint8)
|
|
464
|
-
process_i.process_binary_image()
|
|
465
|
-
process_i.csc_dict = {list(self.saved_color_space_list[least1].keys())[0]: self.combination_features[least1, :3],
|
|
466
|
-
"logical": "Or",
|
|
467
|
-
list(self.saved_color_space_list[least2].keys())[0] + "2": self.combination_features[least2, :3]}
|
|
468
|
-
process_i.unaltered_concomp_nb = np.max(self.combination_features[(least1, least2), unaltered_cc_nb])
|
|
469
|
-
process_i.total_area = process_i.binary_image.sum()
|
|
470
|
-
self.save_combination_features(process_i)
|
|
471
|
-
|
|
472
|
-
# self.save_combination_features(csc_dict, unaltered_concomp_nb, self.binary_image.sum(), biomask, backmask)
|
|
473
|
-
|
|
474
|
-
# If most images are very low in biosum or backsum, try to mix them together to improve that score
|
|
475
|
-
# Do a logical And between the two best biomasks
|
|
476
|
-
if biomask is not None:
|
|
477
|
-
if not np.all(np.isin((bio1, bio2), (most1, most2))):
|
|
478
|
-
process_i.image = self.converted_images_list[bio1]
|
|
479
|
-
process_i.binary_image = np.logical_and(self.saved_images_list[bio1], self.saved_images_list[bio2]).astype(
|
|
480
|
-
np.uint8)
|
|
481
|
-
process_i.process_binary_image()
|
|
482
|
-
process_i.csc_dict = {list(self.saved_color_space_list[bio1].keys())[0]: self.combination_features[bio1, :3],
|
|
483
|
-
"logical": "And",
|
|
484
|
-
list(self.saved_color_space_list[bio2].keys())[0] + "2": self.combination_features[bio2,:3]}
|
|
485
|
-
process_i.unaltered_concomp_nb = np.min(self.combination_features[(bio1, bio2), unaltered_cc_nb])
|
|
486
|
-
process_i.total_area = process_i.binary_image.sum()
|
|
487
|
-
|
|
488
|
-
self.save_combination_features(process_i)
|
|
489
|
-
|
|
490
|
-
# Do a logical And between the two best backmask
|
|
491
|
-
if backmask is not None:
|
|
492
|
-
if not np.all(np.isin((back1, back2), (most1, most2))):
|
|
493
|
-
process_i.image = self.converted_images_list[back1]
|
|
494
|
-
process_i.binary_image = np.logical_and(self.saved_images_list[back1], self.saved_images_list[back2]).astype(
|
|
495
|
-
np.uint8)
|
|
496
|
-
process_i.process_binary_image()
|
|
497
|
-
process_i.csc_dict = {list(self.saved_color_space_list[back1].keys())[0]: self.combination_features[back1, :3],
|
|
498
|
-
"logical": "And",
|
|
499
|
-
list(self.saved_color_space_list[back2].keys())[0] + "2": self.combination_features[back2,:3]}
|
|
500
|
-
process_i.unaltered_concomp_nb = np.min(self.combination_features[(back1, back2), unaltered_cc_nb])
|
|
501
|
-
process_i.total_area = process_i.binary_image.sum()
|
|
502
|
-
self.save_combination_features(process_i)
|
|
503
|
-
# Do a logical Or between the best biomask and the best backmask
|
|
504
|
-
if biomask is not None and backmask is not None:
|
|
505
|
-
if not np.all(np.isin((bio1, back1), (least1, least2))):
|
|
506
|
-
process_i.image = self.converted_images_list[bio1]
|
|
507
|
-
process_i.binary_image = np.logical_and(self.saved_images_list[bio1], self.saved_images_list[back1]).astype(
|
|
508
|
-
np.uint8)
|
|
509
|
-
process_i.process_binary_image()
|
|
510
|
-
process_i.csc_dict = {list(self.saved_color_space_list[bio1].keys())[0]: self.combination_features[bio1, :3],
|
|
511
|
-
"logical": "Or",
|
|
512
|
-
list(self.saved_color_space_list[back1].keys())[0] + "2": self.combination_features[back1, :3]}
|
|
513
|
-
process_i.unaltered_concomp_nb = np.max(self.combination_features[(bio1, back1), unaltered_cc_nb])
|
|
514
|
-
# self.save_combination_features(csc_dict, unaltered_concomp_nb, self.binary_image.sum(), biomask,
|
|
515
|
-
# backmask)
|
|
516
|
-
process_i.total_area = self.binary_image.sum()
|
|
517
|
-
self.save_combination_features(process_i)
|
|
518
|
-
|
|
519
|
-
if self.save_combination_thread.is_alive():
|
|
520
|
-
self.save_combination_thread.join()
|
|
521
|
-
self.combination_features = self.combination_features[:self.saved_csc_nb, :]
|
|
522
|
-
# Only keep the row that filled conditions
|
|
523
|
-
# Save all combinations if they fulfill the following conditions:
|
|
524
|
-
# - Their conncomp number is lower than 3 times the smaller conncomp number.
|
|
525
|
-
# - OR The minimal area variations
|
|
526
|
-
# - OR The minimal width variations
|
|
527
|
-
# - OR The minimal height variations
|
|
528
|
-
# - AND/OR their segmentation fits with biomask and backmask
|
|
529
|
-
width_std_fit = self.combination_features[:, width_std] == np.min(self.combination_features[:, width_std])
|
|
530
|
-
height_std_fit = self.combination_features[:, height_std] == np.min(self.combination_features[:, height_std])
|
|
531
|
-
area_std_fit = self.combination_features[:, area_std] < np.min(self.combination_features[:, area_std]) * 10
|
|
532
|
-
fit = np.logical_or(np.logical_or(width_std_fit, height_std_fit), area_std_fit)
|
|
533
|
-
biomask_fit = np.ones(self.saved_csc_nb, dtype=bool)
|
|
534
|
-
backmask_fit = np.ones(self.saved_csc_nb, dtype=bool)
|
|
535
|
-
if biomask is not None or backmask is not None:
|
|
536
|
-
if biomask is not None:
|
|
537
|
-
biomask_fit = self.combination_features[:, biosum] > 0.9 * len(biomask[0])
|
|
538
|
-
if backmask is not None:
|
|
539
|
-
backmask_fit = self.combination_features[:, backsum] > 0.9 * len(backmask[0])
|
|
540
|
-
# First test a logical OR between the precedent options and the mask fits.
|
|
541
|
-
fit = np.logical_or(fit, np.logical_and(biomask_fit, backmask_fit))
|
|
542
|
-
# If this is not stringent enough, use a logical AND and increase progressively the proportion of pixels that
|
|
543
|
-
# must match the biomask and the backmask
|
|
544
|
-
if np.sum(fit) > 5:
|
|
545
|
-
to_add = 0
|
|
546
|
-
while np.sum(fit) > 5 and to_add <= 0.25:
|
|
547
|
-
if biomask is not None:
|
|
548
|
-
biomask_fit = self.combination_features[:, biosum] > (0.75 + to_add) * len(biomask[0])
|
|
549
|
-
if backmask is not None:
|
|
550
|
-
backmask_fit = self.combination_features[:, backsum] > (0.75 + to_add) * len(backmask[0])
|
|
551
|
-
test_fit = np.logical_and(fit, np.logical_and(biomask_fit, backmask_fit))
|
|
552
|
-
if np.sum(test_fit) != 0:
|
|
553
|
-
fit = test_fit
|
|
554
|
-
to_add += 0.05
|
|
555
|
-
# If saved_csc_nb is too low, try bool operators to mix them together to fill holes for instance
|
|
556
|
-
# Order the table according to the number of shapes that have been removed by filters
|
|
557
|
-
# cc_efficiency_order = np.argsort(self.combination_features[:, unaltered_cc_nb] - self.combination_features[:, cc_nb])
|
|
558
|
-
cc_efficiency_order = np.argsort(self.combination_features[:, area_std])
|
|
559
|
-
# Save and return a dictionnary containing the selected color space combinations
|
|
560
|
-
# and their corresponding binary images
|
|
425
|
+
params['max_blob_size'] = .9 * im_size
|
|
426
|
+
params['total_surface_area'] = .99 * im_size
|
|
561
427
|
|
|
428
|
+
# 2. Get color_space_dictionaries
|
|
429
|
+
if only_bgr:
|
|
430
|
+
if not 'bgr' in self.all_c_spaces:
|
|
431
|
+
self.all_c_spaces['bgr'] = self.bgr
|
|
432
|
+
else:
|
|
433
|
+
self._get_all_color_spaces()
|
|
434
|
+
|
|
435
|
+
# 3. Init combination_features table
|
|
436
|
+
unaltered_blob_nb_idx, blob_number_idx, blob_shape_idx, blob_size_idx, total_area_idx, width_std_idx, height_std_idx, area_std_idx, out_of_arenas_idx, in_arena_idx, common_with_ref_idx, bio_sum_idx, back_sum_idx, score_idx = np.arange(3, 17)
|
|
437
|
+
self.factors = ['unaltered_blob_nb', 'blob_nb', 'total_area', 'width_std', 'height_std', 'area_std', 'out_of_arenas', 'in_arenas', 'common_with_ref', 'bio_sum', 'back_sum', 'score']
|
|
438
|
+
self.combination_features = pd.DataFrame(np.zeros((100, len(self.factors)), dtype=np.float64), columns=self.factors)
|
|
439
|
+
|
|
440
|
+
# 4. Test every channel separately
|
|
441
|
+
process = 'one'
|
|
442
|
+
for csc_dict in one_dict_per_channel:
|
|
443
|
+
ProcessImage([self, params, process, csc_dict])
|
|
444
|
+
# If the blob number is known, try applying filters to improve detection
|
|
445
|
+
if params['blob_nb'] is not None and (params['filter_spec'] is None or params['filter_spec']['filter1_type'] == ''):
|
|
446
|
+
if not (self.combination_features['blob_nb'].iloc[:self.saved_csc_nb] == params['blob_nb']).any():
|
|
447
|
+
tested_filters = ['Gaussian', 'Median', 'Mexican hat', 'Laplace', '']
|
|
448
|
+
for tested_filter in tested_filters:
|
|
449
|
+
self.init_combinations_lists()
|
|
450
|
+
params['filter_spec'] = {'filter1_type': tested_filter, 'filter1_param': [.5, 1.], 'filter2_type': "", 'filter2_param': [.5, 1.]}
|
|
451
|
+
if 'Param1' in filter_dict[tested_filter]:
|
|
452
|
+
params['filter_spec']['filter1_param'] = [filter_dict[tested_filter]['Param1']['Default']]
|
|
453
|
+
if 'Param2' in filter_dict[tested_filter]:
|
|
454
|
+
params['filter_spec']['filter1_param'].append(filter_dict[tested_filter]['Param2']['Default'])
|
|
455
|
+
for csc_dict in one_dict_per_channel:
|
|
456
|
+
ProcessImage([self, params, process, csc_dict])
|
|
457
|
+
if (self.combination_features['blob_nb'].iloc[:self.saved_csc_nb] == params['blob_nb']).any():
|
|
458
|
+
break
|
|
459
|
+
|
|
460
|
+
self.score_combination_features()
|
|
461
|
+
# 5. Try adding each valid channel with one another
|
|
462
|
+
# 5.1. Generate an index vector containing, for each color space, the channel maximizing the score
|
|
463
|
+
possibilities = []
|
|
464
|
+
self.all_combined = Dict()
|
|
465
|
+
different_color_spaces = np.unique(self.saved_color_space_list)
|
|
466
|
+
for color_space in different_color_spaces:
|
|
467
|
+
indices = np.nonzero(np.isin(self.saved_color_space_list, color_space))[0]
|
|
468
|
+
csc_idx = indices[0] + np.argmax(self.combination_features.loc[indices, 'score'])
|
|
469
|
+
possibilities.append(csc_idx)
|
|
470
|
+
for k, v in self.saved_color_space_list[csc_idx].items():
|
|
471
|
+
self.all_combined[k] = v
|
|
472
|
+
|
|
473
|
+
# 5.2. Try combining each selected channel with every other in all possible order
|
|
474
|
+
params['possibilities'] = possibilities
|
|
475
|
+
pool = mp.ThreadPool(processes=os.cpu_count() - 1)
|
|
476
|
+
process = 'add'
|
|
477
|
+
list_args = [[self, params, process, i] for i in possibilities]
|
|
478
|
+
for process_i in pool.imap_unordered(ProcessImage, list_args):
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
# 6. Take a combination of all selected channels and try to remove each color space one by one
|
|
482
|
+
ProcessImage([self, params, 'subtract', 0])
|
|
483
|
+
|
|
484
|
+
# 7. Add PCA:
|
|
485
|
+
ProcessImage([self, params, 'PCA', None])
|
|
486
|
+
|
|
487
|
+
# 8. Make logical operations between pairs of segmentation result
|
|
488
|
+
coverage = np.argsort(self.combination_features['total_area'].iloc[:self.saved_csc_nb])
|
|
489
|
+
|
|
490
|
+
# 8.1 Try a logical And between the most covered images
|
|
491
|
+
most1, most2 = coverage.values[-1], coverage.values[-2]
|
|
492
|
+
operation = {0: most1, 1: most2, 'logical': 'And'}
|
|
493
|
+
ProcessImage([self, params, 'logical', operation])
|
|
494
|
+
|
|
495
|
+
# 8.2 Try a logical Or between the least covered images
|
|
496
|
+
least1, least2 = coverage.values[0], coverage.values[1]
|
|
497
|
+
operation = {0: least1, 1: least2, 'logical': 'Or'}
|
|
498
|
+
ProcessImage([self, params, 'logical', operation])
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# 8.3 Try a logical And between the best bio_mask images
|
|
502
|
+
if params['bio_mask'] is not None:
|
|
503
|
+
bio_sort = np.argsort(self.combination_features['bio_sum'].iloc[:self.saved_csc_nb])
|
|
504
|
+
bio1, bio2 = bio_sort.values[-1], bio_sort.values[-2]
|
|
505
|
+
operation = {0: bio1, 1: bio2, 'logical': 'And'}
|
|
506
|
+
ProcessImage([self, params, 'logical', operation])
|
|
507
|
+
|
|
508
|
+
# 8.4 Try a logical And between the best back_mask images
|
|
509
|
+
if params['back_mask'] is not None:
|
|
510
|
+
back_sort = np.argsort(self.combination_features['back_sum'].iloc[:self.saved_csc_nb])
|
|
511
|
+
back1, back2 = back_sort.values[-1], back_sort.values[-2]
|
|
512
|
+
operation = {0: back1, 1: back2, 'logical': 'And'}
|
|
513
|
+
ProcessImage([self, params, 'logical', operation])
|
|
514
|
+
|
|
515
|
+
# 8.5 Try a logical Or between the best bio_mask and the best back_mask images
|
|
516
|
+
if params['bio_mask'] is not None and params['back_mask'] is not None:
|
|
517
|
+
operation = {0: bio1, 1: back1, 'logical': 'Or'}
|
|
518
|
+
ProcessImage([self, params, 'logical', operation])
|
|
519
|
+
|
|
520
|
+
# 9. Order all saved features
|
|
521
|
+
self.combination_features = self.combination_features.iloc[:self.saved_csc_nb, :]
|
|
522
|
+
self.score_combination_features()
|
|
523
|
+
if params['is_first_image'] and params['blob_nb'] is not None:
|
|
524
|
+
distances = np.abs(self.combination_features['blob_nb'] - params['blob_nb'])
|
|
525
|
+
cc_efficiency_order = np.argsort(distances)
|
|
526
|
+
else:
|
|
527
|
+
cc_efficiency_order = np.argsort(self.combination_features['score'])
|
|
528
|
+
cc_efficiency_order = cc_efficiency_order.max() - cc_efficiency_order
|
|
529
|
+
|
|
530
|
+
# 7. Save and return a dictionary containing the selected color space combinations
|
|
531
|
+
# and their corresponding binary images
|
|
532
|
+
self.im_combinations = []
|
|
562
533
|
for saved_csc in cc_efficiency_order:
|
|
563
|
-
if
|
|
534
|
+
if len(self.saved_color_space_list[saved_csc]) > 0:
|
|
564
535
|
self.im_combinations.append({})
|
|
565
|
-
# self.im_combinations.append({})
|
|
566
|
-
# self.im_combinations[len(self.im_combinations) - 1]["csc"] = self.saved_color_space_list[saved_csc]
|
|
567
536
|
self.im_combinations[len(self.im_combinations) - 1]["csc"] = {}
|
|
568
537
|
self.im_combinations[len(self.im_combinations) - 1]["csc"]['logical'] = 'None'
|
|
569
538
|
for k, v in self.saved_color_space_list[saved_csc].items():
|
|
570
539
|
self.im_combinations[len(self.im_combinations) - 1]["csc"][k] = v
|
|
571
|
-
if backmask is not None:
|
|
572
|
-
shape_number, shapes = cv2.connectedComponents(self.saved_images_list[saved_csc], connectivity=8)
|
|
573
|
-
if np.any(shapes[backmask]):
|
|
574
|
-
shapes[np.isin(shapes, np.unique(shapes[backmask]))] = 0
|
|
575
|
-
self.saved_images_list[saved_csc] = (shapes > 0).astype(np.uint8)
|
|
576
|
-
if biomask is not None:
|
|
577
|
-
self.saved_images_list[saved_csc][biomask] = 1
|
|
578
|
-
if backmask is not None or biomask is not None:
|
|
579
|
-
self.combination_features[saved_csc, cc_nb], shapes = cv2.connectedComponents(self.saved_images_list[saved_csc], connectivity=8)
|
|
580
|
-
self.combination_features[saved_csc, cc_nb] -= 1
|
|
581
540
|
self.im_combinations[len(self.im_combinations) - 1]["binary_image"] = self.saved_images_list[saved_csc]
|
|
582
|
-
self.im_combinations[len(self.im_combinations) - 1]["
|
|
583
|
-
|
|
584
|
-
|
|
541
|
+
self.im_combinations[len(self.im_combinations) - 1]["converted_image"] = np.round(self.converted_images_list[
|
|
542
|
+
saved_csc]).astype(np.uint8)
|
|
543
|
+
self.im_combinations[len(self.im_combinations) - 1]["shape_number"] = int(self.combination_features['blob_nb'].iloc[saved_csc])
|
|
544
|
+
self.im_combinations[len(self.im_combinations) - 1]['filter_spec']= params['filter_spec']
|
|
585
545
|
self.saved_color_space_list = []
|
|
586
|
-
self.saved_images_list
|
|
587
|
-
self.converted_images_list
|
|
588
|
-
self.
|
|
546
|
+
del self.saved_images_list
|
|
547
|
+
del self.converted_images_list
|
|
548
|
+
del self.all_combined
|
|
589
549
|
|
|
590
550
|
def save_combination_features(self, process_i: object):
|
|
591
551
|
"""
|
|
@@ -594,7 +554,7 @@ class OneImageAnalysis:
|
|
|
594
554
|
Args:
|
|
595
555
|
process_i (object): The processed image object containing various attributes
|
|
596
556
|
such as validated_shapes, image, csc_dict, unaltered_concomp_nb,
|
|
597
|
-
shape_number, total_area, stats,
|
|
557
|
+
shape_number, total_area, stats, bio_mask, and back_mask.
|
|
598
558
|
|
|
599
559
|
Attributes:
|
|
600
560
|
processed image object
|
|
@@ -603,24 +563,20 @@ class OneImageAnalysis:
|
|
|
603
563
|
csc_dict (dict): Color space conversion dictionary
|
|
604
564
|
"""
|
|
605
565
|
if process_i.validated_shapes.any():
|
|
566
|
+
saved_csc_nb = self.saved_csc_nb
|
|
567
|
+
self.saved_csc_nb += 1
|
|
606
568
|
self.saved_images_list.append(process_i.validated_shapes)
|
|
607
|
-
self.converted_images_list.append(
|
|
569
|
+
self.converted_images_list.append(bracket_to_uint8_image_contrast(process_i.greyscale))
|
|
608
570
|
self.saved_color_space_list.append(process_i.csc_dict)
|
|
609
|
-
self.combination_features[
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
self.combination_features[
|
|
614
|
-
self.combination_features[
|
|
615
|
-
|
|
616
|
-
self.combination_features[
|
|
617
|
-
|
|
618
|
-
self.combination_features[self.saved_csc_nb, 9] = np.sum(
|
|
619
|
-
process_i.validated_shapes[process_i.biomask[0], process_i.biomask[1]])
|
|
620
|
-
if process_i.backmask is not None:
|
|
621
|
-
self.combination_features[self.saved_csc_nb, 10] = np.sum(
|
|
622
|
-
(1 - process_i.validated_shapes)[process_i.backmask[0], process_i.backmask[1]])
|
|
623
|
-
self.saved_csc_nb += 1
|
|
571
|
+
self.combination_features.iloc[saved_csc_nb, :] = process_i.fact
|
|
572
|
+
|
|
573
|
+
def score_combination_features(self):
|
|
574
|
+
for to_minimize in ['unaltered_blob_nb', 'blob_nb', 'area_std', 'width_std', 'height_std', 'back_sum', 'out_of_arenas']:
|
|
575
|
+
values = rankdata(self.combination_features[to_minimize], method='dense')
|
|
576
|
+
self.combination_features['score'] += values.max() - values
|
|
577
|
+
for to_maximize in ['bio_sum', 'in_arenas', 'common_with_ref']:
|
|
578
|
+
values = rankdata(self.combination_features[to_maximize], method='dense') - 1
|
|
579
|
+
self.combination_features['score'] += values
|
|
624
580
|
|
|
625
581
|
def update_current_images(self, current_combination_id: int):
|
|
626
582
|
"""
|
|
@@ -641,262 +597,7 @@ class OneImageAnalysis:
|
|
|
641
597
|
self.image = self.im_combinations[current_combination_id]["converted_image"]
|
|
642
598
|
self.validated_shapes = self.im_combinations[current_combination_id]["binary_image"]
|
|
643
599
|
|
|
644
|
-
def
|
|
645
|
-
ref_image: NDArray=None, subtract_background: NDArray=None, kmeans_clust_nb: int=None,
|
|
646
|
-
biomask: NDArray[np.uint8]=None, backmask: NDArray[np.uint8]=None,
|
|
647
|
-
color_space_dictionaries: dict=None, basic: bool=True):
|
|
648
|
-
"""
|
|
649
|
-
Find the last image color space configurations that meets given criteria.
|
|
650
|
-
|
|
651
|
-
Args:
|
|
652
|
-
concomp_nb (int): A tuple of two integers representing the minimum and maximum number of connected components.
|
|
653
|
-
total_surfarea (int): The total surface area required for the image.
|
|
654
|
-
max_shape_size (int): The maximum shape size allowed in the image.
|
|
655
|
-
arenas_mask (NDArray, optional): A numpy array representing areas inside the field of interest.
|
|
656
|
-
ref_image (NDArray, optional): A reference image for comparison.
|
|
657
|
-
subtract_background (NDArray, optional): A numpy array representing the background to be subtracted.
|
|
658
|
-
kmeans_clust_nb (int, optional): The number of clusters for k-means clustering.
|
|
659
|
-
biomask (NDArray[np.uint8], optional): A binary mask for biological structures.
|
|
660
|
-
backmask (NDArray[np.uint8], optional): A binary mask for background areas.
|
|
661
|
-
color_space_dictionaries (dict, optional): Dictionaries of color space configurations.
|
|
662
|
-
basic (bool, optional): A flag indicating whether to process colorspaces basic.
|
|
663
|
-
|
|
664
|
-
"""
|
|
665
|
-
logging.info(f"Start automatic detection of the last image")
|
|
666
|
-
self.im_combinations = []
|
|
667
|
-
self.saved_images_list = TList()
|
|
668
|
-
self.converted_images_list = TList()
|
|
669
|
-
self.saved_color_space_list = list()
|
|
670
|
-
self.saved_csc_nb = 0
|
|
671
|
-
|
|
672
|
-
if self.image.any():
|
|
673
|
-
if arenas_mask is None:
|
|
674
|
-
arenas_mask = np.ones_like(self.binary_image)
|
|
675
|
-
out_of_arenas = 1 - arenas_mask
|
|
676
|
-
self._get_all_color_spaces()
|
|
677
|
-
if color_space_dictionaries is None:
|
|
678
|
-
if basic:
|
|
679
|
-
colorspace_list = TList(("bgr", "lab", "hsv", "luv", "hls", "yuv"))
|
|
680
|
-
else:
|
|
681
|
-
colorspace_list = TList(("lab", "hsv"))
|
|
682
|
-
color_space_dictionaries = TList()
|
|
683
|
-
channels = np.array((1, 1, 1), dtype=np.int8)
|
|
684
|
-
csc_dict = TDict()
|
|
685
|
-
csc_dict["bgr"] = channels
|
|
686
|
-
color_space_dictionaries.append(csc_dict)
|
|
687
|
-
for i, c_space in enumerate(colorspace_list):
|
|
688
|
-
for i in np.arange(3):
|
|
689
|
-
channels = np.array((0, 0, 0), dtype=np.int8)
|
|
690
|
-
channels[i] = 1
|
|
691
|
-
csc_dict = TDict()
|
|
692
|
-
csc_dict[c_space] = channels
|
|
693
|
-
color_space_dictionaries.append(csc_dict)
|
|
694
|
-
if ref_image is not None:
|
|
695
|
-
ref_image = cv2.dilate(ref_image, cross_33)
|
|
696
|
-
else:
|
|
697
|
-
ref_image = np.ones(self.bgr.shape[:2], dtype=np.uint8)
|
|
698
|
-
out_of_arenas_threshold = 0.01 * out_of_arenas.sum()
|
|
699
|
-
self.combination_features = np.zeros((len(color_space_dictionaries) + 50, 10), dtype=np.uint32)
|
|
700
|
-
cc_nb_idx, area_idx, out_of_arenas_idx, in_arena_idx, surf_in_common_idx, biosum_idx, backsum_idx = 3, 4, 5, 6, 7, 8, 9
|
|
701
|
-
self.save_combination_thread = SaveCombinationThread(self)
|
|
702
|
-
|
|
703
|
-
# Start with a PCA:
|
|
704
|
-
pca_dict = TDict()
|
|
705
|
-
pca_dict['PCA'] = np.array([1, 1, 1], dtype=np.int8)
|
|
706
|
-
self.image, explained_variance_ratio, first_pc_vector = extract_first_pc(self.bgr)
|
|
707
|
-
self.binary_image = otsu_thresholding(self.image)
|
|
708
|
-
nb, shapes = cv2.connectedComponents(self.binary_image)
|
|
709
|
-
nb -= 1
|
|
710
|
-
surf = self.binary_image.sum()
|
|
711
|
-
outside_pixels = np.sum(self.binary_image * out_of_arenas)
|
|
712
|
-
inside_pixels = np.sum(self.binary_image * arenas_mask)
|
|
713
|
-
in_common = np.sum(ref_image * self.binary_image)
|
|
714
|
-
self.converted_images_list.append(self.image)
|
|
715
|
-
self.saved_images_list.append(self.binary_image)
|
|
716
|
-
self.saved_color_space_list.append(pca_dict)
|
|
717
|
-
self.combination_features[self.saved_csc_nb, :3] = list(pca_dict.values())[0]
|
|
718
|
-
self.combination_features[self.saved_csc_nb, cc_nb_idx] = nb
|
|
719
|
-
self.combination_features[self.saved_csc_nb, area_idx] = surf
|
|
720
|
-
self.combination_features[self.saved_csc_nb, out_of_arenas_idx] = outside_pixels
|
|
721
|
-
self.combination_features[self.saved_csc_nb, in_arena_idx] = inside_pixels
|
|
722
|
-
self.combination_features[self.saved_csc_nb, surf_in_common_idx] = in_common
|
|
723
|
-
if biomask is not None:
|
|
724
|
-
self.combination_features[self.saved_csc_nb, biosum_idx] = np.sum(
|
|
725
|
-
self.binary_image[biomask[0], biomask[1]])
|
|
726
|
-
if backmask is not None:
|
|
727
|
-
self.combination_features[self.saved_csc_nb, backsum_idx] = np.sum(
|
|
728
|
-
(1 - self.binary_image)[backmask[0], backmask[1]])
|
|
729
|
-
self.saved_csc_nb += 1
|
|
730
|
-
|
|
731
|
-
potentials = TDict()
|
|
732
|
-
# One channel processing
|
|
733
|
-
for csc_dict in color_space_dictionaries:
|
|
734
|
-
self.image = combine_color_spaces(csc_dict, self.all_c_spaces, subtract_background)
|
|
735
|
-
if kmeans_clust_nb is not None and (biomask is not None or backmask is not None):
|
|
736
|
-
self.binary_image, self.binary_image2, self.bio_label, self.bio_label2 = kmeans(self.image, self.image2, kmeans_clust_nb, biomask, backmask)
|
|
737
|
-
else:
|
|
738
|
-
self.binary_image = otsu_thresholding(self.image)
|
|
739
|
-
surf = np.sum(self.binary_image)
|
|
740
|
-
if surf < total_surfarea:
|
|
741
|
-
nb, shapes = cv2.connectedComponents(self.binary_image)
|
|
742
|
-
outside_pixels = np.sum(self.binary_image * out_of_arenas)
|
|
743
|
-
inside_pixels = np.sum(self.binary_image * arenas_mask)
|
|
744
|
-
if outside_pixels < inside_pixels:
|
|
745
|
-
if (nb > concomp_nb[0] - 1) and (nb < concomp_nb[1]):
|
|
746
|
-
in_common = np.sum(ref_image * self.binary_image)
|
|
747
|
-
if in_common > 0:
|
|
748
|
-
nb, shapes, stats, centroids = cv2.connectedComponentsWithStats(self.binary_image)
|
|
749
|
-
nb -= 1
|
|
750
|
-
if np.all(np.sort(stats[:, 4])[:-1] < max_shape_size):
|
|
751
|
-
c_space = list(csc_dict.keys())[0]
|
|
752
|
-
self.converted_images_list.append(self.image)
|
|
753
|
-
self.saved_images_list.append(self.binary_image)
|
|
754
|
-
self.saved_color_space_list.append(csc_dict)
|
|
755
|
-
self.combination_features[self.saved_csc_nb, :3] = csc_dict[c_space]
|
|
756
|
-
self.combination_features[self.saved_csc_nb, cc_nb_idx] = nb
|
|
757
|
-
self.combination_features[self.saved_csc_nb, area_idx] = surf
|
|
758
|
-
self.combination_features[self.saved_csc_nb, out_of_arenas_idx] = outside_pixels
|
|
759
|
-
self.combination_features[self.saved_csc_nb, in_arena_idx] = inside_pixels
|
|
760
|
-
self.combination_features[self.saved_csc_nb, surf_in_common_idx] = in_common
|
|
761
|
-
if biomask is not None:
|
|
762
|
-
self.combination_features[self.saved_csc_nb, biosum_idx] = np.sum(
|
|
763
|
-
self.binary_image[biomask[0], biomask[1]])
|
|
764
|
-
if backmask is not None:
|
|
765
|
-
self.combination_features[self.saved_csc_nb, backsum_idx] = np.sum(
|
|
766
|
-
(1 - self.binary_image)[backmask[0], backmask[1]])
|
|
767
|
-
if np.isin(c_space, list(potentials.keys())):
|
|
768
|
-
potentials[c_space] += csc_dict[c_space]
|
|
769
|
-
else:
|
|
770
|
-
potentials[c_space] = csc_dict[c_space]
|
|
771
|
-
self.saved_csc_nb += 1
|
|
772
|
-
if len(potentials) > 0:
|
|
773
|
-
# All combination processing
|
|
774
|
-
|
|
775
|
-
# Add a combination of all selected channels :
|
|
776
|
-
self.saved_color_space_list.append(potentials)
|
|
777
|
-
self.image = combine_color_spaces(potentials, self.all_c_spaces, subtract_background)
|
|
778
|
-
if kmeans_clust_nb is not None and (biomask is not None or backmask is not None):
|
|
779
|
-
self.binary_image, self.binary_image2, self.bio_label, self.bio_label2 = kmeans(self.image, kmeans_clust_nb=kmeans_clust_nb, biomask=biomask, backmask=backmask)
|
|
780
|
-
else:
|
|
781
|
-
self.binary_image = otsu_thresholding(self.image)
|
|
782
|
-
surf = self.binary_image.sum()
|
|
783
|
-
nb, shapes = cv2.connectedComponents(self.binary_image)
|
|
784
|
-
nb -= 1
|
|
785
|
-
outside_pixels = np.sum(self.binary_image * out_of_arenas)
|
|
786
|
-
inside_pixels = np.sum(self.binary_image * arenas_mask)
|
|
787
|
-
in_common = np.sum(ref_image * self.binary_image)
|
|
788
|
-
self.converted_images_list.append(self.image)
|
|
789
|
-
self.saved_images_list.append(self.binary_image)
|
|
790
|
-
self.saved_color_space_list.append(potentials)
|
|
791
|
-
self.combination_features[self.saved_csc_nb, :3] = list(potentials.values())[0]
|
|
792
|
-
self.combination_features[self.saved_csc_nb, cc_nb_idx] = nb
|
|
793
|
-
self.combination_features[self.saved_csc_nb, area_idx] = surf
|
|
794
|
-
self.combination_features[self.saved_csc_nb, out_of_arenas_idx] = outside_pixels
|
|
795
|
-
self.combination_features[self.saved_csc_nb, in_arena_idx] = inside_pixels
|
|
796
|
-
self.combination_features[self.saved_csc_nb, surf_in_common_idx] = in_common
|
|
797
|
-
if biomask is not None:
|
|
798
|
-
self.combination_features[self.saved_csc_nb, biosum_idx] = np.sum(
|
|
799
|
-
self.binary_image[biomask[0], biomask[1]])
|
|
800
|
-
if backmask is not None:
|
|
801
|
-
self.combination_features[self.saved_csc_nb, backsum_idx] = np.sum(
|
|
802
|
-
(1 - self.binary_image)[backmask[0], backmask[1]])
|
|
803
|
-
self.saved_csc_nb += 1
|
|
804
|
-
# All combination processing
|
|
805
|
-
# Try to remove color space one by one
|
|
806
|
-
i = 0
|
|
807
|
-
original_length = len(potentials)
|
|
808
|
-
while np.logical_and(len(potentials) > 1, i < original_length // 2):
|
|
809
|
-
color_space_to_remove = TList()
|
|
810
|
-
# The while loop until one col space remains or the removal of one implies a strong enough area change
|
|
811
|
-
previous_c_space = list(potentials.keys())[-1]
|
|
812
|
-
for c_space in potentials.keys():
|
|
813
|
-
try_potentials = potentials.copy()
|
|
814
|
-
try_potentials.pop(c_space)
|
|
815
|
-
if i > 0:
|
|
816
|
-
try_potentials.pop(previous_c_space)
|
|
817
|
-
self.image = combine_color_spaces(try_potentials, self.all_c_spaces, subtract_background)
|
|
818
|
-
if kmeans_clust_nb is not None and (biomask is not None or backmask is not None):
|
|
819
|
-
self.binary_image, self.binary_image2, self.bio_label, self.bio_label2 = kmeans(self.image, kmeans_clust_nb=kmeans_clust_nb, biomask=biomask, backmask=backmask)
|
|
820
|
-
else:
|
|
821
|
-
self.binary_image = otsu_thresholding(self.image)
|
|
822
|
-
surf = np.sum(self.binary_image)
|
|
823
|
-
if surf < total_surfarea:
|
|
824
|
-
nb, shapes = cv2.connectedComponents(self.binary_image)
|
|
825
|
-
outside_pixels = np.sum(self.binary_image * out_of_arenas)
|
|
826
|
-
inside_pixels = np.sum(self.binary_image * arenas_mask)
|
|
827
|
-
if outside_pixels < inside_pixels:
|
|
828
|
-
if (nb > concomp_nb[0] - 1) and (nb < concomp_nb[1]):
|
|
829
|
-
in_common = np.sum(ref_image * self.binary_image)
|
|
830
|
-
if in_common > 0:
|
|
831
|
-
nb, shapes, stats, centroids = cv2.connectedComponentsWithStats(self.binary_image)
|
|
832
|
-
nb -= 1
|
|
833
|
-
if np.all(np.sort(stats[:, 4])[:-1] < max_shape_size):
|
|
834
|
-
# If a color space remove fits in the requirements, we store its values
|
|
835
|
-
self.converted_images_list.append(self.image)
|
|
836
|
-
self.saved_images_list.append(self.binary_image)
|
|
837
|
-
self.saved_color_space_list.append(try_potentials)
|
|
838
|
-
self.combination_features[self.saved_csc_nb, cc_nb_idx] = nb
|
|
839
|
-
self.combination_features[self.saved_csc_nb, area_idx] = surf
|
|
840
|
-
self.combination_features[self.saved_csc_nb, out_of_arenas_idx] = outside_pixels
|
|
841
|
-
self.combination_features[self.saved_csc_nb, in_arena_idx] = inside_pixels
|
|
842
|
-
self.combination_features[self.saved_csc_nb, surf_in_common_idx] = in_common
|
|
843
|
-
if biomask is not None:
|
|
844
|
-
self.combination_features[self.saved_csc_nb, biosum_idx] = np.sum(
|
|
845
|
-
self.binary_image[biomask[0], biomask[1]])
|
|
846
|
-
if backmask is not None:
|
|
847
|
-
self.combination_features[self.saved_csc_nb, backsum_idx] = np.sum(
|
|
848
|
-
(1 - self.binary_image)[backmask[0], backmask[1]])
|
|
849
|
-
self.saved_csc_nb += 1
|
|
850
|
-
color_space_to_remove.append(c_space)
|
|
851
|
-
if i > 0:
|
|
852
|
-
color_space_to_remove.append(previous_c_space)
|
|
853
|
-
# If it does not (if it did not pass every "if" layers), we definitely remove that color space
|
|
854
|
-
previous_c_space = c_space
|
|
855
|
-
color_space_to_remove = np.unique(color_space_to_remove)
|
|
856
|
-
for remove_col_space in color_space_to_remove:
|
|
857
|
-
potentials.pop(remove_col_space)
|
|
858
|
-
i += 1
|
|
859
|
-
if np.logical_and(len(potentials) > 0, i > 1):
|
|
860
|
-
self.converted_images_list.append(self.image)
|
|
861
|
-
self.saved_images_list.append(self.binary_image)
|
|
862
|
-
self.saved_color_space_list.append(potentials)
|
|
863
|
-
self.combination_features[self.saved_csc_nb, :3] = list(potentials.values())[0]
|
|
864
|
-
self.combination_features[self.saved_csc_nb, cc_nb_idx] = nb
|
|
865
|
-
self.combination_features[self.saved_csc_nb, area_idx] = surf
|
|
866
|
-
self.combination_features[self.saved_csc_nb, out_of_arenas_idx] = outside_pixels
|
|
867
|
-
self.combination_features[self.saved_csc_nb, in_arena_idx] = inside_pixels
|
|
868
|
-
self.combination_features[self.saved_csc_nb, surf_in_common_idx] = in_common
|
|
869
|
-
if biomask is not None:
|
|
870
|
-
self.combination_features[self.saved_csc_nb, biosum_idx] = np.sum(
|
|
871
|
-
self.binary_image[biomask[0], biomask[1]])
|
|
872
|
-
if backmask is not None:
|
|
873
|
-
self.combination_features[self.saved_csc_nb, backsum_idx] = np.sum(
|
|
874
|
-
(1 - self.binary_image)[backmask[0], backmask[1]])
|
|
875
|
-
self.saved_csc_nb += 1
|
|
876
|
-
|
|
877
|
-
self.combination_features = self.combination_features[:self.saved_csc_nb, :]
|
|
878
|
-
# Among all potentials, select the best one, according to criterion decreasing in importance
|
|
879
|
-
cc_efficiency_order = np.argsort(self.combination_features[:, surf_in_common_idx] + self.combination_features[:, in_arena_idx] - self.combination_features[:, out_of_arenas_idx])
|
|
880
|
-
|
|
881
|
-
# Save and return a dictionnary containing the selected color space combinations
|
|
882
|
-
# and their corresponding binary images
|
|
883
|
-
self.im_combinations = []
|
|
884
|
-
for saved_csc in cc_efficiency_order:
|
|
885
|
-
if len(self.saved_color_space_list[saved_csc]) > 0:
|
|
886
|
-
self.im_combinations.append({})
|
|
887
|
-
self.im_combinations[len(self.im_combinations) - 1]["csc"] = {}
|
|
888
|
-
self.im_combinations[len(self.im_combinations) - 1]["csc"]['logical'] = 'None'
|
|
889
|
-
for k, v in self.saved_color_space_list[saved_csc].items():
|
|
890
|
-
self.im_combinations[len(self.im_combinations) - 1]["csc"][k] = v
|
|
891
|
-
self.im_combinations[len(self.im_combinations) - 1]["binary_image"] = self.saved_images_list[saved_csc]
|
|
892
|
-
self.im_combinations[len(self.im_combinations) - 1]["converted_image"] = np.round(self.converted_images_list[
|
|
893
|
-
saved_csc]).astype(np.uint8)
|
|
894
|
-
self.saved_color_space_list = []
|
|
895
|
-
self.saved_images_list = None
|
|
896
|
-
self.converted_images_list = None
|
|
897
|
-
self.combination_features = None
|
|
898
|
-
|
|
899
|
-
def network_detection(self, arenas_mask: NDArray=None, pseudopod_min_size: int=50, csc_dict: dict=None, biomask=None, backmask=None):
|
|
600
|
+
def network_detection(self, arenas_mask: NDArray=None, pseudopod_min_size: int=50, csc_dict: dict=None, lighter_background: bool= None, bio_mask=None, back_mask=None):
|
|
900
601
|
"""
|
|
901
602
|
Network Detection Function
|
|
902
603
|
|
|
@@ -911,9 +612,11 @@ class OneImageAnalysis:
|
|
|
911
612
|
csc_dict : dict, optional
|
|
912
613
|
A dictionary containing color space conversion parameters. If None,
|
|
913
614
|
defaults to {'bgr': np.array((1, 1, 1), np.int8), 'logical': 'None'}
|
|
914
|
-
|
|
615
|
+
lighter_background : bool, optional
|
|
616
|
+
Whether the background is lighter or not
|
|
617
|
+
bio_mask : NDArray, optional
|
|
915
618
|
The mask for biological objects in the image.
|
|
916
|
-
|
|
619
|
+
back_mask : NDArray, optional
|
|
917
620
|
The background mask.
|
|
918
621
|
|
|
919
622
|
Notes
|
|
@@ -935,7 +638,10 @@ class OneImageAnalysis:
|
|
|
935
638
|
greyscale = self.image
|
|
936
639
|
NetDet = NetworkDetection(greyscale, possibly_filled_pixels=arenas_mask)
|
|
937
640
|
NetDet.get_best_network_detection_method()
|
|
938
|
-
lighter_background
|
|
641
|
+
if lighter_background is None:
|
|
642
|
+
lighter_background = True
|
|
643
|
+
if arenas_mask.any() and not arenas_mask.all():
|
|
644
|
+
lighter_background = NetDet.greyscale_image[arenas_mask > 0].mean() < NetDet.greyscale_image[arenas_mask == 0].mean()
|
|
939
645
|
NetDet.detect_pseudopods(lighter_background, pseudopod_min_size=pseudopod_min_size, only_one_connected_component=False)
|
|
940
646
|
NetDet.merge_network_with_pseudopods()
|
|
941
647
|
cc_efficiency_order = np.argsort(NetDet.quality_metrics)
|
|
@@ -1079,4 +785,3 @@ class OneImageAnalysis:
|
|
|
1079
785
|
self.y_boundaries, y_max_sum = self.projection_to_get_peaks_boundaries(axis=1)
|
|
1080
786
|
self.x_boundaries, x_max_sum = self.projection_to_get_peaks_boundaries(axis=0)
|
|
1081
787
|
|
|
1082
|
-
|