dito.processing

This submodule provides functionality for basic image processing.

   1"""
   2This submodule provides functionality for basic image processing.
   3"""
   4
   5import itertools
   6import math
   7import operator
   8
   9import cv2
  10import numpy as np
  11
  12import dito.core
  13import dito.visual
  14
  15
  16#
  17# basic processing
  18#
  19
  20
  21def argmin(image):
  22    """
  23    Compute the coordinates of the minimum value in the image.
  24
  25    Parameters
  26    ----------
  27    image : numpy.ndarray
  28        The input image.
  29
  30    Returns
  31    -------
  32    tuple
  33        The coordinates of the minimum value in the image.
  34
  35    Notes
  36    -----
  37    The order of the indices is equivalent to the order of the image axes.
  38    This may differ from common conventions that use (x, y) coordinates.
  39    """
  40    return np.unravel_index(np.argmin(image), image.shape)
  41
  42
  43def argmax(image):
  44    """
  45    Compute the coordinates of the maximum value in the image.
  46
  47    Parameters
  48    ----------
  49    image : numpy.ndarray
  50        The input image.
  51
  52    Returns
  53    -------
  54    tuple
  55        The coordinates of the maximum value in the image.
  56
  57    Notes
  58    -----
  59    The order of the indices is equivalent to the order of the image axes.
  60    This may differ from common conventions that use (x, y) coordinates.
  61    """
  62    return np.unravel_index(np.argmax(image), image.shape)
  63
  64
  65def nms_iter(image, peak_radius):
  66    """
  67    Iterate through peaks in the image using non-maximum suppression (NMS).
  68
  69    The function yields peaks by repeatedly finding the maximum value in the image, suppressing its
  70    neighborhood within the given peak radius, and repeating the process. The iteration stops when no
  71    more positive values remain in the image.
  72
  73    Parameters
  74    ----------
  75    image : numpy.ndarray
  76        The input grayscale image.
  77    peak_radius : int
  78        The radius around each peak to suppress in subsequent iterations.
  79
  80    Yields
  81    ------
  82    dict
  83        A dictionary containing the peak index, coordinates, and value:
  84        - 'n_peak' : int, the index of the current peak.
  85        - 'peak_xy' : tuple, the (x, y) coordinates of the peak.
  86        - 'peak_value' : float, the value of the peak.
  87    """
  88
  89    # peak radius must be a non-negative int
  90    if not (isinstance(peak_radius, int) and (peak_radius >= 0)):
  91        raise ValueError(f"Argument 'peak_radius' must be a non-negative integer (but is: {peak_radius})")
  92
  93    # only allow images of shape (Y, X) or (Y, X, 1)
  94    if not dito.is_gray(image):
  95        raise ValueError(f"Image must be grayscale (shapes (Y, X) or (Y, X, 1)), but has shape {image.shape}")
  96
  97    # remove the last singleton dimension if necessary
  98    image_work = image.copy()
  99    if len(image_work.shape) == 3:
 100        image_work = image_work[:, :, 0]
 101
 102    for n_peak in itertools.count():
 103        # extract max
 104        (peak_y, peak_x) = argmax(image_work)
 105        peak_value = image_work[peak_y, peak_x]
 106
 107        # stop if there are no positive values left (otherwise, we might hit an infinite loop)
 108        if peak_value <= 0:
 109            return
 110
 111        # suppress neighborhood
 112        image_work[
 113            max(0, peak_y - peak_radius):min(image_work.shape[0], peak_y + peak_radius + 1),
 114            max(0, peak_x - peak_radius):min(image_work.shape[1], peak_x + peak_radius + 1),
 115        ] = 0
 116
 117        yield {
 118            "n_peak": n_peak,
 119            "peak_xy": (peak_x, peak_y),
 120            "peak_value": peak_value,
 121        }
 122
 123
 124def nms(image, peak_radius, max_peak_count=1000, rel_max_value=0.1):
 125    """
 126    Perform non-maximum suppression (NMS) to extract peaks from the image.
 127
 128    The function finds peaks in the image, suppressing neighboring values around each peak and iterating
 129    until the specified maximum peak count or relative peak value threshold is reached.
 130
 131    Parameters
 132    ----------
 133    image : numpy.ndarray
 134        The input grayscale image.
 135    peak_radius : int
 136        The radius around each peak to suppress in subsequent iterations.
 137    max_peak_count : int, optional
 138        The maximum number of peaks to extract (default is 1000).
 139    rel_max_value : float, optional
 140        The relative peak value threshold. The extraction process stops when the peak value falls below
 141        this proportion of the maximum peak value (default is 0.1).
 142
 143    Returns
 144    -------
 145    list of dict
 146        A list of dictionaries, where each dictionary contains information about a detected peak:
 147        - 'n_peak' : int, the index of the peak.
 148        - 'peak_xy' : tuple, the (x, y) coordinates of the peak.
 149        - 'peak_value' : the value of the peak.
 150    """
 151
 152    # check argument 'max_peak_count'
 153    if not (isinstance(max_peak_count, int) and (max_peak_count >= 1)):
 154        raise ValueError(f"Argument 'max_peak_count' must be an integer >= 1, but is: {max_peak_count}")
 155
 156    # check argument 'rel_max_value'
 157    if not (isinstance(rel_max_value, float) and (0.0 <= rel_max_value <= 1.0)):
 158        raise ValueError(f"Argument 'rel_max_value' must be a float between 0.0 and 1.0 (both inclusive), but is: {rel_max_value}")
 159
 160    peaks = []
 161    max_value = None
 162    for peak in nms_iter(image=image, peak_radius=peak_radius):
 163        if (peak["n_peak"] + 1) >= max_peak_count:
 164            # stop if max peak count was reached
 165            break
 166        if max_value is None:
 167            # use first peak's value as reference for all other values (when comparing)
 168            max_value = peak["peak_value"]
 169        else:
 170            # stop if peak value is too small
 171            if (peak["peak_value"] / max_value) < rel_max_value:
 172                break
 173        peaks.append(peak)
 174
 175    return peaks
 176
 177
 178def clipped_diff(image1, image2, scale=None, offset=None, apply_abs=False):
 179    """
 180    Compute the clipped difference between two images.
 181
 182    The `image1` and `image2` inputs must have the same dtype. The function computes the element-wise difference
 183    between `image1` and `image2`, and then applies an optional offset and scale factor to the difference values.
 184    The resulting values are clipped to the original dtype range, to prevent overflow or underflow.
 185
 186    Parameters
 187    ----------
 188    image1 : numpy.ndarray
 189        The first input image (minuend).
 190    image2 : numpy.ndarray
 191        The second input image (subtrahend).
 192    scale : float, optional
 193        The scale factor to apply to the difference values. If specified, the difference values are multiplied by
 194        `scale` before any offset is applied. The default value is `None`, which means no scaling is applied.
 195    offset : float, optional
 196        The offset value to add to the difference values. If specified, the difference values are increased by
 197        `offset` after any scaling is applied. The default value is `None`, which means no offset is applied.
 198    apply_abs : bool, optional
 199        If `True`, the absolute value of the difference image is computed after a scaling and/or offset is applied.
 200        The default value is `False`.
 201
 202    Returns
 203    -------
 204    numpy.ndarray
 205        The clipped difference image, with the same shape and dtype as the input images.
 206    """
 207
 208    # assert equal dtypes
 209    if image1.dtype != image2.dtype:
 210        raise ValueError("Both images must have the same dtypes (but have '{}' and '{}')".format(image1.dtype, image2.dtype))
 211    dtype = image1.dtype
 212    dtype_range = dito.core.dtype_range(dtype=dtype)
 213
 214    # raw diff
 215    diff = image1.astype(np.float32) - image2.astype(np.float32)
 216
 217    # apply offset, scale, and abs if specified
 218    if scale is not None:
 219        diff *= scale
 220    if offset is not None:
 221        diff += offset
 222    if apply_abs:
 223        diff = np.abs(diff)
 224
 225    # clip values outside of original range
 226    diff = dito.clip(image=diff, lower=dtype_range[0], upper=dtype_range[1])
 227
 228    return diff.astype(dtype)
 229
 230
 231def abs_diff(image1, image2):
 232    """
 233    Compute the absolute difference between two images.
 234
 235    The `image1` and `image2` inputs must have the same dtype. The function computes the element-wise absolute
 236    difference between `image1` and `image2`, and then clips the resulting values to the original dtype range,
 237    to prevent overflow or underflow (which might happen for signed integer dtypes).
 238
 239    Parameters
 240    ----------
 241    image1 : numpy.ndarray
 242        The first input image (minuend).
 243    image2 : numpy.ndarray
 244        The second input image (subtrahend).
 245
 246    Returns
 247    -------
 248    numpy.ndarray
 249        The absolute difference image, with the same shape and dtype as the input images.
 250    """
 251    return clipped_diff(image1=image1, image2=image2, scale=None, offset=None, apply_abs=True)
 252
 253
 254def shifted_diff(image1, image2):
 255    """
 256    Compute the shifted difference between two images.
 257
 258    The `image1` and `image2` inputs must have the same dtype. The function computes the element-wise difference
 259    between `image1` and `image2`, and then applies a scale and offset to the difference values to shift the result
 260    back into the original dtype range such that there is no need for clipping
 261
 262    Parameters
 263    ----------
 264    image1 : numpy.ndarray
 265        The first input image (minuend).
 266    image2 : numpy.ndarray
 267        The second input image (subtrahend).
 268
 269    Returns
 270    -------
 271    numpy.ndarray
 272        The shifted difference image, with the same shape and dtype as the input images.
 273    """
 274    dtype_range = dito.core.dtype_range(dtype=image1.dtype)
 275    return clipped_diff(image1=image1, image2=image2, scale=0.5, offset=0.5 * (dtype_range[0] + dtype_range[1]), apply_abs=False)
 276
 277
 278def gaussian_blur(image, sigma):
 279    """
 280    Apply Gaussian blur to an image.
 281
 282    The filter kernel size is adapted to `sigma` automatically by OpenCV's
 283    `cv2.GaussianBlur`.
 284
 285    Parameters
 286    ----------
 287    image : numpy.ndarray
 288        Input image to be blurred.
 289    sigma : float
 290        Standard deviation for Gaussian kernel. Must be greater than 0.0.
 291
 292    Returns
 293    -------
 294    numpy.ndarray
 295        Blurred image, with the same shape and dtype as the input image.
 296    """
 297    if sigma <= 0.0:
 298        return image
 299    return cv2.GaussianBlur(src=image, ksize=None, sigmaX=sigma)
 300
 301
 302def median_blur(image, kernel_size):
 303    """
 304    Apply a median filter to an image.
 305
 306    Parameters
 307    ----------
 308    image : numpy.ndarray
 309        Input image to be filtered.
 310    kernel_size : int
 311        Size of the median filter kernel. Must be a positive odd integer.
 312
 313    Returns
 314    -------
 315    numpy.ndarray
 316        Filtered image, with the same shape and dtype as the input image.
 317    """
 318    return cv2.medianBlur(src=image, ksize=kernel_size)
 319
 320
 321def clahe(image, clip_limit=None, tile_grid_size=None):
 322    """
 323    Apply Contrast Limited Adaptive Histogram Equalization (CLAHE) to an image.
 324
 325    Parameters
 326    ----------
 327    image : numpy.ndarray
 328        Input image to be equalized.
 329    clip_limit : float, optional
 330        Threshold for contrast limiting. If `None`, no clipping is performed.
 331        Default is `None`.
 332    tile_grid_size : tuple(int, int), optional
 333        Number of rows and columns into which the image will be divided.
 334        Default is `(8, 8)` (as for OpenCV).
 335
 336    Returns
 337    -------
 338    numpy.ndarray
 339        Equalized image, with the same shape and dtype as the input image.
 340    """
 341    clahe_op = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)
 342    return clahe_op.apply(image)
 343
 344
 345#
 346# thresholding
 347#
 348
 349
 350def otsu(image):
 351    """
 352    Perform Otsu thresholding on a grayscale image.
 353
 354    This function computes the optimal threshold for the input grayscale image using the Otsu method,
 355    and returns the thresholded image and the computed threshold.
 356
 357    Parameters
 358    ----------
 359    image : numpy.ndarray
 360        Input grayscale image to be thresholded.
 361
 362    Returns
 363    -------
 364    tuple
 365        A tuple containing the threshold value and the thresholded image, both as numpy.ndarray
 366        with the same shape and dtype as the input image.
 367
 368    Raises
 369    ------
 370    ValueError
 371        If the input image is not grayscale.
 372    """
 373    if dito.core.is_color(image=image):
 374        raise ValueError("Expected gray image but got color image for Otsu thresholding")
 375    (theta, image2) = cv2.threshold(src=image, thresh=-1, maxval=255, type=cv2.THRESH_BINARY | cv2.THRESH_OTSU)
 376    return (theta, image2)
 377
 378
 379def otsu_theta(image):
 380    """
 381    Compute the Otsu threshold for a grayscale image.
 382
 383    This function computes the optimal threshold for the input grayscale image using the Otsu method,
 384    and returns only the threshold value.
 385
 386    Parameters
 387    ----------
 388    image : numpy.ndarray
 389        Input grayscale image.
 390
 391    Returns
 392    -------
 393    float
 394        The computed threshold value.
 395    """
 396    (theta, image2) = otsu(image=image)
 397    return theta
 398
 399
 400def otsu_image(image):
 401    """
 402    Threshold a grayscale image using the Otsu method.
 403
 404    This function computes the optimal threshold for the input grayscale image using the Otsu method,
 405    and returns the thresholded image.
 406
 407    Parameters
 408    ----------
 409    image : numpy.ndarray
 410        Input grayscale image to be thresholded.
 411
 412    Returns
 413    -------
 414    numpy.ndarray
 415        The thresholded image, as a numpy.ndarray with the same shape and dtype as the input image.
 416    """
 417    (theta, image2) = otsu(image=image)
 418    return image2
 419
 420
 421#
 422# morphological operations
 423#
 424
 425
 426def morpho_op_kernel(shape, size):
 427    """
 428    Create a morphological operation kernel.
 429
 430    Parameters
 431    ----------
 432    shape : int
 433        Type of the kernel. The following values are supported:
 434        - `cv2.MORPH_RECT`: rectangular kernel
 435        - `cv2.MORPH_CROSS`: cross-shaped kernel
 436        - `cv2.MORPH_ELLIPSE`: elliptical kernel
 437    size : int or tuple of int
 438        Size (width, height) of the kernel. If a single integer is provided, assume equal width and height.
 439
 440    Returns
 441    -------
 442    numpy.ndarray
 443        The morphological operation kernel.
 444    """
 445    ksize = dito.utils.get_validated_tuple(x=size, type_=int, count=2)
 446    kernel = cv2.getStructuringElement(shape=shape, ksize=ksize, anchor=(-1, -1))
 447    return kernel
 448
 449
 450def morpho_op(image, operation, shape=cv2.MORPH_ELLIPSE, size=3, anchor=(-1, -1), iterations=1):
 451    """
 452    Apply a morphological operation to an image.
 453
 454    Parameters
 455    ----------
 456    image : numpy.ndarray
 457        Input image to which the morphological operation is applied.
 458    operation : int
 459        Type of morphological operation. The following values are supported:
 460        - `cv2.MORPH_ERODE`: erosion
 461        - `cv2.MORPH_DILATE`: dilation
 462        - `cv2.MORPH_OPEN`: opening
 463        - `cv2.MORPH_CLOSE`: closing
 464        - `cv2.MORPH_GRADIENT`: gradient
 465        - `cv2.MORPH_TOPHAT`: top hat
 466        - `cv2.MORPH_BLACKHAT`: black hat
 467    shape : int
 468        Type of the kernel. The following values are supported:
 469        - `cv2.MORPH_RECT`: rectangular kernel
 470        - `cv2.MORPH_CROSS`: cross-shaped kernel
 471        - `cv2.MORPH_ELLIPSE`: elliptical kernel
 472        The default value is `cv2.MORPH_ELLIPSE`.
 473    size : int or tuple of int
 474        Size (width, height) of the kernel. If a single integer is provided, assume equal width and height.
 475        The default value is `3`.
 476    anchor : tuple of int, optional
 477        Anchor point of the structuring element used for the morphological operation. The anchor should lie within
 478        the kernel. The default value is `(-1, -1)`, which means that the anchor is at the center of the kernel.
 479    iterations : int, optional
 480        Number of times the morphological operation is applied. The default value is `1`.
 481
 482    Returns
 483    -------
 484    numpy.ndarray
 485        Resulting image after applying the morphological operation, with the same shape and dtype as the input image.
 486    """
 487    kernel = morpho_op_kernel(shape=shape, size=size)
 488    return cv2.morphologyEx(src=image, op=operation, kernel=kernel, anchor=anchor, iterations=iterations)
 489
 490
 491def dilate(image, **kwargs):
 492    """
 493    Apply morphological dilation to an image.
 494
 495    Parameters
 496    ----------
 497    image : numpy.ndarray
 498        Input image to be dilated.
 499    **kwargs
 500        Optional arguments to be passed to `morpho_op`.
 501
 502    Returns
 503    -------
 504    numpy.ndarray
 505        Dilated image, with the same shape and dtype as the input image.
 506    """
 507    return morpho_op(image=image, operation=cv2.MORPH_DILATE, **kwargs)
 508
 509
 510def erode(image, **kwargs):
 511    """
 512    Apply morphological erosion to an image.
 513
 514    Parameters
 515    ----------
 516    image : numpy.ndarray
 517        Input image to be eroded.
 518    **kwargs
 519        Optional arguments to be passed to `morpho_op`.
 520
 521    Returns
 522    -------
 523    numpy.ndarray
 524        Eroded image, with the same shape and dtype as the input image.
 525    """
 526    return morpho_op(image=image, operation=cv2.MORPH_ERODE, **kwargs)
 527
 528
 529def morpho_open(image, **kwargs):
 530    """
 531    Apply morphological opening to an image.
 532
 533    Parameters
 534    ----------
 535    image : numpy.ndarray
 536        Input image to be opened.
 537    **kwargs
 538        Optional arguments to be passed to `morpho_op`.
 539
 540    Returns
 541    -------
 542    numpy.ndarray
 543        Image after the opening operation, with the same shape and dtype as the input image.
 544    """
 545    return morpho_op(image=image, operation=cv2.MORPH_OPEN, **kwargs)
 546
 547
 548def morpho_close(image, **kwargs):
 549    """
 550    Apply morphological closing to an image.
 551
 552    Parameters
 553    ----------
 554    image : numpy.ndarray
 555        Input image to be closed.
 556    **kwargs
 557        Optional arguments to be passed to `morpho_op`.
 558
 559    Returns
 560    -------
 561    numpy.ndarray
 562        Image after the closing operation, with the same shape and dtype as the input image.
 563    """
 564    return morpho_op(image=image, operation=cv2.MORPH_CLOSE, **kwargs)
 565
 566
 567def blackhat(image, **kwargs):
 568    """
 569    Apply the morphological blackhat operation to an image.
 570
 571    Parameters
 572    ----------
 573    image : numpy.ndarray
 574        Input image.
 575    **kwargs
 576        Optional arguments to be passed to `morpho_op`.
 577
 578    Returns
 579    -------
 580    numpy.ndarray
 581        Image after the blackhat operation, with the same shape and dtype as the input image.
 582    """
 583    return morpho_op(image=image, operation=cv2.MORPH_BLACKHAT, **kwargs)
 584
 585
 586def tophat(image, **kwargs):
 587    """
 588    Apply the morphological tophat operation to an image.
 589
 590    Parameters
 591    ----------
 592    image : numpy.ndarray
 593        Input image.
 594    **kwargs
 595        Optional arguments to be passed to `morpho_op`.
 596
 597    Returns
 598    -------
 599    numpy.ndarray
 600        Image after the tophat operation, with the same shape and dtype as the input image.
 601    """
 602    return morpho_op(image=image, operation=cv2.MORPH_TOPHAT, **kwargs)
 603
 604
 605#
 606# filters
 607#
 608
 609
 610def dog(image, sigma1, sigma2, return_raw=False, colormap=None):
 611    """
 612    Apply the difference of Gaussians (DoG) operation to an image.
 613
 614    Parameters
 615    ----------
 616    image : numpy.ndarray
 617        Input image to which the DoG is applied.
 618    sigma1 : float
 619        Standard deviation of the first Gaussian filter.
 620    sigma2 : float
 621        Standard deviation of the second Gaussian filter.
 622    return_raw : bool, optional
 623        If True, return the raw difference image. Otherwise, the difference image is shifted and scaled to match the
 624        original dtype range. The default value is False.
 625    colormap : str, optional
 626        Name of the colormap to use for colorizing the result. If None, no colorization is applied. The default value
 627        is None.
 628
 629    Returns
 630    -------
 631    numpy.ndarray
 632        Resulting image after applying the DoG, with the same shape and dtype as the input image.
 633    """
 634    blur1 = gaussian_blur(image=image, sigma=sigma1).astype(np.float32)
 635    blur2 = gaussian_blur(image=image, sigma=sigma2).astype(np.float32)
 636    diff = blur1 - blur2
 637    if return_raw:
 638        return diff
 639    else:
 640        diff_11 = diff / dito.core.dtype_range(dtype=image.dtype)[1]
 641        diff_01 = (diff_11 + 1.0) * 0.5
 642        result = dito.convert(image=diff_01, dtype=image.dtype)
 643        if colormap is not None:
 644            result = dito.visual.colorize(image=result, colormap=colormap)
 645        return result
 646
 647
 648def dog_interactive(image, colormap=None):
 649    """
 650    Display an interactive window for exploring the difference of Gaussians (DoG) of an image.
 651
 652    The window displays the input image, two sliders for adjusting the standard deviations of the Gaussian filters used
 653    in the DoG, and a visualization of the DoG result.
 654
 655    Parameters
 656    ----------
 657    image : numpy.ndarray
 658        Input image to which the DoG is applied.
 659    colormap : str, optional
 660        Name of the colormap to use for colorizing the result. If None, no colorization is applied. The default value
 661        is None.
 662
 663    Returns
 664    -------
 665    None
 666    """
 667    window_name = "dito.dog_interactive"
 668    sliders = [dito.highgui.FloatSlider(window_name=window_name, name="sigma{}".format(n_slider + 1), min_value=0.0, max_value=15.0, value_count=1001) for n_slider in range(2)]
 669    sliders[0].set_value(0.5)
 670    sliders[1].set_value(0.8)
 671
 672    image_show = None
 673    while True:
 674        if (image_show is None) or any(slider.changed for slider in sliders):
 675            sigmas = [sliders[n_slider].get_value() for n_slider in range(2)]
 676            images_blur = [gaussian_blur(image=image, sigma=sigmas[n_slider]) for n_slider in range(2)]
 677            images_blur = [dito.visual.text(image=image_blur, message="sigma{} = {:.2f}".format(n_slider + 1, sigmas[n_slider])) for (n_slider, image_blur) in enumerate(images_blur)]
 678            image_dog = dog(image, sigma1=sigmas[0], sigma2=sigmas[1], return_raw=False, colormap=colormap)
 679            image_show = dito.stack([[image, image_dog], images_blur])
 680        key = dito.show(image=image_show, window_name=window_name, wait=10)
 681        if key in dito.qkeys():
 682            return
 683
 684
 685#
 686# contours
 687#
 688
 689
 690class Contour():
 691    """
 692    A class to represent a contour.
 693
 694    Attributes
 695    ----------
 696    points : numpy.ndarray
 697        The points that make up the contour. A 2D numpy array of shape `(n, 2)`, where `n` is the number of points
 698        in the contour. Each row contains the `(x, y)` coordinates of a point.
 699    """
 700
 701    def __init__(self, points):
 702        """
 703        Parameters
 704        ----------
 705        points : numpy.ndarray
 706            The points defining the contour. A 2D numpy array of shape `(n, 2)`, where `n` is the number of points
 707            in the contour. Each row contains the `(x, y)` coordinates of a point.
 708        """
 709        self.points = points
 710
 711    def __len__(self):
 712        """
 713        Return the number of points in the contour.
 714
 715        Returns
 716        -------
 717        int
 718            The number of points in the contour.
 719        """
 720        return len(self.points)
 721
 722    def __eq__(self, other):
 723        """
 724        Check if two contours are equal.
 725
 726        Parameters
 727        ----------
 728        other : Contour
 729            Another instance of the `Contour` class.
 730
 731        Returns
 732        -------
 733        bool
 734            True if the two contours are equal, False otherwise.
 735        """
 736        if not isinstance(other, Contour):
 737            raise TypeError("Argument 'other' must be a contour")
 738
 739        if len(self) != len(other):
 740            return False
 741
 742        return np.array_equal(self.points, other.points)
 743
 744    def copy(self):
 745        """
 746        Return a copy of the current instance of the `Contour` class.
 747
 748        Returns
 749        -------
 750        Contour
 751            A copy of the current instance of the `Contour` class.
 752        """
 753        return Contour(points=self.points.copy())
 754
 755    def get_center(self):
 756        """
 757        Return the center point `(x, y)` of the contour.
 758
 759        Returns
 760        -------
 761        numpy.ndarray
 762            A 2D numpy array representing the `(x, y)` coordinates of the center point of the contour.
 763        """
 764        return np.mean(self.points, axis=0)
 765
 766    def get_center_x(self):
 767        """
 768        Return the x-coordinate of the center point of the contour.
 769
 770        Returns
 771        -------
 772        float
 773            The x-coordinate of the center point of the contour.
 774        """
 775        return np.mean(self.points[:, 0])
 776
 777    def get_center_y(self):
 778        """
 779        Return the y-coordinate of the center point of the contour.
 780
 781        Returns
 782        -------
 783        float
 784            The y-coordinate of the center point of the contour.
 785        """
 786        return np.mean(self.points[:, 1])
 787
 788    def get_min_x(self):
 789        """
 790        Return the minimum x-coordinate of the contour.
 791
 792        Returns
 793        -------
 794        float
 795            The minimum x-coordinate of the contour.
 796        """
 797        return np.min(self.points[:, 0])
 798
 799    def get_max_x(self):
 800        """
 801        Return the maximum x-coordinate of the contour.
 802
 803        Returns
 804        -------
 805        float
 806            The maximum x-coordinate of the contour.
 807        """
 808        return np.max(self.points[:, 0])
 809
 810    def get_width(self):
 811        """
 812        Return the width of the contour.
 813
 814        Returns
 815        -------
 816        float
 817            The width of the contour.
 818        """
 819        return self.get_max_x() - self.get_min_x()
 820
 821    def get_min_y(self):
 822        """
 823        Return the minimum y-coordinate of the contour.
 824
 825        Returns
 826        -------
 827        float
 828            The minimum y-coordinate of the contour.
 829        """
 830        return np.min(self.points[:, 1])
 831
 832    def get_max_y(self):
 833        """
 834        Return the maximum y-coordinate of the contour.
 835
 836        Returns
 837        -------
 838        float
 839            The maximum y-coordinate of the contour.
 840        """
 841        return np.max(self.points[:, 1])
 842
 843    def get_height(self):
 844        """
 845        Return the height of the contour.
 846
 847        Returns
 848        -------
 849        float
 850            The height of the contour.
 851        """
 852        return self.get_max_y() - self.get_min_y()
 853
 854    def get_area(self, mode="draw"):
 855        """
 856        Compute the area of the contour.
 857
 858        Parameters
 859        ----------
 860        mode : {"draw", "calc"}, optional
 861            The method to use for computing the area. If "draw", the area is computed by drawing the contour as a filled
 862            white shape on a black image and counting the number of white pixels. If "calc", the area is computed using the
 863            cv2.contourArea() function. Default is "draw".
 864
 865        Returns
 866        -------
 867        float
 868            The area of the contour.
 869        """
 870        if mode == "draw":
 871            image = self.draw_standalone(color=(1,), thickness=1, filled=True, antialias=False, border=2)
 872            return np.sum(image)
 873
 874        elif mode == "calc":
 875            return cv2.contourArea(contour=self.points)
 876
 877        else:
 878            raise ValueError("Invalid value for argument 'mode': '{}'".format(mode))
 879
 880    def get_perimeter(self):
 881        """
 882        Compute the perimeter of the contour.
 883
 884        Returns
 885        -------
 886        float
 887            The perimeter of the contour.
 888        """
 889        return cv2.arcLength(curve=self.points, closed=True)
 890
 891    def get_circularity(self):
 892        """
 893        Compute the circularity of the contour.
 894
 895        Returns
 896        -------
 897        float
 898            The circularity of the contour.
 899        """
 900        r_area = np.sqrt(self.get_area() / np.pi)
 901        r_perimeter = self.get_perimeter() / (2.0 * np.pi)
 902        return r_area / r_perimeter
 903
 904    def get_ellipse(self):
 905        """
 906        Fit an ellipse to the contour.
 907
 908        Returns
 909        -------
 910        tuple
 911            A tuple `(center, size, angle)`, where `center` is a tuple `(x, y)` representing the center point
 912            of the ellipse, `size` is a tuple `(width, height)` representing the size of the ellipse, and `angle`
 913            is the angle (in degrees) between the major axis of the ellipse and the x-axis.
 914        """
 915        return cv2.fitEllipse(points=self.points)
 916
 917    def get_eccentricity(self):
 918        """
 919        Calculate the eccentricity of the contour.
 920
 921        Returns
 922        -------
 923        float
 924            The eccentricity of the contour.
 925        """
 926        ellipse = self.get_ellipse()
 927        (width, height) = ellipse[1]
 928        semi_major_axis = max(width, height) * 0.5
 929        semi_minor_axis = min(width, height) * 0.5
 930        eccentricity = math.sqrt(1.0 - (semi_minor_axis / semi_major_axis)**2)
 931        return eccentricity
 932
 933    def get_moments(self):
 934        """
 935        Calculate the moments of the contour.
 936
 937        The moment values can be used to calculate various properties of the contour, such as its centroid,
 938        orientation, and size.
 939
 940        Returns
 941        -------
 942        dict
 943            A dictionary of moment values for the contour.
 944        """
 945        return cv2.moments(array=self.points, binaryImage=False)
 946
 947    def get_hu_moments(self, log=True):
 948        """
 949        Calculate the seven Hu moments for the contour.
 950
 951        These moments are invariant shape descriptors that can be used to capture shape properties of a contour, and
 952        the first six of them are invariant to translation, scale, and rotation. However, the seventh moment is not
 953        invariant to reflection and changes its sign under reflection.
 954
 955        Parameters
 956        ----------
 957        log : bool, optional
 958            If True (default), the logarithm of the absolute value of the Hu moments will be returned. If False, the raw
 959            Hu moments will be returned.
 960
 961        Returns
 962        -------
 963        numpy.ndarray
 964            A 1D numpy array containing the seven Hu moments for the contour. If `log=True`, the values will be the
 965            logarithm of the absolute value of the Hu moments.
 966        """
 967        hu_moments = cv2.HuMoments(m=self.get_moments())
 968        if log:
 969            return np.sign(hu_moments) * np.log10(np.abs(hu_moments))
 970        else:
 971            return hu_moments
 972
 973    def shift(self, offset_x=None, offset_y=None):
 974        """
 975        Shift the contour by a given offset along the x and/or y axis.
 976
 977        Parameters
 978        ----------
 979        offset_x : float, optional
 980            The amount by which to shift the contour along the x-axis. If not specified, the contour is not shifted along the x-axis.
 981        offset_y : float, optional
 982            The amount by which to shift the contour along the y-axis. If not specified, the contour is not shifted along the y-axis.
 983
 984        Returns
 985        -------
 986        None
 987        """
 988        if offset_x is not None:
 989            self.points[:, 0] += offset_x
 990        if offset_y is not None:
 991            self.points[:, 1] += offset_y
 992
 993    def draw(self, image, color, thickness=1, filled=True, antialias=False, offset=None):
 994        """
 995        Draw the contour into an existing image.
 996
 997        The image is changed in-place.
 998
 999        Parameters
1000        ----------
1001        image : numpy.ndarray
1002            The image into which the contour will be drawn.
1003        color : tuple
1004            The color of the contour. Its length must equal the channel count of the image.
1005        thickness : int, optional
1006            The thickness of the contour lines. Has no effect if `filled` is True. Default is 1.
1007        filled : bool, optional
1008            Whether the contour should be filled. If True, the interior of the contour is filled with the `color` value. Default is True.
1009        antialias : bool, optional
1010            Whether antialiasing should be applied when drawing the contour. Default is False.
1011        offset : tuple or list or numpy.ndarray, optional
1012            The `(x, y)` coordinates of the offset of the contour from the origin of the image. Default is None, which corresponds to no offset.
1013
1014        Returns
1015        -------
1016        None
1017        """
1018        cv2.drawContours(image=image, contours=[np.round(self.points).astype(np.int32)], contourIdx=0, color=color, thickness=cv2.FILLED if filled else thickness, lineType=cv2.LINE_AA if antialias else cv2.LINE_8, offset=offset)
1019
1020    def draw_standalone(self, color, thickness=1, filled=True, antialias=False, border=0):
1021        """
1022        Draw the contour as a standalone image.
1023
1024        The image has the same size as the contour (which thus is centered in the image),
1025        but an additional border can be specified.
1026
1027        Parameters
1028        ----------
1029        color : tuple or list or numpy.ndarray
1030            The color of the contour. Currently, only grayscale mode is supported.
1031        thickness : int, optional
1032            The thickness of the contour lines. Has no effect if `filled` is True. Default is 1.
1033        filled : bool, optional
1034            Whether the contour should be filled. If True, the interior of the contour is filled with the `color` value. Default is True.
1035        antialias : bool, optional
1036            Whether antialiasing should be applied when drawing the contour. Default is False.
1037        border : int, optional
1038            The size of the border around the contour. Default is 0.
1039
1040        Returns
1041        -------
1042        numpy.ndarray
1043            A 2D numpy array representing the image of the contour.
1044        """
1045        image = np.zeros(shape=(2 * border + self.get_height(), 2 * border + self.get_width()), dtype=np.uint8)
1046        self.draw(image=image, color=color, thickness=thickness, filled=filled, antialias=antialias, offset=(border - self.get_min_x(), border - self.get_min_y()))
1047        return image
1048
1049
1050class ContourList():
1051    """
1052    A class representing a list of contours.
1053
1054    It allows for easy filtering of contours by various properties and offers
1055    additional helper functions such as drawing.
1056
1057    Attributes
1058    ----------
1059    contours : list of Contour objects
1060        The list of contours stored in the ContourList object.
1061    """
1062
1063    def __init__(self, contours_):
1064        """
1065        Parameters
1066        ----------
1067        contours_ : list of Contour objects
1068            The list of contours to be stored.
1069        """
1070        self.contours = contours_
1071
1072    def __len__(self):
1073        """
1074        Return the number of contours.
1075
1076        Returns
1077        -------
1078        int
1079            The number of contours stored in the ContourList object.
1080        """
1081        return len(self.contours)
1082
1083    def __eq__(self, other):
1084        """
1085        Check if two ContourList objects are equal.
1086
1087        Parameters
1088        ----------
1089        other : object
1090            The object to compare to.
1091
1092        Returns
1093        -------
1094        bool
1095            True if the two objects are equal, False otherwise.
1096
1097        Raises
1098        ------
1099        TypeError
1100            If the argument `other` is not a ContourList object.
1101        """
1102        if not isinstance(other, ContourList):
1103            raise TypeError("Argument 'other' must be a contour list")
1104
1105        if len(self) != len(other):
1106            return False
1107
1108        for (contour_self, contour_other) in zip(self.contours, other.contours):
1109            if contour_self != contour_other:
1110                return False
1111
1112        return True
1113
1114    def __getitem__(self, key):
1115        """
1116        Return the contour object at the specified index.
1117
1118        Parameters
1119        ----------
1120        key : int
1121            The index of the contour object to be returned.
1122
1123        Returns
1124        -------
1125        object
1126            The contour object at the specified index.
1127        """
1128        return self.contours[key]
1129
1130    def copy(self):
1131        """
1132        Return a copy of the ContourList object.
1133
1134        Returns
1135        -------
1136        object
1137            A copy of the ContourList object.
1138        """
1139        contours_copy = [contour.copy() for contour in self.contours]
1140        return ContourList(contours_=contours_copy)
1141
1142    def filter(self, func, min_value=None, max_value=None):
1143        """
1144        Filter the contour list based on a given function and range of values.
1145
1146        Only contours whose function values fall within the specified range are retained.
1147        The contour list is modified in place.
1148
1149        Parameters
1150        ----------
1151        func : function
1152            The function used to extract a value from each contour. It must return a value which can be compared against
1153            `min_value` and/or `max_value`.
1154        min_value : float or None, optional
1155            The minimum value of the extracted value for a contour to be kept. Contours with extracted values lower than
1156            this will be removed. If None, no minimum filter is applied. Default is None.
1157        max_value : float or None, optional
1158            The maximum value of the extracted value for a contour to be kept. Contours with extracted values higher than
1159            this will be removed. If None, no maximum filter is applied. Default is None.
1160
1161        Returns
1162        -------
1163        None
1164        """
1165        if (min_value is None) and (max_value is None):
1166            # nothing to do
1167            return
1168
1169        # filter
1170        contours_filtered = []
1171        for contour in self.contours:
1172            value = func(contour)
1173            if (min_value is not None) and (value < min_value):
1174                continue
1175            if (max_value is not None) and (value > max_value):
1176                continue
1177            contours_filtered.append(contour)
1178        self.contours = contours_filtered
1179
1180    def filter_center_x(self, min_value=None, max_value=None):
1181        """
1182        Filter the list of contours by the x-coordinate of their centers.
1183
1184        Only contours whose center x-coordinates fall within the specified range are retained.
1185        The contour list is modified in place.
1186
1187        Parameters
1188        ----------
1189        min_value : float or None, optional
1190            The minimum allowed value of the x-coordinate. If a contour's center x-coordinate is less than this, it is
1191            discarded. If None, no lower bound is applied. Default is None.
1192        max_value : float or None, optional
1193            The maximum allowed value of the x-coordinate. If a contour's center x-coordinate is greater than this, it is
1194            discarded. If None, no upper bound is applied. Default is None.
1195
1196        Returns
1197        -------
1198        None
1199        """
1200        self.filter(func=operator.methodcaller("get_center_x"), min_value=min_value, max_value=max_value)
1201
1202    def filter_center_y(self, min_value=None, max_value=None):
1203        """
1204        Filter the list of contours by the y-coordinate of their centers.
1205
1206        Only contours whose center y-coordinates fall within the specified range are retained.
1207        The contour list is modified in place.
1208
1209        Parameters
1210        ----------
1211        min_value : float or None, optional
1212            The minimum allowed value of the y-coordinate. If a contour's center y-coordinate is less than this, it is
1213            discarded. If None, no lower bound is applied. Default is None.
1214        max_value : float or None, optional
1215            The maximum allowed value of the y-coordinate. If a contour's center y-coordinate is greater than this, it is
1216            discarded. If None, no upper bound is applied. Default is None.
1217
1218        Returns
1219        -------
1220        None
1221        """
1222        self.filter(func=operator.methodcaller("get_center_y"), min_value=min_value, max_value=max_value)
1223
1224    def filter_area(self, min_value=None, max_value=None, mode="draw"):
1225        """
1226        Filter the list of contours by their area.
1227
1228        Only contours whose areas fall within the specified range are retained.
1229        The contour list is modified in place.
1230
1231        Parameters
1232        ----------
1233        min_value : float or None, optional
1234            The minimum allowed area. If a contour's area is less than this, it is discarded. If None, no lower bound
1235            is applied. Default is None.
1236        max_value : float or None, optional
1237            The maximum allowed area. If a contour's area is greater than this, it is discarded. If None, no upper bound
1238            is applied. Default is None.
1239        mode : str, optional
1240            The mode to use for computing the area. See `get_area` for details. Default is 'draw'.
1241
1242        Returns
1243        -------
1244        None
1245        """
1246        self.filter(func=operator.methodcaller("get_area", mode=mode), min_value=min_value, max_value=max_value)
1247
1248    def filter_perimeter(self, min_value=None, max_value=None):
1249        """
1250        Filter the list of contours by their perimeters.
1251
1252        Only contours whose perimeters fall within the specified range are retained.
1253        The contour list is modified in place.
1254
1255        Parameters
1256        ----------
1257        min_value : float or None, optional
1258            The minimum allowed perimeter. If a contour's perimeter is less than this, it is discarded.
1259            If None, no lower bound is applied. Default is None.
1260        max_value : float or None, optional
1261            The maximum allowed perimeter. If a contour's perimeter is greater than this, it is discarded.
1262            If None, no upper bound is applied. Default is None.
1263
1264        Returns
1265        -------
1266        None
1267        """
1268        self.filter(func=operator.methodcaller("get_perimeter"), min_value=min_value, max_value=max_value)
1269
1270    def filter_circularity(self, min_value=None, max_value=None):
1271        """
1272        Filter the list of contours by their circularity.
1273
1274        Only contours whose circularities fall within the specified range are retained.
1275        The contour list is modified in place.
1276
1277        Parameters
1278        ----------
1279        min_value : float or None, optional
1280            The minimum allowed circularity. If a contour's circularity is less than this, it is discarded.
1281            If None, no lower bound is applied. Default is None.
1282        max_value : float or None, optional
1283            The maximum allowed circularity. If a contour's circularity is greater than this, it is discarded.
1284            If None, no upper bound is applied. Default is None.
1285
1286        Returns
1287        -------
1288        None
1289        """
1290        self.filter(func=operator.methodcaller("get_circularity"), min_value=min_value, max_value=max_value)
1291
1292    def find_largest(self, return_index=True):
1293        """
1294        Return the contour (or the contour index) with the largest area.
1295
1296        Parameters
1297        ----------
1298        return_index : bool, optional
1299            If `True`, return the index of the largest contour. Otherwise, return the contour object itself.
1300            Default is `True`.
1301
1302        Returns
1303        -------
1304        int or Contour or None
1305            The index of the largest contour if `return_index` is `True`, or the largest contour object if
1306            `return_index` is `False`. If no contour is found, returns None.
1307        """
1308        max_area = None
1309        argmax_area = None
1310        for (n_contour, contour) in enumerate(self.contours):
1311            area = contour.get_area()
1312            if (max_area is None) or (area > max_area):
1313                max_area = area
1314                argmax_area = n_contour
1315
1316        if argmax_area is None:
1317            return None
1318        else:
1319            if return_index:
1320                return argmax_area
1321            else:
1322                return self.contours[argmax_area]
1323
1324    def draw_all(self, image, colors=None, **kwargs):
1325        """
1326        Draw all the contours into an image using a different color for each contour.
1327
1328        Parameters
1329        ----------
1330        image : numpy.ndarray
1331            The image into which the contours will be drawn.
1332        colors : list of tuples, optional
1333            The colors to use for each contour. The length of the list must be equal to the number of contours.
1334            If not provided, random colors will be generated. Default is None.
1335        **kwargs
1336            Additional keyword arguments to pass to the `Contour.draw()` method.
1337
1338        Returns
1339        -------
1340        None
1341        """
1342        if colors is None:
1343            colors = tuple(dito.random_color() for _ in range(len(self)))
1344
1345        for (contour, color) in zip(self.contours, colors):
1346            contour.draw(image=image, color=color, **kwargs)
1347
1348
1349class ContourFinder(ContourList):
1350    """
1351    Extension of `ContourList` with support to find contours in an image.
1352
1353    Between OpenCV 3.x and 4.x, the API of `cv2.findContours` changed. This
1354    class is basically a wrapper for `cv2.findContours` which works for both
1355    versions.
1356
1357    As a subclass of `ContourList`, it inherits all the contour list
1358    manipulation methods defined by its parent class.
1359
1360    Attributes
1361    ----------
1362    image : numpy.ndarray
1363        The input image, stored as a copy of the input argument.
1364    contours : list of Contour
1365        The contours found in the input image.
1366    """
1367
1368    def __init__(self, image):
1369        """
1370        Initialize a `ContourFinder` instance and find contours in the given `image`.
1371
1372        Parameters
1373        ----------
1374        image : numpy.ndarray
1375            The image from which to find contours. Will be converted to dtype `numpy.uint8`
1376        """
1377        self.image = image.copy()
1378        if self.image.dtype == bool:
1379            self.image = dito.core.convert(image=self.image, dtype=np.uint8)
1380        contours_ = self.find_contours(image=self.image)
1381        super().__init__(contours_=contours_)
1382
1383    @staticmethod
1384    def find_contours(image):
1385        """
1386        Find the contours in the given `image`.
1387
1388        Parameters
1389        ----------
1390        image : numpy.ndarray
1391            The image from which to find contours.
1392
1393        Returns
1394        -------
1395        list of Contour
1396            A list of instances of the `Contour` class, one for each contour found in the input image.
1397        """
1398
1399        # find raw contours
1400        result = cv2.findContours(image=image, mode=cv2.RETR_LIST, method=cv2.CHAIN_APPROX_NONE)
1401
1402        # compatible with OpenCV 3.x and 4.x, see https://stackoverflow.com/a/53909713/1913780
1403        contours_raw = result[-2]
1404
1405        # return tuple of instances of class `Contour`
1406        return [Contour(points=contour_raw[:, 0, :]) for contour_raw in contours_raw]
1407
1408
1409def contours(image):
1410    """
1411    Find and return all contours in the given image.
1412
1413    It is a convenience wrapper for `ContourFinder`.
1414
1415    Parameters
1416    ----------
1417    image : numpy.ndarray
1418        The image in which to find the contours.
1419
1420    Returns
1421    -------
1422    ContourList
1423        An instance of the `ContourList` class containing all the found contours.
1424    """
1425    contour_finder = ContourFinder(image=image)
1426    return contour_finder.contours
1427
1428
1429class VoronoiPartition(ContourList):
1430    """
1431    Extension of `ContourList` where the contours are derived as facets of the Voronoi partition of a set of given points.
1432
1433    As a subclass of `ContourList`, it inherits all the contour list
1434    manipulation methods defined by its parent class.
1435
1436    Attributes
1437    ----------
1438    contours : list of Contour
1439        The list of Voronoi facets, each represented as a `Contour` object.
1440    """
1441
1442    def __init__(self, image_size, points):
1443        """
1444        Initialize a `VoronoiPartition` instance from a set of input points.
1445
1446        Parameters
1447        ----------
1448        image_size : tuple of int
1449            The size of the image (width, height) in pixels.
1450        points : numpy.ndarray
1451            The array of input points for which to compute the Voronoi partition. Each row represents a point (x, y).
1452        """
1453        contours_ = self.get_facets(image_size=image_size, points=points)
1454        super().__init__(contours_=contours_)
1455
1456    @staticmethod
1457    def get_facets(image_size, points):
1458        """
1459        Calculate the Voronoi partition based on the given points.
1460
1461        Parameters
1462        ----------
1463        image_size : tuple of int
1464            The size of the image (width, height) in pixels.
1465        points : numpy.ndarray
1466            The array of input points for which to compute the Voronoi partition. Each row represents a point (x, y).
1467
1468        Returns
1469        -------
1470        list of Contour
1471            The list of Voronoi facets, each represented as a `Contour` object.
1472        """
1473        subdiv = cv2.Subdiv2D((0, 0, image_size[0], image_size[1]))
1474        for point in points:
1475            subdiv.insert(pt=point)
1476        (voronoi_facets, voronoi_centers) = subdiv.getVoronoiFacetList(idx=[])
1477        return [Contour(voronoi_facet) for voronoi_facet in voronoi_facets]
1478
1479
1480def voronoi(image_size, points):
1481    """
1482    Compute the Voronoi partition of a set of input points.
1483
1484    It is a convenience wrapper for `VoronoiPartition`.
1485
1486    Parameters
1487    ----------
1488    image_size : tuple of int
1489        The size of the image (width, height) in pixels.
1490    points : numpy.ndarray
1491        The array of input points. Each row represents a point (x, y).
1492
1493    Returns
1494    -------
1495    list of Contour
1496        The list of Voronoi facets, each represented as a `Contour` object.
1497    """
1498    voronoi_partition = VoronoiPartition(image_size=image_size, points=points)
1499    return voronoi_partition.contours
def argmin(image):
22def argmin(image):
23    """
24    Compute the coordinates of the minimum value in the image.
25
26    Parameters
27    ----------
28    image : numpy.ndarray
29        The input image.
30
31    Returns
32    -------
33    tuple
34        The coordinates of the minimum value in the image.
35
36    Notes
37    -----
38    The order of the indices is equivalent to the order of the image axes.
39    This may differ from common conventions that use (x, y) coordinates.
40    """
41    return np.unravel_index(np.argmin(image), image.shape)

