Expand regional corpus and aerial finetuning controls
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-27 06:19:11 +02:00
parent 8a4a995659
commit 2316ff28f5
6 changed files with 105 additions and 2 deletions
@@ -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
+4
View File
@@ -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,
+11 -1
View File
@@ -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),
+23 -1
View File
@@ -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))