Skip to content

jacotay7/getframes

Repository files navigation

getframes

CI PyPI Python License: MIT

Realistic synthetic camera frames for scientific imaging pipelines.

getframes gives you a clean, small API for generating physically realistic frames from CCD, CMOS, EMCCD, eAPD, and sCMOS detectors — with accurate, auditable noise physics (read noise, dark current, shot noise, fixed-pattern non-uniformity, a unified stochastic gain stage, clock-induced charge, nonlinearity, and cosmic rays) so you can build and validate image-processing pipelines against ground truth.

It generates dark, bias, and flat frames, and renders star fields through a PSF and telescope into a realistic science frame — the full photon → electron → ADU signal path, with optional opt-in spectral mode.

Status: stable. getframes 2.0 freezes the full public surface — the detector, scene, calibration, observation, radiometry, and dataset APIs — under Semantic Versioning; see API stability. An optional CuPy backend now keeps the photon → electron → ADU detector path and truth arrays on NVIDIA GPUs.

Install

pip install getframes
pip install 'getframes[gpu]'  # optional CUDA 12.x / CuPy detector backend

From source (for development):

git clone https://github.com/jacotay7/getframes
cd getframes
pip install -e ".[dev]"

Quick start

import getframes as gf

# Pick a camera from the built-in preset library...
cam = gf.Camera.from_preset("andor_ikon_m934")

# ...and generate a reproducible dark frame.
frame = cam.dark_frame(exposure=60.0, temperature=-60.0, seed=0)

frame.data            # 2-D numpy array of ADU, shape (1024, 1024)
frame.stats()         # {'mean': ..., 'median': ..., 'std': ..., 'min': ..., 'max': ...}
frame.metadata        # camera/exposure/temperature provenance

Frame is array-like, so it drops straight into NumPy:

import numpy as np
master_dark = np.mean([np.asarray(f) for f in cam.dark_series(60.0, n_frames=20, seed=1)], axis=0)

Define your own camera

cam = gf.Camera(
    gf.CameraConfig(
        name="My Lab CMOS",
        sensor_type="CMOS",
        resolution=(2048, 2048),       # (height, width)
        pixel_size_um=6.5,
        quantum_efficiency=0.82,
        full_well_e=30_000,
        bit_depth=12,
        gain_e_per_adu=0.8,
        bias_offset_adu=300,
        read_noise_e=1.8,
        dark_current_e_per_s=0.5,      # at the reference temperature
        dark_current_ref_temp_c=20.0,
        dark_current_doubling_temp_c=6.0,
    )
)
frame = cam.dark_frame(exposure=30.0, temperature=-10.0)

Run the detector on a GPU

import cupy as cp
import getframes as gf

cam = gf.Camera.from_preset("andor_ocam2k", device="gpu", precision="float32")
rate = cp.full(cam.resolution, 2.0e6, dtype=cp.float32)  # photons/s/pixel
frame = cam.expose(rate, exposure=1.0e-3, seed=0)

frame.data                  # CuPy uint32 ADU array; no host copy
frame.truth.mean_electrons  # CuPy truth array
host_adu = gf.to_numpy(frame.data)  # explicit device-to-host boundary

CPU and GPU use independent random-number streams: a seed repeats exactly on a fixed backend, while parity across backends means matching detector statistics, not identical pixels. See the GPU guide.

CPU/GPU throughput

Warm bulk-frame throughput on an AMD Ryzen 9 9950X3D and NVIDIA RTX 5090 (float32, truth enabled, persistent camera, device-resident input/output, no host transfers) is:

Workflow Native shape CPU (frames/s) GPU (frames/s) Speedup
Pyramid WFS CMOS 80x80 5,264.9 10,565.4 2.01x
Shack-Hartmann WFS CMOS 160x160 1,372.0 10,538.1 7.68x
OCAM2K EMCCD 240x240 347.7 8,057.1 23.17x
SAPHIRA eAPD 256x320 283.2 7,481.8 26.42x
Large science CMOS 1024x1024 31.5 1,431.9 45.39x

