mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
A deterministic WebGL film of God's Eye View coming online: a seed on Null Island grows into a culture that draws the planet, indexes the surface tile by tile, ingests the live feeds and outgrows its dish, ending on the logo. Renders 16:9 and 9:16 frame by frame in headless Chromium; the procedural score is locked to the same clock. Data and imagery are fetched by the scripts in tools/ and are not committed. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KqP857pippaUs6konSDfe8
77 lines
3.0 KiB
Python
77 lines
3.0 KiB
Python
"""Deliverable 3: clouds.jpg (4096x2048) grayscale cloud opacity map for one recent day.
|
|
|
|
Source: NASA GIBS daily VIIRS Corrected Reflectance True Colour for the same UTC
|
|
date from three satellites (NOAA-20, NOAA-21, Suomi NPP), EPSG:4326 level 3
|
|
(10x5 tiles of 512px = 5120x2560), downsampled to 4096x2048.
|
|
|
|
Cloud metric per satellite:
|
|
whiteness = min(R,G,B) (clouds are bright in every channel)
|
|
greyness = 1 - saturation (clouds are colourless)
|
|
c = ramp(whiteness) * smoothstep(greyness)
|
|
The same metric computed on the Esri base map (earth_day_4k.jpg) is subtracted
|
|
to suppress bright deserts, salt flats, snow and ice that would otherwise read
|
|
as cloud. The three per-satellite results are combined with a per-pixel median
|
|
(nan-aware), which removes swath-edge haze / glint streaks that only one
|
|
satellite shows, and fills swath gaps. Remaining no-data (polar night) -> 0.
|
|
|
|
Usage: python3 build_clouds.py [YYYY-MM-DD]
|
|
"""
|
|
import os
|
|
import sys
|
|
import numpy as np
|
|
from PIL import Image
|
|
from common import OUT, gibs_mosaic
|
|
|
|
Image.MAX_IMAGE_PIXELS = None
|
|
DATE = sys.argv[1] if len(sys.argv) > 1 else "2026-10-15"
|
|
LAYERS = [
|
|
"VIIRS_NOAA20_CorrectedReflectance_TrueColor",
|
|
"VIIRS_NOAA21_CorrectedReflectance_TrueColor",
|
|
"VIIRS_SNPP_CorrectedReflectance_TrueColor",
|
|
]
|
|
W, H = 4096, 2048
|
|
|
|
|
|
def smooth(x, a, b):
|
|
t = np.clip((x - a) / (b - a), 0, 1)
|
|
return t * t * (3 - 2 * t)
|
|
|
|
|
|
def metric(rgb):
|
|
rgb = rgb.astype(np.float32) / 255.0
|
|
mn = rgb.min(2)
|
|
mx = rgb.max(2)
|
|
sat = (mx - mn) / (mx + 1e-4)
|
|
white = np.clip((mn - 0.14) / 0.86, 0, 1) ** 1.15 # linear-ish: keeps texture inside thick cloud
|
|
return white * smooth(1.0 - sat, 0.65, 0.95), mx
|
|
|
|
|
|
def main():
|
|
base = np.asarray(Image.open(os.path.join(OUT, "earth_day_4k.jpg")).convert("RGB"))
|
|
base_c, _ = metric(base)
|
|
# dilate the base metric a few px so small Esri/VIIRS coastline misregistration
|
|
# does not leave bright outlines along desert and ice coasts
|
|
from PIL import ImageFilter
|
|
base_c = np.asarray(Image.fromarray((base_c * 255).astype(np.uint8)).filter(ImageFilter.MaxFilter(3)).filter(ImageFilter.GaussianBlur(2))).astype(np.float32) / 255
|
|
stack = []
|
|
for L in LAYERS:
|
|
m = gibs_mosaic(L, DATE, "250m", 3, 5, 10, "jpg").resize((W, H), Image.LANCZOS)
|
|
c, mx = metric(np.asarray(m))
|
|
c = np.clip(c - base_c * 1.1, 0, 1)
|
|
c[mx < 0.03] = np.nan # no-data (black)
|
|
stack.append(c)
|
|
stack = np.stack(stack)
|
|
with np.errstate(all="ignore"):
|
|
import warnings
|
|
warnings.simplefilter("ignore")
|
|
cloud = np.nanmedian(stack, axis=0)
|
|
cloud = np.nan_to_num(cloud, nan=0.0)
|
|
cloud = np.clip((cloud - 0.06) / 0.94, 0, 1) # black point: kill faint surface/haze residue
|
|
img = Image.fromarray(np.clip(cloud * 255 + 0.5, 0, 255).astype(np.uint8), "L")
|
|
img.save(os.path.join(OUT, "clouds.jpg"), quality=92, optimize=True, progressive=True)
|
|
print("done", DATE, "mean opacity %.3f" % cloud.mean())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|