Dataset preparation script with documentation
- Satellite crop generation (512x512, stride 256, resize 256x256) - Route 09 tile stitching (4 tiles -> 44800x33280) - GPS matching drone->crop via vectorized haversine - Index files in UAV-GeoLoc format (train/test query + DB) - positive.json / semi_positive.json / db_postion.txt per route - Route 07 excluded (satellite too narrow) - Fixed: full gallery in DB files, db_postion.txt format, frame_id keys - Fixed: file handle leaks in image processing loops Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
559
scripts/prepare_dataset.py
Normal file
559
scripts/prepare_dataset.py
Normal file
@@ -0,0 +1,559 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare UAV-VisLoc dataset for retrieval training.
|
||||
|
||||
Pipeline:
|
||||
1. Resize drone images -> 256x256
|
||||
2. Stitch satellite tiles for route 09
|
||||
3. Crop satellite maps -> 512x512 patches with stride 256, resize -> 256x256
|
||||
4. Compute GPS for each crop center
|
||||
5. Match drone -> crops via GPS (positive, semi-positive)
|
||||
6. Generate metadata: positive.json, semi_positive.json, db_postion.txt
|
||||
7. Generate Index files (train_query.txt, train_db.txt, etc.)
|
||||
|
||||
Usage:
|
||||
python scripts/prepare_dataset.py \
|
||||
--src /path/to/UAV_VisLoc_dataset \
|
||||
--dst /path/to/UAV_VisLoc_processed \
|
||||
--crop-size 512 \
|
||||
--stride 256 \
|
||||
--target-size 256
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
warnings.filterwarnings("ignore", category=Image.DecompressionBombWarning)
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
|
||||
# Route 07 excluded: satellite map 3000x170 — too narrow for 512x512 crops
|
||||
EXCLUDED_ROUTES = {"07"}
|
||||
|
||||
# Route 09 has satellite split into 4 tiles
|
||||
SPLIT_TILE_ROUTE = "09"
|
||||
TILE_LAYOUT_09 = {
|
||||
# (col, row): filename
|
||||
(0, 0): "satellite09_01-01.tif",
|
||||
(1, 0): "satellite09_01-02.tif",
|
||||
(0, 1): "satellite09_02-01.tif",
|
||||
(1, 1): "satellite09_02-02.tif",
|
||||
}
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Prepare UAV-VisLoc for retrieval.")
|
||||
parser.add_argument("--src", required=True, help="Path to raw UAV_VisLoc_dataset")
|
||||
parser.add_argument("--dst", required=True, help="Path to output processed dataset")
|
||||
parser.add_argument("--crop-size", type=int, default=512, help="Satellite crop size in px")
|
||||
parser.add_argument("--stride", type=int, default=256, help="Crop stride in px")
|
||||
parser.add_argument("--target-size", type=int, default=256, help="Final resize for both drone and crops")
|
||||
parser.add_argument("--routes", nargs="*", default=None, help="Process only these routes (e.g. 01 02)")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Read metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def read_satellite_bbox(src: Path) -> dict:
|
||||
"""Read satellite GPS bounding boxes from CSV."""
|
||||
bbox = {}
|
||||
csv_path = src / "satellite_ coordinates_range.csv"
|
||||
with open(csv_path) as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
name = row["mapname"]
|
||||
route = name.replace("satellite", "").replace(".tif", "")
|
||||
bbox[route] = {
|
||||
"lt_lat": float(row["LT_lat_map"]),
|
||||
"lt_lon": float(row["LT_lon_map"]),
|
||||
"rb_lat": float(row["RB_lat_map"]),
|
||||
"rb_lon": float(row["RB_lon_map"]),
|
||||
}
|
||||
return bbox
|
||||
|
||||
|
||||
def read_drone_metadata(src: Path, route: str) -> list[dict]:
|
||||
"""Read drone GPS + pose from per-route CSV."""
|
||||
csv_path = src / route / f"{route}.csv"
|
||||
entries = []
|
||||
with open(csv_path) as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
entries.append({
|
||||
"filename": row["filename"],
|
||||
"lat": float(row["lat"]),
|
||||
"lon": float(row["lon"]),
|
||||
"height": float(row["height"]),
|
||||
})
|
||||
return entries
|
||||
|
||||
|
||||
def read_split(src: Path, split: str) -> set[str]:
|
||||
"""Read train/test split CSV, return set of drone filenames."""
|
||||
csv_path = src / f"visloc_{split}.csv"
|
||||
filenames = set()
|
||||
with open(csv_path) as f:
|
||||
reader = csv.DictReader(f, delimiter="\t")
|
||||
for row in reader:
|
||||
fn = row["filename"]
|
||||
basename = os.path.basename(fn)
|
||||
filenames.add(basename)
|
||||
return filenames
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Resize drone images
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def resize_drone_images(src: Path, dst: Path, route: str, target_size: int) -> int:
|
||||
"""Resize all drone images for a route to target_size x target_size."""
|
||||
drone_src = src / route / "drone"
|
||||
drone_dst = dst / route / "drone"
|
||||
drone_dst.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
count = 0
|
||||
for img_name in sorted(os.listdir(drone_src)):
|
||||
if not img_name.upper().endswith(".JPG"):
|
||||
continue
|
||||
src_path = drone_src / img_name
|
||||
dst_path = drone_dst / img_name
|
||||
|
||||
if dst_path.exists():
|
||||
count += 1
|
||||
continue
|
||||
|
||||
with Image.open(src_path) as img:
|
||||
img_resized = img.resize((target_size, target_size), Image.LANCZOS)
|
||||
img_resized.save(dst_path, "JPEG", quality=95)
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Stitch satellite tiles (route 09)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def stitch_route09(src: Path) -> Image.Image:
|
||||
"""Stitch 4 satellite tiles for route 09 into a single image."""
|
||||
tiles = {}
|
||||
for (col, row), fname in TILE_LAYOUT_09.items():
|
||||
tile_path = src / SPLIT_TILE_ROUTE / fname
|
||||
tiles[(col, row)] = Image.open(tile_path)
|
||||
|
||||
# Compute full image dimensions
|
||||
# Row 0: tiles (0,0) and (1,0) side by side
|
||||
# Row 1: tiles (0,1) and (1,1) side by side
|
||||
w0 = tiles[(0, 0)].width + tiles[(1, 0)].width
|
||||
w1 = tiles[(0, 1)].width + tiles[(1, 1)].width
|
||||
full_w = max(w0, w1)
|
||||
|
||||
h0 = max(tiles[(0, 0)].height, tiles[(1, 0)].height)
|
||||
h1 = max(tiles[(0, 1)].height, tiles[(1, 1)].height)
|
||||
full_h = h0 + h1
|
||||
|
||||
merged = Image.new("RGB", (full_w, full_h))
|
||||
merged.paste(tiles[(0, 0)], (0, 0))
|
||||
merged.paste(tiles[(1, 0)], (tiles[(0, 0)].width, 0))
|
||||
merged.paste(tiles[(0, 1)], (0, h0))
|
||||
merged.paste(tiles[(1, 1)], (tiles[(0, 1)].width, h0))
|
||||
|
||||
# Close tiles to free memory (~4.3 GB).
|
||||
for tile in tiles.values():
|
||||
tile.close()
|
||||
|
||||
return merged
|
||||
|
||||
|
||||
def load_satellite(src: Path, route: str) -> Image.Image:
|
||||
"""Load satellite map for a route (handles route 09 stitching)."""
|
||||
if route == SPLIT_TILE_ROUTE:
|
||||
return stitch_route09(src)
|
||||
sat_path = src / route / f"satellite{route}.tif"
|
||||
return Image.open(sat_path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Crop satellite map
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def crop_satellite(
|
||||
sat_img: Image.Image,
|
||||
dst: Path,
|
||||
route: str,
|
||||
crop_size: int,
|
||||
stride: int,
|
||||
target_size: int,
|
||||
) -> list[dict]:
|
||||
"""Crop satellite image into patches and save resized versions.
|
||||
|
||||
Returns list of crop metadata: [{"name": "crop_X_Y.png", "x": X, "y": Y, "px_x": ..., "px_y": ...}]
|
||||
"""
|
||||
crop_dir = dst / route / "DB" / "img"
|
||||
crop_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
w, h = sat_img.size
|
||||
cols = max(0, (w - crop_size) // stride + 1)
|
||||
rows = max(0, (h - crop_size) // stride + 1)
|
||||
|
||||
crops_meta = []
|
||||
|
||||
for cx in range(cols):
|
||||
for cy in range(rows):
|
||||
px_x = cx * stride
|
||||
px_y = cy * stride
|
||||
|
||||
crop_name = f"crop_{cx}_{cy}.png"
|
||||
crop_path = crop_dir / crop_name
|
||||
|
||||
if not crop_path.exists():
|
||||
box = (px_x, px_y, px_x + crop_size, px_y + crop_size)
|
||||
patch = sat_img.crop(box)
|
||||
patch_resized = patch.resize((target_size, target_size), Image.LANCZOS)
|
||||
patch_resized.save(crop_path, "PNG")
|
||||
patch.close()
|
||||
patch_resized.close()
|
||||
|
||||
crops_meta.append({
|
||||
"name": crop_name,
|
||||
"x": cx,
|
||||
"y": cy,
|
||||
"px_x": px_x,
|
||||
"px_y": px_y,
|
||||
})
|
||||
|
||||
return crops_meta
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Compute GPS for crops + match drone -> crops
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def compute_crop_gps(crops_meta: list[dict], bbox: dict, sat_w: int, sat_h: int, crop_size: int) -> list[dict]:
|
||||
"""Add GPS center coordinates to each crop metadata entry."""
|
||||
lt_lat = bbox["lt_lat"]
|
||||
lt_lon = bbox["lt_lon"]
|
||||
rb_lat = bbox["rb_lat"]
|
||||
rb_lon = bbox["rb_lon"]
|
||||
|
||||
for crop in crops_meta:
|
||||
center_px_x = crop["px_x"] + crop_size / 2
|
||||
center_px_y = crop["px_y"] + crop_size / 2
|
||||
|
||||
crop["lon"] = lt_lon + (center_px_x / sat_w) * (rb_lon - lt_lon)
|
||||
crop["lat"] = lt_lat + (center_px_y / sat_h) * (rb_lat - lt_lat)
|
||||
|
||||
return crops_meta
|
||||
|
||||
|
||||
def _frame_id(filename: str) -> str:
|
||||
"""Extract frame ID from drone filename: '01_0001.JPG' -> '0001'."""
|
||||
stem = os.path.splitext(filename)[0] # '01_0001'
|
||||
return stem.split("_", 1)[1] # '0001'
|
||||
|
||||
|
||||
def match_drone_to_crops(
|
||||
drone_entries: list[dict],
|
||||
crops_meta: list[dict],
|
||||
) -> tuple[dict, dict]:
|
||||
"""Match each drone image to its positive and semi-positive crops.
|
||||
|
||||
positive: crop with minimum GPS distance to drone center.
|
||||
semi-positive: all crops within ±1 grid step from the positive crop.
|
||||
|
||||
Returns:
|
||||
positive_map: {frame_id: [crop_name]}
|
||||
semi_positive_map: {frame_id: [crop_name, ...]}
|
||||
"""
|
||||
# Build arrays for vectorized distance computation
|
||||
crop_lats = np.array([c["lat"] for c in crops_meta])
|
||||
crop_lons = np.array([c["lon"] for c in crops_meta])
|
||||
|
||||
# Grid lookup: (x, y) -> crop_name for O(1) neighbor search
|
||||
grid = {}
|
||||
for crop in crops_meta:
|
||||
grid[(crop["x"], crop["y"])] = crop["name"]
|
||||
|
||||
positive_map = {}
|
||||
semi_positive_map = {}
|
||||
|
||||
for drone in drone_entries:
|
||||
d_lat, d_lon = drone["lat"], drone["lon"]
|
||||
fid = _frame_id(drone["filename"])
|
||||
|
||||
# Compute distances to all crops (vectorized haversine)
|
||||
dlat = np.radians(crop_lats - d_lat)
|
||||
dlon = np.radians(crop_lons - d_lon)
|
||||
a = np.sin(dlat / 2) ** 2 + np.cos(math.radians(d_lat)) * np.cos(np.radians(crop_lats)) * np.sin(dlon / 2) ** 2
|
||||
dists = 6_371_000 * 2 * np.arcsin(np.sqrt(a))
|
||||
|
||||
# Positive: closest crop
|
||||
best_idx = int(np.argmin(dists))
|
||||
best_crop = crops_meta[best_idx]
|
||||
positive_map[fid] = [best_crop["name"]]
|
||||
|
||||
# Semi-positives: ±1 neighbors in grid via dict lookup
|
||||
bx, by = best_crop["x"], best_crop["y"]
|
||||
semi = []
|
||||
for dx in range(-1, 2):
|
||||
for dy in range(-1, 2):
|
||||
if dx == 0 and dy == 0:
|
||||
continue
|
||||
neighbor = grid.get((bx + dx, by + dy))
|
||||
if neighbor is not None:
|
||||
semi.append(neighbor)
|
||||
|
||||
semi_positive_map[fid] = semi
|
||||
|
||||
return positive_map, semi_positive_map
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. Write metadata files
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def write_positive_json(dst: Path, route: str, positive_map: dict):
|
||||
out_path = dst / route / "positive.json"
|
||||
with open(out_path, "w") as f:
|
||||
json.dump(positive_map, f, indent=2)
|
||||
|
||||
|
||||
def write_semi_positive_json(dst: Path, route: str, semi_positive_map: dict):
|
||||
out_path = dst / route / "semi_positive.json"
|
||||
with open(out_path, "w") as f:
|
||||
json.dump(semi_positive_map, f, indent=2)
|
||||
|
||||
|
||||
def write_db_position(dst: Path, route: str, crops_meta: list[dict],
|
||||
sat_w: int, sat_h: int, bbox: dict):
|
||||
"""Write db_postion.txt with GPS coordinates and scale for each crop.
|
||||
|
||||
Format matches UAV-GeoLoc: name\tlon\tlat\tscale_lon\tscale_lat (tab-separated).
|
||||
scale_lon/scale_lat = degrees per pixel in the satellite map.
|
||||
Note: filename uses original UAV-GeoLoc spelling 'postion' (sic).
|
||||
"""
|
||||
scale_lon = (bbox["rb_lon"] - bbox["lt_lon"]) / sat_w
|
||||
scale_lat = (bbox["rb_lat"] - bbox["lt_lat"]) / sat_h
|
||||
|
||||
out_path = dst / route / "DB" / "db_postion.txt"
|
||||
with open(out_path, "w") as f:
|
||||
for crop in crops_meta:
|
||||
f.write(f"{crop['name']}\t{crop['lon']:.8f}\t{crop['lat']:.8f}"
|
||||
f"\t{scale_lon:.2e}\t{scale_lat:.2e}\n")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 7. Generate Index files
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def generate_index_files(
|
||||
dst: Path,
|
||||
all_routes: list[str],
|
||||
route_data: dict,
|
||||
train_files: set[str],
|
||||
test_files: set[str],
|
||||
):
|
||||
"""Generate train/test query and DB index files in UAV-GeoLoc format.
|
||||
|
||||
train_query.txt format:
|
||||
route/drone/filename label positive_crop1 positive_crop2 ...
|
||||
train_db.txt format:
|
||||
route/DB/img/crop_name
|
||||
"""
|
||||
index_dir = dst / "Index"
|
||||
index_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
train_query_lines = []
|
||||
test_query_lines = []
|
||||
all_db_set = set()
|
||||
|
||||
for route in all_routes:
|
||||
data = route_data[route]
|
||||
positive_map = data["positive_map"]
|
||||
semi_positive_map = data["semi_positive_map"]
|
||||
crops_meta = data["crops_meta"]
|
||||
|
||||
# All DB crops for this route go into the gallery
|
||||
for crop in crops_meta:
|
||||
db_path = f"{route}/DB/img/{crop['name']}"
|
||||
all_db_set.add(db_path)
|
||||
|
||||
for drone_entry in data["drone_entries"]:
|
||||
fname = drone_entry["filename"]
|
||||
fid = _frame_id(fname)
|
||||
drone_path = f"{route}/drone/{fname}"
|
||||
|
||||
positives = positive_map.get(fid, [])
|
||||
semi_pos = semi_positive_map.get(fid, [])
|
||||
all_pos = positives + semi_pos
|
||||
|
||||
pos_paths = [f"{route}/DB/img/{p}" for p in all_pos]
|
||||
line = f"{drone_path} 0 {' '.join(pos_paths)}"
|
||||
|
||||
if fname in train_files:
|
||||
train_query_lines.append(line)
|
||||
elif fname in test_files:
|
||||
test_query_lines.append(line)
|
||||
|
||||
# DB gallery is the same for train and test (split is by query, not by route)
|
||||
sorted_db = sorted(all_db_set)
|
||||
|
||||
# Write files
|
||||
_write_lines(index_dir / "train_query.txt", train_query_lines)
|
||||
_write_lines(index_dir / "test_query.txt", test_query_lines)
|
||||
_write_lines(index_dir / "train_db.txt", sorted_db)
|
||||
_write_lines(index_dir / "test_db.txt", sorted_db)
|
||||
_write_lines(index_dir / "all_db.txt", sorted_db)
|
||||
|
||||
# Route lists
|
||||
_write_lines(index_dir / "train.txt", all_routes)
|
||||
_write_lines(index_dir / "test.txt", all_routes)
|
||||
|
||||
return {
|
||||
"train_queries": len(train_query_lines),
|
||||
"test_queries": len(test_query_lines),
|
||||
"total_db": len(all_db_set),
|
||||
}
|
||||
|
||||
|
||||
def _write_lines(path: Path, lines: list[str]):
|
||||
with open(path, "w") as f:
|
||||
for line in lines:
|
||||
f.write(line + "\n")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def process_route(
|
||||
src: Path,
|
||||
dst: Path,
|
||||
route: str,
|
||||
bbox: dict,
|
||||
crop_size: int,
|
||||
stride: int,
|
||||
target_size: int,
|
||||
) -> dict:
|
||||
"""Process a single route: resize drone, crop satellite, match pairs."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Route {route}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# 1. Read drone metadata
|
||||
drone_entries = read_drone_metadata(src, route)
|
||||
print(f" Drone images: {len(drone_entries)}")
|
||||
|
||||
# 2. Resize drone images
|
||||
n_drone = resize_drone_images(src, dst, route, target_size)
|
||||
print(f" Drone resized: {n_drone}")
|
||||
|
||||
# 3. Load and crop satellite
|
||||
print(f" Loading satellite map...")
|
||||
sat_img = load_satellite(src, route)
|
||||
sat_w, sat_h = sat_img.size
|
||||
print(f" Satellite size: {sat_w}x{sat_h}")
|
||||
|
||||
crops_meta = crop_satellite(sat_img, dst, route, crop_size, stride, target_size)
|
||||
print(f" Satellite crops: {len(crops_meta)}")
|
||||
|
||||
# Free memory
|
||||
del sat_img
|
||||
|
||||
# 4. Compute GPS for crops
|
||||
route_bbox = bbox[route]
|
||||
crops_meta = compute_crop_gps(crops_meta, route_bbox, sat_w, sat_h, crop_size)
|
||||
|
||||
# 5. Match drone -> crops
|
||||
positive_map, semi_positive_map = match_drone_to_crops(drone_entries, crops_meta)
|
||||
print(f" Positives matched: {len(positive_map)}")
|
||||
|
||||
# Stats
|
||||
avg_semi = sum(len(v) for v in semi_positive_map.values()) / max(1, len(semi_positive_map))
|
||||
print(f" Avg semi-positives per drone: {avg_semi:.1f}")
|
||||
|
||||
# 6. Write metadata
|
||||
write_positive_json(dst, route, positive_map)
|
||||
write_semi_positive_json(dst, route, semi_positive_map)
|
||||
write_db_position(dst, route, crops_meta, sat_w, sat_h, route_bbox)
|
||||
print(f" Metadata written.")
|
||||
|
||||
return {
|
||||
"drone_entries": drone_entries,
|
||||
"crops_meta": crops_meta,
|
||||
"positive_map": positive_map,
|
||||
"semi_positive_map": semi_positive_map,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
src = Path(args.src)
|
||||
dst = Path(args.dst)
|
||||
|
||||
print(f"Source: {src}")
|
||||
print(f"Destination: {dst}")
|
||||
print(f"Crop: {args.crop_size}x{args.crop_size}, stride: {args.stride}")
|
||||
print(f"Target size: {args.target_size}x{args.target_size}")
|
||||
print(f"Excluded routes: {EXCLUDED_ROUTES}")
|
||||
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Read satellite bounding boxes
|
||||
bbox = read_satellite_bbox(src)
|
||||
|
||||
# Determine routes to process
|
||||
all_routes = sorted([
|
||||
d for d in os.listdir(src)
|
||||
if os.path.isdir(src / d) and d.isdigit() and d not in EXCLUDED_ROUTES
|
||||
])
|
||||
if args.routes:
|
||||
all_routes = [r for r in all_routes if r in args.routes]
|
||||
|
||||
print(f"Routes to process: {all_routes}")
|
||||
|
||||
# Read train/test splits
|
||||
train_files = read_split(src, "train")
|
||||
test_files = read_split(src, "test")
|
||||
print(f"Train split: {len(train_files)} files")
|
||||
print(f"Test split: {len(test_files)} files")
|
||||
|
||||
# Process each route
|
||||
route_data = {}
|
||||
total_drone = 0
|
||||
total_crops = 0
|
||||
|
||||
for route in all_routes:
|
||||
data = process_route(src, dst, route, bbox, args.crop_size, args.stride, args.target_size)
|
||||
route_data[route] = data
|
||||
total_drone += len(data["drone_entries"])
|
||||
total_crops += len(data["crops_meta"])
|
||||
|
||||
# Generate Index files
|
||||
print(f"\n{'='*60}")
|
||||
print("Generating Index files...")
|
||||
idx_stats = generate_index_files(dst, all_routes, route_data, train_files, test_files)
|
||||
print(f" Train queries: {idx_stats['train_queries']}")
|
||||
print(f" Test queries: {idx_stats['test_queries']}")
|
||||
print(f" DB gallery (all crops): {idx_stats['total_db']}")
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*60}")
|
||||
print("SUMMARY")
|
||||
print(f"{'='*60}")
|
||||
print(f" Routes processed: {len(all_routes)}")
|
||||
print(f" Total drone images (resized to {args.target_size}): {total_drone}")
|
||||
print(f" Total satellite crops ({args.crop_size}→{args.target_size}): {total_crops}")
|
||||
print(f" Output: {dst}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user