feat: TV 亚像素精修(alpha 反推背景自然性)4/4 标定命中 ≤1px
- 贴片=bg*(1-a)+white*a 物理模型,反推 bg 的 TV 在真位置最低 - 与 chamfer z 分数联合 (λ=1.0),±5px 窗口;信噪保护防无信号题扰动 - 离线 9/10 保持;post_pos 读数只在 PASS 时可信的新认知
This commit is contained in:
@@ -334,3 +334,28 @@ ddddocr 1.6.1(slide_match,simple_target=True)对抖音贴图题全灭:
|
||||
- 原因:slide_match 为「窄长滑块 + 挖空缺口」设计;抖音 mark 是约 100px 大块
|
||||
贴图、缺口是半透明白贴片(非挖空),模板纹理关系不成立
|
||||
- 结论:ddddocr 不适用于该场景,维持方向感知 chamfer 路线
|
||||
|
||||
### TV 亚像素精修(2026-09-15 深夜,自研方案终局)
|
||||
|
||||
原理:缺口渲染 = bg×(1−a)+white×a(半透明白贴片)。给定候选位置,用 mark 的
|
||||
alpha 反推 bg(=(pixel−white·a)/(1−a)),反推结果的总变差(TV)在正确位置最低
|
||||
(反推出的背景应是自然图像)。与 chamfer z 分数相加(λ=1.0),±5px 窗口联合取谷。
|
||||
|
||||
标定(4 样本,bbox 系真值):
|
||||
|
||||
| 题 | 真值 | 精修前 Δ | 精修后 Δ |
|
||||
| --- | --- | --- | --- |
|
||||
| live_164551 | 309 | +1 | **0** |
|
||||
| live_165204 | 177 | −1 | **0** |
|
||||
| live_172458 | 388 | −1 | **+1** |
|
||||
| live_173321 | 391 | 0 | **0** |
|
||||
|
||||
离线 10 题回归 9/10 保持(TV 保护逻辑:谷值信噪 <0.5σ 时跳过,防噪声扰动)。
|
||||
|
||||
同日上线:live_172415(=155342 同题二出),solve x=395 未被 TV 移动
|
||||
(comb 谷在原位),用户目检确认「这个很准」。仍 VerifyErr。
|
||||
修正认知:**post_pos 位移读数只在 PASS 时可信**(VerifyErr 后 SDK 立即重置,
|
||||
读到的是重置中间态——172415 读到 231.8/243.3「截断」实为重置,非不跟手)。
|
||||
|
||||
VerifyErr 尾案:位置准 + VerifyErr 并存 → 服务端容差可能比预想更紧(±2px 级),
|
||||
或存在尚未观测到的判据(行为分残留 / mark 与服务端渲染的 ±2px 源差异)。
|
||||
|
||||
@@ -242,6 +242,92 @@ def rotation_scan(d, pts, angs, w, h, x, y):
|
||||
return None
|
||||
|
||||
|
||||
def _tv_refine(big, mark, d, target, win=5, lam=1.0):
|
||||
"""基于半透明贴片物理模型的亚像素精修。
|
||||
|
||||
缺口渲染 = bg*(1-a) + white*a。给定候选位置,用 mark 的 alpha 反推 bg,
|
||||
反推结果的 Total Variation 越低越自然 → 位置越正确。与 chamfer 分数
|
||||
z-normalize 后相加(λ=1.0),在 ±win px 窗口取联合最低点。
|
||||
无有效 TV 信号(alpha 全不透明 / 信号弱)时保持原位。
|
||||
"""
|
||||
try:
|
||||
if big is None or mark is None or mark.ndim != 3 or mark.shape[2] != 4:
|
||||
return target
|
||||
bigf = big.astype(np.float32)
|
||||
a_full = mark[..., 3].astype(np.float32) / 255.0
|
||||
s = target["scale"]
|
||||
bx0, by0 = d["bbox"]
|
||||
h, w = a_full.shape
|
||||
ar_full = cv2.resize(a_full, (max(1, round(w * s)), max(1, round(h * s))),
|
||||
interpolation=cv2.INTER_AREA)
|
||||
ay0, ax0 = round(by0 * s), round(bx0 * s)
|
||||
bh, bw = d["am"].shape
|
||||
ay1, ax1 = round((by0 + bh) * s), round((bx0 + bw) * s)
|
||||
ay0, ax0 = max(0, ay0), max(0, ax0)
|
||||
ar = ar_full[ay0:ay1, ax0:ax1]
|
||||
if ar.size == 0 or ((ar > 0.05) & (ar < 0.85)).sum() < 200:
|
||||
return target
|
||||
H, W = d["shape"]
|
||||
score2, _, _, _ = score_at_scale(d, s)
|
||||
if score2 is None:
|
||||
return target
|
||||
ch_vals, tv_vals, dxs = [], [], []
|
||||
for dy in range(-win, win + 1):
|
||||
for dx in range(-win, win + 1):
|
||||
x0 = target["x"] + dx
|
||||
y0 = target["y"] + dy
|
||||
if y0 < 0 or x0 < 0 or y0 + ar.shape[0] > H or x0 + ar.shape[1] > W:
|
||||
ch_vals.append(None); tv_vals.append(None); dxs.append((dx, dy))
|
||||
continue
|
||||
tv = _tv_at(bigf, ar, x0, y0)
|
||||
ch_vals.append(float(score2[y0, x0]))
|
||||
tv_vals.append(tv)
|
||||
dxs.append((dx, dy))
|
||||
chv = np.array([v if v is not None else np.nan for v in ch_vals], np.float32)
|
||||
tvv = np.array([v if v is not None else np.nan for v in tv_vals], np.float32)
|
||||
if np.isnan(tvv).all():
|
||||
return target
|
||||
sd_t = np.nanstd(tvv)
|
||||
# TV 信噪保护:谷值不显著(<0.5σ 低于均值)说明该题 TV 无区分度,不采纳
|
||||
if sd_t < 1e-6 or (np.nanmean(tvv) - np.nanmin(tvv)) < 0.5 * sd_t:
|
||||
return target
|
||||
def zn(v):
|
||||
m = np.nanmean(v)
|
||||
sd = np.nanstd(v) + 1e-9
|
||||
return (v - m) / sd
|
||||
comb = zn(chv) + lam * zn(tvv)
|
||||
if np.isnan(comb).all():
|
||||
return target
|
||||
bi = int(np.nanargmin(comb))
|
||||
dx, dy = dxs[bi]
|
||||
if dx == 0 and dy == 0:
|
||||
return target
|
||||
out = dict(target)
|
||||
out["x"] = target["x"] + dx
|
||||
out["y"] = target["y"] + dy
|
||||
return out
|
||||
except Exception:
|
||||
return target
|
||||
|
||||
|
||||
def _tv_at(bigf, ar, x0, y0):
|
||||
"""在 (x0,y0)(bbox 左上)反推背景并返回绿色通道 TV。"""
|
||||
try:
|
||||
th, tw = ar.shape
|
||||
roi = bigf[y0:y0 + th, x0:x0 + tw]
|
||||
m = (ar > 0.05) & (ar < 0.85)
|
||||
if m.sum() < 200:
|
||||
return None
|
||||
white = np.array([255.0, 255.0, 255.0], np.float32)
|
||||
bg = (roi - white * ar[..., None]) / np.clip(1.0 - ar[..., None], 0.15, 1.0)
|
||||
g = bg[..., 1]
|
||||
if not np.isfinite(g).all():
|
||||
return None
|
||||
return float(np.abs(np.diff(g, axis=1)).mean() + np.abs(np.diff(g, axis=0)).mean())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def solve_pair(big, mark, tip_y=None):
|
||||
"""纯函数:给定大图与 mark 图(BGR/BGRA ndarray),返回求解结果。
|
||||
|
||||
@@ -400,6 +486,11 @@ def solve_pair(big, mark, tip_y=None):
|
||||
if best[0] < target["refined"]:
|
||||
target = dict(scale=best[1], x=best[2], y=best[3], rot=best[4],
|
||||
refined=best[0])
|
||||
# TV 精修(2026-09-15):贴片 = bg*(1-a)+white*a,位置正确时反推出的
|
||||
# bg(=(pixel-white*a)/(1-a))应是自然图像(总变差最低)。
|
||||
# 4 标定样本上真值处 TV 全部谷底或并列谷底;与 chamfer z-分数相加
|
||||
# (λ=1.0) 后 4/4 命中 ≤1px。仅 OK 候选做,窗口 ±5px。
|
||||
target = _tv_refine(big, mark, d, target)
|
||||
s = target["scale"]
|
||||
off = (target["x"] - round(bx0 * s), target["y"] - round(by0 * s))
|
||||
return dict(x=off[0], y=off[1], scale=s, rot=target["rot"],
|
||||
|
||||
Reference in New Issue
Block a user