基于YOLOv11的暴力行为检测模型训练与前端部署完整指南

本文详细记录了使用YOLOv11训练暴力行为检测模型的完整过程,包括数据集准备、模型训练、问题排查以及前端集成部署。适合有深度学习基础但缺乏项目实战经验的开发者参考。

📋 项目概述

在这里插入图片描述

目标

构建一个能够实时检测视频中打架斗殴人员推搡等暴力行为的AI系统,并将检测结果显示在前端Web页面上。

技术栈

  • 深度学习框架: Ultralytics YOLOv11
  • 后端服务: Flask + Python
  • 前端技术: HTML5 + JavaScript + Canvas API
  • 数据集: Real Life Violence Situations Dataset (Kaggle)
  • 硬件环境: NVIDIA RTX 4070 (12GB显存)

项目结构

e:\测试大模型告警\
├── preprocess_violence_data_v2.py  # 数据预处理脚本
├── train_violence_yolo11.py        # 模型训练脚本
├── violence_detection_backend.py   # 后端API服务
├── index.html                      # 前端页面(已集成暴力检测)
├── script.js                       # 前端检测逻辑
└── runs\detect\violence_yolo11n4\   # 训练输出目录
    └── weights\best.pt             # 训练好的模型

🚀 第一步:环境准备

1.1 安装依赖

# 更新pip并安装Ultralytics
pip install -U pip
pip install -U ultralytics

# 安装其他依赖
pip install opencv-python numpy matplotlib seaborn tqdm supervision flask flask-cors pillow

1.2 验证安装

from ultralytics import YOLO
print(YOLO)  # 应输出版本信息,如 8.3.x

⚠️ 常见问题: NumPy版本冲突

错误: A module that was compiled using NumPy 1.x cannot be run in NumPy 2.x

解决方案:

pip install "numpy<2" --force-reinstall
pip install opencv-python==4.8.1.78

📊 第二步:数据准备

2.1 数据集介绍

使用 Real Life Violence Situations Dataset 数据集:

  • 来源: Kaggle
  • 内容: 包含 Violence(暴力)和 NonViolence(非暴力)两个类别的视频
  • 格式: 视频文件(.mp4)
  • 路径: E:\测试大模型告警\Real Life Violence Dataset

2.2 数据预处理

原始数据是视频格式,需要转换为YOLO训练格式(图像+标注)。

预处理脚本 (preprocess_violence_data_v2.py):

import cv2
import os
from pathlib import Path
import random
import shutil

def preprocess_violence_dataset():
    # 配置路径
    dataset_dir = r"E:\测试大模型告警\Real Life Violence Dataset"
    output_dir = r"E:\Downloads\violence_yolo_dataset"

    # 清理并创建输出目录
    if os.path.exists(output_dir):
        shutil.rmtree(output_dir)
    os.makedirs(output_dir, exist_ok=True)

    # 类别定义:0=non_violence, 1=violence
    classes = ["non_violence", "violence"]
    class_folders = {"non_violence": "NonViolence", "violence": "Violence"}
    train_ratio = 0.8

    # 创建目录结构
    for split in ["train", "val"]:
        os.makedirs(os.path.join(output_dir, "images", split), exist_ok=True)
        os.makedirs(os.path.join(output_dir, "labels", split), exist_ok=True)

    total_frames = 0

    for cls_idx, cls_name in enumerate(classes):
        folder_name = class_folders[cls_name]
        cls_path = os.path.join(dataset_dir, folder_name)
        
        # 获取所有视频文件
        videos = [f for f in os.listdir(cls_path) if f.endswith(('.mp4', '.avi', '.mov'))]
        random.seed(42)
        random.shuffle(videos)

        # 划分训练集和验证集
        train_count = int(len(videos) * train_ratio)
        train_videos = videos[:train_count]
        val_videos = videos[train_count:]

        for split, video_list in [("train", train_videos), ("val", val_videos)]:
            split_img_dir = os.path.join(output_dir, "images", split)
            split_lbl_dir = os.path.join(output_dir, "labels", split)

            for video in video_list:
                video_path = os.path.join(cls_path, video)
                cap = cv2.VideoCapture(video_path)
                frame_count = 0
                saved_frames = 0

                while cap.isOpened():
                    ret, frame = cap.read()
                    if not ret:
                        break

                    # 每5帧取一帧(减少冗余)
                    if frame_count % 5 == 0:
                        video_stem = Path(video).stem
                        img_name = f"{cls_name}_{video_stem}_{frame_count:06d}.jpg"
                        img_path = os.path.join(split_img_dir, img_name)

                        # 保存图像
                        cv2.imwrite(img_path, frame)

                        # 创建YOLO格式标注
                        label_path = os.path.join(split_lbl_dir, img_name.replace('.jpg', '.txt'))
                        with open(label_path, 'w') as f:
                            # 整图作为检测区域(简化方案)
                            f.write(f"{cls_idx} 0.5 0.5 1.0 1.0\n")

                        saved_frames += 1
                        total_frames += 1

                    frame_count += 1

                cap.release()

    print(f"预处理完成!总帧数: {total_frames}")
    return output_dir

