mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
- City engine: Esri ground, OSM buildings that rise with the scan, road ribbons carrying modelled traffic coloured by congestion, NYC DOT cameras, real ADS-B, MBTA GTFS lines + realtime vehicles, LAX runways with approach strobes, Fort Bragg restricted volumes, Everest terrain with contours. - Indexing keeps accelerating into a 260-capture global sweep while the planet whips a full turn. - The eye opens: aperture blades, sensor array waking, optics calibration, radar sweep, orbiting coordinate ring. - Camera wall with YOLOv8n detections and a city-pulse trace. - Ends on GOD'S EYE VIEW / OMNISCIENCE. - audio/sfx.py: sound design only, built to sit under a music track. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KqP857pippaUs6konSDfe8
176 lines
7.9 KiB
Python
176 lines
7.9 KiB
Python
"""Build the flurry montage captures.
|
|
|
|
python3 select_cities.py # -> city_candidates.json (primary + backup per region)
|
|
python3 build.py # -> imagery/flurry/fx_###.jpg, index.json, tools/flurry/sheet.jpg
|
|
|
|
Each capture: Esri World Imagery, 3x3 tiles around the point, centred 512x512
|
|
crop, JPEG q86. Placeholder / low-variance captures are rejected automatically
|
|
(tiles.quality_ok); captures that passed the metrics but looked wrong on the
|
|
review sheets (forest, farmland, haze) are listed in VISUAL_REJECT. Rejected
|
|
items are replaced by the next backup of the same region (cities) or by the
|
|
next backup landmark.
|
|
"""
|
|
import glob
|
|
import json
|
|
import os
|
|
import sys
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
from PIL import Image
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from landmarks import LANDMARKS # noqa: E402
|
|
from tiles import HERE, MAX_WORKERS, OUT, capture, quality_ok # noqa: E402
|
|
|
|
N_CITIES = 170
|
|
N_LANDMARKS = 90
|
|
|
|
# Natural Earth points that landed on a park/water/cemetery: moved to the dense centre.
|
|
CITY_OVERRIDES = {
|
|
"Lagos": (6.4541, 3.3947), # Lagos Island (was open lagoon)
|
|
"Cairns": (-16.9230, 145.7740), # CBD (was mangrove mudflat)
|
|
"Calgary": (51.0460, -114.0650), # downtown (was cemetery)
|
|
"Dubai": (25.0805, 55.1403), # Dubai Marina (was construction lot)
|
|
"Perth": (-31.9530, 115.8575), # CBD (was Kings Park bush)
|
|
"Dhaka": (23.7330, 90.4172), # Motijheel (was hazy open ground)
|
|
"Delhi": (28.6315, 77.2167), # Connaught Place
|
|
"Montréal": (45.5017, -73.5673), # downtown (was Mount Royal park)
|
|
"Busan": (35.1000, 129.0350), # Nampo / harbour (was forested hill)
|
|
"Jeddah": (21.4858, 39.1925), # Al-Balad old town (was empty lot)
|
|
"Tokyo": (35.6717, 139.7650), # Ginza (was Imperial Palace woods)
|
|
}
|
|
VISUAL_REJECT = {
|
|
"Venice": "blurry low-res imagery",
|
|
"Conakry": "heavy haze",
|
|
"Maracaibo": "farmland / scrub",
|
|
"Barquisimeto": "dry riverbed, no city",
|
|
"Natal": "solid forest canopy",
|
|
"João Pessoa": "forest canopy",
|
|
"Mbuji-Mayi": "forest/scrub",
|
|
"Huambo": "scrub, sparse",
|
|
"Port Harcourt": "haze",
|
|
"Qaraghandy": "dark featureless rail yard",
|
|
"Herat": "low-res blur",
|
|
"Chengdu": "smog haze",
|
|
"Xian": "washed-out industrial lot",
|
|
"Kaohsiung": "forested hill",
|
|
"Sapporo": "university farm fields",
|
|
"Isfahan": "low contrast blur",
|
|
"Stuttgart": "hazy woodland",
|
|
"Townsville": "airport apron, not city",
|
|
}
|
|
LANDMARK_BACKUPS = [
|
|
"Glen Canyon Dam", "Grand Coulee Dam", "Mount St. Helens", "Groom Lake", "Diavik Diamond Mine",
|
|
"Syncrude Mildred Lake oil sands", "Centre-pivot fields, Kansas", "Indianapolis Motor Speedway",
|
|
"Brooklyn Bridge", "Panama Canal, Miraflores Locks", "Escondida Mine", "ALMA Observatory",
|
|
"Meeting of the Waters, Manaus", "Brasilia, National Congress", "PS10 / PS20 Solar Towers", "Tower Bridge",
|
|
"Port of Tema", "Akosombo Dam", "Okavango Delta", "FNB Stadium, Soweto", "East Oweinat pivots",
|
|
"Kilimanjaro, Kibo crater", "Ras Laffan Industrial City", "Dubai International Airport", "Bagan temples",
|
|
"Jiuquan Launch Centre", "Mount Bromo", "Vostochny Cosmodrome", "Pine Gap", "Tanegashima Space Center",
|
|
"Teide", "Faa'a International Airport", "Hartsfield-Jackson Atlanta Airport", "Niagara Horseshoe Falls",
|
|
"Iguazu Falls, Devil's Throat", "Vandenberg SLC-4E", "Paranal Observatory", "Cerro Dominador Solar",
|
|
"Gibraltar Airport runway", "Erg Chebbi dunes", "Millau Viaduct", "Garzweiler Lignite Mine", "Inga Dams",
|
|
"Grand Ethiopian Renaissance Dam", "Ras Tanura refinery", "Udachnaya Pipe", "Busan New Port",
|
|
"Mount Taranaki", "Beijing National Stadium", "Port of Hamburg, Burchardkai", "Port of Santos",
|
|
"Jebel Ali Port", "Mont-Saint-Michel", "Angkor Wat", "Mount Vesuvius", "Horseshoe Bend",
|
|
"Benban Solar Park", "Borobudur", "Bhadla Solar Park", "Kariba Dam", "Wake Island Airfield",
|
|
"Diamond Head", "Port Hedland salt ponds",
|
|
]
|
|
|
|
|
|
def evaluate(cands):
|
|
def job(c):
|
|
try:
|
|
img, info = capture(c["lat"], c["lon"], c["zoom"])
|
|
ok, why = quality_ok(info)
|
|
except Exception as e: # network failure counts as a reject
|
|
img, info, ok, why = None, {}, False, f"fetch error: {e}"
|
|
if ok and c["name"] in VISUAL_REJECT and c.get("kind") == "city":
|
|
ok, why = False, "visual: " + VISUAL_REJECT[c["name"]]
|
|
return img, ok, why
|
|
|
|
with ThreadPoolExecutor(MAX_WORKERS) as ex:
|
|
return list(ex.map(job, cands))
|
|
|
|
|
|
def pick(cands, results, quota_by_group):
|
|
"""Primaries first in order; failures replaced by the group's backups in order."""
|
|
chosen, rejects = [], []
|
|
for g, quota in quota_by_group.items():
|
|
items = [(c, r) for c, r in zip(cands, results) if c["group"] == g]
|
|
prim = [x for x in items if x[0]["rank"] == "primary"]
|
|
back = [x for x in items if x[0]["rank"] == "backup"]
|
|
got = []
|
|
for c, r in prim + back:
|
|
if len(got) >= quota:
|
|
break
|
|
if r[1]:
|
|
got.append((c, r[0]))
|
|
elif c["rank"] == "primary":
|
|
rejects.append((c, r[2]))
|
|
if len(got) < quota:
|
|
print(f"!! group {g}: only {len(got)}/{quota}")
|
|
chosen += got
|
|
return chosen, rejects
|
|
|
|
|
|
def main():
|
|
cities = json.load(open(os.path.join(HERE, "city_candidates.json")))
|
|
for c in cities:
|
|
c["kind"], c["group"] = "city", "c:" + c["region"]
|
|
if c["name"] in CITY_OVERRIDES:
|
|
c["lat"], c["lon"] = CITY_OVERRIDES[c["name"]]
|
|
cquota = {}
|
|
for c in cities:
|
|
if c["rank"] == "primary":
|
|
cquota[c["group"]] = cquota.get(c["group"], 0) + 1
|
|
assert sum(cquota.values()) == N_CITIES, cquota
|
|
|
|
lms = [dict(name=n, country=co, lat=la, lon=lo, zoom=z, kind="landmark", group="landmark",
|
|
rank="backup" if n in LANDMARK_BACKUPS else "primary") for n, co, la, lo, z in LANDMARKS]
|
|
lms.sort(key=lambda c: c["rank"] != "primary") # stable: primaries first
|
|
nprim = sum(c["rank"] == "primary" for c in lms)
|
|
assert nprim == N_LANDMARKS, nprim
|
|
|
|
allc = cities + lms
|
|
print(f"evaluating {len(allc)} candidates ...", flush=True)
|
|
res = evaluate(allc)
|
|
quotas = dict(cquota)
|
|
quotas["landmark"] = N_LANDMARKS
|
|
chosen, rejects = pick(allc, res, quotas)
|
|
|
|
chosen.sort(key=lambda x: x[0]["lon"])
|
|
os.makedirs(OUT, exist_ok=True)
|
|
for f in glob.glob(os.path.join(OUT, "fx_*.jpg")):
|
|
os.remove(f)
|
|
index = []
|
|
for i, (c, img) in enumerate(chosen):
|
|
fn = f"fx_{i:03d}.jpg"
|
|
img.save(os.path.join(OUT, fn), quality=86, optimize=True)
|
|
index.append({"file": fn, "name": c["name"], "country": c["country"], "lat": round(c["lat"], 5),
|
|
"lon": round(c["lon"], 5), "zoom": c["zoom"], "kind": c["kind"]})
|
|
with open(os.path.join(OUT, "index.json"), "w") as f:
|
|
json.dump(index, f, indent=1, ensure_ascii=False)
|
|
|
|
# contact sheet: 16 x 17 grid of 96px thumbs, west->east reading order
|
|
cols, cell = 16, 96
|
|
rows = (len(chosen) + cols - 1) // cols
|
|
sheet = Image.new("RGB", (cols * cell, rows * cell), (10, 10, 10))
|
|
for i, (c, img) in enumerate(chosen):
|
|
sheet.paste(img.resize((cell, cell), Image.LANCZOS), ((i % cols) * cell, (i // cols) * cell))
|
|
sheet.save(os.path.join(HERE, "sheet.jpg"), quality=88)
|
|
|
|
from collections import Counter
|
|
print(f"wrote {len(index)} captures: {Counter(x['kind'] for x in index)}")
|
|
print("rejected primaries (replaced by backups):")
|
|
for c, why in rejects:
|
|
print(f" - {c['name']} ({c['country']}): {why}")
|
|
bands = Counter(int((x["lon"] + 180) // 30) for x in index)
|
|
print("30-degree longitude bands W->E:", [bands.get(i, 0) for i in range(12)])
|
|
json.dump([{"name": c["name"], "country": c["country"], "reason": why} for c, why in rejects],
|
|
open(os.path.join(HERE, "rejects.json"), "w"), indent=1, ensure_ascii=False)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|