cellects 0.1.3__py3-none-any.whl → 0.2.7__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 (38) hide show
  1. cellects/__main__.py +65 -25
  2. cellects/config/all_vars_dict.py +18 -17
  3. cellects/core/cellects_threads.py +1034 -396
  4. cellects/core/motion_analysis.py +1664 -2010
  5. cellects/core/one_image_analysis.py +1082 -1061
  6. cellects/core/program_organizer.py +1687 -1316
  7. cellects/core/script_based_run.py +80 -76
  8. cellects/gui/advanced_parameters.py +365 -326
  9. cellects/gui/cellects.py +102 -91
  10. cellects/gui/custom_widgets.py +4 -3
  11. cellects/gui/first_window.py +226 -104
  12. cellects/gui/if_several_folders_window.py +117 -68
  13. cellects/gui/image_analysis_window.py +841 -450
  14. cellects/gui/required_output.py +100 -56
  15. cellects/gui/ui_strings.py +840 -0
  16. cellects/gui/video_analysis_window.py +317 -135
  17. cellects/image_analysis/cell_leaving_detection.py +64 -4
  18. cellects/image_analysis/image_segmentation.py +451 -22
  19. cellects/image_analysis/morphological_operations.py +2166 -1635
  20. cellects/image_analysis/network_functions.py +616 -253
  21. cellects/image_analysis/one_image_analysis_threads.py +94 -153
  22. cellects/image_analysis/oscillations_functions.py +131 -0
  23. cellects/image_analysis/progressively_add_distant_shapes.py +2 -3
  24. cellects/image_analysis/shape_descriptors.py +517 -466
  25. cellects/utils/formulas.py +169 -6
  26. cellects/utils/load_display_save.py +362 -105
  27. cellects/utils/utilitarian.py +86 -9
  28. cellects-0.2.7.dist-info/LICENSE +675 -0
  29. cellects-0.2.7.dist-info/METADATA +829 -0
  30. cellects-0.2.7.dist-info/RECORD +44 -0
  31. cellects/core/one_video_per_blob.py +0 -540
  32. cellects/image_analysis/cluster_flux_study.py +0 -102
  33. cellects-0.1.3.dist-info/LICENSE.odt +0 -0
  34. cellects-0.1.3.dist-info/METADATA +0 -176
  35. cellects-0.1.3.dist-info/RECORD +0 -44
  36. {cellects-0.1.3.dist-info → cellects-0.2.7.dist-info}/WHEEL +0 -0
  37. {cellects-0.1.3.dist-info → cellects-0.2.7.dist-info}/entry_points.txt +0 -0
  38. {cellects-0.1.3.dist-info → cellects-0.2.7.dist-info}/top_level.txt +0 -0
