pixhunt 0.8.2

Fast screen finding: template matching (RGB tolerance / ZNCC), color-blob search and wait/poll APIs, with pluggable capture backends (cross-platform xcap, plus Windows GDI / DXGI / PrintWindow window capture).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# pixhunt


[![CI](https://github.com/xiaoheliu1/pixhunt/actions/workflows/ci.yml/badge.svg)](https://github.com/xiaoheliu1/pixhunt/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/pixhunt.svg)](https://crates.io/crates/pixhunt)
[![license](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#许可)

> 快速的**屏幕找图**库:截一帧屏幕,在其中定位一张小图(模板)的坐标。
> 纯 Rust,无需 OpenCV。适合自动化测试、脚本辅助、UI 定位等。

## 它做什么
给一张小图 `template.png`,`pixhunt` 告诉你在当前屏幕的哪个位置(用 `Corr` 时还给一个
相似度分数):

```rust
use pixhunt::{Finder, CaptureKind, MatchKind, Template};

let tpl = Template::load("template.png")?;
let mut finder = Finder::builder()
    .capture(CaptureKind::Monitor)             // 截图后端
    .matcher(MatchKind::Rgb { tolerance: 25 }) // 找法
    .build()?;

if let Some(m) = finder.find_on_screen(&tpl)? {
    // 注意:`Rgb` 是"容差内逐像素全对才算命中"的判定,它的 m.score 恒为 1.0;
    // 想要真正的相似度分数(0.0..=1.0)请用 MatchKind::Corr。
    // 另:`Some` 只说明"至少有一处"。命中不止一处时它给的是最上、最左的那个,
    // 不告诉你还有第二个 —— 要判唯一用 finder.find_all_on_screen(&tpl, 2)。
    let (cx, cy) = m.center(&tpl); // 要点击的是中心,不是左上角
    println!("命中 @ ({}, {}),中心 ({}, {})", m.x, m.y, cx, cy);
}
```

## 文档
- **[使用说明书(从零开始)]docs/GUIDE.md** —— 面向没写过 Rust 的读者:环境准备、最小语法课、
  核心概念、**全部对外 API 逐个详解**(真实签名 + 参数 + 返回 + 坑)、任务配方、性能调参、
  报错排查、API 速查表。
- `cargo doc --no-deps --open` —— 本地生成的 API 参考(以源码注释为准)。
- `examples/` 三个可运行示例的用法见说明书 12.6。

## 设计:两个可插拔"插座"
- **`Capture`** 负责"怎么拿到画面"。
- **`Matcher`** 负责"怎么找模板"。

上层只跟"插座"打交道,新增后端/算法不改上层代码。

## 功能开关 (Cargo features)
| feature | 提供 | 平台 |
| --- | --- | --- |
| (默认) | `XCapCapture`(基于 xcap)+ `RgbMatcher` | 跨平台 |
| `capture-gdi` | `GdiCapture`(复用 DC + BitBlt,输出 BGRA) | 仅 Windows |
| `capture-dxgi` | `DxgiCapture`(桌面复制,GPU 取帧) | 仅 Windows |
| `capture-window` | `WindowCapture`(PrintWindow 截单个窗口客户区,遮挡也可截)+ `CaptureKind::WindowByTitle` | 仅 Windows |
| `match-corr` | `CorrMatcher`(corrmatch 的 ZNCC,灰度;调参见 `CorrConfig`) | 跨平台 |
| `parallel` | `RgbMatcher` 按行并行(find / find_all,rayon);`CorrMatcher` 分层搜索。**例外**:平坦画面的 `find_all` 里并行帮不上忙,库里会自己退回串行流式(见 v0.8.2 变更) | 跨平台 |
| `tracing` | trace 级诊断事件(截图耗时、缓存跳过、命中与否);关闭零开销 | 跨平台 |

```toml
[dependencies]
pixhunt = { version = "0.8", features = ["capture-dxgi", "capture-gdi", "match-corr", "parallel"] }
```

启用后 `CaptureKind` 多出 `Gdi` / `Dxgi` / `Auto`(`Auto` 依次试 DXGI → GDI → xcap),
`MatchKind` 多出 `Corr`:

```rust
let mut finder = Finder::builder()
    .capture(CaptureKind::Auto)     // 自动选最快可用的截图后端
    .matcher(MatchKind::Rgb { tolerance: 25 })
    .build()?;
```

## 后端与算法怎么选
| 截图后端 | 特点 | 成本(1920x1200 实测) |
| --- | --- | --- |
| `Monitor`(xcap) | 跨平台保底;唯一支持**区域直抓**的后端 | 全屏 ~34ms / 400x300 区域 ~16.5ms |
| `Gdi` | 复用 DC + BitBlt + 免翻转 | 全屏 ~32ms |
| `Dxgi` | 桌面复制,GPU 取帧 + 静态帧跳过 | 全屏 ~16ms(无新帧时更低) |

(以上为同一台 1920x1200 桌面、`--release`、连续取帧的中位数;`xcap` / `Gdi` 走 GDI
路径,量级接近,`xcap` 的价值在于跨平台 + 区域直抓。)

| 匹配算法 | 适合 | 特点 |
| --- | --- | --- |
| `Rgb` | 屏幕内容与模板几乎一致、追求速度 | 不转灰度、锚点 + 逐像素早失败,通道序自适应(RGBA/BGRA) |
| `Corr` | 有光照/轻微缩放变化、追求稳 | 灰度 ZNCC + 金字塔,只做平移(不搜旋转),较慢但鲁棒 |

## 更多用法 (v0.2)
```rust
use pixhunt::{Finder, CaptureKind, MatchKind, Template, Rect};

// 1) 限定区域:只在该矩形内找,返回的仍是屏幕绝对坐标
let finder = Finder::builder()
    .capture(CaptureKind::Auto)
    .matcher(MatchKind::Rgb { tolerance: 25 })
    .region(Rect::new(100, 80, 640, 480))
    .build()?;

// 2) 多结果:找全部不重叠匹配(重叠自动去重),按 (y, x) 升序;max=0 表示不限。
//    ⚠️ 平坦画面(纯色背景 + 纯色小模板)上"每个位置都是命中",别随手写 0:
//    1920x1200 纯色帧 + 8x8 纯色模板实测返回 36000 个。给个真实上限(比如 20)能提前停止扫描。
let all = finder.find_all_on_screen(&tpl, 20)?;
for m in &all {
    let (cx, cy) = m.center(&tpl); // 多结果要点中心时用它,不用自己折算
    println!("命中 @ ({}, {}) → 点击 ({cx}, {cy})", m.x, m.y);
}

// 3) 批量:只截一屏,一次匹配多张模板(省掉重复截图)
let hits = finder.find_many_on_screen(&[&tpl_a, &tpl_b, &tpl_c])?;
```

匹配器层面也可直接调用 trait 方法:
[`Matcher::find`] 整帧单个、[`Matcher::find_in`] 区域内单个、
[`Matcher::find_all`] 区域内多个。开启 `parallel` 后,`RgbMatcher` 用 rayon 把
扫描按行分到多核,全屏 `find` / `find_all` 在大分辨率下更快(结果与串行完全一致)。
⚠️ 唯一例外:**平坦**(到处都匹配)画面上的 `find_all` —— 重叠抑制必须按 `(y, x)` 顺序
流式进行,并行反而会慢几个数量级,库里会自己退回串行(见 v0.8.2 变更)。

## 等待与窗口 (v0.3)
```rust
use std::time::Duration;
use pixhunt::{Finder, CaptureKind, MatchKind, Template, WindowCapture};

// 4) 轮询等待:等按钮出现(命中即返回,超时返回 None);等遮罩消失同理。
//    timeout 可以传 Duration::MAX(永不超时,不会 panic)。
//    静态画面会被跳过搜索:DXGI 后端自己上报,或设了 region 时由库里逐字节比对区域帧。
let m = finder.find_until(&tpl, Duration::from_secs(10), Duration::from_millis(50))?;
let gone = finder.wait_gone(&loading_tpl, Duration::from_secs(30), Duration::from_millis(100))?;

// 5) 窗口级截图(Windows, feature `capture-window`):PrintWindow 渲染客户区,
//    窗口被遮挡也能截;返回坐标为窗口相对。也可用 CaptureKind::Window(hwnd)。
let cap = WindowCapture::from_title("无标题 - 记事本")?;
let mut finder = Finder::new(Box::new(cap), Box::new(pixhunt::RgbMatcher::new(25)));
let m = finder.find_on_screen(&tpl)?; // 相对该窗口客户区的坐标
```

## 与键鼠操作的关系(生态分工)
pixhunt 专注做**眼睛**(截图 + 定位),不做"手"——点击/输入交给
[enigo](https://crates.io/crates/enigo)、[rdev](https://crates.io/crates/rdev)
等成熟跨平台库,坐标就是两者的接口:

```rust,ignore
let m = finder.find_until(&button, Duration::from_secs(10), Duration::from_millis(80))?.unwrap();
let (cx, cy) = m.center(&button); // 匹配坐标是左上角,center() 折算成点击的中心
enigo.move_mouse(cx, cy, Coordinate::Abs)?;
enigo.button(Button::Left, Direction::Click)?;
```

完整可运行示例见 `examples/wait_and_click.rs`(Windows,`--features capture-gdi`)。

## 颜色搜索与诊断 (v0.4)
```rust
use pixhunt::{ColorSpec, Finder, CaptureKind, MatchKind};

// 6) 颜色范围搜索:没有模板、只有"大概这个颜色"的场景(血条/状态灯/高亮区)。
//    返回 8-连通色块(包围盒+面积),按 (y,x) 排序,min_area 过滤碎点。
let red_lights = finder.find_color_on_screen(&ColorSpec::new(255, 0, 0, 40), 50)?;
for b in red_lights {
    println!("色块 {:?} 面积 {} 中心 {:?}", b.bounds, b.area, b.center());
}
// 也可离线对任意帧用:pixhunt::color::find_blobs(&frame, &spec, region, min_area)
```

开 `tracing` feature 后,pixhunt 在 `pixhunt` target 下输出 trace 级事件:
后端名、截图耗时、静态帧缓存是否命中、扫描结果等——"为什么找不到"先看日志:

```rust,ignore
// 用户侧接一个 subscriber 即可看到
tracing_subscriber::fmt().with_max_level(tracing::Level::TRACE).init();
finder.find_on_screen(&tpl)?; // trace: op="find_on_screen" backend="dxgi" changed=false cache_hit=true ...
```

## 性能(参考)

> ⚠️ **下面所有数字都是 `--release` 构建实测。** debug 构建(`cargo run` / `cargo build`
> 默认)慢一个数量级:同一台机器、同一份 1920x1080 合成场景,全屏扫一遍
> debug ~60ms / release ~4ms。别拿 debug 的耗时评价这个库。
>
> 另:`Cargo.toml` 里的 `[profile.release]`(`lto`、`codegen-units = 1`)**只对在本仓库里
> 跑示例/基准生效**。你依赖 pixhunt 时,优化级别由你自己项目的 `[profile.release]` 决定
> ——想吃到同样性能,请在你项目根 `Cargo.toml` 里也写上这三行(见 `docs/GUIDE.md` 10.4)。

同一台 8 逻辑核机器、`--release` 实测。截图为 1920x1200 真实桌面的连续取帧中位数,
纯匹配为 `cargo bench --bench match`(1920x1080 合成帧;单目标用 64px 模板,`find_all`
用 48px):

| 组合 | 纯匹配 | 截图 | 端到端(截图+匹配) |
| --- | --- | --- | --- |
| RGB + `Monitor`(xcap) | 串行 ~2.8ms / `parallel` ~1.8ms | ~34ms | ~36ms |
| RGB + `Gdi` | 同上 | ~32ms | ~34ms |
| RGB + `Dxgi` | 同上 | ~16ms(静态桌面更低) | ~18ms |

要点:找图瓶颈主要在**截图**,换更快的后端收益最大;`RgbMatcher` 本身已是毫秒级。
限定区域 + `Monitor` 会走**区域直抓**(400x300 实测截图 ~16.5ms,与"截全屏再裁剪"
逐字节一致),同一模板端到端从 ~47ms 降到 ~20ms。

掩码与差分另有两个实测数:`rgb_find_masked_1080p`(64px 模板掩掉 6% 像素)~2.9ms,与同尺寸
无掩码的 ~2.8ms 只在抖动内 —— 掩码买的是正确性、不是速度(旧文档写的"慢 18%"本机复现
不出来,已删);`diff_since_last_400x300_on_1080p` ~3.4ms
(基线缓冲复用之后;复用前 ~5.6ms)。

`Corr`(ZNCC)不受截图后端制约,成本在搜索本身(同样 `cargo bench --features
match-corr[,parallel] --bench match`):

| 组合 | 热路径(模板已缓存) | 冷启动(含模板编译) |
| --- | --- | --- |
| `Corr` | ~13.4ms | ~13.7ms |
| `Corr` + `parallel`(8 逻辑核) | ~6.4ms | ~6.5ms |

模板只做平移匹配(`compile_unrotated`),不建角度模板库,因此冷启动≈热路径;
开 `parallel` 后 ZNCC 分层并行,约 2x(结果仍确定性)。

## ZNCC 调参 (v0.5)
`match-corr` 下用 `MatchKind::CorrWith(CorrConfig { .. })` 调搜索参数(只想用默认值
就继续写 `MatchKind::Corr`):

```rust,ignore
use pixhunt::{CorrConfig, Finder, CaptureKind, MatchKind, Template};

// 已知目标只在附近小范围移动:砍深层金字塔 + 缩小精修 ROI 换低延迟,
// 并用 min_score 把"长得像但不够像"的结果当未命中。
let mut finder = Finder::builder()
    .capture(CaptureKind::Auto)
    .matcher(MatchKind::CorrWith(CorrConfig {
        max_image_levels: 3,
        roi_radius: 4,
        min_score: 0.7,
        ..CorrConfig::default()
    }))
    .build()?;
let m = finder.find_on_screen(&Template::load("btn.png")?)?;
```

| 字段 | 默认 | 调小的收益 / 调大的收益 |
| --- | --- | --- |
| `max_image_levels` | 6 | 粗筛更便宜 / 大位移、轻微缩放更稳 |
| `beam_width` | 8 | 每层候选更少更快 / 遮挡、伪峰多时更稳 |
| `roi_radius` | 8 | 精修扫描范围更小更快 / 容忍层间位移误差更大 |
| `min_score` | 不过滤 | 误命中更少(注意会漏判) |
| `parallel` | 跟随 `parallel` feature | — |

要点:
- 非法值(0、NaN、±inf)会被**夹到安全下限**而不是报错,不会出现"配错就永远找不到"。
- `parallel: true` 只在开了 pixhunt `parallel` feature 时生效——该 feature 会把 `rayon`
  传导给 corrmatch;否则会被归一为 `false`(否则 corrmatch 会在 `validate()` 直接报错,
  表现为静默找不到)。
- `min_score` 是**最终结果**的阈值,在 pixhunt 侧把关,不传给 corrmatch。corrmatch 自己的
  同名字段是**逐金字塔层**的候选门槛,而粗筛层分数天然偏低,拿它当最终阈值会把真命中
  整条链路削空。

## v0.8.2 变更

修订号级别:没有破坏性改动,`find_all` 的**命中集合与顺序和 v0.8.1 逐个字节相同**(测试里
与朴素 O(n²) 参照逐字节对照过)。改的是"知情权"和一件算法:

- **`find_on_screen` 现在明写:命中不止一处时,你从返回值里看不出来**。它返回扫描序
  `(y, x)` 的第一个(最上、同样靠上时最靠左)——这个选择是确定的,串行/并行、换后端都给
  同一个坐标——但 `Some` 只表示"至少有一处"。要判唯一就数到 2:

  ```rust
  match finder.find_all_on_screen(&tpl, 2)?.len() {
      0 => {}                                       // 没找到
      1 => {}                                       // 唯一,可以放心点
      _ => {}                                       // 不止一处:缩小 region 或换更有特征的模板
  }
  ```

  ⚠️ `MatchKind::Corr` 下这个守卫**无效**:`CorrMatcher` 没有覆写 `find_all`,永远只返回
  1 个,于是它看起来"永远唯一"。
- **`find_all` 的早停从"行"粒度细化到"列"粒度**:重叠覆盖的判定挪到逐像素验证**之前**
  (被前面的命中盖住的列直接不验证)、攒够 `max` 当场收工、整行/整块都被覆盖就一行都不扫。
  平坦画面因此再掉一到四个数量级(1920x1200 纯色帧,`--release`,同一台机器上同一份探针
  代码、同一场景顺序;`max=0` 的命中数见括号。每格是"该场景只跑一次"的冷启动值):

  | 模板 | `max` | v0.8.1 | v0.8.2 |
  | --- | --- | --- | --- |
  | 8x8(36000 命中) | 0 | 串行 571ms / parallel 184.9ms | **串行 ~9.9ms / parallel ~9.8ms** |
  | 8x8 | 1 | 串行 604µs / parallel 3.41ms | **~83µs / ~77µs**(稳态 ~0.7µs) |
  | 8x8(唯一性守卫) | 2 | 串行 442µs / parallel 3.10ms | **~7µs / ~7.5µs** |
  | 240x80(唯一性守卫) | 2 | 串行 104.4ms / parallel 553.2ms | **~147µs / ~147µs** |
  | 240x80(120 命中) | 0 | 串行 121.0s / parallel 35.3s | **~6.6ms / ~7.5ms** |

  稳态中位数见 `docs/GUIDE.md` 第 10 章的 criterion 交替对拍表(1080p:`max=0` 串行
  461ms → 8.2ms、parallel 138.2ms → 8.7ms;`max=1` 437.7µs → 0.64µs)。
- ⚠️ **两点代价,请按需评估**:① 有纹理画面的 `find_all` 慢一点(1080p criterion 交替 6 轮
  的中位:串行 3.99ms → 4.17ms,+4.5%;parallel 2.59ms → 2.82ms,+9%);② **平坦画面上
  `parallel` 不再比串行快**——
  重叠抑制必须严格按 `(y, x)` 顺序增量做,"被覆盖的列免验证"天生是顺序的,行块并行会把它
  打回逐候选验证(实测:240x80 模板 `max=0`,只做到块粒度早停的并行要 ~7.3s,而串行流式
  ~6.6ms)。所以这种画面下库里会主动放弃并行。

## v0.8.1 变更

修订号级别:没有破坏性改动(只新增一个方法),但有四处**行为修正**,升级后结果可能和 0.8.0 不同:

- **`find_all` 的返回顺序改成文档承诺的 `(y, x)` 升序**(从上到下、从左到右)。此前实际按
  `x` 优先排,且重叠去重"留先遇到的那个"因此留下的是**最左**而非**最上**的命中。现在与
  `find_color_on_screen` 口径一致。依赖旧顺序的代码需要复核。
- **`find_all` 的去重从二次方改为按行增量,`max` 现在真的限制工作量**。此前 `max` 只截断
  结果长度、扫描和去重照样跑完全程:1920x1200 纯色帧 + 8x8 纯色模板、`max=0`(36000 命中)
  实测 **23.5 秒**,`max=1` 也要 ~471ms。当时(v0.8.1)降到 **串行 ~571ms / 开 `parallel`
  ~185ms**(1920x1200 冷启动单次,与上面 v0.8.2 那表同一协议;这一节早期写的 ~347ms /
  ~151ms 在本机没能复现,已按复测值改掉),`max=1` ~0.6ms —— v0.8.2 又把它们压到
  **~9.9ms** 与 **~0.7µs**,见上面 v0.8.2 一节。
  `max` 给 0 的旧代码不会变错,只是从"卡住半小时"变成能跑完。
- **设了 `region` 且后端支持区域直抓(目前只有 `XCapCapture`)时,静态帧缓存开始生效**。
  此前这条路径把每帧都当"画面变了",于是"盯着一小块反复轮询"时缓存 100% 失效(实测静态
  画面连查 3 次搜 3 次,现在搜 1 次)。复用结果与重新搜一遍**完全一致**。`Gdi`/`Dxgi`/
  `Window` 走全屏路径,`changed` 由后端自己报,不受影响。
- **`find_until` / `wait_gone` 传 `Duration::MAX` 不再 panic**。此前 `Instant::now() + timeout`
  溢出会当场中止进程;现在算不出截止时刻就当永不超时。

新增:

- **`Match::center(&tpl) -> (i32, i32)`**:把命中换算成中心坐标。`find_center_on_screen`
  只覆盖单结果,`find_all` / `find_many` 的结果此前要自己写 `m.x + tpl.width as i32 / 2`。
- **`region` 完全落在画面外时提示一次**:`tracing` feature 开启后输出 `warn`(未改动返回值,
  仍是 `Ok(None)`),用来区分"region 坐标写错了"和"屏幕上确实没这张图"。
- **文档口径**:README/`Match` 字段注释都写明 `Rgb` 的 `score` 恒为 `1.0`(此前只有说明书
  里有);`MatchKind::Corr` 侧补上"ZNCC 走灰度、不认模板掩码"的提醒。

## v0.8 新功能

- **透明掩码模板**:`Template::load` 自动识别 PNG alpha;新增 `Template::from_rgba` / `with_mask`;`RgbMatcher` 比较时跳过掩码像素。
- **`find_center_on_screen`**:返回模板**中心**坐标,省去手动加半尺寸。
- **`diff_since_last(rect)`**:对比上一次截图,返回指定区域内颜色有变的像素数。
- **`CaptureKind::WindowByTitle(..)`**:`FinderBuilder` 直接按窗口标题精确匹配截窗口,无需先拿句柄。
- **图片格式扩展**:`Template::load` 现支持 **PNG + JPEG + WebP**(之前仅 PNG)。
- **GDI 后端不再 panic**:`BitBlt` / `GetDIBits` 失败(锁屏、休眠唤醒瞬间等)改为返回
  `Err`,与 DXGI / Window 后端行为一致。之前这里会 `assert!` 直接中止进程。
- **截图后端会自己跟上桌面尺寸变化**:GDI 每次抓取前比对桌面分辨率,变了就重建位图;DXGI
  在复制对象失效(改分辨率、锁屏 / 休眠唤醒、显卡重置)后自动重建并重取。之前 GDI 会一直
  输出错位画面、DXGI 会**永远停在失效前的旧帧**。
- **`find_all` 对带掩码的模板按可见区外接框去重**:此前按整张模板外接框判定,大模板 +
  小图标会把相邻目标漏报成一个。
- **`diff_since_last` 复用基线缓冲**,不再每次重新分配整帧:1080p 实测 5.6 ms → 3.4 ms。

## 升级到 v0.8 (breaking)
破坏面集中在 `Template` 的字段与两处行为变化:

| v0.7 | v0.8 |
| --- | --- |
| `Template` 只有 `rgb` / `width` / `height` 三个字段 | 多出 `pub mask: Option<Vec<bool>>`。用结构体字面量手工构造的代码需要补这个字段;走 `load` / `from_rgb` / `from_rgba` 构造的**不受影响** |
| `find_all` 按整张模板的外接框抑制重叠 | 带掩码的模板改按**可见像素外接框**抑制。同一张图上报的结果可能**变多**(小图标不再被大模板的外接框吞掉);无掩码时行为不变 |
| GDI 截图失败时 `assert!` panic | 返回 `Err`。原先靠捕获 panic 兜底的写法要改成处理 `Result` |
| 改分辨率 / 锁屏唤醒后需重建 `Finder` 才能恢复 | 后端自己跟上桌面尺寸变化,**无需重建** |

另一点:`CorrMatcher` 收到带掩码的模板时,debug 构建会 `debug_assert!` 提示,因为 ZNCC 那条路径
没有掩码接口、会给出与 `RgbMatcher` 不一致的结果。release 构建零开销,照常运行。

## 升级到 v0.7 (breaking)
`Error::Capture` 改成命名字段变体,让截图失败能保住**错误链**:

| v0.6 | v0.7 |
| --- | --- |
| `Error::Capture(String)`(`source()` 恒为 `None`) | `Error::Capture { message, source }` |
| 手写 `Error::Capture(msg.into())` | `Error::capture(msg)`;要带底层错误用 `Error::capture_from(ctx, e)` |

`message` 是"在哪一步失败"(如 `xcap Monitor::capture_image()`),`source` 是原始错误
(xcap 内部还会再包一层 io / D-Bus / Win32 错误,不保留就取不到了)。`Display` 仍把两者
拼成一行,所以只做 `e.to_string()` / `?` 上抛的代码**输出不变**,只有 `match` 到
`Error::Capture(s)` 的地方需要改成 `Error::Capture { message, .. }`。

## 升级到 v0.6 (breaking)
v0.6 把默认截图后端从已停维的 `screenshots` 换成 [xcap](https://crates.io/crates/xcap):

| v0.5 | v0.6 |
| --- | --- |
| `CaptureKind::Screenshots` | `CaptureKind::Monitor` |
| `ScreenshotsCapture` | `XCapCapture`(另有 `from_point()` 可绑指定显示器) |
| MSRV 1.75 | **MSRV 1.88**(下限来自 xcap 在 Linux 上的依赖 zbus;开 `match-corr` 需 1.89) |
| 限定区域 = 抓全屏再裁剪 | `Monitor` 后端**直接区域抓取**(端到端实测 47ms → 20ms) |

命中坐标语义不变:仍是**屏幕绝对坐标**。`Gdi` / `Dxgi` / `Window` 不支持区域直抓,
限定区域时自动回退为"抓全屏再裁剪",结果一致。

## 运行示例
```bash
# 默认(xcap / Monitor 后端)
cargo run --release --example find_on_screen -- path/to/template.png
# 用 Windows 快后端 + 自动选择
cargo run --release --features capture-dxgi,capture-gdi --example find_on_screen -- path/to/template.png
```

## 注意
- **分辨率 / DPI**:模板与截图需同一分辨率尺度,否则找不到。
- **文档**:`cargo doc --no-deps --open` 看完整 API 注释。**docs.rs 上本 crate 的构建目前是
  失败的**(`pixhunt-0.8.0` 在 docs.rs 的 builds 页显示 failed):原因是要编译依赖 `xcap`,
  而 `xcap` 的构建脚本需要系统库(libclang / PipeWire headers 等),docs.rs 环境没有这些包,
  不是本 crate 的代码问题。连带后果:在线文档里看不到 `CaptureKind::Gdi` / `Dxgi` /
  `Window` / `Auto` 这些 Windows 专有变体 —— 想查这些请直接看本地文档或源码。
- **截图 ≠ 匹配**:两者是分开计时/分开的步骤,别把截图耗时算进算法。
- **DXGI 限制**:RDP / 锁屏 / 无 GPU 时不可用,`CaptureKind::Auto` 会自动回退。
- **Linux 系统依赖**:默认后端 xcap 在 X11 走 xcb、Wayland 走 PipeWire/Wayland,
  编译需要:`pkg-config libclang-dev libxcb1-dev libxrandr-dev libdbus-1-dev
  libpipewire-0.3-dev libwayland-dev libegl-dev libgbm-dev`(本仓库 CI 已按此配置)。
  缺失时报的是链接错误(如 `unable to find library -lgbm`),不是编译错误。
- **多显示器**:`Monitor` / `Gdi` / `Dxgi` 都只覆盖**主显示器**。要抓副屏,用
  `XCapCapture::from_point(x, y)`(按屏幕坐标落在哪块屏来选显示器),再交给
  `Finder::new(Box::new(cap), ...)`。
- 帧字节序:GDI / DXGI 产出 BGRA,xcap / PrintWindow 产出 RGBA;`RgbMatcher` 按帧的
  `PixelFormat` 自动映射通道,无需你手动转换。
- **平坦内容会让 `Rgb` 退化**:当模板与搜索区域**都**近乎单色(相邻像素几乎没有差异)
  时,锚点与逐像素早失败全部失效,扫描退化为暴力全量。1080p + 64px 实测:有纹理
  ~3ms,零方差模板放在纯色背景上 ~1.9s(慢 600 倍),且平坦区里会有多个"等价"命中点。
  `find_all` 在这种画面上还会返回**海量命中**(1920x1200 纯色帧 + 8x8 模板实测 36000 个,
  扫描本身现在只要 ~9ms,但这几万个 `Match` 既没意义又占内存),所以 `max` 请给真实上限,
  别习惯性写 0;只想确认"是不是只有一处"的话 `max = 2` 就够。
  同样,`find_on_screen` 在这种画面上给的 `Some` 只是**最上最左**的那个,不代表唯一。
  裁模板请选**有边缘、有纹理**的区域,别从纯色背景上切一块。

## 说明:内容由 AI 生成
本仓库的**代码、注释、测试与文档(含本 README)由 AI 编码助手生成或改写**,并经
`cargo test` / `cargo clippy -D warnings` / GitHub Actions 验证。仓库内的性能数字均为
特定机器上的测量值,只能当量级参考。内容**不保证逐行经过人工细读**,请按对待任何
第三方 crate 的方式自行评审后再用于生产。

## 许可
Licensed under either of **MIT** or **Apache License, Version 2.0** at your option.
See `LICENSE-MIT` and `LICENSE-APACHE`.