AI学习笔记三十六:基于 YOLOv8 与 Qwen3.5 的多模态视频行为分析系统

若该文为原创文章,转载请注明原文出处。

在B站看到大神使用YOLOV26+LVM多模态视频检测,所以想偿试一下,

基于 YOLOv8 与 Qwen3.5 的多模态视频行为分析系统.

一、背景

随着智慧城市、智能安防、工业巡检等领域的快速发展,传统视频监控系统仅能实现"看得见",却难以"看得懂"。海量监控视频依赖人工值守,存在效率低、漏报率高、响应滞后等问题。

近年来,计算机视觉与大型语言模型(LLM)技术取得突破性进展。YOLO 系列目标检测算法以其高实时性与准确率成为边缘端视觉感知的主流选择;而多模态大模型(如通义千问 Qwen3.5)具备强大的图文理解与推理能力,能够对检测到的物体、场景进行深度语义分析。将两者融合,可以构建"感知‑理解‑决策"一体化的智能视频分析系统,实现从"目标检测"到"行为理解"的跨越。

本程序正是在此背景下开发:利用 YOLOv8 对视频流进行实时目标检测与跟踪,提取物体类别、位置、轨迹等结构化信息,并调用 Qwen3.5 多模态 API 对关键帧进行行为分析,最终在视频画面上叠加检测框、跟踪 ID 及行为分析文本,输出带智能标注的高清视频。系统适用于校园安全、交通路口、工地监管、居家看护等场景,可有效降低人工监控负担,提升异常行为预警能力。

二、方案

2.1 系统架构

本系统采用 异步解耦、分层处理 的设计思想,整体架构分为三层:

  • 感知层:YOLOv8 目标检测与跟踪模块,逐帧处理视频,输出带跟踪 ID 的物体检测结果。

  • 理解层:异步任务队列 + Qwen3.5 API 调用,对关键帧的结构化检测信息进行语义分析,生成场景描述、异常判断、风险等级及处理建议。

  • 应用层:视频渲染与输出模块,将检测框、跟踪 ID 以及分析文本叠加到原始视频上,支持实时预览与保存为 1080P 高清视频。

系统工作流程如下图所示:

2.2 核心功能模块

模块 技术实现 功能描述
目标检测与跟踪 YOLOv8 + ByteTrack(内置) 检测视频中的人、车、物体等,并为每个目标分配唯一 ID,输出边界框、类别、置信度、跟踪 ID。
多模态行为分析 Qwen3.5‑omni‑flash(阿里云百炼 API) 将检测结果与关键帧图像一同发送给大模型,分析场景是否存在异常行为,返回 JSON 格式的分析结论(场景概括、异常标志、风险等级、详细说明、处理建议)。
中文文本渲染 OpenCV + PIL(Pillow) 解决 OpenCV 原生 putText 不支持中文的问题,使用 TrueType 字体(如黑体)在视频左上角绘制清晰的中文分析文本。
视频处理与输出 OpenCV + 高质量缩放 支持输入任意分辨率视频,输出 1080P(1920×1080)高清视频;使用 INTER_LANCZOS4 插值与 20 Mbps 比特率设置,保证画面清晰。
异步调用与频率控制 threading + Queue 为避免 API 调用阻塞视频处理,采用后台线程队列,每隔 FRAME_SKIP 帧(默认 30 帧)触发一次分析,并限制调用间隔(2 秒),平衡成本与实时性。

2.3 关键技术参数

  • 检测模型:YOLOv8n(可替换为 s/m/l/x 以提升精度)

  • 多模态模型:qwen3.5‑omni‑flash(成本低、速度快)

  • 分析频率:每 30 帧调用一次 API(约 1 秒一次,若视频 30fps)

  • 输出分辨率:1920×1080(保持原始宽高比)

  • 字体大小:主文本 32px,辅助文本 24px

  • 编码格式:H.264(MP4),比特率 20 Mbps

2.4 部署与使用

  1. 环境准备

    • Python 3.9+,安装依赖:pip install ultralytics opencv-python openai python-dotenv Pillow

    • 获取阿里云百炼 API Key,并在 .env 文件中配置 DASHSCOPE_API_KEY

  2. 运行方式

    • 修改 INPUT_VIDEO_PATH 为待分析视频路径

    • 执行 python video_analyzer.py

    • 程序自动下载 YOLOv8 模型,逐帧处理,实时显示预览画面(按 q 可提前终止)

    • 处理完成后在指定路径生成 output_analyzed.mp4

  3. 参数调优

    • FRAME_SKIP:增大可降低 API 调用成本,减小可提高分析密度

    • CONF_THRESHOLD:调整检测置信度阈值,过滤低质量框

    • TARGET_WIDTH:可改为 1280 等其他宽度

    • FONT_PATH:根据操作系统修改中文字体路径

