Skip to content

cellects.image.shape_descriptors

cellects.image.shape_descriptors

Module for computing shape descriptors from binary images.

This module provides a framework for calculating various geometric and statistical descriptors of shapes in binary images through configurable dictionaries and a core class. Supported metrics include area, perimeter, axis lengths, orientation, and more. Descriptor computation is controlled via category dictionaries (e.g., descriptors_categories) and implemented as methods in the ShapeDescriptors class.

Classes:

Name Description
ShapeDescriptors : Class to compute various descriptors for a binary image
Notes

Relies on OpenCV and NumPy for image processing operations. Shape descriptors: The following names, lists and computes all the variables describing a shape in a binary image. If you want to allow the software to compute another variable: 1) In the following dicts and list, you need to: add the variable name and whether to compute it (True/False) by default 2) In the ShapeDescriptors class: add a method to compute that variable 3) In the init method of the ShapeDescriptors class attribute a None value to the variable that store it add a if condition in the for loop to compute that variable when its name appear in the wanted_descriptors_list

ShapeDescriptors

This class takes :
- a binary image of 0 and 1 drawing one shape
- a list of descriptors to calculate from that image
["area", "perimeter", "circularity", "rectangularity", "total_hole_area", "solidity", "convexity",
 "eccentricity", "euler_number",

"standard_deviation_y", "standard_deviation_x", "skewness_y", "skewness_x", "kurtosis_y", "kurtosis_x",
"major_axis_len", "minor_axis_len", "axes_orientation",

"mo", "contours", "min_bounding_rectangle", "convex_hull"]

Be careful! mo, contours, min_bounding_rectangle, convex_hull,
standard_deviations, skewness and kurtosis are not atomics

https://www.researchgate.net/publication/27343879_Estimators_for_Orientation_and_Anisotropy_in_Digitized_Images

Source code in src/cellects/image/shape_descriptors.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
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
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
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
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
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
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
class ShapeDescriptors:
    """
        This class takes :
        - a binary image of 0 and 1 drawing one shape
        - a list of descriptors to calculate from that image
        ["area", "perimeter", "circularity", "rectangularity", "total_hole_area", "solidity", "convexity",
         "eccentricity", "euler_number",

        "standard_deviation_y", "standard_deviation_x", "skewness_y", "skewness_x", "kurtosis_y", "kurtosis_x",
        "major_axis_len", "minor_axis_len", "axes_orientation",

        "mo", "contours", "min_bounding_rectangle", "convex_hull"]

        Be careful! mo, contours, min_bounding_rectangle, convex_hull,
        standard_deviations, skewness and kurtosis are not atomics
    https://www.researchgate.net/publication/27343879_Estimators_for_Orientation_and_Anisotropy_in_Digitized_Images
    """

    def __init__(self, binary_image, wanted_descriptors_list):
        """
        Class to compute various descriptors for a binary image.

        Parameters
        ----------
        binary_image : ndarray
            Binary image used to compute the descriptors.
        wanted_descriptors_list : list
            List of strings with the names of the wanted descriptors.

        Attributes
        ----------
        binary_image : ndarray
            The binary image.
        descriptors : dict
            Dictionary containing the computed descriptors.
        mo : float or None, optional
            Moment of inertia (default is `None`).
        area : int or None, optional
            Area of the object (default is `None`).
        contours : ndarray or None, optional
            Contours of the object (default is `None`).
        min_bounding_rectangle : tuple or None, optional
            Minimum bounding rectangle of the object (default is `None`).
        convex_hull : ndarray or None, optional
            Convex hull of the object (default is `None`).
        major_axis_len : float or None, optional
            Major axis length of the object (default is `None`).
        minor_axis_len : float or None, optional
            Minor axis length of the object (default is `None`).
        axes_orientation : float or None, optional
            Orientation of the axes (default is `None`).
        sx : float or None, optional
            Standard deviation in x-axis (default is `None`).
        kx : float or None, optional
            Kurtosis in x-axis (default is `None`).
        skx : float or None, optional
            Skewness in x-axis (default is `None`).
        perimeter : float or None, optional
            Perimeter of the object (default is `None`).
        convexity : float or None, optional
            Convexity of the object (default is `None`).

        Examples
        --------
        >>> binary_image = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8)
        >>> wanted_descriptors_list = ["area", "perimeter"]
        >>> SD = ShapeDescriptors(binary_image, wanted_descriptors_list)
        >>> SD.descriptors
        {'area': np.uint64(9), 'perimeter': 8.0}
        """
        # Give a None value to each parameters whose presence is assessed before calculation (less calculus for speed)
        self.mo = None
        self.area = None
        self.contours = None
        self.min_bounding_rectangle = None
        self.convex_hull = None
        self.major_axis_len = None
        self.minor_axis_len = None
        self.axes_orientation = None
        self.sx = None
        self.kx = None
        self.skx = None
        self.perimeter = None
        self.convexity = None

        self.binary_image = binary_image
        if self.binary_image.dtype == 'bool':
            self.binary_image = self.binary_image.astype(np.uint8)

        self.descriptors = {i: np.empty(0, dtype=np.float64) for i in wanted_descriptors_list}
        self.get_area()

        for name in self.descriptors.keys():
            if name == "mo":
                self.get_mo()
                self.descriptors[name] = self.mo
            elif name == "area":
                self.descriptors[name] = self.area
            elif name == "contours":
                self.get_contours()
                self.descriptors[name] = self.contours
            elif name == "min_bounding_rectangle":
                self.get_min_bounding_rectangle()
                self.descriptors[name] = self.min_bounding_rectangle
            elif name == "major_axis_len":
                self.get_major_axis_len()
                self.descriptors[name] = self.major_axis_len
            elif name == "minor_axis_len":
                self.get_minor_axis_len()
                self.descriptors[name] = self.minor_axis_len
            elif name == "axes_orientation":
                self.get_inertia_axes()
                self.descriptors[name] = self.axes_orientation
            elif name == "standard_deviation_y":
                self.get_standard_deviations()
                self.descriptors[name] = self.sy
            elif name == "standard_deviation_x":
                self.get_standard_deviations()
                self.descriptors[name] = self.sx
            elif name == "skewness_y":
                self.get_skewness()
                self.descriptors[name] = self.sky
            elif name == "skewness_x":
                self.get_skewness()
                self.descriptors[name] = self.skx
            elif name == "kurtosis_y":
                self.get_kurtosis()
                self.descriptors[name] = self.ky
            elif name == "kurtosis_x":
                self.get_kurtosis()
                self.descriptors[name] = self.kx
            elif name == "convex_hull":
                self.get_convex_hull()
                self.descriptors[name] = self.convex_hull
            elif name == "perimeter":
                self.get_perimeter()
                self.descriptors[name] = self.perimeter
            elif name == "circularity":
                self.get_circularity()
                self.descriptors[name] = self.circularity
            elif name == "rectangularity":
                self.get_rectangularity()
                self.descriptors[name] = self.rectangularity
            elif name == "total_hole_area":
                self.get_total_hole_area()
                self.descriptors[name] = self.total_hole_area
            elif name == "solidity":
                self.get_solidity()
                self.descriptors[name] = self.solidity
            elif name == "convexity":
                self.get_convexity()
                self.descriptors[name] = self.convexity
            elif name == "eccentricity":
                self.get_eccentricity()
                self.descriptors[name] = self.eccentricity
            elif name == "euler_number":
                self.get_euler_number()
                self.descriptors[name] = self.euler_number

    """
        The following methods can be called to compute parameters for descriptors requiring it
    """

    def get_mo(self):
        """
        Get moments of a binary image.

        Calculate the image moments for a given binary image using OpenCV's
        `cv2.moments` function and then translate these moments into a formatted
        dictionary.

        Notes
        -----
        This function assumes the binary image has already been processed and is in a
        suitable format for moment calculation.

        Returns
        -------
        None

        Examples
        --------
       >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["mo"])
        >>> print(SD.mo["m00"])
        9.0
        """
        self.mo = translate_dict(cv2.moments(self.binary_image))

    def get_area(self):
        """
        Calculate the area of a binary image by summing its pixel values.

        This function computes the area covered by white pixels (value 1) in a binary image,
        which is equivalent to counting the number of 'on' pixels.

        Notes
        -----
        Sums values in `self.binary_image` and stores the result in `self.area`.

        Returns
        -------
        None

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["area"])
        >>> print(SD.area)
        9.0
        """
        self.area = self.binary_image.sum()

    def get_contours(self):
        """
        Find and process the largest contour in a binary image.

        Retrieves contours from a binary image, calculates the Euler number,
        and identifies the largest contour based on its length.

        Notes
        -----
        This function modifies the internal state of the `self` object to store
        the largest contour and Euler number.

        Returns
        -------
        None

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["euler_number"])
        >>> print(len(SD.contours))
        8
        """
        if self.area == 0:
            self.euler_number = 0.
            self.contours = np.array([], np.uint8)
        else:
            contours, hierarchy = cv2.findContours(self.binary_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
            nb, shapes = cv2.connectedComponents(self.binary_image, ltype=cv2.CV_16U)
            self.euler_number = (nb - 1) - len(contours)
            self.contours = contours[0]
            if len(contours) > 1:
                all_lengths = np.zeros(len(contours))
                for i, contour in enumerate(contours):
                    all_lengths[i] = len(contour)
                self.contours = contours[np.argmax(all_lengths)]

    def get_min_bounding_rectangle(self):
        """
        Retrieve the minimum bounding rectangle from the contours of an image.

        This method calculates the smallest area rectangle that can enclose
        the object outlines present in the image, which is useful for
        object detection and analysis tasks.

        Notes
        -----
        - The bounding rectangle is calculated only if contours are available.
          If not, they will be retrieved first before calculating the rectangle.

        Raises
        ------
        RuntimeError
            If the contours are not available and cannot be retrieved,
            indicating a problem with the image or preprocessing steps.

        Returns
        -------
        None

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["rectangularity"])
        >>> print(len(SD.min_bounding_rectangle))
        3
        """
        if self.area == 0:
            self.min_bounding_rectangle = np.array([], np.uint8)
        else:
            if self.contours is None:
                self.get_contours()
            if len(self.contours) == 0:
                self.min_bounding_rectangle = np.array([], np.uint8)
            else:
                self.min_bounding_rectangle = cv2.minAreaRect(self.contours)  # ((cx, cy), (width, height), angle)

    def get_inertia_axes(self):
        """
        Calculate and set the moments of inertia properties of an object.

        This function computes the centroid, major axis length,
        minor axis length, and axes orientation for an object. It
        first ensures that the moments of inertia (`mo`) attribute is available,
        computing them if necessary, before using the `get_inertia_axes` function.

        Returns
        -------
        None

            This method sets the following attributes:
            - `cx` : float
                The x-coordinate of the centroid.
            - `cy` : float
                The y-coordinate of the centroid.
            - `major_axis_len` : float
                The length of the major axis.
            - `minor_axis_len` : float
                The length of the minor axis.
            - `axes_orientation` : float
                The orientation angle of the axes.

        Raises
        ------
        ValueError
            If there is an issue with the moments of inertia computation.

        Notes
        -----
        This function modifies in-place the object's attributes related to its geometry.

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["major_axis_len"])
        >>> print(SD.axes_orientation)
        0.0
        """
        if self.mo is None:
            self.get_mo()
        if self.area == 0:
            self.cx, self.cy, self.major_axis_len, self.minor_axis_len, self.axes_orientation = 0, 0, 0, 0, 0
        else:
            self.cx, self.cy, self.major_axis_len, self.minor_axis_len, self.axes_orientation = get_inertia_axes(self.mo)

    def get_standard_deviations(self):
        """
        Calculate and store standard deviations along x and y (sx, sy).

        Notes
        -----
        Requires centroid and moments; values are stored in `self.sx` and `self.sy`.

        Returns
        -------
        None

        Examples
        --------
       >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["standard_deviation_x", "standard_deviation_y"])
        >>> print(SD.sx, SD.sy)
        0.816496580927726 0.816496580927726
        """
        if self.sx is None:
            if self.axes_orientation is None:
                self.get_inertia_axes()
            self.sx, self.sy = get_standard_deviations(self.mo, self.binary_image, self.cx, self.cy)

    def get_skewness(self):
        """
        Calculate and store skewness along x and y (skx, sky).

        This function computes the skewness about the x-axis and y-axis of
        an image. Skewness is a measure of the asymmetry of the probability
        distribution of values in an image.

        Notes
        -----
        Requires standard deviations; values are stored in `self.skx` and `self.sky`.

        Returns
        -------
        None

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["skewness_x", "skewness_y"])
        >>> print(SD.skx, SD.sky)
        0.0 0.0
        """
        if self.skx is None:
            if self.sx is None:
                self.get_standard_deviations()

            self.skx, self.sky = get_skewness(self.mo, self.binary_image, self.cx, self.cy, self.sx, self.sy)

    def get_kurtosis(self):
        """
        Calculates the kurtosis of the image moments.

        Kurtosis is a statistical measure that describes the shape of
        a distribution's tails in relation to its overall shape. It is
        used here in the context of image moments analysis.

        Notes
        -----
        This function first checks if the kurtosis values have already been calculated.
        If not, it calculates them using the `get_kurtosis` function.

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["kurtosis_x", "kurtosis_y"])
        >>> print(SD.kx, SD.ky)
        1.5 1.5
        """
        if self.kx is None:
            if self.sx is None:
                self.get_standard_deviations()

            self.kx, self.ky = get_kurtosis(self.mo, self.binary_image, self.cx, self.cy, self.sx, self.sy)

    def get_convex_hull(self):
        """
        Compute and store the convex hull of the object's contour.

        Notes
        -----
        Stores the result in `self.convex_hull`. Computes contours if needed.

        Returns
        -------
        None

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["solidity"])
        >>> print(len(SD.convex_hull))
        4
        """
        if self.area == 0:
            self.convex_hull = np.array([], np.uint8)
        else:
            if self.contours is None:
                self.get_contours()
            self.convex_hull = cv2.convexHull(self.contours)

    def get_perimeter(self):
        """
        Compute and store the contour perimeter length.

        Notes
        -----
        Computes contours if needed and stores the length in `self.perimeter`.

        Returns
        -------
        None

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["perimeter"])
        >>> print(SD.perimeter)
        8.0
        """
        if self.area == 0:
            self.perimeter = 0.
        else:
            if self.contours is None:
                self.get_contours()
            if len(self.contours) == 0:
                self.perimeter = 0.
            else:
                self.perimeter = cv2.arcLength(self.contours, True)

    def get_circularity(self):
        """
        Compute and store circularity: 4πA / P².

        Notes
        -----
        Uses `self.area` and `self.perimeter`; stores result in `self.circularity`.

        Returns
        -------
        None

        Examples
        --------
         >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["circularity"])
        >>> print(SD.circularity)
        1.7671458676442586
        """
        if self.area == 0:
            self.circularity = 0.
        else:
            if self.perimeter is None:
                self.get_perimeter()
            if self.perimeter == 0:
                self.circularity = 0.
            else:
                self.circularity = (4 * np.pi * self.binary_image.sum()) / np.square(self.perimeter)

    def get_rectangularity(self):
        """
        Compute and store rectangularity: area / bounding-rectangle-area.

        Notes
        -----
        Uses `self.binary_image` and `self.min_bounding_rectangle`. Computes the MBR if needed.

        Returns
        -------
        None

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["rectangularity"])
        >>> print(SD.rectangularity)
        2.25
        """
        if self.area == 0:
            self.rectangularity = 0.
        else:
            if self.min_bounding_rectangle is None:
                self.get_min_bounding_rectangle()
            bounding_rectangle_area = self.min_bounding_rectangle[1][0] * self.min_bounding_rectangle[1][1]
            if bounding_rectangle_area == 0:
                self.rectangularity = 0.
            else:
                self.rectangularity = self.binary_image.sum() / bounding_rectangle_area

    def get_total_hole_area(self):
        """
        Calculate the total area of holes in a binary image.

        This function uses connected component labeling to detect and
        measure the area of holes in a binary image.

        Returns
        -------
        float
            The total area of all detected holes in the binary image.

        Notes
        -----
        This function assumes that the binary image has been pre-processed
        and that holes are represented as connected components of zero
        pixels within the foreground

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["total_hole_area"])
        >>> print(SD.total_hole_area)
        0
        """
        nb, new_order = cv2.connectedComponents(1 - self.binary_image)
        if nb > 2:
            self.total_hole_area = (new_order > 1).sum()
        else:
            self.total_hole_area = 0.

    def get_solidity(self):
        """
        Compute and store solidity: contour area / convex hull area.

        Extended Summary
        ----------------
        The solidity is a dimensionless measure that compares the area of a shape to
        its convex hull. A solidity of 1 means the contour is fully convex, while a
        value less than 1 indicates concavities.

        Notes
        -----
        If the convex hull area is 0 or absent, solidity is set to 0.

        Returns
        -------
        None

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["solidity"])
        >>> print(SD.solidity)
        1.0
        """
        if self.area == 0:
            self.solidity = 0.
        else:
            if self.convex_hull is None:
                self.get_convex_hull()
            if len(self.convex_hull) == 0:
                self.solidity = 0.
            else:
                hull_area = cv2.contourArea(self.convex_hull)
                if hull_area == 0:
                    self.solidity = 0.
                else:
                    self.solidity = cv2.contourArea(self.contours) / hull_area

    def get_convexity(self):
        """
        Compute and store convexity: convex hull perimeter / contour perimeter.

        Notes
        -----
        Requires `self.perimeter` and `self.convex_hull`.

        Returns
        -------
        None

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["convexity"])
        >>> print(SD.convexity)
        1.0
        """
        if self.perimeter is None:
            self.get_perimeter()
        if self.convex_hull is None:
            self.get_convex_hull()
        if self.perimeter == 0 or len(self.convex_hull) == 0:
            self.convexity = 0.
        else:
            self.convexity = cv2.arcLength(self.convex_hull, True) / self.perimeter

    def get_eccentricity(self):
        """
        Compute and store eccentricity from major and minor axis lengths.

        Notes
        -----
        Calls `get_inertia_axes()` if needed and stores result in `self.eccentricity`.

        Returns
        -------
        None

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["eccentricity"])
        >>> print(SD.eccentricity)
        0.0
        """
        self.get_inertia_axes()
        if self.major_axis_len == 0:
            self.eccentricity = 0.
        else:
            self.eccentricity = np.sqrt(1 - np.square(self.minor_axis_len / self.major_axis_len))

    def get_euler_number(self):
        """
        Ensure contours are computed; stores Euler number in `self.euler_number` via `get_contours()`.

        Returns
        -------
        None

        Notes
        -----
        Euler number is computed in `get_contours()` as `(components - 1) - len(contours)`.
        """
        if self.contours is None:
            self.get_contours()

    def get_major_axis_len(self):
        """
        Ensure the major axis length is computed and stored in `self.major_axis_len`.

        Returns
        -------
        None

        Notes
        -----
        Triggers `get_inertia_axes()` if needed.

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8), ["major_axis_len"])
        >>> print(SD.major_axis_len)
        2.8284271247461907
        """
        if self.major_axis_len is None:
            self.get_inertia_axes()

    def get_minor_axis_len(self):
        """
        Ensure the minor axis length is computed and stored in `self.minor_axis_len`.

        Returns
        -------
        None

        Notes
        -----
        Triggers `get_inertia_axes()` if needed.

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8), ["minor_axis_len"])
        >>> print(SD.minor_axis_len)
        0.0
        """
        if self.minor_axis_len is None:
            self.get_inertia_axes()

    def get_axes_orientation(self):
        """
        Ensure the axes orientation angle is computed and stored in `self.axes_orientation`.

        Returns
        -------
        None

        Notes
        -----
        Calls `get_inertia_axes()` if orientation is not yet computed.

        Examples
        --------
        >>> SD = ShapeDescriptors(np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8), ["axes_orientation"])
        >>> print(SD.axes_orientation)
        1.5707963267948966
        """
        if self.axes_orientation is None:
            self.get_inertia_axes()

__init__(binary_image, wanted_descriptors_list)

Class to compute various descriptors for a binary image.

Parameters:

Name Type Description Default
binary_image ndarray

Binary image used to compute the descriptors.

required
wanted_descriptors_list list

List of strings with the names of the wanted descriptors.

required

Attributes:

Name Type Description
binary_image ndarray

The binary image.

descriptors dict

Dictionary containing the computed descriptors.

mo (float or None, optional)

Moment of inertia (default is None).

area (int or None, optional)

Area of the object (default is None).

contours (ndarray or None, optional)

Contours of the object (default is None).

min_bounding_rectangle (tuple or None, optional)

Minimum bounding rectangle of the object (default is None).

convex_hull (ndarray or None, optional)

Convex hull of the object (default is None).

major_axis_len (float or None, optional)

Major axis length of the object (default is None).

minor_axis_len (float or None, optional)

Minor axis length of the object (default is None).

axes_orientation (float or None, optional)

Orientation of the axes (default is None).

sx (float or None, optional)

Standard deviation in x-axis (default is None).

kx (float or None, optional)

Kurtosis in x-axis (default is None).

skx (float or None, optional)

Skewness in x-axis (default is None).

perimeter (float or None, optional)

Perimeter of the object (default is None).

convexity (float or None, optional)

Convexity of the object (default is None).

Examples:

>>> binary_image = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8)
>>> wanted_descriptors_list = ["area", "perimeter"]
>>> SD = ShapeDescriptors(binary_image, wanted_descriptors_list)
>>> SD.descriptors
{'area': np.uint64(9), 'perimeter': 8.0}
Source code in src/cellects/image/shape_descriptors.py
def __init__(self, binary_image, wanted_descriptors_list):
    """
    Class to compute various descriptors for a binary image.

    Parameters
    ----------
    binary_image : ndarray
        Binary image used to compute the descriptors.
    wanted_descriptors_list : list
        List of strings with the names of the wanted descriptors.

    Attributes
    ----------
    binary_image : ndarray
        The binary image.
    descriptors : dict
        Dictionary containing the computed descriptors.
    mo : float or None, optional
        Moment of inertia (default is `None`).
    area : int or None, optional
        Area of the object (default is `None`).
    contours : ndarray or None, optional
        Contours of the object (default is `None`).
    min_bounding_rectangle : tuple or None, optional
        Minimum bounding rectangle of the object (default is `None`).
    convex_hull : ndarray or None, optional
        Convex hull of the object (default is `None`).
    major_axis_len : float or None, optional
        Major axis length of the object (default is `None`).
    minor_axis_len : float or None, optional
        Minor axis length of the object (default is `None`).
    axes_orientation : float or None, optional
        Orientation of the axes (default is `None`).
    sx : float or None, optional
        Standard deviation in x-axis (default is `None`).
    kx : float or None, optional
        Kurtosis in x-axis (default is `None`).
    skx : float or None, optional
        Skewness in x-axis (default is `None`).
    perimeter : float or None, optional
        Perimeter of the object (default is `None`).
    convexity : float or None, optional
        Convexity of the object (default is `None`).

    Examples
    --------
    >>> binary_image = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8)
    >>> wanted_descriptors_list = ["area", "perimeter"]
    >>> SD = ShapeDescriptors(binary_image, wanted_descriptors_list)
    >>> SD.descriptors
    {'area': np.uint64(9), 'perimeter': 8.0}
    """
    # Give a None value to each parameters whose presence is assessed before calculation (less calculus for speed)
    self.mo = None
    self.area = None
    self.contours = None
    self.min_bounding_rectangle = None
    self.convex_hull = None
    self.major_axis_len = None
    self.minor_axis_len = None
    self.axes_orientation = None
    self.sx = None
    self.kx = None
    self.skx = None
    self.perimeter = None
    self.convexity = None

    self.binary_image = binary_image
    if self.binary_image.dtype == 'bool':
        self.binary_image = self.binary_image.astype(np.uint8)

    self.descriptors = {i: np.empty(0, dtype=np.float64) for i in wanted_descriptors_list}
    self.get_area()

    for name in self.descriptors.keys():
        if name == "mo":
            self.get_mo()
            self.descriptors[name] = self.mo
        elif name == "area":
            self.descriptors[name] = self.area
        elif name == "contours":
            self.get_contours()
            self.descriptors[name] = self.contours
        elif name == "min_bounding_rectangle":
            self.get_min_bounding_rectangle()
            self.descriptors[name] = self.min_bounding_rectangle
        elif name == "major_axis_len":
            self.get_major_axis_len()
            self.descriptors[name] = self.major_axis_len
        elif name == "minor_axis_len":
            self.get_minor_axis_len()
            self.descriptors[name] = self.minor_axis_len
        elif name == "axes_orientation":
            self.get_inertia_axes()
            self.descriptors[name] = self.axes_orientation
        elif name == "standard_deviation_y":
            self.get_standard_deviations()
            self.descriptors[name] = self.sy
        elif name == "standard_deviation_x":
            self.get_standard_deviations()
            self.descriptors[name] = self.sx
        elif name == "skewness_y":
            self.get_skewness()
            self.descriptors[name] = self.sky
        elif name == "skewness_x":
            self.get_skewness()
            self.descriptors[name] = self.skx
        elif name == "kurtosis_y":
            self.get_kurtosis()
            self.descriptors[name] = self.ky
        elif name == "kurtosis_x":
            self.get_kurtosis()
            self.descriptors[name] = self.kx
        elif name == "convex_hull":
            self.get_convex_hull()
            self.descriptors[name] = self.convex_hull
        elif name == "perimeter":
            self.get_perimeter()
            self.descriptors[name] = self.perimeter
        elif name == "circularity":
            self.get_circularity()
            self.descriptors[name] = self.circularity
        elif name == "rectangularity":
            self.get_rectangularity()
            self.descriptors[name] = self.rectangularity
        elif name == "total_hole_area":
            self.get_total_hole_area()
            self.descriptors[name] = self.total_hole_area
        elif name == "solidity":
            self.get_solidity()
            self.descriptors[name] = self.solidity
        elif name == "convexity":
            self.get_convexity()
            self.descriptors[name] = self.convexity
        elif name == "eccentricity":
            self.get_eccentricity()
            self.descriptors[name] = self.eccentricity
        elif name == "euler_number":
            self.get_euler_number()
            self.descriptors[name] = self.euler_number

get_area()

Calculate the area of a binary image by summing its pixel values.

This function computes the area covered by white pixels (value 1) in a binary image, which is equivalent to counting the number of 'on' pixels.

Notes

Sums values in self.binary_image and stores the result in self.area.

Returns:

Type Description
None

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["area"])
>>> print(SD.area)
9.0
Source code in src/cellects/image/shape_descriptors.py
def get_area(self):
    """
    Calculate the area of a binary image by summing its pixel values.

    This function computes the area covered by white pixels (value 1) in a binary image,
    which is equivalent to counting the number of 'on' pixels.

    Notes
    -----
    Sums values in `self.binary_image` and stores the result in `self.area`.

    Returns
    -------
    None

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["area"])
    >>> print(SD.area)
    9.0
    """
    self.area = self.binary_image.sum()

get_axes_orientation()

Ensure the axes orientation angle is computed and stored in self.axes_orientation.

Returns:

Type Description
None
Notes

Calls get_inertia_axes() if orientation is not yet computed.

Examples:

>>> SD = ShapeDescriptors(np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8), ["axes_orientation"])
>>> print(SD.axes_orientation)
1.5707963267948966
Source code in src/cellects/image/shape_descriptors.py
def get_axes_orientation(self):
    """
    Ensure the axes orientation angle is computed and stored in `self.axes_orientation`.

    Returns
    -------
    None

    Notes
    -----
    Calls `get_inertia_axes()` if orientation is not yet computed.

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8), ["axes_orientation"])
    >>> print(SD.axes_orientation)
    1.5707963267948966
    """
    if self.axes_orientation is None:
        self.get_inertia_axes()

