搜索文档

输入关键词,回车打开结果

基于本地部署 Qwen2-VL 的审计单据识别与 17 类凭证多模态分类引擎架构与实现

在企业年审项目中,审计人员需要处理成百上千份格式各异的原始单据,包括增值税专用发票、银行对账单、记账凭证、付款申请、出入库单等。传统的 OCR 技术依赖固定的模板匹配,一旦单据版式微调或受到拍摄阴影、折角影响,识别率便呈断崖式下跌。

为了解决这一痛点,三函代码开发了基于本地部署的多模态视觉大模型(VL)单据分类与结构化提取系统。本文将从技术架构、核心算法到 Python 实现,深度剖析这一系统的设计。

一、 系统技术架构

整个系统的输入是未经整理的混杂卷宗,通过图像预处理、大模型视觉推理和结构化验证,最终输出规范化的 Excel 底稿。

[原始卷宗] ➔ [OpenCV 畸变校正与去噪] ➔ [Qwen2-VL-7B-Instruct] ➔ [Pydantic 结构化约束] ➔ [台账回写]
  1. 预处理层:使用 OpenCV 进行图像透视变换(Deskewing)、对比度增强以及色彩空间转换,消除由于拍照产生的阴影和倾斜。
  2. 理解分类层:利用本地运行的 Qwen2-VL 模型对图像进行端到端语义理解,输出单据的逻辑分类。
  3. 结构化提取层:结合 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 的图像预处理技术,大大提升了文字的可读性。下一步,我们将探讨如何将提取出的数据进行多层台账的对账与勾稽校验。

准备好体验全能智能体了吗?

下载 OmniAgent 社区版,体验真正的本地 AI 自动化。数据安全、永久免费。