Expand regional corpus and aerial finetuning controls
This commit is contained in:
@@ -22,3 +22,7 @@ def test_empty_reference_counts_false_positives() -> None:
|
||||
predictions = [((0.0, 0.0, 10.0, 10.0), 0.4)]
|
||||
assert MODULE.match_boxes(predictions, [], confidence=0.25, match_iou=0.5) == (0, 1, 0)
|
||||
assert MODULE.match_boxes(predictions, [], confidence=0.5, match_iou=0.5) == (0, 0, 0)
|
||||
|
||||
|
||||
def test_box_scaling_preserves_center() -> None:
|
||||
assert MODULE.scale_box((10.0, 20.0, 30.0, 40.0), 1.5) == (5.0, 15.0, 35.0, 45.0)
|
||||
|
||||
@@ -33,6 +33,32 @@ def test_training_command_is_cuda_deterministic_and_bound_to_frozen_inputs(tmp_p
|
||||
assert "epochs=160" in command
|
||||
assert "max_det=1000" in command
|
||||
assert "imgsz=640" in command
|
||||
assert "optimizer=auto" in command
|
||||
assert "mosaic=1.0" in command
|
||||
|
||||
|
||||
def test_training_command_supports_conservative_aerial_finetuning(tmp_path: Path) -> None:
|
||||
command = MODULE.training_command(
|
||||
"yolo",
|
||||
model=tmp_path / "base.pt",
|
||||
data=tmp_path / "dataset.yaml",
|
||||
project=tmp_path / "runs",
|
||||
name="aerial",
|
||||
epochs=50,
|
||||
seed=42,
|
||||
batch=2,
|
||||
workers=0,
|
||||
optimizer="AdamW",
|
||||
lr0=0.0001,
|
||||
mosaic=0.0,
|
||||
scale=0.2,
|
||||
translate=0.05,
|
||||
)
|
||||
assert "optimizer=AdamW" in command
|
||||
assert "lr0=0.0001" in command
|
||||
assert "mosaic=0.0" in command
|
||||
assert "scale=0.2" in command
|
||||
assert "translate=0.05" in command
|
||||
assert f"data={tmp_path / 'dataset.yaml'}" in command
|
||||
|
||||
|
||||
|
||||
@@ -80,6 +80,10 @@ The active production model remains unchanged while any gate fails.
|
||||
|
||||
Every failed assessment returns `continue_training_loop`. Only a report with
|
||||
`training_complete` may proceed to final human review and guarded activation.
|
||||
Optimizer, initial learning rate, image size and geometric augmentation are
|
||||
explicit loop inputs. This permits a conservative aerial-imagery finetune
|
||||
(for example AdamW with mosaic disabled) without changing calibration, test
|
||||
or release gates.
|
||||
|
||||
After a failed assessment,
|
||||
`scripts/build_failure_driven_yolo_sampling.py` creates a checksummed,
|
||||
|
||||
@@ -52,6 +52,14 @@ def metrics(tp: int, fp: int, fn: int) -> dict[str, float | int]:
|
||||
}
|
||||
|
||||
|
||||
def scale_box(box: tuple[float, float, float, float], factor: float) -> tuple[float, float, float, float]:
|
||||
"""Scale a detector box around its center for calibration-only geometry correction."""
|
||||
x1, y1, x2, y2 = box
|
||||
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
|
||||
half_width, half_height = (x2 - x1) * factor / 2, (y2 - y1) * factor / 2
|
||||
return cx - half_width, cy - half_height, cx + half_width, cy + half_height
|
||||
|
||||
|
||||
def read_references(path: Path, width: int, height: int) -> list[tuple[float, float, float, float]]:
|
||||
boxes = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines() if path.is_file() else []:
|
||||
@@ -88,6 +96,7 @@ def main() -> int:
|
||||
default=1000,
|
||||
help="Maximum detections retained per tile; dense Belgian urban tiles exceed YOLO's default 300.",
|
||||
)
|
||||
parser.add_argument("--box-scale", type=float, default=1.0)
|
||||
args = parser.parse_args()
|
||||
|
||||
from ultralytics import YOLO
|
||||
@@ -119,7 +128,7 @@ def main() -> int:
|
||||
for tile, result in zip(tiles, results, strict=True):
|
||||
height, width = result.orig_shape
|
||||
predictions = [
|
||||
(tuple(map(float, box)), float(score))
|
||||
(scale_box(tuple(map(float, box)), args.box_scale), float(score))
|
||||
for box, score in zip(result.boxes.xyxy.cpu().tolist(), result.boxes.conf.cpu().tolist(), strict=True)
|
||||
]
|
||||
observations.append(
|
||||
@@ -163,6 +172,7 @@ def main() -> int:
|
||||
"test_time_augmentation": args.augment,
|
||||
"inference_imgsz": args.imgsz,
|
||||
"max_detections_per_tile": args.max_det,
|
||||
"box_scale": args.box_scale,
|
||||
"tile_count": len(tiles),
|
||||
"sweeps": sweeps,
|
||||
}
|
||||
|
||||
@@ -52,6 +52,35 @@ AOIS = (
|
||||
Aoi("zeebrugge-port-train-hard", "flanders", "port-hard-negative", "train", 3.205, 51.330, "background_candidate"),
|
||||
Aoi("hoge-kempen-train-bg", "flanders", "forest-heath-negative", "train", 5.650, 50.990, "background_candidate"),
|
||||
Aoi("westhoek-dunes-train-bg", "flanders", "dunes-negative", "train", 2.590, 51.095, "background_candidate"),
|
||||
# v11 full-AOI review: independent train-only coverage for the Flemish
|
||||
# image/roof domain that remained the limiting release region. These
|
||||
# centers are separated from the frozen calibration and test AOIs.
|
||||
Aoi("geel-urban-train-v12", "flanders", "mixed-urban", "train", 4.989, 51.165),
|
||||
Aoi("mol-urban-train-v12", "flanders", "ribbon-development", "train", 5.116, 51.190),
|
||||
Aoi("beringen-train-v12", "flanders", "suburban", "train", 5.226, 51.050),
|
||||
Aoi("bilzen-train-v12", "flanders", "small-city", "train", 5.519, 50.871),
|
||||
Aoi("bree-train-v12", "flanders", "small-city", "train", 5.597, 51.141),
|
||||
Aoi("maaseik-train-v12", "flanders", "historic-urban", "train", 5.790, 51.095),
|
||||
Aoi("peer-train-v12", "flanders", "ribbon-development", "train", 5.459, 51.130),
|
||||
Aoi("diksmuide-train-v12", "flanders", "small-city", "train", 2.865, 51.034),
|
||||
Aoi("ieper-train-v12", "flanders", "historic-urban", "train", 2.885, 50.851),
|
||||
Aoi("poperinge-train-v12", "flanders", "rural-town", "train", 2.726, 50.855),
|
||||
Aoi("eeklo-train-v12", "flanders", "mixed-urban", "train", 3.570, 51.185),
|
||||
Aoi("oudenaarde-train-v12", "flanders", "historic-urban", "train", 3.600, 50.845),
|
||||
Aoi("geraardsbergen-train-v12", "flanders", "hilly-urban", "train", 3.882, 50.771),
|
||||
Aoi("ninove-train-v12", "flanders", "mixed-urban", "train", 4.025, 50.835),
|
||||
Aoi("zottegem-train-v12", "flanders", "ribbon-development", "train", 3.815, 50.870),
|
||||
Aoi("ronse-train-v12", "flanders", "regional-architecture", "train", 3.600, 50.745),
|
||||
Aoi("halle-train-v12", "flanders", "dense-urban", "train", 4.235, 50.735),
|
||||
Aoi("vilvoorde-train-v12", "flanders", "urban-industrial", "train", 4.430, 50.930),
|
||||
Aoi("boom-train-v12", "flanders", "dense-urban", "train", 4.370, 51.090),
|
||||
Aoi("heist-op-den-berg-train-v12", "flanders", "ribbon-development", "train", 4.715, 51.075),
|
||||
Aoi("aalter-train-v12", "flanders", "suburban", "train", 3.445, 51.090),
|
||||
Aoi("wetteren-train-v12", "flanders", "mixed-urban", "train", 3.885, 51.005),
|
||||
Aoi("sint-truiden-train-v12", "flanders", "historic-urban", "train", 5.190, 50.815),
|
||||
Aoi("brasschaat-heath-train-v12-bg", "flanders", "heath-hard-negative", "train", 4.525, 51.350, "background_candidate"),
|
||||
Aoi("limburg-pine-train-v12-bg", "flanders", "forest-hard-negative", "train", 5.680, 51.020, "background_candidate"),
|
||||
Aoi("westhoek-field-train-v12-bg", "flanders", "farmland-hard-negative", "train", 2.720, 50.990, "background_candidate"),
|
||||
Aoi("bruges-val", "flanders", "historic-urban", "val", 3.224, 51.209),
|
||||
Aoi("turnhout-val", "flanders", "suburban", "val", 4.944, 51.322),
|
||||
Aoi("hasselt-cal", "flanders", "suburban", "calibration", 5.340, 50.930),
|
||||
@@ -82,6 +111,14 @@ AOIS = (
|
||||
Aoi("arlon-industry-train", "wallonia", "industrial", "train", 5.790, 49.676),
|
||||
Aoi("liege-rail-train-hard", "wallonia", "rail-hard-negative", "train", 5.615, 50.625, "background_candidate"),
|
||||
Aoi("famenne-quarry-train-hard", "wallonia", "quarry-hard-negative", "train", 5.080, 50.170, "background_candidate"),
|
||||
Aoi("spa-train-v12", "wallonia", "regional-architecture", "train", 5.867, 50.492),
|
||||
Aoi("malmedy-train-v12", "wallonia", "small-city", "train", 6.027, 50.426),
|
||||
Aoi("rochefort-train-v12", "wallonia", "rural-town", "train", 5.222, 50.159),
|
||||
Aoi("beauraing-train-v12", "wallonia", "rural-town", "train", 4.956, 50.110),
|
||||
Aoi("thuin-train-v12", "wallonia", "valley-urban", "train", 4.286, 50.339),
|
||||
Aoi("peruwelz-train-v12", "wallonia", "mixed-urban", "train", 3.591, 50.509),
|
||||
Aoi("ardenne-clearing-train-v12-bg", "wallonia", "forest-hard-negative", "train", 5.460, 50.080, "background_candidate"),
|
||||
Aoi("condroz-open-train-v12-bg", "wallonia", "farmland-hard-negative", "train", 5.020, 50.390, "background_candidate"),
|
||||
Aoi("tournai-val", "wallonia", "historic-urban", "val", 3.389, 50.606),
|
||||
Aoi("arlon-val", "wallonia", "small-city", "val", 5.817, 49.683),
|
||||
Aoi("verviers-cal", "wallonia", "suburban", "calibration", 5.860, 50.590),
|
||||
|
||||
@@ -42,8 +42,13 @@ def training_command(
|
||||
workers: int,
|
||||
max_det: int = 1000,
|
||||
imgsz: int = 640,
|
||||
optimizer: str = "auto",
|
||||
lr0: float | None = None,
|
||||
mosaic: float = 1.0,
|
||||
scale: float = 0.5,
|
||||
translate: float = 0.1,
|
||||
) -> list[str]:
|
||||
return [
|
||||
command = [
|
||||
yolo,
|
||||
"train",
|
||||
f"model={model}",
|
||||
@@ -57,12 +62,19 @@ def training_command(
|
||||
"cache=disk",
|
||||
"close_mosaic=20",
|
||||
f"max_det={max_det}",
|
||||
f"optimizer={optimizer}",
|
||||
f"mosaic={mosaic}",
|
||||
f"scale={scale}",
|
||||
f"translate={translate}",
|
||||
f"seed={seed}",
|
||||
"deterministic=True",
|
||||
f"project={project}",
|
||||
f"name={name}",
|
||||
"exist_ok=True",
|
||||
]
|
||||
if lr0 is not None:
|
||||
command.append(f"lr0={lr0}")
|
||||
return command
|
||||
|
||||
|
||||
def run(command: list[str], log_path: Path | None = None, *, allowed: set[int] = {0}) -> int:
|
||||
@@ -93,6 +105,11 @@ def main() -> int:
|
||||
parser.add_argument("--workers", type=int, default=4)
|
||||
parser.add_argument("--max-det", type=int, default=1000)
|
||||
parser.add_argument("--imgsz", type=int, default=640)
|
||||
parser.add_argument("--optimizer", default="auto")
|
||||
parser.add_argument("--lr0", type=float)
|
||||
parser.add_argument("--mosaic", type=float, default=1.0)
|
||||
parser.add_argument("--scale", type=float, default=0.5)
|
||||
parser.add_argument("--translate", type=float, default=0.1)
|
||||
parser.add_argument("--seed", type=int, default=20260731)
|
||||
parser.add_argument("--yolo", default="yolo")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
@@ -140,6 +157,11 @@ def main() -> int:
|
||||
workers=args.workers,
|
||||
max_det=args.max_det,
|
||||
imgsz=args.imgsz,
|
||||
optimizer=args.optimizer,
|
||||
lr0=args.lr0,
|
||||
mosaic=args.mosaic,
|
||||
scale=args.scale,
|
||||
translate=args.translate,
|
||||
)
|
||||
if args.dry_run:
|
||||
print(json.dumps({"training_command": command}, indent=2))
|
||||
|
||||
Reference in New Issue
Block a user