Higher is better. CUDA was synchronized around every timed region; construction was excluded. Even the smallest case reaches about 2x, while larger arrays and gain-stage detectors expose much more parallel work. See the full snapshot, raw JSON, and GPU benchmark methodology.

Observe a simulated star field

Render astronomical sources through a PSF and telescope, then expose them on a detector — the full photon → electron → ADU path:

scene = gf.Scene(
    shape=(256, 256),
    optics=gf.Telescope(aperture_diameter_m=2.5, throughput=0.3,
                        plate_scale_arcsec_per_pixel=0.4, band=gf.Bandpass.johnson("V")),
    psf=gf.MoffatPSF(fwhm_arcsec=1.1, beta=3.0),
    sources=[gf.PointSource(x=128, y=128, magnitude=20.0)],
    sky=gf.Sky(surface_brightness_mag_arcsec2=21.0),
)
cam = gf.Camera.from_preset("zwo_asi2600mm").with_config(resolution=(256, 256))
frame = cam.observe(scene, exposure=300.0, seed=0)   # a realistic science frame

You can also drive the detector directly with a photon-rate map (a scalar for a uniform flat, or a per-pixel array): cam.expose(photon_rate, exposure).

Browse the preset library

from getframes import available_presets
from getframes.presets import preset_info

available_presets()   # ['andor_ikon_m934', 'andor_ixon_ultra_888', 'generic_ccd', ...]
preset_info()         # rich descriptors for each preset
Preset Sensor Notes
andor_ikon_m934 CCD Deep-cooled back-illuminated scientific CCD
andor_ixon_ultra_888 EMCCD Single-photon-sensitive EMCCD
leonardo_saphira EAPD HgCdTe avalanche IR array (AO wavefront sensing)
zwo_asi2600mm CMOS Sony IMX571 cooled CMOS
hamamatsu_orca_fusion sCMOS Back-thinned sCMOS with per-pixel read noise
andor_marana_4_2b_11 sCMOS 11 µm-pixel, back-illuminated scientific CMOS
photometrics_prime_95b sCMOS 11 µm-pixel, 95% peak-QE scientific CMOS
princeton_instruments_kuro_1200b sCMOS Back-illuminated scientific CMOS
qhy530_pro_ii CMOS Global-shutter Sony IMX530 scientific camera
tucsen_aries_6504_pro sCMOS Sub-electron sensitive-mode read noise
generic_ccd / generic_cmos / generic_emccd / generic_eapd / generic_scmos Idealised references for teaching/testing

How the dark-frame model works

The dark signal chain (see getframes/noise.py):

  1. Dark current vs. temperatureD(T) = D_ref · 2^((T − T_ref) / T_double)
  2. Fixed-pattern non-uniformity (DSNU) and hot pixels modulate the per-pixel mean
  3. Shot noise — Poisson statistics on the dark electrons
  4. Clock-induced charge (EMCCD) — small Poisson term
  5. EM gain (EMCCD) — stochastic multiplication with realistic excess noise
  6. Read noise — Gaussian at the output amplifier
  7. Digitisation — gain conversion to ADU, bias pedestal, saturation, quantisation

All randomness flows through a seeded numpy.random.Generator, so every frame is reproducible.

Documentation

Roadmap

1.0 is shipped: the full photon → electron → ADU signal path (dark, bias, flat, and rendered scenes) across CCD / CMOS / EMCCD / eAPD / sCMOS, with a unified gain stage, detector-realism effects, opt-in spectral mode, analysis helpers, and a frozen API.

The 2.0 plan moves from a frame to an observation — closing the raw → reduced → ground-truth validation loop, making time-series (variability, jitter, persistence) and richer scenes (extended sources, catalogs, sky coordinates) first-class, and deepening detector and radiometric fidelity. See docs/roadmap.md for the full critique, phased plan, and worked examples.

Contributing

Contributions — especially new camera presets — are welcome. See CONTRIBUTING.md. Run the checks locally with:

ruff check . && ruff format --check . && mypy && pytest

License

MIT — see LICENSE.

About

Need some fake camera frames? This is the place to go.

Topics

Resources

License

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages