Module brevettiai.platform.models.annotation

Expand source code
from itertools import groupby
from typing import Dict, Any, Tuple
from uuid import UUID, uuid4

import cv2
import numpy as np
import pandas as pd
import pydantic.color as pydantic_color
from pydantic import BaseModel, Field, conint, constr, validator
from pydantic.typing import List, Union
from shapely.geometry import Polygon, LineString, Point, box, MultiPolygon
from shapely.ops import unary_union

from brevettiai.data.image import feature_calculator
from brevettiai.io import io_tools
from brevettiai.utils.polygon_utils import cv2_contour_to_shapely, simplify_polygon


def flatten_structure(x, name='', out=None):
    out = {} if out is None else out
    x = x.dict() if isinstance(x, BaseModel) else x

    if type(x) is dict:
        for a in x:
            flatten_structure(x[a], name + a + '.', out)
    elif isinstance(x, (list, tuple, np.ndarray)):
        i = 0
        for a in x:
            flatten_structure(a, name + str(i) + '.', out)
            i += 1
    else:
        out[name[:-1]] = x
    return out


class ArrayMeta(type):
    def __getitem__(self, t):
        return type('Array', (PointsArray,), {'inner_type': t})


class PointsArray(np.ndarray, metaclass=ArrayMeta):
    @classmethod
    def from_polygon(cls, polygon):
        return np.stack(polygon.exterior.xy, -1)[:-1].view(cls)

    @classmethod
    def __get_validators__(cls):
        yield cls.validate_type

    @classmethod
    def validate_type(cls, val):
        if isinstance(val, np.ndarray):
            assert val.ndim == 2
            assert val.shape[1] == 2
            array = val.astype(cls.inner_type)
        elif isinstance(val, (list, tuple)):
            try:
                array = np.array(tuple((p["x"], p["y"]) for p in val), dtype=cls.inner_type)
            except TypeError:
                array = np.array(tuple((p[0], p[0]) for p in val), dtype=cls.inner_type)
        else:
            raise NotImplementedError("type not implemented")
        return array.view(PointsArray)

    def dict(self):
        return [{"x": p[0].tolist(), "y": p[1].tolist()} for p in self]


class Color(pydantic_color.Color):
    def __init__(self, value: pydantic_color.ColorType) -> None:
        if isinstance(value, str):
            value = value.replace("hsla", "hsl")
        super().__init__(value)

    @classmethod
    def from_hsl(cls, h, s, l, a=None):
        r, g, b = pydantic_color.hls_to_rgb(h, l, s)
        return cls((255*r, 255*g, 255*b, a))


class Annotation(BaseModel):
    type: str
    label: str
    color: Color
    uuid: UUID = Field(default_factory=uuid4)
    visibility: conint(ge=-1, le=3) = -1
    severity: conint(ge=-1, le=3) = -1
    features: Dict[str, Any] = Field(default_factory=dict)

    class Config:
        validate_assignment = True

    @validator("visibility", "severity", pre=True, allow_reuse=True)
    def default_negative_1(cls, v, field):
        return -1 if v is None else v


class ClassAnnotation(Annotation):
    type: constr(regex="^class$") = "class"


class Mask(np.ndarray):
    @classmethod
    def __get_validators__(cls):
        yield cls.validate_type

    @classmethod
    def validate_type(cls, val):
        assert isinstance(val, np.ndarray)
        assert val.ndim == 2
        array = val.astype(np.int32)
        return array.view(Mask)


class GroundTruth(BaseModel):
    label: str
    iou: float = -1
    coverage: float = -1
    severity: float = -1

    def dict(self, **kwargs):
        return {"label": self.label, "iou": self.iou}

    @classmethod
    def from_annotation(cls, annotation, target=None):
        iou = -1 if target is None else target.iou(annotation)
        return cls(label=annotation.label, iou=iou)


class PointsAnnotation(Annotation):
    points: PointsArray[np.float32]
    parent: UUID = None
    is_hole: bool = False
    ground_truth: GroundTruth = None
    _shapely: Union[None, Polygon] = None
    _centroid: Union[None, Tuple[int, int]] = None
    _moments: Union[None, Tuple[float, float, float]] = None
    _hu_moments: Tuple[float, float, float, float, float, float, float] = None
    _mask = None

    class Config:
        arbitrary_types_allowed = True
        underscore_attrs_are_private = True

    def clear_calculated(self):
        self._shapely = None
        self._centroid = None
        self._moments = None
        self._mask = None

    def transform_points(self, matrix):
        padded = np.pad(np.array(self.points), ((0, 0), (0, 1)), constant_values=1)
        t_points = np.matmul(matrix, padded.T).T[..., :2]
        self.points = t_points
        self.clear_calculated()

    def dict(self, *args, **kwargs):
        cfg = BaseModel.dict(self, *args, **kwargs)
        if "points" in cfg:
            cfg["points"] = cfg["points"].dict()
        return cfg

    @property
    def bbox(self):
        return np.concatenate((self.points.min(axis=0), self.points.max(axis=0)))

    @property
    def path_length(self):
        return cv2.arcLength(self.points, True)
        #return sum((self._dist(a, b) for a, b in zip(points, np.roll(points, 1))))

    @property
    def area(self):
        return cv2.contourArea(self.points)
        #return sum([a['x'] * b['y'] - a['y'] * b['x'] for (a, b) in zip(points, np.roll(points, 1))]) / 2

    @property
    def centroid(self):
        if self._centroid is None:
            m = self.moments
            m00 = max(m['m00'], 1e-6)
            self._centroid = m['m10']/m00, m['m01']/m00
        return self._centroid

    @property
    def moments(self):
        if self._moments is None:
            self._moments = cv2.moments(self.points)
        return self._moments

    @property
    def hu_moments(self):
        if self._hu_moments is None:
            self._hu_moments = cv2.HuMoments(self.moments)
        return self._hu_moments

    @property
    def mask(self):
        if self._mask is None:
            bbox = self.bbox.astype(np.int)
            mask = np.zeros((bbox[2:] - bbox[:2]).astype(np.int32)[::-1])
            cnt = (self.points - np.amin(self.points, axis=0))[:, np.newaxis, :]
            cv2.drawContours(mask, np.array([cnt]).astype(np.int32), -1, 1, cv2.FILLED)
            self._mask = mask.T
        return self._mask

    @property
    def polygon(self):
        if self._shapely is None:
            self._shapely = Polygon(np.concatenate((self.points, self.points[0, None])))
        return self._shapely

    def fix_polygon(self):
        self._shapely = self.polygon.buffer(0)
        return self

    def sample_points(self, tries=100000):
        _min, _max = self.points.min(axis=0), self.points.max(axis=0)
        _range = (_max - _min)
        for i in range(tries):
            p = (np.random.rand(2) * _range) + _min
            if cv2.pointPolygonTest(self.points, tuple(p), False) >= 0:
                yield np.round(p).astype(np.int)

    def intersection(self, p2):
        if isinstance(p2, PointsAnnotation):
            p2 = p2.polygon
        try:
            return self.polygon.intersection(p2).area
        except Exception:
            return 0

    def iou(self, p2):
        if isinstance(p2, PointsAnnotation):
            p2 = p2.polygon
        inters = self.intersection(p2)
        return inters / (self.polygon.area + p2.area - inters)

    def flat_features(self):
        return flatten_structure(self.features)


class PolygonAnnotation(PointsAnnotation):
    type: constr(regex="^polygon$") = "polygon"


