first commit
This commit is contained in:
163
download_tiles.py
Normal file
163
download_tiles.py
Normal file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Download satellite tiles from Mapy.com REST API for Hradec Králové.
|
||||
Zoom 18 ≈ 0.38 m/pixel at lat 50° — good for car detection.
|
||||
|
||||
API key: https://developer.mapy.com/ (zdarma po registraci)
|
||||
"""
|
||||
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
# Hradec Králové bounding box (min_lon, min_lat, max_lon, max_lat)
|
||||
HK_BBOX = (15.7800, 50.1800, 15.8800, 50.2400)
|
||||
|
||||
# Mapy.com REST API v1 — mapset "aerial" = satelitní snímky
|
||||
# Aerial: ČR až zoom 20, zbytek světa jen zoom 13
|
||||
TILE_URL = "https://api.mapy.com/v1/maptiles/aerial/256/{z}/{x}/{y}?lang=cs&apikey={apikey}"
|
||||
TILE_SIZE = 256
|
||||
DEFAULT_ZOOM = 18
|
||||
OUT_DIR = Path("tiles")
|
||||
|
||||
|
||||
def lat_lon_to_tile(lat: float, lon: float, zoom: int) -> tuple[int, int]:
|
||||
"""Convert lat/lon to tile x, y at given zoom (Slippy Map convention)."""
|
||||
n = 2 ** zoom
|
||||
x = int((lon + 180) / 360 * n)
|
||||
lat_rad = math.radians(lat)
|
||||
y = int((1 - math.log(math.tan(lat_rad) + 1 / math.cos(lat_rad)) / math.pi) / 2 * n)
|
||||
return x, y
|
||||
|
||||
|
||||
def tile_to_lat_lon(x: int, y: int, zoom: int) -> tuple[float, float]:
|
||||
"""Return NW corner (lat, lon) of tile."""
|
||||
n = 2 ** zoom
|
||||
lon = x / n * 360 - 180
|
||||
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * y / n)))
|
||||
lat = math.degrees(lat_rad)
|
||||
return lat, lon
|
||||
|
||||
|
||||
def ground_resolution(lat: float, zoom: int) -> float:
|
||||
"""Return meters per pixel at given lat and zoom."""
|
||||
return 156543.03 * math.cos(math.radians(lat)) / (2 ** zoom)
|
||||
|
||||
|
||||
def download_tile(x: int, y: int, zoom: int, out_dir: Path, session: requests.Session, apikey: str) -> Path | None:
|
||||
path = out_dir / f"{zoom}_{x}_{y}.jpg"
|
||||
if path.exists():
|
||||
return path
|
||||
|
||||
url = TILE_URL.format(z=zoom, x=x, y=y, apikey=apikey)
|
||||
try:
|
||||
r = session.get(url, timeout=15)
|
||||
r.raise_for_status()
|
||||
path.write_bytes(r.content)
|
||||
return path
|
||||
except Exception as e:
|
||||
print(f" FAIL {x},{y}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def stitch_tiles(tiles: dict[tuple, Path], x_range: range, y_range: range, zoom: int, out_path: Path):
|
||||
"""Stitch downloaded tiles into one big image."""
|
||||
cols = len(x_range)
|
||||
rows = len(y_range)
|
||||
canvas = Image.new("RGB", (cols * TILE_SIZE, rows * TILE_SIZE))
|
||||
|
||||
for j, y in enumerate(y_range):
|
||||
for i, x in enumerate(x_range):
|
||||
key = (x, y)
|
||||
if key in tiles and tiles[key]:
|
||||
try:
|
||||
img = Image.open(tiles[key])
|
||||
canvas.paste(img, (i * TILE_SIZE, j * TILE_SIZE))
|
||||
except Exception as e:
|
||||
print(f" Stitch error {x},{y}: {e}")
|
||||
|
||||
canvas.save(out_path)
|
||||
print(f"Stitched image saved: {out_path} ({canvas.width}x{canvas.height} px)")
|
||||
return canvas
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Download Mapy.cz satellite tiles for Hradec Králové")
|
||||
parser.add_argument("--zoom", type=int, default=DEFAULT_ZOOM,
|
||||
help=f"Zoom level (default: {DEFAULT_ZOOM})")
|
||||
parser.add_argument("--bbox", nargs=4, type=float,
|
||||
metavar=("MIN_LON", "MIN_LAT", "MAX_LON", "MAX_LAT"),
|
||||
default=HK_BBOX,
|
||||
help="Bounding box (default: Hradec Králové)")
|
||||
parser.add_argument("--out", type=Path, default=OUT_DIR,
|
||||
help="Output directory for tiles")
|
||||
parser.add_argument("--stitch", action="store_true",
|
||||
help="Stitch all tiles into one image")
|
||||
parser.add_argument("--workers", type=int, default=8,
|
||||
help="Parallel download workers")
|
||||
parser.add_argument("--delay", type=float, default=0.05,
|
||||
help="Delay between requests (seconds)")
|
||||
parser.add_argument("--apikey", type=str, default=os.environ.get("MAPY_API_KEY", ""),
|
||||
help="Mapy.com API key (nebo env MAPY_API_KEY)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.apikey:
|
||||
parser.error("API klíč je povinný — použij --apikey nebo nastav env MAPY_API_KEY")
|
||||
|
||||
min_lon, min_lat, max_lon, max_lat = args.bbox
|
||||
zoom = args.zoom
|
||||
|
||||
res = ground_resolution((min_lat + max_lat) / 2, zoom)
|
||||
print(f"Zoom {zoom} → ~{res:.2f} m/pixel")
|
||||
|
||||
# Tile range (NW corner has smaller y value)
|
||||
x_min, y_max = lat_lon_to_tile(min_lat, min_lon, zoom)
|
||||
x_max, y_min = lat_lon_to_tile(max_lat, max_lon, zoom)
|
||||
|
||||
x_range = range(x_min, x_max + 1)
|
||||
y_range = range(y_min, y_max + 1)
|
||||
total = len(x_range) * len(y_range)
|
||||
|
||||
print(f"Tiles: {len(x_range)} cols × {len(y_range)} rows = {total} total")
|
||||
print(f"Area: ~{len(x_range)*TILE_SIZE*res/1000:.1f} km × {len(y_range)*TILE_SIZE*res/1000:.1f} km")
|
||||
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
session = requests.Session()
|
||||
session.headers.update({
|
||||
"User-Agent": "Mozilla/5.0 (compatible; satellite-downloader/1.0)",
|
||||
"Referer": "https://mapy.cz/",
|
||||
})
|
||||
|
||||
tiles: dict[tuple, Path | None] = {}
|
||||
done = 0
|
||||
|
||||
jobs = [(x, y) for y in y_range for x in x_range]
|
||||
|
||||
with ThreadPoolExecutor(max_workers=args.workers) as pool:
|
||||
futures = {pool.submit(download_tile, x, y, zoom, args.out, session, args.apikey): (x, y)
|
||||
for x, y in jobs}
|
||||
for future in as_completed(futures):
|
||||
x, y = futures[future]
|
||||
tiles[(x, y)] = future.result()
|
||||
done += 1
|
||||
if done % 20 == 0 or done == total:
|
||||
print(f" {done}/{total} tiles downloaded", end="\r")
|
||||
time.sleep(args.delay)
|
||||
|
||||
ok = sum(1 for v in tiles.values() if v)
|
||||
print(f"\nDownloaded: {ok}/{total} tiles → {args.out}/")
|
||||
|
||||
if args.stitch:
|
||||
stitch_out = args.out / f"hk_z{zoom}_stitched.jpg"
|
||||
stitch_tiles(tiles, x_range, y_range, zoom, stitch_out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user