Move the processing code under pipeline/

Preparing to merge this repository into a combined astrophotography
repo. session-scripts/ becomes pipeline/ because the scripts import
layout.py from their own directory and must stay together, and because
'pipeline' says what it is rather than how it came about. observing/
stays at the top level: observing plans are not processing code.
This commit is contained in:
laurence 2026-07-21 17:13:54 +01:00
parent 653ca103cd
commit c6299f41ab
53 changed files with 0 additions and 0 deletions

View file

@ -1,102 +0,0 @@
"""Image 4: the close-up, framed by the galaxy's own measured extent.
The crop box is not chosen by eye. The surface photometry produced an isophote
model of NGC 5128, and the region where that model carries real signal defines
how much sky the galaxy actually occupies; the frame is that region plus a
margin. So "keeping the object in frame" is a measured statement rather than a
judgement, and the same rule would frame any other galaxy without retuning.
It is cut from image 3's 16-bit TIFF rather than from the PNG, and never
resampled, so every pixel is exactly the pixel that came out of the pipeline.
Re-rendering was considered and rejected: the stretch in image 3 is already
anchored on sky statistics measured across the whole frame, and a tight crop
contains too little empty sky to measure a better one. Cropping is therefore
not a compromise here - it is the correct operation.
"""
import os
import numpy as np
import tifffile
from astropy.io import fits
from astropy.wcs import WCS
from PIL import Image
import layout
OUT = layout.SESSION
# The finished renderings live in their own directory: they are the
# deliverables, and keeping them apart from the masters, the
# intermediates and the analysis figures makes it obvious which
# files are meant to be looked at.
FINAL = layout.path("final")
SOURCE = "NGC5128-final-3-best.tif"
CROP = 48 # the border already removed from image 3
MARGIN = 0.08 # sky margin around the galaxy, as a fraction
MODEL_FLOOR = 5.0 # ADU/px: where the isophote model still has signal
def main():
model = fits.getdata(layout.path("sb-model.fits")).astype(np.float32)
ys, xs = np.where(model > MODEL_FLOOR)
# Model coordinates are on the uncropped grid; image 3 lost CROP px.
x0, x1 = xs.min() - CROP, xs.max() - CROP
y0, y1 = ys.min() - CROP, ys.max() - CROP
print(f"galaxy extent (model > {MODEL_FLOOR} ADU/px): "
f"x {x0}-{x1}, y {y0}-{y1} = {x1 - x0} x {y1 - y0} px")
with fits.open(layout.path("master-Luminance.fit")) as hd:
wcs = WCS(hd[0].header, naxis=2)
img = tifffile.imread(layout.path(SOURCE))
ny, nx = img.shape[:2]
print(f"source {SOURCE}: {nx} x {ny}, {img.dtype}")
# Centre the frame on the galaxy's own centre, not on the frame's.
cx, cy = (x0 + x1) / 2.0, (y0 + y1) / 2.0
half_x = (x1 - x0) / 2.0 * (1 + MARGIN)
half_y = (y1 - y0) / 2.0 * (1 + MARGIN)
# One box for both axes keeps the galaxy from touching a short edge, and a
# round object in a near-square frame reads better than in a letterbox.
half = max(half_x, half_y)
# Clamp inside the image while keeping the galaxy centred as far as
# possible; report honestly if the frame has to shrink.
half = min(half, cx, cy, nx - cx, ny - cy)
left, right = int(round(cx - half)), int(round(cx + half))
top, bottom = int(round(cy - half)), int(round(cy + half))
print(f"crop box x {left}-{right}, y {top}-{bottom} "
f"({right - left} x {bottom - top} px)")
# Verify the whole galaxy really is inside before writing anything.
assert left <= x0 and right >= x1 and top <= y0 and bottom >= y1, \
"galaxy would be clipped - refusing to write"
inset_x = min(x0 - left, right - x1)
inset_y = min(y0 - top, bottom - y1)
print(f"clearance to the nearest edge: {inset_x} px horizontally, "
f"{inset_y} px vertically")
scale = 0.5376
print(f"field of view {(right - left) * scale / 60:.1f}' x "
f"{(bottom - top) * scale / 60:.1f}'")
corner = wcs.pixel_to_world(left + CROP, top + CROP)
centre = wcs.pixel_to_world((left + right) / 2 + CROP,
(top + bottom) / 2 + CROP)
print(f"frame centre {centre.to_string('hmsdms')}")
print(f"top-left corner {corner.to_string('hmsdms')}")
crop = img[top:bottom, left:right]
tifffile.imwrite(layout.path("NGC5128-final-4-closeup.tif"), crop,
photometric="rgb")
u8 = (crop.astype(np.float32) / 65535.0 * 255 + 0.5).astype(np.uint8)
Image.fromarray(u8).save(layout.path("NGC5128-final-4-closeup.png"))
prev = Image.fromarray(u8)
prev.thumbnail((2400, 2400), Image.LANCZOS)
prev.save(layout.path("NGC5128-final-4-closeup-preview.jpg"),
quality=93)
print("wrote NGC5128-final-4-closeup.tif / .png / -preview.jpg")
os.makedirs(FINAL, exist_ok=True)
if __name__ == "__main__":
main()