get_circularity()

Compute and store circularity: 4πA / P².

Notes

Uses self.area and self.perimeter; stores result in self.circularity.

Returns:

Type Description
None

Examples:

SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["circularity"])

>>> print(SD.circularity)
1.7671458676442586
Source code in src/cellects/image/shape_descriptors.py
def get_circularity(self):
    """
    Compute and store circularity: 4πA / P².

    Notes
    -----
    Uses `self.area` and `self.perimeter`; stores result in `self.circularity`.

    Returns
    -------
    None

    Examples
    --------
     >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["circularity"])
    >>> print(SD.circularity)
    1.7671458676442586
    """
    if self.area == 0:
        self.circularity = 0.
    else:
        if self.perimeter is None:
            self.get_perimeter()
        if self.perimeter == 0:
            self.circularity = 0.
        else:
            self.circularity = (4 * np.pi * self.binary_image.sum()) / np.square(self.perimeter)

get_contours()

Find and process the largest contour in a binary image.

Retrieves contours from a binary image, calculates the Euler number, and identifies the largest contour based on its length.

Notes

This function modifies the internal state of the self object to store the largest contour and Euler number.

Returns:

Type Description
None

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["euler_number"])
>>> print(len(SD.contours))
8
Source code in src/cellects/image/shape_descriptors.py
def get_contours(self):
    """
    Find and process the largest contour in a binary image.

    Retrieves contours from a binary image, calculates the Euler number,
    and identifies the largest contour based on its length.

    Notes
    -----
    This function modifies the internal state of the `self` object to store
    the largest contour and Euler number.

    Returns
    -------
    None

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["euler_number"])
    >>> print(len(SD.contours))
    8
    """
    if self.area == 0:
        self.euler_number = 0.
        self.contours = np.array([], np.uint8)
    else:
        contours, hierarchy = cv2.findContours(self.binary_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
        nb, shapes = cv2.connectedComponents(self.binary_image, ltype=cv2.CV_16U)
        self.euler_number = (nb - 1) - len(contours)
        self.contours = contours[0]
        if len(contours) > 1:
            all_lengths = np.zeros(len(contours))
            for i, contour in enumerate(contours):
                all_lengths[i] = len(contour)
            self.contours = contours[np.argmax(all_lengths)]

get_convex_hull()

Compute and store the convex hull of the object's contour.

Notes

Stores the result in self.convex_hull. Computes contours if needed.

Returns:

Type Description
None

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["solidity"])
>>> print(len(SD.convex_hull))
4
Source code in src/cellects/image/shape_descriptors.py
def get_convex_hull(self):
    """
    Compute and store the convex hull of the object's contour.

    Notes
    -----
    Stores the result in `self.convex_hull`. Computes contours if needed.

    Returns
    -------
    None

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["solidity"])
    >>> print(len(SD.convex_hull))
    4
    """
    if self.area == 0:
        self.convex_hull = np.array([], np.uint8)
    else:
        if self.contours is None:
            self.get_contours()
        self.convex_hull = cv2.convexHull(self.contours)

