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,1016 @@
1
+ """Module for computing shape descriptors from binary images.
2
+
3
+ This module provides a framework for calculating various geometric and statistical
4
+ descriptors of shapes in binary images through configurable dictionaries and a core class.
5
+ Supported metrics include area, perimeter, axis lengths, orientation, and more.
6
+ Descriptor computation is controlled via category dictionaries (e.g., `descriptors_categories`)
7
+ and implemented as methods in the ShapeDescriptors class.
8
+
9
+ Classes
10
+ -------
11
+ ShapeDescriptors : Class to compute various descriptors for a binary image
12
+
13
+ Notes
14
+ -----
15
+ Relies on OpenCV and NumPy for image processing operations.
16
+ Shape descriptors: The following names, lists and computes all the variables describing a shape in a binary image.
17
+ If you want to allow the software to compute another variable:
18
+ 1) In the following dicts and list, you need to:
19
+ add the variable name and whether to compute it (True/False) by default
20
+ 2) In the ShapeDescriptors class:
21
+ add a method to compute that variable
22
+ 3) In the init method of the ShapeDescriptors class
23
+ attribute a None value to the variable that store it
24
+ add a if condition in the for loop to compute that variable when its name appear in the wanted_descriptors_list
25
+ """
26
+ import cv2
27
+ import numpy as np
28
+ from copy import deepcopy
29
+ from cellects.utils.utilitarian import translate_dict
30
+ from cellects.utils.formulas import get_inertia_axes, get_standard_deviations, get_skewness, get_kurtosis
31
+
32
+ descriptors_categories = {'area': True, 'perimeter': False, 'circularity': False, 'rectangularity': False,
33
+ 'total_hole_area': False, 'solidity': False, 'convexity': False, 'eccentricity': False,
34
+ 'euler_number': False, 'standard_deviation_xy': False, 'skewness_xy': False,
35
+ 'kurtosis_xy': False, 'major_axes_len_and_angle': True, 'iso_digi_analysis': False,
36
+ 'oscilacyto_analysis': False, 'network_analysis': False, 'graph_extraction': False,
37
+ 'fractal_analysis': False
38
+ }
39
+
40
+ descriptors_names_to_display = ['Area', 'Perimeter', 'Circularity', 'Rectangularity', 'Total hole area',
41
+ 'Solidity', 'Convexity', 'Eccentricity', 'Euler number', 'Standard deviation xy',
42
+ 'Skewness xy', 'Kurtosis xy', 'Major axes lengths and angle',
43
+ 'Growth transitions', 'Oscillations', 'Network', 'Graph',
44
+ 'Fractals'
45
+ ]#, 'Oscillating cluster nb and size'
46
+
47
+ from_shape_descriptors_class = {'area': True, 'perimeter': False, 'circularity': False, 'rectangularity': False,
48
+ 'total_hole_area': False, 'solidity': False, 'convexity': False, 'eccentricity': False,
49
+ 'euler_number': False, 'standard_deviation_y': False, 'standard_deviation_x': False,
50
+ 'skewness_y': False, 'skewness_x': False, 'kurtosis_y': False, 'kurtosis_x': False,
51
+ 'major_axis_len': True, 'minor_axis_len': True, 'axes_orientation': True
52
+ }
53
+
54
+ descriptors = deepcopy(from_shape_descriptors_class)
55
+ descriptors.update({'cluster_number': False, 'mean_cluster_area': False, 'minkowski_dimension': False,
56
+ 'vertices_number': False, 'edges_number': False})
57
+
58
+
59
+
60
+ class ShapeDescriptors:
61
+ """
62
+ This class takes :
63
+ - a binary image of 0 and 1 drawing one shape
64
+ - a list of descriptors to calculate from that image
65
+ ["area", "perimeter", "circularity", "rectangularity", "total_hole_area", "solidity", "convexity",
66
+ "eccentricity", "euler_number",
67
+
68
+ "standard_deviation_y", "standard_deviation_x", "skewness_y", "skewness_x", "kurtosis_y", "kurtosis_x",
69
+ "major_axis_len", "minor_axis_len", "axes_orientation",
70
+
71
+ "mo", "contours", "min_bounding_rectangle", "convex_hull"]
72
+
73
+ Be careful! mo, contours, min_bounding_rectangle, convex_hull,
74
+ standard_deviations, skewness and kurtosis are not atomics
75
+ https://www.researchgate.net/publication/27343879_Estimators_for_Orientation_and_Anisotropy_in_Digitized_Images
76
+ """
77
+
78
+ def __init__(self, binary_image, wanted_descriptors_list):
79
+ """
80
+ Class to compute various descriptors for a binary image.
81
+
82
+ Parameters
83
+ ----------
84
+ binary_image : ndarray
85
+ Binary image used to compute the descriptors.
86
+ wanted_descriptors_list : list
87
+ List of strings with the names of the wanted descriptors.
88
+
89
+ Attributes
90
+ ----------
91
+ binary_image : ndarray
92
+ The binary image.
93
+ descriptors : dict
94
+ Dictionary containing the computed descriptors.
95
+ mo : float or None, optional
96
+ Moment of inertia (default is `None`).
97
+ area : int or None, optional
98
+ Area of the object (default is `None`).
99
+ contours : ndarray or None, optional
100
+ Contours of the object (default is `None`).
101
+ min_bounding_rectangle : tuple or None, optional
102
+ Minimum bounding rectangle of the object (default is `None`).
103
+ convex_hull : ndarray or None, optional
104
+ Convex hull of the object (default is `None`).
105
+ major_axis_len : float or None, optional
106
+ Major axis length of the object (default is `None`).
107
+ minor_axis_len : float or None, optional
108
+ Minor axis length of the object (default is `None`).
109
+ axes_orientation : float or None, optional
110
+ Orientation of the axes (default is `None`).
111
+ sx : float or None, optional
112
+ Standard deviation in x-axis (default is `None`).
113
+ kx : float or None, optional
114
+ Kurtosis in x-axis (default is `None`).
115
+ skx : float or None, optional
116
+ Skewness in x-axis (default is `None`).
117
+ perimeter : float or None, optional
118
+ Perimeter of the object (default is `None`).
119
+ convexity : float or None, optional
120
+ Convexity of the object (default is `None`).
121
+
122
+ Examples
123
+ --------
124
+ >>> binary_image = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8)
125
+ >>> wanted_descriptors_list = ["area", "perimeter"]
126
+ >>> SD = ShapeDescriptors(binary_image, wanted_descriptors_list)
127
+ >>> SD.descriptors
128
+ {'area': np.uint64(9), 'perimeter': 8.0}
129
+ """
130
+ # Give a None value to each parameters whose presence is assessed before calculation (less calculus for speed)
131
+ self.mo = None
132
+ self.area = None
133
+ self.contours = None
134
+ self.min_bounding_rectangle = None
135
+ self.convex_hull = None
136
+ self.major_axis_len = None
137
+ self.minor_axis_len = None
138
+ self.axes_orientation = None
139
+ self.sx = None
140
+ self.kx = None
141
+ self.skx = None
142
+ self.perimeter = None
143
+ self.convexity = None
144
+
145
+ self.binary_image = binary_image
146
+ if self.binary_image.dtype == 'bool':
147
+ self.binary_image = self.binary_image.astype(np.uint8)
148
+
149
+ self.descriptors = {i: np.empty(0, dtype=np.float64) for i in wanted_descriptors_list}
150
+ self.get_area()
151
+
152
+ for name in self.descriptors.keys():
153
+ if self.area == 0:
154
+ self.descriptors[name] = 0
155
+ else:
156
+ if name == "mo":
157
+ self.get_mo()
158
+ self.descriptors[name] = self.mo
159
+ elif name == "area":
160
+ self.descriptors[name] = self.area
161
+ elif name == "contours":
162
+ self.get_contours()
163
+ self.descriptors[name] = self.contours
164
+ elif name == "min_bounding_rectangle":
165
+ self.get_min_bounding_rectangle()
166
+ self.descriptors[name] = self.min_bounding_rectangle
167
+ elif name == "major_axis_len":
168
+ self.get_major_axis_len()
169
+ self.descriptors[name] = self.major_axis_len
170
+ elif name == "minor_axis_len":
171
+ self.get_minor_axis_len()
172
+ self.descriptors[name] = self.minor_axis_len
173
+ elif name == "axes_orientation":
174
+ self.get_inertia_axes()
175
+ self.descriptors[name] = self.axes_orientation
176
+ elif name == "standard_deviation_y":
177
+ self.get_standard_deviations()
178
+ self.descriptors[name] = self.sy
179
+ elif name == "standard_deviation_x":
180
+ self.get_standard_deviations()
181
+ self.descriptors[name] = self.sx
182
+ elif name == "skewness_y":
183
+ self.get_skewness()
184
+ self.descriptors[name] = self.sky
185
+ elif name == "skewness_x":
186
+ self.get_skewness()
187
+ self.descriptors[name] = self.skx
188
+ elif name == "kurtosis_y":
189
+ self.get_kurtosis()
190
+ self.descriptors[name] = self.ky
191
+ elif name == "kurtosis_x":
192
+ self.get_kurtosis()
193
+ self.descriptors[name] = self.kx
194
+ elif name == "convex_hull":
195
+ self.get_convex_hull()
196
+ self.descriptors[name] = self.convex_hull
197
+ elif name == "perimeter":
198
+ self.get_perimeter()
199
+ self.descriptors[name] = self.perimeter
200
+ elif name == "circularity":
201
+ self.get_circularity()
202
+ self.descriptors[name] = self.circularity
203
+ elif name == "rectangularity":
204
+ self.get_rectangularity()
205
+ self.descriptors[name] = self.rectangularity
206
+ elif name == "total_hole_area":
207
+ self.get_total_hole_area()
208
+ self.descriptors[name] = self.total_hole_area
209
+ elif name == "solidity":
210
+ self.get_solidity()
211
+ self.descriptors[name] = self.solidity
212
+ elif name == "convexity":
213
+ self.get_convexity()
214
+ self.descriptors[name] = self.convexity
215
+ elif name == "eccentricity":
216
+ self.get_eccentricity()
217
+ self.descriptors[name] = self.eccentricity
218
+ elif name == "euler_number":
219
+ self.get_euler_number()
220
+ self.descriptors[name] = self.euler_number
221
+
222
+ """
223
+ The following methods can be called to compute parameters for descriptors requiring it
224
+ """
225
+
226
+ def get_mo(self):
227
+ """
228
+ Get moments of a binary image.
229
+
230
+ Calculate the image moments for a given binary image using OpenCV's
231
+ `cv2.moments` function and then translate these moments into a formatted
232
+ dictionary.
233
+
234
+ Parameters
235
+ ----------
236
+ self : object
237
+ The instance of the class containing this method.
238
+ binary_image : numpy.ndarray
239
+ A binary image (2D array) where pixels are 0 or 255.
240
+
241
+ Other Parameters
242
+ ----------------
243
+ None
244
+
245
+ Returns
246
+ -------
247
+ dict
248
+ A dictionary containing the translated moments of the binary image.
249
+
250
+ Raises
251
+ ------
252
+ TypeError
253
+ If `binary_image` is not a NumPy array.
254
+
255
+ Notes
256
+ -----
257
+ This function assumes the binary image has already been processed and is in a
258
+ suitable format for moment calculation.
259
+
260
+ Examples
261
+ --------
262
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["mo"])
263
+ >>> print(SD.mo["m00"])
264
+ 9.0
265
+ """
266
+ self.mo = translate_dict(cv2.moments(self.binary_image))
267
+
268
+ def get_area(self):
269
+ """
270
+ Calculate the area of a binary image by summing its pixel values.
271
+
272
+ This function computes the area covered by white pixels (value 1) in a binary image,
273
+ which is equivalent to counting the number of 'on' pixels.
274
+
275
+ Parameters
276
+ ----------
277
+ self : object
278
+ The instance of a class containing the binary_image attribute.
279
+
280
+ Returns
281
+ -------
282
+ int
283
+ The total number of white pixels in the binary image, representing its area.
284
+
285
+ Notes
286
+ -----
287
+ This function assumes the binary_image attribute is a NumPy array containing only 0s and 1s.
288
+ If the image contains other values, this function might not produce accurate results.
289
+
290
+ Examples
291
+ --------
292
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["area"])
293
+ >>> print(SD.area)
294
+ 9.0
295
+ """
296
+ self.area = self.binary_image.sum()
297
+
298
+ def get_contours(self):
299
+ """
300
+ Find and process the largest contour in a binary image.
301
+
302
+ Retrieves contours from a binary image, calculates the Euler number,
303
+ and identifies the largest contour based on its length.
304
+
305
+ Parameters
306
+ ----------
307
+ self : ImageProcessingObject
308
+ The image processing object containing the binary image.
309
+
310
+ Other Parameters
311
+ ----------------
312
+ None
313
+
314
+ Returns
315
+ -------
316
+ None
317
+
318
+ Raises
319
+ ------
320
+ None
321
+
322
+ Notes
323
+ -----
324
+ This function modifies the internal state of the `self` object to store
325
+ the largest contour and Euler number.
326
+
327
+ Examples
328
+ --------
329
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["euler_number"])
330
+ >>> print(len(SD.contours))
331
+ 8
332
+ """
333
+ contours, hierarchy = cv2.findContours(self.binary_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
334
+ nb, shapes = cv2.connectedComponents(self.binary_image, ltype=cv2.CV_16U)
335
+ self.euler_number = (nb - 1) - len(contours)
336
+ self.contours = contours[0]
337
+ if len(contours) > 1:
338
+ all_lengths = np.zeros(len(contours))
339
+ for i, contour in enumerate(contours):
340
+ all_lengths[i] = len(contour)
341
+ self.contours = contours[np.argmax(all_lengths)]
342
+
343
+ def get_min_bounding_rectangle(self):
344
+ """
345
+ Retrieve the minimum bounding rectangle from the contours of an image.
346
+
347
+ This method calculates the smallest area rectangle that can enclose
348
+ the object outlines present in the image, which is useful for
349
+ object detection and analysis tasks.
350
+
351
+ Parameters
352
+ ----------
353
+ None
354
+
355
+ Returns
356
+ -------
357
+ tuple
358
+ A tuple containing the following elements:
359
+ - (cx, cy): The center point of the rectangle.
360
+ - (width, height): Width and height of the bounding rectangle.
361
+ - angle: Angle in degrees describing how much the ellipse is rotated.
362
+
363
+ Raises
364
+ ------
365
+ RuntimeError
366
+ If the contours are not available and cannot be retrieved,
367
+ indicating a problem with the image or preprocessing steps.
368
+
369
+ Notes
370
+ -----
371
+ - The bounding rectangle is calculated only if contours are available.
372
+ If not, they will be retrieved first before calculating the rectangle.
373
+
374
+ Examples
375
+ --------
376
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["rectangularity"])
377
+ >>> print(len(SD.min_bounding_rectangle))
378
+ 3
379
+ """
380
+ if self.contours is None:
381
+ self.get_contours()
382
+ self.min_bounding_rectangle = cv2.minAreaRect(self.contours) # ((cx, cy), (width, height), angle)
383
+
384
+ def get_inertia_axes(self):
385
+ """
386
+ Calculate and set the moments of inertia properties of an object.
387
+
388
+ This function computes the centroid, major axis length,
389
+ minor axis length, and axes orientation for an object. It
390
+ first ensures that the moments of inertia (`mo`) attribute is available,
391
+ computing them if necessary, before using the `get_inertia_axes` function.
392
+
393
+ Returns
394
+ -------
395
+ None
396
+
397
+ This method sets the following attributes:
398
+ - `cx` : float
399
+ The x-coordinate of the centroid.
400
+ - `cy` : float
401
+ The y-coordinate of the centroid.
402
+ - `major_axis_len` : float
403
+ The length of the major axis.
404
+ - `minor_axis_len` : float
405
+ The length of the minor axis.
406
+ - `axes_orientation` : float
407
+ The orientation angle of the axes.
408
+
409
+ Raises
410
+ ------
411
+ ValueError
412
+ If there is an issue with the moments of inertia computation.
413
+
414
+ Notes
415
+ -----
416
+ This function modifies in-place the object's attributes related to its geometry.
417
+
418
+ Examples
419
+ --------
420
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["major_axis_len"])
421
+ >>> print(SD.axes_orientation)
422
+ 0.0
423
+ """
424
+ if self.mo is None:
425
+ self.get_mo()
426
+
427
+ self.cx, self.cy, self.major_axis_len, self.minor_axis_len, self.axes_orientation = get_inertia_axes(self.mo)
428
+
429
+ def get_standard_deviations(self):
430
+ """
431
+ Calculate the standard deviations along x and y.
432
+
433
+ Parameters
434
+ ----------
435
+ moment_order : int
436
+ The order of moments to consider in calculations.
437
+ binary_image : numpy.ndarray
438
+ A 2D binary image where the shape corresponds to `[height, width]`.
439
+ center_x : float
440
+ The x-coordinate of the centroid of the object in the image.
441
+ center_y : float
442
+ The y-coordinate of the centroid of the object in the image.
443
+
444
+ Returns
445
+ -------
446
+ tuple[float, float]
447
+ A tuple containing the standard deviations along x and y (sx, sy).
448
+
449
+ Notes
450
+ -----
451
+ The function calculates the standard deviations of a binary image about its centroid.
452
+
453
+ Examples
454
+ --------
455
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["standard_deviation_x", "standard_deviation_y"])
456
+ >>> print(SD.sx, SD.sy)
457
+ 0.816496580927726 0.816496580927726
458
+ """
459
+ if self.sx is None:
460
+ if self.axes_orientation is None:
461
+ self.get_inertia_axes()
462
+ self.sx, self.sy = get_standard_deviations(self.mo, self.binary_image, self.cx, self.cy)
463
+
464
+ def get_skewness(self):
465
+ """
466
+ Calculate the skewness of an image.
467
+
468
+ This function computes the skewness about the x-axis and y-axis of
469
+ an image. Skewness is a measure of the asymmetry of the probability
470
+ distribution of values in an image.
471
+
472
+ Parameters
473
+ ----------
474
+ binary_image : numpy.ndarray
475
+ A binary image represented as a 2D array of integers, where 0 represents
476
+ background and other values represent foreground.
477
+ mo : dict
478
+ Moments of the image.
479
+ cx, cy : float
480
+ The x and y coordinates of the centroid of the object in the image.
481
+ sx, sy : float
482
+ The standard deviations along the x and y axes.
483
+
484
+ Other Parameters
485
+ ----------------
486
+ None
487
+
488
+ Returns
489
+ -------
490
+ skx, sky : tuple of float
491
+ The skewness about the x-axis and y-axis.
492
+
493
+ Notes
494
+ -----
495
+ This method internally calls `get_standard_deviations` if the standard deviation
496
+ values are not already computed.
497
+
498
+ Examples
499
+ --------
500
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["skewness_x", "skewness_y"])
501
+ >>> print(SD.skx, SD.sky)
502
+ 0.0 0.0
503
+ """
504
+ if self.skx is None:
505
+ if self.sx is None:
506
+ self.get_standard_deviations()
507
+
508
+ self.skx, self.sky = get_skewness(self.mo, self.binary_image, self.cx, self.cy, self.sx, self.sy)
509
+
510
+ def get_kurtosis(self):
511
+ """
512
+ Calculates the kurtosis of the image moments.
513
+
514
+ Kurtosis is a statistical measure that describes the shape of
515
+ a distribution's tails in relation to its overall shape. It is
516
+ used here in the context of image moments analysis.
517
+
518
+ Notes
519
+ -----
520
+ This function first checks if the kurtosis values have already been calculated.
521
+ If not, it calculates them using the `get_kurtosis` function.
522
+
523
+ Examples
524
+ --------
525
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["kurtosis_x", "kurtosis_y"])
526
+ >>> print(SD.kx, SD.ky)
527
+ 1.5 1.5
528
+ """
529
+ if self.kx is None:
530
+ if self.sx is None:
531
+ self.get_standard_deviations()
532
+
533
+ self.kx, self.ky = get_kurtosis(self.mo, self.binary_image, self.cx, self.cy, self.sx, self.sy)
534
+
535
+ def get_convex_hull(self):
536
+ """
537
+ Compute the convex hull of an object's contours.
538
+
539
+ This method calculates the convex hull for the object represented by its
540
+ contours. If the contours are not already computed, it will first compute them.
541
+
542
+ Parameters
543
+ ----------
544
+ self : Object
545
+ The object containing the contours and convex hull attributes.
546
+
547
+ Returns
548
+ -------
549
+ None
550
+
551
+ Notes
552
+ -----
553
+ This method modifies the object in place by setting its `convex_hull`
554
+ attribute to the result of the convex hull computation.
555
+
556
+ Examples
557
+ --------
558
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["solidity"])
559
+ >>> print(len(SD.convex_hull))
560
+ 4
561
+ """
562
+ if self.contours is None:
563
+ self.get_contours()
564
+ self.convex_hull = cv2.convexHull(self.contours)
565
+
566
+ def get_perimeter(self):
567
+ """
568
+ Get the perimeter of a contour.
569
+
570
+ Calculates the perimeter length of the contours present in
571
+ the image after determining them if they are not already available.
572
+
573
+ Parameters
574
+ ----------
575
+ None
576
+
577
+ Returns
578
+ -------
579
+ float
580
+ The perimeter length of the contours in the image.
581
+
582
+ Notes
583
+ -----
584
+ This function retrieves the contours if they have not been determined
585
+ yet using `self.get_contours()`. This is crucial for accurate perimeter
586
+ measurement.
587
+
588
+ Examples
589
+ --------
590
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["perimeter"])
591
+ >>> print(SD.perimeter)
592
+ 8.0
593
+ """
594
+ if self.contours is None:
595
+ self.get_contours()
596
+ self.perimeter = cv2.arcLength(self.contours, True)
597
+
598
+ def get_circularity(self):
599
+ """
600
+ Calculate and set the circularity of a binary image object.
601
+
602
+ Circularity is defined as (4 * pi * area) / perimeter^2.
603
+ If the perimeter has not been calculated yet, it will be computed first.
604
+
605
+ Parameters
606
+ ----------
607
+ self : ShapeObject
608
+ The object which contains the binary image and its properties.
609
+
610
+ Attributes
611
+ ----------
612
+ circularity : float
613
+ The calculated circularity value, set as an attribute of the object.
614
+
615
+ Returns
616
+ -------
617
+ None
618
+
619
+ Notes
620
+ -----
621
+ Circularity is a measure of how closely the shape of an object approximates a circle.
622
+ A perfect circle has a circularity of 1.0.
623
+
624
+ Examples
625
+ --------
626
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["circularity"])
627
+ >>> print(SD.circularity)
628
+ 1.7671458676442586
629
+ """
630
+ if self.perimeter is None:
631
+ self.get_perimeter()
632
+ if self.perimeter == 0:
633
+ self.circularity = 0
634
+ else:
635
+ self.circularity = (4 * np.pi * self.binary_image.sum()) / np.square(self.perimeter)
636
+
637
+ def get_rectangularity(self):
638
+ """
639
+ Calculates the rectangularity of a binary image.
640
+
641
+ Rectangularity is defined as the ratio of the number of pixels in
642
+ the shape to the area of its bounding rectangle. This function
643
+ computes this value by considering the binary image stored in
644
+ `self.binary_image`.
645
+
646
+ Parameters
647
+ ----------
648
+ None
649
+
650
+ Other Parameters
651
+ ----------------
652
+ None
653
+
654
+ Returns
655
+ -------
656
+ float
657
+ The rectangularity of the binary image.
658
+
659
+ Raises
660
+ ------
661
+ None
662
+
663
+ Notes
664
+ -----
665
+ This function uses `self.binary_image` to determine the number of pixels in
666
+ the shape and `self.min_bounding_rectangle` to determine the bounding rectangle area.
667
+ If the minimum bounding rectangle has not been computed yet, it will be calculated
668
+ through `self.get_min_bounding_rectangle()`.
669
+
670
+ Attributes
671
+ ----------
672
+ None
673
+
674
+ Examples
675
+ --------
676
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["rectangularity"])
677
+ >>> print(SD.rectangularity)
678
+ 2.25
679
+ """
680
+ if self.min_bounding_rectangle is None:
681
+ self.get_min_bounding_rectangle()
682
+ bounding_rectangle_area = self.min_bounding_rectangle[1][0] * self.min_bounding_rectangle[1][1]
683
+ if bounding_rectangle_area != 0:
684
+ self.rectangularity = self.binary_image.sum() / bounding_rectangle_area
685
+ else:
686
+ self.rectangularity = 1
687
+
688
+ def get_total_hole_area(self):
689
+ """
690
+ Calculate the total area of holes in a binary image.
691
+
692
+ This function uses connected component labeling to detect and
693
+ measure the area of holes in a binary image.
694
+
695
+ Returns
696
+ -------
697
+ float
698
+ The total area of all detected holes in the binary image.
699
+
700
+ Notes
701
+ -----
702
+ This function assumes that the binary image has been pre-processed
703
+ and that holes are represented as connected components of zero
704
+ pixels within the foreground
705
+
706
+ Examples
707
+ --------
708
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["total_hole_area"])
709
+ >>> print(SD.total_hole_area)
710
+ 0
711
+ """
712
+ # FIRST VERSION
713
+ # nb, new_order, stats, centers = cv2.connectedComponentsWithStats(1 - self.binary_image)
714
+ # if stats.shape[0] > 2:
715
+ # self.total_hole_area = stats[2:, 4].sum()
716
+ # else:
717
+ # self.total_hole_area = 0
718
+ # tic = default_timer()
719
+ # SECOND VERSION
720
+ # nb, new_order = cv2.connectedComponents(1 - self.binary_image)
721
+ # if nb <= 1:
722
+ # self.total_hole_area = 0
723
+ # else:
724
+ # label_counts = np.bincount(new_order.flatten())
725
+ # self.total_hole_area = label_counts[2:].sum()
726
+ # tac = default_timer()
727
+ # print( tac-tic)
728
+ # THIDS VERSION
729
+ nb, new_order = cv2.connectedComponents(1 - self.binary_image)
730
+ if nb > 2:
731
+ self.total_hole_area = (new_order > 2).sum()
732
+ else:
733
+ self.total_hole_area = 0
734
+
735
+ def get_solidity(self):
736
+ """
737
+ Calculate the solidity of a contour, which is the ratio of the area
738
+ of the contour to the area of its convex hull.
739
+
740
+ Extended Summary
741
+ ----------------
742
+ The solidity is a dimensionless measure that compares the area of a shape to
743
+ its convex hull. A solidity of 1 means the contour is fully convex, while a
744
+ value less than 1 indicates concavities.
745
+
746
+ Parameters
747
+ ----------
748
+ None
749
+
750
+ Other Parameters
751
+ -----------------
752
+ None
753
+
754
+ Returns
755
+ -------
756
+ float
757
+ The solidity of the contour.
758
+
759
+ Raises
760
+ ------
761
+ None
762
+
763
+ Notes
764
+ -----
765
+ The solidity is computed by dividing the area of the contour by the area of its
766
+ convex hull. If the convex hull is empty, solidity defaults to 1.
767
+
768
+ Attributes
769
+ ----------
770
+ self.convex_hull : array-like or None
771
+ The convex hull of the contour.
772
+ self.solidity : float
773
+ The calculated solidity value of the contour.
774
+
775
+ Examples
776
+ --------
777
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["solidity"])
778
+ >>> print(SD.solidity)
779
+ 1.0
780
+ """
781
+ if self.convex_hull is None:
782
+ self.get_convex_hull()
783
+ conv_h_cont_area = cv2.contourArea(self.convex_hull)
784
+ if conv_h_cont_area != 0:
785
+ self.solidity = cv2.contourArea(self.contours) / cv2.contourArea(self.convex_hull)
786
+ else:
787
+ self.solidity = 1
788
+
789
+ def get_convexity(self):
790
+ """
791
+ Calculate the convexity of a shape.
792
+
793
+ Convexity is defined as the ratio of the length of
794
+ the contour to the perimeter of the convex hull.
795
+
796
+ Parameters
797
+ ----------
798
+ None
799
+
800
+ Returns
801
+ -------
802
+ float
803
+ The convexity ratio of the shape.
804
+
805
+ Raises
806
+ ------
807
+ None
808
+
809
+ Notes
810
+ -----
811
+ This method requires that both `perimeter` and `convex_hull`
812
+ attributes are computed before calling this method.
813
+ Convexity is a dimensionless quantity and should always be in the range [0, 1].
814
+
815
+ Examples
816
+ --------
817
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["convexity"])
818
+ >>> print(SD.convexity)
819
+ 1.0
820
+ """
821
+ if self.perimeter is None:
822
+ self.get_perimeter()
823
+ if self.convex_hull is None:
824
+ self.get_convex_hull()
825
+ self.convexity = cv2.arcLength(self.convex_hull, True) / self.perimeter
826
+
827
+ def get_eccentricity(self):
828
+ """
829
+ Calculate the eccentricity of an ellipsoid based on its major and minor axis lengths.
830
+
831
+ This function computes the eccentricity of an ellipsoid using its major
832
+ and minor axis lengths. The eccentricity is a measure of how much the
833
+ ellipsoid deviates from being circular.
834
+
835
+ Parameters
836
+ ----------
837
+ self : Ellipsoid
838
+ The ellipsoid object containing the major and minor axis lengths.
839
+ These values are assumed to be already set.
840
+
841
+ Other Parameters
842
+ ----------------
843
+ None
844
+
845
+ Returns
846
+ -------
847
+ float
848
+ The calculated eccentricity of the ellipsoid.
849
+
850
+ Raises
851
+ ------
852
+ None
853
+
854
+ Notes
855
+ -----
856
+ - This function assumes that the major and minor axis lengths are already set.
857
+ If not, you must call `self.get_inertia_axes()` to update these values before
858
+ calling this function.
859
+
860
+ Examples
861
+ --------
862
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["eccentricity"])
863
+ >>> print(SD.eccentricity)
864
+ 0.0
865
+ """
866
+ self.get_inertia_axes()
867
+ self.eccentricity = np.sqrt(1 - np.square(self.minor_axis_len / self.major_axis_len))
868
+
869
+ def get_euler_number(self):
870
+ """
871
+ Get Euler number from the contour data.
872
+
873
+ Calculate and return the Euler characteristic of the current contour or
874
+ contours, which is a topological invariant that describes the shape.
875
+
876
+ Parameters
877
+ ----------
878
+ None
879
+
880
+ Returns
881
+ -------
882
+ int or None
883
+ The Euler number of the contour(s). Returns `None` if no contours exist.
884
+
885
+ Raises
886
+ ------
887
+ ValueError
888
+ If the Euler number cannot be calculated due to invalid contour data.
889
+
890
+ Notes
891
+ -----
892
+ The Euler characteristic is computed as: ``vertices - edges + faces``.
893
+ This method handles both single contour and multiple contours cases.
894
+
895
+ Examples
896
+ --------
897
+ >>> contour_object.get_euler_number()
898
+ 1
899
+
900
+ >>> no_contours = Contour() # Object with no contours
901
+ >>> no_contours.get_euler_number()
902
+ None
903
+ """
904
+ if self.contours is None:
905
+ self.get_contours()
906
+
907
+ def get_major_axis_len(self):
908
+ """
909
+ Get the length of the major axis.
910
+
911
+ Calculate or retrieve the length of the major axis, ensuring it is
912
+ computed if not already available.
913
+
914
+ Parameters
915
+ ----------
916
+ None
917
+
918
+ Returns
919
+ -------
920
+ float or None
921
+ The length of the major axis. If the major_axis_len could not be
922
+ computed, returns `None`.
923
+
924
+ Raises
925
+ ------
926
+ AttributeError
927
+ If the major axis length is not available and cannot be computed.
928
+
929
+ Notes
930
+ -----
931
+ - This method may trigger computation of inertia axes if they haven't been
932
+ precomputed to determine the major axis length.
933
+
934
+ Examples
935
+ --------
936
+ >>> SD = ShapeDescriptors(np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8), ["major_axis_len"])
937
+ >>> print(SD.major_axis_len)
938
+ 2.8284271247461907
939
+ """
940
+ if self.major_axis_len is None:
941
+ self.get_inertia_axes()
942
+
943
+ def get_minor_axis_len(self):
944
+ """
945
+ Get the length of the minor axis.
946
+
947
+ This method returns the calculated length of the minor axis. If
948
+ `self.minor_axis_len` is `None`, it will first compute the inertia axes.
949
+
950
+ Parameters
951
+ ----------
952
+ none
953
+
954
+ Returns
955
+ -------
956
+ float:
957
+ The length of the minor axis.
958
+
959
+ Raises
960
+ ------
961
+ RuntimeError:
962
+ If the minor axis cannot be calculated. This might happen if there are not enough data points
963
+ to determine the inertia axes.
964
+
965
+ Notes
966
+ -----
967
+ This method will compute the inertia axes if `self.minor_axis_len` is not cached.
968
+
969
+ Examples
970
+ --------
971
+ >>> SD = ShapeDescriptors(np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8), ["minor_axis_len"])
972
+ >>> print(SD.minor_axis_len)
973
+ 0.0
974
+ """
975
+ if self.minor_axis_len is None:
976
+ self.get_inertia_axes()
977
+
978
+ def get_axes_orientation(self):
979
+ """
980
+
981
+ Get the orientation of the axes.
982
+
983
+ Extended summary
984
+ ----------------
985
+
986
+ This method retrieves the current orientation of the axes. If the orientation is not already computed, it will compute and store it by calling `get_inertia_axes()`.
987
+
988
+ Parameters
989
+ ----------
990
+ None
991
+
992
+ Returns
993
+ -------
994
+ `np.ndarray`
995
+ A 3x3 matrix representing the orientation of the axes.
996
+
997
+ Raises
998
+ ------
999
+ None
1000
+
1001
+ Notes
1002
+ -----
1003
+ This method may trigger a computation of inertia axes if they haven't been computed yet.
1004
+
1005
+ Examples
1006
+ --------
1007
+
1008
+ Examples
1009
+ --------
1010
+ >>> SD = ShapeDescriptors(np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8), ["axes_orientation"])
1011
+ >>> print(SD.axes_orientation)
1012
+ 1.5707963267948966
1013
+ """
1014
+ if self.axes_orientation is None:
1015
+ self.get_inertia_axes()
1016
+