Compute the coordinates of the minimum value in the image.

Parameters
  • image (numpy.ndarray): The input image.
Returns
  • tuple: The coordinates of the minimum value in the image.
Notes

The order of the indices is equivalent to the order of the image axes. This may differ from common conventions that use (x, y) coordinates.

def argmax(image):
44def argmax(image):
45    """
46    Compute the coordinates of the maximum value in the image.
47
48    Parameters
49    ----------
50    image : numpy.ndarray
51        The input image.
52
53    Returns
54    -------
55    tuple
56        The coordinates of the maximum value in the image.
57
58    Notes
59    -----
60    The order of the indices is equivalent to the order of the image axes.
61    This may differ from common conventions that use (x, y) coordinates.
62    """
63    return np.unravel_index(np.argmax(image), image.shape)

Compute the coordinates of the maximum value in the image.

Parameters
  • image (numpy.ndarray): The input image.
Returns
  • tuple: The coordinates of the maximum value in the image.
Notes

The order of the indices is equivalent to the order of the image axes. This may differ from common conventions that use (x, y) coordinates.

def nms_iter(image, peak_radius):
 66def nms_iter(image, peak_radius):
 67    """
 68    Iterate through peaks in the image using non-maximum suppression (NMS).
 69
 70    The function yields peaks by repeatedly finding the maximum value in the image, suppressing its
 71    neighborhood within the given peak radius, and repeating the process. The iteration stops when no
 72    more positive values remain in the image.
 73
 74    Parameters
 75    ----------
 76    image : numpy.ndarray
 77        The input grayscale image.
 78    peak_radius : int
 79        The radius around each peak to suppress in subsequent iterations.
 80
 81    Yields
 82    ------
 83    dict
 84        A dictionary containing the peak index, coordinates, and value:
 85        - 'n_peak' : int, the index of the current peak.
 86        - 'peak_xy' : tuple, the (x, y) coordinates of the peak.
 87        - 'peak_value' : float, the value of the peak.
 88    """
 89
 90    # peak radius must be a non-negative int
 91    if not (isinstance(peak_radius, int) and (peak_radius >= 0)):
 92        raise ValueError(f"Argument 'peak_radius' must be a non-negative integer (but is: {peak_radius})")
 93
 94    # only allow images of shape (Y, X) or (Y, X, 1)
 95    if not dito.is_gray(image):
 96        raise ValueError(f"Image must be grayscale (shapes (Y, X) or (Y, X, 1)), but has shape {image.shape}")
 97
 98    # remove the last singleton dimension if necessary
 99    image_work = image.copy()
