PRNU, DSNU, and hot pixels
This commit is contained in:
1 parent
1ed95982ad
commit
3d0b129582
17 files changed
+763
-37
No files matched your search
@@ -60,7 +60,7 @@ struct RenderArgs {
|
||||
/// Camera fidelity tier: C0, or C1 (adds pixels that collect light over their area,
|
||||
/// for graded edges and continuous thin parts, lens distortion, lens blur, and the
|
||||
/// Sun, Moon and Earth where light time and aberration make them appear, detector
|
||||
/// noise, and at lighting L1 stars).
|
||||
/// noise and fixed pattern, and at lighting L1 stars).
|
||||
#[arg(long, default_value = "C0")]
|
||||
camera: CameraTier,
|
||||
/// From camera C1: render each pixel as N x N finer ones (1 to 4; default 2), which
|
||||
@@ -229,6 +229,7 @@ fn render(args: &RenderArgs) -> Result<(), String> {
|
||||
supersampling: args.supersampling,
|
||||
stars,
|
||||
noise_seed: args.seed,
|
||||
fixed_pattern: None,
|
||||
};
|
||||
let start = Instant::now();
|
||||
let mut renderer = Renderer::new(camera, options).map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -74,6 +74,14 @@ pub struct CameraFile {
|
||||
pub dark_current: Option<f64>,
|
||||
/// From camera C1: whether the electrons collected carry shot noise.
|
||||
pub shot_noise: Option<bool>,
|
||||
/// From camera C1: photo-response nonuniformity, a fraction RMS.
|
||||
pub prnu: Option<f64>,
|
||||
/// From camera C1: dark signal nonuniformity, a fraction RMS.
|
||||
pub dsnu: Option<f64>,
|
||||
/// From camera C1: the fraction of hot pixels and their dark current's multiple.
|
||||
pub hot_pixels: Option<[f64; 2]>,
|
||||
/// From camera C1: the seed of the fixed pattern.
|
||||
pub pattern_seed: Option<u64>,
|
||||
/// The Gaussian blur's sigma in pixels, from camera C1: one for every channel, one
|
||||
/// per channel, or 0 for none.
|
||||
pub psf_sigma: Option<Sigma>,
|
||||
@@ -174,6 +182,15 @@ impl SceneFile {
|
||||
if let Some(shot) = c.shot_noise {
|
||||
camera.detector.shot_noise = shot;
|
||||
}
|
||||
set(&mut camera.detector.prnu, c.prnu);
|
||||
set(&mut camera.detector.dsnu, c.dsnu);
|
||||
if let Some([fraction, multiple]) = c.hot_pixels {
|
||||
camera.detector.hot_pixel_fraction = fraction;
|
||||
camera.detector.hot_pixel_multiple = multiple;
|
||||
}
|
||||
if let Some(seed) = c.pattern_seed {
|
||||
camera.detector.pattern_seed = seed;
|
||||
}
|
||||
if let Some([u, v]) = c.principal_point {
|
||||
camera.principal_point = Some(Vector2::new(u, v));
|
||||
}
|
||||
@@ -453,7 +470,8 @@ mod tests {
|
||||
fn camera_overrides_apply() {
|
||||
let file: SceneFile = toml::from_str(&format!(
|
||||
"{MINIMAL}\n[camera]\nwidth = 640\nexposure = 2e-3\nadc_bits = 10\nread_noise = 2.5\n\
|
||||
dark_current = 0\nshot_noise = false\n"
|
||||
dark_current = 0\nshot_noise = false\nprnu = 0.02\nhot_pixels = [1e-3, 20]\n\
|
||||
pattern_seed = 9\n"
|
||||
))
|
||||
.unwrap();
|
||||
let camera = file.camera();
|
||||
@@ -465,6 +483,8 @@ mod tests {
|
||||
(d.read_noise, d.dark_current, d.shot_noise),
|
||||
(2.5, 0.0, false)
|
||||
);
|
||||
assert_eq!((d.prnu, d.dsnu, d.pattern_seed), (0.02, 0.1, 9));
|
||||
assert_eq!((d.hot_pixel_fraction, d.hot_pixel_multiple), (1e-3, 20.0));
|
||||
|
||||
for (line, psf) in [
|
||||
(
|
||||
|
||||
@@ -22,9 +22,9 @@ pub struct Channel {
|
||||
|
||||
/// How the detector turns electrons into numbers.
|
||||
///
|
||||
/// The noise fields apply from camera tier C1 up. A detector with no read noise, no
|
||||
/// dark current and no shot noise ([`Detector::noiseless`]) gives noise-free images at
|
||||
/// C1, for ground truth and labels.
|
||||
/// The noise and fixed-pattern fields apply from camera tier C1 up. A detector with
|
||||
/// neither ([`Detector::noiseless`]) gives noise-free images at C1, for ground truth
|
||||
/// and labels.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Detector {
|
||||
/// Electrons per DN.
|
||||
@@ -42,6 +42,18 @@ pub struct Detector {
|
||||
pub dark_current: f64,
|
||||
/// Whether the electrons collected vary as a Poisson count about their mean.
|
||||
pub shot_noise: bool,
|
||||
/// Photo-response nonuniformity: the spread of each pixel's response to light
|
||||
/// about the average, as a fraction RMS ([`FixedPattern`](crate::FixedPattern)).
|
||||
pub prnu: f64,
|
||||
/// Dark signal nonuniformity: the spread of each pixel's dark current about the
|
||||
/// average, as a fraction RMS, apart from hot pixels.
|
||||
pub dsnu: f64,
|
||||
/// The fraction of values (pixels and channels) that are hot.
|
||||
pub hot_pixel_fraction: f64,
|
||||
/// A hot pixel's dark current, as a multiple of the average.
|
||||
pub hot_pixel_multiple: f64,
|
||||
/// The seed of the generated pattern: one camera, one pattern, whatever the noise.
|
||||
pub pattern_seed: u64,
|
||||
}
|
||||
|
||||
impl Detector {
|
||||
@@ -61,12 +73,16 @@ impl Detector {
|
||||
.clamp(0.0, self.dn_max() as f64) as u16
|
||||
}
|
||||
|
||||
/// The same detector with no read noise, dark current or shot noise.
|
||||
/// The same detector with no read noise, dark current, shot noise or fixed
|
||||
/// pattern.
|
||||
pub fn noiseless(self) -> Self {
|
||||
Self {
|
||||
read_noise: 0.0,
|
||||
dark_current: 0.0,
|
||||
shot_noise: false,
|
||||
prnu: 0.0,
|
||||
dsnu: 0.0,
|
||||
hot_pixel_fraction: 0.0,
|
||||
..self
|
||||
}
|
||||
}
|
||||
@@ -76,6 +92,11 @@ impl Detector {
|
||||
self.shot_noise || self.read_noise > 0.0 || self.dark_current > 0.0
|
||||
}
|
||||
|
||||
/// Whether the detector generates a fixed pattern.
|
||||
pub fn has_fixed_pattern(&self) -> bool {
|
||||
self.prnu > 0.0 || self.dsnu > 0.0 || self.hot_pixel_fraction > 0.0
|
||||
}
|
||||
|
||||
/// The variance, in electrons squared, of one pixel's reading for a mean of
|
||||
/// `electrons` from light over an exposure of `exposure` seconds, below the full
|
||||
/// well: the shot noise of the light and of the dark current, the read noise, and
|
||||
@@ -264,6 +285,8 @@ impl CameraConfig {
|
||||
/// - 20,000 electron full well, 12-bit ADC, 5 electrons per DN, 64 DN bias.
|
||||
/// - 5 electrons RMS read noise, 20 electrons per second dark current, and shot
|
||||
/// noise: a CMOS sensor at room temperature.
|
||||
/// - 1 % PRNU, 10 % DSNU, and 1 value in 10,000 hot at 50 times the dark current,
|
||||
/// from pattern seed 0.
|
||||
/// - 0.1 ms exposure: a white Lambertian surface facing the Sun fills about half the
|
||||
/// well (48-57 % across the channels), so Earth and the target are both unsaturated.
|
||||
/// - No lens distortion.
|
||||
@@ -299,6 +322,11 @@ impl CameraConfig {
|
||||
read_noise: 5.0,
|
||||
dark_current: 20.0,
|
||||
shot_noise: true,
|
||||
prnu: 0.01,
|
||||
dsnu: 0.1,
|
||||
hot_pixel_fraction: 1e-4,
|
||||
hot_pixel_multiple: 50.0,
|
||||
pattern_seed: 0,
|
||||
},
|
||||
near_clip: 0.01,
|
||||
mount: Pose::new(Vector3::zeros(), UnitQuaternion::identity()),
|
||||
@@ -354,6 +382,12 @@ impl CameraConfig {
|
||||
if !nonnegative(d.read_noise) || !nonnegative(d.dark_current) {
|
||||
return fail("read noise and dark current must be nonnegative");
|
||||
}
|
||||
if !nonnegative(d.prnu) || !nonnegative(d.dsnu) || !nonnegative(d.hot_pixel_multiple) {
|
||||
return fail("PRNU, DSNU and the hot pixels' multiple must be nonnegative");
|
||||
}
|
||||
if !(0.0..=1.0).contains(&d.hot_pixel_fraction) {
|
||||
return fail("the hot pixels' fraction must be between 0 and 1");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@ pub enum CameraTier {
|
||||
/// ([`Scene::velocities`](crate::Scene::velocities)). Target meshes are covered at several
|
||||
/// points per rendered pixel, and rendered pixels on the edge of Earth, the Moon or
|
||||
/// the Sun cast a grid of rays, 4 x 4 and more for bodies only a few pixels across.
|
||||
/// The detector adds its shot, dark and read noise
|
||||
/// ([`Detector`](crate::camera::Detector)), seeded by
|
||||
/// The detector adds its fixed pattern ([`FixedPattern`](crate::FixedPattern)) and
|
||||
/// its shot, dark and read noise ([`Detector`](crate::camera::Detector)), seeded by
|
||||
/// [`RendererOptions::noise_seed`](crate::RendererOptions::noise_seed) and the
|
||||
/// frame number. More of the camera joins this tier as it is modeled.
|
||||
C1,
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
//! Fixed-pattern nonuniformity: how each pixel's response to light (PRNU) and dark
|
||||
//! current (DSNU) differ from the detector's average, the same in every frame.
|
||||
//!
|
||||
//! From camera tier C1 the sensor pass applies a pattern to every value. One generated
|
||||
//! from the detector's [`Detector::prnu`], [`Detector::dsnu`], hot pixels and
|
||||
//! [`Detector::pattern_seed`] is computed in the shader from a hash, so it costs no
|
||||
//! memory; [`FixedPattern::generate`] computes the same one on the CPU. One measured
|
||||
//! from calibration frames ([`FixedPattern::from_calibration`]) is uploaded instead
|
||||
//! when given ([`RendererOptions::fixed_pattern`](crate::RendererOptions::fixed_pattern)).
|
||||
|
||||
use crate::camera::Detector;
|
||||
use crate::Error;
|
||||
|
||||
/// Per pixel and channel, the factor on the light's electrons and the factor on the
|
||||
/// dark current's.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FixedPattern {
|
||||
/// Width in pixels.
|
||||
pub width: u32,
|
||||
/// Height in pixels.
|
||||
pub height: u32,
|
||||
/// Channels per pixel.
|
||||
pub channels: u32,
|
||||
/// `[response, dark]` per value, channels interleaved per pixel, rows from the top,
|
||||
/// as in [`Frame::dn`](crate::Frame::dn). Both average about 1.
|
||||
pub values: Vec<[f32; 2]>,
|
||||
}
|
||||
|
||||
/// The hash the sensor shader draws its noise and pattern from (Jarzynski and Olano
|
||||
/// 2020, pcg4d): four words into four.
|
||||
pub(crate) fn pcg4d(input: [u32; 4]) -> [u32; 4] {
|
||||
let mut v = input.map(|x| x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223));
|
||||
let mix = |v: &mut [u32; 4]| {
|
||||
v[0] = v[0].wrapping_add(v[1].wrapping_mul(v[3]));
|
||||
v[1] = v[1].wrapping_add(v[2].wrapping_mul(v[0]));
|
||||
v[2] = v[2].wrapping_add(v[0].wrapping_mul(v[1]));
|
||||
v[3] = v[3].wrapping_add(v[1].wrapping_mul(v[2]));
|
||||
};
|
||||
mix(&mut v);
|
||||
v = v.map(|x| x ^ (x >> 16));
|
||||
mix(&mut v);
|
||||
v
|
||||
}
|
||||
|
||||
/// Uniform in (0, 1) from the top 24 bits, as `unit` in `sensor.wgsl`.
|
||||
pub(crate) fn unit(x: u32) -> f64 {
|
||||
((x >> 8) as f64 + 0.5) / 16_777_216.0
|
||||
}
|
||||
|
||||
/// The fourth word of the pattern's hash input, which the noise's never takes with the
|
||||
/// same other words in practice: it keeps the two streams apart.
|
||||
pub(crate) const PATTERN_STREAM: u32 = 0x8000_0000;
|
||||
|
||||
impl FixedPattern {
|
||||
/// The pattern `detector` generates for value `index` (pixel times channels plus
|
||||
/// channel): `[response, dark]`. The sensor shader computes the same in f32.
|
||||
pub fn generated_value(detector: &Detector, index: u32) -> [f64; 2] {
|
||||
let seed = detector.pattern_seed;
|
||||
let r = pcg4d([seed as u32, index, PATTERN_STREAM, (seed >> 32) as u32]);
|
||||
let radius = (-2.0 * unit(r[0]).ln()).sqrt();
|
||||
let angle = std::f64::consts::TAU * unit(r[1]);
|
||||
let response = (1.0 + detector.prnu * radius * angle.cos()).max(0.0);
|
||||
let dark = if unit(r[2]) < detector.hot_pixel_fraction {
|
||||
detector.hot_pixel_multiple
|
||||
} else {
|
||||
(1.0 + detector.dsnu * radius * angle.sin()).max(0.0)
|
||||
};
|
||||
[response, dark]
|
||||
}
|
||||
|
||||
/// The pattern `detector` generates for an image `width` by `height` with
|
||||
/// `channels` channels.
|
||||
pub fn generate(detector: &Detector, width: u32, height: u32, channels: u32) -> Self {
|
||||
let values = (0..width * height * channels)
|
||||
.map(|i| Self::generated_value(detector, i).map(|x| x as f32))
|
||||
.collect();
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
channels,
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
/// A pattern from calibration frames: `flat`, a uniformly lit frame, and `dark`, a
|
||||
/// frame in the dark, both with the bias removed and averaged over many exposures
|
||||
/// so their own noise is small, laid out as [`FixedPattern::values`]. Each channel
|
||||
/// of each is divided by its mean.
|
||||
pub fn from_calibration(
|
||||
width: u32,
|
||||
height: u32,
|
||||
channels: u32,
|
||||
flat: &[f32],
|
||||
dark: &[f32],
|
||||
) -> Result<Self, Error> {
|
||||
let n = (width * height * channels) as usize;
|
||||
let fail = |what: &str| Err(Error::InvalidCamera(what.to_owned()));
|
||||
if flat.len() != n || dark.len() != n {
|
||||
return fail("calibration frames must have one value per pixel and channel");
|
||||
}
|
||||
if !flat.iter().chain(dark).all(|x| x.is_finite() && *x >= 0.0) {
|
||||
return fail("calibration frames must be finite and nonnegative");
|
||||
}
|
||||
let c = channels as usize;
|
||||
let mean = |frame: &[f32], k: usize| {
|
||||
frame
|
||||
.iter()
|
||||
.skip(k)
|
||||
.step_by(c)
|
||||
.map(|&x| x as f64)
|
||||
.sum::<f64>()
|
||||
/ (n / c) as f64
|
||||
};
|
||||
let flat_means: Vec<f64> = (0..c).map(|k| mean(flat, k)).collect();
|
||||
let dark_means: Vec<f64> = (0..c).map(|k| mean(dark, k)).collect();
|
||||
if flat_means.iter().chain(&dark_means).any(|&m| m <= 0.0) {
|
||||
return fail("each channel of the calibration frames needs a positive mean");
|
||||
}
|
||||
let values = (0..n)
|
||||
.map(|i| {
|
||||
[
|
||||
(flat[i] as f64 / flat_means[i % c]) as f32,
|
||||
(dark[i] as f64 / dark_means[i % c]) as f32,
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
Ok(Self {
|
||||
width,
|
||||
height,
|
||||
channels,
|
||||
values,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::CameraConfig;
|
||||
|
||||
#[test]
|
||||
fn a_generated_pattern_has_the_detectors_statistics() {
|
||||
// 1 % PRNU and 10 % DSNU over 600,000 values, with 1e-3 hot pixels at 50 times:
|
||||
// the response's mean is 1 and its spread 1 %, the dark factor's spread among
|
||||
// ordinary pixels 10 %, each to 1 %, and the hot pixels number 600 to 5 standard
|
||||
// deviations.
|
||||
let detector = Detector {
|
||||
hot_pixel_fraction: 1e-3,
|
||||
..CameraConfig::example().detector
|
||||
};
|
||||
let p = FixedPattern::generate(&detector, 400, 500, 3);
|
||||
let n = p.values.len() as f64;
|
||||
let response: Vec<f64> = p.values.iter().map(|v| v[0] as f64).collect();
|
||||
let mean = response.iter().sum::<f64>() / n;
|
||||
let sd = (response.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / n).sqrt();
|
||||
assert!((mean - 1.0).abs() < 1e-4, "{mean}");
|
||||
assert!((sd / 0.01 - 1.0).abs() < 0.01, "{sd}");
|
||||
let hot = p.values.iter().filter(|v| v[1] == 50.0).count() as f64;
|
||||
assert!((hot - n * 1e-3).abs() < 5.0 * (n * 1e-3).sqrt(), "{hot}");
|
||||
let dark: Vec<f64> = p
|
||||
.values
|
||||
.iter()
|
||||
.filter(|v| v[1] != 50.0)
|
||||
.map(|v| v[1] as f64)
|
||||
.collect();
|
||||
let m = dark.iter().sum::<f64>() / dark.len() as f64;
|
||||
let sd = (dark.iter().map(|d| (d - m).powi(2)).sum::<f64>() / dark.len() as f64).sqrt();
|
||||
assert!((sd / 0.1 - 1.0).abs() < 0.01, "{sd}");
|
||||
let other = FixedPattern::generate(
|
||||
&Detector {
|
||||
pattern_seed: 1,
|
||||
..detector
|
||||
},
|
||||
400,
|
||||
500,
|
||||
3,
|
||||
);
|
||||
assert_ne!(p, other);
|
||||
assert_eq!(p, FixedPattern::generate(&detector, 400, 500, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calibration_frames_are_normalized_per_channel() {
|
||||
let flat = [2.0, 10.0, 4.0, 30.0];
|
||||
let dark = [1.0, 0.5, 3.0, 1.5];
|
||||
// Channel 0's flat is 2 and 4 (mean 3), channel 1's 10 and 30 (mean 20); the
|
||||
// darks' means are 2 and 1.
|
||||
let p = FixedPattern::from_calibration(2, 1, 2, &flat, &dark).unwrap();
|
||||
let third = |x: f64| (x / 3.0) as f32;
|
||||
assert_eq!(
|
||||
p.values,
|
||||
vec![[third(2.0), 0.5], [0.5, 0.5], [third(4.0), 1.5], [1.5, 1.5]]
|
||||
);
|
||||
assert!(FixedPattern::from_calibration(2, 1, 2, &flat[..3], &dark).is_err());
|
||||
assert!(FixedPattern::from_calibration(2, 1, 2, &[0.0; 4], &dark).is_err());
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,20 @@ use super::targets::FrameTargets;
|
||||
use super::warp::DistortionUniform;
|
||||
use super::GpuContext;
|
||||
use crate::camera::CameraConfig;
|
||||
use crate::fixed_pattern::{FixedPattern, PATTERN_STREAM};
|
||||
use crate::radiometry::Radiometry;
|
||||
|
||||
/// Where the sensor pass takes the detector's fixed pattern from.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum SensorPattern<'a> {
|
||||
/// No fixed pattern.
|
||||
None,
|
||||
/// Generated in the shader from the detector's fields.
|
||||
Generated,
|
||||
/// From calibration maps.
|
||||
Loaded(&'a FixedPattern),
|
||||
}
|
||||
|
||||
/// The shader's uniform block; the layout matches `Sensor` in `sensor.wgsl`.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
@@ -22,6 +34,8 @@ pub struct SensorUniforms {
|
||||
dims: [u32; 4],
|
||||
noise: [f32; 4],
|
||||
key: [u32; 4],
|
||||
pattern: [f32; 4],
|
||||
pattern_key: [u32; 4],
|
||||
distortion: DistortionUniform,
|
||||
}
|
||||
|
||||
@@ -55,6 +69,13 @@ impl SensorUniforms {
|
||||
]
|
||||
.map(|x| x as f32),
|
||||
key: [0; 4],
|
||||
pattern: [d.prnu, d.dsnu, d.hot_pixel_fraction, d.hot_pixel_multiple].map(|x| x as f32),
|
||||
pattern_key: [
|
||||
d.pattern_seed as u32,
|
||||
(d.pattern_seed >> 32) as u32,
|
||||
PATTERN_STREAM,
|
||||
0,
|
||||
],
|
||||
distortion: DistortionUniform::new(k.distortion.as_ref().filter(|_| distorted)),
|
||||
}
|
||||
}
|
||||
@@ -76,19 +97,30 @@ pub struct SensorPass {
|
||||
/// Bound for the stars' electrons without stars: the shader names the binding even
|
||||
/// where it never reads it.
|
||||
no_stars: wgpu::Buffer,
|
||||
/// The loaded pattern, or a placeholder bound the same way.
|
||||
pattern: wgpu::Buffer,
|
||||
}
|
||||
|
||||
impl SensorPass {
|
||||
/// Compiles the shader, adding the stars' electrons if `stars` and the detector's
|
||||
/// noise if `noise`, and allocates the uniform buffer.
|
||||
pub fn new(ctx: &GpuContext, stars: bool, noise: bool) -> Self {
|
||||
/// Compiles the shader, adding the stars' electrons if `stars`, the detector's
|
||||
/// noise if `noise`, and its fixed pattern from `pattern`, and allocates the uniform
|
||||
/// buffer and uploads a loaded pattern.
|
||||
pub fn new(ctx: &GpuContext, stars: bool, noise: bool, pattern: SensorPattern) -> Self {
|
||||
let kind = match pattern {
|
||||
SensorPattern::None => 0,
|
||||
SensorPattern::Generated => 1,
|
||||
SensorPattern::Loaded(_) => 2,
|
||||
};
|
||||
let module = ctx
|
||||
.device
|
||||
.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("sensor"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
[
|
||||
&format!("const STARS: bool = {stars};\nconst NOISE: bool = {noise};\n"),
|
||||
&format!(
|
||||
"const STARS: bool = {stars};\nconst NOISE: bool = {noise};\n\
|
||||
const PATTERN: u32 = {kind}u;\n"
|
||||
),
|
||||
include_str!("../shaders/distortion.wgsl"),
|
||||
include_str!("../shaders/sensor.wgsl"),
|
||||
]
|
||||
@@ -122,6 +154,20 @@ impl SensorPass {
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
mapped_at_creation: false,
|
||||
}),
|
||||
pattern: {
|
||||
use wgpu::util::DeviceExt;
|
||||
let placeholder = [[0.0f32; 2]];
|
||||
let values: &[[f32; 2]] = match pattern {
|
||||
SensorPattern::Loaded(p) => &p.values,
|
||||
_ => &placeholder,
|
||||
};
|
||||
ctx.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("fixed pattern"),
|
||||
contents: bytemuck::cast_slice(values),
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +206,10 @@ impl SensorPass {
|
||||
binding: 3,
|
||||
resource: stars.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 4,
|
||||
resource: self.pattern.as_entire_binding(),
|
||||
},
|
||||
];
|
||||
let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("sensor"),
|
||||
|
||||
@@ -25,6 +25,7 @@ pub mod eclipse;
|
||||
pub mod ephemeris;
|
||||
pub mod error;
|
||||
pub mod fidelity;
|
||||
pub mod fixed_pattern;
|
||||
pub mod gpu;
|
||||
pub mod model;
|
||||
pub mod moon;
|
||||
@@ -42,6 +43,7 @@ pub use camera::{BrownConrady, CameraConfig, Channel, Detector, Intrinsics, Psf}
|
||||
pub use earth::EarthTexture;
|
||||
pub use error::Error;
|
||||
pub use fidelity::{CameraTier, Fidelity, LightingTier};
|
||||
pub use fixed_pattern::FixedPattern;
|
||||
pub use model::{LoadOptions, Material, Mesh, ModelId};
|
||||
pub use moon::MoonTexture;
|
||||
pub use preview::{Look, Preview};
|
||||
|
||||
@@ -18,12 +18,13 @@ use std::sync::Arc;
|
||||
use crate::earth::EarthTexture;
|
||||
use crate::eclipse::{sun_visible_fraction, sun_visible_light};
|
||||
use crate::fidelity::{CameraTier, Fidelity, LightingTier};
|
||||
use crate::fixed_pattern::FixedPattern;
|
||||
use crate::gpu::blur::BlurPass;
|
||||
use crate::gpu::bodies::{BodiesPass, BodiesUniforms, BodyRadiometry};
|
||||
use crate::gpu::earthshine::{EarthshinePass, TargetLight};
|
||||
use crate::gpu::mesh::{GpuMesh, MeshDraw, MeshPass, ShadowDraw};
|
||||
use crate::gpu::profile::{PassTimings, Profiler};
|
||||
use crate::gpu::sensor::{SensorPass, SensorUniforms};
|
||||
use crate::gpu::sensor::{SensorPass, SensorPattern, SensorUniforms};
|
||||
use crate::gpu::shadow::{CasterDraw, ShadowPass};
|
||||
use crate::gpu::stars::{draws as star_draws, StarPass};
|
||||
use crate::gpu::targets::{FrameTargets, TargetLayout, DEPTH_FORMAT, RADIANCE_FORMAT};
|
||||
@@ -82,6 +83,10 @@ pub struct RendererOptions {
|
||||
/// on the seed and its number ([`Frame::number`]), so the same seed and number give
|
||||
/// the same image, and different numbers independent noise.
|
||||
pub noise_seed: u64,
|
||||
/// From camera C1, the detector's fixed pattern measured from calibration frames,
|
||||
/// in place of the one its fields generate. It must match the camera's size and
|
||||
/// channels.
|
||||
pub fixed_pattern: Option<Arc<FixedPattern>>,
|
||||
}
|
||||
|
||||
/// One rendered image.
|
||||
@@ -299,6 +304,16 @@ impl Renderer {
|
||||
row,
|
||||
)
|
||||
});
|
||||
if let Some(p) = &options.fixed_pattern {
|
||||
if (p.width, p.height, p.channels)
|
||||
!= (camera.width, camera.height, camera.channels.len() as u32)
|
||||
|| p.values.len() != (p.width * p.height * p.channels) as usize
|
||||
{
|
||||
return Err(Error::InvalidCamera(
|
||||
"the fixed pattern must match the camera's size and channels".to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if options.fidelity.lighting >= LightingTier::L1 && options.earth_texture.is_none() {
|
||||
return Err(Error::MissingInput(format!(
|
||||
"lighting {} draws Earth's surface from an image: set RendererOptions::earth_texture",
|
||||
@@ -322,7 +337,17 @@ impl Renderer {
|
||||
blur,
|
||||
shadows: ShadowPass::new(&ctx, options.shadow_map_size.unwrap_or(4096)),
|
||||
earthshine: EarthshinePass::new(&ctx),
|
||||
sensor: SensorPass::new(&ctx, stars.is_some(), area && camera.detector.is_noisy()),
|
||||
sensor: SensorPass::new(
|
||||
&ctx,
|
||||
stars.is_some(),
|
||||
area && camera.detector.is_noisy(),
|
||||
match &options.fixed_pattern {
|
||||
_ if !area => SensorPattern::None,
|
||||
Some(p) => SensorPattern::Loaded(p),
|
||||
None if camera.detector.has_fixed_pattern() => SensorPattern::Generated,
|
||||
None => SensorPattern::None,
|
||||
},
|
||||
),
|
||||
stars: stars.map(|catalog| {
|
||||
(
|
||||
catalog,
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
// from a counter-based generator keyed by the seed, the frame number and the value's
|
||||
// index, so a frame's noise is the same every time it is rendered.
|
||||
//
|
||||
// With PATTERN (also declared by the includer: 0 for none, 1 generated, 2 loaded):
|
||||
// each value's response to light and its dark current scaled by the detector's fixed
|
||||
// pattern, generated from a hash of the pattern seed and the index (as
|
||||
// FixedPattern::generated_value computes on the CPU), or read from calibration maps.
|
||||
//
|
||||
// Output is u16 per channel, channels interleaved per pixel, rows from the top. WGSL
|
||||
// has no 16-bit integers, so each invocation computes two consecutive values and packs
|
||||
// them into one u32 word, low half first. Two invocations never touch the same word.
|
||||
@@ -21,6 +26,10 @@ struct Sensor {
|
||||
noise: vec4<f32>,
|
||||
// The frame number, then the seed's low and high words.
|
||||
key: vec4<u32>,
|
||||
// PRNU and DSNU (fractions RMS), the hot pixels' fraction and multiple.
|
||||
pattern: vec4<f32>,
|
||||
// The pattern seed's low and high words, then the pattern's stream word.
|
||||
pattern_key: vec4<u32>,
|
||||
// The lens distortion (distortion.wgsl, included first).
|
||||
distortion: Distortion,
|
||||
}
|
||||
@@ -31,6 +40,8 @@ struct Sensor {
|
||||
// With STARS (declared by the code that includes this file): the stars' electrons per
|
||||
// value, 1/64 of an electron per unit (stars.wgsl).
|
||||
@group(0) @binding(3) var<storage, read> star_electrons: array<u32>;
|
||||
// With PATTERN 2: response and dark factors per value.
|
||||
@group(0) @binding(4) var<storage, read> loaded_pattern: array<vec2<f32>>;
|
||||
|
||||
// A hash of four words into four (Jarzynski and Olano 2020, pcg4d).
|
||||
fn pcg4d(input: vec4<u32>) -> vec4<u32> {
|
||||
@@ -68,6 +79,22 @@ fn poisson(mean: f32, u: f32) -> f32 {
|
||||
return k;
|
||||
}
|
||||
|
||||
// Value i's response and dark factors.
|
||||
fn fixed_pattern(i: u32) -> vec2<f32> {
|
||||
if PATTERN == 2u {
|
||||
return loaded_pattern[i];
|
||||
}
|
||||
let r = pcg4d(vec4<u32>(s.pattern_key.x, i, s.pattern_key.z, s.pattern_key.y));
|
||||
let radius = sqrt(-2.0 * log(unit(r.x)));
|
||||
let angle = 6.2831853 * unit(r.y);
|
||||
let response = max(1.0 + s.pattern.x * radius * cos(angle), 0.0);
|
||||
var dark = max(1.0 + s.pattern.y * radius * sin(angle), 0.0);
|
||||
if unit(r.z) < s.pattern.z {
|
||||
dark = s.pattern.w;
|
||||
}
|
||||
return vec2<f32>(response, dark);
|
||||
}
|
||||
|
||||
// Above this mean a Poisson count is drawn as a Gaussian of the same variance.
|
||||
const SMALL: f32 = 50.0;
|
||||
|
||||
@@ -98,13 +125,19 @@ fn value(i: u32) -> u32 {
|
||||
// Light from the stars adds before the pixel fills.
|
||||
collected += f32(star_electrons[i]) / 64.0;
|
||||
}
|
||||
var dark = 1.0;
|
||||
if PATTERN != 0u {
|
||||
let f = fixed_pattern(i);
|
||||
collected *= f.x;
|
||||
dark = f.y;
|
||||
}
|
||||
var electrons = clamp(collected, 0.0, s.detector.y);
|
||||
if NOISE {
|
||||
let r = pcg4d(vec4<u32>(i, s.key.x, s.key.y, s.key.z));
|
||||
// Two independent standard normals (Box-Muller).
|
||||
let radius = sqrt(-2.0 * log(unit(r.x)));
|
||||
let angle = 6.2831853 * unit(r.y);
|
||||
let mean = collected + s.noise.z;
|
||||
let mean = collected + s.noise.z * dark;
|
||||
var count = mean;
|
||||
if s.noise.x > 0.5 {
|
||||
if mean > SMALL {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use crate::common;
|
||||
use crate::pixel_area::plate;
|
||||
|
||||
use resolved::gpu::sensor::{SensorPass, SensorUniforms};
|
||||
use resolved::gpu::sensor::{SensorPass, SensorPattern, SensorUniforms};
|
||||
use resolved::gpu::targets::{FrameTargets, TargetLayout};
|
||||
use resolved::gpu::AdapterChoice;
|
||||
use resolved::nalgebra::{UnitQuaternion, Vector2, Vector3};
|
||||
@@ -302,7 +302,7 @@ fn the_sensor_uses_the_undistorted_line_of_sight() {
|
||||
targets.radiance.size(),
|
||||
);
|
||||
let mut encoder = ctx.device.create_command_encoder(&Default::default());
|
||||
SensorPass::new(ctx, false, false).encode(
|
||||
SensorPass::new(ctx, false, false, SensorPattern::None).encode(
|
||||
ctx,
|
||||
&mut encoder,
|
||||
&targets,
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
//! Camera tier C1's fixed pattern: generated in the shader as on the CPU, the same in
|
||||
//! every frame, and loaded from calibration frames, on the GPU and on the CPU
|
||||
//! (lavapipe).
|
||||
|
||||
use crate::common;
|
||||
|
||||
use resolved::camera::Detector;
|
||||
use resolved::gpu::sensor::{SensorPass, SensorPattern, SensorUniforms};
|
||||
use resolved::gpu::targets::{FrameTargets, TargetLayout};
|
||||
use resolved::gpu::GpuContext;
|
||||
use resolved::radiometry::Radiometry;
|
||||
use resolved::{CameraConfig, FixedPattern};
|
||||
|
||||
const SIZE: u32 = 256;
|
||||
|
||||
/// 256 x 256 pixels of a 1 m focal length (falloff off axis under 1e-5), reading whole
|
||||
/// electrons (1 e-/DN, 16 bits, bias 100) with no noise or pattern unless set.
|
||||
fn camera(exposure: f64, detector: Detector) -> CameraConfig {
|
||||
CameraConfig {
|
||||
width: SIZE,
|
||||
height: SIZE,
|
||||
focal_length: 1.0,
|
||||
exposure,
|
||||
detector,
|
||||
..CameraConfig::example()
|
||||
}
|
||||
}
|
||||
|
||||
fn counting() -> Detector {
|
||||
Detector {
|
||||
gain: 1.0,
|
||||
bias: 100.0,
|
||||
full_well: 60_000.0,
|
||||
adc_bits: 16,
|
||||
..CameraConfig::example().detector.noiseless()
|
||||
}
|
||||
}
|
||||
|
||||
/// The sensor pass over a flat field of `electrons` per value, with `pattern`, for
|
||||
/// frame `frame` of noise seed `seed`: the DN less the bias.
|
||||
fn flat(
|
||||
ctx: &GpuContext,
|
||||
config: &CameraConfig,
|
||||
pattern: SensorPattern,
|
||||
electrons: f64,
|
||||
seed: u64,
|
||||
frame: u64,
|
||||
) -> Vec<f64> {
|
||||
let targets = FrameTargets::new(ctx, &TargetLayout::new(SIZE, SIZE, 3));
|
||||
let radiometry = Radiometry::new(config);
|
||||
let level = (electrons / radiometry.electrons_per_radiance) as f32;
|
||||
let radiance = vec![[level, level, level, 0.0]; (SIZE * SIZE) as usize];
|
||||
ctx.queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &targets.radiance,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
bytemuck::cast_slice(&radiance),
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(SIZE * 16),
|
||||
rows_per_image: Some(SIZE),
|
||||
},
|
||||
targets.radiance.size(),
|
||||
);
|
||||
let pass = SensorPass::new(ctx, false, config.detector.is_noisy(), pattern);
|
||||
let mut uniforms = SensorUniforms::new(config, &radiometry, false);
|
||||
uniforms.set_key(seed, frame);
|
||||
let mut encoder = ctx.device.create_command_encoder(&Default::default());
|
||||
pass.encode(ctx, &mut encoder, &targets, &uniforms, None);
|
||||
ctx.queue.submit([encoder.finish()]);
|
||||
let bias = config.detector.bias;
|
||||
targets
|
||||
.read_dn(ctx)
|
||||
.into_iter()
|
||||
.map(|d| d as f64 - bias)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn mean(x: &[f64]) -> f64 {
|
||||
x.iter().sum::<f64>() / x.len() as f64
|
||||
}
|
||||
|
||||
fn sd(x: &[f64]) -> f64 {
|
||||
let m = mean(x);
|
||||
(x.iter().map(|v| (v - m) * (v - m)).sum::<f64>() / (x.len() - 1) as f64).sqrt()
|
||||
}
|
||||
|
||||
fn correlation(a: &[f64], b: &[f64]) -> f64 {
|
||||
let (ma, mb) = (mean(a), mean(b));
|
||||
let cov: f64 = a.iter().zip(b).map(|(x, y)| (x - ma) * (y - mb)).sum();
|
||||
cov / (sd(a) * sd(b)) / (a.len() - 1) as f64
|
||||
}
|
||||
|
||||
/// The largest difference between `got` and `want`: at most 0.5 DN if `got` is `want`
|
||||
/// rounded, and up to 0.005 more for the shader's f32 arithmetic, a few parts in 10^7
|
||||
/// of 12,000 electrons.
|
||||
fn largest_error(got: &[f64], want: impl Iterator<Item = f64>) -> f64 {
|
||||
got.iter()
|
||||
.zip(want)
|
||||
.fold(0.0f64, |worst, (g, w)| worst.max((g - w).abs()))
|
||||
}
|
||||
|
||||
/// Value `i`'s share of the light on axis: cos^4 of its pixel's angle off axis, at most
|
||||
/// 2e-6 short of 1 here.
|
||||
fn falloff(config: &CameraConfig, i: usize) -> f64 {
|
||||
let pixel = i as u32 / 3;
|
||||
let ray = config.intrinsics().ray(&resolved::nalgebra::Vector2::new(
|
||||
(pixel % SIZE) as f64 + 0.5,
|
||||
(pixel / SIZE) as f64 + 0.5,
|
||||
));
|
||||
ray.z.powi(4)
|
||||
}
|
||||
|
||||
const ROUNDED: f64 = 0.505;
|
||||
|
||||
#[test]
|
||||
fn the_generated_pattern_is_the_cpus() {
|
||||
// No noise. A flat field of 10,000 electrons reads 10,000 times each value's
|
||||
// response, and a dark frame of 100 or 200 dark electrons that times each value's
|
||||
// dark factor, as FixedPattern::generate gives them, to the rounding (the shader's
|
||||
// f32 against the CPU's f64). The
|
||||
// response has mean 1 and the requested spread to 2 %, the dark factor (hot pixels
|
||||
// aside) too, and the dark frames scale with the exposure.
|
||||
let detector = Detector {
|
||||
prnu: 0.01,
|
||||
dsnu: 0.1,
|
||||
hot_pixel_fraction: 1e-3,
|
||||
hot_pixel_multiple: 50.0,
|
||||
pattern_seed: 3,
|
||||
..counting()
|
||||
};
|
||||
let cpu = FixedPattern::generate(&detector, SIZE, SIZE, 3);
|
||||
for ctx in common::contexts() {
|
||||
let name = &ctx.info.name;
|
||||
let lit = flat(
|
||||
ctx,
|
||||
&camera(1e-4, detector),
|
||||
SensorPattern::Generated,
|
||||
1e4,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
let lens = camera(1e-4, counting());
|
||||
let want = cpu
|
||||
.values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| 1e4 * v[0] as f64 * falloff(&lens, i));
|
||||
let worst = largest_error(&lit, want);
|
||||
assert!(worst <= ROUNDED, "{name}: off by {worst}");
|
||||
let response: Vec<f64> = lit.iter().map(|d| d / 1e4).collect();
|
||||
assert!(
|
||||
(mean(&response) - 1.0).abs() < 1e-4,
|
||||
"{name}: mean {}",
|
||||
mean(&response)
|
||||
);
|
||||
assert!(
|
||||
(sd(&response) / 0.01 - 1.0).abs() < 0.02,
|
||||
"{name}: PRNU {}",
|
||||
sd(&response)
|
||||
);
|
||||
|
||||
let lit = Detector {
|
||||
dark_current: 1000.0,
|
||||
..detector
|
||||
};
|
||||
let short = flat(ctx, &camera(0.1, lit), SensorPattern::Generated, 0.0, 0, 0);
|
||||
let long = flat(ctx, &camera(0.2, lit), SensorPattern::Generated, 0.0, 0, 0);
|
||||
for (frame, electrons) in [(&short, 100.0), (&long, 200.0)] {
|
||||
let want = cpu.values.iter().map(|v| electrons * v[1] as f64);
|
||||
let worst = largest_error(frame, want);
|
||||
assert!(worst <= ROUNDED, "{name}: {electrons}: off by {worst}");
|
||||
}
|
||||
let hot = short.iter().filter(|&&d| d == 5000.0).count();
|
||||
let cpu_hot = cpu.values.iter().filter(|v| v[1] == 50.0).count();
|
||||
assert_eq!(hot, cpu_hot, "{name}: hot pixels");
|
||||
let ordinary: Vec<f64> = long
|
||||
.iter()
|
||||
.filter(|&&d| d != 10_000.0)
|
||||
.map(|d| d / 200.0)
|
||||
.collect();
|
||||
assert!(
|
||||
(sd(&ordinary) / 0.1 - 1.0).abs() < 0.02,
|
||||
"{name}: DSNU {}",
|
||||
sd(&ordinary)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pattern_stays_through_frames_and_noise_seeds() {
|
||||
// With shot and read noise on a flat field of 10,000 electrons, the 1 % pattern
|
||||
// (100 electrons) and the noise (about 100) are equal parts of each frame's spread,
|
||||
// so two frames, or two noise seeds, correlate by 10,000 / 20,025 = 0.50. Another
|
||||
// pattern seed shares nothing: |r| < 0.01.
|
||||
let detector = Detector {
|
||||
prnu: 0.01,
|
||||
read_noise: 5.0,
|
||||
shot_noise: true,
|
||||
..counting()
|
||||
};
|
||||
let config = camera(1e-4, detector);
|
||||
let other = camera(
|
||||
1e-4,
|
||||
Detector {
|
||||
pattern_seed: 1,
|
||||
..detector
|
||||
},
|
||||
);
|
||||
let want = 1e4 / (1e4 + 1e4 + 25.0 + 1.0 / 12.0);
|
||||
for ctx in common::contexts() {
|
||||
let name = &ctx.info.name;
|
||||
let base = flat(ctx, &config, SensorPattern::Generated, 1e4, 0, 0);
|
||||
for (what, frame, r_want) in [
|
||||
(
|
||||
"next frame",
|
||||
flat(ctx, &config, SensorPattern::Generated, 1e4, 0, 1),
|
||||
want,
|
||||
),
|
||||
(
|
||||
"next seed",
|
||||
flat(ctx, &config, SensorPattern::Generated, 1e4, 1, 0),
|
||||
want,
|
||||
),
|
||||
(
|
||||
"other pattern",
|
||||
flat(ctx, &other, SensorPattern::Generated, 1e4, 0, 1),
|
||||
0.0,
|
||||
),
|
||||
] {
|
||||
let r = correlation(&base, &frame);
|
||||
assert!(
|
||||
(r - r_want).abs() < 0.01,
|
||||
"{name}: {what}: r = {r}, expected {r_want}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calibration_maps_apply_exactly() {
|
||||
// A pattern from made-up calibration frames, a response from 0.8 to 1.2 and a
|
||||
// dark factor from 0 to 3. Each value of a flat field reads its 10,000 electrons
|
||||
// times its response, and of a dark frame its 100 dark electrons times its dark
|
||||
// factor, to the rounding; a pattern the wrong size is refused.
|
||||
let n = (SIZE * SIZE * 3) as usize;
|
||||
let flat_frame: Vec<f32> = (0..n).map(|i| 1000.0 + (i * 7919 % 400) as f32).collect();
|
||||
let dark_frame: Vec<f32> = (0..n).map(|i| (i * 104_729 % 300) as f32).collect();
|
||||
let pattern = FixedPattern::from_calibration(SIZE, SIZE, 3, &flat_frame, &dark_frame).unwrap();
|
||||
for ctx in common::contexts() {
|
||||
let name = &ctx.info.name;
|
||||
let lit = flat(
|
||||
ctx,
|
||||
&camera(1e-4, counting()),
|
||||
SensorPattern::Loaded(&pattern),
|
||||
1e4,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
let lens = camera(1e-4, counting());
|
||||
let want = pattern
|
||||
.values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| 1e4 * v[0] as f64 * falloff(&lens, i));
|
||||
let worst = largest_error(&lit, want);
|
||||
assert!(worst <= ROUNDED, "{name}: flat: off by {worst}");
|
||||
|
||||
let dark = Detector {
|
||||
dark_current: 1000.0,
|
||||
..counting()
|
||||
};
|
||||
let frame = flat(
|
||||
ctx,
|
||||
&camera(0.1, dark),
|
||||
SensorPattern::Loaded(&pattern),
|
||||
0.0,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
let want = pattern.values.iter().map(|v| 100.0 * v[1] as f64);
|
||||
let worst = largest_error(&frame, want);
|
||||
assert!(worst <= ROUNDED, "{name}: dark: off by {worst}");
|
||||
}
|
||||
|
||||
let _devices = common::device_lock();
|
||||
let small = FixedPattern::from_calibration(2, 2, 3, &[1.0; 12], &[1.0; 12]).unwrap();
|
||||
let options = resolved::RendererOptions {
|
||||
adapter: resolved::gpu::AdapterChoice::Cpu,
|
||||
fidelity: resolved::Fidelity {
|
||||
camera: resolved::CameraTier::C1,
|
||||
..Default::default()
|
||||
},
|
||||
fixed_pattern: Some(std::sync::Arc::new(small)),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(resolved::Renderer::new(camera(1e-4, counting()), options).is_err());
|
||||
}
|
||||
@@ -11,6 +11,7 @@ mod distortion;
|
||||
mod earth_texture;
|
||||
mod earthshine;
|
||||
mod eclipses;
|
||||
mod fixed_pattern;
|
||||
mod gpu_context;
|
||||
mod mesh;
|
||||
mod moon_texture;
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::common;
|
||||
use std::sync::Arc;
|
||||
|
||||
use resolved::camera::Detector;
|
||||
use resolved::gpu::sensor::{SensorPass, SensorUniforms};
|
||||
use resolved::gpu::sensor::{SensorPass, SensorPattern, SensorUniforms};
|
||||
use resolved::gpu::targets::{FrameTargets, TargetLayout};
|
||||
use resolved::gpu::{AdapterChoice, GpuContext};
|
||||
use resolved::nalgebra::{UnitQuaternion, Vector2, Vector3};
|
||||
@@ -32,13 +32,13 @@ fn camera(detector: Detector) -> CameraConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// The example detector with its noise replaced.
|
||||
/// The example detector with its noise replaced and no fixed pattern.
|
||||
fn detector(read_noise: f64, dark_current: f64, shot_noise: bool) -> Detector {
|
||||
Detector {
|
||||
read_noise,
|
||||
dark_current,
|
||||
shot_noise,
|
||||
..CameraConfig::example().detector
|
||||
..CameraConfig::example().detector.noiseless()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ fn counting(read_noise: f64, dark_current: f64) -> Detector {
|
||||
read_noise,
|
||||
dark_current,
|
||||
shot_noise: true,
|
||||
..CameraConfig::example().detector.noiseless()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +84,7 @@ fn flat(
|
||||
},
|
||||
targets.radiance.size(),
|
||||
);
|
||||
let pass = SensorPass::new(ctx, false, true);
|
||||
let pass = SensorPass::new(ctx, false, true, SensorPattern::None);
|
||||
let mut uniforms = SensorUniforms::new(config, &radiometry, false);
|
||||
uniforms.set_key(seed, frame);
|
||||
let mut encoder = ctx.device.create_command_encoder(&Default::default());
|
||||
@@ -271,12 +272,14 @@ fn a_faint_star_has_the_signal_to_noise_the_detector_gives() {
|
||||
// mean and spread match that to 2 % and 12 % (400 stars measure a spread to 3.5 %). The
|
||||
// renderer numbers its frames, and setting the number back repeats a frame.
|
||||
let _devices = common::device_lock();
|
||||
let photometry = StarPhotometry::new(&camera(CameraConfig::example().detector));
|
||||
// The example detector's noise, without its fixed pattern.
|
||||
let noisy = detector(5.0, 20.0, true);
|
||||
let photometry = StarPhotometry::new(&camera(noisy));
|
||||
let signal = 300.0;
|
||||
let exposure = signal / (photometry.rate(1, 11.5, 0.6) * photometry.aperture);
|
||||
let config = CameraConfig {
|
||||
exposure,
|
||||
..camera(CameraConfig::example().detector)
|
||||
..camera(noisy)
|
||||
};
|
||||
let k = config.intrinsics();
|
||||
let position = Vector3::new(4.2e7, 0.0, 0.0);
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::common;
|
||||
|
||||
use resolved::bodies::Appearance;
|
||||
use resolved::gpu::bodies::{BodiesPass, BodiesUniforms, BodyRadiometry};
|
||||
use resolved::gpu::sensor::{SensorPass, SensorUniforms};
|
||||
use resolved::gpu::sensor::{SensorPass, SensorPattern, SensorUniforms};
|
||||
use resolved::gpu::targets::{FrameTargets, TargetLayout};
|
||||
use resolved::gpu::GpuContext;
|
||||
use resolved::nalgebra::{UnitQuaternion, Vector2, Vector3};
|
||||
@@ -32,7 +32,7 @@ fn sense(ctx: &GpuContext, name: &str, radiance: &[[f32; 4]]) -> Vec<u16> {
|
||||
},
|
||||
targets.radiance.size(),
|
||||
);
|
||||
let pass = SensorPass::new(ctx, false, false);
|
||||
let pass = SensorPass::new(ctx, false, false, SensorPattern::None);
|
||||
let uniforms = SensorUniforms::new(&config, &Radiometry::new(&config), false);
|
||||
let mut encoder = ctx.device.create_command_encoder(&Default::default());
|
||||
pass.encode(ctx, &mut encoder, &targets, &uniforms, None);
|
||||
@@ -164,7 +164,13 @@ fn sunlit_earth_through_the_whole_chain() {
|
||||
let targets = FrameTargets::new(ctx, &TargetLayout::new(config.width, config.height, 3));
|
||||
let mut encoder = ctx.device.create_command_encoder(&Default::default());
|
||||
BodiesPass::new(ctx, 1, false).encode(ctx, &mut encoder, &targets, &bodies, None);
|
||||
SensorPass::new(ctx, false, false).encode(ctx, &mut encoder, &targets, &sensor, None);
|
||||
SensorPass::new(ctx, false, false, SensorPattern::None).encode(
|
||||
ctx,
|
||||
&mut encoder,
|
||||
&targets,
|
||||
&sensor,
|
||||
None,
|
||||
);
|
||||
ctx.queue.submit([encoder.finish()]);
|
||||
let dn = targets.read_dn(ctx);
|
||||
common::save_dn("sensor-earth", &ctx.info, &dn);
|
||||
|
||||
@@ -36,9 +36,7 @@ fn camera(exposure: f64) -> CameraConfig {
|
||||
bias: 100.0,
|
||||
full_well: 60_000.0,
|
||||
adc_bits: 16,
|
||||
read_noise: 0.0,
|
||||
dark_current: 0.0,
|
||||
shot_noise: false,
|
||||
..CameraConfig::example().detector.noiseless()
|
||||
},
|
||||
..CameraConfig::example()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user