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