@@ -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