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.
102 lines
4.4 KiB
Python
102 lines
4.4 KiB
Python
"""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()
|