GLM-OCR是智谱AI于2026年2月开源的专业级OCR模型,以0.9B小参数实现高精度文档解析,在多项权威评测中刷新SOTA记录。以下是核心结论及关键信息:

核心优势

  1. 性能领先

    • 登顶文档解析榜单OmniDocBench v1.5(94.6分),超越PaddleOCR-VL-1.5(94.5分)、DeepSeek-OCR2(91.1分)等竞品。
    • 细分场景优势突出:
      • 表格识别(复杂结构91.5分)
      • 印章识别(90.5分)
      • 手写体识别(87.0分)
      • 代码文档解析(84.7分)
  2. 场景优化深度
    针对真实业务痛点优化,在以下场景表现卓越:

    • 合并单元格/跨页表格 → 直接输出HTML代码
    • 印章遮挡文字 → 精准分离重叠内容
    • 多语言混排(含竖排中文、日韩文)→ 准确率达69.3%
    • 票据/卡证字段 → 结构化JSON输出(如报关单关键字段提取)
  3. 高效推理

    • 吞吐量达 1.86页/秒(PDF)0.67张/秒(图片),速度达PaddleOCR-VL-1.5的1.5倍。
    • 显存占用<3GB,支持边缘设备部署。

技术突破

  • 架构设计
    采用“CogViT视觉编码器(400M)+跨模态连接层+GLM-0.5B解码器”三级架构,通过4倍下采样压缩视觉Token,提升信息传递效率。
  • 训练创新
    • 引入多Tokens预测损失(MTP),增强长文本依赖学习。
    • 全任务强化学习优化复杂场景鲁棒性(如潦草手写体)。
  • 两阶段流程
    先通过PP-DocLayoutV3分析版面,再并行识别各区域,提升复杂文档处理效率。

使用方式对比

部署方式 适用场景 注意事项
云端API 快速验证/小规模应用 0.2元/百万Tokens,需Base64编码本地文件
VLLM/SGLang 高并发生产环境 需配置Python环境,支持投机解码加速
Ollama MacOS轻量部署(MLX优化) 当前版本(v0.15.5)需拖拽图片路径

我们使用的方式是通过ollama进行本地部署,通过python与pyq5构建gui界面调用ollama的api接口。

import sys
import subprocess
import re
import base64
import json
import requests
import io
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton,
    QLabel, QFileDialog, QLineEdit, QTextEdit, QHBoxLayout, QSplitter, QMessageBox
)
from PyQt5.QtGui import QPixmap, QPalette, QColor
from PyQt5.QtCore import Qt, QThread, pyqtSignal

try:
    from PIL import Image
except ImportError:
    print("错误: 需要安装Pillow库来处理图片")
    print("请运行: pip install Pillow")
    sys.exit(1)


