converter¶
The conversion behind the hbb2obb command: one image in, its OBBs out. How the conversion works describes the steps, and the guide has a worked example.
from pathlib import Path
from hbb2obb.converter import hbb2obb, save_obb_annotations
img_path = Path("data/images/04433.jpg")
obbs = hbb2obb(img_path, sam_models=["sam_l", "sam_b"], scale_factors=0.05) # (N, 9) array
save_obb_annotations(obbs, Path("data/labels_obb"), img_path)
Converting an image¶
hbb2obb
¶
hbb2obb(img_path: Path, hbb_dir: Path = None, sam_models: Union[str, List[str]] = 'sam_b.pt', imgsz: int = 1280, scale_factors: Union[float, Tuple[float, float], List[float]] = 0.05, opening_kernel_percentage: float = 0.15, fragment_ratio: float = DEFAULT_FRAGMENT_RATIO, save_img: bool = False, viz_dir: Path = None, show_hbb: bool = True, show_masks: bool = True, show_segments: bool = True, show_obb: bool = True, show_labels: bool = True, show_confidence: bool = False, model_kwargs: Dict[str, Any] = None, device: str = None, return_confidence: bool = False, confidence_source: str = 'conversion', return_contours: bool = False, return_stats: bool = False, models_dir: Optional[Path] = None) -> Union[ndarray, Tuple]
Convert HBB to OBB annotations using multiple SAM models and aggregating the masks by majority vote.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
img_path
|
Path
|
Path to the image |
required |
hbb_dir
|
Path
|
Directory containing HBB annotations |
None
|
sam_models
|
Union[str, List[str]]
|
Name(s) of SAM model(s) to use. Multiple models can be specified to average results |
'sam_b.pt'
|
imgsz
|
int
|
Image size for SAM model inference |
1280
|
scale_factors
|
Union[float, Tuple[float, float], List[float]]
|
Factor(s) to scale HBB bounding boxes. If single value: same factor for both dimensions. If two values: first for shorter side, second for longer side |
0.05
|
opening_kernel_percentage
|
float
|
Fraction of the mask's smaller dimension used as the morphological kernel. Positive opens (removes thin protrusions), negative closes (fills holes, rejoins fragments), 0 disables it |
0.15
|
fragment_ratio
|
float
|
Minimum area, as a fraction of the largest mask piece's, for another piece to be fitted together with it rather than discarded; 0 fits the largest piece alone |
DEFAULT_FRAGMENT_RATIO
|
save_img
|
bool
|
Save visualization images |
False
|
viz_dir
|
Path
|
Directory to save visualization images |
None
|
show_hbb
|
bool
|
Show horizontal bounding boxes |
True
|
show_masks
|
bool
|
Show segmentation masks |
True
|
show_segments
|
bool
|
Show segmentation contours |
True
|
show_obb
|
bool
|
Show oriented bounding boxes |
True
|
show_labels
|
bool
|
Show class labels |
True
|
show_confidence
|
bool
|
Print the per-OBB confidence score in the visualization |
False
|
model_kwargs
|
Dict[str, Any]
|
Additional keyword arguments for the SAM model |
None
|
device
|
str
|
Inference device, e.g. 'cpu', '0', 'cuda:0', 'mps' (default: ultralytics picks). An explicit 'device' in model_kwargs takes precedence. |
None
|
return_confidence
|
bool
|
If True, also return the per-OBB confidence scores |
False
|
confidence_source
|
str
|
Which score the returned confidences carry: 'conversion' (the heuristic conversion-quality score), 'detector' (the confidence read from the HBB input file), or 'combined' (their product) |
'conversion'
|
return_contours
|
bool
|
If True, also return the per-object segmentation contours, in absolute image pixel coordinates, with None where the OBB is a fallback HBB |
False
|
return_stats
|
bool
|
If True, also return a |
False
|
models_dir
|
Optional[Path]
|
Checkpoint directory (default: |
None
|
Returns:
| Type | Description |
|---|---|
Union[ndarray, Tuple]
|
OBB annotations as a numpy array, with the requested extras appended in a tuple, in the
order confidences, contours, stats. With no flag set, the array alone; with every flag,
The confidences and contours lists are always the same length as obb_annotations. |
Source code in hbb2obb/converter.py
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 | |
ConversionStats
dataclass
¶
What one frame's conversion produced, as opposed to what it was asked to produce.
A fallback box is one where no usable mask or contour was found and the source HBB was
emitted unchanged. It is recoverable afterwards only from a 0.0 confidence, and only when
the scores were written and --confidence_source left them alone, so a run that reports
nothing leaves a dataset release recomputing a number it should have been handed.
masks_missing counts prompts that came back without a mask of their own: ultralytics
drops a SAM mask whose predicted IoU is below conf, so a model can return fewer masks
than it was given boxes without returning none at all. That is the case a whole frame of
fallbacks looks like from the inside, and nothing used to say it had happened.
Source code in hbb2obb/converter.py
share
property
¶
Fraction of this frame's boxes that fell back, 0.0 for a frame with no boxes.
Writing the results¶
save_obb_annotations
¶
save_obb_annotations(obb_annotations: ndarray, obb_dir: Path, img_path: Path, confidences: List[float] = None, normalize: bool = False, img_shape: Tuple[int, int] = None, precision: int = DEFAULT_NORMALIZED_PRECISION)
Save OBB annotations to a text file.
Each line is class x1 y1 x2 y2 x3 y3 x4 y4, in absolute pixels unless normalize is
set, in which case the eight coordinates are relative to the frame and Ultralytics reads the
file unchanged. When confidences is provided, a 10th field with the per-OBB confidence
score is appended (... x4 y4 conf).
Source code in hbb2obb/converter.py
save_confidence_annotations
¶
Save the per-object confidence scores to their own file, one score per line.
Row-aligned with the OBB file for the same image: line i of both describes the same object.
This is the alternative to the 10th column save_obb_annotations writes, for annotation
sets whose label files have to stay strictly standard. Ultralytics, for one, rejects a 10th
column on a YOLO OBB label, so a release that wants both a usable label file and a per-box
score has to put the score somewhere else.
Source code in hbb2obb/converter.py
save_polygon_annotations
¶
save_polygon_annotations(contours: List[ndarray], obb_annotations: ndarray, polygon_dir: Path, img_path: Path, confidences: List[float] = None, simplify_epsilon: float = 0.0, normalize: bool = False, img_shape: Tuple[int, int] = None, precision: int = DEFAULT_NORMALIZED_PRECISION) -> None
Save the segmentation polygons to a text file, as a tighter alternative to the OBBs.
Each line is class x1 y1 x2 y2 ... xN yN, the corners in absolute pixel coordinates,
the same convention save_obb_annotations uses. When confidences is provided, the
per-object score is appended as a final field, exactly as the OBB writer does.
Row alignment is guaranteed: the file holds exactly one line per OBB, in the same order,
so line i here and line i of the OBB file for the same image describe the same object.
Objects that fell back to the HBB (no usable mask, contour None) are written as the
four corners of their OBB row rather than skipped, which makes a fallback recognizable
as a four-point polygon identical to its OBB line.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
contours
|
List[ndarray]
|
Per-object contours from hbb2obb(..., return_contours=True), None for fallbacks |
required |
obb_annotations
|
ndarray
|
Matching OBB annotations, supplying the class label and the fallback corners |
required |
polygon_dir
|
Path
|
Directory to save the polygon annotations |
required |
img_path
|
Path
|
Image the annotations belong to, used to derive the output file name |
required |
confidences
|
List[float]
|
Per-object confidence scores, written as a trailing field |
None
|
simplify_epsilon
|
float
|
If > 0, simplify each contour with cv2.approxPolyDP using an epsilon of this fraction of the contour perimeter (cv2.arcLength), so one value behaves consistently across object sizes; 0.005 to 0.02 are typical. The default 0 writes the raw contour. Fallback rectangles are never simplified. |
0.0
|
normalize
|
bool
|
Write coordinates relative to the frame size instead of in pixels. Follows the OBB file it sits beside, so one directory never mixes the two conventions. |
False
|
img_shape
|
Tuple[int, int]
|
(width, height) of the frame, required when |
None
|
precision
|
int
|
Decimal places for normalized output; ignored for absolute output |
DEFAULT_NORMALIZED_PRECISION
|
Source code in hbb2obb/converter.py
Pipeline steps¶
scale_bounding_boxes
¶
scale_bounding_boxes(annotations: Annotations, factors: Union[float, Tuple[float, float], List[float]]) -> ndarray
Scale bounding boxes according to specified factors while ensuring they stay within image dimensions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
annotations
|
Annotations
|
Annotations object containing bounding boxes and image dimensions |
required |
factors
|
Union[float, Tuple[float, float], List[float]]
|
Scale factor(s). If a single value, both dimensions are scaled by the same factor. If two values, the first applies to the shorter side and the second to the longer side. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Scaled bounding boxes |
Source code in hbb2obb/converter.py
aggregate_masks_by_majority_vote
¶
Aggregate multiple masks using majority voting. For each pixel, it's kept if it's confirmed by the majority of models.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
masks
|
List[ndarray]
|
List of boolean masks to aggregate |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Aggregated boolean mask |
Source code in hbb2obb/converter.py
apply_morphological_opening
¶
Applies a morphological opening or closing to a boolean mask.
The sign of kernel_percentage selects the operation and its magnitude sets the kernel
size, so one axis covers both directions and zero is the "off" point in the middle.
Positive is an opening (erode, then dilate), which removes thin protrusions and small specks. It can only take pixels away, so it tightens a mask that has bled into the road or into a neighbouring vehicle.
Negative is a closing (dilate, then erode), which fills holes and joins fragments. It can only add pixels. That matters here because the caller keeps the largest contour only: a vehicle split in two by windscreen glare, a roof-colour break or an occluding pole loses the smaller piece for good, and a closing is the only step in the pipeline that can put the two back together before that choice is made. Its risk is the mirror image of opening's: it can also reach across a narrow gap to a neighbour, bounded by the clip to the expanded HBB.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mask
|
ndarray
|
The input boolean mask (True for foreground) or None. |
required |
kernel_percentage
|
float
|
Fraction of the mask's smaller dimension to use as kernel size. Positive opens, negative closes, and 0 returns the mask unchanged, as does a None or empty mask. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The processed boolean mask, or None if the input was None. |
Source code in hbb2obb/converter.py
1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 | |
merge_fragment_contours
¶
merge_fragment_contours(contours: Sequence[ndarray], largest: ndarray, fragment_ratio: float) -> List[ndarray]
The contours to fit the rotated box to: the largest one, plus any other piece holding at
least fragment_ratio of its area.
Closing the mask rejoins fragments that are close enough to bridge; this catches the ones that are not, where the occluder is wider than the kernel. Everything here is already clipped to the expanded HBB, which is what bounds how far a merge can reach.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
contours
|
Sequence[ndarray]
|
All contours found in the mask, in any order |
required |
largest
|
ndarray
|
The largest valid contour, which is always kept |
required |
fragment_ratio
|
float
|
Minimum area of a second piece as a fraction of the largest one's; 0 or less keeps the largest contour alone |
required |
Returns:
| Type | Description |
|---|---|
List[ndarray]
|
The contours to fit together, always beginning with |
Source code in hbb2obb/converter.py
Models¶
load_sam_model
¶
Load a SAM/FastSAM model by name, reusing a cached instance when one is already loaded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name
|
str
|
Model name (e.g. "sam_b", "sam2.1_b", "FastSAM-s"), with or without a ".pt" suffix. |
required |
models_dir
|
Optional[Path]
|
Checkpoint directory (default: |
None
|
Returns:
| Type | Description |
|---|---|
|
The loaded ultralytics SAM or FastSAM model instance. |
Source code in hbb2obb/converter.py
clear_model_cache
¶
Clear the in-process SAM/FastSAM model cache, releasing the associated memory.
Dropping the last reference frees the weights, but on CUDA the caching allocator keeps the freed blocks reserved, shaped like the checkpoints that just went away. The next ensemble then meets a fragmented heap rather than an empty one, which is what stops a SAM image encoder from allocating the multi-gigabyte contiguous attention tensors it asks for. A benchmark clears the cache between runs precisely so the later, larger runs start clean.
Metal keeps freed blocks reserved the same way, so an Apple Silicon run needs the same
release; torch.mps is always importable and empty_cache() is a no-op where no
Metal device was ever used, so the branch costs nothing off that hardware.