100    if len(image_work.shape) == 3:
101        image_work = image_work[:, :, 0]
102
103    for n_peak in itertools.count():
104        # extract max
105        (peak_y, peak_x) = argmax(image_work)
106        peak_value = image_work[peak_y, peak_x]
107
108        # stop if there are no positive values left (otherwise, we might hit an infinite loop)
109        if peak_value <= 0:
110            return
111
112        # suppress neighborhood
113        image_work[
114            max(0, peak_y - peak_radius):min(image_work.shape[0], peak_y + peak_radius + 1),
115            max(0, peak_x - peak_radius):min(image_work.shape[1], peak_x + peak_radius + 1),
116        ] = 0
117
118        yield {
119            "n_peak": n_peak,
120            "peak_xy": (peak_x, peak_y),
121            "peak_value": peak_value,
122        }

Iterate through peaks in the image using non-maximum suppression (NMS).

The function yields peaks by repeatedly finding the maximum value in the image, suppressing its neighborhood within the given peak radius, and repeating the process. The iteration stops when no more positive values remain in the image.

Parameters
  • image (numpy.ndarray): The input grayscale image.
  • peak_radius (int): The radius around each peak to suppress in subsequent iterations.
Yields
  • dict: A dictionary containing the peak index, coordinates, and value:
    • 'n_peak' : int, the index of the current peak.
    • 'peak_xy' : tuple, the (x, y) coordinates of the peak.
    • 'peak_value' : float, the value of the peak.
