Skip to content

Commit

Permalink
Fixing a bug in the masking functions, and making mypy happy
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasteuwen committed Aug 16, 2024
1 parent f8f241b commit 0d3b7a8
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 43 deletions.
27 changes: 27 additions & 0 deletions dlup/_geometry.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import Any, Callable

from dlup.geometry import DlupPoint, DlupPolygon

class Polygon:
@property
def fields(self) -> list[str]: ...
def get_exterior(self) -> list[tuple[float, float]]: ...
def get_interiors(self) -> list[list[tuple[float, float]]]: ...

def set_polygon_factory(factory: Callable[[Polygon], DlupPolygon]) -> None: ...

class Point:
@property
def fields(self) -> list[str]: ...
def scale(self, scaling: float, origin: DlupPoint) -> None: ...
def get_coordinates(self) -> tuple[float, float]: ...

def set_point_factory(factory: Callable[[Point], DlupPoint]) -> None: ...

class GeometryCollection:
def add_polygon(self, polygon: Polygon) -> None: ...
def add_point(self, point: Point) -> None: ...
@property
def polygons(self) -> list[DlupPolygon]: ...
@property
def points(self) -> list[DlupPoint]: ...
99 changes: 63 additions & 36 deletions dlup/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import abc
import copy
import warnings
from typing import Any
from typing import Any, Optional

import numpy as np
import numpy.typing as npt

import dlup._geometry as _dg
from dlup.utils.imports import SHAPELY_AVAILABLE
Expand All @@ -26,12 +27,26 @@ def from_shapely(cls, shapely_geometry: ShapelyPoint | ShapelyPolygon) -> "_Base
def set_field(self, name: str, value: Any) -> None:
raise NotImplementedError

def get_field(self, name: str) -> None:
def get_field(self, name: str) -> Any:
raise NotImplementedError

@property
def label(self) -> str:
return self.get_field("label")
def fields(self) -> list[str]:
raise NotImplementedError

@property
def wkt(self) -> str:
raise NotImplementedError

@property
def label(self) -> Optional[str]:
field = self.get_field("label")
if field is None:
return None
if field is not isinstance(field, str):
raise ValueError(f"Label must be a string, got {type(field)}")
assert isinstance(field, str)
return field

@label.setter
def label(self, value: str) -> None:
Expand All @@ -40,8 +55,14 @@ def label(self, value: str) -> None:
self.set_field("label", value)

@property
def index(self) -> int:
return self.get_field("index")
def index(self) -> Optional[int]:
field = self.get_field("index")
if field is None:
return None
if not isinstance(field, int):
raise ValueError(f"Index must be an integer, got {type(field)}")
assert isinstance(field, int)
return field

@index.setter
def index(self, value: int) -> None:
Expand All @@ -50,16 +71,22 @@ def index(self, value: int) -> None:
self.set_field("index", value)

@property
def color(self):
return self.get_field("color")
def color(self) -> Optional[tuple[int, int, int]]:
field = self.get_field("color")
if field is None:
return None
if not isinstance(field, tuple) or len(field) != 3:
raise ValueError(f"Color must be an RGB tuple, got {type(field)}")
assert isinstance(field, tuple)
return field

@color.setter
def color(self, value: tuple[int, int, int]) -> None:
if not isinstance(value, tuple) or len(value) != 3:
raise ValueError(f"Color must be an RGB tuple, got {type(value)}")
self.set_field("color", value)

def __eq__(self, other: "_BaseGeometry") -> bool:
def __eq__(self, other: Any) -> bool:
if not isinstance(other, type(self)):
return False

Expand All @@ -85,7 +112,7 @@ def __isub__(self, other: Any) -> None:
# TODO: We can support MultiPoint and MultiPolygon
raise TypeError(f"Unsupported operand type(s) for -=: {type(self)} and {type(other)}")

def __repr__(self):
def __repr__(self) -> str:
repr_string = f"<{self.__class__.__name__}("
parts = []
for field in self.fields:
Expand All @@ -102,7 +129,7 @@ def __repr__(self):


class DlupPolygon(_dg.Polygon, _BaseGeometry):
def __init__(self, *args, **kwargs):
def __init__(self, *args: Any, **kwargs: Any):
_BaseGeometry.__init__(self)
if SHAPELY_AVAILABLE:
if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], ShapelyPolygon):
Expand Down Expand Up @@ -137,7 +164,7 @@ def __init__(self, *args, **kwargs):
self.set_field(key, value)

