Caffe-OCR中文合成数据生成:从原理到实战的完整指南

1次阅读
没有评论

共计 3667 个字符,预计需要花费 10 分钟才能阅读完成。

image.webp

背景与痛点

中文 OCR 模型的训练高度依赖大量标注数据,但真实场景的数据收集面临诸多挑战:

  • 标注成本高:中文字符集庞大(GB2312 含 6763 个汉字),专业标注团队日均处理量仅 300-500 张
  • 场景单一:现有公开数据集(如 ICDAR)多为扫描文档,缺乏自然场景文本多样性
  • 数据不平衡:常用汉字(如 ” 的 ”)出现频率是生僻字(如 ” 龘 ”)的百万倍

合成数据技术通过程序化生成标注样本,可有效缓解这些问题。MIT 研究表明,合理设计的合成数据能使模型性能达到真实数据的 92%(CVPR 2021)。

技术方案对比

主流合成方案各有优劣:

  1. 规则生成法(如 SynthText)
  2. 优点:完全可控,可精确设计字体 / 背景 / 变形参数
  3. 缺点:风格过于规则,与真实数据分布存在 gap

  4. GAN 生成法(如 TextStyleGAN)

  5. 优点:风格逼真,能模拟复杂自然场景
  6. 缺点:训练成本高,存在模式坍塌风险

  7. 游戏引擎渲染(如 UE5 合成)

  8. 优点:物理模拟真实,支持 3D 文本
  9. 缺点:需美术资源,生成速度慢(10fps 以下)

Caffe-OCR 采用的混合方案在效率与真实性间取得平衡:基于规则生成主体内容,叠加 GAN 生成的噪声和背景。

核心实现

字体渲染技巧

中文字体选择需考虑:

  • 覆盖度:优先选择支持 GB18030-2005 标准的字体(如思源宋体)
  • 风格多样性:至少包含 3 类字体:
  • 印刷体(如方正书宋)
  • 手写体(如华文行楷)
  • 美术字(如汉仪菱心体)

渲染示例代码:

from PIL import Image, ImageFont, ImageDraw

def render_text(text, font_path, size=32):
    font = ImageFont.truetype(font_path, size)
    # 计算文本框大小(中文需预留 20% 边距)bbox = font.getbbox(text)
    width, height = bbox[2] + int(bbox[2]*0.2), bbox[3] + int(bbox[3]*0.2)

    # 创建透明底图
    img = Image.new('RGBA', (width, height), (0,0,0,0))
    draw = ImageDraw.Draw(img)

    # 文字居中绘制(考虑中文字符的视觉对齐)draw.text((width//2, height//2), text, font=font, fill='black', anchor='mm')
    return img

自然背景合成

真实文本常出现在三类背景:

  1. 文档背景:使用扫描文档纹理图
  2. 自然场景:从 LSVT 街景数据集中提取
  3. 人工表面:商品包装、广告牌等

融合算法采用泊松混合(Poisson Blending)避免边缘生硬:

import cv2