def nms(image, peak_radius, max_peak_count=1000, rel_max_value=0.1):
125def nms(image, peak_radius, max_peak_count=1000, rel_max_value=0.1):
126    """
127    Perform non-maximum suppression (NMS) to extract peaks from the image.
128
129    The function finds peaks in the image, suppressing neighboring values around each peak and iterating
130    until the specified maximum peak count or relative peak value threshold is reached.
131
132    Parameters
133    ----------
134    image : numpy.ndarray
135        The input grayscale image.
136    peak_radius : int
137        The radius around each peak to suppress in subsequent iterations.
138    max_peak_count : int, optional
139        The maximum number of peaks to extract (default is 1000).
140    rel_max_value : float, optional
141        The relative peak value threshold. The extraction process stops when the peak value falls below
142        this proportion of the maximum peak value (default is 0.1).
143
144    Returns
145    -------
146    list of dict
147        A list of dictionaries, where each dictionary contains information about a detected peak:
148        - 'n_peak' : int, the index of the peak.
149        - 'peak_xy' : tuple, the (x, y) coordinates of the peak.
150        - 'peak_value' : the value of the peak.
151    """
152
153    # check argument 'max_peak_count'
154    if not (isinstance(max_peak_count, int) and (max_peak_count >= 1)):
155        raise ValueError(f"Argument 'max_peak_count' must be an integer >= 1, but is: {max_peak_count}")
156
157    # check argument 'rel_max_value'
158    if not (isinstance(rel_max_value, float) and (0.0 <= rel_max_value <= 1.0)):
159        raise ValueError(f"Argument 'rel_max_value' must be a float between 0.0 and 1.0 (both inclusive), but is: {rel_max_value}")
160
161    peaks = []
162    max_value = None
163    for peak in nms_iter(image=image, peak_radius=peak_radius):
164        if (peak["n_peak"] + 1) >= max_peak_count:
165            # stop if max peak count was reached
166            break
167        if max_value is None:
168            # use first peak's value as reference for all other values (when comparing)
169            max_value = peak["peak_value"]
170        else:
171            # stop if peak value is too small
172            if (peak["peak_value"] / max_value) < rel_max_value:
173                break
174        peaks.append(peak)
175
176    return peaks

Perform non-maximum suppression (NMS) to extract peaks from the image.

The function finds peaks in the image, suppressing neighboring values around each peak and iterating until the specified maximum peak count or relative peak value threshold is reached.

Parameters
  • image (numpy.ndarray): The input grayscale image.
  • peak_radius (int): The radius around each peak to suppress in subsequent iterations.
  • max_peak_count (int, optional): The maximum number of peaks to extract (default is 1000).
  • rel_max_value (float, optional): The relative peak value threshold. The extraction process stops when the peak value falls below this proportion of the maximum peak value (default is 0.1).
Returns
  • list of dict: A list of dictionaries, where each dictionary contains information about a detected peak:
    • 'n_peak' : int, the index of the peak.
    • 'peak_xy' : tuple, the (x, y) coordinates of the peak.
    • 'peak_value' : the value of the peak.
def clipped_diff(image1, image2, scale=None, offset=None, apply_abs=False):
179def clipped_diff(image1, image2, scale=None, offset=None, apply_abs=False):
180    """
181    Compute the clipped difference between two images.
182
183    The `image1` and `image2` inputs must have the same dtype. The function computes the element-wise difference
184    between `image1` and `image2`, and then applies an optional offset and scale factor to the difference values.
185    The resulting values are clipped to the original dtype range, to prevent overflow or underflow.
186
187    Parameters
188    ----------
189    image1 : numpy.ndarray
190        The first input image (minuend).
191    image2 : numpy.ndarray
192        The second input image (subtrahend).
193    scale : float, optional
194        The scale factor to apply to the difference values. If specified, the difference values are multiplied by
195        `scale` before any offset is applied. The default value is `None`, which means no scaling is applied.
196    offset : float, optional
197        The offset value to add to the difference values. If specified, the difference values are increased by
198        `offset` after any scaling is applied. The default value is `None`, which means no offset is applied.
199    apply_abs : bool, optional
200        If `True`, the absolute value of the difference image is computed after a scaling and/or offset is applied.
201        The default value is `False`.
202
203    Returns
204    -------
205    numpy.ndarray
206        The clipped difference image, with the same shape and dtype as the input images.
207    """
208
209    # assert equal dtypes
210    if image1.dtype != image2.dtype:
211        raise ValueError("Both images must have the same dtypes (but have '{}' and '{}')".format(image1.dtype, image2.dtype))
212    dtype = image1.dtype
213    dtype_range = dito.core.dtype_range(dtype=dtype)
214
215    # raw diff
216    diff = image1.astype(np.float32) - image2.astype(np.float32)
217
218    # apply offset, scale, and abs if specified
219    if scale is not None:
220        diff *= scale
221    if offset is not None:
222        diff += offset
223    if apply_abs:
224        diff = np.abs(diff)
225
226    # clip values outside of original range
227    diff = dito.clip(image=diff, lower=dtype_range[0], upper=dtype_range[1])
228
229    return diff.astype(dtype)

Compute the clipped difference between two images.

The image1 and image2 inputs must have the same dtype. The function computes the element-wise difference between image1 and image2, and then applies an optional offset and scale factor to the difference values. The resulting values are clipped to the original dtype range, to prevent overflow or underflow.

Parameters
  • image1 (numpy.ndarray): The first input image (minuend).
  • image2 (numpy.ndarray): The second input image (subtrahend).
  • scale (float, optional): The scale factor to apply to the difference values. If specified, the difference values are multiplied by scale before any offset is applied. The default value is None, which means no scaling is applied.
  • offset (float, optional): The offset value to add to the difference values. If specified, the difference values are increased by offset after any scaling is applied. The default value is None, which means no offset is applied.
  • apply_abs (bool, optional): If True, the absolute value of the difference image is computed after a scaling and/or offset is applied. The default value is False.
Returns
  • numpy.ndarray: The clipped difference image, with the same shape and dtype as the input images.
def abs_diff(image1, image2):
232def abs_diff(image1, image2):
233    """
234    Compute the absolute difference between two images.
235
236    The `image1` and `image2` inputs must have the same dtype. The function computes the element-wise absolute
237    difference between `image1` and `image2`, and then clips the resulting values to the original dtype range,
238    to prevent overflow or underflow (which might happen for signed integer dtypes).
239
240    Parameters
241    ----------
242    image1 : numpy.ndarray
243        The first input image (minuend).
244    image2 : numpy.ndarray
245        The second input image (subtrahend).
246
247    Returns
248    -------
249    numpy.ndarray
250        The absolute difference image, with the same shape and dtype as the input images.
251    """
252    return clipped_diff(image1=image1, image2=image2, scale=None, offset=None, apply_abs=True)

Compute the absolute difference between two images.

The image1 and image2 inputs must have the same dtype. The function computes the element-wise absolute difference between image1 and image2, and then clips the resulting values to the original dtype range, to prevent overflow or underflow (which might happen for signed integer dtypes).

Parameters
  • image1 (numpy.ndarray): The first input image (minuend).
  • image2 (numpy.ndarray): The second input image (subtrahend).
Returns
  • numpy.ndarray: The absolute difference image, with the same shape and dtype as the input images.
def shifted_diff(image1, image2):
255def shifted_diff(image1, image2):
256    """
257    Compute the shifted difference between two images.
258
259    The `image1` and `image2` inputs must have the same dtype. The function computes the element-wise difference
260    between `image1` and `image2`, and then applies a scale and offset to the difference values to shift the result
261    back into the original dtype range such that there is no need for clipping
262
263    Parameters
264    ----------
265    image1 : numpy.ndarray
266        The first input image (minuend).
267    image2 : numpy.ndarray
268        The second input image (subtrahend).
269
270    Returns
271    -------
272    numpy.ndarray
273        The shifted difference image, with the same shape and dtype as the input images.
274    """
275    dtype_range = dito.core.dtype_range(dtype=image1.dtype)
276    return clipped_diff(image1=image1, image2=image2, scale=0.5, offset=0.5 * (dtype_range[0] + dtype_range[1]), apply_abs=False)

Compute the shifted difference between two images.

The image1 and image2 inputs must have the same dtype. The function computes the element-wise difference between image1 and image2, and then applies a scale and offset to the difference values to shift the result back into the original dtype range such that there is no need for clipping

Parameters
  • image1 (numpy.ndarray): The first input image (minuend).
  • image2 (numpy.ndarray): The second input image (subtrahend).
Returns
  • numpy.ndarray: The shifted difference image, with the same shape and dtype as the input images.
def gaussian_blur(image, sigma):
279def gaussian_blur(image, sigma):
280    """
281    Apply Gaussian blur to an image.
282
283    The filter kernel size is adapted to `sigma` automatically by OpenCV's
284    `cv2.GaussianBlur`.
285
286    Parameters
287    ----------
288    image : numpy.ndarray
289        Input image to be blurred.
290    sigma : float
291        Standard deviation for Gaussian kernel. Must be greater than 0.0.
292
293    Returns
294    -------
295    numpy.ndarray
296        Blurred image, with the same shape and dtype as the input image.
297    """
298    if sigma <= 0.0:
299        return image
300    return cv2.GaussianBlur(src=image, ksize=None, sigmaX=sigma)

Apply Gaussian blur to an image.

The filter kernel size is adapted to sigma automatically by OpenCV's cv2.GaussianBlur.

Parameters
  • image (numpy.ndarray): Input image to be blurred.
  • sigma (float): Standard deviation for Gaussian kernel. Must be greater than 0.0.
Returns
  • numpy.ndarray: Blurred image, with the same shape and dtype as the input image.
def median_blur(image, kernel_size):
303def median_blur(image, kernel_size):
304    """
305    Apply a median filter to an image.
306
307    Parameters
308    ----------
309    image : numpy.ndarray
310        Input image to be filtered.
311    kernel_size : int
312        Size of the median filter kernel. Must be a positive odd integer.
313
314    Returns
315    -------
316    numpy.ndarray
317        Filtered image, with the same shape and dtype as the input image.
318    """
319    return cv2.medianBlur(src=image, ksize=kernel_size)

Apply a median filter to an image.

Parameters
  • image (numpy.ndarray): Input image to be filtered.
  • kernel_size (int): Size of the median filter kernel. Must be a positive odd integer.
Returns
  • numpy.ndarray: Filtered image, with the same shape and dtype as the input image.
def clahe(image, clip_limit=None, tile_grid_size=None):
322def clahe(image, clip_limit=None, tile_grid_size=None):
323    """
324    Apply Contrast Limited Adaptive Histogram Equalization (CLAHE) to an image.
325
326    Parameters
327    ----------
328    image : numpy.ndarray
329        Input image to be equalized.
330    clip_limit : float, optional
331        Threshold for contrast limiting. If `None`, no clipping is performed.
332        Default is `None`.
333    tile_grid_size : tuple(int, int), optional
334        Number of rows and columns into which the image will be divided.
335        Default is `(8, 8)` (as for OpenCV).
336
337    Returns
338    -------
339    numpy.ndarray
340        Equalized image, with the same shape and dtype as the input image.
341    """
342    clahe_op = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)
343    return clahe_op.apply(image)

Apply Contrast Limited Adaptive Histogram Equalization (CLAHE) to an image.

Parameters
  • image (numpy.ndarray): Input image to be equalized.
  • clip_limit (float, optional): Threshold for contrast limiting. If None, no clipping is performed. Default is None.
  • tile_grid_size (tuple(int, int), optional): Number of rows and columns into which the image will be divided. Default is (8, 8) (as for OpenCV).
Returns
  • numpy.ndarray: Equalized image, with the same shape and dtype as the input image.
def otsu(image):
351def otsu(image):
352    """
353    Perform Otsu thresholding on a grayscale image.
354
355    This function computes the optimal threshold for the input grayscale image using the Otsu method,
356    and returns the thresholded image and the computed threshold.
357
358    Parameters
359    ----------
360    image : numpy.ndarray
361        Input grayscale image to be thresholded.
362
363    Returns
364    -------
365    tuple
366        A tuple containing the threshold value and the thresholded image, both as numpy.ndarray
367        with the same shape and dtype as the input image.
368
369    Raises
370    ------
371    ValueError
372        If the input image is not grayscale.
373    """
374    if dito.core.is_color(image=image):
375        raise ValueError("Expected gray image but got color image for Otsu thresholding")
376    (theta, image2) = cv2.threshold(src=image, thresh=-1, maxval=255, type=cv2.THRESH_BINARY | cv2.THRESH_OTSU)
377    return (theta, image2)

Perform Otsu thresholding on a grayscale image.

This function computes the optimal threshold for the input grayscale image using the Otsu method, and returns the thresholded image and the computed threshold.

Parameters
  • image (numpy.ndarray): Input grayscale image to be thresholded.
Returns
  • tuple: A tuple containing the threshold value and the thresholded image, both as numpy.ndarray with the same shape and dtype as the input image.
Raises
  • ValueError: If the input image is not grayscale.
def otsu_theta(image):
380def otsu_theta(image):
381    """
382    Compute the Otsu threshold for a grayscale image.
383
384    This function computes the optimal threshold for the input grayscale image using the Otsu method,
385    and returns only the threshold value.
386
387    Parameters
388    ----------
389    image : numpy.ndarray
390        Input grayscale image.
391
392    Returns
393    -------
394    float
395        The computed threshold value.
396    """
397    (theta, image2) = otsu(image=image)
398    return theta

Compute the Otsu threshold for a grayscale image.

This function computes the optimal threshold for the input grayscale image using the Otsu method, and returns only the threshold value.

Parameters
  • image (numpy.ndarray): Input grayscale image.
Returns
  • float: The computed threshold value.
def otsu_image(image):
401def otsu_image(image):
402    """
403    Threshold a grayscale image using the Otsu method.
404
405    This function computes the optimal threshold for the input grayscale image using the Otsu method,
406    and returns the thresholded image.
407
408    Parameters
409    ----------
410    image : numpy.ndarray
411        Input grayscale image to be thresholded.
412
413    Returns
414    -------
415    numpy.ndarray
416        The thresholded image, as a numpy.ndarray with the same shape and dtype as the input image.
417    """
418    (theta, image2) = otsu(image=image)
419    return image2

Threshold a grayscale image using the Otsu method.

This function computes the optimal threshold for the input grayscale image using the Otsu method, and returns the thresholded image.

Parameters
  • image (numpy.ndarray): Input grayscale image to be thresholded.
Returns
  • numpy.ndarray: The thresholded image, as a numpy.ndarray with the same shape and dtype as the input image.
def morpho_op_kernel(shape, size):
427def morpho_op_kernel(shape, size):
428    """
429    Create a morphological operation kernel.
430
431    Parameters
432    ----------
433    shape : int
434        Type of the kernel. The following values are supported:
435        - `cv2.MORPH_RECT`: rectangular kernel
436        - `cv2.MORPH_CROSS`: cross-shaped kernel
437        - `cv2.MORPH_ELLIPSE`: elliptical kernel
438    size : int or tuple of int
439        Size (width, height) of the kernel. If a single integer is provided, assume equal width and height.
440
441    Returns
442    -------
443    numpy.ndarray
444        The morphological operation kernel.
445    """
446    ksize = dito.utils.get_validated_tuple(x=size, type_=int, count=2)
447    kernel = cv2.getStructuringElement(shape=shape, ksize=ksize, anchor=(-1, -1))
448    return kernel

Create a morphological operation kernel.

Parameters
  • shape (int): Type of the kernel. The following values are supported:
    • cv2.MORPH_RECT: rectangular kernel
    • cv2.MORPH_CROSS: cross-shaped kernel
    • cv2.MORPH_ELLIPSE: elliptical kernel
  • size (int or tuple of int): Size (width, height) of the kernel. If a single integer is provided, assume equal width and height.
Returns
  • numpy.ndarray: The morphological operation kernel.