@@ -25,24 +25,28 @@ If you want to allow the software to compute another variable:
25
25
  """
26
26
  import cv2
27
27
  import numpy as np
28
+ from typing import Tuple
29
+ from numpy.typing import NDArray
28
30
  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
+ import pandas as pd
32
+ from cellects.utils.utilitarian import translate_dict, smallest_memory_array
33
+ from cellects.utils.formulas import (get_inertia_axes, get_standard_deviations, get_skewness, get_kurtosis,
34
+ get_newly_explored_area)
31
35
 
32
36
  descriptors_categories = {'area': True, 'perimeter': False, 'circularity': False, 'rectangularity': False,
33
37
  'total_hole_area': False, 'solidity': False, 'convexity': False, 'eccentricity': False,
34
38
  'euler_number': False, 'standard_deviation_xy': False, 'skewness_xy': False,
35
39
  'kurtosis_xy': False, 'major_axes_len_and_angle': True, 'iso_digi_analysis': False,
36
- 'oscilacyto_analysis': False, 'network_analysis': False, 'graph_extraction': False,
40
+ 'oscilacyto_analysis': False,
37
41
  'fractal_analysis': False
38
42
  }
39
43
 
40
44
  descriptors_names_to_display = ['Area', 'Perimeter', 'Circularity', 'Rectangularity', 'Total hole area',
41
45
  'Solidity', 'Convexity', 'Eccentricity', 'Euler number', 'Standard deviation xy',
42
46
  'Skewness xy', 'Kurtosis xy', 'Major axes lengths and angle',
43
- 'Growth transitions', 'Oscillations', 'Network', 'Graph',
44
- 'Fractals'
45
- ]#, 'Oscillating cluster nb and size'
47
+ 'Growth transitions', 'Oscillations',
48
+ 'Minkowski dimension'
49
+ ]
46
50
 
47
51
  from_shape_descriptors_class = {'area': True, 'perimeter': False, 'circularity': False, 'rectangularity': False,
48
52
  'total_hole_area': False, 'solidity': False, 'convexity': False, 'eccentricity': False,
@@ -51,10 +55,298 @@ from_shape_descriptors_class = {'area': True, 'perimeter': False, 'circularity':
51
55
  'major_axis_len': True, 'minor_axis_len': True, 'axes_orientation': True
52
56
  }
53
57
 
58
+ length_descriptors = ['perimeter', 'major_axis_len', 'minor_axis_len']
59
+ area_descriptors = ['area', 'area_total', 'total_hole_area', 'newly_explored_area', 'final_area']
60
+
54
61
  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})
62
+ descriptors.update({'minkowski_dimension': False})
63
+
64
+ def compute_one_descriptor_per_frame(binary_vid: NDArray[np.uint8], arena_label: int, timings: NDArray,
65
+ descriptors_dict: dict, output_in_mm: bool, pixel_size: float,
66
+ do_fading: bool, save_coord_specimen:bool):
67
+ """
68
+ Computes descriptors for each frame in a binary video and returns them as a DataFrame.
69
+
70
+ Parameters
71
+ ----------
72
+ binary_vid : NDArray[np.uint8]
73
+ The binary video data where each frame is a 2D array.
74
+ arena_label : int
75
+ Label for the arena in the video.
76
+ timings : NDArray
77
+ Array of timestamps corresponding to each frame.
78
+ descriptors_dict : dict
79
+ Dictionary containing the descriptors to be computed.
80
+ output_in_mm : bool, optional
81
+ Flag indicating if output should be in millimeters. Default is False.
82
+ pixel_size : float, optional
83
+ Size of a pixel in the video when `output_in_mm` is True. Default is None.
84
+ do_fading : bool, optional
85
+ Flag indicating if the fading effect should be applied. Default is False.
86
+ save_coord_specimen : bool, optional
87
+ Flag indicating if the coordinates of specimens should be saved. Default is False.
88
+
89
+ Returns
90
+ -------
91
+ pandas.DataFrame
92
+ DataFrame containing the descriptors for each frame in the video.
93
+
94
+ Notes
95
+ -----
96
+ For large inputs, consider pre-allocating memory for efficiency.
97
+ The `save_coord_specimen` flag will save coordinate data to a file.
98
+
99
+ Examples
100
+ --------
101
+ >>> binary_vid = np.ones((10, 640, 480), dtype=np.uint8)
102
+ >>> timings = np.arange(10)
103
+ >>> descriptors_dict = {'area': True, 'perimeter': True}
104
+ >>> result = compute_one_descriptor_per_frame(binary_vid, 1, timings, descriptors_dict)
105
+ >>> print(result.head())
106
+ arena time area perimeter
107
+ 0 1 0 0 0
108
+ 1 1 1 0 0
109
+ 2 1 2 0 0
110
+ 3 1 3 0 0
111
+ 4 1 4 0 0
112
+
113
+ >>> binary_vid = np.ones((5, 640, 480), dtype=np.uint8)
114
+ >>> timings = np.arange(5)
115
+ >>> descriptors_dict = {'area': True, 'perimeter': True}
116
+ >>> result = compute_one_descriptor_per_frame(binary_vid, 2, timings,
117
+ ... descriptors_dict,
118
+ ... output_in_mm=True,
119
+ ... pixel_size=0.1)
120
+ >>> print(result.head())
121
+ arena time area perimeter
122
+ 0 2 0 0 0.0
123
+ 1 2 1 0 0.0
124
+ 2 2 2 0 0.0
125
+ 3 2 3 0 0.0
126
+ 4 2 4 0 0.0
127
+ """
128
+ dims = binary_vid.shape
129
+ all_descriptors, to_compute_from_sd, length_measures, area_measures = initialize_descriptor_computation(descriptors_dict)
130
+ one_row_per_frame = pd.DataFrame(np.zeros((dims[0], 2 + len(all_descriptors))),
131
+ columns=['arena', 'time'] + all_descriptors)
132
+ one_row_per_frame['arena'] = [arena_label] * dims[0]
133
+ one_row_per_frame['time'] = timings
134
+ for t in np.arange(dims[0]):
135
+ SD = ShapeDescriptors(binary_vid[t, :, :], to_compute_from_sd)
136
+ for descriptor in to_compute_from_sd:
137
+ one_row_per_frame.loc[t, descriptor] = SD.descriptors[descriptor]
138
+ if save_coord_specimen:
139
+ np.save(f"coord_specimen{arena_label}_t{dims[0]}_y{dims[1]}_x{dims[2]}.npy",
140
+ smallest_memory_array(np.nonzero(binary_vid), "uint"))
141
+ # Adjust descriptors scale if output_in_mm is specified
142
+ if do_fading:
143
+ one_row_per_frame['newly_explored_area'] = get_newly_explored_area(binary_vid)
144
+ if output_in_mm:
145
+ one_row_per_frame = scale_descriptors(one_row_per_frame, pixel_size,
146
+ length_measures, area_measures)
147
+ return one_row_per_frame
148
+
149
+
150
+ def compute_one_descriptor_per_colony(binary_vid: NDArray[np.uint8], arena_label: int, timings: NDArray,
151
+ descriptors_dict: dict, output_in_mm: bool, pixel_size: float,
152
+ do_fading: bool, min_colony_size: int, save_coord_specimen: bool):
153
+ dims = binary_vid.shape
154
+ all_descriptors, to_compute_from_sd, length_measures, area_measures = initialize_descriptor_computation(
155
+ descriptors_dict)
156
+ # Objective: create a matrix with 4 columns (time, y, x, colony) containing the coordinates of all colonies
157
+ # against time
158
+ max_colonies = 0
159
+ for t in np.arange(dims[0]):
160
+ nb, shapes = cv2.connectedComponents(binary_vid[t, :, :])
161
+ max_colonies = np.max((max_colonies, nb))
162
+
163
+ time_descriptor_colony = np.zeros((dims[0], len(to_compute_from_sd) * max_colonies * dims[0]),
164
+ dtype=np.float32) # Adjust max_colonies
165
+ colony_number = 0
166
+ colony_id_matrix = np.zeros(dims[1:], dtype=np.uint64)
167
+ coord_colonies = []
168
+ centroids = []
169
+
170
+ # pat_tracker = PercentAndTimeTracker(dims[0], compute_with_elements_number=True)
171
+ for t in np.arange(dims[0]):
172
+ # We rank colonies in increasing order to make sure that the larger colony issued from a colony division
173
+ # keeps the previous colony name.
174
+ # shapes, stats, centers = cc(binary_vid[t, :, :])
175
+ nb, shapes, stats, centers = cv2.connectedComponentsWithStats(binary_vid[t, :, :])
176
+ true_colonies = np.nonzero(stats[:, 4] >= min_colony_size)[1:]
177
+ # Consider that shapes bellow 3 pixels are noise. The loop will stop at nb and not compute them
178
+
179
+ # current_percentage, eta = pat_tracker.get_progress(t, element_number=nb)
180
+ # logging.info(f"Arena n°{arena_label}, Colony descriptors computation: {current_percentage}%{eta}")
181
+
182
+ updated_colony_names = np.zeros(1, dtype=np.uint32)
183
+ for colon_i in true_colonies: # 120)):# #92
184
+ current_colony_img = shapes == colon_i
185
+ if current_colony_img.sum() >= 4:
186
+ current_colony_img = current_colony_img.astype(np.uint8)
187
+
188
+ # I/ Find out which names the current colony had at t-1
189
+ colony_previous_names = np.unique(current_colony_img * colony_id_matrix)
190
+ colony_previous_names = colony_previous_names[colony_previous_names != 0]
191
+ # II/ Find out if the current colony name had already been analyzed at t
192
+ # If there no match with the saved colony_id_matrix, assign colony ID
193
+ if t == 0 or len(colony_previous_names) == 0:
194
+ # logging.info("New colony")
195
+ colony_number += 1
196
+ colony_names = [colony_number]
197
+ # If there is at least 1 match with the saved colony_id_matrix, we keep the colony_previous_name(s)
198
+ else:
199
+ colony_names = colony_previous_names.tolist()
200
+ # Handle colony division if necessary
201
+ if np.any(np.isin(updated_colony_names, colony_names)):
202
+ colony_number += 1
203
+ colony_names = [colony_number]
204
+
205
+ # Update colony ID matrix for the current frame
206
+ coords = np.nonzero(current_colony_img)
207
+ colony_id_matrix[coords[0], coords[1]] = colony_names[0]
208
+
209
+ # Add coordinates to coord_colonies
210
+ time_column = np.full(coords[0].shape, t, dtype=np.uint32)
211
+ colony_column = np.full(coords[0].shape, colony_names[0], dtype=np.uint32)
212
+ coord_colonies.append(np.column_stack((time_column, colony_column, coords[0], coords[1])))
213
+
214
+ # Calculate centroid and add to centroids list
215
+ centroid_x, centroid_y = centers[colon_i, :]
216
+ centroids.append((t, colony_names[0], centroid_y, centroid_x))
217
+
218
+ # Compute shape descriptors
219
+ SD = ShapeDescriptors(current_colony_img, to_compute_from_sd)
220
+ # descriptors = list(SD.descriptors.values())
221
+ descriptors = SD.descriptors
222
+ # Adjust descriptors if output_in_mm is specified
223
+ if output_in_mm:
224
+ descriptors = scale_descriptors(descriptors, pixel_size, length_measures, area_measures)
225
+ # Store descriptors in time_descriptor_colony
226
+ descriptor_index = (colony_names[0] - 1) * len(to_compute_from_sd)
227
+ time_descriptor_colony[t, descriptor_index:(descriptor_index + len(descriptors))] = list(
228
+ descriptors.values())
229
+
230
+ updated_colony_names = np.append(updated_colony_names, colony_names)
231
+
232
+ # Reset colony_id_matrix for the next frame
233
+ colony_id_matrix *= binary_vid[t, :, :]
234
+ if len(centroids) > 0:
235
+ centroids = np.array(centroids, dtype=np.float32)
236
+ else:
237
+ centroids = np.zeros((0, 4), dtype=np.float32)
238
+ time_descriptor_colony = time_descriptor_colony[:, :(colony_number * len(to_compute_from_sd))]
239
+ if len(coord_colonies) > 0:
240
+ coord_colonies = np.vstack(coord_colonies)
241
+ if save_coord_specimen:
242
+ coord_colonies = pd.DataFrame(coord_colonies, columns=["time", "colony", "y", "x"])
243
+ coord_colonies.to_csv(
244
+ f"coord_colonies{arena_label}_t{dims[0]}_col{colony_number}_y{dims[1]}_x{dims[2]}.csv",
245
+ sep=';', index=False, lineterminator='\n')
246
+
247
+ centroids = pd.DataFrame(centroids, columns=["time", "colony", "y", "x"])
248
+ centroids.to_csv(
249
+ f"colony_centroids{arena_label}_t{dims[0]}_col{colony_number}_y{dims[1]}_x{dims[2]}.csv",
250
+ sep=';', index=False, lineterminator='\n')
251
+
252
+ # Format the final dataframe to have one row per time frame, and one column per descriptor_colony_name
253
+ one_row_per_frame = pd.DataFrame({'arena': arena_label, 'time': timings,
254
+ 'area_total': binary_vid.sum((1, 2)).astype(np.float64)})
255
+
256
+ if do_fading:
257
+ one_row_per_frame['newly_explored_area'] = get_newly_explored_area(binary_vid)
258
+ if output_in_mm:
259
+ one_row_per_frame = scale_descriptors(one_row_per_frame, pixel_size)
260
+
261
+ column_names = np.char.add(np.repeat(to_compute_from_sd, colony_number),
262
+ np.tile((np.arange(colony_number) + 1).astype(str), len(to_compute_from_sd)))
263
+ time_descriptor_colony = pd.DataFrame(time_descriptor_colony, columns=column_names)
264
+ one_row_per_frame = pd.concat([one_row_per_frame, time_descriptor_colony], axis=1)
265
+
266
+ return one_row_per_frame
267
+
268
+ def initialize_descriptor_computation(descriptors_dict: dict) -> Tuple[list, list, list, list]:
269
+ """
270
+
271
+ Initialize descriptor computation based on available and requested descriptors.
272
+
273
+ Parameters
274
+ ----------
275
+ descriptors_dict : dict
276
+ A dictionary where keys are descriptor names and values are booleans indicating whether
277
+ to compute the corresponding descriptor.
278
+
279
+ Returns
280
+ -------
281
+ tuple
282
+ A tuple containing four lists:
283
+ - all_descriptors: List of all requested descriptor names.
284
+ - to_compute_from_sd: Array of descriptor names that need to be computed from the shape descriptors class.
285
+ - length_measures: Array of descriptor names that are length measures and need to be computed.
286
+ - area_measures: Array of descriptor names that are area measures and need to be computed.
287
+
288
+ Examples
289
+ --------
290
+ >>> descriptors_dict = {'perimeter': True, 'area': False}
291
+ >>> all_descriptors, to_compute_from_sd, length_measures, area_measures = initialize_descriptor_computation(descriptors_dict)
292
+ >>> print(all_descriptors, to_compute_from_sd, length_measures, area_measures)
293
+ ['length'] ['length'] ['length'] []
57
294
 
