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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""DOTA v1.0 canonical class list (aligned with MMRotate / common DOTA LE90 configs)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import FrozenSet
|
|
6
|
+
|
|
7
|
+
# Official 15 foreground categories; order matches MMRotate DOTADataset / typical 1x DOTA configs.
|
|
8
|
+
DOTA_V1_CLASSES: list[str] = [
|
|
9
|
+
"plane",
|
|
10
|
+
"baseball-diamond",
|
|
11
|
+
"bridge",
|
|
12
|
+
"ground-track-field",
|
|
13
|
+
"small-vehicle",
|
|
14
|
+
"large-vehicle",
|
|
15
|
+
"ship",
|
|
16
|
+
"tennis-court",
|
|
17
|
+
"basketball-court",
|
|
18
|
+
"storage-tank",
|
|
19
|
+
"soccer-ball-field",
|
|
20
|
+
"roundabout",
|
|
21
|
+
"harbor",
|
|
22
|
+
"swimming-pool",
|
|
23
|
+
"helicopter",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
DOTA_V1_CLASS_SET: FrozenSet[str] = frozenset(DOTA_V1_CLASSES)
|
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
"""Oriented mAP evaluation compatible with DOTA protocol.
|
|
2
|
+
|
|
3
|
+
Uses detection and ground-truth RBoxes exactly as provided (no coordinate rescaling).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
12
|
+
import math
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from tqdm import tqdm
|
|
17
|
+
except ImportError:
|
|
18
|
+
tqdm = None # type: ignore
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import torch
|
|
22
|
+
except ImportError:
|
|
23
|
+
torch = None # type: ignore
|
|
24
|
+
|
|
25
|
+
# Type hint for device - use Optional[torch.device] if torch is available
|
|
26
|
+
if torch is not None:
|
|
27
|
+
DeviceType = Optional[torch.device]
|
|
28
|
+
else:
|
|
29
|
+
DeviceType = Optional[object] # Fallback when torch is not available
|
|
30
|
+
|
|
31
|
+
from ..geometry import RBox
|
|
32
|
+
from ..ops import iou
|
|
33
|
+
from ..ops.utils import resolve_exact_polygon_iou_backend
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class Detection:
|
|
38
|
+
"""Single detection result."""
|
|
39
|
+
|
|
40
|
+
rbox: RBox
|
|
41
|
+
score: float
|
|
42
|
+
class_id: int
|
|
43
|
+
class_name: str
|
|
44
|
+
image_id: Optional[str] = None # Required for correct mAP: match only to GTs from same image
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class GroundTruth:
|
|
49
|
+
"""Single ground truth annotation."""
|
|
50
|
+
|
|
51
|
+
rbox: RBox
|
|
52
|
+
class_id: int
|
|
53
|
+
class_name: str
|
|
54
|
+
difficult: int = 0
|
|
55
|
+
image_id: Optional[str] = None # Required for correct mAP: match only to detections from same image
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class ClassEvalMetrics:
|
|
60
|
+
"""Per-class detection stats (MMRotate-style eval table)."""
|
|
61
|
+
|
|
62
|
+
num_gts: int
|
|
63
|
+
num_dets: int
|
|
64
|
+
recall: float
|
|
65
|
+
ap: float
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class APCalculator:
|
|
69
|
+
"""Calculate Average Precision (AP) for oriented object detection."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
iou_threshold: float = 0.5,
|
|
74
|
+
class_names: Optional[Sequence[str]] = None,
|
|
75
|
+
device: DeviceType = None,
|
|
76
|
+
*,
|
|
77
|
+
use_exact_rotated_iou: bool = True,
|
|
78
|
+
):
|
|
79
|
+
if not 0 < iou_threshold <= 1:
|
|
80
|
+
raise ValueError("IoU threshold must be in (0, 1]")
|
|
81
|
+
self.iou_threshold = iou_threshold
|
|
82
|
+
self.class_names = class_names or []
|
|
83
|
+
self.class_to_id = {name: i for i, name in enumerate(self.class_names)} if self.class_names else {}
|
|
84
|
+
self.use_exact_rotated_iou = bool(use_exact_rotated_iou)
|
|
85
|
+
self.device = None if self.use_exact_rotated_iou else device
|
|
86
|
+
self._exact_iou_backend: Optional[str] = (
|
|
87
|
+
resolve_exact_polygon_iou_backend() if self.use_exact_rotated_iou else None
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def compute_ap(
|
|
91
|
+
self,
|
|
92
|
+
detections: Sequence[Detection],
|
|
93
|
+
ground_truths: Sequence[GroundTruth],
|
|
94
|
+
*,
|
|
95
|
+
class_name: Optional[str] = None,
|
|
96
|
+
show_progress: bool = False,
|
|
97
|
+
progress_stream: Optional[Any] = None,
|
|
98
|
+
max_iou_calculations: Optional[int] = None,
|
|
99
|
+
min_box_size: float = 1.0,
|
|
100
|
+
) -> Tuple[float, ClassEvalMetrics]:
|
|
101
|
+
"""Compute Average Precision for a single class.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
detections: List of detections
|
|
105
|
+
ground_truths: List of ground truths
|
|
106
|
+
class_name: Optional class name
|
|
107
|
+
show_progress: Whether to show progress bar
|
|
108
|
+
max_iou_calculations: Maximum number of IoU calculations to allow.
|
|
109
|
+
If exceeded, will use approximate method or return 0.0.
|
|
110
|
+
min_box_size: Minimum box size (width/height) in pixels to consider valid.
|
|
111
|
+
Boxes smaller than this will be filtered out (default: 1.0).
|
|
112
|
+
"""
|
|
113
|
+
if class_name:
|
|
114
|
+
detections = [d for d in detections if d.class_name == class_name]
|
|
115
|
+
ground_truths = [gt for gt in ground_truths if gt.class_name == class_name]
|
|
116
|
+
|
|
117
|
+
# Filter out degenerated boxes
|
|
118
|
+
original_det_count = len(detections)
|
|
119
|
+
original_gt_count = len(ground_truths)
|
|
120
|
+
detections = [d for d in detections if d.rbox.is_valid(min_size=min_box_size)]
|
|
121
|
+
ground_truths = [gt for gt in ground_truths if gt.rbox.is_valid(min_size=min_box_size)]
|
|
122
|
+
|
|
123
|
+
if show_progress:
|
|
124
|
+
filtered_dets = original_det_count - len(detections)
|
|
125
|
+
filtered_gts = original_gt_count - len(ground_truths)
|
|
126
|
+
if filtered_dets > 0 or filtered_gts > 0:
|
|
127
|
+
filter_msg = f" Filtered {filtered_dets} degenerated detections, {filtered_gts} degenerated ground truths"
|
|
128
|
+
print(filter_msg)
|
|
129
|
+
|
|
130
|
+
if not ground_truths:
|
|
131
|
+
ap = 0.0 if detections else 1.0
|
|
132
|
+
return ap, ClassEvalMetrics(
|
|
133
|
+
num_gts=0,
|
|
134
|
+
num_dets=len(detections),
|
|
135
|
+
recall=0.0,
|
|
136
|
+
ap=ap,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Check if we should optimize due to too many calculations
|
|
140
|
+
num_dets = len(detections)
|
|
141
|
+
num_gts = len(ground_truths)
|
|
142
|
+
total_calculations = num_dets * num_gts
|
|
143
|
+
|
|
144
|
+
# Use batch IoU if available and calculations are too many
|
|
145
|
+
use_batch_iou = max_iou_calculations and total_calculations > max_iou_calculations
|
|
146
|
+
|
|
147
|
+
# Exact polygon mAP (final_nms_use_cpu): never use GPU sampling IoU
|
|
148
|
+
from ..utils import is_gpu_device
|
|
149
|
+
use_gpu = (
|
|
150
|
+
not self.use_exact_rotated_iou
|
|
151
|
+
and torch is not None
|
|
152
|
+
and self.device is not None
|
|
153
|
+
and is_gpu_device(self.device)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
sorted_dets = sorted(detections, key=lambda d: d.score, reverse=True)
|
|
157
|
+
tp_fp_from_batch = False
|
|
158
|
+
tp: List[int] = []
|
|
159
|
+
fp: List[int] = []
|
|
160
|
+
|
|
161
|
+
if use_batch_iou:
|
|
162
|
+
# Try using batch IoU computation which is much faster
|
|
163
|
+
try:
|
|
164
|
+
det_rboxes = [d.rbox for d in sorted_dets]
|
|
165
|
+
gt_rboxes = [gt.rbox for gt in ground_truths]
|
|
166
|
+
|
|
167
|
+
# Use chunking for very large matrices to avoid memory issues
|
|
168
|
+
# Process detections in chunks to limit memory usage.
|
|
169
|
+
# GPU IoU builds [chunk*S, num_gt] tensors with S ~= default oriented IoU samples
|
|
170
|
+
# (typically 100; tier/env can raise it). When num_gt is large (e.g. 72k)
|
|
171
|
+
# a fixed 5000-detect chunk can OOM. Cap chunk so chunk*S*num_gt <= ~500M elements.
|
|
172
|
+
num_gt = len(ground_truths)
|
|
173
|
+
max_elements = 500_000_000 # ~500M elements for bool tensor (~500 MB)
|
|
174
|
+
samples_per_box = 100 # conservative match to oriented_box_iou_gpu default envelope
|
|
175
|
+
adaptive_chunk = max(1, max_elements // (samples_per_box * max(1, num_gt)))
|
|
176
|
+
CHUNK_SIZE = min(5000, adaptive_chunk)
|
|
177
|
+
num_dets = len(sorted_dets)
|
|
178
|
+
|
|
179
|
+
if show_progress and tqdm is not None:
|
|
180
|
+
if self.use_exact_rotated_iou:
|
|
181
|
+
iou_type = f"CPU exact ({self._exact_iou_backend})"
|
|
182
|
+
else:
|
|
183
|
+
iou_type = "GPU-accelerated" if use_gpu else "CPU"
|
|
184
|
+
print(f" Using chunked batch IoU computation ({iou_type}) for {class_name} ({total_calculations:,} calculations)")
|
|
185
|
+
if num_dets > CHUNK_SIZE:
|
|
186
|
+
print(f" Processing {num_dets:,} detections in chunks of {CHUNK_SIZE:,}")
|
|
187
|
+
|
|
188
|
+
# Process detections in chunks
|
|
189
|
+
gt_matched = [False] * len(ground_truths)
|
|
190
|
+
tp = []
|
|
191
|
+
fp = []
|
|
192
|
+
|
|
193
|
+
# Process detections in chunks; tqdm to progress_stream so console sees it but train.log does not
|
|
194
|
+
tqdm_file = progress_stream if progress_stream is not None else sys.stderr
|
|
195
|
+
det_iter = (
|
|
196
|
+
tqdm(range(0, num_dets, CHUNK_SIZE), desc=" Processing chunks", unit="chunk", leave=False, file=tqdm_file)
|
|
197
|
+
if (show_progress and tqdm is not None and num_dets > CHUNK_SIZE)
|
|
198
|
+
else range(0, num_dets, CHUNK_SIZE)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
for chunk_start in det_iter:
|
|
202
|
+
chunk_end = min(chunk_start + CHUNK_SIZE, num_dets)
|
|
203
|
+
chunk_dets = sorted_dets[chunk_start:chunk_end]
|
|
204
|
+
chunk_det_rboxes = det_rboxes[chunk_start:chunk_end]
|
|
205
|
+
|
|
206
|
+
# Compute IoU matrix for this chunk
|
|
207
|
+
chunk_iou_matrix = None
|
|
208
|
+
try:
|
|
209
|
+
if use_gpu:
|
|
210
|
+
# Use GPU-accelerated IoU (much faster)
|
|
211
|
+
from ..ops.gpu_ops import oriented_box_iou_gpu
|
|
212
|
+
from ..ops.utils import rboxes_to_tensor
|
|
213
|
+
|
|
214
|
+
# Convert RBox objects to tensors
|
|
215
|
+
chunk_det_tensors = rboxes_to_tensor(chunk_det_rboxes, device=self.device)
|
|
216
|
+
gt_tensors = rboxes_to_tensor(gt_rboxes, device=self.device)
|
|
217
|
+
|
|
218
|
+
# Compute IoU matrix on GPU
|
|
219
|
+
iou_matrix_gpu = oriented_box_iou_gpu(chunk_det_tensors, gt_tensors)
|
|
220
|
+
|
|
221
|
+
# Convert to CPU list of lists for compatibility
|
|
222
|
+
chunk_iou_matrix = iou_matrix_gpu.cpu().tolist()
|
|
223
|
+
else:
|
|
224
|
+
from ..ops.iou import batch_rbox_iou
|
|
225
|
+
chunk_iou_matrix = batch_rbox_iou(
|
|
226
|
+
chunk_det_rboxes,
|
|
227
|
+
gt_rboxes,
|
|
228
|
+
device=self.device,
|
|
229
|
+
intersection_backend=(
|
|
230
|
+
self._exact_iou_backend
|
|
231
|
+
if self.use_exact_rotated_iou
|
|
232
|
+
else "auto"
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
except Exception as chunk_e:
|
|
236
|
+
# If chunk batch fails, fall back to per-detection computation for this chunk
|
|
237
|
+
if show_progress:
|
|
238
|
+
print(f" Chunk batch IoU failed, using per-detection for chunk {chunk_start}-{chunk_end}: {chunk_e}")
|
|
239
|
+
chunk_iou_matrix = None
|
|
240
|
+
|
|
241
|
+
# Process each detection in this chunk
|
|
242
|
+
for local_idx, det in enumerate(chunk_dets):
|
|
243
|
+
det_idx = chunk_start + local_idx
|
|
244
|
+
best_iou = 0.0
|
|
245
|
+
best_gt_idx = -1
|
|
246
|
+
|
|
247
|
+
if chunk_iou_matrix is not None:
|
|
248
|
+
# Use pre-computed IoU matrix
|
|
249
|
+
for gt_idx, gt in enumerate(ground_truths):
|
|
250
|
+
if gt_matched[gt_idx] or gt.class_name != det.class_name:
|
|
251
|
+
continue
|
|
252
|
+
if det.image_id is not None and gt.image_id is not None and det.image_id != gt.image_id:
|
|
253
|
+
continue
|
|
254
|
+
det_iou = chunk_iou_matrix[local_idx][gt_idx]
|
|
255
|
+
if det_iou > best_iou:
|
|
256
|
+
best_iou = det_iou
|
|
257
|
+
best_gt_idx = gt_idx
|
|
258
|
+
else:
|
|
259
|
+
# Fall back to per-detection IoU computation
|
|
260
|
+
for gt_idx, gt in enumerate(ground_truths):
|
|
261
|
+
if gt_matched[gt_idx] or gt.class_name != det.class_name:
|
|
262
|
+
continue
|
|
263
|
+
if det.image_id is not None and gt.image_id is not None and det.image_id != gt.image_id:
|
|
264
|
+
continue
|
|
265
|
+
try:
|
|
266
|
+
from ..ops.iou import rbox_iou
|
|
267
|
+
det_iou = rbox_iou(
|
|
268
|
+
det.rbox,
|
|
269
|
+
gt.rbox,
|
|
270
|
+
intersection_backend=(
|
|
271
|
+
self._exact_iou_backend
|
|
272
|
+
if self.use_exact_rotated_iou
|
|
273
|
+
else "auto"
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
if det_iou > best_iou:
|
|
277
|
+
best_iou = det_iou
|
|
278
|
+
best_gt_idx = gt_idx
|
|
279
|
+
except Exception:
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
# Check if match is valid
|
|
283
|
+
if best_iou >= self.iou_threshold and best_gt_idx >= 0:
|
|
284
|
+
gt = ground_truths[best_gt_idx]
|
|
285
|
+
if gt.difficult == 0:
|
|
286
|
+
gt_matched[best_gt_idx] = True
|
|
287
|
+
tp.append(1)
|
|
288
|
+
fp.append(0)
|
|
289
|
+
else:
|
|
290
|
+
tp.append(0)
|
|
291
|
+
fp.append(0)
|
|
292
|
+
else:
|
|
293
|
+
tp.append(0)
|
|
294
|
+
fp.append(1)
|
|
295
|
+
|
|
296
|
+
# Clear chunk matrix from memory
|
|
297
|
+
del chunk_iou_matrix
|
|
298
|
+
if torch is not None and torch.cuda.is_available():
|
|
299
|
+
torch.cuda.empty_cache()
|
|
300
|
+
elif torch is not None and getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
|
|
301
|
+
torch.mps.empty_cache()
|
|
302
|
+
|
|
303
|
+
tp_fp_from_batch = True
|
|
304
|
+
|
|
305
|
+
except Exception as e:
|
|
306
|
+
# Fall back to regular computation if batch fails
|
|
307
|
+
if show_progress:
|
|
308
|
+
print(f" Batch IoU failed for {class_name}, using regular method (will be slow): {e}")
|
|
309
|
+
use_batch_iou = False
|
|
310
|
+
|
|
311
|
+
if not tp_fp_from_batch:
|
|
312
|
+
# Track which ground truths have been matched (standard per-detection path)
|
|
313
|
+
gt_matched = [False] * len(ground_truths)
|
|
314
|
+
tp = []
|
|
315
|
+
fp = []
|
|
316
|
+
|
|
317
|
+
# Use progress bar if requested and many detections; write to progress_stream so console sees it but train.log does not
|
|
318
|
+
tqdm_file = progress_stream if progress_stream is not None else sys.stderr
|
|
319
|
+
use_pbar = show_progress and tqdm is not None and len(sorted_dets) > 5000
|
|
320
|
+
det_iter = tqdm(sorted_dets, desc=f" {class_name or 'All'}", unit="det", leave=False, ncols=100, file=tqdm_file) if use_pbar else sorted_dets
|
|
321
|
+
|
|
322
|
+
for det_idx, det in enumerate(det_iter):
|
|
323
|
+
best_iou = 0.0
|
|
324
|
+
best_gt_idx = -1
|
|
325
|
+
|
|
326
|
+
# Find best matching ground truth (only from same image when image_id is set)
|
|
327
|
+
for gt_idx, gt in enumerate(ground_truths):
|
|
328
|
+
if gt_matched[gt_idx] or gt.class_name != det.class_name:
|
|
329
|
+
continue
|
|
330
|
+
if det.image_id is not None and gt.image_id is not None and det.image_id != gt.image_id:
|
|
331
|
+
continue
|
|
332
|
+
try:
|
|
333
|
+
det_iou = iou.rbox_iou(
|
|
334
|
+
det.rbox,
|
|
335
|
+
gt.rbox,
|
|
336
|
+
intersection_backend=(
|
|
337
|
+
self._exact_iou_backend
|
|
338
|
+
if self.use_exact_rotated_iou
|
|
339
|
+
else "auto"
|
|
340
|
+
),
|
|
341
|
+
)
|
|
342
|
+
if det_iou > best_iou:
|
|
343
|
+
best_iou = det_iou
|
|
344
|
+
best_gt_idx = gt_idx
|
|
345
|
+
except Exception:
|
|
346
|
+
# Skip problematic IoU calculations and continue with next GT
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
# Check if match is valid
|
|
350
|
+
if best_iou >= self.iou_threshold and best_gt_idx >= 0:
|
|
351
|
+
gt = ground_truths[best_gt_idx]
|
|
352
|
+
if gt.difficult == 0: # Only count non-difficult as TP
|
|
353
|
+
gt_matched[best_gt_idx] = True
|
|
354
|
+
tp.append(1)
|
|
355
|
+
fp.append(0)
|
|
356
|
+
else:
|
|
357
|
+
tp.append(0)
|
|
358
|
+
fp.append(0) # Difficult GTs don't count as FP either
|
|
359
|
+
else:
|
|
360
|
+
tp.append(0)
|
|
361
|
+
fp.append(1)
|
|
362
|
+
|
|
363
|
+
# Compute precision and recall
|
|
364
|
+
tp_cumsum = []
|
|
365
|
+
fp_cumsum = []
|
|
366
|
+
cumsum_tp = 0
|
|
367
|
+
cumsum_fp = 0
|
|
368
|
+
|
|
369
|
+
for t, f in zip(tp, fp):
|
|
370
|
+
cumsum_tp += t
|
|
371
|
+
cumsum_fp += f
|
|
372
|
+
tp_cumsum.append(cumsum_tp)
|
|
373
|
+
fp_cumsum.append(cumsum_fp)
|
|
374
|
+
|
|
375
|
+
num_positives = sum(1 for gt in ground_truths if gt.difficult == 0)
|
|
376
|
+
num_dets = len(sorted_dets)
|
|
377
|
+
if num_positives == 0:
|
|
378
|
+
return 0.0, ClassEvalMetrics(
|
|
379
|
+
num_gts=0,
|
|
380
|
+
num_dets=num_dets,
|
|
381
|
+
recall=0.0,
|
|
382
|
+
ap=0.0,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
recalls = [t / num_positives for t in tp_cumsum]
|
|
386
|
+
precisions = [
|
|
387
|
+
tp_cumsum[i] / (tp_cumsum[i] + fp_cumsum[i]) if (tp_cumsum[i] + fp_cumsum[i]) > 0 else 0.0
|
|
388
|
+
for i in range(len(tp_cumsum))
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
final_recall = recalls[-1] if recalls else 0.0
|
|
392
|
+
|
|
393
|
+
# Compute AP using 11-point interpolation
|
|
394
|
+
ap = 0.0
|
|
395
|
+
for t in [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]:
|
|
396
|
+
precisions_at_recall = [p for r, p in zip(recalls, precisions) if r >= t]
|
|
397
|
+
if precisions_at_recall:
|
|
398
|
+
ap += max(precisions_at_recall) / 11.0
|
|
399
|
+
|
|
400
|
+
return ap, ClassEvalMetrics(
|
|
401
|
+
num_gts=num_positives,
|
|
402
|
+
num_dets=num_dets,
|
|
403
|
+
recall=final_recall,
|
|
404
|
+
ap=ap,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def compute_map(
|
|
408
|
+
self,
|
|
409
|
+
all_detections: Dict[str, List[Detection]], # image_id -> detections
|
|
410
|
+
all_ground_truths: Dict[str, List[GroundTruth]], # image_id -> ground_truths
|
|
411
|
+
show_progress: bool = True,
|
|
412
|
+
progress_stream: Optional[Any] = None,
|
|
413
|
+
max_iou_calculations_per_class: int = 10_000_000, # 10M IoU calculations max per class
|
|
414
|
+
min_box_size: float = 1.0,
|
|
415
|
+
) -> Tuple[Dict[str, float], Dict[str, ClassEvalMetrics]]:
|
|
416
|
+
"""Compute mAP across all classes.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
all_detections: Dictionary mapping image_id -> list of Detection objects
|
|
420
|
+
all_ground_truths: Dictionary mapping image_id -> list of GroundTruth objects
|
|
421
|
+
show_progress: Whether to show progress bars
|
|
422
|
+
max_iou_calculations_per_class: Maximum IoU calculations per class before optimization
|
|
423
|
+
min_box_size: Minimum box size (width/height) in pixels to consider valid.
|
|
424
|
+
Boxes smaller than this will be filtered out (default: 1.0).
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
``(class_aps, class_metrics)`` — AP and MMRotate-style stats per class.
|
|
428
|
+
"""
|
|
429
|
+
# Collect all unique class names
|
|
430
|
+
all_classes = set()
|
|
431
|
+
for dets in all_detections.values():
|
|
432
|
+
all_classes.update(d.class_name for d in dets)
|
|
433
|
+
for gts in all_ground_truths.values():
|
|
434
|
+
all_classes.update(gt.class_name for gt in gts)
|
|
435
|
+
|
|
436
|
+
if self.class_names:
|
|
437
|
+
all_classes = all_classes.intersection(set(self.class_names))
|
|
438
|
+
|
|
439
|
+
# Aggregate detections and ground truths by class
|
|
440
|
+
class_aps: Dict[str, float] = {}
|
|
441
|
+
class_metrics: Dict[str, ClassEvalMetrics] = {}
|
|
442
|
+
sorted_classes = sorted(all_classes)
|
|
443
|
+
num_classes = len(sorted_classes)
|
|
444
|
+
|
|
445
|
+
# Use progress bar for classes; write to progress_stream so console sees it but train.log does not
|
|
446
|
+
tqdm_file = progress_stream if progress_stream is not None else sys.stderr
|
|
447
|
+
use_pbar = show_progress and tqdm is not None and num_classes > 1
|
|
448
|
+
class_iter = tqdm(sorted_classes, desc="Computing AP per class", unit="class", ncols=120, file=tqdm_file) if use_pbar else sorted_classes
|
|
449
|
+
|
|
450
|
+
for class_name in class_iter:
|
|
451
|
+
try:
|
|
452
|
+
class_detections = []
|
|
453
|
+
class_ground_truths = []
|
|
454
|
+
|
|
455
|
+
for image_id in set(all_detections.keys()) | set(all_ground_truths.keys()):
|
|
456
|
+
dets = all_detections.get(image_id, [])
|
|
457
|
+
gts = all_ground_truths.get(image_id, [])
|
|
458
|
+
|
|
459
|
+
class_detections.extend([d for d in dets if d.class_name == class_name])
|
|
460
|
+
class_ground_truths.extend([gt for gt in gts if gt.class_name == class_name])
|
|
461
|
+
|
|
462
|
+
# Log class statistics
|
|
463
|
+
num_dets = len(class_detections)
|
|
464
|
+
num_gts = len(class_ground_truths)
|
|
465
|
+
|
|
466
|
+
# Compute AP for this class (show progress if many detections or ground truths)
|
|
467
|
+
# Show progress if computation will be slow (many IoU calculations)
|
|
468
|
+
total_iou_calculations = num_dets * num_gts
|
|
469
|
+
show_det_progress = total_iou_calculations > 100000 or num_dets > 5000 or num_gts > 1000
|
|
470
|
+
|
|
471
|
+
# Warn if too many calculations
|
|
472
|
+
if total_iou_calculations > max_iou_calculations_per_class:
|
|
473
|
+
warning_msg = (f" WARNING: {class_name} has {total_iou_calculations:,} IoU calculations "
|
|
474
|
+
f"({num_dets:,} dets × {num_gts:,} GTs). This will be slow!")
|
|
475
|
+
if use_pbar and isinstance(class_iter, tqdm):
|
|
476
|
+
class_iter.write(warning_msg)
|
|
477
|
+
else:
|
|
478
|
+
print(warning_msg)
|
|
479
|
+
|
|
480
|
+
# Update progress bar with class info
|
|
481
|
+
if use_pbar and isinstance(class_iter, tqdm):
|
|
482
|
+
if show_det_progress:
|
|
483
|
+
class_iter.set_postfix_str(f"{class_name}: {num_dets} dets × {num_gts} GTs = {total_iou_calculations:,} IoUs")
|
|
484
|
+
class_iter.write(f" Computing AP for {class_name}: {num_dets:,} dets, {num_gts:,} GTs ({total_iou_calculations:,} IoU calculations)")
|
|
485
|
+
else:
|
|
486
|
+
class_iter.set_postfix_str(f"{class_name}: {num_dets} dets, {num_gts} GTs")
|
|
487
|
+
|
|
488
|
+
ap, metrics = self.compute_ap(
|
|
489
|
+
class_detections,
|
|
490
|
+
class_ground_truths,
|
|
491
|
+
class_name=class_name,
|
|
492
|
+
show_progress=show_det_progress,
|
|
493
|
+
progress_stream=progress_stream,
|
|
494
|
+
max_iou_calculations=max_iou_calculations_per_class,
|
|
495
|
+
min_box_size=min_box_size,
|
|
496
|
+
)
|
|
497
|
+
class_aps[class_name] = ap
|
|
498
|
+
class_metrics[class_name] = metrics
|
|
499
|
+
|
|
500
|
+
# Update progress bar with current AP if using tqdm
|
|
501
|
+
if use_pbar and isinstance(class_iter, tqdm):
|
|
502
|
+
class_iter.set_postfix_str(f"{class_name}: AP={ap:.4f}")
|
|
503
|
+
|
|
504
|
+
except Exception as e:
|
|
505
|
+
# Log error and continue with other classes
|
|
506
|
+
error_msg = f"Error computing AP for class '{class_name}': {e}"
|
|
507
|
+
if use_pbar and isinstance(class_iter, tqdm):
|
|
508
|
+
class_iter.write(f" ERROR: {error_msg}")
|
|
509
|
+
else:
|
|
510
|
+
print(f" ERROR: {error_msg}")
|
|
511
|
+
import traceback
|
|
512
|
+
traceback.print_exc()
|
|
513
|
+
# Set AP to 0.0 for failed classes
|
|
514
|
+
class_aps[class_name] = 0.0
|
|
515
|
+
class_metrics[class_name] = ClassEvalMetrics(
|
|
516
|
+
num_gts=0,
|
|
517
|
+
num_dets=0,
|
|
518
|
+
recall=0.0,
|
|
519
|
+
ap=0.0,
|
|
520
|
+
)
|
|
521
|
+
if use_pbar and isinstance(class_iter, tqdm):
|
|
522
|
+
class_iter.set_postfix_str(f"{class_name}: ERROR")
|
|
523
|
+
|
|
524
|
+
return class_aps, class_metrics
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def compute_oriented_map(
|
|
528
|
+
detections: Dict[str, List[Detection]],
|
|
529
|
+
ground_truths: Dict[str, List[GroundTruth]],
|
|
530
|
+
*,
|
|
531
|
+
iou_threshold: float = 0.5,
|
|
532
|
+
class_names: Optional[Sequence[str]] = None,
|
|
533
|
+
show_progress: bool = True,
|
|
534
|
+
progress_stream: Optional[Any] = None,
|
|
535
|
+
max_iou_calculations_per_class: int = 10_000_000,
|
|
536
|
+
device: DeviceType = None,
|
|
537
|
+
min_box_size: float = 1.0,
|
|
538
|
+
use_exact_rotated_iou: bool = True,
|
|
539
|
+
) -> Tuple[float, Dict[str, float], Dict[str, ClassEvalMetrics]]:
|
|
540
|
+
"""Compute mean Average Precision (mAP) for oriented detection.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
detections: Dictionary mapping image_id -> list of Detection objects
|
|
544
|
+
ground_truths: Dictionary mapping image_id -> list of GroundTruth objects
|
|
545
|
+
iou_threshold: IoU threshold for positive matches
|
|
546
|
+
class_names: Optional list of class names to evaluate
|
|
547
|
+
show_progress: Whether to show progress bars during computation
|
|
548
|
+
progress_stream: If set (e.g. original stderr when stdout is teed to a log file),
|
|
549
|
+
progress bars write here so they appear on console but not in the log file.
|
|
550
|
+
max_iou_calculations_per_class: Maximum IoU calculations per class before using optimization
|
|
551
|
+
device: Optional torch device for GPU acceleration. If None, uses CPU.
|
|
552
|
+
Ignored when ``use_exact_rotated_iou`` is True (Shapely/python polygon IoU only).
|
|
553
|
+
min_box_size: Minimum box size (width/height) in pixels to consider valid.
|
|
554
|
+
Boxes smaller than this will be filtered out (default: 1.0).
|
|
555
|
+
use_exact_rotated_iou: When True, mAP matching uses exact polygon IoU on CPU
|
|
556
|
+
(Shapely when installed); never ``oriented_box_iou_gpu``. When False, uses GPU
|
|
557
|
+
sampling IoU (controlled by ``evaluation.use_exact_rotated_iou`` in training config).
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
Tuple of (mAP, class_ap_dict, class_metrics) where mAP is the mean of all
|
|
561
|
+
class APs and ``class_metrics`` holds per-class gts/dets/recall (MMRotate-style).
|
|
562
|
+
"""
|
|
563
|
+
if use_exact_rotated_iou and show_progress:
|
|
564
|
+
backend = resolve_exact_polygon_iou_backend()
|
|
565
|
+
print(
|
|
566
|
+
f"mAP: exact rotated IoU on CPU (backend={backend!r}; "
|
|
567
|
+
"GPU sampling IoU disabled)",
|
|
568
|
+
flush=True,
|
|
569
|
+
)
|
|
570
|
+
calculator = APCalculator(
|
|
571
|
+
iou_threshold=iou_threshold,
|
|
572
|
+
class_names=class_names,
|
|
573
|
+
device=device,
|
|
574
|
+
use_exact_rotated_iou=use_exact_rotated_iou,
|
|
575
|
+
)
|
|
576
|
+
class_aps, class_metrics = calculator.compute_map(
|
|
577
|
+
detections,
|
|
578
|
+
ground_truths,
|
|
579
|
+
show_progress=show_progress,
|
|
580
|
+
progress_stream=progress_stream,
|
|
581
|
+
max_iou_calculations_per_class=max_iou_calculations_per_class,
|
|
582
|
+
min_box_size=min_box_size,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
if not class_aps:
|
|
586
|
+
return 0.0, {}, {}
|
|
587
|
+
|
|
588
|
+
mean_ap = sum(class_aps.values()) / len(class_aps)
|
|
589
|
+
return mean_ap, class_aps, class_metrics
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def format_mmrotate_class_metrics_table(
|
|
593
|
+
class_metrics: Dict[str, ClassEvalMetrics],
|
|
594
|
+
class_names: Optional[Sequence[str]] = None,
|
|
595
|
+
*,
|
|
596
|
+
mean_ap: Optional[float] = None,
|
|
597
|
+
) -> str:
|
|
598
|
+
"""Format per-class gts / dets / recall / ap table like MMRotate eval output."""
|
|
599
|
+
if not class_metrics:
|
|
600
|
+
return ""
|
|
601
|
+
|
|
602
|
+
if class_names:
|
|
603
|
+
ordered = [c for c in class_names if c in class_metrics]
|
|
604
|
+
ordered.extend(c for c in sorted(class_metrics) if c not in ordered)
|
|
605
|
+
else:
|
|
606
|
+
ordered = sorted(class_metrics.keys())
|
|
607
|
+
|
|
608
|
+
rows: List[Tuple[str, ClassEvalMetrics]] = [
|
|
609
|
+
(name, class_metrics[name]) for name in ordered
|
|
610
|
+
]
|
|
611
|
+
class_w = max(len("class"), *(len(name) for name, _ in rows))
|
|
612
|
+
col_gts = max(len("gts"), 3)
|
|
613
|
+
col_dets = max(len("dets"), 4)
|
|
614
|
+
col_recall = max(len("recall"), 6)
|
|
615
|
+
col_ap = max(len("ap"), 2)
|
|
616
|
+
|
|
617
|
+
def _sep() -> str:
|
|
618
|
+
return (
|
|
619
|
+
f"|{'-' * (class_w + 2)}|{'-' * (col_gts + 2)}:|"
|
|
620
|
+
f"{'-' * (col_dets + 2)}:|{'-' * (col_recall + 2)}:|{'-' * (col_ap + 2)}:|"
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
lines = [
|
|
624
|
+
f"| {'class'.ljust(class_w)} | {'gts'.rjust(col_gts)} | "
|
|
625
|
+
f"{'dets'.rjust(col_dets)} | {'recall'.rjust(col_recall)} | {'ap'.rjust(col_ap)} |",
|
|
626
|
+
_sep(),
|
|
627
|
+
]
|
|
628
|
+
for name, m in rows:
|
|
629
|
+
lines.append(
|
|
630
|
+
f"| {name.ljust(class_w)} | {m.num_gts:>{col_gts}d} | {m.num_dets:>{col_dets}d} | "
|
|
631
|
+
f"{m.recall:>{col_recall}.3f} | {m.ap:>{col_ap}.3f} |"
|
|
632
|
+
)
|
|
633
|
+
if mean_ap is not None:
|
|
634
|
+
lines.append(
|
|
635
|
+
f"| {'mAP'.ljust(class_w)} | {'':>{col_gts}} | {'':>{col_dets}} | "
|
|
636
|
+
f"{'':>{col_recall}} | {mean_ap:>{col_ap}.3f} |"
|
|
637
|
+
)
|
|
638
|
+
return "\n".join(lines)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
__all__ = [
|
|
642
|
+
"Detection",
|
|
643
|
+
"GroundTruth",
|
|
644
|
+
"ClassEvalMetrics",
|
|
645
|
+
"APCalculator",
|
|
646
|
+
"compute_oriented_map",
|
|
647
|
+
"format_mmrotate_class_metrics_table",
|
|
648
|
+
]
|