#[cfg_attr(not(feature = "segmentation"), allow(dead_code))]
#[derive(Debug, Clone)]
pub(crate) struct HysteresisGate {
onset: f32,
offset: f32,
on: bool,
}
#[cfg_attr(not(feature = "segmentation"), allow(dead_code))]
impl HysteresisGate {
pub(crate) fn new(onset: f32, offset: f32) -> Self {
Self {
onset,
offset,
on: false,
}
}
pub(crate) fn update(&mut self, prob: f32) -> bool {
self.on = if self.on {
prob >= self.offset
} else {
prob >= self.onset
};
self.on
}
pub(crate) fn reset(&mut self) {
self.on = false;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RegionEvent {
Start { start_frame: usize },
End {
start_frame: usize,
end_frame: usize,
},
}
#[cfg_attr(not(feature = "segmentation"), allow(dead_code))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TailPolicy {
Keep,
Trim,
}
#[derive(Debug, Clone)]
pub(crate) struct RegionTracker {
min_off: usize,
min_on: usize,
tail: TailPolicy,
in_region: bool,
start_frame: usize,
last_active_frame: usize,
off_count: usize,
}
impl RegionTracker {
pub(crate) fn new(min_off: usize, min_on: usize, tail: TailPolicy) -> Self {
Self {
min_off,
min_on,
tail,
in_region: false,
start_frame: 0,
last_active_frame: 0,
off_count: 0,
}
}
pub(crate) fn advance(&mut self, active: bool, frame: usize) -> Option<RegionEvent> {
if self.in_region {
if active {
self.last_active_frame = frame;
self.off_count = 0;
None
} else {
self.off_count += 1;
if self.off_count >= self.min_off {
let end_frame = match self.tail {
TailPolicy::Keep => frame + 1,
TailPolicy::Trim => self.last_active_frame + 1,
};
Some(self.close(end_frame))
} else {
None
}
}
} else if active {
self.in_region = true;
self.start_frame = frame;
self.last_active_frame = frame;
self.off_count = 0;
Some(RegionEvent::Start { start_frame: frame })
} else {
None
}
}
pub(crate) fn flush(&mut self, frame: usize) -> Option<RegionEvent> {
if self.in_region {
let end_frame = match self.tail {
TailPolicy::Keep => frame,
TailPolicy::Trim => self.last_active_frame + 1,
};
Some(self.close(end_frame))
} else {
None
}
}
#[cfg_attr(not(feature = "segmentation"), allow(dead_code))]
pub(crate) fn reset(&mut self) -> Option<RegionEvent> {
if self.in_region {
Some(self.close(self.last_active_frame + 1))
} else {
None
}
}
pub(crate) fn in_region(&self) -> bool {
self.in_region
}
pub(crate) fn min_on(&self) -> usize {
self.min_on
}
pub(crate) fn keeps(&self, start_frame: usize, end_frame: usize) -> bool {
end_frame - start_frame >= self.min_on
}
fn close(&mut self, end_frame: usize) -> RegionEvent {
let event = RegionEvent::End {
start_frame: self.start_frame,
end_frame,
};
self.in_region = false;
self.off_count = 0;
event
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gate_plain_threshold_flips_immediately() {
let mut gate = HysteresisGate::new(0.5, 0.5);
assert!(!gate.update(0.4));
assert!(gate.update(0.5));
assert!(!gate.update(0.4));
}
#[test]
fn gate_hysteresis_holds_through_dip() {
let mut gate = HysteresisGate::new(0.6, 0.3);
assert!(!gate.update(0.55)); assert!(gate.update(0.7));
assert!(gate.update(0.45)); assert!(!gate.update(0.2)); assert!(!gate.update(0.45)); }
#[test]
fn tracker_keep_bridges_short_gap_and_keeps_closing_tail() {
let mut t = RegionTracker::new(3, 0, TailPolicy::Keep);
assert_eq!(
t.advance(true, 0),
Some(RegionEvent::Start { start_frame: 0 })
);
assert_eq!(t.advance(false, 1), None);
assert_eq!(t.advance(false, 2), None);
assert_eq!(t.advance(true, 3), None); assert_eq!(t.advance(false, 4), None);
assert_eq!(t.advance(false, 5), None);
assert_eq!(
t.advance(false, 6),
Some(RegionEvent::End {
start_frame: 0,
end_frame: 7,
})
);
assert!(!t.in_region());
}
#[test]
fn tracker_trim_rewinds_to_last_active_frame() {
let mut t = RegionTracker::new(3, 0, TailPolicy::Trim);
assert_eq!(
t.advance(true, 0),
Some(RegionEvent::Start { start_frame: 0 })
);
assert_eq!(t.advance(true, 2), None);
assert_eq!(t.advance(false, 3), None);
assert_eq!(t.advance(false, 4), None);
assert_eq!(
t.advance(false, 5),
Some(RegionEvent::End {
start_frame: 0,
end_frame: 3, })
);
let mut t = RegionTracker::new(3, 0, TailPolicy::Trim);
let _ = t.advance(true, 1);
let _ = t.advance(false, 2);
assert_eq!(
t.flush(10),
Some(RegionEvent::End {
start_frame: 1,
end_frame: 2,
})
);
}
#[test]
fn tracker_keep_flush_extends_to_flush_point() {
let mut t = RegionTracker::new(3, 0, TailPolicy::Keep);
let _ = t.advance(true, 0);
let _ = t.advance(false, 1);
assert_eq!(
t.flush(5),
Some(RegionEvent::End {
start_frame: 0,
end_frame: 5,
})
);
assert_eq!(t.flush(5), None, "already closed");
}
#[test]
fn tracker_reset_hard_closes_at_last_active_frame() {
let mut t = RegionTracker::new(5, 0, TailPolicy::Trim);
let _ = t.advance(true, 0);
let _ = t.advance(false, 1); assert_eq!(
t.reset(),
Some(RegionEvent::End {
start_frame: 0,
end_frame: 1,
}),
"...but a coverage hole closes the region immediately"
);
assert_eq!(t.reset(), None);
}
#[test]
fn tracker_min_off_zero_or_one_disables_bridging() {
for min_off in [0, 1] {
let mut t = RegionTracker::new(min_off, 0, TailPolicy::Trim);
let _ = t.advance(true, 0);
assert_eq!(
t.advance(false, 1),
Some(RegionEvent::End {
start_frame: 0,
end_frame: 1,
}),
"min_off {min_off} must close on the first inactive frame"
);
}
}
#[test]
fn tracker_keeps_applies_min_on() {
let t = RegionTracker::new(0, 3, TailPolicy::Trim);
assert!(t.keeps(0, 3));
assert!(!t.keeps(0, 2));
assert_eq!(t.min_on(), 3);
let t = RegionTracker::new(0, 1, TailPolicy::Trim);
assert!(t.keeps(4, 5));
}
}