295
+ """
296
+ available_descriptors_in_sd = list(from_shape_descriptors_class.keys())
297
+ all_descriptors = []
298
+ to_compute_from_sd = []
299
+ for name, do_compute in descriptors_dict.items():
300
+ if do_compute:
301
+ all_descriptors.append(name)
302
+ if np.isin(name, available_descriptors_in_sd):
303
+ to_compute_from_sd.append(name)
304
+ to_compute_from_sd = np.array(to_compute_from_sd)
305
+ length_measures = to_compute_from_sd[np.isin(to_compute_from_sd, length_descriptors)]
306
+ area_measures = to_compute_from_sd[np.isin(to_compute_from_sd, area_descriptors)]
307
+
308
+ return all_descriptors, to_compute_from_sd, length_measures, area_measures
309
+
310
+ def scale_descriptors(descriptors_dict, pixel_size: float, length_measures: NDArray[str]=None, area_measures: NDArray[str]=None):
311
+ """
312
+ Scale the spatial descriptors in a dictionary based on pixel size.
313
+
314
+ Parameters
315
+ ----------
316
+ descriptors_dict : dict
317
+ Dictionary containing spatial descriptors.
318
+ pixel_size : float
319
+ Pixel size used for scaling.
320
+ length_measures : numpy.ndarray, optional
321
+ Array of descriptors that represent lengths. If not provided,
322
+ they will be initialized.
323
+ area_measures : numpy.ndarray, optional
324
+ Array of descriptors that represent areas. If not provided,
325
+ they will be initialized.
326
+
327
+ Returns
328
+ -------
329
+ dict
330
+ Dictionary with scaled spatial descriptors.
331
+
332
+ Examples
333
+ --------
334
+ >>> from numpy import array as ndarray
335
+ >>> descriptors_dict = {'length': ndarray([1, 2]), 'area': ndarray([3, 4])}
336
+ >>> pixel_size = 0.5
337
+ >>> scaled_dict = scale_descriptors(descriptors_dict, pixel_size)
338
+ >>> print(scaled_dict)
339
+ {'length': array([0.5, 1.]), 'area': array([1.58421369, 2.])}
340
+ """
341
+ if length_measures is None or area_measures is None:
342
+ to_compute_from_sd = np.array(list(descriptors_dict.keys()))
343
+ length_measures = to_compute_from_sd[np.isin(to_compute_from_sd, length_descriptors)]
344
+ area_measures = to_compute_from_sd[np.isin(to_compute_from_sd, area_descriptors)]
345
+ for descr in length_measures:
346
+ descriptors_dict[descr] *= pixel_size
347
+ for descr in area_measures:
348
+ descriptors_dict[descr] *= np.sqrt(pixel_size)
349
+ return descriptors_dict
58
350
 
59
351
 
60
352
  class ShapeDescriptors:
@@ -150,74 +442,71 @@ class ShapeDescriptors:
150
442
  self.get_area()
151
443
 
152
444
  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
445
+ if name == "mo":
446
+ self.get_mo()
447
+ self.descriptors[name] = self.mo
448
+ elif name == "area":
449
+ self.descriptors[name] = self.area
450
+ elif name == "contours":
451
+ self.get_contours()
452
+ self.descriptors[name] = self.contours
453
+ elif name == "min_bounding_rectangle":
454
+ self.get_min_bounding_rectangle()
455
+ self.descriptors[name] = self.min_bounding_rectangle
456
+ elif name == "major_axis_len":
457
+ self.get_major_axis_len()
458
+ self.descriptors[name] = self.major_axis_len
459
+ elif name == "minor_axis_len":
460
+ self.get_minor_axis_len()
461
+ self.descriptors[name] = self.minor_axis_len
462
+ elif name == "axes_orientation":
463
+ self.get_inertia_axes()
464
+ self.descriptors[name] = self.axes_orientation
465
+ elif name == "standard_deviation_y":
466
+ self.get_standard_deviations()
467
+ self.descriptors[name] = self.sy
468
+ elif name == "standard_deviation_x":
469
+ self.get_standard_deviations()
470
+ self.descriptors[name] = self.sx
471
+ elif name == "skewness_y":
472
+ self.get_skewness()
473
+ self.descriptors[name] = self.sky
474
+ elif name == "skewness_x":
475
+ self.get_skewness()
476
+ self.descriptors[name] = self.skx
477
+ elif name == "kurtosis_y":
478
+ self.get_kurtosis()
479
+ self.descriptors[name] = self.ky
480
+ elif name == "kurtosis_x":
481
+ self.get_kurtosis()
482
+ self.descriptors[name] = self.kx
483
+ elif name == "convex_hull":
484
+ self.get_convex_hull()
485
+ self.descriptors[name] = self.convex_hull
486
+ elif name == "perimeter":
487
+ self.get_perimeter()
488
+ self.descriptors[name] = self.perimeter
489
+ elif name == "circularity":
490
+ self.get_circularity()
491
+ self.descriptors[name] = self.circularity
492
+ elif name == "rectangularity":
493
+ self.get_rectangularity()
494
+ self.descriptors[name] = self.rectangularity
495
+ elif name == "total_hole_area":
496
+ self.get_total_hole_area()
497
+ self.descriptors[name] = self.total_hole_area
498
+ elif name == "solidity":
499
+ self.get_solidity()
500
+ self.descriptors[name] = self.solidity
501
+ elif name == "convexity":
502
+ self.get_convexity()
503
+ self.descriptors[name] = self.convexity
504
+ elif name == "eccentricity":
505
+ self.get_eccentricity()
506
+ self.descriptors[name] = self.eccentricity
507
+ elif name == "euler_number":
508
+ self.get_euler_number()
509
+ self.descriptors[name] = self.euler_number
221
510
 
222
511
  """
