summaryrefslogtreecommitdiff
path: root/tools/sky/build-milkyway.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/sky/build-milkyway.py')
-rw-r--r--tools/sky/build-milkyway.py41
1 files changed, 41 insertions, 0 deletions
diff --git a/tools/sky/build-milkyway.py b/tools/sky/build-milkyway.py
new file mode 100644
index 0000000..c9a63bd
--- /dev/null
+++ b/tools/sky/build-milkyway.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+# Turns NASA SVS's "Deep Star Maps 2020" Milky Way background (the star map
+# without its Hipparcos and Tycho stars, which the sky draws itself) into the
+# sRGB JPEG the sky component maps onto the celestial sphere.
+#
+# curl -O https://svs.gsfc.nasa.gov/vis/a000000/a004800/a004851/milkyway_2020_4k.exr
+# python3 tools/sky/build-milkyway.py milkyway_2020_4k.exr public/assets/sky/milkyway-4k.jpg
+#
+# Needs vips (to read the OpenEXR), numpy and Pillow. The map is plate carrée
+# in J2000 right ascension and declination, centred on 0h with r.a. increasing
+# to the left; this keeps that layout. Values stay linear in brightness up to
+# WHITE (only the heart of the Sagittarius star cloud goes past it), and are
+# stored with the sRGB curve so the faint parts keep their detail in 8 bits.
+import os
+import subprocess
+import sys
+import tempfile
+
+import numpy as np
+from PIL import Image
+
+WHITE = 0.6
+
+
+def main(src, dst):
+ width, height = (int(subprocess.check_output(['vipsheader', '-f', f, src], stderr=subprocess.DEVNULL)) for f in ('width', 'height'))
+ with tempfile.TemporaryDirectory() as tmp:
+ raw = os.path.join(tmp, 'map.raw')
+ subprocess.run(['vips', 'rawsave', src, raw], check=True, stderr=subprocess.DEVNULL)
+ rgb = np.fromfile(raw, dtype=np.float32).reshape(height, width, -1)[..., :3]
+ x = np.clip(rgb / WHITE, 0.0, 1.0)
+ srgb = np.where(x <= 0.0031308, 12.92 * x, 1.055 * np.power(x, 1 / 2.4) - 0.055)
+ # A little noise before rounding keeps the dark gradients from banding.
+ noise = np.random.default_rng(1).uniform(-0.5, 0.5, srgb.shape)
+ out = np.clip(np.round(srgb * 255 + noise), 0, 255).astype(np.uint8)
+ Image.fromarray(out, 'RGB').save(dst, quality=88, optimize=True, progressive=True)
+ print(f'{width}x{height} -> {dst} ({os.path.getsize(dst) // 1024} KB)')
+
+
+if __name__ == '__main__':
+ main(sys.argv[1], sys.argv[2])