基于pp-ocr,从 SVTR logits 中估算字符 bbox
一般调用pp-ocr模型,我们会得到按照行输出的bbox,但是如果希望按照单字输出,就需要从SVTR logits 中估算字符 bbox。
【应用场景】
1.为了单字检测,在AIGC项目中并进行改字;
2.解决生图模型后处理中的小字崩坏问题,结合 https://github.com/limuloo/RefineAnything 这个开源项目,就可以实现对小字崩坏的修复;
3.训练模型时,为了缓解小字崩坏,在输入的时候,就可以在提示词里加入小字,此时可以进行单字检测后进行加入,并带有精确的bbox。
但是,要解决小字崩坏,核心还是要避免VAE这个结构,或许找到一种纯自回归的办法,能在根本上解决小字崩坏的问题,还没有调研实践。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
input_tensor, img_width_rec = preprocess_for_recognizer(crop_img)
print(f"Input shape: {input_tensor.shape}")
input_paddle = paddle.to_tensor(input_tensor, place=paddle.CPUPlace())
outputs = recognizer.predictor.run([input_paddle])
logits = outputs[0].numpy()
rec_result = recognizer.postprocess_op(logits)
if not rec_result:
continue
text, score = rec_result[0]
T = logits.shape[1]
print(f"logits shape: {logits.shape}, inferred W = {T * 4}, actual W = {img_width_rec}")
char_boxes_rec = get_char_boxes_from_logits(logits, text, img_width_rec, 48)
其中的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def preprocess_for_recognizer(img):
"""
复现 PaddleOCR 的 recognizer 预处理逻辑
输入: HWC uint8 BGR image (e.g., from get_rotate_crop_image)
输出: CHW float32 tensor ready for model input, and the resized width
"""
# 确保是 3 通道
if len(img.shape) == 2:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
h, w = img.shape[:2]
# 固定高度为 48,宽度按比例缩放(最小 32)
new_h = 72#48
# new_w = max(32, int(w * new_h / h))
new_w = max(48, int(w * new_h / h))
resized = cv2.resize(img, (new_w, new_h))
# 归一化: (x / 255 - 0.5) / 0.5
normalized = (resized.astype(np.float32) / 255.0 - 0.5) / 0.5
# 转 CHW + batch
chw = normalized.transpose(2, 0, 1)
input_tensor = chw[np.newaxis, :] # [1, 3, 48, new_w]
return input_tensor, new_w
def get_char_boxes_from_logits(logits, text, img_width, img_height=48):
"""
从 SVTR logits 中估算字符 bbox(无需 attention,仅用解码路径)
Args:
logits: [1, T, num_classes] - 模型原始输出
text: str - 识别出的文本
img_width: int - 裁剪图像宽度(如 320)
img_height: int - 裁剪图像高度(固定为 48)
Returns:
List[[x1, y1, x2, y2]] - 字符 bbox 列表
"""
T = logits.shape[1] # 时间步数,通常 T = img_width // 4 (e.g., 320//4=80)
# Step 1: 执行 greedy decode 并记录每个字符对应的时间步
# probs = np.softmax(logits[0], axis=-1)
x = logits[0]
x_stable = x - np.max(x, axis=-1, keepdims=True)
probs = np.exp(x_stable) / np.sum(np.exp(x_stable), axis=-1, keepdims=True)
pred_indices = np.argmax(probs, axis=-1) # [T]
# 移除 blank (index=0) 和重复
char_indices = []
prev_idx = 0
for t, idx in enumerate(pred_indices):
if idx != 0 and idx != prev_idx:
char_indices.append((idx, t))
prev_idx = idx
# 确保字符数匹配(防止解码误差)
if len(char_indices) != len(text):
# fallback: 等分
char_width = img_width / len(text)
return [[int(i * char_width), 0, int((i + 1) * char_width), img_height]
for i in range(len(text))]
# Step 2: 将时间步 t 映射到像素 x 坐标
char_boxes = []
for i, (char_idx, t) in enumerate(char_indices):
# 特征图位置 -> 像素位置(线性映射)
x_center = (t / T) * img_width
# 估算字符宽度:相邻字符中点距离
if i == 0:
next_t = char_indices[1][1] if len(char_indices) > 1 else T
char_w = ((next_t - t) / T) * img_width * 0.995
elif i == len(char_indices) - 1:
prev_t = char_indices[i - 1][1]
char_w = ((t - prev_t) / T) * img_width * 0.995
else:
prev_t = char_indices[i - 1][1]
next_t = char_indices[i + 1][1]
char_w = (((next_t - prev_t) / 2) / T) * img_width * 0.995
x1 = max(0, int(x_center - char_w / 2))
x2 = min(img_width, int(x_center + char_w / 2))
y1, y2 = 0, img_height
char_boxes.append([x1, y1, x2, y2])
return char_boxes
这样就可以估算出单个字的box了。
本文由作者按照 CC BY 4.0 进行授权