cellects 0.1.2__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/__init__.py +0 -0
- cellects/__main__.py +49 -0
- cellects/config/__init__.py +0 -0
- cellects/config/all_vars_dict.py +155 -0
- cellects/core/__init__.py +0 -0
- cellects/core/cellects_paths.py +31 -0
- cellects/core/cellects_threads.py +1451 -0
- cellects/core/motion_analysis.py +2010 -0
- cellects/core/one_image_analysis.py +1061 -0
- cellects/core/one_video_per_blob.py +540 -0
- cellects/core/program_organizer.py +1316 -0
- cellects/core/script_based_run.py +154 -0
- cellects/gui/__init__.py +0 -0
- cellects/gui/advanced_parameters.py +1258 -0
- cellects/gui/cellects.py +189 -0
- cellects/gui/custom_widgets.py +790 -0
- cellects/gui/first_window.py +449 -0
- cellects/gui/if_several_folders_window.py +239 -0
- cellects/gui/image_analysis_window.py +2066 -0
- cellects/gui/required_output.py +232 -0
- cellects/gui/video_analysis_window.py +656 -0
- cellects/icons/__init__.py +0 -0
- cellects/icons/cellects_icon.icns +0 -0
- cellects/icons/cellects_icon.ico +0 -0
- cellects/image_analysis/__init__.py +0 -0
- cellects/image_analysis/cell_leaving_detection.py +54 -0
- cellects/image_analysis/cluster_flux_study.py +102 -0
- cellects/image_analysis/image_segmentation.py +706 -0
- cellects/image_analysis/morphological_operations.py +1635 -0
- cellects/image_analysis/network_functions.py +1757 -0
- cellects/image_analysis/one_image_analysis_threads.py +289 -0
- cellects/image_analysis/progressively_add_distant_shapes.py +508 -0
- cellects/image_analysis/shape_descriptors.py +1016 -0
- cellects/utils/__init__.py +0 -0
- cellects/utils/decorators.py +14 -0
- cellects/utils/formulas.py +637 -0
- cellects/utils/load_display_save.py +1054 -0
- cellects/utils/utilitarian.py +490 -0
- cellects-0.1.2.dist-info/LICENSE.odt +0 -0
- cellects-0.1.2.dist-info/METADATA +132 -0
- cellects-0.1.2.dist-info/RECORD +44 -0
- cellects-0.1.2.dist-info/WHEEL +5 -0
- cellects-0.1.2.dist-info/entry_points.txt +2 -0
- cellects-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Progressively Add Distant Shapes Module
|
|
5
|
+
|
|
6
|
+
This module contains the `ProgressivelyAddDistantShapes` class which is designed to analyze
|
|
7
|
+
and connect shapes in binary images based on their size and distance from a main shape. It can
|
|
8
|
+
progressively grow bridges between shapes in binary video sequences, with growth speeds that depend on neighboring growth speed.
|
|
9
|
+
|
|
10
|
+
The module provides functionality to:
|
|
11
|
+
- Check and adjust main shape labels
|
|
12
|
+
- Consider shapes based on size criteria
|
|
13
|
+
- Connect shapes that meet distance and size requirements
|
|
14
|
+
- Expand small shapes toward the main shape
|
|
15
|
+
- Modify past analysis by progressively filling pixels based on shape growth patterns
|
|
16
|
+
|
|
17
|
+
Classes:
|
|
18
|
+
ProgressivelyAddDistantShapes: Main class for analyzing and connecting shapes in binary images.
|
|
19
|
+
|
|
20
|
+
Functions:
|
|
21
|
+
make_gravity_field: Creates a gravity field around the main shape.
|
|
22
|
+
CompareNeighborsWithValue: Compares neighbor values in an array.
|
|
23
|
+
get_radius_distance_against_time: Calculates the relationship between distance and time for shape expansion.
|
|
24
|
+
|
|
25
|
+
This module is particularly useful in image analysis tasks where shapes need to be tracked and connected over time based on spatial relationships.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
from copy import deepcopy
|
|
30
|
+
import numpy as np
|
|
31
|
+
import cv2
|
|
32
|
+
from numpy.typing import NDArray
|
|
33
|
+
from typing import Tuple
|
|
34
|
+
from cellects.image_analysis.morphological_operations import cross_33, rounded_inverted_distance_transform, CompareNeighborsWithValue, get_radius_distance_against_time, cc, rhombus_55, keep_shape_connected_with_ref
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ProgressivelyAddDistantShapes:
|
|
39
|
+
"""
|
|
40
|
+
This class checks new potential shapes sizes and distance to a main shape.
|
|
41
|
+
|
|
42
|
+
If these sizes and distance match requirements, create a bridge between
|
|
43
|
+
these and the main shape. Then, the `modify_past_analysis` method progressively grows that bridge
|
|
44
|
+
in a binary video. Bridge growth speed depends on neighboring growth speed.
|
|
45
|
+
|
|
46
|
+
Attributes
|
|
47
|
+
----------
|
|
48
|
+
new_order : numpy.ndarray
|
|
49
|
+
A binary image of all shapes detected at t.
|
|
50
|
+
main_shape : numpy.ndarray
|
|
51
|
+
A binary image of the main shape (1) at t - 1.
|
|
52
|
+
stats : numpy.ndarray
|
|
53
|
+
Statistics about the connected components found in `new_order`.
|
|
54
|
+
max_distance : int
|
|
55
|
+
The maximal distance for a shape from new_potentials to get bridged.
|
|
56
|
+
gravity_field : numpy.ndarray
|
|
57
|
+
The gravity field used for connecting shapes.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
new_potentials : numpy.ndarray
|
|
62
|
+
A binary image of all shapes detected at t.
|
|
63
|
+
previous_shape : numpy.ndarray
|
|
64
|
+
A binary image of the main shape (1) at t - 1.
|
|
65
|
+
max_distance : int
|
|
66
|
+
The maximal distance for a shape from new_potentials to get bridged.
|
|
67
|
+
|
|
68
|
+
Methods
|
|
69
|
+
-------
|
|
70
|
+
check_main_shape_label(previous_shape)
|
|
71
|
+
Check if the main shape label is correctly set.
|
|
72
|
+
consider_shapes_sizes(min_shape_size=None, max_shape_size=None)
|
|
73
|
+
Consider shapes sizes and eliminate too small or large ones.
|
|
74
|
+
connect_shapes(only_keep_connected_shapes, rank_connecting_pixels, intensity_valley=None)
|
|
75
|
+
Connect shapes that are within the maximal distance and of appropriate size.
|
|
76
|
+
_expand_smalls_toward_main()
|
|
77
|
+
Expand small shapes toward the main shape.
|
|
78
|
+
|
|
79
|
+
Example
|
|
80
|
+
-------
|
|
81
|
+
>>> new_potentials = np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]])
|
|
82
|
+
>>> previous_shape = np.array([[0, 1, 0], [1, 0, 0], [0, 1, 0]])
|
|
83
|
+
>>> max_distance = 2
|
|
84
|
+
>>> bridge_shapes = ProgressivelyAddDistantShapes(new_potentials, previous_shape, max_distance)
|
|
85
|
+
>>> bridge_shapes.consider_shapes_sizes(min_shape_size=2, max_shape_size=10)
|
|
86
|
+
>>> bridge_shapes.connect_shapes(only_keep_connected_shapes=True, rank_connecting_pixels=False)
|
|
87
|
+
>>> print(bridge_shapes.expanded_shape)
|
|
88
|
+
[[0 1 0]
|
|
89
|
+
[1 1 1]
|
|
90
|
+
[0 1 0]]
|
|
91
|
+
"""
|
|
92
|
+
def __init__(self, new_potentials: NDArray[np.uint8], previous_shape: NDArray[np.uint8], max_distance):
|
|
93
|
+
"""
|
|
94
|
+
Find connected components and update order.
|
|
95
|
+
|
|
96
|
+
This class processes new potentials and previous shape to find
|
|
97
|
+
connected components and updates the main shape based on a maximum
|
|
98
|
+
distance threshold.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
new_potentials : ndarray of uint8
|
|
103
|
+
The new potential values to process.
|
|
104
|
+
previous_shape : ndarray of uint8
|
|
105
|
+
The previous shape information.
|
|
106
|
+
max_distance :
|
|
107
|
+
The maximum distance threshold for processing.
|
|
108
|
+
|
|
109
|
+
Attributes
|
|
110
|
+
----------
|
|
111
|
+
new_order : ndarray of uint8
|
|
112
|
+
The result after applying logical OR on `new_potentials` and
|
|
113
|
+
`previous_shape`.
|
|
114
|
+
stats : ndarray of int64
|
|
115
|
+
Statistics of the connected components.
|
|
116
|
+
centers : ndarray of float64
|
|
117
|
+
Centers of the connected components.
|
|
118
|
+
main_shape : ndarray of uint8
|
|
119
|
+
The main shape array initialized to zeros.
|
|
120
|
+
max_distance : int
|
|
121
|
+
The maximum distance threshold for processing.
|
|
122
|
+
|
|
123
|
+
Examples
|
|
124
|
+
--------
|
|
125
|
+
>>> new_potentials = np.array([[0, 1, 2], [3, 4, 5]])
|
|
126
|
+
>>> previous_shape = np.array([[0, 1, 0], [1, 0, 1]])
|
|
127
|
+
>>> max_distance = 2
|
|
128
|
+
>>> obj = ClassName(new_potentials, previous_shape, max_distance)
|
|
129
|
+
>>> print(obj.new_order)
|
|
130
|
+
[[1 1 2]
|
|
131
|
+
[1 1 1]]
|
|
132
|
+
"""
|
|
133
|
+
self.new_order = np.logical_or(new_potentials, previous_shape).astype(np.uint8)
|
|
134
|
+
self.new_order, self.stats, centers = cc(self.new_order)
|
|
135
|
+
self.main_shape = np.zeros(self.new_order.shape, np.uint8)
|
|
136
|
+
self.max_distance = max_distance
|
|
137
|
+
self._check_main_shape_label(previous_shape)
|
|
138
|
+
|
|
139
|
+
def _check_main_shape_label(self, previous_shape: NDArray[np.uint8]):
|
|
140
|
+
"""
|
|
141
|
+
Check and update main shape label based on previous shape data when multiple shapes exist in new_order.
|
|
142
|
+
|
|
143
|
+
This method ensures consistent labeling of the primary shape (labeled 1) in `new_order` by analyzing overlaps
|
|
144
|
+
with labels from a prior segmentation step. If multiple candidate labels exist for the main shape, it selects
|
|
145
|
+
the one with the highest pixel count and swaps its label with '1' in both `new_order` and associated statistics.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
previous_shape
|
|
150
|
+
Input array representing previous segmentation labels used to identify the primary shape when
|
|
151
|
+
`new_order` contains multiple potential candidates (labels > 1).
|
|
152
|
+
|
|
153
|
+
Examples
|
|
154
|
+
--------
|
|
155
|
+
>>> new_potentials = np.array([[1, 1, 0], [0, 0, 0], [0, 1, 1]])
|
|
156
|
+
>>> previous_shape = np.array([[0, 0, 0], [0, 0, 0], [0, 1, 1]])
|
|
157
|
+
>>> max_distance = 2
|
|
158
|
+
>>> pads = ProgressivelyAddDistantShapes(new_potentials, previous_shape, max_distance)
|
|
159
|
+
>>> pads.main_shape
|
|
160
|
+
array([[0, 0, 0],
|
|
161
|
+
[0, 0, 0],
|
|
162
|
+
[0, 1, 1]], dtype=np.uint8)
|
|
163
|
+
"""
|
|
164
|
+
if np.any(self.new_order > 1):
|
|
165
|
+
# If there is at least one pixel of the previous shape that is not among pixels labelled 1,
|
|
166
|
+
# clarify who's main shape
|
|
167
|
+
main_shape_label = np.unique(previous_shape * self.new_order)
|
|
168
|
+
main_shape_label = main_shape_label[main_shape_label != 0]
|
|
169
|
+
|
|
170
|
+
# If the main shape is not labelled 1 in main_shape:
|
|
171
|
+
if not np.isin(1, main_shape_label):
|
|
172
|
+
# If it is not 1, find which label correspond to the previous shape
|
|
173
|
+
if len(main_shape_label) > 1:
|
|
174
|
+
pixel_sum_per_label = np.zeros(len(main_shape_label), dtype =np.uint64)
|
|
175
|
+
# Find out the label corresponding to the largest shape
|
|
176
|
+
for li, label in enumerate(main_shape_label):
|
|
177
|
+
pixel_sum_per_label[li] = self.new_order[self.new_order == label].sum()
|
|
178
|
+
main_shape_label = main_shape_label[np.argmax(pixel_sum_per_label)]
|
|
179
|
+
# Attribute the correct main shape
|
|
180
|
+
self.main_shape[self.new_order == main_shape_label] = 1
|
|
181
|
+
# Exchange the 1 and the main shape label in new_order image
|
|
182
|
+
not_one_idx = np.nonzero(self.new_order == main_shape_label)
|
|
183
|
+
one_idx = np.nonzero(self.new_order == 1)
|
|
184
|
+
self.new_order[not_one_idx[0], not_one_idx[1]] = 1
|
|
185
|
+
self.new_order[one_idx[0], one_idx[1]] = main_shape_label
|
|
186
|
+
# Do the same for stats
|
|
187
|
+
not_one_stats = deepcopy(self.stats[main_shape_label - 1, :])
|
|
188
|
+
self.stats[main_shape_label - 1, :] = self.stats[1, :]
|
|
189
|
+
self.stats[1, :] = not_one_stats
|
|
190
|
+
else:
|
|
191
|
+
#if np.any(previous_shape * (self.new_order == 1)):
|
|
192
|
+
# Create an image of the principal shape
|
|
193
|
+
self.main_shape[self.new_order == 1] = 1
|
|
194
|
+
else:
|
|
195
|
+
self.main_shape[np.nonzero(self.new_order)] = 1
|
|
196
|
+
|
|
197
|
+
def consider_shapes_sizes(self, min_shape_size: int=None, max_shape_size: int=None):
|
|
198
|
+
"""Filter shapes based on minimum and maximum size thresholds.
|
|
199
|
+
|
|
200
|
+
This method adjusts `new_order` by excluding indices of shapes that are either
|
|
201
|
+
smaller than `min_shape_size` or larger than `max_shape_size`. The main shape index
|
|
202
|
+
(1) is preserved even if it meets the filtering criteria. When no constraints apply,
|
|
203
|
+
the expanded shape defaults to the main shape.
|
|
204
|
+
|
|
205
|
+
Parameters
|
|
206
|
+
----------
|
|
207
|
+
min_shape_size : int, optional
|
|
208
|
+
Minimum allowed size for shapes (compared against 4th column of `self.stats`).
|
|
209
|
+
max_shape_size : int, optional
|
|
210
|
+
Maximum allowed size for shapes (compared against 4th column of `self.stats`).
|
|
211
|
+
|
|
212
|
+
Examples
|
|
213
|
+
--------
|
|
214
|
+
>>> new_potentials = np.array([[1, 1, 0], [0, 0, 0], [0, 1, 1]])
|
|
215
|
+
>>> previous_shape = np.array([[0, 0, 0], [0, 0, 0], [0, 1, 1]])
|
|
216
|
+
>>> max_distance = 2
|
|
217
|
+
>>> pads = ProgressivelyAddDistantShapes(new_potentials, previous_shape, max_distance)
|
|
218
|
+
>>> pads.consider_shapes_sizes(min_shape_size=2, max_shape_size=10)
|
|
219
|
+
>>> pads.new_order
|
|
220
|
+
array([[2, 2, 0],
|
|
221
|
+
[0, 0, 0],
|
|
222
|
+
[0, 1, 1]], dtype=np.uint8)
|
|
223
|
+
"""
|
|
224
|
+
if self.max_distance != 0:
|
|
225
|
+
# Eliminate too small and too large shapes
|
|
226
|
+
if min_shape_size is not None or max_shape_size is not None:
|
|
227
|
+
if min_shape_size is not None:
|
|
228
|
+
small_shapes = self.stats[:, 4] < min_shape_size
|
|
229
|
+
extreme_shapes = deepcopy(small_shapes)
|
|
230
|
+
if max_shape_size is not None:
|
|
231
|
+
large_shapes = self.stats[:, 4] > max_shape_size
|
|
232
|
+
extreme_shapes = deepcopy(large_shapes)
|
|
233
|
+
if min_shape_size is not None and max_shape_size is not None:
|
|
234
|
+
extreme_shapes = np.nonzero(np.logical_or(small_shapes, large_shapes))[0]
|
|
235
|
+
is_main_in_it = np.isin(extreme_shapes, 1)
|
|
236
|
+
if np.any(is_main_in_it):
|
|
237
|
+
extreme_shapes = np.delete(extreme_shapes, is_main_in_it)
|
|
238
|
+
for extreme_shape in extreme_shapes:
|
|
239
|
+
self.new_order[self.new_order == extreme_shape] = 0
|
|
240
|
+
else:
|
|
241
|
+
self.expanded_shape = self.main_shape
|
|
242
|
+
|
|
243
|
+
def _find_shape_connection_order(self):
|
|
244
|
+
# Dilate the main shape, progressively to infer in what order other shapes should be expanded toward it
|
|
245
|
+
other_shapes = np.zeros(self.main_shape.shape, np.uint8)
|
|
246
|
+
other_shapes[self.new_order > 1] = 1
|
|
247
|
+
new_order = deepcopy(self.new_order)
|
|
248
|
+
dil_main_shape = deepcopy(self.main_shape)
|
|
249
|
+
order_of_shapes_to_expand = np.empty(0, dtype=np.uint32)
|
|
250
|
+
nb = 3
|
|
251
|
+
while nb > 2:
|
|
252
|
+
dil_main_shape = cv2.dilate(dil_main_shape, rhombus_55)
|
|
253
|
+
connections = dil_main_shape * new_order
|
|
254
|
+
new_connections = np.unique(connections)[2:]
|
|
255
|
+
new_order[np.isin(new_order, new_connections)] = 1
|
|
256
|
+
order_of_shapes_to_expand = np.append(order_of_shapes_to_expand, new_connections)
|
|
257
|
+
connections[dil_main_shape > 0] = 1
|
|
258
|
+
connections[other_shapes > 0] = 1
|
|
259
|
+
nb, connections = cv2.connectedComponents(connections)
|
|
260
|
+
if len(order_of_shapes_to_expand) == 0:
|
|
261
|
+
order_of_shapes_to_expand = np.unique(new_order)[2:]
|
|
262
|
+
return order_of_shapes_to_expand
|
|
263
|
+
|
|
264
|
+
def _expand_smalls_toward_main(self):
|
|
265
|
+
"""Expands small shapes toward a main shape using morphological operations and gravity field analysis.
|
|
266
|
+
|
|
267
|
+
The method dilates the main shape to determine an order of expansion for connected regions.
|
|
268
|
+
Each identified region is iteratively expanded until overlapping with the main shape, guided by a gravity field gradient.
|
|
269
|
+
Results include both the final expanded binary mask and peak values from the gravity field during expansion phases.
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
numpy.ndarray[numpy.uint8]
|
|
274
|
+
Binary array where small shapes are fully expanded to connect with the main shape.
|
|
275
|
+
numpy.ndarray[numpy.uint32]
|
|
276
|
+
Array containing maximum detected field strengths for each expanded region, in order of connection.
|
|
277
|
+
|
|
278
|
+
Examples
|
|
279
|
+
--------
|
|
280
|
+
>>> new_potentials = np.array([[1, 1, 0], [0, 0, 0], [0, 1, 1]])
|
|
281
|
+
>>> previous_shape = np.array([[0, 0, 0], [0, 0, 0], [0, 1, 1]])
|
|
282
|
+
>>> max_distance = 3
|
|
283
|
+
>>> pads = ProgressivelyAddDistantShapes(new_potentials, previous_shape, max_distance)
|
|
284
|
+
>>> pads.consider_shapes_sizes(min_shape_size=2, max_shape_size=10)
|
|
285
|
+
>>> pads.gravity_field = make_gravity_field(pads.main_shape, max_distance=pads.max_distance, with_erosion=0)
|
|
286
|
+
>>> expanded_main, max_field_feelings = pads._expand_smalls_toward_main()
|
|
287
|
+
>>> print(expanded_main)
|
|
288
|
+
[[1 1 0]
|
|
289
|
+
[0 1 1]
|
|
290
|
+
[0 1 1]]
|
|
291
|
+
"""
|
|
292
|
+
simple_disk = cross_33
|
|
293
|
+
order_of_shapes_to_expand = self._find_shape_connection_order()
|
|
294
|
+
expanded_main = deepcopy(self.main_shape)
|
|
295
|
+
max_field_feelings = np.empty(0, dtype=np.uint32)
|
|
296
|
+
# Loop over each shape to connect, from the nearest to the furthest to the main shape
|
|
297
|
+
for shape_i in order_of_shapes_to_expand:# shape_i = order_of_shapes_to_expand[0]
|
|
298
|
+
current_shape = np.zeros(self.main_shape.shape, np.uint8)
|
|
299
|
+
current_shape[self.new_order == shape_i] = 1
|
|
300
|
+
dil = 0
|
|
301
|
+
# Dilate that shape until it overlaps the main shape
|
|
302
|
+
while np.logical_and(dil <= self.max_distance, not np.any(current_shape * expanded_main)):
|
|
303
|
+
dil += 1
|
|
304
|
+
rings = cv2.dilate(current_shape, simple_disk, iterations=1, borderType=cv2.BORDER_CONSTANT,
|
|
305
|
+
borderValue=0)
|
|
306
|
+
|
|
307
|
+
rings = self.gravity_field * (rings - current_shape)
|
|
308
|
+
max_field_feeling = np.max(rings) # np.min(rings[rings>0])
|
|
309
|
+
max_field_feelings = np.append(max_field_feeling, max_field_feelings)
|
|
310
|
+
if max_field_feeling > 0: # If there is no shape within max_distance range, quit the loop
|
|
311
|
+
|
|
312
|
+
if dil == 1:
|
|
313
|
+
initial_pixel_number = np.sum(rings == max_field_feeling)
|
|
314
|
+
while np.sum(rings == max_field_feeling) > initial_pixel_number:
|
|
315
|
+
shrinking_stick = CompareNeighborsWithValue(rings, 8, np.uint32)
|
|
316
|
+
shrinking_stick.is_equal(max_field_feeling, True)
|
|
317
|
+
rings[shrinking_stick.equal_neighbor_nb < 2] = 0
|
|
318
|
+
current_shape[rings == max_field_feeling] = 1
|
|
319
|
+
else:
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
expanded_main[current_shape != 0] = 1
|
|
323
|
+
return expanded_main, max_field_feelings
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def connect_shapes(self, only_keep_connected_shapes: bool, rank_connecting_pixels: bool, intensity_valley: NDArray=None):
|
|
327
|
+
"""Connects small shapes to a main shape using gravity field expansion and filtering based on distance and intensity conditions.
|
|
328
|
+
|
|
329
|
+
Extended Description
|
|
330
|
+
--------------------
|
|
331
|
+
When distant shapes of sufficient size are present, this method generates a gravity field around the main shape. It then expands smaller shapes toward the main one according to gradient values. If shapes fall within the gravity field range:
|
|
332
|
+
- Shapes not connected to the main one (via `only_keep_connected_shapes`) are filtered out.
|
|
333
|
+
- Connecting pixels between small and main shapes (via `rank_connecting_pixels`) receive distance-based ranking.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
only_keep_connected_shapes : bool
|
|
338
|
+
If True, filters expanded shapes to retain only those connected directly to the main shape.
|
|
339
|
+
rank_connecting_pixels : bool
|
|
340
|
+
If True, ranks connecting pixel extensions based on distance between small/main shapes.
|
|
341
|
+
intensity_valley : array-like, optional
|
|
342
|
+
Optional intensity values defining a valley region for gravity field calculation. Default is None.
|
|
343
|
+
|
|
344
|
+
Attributes
|
|
345
|
+
----------
|
|
346
|
+
gravity_field : ndarray or array-like
|
|
347
|
+
Stores the computed gravity field used to guide shape expansion.
|
|
348
|
+
expanded_shape : ndarray of dtype uint8
|
|
349
|
+
Final combined shape after processing; contains main and connected small shapes.
|
|
350
|
+
Examples
|
|
351
|
+
--------
|
|
352
|
+
>>> new_potentials = np.array([[1, 1, 0], [0, 0, 0], [0, 1, 1]])
|
|
353
|
+
>>> previous_shape = np.array([[0, 0, 0], [0, 0, 0], [0, 1, 1]])
|
|
354
|
+
>>> max_distance = 3
|
|
355
|
+
>>> pads = ProgressivelyAddDistantShapes(new_potentials, previous_shape, max_distance)
|
|
356
|
+
>>> pads.consider_shapes_sizes(min_shape_size=2, max_shape_size=10)
|
|
357
|
+
>>> pads.gravity_field = make_gravity_field(pads.main_shape, max_distance=pads.max_distance, with_erosion=0)
|
|
358
|
+
>>> pads.connect_shapes(only_keep_connected_shapes=False, rank_connecting_pixels=True)
|
|
359
|
+
>>> expanded_main, max_field_feelings = pads._expand_smalls_toward_main()
|
|
360
|
+
>>> print(expanded_main)
|
|
361
|
+
[[1 1 0]
|
|
362
|
+
[0 1 1]
|
|
363
|
+
[0 1 1]]
|
|
364
|
+
"""
|
|
365
|
+
# If there are distant shapes of the good size, run the following:
|
|
366
|
+
if self.max_distance != 0 and np.any(self.new_order > 1):
|
|
367
|
+
# The intensity valley method does not work yet, don't use it
|
|
368
|
+
if intensity_valley is not None:
|
|
369
|
+
self.gravity_field = intensity_valley # make sure that the values correspond to the coord
|
|
370
|
+
else:
|
|
371
|
+
# 1) faire un champ gravitationnel autour de la forme principale
|
|
372
|
+
self.gravity_field = rounded_inverted_distance_transform(self.main_shape, max_distance=self.max_distance, with_erosion=1)
|
|
373
|
+
|
|
374
|
+
# If there are near enough shapes, run the following
|
|
375
|
+
# 2) Dilate other shapes toward the main according to the gradient
|
|
376
|
+
other_shapes, max_field_feelings = self._expand_smalls_toward_main()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# plt.imshow(other_shapes)
|
|
380
|
+
# If there are shapes within gravity field range
|
|
381
|
+
if np.any(max_field_feelings > 0):
|
|
382
|
+
self.expanded_shape = np.zeros(self.main_shape.shape, np.uint8)
|
|
383
|
+
self.expanded_shape[np.nonzero(self.main_shape + other_shapes)] = 1
|
|
384
|
+
if only_keep_connected_shapes:
|
|
385
|
+
# Make sure that only shapes connected with the main one remain on the final image
|
|
386
|
+
expanded_shape = keep_shape_connected_with_ref(self.expanded_shape, self.main_shape)
|
|
387
|
+
if expanded_shape is not None:
|
|
388
|
+
self.expanded_shape = expanded_shape
|
|
389
|
+
if rank_connecting_pixels:
|
|
390
|
+
# Rate the extension of small shapes according to the distance between the small and the main shapes
|
|
391
|
+
self.distance_ranking_of_connecting_pixels()
|
|
392
|
+
#self.expanded_shape
|
|
393
|
+
# plt.imshow(self.expanded_shape)
|
|
394
|
+
else:
|
|
395
|
+
self.expanded_shape = self.main_shape
|
|
396
|
+
# Otherwise, end by putting the main shape as output
|
|
397
|
+
else:
|
|
398
|
+
self.expanded_shape = self.main_shape
|
|
399
|
+
|
|
400
|
+
# else:
|
|
401
|
+
# self.expanded_shape = other_shapes + self.main_shape
|
|
402
|
+
# self.expanded_shape[self.expanded_shape > 1] = 1
|
|
403
|
+
|
|
404
|
+
def distance_ranking_of_connecting_pixels(self):
|
|
405
|
+
"""
|
|
406
|
+
Calculate the distance ranking of connecting pixels.
|
|
407
|
+
|
|
408
|
+
This function computes a ranked extension map based on the difference between
|
|
409
|
+
`main_shape` and `expanded_shape`, modifies it using a gravity field, and then
|
|
410
|
+
updates the `expanded_shape` with this ranked extension.
|
|
411
|
+
"""
|
|
412
|
+
rated_extension = np.zeros(self.main_shape.shape, np.uint8)
|
|
413
|
+
rated_extension[(self.main_shape - self.expanded_shape) == 255] = 1
|
|
414
|
+
rated_extension = rated_extension * self.gravity_field
|
|
415
|
+
if np.any(rated_extension):
|
|
416
|
+
rated_extension[np.nonzero(rated_extension)] -= np.min(
|
|
417
|
+
rated_extension[np.nonzero(rated_extension)]) - 1
|
|
418
|
+
rated_extension *= self.expanded_shape
|
|
419
|
+
self.expanded_shape += rated_extension
|
|
420
|
+
|
|
421
|
+
#binary_video = self.binary[(self.step // 2):(self.t + 1), :, :]
|
|
422
|
+
#draft_seg = self.segmentation[(self.step // 2):(self.t + 1), :, :]
|
|
423
|
+
def modify_past_analysis(self, binary_video: NDArray[np.uint8], draft_seg: NDArray[np.uint8]) -> NDArray[np.uint8]:
|
|
424
|
+
"""
|
|
425
|
+
Modify past analysis based on binary video and draft segmentation.
|
|
426
|
+
|
|
427
|
+
This method modifies the past analysis by updating `binary_video` with
|
|
428
|
+
information from `draft_seg`, and then iteratively filling pixels based on
|
|
429
|
+
expansion timings.
|
|
430
|
+
|
|
431
|
+
Parameters
|
|
432
|
+
----------
|
|
433
|
+
binary_video : ndarray of uint8
|
|
434
|
+
Input binary video to be modified.
|
|
435
|
+
draft_seg : ndarray of uint8
|
|
436
|
+
Draft segmentation used for expanding the shape.
|
|
437
|
+
|
|
438
|
+
Returns
|
|
439
|
+
-------
|
|
440
|
+
ndarray of uint8
|
|
441
|
+
Modified binary video after past analysis.
|
|
442
|
+
"""
|
|
443
|
+
self.binary_video = binary_video
|
|
444
|
+
self.draft_seg = draft_seg
|
|
445
|
+
self.expanded_shape[self.expanded_shape == 1] = 0
|
|
446
|
+
# Find the time at which the shape became connected to the expanded shape
|
|
447
|
+
# (i.e. the time to start looking for a growth)
|
|
448
|
+
distance_against_time, time_start, time_end = self.find_expansion_timings()
|
|
449
|
+
|
|
450
|
+
# Use that vector to progressively fill pixels at the same speed as shape grows
|
|
451
|
+
for t in np.arange(len(distance_against_time)):
|
|
452
|
+
image_garbage = (self.expanded_shape >= distance_against_time[t]).astype(np.uint8)
|
|
453
|
+
new_order, stats, centers = cc(image_garbage)
|
|
454
|
+
for comp_i in np.arange(1, stats.shape[0]):
|
|
455
|
+
past_image = deepcopy(self.binary_video[time_start + t, :, :])
|
|
456
|
+
with_new_comp = new_order == comp_i
|
|
457
|
+
past_image[with_new_comp] = 1
|
|
458
|
+
nb_comp, image_garbage = cv2.connectedComponents(past_image)
|
|
459
|
+
if nb_comp == 2:
|
|
460
|
+
self.binary_video[time_start + t, :, :][with_new_comp] = 1
|
|
461
|
+
#self.expanded_shape[self.expanded_shape > 0] = 1
|
|
462
|
+
#self.binary_video[time_end:, :, :] += self.expanded_shape
|
|
463
|
+
for t in np.arange(time_end, self.binary_video.shape[0]):
|
|
464
|
+
self.binary_video[t, :, :][np.nonzero(self.expanded_shape)] = 1
|
|
465
|
+
last_image = self.binary_video[t, :, :] + self.binary_video[t - 1, :, :]
|
|
466
|
+
last_image[last_image > 0] = 1
|
|
467
|
+
self.binary_video[-1, :, :] = last_image
|
|
468
|
+
return self.binary_video
|
|
469
|
+
|
|
470
|
+
def find_expansion_timings(self) -> Tuple[NDArray[np.float64], int, int]:
|
|
471
|
+
"""
|
|
472
|
+
Find the expansion timings of a shape in binary video.
|
|
473
|
+
|
|
474
|
+
This method calculates the time at which an expanded shape reaches
|
|
475
|
+
the main shape, as well as the distance and time relationship during
|
|
476
|
+
expansion.
|
|
477
|
+
|
|
478
|
+
Returns
|
|
479
|
+
-------
|
|
480
|
+
distance_against_time : ndarray of float64
|
|
481
|
+
Array representing the distance against time.
|
|
482
|
+
time_start : int
|
|
483
|
+
The start time of expansion in frames.
|
|
484
|
+
time_end : int
|
|
485
|
+
The end time of expansion in frames.
|
|
486
|
+
|
|
487
|
+
Raises
|
|
488
|
+
------
|
|
489
|
+
AttributeError
|
|
490
|
+
If 'binary_video', 'expanded_shape' or 'main_shape' are not defined.
|
|
491
|
+
"""
|
|
492
|
+
max_t = self.binary_video.shape[0] - 1
|
|
493
|
+
dilated_one = cv2.dilate(self.expanded_shape, cross_33)
|
|
494
|
+
# Find the time at which the nearest pixel of the expanded_shape si reached by the main shape
|
|
495
|
+
closest_pixels = np.zeros(self.main_shape.shape, dtype=np.uint8)
|
|
496
|
+
closest_pixels[self.expanded_shape == np.max(dilated_one)] = 1
|
|
497
|
+
expand_start = max_t
|
|
498
|
+
# Loop until there is no overlap between the dilated added shape and the original shape
|
|
499
|
+
# Stop one frame before in order to obtain the exact reaching moment.
|
|
500
|
+
while np.any(self.binary_video[expand_start - 1, :, :] * closest_pixels):
|
|
501
|
+
expand_start -= 1
|
|
502
|
+
|
|
503
|
+
# Find the relationship between distance and time
|
|
504
|
+
distance_against_time, time_start, time_end = get_radius_distance_against_time(
|
|
505
|
+
self.draft_seg[expand_start:(max_t + 1), :, :], dilated_one)
|
|
506
|
+
time_start += expand_start
|
|
507
|
+
time_end += expand_start
|
|
508
|
+
return distance_against_time, time_start, time_end
|