2.5 应用示例

  • 校园安全:检测儿童在危险区域(如马路、水池边)玩耍,及时预警。

  • 工地监管:识别工人未戴安全帽、闯入禁区等违规行为。

  • 交通路口:分析行人闯红灯、车辆违规变道等事件。

  • 居家养老:监测老人摔倒、长时间静止等异常状况。

三、环境安装

需要安装以下 Python 依赖库。请在终端或命令提示符中运行以下命令进行安装:

复制代码
pip install ultralytics opencv-python openai python-dotenv Pillow

依赖说明

用途
ultralytics 加载并运行 YOLOv8 模型,支持目标检测与跟踪
opencv-python 视频读取、图像处理、绘制检测框、保存输出视频
openai 调用阿里云百炼平台的 Qwen3.5 API(兼容 OpenAI 接口)
python-dotenv .env 文件中读取 DASHSCOPE_API_KEY 环境变量
Pillow 在视频帧上绘制中文字符(OpenCV 原生不支持中文)

四、测试代码

复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import cv2
import json
import base64
import re
import threading
from queue import Queue
from dotenv import load_dotenv
from openai import OpenAI
from ultralytics import YOLO
import numpy as np
from PIL import Image, ImageDraw, ImageFont

load_dotenv()

QWEN_API_KEY = os.getenv("DASHSCOPE_API_KEY", "your-api-key-here")
QWEN_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
MODEL_NAME = "qwen3.5-omni-flash"

INPUT_VIDEO_PATH = "input_video.mp4"
OUTPUT_VIDEO_PATH = "output_analyzed.mp4"
YOLO_MODEL_PATH = "yolov8n.pt"
FRAME_SKIP = 30
CONF_THRESHOLD = 0.5
SHOW_PREVIEW = True
SAVE_RESULT = True

# 1080P 宽度 = 1920
TARGET_WIDTH = 1920
FONT_SIZE = 32          # 主文本字体大小
FONT_SMALL_SIZE = 24    # 辅助文本字体大小

# 中文字体路径(Windows 黑体,其他系统请自行修改)
FONT_PATH = "C:/Windows/Fonts/simhei.ttf"

client = OpenAI(api_key=QWEN_API_KEY, base_url=QWEN_BASE_URL)

def load_yolo_model(model_path):
    try:
        print(f"Loading YOLOv8 model: {model_path}")
        model = YOLO(model_path)
        return model
    except Exception as e:
        print(f"模型加载失败: {e}")
        if os.path.exists(model_path):
            os.remove(model_path)
        model = YOLO(model_path)
        return model

model = load_yolo_model(YOLO_MODEL_PATH)

def frame_to_base64(frame):
    _, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
    b64_str = base64.b64encode(buffer).decode('utf-8')
    return f"data:image/jpeg;base64,{b64_str}"

def extract_json_from_text(text):
    pattern = r'```(?:json)?\s*(\{.*?\})\s*```'
    match = re.search(pattern, text, re.DOTALL)
    if match:
        return match.group(1)
    start = text.find('{')
    end = text.rfind('}')
    if start != -1 and end != -1 and end > start:
        return text[start:end+1]
    return text