get_convexity()

Compute and store convexity: convex hull perimeter / contour perimeter.

Notes

Requires self.perimeter and self.convex_hull.

Returns:

Type Description
None

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["convexity"])
>>> print(SD.convexity)
1.0
Source code in src/cellects/image/shape_descriptors.py
def get_convexity(self):
    """
    Compute and store convexity: convex hull perimeter / contour perimeter.

    Notes
    -----
    Requires `self.perimeter` and `self.convex_hull`.

    Returns
    -------
    None

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["convexity"])
    >>> print(SD.convexity)
    1.0
    """
    if self.perimeter is None:
        self.get_perimeter()
    if self.convex_hull is None:
        self.get_convex_hull()
    if self.perimeter == 0 or len(self.convex_hull) == 0:
        self.convexity = 0.
    else:
        self.convexity = cv2.arcLength(self.convex_hull, True) / self.perimeter

get_eccentricity()

Compute and store eccentricity from major and minor axis lengths.

Notes

Calls get_inertia_axes() if needed and stores result in self.eccentricity.

Returns:

Type Description
None

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["eccentricity"])
>>> print(SD.eccentricity)
0.0
Source code in src/cellects/image/shape_descriptors.py
def get_eccentricity(self):
    """
    Compute and store eccentricity from major and minor axis lengths.

    Notes
    -----
    Calls `get_inertia_axes()` if needed and stores result in `self.eccentricity`.

    Returns
    -------
    None

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["eccentricity"])
    >>> print(SD.eccentricity)
    0.0
    """
    self.get_inertia_axes()
    if self.major_axis_len == 0:
        self.eccentricity = 0.
    else:
        self.eccentricity = np.sqrt(1 - np.square(self.minor_axis_len / self.major_axis_len))

get_euler_number()

Ensure contours are computed; stores Euler number in self.euler_number via get_contours().

Returns:

Type Description
None
Notes

Euler number is computed in get_contours() as (components - 1) - len(contours).

Source code in src/cellects/image/shape_descriptors.py
def get_euler_number(self):
    """
    Ensure contours are computed; stores Euler number in `self.euler_number` via `get_contours()`.

    Returns
    -------
    None

    Notes
    -----
    Euler number is computed in `get_contours()` as `(components - 1) - len(contours)`.
    """
    if self.contours is None:
        self.get_contours()

get_inertia_axes()

Calculate and set the moments of inertia properties of an object.

This function computes the centroid, major axis length, minor axis length, and axes orientation for an object. It first ensures that the moments of inertia (mo) attribute is available, computing them if necessary, before using the get_inertia_axes function.

Returns:

Type Description
None

This method sets the following attributes: - cx : float The x-coordinate of the centroid. - cy : float The y-coordinate of the centroid. - major_axis_len : float The length of the major axis. - minor_axis_len : float The length of the minor axis. - axes_orientation : float The orientation angle of the axes.

Raises:

Type Description
ValueError

If there is an issue with the moments of inertia computation.

Notes

This function modifies in-place the object's attributes related to its geometry.

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["major_axis_len"])
>>> print(SD.axes_orientation)
0.0
Source code in src/cellects/image/shape_descriptors.py
def get_inertia_axes(self):
    """
    Calculate and set the moments of inertia properties of an object.

    This function computes the centroid, major axis length,
    minor axis length, and axes orientation for an object. It
    first ensures that the moments of inertia (`mo`) attribute is available,
    computing them if necessary, before using the `get_inertia_axes` function.

    Returns
    -------
    None

        This method sets the following attributes:
        - `cx` : float
            The x-coordinate of the centroid.
        - `cy` : float
            The y-coordinate of the centroid.
        - `major_axis_len` : float
            The length of the major axis.
        - `minor_axis_len` : float
            The length of the minor axis.
        - `axes_orientation` : float
            The orientation angle of the axes.

    Raises
    ------
    ValueError
        If there is an issue with the moments of inertia computation.

    Notes
    -----
    This function modifies in-place the object's attributes related to its geometry.

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["major_axis_len"])
    >>> print(SD.axes_orientation)
    0.0
    """
    if self.mo is None:
        self.get_mo()
    if self.area == 0:
        self.cx, self.cy, self.major_axis_len, self.minor_axis_len, self.axes_orientation = 0, 0, 0, 0, 0
    else:
        self.cx, self.cy, self.major_axis_len, self.minor_axis_len, self.axes_orientation = get_inertia_axes(self.mo)

get_kurtosis()

Calculates the kurtosis of the image moments.

Kurtosis is a statistical measure that describes the shape of a distribution's tails in relation to its overall shape. It is used here in the context of image moments analysis.

Notes

This function first checks if the kurtosis values have already been calculated. If not, it calculates them using the get_kurtosis function.

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["kurtosis_x", "kurtosis_y"])
>>> print(SD.kx, SD.ky)
1.5 1.5
Source code in src/cellects/image/shape_descriptors.py
def get_kurtosis(self):
    """
    Calculates the kurtosis of the image moments.

    Kurtosis is a statistical measure that describes the shape of
    a distribution's tails in relation to its overall shape. It is
    used here in the context of image moments analysis.

    Notes
    -----
    This function first checks if the kurtosis values have already been calculated.
    If not, it calculates them using the `get_kurtosis` function.

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["kurtosis_x", "kurtosis_y"])
    >>> print(SD.kx, SD.ky)
    1.5 1.5
    """
    if self.kx is None:
        if self.sx is None:
            self.get_standard_deviations()

        self.kx, self.ky = get_kurtosis(self.mo, self.binary_image, self.cx, self.cy, self.sx, self.sy)

