use crate::engine::diagnostic::{Diagnostic, Severity};
use crate::engine::layers::{ManualRegion, RegionLearner};
use crate::kernels::features::{extract_features, match_features, Feature, MAX_FEATURES_DEFAULT};
use crate::kernels::layers::{Field, FieldMotion};
use crate::kernels::motion::{self, Gray, MatchPoints, Motion, MotionField, MOTION_CELL};
use crate::kernels::raster::{downscale_gray, downscaled_size, mean_difference};
use crate::kernels::regions::{Kind, RegionOut};
use std::fmt;
use std::rc::Rc;
const LONG_BASELINE_MIN_GAP: u32 = 4;
const LONG_BASELINE_MAX_GAP: u32 = 24;
const PAUSED_DIFFERENCE: f64 = 0.12;
const UNDERSAMPLED_DURATION: f64 = 0.12;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FieldLayout {
Initial,
Estimated,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ScanRecord {
pub index: u32,
pub time: f64,
pub duration: f64,
pub field: MotionField,
pub layout: FieldLayout,
pub duplicate: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CompactFeatures {
pub xy: Vec<i16>,
pub score: Vec<f32>,
pub descriptors: Vec<u32>,
}
impl CompactFeatures {
pub fn encode(features: &[Feature]) -> CompactFeatures {
CompactFeatures {
xy: features
.iter()
.flat_map(|f| [f.x as i16, f.y as i16])
.collect(),
score: features.iter().map(|f| f.score as f32).collect(),
descriptors: features.iter().flat_map(|f| f.descriptor).collect(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ScanStep {
pub record: ScanRecord,
pub features: Option<CompactFeatures>,
pub first: bool,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FrameMeta {
pub index: u32,
pub time: f64,
pub duration: f64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ScanError {
NoFrame,
GrayGeometry { expected: usize, actual: usize },
FrameGeometryChanged,
}
impl fmt::Display for ScanError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScanError::NoFrame => write!(
f,
"CORE_BAD_ARGUMENT: no frame was written before this scan step."
),
ScanError::GrayGeometry { expected, actual } => write!(
f,
"CORE_BAD_ARGUMENT: the analysis gray holds {actual} pixels, not {expected}."
),
ScanError::FrameGeometryChanged => write!(f, "FRAME_GEOMETRY_CHANGED"),
}
}
}
impl std::error::Error for ScanError {}
pub struct ScanRegions {
pub regions: Vec<RegionOut>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone)]
struct Observed {
slot: usize,
gray: Rc<Vec<u8>>,
features: Rc<Vec<Feature>>,
index: u32,
}
pub struct ScanSession {
width: usize,
height: usize,
factor: usize,
analysis_width: usize,
analysis_height: usize,
slots: Vec<Vec<u8>>,
staged: Option<usize>,
staged_duplicate: Option<bool>,
previous: Option<Observed>,
baseline: Option<Observed>,
last_field: Option<(MotionField, FieldLayout)>,
learner: RegionLearner,
analysed: u32,
}
impl ScanSession {
pub fn new(width: usize, height: usize, factor: usize) -> ScanSession {
let factor = factor.max(1);
let (analysis_width, analysis_height) = downscaled_size(width, height, factor);
ScanSession {
width,
height,
factor,
analysis_width,
analysis_height,
slots: Vec::new(),
staged: None,
staged_duplicate: None,
previous: None,
baseline: None,
last_field: None,
learner: RegionLearner::new(analysis_width, analysis_height),
analysed: 0,
}
}
pub fn native_size(&self) -> (usize, usize) {
(self.width, self.height)
}
pub fn analysis_size(&self) -> (usize, usize) {
(self.analysis_width, self.analysis_height)
}
pub fn learner(&self) -> &RegionLearner {
&self.learner
}
pub fn frame_buffer(&mut self) -> &mut [u8] {
let slot = match self.staged {
Some(slot) => slot,
None => {
let held = |s: usize| {
self.previous.as_ref().is_some_and(|p| p.slot == s)
|| self.baseline.as_ref().is_some_and(|b| b.slot == s)
};
let slot = match (0..self.slots.len()).find(|&s| !held(s)) {
Some(slot) => slot,
None => {
self.slots.push(vec![0; self.width * self.height * 4]);
self.slots.len() - 1
}
};
self.staged = Some(slot);
self.staged_duplicate = None;
slot
}
};
&mut self.slots[slot]
}
pub fn is_duplicate(&mut self) -> bool {
let Some(slot) = self.staged else {
return false;
};
if let Some(known) = self.staged_duplicate {
return known;
}
let duplicate = self
.previous
.as_ref()
.is_some_and(|p| self.slots[p.slot] == self.slots[slot]);
self.staged_duplicate = Some(duplicate);
duplicate
}
pub fn push(&mut self, meta: FrameMeta, gray: Option<&[u8]>) -> Result<ScanStep, ScanError> {
let Some(slot) = self.staged else {
return Err(ScanError::NoFrame);
};
let duplicate = self.is_duplicate();
let (aw, ah) = (self.analysis_width, self.analysis_height);
let (g, features) = match (&self.previous, duplicate) {
(Some(previous), true) => (previous.gray.clone(), previous.features.clone()),
_ => {
let g = match gray {
Some(g) if g.len() != aw * ah => {
return Err(ScanError::GrayGeometry {
expected: aw * ah,
actual: g.len(),
})
}
Some(g) => g.to_vec(),
None => {
let mut out = vec![0; aw * ah];
downscale_gray(
&self.slots[slot],
self.width,
self.height,
self.factor,
&mut out,
);
out
}
};
let features = features_of(&g, aw, ah);
(Rc::new(g), Rc::new(features))
}
};
self.staged = None;
self.staged_duplicate = None;
self.analysed += 1;
let current = Observed {
slot,
gray: g,
features,
index: meta.index,
};
let (field, layout, first) = match &self.last_field {
Some((last, layout)) if duplicate => (
duplicate_field(last, current.features.len()),
*layout,
false,
),
_ if self.previous.is_some() => {
(self.step_motion(¤t)?, FieldLayout::Estimated, false)
}
_ => {
self.baseline = Some(current.clone());
(
initial_field(aw, ah, current.features.len()),
FieldLayout::Initial,
true,
)
}
};
let diagnostics = frame_diagnostics(&meta, current.features.len(), &field);
let features = (!duplicate).then(|| CompactFeatures::encode(¤t.features));
self.last_field = Some((field.clone(), layout));
self.previous = Some(current);
Ok(ScanStep {
record: ScanRecord {
index: meta.index,
time: meta.time,
duration: meta.duration,
field,
layout,
duplicate,
},
features,
first,
diagnostics,
})
}
fn step_motion(&mut self, current: &Observed) -> Result<MotionField, ScanError> {
let (aw, ah) = (self.analysis_width, self.analysis_height);
let previous = self
.previous
.clone()
.expect("step_motion runs with a previous frame");
let field = estimate(&previous, current, aw, ah)?;
if informative_field(&field) {
self.learn(&field, &previous, current);
self.baseline = Some(current.clone());
} else if let Some(baseline) = self.baseline.clone() {
let gap = current.index - baseline.index;
if gap >= LONG_BASELINE_MIN_GAP {
let long = estimate(&baseline, current, aw, ah)?;
if informative_field(&long) {
self.learn(&long, &baseline, current);
self.baseline = Some(current.clone());
} else if gap > LONG_BASELINE_MAX_GAP {
self.baseline = Some(current.clone());
}
}
}
Ok(field)
}
fn learn(&mut self, field: &MotionField, from: &Observed, to: &Observed) {
let motions: Vec<FieldMotion> = field
.motions
.iter()
.map(|m| FieldMotion {
x: m.x,
y: m.y,
support: m.support,
confidence: m.confidence,
})
.collect();
let learner_field = Field {
motions: &motions,
labels: &field.labels,
confidence: &field.confidence,
dynamic: &field.dynamic,
cols: field.cols,
rows: field.rows,
difference: field.difference,
unknown: field.unknown,
};
let native = Some((
self.slots[from.slot].as_slice(),
self.slots[to.slot].as_slice(),
self.width,
self.height,
));
self.learner
.add(&learner_field, &from.gray, &to.gray, native);
}
pub fn finish(&self, manual: &[ManualRegion]) -> ScanRegions {
let regions = if self.analysed == 0 {
Vec::new()
} else {
self.learner.finish(
self.width as f64,
self.height as f64,
manual,
self.factor as u32,
)
};
let diagnostics = region_diagnostics(®ions, manual.len());
ScanRegions {
regions,
diagnostics,
}
}
}
fn features_of(gray: &[u8], width: usize, height: usize) -> Vec<Feature> {
let mut features = extract_features(gray, width, height, MAX_FEATURES_DEFAULT, None);
for f in &mut features {
f.score = f.score as f32 as f64;
}
features
}
fn estimate(
a: &Observed,
b: &Observed,
width: usize,
height: usize,
) -> Result<MotionField, ScanError> {
fn gray(data: &[u8], width: usize, height: usize) -> Gray<'_> {
Gray {
width,
height,
data,
}
}
estimate_motion(
gray(&a.gray, width, height),
gray(&b.gray, width, height),
Some(&a.features),
Some(&b.features),
)
}
pub fn estimate_motion(
a: Gray<'_>,
b: Gray<'_>,
a_features: Option<&[Feature]>,
b_features: Option<&[Feature]>,
) -> Result<MotionField, ScanError> {
if a.width != b.width || a.height != b.height {
return Err(ScanError::FrameGeometryChanged);
}
if mean_difference(a.data, b.data) < PAUSED_DIFFERENCE {
return Ok(motion::estimate_motion(a, b, &[], 0));
}
let extracted_a;
let fa = match a_features {
Some(f) => f,
None => {
extracted_a = features_of(a.data, a.width, a.height);
&extracted_a
}
};
let extracted_b;
let fb = match b_features {
Some(f) => f,
None => {
extracted_b = features_of(b.data, b.width, b.height);
&extracted_b
}
};
let points: Vec<MatchPoints> = if fa.is_empty() || fb.is_empty() {
Vec::new()
} else {
match_features(fa, fb, true)
.iter()
.map(|m| {
let (p, q) = (&fa[m.a as usize], &fb[m.b as usize]);
MatchPoints {
ax: p.x as f64,
ay: p.y as f64,
bx: q.x as f64,
by: q.y as f64,
unique: m.unique,
}
})
.collect()
};
Ok(motion::estimate_motion(a, b, &points, fb.len() as u32))
}
pub fn informative_field(field: &MotionField) -> bool {
!field.unknown
&& field.difference >= 0.2
&& field
.motions
.iter()
.any(|m| m.x * m.x + m.y * m.y > 1.0 && m.confidence > 0.3)
}
fn still(support: usize) -> Motion {
Motion {
x: 0.0,
y: 0.0,
support: support as u32,
unique: support as u32,
confidence: 1.0,
error: 0.0,
ambiguous: false,
}
}
fn initial_field(width: usize, height: usize, features: usize) -> MotionField {
let (cols, rows) = (width.div_ceil(MOTION_CELL), height.div_ceil(MOTION_CELL));
MotionField {
motions: vec![still(features)],
labels: vec![0; cols * rows],
confidence: vec![255; cols * rows],
dynamic: vec![0; cols * rows],
cols,
rows,
cell: MOTION_CELL,
difference: 0.0,
feature_count: features as u32,
unknown: false,
zoom: 1.0,
}
}
fn duplicate_field(last: &MotionField, features: usize) -> MotionField {
MotionField {
motions: vec![still(features)],
labels: vec![0; last.labels.len()],
dynamic: vec![0; last.labels.len()],
difference: 0.0,
unknown: false,
zoom: 1.0,
..(*last).clone()
}
}
fn frame_diagnostics(meta: &FrameMeta, features: usize, field: &MotionField) -> Vec<Diagnostic> {
let mut out = Vec::new();
let at = |code, severity| Diagnostic::new(code, severity).at(meta.index, meta.time);
if features < 8 && meta.index == 0 {
out.push(at("LOW_TEXTURE_UNOBSERVABLE", Severity::Warning));
}
if field.unknown {
out.push(at("UNRESOLVED_MOTION", Severity::Warning));
}
if field
.motions
.first()
.is_some_and(|m| m.ambiguous && m.support >= 6)
{
out.push(at("AMBIGUOUS_PATTERN", Severity::Warning));
}
if meta.duration > UNDERSAMPLED_DURATION {
out.push(at("TEMPORAL_UNDERSAMPLING", Severity::Info));
}
out
}
fn region_diagnostics(regions: &[RegionOut], manual: usize) -> Vec<Diagnostic> {
let mut out = Vec::new();
if regions.iter().any(|r| r.unassigned) {
out.push(Diagnostic::new("MANUAL_UNASSIGNED", Severity::Warning));
}
if regions.iter().any(|r| r.kind == Kind::Ignore) {
out.push(Diagnostic::new(
"EXPLICITLY_EXCLUDED_REGION",
Severity::Warning,
));
}
if manual > 0 {
out.push(Diagnostic::new("MANUAL_REGION_PRIORITY", Severity::Info));
}
let count = |kind| regions.iter().filter(|r| r.kind == kind).count() as f64;
let (moving, fixed) = (count(Kind::Moving), count(Kind::Fixed));
if manual == 0 {
out.push(
Diagnostic::new("AUTOMATIC_LAYER_MASK", Severity::Info)
.param("moving", moving)
.param("fixed", fixed),
);
}
if moving > 1.0 {
out.push(Diagnostic::new("MULTIPLE_SCROLL_LAYERS", Severity::Info));
}
out
}
#[cfg(test)]
mod tests;