class RectangleAnnotation(PointsAnnotation):
    type: constr(regex="^rectangle$") = "rectangle"

    @property
    def contour(self):
        pt_list = np.array(tuple(map(tuple, self.points)), dtype=np.float32)
        return np.array([pt_list[[0, 0, 1, 1], 0], pt_list[[0, 1, 1, 0], 1]]).T

    @property
    def polygon(self):
        if self._shapely is None:
            self._shapely = box(*self.points[:2].flatten())
        return self._shapely


class LineAnnotation(PointsAnnotation):
    type: constr(regex="^line$") = "line"


class PointAnnotation(PointsAnnotation):
    type: constr(regex="^point$") = "point"

    def sample_points(self, tries=100000):
        p = self.points.astype(np.int)[0]
        for i in range(tries):
            yield p


color_list = 'blue', 'cyan', 'goldenrod', 'green', 'magenta', 'orange', 'red', 'violet'


def sub_ious(annotation, polygons):
    iter_ = (polygons if isinstance(polygons, MultiPolygon) else [polygons])
    return max(annotation.iou(polygon) for polygon in iter_)


class ImageAnnotation(BaseModel):
    annotations: List[Union[PolygonAnnotation, RectangleAnnotation, LineAnnotation, PointAnnotation, ClassAnnotation]]
    source: dict = None
    image: dict = None

    def transform_annotation(self, matrix):
        for annotation in self.annotations:
            annotation.transform_points(matrix)

    def label_map(self):
        lbl_map = {}
        for ann in self.annotations:
            lbl_map.setdefault(ann.label, []).append(ann)
        return lbl_map

    def draw_contours_CHW(self, draw_buffer, label_space=None):
        if label_space is None:
            label_space = {k: v[0].color.as_rgb_tuple() for k, v in self.label_map().items()}
        return draw_contours_CHW(self.annotations, label_space=label_space, draw_buffer=draw_buffer)

    def intersections(self, right):
        left = self.annotations
        right = right.annotations if isinstance(right, ImageAnnotation) else right
        matrix = np.zeros((len(left), len(right)))
        for i, ann1 in enumerate(left):
            for j, ann2 in enumerate(right):
                matrix[i,j] = ann1.intersection(ann2)
        return matrix

    def fix_invalid(self):
        self.annotations = [a if a.polygon is not None and a.polygon.is_valid else a.fix_polygon() for a in self.annotations]
        self.annotations = [a for a in self.annotations if a.polygon is not None and not a.polygon.is_empty]
        return self

    def ious(self, right):
        left = self.annotations
        right = right.annotations if isinstance(right, ImageAnnotation) else right
        matrix = np.zeros((len(left), len(right)))
        for i, ann1 in enumerate(left):
            for j, ann2 in enumerate(right):
                matrix[i, j] = ann1.iou(ann2)
        return matrix

    def to_dataframe(self):
        return pd.DataFrame(map(lambda x: {
            **x.dict(),
            "Defect": "Unmatched" if x.ground_truth is None else x.ground_truth.label,
            "polygon": x.polygon,
            **x.flat_features()
        }, self.annotations))

    def match_annotations(self, b, min_coverage=0.2):
        b = [x for x in b.annotations if isinstance(x, PointsAnnotation) and x.label not in {"TODO"} and not x.is_hole]
        if len(b) > 0:
            label_groups = groupby(b, lambda x: x.label)
            labels, polygons = zip(*map(lambda x: (x[0], unary_union([a.polygon for a in x[1]])), label_groups))
            labels, polygons = np.array(labels), np.array(polygons)
            # Calculate areas, intersections and iou
            intersections = self.intersections(polygons)
            intersection_count = (intersections > 0).sum(-1)

            for ix, count in enumerate(intersection_count):
                if count == 1:
                    annotation = self.annotations[ix]
                    m_ix = np.argmax(intersections[ix])
                    coverage = intersections[ix, m_ix] / annotation.polygon.area
                    label = labels[m_ix]
                    if coverage > min_coverage:
                        annotation.ground_truth = GroundTruth(label=label, coverage=coverage, severity=b[m_ix].severity)
                elif count > 0:
                    annotation = self.annotations[ix]
                    match_mask = intersections[ix] > 0
                    matches = polygons[match_mask]
                    max_iou = [sub_ious(annotation, m) for m in matches]
                    arg_max_iou = np.argmax(max_iou)
                    label = labels[match_mask][arg_max_iou]
                    severity = b[arg_max_iou].severity
                    coverage = intersections[ix, match_mask][arg_max_iou] / annotation.polygon.area

                    if coverage > min_coverage:
                        annotation.ground_truth = GroundTruth(label=label, coverage=coverage, severity=severity)

    @staticmethod
    def extract_features(channel, features, classes, bbox, mask, threshold, sample, CHW, chierarchy, annotations, annotation, get_features):
        try:
            iter(get_features)
        except TypeError as te:
            return annotation # get_features is not iterable

        for f in get_features:
            if f == "segmentation":
                if CHW:
                    masked = np.where(mask[None], features, np.nan)
                    axis = (1, 2)
                else:
                    masked = np.where(mask[..., None], features, np.nan)
                    axis = (0, 1)

                stats = np.nanpercentile(masked, [10, 50, 90], axis=axis).T
                stats2 = np.array([np.nanmin(masked, axis=axis), np.nanmean(masked, axis=axis), np.nanmax(masked, axis=axis)]).T
                stats = np.hstack((stats, stats2))

                seg_names = ["10th_percentile", "median", "90th_percentile", "min", "mean", "max"]
                annotation.features["segmentation"] = {k: dict(zip(seg_names, v.tolist())) for k, v in zip(classes, stats)}
                annotation.visibility = int(sum(np.linspace(threshold, 1, 4)[:-1] <= stats[channel, 1]))
            elif f == "polygon":
                annotation.features["polygon"] = feature_calculator.PolygonFeatures.calculate_features(annotation)

        return annotation

    @classmethod
    def from_path(cls, path, io=io_tools, errors="raise"):
        try:
            return cls.parse_raw(io.read_file(path))
        except Exception as ex:
            if errors == "raise":
                raise ex
            return None

    @classmethod
    def from_segmentation(cls, segmentation, classes, sample, image_shape, tform=None,
                          threshold=0.5, simplify=False,
                          output_classes=None, CHW=False, feature_func=None, get_features=None):

        processing_kernel = np.array([[0, 1, 1, 1, 0], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1],
                                      [1, 1, 1, 1, 1], [0, 1, 1, 1, 0]], np.uint8)

        if not CHW:
            segmentation = np.transpose(segmentation, [2, 0, 1])

        annotations = []
        for channel, class_ in enumerate(classes):
            if output_classes and class_ not in output_classes:
                continue
            color = color_list[channel % len(color_list)]

            features = segmentation[channel]
            mask = (features > threshold).astype(np.uint8)

            # cleanup
            mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, processing_kernel)

            # Find labels and contours
            # Note labels index and contour id does not match
            num_labels, labels = cv2.connectedComponents(mask)
            # https://docs.opencv.org/master/d9/d8b/tutorial_py_contours_hierarchy.html
            contours, hierarchy = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

            if len(contours):
                for contour, chierarchy in zip(contours, hierarchy[0]):
                    # get bounding box
                    min_, max_ = contour.min((0, 1)), contour.max((0, 1))
                    # get label id of contour
                    clabel = labels[contour[0, 0, 1], contour[0, 0, 0]]

                    # extract masks and features in bbox
                    cmask = labels[min_[1]:max_[1] + 1, min_[0]:max_[0] + 1] == clabel
                    cfeatures = segmentation[:, min_[1]:max_[1] + 1, min_[0]:max_[0] + 1]

                    parent_annotation_ix, parent, is_hole = chierarchy[3], None, False
                    # if parent exists
                    if parent_annotation_ix > 0:
                        parent = annotations[parent_annotation_ix]
                        is_hole = not parent.is_hole

                    polygon = cv2_contour_to_shapely(contour)
                    if simplify:
                        polygon = simplify_polygon(polygon)
                    cnt = np.stack(polygon.exterior.xy, 1)

                    # Rescale contours to input shape
                    if tform is not None:
                        pts = np.pad(cnt.astype(np.float32).reshape(-1, 2), [[0, 0], [0, 1]], constant_values=1)
                        cnt = (tform @ pts.T)[:2].T

                    cnt = cnt.round(2)

                    annotation = PolygonAnnotation(
                        label=class_,
                        points=cnt,
                        is_hole=is_hole,
                        parent=None if parent is None else parent.uuid,
                        color=Color("dark" + color) if is_hole else color,
                    )

                    if get_features is not None:
                        annotation = (feature_func or ImageAnnotation.extract_features)(
                            channel=channel,
                            features=cfeatures,
                            classes=classes,
                            bbox=(slice(min_[1], max_[1] + 1), slice(min_[0], max_[0] + 1)),
                            mask=cmask,
                            threshold=threshold,
                            sample=sample,
                            CHW=True,
                            chierarchy=chierarchy,
                            annotations=annotations,
                            annotation=annotation,
                            get_features=get_features
                        )

                    annotations.append(
                        annotation
                    )

        return cls(annotations=annotations, image=dict(
            fileName=sample.get("path"),
            sampleId=sample.get("sample_id"),
            etag=sample.get("etag"),
            width=int(image_shape[1]),
            height=int(image_shape[0])
        ))

    def __repr__(self):
        return f"ImageAnnotation({self.image})"