def morpho_op(image, operation, shape=2, size=3, anchor=(-1, -1), iterations=1):
451def morpho_op(image, operation, shape=cv2.MORPH_ELLIPSE, size=3, anchor=(-1, -1), iterations=1):
452    """
453    Apply a morphological operation to an image.
454
455    Parameters
456    ----------
457    image : numpy.ndarray
458        Input image to which the morphological operation is applied.
459    operation : int
460        Type of morphological operation. The following values are supported:
461        - `cv2.MORPH_ERODE`: erosion
462        - `cv2.MORPH_DILATE`: dilation
463        - `cv2.MORPH_OPEN`: opening
464        - `cv2.MORPH_CLOSE`: closing
465        - `cv2.MORPH_GRADIENT`: gradient
466        - `cv2.MORPH_TOPHAT`: top hat
467        - `cv2.MORPH_BLACKHAT`: black hat
468    shape : int
469        Type of the kernel. The following values are supported:
470        - `cv2.MORPH_RECT`: rectangular kernel
471        - `cv2.MORPH_CROSS`: cross-shaped kernel
472        - `cv2.MORPH_ELLIPSE`: elliptical kernel
473        The default value is `cv2.MORPH_ELLIPSE`.
474    size : int or tuple of int
475        Size (width, height) of the kernel. If a single integer is provided, assume equal width and height.
476        The default value is `3`.
477    anchor : tuple of int, optional
478        Anchor point of the structuring element used for the morphological operation. The anchor should lie within
479        the kernel. The default value is `(-1, -1)`, which means that the anchor is at the center of the kernel.
480    iterations : int, optional
481        Number of times the morphological operation is applied. The default value is `1`.
482
483    Returns
484    -------
485    numpy.ndarray
486        Resulting image after applying the morphological operation, with the same shape and dtype as the input image.
487    """
488    kernel = morpho_op_kernel(shape=shape, size=size)
489    return cv2.morphologyEx(src=image, op=operation, kernel=kernel, anchor=anchor, iterations=iterations)

Apply a morphological operation to an image.

Parameters
  • image (numpy.ndarray): Input image to which the morphological operation is applied.
  • operation (int): Type of morphological operation. The following values are supported:
    • cv2.MORPH_ERODE: erosion
    • cv2.MORPH_DILATE: dilation
    • cv2.MORPH_OPEN: opening
    • cv2.MORPH_CLOSE: closing
    • cv2.MORPH_GRADIENT: gradient
    • cv2.MORPH_TOPHAT: top hat
    • cv2.MORPH_BLACKHAT: black hat
  • shape (int): Type of the kernel. The following values are supported:
    • cv2.MORPH_RECT: rectangular kernel
    • cv2.MORPH_CROSS: cross-shaped kernel
    • cv2.MORPH_ELLIPSE: elliptical kernel The default value is cv2.MORPH_ELLIPSE.
  • size (int or tuple of int): Size (width, height) of the kernel. If a single integer is provided, assume equal width and height. The default value is 3.
  • anchor (tuple of int, optional): Anchor point of the structuring element used for the morphological operation. The anchor should lie within the kernel. The default value is (-1, -1), which means that the anchor is at the center of the kernel.
  • iterations (int, optional): Number of times the morphological operation is applied. The default value is 1.
Returns
  • numpy.ndarray: Resulting image after applying the morphological operation, with the same shape and dtype as the input image.
def dilate(image, **kwargs):
492def dilate(image, **kwargs):
493    """
494    Apply morphological dilation to an image.
495
496    Parameters
497    ----------
498    image : numpy.ndarray
499        Input image to be dilated.
500    **kwargs
501        Optional arguments to be passed to `morpho_op`.
502
503    Returns
504    -------
505    numpy.ndarray
506        Dilated image, with the same shape and dtype as the input image.
507    """
508    return morpho_op(image=image, operation=cv2.MORPH_DILATE, **kwargs)

Apply morphological dilation to an image.

Parameters
  • image (numpy.ndarray): Input image to be dilated.
  • **kwargs: Optional arguments to be passed to morpho_op.
Returns
  • numpy.ndarray: Dilated image, with the same shape and dtype as the input image.
def erode(image, **kwargs):
511def erode(image, **kwargs):
512    """
513    Apply morphological erosion to an image.
514
515    Parameters
516    ----------
517    image : numpy.ndarray
518        Input image to be eroded.
519    **kwargs
520        Optional arguments to be passed to `morpho_op`.
521
522    Returns
523    -------
524    numpy.ndarray
525        Eroded image, with the same shape and dtype as the input image.
526    """
527    return morpho_op(image=image, operation=cv2.MORPH_ERODE, **kwargs)

Apply morphological erosion to an image.

Parameters
  • image (numpy.ndarray): Input image to be eroded.
  • **kwargs: Optional arguments to be passed to morpho_op.
Returns
  • numpy.ndarray: Eroded image, with the same shape and dtype as the input image.
def morpho_open(image, **kwargs):
530def morpho_open(image, **kwargs):
531    """
532    Apply morphological opening to an image.
533
534    Parameters
535    ----------
536    image : numpy.ndarray
537        Input image to be opened.
538    **kwargs
539        Optional arguments to be passed to `morpho_op`.
540
541    Returns
542    -------
543    numpy.ndarray
544        Image after the opening operation, with the same shape and dtype as the input image.
545    """
546    return morpho_op(image=image, operation=cv2.MORPH_OPEN, **kwargs)

Apply morphological opening to an image.

Parameters
  • image (numpy.ndarray): Input image to be opened.
  • **kwargs: Optional arguments to be passed to morpho_op.
Returns
  • numpy.ndarray: Image after the opening operation, with the same shape and dtype as the input image.
def morpho_close(image, **kwargs):
549def morpho_close(image, **kwargs):
550    """
551    Apply morphological closing to an image.
552
553    Parameters
554    ----------
555    image : numpy.ndarray
556        Input image to be closed.
557    **kwargs
558        Optional arguments to be passed to `morpho_op`.
559
560    Returns
561    -------
562    numpy.ndarray
563        Image after the closing operation, with the same shape and dtype as the input image.
564    """
565    return morpho_op(image=image, operation=cv2.MORPH_CLOSE, **kwargs)

Apply morphological closing to an image.

Parameters
  • image (numpy.ndarray): Input image to be closed.
  • **kwargs: Optional arguments to be passed to morpho_op.
Returns
  • numpy.ndarray: Image after the closing operation, with the same shape and dtype as the input image.
def blackhat(image, **kwargs):
568def blackhat(image, **kwargs):
569    """
570    Apply the morphological blackhat operation to an image.
571
572    Parameters
573    ----------
574    image : numpy.ndarray
575        Input image.
576    **kwargs
577        Optional arguments to be passed to `morpho_op`.
578
579    Returns
580    -------
581    numpy.ndarray
582        Image after the blackhat operation, with the same shape and dtype as the input image.
583    """
584    return morpho_op(image=image, operation=cv2.MORPH_BLACKHAT, **kwargs)

Apply the morphological blackhat operation to an image.

Parameters
  • image (numpy.ndarray): Input image.
  • **kwargs: Optional arguments to be passed to morpho_op.
Returns
  • numpy.ndarray: Image after the blackhat operation, with the same shape and dtype as the input image.
def tophat(image, **kwargs):
587def tophat(image, **kwargs):
588    """
589    Apply the morphological tophat operation to an image.
590
591    Parameters
592    ----------
593    image : numpy.ndarray
594        Input image.
595    **kwargs
596        Optional arguments to be passed to `morpho_op`.
597
598    Returns
599    -------
600    numpy.ndarray
601        Image after the tophat operation, with the same shape and dtype as the input image.
602    """
603    return morpho_op(image=image, operation=cv2.MORPH_TOPHAT, **kwargs)

Apply the morphological tophat operation to an image.

Parameters
  • image (numpy.ndarray): Input image.
  • **kwargs: Optional arguments to be passed to morpho_op.
Returns
  • numpy.ndarray: Image after the tophat operation, with the same shape and dtype as the input image.
def dog(image, sigma1, sigma2, return_raw=False, colormap=None):
611def dog(image, sigma1, sigma2, return_raw=False, colormap=None):
612    """
613    Apply the difference of Gaussians (DoG) operation to an image.
614
615    Parameters
616    ----------
617    image : numpy.ndarray
618        Input image to which the DoG is applied.
619    sigma1 : float
620        Standard deviation of the first Gaussian filter.
621    sigma2 : float
622        Standard deviation of the second Gaussian filter.
623    return_raw : bool, optional
624        If True, return the raw difference image. Otherwise, the difference image is shifted and scaled to match the
625        original dtype range. The default value is False.
626    colormap : str, optional
627        Name of the colormap to use for colorizing the result. If None, no colorization is applied. The default value
628        is None.
629
630    Returns
631    -------
632    numpy.ndarray
633        Resulting image after applying the DoG, with the same shape and dtype as the input image.
634    """
635    blur1 = gaussian_blur(image=image, sigma=sigma1).astype(np.float32)
636    blur2 = gaussian_blur(image=image, sigma=sigma2).astype(np.float32)
637    diff = blur1 - blur2
638    if return_raw:
639        return diff
640    else:
641        diff_11 = diff / dito.core.dtype_range(dtype=image.dtype)[1]
642        diff_01 = (diff_11 + 1.0) * 0.5
643        result = dito.convert(image=diff_01, dtype=image.dtype)
644        if colormap is not None:
645            result = dito.visual.colorize(image=result, colormap=colormap)
646        return result

Apply the difference of Gaussians (DoG) operation to an image.

Parameters
  • image (numpy.ndarray): Input image to which the DoG is applied.
  • sigma1 (float): Standard deviation of the first Gaussian filter.
  • sigma2 (float): Standard deviation of the second Gaussian filter.
  • return_raw (bool, optional): If True, return the raw difference image. Otherwise, the difference image is shifted and scaled to match the original dtype range. The default value is False.
  • colormap (str, optional): Name of the colormap to use for colorizing the result. If None, no colorization is applied. The default value is None.
Returns
  • numpy.ndarray: Resulting image after applying the DoG, with the same shape and dtype as the input image.
def dog_interactive(image, colormap=None):
649def dog_interactive(image, colormap=None):
650    """
651    Display an interactive window for exploring the difference of Gaussians (DoG) of an image.
652
653    The window displays the input image, two sliders for adjusting the standard deviations of the Gaussian filters used
654    in the DoG, and a visualization of the DoG result.
655
656    Parameters
657    ----------
658    image : numpy.ndarray
659        Input image to which the DoG is applied.
660    colormap : str, optional
661        Name of the colormap to use for colorizing the result. If None, no colorization is applied. The default value
662        is None.
663
664    Returns
665    -------
666    None
667    """
668    window_name = "dito.dog_interactive"
669    sliders = [dito.highgui.FloatSlider(window_name=window_name, name="sigma{}".format(n_slider + 1), min_value=0.0, max_value=15.0, value_count=1001) for n_slider in range(2)]
670    sliders[0].set_value(0.5)
671    sliders[1].set_value(0.8)
672
673    image_show = None
674    while True:
675        if (image_show is None) or any(slider.changed for slider in sliders):
676            sigmas = [sliders[n_slider].get_value() for n_slider in range(2)]
677            images_blur = [gaussian_blur(image=image, sigma=sigmas[n_slider]) for n_slider in range(2)]
678            images_blur = [dito.visual.text(image=image_blur, message="sigma{} = {:.2f}".format(n_slider + 1, sigmas[n_slider])) for (n_slider, image_blur) in enumerate(images_blur)]
679            image_dog = dog(image, sigma1=sigmas[0], sigma2=sigmas[1], return_raw=False, colormap=colormap)
680            image_show = dito.stack([[image, image_dog], images_blur])
681        key = dito.show(image=image_show, window_name=window_name, wait=10)
682        if key in dito.qkeys():
683            return

Display an interactive window for exploring the difference of Gaussians (DoG) of an image.

The window displays the input image, two sliders for adjusting the standard deviations of the Gaussian filters used in the DoG, and a visualization of the DoG result.

Parameters
  • image (numpy.ndarray): Input image to which the DoG is applied.
  • colormap (str, optional): Name of the colormap to use for colorizing the result. If None, no colorization is applied. The default value is None.
Returns
  • None
class Contour:
 691class Contour():
 692    """
 693    A class to represent a contour.
 694
 695    Attributes
 696    ----------
 697    points : numpy.ndarray
 698        The points that make up the contour. A 2D numpy array of shape `(n, 2)`, where `n` is the number of points
 699        in the contour. Each row contains the `(x, y)` coordinates of a point.
 700    """
 701
 702    def __init__(self, points):
 703        """
 704        Parameters
 705        ----------
 706        points : numpy.ndarray
 707            The points defining the contour. A 2D numpy array of shape `(n, 2)`, where `n` is the number of points
 708            in the contour. Each row contains the `(x, y)` coordinates of a point.
 709        """
 710        self.points = points
 711
 712    def __len__(self):
 713        """
 714        Return the number of points in the contour.
 715
 716        Returns
 717        -------
 718        int
 719            The number of points in the contour.
 720        """
 721        return len(self.points)
 722
 723    def __eq__(self, other):
 724        """
 725        Check if two contours are equal.
 726
 727        Parameters
 728        ----------
 729        other : Contour
 730            Another instance of the `Contour` class.
 731
 732        Returns
 733        -------
 734        bool
 735            True if the two contours are equal, False otherwise.
 736        """
 737        if not isinstance(other, Contour):
 738            raise TypeError("Argument 'other' must be a contour")
 739
 740        if len(self) != len(other):
 741            return False
 742
 743        return np.array_equal(self.points, other.points)
 744
 745    def copy(self):
 746        """
 747        Return a copy of the current instance of the `Contour` class.
 748
 749        Returns
 750        -------
 751        Contour
 752            A copy of the current instance of the `Contour` class.
 753        """
 754        return Contour(points=self.points.copy())
 755
 756    def get_center(self):
 757        """
 758        Return the center point `(x, y)` of the contour.
 759
 760        Returns
 761        -------
 762        numpy.ndarray
 763            A 2D numpy array representing the `(x, y)` coordinates of the center point of the contour.
 764        """
 765        return np.mean(self.points, axis=0)
 766
 767    def get_center_x(self):
 768        """
 769        Return the x-coordinate of the center point of the contour.
 770
 771        Returns
 772        -------
 773        float
 774            The x-coordinate of the center point of the contour.
 775        """
 776        return np.mean(self.points[:, 0])
 777
 778    def get_center_y(self):
 779        """
 780        Return the y-coordinate of the center point of the contour.
 781
 782        Returns
 783        -------
 784        float
 785            The y-coordinate of the center point of the contour.
 786        """
 787        return np.mean(self.points[:, 1])
 788
 789    def get_min_x(self):
 790        """
 791        Return the minimum x-coordinate of the contour.
 792
 793        Returns
 794        -------
 795        float
 796            The minimum x-coordinate of the contour.
 797        """
 798        return np.min(self.points[:, 0])
 799
 800    def get_max_x(self):
 801        """
 802        Return the maximum x-coordinate of the contour.
 803
 804        Returns
 805        -------
 806        float
 807            The maximum x-coordinate of the contour.
 808        """
 809        return np.max(self.points[:, 0])
 810
 811    def get_width(self):
 812        """
 813        Return the width of the contour.
 814
 815        Returns
 816        -------
 817        float
 818            The width of the contour.
 819        """
 820        return self.get_max_x() - self.get_min_x()
 821
 822    def get_min_y(self):
 823        """
 824        Return the minimum y-coordinate of the contour.
 825
 826        Returns
 827        -------
 828        float
 829            The minimum y-coordinate of the contour.
 830        """
 831        return np.min(self.points[:, 1])
 832
 833    def get_max_y(self):
 834        """
 835        Return the maximum y-coordinate of the contour.
 836
 837        Returns
 838        -------
 839        float
 840            The maximum y-coordinate of the contour.
 841        """
 842        return np.max(self.points[:, 1])
 843
 844    def get_height(self):
 845        """
 846        Return the height of the contour.
 847
 848        Returns
 849        -------
 850        float
 851            The height of the contour.
 852        """
 853        return self.get_max_y() - self.get_min_y()
 854
 855    def get_area(self, mode="draw"):
 856        """
 857        Compute the area of the contour.
 858
 859        Parameters
 860        ----------
 861        mode : {"draw", "calc"}, optional
 862            The method to use for computing the area. If "draw", the area is computed by drawing the contour as a filled
 863            white shape on a black image and counting the number of white pixels. If "calc", the area is computed using the
 864            cv2.contourArea() function. Default is "draw".
 865
 866        Returns
 867        -------
 868        float
 869            The area of the contour.
 870        """
 871        if mode == "draw":
 872            image = self.draw_standalone(color=(1,), thickness=1, filled=True, antialias=False, border=2)
 873            return np.sum(image)
 874
 875        elif mode == "calc":
 876            return cv2.contourArea(contour=self.points)
 877
 878        else:
 879            raise ValueError("Invalid value for argument 'mode': '{}'".format(mode))
 880
 881    def get_perimeter(self):
 882        """
 883        Compute the perimeter of the contour.
 884
 885        Returns
 886        -------
 887        float
 888            The perimeter of the contour.
 889        """
 890        return cv2.arcLength(curve=self.points, closed=True)
 891
 892    def get_circularity(self):
 893        """
 894        Compute the circularity of the contour.
 895
 896        Returns
 897        -------
 898        float
 899            The circularity of the contour.
 900        """
 901        r_area = np.sqrt(self.get_area() / np.pi)
 902        r_perimeter = self.get_perimeter() / (2.0 * np.pi)
 903        return r_area / r_perimeter
 904
 905    def get_ellipse(self):
 906        """
 907        Fit an ellipse to the contour.
 908
 909        Returns
 910        -------
 911        tuple
 912            A tuple `(center, size, angle)`, where `center` is a tuple `(x, y)` representing the center point
 913            of the ellipse, `size` is a tuple `(width, height)` representing the size of the ellipse, and `angle`
 914            is the angle (in degrees) between the major axis of the ellipse and the x-axis.
 915        """
 916        return cv2.fitEllipse(points=self.points)
 917
 918    def get_eccentricity(self):
 919        """
 920        Calculate the eccentricity of the contour.
 921
 922        Returns
 923        -------
 924        float
 925            The eccentricity of the contour.
 926        """
 927        ellipse = self.get_ellipse()
 928        (width, height) = ellipse[1]
 929        semi_major_axis = max(width, height) * 0.5
 930        semi_minor_axis = min(width, height) * 0.5
 931        eccentricity = math.sqrt(1.0 - (semi_minor_axis / semi_major_axis)**2)
 932        return eccentricity
 933
 934    def get_moments(self):
 935        """
 936        Calculate the moments of the contour.
 937
 938        The moment values can be used to calculate various properties of the contour, such as its centroid,
 939        orientation, and size.
 940
 941        Returns
 942        -------
 943        dict
 944            A dictionary of moment values for the contour.
 945        """
 946        return cv2.moments(array=self.points, binaryImage=False)
 947
 948    def get_hu_moments(self, log=True):
 949        """
 950        Calculate the seven Hu moments for the contour.
 951
 952        These moments are invariant shape descriptors that can be used to capture shape properties of a contour, and
 953        the first six of them are invariant to translation, scale, and rotation. However, the seventh moment is not
 954        invariant to reflection and changes its sign under reflection.
 955
 956        Parameters
 957        ----------
 958        log : bool, optional
 959            If True (default), the logarithm of the absolute value of the Hu moments will be returned. If False, the raw
 960            Hu moments will be returned.
 961
 962        Returns
 963        -------
 964        numpy.ndarray
 965            A 1D numpy array containing the seven Hu moments for the contour. If `log=True`, the values will be the
 966            logarithm of the absolute value of the Hu moments.
 967        """
 968        hu_moments = cv2.HuMoments(m=self.get_moments())
 969        if log:
 970            return np.sign(hu_moments) * np.log10(np.abs(hu_moments))
 971        else:
 972            return hu_moments
 973
 974    def shift(self, offset_x=None, offset_y=None):
 975        """
 976        Shift the contour by a given offset along the x and/or y axis.
 977
 978        Parameters
 979        ----------
 980        offset_x : float, optional
 981            The amount by which to shift the contour along the x-axis. If not specified, the contour is not shifted along the x-axis.
 982        offset_y : float, optional
 983            The amount by which to shift the contour along the y-axis. If not specified, the contour is not shifted along the y-axis.
 984
 985        Returns
 986        -------
 987        None
 988        """
 989        if offset_x is not None:
 990            self.points[:, 0] += offset_x
 991        if offset_y is not None:
 992            self.points[:, 1] += offset_y
 993
 994    def draw(self, image, color, thickness=1, filled=True, antialias=False, offset=None):
 995        """
 996        Draw the contour into an existing image.
 997
 998        The image is changed in-place.
 999
1000        Parameters
1001        ----------
1002        image : numpy.ndarray
1003            The image into which the contour will be drawn.
1004        color : tuple
1005            The color of the contour. Its length must equal the channel count of the image.
1006        thickness : int, optional
1007            The thickness of the contour lines. Has no effect if `filled` is True. Default is 1.
1008        filled : bool, optional
1009            Whether the contour should be filled. If True, the interior of the contour is filled with the `color` value. Default is True.
1010        antialias : bool, optional
1011            Whether antialiasing should be applied when drawing the contour. Default is False.
1012        offset : tuple or list or numpy.ndarray, optional
1013            The `(x, y)` coordinates of the offset of the contour from the origin of the image. Default is None, which corresponds to no offset.
1014
1015        Returns
1016        -------
1017        None
1018        """
1019        cv2.drawContours(image=image, contours=[np.round(self.points).astype(np.int32)], contourIdx=0, color=color, thickness=cv2.FILLED if filled else thickness, lineType=cv2.LINE_AA if antialias else cv2.LINE_8, offset=offset)
1020
1021    def draw_standalone(self, color, thickness=1, filled=True, antialias=False, border=0):
1022        """
1023        Draw the contour as a standalone image.
1024
1025        The image has the same size as the contour (which thus is centered in the image),
1026        but an additional border can be specified.
1027
1028        Parameters
1029        ----------
1030        color : tuple or list or numpy.ndarray
1031            The color of the contour. Currently, only grayscale mode is supported.
1032        thickness : int, optional
1033            The thickness of the contour lines. Has no effect if `filled` is True. Default is 1.
1034        filled : bool, optional
1035            Whether the contour should be filled. If True, the interior of the contour is filled with the `color` value. Default is True.
1036        antialias : bool, optional
1037            Whether antialiasing should be applied when drawing the contour. Default is False.
1038        border : int, optional
1039            The size of the border around the contour. Default is 0.
1040
1041        Returns
1042        -------
1043        numpy.ndarray
1044            A 2D numpy array representing the image of the contour.
1045        """
1046        image = np.zeros(shape=(2 * border + self.get_height(), 2 * border + self.get_width()), dtype=np.uint8)
1047        self.draw(image=image, color=color, thickness=thickness, filled=filled, antialias=antialias, offset=(border - self.get_min_x(), border - self.get_min_y()))
1048        return image

A class to represent a contour.

Attributes
  • points (numpy.ndarray): The points that make up the contour. A 2D numpy array of shape (n, 2), where n is the number of points in the contour. Each row contains the (x, y) coordinates of a point.
Contour(points)
702    def __init__(self, points):
703        """
704        Parameters
705        ----------
706        points : numpy.ndarray
707            The points defining the contour. A 2D numpy array of shape `(n, 2)`, where `n` is the number of points
708            in the contour. Each row contains the `(x, y)` coordinates of a point.
709        """
710        self.points = points
Parameters
  • points (numpy.ndarray): The points defining the contour. A 2D numpy array of shape (n, 2), where n is the number of points in the contour. Each row contains the (x, y) coordinates of a point.
points
def copy(self):
745    def copy(self):
746        """
747        Return a copy of the current instance of the `Contour` class.
748
749        Returns
750        -------
751        Contour
752            A copy of the current instance of the `Contour` class.
753        """
754        return Contour(points=self.points.copy())

Return a copy of the current instance of the Contour class.

Returns
  • Contour: A copy of the current instance of the Contour class.
def get_center(self):
756    def get_center(self):
757        """
758        Return the center point `(x, y)` of the contour.
759
760        Returns
761        -------
762        numpy.ndarray
763            A 2D numpy array representing the `(x, y)` coordinates of the center point of the contour.
764        """
765        return np.mean(self.points, axis=0)

Return the center point (x, y) of the contour.

Returns
  • numpy.ndarray: A 2D numpy array representing the (x, y) coordinates of the center point of the contour.
def get_center_x(self):
767    def get_center_x(self):
768        """
769        Return the x-coordinate of the center point of the contour.
770
771        Returns
772        -------
773        float
774            The x-coordinate of the center point of the contour.
775        """
776        return np.mean(self.points[:, 0])

Return the x-coordinate of the center point of the contour.

Returns
  • float: The x-coordinate of the center point of the contour.
def get_center_y(self):
778    def get_center_y(self):
779        """
780        Return the y-coordinate of the center point of the contour.
781
782        Returns
783        -------
784        float
785            The y-coordinate of the center point of the contour.
786        """
787        return np.mean(self.points[:, 1])

Return the y-coordinate of the center point of the contour.

Returns
  • float: The y-coordinate of the center point of the contour.
def get_min_x(self):
789    def get_min_x(self):
790        """
791        Return the minimum x-coordinate of the contour.
792
793        Returns
794        -------
795        float
796            The minimum x-coordinate of the contour.
797        """
798        return np.min(self.points[:, 0])

Return the minimum x-coordinate of the contour.

Returns
  • float: The minimum x-coordinate of the contour.
def get_max_x(self):
800    def get_max_x(self):
801        """
802        Return the maximum x-coordinate of the contour.
803
804        Returns
805        -------
806        float
807            The maximum x-coordinate of the contour.
808        """
809        return np.max(self.points[:, 0])

Return the maximum x-coordinate of the contour.

Returns
  • float: The maximum x-coordinate of the contour.
def get_width(self):
811    def get_width(self):
812        """
813        Return the width of the contour.
814
815        Returns
816        -------
817        float
818            The width of the contour.
819        """
820        return self.get_max_x() - self.get_min_x()

Return the width of the contour.

Returns
  • float: The width of the contour.
def get_min_y(self):
822    def get_min_y(self):
823        """
824        Return the minimum y-coordinate of the contour.
825
826        Returns
827        -------
828        float
829            The minimum y-coordinate of the contour.
830        """
831        return np.min(self.points[:, 1])

Return the minimum y-coordinate of the contour.

Returns
  • float: The minimum y-coordinate of the contour.
def get_max_y(self):
833    def get_max_y(self):
834        """
835        Return the maximum y-coordinate of the contour.
836
837        Returns
838        -------
839        float
840            The maximum y-coordinate of the contour.
841        """
842        return np.max(self.points[:, 1])

Return the maximum y-coordinate of the contour.

Returns
  • float: The maximum y-coordinate of the contour.
def get_height(self):
844    def get_height(self):
845        """
846        Return the height of the contour.
847
848        Returns
849        -------
850        float
851            The height of the contour.
852        """
853        return self.get_max_y() - self.get_min_y()

Return the height of the contour.

Returns
  • float: The height of the contour.
def get_area(self, mode='draw'):
855    def get_area(self, mode="draw"):
856        """
857        Compute the area of the contour.
858
859        Parameters
860        ----------
861        mode : {"draw", "calc"}, optional
862            The method to use for computing the area. If "draw", the area is computed by drawing the contour as a filled
863            white shape on a black image and counting the number of white pixels. If "calc", the area is computed using the
864            cv2.contourArea() function. Default is "draw".
865
866        Returns
867        -------
868        float
869            The area of the contour.
870        """
871        if mode == "draw":
872            image = self.draw_standalone(color=(1,), thickness=1, filled=True, antialias=False, border=2)
873            return np.sum(image)
874
875        elif mode == "calc":
876            return cv2.contourArea(contour=self.points)
877
878        else:
879            raise ValueError("Invalid value for argument 'mode': '{}'".format(mode))

Compute the area of the contour.

Parameters
  • mode ({"draw", "calc"}, optional): The method to use for computing the area. If "draw", the area is computed by drawing the contour as a filled white shape on a black image and counting the number of white pixels. If "calc", the area is computed using the cv2.contourArea() function. Default is "draw".
Returns
  • float: The area of the contour.
def get_perimeter(self):
881    def get_perimeter(self):
882        """
883        Compute the perimeter of the contour.
884
885        Returns
886        -------
887        float
888            The perimeter of the contour.
889        """
890        return cv2.arcLength(curve=self.points, closed=True)

Compute the perimeter of the contour.

Returns
  • float: The perimeter of the contour.
def get_circularity(self):
892    def get_circularity(self):
893        """
894        Compute the circularity of the contour.
895
896        Returns
897        -------
898        float
899            The circularity of the contour.
900        """
901        r_area = np.sqrt(self.get_area() / np.pi)
902        r_perimeter = self.get_perimeter() / (2.0 * np.pi)
903        return r_area / r_perimeter

Compute the circularity of the contour.

Returns
  • float: The circularity of the contour.
def get_ellipse(self):
905    def get_ellipse(self):
906        """
907        Fit an ellipse to the contour.
908
909        Returns
910        -------
911        tuple
912            A tuple `(center, size, angle)`, where `center` is a tuple `(x, y)` representing the center point
913            of the ellipse, `size` is a tuple `(width, height)` representing the size of the ellipse, and `angle`
914            is the angle (in degrees) between the major axis of the ellipse and the x-axis.
915        """
916        return cv2.fitEllipse(points=self.points)

Fit an ellipse to the contour.

Returns
  • tuple: A tuple (center, size, angle), where center is a tuple (x, y) representing the center point of the ellipse, size is a tuple (width, height) representing the size of the ellipse, and angle is the angle (in degrees) between the major axis of the ellipse and the x-axis.
def get_eccentricity(self):
918    def get_eccentricity(self):
919        """
920        Calculate the eccentricity of the contour.
921
922        Returns
923        -------
924        float
925            The eccentricity of the contour.
926        """
927        ellipse = self.get_ellipse()
928        (width, height) = ellipse[1]
929        semi_major_axis = max(width, height) * 0.5
930        semi_minor_axis = min(width, height) * 0.5
931        eccentricity = math.sqrt(1.0 - (semi_minor_axis / semi_major_axis)**2)
932        return eccentricity

Calculate the eccentricity of the contour.

Returns
  • float: The eccentricity of the contour.
def get_moments(self):
934    def get_moments(self):
935        """
936        Calculate the moments of the contour.
937
938        The moment values can be used to calculate various properties of the contour, such as its centroid,
939        orientation, and size.
940
941        Returns
942        -------
943        dict
944            A dictionary of moment values for the contour.
945        """
946        return cv2.moments(array=self.points, binaryImage=False)

Calculate the moments of the contour.

The moment values can be used to calculate various properties of the contour, such as its centroid, orientation, and size.

Returns
  • dict: A dictionary of moment values for the contour.
def get_hu_moments(self, log=True):
948    def get_hu_moments(self, log=True):
949        """
950        Calculate the seven Hu moments for the contour.
951
952        These moments are invariant shape descriptors that can be used to capture shape properties of a contour, and
953        the first six of them are invariant to translation, scale, and rotation. However, the seventh moment is not
954        invariant to reflection and changes its sign under reflection.
955
956        Parameters
957        ----------
958        log : bool, optional
959            If True (default), the logarithm of the absolute value of the Hu moments will be returned. If False, the raw
960            Hu moments will be returned.
961
962        Returns
963        -------
964        numpy.ndarray
965            A 1D numpy array containing the seven Hu moments for the contour. If `log=True`, the values will be the
966            logarithm of the absolute value of the Hu moments.
967        """
968        hu_moments = cv2.HuMoments(m=self.get_moments())
969        if log:
970            return np.sign(hu_moments) * np.log10(np.abs(hu_moments))
971        else:
972            return hu_moments

Calculate the seven Hu moments for the contour.

These moments are invariant shape descriptors that can be used to capture shape properties of a contour, and the first six of them are invariant to translation, scale, and rotation. However, the seventh moment is not invariant to reflection and changes its sign under reflection.

Parameters
  • log (bool, optional): If True (default), the logarithm of the absolute value of the Hu moments will be returned. If False, the raw Hu moments will be returned.
Returns
  • numpy.ndarray: A 1D numpy array containing the seven Hu moments for the contour. If log=True, the values will be the logarithm of the absolute value of the Hu moments.
def shift(self, offset_x=None, offset_y=None):
974    def shift(self, offset_x=None, offset_y=None):
975        """
976        Shift the contour by a given offset along the x and/or y axis.
977
978        Parameters
979        ----------
980        offset_x : float, optional
981            The amount by which to shift the contour along the x-axis. If not specified, the contour is not shifted along the x-axis.
982        offset_y : float, optional
983            The amount by which to shift the contour along the y-axis. If not specified, the contour is not shifted along the y-axis.
984
985        Returns
986        -------
987        None
988        """
989        if offset_x is not None:
990            self.points[:, 0] += offset_x
991        if offset_y is not None:
992            self.points[:, 1] += offset_y

Shift the contour by a given offset along the x and/or y axis.

Parameters
  • offset_x (float, optional): The amount by which to shift the contour along the x-axis. If not specified, the contour is not shifted along the x-axis.
  • offset_y (float, optional): The amount by which to shift the contour along the y-axis. If not specified, the contour is not shifted along the y-axis.
Returns
  • None
def draw( self, image, color, thickness=1, filled=True, antialias=False, offset=None):
 994    def draw(self, image, color, thickness=1, filled=True, antialias=False, offset=None):
 995        """
 996        Draw the contour into an existing image.
 997
 998        The image is changed in-place.
 999
1000        Parameters
1001        ----------
1002        image : numpy.ndarray
1003            The image into which the contour will be drawn.
1004        color : tuple
1005            The color of the contour. Its length must equal the channel count of the image.
1006        thickness : int, optional
1007            The thickness of the contour lines. Has no effect if `filled` is True. Default is 1.
1008        filled : bool, optional
1009            Whether the contour should be filled. If True, the interior of the contour is filled with the `color` value. Default is True.
1010        antialias : bool, optional
1011            Whether antialiasing should be applied when drawing the contour. Default is False.
1012        offset : tuple or list or numpy.ndarray, optional
1013            The `(x, y)` coordinates of the offset of the contour from the origin of the image. Default is None, which corresponds to no offset.
1014
1015        Returns
1016        -------
1017        None
1018        """
1019        cv2.drawContours(image=image, contours=[np.round(self.points).astype(np.int32)], contourIdx=0, color=color, thickness=cv2.FILLED if filled else thickness, lineType=cv2.LINE_AA if antialias else cv2.LINE_8, offset=offset)

