1. 项目背景与核心价值
火灾检测一直是公共安全领域的重要课题。传统基于烟雾传感器或温度传感器的检测方式存在响应延迟、误报率高的问题。而基于计算机视觉的火灾检测系统能够实现早期预警,大幅提升响应速度。这个项目将YOLOv8/v10/v11/v12系列目标检测算法与SpringBoot框架结合,构建了一个前后端分离的智能火灾检测Web系统。
我选择YOLOv8系列算法是因为它们在目标检测任务中表现出色,特别是对小目标和动态目标的检测能力。相比前代YOLO版本,v8及后续版本在精度和速度之间取得了更好的平衡。而SpringBoot作为Java生态中最流行的Web框架,其自动配置特性和丰富的starter依赖可以大幅简化后端开发工作。
前后端分离架构是这个项目的另一个亮点。前端采用Vue.js框架,后端使用SpringBoot提供RESTful API,两者通过HTTP协议通信。这种架构模式相比传统的单体应用具有更好的可维护性和扩展性。当需要升级算法或调整界面时,可以独立进行而不会互相影响。
提示:在实际部署时,建议将YOLO模型部署在GPU服务器上以获得最佳推理性能,Web服务则可以部署在普通云服务器上。
2. 系统架构设计
2.1 整体架构
系统采用典型的三层架构:
- 前端展示层:Vue.js构建的Web界面
- 后端服务层:SpringBoot提供的REST API
- 算法推理层:基于YOLOv8的火灾检测模型
这种分层设计使得各组件可以独立开发和部署。前端工程师可以专注于UI交互,后端工程师处理业务逻辑,算法工程师优化模型性能。
2.2 技术选型分析
前端技术栈:
- Vue 3.x:响应式前端框架
- Element Plus:UI组件库
- Axios:HTTP客户端
- ECharts:数据可视化
后端技术栈:
- SpringBoot 2.7.x:Web框架
- Spring Security:认证授权
- MyBatis-Plus:ORM框架
- Redis:缓存
- MySQL:关系型数据库
算法技术栈:
- YOLOv8/v10/v11/v12:目标检测模型
- OpenCV:图像处理
- ONNX Runtime:模型推理加速
选择这些技术的主要考虑因素是社区活跃度、文档完善度和团队熟悉程度。例如,Vue 3.x的Composition API相比Options API更适合大型项目;MyBatis-Plus在MyBatis基础上提供了更多开箱即用的功能,可以显著减少样板代码。
3. YOLO模型训练与优化
3.1 数据集准备
火灾检测需要专门的数据集。我使用了以下公开数据集进行训练:
- Fire Detection Dataset (FDD):包含多种场景下的火灾图像
- BowFire:专注于森林火灾
- 自采集数据:通过监控摄像头获取的真实场景
数据增强策略:
- 随机旋转(-15°到15°)
- 色彩抖动
- 高斯噪声
- 随机裁剪
注意:火灾数据集中正负样本比例往往严重失衡,需要采用过采样或类别加权损失函数来解决。
3.2 模型训练
YOLOv8提供了命令行工具和Python API两种训练方式。这里使用Python API:
python复制from ultralytics import YOLO
# 加载预训练模型
model = YOLO('yolov8n.pt') # 基础模型
# 训练配置
results = model.train(
data='fire.yaml',
epochs=100,
imgsz=640,
batch=16,
device='0', # 使用GPU
optimizer='AdamW',
lr0=0.001,
weight_decay=0.0005
)
关键参数说明:
imgsz:输入图像尺寸,越大精度可能越高但速度越慢batch:批大小,取决于GPU显存device:指定GPU设备optimizer:优化器选择,AdamW通常表现良好
3.3 模型优化技巧
- SPD-Conv替换:用SPD-Conv替换传统卷积可以显著提升小目标检测性能。具体实现需要修改模型定义文件:
yaml复制# yolov8.yaml
backbone:
# [from, repeats, module, args]
[-1, 1, SPDConv, [32, 3, 2]] # 0-P1/2
[-1, 1, SPDConv, [64, 3, 2]] # 1-P2/4
# 其余层...
- 注意力机制:在骨干网络中添加CBAM或SE注意力模块
- 损失函数改进:使用Focal Loss解决类别不平衡问题
- 后处理优化:调整NMS阈值和置信度阈值
4. SpringBoot后端实现
4.1 项目结构
code复制src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── firesafe/
│ │ ├── config/ # 配置类
│ │ ├── controller/ # 控制器
│ │ ├── dto/ # 数据传输对象
│ │ ├── entity/ # 实体类
│ │ ├── repository/ # 数据访问
│ │ ├── service/ # 业务逻辑
│ │ └── FireSafeApplication.java
│ └── resources/
│ ├── application.yml # 应用配置
│ └── static/ # 静态资源
└── test/ # 测试代码
4.2 核心API设计
1. 文件上传接口
java复制@RestController
@RequestMapping("/api/v1/detection")
public class DetectionController {
@PostMapping("/upload")
public ResponseEntity<DetectionResult> uploadImage(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "threshold", defaultValue = "0.5") float threshold) {
if (file.isEmpty()) {
throw new BadRequestException("上传文件不能为空");
}
// 调用算法服务
DetectionResult result = detectionService.detect(file, threshold);
return ResponseEntity.ok(result);
}
}
2. 历史记录查询接口
java复制@GetMapping("/history")
public ResponseEntity<Page<DetectionRecord>> getHistory(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String startTime,
@RequestParam(required = false) String endTime) {
Pageable pageable = PageRequest.of(page, size, Sort.by("detectionTime").descending());
Specification<DetectionRecord> spec = buildSpecification(startTime, endTime);
return ResponseEntity.ok(recordRepository.findAll(spec, pageable));
}
4.3 算法服务集成
将Python训练的YOLO模型集成到Java服务中,有两种主要方式:
方案1:ONNX Runtime部署
- 将PyTorch模型导出为ONNX格式:
python复制model.export(format='onnx')
- Java端使用ONNX Runtime加载模型:
java复制OrtEnvironment env = OrtEnvironment.getEnvironment();
OrtSession.SessionOptions options = new OrtSession.SessionOptions();
OrtSession session = env.createSession("fire_detection.onnx", options);
// 准备输入
OnnxTensor inputTensor = OnnxTensor.createTensor(env, preprocessedImage);
Map<String, OnnxTensor> inputs = Collections.singletonMap("images", inputTensor);
// 推理
OrtSession.Result results = session.run(inputs);
方案2:gRPC服务
- Python端实现gRPC服务:
python复制class DetectionServicer(detection_pb2_grpc.DetectionServiceServicer):
def Detect(self, request, context):
image = Image.open(io.BytesIO(request.image))
results = model(image)
return detection_pb2.DetectionResponse(
boxes=results[0].boxes.xyxy.tolist(),
scores=results[0].boxes.conf.tolist(),
classes=results[0].boxes.cls.tolist()
)
- Java客户端调用:
java复制ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
.usePlaintext()
.build();
DetectionServiceGrpc.DetectionServiceBlockingStub stub = DetectionServiceGrpc.newBlockingStub(channel);
DetectionRequest request = DetectionRequest.newBuilder()
.setImage(ByteString.copyFrom(imageBytes))
.build();
DetectionResponse response = stub.detect(request);
5. 前端实现与系统集成
5.1 Vue 3组件设计
实时检测组件
vue复制<template>
<div class="detection-container">
<video ref="video" autoplay muted></video>
<canvas ref="canvas" class="overlay"></canvas>
<button @click="startDetection">开始检测</button>
<div v-if="results.length" class="results">
<div v-for="(item, index) in results" :key="index" class="result-item">
<span>火灾 {{ item.confidence.toFixed(2) }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
const video = ref(null)
const canvas = ref(null)
const results = ref([])
const startDetection = async () => {
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
video.value.srcObject = stream
setInterval(async () => {
const blob = await captureFrame()
const formData = new FormData()
formData.append('file', blob)
const response = await axios.post('/api/v1/detection/upload', formData)
results.value = response.data
drawBoxes(response.data)
}, 1000) // 每秒检测一次
}
const captureFrame = () => {
return new Promise((resolve) => {
canvas.value.width = video.value.videoWidth
canvas.value.height = video.value.videoHeight
const ctx = canvas.value.getContext('2d')
ctx.drawImage(video.value, 0, 0)
canvas.value.toBlob(resolve, 'image/jpeg')
})
}
const drawBoxes = (detections) => {
const ctx = canvas.value.getContext('2d')
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height)
detections.forEach(det => {
const [x1, y1, x2, y2] = det.bbox
ctx.strokeStyle = '#FF0000'
ctx.lineWidth = 2
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1)
ctx.fillStyle = '#FF0000'
ctx.fillText(`Fire ${det.confidence.toFixed(2)}`, x1, y1 - 5)
})
}
</script>
5.2 前后端联调要点
- 跨域问题解决
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*");
}
}
- 接口文档生成
使用Swagger或Knife4j生成API文档:
java复制@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.firesafe.controller"))
.paths(PathSelectors.any())
.build();
}
}
- 文件上传大小限制
在application.yml中配置:
yaml复制spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
6. 系统部署方案
6.1 开发环境部署
- 前端开发环境
bash复制# 安装依赖
npm install
# 启动开发服务器
npm run dev
- 后端开发环境
bash复制# 使用Maven运行
mvn spring-boot:run
6.2 生产环境部署
方案1:Docker容器化
- 前端Dockerfile
dockerfile复制FROM node:16 as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
- 后端Dockerfile
dockerfile复制FROM openjdk:17-jdk-slim
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
EXPOSE 8080
- docker-compose.yml
yaml复制version: '3'
services:
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/firesafe
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=password
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=firesafe
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
方案2:宝塔面板部署
- 安装Nginx和MySQL
- 上传前端dist目录到网站根目录
- 配置Nginx反向代理到后端服务
- 使用PM2管理Node.js进程(如果需要)
6.3 性能优化
- 模型量化:将FP32模型量化为INT8,减少模型大小和推理时间
python复制model.export(format='onnx', int8=True)
- TensorRT加速:使用TensorRT优化ONNX模型
python复制trt_model = YOLO('fire_detection.onnx').export(format='engine')
- 缓存策略:对频繁访问的检测结果进行缓存
java复制@Cacheable(value = "detections", key = "#file.hashCode()")
public DetectionResult detect(MultipartFile file) {
// 检测逻辑
}
7. 实际应用中的经验分享
7.1 常见问题与解决方案
问题1:YOLO模型在特定场景下误报率高
解决方案:
- 收集更多该场景下的负样本重新训练
- 调整NMS和置信度阈值
- 添加后处理规则(如火焰必须出现在特定区域)
问题2:前后端分离项目接口访问失败
排查步骤:
- 检查浏览器开发者工具中的网络请求
- 确认后端服务是否正常运行
- 检查Nginx配置是否正确转发请求
- 验证CORS配置
问题3:模型推理速度慢
优化方向:
- 减小输入图像尺寸
- 使用更小的模型变体(如YOLOv8s)
- 启用GPU加速
- 使用TensorRT优化
7.2 性能调优记录
在RK3588开发板上部署YOLOv8n模型时,原始ONNX模型推理速度约为200ms/帧。经过以下优化后提升到50ms/帧:
- 模型量化(FP32 → INT8):速度提升2倍
- 使用RKNN-Toolkit2转换为RKNN格式:速度提升1.5倍
- 输入尺寸从640x640减小到320x320:速度提升1.8倍
关键代码:
python复制# 转换为RKNN格式
rknn = RKNN()
rknn.config(target_platform='rk3588')
rknn.load_onnx(model='fire_detection.onnx')
rknn.build(do_quantization=True, dataset='./quant.txt')
rknn.export_rknn('fire_detection.rknn')
7.3 扩展功能建议
- 多摄像头支持:接入RTSP流实现多路监控
- 报警联动:检测到火灾后触发声光报警或通知消防系统
- 历史数据分析:使用ECharts展示火灾发生的时间分布和位置热力图
- 移动端适配:开发微信小程序或APP实现远程监控
在实现多摄像头支持时,可以使用FFmpeg处理RTSP流:
java复制ProcessBuilder pb = new ProcessBuilder(
"ffmpeg",
"-i", "rtsp://camera1/stream",
"-f", "image2",
"-vf", "fps=1",
"-update", "1",
"snapshot.jpg"
);
Process process = pb.start();
这个项目从技术选型到最终部署,每个环节都需要考虑实际应用场景的需求。特别是在模型优化和系统集成方面,需要反复测试和调整才能达到理想的性能。我在实际开发中发现,将置信度阈值设置为0.3-0.5之间,能够在误报率和漏检率之间取得较好的平衡。
