fix(app-iced): M79 r3 —— 分隔线守卫的三处「假绿」根治(众数参考色 / 角色身份校验 / 锚定行无条件打印)
代码复审 r3 = p1-2items,两条都是判据自身的假绿,产品代码零改动。
1. **参考色取「跨度中点那一个像素」→ 缺口落在中点就假绿**
`line_segments` 改用该行**墨迹众数色**(`modal_color`):242px 的线 vs ≤4px 的缺口,
众数天然是线自己的颜色。反例 `gap_at_mid`(r2 图在 x=144..145 挖 2px 孔):
改前 `ok. 1 passed`(假绿),改后 **2 段 red**。
2. **行锚定可被「更靠上的另一条细低对比横线」抢走**
`find_thin_line` 增加 `expected` 身份色 + `blend_coverage`:线色必须落在
`Surface → SurfaceVariant` 的插值线上(覆盖率 0.30~1.05,三通道 t 最大差 ≤0.25)。
这比「单通道差 ≤20」稳得多:实测 `OutlineVariant (200,212,224)` → t=(1.84,2.24,3.1) 出界;
hover 8% `(228,233,238)` → t=(0.72,1.0,1.7) 散开 0.98;两者都被拒。
反例 `decoy_outline_gap` / `decoy_hover_gap`:改前 `ok`(假绿,锚到 y=400/401),
改后打印 `divider skip y=… (不是预期角色色的插值,拒绝)` 并退回真分隔线 → **3 段 red**。
3. **锚定行无条件打印**(不管红绿):
`[probe] divider anchored at y=471 span=x24..266 color=[221, 233, 245] segments=1 (light, 1180x760)`
——「锚错行」从此不再静默。
4. **深色档不再是零覆盖**:`divider::check` 浅色必跑,`tabs-dark.png` 存在就跑深色
(实测 `y=471 color=[46,60,70] segments=1 (dark)`)。判据/锚定逻辑抽到 `probe/divider.rs`,
`probe.rs` 393 行仍 <400。
9 张图全跑(退出码即判据):
r2 rc=0(1段) · before rc=101(3段@471) · mutB rc=101(2段) · mutC rc=101(2段) ·
mutE rc=101(3段@471) · mutF rc=101(3段@471) · **gap_at_mid rc=101(2段)** ·
**decoy_outline_gap rc=101(3段@471,y=400 被身份拒)** · **decoy_hover_gap rc=101(3段@471,y=401 被身份拒)**。
已知边界写进 `probe/scan.rs` 模块文档:≤25% 覆盖的淡线 → **loud red**(锚定失败 panic,
不会假绿);身份校验绑定 `SurfaceVariant`(换角色色会 loud red,改 `divider.rs` 一行);
只认横线;深色图缺失时跳过深色断言但不失败。
门禁:fmt ✓ · `cargo test -p unlucky-app-iced` 197 passed · gate 6 passed · probe 5 passed ·
`clippy --all-targets -D warnings` 干净。变异图在 `target/m79/{mutB,mutC,mutE,mutF,gap_at_mid,decoy_outline_gap,decoy_hover_gap}/`,不入库。
This commit is contained in:
1 parent
dffae2c429
commit
111955efae
3 files changed
+153
-33
No files matched your search
@@ -1,7 +1,21 @@
|
||||
//! `probe` 的读图器(`probe.rs` 拆出来的:单文件 ≤400 行,AGENTS.md §3-1)。
|
||||
//!
|
||||
//! 只做三件事:读 PNG、找同色的连通块(`BBox`)、量一条横带里某颜色的墨迹范围。
|
||||
//! 判据全在 `probe.rs`。
|
||||
//! 只做三件事:读 PNG、找同色的连通块(`BBox`)、量一条横带里某颜色的墨迹范围
|
||||
//! 与「一条细线」。判据在 `probe.rs` / `probe/divider.rs` / `probe/long.rs`。
|
||||
//!
|
||||
//! ## 已知边界(分隔线守卫,写在这里供以后改的人查)
|
||||
//!
|
||||
//! 1. **≤25% 覆盖的淡线找不到** → 表现是 **loud red**(`find_thin_line` 返回 `None`,
|
||||
//! `divider::check` 直接 panic「锚定失败」),**不是假绿**。原因:`find_thin_line` 的
|
||||
//! `TOL=6` 是单通道可辨差下限,40% 覆盖行实测 `(9,6,4)` 刚好在线上,25% 覆盖行只有
|
||||
//! `(6,4,2)`,会落到判据外——但相邻那行是满覆盖(`(25,17,10)`),所以真实渲染的画法下
|
||||
//! 行锚定不会丢;只有「整条线都画在半像素上」才会既找不到又看起来像没画。
|
||||
//! 2. **身份校验绑定了 `SurfaceVariant`**:`divider::check` 用
|
||||
//! `Surface → SurfaceVariant` 的插值做身份校验。若哪天 Secondary 分隔线换了角色色
|
||||
//! (比如改回 `OutlineVariant`),这里会 loud red——届时改 `divider.rs` 里那一行 `Role`。
|
||||
//! 3. **只认「一条横线」**:竖向分隔线(本项目不用)与圆角/虚线画法不在判据内。
|
||||
//! 4. **深浅两档**:浅色图必跑;深色图存在就跑(`tabs-dark.png`),深色 divider 对比更低
|
||||
//! (实测线色 `(46,60,70)`,与深色 Surface 的差比浅色档小),更值得测。
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
@@ -200,9 +214,10 @@ impl Shot {
|
||||
pub(super) fn find_thin_line(
|
||||
&self,
|
||||
background: Rgb,
|
||||
expected: Rgb,
|
||||
min_len: usize,
|
||||
max_contrast: u8,
|
||||
) -> Option<(usize, usize, usize)> {
|
||||
) -> Option<(usize, usize, usize, Rgb)> {
|
||||
const TOL: u8 = 6;
|
||||
let is_ink = |pixel: Rgb| differs(pixel, background, TOL);
|
||||
let contrast = |pixel: Rgb| {
|
||||
@@ -244,24 +259,38 @@ impl Shot {
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(y);
|
||||
let line = self.at(
|
||||
(first..=last)
|
||||
.find(|x| is_ink(self.at(*x, best)))
|
||||
.unwrap_or(mid),
|
||||
best,
|
||||
);
|
||||
let line = self.modal_color(best, first, last);
|
||||
let c = contrast(line);
|
||||
if c > max_contrast || c <= TOL {
|
||||
continue;
|
||||
}
|
||||
return Some((best, first, last));
|
||||
// **身份校验**:线色必须落在 `底色 → 预期角色色` 的插值线上(覆盖率 30%~105%)。
|
||||
// 只靠「跨度/薄/低对比」挡不住「更靠上的另一条同类细线」(复审 r3 的
|
||||
// `decoy_outline_gap` / `decoy_hover_gap` 两个反例都因此锚错行)。
|
||||
let Some(coverage) = Self::blend_coverage(line, background, expected) else {
|
||||
eprintln!(
|
||||
"[probe] divider skip y={best} color={line:?}(不是预期角色色的插值,拒绝)"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
if !(0.30..=1.05).contains(&coverage) {
|
||||
eprintln!(
|
||||
"[probe] divider skip y={best} color={line:?}(覆盖率 {coverage:.2} 出界,拒绝)"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
return Some((best, first, last, line));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// `y` 行 `[x0, x1]` 上「按该行中点的颜色」数的连通段数(分隔线断没断)。
|
||||
/// `y` 行 `[x0, x1]` 上「按该行**墨迹众数色**」数的连通段数(分隔线断没断)。
|
||||
///
|
||||
/// 参考色为什么不能取「跨度中点那一个像素」:缺口恰落在中点时,参考色变成页面底色,
|
||||
/// 数出来的是「底色有几段」→ 恒为 1 → **假绿**(复审 r3 的 `gap_at_mid` 反例)。
|
||||
/// 众数色在整段跨度里天然是线自己的颜色(242px 的线 vs ≤4px 的缺口)。
|
||||
pub(super) fn line_segments(&self, y: usize, x0: usize, x1: usize) -> usize {
|
||||
let line = self.at((x0 + x1) / 2, y);
|
||||
let line = self.modal_color(y, x0, x1);
|
||||
let mut segments = 0;
|
||||
let mut in_run = false;
|
||||
for x in x0..=x1 {
|
||||
@@ -276,6 +305,47 @@ impl Shot {
|
||||
segments
|
||||
}
|
||||
|
||||
/// `y` 行 `[x0, x1]` 里出现次数最多的颜色(分隔线的线色)。
|
||||
pub(super) fn modal_color(&self, y: usize, x0: usize, x1: usize) -> Rgb {
|
||||
let mut counts: std::collections::HashMap<Rgb, usize> = std::collections::HashMap::new();
|
||||
for x in x0..=x1.min(self.width - 1) {
|
||||
*counts.entry(self.at(x, y)).or_insert(0) += 1;
|
||||
}
|
||||
counts
|
||||
.into_iter()
|
||||
.max_by_key(|(_, n)| *n)
|
||||
.map(|(c, _)| c)
|
||||
.unwrap_or(self.at(x0.min(self.width - 1), y))
|
||||
}
|
||||
|
||||
/// `pixel` 是否落在 `from → to` 的插值线上(覆盖率 t),返回三个通道一致时的平均 t。
|
||||
///
|
||||
/// 用来给「这条线是不是某个角色色」做**身份校验**:比「单通道差 ≤N」稳得多——
|
||||
/// 实测 `OutlineVariant (200,212,224)` → t=(1.84,2.24,3.1) 出界;
|
||||
/// hover 8% `(228,233,238)` → t=(0.72,1.0,1.7) 三通道散开 0.98;两者都被拒。
|
||||
/// 而真分隔线满覆盖 `(221,233,245)` → t=(1,1,1);40% 覆盖的抗锯齿行 → t≈(0.36,0.35,0.40) 被接受。
|
||||
///
|
||||
/// 只统计 `|to - from| > 4` 的通道(差太小的通道不携带信息,噪声会把 t 带偏)。
|
||||
pub(super) fn blend_coverage(pixel: Rgb, from: Rgb, to: Rgb) -> Option<f32> {
|
||||
let mut ts = Vec::new();
|
||||
for ((p, f), t) in pixel.iter().zip(from).zip(to) {
|
||||
let delta = f32::from(t) - f32::from(f);
|
||||
if delta.abs() <= 4.0 {
|
||||
continue;
|
||||
}
|
||||
ts.push((f32::from(*p) - f32::from(f)) / delta);
|
||||
}
|
||||
if ts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let max = ts.iter().cloned().fold(f32::MIN, f32::max);
|
||||
let min = ts.iter().cloned().fold(f32::MAX, f32::min);
|
||||
if max - min > 0.25 {
|
||||
return None;
|
||||
}
|
||||
Some(ts.iter().sum::<f32>() / ts.len() as f32)
|
||||
}
|
||||
|
||||
/// 矩形内「与底色不同」的像素的左右范围(量文本墨迹用)。
|
||||
///
|
||||
/// 为什么不按精确文字色找:小字号抗锯齿后几乎没有像素落在**精确**色上(容差 3 时实测
|
||||
|
||||
Reference in new issue
Block a user