class OCRThread(QThread):
    """后台线程执行OCR识别,避免界面卡顿"""
    finished = pyqtSignal(str, str)  # 结果, 状态
    error = pyqtSignal(str)
    
    def __init__(self, prompt, image_path):
        super().__init__()
        self.prompt = prompt
        self.image_path = image_path
    
    def image_to_base64(self, image_path):
        """将图片转换为base64编码,并进行压缩优化"""
        try:
            from PIL import Image
            
            # 打开图片
            with Image.open(image_path) as img:
                # 转换为RGB模式(移除alpha通道)
                if img.mode in ('RGBA', 'LA', 'P'):
                    background = Image.new('RGB', img.size, (255, 255, 255))
                    if img.mode == 'P':
                        img = img.convert('RGBA')
                    if img.mode == 'RGBA':
                        background.paste(img, mask=img.split()[-1])
                    else:
                        background.paste(img)
                    img = background
                elif img.mode != 'RGB':
                    img = img.convert('RGB')
                
                # 调整图片尺寸(如果太大)
                max_width = 1024
                max_height = 1024
                
                if img.width > max_width or img.height > max_height:
                    # 计算缩放比例
                    scale = min(max_width / img.width, max_height / img.height)
                    new_width = int(img.width * scale)
                    new_height = int(img.height * scale)
                    
                    print(f"图片尺寸过大 ({img.width}x{img.height}),调整为 ({new_width}x{new_height})")
                    img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
                
                # 调整图片质量以减小文件大小
                buffer = io.BytesIO()
                # 使用JPEG格式(比PNG更小)
                img.save(buffer, format='JPEG', quality=85, optimize=True)
                buffer.seek(0)
                
                # 转换为base64
                return base64.b64encode(buffer.read()).decode('utf-8')
        except Exception as e:
            raise Exception(f"处理图片失败: {str(e)}")
    
    def run(self):
        try:
            # 检查Ollama服务是否可用
            try:
                response = requests.get("http://localhost:11434/api/tags", timeout=5)
                if response.status_code != 200:
                    self.error.emit("Ollama服务未响应")
                    return
            except requests.exceptions.ConnectionError:
                self.error.emit("无法连接到Ollama服务\n请确保Ollama正在运行(端口11434)")
                return
            except Exception as e:
                self.error.emit(f"连接Ollama失败: {str(e)}")
                return
            
            # 将图片转为base64
            try:
                image_base64 = self.image_to_base64(self.image_path)
                # 检查base64长度(调试信息)
                print(f"图片base64长度: {len(image_base64)} 字符")
            except Exception as e:
                self.error.emit(f"图片处理失败: {str(e)}")
                return
            
            # 构建API请求数据
            payload = {
                "model": "glm-ocr",
                "prompt": self.prompt,
                "images": [image_base64],
                "stream": False,
                "options": {
                    "temperature": 0.1  # 降低随机性,提高准确性
                }
            }
            
            # 发送API请求
            print(f"发送请求到: http://localhost:11434/api/generate")
            print(f"请求payload大小: {len(json.dumps(payload))} 字节")
            
            response = requests.post(
                "http://localhost:11434/api/generate",
                json=payload,
                timeout=300  # 5分钟超时
            )
            
            print(f"响应状态码: {response.status_code}")
            
            if response.status_code == 200:
                result = response.json()
                if "response" in result:
                    self.finished.emit(result["response"], "success")
                else:
                    error_msg = "API返回数据格式错误:缺少response字段"
                    if "error" in result:
                        error_msg += f"\n错误: {result['error']}"
                    self.error.emit(error_msg)
            else:
                error_msg = f"API请求失败 (状态码: {response.status_code})"
                try:
                    error_detail = response.json()
                    if "error" in error_detail:
                        error_msg += f"\n{error_detail['error']}"
                except:
                    error_msg += f"\n{response.text}"
                self.error.emit(error_msg)
                
        except requests.exceptions.Timeout:
            self.error.emit("请求超时(超过5分钟)")
        except Exception as e:
            self.error.emit(f"执行错误: {str(e)}")
            print(f"异常详情: {str(e)}")


class GLMOCRApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("GLM-OCR 识别工具")
        self.setMinimumSize(1000, 600)
        self.resize(1200, 700)
        
        # 初始化OCR线程
        self.ocr_thread = None
        
        # ui风格
        self.set_apple_style()

        # 主布局
        central_widget = QWidget()
        main_layout = QVBoxLayout(central_widget)
        main_layout.setSpacing(0)
        main_layout.setContentsMargins(0, 0, 0, 0)

        # 顶部工具栏
        toolbar = self.create_toolbar()
        toolbar.setFixedHeight(50)
        main_layout.addWidget(toolbar)

        # 主体内容 - 水平三分布局
        splitter = QSplitter(Qt.Horizontal)
        splitter.setStyleSheet("QSplitter::handle { background-color: #E5E5EA; }")
        
        # 左侧 - 图片预览
        left_widget = QWidget()
        left_layout = QVBoxLayout(left_widget)
        left_layout.setSpacing(0)
        left_layout.setContentsMargins(0, 0, 0, 0)
        
        self.image_label = QLabel("图片预览区域\n\n点击上方 '选择图片' 按钮加载图片")
        self.image_label.setAlignment(Qt.AlignCenter)
        self.image_label.setStyleSheet("background-color: white; color: #999999; font-size: 16px;")
        left_layout.addWidget(self.image_label)
        
        splitter.addWidget(left_widget)
        
        # 中间 - 提示词输入(放大区域)
        middle_widget = QWidget()
        middle_layout = QVBoxLayout(middle_widget)
        middle_layout.setSpacing(0)
        middle_layout.setContentsMargins(0, 0, 0, 0)
        
        # 标签区域(固定高度)
        self.prompt_label = QLabel("提示词")
        self.prompt_label.setStyleSheet("padding: 15px; font-size: 14px; font-weight: 500; border-bottom: 1px solid #E5E5EA;")
        self.prompt_label.setFixedHeight(45)
        middle_layout.addWidget(self.prompt_label)
        
        # 提示词输入框(占据更多空间)
        self.prompt_input = QTextEdit()
        self.prompt_input.setPlaceholderText("输入提示词,如:\nText Recognition:\n\n或JSON Schema:\n{'id_number': '', 'name': ''}")
        self.prompt_input.setStyleSheet("""
            background-color: white;
            border: none;
            padding: 15px;
            font-size: 14px;
            font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
        """)
        self.prompt_input.setAcceptRichText(False)
        self.prompt_input.setAlignment(Qt.AlignLeft)
        # 设置最小高度,让输入框有足够空间
        self.prompt_input.setMinimumHeight(250)
        middle_layout.addWidget(self.prompt_input)
        
        # 按钮区域(固定高度)
        button_container = QWidget()
        button_layout = QHBoxLayout(button_container)
        button_layout.setContentsMargins(15, 10, 15, 10)
        
        execute_button = QPushButton("执行识别")
        execute_button.setStyleSheet("""
            background-color: #34C759;
            color: white;
            border: none;
            padding: 12px;
            border-radius: 6px;
            font-size: 14px;
            font-weight: 500;
        """)
        execute_button.clicked.connect(self.run_ocr)
        button_layout.addWidget(execute_button)
        
        middle_layout.addWidget(button_container)
        
        splitter.addWidget(middle_widget)
        
        # 右侧 - 结果显示
        right_widget = QWidget()
        right_layout = QVBoxLayout(right_widget)
        right_layout.setSpacing(0)
        right_layout.setContentsMargins(0, 0, 0, 0)
        
        self.result_label = QLabel("识别结果")
        self.result_label.setStyleSheet("padding: 15px; font-size: 14px; font-weight: 500; border-bottom: 1px solid #E5E5EA;")
        self.result_label.setFixedHeight(45)
        right_layout.addWidget(self.result_label)
        
        self.result_display = QTextEdit("识别结果将显示在这里...")
        self.result_display.setReadOnly(True)
        self.result_display.setStyleSheet("background-color: white; border: none; padding: 15px; font-size: 13px; color: #999999;")
        right_layout.addWidget(self.result_display)
        
        splitter.addWidget(right_widget)
        
        splitter.setStretchFactor(0, 4)
        splitter.setStretchFactor(1, 2)
        splitter.setStretchFactor(2, 4)
        
        main_layout.addWidget(splitter)
        self.setCentralWidget(central_widget)

        # 初始化变量
        self.selected_file = None

    def create_toolbar(self):
        toolbar = QWidget()
        toolbar.setStyleSheet("background-color: #F2F2F7; border-bottom: 1px solid #E5E5EA;")
        layout = QHBoxLayout(toolbar)
        layout.setSpacing(15)
        layout.setContentsMargins(20, 0, 20, 0)
        
        # 左侧 - 文件选择
        self.file_label = QLabel("未选择文件")
        self.file_label.setStyleSheet("font-size: 13px; color: #666666;")
        layout.addWidget(self.file_label)
        
        self.file_button = QPushButton("选择图片")
        self.file_button.setFixedHeight(32)
        self.file_button.clicked.connect(self.select_file)  # 连接按钮点击事件
        layout.addWidget(self.file_button)
        
        layout.addStretch()
        
        # 右侧 - 状态显示
        self.status_label = QLabel("等待连接")
        self.status_label.setStyleSheet("font-size: 13px; color: #666666;")
        layout.addWidget(self.status_label)
        
        return toolbar

    def set_apple_style(self):
        palette = QPalette()
        palette.setColor(QPalette.Window, QColor(242, 242, 247))
        palette.setColor(QPalette.WindowText, QColor(0, 0, 0))
        palette.setColor(QPalette.Base, QColor(255, 255, 255))
        palette.setColor(QPalette.Text, QColor(0, 0, 0))
        self.setPalette(palette)
        
        self.setStyleSheet("""
            QMainWindow {
                background-color: #f2f2f7;
            }
            QPushButton {
                background-color: #007AFF;
                color: white;
                border: none;
                padding: 0px 16px;
                border-radius: 6px;
                font-size: 13px;
                font-weight: 500;
            }
            QPushButton:hover {
                background-color: #0066CC;
            }
            QLabel {
                color: #000000;
            }
            QLineEdit {
                color: #000000;
            }
            QTextEdit {
                color: #000000;
            }
        """)

    def select_file(self):
        """选择图片文件并显示预览"""
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.tiff);;所有文件 (*.*)"
        )
        
        if file_path:
            try:
                self.selected_file = file_path
                self.file_label.setText(f"已选择: {file_path}")
                
                # 显示图片预览
                pixmap = QPixmap(file_path)
                if not pixmap.isNull():
                    # 清除文字,显示图片
                    self.image_label.setText("")
                    # 自适应缩放
                    scaled_pixmap = pixmap.scaled(
                        self.image_label.width(),
                        self.image_label.height(),
                        Qt.KeepAspectRatio,
                        Qt.SmoothTransformation
                    )
                    self.image_label.setPixmap(scaled_pixmap)
                    self.update_status("图片已加载", "normal")
                else:
                    self.image_label.setText("无法加载图片\n\n请检查文件格式")
                    self.update_status("加载失败", "error")
                    self.selected_file = None
            except Exception as e:
                self.image_label.setText(f"加载错误\n\n{str(e)}")
                self.update_status("加载错误", "error")
                self.selected_file = None
                print(f"加载图片错误: {e}")  # 调试信息

    def resizeEvent(self, event):
        super().resizeEvent(event)
        # 窗口大小改变时更新图片显示
        if self.selected_file and self.image_label.pixmap():
            pixmap = QPixmap(self.selected_file)
            if not pixmap.isNull():
                scaled_pixmap = pixmap.scaled(
                    self.image_label.width(),
                    self.image_label.height(),
                    Qt.KeepAspectRatio,
                    Qt.SmoothTransformation
                )
                self.image_label.setPixmap(scaled_pixmap)

    def run_ocr(self):
        """执行OCR识别(使用后台线程)"""
        if not self.selected_file:
            self.update_status("请先选择图片文件", "error")
            QMessageBox.warning(self, "警告", "请先选择图片文件!")
            return

        prompt = self.prompt_input.toPlainText().strip()
        if not prompt:
            self.update_status("请输入提示词", "error")
            QMessageBox.warning(self, "警告", "请输入提示词!")
            return

        # 更新UI状态
        self.update_status("识别中...", "processing")
        self.result_display.clear()
        self.result_display.setText("正在识别,请稍候...")
        self.result_display.setStyleSheet("background-color: white; border: none; padding: 15px; font-size: 13px; color: #007AFF;")
        
        # 禁用执行按钮,防止重复点击
        self.set_buttons_enabled(False)
        
        # 创建并启动OCR线程
        self.ocr_thread = OCRThread(prompt, self.selected_file)
        self.ocr_thread.finished.connect(self.on_ocr_finished)
        self.ocr_thread.error.connect(self.on_ocr_error)
        self.ocr_thread.start()

    def on_ocr_finished(self, result, status):
        """OCR识别完成的回调"""
        self.result_display.setText(result)
        self.result_display.setStyleSheet("background-color: white; border: none; padding: 15px; font-size: 13px; color: #000000;")
        self.update_status("识别完成", "success")
        self.set_buttons_enabled(True)
        
        # 自动滚动到顶部
        self.result_display.verticalScrollBar().setValue(0)

    def on_ocr_error(self, error_msg):
        """OCR识别出错的回调"""
        self.result_display.setText(f"错误: {error_msg}")
        self.result_display.setStyleSheet("background-color: white; border: none; padding: 15px; font-size: 13px; color: #FF3B30;")
        self.update_status("识别失败", "error")
        self.set_buttons_enabled(True)
        
        # 显示错误对话框
        QMessageBox.critical(self, "错误", error_msg)

    def set_buttons_enabled(self, enabled):
        """启用/禁用所有按钮"""
        self.file_button.setEnabled(enabled)
        # 禁用提示词区域的执行按钮
        middle_widget = self.findChild(QWidget)
        if middle_widget:
            for child in middle_widget.findChildren(QPushButton):
                if child.text() == "执行识别":
                    child.setEnabled(enabled)

    def update_status(self, text, status_type):
        """统一更新状态标签"""
        self.status_label.setText(text)
        if status_type == "success":
            self.status_label.setStyleSheet("font-size: 13px; color: #34C759; font-weight: 500;")
        elif status_type == "processing":
            self.status_label.setStyleSheet("font-size: 13px; color: #007AFF; font-weight: 500;")
        elif status_type == "error":
            self.status_label.setStyleSheet("font-size: 13px; color: #FF3B30; font-weight: 500;")
        else:
            self.status_label.setStyleSheet("font-size: 13px; color: #666666; font-weight: 500;")


