全部产品
Search
文档中心

向量检索服务 Milvus 版:通过阿里云Milvus构建智能驾驶截帧分析与多模态检索链路

更新时间:Aug 13, 2026

本文介绍如何用阿里云 Milvus 的 AI Function 处理智能驾驶行车截帧:一次建表挂载多个 Function,写入帧地址时即自动完成多模态向量化、场景分类、交通元素结构化抽取与命名实体识别,随后支持以文搜帧、以图搜帧、结构化过滤的 Corner Case 挖掘与多模态重排。

方案概述

在智能驾驶研发链路里,车端摄像头是数据的第一入口。一辆测试车通常搭载 6~12 路环视相机,以 20~30 FPS 持续采集行车画面,单车单日路测即可产生数 TB 视频。这些视频是训练感知模型、复现事故、迭代规控策略的核心资产,但它们海量、非结构化、难以检索。

工程团队关心的不是「某段视频」本身,而是视频里发生了什么。按帧做场景理解,需要从画面中识别出交通参与者(行人、非机动车、前车)、交通管制元素(红绿灯状态、车道线、限速牌)、道路环境(路口、高速、隧道、施工区)以及异常事件(加塞、闯红灯、急刹、抛洒物)。把这些理解结果结构化沉淀下来,才能支撑以下高价值场景:

  • 数据回灌:把真实路况帧回灌到仿真与训练管线,持续迭代感知模型。

  • Corner Case 挖掘:从海量帧中精准捞出「雨夜隧道口的施工区」这类长尾场景,这正是模型最容易失效、也最缺样本的地方。

  • 自动标注:用大模型对帧做粗标注,替代大量人工拉框打标,人工只做审核与精修。

  • 路测报告:按场景与事件聚合统计,快速产出可量化的周期报告。

若不借助统一平台,通常要自建「抽帧服务 → 目标检测/多模态模型 → 向量库 → 元数据库」,需要自行维护 GPU 推理集群的扩缩容与故障恢复,帧数据在对象存储、推理集群、向量库之间反复搬运,链路长、故障点多;人工标注还存在口径不统一导致标签噪声大的问题。

阿里云 Milvus 2.6 的 AI Function 把这些能力收敛进同一个向量数据库:推理即查询。模型推理不再是独立的外部链路,而是在 insert 与 search 时由 Milvus 内部自动触发的函数调用。针对截帧分析,本文用到以下 Function:

Function

作用

在截帧分析中的用途

AI_EMBEDDING

用 qwen3-vl-embedding 把截帧图片转成 2560 维向量;多模态模型让文本与图像映射到同一向量空间。

支持以文搜帧与以图搜帧,「雨天施工区」这类语义需求可被向量检索命中。

AI_CLASSIFY

从预设标签集中为每帧选出最匹配的一项,写入分类字段。

给每帧打上路口、高速、隧道、施工区等场景标签。

AI_EXTRACT

按指定标签从帧中抽取要素,以 JSON 写入结构化字段。

抽取红绿灯状态、车道数、天气、异常事件,用于精确过滤。

AI_ENTITY_EXTRACT

识别帧中明确出现的命名实体。

抽取标志牌上的地名、路名与限速数值。

AI_RERANK

向量召回 Top-N 后用 qwen3-vl-rerank 二次打分(可选)。

按主体、动作、场景一致性重排,提升 Corner Case 挖掘的精排质量。

说明

前四个 Function 在写入时自动执行,构成「写入即推理」的主链路;AI_RERANK 作用于检索阶段,是可选的精排增强。

前提条件

  • 已创建 Milvus 2.6 版本实例。AI Function 依赖 2.6 版本内核,创建后无需单独绑定模型服务。

  • 如需从公网访问实例,已在实例详情页的 安全配置 页签开启 公网访问 并将客户端出口 IP 加入公网访问白名单。

  • 已安装 pymilvus,本文示例基于 pymilvus 3.0.0 验证;如需自行抽帧还需安装 ffmpeg。

  • 已准备好截帧图片,并上传到模型可访问的公网地址(如 OSS)。

说明

RESTful 接口与 gRPC 共用 19530 端口,调用时必须显式带端口,例如 http://c-xxx.milvus.aliyuncs.com:19530;省略端口会默认访问 80 端口并导致连接超时。

操作步骤

步骤一:视频抽帧

车端相机采集的是连续视频,需先用 ffmpeg 抽帧,得到帧图片并上传到模型可访问的地址(如 OSS),再把 frame_url 交给后续代码入库。抽帧时同步记录每帧的 clip_id 与 ts_ms(视频片段 ID 与帧时间戳),便于检索命中后定位回原视频。