if __name__ == "__main__":
    preprocess_violence_dataset()

2.3 创建data.yaml配置文件

path: E:/Downloads/violence_yolo_dataset

train: images/train
val: images/val

nc: 2
names: ['non_violence', 'violence']

🔧 第三步:模型训练

3.1 训练脚本

from ultralytics import YOLO
import torch

def train_violence_model():
    # 检查CUDA可用性
    device = "cuda:0" if torch.cuda.is_available() else "cpu"
    print(f"使用设备: {device}")
    
    if torch.cuda.is_available():
        print(f"GPU: {torch.cuda.get_device_name(0)}")

    # 加载YOLOv11n预训练模型(轻量版)
    model = YOLO("yolo11n.pt")

    # 训练配置
    results = model.train(
        data="E:/Downloads/violence_yolo_dataset/data.yaml",
        epochs=50,
        imgsz=640,
        batch=16,
        workers=4,
        device=0 if torch.cuda.is_available() else "cpu",
        name="violence_yolo11n",
        patience=20,
        cache=False,
        amp=True,  # 混合精度加速
        verbose=True,
        # 数据增强
        hsv_h=0.015,
        hsv_s=0.7,
        hsv_v=0.4,
        translate=0.1,
        scale=0.5,
        fliplr=0.5,
        mosaic=1.0,
    )

    print(f"训练完成!最佳权重: {model.best}")
    
    # 验证模型
    metrics = model.val()
    print(f"mAP50: {metrics.box.map50:.4f}")
    print(f"mAP50-95: {metrics.box.map:.4f}")

if __name__ == "__main__":
    train_violence_model()

3.2 运行训练

python train_violence_yolo11.py

训练参数说明:

  • epochs=50: 训练轮数
  • batch=16: 批次大小(RTX 4070可支持)
  • imgsz=640: 输入图像尺寸
  • amp=True: 启用混合精度训练,加速且省显存
  • patience=20: 早停耐心值

🐛 第四步:问题排查与解决

问题1: cv2.imwrite无法写入图像

现象: 预处理时labels文件夹创建成功,但images文件夹为空

原因: 目录权限问题,某些路径下OpenCV无法写入文件

解决方案: 更换输出目录到用户有权限的路径

# 原路径(可能无权限)
output_dir = r"E:\测试大模型告警\violence_yolo_dataset"

# 修改为Downloads目录
output_dir = r"E:\Downloads\violence_yolo_dataset"

问题2: 训练时数据集路径错误

现象:

FileNotFoundError: train: Error loading data from E:\...\images\train

原因: data.yaml中的path使用了相对路径或错误路径

解决方案: 使用绝对路径

path: E:/Downloads/violence_yolo_dataset  # 绝对路径
train: images/train
val: images/val

