"""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()