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.
Files changed (44) 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 +155 -0
  5. cellects/core/__init__.py +0 -0
  6. cellects/core/cellects_paths.py +31 -0
  7. cellects/core/cellects_threads.py +1451 -0
  8. cellects/core/motion_analysis.py +2010 -0
  9. cellects/core/one_image_analysis.py +1061 -0
  10. cellects/core/one_video_per_blob.py +540 -0
  11. cellects/core/program_organizer.py +1316 -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 +790 -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 +2066 -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/image_segmentation.py +706 -0
  29. cellects/image_analysis/morphological_operations.py +1635 -0
  30. cellects/image_analysis/network_functions.py +1757 -0
  31. cellects/image_analysis/one_image_analysis_threads.py +289 -0
  32. cellects/image_analysis/progressively_add_distant_shapes.py +508 -0
  33. cellects/image_analysis/shape_descriptors.py +1016 -0
  34. cellects/utils/__init__.py +0 -0
  35. cellects/utils/decorators.py +14 -0
  36. cellects/utils/formulas.py +637 -0
  37. cellects/utils/load_display_save.py +1054 -0
  38. cellects/utils/utilitarian.py +490 -0
  39. cellects-0.1.2.dist-info/LICENSE.odt +0 -0
  40. cellects-0.1.2.dist-info/METADATA +132 -0
  41. cellects-0.1.2.dist-info/RECORD +44 -0
  42. cellects-0.1.2.dist-info/WHEEL +5 -0
  43. cellects-0.1.2.dist-info/entry_points.txt +2 -0
  44. 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