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实现火灾检测调用

目录
一、系统架构
本系统分两大部分:
┌────────────────────────────────────────────────┐
│ 火灾识别监测系统 │
├────────────────────────────────────────────────┤
│ ① 模型侧(离线) │
│ └─ 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
标注要点:
- 类别只设两类:
fire、smoke(也可合并为一类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_loss、cls_loss是否持续下降,以及每轮末尾的mAP50、mAP50-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;
九、进阶优化
- 摄像头实时检测:新增
/api/stream接口,用cv2.VideoCapture(rtsp_url)拉流 + 定时抓帧送入detect_image_bytes,检出即告警。 - 告警去重/时序过滤:连续 N 帧检出才触发告警,减少误报(火苗闪烁、水蒸气干扰)。
- 邮件/钉钉/微信通知:检出火情后调用企业微信/钉钉机器人 Webhook 推送告警。
- 模型导出部署:
model.export(format='onnx')导出 ONNX,配合 ONNX Runtime 或 TensorRT 加速;轻量设备可导出 NCNN。 - 提升小目标检测:
imgsz=768、加 TTA(augment=True)、或换yolo11m/x大模型。 - 告警图片留存策略:定期把
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.yaml 的 names 使用英文(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
347

被折叠的 条评论
为什么被折叠?



