视觉模型与多模态理解

更新时间: 2026-04-15 14:57:33

# CSAE Qwen-VL-本地图片

import json
import dashscope
from dashscope.api_entities.dashscope_response import Role
from dashscope import MultiModalConversation

dashscope.api_key = 'sk-cd87191136be484d92f1ac53551440f1'

local_file_path = 'file://2-Japanese-document-extraction.jpg'

messages = [{
    'role': 'system',
    'content': [{
        'text': 'You are a helpful assistant.'
    }]
},{
    'role': 'user',
    'content': [
        {
            'image': local_file_path
        },
        {
            'text': '图片里有什么东西?'
        }
    ]
}]

response = MultiModalConversation.call(model='qwen-vl-plus', messages= messages)

print(response)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# InternVideo2/2.5(视频理解SOTA)

InternVideo2,新型的视频基础模型(ViFM),在视频识别、视频文本任务和对话任务中取得了SOTA。

InternVideo2的核心设计是渐进式训练方法,统一了掩蔽视频建模、跨模态对比学习和下一个token预测,将视频编码器的规模扩大到60亿参数。在数据层面,通过语义分割视频并生成视频-音频-语音字幕,优先考虑时空一致性,从而提高了视频和文本之间的对齐。

InternVideo2的三个训练阶段:

  1. 通过未掩蔽重建捕获时空结构,
  2. 与其他模态的语义对齐,
  3. 通过下一个token预测增强其开放式对话能力。

渐进式训练方法:通过三个阶段依次深入理解视频内容:

  • 无监督结构学习:200万视频数据无标注训练,让AI自主掌握时空关系
  • 多模态对齐:对齐视频-音频-文本三种模态(如10秒视频片段与生成字幕对齐)
  • 对话能力增强:通过QA任务训练开放式对话能力(如视频内容问答)

不同的训练阶段将引导该模型通过不同的前置任务捕捉不同层次的结构和语义信息。

InternVideo2在超过60个视频和音频任务上取得了SOTA。模型在各种视频相关的字幕、对话和长视频理解基准测试中表现优异,凸显了其在推理和理解长时间上下文方面的能力。

Kinetics-400是一个大规模的视频数据集,专门用于动作识别和理解的研究。它由Google DeepMind创建。Kinetics-400包含了大量的视频剪辑,每个剪辑大约持续10秒钟,涵盖了400种不同的动作类别,比如日常生活活动(如做饭、跳舞)、体育运动(如踢足球、打篮球)、社交互动(如握手)等。

# InternVideo2预训练

对于InternVideo2的训练,强调数据中的时空一致性和标签质量。

数据集包含4.02亿数据条目,其中包括200万个视频、5000万个视频-文本对(来自WebVid和InternVid)、5000万个视频-音频-语音-文本对(InternVid2)和3亿个图像-文本对。

对于InternVid2,我们将视频语义分割成剪辑,并专注于使用三种模态:音频、视频和语音重新校准剪辑描述。我们首先为这三种模态分别生成字幕。然后,单独的字幕被融合在一起,用于创建一个更全面的描述。

# 视频多模态注释框架VidCap

# CASE 汽车剐蹭视频理解

"""
属于 InternVL 2.5系列
视频理解与生成:可以用于视频内容的分析、总结和生成相关的文本描述。
视觉问答:能够回答与图像或视频内容相关的问题。
多模态对话:支持与用户进行包含视觉信息的对话。
"""


# In[2]:


# 模型下载,需要下载3个大模型
from modelscope import snapshot_download

model_dir = snapshot_download('OpenGVLab/InternVideo2_5_Chat_8B', cache_dir='/root/autodl-tmp/models')
# model_dir = snapshot_download('internlm/internlm2_5-7b-chat', cache_dir='/root/autodl-tmp/models')
# model_dir = snapshot_download('LLM-Research/Mistral-7B-Instruct-v0.3', cache_dir='/root/autodl-tmp/models')
# model_dir = snapshot_download('AI-ModelScope/bert-base-uncased', cache_dir='/root/autodl-tmp/models')


# In[1]:


# 导入必要的库
import numpy as np
import torch
import torchvision.transforms as T
from decord import VideoReader, cpu
from PIL import Image
from torchvision.transforms.functional import InterpolationMode
from modelscope import AutoModel, AutoTokenizer


# 模型配置
model_path = '/root/autodl-tmp/models/OpenGVLab/InternVideo2_5_Chat_8B'

