Added Airy and PSF via FFT. Decided to make it a C2 feature
This commit is contained in:
1 parent
fbd7ce4147
commit
5e2769faa8
25 files changed
+2319
-117
No files matched your search
@@ -57,10 +57,11 @@ struct RenderArgs {
|
||||
/// catalog).
|
||||
#[arg(long, default_value = "L0")]
|
||||
lighting: LightingTier,
|
||||
/// Camera fidelity tier: C0, or C1 (adds pixels that collect light over their area,
|
||||
/// Camera fidelity tier: C0, 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, fixed pattern and blooming, and at lighting L1 stars).
|
||||
/// noise, fixed pattern and blooming, and at lighting L1 stars), or C2 (adds the
|
||||
/// lens blur by FFT, with diffraction or a measured PSF, whose wings reach far).
|
||||
#[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
|
||||
@@ -74,6 +75,11 @@ struct RenderArgs {
|
||||
/// give the same image.
|
||||
#[arg(long, default_value_t = 0)]
|
||||
frame: u64,
|
||||
/// From camera C2: how far in pixels the PSF must reach from a point (default 16).
|
||||
/// The FFT grid grows to hold it, and the PSF then reaches as far as the grid
|
||||
/// allows: 64 px at 1920 x 1080 and 2 x 2 supersampling, up to 1088 px at 1 x 1.
|
||||
#[arg(long, value_name = "PIXELS")]
|
||||
psf_reach: Option<u32>,
|
||||
/// Device: auto, gpu, cpu, or part of an adapter's name.
|
||||
#[arg(long, default_value = "auto")]
|
||||
adapter: String,
|
||||
@@ -162,7 +168,8 @@ fn render(args: &RenderArgs) -> Result<(), String> {
|
||||
}
|
||||
let path = args.scene.as_path();
|
||||
let file = SceneFile::read(path)?;
|
||||
let camera = file.camera();
|
||||
let dir = path.parent().unwrap_or(Path::new("."));
|
||||
let camera = file.camera(dir)?;
|
||||
|
||||
// What the tiers need, gathered before the renderer exists so it can check them.
|
||||
let start = Instant::now();
|
||||
@@ -230,12 +237,12 @@ fn render(args: &RenderArgs) -> Result<(), String> {
|
||||
stars,
|
||||
noise_seed: args.seed,
|
||||
fixed_pattern: None,
|
||||
psf_reach: args.psf_reach,
|
||||
};
|
||||
let start = Instant::now();
|
||||
let mut renderer = Renderer::new(camera, options).map_err(|e| e.to_string())?;
|
||||
let setup = start.elapsed();
|
||||
let start = Instant::now();
|
||||
let dir = path.parent().unwrap_or(Path::new("."));
|
||||
let scene = file.scene(&mut renderer, dir, args.kernels.as_deref())?;
|
||||
let observer = file.observer()?;
|
||||
let loading = start.elapsed();
|
||||
|
||||
@@ -12,7 +12,7 @@ use resolved::nalgebra::{Quaternion, UnitQuaternion, Vector2, Vector3};
|
||||
use resolved::scene::Velocities;
|
||||
use resolved::{
|
||||
Blooming, BrownConrady, CameraConfig, LoadOptions, Material, Mesh, ModelId, Pose, Psf,
|
||||
Renderer, Scene, Spectrum, Target,
|
||||
PsfImage, Renderer, Scene, Spectrum, Target,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -88,6 +88,13 @@ pub struct CameraFile {
|
||||
/// 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>,
|
||||
/// Diffraction by the aperture, from camera C2: the diameter of a centered
|
||||
/// obstruction as a fraction of the aperture's, 0 for none.
|
||||
pub psf_airy: Option<f64>,
|
||||
/// A measured PSF, from camera C2: `{ image = "spot.png", pixel_scale = 0.25 }`, a
|
||||
/// PNG relative to the scene file, grayscale for every channel or RGB for three,
|
||||
/// whose samples are `pixel_scale` pixels apart.
|
||||
pub psf_measured: Option<MeasuredFile>,
|
||||
/// Lens distortion from camera C1, in OpenCV's order: [k1, k2, p1, p2, k3].
|
||||
pub distortion: Option<[f64; 5]>,
|
||||
}
|
||||
@@ -101,6 +108,14 @@ pub enum BloomingFile {
|
||||
Neighbors(f64),
|
||||
}
|
||||
|
||||
/// A measured PSF in a scene file.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MeasuredFile {
|
||||
pub image: PathBuf,
|
||||
pub pixel_scale: f64,
|
||||
}
|
||||
|
||||
/// One sigma, or one per channel.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
@@ -165,7 +180,7 @@ impl SceneFile {
|
||||
}
|
||||
|
||||
/// The example camera with this file's overrides.
|
||||
pub fn camera(&self) -> CameraConfig {
|
||||
pub fn camera(&self, dir: &Path) -> Result<CameraConfig> {
|
||||
let c = &self.camera;
|
||||
let mut camera = CameraConfig::example();
|
||||
let set = |field: &mut f64, value: Option<f64>| {
|
||||
@@ -219,17 +234,30 @@ impl SceneFile {
|
||||
if let Some([k1, k2, p1, p2, k3]) = c.distortion {
|
||||
camera.distortion = Some(BrownConrady { k1, k2, k3, p1, p2 });
|
||||
}
|
||||
camera.psf = match &c.psf_sigma {
|
||||
None => camera.psf,
|
||||
Some(Sigma::All(s)) if *s == 0.0 => Psf::None,
|
||||
Some(Sigma::All(s)) => Psf::Gaussian {
|
||||
let given = [
|
||||
c.psf_sigma.is_some(),
|
||||
c.psf_airy.is_some(),
|
||||
c.psf_measured.is_some(),
|
||||
];
|
||||
if given.iter().filter(|&&g| g).count() > 1 {
|
||||
return Err("give at most one of psf_sigma, psf_airy and psf_measured".into());
|
||||
}
|
||||
camera.psf = match (&c.psf_sigma, c.psf_airy, &c.psf_measured) {
|
||||
(Some(Sigma::All(s)), _, _) if *s == 0.0 => Psf::None,
|
||||
(Some(Sigma::All(s)), _, _) => Psf::Gaussian {
|
||||
sigma: vec![*s; camera.channels.len()],
|
||||
},
|
||||
Some(Sigma::PerChannel(sigma)) => Psf::Gaussian {
|
||||
(Some(Sigma::PerChannel(sigma)), _, _) => Psf::Gaussian {
|
||||
sigma: sigma.clone(),
|
||||
},
|
||||
(_, Some(obstruction), _) => Psf::Airy { obstruction },
|
||||
(_, _, Some(m)) => Psf::Measured {
|
||||
image: psf_image(&dir.join(&m.image))?,
|
||||
pixel_scale: m.pixel_scale,
|
||||
},
|
||||
(None, None, None) => camera.psf,
|
||||
};
|
||||
camera
|
||||
Ok(camera)
|
||||
}
|
||||
|
||||
/// Earth's and the Moon's appearance.
|
||||
@@ -407,6 +435,27 @@ impl SceneFile {
|
||||
}
|
||||
|
||||
/// Where to look for astrokit's kernels: `explicit` alone if given; otherwise
|
||||
/// A measured PSF's image from a PNG: one image from a grayscale one, three from an RGB
|
||||
/// one, each sample's value as a fraction of the largest the format holds.
|
||||
fn psf_image(path: &Path) -> Result<PsfImage> {
|
||||
let image = image::open(path).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
let (width, height) = (image.width(), image.height());
|
||||
let channels = if image.color().has_color() {
|
||||
let rgb = image.into_rgb16();
|
||||
(0..3)
|
||||
.map(|k| rgb.pixels().map(|p| p[k] as f64 / 65535.0).collect())
|
||||
.collect()
|
||||
} else {
|
||||
let gray = image.into_luma16();
|
||||
vec![gray.pixels().map(|p| p[0] as f64 / 65535.0).collect()]
|
||||
};
|
||||
Ok(PsfImage {
|
||||
width,
|
||||
height,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
|
||||
/// `ASTROKIT_KERNELS`, then `data/kernels` under the working directory, then the
|
||||
/// sibling astrokit checkout this binary was built against.
|
||||
#[cfg(feature = "astrokit")]
|
||||
@@ -478,7 +527,10 @@ mod tests {
|
||||
#[test]
|
||||
fn minimal_file_uses_the_example_camera() {
|
||||
let file: SceneFile = toml::from_str(MINIMAL).unwrap();
|
||||
assert_eq!(file.camera(), CameraConfig::example());
|
||||
assert_eq!(
|
||||
file.camera(Path::new(".")).unwrap(),
|
||||
CameraConfig::example()
|
||||
);
|
||||
assert!(file.targets.is_empty());
|
||||
let observer = file.observer().unwrap();
|
||||
// Looking from +X toward the origin.
|
||||
@@ -493,7 +545,7 @@ mod tests {
|
||||
pattern_seed = 9\nblooming = {{ column = 0.25 }}\n"
|
||||
))
|
||||
.unwrap();
|
||||
let camera = file.camera();
|
||||
let camera = file.camera(Path::new(".")).unwrap();
|
||||
assert_eq!((camera.width, camera.height), (640, 1080));
|
||||
assert_eq!(camera.exposure, 2e-3);
|
||||
assert_eq!(camera.detector.adc_bits, 10);
|
||||
@@ -520,18 +572,24 @@ mod tests {
|
||||
},
|
||||
),
|
||||
("psf_sigma = 0", Psf::None),
|
||||
("psf_airy = 0.3", Psf::Airy { obstruction: 0.3 }),
|
||||
] {
|
||||
let file: SceneFile =
|
||||
toml::from_str(&format!("{MINIMAL}\n[camera]\n{line}\n")).unwrap();
|
||||
assert_eq!(file.camera().psf, psf, "{line}");
|
||||
assert_eq!(file.camera(Path::new(".")).unwrap().psf, psf, "{line}");
|
||||
}
|
||||
let file: SceneFile = toml::from_str(&format!(
|
||||
"{MINIMAL}\n[camera]\npsf_sigma = 1\npsf_airy = 0\n"
|
||||
))
|
||||
.unwrap();
|
||||
assert!(file.camera(Path::new(".")).is_err());
|
||||
|
||||
let file: SceneFile = toml::from_str(&format!(
|
||||
"{MINIMAL}\n[camera]\ndistortion = [-0.1, 0.02, 1e-3, -2e-3, 0.005]\n"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
file.camera().distortion,
|
||||
file.camera(Path::new(".")).unwrap().distortion,
|
||||
Some(BrownConrady {
|
||||
k1: -0.1,
|
||||
k2: 0.02,
|
||||
@@ -542,6 +600,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_measured_psf_loads_from_a_png() {
|
||||
// 16-bit RGB, 3 x 2: one image per channel, each sample's value over 65,535.
|
||||
let dir = std::env::temp_dir().join(format!("resolved-psf-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let values: Vec<u16> = (0..18).map(|i| i * 1000).collect();
|
||||
image::ImageBuffer::<image::Rgb<u16>, _>::from_raw(3, 2, values)
|
||||
.unwrap()
|
||||
.save(dir.join("spot.png"))
|
||||
.unwrap();
|
||||
let file: SceneFile = toml::from_str(&format!(
|
||||
"{MINIMAL}\n[camera]\npsf_measured = {{ image = \"spot.png\", pixel_scale = 0.5 }}\n"
|
||||
))
|
||||
.unwrap();
|
||||
let camera = file.camera(&dir).unwrap();
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
let Psf::Measured { image, pixel_scale } = camera.psf else {
|
||||
panic!("{:?}", camera.psf);
|
||||
};
|
||||
assert_eq!((image.width, image.height, pixel_scale), (3, 2, 0.5));
|
||||
for (k, channel) in image.channels.iter().enumerate() {
|
||||
let want: Vec<f64> = (0..6)
|
||||
.map(|i| ((3 * i + k) * 1000) as f64 / 65535.0)
|
||||
.collect();
|
||||
for (a, b) in channel.iter().zip(&want) {
|
||||
assert!((a - b).abs() < 1e-6, "channel {k}: {channel:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn earth_month_comes_from_the_setting() {
|
||||
let with = |extra: &str| -> SceneFile {
|
||||
|
||||
Reference in new issue
Block a user