223
512
  The following methods can be called to compute parameters for descriptors requiring it
@@ -231,35 +520,18 @@ class ShapeDescriptors:
231
520
  `cv2.moments` function and then translate these moments into a formatted
232
521
  dictionary.
233
522
 
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
523
  Notes
256
524
  -----
257
525
  This function assumes the binary image has already been processed and is in a
258
526
  suitable format for moment calculation.
259
527
 
528
+ Returns
529
+ -------
530
+ None
531
+
260
532
  Examples
261
533
  --------
262
- >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["mo"])
534
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["mo"])
263
535
  >>> print(SD.mo["m00"])
264
536
  9.0
265
537
  """
@@ -272,20 +544,13 @@ class ShapeDescriptors:
272
544
  This function computes the area covered by white pixels (value 1) in a binary image,
273
545
  which is equivalent to counting the number of 'on' pixels.
274
546
 
275
- Parameters
276
- ----------
277
- self : object
278
- The instance of a class containing the binary_image attribute.
547
+ Notes
548
+ -----
549
+ Sums values in `self.binary_image` and stores the result in `self.area`.
279
550
 
280
551
  Returns
281
552
  -------
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.
553
+ None
289
554
 
290
555
  Examples
291
556
  --------
@@ -302,43 +567,34 @@ class ShapeDescriptors:
302
567
  Retrieves contours from a binary image, calculates the Euler number,
303
568
  and identifies the largest contour based on its length.
304
569
 
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
570
  Notes
323
571
  -----
324
572
  This function modifies the internal state of the `self` object to store
325
573
  the largest contour and Euler number.
326
574
 
575
+ Returns
576
+ -------
577
+ None
578
+
327
579
  Examples
328
580
  --------
329
581
  >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["euler_number"])
330
582
  >>> print(len(SD.contours))
331
583
  8
332
584
  """
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)]
585
+ if self.area == 0:
586
+ self.euler_number = 0.
587
+ self.contours = np.array([], np.uint8)
588
+ else:
589
+ contours, hierarchy = cv2.findContours(self.binary_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
590
+ nb, shapes = cv2.connectedComponents(self.binary_image, ltype=cv2.CV_16U)
591
+ self.euler_number = (nb - 1) - len(contours)
592
+ self.contours = contours[0]
593
+ if len(contours) > 1:
594
+ all_lengths = np.zeros(len(contours))
595
+ for i, contour in enumerate(contours):
596
+ all_lengths[i] = len(contour)
597
+ self.contours = contours[np.argmax(all_lengths)]
342
598
 
343
599
  def get_min_bounding_rectangle(self):
344
600
  """
@@ -348,17 +604,10 @@ class ShapeDescriptors:
348
604
  the object outlines present in the image, which is useful for
349
605
  object detection and analysis tasks.
350
606
 
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.
607
+ Notes
608
+ -----
609
+ - The bounding rectangle is calculated only if contours are available.
610
+ If not, they will be retrieved first before calculating the rectangle.
362
611
 
363
612
  Raises
364
613
  ------
@@ -366,10 +615,9 @@ class ShapeDescriptors:
366
615
  If the contours are not available and cannot be retrieved,
367
616
  indicating a problem with the image or preprocessing steps.
368
617
 
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.
618
+ Returns
619
+ -------
620
+ None
373
621
 
374
622
  Examples
375
623
  --------
@@ -377,9 +625,15 @@ class ShapeDescriptors:
377
625
  >>> print(len(SD.min_bounding_rectangle))
378
626
  3
379
627
  """
380
- if self.contours is None:
381
- self.get_contours()
382
- self.min_bounding_rectangle = cv2.minAreaRect(self.contours) # ((cx, cy), (width, height), angle)
628
+ if self.area == 0:
629
+ self.min_bounding_rectangle = np.array([], np.uint8)
630
+ else:
631
+ if self.contours is None:
632
+ self.get_contours()
633
+ if len(self.contours) == 0:
634
+ self.min_bounding_rectangle = np.array([], np.uint8)
635
+ else:
636
+ self.min_bounding_rectangle = cv2.minAreaRect(self.contours) # ((cx, cy), (width, height), angle)
383
637
 
384
638
  def get_inertia_axes(self):
385
639
  """
@@ -423,36 +677,26 @@ class ShapeDescriptors:
423
677
  """
424
678
  if self.mo is None:
425
679
  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)