if __name__ == "__main__":
    # 检查依赖
    try:
        import requests
        from PIL import Image
    except ImportError as e:
        missing = str(e).split()[-1]
        print(f"错误: 缺少依赖库 '{missing}'")
        print("请运行以下命令安装:")
        print("  pip install requests Pillow")
        sys.exit(1)
    
    # 检查PIL版本
    try:
        pil_version = Image.__version__
        print(f"Pillow版本: {pil_version}")
    except:
        print("警告: 无法检查Pillow版本")
    
    app = QApplication(sys.argv)
    
    # 检查Ollama是否可用
    try:
        result = subprocess.run(["ollama", "list"], capture_output=True, text=True, timeout=10)
        if result.returncode != 0:
            QMessageBox.warning(None, "警告", "Ollama未安装或未启动!\n\n请先安装并启动Ollama服务,然后下载glm-ocr模型:\nollama run glm-ocr")
    except Exception as e:
        QMessageBox.critical(None, "错误", f"无法连接到Ollama服务:\n{str(e)}\n\n请确保Ollama已安装并运行。\n\n安装步骤:\n1. 访问 https://ollama.ai 下载安装Ollama\n2. 运行命令: ollama run glm-ocr")
        sys.exit(1)
    
    window = GLMOCRApp()
    window.show()
    sys.exit(app.exec_())

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