# 定时截帧:每隔 1 秒取一帧,缩放到宽 960
ffmpeg -i clip001.mp4 -vf "fps=1,scale=960:-1" -q:v 3 frames/clip001_%04d.jpg

# 只取关键帧(I 帧),信息量更高、冗余更少
ffmpeg -i clip001.mp4 -vf "select='eq(pict_type,I)',scale=960:-1" -fps_mode vfr -q:v 3 frames/clip001_key_%04d.jpg

# 截取指定时间点的单帧(例如第 5.2s,对应 ts_ms=5200)
ffmpeg -ss 5.2 -i clip001.mp4 -frames:v 1 -vf scale=960:-1 -q:v 3 frames/clip001_5200ms.jpg
说明

scale=960:-1 中的 -1 表示按原宽高比自动计算高度。若使用 ffmpeg 8.x 及以上版本,取 I 帧请用 -fps_mode vfr;旧写法 -vsync vfr 仍可运行但会提示已废弃。

步骤二:准备公共代码

以下代码包含连接配置、REST 调用工具与 TEXTTRANSFORM 函数类型的兼容封装。请将 MILVUS_URI 与 MILVUS_TOKEN 替换为实际实例信息。

from __future__ import annotations

import json
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen

from pymilvus import DataType, Function, FunctionType, MilvusClient

MILVUS_URI = "http://c-xxxx.milvus.aliyuncs.com:19530"
MILVUS_TOKEN = "root:xxx"


