diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 35410ca..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/License_plate_recognition.iml b/.idea/License_plate_recognition.iml deleted file mode 100644 index fb56de3..0000000 --- a/.idea/License_plate_recognition.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index fb9fc56..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 9b0ce31..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 288b36b..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/BUILD.gn b/BUILD.gn new file mode 100644 index 0000000..d880042 --- /dev/null +++ b/BUILD.gn @@ -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接口 + ] +} diff --git a/CRNN_part/best_model.pth b/CRNN_part/best_model.pth deleted file mode 100644 index 4054755..0000000 Binary files a/CRNN_part/best_model.pth and /dev/null differ diff --git a/CRNN_part/crnn_interface.py b/CRNN_part/crnn_interface.py deleted file mode 100644 index 5aac245..0000000 --- a/CRNN_part/crnn_interface.py +++ /dev/null @@ -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需要) - '', - # 中文省份简称 - '京', '沪', '津', '渝', '冀', '晋', '蒙', '辽', '吉', '黑', - '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤', - '桂', '琼', '川', '贵', '云', '藏', '陕', '甘', '青', '宁', '新', - # 字母 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'] diff --git a/LPRNET_part/LPRNet__iteration_74000.pth b/LPRNET_part/LPRNet__iteration_74000.pth deleted file mode 100644 index 6189faa..0000000 Binary files a/LPRNET_part/LPRNet__iteration_74000.pth and /dev/null differ diff --git a/LPRNET_part/lpr_interface.py b/LPRNET_part/lpr_interface.py deleted file mode 100644 index 2b688ba..0000000 --- a/LPRNET_part/lpr_interface.py +++ /dev/null @@ -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'] \ No newline at end of file diff --git a/LPRNET_part/吉CF18040.jpg b/LPRNET_part/吉CF18040.jpg deleted file mode 100644 index 29d94e6..0000000 Binary files a/LPRNET_part/吉CF18040.jpg and /dev/null differ diff --git a/LPRNET_part/藏A0DBN8.jpg b/LPRNET_part/藏A0DBN8.jpg deleted file mode 100644 index 32ddef3..0000000 Binary files a/LPRNET_part/藏A0DBN8.jpg and /dev/null differ diff --git a/OCR_part/ocr_interface.py b/OCR_part/ocr_interface.py deleted file mode 100644 index b98c5b8..0000000 --- a/OCR_part/ocr_interface.py +++ /dev/null @@ -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 - - - diff --git a/README.md b/README.md deleted file mode 100644 index 40b4614..0000000 --- a/README.md +++ /dev/null @@ -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解码**: 使用CTC(Connectionist 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接口 -- 数据库集成 -- 性能统计和分析 \ No newline at end of file diff --git a/chinese_char_map.h b/chinese_char_map.h new file mode 100644 index 0000000..6df0034 --- /dev/null +++ b/chinese_char_map.h @@ -0,0 +1,56 @@ +#ifndef CHINESE_CHAR_MAP_H +#define CHINESE_CHAR_MAP_H + +#include + +// 中文字符映射结构体 +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 \ No newline at end of file diff --git a/demo_entry_cmsis.c b/demo_entry_cmsis.c new file mode 100644 index 0000000..cdbc809 --- /dev/null +++ b/demo_entry_cmsis.c @@ -0,0 +1,509 @@ +#include // 标准输入输出 +#include // POSIX标准接口 +#include // 字符串处理(操作字符数组) + +#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); + } + } + // 兼容旧命令的处理逻辑(只有在非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; + } + } + + // 兼容旧的计时逻辑 + 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); diff --git a/display_helper.c b/display_helper.c new file mode 100644 index 0000000..fb29740 --- /dev/null +++ b/display_helper.c @@ -0,0 +1,64 @@ +#include "display_helper.h" +#include "chinese_char_map.h" +#include "json_parser.h" +#include "oled_ssd1306.h" +#include +#include + +// 显示混合字符串(中文+英文数字) +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像素) + } + } +} \ No newline at end of file diff --git a/display_helper.h b/display_helper.h new file mode 100644 index 0000000..fb0057f --- /dev/null +++ b/display_helper.h @@ -0,0 +1,9 @@ +#ifndef DISPLAY_HELPER_H +#define DISPLAY_HELPER_H + +#include + +// 显示混合字符串(中文+英文数字) +void DisplayMixedString(uint8_t start_x, uint8_t start_y, const char* text); + +#endif // DISPLAY_HELPER_H \ No newline at end of file diff --git a/json_parser.c b/json_parser.c new file mode 100644 index 0000000..51f2202 --- /dev/null +++ b/json_parser.c @@ -0,0 +1,72 @@ +#include "json_parser.h" +#include "chinese_char_map.h" +#include +#include +#include + +// 简单的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; +} \ No newline at end of file diff --git a/json_parser.h b/json_parser.h new file mode 100644 index 0000000..1510a4f --- /dev/null +++ b/json_parser.h @@ -0,0 +1,29 @@ +#ifndef JSON_PARSER_H +#define JSON_PARSER_H + +#include + +// 定义命令类型 +#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 \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index e0d6477..0000000 --- a/main.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/oled_fonts.h b/oled_fonts.h new file mode 100644 index 0000000..0f2ebab --- /dev/null +++ b/oled_fonts.h @@ -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 diff --git a/oled_ssd1306.c b/oled_ssd1306.c new file mode 100644 index 0000000..2c92b8e --- /dev/null +++ b/oled_ssd1306.c @@ -0,0 +1,294 @@ +// OLED显示屏简化版驱动源文件 + +#include // 标准输入输出 +#include // 标准类型定义 + +#include "iot_gpio.h" // OpenHarmony HAL:IoT硬件设备操作接口-GPIO +#include "iot_i2c.h" // OpenHarmony HAL:IoT硬件设备操作接口-I2C +#include "iot_errno.h" // OpenHarmony HAL:IoT硬件设备操作接口-错误代码定义 +#include "hi_io.h" // 海思 Pegasus SDK:IoT硬件设备操作接口-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++; + } +} diff --git a/oled_ssd1306.h b/oled_ssd1306.h new file mode 100644 index 0000000..556f60b --- /dev/null +++ b/oled_ssd1306.h @@ -0,0 +1,32 @@ +// OLED显示屏简化版驱动接口文件 + +// 定义条件编译宏,防止头文件的重复包含和编译 +#ifndef OLED_SSD1306_H +#define OLED_SSD1306_H + +#include // 定义了几种扩展的整数类型和宏 + +// 声明接口函数 + +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 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f5e0e68..0000000 --- a/requirements.txt +++ /dev/null @@ -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 # 科学计算 \ No newline at end of file diff --git a/robot_sg90.c b/robot_sg90.c new file mode 100644 index 0000000..c627f31 --- /dev/null +++ b/robot_sg90.c @@ -0,0 +1,69 @@ +#include +#include +#include + +#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(); +} diff --git a/robot_sg90.h b/robot_sg90.h new file mode 100644 index 0000000..41c3d7c --- /dev/null +++ b/robot_sg90.h @@ -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 \ No newline at end of file diff --git a/simple_client.py b/simple_client.py new file mode 100644 index 0000000..92fe9a4 --- /dev/null +++ b/simple_client.py @@ -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("程序结束") \ No newline at end of file diff --git a/test_json.c b/test_json.c new file mode 100644 index 0000000..48e3b3b --- /dev/null +++ b/test_json.c @@ -0,0 +1,56 @@ +#include +#include +#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; +} \ No newline at end of file diff --git a/test_lpr_real_images.py b/test_lpr_real_images.py deleted file mode 100644 index ce32954..0000000 --- a/test_lpr_real_images.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -LPRNet接口真实图片测试脚本 -测试LPRNET_part目录下的真实车牌图片 -""" - -import cv2 -import numpy as np -import os -from LPRNET_part.lpr_interface import LPRNinitialize_model, LPRNmodel_predict - -def test_real_images(): - """ - 测试LPRNET_part目录下的真实车牌图片 - """ - print("=== LPRNet真实图片测试 ===") - - # 初始化模型 - print("1. 初始化LPRNet模型...") - success = LPRNinitialize_model() - if not success: - print("模型初始化失败!") - return - - # 获取LPRNET_part目录下的图片文件 - lprnet_dir = "LPRNET_part" - image_files = [] - - if os.path.exists(lprnet_dir): - for file in os.listdir(lprnet_dir): - if file.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')): - image_files.append(os.path.join(lprnet_dir, file)) - - if not image_files: - print("未找到图片文件!") - return - - print(f"2. 找到 {len(image_files)} 个图片文件") - - # 测试每个图片 - for i, image_path in enumerate(image_files, 1): - print(f"\n--- 测试图片 {i}: {os.path.basename(image_path)} ---") - - try: - # 使用支持中文路径的方式读取图片 - image = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), cv2.IMREAD_COLOR) - - if image is None: - print(f"无法读取图片: {image_path}") - continue - - print(f"图片尺寸: {image.shape}") - - # 进行预测 - result = LPRNmodel_predict(image) - print(f"识别结果: {result}") - print(f"识别车牌号: {''.join(result)}") - - except Exception as e: - print(f"处理图片 {image_path} 时出错: {e}") - import traceback - traceback.print_exc() - - print("\n=== 测试完成 ===") - -def test_image_loading(): - """ - 测试图片加载方式 - """ - print("\n=== 图片加载测试 ===") - - lprnet_dir = "LPRNET_part" - - if os.path.exists(lprnet_dir): - for file in os.listdir(lprnet_dir): - if file.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')): - image_path = os.path.join(lprnet_dir, file) - print(f"\n测试文件: {file}") - - # 方法1: 普通cv2.imread - img1 = cv2.imread(image_path) - print(f"cv2.imread结果: {img1 is not None}") - - # 方法2: 支持中文路径的方式 - try: - img2 = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), cv2.IMREAD_COLOR) - print(f"cv2.imdecode结果: {img2 is not None}") - if img2 is not None: - print(f"图片尺寸: {img2.shape}") - except Exception as e: - print(f"cv2.imdecode失败: {e}") - -if __name__ == "__main__": - # 首先测试图片加载 - test_image_loading() - - # 然后测试完整的识别流程 - test_real_images() \ No newline at end of file diff --git a/udp_client_test.c b/udp_client_test.c new file mode 100644 index 0000000..1fbdeab --- /dev/null +++ b/udp_client_test.c @@ -0,0 +1,242 @@ +#include // 标准输入输出 +#include // POSIX标准接口 +#include // 错误码 +#include // 字符串处理(操作字符数组) + +#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 + // + // 参数: + // s:socket文件描述符 + // 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 + // + // 参数: + // s:socket文件描述符 + // 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); +} diff --git a/wifi_connecter.c b/wifi_connecter.c new file mode 100644 index 0000000..cbb1e04 --- /dev/null +++ b/wifi_connecter.c @@ -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; +} + diff --git a/wifi_connecter.h b/wifi_connecter.h new file mode 100644 index 0000000..540a3c0 --- /dev/null +++ b/wifi_connecter.h @@ -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 \ No newline at end of file diff --git a/yolopart/detector.py b/yolopart/detector.py deleted file mode 100644 index b95fd80..0000000 --- a/yolopart/detector.py +++ /dev/null @@ -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()) \ No newline at end of file diff --git a/yolopart/yolo11s-pose42.pt b/yolopart/yolo11s-pose42.pt deleted file mode 100644 index 45f8ddc..0000000 Binary files a/yolopart/yolo11s-pose42.pt and /dev/null differ