def draw_contours_CHW(annotations, draw_buffer, label_space=None):
    lbl_map = {}
    for ann in annotations:
        lbl_map.setdefault(ann.label, []).append(ann)

    for lbl, color in label_space.items():
        contours = []
        for ann in annotations:
            if isinstance(ann, PointsAnnotation) and (
                lbl == ann.label or
                (isinstance(lbl, tuple) and np.any([lbl_ii == ann.label for lbl_ii in lbl]))
            ):
                contours.append(ann.points.astype(np.int32))

        # Draw contours
        if len(contours):
            color = color if isinstance(color, (tuple, list)) else color.tolist()
            for i, c in enumerate(color):
                if c != 0:
                    cv2.drawContours(draw_buffer[i], contours, -1, c, thickness=cv2.FILLED)
    return draw_buffer

Functions

def draw_contours_CHW(annotations, draw_buffer, label_space=None)
Expand source code
def draw_contours_CHW(annotations, draw_buffer, label_space=None):
    lbl_map = {}
    for ann in annotations:
        lbl_map.setdefault(ann.label, []).append(ann)

    for lbl, color in label_space.items():
        contours = []
        for ann in annotations:
            if isinstance(ann, PointsAnnotation) and (
                lbl == ann.label or
                (isinstance(lbl, tuple) and np.any([lbl_ii == ann.label for lbl_ii in lbl]))
            ):
                contours.append(ann.points.astype(np.int32))

        # Draw contours
        if len(contours):
            color = color if isinstance(color, (tuple, list)) else color.tolist()
            for i, c in enumerate(color):
                if c != 0:
                    cv2.drawContours(draw_buffer[i], contours, -1, c, thickness=cv2.FILLED)
    return draw_buffer
def flatten_structure(x, name='', out=None)
Expand source code
def flatten_structure(x, name='', out=None):
    out = {} if out is None else out
    x = x.dict() if isinstance(x, BaseModel) else x

    if type(x) is dict:
        for a in x:
            flatten_structure(x[a], name + a + '.', out)
    elif isinstance(x, (list, tuple, np.ndarray)):
        i = 0
        for a in x:
            flatten_structure(a, name + str(i) + '.', out)
            i += 1
    else:
        out[name[:-1]] = x
    return out
def sub_ious(annotation, polygons)
Expand source code
def sub_ious(annotation, polygons):
    iter_ = (polygons if isinstance(polygons, MultiPolygon) else [polygons])
    return max(annotation.iou(polygon) for polygon in iter_)

Classes

class Annotation (**data: Any)

Create a new model by parsing and validating input data from keyword arguments.

Raises ValidationError if the input data cannot be parsed to form a valid model.

Expand source code
class Annotation(BaseModel):
    type: str
    label: str
    color: Color
    uuid: UUID = Field(default_factory=uuid4)
    visibility: conint(ge=-1, le=3) = -1
    severity: conint(ge=-1, le=3) = -1
    features: Dict[str, Any] = Field(default_factory=dict)

    class Config:
        validate_assignment = True

    @validator("visibility", "severity", pre=True, allow_reuse=True)
    def default_negative_1(cls, v, field):
        return -1 if v is None else v

Ancestors

  • pydantic.main.BaseModel
  • pydantic.utils.Representation

Subclasses

Class variables

var Config
var colorColor
var features : Dict[str, Any]
var label : str
var severity : brevettiai.platform.models.annotation.ConstrainedIntValue
var type : str
var uuid : uuid.UUID
var visibility : brevettiai.platform.models.annotation.ConstrainedIntValue

Static methods

def default_negative_1(v, field)
Expand source code
@validator("visibility", "severity", pre=True, allow_reuse=True)
def default_negative_1(cls, v, field):
    return -1 if v is None else v
class ArrayMeta (*args, **kwargs)

type(object_or_name, bases, dict) type(object) -> the object's type type(name, bases, dict) -> a new type

Expand source code
class ArrayMeta(type):
    def __getitem__(self, t):
        return type('Array', (PointsArray,), {'inner_type': t})

Ancestors

  • builtins.type
class ClassAnnotation (**data: Any)

Create a new model by parsing and validating input data from keyword arguments.

Raises ValidationError if the input data cannot be parsed to form a valid model.

Expand source code
class ClassAnnotation(Annotation):
    type: constr(regex="^class$") = "class"

Ancestors

  • Annotation
  • pydantic.main.BaseModel
  • pydantic.utils.Representation

Class variables

var type : brevettiai.platform.models.annotation.ConstrainedStrValue
class Color (value: Union[Tuple[int, int, int], Tuple[int, int, int, float], str])

Mixin to provide str, repr, and pretty methods. See #884 for more details.

pretty is used by devtools to provide human readable representations of objects.

Expand source code
class Color(pydantic_color.Color):
    def __init__(self, value: pydantic_color.ColorType) -> None:
        if isinstance(value, str):
            value = value.replace("hsla", "hsl")
        super().__init__(value)

    @classmethod
    def from_hsl(cls, h, s, l, a=None):
        r, g, b = pydantic_color.hls_to_rgb(h, l, s)
        return cls((255*r, 255*g, 255*b, a))

Ancestors

  • pydantic.color.Color
  • pydantic.utils.Representation

Static methods

def from_hsl(h, s, l, a=None)
Expand source code
@classmethod
def from_hsl(cls, h, s, l, a=None):
    r, g, b = pydantic_color.hls_to_rgb(h, l, s)
    return cls((255*r, 255*g, 255*b, a))
class GroundTruth (**data: Any)

Create a new model by parsing and validating input data from keyword arguments.

Raises ValidationError if the input data cannot be parsed to form a valid model.