@classmethod
def from_shapely(cls, shapely_polygon):
def from_shapely(cls, shapely_polygon: "ShapelyPolygon") -> "DlupPolygon":
if not SHAPELY_AVAILABLE:
raise ImportError(
"Shapely is not available, and this functionality requires it. Install it using `pip install shapely`, or consult the documentation https://shapely.readthedocs.io/en/stable/installation.html for more information."
Expand All @@ -150,26 +177,26 @@ def from_shapely(cls, shapely_polygon):
interiors = [list(interior.coords) for interior in shapely_polygon.interiors]
return cls(exterior, interiors)

def __getstate__(self):
def __getstate__(self) -> dict[str, dict[str, Any]]:
state = {
"_fields": {field: self.get_field(field) for field in self.fields},
"_object": {"interiors": self.get_interiors(), "exterior": self.get_exterior()},
}
return state

def __setstate__(self, state):
def __setstate__(self, state: dict[str, dict[str, Any]]) -> None:
self.__init__(state["_object"]["exterior"], state["_object"]["interiors"])
for key, value in state["_fields"].items():
self.set_field(key, value)

def __copy__(self):
def __copy__(self) -> "DlupPolygon":
warnings.warn(
"Copying a Polygon currently creates a complete new object, without reference to the previous one, and is essentially the same as a deepcopy."
)
new_copy = DlupPolygon(self)
return new_copy

def __deepcopy__(self, memo):
def __deepcopy__(self, memo: Any) -> "DlupPolygon":
# Create a deepcopy of the geometry
new_copy = DlupPolygon(copy.deepcopy(self.get_exterior(), memo), copy.deepcopy(self.get_interiors(), memo))

Expand All @@ -179,7 +206,7 @@ def __deepcopy__(self, memo):

return new_copy

def to_shapely(self):
def to_shapely(self) -> "ShapelyPolygon":
if not SHAPELY_AVAILABLE:
raise ImportError(
"Shapely is not available, and this functionality requires it. Install it using `pip install shapely`, or consult the documentation https://shapely.readthedocs.io/en/stable/installation.html for more information."
Expand All @@ -191,7 +218,7 @@ def to_shapely(self):
return shapely.geometry.Polygon(exterior, interiors)


def _polygon_factory(polygon):
def _polygon_factory(polygon: _dg.Polygon) -> "DlupPolygon":
return DlupPolygon(polygon)


Expand All @@ -200,7 +227,7 @@ def _polygon_factory(polygon):


class DlupPoint(_dg.Point, _BaseGeometry):
def __init__(self, *args, **kwargs):
def __init__(self, *args: Any, **kwargs: Any) -> None:
_BaseGeometry.__init__(self)
if SHAPELY_AVAILABLE:
if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], ShapelyPoint):
Expand Down Expand Up @@ -228,7 +255,7 @@ def __init__(self, *args, **kwargs):
self.set_field(key, value)

