Skip to content

Calibration & ground truth

The reason to generate synthetic frames is to test a reduction pipeline against something you can't get from real data: the exact ground truth. getframes gives you both halves — raw frames that carry their truth, and the calibration helpers to reduce them — so you can close the loop and measure your pipeline's residuals.

Build calibration masters

Averaging many frames beats down the random noise (by about sqrt(n)); the median or a sigma clip also rejects outliers like cosmic rays. Camera builds the three standard masters for you:

import getframes as gf

cam = gf.Camera.from_preset("generic_cmos", default_temperature_c=-10.0)

master_bias = cam.master_bias(n_frames=50, seed=0)
master_dark = cam.master_dark(exposure=60.0, n_frames=25, seed=1)   # exposure-matched
master_flat = cam.master_flat(photon_rate=20_000.0, exposure=1.0,
                              n_frames=25, seed=2, bias=master_bias)  # pedestal-free

Under the hood these call combine, which you can use directly on any stack of frames or arrays:

master = gf.combine(cam.dark_series(60.0, 25, seed=1), method="sigma_clip", sigma=3.0)

Fixed-pattern noise is fixed

PRNU, DSNU, and hot pixels are a property of the sensor, so getframes imprints the same pattern in every frame a camera produces (keyed on CameraConfig.fixed_pattern_seed). That is exactly what lets a master flat or dark capture and remove them — give two cameras different seeds to mint different sensors.

Reduce a science frame

calibrate applies the standard, exposure-matched reduction (raw - dark) / normalised(flat):

sci = cam.expose(photon_rate=300.0, exposure=60.0, seed=3)
reduced = gf.calibrate(sci, dark=master_dark, flat=master_flat)

Subtracting an exposure-matched master dark removes the bias pedestal and dark current together; dividing by the normalised flat removes the pixel-to-pixel response. Pass bias= instead of dark= to remove only the pedestal, or dark_scale= to scale a dark to a different exposure.

Check against the truth

Every illuminated frame carries the noise-free signal it was built from in Frame.truth:

import numpy as np

truth_adu = sci.truth.mean_photoelectrons / cam.config.gain_e_per_adu
residual = np.asarray(reduced) - truth_adu
print(f"residual RMS: {residual.std():.2f} ADU")   # → the shot + read noise floor

A correct pipeline drives the residual down to the irreducible shot + read noise floor. (After flat-fielding you recover the PRNU-free signal, so compare against the incident photon signal rather than the PRNU-imprinted mean_photoelectrons.)

Frame series

The masters above are built from series. The same series methods mirror dark_series for light and scene frames:

flats   = cam.expose_series(20_000.0, 1.0, n_frames=25, seed=2)
science = cam.observe_series(scene, exposure=300.0, n_frames=10, seed=0)

Each frame in a seeded series is statistically independent but the whole series is reproducible.