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
tools/app.py
ADDED
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Gradio app to browse inference results (predictions mode) or explore a DOTA
|
|
4
|
+
dataset without predictions (dataset mode). Supports tiled and original
|
|
5
|
+
DOTA directory layouts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import argparse
|
|
11
|
+
import tempfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List, Tuple, Optional
|
|
14
|
+
|
|
15
|
+
# Patch Gradio to handle schema errors
|
|
16
|
+
try:
|
|
17
|
+
from gradio_client import utils as gradio_client_utils
|
|
18
|
+
|
|
19
|
+
# Patch _json_schema_to_python_type to handle boolean schemas
|
|
20
|
+
original_json_schema = gradio_client_utils._json_schema_to_python_type
|
|
21
|
+
|
|
22
|
+
def patched_json_schema(schema, defs=None):
|
|
23
|
+
"""Patched version that handles boolean and invalid schemas"""
|
|
24
|
+
if isinstance(schema, bool):
|
|
25
|
+
return "Any"
|
|
26
|
+
if not isinstance(schema, dict):
|
|
27
|
+
return "Any"
|
|
28
|
+
try:
|
|
29
|
+
return original_json_schema(schema, defs)
|
|
30
|
+
except Exception:
|
|
31
|
+
return "Any"
|
|
32
|
+
|
|
33
|
+
gradio_client_utils._json_schema_to_python_type = patched_json_schema
|
|
34
|
+
|
|
35
|
+
# Also patch get_type
|
|
36
|
+
if hasattr(gradio_client_utils, 'get_type'):
|
|
37
|
+
original_get_type = gradio_client_utils.get_type
|
|
38
|
+
|
|
39
|
+
def patched_get_type(schema):
|
|
40
|
+
if isinstance(schema, bool):
|
|
41
|
+
return "Any"
|
|
42
|
+
if not isinstance(schema, dict):
|
|
43
|
+
return "Any"
|
|
44
|
+
try:
|
|
45
|
+
return original_get_type(schema)
|
|
46
|
+
except Exception:
|
|
47
|
+
return "Any"
|
|
48
|
+
|
|
49
|
+
gradio_client_utils.get_type = patched_get_type
|
|
50
|
+
except Exception:
|
|
51
|
+
pass # If patching fails, continue anyway
|
|
52
|
+
|
|
53
|
+
import gradio as gr
|
|
54
|
+
import cv2
|
|
55
|
+
import numpy as np
|
|
56
|
+
from PIL import Image
|
|
57
|
+
|
|
58
|
+
# Add project root to path
|
|
59
|
+
import sys
|
|
60
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
61
|
+
|
|
62
|
+
from oriented_det.geometry import RBox
|
|
63
|
+
from oriented_det.data import DOTAAnnotation
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Image extensions to scan in dataset mode
|
|
67
|
+
_IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff'}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def detect_data_root() -> str:
|
|
71
|
+
"""Detect the data root directory - placeholder for now."""
|
|
72
|
+
# Try to detect from common locations or use environment variable
|
|
73
|
+
data_root = os.environ.get('DOTA_DATA_ROOT', '/mnt/data/share/DOTA-v2.0')
|
|
74
|
+
return data_root
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def list_dota_images(data_root: str, tiles_dir: Optional[str] = None) -> List[Dict]:
|
|
78
|
+
"""Build a list of image entries by scanning a DOTA dataset directory.
|
|
79
|
+
|
|
80
|
+
Supports both tiled layout (e.g. train/tiles_1024/images) and flat layout
|
|
81
|
+
(e.g. train/images or train/ with images directly inside).
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
data_root: Root path of the dataset (e.g. /path/to/dota).
|
|
85
|
+
tiles_dir: Optional subpath (e.g. 'train', 'val', 'train/tiles_1024').
|
|
86
|
+
If None, scans data_root directly.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
List of dicts with keys: image_path (relative to data_root), and
|
|
90
|
+
optionally image_width, image_height (left unset; filled lazily in get_images).
|
|
91
|
+
"""
|
|
92
|
+
root = Path(data_root)
|
|
93
|
+
base = root if not tiles_dir else root / tiles_dir
|
|
94
|
+
images_dir = base / "images" if (base / "images").is_dir() else base
|
|
95
|
+
if not images_dir.is_dir():
|
|
96
|
+
raise FileNotFoundError(f"Images directory not found: {images_dir}")
|
|
97
|
+
|
|
98
|
+
entries = []
|
|
99
|
+
for path in sorted(images_dir.iterdir()):
|
|
100
|
+
if path.is_file() and path.suffix.lower() in _IMAGE_EXTENSIONS:
|
|
101
|
+
try:
|
|
102
|
+
rel = path.relative_to(root)
|
|
103
|
+
except ValueError:
|
|
104
|
+
rel = path
|
|
105
|
+
entries.append({
|
|
106
|
+
"image_path": str(rel).replace(os.sep, '/'),
|
|
107
|
+
"predictions": [],
|
|
108
|
+
})
|
|
109
|
+
return entries
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def format_stats_markdown(metadata: Dict, result: Optional[Dict], class_names: Optional[List[str]] = None) -> str:
|
|
113
|
+
"""Format diagnostics (global) and per-image stats as markdown for the Stats tab."""
|
|
114
|
+
class_names = class_names or []
|
|
115
|
+
lines = []
|
|
116
|
+
diag = metadata.get("diagnostics")
|
|
117
|
+
if diag:
|
|
118
|
+
lines.append("## Global diagnostics")
|
|
119
|
+
if isinstance(diag.get('mAP'), (int, float)):
|
|
120
|
+
lines.append(f"- **mAP** (IoU={diag.get('iou_threshold', 0.5)}): {diag['mAP']:.4f}")
|
|
121
|
+
else:
|
|
122
|
+
lines.append(f"- **mAP**: {diag.get('mAP_error', 'N/A')}")
|
|
123
|
+
lines.append(f"- Pipeline: raw → after threshold → after NMS: **{diag.get('total_raw', 0)}** → **{diag.get('total_after_threshold', 0)}** → **{diag.get('total_after_nms', 0)}**")
|
|
124
|
+
if diag.get('score_min') is not None:
|
|
125
|
+
lines.append(f"- Score (all images): min={diag['score_min']:.4f} max={diag['score_max']:.4f} mean={diag['score_mean']:.4f}")
|
|
126
|
+
if diag.get('class_aps'):
|
|
127
|
+
lines.append("\n**Per-class AP:**")
|
|
128
|
+
for cname, ap in sorted(diag["class_aps"].items()):
|
|
129
|
+
lines.append(f" - {cname}: {ap:.4f}")
|
|
130
|
+
best_f1 = metadata.get("best_threshold_f1") or metadata.get("best_threshold_f2")
|
|
131
|
+
if isinstance(best_f1, dict) and best_f1:
|
|
132
|
+
lines.append("\n**Best threshold by F1:**")
|
|
133
|
+
lines.append(
|
|
134
|
+
f" - threshold={best_f1.get('threshold', 0.0):.4f}, "
|
|
135
|
+
f"precision={best_f1.get('precision', 0.0):.4f}, "
|
|
136
|
+
f"recall={best_f1.get('recall', 0.0):.4f}, "
|
|
137
|
+
f"F1={best_f1.get('f1', 0.0):.4f}"
|
|
138
|
+
)
|
|
139
|
+
lines.append("")
|
|
140
|
+
if result and result.get("stats"):
|
|
141
|
+
st = result["stats"]
|
|
142
|
+
lines.append("## This image")
|
|
143
|
+
lines.append(f"- Raw → after threshold → after NMS: **{st.get('num_raw', 0)}** → **{st.get('num_after_threshold', 0)}** → **{st.get('num_after_nms', 0)}**")
|
|
144
|
+
if st.get("score_min") is not None:
|
|
145
|
+
lines.append(f"- Score: min={st['score_min']:.4f} max={st['score_max']:.4f} mean={st['score_mean']:.4f}")
|
|
146
|
+
def _class_label(k, names):
|
|
147
|
+
ki = int(k) if isinstance(k, str) and str(k).lstrip("-").isdigit() else (k if isinstance(k, int) else -1)
|
|
148
|
+
return names[ki] if isinstance(ki, int) and 0 <= ki < len(names) else f"class_{k}"
|
|
149
|
+
|
|
150
|
+
if st.get("per_class_raw"):
|
|
151
|
+
items = st["per_class_raw"].items()
|
|
152
|
+
sorted_items = sorted(items, key=lambda kv: (0, kv[0]) if isinstance(kv[0], int) else (1, str(kv[0])))
|
|
153
|
+
lines.append("\n**Per-class (raw):** " + ", ".join(f"{_class_label(k, class_names)}:{v}" for k, v in sorted_items))
|
|
154
|
+
if st.get("per_class_final"):
|
|
155
|
+
items = st["per_class_final"].items()
|
|
156
|
+
sorted_items = sorted(items, key=lambda kv: (0, kv[0]) if isinstance(kv[0], int) else (1, str(kv[0])))
|
|
157
|
+
lines.append("\n**Per-class (after NMS):** " + ", ".join(f"{_class_label(k, class_names)}:{v}" for k, v in sorted_items))
|
|
158
|
+
if not lines:
|
|
159
|
+
return (
|
|
160
|
+
"No diagnostics in this predictions directory. "
|
|
161
|
+
"`make preds` writes inference only; run **`make metrics`** on that output folder "
|
|
162
|
+
"(or **`make metrics`**) to compute mAP / PR and per-image stats, then reopen the viewer."
|
|
163
|
+
)
|
|
164
|
+
return "\n".join(lines)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def load_dota_annotations(txt_file: str) -> Tuple[np.ndarray, List[str]]:
|
|
168
|
+
"""Load DOTA format annotations from txt file
|
|
169
|
+
Returns: (boxes array, class_names list)"""
|
|
170
|
+
rboxes = []
|
|
171
|
+
class_names = []
|
|
172
|
+
if not os.path.exists(txt_file):
|
|
173
|
+
return np.array([]).reshape(0, 5), []
|
|
174
|
+
|
|
175
|
+
with open(txt_file, 'r') as f:
|
|
176
|
+
for line in f:
|
|
177
|
+
line = line.strip()
|
|
178
|
+
if not line:
|
|
179
|
+
continue
|
|
180
|
+
try:
|
|
181
|
+
ann = DOTAAnnotation.from_line(line)
|
|
182
|
+
rboxes.append(ann.rbox)
|
|
183
|
+
class_names.append(ann.class_name)
|
|
184
|
+
except Exception:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
if len(rboxes) == 0:
|
|
188
|
+
return np.array([]).reshape(0, 5), []
|
|
189
|
+
|
|
190
|
+
# Convert to array format (cx, cy, w, h, angle)
|
|
191
|
+
rbox_array = np.array([[rbox.cx, rbox.cy, rbox.width, rbox.height, rbox.angle]
|
|
192
|
+
for rbox in rboxes])
|
|
193
|
+
return rbox_array, class_names
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def array_to_rbox(bbox_array: List[float]) -> RBox:
|
|
197
|
+
"""Convert array format [cx, cy, w, h, angle] to RBox."""
|
|
198
|
+
return RBox(cx=bbox_array[0], cy=bbox_array[1],
|
|
199
|
+
width=bbox_array[2], height=bbox_array[3],
|
|
200
|
+
angle=bbox_array[4])
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _canonicalize_name(name: str) -> str:
|
|
204
|
+
return str(name).strip().lower()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _to_float(value: Any, default: float) -> float:
|
|
208
|
+
try:
|
|
209
|
+
return float(value)
|
|
210
|
+
except (TypeError, ValueError):
|
|
211
|
+
return float(default)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _slider_index(value: Any, default: int = 0) -> int:
|
|
215
|
+
"""Coerce Gradio slider/state payloads; None fails Slider preprocess (min bound check)."""
|
|
216
|
+
if value is None:
|
|
217
|
+
return default
|
|
218
|
+
try:
|
|
219
|
+
return int(value)
|
|
220
|
+
except (TypeError, ValueError):
|
|
221
|
+
return default
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class SimpleViewer:
|
|
225
|
+
"""Unified viewer for inference results or dataset-only exploration."""
|
|
226
|
+
|
|
227
|
+
def __init__(
|
|
228
|
+
self,
|
|
229
|
+
predictions_dir: Optional[str] = None,
|
|
230
|
+
data_root: Optional[str] = None,
|
|
231
|
+
results: Optional[List[Dict]] = None,
|
|
232
|
+
):
|
|
233
|
+
self.current_idx = 0
|
|
234
|
+
self.sorted_indices = []
|
|
235
|
+
self.current_sort = "no order"
|
|
236
|
+
self.last_thresh_signature = None
|
|
237
|
+
self._gt_count_cache: Dict[int, int] = {} # for dataset mode sort by object count
|
|
238
|
+
|
|
239
|
+
if predictions_dir is not None:
|
|
240
|
+
self._init_from_predictions(predictions_dir, data_root)
|
|
241
|
+
elif results is not None and data_root is not None:
|
|
242
|
+
self._init_from_dataset(results, data_root)
|
|
243
|
+
else:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
"Provide either predictions_dir (and optional data_root) "
|
|
246
|
+
"or results and data_root for dataset mode."
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def _init_from_predictions(self, predictions_dir: str, data_root: Optional[str]) -> None:
|
|
250
|
+
self.has_predictions = True
|
|
251
|
+
self.predictions_dir = predictions_dir
|
|
252
|
+
self.json_path = os.path.join(predictions_dir, 'predictions.json')
|
|
253
|
+
if not os.path.exists(self.json_path):
|
|
254
|
+
raise ValueError(f"predictions.json not found in {predictions_dir}")
|
|
255
|
+
with open(self.json_path, 'r') as f:
|
|
256
|
+
data = json.load(f)
|
|
257
|
+
self.metadata = data['metadata']
|
|
258
|
+
self.results = data['results']
|
|
259
|
+
self.class_names = list(self.metadata.get("class_names") or [])
|
|
260
|
+
best_meta = self.metadata.get("best_threshold_f1") or self.metadata.get("best_threshold_f2")
|
|
261
|
+
best_thr = (
|
|
262
|
+
best_meta.get("threshold")
|
|
263
|
+
if isinstance(best_meta, dict)
|
|
264
|
+
else None
|
|
265
|
+
)
|
|
266
|
+
if isinstance(best_thr, (int, float)):
|
|
267
|
+
self.conf_threshold = float(best_thr)
|
|
268
|
+
else:
|
|
269
|
+
self.conf_threshold = _to_float(
|
|
270
|
+
self.metadata.get('score_threshold', self.metadata.get('conf_threshold', 0.3)),
|
|
271
|
+
0.3,
|
|
272
|
+
)
|
|
273
|
+
self.per_class_threshold_defaults = {
|
|
274
|
+
cname: float(self.conf_threshold) for cname in self.class_names
|
|
275
|
+
}
|
|
276
|
+
self.best_threshold_per_class: Dict[str, Dict[str, Any]] = {}
|
|
277
|
+
self._set_defaults_from_config_thresholds()
|
|
278
|
+
if data_root is not None:
|
|
279
|
+
self.data_root = data_root
|
|
280
|
+
elif 'data_root' in self.metadata:
|
|
281
|
+
self.data_root = self.metadata['data_root']
|
|
282
|
+
else:
|
|
283
|
+
self.data_root = detect_data_root()
|
|
284
|
+
self.f1_scores = {}
|
|
285
|
+
self.load_f1_scores()
|
|
286
|
+
self._set_defaults_from_analysis_thresholds()
|
|
287
|
+
if self.best_threshold_per_class:
|
|
288
|
+
self.metadata["best_threshold_per_class"] = self.best_threshold_per_class
|
|
289
|
+
self.sorted_indices = list(range(len(self.results)))
|
|
290
|
+
print(f"Using data root: {self.data_root}")
|
|
291
|
+
print(f"Loaded {len(self.results)} images from {predictions_dir}")
|
|
292
|
+
|
|
293
|
+
def _init_from_dataset(self, results: List[Dict], data_root: str) -> None:
|
|
294
|
+
self.has_predictions = False
|
|
295
|
+
self.predictions_dir = None
|
|
296
|
+
self.metadata = {}
|
|
297
|
+
self.results = results
|
|
298
|
+
self.conf_threshold = 0.3
|
|
299
|
+
self.class_names = []
|
|
300
|
+
self.per_class_threshold_defaults = {}
|
|
301
|
+
self.best_threshold_per_class = {}
|
|
302
|
+
self.data_root = data_root
|
|
303
|
+
self.f1_scores = {}
|
|
304
|
+
self.sorted_indices = list(range(len(self.results)))
|
|
305
|
+
print(f"Using data root: {self.data_root}")
|
|
306
|
+
print(f"Dataset mode: loaded {len(self.results)} images")
|
|
307
|
+
|
|
308
|
+
def _set_defaults_from_config_thresholds(self) -> None:
|
|
309
|
+
raw = self.metadata.get("per_class_score_threshold")
|
|
310
|
+
if not isinstance(raw, dict):
|
|
311
|
+
return
|
|
312
|
+
by_key = {_canonicalize_name(k): _to_float(v, self.conf_threshold) for k, v in raw.items()}
|
|
313
|
+
for cname in self.class_names:
|
|
314
|
+
thr = by_key.get(_canonicalize_name(cname))
|
|
315
|
+
if thr is not None:
|
|
316
|
+
self.per_class_threshold_defaults[cname] = float(thr)
|
|
317
|
+
|
|
318
|
+
def _set_defaults_from_analysis_thresholds(self) -> None:
|
|
319
|
+
if not isinstance(self.best_threshold_per_class, dict):
|
|
320
|
+
return
|
|
321
|
+
by_key = {_canonicalize_name(k): v for k, v in self.best_threshold_per_class.items()}
|
|
322
|
+
for cname in self.class_names:
|
|
323
|
+
entry = by_key.get(_canonicalize_name(cname))
|
|
324
|
+
if isinstance(entry, dict) and "threshold" in entry:
|
|
325
|
+
self.per_class_threshold_defaults[cname] = _to_float(entry.get("threshold"), self.conf_threshold)
|
|
326
|
+
|
|
327
|
+
def load_f1_scores(self):
|
|
328
|
+
"""Load F1 scores from analysis JSON if available"""
|
|
329
|
+
analysis_file = self.metadata.get("analysis_file", "analysis_iou0.50.json")
|
|
330
|
+
analysis_path = (
|
|
331
|
+
analysis_file
|
|
332
|
+
if os.path.isabs(str(analysis_file))
|
|
333
|
+
else os.path.join(self.predictions_dir, str(analysis_file))
|
|
334
|
+
)
|
|
335
|
+
if os.path.exists(analysis_path):
|
|
336
|
+
try:
|
|
337
|
+
with open(analysis_path, 'r') as f:
|
|
338
|
+
analysis_data = json.load(f)
|
|
339
|
+
per_image_metrics = analysis_data.get('per_image_metrics', [])
|
|
340
|
+
for metric in per_image_metrics:
|
|
341
|
+
image_name = metric.get('image_name')
|
|
342
|
+
f1 = metric.get('f1', 0.0)
|
|
343
|
+
if image_name:
|
|
344
|
+
self.f1_scores[image_name] = float(f1)
|
|
345
|
+
best_per_class = analysis_data.get("best_threshold_per_class") or {}
|
|
346
|
+
if isinstance(best_per_class, dict):
|
|
347
|
+
self.best_threshold_per_class = best_per_class
|
|
348
|
+
print(f"Loaded F1 scores for {len(self.f1_scores)} images from analysis JSON")
|
|
349
|
+
except Exception as e:
|
|
350
|
+
print(f"Warning: Could not load F1 scores from analysis JSON: {e}")
|
|
351
|
+
|
|
352
|
+
def _effective_threshold_for_pred(
|
|
353
|
+
self,
|
|
354
|
+
pred: Dict[str, Any],
|
|
355
|
+
threshold: float,
|
|
356
|
+
use_per_class_thresholds: bool = False,
|
|
357
|
+
per_class_thresholds: Optional[Dict[str, float]] = None,
|
|
358
|
+
) -> float:
|
|
359
|
+
if not use_per_class_thresholds or not per_class_thresholds:
|
|
360
|
+
return float(threshold)
|
|
361
|
+
cls_name = str(pred.get("class_name", ""))
|
|
362
|
+
if cls_name in per_class_thresholds:
|
|
363
|
+
return float(per_class_thresholds[cls_name])
|
|
364
|
+
key = _canonicalize_name(cls_name)
|
|
365
|
+
if key in per_class_thresholds:
|
|
366
|
+
return float(per_class_thresholds[key])
|
|
367
|
+
return float(threshold)
|
|
368
|
+
|
|
369
|
+
def get_detection_count(
|
|
370
|
+
self,
|
|
371
|
+
idx: int,
|
|
372
|
+
threshold: float,
|
|
373
|
+
use_per_class_thresholds: bool = False,
|
|
374
|
+
per_class_thresholds: Optional[Dict[str, float]] = None,
|
|
375
|
+
) -> int:
|
|
376
|
+
"""Get number of detections for an image at given threshold."""
|
|
377
|
+
result = self.results[idx]
|
|
378
|
+
count = 0
|
|
379
|
+
for pred in result.get('predictions', []):
|
|
380
|
+
thr = self._effective_threshold_for_pred(
|
|
381
|
+
pred,
|
|
382
|
+
threshold,
|
|
383
|
+
use_per_class_thresholds=use_per_class_thresholds,
|
|
384
|
+
per_class_thresholds=per_class_thresholds,
|
|
385
|
+
)
|
|
386
|
+
if pred['score'] >= thr:
|
|
387
|
+
count += 1
|
|
388
|
+
return count
|
|
389
|
+
|
|
390
|
+
def get_stats_markdown(self, display_idx: int) -> str:
|
|
391
|
+
"""Get stats markdown for the image at display_idx (after sorting)."""
|
|
392
|
+
if display_idx < 0 or display_idx >= len(self.sorted_indices):
|
|
393
|
+
return ""
|
|
394
|
+
actual_idx = self.sorted_indices[display_idx]
|
|
395
|
+
result = self.results[actual_idx]
|
|
396
|
+
class_names = self.metadata.get("class_names") or []
|
|
397
|
+
return format_stats_markdown(self.metadata, result, class_names)
|
|
398
|
+
|
|
399
|
+
def get_image_label_markdown(self, display_idx: int) -> str:
|
|
400
|
+
"""Return markdown identifying the current image (path or name)."""
|
|
401
|
+
if display_idx < 0 or display_idx >= len(self.sorted_indices):
|
|
402
|
+
return ""
|
|
403
|
+
actual_idx = self.sorted_indices[display_idx]
|
|
404
|
+
result = self.results[actual_idx]
|
|
405
|
+
rel_path = str(result.get("image_path", "")).replace(os.sep, "/")
|
|
406
|
+
name = result.get("image_name")
|
|
407
|
+
if rel_path:
|
|
408
|
+
display = rel_path
|
|
409
|
+
elif name:
|
|
410
|
+
display = name
|
|
411
|
+
else:
|
|
412
|
+
return ""
|
|
413
|
+
idx_str = f"{display_idx + 1} / {len(self.sorted_indices)}"
|
|
414
|
+
return f"**Image {idx_str}:** `{display}`"
|
|
415
|
+
|
|
416
|
+
def _get_gt_count(self, idx: int) -> int:
|
|
417
|
+
"""Get number of ground-truth objects for an image (for dataset mode sorting)."""
|
|
418
|
+
if idx in self._gt_count_cache:
|
|
419
|
+
return self._gt_count_cache[idx]
|
|
420
|
+
result = self.results[idx]
|
|
421
|
+
relative_path = result['image_path']
|
|
422
|
+
img_path = os.path.join(self.data_root, relative_path) if not os.path.isabs(relative_path) else relative_path
|
|
423
|
+
img_path_obj = Path(img_path)
|
|
424
|
+
possible_label_dirs = [
|
|
425
|
+
img_path_obj.parent.parent / 'labels',
|
|
426
|
+
img_path_obj.parent.parent / 'labelTxt',
|
|
427
|
+
img_path_obj.parent / 'labels',
|
|
428
|
+
]
|
|
429
|
+
txt_path = None
|
|
430
|
+
for label_dir in possible_label_dirs:
|
|
431
|
+
potential_txt = label_dir / f"{img_path_obj.stem}.txt"
|
|
432
|
+
if potential_txt.exists():
|
|
433
|
+
txt_path = potential_txt
|
|
434
|
+
break
|
|
435
|
+
if txt_path is None:
|
|
436
|
+
txt_path = img_path_obj.with_suffix('.txt')
|
|
437
|
+
gt_boxes, _ = load_dota_annotations(str(txt_path))
|
|
438
|
+
count = len(gt_boxes)
|
|
439
|
+
self._gt_count_cache[idx] = count
|
|
440
|
+
return count
|
|
441
|
+
|
|
442
|
+
def apply_sorting(
|
|
443
|
+
self,
|
|
444
|
+
sort_mode: str,
|
|
445
|
+
threshold: float,
|
|
446
|
+
use_per_class_thresholds: bool = False,
|
|
447
|
+
per_class_thresholds: Optional[Dict[str, float]] = None,
|
|
448
|
+
) -> None:
|
|
449
|
+
"""Apply sorting to results and update sorted_indices."""
|
|
450
|
+
self.current_sort = sort_mode
|
|
451
|
+
self.last_thresh_signature = (
|
|
452
|
+
float(threshold),
|
|
453
|
+
bool(use_per_class_thresholds),
|
|
454
|
+
tuple(sorted((str(k), float(v)) for k, v in (per_class_thresholds or {}).items())),
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
if sort_mode == "no order":
|
|
458
|
+
self.sorted_indices = list(range(len(self.results)))
|
|
459
|
+
elif not self.has_predictions:
|
|
460
|
+
# Dataset mode: only no order and object count
|
|
461
|
+
if sort_mode == "objs asc":
|
|
462
|
+
indices_with_objs = [(idx, self._get_gt_count(idx)) for idx in range(len(self.results))]
|
|
463
|
+
indices_with_objs.sort(key=lambda x: x[1])
|
|
464
|
+
self.sorted_indices = [idx for idx, _ in indices_with_objs]
|
|
465
|
+
elif sort_mode == "objs desc":
|
|
466
|
+
indices_with_objs = [(idx, self._get_gt_count(idx)) for idx in range(len(self.results))]
|
|
467
|
+
indices_with_objs.sort(key=lambda x: x[1], reverse=True)
|
|
468
|
+
self.sorted_indices = [idx for idx, _ in indices_with_objs]
|
|
469
|
+
else:
|
|
470
|
+
self.sorted_indices = list(range(len(self.results)))
|
|
471
|
+
else:
|
|
472
|
+
# Predictions mode: F1 and det count
|
|
473
|
+
if sort_mode == "F1 asc":
|
|
474
|
+
indices_with_f1 = []
|
|
475
|
+
for idx in range(len(self.results)):
|
|
476
|
+
result = self.results[idx]
|
|
477
|
+
image_name = result.get('image_name', Path(self.results[idx]['image_path']).stem)
|
|
478
|
+
f1 = self.f1_scores.get(image_name, 0.0)
|
|
479
|
+
indices_with_f1.append((idx, f1))
|
|
480
|
+
indices_with_f1.sort(key=lambda x: x[1])
|
|
481
|
+
self.sorted_indices = [idx for idx, _ in indices_with_f1]
|
|
482
|
+
elif sort_mode == "F1 desc":
|
|
483
|
+
indices_with_f1 = []
|
|
484
|
+
for idx in range(len(self.results)):
|
|
485
|
+
result = self.results[idx]
|
|
486
|
+
image_name = result.get('image_name', Path(self.results[idx]['image_path']).stem)
|
|
487
|
+
f1 = self.f1_scores.get(image_name, 0.0)
|
|
488
|
+
indices_with_f1.append((idx, f1))
|
|
489
|
+
indices_with_f1.sort(key=lambda x: x[1], reverse=True)
|
|
490
|
+
self.sorted_indices = [idx for idx, _ in indices_with_f1]
|
|
491
|
+
elif sort_mode == "dets asc":
|
|
492
|
+
indices_with_dets = [
|
|
493
|
+
(
|
|
494
|
+
idx,
|
|
495
|
+
self.get_detection_count(
|
|
496
|
+
idx,
|
|
497
|
+
threshold,
|
|
498
|
+
use_per_class_thresholds=use_per_class_thresholds,
|
|
499
|
+
per_class_thresholds=per_class_thresholds,
|
|
500
|
+
),
|
|
501
|
+
)
|
|
502
|
+
for idx in range(len(self.results))
|
|
503
|
+
]
|
|
504
|
+
indices_with_dets.sort(key=lambda x: x[1])
|
|
505
|
+
self.sorted_indices = [idx for idx, _ in indices_with_dets]
|
|
506
|
+
elif sort_mode == "dets desc":
|
|
507
|
+
indices_with_dets = [
|
|
508
|
+
(
|
|
509
|
+
idx,
|
|
510
|
+
self.get_detection_count(
|
|
511
|
+
idx,
|
|
512
|
+
threshold,
|
|
513
|
+
use_per_class_thresholds=use_per_class_thresholds,
|
|
514
|
+
per_class_thresholds=per_class_thresholds,
|
|
515
|
+
),
|
|
516
|
+
)
|
|
517
|
+
for idx in range(len(self.results))
|
|
518
|
+
]
|
|
519
|
+
indices_with_dets.sort(key=lambda x: x[1], reverse=True)
|
|
520
|
+
self.sorted_indices = [idx for idx, _ in indices_with_dets]
|
|
521
|
+
else:
|
|
522
|
+
self.sorted_indices = list(range(len(self.results)))
|
|
523
|
+
|
|
524
|
+
def get_images(
|
|
525
|
+
self,
|
|
526
|
+
idx: int,
|
|
527
|
+
threshold: float,
|
|
528
|
+
zoom_scale: float = 2.0,
|
|
529
|
+
show_labels: bool = True,
|
|
530
|
+
view_mode: str = "preds_gt",
|
|
531
|
+
use_per_class_thresholds: bool = False,
|
|
532
|
+
per_class_thresholds: Optional[Dict[str, float]] = None,
|
|
533
|
+
) -> Image.Image:
|
|
534
|
+
"""Render one view of the current image.
|
|
535
|
+
|
|
536
|
+
view_mode:
|
|
537
|
+
- ``image``: original image only
|
|
538
|
+
- ``gt``: ground truth only (green boxes)
|
|
539
|
+
- ``preds``: predictions only (red boxes)
|
|
540
|
+
- ``preds_gt``: GT (green) then predictions (red) on top
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
idx: Displayed index (after sorting)
|
|
544
|
+
threshold: Confidence threshold for predictions
|
|
545
|
+
zoom_scale: Zoom scale factor
|
|
546
|
+
"""
|
|
547
|
+
if idx < 0 or idx >= len(self.sorted_indices):
|
|
548
|
+
idx = 0
|
|
549
|
+
|
|
550
|
+
self.current_idx = idx
|
|
551
|
+
# Map displayed index to actual result index
|
|
552
|
+
actual_idx = self.sorted_indices[idx]
|
|
553
|
+
result = self.results[actual_idx]
|
|
554
|
+
|
|
555
|
+
# Load image
|
|
556
|
+
relative_path = result['image_path']
|
|
557
|
+
if os.path.isabs(relative_path):
|
|
558
|
+
img_path = relative_path
|
|
559
|
+
else:
|
|
560
|
+
img_path = os.path.join(self.data_root, relative_path)
|
|
561
|
+
|
|
562
|
+
img = cv2.imread(img_path)
|
|
563
|
+
if img is None:
|
|
564
|
+
return Image.new('RGB', (800, 600), color='red')
|
|
565
|
+
|
|
566
|
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
|
567
|
+
img_height, img_width = img_rgb.shape[:2]
|
|
568
|
+
|
|
569
|
+
# Scale from preprocessed (model) space to the image we're drawing on.
|
|
570
|
+
# Use the actual loaded image dimensions so boxes always match the displayed image
|
|
571
|
+
# (avoids mismatch when JSON has different image_width/height than the file on disk).
|
|
572
|
+
# Match training/save_predictions: fixed resize to (target_h, target_w).
|
|
573
|
+
resize_mode = result.get('resize_mode', 'fixed')
|
|
574
|
+
target = result.get('target_size', [1024, 1024])
|
|
575
|
+
if isinstance(target, (list, tuple)):
|
|
576
|
+
target_h, target_w = int(target[0]), int(target[1])
|
|
577
|
+
else:
|
|
578
|
+
target_h = target_w = int(target)
|
|
579
|
+
pad_offset_x = pad_offset_y = 0
|
|
580
|
+
if resize_mode == 'pad':
|
|
581
|
+
from oriented_det.data.preprocessing import build_spatial_meta_from_dims
|
|
582
|
+
meta = build_spatial_meta_from_dims('pad', img_width, img_height, target)
|
|
583
|
+
inv = 1.0 / max(meta.scale, 1e-8)
|
|
584
|
+
scale_back_x = scale_back_y = inv
|
|
585
|
+
pad_offset_x = meta.pad_left
|
|
586
|
+
pad_offset_y = meta.pad_top
|
|
587
|
+
elif resize_mode == 'crop':
|
|
588
|
+
from oriented_det.data.preprocessing import build_spatial_meta_from_dims
|
|
589
|
+
meta = build_spatial_meta_from_dims('crop', img_width, img_height, target)
|
|
590
|
+
scale_back_x = scale_back_y = 1.0
|
|
591
|
+
pad_offset_x = meta.pad_left - meta.crop_left
|
|
592
|
+
pad_offset_y = meta.pad_top - meta.crop_top
|
|
593
|
+
else:
|
|
594
|
+
scale_back_x = img_width / max(target_w, 1)
|
|
595
|
+
scale_back_y = img_height / max(target_h, 1)
|
|
596
|
+
|
|
597
|
+
# save_predictions (pad/tile inference): bboxes are already in the loaded image's pixel space.
|
|
598
|
+
if self.metadata.get('bbox_coordinate_space') == 'image_pixels':
|
|
599
|
+
scale_back_x = scale_back_y = 1.0
|
|
600
|
+
pad_offset_x = pad_offset_y = 0
|
|
601
|
+
|
|
602
|
+
# Predictions: red (BGR); ground truth: green
|
|
603
|
+
pred_color_bgr = (0, 0, 255) # red in BGR
|
|
604
|
+
gt_colors = {
|
|
605
|
+
'car': (0, 255, 0),
|
|
606
|
+
'truck': (0, 255, 0),
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
# Ground truth boxes (prefer GT serialized in predictions.json; fallback to label files)
|
|
610
|
+
gt_items = result.get("ground_truths", [])
|
|
611
|
+
if gt_items:
|
|
612
|
+
gt_boxes = np.array([g.get("bbox", [0, 0, 0, 0, 0]) for g in gt_items], dtype=np.float32)
|
|
613
|
+
gt_class_names = [g.get("class_name", "unknown") for g in gt_items]
|
|
614
|
+
else:
|
|
615
|
+
img_path_obj = Path(img_path)
|
|
616
|
+
possible_label_dirs = [
|
|
617
|
+
img_path_obj.parent.parent / 'labels',
|
|
618
|
+
img_path_obj.parent.parent / 'labelTxt',
|
|
619
|
+
img_path_obj.parent / 'labels',
|
|
620
|
+
]
|
|
621
|
+
|
|
622
|
+
txt_path = None
|
|
623
|
+
for label_dir in possible_label_dirs:
|
|
624
|
+
potential_txt = label_dir / f"{img_path_obj.stem}.txt"
|
|
625
|
+
if potential_txt.exists():
|
|
626
|
+
txt_path = potential_txt
|
|
627
|
+
break
|
|
628
|
+
|
|
629
|
+
if txt_path is None:
|
|
630
|
+
txt_path = img_path_obj.with_suffix('.txt')
|
|
631
|
+
|
|
632
|
+
gt_boxes, gt_class_names = load_dota_annotations(str(txt_path))
|
|
633
|
+
|
|
634
|
+
if view_mode == "image":
|
|
635
|
+
out_rgb = img_rgb
|
|
636
|
+
else:
|
|
637
|
+
out_bgr = cv2.cvtColor(img_rgb.copy(), cv2.COLOR_RGB2BGR)
|
|
638
|
+
pred_font = cv2.FONT_HERSHEY_SIMPLEX
|
|
639
|
+
pred_font_scale = 0.4
|
|
640
|
+
pred_font_thickness = 1
|
|
641
|
+
pred_label_bg_bgr = (0, 0, 0)
|
|
642
|
+
pred_label_fg_bgr = (255, 255, 255)
|
|
643
|
+
gt_fill_alpha = 0.25
|
|
644
|
+
font = cv2.FONT_HERSHEY_SIMPLEX
|
|
645
|
+
font_scale = 0.35
|
|
646
|
+
font_thickness = 1
|
|
647
|
+
label_bg_bgr = (0, 0, 0)
|
|
648
|
+
label_fg_bgr = (255, 255, 255)
|
|
649
|
+
|
|
650
|
+
draw_gt = view_mode in ("gt", "preds_gt")
|
|
651
|
+
draw_preds = view_mode in ("preds", "preds_gt") and self.has_predictions
|
|
652
|
+
|
|
653
|
+
if draw_gt:
|
|
654
|
+
gt_draw_items = []
|
|
655
|
+
gt_overlay = out_bgr.copy()
|
|
656
|
+
for i, gt_box in enumerate(gt_boxes):
|
|
657
|
+
rbox = array_to_rbox(gt_box)
|
|
658
|
+
polygon = rbox.to_polygon()
|
|
659
|
+
points = np.array([list(p) for p in polygon.points], dtype=np.int32)
|
|
660
|
+
class_name = gt_class_names[i] if i < len(gt_class_names) else 'unknown'
|
|
661
|
+
color_rgb = gt_colors.get(class_name, (0, 255, 0))
|
|
662
|
+
color_bgr = (color_rgb[2], color_rgb[1], color_rgb[0])
|
|
663
|
+
gt_draw_items.append((points, color_bgr))
|
|
664
|
+
# Fill GT polygons lightly so overlaps with predictions are easier to inspect.
|
|
665
|
+
cv2.fillPoly(gt_overlay, [points], color_bgr)
|
|
666
|
+
if show_labels:
|
|
667
|
+
(text_w, text_h), _ = cv2.getTextSize(class_name, font, font_scale, font_thickness)
|
|
668
|
+
right_x = int(np.max(points[:, 0]))
|
|
669
|
+
center_y = int(np.mean(points[:, 1]))
|
|
670
|
+
tx = max(0, min(right_x + 4, img_width - text_w - 4))
|
|
671
|
+
ty = max(text_h + 2, min(center_y, img_height - 2))
|
|
672
|
+
cv2.rectangle(
|
|
673
|
+
out_bgr, (tx - 2, ty - text_h - 2), (tx + text_w + 2, ty + 2),
|
|
674
|
+
label_bg_bgr, -1,
|
|
675
|
+
)
|
|
676
|
+
cv2.putText(out_bgr, class_name, (tx, ty), font, font_scale, label_fg_bgr, font_thickness)
|
|
677
|
+
out_bgr = cv2.addWeighted(gt_overlay, gt_fill_alpha, out_bgr, 1.0 - gt_fill_alpha, 0)
|
|
678
|
+
for points, color_bgr in gt_draw_items:
|
|
679
|
+
cv2.polylines(out_bgr, [points], isClosed=True, color=color_bgr, thickness=2)
|
|
680
|
+
|
|
681
|
+
if draw_preds:
|
|
682
|
+
pred_draw_items = []
|
|
683
|
+
for pred in result.get("predictions", []):
|
|
684
|
+
class_name = pred.get('class_name', 'car')
|
|
685
|
+
effective_thr = self._effective_threshold_for_pred(
|
|
686
|
+
pred,
|
|
687
|
+
threshold,
|
|
688
|
+
use_per_class_thresholds=use_per_class_thresholds,
|
|
689
|
+
per_class_thresholds=per_class_thresholds,
|
|
690
|
+
)
|
|
691
|
+
if pred['score'] < effective_thr:
|
|
692
|
+
continue
|
|
693
|
+
bbox = pred['bbox']
|
|
694
|
+
rbox = array_to_rbox(bbox)
|
|
695
|
+
rbox_scaled = RBox(
|
|
696
|
+
cx=(rbox.cx - pad_offset_x) * scale_back_x,
|
|
697
|
+
cy=(rbox.cy - pad_offset_y) * scale_back_y,
|
|
698
|
+
width=rbox.width * scale_back_x,
|
|
699
|
+
height=rbox.height * scale_back_y,
|
|
700
|
+
angle=rbox.angle,
|
|
701
|
+
)
|
|
702
|
+
polygon = rbox_scaled.to_polygon()
|
|
703
|
+
points = np.array([list(p) for p in polygon.points], dtype=np.int32)
|
|
704
|
+
pred_draw_items.append((points, pred_color_bgr))
|
|
705
|
+
if show_labels:
|
|
706
|
+
label_text = f"{class_name}: {float(pred['score']):.2f}"
|
|
707
|
+
(tw, th), _ = cv2.getTextSize(label_text, pred_font, pred_font_scale, pred_font_thickness)
|
|
708
|
+
right_x = int(np.max(points[:, 0]))
|
|
709
|
+
center_y = int(np.mean(points[:, 1]))
|
|
710
|
+
tx = max(0, min(right_x + 4, img_width - tw - 4))
|
|
711
|
+
ty = max(th + 2, min(center_y, img_height - 2))
|
|
712
|
+
cv2.rectangle(
|
|
713
|
+
out_bgr,
|
|
714
|
+
(tx - 2, ty - th - 2),
|
|
715
|
+
(tx + tw + 2, ty + 2),
|
|
716
|
+
pred_label_bg_bgr,
|
|
717
|
+
-1,
|
|
718
|
+
)
|
|
719
|
+
cv2.putText(
|
|
720
|
+
out_bgr,
|
|
721
|
+
label_text,
|
|
722
|
+
(tx, ty),
|
|
723
|
+
pred_font,
|
|
724
|
+
pred_font_scale,
|
|
725
|
+
pred_label_fg_bgr,
|
|
726
|
+
pred_font_thickness,
|
|
727
|
+
)
|
|
728
|
+
for points, color_bgr in pred_draw_items:
|
|
729
|
+
cv2.polylines(out_bgr, [points], isClosed=True, color=color_bgr, thickness=2)
|
|
730
|
+
|
|
731
|
+
out_rgb = cv2.cvtColor(out_bgr, cv2.COLOR_BGR2RGB)
|
|
732
|
+
|
|
733
|
+
def apply_zoom(img_array: np.ndarray, scale: float) -> Image.Image:
|
|
734
|
+
if scale == 1.0:
|
|
735
|
+
return Image.fromarray(img_array, "RGB")
|
|
736
|
+
h, w = img_array.shape[:2]
|
|
737
|
+
new_h, new_w = int(h * scale), int(w * scale)
|
|
738
|
+
resized = cv2.resize(img_array, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
|
|
739
|
+
return Image.fromarray(resized, "RGB")
|
|
740
|
+
|
|
741
|
+
return apply_zoom(out_rgb, zoom_scale)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def create_app(
|
|
745
|
+
mode: str = "predictions",
|
|
746
|
+
predictions_dir: Optional[str] = None,
|
|
747
|
+
data_root: Optional[str] = None,
|
|
748
|
+
tiles_dir: Optional[str] = None,
|
|
749
|
+
threshold: Optional[float] = None,
|
|
750
|
+
):
|
|
751
|
+
"""Create Gradio app for predictions viewer or dataset explorer.
|
|
752
|
+
|
|
753
|
+
Args:
|
|
754
|
+
mode: "predictions" or "dataset"
|
|
755
|
+
predictions_dir: Path to predictions directory (required when mode is "predictions")
|
|
756
|
+
data_root: Path to data root (required for dataset mode; optional for predictions)
|
|
757
|
+
tiles_dir: Subpath for dataset (e.g. "train", "train/tiles_1024") when mode is "dataset"
|
|
758
|
+
threshold: Confidence threshold for predictions mode (default: 0.3)
|
|
759
|
+
"""
|
|
760
|
+
if mode == "dataset":
|
|
761
|
+
if not data_root:
|
|
762
|
+
raise ValueError("Dataset mode requires --data-root")
|
|
763
|
+
results = list_dota_images(data_root, tiles_dir)
|
|
764
|
+
if not results:
|
|
765
|
+
raise ValueError(f"No images found under {data_root}" + (f" / {tiles_dir}" if tiles_dir else ""))
|
|
766
|
+
viewer = SimpleViewer(results=results, data_root=data_root)
|
|
767
|
+
else:
|
|
768
|
+
if not predictions_dir:
|
|
769
|
+
raise ValueError("Predictions mode requires --predictions-dir")
|
|
770
|
+
viewer = SimpleViewer(predictions_dir=predictions_dir, data_root=data_root)
|
|
771
|
+
if threshold is not None:
|
|
772
|
+
viewer.conf_threshold = threshold
|
|
773
|
+
|
|
774
|
+
def _build_per_class_thresholds(values: Tuple[Any, ...]) -> Dict[str, float]:
|
|
775
|
+
if not viewer.class_names:
|
|
776
|
+
return {}
|
|
777
|
+
return {
|
|
778
|
+
cname: _to_float(v, viewer.conf_threshold)
|
|
779
|
+
for cname, v in zip(viewer.class_names, values)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
def _maybe_sort(
|
|
783
|
+
sort_mode: str,
|
|
784
|
+
threshold_val: float,
|
|
785
|
+
use_per_class_val: bool = False,
|
|
786
|
+
per_class_thresholds: Optional[Dict[str, float]] = None,
|
|
787
|
+
) -> None:
|
|
788
|
+
needs_resort = viewer.current_sort != sort_mode
|
|
789
|
+
if viewer.has_predictions and sort_mode in ["dets asc", "dets desc"]:
|
|
790
|
+
threshold_signature = (
|
|
791
|
+
float(threshold_val),
|
|
792
|
+
bool(use_per_class_val),
|
|
793
|
+
tuple(sorted((str(k), float(v)) for k, v in (per_class_thresholds or {}).items())),
|
|
794
|
+
)
|
|
795
|
+
if viewer.last_thresh_signature != threshold_signature:
|
|
796
|
+
needs_resort = True
|
|
797
|
+
if needs_resort:
|
|
798
|
+
viewer.apply_sorting(
|
|
799
|
+
sort_mode,
|
|
800
|
+
float(threshold_val),
|
|
801
|
+
use_per_class_thresholds=use_per_class_val,
|
|
802
|
+
per_class_thresholds=per_class_thresholds,
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
def refresh_prediction_tabs(
|
|
806
|
+
idx: int,
|
|
807
|
+
threshold_val: float,
|
|
808
|
+
zoom_str: str,
|
|
809
|
+
sort_mode: str,
|
|
810
|
+
show_labels_val: bool,
|
|
811
|
+
use_per_class_val: bool,
|
|
812
|
+
*per_class_vals: Any,
|
|
813
|
+
):
|
|
814
|
+
per_class_thresholds = _build_per_class_thresholds(per_class_vals)
|
|
815
|
+
_maybe_sort(sort_mode, threshold_val, use_per_class_val, per_class_thresholds)
|
|
816
|
+
zoom = float(zoom_str.replace("x", ""))
|
|
817
|
+
i = int(idx)
|
|
818
|
+
img_pg = viewer.get_images(
|
|
819
|
+
i,
|
|
820
|
+
float(threshold_val),
|
|
821
|
+
zoom,
|
|
822
|
+
show_labels_val,
|
|
823
|
+
view_mode="preds_gt",
|
|
824
|
+
use_per_class_thresholds=use_per_class_val,
|
|
825
|
+
per_class_thresholds=per_class_thresholds,
|
|
826
|
+
)
|
|
827
|
+
img_p = viewer.get_images(
|
|
828
|
+
i,
|
|
829
|
+
float(threshold_val),
|
|
830
|
+
zoom,
|
|
831
|
+
show_labels_val,
|
|
832
|
+
view_mode="preds",
|
|
833
|
+
use_per_class_thresholds=use_per_class_val,
|
|
834
|
+
per_class_thresholds=per_class_thresholds,
|
|
835
|
+
)
|
|
836
|
+
img_g = viewer.get_images(
|
|
837
|
+
i,
|
|
838
|
+
float(threshold_val),
|
|
839
|
+
zoom,
|
|
840
|
+
show_labels_val,
|
|
841
|
+
view_mode="gt",
|
|
842
|
+
use_per_class_thresholds=use_per_class_val,
|
|
843
|
+
per_class_thresholds=per_class_thresholds,
|
|
844
|
+
)
|
|
845
|
+
img_i = viewer.get_images(
|
|
846
|
+
i,
|
|
847
|
+
float(threshold_val),
|
|
848
|
+
zoom,
|
|
849
|
+
show_labels_val,
|
|
850
|
+
view_mode="image",
|
|
851
|
+
use_per_class_thresholds=use_per_class_val,
|
|
852
|
+
per_class_thresholds=per_class_thresholds,
|
|
853
|
+
)
|
|
854
|
+
stats_md = viewer.get_stats_markdown(i)
|
|
855
|
+
image_label = viewer.get_image_label_markdown(i)
|
|
856
|
+
return img_pg, img_p, img_g, img_i, stats_md, image_label
|
|
857
|
+
|
|
858
|
+
def _tabs_and_slider(
|
|
859
|
+
idx: int,
|
|
860
|
+
threshold_val: float,
|
|
861
|
+
zoom_str: str,
|
|
862
|
+
sort_mode: str,
|
|
863
|
+
show_labels_val: bool,
|
|
864
|
+
use_per_class_val: bool,
|
|
865
|
+
slider_idx: int,
|
|
866
|
+
*per_class_vals: Any,
|
|
867
|
+
):
|
|
868
|
+
"""Same as ``refresh_prediction_tabs`` but order matches Gradio: 4 images, slider, then markdown."""
|
|
869
|
+
img_pg, img_p, img_g, img_i, stats_md, image_label = refresh_prediction_tabs(
|
|
870
|
+
idx,
|
|
871
|
+
threshold_val,
|
|
872
|
+
zoom_str,
|
|
873
|
+
sort_mode,
|
|
874
|
+
show_labels_val,
|
|
875
|
+
use_per_class_val,
|
|
876
|
+
*per_class_vals,
|
|
877
|
+
)
|
|
878
|
+
idx = _slider_index(slider_idx, 0)
|
|
879
|
+
return img_pg, img_p, img_g, img_i, idx, idx, stats_md, image_label
|
|
880
|
+
|
|
881
|
+
def refresh_dataset_tabs(idx: int, zoom_str: str, sort_mode: str):
|
|
882
|
+
_maybe_sort(sort_mode, threshold_for_api)
|
|
883
|
+
zoom = float(zoom_str.replace("x", ""))
|
|
884
|
+
i = int(idx)
|
|
885
|
+
img_g = viewer.get_images(i, threshold_for_api, zoom, show_labels=False, view_mode="gt")
|
|
886
|
+
img_i = viewer.get_images(i, threshold_for_api, zoom, show_labels=False, view_mode="image")
|
|
887
|
+
image_label = viewer.get_image_label_markdown(i)
|
|
888
|
+
return img_g, img_i, image_label
|
|
889
|
+
|
|
890
|
+
def build_download_image_predictions(
|
|
891
|
+
idx: int,
|
|
892
|
+
threshold_val: float,
|
|
893
|
+
zoom_str: str,
|
|
894
|
+
sort_mode: str,
|
|
895
|
+
show_labels_val: bool,
|
|
896
|
+
use_per_class_val: bool,
|
|
897
|
+
active_view_mode: str,
|
|
898
|
+
*per_class_vals: Any,
|
|
899
|
+
) -> str:
|
|
900
|
+
per_class_thresholds = _build_per_class_thresholds(per_class_vals)
|
|
901
|
+
_maybe_sort(sort_mode, threshold_val, use_per_class_val, per_class_thresholds)
|
|
902
|
+
zoom = float(zoom_str.replace("x", ""))
|
|
903
|
+
i = int(idx)
|
|
904
|
+
view_mode = str(active_view_mode or "preds_gt")
|
|
905
|
+
if view_mode not in {"preds_gt", "preds", "gt", "image"}:
|
|
906
|
+
view_mode = "preds_gt"
|
|
907
|
+
rendered = viewer.get_images(
|
|
908
|
+
i,
|
|
909
|
+
float(threshold_val),
|
|
910
|
+
zoom,
|
|
911
|
+
show_labels_val,
|
|
912
|
+
view_mode=view_mode,
|
|
913
|
+
use_per_class_thresholds=use_per_class_val,
|
|
914
|
+
per_class_thresholds=per_class_thresholds,
|
|
915
|
+
)
|
|
916
|
+
with tempfile.NamedTemporaryFile(prefix="viewer_", suffix=".png", delete=False) as f:
|
|
917
|
+
out_path = f.name
|
|
918
|
+
rendered.save(out_path, format="PNG")
|
|
919
|
+
return out_path
|
|
920
|
+
|
|
921
|
+
def build_download_image_dataset(
|
|
922
|
+
idx: int,
|
|
923
|
+
zoom_str: str,
|
|
924
|
+
sort_mode: str,
|
|
925
|
+
active_view_mode: str,
|
|
926
|
+
) -> str:
|
|
927
|
+
_maybe_sort(sort_mode, threshold_for_api)
|
|
928
|
+
zoom = float(zoom_str.replace("x", ""))
|
|
929
|
+
i = int(idx)
|
|
930
|
+
view_mode = "gt" if str(active_view_mode) == "gt" else "image"
|
|
931
|
+
rendered = viewer.get_images(i, threshold_for_api, zoom, show_labels=False, view_mode=view_mode)
|
|
932
|
+
with tempfile.NamedTemporaryFile(prefix="viewer_", suffix=".png", delete=False) as f:
|
|
933
|
+
out_path = f.name
|
|
934
|
+
rendered.save(out_path, format="PNG")
|
|
935
|
+
return out_path
|
|
936
|
+
|
|
937
|
+
if viewer.has_predictions:
|
|
938
|
+
initial_pg, initial_p, initial_g, initial_i, initial_stats, initial_image_label = refresh_prediction_tabs(
|
|
939
|
+
0, viewer.conf_threshold, "2x", "no order", False, False
|
|
940
|
+
)
|
|
941
|
+
else:
|
|
942
|
+
initial_g, initial_i, initial_image_label = refresh_dataset_tabs(0, "2x", "no order")
|
|
943
|
+
initial_stats = ""
|
|
944
|
+
|
|
945
|
+
threshold_for_api = viewer.conf_threshold
|
|
946
|
+
|
|
947
|
+
app_title = "Dataset Explorer" if mode == "dataset" else "Object Detection Viewer"
|
|
948
|
+
with gr.Blocks(title=app_title) as app:
|
|
949
|
+
gr.Markdown("# Dataset Explorer" if mode == "dataset" else "# Object Detection Inference Viewer")
|
|
950
|
+
image_label_display = gr.Markdown(value=initial_image_label)
|
|
951
|
+
|
|
952
|
+
with gr.Tabs():
|
|
953
|
+
if viewer.has_predictions:
|
|
954
|
+
with gr.Tab(label="Predictions + Ground Truth"):
|
|
955
|
+
tab_preds_gt = gr.Image(value=initial_pg, label="Predictions (red) + ground truth (green)", height=800)
|
|
956
|
+
with gr.Tab(label="Predictions Only"):
|
|
957
|
+
tab_preds = gr.Image(value=initial_p, label="Predictions only", height=800)
|
|
958
|
+
with gr.Tab(label="Ground Truth Only"):
|
|
959
|
+
tab_gt = gr.Image(value=initial_g, label="Ground truth only", height=800)
|
|
960
|
+
with gr.Tab(label="Original Image"):
|
|
961
|
+
tab_image = gr.Image(value=initial_i, label="Original image", height=800)
|
|
962
|
+
else:
|
|
963
|
+
with gr.Tab(label="Ground Truth Only"):
|
|
964
|
+
tab_gt = gr.Image(value=initial_g, label="Ground truth only", height=800)
|
|
965
|
+
with gr.Tab(label="Original Image"):
|
|
966
|
+
tab_image = gr.Image(value=initial_i, label="Original image", height=800)
|
|
967
|
+
|
|
968
|
+
active_view_mode = gr.State("preds_gt" if viewer.has_predictions else "gt")
|
|
969
|
+
|
|
970
|
+
with gr.Row():
|
|
971
|
+
first_btn = gr.Button("First")
|
|
972
|
+
prev_btn = gr.Button("Previous")
|
|
973
|
+
next_btn = gr.Button("Next")
|
|
974
|
+
last_btn = gr.Button("Last")
|
|
975
|
+
|
|
976
|
+
with gr.Row():
|
|
977
|
+
image_idx = gr.Slider(
|
|
978
|
+
minimum=0,
|
|
979
|
+
maximum=max(0, len(viewer.results) - 1),
|
|
980
|
+
value=0,
|
|
981
|
+
step=1,
|
|
982
|
+
label="Image Index",
|
|
983
|
+
)
|
|
984
|
+
image_idx_state = gr.State(0)
|
|
985
|
+
zoom_scale = gr.Radio(
|
|
986
|
+
choices=["1x", "2x", "4x"],
|
|
987
|
+
value="2x",
|
|
988
|
+
label="Zoom Scale",
|
|
989
|
+
interactive=True,
|
|
990
|
+
)
|
|
991
|
+
if viewer.has_predictions:
|
|
992
|
+
conf_threshold = gr.Slider(
|
|
993
|
+
minimum=0.0,
|
|
994
|
+
maximum=1.0,
|
|
995
|
+
value=viewer.conf_threshold,
|
|
996
|
+
step=0.01,
|
|
997
|
+
label="Confidence Threshold",
|
|
998
|
+
)
|
|
999
|
+
with gr.Column():
|
|
1000
|
+
show_labels = gr.Checkbox(value=False, label="Show Labels")
|
|
1001
|
+
use_per_class_thresholds = gr.Checkbox(
|
|
1002
|
+
value=False,
|
|
1003
|
+
label="Use per-class thresholds (from metrics/config)",
|
|
1004
|
+
)
|
|
1005
|
+
per_class_sliders: List[gr.Slider] = []
|
|
1006
|
+
if viewer.has_predictions and viewer.class_names:
|
|
1007
|
+
with gr.Accordion("Per-class thresholds", open=False):
|
|
1008
|
+
gr.Markdown(
|
|
1009
|
+
"Defaults are loaded from per-class metrics when available "
|
|
1010
|
+
"(otherwise from config, then global threshold)."
|
|
1011
|
+
)
|
|
1012
|
+
for cname in viewer.class_names:
|
|
1013
|
+
default_thr = _to_float(
|
|
1014
|
+
viewer.per_class_threshold_defaults.get(cname, viewer.conf_threshold),
|
|
1015
|
+
viewer.conf_threshold,
|
|
1016
|
+
)
|
|
1017
|
+
per_class_sliders.append(
|
|
1018
|
+
gr.Slider(
|
|
1019
|
+
minimum=0.0,
|
|
1020
|
+
maximum=1.0,
|
|
1021
|
+
value=default_thr,
|
|
1022
|
+
step=0.01,
|
|
1023
|
+
label=f"{cname} threshold",
|
|
1024
|
+
)
|
|
1025
|
+
)
|
|
1026
|
+
with gr.Row():
|
|
1027
|
+
sort_choices = (
|
|
1028
|
+
["no order", "objs asc", "objs desc"]
|
|
1029
|
+
if mode == "dataset"
|
|
1030
|
+
else ["no order", "F1 asc", "F1 desc", "dets asc", "dets desc"]
|
|
1031
|
+
)
|
|
1032
|
+
sort_mode = gr.Radio(
|
|
1033
|
+
choices=sort_choices,
|
|
1034
|
+
value="no order",
|
|
1035
|
+
label="Sort by",
|
|
1036
|
+
interactive=True,
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
with gr.Row():
|
|
1040
|
+
download_btn = gr.Button("Download Current Image")
|
|
1041
|
+
download_file = gr.File(label="Download (PNG)", interactive=False)
|
|
1042
|
+
|
|
1043
|
+
if viewer.has_predictions:
|
|
1044
|
+
gr.Markdown("### Diagnostics")
|
|
1045
|
+
stats_display = gr.Markdown(value=initial_stats)
|
|
1046
|
+
tab_outputs_p = [tab_preds_gt, tab_preds, tab_gt, tab_image, stats_display, image_label_display]
|
|
1047
|
+
tab_outputs_p_nav = [
|
|
1048
|
+
tab_preds_gt, tab_preds, tab_gt, tab_image, image_idx, image_idx_state, stats_display, image_label_display
|
|
1049
|
+
]
|
|
1050
|
+
|
|
1051
|
+
def update_sorting_predictions(threshold_val, zoom_str, sort_mode_val, show_labels_val, use_pc_val, *pc_vals):
|
|
1052
|
+
return _tabs_and_slider(0, threshold_val, zoom_str, sort_mode_val, show_labels_val, use_pc_val, 0, *pc_vals)
|
|
1053
|
+
|
|
1054
|
+
def nav_first_p(t, z, s, sl, upc, *pc_vals):
|
|
1055
|
+
return _tabs_and_slider(0, t, z, s, sl, upc, 0, *pc_vals)
|
|
1056
|
+
|
|
1057
|
+
def nav_prev_p(cur, t, z, s, sl, upc, *pc_vals):
|
|
1058
|
+
new_idx = max(0, _slider_index(cur, 0) - 1)
|
|
1059
|
+
return _tabs_and_slider(new_idx, t, z, s, sl, upc, new_idx, *pc_vals)
|
|
1060
|
+
|
|
1061
|
+
def nav_next_p(cur, t, z, s, sl, upc, *pc_vals):
|
|
1062
|
+
last = len(viewer.results) - 1
|
|
1063
|
+
new_idx = min(last, _slider_index(cur, 0) + 1)
|
|
1064
|
+
return _tabs_and_slider(new_idx, t, z, s, sl, upc, new_idx, *pc_vals)
|
|
1065
|
+
|
|
1066
|
+
def nav_last_p(t, z, s, sl, upc, *pc_vals):
|
|
1067
|
+
last = len(viewer.results) - 1
|
|
1068
|
+
return _tabs_and_slider(last, t, z, s, sl, upc, last, *pc_vals)
|
|
1069
|
+
|
|
1070
|
+
def on_image_idx_change(slider_val, t, z, s, sl, upc, *pc_vals):
|
|
1071
|
+
idx = _slider_index(slider_val, 0)
|
|
1072
|
+
tabs = refresh_prediction_tabs(idx, t, z, s, sl, upc, *pc_vals)
|
|
1073
|
+
return tabs + (idx,)
|
|
1074
|
+
|
|
1075
|
+
prediction_inputs = [
|
|
1076
|
+
image_idx_state, conf_threshold, zoom_scale, sort_mode, show_labels, use_per_class_thresholds
|
|
1077
|
+
] + per_class_sliders
|
|
1078
|
+
prediction_inputs_no_idx = [
|
|
1079
|
+
conf_threshold, zoom_scale, sort_mode, show_labels, use_per_class_thresholds
|
|
1080
|
+
] + per_class_sliders
|
|
1081
|
+
|
|
1082
|
+
first_btn.click(
|
|
1083
|
+
fn=nav_first_p,
|
|
1084
|
+
inputs=prediction_inputs_no_idx,
|
|
1085
|
+
outputs=tab_outputs_p_nav,
|
|
1086
|
+
)
|
|
1087
|
+
prev_btn.click(
|
|
1088
|
+
fn=nav_prev_p,
|
|
1089
|
+
inputs=prediction_inputs,
|
|
1090
|
+
outputs=tab_outputs_p_nav,
|
|
1091
|
+
)
|
|
1092
|
+
next_btn.click(
|
|
1093
|
+
fn=nav_next_p,
|
|
1094
|
+
inputs=prediction_inputs,
|
|
1095
|
+
outputs=tab_outputs_p_nav,
|
|
1096
|
+
)
|
|
1097
|
+
last_btn.click(
|
|
1098
|
+
fn=nav_last_p,
|
|
1099
|
+
inputs=prediction_inputs_no_idx,
|
|
1100
|
+
outputs=tab_outputs_p_nav,
|
|
1101
|
+
)
|
|
1102
|
+
image_idx.change(
|
|
1103
|
+
fn=on_image_idx_change,
|
|
1104
|
+
inputs=[image_idx] + prediction_inputs_no_idx,
|
|
1105
|
+
outputs=tab_outputs_p + [image_idx_state],
|
|
1106
|
+
)
|
|
1107
|
+
conf_threshold.change(
|
|
1108
|
+
fn=refresh_prediction_tabs,
|
|
1109
|
+
inputs=prediction_inputs,
|
|
1110
|
+
outputs=tab_outputs_p,
|
|
1111
|
+
)
|
|
1112
|
+
zoom_scale.change(
|
|
1113
|
+
fn=refresh_prediction_tabs,
|
|
1114
|
+
inputs=prediction_inputs,
|
|
1115
|
+
outputs=tab_outputs_p,
|
|
1116
|
+
)
|
|
1117
|
+
show_labels.change(
|
|
1118
|
+
fn=refresh_prediction_tabs,
|
|
1119
|
+
inputs=prediction_inputs,
|
|
1120
|
+
outputs=tab_outputs_p,
|
|
1121
|
+
)
|
|
1122
|
+
use_per_class_thresholds.change(
|
|
1123
|
+
fn=refresh_prediction_tabs,
|
|
1124
|
+
inputs=prediction_inputs,
|
|
1125
|
+
outputs=tab_outputs_p,
|
|
1126
|
+
)
|
|
1127
|
+
sort_mode.change(
|
|
1128
|
+
fn=update_sorting_predictions,
|
|
1129
|
+
inputs=prediction_inputs_no_idx,
|
|
1130
|
+
outputs=tab_outputs_p_nav,
|
|
1131
|
+
)
|
|
1132
|
+
for pc_slider in per_class_sliders:
|
|
1133
|
+
pc_slider.change(
|
|
1134
|
+
fn=refresh_prediction_tabs,
|
|
1135
|
+
inputs=prediction_inputs,
|
|
1136
|
+
outputs=tab_outputs_p,
|
|
1137
|
+
)
|
|
1138
|
+
tab_preds_gt.select(fn=lambda: "preds_gt", outputs=active_view_mode)
|
|
1139
|
+
tab_preds.select(fn=lambda: "preds", outputs=active_view_mode)
|
|
1140
|
+
tab_gt.select(fn=lambda: "gt", outputs=active_view_mode)
|
|
1141
|
+
tab_image.select(fn=lambda: "image", outputs=active_view_mode)
|
|
1142
|
+
download_btn.click(
|
|
1143
|
+
fn=build_download_image_predictions,
|
|
1144
|
+
inputs=[image_idx_state, conf_threshold, zoom_scale, sort_mode, show_labels, use_per_class_thresholds, active_view_mode] + per_class_sliders,
|
|
1145
|
+
outputs=download_file,
|
|
1146
|
+
)
|
|
1147
|
+
else:
|
|
1148
|
+
tab_outputs_d = [tab_gt, tab_image, image_label_display]
|
|
1149
|
+
tab_outputs_d_nav = [tab_gt, tab_image, image_idx, image_idx_state, image_label_display]
|
|
1150
|
+
|
|
1151
|
+
def update_sorting_dataset(zoom_str, sort_mode_val):
|
|
1152
|
+
img_g, img_i, image_label = refresh_dataset_tabs(0, zoom_str, sort_mode_val)
|
|
1153
|
+
return img_g, img_i, 0, 0, image_label
|
|
1154
|
+
|
|
1155
|
+
def nav_first_d(z, s):
|
|
1156
|
+
img_g, img_i, image_label = refresh_dataset_tabs(0, z, s)
|
|
1157
|
+
return img_g, img_i, 0, 0, image_label
|
|
1158
|
+
|
|
1159
|
+
def nav_prev_d(cur, z, s):
|
|
1160
|
+
new_idx = max(0, _slider_index(cur, 0) - 1)
|
|
1161
|
+
img_g, img_i, image_label = refresh_dataset_tabs(new_idx, z, s)
|
|
1162
|
+
return img_g, img_i, new_idx, new_idx, image_label
|
|
1163
|
+
|
|
1164
|
+
def nav_next_d(cur, z, s):
|
|
1165
|
+
last = len(viewer.results) - 1
|
|
1166
|
+
new_idx = min(last, _slider_index(cur, 0) + 1)
|
|
1167
|
+
img_g, img_i, image_label = refresh_dataset_tabs(new_idx, z, s)
|
|
1168
|
+
return img_g, img_i, new_idx, new_idx, image_label
|
|
1169
|
+
|
|
1170
|
+
def nav_last_d(z, s):
|
|
1171
|
+
last = len(viewer.results) - 1
|
|
1172
|
+
img_g, img_i, image_label = refresh_dataset_tabs(last, z, s)
|
|
1173
|
+
return img_g, img_i, last, last, image_label
|
|
1174
|
+
|
|
1175
|
+
def on_image_idx_change_d(slider_val, z, s):
|
|
1176
|
+
idx = _slider_index(slider_val, 0)
|
|
1177
|
+
img_g, img_i, image_label = refresh_dataset_tabs(idx, z, s)
|
|
1178
|
+
return img_g, img_i, idx, image_label
|
|
1179
|
+
|
|
1180
|
+
first_btn.click(
|
|
1181
|
+
fn=nav_first_d,
|
|
1182
|
+
inputs=[zoom_scale, sort_mode],
|
|
1183
|
+
outputs=tab_outputs_d_nav,
|
|
1184
|
+
)
|
|
1185
|
+
prev_btn.click(
|
|
1186
|
+
fn=nav_prev_d,
|
|
1187
|
+
inputs=[image_idx_state, zoom_scale, sort_mode],
|
|
1188
|
+
outputs=tab_outputs_d_nav,
|
|
1189
|
+
)
|
|
1190
|
+
next_btn.click(
|
|
1191
|
+
fn=nav_next_d,
|
|
1192
|
+
inputs=[image_idx_state, zoom_scale, sort_mode],
|
|
1193
|
+
outputs=tab_outputs_d_nav,
|
|
1194
|
+
)
|
|
1195
|
+
last_btn.click(
|
|
1196
|
+
fn=nav_last_d,
|
|
1197
|
+
inputs=[zoom_scale, sort_mode],
|
|
1198
|
+
outputs=tab_outputs_d_nav,
|
|
1199
|
+
)
|
|
1200
|
+
image_idx.change(
|
|
1201
|
+
fn=on_image_idx_change_d,
|
|
1202
|
+
inputs=[image_idx, zoom_scale, sort_mode],
|
|
1203
|
+
outputs=tab_outputs_d + [image_idx_state],
|
|
1204
|
+
)
|
|
1205
|
+
zoom_scale.change(
|
|
1206
|
+
fn=refresh_dataset_tabs,
|
|
1207
|
+
inputs=[image_idx_state, zoom_scale, sort_mode],
|
|
1208
|
+
outputs=tab_outputs_d,
|
|
1209
|
+
)
|
|
1210
|
+
sort_mode.change(
|
|
1211
|
+
fn=update_sorting_dataset,
|
|
1212
|
+
inputs=[zoom_scale, sort_mode],
|
|
1213
|
+
outputs=tab_outputs_d_nav,
|
|
1214
|
+
)
|
|
1215
|
+
tab_gt.select(fn=lambda: "gt", outputs=active_view_mode)
|
|
1216
|
+
tab_image.select(fn=lambda: "image", outputs=active_view_mode)
|
|
1217
|
+
download_btn.click(
|
|
1218
|
+
fn=build_download_image_dataset,
|
|
1219
|
+
inputs=[image_idx_state, zoom_scale, sort_mode, active_view_mode],
|
|
1220
|
+
outputs=download_file,
|
|
1221
|
+
)
|
|
1222
|
+
return app
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def main():
|
|
1226
|
+
parser = argparse.ArgumentParser(
|
|
1227
|
+
description="Launch Gradio app: predictions viewer or dataset explorer."
|
|
1228
|
+
)
|
|
1229
|
+
parser.add_argument(
|
|
1230
|
+
"--mode",
|
|
1231
|
+
type=str,
|
|
1232
|
+
choices=["predictions", "dataset"],
|
|
1233
|
+
default="predictions",
|
|
1234
|
+
help="Mode: 'predictions' (browse inference results) or 'dataset' (explore dataset only)",
|
|
1235
|
+
)
|
|
1236
|
+
parser.add_argument(
|
|
1237
|
+
"--predictions-dir",
|
|
1238
|
+
type=str,
|
|
1239
|
+
default=None,
|
|
1240
|
+
help="Path to predictions directory (required when --mode predictions)",
|
|
1241
|
+
)
|
|
1242
|
+
parser.add_argument(
|
|
1243
|
+
"--data-root",
|
|
1244
|
+
type=str,
|
|
1245
|
+
default=None,
|
|
1246
|
+
help="Path to data root (required when --mode dataset; optional for predictions)",
|
|
1247
|
+
)
|
|
1248
|
+
parser.add_argument(
|
|
1249
|
+
"--tiles-dir",
|
|
1250
|
+
type=str,
|
|
1251
|
+
default=None,
|
|
1252
|
+
help="Subpath under data-root for dataset mode (e.g. train, train/tiles_1024)",
|
|
1253
|
+
)
|
|
1254
|
+
parser.add_argument(
|
|
1255
|
+
"--threshold",
|
|
1256
|
+
type=float,
|
|
1257
|
+
default=None,
|
|
1258
|
+
help="Confidence threshold for predictions mode (default: 0.3)",
|
|
1259
|
+
)
|
|
1260
|
+
parser.add_argument(
|
|
1261
|
+
"--port",
|
|
1262
|
+
type=int,
|
|
1263
|
+
default=7860,
|
|
1264
|
+
help="Port to run on",
|
|
1265
|
+
)
|
|
1266
|
+
args = parser.parse_args()
|
|
1267
|
+
|
|
1268
|
+
if args.mode == "predictions" and not args.predictions_dir:
|
|
1269
|
+
parser.error("--predictions-dir is required when --mode predictions")
|
|
1270
|
+
if args.mode == "dataset" and not args.data_root:
|
|
1271
|
+
parser.error("--data-root is required when --mode dataset")
|
|
1272
|
+
|
|
1273
|
+
app = create_app(
|
|
1274
|
+
mode=args.mode,
|
|
1275
|
+
predictions_dir=args.predictions_dir,
|
|
1276
|
+
data_root=args.data_root,
|
|
1277
|
+
tiles_dir=args.tiles_dir,
|
|
1278
|
+
threshold=args.threshold,
|
|
1279
|
+
)
|
|
1280
|
+
app.launch(server_port=args.port, server_name="0.0.0.0")
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
if __name__ == '__main__':
|
|
1284
|
+
main()
|