def analyze_behavior_with_qwen(detections, frame_image_path=None):
    detections_summary = []
    for d in detections:
        detections_summary.append({
            "track_id": d['track_id'],
            "class": d['class'],
            "confidence": d['confidence'],
            "bbox": d['bbox']
        })
    
    prompt = f"""
你是一个专业的视频监控行为分析专家。请基于以下YOLOv8模型检测到的物体信息,分析当前场景是否存在异常行为。
检测到的物体列表(JSON格式):{json.dumps(detections_summary, ensure_ascii=False, indent=2)}
请按以下JSON格式输出你的分析结果,不要包含其他任何解释或标记:
{{
    "scene_summary": "一句话概括当前场景",
    "abnormal_behavior": true/false,
    "risk_level": "低/中/高",
    "analysis_details": "详细的行为分析说明",
    "suggestion": "如果有异常,请给出处理建议"
}}
"""
    print("\n" + "="*50)
    print(f"[API调用] 发送分析请求,检测到 {len(detections)} 个物体")
    print("="*50)

    user_content = [{"type": "text", "text": prompt}]
    if frame_image_path:
        user_content.insert(0, {"type": "image_url", "image_url": {"url": frame_image_path}})
    
    try:
        response = client.chat.completions.create(
            model=MODEL_NAME,
            messages=[{"role": "user", "content": user_content}],
            temperature=0.3,
            max_tokens=500,
        )
        raw_text = response.choices[0].message.content
        print(f"[API返回原始内容] {raw_text}")
        
        json_str = extract_json_from_text(raw_text)
        try:
            result_json = json.loads(json_str)
            print(f"[解析结果] 异常行为: {result_json.get('abnormal_behavior')}, 风险等级: {result_json.get('risk_level')}")
            print(f"           场景概括: {result_json.get('scene_summary')}")
            return result_json
        except json.JSONDecodeError as e:
            print(f"[JSON解析失败] {e}")
            return {"scene_summary": raw_text[:100], "abnormal_behavior": False, "risk_level": "未知"}
    except Exception as e:
        print(f"[API调用失败] {e}")
        return None

def draw_chinese_text_opencv(img, text, position, font_path, font_size, color_bgr):
    """使用 PIL 在 OpenCV 图像上绘制中文文本"""
    try:
        img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(img_pil)
        font = ImageFont.truetype(font_path, font_size)
        color_rgb = (color_bgr[2], color_bgr[1], color_bgr[0])
        draw.text(position, text, font=font, fill=color_rgb)
        img[:] = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
    except Exception as e:
        print(f"[中文绘制错误] {e}")
    return img