问题3: 视频检测时标记框不显示

现象: 检测到暴力行为但视频上没有红色/绿色框

原因: Canvas内部分辨率与显示尺寸不匹配

解决方案: 在绘制前动态调整canvas分辨率

// 获取canvas显示尺寸
const rect = canvas.getBoundingClientRect();
const displayWidth = rect.width;
const displayHeight = rect.height;

// 设置canvas内部分辨率与显示尺寸一致
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
    canvas.width = displayWidth;
    canvas.height = displayHeight;
}

// 计算缩放比例
const scaleX = canvas.width / originalWidth;
const scaleY = canvas.height / originalHeight;

// 绘制时进行坐标缩放
const scaledX1 = x1 * scaleX;
const scaledY1 = y1 * scaleY;

🌐 第五步:后端API开发

5.1 创建Flask服务

from flask import Flask, request, jsonify
from flask_cors import CORS
from ultralytics import YOLO
import cv2
import numpy as np
import base64
from PIL import Image
import io

app = Flask(__name__)
CORS(app)

# 加载模型
MODEL_PATH = r"E:\测试大模型告警\runs\detect\violence_yolo11n4\weights\best.pt"
model = YOLO(MODEL_PATH)

@app.route('/detect/violence', methods=['POST'])
def detect_violence():
    try:
        data = request.json
        image_base64 = data.get('image', '')
        conf_threshold = data.get('conf_threshold', 0.5)
        
        # 解码base64图像
        if ',' in image_base64:
            image_base64 = image_base64.split(',')[1]
        
        image_bytes = base64.b64decode(image_base64)
        image = Image.open(io.BytesIO(image_bytes))
        image_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
        
        # 执行检测
        results = model(image_cv, conf=conf_threshold)
        
        # 解析结果
        detections = []
        for result in results:
            boxes = result.boxes
            if boxes is not None:
                for box in boxes:
                    x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
                    confidence = float(box.conf[0].cpu().numpy())
                    class_id = int(box.cls[0].cpu().numpy())
                    class_name = result.names[class_id]
                    
                    detections.append({
                        'class': class_name,
                        'confidence': confidence,
                        'bbox': [float(x1), float(y1), float(x2), float(y2)]
                    })
        
        return jsonify({
            'success': True,
            'detections': detections,
            'count': len(detections)
        })
        
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5003)

5.2 启动服务

python violence_detection_backend.py

🎨 第六步:前端集成

6.1 HTML结构

index.html底部添加暴力行为检测区域:

