Compare commits

3 Commits
main ... hi3861

Author SHA1 Message Date
16d6108292 修复4号命令不能重复执行的bug 2025-10-19 18:07:06 +08:00
30edbda7de hi3861软件 2025-10-18 16:05:39 +08:00
3d7a4dcb4d hi3861软件 2025-10-18 16:01:35 +08:00
32 changed files with 1904 additions and 1686 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="cnm" 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>

7
.idea/misc.xml generated
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="cnm" 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="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

20
BUILD.gn Normal file
View File

@@ -0,0 +1,20 @@
static_library("barrier_gate_client") {
sources = [
# 根据功能划分文件
"demo_entry_cmsis.c", # 入口和主线程 源文件
"udp_client_test.c", # UDP客户端测试 源文件
"oled_ssd1306.c", # oled 显示屏驱动文件
"wifi_connecter.c", # wifi
"robot_sg90.c", # sg90 舵机
"json_parser.c", # JSON解析器
"display_helper.c" # 显示辅助文件
]
include_dirs = [
"//utils/native/lite/include",
"//kernel/liteos_m/kal/cmsis",
"//base/iot_hardware/peripheral/interfaces/kits",
"//foundation/communication/wifi_lite/interfaces/wifiservice", # HAL接口中的WiFi接口
]
}

Binary file not shown.

View File

@@ -1,336 +0,0 @@
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']

View File

