use crate::protocol::decode::Image;
use std::ops::{Range, RangeInclusive};
use tracing::*;
const TRIM: usize = 8;
const REACH: usize = 2;
const SCALE: f32 = 0.90;
const BARE: f32 = 0.10;
const TOLERANCE: f32 = 0.15;
const CLOSE: f32 = 0.70;
const PITCH: RangeInclusive<usize> = 18..=31;
const PICTURE: usize = 8;
const RUN: f32 = 0.50;
#[derive(Debug, Clone, PartialEq)]
pub struct Strip {
pub frames: Vec<Range<usize>>,
pub pitch: usize,
pub contrast: f32,
}
fn detail(image: &Image) -> Vec<f32> {
let band = TRIM..image.rows.saturating_sub(TRIM);
let mut out = vec![0.0; image.cols];
if band.len() < 2 || image.colors.is_empty() {
return out;
}
let full = match image.bits {
1..16 => ((1u32 << image.bits) - 1) as f32,
_ => f32::from(u16::MAX),
};
let n = band.len() as f32;
for (x, out) in out.iter_mut().enumerate() {
let mut sum = 0.0;
for plane in &image.colors {
let at = |y: usize| f32::from(plane[y * image.cols + x]) / full;
let mean = band.clone().map(at).sum::<f32>() / n;
let var = band.clone().map(|y| (at(y) - mean).powi(2)).sum::<f32>() / n;
sum += var.sqrt();
}
*out = sum / image.colors.len() as f32;
}
let mut sorted = out.clone();
sorted.sort_by(f32::total_cmp);
let scale = sorted[(sorted.len() as f32 * SCALE) as usize % sorted.len()];
if scale > 0.0 {
for v in &mut out {
*v = (*v / scale).min(1.0);
}
}
out
}
struct Detail {
at: Vec<f32>,
upto: Vec<f32>,
}
impl Detail {
fn new(at: Vec<f32>) -> Self {
let mut upto = Vec::with_capacity(at.len() + 1);
upto.push(0.0);
for v in &at {
upto.push(upto[upto.len() - 1] + v);
}
Self { at, upto }
}
fn mean(&self, span: Range<usize>) -> f32 {
let end = span.end.min(self.at.len());
if span.start >= end {
return 0.0;
}
(self.upto[end] - self.upto[span.start]) / (end - span.start) as f32
}
fn film(&self, length: usize) -> Range<usize> {
let mut sorted = self.at.clone();
sorted.sort_by(f32::total_cmp);
let at = |q: f32| sorted[((sorted.len() as f32 * q) as usize).min(sorted.len() - 1)];
let bare = at(BARE);
let level = bare + (at(SCALE) - bare) * TOLERANCE;
let close = (length as f32 * CLOSE) as usize;
let least = ((length as f32 * RUN) as usize).max(1);
let mut runs: Vec<Range<usize>> = Vec::new();
let mut x = 0;
while x < self.at.len() {
if self.at[x] <= level {
x += 1;
continue;
}
let from = x;
while x + 1 < self.at.len() && self.at[x + 1] > level {
x += 1;
}
match runs.last_mut() {
Some(last) if from - last.end <= close => last.end = x + 1,
_ => runs.push(from..x + 1),
}
x += 1;
}
let kept: Vec<&Range<usize>> = runs.iter().filter(|r| r.len() >= least).collect();
match (kept.first(), kept.last()) {
(Some(a), Some(b)) => a.start..b.end,
_ => 0..0,
}
}
fn gap(&self, at: usize) -> f32 {
let to = (at + REACH + 1).min(self.at.len());
let from = at.saturating_sub(REACH).min(to.saturating_sub(1));
self.at[from..to].iter().copied().fold(f32::MAX, f32::min)
}
}
fn picture(first: usize, pitch: usize, k: usize) -> Range<usize> {
let gap = first + k * pitch;
(gap + REACH + 1)..(gap + pitch).saturating_sub(REACH)
}
fn score(detail: &Detail, first: usize, pitch: usize, frames: usize) -> f32 {
let mut gaps = 0.0;
for k in 0..=frames {
gaps += detail.gap(first + k * pitch);
}
let mut pictures = 0.0;
for k in 0..frames {
pictures += detail.mean(picture(first, pitch, k));
}
pictures / frames as f32 - gaps / (frames + 1) as f32
}
pub fn find(image: &Image, length: usize) -> Option<Strip> {
if length == 0 || image.cols == 0 {
return None;
}
let detail = Detail::new(detail(image));
let cols = image.cols;
let film = detail.film(length);
debug!(?film, cols, length, "the picture in the pass");
let least = PICTURE + 2 * REACH + 2;
let mut best: Option<(f32, usize, usize, usize)> = None;
for pitch in (length * PITCH.start() / 20).max(least)..=(length * PITCH.end() / 20).max(least) {
let frames = (film.len() + pitch / 2) / pitch;
if frames == 0 {
continue;
}
let from = film.start.saturating_sub(pitch);
let to = (film.end + pitch).min(cols.saturating_sub(1));
let Some(last) = to.checked_sub(frames * pitch).filter(|&l| l >= from) else {
continue;
};
for first in from..=last {
let at = score(&detail, first, pitch, frames);
if best.is_none_or(|(had, ..)| at > had) {
best = Some((at, first, pitch, frames));
}
}
}
let (contrast, first, pitch, frames) = best?;
let found: Vec<Range<usize>> = (0..frames).map(|k| picture(first, pitch, k)).collect();
debug!(?found, pitch, contrast, "fitted the strip");
Some(Strip {
frames: found,
pitch,
contrast,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::{decode::Samples, image::Layout};
const SENSOR: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Polarity {
Positive,
Negative,
}
struct Levels {
between: u16,
picture: u16,
}
fn levels(polarity: Polarity) -> Levels {
match polarity {
Polarity::Positive => Levels {
between: 700,
picture: 9000,
},
Polarity::Negative => Levels {
between: 30000,
picture: 8000,
},
}
}
fn column(plane: &mut [u16], feed: usize, x: usize, level: u16, contrast: f32) {
for y in 0..SENSOR {
let swing = ((y * 7 + x * 3) % 11) as f32 / 11.0 - 0.5;
let v = f32::from(level) * (1.0 + contrast * swing);
plane[y * feed + x] = v.clamp(0.0, f32::from(u16::MAX)) as u16;
}
}
struct Film {
feed: usize,
length: usize,
polarity: Polarity,
frames: Vec<usize>,
flat: Option<usize>,
blank: Option<usize>,
gate: Option<(usize, usize)>,
mask: usize,
edges: Vec<(usize, usize)>,
}
impl Film {
fn new(frames: Vec<usize>, length: usize, polarity: Polarity) -> Self {
let feed = frames.iter().max().unwrap_or(&0) + length + 60;
Self {
feed,
length,
polarity,
frames,
flat: None,
blank: None,
gate: None,
mask: 0,
edges: Vec::new(),
}
}
fn render(&self) -> Samples {
let level = levels(self.polarity);
let mut colors = vec![vec![0u16; SENSOR * self.feed]; 3];
for x in 0..self.feed {
let inside = self
.frames
.iter()
.position(|&top| (top..top + self.length).contains(&x));
let (value, contrast) = match inside {
_ if self.edges.iter().any(|&(a, b)| (a..b).contains(&x)) => (30000, 0.95),
_ if x < self.mask => (140, 0.10),
_ if self.gate.is_some_and(|(a, b)| (a..b).contains(&x)) => (65200, 0.0),
Some(n) if Some(n) == self.blank => (level.between, 0.0),
Some(n) if Some(n) == self.flat => (level.picture, 0.0),
Some(_) => (level.picture, 0.55),
None => (level.between, 0.0),
};
for plane in &mut colors {
column(plane, self.feed, x, value, contrast);
}
}
Samples { colors, ir: None }
}
fn layout(&self) -> Layout {
Layout::single_line(SENSOR as u32, self.feed as u32, vec![1, 2, 3])
}
}
fn fit(film: &Film, length: usize) -> Strip {
let samples: Samples = film.render();
let layout = film.layout();
let image = Image::new(&layout, &samples).expect("the buffer is the layout's size");
find(&image, length).expect("a strip with frames on it")
}
fn holds(found: &Strip, want: &[usize], length: usize) {
let places: Vec<Range<usize>> = found.frames.clone();
assert_eq!(places.len(), want.len(), "{places:?} against {want:?}");
for (place, &top) in places.iter().zip(want) {
let middle = top + length / 2;
assert!(
place.contains(&middle),
"{places:?} should each hold the middle of a frame of {want:?}"
);
}
}
#[test]
fn a_fitted_strip_is_regular() {
let film = Film::new(vec![30, 162, 294, 426], 120, Polarity::Negative);
let found = fit(&film, 120);
assert!(
found.pitch.abs_diff(132) <= REACH,
"pitch {} in {:?}",
found.pitch,
found.frames
);
for pair in found.frames.windows(2) {
assert_eq!(pair[0].len(), pair[1].len(), "{:?}", found.frames);
assert_eq!(pair[1].start - pair[0].start, found.pitch);
}
}
#[test]
fn polarity_does_not_change_the_answer() {
let want = [30, 162, 294, 426];
let fits: Vec<Strip> = [Polarity::Positive, Polarity::Negative]
.into_iter()
.map(|polarity| fit(&Film::new(want.to_vec(), 120, polarity), 120))
.collect();
holds(&fits[0], &want, 120);
assert_eq!(fits[0].frames, fits[1].frames);
}
#[test]
fn the_bare_gate_past_the_film_is_not_a_frame() {
let mut film = Film::new(vec![30, 162, 294], 120, Polarity::Positive);
film.feed = 560;
film.gate = Some((430, 520));
holds(&fit(&film, 120), &[30, 162, 294], 120);
}
#[test]
fn a_frame_behind_the_holder_mask_keeps_its_place() {
let mut film = Film::new(vec![20, 152, 284], 120, Polarity::Positive);
film.mask = 40;
holds(&fit(&film, 120), &[20, 152, 284], 120);
}
#[test]
fn a_flat_picture_is_still_a_frame() {
for polarity in [Polarity::Positive, Polarity::Negative] {
let mut film = Film::new(vec![30, 162, 294], 120, polarity);
film.flat = Some(1);
holds(&fit(&film, 120), &[30, 162, 294], 120);
}
}
#[test]
fn a_frame_with_no_picture_in_it_still_gets_a_place() {
let mut film = Film::new(vec![30, 162, 294, 426], 120, Polarity::Positive);
film.blank = Some(2);
holds(&fit(&film, 120), &[30, 162, 294, 426], 120);
}
#[test]
fn a_short_wind_comes_back_short() {
let film = Film::new(vec![30, 155, 280], 120, Polarity::Negative);
let found = fit(&film, 120);
assert!(
found.pitch.abs_diff(125) <= REACH,
"pitch {} in {:?}",
found.pitch,
found.frames
);
holds(&found, &[30, 155, 280], 120);
}
#[test]
fn the_film_says_how_many_frames() {
let film = Film::new(vec![30, 162, 294, 426], 120, Polarity::Negative);
let found = fit(&film, 120);
holds(&found, &[30, 162, 294, 426], 120);
assert_eq!(found.frames.len(), 4);
}
#[test]
fn an_edge_at_the_end_of_the_pass_is_not_a_frame() {
let mut film = Film::new(vec![30, 162, 294], 120, Polarity::Negative);
film.feed = 700;
film.edges = vec![(20, 24), (540, 578)];
let found = fit(&film, 120);
assert_eq!(found.frames.len(), 3, "{:?}", found.frames);
holds(&found, &[30, 162, 294], 120);
}
#[test]
fn an_empty_holder_holds_no_frames() {
let film = Film::new(Vec::new(), 120, Polarity::Positive);
let samples = film.render();
let layout = film.layout();
let image = Image::new(&layout, &samples).expect("the buffer is the layout's size");
assert!(find(&image, 120).is_none());
}
}