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,510 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Preview training augmentations from an experiment config.
|
|
3
|
+
|
|
4
|
+
Loads the train split, applies the same collate path as training (Albumentations,
|
|
5
|
+
random flips, resize), and writes comparison grids so you can sanity-check
|
|
6
|
+
augmentation.json / recipe overrides before a long run.
|
|
7
|
+
|
|
8
|
+
Usage (from odet-planes/, with oriented-det venv active):
|
|
9
|
+
|
|
10
|
+
python tools/preview_augmentation.py --config configs/rotated_faster_rcnn/airbus_planes_dota081_ft.json
|
|
11
|
+
python tools/preview_augmentation.py --config runs/rotated_faster_rcnn/20260603-090228/config.json --num-images 4 --variants 6
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import random
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
|
|
23
|
+
import torch
|
|
24
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
25
|
+
|
|
26
|
+
from dataclasses import fields
|
|
27
|
+
|
|
28
|
+
from oriented_det.data import AirbusPlaygroundCSVDataset, build_dota_split_dataset, dota_dataset_class_names
|
|
29
|
+
from oriented_det.geometry.rbox import RBox
|
|
30
|
+
from oriented_det.runtime.collate import create_collate_fn, create_train_augmentation
|
|
31
|
+
from oriented_det.train.config import (
|
|
32
|
+
AugmentationConfig,
|
|
33
|
+
CheckpointConfig,
|
|
34
|
+
DataLoaderConfig,
|
|
35
|
+
DatasetConfig,
|
|
36
|
+
EvaluationConfig,
|
|
37
|
+
LossConfig,
|
|
38
|
+
ModelConfig,
|
|
39
|
+
PreprocessingConfig,
|
|
40
|
+
ProductionConfig,
|
|
41
|
+
TensorboardConfig,
|
|
42
|
+
TrainingConfig,
|
|
43
|
+
TrainingExperimentConfig,
|
|
44
|
+
_normalize_legacy_loss_type,
|
|
45
|
+
)
|
|
46
|
+
from oriented_det.utils import viz
|
|
47
|
+
from oriented_det.utils.config import load_config
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _repo_root() -> Path:
|
|
51
|
+
return Path(__file__).resolve().parent.parent
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_SECTION_CLASSES = {
|
|
55
|
+
"dataset": DatasetConfig,
|
|
56
|
+
"augmentation": AugmentationConfig,
|
|
57
|
+
"data_loader": DataLoaderConfig,
|
|
58
|
+
"model": ModelConfig,
|
|
59
|
+
"training": TrainingConfig,
|
|
60
|
+
"evaluation": EvaluationConfig,
|
|
61
|
+
"production": ProductionConfig,
|
|
62
|
+
"checkpoint": CheckpointConfig,
|
|
63
|
+
"loss": LossConfig,
|
|
64
|
+
"tensorboard": TensorboardConfig,
|
|
65
|
+
"preprocessing": PreprocessingConfig,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _filter_section(section: dict, dc_cls: type) -> dict:
|
|
70
|
+
valid = {f.name for f in fields(dc_cls)}
|
|
71
|
+
return {k: v for k, v in section.items() if k in valid}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_training_config(path: Path) -> TrainingExperimentConfig:
|
|
75
|
+
"""Load config; ignore unknown keys in sections (preview-only lenient mode)."""
|
|
76
|
+
try:
|
|
77
|
+
return TrainingExperimentConfig.load(path)
|
|
78
|
+
except ValueError as exc:
|
|
79
|
+
if "Unknown key" not in str(exc):
|
|
80
|
+
raise
|
|
81
|
+
|
|
82
|
+
frozen_cfg = load_config(path)
|
|
83
|
+
config_dict = frozen_cfg.to_dict()
|
|
84
|
+
_normalize_legacy_loss_type(config_dict)
|
|
85
|
+
|
|
86
|
+
for key in list(config_dict):
|
|
87
|
+
if config_dict[key] is None and key in _SECTION_CLASSES:
|
|
88
|
+
del config_dict[key]
|
|
89
|
+
|
|
90
|
+
root_valid = {f.name for f in fields(TrainingExperimentConfig)}
|
|
91
|
+
root = {k: v for k, v in config_dict.items() if k in root_valid}
|
|
92
|
+
|
|
93
|
+
for section, dc_cls in _SECTION_CLASSES.items():
|
|
94
|
+
if section not in config_dict or config_dict[section] is None:
|
|
95
|
+
continue
|
|
96
|
+
root[section] = dc_cls(**_filter_section(config_dict[section], dc_cls))
|
|
97
|
+
|
|
98
|
+
return TrainingExperimentConfig(**root)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def resolve_class_map(
|
|
102
|
+
config: TrainingExperimentConfig, dataset
|
|
103
|
+
) -> tuple[dict[str, int], list[str]]:
|
|
104
|
+
if config.class_map:
|
|
105
|
+
names = list(config.class_names or [])
|
|
106
|
+
return dict(config.class_map), names
|
|
107
|
+
if hasattr(dataset, "get_class_names"):
|
|
108
|
+
names = dataset.get_class_names()
|
|
109
|
+
else:
|
|
110
|
+
names = dota_dataset_class_names(dataset)
|
|
111
|
+
class_map = {name: i + 1 for i, name in enumerate(names)}
|
|
112
|
+
return class_map, names
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def resolve_config_path(config_arg: str) -> Path:
|
|
116
|
+
raw = Path(config_arg)
|
|
117
|
+
if raw.is_absolute() and raw.exists():
|
|
118
|
+
return raw
|
|
119
|
+
for root in (_repo_root(), Path.cwd()):
|
|
120
|
+
candidate = root / raw
|
|
121
|
+
if candidate.exists():
|
|
122
|
+
return candidate.resolve()
|
|
123
|
+
raise FileNotFoundError(f"Config not found: {config_arg}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_dataset_from_config(config: TrainingExperimentConfig, split: str):
|
|
127
|
+
"""Build train or val dataset (same logic as tools/train.py / dataset_stats.py)."""
|
|
128
|
+
dataset_format = getattr(config.dataset, "format", "dota").lower()
|
|
129
|
+
filter_empty = getattr(config.dataset, "filter_empty_gt", False) if split == "train" else False
|
|
130
|
+
|
|
131
|
+
if dataset_format == "airbus_playground":
|
|
132
|
+
if config.dataset.annotations_file is None or config.dataset.split_file is None:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
"Airbus Playground requires dataset.annotations_file and dataset.split_file."
|
|
135
|
+
)
|
|
136
|
+
return AirbusPlaygroundCSVDataset(
|
|
137
|
+
data_root=config.dataset.data_root,
|
|
138
|
+
split=split,
|
|
139
|
+
annotations_file=config.dataset.annotations_file,
|
|
140
|
+
split_file=config.dataset.split_file,
|
|
141
|
+
val_split_id=config.dataset.val_split_id,
|
|
142
|
+
difficult_strategy=config.dataset.difficult_strategy,
|
|
143
|
+
allowed_classes=config.dataset.allowed_classes,
|
|
144
|
+
ignore_labels=config.dataset.ignore_labels,
|
|
145
|
+
map_labels=config.dataset.map_labels,
|
|
146
|
+
filter_empty_gt=filter_empty,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if not config.dataset.has_dota_tiles_config():
|
|
150
|
+
raise ValueError(
|
|
151
|
+
"DOTA format requires dataset.train_tiles_dir(s) and dataset.val_tiles_dir(s)."
|
|
152
|
+
)
|
|
153
|
+
tile_roots = (
|
|
154
|
+
config.dataset.get_train_tile_roots()
|
|
155
|
+
if split == "train"
|
|
156
|
+
else config.dataset.get_val_tile_roots()
|
|
157
|
+
)
|
|
158
|
+
same_folder = getattr(config.dataset, "same_folder", False)
|
|
159
|
+
return build_dota_split_dataset(
|
|
160
|
+
tile_roots,
|
|
161
|
+
split=split,
|
|
162
|
+
same_folder=same_folder,
|
|
163
|
+
difficult_strategy=config.dataset.difficult_strategy,
|
|
164
|
+
allowed_classes=config.dataset.allowed_classes,
|
|
165
|
+
ignore_labels=config.dataset.ignore_labels,
|
|
166
|
+
filter_empty_gt=filter_empty,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_resize_and_flips(config: TrainingExperimentConfig) -> tuple[str, tuple[int, int], bool, bool, bool, int]:
|
|
171
|
+
from oriented_det.data.preprocessing import parse_canvas_size
|
|
172
|
+
|
|
173
|
+
prep = getattr(config, "preprocessing", None)
|
|
174
|
+
if prep is not None:
|
|
175
|
+
resize_mode = getattr(prep, "resize_mode", "fixed")
|
|
176
|
+
ts = getattr(prep, "target_size", [1024, 1024])
|
|
177
|
+
resize_to = parse_canvas_size(resize_mode, ts)
|
|
178
|
+
else:
|
|
179
|
+
resize_mode = "fixed"
|
|
180
|
+
resize_to = (1024, 1024)
|
|
181
|
+
flip_h = getattr(prep, "enable_flip_horizontal", True) if prep is not None else True
|
|
182
|
+
flip_v = getattr(prep, "enable_flip_vertical", True) if prep is not None else True
|
|
183
|
+
flip_d = getattr(prep, "enable_flip_diagonal", False) if prep is not None else False
|
|
184
|
+
pad_div = getattr(prep, "pad_size_divisor", 32) if prep is not None else 32
|
|
185
|
+
return resize_mode, resize_to, flip_h, flip_v, flip_d, pad_div
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def build_train_augmentation_from_config(config: TrainingExperimentConfig):
|
|
189
|
+
if not config.enable_albumentation:
|
|
190
|
+
return None
|
|
191
|
+
aug = config.augmentation
|
|
192
|
+
return create_train_augmentation(
|
|
193
|
+
brightness_limit=aug.brightness_limit,
|
|
194
|
+
contrast_limit=aug.contrast_limit,
|
|
195
|
+
gamma_limit=aug.gamma_limit,
|
|
196
|
+
gauss_noise_var_limit=aug.gauss_noise_var_limit,
|
|
197
|
+
blur_limit=aug.blur_limit,
|
|
198
|
+
clahe_clip_limit=aug.clahe_clip_limit,
|
|
199
|
+
p_brightness_contrast=aug.p_brightness_contrast,
|
|
200
|
+
p_gamma=aug.p_gamma,
|
|
201
|
+
p_noise=aug.p_noise,
|
|
202
|
+
p_blur=aug.p_blur,
|
|
203
|
+
p_clahe=aug.p_clahe,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def make_collate_fn(
|
|
208
|
+
config: TrainingExperimentConfig,
|
|
209
|
+
*,
|
|
210
|
+
augmentation: Any,
|
|
211
|
+
enable_flip_horizontal: bool,
|
|
212
|
+
enable_flip_vertical: bool,
|
|
213
|
+
enable_flip_diagonal: bool,
|
|
214
|
+
):
|
|
215
|
+
resize_mode, resize_to, _, _, _, pad_div = get_resize_and_flips(config)
|
|
216
|
+
prep = getattr(config, "preprocessing", None)
|
|
217
|
+
norm_mean = getattr(prep, "normalize_mean", None) if prep is not None else None
|
|
218
|
+
norm_std = getattr(prep, "normalize_std", None) if prep is not None else None
|
|
219
|
+
return create_collate_fn(
|
|
220
|
+
config.class_map,
|
|
221
|
+
augmentation=augmentation,
|
|
222
|
+
normalize=False,
|
|
223
|
+
resize_mode=resize_mode,
|
|
224
|
+
resize_to=resize_to,
|
|
225
|
+
pad_size_divisor=pad_div,
|
|
226
|
+
enable_flip_horizontal=enable_flip_horizontal,
|
|
227
|
+
enable_flip_vertical=enable_flip_vertical,
|
|
228
|
+
enable_flip_diagonal=enable_flip_diagonal,
|
|
229
|
+
normalize_mean=norm_mean,
|
|
230
|
+
normalize_std=norm_std,
|
|
231
|
+
difficult_strategy=getattr(config.dataset, "difficult_strategy", "drop"),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def tensor_to_pil(image_tensor: torch.Tensor) -> Image.Image:
|
|
236
|
+
t = image_tensor.detach().cpu().float().clamp(0, 1)
|
|
237
|
+
if t.dim() != 3 or t.shape[0] != 3:
|
|
238
|
+
raise ValueError(f"Expected CHW tensor, got shape {tuple(t.shape)}")
|
|
239
|
+
arr = (t.permute(1, 2, 0).numpy() * 255.0).astype("uint8")
|
|
240
|
+
return Image.fromarray(arr, mode="RGB")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def draw_targets(
|
|
244
|
+
image: Image.Image,
|
|
245
|
+
target: dict,
|
|
246
|
+
class_names: list[str],
|
|
247
|
+
title: str,
|
|
248
|
+
) -> Image.Image:
|
|
249
|
+
image = image.copy()
|
|
250
|
+
rboxes = target.get("rboxes")
|
|
251
|
+
labels = target.get("labels")
|
|
252
|
+
if rboxes is None or rboxes.numel() == 0:
|
|
253
|
+
specs = []
|
|
254
|
+
box_objs = []
|
|
255
|
+
else:
|
|
256
|
+
box_objs = [
|
|
257
|
+
RBox(
|
|
258
|
+
float(row[0]),
|
|
259
|
+
float(row[1]),
|
|
260
|
+
float(row[2]),
|
|
261
|
+
float(row[3]),
|
|
262
|
+
float(row[4]),
|
|
263
|
+
)
|
|
264
|
+
for row in rboxes
|
|
265
|
+
]
|
|
266
|
+
specs = [viz.DrawingSpec(outline=(0, 255, 0), width=2) for _ in box_objs]
|
|
267
|
+
image = viz.draw_boxes(image, box_objs, specs=specs)
|
|
268
|
+
if labels is not None and len(labels) == len(box_objs):
|
|
269
|
+
label_texts = []
|
|
270
|
+
for lid in labels.tolist():
|
|
271
|
+
idx = int(lid) - 1
|
|
272
|
+
if 0 <= idx < len(class_names):
|
|
273
|
+
label_texts.append(class_names[idx])
|
|
274
|
+
else:
|
|
275
|
+
label_texts.append(str(lid))
|
|
276
|
+
polys = [b.to_polygon().points for b in box_objs]
|
|
277
|
+
for poly, text in zip(polys, label_texts):
|
|
278
|
+
xs = [p[0] for p in poly]
|
|
279
|
+
ys = [p[1] for p in poly]
|
|
280
|
+
x, y = min(xs), min(ys)
|
|
281
|
+
draw = ImageDraw.Draw(image)
|
|
282
|
+
draw.text((x, y - 12), text, fill=(255, 255, 0))
|
|
283
|
+
|
|
284
|
+
draw = ImageDraw.Draw(image)
|
|
285
|
+
try:
|
|
286
|
+
font = ImageFont.load_default()
|
|
287
|
+
except Exception:
|
|
288
|
+
font = None
|
|
289
|
+
draw.rectangle([0, 0, image.width, 22], fill=(0, 0, 0))
|
|
290
|
+
draw.text((4, 4), title, fill=(255, 255, 255), font=font)
|
|
291
|
+
return image
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def stitch_row(panels: list[Image.Image], gap: int = 8) -> Image.Image:
|
|
295
|
+
h = max(im.height for im in panels)
|
|
296
|
+
w = sum(im.width for im in panels) + gap * (len(panels) - 1)
|
|
297
|
+
canvas = Image.new("RGB", (w, h), (32, 32, 32))
|
|
298
|
+
x = 0
|
|
299
|
+
for im in panels:
|
|
300
|
+
canvas.paste(im, (x, 0))
|
|
301
|
+
x += im.width + gap
|
|
302
|
+
return canvas
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def stitch_grid(rows: list[Image.Image], gap: int = 12) -> Image.Image:
|
|
306
|
+
w = max(r.width for r in rows)
|
|
307
|
+
h = sum(r.height for r in rows) + gap * (len(rows) - 1)
|
|
308
|
+
canvas = Image.new("RGB", (w, h), (24, 24, 24))
|
|
309
|
+
y = 0
|
|
310
|
+
for row in rows:
|
|
311
|
+
canvas.paste(row, (0, y))
|
|
312
|
+
y += row.height + gap
|
|
313
|
+
return canvas
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def print_augmentation_summary(config: TrainingExperimentConfig, resize_to: tuple[int, int]) -> None:
|
|
317
|
+
prep = getattr(config, "preprocessing", None)
|
|
318
|
+
resize_mode, _, flip_h, flip_v, flip_d, _ = get_resize_and_flips(config)
|
|
319
|
+
print(f" enable_albumentation: {config.enable_albumentation}")
|
|
320
|
+
print(f" resize_mode: {resize_mode}")
|
|
321
|
+
print(f" resize_to (H×W): {resize_to[0]}×{resize_to[1]}")
|
|
322
|
+
print(f" flips: horizontal={flip_h}, vertical={flip_v}, diagonal={flip_d}")
|
|
323
|
+
if config.enable_albumentation:
|
|
324
|
+
aug = config.augmentation
|
|
325
|
+
print(" augmentation:")
|
|
326
|
+
for key in (
|
|
327
|
+
"brightness_limit",
|
|
328
|
+
"contrast_limit",
|
|
329
|
+
"gamma_limit",
|
|
330
|
+
"gauss_noise_var_limit",
|
|
331
|
+
"blur_limit",
|
|
332
|
+
"clahe_clip_limit",
|
|
333
|
+
"p_brightness_contrast",
|
|
334
|
+
"p_gamma",
|
|
335
|
+
"p_noise",
|
|
336
|
+
"p_blur",
|
|
337
|
+
"p_clahe",
|
|
338
|
+
):
|
|
339
|
+
print(f" {key}: {getattr(aug, key)}")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def main() -> None:
|
|
343
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
344
|
+
parser.add_argument(
|
|
345
|
+
"--config",
|
|
346
|
+
required=True,
|
|
347
|
+
help="Training recipe or resolved run config.json",
|
|
348
|
+
)
|
|
349
|
+
parser.add_argument(
|
|
350
|
+
"--output-dir",
|
|
351
|
+
type=Path,
|
|
352
|
+
default=None,
|
|
353
|
+
help="Output directory (default: <repo>/previews/augmentation/<config_stem>)",
|
|
354
|
+
)
|
|
355
|
+
parser.add_argument(
|
|
356
|
+
"--split",
|
|
357
|
+
choices=("train", "val"),
|
|
358
|
+
default="train",
|
|
359
|
+
help="Dataset split (default: train)",
|
|
360
|
+
)
|
|
361
|
+
parser.add_argument(
|
|
362
|
+
"--num-images",
|
|
363
|
+
type=int,
|
|
364
|
+
default=3,
|
|
365
|
+
help="Number of distinct tiles to preview (default: 3)",
|
|
366
|
+
)
|
|
367
|
+
parser.add_argument(
|
|
368
|
+
"--variants",
|
|
369
|
+
type=int,
|
|
370
|
+
default=4,
|
|
371
|
+
help="Random augmented variants per tile (default: 4)",
|
|
372
|
+
)
|
|
373
|
+
parser.add_argument(
|
|
374
|
+
"--seed",
|
|
375
|
+
type=int,
|
|
376
|
+
default=42,
|
|
377
|
+
help="RNG seed for tile and augmentation sampling",
|
|
378
|
+
)
|
|
379
|
+
parser.add_argument(
|
|
380
|
+
"--indices",
|
|
381
|
+
type=str,
|
|
382
|
+
default=None,
|
|
383
|
+
help="Comma-separated dataset indices (overrides random sampling)",
|
|
384
|
+
)
|
|
385
|
+
parser.add_argument(
|
|
386
|
+
"--include-albumentations-only",
|
|
387
|
+
action="store_true",
|
|
388
|
+
help="Add a column with Albumentations only (no random flips)",
|
|
389
|
+
)
|
|
390
|
+
args = parser.parse_args()
|
|
391
|
+
|
|
392
|
+
config_path = resolve_config_path(args.config)
|
|
393
|
+
print(f"Loading config: {config_path}")
|
|
394
|
+
config = load_training_config(config_path)
|
|
395
|
+
|
|
396
|
+
dataset = build_dataset_from_config(config, args.split)
|
|
397
|
+
class_map, class_names = resolve_class_map(config, dataset)
|
|
398
|
+
config.class_map = class_map
|
|
399
|
+
config.class_names = class_names
|
|
400
|
+
if len(dataset) == 0:
|
|
401
|
+
print("Dataset is empty.", file=sys.stderr)
|
|
402
|
+
sys.exit(1)
|
|
403
|
+
|
|
404
|
+
resize_mode, resize_to, flip_h, flip_v, flip_d, _ = get_resize_and_flips(config)
|
|
405
|
+
print(f"Building {args.split} dataset ({len(dataset)} samples)")
|
|
406
|
+
print_augmentation_summary(config, resize_to)
|
|
407
|
+
|
|
408
|
+
train_aug = build_train_augmentation_from_config(config)
|
|
409
|
+
collate_baseline = make_collate_fn(
|
|
410
|
+
config,
|
|
411
|
+
augmentation=None,
|
|
412
|
+
enable_flip_horizontal=False,
|
|
413
|
+
enable_flip_vertical=False,
|
|
414
|
+
enable_flip_diagonal=False,
|
|
415
|
+
)
|
|
416
|
+
collate_train = make_collate_fn(
|
|
417
|
+
config,
|
|
418
|
+
augmentation=train_aug,
|
|
419
|
+
enable_flip_horizontal=flip_h,
|
|
420
|
+
enable_flip_vertical=flip_v,
|
|
421
|
+
enable_flip_diagonal=flip_d,
|
|
422
|
+
)
|
|
423
|
+
collate_albu_only = None
|
|
424
|
+
if args.include_albumentations_only and train_aug is not None:
|
|
425
|
+
collate_albu_only = make_collate_fn(
|
|
426
|
+
config,
|
|
427
|
+
augmentation=train_aug,
|
|
428
|
+
enable_flip_horizontal=False,
|
|
429
|
+
enable_flip_vertical=False,
|
|
430
|
+
enable_flip_diagonal=False,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
if args.indices:
|
|
434
|
+
indices = [int(x.strip()) for x in args.indices.split(",") if x.strip()]
|
|
435
|
+
else:
|
|
436
|
+
rng = random.Random(args.seed)
|
|
437
|
+
k = min(args.num_images, len(dataset))
|
|
438
|
+
indices = rng.sample(range(len(dataset)), k=k)
|
|
439
|
+
|
|
440
|
+
out_dir = args.output_dir
|
|
441
|
+
if out_dir is None:
|
|
442
|
+
out_dir = _repo_root() / "previews" / "augmentation" / config_path.stem
|
|
443
|
+
out_dir = out_dir.resolve()
|
|
444
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
445
|
+
|
|
446
|
+
meta = {
|
|
447
|
+
"config": str(config_path),
|
|
448
|
+
"split": args.split,
|
|
449
|
+
"indices": indices,
|
|
450
|
+
"seed": args.seed,
|
|
451
|
+
"variants": args.variants,
|
|
452
|
+
"enable_albumentation": config.enable_albumentation,
|
|
453
|
+
}
|
|
454
|
+
(out_dir / "meta.json").write_text(json.dumps(meta, indent=2), encoding="utf-8")
|
|
455
|
+
|
|
456
|
+
grid_rows: list[Image.Image] = []
|
|
457
|
+
for idx in indices:
|
|
458
|
+
sample = dataset[idx]
|
|
459
|
+
stem = Path(sample.image_path).stem
|
|
460
|
+
panels: list[Image.Image] = []
|
|
461
|
+
|
|
462
|
+
images, targets = collate_baseline([sample])
|
|
463
|
+
panels.append(
|
|
464
|
+
draw_targets(
|
|
465
|
+
tensor_to_pil(images[0]),
|
|
466
|
+
targets[0],
|
|
467
|
+
class_names,
|
|
468
|
+
"baseline (resize, no aug)",
|
|
469
|
+
)
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if collate_albu_only is not None:
|
|
473
|
+
images, targets = collate_albu_only([sample])
|
|
474
|
+
panels.append(
|
|
475
|
+
draw_targets(
|
|
476
|
+
tensor_to_pil(images[0]),
|
|
477
|
+
targets[0],
|
|
478
|
+
class_names,
|
|
479
|
+
"albumentations only",
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
label_prefix = "train aug" if train_aug is not None else "train flip"
|
|
484
|
+
for v in range(args.variants):
|
|
485
|
+
random.seed(args.seed + idx * 1000 + v)
|
|
486
|
+
images, targets = collate_train([sample])
|
|
487
|
+
panels.append(
|
|
488
|
+
draw_targets(
|
|
489
|
+
tensor_to_pil(images[0]),
|
|
490
|
+
targets[0],
|
|
491
|
+
class_names,
|
|
492
|
+
f"{label_prefix} #{v + 1}",
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
row = stitch_row(panels)
|
|
497
|
+
row_path = out_dir / f"{idx:05d}_{stem}.png"
|
|
498
|
+
row.save(row_path)
|
|
499
|
+
grid_rows.append(row)
|
|
500
|
+
print(f" wrote {row_path.name}")
|
|
501
|
+
|
|
502
|
+
combined = stitch_grid(grid_rows)
|
|
503
|
+
combined_path = out_dir / "grid_all.png"
|
|
504
|
+
combined.save(combined_path)
|
|
505
|
+
print(f"\nCombined grid: {combined_path}")
|
|
506
|
+
print(f"Output directory: {out_dir}")
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
if __name__ == "__main__":
|
|
510
|
+
main()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Publish a checkpoint for Hub distribution (MMDet-style content hash suffix).
|
|
3
|
+
|
|
4
|
+
Strips optimizer state, saves CPU tensors, appends the first 8 hex chars of SHA-256.
|
|
5
|
+
|
|
6
|
+
Example::
|
|
7
|
+
|
|
8
|
+
python tools/publish_checkpoint.py \\
|
|
9
|
+
runs/rotated_retinanet/20260611-101135/checkpoints/best_mAP_0.70.pth \\
|
|
10
|
+
pretrained/rotated_retinanet_r50_fpn_dota_le90_1x
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import hashlib
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Iterable, Optional, Sequence
|
|
20
|
+
|
|
21
|
+
import torch
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _sha256_file(path: Path) -> str:
|
|
25
|
+
digest = hashlib.sha256()
|
|
26
|
+
with path.open("rb") as f:
|
|
27
|
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
28
|
+
digest.update(chunk)
|
|
29
|
+
return digest.hexdigest()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def publish_checkpoint(
|
|
33
|
+
in_file: Path,
|
|
34
|
+
out_stem: Path,
|
|
35
|
+
*,
|
|
36
|
+
save_keys: Optional[Sequence[str]] = None,
|
|
37
|
+
) -> Path:
|
|
38
|
+
"""Write a published checkpoint; return final path with ``-{hash8}.pth`` suffix."""
|
|
39
|
+
in_file = in_file.expanduser().resolve()
|
|
40
|
+
if not in_file.is_file():
|
|
41
|
+
raise FileNotFoundError(f"Input checkpoint not found: {in_file}")
|
|
42
|
+
|
|
43
|
+
checkpoint = torch.load(in_file, map_location="cpu", weights_only=False)
|
|
44
|
+
if not isinstance(checkpoint, dict):
|
|
45
|
+
raise TypeError(f"Expected dict checkpoint, got {type(checkpoint).__name__}")
|
|
46
|
+
|
|
47
|
+
if save_keys is not None:
|
|
48
|
+
keys = list(save_keys)
|
|
49
|
+
for key in list(checkpoint):
|
|
50
|
+
if key not in keys:
|
|
51
|
+
checkpoint.pop(key, None)
|
|
52
|
+
else:
|
|
53
|
+
for key in ("optimizer", "ema_state_dict", "lr_scheduler"):
|
|
54
|
+
checkpoint.pop(key, None)
|
|
55
|
+
|
|
56
|
+
out_stem = out_stem.expanduser().resolve()
|
|
57
|
+
if out_stem.suffix == ".pth":
|
|
58
|
+
out_stem = out_stem.with_suffix("")
|
|
59
|
+
|
|
60
|
+
out_stem.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
temp_path = out_stem.parent / f"{out_stem.name}.pth"
|
|
62
|
+
torch.save(checkpoint, temp_path, _use_new_zipfile_serialization=False)
|
|
63
|
+
|
|
64
|
+
sha = _sha256_file(temp_path)
|
|
65
|
+
final_path = out_stem.parent / f"{out_stem.name}-{sha[:8]}.pth"
|
|
66
|
+
if final_path.exists():
|
|
67
|
+
final_path.unlink()
|
|
68
|
+
temp_path.rename(final_path)
|
|
69
|
+
return final_path
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv: Optional[Iterable[str]] = None) -> int:
|
|
73
|
+
parser = argparse.ArgumentParser(description="Publish a checkpoint with SHA256[:8] suffix.")
|
|
74
|
+
parser.add_argument("in_file", type=Path, help="Training checkpoint (.pth)")
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"out_stem",
|
|
77
|
+
type=Path,
|
|
78
|
+
help="Output path without hash suffix (e.g. pretrained/foo_bar)",
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--save-keys",
|
|
82
|
+
nargs="+",
|
|
83
|
+
default=None,
|
|
84
|
+
help="Keep only these top-level keys (default: drop optimizer only)",
|
|
85
|
+
)
|
|
86
|
+
args = parser.parse_args(list(argv) if argv is not None else None)
|
|
87
|
+
|
|
88
|
+
final = publish_checkpoint(args.in_file, args.out_stem, save_keys=args.save_keys)
|
|
89
|
+
sha = _sha256_file(final)
|
|
90
|
+
print(f"Published: {final}")
|
|
91
|
+
print(f"sha256: {sha}")
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
sys.exit(main())
|