get_major_axis_len()

Ensure the major axis length is computed and stored in self.major_axis_len.

Returns:

Type Description
None
Notes

Triggers get_inertia_axes() if needed.

Examples:

>>> SD = ShapeDescriptors(np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8), ["major_axis_len"])
>>> print(SD.major_axis_len)
2.8284271247461907
Source code in src/cellects/image/shape_descriptors.py
def get_major_axis_len(self):
    """
    Ensure the major axis length is computed and stored in `self.major_axis_len`.

    Returns
    -------
    None

    Notes
    -----
    Triggers `get_inertia_axes()` if needed.

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8), ["major_axis_len"])
    >>> print(SD.major_axis_len)
    2.8284271247461907
    """
    if self.major_axis_len is None:
        self.get_inertia_axes()

get_min_bounding_rectangle()

Retrieve the minimum bounding rectangle from the contours of an image.

This method calculates the smallest area rectangle that can enclose the object outlines present in the image, which is useful for object detection and analysis tasks.

Notes
  • The bounding rectangle is calculated only if contours are available. If not, they will be retrieved first before calculating the rectangle.

Raises:

Type Description
RuntimeError

If the contours are not available and cannot be retrieved, indicating a problem with the image or preprocessing steps.

Returns:

Type Description
None

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["rectangularity"])
>>> print(len(SD.min_bounding_rectangle))
3
Source code in src/cellects/image/shape_descriptors.py
def get_min_bounding_rectangle(self):
    """
    Retrieve the minimum bounding rectangle from the contours of an image.

    This method calculates the smallest area rectangle that can enclose
    the object outlines present in the image, which is useful for
    object detection and analysis tasks.

    Notes
    -----
    - The bounding rectangle is calculated only if contours are available.
      If not, they will be retrieved first before calculating the rectangle.

    Raises
    ------
    RuntimeError
        If the contours are not available and cannot be retrieved,
        indicating a problem with the image or preprocessing steps.

    Returns
    -------
    None

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["rectangularity"])
    >>> print(len(SD.min_bounding_rectangle))
    3
    """
    if self.area == 0:
        self.min_bounding_rectangle = np.array([], np.uint8)
    else:
        if self.contours is None:
            self.get_contours()
        if len(self.contours) == 0:
            self.min_bounding_rectangle = np.array([], np.uint8)
        else:
            self.min_bounding_rectangle = cv2.minAreaRect(self.contours)  # ((cx, cy), (width, height), angle)

get_minor_axis_len()

Ensure the minor axis length is computed and stored in self.minor_axis_len.

Returns:

Type Description
None
Notes

Triggers get_inertia_axes() if needed.

Examples:

>>> SD = ShapeDescriptors(np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8), ["minor_axis_len"])
>>> print(SD.minor_axis_len)
0.0
Source code in src/cellects/image/shape_descriptors.py
def get_minor_axis_len(self):
    """
    Ensure the minor axis length is computed and stored in `self.minor_axis_len`.

    Returns
    -------
    None

    Notes
    -----
    Triggers `get_inertia_axes()` if needed.

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8), ["minor_axis_len"])
    >>> print(SD.minor_axis_len)
    0.0
    """
    if self.minor_axis_len is None:
        self.get_inertia_axes()

get_mo()

Get moments of a binary image.

Calculate the image moments for a given binary image using OpenCV's cv2.moments function and then translate these moments into a formatted dictionary.

Notes

This function assumes the binary image has already been processed and is in a suitable format for moment calculation.

Returns

None

Examples

SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["mo"]) print(SD.mo["m00"]) 9.0

Source code in src/cellects/image/shape_descriptors.py
def get_mo(self):
    """
    Get moments of a binary image.

    Calculate the image moments for a given binary image using OpenCV's
    `cv2.moments` function and then translate these moments into a formatted
    dictionary.

    Notes
    -----
    This function assumes the binary image has already been processed and is in a
    suitable format for moment calculation.

    Returns
    -------
    None

    Examples
    --------
   >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["mo"])
    >>> print(SD.mo["m00"])
    9.0
    """
    self.mo = translate_dict(cv2.moments(self.binary_image))

get_perimeter()

Compute and store the contour perimeter length.

Notes

Computes contours if needed and stores the length in self.perimeter.

Returns:

Type Description
None

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["perimeter"])
>>> print(SD.perimeter)
8.0
Source code in src/cellects/image/shape_descriptors.py
def get_perimeter(self):
    """
    Compute and store the contour perimeter length.

    Notes
    -----
    Computes contours if needed and stores the length in `self.perimeter`.

    Returns
    -------
    None

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["perimeter"])
    >>> print(SD.perimeter)
    8.0
    """
    if self.area == 0:
        self.perimeter = 0.
    else:
        if self.contours is None:
            self.get_contours()
        if len(self.contours) == 0:
            self.perimeter = 0.
        else:
            self.perimeter = cv2.arcLength(self.contours, True)

get_rectangularity()

Compute and store rectangularity: area / bounding-rectangle-area.

Notes

Uses self.binary_image and self.min_bounding_rectangle. Computes the MBR if needed.

Returns:

Type Description
None

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["rectangularity"])
>>> print(SD.rectangularity)
2.25
Source code in src/cellects/image/shape_descriptors.py
def get_rectangularity(self):
    """
    Compute and store rectangularity: area / bounding-rectangle-area.

    Notes
    -----
    Uses `self.binary_image` and `self.min_bounding_rectangle`. Computes the MBR if needed.

    Returns
    -------
    None

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["rectangularity"])
    >>> print(SD.rectangularity)
    2.25
    """
    if self.area == 0:
        self.rectangularity = 0.
    else:
        if self.min_bounding_rectangle is None:
            self.get_min_bounding_rectangle()
        bounding_rectangle_area = self.min_bounding_rectangle[1][0] * self.min_bounding_rectangle[1][1]
        if bounding_rectangle_area == 0:
            self.rectangularity = 0.
        else:
            self.rectangularity = self.binary_image.sum() / bounding_rectangle_area

get_skewness()

Calculate and store skewness along x and y (skx, sky).

This function computes the skewness about the x-axis and y-axis of an image. Skewness is a measure of the asymmetry of the probability distribution of values in an image.

Notes

Requires standard deviations; values are stored in self.skx and self.sky.

Returns:

Type Description
None

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["skewness_x", "skewness_y"])
>>> print(SD.skx, SD.sky)
0.0 0.0
Source code in src/cellects/image/shape_descriptors.py
def get_skewness(self):
    """
    Calculate and store skewness along x and y (skx, sky).

    This function computes the skewness about the x-axis and y-axis of
    an image. Skewness is a measure of the asymmetry of the probability
    distribution of values in an image.

    Notes
    -----
    Requires standard deviations; values are stored in `self.skx` and `self.sky`.

    Returns
    -------
    None

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["skewness_x", "skewness_y"])
    >>> print(SD.skx, SD.sky)
    0.0 0.0
    """
    if self.skx is None:
        if self.sx is None:
            self.get_standard_deviations()

        self.skx, self.sky = get_skewness(self.mo, self.binary_image, self.cx, self.cy, self.sx, self.sy)

get_solidity()

