The generated spot-and-ring drawing is replaced by one 0.2 deg rotation frame of the in-house lysozyme standard (EIGER2 16M, SLS X10SA): Bragg spots on their lunes, the beam stop and its arm, the module gaps. docs/landing/hero_frame.py makes the 920 px WebP (67 KB) from the master file - crop about the beam centre, log tone map from the frame's own background, block mean+max downsampling - and is run by hand; the WebP is committed and copied by the landing-page step. The dark theme shows it inverted through a CSS filter. The hero's command line drops its explanatory sentence and gains the second form, rugnux <diffraction image file> <CIF/PDB model file>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu (cherry picked from commit ff26000bfe7446cb4fda739c3b72596f8a8b7047)
59 lines
2.3 KiB
Python
59 lines
2.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Make the front page's hero image (docs/landing/hero_frame.webp) from one frame of an NXmx
|
|
HDF5 dataset. Not part of the documentation build: run it by hand when the image is to change,
|
|
and commit the WebP.
|
|
|
|
hero_frame.py <master.h5> [frame] [out.webp]
|
|
|
|
The frame is cropped to a square about the beam centre (from the master file), tone-mapped with
|
|
a log curve set from its own background level, and downsampled with a block mean+max blend so
|
|
the Bragg spots stay visible at a ninth of the size. Masked pixels (module gaps, bad pixels) are
|
|
drawn light grey. Dark spots on a light background; the page inverts it for the dark theme.
|
|
Needs numpy, h5py, hdf5plugin and Pillow.
|
|
"""
|
|
import sys
|
|
|
|
import h5py
|
|
import hdf5plugin # noqa: F401 - registers the bitshuffle/LZ4 filters for h5py
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
OUT_PX = 920 # twice the largest size the hero shows it at
|
|
CROP = 0.68 # fraction of the largest square about the beam centre: larger spots, fewer gaps
|
|
|
|
master = sys.argv[1]
|
|
frame = int(sys.argv[2]) if len(sys.argv) > 2 else 0
|
|
out = sys.argv[3] if len(sys.argv) > 3 else "hero_frame.webp"
|
|
|
|
with h5py.File(master, "r") as f:
|
|
data = f["/entry/data"]
|
|
dset = data["data"] if "data" in data else data["data_000001"]
|
|
img = dset[frame].astype(np.float64)
|
|
det = f["/entry/instrument/detector"]
|
|
cx, cy = float(det["beam_center_x"][()]), float(det["beam_center_y"][()])
|
|
|
|
bad = img >= 2**31 # masked and gap pixels carry the all-ones value
|
|
img[bad] = 0
|
|
|
|
# Square crop about the beam centre, a fraction of the largest the detector allows, in whole
|
|
# output pixels.
|
|
k = int(CROP * 2 * min(cx, cy, img.shape[1] - cx, img.shape[0] - cy)) // OUT_PX
|
|
half = k * OUT_PX // 2
|
|
x0, y0 = int(cx) - half, int(cy) - half
|
|
img = img[y0:y0 + 2 * half, x0:x0 + 2 * half]
|
|
bad = bad[y0:y0 + 2 * half, x0:x0 + 2 * half]
|
|
|
|
# Log tone map: the background sits at a light grey, spots at 20x background are black.
|
|
bkg = max(np.median(img[~bad]), 1.0)
|
|
t = np.clip(np.log1p(img / (1.2 * bkg)) / np.log1p(20.0), 0, 1)
|
|
|
|
n = OUT_PX
|
|
t = t.reshape(n, k, n, k)
|
|
gap = bad.reshape(n, k, n, k).mean(axis=(1, 3)) > 0.5
|
|
v = 0.3 * t.mean(axis=(1, 3)) + 0.7 * t.max(axis=(1, 3))
|
|
|
|
g = (255 * (1 - 0.92 * v)).astype(np.uint8)
|
|
g[gap] = 238
|
|
Image.fromarray(g).save(out, quality=82, method=6)
|
|
print(out, "beam centre", cx, cy, "crop", 2 * half, "px, block", k, "background", bkg)
|