{
  "title": "Practice: Text \u0026 Mosaic Auto-Erase + Enhancement — a Full Image Pipeline on CPU",
  "url": "/en/posts/practice-erase-text-mosaic/",
  "permalink": "https://hackcv.com/en/posts/practice-erase-text-mosaic/",
  "date": "2026-08-23",
  "lastmod": "2026-08-23",
  "author": "hackcv",
  "description": "PP-OCRv4 DBNet text detection + pure-CV mosaic detection + multi-source mask OR-merging + LaMa inpainting, then an optional analyze→repair→upscale→refine four-stage enhancer — all CPU; includes real color-fidelity debugging notes.",
  "categories": ["Research Brief"],
  "tags": ["AI","OCR","Image Inpainting","PP-OCRv4","Mosaic Detection","Practice"],
  "cover": "https://picsum.photos/seed/practice-text-mosaic-auto-erase-\u0026#43;-enhancement-a-full-image-pipeline-on-cpu/1200/675",
  "readingTime": 3,
  "wordCount": 748,
  "content": "\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eOne-line takeaway\u003c/strong\u003e: An end-to-end CPU-only image pipeline — \u003cstrong\u003ePP-OCRv4 DBNet locates text (0.2–0.5s) + pure-CV mosaic detection (real-time) + OR-merged masks into LaMa inpainting (2.1s)\u003c/strong\u003e, then optionally a four-stage enhancer (analyze → repair → upscale → refine). Measured: subtitle-bar workflow ≈ \u003cstrong\u003e3s/frame at 1920×1080\u003c/strong\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground \u0026amp; Motivation\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eScenario\u003c/strong\u003e: batch-remove subtitles/watermarks/annotations, de-pixelation of privacy mosaics, old-photo rescue\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePain\u003c/strong\u003e: full OCR (with recognition branch) is big and slow; mosaic detection is usually a trained model; and post-erase quality often needs enhancement — \u003cstrong\u003eno complete CPU-only loop existed\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConstraint\u003c/strong\u003e: same ONNX Runtime CPU route as the benchmark article\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"core-approach-three-layers\"\u003eCore Approach (Three Layers)\u003c/h2\u003e\n\u003ch3 id=\"layer-1-detection-auto-generating-masks\"\u003eLayer 1: Detection (auto-generating masks)\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eText detection (PP-OCRv4 DBNet)\u003c/strong\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDetection branch only (DB text detection), no recognition — model ~4.5 MB, 0.2–0.5s on CPU\u003c/li\u003e\n\u003cli\u003eText polygons → binary mask → dilate per box to cover stroke edges\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eMosaic detection (pure CV, zero models)\u003c/strong\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eUses the visual signature of mosaics: \u003cstrong\u003eblock-grid + missing local high-frequency\u003c/strong\u003e — grid-gradient consistency decides\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--mosaic-grid-thr\u003c/code\u003e controls strictness (0.7–0.9 is safe for most images; 0.9 for face censor)\u003c/li\u003e\n\u003cli\u003eReal-time (milliseconds), no model loading\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"layer-2-mask-merge--inpainting\"\u003eLayer 2: Mask merge + inpainting\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eMulti-source masks (text + mosaic + manual) \u003cstrong\u003eOR-merged\u003c/strong\u003e into one total mask\u003c/li\u003e\n\u003cli\u003eUnified \u003ccode\u003e--dilate + --feather\u003c/code\u003e, then LaMa (model selection in the benchmark article)\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--roi x,y,w,h\u003c/code\u003e restricts detection — erase only the subtitle bar, avoid touching the subject\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eDetection params (real values)\u003c/strong\u003e: DBNet inference threshold \u003ccode\u003ethresh=0.2\u003c/code\u003e, box filter \u003ccode\u003ebox_thresh=0.35\u003c/code\u003e, \u003ccode\u003eunclip_ratio=1.8\u003c/code\u003e — the 1.8× outward expansion of text boxes is exactly what covers stroke-edge residue, the key to clean text removal.\u003c/p\u003e\n\u003ch3 id=\"layer-3-optional-four-stage-enhancer\"\u003eLayer 3 (optional): four-stage enhancer\u003c/h3\u003e\n\u003cp\u003ePost-erase, \u003ccode\u003eenhance.py\u003c/code\u003e runs \u003cstrong\u003eanalyze → repair → upscale → refine\u003c/strong\u003e (all ONNX Runtime CPU):\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eStage\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eWhat\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eKey models (CPU-verified)\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eAnalyze\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNIMA quality score + photo/anime classification\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNIMA mobilenet\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eRepair\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDe-JPEG / denoise / de-blur\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1xDeJPG/1xDeNoise (PLKSR), 1x-hurrdeblur (0.18 MB, very fast)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eUpscale\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2x/4x super-resolution\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSPAN-4x (1.7 MB fastest), Real-ESRGAN general-fast (5 MB, solid detail), Real-CUGAN (anime)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eRefine\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFace restoration\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYOLOv8n-face 5-keypoints → GPEN-BFR-512 (284 MB)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003ePresets: \u003ccode\u003efast\u003c/code\u003e (sharpen only) / \u003ccode\u003ebalanced\u003c/code\u003e (default 2x SPAN) / \u003ccode\u003equality\u003c/code\u003e (4x Real-ESRGAN + full repair + faces) / \u003ccode\u003eanime\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"key-debugging-note-color-fidelity-upscaling-must-not-change-color\"\u003eKey Debugging Note: Color Fidelity (upscaling must not change color)\u003c/h2\u003e\n\u003cp\u003eEarly \u003ccode\u003equality\u003c/code\u003e (4x + faces) output was \u003cstrong\u003edark and color-shifted\u003c/strong\u003e. Two root causes, both fixed:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eWhite-balance false triggers\u003c/strong\u003e: the old criterion estimated color cast from full-image channel-mean spread (threshold 12), misjudging sunsets/greenery/red brick — images with legitimate dominant tints — as cast and auto-white-balancing them. Fix: estimate light-source cast only on \u003cstrong\u003epixels that should be neutral\u003c/strong\u003e (low saturation), threshold raised to 22 — normal photos read \u0026lt;10, never trip it; WB gains clamped (\u003ccode\u003emax_gain=1.30\u003c/code\u003e + luminance preservation) so real casts still get fixed (tungsten 60→12).\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eUpscaler channel bias\u003c/strong\u003e: measured \u003ccode\u003eSPAN-4x.onnx\u003c/code\u003e output ≈ \u003ccode\u003e0.958·in + bias\u003c/code\u003e (bias≈R−0.8/G+3.3/B+6.1) — darkens ~4% overall and shifts red down/blue up; that\u0026rsquo;s the \u0026ldquo;colors change the moment you upscale.\u0026rdquo; Fix: generic \u003cstrong\u003elow-frequency alignment\u003c/strong\u003e (\u003ccode\u003eenhance.align_low_freq\u003c/code\u003e) — an upscaler should only add high frequency; overall tone/color should equal the input, so pull the output\u0026rsquo;s low frequency back to the input\u0026rsquo;s. Near no-op for clean Real-ESRGAN/Real-CUGAN (bias \u0026lt;1) — not a magic constant for SPAN.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAfter the fix, \u003ccode\u003equality\u003c/code\u003e/\u003ccode\u003ebalanced\u003c/code\u003e channel gains return to ~1.000 (max deviation \u0026lt;0.005). \u003ccode\u003e--keep-color\u003c/code\u003e hands full control to the user (Web UI: \u0026ldquo;keep original color\u0026rdquo; checkbox, on by default).\u003c/p\u003e\n\u003ch2 id=\"measured-data\"\u003eMeasured Data\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# Detect + inpaint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epython erase.py -i frame.jpg -o out.png --detect-text                      \u003cspan style=\"color:#75715e\"\u003e# whole-image text (watermark)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epython erase.py -i frame.jpg -o out.png --detect-text --roi 50,450,1020,180 \u003cspan style=\"color:#75715e\"\u003e# subtitle bar only\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epython erase.py -i frame.jpg -o out.png --detect-mosaic --mosaic-grid-thr 0.9\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epython erase.py -i frame.jpg -o out.png --detect-text --detect-mosaic      \u003cspan style=\"color:#75715e\"\u003e# OR-merged masks\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# Erase + enhance combo\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epython erase.py -i text.jpg -o erased.png --detect-text --enhance --enhance-preset balanced\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epython enhance.py -i old.jpg -o out.png --preset quality --keep-color      \u003cspan style=\"color:#75715e\"\u003e# old-photo rescue, keep color\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eStep\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eTime (CPU)\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eDBNet text detection\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.2 ~ 0.5 s/frame\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMosaic detection (pure CV)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ereal-time (ms)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eLaMa inpainting\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~2.1 s/frame\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eSubtitle-bar full workflow\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e≈3 s/frame (1920×1080)\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eEnhance balanced (2x)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eseconds; quality (4x+faces) tens of seconds\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"scope--trade-offs\"\u003eScope \u0026amp; Trade-offs\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eFits\u003c/strong\u003e: subtitle/watermark removal, privacy mosaics, video-frame batch cleanup, full erase-then-enhance loops\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDoesn\u0026rsquo;t fit\u003c/strong\u003e: curved artistic text (complex masks blur when inpainting), \u0026lt;12px tiny text (DBNet misses), publication-grade color-critical work (manual color grading needed)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTrade-off\u003c/strong\u003e: detection-only DBNet trades recognition for speed; if you need \u0026ldquo;what does the text say\u0026rdquo; (e.g., sensitive-word gating), add the recognition branch — but latency rises significantly. Pick per need\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"reproduction-notes\"\u003eReproduction Notes\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eDetection model: PP-OCRv4 DBNet ONNX (~4.5 MB); enhancer models ≈1 GB, fetched by \u003ccode\u003escripts/fetch_enhance_models.py\u003c/code\u003e from \u003cstrong\u003ehf-mirror.com\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eVerification: \u003ccode\u003escripts/test_color_fidelity.py\u003c/code\u003e (seconds; covers false-trigger/true-cast/clamp/keep-color)\u003c/li\u003e\n\u003cli\u003eEnvironment: Python 3.13 + OpenCV 5.0 + onnxruntime, pure CPU\u003c/li\u003e\n\u003c/ul\u003e\n",
  "summary": " One-line takeaway: An end-to-end CPU-only image pipeline — PP-OCRv4 DBNet locates text (0.2–0.5s) + pure-CV mosaic detection (real-time) + OR-merged masks into LaMa inpainting (2.1s), then optionally a four-stage enhancer (analyze → repair → upscale → refine). Measured: subtitle-bar workflow ≈ 3s/frame at 1920×1080.\nBackground \u0026amp; Motivation Scenario: batch-remove subtitles/watermarks/annotations, de-pixelation of privacy mosaics, old-photo rescue Pain: full OCR (with recognition branch) is big and slow; mosaic detection is usually a trained model; and post-erase quality often needs enhancement — no complete CPU-only loop existed Constraint: same ONNX Runtime CPU route as the benchmark article Core Approach (Three Layers) Layer 1: Detection (auto-generating masks) Text detection (PP-OCRv4 DBNet):\n"
}
