#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FocusZone {
pub near_m: f64,
pub far_m: f64,
}
impl FocusZone {
#[must_use]
pub fn new(near_m: f64, far_m: f64) -> Self {
let near = near_m.max(0.0);
let far = far_m.max(near);
Self {
near_m: near,
far_m: far,
}
}
#[must_use]
pub fn depth_m(&self) -> f64 {
self.far_m - self.near_m
}
#[must_use]
pub fn extends_to_infinity(&self) -> bool {
self.far_m.is_infinite()
}
#[must_use]
pub fn contains(&self, distance_m: f64) -> bool {
distance_m >= self.near_m && distance_m <= self.far_m
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DepthOfField {
pub focus_zone: FocusZone,
pub focal_length_mm: f64,
pub f_number: f64,
pub coc_mm: f64,
pub focus_distance_m: f64,
}
impl DepthOfField {
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn compute(
focal_length_mm: f64,
f_number: f64,
focus_distance_m: f64,
coc_mm: f64,
) -> Self {
let f = focal_length_mm / 1000.0; let d = focus_distance_m;
let c = coc_mm / 1000.0;
let denominator = f * f;
let depth_factor = f_number * c * d * d;
let near = if denominator > 0.0 {
(d * denominator) / (denominator + depth_factor)
} else {
d
};
let far_denom = denominator - depth_factor;
let far = if far_denom <= 0.0 {
f64::INFINITY
} else {
(d * denominator) / far_denom
};
Self {
focus_zone: FocusZone::new(near, far),
focal_length_mm,
f_number,
coc_mm,
focus_distance_m,
}
}
#[must_use]
pub fn is_in_focus(&self, distance_m: f64) -> bool {
self.focus_zone.contains(distance_m)
}
#[must_use]
pub fn hyperfocal_distance(&self) -> f64 {
let f = self.focal_length_mm / 1000.0;
let c = self.coc_mm / 1000.0;
(f * f) / (self.f_number * c) + f
}
#[must_use]
pub fn total_depth_m(&self) -> f64 {
self.focus_zone.depth_m()
}
#[must_use]
pub fn front_depth_m(&self) -> f64 {
(self.focus_distance_m - self.focus_zone.near_m).max(0.0)
}
#[must_use]
pub fn rear_depth_m(&self) -> f64 {
if self.focus_zone.far_m.is_infinite() {
f64::INFINITY
} else {
(self.focus_zone.far_m - self.focus_distance_m).max(0.0)
}
}
}
#[derive(Debug, Clone)]
pub struct DofAnalyzer {
pub coc_mm: f64,
}
impl DofAnalyzer {
#[must_use]
pub fn full_frame() -> Self {
Self { coc_mm: 0.029 }
}
#[must_use]
pub fn super35() -> Self {
Self { coc_mm: 0.019 }
}
#[must_use]
pub fn with_coc(coc_mm: f64) -> Self {
Self {
coc_mm: coc_mm.max(0.001),
}
}
#[must_use]
pub fn compute_dof(
&self,
focal_length_mm: f64,
f_number: f64,
focus_distance_m: f64,
) -> DepthOfField {
DepthOfField::compute(focal_length_mm, f_number, focus_distance_m, self.coc_mm)
}
#[must_use]
pub fn sharp_region_m(
&self,
focal_length_mm: f64,
f_number: f64,
focus_distance_m: f64,
) -> (f64, f64) {
let dof = self.compute_dof(focal_length_mm, f_number, focus_distance_m);
(dof.focus_zone.near_m, dof.focus_zone.far_m)
}
#[must_use]
pub fn is_shallow_dof(
&self,
focal_length_mm: f64,
f_number: f64,
focus_distance_m: f64,
) -> bool {
let dof = self.compute_dof(focal_length_mm, f_number, focus_distance_m);
dof.total_depth_m() < 0.5
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_focus_zone_contains() {
let zone = FocusZone::new(1.5, 3.0);
assert!(zone.contains(2.0));
assert!(!zone.contains(1.0));
assert!(!zone.contains(4.0));
}
#[test]
fn test_focus_zone_depth() {
let zone = FocusZone::new(2.0, 5.0);
assert!((zone.depth_m() - 3.0).abs() < 1e-10);
}
#[test]
fn test_focus_zone_infinity() {
let zone = FocusZone::new(3.0, f64::INFINITY);
assert!(zone.extends_to_infinity());
assert!(zone.depth_m().is_infinite());
}
#[test]
fn test_focus_zone_clamps_near() {
let zone = FocusZone::new(-1.0, 5.0);
assert_eq!(zone.near_m, 0.0);
}
#[test]
fn test_dof_compute_focus_within_zone() {
let dof = DepthOfField::compute(50.0, 2.8, 3.0, 0.029);
assert!(dof.is_in_focus(3.0));
}
#[test]
fn test_dof_near_less_than_focus_distance() {
let dof = DepthOfField::compute(50.0, 2.8, 3.0, 0.029);
assert!(dof.focus_zone.near_m < 3.0);
}
#[test]
fn test_dof_far_greater_than_focus_distance() {
let dof = DepthOfField::compute(50.0, 2.8, 3.0, 0.029);
assert!(dof.focus_zone.far_m > 3.0);
}
#[test]
fn test_hyperfocal_distance_positive() {
let dof = DepthOfField::compute(50.0, 8.0, 10.0, 0.029);
assert!(dof.hyperfocal_distance() > 0.0);
}
#[test]
fn test_wide_aperture_shallower_dof() {
let dof_wide = DepthOfField::compute(85.0, 1.4, 2.0, 0.029);
let dof_narrow = DepthOfField::compute(85.0, 16.0, 2.0, 0.029);
assert!(dof_wide.total_depth_m() < dof_narrow.total_depth_m());
}
#[test]
fn test_longer_focal_shallower_dof() {
let dof_wide = DepthOfField::compute(24.0, 5.6, 5.0, 0.029);
let dof_tele = DepthOfField::compute(200.0, 5.6, 5.0, 0.029);
assert!(dof_tele.total_depth_m() < dof_wide.total_depth_m());
}
#[test]
fn test_dof_analyzer_full_frame_coc() {
let analyzer = DofAnalyzer::full_frame();
assert!((analyzer.coc_mm - 0.029).abs() < 1e-6);
}
#[test]
fn test_dof_analyzer_super35_coc() {
let analyzer = DofAnalyzer::super35();
assert!((analyzer.coc_mm - 0.019).abs() < 1e-6);
}
#[test]
fn test_sharp_region_near_far() {
let analyzer = DofAnalyzer::full_frame();
let (near, far) = analyzer.sharp_region_m(50.0, 5.6, 5.0);
assert!(near < 5.0);
assert!(far > 5.0);
}
#[test]
fn test_is_shallow_dof() {
let analyzer = DofAnalyzer::full_frame();
assert!(analyzer.is_shallow_dof(85.0, 1.4, 1.5));
assert!(!analyzer.is_shallow_dof(24.0, 16.0, 5.0));
}
#[test]
fn test_front_rear_depth_sum() {
let dof = DepthOfField::compute(50.0, 5.6, 5.0, 0.029);
if !dof.focus_zone.extends_to_infinity() {
let sum = dof.front_depth_m() + dof.rear_depth_m();
assert!((sum - dof.total_depth_m()).abs() < 1e-6);
}
}
}