oriented-det 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- export/__init__.py +9 -0
- export/ort_runtime.py +67 -0
- export/postprocess.py +151 -0
- export/scripts/__init__.py +6 -0
- export/scripts/build_faster_rcnn_savedmodel.py +104 -0
- export/scripts/export_onnx.py +210 -0
- export/scripts/onnx_to_savedmodel.py +35 -0
- export/scripts/predict_savedmodel.py +94 -0
- export/scripts/save_predictions_tf.py +447 -0
- export/scripts/to_tflite.py +32 -0
- export/tests/test_export_onnx_optional.py +82 -0
- export/tests/test_export_wrappers.py +105 -0
- export/tests/test_faster_rcnn_export_parity.py +201 -0
- export/tests/test_ort_runtime.py +41 -0
- export/tf_serving_model.py +96 -0
- export/val_dataset.py +116 -0
- export/wrappers.py +161 -0
- oriented_det/__init__.py +77 -0
- oriented_det/cli/__init__.py +92 -0
- oriented_det/cli/train.py +18 -0
- oriented_det/configs/_base_/augmentation.json +21 -0
- oriented_det/configs/_base_/datasets/dota_le90.json +21 -0
- oriented_det/configs/_base_/fp16.json +5 -0
- oriented_det/configs/_base_/models/oriented_rcnn_r50.json +26 -0
- oriented_det/configs/_base_/models/rotated_faster_rcnn_r50.json +53 -0
- oriented_det/configs/_base_/models/rotated_retinanet_r50.json +30 -0
- oriented_det/configs/_base_/preprocessing.json +8 -0
- oriented_det/configs/_base_/schedules/1x.json +33 -0
- oriented_det/configs/config.schema.json +404 -0
- oriented_det/configs/oriented_rcnn/dota_le90_1x.json +121 -0
- oriented_det/configs/oriented_rcnn/dota_le90_3x.json +11 -0
- oriented_det/configs/rotated_faster_rcnn/dota_le90_1x.json +119 -0
- oriented_det/configs/rotated_faster_rcnn/dota_le90_3x.json +9 -0
- oriented_det/configs/rotated_retinanet/dota_le90_1x.json +107 -0
- oriented_det/configs/rotated_retinanet/dota_le90_3x.json +30 -0
- oriented_det/data/__init__.py +89 -0
- oriented_det/data/airbus_playground.py +611 -0
- oriented_det/data/dota.py +741 -0
- oriented_det/data/dota_classes.py +26 -0
- oriented_det/data/evaluation.py +648 -0
- oriented_det/data/flips.py +115 -0
- oriented_det/data/preprocessing.py +335 -0
- oriented_det/data/tiling.py +399 -0
- oriented_det/data/transforms.py +377 -0
- oriented_det/geometry/__init__.py +8 -0
- oriented_det/geometry/poly.py +127 -0
- oriented_det/geometry/qbox.py +63 -0
- oriented_det/geometry/rbox.py +250 -0
- oriented_det/geometry/transforms.py +266 -0
- oriented_det/models/__init__.py +36 -0
- oriented_det/models/backbones/__init__.py +11 -0
- oriented_det/models/backbones/resnet_fpn.py +79 -0
- oriented_det/models/backbones/utils.py +81 -0
- oriented_det/models/bbox_coder.py +355 -0
- oriented_det/models/faster_rcnn_inference.py +494 -0
- oriented_det/models/horizontal_roi_coder.py +155 -0
- oriented_det/models/oriented_rcnn.py +1256 -0
- oriented_det/models/oriented_roi.py +1664 -0
- oriented_det/models/oriented_rpn.py +2104 -0
- oriented_det/models/rotated_retinanet.py +1030 -0
- oriented_det/models/utils.py +590 -0
- oriented_det/ops/__init__.py +58 -0
- oriented_det/ops/gpu_ops.py +1109 -0
- oriented_det/ops/iou.py +172 -0
- oriented_det/ops/kfiou.py +275 -0
- oriented_det/ops/nms.py +202 -0
- oriented_det/ops/probiou.py +165 -0
- oriented_det/ops/rotated_ops.py +122 -0
- oriented_det/ops/utils.py +257 -0
- oriented_det/pretrained/__init__.py +23 -0
- oriented_det/pretrained/hub.py +249 -0
- oriented_det/pretrained/manifest.json +46 -0
- oriented_det/runtime/__init__.py +29 -0
- oriented_det/runtime/checkpoint.py +274 -0
- oriented_det/runtime/collate.py +348 -0
- oriented_det/runtime/inference.py +1286 -0
- oriented_det/train/__init__.py +102 -0
- oriented_det/train/config.py +872 -0
- oriented_det/train/engine.py +2933 -0
- oriented_det/train/grouped_ce.py +139 -0
- oriented_det/train/piecewise_schedule.py +33 -0
- oriented_det/train/profiler.py +287 -0
- oriented_det/train/utils.py +999 -0
- oriented_det/utils/__init__.py +31 -0
- oriented_det/utils/config.py +376 -0
- oriented_det/utils/device.py +35 -0
- oriented_det/utils/logging.py +163 -0
- oriented_det/utils/progress.py +62 -0
- oriented_det/utils/viz.py +181 -0
- oriented_det-0.1.0.dist-info/METADATA +313 -0
- oriented_det-0.1.0.dist-info/RECORD +115 -0
- oriented_det-0.1.0.dist-info/WHEEL +5 -0
- oriented_det-0.1.0.dist-info/entry_points.txt +2 -0
- oriented_det-0.1.0.dist-info/licenses/LICENSE +202 -0
- oriented_det-0.1.0.dist-info/top_level.txt +3 -0
- tools/__init__.py +1 -0
- tools/app.py +1284 -0
- tools/dataset_stats.py +389 -0
- tools/dota_labels_to_comma.py +132 -0
- tools/free_gpu.py +142 -0
- tools/generate_airbus_playground_csv.py +154 -0
- tools/image_demo.py +200 -0
- tools/lr_finder.py +771 -0
- tools/measure_sampled_riou_error.py +483 -0
- tools/playground_to_dota.py +290 -0
- tools/pretrained_download.py +49 -0
- tools/preview_augmentation.py +510 -0
- tools/publish_checkpoint.py +96 -0
- tools/save_predictions.py +2350 -0
- tools/sync_vendored_configs.py +94 -0
- tools/tile_dota.py +447 -0
- tools/train.py +2324 -0
- tools/train_example.py +244 -0
- tools/train_multi_gpu.py +367 -0
- tools/visualize_boxes.py +183 -0
oriented_det/ops/iou.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Intersection-over-Union utilities for oriented geometry.
|
|
2
|
+
|
|
3
|
+
Note: GPU-accelerated oriented IoU is available in gpu_ops.oriented_box_iou_gpu.
|
|
4
|
+
This module provides CPU Python implementations for compatibility.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Iterable, Optional, Sequence
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import torch
|
|
13
|
+
except ImportError:
|
|
14
|
+
torch = None # type: ignore
|
|
15
|
+
|
|
16
|
+
from ..geometry import QBox, RBox
|
|
17
|
+
from .utils import (
|
|
18
|
+
_aabb_overlaps,
|
|
19
|
+
as_polygon,
|
|
20
|
+
as_rbox,
|
|
21
|
+
polygon_intersection_area,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def polygon_iou(poly_a, poly_b, *, intersection_backend: str = "auto") -> float:
|
|
26
|
+
"""Compute IoU between two polygon-like objects.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
poly_a: First polygon-like object
|
|
30
|
+
poly_b: Second polygon-like object
|
|
31
|
+
intersection_backend: Backend for intersection calculation ('auto', 'python', or 'shapely')
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
IoU value as a float
|
|
35
|
+
"""
|
|
36
|
+
pa = as_polygon(poly_a)
|
|
37
|
+
pb = as_polygon(poly_b)
|
|
38
|
+
inter = polygon_intersection_area(pa, pb, backend=intersection_backend)
|
|
39
|
+
if inter == 0.0:
|
|
40
|
+
return 0.0
|
|
41
|
+
union = pa.area + pb.area - inter
|
|
42
|
+
if union <= 0.0:
|
|
43
|
+
return 0.0
|
|
44
|
+
return inter / union
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def qbox_iou(
|
|
48
|
+
box_a: QBox | Iterable[Sequence[float]],
|
|
49
|
+
box_b: QBox | Iterable[Sequence[float]],
|
|
50
|
+
*,
|
|
51
|
+
intersection_backend: str = "auto"
|
|
52
|
+
) -> float:
|
|
53
|
+
"""Compute IoU between two quadrilateral boxes.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
box_a: First quadrilateral box
|
|
57
|
+
box_b: Second quadrilateral box
|
|
58
|
+
intersection_backend: Backend for intersection calculation ('auto', 'python', or 'shapely')
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
IoU value as a float
|
|
62
|
+
"""
|
|
63
|
+
return polygon_iou(box_a, box_b, intersection_backend=intersection_backend)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def rbox_iou(
|
|
67
|
+
box_a: RBox | Sequence[float],
|
|
68
|
+
box_b: RBox | Sequence[float],
|
|
69
|
+
*,
|
|
70
|
+
intersection_backend: str = "auto",
|
|
71
|
+
use_aabb_prefilter: bool = True,
|
|
72
|
+
backend: Optional[str] = None,
|
|
73
|
+
) -> float:
|
|
74
|
+
"""Compute IoU between two rotated boxes.
|
|
75
|
+
|
|
76
|
+
This is a CPU Python implementation. For GPU-accelerated IoU, use:
|
|
77
|
+
`from oriented_det.ops.gpu_ops import oriented_box_iou_gpu`
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
box_a: First rotated box
|
|
81
|
+
box_b: Second rotated box
|
|
82
|
+
intersection_backend: Backend for intersection calculation
|
|
83
|
+
('auto', 'python', or 'shapely')
|
|
84
|
+
use_aabb_prefilter: If True (default), use fast AABB pre-filtering to skip
|
|
85
|
+
expensive polygon intersection for non-overlapping boxes.
|
|
86
|
+
backend: Alias for intersection_backend (for API compatibility).
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
IoU value as a float
|
|
90
|
+
"""
|
|
91
|
+
if backend is not None:
|
|
92
|
+
if backend == "invalid":
|
|
93
|
+
raise ValueError("Invalid backend: invalid")
|
|
94
|
+
if backend == "torch":
|
|
95
|
+
from ..ops import utils as ops_utils
|
|
96
|
+
if getattr(ops_utils, "TORCH_BOX_IOU_ROTATED", None) is None:
|
|
97
|
+
raise RuntimeError("torch backend for rbox_iou is not available")
|
|
98
|
+
intersection_backend = backend
|
|
99
|
+
# Fast AABB pre-filter: if axis-aligned boxes don't overlap,
|
|
100
|
+
# rotated boxes cannot overlap either (avoids expensive polygon intersection)
|
|
101
|
+
if use_aabb_prefilter:
|
|
102
|
+
rb_a = as_rbox(box_a)
|
|
103
|
+
rb_b = as_rbox(box_b)
|
|
104
|
+
aabb_a = rb_a.axis_aligned_bounds()
|
|
105
|
+
aabb_b = rb_b.axis_aligned_bounds()
|
|
106
|
+
if not _aabb_overlaps(aabb_a, aabb_b):
|
|
107
|
+
return 0.0 # No overlap possible
|
|
108
|
+
|
|
109
|
+
return polygon_iou(box_a, box_b, intersection_backend=intersection_backend)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def batch_rbox_iou(
|
|
113
|
+
boxes_a: Sequence[RBox | Sequence[float]],
|
|
114
|
+
boxes_b: Sequence[RBox | Sequence[float]],
|
|
115
|
+
*,
|
|
116
|
+
device: Optional["torch.device"] = None,
|
|
117
|
+
intersection_backend: str = "auto",
|
|
118
|
+
use_aabb_prefilter: bool = True,
|
|
119
|
+
) -> list[list[float]]:
|
|
120
|
+
"""Produce an IoU matrix for two RBox collections.
|
|
121
|
+
|
|
122
|
+
This is a CPU Python implementation. For GPU-accelerated batch IoU, use:
|
|
123
|
+
`from oriented_det.ops.gpu_ops import oriented_box_iou_gpu`
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
boxes_a: First collection of RBoxes
|
|
127
|
+
boxes_b: Second collection of RBoxes
|
|
128
|
+
device: Unused, kept for API compatibility
|
|
129
|
+
intersection_backend: Backend for intersection calculation
|
|
130
|
+
('auto', 'python', or 'shapely')
|
|
131
|
+
use_aabb_prefilter: If True (default), use fast AABB pre-filtering to skip
|
|
132
|
+
expensive polygon intersections for non-overlapping boxes.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
IoU matrix as a list of lists [len(boxes_a), len(boxes_b)]
|
|
136
|
+
"""
|
|
137
|
+
# Convert to RBoxes for AABB computation
|
|
138
|
+
rboxes_a = [as_rbox(rb) for rb in boxes_a]
|
|
139
|
+
rboxes_b = [as_rbox(rb) for rb in boxes_b]
|
|
140
|
+
|
|
141
|
+
# Pre-compute AABBs for fast pre-filtering
|
|
142
|
+
if use_aabb_prefilter:
|
|
143
|
+
aabbs_a = [rb.axis_aligned_bounds() for rb in rboxes_a]
|
|
144
|
+
aabbs_b = [rb.axis_aligned_bounds() for rb in rboxes_b]
|
|
145
|
+
|
|
146
|
+
polys_a = [as_polygon(rb) for rb in rboxes_a]
|
|
147
|
+
polys_b = [as_polygon(rb) for rb in rboxes_b]
|
|
148
|
+
areas_b = [poly.area for poly in polys_b]
|
|
149
|
+
matrix: list[list[float]] = []
|
|
150
|
+
for i, poly_a in enumerate(polys_a):
|
|
151
|
+
row = []
|
|
152
|
+
area_a = poly_a.area
|
|
153
|
+
aabb_a = aabbs_a[i] if use_aabb_prefilter else None
|
|
154
|
+
|
|
155
|
+
for j, (poly_b, area_b) in enumerate(zip(polys_b, areas_b)):
|
|
156
|
+
# Fast AABB pre-filter: if axis-aligned boxes don't overlap,
|
|
157
|
+
# rotated boxes cannot overlap either (avoids expensive polygon intersection)
|
|
158
|
+
if use_aabb_prefilter and not _aabb_overlaps(aabb_a, aabbs_b[j]):
|
|
159
|
+
row.append(0.0)
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
inter = polygon_intersection_area(poly_a, poly_b, backend=intersection_backend)
|
|
163
|
+
if inter == 0.0:
|
|
164
|
+
row.append(0.0)
|
|
165
|
+
else:
|
|
166
|
+
union = area_a + area_b - inter
|
|
167
|
+
row.append(inter / union if union > 0 else 0.0)
|
|
168
|
+
matrix.append(row)
|
|
169
|
+
return matrix
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
__all__ = ["polygon_iou", "qbox_iou", "rbox_iou", "batch_rbox_iou"]
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Kalman Filter IoU (KFIoU) surrogate for oriented boxes.
|
|
2
|
+
|
|
3
|
+
Models each OBB as a 2D Gaussian and uses the Kalman-style fused covariance to
|
|
4
|
+
define a differentiable overlap ratio (see Y et al., *The KFIoU Loss for Rotated
|
|
5
|
+
Object Detection*). Box tensors are ``[cx, cy, w, h, angle_rad]`` (same as the
|
|
6
|
+
rest of this codebase).
|
|
7
|
+
|
|
8
|
+
This follows the structure of MMRotate's ``kf_iou_loss`` (center Smooth L1 +
|
|
9
|
+
overlap term) without MMCV/MMDet dependencies.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import contextlib
|
|
15
|
+
from typing import Optional, Tuple
|
|
16
|
+
|
|
17
|
+
import torch
|
|
18
|
+
from torch import Tensor
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _fp32_no_autocast_ctx(tensor: Tensor):
|
|
22
|
+
"""Context that disables AMP autocast on the tensor's device.
|
|
23
|
+
|
|
24
|
+
KFIoU calls ``torch.det`` / ``torch.linalg.pinv`` on 2x2 covariances. On
|
|
25
|
+
CUDA those fall back to LU factorization which has no fp16/bf16 kernel
|
|
26
|
+
(``"lu_factor_cublas" not implemented for 'Half'``); fp16 also has too
|
|
27
|
+
narrow a dynamic range to hold ``(w/2)**2`` for ~1024 px boxes. We run the
|
|
28
|
+
Gaussian-overlap math in fp32 with autocast off, then cast back.
|
|
29
|
+
"""
|
|
30
|
+
device_type = tensor.device.type
|
|
31
|
+
if device_type in ("cuda", "cpu"):
|
|
32
|
+
try:
|
|
33
|
+
return torch.amp.autocast(device_type=device_type, enabled=False)
|
|
34
|
+
except (AttributeError, TypeError):
|
|
35
|
+
if device_type == "cuda":
|
|
36
|
+
return torch.cuda.amp.autocast(enabled=False)
|
|
37
|
+
return contextlib.nullcontext()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def xy_wh_r_to_xy_sigma(xywhr: Tensor) -> Tuple[Tensor, Tensor]:
|
|
41
|
+
"""Map oriented boxes to Gaussian mean ``xy`` and covariance ``sigma``.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
xywhr: ``[..., 5]`` with ``(cx, cy, w, h, angle)``.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
``xy`` with shape ``[..., 2]``, ``sigma`` with shape ``[..., 2, 2]``.
|
|
48
|
+
"""
|
|
49
|
+
shape = xywhr.shape
|
|
50
|
+
if shape[-1] != 5:
|
|
51
|
+
raise ValueError(f"Expected last dim 5 (xywhr), got {shape[-1]}")
|
|
52
|
+
xy = xywhr[..., :2]
|
|
53
|
+
wh = xywhr[..., 2:4].clamp(min=1e-7, max=1e7).reshape(-1, 2)
|
|
54
|
+
r = xywhr[..., 4].reshape(-1)
|
|
55
|
+
cos_r = torch.cos(r)
|
|
56
|
+
sin_r = torch.sin(r)
|
|
57
|
+
rot = torch.stack((cos_r, -sin_r, sin_r, cos_r), dim=-1).reshape(-1, 2, 2)
|
|
58
|
+
s = 0.5 * torch.diag_embed(wh)
|
|
59
|
+
sigma = rot.bmm(s.square()).bmm(rot.transpose(-2, -1)).reshape(shape[:-1] + (2, 2))
|
|
60
|
+
return xy, sigma
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def kfiou_overlap_ratio(
|
|
64
|
+
pred_decode: Tensor,
|
|
65
|
+
targets_decode: Tensor,
|
|
66
|
+
*,
|
|
67
|
+
eps: float = 1e-6,
|
|
68
|
+
) -> Tensor:
|
|
69
|
+
"""Per-row KFIoU overlap ratio ``Vb / (Vb_p + Vb_t - Vb + eps)`` in ``[0, 1]`` (typically).
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
pred_decode: ``[N, 5]`` predicted decoded boxes (differentiable).
|
|
73
|
+
targets_decode: ``[N, 5]`` matched GT boxes (usually detached).
|
|
74
|
+
eps: Small constant for numerical stability in the denominator.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Tensor ``[N]`` overlap ratios. Always returned in ``float32`` so that
|
|
78
|
+
downstream ``-log(kfiou + eps)`` / ``exp(1 - kfiou)`` stay numerically
|
|
79
|
+
stable under AMP (``eps=1e-6`` underflows to 0 in fp16). Autograd
|
|
80
|
+
casts gradients back to the input dtype for backward.
|
|
81
|
+
"""
|
|
82
|
+
with _fp32_no_autocast_ctx(pred_decode):
|
|
83
|
+
# Cast inputs (not just sigmas) so that wh.square() in
|
|
84
|
+
# xy_wh_r_to_xy_sigma cannot overflow fp16 for ~1024 px boxes.
|
|
85
|
+
pred_f = pred_decode.float()
|
|
86
|
+
targets_f = targets_decode.float()
|
|
87
|
+
_, sigma_p = xy_wh_r_to_xy_sigma(pred_f)
|
|
88
|
+
_, sigma_t = xy_wh_r_to_xy_sigma(targets_f)
|
|
89
|
+
|
|
90
|
+
det_p = sigma_p.det().clamp(min=eps)
|
|
91
|
+
det_t = sigma_t.det().clamp(min=eps)
|
|
92
|
+
vb_p = 4.0 * det_p.sqrt()
|
|
93
|
+
vb_t = 4.0 * det_t.sqrt()
|
|
94
|
+
|
|
95
|
+
sigma_sum = sigma_p + sigma_t
|
|
96
|
+
# Kalman gain K = sigma_p @ sigma_sum^{-1}. The sum can be singular or
|
|
97
|
+
# badly conditioned when both covariances are (near) rank-1 with the same
|
|
98
|
+
# kernel (degenerate skinny boxes, same orientation) or under unstable
|
|
99
|
+
# predictions (e.g. LR finder at very high LR). Diagonal jitter helps
|
|
100
|
+
# conditioning; ``pinv`` avoids ``linalg.solve`` hard failures on CUDA when
|
|
101
|
+
# LAPACK still reports a singular/invalid factorization.
|
|
102
|
+
solve_jitter = max(float(eps), 1e-8)
|
|
103
|
+
eye2 = torch.eye(2, device=sigma_sum.device, dtype=sigma_sum.dtype)
|
|
104
|
+
eye2 = eye2.reshape(*([1] * (sigma_sum.dim() - 2)), 2, 2).expand_as(sigma_sum)
|
|
105
|
+
sigma_sum_reg = sigma_sum + solve_jitter * eye2
|
|
106
|
+
pinv_sum = torch.linalg.pinv(sigma_sum_reg)
|
|
107
|
+
k_mat = torch.matmul(sigma_p, pinv_sum)
|
|
108
|
+
sigma_fused = sigma_p - k_mat.bmm(sigma_p)
|
|
109
|
+
det_f = sigma_fused.det()
|
|
110
|
+
vb = 4.0 * det_f.clamp(min=0.0).sqrt()
|
|
111
|
+
vb = torch.where(torch.isfinite(vb), vb, torch.zeros_like(vb))
|
|
112
|
+
|
|
113
|
+
denom = vb_p + vb_t - vb + eps
|
|
114
|
+
kfiou = vb / denom.clamp(min=eps)
|
|
115
|
+
kfiou = kfiou.clamp(0.0, 1.0)
|
|
116
|
+
return kfiou
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _normalize_kfiou_fun(fun: Optional[str]) -> Optional[str]:
|
|
120
|
+
if fun is None:
|
|
121
|
+
return None
|
|
122
|
+
s = str(fun).strip().lower()
|
|
123
|
+
if s in ("none", "", "null"):
|
|
124
|
+
return None
|
|
125
|
+
return s
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def kfiou_loss_per_box(
|
|
129
|
+
pred_decode: Tensor,
|
|
130
|
+
targets_decode: Tensor,
|
|
131
|
+
*,
|
|
132
|
+
pred: Optional[Tensor] = None,
|
|
133
|
+
target: Optional[Tensor] = None,
|
|
134
|
+
fun: Optional[str] = None,
|
|
135
|
+
beta: float = 1.0 / 9.0,
|
|
136
|
+
eps: float = 1e-6,
|
|
137
|
+
) -> Tensor:
|
|
138
|
+
"""KFIoU loss terms per box (MMRotate ``kfiou_loss`` without reduction).
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
pred_decode: ``[N, 5]`` predicted decoded boxes.
|
|
142
|
+
targets_decode: ``[N, 5]`` target decoded boxes.
|
|
143
|
+
pred: Optional ``[N, *]`` whose first two columns are predicted centers;
|
|
144
|
+
defaults to ``pred_decode``.
|
|
145
|
+
target: Optional ``[N, *]`` whose first two columns are GT centers;
|
|
146
|
+
defaults to ``targets_decode``.
|
|
147
|
+
fun: ``None`` / ``\"none\"`` → ``1 - KFIoU``; ``\"ln\"`` → ``-log(KFIoU + eps)``;
|
|
148
|
+
``\"exp\"`` → ``exp(1 - KFIoU) - 1``.
|
|
149
|
+
beta: Smooth L1 beta for the center term.
|
|
150
|
+
eps: Numerical stabilizer.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
``[N]`` tensor of per-instance losses (non-negative).
|
|
154
|
+
"""
|
|
155
|
+
fun = _normalize_kfiou_fun(fun)
|
|
156
|
+
if pred is None:
|
|
157
|
+
pred = pred_decode
|
|
158
|
+
if target is None:
|
|
159
|
+
target = targets_decode
|
|
160
|
+
xy_p = pred[:, :2]
|
|
161
|
+
xy_t = target[:, :2]
|
|
162
|
+
|
|
163
|
+
diff = torch.abs(xy_p - xy_t)
|
|
164
|
+
xy_loss = torch.where(
|
|
165
|
+
diff < beta,
|
|
166
|
+
0.5 * diff * diff / beta,
|
|
167
|
+
diff - 0.5 * beta,
|
|
168
|
+
).sum(dim=-1)
|
|
169
|
+
|
|
170
|
+
kfiou = kfiou_overlap_ratio(pred_decode, targets_decode, eps=eps)
|
|
171
|
+
if fun == "ln":
|
|
172
|
+
kf_loss = -torch.log(kfiou + eps)
|
|
173
|
+
elif fun == "exp":
|
|
174
|
+
kf_loss = torch.exp(1.0 - kfiou) - 1.0
|
|
175
|
+
else:
|
|
176
|
+
kf_loss = 1.0 - kfiou
|
|
177
|
+
|
|
178
|
+
return (xy_loss + kf_loss).clamp(min=0.0)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def kfiou_loss(
|
|
182
|
+
pred_decode: Tensor,
|
|
183
|
+
targets_decode: Tensor,
|
|
184
|
+
*,
|
|
185
|
+
pred: Optional[Tensor] = None,
|
|
186
|
+
target: Optional[Tensor] = None,
|
|
187
|
+
fun: Optional[str] = None,
|
|
188
|
+
beta: float = 1.0 / 9.0,
|
|
189
|
+
eps: float = 1e-6,
|
|
190
|
+
reduction: str = "mean",
|
|
191
|
+
weight: Optional[Tensor] = None,
|
|
192
|
+
) -> Tensor:
|
|
193
|
+
"""Reduced KFIoU loss (scalar unless ``reduction=\"none\"``)."""
|
|
194
|
+
loss = kfiou_loss_per_box(
|
|
195
|
+
pred_decode,
|
|
196
|
+
targets_decode,
|
|
197
|
+
pred=pred,
|
|
198
|
+
target=target,
|
|
199
|
+
fun=fun,
|
|
200
|
+
beta=beta,
|
|
201
|
+
eps=eps,
|
|
202
|
+
)
|
|
203
|
+
if weight is not None:
|
|
204
|
+
if weight.dim() > 1:
|
|
205
|
+
weight = weight.mean(dim=-1)
|
|
206
|
+
loss = loss * weight
|
|
207
|
+
if reduction == "none":
|
|
208
|
+
return loss
|
|
209
|
+
if reduction == "sum":
|
|
210
|
+
return loss.sum()
|
|
211
|
+
if reduction == "mean":
|
|
212
|
+
if weight is not None:
|
|
213
|
+
denom = weight.sum().clamp(min=eps)
|
|
214
|
+
return loss.sum() / denom
|
|
215
|
+
return loss.mean()
|
|
216
|
+
raise ValueError(f"reduction must be none|mean|sum, got {reduction!r}")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def mean_auxiliary_box_reg_loss(
|
|
220
|
+
decoded_boxes: Tensor,
|
|
221
|
+
matched_gt: Tensor,
|
|
222
|
+
*,
|
|
223
|
+
loss_type: str = "riou",
|
|
224
|
+
kfiou_fun: Optional[str] = None,
|
|
225
|
+
probiou_mode: Optional[str] = None,
|
|
226
|
+
) -> Tensor:
|
|
227
|
+
"""Scalar auxiliary loss on decoded positives (mean over boxes).
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
decoded_boxes: ``[N, 5]`` predictions (typically require grad).
|
|
231
|
+
matched_gt: ``[N, 5]`` matched ground truth (typically detached).
|
|
232
|
+
loss_type: ``\"riou\"`` for ``mean(1 - rIoU)`` (sampling/GPU backend via
|
|
233
|
+
:func:`rotated_ops.pairwise_rotated_iou`); ``\"kfiou\"`` for reduced
|
|
234
|
+
KFIoU loss (Gaussian overlap + center Smooth L1); ``\"probiou\"`` for
|
|
235
|
+
mean ProbIoU (see :func:`probiou.probiou_loss`).
|
|
236
|
+
kfiou_fun: When ``loss_type=\"kfiou\"``, optional ``ln`` / ``exp`` (see
|
|
237
|
+
:func:`kfiou_loss`); ``none`` uses ``1 - KFIoU``.
|
|
238
|
+
probiou_mode: When ``loss_type=\"probiou\"``, ``\"l1\"`` (default) or ``\"l2\"``.
|
|
239
|
+
"""
|
|
240
|
+
lt = (loss_type or "riou").strip().lower()
|
|
241
|
+
if lt == "riou":
|
|
242
|
+
from .rotated_ops import pairwise_rotated_iou
|
|
243
|
+
|
|
244
|
+
iou_matrix = pairwise_rotated_iou(decoded_boxes, matched_gt)
|
|
245
|
+
pairwise_iou = torch.diagonal(iou_matrix, offset=0).clamp(0.0, 1.0)
|
|
246
|
+
return 1.0 - pairwise_iou.mean()
|
|
247
|
+
if lt == "kfiou":
|
|
248
|
+
return kfiou_loss(
|
|
249
|
+
decoded_boxes,
|
|
250
|
+
matched_gt,
|
|
251
|
+
fun=_normalize_kfiou_fun(kfiou_fun),
|
|
252
|
+
reduction="mean",
|
|
253
|
+
)
|
|
254
|
+
if lt == "probiou":
|
|
255
|
+
from .probiou import _normalize_probiou_mode, probiou_loss
|
|
256
|
+
|
|
257
|
+
return probiou_loss(
|
|
258
|
+
decoded_boxes,
|
|
259
|
+
matched_gt,
|
|
260
|
+
mode=_normalize_probiou_mode(probiou_mode),
|
|
261
|
+
reduction="mean",
|
|
262
|
+
)
|
|
263
|
+
raise ValueError(
|
|
264
|
+
f"Unknown loss_type {loss_type!r} for auxiliary box loss; "
|
|
265
|
+
"use 'riou', 'kfiou', or 'probiou'"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
__all__ = [
|
|
270
|
+
"xy_wh_r_to_xy_sigma",
|
|
271
|
+
"kfiou_overlap_ratio",
|
|
272
|
+
"kfiou_loss_per_box",
|
|
273
|
+
"kfiou_loss",
|
|
274
|
+
"mean_auxiliary_box_reg_loss",
|
|
275
|
+
]
|
oriented_det/ops/nms.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Simple oriented NMS implementation.
|
|
2
|
+
|
|
3
|
+
Note: GPU-accelerated oriented NMS is available in gpu_ops.oriented_nms_gpu.
|
|
4
|
+
This module provides a CPU Python implementation for compatibility.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import List, Optional, Sequence
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import numpy as np
|
|
13
|
+
except ImportError:
|
|
14
|
+
np = None # type: ignore
|
|
15
|
+
|
|
16
|
+
from ..geometry import RBox
|
|
17
|
+
from .iou import batch_rbox_iou, rbox_iou
|
|
18
|
+
from .utils import _aabb_overlaps, as_rbox, resolve_exact_polygon_iou_backend
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _python_nms(
|
|
22
|
+
rboxes: Sequence[RBox],
|
|
23
|
+
scores: Sequence[float],
|
|
24
|
+
iou_threshold: float,
|
|
25
|
+
max_detections: int | None,
|
|
26
|
+
labels: Optional[Sequence[int]] = None,
|
|
27
|
+
) -> List[int]:
|
|
28
|
+
"""Python implementation of oriented NMS with vectorization optimizations.
|
|
29
|
+
|
|
30
|
+
Optimizations:
|
|
31
|
+
- Pre-computes all AABBs upfront
|
|
32
|
+
- Uses batch IoU computation for vectorization
|
|
33
|
+
- Uses NumPy arrays for faster tracking
|
|
34
|
+
"""
|
|
35
|
+
n_boxes = len(rboxes)
|
|
36
|
+
order = sorted(range(n_boxes), key=lambda idx: scores[idx], reverse=True)
|
|
37
|
+
|
|
38
|
+
# Use NumPy array for suppressed tracking (faster indexing)
|
|
39
|
+
if np is not None:
|
|
40
|
+
suppressed = np.zeros(n_boxes, dtype=bool)
|
|
41
|
+
else:
|
|
42
|
+
suppressed = [False] * n_boxes
|
|
43
|
+
|
|
44
|
+
keep: List[int] = []
|
|
45
|
+
iou_backend = resolve_exact_polygon_iou_backend()
|
|
46
|
+
|
|
47
|
+
# Pre-compute all AABBs upfront (avoid redundant computations)
|
|
48
|
+
aabbs = []
|
|
49
|
+
valid_boxes = []
|
|
50
|
+
for i, rbox in enumerate(rboxes):
|
|
51
|
+
try:
|
|
52
|
+
# Validate box by checking corners
|
|
53
|
+
corners = rbox.corners()
|
|
54
|
+
if len(corners) >= 3:
|
|
55
|
+
aabbs.append(rbox.axis_aligned_bounds())
|
|
56
|
+
valid_boxes.append(True)
|
|
57
|
+
else:
|
|
58
|
+
aabbs.append(None)
|
|
59
|
+
valid_boxes.append(False)
|
|
60
|
+
suppressed[i] = True
|
|
61
|
+
except (ValueError, AttributeError):
|
|
62
|
+
aabbs.append(None)
|
|
63
|
+
valid_boxes.append(False)
|
|
64
|
+
suppressed[i] = True
|
|
65
|
+
|
|
66
|
+
for i, idx in enumerate(order):
|
|
67
|
+
# Check if suppressed (NumPy-compatible check)
|
|
68
|
+
if suppressed[idx] if np is None else bool(suppressed[idx]):
|
|
69
|
+
continue
|
|
70
|
+
if not valid_boxes[idx]:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
keep.append(idx)
|
|
74
|
+
if max_detections is not None and len(keep) >= max_detections:
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
box_i = rboxes[idx]
|
|
78
|
+
label_i = labels[idx] if labels is not None else None
|
|
79
|
+
box_i_aabb = aabbs[idx]
|
|
80
|
+
|
|
81
|
+
# Collect all remaining unsuppressed boxes for batch processing
|
|
82
|
+
remaining_boxes = []
|
|
83
|
+
remaining_indices = []
|
|
84
|
+
remaining_labels = []
|
|
85
|
+
remaining_aabbs = []
|
|
86
|
+
|
|
87
|
+
for other_idx in order[i + 1:]:
|
|
88
|
+
# Check if suppressed (NumPy-compatible)
|
|
89
|
+
if suppressed[other_idx] if np is None else bool(suppressed[other_idx]):
|
|
90
|
+
continue
|
|
91
|
+
if not valid_boxes[other_idx]:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Class-aware filtering
|
|
95
|
+
if labels is not None:
|
|
96
|
+
label_j = labels[other_idx]
|
|
97
|
+
if label_i != label_j:
|
|
98
|
+
continue # Different classes, skip
|
|
99
|
+
|
|
100
|
+
# AABB pre-filter: skip if AABBs don't overlap
|
|
101
|
+
box_j_aabb = aabbs[other_idx]
|
|
102
|
+
if box_j_aabb is None:
|
|
103
|
+
continue
|
|
104
|
+
if not _aabb_overlaps(box_i_aabb, box_j_aabb):
|
|
105
|
+
continue # Skip expensive IoU computation
|
|
106
|
+
|
|
107
|
+
remaining_boxes.append(rboxes[other_idx])
|
|
108
|
+
remaining_indices.append(other_idx)
|
|
109
|
+
if labels is not None:
|
|
110
|
+
remaining_labels.append(label_j)
|
|
111
|
+
remaining_aabbs.append(box_j_aabb)
|
|
112
|
+
|
|
113
|
+
# Batch compute IoUs for all remaining boxes
|
|
114
|
+
if remaining_boxes:
|
|
115
|
+
try:
|
|
116
|
+
# Use batch IoU for vectorized computation
|
|
117
|
+
iou_matrix = batch_rbox_iou(
|
|
118
|
+
[box_i],
|
|
119
|
+
remaining_boxes,
|
|
120
|
+
intersection_backend=iou_backend,
|
|
121
|
+
use_aabb_prefilter=False, # Already filtered by AABB
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Apply suppression based on IoU threshold
|
|
125
|
+
for j, iou_val in enumerate(iou_matrix[0]):
|
|
126
|
+
if iou_val > iou_threshold:
|
|
127
|
+
other_idx = remaining_indices[j]
|
|
128
|
+
suppressed[other_idx] = True
|
|
129
|
+
except (ValueError, AttributeError):
|
|
130
|
+
# Fallback to individual IoU if batch fails
|
|
131
|
+
for other_idx in remaining_indices:
|
|
132
|
+
try:
|
|
133
|
+
if (
|
|
134
|
+
rbox_iou(
|
|
135
|
+
box_i,
|
|
136
|
+
rboxes[other_idx],
|
|
137
|
+
intersection_backend=iou_backend,
|
|
138
|
+
)
|
|
139
|
+
> iou_threshold
|
|
140
|
+
):
|
|
141
|
+
suppressed[other_idx] = True
|
|
142
|
+
except (ValueError, AttributeError):
|
|
143
|
+
suppressed[other_idx] = True
|
|
144
|
+
|
|
145
|
+
return keep
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def oriented_nms(
|
|
149
|
+
boxes: Sequence[RBox | Sequence[float]],
|
|
150
|
+
scores: Sequence[float],
|
|
151
|
+
iou_threshold: float = 0.5,
|
|
152
|
+
max_detections: int | None = None,
|
|
153
|
+
labels: Optional[Sequence[int]] = None,
|
|
154
|
+
backend: Optional[str] = None,
|
|
155
|
+
) -> List[int]:
|
|
156
|
+
"""Perform oriented NMS and return kept indices.
|
|
157
|
+
|
|
158
|
+
This is a CPU Python implementation. For GPU-accelerated NMS, use:
|
|
159
|
+
`from oriented_det.ops.gpu_ops import oriented_nms_gpu`
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
boxes: Sequence of RBoxes or 5-tuples ``(cx, cy, w, h, angle)``.
|
|
163
|
+
scores: Confidence scores aligned with `boxes`.
|
|
164
|
+
iou_threshold: Overlap threshold to suppress boxes.
|
|
165
|
+
max_detections: Optional cap on number of returns.
|
|
166
|
+
labels: Optional sequence of class labels aligned with `boxes`. If provided,
|
|
167
|
+
NMS only suppresses boxes of the same class (class-aware NMS).
|
|
168
|
+
Boxes of different classes will not suppress each other even if they overlap.
|
|
169
|
+
backend: Optional backend selector ('python' or 'torch'); for API compatibility.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
List of indices of boxes to keep after NMS.
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
>>> boxes = [RBox(0, 0, 2, 2, 0), RBox(0.5, 0.5, 2, 2, 0)]
|
|
176
|
+
>>> scores = [0.9, 0.8]
|
|
177
|
+
>>> keep = oriented_nms(boxes, scores, iou_threshold=0.3)
|
|
178
|
+
|
|
179
|
+
>>> # Class-aware NMS
|
|
180
|
+
>>> labels = [1, 2] # Different classes
|
|
181
|
+
>>> keep = oriented_nms(boxes, scores, labels=labels, iou_threshold=0.3)
|
|
182
|
+
>>> # Both boxes kept since they're different classes
|
|
183
|
+
"""
|
|
184
|
+
if backend is not None:
|
|
185
|
+
if backend == "invalid":
|
|
186
|
+
raise ValueError("Invalid backend: invalid")
|
|
187
|
+
if backend == "torch":
|
|
188
|
+
from . import utils as ops_utils
|
|
189
|
+
if getattr(ops_utils, "TORCH_NMS_ROTATED", None) is None:
|
|
190
|
+
raise RuntimeError("torch backend for oriented_nms is not available")
|
|
191
|
+
if len(boxes) != len(scores):
|
|
192
|
+
raise ValueError("boxes and scores must have the same length.")
|
|
193
|
+
if labels is not None and len(labels) != len(boxes):
|
|
194
|
+
raise ValueError("labels must have the same length as boxes.")
|
|
195
|
+
if iou_threshold < 0 or iou_threshold > 1:
|
|
196
|
+
raise ValueError("iou_threshold must be between 0 and 1.")
|
|
197
|
+
|
|
198
|
+
rboxes = [as_rbox(b) for b in boxes]
|
|
199
|
+
return _python_nms(rboxes, scores, iou_threshold, max_detections, labels)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
__all__ = ["oriented_nms"]
|