Expand source code
class GroundTruth(BaseModel):
    label: str
    iou: float = -1
    coverage: float = -1
    severity: float = -1

    def dict(self, **kwargs):
        return {"label": self.label, "iou": self.iou}

    @classmethod
    def from_annotation(cls, annotation, target=None):
        iou = -1 if target is None else target.iou(annotation)
        return cls(label=annotation.label, iou=iou)

Ancestors

  • pydantic.main.BaseModel
  • pydantic.utils.Representation

Class variables

var coverage : float
var iou : float
var label : str
var severity : float

Static methods

def from_annotation(annotation, target=None)
Expand source code
@classmethod
def from_annotation(cls, annotation, target=None):
    iou = -1 if target is None else target.iou(annotation)
    return cls(label=annotation.label, iou=iou)

Methods

def dict(self, **kwargs)

Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.

Expand source code
def dict(self, **kwargs):
    return {"label": self.label, "iou": self.iou}
class ImageAnnotation (**data: Any)

Create a new model by parsing and validating input data from keyword arguments.

Raises ValidationError if the input data cannot be parsed to form a valid model.

Expand source code
class ImageAnnotation(BaseModel):
    annotations: List[Union[PolygonAnnotation, RectangleAnnotation, LineAnnotation, PointAnnotation, ClassAnnotation]]
    source: dict = None
    image: dict = None

    def transform_annotation(self, matrix):
        for annotation in self.annotations:
            annotation.transform_points(matrix)

    def label_map(self):
        lbl_map = {}
        for ann in self.annotations:
            lbl_map.setdefault(ann.label, []).append(ann)
        return lbl_map

    def draw_contours_CHW(self, draw_buffer, label_space=None):
        if label_space is None:
            label_space = {k: v[0].color.as_rgb_tuple() for k, v in self.label_map().items()}
        return draw_contours_CHW(self.annotations, label_space=label_space, draw_buffer=draw_buffer)

    def intersections(self, right):
        left = self.annotations
        right = right.annotations if isinstance(right, ImageAnnotation) else right
        matrix = np.zeros((len(left), len(right)))
        for i, ann1 in enumerate(left):
            for j, ann2 in enumerate(right):
                matrix[i,j] = ann1.intersection(ann2)
        return matrix

    def fix_invalid(self):
        self.annotations = [a if a.polygon is not None and a.polygon.is_valid else a.fix_polygon() for a in self.annotations]
        self.annotations = [a for a in self.annotations if a.polygon is not None and not a.polygon.is_empty]
        return self

    def ious(self, right):
        left = self.annotations
        right = right.annotations if isinstance(right, ImageAnnotation) else right
        matrix = np.zeros((len(left), len(right)))
        for i, ann1 in enumerate(left):
            for j, ann2 in enumerate(right):
                matrix[i, j] = ann1.iou(ann2)
        return matrix

    def to_dataframe(self):
        return pd.DataFrame(map(lambda x: {
            **x.dict(),
            "Defect": "Unmatched" if x.ground_truth is None else x.ground_truth.label,
            "polygon": x.polygon,
            **x.flat_features()
        }, self.annotations))

    def match_annotations(self, b, min_coverage=0.2):
        b = [x for x in b.annotations if isinstance(x, PointsAnnotation) and x.label not in {"TODO"} and not x.is_hole]
        if len(b) > 0:
            label_groups = groupby(b, lambda x: x.label)
            labels, polygons = zip(*map(lambda x: (x[0], unary_union([a.polygon for a in x[1]])), label_groups))
            labels, polygons = np.array(labels), np.array(polygons)
            # Calculate areas, intersections and iou
            intersections = self.intersections(polygons)
            intersection_count = (intersections > 0).sum(-1)

            for ix, count in enumerate(intersection_count):
                if count == 1:
                    annotation = self.annotations[ix]
                    m_ix = np.argmax(intersections[ix])
                    coverage = intersections[ix, m_ix] / annotation.polygon.area
                    label = labels[m_ix]
                    if coverage > min_coverage:
                        annotation.ground_truth = GroundTruth(label=label, coverage=coverage, severity=b[m_ix].severity)
                elif count > 0:
                    annotation = self.annotations[ix]
                    match_mask = intersections[ix] > 0
                    matches = polygons[match_mask]
                    max_iou = [sub_ious(annotation, m) for m in matches]
                    arg_max_iou = np.argmax(max_iou)
                    label = labels[match_mask][arg_max_iou]
                    severity = b[arg_max_iou].severity
                    coverage = intersections[ix, match_mask][arg_max_iou] / annotation.polygon.area

                    if coverage > min_coverage:
                        annotation.ground_truth = GroundTruth(label=label, coverage=coverage, severity=severity)

    @staticmethod
    def extract_features(channel, features, classes, bbox, mask, threshold, sample, CHW, chierarchy, annotations, annotation, get_features):
        try:
            iter(get_features)
        except TypeError as te:
            return annotation # get_features is not iterable

        for f in get_features:
            if f == "segmentation":
                if CHW:
                    masked = np.where(mask[None], features, np.nan)
                    axis = (1, 2)
                else:
                    masked = np.where(mask[..., None], features, np.nan)
                    axis = (0, 1)

                stats = np.nanpercentile(masked, [10, 50, 90], axis=axis).T
                stats2 = np.array([np.nanmin(masked, axis=axis), np.nanmean(masked, axis=axis), np.nanmax(masked, axis=axis)]).T
                stats = np.hstack((stats, stats2))

                seg_names = ["10th_percentile", "median", "90th_percentile", "min", "mean", "max"]
                annotation.features["segmentation"] = {k: dict(zip(seg_names, v.tolist())) for k, v in zip(classes, stats)}
                annotation.visibility = int(sum(np.linspace(threshold, 1, 4)[:-1] <= stats[channel, 1]))
            elif f == "polygon":
                annotation.features["polygon"] = feature_calculator.PolygonFeatures.calculate_features(annotation)

        return annotation

    @classmethod
    def from_path(cls, path, io=io_tools, errors="raise"):
        try:
            return cls.parse_raw(io.read_file(path))
        except Exception as ex:
            if errors == "raise":
                raise ex
            return None

    @classmethod
    def from_segmentation(cls, segmentation, classes, sample, image_shape, tform=None,
                          threshold=0.5, simplify=False,
                          output_classes=None, CHW=False, feature_func=None, get_features=None):

        processing_kernel = np.array([[0, 1, 1, 1, 0], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1],
                                      [1, 1, 1, 1, 1], [0, 1, 1, 1, 0]], np.uint8)

        if not CHW:
            segmentation = np.transpose(segmentation, [2, 0, 1])

        annotations = []
        for channel, class_ in enumerate(classes):
            if output_classes and class_ not in output_classes:
                continue
            color = color_list[channel % len(color_list)]

            features = segmentation[channel]
            mask = (features > threshold).astype(np.uint8)

            # cleanup
            mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, processing_kernel)

            # Find labels and contours
            # Note labels index and contour id does not match
            num_labels, labels = cv2.connectedComponents(mask)
            # https://docs.opencv.org/master/d9/d8b/tutorial_py_contours_hierarchy.html
            contours, hierarchy = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

            if len(contours):
                for contour, chierarchy in zip(contours, hierarchy[0]):
                    # get bounding box
                    min_, max_ = contour.min((0, 1)), contour.max((0, 1))
                    # get label id of contour
                    clabel = labels[contour[0, 0, 1], contour[0, 0, 0]]

                    # extract masks and features in bbox
                    cmask = labels[min_[1]:max_[1] + 1, min_[0]:max_[0] + 1] == clabel
                    cfeatures = segmentation[:, min_[1]:max_[1] + 1, min_[0]:max_[0] + 1]

                    parent_annotation_ix, parent, is_hole = chierarchy[3], None, False
                    # if parent exists
                    if parent_annotation_ix > 0:
                        parent = annotations[parent_annotation_ix]
                        is_hole = not parent.is_hole

                    polygon = cv2_contour_to_shapely(contour)
                    if simplify:
                        polygon = simplify_polygon(polygon)
                    cnt = np.stack(polygon.exterior.xy, 1)

                    # Rescale contours to input shape
                    if tform is not None:
                        pts = np.pad(cnt.astype(np.float32).reshape(-1, 2), [[0, 0], [0, 1]], constant_values=1)
                        cnt = (tform @ pts.T)[:2].T

                    cnt = cnt.round(2)

                    annotation = PolygonAnnotation(
                        label=class_,
                        points=cnt,
                        is_hole=is_hole,
                        parent=None if parent is None else parent.uuid,
                        color=Color("dark" + color) if is_hole else color,
                    )

                    if get_features is not None:
                        annotation = (feature_func or ImageAnnotation.extract_features)(
                            channel=channel,
                            features=cfeatures,
                            classes=classes,
                            bbox=(slice(min_[1], max_[1] + 1), slice(min_[0], max_[0] + 1)),
                            mask=cmask,
                            threshold=threshold,
                            sample=sample,
                            CHW=True,
                            chierarchy=chierarchy,
                            annotations=annotations,
                            annotation=annotation,
                            get_features=get_features
                        )

                    annotations.append(
                        annotation
                    )

        return cls(annotations=annotations, image=dict(
            fileName=sample.get("path"),
            sampleId=sample.get("sample_id"),
            etag=sample.get("etag"),
            width=int(image_shape[1]),
            height=int(image_shape[0])
        ))

    def __repr__(self):
        return f"ImageAnnotation({self.image})"