<!-- 暴力行为检测区域 -->
<section class="container mx-auto px-4 py-6">
    <div class="bg-white rounded-lg shadow-sm p-4">
        <h2 class="text-lg font-semibold mb-3 flex items-center">
            <i class="fa fa-exclamation-triangle mr-2 text-red-500"></i>暴力行为检测
            <span class="ml-2 text-xs text-gray-500">(YOLOv11模型)</span>
        </h2>
        <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
            <!-- 视频上传区域 -->
            <div class="border-2 border-dashed border-gray-300 rounded-lg p-4 text-center" id="violenceVideoUploadArea">
                <i class="fa fa-video-camera text-4xl text-gray-400 mb-3"></i>
                <p class="text-gray-500 mb-3">上传视频进行暴力行为检测</p>
                <input type="file" id="violenceVideoInput" accept="video/*" class="hidden">
                <button id="selectViolenceVideoBtn" class="px-4 py-2 bg-red-100 text-red-700 rounded-md hover:bg-red-200 transition-colors">
                    <i class="fa fa-folder-open mr-2"></i>选择视频
                </button>
            </div>
            
            <!-- 视频播放和检测区域 -->
            <div id="violenceVideoPlayerArea" class="hidden">
                <div class="relative">
                    <video id="violenceVideo" class="w-full h-64 bg-black rounded-lg object-cover" controls></video>
                    <canvas id="violenceDetectionCanvas" class="absolute top-0 left-0 w-full h-64 pointer-events-none"></canvas>
                    <!-- 检测状态 -->
                    <div id="violenceDetectionStatus" class="absolute top-2 right-2 px-2 py-1 bg-gray-800 bg-opacity-80 text-white text-xs rounded hidden">
                        <i class="fa fa-spinner fa-spin mr-1"></i>检测中
                    </div>
                    <!-- 检测结果统计 -->
                    <div id="violenceDetectionStats" class="absolute bottom-2 left-2 px-3 py-2 bg-black bg-opacity-70 text-white text-sm rounded hidden">
                        <div class="flex gap-4">
                            <span id="violenceCount" class="text-red-400 font-bold">暴力行为: 0</span>
                            <span id="nonViolenceCount" class="text-green-400 font-bold">正常: 0</span>
                        </div>
                        <div id="violenceAlert" class="hidden mt-1 text-red-300 text-xs">
                            <i class="fa fa-warning mr-1"></i>检测到暴力行为!
                        </div>
                    </div>
                </div>
                <!-- 控制按钮 -->
                <div class="flex gap-2 mt-3">
                    <button id="startViolenceDetectionBtn" class="flex-1 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors flex items-center justify-center">
                        <i class="fa fa-play mr-2"></i>开始检测
                    </button>
                    <button id="stopViolenceDetectionBtn" class="flex-1 px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600 transition-colors flex items-center justify-center hidden">
                        <i class="fa fa-stop mr-2"></i>停止检测
                    </button>
                    <button id="removeViolenceVideoBtn" class="px-4 py-2 bg-red-100 text-red-600 rounded-md hover:bg-red-200 transition-colors">
                        <i class="fa fa-trash"></i>
                    </button>
                </div>
                <!-- 检测参数 -->
                <div class="mt-3 p-3 bg-gray-50 rounded-md">
                    <label class="block text-sm font-medium text-gray-700 mb-2">
                        置信度阈值: <span id="violenceThresholdValue" class="text-red-600 font-bold">0.5</span>
                    </label>
                    <input type="range" id="violenceConfidenceThreshold" min="0.1" max="0.9" step="0.05" value="0.5"
                           class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
                    <div class="flex justify-between text-xs text-gray-500 mt-1">
                        <span>0.1 (更敏感)</span>
                        <span>0.9 (更严格)</span>
                    </div>
                </div>
            </div>
        </div>
    </div>
</section>

6.2 JavaScript检测逻辑

// 暴力行为检测相关变量
let violenceVideoFile = null;
let isViolenceDetecting = false;
let violenceDetectionInterval = null;
let violenceDetectionCount = 0;
let nonViolenceDetectionCount = 0;
let violenceDetectionConfig = {
    confidenceThreshold: 0.5
};

// 初始化事件监听
document.getElementById('selectViolenceVideoBtn').addEventListener('click', () => {
    document.getElementById('violenceVideoInput').click();
});

document.getElementById('violenceVideoInput').addEventListener('change', handleViolenceVideoSelect);
document.getElementById('startViolenceDetectionBtn').addEventListener('click', startViolenceDetection);
document.getElementById('stopViolenceDetectionBtn').addEventListener('click', stopViolenceDetection);
document.getElementById('removeViolenceVideoBtn').addEventListener('click', removeViolenceVideo);
document.getElementById('violenceConfidenceThreshold').addEventListener('input', handleViolenceThresholdChange);

// 处理视频选择
function handleViolenceVideoSelect(e) {
    const file = e.target.files[0];
    if (!file || !file.type.startsWith('video/')) {
        alert('请选择视频文件');
        return;
    }
    
    violenceVideoFile = file;
    const videoUrl = URL.createObjectURL(file);
    document.getElementById('violenceVideo').src = videoUrl;
    
    // 切换显示
    document.getElementById('violenceVideoUploadArea').classList.add('hidden');
    document.getElementById('violenceVideoPlayerArea').classList.remove('hidden');
    
    // 重置统计
    violenceDetectionCount = 0;
    nonViolenceDetectionCount = 0;
    updateViolenceDetectionStats();
}

