Source code for pado.annotations.formats

"""different Annotation formats"""
from __future__ import annotations

import enum
import struct
from typing import ClassVar
from typing import List
from typing import Optional

from geojson_pydantic import Feature
from geojson_pydantic.geometries import Geometry
from geojson_pydantic.geometries import parse_geometry_obj
from pydantic import BaseModel
from pydantic import validator
from pydantic.color import Color
from shapely.geometry import shape
from shapely.geometry.base import BaseGeometry
from shapely.wkt import loads as wkt_loads

from pado.images.ids import ImageId

# === PADO annotation model ===


[docs]class AnnotationState(enum.IntEnum): NOT_SET = -1 PLANNED = 0 ASSIGNED = 1 IN_PROGRESS = 2 DONE = 3 REVIEWED = 4
[docs]class AnnotationQuality(str, enum.Enum): NOT_SET = "not_set" BROKEN = "broken" GOOD = "good"
[docs]class AnnotatorType(str, enum.Enum): HUMAN = "human" MODEL = "model" UNKNOWN = "unknown"
class Annotator(BaseModel): type: AnnotatorType name: str UNKNOWN: ClassVar[Annotator] # add a default instance to the model in case the annotator is unknown. Annotator.UNKNOWN = Annotator(type=AnnotatorType.UNKNOWN, name="unknown")
[docs]class AnnotationStyle(str, enum.Enum): NOT_SET = "not_set" EXPLICIT_ROI_ONLY = "explicit_roi_only" ALL_WITHIN_REGION = "all_within_region"
class AnnotationModel(BaseModel): image_id: Optional[ImageId] identifier: Optional[str] = None project: Optional[str] = None annotator: Annotator = Annotator.UNKNOWN state: AnnotationState = AnnotationState.NOT_SET style: AnnotationStyle = AnnotationStyle.NOT_SET classification: str color: Color description: Optional[str] comment: str geometry: BaseGeometry class Config: arbitrary_types_allowed = True # needed for geometry to work @validator("image_id", pre=True) def image_id_from_str(cls, v): if isinstance(v, str): return ImageId.from_str(v) return v @validator("geometry", pre=True) def geometry_from_str(cls, v): if isinstance(v, str): return wkt_loads(v) return v # === QUPATH geojson annotations ===
[docs]class QPPathObjectId(str, enum.Enum): """these have been used as `.id` in legacy qupath<0.3""" ANNOTATION = "PathAnnotationObject" CELL = "PathCellObject" DETECTION = "PathDetectionObject" ROOT = "PathRootObject" TILE = "PathTileObject" TMA_CORE = "TMACoreObject"
[docs]class QPPathObjectType(str, enum.Enum): """these are used in `.properties.object_type` qupath>=0.3""" ANNOTATION = "annotation" CELL = "cell" DETECTION = "detection" ROOT = "root" TILE = "tile" TMA_CORE = "tma_core"
[docs]class QPClassification(BaseModel): """qupath classifications with colors""" name: str colorRGB: Color
[docs] @validator("colorRGB", pre=True) def qupath_color_to_rgba(cls, v): """convert a qupath color to a pydantic Color""" if not (isinstance(v, int) and -(2**31) <= v <= 2**31 - 1): raise ValueError(f"color out of bounds: {v}") alpha, red, blue, green = struct.pack(">i", v) return Color((red, blue, green, alpha / 255.0))
[docs]class QPProperties(BaseModel): """qupath properties""" classification: QPClassification isLocked: bool measurements: Optional[List] # todo: add measurement type? object_type: Optional[QPPathObjectType] = None # if provided must be of type name: Optional[str] = None color: Optional[Color] = None
[docs]class QuPathAnnotation(Feature[Geometry, QPProperties]): # type: ignore """model for qupath annotations""" id: Optional[QPPathObjectId] = None @validator("geometry", pre=True) def parse_geometry_type(cls, v): if isinstance(v, dict): if "type" in v and v["type"] == "Polygon": # workaround for Polygons that do not adhere to geojson RFC 7946 # fixme: I'm not sure if this should not be handled here... v = shape(v).__geo_interface__ return parse_geometry_obj(v) return v