Ancestors

  • pydantic.main.BaseModel
  • pydantic.utils.Representation

Class variables

var annotations : List[Union[PolygonAnnotationRectangleAnnotationLineAnnotationPointAnnotationClassAnnotation]]
var image : dict
var source : dict

Static methods

def extract_features(channel, features, classes, bbox, mask, threshold, sample, CHW, chierarchy, annotations, annotation, get_features)
Expand source code
@staticmethod
def extract_features(channel, features, classes, bbox, mask, threshold, sample, CHW, chierarchy, annotations, annotation, get_features):
    try:
        iter(get_features)
    except TypeError as te:
        return annotation # get_features is not iterable

    for f in get_features:
        if f == "segmentation":
            if CHW:
                masked = np.where(mask[None], features, np.nan)
                axis = (1, 2)
            else:
                masked = np.where(mask[..., None], features, np.nan)
                axis = (0, 1)

            stats = np.nanpercentile(masked, [10, 50, 90], axis=axis).T
            stats2 = np.array([np.nanmin(masked, axis=axis), np.nanmean(masked, axis=axis), np.nanmax(masked, axis=axis)]).T
            stats = np.hstack((stats, stats2))

            seg_names = ["10th_percentile", "median", "90th_percentile", "min", "mean", "max"]
            annotation.features["segmentation"] = {k: dict(zip(seg_names, v.tolist())) for k, v in zip(classes, stats)}
            annotation.visibility = int(sum(np.linspace(threshold, 1, 4)[:-1] <= stats[channel, 1]))
        elif f == "polygon":
            annotation.features["polygon"] = feature_calculator.PolygonFeatures.calculate_features(annotation)

    return annotation
def from_path(path, io=<brevettiai.io.utils.IoTools object>, errors='raise')
Expand source code
@classmethod
def from_path(cls, path, io=io_tools, errors="raise"):
    try:
        return cls.parse_raw(io.read_file(path))
    except Exception as ex:
        if errors == "raise":
            raise ex
        return None
def from_segmentation(segmentation, classes, sample, image_shape, tform=None, threshold=0.5, simplify=False, output_classes=None, CHW=False, feature_func=None, get_features=None)
Expand source code
@classmethod
def from_segmentation(cls, segmentation, classes, sample, image_shape, tform=None,
                      threshold=0.5, simplify=False,
                      output_classes=None, CHW=False, feature_func=None, get_features=None):

    processing_kernel = np.array([[0, 1, 1, 1, 0], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1],
                                  [1, 1, 1, 1, 1], [0, 1, 1, 1, 0]], np.uint8)

    if not CHW:
        segmentation = np.transpose(segmentation, [2, 0, 1])

    annotations = []
    for channel, class_ in enumerate(classes):
        if output_classes and class_ not in output_classes:
            continue
        color = color_list[channel % len(color_list)]

        features = segmentation[channel]
        mask = (features > threshold).astype(np.uint8)

        # cleanup
        mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, processing_kernel)

        # Find labels and contours
        # Note labels index and contour id does not match
        num_labels, labels = cv2.connectedComponents(mask)
        # https://docs.opencv.org/master/d9/d8b/tutorial_py_contours_hierarchy.html
        contours, hierarchy = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

        if len(contours):
            for contour, chierarchy in zip(contours, hierarchy[0]):
                # get bounding box
                min_, max_ = contour.min((0, 1)), contour.max((0, 1))
                # get label id of contour
                clabel = labels[contour[0, 0, 1], contour[0, 0, 0]]

                # extract masks and features in bbox
                cmask = labels[min_[1]:max_[1] + 1, min_[0]:max_[0] + 1] == clabel
                cfeatures = segmentation[:, min_[1]:max_[1] + 1, min_[0]:max_[0] + 1]

                parent_annotation_ix, parent, is_hole = chierarchy[3], None, False
                # if parent exists
                if parent_annotation_ix > 0:
                    parent = annotations[parent_annotation_ix]
                    is_hole = not parent.is_hole

                polygon = cv2_contour_to_shapely(contour)
                if simplify:
                    polygon = simplify_polygon(polygon)
                cnt = np.stack(polygon.exterior.xy, 1)

                # Rescale contours to input shape
                if tform is not None:
                    pts = np.pad(cnt.astype(np.float32).reshape(-1, 2), [[0, 0], [0, 1]], constant_values=1)
                    cnt = (tform @ pts.T)[:2].T

                cnt = cnt.round(2)

                annotation = PolygonAnnotation(
                    label=class_,
                    points=cnt,
                    is_hole=is_hole,
                    parent=None if parent is None else parent.uuid,
                    color=Color("dark" + color) if is_hole else color,
                )

                if get_features is not None:
                    annotation = (feature_func or ImageAnnotation.extract_features)(
                        channel=channel,
                        features=cfeatures,
                        classes=classes,
                        bbox=(slice(min_[1], max_[1] + 1), slice(min_[0], max_[0] + 1)),
                        mask=cmask,
                        threshold=threshold,
                        sample=sample,
                        CHW=True,
                        chierarchy=chierarchy,
                        annotations=annotations,
                        annotation=annotation,
                        get_features=get_features
                    )

                annotations.append(
                    annotation
                )

    return cls(annotations=annotations, image=dict(
        fileName=sample.get("path"),
        sampleId=sample.get("sample_id"),
        etag=sample.get("etag"),
        width=int(image_shape[1]),
        height=int(image_shape[0])
    ))

Methods

def draw_contours_CHW(self, draw_buffer, label_space=None)
Expand source code
def draw_contours_CHW(self, draw_buffer, label_space=None):
    if label_space is None:
        label_space = {k: v[0].color.as_rgb_tuple() for k, v in self.label_map().items()}
    return draw_contours_CHW(self.annotations, label_space=label_space, draw_buffer=draw_buffer)
def fix_invalid(self)
Expand source code
def fix_invalid(self):
    self.annotations = [a if a.polygon is not None and a.polygon.is_valid else a.fix_polygon() for a in self.annotations]
    self.annotations = [a for a in self.annotations if a.polygon is not None and not a.polygon.is_empty]
    return self
def intersections(self, right)
Expand source code
def intersections(self, right):
    left = self.annotations
    right = right.annotations if isinstance(right, ImageAnnotation) else right
    matrix = np.zeros((len(left), len(right)))
    for i, ann1 in enumerate(left):
        for j, ann2 in enumerate(right):
            matrix[i,j] = ann1.intersection(ann2)
    return matrix
def ious(self, right)
Expand source code
def ious(self, right):
    left = self.annotations
    right = right.annotations if isinstance(right, ImageAnnotation) else right
    matrix = np.zeros((len(left), len(right)))
    for i, ann1 in enumerate(left):
        for j, ann2 in enumerate(right):
            matrix[i, j] = ann1.iou(ann2)
    return matrix
def label_map(self)
Expand source code
def label_map(self):
    lbl_map = {}
    for ann in self.annotations:
        lbl_map.setdefault(ann.label, []).append(ann)
    return lbl_map
def match_annotations(self, b, min_coverage=0.2)
Expand source code
def match_annotations(self, b, min_coverage=0.2):
    b = [x for x in b.annotations if isinstance(x, PointsAnnotation) and x.label not in {"TODO"} and not x.is_hole]
    if len(b) > 0:
        label_groups = groupby(b, lambda x: x.label)
        labels, polygons = zip(*map(lambda x: (x[0], unary_union([a.polygon for a in x[1]])), label_groups))
        labels, polygons = np.array(labels), np.array(polygons)
        # Calculate areas, intersections and iou
        intersections = self.intersections(polygons)
        intersection_count = (intersections > 0).sum(-1)

        for ix, count in enumerate(intersection_count):
            if count == 1:
                annotation = self.annotations[ix]
                m_ix = np.argmax(intersections[ix])
                coverage = intersections[ix, m_ix] / annotation.polygon.area
                label = labels[m_ix]
                if coverage > min_coverage:
                    annotation.ground_truth = GroundTruth(label=label, coverage=coverage, severity=b[m_ix].severity)
            elif count > 0:
                annotation = self.annotations[ix]
                match_mask = intersections[ix] > 0
                matches = polygons[match_mask]
                max_iou = [sub_ious(annotation, m) for m in matches]
                arg_max_iou = np.argmax(max_iou)
                label = labels[match_mask][arg_max_iou]
                severity = b[arg_max_iou].severity
                coverage = intersections[ix, match_mask][arg_max_iou] / annotation.polygon.area

                if coverage > min_coverage:
                    annotation.ground_truth = GroundTruth(label=label, coverage=coverage, severity=severity)
def to_dataframe(self)
Expand source code
def to_dataframe(self):
    return pd.DataFrame(map(lambda x: {
        **x.dict(),
        "Defect": "Unmatched" if x.ground_truth is None else x.ground_truth.label,
        "polygon": x.polygon,
        **x.flat_features()
    }, self.annotations))
def transform_annotation(self, matrix)
Expand source code
def transform_annotation(self, matrix):
    for annotation in self.annotations:
        annotation.transform_points(matrix)
class LineAnnotation (**data: Any)

Create a new model by parsing and validating input data from keyword arguments.

Raises ValidationError if the input data cannot be parsed to form a valid model.

Expand source code
class LineAnnotation(PointsAnnotation):
    type: constr(regex="^line$") = "line"

Ancestors

Class variables

var type : brevettiai.platform.models.annotation.ConstrainedStrValue

Inherited members

class Mask (...)

ndarray(shape, dtype=float, buffer=None, offset=0, strides=None, order=None)

An array object represents a multidimensional, homogeneous array of fixed-size items. An associated data-type object describes the format of each element in the array (its byte-order, how many bytes it occupies in memory, whether it is an integer, a floating point number, or something else, etc.)

Arrays should be constructed using array, zeros or empty (refer to the See Also section below). The parameters given here refer to a low-level method (ndarray(…)) for instantiating an array.

For more information, refer to the numpy module and examine the methods and attributes of an array.

Parameters

(for the new method; see Notes below)

shape : tuple of ints
Shape of created array.
dtype : data-type, optional
Any object that can be interpreted as a numpy data type.
buffer : object exposing buffer interface, optional
Used to fill the array with data.
offset : int, optional
Offset of array data in buffer.
strides : tuple of ints, optional
Strides of data in memory.
order : {'C', 'F'}, optional
Row-major (C-style) or column-major (Fortran-style) order.

Attributes

T : ndarray
Transpose of the array.
data : buffer
The array's elements, in memory.
dtype : dtype object
Describes the format of the elements in the array.
flags : dict
Dictionary containing information related to memory use, e.g., 'C_CONTIGUOUS', 'OWNDATA', 'WRITEABLE', etc.
flat : numpy.flatiter object
Flattened version of the array as an iterator. The iterator allows assignments, e.g., x.flat = 3 (See ndarray.flat for assignment examples; TODO).
imag : ndarray
Imaginary part of the array.
real : ndarray
Real part of the array.
size : int
Number of elements in the array.
itemsize : int
The memory use of each array element in bytes.
nbytes : int
The total number of bytes required to store the array data, i.e., itemsize * size.
ndim : int
The array's number of dimensions.
shape : tuple of ints
Shape of the array.
strides : tuple of ints
The step-size required to move from one element to the next in memory. For example, a contiguous (3, 4) array of type int16 in C-order has strides (8, 2). This implies that to move from element to element in memory requires jumps of 2 bytes. To move from row-to-row, one needs to jump 8 bytes at a time (2 * 4).
ctypes : ctypes object
Class containing properties of the array needed for interaction with ctypes.
base : ndarray
If the array is a view into another array, that array is its base (unless that array is also a view). The base array is where the array data is actually stored.

See Also

array
Construct an array.
zeros
Create an array, each element of which is zero.
empty
Create an array, but leave its allocated memory unchanged (i.e., it contains "garbage").
dtype
Create a data-type.
numpy.typing.NDArray
A :term:generic <generic type> version of ndarray.

Notes

There are two modes of creating an array using __new__:

  1. If buffer is None, then only shape, dtype, and order are used.
  2. If buffer is an object exposing the buffer interface, then all keywords are interpreted.

No __init__ method is needed because the array is fully initialized after the __new__ method.

Examples

These examples illustrate the low-level ndarray constructor. Refer to the See Also section above for easier ways of constructing an ndarray.

First mode, buffer is None:

>>> np.ndarray(shape=(2,2), dtype=float, order='F')
array([[0.0e+000, 0.0e+000], # random
       [     nan, 2.5e-323]])

Second mode:

>>> np.ndarray((2,), buffer=np.array([1,2,3]),
...            offset=np.int_().itemsize,
...            dtype=int) # offset = 1*itemsize, i.e. skip first element
array([2, 3])
Expand source code
class Mask(np.ndarray):
    @classmethod
    def __get_validators__(cls):
        yield cls.validate_type

    @classmethod
    def validate_type(cls, val):
        assert isinstance(val, np.ndarray)
        assert val.ndim == 2
        array = val.astype(np.int32)
        return array.view(Mask)

Ancestors

  • numpy.ndarray

Static methods

def validate_type(val)
Expand source code
@classmethod
def validate_type(cls, val):
    assert isinstance(val, np.ndarray)
    assert val.ndim == 2
    array = val.astype(np.int32)
    return array.view(Mask)