// 开始检测
async function startViolenceDetection() {
    if (!violenceVideoFile) {
        alert('请先上传视频');
        return;
    }
    
    isViolenceDetecting = true;
    
    // 更新UI
    document.getElementById('startViolenceDetectionBtn').classList.add('hidden');
    document.getElementById('stopViolenceDetectionBtn').classList.remove('hidden');
    document.getElementById('violenceDetectionStatus').classList.remove('hidden');
    document.getElementById('violenceDetectionStats').classList.remove('hidden');
    
    // 播放视频
    const video = document.getElementById('violenceVideo');
    video.play();
    
    // 开始检测循环(每200ms检测一帧)
    violenceDetectionInterval = setInterval(async () => {
        if (!isViolenceDetecting || video.paused || video.ended) {
            if (video.ended) stopViolenceDetection();
            return;
        }
        await detectViolenceFrame();
    }, 200);
}

// 检测单帧
async function detectViolenceFrame() {
    const video = document.getElementById('violenceVideo');
    const canvas = document.getElementById('violenceDetectionCanvas');
    const ctx = canvas.getContext('2d');

    const videoWidth = video.videoWidth || 640;
    const videoHeight = video.videoHeight || 480;

    // 调整canvas尺寸
    const rect = canvas.getBoundingClientRect();
    if (canvas.width !== rect.width || canvas.height !== rect.height) {
        canvas.width = rect.width;
        canvas.height = rect.height;
    }

    // 捕获当前帧
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = videoWidth;
    tempCanvas.height = videoHeight;
    tempCanvas.getContext('2d').drawImage(video, 0, 0);
    
    const imageData = tempCanvas.toDataURL('image/jpeg', 0.8);

    try {
        // 调用检测API
        const response = await fetch('http://localhost:5003/detect/violence', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                image: imageData,
                conf_threshold: violenceDetectionConfig.confidenceThreshold
            })
        });

        const result = await response.json();
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        if (result.success && result.detections.length > 0) {
            drawViolenceDetections(ctx, result.detections, canvas.width, canvas.height, videoWidth, videoHeight);
            updateViolenceDetectionCounts(result.detections);
        }
    } catch (error) {
        console.error('检测失败:', error);
    }
}

// 绘制检测结果
function drawViolenceDetections(ctx, detections, canvasWidth, canvasHeight, originalWidth, originalHeight) {
    const colors = {
        'violence': '#ff0000',
        'non_violence': '#00ff00'
    };
    
    const labels = {
        'violence': '暴力行为',
        'non_violence': '正常'
    };

    const scaleX = canvasWidth / originalWidth;
    const scaleY = canvasHeight / originalHeight;

    detections.forEach(det => {
        const { bbox, class: className, confidence } = det;
        const [x1, y1, x2, y2] = bbox;

        // 坐标缩放
        const scaledX1 = x1 * scaleX;
        const scaledY1 = y1 * scaleY;
        const scaledX2 = x2 * scaleX;
        const scaledY2 = y2 * scaleY;
        const width = scaledX2 - scaledX1;
        const height = scaledY2 - scaledY1;

        // 绘制检测框
        ctx.strokeStyle = colors[className] || '#00ff00';
        ctx.lineWidth = 2;
        ctx.strokeRect(scaledX1, scaledY1, width, height);

        // 绘制标签
        const label = `${labels[className]}: ${(confidence * 100).toFixed(0)}%`;
        ctx.font = 'bold 12px Arial';
        const textMetrics = ctx.measureText(label);
        
        ctx.fillStyle = colors[className] || '#00ff00';
        ctx.fillRect(scaledX1, scaledY1 - 18, textMetrics.width + 8, 18);
        
        ctx.fillStyle = '#ffffff';
        ctx.fillText(label, scaledX1 + 4, scaledY1 - 6);
    });
}

