Compare commits

..

46 Commits

Author SHA1 Message Date
428b577808 更新接口 2025-10-25 14:31:55 +08:00
15a83a5f06 出入库记录上线 2025-10-19 22:42:30 +08:00
418f7f3bc9 费用计算及确认系统上线 2025-10-19 22:29:55 +08:00
a99e8fccb2 再次修复了车牌判断的bug 2025-10-19 20:21:21 +08:00
40f5e1c1be 修复了车牌判断的bug 2025-10-19 19:10:10 +08:00
c1fbccd7ee 删一下缓存 2025-10-19 18:05:46 +08:00
d649738f6c 道闸管理上线 2025-10-19 18:03:57 +08:00
6831a8cd01 更新接口 2025-10-18 18:56:02 +08:00
cf60d96066 更新接口 2025-10-18 18:21:30 +08:00
09c3117f12 更新接口 2025-10-18 11:20:11 +08:00
2a77e6ca8a Merge pull request '图片与视频' (#6) from main-v2 into main
Reviewed-on: #6
2025-10-14 13:22:43 +08:00
56e7347c01 6666666 2025-09-04 01:50:49 +08:00
1c8e15bcd8 更新接口 2025-09-04 00:10:18 +08:00
6c7f013a0c 更新接口 2025-09-04 00:07:52 +08:00
95aa6b6bba LPR 2025-09-02 11:40:41 +08:00
739cd1d914 更新 README.md 2025-09-01 15:42:42 +08:00
01df759772 删除 yolopart/utils/__pycache__/video_capture.cpython-38.pyc 2025-09-01 15:37:13 +08:00
cb88e6fccd 删除 yolopart/utils/__pycache__/blue_plate_processor.cpython-38.pyc 2025-09-01 15:37:10 +08:00
80e995b47c 删除 yolopart/utils/__pycache__/__init__.cpython-38.pyc 2025-09-01 15:37:07 +08:00
f82df06a68 删除 yolopart/ui/__pycache__/video_widget.cpython-38.pyc 2025-09-01 15:36:59 +08:00
dc651af561 删除 yolopart/ui/__pycache__/main_window.cpython-38.pyc 2025-09-01 15:36:57 +08:00
9f9bd25ce7 删除 yolopart/ui/__pycache__/__init__.cpython-38.pyc 2025-09-01 15:36:54 +08:00
97ca0d75c2 删除 yolopart/models/__pycache__/yolo_detector.cpython-38.pyc 2025-09-01 15:36:45 +08:00
75cc3b8ea3 删除 yolopart/models/__pycache__/plate_recognizer.cpython-38.pyc 2025-09-01 15:36:37 +08:00
aca5703b9e 删除 yolopart/models/__pycache__/__init__.cpython-38.pyc 2025-09-01 15:36:32 +08:00
2eba46bc40 Merge pull request 'ocr-v1' (#4) from ocr-v1 into main
Reviewed-on: #4
2025-09-01 15:35:07 +08:00
f342d37d63 修改了模块的函数名,现在想用哪个模块直接导入即可 2025-09-01 15:33:05 +08:00
1c914cf89f OCR 2025-09-01 15:23:44 +08:00
afba7af80b OCR 2025-09-01 00:01:38 +08:00
8eef0d9414 Merge pull request 'yolorestart' (#1) from yolopart_restart into main
Reviewed-on: #1
2025-08-31 18:42:35 +08:00
8e8fda7fe9 Merge remote-tracking branch 'origin/ocr-v1' into ocr-v1
# Conflicts:
#	OCR_part/ocr_interface.py
2025-08-31 18:37:40 +08:00
9879cb1547 Merge pull request 'yolorestart' (#1) from yolopart_restart into main
Reviewed-on: #1
2025-08-31 18:36:36 +08:00
3829cf76ee Merge pull request 'yolorestart' (#1) from yolopart_restart into main
Reviewed-on: #1
2025-08-31 18:28:57 +08:00
c8a541ec11 Merge pull request 'yolorestart' (#1) from yolopart_restart into main
Reviewed-on: #1
2025-08-31 16:11:18 +08:00
b5839d2c36 更新 README.md 2025-08-31 12:53:11 +08:00
afe15b990a 更新 main.py 2025-08-31 12:19:25 +08:00
7f89965956 上传文件至 CRNN_part 2025-08-31 12:18:48 +08:00
c7ecc5325e 删除 CRNN_part/best_model.pth 2025-08-31 12:17:59 +08:00
01b286fce1 更新 CRNN_part/crnn_interface.py 2025-08-31 12:15:38 +08:00
85c8302fc1 Merge pull request 'yolopart_restart' (#3) from yolopart_restart into main
Reviewed-on: #3
2025-08-31 01:26:01 +08:00
0cd70df215 CRNN model 2025-08-31 01:16:08 +08:00
658560c34f Merge pull request 'yolorestart' (#2) from yolopart_restart into main
Reviewed-on: #2
2025-08-30 12:33:05 +08:00
c773a12f90 Merge remote-tracking branch 'origin/main' into yolopart_restart 2025-08-30 12:28:53 +08:00
a41a4a2236 yolorestart 2025-08-30 12:23:01 +08:00
3d7c7a06e4 Merge pull request 'yolorestart' (#1) from yolopart_restart into main
Reviewed-on: #1
2025-08-30 11:55:37 +08:00
19176c71b7 yolorestart 2025-08-30 11:53:20 +08:00
45 changed files with 3724 additions and 1868 deletions

8
.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -1,12 +0,0 @@
<?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="Python 3.12" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml generated
View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/License_plate_recognition.iml" filepath="$PROJECT_DIR$/.idea/License_plate_recognition.iml" />
</modules>
</component>
</project>

7
.idea/vcs.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

BIN
CRNN_part/best_model.pth Normal file

Binary file not shown.

336
CRNN_part/crnn_interface.py Normal file
View File

@@ -0,0 +1,336 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from PIL import Image
import cv2
from torchvision import transforms
import os
# 全局变量
crnn_model = None
crnn_decoder = None
crnn_preprocessor = None
device = None
class CRNN(nn.Module):
"""CRNN车牌识别模型"""
def __init__(self, img_height=32, num_classes=68, hidden_size=256):
super(CRNN, self).__init__()
self.img_height = img_height
self.num_classes = num_classes
self.hidden_size = hidden_size
# CNN特征提取部分 - 7层卷积
self.cnn = nn.Sequential(
# 第1层3->64, 3x3卷积
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 第2层64->128, 3x3卷积
nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 第3层128->256, 3x3卷积
nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
# 第4层256->256, 3x3卷积
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=(2, 1), stride=(2, 1)),
# 第5层256->512, 3x3卷积
nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
# 第6层512->512, 3x3卷积
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=(2, 1), stride=(2, 1)),
# 第7层512->512, 2x2卷积
nn.Conv2d(512, 512, kernel_size=2, stride=1, padding=0),
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
)
# RNN序列建模部分 - 2层双向LSTM
self.rnn = nn.LSTM(
input_size=512,
hidden_size=hidden_size,
num_layers=2,
batch_first=True,
bidirectional=True
)
# 全连接分类层
self.fc = nn.Linear(hidden_size * 2, num_classes)
def forward(self, x):
batch_size = x.size(0)
# CNN特征提取
conv_out = self.cnn(x)
# 重塑为RNN输入格式
batch_size, channels, height, width = conv_out.size()
conv_out = conv_out.permute(0, 3, 1, 2)
conv_out = conv_out.contiguous().view(batch_size, width, channels * height)
# RNN序列建模
rnn_out, _ = self.rnn(conv_out)
# 全连接分类
output = self.fc(rnn_out)
# 转换为CTC需要的格式(width, batch_size, num_classes)
output = output.permute(1, 0, 2)
return output
class CTCDecoder:
"""CTC解码器"""
def __init__(self):
# 定义中国车牌字符集68个字符
self.chars = [
# 空白字符CTC需要
'<BLANK>',
# 中文省份简称
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '',
# 字母 A-Z
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
# 数字 0-9
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
]
self.char_to_idx = {char: idx for idx, char in enumerate(self.chars)}
self.idx_to_char = {idx: char for idx, char in enumerate(self.chars)}
self.blank_idx = 0
def decode_greedy(self, predictions):
"""贪婪解码"""
# 获取每个时间步的最大概率索引
indices = torch.argmax(predictions, dim=1)
# CTC解码移除重复字符和空白字符
decoded_chars = []
prev_idx = -1
for idx in indices:
idx = idx.item()
if idx != prev_idx and idx != self.blank_idx:
if idx < len(self.chars):
decoded_chars.append(self.chars[idx])
prev_idx = idx
return ''.join(decoded_chars)
def decode_with_confidence(self, predictions):
"""解码并返回置信度信息"""
# 应用softmax获得概率
probs = torch.softmax(predictions, dim=1)
# 贪婪解码
indices = torch.argmax(probs, dim=1)
max_probs = torch.max(probs, dim=1)[0]
# CTC解码
decoded_chars = []
char_confidences = []
prev_idx = -1
for i, idx in enumerate(indices):
idx = idx.item()
confidence = max_probs[i].item()
if idx != prev_idx and idx != self.blank_idx:
if idx < len(self.chars):
decoded_chars.append(self.chars[idx])
char_confidences.append(confidence)
prev_idx = idx
text = ''.join(decoded_chars)
avg_confidence = np.mean(char_confidences) if char_confidences else 0.0
return text, avg_confidence, char_confidences
class LicensePlatePreprocessor:
"""车牌图像预处理器"""
def __init__(self, target_height=32, target_width=128):
self.target_height = target_height
self.target_width = target_width
# 定义图像变换
self.transform = transforms.Compose([
transforms.Resize((target_height, target_width)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
def preprocess_numpy_array(self, image_array):
"""预处理numpy数组格式的图像"""
try:
# 确保图像是RGB格式
if len(image_array.shape) == 3 and image_array.shape[2] == 3:
# 如果是BGR格式转换为RGB
if image_array.dtype == np.uint8:
image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB)
# 转换为PIL图像
if image_array.dtype != np.uint8:
image_array = (image_array * 255).astype(np.uint8)
image = Image.fromarray(image_array)
# 应用变换
tensor = self.transform(image)
# 添加batch维度
tensor = tensor.unsqueeze(0)
return tensor
except Exception as e:
print(f"图像预处理失败: {e}")
return None
def LPRNinitialize_model():
"""
初始化CRNN模型
返回:
bool: 初始化是否成功
"""
global crnn_model, crnn_decoder, crnn_preprocessor, device
try:
# 设置设备
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"CRNN使用设备: {device}")
# 初始化组件
crnn_decoder = CTCDecoder()
crnn_preprocessor = LicensePlatePreprocessor(target_height=32, target_width=128)
# 创建模型实例
crnn_model = CRNN(num_classes=len(crnn_decoder.chars), hidden_size=256)
# 加载模型权重
model_path = os.path.join(os.path.dirname(__file__), 'best_model.pth')
if not os.path.exists(model_path):
raise FileNotFoundError(f"模型文件不存在: {model_path}")
print(f"正在加载CRNN模型: {model_path}")
# 加载检查点
checkpoint = torch.load(model_path, map_location=device, weights_only=False)
# 处理不同的模型保存格式
if isinstance(checkpoint, dict):
if 'model_state_dict' in checkpoint:
# 完整检查点格式
state_dict = checkpoint['model_state_dict']
print(f"检查点信息:")
print(f" - 训练轮次: {checkpoint.get('epoch', 'N/A')}")
print(f" - 最佳验证损失: {checkpoint.get('best_val_loss', 'N/A')}")
else:
# 精简模型格式(只包含权重)
print("加载精简模型(仅权重)")
state_dict = checkpoint
else:
# 直接是状态字典
state_dict = checkpoint
# 加载权重
crnn_model.load_state_dict(state_dict)
crnn_model.to(device)
crnn_model.eval()
print("CRNN模型初始化完成")
# 统计模型参数
total_params = sum(p.numel() for p in crnn_model.parameters())
print(f"CRNN模型参数数量: {total_params:,}")
return True
except Exception as e:
print(f"CRNN模型初始化失败: {e}")
import traceback
traceback.print_exc()
return False
def LPRNmodel_predict(image_array):
"""
CRNN车牌号识别接口函数
参数:
image_array: numpy数组格式的车牌图像已经过矫正处理
返回:
list: 包含最多8个字符的列表代表车牌号的每个字符
例如: ['', 'A', '1', '2', '3', '4', '5', ''] (蓝牌7位+占位符)
['', 'A', 'D', '1', '2', '3', '4', '5'] (绿牌8位)
"""
global crnn_model, crnn_decoder, crnn_preprocessor, device
if crnn_model is None or crnn_decoder is None or crnn_preprocessor is None:
print("CRNN模型未初始化请先调用initialize_crnn_model()")
return ['', '', '', '0', '0', '0', '0', '0']
try:
# 预处理图像
input_tensor = crnn_preprocessor.preprocess_numpy_array(image_array)
if input_tensor is None:
raise ValueError("图像预处理失败")
input_tensor = input_tensor.to(device)
# 模型推理
with torch.no_grad():
outputs = crnn_model(input_tensor) # (seq_len, batch_size, num_classes)
# 移除batch维度
outputs = outputs.squeeze(1) # (seq_len, num_classes)
# CTC解码
predicted_text, confidence, char_confidences = crnn_decoder.decode_with_confidence(outputs)
print(f"CRNN识别结果: {predicted_text}, 置信度: {confidence:.3f}")
# 将字符串转换为字符列表
char_list = list(predicted_text)
# 确保返回至少7个字符最多8个字符
if len(char_list) < 7:
# 如果识别结果少于7个字符用'0'补齐到7位
char_list.extend(['0'] * (7 - len(char_list)))
elif len(char_list) > 8:
# 如果识别结果多于8个字符截取前8个
char_list = char_list[:8]
# 如果是7位补齐到8位以保持接口一致性第8位用空字符或占位符
if len(char_list) == 7:
char_list.append('') # 添加空字符作为第8位占位符
return char_list
except Exception as e:
print(f"CRNN识别失败: {e}")
import traceback
traceback.print_exc()
return ['', '', '', '', '0', '0', '0', '0']

90
OCR_part/ocr_interface.py Normal file
View File

@@ -0,0 +1,90 @@
import numpy as np
from paddleocr import TextRecognition
import cv2
class OCRProcessor:
def __init__(self):
self.model = TextRecognition(model_name="PP-OCRv5_server_rec")
# 定义允许的字符集合(不包含空白字符)
self.allowed_chars = [
# 中文省份简称
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '',
# 字母 A-Z
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
# 数字 0-9
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
]
print("OCR模型初始化完成占位")
def predict(self, image_array):
# 保持原有模型调用方式
output = self.model.predict(input=image_array)
# 结构化输出结果
results = output[0]["rec_text"]
placeholder_result = results.split(',')
return placeholder_result
def filter_allowed_chars(self, text):
"""只保留允许的字符"""
filtered_text = ""
for char in text:
if char in self.allowed_chars:
filtered_text += char
return filtered_text
# 保留原有函数接口
_processor = OCRProcessor()
def LPRNinitialize_model():
return _processor
def LPRNmodel_predict(image_array):
"""
OCR车牌号识别接口函数
参数:
image_array: numpy数组格式的车牌图像已经过矫正处理
返回:
list: 包含最多8个字符的列表代表车牌号的每个字符
例如: ['', 'A', '1', '2', '3', '4', '5', ''] (蓝牌7位+占位符)
['', 'A', 'D', '1', '2', '3', '4', '5'] (绿牌8位)
"""
# 获取原始预测结果
raw_result = _processor.predict(image_array)
# 将结果合并为字符串(如果是列表的话)
if isinstance(raw_result, list):
result_str = ''.join(raw_result)
else:
result_str = str(raw_result)
# 过滤掉'·'和'-'字符
filtered_str = result_str.replace('·', '')
filtered_str = filtered_str.replace('-', '')
# 只保留允许的字符
filtered_str = _processor.filter_allowed_chars(filtered_str)
# 转换为字符列表
char_list = list(filtered_str)
# 确保返回至少7个字符最多8个字符
if len(char_list) < 7:
# 如果识别结果少于7个字符用'0'补齐到7位
char_list.extend(['0'] * (7 - len(char_list)))
elif len(char_list) > 8:
# 如果识别结果多于8个字符截取前8个
char_list = char_list[:8]
# 如果是7位补齐到8位以保持接口一致性第8位用空字符或占位符
if len(char_list) == 7:
char_list.append('') # 添加空字符作为第8位占位符
return char_list

172
README.md Normal file
View File

@@ -0,0 +1,172 @@
# 车牌识别系统
基于YOLO11 Pose模型的实时车牌检测与识别系统支持蓝牌和绿牌的检测、四角点定位、透视矫正和车牌号识别。
## 项目结构
```
License_plate_recognition/
├── main.py # 主程序入口PyQt界面
├── requirements.txt # 依赖包列表
├── README.md # 项目说明文档
├── yolopart/ # YOLO检测模块
│ ├── detector.py # YOLO检测器类
│ └── yolo11s-pose42.pt # YOLO pose模型文件
├── OCR_part/ # OCR识别模块
│ └── ocr_interface.py # OCR接口占位
├── CRNN_part/ # CRNN识别模块
│ └── crnn_interface.py # CRNN接口占位
└── LPRNET_part/ # LPRNet识别模块
├── lpr_interface.py # LPRNet接口已完成
└── LPRNet__iteration_74000.pth # LPRNet模型权重文件
```
## 功能特性
### 1. 实时车牌检测
- 基于YOLO11 Pose模型进行车牌检测
- 支持蓝牌类别0和绿牌类别1识别
- 实时摄像头画面处理
### 2. 四角点定位
- 检测车牌的四个角点right_bottom, left_bottom, left_top, right_top
- 只有检测到完整四个角点的车牌才进行后续处理
- 用黄色线条连接四个角点显示检测结果
### 3. 透视矫正
- 使用四个角点进行透视变换
- 将倾斜的车牌矫正为标准矩形
- 输出标准尺寸的车牌图像供识别使用
### 4. 多种识别方案
- 支持OCR、CRNN和LPRNet三种车牌识别方法
- LPRNet模型准确率高达98%
- 模块化接口设计,便于切换不同识别算法
### 5. PyQt界面
- 左侧:实时摄像头画面显示
- 右侧:检测结果展示区域
- 顶部显示识别到的车牌数量
- 每行显示:车牌类型、矫正后图像、车牌号
- 美观的现代化界面设计
### 6. 模块化设计
- yolopart负责车牌定位和矫正
- OCR_part/CRNN_part/LPRNET_part负责车牌号识别
- 各模块独立,便于维护和扩展
## 安装和使用
### 1. 环境要求
- Python 3.7+
- Windows/Linux/macOS
- 摄像头设备
### 2. 安装依赖
```bash
pip install -r requirements.txt
```
### 3. 模型文件
确保 `yolopart/yolo11s-pose42.pt` 模型文件存在。这是一个YOLO11 Pose模型专门训练用于车牌的四角点检测。
### 4. 运行程序
```bash
python main.py
```
### 5. 选择识别模块
`main.py` 中修改导入语句来选择不同的识别方案:
```python
# 使用LPRNet推荐准确率98%
from LPRNET_part.lpr_interface import LPRNmodel_predict, LPRNinitialize_model
# 使用OCR
from OCR_part.ocr_interface import LPRNmodel_predict, LPRNinitialize_model
# 使用CRNN
from CRNN_part.crnn_interface import LPRNmodel_predict, LPRNinitialize_model
```
### 6. 使用说明
1. 点击"启动摄像头"按钮开始检测
2. 将车牌对准摄像头
3. 系统会自动检测车牌并显示:
- 检测框和角点连线
- 右侧显示车牌类型、矫正图像和车牌号
4. 点击"停止摄像头"结束检测
## 模型输出格式
YOLO Pose模型输出包含
- **检测框**:车牌的边界框坐标
- **类别**0=蓝牌1=绿牌
- **置信度**:检测置信度分数
- **关键点**:四个角点坐标
- right_bottom右下角
- left_bottom左下角
- left_top左上角
- right_top右上角
## 接口说明
### 车牌识别接口
项目为OCR、CRNN和LPRNet识别模块提供了标准接口
```python
# 接口函数名(导入所需模块,每个模块统一函数名)
# 初始化
from 对应模块 import LPRNinitialize_model
LPRNinitialize_model()
# 预测主函数
from 对应模块 import LPRNmodel_predict
result = LPRNmodel_predict(corrected_image) # 返回7个字符的列表
```
### 输入参数
- `corrected_image`numpy数组格式的矫正后车牌图像
### 返回值
- 长度为7的字符列表包含车牌号的每个字符
- 例如:`['京', 'A', '1', '2', '3', '4', '5']`
### LPRNet模块特性
- **高准确率**: 模型准确率高达98%
- **快速推理**: 基于深度学习的端到端识别
- **CTC解码**: 使用CTCConnectionist Temporal Classification解码算法
- **支持中文**: 完整支持中文省份简称和字母数字组合
- **模型权重**: 使用预训练的LPRNet__iteration_74000.pth权重文件
## 开发说明
### 添加新的识别算法
1. 在对应目录OCR_part或CRNN_part实现识别函数
2. 确保函数签名与接口一致
3. 在main.py中导入对应模块即可
### 自定义模型
1. 替换 `yolopart/yolo11s-pose42.pt` 文件
2. 确保新模型输出格式与现有接口兼容
3. 根据需要调整类别名称和数量
### 调试模式
在代码中设置调试标志可以输出更多信息:
```python
# 在detector.py中设置verbose=True
results = self.model(image, conf=conf_threshold, verbose=True)
```
## 扩展功能
系统设计支持以下扩展:
- 多摄像头支持
- 批量图像处理
- 检测结果保存
- 网络API接口
- 数据库集成
- 性能统计和分析

69
communicate.py Normal file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
向Hi3861设备发送JSON命令
"""
import socket
import json
import time
import pyttsx3
import threading
target_ip = "192.168.43.12"
target_port = 8081
def speak_text(text):
"""
使用文本转语音播放文本
每次调用都创建新的引擎实例以避免并发问题
"""
def _speak():
try:
if text and text.strip(): # 确保文本不为空
# 在线程内部创建新的引擎实例
engine = pyttsx3.init()
# 设置语音速度
engine.setProperty('rate', 150)
# 设置音量0.0到1.0
engine.setProperty('volume', 0.8)
engine.say(text)
engine.runAndWait()
# 清理引擎
engine.stop()
del engine
except Exception as e:
print(f"语音播放失败: {e}")
# 在新线程中播放语音,避免阻塞
speech_thread = threading.Thread(target=_speak)
speech_thread.daemon = True
speech_thread.start()
def send_command(cmd, text):
#cmd为1道闸打开十秒后关闭,oled显示字符串信息默认使用及cmd为4
#cmd为2道闸舵机向打开方向旋转90度oled上不显示仅在qt界面手动开闸时调用
#cmd为3道闸舵机向关闭方向旋转90度oled上不显示仅在qt界面手动关闸时调用
#cmd为4oled显示字符串信息道闸舵机不旋转
command = {
"cmd": cmd,
"text": text
}
json_command = json.dumps(command, ensure_ascii=False)
try:
# 创建UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(json_command.encode('utf-8'), (target_ip, target_port))
# 发送命令后播放语音
if text and text.strip():
speak_text(text)
except Exception as e:
print(f"发送命令失败: {e}")
finally:
sock.close()

251
gate_control.py Normal file
View File

@@ -0,0 +1,251 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
道闸控制模块
负责与Hi3861设备通信控制道闸开关
"""
import socket
import json
import time
from datetime import datetime, timedelta
from PyQt5.QtCore import QObject, pyqtSignal, QThread
class GateControlThread(QThread):
"""道闸控制线程,用于异步发送命令"""
command_sent = pyqtSignal(str, bool) # 信号:命令内容,是否成功
def __init__(self, ip, port, command):
super().__init__()
self.ip = ip
self.port = port
self.command = command
def run(self):
"""发送命令到Hi3861设备"""
try:
# 创建UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 发送命令
json_command = json.dumps(self.command, ensure_ascii=False)
sock.sendto(json_command.encode('utf-8'), (self.ip, self.port))
# 发出成功信号
self.command_sent.emit(json_command, True)
except Exception as e:
# 发出失败信号
self.command_sent.emit(f"发送失败: {e}", False)
finally:
sock.close()
class GateController(QObject):
"""道闸控制器"""
# 信号
log_message = pyqtSignal(str) # 日志消息
gate_opened = pyqtSignal(str) # 道闸打开信号,附带车牌号
def __init__(self, ip="192.168.43.12", port=8081):
super().__init__()
self.ip = ip
self.port = port
self.last_pass_times = {} # 记录车牌上次通过时间
self.thread_pool = [] # 线程池
def send_command(self, cmd, text=""):
"""
发送命令到道闸
参数:
cmd: 命令类型 (1-4)
text: 显示文本
返回:
bool: 是否发送成功
"""
# 创建JSON命令
command = {
"cmd": cmd,
"text": text
}
# 创建并启动线程发送命令
thread = GateControlThread(self.ip, self.port, command)
thread.command_sent.connect(self.on_command_sent)
thread.start()
self.thread_pool.append(thread)
# 记录日志
cmd_desc = {
1: "自动开闸(10秒后关闭)",
2: "手动开闸",
3: "手动关闸",
4: "仅显示信息"
}
self.log_message.emit(f"发送命令: {cmd_desc.get(cmd, '未知命令')} - {text}")
return True
def on_command_sent(self, message, success):
"""命令发送结果处理"""
if success:
self.log_message.emit(f"命令发送成功: {message}")
else:
self.log_message.emit(f"命令发送失败: {message}")
def auto_open_gate(self, plate_number):
"""
自动开闸(检测到白名单车牌时调用)
参数:
plate_number: 车牌号
"""
# 获取当前时间
current_time = datetime.now()
time_diff_str = ""
# 检查是否是第一次通行
if plate_number in self.last_pass_times:
# 第二次或更多次通行,计算时间差
last_time = self.last_pass_times[plate_number]
time_diff = current_time - last_time
# 格式化时间差
total_seconds = int(time_diff.total_seconds())
minutes = total_seconds // 60
seconds = total_seconds % 60
if minutes > 0:
time_diff_str = f" {minutes}min{seconds}sec"
else:
time_diff_str = f" {seconds}sec"
# 计算时间差后清空之前记录的时间点
del self.last_pass_times[plate_number]
log_msg = f"检测到白名单车牌: {plate_number},自动开闸{time_diff_str},已清空时间记录"
else:
# 第一次通行,只记录时间,不计算时间差
self.last_pass_times[plate_number] = current_time
log_msg = f"检测到白名单车牌: {plate_number},首次通行,已记录时间"
# 发送开闸命令
display_text = f"{plate_number} 通行{time_diff_str}"
self.send_command(1, display_text)
# 发出信号
self.gate_opened.emit(plate_number)
# 记录日志
self.log_message.emit(log_msg)
def manual_open_gate(self):
"""手动开闸"""
self.send_command(2, "")
self.log_message.emit("手动开闸")
def manual_close_gate(self):
"""手动关闸"""
self.send_command(3, "")
self.log_message.emit("手动关闸")
def display_message(self, text):
"""仅显示信息,不控制道闸"""
self.send_command(4, text)
self.log_message.emit(f"显示信息: {text}")
def deny_access(self, plate_number):
"""
拒绝通行(检测到非白名单车牌时调用)
参数:
plate_number: 车牌号
"""
self.send_command(4, f"{plate_number} 禁止通行")
self.log_message.emit(f"检测到非白名单车牌: {plate_number},拒绝通行")
class WhitelistManager(QObject):
"""白名单管理器"""
# 信号
whitelist_changed = pyqtSignal(list) # 白名单变更信号
def __init__(self):
super().__init__()
self.whitelist = [] # 白名单车牌列表
def add_plate(self, plate_number):
"""
添加车牌到白名单
参数:
plate_number: 车牌号
返回:
bool: 是否添加成功
"""
if not plate_number or plate_number in self.whitelist:
return False
self.whitelist.append(plate_number)
self.whitelist_changed.emit(self.whitelist.copy())
return True
def remove_plate(self, plate_number):
"""
从白名单移除车牌
参数:
plate_number: 车牌号
返回:
bool: 是否移除成功
"""
if plate_number in self.whitelist:
self.whitelist.remove(plate_number)
self.whitelist_changed.emit(self.whitelist.copy())
return True
return False
def edit_plate(self, old_plate, new_plate):
"""
编辑白名单中的车牌
参数:
old_plate: 原车牌号
new_plate: 新车牌号
返回:
bool: 是否编辑成功
"""
if old_plate in self.whitelist and new_plate not in self.whitelist:
index = self.whitelist.index(old_plate)
self.whitelist[index] = new_plate
self.whitelist_changed.emit(self.whitelist.copy())
return True
return False
def is_whitelisted(self, plate_number):
"""
检查车牌是否在白名单中
参数:
plate_number: 车牌号
返回:
bool: 是否在白名单中
"""
return plate_number in self.whitelist
def get_whitelist(self):
"""获取白名单副本"""
return self.whitelist.copy()
def clear_whitelist(self):
"""清空白名单"""
self.whitelist.clear()
self.whitelist_changed.emit(self.whitelist.copy())

Binary file not shown.

View File

@@ -0,0 +1,546 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from PIL import Image
import cv2
from torchvision import transforms
import os
import math
# 全局变量
lightcrnn_model = None
lightcrnn_decoder = None
lightcrnn_preprocessor = None
device = None
class DepthwiseSeparableConv(nn.Module):
"""深度可分离卷积"""
def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1):
super(DepthwiseSeparableConv, self).__init__()
# 深度卷积
self.depthwise = nn.Conv2d(in_channels, in_channels, kernel_size=kernel_size,
stride=stride, padding=padding, groups=in_channels, bias=False)
# 逐点卷积
self.pointwise = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
self.bn = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU6(inplace=True)
def forward(self, x):
x = self.depthwise(x)
x = self.pointwise(x)
x = self.bn(x)
x = self.relu(x)
return x
class ChannelAttention(nn.Module):
"""通道注意力机制"""
def __init__(self, in_channels, reduction=16):
super(ChannelAttention, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.max_pool = nn.AdaptiveMaxPool2d(1)
self.fc = nn.Sequential(
nn.Conv2d(in_channels, in_channels // reduction, 1, bias=False),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels // reduction, in_channels, 1, bias=False)
)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
avg_out = self.fc(self.avg_pool(x))
max_out = self.fc(self.max_pool(x))
out = avg_out + max_out
return x * self.sigmoid(out)
class InvertedResidual(nn.Module):
"""MobileNetV2的倒残差块"""
def __init__(self, in_channels, out_channels, stride=1, expand_ratio=6):
super(InvertedResidual, self).__init__()
self.stride = stride
self.use_residual = stride == 1 and in_channels == out_channels
hidden_dim = int(round(in_channels * expand_ratio))
layers = []
if expand_ratio != 1:
# 扩展层
layers.extend([
nn.Conv2d(in_channels, hidden_dim, 1, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.ReLU6(inplace=True)
])
# 深度卷积
layers.extend([
nn.Conv2d(hidden_dim, hidden_dim, 3, stride=stride, padding=1, groups=hidden_dim, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.ReLU6(inplace=True),
# 线性瓶颈
nn.Conv2d(hidden_dim, out_channels, 1, bias=False),
nn.BatchNorm2d(out_channels)
])
self.conv = nn.Sequential(*layers)
def forward(self, x):
if self.use_residual:
return x + self.conv(x)
else:
return self.conv(x)
class LightweightCNN(nn.Module):
"""增强版轻量化CNN特征提取器"""
def __init__(self, num_channels=3):
super(LightweightCNN, self).__init__()
# 初始卷积层 - 适当增加通道数
self.conv1 = nn.Sequential(
nn.Conv2d(num_channels, 48, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(48),
nn.ReLU6(inplace=True)
)
# 增强版MobileNet风格的特征提取
self.features = nn.Sequential(
# 第一组48 -> 32
InvertedResidual(48, 32, stride=1, expand_ratio=2),
InvertedResidual(32, 32, stride=1, expand_ratio=2), # 增加一层
nn.MaxPool2d(kernel_size=2, stride=2), # 32x128 -> 16x64
# 第二组32 -> 48
InvertedResidual(32, 48, stride=1, expand_ratio=4),
InvertedResidual(48, 48, stride=1, expand_ratio=4),
nn.MaxPool2d(kernel_size=2, stride=2), # 16x64 -> 8x32
# 第三组48 -> 64
InvertedResidual(48, 64, stride=1, expand_ratio=4),
InvertedResidual(64, 64, stride=1, expand_ratio=4),
# 第四组64 -> 96
InvertedResidual(64, 96, stride=1, expand_ratio=4),
InvertedResidual(96, 96, stride=1, expand_ratio=4),
nn.MaxPool2d(kernel_size=(2, 1), stride=(2, 1)), # 8x32 -> 4x32
# 第五组96 -> 128
InvertedResidual(96, 128, stride=1, expand_ratio=4),
InvertedResidual(128, 128, stride=1, expand_ratio=4),
nn.MaxPool2d(kernel_size=(2, 1), stride=(2, 1)), # 4x32 -> 2x32
# 最后的卷积层 - 增加通道数
nn.Conv2d(128, 160, kernel_size=2, stride=1, padding=0, bias=False), # 2x32 -> 1x31
nn.BatchNorm2d(160),
nn.ReLU6(inplace=True)
)
# 通道注意力
self.channel_attention = ChannelAttention(160)
def forward(self, x):
x = self.conv1(x)
x = self.features(x)
x = self.channel_attention(x)
return x
class LightweightGRU(nn.Module):
"""增强版轻量化GRU层"""
def __init__(self, input_size, hidden_size, num_layers=2): # 默认增加到2层
super(LightweightGRU, self).__init__()
self.gru = nn.GRU(input_size, hidden_size, num_layers=num_layers,
bidirectional=True, batch_first=True, dropout=0.2 if num_layers > 1 else 0)
# 增加一个额外的线性层
self.linear1 = nn.Linear(hidden_size * 2, hidden_size * 2)
self.linear2 = nn.Linear(hidden_size * 2, hidden_size)
self.dropout = nn.Dropout(0.2) # 增加dropout率
self.norm = nn.LayerNorm(hidden_size) # 添加层归一化
def forward(self, x):
gru_out, _ = self.gru(x)
output = self.linear1(gru_out)
output = F.relu(output) # 添加激活函数
output = self.dropout(output)
output = self.linear2(output)
output = self.norm(output) # 应用层归一化
output = self.dropout(output)
return output
class LightweightCRNN(nn.Module):
"""增强版轻量化CRNN模型"""
def __init__(self, img_height, num_classes, num_channels=3, hidden_size=160): # 调整隐藏层大小
super(LightweightCRNN, self).__init__()
self.img_height = img_height
self.num_classes = num_classes
self.hidden_size = hidden_size
# 增强版轻量化CNN特征提取器
self.cnn = LightweightCNN(num_channels)
# 增强版轻量化RNN序列建模器
self.rnn = LightweightGRU(160, hidden_size, num_layers=2) # 使用更大的输入尺寸和2层GRU
# 输出层 - 添加额外的全连接层
self.fc = nn.Linear(hidden_size, hidden_size // 2)
self.dropout = nn.Dropout(0.2)
self.classifier = nn.Linear(hidden_size // 2, num_classes)
# 初始化权重
self._initialize_weights()
def _initialize_weights(self):
"""初始化模型权重"""
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
def forward(self, input):
"""
input: [batch_size, channels, height, width]
output: [seq_len, batch_size, num_classes]
"""
# CNN特征提取
conv_features = self.cnn(input) # [batch_size, 160, 1, seq_len]
# 重塑为RNN输入格式
batch_size, channels, height, width = conv_features.size()
assert height == 1, f"Height should be 1, got {height}"
# [batch_size, 160, 1, seq_len] -> [batch_size, seq_len, 160]
conv_features = conv_features.squeeze(2) # [batch_size, 160, seq_len]
conv_features = conv_features.permute(0, 2, 1) # [batch_size, seq_len, 160]
# RNN序列建模
rnn_output = self.rnn(conv_features) # [batch_size, seq_len, hidden_size]
# 全连接层处理
fc_output = self.fc(rnn_output) # [batch_size, seq_len, hidden_size//2]
fc_output = F.relu(fc_output)
fc_output = self.dropout(fc_output)
# 分类
output = self.classifier(fc_output) # [batch_size, seq_len, num_classes]
# 转换为CTC期望的格式: [seq_len, batch_size, num_classes]
output = output.permute(1, 0, 2)
return output
class LightCTCDecoder:
"""轻量化CTC解码器"""
def __init__(self):
# 中国车牌字符集
# 省份简称
provinces = ['', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '']
# 字母包含I和O
letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
# 数字
digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
# 组合所有字符
self.character = provinces + letters + digits
# 添加空白字符用于CTC
self.character = ['[blank]'] + self.character
# 创建字符到索引的映射
self.dict = {char: i for i, char in enumerate(self.character)}
self.dict_reverse = {i: char for i, char in enumerate(self.character)}
self.num_classes = len(self.character)
self.blank_idx = 0
def decode_greedy(self, predictions):
"""贪婪解码"""
# 获取每个时间步的最大概率索引
indices = torch.argmax(predictions, dim=1)
# CTC解码移除重复字符和空白字符
decoded_chars = []
prev_idx = -1
for idx in indices:
idx = idx.item()
if idx != prev_idx and idx != self.blank_idx:
if idx < len(self.character):
decoded_chars.append(self.character[idx])
prev_idx = idx
return ''.join(decoded_chars)
def decode_with_confidence(self, predictions):
"""解码并返回置信度信息"""
# 应用softmax获得概率
probs = torch.softmax(predictions, dim=1)
# 贪婪解码
indices = torch.argmax(probs, dim=1)
max_probs = torch.max(probs, dim=1)[0]
# CTC解码
decoded_chars = []
char_confidences = []
prev_idx = -1
for i, idx in enumerate(indices):
idx = idx.item()
confidence = max_probs[i].item()
if idx != prev_idx and idx != self.blank_idx:
if idx < len(self.character):
decoded_chars.append(self.character[idx])
char_confidences.append(confidence)
prev_idx = idx
text = ''.join(decoded_chars)
avg_confidence = np.mean(char_confidences) if char_confidences else 0.0
return text, avg_confidence, char_confidences
class LightLicensePlatePreprocessor:
"""轻量化车牌图像预处理器"""
def __init__(self, target_height=32, target_width=128):
self.target_height = target_height
self.target_width = target_width
# 定义图像变换
self.transform = transforms.Compose([
transforms.Resize((target_height, target_width)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
def preprocess_numpy_array(self, image_array):
"""预处理numpy数组格式的图像"""
try:
# 确保图像是RGB格式
if len(image_array.shape) == 3 and image_array.shape[2] == 3:
# 如果是BGR格式转换为RGB
if image_array.dtype == np.uint8:
image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB)
# 转换为PIL图像
if image_array.dtype != np.uint8:
image_array = (image_array * 255).astype(np.uint8)
image = Image.fromarray(image_array)
# 应用变换
tensor = self.transform(image)
# 添加batch维度
tensor = tensor.unsqueeze(0)
return tensor
except Exception as e:
print(f"图像预处理失败: {e}")
return None
def LPRNinitialize_model():
"""
初始化轻量化CRNN模型
返回:
bool: 初始化是否成功
"""
global lightcrnn_model, lightcrnn_decoder, lightcrnn_preprocessor, device
try:
# 设置设备
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"LightCRNN使用设备: {device}")
# 初始化组件
lightcrnn_decoder = LightCTCDecoder()
lightcrnn_preprocessor = LightLicensePlatePreprocessor(target_height=32, target_width=128)
# 创建模型实例
lightcrnn_model = LightweightCRNN(
img_height=32,
num_classes=lightcrnn_decoder.num_classes,
hidden_size=160
)
# 加载模型权重
model_path = os.path.join(os.path.dirname(__file__), 'best_model.pth')
if not os.path.exists(model_path):
raise FileNotFoundError(f"模型文件不存在: {model_path}")
print(f"正在加载LightCRNN模型: {model_path}")
# 加载检查点,处理可能的模块依赖问题
try:
checkpoint = torch.load(model_path, map_location=device, weights_only=False)
except (ModuleNotFoundError, AttributeError) as e:
if 'config' in str(e) or 'Config' in str(e):
print("检测到模型文件包含config依赖尝试使用weights_only模式加载...")
try:
# 尝试使用weights_only=True来避免pickle问题
checkpoint = torch.load(model_path, map_location=device, weights_only=True)
except Exception:
# 如果还是失败创建一个更完整的mock config
import sys
import types
# 创建mock config模块
mock_config = types.ModuleType('config')
# 添加可能需要的Config类
class Config:
def __init__(self):
pass
mock_config.Config = Config
sys.modules['config'] = mock_config
try:
checkpoint = torch.load(model_path, map_location=device, weights_only=False)
finally:
# 清理临时模块
if 'config' in sys.modules:
del sys.modules['config']
else:
raise e
# 处理不同的模型保存格式
if isinstance(checkpoint, dict):
if 'model_state_dict' in checkpoint:
# 完整检查点格式
state_dict = checkpoint['model_state_dict']
print(f"检查点信息:")
print(f" - 训练轮次: {checkpoint.get('epoch', 'N/A')}")
print(f" - 最佳验证损失: {checkpoint.get('best_val_loss', 'N/A')}")
else:
# 精简模型格式(只包含权重)
print("加载精简模型(仅权重)")
state_dict = checkpoint
else:
# 直接是状态字典
state_dict = checkpoint
# 加载权重
lightcrnn_model.load_state_dict(state_dict)
lightcrnn_model.to(device)
lightcrnn_model.eval()
print("LightCRNN模型初始化完成")
# 统计模型参数
total_params = sum(p.numel() for p in lightcrnn_model.parameters())
print(f"LightCRNN模型参数数量: {total_params:,}")
return True
except Exception as e:
print(f"LightCRNN模型初始化失败: {e}")
import traceback
traceback.print_exc()
return False
def LPRNmodel_predict(image_array):
"""
轻量化CRNN车牌号识别接口函数
参数:
image_array: numpy数组格式的车牌图像已经过矫正处理
返回:
list: 包含最多8个字符的列表代表车牌号的每个字符
例如: ['', 'A', '1', '2', '3', '4', '5', ''] (蓝牌7位+占位符)
['', 'A', 'D', '1', '2', '3', '4', '5'] (绿牌8位)
"""
global lightcrnn_model, lightcrnn_decoder, lightcrnn_preprocessor, device
if lightcrnn_model is None or lightcrnn_decoder is None or lightcrnn_preprocessor is None:
print("LightCRNN模型未初始化请先调用LPRNinitialize_model()")
return ['', '', '', '0', '0', '0', '0', '0']
try:
# 预处理图像
input_tensor = lightcrnn_preprocessor.preprocess_numpy_array(image_array)
if input_tensor is None:
raise ValueError("图像预处理失败")
input_tensor = input_tensor.to(device)
# 模型推理
with torch.no_grad():
outputs = lightcrnn_model(input_tensor) # (seq_len, batch_size, num_classes)
# 移除batch维度
outputs = outputs.squeeze(1) # (seq_len, num_classes)
# CTC解码
predicted_text, confidence, char_confidences = lightcrnn_decoder.decode_with_confidence(outputs)
print(f"LightCRNN识别结果: {predicted_text}, 置信度: {confidence:.3f}")
# 将字符串转换为字符列表
char_list = list(predicted_text)
# 确保返回至少7个字符最多8个字符
if len(char_list) < 7:
# 如果识别结果少于7个字符用'0'补齐到7位
char_list.extend(['0'] * (7 - len(char_list)))
elif len(char_list) > 8:
# 如果识别结果多于8个字符截取前8个
char_list = char_list[:8]
# 如果是7位补齐到8位以保持接口一致性第8位用空字符或占位符
if len(char_list) == 7:
char_list.append('') # 添加空字符作为第8位占位符
return char_list
except Exception as e:
print(f"LightCRNN识别失败: {e}")
import traceback
traceback.print_exc()
return ['', '', '', '', '0', '0', '0', '0']
def create_lightweight_model(model_type='lightweight_crnn', img_height=32, num_classes=66, hidden_size=160):
"""创建增强版轻量化模型"""
if model_type == 'lightweight_crnn':
return LightweightCRNN(img_height, num_classes, hidden_size=hidden_size)
else:
raise ValueError(f"Unknown lightweight model type: {model_type}")
if __name__ == "__main__":
# 测试轻量化模型
print("测试LightCRNN模型...")
# 初始化模型
success = LPRNinitialize_model()
if success:
print("模型初始化成功")
# 创建测试输入
test_input = np.random.randint(0, 255, (32, 128, 3), dtype=np.uint8)
# 测试预测
result = LPRNmodel_predict(test_input)
print(f"测试预测结果: {result}")
else:
print("模型初始化失败")

1907
main.py Normal file

File diff suppressed because it is too large Load Diff

5
parking_config.json Normal file
View File

@@ -0,0 +1,5 @@
{
"free_parking_duration": 5,
"billing_cycle": 3,
"price_per_cycle": 5.0
}

25
requirements.txt Normal file
View File

@@ -0,0 +1,25 @@
# 车牌识别系统依赖包
# 深度学习和计算机视觉
ultralytics>=8.0.0
opencv-python>=4.5.0
numpy>=1.21.0
# PyQt5界面
PyQt5>=5.15.0
# 图像处理
Pillow>=8.0.0
#paddleocr
python -m pip install paddlepaddle-gpu==3.0.0 -i https://www.paddlepaddle.org.cn/packages/stable/cu118/
python -m pip install "paddleocr[all]"
# 可选如果需要GPU加速
# torch>=1.9.0
# torchvision>=0.10.0
# 可选:如果需要其他功能
# matplotlib>=3.3.0 # 用于调试和可视化
# scipy>=1.7.0 # 科学计算

8
yolopart/.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

View File

@@ -1,7 +0,0 @@
<?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>

View File

@@ -1,8 +0,0 @@
<?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>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@@ -1,12 +0,0 @@
<?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>

View File

@@ -1,177 +0,0 @@
# 车牌检测系统
基于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界面
- 预留车牌识别接口

323
yolopart/detector.py Normal file
View File

@@ -0,0 +1,323 @@
import cv2
import numpy as np
from ultralytics import YOLO
import os
from PIL import Image, ImageDraw, ImageFont
class LicensePlateYOLO:
"""
车牌YOLO检测器类
负责加载YOLO pose模型并进行车牌检测和角点提取
"""
def __init__(self, model_path=None):
"""
初始化YOLO检测器
参数:
model_path: 模型文件路径如果为None则使用默认路径
"""
self.model = None
self.model_path = model_path or self._get_default_model_path()
self.class_names = {0: '蓝牌', 1: '绿牌'}
self.load_model()
def _get_default_model_path(self):
"""获取默认模型路径"""
current_dir = os.path.dirname(__file__)
return os.path.join(current_dir, "yolo11s-pose42.pt")
def load_model(self):
"""
加载YOLO pose模型
返回:
bool: 加载是否成功
"""
try:
if os.path.exists(self.model_path):
self.model = YOLO(self.model_path)
print(f"YOLO模型加载成功: {self.model_path}")
return True
else:
print(f"模型文件不存在: {self.model_path}")
return False
except Exception as e:
print(f"YOLO模型加载失败: {e}")
return False
def detect_license_plates(self, image, conf_threshold=0.6):
"""
检测图像中的车牌
参数:
image: 输入图像 (numpy数组)
conf_threshold: 置信度阈值
返回:
list: 检测结果列表,每个元素包含:
- box: 边界框坐标 [x1, y1, x2, y2]
- keypoints: 四个角点坐标 [[x1,y1], [x2,y2], [x3,y3], [x4,y4]]
- confidence: 置信度
- class_id: 类别ID (0=蓝牌, 1=绿牌)
- class_name: 类别名称
"""
if self.model is None:
print("模型未加载")
return []
try:
# 进行推理
results = self.model(image, conf=conf_threshold, verbose=False)
detections = []
for result in results:
# 检查是否有检测结果
if result.boxes is None or result.keypoints is None:
continue
# 提取检测信息
boxes = result.boxes.xyxy.cpu().numpy() # 边界框
keypoints = result.keypoints.xy.cpu().numpy() # 关键点
confidences = result.boxes.conf.cpu().numpy() # 置信度
classes = result.boxes.cls.cpu().numpy() # 类别
# 处理每个检测结果
for i in range(len(boxes)):
# 检查关键点数量是否为4个
if len(keypoints[i]) == 4:
class_id = int(classes[i])
detection = {
'box': boxes[i],
'keypoints': keypoints[i],
'confidence': confidences[i],
'class_id': class_id,
'class_name': self.class_names.get(class_id, '未知')
}
detections.append(detection)
else:
# 关键点不足4个记录但标记为不完整
class_id = int(classes[i])
detection = {
'box': boxes[i],
'keypoints': keypoints[i] if len(keypoints[i]) > 0 else [],
'confidence': confidences[i],
'class_id': class_id,
'class_name': self.class_names.get(class_id, '未知'),
'incomplete': True # 标记为不完整
}
detections.append(detection)
return detections
except Exception as e:
print(f"检测过程中出错: {e}")
return []
def draw_detections(self, image, detections, plate_numbers=None):
"""
在图像上绘制检测结果
参数:
image: 输入图像
detections: 检测结果列表
plate_numbers: 车牌号列表与detections对应
返回:
numpy.ndarray: 绘制了检测结果的图像
"""
draw_image = image.copy()
# 转换为PIL图像以支持中文字符
pil_image = Image.fromarray(cv2.cvtColor(draw_image, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_image)
# 尝试加载中文字体
try:
# Windows系统常见的中文字体
font_path = "C:/Windows/Fonts/simhei.ttf" # 黑体
if not os.path.exists(font_path):
font_path = "C:/Windows/Fonts/msyh.ttc" # 微软雅黑
if not os.path.exists(font_path):
font_path = "C:/Windows/Fonts/simsun.ttc" # 宋体
font = ImageFont.truetype(font_path, 20)
except:
# 如果无法加载字体,使用默认字体
font = ImageFont.load_default()
for i, detection in enumerate(detections):
box = detection['box']
keypoints = detection['keypoints']
class_name = detection['class_name']
confidence = detection['confidence']
incomplete = detection.get('incomplete', False)
# 获取对应的车牌号
plate_number = ""
if plate_numbers and i < len(plate_numbers):
plate_number = plate_numbers[i]
# 绘制边界框
x1, y1, x2, y2 = map(int, box)
# 根据车牌类型选择颜色
if class_name == '绿牌':
box_color = (0, 255, 0) # 绿色
elif class_name == '蓝牌':
box_color = (0, 0, 255) # 蓝色
else:
box_color = (128, 128, 128) # 灰色
# 在PIL图像上绘制边界框
draw.rectangle([(x1, y1), (x2, y2)], outline=box_color, width=2)
# 构建标签文本
if plate_number:
label = f"{class_name} {plate_number} {confidence:.2f}"
else:
label = f"{class_name} {confidence:.2f}"
if incomplete:
label += " (不完整)"
# 计算文本大小
bbox = draw.textbbox((0, 0), label, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# 绘制文本背景
draw.rectangle([(x1, y1 - text_height - 10), (x1 + text_width, y1)],
fill=box_color)
# 绘制文本
draw.text((x1, y1 - text_height - 5), label, fill=(255, 255, 255), font=font)
# 转换回OpenCV格式
draw_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
# 绘制关键点和连线使用OpenCV
for i, detection in enumerate(detections):
box = detection['box']
keypoints = detection['keypoints']
incomplete = detection.get('incomplete', False)
x1, y1, x2, y2 = map(int, box)
# 根据车牌类型选择颜色
class_name = detection['class_name']
if class_name == '绿牌':
box_color = (0, 255, 0) # 绿色
elif class_name == '蓝牌':
box_color = (0, 0, 255) # 蓝色
else:
box_color = (128, 128, 128) # 灰色
# 绘制关键点和连线
if len(keypoints) >= 4 and not incomplete:
# 四个角点完整,用黄色连线
points = [(int(kp[0]), int(kp[1])) for kp in keypoints[:4]]
# 绘制关键点
for point in points:
cv2.circle(draw_image, point, 5, (0, 255, 255), -1)
# 连接关键点形成四边形(按顺序连接)
# 假设关键点顺序为: right_bottom, left_bottom, left_top, right_top
for j in range(4):
cv2.line(draw_image, points[j], points[(j+1)%4], (0, 255, 255), 2)
elif len(keypoints) > 0:
# 关键点不完整,用红色标记现有点
for kp in keypoints:
point = (int(kp[0]), int(kp[1]))
cv2.circle(draw_image, point, 5, (0, 0, 255), -1)
return draw_image
def correct_license_plate(self, image, keypoints, target_size=(240, 80)):
"""
使用四个角点对车牌进行透视变换矫正
参数:
image: 原始图像
keypoints: 四个角点坐标
target_size: 目标尺寸 (width, height)
返回:
numpy.ndarray: 矫正后的车牌图像如果失败返回None
"""
if len(keypoints) != 4:
return None
try:
# 将关键点转换为numpy数组
src_points = np.array(keypoints, dtype=np.float32)
# 定义目标矩形的四个角点
# 假设关键点顺序为: right_bottom, left_bottom, left_top, right_top
# 重新排序为标准顺序: left_top, right_top, right_bottom, left_bottom
width, height = target_size
dst_points = np.array([
[0, 0], # left_top
[width, 0], # right_top
[width, height], # right_bottom
[0, height] # left_bottom
], dtype=np.float32)
# 重新排序源点以匹配目标点
# 原顺序: right_bottom, left_bottom, left_top, right_top
# 目标顺序: left_top, right_top, right_bottom, left_bottom
reordered_src = np.array([
src_points[2], # left_top
src_points[3], # right_top
src_points[0], # right_bottom
src_points[1] # left_bottom
], dtype=np.float32)
# 计算透视变换矩阵
matrix = cv2.getPerspectiveTransform(reordered_src, dst_points)
# 应用透视变换
corrected = cv2.warpPerspective(image, matrix, target_size)
return corrected
except Exception as e:
print(f"车牌矫正失败: {e}")
return None
def get_model_info(self):
"""
获取模型信息
返回:
dict: 模型信息字典
"""
if self.model is None:
return {"status": "未加载", "path": self.model_path}
return {
"status": "已加载",
"path": self.model_path,
"model_type": "YOLO11 Pose",
"classes": self.class_names
}
def initialize_yolo_detector(model_path=None):
"""
初始化YOLO检测器的便捷函数
参数:
model_path: 模型文件路径
返回:
LicensePlateYOLO: 初始化后的检测器实例
"""
detector = LicensePlateYOLO(model_path)
return detector
if __name__ == "__main__":
# 测试代码
detector = initialize_yolo_detector()
print("检测器信息:", detector.get_model_info())

View File

@@ -1,34 +0,0 @@
#!/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()

View File

@@ -1 +0,0 @@
# 模型模块初始化文件

View File

@@ -1,490 +0,0 @@
#!/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

View File

@@ -1,368 +0,0 @@
#!/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

View File

@@ -1,17 +0,0 @@
# 车牌检测系统依赖包
# 核心依赖
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

View File

@@ -1 +0,0 @@
# UI模块初始化文件

View File

@@ -1,348 +0,0 @@
#!/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()

View File

@@ -1,59 +0,0 @@
#!/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点击'开始检测'开始")

View File

@@ -1 +0,0 @@
# 工具模块初始化文件

View File

@@ -1,280 +0,0 @@
#!/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()