feat(intro): v2 — cities, accelerating sweep, detections, thriller sound design

- 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
This commit is contained in:
Claude committed 2026-10-19 01:10:39 +00:00
1 parent e9ddc37788
commit 02e535cb55
30 files changed
+8921 -1442

No files matched your search

+175
View File
@@ -0,0 +1,175 @@
"""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()
File diff suppressed because it is too large. Load diff
+205
View File
@@ -0,0 +1,205 @@
"""Hand-picked non-city landmarks for the flurry montage.
(name, country, lat, lon, zoom). zoom 16 for compact features, 15 for very large
ones (deltas, glaciers, big pits, pivot fields). Coordinates are verified
visually via tools/flurry/review.py sheets; entries that came back as featureless
terrain were corrected or dropped (see REJECTED at the bottom).
"""
LANDMARKS = [
# --- ports / container terminals ---
("Port of Rotterdam, Maasvlakte", "Netherlands", 51.9575, 4.0500, 16),
("Port of Singapore, Pasir Panjang", "Singapore", 1.2735, 103.7750, 16),
("Yangshan Deep-Water Port", "China", 30.6280, 122.0650, 16),
("Port of Los Angeles, Pier 400", "United States of America", 33.7350, -118.2550, 16),
("Jebel Ali Port", "United Arab Emirates", 25.0100, 55.0600, 16),
("Port of Santos", "Brazil", -23.9700, -46.3000, 16),
("Port of Hamburg, Burchardkai", "Germany", 53.5350, 9.9200, 16),
("Port Hedland salt ponds", "Australia", -20.3450, 118.6550, 15),
("Busan New Port", "South Korea", 35.0800, 128.8000, 16),
("Tanger Med Port", "Morocco", 35.8850, -5.5000, 16),
("Port of Tema", "Ghana", 5.6330, 0.0100, 16),
# --- airports ---
("Hartsfield-Jackson Atlanta Airport", "United States of America", 33.6407, -84.4277, 16),
("Dubai International Airport", "United Arab Emirates", 25.2528, 55.3644, 16),
("Hong Kong International Airport", "Hong Kong S.A.R.", 22.3080, 113.9185, 16),
("Denver International Airport", "United States of America", 39.8494, -104.6738, 16),
("Kansai International Airport", "Japan", 34.4320, 135.2304, 16),
("Beijing Daxing Airport", "China", 39.5098, 116.4105, 16),
("Davis-Monthan Aircraft Boneyard", "United States of America", 32.1500, -110.8300, 16),
("Princess Juliana Airport", "Sint Maarten", 18.0410, -63.1089, 16),
("Gibraltar Airport runway", "Gibraltar", 36.1512, -5.3497, 16),
("Faa'a International Airport", "French Polynesia", -17.5537, -149.6067, 16),
("Funafuti Airfield", "Tuvalu", -8.5250, 179.1960, 16),
("Midway Atoll, Sand Island", "United States of America", 28.2100, -177.3800, 16),
("Wake Island Airfield", "United States of America", 19.2820, 166.6470, 16),
# --- dams ---
("Hoover Dam", "United States of America", 36.0160, -114.7377, 16),
("Three Gorges Dam", "China", 30.8233, 111.0036, 16),
("Itaipu Dam", "Brazil", -25.4085, -54.5888, 16),
("Grand Coulee Dam", "United States of America", 47.9560, -118.9817, 16),
("Kariba Dam", "Zimbabwe", -16.5222, 28.7614, 16),
("Aswan High Dam", "Egypt", 23.9707, 32.8780, 16),
("Grand Ethiopian Renaissance Dam", "Ethiopia", 11.2150, 35.0900, 16),
("Glen Canyon Dam", "United States of America", 36.9375, -111.4844, 16),
("Akosombo Dam", "Ghana", 6.2999, 0.0592, 16),
("Inga Dams", "Congo (Kinshasa)", -5.5240, 13.6200, 16),
# --- open-pit mines ---
("Bingham Canyon Mine", "United States of America", 40.5230, -112.1510, 15),
("Chuquicamata Mine", "Chile", -22.2900, -68.9000, 15),
("Mir Diamond Mine", "Russia", 62.5275, 113.9936, 15),
("Escondida Mine", "Chile", -24.2700, -69.0700, 15),
("Kalgoorlie Super Pit", "Australia", -30.7775, 121.5040, 15),
("Udachnaya Pipe", "Russia", 66.4300, 112.3200, 15),
("Garzweiler Lignite Mine", "Germany", 51.0600, 6.4800, 15),
("Kimberley Big Hole", "South Africa", -28.7383, 24.7585, 16),
("Diavik Diamond Mine", "Canada", 64.4960, -110.2700, 15),
("Syncrude Mildred Lake oil sands", "Canada", 57.0400, -111.6000, 15),
# --- solar ---
("Noor Ouarzazate Solar", "Morocco", 31.0498, -6.8686, 16),
("Ivanpah Solar", "United States of America", 35.5525, -115.4680, 16),
("Crescent Dunes Solar", "United States of America", 38.2386, -117.3636, 15),
("Bhadla Solar Park", "India", 27.5400, 71.9100, 16),
("Gemasolar", "Spain", 37.5606, -5.3317, 16),
("PS10 / PS20 Solar Towers", "Spain", 37.4425, -6.2550, 16),
("Benban Solar Park", "Egypt", 24.4500, 32.7300, 16),
("Cerro Dominador Solar", "Chile", -22.7700, -69.4800, 16),
# --- centre-pivot irrigation ---
("Centre-pivot fields, Kansas", "United States of America", 37.9000, -100.9000, 15),
("Centre-pivot fields, Wadi As-Sirhan", "Saudi Arabia", 30.2000, 38.0800, 15),
("Kufra agricultural project", "Libya", 24.1400, 23.3500, 15),
("East Oweinat pivots", "Egypt", 22.6000, 28.6000, 15),
("Centre-pivot fields, Nebraska", "United States of America", 41.1000, -101.7000, 15),
# --- river deltas / water ---
("Lena Delta", "Russia", 72.8000, 126.5000, 15),
("Okavango Delta", "Botswana", -19.3000, 22.9000, 15),
("Betsiboka Estuary", "Madagascar", -15.7500, 46.3500, 15),
("Meeting of the Waters, Manaus", "Brazil", -3.1370, -59.9100, 15),
# --- salt / evaporation ---
("Salar de Atacama lithium ponds", "Chile", -23.5280, -68.3680, 16),
("Dead Sea evaporation ponds", "Israel", 31.1000, 35.4000, 15),
("San Francisco Bay salt ponds", "United States of America", 37.5000, -122.0500, 15),
("Lake Retba (Pink Lake)", "Senegal", 14.8400, -17.2300, 15),
("Hutt Lagoon", "Australia", -28.1700, 114.2500, 15),
# --- glaciers / ice ---
("Perito Moreno Glacier", "Argentina", -50.4800, -73.0500, 15),
("Aletsch Glacier, Konkordiaplatz", "Switzerland", 46.5000, 8.0300, 15),
("Hubbard Glacier", "United States of America", 60.0200, -139.5000, 15),
# Antarctica: Esri has only Landsat-class imagery here (z14+ is placeholder),
# so these use z11-12 where the source pixels are native rather than upsampled.
("Taylor Valley, Canada Glacier", "Antarctica", -77.6300, 162.9500, 12),
("McMurdo Dry Valleys, Wright Valley", "Antarctica", -77.5700, 161.8500, 11),
("Deception Island caldera", "Antarctica", -62.9600, -60.6300, 12),
("Mount Everest", "Nepal", 27.9881, 86.9250, 15),
# --- volcanoes / craters ---
("Mount Bromo", "Indonesia", -7.9420, 112.9530, 15),
("Erta Ale", "Ethiopia", 13.6065, 40.6655, 16),
("Kilauea, Halemaumau", "United States of America", 19.4060, -155.2830, 15),
("Mount Fuji summit crater", "Japan", 35.3628, 138.7307, 16),
("Nyiragongo", "Congo (Kinshasa)", -1.5200, 29.2500, 16),
("Meteor Crater", "United States of America", 35.0275, -111.0225, 15),
("Mount St. Helens", "United States of America", 46.2000, -122.1900, 15),
("Kawah Ijen", "Indonesia", -8.0580, 114.2420, 16),
("Teide", "Spain", 28.2725, -16.6425, 16),
("Mount Vesuvius", "Italy", 40.8216, 14.4263, 16),
("Rano Kau, Easter Island", "Chile", -27.1860, -109.4350, 15),
("Diamond Head", "United States of America", 21.2620, -157.8050, 15),
("Darvaza Gas Crater", "Turkmenistan", 40.2525, 58.4394, 16),
("Kilimanjaro, Kibo crater", "Tanzania", -3.0758, 37.3533, 15),
("Mount Taranaki", "New Zealand", -39.2968, 174.0634, 15),
("Whakaari / White Island", "New Zealand", -37.5200, 177.1800, 15),
("Dallol", "Ethiopia", 14.2400, 40.3000, 16),
# --- stadiums / venues ---
("Maracana Stadium", "Brazil", -22.9122, -43.2302, 16),
("Rungrado May Day Stadium", "North Korea", 39.0497, 125.7756, 16),
("Camp Nou", "Spain", 41.3809, 2.1228, 16),
("Beijing National Stadium", "China", 39.9925, 116.3910, 16),
("Melbourne Cricket Ground", "Australia", -37.8200, 144.9834, 16),
("Indianapolis Motor Speedway", "United States of America", 39.7950, -86.2350, 15),
("Estadio Azteca", "Mexico", 19.3029, -99.1505, 16),
("FNB Stadium, Soweto", "South Africa", -26.2348, 27.9825, 16),
# --- bridges / canals ---
("Golden Gate Bridge", "United States of America", 37.8100, -122.4770, 16),
("Sydney Harbour Bridge & Opera House", "Australia", -33.8550, 151.2120, 16),
("Tower Bridge", "United Kingdom", 51.5055, -0.0754, 16),
("Oresund Bridge, Peberholm", "Denmark", 55.5900, 12.7740, 16),
("Millau Viaduct", "France", 44.0794, 3.0225, 16),
("Brooklyn Bridge", "United States of America", 40.7061, -73.9969, 16),
("Panama Canal, Miraflores Locks", "Panama", 8.9970, -79.5915, 16),
("Panama Canal, Agua Clara Locks", "Panama", 9.2720, -79.9180, 16),
# --- launch sites ---
("Kennedy Space Center LC-39A", "United States of America", 28.6083, -80.6041, 16),
("Baikonur, Gagarin's Start", "Kazakhstan", 45.9203, 63.3422, 16),
("Vandenberg SLC-4E", "United States of America", 34.6320, -120.6110, 16),
("Starbase, Boca Chica", "United States of America", 25.9972, -97.1566, 16),
("Wenchang Launch Site", "China", 19.6145, 110.9510, 16),
("Jiuquan Launch Centre", "China", 40.9580, 100.2910, 16),
("Tanegashima Space Center", "Japan", 30.4009, 130.9749, 16),
("Vostochny Cosmodrome", "Russia", 51.8840, 128.3330, 16),
("Rocket Lab LC-1, Mahia", "New Zealand", -39.2615, 177.8649, 16),
# --- energy / industry ---
("Permian Basin well pads", "United States of America", 32.2100, -103.8300, 15),
("Ras Tanura refinery", "Saudi Arabia", 26.6400, 50.1600, 16),
("Jamnagar Refinery", "India", 22.3500, 69.8500, 16),
("Ras Laffan Industrial City", "Qatar", 25.9100, 51.5500, 16),
("Yamal LNG, Sabetta", "Russia", 71.2700, 72.0700, 16),
("Chernobyl Nuclear Power Plant", "Ukraine", 51.3890, 30.0990, 16),
# --- monuments / sites ---
("Jiayuguan Pass, Great Wall", "China", 39.8016, 98.2133, 16),
("Pyramids of Giza", "Egypt", 29.9792, 31.1342, 16),
("Palm Jumeirah", "United Arab Emirates", 25.1124, 55.1390, 15),
("Angkor Wat", "Cambodia", 13.4125, 103.8670, 16),
("The Pentagon", "United States of America", 38.8719, -77.0563, 16),
("Groom Lake", "United States of America", 37.2350, -115.8111, 15),
("FAST Radio Telescope", "China", 25.6529, 106.8566, 16),
("Very Large Array", "United States of America", 34.0784, -107.6184, 16),
("Uluru", "Australia", -25.3444, 131.0369, 15),
("Mont-Saint-Michel", "France", 48.6361, -1.5115, 16),
("Grand Prismatic Spring", "United States of America", 44.5251, -110.8382, 16),
("Masjid al-Haram, Mecca", "Saudi Arabia", 21.4225, 39.8262, 16),
("Forbidden City", "China", 39.9155, 116.3910, 16),
("Taj Mahal", "India", 27.1751, 78.0421, 16),
("Greenhouses of Almeria", "Spain", 36.7500, -2.7200, 16),
("Pine Gap", "Australia", -23.7990, 133.7370, 16),
("ALMA Observatory", "Chile", -23.0229, -67.7552, 16),
("Paranal Observatory", "Chile", -24.6275, -70.4044, 16),
("Brasilia, National Congress", "Brazil", -15.7998, -47.8645, 16),
("Iguazu Falls, Devil's Throat", "Argentina", -25.6950, -54.4367, 16),
("Niagara Horseshoe Falls", "Canada", 43.0773, -79.0757, 16),
("Victoria Falls", "Zambia", -17.9243, 25.8572, 16),
("Horseshoe Bend", "United States of America", 36.8791, -111.5103, 16),
("Sossusvlei dunes", "Namibia", -24.7300, 15.2900, 15),
("Erg Chebbi dunes", "Morocco", 31.1500, -3.9800, 15),
("Bora Bora", "French Polynesia", -16.4950, -151.7050, 15),
("Borobudur", "Indonesia", -7.6079, 110.2038, 16),
("Bagan temples", "Myanmar", 21.1700, 94.8600, 16),
("Svalbard, Longyearbyen", "Norway", 78.2200, 15.6500, 16),
("Edinburgh of the Seven Seas, Tristan da Cunha", "United Kingdom", -37.0670, -12.3100, 16),
("Diego Garcia", "United Kingdom", -7.3100, 72.4100, 15),
("Kwajalein Atoll", "Marshall Islands", 8.7200, 167.7300, 16),
]
REJECTED = {
'Pituffik Space Base': 'placeholder above z13; z13 too blurry',
'Ilulissat Icefjord': 'placeholder above z13; open water at z13',
'McMurdo Station': 'placeholder above z13; blurry ice at z13',
'Rothera Research Station': 'placeholder above z13; mostly sea',
'Mount Erebus crater': 'placeholder above z13; cloud-like blur',
'Port of Durban': "featureless/cloud/low-res/off-target at tested coords",
'Port of Mombasa': "featureless/cloud/low-res/off-target at tested coords",
'Sundarbans, Ganges Delta': "featureless/cloud/low-res/off-target at tested coords",
"Mississippi Bird's-Foot Delta": "featureless/cloud/low-res/off-target at tested coords",
'Volga Delta': "featureless/cloud/low-res/off-target at tested coords",
'Great Salt Lake causeway': "featureless/cloud/low-res/off-target at tested coords",
'Jokulsarlon Glacier Lagoon': "featureless/cloud/low-res/off-target at tested coords",
'Baltoro Glacier, Concordia': "featureless/cloud/low-res/off-target at tested coords",
'Cotopaxi': "featureless/cloud/low-res/off-target at tested coords",
'Guiana Space Centre ELA-3': "featureless/cloud/low-res/off-target at tested coords",
'Satish Dhawan Space Centre': "featureless/cloud/low-res/off-target at tested coords",
'Abqaiq processing plant': "featureless/cloud/low-res/off-target at tested coords",
'Prudhoe Bay oil field': "featureless/cloud/low-res/off-target at tested coords",
'Great Wall at Mutianyu': "featureless/cloud/low-res/off-target at tested coords",
'Machu Picchu': "featureless/cloud/low-res/off-target at tested coords",
'Baltra Island airport, Galapagos': "featureless/cloud/low-res/off-target at tested coords",
'Moynaq ship graveyard, Aral Sea': "featureless/cloud/low-res/off-target at tested coords",
}
+78
View File
@@ -0,0 +1,78 @@
"""Locator sheets: a wider mosaic around a guess with a lat/lon grid, to find exact coords.
usage: python3 locate.py out.jpg "name|lat|lon|z|ntiles" ...
Each panel is ntiles x ntiles tiles at zoom z (resized to 640px) with grid
lines every `step` degrees, labelled. Panels are stacked in a 2-column grid.
"""
import io
import math
import os
import sys
from concurrent.futures import ThreadPoolExecutor
from PIL import Image, ImageDraw
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from tiles import fetch_tile, pixel_xy # noqa: E402
P = 640
def panel(name, lat, lon, z, nt):
px, py = pixel_xy(lat, lon, z)
tx, ty = int(px // 256), int(py // 256)
h = nt // 2
coords = [(tx - h + i, ty - h + j) for j in range(nt) for i in range(nt)]
with ThreadPoolExecutor(8) as ex:
datas = list(ex.map(lambda c: fetch_tile(z, c[0], c[1]), coords))
m = Image.new("RGB", (nt * 256, nt * 256))
for (x, y), d in zip(coords, datas):
m.paste(Image.open(io.BytesIO(d)).convert("RGB"), ((x - tx + h) * 256, (y - ty + h) * 256))
ox, oy = (tx - h) * 256, (ty - h) * 256
s = P / (nt * 256)
m = m.resize((P, P), Image.LANCZOS)
d = ImageDraw.Draw(m)
n = 2 ** z * 256
def lon2x(lo):
return ((lo + 180) / 360 * n - ox) * s
def lat2y(la):
si = math.sin(math.radians(la))
return ((0.5 - math.log((1 + si) / (1 - si)) / (4 * math.pi)) * n - oy) * s
span = nt * 256 / n * 360
step = [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5][
min(range(9), key=lambda k: abs(span / [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5][k] - 6))]
lo0 = math.floor((lon - span) / step) * step
for k in range(40):
lo = lo0 + k * step
x = lon2x(lo)
if 0 <= x <= P:
d.line((x, 0, x, P), fill=(255, 255, 0), width=1)
d.text((x + 2, P - 12), f"{lo:.3f}", fill=(255, 255, 0))
la0 = math.floor((lat - span) / step) * step
for k in range(40):
la = la0 + k * step
y = lat2y(la)
if 0 <= y <= P:
d.line((0, y, P, y), fill=(0, 255, 255), width=1)
d.text((2, y + 1), f"{la:.3f}", fill=(0, 255, 255))
cx, cy = lon2x(lon), lat2y(lat)
d.ellipse((cx - 5, cy - 5, cx + 5, cy + 5), outline=(255, 0, 0), width=2)
d.rectangle((0, 0, P, 14), fill=(0, 0, 0))
d.text((3, 1), f"{name} z{z}", fill=(255, 255, 255))
return m
if __name__ == "__main__":
out = sys.argv[1]
specs = [a.split("|") for a in sys.argv[2:]]
ps = [panel(s[0], float(s[1]), float(s[2]), int(s[3]), int(s[4])) for s in specs]
cols = 2 if len(ps) > 1 else 1
rows = (len(ps) + cols - 1) // cols
sheet = Image.new("RGB", (cols * P, rows * P))
for i, p in enumerate(ps):
sheet.paste(p, ((i % cols) * P, (i // cols) * P))
sheet.save(out, quality=85)
print(out)
+32
View File
@@ -0,0 +1,32 @@
[
{
"name": "Venice",
"country": "Italy",
"reason": "visual: blurry low-res imagery"
},
{
"name": "Chengdu",
"country": "China",
"reason": "visual: smog haze"
},
{
"name": "Xian",
"country": "China",
"reason": "visual: washed-out industrial lot"
},
{
"name": "Maracaibo",
"country": "Venezuela",
"reason": "visual: farmland / scrub"
},
{
"name": "Barquisimeto",
"country": "Venezuela",
"reason": "visual: dry riverbed, no city"
},
{
"name": "Conakry",
"country": "Guinea",
"reason": "visual: heavy haze"
}
]
+69
View File
@@ -0,0 +1,69 @@
"""Fetch candidates and render labelled review sheets for visual verification.
usage: python3 review.py landmarks|cities [start] [count]
Writes tools/flurry/review/<kind>_NN.jpg (5x4 grid, 256px cells, labelled).
"""
import json
import os
import sys
from concurrent.futures import ThreadPoolExecutor
from PIL import Image, ImageDraw
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from tiles import HERE, MAX_WORKERS, capture, quality_ok # noqa: E402
REV = os.path.join(HERE, "review")
def load(kind):
if kind == "landmarks":
from landmarks import LANDMARKS
return [dict(name=n, country=c, lat=la, lon=lo, zoom=z) for n, c, la, lo, z in LANDMARKS]
return json.load(open(os.path.join(HERE, "city_candidates.json")))
def run(cands, prefix, cell=256, cols=5, rows=4):
os.makedirs(REV, exist_ok=True)
def job(c):
try:
img, info = capture(c["lat"], c["lon"], c["zoom"])
return img, info
except Exception as e:
return None, {"error": str(e)}
with ThreadPoolExecutor(MAX_WORKERS) as ex:
res = list(ex.map(job, cands))
per = cols * rows
report = []
for s in range(0, len(cands), per):
sheet = Image.new("RGB", (cols * cell, rows * cell), (20, 20, 20))
d = ImageDraw.Draw(sheet)
for k, (c, (img, info)) in enumerate(zip(cands[s:s + per], res[s:s + per])):
x, y = (k % cols) * cell, (k // cols) * cell
idx = s + k
if img is not None:
sheet.paste(img.resize((cell, cell), Image.LANCZOS), (x, y))
ok, why = quality_ok(info)
lab2 = f"s{info['std']:.0f} g{info['grad']:.1f}" + ("" if ok else " X " + why[:18])
else:
ok, lab2 = False, "ERR"
d.rectangle((x, y, x + cell, y + 24), fill=(0, 0, 0))
d.text((x + 3, y + 1), f"{idx} {c['name'][:36]}", fill=(255, 255, 0))
d.text((x + 3, y + 12), lab2, fill=(255, 120, 120) if not ok else (160, 255, 160))
report.append((idx, c["name"], ok, lab2))
out = os.path.join(REV, f"{prefix}_{s // per:02d}.jpg")
sheet.save(out, quality=85)
print(out)
for r in report:
if not r[2]:
print("REJECT", r)
if __name__ == "__main__":
kind = sys.argv[1]
cands = load(kind)
st = int(sys.argv[2]) if len(sys.argv) > 2 else 0
n = int(sys.argv[3]) if len(sys.argv) > 3 else len(cands)
run(cands[st:st + n], kind[:2] + (f"_{st}" if st else ""))
@@ -0,0 +1,109 @@
"""Pick ~170 city centres from data/cities.json with geographic spread.
Writes tools/flurry/city_candidates.json: ordered list of dicts with
rank ("primary"/"backup") and region. Backups replace rejected primaries of the
same region in build.py.
"""
import json
import math
import os
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
AFRICA = {
"Nigeria", "Egypt", "Congo (Kinshasa)", "South Africa", "Algeria", "Sudan", "Morocco", "Tanzania",
"Cameroon", "Mozambique", "Libya", "Ethiopia", "Ivory Coast", "Tunisia", "Zambia", "Angola", "Ghana",
"Uganda", "Kenya", "Guinea", "Madagascar", "Senegal", "Zimbabwe", "Mali", "Somalia", "Chad", "Niger",
"Benin", "Eritrea", "Somaliland", "South Sudan", "Sierra Leone", "Mauritania", "Malawi", "Togo",
"Congo (Brazzaville)", "Burkina Faso", "Rwanda", "Mauritius", "Gabon", "Equatorial Guinea", "Liberia",
"Djibouti", "Central African Republic", "Guinea Bissau", "Lesotho", "Burundi", "Namibia", "Botswana",
"The Gambia", "Comoros", "Cape Verde", "eSwatini",
}
SOUTH_AMERICA = {"Brazil", "Argentina", "Colombia", "Venezuela", "Peru", "Chile", "Ecuador", "Bolivia",
"Paraguay", "Uruguay", "Guyana", "Suriname"}
OCEANIA = {"Australia", "New Zealand", "Papua New Guinea", "Fiji", "French Polynesia", "Guam"}
CENTRAL_ASIA = {"Kazakhstan", "Uzbekistan", "Turkmenistan", "Kyrgyzstan", "Tajikistan", "Afghanistan", "Mongolia"}
# (region, primary quota, backups, per-country cap, min spacing km)
PASSES = [
("africa", AFRICA, 31, 16, 3, 300),
("south_america", SOUTH_AMERICA, 22, 10, 5, 250),
("oceania", OCEANIA, 12, 4, 5, 120),
("central_asia", CENTRAL_ASIA, 9, 5, 3, 150),
("rest", None, 96, 40, 11, 200),
]
# Picked first (in "rest" unless their country is in a quota region): fill the
# Pacific / high-latitude longitude bands and add visually distinctive cities.
FORCED = [
"Honolulu", "Anchorage", "Papeete", "Agana", "Petropavlovsk-Kamchatsky", "Vancouver", "San Francisco",
"Las Vegas", "Seattle", "Phoenix", "Denver", "Calgary", "Guatemala City", "Havana", "Reykjavík",
"Dubai", "Doha", "Abu Dhabi", "Hong Kong", "Macau", "Venice", "Amsterdam", "København", "Oslo",
"Stockholm", "Helsinki", "Prague", "Kathmandu", "Colombo", "Vladivostok", "Novosibirsk", "Irkutsk",
"Yakutsk", "Norilsk", "Murmansk", "Manaus", "Mombasa", "Windhoek", "Nouakchott", "Marrakesh",
"Samarkand", "Cairns", "Muscat", "Québec",
]
# Natural Earth rows whose point lands in farmland/hills rather than a dense centre.
EXCLUDE = {"Amaravati", "Hechi", "Saidu", "Cranbourne", "Manukau", "Waitakere", "North Shore", "Wahiawa"}
COUNTRY_CAP_TOTAL = 12
def km(a, b):
la1, lo1, la2, lo2 = map(math.radians, (a[1], a[0], b[1], b[0]))
h = math.sin((la2 - la1) / 2) ** 2 + math.cos(la1) * math.cos(la2) * math.sin((lo2 - lo1) / 2) ** 2
return 6371 * 2 * math.asin(math.sqrt(h))
def main():
rows = json.load(open(os.path.join(ROOT, "data", "cities.json")))["rows"]
special = AFRICA | SOUTH_AMERICA | OCEANIA | CENTRAL_ASIA
chosen, out = [], []
byname = {}
for r in rows:
byname.setdefault(r[3], r) # first = most populous
per = {}
def region_of(country):
for reg, mem, *_ in PASSES:
if mem and country in mem:
return reg
return "rest"
for nm in FORCED:
lon, lat, pop, name, country = byname[nm]
chosen.append((lon, lat))
per[country] = per.get(country, 0) + 1
out.append(dict(name=name, country=country, lat=lat, lon=lon, zoom=16, pop=pop,
region=region_of(country), rank="primary", forced=True))
for region, members, quota, nback, cap, spacing in PASSES:
got = sum(1 for c in out if c["region"] == region)
for lon, lat, pop, name, country in rows:
if name in EXCLUDE or name in byname and any(c["name"] == name and c["country"] == country for c in out):
continue
inreg = (country in members) if members else (country not in special)
if not inreg:
continue
if per.get(country, 0) >= cap and got < quota or per.get(country, 0) >= COUNTRY_CAP_TOTAL:
continue
if any(km((lon, lat), (c[0], c[1])) < spacing for c in chosen):
continue
rank = "primary" if got < quota else "backup"
chosen.append((lon, lat))
per[country] = per.get(country, 0) + 1
out.append(dict(name=name, country=country, lat=lat, lon=lon, zoom=16, pop=pop,
region=region, rank=rank))
got += 1
if got >= quota + nback:
break
json.dump(out, open(os.path.join(HERE, "city_candidates.json"), "w"), indent=0, ensure_ascii=False)
prim = [c for c in out if c["rank"] == "primary"]
print(len(prim), "primary,", len(out) - len(prim), "backup")
from collections import Counter
print(Counter(c["region"] for c in prim))
print(Counter(c["country"] for c in prim).most_common(12))
bands = Counter(int((c["lon"] + 180) // 30) for c in prim)
print("30deg bands:", [bands.get(i, 0) for i in range(12)])
if __name__ == "__main__":
main()
+132
View File
@@ -0,0 +1,132 @@
"""Polite cached Esri World Imagery fetch + 512px centred capture + quality metrics."""
import hashlib
import io
import math
import os
import threading
import time
import numpy as np
import requests
from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
CACHE = os.path.join(HERE, "cache")
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
OUT = os.path.join(ROOT, "imagery", "flurry")
ESRI = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
UA = "gods-eye-view-intro/1.0 (flurry montage prep)"
MAX_WORKERS = 8
PLACEHOLDER_MD5 = {"f27d9de7f80c13501f470595e327aa6d"} # "Map data not yet available"
_local = threading.local()
def _session():
s = getattr(_local, "s", None)
if s is None:
s = requests.Session()
s.headers["User-Agent"] = UA
_local.s = s
return s
def fetch_tile(z, x, y, retries=6):
n = 2 ** z
x %= n
path = os.path.join(CACHE, str(z), str(x), f"{y}.jpg")
if os.path.exists(path) and os.path.getsize(path) > 0:
with open(path, "rb") as f:
return f.read()
os.makedirs(os.path.dirname(path), exist_ok=True)
url = ESRI.format(z=z, x=x, y=y)
last = None
for attempt in range(retries):
try:
r = _session().get(url, timeout=45)
if r.status_code == 200 and r.content:
tmp = path + f".{threading.get_ident()}.part"
with open(tmp, "wb") as f:
f.write(r.content)
os.replace(tmp, path)
return r.content
last = f"HTTP {r.status_code}"
if r.status_code in (400, 404):
break
except Exception as e:
last = repr(e)
time.sleep(min(20, 1.0 * 2 ** attempt))
raise RuntimeError(f"failed {url}: {last}")
def is_placeholder(data):
if hashlib.md5(data).hexdigest() in PLACEHOLDER_MD5:
return True
a = np.asarray(Image.open(io.BytesIO(data)).convert("RGB")).astype(np.float32)
m = a.mean(axis=(0, 1))
return bool(abs(m - 204.7).max() < 4 and a.std() < 9)
def pixel_xy(lat, lon, z):
n = 2 ** z * 256
px = (lon + 180.0) / 360.0 * n
s = math.sin(math.radians(lat))
py = (0.5 - math.log((1 + s) / (1 - s)) / (4 * math.pi)) * n
return px, py
def capture(lat, lon, z, size=512):
"""Return (image, info). info has placeholder flag and quality metrics."""
px, py = pixel_xy(lat, lon, z)
tx, ty = int(px // 256), int(py // 256)
mosaic = Image.new("RGB", (768, 768))
ox, oy = (tx - 1) * 256, (ty - 1) * 256
left, top = int(round(px - ox - size / 2)), int(round(py - oy - size / 2))
ph = False
for j in range(3):
for i in range(3):
data = fetch_tile(z, tx - 1 + i, ty - 1 + j)
# does this tile overlap the crop window?
tl, tt = i * 256, j * 256
overlaps = tl < left + size and tl + 256 > left and tt < top + size and tt + 256 > top
if overlaps and is_placeholder(data):
ph = True
mosaic.paste(Image.open(io.BytesIO(data)).convert("RGB"), (tl, tt))
img = mosaic.crop((left, top, left + size, top + size))
info = metrics(img)
info["placeholder"] = ph
return img, info
def metrics(img):
a = np.asarray(img).astype(np.float32)
lum = a @ np.array([0.299, 0.587, 0.114], np.float32)
small = np.asarray(img.convert("L").resize((128, 128), Image.BILINEAR)).astype(np.float32)
gx = np.abs(np.diff(lum, axis=1)).mean()
gy = np.abs(np.diff(lum, axis=0)).mean()
# coarse-scale structure: std of 32px block means (catches flat ocean/cloud/ice)
blocks = lum.reshape(16, 32, 16, 32).mean(axis=(1, 3))
sat = (a.max(axis=2) - a.min(axis=2)).mean()
return {
"std": float(lum.std()),
"grad": float((gx + gy) / 2),
"block_std": float(blocks.std()),
"small_std": float(small.std()),
"sat": float(sat),
"mean": float(lum.mean()),
}
def quality_ok(info):
if info["placeholder"]:
return False, "placeholder"
if info["std"] < 12:
return False, f"low variance (std {info['std']:.1f})"
if info["grad"] < 1.3:
return False, f"featureless (grad {info['grad']:.1f})"
if info["mean"] > 225 and info["std"] < 25:
return False, "cloud/snow white-out"
if info["mean"] < 22:
return False, "too dark"
return True, ""