Compute and store solidity: contour area / convex hull area.

Extended Summary

The solidity is a dimensionless measure that compares the area of a shape to its convex hull. A solidity of 1 means the contour is fully convex, while a value less than 1 indicates concavities.

Notes

If the convex hull area is 0 or absent, solidity is set to 0.

Returns:

Type Description
None

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["solidity"])
>>> print(SD.solidity)
1.0
Source code in src/cellects/image/shape_descriptors.py
def get_solidity(self):
    """
    Compute and store solidity: contour area / convex hull area.

    Extended Summary
    ----------------
    The solidity is a dimensionless measure that compares the area of a shape to
    its convex hull. A solidity of 1 means the contour is fully convex, while a
    value less than 1 indicates concavities.

    Notes
    -----
    If the convex hull area is 0 or absent, solidity is set to 0.

    Returns
    -------
    None

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["solidity"])
    >>> print(SD.solidity)
    1.0
    """
    if self.area == 0:
        self.solidity = 0.
    else:
        if self.convex_hull is None:
            self.get_convex_hull()
        if len(self.convex_hull) == 0:
            self.solidity = 0.
        else:
            hull_area = cv2.contourArea(self.convex_hull)
            if hull_area == 0:
                self.solidity = 0.
            else:
                self.solidity = cv2.contourArea(self.contours) / hull_area

get_standard_deviations()

Calculate and store standard deviations along x and y (sx, sy).

Notes

Requires centroid and moments; values are stored in self.sx and self.sy.

Returns

None

Examples

SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["standard_deviation_x", "standard_deviation_y"]) print(SD.sx, SD.sy) 0.816496580927726 0.816496580927726

Source code in src/cellects/image/shape_descriptors.py
def get_standard_deviations(self):
    """
    Calculate and store standard deviations along x and y (sx, sy).

    Notes
    -----
    Requires centroid and moments; values are stored in `self.sx` and `self.sy`.

    Returns
    -------
    None

    Examples
    --------
   >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["standard_deviation_x", "standard_deviation_y"])
    >>> print(SD.sx, SD.sy)
    0.816496580927726 0.816496580927726
    """
    if self.sx is None:
        if self.axes_orientation is None:
            self.get_inertia_axes()
        self.sx, self.sy = get_standard_deviations(self.mo, self.binary_image, self.cx, self.cy)

get_total_hole_area()

Calculate the total area of holes in a binary image.

This function uses connected component labeling to detect and measure the area of holes in a binary image.

Returns:

Type Description
float

The total area of all detected holes in the binary image.

Notes

This function assumes that the binary image has been pre-processed and that holes are represented as connected components of zero pixels within the foreground

Examples:

>>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["total_hole_area"])
>>> print(SD.total_hole_area)
0
Source code in src/cellects/image/shape_descriptors.py
def get_total_hole_area(self):
    """
    Calculate the total area of holes in a binary image.

    This function uses connected component labeling to detect and
    measure the area of holes in a binary image.

    Returns
    -------
    float
        The total area of all detected holes in the binary image.

    Notes
    -----
    This function assumes that the binary image has been pre-processed
    and that holes are represented as connected components of zero
    pixels within the foreground

    Examples
    --------
    >>> SD = ShapeDescriptors(np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8), ["total_hole_area"])
    >>> print(SD.total_hole_area)
    0
    """
    nb, new_order = cv2.connectedComponents(1 - self.binary_image)
    if nb > 2:
        self.total_hole_area = (new_order > 1).sum()
    else:
        self.total_hole_area = 0.

compute_one_descriptor_per_frame(binary_vid, arena_label, timings, descriptors_dict, output_in_mm, pixel_size, do_fading, save_coord_specimen)

Computes descriptors for each frame in a binary video and returns them as a DataFrame.

Parameters:

Name Type Description Default
binary_vid NDArray[uint8]

The binary video data where each frame is a 2D array.

required
arena_label int

Label for the arena in the video.

required
timings NDArray

Array of timestamps corresponding to each frame.

required
descriptors_dict dict

Dictionary containing the descriptors to be computed.

required
output_in_mm bool

Flag indicating if output should be in millimeters. Default is False.

required
pixel_size float

Size of a pixel in the video when output_in_mm is True. Default is None.

required
do_fading bool

Flag indicating if the fading effect should be applied. Default is False.

required
save_coord_specimen bool

Flag indicating if the coordinates of specimens should be saved. Default is False.

required

Returns:

Type Description
DataFrame

DataFrame containing the descriptors for each frame in the video.

Notes

For large inputs, consider pre-allocating memory for efficiency. The save_coord_specimen flag will save coordinate data to a file.

Examples:

>>> binary_vid = np.ones((10, 640, 480), dtype=np.uint8)
>>> timings = np.arange(10)
>>> descriptors_dict = {'area': True, 'perimeter': True}
>>> result = compute_one_descriptor_per_frame(binary_vid, 1, timings, descriptors_dict)
>>> print(result.head())
   arena  time  area  perimeter
0      1     0     0          0
1      1     1     0          0
2      1     2     0          0
3      1     3     0          0
4      1     4     0          0
>>> binary_vid = np.ones((5, 640, 480), dtype=np.uint8)
>>> timings = np.arange(5)
>>> descriptors_dict = {'area': True, 'perimeter': True}
>>> result = compute_one_descriptor_per_frame(binary_vid, 2, timings,
...                                            descriptors_dict,
...                                            output_in_mm=True,
...                                            pixel_size=0.1)
>>> print(result.head())
   arena  time  area  perimeter