# 初始化分词器和模型
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModel.from_pretrained(model_path, trust_remote_code=True).half().cuda().to(torch.bfloat16)

# ImageNet 数据集的均值和标准差
IMAGENET_MEAN = (0.485, 0.456, 0.406)
IMAGENET_STD = (0.229, 0.224, 0.225)

def build_transform(input_size):
    """
    构建图像转换pipeline
    
    参数:
        input_size: 输入图像大小
    
    返回:
        transform: 转换pipeline
    """
    MEAN, STD = IMAGENET_MEAN, IMAGENET_STD
    transform = T.Compose([
        T.Lambda(lambda img: img.convert("RGB") if img.mode != "RGB" else img), 
        T.Resize((input_size, input_size), interpolation=InterpolationMode.BICUBIC), 
        T.ToTensor(), 
        T.Normalize(mean=MEAN, std=STD)
    ])
    return transform


def find_closest_aspect_ratio(aspect_ratio, target_ratios, width, height, image_size):
    """
    寻找最接近原始图像宽高比的目标比例
    
    参数:
        aspect_ratio: 原始图像的宽高比
        target_ratios: 目标比例列表
        width: 原始图像宽度
        height: 原始图像高度
        image_size: 目标图像大小
        
    返回:
        best_ratio: 最佳比例
    """
    best_ratio_diff = float("inf")
    best_ratio = (1, 1)
    area = width * height
    for ratio in target_ratios:
        target_aspect_ratio = ratio[0] / ratio[1]
        ratio_diff = abs(aspect_ratio - target_aspect_ratio)
        if ratio_diff < best_ratio_diff:
            best_ratio_diff = ratio_diff
            best_ratio = ratio
        elif ratio_diff == best_ratio_diff:
            if area > 0.5 * image_size * image_size * ratio[0] * ratio[1]:
                best_ratio = ratio
    return best_ratio


