use crate::capture::{Capture, XCapCapture};
use crate::color::{self, ColorBlob, ColorSpec};
use crate::frame::{Frame, Rect};
use crate::matcher::{Match, Matcher, RgbMatcher};
use crate::template::Template;
use crate::Result;
use std::time::{Duration, Instant};
#[cfg(all(windows, feature = "capture-dxgi"))]
use crate::Error;
#[cfg(feature = "match-corr")]
use crate::matcher_corr::{CorrConfig, CorrMatcher};
pub enum CaptureKind {
Monitor,
#[cfg(all(windows, feature = "capture-gdi"))]
Gdi,
#[cfg(all(windows, feature = "capture-dxgi"))]
Dxgi,
#[cfg(all(windows, feature = "capture-window"))]
Window(crate::capture_window::WindowHandle),
#[cfg(all(windows, any(feature = "capture-gdi", feature = "capture-dxgi")))]
Auto,
}
pub enum MatchKind {
Rgb { tolerance: i32 },
#[cfg(feature = "match-corr")]
Corr,
#[cfg(feature = "match-corr")]
CorrWith(CorrConfig),
}
pub struct Finder {
capture: Box<dyn Capture>,
matcher: Box<dyn Matcher>,
region: Option<Rect>,
frame: Frame,
cache_key: Option<u64>,
cache_result: Option<Option<Match>>,
}
impl Finder {
pub fn builder() -> FinderBuilder {
FinderBuilder {
capture: None,
matcher: None,
region: None,
}
}
pub fn new(capture: Box<dyn Capture>, matcher: Box<dyn Matcher>) -> Self {
Finder {
capture,
matcher,
region: None,
frame: Frame::bgra8(0, 0, Vec::new()),
cache_key: None,
cache_result: None,
}
}
pub fn find_on_screen(&mut self, tpl: &Template) -> Result<Option<Match>> {
let _t0 = px_timer!();
let (changed, origin) = self.grab_scoped()?;
let key = self.key_of(tpl);
if !changed && self.cache_key == Some(key) {
if let Some(prev) = self.cache_result {
px_trace!(
op = "find_on_screen",
backend = self.capture.backend(),
cache_hit = true,
hit = prev.is_some(),
elapsed_us = _t0.map(|t| t.elapsed().as_micros() as u64).unwrap_or(0),
);
return Ok(prev);
}
}
let m = self.search(tpl, origin);
self.cache_key = Some(key);
self.cache_result = Some(m);
px_trace!(
op = "find_on_screen",
backend = self.capture.backend(),
cache_hit = false,
changed,
frame = format!("{}x{}", self.frame.width, self.frame.height),
tpl_key = format!("{:016x}", key),
hit = m.is_some(),
elapsed_us = _t0.map(|t| t.elapsed().as_micros() as u64).unwrap_or(0),
);
Ok(m)
}
pub fn find_color_on_screen(
&mut self,
spec: &ColorSpec,
min_area: usize,
) -> Result<Vec<ColorBlob>> {
let _t0 = px_timer!();
let (_, origin) = self.grab_scoped()?;
let region = self.search_rect(origin);
let mut blobs = color::find_blobs(&self.frame, spec, region, min_area);
if origin != (0, 0) {
for b in &mut blobs {
b.bounds.x += origin.0 as usize;
b.bounds.y += origin.1 as usize;
}
}
px_trace!(
op = "find_color_on_screen",
backend = self.capture.backend(),
color = format!("{:?}±{}", spec.rgb, spec.tolerance),
region = format!(
"({},{},{}x{})",
region.x, region.y, region.width, region.height
),
blobs = blobs.len(),
elapsed_us = _t0.map(|t| t.elapsed().as_micros() as u64).unwrap_or(0),
);
Ok(blobs)
}
pub fn find_until(
&mut self,
tpl: &Template,
timeout: Duration,
interval: Duration,
) -> Result<Option<Match>> {
let deadline = Instant::now() + timeout;
loop {
if let Some(m) = self.find_on_screen(tpl)? {
px_trace!(op = "find_until", result = "hit", timeout = ?timeout);
return Ok(Some(m));
}
let now = Instant::now();
if now >= deadline {
px_trace!(op = "find_until", result = "timeout", timeout = ?timeout);
return Ok(None);
}
std::thread::sleep(interval.min(deadline - now));
}
}
pub fn wait_gone(
&mut self,
tpl: &Template,
timeout: Duration,
interval: Duration,
) -> Result<bool> {
let deadline = Instant::now() + timeout;
loop {
if self.find_on_screen(tpl)?.is_none() {
px_trace!(op = "wait_gone", result = "gone", timeout = ?timeout);
return Ok(true);
}
let now = Instant::now();
if now >= deadline {
px_trace!(op = "wait_gone", result = "still_present", timeout = ?timeout);
return Ok(false);
}
std::thread::sleep(interval.min(deadline - now));
}
}
pub fn find_all_on_screen(&mut self, tpl: &Template, max: usize) -> Result<Vec<Match>> {
let (_, origin) = self.grab_scoped()?;
let region = self.search_rect(origin);
let mut ms = self.matcher.find_all(&self.frame, tpl, region, max);
if origin != (0, 0) {
for m in &mut ms {
m.x += origin.0;
m.y += origin.1;
}
}
Ok(ms)
}
pub fn find_many_on_screen(&mut self, tpls: &[&Template]) -> Result<Vec<Option<Match>>> {
let (_, origin) = self.grab_scoped()?;
Ok(tpls.iter().map(|t| self.search(t, origin)).collect())
}
pub fn find_in_frame(&self, frame: &Frame, tpl: &Template) -> Option<Match> {
match self.region {
Some(r) => self.matcher.find_in(frame, tpl, r),
None => self.matcher.find(frame, tpl),
}
}
pub fn region(&self) -> Option<Rect> {
self.region
}
fn grab_scoped(&mut self) -> Result<(bool, (i32, i32))> {
if let Some(r) = self.region {
if let Some(f) = self.capture.grab_region(r)? {
self.frame = f;
return Ok((true, (r.x as i32, r.y as i32)));
}
}
Ok((self.capture.grab_into(&mut self.frame)?, (0, 0)))
}
fn search_rect(&self, origin: (i32, i32)) -> Rect {
if origin == (0, 0) {
self.region.unwrap_or_else(|| self.frame.full_rect())
} else {
self.frame.full_rect()
}
}
fn search(&self, tpl: &Template, origin: (i32, i32)) -> Option<Match> {
let mut m = if origin != (0, 0) {
self.matcher.find(&self.frame, tpl)?
} else {
match self.region {
Some(r) => self.matcher.find_in(&self.frame, tpl, r)?,
None => self.matcher.find(&self.frame, tpl)?,
}
};
m.x += origin.0;
m.y += origin.1;
Some(m)
}
pub fn set_region(&mut self, region: Option<Rect>) {
self.region = region;
self.cache_key = None;
self.cache_result = None;
}
fn key_of(&self, tpl: &Template) -> u64 {
let rk = match self.region {
None => 0u64,
Some(r) => {
(r.x as u64).wrapping_mul(1000003)
^ (r.y as u64).wrapping_mul(7349287)
^ (r.width as u64).wrapping_mul(911)
^ (r.height as u64)
}
};
tpl.content_key() ^ rk.rotate_left(32)
}
}
pub struct FinderBuilder {
capture: Option<CaptureKind>,
matcher: Option<MatchKind>,
region: Option<Rect>,
}
impl FinderBuilder {
pub fn capture(mut self, k: CaptureKind) -> Self {
self.capture = Some(k);
self
}
pub fn matcher(mut self, k: MatchKind) -> Self {
self.matcher = Some(k);
self
}
pub fn region(mut self, r: impl Into<Rect>) -> Self {
self.region = Some(r.into());
self
}
pub fn build(self) -> Result<Finder> {
let capture: Box<dyn Capture> = match self.capture.unwrap_or(CaptureKind::Monitor) {
CaptureKind::Monitor => Box::new(XCapCapture::primary()?),
#[cfg(all(windows, feature = "capture-gdi"))]
CaptureKind::Gdi => Box::new(crate::capture_gdi::GdiCapture::new_primary()),
#[cfg(all(windows, feature = "capture-dxgi"))]
CaptureKind::Dxgi => Box::new(
crate::capture_dxgi::DxgiCapture::new_primary()
.ok_or_else(|| Error::capture("DXGI desktop duplication unavailable"))?,
),
#[cfg(all(windows, feature = "capture-window"))]
CaptureKind::Window(h) => Box::new(crate::capture_window::WindowCapture::new(h)),
#[cfg(all(windows, any(feature = "capture-gdi", feature = "capture-dxgi")))]
CaptureKind::Auto => auto_capture()?,
};
let matcher: Box<dyn Matcher> =
match self.matcher.unwrap_or(MatchKind::Rgb { tolerance: 25 }) {
MatchKind::Rgb { tolerance } => Box::new(RgbMatcher::new(tolerance)),
#[cfg(feature = "match-corr")]
MatchKind::Corr => Box::new(CorrMatcher::new()),
#[cfg(feature = "match-corr")]
MatchKind::CorrWith(cfg) => Box::new(CorrMatcher::with_config(cfg)),
};
let mut f = Finder::new(capture, matcher);
f.region = self.region;
Ok(f)
}
}
#[cfg(all(windows, any(feature = "capture-gdi", feature = "capture-dxgi")))]
fn auto_capture() -> Result<Box<dyn Capture>> {
#[cfg(feature = "capture-dxgi")]
if let Some(c) = crate::capture_dxgi::DxgiCapture::new_primary() {
return Ok(Box::new(c));
}
#[cfg(feature = "capture-gdi")]
{
return Ok(Box::new(crate::capture_gdi::GdiCapture::new_primary()));
}
#[allow(unreachable_code)]
Ok(Box::new(XCapCapture::primary()?))
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::rc::Rc;
const W: usize = 32;
const H: usize = 32;
fn frame(with_target: bool) -> Frame {
let mut px = vec![0u8; W * H * 4];
if with_target {
for y in 10..18 {
for x in 10..18 {
let i = (y * W + x) * 4;
px[i] = 255;
px[i + 1] = 0;
px[i + 2] = 0;
px[i + 3] = 255;
}
}
}
Frame::rgba8(W, H, px)
}
struct SeqCapture {
present: Vec<bool>,
grabs: usize,
}
impl Capture for SeqCapture {
fn grab(&mut self) -> Result<Frame> {
let i = self.grabs.min(self.present.len() - 1);
self.grabs += 1;
Ok(frame(self.present[i]))
}
fn backend(&self) -> &'static str {
"mock"
}
}
fn target_tpl() -> Template {
Template::from_rgb([255, 0, 0].repeat(8 * 8), 8, 8)
}
fn finder_with(present: Vec<bool>) -> Finder {
Finder::new(
Box::new(SeqCapture { present, grabs: 0 }),
Box::new(RgbMatcher::new(0)),
)
}
#[test]
fn find_until_hits_after_a_few_misses() {
let mut f = finder_with(vec![false, false, true]);
let m = f
.find_until(
&target_tpl(),
Duration::from_secs(5),
Duration::from_millis(2),
)
.expect("mock 不会出错")
.expect("第 3 帧应命中");
assert_eq!((m.x, m.y), (10, 10));
}
#[test]
fn find_until_times_out() {
let mut f = finder_with(vec![false]);
let m = f
.find_until(
&target_tpl(),
Duration::from_millis(30),
Duration::from_millis(5),
)
.expect("mock 不会出错");
assert!(m.is_none(), "一直找不到应在超时后返回 None");
}
#[test]
fn wait_gone_returns_true_and_false() {
let mut f = finder_with(vec![true, true, false]);
assert!(f
.wait_gone(
&target_tpl(),
Duration::from_secs(5),
Duration::from_millis(2)
)
.unwrap());
let mut f2 = finder_with(vec![true]);
assert!(!f2
.wait_gone(
&target_tpl(),
Duration::from_millis(30),
Duration::from_millis(5)
)
.unwrap());
}
#[test]
fn find_color_on_screen_high_level_entry() {
let mut f = finder_with(vec![true]);
let blobs = f
.find_color_on_screen(&ColorSpec::new(255, 0, 0, 10), 32)
.unwrap();
assert_eq!(blobs.len(), 1);
assert_eq!(blobs[0].bounds, Rect::new(10, 10, 8, 8));
assert_eq!(blobs[0].area, 64);
f.set_region(Some(Rect::new(0, 20, 32, 12)));
assert!(f
.find_color_on_screen(&ColorSpec::new(255, 0, 0, 10), 1)
.unwrap()
.is_empty());
}
fn region_frame() -> Frame {
let (w, h) = (24usize, 24usize);
let mut px = vec![0u8; w * h * 4];
for y in 5..13 {
for x in 4..12 {
let i = (y * w + x) * 4;
px[i] = 255;
px[i + 3] = 255;
}
}
Frame::rgba8(w, h, px)
}
struct RegionCap {
region_grabs: Rc<Cell<usize>>,
full_grabs: Rc<Cell<usize>>,
}
impl Capture for RegionCap {
fn grab(&mut self) -> Result<Frame> {
self.full_grabs.set(self.full_grabs.get() + 1);
Ok(frame(false))
}
fn grab_region(&mut self, r: Rect) -> Result<Option<Frame>> {
assert_eq!(
(r.x, r.y, r.width, r.height),
(20, 16, 24, 24),
"应收到屏幕绝对坐标区域"
);
self.region_grabs.set(self.region_grabs.get() + 1);
Ok(Some(region_frame()))
}
fn backend(&self) -> &'static str {
"mock-region"
}
}
fn region_finder() -> (Finder, Rc<Cell<usize>>, Rc<Cell<usize>>) {
let (rg, fg) = (Rc::new(Cell::new(0)), Rc::new(Cell::new(0)));
let mut f = Finder::new(
Box::new(RegionCap {
region_grabs: rg.clone(),
full_grabs: fg.clone(),
}),
Box::new(RgbMatcher::new(0)),
);
f.set_region(Some(Rect::new(20, 16, 24, 24)));
(f, rg, fg)
}
#[test]
fn region_prefers_backend_region_grab() {
let (mut f, rg, fg) = region_finder();
let m = f
.find_on_screen(&target_tpl())
.unwrap()
.expect("区域内应命中");
assert_eq!((m.x, m.y), (24, 21), "应返回屏幕绝对坐标");
assert_eq!(rg.get(), 1);
assert_eq!(fg.get(), 0, "能只截区域时不应再截全屏");
}
#[test]
fn region_grab_shifts_all_and_color_results() {
let (mut f, rg, _) = region_finder();
let all = f.find_all_on_screen(&target_tpl(), 0).unwrap();
assert_eq!(all.len(), 1);
assert_eq!((all[0].x, all[0].y), (24, 21));
let blobs = f
.find_color_on_screen(&ColorSpec::new(255, 0, 0, 10), 32)
.unwrap();
assert_eq!(blobs.len(), 1);
assert_eq!(blobs[0].bounds, Rect::new(24, 21, 8, 8));
assert_eq!(blobs[0].area, 64);
assert_eq!(rg.get(), 2, "两次入口都应走区域路径");
}
struct SpyMatcher {
finds: Rc<Cell<usize>>,
find_ins: Rc<Cell<usize>>,
}
impl Matcher for SpyMatcher {
fn find(&self, _frame: &Frame, _tpl: &Template) -> Option<Match> {
self.finds.set(self.finds.get() + 1);
Some(Match {
x: 1,
y: 2,
score: 1.0,
})
}
fn find_in(&self, _frame: &Frame, _tpl: &Template, _region: Rect) -> Option<Match> {
self.find_ins.set(self.find_ins.get() + 1);
Some(Match {
x: 1,
y: 2,
score: 1.0,
})
}
}
fn spy() -> (SpyMatcher, Rc<Cell<usize>>, Rc<Cell<usize>>) {
let (f, i) = (Rc::new(Cell::new(0)), Rc::new(Cell::new(0)));
(
SpyMatcher {
finds: f.clone(),
find_ins: i.clone(),
},
f,
i,
)
}
#[test]
fn no_region_uses_find_not_find_in() {
let (m, finds, find_ins) = spy();
let mut f = Finder::new(
Box::new(SeqCapture {
present: vec![true],
grabs: 0,
}),
Box::new(m),
);
assert!(f.find_on_screen(&target_tpl()).unwrap().is_some());
assert_eq!((finds.get(), find_ins.get()), (1, 0));
}
#[test]
fn region_without_backend_support_uses_find_in() {
let (m, finds, find_ins) = spy();
let mut f = Finder::new(
Box::new(SeqCapture {
present: vec![true],
grabs: 0,
}),
Box::new(m),
);
f.set_region(Some(Rect::new(2, 3, 8, 8)));
assert!(f.find_on_screen(&target_tpl()).unwrap().is_some());
assert_eq!((finds.get(), find_ins.get()), (0, 1));
}
#[test]
fn region_grab_searches_whole_frame_with_find() {
let (m, finds, find_ins) = spy();
let mut f = Finder::new(
Box::new(RegionCap {
region_grabs: Rc::new(Cell::new(0)),
full_grabs: Rc::new(Cell::new(0)),
}),
Box::new(m),
);
f.set_region(Some(Rect::new(20, 16, 24, 24)));
let hit = f.find_on_screen(&target_tpl()).unwrap().unwrap();
assert_eq!((hit.x, hit.y), (21, 18));
assert_eq!((finds.get(), find_ins.get()), (1, 0));
}
}