YOLOv11 python3.9 Flask Mysql8火灾识别监测系统 全流程实战教程源码

YOLOv11 python3.9 Flask Mysql8火灾识别监测系统 全流程实战教程源码

从模型训练、识别到 Flask + MySQL 后端部署,手把手搭建一套可运行的火灾识别监测系统。

注:YOLOv11 即 Ultralytics 官方发布的 YOLO11(社区常称 YOLOv11),通过 ultralytics 库使用,权重文件为 yolo11n.pt / yolo11s.pt 等。


我用vue3写个了前端,后端python3.9+flask,yolov11,也加入了yolov8的模型识别,根据需要加载对应的模型。

支持多摄像头,4* 4,3*3,等多种分屏画面展示,效果看起还不错,哈哈,可以交流。

【接上一篇文章, 系列】
YOLOv11 python3.9 Flask Mysql8火灾识别监测系统 全流程实战教程源码

Python 3.9 · Flask 火灾监测识别系统 与 钉钉告警机器人 集成说明文档

【其他系列,2026持续更新中】

VisionForge SDK使用教程06-AI安全帽检测API调用说明文档

VisionForge SDK使用教程05-AI人员打架检测API调用教程

VisionForge SDK使用教程04-AI打电话检测API调用说明文档

VisionForge SDK使用教程03- opencv和FaceDetector类实现人脸检测API调用

VisionForge SDK使用教程02- yolov8实现垃圾检测调用

VisionForge SDK使用教程01- yolov8实现火灾检测调用

在这里插入图片描述

目录

  1. 系统架构
  2. 环境搭建
  3. 数据集准备
  4. 模型训练
  5. 模型评估与推理
  6. Flask + MySQL 后端开发
  7. 前端监控页面
  8. 启动与测试
  9. 进阶优化

一、系统架构

本系统分两大部分:

┌────────────────────────────────────────────────┐
│                火灾识别监测系统                  │
├────────────────────────────────────────────────┤
│  ① 模型侧(离线)                               │
│     └─ YOLOv11 训练火灾/烟雾检测模型 → best.pt   │
├────────────────────────────────────────────────┤
│  ② 后端侧(在线)                               │
│     └─ Flask Web 服务                           │
│         ├─ 模型推理服务(YOLO 封装)             │
│         ├─ 检测 API(图片/视频/摄像头)          │
│         ├─ 告警记录 API                         │
│         └─ MySQL 数据库(告警/设备存储)         │
└────────────────────────────────────────────────┘

核心流程:摄像头/图片/视频 → Flask 接收 → YOLOv11 推理 → 检出 fire/smoke → 写入 MySQL 告警记录 → 前端监控页面实时展示。

项目结构规划

fire_detection_system/
├── yolov11/                     # 模型部分
│   ├── data/
│   │   └── fire_data/           # 数据集(Ultralytics 标准格式)
│   │       ├── images/{train,val,test}
│   │       ├── labels/{train,val,test}
│   │       └── data.yaml
│   ├── split_data.py            # 数据集划分脚本
│   ├── train.py                 # 训练脚本
│   ├── detect.py                # 推理脚本(图片/视频/摄像头)
│   └── runs/detect/train/...    # 训练产出(best.pt 在此)
├── backend/                     # 后端部分
│   ├── app.py                   # Flask 入口 + API
│   ├── model_service.py         # YOLO 推理封装
│   ├── db.py                    # MySQL 连接与建表
│   ├── config.py                # 配置
│   ├── uploads/                 # 上传文件存放
│   └── static/
│       ├── index.html           # 监控页面
│       └── detect.html          # 图片检测页面
└── requirements.txt

二、环境搭建

2.1 创建虚拟环境

# 建议使用 Python 3.9+
conda create -n fire python=3.9 -y
conda activate fire
# 或 Windows 下:
# python -m venv fire_env && fire_env\Scripts\activate

2.2 安装依赖