@classmethod
def from_shapely(cls, shapely_point: "ShapelyPoint"):
def from_shapely(cls, shapely_point: "ShapelyPoint") -> "DlupPoint":
if not SHAPELY_AVAILABLE:
raise ImportError(
"Shapely is not available, and this functionality requires it. Install it using `pip install shapely`, or consult the documentation https://shapely.readthedocs.io/en/stable/installation.html for more information."
Expand All @@ -239,7 +266,7 @@ def from_shapely(cls, shapely_point: "ShapelyPoint"):

return cls(shapely_point.x, shapely_point.y)

def to_shapely(self):
def to_shapely(self) -> "ShapelyPoint":
if not SHAPELY_AVAILABLE:
raise ImportError(
"Shapely is not available, and this functionality requires it. Install it using `pip install shapely`, or consult the documentation https://shapely.readthedocs.io/en/stable/installation.html for more information."
Expand All @@ -248,14 +275,14 @@ def to_shapely(self):
return ShapelyPoint(self.get_coordinates())

@property
def x(self):
def x(self) -> float:
return self.get_coordinates()[0]

@property
def y(self):
def y(self) -> float:
return self.get_coordinates()[1]

def __copy__(self):
def __copy__(self) -> "DlupPoint":
# Create a new instance of DlupPolygon with the same geometry
new_copy = DlupPoint(self.x, self.y)

Expand All @@ -264,7 +291,7 @@ def __copy__(self):

return new_copy

def __deepcopy__(self, memo):
def __deepcopy__(self, memo: Any) -> "DlupPoint":
# Create a deepcopy of the geometry
new_copy = DlupPoint(copy.deepcopy(self.x), copy.deepcopy(self.y))

Expand All @@ -274,38 +301,38 @@ def __deepcopy__(self, memo):

return new_copy

def __getstate__(self):
def __getstate__(self) -> dict[str, dict[str, Any]]:
state = {
"_fields": {field: self.get_field(field) for field in self.fields},
"_object": {"coordinates": self.get_coordinates()},
}
return state

def __setstate__(self, state):
def __setstate__(self, state) -> None:
self.__init__(state["_object"]["coordinates"][0], state["_object"]["coordinates"][1])
for key, value in state["_fields"].items():
self.set_field(key, value)

def scale(self, scaling, origin=None) -> None:
def scale(self, scaling, origin: Optional["DlupPoint"] = None) -> None:
if origin is None:
origin = DlupPoint(0, 0)
super().scale(scaling, origin)


def _point_factory(point):
def _point_factory(point: _dg.Point) -> DlupPoint:
return DlupPoint(point)


# Register the point factory
_dg.set_point_factory(_point_factory)


class GeometryCollection(_dg.GeometryContainer):
def __init__(self):
class GeometryCollection(_dg.GeometryCollection):
def __init__(self) -> None:
super().__init__()

@property
def color_lut(self):
def color_lut(self) -> npt.NDArray[np.uint8]:
color_map = {}
for r in self.polygons:
color = r.color
Expand All @@ -324,14 +351,14 @@ def color_lut(self):

return LUT

def __getstate__(self):
def __getstate__(self) -> dict[str, Any]:
state = {
"_polygons": [polygon.__getstate__() for polygon in self.polygons],
"_points": [point.__getstate__() for point in self.points],
}
return state

def __setstate__(self, state):
def __setstate__(self, state) -> None:
polygons = [DlupPolygon.__new__(DlupPolygon) for _ in state["_polygons"]]
for polygon, polygon_state in zip(polygons, state["_polygons"]):
polygon.__setstate__(polygon_state)
Expand All @@ -348,7 +375,7 @@ def __setstate__(self, state):
for point in points:
self.add_point(point)

def __eq__(self, other: "GeometryCollection") -> bool:
def __eq__(self, other: Any) -> bool:
if not isinstance(other, type(self)):
return False

Expand All @@ -363,6 +390,6 @@ def __eq__(self, other: "GeometryCollection") -> bool:

return True

def __len__(self):
def __len__(self) -> int:
# Also self.size()
return len(self.polygons) + len(self.points)
6 changes: 3 additions & 3 deletions src/geometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ py::list GeometryCollection::getPolygons() {
}
#ifdef DLUPDEBUG
std::chrono::steady_clock::time_point stop = std::chrono::steady_clock::now();
std::cout << "Elapsed time in GeometryContainer::getPolygons: "
std::cout << "Elapsed time in GeometryCollection::getPolygons: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(stop - end).count() << " ms" << std::endl;
#endif
return py_polygons;
Expand Down Expand Up @@ -456,7 +456,7 @@ AnnotationRegion GeometryCollection::readRegion(const std::pair<double, double>
}
#ifdef DLUPDEBUG
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
std::cout << "Elapsed time in GeometryContainer::readRegion: "
std::cout << "Elapsed time in GeometryCollection:readRegion: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count() << " ms" << std::endl;
#endif
auto returnValue = AnnotationRegion(std::move(intersectedPolygons), std::move(intersectedPoints), std::move(size));
Expand Down Expand Up @@ -529,7 +529,7 @@ PYBIND11_MODULE(_geometry, m) {
m.def("set_polygon_factory", &AnnotationRegion::setPolygonFactory);
m.def("set_point_factory", &AnnotationRegion::setPointFactory);

py::class_<GeometryCollection, std::shared_ptr<GeometryCollection>>(m, "GeometryContainer")
py::class_<GeometryCollection, std::shared_ptr<GeometryCollection>>(m, "GeometryCollection")
.def(py::init<>())
.def("add_polygon", &GeometryCollection::addPolygon)
.def("add_point", &GeometryCollection::addPoint)
Expand Down
3 changes: 0 additions & 3 deletions src/opencv.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ cv::Mat generateMaskFromAnnotations(const std::vector<std::shared_ptr<Polygon>>
std::vector<cv::Point> exterior_cv_points;
std::vector<std::vector<cv::Point>> interiors_cv_points;

// exterior_cv_points.reserve(100000);
// interiors_cv_points.reserve(100000);

for (const auto &annotation : annotations) {
auto index_value_field = annotation->getField("index");
if (!index_value_field) {
Expand Down
2 changes: 1 addition & 1 deletion src/region.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class AnnotationRegion {
#ifdef DLUPDEBUG
std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
#endif
cv::Size region_size(std::get<1>(mask_size_), std::get<0>(mask_size_));
cv::Size region_size(std::get<0>(mask_size_), std::get<1>(mask_size_));
cv::Mat mask = generateMaskFromAnnotations(polygons_, region_size, default_value);
#ifdef DLUPDEBUG
std::cout
Expand Down

0 comments on commit 0d3b7a8

Please sign in to comment.