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,483 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Compare sampled GPU rIoU vs exact Shapely polygon IoU.
|
|
3
|
+
|
|
4
|
+
Generates stratified oriented box pairs (squares, elongated ships, thin slivers),
|
|
5
|
+
computes exact IoU on CPU (Shapely) and sampling-based IoU via
|
|
6
|
+
``oriented_box_iou_gpu``, then reports absolute / relative error statistics.
|
|
7
|
+
|
|
8
|
+
Use this to validate geometry sampling defaults in ``oriented_det/ops/gpu_ops.py``:
|
|
9
|
+
|
|
10
|
+
target_spacing_px=2.0, min_samples=25, max_samples=1024, min_points_short=3
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
python tools/measure_sampled_riou_error.py
|
|
14
|
+
python tools/measure_sampled_riou_error.py --pairs 5000 --seed 0
|
|
15
|
+
python tools/measure_sampled_riou_error.py --sweep-spacing 2 3 4 5 6 8
|
|
16
|
+
python tools/measure_sampled_riou_error.py --target-spacing 4 --max-samples 1024 --csv out.csv
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import csv
|
|
23
|
+
import math
|
|
24
|
+
import sys
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Iterator, List, Optional, Sequence, Tuple
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
import torch
|
|
31
|
+
|
|
32
|
+
from oriented_det.geometry import RBox
|
|
33
|
+
from oriented_det.ops.gpu_ops import (
|
|
34
|
+
_DEFAULT_MAX_SAMPLES,
|
|
35
|
+
_DEFAULT_MIN_SAMPLES,
|
|
36
|
+
_DEFAULT_TARGET_SPACING_PX,
|
|
37
|
+
_MIN_POINTS_ALONG_SHORT_SIDE,
|
|
38
|
+
geometry_sample_count_for_boxes,
|
|
39
|
+
oriented_box_iou_gpu,
|
|
40
|
+
)
|
|
41
|
+
from oriented_det.ops.iou import rbox_iou
|
|
42
|
+
from oriented_det.ops.utils import SHAPELY_AVAILABLE
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class GeometryParams:
|
|
47
|
+
target_spacing_px: float = _DEFAULT_TARGET_SPACING_PX
|
|
48
|
+
min_samples: int = _DEFAULT_MIN_SAMPLES
|
|
49
|
+
max_samples: int = _DEFAULT_MAX_SAMPLES
|
|
50
|
+
min_points_short: int = _MIN_POINTS_ALONG_SHORT_SIDE
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class PairRecord:
|
|
55
|
+
category: str
|
|
56
|
+
w1: float
|
|
57
|
+
h1: float
|
|
58
|
+
w2: float
|
|
59
|
+
h2: float
|
|
60
|
+
exact: float
|
|
61
|
+
sampled: float
|
|
62
|
+
num_samples: int
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def abs_error(self) -> float:
|
|
66
|
+
return abs(self.sampled - self.exact)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def signed_error(self) -> float:
|
|
70
|
+
return self.sampled - self.exact
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def rel_error_pct(self) -> float:
|
|
74
|
+
if self.exact <= 1e-8:
|
|
75
|
+
return float("nan")
|
|
76
|
+
return 100.0 * self.abs_error / self.exact
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _require_shapely() -> None:
|
|
80
|
+
if not SHAPELY_AVAILABLE:
|
|
81
|
+
print(
|
|
82
|
+
"Shapely is required for exact IoU. Install oriented-det with shapely.",
|
|
83
|
+
file=sys.stderr,
|
|
84
|
+
)
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _box_tensor(cx: float, cy: float, w: float, h: float, angle: float) -> torch.Tensor:
|
|
89
|
+
return torch.tensor([cx, cy, w, h, angle], dtype=torch.float32)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _exact_iou(a: torch.Tensor, b: torch.Tensor) -> float:
|
|
93
|
+
ra = RBox(*a.tolist())
|
|
94
|
+
rb = RBox(*b.tolist())
|
|
95
|
+
return float(rbox_iou(ra, rb, intersection_backend="shapely"))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _sampled_iou_batch(
|
|
99
|
+
boxes1: torch.Tensor,
|
|
100
|
+
boxes2: torch.Tensor,
|
|
101
|
+
*,
|
|
102
|
+
device: torch.device,
|
|
103
|
+
num_samples: int,
|
|
104
|
+
) -> torch.Tensor:
|
|
105
|
+
b1 = boxes1.to(device)
|
|
106
|
+
b2 = boxes2.to(device)
|
|
107
|
+
mat = oriented_box_iou_gpu(b1, b2, num_samples=num_samples)
|
|
108
|
+
return torch.diagonal(mat).cpu()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _resolve_num_samples(
|
|
112
|
+
boxes1: torch.Tensor,
|
|
113
|
+
boxes2: torch.Tensor,
|
|
114
|
+
params: GeometryParams,
|
|
115
|
+
) -> int:
|
|
116
|
+
combined = torch.cat([boxes1, boxes2], dim=0)
|
|
117
|
+
return geometry_sample_count_for_boxes(
|
|
118
|
+
combined,
|
|
119
|
+
target_spacing_px=params.target_spacing_px,
|
|
120
|
+
min_samples=params.min_samples,
|
|
121
|
+
max_samples=params.max_samples,
|
|
122
|
+
min_points_short=params.min_points_short,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _random_box(rng: np.random.Generator, spec: str) -> torch.Tensor:
|
|
127
|
+
if spec == "tiny_square":
|
|
128
|
+
side = rng.uniform(4.0, 16.0)
|
|
129
|
+
w = h = side
|
|
130
|
+
elif spec == "small_square":
|
|
131
|
+
side = rng.uniform(16.0, 32.0)
|
|
132
|
+
w = h = side
|
|
133
|
+
elif spec == "vehicle":
|
|
134
|
+
# Cars / trucks in DOTA tiles (~10–25 px short side)
|
|
135
|
+
side = rng.uniform(10.0, 25.0)
|
|
136
|
+
w = h = side
|
|
137
|
+
elif spec == "medium_square":
|
|
138
|
+
side = rng.uniform(32.0, 128.0)
|
|
139
|
+
w = h = side
|
|
140
|
+
elif spec == "large_square":
|
|
141
|
+
side = rng.uniform(128.0, 512.0)
|
|
142
|
+
w = h = side
|
|
143
|
+
elif spec == "elongated":
|
|
144
|
+
short = rng.uniform(8.0, 40.0)
|
|
145
|
+
aspect = rng.uniform(3.0, 8.0)
|
|
146
|
+
w, h = short, short * aspect
|
|
147
|
+
if rng.random() < 0.5:
|
|
148
|
+
w, h = h, w
|
|
149
|
+
elif spec == "elongated_thin":
|
|
150
|
+
short = rng.uniform(4.0, 12.0)
|
|
151
|
+
aspect = rng.uniform(8.0, 40.0)
|
|
152
|
+
w, h = short, short * aspect
|
|
153
|
+
if rng.random() < 0.5:
|
|
154
|
+
w, h = h, w
|
|
155
|
+
else:
|
|
156
|
+
w = rng.uniform(4.0, 512.0)
|
|
157
|
+
h = rng.uniform(4.0, 512.0)
|
|
158
|
+
|
|
159
|
+
angle = rng.uniform(-math.pi / 2, math.pi / 2)
|
|
160
|
+
cx = rng.uniform(0.0, 1024.0)
|
|
161
|
+
cy = rng.uniform(0.0, 1024.0)
|
|
162
|
+
return _box_tensor(cx, cy, w, h, angle)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _offset_for_target_iou(
|
|
166
|
+
rng: np.random.Generator,
|
|
167
|
+
box_a: torch.Tensor,
|
|
168
|
+
box_b: torch.Tensor,
|
|
169
|
+
) -> torch.Tensor:
|
|
170
|
+
"""Shift box_b center to vary overlap (rough control via fraction of max side)."""
|
|
171
|
+
overlap_frac = rng.uniform(0.0, 1.2)
|
|
172
|
+
max_side = max(float(box_a[2]), float(box_a[3]), float(box_b[2]), float(box_b[3]))
|
|
173
|
+
dist = (1.0 - overlap_frac) * max_side
|
|
174
|
+
angle = rng.uniform(0.0, 2.0 * math.pi)
|
|
175
|
+
cx = float(box_a[0]) + dist * math.cos(angle)
|
|
176
|
+
cy = float(box_a[1]) + dist * math.sin(angle)
|
|
177
|
+
out = box_b.clone()
|
|
178
|
+
out[0] = cx
|
|
179
|
+
out[1] = cy
|
|
180
|
+
return out
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def generate_pairs(
|
|
184
|
+
rng: np.random.Generator,
|
|
185
|
+
*,
|
|
186
|
+
n_per_category: int,
|
|
187
|
+
categories: Sequence[str],
|
|
188
|
+
) -> Iterator[Tuple[str, torch.Tensor, torch.Tensor]]:
|
|
189
|
+
for cat in categories:
|
|
190
|
+
for _ in range(n_per_category):
|
|
191
|
+
a = _random_box(rng, cat)
|
|
192
|
+
b = _random_box(rng, cat)
|
|
193
|
+
b = _offset_for_target_iou(rng, a, b)
|
|
194
|
+
yield cat, a, b
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
DEFAULT_CATEGORIES = (
|
|
198
|
+
"tiny_square",
|
|
199
|
+
"vehicle",
|
|
200
|
+
"small_square",
|
|
201
|
+
"medium_square",
|
|
202
|
+
"large_square",
|
|
203
|
+
"elongated",
|
|
204
|
+
"elongated_thin",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def evaluate_pairs(
|
|
209
|
+
pairs: Sequence[Tuple[str, torch.Tensor, torch.Tensor]],
|
|
210
|
+
*,
|
|
211
|
+
device: torch.device,
|
|
212
|
+
params: GeometryParams,
|
|
213
|
+
chunk_size: int = 256,
|
|
214
|
+
) -> List[PairRecord]:
|
|
215
|
+
records: List[PairRecord] = []
|
|
216
|
+
n = len(pairs)
|
|
217
|
+
for start in range(0, n, chunk_size):
|
|
218
|
+
chunk = pairs[start : start + chunk_size]
|
|
219
|
+
boxes1 = torch.stack([p[1] for p in chunk])
|
|
220
|
+
boxes2 = torch.stack([p[2] for p in chunk])
|
|
221
|
+
num_samples = _resolve_num_samples(boxes1, boxes2, params)
|
|
222
|
+
sampled = _sampled_iou_batch(
|
|
223
|
+
boxes1, boxes2, device=device, num_samples=num_samples
|
|
224
|
+
)
|
|
225
|
+
for i, (cat, a, b) in enumerate(chunk):
|
|
226
|
+
exact = _exact_iou(a, b)
|
|
227
|
+
records.append(
|
|
228
|
+
PairRecord(
|
|
229
|
+
category=cat,
|
|
230
|
+
w1=float(a[2]),
|
|
231
|
+
h1=float(a[3]),
|
|
232
|
+
w2=float(b[2]),
|
|
233
|
+
h2=float(b[3]),
|
|
234
|
+
exact=exact,
|
|
235
|
+
sampled=float(sampled[i].item()),
|
|
236
|
+
num_samples=num_samples,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
return records
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@dataclass
|
|
243
|
+
class ErrorStats:
|
|
244
|
+
count: int
|
|
245
|
+
mean_abs: float
|
|
246
|
+
p50_abs: float
|
|
247
|
+
p90_abs: float
|
|
248
|
+
p99_abs: float
|
|
249
|
+
max_abs: float
|
|
250
|
+
mean_signed: float
|
|
251
|
+
mean_rel_pct: float
|
|
252
|
+
p90_rel_pct: float
|
|
253
|
+
frac_gt_5pct: float
|
|
254
|
+
frac_gt_10pct: float
|
|
255
|
+
frac_gt_20pct: float
|
|
256
|
+
mean_samples: float
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _summarize(records: Sequence[PairRecord]) -> ErrorStats:
|
|
260
|
+
if not records:
|
|
261
|
+
return ErrorStats(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
|
262
|
+
abs_err = np.array([r.abs_error for r in records], dtype=np.float64)
|
|
263
|
+
signed = np.array([r.signed_error for r in records], dtype=np.float64)
|
|
264
|
+
rel = np.array([r.rel_error_pct for r in records], dtype=np.float64)
|
|
265
|
+
rel = rel[np.isfinite(rel)]
|
|
266
|
+
exact = np.array([r.exact for r in records], dtype=np.float64)
|
|
267
|
+
samples = np.array([r.num_samples for r in records], dtype=np.float64)
|
|
268
|
+
|
|
269
|
+
def _frac(th: float) -> float:
|
|
270
|
+
mask = exact > 1e-6
|
|
271
|
+
if not mask.any():
|
|
272
|
+
return 0.0
|
|
273
|
+
return float((abs_err[mask] > th).mean())
|
|
274
|
+
|
|
275
|
+
return ErrorStats(
|
|
276
|
+
count=len(records),
|
|
277
|
+
mean_abs=float(abs_err.mean()),
|
|
278
|
+
p50_abs=float(np.percentile(abs_err, 50)),
|
|
279
|
+
p90_abs=float(np.percentile(abs_err, 90)),
|
|
280
|
+
p99_abs=float(np.percentile(abs_err, 99)),
|
|
281
|
+
max_abs=float(abs_err.max()),
|
|
282
|
+
mean_signed=float(signed.mean()),
|
|
283
|
+
mean_rel_pct=float(rel.mean()) if rel.size else float("nan"),
|
|
284
|
+
p90_rel_pct=float(np.percentile(rel, 90)) if rel.size else float("nan"),
|
|
285
|
+
frac_gt_5pct=_frac(0.05),
|
|
286
|
+
frac_gt_10pct=_frac(0.10),
|
|
287
|
+
frac_gt_20pct=_frac(0.20),
|
|
288
|
+
mean_samples=float(samples.mean()),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _fmt_stats(label: str, stats: ErrorStats) -> str:
|
|
293
|
+
if stats.count == 0:
|
|
294
|
+
return f"{label:16s} (no pairs)"
|
|
295
|
+
return (
|
|
296
|
+
f"{label:16s} n={stats.count:5d} "
|
|
297
|
+
f"|err| mean={stats.mean_abs:.4f} p50={stats.p50_abs:.4f} "
|
|
298
|
+
f"p90={stats.p90_abs:.4f} p99={stats.p99_abs:.4f} max={stats.max_abs:.4f} "
|
|
299
|
+
f"rel% mean={stats.mean_rel_pct:6.1f} p90={stats.p90_rel_pct:6.1f} "
|
|
300
|
+
f">5%={stats.frac_gt_5pct:.1%} >10%={stats.frac_gt_10pct:.1%} >20%={stats.frac_gt_20pct:.1%} "
|
|
301
|
+
f"grid≈{stats.mean_samples:.0f}"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _print_report(
|
|
306
|
+
records: List[PairRecord],
|
|
307
|
+
params: GeometryParams,
|
|
308
|
+
*,
|
|
309
|
+
title: str = "Sampled vs Shapely rIoU",
|
|
310
|
+
) -> None:
|
|
311
|
+
print(f"\n=== {title} ===")
|
|
312
|
+
print(
|
|
313
|
+
f"Geometry: spacing={params.target_spacing_px}px "
|
|
314
|
+
f"min={params.min_samples} max={params.max_samples} "
|
|
315
|
+
f"min_short_pts={params.min_points_short}"
|
|
316
|
+
)
|
|
317
|
+
overall = _summarize(records)
|
|
318
|
+
print(_fmt_stats("ALL", overall))
|
|
319
|
+
|
|
320
|
+
by_cat: dict[str, List[PairRecord]] = {}
|
|
321
|
+
for r in records:
|
|
322
|
+
by_cat.setdefault(r.category, []).append(r)
|
|
323
|
+
for cat in sorted(by_cat):
|
|
324
|
+
print(_fmt_stats(cat, _summarize(by_cat[cat])))
|
|
325
|
+
|
|
326
|
+
# IoU bins (exact)
|
|
327
|
+
bins = [(0.0, 0.0), (0.0, 0.3), (0.3, 0.7), (0.7, 1.0), (1.0, 1.0 + 1e-9)]
|
|
328
|
+
labels = ["exact=0", "0-0.3", "0.3-0.7", "0.7-1", "exact=1"]
|
|
329
|
+
print("\nBy exact IoU bin:")
|
|
330
|
+
for (lo, hi), lab in zip(bins, labels):
|
|
331
|
+
if lo == hi == 0.0:
|
|
332
|
+
subset = [r for r in records if r.exact <= 1e-8]
|
|
333
|
+
elif lo >= 1.0:
|
|
334
|
+
subset = [r for r in records if r.exact >= 1.0 - 1e-6]
|
|
335
|
+
else:
|
|
336
|
+
subset = [r for r in records if lo < r.exact <= hi]
|
|
337
|
+
print(f" {_fmt_stats(lab, _summarize(subset))}")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _write_csv(path: Path, records: Sequence[PairRecord], params: GeometryParams) -> None:
|
|
341
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
342
|
+
with path.open("w", newline="") as f:
|
|
343
|
+
w = csv.writer(f)
|
|
344
|
+
w.writerow(
|
|
345
|
+
[
|
|
346
|
+
"category",
|
|
347
|
+
"w1",
|
|
348
|
+
"h1",
|
|
349
|
+
"w2",
|
|
350
|
+
"h2",
|
|
351
|
+
"exact_iou",
|
|
352
|
+
"sampled_iou",
|
|
353
|
+
"abs_error",
|
|
354
|
+
"signed_error",
|
|
355
|
+
"rel_error_pct",
|
|
356
|
+
"num_samples",
|
|
357
|
+
"target_spacing_px",
|
|
358
|
+
"min_samples",
|
|
359
|
+
"max_samples",
|
|
360
|
+
"min_points_short",
|
|
361
|
+
]
|
|
362
|
+
)
|
|
363
|
+
for r in records:
|
|
364
|
+
w.writerow(
|
|
365
|
+
[
|
|
366
|
+
r.category,
|
|
367
|
+
f"{r.w1:.4f}",
|
|
368
|
+
f"{r.h1:.4f}",
|
|
369
|
+
f"{r.w2:.4f}",
|
|
370
|
+
f"{r.h2:.4f}",
|
|
371
|
+
f"{r.exact:.6f}",
|
|
372
|
+
f"{r.sampled:.6f}",
|
|
373
|
+
f"{r.abs_error:.6f}",
|
|
374
|
+
f"{r.signed_error:.6f}",
|
|
375
|
+
f"{r.rel_error_pct:.4f}",
|
|
376
|
+
r.num_samples,
|
|
377
|
+
params.target_spacing_px,
|
|
378
|
+
params.min_samples,
|
|
379
|
+
params.max_samples,
|
|
380
|
+
params.min_points_short,
|
|
381
|
+
]
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _build_pairs(
|
|
386
|
+
rng: np.random.Generator,
|
|
387
|
+
*,
|
|
388
|
+
pairs: int,
|
|
389
|
+
categories: Sequence[str],
|
|
390
|
+
) -> List[Tuple[str, torch.Tensor, torch.Tensor]]:
|
|
391
|
+
n_per = max(1, pairs // len(categories))
|
|
392
|
+
out = list(generate_pairs(rng, n_per_category=n_per, categories=categories))
|
|
393
|
+
if len(out) < pairs:
|
|
394
|
+
extra = pairs - len(out)
|
|
395
|
+
out.extend(
|
|
396
|
+
generate_pairs(rng, n_per_category=1, categories=categories[:extra])
|
|
397
|
+
)
|
|
398
|
+
return out[:pairs]
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
402
|
+
_require_shapely()
|
|
403
|
+
|
|
404
|
+
parser = argparse.ArgumentParser(
|
|
405
|
+
description="Measure sampling error vs exact Shapely rIoU.",
|
|
406
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
407
|
+
)
|
|
408
|
+
parser.add_argument("--pairs", type=int, default=3000, help="Number of box pairs")
|
|
409
|
+
parser.add_argument("--seed", type=int, default=42)
|
|
410
|
+
parser.add_argument(
|
|
411
|
+
"--categories",
|
|
412
|
+
nargs="+",
|
|
413
|
+
default=list(DEFAULT_CATEGORIES),
|
|
414
|
+
choices=list(DEFAULT_CATEGORIES),
|
|
415
|
+
help="Strata to sample",
|
|
416
|
+
)
|
|
417
|
+
parser.add_argument("--target-spacing", type=float, default=_DEFAULT_TARGET_SPACING_PX)
|
|
418
|
+
parser.add_argument("--min-samples", type=int, default=_DEFAULT_MIN_SAMPLES)
|
|
419
|
+
parser.add_argument("--max-samples", type=int, default=_DEFAULT_MAX_SAMPLES)
|
|
420
|
+
parser.add_argument(
|
|
421
|
+
"--min-points-short", type=int, default=_MIN_POINTS_ALONG_SHORT_SIDE
|
|
422
|
+
)
|
|
423
|
+
parser.add_argument(
|
|
424
|
+
"--device",
|
|
425
|
+
default="cuda" if torch.cuda.is_available() else "cpu",
|
|
426
|
+
help="Device for oriented_box_iou_gpu",
|
|
427
|
+
)
|
|
428
|
+
parser.add_argument("--chunk-size", type=int, default=256)
|
|
429
|
+
parser.add_argument("--csv", type=Path, default=None, help="Write per-pair CSV")
|
|
430
|
+
parser.add_argument(
|
|
431
|
+
"--sweep-spacing",
|
|
432
|
+
type=float,
|
|
433
|
+
nargs="+",
|
|
434
|
+
default=None,
|
|
435
|
+
help="Sweep target_spacing_px values (overrides --target-spacing)",
|
|
436
|
+
)
|
|
437
|
+
args = parser.parse_args(argv)
|
|
438
|
+
|
|
439
|
+
device = torch.device(args.device)
|
|
440
|
+
rng = np.random.default_rng(args.seed)
|
|
441
|
+
pairs = _build_pairs(rng, pairs=args.pairs, categories=args.categories)
|
|
442
|
+
|
|
443
|
+
spacings: List[float]
|
|
444
|
+
if args.sweep_spacing:
|
|
445
|
+
spacings = list(args.sweep_spacing)
|
|
446
|
+
else:
|
|
447
|
+
spacings = [args.target_spacing]
|
|
448
|
+
|
|
449
|
+
all_records: List[PairRecord] = []
|
|
450
|
+
for spacing in spacings:
|
|
451
|
+
params = GeometryParams(
|
|
452
|
+
target_spacing_px=spacing,
|
|
453
|
+
min_samples=args.min_samples,
|
|
454
|
+
max_samples=args.max_samples,
|
|
455
|
+
min_points_short=args.min_points_short,
|
|
456
|
+
)
|
|
457
|
+
title = f"spacing={spacing}px"
|
|
458
|
+
records = evaluate_pairs(
|
|
459
|
+
pairs, device=device, params=params, chunk_size=args.chunk_size
|
|
460
|
+
)
|
|
461
|
+
_print_report(records, params, title=title)
|
|
462
|
+
if args.csv and len(spacings) == 1:
|
|
463
|
+
_write_csv(args.csv, records, params)
|
|
464
|
+
all_records = records
|
|
465
|
+
|
|
466
|
+
if args.csv and len(spacings) > 1:
|
|
467
|
+
print("\nNote: --csv ignored during multi-value --sweep-spacing", file=sys.stderr)
|
|
468
|
+
|
|
469
|
+
if len(spacings) == 1 and all_records:
|
|
470
|
+
worst = sorted(all_records, key=lambda r: r.abs_error, reverse=True)[:10]
|
|
471
|
+
print("\nTop 10 |error| pairs:")
|
|
472
|
+
for r in worst:
|
|
473
|
+
print(
|
|
474
|
+
f" {r.category:14s} box1={r.w1:.1f}x{r.h1:.1f} box2={r.w2:.1f}x{r.h2:.1f} "
|
|
475
|
+
f"exact={r.exact:.4f} sampled={r.sampled:.4f} |err|={r.abs_error:.4f} "
|
|
476
|
+
f"grid={r.num_samples}"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
return 0
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
if __name__ == "__main__":
|
|
483
|
+
raise SystemExit(main())
|