pip install -U pip
pip install ultralytics torch torchvision torchaudio
pip install opencv-python flask pymysql pandas matplotlib

验证安装(重点确认 CUDA 是否可用):

import torch
from ultralytics import YOLO

print('CUDA 可用:', torch.cuda.is_available())
print('PyTorch 版本:', torch.__version__)

# 下载 yolo11n 预训练权重(首次自动下载)
model = YOLO('yolo11n.pt')

无 GPU 也能训练和推理,只是速度较慢。训练建议显存 ≥ 6GB(batch 调小即可)。


三、数据集准备

3.1 方案 A:使用公开数据集(推荐入门)

Kaggle 有现成的 YOLO 格式火灾/烟雾数据集(如 Smoke and Fire Detection YOLO Dataset,约 2.1 万张图,包含 smoke / fire / both / none 四类)。

下载后通常结构如下,直接得到 YOLO 格式标注(.txt 与图片同名):

fire_data/
├── train/images/*.jpg   train/labels/*.txt
├── valid/images/*.jpg   valid/labels/*.txt
└── test/images/*.jpg    test/labels/*.txt

若下载的数据集是 COCO/VOC 格式,需用 Roboflow 导出为 YOLOv11(YOLO)格式,或自行转换。

3.2 方案 B:自建数据集 + labelImg 标注

pip install labelImg
labelImg          # 打开标注工具,选择图片目录,保存目录设为 labels

标注要点:

  • 类别只设两类:firesmoke(也可合并为一类 fire
  • 保存格式选 YOLO
  • 自动保存开启;一个框对应一行 class_id x_center y_center w h(归一化坐标)

3.3 数据集划分脚本 split_data.py

若你只有一份图片 + 标注,用此脚本按 70/15/15 划分:

import os
import random
import shutil

random.seed(42)

SRC_IMAGES = 'all_images'   # 原始图片目录
SRC_LABELS = 'all_labels'   # 原始标注目录
DEST = 'data/fire_data'

SPLITS = {'train': 0.70, 'val': 0.15, 'test': 0.15}

def main():
    names = [f[:-4] for f in os.listdir(SRC_IMAGES) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
    random.shuffle(names)

    # 计算每个集合的数量
    n_train = int(len(names) * SPLITS['train'])
    n_val = int(len(names) * SPLITS['val'])
    parts = {
        'train': names[:n_train],
        'val': names[n_train:n_train + n_val],
        'test': names[n_train + n_val:],
    }

    for split, files in parts.items():
        img_dir = os.path.join(DEST, 'images', split)
        lbl_dir = os.path.join(DEST, 'labels', split)
        os.makedirs(img_dir, exist_ok=True)
        os.makedirs(lbl_dir, exist_ok=True)

        for name in files:
            img_src = os.path.join(SRC_IMAGES, name + '.jpg')
            img_dst = os.path.join(img_dir, name + '.jpg')
            lbl_src = os.path.join(SRC_LABELS, name + '.txt')
            lbl_dst = os.path.join(lbl_dir, name + '.txt')
            shutil.copy(img_src, img_dst)
            if os.path.exists(lbl_src):
                shutil.copy(lbl_src, lbl_dst)
        print(f'{split}: {len(files)} 张')

if __name__ == '__main__':
    main()

3.4 编写 data.yaml

# data/fire_data/data.yaml
path: D:/fire_detection_system/yolov11/data/fire_data   # 数据集根目录(绝对路径)
train: images/train
val: images/val
test: images/test

nc: 2                        # 类别数
names: ['fire', 'smoke']     # 与标注 class_id 一一对应,建议用英文

四、模型训练

4.1 CLI 方式(一行命令)

cd yolov11
yolo train \
    data=data/fire_data/data.yaml \
    model=yolo11n.pt \
    epochs=100 \
    imgsz=640 \
    batch=16 \
    device=0 \
    name=fire_v1

4.2 Python 脚本方式 train.py(推荐,便于调试)

# train.py
from ultralytics import YOLO

def main():
    # 基于预训练权重微调(迁移学习,收敛快、效果好)
    model = YOLO('yolo11n.pt')

    results = model.train(
        data='data/fire_data/data.yaml',
        epochs=100,          # 数据量大可 100~300,小数据 50 即可
        imgsz=640,           # 训练输入尺寸
        batch=16,            # 显存不够就调小(8/4)
        device=0,            # 0=GPU, cpu 用 'cpu'
        patience=20,         # 20 个 epoch 无提升则早停
        lr0=0.01,
        name='fire_v1',      # 实验名,产出在 runs/detect/fire_v1/
        pretrained=True,
        workers=4,
        seed=42,
    )
    print('训练完成,最佳权重: runs/detect/fire_v1/weights/best.pt')

if __name__ == '__main__':
    main()

4.3 训练过程说明

  • 预训练权重选择yolo11n.pt(nano,最快、显存占用最小)适合入门;追求精度选 yolo11s.pt / yolo11m.pt

  • 关键超参

    参数作用建议
    epochs训练轮数小数据集 50~100,大数据 150~300
    batch每批图片数显存 6G 用 8~16,不够减半
    imgsz输入分辨率默认 640,小目标多可 768/1024
    patience早停轮数20~50
    lr0初始学习率0.01(默认)
  • 训练输出runs/detect/fire_v1/):

    • weights/best.pt 验证集最优权重(部署用这个
    • weights/last.pt 最后一轮权重
    • results.png 损失与 mAP 曲线
    • confusion_matrix.png 混淆矩阵
    • PR_curve.png / F1_curve.png 精度曲线

日志中关注 box_losscls_loss 是否持续下降,以及每轮末尾的 mAP50mAP50-95 是否上升。


五、模型评估与推理

5.1 评估

from ultralytics import YOLO

model = YOLO('runs/detect/fire_v1/weights/best.pt')
metrics = model.val(data='data/fire_data/data.yaml')
print('mAP50:', metrics.box.map50)
print('mAP50-95:', metrics.box.map)

5.2 通用推理脚本 detect.py

支持三种输入:图片、视频、摄像头(RTSP/USB)。

# detect.py
import sys
import cv2
from ultralytics import YOLO

MODEL_PATH = 'runs/detect/fire_v1/weights/best.pt'
CONF = 0.4            # 置信度阈值

model = YOLO(MODEL_PATH)
CLASS_NAMES = model.names


def detect_image(src, save='output.jpg'):
    results = model.predict(source=src, conf=CONF, save=True)
    # 打印检测结果
    for r in results:
        for box in r.boxes:
            cls = int(box.cls[0])
            conf = float(box.conf[0])
            xyxy = box.xyxy[0].tolist()
            print(f'类别: {CLASS_NAMES[cls]}  置信度: {conf:.2f}  框: {xyxy}')


def detect_video(src):
    cap = cv2.VideoCapture(src)
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        results = model.predict(source=frame, conf=CONF, verbose=False)
        annotated = results[0].plot()          # 画框后的画面
        cv2.imshow('Fire Detection', annotated)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    cap.release()
    cv2.destroyAllWindows()


if __name__ == '__main__':
    mode = sys.argv[1] if len(sys.argv) > 1 else 'image'
    src = sys.argv[2] if len(sys.argv) > 2 else 'test.jpg'

    if mode == 'image':
        detect_image(src)
    elif mode == 'video':
        detect_video(src)
    elif mode == 'camera':
        detect_video(0)                        # 0 = 本机摄像头
    else:
        print('用法: python detect.py [image|video|camera] <路径>')
python detect.py image test.jpg
python detect.py video test.mp4
python detect.py camera

六、Flask + MySQL 后端开发

6.1 MySQL 准备

先启动 MySQL,创建数据库:

CREATE DATABASE IF NOT EXISTS fire_monitor DEFAULT CHARACTER SET utf8mb4;

6.2 后端配置 backend/config.py

# config.py
import os

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# MySQL 连接配置
MYSQL_HOST = '127.0.0.1'
MYSQL_PORT = 3306
MYSQL_USER = 'root'
MYSQL_PASSWORD = '123456'
MYSQL_DB = 'fire_monitor'

# 模型配置
MODEL_PATH = r'D:\fire_detection_system\yolov11\runs\detect\fire_v1\weights\best.pt'
CONF_THRESHOLD = 0.4
IMG_SIZE = 640

# 上传目录
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'bmp', 'mp4', 'avi', 'mov'}
MAX_CONTENT_LENGTH = 50 * 1024 * 1024   # 50MB

os.makedirs(UPLOAD_FOLDER, exist_ok=True)

6.3 数据库模块 backend/db.py

# db.py
import pymysql
from config import (MYSQL_HOST, MYSQL_PORT, MYSQL_USER,
                    MYSQL_PASSWORD, MYSQL_DB)

# 建表 SQL
CREATE_TABLES = """
CREATE TABLE IF NOT EXISTS alert_record (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    device_name VARCHAR(100) DEFAULT '',
    source_type VARCHAR(20)  DEFAULT 'image',   -- image/video/camera
    class_name  VARCHAR(50)  NOT NULL,          -- fire/smoke
    confidence  FLOAT        NOT NULL,
    bbox        VARCHAR(200) DEFAULT '',        -- "x,y,w,h"
    image_path  VARCHAR(300) DEFAULT '',
    detect_time DATETIME     NOT NULL,
    status      TINYINT      DEFAULT 0          -- 0未处理 1已处理
);

CREATE TABLE IF NOT EXISTS device (
    id         INT AUTO_INCREMENT PRIMARY KEY,
    name       VARCHAR(100) NOT NULL,
    stream_url VARCHAR(500) DEFAULT '',
    enabled    TINYINT      DEFAULT 1,
    created_at DATETIME     DEFAULT CURRENT_TIMESTAMP
);
"""


def get_conn():
    """获取数据库连接"""
    return pymysql.connect(
        host=MYSQL_HOST, port=MYSQL_PORT,
        user=MYSQL_USER, password=MYSQL_PASSWORD,
        database=MYSQL_DB, charset='utf8mb4',
        cursorclass=pymysql.cursors.DictCursor,
        autocommit=True,
    )


def init_db():
    """初始化表结构"""
    conn = get_conn()
    with conn.cursor() as cur:
        for stmt in CREATE_TABLES.split(';'):
            if stmt.strip():
                cur.execute(stmt)
    conn.close()
    print('[db] 表结构初始化完成')

6.4 模型推理封装 backend/model_service.py

# model_service.py
import os
import cv2
from ultralytics import YOLO
from config import MODEL_PATH, CONF_THRESHOLD, IMG_SIZE

_model = None


def get_model():
    """懒加载单例模型,避免每次请求重复加载"""
    global _model
    if _model is None:
        _model = YOLO(MODEL_PATH)
    return _model


def detect_image_bytes(img_bytes):
    """
    对图片字节流做检测,返回检测结果列表
    结果元素: {class_name, confidence, bbox:[x,y,w,h]}
    """
    import numpy as np
    nparr = np.frombuffer(img_bytes, np.uint8)
    img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

    model = get_model()
    results = model.predict(source=img, conf=CONF_THRESHOLD, imgsz=IMG_SIZE, verbose=False)
    dets = []
    for r in results:
        for box in r.boxes:
            cls = int(box.cls[0])
            conf = float(box.conf[0])
            x1, y1, x2, y2 = box.xyxy[0].tolist()
            dets.append({
                'class_name': model.names[cls],
                'confidence': round(conf, 4),
                'bbox': [round(x1, 1), round(y1, 1),
                         round(x2 - x1, 1), round(y2 - y1, 1)],  # x,y,w,h
            })
    return dets


def detect_video_file(video_path, step=5):
    """
    对视频抽帧检测(每 step 帧检测一帧),返回是否发生火灾
    """
    cap = cv2.VideoCapture(video_path)
    model = get_model()
    found = []
    frame_idx = 0
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        if frame_idx % step == 0:
            results = model.predict(source=frame, conf=CONF_THRESHOLD,
                                    imgsz=IMG_SIZE, verbose=False)
            for r in results:
                for box in r.boxes:
                    cls = int(box.cls[0])
                    found.append({'class_name': model.names[cls],
                                  'confidence': round(float(box.conf[0]), 4)})
        frame_idx += 1
    cap.release()
    return found

6.5 Flask 主应用 backend/app.py

# app.py
import os
import uuid
from datetime import datetime

from flask import Flask, request, jsonify, render_template
from werkzeug.utils import secure_filename

from config import UPLOAD_FOLDER, ALLOWED_EXTENSIONS
from db import get_conn, init_db
from model_service import detect_image_bytes, detect_video_file

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024


# ---------- 工具 ----------
def allowed_file(name):
    return '.' in name and name.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


def save_upload(file):
    """保存上传文件,返回相对路径"""
    ext = file.filename.rsplit('.', 1)[1].lower()
    fname = f'{uuid.uuid4().hex}.{ext}'
    fpath = os.path.join(app.config['UPLOAD_FOLDER'], fname)
    file.save(fpath)
    return fname


def insert_alert(device_name, source_type, dets, image_path=''):
    """将检测结果写入告警表,返回告警 id 列表"""
    conn = get_conn()
    ids = []
    with conn.cursor() as cur:
        for d in dets:
            x, y, w, h = d['bbox']
            cur.execute(
                """INSERT INTO alert_record
                   (device_name, source_type, class_name, confidence,
                    bbox, image_path, detect_time, status)
                   VALUES (%s, %s, %s, %s, %s, %s, %s, 0)""",
                (device_name, source_type, d['class_name'],
                 d['confidence'], f'{x},{y},{w},{h}', image_path,
                 datetime.now().strftime('%Y-%m-%d %H:%M:%S')),
            )
            ids.append(cur.lastrowid)
    conn.close()
    return ids


# ---------- 页面 ----------
@app.route('/')
def index():
    return render_template('index.html')


@app.route('/detect')
def detect_page():
    return render_template('detect.html')


# ---------- API:图片检测 ----------
@app.route('/api/detect/image', methods=['POST'])
def api_detect_image():
    file = request.files.get('image')
    device_name = request.form.get('device_name', '默认摄像头')

    if not file or file.filename == '':
        return jsonify({'success': False, 'msg': '请上传图片'}), 400
    if not allowed_file(file.filename):
        return jsonify({'success': False, 'msg': '不支持的图片格式'}), 400

    img_bytes = file.read()
    dets = detect_image_bytes(img_bytes)

    # 有检出则保存图片并写库
    image_path = ''
    if dets:
        fname = save_upload(file)
        image_path = fname
        ids = insert_alert(device_name, 'image', dets, image_path)
        return jsonify({'success': True, 'fire_detected': True,
                        'results': dets, 'alert_ids': ids,
                        'image': image_path})

    return jsonify({'success': True, 'fire_detected': False,
                    'results': [], 'msg': '未检出火情'})


# ---------- API:视频检测 ----------
@app.route('/api/detect/video', methods=['POST'])
def api_detect_video():
    file = request.files.get('video')
    device_name = request.form.get('device_name', '默认摄像头')

    if not file or not allowed_file(file.filename):
        return jsonify({'success': False, 'msg': '请上传视频文件'}), 400

    fname = save_upload(file)
    fpath = os.path.join(app.config['UPLOAD_FOLDER'], fname)
    dets = detect_video_file(fpath)

    if dets:
        ids = insert_alert(device_name, 'video', dets, fname)
        return jsonify({'success': True, 'fire_detected': True,
                        'results': dets, 'alert_ids': ids,
                        'video': fname})
    return jsonify({'success': True, 'fire_detected': False,
                    'results': [], 'msg': '未检出火情'})


# ---------- API:告警记录 ----------
@app.route('/api/alerts')
def api_alerts():
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 20, type=int)
    class_name = request.args.get('class_name', '').strip()

    conn = get_conn()
    where = ''
    params = []
    if class_name:
        where = 'WHERE class_name = %s'
        params = [class_name]

    with conn.cursor() as cur:
        cur.execute(f'SELECT COUNT(*) AS c FROM alert_record {where}', params)
        total = cur.fetchone()['c']

        cur.execute(
            f"""SELECT * FROM alert_record {where}
                ORDER BY detect_time DESC LIMIT %s OFFSET %s""",
            params + [per_page, (page - 1) * per_page])
        rows = cur.fetchall()
    conn.close()

    return jsonify({'success': True, 'total': total,
                    'page': page, 'per_page': per_page, 'items': rows})


@app.route('/api/alerts/<int:alert_id>/handle', methods=['POST'])
def api_handle_alert(alert_id):
    conn = get_conn()
    with conn.cursor() as cur:
        cur.execute('UPDATE alert_record SET status = 1 WHERE id = %s', (alert_id,))
    conn.close()
    return jsonify({'success': True, 'msg': '已标记为已处理'})


@app.route('/api/alerts/clear', methods=['POST'])
def api_clear_alerts():
    conn = get_conn()
    with conn.cursor() as cur:
        cur.execute('DELETE FROM alert_record')
    conn.close()
    return jsonify({'success': True, 'msg': '已清空告警记录'})


if __name__ == '__main__':
    init_db()
    print(f'[info] 模型: {MODEL_PATH}')
    app.run(host='0.0.0.0', port=18032, debug=True)

七、前端监控页面

7.1 监控首页 backend/templates/index.html

在这里插入图片描述

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>火灾识别监测系统</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-danger">
    <div class="container-fluid">
        <span class="navbar-brand mb-0 h1">火灾识别监测系统</span>
        <span class="navbar-text text-white">
            <a href="/detect" class="text-white me-3">图片检测</a>
            <a href="#" class="text-white" onclick="refresh()">刷新告警</a>
        </span>
    </div>
</nav>

<div class="container mt-4">
    <div class="row mb-3">
        <div class="col-3">
            <select class="form-select" id="classFilter">
                <option value="">全部类型</option>
                <option value="fire">fire</option>
                <option value="smoke">smoke</option>
            </select>
        </div>
        <div class="col-2">
            <button class="btn btn-outline-danger w-100" onclick="clearAlerts()">清空告警</button>
        </div>
    </div>

    <table class="table table-hover table-striped">
        <thead class="table-danger">
        <tr>
            <th>ID</th><th>设备</th><th>来源</th><th>类型</th>
            <th>置信度</th><th>检测时间</th><th>状态</th><th>操作</th>
        </tr>
        </thead>
        <tbody id="alertBody"></tbody>
    </table>
    <nav><ul class="pagination pagination-sm" id="pager"></ul></nav>
</div>

<script>
let page = 1;
const perPage = 20;

function fmtTime(t) {
    return t ? t.replace('T', ' ') : '';
}

function loadAlerts() {
    const cls = document.getElementById('classFilter').value;
    fetch(`/api/alerts?page=${page}&per_page=${perPage}&class_name=${cls}`)
        .then(r => r.json())
        .then(data => {
            const body = document.getElementById('alertBody');
            body.innerHTML = data.items.map(a => `
                <tr>
                    <td>${a.id}</td>
                    <td>${a.device_name}</td>
                    <td>${a.source_type}</td>
                    <td>
                        <span class="badge ${a.class_name === 'fire' ? 'bg-danger' : 'bg-warning text-dark'}">
                            ${a.class_name}
                        </span>
                    </td>
                    <td>${(a.confidence * 100).toFixed(1)}%</td>
                    <td>${fmtTime(a.detect_time)}</td>
                    <td>${a.status === 1 ? '<span class="badge bg-success">已处理</span>' : '<span class="badge bg-secondary">未处理</span>'}</td>
                    <td>
                        ${a.status === 0 ? `<button class="btn btn-sm btn-outline-success" onclick="handleAlert(${a.id})">处理</button>` : ''}
                    </td>
                </tr>`).join('');
            renderPager(data.total);
        });
}

function renderPager(total) {
    const pages = Math.ceil(total / perPage);
    let html = '';
    for (let i = 1; i <= pages; i++) {
        html += `<li class="page-item ${i === page ? 'active' : ''}">
                   <a class="page-link" href="javascript:page=${i};loadAlerts()">${i}</a></li>`;
    }
    document.getElementById('pager').innerHTML = html;
}

function handleAlert(id) {
    fetch(`/api/alerts/${id}/handle`, {method: 'POST'}).then(() => loadAlerts());
}

function clearAlerts() {
    if (!confirm('确定清空所有告警记录?')) return;
    fetch('/api/alerts/clear', {method: 'POST'}).then(() => loadAlerts());
}

function refresh() { loadAlerts(); }

document.getElementById('classFilter').addEventListener('change', () => {
    page = 1;
    loadAlerts();
});
loadAlerts();
setInterval(loadAlerts, 5000);   // 每 5 秒自动刷新告警列表
</script>
</body>
</html>

7.2 图片检测页 backend/templates/detect.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>图片检测 - 火灾识别</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-4" style="max-width: 760px;">
    <h4 class="mb-3">火灾图片检测</h4>
    <div class="card p-3">
        <form id="detForm">
            <div class="mb-3">
                <label class="form-label">选择图片</label>
                <input type="file" class="form-control" id="imageFile" accept="image/*" required>
            </div>
            <div class="mb-3">
                <label class="form-label">设备名称</label>
                <input type="text" class="form-control" id="deviceName" value="测试摄像头">
            </div>
            <button type="submit" class="btn btn-danger">开始检测</button>
        </form>
    </div>

    <div id="result" class="mt-4"></div>
    <a href="/" class="btn btn-outline-secondary mt-3">返回监控首页</a>
</div>

<script>
document.getElementById('detForm').addEventListener('submit', function (e) {
    e.preventDefault();
    const file = document.getElementById('imageFile').files[0];
    if (!file) return;

    const formData = new FormData();
    formData.append('image', file);
    formData.append('device_name', document.getElementById('deviceName').value);

    const box = document.getElementById('result');
    box.innerHTML = '<div class="spinner-border text-danger"></div> 检测中...';

    fetch('/api/detect/image', {method: 'POST', body: formData})
        .then(r => r.json())
        .then(data => {
            if (data.fire_detected) {
                let html = '<div class="alert alert-danger">检出火情!' + data.results.length + ' 个目标</div>';
                data.results.forEach(d => {
                    html += `<span class="badge bg-danger me-1">${d.class_name} ${(d.confidence * 100).toFixed(1)}%</span>`;
                });
                box.innerHTML = html;
            } else {
                box.innerHTML = '<div class="alert alert-success">未检出火情</div>';
            }
        })
        .catch(err => {
            box.innerHTML = '<div class="alert alert-warning">检测失败: ' + err + '</div>';
        });
});
</script>
</body>
</html>

八、启动与测试

8.1 依赖清单 requirements.txt

ultralytics>=8.3.0
torch
torchvision
opencv-python
flask
pymysql
numpy

8.2 启动后端

cd backend
python app.py

启动日志应看到:

[db] 表结构初始化完成
[info] 模型: D:\...\best.pt
 * Running on http://127.0.0.1:18032

8.3 功能测试

测试项方法预期结果
图片检测打开 http://127.0.0.1:18032/detect 上传带火焰图片返回检出结果,监控页出现告警记录
视频检测curl -F "video=@test.mp4" http://127.0.0.1:18032/api/detect/video抽帧检测并写告警
告警查询打开 http://127.0.0.1:18032/列表展示,5 秒自动刷新
标记处理点击「处理」状态变为已处理
清空告警点击「清空告警」记录清空
未检出上传无火图片返回 fire_detected: false,不写库

curl 测试命令

# 图片检测
curl -F "image=@fire.jpg" -F "device_name=车间1号" http://127.0.0.1:18032/api/detect/image

# 视频检测
curl -F "video=@fire.mp4" http://127.0.0.1:18032/api/detect/video

# 查询告警
curl "http://127.0.0.1:18032/api/alerts?page=1&class_name=fire"

8.4 MySQL 验证

USE fire_monitor;
SELECT id, device_name, class_name, confidence, detect_time, status
FROM alert_record ORDER BY detect_time DESC;

九、进阶优化

  1. 摄像头实时检测:新增 /api/stream 接口,用 cv2.VideoCapture(rtsp_url) 拉流 + 定时抓帧送入 detect_image_bytes,检出即告警。
  2. 告警去重/时序过滤:连续 N 帧检出才触发告警,减少误报(火苗闪烁、水蒸气干扰)。
  3. 邮件/钉钉/微信通知:检出火情后调用企业微信/钉钉机器人 Webhook 推送告警。
  4. 模型导出部署model.export(format='onnx') 导出 ONNX,配合 ONNX Runtime 或 TensorRT 加速;轻量设备可导出 NCNN。
  5. 提升小目标检测imgsz=768、加 TTA(augment=True)、或换 yolo11m/x 大模型。
  6. 告警图片留存策略:定期把 uploads/ 中 30 天前的告警图片归档/清理(可参照本仓库 clean_folders.py 思路)。

系统技术架构

在这里插入图片描述

首页:
在这里插入图片描述
分屏展示:
在这里插入图片描述
告警记录
在这里插入图片描述
千问AI大模型图片语义分析:
在这里插入图片描述
设备管理:
在这里插入图片描述
用户管理
在这里插入图片描述

系统配置
在这里插入图片描述

操作日志
在这里插入图片描述

告警机制和流程:告警触发==》人工确认==》事件处理,使用千问大模型二次确认告警和语义分析
在这里插入图片描述

在这里插入图片描述
部署要求:建议使用GPU 显卡,监测识别效果更好; 如果cpu的话,建议 4核以上,16G内存效果更好;
在这里插入图片描述

附:常见问题

问题解决方案
CUDA out of memory调小 batch(16→8→4)、imgsz(640→480)
训练不上 GPU检查 torch.cuda.is_available(),重装 CUDA 版 PyTorch
检测效果差加数据、做增强、换更大模型、调高 imgsz
中文类别名乱码data.yamlnames 使用英文(fire/smoke)
MySQL 连接失败确认 MySQL 已启动、密码正确、utf8mb4 编码
视频检测慢增大抽帧 step 参数(5→10)

本教程基于 Ultralytics YOLO11 与 Flask + MySQL 构建,代码可直接套用到森林防火、厂房监控、智慧园区等场景。

您的关注就是我们前进的动力!一起学习进步!

VisionForgeSDK: VisionForge SDK 为用户提供新一代人工智能解决方案,释放数据的真正潜力;
1、火灾监测识别系统:可用于森林、厂区等防火区域;
2、垃圾监测识别系统:支持常见垃圾监测;
3、人脸轨迹提取系统:根据视频画面提取人员的时间活动轨迹,追踪目标;
4、智慧工地监测系统:实时监控施工场景,保障工人安全,提高管理效率;
5、头盔监测识别系统:头盔佩戴等
项目地址: https://gitee.com/51diysoft/VisionForgeSDK

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

icoder888

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值