0      2     0    0         0.0
1      2     1    0         0.0
2      2     2    0         0.0
3      2     3    0         0.0
4      2     4    0         0.0
Source code in src/cellects/image/shape_descriptors.py
def compute_one_descriptor_per_frame(binary_vid: NDArray[np.uint8], arena_label: int, timings: NDArray,
                                     descriptors_dict: dict, output_in_mm: bool, pixel_size: float,
                                     do_fading: bool, save_coord_specimen:bool):
    """
    Computes descriptors for each frame in a binary video and returns them as a DataFrame.

    Parameters
    ----------
    binary_vid : NDArray[np.uint8]
        The binary video data where each frame is a 2D array.
    arena_label : int
        Label for the arena in the video.
    timings : NDArray
        Array of timestamps corresponding to each frame.
    descriptors_dict : dict
        Dictionary containing the descriptors to be computed.
    output_in_mm : bool, optional
        Flag indicating if output should be in millimeters. Default is False.
    pixel_size : float, optional
        Size of a pixel in the video when `output_in_mm` is True. Default is None.
    do_fading : bool, optional
        Flag indicating if the fading effect should be applied. Default is False.
    save_coord_specimen : bool, optional
        Flag indicating if the coordinates of specimens should be saved. Default is False.

    Returns
    -------
    pandas.DataFrame
        DataFrame containing the descriptors for each frame in the video.

    Notes
    -----
    For large inputs, consider pre-allocating memory for efficiency.
    The `save_coord_specimen` flag will save coordinate data to a file.

    Examples
    --------
    >>> binary_vid = np.ones((10, 640, 480), dtype=np.uint8)
    >>> timings = np.arange(10)
    >>> descriptors_dict = {'area': True, 'perimeter': True}
    >>> result = compute_one_descriptor_per_frame(binary_vid, 1, timings, descriptors_dict)
    >>> print(result.head())
       arena  time  area  perimeter
    0      1     0     0          0
    1      1     1     0          0
    2      1     2     0          0
    3      1     3     0          0
    4      1     4     0          0

    >>> binary_vid = np.ones((5, 640, 480), dtype=np.uint8)
    >>> timings = np.arange(5)
    >>> descriptors_dict = {'area': True, 'perimeter': True}
    >>> result = compute_one_descriptor_per_frame(binary_vid, 2, timings,
    ...                                            descriptors_dict,
    ...                                            output_in_mm=True,
    ...                                            pixel_size=0.1)
    >>> print(result.head())
       arena  time  area  perimeter
    0      2     0    0         0.0
    1      2     1    0         0.0
    2      2     2    0         0.0
    3      2     3    0         0.0
    4      2     4    0         0.0
    """
    dims = binary_vid.shape
    all_descriptors, to_compute_from_sd, length_measures, area_measures = initialize_descriptor_computation(descriptors_dict)
    one_row_per_frame = pd.DataFrame(np.zeros((dims[0], 2 + len(all_descriptors))),
                                          columns=['arena', 'time'] + all_descriptors)
    one_row_per_frame['arena'] = [arena_label] * dims[0]
    one_row_per_frame['time'] = timings
    for t in np.arange(dims[0]):
        SD = ShapeDescriptors(binary_vid[t, :, :], to_compute_from_sd)
        for descriptor in to_compute_from_sd:
            one_row_per_frame.loc[t, descriptor] = SD.descriptors[descriptor]
    if save_coord_specimen:
        write_h5(f"coord_specimen{arena_label}_t{dims[0]}_y{dims[1]}_x{dims[2]}.h5",
                smallest_memory_array(np.nonzero(binary_vid), "uint"))
    # Adjust descriptors scale if output_in_mm is specified
    if do_fading:
        one_row_per_frame['newly_explored_area'] = get_newly_explored_area(binary_vid)
    if output_in_mm:
        one_row_per_frame = scale_descriptors(one_row_per_frame, pixel_size,
                                                   length_measures, area_measures)
    return one_row_per_frame

initialize_descriptor_computation(descriptors_dict)

Initialize descriptor computation based on available and requested descriptors.

Parameters:

Name Type Description Default
descriptors_dict dict

A dictionary where keys are descriptor names and values are booleans indicating whether to compute the corresponding descriptor.

required

Returns:

Type Description
tuple

A tuple containing four lists: - all_descriptors: List of all requested descriptor names. - to_compute_from_sd: Array of descriptor names that need to be computed from the shape descriptors class. - length_measures: Array of descriptor names that are length measures and need to be computed. - area_measures: Array of descriptor names that are area measures and need to be computed.

Examples:

>>> descriptors_dict = {'perimeter': True, 'area': False}
>>> all_descriptors, to_compute_from_sd, length_measures, area_measures = initialize_descriptor_computation(descriptors_dict)
>>> print(all_descriptors, to_compute_from_sd, length_measures, area_measures)
['length'] ['length'] ['length'] []
Source code in src/cellects/image/shape_descriptors.py
def initialize_descriptor_computation(descriptors_dict: dict) -> Tuple[list, list, list, list]:
    """

    Initialize descriptor computation based on available and requested descriptors.

    Parameters
    ----------
    descriptors_dict : dict
        A dictionary where keys are descriptor names and values are booleans indicating whether
        to compute the corresponding descriptor.

    Returns
    -------
    tuple
        A tuple containing four lists:
        - all_descriptors: List of all requested descriptor names.
        - to_compute_from_sd: Array of descriptor names that need to be computed from the shape descriptors class.
        - length_measures: Array of descriptor names that are length measures and need to be computed.
        - area_measures: Array of descriptor names that are area measures and need to be computed.

    Examples
    --------
    >>> descriptors_dict = {'perimeter': True, 'area': False}
    >>> all_descriptors, to_compute_from_sd, length_measures, area_measures = initialize_descriptor_computation(descriptors_dict)
    >>> print(all_descriptors, to_compute_from_sd, length_measures, area_measures)
    ['length'] ['length'] ['length'] []

    """
    available_descriptors_in_sd = list(from_shape_descriptors_class.keys())
    all_descriptors = []
    to_compute_from_sd = []
    for name, do_compute in descriptors_dict.items():
        if do_compute:
            all_descriptors.append(name)
            if np.isin(name, available_descriptors_in_sd):
                to_compute_from_sd.append(name)
    to_compute_from_sd = np.array(to_compute_from_sd)
    length_measures = to_compute_from_sd[np.isin(to_compute_from_sd, length_descriptors)]
    area_measures = to_compute_from_sd[np.isin(to_compute_from_sd, area_descriptors)]

    return all_descriptors, to_compute_from_sd, length_measures, area_measures

scale_descriptors(descriptors_dict, pixel_size, length_measures=None, area_measures=None)

Scale the spatial descriptors in a dictionary based on pixel size.

Parameters:

Name Type Description Default
descriptors_dict

Dictionary containing spatial descriptors.

required
pixel_size float

Pixel size used for scaling.

required
length_measures ndarray

Array of descriptors that represent lengths. If not provided, they will be initialized.

None
area_measures ndarray

Array of descriptors that represent areas. If not provided, they will be initialized.

None

Returns:

Type Description
dict

Dictionary with scaled spatial descriptors.

Examples:

>>> from numpy import array as ndarray
>>> descriptors_dict = {'length': ndarray([1, 2]), 'area': ndarray([3, 4])}
>>> pixel_size = 0.5
>>> scaled_dict = scale_descriptors(descriptors_dict, pixel_size)
>>> print(scaled_dict)
{'length': array([0.5, 1.]), 'area': array([1.58421369, 2.])}
Source code in src/cellects/image/shape_descriptors.py
def scale_descriptors(descriptors_dict, pixel_size: float, length_measures: NDArray[str]=None, area_measures: NDArray[str]=None):
    """
    Scale the spatial descriptors in a dictionary based on pixel size.

    Parameters
    ----------
    descriptors_dict
        Dictionary containing spatial descriptors.
    pixel_size : float
        Pixel size used for scaling.
    length_measures : numpy.ndarray, optional
        Array of descriptors that represent lengths. If not provided,
        they will be initialized.
    area_measures : numpy.ndarray, optional
        Array of descriptors that represent areas. If not provided,
        they will be initialized.

    Returns
    -------
    dict
        Dictionary with scaled spatial descriptors.

    Examples
    --------
    >>> from numpy import array as ndarray
    >>> descriptors_dict = {'length': ndarray([1, 2]), 'area': ndarray([3, 4])}
    >>> pixel_size = 0.5
    >>> scaled_dict = scale_descriptors(descriptors_dict, pixel_size)
    >>> print(scaled_dict)
    {'length': array([0.5, 1.]), 'area': array([1.58421369, 2.])}
    """
    if length_measures is None or area_measures is None:
        to_compute_from_sd = np.array(list(descriptors_dict.keys()))
        length_measures = to_compute_from_sd[np.isin(to_compute_from_sd, length_descriptors)]
        area_measures = to_compute_from_sd[np.isin(to_compute_from_sd, area_descriptors)]
    for descr in length_measures:
        descriptors_dict[descr] *= pixel_size
    for descr in area_measures:
        descriptors_dict[descr] *= np.sqrt(pixel_size)
    return descriptors_dict