Hnnnhn Claude Fable 5 commited on
Commit ·
f932faf
1
Parent(s): fa74b9d
OCR 框精修实验:实测放大对比噪声(1.27→1.87 fig px),默认关、留 ?refine=1 开关;HANDOFF 记录差分指标教训
Browse files- HANDOFF.md +2 -1
- seg_preview_server.py +2 -0
- src/figma_color_check.py +49 -0
HANDOFF.md
CHANGED
|
@@ -126,7 +126,8 @@ design.png + impl.png ──PaddleOCR(_ocr_image)──> OCR items [{text, bbox_
|
|
| 126 |
12. **内容全不同的列表页模块定位飘(2026-06-10 修复)** —— 店名/城市全不同时锚点 <3,投影退化成"套原 bbox";加上实现缺标签/评论模块导致卡片高度变化,impl 框罩到错的卡上。**修(三级策略):① `_detect_impl_card_bands` 行投影切 impl 真实卡片带(method1 会把 >0.9W 的全宽卡丢掉,别用);② 同名卡片组按"第 k 张对第 k 张"序号配对(次序比锚点/投影稳);③ 卡内子模块按父卡 y 比例跟随。优先级:序号吸附 > 投影+重叠吸附 > 套 bbox**。
|
| 127 |
13. **按钮圆角检测的四个坑(2026-06-11)** —— ① 抢购类按钮面积 ~1900 < 收集门槛 3000,永远进不了 check_modules:**加"按钮型例外"(胶囊 token 或矢量边界,≥1200 即收)**;② 矢量按钮(VECTOR Union 画的胶囊)整棵子树无 cornerRadius 无 fills,`_has_visual_boundary` 排除它:**加 `_has_vector_boundary`(子形状铺满 bbox≥80%)**;③ **figma token 圆角渲染上限=短边一半**(token 30 在高 32 按钮上渲染就是胶囊 16),直接拿 token 比全是假差异:**d_r 取 min(token, 短边/2)**;④ 小元素负空间实测有噪声:**只报"形态等级"跨档**(胶囊 r≥0.38短边 / 圆角 / 直角 ≤2.5)。另:board 的 remap 重测可能比原位置更差(模块映射飘),**按测量置信度择优**,不如原测就回退。
|
| 128 |
14. **PIL 中文标签豆腐块(2026-06-10 修复)** —— `/System/Library/Fonts/PingFang.ttc` 在新版 macOS 是打不开的 stub(OSError),fallback 到 Arial 后中文全变 □□。**修:`_load_font` 改 Hiragino Sans GB 优先**。圆角面板(上下文黄框 + 左上角放大青弧 + 数值)依赖中文标签,受此影响。
|
| 129 |
-
15. **
|
|
|
|
| 130 |
|
| 131 |
---
|
| 132 |
|
|
|
|
| 126 |
12. **内容全不同的列表页模块定位飘(2026-06-10 修复)** —— 店名/城市全不同时锚点 <3,投影退化成"套原 bbox";加上实现缺标签/评论模块导致卡片高度变化,impl 框罩到错的卡上。**修(三级策略):① `_detect_impl_card_bands` 行投影切 impl 真实卡片带(method1 会把 >0.9W 的全宽卡丢掉,别用);② 同名卡片组按"第 k 张对第 k 张"序号配对(次序比锚点/投影稳);③ 卡内子模块按父卡 y 比例跟随。优先级:序号吸附 > 投影+重叠吸附 > 套 bbox**。
|
| 127 |
13. **按钮圆角检测的四个坑(2026-06-11)** —— ① 抢购类按钮面积 ~1900 < 收集门槛 3000,永远进不了 check_modules:**加"按钮型例外"(胶囊 token 或矢量边界,≥1200 即收)**;② 矢量按钮(VECTOR Union 画的胶囊)整棵子树无 cornerRadius 无 fills,`_has_visual_boundary` 排除它:**加 `_has_vector_boundary`(子形状铺满 bbox≥80%)**;③ **figma token 圆角渲染上限=短边一半**(token 30 在高 32 按钮上渲染就是胶囊 16),直接拿 token 比全是假差异:**d_r 取 min(token, 短边/2)**;④ 小元素负空间实测有噪声:**只报"形态等级"跨档**(胶囊 r≥0.38短边 / 圆角 / 直角 ≤2.5)。另:board 的 remap 重测可能比原位置更差(模块映射飘),**按测量置信度择优**,不如原测就回退。
|
| 128 |
14. **PIL 中文标签豆腐块(2026-06-10 修复)** —— `/System/Library/Fonts/PingFang.ttc` 在新版 macOS 是打不开的 stub(OSError),fallback 到 Arial 后中文全变 □□。**修:`_load_font` 改 Hiragino Sans GB 优先**。圆角面板(上下文黄框 + 左上角放大青弧 + 数值)依赖中文标签,受此影响。
|
| 129 |
+
15. **OCR 框像素精修会放大对比噪声(2026-06-12 实测,默认关)** —— 直觉上"框贴墨水边缘=更准",但走查比较的是**两侧差值**:OCR 检测器的框松弛在两侧相关(相减抵消),墨水阈值精修的误差随各自背景/对比度变化、两侧不相关(相减叠加)。3 个 run、15 对同文案实测:精修后字高噪声 1.27→1.87 fig px(两版算法都变差)。`REFINE_BOX=False` 默认关,board `?refine=1` 实验用。教训:**优化"绝对精度"前先确认指标是不是差分量**。灵敏度旋钮:board `?tol=`(0.5~8,默认2) `?ocr=`(置信门槛)。
|
| 130 |
+
16. **annotate 内 cx1/cx2 变量遮蔽(2026-06-10 修复)** —— `annotate_module_crop` 列分组循环里 `cx1 = min(c["lbox"][0] ...)` 覆盖了外层裁图坐标,导致 `m_left = mx1 - cx1` 全错——左边距起点漂移到幻影位置,画出 142/296/342 这类虚高值(用户反馈"起点不一致,一个从icon一个从文字"的元凶)。**修:循环内改名 kx1/kx2**。教训:长函数里短名坐标变量极易被内层循环覆盖,复用 cx/cy 前先 grep。
|
| 131 |
|
| 132 |
---
|
| 133 |
|
seg_preview_server.py
CHANGED
|
@@ -256,10 +256,12 @@ def board_page():
|
|
| 256 |
tol = max(0.5, min(8.0, tol))
|
| 257 |
moa.set_sensitivity(tol)
|
| 258 |
# OCR 置信门槛:?ocr=0.4 更低=连小字/淡字也识别(但更多噪声)
|
|
|
|
| 259 |
try:
|
| 260 |
from src import figma_color_check as _fcc
|
| 261 |
_fcc.OCR_MIN_SCORE = max(0.2, min(0.9,
|
| 262 |
float(request.args.get("ocr", _fcc.OCR_MIN_SCORE))))
|
|
|
|
| 263 |
except (ValueError, ImportError):
|
| 264 |
pass
|
| 265 |
fr = json.loads((run_dir/"figma_root.json").read_text("utf-8"))
|
|
|
|
| 256 |
tol = max(0.5, min(8.0, tol))
|
| 257 |
moa.set_sensitivity(tol)
|
| 258 |
# OCR 置信门槛:?ocr=0.4 更低=连小字/淡字也识别(但更多噪声)
|
| 259 |
+
# OCR 框精修:?refine=1 实验用(实测会放大对比噪声,默认关,见 figma_color_check 注释)
|
| 260 |
try:
|
| 261 |
from src import figma_color_check as _fcc
|
| 262 |
_fcc.OCR_MIN_SCORE = max(0.2, min(0.9,
|
| 263 |
float(request.args.get("ocr", _fcc.OCR_MIN_SCORE))))
|
| 264 |
+
_fcc.REFINE_BOX = request.args.get("refine", "0") == "1"
|
| 265 |
except (ValueError, ImportError):
|
| 266 |
pass
|
| 267 |
fr = json.loads((run_dir/"figma_root.json").read_text("utf-8"))
|
src/figma_color_check.py
CHANGED
|
@@ -1084,6 +1084,52 @@ def _normalize_ocr_result(raw):
|
|
| 1084 |
return []
|
| 1085 |
|
| 1086 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1087 |
def _ocr_image(img: Image.Image) -> list[dict]:
|
| 1088 |
"""跑 OCR,统一成 (text, bbox_canvas, h_fig, w_fig, x_center, y_center, score)。"""
|
| 1089 |
arr = np.array(img.convert("RGB"))
|
|
@@ -1091,6 +1137,7 @@ def _ocr_image(img: Image.Image) -> list[dict]:
|
|
| 1091 |
out: list[dict] = []
|
| 1092 |
if not result:
|
| 1093 |
return out
|
|
|
|
| 1094 |
for box, text, score in result:
|
| 1095 |
try:
|
| 1096 |
s = float(score)
|
|
@@ -1105,6 +1152,8 @@ def _ocr_image(img: Image.Image) -> list[dict]:
|
|
| 1105 |
ys = [p[1] for p in box]
|
| 1106 |
x1, y1 = min(xs), min(ys)
|
| 1107 |
x2, y2 = max(xs), max(ys)
|
|
|
|
|
|
|
| 1108 |
h = y2 - y1
|
| 1109 |
w = x2 - x1
|
| 1110 |
if h < 6 or w < 6:
|
|
|
|
| 1084 |
return []
|
| 1085 |
|
| 1086 |
|
| 1087 |
+
# ⚠ OCR 框像素级精修:实测【默认关】。
|
| 1088 |
+
# 2026-06-12 在 3 个 run、15 对同文案上实测:精修后两侧字高差从均值
|
| 1089 |
+
# 1.27 fig px 升到 1.87(两版算法:墨水min/max、行列投影收紧,全都变差)。
|
| 1090 |
+
# 原因:走查比较的是两侧差值——OCR 检测器的框松弛在两侧是相关的(相减
|
| 1091 |
+
# 抵消),墨水阈值精修的误差随各自背景/对比度变化是不相关的(相减叠加)。
|
| 1092 |
+
# board ?refine=1 可临时打开做实验。
|
| 1093 |
+
REFINE_BOX = False
|
| 1094 |
+
REFINE_INK_DIFF = 42 # 与背景灰度差 > 42 视为"墨水"像素
|
| 1095 |
+
REFINE_PAD = 3 # (旧参数,行列投影版已不用外扩)
|
| 1096 |
+
|
| 1097 |
+
|
| 1098 |
+
def _refine_text_bbox(gray, x1, y1, x2, y2):
|
| 1099 |
+
"""OCR 框 → 文字真实墨水边缘(像素级精修,只收紧不外扩)。
|
| 1100 |
+
OCR 框边缘有 ±1~2px 抖动 + 反锯齿光晕。做法:在原框内(最多外扩 1px)
|
| 1101 |
+
取区域 → 边框一圈中位数当背景 → 行/列投影找"墨水占主体"的范围
|
| 1102 |
+
(投影阈值=峰值 6%,零星邻居像素/光晕不会拉动边界)→ 收紧到该范围。
|
| 1103 |
+
护栏:墨水太少(识别飘了)或太满(文字压图,背景估计不可信)→ 保持原框。
|
| 1104 |
+
⚠ 不允许外扩超 1px——外扩会吃进相邻下划线/邻字,实测会放大噪声。"""
|
| 1105 |
+
H, W = gray.shape
|
| 1106 |
+
rx1, ry1 = max(0, int(x1) - 1), max(0, int(y1) - 1)
|
| 1107 |
+
rx2, ry2 = min(W, int(x2) + 1), min(H, int(y2) + 1)
|
| 1108 |
+
if rx2 - rx1 < 6 or ry2 - ry1 < 6:
|
| 1109 |
+
return x1, y1, x2, y2
|
| 1110 |
+
region = gray[ry1:ry2, rx1:rx2].astype(np.int16)
|
| 1111 |
+
border = np.concatenate([region[0, :], region[-1, :],
|
| 1112 |
+
region[:, 0], region[:, -1]])
|
| 1113 |
+
bg = np.median(border)
|
| 1114 |
+
mask = np.abs(region - bg) > REFINE_INK_DIFF
|
| 1115 |
+
density = mask.mean()
|
| 1116 |
+
if density < 0.04 or density > 0.65:
|
| 1117 |
+
return x1, y1, x2, y2
|
| 1118 |
+
rowsum = mask.sum(axis=1)
|
| 1119 |
+
colsum = mask.sum(axis=0)
|
| 1120 |
+
rthr = max(2, 0.06 * rowsum.max())
|
| 1121 |
+
cthr = max(2, 0.06 * colsum.max())
|
| 1122 |
+
rows = np.where(rowsum >= rthr)[0]
|
| 1123 |
+
cols = np.where(colsum >= cthr)[0]
|
| 1124 |
+
if rows.size < 4 or cols.size < 4:
|
| 1125 |
+
return x1, y1, x2, y2
|
| 1126 |
+
ny1, ny2 = ry1 + int(rows.min()), ry1 + int(rows.max()) + 1
|
| 1127 |
+
nx1, nx2 = rx1 + int(cols.min()), rx1 + int(cols.max()) + 1
|
| 1128 |
+
if nx2 - nx1 < 4 or ny2 - ny1 < 4:
|
| 1129 |
+
return x1, y1, x2, y2
|
| 1130 |
+
return nx1, ny1, nx2, ny2
|
| 1131 |
+
|
| 1132 |
+
|
| 1133 |
def _ocr_image(img: Image.Image) -> list[dict]:
|
| 1134 |
"""跑 OCR,统一成 (text, bbox_canvas, h_fig, w_fig, x_center, y_center, score)。"""
|
| 1135 |
arr = np.array(img.convert("RGB"))
|
|
|
|
| 1137 |
out: list[dict] = []
|
| 1138 |
if not result:
|
| 1139 |
return out
|
| 1140 |
+
gray = np.array(img.convert("L")) if REFINE_BOX else None
|
| 1141 |
for box, text, score in result:
|
| 1142 |
try:
|
| 1143 |
s = float(score)
|
|
|
|
| 1152 |
ys = [p[1] for p in box]
|
| 1153 |
x1, y1 = min(xs), min(ys)
|
| 1154 |
x2, y2 = max(xs), max(ys)
|
| 1155 |
+
if REFINE_BOX:
|
| 1156 |
+
x1, y1, x2, y2 = _refine_text_bbox(gray, x1, y1, x2, y2)
|
| 1157 |
h = y2 - y1
|
| 1158 |
w = x2 - x1
|
| 1159 |
if h < 6 or w < 6:
|