@@ -1,328 +0,0 @@
import torch
import torch.nn as nn
import cv2
import numpy as np
import os
import sys
from torch.autograd import Variable
from PIL import Image
# 添加父目录到路径,以便导入模型和数据加载器
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# LPRNet字符集定义与训练时保持一致
CHARS = ['', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K',
'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
'W', 'X', 'Y', 'Z', 'I', 'O', '-']
CHARS_DICT = {char: i for i, char in enumerate(CHARS)}
# 简化的LPRNet模型定义
class small_basic_block(nn.Module):
def __init__(self, ch_in, ch_out):
super(small_basic_block, self).__init__()
self.block = nn.Sequential(
nn.Conv2d(ch_in, ch_out // 4, kernel_size=1),
nn.ReLU(),
nn.Conv2d(ch_out // 4, ch_out // 4, kernel_size=(3, 1), padding=(1, 0)),
nn.ReLU(),
nn.Conv2d(ch_out // 4, ch_out // 4, kernel_size=(1, 3), padding=(0, 1)),
nn.ReLU(),
nn.Conv2d(ch_out // 4, ch_out, kernel_size=1),
)
def forward(self, x):
return self.block(x)
class LPRNet(nn.Module):
def __init__(self, lpr_max_len, phase, class_num, dropout_rate):
super(LPRNet, self).__init__()
self.phase = phase
self.lpr_max_len = lpr_max_len
self.class_num = class_num
self.backbone = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, stride=1), # 0
nn.BatchNorm2d(num_features=64),
nn.ReLU(), # 2
nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(1, 1, 1)),
small_basic_block(ch_in=64, ch_out=128), # *** 4 ***
nn.BatchNorm2d(num_features=128),
nn.ReLU(), # 6
nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(2, 1, 2)),
small_basic_block(ch_in=64, ch_out=256), # 8
nn.BatchNorm2d(num_features=256),
nn.ReLU(), # 10
small_basic_block(ch_in=256, ch_out=256), # *** 11 ***
nn.BatchNorm2d(num_features=256),
nn.ReLU(), # 13
nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(4, 1, 2)), # 14
nn.Dropout(dropout_rate),
nn.Conv2d(in_channels=64, out_channels=256, kernel_size=(1, 4), stride=1), # 16
nn.BatchNorm2d(num_features=256),
nn.ReLU(), # 18
nn.Dropout(dropout_rate),
nn.Conv2d(in_channels=256, out_channels=class_num, kernel_size=(13, 1), stride=1), # 20
nn.BatchNorm2d(num_features=class_num),
nn.ReLU(), # 22
)
self.container = nn.Sequential(
nn.Conv2d(in_channels=448+self.class_num, out_channels=self.class_num, kernel_size=(1,1), stride=(1,1)),
)
def forward(self, x):
keep_features = list()
for i, layer in enumerate(self.backbone.children()):
x = layer(x)
if i in [2, 6, 13, 22]: # [2, 4, 8, 11, 22]
keep_features.append(x)
global_context = list()
for i, f in enumerate(keep_features):
if i in [0, 1]:
f = nn.AvgPool2d(kernel_size=5, stride=5)(f)
if i in [2]:
f = nn.AvgPool2d(kernel_size=(4, 10), stride=(4, 2))(f)
f_pow = torch.pow(f, 2)
f_mean = torch.mean(f_pow)
f = torch.div(f, f_mean)
global_context.append(f)
x = torch.cat(global_context, 1)
x = self.container(x)
logits = torch.mean(x, dim=2)
return logits
class LPRNetInference:
def __init__(self, model_path=None, img_size=[94, 24], lpr_max_len=8, dropout_rate=0.5):
"""
初始化LPRNet推理类
Args:
model_path: 训练好的模型权重文件路径
img_size: 输入图像尺寸 [width, height]
lpr_max_len: 车牌最大长度
dropout_rate: dropout率
"""
self.img_size = img_size
self.lpr_max_len = lpr_max_len
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 设置默认模型路径
if model_path is None:
current_dir = os.path.dirname(os.path.abspath(__file__))
model_path = os.path.join(current_dir, 'LPRNet__iteration_74000.pth')
# 初始化模型
self.model = LPRNet(lpr_max_len=lpr_max_len, phase=False, class_num=len(CHARS), dropout_rate=dropout_rate)
# 加载模型权重
if model_path and os.path.exists(model_path):
print(f"Loading LPRNet model from {model_path}")
try:
self.model.load_state_dict(torch.load(model_path, map_location=self.device))
print("LPRNet模型权重加载成功")
except Exception as e:
print(f"Warning: 加载模型权重失败: {e}. 使用随机权重.")
else:
print(f"Warning: 模型文件不存在或未指定: {model_path}. 使用随机权重.")
self.model.to(self.device)
self.model.eval()
print(f"LPRNet模型加载完成设备: {self.device}")
print(f"模型参数数量: {sum(p.numel() for p in self.model.parameters()):,}")
def preprocess_image(self, image_array):
"""
预处理图像数组 - 使用与训练时相同的预处理方式
Args:
image_array: numpy数组格式的图像 (H, W, C)
Returns:
preprocessed_image: 预处理后的图像tensor
"""
if image_array is None:
raise ValueError("Input image is None")
# 确保图像是numpy数组
if not isinstance(image_array, np.ndarray):
raise ValueError("Input must be numpy array")
# 检查图像维度
if len(image_array.shape) != 3:
raise ValueError(f"Expected 3D image array, got {len(image_array.shape)}D")
height, width, channels = image_array.shape
if channels != 3:
raise ValueError(f"Expected 3 channels, got {channels}")
# 调整图像尺寸到模型要求的尺寸
if height != self.img_size[1] or width != self.img_size[0]:
image_array = cv2.resize(image_array, tuple(self.img_size))
# 使用与训练时相同的预处理方式
image_array = image_array.astype('float32')
image_array -= 127.5
image_array *= 0.0078125
image_array = np.transpose(image_array, (2, 0, 1)) # HWC -> CHW
# 转换为tensor并添加batch维度
image_tensor = torch.from_numpy(image_array).unsqueeze(0)
return image_tensor
def decode_prediction(self, logits):
"""
解码模型预测结果 - 使用正确的CTC贪婪解码
Args:
logits: 模型输出的logits [batch_size, num_classes, sequence_length]
Returns:
predicted_text: 预测的车牌号码
"""
# 转换为numpy进行处理
prebs = logits.cpu().detach().numpy()
preb = prebs[0, :, :] # 取第一个batch [num_classes, sequence_length]
# 贪婪解码:对每个时间步选择最大概率的字符
preb_label = []
for j in range(preb.shape[1]): # 遍历每个时间步
preb_label.append(np.argmax(preb[:, j], axis=0))
# CTC解码去除重复字符和空白字符
no_repeat_blank_label = []
pre_c = preb_label[0]
# 处理第一个字符
if pre_c != len(CHARS) - 1: # 不是空白字符
no_repeat_blank_label.append(pre_c)
# 处理后续字符
for c in preb_label:
if (pre_c == c) or (c == len(CHARS) - 1): # 重复字符或空白字符
if c == len(CHARS) - 1:
pre_c = c
continue
no_repeat_blank_label.append(c)
pre_c = c
# 转换为字符
decoded_chars = [CHARS[idx] for idx in no_repeat_blank_label]
return ''.join(decoded_chars)
def predict(self, image_array):
"""
预测单张图像的车牌号码
Args:
image_array: numpy数组格式的图像
Returns:
prediction: 预测的车牌号码
confidence: 预测置信度
"""
try:
# 预处理图像
image = self.preprocess_image(image_array)
if image is None:
return None, 0.0
image = image.to(self.device)
# 模型推理
with torch.no_grad():
logits = self.model(image)
# logits shape: [batch_size, class_num, sequence_length]
# 计算置信度使用softmax后的最大概率平均值
probs = torch.softmax(logits, dim=1)
max_probs = torch.max(probs, dim=1)[0]
confidence = torch.mean(max_probs).item()
# 解码预测结果
prediction = self.decode_prediction(logits)
return prediction, confidence
except Exception as e:
print(f"预测图像失败: {e}")
return None, 0.0
# 全局变量
lpr_model = None
def LPRNinitialize_model():
"""
初始化LPRNet模型
返回:
bool: 初始化是否成功
"""
global lpr_model
try:
# 模型权重文件路径
model_path = os.path.join(os.path.dirname(__file__), 'LPRNet__iteration_74000.pth')
# 创建推理对象
lpr_model = LPRNetInference(model_path)
print("LPRNet模型初始化完成")
return True
except Exception as e:
print(f"LPRNet模型初始化失败: {e}")
import traceback
traceback.print_exc()
return False
def LPRNmodel_predict(image_array):
"""
LPRNet车牌号识别接口函数
参数:
image_array: numpy数组格式的车牌图像已经过矫正处理
返回:
list: 包含最多8个字符的列表代表车牌号的每个字符
例如: ['', 'A', '1', '2', '3', '4', '5'] (蓝牌7位)
['', 'A', 'D', '1', '2', '3', '4', '5'] (绿牌8位)
"""
global lpr_model
if lpr_model is None:
print("LPRNet模型未初始化请先调用LPRNinitialize_model()")
return ['', '', '', '0', '0', '0', '0', '0']
try:
# 预测车牌号
predicted_text, confidence = lpr_model.predict(image_array)
if predicted_text is None:
print("LPRNet识别失败")
return ['', '', '', '', '0', '0', '0', '0']
print(f"LPRNet识别结果: {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"LPRNet识别失败: {e}")
import traceback
traceback.print_exc()
return ['', '', '', '', '0', '0', '0', '0']

View File

@@ -1,66 +0,0 @@
import numpy as np
from paddleocr import TextRecognition
import cv2
class OCRProcessor:
def __init__(self):
self.model = TextRecognition(model_name="PP-OCRv5_server_rec")
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
# 保留原有函数接口
_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('·', '')
# 转换为字符列表
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
View File

@@ -1,172 +0,0 @@
# 车牌识别系统
基于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接口
- 数据库集成
- 性能统计和分析

56
chinese_char_map.h Normal file
View File

@@ -0,0 +1,56 @@
#ifndef CHINESE_CHAR_MAP_H
#define CHINESE_CHAR_MAP_H
#include <stdint.h>
// 中文字符映射结构体
typedef struct {
const char* utf8_char; // UTF-8编码的中文字符
uint8_t font_index; // 在fonts3数组中的索引
} ChineseCharMap;
// 中文字符映射表 - 根据fonts3数组中的字符顺序
static const ChineseCharMap chinese_char_map[] = {
{"", 0}, // ID:0 - 北京
{"", 1}, // ID:1 - 上海
{"", 2}, // ID:2 - 天津
{"", 3}, // ID:3 - 重庆
{"", 4}, // ID:4 - 河北
{"", 5}, // ID:5 - 山西
{"", 6}, // ID:6 - 内蒙古
{"", 7}, // ID:7 - 辽宁
{"", 8}, // ID:8 - 吉林
{"", 9}, // ID:9 - 黑龙江
{"", 10}, // ID:10 - 江苏
{"", 11}, // ID:11 - 浙江
{"", 12}, // ID:12 - 安徽
{"", 13}, // ID:13 - 福建
{"", 14}, // ID:14 - 江西
{"", 15}, // ID:15 - 山东
{"", 16}, // ID:16 - 河南
{"", 17}, // ID:17 - 湖北
{"", 18}, // ID:18 - 湖南
{"", 19}, // ID:19 - 广东
{"", 20}, // ID:20 - 广西
{"", 21}, // ID:21 - 海南
{"", 22}, // ID:22 - 四川
{"", 23}, // ID:23 - 贵州
{"", 24}, // ID:24 - 云南
{"", 25}, // ID:25 - 西藏
{"", 26}, // ID:26 - 陕西
{"", 27}, // ID:27 - 甘肃
{"", 28}, // ID:28 - 青海
{"", 29}, // ID:29 - 宁夏
{"", 30}, // ID:30 - 新疆
{"", 31}, // ID:31 - 禁止
{"", 32}, // ID:32 - 通行
{"", 33} // ID:33 - 行驶
};
// 映射表大小
#define CHINESE_CHAR_MAP_SIZE (sizeof(chinese_char_map) / sizeof(ChineseCharMap))
// 函数声明
int FindChineseCharIndex(const char* utf8_char);
#endif // CHINESE_CHAR_MAP_H

525
demo_entry_cmsis.c Normal file
View File

@@ -0,0 +1,525 @@
#include <stdio.h> // 标准输入输出
#include <unistd.h> // POSIX标准接口
#include <string.h> // 字符串处理(操作字符数组)
#include "ohos_init.h" // 用于初始化服务(services)和功能(features)
#include "cmsis_os2.h" // CMSIS-RTOS API V2
#include "wifi_connecter.h" // easy wifi (station模式)
#include "oled_ssd1306.h" // OLED驱动接口
#include "json_parser.h"
#include "display_helper.h" // 显示辅助函数
#include "robot_sg90.h" // 舵机控制接口
#include "iot_gpio.h"
#include "hi_io.h"
#include "hi_time.h"
#include "iot_gpio.h"
#include "hi_adc.h"
#include "iot_errno.h"
#if 1
// 定义一个宏用于标识SSID。请根据实际情况修改
#define PARAM_HOTSPOT_SSID "tarikPura"
// 定义一个宏,用于标识密码。请根据实际情况修改
#define PARAM_HOTSPOT_PSK "66668888"
#elif
#define PARAM_HOTSPOT_SSID "DYJY"
// 定义一个宏,用于标识密码。请根据实际情况修改
#define PARAM_HOTSPOT_PSK "12345678"
#endif
// 定义一个宏,用于标识加密方式
#define PARAM_HOTSPOT_TYPE WIFI_SEC_TYPE_PSK
// 定义一个宏用于标识UDP服务器IP地址。请根据实际情况修改
#define PARAM_SERVER_ADDR "192.168.43.137"
#define GPIO5 5
#define ADC_TEST_LENGTH (20)
#define VLT_MIN (100)
#define KEY_INTERRUPT_PROTECT_TIME (30)
unsigned short g_adc_buf[ADC_TEST_LENGTH] = { 0 };
unsigned short g_gpio5_adc_buf[ADC_TEST_LENGTH] = { 0 };
unsigned int g_gpio5_tick = 0;
int control_flag = 0;
extern char response[128];
extern JsonCommand g_current_command; // 外部声明JSON命令变量
// 舵机控制函数声明
extern void servo_rotate_clockwise_90(void);
extern void servo_rotate_counter_clockwise_90(void);
extern void regress_middle(void);
uint8_t fonts3[] = {
/*-- ID:0,字符:"京",ASCII编码:BEA9,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x08,0x08,0x08,0xe8,0x28,0x28,0x29,0x2e,0x28,0x28,0x28,0xf8,0x28,0x0c,0x08,0x00,
0x00,0x00,0x40,0x23,0x1a,0x42,0x82,0x7e,0x02,0x0a,0x12,0x33,0x60,0x00,0x00,0x00,
/*-- ID:1,字符:"沪",ASCII编码:BBA6,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x10,0x22,0x64,0x0c,0x80,0x00,0xf8,0x88,0x89,0x8a,0x8e,0x88,0x88,0xfc,0x08,0x00,
0x04,0x04,0xfc,0x03,0x80,0x60,0x1f,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,
/*-- ID:2,字符:"津",ASCII编码:BDF2,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x20,0x42,0xc4,0x0c,0x10,0x54,0x54,0x54,0xff,0x54,0x54,0x54,0x7e,0x14,0x10,0x00,
0x04,0x04,0xfc,0x02,0x11,0x12,0x12,0x12,0xff,0x12,0x12,0x13,0x1a,0x10,0x00,0x00,
/*-- ID:3,字符:"渝",ASCII编码:D3E5,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x10,0x22,0x64,0x0c,0xa0,0xd0,0x48,0x54,0xd2,0x13,0x94,0x08,0xd0,0x30,0x10,0x00,
0x04,0x04,0xfe,0x01,0x00,0xff,0x12,0x92,0xff,0x00,0x5f,0x80,0x7f,0x00,0x00,0x00,
/*-- ID:4,字符:"冀",ASCII编码:BCBD,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x08,0x0a,0xea,0xaa,0xaa,0xaa,0xaf,0xe0,0xaf,0xaa,0xaa,0xaa,0xfa,0x28,0x0c,0x00,
0x20,0xa0,0xab,0x6a,0x2a,0x3e,0x2a,0x2b,0x2a,0x3e,0x2a,0x6a,0xab,0xa0,0x20,0x00,
/*-- ID:5,字符:"晋",ASCII编码:BDFA,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x40,0x42,0x4a,0x52,0x42,0x7e,0x42,0x42,0x42,0x7e,0x42,0xd2,0x4b,0x62,0x40,0x00,
0x00,0x00,0x00,0xff,0x49,0x49,0x49,0x49,0x49,0x49,0x49,0xff,0x01,0x00,0x00,0x00,
/*-- ID:6,字符:"蒙",ASCII编码:C3C9,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x20,0x9a,0x8a,0x8a,0xaa,0xaf,0xaa,0xaa,0xaa,0xaf,0xaa,0x8a,0x8b,0xaa,0x18,0x00,
0x00,0x50,0x52,0x2a,0x2a,0x15,0x4b,0x86,0x7c,0x04,0x0a,0x13,0x20,0x60,0x20,0x00,
/*-- ID:7,字符:"辽",ASCII编码:C1C9,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x00,0x21,0x22,0xe6,0x00,0x02,0x02,0x02,0x02,0xf2,0x12,0x0a,0x06,0x02,0x00,0x00,
0x00,0x40,0x20,0x1f,0x20,0x40,0x40,0x48,0x50,0x4f,0x40,0x40,0x40,0x60,0x20,0x00,
/*-- ID:8,字符:"吉",ASCII编码:BCAA,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x08,0x08,0x88,0x88,0x88,0x88,0x88,0xff,0x88,0x88,0x88,0xc8,0x88,0x0c,0x08,0x00,
0x00,0x00,0x00,0xfc,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0xfe,0x04,0x00,0x00,0x00,
/*-- ID:9,字符:"黑",ASCII编码:BADA,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x00,0x00,0xfe,0x8a,0x92,0xb2,0x82,0xfe,0x82,0xa2,0x9a,0x92,0xff,0x02,0x00,0x00,
0x08,0x8a,0x6a,0x0a,0x2a,0xca,0x0a,0x0f,0x2a,0xca,0x0a,0x2a,0x4a,0xca,0x08,0x00,
/*-- ID:10,字符:"苏",ASCII编码:CBD5,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x04,0x04,0x44,0x44,0x5f,0x44,0xf4,0x44,0x44,0x44,0x5f,0xe4,0x44,0x06,0x04,0x00,
0x00,0x88,0x46,0x20,0x10,0x0c,0x03,0x00,0x00,0x40,0x80,0x7f,0x02,0x04,0x0c,0x00,
/*-- ID:11,字符:"浙",ASCII编码:D5E3,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x10,0x22,0x64,0x0c,0x90,0x10,0xff,0x10,0x90,0xfc,0x44,0x44,0xc2,0x62,0x40,0x00,
0x04,0x04,0xfe,0x01,0x42,0x82,0x7f,0x41,0x20,0x1f,0x00,0x00,0xff,0x00,0x00,0x00,
/*-- ID:12,字符:"皖",ASCII编码:CDEE,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x00,0xf8,0x8c,0x8b,0x88,0xf8,0x10,0x0c,0x24,0x25,0x26,0x34,0x24,0x94,0x0c,0x00,
0x00,0x3f,0x10,0x10,0x10,0xbf,0x41,0x31,0x0f,0x01,0x01,0x3f,0x41,0x41,0x71,0x00,
/*-- ID:13,字符:"闽",ASCII编码:C3F6,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x00,0xfc,0x01,0x02,0xe6,0x20,0x22,0xfa,0x22,0x22,0xf2,0x22,0x02,0xff,0x02,0x00,
0x00,0xff,0x00,0x20,0x27,0x22,0x22,0x3f,0x12,0x12,0x1b,0x70,0x80,0x7f,0x00,0x00,
/*-- ID:14,字符:"赣",ASCII编码:B8D3,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x12,0xd6,0x5a,0x53,0x5a,0xd6,0x12,0x28,0x64,0x57,0xca,0x56,0x52,0x20,0x20,0x00,
0x10,0x17,0x15,0xfd,0x15,0x17,0x10,0x81,0xbd,0x45,0x35,0x45,0x7d,0x81,0x00,0x00,
/*-- ID:15,字符:"鲁",ASCII编码:C2B3,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x10,0x10,0xf8,0xac,0xaa,0xab,0xaa,0xfa,0xae,0xaa,0xaa,0xa8,0xfc,0x08,0x00,0x00,
0x02,0x02,0x02,0xfa,0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,0xfe,0x0a,0x02,0x02,0x00,
/*-- ID:16,字符:"豫",ASCII编码:D4A5,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x40,0x42,0x52,0xe2,0x5a,0xc6,0x50,0xf8,0x94,0xd3,0xba,0x96,0x92,0xf8,0x10,0x00,
0x00,0x40,0x80,0x7f,0x00,0x10,0x54,0x4a,0x25,0x92,0xfc,0x0c,0x12,0x61,0x20,0x00,
/*-- ID:17,字符:"鄂",ASCII编码:B6F5,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x00,0x5e,0x52,0x5e,0x40,0x5e,0x52,0x5e,0x00,0xfe,0x02,0x42,0xb2,0x0e,0x00,0x00,
0x01,0x01,0x0d,0x4b,0x89,0x89,0x7d,0x09,0x01,0xff,0x08,0x10,0x20,0x11,0x0e,0x00,
/*-- ID:18,字符:"湘",ASCII编码:CFE6,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x10,0x22,0x64,0x0c,0x10,0xd0,0xff,0x90,0x10,0xfc,0x44,0x44,0x44,0xfe,0x04,0x00,
0x04,0x04,0xfe,0x05,0x03,0x00,0xff,0x00,0x01,0xff,0x44,0x44,0x44,0xff,0x00,0x00,
/*-- ID:19,字符:"粤",ASCII编码:D4C1,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x00,0x00,0x00,0xfe,0x92,0xd6,0x93,0xfe,0x92,0xd6,0x92,0xff,0x02,0x00,0x00,0x00,
0x02,0x02,0x02,0x02,0x0a,0x0e,0x0a,0x0a,0x4a,0x8a,0x4a,0x3a,0x02,0x03,0x02,0x00,
/*-- ID:20,字符:"桂",ASCII编码:B9F0,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x10,0x10,0xd0,0xff,0x90,0x50,0x48,0x48,0x48,0xff,0x48,0x48,0x4c,0x68,0x40,0x00,
0x04,0x03,0x00,0xff,0x40,0x41,0x44,0x44,0x44,0x7f,0x44,0x44,0x46,0x64,0x40,0x00,
/*-- ID:21,字符:"琼",ASCII编码:C7ED,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x44,0x44,0xfc,0x46,0x44,0x08,0xe8,0x28,0x29,0x2a,0x28,0x28,0xf8,0x2c,0x08,0x00,
0x10,0x30,0x1f,0x08,0x08,0x20,0x13,0x5a,0x82,0x7e,0x02,0x0a,0x13,0x30,0x00,0x00,
/*-- ID:22,字符:"川",ASCII编码:B4A8,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x00,0x00,0x00,0xfe,0x00,0x00,0x00,0x00,0xfc,0x00,0x00,0x00,0x00,0xff,0x00,0x00,
0x00,0x40,0x20,0x1f,0x00,0x00,0x00,0x00,0x1f,0x00,0x00,0x00,0x00,0xff,0x00,0x00,
/*-- ID:23,字符:"贵",ASCII编码:B9F3,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x40,0x40,0x5c,0x54,0x54,0x54,0x54,0x7f,0x54,0x54,0x54,0xd4,0x5e,0x44,0x40,0x00,
0x00,0x00,0x80,0x9f,0x41,0x41,0x21,0x1d,0x21,0x21,0x41,0x5f,0x81,0x00,0x00,0x00,
/*-- ID:24,字符:"云",ASCII编码:D4C6,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x40,0x40,0x40,0x44,0x44,0x44,0xc4,0x44,0x44,0x44,0x46,0x44,0x40,0x60,0x40,0x00,
0x00,0x00,0x40,0x60,0x58,0x46,0x41,0x40,0x40,0x40,0x50,0x60,0xc0,0x00,0x00,0x00,
/*-- ID:25,字符:"藏",ASCII编码:B2D8,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x02,0xf2,0x82,0xf2,0x12,0xdf,0x52,0xd2,0x5f,0x12,0xfe,0x12,0x16,0x9b,0x12,0x00,
0x90,0x4e,0x22,0x1f,0x00,0x7f,0x25,0x3d,0xa7,0x40,0x2f,0x30,0x4c,0x83,0xe0,0x00,
/*-- ID:26,字符:"陕",ASCII编码:C9C2,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x00,0xfe,0x22,0x5a,0x86,0x08,0x28,0x48,0x08,0xff,0x08,0x48,0x2c,0x88,0x00,0x00,
0x00,0xff,0x04,0x08,0x87,0x41,0x21,0x11,0x0d,0x03,0x0d,0x11,0x61,0xc1,0x41,0x00,
/*-- ID:27,字符:"甘",ASCII编码:B8CA,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x10,0x10,0x10,0x10,0xff,0x10,0x10,0x10,0x10,0x10,0xff,0x10,0x10,0x18,0x10,0x00,
0x00,0x00,0x00,0x00,0xff,0x42,0x42,0x42,0x42,0x42,0xff,0x00,0x00,0x00,0x00,0x00,
/*-- ID:28,字符:"青",ASCII编码:C7E0,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x40,0x44,0x54,0x54,0x54,0x54,0x54,0x7f,0x54,0x54,0x54,0xd4,0x56,0x44,0x40,0x00,
0x00,0x00,0x00,0xff,0x15,0x15,0x15,0x15,0x15,0x55,0x95,0x7f,0x01,0x00,0x00,0x00,
/*-- ID:29,字符:"宁",ASCII编码:C4FE,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x50,0x4c,0x44,0x44,0x44,0x44,0x45,0xc6,0x44,0x44,0x44,0x44,0x44,0x54,0x4c,0x00,
0x00,0x00,0x00,0x00,0x00,0x40,0x80,0x7f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
/*-- ID:30,字符:"新",ASCII编码:D0C2,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x40,0x44,0x54,0x65,0xc6,0x64,0xd6,0x44,0x40,0xfc,0x44,0x42,0xc3,0x62,0x40,0x00,
0x20,0x11,0x49,0x81,0x7f,0x01,0x05,0x29,0x18,0x07,0x00,0x00,0xff,0x00,0x00,0x00,
/*-- ID:31,字符:"禁",ASCII编码:BDFB,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x40,0x24,0x14,0x0c,0x7f,0x14,0x24,0x20,0x14,0x0c,0x7f,0x0c,0x16,0x24,0x40,0x00,
0x04,0x04,0x45,0x25,0x15,0x45,0x85,0x7d,0x05,0x05,0x15,0x25,0x65,0x04,0x04,0x00,
/*-- ID:32,字符:"通",ASCII编码:CDA8,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x40,0x42,0x44,0xcc,0x00,0x00,0xf1,0x91,0x95,0xf9,0x95,0x93,0xf9,0x10,0x00,0x00,
0x00,0x40,0x20,0x1f,0x20,0x40,0xbf,0x84,0x84,0xbf,0x94,0xa4,0x9f,0xc0,0x40,0x00,
/*-- ID:33,字符:"行",ASCII编码:D0D0,对应字:宽x高=16x16,画布:宽W=16 高H=16,共32字节*/
0x00,0x10,0x88,0xc4,0x23,0x40,0x42,0x42,0x42,0x42,0x42,0xc2,0x43,0x62,0x40,0x00,
0x02,0x01,0x00,0xff,0x00,0x00,0x00,0x00,0x00,0x40,0x80,0x7f,0x00,0x00,0x00,0x00};
// 定义一个宏用于标识UDP服务器端口
#define PARAM_SERVER_PORT 8081
void switch_init(void)
{
IoTGpioInit(5);
hi_io_set_func(5, 0);
IoTGpioSetDir(5, IOT_GPIO_DIR_IN);
hi_io_set_pull(5, 1);
}
//按键中断响应函数
void gpio5_isr_func_mode(void)
{
printf("gpio5_isr_func_mode start\n");
unsigned int tick_interval = 0;
unsigned int current_gpio5_tick = 0;
current_gpio5_tick = hi_get_tick();
tick_interval = current_gpio5_tick - g_gpio5_tick;
if (tick_interval < KEY_INTERRUPT_PROTECT_TIME) {
return NULL;
}
g_gpio5_tick = current_gpio5_tick;
control_flag = !control_flag;
}
unsigned char get_gpio5_voltage(void *param)
{
int i;
unsigned short data;
unsigned int ret;
unsigned short vlt;
float voltage;
float vlt_max = 0;
float vlt_min = VLT_MIN;
hi_unref_param(param);
memset_s(g_gpio5_adc_buf, sizeof(g_gpio5_adc_buf), 0x0, sizeof(g_gpio5_adc_buf));
for (i = 0; i < ADC_TEST_LENGTH; i++) {
ret = hi_adc_read(HI_ADC_CHANNEL_2, &data, HI_ADC_EQU_MODEL_4, HI_ADC_CUR_BAIS_DEFAULT, 0xF0);
//ADC_Channal_2 自动识别模式 CNcomment:4次平均算法模式 CNend */
if (ret != IOT_SUCCESS) {
printf("ADC Read Fail\n");
return NULL;
}
g_gpio5_adc_buf[i] = data;
}
for (i = 0; i < ADC_TEST_LENGTH; i++) {
vlt = g_gpio5_adc_buf[i];
voltage = (float)vlt * 1.8 * 4 / 4096.0;
/* vlt * 1.8* 4 / 4096.0为将码字转换为电压 */
vlt_max = (voltage > vlt_max) ? voltage : vlt_max;
vlt_min = (voltage < vlt_min) ? voltage : vlt_min;
}
printf("vlt_max is %f\r\n", vlt_max);
if (vlt_max > 0.6 && vlt_max < 1.0) {
gpio5_isr_func_mode();
}
}
//按键中断
void interrupt_monitor(void)
{
unsigned int ret = 0;
/*gpio5 switch2 mode*/
g_gpio5_tick = hi_get_tick();
ret = IoTGpioRegisterIsrFunc(GPIO5, IOT_INT_TYPE_EDGE, IOT_GPIO_EDGE_FALL_LEVEL_LOW, get_gpio5_voltage, NULL);
if (ret == IOT_SUCCESS) {
printf(" register gpio5\r\n");
}
}
void OledShowChinese3(uint8_t x, uint8_t y, uint8_t idx)
{
// 控制循环
uint8_t t;
// 显示汉字的上半部分
OledSetPosition(x, y);
for (t = 0; t < 16; t++)
{
WriteData(fonts3[32 * idx + t]);
}
// 显示汉字的下半部分
OledSetPosition(x, y + 1);
for (t = 16; t < 32; t++)
{
WriteData(fonts3[32 * idx + t]);
}
}
static void controlTask(void *arg)
{
(void)arg;
int control_temp = control_flag;
int count = 0;
static int display_timer = 0; // 显示计时器
while (1)
{
if(control_flag != control_temp)
{
count = 0;
display_timer = 0;
OledClearScreen(); // 使用新的清屏函数
control_temp = control_flag;
// 处理新的JSON命令格式
if(control_flag == CMD_ROTATE_DISPLAY_CLEAR) // 命令1顺时针90°+显示字符串+10秒后逆时针90°+清屏
{
printf("Command 1: Rotate clockwise, display text, wait 10s, rotate back, clear\r\n");
// 顺时针旋转90度
servo_rotate_clockwise_90();
// 显示字符串
if (strlen(g_current_command.text) > 0) {
printf("Displaying text: %s\r\n", g_current_command.text);
DisplayMixedString(0, 0, g_current_command.text);
}
display_timer = 10; // 设置10秒计时器
}
else if(control_flag == CMD_ROTATE_CLOCKWISE) // 命令2顺时针90°
{
printf("Command 2: Rotate clockwise 90 degrees\r\n");
servo_rotate_clockwise_90();
}
else if(control_flag == CMD_ROTATE_COUNTER) // 命令3逆时针90°
{
printf("Command 3: Rotate counter-clockwise 90 degrees\r\n");
servo_rotate_counter_clockwise_90();
}
else if(control_flag == CMD_DISPLAY_ONLY) // 命令4只显示字符串舵机不动
{
printf("Command 4: Display text only, no servo movement\r\n");
// 只显示字符串,不控制舵机
if (strlen(g_current_command.text) > 0) {
printf("Displaying text: %s\r\n", g_current_command.text);
DisplayMixedString(0, 0, g_current_command.text);
}
display_timer = 2; // 设置2秒计时器
}
// 兼容旧命令的处理逻辑只有在非JSON命令时才执行
else if(control_flag == 0 && g_current_command.cmd == 0) // 兼容旧命令:关闭
{
regress_middle();
printf("barrier off\n");
}
else if(control_flag == 1 && g_current_command.cmd == 0) // 兼容旧命令开启只有非JSON命令时
{
servo_rotate_counter_clockwise_90();
printf("barrier on\n");
}
// 兼容旧的复杂字符串解析逻辑 - 只有在非JSON命令时才执行
if(control_flag == 2 && g_current_command.cmd == 0)
{
char prefix[20]; // 存储前部分字符串
int num; // 存储后部分数字
int index1 = 0;
// 使用 sscanf 解析字符串
int result = sscanf(response, "%[^i]index:%dflag:%d", prefix, &num,&index1);
if (result == 3) {
memset(response,0,sizeof(response));
OledShowChinese3(0,0,num);
OledShowString(18, 0, prefix, FONT8x16);
OledShowChinese3(0 , 2, index1);
OledShowChinese3(18 , 2, 33);
}
if (index1 == 32)
{
servo_rotate_counter_clockwise_90();
}
}
}
// 处理命令1的10秒计时器
if(control_flag == CMD_ROTATE_DISPLAY_CLEAR && display_timer > 0)
{
display_timer--;
if(display_timer == 0)
{
printf("10 seconds elapsed, rotating back and clearing screen\r\n");
// 逆时针旋转90度
servo_rotate_counter_clockwise_90();
// 清屏
OledClearScreen();
// 重置控制标志
control_flag = 0;
}
}
// 处理命令4的2秒计时器
if(control_flag == CMD_DISPLAY_ONLY && display_timer > 0)
{
display_timer--;
if(display_timer == 0)
{
printf("2 seconds elapsed, clearing screen for command 4\r\n");
// 清屏
OledClearScreen();
// 重置控制标志使其能够响应下一个命令4
control_flag = 0;
}
}
// 兼容旧的计时逻辑
if(control_flag == 2 && g_current_command.cmd == 0)
{
count++;
if(count > 10)
{
control_flag = 0;
count = 0;
}
}
sleep(1);
}
}
// 主线程函数
static void NetDemoTask(void *arg)
{
(void)arg;
int control_temp = 0;
// 定义热点配置
WifiDeviceConfig config = {0};
// 设置热点配置中的SSID
strcpy(config.ssid, PARAM_HOTSPOT_SSID);
// 设置热点配置中的密码
strcpy(config.preSharedKey, PARAM_HOTSPOT_PSK);
// 设置热点配置中的加密方式(Wi-Fi security types)
config.securityType = PARAM_HOTSPOT_TYPE;
// 显示启动信息
printf("=== Hi3861 智能闸机控制系统启动 ===\r\n");
printf("正在连接WiFi: %s\r\n", PARAM_HOTSPOT_SSID);
printf("服务器地址: %s:%d\r\n", PARAM_SERVER_ADDR, PARAM_SERVER_PORT);
// 在OLED上显示启动信息
OledFillScreen(0x00);
OledShowString(0, 0, "Starting...", FONT8x16);
OledShowString(0, 2, PARAM_HOTSPOT_SSID, FONT8x16);
// 等待100ms
osDelay(10);
// 连接到热点
printf("开始连接WiFi...\r\n");
int netId = ConnectToHotspot(&config);
// 检查是否成功连接到热点
if (netId < 0)
{
// 连接到热点失败
printf("WiFi连接失败错误代码: %d\r\n", netId);
OledFillScreen(0x00);
OledShowString(0, 0, "WiFi Failed!", FONT8x16);
OledShowString(0, 2, "Check Config", FONT8x16);
return;
}
// 连接到热点成功,显示连接成功信息
printf("WiFi连接成功网络ID: %d\r\n", netId);
OledFillScreen(0x00);
OledShowString(0, 0, "WiFi Connected", FONT8x16);
OledShowString(0, 2, "Starting UDP...", FONT8x16);
// 等待一段时间确保连接稳定
printf("等待网络稳定...\r\n");
sleep(3);
// 运行UDP客户端测试发送IP地址到服务器
printf("启动UDP客户端连接服务器 %s:%d\r\n", PARAM_SERVER_ADDR, PARAM_SERVER_PORT);
UdpClientTest(PARAM_SERVER_ADDR, PARAM_SERVER_PORT);
// 断开热点连接
printf("断开WiFi连接...\r\n");
DisconnectWithHotspot(netId);
printf("WiFi连接已断开\r\n");
}
// 入口函数
static void NetDemoEntry(void)
{
switch_init();
interrupt_monitor();
// 初始化OLED
OledInit();
// 全屏填充黑色
OledFillScreen(0x00);
// OLED显示APP标题
OledShowString(0, 0, "UdpClient Test", FONT8x16);
// 定义线程属性
osThreadAttr_t attr;
attr.name = "NetDemoTask";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 10240;
attr.priority = osPriorityNormal;
// 创建线程
if (osThreadNew(NetDemoTask, NULL, &attr) == NULL)
{
printf("[NetDemoEntry] Falied to create NetDemoTask!\n");
}
attr.name = "controlTask";
attr.stack_size = 2048;
if (osThreadNew(controlTask, NULL, &attr) == NULL)
{
printf("[control] Falied to create NetDemoTask!\n");
}
}
// 运行入口函数
SYS_RUN(NetDemoEntry);

64
display_helper.c Normal file
View File

@@ -0,0 +1,64 @@
#include "display_helper.h"
#include "chinese_char_map.h"
#include "json_parser.h"
#include "oled_ssd1306.h"
#include <string.h>
#include <stdio.h>
// 显示混合字符串(中文+英文数字)
void DisplayMixedString(uint8_t start_x, uint8_t start_y, const char* text) {
if (!text) {
return;
}
uint8_t x = start_x;
uint8_t y = start_y;
int i = 0;
int text_len = strlen(text);
printf("DisplayMixedString: Processing text '%s' (length: %d)\r\n", text, text_len);
while (i < text_len) {
// 检查是否为UTF-8中文字符通常以0xE开头的3字节序列
if ((unsigned char)text[i] >= 0xE0 && i + 2 < text_len) {
// 提取3字节的UTF-8中文字符
char chinese_char[4] = {0};
chinese_char[0] = text[i];
chinese_char[1] = text[i + 1];
chinese_char[2] = text[i + 2];
chinese_char[3] = '\0';
printf("Found Chinese char: %02X %02X %02X\r\n",
(unsigned char)chinese_char[0],
(unsigned char)chinese_char[1],
(unsigned char)chinese_char[2]);
// 查找字符在fonts3数组中的索引
int font_index = FindChineseCharIndex(chinese_char);
if (font_index >= 0) {
printf("Displaying Chinese char at index %d, position (%d, %d)\r\n", font_index, x, y);
OledShowChinese3(x, y, font_index);
x += 16; // 中文字符宽度为16像素
} else {
printf("Chinese char not found in font table, displaying as '?'\r\n");
// 如果找不到字符,显示问号
OledShowString(x, y, "?", FONT8x16);
x += 8; // 英文字符宽度为8像素
}
i += 3; // 跳过3字节的UTF-8字符
} else {
// 处理ASCII字符英文、数字、符号
char ascii_char[2] = {text[i], '\0'};
printf("Found ASCII char: '%c' (0x%02X)\r\n", text[i], (unsigned char)text[i]);
OledShowString(x, y, ascii_char, FONT8x16);
x += 8; // 英文字符宽度为8像素
i++;
}
// 检查是否需要换行假设屏幕宽度为128像素
if (x >= 120) {
x = start_x;
y += 2; // 每行高度为2个单位16像素
}
}
}

9
display_helper.h Normal file
View File

@@ -0,0 +1,9 @@
#ifndef DISPLAY_HELPER_H
#define DISPLAY_HELPER_H
#include <stdint.h>
// 显示混合字符串(中文+英文数字)
void DisplayMixedString(uint8_t start_x, uint8_t start_y, const char* text);
#endif // DISPLAY_HELPER_H

72
json_parser.c Normal file
View File

@@ -0,0 +1,72 @@
#include "json_parser.h"
#include "chinese_char_map.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 简单的JSON解析函数解析格式: {"cmd":1,"text":"hello"}
int ParseJsonCommand(const char* json_str, JsonCommand* command) {
if (!json_str || !command) {
return -1;
}
// 初始化结构体
command->cmd = 0;
memset(command->text, 0, MAX_TEXT_LENGTH);
// 查找cmd字段
const char* cmd_pos = strstr(json_str, "\"cmd\":");
if (cmd_pos) {
cmd_pos += 6; // 跳过"cmd":
// 跳过空格
while (*cmd_pos == ' ') cmd_pos++;
command->cmd = atoi(cmd_pos);
}
// 查找text字段
const char* text_pos = strstr(json_str, "\"text\":");
if (text_pos) {
text_pos += 7; // 跳过"text":
// 跳过空格和引号
while (*text_pos == ' ' || *text_pos == '\"') text_pos++;
// 复制文本直到遇到引号或字符串结束
int i = 0;
while (*text_pos && *text_pos != '\"' && i < MAX_TEXT_LENGTH - 1) {
command->text[i++] = *text_pos++;
}
command->text[i] = '\0';
}
return 0;
}
// 创建IP地址消息格式: {"type":"ip","address":"192.168.1.100"}
int CreateIpMessage(char* buffer, int buffer_size, const char* ip_address) {
if (!buffer || !ip_address || buffer_size < 50) {
return -1;
}
int len = snprintf(buffer, buffer_size,
"{\"type\":\"ip\",\"address\": \"%s\"}",
ip_address);
return (len > 0 && len < buffer_size) ? 0 : -1;
}
// 查找中文字符在fonts3数组中的索引
int FindChineseCharIndex(const char* utf8_char) {
if (!utf8_char) {
return -1;
}
// 遍历映射表查找匹配的字符
for (int i = 0; i < CHINESE_CHAR_MAP_SIZE; i++) {
if (strcmp(utf8_char, chinese_char_map[i].utf8_char) == 0) {
return chinese_char_map[i].font_index;
}
}
// 未找到匹配的字符
return -1;
}

29
json_parser.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef JSON_PARSER_H
#define JSON_PARSER_H
#include <stdint.h>
// 定义命令类型
#define CMD_ROTATE_DISPLAY_CLEAR 1 // 顺时针90°+显示字符串+10秒后逆时针90°+清屏
#define CMD_ROTATE_CLOCKWISE 2 // 顺时针90°
#define CMD_ROTATE_COUNTER 3 // 逆时针90°
#define CMD_DISPLAY_ONLY 4 // 只显示字符串,舵机不动
// 定义最大字符串长度
#define MAX_TEXT_LENGTH 64
// JSON命令结构体
typedef struct {
int cmd; // 命令类型
char text[MAX_TEXT_LENGTH]; // 显示文本
} JsonCommand;
// 函数声明
int ParseJsonCommand(const char* json_str, JsonCommand* command);
// 创建IP地址消息格式: {"type":"ip","address":"192.168.1.100"}
int CreateIpMessage(char* buffer, int buffer_size, const char* ip_address);
// 查找中文字符在fonts3数组中的索引
int FindChineseCharIndex(const char* utf8_char);
#endif // JSON_PARSER_H

436
main.py
View File

@@ -1,436 +0,0 @@
import sys
import cv2
import numpy as np
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QScrollArea, QFrame, QSizePolicy
)
from PyQt5.QtCore import QTimer, Qt, pyqtSignal, QThread
from PyQt5.QtGui import QImage, QPixmap, QFont, QPainter, QPen, QColor
import os
from yolopart.detector import LicensePlateYOLO
#选择使用哪个模块
from LPRNET_part.lpr_interface import LPRNmodel_predict
from LPRNET_part.lpr_interface import LPRNinitialize_model
#使用OCR
#from OCR_part.ocr_interface import LPRNmodel_predict
#from OCR_part.ocr_interface import LPRNinitialize_model
# 使用CRNN
#from CRNN_part.crnn_interface import LPRNmodel_predict
#from CRNN_part.crnn_interface import LPRNinitialize_model
class CameraThread(QThread):
"""摄像头线程类"""
frame_ready = pyqtSignal(np.ndarray)
def __init__(self):
super().__init__()
self.camera = None
self.running = False
def start_camera(self):
"""启动摄像头"""
self.camera = cv2.VideoCapture(0)
if self.camera.isOpened():
self.running = True
self.start()
return True
return False
def stop_camera(self):
"""停止摄像头"""
self.running = False
if self.camera:
self.camera.release()
self.quit()
self.wait()
def run(self):
"""线程运行函数"""
while self.running:
if self.camera and self.camera.isOpened():
ret, frame = self.camera.read()
if ret:
self.frame_ready.emit(frame)
self.msleep(30) # 约30fps
class LicensePlateWidget(QWidget):
"""单个车牌结果显示组件"""
def __init__(self, plate_id, class_name, corrected_image, plate_number):
super().__init__()
self.plate_id = plate_id
self.init_ui(class_name, corrected_image, plate_number)
def init_ui(self, class_name, corrected_image, plate_number):
layout = QHBoxLayout()
layout.setContentsMargins(10, 5, 10, 5)
# 车牌类型标签
type_label = QLabel(class_name)
type_label.setFixedWidth(60)
type_label.setAlignment(Qt.AlignCenter)
type_label.setStyleSheet(
"QLabel { "
"background-color: #4CAF50 if class_name == '绿牌' else #2196F3; "
"color: white; "
"border-radius: 5px; "
"padding: 5px; "
"font-weight: bold; "
"}"
)
if class_name == '绿牌':
type_label.setStyleSheet(
"QLabel { "
"background-color: #4CAF50; "
"color: white; "
"border-radius: 5px; "
"padding: 5px; "
"font-weight: bold; "
"}"
)
else:
type_label.setStyleSheet(
"QLabel { "
"background-color: #2196F3; "
"color: white; "
"border-radius: 5px; "
"padding: 5px; "
"font-weight: bold; "
"}"
)
# 矫正后的车牌图像
image_label = QLabel()
image_label.setFixedSize(120, 40)
image_label.setStyleSheet("border: 1px solid #ddd; background-color: white;")
if corrected_image is not None:
# 转换numpy数组为QPixmap
h, w = corrected_image.shape[:2]
if len(corrected_image.shape) == 3:
bytes_per_line = 3 * w
q_image = QImage(corrected_image.data, w, h, bytes_per_line, QImage.Format_RGB888).rgbSwapped()
else:
bytes_per_line = w
q_image = QImage(corrected_image.data, w, h, bytes_per_line, QImage.Format_Grayscale8)
pixmap = QPixmap.fromImage(q_image)
scaled_pixmap = pixmap.scaled(120, 40, Qt.KeepAspectRatio, Qt.SmoothTransformation)
image_label.setPixmap(scaled_pixmap)
else:
image_label.setText("车牌未完全\n进入摄像头")
image_label.setAlignment(Qt.AlignCenter)
image_label.setStyleSheet("border: 1px solid #ddd; background-color: #f5f5f5; color: #666;")
# 车牌号标签
number_label = QLabel(plate_number)
number_label.setFixedWidth(150)
number_label.setAlignment(Qt.AlignCenter)
number_label.setStyleSheet(
"QLabel { "
"border: 1px solid #ddd; "
"background-color: white; "
"padding: 8px; "
"font-family: 'Courier New'; "
"font-size: 14px; "
"font-weight: bold; "
"}"
)
layout.addWidget(type_label)
layout.addWidget(image_label)
layout.addWidget(number_label)
layout.addStretch()
self.setLayout(layout)
self.setStyleSheet(
"QWidget { "
"background-color: white; "
"border: 1px solid #e0e0e0; "
"border-radius: 8px; "
"margin: 2px; "
"}"
)
class MainWindow(QMainWindow):
"""主窗口类"""
def __init__(self):
super().__init__()
self.detector = None
self.camera_thread = None
self.current_frame = None
self.detections = []
self.init_ui()
self.init_detector()
self.init_camera()
# 初始化OCR/CRNN模型函数名改成一样的了所以不要修改这里了想用哪个模块直接导入
LPRNinitialize_model()
def init_ui(self):
"""初始化用户界面"""
self.setWindowTitle("车牌识别系统")
self.setGeometry(100, 100, 1200, 800)
# 创建中央widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 创建主布局
main_layout = QHBoxLayout(central_widget)
# 左侧摄像头显示区域
left_frame = QFrame()
left_frame.setFrameStyle(QFrame.StyledPanel)
left_frame.setStyleSheet("QFrame { background-color: #f0f0f0; border: 2px solid #ddd; }")
left_layout = QVBoxLayout(left_frame)
# 摄像头显示标签
self.camera_label = QLabel()
self.camera_label.setMinimumSize(640, 480)
self.camera_label.setStyleSheet("QLabel { background-color: black; border: 1px solid #ccc; }")
self.camera_label.setAlignment(Qt.AlignCenter)
self.camera_label.setText("摄像头未启动")
self.camera_label.setScaledContents(True)
# 控制按钮
button_layout = QHBoxLayout()
self.start_button = QPushButton("启动摄像头")
self.stop_button = QPushButton("停止摄像头")
self.start_button.clicked.connect(self.start_camera)
self.stop_button.clicked.connect(self.stop_camera)
self.stop_button.setEnabled(False)
button_layout.addWidget(self.start_button)
button_layout.addWidget(self.stop_button)
button_layout.addStretch()
left_layout.addWidget(self.camera_label)
left_layout.addLayout(button_layout)
# 右侧结果显示区域
right_frame = QFrame()
right_frame.setFrameStyle(QFrame.StyledPanel)
right_frame.setFixedWidth(400)
right_frame.setStyleSheet("QFrame { background-color: #fafafa; border: 2px solid #ddd; }")
right_layout = QVBoxLayout(right_frame)
# 标题
title_label = QLabel("检测结果")
title_label.setAlignment(Qt.AlignCenter)
title_label.setFont(QFont("Arial", 16, QFont.Bold))
title_label.setStyleSheet("QLabel { color: #333; padding: 10px; }")
# 车牌数量显示
self.count_label = QLabel("识别到的车牌数量: 0")
self.count_label.setAlignment(Qt.AlignCenter)
self.count_label.setFont(QFont("Arial", 12))
self.count_label.setStyleSheet(
"QLabel { "
"background-color: #e3f2fd; "
"border: 1px solid #2196f3; "
"border-radius: 5px; "
"padding: 8px; "
"color: #1976d2; "
"font-weight: bold; "
"}"
)
# 滚动区域用于显示车牌结果
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setStyleSheet("QScrollArea { border: none; background-color: transparent; }")
self.results_widget = QWidget()
self.results_layout = QVBoxLayout(self.results_widget)
self.results_layout.setAlignment(Qt.AlignTop)
scroll_area.setWidget(self.results_widget)
right_layout.addWidget(title_label)
right_layout.addWidget(self.count_label)
right_layout.addWidget(scroll_area)
# 添加到主布局
main_layout.addWidget(left_frame, 2)
main_layout.addWidget(right_frame, 1)
# 设置样式
self.setStyleSheet("""
QMainWindow {
background-color: #f5f5f5;
}
QPushButton {
background-color: #2196F3;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #1976D2;
}
QPushButton:pressed {
background-color: #0D47A1;
}
QPushButton:disabled {
background-color: #cccccc;
color: #666666;
}
""")
def init_detector(self):
"""初始化检测器"""
model_path = os.path.join(os.path.dirname(__file__), "yolopart", "yolo11s-pose42.pt")
self.detector = LicensePlateYOLO(model_path)
def init_camera(self):
"""初始化摄像头线程"""
self.camera_thread = CameraThread()
self.camera_thread.frame_ready.connect(self.process_frame)
def start_camera(self):
"""启动摄像头"""
if self.camera_thread.start_camera():
self.start_button.setEnabled(False)
self.stop_button.setEnabled(True)
self.camera_label.setText("摄像头启动中...")
else:
self.camera_label.setText("摄像头启动失败")
def stop_camera(self):
"""停止摄像头"""
self.camera_thread.stop_camera()
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
self.camera_label.setText("摄像头已停止")
self.camera_label.clear()
def process_frame(self, frame):
"""处理摄像头帧"""
self.current_frame = frame.copy()
# 进行车牌检测
self.detections = self.detector.detect_license_plates(frame)
# 在图像上绘制检测结果
display_frame = self.draw_detections(frame.copy())
# 转换为Qt格式并显示
self.display_frame(display_frame)
# 更新右侧结果显示
self.update_results_display()
def draw_detections(self, frame):
"""在图像上绘制检测结果"""
return self.detector.draw_detections(frame, self.detections)
def display_frame(self, frame):
"""显示帧到界面"""
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_frame.shape
bytes_per_line = ch * w
qt_image = QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(qt_image)
scaled_pixmap = pixmap.scaled(self.camera_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.camera_label.setPixmap(scaled_pixmap)
def update_results_display(self):
"""更新右侧结果显示"""
# 更新车牌数量
count = len(self.detections)
self.count_label.setText(f"识别到的车牌数量: {count}")
# 清除之前的结果
for i in reversed(range(self.results_layout.count())):
child = self.results_layout.itemAt(i).widget()
if child:
child.setParent(None)
# 添加新的结果
for i, detection in enumerate(self.detections):
# 矫正车牌图像
corrected_image = self.correct_license_plate(detection)
# 获取车牌号,传入车牌类型信息
plate_number = self.recognize_plate_number(corrected_image, detection['class_name'])
# 创建车牌显示组件
plate_widget = LicensePlateWidget(
i + 1,
detection['class_name'],
corrected_image,
plate_number
)
self.results_layout.addWidget(plate_widget)
def correct_license_plate(self, detection):
"""矫正车牌图像"""
if self.current_frame is None:
return None
# 检查是否为不完整检测
if detection.get('incomplete', False):
return None
# 使用检测器的矫正方法
return self.detector.correct_license_plate(
self.current_frame,
detection['keypoints']
)
def recognize_plate_number(self, corrected_image, class_name):
"""识别车牌号"""
if corrected_image is None:
return "识别失败"
try:
# 预测函数(来自模块)
# 函数名改成一样的了,所以不要修改这里了,想用哪个模块直接导入
result = LPRNmodel_predict(corrected_image)
# 将字符列表转换为字符串支持8位车牌号
if isinstance(result, list) and len(result) >= 7:
# 根据车牌类型决定显示位数
if class_name == '绿牌' and len(result) >= 8:
# 绿牌显示8位过滤掉空字符占位符
plate_chars = [char for char in result[:8] if char != '']
# 如果过滤后确实有8位显示8位否则显示7位
if len(plate_chars) == 8:
return ''.join(plate_chars)
else:
return ''.join(plate_chars[:7])
else:
# 蓝牌或其他类型显示前7位过滤掉空字符
plate_chars = [char for char in result[:7] if char != '']
return ''.join(plate_chars)
else:
return "识别失败"
except Exception as e:
print(f"车牌号识别失败: {e}")
return "识别失败"
def closeEvent(self, event):
"""窗口关闭事件"""
if self.camera_thread:
self.camera_thread.stop_camera()
event.accept()
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

211
oled_fonts.h Normal file
View File

@@ -0,0 +1,211 @@
// 字库头文件
// 定义条件编译宏,防止头文件的重复包含和编译
#ifndef OLOED_FONTS_H
#define OLOED_FONTS_H
/************************************6*8的点阵************************************/
// 取模方式纵向8点下高位
// 采用N*6的二维数组
// 第一维表示字符
// 每个字符对应第二维的6个数组元素每个数组元素1字节表示1列像素一共6列8行
static unsigned char F6x8[][6] =
{
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 空格
{ 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00 }, // !
{ 0x00, 0x00, 0x07, 0x00, 0x07, 0x00 }, // "
{ 0x00, 0x14, 0x7f, 0x14, 0x7f, 0x14 }, // #
{ 0x00, 0x24, 0x2a, 0x7f, 0x2a, 0x12 }, // $
{ 0x00, 0x62, 0x64, 0x08, 0x13, 0x23 }, // %
{ 0x00, 0x36, 0x49, 0x55, 0x22, 0x50 }, // &
{ 0x00, 0x00, 0x05, 0x03, 0x00, 0x00 }, // '
{ 0x00, 0x00, 0x1c, 0x22, 0x41, 0x00 }, // (
{ 0x00, 0x00, 0x41, 0x22, 0x1c, 0x00 }, // )
{ 0x00, 0x14, 0x08, 0x3E, 0x08, 0x14 }, // *
{ 0x00, 0x08, 0x08, 0x3E, 0x08, 0x08 }, // +
{ 0x00, 0x00, 0x00, 0xA0, 0x60, 0x00 }, // ,
{ 0x00, 0x08, 0x08, 0x08, 0x08, 0x08 }, // -
{ 0x00, 0x00, 0x60, 0x60, 0x00, 0x00 }, // .
{ 0x00, 0x20, 0x10, 0x08, 0x04, 0x02 }, // /
{ 0x00, 0x3E, 0x51, 0x49, 0x45, 0x3E }, // 0
{ 0x00, 0x00, 0x42, 0x7F, 0x40, 0x00 }, // 1
{ 0x00, 0x42, 0x61, 0x51, 0x49, 0x46 }, // 2
{ 0x00, 0x21, 0x41, 0x45, 0x4B, 0x31 }, // 3
{ 0x00, 0x18, 0x14, 0x12, 0x7F, 0x10 }, // 4
{ 0x00, 0x27, 0x45, 0x45, 0x45, 0x39 }, // 5
{ 0x00, 0x3C, 0x4A, 0x49, 0x49, 0x30 }, // 6
{ 0x00, 0x01, 0x71, 0x09, 0x05, 0x03 }, // 7
{ 0x00, 0x36, 0x49, 0x49, 0x49, 0x36 }, // 8
{ 0x00, 0x06, 0x49, 0x49, 0x29, 0x1E }, // 9
{ 0x00, 0x00, 0x36, 0x36, 0x00, 0x00 }, // :
{ 0x00, 0x00, 0x56, 0x36, 0x00, 0x00 }, // ;
{ 0x00, 0x08, 0x14, 0x22, 0x41, 0x00 }, // <
{ 0x00, 0x14, 0x14, 0x14, 0x14, 0x14 }, // =
{ 0x00, 0x00, 0x41, 0x22, 0x14, 0x08 }, // >
{ 0x00, 0x02, 0x01, 0x51, 0x09, 0x06 }, // ?
{ 0x00, 0x32, 0x49, 0x59, 0x51, 0x3E }, // @
{ 0x00, 0x7C, 0x12, 0x11, 0x12, 0x7C }, // A
{ 0x00, 0x7F, 0x49, 0x49, 0x49, 0x36 }, // B
{ 0x00, 0x3E, 0x41, 0x41, 0x41, 0x22 }, // C
{ 0x00, 0x7F, 0x41, 0x41, 0x22, 0x1C }, // D
{ 0x00, 0x7F, 0x49, 0x49, 0x49, 0x41 }, // E
{ 0x00, 0x7F, 0x09, 0x09, 0x09, 0x01 }, // F
{ 0x00, 0x3E, 0x41, 0x49, 0x49, 0x7A }, // G
{ 0x00, 0x7F, 0x08, 0x08, 0x08, 0x7F }, // H
{ 0x00, 0x00, 0x41, 0x7F, 0x41, 0x00 }, // I
{ 0x00, 0x20, 0x40, 0x41, 0x3F, 0x01 }, // J
{ 0x00, 0x7F, 0x08, 0x14, 0x22, 0x41 }, // K
{ 0x00, 0x7F, 0x40, 0x40, 0x40, 0x40 }, // L
{ 0x00, 0x7F, 0x02, 0x0C, 0x02, 0x7F }, // M
{ 0x00, 0x7F, 0x04, 0x08, 0x10, 0x7F }, // N
{ 0x00, 0x3E, 0x41, 0x41, 0x41, 0x3E }, // O
{ 0x00, 0x7F, 0x09, 0x09, 0x09, 0x06 }, // P
{ 0x00, 0x3E, 0x41, 0x51, 0x21, 0x5E }, // Q
{ 0x00, 0x7F, 0x09, 0x19, 0x29, 0x46 }, // R
{ 0x00, 0x46, 0x49, 0x49, 0x49, 0x31 }, // S
{ 0x00, 0x01, 0x01, 0x7F, 0x01, 0x01 }, // T
{ 0x00, 0x3F, 0x40, 0x40, 0x40, 0x3F }, // U
{ 0x00, 0x1F, 0x20, 0x40, 0x20, 0x1F }, // V
{ 0x00, 0x3F, 0x40, 0x38, 0x40, 0x3F }, // W
{ 0x00, 0x63, 0x14, 0x08, 0x14, 0x63 }, // X
{ 0x00, 0x07, 0x08, 0x70, 0x08, 0x07 }, // Y
{ 0x00, 0x61, 0x51, 0x49, 0x45, 0x43 }, // Z
{ 0x00, 0x00, 0x7F, 0x41, 0x41, 0x00 }, // [
{ 0x00, 0x55, 0x2A, 0x55, 0x2A, 0x55 }, /* \ */
{ 0x00, 0x00, 0x41, 0x41, 0x7F, 0x00 }, // ]
{ 0x00, 0x04, 0x02, 0x01, 0x02, 0x04 }, // ^
{ 0x00, 0x40, 0x40, 0x40, 0x40, 0x40 }, // _
{ 0x00, 0x00, 0x01, 0x02, 0x04, 0x00 }, // '
{ 0x00, 0x20, 0x54, 0x54, 0x54, 0x78 }, // a
{ 0x00, 0x7F, 0x48, 0x44, 0x44, 0x38 }, // b
{ 0x00, 0x38, 0x44, 0x44, 0x44, 0x20 }, // c
{ 0x00, 0x38, 0x44, 0x44, 0x48, 0x7F }, // d
{ 0x00, 0x38, 0x54, 0x54, 0x54, 0x18 }, // e
{ 0x00, 0x08, 0x7E, 0x09, 0x01, 0x02 }, // f
{ 0x00, 0x18, 0xA4, 0xA4, 0xA4, 0x7C }, // g
{ 0x00, 0x7F, 0x08, 0x04, 0x04, 0x78 }, // h
{ 0x00, 0x00, 0x44, 0x7D, 0x40, 0x00 }, // i
{ 0x00, 0x40, 0x80, 0x84, 0x7D, 0x00 }, // j
{ 0x00, 0x7F, 0x10, 0x28, 0x44, 0x00 }, // k
{ 0x00, 0x00, 0x41, 0x7F, 0x40, 0x00 }, // l
{ 0x00, 0x7C, 0x04, 0x18, 0x04, 0x78 }, // m
{ 0x00, 0x7C, 0x08, 0x04, 0x04, 0x78 }, // n
{ 0x00, 0x38, 0x44, 0x44, 0x44, 0x38 }, // o
{ 0x00, 0xFC, 0x24, 0x24, 0x24, 0x18 }, // p
{ 0x00, 0x18, 0x24, 0x24, 0x18, 0xFC }, // q
{ 0x00, 0x7C, 0x08, 0x04, 0x04, 0x08 }, // r
{ 0x00, 0x48, 0x54, 0x54, 0x54, 0x20 }, // s
{ 0x00, 0x04, 0x3F, 0x44, 0x40, 0x20 }, // t
{ 0x00, 0x3C, 0x40, 0x40, 0x20, 0x7C }, // u
{ 0x00, 0x1C, 0x20, 0x40, 0x20, 0x1C }, // v
{ 0x00, 0x3C, 0x40, 0x30, 0x40, 0x3C }, // w
{ 0x00, 0x44, 0x28, 0x10, 0x28, 0x44 }, // x
{ 0x00, 0x1C, 0xA0, 0xA0, 0xA0, 0x7C }, // y
{ 0x00, 0x44, 0x64, 0x54, 0x4C, 0x44 }, // z
{ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14 }, // horiz lines
};
/****************************************8*16的点阵************************************/
// 取模方式纵向8点下高位
// 采用一维数组每个字符对应16个数组元素
// 每16个数组元素的前8个表示字符的上半部分8*8点阵后8个表示字符的下半部分8*8点阵一共8列16行
static const unsigned char F8X16[]=
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//空格 0
0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x33,0x30,0x00,0x00,0x00,//! 1
0x00,0x10,0x0C,0x06,0x10,0x0C,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//" 2
0x40,0xC0,0x78,0x40,0xC0,0x78,0x40,0x00,0x04,0x3F,0x04,0x04,0x3F,0x04,0x04,0x00,//# 3
0x00,0x70,0x88,0xFC,0x08,0x30,0x00,0x00,0x00,0x18,0x20,0xFF,0x21,0x1E,0x00,0x00,//$ 4
0xF0,0x08,0xF0,0x00,0xE0,0x18,0x00,0x00,0x00,0x21,0x1C,0x03,0x1E,0x21,0x1E,0x00,//% 5
0x00,0xF0,0x08,0x88,0x70,0x00,0x00,0x00,0x1E,0x21,0x23,0x24,0x19,0x27,0x21,0x10,//& 6
0x10,0x16,0x0E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//' 7
0x00,0x00,0x00,0xE0,0x18,0x04,0x02,0x00,0x00,0x00,0x00,0x07,0x18,0x20,0x40,0x00,//( 8
0x00,0x02,0x04,0x18,0xE0,0x00,0x00,0x00,0x00,0x40,0x20,0x18,0x07,0x00,0x00,0x00,//) 9
0x40,0x40,0x80,0xF0,0x80,0x40,0x40,0x00,0x02,0x02,0x01,0x0F,0x01,0x02,0x02,0x00,//* 10
0x00,0x00,0x00,0xF0,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x1F,0x01,0x01,0x01,0x00,//+ 11
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0xB0,0x70,0x00,0x00,0x00,0x00,0x00,//, 12
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,//- 13
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00,0x00,0x00,//. 14
0x00,0x00,0x00,0x00,0x80,0x60,0x18,0x04,0x00,0x60,0x18,0x06,0x01,0x00,0x00,0x00,/// 15
0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00,//0 16
0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,//1 17
0x00,0x70,0x08,0x08,0x08,0x88,0x70,0x00,0x00,0x30,0x28,0x24,0x22,0x21,0x30,0x00,//2 18
0x00,0x30,0x08,0x88,0x88,0x48,0x30,0x00,0x00,0x18,0x20,0x20,0x20,0x11,0x0E,0x00,//3 19
0x00,0x00,0xC0,0x20,0x10,0xF8,0x00,0x00,0x00,0x07,0x04,0x24,0x24,0x3F,0x24,0x00,//4 20
0x00,0xF8,0x08,0x88,0x88,0x08,0x08,0x00,0x00,0x19,0x21,0x20,0x20,0x11,0x0E,0x00,//5 21
0x00,0xE0,0x10,0x88,0x88,0x18,0x00,0x00,0x00,0x0F,0x11,0x20,0x20,0x11,0x0E,0x00,//6 22
0x00,0x38,0x08,0x08,0xC8,0x38,0x08,0x00,0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x00,//7 23
0x00,0x70,0x88,0x08,0x08,0x88,0x70,0x00,0x00,0x1C,0x22,0x21,0x21,0x22,0x1C,0x00,//8 24
0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x00,0x31,0x22,0x22,0x11,0x0F,0x00,//9 25
0x00,0x00,0x00,0xC0,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00,//: 26
0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x60,0x00,0x00,0x00,0x00,//; 27
0x00,0x00,0x80,0x40,0x20,0x10,0x08,0x00,0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x00,//< 28
0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x00,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00,//= 29
0x00,0x08,0x10,0x20,0x40,0x80,0x00,0x00,0x00,0x20,0x10,0x08,0x04,0x02,0x01,0x00,//> 30
0x00,0x70,0x48,0x08,0x08,0x08,0xF0,0x00,0x00,0x00,0x00,0x30,0x36,0x01,0x00,0x00,//? 31
0xC0,0x30,0xC8,0x28,0xE8,0x10,0xE0,0x00,0x07,0x18,0x27,0x24,0x23,0x14,0x0B,0x00,//@ 32
0x00,0x00,0xC0,0x38,0xE0,0x00,0x00,0x00,0x20,0x3C,0x23,0x02,0x02,0x27,0x38,0x20,//A 33
0x08,0xF8,0x88,0x88,0x88,0x70,0x00,0x00,0x20,0x3F,0x20,0x20,0x20,0x11,0x0E,0x00,//B 34
0xC0,0x30,0x08,0x08,0x08,0x08,0x38,0x00,0x07,0x18,0x20,0x20,0x20,0x10,0x08,0x00,//C 35
0x08,0xF8,0x08,0x08,0x08,0x10,0xE0,0x00,0x20,0x3F,0x20,0x20,0x20,0x10,0x0F,0x00,//D 36
0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,0x20,0x3F,0x20,0x20,0x23,0x20,0x18,0x00,//E 37
0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,0x20,0x3F,0x20,0x00,0x03,0x00,0x00,0x00,//F 38
0xC0,0x30,0x08,0x08,0x08,0x38,0x00,0x00,0x07,0x18,0x20,0x20,0x22,0x1E,0x02,0x00,//G 39
0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,0x20,0x3F,0x21,0x01,0x01,0x21,0x3F,0x20,//H 40
0x00,0x08,0x08,0xF8,0x08,0x08,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,//I 41
0x00,0x00,0x08,0x08,0xF8,0x08,0x08,0x00,0xC0,0x80,0x80,0x80,0x7F,0x00,0x00,0x00,//J 42
0x08,0xF8,0x88,0xC0,0x28,0x18,0x08,0x00,0x20,0x3F,0x20,0x01,0x26,0x38,0x20,0x00,//K 43
0x08,0xF8,0x08,0x00,0x00,0x00,0x00,0x00,0x20,0x3F,0x20,0x20,0x20,0x20,0x30,0x00,//L 44
0x08,0xF8,0xF8,0x00,0xF8,0xF8,0x08,0x00,0x20,0x3F,0x00,0x3F,0x00,0x3F,0x20,0x00,//M 45
0x08,0xF8,0x30,0xC0,0x00,0x08,0xF8,0x08,0x20,0x3F,0x20,0x00,0x07,0x18,0x3F,0x00,//N 46
0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,0x0F,0x10,0x20,0x20,0x20,0x10,0x0F,0x00,//O 47
0x08,0xF8,0x08,0x08,0x08,0x08,0xF0,0x00,0x20,0x3F,0x21,0x01,0x01,0x01,0x00,0x00,//P 48
0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,0x0F,0x18,0x24,0x24,0x38,0x50,0x4F,0x00,//Q 49
0x08,0xF8,0x88,0x88,0x88,0x88,0x70,0x00,0x20,0x3F,0x20,0x00,0x03,0x0C,0x30,0x20,//R 50
0x00,0x70,0x88,0x08,0x08,0x08,0x38,0x00,0x00,0x38,0x20,0x21,0x21,0x22,0x1C,0x00,//S 51
0x18,0x08,0x08,0xF8,0x08,0x08,0x18,0x00,0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00,//T 52
0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00,//U 53
0x08,0x78,0x88,0x00,0x00,0xC8,0x38,0x08,0x00,0x00,0x07,0x38,0x0E,0x01,0x00,0x00,//V 54
0xF8,0x08,0x00,0xF8,0x00,0x08,0xF8,0x00,0x03,0x3C,0x07,0x00,0x07,0x3C,0x03,0x00,//W 55
0x08,0x18,0x68,0x80,0x80,0x68,0x18,0x08,0x20,0x30,0x2C,0x03,0x03,0x2C,0x30,0x20,//X 56
0x08,0x38,0xC8,0x00,0xC8,0x38,0x08,0x00,0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00,//Y 57
0x10,0x08,0x08,0x08,0xC8,0x38,0x08,0x00,0x20,0x38,0x26,0x21,0x20,0x20,0x18,0x00,//Z 58
0x00,0x00,0x00,0xFE,0x02,0x02,0x02,0x00,0x00,0x00,0x00,0x7F,0x40,0x40,0x40,0x00,//[ 59
0x00,0x0C,0x30,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x06,0x38,0xC0,0x00,//\ 60
0x00,0x02,0x02,0x02,0xFE,0x00,0x00,0x00,0x00,0x40,0x40,0x40,0x7F,0x00,0x00,0x00,//] 61
0x00,0x00,0x04,0x02,0x02,0x02,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//^ 62
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,//_ 63
0x00,0x02,0x02,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//` 64
0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x19,0x24,0x22,0x22,0x22,0x3F,0x20,//a 65
0x08,0xF8,0x00,0x80,0x80,0x00,0x00,0x00,0x00,0x3F,0x11,0x20,0x20,0x11,0x0E,0x00,//b 66
0x00,0x00,0x00,0x80,0x80,0x80,0x00,0x00,0x00,0x0E,0x11,0x20,0x20,0x20,0x11,0x00,//c 67
0x00,0x00,0x00,0x80,0x80,0x88,0xF8,0x00,0x00,0x0E,0x11,0x20,0x20,0x10,0x3F,0x20,//d 68
0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x22,0x22,0x22,0x22,0x13,0x00,//e 69
0x00,0x80,0x80,0xF0,0x88,0x88,0x88,0x18,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,//f 70
0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x6B,0x94,0x94,0x94,0x93,0x60,0x00,//g 71
0x08,0xF8,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20,//h 72
0x00,0x80,0x98,0x98,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,//i 73
0x00,0x00,0x00,0x80,0x98,0x98,0x00,0x00,0x00,0xC0,0x80,0x80,0x80,0x7F,0x00,0x00,//j 74
0x08,0xF8,0x00,0x00,0x80,0x80,0x80,0x00,0x20,0x3F,0x24,0x02,0x2D,0x30,0x20,0x00,//k 75
0x00,0x08,0x08,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,//l 76
0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x00,0x20,0x3F,0x20,0x00,0x3F,0x20,0x00,0x3F,//m 77
0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20,//n 78
0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00,//o 79
0x80,0x80,0x00,0x80,0x80,0x00,0x00,0x00,0x80,0xFF,0xA1,0x20,0x20,0x11,0x0E,0x00,//p 80
0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x0E,0x11,0x20,0x20,0xA0,0xFF,0x80,//q 81
0x80,0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x20,0x20,0x3F,0x21,0x20,0x00,0x01,0x00,//r 82
0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x33,0x24,0x24,0x24,0x24,0x19,0x00,//s 83
0x00,0x80,0x80,0xE0,0x80,0x80,0x00,0x00,0x00,0x00,0x00,0x1F,0x20,0x20,0x00,0x00,//t 84
0x80,0x80,0x00,0x00,0x00,0x80,0x80,0x00,0x00,0x1F,0x20,0x20,0x20,0x10,0x3F,0x20,//u 85
0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,0x00,0x01,0x0E,0x30,0x08,0x06,0x01,0x00,//v 86
0x80,0x80,0x00,0x80,0x00,0x80,0x80,0x80,0x0F,0x30,0x0C,0x03,0x0C,0x30,0x0F,0x00,//w 87
0x00,0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x31,0x2E,0x0E,0x31,0x20,0x00,//x 88
0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,0x80,0x81,0x8E,0x70,0x18,0x06,0x01,0x00,//y 89
0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x21,0x30,0x2C,0x22,0x21,0x30,0x00,//z 90
0x00,0x00,0x00,0x00,0x80,0x7C,0x02,0x02,0x00,0x00,0x00,0x00,0x00,0x3F,0x40,0x40,//{ 91
0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,//| 92
0x00,0x02,0x02,0x7C,0x80,0x00,0x00,0x00,0x00,0x40,0x40,0x3F,0x00,0x00,0x00,0x00,//} 93
0x00,0x06,0x01,0x01,0x02,0x02,0x04,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//~ 94
};
#endif

