Hnnnhn Claude Fable 5 commited on
Commit
f932faf
·
1 Parent(s): fa74b9d

OCR 框精修实验:实测放大对比噪声(1.27→1.87 fig px),默认关、留 ?refine=1 开关;HANDOFF 记录差分指标教训

Browse files
Files changed (3) hide show
  1. HANDOFF.md +2 -1
  2. seg_preview_server.py +2 -0
  3. 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. **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
 
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: