86 lines
3 KiB
Python
86 lines
3 KiB
Python
#!/usr/bin/env python3
|
|
"""Gera a textura galáctica de alta resolução usada pelo wallpaper.
|
|
|
|
Fonte: "Gaia's sky in colour" (ESA/Gaia/DPAC, CC BY-SA 3.0 IGO),
|
|
8000x4000 em projeção Hammer e coordenadas galácticas. A saída é plate
|
|
carrée, adequada para amostragem direta por longitude/latitude no canvas.
|
|
|
|
O processamento é feito em blocos para não manter várias grades 6K inteiras
|
|
na memória. Requer Pillow e numpy.
|
|
"""
|
|
|
|
import io
|
|
import math
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
|
|
URL = (
|
|
"https://upload.wikimedia.org/wikipedia/commons/e/ea/"
|
|
"Gaia%E2%80%99s_sky_in_colour_ESA393127.png"
|
|
)
|
|
CACHE = Path.home() / ".cache/skeledance/gaia_allsky.png"
|
|
OUT = Path(__file__).resolve().parent.parent / "plugin/contents/ui/milkyway.jpg"
|
|
SIZE = (6144, 3072)
|
|
BLOCK_ROWS = 96
|
|
QUALITY = 92
|
|
|
|
|
|
def fetch() -> bytes:
|
|
if CACHE.exists():
|
|
print(f"Usando cache {CACHE}")
|
|
return CACHE.read_bytes()
|
|
print(f"Baixando {URL} ...")
|
|
req = urllib.request.Request(URL, headers={"User-Agent": "skeledance-build/2.0"})
|
|
raw = urllib.request.urlopen(req, timeout=180).read()
|
|
CACHE.parent.mkdir(parents=True, exist_ok=True)
|
|
CACHE.write_bytes(raw)
|
|
print(f" {len(raw) / 1e6:.1f} MB (cacheado)")
|
|
return raw
|
|
|
|
|
|
def main() -> None:
|
|
Image.MAX_IMAGE_PIXELS = None
|
|
src = np.asarray(Image.open(io.BytesIO(fetch())).convert("RGB"))
|
|
sh, sw = src.shape[:2]
|
|
tw, th = SIZE
|
|
out = np.empty((th, tw, 3), dtype=np.uint8)
|
|
|
|
# Longitude cresce para a esquerda para coincidir com a imagem Gaia.
|
|
lon = np.deg2rad(180 - (np.arange(tw, dtype=np.float64) + 0.5) * 360 / tw)
|
|
|
|
for y_start in range(0, th, BLOCK_ROWS):
|
|
y_stop = min(th, y_start + BLOCK_ROWS)
|
|
lat = np.deg2rad(
|
|
90 - (np.arange(y_start, y_stop, dtype=np.float64) + 0.5) * 180 / th
|
|
)
|
|
lon_grid, lat_grid = np.meshgrid(lon, lat)
|
|
|
|
denom = np.sqrt(1 + np.cos(lat_grid) * np.cos(lon_grid / 2))
|
|
hx = 2 * math.sqrt(2) * np.cos(lat_grid) * np.sin(lon_grid / 2) / denom
|
|
hy = math.sqrt(2) * np.sin(lat_grid) / denom
|
|
sx = (1 - hx / (2 * math.sqrt(2))) * sw / 2
|
|
sy = (1 - hy / math.sqrt(2)) * sh / 2
|
|
|
|
# Amostragem bilinear preserva filamentos sem serrilhar a reprojeção.
|
|
x0 = np.clip(np.floor(sx).astype(np.int32), 0, sw - 1)
|
|
y0 = np.clip(np.floor(sy).astype(np.int32), 0, sh - 1)
|
|
x1 = np.minimum(x0 + 1, sw - 1)
|
|
y1 = np.minimum(y0 + 1, sh - 1)
|
|
fx = (sx - x0)[..., None]
|
|
fy = (sy - y0)[..., None]
|
|
top = src[y0, x0] * (1 - fx) + src[y0, x1] * fx
|
|
bottom = src[y1, x0] * (1 - fx) + src[y1, x1] * fx
|
|
out[y_start:y_stop] = np.clip(top * (1 - fy) + bottom * fy, 0, 255)
|
|
print(f"\rReprojetando: {y_stop * 100 // th:3d}%", end="", flush=True)
|
|
|
|
print()
|
|
Image.fromarray(out).save(OUT, "JPEG", quality=QUALITY, optimize=True)
|
|
print(f"{OUT} ({OUT.stat().st_size / 1024 / 1024:.1f} MB, {tw}x{th})")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|