|
126 | 126 | # 當背景寫字的語意證據(demo06 教堂 0.167 / 金髮女孩格 |
127 | 127 | # 0.228 手寫字直接落在背景白 ⇒ 放行;老人格 0.000、 |
128 | 128 | # ch34_010 披肩 0.023 ⇒ 無證據) |
| 129 | +# 修法5:閉合格背景擢升(貼框白元件 → sticker 候選) |
| 130 | +FRAME_HUG_DILATE = 5 # 元件外擴後與格線遮罩的交集算「貼框」 |
| 131 | +FRAME_HUG_THICK = 3.0 # 格線名目厚度(px):交集數 → 貼框長度的除數 |
| 132 | +FRAME_HUG_MIN = 0.25 # 貼框長度 / bbox 周長 下限(背景沿格框跑;前景白衣只點狀碰框) |
| 133 | +FRAME_HUG_STRONG = 0.40 # 強貼框:背景證據足夠強 → 走放寬門(見 sticker_plan 兩級制) |
| 134 | +PROMOTED_TEXTON_MAX = 0.30 # 強貼框仍拒的文字覆蓋上限:真有大量文字壓在上面=旁白框, |
| 135 | + # 填黑會把字變描邊糊 → 留灰(demo06 教堂 textOn 0.167 是可接受上界的參照) |
129 | 136 | STICKER_TEXTON_PAD = 8 # textOn 的文字 bbox 外擴(語意證據要「真的壓在元件上」 |
130 | 137 | # ⇒ 只容 bbox 抖動的小 pad;BUBBLE_PAD 40px 窗會把鄰格 |
131 | 138 | # 文字掃進相鄰白元件湊假證據——ch34_010 披肩 pad40=0.207 |
@@ -227,17 +234,21 @@ def detect(img_bgr): |
227 | 234 |
|
228 | 235 | # ── 遮罩:白元件分類(修法2/3)+氣泡(修法1)─────────────────────── |
229 | 236 |
|
230 | | -def page_is_frameless(g): |
231 | | - """修法2:長直格框線存在性。回傳 (frameless, h_px_per_k, v_px_per_k)。 |
232 | | -
|
233 | | - 暗像素對「長水平/垂直線」形態學開運算=只留貼直的長線(格框); |
234 | | - 無框/白背景頁(demo04/05 型)兩向都近零。 |
235 | | - """ |
| 237 | +def frame_line_mask(g): |
| 238 | + """長直格框線遮罩:暗像素對「長水平/垂直線」形態學開運算=只留貼直的長線(格框)。 |
| 239 | + 回傳 (lh, lv) 兩個 0/1 uint8。修法2 的頁型判別與修法5 的貼框擢升共用。""" |
236 | 240 | H, W = g.shape |
237 | 241 | dark = (g < FRAME_DARK_TH).astype(np.uint8) |
238 | 242 | L = max(60, min(W, H) // FRAME_LINE_L_DIV) |
239 | 243 | lh = cv2.morphologyEx(dark, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (L, 1))) |
240 | 244 | lv = cv2.morphologyEx(dark, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (1, L))) |
| 245 | + return lh, lv |
| 246 | + |
| 247 | + |
| 248 | +def page_is_frameless(g): |
| 249 | + """修法2:長直格框線存在性。回傳 (frameless, h_px_per_k, v_px_per_k)。 |
| 250 | + 無框/白背景頁(demo04/05 型)兩向都近零。""" |
| 251 | + lh, lv = frame_line_mask(g) |
241 | 252 | hk, vk = 1000.0 * lh.mean(), 1000.0 * lv.mean() |
242 | 253 | frameless = not (min(hk, vk) >= FRAME_MIN_EACH and hk + vk >= FRAME_MIN_SUM) |
243 | 254 | return frameless, hk, vk |
@@ -503,20 +514,60 @@ def _rects(pad): |
503 | 514 | if frameless: |
504 | 515 | cand = {i for i in (gutter_ids | panel_ids) |
505 | 516 | if stats[i, cv2.CC_STAT_AREA] >= STICKER_MIN_FRAC * g.size} |
| 517 | + hug = {} |
506 | 518 | else: |
507 | 519 | cand = set(panel_ids) |
| 520 | + # 修法5:閉合格背景擢升——未列管(非 gutter 非 panel)的大白元件,若「貼格線長度 / |
| 521 | + # bbox 周長」夠高=背景沿格框跑(閉合格內的天空/空白背景被格線封住、永遠進不了 |
| 522 | + # 修法3 的 gutter/panel 分類),擢升進候選、照走下面同一套安全網。前景白(臉/白衣) |
| 523 | + # 是獨立元件、只點狀碰框 → hug 低、天然不擢升。ch34_014 型(22% 頁面積)的主修。 |
| 524 | + lh, lv = frame_line_mask(g) |
| 525 | + frame = (lh | lv).astype(np.uint8) |
| 526 | + kd = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (FRAME_HUG_DILATE * 2 + 1,) * 2) |
| 527 | + hug = {} |
| 528 | + min_area = GUTTER_MIN_AREA_FRAC * g.size |
| 529 | + listed = gutter_ids | panel_ids |
| 530 | + for i in range(1, stats.shape[0]): |
| 531 | + if i in listed or stats[i, cv2.CC_STAT_AREA] < min_area: |
| 532 | + continue |
| 533 | + x, y, w_, h_ = (stats[i, cv2.CC_STAT_LEFT], stats[i, cv2.CC_STAT_TOP], |
| 534 | + stats[i, cv2.CC_STAT_WIDTH], stats[i, cv2.CC_STAT_HEIGHT]) |
| 535 | + pad = FRAME_HUG_DILATE + 1 |
| 536 | + x0, y0 = max(0, x - pad), max(0, y - pad) |
| 537 | + x1, y1 = min(g.shape[1], x + w_ + pad), min(g.shape[0], y + h_ + pad) |
| 538 | + comp = (lab[y0:y1, x0:x1] == i).astype(np.uint8) |
| 539 | + contact = int((cv2.dilate(comp, kd) & frame[y0:y1, x0:x1]).sum()) |
| 540 | + hug_len = contact / FRAME_HUG_THICK |
| 541 | + frac = hug_len / max(1.0, 2.0 * (w_ + h_)) |
| 542 | + hug[i] = round(float(frac), 3) |
| 543 | + if frac >= FRAME_HUG_MIN: |
| 544 | + cand.add(i) |
508 | 545 | accept, audit = set(), [] |
509 | 546 | for i in sorted(cand): |
510 | 547 | met = sticker_metrics(g, img_bgr, lab, stats, i, text_rects, text_rects_on) |
511 | | - ok = (STICKER_FIG_MIN <= met["figFrac"] <= STICKER_FIG_MAX |
512 | | - and met["thinFrac"] <= STICKER_THIN_MAX |
513 | | - and met["chroma"] <= STICKER_CHROMA_MAX |
514 | | - and met["textCov"] <= STICKER_TEXT_MAX |
515 | | - and met["eatenFrac"] <= STICKER_EATEN_HARD |
516 | | - and (met["eatenFrac"] <= STICKER_EATEN_MAX |
517 | | - or met["textOn"] >= STICKER_TEXT_BG_MIN) |
518 | | - and (met["areaFrac"] >= STICKER_SMALL_AREA |
519 | | - or met["textOn"] >= STICKER_TEXT_BG_MIN)) |
| 548 | + if i in hug: |
| 549 | + met["frameHug"] = hug[i] |
| 550 | + if hug.get(i, 0.0) >= FRAME_HUG_STRONG: |
| 551 | + # 修法5 兩級制——強貼框(背景證據極強)走放寬門: |
| 552 | + # · 跳過 textCov(pad40 窗對大背景是鄰泡污染;真文字壓上用 textOn 擋) |
| 553 | + # · 跳過 eaten 中段門與小面積門(eaten 聚團交給 paint_sticker 的 |
| 554 | + # _sticker_protect 區域級保護=白鬍/髮絲局部不填、其餘照填) |
| 555 | + # · 保留 fig/thin/chroma/eatenHARD 四道硬底線 |
| 556 | + ok = (STICKER_FIG_MIN <= met["figFrac"] <= STICKER_FIG_MAX |
| 557 | + and met["thinFrac"] <= STICKER_THIN_MAX |
| 558 | + and met["chroma"] <= STICKER_CHROMA_MAX |
| 559 | + and met["eatenFrac"] <= STICKER_EATEN_HARD |
| 560 | + and met["textOn"] <= PROMOTED_TEXTON_MAX) |
| 561 | + else: |
| 562 | + ok = (STICKER_FIG_MIN <= met["figFrac"] <= STICKER_FIG_MAX |
| 563 | + and met["thinFrac"] <= STICKER_THIN_MAX |
| 564 | + and met["chroma"] <= STICKER_CHROMA_MAX |
| 565 | + and met["textCov"] <= STICKER_TEXT_MAX |
| 566 | + and met["eatenFrac"] <= STICKER_EATEN_HARD |
| 567 | + and (met["eatenFrac"] <= STICKER_EATEN_MAX |
| 568 | + or met["textOn"] >= STICKER_TEXT_BG_MIN) |
| 569 | + and (met["areaFrac"] >= STICKER_SMALL_AREA |
| 570 | + or met["textOn"] >= STICKER_TEXT_BG_MIN)) |
520 | 571 | met["accept"] = bool(ok) |
521 | 572 | audit.append(met) |
522 | 573 | if ok: |
@@ -603,7 +654,7 @@ def lut_knee(knee=KNEE_X, ceil=DIM_CEIL, end_slope=KNEE_END_SLOPE): |
603 | 654 | "lin8": lambda: lut_linear(8), # 極暗保護:OLED 黑碎顧慮的最小抬升(8 遠小於舊 30) |
604 | 655 | "knee": lambda: lut_knee(), |
605 | 656 | } |
606 | | -SCENE_CURVE = os.environ.get("NIGHTREAD_CURVE", "d2") |
| 657 | +SCENE_CURVE = os.environ.get("NIGHTREAD_CURVE", "lin8") # 2026-08-25 使用者目檢拍板 lin 系(黑實、對比大) |
607 | 658 |
|
608 | 659 |
|
609 | 660 | def lut_scene(): |
|
0 commit comments