use std::collections::{HashMap, HashSet};
use crate::core::Detections;
use crate::geometry::{Point, Zone};
#[derive(Debug, Clone)]
pub struct LineZone {
pub start: Point,
pub end: Point,
in_count: usize,
out_count: usize,
counted: HashSet<usize>,
}
impl LineZone {
pub fn new(start: Point, end: Point) -> Self {
Self {
start,
end,
in_count: 0,
out_count: 0,
counted: HashSet::new(),
}
}
fn orientation(a: Point, b: Point, c: Point) -> f32 {
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
}
fn on_segment(a: Point, b: Point, c: Point) -> bool {
c.x <= a.x.max(b.x) && c.x >= a.x.min(b.x) && c.y <= a.y.max(b.y) && c.y >= a.y.min(b.y)
}
fn segments_intersect(p1: Point, p2: Point, p3: Point, p4: Point) -> bool {
let d1 = Self::orientation(p3, p4, p1);
let d2 = Self::orientation(p3, p4, p2);
let d3 = Self::orientation(p1, p2, p3);
let d4 = Self::orientation(p1, p2, p4);
if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
&& ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
{
return true;
}
if d1 == 0.0 && Self::on_segment(p3, p4, p1) {
return true;
}
if d2 == 0.0 && Self::on_segment(p3, p4, p2) {
return true;
}
if d3 == 0.0 && Self::on_segment(p1, p2, p3) {
return true;
}
if d4 == 0.0 && Self::on_segment(p1, p2, p4) {
return true;
}
false
}
}
impl Zone for LineZone {
fn trigger(&mut self, detections: &Detections, previous_centroids: &HashMap<usize, Point>) {
for detection in detections.iter() {
let Some(tracker_id) = detection.tracker_id else {
continue;
};
if self.counted.contains(&tracker_id) {
continue;
}
let Some(&previous) = previous_centroids.get(&tracker_id) else {
continue;
};
let (cx, cy) = detection.centroid();
let current = Point::new(cx, cy);
if previous == current {
continue;
}
if Self::segments_intersect(self.start, self.end, previous, current) {
let line_vector = self.end - self.start;
let movement_vector = current - previous;
let cross = line_vector.cross(&movement_vector);
if cross > 0.0 {
self.in_count += 1;
} else {
self.out_count += 1;
}
self.counted.insert(tracker_id);
}
}
}
fn in_count(&self) -> usize {
self.in_count
}
fn out_count(&self) -> usize {
self.out_count
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Detection;
fn tracked_detection(tracker_id: usize, x: f32, y: f32) -> Detection {
let mut d = Detection::new([x - 1.0, y - 1.0, x + 1.0, y + 1.0], 0.9, 0);
d.tracker_id = Some(tracker_id);
d
}
#[test]
fn segments_intersect_simple_crossing() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(10.0, 10.0);
let p3 = Point::new(0.0, 10.0);
let p4 = Point::new(10.0, 0.0);
assert!(LineZone::segments_intersect(p1, p2, p3, p4));
}
#[test]
fn segments_do_not_intersect_when_parallel_and_apart() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(10.0, 0.0);
let p3 = Point::new(0.0, 5.0);
let p4 = Point::new(10.0, 5.0);
assert!(!LineZone::segments_intersect(p1, p2, p3, p4));
}
#[test]
fn segments_do_not_intersect_when_disjoint_on_same_line() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(1.0, 0.0);
let p3 = Point::new(2.0, 0.0);
let p4 = Point::new(3.0, 0.0);
assert!(!LineZone::segments_intersect(p1, p2, p3, p4));
}
#[test]
fn segments_intersect_when_collinear_and_overlapping() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(2.0, 0.0);
let p3 = Point::new(1.0, 0.0);
let p4 = Point::new(3.0, 0.0);
assert!(LineZone::segments_intersect(p1, p2, p3, p4));
}
#[test]
fn trigger_counts_a_crossing_exactly_once() {
let mut zone = LineZone::new(Point::new(0.0, 0.0), Point::new(0.0, 10.0));
let mut previous = HashMap::new();
previous.insert(1usize, Point::new(-5.0, 5.0));
let detections = Detections::new(vec![tracked_detection(1, 5.0, 5.0)]);
zone.trigger(&detections, &previous);
assert_eq!(zone.in_count() + zone.out_count(), 1);
previous.insert(1usize, Point::new(5.0, 5.0));
let detections2 = Detections::new(vec![tracked_detection(1, 15.0, 5.0)]);
zone.trigger(&detections2, &previous);
assert_eq!(zone.in_count() + zone.out_count(), 1);
}
#[test]
fn trigger_ignores_detections_without_tracker_id() {
let mut zone = LineZone::new(Point::new(0.0, 0.0), Point::new(0.0, 10.0));
let previous = HashMap::new();
let detections = Detections::new(vec![Detection::new([4.0, 4.0, 6.0, 6.0], 0.9, 0)]);
zone.trigger(&detections, &previous);
assert_eq!(zone.in_count(), 0);
assert_eq!(zone.out_count(), 0);
}
#[test]
fn trigger_ignores_motion_that_does_not_cross_the_line() {
let mut zone = LineZone::new(Point::new(0.0, 0.0), Point::new(0.0, 10.0));
let mut previous = HashMap::new();
previous.insert(1usize, Point::new(5.0, 5.0));
let detections = Detections::new(vec![tracked_detection(1, 8.0, 5.0)]);
zone.trigger(&detections, &previous);
assert_eq!(zone.in_count(), 0);
assert_eq!(zone.out_count(), 0);
}
#[test]
fn trigger_opposite_directions_produce_opposite_counts() {
let mut zone_a = LineZone::new(Point::new(0.0, 0.0), Point::new(0.0, 10.0));
let mut previous_a = HashMap::new();
previous_a.insert(1usize, Point::new(-5.0, 5.0));
zone_a.trigger(
&Detections::new(vec![tracked_detection(1, 5.0, 5.0)]),
&previous_a,
);
let mut zone_b = LineZone::new(Point::new(0.0, 0.0), Point::new(0.0, 10.0));
let mut previous_b = HashMap::new();
previous_b.insert(1usize, Point::new(5.0, 5.0));
zone_b.trigger(
&Detections::new(vec![tracked_detection(1, -5.0, 5.0)]),
&previous_b,
);
assert_ne!(
zone_a.in_count() > 0,
zone_b.in_count() > 0,
"crossing in opposite directions should flip which counter increments"
);
}
}