680
+ if self.area == 0:
681
+ self.cx, self.cy, self.major_axis_len, self.minor_axis_len, self.axes_orientation = 0, 0, 0, 0, 0
682
+ else:
683
+ self.cx, self.cy, self.major_axis_len, self.minor_axis_len, self.axes_orientation = get_inertia_axes(self.mo)
428
684
 
429
685
  def get_standard_deviations(self):
430
686
  """
431
- Calculate the standard deviations along x and y.
687
+ Calculate and store standard deviations along x and y (sx, sy).
432
688
 
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.
689
+ Notes
690
+ -----
691
+ Requires centroid and moments; values are stored in `self.sx` and `self.sy`.
443
692
 
444
693
  Returns
445
694
  -------
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.
695
+ None
452
696
 
453
697
  Examples
454
698
  --------
455
- >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["standard_deviation_x", "standard_deviation_y"])
699
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["standard_deviation_x", "standard_deviation_y"])
456
700
  >>> print(SD.sx, SD.sy)
457
701
  0.816496580927726 0.816496580927726
458
702
  """
@@ -463,37 +707,19 @@ class ShapeDescriptors:
463
707
 
464
708
  def get_skewness(self):
465
709
  """
466
- Calculate the skewness of an image.
710
+ Calculate and store skewness along x and y (skx, sky).
467
711
 
468
712
  This function computes the skewness about the x-axis and y-axis of
469
713
  an image. Skewness is a measure of the asymmetry of the probability
470
714
  distribution of values in an image.
471
715
 
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
716
+ Notes
717
+ -----
718
+ Requires standard deviations; values are stored in `self.skx` and `self.sky`.
487
719
 
488
720
  Returns
489
721
  -------
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.
722
+ None
497
723
 
498
724
  Examples
499
725
  --------
@@ -534,56 +760,40 @@ class ShapeDescriptors:
534
760
 
535
761
  def get_convex_hull(self):
536
762
  """
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.
763
+ Compute and store the convex hull of the object's contour.
541
764
 