class PointAnnotation (**data: Any)

Create a new model by parsing and validating input data from keyword arguments.

Raises ValidationError if the input data cannot be parsed to form a valid model.

Expand source code
class PointAnnotation(PointsAnnotation):
    type: constr(regex="^point$") = "point"

    def sample_points(self, tries=100000):
        p = self.points.astype(np.int)[0]
        for i in range(tries):
            yield p

Ancestors

Class variables

var type : brevettiai.platform.models.annotation.ConstrainedStrValue

Methods

def sample_points(self, tries=100000)
Expand source code
def sample_points(self, tries=100000):
    p = self.points.astype(np.int)[0]
    for i in range(tries):
        yield p

Inherited members

class PointsAnnotation (**data: Any)

Create a new model by parsing and validating input data from keyword arguments.

Raises ValidationError if the input data cannot be parsed to form a valid model.

Expand source code
class PointsAnnotation(Annotation):
    points: PointsArray[np.float32]
    parent: UUID = None
    is_hole: bool = False
    ground_truth: GroundTruth = None
    _shapely: Union[None, Polygon] = None
    _centroid: Union[None, Tuple[int, int]] = None
    _moments: Union[None, Tuple[float, float, float]] = None
    _hu_moments: Tuple[float, float, float, float, float, float, float] = None
    _mask = None

    class Config:
        arbitrary_types_allowed = True
        underscore_attrs_are_private = True

    def clear_calculated(self):
        self._shapely = None
        self._centroid = None
        self._moments = None
        self._mask = None

    def transform_points(self, matrix):
        padded = np.pad(np.array(self.points), ((0, 0), (0, 1)), constant_values=1)
        t_points = np.matmul(matrix, padded.T).T[..., :2]
        self.points = t_points
        self.clear_calculated()

    def dict(self, *args, **kwargs):
        cfg = BaseModel.dict(self, *args, **kwargs)
        if "points" in cfg:
            cfg["points"] = cfg["points"].dict()
        return cfg

    @property
    def bbox(self):
        return np.concatenate((self.points.min(axis=0), self.points.max(axis=0)))

    @property
    def path_length(self):
        return cv2.arcLength(self.points, True)
        #return sum((self._dist(a, b) for a, b in zip(points, np.roll(points, 1))))

    @property
    def area(self):
        return cv2.contourArea(self.points)
        #return sum([a['x'] * b['y'] - a['y'] * b['x'] for (a, b) in zip(points, np.roll(points, 1))]) / 2

    @property
    def centroid(self):
        if self._centroid is None:
            m = self.moments
            m00 = max(m['m00'], 1e-6)
            self._centroid = m['m10']/m00, m['m01']/m00
        return self._centroid

    @property
    def moments(self):
        if self._moments is None:
            self._moments = cv2.moments(self.points)
        return self._moments

    @property
    def hu_moments(self):
        if self._hu_moments is None:
            self._hu_moments = cv2.HuMoments(self.moments)
        return self._hu_moments

    @property
    def mask(self):
        if self._mask is None:
            bbox = self.bbox.astype(np.int)
            mask = np.zeros((bbox[2:] - bbox[:2]).astype(np.int32)[::-1])
            cnt = (self.points - np.amin(self.points, axis=0))[:, np.newaxis, :]
            cv2.drawContours(mask, np.array([cnt]).astype(np.int32), -1, 1, cv2.FILLED)
            self._mask = mask.T
        return self._mask

    @property
    def polygon(self):
        if self._shapely is None:
            self._shapely = Polygon(np.concatenate((self.points, self.points[0, None])))
        return self._shapely

    def fix_polygon(self):
        self._shapely = self.polygon.buffer(0)
        return self

    def sample_points(self, tries=100000):
        _min, _max = self.points.min(axis=0), self.points.max(axis=0)
        _range = (_max - _min)
        for i in range(tries):
            p = (np.random.rand(2) * _range) + _min
            if cv2.pointPolygonTest(self.points, tuple(p), False) >= 0:
                yield np.round(p).astype(np.int)

    def intersection(self, p2):
        if isinstance(p2, PointsAnnotation):
            p2 = p2.polygon
        try:
            return self.polygon.intersection(p2).area
        except Exception:
            return 0

    def iou(self, p2):
        if isinstance(p2, PointsAnnotation):
            p2 = p2.polygon
        inters = self.intersection(p2)
        return inters / (self.polygon.area + p2.area - inters)

    def flat_features(self):
        return flatten_structure(self.features)

Ancestors

  • Annotation
  • pydantic.main.BaseModel
  • pydantic.utils.Representation

Subclasses

Class variables

var Config
var ground_truthGroundTruth
var is_hole : bool
var parent : uuid.UUID
var points : brevettiai.platform.models.annotation.Array

Instance variables

var area
Expand source code
@property
def area(self):
    return cv2.contourArea(self.points)
var bbox
Expand source code
@property
def bbox(self):
    return np.concatenate((self.points.min(axis=0), self.points.max(axis=0)))
var centroid
Expand source code
@property
def centroid(self):
    if self._centroid is None:
        m = self.moments
        m00 = max(m['m00'], 1e-6)
        self._centroid = m['m10']/m00, m['m01']/m00
    return self._centroid
var hu_moments
Expand source code
@property
def hu_moments(self):
    if self._hu_moments is None:
        self._hu_moments = cv2.HuMoments(self.moments)
    return self._hu_moments
var mask
Expand source code
@property
def mask(self):
    if self._mask is None:
        bbox = self.bbox.astype(np.int)
        mask = np.zeros((bbox[2:] - bbox[:2]).astype(np.int32)[::-1])
        cnt = (self.points - np.amin(self.points, axis=0))[:, np.newaxis, :]
        cv2.drawContours(mask, np.array([cnt]).astype(np.int32), -1, 1, cv2.FILLED)
        self._mask = mask.T
    return self._mask
var moments
Expand source code
@property
def moments(self):
    if self._moments is None:
        self._moments = cv2.moments(self.points)
    return self._moments
var path_length
Expand source code
@property
def path_length(self):
    return cv2.arcLength(self.points, True)
var polygon
Expand source code
@property
def polygon(self):
    if self._shapely is None:
        self._shapely = Polygon(np.concatenate((self.points, self.points[0, None])))
    return self._shapely

Methods

def clear_calculated(self)
Expand source code
def clear_calculated(self):
    self._shapely = None
    self._centroid = None
    self._moments = None
    self._mask = None
def dict(self, *args, **kwargs)

Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.

Expand source code
def dict(self, *args, **kwargs):
    cfg = BaseModel.dict(self, *args, **kwargs)
    if "points" in cfg:
        cfg["points"] = cfg["points"].dict()
    return cfg
def fix_polygon(self)
Expand source code
def fix_polygon(self):
    self._shapely = self.polygon.buffer(0)
    return self
def flat_features(self)
Expand source code
def flat_features(self):
    return flatten_structure(self.features)
def intersection(self, p2)
Expand source code
def intersection(self, p2):
    if isinstance(p2, PointsAnnotation):
        p2 = p2.polygon
    try:
        return self.polygon.intersection(p2).area
    except Exception:
        return 0
def iou(self, p2)
Expand source code
def iou(self, p2):
    if isinstance(p2, PointsAnnotation):
        p2 = p2.polygon
    inters = self.intersection(p2)
    return inters / (self.polygon.area + p2.area - inters)
