Détection de contours#

DataLab fournit une fonctionnalté de « Détection de contours » qui est basée sur l’algorithme des marching cubes

../../_images/contour_app_param.png

Paramètres de détection de contours.#

La fonctionnalité s’utilise de la façon suivante :
  • Créer ou ouvrir une image dans l’espace de travail de DataLab

  • Créer éventuellement une ROI autour de la zone cible de l’image

  • Sélectionner « Détection de contours » dans le menu Analyse »

  • Saisir le paramètre « Forme » (« Ellipse », « Cercle » ou « Polygone »)

../../_images/contour_app_results.png

Résultats de la détection de contours (cf. test « contour_app.py »)#

Les résultats sont affichés dans un tableau :
  • Chaque ligne est associée à un contour

  • La première colonne contient l’indice de la ROI (0 si aucune ROI n’est définie)

  • Les colonnes suivantes présentent les coordonnées des contours : 4 colonnes pour les cercles (coordonnées du diamètre) et 8 colonnes pour les ellipses (coordonnées des diamètres)

../../_images/contour_app.png

Exemple de détection de contours.#

L’algorithme de détection de contours fonctionne de la manière suivante :

La fonctionnalité est basée sur la fonction get_contour_shapes du module cdl.computation :

def get_contour_shapes(
    data: np.ndarray | ma.MaskedArray,
    shape: Literal["circle", "ellipse", "polygon"] = "ellipse",
    level: float = 0.5,
) -> np.ndarray:
    """Find iso-valued contours in a 2D array, above relative level (.5 means FWHM),
    then fit contours with shape ('ellipse' or 'circle')

    Args:
        data: Input data
        shape: Shape to fit. Default is 'ellipse'
        level: Relative level (default: 0.5)

    Returns:
        Coordinates of shapes
    """
    # pylint: disable=too-many-locals
    assert shape in ("circle", "ellipse", "polygon")
    contours = measure.find_contours(data, level=get_absolute_level(data, level))
    coords = []
    for contour in contours:
        # `contour` is a (N, 2) array (rows, cols): we need to check if all those
        # coordinates are masked: if so, we skip this contour
        if isinstance(data, ma.MaskedArray) and np.all(
            data.mask[contour[:, 0].astype(int), contour[:, 1].astype(int)]
        ):
            continue
        if shape == "circle":
            model = measure.CircleModel()
            if model.estimate(contour):
                yc, xc, r = model.params
                if r <= 1.0:
                    continue
                coords.append([xc, yc, r])
        elif shape == "ellipse":
            model = measure.EllipseModel()
            if model.estimate(contour):
                yc, xc, b, a, theta = model.params
                if a <= 1.0 or b <= 1.0:
                    continue
                coords.append([xc, yc, a, b, theta])
        elif shape == "polygon":
            # `contour` is a (N, 2) array (rows, cols): we need to convert it
            # to a list of x, y coordinates flattened in a single list
            coords.append(contour[:, ::-1].flatten())
        else:
            raise NotImplementedError(f"Invalid contour model {model}")
    if shape == "polygon":
        # `coords` is a list of arrays of shape (N, 2) where N is the number of points
        # that can vary from one array to another, so we need to padd with NaNs each
        # array to get a regular array:
        max_len = max(coord.shape[0] for coord in coords)
        arr = np.full((len(coords), max_len), np.nan)
        for i_row, coord in enumerate(coords):
            arr[i_row, : coord.shape[0]] = coord
        return arr
    return np.array(coords)