Skip to content

Commit 91d41a5

Browse files
committed
remove detection dataset and update image dataset
1 parent 8e4dcfa commit 91d41a5

12 files changed

Lines changed: 220 additions & 322 deletions

File tree

datamint/dataset/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from .video_dataset import VideoDataset
1818
from .sliced_dataset import SlicedVolumeDataset
1919
from .sliced_video_dataset import SlicedVideoDataset
20-
from .detection_dataset import DetectionDataset, detection_collate_fn
20+
from .image_dataset import detection_collate_fn
2121
from .factory import build_dataset
2222
from .split_result import SplitResult
2323

@@ -32,7 +32,6 @@
3232
'VideoDataset',
3333
'SlicedVolumeDataset',
3434
'SlicedVideoDataset',
35-
'DetectionDataset',
3635
'detection_collate_fn',
3736
# Factory
3837
'build_dataset',

datamint/dataset/base.py

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def __init__(
8686
# all_annotations: bool = False,
8787
return_metainfo: bool = True,
8888
return_segmentations: bool = True,
89+
return_boxes: bool = False,
8990
return_as_semantic_segmentation: bool = False,
9091
semantic_seg_merge_strategy: MergeStrategy | None = None,
9192
alb_transform: 'Callable | BaseCompose | None' = None,
@@ -152,6 +153,7 @@ def __init__(
152153
# Store configuration
153154
self.return_metainfo = return_metainfo
154155
self.return_segmentations = return_segmentations
156+
self.return_boxes = return_boxes
155157
self.return_as_semantic_segmentation = return_as_semantic_segmentation
156158
self.semantic_seg_merge_strategy: MergeStrategy | None = semantic_seg_merge_strategy
157159
self.include_unannotated = include_unannotated
@@ -629,6 +631,8 @@ def _setup_labels(self) -> None:
629631
self.image_lsets, self.image_lcodes = self._infer_labels_set(framed=False)
630632
self.seglabel_list, self.seglabel2code = self._infer_segmentation_group()
631633

634+
self.box_class_map: dict[str, int] = self._build_box_class_map()
635+
632636
def _augment_labels_from_annotations(self) -> None:
633637
"""Augment project-defined label sets with identifiers found in actual annotations.
634638
@@ -855,6 +859,11 @@ def segmentation_labels_set(self) -> list[str]:
855859
"""Segmentation label names."""
856860
return self.seglabel_list
857861

862+
@property
863+
def box_labels_set(self) -> list[str]:
864+
"""Box annotation class names, alphabetically ordered."""
865+
return sorted(self.box_class_map, key=self.box_class_map.__getitem__)
866+
858867
def _infer_segmentation_group(self) -> tuple[list[str], dict[str, int]]:
859868
"""Infer segmentation labels from annotations when no project is provided."""
860869
seglabel_set: set[str] = set()
@@ -872,6 +881,55 @@ def _infer_segmentation_group(self) -> tuple[list[str], dict[str, int]]:
872881

873882
return seglabel_list, seglabel2code
874883

884+
def _build_box_class_map(self) -> dict[str, int]:
885+
"""Build {class_name: index} for box annotations, alphabetically sorted."""
886+
from datamint.entities.annotations import AnnotationType
887+
class_names: set[str] = set()
888+
for anns in self.resource_annotations:
889+
for ann in anns:
890+
if getattr(ann, 'annotation_type', None) == AnnotationType.SQUARE and ann.identifier:
891+
class_names.add(ann.identifier)
892+
return {name: idx for idx, name in enumerate(sorted(class_names))}
893+
894+
def _load_boxes(
895+
self,
896+
annotations: 'Sequence[Annotation]',
897+
) -> tuple['Tensor', 'Tensor']:
898+
"""Extract box tensors from square annotations.
899+
900+
Returns:
901+
Tuple of (boxes, box_labels) where boxes is (N, 4) float32 in
902+
pascal_voc pixel coords and box_labels is (N,) int64 class indices.
903+
"""
904+
valid: list[tuple[float, float, float, float, str]] = []
905+
for ann in annotations:
906+
if not ann.identifier:
907+
_LOGGER.warning("Skipping box annotation with no identifier.")
908+
continue
909+
geometry = getattr(ann, 'geometry', None)
910+
if geometry is None:
911+
continue
912+
x1, y1, _ = geometry.point1
913+
x2, y2, _ = geometry.point2
914+
x1, y1, x2, y2 = float(x1), float(y1), float(x2), float(y2)
915+
if x2 <= x1 or y2 <= y1:
916+
_LOGGER.warning(
917+
"Skipping degenerate box (x2<=x1 or y2<=y1): (%s, %s, %s, %s)",
918+
x1, y1, x2, y2,
919+
)
920+
continue
921+
valid.append((x1, y1, x2, y2, ann.identifier))
922+
923+
if not valid:
924+
return torch.zeros((0, 4), dtype=torch.float32), torch.zeros((0,), dtype=torch.int64)
925+
926+
boxes = torch.tensor([(x1, y1, x2, y2) for x1, y1, x2, y2, _ in valid], dtype=torch.float32)
927+
labels = torch.tensor(
928+
[self.box_class_map.get(name, 0) for _, _, _, _, name in valid],
929+
dtype=torch.int64,
930+
)
931+
return boxes, labels
932+
875933
def _process_segmentation_group(self, groups: dict) -> tuple[list[str], dict[str, int]]:
876934
"""Get segmentation labels from the server."""
877935
try:
@@ -979,28 +1037,41 @@ def __getitem__(self, index: int) -> dict[str, Any]:
9791037
if isinstance(img, np.ndarray):
9801038
img = self._preprocess_image_array(img)
9811039
annotations = result['annotations']
982-
# resource = result['resource']
983-
# _LOGGER.debug(f"Loaded image {resource.filename} with shape {img.shape}")
9841040

985-
# Process segmentations
1041+
# Load all requested annotation targets
1042+
targets: dict[str, Any] = {}
1043+
seg_labels = None
1044+
9861045
if self.return_segmentations:
987-
seg_anns = AnnotationProcessor.filter_annotations(annotations,
988-
type='segmentation',
989-
scope='all')
1046+
seg_anns = AnnotationProcessor.filter_annotations(annotations, type='segmentation', scope='all')
9901047
segmentations, seg_labels, _ = self.annotation_processor.load_segmentations(seg_anns)
991-
# Apply albumentations if present
992-
if self.alb_transform:
993-
aug_result = self.apply_alb_transform(img, segmentations)
994-
img = aug_result['image']
995-
result['image'] = img
996-
segmentations = aug_result['segmentations']
1048+
targets['masks'] = segmentations
1049+
1050+
if self.return_boxes:
1051+
box_anns = [ann for ann in annotations if getattr(ann, 'annotation_type', None) == 'square']
1052+
boxes, box_labels_tensor = self._load_boxes(box_anns)
1053+
targets['boxes'] = boxes
1054+
targets['box_labels'] = box_labels_tensor
1055+
1056+
# Apply albumentations to all targets at once
1057+
if self.alb_transform:
1058+
aug = self.apply_alb_transform(img, targets)
1059+
img = aug.pop('image')
1060+
targets.update(aug)
9971061

998-
segmentations, seg_labels = self._process_segmentations(segmentations, seg_labels,
999-
output_shape=img.shape[1:])
1062+
result['image'] = img
10001063

1001-
result['segmentations'] = segmentations
1064+
# Post-process and write to result
1065+
if self.return_segmentations:
1066+
masks = targets.get('masks', {})
1067+
masks, seg_labels = self._process_segmentations(masks, seg_labels, output_shape=img.shape[1:])
1068+
result['masks'] = masks
10021069
if seg_labels:
1003-
result['seg_labels'] = seg_labels
1070+
result['mask_labels'] = seg_labels
1071+
1072+
if self.return_boxes:
1073+
result['boxes'] = targets.get('boxes', torch.zeros((0, 4), dtype=torch.float32))
1074+
result['box_labels'] = targets.get('box_labels', torch.zeros((0,), dtype=torch.int64))
10041075

10051076
# Process image-level labels
10061077
result['image_labels'] = self._extract_image_labels(annotations)
@@ -1012,17 +1083,19 @@ def __getitem__(self, index: int) -> dict[str, Any]:
10121083
def apply_alb_transform(
10131084
self,
10141085
img: np.ndarray,
1015-
segmentations: dict[str, np.ndarray]
1086+
targets: dict[str, Any],
10161087
) -> dict[str, Any]:
1017-
"""Apply albumentations transform to image and masks.
1088+
"""Apply albumentations transform to image and annotation targets.
10181089
1019-
Returns:
1020-
Dict with transformed 'image' and 'segmentations' (dict).
1021-
It is recommended that 'image' has shape (C, depth, H, W)
1022-
and each segmentation of 'segmentations' has shape (num_instances, depth, H, W), so that
1023-
common downstream processing can be applied.
1024-
If not, please override :py:meth:`_process_segmentations` accordingly.
1090+
Args:
1091+
img: Image array.
1092+
targets: Dict of annotation targets to transform. May contain:
1093+
- ``'masks'``: per-annotator segmentation masks
1094+
- ``'boxes'``: (N, 4) float32 tensor in pascal_voc pixel coords
1095+
- ``'box_labels'``: (N,) int64 tensor of class indices
10251096
1097+
Returns:
1098+
Dict with ``'image'`` key plus the same target keys, all transformed.
10261099
"""
10271100
pass
10281101

@@ -1052,6 +1125,8 @@ def build_mlflow_dataset(self) -> 'DatamintMLflowDataset':
10521125
project_id = getattr(project, 'id', 'unknown') if project is not None else 'unknown'
10531126

10541127
extra_params = {
1128+
'return_segmentations': self.return_segmentations,
1129+
'return_boxes': self.return_boxes,
10551130
'return_as_semantic_segmentation': self.return_as_semantic_segmentation,
10561131
'semantic_seg_merge_strategy': str(self.semantic_seg_merge_strategy),
10571132
'include_unannotated': self.include_unannotated,

0 commit comments

Comments
 (0)