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.
Files changed (115) hide show
  1. export/__init__.py +9 -0
  2. export/ort_runtime.py +67 -0
  3. export/postprocess.py +151 -0
  4. export/scripts/__init__.py +6 -0
  5. export/scripts/build_faster_rcnn_savedmodel.py +104 -0
  6. export/scripts/export_onnx.py +210 -0
  7. export/scripts/onnx_to_savedmodel.py +35 -0
  8. export/scripts/predict_savedmodel.py +94 -0
  9. export/scripts/save_predictions_tf.py +447 -0
  10. export/scripts/to_tflite.py +32 -0
  11. export/tests/test_export_onnx_optional.py +82 -0
  12. export/tests/test_export_wrappers.py +105 -0
  13. export/tests/test_faster_rcnn_export_parity.py +201 -0
  14. export/tests/test_ort_runtime.py +41 -0
  15. export/tf_serving_model.py +96 -0
  16. export/val_dataset.py +116 -0
  17. export/wrappers.py +161 -0
  18. oriented_det/__init__.py +77 -0
  19. oriented_det/cli/__init__.py +92 -0
  20. oriented_det/cli/train.py +18 -0
  21. oriented_det/configs/_base_/augmentation.json +21 -0
  22. oriented_det/configs/_base_/datasets/dota_le90.json +21 -0
  23. oriented_det/configs/_base_/fp16.json +5 -0
  24. oriented_det/configs/_base_/models/oriented_rcnn_r50.json +26 -0
  25. oriented_det/configs/_base_/models/rotated_faster_rcnn_r50.json +53 -0
  26. oriented_det/configs/_base_/models/rotated_retinanet_r50.json +30 -0
  27. oriented_det/configs/_base_/preprocessing.json +8 -0
  28. oriented_det/configs/_base_/schedules/1x.json +33 -0
  29. oriented_det/configs/config.schema.json +404 -0
  30. oriented_det/configs/oriented_rcnn/dota_le90_1x.json +121 -0
  31. oriented_det/configs/oriented_rcnn/dota_le90_3x.json +11 -0
  32. oriented_det/configs/rotated_faster_rcnn/dota_le90_1x.json +119 -0
  33. oriented_det/configs/rotated_faster_rcnn/dota_le90_3x.json +9 -0
  34. oriented_det/configs/rotated_retinanet/dota_le90_1x.json +107 -0
  35. oriented_det/configs/rotated_retinanet/dota_le90_3x.json +30 -0
  36. oriented_det/data/__init__.py +89 -0
  37. oriented_det/data/airbus_playground.py +611 -0
  38. oriented_det/data/dota.py +741 -0
  39. oriented_det/data/dota_classes.py +26 -0
  40. oriented_det/data/evaluation.py +648 -0
  41. oriented_det/data/flips.py +115 -0
  42. oriented_det/data/preprocessing.py +335 -0
  43. oriented_det/data/tiling.py +399 -0
  44. oriented_det/data/transforms.py +377 -0
  45. oriented_det/geometry/__init__.py +8 -0
  46. oriented_det/geometry/poly.py +127 -0
  47. oriented_det/geometry/qbox.py +63 -0
  48. oriented_det/geometry/rbox.py +250 -0
  49. oriented_det/geometry/transforms.py +266 -0
  50. oriented_det/models/__init__.py +36 -0
  51. oriented_det/models/backbones/__init__.py +11 -0
  52. oriented_det/models/backbones/resnet_fpn.py +79 -0
  53. oriented_det/models/backbones/utils.py +81 -0
  54. oriented_det/models/bbox_coder.py +355 -0
  55. oriented_det/models/faster_rcnn_inference.py +494 -0
  56. oriented_det/models/horizontal_roi_coder.py +155 -0
  57. oriented_det/models/oriented_rcnn.py +1256 -0
  58. oriented_det/models/oriented_roi.py +1664 -0
  59. oriented_det/models/oriented_rpn.py +2104 -0
  60. oriented_det/models/rotated_retinanet.py +1030 -0
  61. oriented_det/models/utils.py +590 -0
  62. oriented_det/ops/__init__.py +58 -0
  63. oriented_det/ops/gpu_ops.py +1109 -0
  64. oriented_det/ops/iou.py +172 -0
  65. oriented_det/ops/kfiou.py +275 -0
  66. oriented_det/ops/nms.py +202 -0
  67. oriented_det/ops/probiou.py +165 -0
  68. oriented_det/ops/rotated_ops.py +122 -0
  69. oriented_det/ops/utils.py +257 -0
  70. oriented_det/pretrained/__init__.py +23 -0
  71. oriented_det/pretrained/hub.py +249 -0
  72. oriented_det/pretrained/manifest.json +46 -0
  73. oriented_det/runtime/__init__.py +29 -0
  74. oriented_det/runtime/checkpoint.py +274 -0
  75. oriented_det/runtime/collate.py +348 -0
  76. oriented_det/runtime/inference.py +1286 -0
  77. oriented_det/train/__init__.py +102 -0
  78. oriented_det/train/config.py +872 -0
  79. oriented_det/train/engine.py +2933 -0
  80. oriented_det/train/grouped_ce.py +139 -0
  81. oriented_det/train/piecewise_schedule.py +33 -0
  82. oriented_det/train/profiler.py +287 -0
  83. oriented_det/train/utils.py +999 -0
  84. oriented_det/utils/__init__.py +31 -0
  85. oriented_det/utils/config.py +376 -0
  86. oriented_det/utils/device.py +35 -0
  87. oriented_det/utils/logging.py +163 -0
  88. oriented_det/utils/progress.py +62 -0
  89. oriented_det/utils/viz.py +181 -0
  90. oriented_det-0.1.0.dist-info/METADATA +313 -0
  91. oriented_det-0.1.0.dist-info/RECORD +115 -0
  92. oriented_det-0.1.0.dist-info/WHEEL +5 -0
  93. oriented_det-0.1.0.dist-info/entry_points.txt +2 -0
  94. oriented_det-0.1.0.dist-info/licenses/LICENSE +202 -0
  95. oriented_det-0.1.0.dist-info/top_level.txt +3 -0
  96. tools/__init__.py +1 -0
  97. tools/app.py +1284 -0
  98. tools/dataset_stats.py +389 -0
  99. tools/dota_labels_to_comma.py +132 -0
  100. tools/free_gpu.py +142 -0
  101. tools/generate_airbus_playground_csv.py +154 -0
  102. tools/image_demo.py +200 -0
  103. tools/lr_finder.py +771 -0
  104. tools/measure_sampled_riou_error.py +483 -0
  105. tools/playground_to_dota.py +290 -0
  106. tools/pretrained_download.py +49 -0
  107. tools/preview_augmentation.py +510 -0
  108. tools/publish_checkpoint.py +96 -0
  109. tools/save_predictions.py +2350 -0
  110. tools/sync_vendored_configs.py +94 -0
  111. tools/tile_dota.py +447 -0
  112. tools/train.py +2324 -0
  113. tools/train_example.py +244 -0
  114. tools/train_multi_gpu.py +367 -0
  115. tools/visualize_boxes.py +183 -0
@@ -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
+ ]
@@ -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"]