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:
parent
653ca103cd
commit
c6299f41ab
53 changed files with 0 additions and 0 deletions
|
|
@ -1,132 +0,0 @@
|
|||
"""Step 1: masks and basic calibration checks for the NGC 5128 analysis.
|
||||
|
||||
Produces
|
||||
sb-mask-stars.fits foreground stars / saturated cores / compact objects
|
||||
sb-mask-dust.fits the dust lane, defined from the B-R colour excess
|
||||
sb-colour-excess.npy E(B-R) instrumental colour excess map (float32)
|
||||
|
||||
The dust mask is built from COLOUR, not from a model residual: the lane is the
|
||||
only thing in the frame that is strongly red relative to the smooth stellar
|
||||
body, so a colour cut is far more specific than a brightness-residual cut and
|
||||
does not eat the galaxy itself.
|
||||
"""
|
||||
import numpy as np, sep
|
||||
from astropy.io import fits
|
||||
from scipy import ndimage
|
||||
from sb_common import *
|
||||
|
||||
# ------------------------------------------------------------------ saturation
|
||||
d = load('Luminance')
|
||||
H, W = d.shape
|
||||
print('-- saturation census (luminance master) --')
|
||||
for thr in [30000, 50000, 60000, 62000, 63000, 64000, 65000, 66000]:
|
||||
print(' > %6d ADU : %6d px' % (thr, (d > thr).sum()))
|
||||
SAT = 63000.0
|
||||
core = ndimage.median_filter(d[int(Y0)-400:int(Y0)+400, int(X0)-400:int(X0)+400], 25)
|
||||
print(' star-free (median-25) peak of galaxy light, inner 800 px: %.1f ADU' % core.max())
|
||||
print(' => the galaxy core is a factor %.0f below the clip level: NOT saturated'
|
||||
% (SAT/core.max()))
|
||||
del core
|
||||
|
||||
bkg = sep.Background(d, bw=128, bh=128)
|
||||
rms = float(bkg.globalrms)
|
||||
print('\nglobal sky rms %.3f ADU/px -> 1-sigma = %.2f mag/arcsec^2' % (rms, mu(rms)))
|
||||
np.save(path('_rms.npy'), np.array([rms]))
|
||||
|
||||
# ------------------------------------------------------------------ star mask
|
||||
mask = np.zeros((H, W), bool)
|
||||
z = np.load(path('_gaia_deep.npz'))
|
||||
gx, gy = wcs().world_to_pixel_values(z['ra'], z['dec']); gg = z['g']
|
||||
rad = np.clip(4.0 + 3.6*(16.5 - gg), 4, 120)
|
||||
sel = (gx > -150) & (gx < W+150) & (gy > -150) & (gy < H+150) & (gg < 19.0)
|
||||
gx, gy, rad = gx[sel], gy[sel], rad[sel]
|
||||
print('\nGaia stars masked: %d (radii %.0f-%.0f px)' % (gx.size, rad.min(), rad.max()))
|
||||
for x, y, r in zip(gx, gy, rad):
|
||||
i0, i1 = max(0, int(y-r)), min(H, int(y+r)+1)
|
||||
j0, j1 = max(0, int(x-r)), min(W, int(x+r)+1)
|
||||
if i1 <= i0 or j1 <= j0: continue
|
||||
sy = np.arange(i0, i1)[:, None] - y; sx = np.arange(j0, j1)[None, :] - x
|
||||
mask[i0:i1, j0:j1] |= (sx*sx + sy*sy) < r*r
|
||||
|
||||
rr = np.hypot(np.arange(W)[None, :]-X0, np.arange(H)[:, None]-Y0)
|
||||
|
||||
# compact non-Gaia objects, but leave the crowded inner 150 px to the
|
||||
# isophote fitter's own sigma clipping (masking there kills the fit)
|
||||
sub = d - bkg.back()
|
||||
obj, seg = sep.extract(sub, 8.0, err=rms, minarea=6, deblend_cont=0.005,
|
||||
segmentation_map=True)
|
||||
compact = obj['npix'] < 20000
|
||||
segmask = ndimage.binary_dilation(np.isin(seg, np.nonzero(compact)[0]+1), np.ones((5, 5)))
|
||||
mask |= segmask & (rr > 150)
|
||||
print('sep compact objects: %d (applied outside r=150 px)' % compact.sum())
|
||||
del sub, seg, segmask
|
||||
|
||||
sat = ndimage.binary_dilation(d > SAT, np.ones((5, 5)), iterations=6)
|
||||
mask |= sat
|
||||
print('saturated-core mask (grown 12 px): %d px' % sat.sum())
|
||||
del sat, d
|
||||
|
||||
print('star mask: %.2f%% of frame' % (100*mask.mean()))
|
||||
for a, b in [(0,25),(25,50),(50,100),(100,200),(200,400),(400,800),(800,1600)]:
|
||||
s = (rr >= a) & (rr < b)
|
||||
print(' r %4d-%4d px: %.1f%%' % (a, b, 100*mask[s].mean()))
|
||||
fits.PrimaryHDU(mask.astype(np.uint8)).writeto(path('sb-mask-stars.fits'), overwrite=True)
|
||||
|
||||
# ------------------------------------------------------------------ dust mask
|
||||
# instrumental B-R from smoothed R and B masters
|
||||
R = ndimage.gaussian_filter(load('Red'), 3.0)
|
||||
B = ndimage.gaussian_filter(load('Blue'), 3.0)
|
||||
good = (R > 15) & (B > 4)
|
||||
col = np.full((H, W), np.nan, np.float32)
|
||||
col[good] = -2.5*np.log10(B[good]/R[good])
|
||||
del R, B
|
||||
|
||||
# Unobscured baseline colour vs elliptical radius. Dust only ever reddens, so
|
||||
# the blue tail of the colour distribution in each annulus is the dust-free
|
||||
# stellar colour. The 15th percentile is measured over 150 < a < 900 px (where
|
||||
# both the colour SNR is high and the lane does not fill the annulus) and fitted
|
||||
# with a quadratic in log a, which is then extrapolated inwards and outwards.
|
||||
a_map = ell_radius((H, W), X0, Y0, 0.17, np.radians(150.0))
|
||||
bins = np.geomspace(5, 2200, 60)
|
||||
ib = np.digitize(a_map, bins)
|
||||
base_r, base_v = [], []
|
||||
valid = good & ~mask
|
||||
for k in range(1, len(bins)):
|
||||
s = (ib == k) & valid
|
||||
if s.sum() < 400: continue
|
||||
base_r.append(0.5*(bins[k-1]+bins[k])); base_v.append(np.nanpercentile(col[s], 15))
|
||||
base_r, base_v = np.array(base_r), np.array(base_v)
|
||||
fitr = (base_r > 150) & (base_r < 900)
|
||||
pcoef = np.polyfit(np.log10(base_r[fitr]), base_v[fitr], 2)
|
||||
print()
|
||||
print('colour baseline: quadratic in log10(a), coeffs', np.round(pcoef, 4))
|
||||
print(' baseline B-R at a = 20/50/150/400/900 px:',
|
||||
np.round(np.polyval(pcoef, np.log10([20, 50, 150, 400, 900])), 3))
|
||||
base = np.polyval(pcoef, np.log10(np.clip(a_map, 5, 3000))).astype(np.float32)
|
||||
exc = (col - base).astype(np.float32) # E(B-R), instrumental
|
||||
np.save(path('sb-colour-excess.npy'), exc)
|
||||
np.save(path('_colbase.npy'), np.c_[base_r, base_v])
|
||||
np.save(path('_colbasefit.npy'), pcoef)
|
||||
|
||||
# Hysteresis threshold: seed on a firm colour excess, grow into the fainter
|
||||
# wings of the same connected structure. A flat low threshold alone picks up a
|
||||
# spurious ring at the edge of the colour-SNR region, so it is not used.
|
||||
e0 = np.nan_to_num(exc, nan=-9.0)
|
||||
seed = (e0 > 0.30) & (a_map < 650)
|
||||
grow = (e0 > 0.20) & (a_map < 650)
|
||||
seed = ndimage.binary_opening(seed, np.ones((5, 5)))
|
||||
lab, n = ndimage.label(ndimage.binary_closing(grow, np.ones((7, 7))))
|
||||
keep = np.unique(lab[seed & (lab > 0)])
|
||||
dust = np.isin(lab, keep[keep > 0])
|
||||
dust = ndimage.binary_closing(dust, np.ones((15, 15)))
|
||||
lab, n = ndimage.label(dust); sz = np.bincount(lab.ravel()); sz[0] = 0
|
||||
dust = np.isin(lab, np.nonzero(sz > 3000)[0])
|
||||
print('dust mask: %d px = %.1f arcmin^2 (%.2f%% of frame)'
|
||||
% (dust.sum(), dust.sum()*PIXAREA/3600., 100*dust.mean()))
|
||||
for a, b in [(0,25),(25,50),(50,100),(100,200),(200,400),(400,800)]:
|
||||
s = (a_map >= a) & (a_map < b)
|
||||
print(' dust a %4d-%4d px: %5.1f%% star+dust %5.1f%%'
|
||||
% (a, b, 100*dust[s].mean(), 100*(dust | mask)[s].mean()))
|
||||
fits.PrimaryHDU(dust.astype(np.uint8)).writeto(path('sb-mask-dust.fits'), overwrite=True)
|
||||
print()
|
||||
print('wrote sb-mask-stars.fits, sb-mask-dust.fits, sb-colour-excess.npy')
|
||||
Loading…
Add table
Add a link
Reference in a new issue