|
8 | 8 | import logging as log |
9 | 9 | import operator |
10 | 10 | import warnings |
| 11 | +from collections import defaultdict |
11 | 12 | from copy import deepcopy |
12 | 13 | from itertools import product |
13 | 14 | from typing import TYPE_CHECKING, Callable |
14 | 15 |
|
15 | 16 | import numpy as np |
16 | 17 | import shapely.geometry as sg |
17 | 18 | import torch |
18 | | -from datumaro import Bbox, DatasetItem, Image, Polygon |
| 19 | +from datumaro import Bbox, DatasetItem, Ellipse, Image, Polygon |
19 | 20 | from datumaro import Dataset as DmDataset |
20 | 21 | from datumaro.components.annotation import AnnotationType |
21 | 22 | from datumaro.plugins.tiling import Tile |
@@ -92,6 +93,7 @@ def __init__( |
92 | 93 | ) |
93 | 94 | self._tile_size = tile_size |
94 | 95 | self._tile_ann_func_map[AnnotationType.polygon] = OTXTileTransform._tile_polygon |
| 96 | + self._tile_ann_func_map[AnnotationType.ellipse] = OTXTileTransform._tile_ellipse |
95 | 97 | self.with_full_img = with_full_img |
96 | 98 |
|
97 | 99 | @staticmethod |
@@ -132,6 +134,45 @@ def _tile_polygon( |
132 | 134 | attributes=deepcopy(ann.attributes), |
133 | 135 | ) |
134 | 136 |
|
| 137 | + @staticmethod |
| 138 | + def _tile_ellipse( |
| 139 | + ann: Ellipse, |
| 140 | + roi_box: sg.Polygon, |
| 141 | + threshold_drop_ann: float = 0.8, |
| 142 | + *args, # noqa: ARG004 |
| 143 | + **kwargs, # noqa: ARG004 |
| 144 | + ) -> Polygon | None: |
| 145 | + polygon = sg.Polygon(ann.get_points(num_points=10)) |
| 146 | + |
| 147 | + # NOTE: polygon may be invalid, e.g. self-intersecting |
| 148 | + if not roi_box.intersects(polygon) or not polygon.is_valid: |
| 149 | + return None |
| 150 | + |
| 151 | + # NOTE: intersection may return a GeometryCollection or MultiPolygon |
| 152 | + inter = polygon.intersection(roi_box) |
| 153 | + if isinstance(inter, (sg.GeometryCollection, sg.MultiPolygon)): |
| 154 | + shapes = [(geom, geom.area) for geom in list(inter.geoms) if geom.is_valid] |
| 155 | + if not shapes: |
| 156 | + return None |
| 157 | + |
| 158 | + inter, _ = max(shapes, key=operator.itemgetter(1)) |
| 159 | + |
| 160 | + if not isinstance(inter, sg.Polygon) and not inter.is_valid: |
| 161 | + return None |
| 162 | + |
| 163 | + prop_area = inter.area / polygon.area |
| 164 | + |
| 165 | + if prop_area < threshold_drop_ann: |
| 166 | + return None |
| 167 | + |
| 168 | + inter = _apply_offset(inter, roi_box) |
| 169 | + |
| 170 | + return Polygon( |
| 171 | + points=[p for xy in inter.exterior.coords for p in xy], |
| 172 | + attributes=deepcopy(ann.attributes), |
| 173 | + label=ann.label, |
| 174 | + ) |
| 175 | + |
135 | 176 | def _extract_rois(self, image: Image) -> list[BboxIntCoords]: |
136 | 177 | """Extracts Tile ROIs from the given image. |
137 | 178 |
|
@@ -467,26 +508,50 @@ def _get_item_impl(self, index: int) -> TileInstSegDataEntity: # type: ignore[o |
467 | 508 | img = item.media_as(Image) |
468 | 509 | img_data, img_shape, _ = self._get_img_data_and_shape(img) |
469 | 510 |
|
| 511 | + anno_collection: dict[str, list] = defaultdict(list) |
| 512 | + for anno in item.annotations: |
| 513 | + anno_collection[anno.__class__.__name__].append(anno) |
| 514 | + |
470 | 515 | gt_bboxes, gt_labels, gt_masks, gt_polygons = [], [], [], [] |
471 | 516 |
|
472 | | - for annotation in item.annotations: |
473 | | - if isinstance(annotation, Polygon): |
474 | | - bbox = np.array(annotation.get_bbox(), dtype=np.float32) |
| 517 | + # TODO(Eugene): https://jira.devtools.intel.com/browse/CVS-159363 |
| 518 | + # Temporary solution to handle multiple annotation types. |
| 519 | + # Ideally, we should pre-filter annotations during initialization of the dataset. |
| 520 | + |
| 521 | + if Polygon.__name__ in anno_collection: # Polygon for InstSeg has higher priority |
| 522 | + for poly in anno_collection[Polygon.__name__]: |
| 523 | + bbox = Bbox(*poly.get_bbox()).points |
475 | 524 | gt_bboxes.append(bbox) |
476 | | - gt_labels.append(annotation.label) |
| 525 | + gt_labels.append(poly.label) |
477 | 526 |
|
478 | 527 | if self._dataset.include_polygons: |
479 | | - gt_polygons.append(annotation) |
| 528 | + gt_polygons.append(poly) |
480 | 529 | else: |
481 | | - gt_masks.append(polygon_to_bitmap([annotation], *img_shape)[0]) |
482 | | - |
483 | | - if empty_anno := len(gt_bboxes) == 0: |
484 | | - warnings.warn(f"Empty annotation for image {item.id}", stacklevel=2) |
485 | | - |
486 | | - # convert xywh to xyxy format |
487 | | - bboxes = np.empty((0, 4), dtype=np.float32) if empty_anno else np.stack(gt_bboxes, dtype=np.float32) |
488 | | - bboxes[:, 2:] += bboxes[:, :2] |
| 530 | + gt_masks.append(polygon_to_bitmap([poly], *img_shape)[0]) |
| 531 | + elif Bbox.__name__ in anno_collection: |
| 532 | + boxes = anno_collection[Bbox.__name__] |
| 533 | + gt_bboxes = [ann.points for ann in boxes] |
| 534 | + gt_labels = [ann.label for ann in boxes] |
| 535 | + for box in boxes: |
| 536 | + poly = Polygon(box.as_polygon()) |
| 537 | + if self._dataset.include_polygons: |
| 538 | + gt_polygons.append(poly) |
| 539 | + else: |
| 540 | + gt_masks.append(polygon_to_bitmap([poly], *img_shape)[0]) |
| 541 | + elif Ellipse.__name__ in anno_collection: |
| 542 | + for ellipse in anno_collection[Ellipse.__name__]: |
| 543 | + bbox = Bbox(*ellipse.get_bbox()).points |
| 544 | + gt_bboxes.append(bbox) |
| 545 | + gt_labels.append(ellipse.label) |
| 546 | + poly = Polygon(ellipse.as_polygon(num_points=10)) |
| 547 | + if self._dataset.include_polygons: |
| 548 | + gt_polygons.append(poly) |
| 549 | + else: |
| 550 | + gt_masks.append(polygon_to_bitmap([poly], *img_shape)[0]) |
| 551 | + else: |
| 552 | + warnings.warn(f"No valid annotations found for image {item.id}!", stacklevel=2) |
489 | 553 |
|
| 554 | + bboxes = np.stack(gt_bboxes, dtype=np.float32) if gt_bboxes else np.empty((0, 4), dtype=np.float32) |
490 | 555 | masks = np.stack(gt_masks, axis=0) if gt_masks else np.empty((0, *img_shape), dtype=bool) |
491 | 556 | labels = np.array(gt_labels, dtype=np.int64) |
492 | 557 |
|
|
0 commit comments