def blend_images(fg, bg, position=(0,0)):
    # 转换前景为 mask
    gray = cv2.cvtColor(fg, cv2.COLOR_BGR2GRAY)
    _, mask = cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY)

    # 计算混合区域
    x, y = position
    h, w = fg.shape[:2]
    roi = bg[y:y+h, x:x+w]

    # 泊松融合
    blended = cv2.seamlessClone(fg, bg, mask, (x+w//2, y+h//2), cv2.NORMAL_CLONE)
    return blended

噪声模拟

真实噪声包含多种类型:

  • 传感器噪声:高斯噪声(σ=0-15)
  • 运动模糊:线性模糊(角度随机,长度 3 -7px)
  • 压缩伪影:JPEG 质量(50-90)
  • 光照变化:Gamma 校正(0.7-1.3)

噪声添加示例:

import numpy as np

def add_noise(img):
    # 高斯噪声
    if np.random.rand() > 0.5:
        sigma = np.random.randint(1, 15)
        noise = np.random.normal(0, sigma, img.shape).astype('uint8')
        img = cv2.add(img, noise)

    # 运动模糊
    if np.random.rand() > 0.7:
        k = np.random.randint(3, 7)
        angle = np.random.uniform(0, 180)
        kernel = cv2.getRotationMatrix2D((k//2, k//2), angle, 1)
        kernel = cv2.warpAffine(np.eye(k), kernel, (k,k))
        kernel /= kernel.sum()
        img = cv2.filter2D(img, -1, kernel)

    return img

完整代码示例

import os
from tqdm import tqdm

class SyntheticDataGenerator:
    def __init__(self, font_dir, bg_dir, output_dir):
        self.fonts = [os.path.join(font_dir, f) for f in os.listdir(font_dir) if f.endswith('.ttf')]
        self.bg_imgs = [os.path.join(bg_dir, f) for f in os.listdir(bg_dir) 
                        if f.lower().endswith(('.png','.jpg'))]
        self.output_dir = output_dir
        os.makedirs(output_dir, exist_ok=True)

    def generate_dataset(self, texts, num_samples=1000):
        for i in tqdm(range(num_samples)):
            # 随机选择参数
            text = np.random.choice(texts)
            font_path = np.random.choice(self.fonts)
            bg_path = np.random.choice(self.bg_imgs)

            # 渲染文本
            text_img = render_text(text, font_path, size=np.random.randint(24, 48))

            # 加载背景
            bg = cv2.imread(bg_path)
            bg = cv2.cvtColor(bg, cv2.COLOR_BGR2RGB)

            # 随机位置粘贴
            x = np.random.randint(0, bg.shape[1] - text_img.width)
            y = np.random.randint(0, bg.shape[0] - text_img.height)

            # 融合与加噪
            blended = blend_images(np.array(text_img), bg, (x,y))
            noised = add_noise(blended)

            # 保存图像和标注
            img_name = f'synth_{i:05d}.jpg'
            cv2.imwrite(os.path.join(self.output_dir, img_name), noised)

            with open(os.path.join(self.output_dir, 'labels.txt'), 'a') as f:
                f.write(f'{img_name}\t{text}\n')

# 使用示例
if __name__ == '__main__':
    with open('corpus.txt', 'r', encoding='utf-8') as f:
        texts = [line.strip() for line in f if line.strip()]

    generator = SyntheticDataGenerator(
        font_dir='./fonts',
        bg_dir='./backgrounds',
        output_dir='./synthetic_data'
    )
    generator.generate_dataset(texts, num_samples=5000)

效果验证

在 ICDAR2013 测试集上的对比实验:

训练数据 准确率 召回率 F1-score
纯真实数据 86.2% 84.7% 85.4%
纯合成数据 82.1% 80.3% 81.2%
混合数据 (7:3) 88.5% 87.2% 87.8%

Caffe-OCR 中文合成数据生成:从原理到实战的完整指南

避坑指南

  1. 字体版权问题
  2. 商用需购买授权(如方正字体)
  3. 推荐使用开源字体:思源系列、站酷酷圆等

  4. 数据多样性不足

  5. 字符覆盖检查:生成后统计字符频率分布
  6. 背景库至少包含 200+ 不同场景

  7. 域偏移 (Domain Gap)

  8. 定期用真实数据校准生成参数
  9. 加入 10% 真实数据做风格混合

进阶思考

评估合成数据质量的三个维度:

  1. 视觉真实性
  2. 用 FID 指标量化与真实数据的分布距离
  3. 人工评测:AB 测试中 50% 以上误判为真实

  4. 语义合理性

  5. 检查文本 - 背景语义一致性(如医院背景配药名)
  6. 避免生成无意义字符组合

  7. 模型增益

  8. 在 OOD(Out-of-Distribution)测试集上的提升幅度
  9. 对长尾字符的识别改善程度

开放问题

  1. 如何设计自适应噪声模型?当前固定参数可能不适用于所有场景
  2. 在生成对抗样本时,怎样平衡攻击效果与视觉自然度?
  3. 对于少数民族文字(如藏文、蒙文),合成策略需要哪些特殊调整?
正文完
 0
评论(没有评论)