简介:一份聚焦文档图像智能处理的学术论文PDF,面向机器学习、文档版面分析与OCR领域的研究者与工程人员。论文针对传统模型直接以图像作为输入导致参数量过大的问题,设计了几何、灰度、区域、纹理和内容共32维特征,并系统对比了传统机器学习、自动机器学习与深度学习方法在文档区块分类任务上的表现,为高准确率与高效率的版面分析提供了可复现的算法思路。资源为单文件PDF格式,整体仅1.9MB,便于下载与离线阅读,内容包含完整的中英文摘要、关键词、引言、方法设计与实验论证,可直接作为论文写作、课题研究或算法选型时的参考文献。目前已有153人学习下载,适合具有一定机器学习基础、需要深入理解文档图像分类特征工程与分类器对比的读者。
1. 为什么一张文档截图要单独做“区块分类”而不是直接丢给深度学习
做文档数字化、档案扫描或票据识别的同学,大概率都撞到过同一个问题:一张 A4 扫描件里可能有标题、正文、表格、印章、手写批注、插入图片,你最终要的不是“这张纸是什么”,而是“这块区域是什么、该走哪条识别管线”。如果你能先把文档图片切成若干区块,再对每个区块做分类,后续的 OCR、表格还原、印章比对就都可以分而治之。这个标题——《基于特征提取和机器学习的文档区块图像分类算法》——讲的就是这条不做深度学习的落地路线:用特征提取器把每一块图像变成数值向量,再用传统的机器学习分类器去区分它们。
为什么还要回到传统方案?因为现实项目里很多文档样本量只有几百上千张,标注成本高,深度学习模型在这种规模下很容易过拟合,而特征提取加机器学习的组合对数据量更友好、推理更快、也更容易在 CPU 上部署。适合谁?适合正在做文档结构化、票据分类、版面分析预处理,或者手里只有少量标注样本、却要把“文本/表格/图片/图形”分清的工程师。这套方案能给你一条可复现、可调参、可解释的路径,而不是一上来就堆 ResNet。
2. 特征体系怎么搭:文档区块分类的“特征提取”从哪些维度下手
2.1 先想清楚:你分类的是什么样子的图像区块
文档区块图像和自然场景图像最大的差别是“结构性强、纹理稀疏”。一张自然照片里哪怕是一小块,也有丰富的颜色渐变和物体边缘;而文档里的一个文本行区块,通常是黑字白底、行高一致、字符间隙规律。表格区块则有横竖线、单元格、行列边界。图片/照片区块有灰度起伏、局部纹理复杂。印章区块颜色饱和、边缘模糊、底色突兀。
这些差异意味着我们可以用比深度学习更便宜的特征来捕捉它们。常见的做法是把特征分成四组:
- 几何与灰度特征:区块的宽高比、面积、灰度均值、灰度方差、二值化后的前景像素占比。
- 投影特征:水平投影(每行前景像素数)、垂直投影(每列前景像素数)的分布形态,文本区块的投影往往有周期性波峰波谷,表格区块则有明显的连续竖线/横线峰值。
- 纹理特征:LBP(局部二值模式)直方图,适合区分“有密集笔画的文本”和“灰度平滑的空白/图片”。
- 形态学特征:连通域的数量、平均连通域面积、最大连通域面积与区块面积比、线条检测结果(霍夫变换找直线条数)。表格区块的直线条数远高于文本和图片。
这个思路和很多经典文档版面分析的实践一致:先用连通域或投影切出候选区块,再对每个候选区块提取多维特征向量,最后交给分类器。特征不需要多高级,关键是每一维都能稳定地区分至少两类目标。
对于图片区块,可以加一个“边缘密度”和“灰度熵”:照片的边缘方向杂乱,熵值高;文本区块的边缘方向集中在水平和垂直方向。我用 OpenCV 的 Sobel 算子算梯度幅值,再统计方向直方图,通常能给文本和图片之间拉开很大的距离。
2.2 特征向量怎么组装:先确定维度,再确定归一化方式
我常用的初始特征向量是 40~60 维。具体构成可以这样拆解:
- 基础几何特征(6 维):宽、高、宽高比、面积、周长平方比、中心点相对整页坐标的归一化位置。
- 灰度与二值化特征(8 维):灰度均值、灰度标准差、OTSU 阈值、前景像素占比、前景平均灰度、背景平均灰度、最大连通域占比、连通域数量。
- 投影特征(12 维):水平投影波峰数、波谷数、峰谷比、行高估计(相邻波峰间距均值);垂直投影同理再加列宽估计。文本区块的水平投影有明显的周期性,表格区块的垂直投影会在列边界处出现很深的谷。
- LBP 纹理特征(16 维):统一模式 LBP 直方图归一化后的主要 bin 值。纯文本的直方图集中在少数几个模式上,照片的直方图分布散。
- 边缘方向特征(8 维):Sobel 梯度幅值的水平/垂直占比、方向直方图的 8bin。
关键参数:LBP 的邻域设为 8,半径设为 1,用 uniform 模式(P=8, R=1, mapping='uniform'),这样直方图只有 59 个 bin,避免维度爆炸。归一化时全部特征做 Min-Max 到 [0,1],少数像“宽高比”这种理论上无上界的值,先做 log 压缩再做归一化。
组装特征时最常翻车的一点是“把不同量纲的特征直接拼进去”。我用 sklearn 的 StandardScaler 或 MinMaxScaler 之前,自己写了一句经验法则:凡是“越大越没边”的特征(面积、像素数、连通域数量),一律先 log1p 再进来;凡是“本来就落在 [0,1]”的特征(占比类),直接进。这样避免分类器被几个大数值特征牵着走。
import cv2 import numpy as np from skimage.feature import local_binary_pattern def extract_block_features(img_gray): h, w = img_gray.shape area = h * w # 1. 几何特征 wh_ratio = w / h if h > 0 else 0 feat = [] feat.extend([w, h, wh_ratio, area]) # 2. 灰度与二值特征 mean_gray = img_gray.mean() std_gray = img_gray.std() otsu_th, img_bin = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) fg_ratio = (img_bin == 0).sum() / area num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(img_bin, 8) max_cc_ratio = stats[1:, 4].max() / area if num_labels > 1 else 0 cc_count = num_labels - 1 feat.extend([mean_gray, std_gray, otsu_th, fg_ratio, max_cc_ratio, cc_count, np.log1p(area)]) # 3. 投影特征 hor_proj = (img_bin == 0).sum(axis=1) # 每行前景像素数 ver_proj = (img_bin == 0).sum(axis=0) # 每列前景像素数 hor_peaks = count_peaks(hor_proj) ver_peaks = count_peaks(ver_proj) feat.extend([hor_peaks, ver_peaks, hor_proj.max(), ver_proj.max()]) # 4. LBP 纹理特征 lbp = local_binary_pattern(img_gray, P=8, R=1, method='uniform') hist, _ = np.histogram(lbp.ravel(), bins=59, range=(0, 59)) hist = hist.astype(float) / (hist.sum() + 1e-6) feat.extend(hist[:16]) # 取主要 bin return np.array(feat)上面这段逻辑说明一下:先把灰度图传进来,依次算几何、灰度、投影、LBP 四组特征。count_peaks是我常用的小函数,判断一个一维序列里“波峰”的数量——阈值为序列最大值的一定比例(比如 0.3 倍),先平滑再去数峰值。文本区块的水平投影会有很多个接近等距的峰,每行一个;表格区块的垂直投影会在竖线处形成尖锐的高峰。LBP 部分只取了前 16 个 bin,后面 43 个 bin 大多对应噪声模式,对分类帮助不大,这是特征维度约束下我踩出来的取舍。这里有几个参数值得记下来:OTSU 二值化的前景以(img_bin == 0)判定(黑色为前景),这是因为扫描文档默认黑字白底;连通域用 8 连通,stats[1:, 4]是去掉背景后的各连通域面积。如果你的输入是白字黑底(比如某些截图),把前景判定反过来就行。
2.3 为什么坚持“机器学习分类器”而不是直接上 CNN
这个标题里明确写了“机器学习”,不是“深度学习”,所以选分类器的思路也顺势而为。在数据量小(每类两三百样本)的时候,随机森林、梯度提升树和 SVM 的表现通常非常接近甚至超过微调的小 CNN。原因有两点:一是树的特征交互能力强,能自动处理“宽高比大且直线多 → 表格”这类组合规则;二是 SVM 的间隔最大化在小样本上泛化更稳。
我自己的默认配置是 LightGBM,因为它快、能处理缺失值,又能直接给出特征重要性,方便我判断哪组特征拖后腿。如果你不想引入额外框架,sklearn 的 RandomForestClassifier 也能达到差不多的效果,只是速度慢一些。SVM 在这个任务上更适合做“最后调的备选”:把所有特征标准化后用 RBF 核,C 设 1.0,gamma 设scale,效果往往不错,但对特征标准化更敏感。
import pandas as pd from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report df = pd.read_csv('block_features.csv') # 每一行是一个区块样本 X = df.drop(columns=['label', 'image_id']).values y = df['label'].values X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y) clf = RandomForestClassifier(n_estimators=300, max_depth=12, min_samples_leaf=4, n_jobs=-1, random_state=42) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(classification_report(y_test, y_pred))为什么stratify=y必须写?因为文档区块的类别天然不平衡——一个版面里文本区块最多,印章或图片最少。不做分层抽样,测试集里可能一类有 50 个、另一类只有 2 个,算出来 F1 全是虚高。min_samples_leaf=4是防止树在训练集上把单个噪声样本单独分一片叶子。max_depth=12限制树的深度,既控过拟合也省训练时间。数据组织上,我一般把特征 CSV 存成image_id, block_id, 特征列..., label,这样后续做区块级分析时能回溯到具体是哪个区块被分错了。
3. 把流程跑成管线:从文档图像到区块特征 CSV 的最小复现方案
3.1 区块从哪里来:两种切割方式,按你的原始数据选
文档区块分类的前提是“已经有一批区块图”。如果原始数据本身就是人工切好的,比如从 PDF 导出时按段落切割的图片,那直接从这一步开始,给每张图打标签就好。但更常见的情况是只有整页扫描件,需要先做版面切割。
我常用的切割方式有两种,按场景选:
一是“连通域 + 投影切割”:先把整页二值化,做水平投影找到文本行分布,再做垂直投影切列,合并相邻连通域,最终得到候选区块。这个方案对“文本段落 + 表格”这类规则版式最有效。二是“固定网格 + 视觉合并”:对大版面,先把页按坐标切固定大小的格子(如 256x256),再用相邻格子的特征相似度合并。这个方案适合排版混乱、有图片穿插的页面,缺点是合并策略要手工调。
无论哪种方式,搞定区块切割后要做一步清洗:去掉面积过小的噪声区块(如单个噪点、页眉页脚的页号),再去掉面积过大的“伪区块”(如跨栏的文本框误合并)。清洗的经验阈值是:区块面积小于整页面积的 0.1% 直接丢弃;宽高比大于 20 且前景占比小于 1% 的,大概率是“整条横线”,也可以先丢掉。
我这里不贴完整切割代码,因为版面切割本身是一个比“区块分类”更大的工程,常见做法是先用 OpenCV 做形态学闭运算把相邻文字连成块,再用cv2.findContours取外接矩形。你只要记住:切割的粒度决定后续分类的上限,宁可切碎一点,也不要切出混合区块(一段文字里混进半张图片)。
3.2 特征 CSV 生成器:把图像目录一键变成样本表
下面这段我一般写成build_feature_csv.py脚本,输入是一个包含若干子目录的图片文件夹,每个子目录名就是类别标签,输出是一张特征表 CSV。这是整个流程里最值得先跑通的一步,因为后续所有调参、错误分析都靠这张表。
import os import cv2 import numpy as np import pandas as pd from glob import glob def process_folder(root_dir, out_csv='block_features.csv'): rows = [] for label in os.listdir(root_dir): label_path = os.path.join(root_dir, label) if not os.path.isdir(label_path): continue for img_path in glob(os.path.join(label_path, '*.png')) + glob(os.path.join(label_path, '*.jpg')): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: print(f'[skip] 无法读取: {img_path}') continue img = cv2.resize(img, (128, 128)) # 统一尺寸, 消除直接面积特征偏差 feat = extract_block_features(img) rows.append([os.path.basename(img_path), label] + feat.tolist()) cols = ['image_id', 'label'] + [f'f{i}' for i in range(len(feat))] df = pd.DataFrame(rows, columns=cols) df.to_csv(out_csv, index=False) print(f'写出 {out_csv}, 共 {len(df)} 个样本')这段代码里最容易忽略的是cv2.resize(img, (128, 128))。如果不做这一步,直接用原图算特征,最终分类器学到的将主要是“图片尺寸大小”而不是“区块内容类型”。比如你切出来的区块本来就是不规则尺寸,我见过很多次翻车:因为表格区块通常比文本区块宽,模型拿宽高比一锤定音,新样本里一个窄表格就被分进文本了。统一尺寸后,宽高比这个特征就失去了原始版式信息,但几何分布特征(如 LBP、投影峰谷形态)依然有效。代价是:印章这种尺寸占比小的区块,在 128x128 下特征会被稀释。折中方案是保留两组特征,一组基于原图、一组基于缩略图,但刚开始做不要贪多,先统一到 224x224 观察一段时间再说。
3.3 分类器评测怎么才算数:混淆矩阵比准确率更重要
区块分类的标签通常有“空白、文本、标题、表格、图片、图形、印章、手写”这几类,类别之间天然存在包含关系。准确率在这种任务里极有欺骗性——如果测试集里 80% 是文本区块,模型把所有样本全判成文本就有 80% 准确率。所以在 2.3 那段训练代码跑完后,我会立刻补一张混淆矩阵,按行看每个类别的召回率。
我一般会特别关注两对最容易混淆的类:
- 文本 vs 表格:单元格里的文字会被误判成文本,表格线少的细长表格会被误判成图形。
- 图片 vs 图形:照片和插画在灰度纹理特征上非常接近,经常互相串。
如果你发现混淆矩阵上这两对问题比较大,不要急着调模型,先回去看特征:文本和表格的区分,加一条“行列线检测特征”(霍夫变换检测直线数);图片和图形的区分,加一条“颜色丰富度”特征(如果原图是彩色的,统计饱和度直方图)。特征工程在数据量小的场景下永远比模型调参见效快。
4. 避坑与排查:文档区块分类最容易翻车的 5 个实战坑
4.1 现象:同一批数据训练时 F1 很高,换一批扫描件就崩
原因:特征里混入了“来源相关”的信息,最常见是 DPI 和缩放差异。你在 300 DPI 的扫描件上提取的特征,换到 150 DPI 的传真件上,LBP 和投影特征的分布完全变样。解决:在特征提取前统一分辨率,把区块图统一resize到固定尺寸,或者至少在特征 CSV 里记下来源 DPI,做分组验证。我自己的经验是 224x224 对文本和表格都比较稳,太小的图会丢失投影周期性。
4.2 现象:表格区块一半被切成“文本”、一半被切成“图形”
原因:切割环节出的问题,不是分类问题。表格的边框线在二值化后可能因为阈值不当断裂,导致连通域把表格拆成上下两段。解决:在做版面切割前,先用形态学闭运算(cv2.morphologyEx配 3x3 的核)把表格线连起来,再切区块。这个步骤一定要在切割之前做,做完分类之后你再怎么调模型都没用。
4.3 现象:灰度图下印章和图片几乎无法区分
原因:印章的颜色信息在转灰度时丢了。红章和黑字在灰度图上都是深色块,纹理也相似。解决:不要全程用灰度图。在特征提取入口对原图做一个“红色通道响应”特征——计算R - (G+B)/2的正值占比,红色区域会给出强响应。把这个特征加进去后,印章识别率通常能从 60% 提到 90% 以上。这也是这类任务里少见的“必须保留颜色信息”的场景。
4.4 现象:空白区块被误判成文本
原因:区块的灰度方差和前景占比是两个强特征,但“带浅色水印的空白”和“字很淡的文本”在二值化后表现相似。解决:在两个特征之外,加一个“笔画宽度特征”——对二值图做距离变换,统计前景像素到最近背景的距离分布,文本的笔画宽度通常集中在 2-5 像素,水印的宽度分布更散。这个特征能有效拉开二者的距离。
4.5 现象:类别不平衡导致模型几乎不预测印章类
原因:样本里印章区块可能只有几十个,文本区块有几千个。随机森林在投票时天然偏向多数类。解决:不只靠class_weight,先做样本配比设计。常见做法是训练集中每类样本数量拉平到同一数量级,可以对文本类做欠采样,对印章类做数据增强(旋转 5 度以内、轻微噪声)。真实场景里印章角度五花八门,旋转增强不是过拟合风险,而是刚需。
5. 从单区块分类到整页结构输出:后处理让模型的输出真正可用
5.1 邻域投票修正:把序贯切割的区块放在一起看
在做版面分析时,整页切出来的区块是有空间顺序的。一个被夹在上下两个“表格”标签之间的区块,被单张图模型判成“图形”,大概率是错的——因为文档里同一个区域的区块类型往往有连续性。我用的办法是做一个滑动窗口后处理:把同一页的区块按从左到右、从上到下的顺序排好,然后对每个区块的预测标签做“前后各两个邻域”的多数投票。如果某个区块与周围区块标签不一致,就把它的置信度最高的第二候补标签拿上来对比,必要时替换。
这个后处理不需要额外模型,只是利用版面本身的先验。它最大的价值是压掉那些“孤立噪声误判”——一张纯文本页面里偶尔把某个文本框判成图片,邻域投票基本可以救回来。
5.2 置信度阈值与“拒识”:宁可不分,不要分错
很多情况下,下游 OCR 或表格还原对错误标签的容忍度极低:一个表格区块被误判成文本,表格解析代码就废了。所以要给分类器加一个“可拒识”机制。我在 LightGBM 上常用的做法是:调用predict_proba,得到各类概率分布,设定一个阈值(比如 0.6)。如果最高概率低于阈值,或最高概率与第二高概率的差值小于 0.2,就把结果标记为uncertain。
对于uncertain的区块,处理策略是:先按“该区块所在页面的主要标签”兜底,再不行就走一条额外的规则管线(比如“有横线且横线超过三根 → 表格”)。这套机制看起来简单,但能把真实场景下下游任务的坏结果率往下压一个数量级。很多人忽略这一点,其实“机器学习分类器输出的置信度”本身就是一个可以拿来做工程决策的产物。
5.3 验证方法:整页还原率比单块准确率更贴近业务
单区块分类的 F1 只能说明“模型对切好的区块分得准不准”,但业务关心的是“整页文档能不能被正确结构化”。所以我的验证流程里有一个额外的指标:整页还原率。做法是取 100 张整页扫描件,对每页跑 切割 → 分类 → 后处理 全流程,然后人工判断“关键结构”是否还原正确。关键结构包括:表格是否被完整识别为表格(而不是被拆散成文本)、图片区域是否被框出、印章有没有被漏掉。
通常你会发现,单块准确率 95% 的模型,整页还原率只有 80%,因为一个页面里只要有一个关键区块被判错,整页的结构化就出问题。这就是后处理存在的理由。
5.4 进阶版:两个小分类器级联,而不是一个万能分类器
当类别数超过 6 类时,单分类器容易在“文本/表格/图片”三者边界上打架。我做过的一个有效改造是级联结构:第一个分类器只做“文本类 vs 非文本类”二分类,非文本类送进第二个分类器做“表格/图片/图形/印章/空白”细分。这样做的好处是,第一个分类器的决策边界非常简单,可以把文本类守得很死;第二个分类器不再需要为“某个东西像不像文本”操心,专注区分视觉结构。
代价是级联会累积误差——第一个分类器把文本漏掉,后面就救不回来。补救措施是:第一个二分类的阈值向“宁可将非文本判成文本”的方向偏,也就是提高文本类的召回率、牺牲一些精度;到了第二个细分阶段再处理“混进来的文本”,效果反而比单分类器直接端到端更好。这算是我在特征加机器学习方案里压箱底的一招。
6. 边界与升级:什么时候这套方案到头了、什么时候该换方案
特征提取加机器学习这套方案,上限在“特征能否表达区块之间的语义差异”上,一旦遇到两个场景,就得考虑换路。第一个场景是手写文本与印刷文本混排——手写笔画的宽度和纹理分布不稳定,LBP 和投影特征很难稳定区分,而且要区分“手写字”和“印刷字”时,人工特征基本摸不到边界。第二个场景是版面结构极不规则、区块形状乱七八糟的杂志或设计稿,几何特征失效,剩下纹理特征也不够。我一般在特征方案上做到“混合区块不能超过 10%”就不再往下钻了,超过这个数就转用简单的 CNN 微调(比如 MobileNet 的 frozen backbone + 顶层替换),或者迁移一个预训练模型回来做特征提取器。
但在大多数业务场景里——银行流水识别、物流单据分类、学术 PDF 分区——这套方案仍然能打。把特征提取和机器学习分类器作为系统里的第一个版本先上线,用结构化数据的可解释性给业务方讲清楚每个类别为什么这么分,这种“白盒”能力在项目早期评审时非常有价值。模型跑稳之后,再把难以区分的那部分样本捞出来逐步加进训练集,形成“数据积累 + 模型小步迭代”的节奏。我自己的习惯是每两周跑一次全量特征 CSV 重建,观察各特征分布的变化,再决定要不要加新特征或切模型。这个过程很朴素,但往往比一门心思追新模型更能解决生产问题。
一句话收尾:特征提取加机器学习这条老路并不过时,它在样本少、预算紧、要解释的时候反而是最佳选择。分类器只是让你能跑起来,真正决定效果的是你对文档版面本身的理解。希望这篇能对正在做文档区块分类的你有点帮助。
本文还有配套的精品资源,点击获取