cartolegend-rc4-modern / scripts /predict_cartolegend_point_detector.py
LockNToad's picture
RC4-Modern: unchanged RC4 adapter + modern full-map pipeline (BENCH-WIDE 77/167 vs 52, FP 0)
35b0bfe verified
Raw
History Blame Contribute Delete
5.68 kB
#!/usr/bin/env python3
"""Write deterministic per-image predictions from a CartoLegend point detector."""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
import torch
from PIL import Image
from torch.utils.data import DataLoader
from train_cartolegend_point_detector import (
PointSymbolDataset,
build_model,
collate,
load_unique_records,
sha256,
)
from cartolegend_point_detector_artifact import load_point_detector_artifact
ROOT = Path(__file__).resolve().parents[1]
def portable_path(path: Path) -> str:
resolved = path.expanduser().resolve()
try:
return "project://" + resolved.relative_to(ROOT).as_posix()
except ValueError:
return resolved.name
def load_prediction_records(path: Path, image_field: str) -> list[dict]:
if image_field == "images":
return load_unique_records(path)
records = []
seen: set[Path] = set()
for line_number, line in enumerate(path.read_text().splitlines(), start=1):
if not line.strip():
continue
row = json.loads(line)
value = str(row.get(image_field) or "")
image = Path(value)
image = (image if image.is_absolute() else ROOT / image).resolve()
if not image.is_file():
raise FileNotFoundError(f"line {line_number}: {image}")
if image in seen:
continue
seen.add(image)
with Image.open(image) as source:
width, height = source.size
records.append(
{
"image": str(image),
"width": width,
"height": height,
"boxes": [],
"text_labels": [],
"group": str(row.get("source_id") or image.stem),
}
)
return records
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--checkpoint", type=Path, required=True)
parser.add_argument("--config", type=Path, default=None)
parser.add_argument(
"--allow-legacy-pt",
action="store_true",
help="Explicitly allow a legacy .pt checkpoint through weights_only=True.",
)
parser.add_argument("--input", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--image-field", default="images")
parser.add_argument("--workers", type=int, default=2)
return parser.parse_args()
def main() -> int:
args = parse_args()
checkpoint_path = args.checkpoint.expanduser().resolve()
input_path = args.input.expanduser().resolve()
output_path = args.output.expanduser().resolve()
artifact = load_point_detector_artifact(
checkpoint_path,
config_path=args.config,
device="cpu",
allow_legacy_pt=args.allow_legacy_pt,
)
config = artifact.model_config
model = build_model(
False,
int(config["min_size"]),
int(config["max_size"]),
int(config.get("trainable_backbone_layers", 6)),
str(config.get("architecture", "mobilenet")),
)
model.load_state_dict(artifact.state_dict, strict=True)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device).eval()
records = load_prediction_records(input_path, args.image_field)
loader = DataLoader(
PointSymbolDataset(records, augment=False),
batch_size=1,
shuffle=False,
num_workers=args.workers,
collate_fn=collate,
pin_memory=device.type == "cuda",
persistent_workers=args.workers > 0,
)
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w") as output_file, torch.inference_mode():
for record, (images, _targets) in zip(records, loader, strict=True):
prediction = model([images[0].to(device)])[0]
boxes = prediction["boxes"].detach().cpu().tolist()
scores = prediction["scores"].detach().cpu().tolist()
row = {
"schema": "cartolegend_point_detector_predictions_v1",
"image": portable_path(Path(record["image"])),
"width": record["width"],
"height": record["height"],
"detections": [
{
"symbol_bbox": [round(float(value), 4) for value in box],
"score": round(float(score), 8),
}
for box, score in zip(boxes, scores, strict=True)
],
}
output_file.write(json.dumps(row, sort_keys=True, separators=(",", ":")) + "\n")
manifest = {
"schema": "cartolegend_point_detector_prediction_manifest_v1",
"generated_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"checkpoint": portable_path(checkpoint_path),
"checkpoint_sha256": artifact.sha256,
"checkpoint_epoch": artifact.metadata.get("completed_epochs"),
"input": portable_path(input_path),
"input_sha256": sha256(input_path),
"output": portable_path(output_path),
"output_sha256": sha256(output_path),
"images": len(records),
"image_field": args.image_field,
"device": str(device),
"config": config,
}
manifest_path = output_path.with_suffix(output_path.suffix + ".manifest.json")
manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
print(json.dumps(manifest, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())