def dynamic_preprocess(image, min_num=1, max_num=6, image_size=448, use_thumbnail=False):
    """
    动态预处理图像,根据宽高比将图像分割成多个块
    
    参数:
        image: 原始图像
        min_num: 最小块数
        max_num: 最大块数
        image_size: 目标图像大小
        use_thumbnail: 是否使用缩略图
        
    返回:
        processed_images: 处理后的图像列表
    """
    orig_width, orig_height = image.size
    aspect_ratio = orig_width / orig_height

    # 计算现有图像宽高比
    target_ratios = set((i, j) for n in range(min_num, max_num + 1) for i in range(1, n + 1) for j in range(1, n + 1) if i * j <= max_num and i * j >= min_num)
    target_ratios = sorted(target_ratios, key=lambda x: x[0] * x[1])

    # 寻找最接近目标的宽高比
    target_aspect_ratio = find_closest_aspect_ratio(aspect_ratio, target_ratios, orig_width, orig_height, image_size)

    # 计算目标宽度和高度
    target_width = image_size * target_aspect_ratio[0]
    target_height = image_size * target_aspect_ratio[1]
    blocks = target_aspect_ratio[0] * target_aspect_ratio[1]

    # 调整图像大小
    resized_img = image.resize((target_width, target_height))
    processed_images = []
    for i in range(blocks):
        box = ((i % (target_width // image_size)) * image_size, (i // (target_width // image_size)) * image_size, 
               ((i % (target_width // image_size)) + 1) * image_size, ((i // (target_width // image_size)) + 1) * image_size)
        # 分割图像
        split_img = resized_img.crop(box)
        processed_images.append(split_img)
    assert len(processed_images) == blocks
    if use_thumbnail and len(processed_images) != 1:
        thumbnail_img = image.resize((image_size, image_size))
        processed_images.append(thumbnail_img)
    return processed_images


def load_image(image, input_size=448, max_num=6):
    """
    加载并处理图像
    
    参数:
        image: 输入图像
        input_size: 输入大小
        max_num: 最大块数
        
    返回:
        pixel_values: 处理后的图像张量
    """
    transform = build_transform(input_size=input_size)
    images = dynamic_preprocess(image, image_size=input_size, use_thumbnail=True, max_num=max_num)
    pixel_values = [transform(image) for image in images]
    pixel_values = torch.stack(pixel_values)
    return pixel_values


def get_index(bound, fps, max_frame, first_idx=0, num_segments=32):
    """
    获取视频帧索引
    
    参数:
        bound: 时间边界 [开始时间, 结束时间]
        fps: 视频帧率
        max_frame: 最大帧数
        first_idx: 第一帧索引
        num_segments: 分段数量
        
    返回:
        frame_indices: 帧索引数组
    """
    if bound:
        start, end = bound[0], bound[1]
    else:
        start, end = -100000, 100000
    start_idx = max(first_idx, round(start * fps))
    end_idx = min(round(end * fps), max_frame)
    seg_size = float(end_idx - start_idx) / num_segments
    frame_indices = np.array([int(start_idx + (seg_size / 2) + np.round(seg_size * idx)) for idx in range(num_segments)])
    return frame_indices

def get_num_frames_by_duration(duration):
    """
    根据视频时长计算帧数
    
    参数:
        duration: 视频时长(秒)
        
    返回:
        num_frames: 计算出的帧数
    """
    local_num_frames = 4        
    num_segments = int(duration // local_num_frames)
    if num_segments == 0:
        num_frames = local_num_frames
    else:
        num_frames = local_num_frames * num_segments
    
    num_frames = min(512, num_frames)
    num_frames = max(128, num_frames)

    return num_frames

def load_video(video_path, bound=None, input_size=448, max_num=1, num_segments=32, get_frame_by_duration = False):
    """
    加载并处理视频
    
    参数:
        video_path: 视频路径
        bound: 时间边界
        input_size: 输入大小
        max_num: 最大块数
        num_segments: 分段数量
        get_frame_by_duration: 是否根据时长获取帧数
        
    返回:
        pixel_values: 处理后的视频帧张量
        num_patches_list: 每帧的块数列表
    """
    vr = VideoReader(video_path, ctx=cpu(0), num_threads=1)
    max_frame = len(vr) - 1
    fps = float(vr.get_avg_fps())

    pixel_values_list, num_patches_list = [], []
    transform = build_transform(input_size=input_size)
    if get_frame_by_duration:
        duration = max_frame / fps
        num_segments = get_num_frames_by_duration(duration)
    frame_indices = get_index(bound, fps, max_frame, first_idx=0, num_segments=num_segments)
    for frame_index in frame_indices:
        img = Image.fromarray(vr[frame_index].asnumpy()).convert("RGB")
        img = dynamic_preprocess(img, image_size=input_size, use_thumbnail=True, max_num=max_num)
        pixel_values = [transform(tile) for tile in img]
        pixel_values = torch.stack(pixel_values)
        num_patches_list.append(pixel_values.shape[0])
        pixel_values_list.append(pixel_values)
    pixel_values = torch.cat(pixel_values_list)
    return pixel_values, num_patches_list

# 评估设置
max_num_frames = 512
generation_config = dict(
    do_sample=False,
    temperature=0.0,
    max_new_tokens=1024,
    top_p=0.1,
    num_beams=1
)
video_path = "car.mp4"
num_segments=128


with torch.no_grad():
  # 加载视频并处理
  pixel_values, num_patches_list = load_video(video_path, num_segments=num_segments, max_num=1, get_frame_by_duration=False)
  pixel_values = pixel_values.to(torch.bfloat16).to(model.device)
  video_prefix = "".join([f"Frame{i+1}: <image>\n" for i in range(len(num_patches_list))])
  
  # 单轮对话:视频详细描述
  question1 = "Describe this video in detail."
  question = video_prefix + question1
  output1, chat_history = model.chat(tokenizer, pixel_values, question, generation_config, num_patches_list=num_patches_list, history=None, return_history=True)
  print(output1)
  
  # 多轮对话:询问视频中的人数
  question2 = "How many people appear in the video?"
  output2, chat_history = model.chat(tokenizer, pixel_values, question2, generation_config, num_patches_list=num_patches_list, history=chat_history, return_history=True)
  
  print(output2)


# In[4]:


# video_prefix


# In[3]:


with torch.no_grad():
  # 单轮对话:询问车辆损伤部位(中文)
  question1 = "车的哪个部位损伤了?"
  question = video_prefix + question1
  output1, chat_history = model.chat(tokenizer, pixel_values, question, generation_config, num_patches_list=num_patches_list, history=None, return_history=True)
  print(output1)
  
  # 多轮对话:询问车辆碰撞位置(中文)
  question2 = "车撞到哪里了?"
  output2, chat_history = model.chat(tokenizer, pixel_values, question2, generation_config, num_patches_list=num_patches_list, history=chat_history, return_history=True)
  
  print(output2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292

好,我从头到尾拆解,用最通俗的话讲。


# 首先理解:本地部署 vs 云端调用

# 云端调用(百炼平台)

你 --[发视频URL]--> 阿里云服务器 --> AI模型 --> 返回结果 --> 你
1

类比:就像点外卖,你不用买菜做饭,只管点餐和吃。


# 本地部署

你 --[下载AI模型到自己电脑]--> 你的电脑运行AI --> 返回结果 --> 你
1

类比:就像自己开餐厅,要买菜、装修厨房、请厨师、自己做饭。


# 这段代码做了什么?(整体流程)

步骤1: 下载AI模型(只做一次)
   ↓
步骤2: 加载模型到内存
   ↓
步骤3: 准备视频数据(读取视频、提取帧)
   ↓
步骤4: 调用AI模型分析
   ↓
步骤5: 获取结果
1
2
3
4
5
6
7
8
9

# 逐模块详解


# 模块一:下载模型

from modelscope import snapshot_download

model_dir = snapshot_download(
    'OpenGVLab/InternVideo2_5_Chat_8B', 
    cache_dir='/root/autodl-tmp/models'
)
1
2
3
4
5
6

通俗解释

想象你要开一家餐厅:

  1. OpenGVLab/InternVideo2_5_Chat_8B = 菜谱的名字(AI模型的名称)
  2. cache_dir = 你要把菜谱放在哪个书架上
  3. snapshot_download = 去图书馆把菜谱借回家

下载的是什么?

InternVideo2_5_Chat_8B/
├── 模型权重文件(AI的大脑,几十GB)  
├── 配置文件(告诉程序怎么运行)
├── 分词器文件(把文字转成数字)
└── 其他辅助文件
1
2
3
4
5

为什么要下载?

因为云端模型是别人电脑上的,本地部署要把模型下载到你自己电脑上才能运行。


# 模块二:导入工具包

import numpy as np
import torch
import torchvision.transforms as T
from decord import VideoReader, cpu
from PIL import Image
from modelscope import AutoModel, AutoTokenizer
1
2
3
4
5
6

通俗解释

这就像做菜前准备各种厨具:

工具 类比 作用
numpy 计算器 做数学运算
torch AI的"操作系统" 让AI模型能运行
torchvision.transforms 图片加工工具 裁剪、缩放、调整图片
decord 视频播放器 从视频里提取图片帧
PIL 图片查看器 打开和处理图片文件
AutoModel 模型加载器 把AI模型加载到内存
AutoTokenizer 文字处理器 把文字转成AI能理解的数字

为什么需要这么多工具?

因为AI不能直接"看"视频,需要:

  1. 把视频拆成一帧帧图片
  2. 把图片调整成AI能处理的格式
  3. 把你的问题转成数字
  4. AI处理后把数字转回文字

# 模块三:加载模型

model_path = '/root/autodl-tmp/models/OpenGVLab/InternVideo2_5_Chat_8B'

tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModel.from_pretrained(model_path, trust_remote_code=True).half().cuda().to(torch.bfloat16)
1
2
3
4

通俗解释

这就像把厨师请到厨房,准备开始工作。

逐行拆解

model_path = '/root/autodl-tmp/models/OpenGVLab/InternVideo2_5_Chat_8B'
1

这只是记录模型放在哪个文件夹。


tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
1

tokenizer是什么?

AI不认识文字,只认识数字。tokenizer就是"翻译官":

"你好" → [101, 2769, 3456, 102]
"Hello" → [15496, 101]
1
2

类比:就像给外国人当翻译,把中文翻译成他们能懂的语言。


model = AutoModel.from_pretrained(model_path, trust_remote_code=True).half().cuda().to(torch.bfloat16)
1

这是一串链式调用,拆开看:

步骤1:加载模型

AutoModel.from_pretrained(model_path)
1

把模型文件加载到内存,就像把菜谱打开。

步骤2:转成半精度

.half()
1

AI模型默认用32位数字存储,太占内存。转成16位,内存占用减半。

类比:把高清视频转成标清,画质差不多但省空间。

步骤3:放到显卡

.cuda()
1

告诉程序用GPU(显卡)运行,而不是CPU。

为什么要用显卡?

对比项 CPU GPU
核心数 几十个 几千个
擅长 复杂逻辑 并行计算
AI运算 快几十倍

类比:CPU像几个博士,GPU像几千个小学生。AI计算不需要博士的智慧,只需要小学生的大量简单计算。

步骤4:转成bfloat16格式

.to(torch.bfloat16)
1

一种更精确的16位数字格式,AI效果更好。


# 模块四:图像预处理(把图片转成AI能吃的格式)

这部分代码很长,但核心就一件事:把图片转成AI能理解的数字矩阵


# 4.1 为什么需要预处理?

AI不能直接"看"图片,它只能处理数字。

原始图片:一张448×448像素的彩色图片

AI需要的数据:一个448×448×3的数字矩阵(每个像素有R、G、B三个值)

类比

  • 原始图片 = 一道做好的菜
  • AI需要的数据 = 菜谱上写的配料表(每种配料多少克)

# 4.2 build_transform函数

def build_transform(input_size):
    MEAN, STD = IMAGENET_MEAN, IMAGENET_STD
    transform = T.Compose([
        T.Lambda(lambda img: img.convert("RGB") if img.mode != "RGB" else img),
        T.Resize((input_size, input_size), interpolation=InterpolationMode.BICUBIC),
        T.ToTensor(),
        T.Normalize(mean=MEAN, std=STD)
    ])
    return transform
1
2
3
4
5
6
7
8
9

通俗解释

这是一条图片加工流水线,图片进去后经过几道工序:

原图 → [工序1: 确保RGB] → [工序2: 调整大小] → [工序3: 转数字] → [工序4: 标准化] → AI能吃的格式
1

逐个工序

工序 代码 作用 类比
1 T.Lambda(...) 确保是RGB格式 有些图片是黑白或RGBA,统一转成RGB
2 T.Resize(...) 调整大小为448×448 不管原图多大,都缩放到统一大小
3 T.ToTensor() 转成数字矩阵 把图片变成数字
4 T.Normalize(...) 标准化数值 让数值在合理范围内(类似归一化)

# 4.3 为什么要标准化?

AI模型是用ImageNet数据集训练的,那个数据集的图片都做了特定的标准化处理。我们用同样的方式处理,AI才能正确理解。

类比:AI是按菜谱A训练的厨师,你给他食材必须按菜谱A的方式处理,他才会做。


# 模块五:视频处理函数(把视频转成图片帧)


# 5.1 视频是什么?

视频 = 一连串的图片(帧)

例如:1秒钟的视频
- 24fps(帧率)= 1秒有24张图片
- 10秒视频 = 240张图片
1
2
3
4
5

类比:动画翻书,每一页是一张图,快速翻动就变成动画。


# 5.2 load_video函数

def load_video(video_path, bound=None, input_size=448, max_num=1, num_segments=32):
    vr = VideoReader(video_path, ctx=cpu(0), num_threads=1)
    max_frame = len(vr) - 1
    fps = float(vr.get_avg_fps())
    # ...
1
2
3
4
5

通俗解释

这个函数做了几件事:

  1. 打开视频文件
  2. 知道视频有多少帧、帧率是多少
  3. 从视频里抽取关键帧(不用全部帧,太多处理不了)
  4. 把每一帧图片处理好
  5. 返回处理好的数据

# 5.3 为什么要抽帧?

假设一个10分钟视频:

  • 30fps × 60秒 × 10分钟 = 18000帧(1.8万张图片)
  • 全部处理:太慢,AI内存不够

解决方案:只抽取关键帧

num_segments = 128  # 从18000帧里抽128帧
1

类比:看一部电影,不用每一秒都看,只看关键场景就能理解剧情。


# 5.4 抽帧的逻辑

def get_index(bound, fps, max_frame, first_idx=0, num_segments=32):
    # 计算要抽取哪些帧
    # ...
    frame_indices = np.array([int(start_idx + (seg_size / 2) + np.round(seg_size * idx)) for idx in range(num_segments)])
    return frame_indices
1
2
3
4
5

通俗解释

把视频均匀切成128段,每段取中间一帧:

视频:[========================================]
         ↓ 均匀分布
帧号:  [3, 150, 297, 444, ...](共128帧)
1
2
3

# 模块六:模型推理(让AI"看"视频并回答问题)

generation_config = dict(
    do_sample=False,
    temperature=0.0,
    max_new_tokens=1024,
    top_p=0.1,
    num_beams=1
)

with torch.no_grad():
    pixel_values, num_patches_list = load_video(video_path, num_segments=num_segments)
    pixel_values = pixel_values.to(torch.bfloat16).to(model.device)
    
    question = video_prefix + "Describe this video in detail."
    output, chat_history = model.chat(
        tokenizer, 
        pixel_values, 
        question, 
        generation_config,
        num_patches_list=num_patches_list,
        history=None,
        return_history=True
    )
    print(output)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 6.1 generation_config是什么?

这是控制AI如何生成回答的配置:

参数 含义
do_sample False 不随机,每次回答一样
temperature 0.0 最确定,不随机
max_new_tokens 1024 最多生成1024个token
top_p 0.1 只从前10%可能的词里选
num_beams 1 贪心搜索(不展开讲)

类比

  • temperature=0:考试时只选最确定的答案
  • temperature=1:考试时有时会猜

# 6.2 torch.no_grad()是什么?

with torch.no_grad():
    # 推理代码
1
2

作用:告诉PyTorch"只用模型,不用计算梯度"。

为什么要这样?

训练AI时需要计算梯度(记录每一步怎么走),但推理时不需要。

类比

  • 训练 = 学习,需要记笔记
  • 推理 = 考试,只需要写答案,不用记笔记

# 6.3 核心调用

output, chat_history = model.chat(
    tokenizer,           # 文字处理器
    pixel_values,        # 视频/图片数据
    question,            # 问题
    generation_config,   # 生成配置
    num_patches_list,    # 图片分块信息
    history=None,        # 对话历史
    return_history=True  # 返回对话历史
)
1
2
3
4
5
6
7
8
9

通俗解释

就像打电话问AI:

  1. 给AI视频数据(pixel_values
  2. 问AI问题(question
  3. AI处理后返回答案(output

# 完整流程图

┌─────────────────────────────────────────────────────────────┐
│                      本地部署流程                            │
└─────────────────────────────────────────────────────────────┘

1. 下载模型(只做一次)
   ┌──────────────┐
   │ ModelScope   │ ──下载──→ 本地硬盘
   │ (模型仓库)    │           InternVideo2_5_Chat_8B/
   └──────────────┘           ├── pytorch_model.bin (16GB)
                              ├── config.json
                              └── ...

2. 加载模型(每次运行都要做)
   ┌──────────────┐
   │ 本地硬盘      │ ──加载──→ GPU显存
   │ 模型文件      │           (AI大脑就位)
   └──────────────┘

3. 处理视频
   ┌──────────────┐
   │ car.mp4      │ ──拆帧──→ 128张图片
   │ (视频文件)    │ ──处理──→ 数字矩阵 (pixel_values)
   └──────────────┘

4. 调用AI
   ┌──────────────┐
   │ pixel_values │ ──输入──→ AI模型 ──输出──→ 答案文字
   │ + question   │
   └──────────────┘

5. 获取结果
   ┌──────────────┐
   │ 答案文字      │ ──打印──→ 屏幕
   └──────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 核心概念总结

概念 解释 类比
模型 AI的大脑 厨师
分词器 把文字转数字 翻译官
GPU 显卡,AI运行的地方 厨房
张量 多维数字数组 AI的数据格式
视频中的单张图片 翻书动画的一页
预处理 把数据转成AI能处理的格式 把食材洗净切好
推理 用模型生成答案 厨师做菜
显存 GPU的内存 厨房的大小

# MinerU使用

MinerU专注于高效解析和提取复杂的PDF文档、网页和电子书,并将其转换为易于分析的Markdown或JSON格式。由上海人工智能实验室OpenDataLab团队开发。

主要功能包括:

  • PDF转Markdown 支持多模态PDF(含图片、表格、公式等)的结构化转换。
    自动去除页眉、页脚、脚注等干扰信息,保留标题、段落、列表等结构。
    公式识别并转换为LaTeX格式,表格转换为HTML或Markdown。

  • 网页内容提取:从网页中剔除广告等干扰信息,精准提取正文、评论、视频文字等内容。

  • 电子书转换:支持epub、mobi、docx、pptx、chm、azw等格式批量转Markdown。

  • 多语言OCR:自动检测扫描版PDF和乱码,支持84种语言的OCR识别

核心技术

  • 布局检测:基于LayoutLMv3微调,识别文本、表格、图片等区域。
  • 公式识别:使用YOLOv8检测公式,UniMERNet模型转换LaTeX。
  • OCR增强:采用PaddleOCR提高文本识别准确率。

应用场景

  • 大模型训练:为书生·浦语等模型提供高质量语料。
  • 学术研究:提取论文、教材中的关键信息。
  • 法律与金融:解析合同、研报等结构化数据。

MinerU支持CPU/GPU,兼容Windows/Linux/Mac

MinerU使用

# MinerU本地化部署

Windows下MinerU的配置要求,给你分两种情况说:


# 纯CPU模式(pipeline后端)

最低配置

  • CPU:i5及以上
  • 内存:16GB(推荐32GB)
  • 磁盘:30GB+(模型约25GB)
  • Python:3.10 ~ 3.12(注意:不支持3.13)

优点:不需要显卡,任何电脑都能跑 缺点:速度较慢


# GPU加速模式(hybrid/vlm后端)

额外要求

  • NVIDIA显卡:RTX 2060及以上(Volta架构及以后)
  • 显存:6GB+(vlm模式需10GB+)
  • CUDA:11.8或更高
  • 驱动版本:≥525.60.13

优点:速度快5-10倍 缺点:需要N卡,显存要够


# Windows特有的坑(必看)

# 1. 必须开启开发者模式

MinerU下载模型时用到了符号链接,Windows默认不允许普通用户创建。

操作步骤

设置 → 隐私和安全性 → 开发者选项 → 开启"开发人员模式"
1

然后重启电脑

不开这个,模型下载会报错 [WinError 1314] 客户端没有所需的特权

# 2. Python版本别用3.13

MinerU依赖的 ray 库在Windows上不支持Python 3.13,最高只能用3.12。

我的显卡:NVIDIA RTX 3060,显存 12GB

这完全满足GPU加速的要求(需要6GB+显存),可以用GPU模式,速度会比CPU快5-10倍。

# Step 1:开启Windows开发者模式

# 为什么需要这步?

MinerU下载模型文件时使用了**符号链接(symlink)**功能。Windows默认不允许普通用户创建符号链接,不开开发者模式会报错:[WinError 1314] 客户端没有所需的特权

# 操作步骤:

  1. Win + I 键打开Windows设置(或者右键开始菜单 → 设置)

  2. 在左侧找到**"隐私和安全性"**,点击进入

  3. 点击**"开发者选项"**

  4. 打开**"开发人员模式"**的开关(变成蓝色就是开了)

  5. 重启电脑(这一步很重要,不重启权限不会生效)

# Step 2:检查Python环境

# 为什么需要这步?

MinerU需要Python运行环境。我们需要确认:

  1. 你电脑上有没有安装Python
  2. Python版本是否正确(需要 3.10 ~ 3.12,不能是3.13)

# 操作步骤:

  1. Win + R 键打开"运行"窗口
  2. 输入 cmd,然后按回车(会打开黑色的命令提示符窗口)
  3. 在窗口里输入以下命令,然后按回车:
py --version
1
  1. 看看显示的是什么

可能的结果

显示内容 说明 下一步
Python 3.10.x3.11.x3.12.x 版本正确 继续下一步
Python 3.9.x 或更低 版本太低 需要安装新版本
Python 3.13.x 版本太高,不支持 需要安装3.12
提示"不是内部或外部命令" 没装Python 需要安装Python

# step 3: 安装MinerU

# 升级pip
python -m pip install --upgrade pip
# 较新的pip版本有更好的依赖解析能力
# 支持更多的安装选项和更快的下载速度
# 避免因pip版本过旧导致的安装失败

# 安装uv(现代Python包管理器)
pip install uv
# uv是一个超快的Python包管理器,用Rust编写
# 比传统pip快10-100倍
# 更好的依赖解析算法
# MinerU推荐使用uv进行安装

# 安装mineru完整版
uv pip install -U "mineru[all]"  
# uv pip install: 使用uv来安装Python包
# -U: 更新到最新版本
# "mineru[all]": 安装mineru及其所有可选依赖
# [all]包括所有功能模块(OCR、VLM、hybrid等后端)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# step 4: 下载模型

为什么要设置模型源?
MinerU的核心功能依赖预训练AI模型
这些模型通常托管在国外服务器(HuggingFace等)
国内网络访问国外服务器速度慢且不稳定
ModelScope是阿里云的模型开放平台,国内访问速度快

# 下载模型
$env:MODELSCOPE_CACHE="D:\models\ModelScope"; $env:MINERU_MODEL_SOURCE="modelscope"; mineru-models-download  
1
2

模型包含什么?

  • OCR模型:用于文字识别
  • VLM模型:视觉语言模型,用于理解图片内容
  • 表格检测模型:专门识别和解析表格
  • 数学公式模型:识别和解析数学公式
    总计约25GB左右

# step 5: 在PyCharm中使用GPU模式解析本地PDF

安装gpu版本的pytorch

uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118  
1

然后新建一个test.py测试一下硬件条件

import torch
import sys

print("=== PyTorch和CUDA环境信息 ===")
print(f"PyTorch版本: {torch.__version__}")
print(f"Python版本: {sys.version}")
print(f"CUDA可用: {torch.cuda.is_available()}")

if torch.cuda.is_available():
    print(f"CUDA版本: {torch.version.cuda}")
    print(f"cuDNN版本: {torch.backends.cudnn.version()}")
    print(f"GPU数量: {torch.cuda.device_count()}")
    print(f"当前GPU: {torch.cuda.current_device()}")
    print(f"GPU名称: {torch.cuda.get_device_name()}")
else:
    print("\nCUDA不可用,可能的原因:")
    print("1. 没有安装CUDA版本的PyTorch")
    print("2. CUDA驱动程序版本过低")
    print("3. 缺少必要的CUDA库")
    print("4. GPU驱动未正确安装")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# step6 基础pdf解析函数

"""
MinerU PDF解析脚本
功能:将PDF文件解析为Markdown格式
"""

import os

# ============================================
# 第0步:设置国内镜像源
# ============================================
os.environ['MINERU_MODEL_SOURCE'] = 'modelscope'
os.environ['MINERU_MODELS_DIR'] = r'D:\models\ModelScope'

import subprocess
import sys

# ============================================
# 第1部分:配置参数
# ============================================

# 获取脚本所在目录
script_dir = os.path.dirname(os.path.abspath(__file__))

# PDF文件的完整路径
pdf_path = os.path.join(script_dir, "云平台快速入门指南.pdf")

# 输出目录(改成绝对路径)
output_dir = os.path.join(script_dir, "output")

# 是否使用GPU加速
use_gpu = False

# ============================================
# 第2部分:检查文件是否存在
# ============================================

if not os.path.exists(pdf_path):
    print(f"错误:找不到PDF文件:{pdf_path}")
    sys.exit(1)

if not os.path.exists(output_dir):
    os.makedirs(output_dir)
    print(f"已创建输出目录:{output_dir}")

# ============================================
# 第3部分:构建MinerU命令
# ============================================

cmd = f'mineru -p "{pdf_path}" -o "{output_dir}"'

if not use_gpu:
    cmd += " -b pipeline"

print(f"正在执行命令:{cmd}")

# ============================================
# 第4部分:执行解析
# ============================================

print("\n开始解析PDF,请稍候...")
print("=" * 50)

process = subprocess.Popen(
    cmd,
    shell=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    encoding='utf-8',
    errors='replace'
)

for line in process.stdout:
    print(line, end='')

process.wait()

# ============================================
# 第5部分:检查结果
# ============================================

print("\n" + "=" * 50)

pdf_name = os.path.splitext(os.path.basename(pdf_path))[0]

# 注意:MinerU会在输出目录下创建一个以PDF名称命名的子目录
md_file = os.path.join(output_dir, pdf_name, f"{pdf_name}.md")

if os.path.exists(md_file):
    print(f"解析成功!")
    print(f"Markdown文件:{md_file}")
else:
    print("解析可能失败,请检查上方日志")
    # 打印实际输出目录的内容,帮助调试
    print(f"\n检查输出目录:{output_dir}")
    if os.path.exists(output_dir):
        for root, dirs, files in os.walk(output_dir):
            for f in files:
                print(f"  发现文件:{os.path.join(root, f)}")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100

如果GPU版本没成功,可能是没安装CUDA Toolkit

  • 第一步:下载

打开这个链接: https://developer.nvidia.com/cuda-11-8-0-download-archive (opens new window)

选择:

  • Operating System: Windows
  • Architecture: x86_64
  • Version: 11
  • Installer Type: exe (local)

点击 Download,文件大约 3GB


  • 第二步:安装
  1. 双击下载的安装文件
  2. 选择 精简 安装(推荐),一路点"下一步"
  3. 安装路径保持默认:C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8
  4. 等待安装完成(约5-10分钟)

  • 第三步:设置环境变量

安装完成后,检查环境变量是否已自动设置:

  1. Win + S,搜索"环境变量"
  2. 点击"编辑系统环境变量"
  3. 点击"环境变量"
  4. 在"系统变量"里找 CUDA_PATH
    • 如果已经有了,值应该是 C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8
    • 如果没有,手动新建一个

  • 第四步:重启PyCharm

重要! 环境变量需要重启PyCharm才能生效。


  • 第五步:测试GPU版本

把代码里的 use_gpu 改回 True

use_gpu = True  # 改回True
1

然后运行。