use crate::error::{CvError, CvResult};
use crate::stabilize::motion::TransformMatrix;
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy)]
pub struct RollingShutterParams {
pub readout_time: f64,
pub top_to_bottom: bool,
pub num_segments: usize,
}
impl Default for RollingShutterParams {
fn default() -> Self {
Self {
readout_time: 0.033, top_to_bottom: true,
num_segments: 10,
}
}
}
impl RollingShutterParams {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_readout_time(mut self, readout_time: f64) -> Self {
self.readout_time = readout_time;
self
}
#[must_use]
pub const fn with_scan_direction(mut self, top_to_bottom: bool) -> Self {
self.top_to_bottom = top_to_bottom;
self
}
#[must_use]
pub const fn with_num_segments(mut self, num_segments: usize) -> Self {
self.num_segments = num_segments;
self
}
}
#[derive(Debug, Clone)]
pub struct RollingShutterCorrector {
params: RollingShutterParams,
motion_model: MotionModel,
}
impl RollingShutterCorrector {
#[must_use]
pub fn new() -> Self {
Self {
params: RollingShutterParams::default(),
motion_model: MotionModel::Linear,
}
}
#[must_use]
pub const fn with_params(mut self, params: RollingShutterParams) -> Self {
self.params = params;
self
}
#[must_use]
pub const fn with_motion_model(mut self, model: MotionModel) -> Self {
self.motion_model = model;
self
}
pub fn correct_transformations(
&self,
transformations: &[TransformMatrix],
) -> CvResult<Vec<TransformMatrix>> {
if transformations.is_empty() {
return Ok(Vec::new());
}
let mut corrected = Vec::with_capacity(transformations.len());
for i in 0..transformations.len() {
let transform = &transformations[i];
let scanline_transforms =
self.compute_scanline_transforms(transform, i, transformations)?;
let averaged = self.average_scanline_transforms(&scanline_transforms);
corrected.push(averaged);
}
Ok(corrected)
}
fn compute_scanline_transforms(
&self,
base_transform: &TransformMatrix,
frame_idx: usize,
all_transforms: &[TransformMatrix],
) -> CvResult<Vec<TransformMatrix>> {
let num_segments = self.params.num_segments;
let mut scanline_transforms = Vec::with_capacity(num_segments);
for segment_idx in 0..num_segments {
let time_offset = if self.params.top_to_bottom {
segment_idx as f64 / num_segments as f64
} else {
1.0 - segment_idx as f64 / num_segments as f64
};
let transform = match self.motion_model {
MotionModel::Linear => self.linear_interpolation(
base_transform,
frame_idx,
all_transforms,
time_offset,
),
MotionModel::Quadratic => self.quadratic_interpolation(
base_transform,
frame_idx,
all_transforms,
time_offset,
),
MotionModel::Cubic => {
self.cubic_interpolation(base_transform, frame_idx, all_transforms, time_offset)
}
};
scanline_transforms.push(transform);
}
Ok(scanline_transforms)
}
fn linear_interpolation(
&self,
base_transform: &TransformMatrix,
frame_idx: usize,
all_transforms: &[TransformMatrix],
t: f64,
) -> TransformMatrix {
if frame_idx + 1 < all_transforms.len() {
let next_transform = &all_transforms[frame_idx + 1];
base_transform.interpolate(next_transform, t)
} else {
*base_transform
}
}
fn quadratic_interpolation(
&self,
base_transform: &TransformMatrix,
frame_idx: usize,
all_transforms: &[TransformMatrix],
t: f64,
) -> TransformMatrix {
if frame_idx + 2 < all_transforms.len() {
let t0 = base_transform;
let t1 = &all_transforms[frame_idx + 1];
let t2 = &all_transforms[frame_idx + 2];
let one_minus_t = 1.0 - t;
let a = one_minus_t * one_minus_t;
let b = 2.0 * one_minus_t * t;
let c = t * t;
TransformMatrix {
tx: a * t0.tx + b * t1.tx + c * t2.tx,
ty: a * t0.ty + b * t1.ty + c * t2.ty,
angle: a * t0.angle + b * t1.angle + c * t2.angle,
scale: a * t0.scale + b * t1.scale + c * t2.scale,
}
} else {
self.linear_interpolation(base_transform, frame_idx, all_transforms, t)
}
}
fn cubic_interpolation(
&self,
base_transform: &TransformMatrix,
frame_idx: usize,
all_transforms: &[TransformMatrix],
t: f64,
) -> TransformMatrix {
if frame_idx + 3 < all_transforms.len() {
let t0 = base_transform;
let t1 = &all_transforms[frame_idx + 1];
let t2 = &all_transforms[frame_idx + 2];
let t3 = &all_transforms[frame_idx + 3];
let one_minus_t = 1.0 - t;
let a = one_minus_t * one_minus_t * one_minus_t;
let b = 3.0 * one_minus_t * one_minus_t * t;
let c = 3.0 * one_minus_t * t * t;
let d = t * t * t;
TransformMatrix {
tx: a * t0.tx + b * t1.tx + c * t2.tx + d * t3.tx,
ty: a * t0.ty + b * t1.ty + c * t2.ty + d * t3.ty,
angle: a * t0.angle + b * t1.angle + c * t2.angle + d * t3.angle,
scale: a * t0.scale + b * t1.scale + c * t2.scale + d * t3.scale,
}
} else {
self.quadratic_interpolation(base_transform, frame_idx, all_transforms, t)
}
}
fn average_scanline_transforms(&self, transforms: &[TransformMatrix]) -> TransformMatrix {
if transforms.is_empty() {
return TransformMatrix::identity();
}
let mut sum_tx = 0.0;
let mut sum_ty = 0.0;
let mut sum_angle = 0.0;
let mut sum_scale = 0.0;
for transform in transforms {
sum_tx += transform.tx;
sum_ty += transform.ty;
sum_angle += transform.angle;
sum_scale += transform.scale;
}
let n = transforms.len() as f64;
TransformMatrix {
tx: sum_tx / n,
ty: sum_ty / n,
angle: sum_angle / n,
scale: sum_scale / n,
}
}
pub fn estimate_params(&mut self, transformations: &[TransformMatrix]) -> CvResult<()> {
if transformations.is_empty() {
return Err(CvError::invalid_parameter(
"transformations",
"empty sequence".to_string(),
));
}
let motion_variance = self.compute_motion_variance(transformations);
let dominant_direction = self.compute_dominant_direction(transformations);
if motion_variance > 0.5 {
self.params.num_segments = 15; } else {
self.params.num_segments = 10; }
self.params.top_to_bottom = dominant_direction >= 0.0;
Ok(())
}
fn compute_motion_variance(&self, transformations: &[TransformMatrix]) -> f64 {
if transformations.is_empty() {
return 0.0;
}
let mean_tx: f64 =
transformations.iter().map(|t| t.tx).sum::<f64>() / transformations.len() as f64;
let mean_ty: f64 =
transformations.iter().map(|t| t.ty).sum::<f64>() / transformations.len() as f64;
let variance: f64 = transformations
.iter()
.map(|t| {
let dx = t.tx - mean_tx;
let dy = t.ty - mean_ty;
dx * dx + dy * dy
})
.sum::<f64>()
/ transformations.len() as f64;
variance.sqrt()
}
fn compute_dominant_direction(&self, transformations: &[TransformMatrix]) -> f64 {
if transformations.is_empty() {
return 0.0;
}
let sum_ty: f64 = transformations.iter().map(|t| t.ty).sum();
sum_ty / transformations.len() as f64
}
}
impl Default for RollingShutterCorrector {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MotionModel {
Linear,
Quadratic,
Cubic,
}
#[derive(Debug, Clone)]
pub struct RollingShutterEstimator {
analysis_window: usize,
motion_threshold: f64,
}
impl RollingShutterEstimator {
#[must_use]
pub const fn new() -> Self {
Self {
analysis_window: 30,
motion_threshold: 5.0,
}
}
#[must_use]
pub const fn with_analysis_window(mut self, window: usize) -> Self {
self.analysis_window = window;
self
}
#[must_use]
pub const fn with_motion_threshold(mut self, threshold: f64) -> Self {
self.motion_threshold = threshold;
self
}
pub fn estimate(&self, transformations: &[TransformMatrix]) -> CvResult<RollingShutterParams> {
if transformations.len() < self.analysis_window {
return Err(CvError::invalid_parameter(
"transformations",
format!("need at least {} frames", self.analysis_window),
));
}
let motion_stats = self.analyze_motion_patterns(transformations);
let has_rolling_shutter = self.detect_rolling_shutter(&motion_stats);
let readout_time = if has_rolling_shutter {
self.estimate_readout_time(&motion_stats)
} else {
0.033 };
let top_to_bottom = motion_stats.vertical_bias >= 0.0;
let num_segments = if has_rolling_shutter { 15 } else { 10 };
Ok(RollingShutterParams {
readout_time,
top_to_bottom,
num_segments,
})
}
fn analyze_motion_patterns(&self, transformations: &[TransformMatrix]) -> MotionStatistics {
let mut vertical_motion = Vec::new();
let mut horizontal_motion = Vec::new();
let mut rotation = Vec::new();
for transform in transformations.iter().take(self.analysis_window) {
vertical_motion.push(transform.ty);
horizontal_motion.push(transform.tx);
rotation.push(transform.angle);
}
let vertical_variance = compute_variance(&vertical_motion);
let horizontal_variance = compute_variance(&horizontal_motion);
let rotation_variance = compute_variance(&rotation);
let vertical_bias = vertical_motion.iter().sum::<f64>() / vertical_motion.len() as f64;
MotionStatistics {
vertical_variance,
horizontal_variance,
rotation_variance,
vertical_bias,
}
}
fn detect_rolling_shutter(&self, stats: &MotionStatistics) -> bool {
stats.vertical_variance > self.motion_threshold
&& stats.vertical_variance > stats.horizontal_variance * 1.5
}
fn estimate_readout_time(&self, stats: &MotionStatistics) -> f64 {
let base_time = 0.033; let variance_factor = (stats.vertical_variance / 100.0).clamp(0.5, 2.0);
base_time * variance_factor
}
}
impl Default for RollingShutterEstimator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
struct MotionStatistics {
vertical_variance: f64,
horizontal_variance: f64,
rotation_variance: f64,
vertical_bias: f64,
}
fn compute_variance(signal: &[f64]) -> f64 {
if signal.is_empty() {
return 0.0;
}
let mean = signal.iter().sum::<f64>() / signal.len() as f64;
let variance = signal
.iter()
.map(|&x| {
let diff = x - mean;
diff * diff
})
.sum::<f64>()
/ signal.len() as f64;
variance
}
#[derive(Debug, Clone, Copy)]
pub struct ScanlineTransform {
pub scanline_idx: u32,
pub transform: TransformMatrix,
pub confidence: f64,
}
impl ScanlineTransform {
#[must_use]
pub const fn new(scanline_idx: u32, transform: TransformMatrix, confidence: f64) -> Self {
Self {
scanline_idx,
transform,
confidence,
}
}
}
#[derive(Debug, Clone)]
pub struct RollingShutterModel {
scanline_transforms: Vec<ScanlineTransform>,
frame_height: u32,
}
impl RollingShutterModel {
#[must_use]
pub fn new(frame_height: u32) -> Self {
Self {
scanline_transforms: Vec::new(),
frame_height,
}
}
pub fn add_scanline(&mut self, transform: ScanlineTransform) {
self.scanline_transforms.push(transform);
}
#[must_use]
pub fn get_scanline_transform(&self, scanline_idx: u32) -> Option<TransformMatrix> {
self.scanline_transforms
.iter()
.min_by_key(|st| (st.scanline_idx as i32 - scanline_idx as i32).abs())
.map(|st| st.transform)
}
#[must_use]
pub fn interpolate_scanline_transform(&self, scanline_idx: u32) -> TransformMatrix {
if self.scanline_transforms.is_empty() {
return TransformMatrix::identity();
}
let mut prev: Option<&ScanlineTransform> = None;
let mut next: Option<&ScanlineTransform> = None;
for st in &self.scanline_transforms {
if st.scanline_idx <= scanline_idx {
prev = Some(st);
}
if st.scanline_idx >= scanline_idx && next.is_none() {
next = Some(st);
}
}
match (prev, next) {
(Some(p), Some(n)) if p.scanline_idx != n.scanline_idx => {
let t = (scanline_idx - p.scanline_idx) as f64
/ (n.scanline_idx - p.scanline_idx) as f64;
p.transform.interpolate(&n.transform, t)
}
(Some(p), _) => p.transform,
(_, Some(n)) => n.transform,
_ => TransformMatrix::identity(),
}
}
#[must_use]
pub fn scanline_transforms(&self) -> &[ScanlineTransform] {
&self.scanline_transforms
}
}
#[derive(Debug, Clone)]
pub struct JelloCorrector {
wobble_frequency: f64,
amplitude_threshold: f64,
}
impl JelloCorrector {
#[must_use]
pub fn new() -> Self {
Self {
wobble_frequency: 0.0,
amplitude_threshold: 2.0,
}
}
pub fn detect_wobble(&mut self, transformations: &[TransformMatrix]) -> f64 {
if transformations.len() < 10 {
return 0.0;
}
let vertical_motion: Vec<f64> = transformations.iter().map(|t| t.ty).collect();
let frequency = self.detect_frequency_zero_crossing(&vertical_motion);
self.wobble_frequency = frequency;
frequency
}
fn detect_frequency_zero_crossing(&self, signal: &[f64]) -> f64 {
if signal.len() < 3 {
return 0.0;
}
let mean = signal.iter().sum::<f64>() / signal.len() as f64;
let centered: Vec<f64> = signal.iter().map(|&x| x - mean).collect();
let mut crossings = 0;
for i in 1..centered.len() {
if (centered[i - 1] >= 0.0 && centered[i] < 0.0)
|| (centered[i - 1] < 0.0 && centered[i] >= 0.0)
{
crossings += 1;
}
}
crossings as f64 / (2.0 * signal.len() as f64)
}
pub fn correct(&self, transformations: &[TransformMatrix]) -> CvResult<Vec<TransformMatrix>> {
if self.wobble_frequency < 0.01 {
return Ok(transformations.to_vec());
}
let mut corrected = Vec::with_capacity(transformations.len());
for (i, transform) in transformations.iter().enumerate() {
let wobble_amplitude = self.estimate_wobble_amplitude(transformations, i);
let phase = 2.0 * PI * self.wobble_frequency * i as f64;
let wobble_offset = wobble_amplitude * phase.sin();
corrected.push(TransformMatrix {
tx: transform.tx,
ty: transform.ty - wobble_offset,
angle: transform.angle,
scale: transform.scale,
});
}
Ok(corrected)
}
fn estimate_wobble_amplitude(
&self,
transformations: &[TransformMatrix],
frame_idx: usize,
) -> f64 {
let window_size = 10;
let start = frame_idx.saturating_sub(window_size / 2);
let end = (frame_idx + window_size / 2).min(transformations.len());
let mut max_ty = f64::MIN;
let mut min_ty = f64::MAX;
for transform in &transformations[start..end] {
max_ty = max_ty.max(transform.ty);
min_ty = min_ty.min(transform.ty);
}
(max_ty - min_ty) / 2.0
}
}
impl Default for JelloCorrector {
fn default() -> Self {
Self::new()
}
}