Source code for acia.utils

"""Global utilities"""

from __future__ import annotations

import numpy as np
import rasterio
import shapely
from rasterio import features
from shapely.geometry import MultiPolygon, Polygon


[docs] def lut_mapping(image, in_min, in_max, out_min, out_max, dtype=None): mapped_data = (np.clip(image, in_min, in_max) - in_min) / (in_max - in_min) * ( out_max - out_min ) + out_min if dtype: mapped_data = mapped_data.astype(dtype) return mapped_data
[docs] def pairwise_distances(points: np.ndarray) -> list[float]: distances: list[float] = [] if len(points) == 0: return distances for a, b in zip(points, points[1:], strict=False): distances.append(float(np.linalg.norm(a - b))) return distances
[docs] def mask_to_polygons(mask: np.ndarray) -> Polygon | MultiPolygon | None: """Convert a mask to a Polygon or Multipolygon Args: mask (np.ndarray): Binary mask for an object Returns: shapely.geometry.Polygon | shapely.geometry.MultiPolygon | None: Extracted polygon structure, or None if the mask is empty """ all_polygons = [] for shape, _ in features.shapes(mask.astype(np.int16), mask=(mask > 0)): all_polygons.append(shapely.geometry.shape(shape)) if len(all_polygons) == 0: return None if len(all_polygons) > 1: all_polygons = shapely.geometry.MultiPolygon(all_polygons) else: all_polygons = all_polygons[0] if not all_polygons.is_valid: all_polygons = all_polygons.buffer(0) return all_polygons
[docs] def largest_polygon( polygon: Polygon | MultiPolygon | None, ) -> Polygon | None: """Reduce a possibly-multi-part polygon to its single largest-area part. :func:`mask_to_polygons` returns a ``MultiPolygon`` whenever an object's mask has disconnected components -- a cell the segmentation split in two, a stray speck sharing the cell's label, or a self-intersecting outline that ``buffer(0)`` repaired into several pieces. Anything that needs *one* closed outline (a contour's coordinates, a drawn shape) has to pick a part, and the largest is the one that represents the object. Note that this discards the smaller parts, so a caller that persists or measures the result is losing whatever area they held; callers in a position to say so should report it. :attr:`~acia.base.Instance.area` is unaffected, being computed from the mask rather than the polygon. Args: polygon: A ``Polygon``, a ``MultiPolygon``, or ``None``. Returns: shapely.geometry.Polygon | None: The largest constituent polygon, ``polygon`` itself when it is already a single ``Polygon``, or ``None`` when ``polygon`` is ``None`` or holds no parts. """ if polygon is None: return None if isinstance(polygon, MultiPolygon): parts = list(polygon.geoms) if not parts: return None return max(parts, key=lambda part: part.area) return polygon
[docs] def multi_mask_to_polygons( mask: np.ndarray, ) -> list[tuple[int, Polygon | MultiPolygon]]: unique_values = np.unique(mask) instance_ids = unique_values[unique_values > 0] polygons = [] for instance_id in instance_ids: polygons.append((instance_id, mask_to_polygons(mask == instance_id))) return polygons
[docs] def polygon_to_mask(polygon, height: int, width: int): """Converts a polygon to a mask Args: polygon (_type_): shapely polygon or multipolygon height (int): height of the mask width (int): width of the mask Returns: (np.ndarray): boolean mask """ return rasterio.features.rasterize( [polygon], out_shape=(height, width), ).astype(bool)
[docs] class ScaleBar: """Scalebar class""" #: width of the rendered bar in pixels (set by concrete subclasses) pixelWidth: int
[docs] def draw(self, image: np.ndarray, xstart: int, ystart: int): raise NotImplementedError("Do not use the base class")