Tesseract OCR之单词识别与字符分类器
一、单词识别(Word Segmentation)
目标
将连续文本行分割成独立单词,处理两种场景:
- 固定间距文本(如打印机输出),字符宽度微小波动导致误切分
- 非固定间距文本(如手写或复杂排版),粘连字符与单词间距难以区分
解决方案与实例
(1) 固定间距文本处理
核心方法:基于字符宽度的统计学特征
def segment_fixed_pacing(blobs, width_var_threshold=0.1):char_widths = [b[2] for b in blobs] # 提取所有blob宽度avg_width = np.mean(char_widths)# 判断是否是固定间距if np.std(char_widths)/avg_width < width_var_threshold:# 均匀切分split_positions = [int((i+1)*avg_width) for i in range(len(blobs)-1)]return np.split(blobs, split_positions)else:return None # 交给非固定间距处理
案例:
输入:打印机输出的单词"OCR"
,字母宽度均为8px(轻微误差±1px)
- 计算平均宽度=8.1px,标准差=0.3 → 判定为固定间距
- 切分位置:[8, 16] → 完美分割
["O", "C", "R"]
(2) 非固定间距文本处理
核心方法:间隙分析与语言模型校验
def segment_variable_pacing(blobs, lang_model, gap_threshold=1.3):blobs.sort(key=lambda x: x[0]) # 按x坐标排序gaps = []for i in range(len(blobs)-1):gap = blobs[i+1][0] - (blobs[i][0] + blobs[i][2])normalized_gap = gap / np.mean([b[2] for b in blobs])gaps.append(normalized_gap)# 动态阈值选择if bimodal_test(gaps): # 检查间隙分布是否双峰threshold = find_valley(gaps) # 找双峰间谷底else:threshold = gap_threshold# 执行切分split_indices = [i+1 for i, g in enumerate(gaps) if g > threshold]words = np.split(blobs, split_indices)# 语言模型校验valid_words = []for word in words:candidate = "".join([recognize_char(b) for b in word])if candidate in lang_model:valid_words.append(word)elif len(word)>1: # 尝试拆分粘连字符split_blobs = split_merged_chars(word[0])valid_words.extend(split_blobs)return valid_words
案例:
输入:手写单词"hello world"
,非均匀间隙
- 计算归一化间隙序列:
[0.3, 0.4, 0.2, 1.5, 0.3...]
- 动态阈值检测到1.5为分界点 → 在第五个间隙切分
- 语言模型验证:
["hello", "world"]
有效
(3)单词分割的数学公式
切分决策=I(dgapwˉ>τ)+λ⋅I(word∈D)\text{切分决策} = \mathbb{I}\left( \frac{d_{\text{gap}}}{\bar{w}} > \tau \right) + \lambda \cdot \mathbb{I}(\text{word} \in \mathcal{D}) 切分决策=I(wˉdgap>τ)+λ⋅I(word∈D)
拆解说明
-
符号解释:
- I(⋅)\mathbb{I}(\cdot)I(⋅) :指示函数(条件成立时=1,否则=0)
- dgapd_{\text{gap}}dgap:相邻字符间的实际间隙宽度
- KaTeX parse error: Can't use function '\)' in math mode at position 9: \bar{w} \̲)̲:当前文本的平均字符宽度
- τ\tauτ:动态阈值(通常1.3~2.0)
- D\mathcal{D}D:词典集合
- λ\lambdaλ:语言模型权重(如0.5)
-
逻辑解读:
- 前半部分:如果
归一化间隙 > 阈值
,则指示函数输出1(建议切分)if (当前间隙 / 平均字符宽度) > 1.5:在此处切分 # 例如"hello|world"中的"o"和"w"之间
- 后半部分:如果候选单词在词典中存在,则指示函数输出1(反对切分)
if "helloworld" in 词典:取消切分 # 认为是一个整体单词
- 前半部分:如果
-
整体意义:
最终切分决策是几何间隙特征和语言规则的加权和。例如:- 当结果为1.2(>1)时切分
- 当结果为0.8(≤1)时不切分
类比理解
假设你在阅读模糊的手写纸条:
- 间隙分析:发现"coffee"和"time"之间空隙较大 → 可能分开
- 词典校验:但发现"coffeetime"是一个合法单词(如品牌名)→ 最终不切分
二、字符分类器(Character Classifier)
目标
将分割后的字符Blob准确分类为具体字符类别
存在问题
- 字体变形导致传统模板匹配失效
- 笔画断裂或粘连影响特征提取
解决方案与实例
(1) 多边形轮廓特征提取
几何特征工程:
- Douglas-Peucker算法简化轮廓
- 提取4维边特征:
[Δx, Δy, length, angle]
def extract_polygon_features(blob_image):contour = find_contours(blob_image)[0]epsilon = 0.02 * cv2.arcLength(contour, True)polygon = cv2.approxPolyDP(contour, epsilon, True)features = []for i in range(len(polygon)):p1 = polygon[i][0]p2 = polygon[(i+1)%len(polygon)][0]dx = p2[0] - p1[0]dy = p2[1] - p1[1]length = np.hypot(dx, dy)angle = np.arctan2(dy, dx)features.append([dx, dy, length, angle])return np.array(features)
案例:
字母"A"
的轮廓 → 简化为5条边:
- 顶边:
[0, -15, 15, -π/2]
- 右下边:
[10, 10, 14.14, π/4]
(2) 原型聚类与匹配
训练阶段:
- 对10万+样本提取特征
- K-means聚类生成500个原型特征(视觉词典)
分类阶段:
class CharClassifier:def __init__(self, prototypes, labels):self.tree = KDTree(prototypes) # 加速最近邻搜索self.labels = labels # 每个原型对应的字符def predict(self, blob):features = extract_polygon_features(blob)distances, indices = self.tree.query(features, k=1)voted_chars = [self.labels[i] for i in indices]return max(set(voted_chars), key=voted_chars.count)
案例:
输入:手写"a"
的Blob
- 匹配到最近3个原型:
[('a',0.8), ('o',0.15), ('d',0.05)]
- 投票结果:
'a'
(3) 动态分段优化
处理笔画断裂:
def adaptive_segment(features, min_len=5):total_len = sum(f[2] for f in features)seg_count = max(3, int(total_len / min_len)) # 至少分3段return np.array_split(features, seg_count)
案例:
断裂的"B"
→ 原始7段特征 → 动态重分为12段 → 提升与原型"B"
的匹配度
(4). 字符分类的数学公式
P(c∣B)∝exp(−1n∑i=1n∥fi−ϕc,i∥2)P(c|B) \propto \exp\left( -\frac{1}{n}\sum_{i=1}^n \|f_i - \phi_{c,i}\|^2 \right) P(c∣B)∝exp(−n1i=1∑n∥fi−ϕc,i∥2)
拆解说明
-
符号解释:
- P(c∣B)P(c|B)P(c∣B):给定Blob ( B ) 时属于字符类别 ( c ) 的概率
- fif_ifi:Blob的第 ( i ) 个轮廓特征(4维向量)
- ϕc,i\phi_{c,i}ϕc,i:字符 ( c ) 的第 ( i ) 个原型特征(聚类中心)
- nnn:Blob被分成的特征段数
-
计算步骤:
- 将Blob轮廓分成若干段,每段提取4维特征 fif_ifi
(例如:第一段的[Δx=5, Δy=0, length=5, angle=0]
) - 计算该特征与字符
A
的所有原型特征的欧氏距离distance = np.sqrt((5-3)**2 + (0-1)**2 + (5-4)**2 + (0-0.2)**2)
- 对所有特征段取距离平均值,再通过指数函数转化为概率
(距离越小 → 负值越大 → 指数结果越大 → 概率越高)
- 将Blob轮廓分成若干段,每段提取4维特征 fif_ifi
类比理解
比对接头暗号:
- 你手持一段断裂的密码条(Blob)
- 与密码本(原型库)中的"代号A"模板逐段比对:
- 完全匹配 → ( |f_i-\phi_{c,i}|=0 ) → 概率最高
- 部分匹配 → 距离值中等 → 概率中等
- 完全不匹配 → 距离值大 → 概率接近0
图示案例
假设识别字母"A"
:
Blob轮廓特征: [3,1,4,0.2] ← 第一段特征
原型库中"A"的特征: [3,1,4,0.2] ← 匹配!距离=0[2,1,5,0.3] ← 距离=1.1[4,0,4,0.1] ← 距离=1.02
平均距离 = (0 + 1.1 + 1.02)/3 ≈ 0.71
概率 = e^(-0.71) ≈ 0.49 → 较高概率
三、协同工作机制案例
场景:识别倾斜手写文本"Cat"
-
单词分割:
- 间隙分析发现
C
与a
间距为1.2倍均宽 → 不切分 a
与t
间距1.8倍 → 疑似单词边界- 语言模型验证
"Cat"
存在,否决切分
- 间隙分析发现
-
字符分类:
C
的轮廓匹配原型:[('C',0.7), ('G',0.2)]
a
的断裂下半部分 → 动态分段后匹配度提升至0.9- 最终输出:
"Cat"
四、关键技术创新点
环节 | 传统方法 | Tesseract方案 | 优势 |
---|---|---|---|
单词分割 | 固定阈值切分 | 动态间隙分析+语言模型校验 | 适应印刷/手写混合排版 |
特征提取 | 网格像素特征 | 自适应多边形轮廓+几何特征 | 抗缩放旋转变形 |
分类决策 | 最近邻模板匹配 | 原型聚类+分段投票机制 | 处理笔画断裂/粘连 |