从视频时间戳中训练一个专用 OCR:OpenCV + PaddleOCR + CRNN + ONNX 实战
最近我把一个和日志/视频分析相关的 OCR 训练流程单独整理成了一个 workspace。这个项目的目标很明确:从视频画面里自动截取时间戳区域,生成可训练的数据集,然后训练一个只识别时间戳格式的小型 OCR 模型,最后导出 ONNX,方便后续在其他工程里直接推理。
相比通用 OCR,这类任务的特点是字符集非常小、格式非常固定、目标区域也比较稳定。所以与其直接依赖一个大而全的 OCR 模型,不如把问题收敛成“固定时间戳识别”:先用图像规则把 ROI 找出来,再用轻量 CRNN 做序列识别。
项目结构
当前目录大致如下:
ocr/
├── README.md
├── requirements-training.txt
├── setup_training_env.sh
├── yolov8n.pt
└── pythonai/
├── orc.py
├── train_crnn.py
├── export_crnn_onnx.py
├── fix_labels.py
├── timestamp_crnn_best.onnx
├── debug_ocr/
├── runs/
├── runs_second/
└── runs_time/
几个核心文件的职责:
orc.py:从视频帧中检测黄色时间戳区域,裁剪 ROI,并生成训练样本和标签。train_crnn.py:训练 CRNN + CTC 时间戳识别模型。export_crnn_onnx.py:把 PyTorch checkpoint 导出成 ONNX。fix_labels.py:根据文件名里的毫秒时间修正labels.csv。requirements-training.txt:训练和数据采集依赖。
整体流程可以概括成:
为什么不直接用通用 OCR
这个场景识别的是类似下面这样的时间戳:
2026-05-07 16:46:59.170
训练时会归一化成:
05-07 16:46:59
也就是说,模型最终只需要识别:
ALPHABET = "0123456789-: "
字符集很小,输出格式固定,识别目标也总是视频画面中的时间戳。这种情况下,通用 OCR 的优势反而没有那么明显。专用模型的好处是:
- 训练目标更单一,模型更容易收敛;
- 字符集小,CTC 解码更稳定;
- 输入尺寸固定,部署链路更简单;
- 可以导出 ONNX,方便在桌面端、服务端或移动端集成。
数据采集:从视频中自动裁剪时间戳 ROI
orc.py 的核心思路是:先用 OpenCV 根据颜色和形态学特征找到疑似时间戳区域,再用 PaddleOCR 做一次校验,避免把其他黄色 UI 元素误收进数据集。
1. 构建黄色区域 mask
时间戳在视频里是黄色文本,所以第一步通过 HSV 和 BGR 两套规则合并 mask:
def build_yellow_mask(frame):
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
hsv_mask = cv2.inRange(
hsv,
np.array([12, 35, 80]),
np.array([70, 255, 255])
)
b, g, r = cv2.split(frame)
bgr_mask = (
(r > 110) &
(g > 110) &
(b < 210) &
((r.astype(np.int16) + g.astype(np.int16)) > (b.astype(np.int16) * 2))
).astype(np.uint8) * 255
mask = cv2.bitwise_or(hsv_mask, bgr_mask)
后面再通过开运算和横向闭运算,把碎字符连接成更完整的文本区域:
kernel_small = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel_small, iterations=1)
kernel_line = cv2.getStructuringElement(cv2.MORPH_RECT, (21, 3))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_line, iterations=2)
2. 根据几何特征筛选候选框
时间戳通常是横向长条,所以候选框过滤主要看宽度、高度、宽高比和面积:
if bw < int(w * 0.18):
continue
if bh < int(h * 0.025):
continue
if ratio < 4.5:
continue
if area < 1000:
continue
if bh > int(h * 0.20):
continue
这一步可以快速排除掉图标、按钮、高亮块等非文本目标。
3. 用 PaddleOCR 做二次验证
为了降低误采样,项目里还引入了 PaddleOCR 判断候选 ROI 是否真的像时间戳:
def is_timestamp_text(text):
text = clean_text(text)
patterns = [
r"\d{4}[-.\s]?\d{2}[-.\s]?\d{2}\s+\d{2}[:.\s]?\d{2}[:.\s]?\d{2}(?:[.:]\d{1,3})?",
r"\d{4}[-.\s]?\d{2}[-.\s]?\d{2}.*?\d{2}[:.\s]?\d{2}[:.\s]?\d{2}",
r"\d{8}.*?\d{6}",
]
这里还做了一些 OCR 常见误识别修正,比如把 O/o/Q/D 归一成 0,把 I/l/|/! 归一成 1,把 _ 归一成 -。这类清洗很实用,因为它不追求最终识别结果,而只是判断“这个区域是不是时间戳”。
4. 保存 ROI 和标签
检测成功后,代码会把 ROI 放大 2 倍,并加白边保存:
roi = cv2.resize(
roi,
None,
fx=2.0,
fy=2.0,
interpolation=cv2.INTER_CUBIC
)
roi = cv2.copyMakeBorder(
roi,
8,
8,
8,
8,
cv2.BORDER_CONSTANT,
value=[255, 255, 255]
)
标签则根据视频帧对应的 time_ms 和一个基准时间生成:
BASE_TIMESTAMP = datetime.strptime(
"2026-05-07 16:46:37.190",
"%Y-%m-%d %H:%M:%S.%f"
)
def make_label_from_video_time(time_ms):
dt = BASE_TIMESTAMP + timedelta(milliseconds=int(time_ms))
return dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
最终得到的数据集形式是:
roi_dataset/
├── images/
│ ├── xxx_frame_00000001_33ms.png
│ └── xxx_frame_00000002_66ms.png
└── labels.csv
labels.csv:
image,label
xxx_frame_00000001_33ms.png,2026-05-07 16:46:37.223
xxx_frame_00000002_66ms.png,2026-05-07 16:46:37.256
模型训练:CRNN + CTC
训练入口是 train_crnn.py。模型输入统一 resize 到:
IMG_H = 64
IMG_W = 512
标签格式会被归一化:
def normalize_label(label: str) -> str:
label = label.strip()
if "." in label:
label = label.split(".")[0]
if len(label) >= 19 and label[4] == "-":
label = label[5:]
return label
也就是:
2026-05-07 16:46:59.170
会变成:
05-07 16:46:59
这样做有两个目的:
- 年份固定时没有必要让模型学习年份;
- 毫秒对画面质量要求更高,先去掉可以降低训练难度。
CRNN 结构
模型前半部分是 CNN,用来提取图像特征;后半部分是双向 LSTM,用来建模文本序列:
self.rnn = nn.LSTM(
input_size=512 * 4,
hidden_size=256,
num_layers=2,
bidirectional=True,
batch_first=False,
)
self.fc = nn.Linear(512, num_classes)
输出交给 CTC Loss:
criterion = nn.CTCLoss(
blank=BLANK_INDEX,
zero_infinity=True
)
CTC 的好处是不用逐字符标注位置,只需要整行文本标签。对于这种固定文本行识别任务,它比检测每个字符框要简单很多。
数据增强
项目里做了几类轻量增强:
if random.random() < 0.2:
img = cv2.GaussianBlur(img, (3, 3), 0)
if random.random() < 0.2:
noise = np.random.normal(0, 5, img.shape).astype(np.uint8)
img = cv2.add(img, noise)
if random.random() < 0.3:
alpha = random.uniform(0.9, 1.15)
beta = random.randint(-10, 10)
img = cv2.convertScaleAbs(img, alpha=alpha, beta=beta)
分别模拟模糊、噪声和亮度变化。视频帧里的时间戳往往会受到压缩、缩放、抖动影响,这类增强能提升模型对真实画面的适应能力。
训练环境和启动方式
依赖文件:
torch==2.9.0
torchvision==0.24.0
tensorboard==2.20.0
opencv-python==4.10.0.84
Pillow==12.2.0
numpy==1.26.4
Flask==3.1.3
paddleocr==2.9.0
paddlepaddle==3.0.0
初始化环境:
cd ~/Ai/ocr
./setup_training_env.sh
source .venv-training/bin/activate
启动数据采集服务:
cd ~Ai/ocr/pythonai
python orc.py
采集视频帧:
curl "http://localhost:5001/collect_video_all_frames?path=/path/to/video.mp4"
查看数据集路径:
curl "http://localhost:5001/dataset_path"
开始训练:
cd ~/Ai/ocr/pythonai
python train_crnn.py
训练过程中会写入 TensorBoard 日志:
tensorboard --logdir runs
导出 ONNX
训练完成后,最佳模型会保存为:
pythonai/timestamp_crnn_best.pt
导出 ONNX:
cd ~/Ai/ocr/pythonai
python export_crnn_onnx.py
脚本会从 checkpoint 中读取:
alphabetimg_himg_wmodel.state_dict
然后导出:
pythonai/timestamp_crnn_best.onnx
导出时设置了动态 batch:
dynamic_axes={
"image": {0: "batch"},
"logits": {1: "batch"},
}
这对后续服务化推理比较友好,可以一次处理多张 ROI。
这个项目里比较关键的工程点
1. 先做 ROI,再做 OCR
不要一上来就把整张视频帧丢给识别模型。整帧里干扰太多,训练数据也难构造。先把问题拆成:
定位时间戳区域 -> 识别时间戳文本
后面的模型会简单很多。
2. 用通用 OCR 辅助造数据,而不是最终依赖它
PaddleOCR 在这里不是最终方案,而是数据采集阶段的过滤器。它负责帮我们判断候选区域是不是时间戳,最终识别还是交给专用 CRNN。
这个思路很适合小场景模型训练:先用成熟工具提高数据生产效率,再训练一个更贴合业务的轻量模型。
3. 标签格式要主动降难度
原始标签是:
2026-05-07 16:46:59.170
训练标签是:
05-07 16:46:59
去掉年份和毫秒后,模型学习目标更短,误差空间更小。如果后面数据量足够,再恢复毫秒也不迟。
4. 保留调试样本很重要
项目里有 debug_ocr/、mask_debug/、reject/ 这类目录。它们看起来像中间产物,但对排查问题很关键:
mask_debug可以看颜色阈值是否合适;reject可以看哪些帧被漏掉;images可以检查 ROI 裁剪是否稳定;labels.csv可以检查时间换算是否正确。
OCR 项目里,数据问题通常比模型问题更常见。能把中间结果保存下来,后面调参会轻松很多。
后续可以优化的方向
这个项目已经跑通了从视频采集到 ONNX 导出的闭环,后面还可以继续增强:
- 增加推理脚本,用 ONNX Runtime 验证导出模型;
- 把
BASE_TIMESTAMP改成接口参数,适配不同视频; - 对
labels.csv做去重和一致性校验; - 增加编辑距离指标,不只统计 exact match;
- 将 ROI 检测参数配置化,适配不同颜色和分辨率;
- 如果时间戳位置固定,可以加入位置先验,减少误检;
- 为训练集/验证集划分生成固定清单,保证实验可复现。
总结
这个 OCR workspace 的核心价值不是模型结构有多复杂,而是把一个具体业务问题拆成了可落地的工程流程:
视频帧 -> 黄色时间戳 ROI -> 自动标签 -> CRNN 训练 -> ONNX 导出
对于固定格式、固定区域、固定字符集的 OCR 任务,专用小模型往往比通用 OCR 更容易部署,也更容易针对业务数据持续优化。这个项目后续只要继续补齐数据清洗、ONNX 推理验证和参数配置化,就可以成为一个比较完整的时间戳识别训练工具链。
更多推荐


所有评论(0)