def post_json(path: str, body: dict[str, Any], timeout: int = 120) -> tuple[int, dict[str, Any]]:
    request = Request(
        f"{MILVUS_URI.rstrip('/')}{path}",
        data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
        headers={"Authorization": f"Bearer {MILVUS_TOKEN}", "Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urlopen(request, timeout=timeout) as response:
            return response.status, json.loads(response.read().decode("utf-8"))
    except HTTPError as exc:
        return exc.code, json.loads(exc.read().decode("utf-8"))


# 阿里云 Milvus 将 TEXTTRANSFORM 作为托管扩展暴露,函数类型值为 9。
# 部分 pymilvus 版本的 FunctionType 枚举中没有该成员,下面做兼容封装。
TEXTTRANSFORM_FUNCTION_TYPE = 9


def texttransform_function_type() -> Any:
    for type_name in ("TEXTTRANSFORM", "TEXT_TRANSFORM", "TextTransform"):
        function_type = getattr(FunctionType, type_name, None)
        if function_type is not None:
            return function_type
    existing = getattr(FunctionType, "_value2member_map_", {}).get(TEXTTRANSFORM_FUNCTION_TYPE)
    if existing is not None:
        return existing
    extension = int.__new__(FunctionType, TEXTTRANSFORM_FUNCTION_TYPE)
    extension._name_ = "TEXTTRANSFORM"
    extension._value_ = TEXTTRANSFORM_FUNCTION_TYPE
    FunctionType._value2member_map_[TEXTTRANSFORM_FUNCTION_TYPE] = extension
    FunctionType._member_map_["TEXTTRANSFORM"] = extension
    return extension


VECTOR_DIM = 2560
EMBED_MODEL = "qwen3-vl-embedding"
VLM_MODEL = "qwen3.7-plus"          # 多模态理解(分类/抽取/实体)模型
client = MilvusClient(uri=MILVUS_URI, token=MILVUS_TOKEN)
说明

AI_CLASSIFY、AI_EXTRACT、AI_ENTITY_EXTRACT 都属于 TEXTTRANSFORM 类型的 Function(函数类型值为 9),通过 task 参数区分具体任务。部分 pymilvus 版本的 FunctionType 枚举中没有该成员,因此需要上面的兼容封装。

步骤三:建 Collection,一次挂载 4 个 AI Function

向量字段用于语义检索,另外三个字段分别承接分类、结构化抽取与实体识别的结果。四个 Function 都以 frame_url 为输入,在写入时自动执行。

# ===== 建 Collection:一次建表挂 4 个 AI Function =====
collection_name = "driving_frames"
if client.has_collection(collection_name):
    client.drop_collection(collection_name)

schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("frame_url", DataType.VARCHAR, max_length=4096)   # 截帧图片地址
schema.add_field("clip_id", DataType.VARCHAR, max_length=128)      # 所属视频片段 ID
schema.add_field("ts_ms", DataType.INT64)                          # 帧时间戳(ms)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=VECTOR_DIM)
schema.add_field("scene", DataType.VARCHAR, max_length=64)         # AI_CLASSIFY 输出
schema.add_field("attributes", DataType.JSON)                      # AI_EXTRACT 输出
schema.add_field("entities", DataType.JSON)                        # AI_ENTITY_EXTRACT 输出

# 1) 多模态向量化:帧图片 -> 2560 维向量
schema.add_function(Function(
    name="embed_frame", function_type=FunctionType.TEXTEMBEDDING,
    input_field_names=["frame_url"], output_field_names=["embedding"],
    params={"provider": "aliyun_milvus", "model_name": EMBED_MODEL,
            "dim": VECTOR_DIM, "is_multimodal": "true"}))

# 2) 场景分类:路口/高速/隧道/施工区/普通道路
schema.add_function(Function(
    name="classify_scene", function_type=texttransform_function_type(),
    input_field_names=["frame_url"], output_field_names=["scene"],
    params={"provider": "aliyun_milvus", "model_name": VLM_MODEL,
            "task": "ai_classify", "media_type": "image",
            "labels": "路口,高速,隧道,施工区,普通道路",
            "prompt": "根据行车画面所处的道路环境分类。", "temperature": "0"}))

# 3) 结构化抽取:红绿灯/车道数/天气/异常事件
#    注意:为每个标签都规定缺省取值,避免模型返回 null(详见下方说明)
schema.add_function(Function(
    name="extract_traffic", function_type=texttransform_function_type(),
    input_field_names=["frame_url"], output_field_names=["attributes"],
    params={"provider": "aliyun_milvus", "model_name": VLM_MODEL,
            "task": "ai_extract", "media_type": "image",
            "labels": "traffic_light,lane_count,weather,anomaly_event",
            "prompt": "traffic_light 取值 red/green/yellow/none;"
                      "weather 取值 sunny/rainy/cloudy/night/unknown,无法判断时填 unknown;"
                      "lane_count 填整数,无法判断时填 0;"
                      "anomaly_event 描述加塞、闯红灯、事故等异常,无则填 none。",
            "temperature": "0"}))

# 4) 命名实体:路名/地名/限速数值
schema.add_function(Function(
    name="extract_entities", function_type=texttransform_function_type(),
    input_field_names=["frame_url"], output_field_names=["entities"],
    params={"provider": "aliyun_milvus", "model_name": VLM_MODEL,
            "task": "ai_entity_extract", "media_type": "image",
            "entity_types": "LOCATION,PRODUCT",
            "prompt": "仅抽取画面中交通标志牌上明确出现的地名、路名与限速数值,不要根据外观猜测。",
            "temperature": "0"}))

index_params = client.prepare_index_params()
index_params.add_index(field_name="embedding", index_type="AUTOINDEX", metric_type="COSINE")
client.create_collection(collection_name=collection_name, schema=schema,
                        index_params=index_params)
警告

多模态 Function 必须带 "is_multimodal": "true",且向量字段的 dim 必须与 Function 参数中的 dim 一致(2560)。

务必为每个抽取标签规定缺省取值。实测中若未规定,模型在无法判断时会返回 null(例如隧道内的帧因看不到天空,weather 被判为 null)。而 JSON 字段为 null 时,!= 条件不会匹配该行——例如 6 帧数据中执行 attributes["weather"] != "rainy" 只返回 4 条,null 那行被静默排除。在 Corner Case 挖掘场景下这类静默漏帧的后果很严重,因此上面的 prompt 为每个标签都给出了缺省值。

步骤四:截帧入库(写入即推理)

只需写入 frame_url、clip_id、ts_ms 三个字段,其余四个字段由 AI Function 自动填充,无需人工标注。

# ===== 截帧入库:写入即推理,embedding/scene/attributes/entities 自动填充 =====
# frame_url 需替换为你自己的、模型可访问的公网图片地址;
# clip_id 与 ts_ms 记录帧的来源片段与时间戳,便于检索后定位回原视频。
frames = [
    {"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_dashcam_5000.jpg",
     "clip_id": "clip_dashcam", "ts_ms": 5000},
    {"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_dashcam_12000.jpg",
     "clip_id": "clip_dashcam", "ts_ms": 12000},
    {"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_highway_3000.jpg",
     "clip_id": "clip_highway", "ts_ms": 3000},
    {"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_highway_9000.jpg",
     "clip_id": "clip_highway", "ts_ms": 9000},
    {"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_urban_2000.jpg",
     "clip_id": "clip_urban", "ts_ms": 2000},
    {"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_urban_8000.jpg",
     "clip_id": "clip_urban", "ts_ms": 8000},
]

# qwen3-vl-embedding 多模态批量上限为 10,超过会报 image batch size can should be [1, 10]
_BATCH = 8
for _i in range(0, len(frames), _BATCH):
    client.insert(collection_name, frames[_i:_i + _BATCH])
client.flush(collection_name)
client.load_collection(collection_name)

# 写入完成后直接 query 出结构化结果验证
ingested_rows = client.query(
    collection_name, filter="",
    output_fields=["frame_url", "clip_id", "ts_ms", "scene", "attributes", "entities"],
    limit=100)
for row in ingested_rows:
    print(f"{row['clip_id']}@{row['ts_ms']}ms  scene={row['scene']}  "
          f"attributes={json.dumps(row['attributes'], ensure_ascii=False)}  "
          f"entities={json.dumps(row['entities'], ensure_ascii=False)}")
说明

qwen3-vl-embedding 的多模态批量上限为 10,单批超过会报 image batch size can should be [1, 10],因此按批写入。写入后必须调用 flush(),否则紧接着检索可能返回空结果。

实测 6 帧写入 + flush + load 共约 12 秒,结构化结果示例:

帧

scene

attributes

entities

dashcam@5000ms

路口

traffic_light=green, lane_count=4, weather=sunny, anomaly_event=none

[]

dashcam@12000ms

施工区

traffic_light=none, lane_count=3, weather=rainy, anomaly_event=none

[]

highway@3000ms

高速

traffic_light=none, lane_count=4, weather=sunny, anomaly_event=none

[{"text":"EXIT 111","type":"LOCATION"}]

highway@9000ms

隧道

traffic_light=none, lane_count=2, anomaly_event=none

[]

urban@2000ms

普通道路

weather=cloudy, anomaly_event=道路上有行人与动物

[]

urban@8000ms

路口

traffic_light=red, lane_count=4, weather=sunny, anomaly_event=none

[]

六帧的场景分类与画面内容全部一致,红绿灯状态、天气、车道数的抽取也与画面相符。命名实体只在出现可读标志牌文字的帧上抽到结果,模型未凭画面外观臆测地名,符合 prompt 中「不要根据外观猜测」的约束。

步骤五:以文搜帧 / 结构化过滤 / 以图搜帧

写入阶段产出的向量与结构化字段,在检索阶段可以组合使用:向量负责语义召回,结构化字段负责精确过滤。

# ===== 以文搜帧 / 结构化过滤 / 以图搜帧 =====
# 1) 以文搜帧:查询文本经同一多模态模型映射到图像向量空间,直接召回
query = "雨天施工区,路面有锥形桶和施工牌"
text_search = client.search(
    collection_name=collection_name, data=[query], anns_field="embedding",
    limit=10, output_fields=["frame_url", "scene", "attributes"])
for hit in text_search[0]:
    print(f"score={hit['distance']:.4f} scene={hit['entity']['scene']}")

# 2) 向量召回 + 结构化过滤(Corner Case 挖掘)
#    结构化字段来自写入时的 AI_EXTRACT / AI_CLASSIFY,可直接参与 filter
corner_query = "夜间道路上的异常事件"
corner_search = client.search(
    collection_name=collection_name, data=[corner_query], anns_field="embedding",
    limit=10, filter='attributes["anomaly_event"] != "none"',
    output_fields=["frame_url", "scene", "attributes"])
print(f"命中 {len(corner_search[0])} 条长尾场景")

# 3) 以图搜帧:把 data 换成图片 URL 即可,其余不变
image_query_url = frames[0]["frame_url"]
image_search = client.search(
    collection_name=collection_name, data=[image_query_url],
    anns_field="embedding", limit=10, output_fields=["frame_url", "scene"])
for hit in image_search[0]:
    print(f"score={hit['distance']:.4f} scene={hit['entity']['scene']}")

实测结果:

检索方式

查询

结果

以文搜帧

雨天施工区,路面有锥形桶和施工牌

施工区帧 0.5904 居首,次名仅 0.1797,区分度明显

以图搜帧

以一张路口帧的 URL 作为查询

查询图自身 1.0000,同类路口帧 0.5534,最不相关帧 0.1162

结构化过滤

attributes["anomaly_event"] != "none"

命中数据中确实存在异常事件的帧

以图搜帧时查询图自身相似度为 1.0000,可用于验证图像编码的一致性。

警告

结构化过滤支持字符串等值与数值比较,例如 attributes["lane_count"] >= 4 可筛出四车道及以上的帧。组合条件时要注意:如果某个 JSON 字段的值为 null,!= 条件不会匹配该行,可能导致长尾样本被静默漏掉——这也是步骤三中为每个标签规定缺省取值的原因。

Corner Case 挖掘的组合条件要与素材库实际情况匹配。例如 scene == "隧道" and attributes["anomaly_event"] != "none" 要求同时满足「隧道场景」且「存在异常事件」,如果素材库中尚无这类帧,查询会正常返回 0 条。返回空说明当前库内没有该长尾场景,这本身就是 Corner Case 挖掘的有效结论,而不是功能异常;调试阶段建议先用单一条件确认链路通畅,再逐步叠加条件收窄范围。

步骤六:AI_RERANK 多模态重排(可选)

向量相似度衡量的是语义接近程度,与「和查询意图有多相关」并不完全等价。可用 qwen3-vl-rerank 对召回的帧做二次精排,提升长尾场景的排序质量。提供两种用法:在 search 中挂 ranker,或对已有的一批帧 URL 走 REST 接口独立重排。

# ===== AI_RERANK 多模态重排(可选)=====
QUERY = "高速匝道处的加塞行为"

# 方式一:search 挂 ranker,向量召回 Top-N 后由 qwen3-vl-rerank 精排
reranker = Function(
    name="rerank_frames", function_type=FunctionType.RERANK,
    input_field_names=["frame_url"],
    params={"reranker": "model", "provider": "aliyun_milvus",
            "model_name": "qwen3-vl-rerank", "queries": [QUERY],
            "is_multimodal": "true",
            "instruct": "Rank candidate frames by relevance to the query, "
                        "prioritizing subject, action, scene and fine-grained visual details.",
            "timeout_sec": 10})
rerank_search = client.search(
    collection_name=collection_name, data=[QUERY], anns_field="embedding",
    limit=20, output_fields=["frame_url", "scene"], ranker=reranker)
for hit in rerank_search[0]:
    print(f"rerank_score={hit['distance']:.4f} scene={hit['entity']['scene']}")

# 方式二:只对已召回的一批帧 URL 做独立重排,走 REST /v2/vectordb/ai/rerank
rest_docs = [f["frame_url"] for f in frames[:3]]
status, data = post_json("/v2/vectordb/ai/rerank", {
    "model_name": "qwen3-vl-rerank", "query": QUERY,
    "documents": rest_docs,
    "params": {"is_multimodal": True,
               "instruct": "Rank candidate frames by relevance to the query, "
                           "prioritizing subject, action, scene and fine-grained visual details.",
               "timeout_sec": 10}})
assert status == 200 and data.get("code") == 0, data
for item in sorted(data["data"]["output"]["results"],
                   key=lambda x: x["relevance_score"], reverse=True):
    print(f"index={item['index']} relevance_score={item['relevance_score']:.4f}")

实测查询「高速匝道处的加塞行为」的重排结果:

排名

帧场景

重排分

1

高速

0.5898

2

施工区

0.5018

3

隧道

0.4858

4

路口

0.4830

5

路口

0.4089

6

普通道路

0.3819

高速帧排在首位,符合查询语义。两种调用方式对同一帧给出的分数完全一致,可按工程需要选择:需要「召回 + 精排」一步完成时用 ranker,已经拿到候选帧列表、只想重排时用 REST 接口。

说明

多模态重排需要在 params 中带 is_multimodal,并可通过 instruct 指定排序侧重(如优先考虑主体、动作、场景与细粒度视觉细节)。REST 接口的 documents 为帧 URL 字符串数组,且不支持 top_n,会为每个候选各返回一个得分。

方案价值

维度

传统自建方案

Milvus AI Function

开发周期

抽帧、推理、向量库、元数据库多系统联调

一次建表挂函数,写入与检索即用

推理运维

自建 GPU 推理集群,需扩缩容与故障恢复

由 Milvus 托管调用,业务侧零运维

数据流转

帧在对象存储、推理集群、向量库间多次搬运

写入即推理,数据不出 Milvus 实例

多模态检索

需自建文本与图像向量空间对齐

qwen3-vl-embedding 原生支持以文搜帧与以图搜帧

标注口径

人工标注慢、成本高、判定口径不一

prompt 统一判定规则,标签口径一致可复现

对智能驾驶团队而言,这套方案的直接价值是:

  • Corner Case 挖掘提速:以文搜帧加结构化过滤,再叠加多模态重排,长尾场景可以用一句自然语言捞出。

  • 自动标注降本:AI_CLASSIFY、AI_EXTRACT、AI_ENTITY_EXTRACT 在写入时以统一口径产出标签,人工只做审核与精修。

  • 链路收敛:推理内聚进向量数据库,写入即向量化与结构化理解,检索即多模态语义召回,行车数据全程不出 Milvus 实例。