use std::collections::VecDeque;
use crate::frame::{Frame, Rect};
use crate::template::Template;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Match {
pub x: i32,
pub y: i32,
pub score: f32,
}
impl Match {
pub fn center(&self, tpl: &Template) -> (i32, i32) {
(
self.x + tpl.width as i32 / 2,
self.y + tpl.height as i32 / 2,
)
}
}
pub trait Matcher {
fn find(&self, frame: &Frame, tpl: &Template) -> Option<Match>;
fn find_in(&self, frame: &Frame, tpl: &Template, region: Rect) -> Option<Match> {
let r = frame.clamp(region);
let sub = frame.crop(r);
self.find(&sub, tpl).map(|m| Match {
x: m.x + r.x as i32,
y: m.y + r.y as i32,
score: m.score,
})
}
fn find_all(&self, frame: &Frame, tpl: &Template, region: Rect, max: usize) -> Vec<Match> {
let mut v = match self.find_in(frame, tpl, region) {
Some(m) => vec![m],
None => Vec::new(),
};
if max != 0 {
v.truncate(max);
}
v
}
}
#[derive(Clone, Copy, Debug)]
pub struct RgbMatcher {
pub tolerance: i32,
}
impl RgbMatcher {
pub fn new(tolerance: i32) -> Self {
RgbMatcher { tolerance }
}
}
impl Matcher for RgbMatcher {
fn find(&self, frame: &Frame, tpl: &Template) -> Option<Match> {
self.find_in(frame, tpl, frame.full_rect())
}
fn find_in(&self, frame: &Frame, tpl: &Template, region: Rect) -> Option<Match> {
let s = Scan::new(frame, tpl, self.tolerance, region)?;
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
let total = s.y_hi - s.y_lo;
let nthreads = rayon::current_num_threads().max(1);
let blocks = (nthreads * 8).max(1).min(total.max(1));
let per = total.div_ceil(blocks);
let mut start = 0usize;
while start < total {
let y0 = s.y_lo + start;
let y1 = (y0 + per).min(s.y_hi);
let hit = (y0..y1)
.into_par_iter()
.filter_map(|yy| s.row_first(yy).map(|x| (x, yy)))
.min_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
if let Some((x, y)) = hit {
return Some(Match {
x: x as i32,
y: y as i32,
score: 1.0,
});
}
start += per;
}
None
}
#[cfg(not(feature = "parallel"))]
{
for y0 in s.y_lo..s.y_hi {
if let Some(x0) = s.row_first(y0) {
return Some(Match {
x: x0 as i32,
y: y0 as i32,
score: 1.0,
});
}
}
None
}
}
fn find_all(&self, frame: &Frame, tpl: &Template, region: Rect, max: usize) -> Vec<Match> {
let max = if max == 0 { usize::MAX } else { max };
let s = match Scan::new(frame, tpl, self.tolerance, region) {
Some(s) => s,
None => return Vec::new(),
};
let mut out: Vec<Match> = Vec::new();
let mut keep = RowDedup::new(s.vbw, s.vbh, s.x_lo, s.x_hi);
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
let total = s.y_hi - s.y_lo;
let lead = s.vbh.clamp(1, (total / 4).clamp(1, 64));
let stop = s.y_lo + lead;
let reached = scan_streaming(&s, &mut keep, max, &mut out, s.y_lo, stop);
if reached {
return out;
}
let nthreads = rayon::current_num_threads().max(1);
if keep.row_fully_covered() {
scan_streaming(&s, &mut keep, max, &mut out, stop, s.y_hi);
return out;
}
let per = total.div_ceil(nthreads * 8).max(1);
let mut y = stop;
while y < s.y_hi {
let y1 = (y + per).min(s.y_hi);
keep.advance_to(y);
if keep.block_skippable(y1) {
y = y1;
continue;
}
let mut rows: Vec<(usize, Vec<usize>)> = (y..y1)
.into_par_iter()
.map(|yy| (yy, s.row_all(yy)))
.collect();
rows.sort_unstable_by_key(|&(yy, _)| yy);
for (yy, xs) in rows {
keep.push_row(yy, &xs, max, &mut out);
if out.len() >= max {
return out;
}
}
y = y1;
}
}
#[cfg(not(feature = "parallel"))]
{
scan_streaming(&s, &mut keep, max, &mut out, s.y_lo, s.y_hi);
}
out
}
}
fn scan_streaming(
s: &Scan,
keep: &mut RowDedup,
max: usize,
out: &mut Vec<Match>,
y_from: usize,
y_to: usize,
) -> bool {
let mut kept_row: Vec<usize> = Vec::new();
for y in y_from..y_to {
keep.advance_to(y);
if keep.row_fully_covered() {
continue;
}
kept_row.clear();
let mut x = s.x_lo;
if keep.covered_span == 0 {
while x < s.x_hi {
if s.candidate_at(y, x) {
keep.keep(y, x, &mut kept_row, out);
if out.len() >= max {
return true;
}
x += 1;
break;
}
x += 1;
}
}
while x < s.x_hi {
if !keep.is_covered(x) && s.candidate_at(y, x) {
keep.keep(y, x, &mut kept_row, out);
if out.len() >= max {
return true;
}
}
x += 1;
}
keep.finish_row(y, std::mem::take(&mut kept_row));
}
false
}
struct RowDedup {
w: usize,
h: usize,
x_lo: usize,
x_hi: usize,
covered: Vec<u32>,
covered_span: usize,
window: VecDeque<(usize, Vec<usize>)>,
}
impl RowDedup {
fn new(w: usize, h: usize, x_lo: usize, x_hi: usize) -> Self {
RowDedup {
w,
h,
x_lo,
x_hi,
covered: vec![0u32; x_hi - x_lo],
covered_span: 0,
window: VecDeque::new(),
}
}
fn advance_to(&mut self, y: usize) {
while matches!(self.window.front(), Some(&(fy, _)) if y - fy >= self.h) {
if let Some((_, kept)) = self.window.pop_front() {
for x in kept {
self.shift(x, false);
}
}
}
}
#[inline]
fn is_covered(&self, x: usize) -> bool {
self.covered[x - self.x_lo] != 0
}
#[inline]
fn row_fully_covered(&self) -> bool {
self.covered_span == self.covered.len()
}
#[cfg(feature = "parallel")]
fn block_skippable(&self, y1: usize) -> bool {
if !self.row_fully_covered() {
return false;
}
match self.window.front() {
Some(&(fy, _)) => (y1 - 1).saturating_sub(fy) < self.h,
None => false,
}
}
fn keep(&mut self, y: usize, x: usize, kept_row: &mut Vec<usize>, out: &mut Vec<Match>) {
self.shift(x, true);
kept_row.push(x);
out.push(Match {
x: x as i32,
y: y as i32,
score: 1.0,
});
}
fn finish_row(&mut self, y: usize, kept: Vec<usize>) {
if !kept.is_empty() {
self.window.push_back((y, kept));
}
}
#[cfg(any(test, feature = "parallel"))]
fn push_row(&mut self, y: usize, xs: &[usize], max: usize, out: &mut Vec<Match>) {
self.advance_to(y);
let mut kept: Vec<usize> = Vec::new();
for &x in xs {
if self.is_covered(x) {
continue;
}
self.keep(y, x, &mut kept, out);
if out.len() >= max {
break;
}
}
self.finish_row(y, kept);
}
fn shift(&mut self, x: usize, add: bool) {
let lo = x.saturating_sub(self.w - 1).max(self.x_lo);
let hi = (x + self.w - 1).min(self.x_hi - 1);
for slot in &mut self.covered[lo - self.x_lo..=hi - self.x_lo] {
if add {
if *slot == 0 {
self.covered_span += 1;
}
*slot += 1;
} else {
if *slot == 1 {
self.covered_span -= 1;
}
*slot = slot.saturating_sub(1);
}
}
}
}
#[derive(Clone, Copy)]
struct Sample {
sx: usize,
sy: usize,
r: i32,
g: i32,
b: i32,
}
struct Scan<'a> {
px: &'a [u8],
tpl: &'a [u8],
sw4: usize,
off: (usize, usize, usize),
tw: usize,
th: usize,
tw3: usize,
thr: i32,
samples: Vec<Sample>,
mask: Option<&'a [bool]>,
vbw: usize,
vbh: usize,
y_lo: usize,
y_hi: usize,
x_lo: usize,
x_hi: usize,
}
impl<'a> Scan<'a> {
fn new(frame: &'a Frame, tpl: &'a Template, thr: i32, region: Rect) -> Option<Scan<'a>> {
let (tw, th) = (tpl.width, tpl.height);
if tw == 0 || th == 0 {
return None;
}
let r = frame.clamp(region);
if tw > r.width || th > r.height {
return None;
}
let tw3 = tw * 3;
let mask = tpl.mask.as_deref();
let (vbw, vbh) = match mask {
None => (tw, th),
Some(m) => {
let (mut x0, mut y0) = (usize::MAX, usize::MAX);
let (mut x1, mut y1) = (0usize, 0usize);
for y in 0..th {
for x in 0..tw {
if m[y * tw + x] {
x0 = x0.min(x);
y0 = y0.min(y);
x1 = x1.max(x);
y1 = y1.max(y);
}
}
}
if x0 == usize::MAX {
(tw, th)
} else {
(x1 - x0 + 1, y1 - y0 + 1)
}
}
};
let inset_x = 2.min(tw / 2);
let inset_y = 2.min(th / 2);
let cx = tw / 2;
let cy = th / 2;
let pts = [
(cx, cy),
(inset_x, inset_y),
(tw - 1 - inset_x, inset_y),
(inset_x, th - 1 - inset_y),
(tw - 1 - inset_x, th - 1 - inset_y),
];
let mut samples: Vec<Sample> = Vec::with_capacity(pts.len());
for (sx, sy) in pts {
if let Some(m) = mask {
if !m[sy * tw + sx] {
continue;
}
}
let i = sy * tw3 + sx * 3;
let s = Sample {
sx,
sy,
r: tpl.rgb[i] as i32,
g: tpl.rgb[i + 1] as i32,
b: tpl.rgb[i + 2] as i32,
};
if !samples.iter().any(|e| e.sx == s.sx && e.sy == s.sy) {
samples.push(s);
}
}
Some(Scan {
px: &frame.pixels,
tpl: &tpl.rgb,
sw4: frame.width * 4,
off: frame.rgb_offsets(),
tw,
th,
tw3,
thr,
samples,
mask,
vbw,
vbh,
x_lo: r.x,
x_hi: r.x + (r.width - tw) + 1,
y_lo: r.y,
y_hi: r.y + (r.height - th) + 1,
})
}
fn row_first(&self, y0: usize) -> Option<usize> {
(self.x_lo..self.x_hi).find(|&x0| self.anchor_ok(y0, x0) && self.verify(y0, x0))
}
#[inline(always)]
fn candidate_at(&self, y0: usize, x0: usize) -> bool {
self.anchor_ok(y0, x0) && self.verify(y0, x0)
}
#[cfg(feature = "parallel")]
fn row_all(&self, y0: usize) -> Vec<usize> {
let mut v = Vec::new();
self.row_all_into(y0, &mut v);
v
}
#[cfg(feature = "parallel")]
fn row_all_into(&self, y0: usize, out: &mut Vec<usize>) {
out.clear();
for x0 in self.x_lo..self.x_hi {
if self.anchor_ok(y0, x0) && self.verify(y0, x0) {
out.push(x0);
}
}
}
#[inline(always)]
fn anchor_ok(&self, y0: usize, x0: usize) -> bool {
let (ro, go, bo) = self.off;
for s in &self.samples {
let p = (y0 + s.sy) * self.sw4 + s.sx * 4 + x0 * 4;
if (self.px[p + ro] as i32 - s.r).abs() > self.thr
|| (self.px[p + go] as i32 - s.g).abs() > self.thr
|| (self.px[p + bo] as i32 - s.b).abs() > self.thr
{
return false;
}
}
true
}
#[inline(always)]
fn verify(&self, y0: usize, x0: usize) -> bool {
let (ro, go, bo) = self.off;
let w4 = self.tw * 4;
for ty in 0..self.th {
let sy = (y0 + ty) * self.sw4 + x0 * 4;
let ti = ty * self.tw3;
let srow = &self.px[sy..sy + w4];
let trow = &self.tpl[ti..ti + self.tw3];
let mut si = 0usize;
let mut tj = 0usize;
for tx in 0..self.tw {
if let Some(m) = self.mask {
if !m[ty * self.tw + tx] {
si += 4;
tj += 3;
continue;
}
}
if (srow[si + ro] as i32 - trow[tj] as i32).abs() > self.thr
|| (srow[si + go] as i32 - trow[tj + 1] as i32).abs() > self.thr
|| (srow[si + bo] as i32 - trow[tj + 2] as i32).abs() > self.thr
{
return false;
}
si += 4;
tj += 3;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_all_with_mask_dedups_by_visible_box() {
let (w, h) = (128usize, 96usize);
let (tw, th) = (64usize, 64usize);
let s = 16usize;
let mut px = vec![0u8; w * h * 4];
for (tx, ty) in [(10usize, 10usize), (50usize, 10usize)] {
for y in 0..s {
for x in 0..s {
let i = ((ty + y) * w + tx + x) * 4;
px[i] = 200;
px[i + 1] = (y * 5 + 2) as u8;
px[i + 2] = (x * 7 + 1) as u8;
px[i + 3] = 255;
}
}
}
let frame = Frame::bgra8(w, h, px);
let mut rgb = vec![0u8; tw * th * 3];
let mut mask = vec![false; tw * th];
for y in 0..s {
for x in 0..s {
let i = (y * tw + x) * 3;
rgb[i] = (x * 7 + 1) as u8;
rgb[i + 1] = (y * 5 + 2) as u8;
rgb[i + 2] = 200;
mask[y * tw + x] = true;
}
}
let tpl = Template::from_rgb(rgb, tw, th).with_mask(mask);
let got = RgbMatcher::new(0).find_all(&frame, &tpl, frame.full_rect(), 0);
assert_eq!(
got.len(),
2,
"可见区只有 16 px,相距 40 px 的两个目标应各自上报"
);
}
fn gradient(w: usize, h: usize) -> Vec<u8> {
let mut px = vec![0u8; w * h * 4];
for y in 0..h {
for x in 0..w {
let i = (y * w + x) * 4;
px[i] = (x % 251) as u8;
px[i + 1] = (y % 253) as u8;
px[i + 2] = ((x + y) % 249) as u8;
px[i + 3] = 255;
}
}
px
}
fn paste_block(
px: &mut [u8],
w: usize,
_h: usize,
tx: usize,
ty: usize,
s: usize,
col: [u8; 3],
) -> Vec<u8> {
let mut tpl = Vec::with_capacity(s * s * 3);
for y in ty..ty + s {
for x in tx..tx + s {
let i = (y * w + x) * 4;
px[i] = col[0];
px[i + 1] = col[1];
px[i + 2] = col[2];
px[i + 3] = 255;
tpl.extend_from_slice(&[col[0], col[1], col[2]]);
}
}
tpl
}
#[test]
fn rgb_finds_embedded_region() {
let (w, h) = (64usize, 64usize);
let mut px = gradient(w, h);
let tpl = paste_block(&mut px, w, h, 20, 15, 8, [255, 0, 255]);
let frame = Frame::rgba8(w, h, px);
let t = Template::from_rgb(tpl, 8, 8);
let r = RgbMatcher::new(0).find(&frame, &t).expect("应命中");
assert_eq!((r.x, r.y), (20, 15));
}
#[test]
fn rgb_handles_bgra_frame() {
let (w, h) = (48usize, 48usize);
let mut bgra = vec![0u8; w * h * 4];
for y in 0..h {
for x in 0..w {
let i = (y * w + x) * 4;
bgra[i] = (x + y) as u8;
bgra[i + 1] = y as u8;
bgra[i + 2] = x as u8;
bgra[i + 3] = 255;
}
}
let frame = Frame::bgra8(w, h, bgra.clone());
let mut tpl = Vec::new();
for y in 10..10 + 6 {
for x in 10..10 + 6 {
let i = (y * w + x) * 4;
tpl.extend_from_slice(&[bgra[i + 2], bgra[i + 1], bgra[i]]);
}
}
let t = Template::from_rgb(tpl, 6, 6);
let r = RgbMatcher::new(0).find(&frame, &t).expect("BGRA 应命中");
assert_eq!((r.x, r.y), (10, 10));
}
#[test]
fn rgb_returns_none_when_absent() {
let (w, h) = (32usize, 32usize);
let frame = Frame::rgba8(w, h, vec![10u8; w * h * 4]);
let t = Template::from_rgb(vec![200u8; 4 * 4 * 3], 4, 4);
assert!(RgbMatcher::new(0).find(&frame, &t).is_none());
}
#[test]
fn find_all_returns_two_matches() {
let (w, h) = (128usize, 64usize);
let mut px = gradient(w, h);
let tpl = paste_block(&mut px, w, h, 6, 6, 10, [10, 200, 20]);
paste_block(&mut px, w, h, 90, 40, 10, [10, 200, 20]);
let frame = Frame::rgba8(w, h, px);
let t = Template::from_rgb(tpl, 10, 10);
let all = RgbMatcher::new(0).find_all(&frame, &t, frame.full_rect(), 0);
assert_eq!(all.len(), 2, "应找到两处(重叠已去重)");
assert_eq!((all[0].x, all[0].y), (6, 6));
assert_eq!((all[1].x, all[1].y), (90, 40));
}
#[test]
fn find_all_respects_max() {
let (w, h) = (128usize, 64usize);
let mut px = gradient(w, h);
let tpl = paste_block(&mut px, w, h, 6, 6, 10, [10, 200, 20]);
paste_block(&mut px, w, h, 90, 40, 10, [10, 200, 20]);
let frame = Frame::rgba8(w, h, px);
let t = Template::from_rgb(tpl, 10, 10);
let one = RgbMatcher::new(0).find_all(&frame, &t, frame.full_rect(), 1);
assert_eq!(one.len(), 1);
}
#[test]
fn find_all_orders_by_y_then_x() {
let (w, h) = (200usize, 120usize);
let mut px = gradient(w, h);
let tpl = paste_block(&mut px, w, h, 150, 20, 10, [10, 200, 20]);
paste_block(&mut px, w, h, 20, 80, 10, [10, 200, 20]);
let frame = Frame::rgba8(w, h, px);
let t = Template::from_rgb(tpl, 10, 10);
let all = RgbMatcher::new(0).find_all(&frame, &t, frame.full_rect(), 0);
assert_eq!(
all.iter().map(|m| (m.x, m.y)).collect::<Vec<_>>(),
vec![(150, 20), (20, 80)],
"应从上到下(而非从左到右)排"
);
}
#[test]
fn find_all_keeps_topmost_leftmost_of_overlapping_hits() {
let (w, h) = (64usize, 64usize);
let mut px = gradient(w, h);
paste_block(&mut px, w, h, 12, 30, 19, [10, 200, 20]);
let frame = Frame::rgba8(w, h, px);
let t = Template::from_rgb([10u8, 200, 20].repeat(100).to_vec(), 10, 10);
let all = RgbMatcher::new(0).find_all(&frame, &t, frame.full_rect(), 0);
assert_eq!(
all.iter().map(|m| (m.x, m.y)).collect::<Vec<_>>(),
vec![(12, 30)],
"一块 19x19 的平坦区只应留最上最左的命中"
);
}
#[test]
fn row_dedup_matches_naive_reference() {
fn naive(w: usize, h: usize, cands: &[(usize, usize)]) -> Vec<(usize, usize)> {
let mut sorted = cands.to_vec();
sorted.sort_unstable_by_key(|&(x, y)| (y, x));
let mut kept: Vec<(usize, usize)> = Vec::new();
for c in sorted {
let clash = kept.iter().any(|k| {
(k.0 as i64 - c.0 as i64).abs() < w as i64
&& (k.1 as i64 - c.1 as i64).abs() < h as i64
});
if !clash {
kept.push(c);
}
}
kept
}
let (x_lo, x_hi, y_hi) = (3usize, 40usize, 24usize);
let mut seed = 0x2545_F491_4F6C_DD1Du64;
let mut rnd = move || {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(seed >> 33) as usize
};
for (w, h) in [(1usize, 1usize), (4, 3), (7, 9), (13, 2), (2, 17), (30, 20)] {
for _ in 0..20 {
let mut rows: Vec<(usize, Vec<usize>)> = Vec::new();
for y in 0..y_hi {
if rnd() % 3 == 0 {
continue; }
let mut xs: Vec<usize> = (x_lo..x_hi).filter(|_| rnd() % 5 == 0).collect();
xs.dedup();
if !xs.is_empty() {
rows.push((y, xs));
}
}
let flat: Vec<(usize, usize)> = rows
.iter()
.flat_map(|(y, xs)| xs.iter().map(move |&x| (x, *y)))
.collect();
let mut keep = RowDedup::new(w, h, x_lo, x_hi);
let mut out: Vec<Match> = Vec::new();
for (y, xs) in &rows {
keep.push_row(*y, xs, usize::MAX, &mut out);
}
let got: Vec<(i32, i32)> = out.iter().map(|m| (m.x, m.y)).collect();
let want: Vec<(i32, i32)> = naive(w, h, &flat)
.into_iter()
.map(|(x, y)| (x as i32, y as i32))
.collect();
assert_eq!(got, want, "w={w} h={h} 滑窗去重与朴素参照不一致");
let mut keep_s = RowDedup::new(w, h, x_lo, x_hi);
let mut out_s: Vec<Match> = Vec::new();
let mut kept_row: Vec<usize> = Vec::new();
for (y, xs) in &rows {
keep_s.advance_to(*y);
if keep_s.row_fully_covered() {
continue;
}
kept_row.clear();
for x in x_lo..x_hi {
if keep_s.is_covered(x) || !xs.contains(&x) {
continue;
}
keep_s.keep(*y, x, &mut kept_row, &mut out_s);
}
keep_s.finish_row(*y, std::mem::take(&mut kept_row));
}
let got_s: Vec<(i32, i32)> = out_s.iter().map(|m| (m.x, m.y)).collect();
assert_eq!(got_s, want, "w={w} h={h} 流式扫描与朴素参照不一致");
}
}
}
#[test]
fn fully_covered_rows_are_skipped_without_losing_matches() {
let (w, h) = (24usize, 24usize);
let frame = Frame::rgba8(w, h, vec![9u8; w * h * 4]);
let tpl = Template::from_rgb(vec![9u8; 4 * 4 * 3], 4, 4);
let all = RgbMatcher::new(0).find_all(&frame, &tpl, frame.full_rect(), 0);
let want: Vec<(i32, i32)> = (0..6)
.flat_map(|y| (0..6).map(move |x| (x * 4, y * 4)))
.collect();
let got: Vec<(i32, i32)> = all.iter().map(|m| (m.x, m.y)).collect();
assert_eq!(got, want);
}
#[test]
fn dense_lower_half_keeps_every_match_across_block_skips() {
let (w, h) = (40usize, 512usize);
let mut px = gradient(w, h);
for y in 203..h {
for x in 0..w {
let i = (y * w + x) * 4;
px[i] = 9;
px[i + 1] = 9;
px[i + 2] = 9;
px[i + 3] = 255;
}
}
let frame = Frame::rgba8(w, h, px);
let tpl = Template::from_rgb(vec![9u8; 8 * 16 * 3], 8, 16);
let all = RgbMatcher::new(0).find_all(&frame, &tpl, frame.full_rect(), 0);
let want: Vec<(i32, i32)> = (0..19i32)
.flat_map(|k| (0..5i32).map(move |j| (j * 8, 203 + k * 16)))
.collect();
let got: Vec<(i32, i32)> = all.iter().map(|m| (m.x, m.y)).collect();
assert_eq!(got, want, "下半密画面应给出 19 行 x 5 列 = 95 个命中");
}
#[test]
fn first_hit_is_deterministic_but_not_unique() {
let (w, h) = (600usize, 400usize);
let mut px = vec![0u8; w * h * 4];
let blobs = [
(300usize, 20usize, (200u8, 100u8, 50u8)),
(100usize, 200usize, (215u8, 110u8, 60u8)),
];
for (x0, y0, c) in blobs {
for yy in 0..20usize {
for xx in 0..40usize {
let i = ((y0 + yy) * w + (x0 + xx)) * 4;
px[i] = c.0;
px[i + 1] = c.1;
px[i + 2] = c.2;
px[i + 3] = 255;
}
}
}
let frame = Frame::rgba8(w, h, px);
let mut tpl_rgb = Vec::with_capacity(40 * 20 * 3);
for _ in 0..40 * 20 {
tpl_rgb.extend_from_slice(&[200, 100, 50]);
}
let tpl = Template::from_rgb(tpl_rgb, 40, 20);
let m = RgbMatcher::new(30);
let hit = m.find(&frame, &tpl).expect("应命中");
assert_eq!((hit.x, hit.y), (300, 20), "find 给的是 (y,x) 序第一个");
let two = m.find_all(&frame, &tpl, frame.full_rect(), 2);
assert_eq!(two.len(), 2, "两处都在容差内,find_all(max=2) 应当都看到");
assert_eq!((two[1].x, two[1].y), (100, 200));
let scoped = m
.find_in(&frame, &tpl, Rect::new(0, 150, w, 250))
.expect("区域内应命中");
assert_eq!((scoped.x, scoped.y), (100, 200));
}
#[test]
fn find_all_stops_scanning_once_max_is_met() {
let (w, h) = (256usize, 128usize);
let px = vec![70u8; w * h * 4];
let frame = Frame::bgra8(w, h, px);
let t = Template::from_rgb(vec![70u8; 8 * 8 * 3], 8, 8);
let m = RgbMatcher::new(0);
let head = m.find_all(&frame, &t, frame.full_rect(), 2);
assert_eq!(
head.iter().map(|k| (k.x, k.y)).collect::<Vec<_>>(),
vec![(0, 0), (8, 0)],
"前两个命中应是首行相隔一个模板宽的两处"
);
let all = m.find_all(&frame, &t, frame.full_rect(), 0);
assert!(all.len() > head.len(), "不限 max 时应给出全部命中");
assert_eq!(
&all[..head.len()],
&head[..],
"max 只是截断前缀,结果顺序不变"
);
}
#[test]
fn region_limits_search_and_returns_absolute_coords() {
let (w, h) = (80usize, 60usize);
let mut px = gradient(w, h);
let tpl = paste_block(&mut px, w, h, 50, 40, 8, [200, 100, 10]);
let frame = Frame::rgba8(w, h, px);
let t = Template::from_rgb(tpl, 8, 8);
let m = RgbMatcher::new(0);
let miss = m.find_in(&frame, &t, Rect::new(0, 0, 40, 60));
assert!(miss.is_none());
let hit = m
.find_in(&frame, &t, Rect::new(40, 30, 40, 30))
.expect("区域内应命中");
assert_eq!((hit.x, hit.y), (50, 40));
}
#[test]
fn find_many_on_one_frame() {
let (w, h) = (96usize, 48usize);
let mut px = gradient(w, h);
let tpl_a = paste_block(&mut px, w, h, 10, 10, 6, [250, 10, 10]);
let tpl_b = paste_block(&mut px, w, h, 60, 30, 6, [10, 10, 250]);
let frame = Frame::rgba8(w, h, px);
let ta = Template::from_rgb(tpl_a, 6, 6);
let tb = Template::from_rgb(tpl_b, 6, 6);
let m = RgbMatcher::new(0);
let ra = m.find(&frame, &ta).expect("a");
let rb = m.find(&frame, &tb).expect("b");
assert_eq!((ra.x, ra.y), (10, 10));
assert_eq!((rb.x, rb.y), (60, 30));
}
fn tolerance_case(delta: i32) -> (Frame, Template) {
let (w, h) = (48usize, 48usize);
let mut px = vec![0u8; w * h * 4];
for y in 9..9 + 8 {
for x in 12..12 + 8 {
let i = (y * w + x) * 4;
px[i] = 200;
px[i + 1] = 100;
px[i + 2] = 50;
px[i + 3] = 255;
}
}
let bright = |v: i32| (v + delta).clamp(0, 255) as u8;
let mut tpl = Vec::with_capacity(8 * 8 * 3);
for _ in 0..8 * 8 {
tpl.extend_from_slice(&[bright(200), bright(100), bright(50)]);
}
(Frame::rgba8(w, h, px), Template::from_rgb(tpl, 8, 8))
}
#[test]
fn tolerance_accepts_small_channel_diff() {
let (frame, t) = tolerance_case(4);
let m = RgbMatcher::new(5)
.find(&frame, &t)
.expect("容差 5 应吸收 +4 偏差");
assert_eq!((m.x, m.y), (12, 9));
assert!(
RgbMatcher::new(0).find(&frame, &t).is_none(),
"tolerance=0 不应命中带偏差的模板"
);
}
#[test]
fn tolerance_rejects_large_channel_diff() {
let (frame, t) = tolerance_case(20);
assert!(
RgbMatcher::new(5).find(&frame, &t).is_none(),
"偏差 20 超出容差 5,不应命中"
);
}
#[test]
fn mask_skips_transparent_pixels() {
let (w, h) = (32usize, 32usize);
let mut px = vec![0u8; w * h * 4];
for x in 0..8 {
let y = 10;
let i = (y * w + 10 + x) * 4;
if x < 4 {
px[i] = 255; } else {
px[i + 2] = 255; }
px[i + 3] = 255;
}
let mut tpl_rgb = Vec::with_capacity(8 * 3);
for _ in 0..8 {
tpl_rgb.extend_from_slice(&[255, 0, 0]);
}
let mask: Vec<bool> = (0..8).map(|i| i < 4).collect(); let t = Template::from_rgb(tpl_rgb, 8, 1).with_mask(mask);
let frame = Frame::rgba8(w, h, px);
let m = RgbMatcher::new(0)
.find(&frame, &t)
.expect("掩码后应命中(被掩像素不比较)");
assert_eq!((m.x, m.y), (10, 10));
}
#[test]
fn mask_all_false_matches_first_position() {
let (w, h) = (16usize, 16usize);
let frame = Frame::rgba8(w, h, vec![0u8; w * h * 4]);
let t = Template::from_rgb(vec![99u8; 4 * 4 * 3], 4, 4).with_mask(vec![false; 4 * 4]);
let m = RgbMatcher::new(0).find(&frame, &t);
assert!(m.is_some(), "全掩模板应给出命中(语义:不需要比较任何像素)");
}
}