Contour Detection#
DataLab provides a “Contour Detection” feature which is based on the marching cubes algorithm.
![../../_images/contour_app_param.png](../../_images/contour_app_param.png)
Contour detection parameters.#
- How to use the feature:
Create or open an image in DataLab workspace
Eventually create a ROI around the target area
Select “Contour detection” in “Analysis” menu
Enter parameter “Shape” (“Ellipse”, “Circle” or “Polygon”)
![../../_images/contour_app_results.png](../../_images/contour_app_results.png)
Contour detection results (see test “contour_app.py”)#
- Results are shown in a table:
Each row is associated to a contour
First column shows the ROI index (0 if no ROI is defined on input image)
Other columns show contour coordinates: 4 columns for circles (coordinates of diameter), 8 columns for ellipses (coordinates of diameters)
![../../_images/contour_app.png](../../_images/contour_app.png)
Example of contour detection.#
- The contour detection algorithm works in the following way:
First, iso-valued contours are computed (implementation based on skimage.measure.find_contours.find_contours)
Then, each contour is fitted to the closest ellipse (or circle)
Feature is based on get_contour_shapes
function
from cdl.algorithms
module:
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)