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,31 @@
|
|
|
1
|
+
"""Utility helpers for configuration management and visualization."""
|
|
2
|
+
|
|
3
|
+
from .config import FrozenConfig, load_config, merge_dicts, apply_overrides
|
|
4
|
+
from . import viz
|
|
5
|
+
from .device import get_device, is_gpu_device
|
|
6
|
+
from .logging import (
|
|
7
|
+
TraceLogger,
|
|
8
|
+
logger,
|
|
9
|
+
trace_function,
|
|
10
|
+
enable_tracing,
|
|
11
|
+
disable_tracing,
|
|
12
|
+
is_tracing_enabled,
|
|
13
|
+
)
|
|
14
|
+
from .progress import tqdm_progress_stream
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"FrozenConfig",
|
|
18
|
+
"load_config",
|
|
19
|
+
"merge_dicts",
|
|
20
|
+
"apply_overrides",
|
|
21
|
+
"get_device",
|
|
22
|
+
"is_gpu_device",
|
|
23
|
+
"viz",
|
|
24
|
+
"TraceLogger",
|
|
25
|
+
"logger",
|
|
26
|
+
"trace_function",
|
|
27
|
+
"enable_tracing",
|
|
28
|
+
"disable_tracing",
|
|
29
|
+
"is_tracing_enabled",
|
|
30
|
+
"tqdm_progress_stream",
|
|
31
|
+
]
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Lightweight configuration helpers with strong validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, MutableMapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Iterable, Iterator, MutableSequence, Sequence
|
|
9
|
+
import ast
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
try: # Optional dependency for YAML configs.
|
|
14
|
+
import yaml # type: ignore
|
|
15
|
+
except Exception: # pragma: no cover - exercised when PyYAML is absent.
|
|
16
|
+
yaml = None # type: ignore
|
|
17
|
+
|
|
18
|
+
# Keys starting with this prefix are ignored when loading configs.
|
|
19
|
+
# Use for keeping alternative values visible without affecting behavior, e.g.:
|
|
20
|
+
# "learning_rate": 0.01,
|
|
21
|
+
# "_muted_learning_rate": 0.005 # ignored
|
|
22
|
+
MUTED_KEY_PREFIX = "_muted_"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _ensure_mapping(value: Any) -> Dict[str, Any]:
|
|
26
|
+
if isinstance(value, Mapping):
|
|
27
|
+
return dict(value)
|
|
28
|
+
raise TypeError("Configuration root must be a mapping.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _deep_freeze(node: Any) -> Any:
|
|
32
|
+
if isinstance(node, Mapping):
|
|
33
|
+
return FrozenConfig({k: _deep_freeze(v) for k, v in node.items()})
|
|
34
|
+
if isinstance(node, list):
|
|
35
|
+
return tuple(_deep_freeze(v) for v in node)
|
|
36
|
+
return node
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _deep_clone(node: Any) -> Any:
|
|
40
|
+
if isinstance(node, Mapping):
|
|
41
|
+
return {k: _deep_clone(v) for k, v in node.items()}
|
|
42
|
+
if isinstance(node, list):
|
|
43
|
+
return [_deep_clone(v) for v in node]
|
|
44
|
+
return node
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _set_by_path(target: MutableMapping[str, Any], dotted_key: str, value: Any) -> None:
|
|
48
|
+
parts = dotted_key.split(".")
|
|
49
|
+
cursor: MutableMapping[str, Any] = target
|
|
50
|
+
for key in parts[:-1]:
|
|
51
|
+
if key not in cursor or not isinstance(cursor[key], MutableMapping):
|
|
52
|
+
cursor[key] = {}
|
|
53
|
+
cursor = cursor[key] # type: ignore[assignment]
|
|
54
|
+
cursor[parts[-1]] = value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _parse_scalar(value: str) -> Any:
|
|
58
|
+
try:
|
|
59
|
+
return ast.literal_eval(value)
|
|
60
|
+
except Exception:
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class FrozenConfig(Mapping[str, Any]):
|
|
66
|
+
"""Immutable configuration wrapper with dotted-key access."""
|
|
67
|
+
|
|
68
|
+
_data: Mapping[str, Any]
|
|
69
|
+
|
|
70
|
+
def __getitem__(self, key: str) -> Any:
|
|
71
|
+
return self._data[key]
|
|
72
|
+
|
|
73
|
+
def __iter__(self) -> Iterator[str]:
|
|
74
|
+
return iter(self._data)
|
|
75
|
+
|
|
76
|
+
def __len__(self) -> int:
|
|
77
|
+
return len(self._data)
|
|
78
|
+
|
|
79
|
+
def get(self, dotted_key: str, default: Any = None) -> Any:
|
|
80
|
+
cursor: Any = self
|
|
81
|
+
for part in dotted_key.split("."):
|
|
82
|
+
if isinstance(cursor, Mapping) and part in cursor:
|
|
83
|
+
cursor = cursor[part]
|
|
84
|
+
else:
|
|
85
|
+
return default
|
|
86
|
+
return cursor
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
89
|
+
def thaw(value: Any) -> Any:
|
|
90
|
+
if isinstance(value, FrozenConfig):
|
|
91
|
+
return {k: thaw(v) for k, v in value.items()}
|
|
92
|
+
if isinstance(value, tuple):
|
|
93
|
+
return [thaw(v) for v in value]
|
|
94
|
+
return value
|
|
95
|
+
|
|
96
|
+
return {k: thaw(v) for k, v in self.items()}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def merge_dicts(base: Mapping[str, Any], override: Mapping[str, Any]) -> Dict[str, Any]:
|
|
100
|
+
result = _deep_clone(base)
|
|
101
|
+
for key, value in override.items():
|
|
102
|
+
if key in result and isinstance(result[key], Mapping) and isinstance(value, Mapping):
|
|
103
|
+
result[key] = merge_dicts(result[key], value)
|
|
104
|
+
else:
|
|
105
|
+
result[key] = _deep_clone(value)
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def apply_overrides(cfg: Mapping[str, Any], overrides: Sequence[str] | Mapping[str, Any]) -> Dict[str, Any]:
|
|
110
|
+
result = _deep_clone(cfg)
|
|
111
|
+
if isinstance(overrides, Mapping):
|
|
112
|
+
for key, value in overrides.items():
|
|
113
|
+
_set_by_path(result, key, value)
|
|
114
|
+
else:
|
|
115
|
+
for override in overrides:
|
|
116
|
+
if "=" not in override:
|
|
117
|
+
raise ValueError(f"Override '{override}' must contain '='.")
|
|
118
|
+
key, value = override.split("=", 1)
|
|
119
|
+
_set_by_path(result, key, _parse_scalar(value))
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _load_from_path(path: Path) -> Dict[str, Any]:
|
|
124
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
125
|
+
if path.suffix in {".yaml", ".yml"}:
|
|
126
|
+
if yaml is None:
|
|
127
|
+
raise RuntimeError("PyYAML is required to load YAML configs.")
|
|
128
|
+
data = yaml.safe_load(handle)
|
|
129
|
+
else:
|
|
130
|
+
data = json.load(handle)
|
|
131
|
+
return _ensure_mapping(data)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# _base_ path prefix: paths are relative to oriented-det repository root (ORIENTED_DET_ROOT).
|
|
135
|
+
ODET_BASE_PREFIX = "@odet:"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _oriented_det_root() -> Path:
|
|
139
|
+
"""oriented-det repository root (``ORIENTED_DET_ROOT``)."""
|
|
140
|
+
raw = os.environ.get("ORIENTED_DET_ROOT")
|
|
141
|
+
if raw:
|
|
142
|
+
return Path(raw).expanduser().resolve()
|
|
143
|
+
try:
|
|
144
|
+
import oriented_det # noqa: WPS433 — runtime package location
|
|
145
|
+
except ImportError as exc:
|
|
146
|
+
raise FileNotFoundError(
|
|
147
|
+
"Cannot resolve @odet: base — set ORIENTED_DET_ROOT or install oriented_det."
|
|
148
|
+
) from exc
|
|
149
|
+
return Path(oriented_det.__file__).resolve().parent.parent
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _framework_config_roots() -> list[Path]:
|
|
153
|
+
"""Framework config directories to try for ``_base_`` fallbacks (in order).
|
|
154
|
+
|
|
155
|
+
Resolution order:
|
|
156
|
+
- ``ORIENTED_DET_CONFIG_ROOT`` (explicit override, sole entry when set)
|
|
157
|
+
- Editable checkout: ``<repo>/configs`` (full tree; preferred over vendored subset)
|
|
158
|
+
- Vendored wheel subset: ``oriented_det/configs``
|
|
159
|
+
"""
|
|
160
|
+
roots: list[Path] = []
|
|
161
|
+
seen: set[Path] = set()
|
|
162
|
+
|
|
163
|
+
def _add(root: Path | None) -> None:
|
|
164
|
+
if root is None:
|
|
165
|
+
return
|
|
166
|
+
resolved = root.expanduser().resolve()
|
|
167
|
+
if resolved.is_dir() and resolved not in seen:
|
|
168
|
+
seen.add(resolved)
|
|
169
|
+
roots.append(resolved)
|
|
170
|
+
|
|
171
|
+
env = os.environ.get("ORIENTED_DET_CONFIG_ROOT")
|
|
172
|
+
if env:
|
|
173
|
+
_add(Path(env))
|
|
174
|
+
return roots
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
import oriented_det
|
|
178
|
+
|
|
179
|
+
pkg = Path(oriented_det.__file__).resolve().parent
|
|
180
|
+
repo_configs = pkg.parent / "configs"
|
|
181
|
+
vendored = pkg / "configs"
|
|
182
|
+
# Repo tree is authoritative in editable installs; vendored may omit schedules like 6x.
|
|
183
|
+
if repo_configs.is_dir() and repo_configs != vendored:
|
|
184
|
+
_add(repo_configs)
|
|
185
|
+
_add(vendored)
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
return roots
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _framework_config_root() -> Path | None:
|
|
192
|
+
"""Primary framework config tree (first entry from :func:`_framework_config_roots`)."""
|
|
193
|
+
roots = _framework_config_roots()
|
|
194
|
+
return roots[0] if roots else None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _normalize_base_ref_for_framework(base_path: str) -> str:
|
|
198
|
+
"""Strip leading ``../`` so ``../_base_/models/x.json`` -> ``_base_/models/x.json``."""
|
|
199
|
+
ref = base_path.replace("\\", "/").lstrip("/")
|
|
200
|
+
while ref.startswith("../"):
|
|
201
|
+
ref = ref[3:]
|
|
202
|
+
if ref.startswith("./"):
|
|
203
|
+
ref = ref[2:]
|
|
204
|
+
return ref
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _resolve_base_path(base_path: str, current_file: Path) -> Path:
|
|
208
|
+
"""Resolve a _base_ reference: @odet:, absolute, file-relative, or framework fallback.
|
|
209
|
+
|
|
210
|
+
External projects can keep dataset bases locally and inherit
|
|
211
|
+
model/schedule bases from the framework ``configs/_base_/`` tree when absent.
|
|
212
|
+
"""
|
|
213
|
+
if base_path.startswith(ODET_BASE_PREFIX):
|
|
214
|
+
rel = base_path[len(ODET_BASE_PREFIX) :].lstrip("/")
|
|
215
|
+
return (_oriented_det_root() / rel).resolve()
|
|
216
|
+
|
|
217
|
+
base_path_obj = Path(base_path)
|
|
218
|
+
if base_path_obj.is_absolute():
|
|
219
|
+
if not base_path_obj.exists():
|
|
220
|
+
raise FileNotFoundError(f"Config file not found: {base_path_obj}")
|
|
221
|
+
return base_path_obj
|
|
222
|
+
|
|
223
|
+
local = (current_file.parent / base_path_obj).resolve()
|
|
224
|
+
if local.exists():
|
|
225
|
+
return local
|
|
226
|
+
|
|
227
|
+
rel = _normalize_base_ref_for_framework(base_path)
|
|
228
|
+
checked: list[Path] = []
|
|
229
|
+
for fw_root in _framework_config_roots():
|
|
230
|
+
fallback = (fw_root / rel).resolve()
|
|
231
|
+
checked.append(fallback)
|
|
232
|
+
if fallback.exists():
|
|
233
|
+
return fallback
|
|
234
|
+
|
|
235
|
+
raise FileNotFoundError(
|
|
236
|
+
f"Config file not found: {local}"
|
|
237
|
+
+ (f" (also checked framework: {', '.join(str(p) for p in checked)})" if checked else "")
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _strip_muted_keys(cfg: Dict[str, Any], prefix: str = MUTED_KEY_PREFIX) -> None:
|
|
242
|
+
"""Remove keys starting with prefix recursively. Modifies cfg in place."""
|
|
243
|
+
keys_to_remove = [k for k in cfg if isinstance(k, str) and k.startswith(prefix)]
|
|
244
|
+
for k in keys_to_remove:
|
|
245
|
+
del cfg[k]
|
|
246
|
+
for v in cfg.values():
|
|
247
|
+
if isinstance(v, dict):
|
|
248
|
+
_strip_muted_keys(v, prefix)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _delete_keys(cfg: Dict[str, Any], keys_to_delete: Dict[str, Any]) -> None:
|
|
252
|
+
"""Delete keys specified in keys_to_delete from cfg.
|
|
253
|
+
|
|
254
|
+
MMRotate-style deletion: {"key": {"_delete_": True}} removes "key" from parent.
|
|
255
|
+
"""
|
|
256
|
+
for key, value in keys_to_delete.items():
|
|
257
|
+
if isinstance(value, dict) and value.get("_delete_") is True:
|
|
258
|
+
if key in cfg:
|
|
259
|
+
del cfg[key]
|
|
260
|
+
elif isinstance(value, dict) and isinstance(cfg.get(key), dict):
|
|
261
|
+
_delete_keys(cfg[key], value)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _load_config_with_base(
|
|
265
|
+
path: Path,
|
|
266
|
+
*,
|
|
267
|
+
visited: set[Path] | None = None,
|
|
268
|
+
) -> Dict[str, Any]:
|
|
269
|
+
"""Load config file and recursively resolve _base_ inheritance.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
path: Path to config file
|
|
273
|
+
visited: Set of already visited paths to detect circular dependencies
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Merged configuration dictionary with _base_ resolved
|
|
277
|
+
"""
|
|
278
|
+
if visited is None:
|
|
279
|
+
visited = set()
|
|
280
|
+
|
|
281
|
+
path = path.resolve()
|
|
282
|
+
|
|
283
|
+
# Detect circular dependencies
|
|
284
|
+
if path in visited:
|
|
285
|
+
raise ValueError(f"Circular dependency detected: {path} is already being loaded")
|
|
286
|
+
visited.add(path)
|
|
287
|
+
|
|
288
|
+
if not path.exists():
|
|
289
|
+
raise FileNotFoundError(f"Config file not found: {path}")
|
|
290
|
+
|
|
291
|
+
cfg_dict = _load_from_path(path)
|
|
292
|
+
|
|
293
|
+
# Handle _base_ inheritance
|
|
294
|
+
if "_base_" in cfg_dict:
|
|
295
|
+
base_refs = cfg_dict.pop("_base_")
|
|
296
|
+
|
|
297
|
+
# Normalize to list
|
|
298
|
+
if isinstance(base_refs, str):
|
|
299
|
+
base_refs = [base_refs]
|
|
300
|
+
elif not isinstance(base_refs, list):
|
|
301
|
+
raise TypeError(f"_base_ must be a string or list of strings, got {type(base_refs)}")
|
|
302
|
+
|
|
303
|
+
# Load and merge base configs in order
|
|
304
|
+
merged = {}
|
|
305
|
+
for base_ref in base_refs:
|
|
306
|
+
if not isinstance(base_ref, str):
|
|
307
|
+
raise TypeError(f"Base reference must be a string, got {type(base_ref)}")
|
|
308
|
+
base_path = _resolve_base_path(base_ref, path)
|
|
309
|
+
base_cfg = _load_config_with_base(base_path, visited=visited)
|
|
310
|
+
merged = merge_dicts(merged, base_cfg)
|
|
311
|
+
|
|
312
|
+
# Merge current config over base configs
|
|
313
|
+
merged = merge_dicts(merged, cfg_dict)
|
|
314
|
+
cfg_dict = merged
|
|
315
|
+
|
|
316
|
+
visited.remove(path)
|
|
317
|
+
return cfg_dict
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def load_config(
|
|
321
|
+
source: str | os.PathLike[str] | Mapping[str, Any],
|
|
322
|
+
*,
|
|
323
|
+
overrides: Sequence[str] | Mapping[str, Any] | None = None,
|
|
324
|
+
) -> FrozenConfig:
|
|
325
|
+
"""Load a configuration file or dict and apply optional overrides.
|
|
326
|
+
|
|
327
|
+
Supports MMRotate-style nested config inheritance via _base_ field.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
source: Path to config file or dict
|
|
331
|
+
overrides: Optional overrides to apply after loading
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
FrozenConfig instance with merged configuration
|
|
335
|
+
|
|
336
|
+
Examples:
|
|
337
|
+
>>> # Simple config
|
|
338
|
+
>>> cfg = load_config("config.json")
|
|
339
|
+
|
|
340
|
+
>>> # Config with base inheritance
|
|
341
|
+
>>> # config.json: {"_base_": "base.json", "lr": 0.01}
|
|
342
|
+
>>> cfg = load_config("config.json")
|
|
343
|
+
|
|
344
|
+
>>> # With overrides
|
|
345
|
+
>>> cfg = load_config("config.json", overrides=["training.lr=0.001"])
|
|
346
|
+
"""
|
|
347
|
+
if isinstance(source, Mapping):
|
|
348
|
+
cfg_dict = _deep_clone(_ensure_mapping(source))
|
|
349
|
+
_strip_muted_keys(cfg_dict)
|
|
350
|
+
else:
|
|
351
|
+
path = Path(source)
|
|
352
|
+
if not path.exists():
|
|
353
|
+
raise FileNotFoundError(path)
|
|
354
|
+
# Load with base inheritance support
|
|
355
|
+
cfg_dict = _load_config_with_base(path)
|
|
356
|
+
|
|
357
|
+
# Strip _muted_* keys (keeps alternative values visible without affecting behavior)
|
|
358
|
+
_strip_muted_keys(cfg_dict)
|
|
359
|
+
|
|
360
|
+
# Handle deletion syntax (must be done before overrides)
|
|
361
|
+
_delete_keys(cfg_dict, cfg_dict)
|
|
362
|
+
|
|
363
|
+
if overrides:
|
|
364
|
+
cfg_dict = apply_overrides(cfg_dict, overrides)
|
|
365
|
+
|
|
366
|
+
return _deep_freeze(cfg_dict)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
__all__ = [
|
|
370
|
+
"ODET_BASE_PREFIX",
|
|
371
|
+
"FrozenConfig",
|
|
372
|
+
"MUTED_KEY_PREFIX",
|
|
373
|
+
"load_config",
|
|
374
|
+
"merge_dicts",
|
|
375
|
+
"apply_overrides",
|
|
376
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Device selection helpers for CPU, CUDA, and MPS (Apple Silicon)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import torch
|
|
7
|
+
except ImportError:
|
|
8
|
+
torch = None # type: ignore
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_device(prefer_cuda: bool = True) -> "torch.device":
|
|
12
|
+
"""Return the best available device: CUDA > MPS (Apple Silicon) > CPU.
|
|
13
|
+
|
|
14
|
+
Use this when you want training or inference to use GPU when possible,
|
|
15
|
+
including on macOS with M1/M2/M3 (MPS).
|
|
16
|
+
"""
|
|
17
|
+
if torch is None:
|
|
18
|
+
raise RuntimeError("PyTorch is required for device selection.")
|
|
19
|
+
if prefer_cuda and torch.cuda.is_available():
|
|
20
|
+
return torch.device("cuda")
|
|
21
|
+
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
|
|
22
|
+
return torch.device("mps")
|
|
23
|
+
return torch.device("cpu")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_gpu_device(device: "torch.device") -> bool:
|
|
27
|
+
"""Return True if the device is a GPU (CUDA or MPS)."""
|
|
28
|
+
if device is None:
|
|
29
|
+
return False
|
|
30
|
+
device_type = device.type if hasattr(device, "type") else str(device).split(":")[0]
|
|
31
|
+
if device_type == "cuda":
|
|
32
|
+
return torch is not None and torch.cuda.is_available()
|
|
33
|
+
if device_type == "mps":
|
|
34
|
+
return torch is not None and getattr(torch.backends, "mps", None) is not None and torch.backends.mps.is_available()
|
|
35
|
+
return False
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Logging and tracing utilities for oriented-det framework.
|
|
2
|
+
|
|
3
|
+
This module provides configurable logging for debugging and performance analysis.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from typing import Optional, Callable, Any
|
|
11
|
+
import functools
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TraceLogger:
|
|
15
|
+
"""Configurable logger for tracing function execution."""
|
|
16
|
+
|
|
17
|
+
_enabled: bool = False
|
|
18
|
+
_indent_level: int = 0
|
|
19
|
+
_indent_str: str = " "
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def enable(cls) -> None:
|
|
23
|
+
"""Enable tracing/logging."""
|
|
24
|
+
cls._enabled = True
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def disable(cls) -> None:
|
|
28
|
+
"""Disable tracing/logging."""
|
|
29
|
+
cls._enabled = False
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def is_enabled(cls) -> bool:
|
|
33
|
+
"""Check if tracing is enabled."""
|
|
34
|
+
return cls._enabled
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def trace(cls, message: str, *args, **kwargs) -> None:
|
|
38
|
+
"""Print a trace message if logging is enabled."""
|
|
39
|
+
if cls._enabled:
|
|
40
|
+
indent = cls._indent_str * cls._indent_level
|
|
41
|
+
formatted_msg = message.format(*args, **kwargs) if args or kwargs else message
|
|
42
|
+
print(f"{indent}{formatted_msg}")
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
@contextmanager
|
|
46
|
+
def trace_block(cls, message: str, *args, **kwargs):
|
|
47
|
+
"""Context manager for tracing a code block with timing."""
|
|
48
|
+
if cls._enabled:
|
|
49
|
+
formatted_msg = message.format(*args, **kwargs) if args or kwargs else message
|
|
50
|
+
cls.trace(f"→ {formatted_msg}")
|
|
51
|
+
cls._indent_level += 1
|
|
52
|
+
start_time = time.time()
|
|
53
|
+
try:
|
|
54
|
+
yield
|
|
55
|
+
finally:
|
|
56
|
+
elapsed = time.time() - start_time
|
|
57
|
+
cls._indent_level -= 1
|
|
58
|
+
cls.trace(f"✅ {formatted_msg} ({elapsed:.3f}s)")
|
|
59
|
+
else:
|
|
60
|
+
yield
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def trace_timing(cls, message: str, elapsed: float, *args, **kwargs) -> None:
|
|
64
|
+
"""Trace a message with timing information."""
|
|
65
|
+
if cls._enabled:
|
|
66
|
+
formatted_msg = message.format(*args, **kwargs) if args or kwargs else message
|
|
67
|
+
cls.trace(f"{formatted_msg} ({elapsed:.3f}s)")
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def trace_value(cls, name: str, value: Any, unit: Optional[str] = None) -> None:
|
|
71
|
+
"""Trace a value with optional unit."""
|
|
72
|
+
if cls._enabled:
|
|
73
|
+
if unit:
|
|
74
|
+
cls.trace(f"{name}: {value} {unit}")
|
|
75
|
+
else:
|
|
76
|
+
cls.trace(f"{name}: {value}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Global instance
|
|
80
|
+
logger = TraceLogger()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def trace_function(func_name: Optional[str] = None):
|
|
84
|
+
"""Decorator to trace function execution.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
func_name: Optional custom name for the function in logs.
|
|
88
|
+
If None, uses the actual function name.
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
@trace_function()
|
|
92
|
+
def my_function(x, y):
|
|
93
|
+
return x + y
|
|
94
|
+
|
|
95
|
+
@trace_function("custom_name")
|
|
96
|
+
def another_function():
|
|
97
|
+
pass
|
|
98
|
+
"""
|
|
99
|
+
def decorator(func: Callable) -> Callable:
|
|
100
|
+
name = func_name or func.__name__
|
|
101
|
+
|
|
102
|
+
@functools.wraps(func)
|
|
103
|
+
def wrapper(*args, **kwargs):
|
|
104
|
+
if logger.is_enabled():
|
|
105
|
+
# Format arguments for display
|
|
106
|
+
args_str = ", ".join([str(arg)[:50] for arg in args[:3]])
|
|
107
|
+
if len(args) > 3:
|
|
108
|
+
args_str += f", ... (+{len(args)-3} more)"
|
|
109
|
+
kwargs_str = ", ".join([f"{k}={str(v)[:30]}" for k, v in list(kwargs.items())[:2]])
|
|
110
|
+
if len(kwargs) > 2:
|
|
111
|
+
kwargs_str += f", ... (+{len(kwargs)-2} more)"
|
|
112
|
+
|
|
113
|
+
call_str = f"{name}({args_str}"
|
|
114
|
+
if kwargs_str:
|
|
115
|
+
call_str += f", {kwargs_str}"
|
|
116
|
+
call_str += ")"
|
|
117
|
+
|
|
118
|
+
logger.trace(f"→ {call_str}")
|
|
119
|
+
logger._indent_level += 1
|
|
120
|
+
start_time = time.time()
|
|
121
|
+
try:
|
|
122
|
+
result = func(*args, **kwargs)
|
|
123
|
+
elapsed = time.time() - start_time
|
|
124
|
+
logger._indent_level -= 1
|
|
125
|
+
logger.trace(f"✅ {name} returned ({elapsed:.3f}s)")
|
|
126
|
+
return result
|
|
127
|
+
except Exception as e:
|
|
128
|
+
elapsed = time.time() - start_time
|
|
129
|
+
logger._indent_level -= 1
|
|
130
|
+
logger.trace(f"❌ {name} raised {type(e).__name__}: {e} ({elapsed:.3f}s)")
|
|
131
|
+
raise
|
|
132
|
+
else:
|
|
133
|
+
return func(*args, **kwargs)
|
|
134
|
+
|
|
135
|
+
return wrapper
|
|
136
|
+
return decorator
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Convenience functions
|
|
140
|
+
def enable_tracing() -> None:
|
|
141
|
+
"""Enable tracing globally."""
|
|
142
|
+
logger.enable()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def disable_tracing() -> None:
|
|
146
|
+
"""Disable tracing globally."""
|
|
147
|
+
logger.disable()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def is_tracing_enabled() -> bool:
|
|
151
|
+
"""Check if tracing is enabled."""
|
|
152
|
+
return logger.is_enabled()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
__all__ = [
|
|
156
|
+
"TraceLogger",
|
|
157
|
+
"logger",
|
|
158
|
+
"trace_function",
|
|
159
|
+
"enable_tracing",
|
|
160
|
+
"disable_tracing",
|
|
161
|
+
"is_tracing_enabled",
|
|
162
|
+
]
|
|
163
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Progress bar streams: show tqdm on the real console when stderr is piped (e.g. ``2>&1 | tee log``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any, Optional, TextIO
|
|
9
|
+
|
|
10
|
+
# Lazily opened /dev/tty (or Windows CON) for tqdm; closed at exit.
|
|
11
|
+
_tqdm_tty: Optional[TextIO] = None
|
|
12
|
+
_tqdm_tty_open_failed: bool = False
|
|
13
|
+
_devnull: Optional[TextIO] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _close_tqdm_tty() -> None:
|
|
17
|
+
global _tqdm_tty
|
|
18
|
+
if _tqdm_tty is not None:
|
|
19
|
+
try:
|
|
20
|
+
_tqdm_tty.close()
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
_tqdm_tty = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_devnull() -> TextIO:
|
|
27
|
+
global _devnull
|
|
28
|
+
if _devnull is None:
|
|
29
|
+
_devnull = open(os.devnull, "w")
|
|
30
|
+
return _devnull
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def tqdm_progress_stream() -> Any:
|
|
34
|
+
"""File object for ``tqdm(..., file=...)``, aligned with :func:`tools.train` / ``progress_stream`` on the training loop.
|
|
35
|
+
|
|
36
|
+
* If ``sys.stderr`` is a TTY, use it (normal interactive run).
|
|
37
|
+
* If not (shell redirect / ``| tee``), try ``/dev/tty`` (POSIX) or ``CON`` (Windows) so the bar
|
|
38
|
+
still updates on the user’s terminal while other output is captured in a file.
|
|
39
|
+
* If none of that works, use ``os.devnull`` so logs are not flooded with control characters.
|
|
40
|
+
"""
|
|
41
|
+
global _tqdm_tty, _tqdm_tty_open_failed
|
|
42
|
+
if sys.stderr.isatty():
|
|
43
|
+
return sys.stderr
|
|
44
|
+
if _tqdm_tty is not None:
|
|
45
|
+
return _tqdm_tty
|
|
46
|
+
if not _tqdm_tty_open_failed:
|
|
47
|
+
if os.name == "posix":
|
|
48
|
+
try:
|
|
49
|
+
# Still works when e.g. ``python ... 2>&1 | tee x.log`` (stderr is a pipe, not a TTY).
|
|
50
|
+
_tqdm_tty = open("/dev/tty", "w", encoding="utf-8", buffering=1)
|
|
51
|
+
atexit.register(_close_tqdm_tty)
|
|
52
|
+
return _tqdm_tty
|
|
53
|
+
except OSError:
|
|
54
|
+
_tqdm_tty_open_failed = True
|
|
55
|
+
elif os.name == "nt":
|
|
56
|
+
try:
|
|
57
|
+
_tqdm_tty = open("CON", "w", encoding="utf-8", errors="replace", buffering=1)
|
|
58
|
+
atexit.register(_close_tqdm_tty)
|
|
59
|
+
return _tqdm_tty
|
|
60
|
+
except OSError:
|
|
61
|
+
_tqdm_tty_open_failed = True
|
|
62
|
+
return _get_devnull()
|