在企业年审项目中,审计人员需要处理成百上千份格式各异的原始单据,包括增值税专用发票、银行对账单、记账凭证、付款申请、出入库单等。传统的 OCR 技术依赖固定的模板匹配,一旦单据版式微调或受到拍摄阴影、折角影响,识别率便呈断崖式下跌。
为了解决这一痛点,三函代码开发了基于本地部署的多模态视觉大模型(VL)单据分类与结构化提取系统。本文将从技术架构、核心算法到 Python 实现,深度剖析这一系统的设计。
一、 系统技术架构
整个系统的输入是未经整理的混杂卷宗,通过图像预处理、大模型视觉推理和结构化验证,最终输出规范化的 Excel 底稿。
[原始卷宗] ➔ [OpenCV 畸变校正与去噪] ➔ [Qwen2-VL-7B-Instruct] ➔ [Pydantic 结构化约束] ➔ [台账回写]- 预处理层:使用 OpenCV 进行图像透视变换(Deskewing)、对比度增强以及色彩空间转换,消除由于拍照产生的阴影和倾斜。
- 理解分类层:利用本地运行的 Qwen2-VL 模型对图像进行端到端语义理解,输出单据的逻辑分类。
- 结构化提取层:结合 Schema 定义,使用 Pydantic 强类型约束输出字段,确保提取数据的高质量。
二、 OpenCV 预处理 Python 代码实现
对于手机拍摄的票据,去阴影和畸变校正是关键的第一步。以下为校正算法的核心代码:
import cv2
import numpy as np
def preprocess_image(image_path):
# 读取图像并转为灰度图
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 使用自适应阈值进行二值化,去除背景噪声与阴影
thresh = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2
)
# 边缘检测与轮廓寻找,为透视变换做准备
edges = cv2.Canny(thresh, 50, 150, apertureSize=3)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 寻找最大四边形轮廓
if contours:
c = max(contours, key=cv2.contourArea)
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.02 * peri, True)
if len(approx) == 4:
# 执行透视变换(Wrap Perspective)
pts = approx.reshape(4, 2)
rect = order_points(pts)
(tl, tr, br, bl) = rect
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
maxWidth = max(int(widthA), int(widthB))
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
maxHeight = max(int(heightA), int(heightB))
dst = np.array([
[0, 0],
[maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1],
[0, maxHeight - 1]
], dtype="float32")
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(img, M, (maxWidth, maxHeight))
return warped
return img
def order_points(pts):
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect三、 基于 Qwen2-VL 的结构化提取
我们使用 transformers 库加载本地 Qwen2-VL-7B 模型,并通过 Prompt 控制其输出 JSON 格式的结构化数据:
from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
from qwen_vl_utils import process_vision_info
import torch
import json
# 初始化模型与处理器
model = Qwen2VLForConditionalGeneration.from_pretrained(
"Qwen/Qwen2-VL-7B-Instruct",
torch_dtype=torch.bfloat16,
device_map="auto"
)
processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-7B-Instruct")
def extract_invoice_fields(image_path):
prompt = """你是一个专业的审计助手。请仔细观察这张图片,识别并提取以下发票信息,必须以 JSON 格式输出:
{
"invoice_code": "发票代码",
"invoice_number": "发票号码",
"date": "开票日期",
"amount_excluding_tax": "不含税金额(数值)",
"tax_amount": "税额(数值)",
"buyer_name": "购方名称",
"seller_name": "销方名称"
}
如果某项信息不存在,请输出 null。不要输出任何解释性文字。"""
messages = [
{
"role": "user",
"content": [
{"type": "image", "image": image_path},
{"type": "text", "text": prompt}
]
}
]
# 准备推理数据
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
image_inputs, video_inputs = process_vision_info(messages)
inputs = processor(
text=[text],
images=image_inputs,
videos=video_inputs,
padding=True,
return_tensors="pt"
).to("cuda")
# 生成输出
with torch.no_grad():
generated_ids = model.generate(**inputs, max_new_tokens=512)
generated_ids_trimmed = [
out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
]
output_text = processor.batch_decode(
generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
)[0]
try:
data = json.loads(output_text)
return data
except Exception as e:
print("解析输出失败:", output_text)
return None四、 总结
通过在本地部署多模态视觉大模型,我们解决了传统 OCR 在非标准化单据识别中的性能瓶颈,结合 OpenCV 的图像预处理技术,大大提升了文字的可读性。下一步,我们将探讨如何将提取出的数据进行多层台账的对账与勾稽校验。