def sample_points(self, tries=100000)
Expand source code
def sample_points(self, tries=100000):
    _min, _max = self.points.min(axis=0), self.points.max(axis=0)
    _range = (_max - _min)
    for i in range(tries):
        p = (np.random.rand(2) * _range) + _min
        if cv2.pointPolygonTest(self.points, tuple(p), False) >= 0:
            yield np.round(p).astype(np.int)
def transform_points(self, matrix)
Expand source code
def transform_points(self, matrix):
    padded = np.pad(np.array(self.points), ((0, 0), (0, 1)), constant_values=1)
    t_points = np.matmul(matrix, padded.T).T[..., :2]
    self.points = t_points
    self.clear_calculated()
class PointsArray (...)

ndarray(shape, dtype=float, buffer=None, offset=0, strides=None, order=None)

An array object represents a multidimensional, homogeneous array of fixed-size items. An associated data-type object describes the format of each element in the array (its byte-order, how many bytes it occupies in memory, whether it is an integer, a floating point number, or something else, etc.)

Arrays should be constructed using array, zeros or empty (refer to the See Also section below). The parameters given here refer to a low-level method (ndarray(…)) for instantiating an array.

For more information, refer to the numpy module and examine the methods and attributes of an array.

Parameters

(for the new method; see Notes below)

shape : tuple of ints
Shape of created array.
dtype : data-type, optional
Any object that can be interpreted as a numpy data type.
buffer : object exposing buffer interface, optional
Used to fill the array with data.
offset : int, optional
Offset of array data in buffer.
strides : tuple of ints, optional
Strides of data in memory.
order : {'C', 'F'}, optional
Row-major (C-style) or column-major (Fortran-style) order.

Attributes

T : ndarray
Transpose of the array.
data : buffer
The array's elements, in memory.
dtype : dtype object
Describes the format of the elements in the array.
flags : dict
Dictionary containing information related to memory use, e.g., 'C_CONTIGUOUS', 'OWNDATA', 'WRITEABLE', etc.
flat : numpy.flatiter object
Flattened version of the array as an iterator. The iterator allows assignments, e.g., x.flat = 3 (See ndarray.flat for assignment examples; TODO).
imag : ndarray
Imaginary part of the array.
real : ndarray
Real part of the array.
size : int
Number of elements in the array.
itemsize : int
The memory use of each array element in bytes.
nbytes : int
The total number of bytes required to store the array data, i.e., itemsize * size.
ndim : int
The array's number of dimensions.
shape : tuple of ints
Shape of the array.
strides : tuple of ints
The step-size required to move from one element to the next in memory. For example, a contiguous (3, 4) array of type int16 in C-order has strides (8, 2). This implies that to move from element to element in memory requires jumps of 2 bytes. To move from row-to-row, one needs to jump 8 bytes at a time (2 * 4).
ctypes : ctypes object
Class containing properties of the array needed for interaction with ctypes.
base : ndarray
If the array is a view into another array, that array is its base (unless that array is also a view). The base array is where the array data is actually stored.

See Also

array
Construct an array.
zeros
Create an array, each element of which is zero.
empty
Create an array, but leave its allocated memory unchanged (i.e., it contains "garbage").
dtype
Create a data-type.
numpy.typing.NDArray
A :term:generic <generic type> version of ndarray.

Notes

There are two modes of creating an array using __new__:

  1. If buffer is None, then only shape, dtype, and order are used.
  2. If buffer is an object exposing the buffer interface, then all keywords are interpreted.

No __init__ method is needed because the array is fully initialized after the __new__ method.

Examples

These examples illustrate the low-level ndarray constructor. Refer to the See Also section above for easier ways of constructing an ndarray.

First mode, buffer is None:

>>> np.ndarray(shape=(2,2), dtype=float, order='F')
array([[0.0e+000, 0.0e+000], # random
       [     nan, 2.5e-323]])

Second mode:

>>> np.ndarray((2,), buffer=np.array([1,2,3]),
...            offset=np.int_().itemsize,
...            dtype=int) # offset = 1*itemsize, i.e. skip first element
array([2, 3])
Expand source code
class PointsArray(np.ndarray, metaclass=ArrayMeta):
    @classmethod
    def from_polygon(cls, polygon):
        return np.stack(polygon.exterior.xy, -1)[:-1].view(cls)

    @classmethod
    def __get_validators__(cls):
        yield cls.validate_type

    @classmethod
    def validate_type(cls, val):
        if isinstance(val, np.ndarray):
            assert val.ndim == 2
            assert val.shape[1] == 2
            array = val.astype(cls.inner_type)
        elif isinstance(val, (list, tuple)):
            try:
                array = np.array(tuple((p["x"], p["y"]) for p in val), dtype=cls.inner_type)
            except TypeError:
                array = np.array(tuple((p[0], p[0]) for p in val), dtype=cls.inner_type)
        else:
            raise NotImplementedError("type not implemented")
        return array.view(PointsArray)

    def dict(self):
        return [{"x": p[0].tolist(), "y": p[1].tolist()} for p in self]

Ancestors

  • numpy.ndarray

Subclasses

  • brevettiai.platform.models.annotation.Array

Static methods

def from_polygon(polygon)
Expand source code
@classmethod
def from_polygon(cls, polygon):
    return np.stack(polygon.exterior.xy, -1)[:-1].view(cls)
def validate_type(val)
Expand source code
@classmethod
def validate_type(cls, val):
    if isinstance(val, np.ndarray):
        assert val.ndim == 2
        assert val.shape[1] == 2
        array = val.astype(cls.inner_type)
    elif isinstance(val, (list, tuple)):
        try:
            array = np.array(tuple((p["x"], p["y"]) for p in val), dtype=cls.inner_type)
        except TypeError:
            array = np.array(tuple((p[0], p[0]) for p in val), dtype=cls.inner_type)
    else:
        raise NotImplementedError("type not implemented")
    return array.view(PointsArray)

Methods

def dict(self)
Expand source code
def dict(self):
    return [{"x": p[0].tolist(), "y": p[1].tolist()} for p in self]
class PolygonAnnotation (**data: Any)

Create a new model by parsing and validating input data from keyword arguments.

Raises ValidationError if the input data cannot be parsed to form a valid model.

Expand source code
class PolygonAnnotation(PointsAnnotation):
    type: constr(regex="^polygon$") = "polygon"

Ancestors

Class variables

var type : brevettiai.platform.models.annotation.ConstrainedStrValue

Inherited members

class RectangleAnnotation (**data: Any)

Create a new model by parsing and validating input data from keyword arguments.

Raises ValidationError if the input data cannot be parsed to form a valid model.

Expand source code
class RectangleAnnotation(PointsAnnotation):
    type: constr(regex="^rectangle$") = "rectangle"

    @property
    def contour(self):
        pt_list = np.array(tuple(map(tuple, self.points)), dtype=np.float32)
        return np.array([pt_list[[0, 0, 1, 1], 0], pt_list[[0, 1, 1, 0], 1]]).T

    @property
    def polygon(self):
        if self._shapely is None:
            self._shapely = box(*self.points[:2].flatten())
        return self._shapely

Ancestors

Class variables

var type : brevettiai.platform.models.annotation.ConstrainedStrValue

Instance variables

var contour
Expand source code
@property
def contour(self):
    pt_list = np.array(tuple(map(tuple, self.points)), dtype=np.float32)
    return np.array([pt_list[[0, 0, 1, 1], 0], pt_list[[0, 1, 1, 0], 1]]).T
var polygon
Expand source code
@property
def polygon(self):
    if self._shapely is None:
        self._shapely = box(*self.points[:2].flatten())
    return self._shapely

Inherited members