Draw the contour into an existing image.

The image is changed in-place.

Parameters
  • image (numpy.ndarray): The image into which the contour will be drawn.
  • color (tuple): The color of the contour. Its length must equal the channel count of the image.
  • thickness (int, optional): The thickness of the contour lines. Has no effect if filled is True. Default is 1.
  • filled (bool, optional): Whether the contour should be filled. If True, the interior of the contour is filled with the color value. Default is True.
  • antialias (bool, optional): Whether antialiasing should be applied when drawing the contour. Default is False.
  • offset (tuple or list or numpy.ndarray, optional): The (x, y) coordinates of the offset of the contour from the origin of the image. Default is None, which corresponds to no offset.
Returns
  • None
def draw_standalone(self, color, thickness=1, filled=True, antialias=False, border=0):
1021    def draw_standalone(self, color, thickness=1, filled=True, antialias=False, border=0):
1022        """
1023        Draw the contour as a standalone image.
1024
1025        The image has the same size as the contour (which thus is centered in the image),
1026        but an additional border can be specified.
1027
1028        Parameters
1029        ----------
1030        color : tuple or list or numpy.ndarray
1031            The color of the contour. Currently, only grayscale mode is supported.
1032        thickness : int, optional
1033            The thickness of the contour lines. Has no effect if `filled` is True. Default is 1.
1034        filled : bool, optional
1035            Whether the contour should be filled. If True, the interior of the contour is filled with the `color` value. Default is True.
1036        antialias : bool, optional
1037            Whether antialiasing should be applied when drawing the contour. Default is False.
1038        border : int, optional
1039            The size of the border around the contour. Default is 0.
1040
1041        Returns
1042        -------
1043        numpy.ndarray
1044            A 2D numpy array representing the image of the contour.
1045        """
1046        image = np.zeros(shape=(2 * border + self.get_height(), 2 * border + self.get_width()), dtype=np.uint8)
1047        self.draw(image=image, color=color, thickness=thickness, filled=filled, antialias=antialias, offset=(border - self.get_min_x(), border - self.get_min_y()))
1048        return image

Draw the contour as a standalone image.

The image has the same size as the contour (which thus is centered in the image), but an additional border can be specified.

Parameters
  • color (tuple or list or numpy.ndarray): The color of the contour. Currently, only grayscale mode is supported.
  • thickness (int, optional): The thickness of the contour lines. Has no effect if filled is True. Default is 1.
  • filled (bool, optional): Whether the contour should be filled. If True, the interior of the contour is filled with the color value. Default is True.
  • antialias (bool, optional): Whether antialiasing should be applied when drawing the contour. Default is False.
  • border (int, optional): The size of the border around the contour. Default is 0.
Returns
  • numpy.ndarray: A 2D numpy array representing the image of the contour.
class ContourList:
1051class ContourList():
1052    """
1053    A class representing a list of contours.
1054
1055    It allows for easy filtering of contours by various properties and offers
1056    additional helper functions such as drawing.
1057
1058    Attributes
1059    ----------
1060    contours : list of Contour objects
1061        The list of contours stored in the ContourList object.
1062    """
1063
1064    def __init__(self, contours_):
1065        """
1066        Parameters
1067        ----------
1068        contours_ : list of Contour objects
1069            The list of contours to be stored.
1070        """
1071        self.contours = contours_
1072
1073    def __len__(self):
1074        """
1075        Return the number of contours.
1076
1077        Returns
1078        -------
1079        int
1080            The number of contours stored in the ContourList object.
1081        """
1082        return len(self.contours)
1083
1084    def __eq__(self, other):
1085        """
1086        Check if two ContourList objects are equal.
1087
1088        Parameters
1089        ----------
1090        other : object
1091            The object to compare to.
1092
1093        Returns
1094        -------
1095        bool
1096            True if the two objects are equal, False otherwise.
1097
1098        Raises
1099        ------
1100        TypeError
1101            If the argument `other` is not a ContourList object.
1102        """
1103        if not isinstance(other, ContourList):
1104            raise TypeError("Argument 'other' must be a contour list")
1105
1106        if len(self) != len(other):
1107            return False
1108
1109        for (contour_self, contour_other) in zip(self.contours, other.contours):
1110            if contour_self != contour_other:
1111                return False
1112
1113        return True
1114
1115    def __getitem__(self, key):
1116        """
1117        Return the contour object at the specified index.
1118
1119        Parameters
1120        ----------
1121        key : int
1122            The index of the contour object to be returned.
1123
1124        Returns
1125        -------
1126        object
1127            The contour object at the specified index.
1128        """
1129        return self.contours[key]
1130
1131    def copy(self):
1132        """
1133        Return a copy of the ContourList object.
1134
1135        Returns
1136        -------
1137        object
1138            A copy of the ContourList object.
1139        """
1140        contours_copy = [contour.copy() for contour in self.contours]
1141        return ContourList(contours_=contours_copy)
1142
1143    def filter(self, func, min_value=None, max_value=None):
1144        """
1145        Filter the contour list based on a given function and range of values.
1146
1147        Only contours whose function values fall within the specified range are retained.
1148        The contour list is modified in place.
1149
1150        Parameters
1151        ----------
1152        func : function
1153            The function used to extract a value from each contour. It must return a value which can be compared against
1154            `min_value` and/or `max_value`.
1155        min_value : float or None, optional
1156            The minimum value of the extracted value for a contour to be kept. Contours with extracted values lower than
1157            this will be removed. If None, no minimum filter is applied. Default is None.
1158        max_value : float or None, optional
1159            The maximum value of the extracted value for a contour to be kept. Contours with extracted values higher than
1160            this will be removed. If None, no maximum filter is applied. Default is None.
1161
1162        Returns
1163        -------
1164        None
1165        """
1166        if (min_value is None) and (max_value is None):
1167            # nothing to do
1168            return
1169
1170        # filter
1171        contours_filtered = []
1172        for contour in self.contours:
1173            value = func(contour)
1174            if (min_value is not None) and (value < min_value):
1175                continue
1176            if (max_value is not None) and (value > max_value):
1177                continue
1178            contours_filtered.append(contour)
1179        self.contours = contours_filtered
1180
1181    def filter_center_x(self, min_value=None, max_value=None):
1182        """
1183        Filter the list of contours by the x-coordinate of their centers.
1184
1185        Only contours whose center x-coordinates fall within the specified range are retained.
1186        The contour list is modified in place.
1187
1188        Parameters
1189        ----------
1190        min_value : float or None, optional
1191            The minimum allowed value of the x-coordinate. If a contour's center x-coordinate is less than this, it is
1192            discarded. If None, no lower bound is applied. Default is None.
1193        max_value : float or None, optional
1194            The maximum allowed value of the x-coordinate. If a contour's center x-coordinate is greater than this, it is
1195            discarded. If None, no upper bound is applied. Default is None.
1196
1197        Returns
1198        -------
1199        None
1200        """
1201        self.filter(func=operator.methodcaller("get_center_x"), min_value=min_value, max_value=max_value)
1202
1203    def filter_center_y(self, min_value=None, max_value=None):
1204        """
1205        Filter the list of contours by the y-coordinate of their centers.
1206
1207        Only contours whose center y-coordinates fall within the specified range are retained.
1208        The contour list is modified in place.
1209
1210        Parameters
1211        ----------
1212        min_value : float or None, optional
1213            The minimum allowed value of the y-coordinate. If a contour's center y-coordinate is less than this, it is
1214            discarded. If None, no lower bound is applied. Default is None.
1215        max_value : float or None, optional
1216            The maximum allowed value of the y-coordinate. If a contour's center y-coordinate is greater than this, it is
1217            discarded. If None, no upper bound is applied. Default is None.
1218
1219        Returns
1220        -------
1221        None
1222        """
1223        self.filter(func=operator.methodcaller("get_center_y"), min_value=min_value, max_value=max_value)
1224
1225    def filter_area(self, min_value=None, max_value=None, mode="draw"):
1226        """
1227        Filter the list of contours by their area.
1228
1229        Only contours whose areas fall within the specified range are retained.
1230        The contour list is modified in place.
1231
1232        Parameters
1233        ----------
1234        min_value : float or None, optional
1235            The minimum allowed area. If a contour's area is less than this, it is discarded. If None, no lower bound
1236            is applied. Default is None.
1237        max_value : float or None, optional
1238            The maximum allowed area. If a contour's area is greater than this, it is discarded. If None, no upper bound
1239            is applied. Default is None.
1240        mode : str, optional
1241            The mode to use for computing the area. See `get_area` for details. Default is 'draw'.
1242
1243        Returns
1244        -------
1245        None
1246        """
1247        self.filter(func=operator.methodcaller("get_area", mode=mode), min_value=min_value, max_value=max_value)
1248
1249    def filter_perimeter(self, min_value=None, max_value=None):
1250        """
1251        Filter the list of contours by their perimeters.
1252
1253        Only contours whose perimeters fall within the specified range are retained.
1254        The contour list is modified in place.
1255
1256        Parameters
1257        ----------
1258        min_value : float or None, optional
1259            The minimum allowed perimeter. If a contour's perimeter is less than this, it is discarded.
1260            If None, no lower bound is applied. Default is None.
1261        max_value : float or None, optional
1262            The maximum allowed perimeter. If a contour's perimeter is greater than this, it is discarded.
1263            If None, no upper bound is applied. Default is None.
1264
1265        Returns
1266        -------
1267        None
1268        """
1269        self.filter(func=operator.methodcaller("get_perimeter"), min_value=min_value, max_value=max_value)
1270
1271    def filter_circularity(self, min_value=None, max_value=None):
1272        """
1273        Filter the list of contours by their circularity.
1274
1275        Only contours whose circularities fall within the specified range are retained.
1276        The contour list is modified in place.
1277
1278        Parameters
1279        ----------
1280        min_value : float or None, optional
1281            The minimum allowed circularity. If a contour's circularity is less than this, it is discarded.
1282            If None, no lower bound is applied. Default is None.
1283        max_value : float or None, optional
1284            The maximum allowed circularity. If a contour's circularity is greater than this, it is discarded.
1285            If None, no upper bound is applied. Default is None.
1286
1287        Returns
1288        -------
1289        None
1290        """
1291        self.filter(func=operator.methodcaller("get_circularity"), min_value=min_value, max_value=max_value)
1292
1293    def find_largest(self, return_index=True):
1294        """
1295        Return the contour (or the contour index) with the largest area.
1296
1297        Parameters
1298        ----------
1299        return_index : bool, optional
1300            If `True`, return the index of the largest contour. Otherwise, return the contour object itself.
1301            Default is `True`.
1302
1303        Returns
1304        -------
1305        int or Contour or None
1306            The index of the largest contour if `return_index` is `True`, or the largest contour object if
1307            `return_index` is `False`. If no contour is found, returns None.
1308        """
1309        max_area = None
1310        argmax_area = None
1311        for (n_contour, contour) in enumerate(self.contours):
1312            area = contour.get_area()
1313            if (max_area is None) or (area > max_area):
1314                max_area = area
1315                argmax_area = n_contour
1316
1317        if argmax_area is None:
1318            return None
1319        else:
1320            if return_index:
1321                return argmax_area
1322            else:
1323                return self.contours[argmax_area]
1324
1325    def draw_all(self, image, colors=None, **kwargs):
1326        """
1327        Draw all the contours into an image using a different color for each contour.
1328
1329        Parameters
1330        ----------
1331        image : numpy.ndarray
1332            The image into which the contours will be drawn.
1333        colors : list of tuples, optional
1334            The colors to use for each contour. The length of the list must be equal to the number of contours.
1335            If not provided, random colors will be generated. Default is None.
1336        **kwargs
1337            Additional keyword arguments to pass to the `Contour.draw()` method.
1338
1339        Returns
1340        -------
1341        None
1342        """
1343        if colors is None:
1344            colors = tuple(dito.random_color() for _ in range(len(self)))
1345
1346        for (contour, color) in zip(self.contours, colors):
1347            contour.draw(image=image, color=color, **kwargs)

A class representing a list of contours.

It allows for easy filtering of contours by various properties and offers additional helper functions such as drawing.

Attributes
  • contours (list of Contour objects): The list of contours stored in the ContourList object.
ContourList(contours_)
1064    def __init__(self, contours_):
1065        """
1066        Parameters
1067        ----------
1068        contours_ : list of Contour objects
1069            The list of contours to be stored.
1070        """
1071        self.contours = contours_
Parameters
  • contours_ (list of Contour objects): The list of contours to be stored.
contours
def copy(self):
1131    def copy(self):
1132        """
1133        Return a copy of the ContourList object.
1134
1135        Returns
1136        -------
1137        object
1138            A copy of the ContourList object.
1139        """
1140        contours_copy = [contour.copy() for contour in self.contours]
1141        return ContourList(contours_=contours_copy)

Return a copy of the ContourList object.

Returns
  • object: A copy of the ContourList object.
def filter(self, func, min_value=None, max_value=None):
1143    def filter(self, func, min_value=None, max_value=None):
1144        """
1145        Filter the contour list based on a given function and range of values.
1146
1147        Only contours whose function values fall within the specified range are retained.
1148        The contour list is modified in place.
1149
1150        Parameters
1151        ----------
1152        func : function
1153            The function used to extract a value from each contour. It must return a value which can be compared against
1154            `min_value` and/or `max_value`.
1155        min_value : float or None, optional
1156            The minimum value of the extracted value for a contour to be kept. Contours with extracted values lower than
1157            this will be removed. If None, no minimum filter is applied. Default is None.
1158        max_value : float or None, optional
1159            The maximum value of the extracted value for a contour to be kept. Contours with extracted values higher than
1160            this will be removed. If None, no maximum filter is applied. Default is None.
1161
1162        Returns
1163        -------
1164        None
1165        """
1166        if (min_value is None) and (max_value is None):
1167            # nothing to do
1168            return
1169
1170        # filter
1171        contours_filtered = []
1172        for contour in self.contours:
1173            value = func(contour)
1174            if (min_value is not None) and (value < min_value):
1175                continue
1176            if (max_value is not None) and (value > max_value):
1177                continue
1178            contours_filtered.append(contour)
1179        self.contours = contours_filtered

Filter the contour list based on a given function and range of values.

Only contours whose function values fall within the specified range are retained. The contour list is modified in place.

Parameters
  • func (function): The function used to extract a value from each contour. It must return a value which can be compared against min_value and/or max_value.
  • min_value (float or None, optional): The minimum value of the extracted value for a contour to be kept. Contours with extracted values lower than this will be removed. If None, no minimum filter is applied. Default is None.
  • max_value (float or None, optional): The maximum value of the extracted value for a contour to be kept. Contours with extracted values higher than this will be removed. If None, no maximum filter is applied. Default is None.
Returns
  • None
def filter_center_x(self, min_value=None, max_value=None):
1181    def filter_center_x(self, min_value=None, max_value=None):
1182        """
1183        Filter the list of contours by the x-coordinate of their centers.
1184
1185        Only contours whose center x-coordinates fall within the specified range are retained.
1186        The contour list is modified in place.
1187
1188        Parameters
1189        ----------
1190        min_value : float or None, optional
1191            The minimum allowed value of the x-coordinate. If a contour's center x-coordinate is less than this, it is
1192            discarded. If None, no lower bound is applied. Default is None.
1193        max_value : float or None, optional
1194            The maximum allowed value of the x-coordinate. If a contour's center x-coordinate is greater than this, it is
1195            discarded. If None, no upper bound is applied. Default is None.
1196
1197        Returns
1198        -------
1199        None
1200        """
1201        self.filter(func=operator.methodcaller("get_center_x"), min_value=min_value, max_value=max_value)

Filter the list of contours by the x-coordinate of their centers.

Only contours whose center x-coordinates fall within the specified range are retained. The contour list is modified in place.

Parameters
  • min_value (float or None, optional): The minimum allowed value of the x-coordinate. If a contour's center x-coordinate is less than this, it is discarded. If None, no lower bound is applied. Default is None.
  • max_value (float or None, optional): The maximum allowed value of the x-coordinate. If a contour's center x-coordinate is greater than this, it is discarded. If None, no upper bound is applied. Default is None.
Returns
  • None
def filter_center_y(self, min_value=None, max_value=None):
1203    def filter_center_y(self, min_value=None, max_value=None):
1204        """
1205        Filter the list of contours by the y-coordinate of their centers.
1206
1207        Only contours whose center y-coordinates fall within the specified range are retained.
1208        The contour list is modified in place.
1209
1210        Parameters
1211        ----------
1212        min_value : float or None, optional
1213            The minimum allowed value of the y-coordinate. If a contour's center y-coordinate is less than this, it is
1214            discarded. If None, no lower bound is applied. Default is None.
1215        max_value : float or None, optional
1216            The maximum allowed value of the y-coordinate. If a contour's center y-coordinate is greater than this, it is
1217            discarded. If None, no upper bound is applied. Default is None.
1218
1219        Returns
1220        -------
1221        None
1222        """
1223        self.filter(func=operator.methodcaller("get_center_y"), min_value=min_value, max_value=max_value)