def draw_analysis_on_frame(frame, detections, latest_analysis):
    # 绘制 YOLO 检测框(英文用 OpenCV 原生方法)
    for det in detections:
        bbox = det['bbox']
        x1, y1, x2, y2 = map(int, bbox)
        label = f"{det['class']} {det['confidence']:.2f} ID:{det['track_id']}"
        cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
        cv2.putText(frame, label, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
    
    # 绘制中文分析结果
    if latest_analysis and isinstance(latest_analysis, dict):
        summary = latest_analysis.get('scene_summary', '无描述')
        abnormal = latest_analysis.get('abnormal_behavior')
        risk = latest_analysis.get('risk_level', '未知')
        status_text = f"异常: {'是' if abnormal else '否'} | 风险: {risk}"
        suggestion = latest_analysis.get('suggestion', '')
        short_sug = suggestion[:60] + ('...' if len(suggestion) > 60 else '') if suggestion else ''
        
        # 绘制三行文本,调整位置以适应 1080P
        frame = draw_chinese_text_opencv(frame, summary, (20, 50), FONT_PATH, FONT_SIZE, (0, 0, 255))
        frame = draw_chinese_text_opencv(frame, status_text, (20, 100), FONT_PATH, FONT_SMALL_SIZE, (0, 0, 255))
        if short_sug:
            frame = draw_chinese_text_opencv(frame, short_sug, (20, 140), FONT_PATH, FONT_SMALL_SIZE, (0, 0, 255))
    return frame

def resize_frame_to_width(frame, target_width):
    """高质量缩放,使用 LANCZOS 插值"""
    h, w = frame.shape[:2]
    if w == target_width:
        return frame
    ratio = target_width / w
    new_h = int(h * ratio)
    # 使用 INTER_LANCZOS4 获得更清晰的缩放结果
    return cv2.resize(frame, (target_width, new_h), interpolation=cv2.INTER_LANCZOS4)

def main():
    if not os.path.exists(INPUT_VIDEO_PATH):
        print(f"错误:视频文件不存在 - {INPUT_VIDEO_PATH}")
        return

    cap = cv2.VideoCapture(INPUT_VIDEO_PATH)
    original_fps = int(cap.get(cv2.CAP_PROP_FPS))
    original_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    original_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    print(f"原始视频: {original_width}x{original_height}, {original_fps} fps")
    
    output_width = TARGET_WIDTH
    # 读取一帧确定缩放后的高度
    ret, sample_frame = cap.read()
    if not ret:
        print("无法读取视频帧")
        return
    cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
    sample_resized = resize_frame_to_width(sample_frame, output_width)
    output_height = sample_resized.shape[0]
    print(f"输出视频: {output_width}x{output_height}, {original_fps} fps")

    out = None
    if SAVE_RESULT:
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        out = cv2.VideoWriter(OUTPUT_VIDEO_PATH, fourcc, original_fps, (output_width, output_height))
        # 尝试设置更高的比特率以减少模糊(部分后端支持)
        if out.isOpened():
            try:
                out.set(cv2.CAP_PROP_BITRATE, 20000000)  # 20 Mbps
                print("已设置输出比特率为 20 Mbps")
            except:
                print("当前后端不支持设置比特率,使用默认编码质量")

    analysis_queue = Queue()
    latest_analysis = None
    analysis_lock = threading.Lock()

    def worker():
        nonlocal latest_analysis
        while True:
            item = analysis_queue.get()
            if item is None:
                break
            frame_id, frame_b64, detections = item
            result = analyze_behavior_with_qwen(detections, frame_b64)
            if result:
                with analysis_lock:
                    latest_analysis = result
            analysis_queue.task_done()

    worker_thread = threading.Thread(target=worker, daemon=True)
    worker_thread.start()

    frame_idx = 0
    last_analysis_frame = -FRAME_SKIP

    for result in model.track(source=INPUT_VIDEO_PATH, stream=True, persist=True, conf=CONF_THRESHOLD):
        frame_idx += 1
        annotated_frame = result.plot()
        
        detections = []
        if result.boxes is not None:
            boxes = result.boxes.xyxy.cpu().numpy()
            track_ids = result.boxes.id.cpu().numpy() if result.boxes.id is not None else [-1]*len(boxes)
            classes = result.boxes.cls.cpu().numpy()
            confs = result.boxes.conf.cpu().numpy()
            for box, tid, cls, conf in zip(boxes, track_ids, classes, confs):
                detections.append({
                    'track_id': int(tid),
                    'class': model.names[int(cls)],
                    'confidence': float(conf),
                    'bbox': box.tolist()
                })

        if (frame_idx - last_analysis_frame) >= FRAME_SKIP:
            last_analysis_frame = frame_idx
            frame_b64 = frame_to_base64(annotated_frame)
            analysis_queue.put((frame_idx, frame_b64, detections))

        with analysis_lock:
            current_analysis = latest_analysis

        output_frame = draw_analysis_on_frame(annotated_frame, detections, current_analysis)
        output_frame = resize_frame_to_width(output_frame, TARGET_WIDTH)

        if SHOW_PREVIEW:
            cv2.imshow("YOLOv8 + Qwen3.5 Analysis", output_frame)
            if cv2.waitKey(1) & 0xFF == ord('q'):
                break
        if out:
            out.write(output_frame)

        if frame_idx % 100 == 0:
            print(f"已处理 {frame_idx} 帧")

    analysis_queue.put(None)
    analysis_queue.join()
    cap.release()
    if out:
        out.release()
    cv2.destroyAllWindows()
    print(f"处理完成!输出视频: {OUTPUT_VIDEO_PATH} (1080P)")

if __name__ == "__main__":
    main()

输出结果

注意QWen3.5模型免费用量是1,000,000,不要超过哈,超过是要费用的。

后面想在RK3568上处理。

如有侵权,或需要完整代码,请及时联系博主。

相关推荐
小陈phd2 小时前
多模态大模型学习笔记(三十七)——模型管理平台实战:Ollama本地部署与全流程指南
笔记·学习
returnthem2 小时前
使用trae 学习AI编程
学习·ai编程
网络安全实验室2 小时前
【程序人生】程序员接私活常用平台汇总_嵌入式开发外包平台
网络·python·学习·程序人生·web安全·面试·职场和发展
深度学习lover2 小时前
<数据集>yolo 葡萄叶片病害识别<目标检测>
人工智能·python·yolo·目标检测·计算机视觉·葡萄叶片病害识别
青衫码上行2 小时前
【从零开始学习JVM】字符串常量池
java·jvm·学习·面试·string
星幻元宇VR2 小时前
VR动感科普单车|让交通安全教育更有参与感
科技·学习·安全·vr·虚拟现实
handler012 小时前
Linux 基本指令知识点(1)
linux·c++·笔记
思绪无限2 小时前
YOLOv5至YOLOv12升级:稻田虫害检测系统的设计与实现(完整代码+界面+数据集项目)
深度学习·yolo·目标检测·yolov12·yolo全家桶·稻田虫害检测
sheeta19982 小时前
苍穹外卖Day09Day10笔记
笔记