542
- Parameters
543
- ----------
544
- self : Object
545
- The object containing the contours and convex hull attributes.
765
+ Notes
766
+ -----
767
+ Stores the result in `self.convex_hull`. Computes contours if needed.
546
768
 
547
769
  Returns
548
770
  -------
549
771
  None
550
772
 
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
773
  Examples
557
774
  --------
558
775
  >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["solidity"])
559
776
  >>> print(len(SD.convex_hull))
560
777
  4
561
778
  """
562
- if self.contours is None:
563
- self.get_contours()
564
- self.convex_hull = cv2.convexHull(self.contours)
779
+ if self.area == 0:
780
+ self.convex_hull = np.array([], np.uint8)
781
+ else:
782
+ if self.contours is None:
783
+ self.get_contours()
784
+ self.convex_hull = cv2.convexHull(self.contours)
565
785
 
566
786
  def get_perimeter(self):
567
787
  """
568
- Get the perimeter of a contour.
788
+ Compute and store the contour perimeter length.
569
789
 
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
790
+ Notes
791
+ -----
792
+ Computes contours if needed and stores the length in `self.perimeter`.
576
793
 
577
794
  Returns
578
795
  -------
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.
796
+ None
587
797
 
588
798
  Examples
589
799
  --------
@@ -591,84 +801,54 @@ class ShapeDescriptors:
591
801
  >>> print(SD.perimeter)
592
802
  8.0
593
803
  """
594
- if self.contours is None:
595
- self.get_contours()
596
- self.perimeter = cv2.arcLength(self.contours, True)
804
+ if self.area == 0:
805
+ self.perimeter = 0.
806
+ else:
807
+ if self.contours is None:
808
+ self.get_contours()
809
+ if len(self.contours) == 0:
810
+ self.perimeter = 0.
811
+ else:
812
+ self.perimeter = cv2.arcLength(self.contours, True)
597
813
 
598
814
  def get_circularity(self):
599
815
  """
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.
816
+ Compute and store circularity: 4πA / P².
609
817
 
610
- Attributes
611
- ----------
612
- circularity : float
613
- The calculated circularity value, set as an attribute of the object.
818
+ Notes
819
+ -----
820
+ Uses `self.area` and `self.perimeter`; stores result in `self.circularity`.
614
821
 
615
822
  Returns
616
823
  -------
617
824
  None
618
825
 
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
826
  Examples
625
827
  --------
626
- >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["circularity"])
828
+ >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["circularity"])
627
829
  >>> print(SD.circularity)
628
830
  1.7671458676442586
629
831
  """
630
- if self.perimeter is None:
631
- self.get_perimeter()
632
- if self.perimeter == 0:
633
- self.circularity = 0
832
+ if self.area == 0:
833
+ self.circularity = 0.
634
834
  else:
635
- self.circularity = (4 * np.pi * self.binary_image.sum()) / np.square(self.perimeter)
835
+ if self.perimeter is None:
836
+ self.get_perimeter()
837
+ if self.perimeter == 0:
838
+ self.circularity = 0.
839
+ else:
840
+ self.circularity = (4 * np.pi * self.binary_image.sum()) / np.square(self.perimeter)
636
841
 
637
842
  def get_rectangularity(self):
638
843
  """
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
844
+ Compute and store rectangularity: area / bounding-rectangle-area.
662
845
 
663
846
  Notes