// 更新统计
function updateViolenceDetectionCounts(detections) {
    const violenceCount = detections.filter(d => d.class === 'violence').length;
    const nonViolenceCount = detections.filter(d => d.class === 'non_violence').length;
    
    violenceDetectionCount += violenceCount;
    nonViolenceDetectionCount += nonViolenceCount;
    
    document.getElementById('violenceCount').textContent = `暴力行为: ${violenceDetectionCount}`;
    document.getElementById('nonViolenceCount').textContent = `正常: ${nonViolenceDetectionCount}`;
    
    // 显示警告
    if (violenceCount > 0) {
        document.getElementById('violenceAlert').classList.remove('hidden');
    }
}

// 停止检测
function stopViolenceDetection() {
    isViolenceDetecting = false;
    clearInterval(violenceDetectionInterval);
    
    document.getElementById('startViolenceDetectionBtn').classList.remove('hidden');
    document.getElementById('stopViolenceDetectionBtn').classList.add('hidden');
    document.getElementById('violenceDetectionStatus').classList.add('hidden');
    
    document.getElementById('violenceVideo').pause();
}

// 移除视频
function removeViolenceVideo() {
    stopViolenceDetection();
    violenceVideoFile = null;
    document.getElementById('violenceVideo').src = '';
    document.getElementById('violenceVideoInput').value = '';
    
    document.getElementById('violenceVideoPlayerArea').classList.add('hidden');
    document.getElementById('violenceVideoUploadArea').classList.remove('hidden');
    
    const canvas = document.getElementById('violenceDetectionCanvas');
    canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
}

// 调整阈值
function handleViolenceThresholdChange(e) {
    violenceDetectionConfig.confidenceThreshold = parseFloat(e.target.value);
    document.getElementById('violenceThresholdValue').textContent = e.target.value;
}

📊 第六步:效果展示

检测效果

  • 红色框: 检测到暴力行为
  • 绿色框: 检测到正常行为
  • 实时统计: 显示暴力行为和正常行为的计数
  • 警告提示: 检测到暴力行为时显示红色警告

性能指标

  • mAP50: 约 85-95%
  • 推理速度: RTX 4070 上约 100-200 FPS
  • 模型大小: 约 5-6 MB (YOLOv11n)

🚀 第七步:一键启动脚本更新

start-all-debug.bat 更新

添加暴力行为检测服务启动:

:: 检查暴力行为检测模型
set "VIOLENCE_PATH=runs\detect\violence_yolo11n4\weights\best.pt"
if exist "%VIOLENCE_PATH%" (
    echo [OK] Violence Detection Model found!
    set "VIOLENCE_MODEL_FOUND=1"
) else (
    echo [Warning] Violence detection model not found
)

:: 启动暴力行为检测服务 (端口5003)
echo [4/6] Starting violence detection service (Port: 5003)...
if %VIOLENCE_MODEL_FOUND% == 1 (
    start "Violence Detection Service" cmd /k "python violence_detection_backend.py"
)

stop-all.bat 更新

添加端口5003的停止逻辑:

:: 停止暴力行为检测服务 (端口5003)
for /f "tokens=5" %%a in ('netstat -ano ^| findstr :5003') do (
    taskkill /F /PID %%a 2>nul
    echo      Stopped violence detection service (PID: %%a)
)

📝 总结

核心要点

  1. 数据预处理是关键: 视频转图像时要注意权限问题和帧率控制
  2. 坐标缩放很重要: Canvas显示尺寸与视频原始尺寸不同时必须做坐标映射
  3. 阈值可调: 根据实际场景调整置信度阈值(0.1-0.9)
  4. 模型轻量化: 使用YOLOv11n保证实时性

后续优化方向

  1. 添加更多数据增强策略提升模型泛化能力
  2. 实现多目标跟踪,追踪暴力行为人员
  3. 添加声音检测,结合音频提升检测准确率
  4. 优化前端UI,支持更多交互功能

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