pub mod motion;
pub mod rolling_shutter;
pub mod smooth;
pub mod transform;
use crate::error::{CvError, CvResult};
use oximedia_codec::VideoFrame;
pub use motion::{
GyroscopeData, GyroscopeFusionConfig, HomographyEstimator, HybridMotionEstimator,
MotionEstimator, TransformMatrix,
};
pub use rolling_shutter::RollingShutterCorrector;
pub use smooth::{AdaptiveSmoother, GaussianSmoother, LowPassFilter, MotionSmoother};
pub use transform::{BorderMode, FrameWarper};
#[derive(Debug, Clone, Copy)]
pub struct StabilizationConfig {
pub smoothing_strength: f64,
pub crop_ratio: f64,
pub enable_rolling_shutter: bool,
pub enable_3d_stabilization: bool,
pub border_mode: BorderMode,
pub max_motion_magnitude: f64,
pub smoothing_window: usize,
pub enable_zoom_crop: bool,
}
impl Default for StabilizationConfig {
fn default() -> Self {
Self {
smoothing_strength: 0.7,
crop_ratio: 0.95,
enable_rolling_shutter: false,
enable_3d_stabilization: true,
border_mode: BorderMode::Replicate,
max_motion_magnitude: 50.0,
smoothing_window: 15,
enable_zoom_crop: false,
}
}
}
impl StabilizationConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_smoothing_strength(mut self, strength: f64) -> Self {
self.smoothing_strength = strength.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn with_crop_ratio(mut self, ratio: f64) -> Self {
self.crop_ratio = ratio.clamp(0.0, 1.0);
self
}
#[must_use]
pub const fn with_rolling_shutter(mut self, enable: bool) -> Self {
self.enable_rolling_shutter = enable;
self
}
#[must_use]
pub const fn with_3d_stabilization(mut self, enable: bool) -> Self {
self.enable_3d_stabilization = enable;
self
}
#[must_use]
pub const fn with_border_mode(mut self, mode: BorderMode) -> Self {
self.border_mode = mode;
self
}
#[must_use]
pub fn with_max_motion_magnitude(mut self, magnitude: f64) -> Self {
self.max_motion_magnitude = magnitude.max(0.0);
self
}
#[must_use]
pub const fn with_smoothing_window(mut self, window: usize) -> Self {
self.smoothing_window = window;
self
}
#[must_use]
pub const fn with_zoom_crop(mut self, enable: bool) -> Self {
self.enable_zoom_crop = enable;
self
}
pub fn validate(&self) -> CvResult<()> {
if !(0.0..=1.0).contains(&self.smoothing_strength) {
return Err(CvError::invalid_parameter(
"smoothing_strength",
format!("{}", self.smoothing_strength),
));
}
if !(0.0..=1.0).contains(&self.crop_ratio) {
return Err(CvError::invalid_parameter(
"crop_ratio",
format!("{}", self.crop_ratio),
));
}
if self.max_motion_magnitude <= 0.0 {
return Err(CvError::invalid_parameter(
"max_motion_magnitude",
format!("{}", self.max_motion_magnitude),
));
}
if self.smoothing_window < 3 {
return Err(CvError::invalid_parameter(
"smoothing_window",
format!("{}", self.smoothing_window),
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct VideoStabilizer {
motion_estimator: MotionEstimator,
gaussian_smoother: GaussianSmoother,
low_pass_filter: LowPassFilter,
adaptive_smoother: AdaptiveSmoother,
frame_warper: FrameWarper,
rolling_shutter_corrector: RollingShutterCorrector,
}
impl VideoStabilizer {
#[must_use]
pub fn new() -> Self {
Self {
motion_estimator: MotionEstimator::new(),
gaussian_smoother: GaussianSmoother::new(15, 3.0),
low_pass_filter: LowPassFilter::new(0.3),
adaptive_smoother: AdaptiveSmoother::new(15, 50.0),
frame_warper: FrameWarper::new(),
rolling_shutter_corrector: RollingShutterCorrector::new(),
}
}
pub fn stabilize(
&mut self,
frames: &[VideoFrame],
config: StabilizationConfig,
) -> CvResult<Vec<VideoFrame>> {
config.validate()?;
if frames.is_empty() {
return Err(CvError::invalid_parameter(
"frames",
"empty frame sequence".to_string(),
));
}
let transformations = self.estimate_motion(frames)?;
let transformations = if config.enable_rolling_shutter {
self.rolling_shutter_corrector
.correct_transformations(&transformations)?
} else {
transformations
};
let smoothed_transformations = self.smooth_motion(&transformations, &config)?;
let stabilizing_transforms =
self.compute_stabilizing_transforms(&transformations, &smoothed_transformations)?;
let stabilized_frames = self.warp_frames(frames, &stabilizing_transforms, &config)?;
Ok(stabilized_frames)
}
fn estimate_motion(&mut self, frames: &[VideoFrame]) -> CvResult<Vec<TransformMatrix>> {
let mut transformations = Vec::with_capacity(frames.len());
transformations.push(TransformMatrix::identity());
for i in 1..frames.len() {
let prev_frame = &frames[i - 1];
let curr_frame = &frames[i];
let transform = self
.motion_estimator
.estimate_transform(prev_frame, curr_frame)?;
transformations.push(transform);
}
Ok(transformations)
}
fn smooth_motion(
&mut self,
transformations: &[TransformMatrix],
config: &StabilizationConfig,
) -> CvResult<Vec<TransformMatrix>> {
let motion_params = self.extract_motion_parameters(transformations);
let smoothed_params = if config.enable_3d_stabilization {
self.adaptive_smoother
.smooth(&motion_params, config.max_motion_magnitude)?
} else {
let gaussian_smoothed = self.gaussian_smoother.smooth(&motion_params)?;
self.low_pass_filter.smooth(&gaussian_smoothed)?
};
self.reconstruct_transformations(&smoothed_params)
}
fn extract_motion_parameters(&self, transformations: &[TransformMatrix]) -> MotionParameters {
let mut dx = Vec::with_capacity(transformations.len());
let mut dy = Vec::with_capacity(transformations.len());
let mut da = Vec::with_capacity(transformations.len());
let mut ds = Vec::with_capacity(transformations.len());
for transform in transformations {
dx.push(transform.tx);
dy.push(transform.ty);
da.push(transform.angle);
ds.push(transform.scale);
}
MotionParameters { dx, dy, da, ds }
}
fn reconstruct_transformations(
&self,
params: &MotionParameters,
) -> CvResult<Vec<TransformMatrix>> {
let len = params.dx.len();
let mut transformations = Vec::with_capacity(len);
for i in 0..len {
let transform = TransformMatrix {
tx: params.dx[i],
ty: params.dy[i],
angle: params.da[i],
scale: params.ds[i],
};
transformations.push(transform);
}
Ok(transformations)
}
fn compute_stabilizing_transforms(
&self,
original: &[TransformMatrix],
smoothed: &[TransformMatrix],
) -> CvResult<Vec<TransformMatrix>> {
if original.len() != smoothed.len() {
return Err(CvError::matrix_error(
"Original and smoothed transformation counts do not match",
));
}
let mut stabilizing = Vec::with_capacity(original.len());
for (orig, smooth) in original.iter().zip(smoothed.iter()) {
let stabilize = TransformMatrix {
tx: smooth.tx - orig.tx,
ty: smooth.ty - orig.ty,
angle: smooth.angle - orig.angle,
scale: smooth.scale / orig.scale,
};
stabilizing.push(stabilize);
}
Ok(stabilizing)
}
fn warp_frames(
&mut self,
frames: &[VideoFrame],
transforms: &[TransformMatrix],
config: &StabilizationConfig,
) -> CvResult<Vec<VideoFrame>> {
if frames.len() != transforms.len() {
return Err(CvError::matrix_error(
"Frame and transformation counts do not match",
));
}
let mut stabilized = Vec::with_capacity(frames.len());
for (frame, transform) in frames.iter().zip(transforms.iter()) {
let warped =
self.frame_warper
.warp(frame, transform, config.border_mode, config.crop_ratio)?;
stabilized.push(warped);
}
Ok(stabilized)
}
}
impl Default for VideoStabilizer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct MotionParameters {
pub(crate) dx: Vec<f64>,
pub(crate) dy: Vec<f64>,
pub(crate) da: Vec<f64>,
pub(crate) ds: Vec<f64>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct StabilizationStats {
pub avg_motion_before: f64,
pub avg_motion_after: f64,
pub max_motion_before: f64,
pub max_motion_after: f64,
pub improvement_ratio: f64,
pub frame_count: usize,
}
impl StabilizationStats {
#[must_use]
pub const fn new() -> Self {
Self {
avg_motion_before: 0.0,
avg_motion_after: 0.0,
max_motion_before: 0.0,
max_motion_after: 0.0,
improvement_ratio: 0.0,
frame_count: 0,
}
}
#[must_use]
pub fn compute(before: &[TransformMatrix], after: &[TransformMatrix]) -> Self {
if before.is_empty() || after.is_empty() {
return Self::new();
}
let avg_before = Self::compute_average_magnitude(before);
let avg_after = Self::compute_average_magnitude(after);
let max_before = Self::compute_max_magnitude(before);
let max_after = Self::compute_max_magnitude(after);
let improvement = if avg_before > 0.0 {
(avg_before - avg_after) / avg_before
} else {
0.0
};
Self {
avg_motion_before: avg_before,
avg_motion_after: avg_after,
max_motion_before: max_before,
max_motion_after: max_after,
improvement_ratio: improvement,
frame_count: before.len(),
}
}
fn compute_average_magnitude(transforms: &[TransformMatrix]) -> f64 {
if transforms.is_empty() {
return 0.0;
}
let sum: f64 = transforms
.iter()
.map(motion::TransformMatrix::magnitude)
.sum();
sum / transforms.len() as f64
}
fn compute_max_magnitude(transforms: &[TransformMatrix]) -> f64 {
transforms
.iter()
.map(motion::TransformMatrix::magnitude)
.fold(0.0, f64::max)
}
}