use std::collections::BTreeMap;
use std::sync::LazyLock;
use regex::Regex;
use crate::auxfun::{NO_NEIGHBOR, catmull_rom_interpolate};
use crate::calib::{
CalibDistortion, CalibTca, CalibVignetting, DistortionModel, TcaModel, VignettingModel,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LensType {
#[default]
Rectilinear,
FisheyeEquidistant,
FisheyeOrthographic,
FisheyeEquisolid,
FisheyeStereographic,
FisheyeThoby,
Equirectangular,
Panoramic,
Unknown,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Lens {
pub maker: String,
pub maker_localized: BTreeMap<String, String>,
pub model: String,
pub model_localized: BTreeMap<String, String>,
pub lens_type: LensType,
pub mounts: Vec<String>,
pub focal_min: f32,
pub focal_max: f32,
pub aperture_min: f32,
pub aperture_max: f32,
pub crop_factor: f32,
pub aspect_ratio: f32,
pub center_x: f32,
pub center_y: f32,
pub calib_distortion: Vec<CalibDistortion>,
pub calib_tca: Vec<CalibTca>,
pub calib_vignetting: Vec<CalibVignetting>,
pub score: i32,
}
const DISTORTION_TERM_COUNT: usize = 5;
const TCA_TERM_COUNT: usize = 6;
const VIGNETTING_TERM_COUNT: usize = 3;
fn distortion_kind(m: &DistortionModel) -> u8 {
match m {
DistortionModel::None => 0,
DistortionModel::Poly3 { .. } => 1,
DistortionModel::Poly5 { .. } => 2,
DistortionModel::Ptlens { .. } => 3,
}
}
fn tca_kind(m: &TcaModel) -> u8 {
match m {
TcaModel::None => 0,
TcaModel::Linear { .. } => 1,
TcaModel::Poly3 { .. } => 2,
}
}
fn vignetting_kind(m: &VignettingModel) -> u8 {
match m {
VignettingModel::None => 0,
VignettingModel::Pa { .. } => 1,
}
}
fn distortion_terms(m: &DistortionModel) -> [f32; DISTORTION_TERM_COUNT] {
match *m {
DistortionModel::None => [0.0; DISTORTION_TERM_COUNT],
DistortionModel::Poly3 { k1 } => [k1, 0.0, 0.0, 0.0, 0.0],
DistortionModel::Poly5 { k1, k2 } => [k1, k2, 0.0, 0.0, 0.0],
DistortionModel::Ptlens { a, b, c } => [a, b, c, 0.0, 0.0],
}
}
fn distortion_from_terms(kind: u8, t: [f32; DISTORTION_TERM_COUNT]) -> DistortionModel {
match kind {
1 => DistortionModel::Poly3 { k1: t[0] },
2 => DistortionModel::Poly5 { k1: t[0], k2: t[1] },
3 => DistortionModel::Ptlens {
a: t[0],
b: t[1],
c: t[2],
},
_ => DistortionModel::None,
}
}
fn tca_terms(m: &TcaModel) -> [f32; TCA_TERM_COUNT] {
match *m {
TcaModel::None => [0.0; TCA_TERM_COUNT],
TcaModel::Linear { kr, kb } => [kr, kb, 0.0, 0.0, 0.0, 0.0],
TcaModel::Poly3 { red, blue } => [red[0], blue[0], red[1], blue[1], red[2], blue[2]],
}
}
fn tca_from_terms(kind: u8, t: [f32; TCA_TERM_COUNT]) -> TcaModel {
match kind {
1 => TcaModel::Linear { kr: t[0], kb: t[1] },
2 => TcaModel::Poly3 {
red: [t[0], t[2], t[4]],
blue: [t[1], t[3], t[5]],
},
_ => TcaModel::None,
}
}
fn vignetting_terms(m: &VignettingModel) -> [f32; VIGNETTING_TERM_COUNT] {
match *m {
VignettingModel::None => [0.0; VIGNETTING_TERM_COUNT],
VignettingModel::Pa { k1, k2, k3 } => [k1, k2, k3],
}
}
fn vignetting_from_terms(kind: u8, t: [f32; VIGNETTING_TERM_COUNT]) -> VignettingModel {
match kind {
1 => VignettingModel::Pa {
k1: t[0],
k2: t[1],
k3: t[2],
},
_ => VignettingModel::None,
}
}
fn distortion_param_scales(_values: &mut [f32], _model_kind: u8, _index: usize) {
}
fn tca_param_scales(values: &mut [f32], _model_kind: u8, index: usize) {
if index < 2 {
for v in values.iter_mut() {
*v = 1.0;
}
}
}
fn vignetting_param_scales(values: &mut [f32], _model_kind: u8, _index: usize) {
for v in values.iter_mut() {
*v = 1.0;
}
}
fn insert_spline<T: Copy>(slots: &mut [Option<T>; 4], dists: &mut [f32; 4], dist: f32, val: T) {
if dist < 0.0 {
if dist > dists[1] {
dists[0] = dists[1];
dists[1] = dist;
slots[0] = slots[1];
slots[1] = Some(val);
} else if dist > dists[0] {
dists[0] = dist;
slots[0] = Some(val);
}
} else if dist < dists[2] {
dists[3] = dists[2];
dists[2] = dist;
slots[3] = slots[2];
slots[2] = Some(val);
} else if dist < dists[3] {
dists[3] = dist;
slots[3] = Some(val);
}
}
fn vignetting_dist(
lens_min_focal: f32,
lens_max_focal: f32,
sample: &CalibVignetting,
focal: f32,
aperture: f32,
distance: f32,
) -> f32 {
let mut f1 = focal - lens_min_focal;
let mut f2 = sample.focal - lens_min_focal;
let df = lens_max_focal - lens_min_focal;
if df != 0.0 {
f1 /= df;
f2 /= df;
}
let a1 = 4.0 / aperture;
let a2 = 4.0 / sample.aperture;
let d1 = 0.1 / distance;
let d2 = 0.1 / sample.distance;
((f2 - f1).powi(2) + (a2 - a1).powi(2) + (d2 - d1).powi(2)).sqrt()
}
impl Lens {
pub fn interpolate_distortion(&self, focal: f32) -> Option<CalibDistortion> {
let mut slots: [Option<&CalibDistortion>; 4] = [None; 4];
let mut dists: [f32; 4] = [-f32::MAX, -f32::MAX, f32::MAX, f32::MAX];
let mut model_kind: u8 = 0;
for c in &self.calib_distortion {
let kind = distortion_kind(&c.model);
if kind == 0 {
continue;
}
if model_kind == 0 {
model_kind = kind;
} else if model_kind != kind {
continue;
}
let df = focal - c.focal;
if df == 0.0 {
return Some(*c);
}
insert_spline(&mut slots, &mut dists, df, c);
}
if slots[1].is_none() || slots[2].is_none() {
return slots[1].or(slots[2]).copied();
}
let s0 = slots[0];
let s1 = slots[1].expect("checked above");
let s2 = slots[2].expect("checked above");
let s3 = slots[3];
let t = (focal - s1.focal) / (s2.focal - s1.focal);
let real_focal = match (s1.real_focal, s2.real_focal) {
(Some(rf1), Some(rf2)) => {
let rf0 = s0.and_then(|c| c.real_focal).unwrap_or(NO_NEIGHBOR);
let rf3 = s3.and_then(|c| c.real_focal).unwrap_or(NO_NEIGHBOR);
Some(catmull_rom_interpolate(rf0, rf1, rf2, rf3, t))
}
_ => None,
};
let t0 = distortion_terms(&s1.model);
let t1 = distortion_terms(&s2.model);
let t_left = s0.map(|c| distortion_terms(&c.model));
let t_right = s3.map(|c| distortion_terms(&c.model));
let mut out_terms = [0.0_f32; DISTORTION_TERM_COUNT];
for i in 0..DISTORTION_TERM_COUNT {
let f0 = s0.map(|c| c.focal).unwrap_or(f32::NAN);
let f3 = s3.map(|c| c.focal).unwrap_or(f32::NAN);
let mut values = [f0, s1.focal, s2.focal, f3, focal];
distortion_param_scales(&mut values, model_kind, i);
let y0 = t_left.map(|t| t[i] * values[0]).unwrap_or(NO_NEIGHBOR);
let y1 = t0[i] * values[1];
let y2 = t1[i] * values[2];
let y3 = t_right.map(|t| t[i] * values[3]).unwrap_or(NO_NEIGHBOR);
out_terms[i] = catmull_rom_interpolate(y0, y1, y2, y3, t) / values[4];
}
Some(CalibDistortion {
focal,
model: distortion_from_terms(model_kind, out_terms),
real_focal,
})
}
pub fn interpolate_tca(&self, focal: f32) -> Option<CalibTca> {
let mut slots: [Option<&CalibTca>; 4] = [None; 4];
let mut dists: [f32; 4] = [-f32::MAX, -f32::MAX, f32::MAX, f32::MAX];
let mut model_kind: u8 = 0;
for c in &self.calib_tca {
let kind = tca_kind(&c.model);
if kind == 0 {
continue;
}
if model_kind == 0 {
model_kind = kind;
} else if model_kind != kind {
continue;
}
let df = focal - c.focal;
if df == 0.0 {
return Some(*c);
}
insert_spline(&mut slots, &mut dists, df, c);
}
if slots[1].is_none() || slots[2].is_none() {
return slots[1].or(slots[2]).copied();
}
let s0 = slots[0];
let s1 = slots[1].expect("checked above");
let s2 = slots[2].expect("checked above");
let s3 = slots[3];
let t = (focal - s1.focal) / (s2.focal - s1.focal);
let t1_arr = tca_terms(&s1.model);
let t2_arr = tca_terms(&s2.model);
let t_left = s0.map(|c| tca_terms(&c.model));
let t_right = s3.map(|c| tca_terms(&c.model));
let mut out_terms = [0.0_f32; TCA_TERM_COUNT];
for i in 0..TCA_TERM_COUNT {
let f0 = s0.map(|c| c.focal).unwrap_or(f32::NAN);
let f3 = s3.map(|c| c.focal).unwrap_or(f32::NAN);
let mut values = [f0, s1.focal, s2.focal, f3, focal];
tca_param_scales(&mut values, model_kind, i);
let y0 = t_left.map(|t| t[i] * values[0]).unwrap_or(NO_NEIGHBOR);
let y1 = t1_arr[i] * values[1];
let y2 = t2_arr[i] * values[2];
let y3 = t_right.map(|t| t[i] * values[3]).unwrap_or(NO_NEIGHBOR);
out_terms[i] = catmull_rom_interpolate(y0, y1, y2, y3, t) / values[4];
}
Some(CalibTca {
focal,
model: tca_from_terms(model_kind, out_terms),
})
}
pub fn interpolate_vignetting(
&self,
focal: f32,
aperture: f32,
distance: f32,
) -> Option<CalibVignetting> {
let mut model_kind: u8 = 0;
let mut accum = [0.0_f32; VIGNETTING_TERM_COUNT];
let mut total_weight = 0.0_f32;
let mut smallest = f32::MAX;
let power = 3.5_f32;
for c in &self.calib_vignetting {
let kind = vignetting_kind(&c.model);
if kind == 0 {
continue;
}
if model_kind == 0 {
model_kind = kind;
} else if model_kind != kind {
continue;
}
let id = vignetting_dist(self.focal_min, self.focal_max, c, focal, aperture, distance);
if id < 0.0001 {
return Some(CalibVignetting {
focal,
aperture,
distance,
model: c.model,
});
}
if id < smallest {
smallest = id;
}
let weight = (1.0 / id.powf(power)).abs();
let terms = vignetting_terms(&c.model);
for i in 0..VIGNETTING_TERM_COUNT {
let mut values = [c.focal];
vignetting_param_scales(&mut values, model_kind, i);
accum[i] += weight * terms[i] * values[0];
}
total_weight += weight;
}
if smallest > 1.0 {
return None;
}
if total_weight <= 0.0 || smallest >= f32::MAX {
return None;
}
let mut out = [0.0_f32; VIGNETTING_TERM_COUNT];
for i in 0..VIGNETTING_TERM_COUNT {
let mut values = [focal];
vignetting_param_scales(&mut values, model_kind, i);
out[i] = accum[i] / (total_weight * values[0]);
}
Some(CalibVignetting {
focal,
aperture,
distance,
model: vignetting_from_terms(model_kind, out),
})
}
}
static LENS_NAME_RE_0: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^[^:]*?([0-9]+[0-9.]*)[-]?([0-9]+[0-9.]*)?(mm)[[:space:]]+(f/|f|1/|1:)?([0-9.]+)(-[0-9.]+)?.*$")
.expect("regex 0 compiles")
});
static LENS_NAME_RE_1: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^.*?1:([0-9.]+)[-]?([0-9.]+)?[[:space:]]+([0-9.]+)[-]?([0-9.]+)?(mm)?.*$")
.expect("regex 1 compiles")
});
static LENS_NAME_RE_2: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^.*?([0-9.]+)[-]?([0-9.]+)?\s*/\s*([0-9.]+)[-]?([0-9.]+)?.*$")
.expect("regex 2 compiles")
});
static EXTENDER_MAGNIFICATION_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^.*?[0-9](\.[0.9]+)?x.*$").expect("extender regex compiles"));
const LENS_NAME_MATCHES: [[usize; 3]; 3] = [[1, 2, 5], [3, 4, 1], [3, 4, 1]];
impl Lens {
pub fn guess_parameters(&mut self) {
let mut minf = f32::MAX;
let mut maxf = f32::MIN;
let mut mina = f32::MAX;
let maxa = f32::MIN;
let model = &self.model;
let model_lower = model.to_ascii_lowercase();
let skip_extender = model_lower.contains("adapter")
|| model_lower.contains("reducer")
|| model_lower.contains("booster")
|| model_lower.contains("extender")
|| model_lower.contains("converter")
|| model_lower.contains("magnifier")
|| EXTENDER_MAGNIFICATION_RE.is_match(model);
if !model.is_empty()
&& (self.aperture_min == 0.0 || self.focal_min == 0.0)
&& !skip_extender
{
let regexes: [&Regex; 3] = [&LENS_NAME_RE_0, &LENS_NAME_RE_1, &LENS_NAME_RE_2];
for (i, re) in regexes.iter().enumerate() {
if let Some(caps) = re.captures(model) {
let idx = LENS_NAME_MATCHES[i];
if let Some(m) = caps.get(idx[0]) {
if let Ok(v) = m.as_str().parse::<f32>() {
minf = v;
}
}
if let Some(m) = caps.get(idx[1]) {
if let Ok(v) = m.as_str().parse::<f32>() {
maxf = v;
}
}
if let Some(m) = caps.get(idx[2]) {
if let Ok(v) = m.as_str().parse::<f32>() {
mina = v;
}
}
break;
}
}
}
if self.aperture_min == 0.0 || self.focal_min == 0.0 {
for c in &self.calib_distortion {
if c.focal < minf {
minf = c.focal;
}
if c.focal > maxf {
maxf = c.focal;
}
}
for c in &self.calib_tca {
if c.focal < minf {
minf = c.focal;
}
if c.focal > maxf {
maxf = c.focal;
}
}
for c in &self.calib_vignetting {
if c.focal < minf {
minf = c.focal;
}
if c.focal > maxf {
maxf = c.focal;
}
if c.aperture < mina {
mina = c.aperture;
}
}
}
if minf != f32::MAX && self.focal_min == 0.0 {
self.focal_min = minf;
}
if maxf != f32::MIN && self.focal_max == 0.0 {
self.focal_max = maxf;
}
if mina != f32::MAX && self.aperture_min == 0.0 {
self.aperture_min = mina;
}
if maxa != f32::MIN && self.aperture_max == 0.0 {
self.aperture_max = maxa;
}
if self.focal_max == 0.0 {
self.focal_max = self.focal_min;
}
}
}