档案数字化系统源码研发手记:老旧档案去污点——中值滤波与 inpainting 效果对比

一、为什么档案去污点是个"必须做但又不能乱做"的活

老旧档案的污点来源五花八门:霉斑、水渍、油渍、虫蛀、胶带残留、墨水滴落、翻阅留下的指纹。这些污点对 OCR 的影响分两种:

  • 浅色污渍(水渍、霉斑浅层):干扰图像质量但文字仍可读;
  • 深色污点(墨滴、虫蛀孔、胶带黑影):直接遮挡文字,或者让 OCR 误检出大量"伪文本框"。

01-全流程地图-系统界面

档案数字化有一个铁律:处理过程可以提升可读性,但绝不能改变原始信息(依据 DA/T 31-2017 关于图像处理的要求,加工后的数字图像应保持与原件的"唯一性对应",不允许修改档案内容)。所以去污点的原则是:只去"噪声",不碰"文字"

二、两大家族:滤波 vs 图像修复(inpainting)

滤波派(中值滤波、高斯滤波、均值滤波)

思路:用邻域统计值替换每个像素,把孤立的小噪点"抹平"。

import cv2

# 中值滤波:对椒盐噪声(孤立的黑白点)效果最好
img_med = cv2.medianBlur(img, 5)   # 5x5 窗口

# 高斯滤波:对均匀噪声平滑,但会模糊边缘
img_gau = cv2.GaussianBlur(img, (5, 5), 0)

优点:速度快(300dpi A4 单页 <100ms)、无需训练、实现简单。
致命缺点:窗口越大越模糊,文字笔画也跟着糊;对面积超过几个像素的污点(霉斑往往成片)完全无效——滤波只是把污点边缘糊化,污点主体还在。

修复派(inpainting:Telea / 基于边缘的 FMM / 深度学习)

思路:把污点区域"抠出来"当蒙版,用周围有效像素的信息把空洞补起来。OpenCV 内置两种:

import cv2
import numpy as np

def inpaint_dots(img, mask):
    """mask:污点区域的二值蒙版(污点为白色255)"""
    # 方法1:Telea(基于快速行进法,默认)
    result1 = cv2.inpaint(img, mask, inpaintRadius=3, flags=cv2.INPAINT_TELEA)
    # 方法2:FMM(基于纳维-斯托克斯方程边缘传播)
    result2 = cv2.inpaint(img, mask, inpaintRadius=3, flags=cv2.INPAINT_NS)
    return result1, result2

优点:对大块污渍(霉斑、水渍边缘、胶带黑影)效果好——它是在"理解周围内容"的基础上补洞,文字笔画方向会被保留。
缺点:必须有一个准确的污点蒙版。蒙版画错(把文字当成污点),inpaint 会"补"出错误的笔画,直接改变档案内容——这是合规大忌。

三、蒙版怎么来:连通域 + 灰度特征

inpainting 成败全在蒙版。我们第一版是人工圈选污点,30 万页显然不现实。自动化蒙版生成方案:

import cv2
import numpy as np

def build_stain_mask(img_gray):
    """生成污点蒙版:深色小连通域(墨滴/虫蛀)"""
    h, w = img_gray.shape
    # 1. 反色二值化,把深色像素变成前景
    _, dark = cv2.threshold(img_gray, 80, 255, cv2.THRESH_BINARY_INV)
    # 2. 连通域分析
    n, labels, stats, _ = cv2.connectedComponentsWithStats(dark, connectivity=8)
    mask = np.zeros_like(img_gray)
    for i in range(1, n):
        area = stats[i, cv2.CC_STAT_AREA]
        bw = stats[i, cv2.CC_STAT_WIDTH]
        bh = stats[i, cv2.CC_STAT_HEIGHT]
        # 污点特征:面积小、尺寸小(不是长文本行)、
        # 且"内部平均灰度"远低于正常文字区域
        if 5 < area < 1500 and bw < 60 and bh < 60:
            # 关键一步:污点内部灰度分布要"实心"
            region = img_gray[labels == i]
            if region.mean() < 50:   # 深色实心,非文字笔画(笔画中间有亮色)
                mask[labels == i] = 255
    return mask

关键技巧:用"实心度"区分文字笔画和污点。文字笔画再粗也是"线条",笔画内部通常有反光或留白;污点是"实心团块",整体灰度均匀且深。判断 region.mean() < 50 且标准差小,能显著降低把文字误判成污点的概率。

02-挂接率验收报表

四、实测对比(老旧人事档案 500 页)

我们挑了一批典型污损档案做对比,指标分"污点清除率"和"文字破坏率"(用 OCR 在修复前后对同一批已知文本做比对,看文字有没有被改):

方案 污点清除率 文字破坏率 单页耗时
中值滤波 5x5 38% 3.5% 90ms
中值滤波 9x9 61% 12% 150ms
高斯滤波 5x5 30% 2.8% 110ms
Telea inpaint 82% 1.2% 450ms
FMM inpaint 79% 1.5% 520ms
组合:小污点用中值 + 大污点用 Telea 91% 0.9% 380ms

(数据来自 500 页实测留底,发布前请用你们自己的样本替换。)

结论非常明确:

  1. 中值滤波窗口不能无脑加大——9x9 时文字破坏率飙到 12%,等于在毁档案;
  2. inpainting 才是去污点的主力,文字破坏率反而更低(因为它理解内容,不会糊笔画);
  3. 组合方案最优:小污点(<5px)用中值滤波快速抹掉,大污点(≥5px)走 inpaint。

五、组合方案的生产实现

def clean_stains(img):
    # 1. 先生成污点蒙版(分大小)
    mask = build_stain_mask(img)
    # 2. 小污点:直接用中值滤波的结果覆盖(蒙版区域)
    med = cv2.medianBlur(img, 5)
    small_mask = cv2.erode(mask, np.ones((3,3), np.uint8))  # 缩一下,只保留小点
    img = np.where(small_mask > 0, med, img)
    # 3. 大污点:inpaint(蒙版区域)
    big_mask = cv2.dilate(mask, np.ones((5,5), np.uint8))   # 扩一下,覆盖边缘
    big_mask = cv2.subtract(big_mask, small_mask)
    img = cv2.inpaint(img, big_mask, 3, cv2.INPAINT_TELEA)
    return img

生产细节:

  • 蒙版必须回退人工复核:我们流水线里每 100 页抽 1 页,把蒙版叠加在图上人工目检,防止自动蒙版误伤文字。抽检不合格批次整体回退重新调参;
  • 水渍处理优先级:水渍往往是浅色大片区域,灰度上接近纸张,build_stain_mask 抓不到。对这类,我们用"局部方差检测"——水渍区域的纹理方差显著低于正常纸面,找到后整片做轻度 inpaint;
  • 合规底线:所有清洗操作只写入"加工层"(展示/OCR 用),原始扫描件永不覆盖。清洗参数和版本号随页记录,可追溯。
posted on 2026-08-28 11:05  程序员李铁牛  阅读(13)  评论(0)    收藏  举报