294
oled_ssd1306.c Normal file
View File

@@ -0,0 +1,294 @@
// OLED显示屏简化版驱动源文件
#include <stdio.h> // 标准输入输出
#include <stddef.h> // 标准类型定义
#include "iot_gpio.h" // OpenHarmony HALIoT硬件设备操作接口-GPIO
#include "iot_i2c.h" // OpenHarmony HALIoT硬件设备操作接口-I2C
#include "iot_errno.h" // OpenHarmony HALIoT硬件设备操作接口-错误代码定义
#include "hi_io.h" // 海思 Pegasus SDKIoT硬件设备操作接口-IO
// 字库头文件
#include "oled_fonts.h"
// OLED显示屏简化版驱动接口文件
#include "oled_ssd1306.h"
// 定义一个宏,用于计算数组的长度
#define ARRAY_SIZE(a) sizeof(a) / sizeof(a[0])
// 定义一个宏用于标识I2C0
#define OLED_I2C_IDX 0
// 定义一个宏用于标识I2C0的波特率传输速率
#define OLED_I2C_BAUDRATE (400 * 1000) // 400KHz
// 定义一个宏用于标识OLED的宽度
#define OLED_WIDTH (128)
// 定义一个宏用于标识SSD1306显示屏驱动芯片的设备地址
#define OLED_I2C_ADDR 0x78
// 定义一个宏,用于标识写命令操作
#define OLED_I2C_CMD 0x00 // 0000 0000 写命令
// 定义一个宏,用于标识写数据操作
#define OLED_I2C_DATA 0x40 // 0100 0000(0x40) 写数据
// 定义一个宏用于标识100ms的延时
#define DELAY_100_MS (100 * 1000)
// 定义一个结构体,表示要发送或接收的数据
typedef struct
{
// 要发送的数据的指针
unsigned char *sendBuf;
// 要发送的数据长度
unsigned int sendLen;
// 要接收的数据的指针
unsigned char *receiveBuf;
// 要接收的数据长度
unsigned int receiveLen;
} IotI2cData;
/// @brief 向OLED写一个字节
/// @param regAddr 写入命令还是数据 OLED_I2C_CMD / OLED_I2C_DATA
/// @param byte 写入的内容
/// @retval 成功返回IOT_SUCCESS失败返回IOT_FAILURE
static uint32_t I2cWiteByte(uint8_t regAddr, uint8_t byte)
{
// 定义字节流
uint8_t buffer[] = {regAddr, byte};
IotI2cData i2cData = {0};
i2cData.sendBuf = buffer;
i2cData.sendLen = sizeof(buffer) / sizeof(buffer[0]);
// 发送字节流
return IoTI2cWrite(OLED_I2C_IDX, OLED_I2C_ADDR, i2cData.sendBuf, i2cData.sendLen);
}
/// @brief 向OLED写一个命令字节
/// @param cmd 写入的命令字节
/// @return 成功返回IOT_SUCCESS失败返回IOT_FAILURE
static uint32_t WriteCmd(uint8_t cmd)
{
return I2cWiteByte(OLED_I2C_CMD, cmd);
}
/// @brief 向OLED写一个数据字节
/// @param cmd 写入的数据字节
/// @return 成功返回IOT_SUCCESS失败返回IOT_FAILURE
uint32_t WriteData(uint8_t data)
{
return I2cWiteByte(OLED_I2C_DATA, data);
}
/// @brief 初始化SSD1306显示屏驱动芯片
uint32_t OledInit(void)
{
// 构造初始化代码
static const uint8_t initCmds[] = {
0xAE, // 显示关闭
0x00, // 页寻址模式时设置列地址的低4位为0000
0x10, // 页寻址模式时设置列地址的高4位为0000
0x40, // 设置起始行地址为第0行
0xB0, // 页寻址模式时设置页面起始地址为PAGE0
0x81, // 设置对比度
0xFF, // 对比度数值
0xA1, // set segment remap
0xA6, // 设置正常显示。0对应像素熄灭1对应像素亮起
0xA8, // --set multiplex ratio(1 to 64)
0x3F, // --1/32 duty
0xC8, // Com scan direction
0xD3, // -set display offset
0x00, //
0xD5, // set osc division
0x80, //
0xD8, // set area color mode off
0x05, //
0xD9, // Set Pre-Charge Period
0xF1, //
0xDA, // set com pin configuartion
0x12, //
0xDB, // set Vcomh
0x30, //
0x8D, // set charge pump enable
0x14, //
0xAF, // 显示开启
};
// 初始化GPIO-13
IoTGpioInit(HI_IO_NAME_GPIO_13);
// 设置GPIO-13引脚功能为I2C0_SDA
hi_io_set_func(HI_IO_NAME_GPIO_13, HI_IO_FUNC_GPIO_13_I2C0_SDA);
// 初始化GPIO-14
IoTGpioInit(HI_IO_NAME_GPIO_14);
// 设置GPIO-14引脚功能为I2C0_SCL
hi_io_set_func(HI_IO_NAME_GPIO_14, HI_IO_FUNC_GPIO_14_I2C0_SCL);
// 用指定的波特速率初始化I2C0
IoTI2cInit(OLED_I2C_IDX, OLED_I2C_BAUDRATE);
// 发送初始化代码初始化SSD1306显示屏驱动芯片
for (size_t i = 0; i < ARRAY_SIZE(initCmds); i++)
{
// 发送一个命令字节
uint32_t status = WriteCmd(initCmds[i]);
if (status != IOT_SUCCESS)
{
return status;
}
}
// OLED初始化完成返回成功
return IOT_SUCCESS;
}
/// @brief 设置显示位置
/// @param x x坐标1像素为单位
/// @param y y坐标8像素为单位。即页面起始地址
/// @return 无
void OledSetPosition(uint8_t x, uint8_t y)
{
//设置页面起始地址
WriteCmd(0xb0 + y);
// 列0~127
// 第0列0x00列二进制00000000。低地址0000即0x00。高地址0000(需要|0x10)0000|0x10=0x10。
// 第127列0x7f列二进制01111111。低地址1111即0x0F。高地址0111(需要|0x10)0111|0x10=0x17。
// 设置显示位置列地址的低4位
// 直接取出列地址低4位作为命令代码的低4位命令代码的高4位为0000
WriteCmd(x & 0x0f);
// 设置显示位置列地址的高4位
// 取出列地址高4位作为命令代码的低4位命令代码的高4位必须为0001
// 实际编程时列地址的高4位和0x10二进制00010000进行按位或即得到命令代码
WriteCmd(((x & 0xf0) >> 4) | 0x10);
}
/// @brief 全屏填充
/// @param fillData 填充的数据1字节
/// @return 无
void OledFillScreen(uint8_t fillData)
{
// 相关变量用于遍历page和列
uint8_t m = 0;
uint8_t n = 0;
// 写入所有页的数据
for (m = 0; m < 8; m++)
{
//设置页地址0~7
WriteCmd(0xb0 + m);
// 设置显示位置为第0列
WriteCmd(0x00); //设置显示位置:列低地址(0000)
WriteCmd(0x10); //设置显示位置:列高地址(0000)
// 写入128列数据
// 在一个页中数据按列写入一次一列对应发送过来的1字节数据
for (n = 0; n < 128; n++)
{
// 写入一个字节数据
WriteData(fillData);
}
}
}
/// @brief 清屏函数
/// @return 无
void OledClearScreen(void)
{
OledFillScreen(0x00); // 用0x00填充整个屏幕实现清屏
}
/// @brief 显示一个字符
/// @param x: x坐标1像素为单位
/// @param y: y坐标8像素为单位
/// @param ch: 要显示的字符
/// @param font: 字库
void OledShowChar(uint8_t x, uint8_t y, uint8_t ch, Font font)
{
// 数组下标
uint8_t c = 0;
// 循环控制
uint8_t i = 0;
// 得到数组下标
// 空格的ASCII码32在字库中的下标是0。字库中的字符-空格即相应的数组下标
c = ch - ' ';
// 显示字符
if (font == FONT8x16) // 8*16的点阵一个page放不下
{
// 显示字符的上半部分
// 设置显示位置
OledSetPosition(x, y);
// 逐个字节写入16个数组元素的前8个
for (i = 0; i < 8; i++)
{
WriteData(F8X16[c * 16 + i]);
}
// 显示字符的下半部分
// 设置显示位置为下一个PAGE
OledSetPosition(x, y + 1);
// 逐个字节写入16个数组元素的后8个
for (i = 0; i < 8; i++)
{
WriteData(F8X16[c * 16 + 8 + i]);
}
}
else // 6*8的点阵在一个page中
{
// 设置显示位置
OledSetPosition(x, y);
// 逐个字节写入数组第二维的6个数组元素
for (i = 0; i < 6; i++)
{
WriteData(F6x8[c][i]);
}
}
}
/// @brief 显示一个字符串
/// @param x: x坐标1像素为单位
/// @param y: y坐标8像素为单位
/// @param str: 要显示的字符串
/// @param font: 字库
void OledShowString(uint8_t x, uint8_t y, const char *str, Font font)
{
// 字符数组(字符串)下标
uint8_t j = 0;
// 检查字符串是否为空
if (str == NULL)
{
printf("param is NULL,Please check!!!\r\n");
return;
}
// 遍历字符串,显示每个字符
while (str[j])
{
// 显示一个字符
OledShowChar(x, y, str[j], font);
// 设置字符间距
x += 8;
// 如果下一个要显示的字符超出了OLED显示的范围则换行
if (x > 120)
{
x = 0;
y += 2;
}
// 下一个字符
j++;
}
}

