GLM-OCR本地部署与Python调用
·

GLM-OCR是智谱AI于2026年2月开源的专业级OCR模型,以0.9B小参数实现高精度文档解析,在多项权威评测中刷新SOTA记录。以下是核心结论及关键信息:
核心优势
-
性能领先
- 登顶文档解析榜单OmniDocBench v1.5(94.6分),超越PaddleOCR-VL-1.5(94.5分)、DeepSeek-OCR2(91.1分)等竞品。
- 细分场景优势突出:
- 表格识别(复杂结构91.5分)
- 印章识别(90.5分)
- 手写体识别(87.0分)
- 代码文档解析(84.7分)
-
场景优化深度
针对真实业务痛点优化,在以下场景表现卓越:- 合并单元格/跨页表格 → 直接输出HTML代码
- 印章遮挡文字 → 精准分离重叠内容
- 多语言混排(含竖排中文、日韩文)→ 准确率达69.3%
- 票据/卡证字段 → 结构化JSON输出(如报关单关键字段提取)
-
高效推理
- 吞吐量达 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_())
更多推荐


所有评论(0)