use std::collections::BTreeMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Side {
Top,
Bottom,
Left,
Right,
}
pub const SIDES: [Side; 4] = [Side::Top, Side::Bottom, Side::Left, Side::Right];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Pose {
pub frame: u32,
pub x: i64,
pub y: i64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Edges {
pub top: Option<i64>,
pub bottom: Option<i64>,
pub left: Option<i64>,
pub right: Option<i64>,
}
impl Edges {
pub fn overshoot(&self, x: i64, y: i64) -> [i64; 4] {
let past = |edge: Option<i64>, outward: i64| edge.map_or(0, |e| (outward - e).max(0));
[
past(self.top.map(|e| -e), -y),
past(self.bottom, y),
past(self.left.map(|e| -e), -x),
past(self.right, x),
]
}
pub fn is_empty(&self) -> bool {
*self == Edges::default()
}
}
const GAP: u32 = 3;
const CONVERGE: i64 = 5;
const TAIL: usize = 3;
const MIN_DEPTH: i64 = 2;
const CAP_NUM: i64 = 1;
const CAP_DEN: i64 = 4;
const NEAR: i64 = 3;
const HEAVY_REST: u32 = 8;
pub fn page_edges(track: &[Pose], width: i64, height: i64) -> Edges {
let along = |f: &dyn Fn(&Pose) -> i64| -> Vec<(u32, i64)> {
track.iter().map(|p| (p.frame, f(p))).collect()
};
Edges {
top: edge(&along(&|p| -p.y), height).map(|e| -e),
bottom: edge(&along(&|p| p.y), height),
left: edge(&along(&|p| -p.x), width).map(|e| -e),
right: edge(&along(&|p| p.x), width),
}
}
struct Return {
end: i64,
settled: bool,
}
fn joined(a: u32, b: u32) -> bool {
b.saturating_sub(a) <= GAP
}
fn returns(seq: &[(u32, i64)], extent: i64) -> Vec<Return> {
let mut out = Vec::new();
let mut start = 0;
for i in 0..seq.len() {
if i > 0 && !joined(seq[i - 1].0, seq[i].0) {
start = i;
}
if i == start {
continue;
}
let u = seq[i].1;
if seq[i - 1].1 <= u {
continue;
}
let next = seq.get(i + 1).filter(|n| joined(seq[i].0, n.0));
if next.is_some_and(|n| n.1 < u) {
continue;
}
let mut j = i - 1;
while seq[j].1 > u && j > start {
j -= 1;
}
if seq[j].1 > u {
continue;
}
let mut tail = 0;
let mut k = i;
while k > j + 1 && tail < TAIL {
let step = seq[k - 1].1 - seq[k].1;
if !(0..=CONVERGE).contains(&step) {
break;
}
if step > 0 {
tail += 1;
}
k -= 1;
}
if tail < TAIL {
continue;
}
let depth = seq[j + 1..i].iter().map(|s| s.1).max().unwrap_or(u) - u;
if depth < MIN_DEPTH || depth * CAP_DEN > extent * CAP_NUM {
continue;
}
let stays = |k: usize| {
seq.get(k)
.is_some_and(|s| s.1 == u && joined(seq[k - 1].0, s.0))
};
out.push(Return {
end: u,
settled: stays(i + 1) && stays(i + 2),
});
}
out
}
fn outermost_group(mut values: Vec<i64>) -> Option<(i64, i64)> {
values.sort_unstable();
let mut groups: Vec<(i64, i64, usize)> = Vec::new();
for v in values {
match groups.last_mut() {
Some(g) if v - g.1 <= NEAR => {
g.1 = v;
g.2 += 1;
}
_ => groups.push((v, v, 1)),
}
}
let largest = groups.iter().map(|g| g.2).max()?;
let need = largest.div_ceil(4);
groups
.iter()
.rev()
.find(|g| g.2 >= need)
.map(|g| (g.0, g.1))
}
fn edge(seq: &[(u32, i64)], extent: i64) -> Option<i64> {
let found = returns(seq, extent);
let mut rests: BTreeMap<i64, u32> = BTreeMap::new();
for w in seq.windows(2) {
if joined(w[0].0, w[1].0) && w[0].1 == w[1].1 {
*rests.entry(w[1].1).or_default() += 1;
}
}
let mass = |lo: i64, hi: i64| rests.range(lo..=hi).map(|(_, &n)| n).sum::<u32>();
let settled: Vec<i64> = found.iter().filter(|r| r.settled).map(|r| r.end).collect();
let (mut lo, mut hi) = if settled.is_empty() {
outermost_group(found.iter().map(|r| r.end).collect())?
} else {
outermost_group(settled.clone())?
};
if settled.is_empty() && mass(lo - NEAR, hi + NEAR) < HEAVY_REST {
let reach = extent * CAP_NUM / CAP_DEN;
let stayed = rests
.range(lo - reach..lo)
.rev()
.map(|(&v, _)| v)
.find(|&v| mass(v - NEAR, v + NEAR) >= HEAVY_REST);
if let Some(v) = stayed {
(lo, hi) = (v, v);
}
}
let edge = rests
.range(lo - NEAR..=hi + NEAR)
.max_by_key(|(&v, &n)| (n, v))
.map_or(lo, |(&v, _)| v);
let beyond = seq.iter().map(|s| s.1 - edge).max().unwrap_or(0);
(beyond * CAP_DEN <= extent * CAP_NUM).then_some(edge)
}
pub struct Lines<'a> {
pub rgba: &'a [u8],
pub width: usize,
pub height: usize,
pub labels: Option<(&'a [u8], u8)>,
pub region: (i64, i64, i64, i64),
pub neighbours: [Option<&'a [u8]>; 2],
pub noise: f64,
}
const LINE_SHARE: usize = 19;
pub const PINNED_RUN: usize = 32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Line {
Background,
Pinned,
Content,
}
pub fn background_lines(input: &Lines, side: Side, max: usize) -> usize {
let (w, h) = (input.width as i64, input.height as i64);
let x0 = input.region.0.clamp(0, w);
let y0 = input.region.1.clamp(0, h);
let x1 = (input.region.0 + input.region.2).clamp(x0, w);
let y1 = (input.region.1 + input.region.3).clamp(y0, h);
let count = match side {
Side::Top | Side::Bottom => y1 - y0,
Side::Left | Side::Right => x1 - x0,
} as usize;
let mut pixels: Vec<usize> = Vec::new();
let (mut counted, mut pinned) = (0, 0);
for line in 0..count.min(max) {
let l = line as i64;
pixels.clear();
let mut push = |x: i64, y: i64| {
let i = (y * w + x) as usize;
if input.labels.is_none_or(|(labels, code)| labels[i] == code) {
pixels.push(i);
}
};
match side {
Side::Top => (x0..x1).for_each(|x| push(x, y0 + l)),
Side::Bottom => (x0..x1).for_each(|x| push(x, y1 - 1 - l)),
Side::Left => (y0..y1).for_each(|y| push(x0 + l, y)),
Side::Right => (y0..y1).for_each(|y| push(x1 - 1 - l, y)),
}
match classify(input, &pixels) {
Line::Background => {
counted = line + 1;
pinned = 0;
}
Line::Pinned if pinned < PINNED_RUN => pinned += 1,
_ => break,
}
}
counted
}
fn classify(input: &Lines, pixels: &[usize]) -> Line {
if pixels.is_empty() {
return Line::Background;
}
let rgba = input.rgba;
let mut histogram = [[0u32; 256]; 3];
for &i in pixels {
for (c, h) in histogram.iter_mut().enumerate() {
h[rgba[i * 4 + c] as usize] += 1;
}
}
let half = pixels.len().div_ceil(2) as u32;
let median = histogram.map(|h| {
let mut seen = 0;
h.iter()
.position(|&n| {
seen += n;
seen >= half
})
.unwrap_or(0) as u8
});
let limit = 3.0 * input.noise;
let near = |a: &[u8], b: &[u8]| {
let d: i32 = (0..3).map(|c| (a[c] as i32 - b[c] as i32).abs()).sum();
d as f64 <= limit
};
let (mut plain, mut pinned) = (0, 0);
for &i in pixels {
let p = &rgba[i * 4..i * 4 + 3];
if near(p, &median) {
plain += 1;
} else if input
.neighbours
.iter()
.flatten()
.any(|n| near(p, &n[i * 4..i * 4 + 3]))
{
pinned += 1;
}
}
let enough = |n: usize| n * 20 >= pixels.len() * LINE_SHARE;
if enough(plain) {
Line::Background
} else if enough(plain + pinned) {
Line::Pinned
} else {
Line::Content
}
}
#[cfg(test)]
mod tests;