32
oled_ssd1306.h Normal file
View File

@@ -0,0 +1,32 @@
// OLED显示屏简化版驱动接口文件
// 定义条件编译宏,防止头文件的重复包含和编译
#ifndef OLED_SSD1306_H
#define OLED_SSD1306_H
#include <stdint.h> // 定义了几种扩展的整数类型和宏
// 声明接口函数
uint32_t OledInit(void);
void OledSetPosition(uint8_t x, uint8_t y);
void OledFillScreen(uint8_t fillData);
uint32_t WriteData(uint8_t data);
// 清屏函数
void OledClearScreen(void);
// 定义字库类型
enum Font {
FONT6x8 = 1,
FONT8x16
};
typedef enum Font Font;
// 声明接口函数
void OledShowChar(uint8_t x, uint8_t y, uint8_t ch, Font font);
void OledShowString(uint8_t x, uint8_t y, const char* str, Font font);
// 条件编译结束
#endif // OLED_SSD1306_H

View File

@@ -1,25 +0,0 @@
# 车牌识别系统依赖包
# 深度学习和计算机视觉
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 # 科学计算

69
robot_sg90.c Normal file
View File

@@ -0,0 +1,69 @@
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "iot_gpio.h"
#include "hi_io.h"
#include "hi_time.h"
//查阅机器人板原理图可知SG90舵机通过GPIO2与3861连接
//SG90舵机的控制需要MCU产生一个周期为20ms的脉冲信号以0.5ms到2.5ms的高电平来控制舵机转动的角度
#define GPIO2 2
//输出20000微秒的脉冲信号(x微秒高电平,20000-x微秒低电平)
void set_angle( unsigned int duty) {
IoTGpioSetDir(GPIO2, IOT_GPIO_DIR_OUT);//设置GPIO2为输出模式
//GPIO2输出x微秒高电平
IoTGpioSetOutputVal(GPIO2, IOT_GPIO_VALUE1);
hi_udelay(duty);
//GPIO2输出20000-x微秒低电平
IoTGpioSetOutputVal(GPIO2, IOT_GPIO_VALUE0);
hi_udelay(20000 - duty);
}
/*Steering gear turn left (counter-clockwise 90 degrees)
1、依据角度与脉冲的关系设置高电平时间为1000微秒
2、发送20次脉冲信号控制舵机逆时针旋转90度
*/
void engine_turn_left(void)
{
for (int i = 0; i < 20; i++) {
set_angle(1000);
}
}
/*Steering gear turn right (clockwise 90 degrees)
1、依据角度与脉冲的关系设置高电平时间为2000微秒
2、发送20次脉冲信号控制舵机顺时针旋转90度
*/
void engine_turn_right(void)
{
for (int i = 0; i < 20; i++) {
set_angle(2000);
}
}
/*Steering gear return to middle
1、依据角度与脉冲的关系设置高电平时间为1500微秒
2、发送20次脉冲信号控制舵机居中
*/
void regress_middle(void)
{
for (int i = 0; i < 20; i++) {
set_angle(1500);
}
}
// 顺时针旋转90度
void servo_rotate_clockwise_90(void) {
engine_turn_right();
}
// 逆时针旋转90度
void servo_rotate_counter_clockwise_90(void) {
engine_turn_left();
}

13
robot_sg90.h Normal file
View File

@@ -0,0 +1,13 @@
#ifndef ROBOT_SG90_H
#define ROBOT_SG90_H
void set_angle(int angle);
void engine_turn_left(void);
void engine_turn_right(void);
void regress_middle(void);
// 新增的90度精确旋转函数
void servo_rotate_clockwise_90(void);
void servo_rotate_counter_clockwise_90(void);
#endif

53
simple_client.py Normal file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
简单的UDP客户端程序
向Hi3861设备发送JSON命令
"""
import socket
import json
import time
def send_command():
"""发送命令到Hi3861设备"""
# 目标设备信息
target_ip = "192.168.43.12"
target_port = 8081
# 创建JSON命令
command = {
"cmd": 1, # 测试命令4只显示字符串舵机不动
"text": "沪AAAAAA 通行 2sec"
}
json_command = json.dumps(command, ensure_ascii=False)
try:
# 创建UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 发送命令
print(f"正在向 {target_ip}:{target_port} 发送命令...")
print(f"命令内容: {json_command}")
sock.sendto(json_command.encode('utf-8'), (target_ip, target_port))
print("命令发送成功!")
print("设备将执行以下操作:")
print("1. 顺时针旋转舵机90度")
print("2. 在OLED屏幕上显示沪AAAAAA")
print("3. 等待10秒")
print("4. 逆时针旋转舵机90度")
print("5. 清空OLED屏幕")
except Exception as e:
print(f"发送命令失败: {e}")
finally:
sock.close()
if __name__ == "__main__":
print("Hi3861 简单客户端程序")
print("=" * 30)
send_command()
print("程序结束")

56
test_json.c Normal file
View File

@@ -0,0 +1,56 @@
#include <stdio.h>
#include <string.h>
#include "json_parser.h"
// 测试JSON解析功能
void test_json_parsing() {
printf("=== JSON解析测试 ===\n");
// 测试命令1旋转+显示+清屏
char test1[] = "{\"cmd\":1,\"text\":\"Hello World\"}";
JsonCommand cmd1;
if (ParseJsonCommand(test1, &cmd1) == 0) {
printf("测试1成功: cmd=%d, text=%s\n", cmd1.cmd, cmd1.text);
} else {
printf("测试1失败\n");
}
// 测试命令2顺时针旋转
char test2[] = "{\"cmd\":2,\"text\":\"\"}";
JsonCommand cmd2;
if (ParseJsonCommand(test2, &cmd2) == 0) {
printf("测试2成功: cmd=%d, text=%s\n", cmd2.cmd, cmd2.text);
} else {
printf("测试2失败\n");
}
// 测试命令3逆时针旋转
char test3[] = "{\"cmd\":3,\"text\":\"ignored\"}";
JsonCommand cmd3;
if (ParseJsonCommand(test3, &cmd3) == 0) {
printf("测试3成功: cmd=%d, text=%s\n", cmd3.cmd, cmd3.text);
} else {
printf("测试3失败\n");
}
// 测试IP消息创建
printf("\n=== IP消息创建测试 ===\n");
char ip_msg[128];
CreateIpMessage("192.168.1.100", ip_msg, sizeof(ip_msg));
printf("IP消息: %s\n", ip_msg);
}
// 测试舵机控制逻辑
void test_servo_commands() {
printf("\n=== 舵机命令测试 ===\n");
printf("命令1: 顺时针90°+显示+10秒后逆时针90°+清屏\n");
printf("命令2: 顺时针90°\n");
printf("命令3: 逆时针90°\n");
}
int main() {
test_json_parsing();
test_servo_commands();
return 0;
}

242
udp_client_test.c Normal file
View File

@@ -0,0 +1,242 @@
#include <stdio.h> // 标准输入输出
#include <unistd.h> // POSIX标准接口
#include <errno.h> // 错误码
#include <string.h> // 字符串处理(操作字符数组)
#include "lwip/sockets.h" // lwIP TCP/IP协议栈Socket API
#include "ohos_init.h" // 用于初始化服务(services)和功能(features)
#include "cmsis_os2.h" // CMSIS-RTOS API V2
#include "oled_ssd1306.h"
#include "json_parser.h" // JSON解析器
#include "wifi_connecter.h" // WiFi连接器
extern int control_flag ;
// 要发送的数据
static char request[] = "connecting";
// 要接收的数据
char response[128] = "";
// 全局变量存储解析后的命令
JsonCommand g_current_command = {0};
// 发送IP地址到服务器
void SendIpToServer(const char *host, unsigned short port) {
char ip_buffer[32] = {0};
char message_buffer[128] = {0};
// 获取本机IP地址
if (GetLocalIpAddress(ip_buffer, sizeof(ip_buffer)) != 0) {
printf("Failed to get local IP address\r\n");
return;
}
// 创建IP消息
if (CreateIpMessage(message_buffer, sizeof(message_buffer), ip_buffer) != 0) {
printf("Failed to create IP message\r\n");
return;
}
// 创建UDP socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
printf("Failed to create socket\r\n");
return;
}
struct sockaddr_in toAddr = {0};
toAddr.sin_family = AF_INET;
toAddr.sin_port = htons(port);
if (inet_pton(AF_INET, host, &toAddr.sin_addr) <= 0) {
printf("inet_pton failed!\r\n");
lwip_close(sockfd);
return;
}
// 发送IP地址消息
ssize_t retval = sendto(sockfd, message_buffer, strlen(message_buffer), 0,
(struct sockaddr *)&toAddr, sizeof(toAddr));
if (retval < 0) {
printf("Failed to send IP message!\r\n");
} else {
printf("IP message sent: %s\r\n", message_buffer);
}
lwip_close(sockfd);
}
/// @brief UDP客户端测试函数
/// @param host UDP服务端IP地址
/// @param port UDP服务端端口
void UdpClientTest(const char *host, unsigned short port)
{
// 用于接收Socket API接口返回值
ssize_t retval = 0;
// 创建一个UDP Socket返回值为文件描述符
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
printf("创建socket失败\r\n");
return;
}
// 用于设置服务端的地址信息
struct sockaddr_in toAddr = {0};
// 用于设置本地绑定的地址信息
struct sockaddr_in localAddr = {0};
// 使用IPv4协议
toAddr.sin_family = AF_INET;
localAddr.sin_family = AF_INET;
// 端口号,从主机字节序转为网络字节序
toAddr.sin_port = htons(port);
localAddr.sin_port = htons(port); // 绑定到相同端口接收数据
localAddr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的网络接口
// 将服务端IP地址从"点分十进制"字符串转化为标准格式32位整数
if (inet_pton(AF_INET, host, &toAddr.sin_addr) <= 0)
{
// 转化失败
printf("inet_pton failed!\r\n"); // 输出日志
lwip_close(sockfd);
return;
}
// 绑定本地端口,这样才能接收数据
printf("绑定本地端口 %d...\r\n", port);
if (bind(sockfd, (struct sockaddr *)&localAddr, sizeof(localAddr)) < 0) {
printf("绑定端口失败!错误: %d\r\n", errno);
lwip_close(sockfd);
return;
}
printf("端口绑定成功!\r\n");
// 发送设备IP地址到服务器
SendIpToServer(sockfd, &toAddr);
// 发送数据
// UDP socket是 "无连接的",因此每次发送都必须先指定目标主机和端口,主机可以是多播地址
// 发送数据的时候使用本地随机端口N
//
// 参数:
// ssocket文件描述符
// dataptr要发送的数据
// size要发送的数据的长度最大65332字节
// flags消息传输标志位
// to目标的地址信息
// tolen目标的地址信息长度
//
// 返回值:
// 发送的字节数,如果出错,返回-1
printf("发送连接消息到服务器...\r\n");
retval = sendto(sockfd, request, sizeof(request), 0, (struct sockaddr *)&toAddr, sizeof(toAddr));
// 检查接口返回值小于0表示发送失败
if (retval < 0)
{
// 发送失败
printf("发送消息失败!错误: %d\r\n", errno); // 输出日志
lwip_close(sockfd);
return;
}
// 发送成功
// 输出日志
printf("发送UDP消息成功: {%s} %ld 字节\r\n", request, retval);
// 显示等待接收状态
printf("开始监听UDP消息等待来自 %s 的数据...\r\n", host);
// 用于记录发送方的地址信息(IP地址和端口号)
struct sockaddr_in fromAddr = {0};
// 用于记录发送方的地址信息长度
socklen_t fromLen = sizeof(fromAddr);
// 在本地随机端口N上面接收数据
// UDP socket是 “无连接的”因此每次接收时并不知道消息来自何处通过fromAddr参数可以得到发送方的信息主机、端口号
// device\hisilicon\hispark_pegasus\sdk_liteos\third_party\lwip_sack\include\lwip\sockets.h -> lwip_recvfrom
//
// 参数:
// ssocket文件描述符
// buffer接收数据的缓冲区的地址
// length接收数据的缓冲区的长度
// flags消息接收标志位
// address发送方的地址信息
// address_len发送方的地址信息长度
//
// 返回值:
// 接收的字节数,如果出错,返回-1
while (1)
{
// 清空接收缓冲区
memset(response, 0, sizeof(response));
retval = lwip_recvfrom(sockfd, response, sizeof(response) - 1, 0, (struct sockaddr *)&fromAddr, &fromLen);
// 检查接口返回值小于0表示接收失败
if (retval <= 0)
{
// 接收失败或者收到0长度的数据(忽略掉)
printf("recvfrom failed or abort, %ld, %d!\r\n", retval, errno); // 输出日志
continue; // 继续接收不要关闭socket
}
// 接收成功
// 末尾添加字符串结束符'\0',以便后续的字符串操作
response[retval] = '\0';
// 输出日志 - 显示所有收到的消息
printf("=== 收到UDP消息 ===\r\n");
printf("消息内容: {%s}\r\n", response);
printf("消息长度: %ld 字节\r\n", retval);
printf("发送方IP: %s\r\n", inet_ntoa(fromAddr.sin_addr));
printf("发送方端口: %d\r\n", ntohs(fromAddr.sin_port));
printf("==================\r\n");
// 尝试解析JSON命令
JsonCommand temp_cmd = {0};
if (ParseJsonCommand(response, &temp_cmd) == 0) {
printf("JSON解析成功: cmd=%d, text=%s\r\n", temp_cmd.cmd, temp_cmd.text);
// 保存解析的命令
g_current_command = temp_cmd;
// 根据命令类型设置控制标志
if (temp_cmd.cmd >= 1 && temp_cmd.cmd <= 4) {
control_flag = temp_cmd.cmd;
printf("设置控制标志为: %d\r\n", control_flag);
} else {
printf("无效的命令类型: %d\r\n", temp_cmd.cmd);
}
} else {
printf("JSON解析失败尝试旧格式命令\r\n");
// 兼容旧的字符串命令格式
if(strlen(response) > 10) {
control_flag = 2;
g_current_command.cmd = 0; // 标记为旧命令
printf("设置为复杂字符串命令,控制标志: %d\r\n", control_flag);
} else if(strcmp(response,"T:on") == 0) {
control_flag = 1;
g_current_command.cmd = 0; // 标记为旧命令
printf("设置为开启命令,控制标志: %d\r\n", control_flag);
} else if(strcmp(response,"T:off") == 0) {
control_flag = 0;
g_current_command.cmd = 0; // 标记为旧命令
printf("设置为关闭命令,控制标志: %d\r\n", control_flag);
} else {
printf("未识别的命令格式: %s\r\n", response);
}
}
sleep(1);
}
// 关闭socket
lwip_close(sockfd);
}

146
wifi_connecter.c Normal file
View File

@@ -0,0 +1,146 @@
#include "cmsis_os2.h" // CMSIS-RTOS API V2
#include "wifi_device.h" // Wi-Fi设备接口station模式
#include "lwip/netifapi.h" // lwIP TCP/IP协议栈网络接口API
#include "lwip/api_shell.h" // lwIP TCP/IP协议栈SHELL命令API
static void PrintLinkedInfo(WifiLinkedInfo* info)
{
if (!info) return;
static char macAddress[32] = {0};
unsigned char* mac = info->bssid;
snprintf(macAddress, sizeof(macAddress), "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
printf("bssid: %s, rssi: %d, connState: %d, reason: %d, ssid: %s\r\n",
macAddress, info->rssi, info->connState, info->disconnectedReason, info->ssid);
}
static volatile int g_connected = 0;
static void OnWifiConnectionChanged(int state, WifiLinkedInfo* info)
{
if (!info) return;
printf("%s %d, state = %d, info = \r\n", __FUNCTION__, __LINE__, state);
PrintLinkedInfo(info);
if (state == WIFI_STATE_AVALIABLE) {
g_connected = 1;
} else {
g_connected = 0;
}
}
static void OnWifiScanStateChanged(int state, int size)
{
printf("%s %d, state = %X, size = %d\r\n", __FUNCTION__, __LINE__, state, size);
}
static WifiEvent g_defaultWifiEventListener = {
.OnWifiConnectionChanged = OnWifiConnectionChanged,
.OnWifiScanStateChanged = OnWifiScanStateChanged
};
static struct netif* g_iface = NULL;
err_t netifapi_set_hostname(struct netif *netif, char *hostname, u8_t namelen);
int ConnectToHotspot(WifiDeviceConfig* apConfig)
{
WifiErrorCode errCode;
int netId = -1;
errCode = RegisterWifiEvent(&g_defaultWifiEventListener);
printf("RegisterWifiEvent: %d\r\n", errCode);
errCode = EnableWifi();
printf("EnableWifi: %d\r\n", errCode);
errCode = AddDeviceConfig(apConfig, &netId);
printf("AddDeviceConfig: %d\r\n", errCode);
g_connected = 0;
errCode = ConnectTo(netId);
printf("ConnectTo(%d): %d\r\n", netId, errCode);
while (!g_connected) { // wait until connect to AP
// printf("connecting\n");
osDelay(10);
}
printf("g_connected: %d\r\n", g_connected);
g_iface = netifapi_netif_find("wlan0");
if (g_iface) {
err_t ret = 0;
char* hostname = "hispark";
ret = netifapi_set_hostname(g_iface, hostname, strlen(hostname));
printf("netifapi_set_hostname: %d\r\n", ret);
ret = netifapi_dhcp_start(g_iface);
printf("netifapi_dhcp_start: %d\r\n", ret);
osDelay(100); // wait DHCP server give me IP
#if 0
ret = netifapi_netif_common(g_iface, dhcp_clients_info_show, NULL);
printf("netifapi_netif_common: %d\r\n", ret);
#else
// 下面这种方式也可以打印 IP、网关、子网掩码信息
ip4_addr_t ip = {0};
ip4_addr_t netmask = {0};
ip4_addr_t gw = {0};
ret = netifapi_netif_get_addr(g_iface, &ip, &netmask, &gw);
if (ret == ERR_OK) {
printf("ip = %s\r\n", ip4addr_ntoa(&ip));
printf("netmask = %s\r\n", ip4addr_ntoa(&netmask));
printf("gw = %s\r\n", ip4addr_ntoa(&gw));
}
printf("netifapi_netif_get_addr: %d\r\n", ret);
#endif
}
return netId;
}
void DisconnectWithHotspot(int netId)
{
if (g_iface) {
err_t ret = netifapi_dhcp_stop(g_iface);
printf("netifapi_dhcp_stop: %d\r\n", ret);
}
WifiErrorCode errCode = Disconnect(); // disconnect with your AP
printf("Disconnect: %d\r\n", errCode);
errCode = UnRegisterWifiEvent(&g_defaultWifiEventListener);
printf("UnRegisterWifiEvent: %d\r\n", errCode);
RemoveDevice(netId); // remove AP config
printf("RemoveDevice: %d\r\n", errCode);
errCode = DisableWifi();
printf("DisableWifi: %d\r\n", errCode);
}
// 获取本机IP地址
int GetLocalIpAddress(char* ip_buffer, int buffer_size)
{
if (!ip_buffer || buffer_size < 16 || !g_iface) {
return -1;
}
ip4_addr_t ip = {0};
ip4_addr_t netmask = {0};
ip4_addr_t gw = {0};
err_t ret = netifapi_netif_get_addr(g_iface, &ip, &netmask, &gw);
if (ret == ERR_OK) {
const char* ip_str = ip4addr_ntoa(&ip);
if (ip_str && strlen(ip_str) < buffer_size) {
strcpy(ip_buffer, ip_str);
return 0;
}
}
return -1;
}

13
wifi_connecter.h Normal file
View File

@@ -0,0 +1,13 @@
#ifndef WIFI_CONNECTER_H
#define WIFI_CONNECTER_H
#include "wifi_device.h" // Wi-Fi设备接口station模式
int ConnectToHotspot(WifiDeviceConfig* apConfig);
void DisconnectWithHotspot(int netId);
// 获取本机IP地址
int GetLocalIpAddress(char* ip_buffer, int buffer_size);
#endif // WIFI_CONNECTER_H

View File

@@ -1,275 +0,0 @@
import cv2
import numpy as np
from ultralytics import YOLO
import os
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.5):
"""
检测图像中的车牌
参数:
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):
"""
在图像上绘制检测结果
参数:
image: 输入图像
detections: 检测结果列表
返回:
numpy.ndarray: 绘制了检测结果的图像
"""
draw_image = image.copy()
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)
# 绘制边界框
x1, y1, x2, y2 = map(int, box)
# 根据车牌类型选择颜色
if class_name == '绿牌':
box_color = (0, 255, 0) # 绿色
elif class_name == '蓝牌':
box_color = (255, 0, 0) # 蓝色
else:
box_color = (128, 128, 128) # 灰色
cv2.rectangle(draw_image, (x1, y1), (x2, y2), box_color, 2)
# 绘制标签
label = f"{class_name} {confidence:.2f}"
if incomplete:
label += " (不完整)"
# 计算文本大小和位置
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 0.6
thickness = 2
(text_width, text_height), _ = cv2.getTextSize(label, font, font_scale, thickness)
# 绘制文本背景
cv2.rectangle(draw_image, (x1, y1 - text_height - 10),
(x1 + text_width, y1), box_color, -1)
# 绘制文本
cv2.putText(draw_image, label, (x1, y1 - 5),
font, font_scale, (255, 255, 255), thickness)
# 绘制关键点和连线
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())

Binary file not shown.