Initial commit
This commit is contained in:
8
yolopart/.idea/.gitignore
generated
vendored
Normal file
8
yolopart/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
6
yolopart/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
yolopart/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
7
yolopart/.idea/misc.xml
generated
Normal file
7
yolopart/.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="pytorh" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="pytorh" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
yolopart/.idea/modules.xml
generated
Normal file
8
yolopart/.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/yolopart.iml" filepath="$PROJECT_DIR$/.idea/yolopart.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
yolopart/.idea/vcs.xml
generated
Normal file
6
yolopart/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
yolopart/.idea/yolopart.iml
generated
Normal file
12
yolopart/.idea/yolopart.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="jdk" jdkName="pytorh" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="PLAIN" />
|
||||||
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
177
yolopart/README.md
Normal file
177
yolopart/README.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# 车牌检测系统
|
||||||
|
|
||||||
|
基于YOLO11s模型的实时车牌检测应用,支持摄像头和视频文件输入,具备GPU加速和车牌识别接口。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- ✅ **实时车牌检测**: 基于YOLO11s ONNX模型
|
||||||
|
- ✅ **GPU加速**: 支持CUDA GPU推理加速
|
||||||
|
- ✅ **多视频源**: 支持摄像头和视频文件切换
|
||||||
|
- ✅ **实时显示**: 显示检测框、置信度和实时FPS
|
||||||
|
- ✅ **图像切割**: 自动切割检测到的车牌区域
|
||||||
|
- ✅ **识别接口**: 预留车牌号识别接口,可接入OCR模型
|
||||||
|
- ✅ **友好界面**: 基于PyQt5的现代化用户界面
|
||||||
|
|
||||||
|
## 系统要求
|
||||||
|
|
||||||
|
- Python 3.7+
|
||||||
|
- Windows/Linux/macOS
|
||||||
|
- 摄像头(可选)
|
||||||
|
- NVIDIA GPU(可选,用于加速)
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装基础依赖
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 如果需要CPU版本的onnxruntime
|
||||||
|
pip uninstall onnxruntime-gpu
|
||||||
|
pip install onnxruntime
|
||||||
|
|
||||||
|
# 可选:安装车牌识别依赖
|
||||||
|
# PaddleOCR
|
||||||
|
pip install paddlepaddle paddleocr
|
||||||
|
|
||||||
|
# 或者 Tesseract
|
||||||
|
pip install pytesseract
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 准备模型文件
|
||||||
|
|
||||||
|
确保项目根目录下有以下文件:
|
||||||
|
- `last.onnx`: YOLO11s车牌检测模型
|
||||||
|
- `video.mp4`: 测试视频文件(可选)
|
||||||
|
|
||||||
|
### 2. 运行应用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 界面操作
|
||||||
|
|
||||||
|
- **开始检测**: 点击"开始检测"按钮启动实时检测
|
||||||
|
- **切换视频源**: 勾选/取消"使用摄像头"切换视频源
|
||||||
|
- **启用检测**: 勾选/取消"启用检测"开关检测功能
|
||||||
|
- **查看结果**: 右侧面板显示检测信息和车牌识别结果
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
yolopart/
|
||||||
|
├── main.py # 主程序入口
|
||||||
|
├── requirements.txt # 依赖包列表
|
||||||
|
├── README.md # 项目说明
|
||||||
|
├── last.onnx # YOLO11s模型文件
|
||||||
|
├── video.mp4 # 测试视频文件
|
||||||
|
├── ui/ # 用户界面模块
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── main_window.py # 主窗口
|
||||||
|
│ └── video_widget.py # 视频显示组件
|
||||||
|
├── models/ # 模型模块
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── yolo_detector.py # YOLO检测器
|
||||||
|
│ └── plate_recognizer.py # 车牌识别接口
|
||||||
|
└── utils/ # 工具模块
|
||||||
|
├── __init__.py
|
||||||
|
└── video_capture.py # 视频捕获管理
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心功能说明
|
||||||
|
|
||||||
|
### YOLO检测器 (`models/yolo_detector.py`)
|
||||||
|
|
||||||
|
- 支持ONNX格式的YOLO11s模型
|
||||||
|
- 自动GPU/CPU推理选择
|
||||||
|
- 640x640输入尺寸
|
||||||
|
- NMS后处理
|
||||||
|
- 检测框绘制和车牌切割
|
||||||
|
|
||||||
|
### 视频捕获 (`utils/video_capture.py`)
|
||||||
|
|
||||||
|
- 摄像头自动检测和配置
|
||||||
|
- 视频文件循环播放
|
||||||
|
- 实时FPS计算和显示
|
||||||
|
- 线程安全的帧获取
|
||||||
|
|
||||||
|
### 车牌识别接口 (`models/plate_recognizer.py`)
|
||||||
|
|
||||||
|
提供了多种识别器实现:
|
||||||
|
- `MockPlateRecognizer`: 模拟识别器(用于测试)
|
||||||
|
- `PaddleOCRRecognizer`: PaddleOCR识别器
|
||||||
|
- `TesseractRecognizer`: Tesseract识别器
|
||||||
|
|
||||||
|
可通过`PlateRecognizerManager`轻松切换不同的识别引擎。
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### 检测参数调整
|
||||||
|
|
||||||
|
在`models/yolo_detector.py`中可以调整:
|
||||||
|
- `conf_threshold`: 置信度阈值(默认0.5)
|
||||||
|
- `nms_threshold`: NMS阈值(默认0.4)
|
||||||
|
- `input_size`: 输入尺寸(默认640x640)
|
||||||
|
|
||||||
|
### 视频参数调整
|
||||||
|
|
||||||
|
在`utils/video_capture.py`中可以调整:
|
||||||
|
- 摄像头分辨率和帧率
|
||||||
|
- FPS计算窗口大小
|
||||||
|
- 视频文件路径
|
||||||
|
|
||||||
|
## 扩展开发
|
||||||
|
|
||||||
|
### 添加新的车牌识别器
|
||||||
|
|
||||||
|
1. 继承`PlateRecognizerInterface`基类
|
||||||
|
2. 实现`recognize`和`batch_recognize`方法
|
||||||
|
3. 在`PlateRecognizerManager`中注册新识别器
|
||||||
|
|
||||||
|
### 添加新功能
|
||||||
|
|
||||||
|
- 检测结果保存
|
||||||
|
- 车牌数据库管理
|
||||||
|
- 网络接口API
|
||||||
|
- 多摄像头支持
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **模型加载失败**
|
||||||
|
- 检查`last.onnx`文件是否存在
|
||||||
|
- 确认onnxruntime版本兼容性
|
||||||
|
|
||||||
|
2. **摄像头无法打开**
|
||||||
|
- 检查摄像头是否被其他程序占用
|
||||||
|
- 尝试不同的摄像头索引
|
||||||
|
|
||||||
|
3. **GPU加速不生效**
|
||||||
|
- 确认安装了`onnxruntime-gpu`
|
||||||
|
- 检查CUDA环境配置
|
||||||
|
|
||||||
|
4. **车牌识别失败**
|
||||||
|
- 检查OCR依赖是否正确安装
|
||||||
|
- 尝试切换不同的识别器
|
||||||
|
|
||||||
|
### 性能优化
|
||||||
|
|
||||||
|
- 使用GPU加速推理
|
||||||
|
- 调整检测阈值减少误检
|
||||||
|
- 优化图像预处理流程
|
||||||
|
- 使用多线程处理
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本项目仅供学习和研究使用。
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.0.0
|
||||||
|
- 初始版本发布
|
||||||
|
- 支持YOLO11s车牌检测
|
||||||
|
- 实现基础UI界面
|
||||||
|
- 预留车牌识别接口
|
||||||
34
yolopart/main.py
Normal file
34
yolopart/main.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
车牌检测系统主程序
|
||||||
|
基于YOLO11s模型的实时车牌检测应用
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from ui.main_window import MainWindow
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
# 创建QApplication实例
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
||||||
|
app.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||||
|
|
||||||
|
# 设置应用信息
|
||||||
|
app.setApplicationName("车牌检测系统")
|
||||||
|
app.setApplicationVersion("1.0.0")
|
||||||
|
app.setOrganizationName("License Plate Detection")
|
||||||
|
|
||||||
|
# 创建主窗口
|
||||||
|
main_window = MainWindow()
|
||||||
|
main_window.show()
|
||||||
|
|
||||||
|
# 运行应用
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
yolopart/models/__init__.py
Normal file
1
yolopart/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 模型模块初始化文件
|
||||||
BIN
yolopart/models/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
yolopart/models/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
yolopart/models/__pycache__/plate_recognizer.cpython-38.pyc
Normal file
BIN
yolopart/models/__pycache__/plate_recognizer.cpython-38.pyc
Normal file
Binary file not shown.
BIN
yolopart/models/__pycache__/yolo_detector.cpython-38.pyc
Normal file
BIN
yolopart/models/__pycache__/yolo_detector.cpython-38.pyc
Normal file
Binary file not shown.
490
yolopart/models/plate_recognizer.py
Normal file
490
yolopart/models/plate_recognizer.py
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
车牌识别接口模块
|
||||||
|
预留接口,可接入各种OCR模型进行车牌号识别
|
||||||
|
"""
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
class PlateRecognizerInterface(ABC):
|
||||||
|
"""车牌识别接口基类"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def recognize(self, plate_image: np.ndarray) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
识别车牌号
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plate_image: 车牌图像 (BGR格式)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
识别结果字典,包含:
|
||||||
|
{
|
||||||
|
'text': str, # 识别的车牌号
|
||||||
|
'confidence': float, # 置信度 (0-1)
|
||||||
|
'success': bool # 是否识别成功
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def batch_recognize(self, plate_images: List[np.ndarray]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
批量识别车牌号
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plate_images: 车牌图像列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
识别结果列表
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MockPlateRecognizer(PlateRecognizerInterface):
|
||||||
|
"""模拟车牌识别器(用于测试)"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.mock_plates = [
|
||||||
|
"京A12345", "沪B67890", "粤C11111", "川D22222",
|
||||||
|
"鲁E33333", "苏F44444", "浙G55555", "闽H66666"
|
||||||
|
]
|
||||||
|
self.call_count = 0
|
||||||
|
|
||||||
|
def recognize(self, plate_image: np.ndarray) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
模拟识别单个车牌
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plate_image: 车牌图像
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
模拟识别结果
|
||||||
|
"""
|
||||||
|
# 模拟处理时间
|
||||||
|
import time
|
||||||
|
time.sleep(0.01) # 10ms模拟处理时间
|
||||||
|
|
||||||
|
# 简单的图像质量检查
|
||||||
|
if plate_image is None or plate_image.size == 0:
|
||||||
|
return {
|
||||||
|
'text': '',
|
||||||
|
'confidence': 0.0,
|
||||||
|
'success': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查图像尺寸
|
||||||
|
height, width = plate_image.shape[:2]
|
||||||
|
if width < 50 or height < 20:
|
||||||
|
return {
|
||||||
|
'text': '',
|
||||||
|
'confidence': 0.3,
|
||||||
|
'success': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# 模拟识别结果
|
||||||
|
plate_text = self.mock_plates[self.call_count % len(self.mock_plates)]
|
||||||
|
confidence = 0.85 + (self.call_count % 10) * 0.01 # 0.85-0.94
|
||||||
|
|
||||||
|
self.call_count += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
'text': plate_text,
|
||||||
|
'confidence': confidence,
|
||||||
|
'success': True
|
||||||
|
}
|
||||||
|
|
||||||
|
def batch_recognize(self, plate_images: List[np.ndarray]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
批量识别车牌
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plate_images: 车牌图像列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
识别结果列表
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for plate_image in plate_images:
|
||||||
|
result = self.recognize(plate_image)
|
||||||
|
results.append(result)
|
||||||
|
return results
|
||||||
|
|
||||||
|
class PaddleOCRRecognizer(PlateRecognizerInterface):
|
||||||
|
"""PaddleOCR车牌识别器(示例实现)"""
|
||||||
|
|
||||||
|
def __init__(self, use_gpu: bool = True):
|
||||||
|
"""
|
||||||
|
初始化PaddleOCR识别器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
use_gpu: 是否使用GPU
|
||||||
|
"""
|
||||||
|
self.use_gpu = use_gpu
|
||||||
|
self.ocr = None
|
||||||
|
self._init_ocr()
|
||||||
|
|
||||||
|
def _init_ocr(self):
|
||||||
|
"""初始化OCR模型"""
|
||||||
|
try:
|
||||||
|
# 这里可以接入PaddleOCR
|
||||||
|
# from paddleocr import PaddleOCR
|
||||||
|
# self.ocr = PaddleOCR(use_angle_cls=True, lang='ch', use_gpu=self.use_gpu)
|
||||||
|
print("PaddleOCR初始化完成(示例代码,需要安装PaddleOCR)")
|
||||||
|
except ImportError:
|
||||||
|
print("PaddleOCR未安装,使用模拟识别器")
|
||||||
|
self.ocr = None
|
||||||
|
|
||||||
|
def recognize(self, plate_image: np.ndarray) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
使用PaddleOCR识别车牌
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plate_image: 车牌图像
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
识别结果
|
||||||
|
"""
|
||||||
|
if self.ocr is None:
|
||||||
|
# 回退到模拟识别
|
||||||
|
mock_recognizer = MockPlateRecognizer()
|
||||||
|
return mock_recognizer.recognize(plate_image)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 使用PaddleOCR进行识别
|
||||||
|
results = self.ocr.ocr(plate_image, cls=True)
|
||||||
|
|
||||||
|
if results and len(results) > 0 and results[0]:
|
||||||
|
# 提取文本和置信度
|
||||||
|
text_results = []
|
||||||
|
for line in results[0]:
|
||||||
|
text = line[1][0]
|
||||||
|
confidence = line[1][1]
|
||||||
|
text_results.append((text, confidence))
|
||||||
|
|
||||||
|
# 选择置信度最高的结果
|
||||||
|
if text_results:
|
||||||
|
best_result = max(text_results, key=lambda x: x[1])
|
||||||
|
return {
|
||||||
|
'text': best_result[0],
|
||||||
|
'confidence': best_result[1],
|
||||||
|
'success': True
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"PaddleOCR识别失败: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'text': '',
|
||||||
|
'confidence': 0.0,
|
||||||
|
'success': False
|
||||||
|
}
|
||||||
|
|
||||||
|
def batch_recognize(self, plate_images: List[np.ndarray]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
批量识别
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plate_images: 车牌图像列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
识别结果列表
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for plate_image in plate_images:
|
||||||
|
result = self.recognize(plate_image)
|
||||||
|
results.append(result)
|
||||||
|
return results
|
||||||
|
|
||||||
|
class TesseractRecognizer(PlateRecognizerInterface):
|
||||||
|
"""Tesseract车牌识别器(示例实现)"""
|
||||||
|
|
||||||
|
def __init__(self, lang: str = 'chi_sim+eng'):
|
||||||
|
"""
|
||||||
|
初始化Tesseract识别器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lang: 识别语言
|
||||||
|
"""
|
||||||
|
self.lang = lang
|
||||||
|
self.tesseract_available = self._check_tesseract()
|
||||||
|
|
||||||
|
def _check_tesseract(self) -> bool:
|
||||||
|
"""检查Tesseract是否可用"""
|
||||||
|
try:
|
||||||
|
import pytesseract
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
print("pytesseract未安装,使用模拟识别器")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def recognize(self, plate_image: np.ndarray) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
使用Tesseract识别车牌
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plate_image: 车牌图像
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
识别结果
|
||||||
|
"""
|
||||||
|
if not self.tesseract_available:
|
||||||
|
# 回退到模拟识别
|
||||||
|
mock_recognizer = MockPlateRecognizer()
|
||||||
|
return mock_recognizer.recognize(plate_image)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pytesseract
|
||||||
|
|
||||||
|
# 图像预处理
|
||||||
|
processed_image = self._preprocess_image(plate_image)
|
||||||
|
|
||||||
|
# 使用Tesseract识别
|
||||||
|
text = pytesseract.image_to_string(
|
||||||
|
processed_image,
|
||||||
|
lang=self.lang,
|
||||||
|
config='--psm 8 --oem 3 -c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ京沪粤川鲁苏浙闽'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 清理识别结果
|
||||||
|
text = text.strip().replace(' ', '').replace('\n', '')
|
||||||
|
|
||||||
|
if text and len(text) >= 5: # 车牌号至少5位
|
||||||
|
return {
|
||||||
|
'text': text,
|
||||||
|
'confidence': 0.8, # Tesseract不直接提供置信度
|
||||||
|
'success': True
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Tesseract识别失败: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'text': '',
|
||||||
|
'confidence': 0.0,
|
||||||
|
'success': False
|
||||||
|
}
|
||||||
|
|
||||||
|
def _preprocess_image(self, image: np.ndarray) -> np.ndarray:
|
||||||
|
"""图像预处理"""
|
||||||
|
# 转换为灰度图
|
||||||
|
if len(image.shape) == 3:
|
||||||
|
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||||
|
else:
|
||||||
|
gray = image
|
||||||
|
|
||||||
|
# 调整尺寸
|
||||||
|
height, width = gray.shape
|
||||||
|
if width < 200:
|
||||||
|
scale = 200 / width
|
||||||
|
new_width = int(width * scale)
|
||||||
|
new_height = int(height * scale)
|
||||||
|
gray = cv2.resize(gray, (new_width, new_height))
|
||||||
|
|
||||||
|
# 二值化
|
||||||
|
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
||||||
|
|
||||||
|
return binary
|
||||||
|
|
||||||
|
def batch_recognize(self, plate_images: List[np.ndarray]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
批量识别
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plate_images: 车牌图像列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
识别结果列表
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for plate_image in plate_images:
|
||||||
|
result = self.recognize(plate_image)
|
||||||
|
results.append(result)
|
||||||
|
return results
|
||||||
|
|
||||||
|
class PlateRecognizerManager:
|
||||||
|
"""车牌识别管理器"""
|
||||||
|
|
||||||
|
def __init__(self, recognizer_type: str = 'mock'):
|
||||||
|
"""
|
||||||
|
初始化识别管理器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
recognizer_type: 识别器类型 ('mock', 'paddleocr', 'tesseract')
|
||||||
|
"""
|
||||||
|
self.recognizer_type = recognizer_type
|
||||||
|
self.recognizer = self._create_recognizer(recognizer_type)
|
||||||
|
|
||||||
|
def _create_recognizer(self, recognizer_type: str) -> PlateRecognizerInterface:
|
||||||
|
"""创建识别器"""
|
||||||
|
if recognizer_type == 'mock':
|
||||||
|
return MockPlateRecognizer()
|
||||||
|
elif recognizer_type == 'paddleocr':
|
||||||
|
return PaddleOCRRecognizer()
|
||||||
|
elif recognizer_type == 'tesseract':
|
||||||
|
return TesseractRecognizer()
|
||||||
|
else:
|
||||||
|
print(f"未知的识别器类型: {recognizer_type},使用模拟识别器")
|
||||||
|
return MockPlateRecognizer()
|
||||||
|
|
||||||
|
def recognize_plates(self, plate_images: List[np.ndarray]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
识别车牌列表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plate_images: 车牌图像列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
识别结果列表
|
||||||
|
"""
|
||||||
|
if not plate_images:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return self.recognizer.batch_recognize(plate_images)
|
||||||
|
|
||||||
|
def switch_recognizer(self, recognizer_type: str):
|
||||||
|
"""
|
||||||
|
切换识别器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
recognizer_type: 新的识别器类型
|
||||||
|
"""
|
||||||
|
self.recognizer_type = recognizer_type
|
||||||
|
self.recognizer = self._create_recognizer(recognizer_type)
|
||||||
|
print(f"已切换到识别器: {recognizer_type}")
|
||||||
|
|
||||||
|
def get_recognizer_info(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
获取识别器信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
识别器信息
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'type': self.recognizer_type,
|
||||||
|
'class': self.recognizer.__class__.__name__
|
||||||
|
}
|
||||||
|
|
||||||
|
def preprocess_blue_plate(self, plate_image: np.ndarray, original_image: np.ndarray, bbox: List[int]) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
蓝色车牌预处理:倾斜矫正
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plate_image: 切割后的车牌图像
|
||||||
|
original_image: 原始图像
|
||||||
|
bbox: 边界框坐标 [x1, y1, x2, y2]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
矫正后的车牌图像
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 从原图中提取车牌区域
|
||||||
|
x1, y1, x2, y2 = bbox
|
||||||
|
roi = original_image[y1:y2, x1:x2]
|
||||||
|
|
||||||
|
# 获取蓝色车牌的二值图像
|
||||||
|
bin_img = self._get_blue_img_bin(roi)
|
||||||
|
|
||||||
|
# 倾斜矫正
|
||||||
|
corrected_img = self._deskew_plate(bin_img, roi)
|
||||||
|
|
||||||
|
return corrected_img
|
||||||
|
except Exception as e:
|
||||||
|
print(f"蓝色车牌预处理失败: {e}")
|
||||||
|
return plate_image
|
||||||
|
|
||||||
|
def _get_blue_img_bin(self, img: np.ndarray) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
获取蓝色车牌的二值图像
|
||||||
|
"""
|
||||||
|
# 掩膜:BGR通道,若像素B分量在 100~255 且 G分量在 0~190 且 R分量在 0~140 置255(白色),否则置0(黑色)
|
||||||
|
mask_bgr = cv2.inRange(img, (100, 0, 0), (255, 190, 140))
|
||||||
|
|
||||||
|
# 转换成 HSV 颜色空间
|
||||||
|
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
||||||
|
h, s, v = cv2.split(img_hsv) # 分离通道 色调(H),饱和度(S),明度(V)
|
||||||
|
mask_s = cv2.inRange(s, 80, 255) # 取饱和度通道进行掩膜得到二值图像
|
||||||
|
|
||||||
|
# 与操作,两个二值图像都为白色才保留,否则置黑
|
||||||
|
rgbs = mask_bgr & mask_s
|
||||||
|
|
||||||
|
# 核的横向分量大,使车牌数字尽量连在一起
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 3))
|
||||||
|
img_rgbs_dilate = cv2.dilate(rgbs, kernel, 3) # 膨胀,减小车牌空洞
|
||||||
|
|
||||||
|
return img_rgbs_dilate
|
||||||
|
|
||||||
|
def _order_points(self, pts: np.ndarray) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
将四点按 左上、右上、右下、左下 排序
|
||||||
|
"""
|
||||||
|
rect = np.zeros((4, 2), dtype="float32")
|
||||||
|
s = pts.sum(axis=1)
|
||||||
|
rect[0] = pts[np.argmin(s)] # 左上
|
||||||
|
rect[2] = pts[np.argmax(s)] # 右下
|
||||||
|
|
||||||
|
diff = np.diff(pts, axis=1)
|
||||||
|
rect[1] = pts[np.argmin(diff)] # 右上
|
||||||
|
rect[3] = pts[np.argmax(diff)] # 左下
|
||||||
|
|
||||||
|
return rect
|
||||||
|
|
||||||
|
def _deskew_plate(self, bin_img: np.ndarray, original_roi: np.ndarray) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
车牌倾斜矫正
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bin_img: 二值图像
|
||||||
|
original_roi: 原始ROI区域
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
矫正后的原始图像(未被掩模,但经过旋转和切割)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 找最大轮廓
|
||||||
|
cnts, _ = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
if not cnts:
|
||||||
|
return original_roi
|
||||||
|
|
||||||
|
c = max(cnts, key=cv2.contourArea)
|
||||||
|
|
||||||
|
# 最小外接矩形
|
||||||
|
rect = cv2.minAreaRect(c)
|
||||||
|
box = cv2.boxPoints(rect)
|
||||||
|
box = np.array(box, dtype="float32")
|
||||||
|
|
||||||
|
# 排序四个点
|
||||||
|
pts_src = self._order_points(box)
|
||||||
|
|
||||||
|
# 计算目标矩形宽高
|
||||||
|
(tl, tr, br, bl) = pts_src
|
||||||
|
widthA = np.linalg.norm(br - bl)
|
||||||
|
widthB = np.linalg.norm(tr - tl)
|
||||||
|
maxWidth = int(max(widthA, widthB))
|
||||||
|
|
||||||
|
heightA = np.linalg.norm(tr - br)
|
||||||
|
heightB = np.linalg.norm(tl - bl)
|
||||||
|
maxHeight = int(max(heightA, heightB))
|
||||||
|
|
||||||
|
# 确保尺寸合理
|
||||||
|
if maxWidth < 10 or maxHeight < 10:
|
||||||
|
return original_roi
|
||||||
|
|
||||||
|
# 目标点集合
|
||||||
|
pts_dst = np.array([
|
||||||
|
[0, 0],
|
||||||
|
[maxWidth - 1, 0],
|
||||||
|
[maxWidth - 1, maxHeight - 1],
|
||||||
|
[0, maxHeight - 1]], dtype="float32")
|
||||||
|
|
||||||
|
# 透视变换
|
||||||
|
M = cv2.getPerspectiveTransform(pts_src, pts_dst)
|
||||||
|
warped = cv2.warpPerspective(original_roi, M, (maxWidth, maxHeight))
|
||||||
|
|
||||||
|
return warped
|
||||||
|
except Exception as e:
|
||||||
|
print(f"车牌矫正失败: {e}")
|
||||||
|
return original_roi
|
||||||
368
yolopart/models/yolo_detector.py
Normal file
368
yolopart/models/yolo_detector.py
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
YOLO车牌检测器
|
||||||
|
基于ONNX Runtime的YOLO11s模型推理
|
||||||
|
"""
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import onnxruntime as ort
|
||||||
|
import time
|
||||||
|
from typing import List, Tuple, Optional
|
||||||
|
|
||||||
|
class YOLODetector:
|
||||||
|
"""YOLO车牌检测器"""
|
||||||
|
|
||||||
|
def __init__(self, model_path: str, conf_threshold: float = 0.25, nms_threshold: float = 0.4):
|
||||||
|
"""
|
||||||
|
初始化YOLO检测器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_path: ONNX模型文件路径
|
||||||
|
conf_threshold: 置信度阈值
|
||||||
|
nms_threshold: NMS阈值
|
||||||
|
"""
|
||||||
|
self.model_path = model_path
|
||||||
|
self.conf_threshold = conf_threshold
|
||||||
|
self.nms_threshold = nms_threshold
|
||||||
|
self.input_size = (640, 640) # YOLO11s输入尺寸
|
||||||
|
self.use_gpu = False
|
||||||
|
|
||||||
|
# 初始化ONNX Runtime会话
|
||||||
|
self._init_session()
|
||||||
|
|
||||||
|
# 获取模型输入输出信息
|
||||||
|
self.input_name = self.session.get_inputs()[0].name
|
||||||
|
self.output_names = [output.name for output in self.session.get_outputs()]
|
||||||
|
|
||||||
|
print(f"YOLO检测器初始化完成")
|
||||||
|
print(f"模型路径: {model_path}")
|
||||||
|
print(f"输入尺寸: {self.input_size}")
|
||||||
|
print(f"GPU加速: {self.use_gpu}")
|
||||||
|
|
||||||
|
def _init_session(self):
|
||||||
|
"""初始化ONNX Runtime会话"""
|
||||||
|
# 获取可用的providers
|
||||||
|
available_providers = ort.get_available_providers()
|
||||||
|
print(f"可用的执行提供者: {available_providers}")
|
||||||
|
|
||||||
|
# 优先使用GPU,如果可用的话
|
||||||
|
providers = []
|
||||||
|
if 'CUDAExecutionProvider' in available_providers:
|
||||||
|
providers.append('CUDAExecutionProvider')
|
||||||
|
self.use_gpu = True
|
||||||
|
print("检测到CUDA支持,将使用GPU加速")
|
||||||
|
elif 'TensorrtExecutionProvider' in available_providers:
|
||||||
|
providers.append('TensorrtExecutionProvider')
|
||||||
|
self.use_gpu = True
|
||||||
|
print("检测到TensorRT支持,将使用GPU加速")
|
||||||
|
else:
|
||||||
|
self.use_gpu = False
|
||||||
|
print("未检测到GPU支持,将使用CPU")
|
||||||
|
|
||||||
|
# 添加CPU作为备选
|
||||||
|
providers.append('CPUExecutionProvider')
|
||||||
|
|
||||||
|
print(f"使用的执行提供者: {providers}")
|
||||||
|
|
||||||
|
# 创建会话
|
||||||
|
session_options = ort.SessionOptions()
|
||||||
|
session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.session = ort.InferenceSession(
|
||||||
|
self.model_path,
|
||||||
|
sess_options=session_options,
|
||||||
|
providers=providers
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查实际使用的provider
|
||||||
|
actual_providers = self.session.get_providers()
|
||||||
|
print(f"实际使用的执行提供者: {actual_providers}")
|
||||||
|
|
||||||
|
if 'CUDAExecutionProvider' in actual_providers or 'TensorrtExecutionProvider' in actual_providers:
|
||||||
|
self.use_gpu = True
|
||||||
|
print("✅ GPU加速已启用")
|
||||||
|
else:
|
||||||
|
self.use_gpu = False
|
||||||
|
print("⚠️ 使用CPU执行")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"模型加载失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def preprocess(self, image: np.ndarray) -> Tuple[np.ndarray, float, float]:
|
||||||
|
"""
|
||||||
|
图像预处理
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: 输入图像 (BGR格式)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
preprocessed_image: 预处理后的图像
|
||||||
|
scale_x: X轴缩放比例
|
||||||
|
scale_y: Y轴缩放比例
|
||||||
|
"""
|
||||||
|
original_height, original_width = image.shape[:2]
|
||||||
|
target_width, target_height = self.input_size
|
||||||
|
|
||||||
|
# 计算缩放比例
|
||||||
|
scale_x = target_width / original_width
|
||||||
|
scale_y = target_height / original_height
|
||||||
|
|
||||||
|
# 等比例缩放
|
||||||
|
scale = min(scale_x, scale_y)
|
||||||
|
new_width = int(original_width * scale)
|
||||||
|
new_height = int(original_height * scale)
|
||||||
|
|
||||||
|
# 缩放图像
|
||||||
|
resized_image = cv2.resize(image, (new_width, new_height))
|
||||||
|
|
||||||
|
# 创建目标尺寸的图像并居中放置
|
||||||
|
padded_image = np.full((target_height, target_width, 3), 114, dtype=np.uint8)
|
||||||
|
|
||||||
|
# 计算填充位置
|
||||||
|
start_x = (target_width - new_width) // 2
|
||||||
|
start_y = (target_height - new_height) // 2
|
||||||
|
|
||||||
|
padded_image[start_y:start_y + new_height, start_x:start_x + new_width] = resized_image
|
||||||
|
|
||||||
|
# 转换为RGB并归一化
|
||||||
|
rgb_image = cv2.cvtColor(padded_image, cv2.COLOR_BGR2RGB)
|
||||||
|
normalized_image = rgb_image.astype(np.float32) / 255.0
|
||||||
|
|
||||||
|
# 转换为NCHW格式
|
||||||
|
input_tensor = np.transpose(normalized_image, (2, 0, 1))
|
||||||
|
input_tensor = np.expand_dims(input_tensor, axis=0)
|
||||||
|
|
||||||
|
return input_tensor, scale, scale
|
||||||
|
|
||||||
|
def postprocess(self, outputs: List[np.ndarray], scale_x: float, scale_y: float,
|
||||||
|
original_shape: Tuple[int, int]) -> List[dict]:
|
||||||
|
"""
|
||||||
|
后处理检测结果
|
||||||
|
|
||||||
|
Args:
|
||||||
|
outputs: 模型输出
|
||||||
|
scale_x: X轴缩放比例
|
||||||
|
scale_y: Y轴缩放比例
|
||||||
|
original_shape: 原始图像尺寸 (height, width)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
检测结果列表
|
||||||
|
"""
|
||||||
|
detections = []
|
||||||
|
|
||||||
|
if len(outputs) == 0:
|
||||||
|
return detections
|
||||||
|
|
||||||
|
# 获取输出张量
|
||||||
|
output = outputs[0]
|
||||||
|
|
||||||
|
# YOLO11输出格式: [batch, 6, 8400] -> [batch, 8400, 6]
|
||||||
|
if len(output.shape) == 3:
|
||||||
|
output = output.transpose(0, 2, 1)
|
||||||
|
|
||||||
|
# 处理每个检测结果
|
||||||
|
for detection in output[0]: # 取第一个batch
|
||||||
|
# 前4个值是边界框坐标,后2个是类别概率
|
||||||
|
x_center, y_center, width, height = detection[:4]
|
||||||
|
class_scores = detection[4:] # 类别概率 [蓝牌概率, 绿牌概率]
|
||||||
|
|
||||||
|
# 获取最高概率的类别
|
||||||
|
class_id = np.argmax(class_scores)
|
||||||
|
confidence = class_scores[class_id] # 使用类别概率作为置信度
|
||||||
|
|
||||||
|
# 过滤低置信度检测
|
||||||
|
if confidence < self.conf_threshold:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 转换坐标到原始图像尺寸
|
||||||
|
original_height, original_width = original_shape
|
||||||
|
|
||||||
|
# 计算实际缩放比例和偏移
|
||||||
|
scale = min(self.input_size[0] / original_width, self.input_size[1] / original_height)
|
||||||
|
pad_x = (self.input_size[0] - original_width * scale) / 2
|
||||||
|
pad_y = (self.input_size[1] - original_height * scale) / 2
|
||||||
|
|
||||||
|
# 转换坐标
|
||||||
|
x_center = (x_center - pad_x) / scale
|
||||||
|
y_center = (y_center - pad_y) / scale
|
||||||
|
width = width / scale
|
||||||
|
height = height / scale
|
||||||
|
|
||||||
|
# 计算边界框
|
||||||
|
x1 = int(x_center - width / 2)
|
||||||
|
y1 = int(y_center - height / 2)
|
||||||
|
x2 = int(x_center + width / 2)
|
||||||
|
y2 = int(y_center + height / 2)
|
||||||
|
|
||||||
|
# 确保坐标在图像范围内
|
||||||
|
x1 = max(0, min(x1, original_width - 1))
|
||||||
|
y1 = max(0, min(y1, original_height - 1))
|
||||||
|
x2 = max(0, min(x2, original_width - 1))
|
||||||
|
y2 = max(0, min(y2, original_height - 1))
|
||||||
|
|
||||||
|
# 定义类别名称
|
||||||
|
class_names = ['blue_plate', 'green_plate'] # 0: 蓝牌, 1: 绿牌
|
||||||
|
class_name = class_names[class_id] if class_id < len(class_names) else 'unknown'
|
||||||
|
|
||||||
|
detections.append({
|
||||||
|
'bbox': [x1, y1, x2, y2],
|
||||||
|
'confidence': float(confidence),
|
||||||
|
'class_id': int(class_id),
|
||||||
|
'class_name': class_name
|
||||||
|
})
|
||||||
|
|
||||||
|
# 应用NMS
|
||||||
|
if detections:
|
||||||
|
detections = self._apply_nms(detections)
|
||||||
|
|
||||||
|
return detections
|
||||||
|
|
||||||
|
def _apply_nms(self, detections: List[dict]) -> List[dict]:
|
||||||
|
"""
|
||||||
|
应用非极大值抑制
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detections: 检测结果列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
NMS后的检测结果
|
||||||
|
"""
|
||||||
|
if len(detections) == 0:
|
||||||
|
return detections
|
||||||
|
|
||||||
|
# 提取边界框和置信度
|
||||||
|
boxes = np.array([det['bbox'] for det in detections])
|
||||||
|
scores = np.array([det['confidence'] for det in detections])
|
||||||
|
|
||||||
|
# 应用NMS
|
||||||
|
indices = cv2.dnn.NMSBoxes(
|
||||||
|
boxes.tolist(),
|
||||||
|
scores.tolist(),
|
||||||
|
self.conf_threshold,
|
||||||
|
self.nms_threshold
|
||||||
|
)
|
||||||
|
|
||||||
|
# 返回保留的检测结果
|
||||||
|
if len(indices) > 0:
|
||||||
|
indices = indices.flatten()
|
||||||
|
return [detections[i] for i in indices]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def detect(self, image: np.ndarray) -> List[dict]:
|
||||||
|
"""
|
||||||
|
检测车牌
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: 输入图像 (BGR格式)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
检测结果列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 预处理
|
||||||
|
input_tensor, scale_x, scale_y = self.preprocess(image)
|
||||||
|
|
||||||
|
# 推理
|
||||||
|
outputs = self.session.run(self.output_names, {self.input_name: input_tensor})
|
||||||
|
|
||||||
|
# 调试输出
|
||||||
|
print(f"模型输出数量: {len(outputs)}")
|
||||||
|
for i, output in enumerate(outputs):
|
||||||
|
print(f"输出 {i} 形状: {output.shape}")
|
||||||
|
print(f"输出 {i} 数据范围: [{output.min():.4f}, {output.max():.4f}]")
|
||||||
|
|
||||||
|
# 后处理
|
||||||
|
detections = self.postprocess(outputs, scale_x, scale_y, image.shape[:2])
|
||||||
|
print(f"检测到的目标数量: {len(detections)}")
|
||||||
|
for i, det in enumerate(detections):
|
||||||
|
print(f"检测 {i}: 类别={det['class_name']}, 置信度={det['confidence']:.3f}")
|
||||||
|
|
||||||
|
return detections
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"检测过程出错: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def draw_detections(self, image: np.ndarray, detections: List[dict]) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
在图像上绘制检测结果
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: 输入图像
|
||||||
|
detections: 检测结果列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
绘制了检测框的图像
|
||||||
|
"""
|
||||||
|
result_image = image.copy()
|
||||||
|
|
||||||
|
for detection in detections:
|
||||||
|
bbox = detection['bbox']
|
||||||
|
confidence = detection['confidence']
|
||||||
|
class_id = detection['class_id']
|
||||||
|
class_name = detection['class_name']
|
||||||
|
|
||||||
|
x1, y1, x2, y2 = bbox
|
||||||
|
|
||||||
|
# 根据车牌类型选择颜色
|
||||||
|
if class_id == 0: # 蓝牌
|
||||||
|
color = (255, 0, 0) # 蓝色 (BGR格式)
|
||||||
|
plate_type = "Blue Plate"
|
||||||
|
elif class_id == 1: # 绿牌
|
||||||
|
color = (0, 255, 0) # 绿色 (BGR格式)
|
||||||
|
plate_type = "Green Plate"
|
||||||
|
else:
|
||||||
|
color = (0, 255, 255) # 黄色 (BGR格式)
|
||||||
|
plate_type = "Unknown"
|
||||||
|
|
||||||
|
# 绘制边界框
|
||||||
|
cv2.rectangle(result_image, (x1, y1), (x2, y2), color, 2)
|
||||||
|
|
||||||
|
# 绘制置信度标签
|
||||||
|
label = f"{plate_type}: {confidence:.2f}"
|
||||||
|
label_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0]
|
||||||
|
|
||||||
|
# 绘制标签背景
|
||||||
|
cv2.rectangle(result_image,
|
||||||
|
(x1, y1 - label_size[1] - 10),
|
||||||
|
(x1 + label_size[0], y1),
|
||||||
|
color, -1)
|
||||||
|
|
||||||
|
# 绘制标签文字
|
||||||
|
cv2.putText(result_image, label,
|
||||||
|
(x1, y1 - 5),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.6,
|
||||||
|
(255, 255, 255), 2)
|
||||||
|
|
||||||
|
return result_image
|
||||||
|
|
||||||
|
def crop_plates(self, image: np.ndarray, detections: List[dict]) -> List[np.ndarray]:
|
||||||
|
"""
|
||||||
|
切割车牌图像
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: 原始图像
|
||||||
|
detections: 检测结果列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
切割后的车牌图像列表
|
||||||
|
"""
|
||||||
|
plate_images = []
|
||||||
|
|
||||||
|
for detection in detections:
|
||||||
|
bbox = detection['bbox']
|
||||||
|
x1, y1, x2, y2 = bbox
|
||||||
|
|
||||||
|
# 确保坐标有效
|
||||||
|
if x2 > x1 and y2 > y1:
|
||||||
|
# 切割车牌区域
|
||||||
|
plate_image = image[y1:y2, x1:x2]
|
||||||
|
if plate_image.size > 0:
|
||||||
|
plate_images.append(plate_image)
|
||||||
|
|
||||||
|
return plate_images
|
||||||
17
yolopart/requirements.txt
Normal file
17
yolopart/requirements.txt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# 车牌检测系统依赖包
|
||||||
|
|
||||||
|
# 核心依赖
|
||||||
|
PyQt5>=5.15.0
|
||||||
|
opencv-python>=4.5.0
|
||||||
|
onnxruntime-gpu>=1.12.0
|
||||||
|
numpy>=1.21.0
|
||||||
|
|
||||||
|
# 可选依赖(车牌识别)
|
||||||
|
# paddlepaddle>=2.4.0
|
||||||
|
# paddleocr>=2.6.0
|
||||||
|
# pytesseract>=0.3.10
|
||||||
|
|
||||||
|
# 开发依赖
|
||||||
|
# pytest>=7.0.0
|
||||||
|
# black>=22.0.0
|
||||||
|
# flake8>=4.0.0
|
||||||
1
yolopart/ui/__init__.py
Normal file
1
yolopart/ui/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# UI模块初始化文件
|
||||||
BIN
yolopart/ui/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
yolopart/ui/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
yolopart/ui/__pycache__/main_window.cpython-38.pyc
Normal file
BIN
yolopart/ui/__pycache__/main_window.cpython-38.pyc
Normal file
Binary file not shown.
BIN
yolopart/ui/__pycache__/video_widget.cpython-38.pyc
Normal file
BIN
yolopart/ui/__pycache__/video_widget.cpython-38.pyc
Normal file
Binary file not shown.
348
yolopart/ui/main_window.py
Normal file
348
yolopart/ui/main_window.py
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
主界面窗口
|
||||||
|
包含视频显示区域、控制按钮和车牌号显示区域
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QLabel, QPushButton, QFrame, QTextEdit, QGroupBox,
|
||||||
|
QCheckBox, QSpinBox, QSlider, QGridLayout
|
||||||
|
)
|
||||||
|
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||||||
|
from PyQt5.QtGui import QFont, QPixmap, QPalette, QImage
|
||||||
|
|
||||||
|
from .video_widget import VideoWidget
|
||||||
|
from utils.video_capture import VideoCapture
|
||||||
|
from models.yolo_detector import YOLODetector
|
||||||
|
from models.plate_recognizer import PlateRecognizerManager
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
"""主窗口类"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.video_capture = None
|
||||||
|
self.yolo_detector = None
|
||||||
|
self.plate_recognizer = PlateRecognizerManager('mock') # 车牌识别管理器
|
||||||
|
self.timer = QTimer()
|
||||||
|
self.use_camera = 1 # 1: 摄像头, 0: 视频文件
|
||||||
|
self.detected_plates = [] # 存储切割后的车牌图像数组
|
||||||
|
self.current_frame = None # 存储当前帧用于车牌矫正
|
||||||
|
|
||||||
|
self.init_ui()
|
||||||
|
self.init_detector()
|
||||||
|
self.init_video_capture()
|
||||||
|
self.connect_signals()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
"""初始化用户界面"""
|
||||||
|
self.setWindowTitle("车牌检测系统 - YOLO11s")
|
||||||
|
self.setGeometry(100, 100, 1200, 800)
|
||||||
|
|
||||||
|
# 创建中央widget
|
||||||
|
central_widget = QWidget()
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
# 主布局
|
||||||
|
main_layout = QHBoxLayout(central_widget)
|
||||||
|
|
||||||
|
# 左侧视频显示区域
|
||||||
|
self.create_video_area(main_layout)
|
||||||
|
|
||||||
|
# 右侧控制和信息显示区域
|
||||||
|
self.create_control_area(main_layout)
|
||||||
|
|
||||||
|
# 设置布局比例
|
||||||
|
main_layout.setStretch(0, 3) # 视频区域占3/4
|
||||||
|
main_layout.setStretch(1, 1) # 控制区域占1/4
|
||||||
|
|
||||||
|
def create_video_area(self, parent_layout):
|
||||||
|
"""创建视频显示区域"""
|
||||||
|
video_frame = QFrame()
|
||||||
|
video_frame.setFrameStyle(QFrame.StyledPanel)
|
||||||
|
video_layout = QVBoxLayout(video_frame)
|
||||||
|
|
||||||
|
# 视频显示widget
|
||||||
|
self.video_widget = VideoWidget()
|
||||||
|
video_layout.addWidget(self.video_widget)
|
||||||
|
|
||||||
|
parent_layout.addWidget(video_frame)
|
||||||
|
|
||||||
|
def create_control_area(self, parent_layout):
|
||||||
|
"""创建控制和信息显示区域"""
|
||||||
|
control_frame = QFrame()
|
||||||
|
control_frame.setFrameStyle(QFrame.StyledPanel)
|
||||||
|
control_frame.setMaximumWidth(300)
|
||||||
|
control_layout = QVBoxLayout(control_frame)
|
||||||
|
|
||||||
|
# 控制按钮组
|
||||||
|
self.create_control_buttons(control_layout)
|
||||||
|
|
||||||
|
# 检测信息显示
|
||||||
|
self.create_detection_info(control_layout)
|
||||||
|
|
||||||
|
# 车牌号显示区域
|
||||||
|
self.create_plate_display(control_layout)
|
||||||
|
|
||||||
|
# 系统状态显示
|
||||||
|
self.create_status_display(control_layout)
|
||||||
|
|
||||||
|
parent_layout.addWidget(control_frame)
|
||||||
|
|
||||||
|
def create_control_buttons(self, parent_layout):
|
||||||
|
"""创建控制按钮"""
|
||||||
|
button_group = QGroupBox("控制面板")
|
||||||
|
button_layout = QVBoxLayout(button_group)
|
||||||
|
|
||||||
|
# 开始/停止按钮
|
||||||
|
self.start_btn = QPushButton("开始检测")
|
||||||
|
self.start_btn.setMinimumHeight(40)
|
||||||
|
self.start_btn.clicked.connect(self.toggle_detection)
|
||||||
|
button_layout.addWidget(self.start_btn)
|
||||||
|
|
||||||
|
# 视频源切换
|
||||||
|
self.camera_checkbox = QCheckBox("使用摄像头")
|
||||||
|
self.camera_checkbox.setChecked(True)
|
||||||
|
self.camera_checkbox.stateChanged.connect(self.toggle_video_source)
|
||||||
|
button_layout.addWidget(self.camera_checkbox)
|
||||||
|
|
||||||
|
# 检测开关
|
||||||
|
self.detection_checkbox = QCheckBox("启用检测")
|
||||||
|
self.detection_checkbox.setChecked(True)
|
||||||
|
button_layout.addWidget(self.detection_checkbox)
|
||||||
|
|
||||||
|
parent_layout.addWidget(button_group)
|
||||||
|
|
||||||
|
def create_detection_info(self, parent_layout):
|
||||||
|
"""创建检测信息显示"""
|
||||||
|
info_group = QGroupBox("检测信息")
|
||||||
|
info_layout = QVBoxLayout(info_group)
|
||||||
|
|
||||||
|
# FPS显示
|
||||||
|
self.fps_label = QLabel("FPS: 0")
|
||||||
|
self.fps_label.setFont(QFont("Arial", 12, QFont.Bold))
|
||||||
|
info_layout.addWidget(self.fps_label)
|
||||||
|
|
||||||
|
# 检测数量
|
||||||
|
self.detection_count_label = QLabel("检测到车牌: 0")
|
||||||
|
info_layout.addWidget(self.detection_count_label)
|
||||||
|
|
||||||
|
# 模型信息
|
||||||
|
self.model_info_label = QLabel("模型: YOLO11s (ONNX)")
|
||||||
|
info_layout.addWidget(self.model_info_label)
|
||||||
|
|
||||||
|
parent_layout.addWidget(info_group)
|
||||||
|
|
||||||
|
def create_plate_display(self, parent_layout):
|
||||||
|
"""创建车牌号显示区域"""
|
||||||
|
plate_group = QGroupBox("车牌识别结果")
|
||||||
|
plate_layout = QVBoxLayout(plate_group)
|
||||||
|
|
||||||
|
# 当前识别的车牌号
|
||||||
|
self.current_plate_label = QLabel("当前车牌: 未识别")
|
||||||
|
self.current_plate_label.setFont(QFont("Arial", 14, QFont.Bold))
|
||||||
|
self.current_plate_label.setStyleSheet("color: blue; padding: 10px; border: 1px solid gray;")
|
||||||
|
plate_layout.addWidget(self.current_plate_label)
|
||||||
|
|
||||||
|
# 矫正后的车牌图像显示
|
||||||
|
self.plate_image_label = QLabel("矫正后车牌图像")
|
||||||
|
self.plate_image_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.plate_image_label.setMinimumHeight(100)
|
||||||
|
self.plate_image_label.setMaximumHeight(150)
|
||||||
|
self.plate_image_label.setStyleSheet("border: 1px solid gray; background-color: #f0f0f0;")
|
||||||
|
plate_layout.addWidget(self.plate_image_label)
|
||||||
|
|
||||||
|
# 历史车牌记录
|
||||||
|
history_label = QLabel("历史记录:")
|
||||||
|
plate_layout.addWidget(history_label)
|
||||||
|
|
||||||
|
self.plate_history = QTextEdit()
|
||||||
|
self.plate_history.setMaximumHeight(150)
|
||||||
|
self.plate_history.setReadOnly(True)
|
||||||
|
plate_layout.addWidget(self.plate_history)
|
||||||
|
|
||||||
|
# 预留接口说明
|
||||||
|
interface_label = QLabel("注: 车牌识别接口已预留,可接入OCR模型")
|
||||||
|
interface_label.setStyleSheet("color: gray; font-size: 10px;")
|
||||||
|
plate_layout.addWidget(interface_label)
|
||||||
|
|
||||||
|
parent_layout.addWidget(plate_group)
|
||||||
|
|
||||||
|
def create_status_display(self, parent_layout):
|
||||||
|
"""创建系统状态显示"""
|
||||||
|
status_group = QGroupBox("系统状态")
|
||||||
|
status_layout = QVBoxLayout(status_group)
|
||||||
|
|
||||||
|
self.status_label = QLabel("状态: 就绪")
|
||||||
|
status_layout.addWidget(self.status_label)
|
||||||
|
|
||||||
|
self.gpu_status_label = QLabel("GPU: 检测中...")
|
||||||
|
status_layout.addWidget(self.gpu_status_label)
|
||||||
|
|
||||||
|
parent_layout.addWidget(status_group)
|
||||||
|
|
||||||
|
# 添加弹性空间
|
||||||
|
parent_layout.addStretch()
|
||||||
|
|
||||||
|
def init_detector(self):
|
||||||
|
"""初始化YOLO检测器"""
|
||||||
|
try:
|
||||||
|
model_path = os.path.join(os.path.dirname(__file__), "..", "yolo11sth50.onnx")
|
||||||
|
self.yolo_detector = YOLODetector(model_path)
|
||||||
|
self.model_info_label.setText(f"模型: YOLO11s (ONNX) - GPU: {self.yolo_detector.use_gpu}")
|
||||||
|
self.gpu_status_label.setText(f"GPU: {'启用' if self.yolo_detector.use_gpu else '禁用'}")
|
||||||
|
except Exception as e:
|
||||||
|
self.status_label.setText(f"模型加载失败: {str(e)}")
|
||||||
|
|
||||||
|
def init_video_capture(self):
|
||||||
|
"""初始化视频捕获"""
|
||||||
|
try:
|
||||||
|
self.video_capture = VideoCapture()
|
||||||
|
self.status_label.setText("视频捕获初始化成功")
|
||||||
|
except Exception as e:
|
||||||
|
self.status_label.setText(f"视频捕获初始化失败: {str(e)}")
|
||||||
|
|
||||||
|
def connect_signals(self):
|
||||||
|
"""连接信号和槽"""
|
||||||
|
self.timer.timeout.connect(self.update_frame)
|
||||||
|
|
||||||
|
def toggle_detection(self):
|
||||||
|
"""切换检测状态"""
|
||||||
|
if self.timer.isActive():
|
||||||
|
self.stop_detection()
|
||||||
|
else:
|
||||||
|
self.start_detection()
|
||||||
|
|
||||||
|
def start_detection(self):
|
||||||
|
"""开始检测"""
|
||||||
|
if self.video_capture and self.video_capture.start_capture(self.use_camera):
|
||||||
|
# 根据视频源类型设置定时器间隔
|
||||||
|
video_fps = self.video_capture.get_video_fps()
|
||||||
|
timer_interval = int(1000 / video_fps) # 转换为毫秒
|
||||||
|
self.timer.start(timer_interval)
|
||||||
|
|
||||||
|
self.start_btn.setText("停止检测")
|
||||||
|
source_type = "摄像头" if self.use_camera else f"视频文件({video_fps:.1f}FPS)"
|
||||||
|
self.status_label.setText(f"检测中... - {source_type}")
|
||||||
|
else:
|
||||||
|
self.status_label.setText("启动失败")
|
||||||
|
|
||||||
|
def stop_detection(self):
|
||||||
|
"""停止检测"""
|
||||||
|
self.timer.stop()
|
||||||
|
if self.video_capture:
|
||||||
|
self.video_capture.stop_capture()
|
||||||
|
self.start_btn.setText("开始检测")
|
||||||
|
self.status_label.setText("已停止")
|
||||||
|
|
||||||
|
def toggle_video_source(self, state):
|
||||||
|
"""切换视频源"""
|
||||||
|
self.use_camera = 1 if state == Qt.Checked else 0
|
||||||
|
if self.timer.isActive():
|
||||||
|
self.stop_detection()
|
||||||
|
self.start_detection()
|
||||||
|
|
||||||
|
def update_frame(self):
|
||||||
|
"""更新帧"""
|
||||||
|
if not self.video_capture:
|
||||||
|
return
|
||||||
|
|
||||||
|
frame, fps = self.video_capture.get_frame()
|
||||||
|
if frame is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 保存当前帧用于车牌矫正
|
||||||
|
self.current_frame = frame.copy()
|
||||||
|
|
||||||
|
# 更新FPS显示
|
||||||
|
self.fps_label.setText(f"FPS: {fps:.1f}")
|
||||||
|
|
||||||
|
# 进行检测
|
||||||
|
if self.detection_checkbox.isChecked() and self.yolo_detector:
|
||||||
|
detections = self.yolo_detector.detect(frame)
|
||||||
|
frame = self.yolo_detector.draw_detections(frame, detections)
|
||||||
|
|
||||||
|
# 切割车牌图像
|
||||||
|
if detections:
|
||||||
|
self.detected_plates = self.yolo_detector.crop_plates(frame, detections)
|
||||||
|
|
||||||
|
# 统计不同类型车牌数量
|
||||||
|
blue_count = sum(1 for d in detections if d['class_id'] == 0)
|
||||||
|
green_count = sum(1 for d in detections if d['class_id'] == 1)
|
||||||
|
total_count = len(detections)
|
||||||
|
|
||||||
|
self.detection_count_label.setText(f"检测到车牌: {total_count} (蓝牌:{blue_count}, 绿牌:{green_count})")
|
||||||
|
|
||||||
|
# 调用车牌识别接口(预留)
|
||||||
|
self.recognize_plates(self.detected_plates, detections)
|
||||||
|
else:
|
||||||
|
self.detection_count_label.setText("检测到车牌: 0")
|
||||||
|
|
||||||
|
# 显示帧
|
||||||
|
self.video_widget.update_frame(frame)
|
||||||
|
|
||||||
|
def recognize_plates(self, plate_images, detections):
|
||||||
|
"""车牌识别接口(预留)"""
|
||||||
|
# 这里是预留的车牌识别接口
|
||||||
|
# 可以接入OCR模型进行车牌号识别
|
||||||
|
if plate_images and detections and self.current_frame is not None:
|
||||||
|
# 获取最新检测到的车牌信息
|
||||||
|
latest_detection = detections[-1] # 取最后一个检测结果
|
||||||
|
plate_type = "Blue Plate" if latest_detection['class_id'] == 0 else "Green Plate"
|
||||||
|
confidence = latest_detection['confidence']
|
||||||
|
|
||||||
|
# 处理蓝色车牌的矫正
|
||||||
|
corrected_image = None
|
||||||
|
if latest_detection['class_id'] == 0: # 蓝色车牌
|
||||||
|
try:
|
||||||
|
bbox = latest_detection['bbox']
|
||||||
|
corrected_image = self.plate_recognizer.preprocess_blue_plate(
|
||||||
|
plate_images[-1], self.current_frame, bbox
|
||||||
|
)
|
||||||
|
self._display_plate_image(corrected_image)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"蓝色车牌矫正失败: {e}")
|
||||||
|
self.plate_image_label.setText("蓝色车牌矫正失败")
|
||||||
|
elif latest_detection['class_id'] == 1: # 绿色车牌
|
||||||
|
# 绿色车牌处理预留
|
||||||
|
self.plate_image_label.setText("绿色车牌处理\n(待实现)")
|
||||||
|
|
||||||
|
# 模拟识别结果
|
||||||
|
plate_text = f"Mock {plate_type}-{len(plate_images)}"
|
||||||
|
self.current_plate_label.setText(f"Current Plate: {plate_text} (Confidence: {confidence:.2f})")
|
||||||
|
|
||||||
|
# 添加到历史记录
|
||||||
|
import datetime
|
||||||
|
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
||||||
|
self.plate_history.append(f"[{timestamp}] {plate_text} (Confidence: {confidence:.2f})")
|
||||||
|
|
||||||
|
def _display_plate_image(self, image):
|
||||||
|
"""在界面上显示车牌图像"""
|
||||||
|
try:
|
||||||
|
# 将OpenCV图像转换为QPixmap
|
||||||
|
if len(image.shape) == 3:
|
||||||
|
height, width, channel = image.shape
|
||||||
|
bytes_per_line = 3 * width
|
||||||
|
q_image = QImage(image.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped()
|
||||||
|
else:
|
||||||
|
height, width = image.shape
|
||||||
|
bytes_per_line = width
|
||||||
|
q_image = QImage(image.data, width, height, bytes_per_line, QImage.Format_Grayscale8)
|
||||||
|
|
||||||
|
# 缩放图像以适应标签大小
|
||||||
|
pixmap = QPixmap.fromImage(q_image)
|
||||||
|
scaled_pixmap = pixmap.scaled(self.plate_image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
|
|
||||||
|
self.plate_image_label.setPixmap(scaled_pixmap)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"显示车牌图像失败: {e}")
|
||||||
|
self.plate_image_label.setText(f"图像显示失败: {str(e)}")
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""窗口关闭事件"""
|
||||||
|
self.stop_detection()
|
||||||
|
event.accept()
|
||||||
59
yolopart/ui/video_widget.py
Normal file
59
yolopart/ui/video_widget.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
视频显示组件
|
||||||
|
用于显示视频帧和检测结果
|
||||||
|
"""
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from PyQt5.QtWidgets import QLabel
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QImage, QPixmap, QPainter, QPen, QFont
|
||||||
|
|
||||||
|
class VideoWidget(QLabel):
|
||||||
|
"""视频显示组件"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setMinimumSize(640, 480)
|
||||||
|
self.setStyleSheet("border: 1px solid gray; background-color: black;")
|
||||||
|
self.setAlignment(Qt.AlignCenter)
|
||||||
|
self.setText("视频显示区域\n点击'开始检测'开始")
|
||||||
|
self.setScaledContents(True)
|
||||||
|
|
||||||
|
def update_frame(self, frame):
|
||||||
|
"""更新显示帧"""
|
||||||
|
if frame is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 转换BGR到RGB
|
||||||
|
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||||
|
h, w, ch = rgb_frame.shape
|
||||||
|
bytes_per_line = ch * w
|
||||||
|
|
||||||
|
# 创建QImage
|
||||||
|
qt_image = QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888)
|
||||||
|
|
||||||
|
# 转换为QPixmap并显示
|
||||||
|
pixmap = QPixmap.fromImage(qt_image)
|
||||||
|
|
||||||
|
# 缩放以适应widget大小,保持宽高比
|
||||||
|
scaled_pixmap = pixmap.scaled(
|
||||||
|
self.size(),
|
||||||
|
Qt.KeepAspectRatio,
|
||||||
|
Qt.SmoothTransformation
|
||||||
|
)
|
||||||
|
|
||||||
|
self.setPixmap(scaled_pixmap)
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
"""绘制事件"""
|
||||||
|
super().paintEvent(event)
|
||||||
|
|
||||||
|
# 如果没有图像,显示提示文本
|
||||||
|
if not self.pixmap():
|
||||||
|
painter = QPainter(self)
|
||||||
|
painter.setPen(QPen(Qt.white))
|
||||||
|
painter.setFont(QFont("Arial", 16))
|
||||||
|
painter.drawText(self.rect(), Qt.AlignCenter, "视频显示区域\n点击'开始检测'开始")
|
||||||
1
yolopart/utils/__init__.py
Normal file
1
yolopart/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 工具模块初始化文件
|
||||||
BIN
yolopart/utils/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
yolopart/utils/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
yolopart/utils/__pycache__/blue_plate_processor.cpython-38.pyc
Normal file
BIN
yolopart/utils/__pycache__/blue_plate_processor.cpython-38.pyc
Normal file
Binary file not shown.
BIN
yolopart/utils/__pycache__/video_capture.cpython-38.pyc
Normal file
BIN
yolopart/utils/__pycache__/video_capture.cpython-38.pyc
Normal file
Binary file not shown.
280
yolopart/utils/video_capture.py
Normal file
280
yolopart/utils/video_capture.py
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
视频捕获管理
|
||||||
|
支持摄像头和视频文件的切换和管理
|
||||||
|
"""
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
class VideoCapture:
|
||||||
|
"""视频捕获管理类"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
初始化视频捕获管理器
|
||||||
|
"""
|
||||||
|
self.cap = None
|
||||||
|
self.is_camera = True
|
||||||
|
self.video_path = None
|
||||||
|
self.fps_counter = FPSCounter()
|
||||||
|
self.frame_lock = threading.Lock()
|
||||||
|
self.current_frame = None
|
||||||
|
self.is_running = False
|
||||||
|
self.video_fps = 30.0 # 视频原始帧率
|
||||||
|
|
||||||
|
# 设置视频文件路径
|
||||||
|
self.video_file_path = os.path.join(os.path.dirname(__file__), "..", "video.mp4")
|
||||||
|
|
||||||
|
def start_capture(self, use_camera: int = 1) -> bool:
|
||||||
|
"""
|
||||||
|
开始视频捕获
|
||||||
|
|
||||||
|
Args:
|
||||||
|
use_camera: 1使用摄像头,0使用视频文件
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功启动
|
||||||
|
"""
|
||||||
|
self.stop_capture()
|
||||||
|
|
||||||
|
self.is_camera = bool(use_camera)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.is_camera:
|
||||||
|
# 使用摄像头
|
||||||
|
self.cap = cv2.VideoCapture(0)
|
||||||
|
if not self.cap.isOpened():
|
||||||
|
# 尝试其他摄像头索引
|
||||||
|
for i in range(1, 5):
|
||||||
|
self.cap = cv2.VideoCapture(i)
|
||||||
|
if self.cap.isOpened():
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("无法打开摄像头")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 设置摄像头参数
|
||||||
|
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
|
||||||
|
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
|
||||||
|
self.cap.set(cv2.CAP_PROP_FPS, 30)
|
||||||
|
|
||||||
|
print("摄像头启动成功")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 使用视频文件
|
||||||
|
if not os.path.exists(self.video_file_path):
|
||||||
|
print(f"视频文件不存在: {self.video_file_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.cap = cv2.VideoCapture(self.video_file_path)
|
||||||
|
if not self.cap.isOpened():
|
||||||
|
print(f"无法打开视频文件: {self.video_file_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 获取视频原始帧率
|
||||||
|
self.video_fps = self.cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
if self.video_fps <= 0:
|
||||||
|
self.video_fps = 25.0 # 默认帧率
|
||||||
|
|
||||||
|
print(f"视频文件加载成功: {self.video_file_path}, FPS: {self.video_fps}")
|
||||||
|
|
||||||
|
self.is_running = True
|
||||||
|
self.fps_counter.reset()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"启动视频捕获失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_capture(self):
|
||||||
|
"""停止视频捕获"""
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
if self.cap is not None:
|
||||||
|
self.cap.release()
|
||||||
|
self.cap = None
|
||||||
|
|
||||||
|
with self.frame_lock:
|
||||||
|
self.current_frame = None
|
||||||
|
|
||||||
|
print("视频捕获已停止")
|
||||||
|
|
||||||
|
def get_frame(self) -> Tuple[Optional[cv2.Mat], float]:
|
||||||
|
"""
|
||||||
|
获取当前帧
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(frame, fps): 当前帧和FPS
|
||||||
|
"""
|
||||||
|
if not self.is_running or self.cap is None:
|
||||||
|
return None, 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
ret, frame = self.cap.read()
|
||||||
|
|
||||||
|
if not ret:
|
||||||
|
if not self.is_camera:
|
||||||
|
# 视频文件播放完毕,重新开始(循环播放)
|
||||||
|
self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||||
|
ret, frame = self.cap.read()
|
||||||
|
|
||||||
|
if not ret:
|
||||||
|
return None, 0.0
|
||||||
|
|
||||||
|
# 更新FPS计数器
|
||||||
|
fps = self.fps_counter.update()
|
||||||
|
|
||||||
|
# 在帧上绘制FPS信息
|
||||||
|
frame_with_fps = self._draw_fps(frame, fps)
|
||||||
|
|
||||||
|
with self.frame_lock:
|
||||||
|
self.current_frame = frame_with_fps.copy()
|
||||||
|
|
||||||
|
return frame_with_fps, fps
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取帧失败: {e}")
|
||||||
|
return None, 0.0
|
||||||
|
|
||||||
|
def _draw_fps(self, frame: cv2.Mat, fps: float) -> cv2.Mat:
|
||||||
|
"""
|
||||||
|
在帧上绘制FPS信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: 输入帧
|
||||||
|
fps: 当前FPS
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
绘制了FPS的帧
|
||||||
|
"""
|
||||||
|
result_frame = frame.copy()
|
||||||
|
|
||||||
|
# FPS文本
|
||||||
|
fps_text = f"FPS: {fps:.1f}"
|
||||||
|
|
||||||
|
# 文本参数
|
||||||
|
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||||
|
font_scale = 0.7
|
||||||
|
color = (0, 255, 0) # 绿色
|
||||||
|
thickness = 2
|
||||||
|
|
||||||
|
# 获取文本尺寸
|
||||||
|
text_size = cv2.getTextSize(fps_text, font, font_scale, thickness)[0]
|
||||||
|
|
||||||
|
# 绘制背景矩形
|
||||||
|
cv2.rectangle(result_frame,
|
||||||
|
(10, 10),
|
||||||
|
(20 + text_size[0], 20 + text_size[1]),
|
||||||
|
(0, 0, 0), -1)
|
||||||
|
|
||||||
|
# 绘制FPS文本
|
||||||
|
cv2.putText(result_frame, fps_text,
|
||||||
|
(15, 15 + text_size[1]),
|
||||||
|
font, font_scale, color, thickness)
|
||||||
|
|
||||||
|
return result_frame
|
||||||
|
|
||||||
|
def get_capture_info(self) -> dict:
|
||||||
|
"""
|
||||||
|
获取捕获信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
捕获信息字典
|
||||||
|
"""
|
||||||
|
info = {
|
||||||
|
'is_running': self.is_running,
|
||||||
|
'is_camera': self.is_camera,
|
||||||
|
'video_path': self.video_file_path if not self.is_camera else None,
|
||||||
|
'fps': self.fps_counter.get_fps(),
|
||||||
|
'video_fps': self.video_fps
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.cap is not None:
|
||||||
|
try:
|
||||||
|
info['width'] = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||||
|
info['height'] = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||||
|
if not self.is_camera:
|
||||||
|
info['total_frames'] = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
info['current_frame'] = int(self.cap.get(cv2.CAP_PROP_POS_FRAMES))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
def get_video_fps(self) -> float:
|
||||||
|
"""
|
||||||
|
获取视频帧率
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
视频帧率,摄像头返回30.0,视频文件返回原始帧率
|
||||||
|
"""
|
||||||
|
if self.is_camera:
|
||||||
|
return 30.0 # 摄像头固定30FPS
|
||||||
|
else:
|
||||||
|
return self.video_fps # 视频文件原始帧率
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""析构函数"""
|
||||||
|
self.stop_capture()
|
||||||
|
|
||||||
|
class FPSCounter:
|
||||||
|
"""FPS计数器"""
|
||||||
|
|
||||||
|
def __init__(self, window_size: int = 30):
|
||||||
|
"""
|
||||||
|
初始化FPS计数器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
window_size: 滑动窗口大小
|
||||||
|
"""
|
||||||
|
self.window_size = window_size
|
||||||
|
self.frame_times = []
|
||||||
|
self.last_time = time.time()
|
||||||
|
|
||||||
|
def update(self) -> float:
|
||||||
|
"""
|
||||||
|
更新FPS计数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
当前FPS
|
||||||
|
"""
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# 添加当前帧时间
|
||||||
|
self.frame_times.append(current_time)
|
||||||
|
|
||||||
|
# 保持窗口大小
|
||||||
|
if len(self.frame_times) > self.window_size:
|
||||||
|
self.frame_times.pop(0)
|
||||||
|
|
||||||
|
# 计算FPS
|
||||||
|
if len(self.frame_times) >= 2:
|
||||||
|
time_diff = self.frame_times[-1] - self.frame_times[0]
|
||||||
|
if time_diff > 0:
|
||||||
|
fps = (len(self.frame_times) - 1) / time_diff
|
||||||
|
return fps
|
||||||
|
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def get_fps(self) -> float:
|
||||||
|
"""
|
||||||
|
获取当前FPS
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
当前FPS
|
||||||
|
"""
|
||||||
|
if len(self.frame_times) >= 2:
|
||||||
|
time_diff = self.frame_times[-1] - self.frame_times[0]
|
||||||
|
if time_diff > 0:
|
||||||
|
return (len(self.frame_times) - 1) / time_diff
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""重置计数器"""
|
||||||
|
self.frame_times.clear()
|
||||||
|
self.last_time = time.time()
|
||||||
BIN
yolopart/yolo11sth50.onnx
Normal file
BIN
yolopart/yolo11sth50.onnx
Normal file
Binary file not shown.
Reference in New Issue
Block a user