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.
- cellects/__main__.py +65 -25
- cellects/config/all_vars_dict.py +18 -17
- cellects/core/cellects_threads.py +1034 -396
- cellects/core/motion_analysis.py +1664 -2010
- cellects/core/one_image_analysis.py +1082 -1061
- cellects/core/program_organizer.py +1687 -1316
- cellects/core/script_based_run.py +80 -76
- cellects/gui/advanced_parameters.py +365 -326
- cellects/gui/cellects.py +102 -91
- cellects/gui/custom_widgets.py +4 -3
- cellects/gui/first_window.py +226 -104
- cellects/gui/if_several_folders_window.py +117 -68
- cellects/gui/image_analysis_window.py +841 -450
- cellects/gui/required_output.py +100 -56
- cellects/gui/ui_strings.py +840 -0
- cellects/gui/video_analysis_window.py +317 -135
- cellects/image_analysis/cell_leaving_detection.py +64 -4
- cellects/image_analysis/image_segmentation.py +451 -22
- cellects/image_analysis/morphological_operations.py +2166 -1635
- cellects/image_analysis/network_functions.py +616 -253
- cellects/image_analysis/one_image_analysis_threads.py +94 -153
- cellects/image_analysis/oscillations_functions.py +131 -0
- cellects/image_analysis/progressively_add_distant_shapes.py +2 -3
- cellects/image_analysis/shape_descriptors.py +517 -466
- cellects/utils/formulas.py +169 -6
- cellects/utils/load_display_save.py +362 -105
- cellects/utils/utilitarian.py +86 -9
- cellects-0.2.7.dist-info/LICENSE +675 -0
- cellects-0.2.7.dist-info/METADATA +829 -0
- cellects-0.2.7.dist-info/RECORD +44 -0
- cellects/core/one_video_per_blob.py +0 -540
- cellects/image_analysis/cluster_flux_study.py +0 -102
- cellects-0.1.3.dist-info/LICENSE.odt +0 -0
- cellects-0.1.3.dist-info/METADATA +0 -176
- cellects-0.1.3.dist-info/RECORD +0 -44
- {cellects-0.1.3.dist-info → cellects-0.2.7.dist-info}/WHEEL +0 -0
- {cellects-0.1.3.dist-info → cellects-0.2.7.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
30
|
-
from cellects.utils.
|
|
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,
|
|
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',
|
|
44
|
-
'
|
|
45
|
-
]
|
|
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({'
|
|
56
|
-
|
|
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
|
|
154
|
-
self.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
self
|
|
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
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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.
|
|
381
|
-
self.
|
|
382
|
-
|
|
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
|
-
|
|
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
|
|
687
|
+
Calculate and store standard deviations along x and y (sx, sy).
|
|
432
688
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
self
|
|
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.
|
|
563
|
-
self.
|
|
564
|
-
|
|
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
|
-
|
|
788
|
+
Compute and store the contour perimeter length.
|
|
569
789
|
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
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.
|
|
595
|
-
self.
|
|
596
|
-
|
|
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
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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.
|
|
631
|
-
self.
|
|
632
|
-
if self.perimeter == 0:
|
|
633
|
-
self.circularity = 0
|
|
832
|
+
if self.area == 0:
|
|
833
|
+
self.circularity = 0.
|
|
634
834
|
else:
|
|
635
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
681
|
-
self.
|
|
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.
|
|
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 >
|
|
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
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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.
|
|
782
|
-
self.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
Examples
|
|
1006
|
-
--------
|
|
1057
|
+
Calls `get_inertia_axes()` if orientation is not yet computed.
|
|
1007
1058
|
|
|
1008
1059
|
Examples
|
|
1009
1060
|
--------
|