Filter the list of contours by the y-coordinate of their centers.

Only contours whose center y-coordinates fall within the specified range are retained. The contour list is modified in place.

Parameters
  • min_value (float or None, optional): The minimum allowed value of the y-coordinate. If a contour's center y-coordinate is less than this, it is discarded. If None, no lower bound is applied. Default is None.
  • max_value (float or None, optional): The maximum allowed value of the y-coordinate. If a contour's center y-coordinate is greater than this, it is discarded. If None, no upper bound is applied. Default is None.
Returns
  • None
def filter_area(self, min_value=None, max_value=None, mode='draw'):
1225    def filter_area(self, min_value=None, max_value=None, mode="draw"):
1226        """
1227        Filter the list of contours by their area.
1228
1229        Only contours whose areas fall within the specified range are retained.
1230        The contour list is modified in place.
1231
1232        Parameters
1233        ----------
1234        min_value : float or None, optional
1235            The minimum allowed area. If a contour's area is less than this, it is discarded. If None, no lower bound
1236            is applied. Default is None.
1237        max_value : float or None, optional
1238            The maximum allowed area. If a contour's area is greater than this, it is discarded. If None, no upper bound
1239            is applied. Default is None.
1240        mode : str, optional
1241            The mode to use for computing the area. See `get_area` for details. Default is 'draw'.
1242
1243        Returns
1244        -------
1245        None
1246        """
1247        self.filter(func=operator.methodcaller("get_area", mode=mode), min_value=min_value, max_value=max_value)

Filter the list of contours by their area.

Only contours whose areas fall within the specified range are retained. The contour list is modified in place.

Parameters
  • min_value (float or None, optional): The minimum allowed area. If a contour's area is less than this, it is discarded. If None, no lower bound is applied. Default is None.
  • max_value (float or None, optional): The maximum allowed area. If a contour's area is greater than this, it is discarded. If None, no upper bound is applied. Default is None.
  • mode (str, optional): The mode to use for computing the area. See get_area for details. Default is 'draw'.
Returns
  • None
def filter_perimeter(self, min_value=None, max_value=None):
1249    def filter_perimeter(self, min_value=None, max_value=None):
1250        """
1251        Filter the list of contours by their perimeters.
1252
1253        Only contours whose perimeters fall within the specified range are retained.
1254        The contour list is modified in place.
1255
1256        Parameters
1257        ----------
1258        min_value : float or None, optional
1259            The minimum allowed perimeter. If a contour's perimeter is less than this, it is discarded.
1260            If None, no lower bound is applied. Default is None.
1261        max_value : float or None, optional
1262            The maximum allowed perimeter. If a contour's perimeter is greater than this, it is discarded.
1263            If None, no upper bound is applied. Default is None.
1264
1265        Returns
1266        -------
1267        None
1268        """
1269        self.filter(func=operator.methodcaller("get_perimeter"), min_value=min_value, max_value=max_value)

Filter the list of contours by their perimeters.

Only contours whose perimeters fall within the specified range are retained. The contour list is modified in place.

Parameters
  • min_value (float or None, optional): The minimum allowed perimeter. If a contour's perimeter is less than this, it is discarded. If None, no lower bound is applied. Default is None.
  • max_value (float or None, optional): The maximum allowed perimeter. If a contour's perimeter is greater than this, it is discarded. If None, no upper bound is applied. Default is None.
Returns
  • None
def filter_circularity(self, min_value=None, max_value=None):
1271    def filter_circularity(self, min_value=None, max_value=None):
1272        """
1273        Filter the list of contours by their circularity.
1274
1275        Only contours whose circularities fall within the specified range are retained.
1276        The contour list is modified in place.
1277
1278        Parameters
1279        ----------
1280        min_value : float or None, optional
1281            The minimum allowed circularity. If a contour's circularity is less than this, it is discarded.
1282            If None, no lower bound is applied. Default is None.
1283        max_value : float or None, optional
1284            The maximum allowed circularity. If a contour's circularity is greater than this, it is discarded.
1285            If None, no upper bound is applied. Default is None.
1286
1287        Returns
1288        -------
1289        None
1290        """
1291        self.filter(func=operator.methodcaller("get_circularity"), min_value=min_value, max_value=max_value)

Filter the list of contours by their circularity.

Only contours whose circularities fall within the specified range are retained. The contour list is modified in place.

Parameters
  • min_value (float or None, optional): The minimum allowed circularity. If a contour's circularity is less than this, it is discarded. If None, no lower bound is applied. Default is None.
  • max_value (float or None, optional): The maximum allowed circularity. If a contour's circularity is greater than this, it is discarded. If None, no upper bound is applied. Default is None.
Returns
  • None
def find_largest(self, return_index=True):
1293    def find_largest(self, return_index=True):
1294        """
1295        Return the contour (or the contour index) with the largest area.
1296
1297        Parameters
1298        ----------
1299        return_index : bool, optional
1300            If `True`, return the index of the largest contour. Otherwise, return the contour object itself.
1301            Default is `True`.
1302
1303        Returns
1304        -------
1305        int or Contour or None
1306            The index of the largest contour if `return_index` is `True`, or the largest contour object if
1307            `return_index` is `False`. If no contour is found, returns None.
1308        """
1309        max_area = None
1310        argmax_area = None
1311        for (n_contour, contour) in enumerate(self.contours):
1312            area = contour.get_area()
1313            if (max_area is None) or (area > max_area):
1314                max_area = area
1315                argmax_area = n_contour
1316
1317        if argmax_area is None:
1318            return None
1319        else:
1320            if return_index:
1321                return argmax_area
1322            else:
1323                return self.contours[argmax_area]

Return the contour (or the contour index) with the largest area.

Parameters
  • return_index (bool, optional): If True, return the index of the largest contour. Otherwise, return the contour object itself. Default is True.
Returns
  • int or Contour or None: The index of the largest contour if return_index is True, or the largest contour object if return_index is False. If no contour is found, returns None.
def draw_all(self, image, colors=None, **kwargs):
1325    def draw_all(self, image, colors=None, **kwargs):
1326        """
1327        Draw all the contours into an image using a different color for each contour.
1328
1329        Parameters
1330        ----------
1331        image : numpy.ndarray
1332            The image into which the contours will be drawn.
1333        colors : list of tuples, optional
1334            The colors to use for each contour. The length of the list must be equal to the number of contours.
1335            If not provided, random colors will be generated. Default is None.
1336        **kwargs
1337            Additional keyword arguments to pass to the `Contour.draw()` method.
1338
1339        Returns
1340        -------
1341        None
1342        """
1343        if colors is None:
1344            colors = tuple(dito.random_color() for _ in range(len(self)))
1345
1346        for (contour, color) in zip(self.contours, colors):
1347            contour.draw(image=image, color=color, **kwargs)

Draw all the contours into an image using a different color for each contour.

Parameters
  • image (numpy.ndarray): The image into which the contours will be drawn.
  • colors (list of tuples, optional): The colors to use for each contour. The length of the list must be equal to the number of contours. If not provided, random colors will be generated. Default is None.
  • **kwargs: Additional keyword arguments to pass to the Contour.draw() method.
Returns
  • None
class ContourFinder(ContourList):
1350class ContourFinder(ContourList):
1351    """
1352    Extension of `ContourList` with support to find contours in an image.
1353
1354    Between OpenCV 3.x and 4.x, the API of `cv2.findContours` changed. This
1355    class is basically a wrapper for `cv2.findContours` which works for both
1356    versions.
1357
1358    As a subclass of `ContourList`, it inherits all the contour list
1359    manipulation methods defined by its parent class.
1360
1361    Attributes
1362    ----------
1363    image : numpy.ndarray
1364        The input image, stored as a copy of the input argument.
1365    contours : list of Contour
1366        The contours found in the input image.
1367    """
1368
1369    def __init__(self, image):
1370        """
1371        Initialize a `ContourFinder` instance and find contours in the given `image`.
1372
1373        Parameters
1374        ----------
1375        image : numpy.ndarray
1376            The image from which to find contours. Will be converted to dtype `numpy.uint8`
1377        """
1378        self.image = image.copy()
1379        if self.image.dtype == bool:
1380            self.image = dito.core.convert(image=self.image, dtype=np.uint8)
1381        contours_ = self.find_contours(image=self.image)
1382        super().__init__(contours_=contours_)
1383
1384    @staticmethod
1385    def find_contours(image):
1386        """
1387        Find the contours in the given `image`.
1388
1389        Parameters
1390        ----------
1391        image : numpy.ndarray
1392            The image from which to find contours.
1393
1394        Returns
1395        -------
1396        list of Contour
1397            A list of instances of the `Contour` class, one for each contour found in the input image.
1398        """
1399
1400        # find raw contours
1401        result = cv2.findContours(image=image, mode=cv2.RETR_LIST, method=cv2.CHAIN_APPROX_NONE)
1402
1403        # compatible with OpenCV 3.x and 4.x, see https://stackoverflow.com/a/53909713/1913780
1404        contours_raw = result[-2]
1405
1406        # return tuple of instances of class `Contour`
1407        return [Contour(points=contour_raw[:, 0, :]) for contour_raw in contours_raw]

Extension of ContourList with support to find contours in an image.

Between OpenCV 3.x and 4.x, the API of cv2.findContours changed. This class is basically a wrapper for cv2.findContours which works for both versions.

As a subclass of ContourList, it inherits all the contour list manipulation methods defined by its parent class.

Attributes
  • image (numpy.ndarray): The input image, stored as a copy of the input argument.
  • contours (list of Contour): The contours found in the input image.
ContourFinder(image)
1369    def __init__(self, image):
1370        """
1371        Initialize a `ContourFinder` instance and find contours in the given `image`.
1372
1373        Parameters
1374        ----------
1375        image : numpy.ndarray
1376            The image from which to find contours. Will be converted to dtype `numpy.uint8`
1377        """
1378        self.image = image.copy()
1379        if self.image.dtype == bool:
1380            self.image = dito.core.convert(image=self.image, dtype=np.uint8)
1381        contours_ = self.find_contours(image=self.image)
1382        super().__init__(contours_=contours_)

Initialize a ContourFinder instance and find contours in the given image.

Parameters
  • image (numpy.ndarray): The image from which to find contours. Will be converted to dtype numpy.uint8
image
@staticmethod
def find_contours(image):
1384    @staticmethod
1385    def find_contours(image):
1386        """
1387        Find the contours in the given `image`.
1388
1389        Parameters
1390        ----------
1391        image : numpy.ndarray
1392            The image from which to find contours.
1393
1394        Returns
1395        -------
1396        list of Contour
1397            A list of instances of the `Contour` class, one for each contour found in the input image.
1398        """
1399
1400        # find raw contours
1401        result = cv2.findContours(image=image, mode=cv2.RETR_LIST, method=cv2.CHAIN_APPROX_NONE)
1402
1403        # compatible with OpenCV 3.x and 4.x, see https://stackoverflow.com/a/53909713/1913780
1404        contours_raw = result[-2]
1405
1406        # return tuple of instances of class `Contour`
1407        return [Contour(points=contour_raw[:, 0, :]) for contour_raw in contours_raw]

Find the contours in the given image.

Parameters
  • image (numpy.ndarray): The image from which to find contours.
Returns
  • list of Contour: A list of instances of the Contour class, one for each contour found in the input image.
def contours(image):
1410def contours(image):
1411    """
1412    Find and return all contours in the given image.
1413
1414    It is a convenience wrapper for `ContourFinder`.
1415
1416    Parameters
1417    ----------
1418    image : numpy.ndarray
1419        The image in which to find the contours.
1420
1421    Returns
1422    -------
1423    ContourList
1424        An instance of the `ContourList` class containing all the found contours.
1425    """
1426    contour_finder = ContourFinder(image=image)
1427    return contour_finder.contours

Find and return all contours in the given image.

It is a convenience wrapper for ContourFinder.

Parameters
  • image (numpy.ndarray): The image in which to find the contours.
Returns
  • ContourList: An instance of the ContourList class containing all the found contours.
class VoronoiPartition(ContourList):
1430class VoronoiPartition(ContourList):
1431    """
1432    Extension of `ContourList` where the contours are derived as facets of the Voronoi partition of a set of given points.
1433
1434    As a subclass of `ContourList`, it inherits all the contour list
1435    manipulation methods defined by its parent class.
1436
1437    Attributes
1438    ----------
1439    contours : list of Contour
1440        The list of Voronoi facets, each represented as a `Contour` object.
1441    """
1442
1443    def __init__(self, image_size, points):
1444        """
1445        Initialize a `VoronoiPartition` instance from a set of input points.
1446
1447        Parameters
1448        ----------
1449        image_size : tuple of int
1450            The size of the image (width, height) in pixels.
1451        points : numpy.ndarray
1452            The array of input points for which to compute the Voronoi partition. Each row represents a point (x, y).
1453        """
1454        contours_ = self.get_facets(image_size=image_size, points=points)
1455        super().__init__(contours_=contours_)
1456
1457    @staticmethod
1458    def get_facets(image_size, points):
1459        """
1460        Calculate the Voronoi partition based on the given points.
1461
1462        Parameters
1463        ----------
1464        image_size : tuple of int
1465            The size of the image (width, height) in pixels.
1466        points : numpy.ndarray
1467            The array of input points for which to compute the Voronoi partition. Each row represents a point (x, y).
1468
1469        Returns
1470        -------
1471        list of Contour
1472            The list of Voronoi facets, each represented as a `Contour` object.
1473        """
1474        subdiv = cv2.Subdiv2D((0, 0, image_size[0], image_size[1]))
1475        for point in points:
1476            subdiv.insert(pt=point)
1477        (voronoi_facets, voronoi_centers) = subdiv.getVoronoiFacetList(idx=[])
1478        return [Contour(voronoi_facet) for voronoi_facet in voronoi_facets]

Extension of ContourList where the contours are derived as facets of the Voronoi partition of a set of given points.

As a subclass of ContourList, it inherits all the contour list manipulation methods defined by its parent class.

Attributes
  • contours (list of Contour): The list of Voronoi facets, each represented as a Contour object.
VoronoiPartition(image_size, points)
1443    def __init__(self, image_size, points):
1444        """
1445        Initialize a `VoronoiPartition` instance from a set of input points.
1446
1447        Parameters
1448        ----------
1449        image_size : tuple of int
1450            The size of the image (width, height) in pixels.
1451        points : numpy.ndarray
1452            The array of input points for which to compute the Voronoi partition. Each row represents a point (x, y).
1453        """
1454        contours_ = self.get_facets(image_size=image_size, points=points)
1455        super().__init__(contours_=contours_)

Initialize a VoronoiPartition instance from a set of input points.

Parameters
  • image_size (tuple of int): The size of the image (width, height) in pixels.
  • points (numpy.ndarray): The array of input points for which to compute the Voronoi partition. Each row represents a point (x, y).
@staticmethod
def get_facets(image_size, points):
1457    @staticmethod
1458    def get_facets(image_size, points):
1459        """
1460        Calculate the Voronoi partition based on the given points.
1461
1462        Parameters
1463        ----------
1464        image_size : tuple of int
1465            The size of the image (width, height) in pixels.
1466        points : numpy.ndarray
1467            The array of input points for which to compute the Voronoi partition. Each row represents a point (x, y).
1468
1469        Returns
1470        -------
1471        list of Contour
1472            The list of Voronoi facets, each represented as a `Contour` object.
1473        """
1474        subdiv = cv2.Subdiv2D((0, 0, image_size[0], image_size[1]))
1475        for point in points:
1476            subdiv.insert(pt=point)
1477        (voronoi_facets, voronoi_centers) = subdiv.getVoronoiFacetList(idx=[])
1478        return [Contour(voronoi_facet) for voronoi_facet in voronoi_facets]

Calculate the Voronoi partition based on the given points.

Parameters
  • image_size (tuple of int): The size of the image (width, height) in pixels.
  • points (numpy.ndarray): The array of input points for which to compute the Voronoi partition. Each row represents a point (x, y).
Returns
  • list of Contour: The list of Voronoi facets, each represented as a Contour object.
def voronoi(image_size, points):
1481def voronoi(image_size, points):
1482    """
1483    Compute the Voronoi partition of a set of input points.
1484
1485    It is a convenience wrapper for `VoronoiPartition`.
1486
1487    Parameters
1488    ----------
1489    image_size : tuple of int
1490        The size of the image (width, height) in pixels.
1491    points : numpy.ndarray
1492        The array of input points. Each row represents a point (x, y).
1493
1494    Returns
1495    -------
1496    list of Contour
1497        The list of Voronoi facets, each represented as a `Contour` object.
1498    """
1499    voronoi_partition = VoronoiPartition(image_size=image_size, points=points)
1500    return voronoi_partition.contours

Compute the Voronoi partition of a set of input points.

It is a convenience wrapper for VoronoiPartition.

Parameters
  • image_size (tuple of int): The size of the image (width, height) in pixels.
  • points (numpy.ndarray): The array of input points. Each row represents a point (x, y).
Returns
  • list of Contour: The list of Voronoi facets, each represented as a Contour object.