664
847
  -----
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()`.
848
+ Uses `self.binary_image` and `self.min_bounding_rectangle`. Computes the MBR if needed.
669
849
 
670
- Attributes
671
- ----------
850
+ Returns
851
+ -------
672
852
  None
673
853
 
674
854
  Examples
@@ -677,13 +857,16 @@ class ShapeDescriptors:
677
857
  >>> print(SD.rectangularity)
678
858
  2.25
679
859
  """
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
860
+ if self.area == 0:
861
+ self.rectangularity = 0.
685
862
  else:
686
- self.rectangularity = 1
863
+ if self.min_bounding_rectangle is None:
864
+ self.get_min_bounding_rectangle()
865
+ bounding_rectangle_area = self.min_bounding_rectangle[1][0] * self.min_bounding_rectangle[1][1]
866
+ if bounding_rectangle_area == 0:
867
+ self.rectangularity = 0.
868
+ else:
869
+ self.rectangularity = self.binary_image.sum() / bounding_rectangle_area
687
870
 
688
871
  def get_total_hole_area(self):
689
872
  """
@@ -709,33 +892,15 @@ class ShapeDescriptors:
709
892
  >>> print(SD.total_hole_area)
710
893
  0
711
894
  """
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
895
  nb, new_order = cv2.connectedComponents(1 - self.binary_image)
730
896
  if nb > 2:
731
- self.total_hole_area = (new_order > 2).sum()
897
+ self.total_hole_area = (new_order > 1).sum()
732
898
  else:
733
- self.total_hole_area = 0
899
+ self.total_hole_area = 0.
734
900
 
735
901
  def get_solidity(self):
736
902
  """
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.
903
+ Compute and store solidity: contour area / convex hull area.
739
904
 
740
905
  Extended Summary
741
906
  ----------------
@@ -743,75 +908,46 @@ class ShapeDescriptors:
743
908
  its convex hull. A solidity of 1 means the contour is fully convex, while a
744
909
  value less than 1 indicates concavities.
745
910
 
746
- Parameters
747
- ----------
748
- None
749
-
750
- Other Parameters
751
- -----------------
752
- None
911
+ Notes
912
+ -----
913
+ If the convex hull area is 0 or absent, solidity is set to 0.
753
914
 
754
915
  Returns
755
916
  -------
756
- float
757
- The solidity of the contour.
758
-
759
- Raises
760
- ------
761
917
  None
762
918
 
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
919
  Examples
776
920
  --------
777
921
  >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["solidity"])
778
922
  >>> print(SD.solidity)
779
923
  1.0
780
924
  """
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)
925
+ if self.area == 0:
926
+ self.solidity = 0.
786
927
  else:
787
- self.solidity = 1
928
+ if self.convex_hull is None:
929
+ self.get_convex_hull()
930
+ if len(self.convex_hull) == 0:
931
+ self.solidity = 0.
932
+ else:
933
+ hull_area = cv2.contourArea(self.convex_hull)
934
+ if hull_area == 0:
935
+ self.solidity = 0.
936
+ else:
937
+ self.solidity = cv2.contourArea(self.contours) / hull_area
788
938
 
789
939
  def get_convexity(self):
790
940
  """
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.
941
+ Compute and store convexity: convex hull perimeter / contour perimeter.
795
942
 
796
- Parameters
797
- ----------
798
- None
943
+ Notes
944
+ -----
945
+ Requires `self.perimeter` and `self.convex_hull`.
799
946
 
800
947
  Returns
801
948
  -------
802
- float
803
- The convexity ratio of the shape.
804
-
805
- Raises
806
- ------
807
949
  None
808
950
 
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
951
  Examples
816
952
  --------
817
953
  >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["convexity"])
@@ -822,41 +958,23 @@ class ShapeDescriptors:
822
958
  self.get_perimeter()
823
959
  if self.convex_hull is None:
824
960
  self.get_convex_hull()
825
- self.convexity = cv2.arcLength(self.convex_hull, True) / self.perimeter
961
+ if self.perimeter == 0 or len(self.convex_hull) == 0:
962
+ self.convexity = 0.
963
+ else:
964
+ self.convexity = cv2.arcLength(self.convex_hull, True) / self.perimeter
826
965
 
827
966
  def get_eccentricity(self):
828
967
  """
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.
968
+ Compute and store eccentricity from major and minor axis lengths.
840
969
 
841
- Other Parameters
842
- ----------------
843
- None
970
+ Notes
971
+ -----
972
+ Calls `get_inertia_axes()` if needed and stores result in `self.eccentricity`.
844
973
 
845
974
  Returns
846
975
  -------
847
- float
848
- The calculated eccentricity of the ellipsoid.
849
-
850
- Raises
851
- ------
852
976
  None
853
977
 
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
978
  Examples
861
979
  --------
862
980
  >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["eccentricity"])
@@ -864,72 +982,37 @@ class ShapeDescriptors:
864
982
  0.0
865
983
  """
866
984
  self.get_inertia_axes()
867
- self.eccentricity = np.sqrt(1 - np.square(self.minor_axis_len / self.major_axis_len))
985
+ if self.major_axis_len == 0:
986
+ self.eccentricity = 0.
987
+ else:
988
+ self.eccentricity = np.sqrt(1 - np.square(self.minor_axis_len / self.major_axis_len))
868
989
 
869
990
  def get_euler_number(self):
870
991
  """
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
992
+ Ensure contours are computed; stores Euler number in `self.euler_number` via `get_contours()`.
879
993
 
880
994
  Returns
881
995
  -------
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.
996
+ None
889
997
 
890
998
  Notes
891
999
  -----
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
1000
+ Euler number is computed in `get_contours()` as `(components - 1) - len(contours)`.
903
1001
  """
904
1002
  if self.contours is None:
905
1003
  self.get_contours()
906
1004
 
907
1005
  def get_major_axis_len(self):
908
1006
  """
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
1007
+ Ensure the major axis length is computed and stored in `self.major_axis_len`.
917
1008
 
918
1009
  Returns
919
1010
  -------
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.
1011
+ None
928
1012
 
929
1013
  Notes
930
1014
  -----
931
- - This method may trigger computation of inertia axes if they haven't been
932
- precomputed to determine the major axis length.
1015
+ Triggers `get_inertia_axes()` if needed.
933
1016
 
934
1017
  Examples
935
1018
  --------
@@ -942,29 +1025,15 @@ class ShapeDescriptors:
942
1025
 
943
1026
  def get_minor_axis_len(self):
944
1027
  """
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
1028
+ Ensure the minor axis length is computed and stored in `self.minor_axis_len`.
953
1029
 
954
1030
  Returns
955
1031
  -------
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.
1032
+ None
964
1033
 
965
1034
  Notes
966
1035
  -----
967
- This method will compute the inertia axes if `self.minor_axis_len` is not cached.
1036
+ Triggers `get_inertia_axes()` if needed.
968
1037
 
969
1038
  Examples
970
1039
  --------
@@ -977,33 +1046,15 @@ class ShapeDescriptors:
977
1046
 
978
1047
  def get_axes_orientation(self):
979
1048
  """
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
1049
+ Ensure the axes orientation angle is computed and stored in `self.axes_orientation`.
991
1050
 
992
1051
  Returns
993
1052
  -------
994
- `np.ndarray`
995
- A 3x3 matrix representing the orientation of the axes.
996
-
997
- Raises
998
- ------
999
1053
  None
1000
1054
 
1001
1055
  Notes
1002
1056
  -----
1003
- This method may trigger a computation of inertia axes if they haven't been computed yet.
1004
-
1005
- Examples
1006
- --------
1057
+ Calls `get_inertia_axes()` if orientation is not yet computed.
1007
1058
 
1008
1059
  Examples
1009
1060
  --------