diff --git a/Cpp_example/D01_test_detection/test_detection.cc b/Cpp_example/D01_test_detection/test_detection.cc index 5a52e847c3dcb49f17bf4a025b6a2819cba90111..61222a26cb8e3921e0b908ce36a75fc850273512 100755 --- a/Cpp_example/D01_test_detection/test_detection.cc +++ b/Cpp_example/D01_test_detection/test_detection.cc @@ -31,8 +31,8 @@ int main(int argc, char *argv[]) std::cout << "Device connected successfully." << std::endl; // 打开摄像头 cv::VideoCapture cap; - cap.set(cv::CAP_PROP_FRAME_WIDTH, 640); - cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480); + cap.set(cv::CAP_PROP_FRAME_WIDTH, 320); + cap.set(cv::CAP_PROP_FRAME_HEIGHT, 240); cap.open(0); if (!cap.isOpened()) @@ -51,7 +51,7 @@ int main(int argc, char *argv[]) std::cerr << "Warning: Captured an empty frame." << std::endl; continue; } - + model.SetThreshold(0.8,0.4); // 调用模型进行预测 high_resolution_clock::time_point start_time = high_resolution_clock::now(); auto results = model.Predict(input_mat); diff --git a/Cpp_example/D13_target_tracking/README.md b/Cpp_example/D13_target_tracking/README.md index 462ebbcff6c9cbc218e67b5e0eb8f5f15fddf73e..96f01ad1be1cbe893cec01a202058208d6ee111c 100755 --- a/Cpp_example/D13_target_tracking/README.md +++ b/Cpp_example/D13_target_tracking/README.md @@ -1,4 +1,4 @@ -# 多目标跟踪我能当 +# 多目标跟踪 本文档介绍了如何使用 lockzhiner_vision_module 库结合卡尔曼滤波和匈牙利算法实现多目标跟踪系统。 diff --git a/Cpp_example/D14_fatigue_detection/CMakeLists.txt b/Cpp_example/D14_fatigue_detection/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..21ec4a50b4ea12e4bbf5d999d5cfccf927eb5aa6 --- /dev/null +++ b/Cpp_example/D14_fatigue_detection/CMakeLists.txt @@ -0,0 +1,50 @@ +cmake_minimum_required(VERSION 3.10) + +project(face_detection) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 定义项目根目录路径 +set(PROJECT_ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../..") +message("PROJECT_ROOT_PATH = " ${PROJECT_ROOT_PATH}) + +include("${PROJECT_ROOT_PATH}/toolchains/arm-rockchip830-linux-uclibcgnueabihf.toolchain.cmake") + +# 定义 OpenCV SDK 路径 +set(OpenCV_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/opencv-mobile-4.10.0-lockzhiner-vision-module") +set(OpenCV_DIR "${OpenCV_ROOT_PATH}/lib/cmake/opencv4") +find_package(OpenCV REQUIRED) +set(OPENCV_LIBRARIES "${OpenCV_LIBS}") + +# 定义 LockzhinerVisionModule SDK 路径 +set(LockzhinerVisionModule_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/lockzhiner_vision_module_sdk") +set(LockzhinerVisionModule_DIR "${LockzhinerVisionModule_ROOT_PATH}/lib/cmake/lockzhiner_vision_module") +find_package(LockzhinerVisionModule REQUIRED) + +# ncnn配置 +set(NCNN_ROOT_DIR "${PROJECT_ROOT_PATH}/third_party/ncnn-20240820-lockzhiner-vision-module") # 确保third_party层级存在 +message(STATUS "Checking ncnn headers in: ${NCNN_ROOT_DIR}/include/ncnn") + +# 验证头文件存在 +if(NOT EXISTS "${NCNN_ROOT_DIR}/include/ncnn/net.h") + message(FATAL_ERROR "ncnn headers not found. Confirm the directory contains ncnn: ${NCNN_ROOT_DIR}") +endif() + +set(NCNN_INCLUDE_DIRS "${NCNN_ROOT_DIR}/include") +set(NCNN_LIBRARIES "${NCNN_ROOT_DIR}/lib/libncnn.a") + +# 配置rknpu2 +set(RKNPU2_BACKEND_BASE_DIR "${LockzhinerVisionModule_ROOT_PATH}/include/lockzhiner_vision_module/vision/deep_learning/runtime") +if(NOT EXISTS ${RKNPU2_BACKEND_BASE_DIR}) + message(FATAL_ERROR "RKNPU2 backend base dir missing: ${RKNPU2_BACKEND_BASE_DIR}") +endif() + +add_executable(Test-facepoint facepoint_detection.cc) +target_include_directories(Test-facepoint PRIVATE ${LOCKZHINER_VISION_MODULE_INCLUDE_DIRS} ${NCNN_INCLUDE_DIRS} ${rknpu2_INCLUDE_DIRS} ${RKNPU2_BACKEND_BASE_DIR}) +target_link_libraries(Test-facepoint PRIVATE ${OPENCV_LIBRARIES} ${LOCKZHINER_VISION_MODULE_LIBRARIES} ${NCNN_LIBRARIES}) + +install( + TARGETS Test-facepoint + RUNTIME DESTINATION . +) \ No newline at end of file diff --git a/Cpp_example/D14_fatigue_detection/README.md b/Cpp_example/D14_fatigue_detection/README.md new file mode 100755 index 0000000000000000000000000000000000000000..cc924380472cd1a5be9460ed9041283addc9e760 --- /dev/null +++ b/Cpp_example/D14_fatigue_detection/README.md @@ -0,0 +1,525 @@ +# 疲劳检测 + +## 1. 实验背景 + +基于计算机视觉的实时疲劳检测系统通过分析眼部状态(闭眼时长、眨眼频率)、嘴部动作(打哈欠频率)以及面部微表情变化,运用轻量化深度学习模型在嵌入式设备上实现超过90%的检测准确率。该系统能在30ms内完成单帧分析,当检测到连续闭眼超过0.5秒、打哈欠超过0.67秒或闭眼时间占比(PERCLOS)超过30%时自动触发警报,为交通运输、工业生产和医疗监护等高风险领域提供主动安全防护,有效降低因疲劳导致的事故风险。 + +## 2. 实验原理 + +### 2.1 人脸检测阶段 + +采用轻量级目标检测网络PicoDet实现实时人脸检测。PicoDet通过深度可分离卷积和特征金字塔网络优化,在保持高精度(>90% mAP)的同时显著降低计算复杂度。该模块接收输入图像,输出人脸边界框坐标,为后续关键点检测提供精准的感兴趣区域(ROI)。 + +### 2.2 关键点定位阶段 + +基于PFLD(Practical Facial Landmark Detector)模型实现106点人脸关键点检测。PFLD采用多尺度特征融合技术,通过主分支预测关键点坐标,辅助分支学习几何约束信息。该阶段将人脸区域统一缩放到112×112分辨率输入网络,输出归一化后的106个关键点坐标。 + +### 2.3 眼部疲劳分析 + +从106个关键点中提取左右眼各8个关键点,计算眼睛纵横比(EAR): + EAR = (‖p2-p6‖ + ‖p3-p5‖) / (2 × ‖p1-p4‖) +其中p1-p6为眼部轮廓点。实验设定EAR阈值0.25,当连续15帧(0.5秒@30fps)EAR低于阈值时,判定为持续性闭眼。同时计算PERCLOS指标(闭眼时间占比),10秒窗口内闭眼占比超过30%判定为眼动疲劳。 + +### 2.4 嘴部疲劳分析 + +提取嘴部20个关键点(外轮廓12点+内部结构8点),计算嘴部纵横比(MAR): + MAR = (垂直距离1 + 垂直距离2 + 垂直距离3) / (3 × 水平距离) +实验设定MAR阈值0.6,当连续20帧(0.67秒)MAR高于阈值时,判定为持续性打哈欠。嘴部关键点采用特殊拓扑结构,外轮廓点检测嘴部开合度,内部点增强哈欠状态识别鲁棒性。 + +### 2.5 多指标融合决策 + +建立基于加权逻辑的疲劳判定模型: + 疲劳状态 = OR(眼部持续性闭眼, 嘴部持续性哈欠, PERCLOS > 30%) +该模型采用并行决策机制,任一条件满足即触发"TIRED"状态。 + +## 3. 模型转换 + +```python +import os +import numpy as np +import cv2 +from rknn.api import RKNN + +def convert_onnx_to_rknn(): + # 初始化RKNN对象 + rknn = RKNN(verbose=True) + + # 模型配置 (与训练一致) + print('--> Config model') + rknn.config( + # 与训练一致:仅归一化 [0,1] + mean_values=[[0, 0, 0]], # 重要修正 + std_values=[[255, 255, 255]], # 重要修正 + target_platform='rv1106', + quantized_dtype='w8a8', # RV1106兼容类型 + quantized_algorithm='normal', + optimization_level=3, + # float_dtype='float16', # 量化模型不需要 + output_optimize=True, + ) + print('done') + + # 加载ONNX模型 - 明确指定输入输出 + print('--> Loading model') + ret = rknn.load_onnx( + model='./pfld_106.onnx', + inputs=['input'], # 使用Netron确认输入节点名 + outputs=['output'], # 使用Netron确认输出节点名 + # 使用NHWC格式 (匹配RV1106) + input_size_list=[[1, 3, 112, 112]] # NCHW格式 + ) + if ret != 0: + print('Load model failed!') + exit(ret) + print('done') + + # 构建RKNN模型 - 使用校准数据集 + print('--> Building model') + ret = rknn.build( + do_quantization=True, + dataset='./dataset.txt', + # 添加量化配置 + rknn_batch_size=1 + ) + if ret != 0: + print('Build model failed!') + exit(ret) + print('done') + + # 导出RKNN模型 + print('--> Export rknn model') + ret = rknn.export_rknn('./pfld_106.rknn') + if ret != 0: + print('Export rknn model failed!') + exit(ret) + print('done') + + # 释放RKNN对象 + rknn.release() + +if __name__ == '__main__': + convert_onnx_to_rknn() + print('Model conversion completed successfully!') +``` +根据目标平台,完成参数配置,运行程序完成转换。在完成模型转换后可以查看 rv1106 的算子支持手册,确保所有的算子是可以使用的,避免白忙活。 + +## 4. 模型部署 + +```cpp +#include +#include +#include +#include +#include +#include +#include "rknpu2_backend/rknpu2_backend.h" +#include + +using namespace cv; +using namespace std; +using namespace std::chrono; + +// 定义关键点索引 (根据106点模型) +const vector LEFT_EYE_POINTS = {35, 41, 40, 42, 39, 37, 33, 36}; // 左眼 +const vector RIGHT_EYE_POINTS = {89, 95, 94, 96, 93, 91, 87, 90}; // 右眼 + +// 嘴部 +const vector MOUTH_OUTLINE = {52, 64, 63, 71, 67, 68, 61, 58, 59, 53, 56, 55}; +const vector MOUTH_INNER = {65, 66, 62, 70, 69, 57, 60, 54}; +vector MOUTH_POINTS; + +// 计算眼睛纵横比(EAR) +float eye_aspect_ratio(const vector& eye_points) { + // 计算垂直距离 + double A = norm(eye_points[1] - eye_points[7]); + double B = norm(eye_points[2] - eye_points[6]); + double C = norm(eye_points[3] - eye_points[5]); + + // 计算水平距离 + double D = norm(eye_points[0] - eye_points[4]); + + // 防止除以零 + if (D < 1e-5) return 0.0f; + + return (float)((A + B + C) / (3.0 * D)); +} + +// 计算嘴部纵横比(MAR) +float mouth_aspect_ratio(const vector& mouth_points) { + // 关键点索引(基于MOUTH_OUTLINE中的位置) + const int LEFT_CORNER = 0; // 52 (左嘴角) + const int UPPER_CENTER = 3; // 71 (上唇中心) + const int RIGHT_CORNER = 6; // 61 (右嘴角) + const int LOWER_CENTER = 9; // 53 (下唇中心) + + // 计算垂直距离 + double A = norm(mouth_points[UPPER_CENTER] - mouth_points[LOWER_CENTER]); // 上唇中心到下唇中心 + double B = norm(mouth_points[UPPER_CENTER] - mouth_points[LEFT_CORNER]); // 上唇中心到左嘴角 + double C = norm(mouth_points[UPPER_CENTER] - mouth_points[RIGHT_CORNER]); // 上唇中心到右嘴角 + + // 计算嘴部宽度(左右嘴角距离) + double D = norm(mouth_points[LEFT_CORNER] - mouth_points[RIGHT_CORNER]); // 左嘴角到右嘴角 + + // 防止除以零 + if (D < 1e-5) return 0.0f; + + // 计算平均垂直距离与水平距离的比值 + return static_cast((A + B + C) / (3.0 * D)); +} + +int main(int argc, char** argv) +{ + // 初始化嘴部关键点 + MOUTH_POINTS.clear(); + MOUTH_POINTS.insert(MOUTH_POINTS.end(), MOUTH_OUTLINE.begin(), MOUTH_OUTLINE.end()); + MOUTH_POINTS.insert(MOUTH_POINTS.end(), MOUTH_INNER.begin(), MOUTH_INNER.end()); + + // 检查命令行参数 + if (argc != 4) { + cerr << "Usage: " << argv[0] << " \n"; + cerr << "Example: " << argv[0] << " picodet_model_dir pfld.rknn 112\n"; + return -1; + } + + const char* paddle_model_path = argv[1]; + const char* pfld_rknn_path = argv[2]; + const int pfld_size = atoi(argv[3]); // PFLD模型输入尺寸 (112) + + // 1. 初始化PaddleDet人脸检测模型 + lockzhiner_vision_module::vision::PaddleDet face_detector; + if (!face_detector.Initialize(paddle_model_path)) { + cerr << "Failed to initialize PaddleDet face detector model." << endl; + return -1; + } + + // 2. 初始化PFLD RKNN模型 + lockzhiner_vision_module::vision::RKNPU2Backend pfld_backend; + if (!pfld_backend.Initialize(pfld_rknn_path)) { + cerr << "Failed to load PFLD RKNN model." << endl; + return -1; + } + + // 获取输入张量信息 + const auto& input_tensor = pfld_backend.GetInputTensor(0); + const vector input_dims = input_tensor.GetDims(); + const float input_scale = input_tensor.GetScale(); + const int input_zp = input_tensor.GetZp(); + + // 打印输入信息 + cout << "PFLD Input Info:" << endl; + cout << " Dimensions: "; + for (auto dim : input_dims) cout << dim << " "; + cout << "\n Scale: " << input_scale << " Zero Point: " << input_zp << endl; + + // 3. 初始化Edit模块 + lockzhiner_vision_module::edit::Edit edit; + if (!edit.StartAndAcceptConnection()) { + cerr << "Error: Failed to start and accept connection." << endl; + return EXIT_FAILURE; + } + cout << "Device connected successfully." << endl; + + // 4. 打开摄像头 + VideoCapture cap; + cap.set(CAP_PROP_FRAME_WIDTH, 640); + cap.set(CAP_PROP_FRAME_HEIGHT, 480); + cap.open(0); + + if (!cap.isOpened()) { + cerr << "Error: Could not open camera." << endl; + return -1; + } + + Mat frame; + const int num_landmarks = 106; + int frame_count = 0; + const int debug_freq = 10; // 每10帧打印一次调试信息 + + // ================== 疲劳检测参数 ================== + const float EAR_THRESHOLD = 0.25f; // 眼睛纵横比阈值 + const float MAR_THRESHOLD = 0.6f; // 嘴部纵横比阈值 + + const int EYE_CLOSED_FRAMES = 15; // 闭眼持续帧数阈值 + const int MOUTH_OPEN_FRAMES = 20; // 张嘴持续帧数阈值 + + int consecutive_eye_closed = 0; // 连续闭眼帧数 + int consecutive_mouth_open = 0; // 连续张嘴帧数 + + bool is_tired = false; // 当前疲劳状态 + + deque eye_state_buffer; // 用于PERCLOS计算 + const int PERCLOS_WINDOW = 200; // PERCLOS计算窗口大小 + float perclos = 0.0f; // 闭眼时间占比 + + // 疲劳状态文本 + const string TIRED_TEXT = "TIRED"; + const string NORMAL_TEXT = "NORMAL"; + const Scalar TIRED_COLOR = Scalar(0, 0, 255); // 红色 + const Scalar NORMAL_COLOR = Scalar(0, 255, 0); // 绿色 + + while (true) { + // 5. 捕获一帧图像 + cap >> frame; + if (frame.empty()) { + cerr << "Warning: Captured an empty frame." << endl; + continue; + } + + // 6. 人脸检测 + auto start_det = high_resolution_clock::now(); + auto face_results = face_detector.Predict(frame); + auto end_det = high_resolution_clock::now(); + auto det_duration = duration_cast(end_det - start_det); + + Mat result_image = frame.clone(); + bool pfld_debug_printed = false; + + // 7. 处理每个检测到的人脸 + for (const auto& face : face_results) { + // 跳过非人脸检测结果 + if (face.label_id != 0) continue; + + // 提取人脸区域 + Rect face_rect = face.box; + + // 确保人脸区域在图像范围内 + face_rect.x = max(0, face_rect.x); + face_rect.y = max(0, face_rect.y); + face_rect.width = min(face_rect.width, frame.cols - face_rect.x); + face_rect.height = min(face_rect.height, frame.rows - face_rect.y); + + if (face_rect.width <= 10 || face_rect.height <= 10) continue; + + // 绘制人脸框 + rectangle(result_image, face_rect, Scalar(0, 255, 0), 2); + + // 8. 关键点检测 + Mat face_roi = frame(face_rect); + Mat face_resized; + resize(face_roi, face_resized, Size(pfld_size, pfld_size)); + + // 8.1 预处理 (转换为RKNN输入格式) + cvtColor(face_resized, face_resized, COLOR_BGR2RGB); + + // 8.2 设置输入数据 + void* input_data = input_tensor.GetData(); + size_t required_size = input_tensor.GetElemsBytes(); + size_t actual_size = face_resized.total() * face_resized.elemSize(); + + if (actual_size != required_size) { + cerr << "Input size mismatch! Required: " << required_size + << ", Actual: " << actual_size << endl; + continue; + } + + memcpy(input_data, face_resized.data, actual_size); + + // 8.3 执行推理 + auto start_pfld = high_resolution_clock::now(); + bool success = pfld_backend.Run(); + auto end_pfld = high_resolution_clock::now(); + auto pfld_duration = duration_cast(end_pfld - start_pfld); + + if (!success) { + cerr << "PFLD inference failed!" << endl; + continue; + } + + // 8.4 获取输出结果 + const auto& output_tensor = pfld_backend.GetOutputTensor(0); + const float output_scale = output_tensor.GetScale(); + const int output_zp = output_tensor.GetZp(); + const int8_t* output_data = static_cast(output_tensor.GetData()); + const vector output_dims = output_tensor.GetDims(); + + // 计算输出元素数量 + size_t total_elems = 1; + for (auto dim : output_dims) total_elems *= dim; + + // 打印输出信息 (调试) + if ((frame_count % debug_freq == 0 || !pfld_debug_printed) && !face_results.empty()) { + cout << "\n--- PFLD Output Debug ---" << endl; + cout << "Output Scale: " << output_scale << " Zero Point: " << output_zp << endl; + cout << "Output Dimensions: "; + for (auto dim : output_dims) cout << dim << " "; + cout << "\nTotal Elements: " << total_elems << endl; + + cout << "First 10 output values: "; + for (int i = 0; i < min(10, static_cast(total_elems)); i++) { + cout << (int)output_data[i] << " "; + } + cout << endl; + pfld_debug_printed = true; + } + + // 9. 处理关键点结果 + vector landmarks; + for (int i = 0; i < num_landmarks; i++) { + // 反量化输出 + float x = (output_data[i * 2] - output_zp) * output_scale; + float y = (output_data[i * 2 + 1] - output_zp) * output_scale; + + // 关键修正: 先缩放到112x112图像坐标 + x = x * pfld_size; + y = y * pfld_size; + + // 映射到原始图像坐标 + float scale_x = static_cast(face_rect.width) / pfld_size; + float scale_y = static_cast(face_rect.height) / pfld_size; + x = x * scale_x + face_rect.x; + y = y * scale_y + face_rect.y; + + landmarks.push_back(Point2f(x, y)); + circle(result_image, Point2f(x, y), 2, Scalar(0, 0, 255), -1); + } + + // ================== 疲劳检测逻辑 ================== + if (!landmarks.empty()) { + + // 9.1 提取眼部关键点 + vector left_eye, right_eye; + for (int idx : LEFT_EYE_POINTS) { + if (idx < landmarks.size()) { + left_eye.push_back(landmarks[idx]); + } + } + for (int idx : RIGHT_EYE_POINTS) { + if (idx < landmarks.size()) { + right_eye.push_back(landmarks[idx]); + } + } + + // 9.3 提取嘴部关键点 + vector mouth; + for (int idx : MOUTH_POINTS) { + if (idx < landmarks.size()) { + mouth.push_back(landmarks[idx]); + } + } + + // 9.4 计算眼部纵横比(EAR) + float ear_left = 0.0f, ear_right = 0.0f, ear_avg = 0.0f; + if (!left_eye.empty() && !right_eye.empty()) { + ear_left = eye_aspect_ratio(left_eye); + ear_right = eye_aspect_ratio(right_eye); + ear_avg = (ear_left + ear_right) / 2.0f; + } + + // 9.5 计算嘴部纵横比(MAR) + float mar = 0.0f; + if (!mouth.empty()) { + mar = mouth_aspect_ratio(mouth); + } + + // 9.6 更新PERCLOS缓冲区 + if (eye_state_buffer.size() >= PERCLOS_WINDOW) { + eye_state_buffer.pop_front(); + } + eye_state_buffer.push_back(ear_avg < EAR_THRESHOLD); + + // 计算PERCLOS (闭眼时间占比) + int closed_count = 0; + for (bool closed : eye_state_buffer) { + if (closed) closed_count++; + } + perclos = static_cast(closed_count) / eye_state_buffer.size(); + + // 9.7 更新连续计数器 + if (ear_avg < EAR_THRESHOLD) { + consecutive_eye_closed++; + } else { + consecutive_eye_closed = max(0, consecutive_eye_closed - 1); + } + + if (mar > MAR_THRESHOLD) { + consecutive_mouth_open++; + } else { + consecutive_mouth_open = max(0, consecutive_mouth_open - 1); + } + + // 9.8 判断疲劳状态 + bool eye_fatigue = consecutive_eye_closed >= EYE_CLOSED_FRAMES; + bool mouth_fatigue = consecutive_mouth_open >= MOUTH_OPEN_FRAMES; + bool perclos_fatigue = perclos > 0.5f; // PERCLOS > 50% + + is_tired = eye_fatigue || mouth_fatigue || perclos_fatigue; + + // 9.9 在图像上标注疲劳状态 + string status_text = is_tired ? TIRED_TEXT : NORMAL_TEXT; + Scalar status_color = is_tired ? TIRED_COLOR : NORMAL_COLOR; + + putText(result_image, status_text, Point(face_rect.x, face_rect.y - 30), + FONT_HERSHEY_SIMPLEX, 0.8, status_color, 2); + + // 9.10 显示检测指标 + string info = format("EAR: %.2f MAR: %.2f PERCLOS: %.1f%%", + ear_avg, mar, perclos*100); + putText(result_image, info, Point(face_rect.x, face_rect.y - 60), + FONT_HERSHEY_SIMPLEX, 0.5, Scalar(200, 200, 0), 1); + } + } + + // 10. 显示性能信息 + auto end_total = high_resolution_clock::now(); + auto total_duration = duration_cast(end_total - start_det); + + string info = "Faces: " + to_string(face_results.size()) + + " | Det: " + to_string(det_duration.count()) + "ms" + + " | Total: " + to_string(total_duration.count()) + "ms"; + putText(result_image, info, Point(10, 30), FONT_HERSHEY_SIMPLEX, 0.6, Scalar(0, 255, 0), 2); + + // 11. 显示结果 + edit.Print(result_image); + + // 帧计数器更新 + frame_count = (frame_count + 1) % debug_freq; + + // 按ESC退出 + if (waitKey(1) == 27) break; + } + + cap.release(); + return 0; +} +``` + +## 5. 编译程序 + +使用 Docker Destop 打开 LockzhinerVisionModule 容器并执行以下命令来编译项目 +```bash +# 进入Demo所在目录 +cd /LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/D14_fatigue_Detection +# 创建编译目录 +rm -rf build && mkdir build && cd build +# 配置交叉编译工具链 +export TOOLCHAIN_ROOT_PATH="/LockzhinerVisionModuleWorkSpace/arm-rockchip830-linux-uclibcgnueabihf" +# 使用cmake配置项目 +cmake .. +# 执行编译项目 +make -j8 && make install +``` + +在执行完上述命令后,会在build目录下生成可执行文件。 + +## 6. 执行结果 +### 6.1 运行前准备 + +- 请确保你已经下载了 [凌智视觉模块人脸检测模型权重文件](https://gitee.com/LockzhinerAI/LockzhinerVisionModule/releases/download/v0.0.3/LZ-Face.rknn) +- 请确保你已经下载了 [凌智视觉模块人脸关键点检测模型权重文件](https://gitee.com/LockzhinerAI/LockzhinerVisionModule/releases/download/v0.0.6/pfld_106.rknn) + +### 6.2 运行过程 +```shell +chmod 777 Test-facepoint +./Test-facepoint face_detection.rknn pfld_106.rknn 112 +``` + +### 6.3 运行效果 +- 测试结果 + +![](./images/result_1.png) + +![](./images/result_2.png) + +## 7. 实验总结 + +本实验成功构建了一套高效实时的监测系统:基于PicoDet实现毫秒级人脸检测,结合PFLD精准定位106个关键点,通过眼部8关键点计算EAR值、嘴部20关键点计算MAR值,融合PERCLOS指标构建三维决策模型。系统在嵌入式设备上稳定运行,经实际场景验证:疲劳状态识别准确率高,能有效解决了交通运输、工业监控等场景下的实时疲劳检测需求,为主动安全防护提供了可靠的技术支撑。 \ No newline at end of file diff --git a/Cpp_example/D14_fatigue_detection/facepoint_detection.cc b/Cpp_example/D14_fatigue_detection/facepoint_detection.cc new file mode 100644 index 0000000000000000000000000000000000000000..36cdf57c0a0f157d9643b765418a2d63ff5bffe6 --- /dev/null +++ b/Cpp_example/D14_fatigue_detection/facepoint_detection.cc @@ -0,0 +1,375 @@ +#include +#include +#include +#include +#include +#include +#include "rknpu2_backend/rknpu2_backend.h" +#include + +using namespace cv; +using namespace std; +using namespace std::chrono; + +// 定义关键点索引 (根据106点模型) +const vector LEFT_EYE_POINTS = {35, 41, 40, 42, 39, 37, 33, 36}; // 左眼 +const vector RIGHT_EYE_POINTS = {89, 95, 94, 96, 93, 91, 87, 90}; // 右眼 + +// 嘴部 +const vector MOUTH_OUTLINE = {52, 64, 63, 71, 67, 68, 61, 58, 59, 53, 56, 55}; +const vector MOUTH_INNER = {65, 66, 62, 70, 69, 57, 60, 54}; +vector MOUTH_POINTS; + +// 计算眼睛纵横比(EAR) +float eye_aspect_ratio(const vector& eye_points) { + // 计算垂直距离 + double A = norm(eye_points[1] - eye_points[7]); + double B = norm(eye_points[2] - eye_points[6]); + double C = norm(eye_points[3] - eye_points[5]); + + // 计算水平距离 + double D = norm(eye_points[0] - eye_points[4]); + + // 防止除以零 + if (D < 1e-5) return 0.0f; + + return (float)((A + B + C) / (3.0 * D)); +} + +// 计算嘴部纵横比(MAR) +float mouth_aspect_ratio(const vector& mouth_points) { + // 关键点索引(基于MOUTH_OUTLINE中的位置) + const int LEFT_CORNER = 0; // 52 (左嘴角) + const int UPPER_CENTER = 3; // 71 (上唇中心) + const int RIGHT_CORNER = 6; // 61 (右嘴角) + const int LOWER_CENTER = 9; // 53 (下唇中心) + + // 计算垂直距离 + double A = norm(mouth_points[UPPER_CENTER] - mouth_points[LOWER_CENTER]); // 上唇中心到下唇中心 + double B = norm(mouth_points[UPPER_CENTER] - mouth_points[LEFT_CORNER]); // 上唇中心到左嘴角 + double C = norm(mouth_points[UPPER_CENTER] - mouth_points[RIGHT_CORNER]); // 上唇中心到右嘴角 + + // 计算嘴部宽度(左右嘴角距离) + double D = norm(mouth_points[LEFT_CORNER] - mouth_points[RIGHT_CORNER]); // 左嘴角到右嘴角 + + // 防止除以零 + if (D < 1e-5) return 0.0f; + + // 计算平均垂直距离与水平距离的比值 + return static_cast((A + B + C) / (3.0 * D)); +} + +int main(int argc, char** argv) +{ + // 初始化嘴部关键点 + MOUTH_POINTS.clear(); + MOUTH_POINTS.insert(MOUTH_POINTS.end(), MOUTH_OUTLINE.begin(), MOUTH_OUTLINE.end()); + MOUTH_POINTS.insert(MOUTH_POINTS.end(), MOUTH_INNER.begin(), MOUTH_INNER.end()); + + // 检查命令行参数 + if (argc != 4) { + cerr << "Usage: " << argv[0] << " \n"; + cerr << "Example: " << argv[0] << " picodet_model_dir pfld.rknn 112\n"; + return -1; + } + + const char* paddle_model_path = argv[1]; + const char* pfld_rknn_path = argv[2]; + const int pfld_size = atoi(argv[3]); // PFLD模型输入尺寸 (112) + + // 1. 初始化PaddleDet人脸检测模型 + lockzhiner_vision_module::vision::PaddleDet face_detector; + if (!face_detector.Initialize(paddle_model_path)) { + cerr << "Failed to initialize PaddleDet face detector model." << endl; + return -1; + } + + // 2. 初始化PFLD RKNN模型 + lockzhiner_vision_module::vision::RKNPU2Backend pfld_backend; + if (!pfld_backend.Initialize(pfld_rknn_path)) { + cerr << "Failed to load PFLD RKNN model." << endl; + return -1; + } + + // 获取输入张量信息 + const auto& input_tensor = pfld_backend.GetInputTensor(0); + const vector input_dims = input_tensor.GetDims(); + const float input_scale = input_tensor.GetScale(); + const int input_zp = input_tensor.GetZp(); + + // 打印输入信息 + cout << "PFLD Input Info:" << endl; + cout << " Dimensions: "; + for (auto dim : input_dims) cout << dim << " "; + cout << "\n Scale: " << input_scale << " Zero Point: " << input_zp << endl; + + // 3. 初始化Edit模块 + lockzhiner_vision_module::edit::Edit edit; + if (!edit.StartAndAcceptConnection()) { + cerr << "Error: Failed to start and accept connection." << endl; + return EXIT_FAILURE; + } + cout << "Device connected successfully." << endl; + + // 4. 打开摄像头 + VideoCapture cap; + cap.set(CAP_PROP_FRAME_WIDTH, 640); + cap.set(CAP_PROP_FRAME_HEIGHT, 480); + cap.open(0); + + if (!cap.isOpened()) { + cerr << "Error: Could not open camera." << endl; + return -1; + } + + Mat frame; + const int num_landmarks = 106; + int frame_count = 0; + const int debug_freq = 10; // 每10帧打印一次调试信息 + + // ================== 疲劳检测参数 ================== + const float EAR_THRESHOLD = 0.25f; // 眼睛纵横比阈值 + const float MAR_THRESHOLD = 0.6f; // 嘴部纵横比阈值 + + const int EYE_CLOSED_FRAMES = 20; // 闭眼持续帧数阈值 + const int MOUTH_OPEN_FRAMES = 25; // 张嘴持续帧数阈值 + + int consecutive_eye_closed = 0; // 连续闭眼帧数 + int consecutive_mouth_open = 0; // 连续张嘴帧数 + + bool is_tired = false; // 当前疲劳状态 + + deque eye_state_buffer; // 用于PERCLOS计算 + const int PERCLOS_WINDOW = 200; // PERCLOS计算窗口大小 + float perclos = 0.0f; // 闭眼时间占比 + + // 疲劳状态文本 + const string TIRED_TEXT = "TIRED"; + const string NORMAL_TEXT = "NORMAL"; + const Scalar TIRED_COLOR = Scalar(0, 0, 255); // 红色 + const Scalar NORMAL_COLOR = Scalar(0, 255, 0); // 绿色 + + while (true) { + // 5. 捕获一帧图像 + cap >> frame; + if (frame.empty()) { + cerr << "Warning: Captured an empty frame." << endl; + continue; + } + + // 6. 人脸检测 + auto start_det = high_resolution_clock::now(); + auto face_results = face_detector.Predict(frame); + auto end_det = high_resolution_clock::now(); + auto det_duration = duration_cast(end_det - start_det); + + Mat result_image = frame.clone(); + bool pfld_debug_printed = false; + + // 7. 处理每个检测到的人脸 + for (const auto& face : face_results) { + // 跳过非人脸检测结果 + if (face.label_id != 0) continue; + + // 提取人脸区域 + Rect face_rect = face.box; + + // 确保人脸区域在图像范围内 + face_rect.x = max(0, face_rect.x); + face_rect.y = max(0, face_rect.y); + face_rect.width = min(face_rect.width, frame.cols - face_rect.x); + face_rect.height = min(face_rect.height, frame.rows - face_rect.y); + + if (face_rect.width <= 10 || face_rect.height <= 10) continue; + + // 绘制人脸框 + rectangle(result_image, face_rect, Scalar(0, 255, 0), 2); + + // 8. 关键点检测 + Mat face_roi = frame(face_rect); + Mat face_resized; + resize(face_roi, face_resized, Size(pfld_size, pfld_size)); + + // 8.1 预处理 (转换为RKNN输入格式) + cvtColor(face_resized, face_resized, COLOR_BGR2RGB); + + // 8.2 设置输入数据 + void* input_data = input_tensor.GetData(); + size_t required_size = input_tensor.GetElemsBytes(); + size_t actual_size = face_resized.total() * face_resized.elemSize(); + + if (actual_size != required_size) { + cerr << "Input size mismatch! Required: " << required_size + << ", Actual: " << actual_size << endl; + continue; + } + + memcpy(input_data, face_resized.data, actual_size); + + // 8.3 执行推理 + auto start_pfld = high_resolution_clock::now(); + bool success = pfld_backend.Run(); + auto end_pfld = high_resolution_clock::now(); + auto pfld_duration = duration_cast(end_pfld - start_pfld); + + if (!success) { + cerr << "PFLD inference failed!" << endl; + continue; + } + + // 8.4 获取输出结果 + const auto& output_tensor = pfld_backend.GetOutputTensor(0); + const float output_scale = output_tensor.GetScale(); + const int output_zp = output_tensor.GetZp(); + const int8_t* output_data = static_cast(output_tensor.GetData()); + const vector output_dims = output_tensor.GetDims(); + + // 计算输出元素数量 + size_t total_elems = 1; + for (auto dim : output_dims) total_elems *= dim; + + // 打印输出信息 (调试) + if ((frame_count % debug_freq == 0 || !pfld_debug_printed) && !face_results.empty()) { + cout << "\n--- PFLD Output Debug ---" << endl; + cout << "Output Scale: " << output_scale << " Zero Point: " << output_zp << endl; + cout << "Output Dimensions: "; + for (auto dim : output_dims) cout << dim << " "; + cout << "\nTotal Elements: " << total_elems << endl; + + cout << "First 10 output values: "; + for (int i = 0; i < min(10, static_cast(total_elems)); i++) { + cout << (int)output_data[i] << " "; + } + cout << endl; + pfld_debug_printed = true; + } + + // 9. 处理关键点结果 + vector landmarks; + for (int i = 0; i < num_landmarks; i++) { + // 反量化输出 + float x = (output_data[i * 2] - output_zp) * output_scale; + float y = (output_data[i * 2 + 1] - output_zp) * output_scale; + + // 关键修正: 先缩放到112x112图像坐标 + x = x * pfld_size; + y = y * pfld_size; + + // 映射到原始图像坐标 + float scale_x = static_cast(face_rect.width) / pfld_size; + float scale_y = static_cast(face_rect.height) / pfld_size; + x = x * scale_x + face_rect.x; + y = y * scale_y + face_rect.y; + + landmarks.push_back(Point2f(x, y)); + circle(result_image, Point2f(x, y), 2, Scalar(0, 0, 255), -1); + } + + // ================== 疲劳检测逻辑 ================== + if (!landmarks.empty()) { + + // 9.1 提取眼部关键点 + vector left_eye, right_eye; + for (int idx : LEFT_EYE_POINTS) { + if (idx < landmarks.size()) { + left_eye.push_back(landmarks[idx]); + } + } + for (int idx : RIGHT_EYE_POINTS) { + if (idx < landmarks.size()) { + right_eye.push_back(landmarks[idx]); + } + } + + // 9.3 提取嘴部关键点 + vector mouth; + for (int idx : MOUTH_POINTS) { + if (idx < landmarks.size()) { + mouth.push_back(landmarks[idx]); + } + } + + // 9.4 计算眼部纵横比(EAR) + float ear_left = 0.0f, ear_right = 0.0f, ear_avg = 0.0f; + if (!left_eye.empty() && !right_eye.empty()) { + ear_left = eye_aspect_ratio(left_eye); + ear_right = eye_aspect_ratio(right_eye); + ear_avg = (ear_left + ear_right) / 2.0f; + } + + // 9.5 计算嘴部纵横比(MAR) + float mar = 0.0f; + if (!mouth.empty()) { + mar = mouth_aspect_ratio(mouth); + } + + // 9.6 更新PERCLOS缓冲区 + if (eye_state_buffer.size() >= PERCLOS_WINDOW) { + eye_state_buffer.pop_front(); + } + eye_state_buffer.push_back(ear_avg < EAR_THRESHOLD); + + // 计算PERCLOS (闭眼时间占比) + int closed_count = 0; + for (bool closed : eye_state_buffer) { + if (closed) closed_count++; + } + perclos = static_cast(closed_count) / eye_state_buffer.size(); + + // 9.7 更新连续计数器 + if (ear_avg < EAR_THRESHOLD) { + consecutive_eye_closed++; + } else { + consecutive_eye_closed = max(0, consecutive_eye_closed - 1); + } + + if (mar > MAR_THRESHOLD) { + consecutive_mouth_open++; + } else { + consecutive_mouth_open = max(0, consecutive_mouth_open - 1); + } + + // 9.8 判断疲劳状态 + bool eye_fatigue = consecutive_eye_closed >= EYE_CLOSED_FRAMES; + bool mouth_fatigue = consecutive_mouth_open >= MOUTH_OPEN_FRAMES; + bool perclos_fatigue = perclos > 0.5f; // PERCLOS > 50% + + is_tired = eye_fatigue || mouth_fatigue || perclos_fatigue; + + // 9.9 在图像上标注疲劳状态 + string status_text = is_tired ? TIRED_TEXT : NORMAL_TEXT; + Scalar status_color = is_tired ? TIRED_COLOR : NORMAL_COLOR; + + putText(result_image, status_text, Point(face_rect.x, face_rect.y - 30), + FONT_HERSHEY_SIMPLEX, 0.8, status_color, 2); + + // 9.10 显示检测指标 + string info = format("EAR: %.2f MAR: %.2f PERCLOS: %.1f%%", + ear_avg, mar, perclos*100); + putText(result_image, info, Point(face_rect.x, face_rect.y - 60), + FONT_HERSHEY_SIMPLEX, 0.5, Scalar(200, 200, 0), 1); + } + } + + // 10. 显示性能信息 + auto end_total = high_resolution_clock::now(); + auto total_duration = duration_cast(end_total - start_det); + + string info = "Faces: " + to_string(face_results.size()) + + " | Det: " + to_string(det_duration.count()) + "ms" + + " | Total: " + to_string(total_duration.count()) + "ms"; + putText(result_image, info, Point(10, 30), FONT_HERSHEY_SIMPLEX, 0.6, Scalar(0, 255, 0), 2); + + // 11. 显示结果 + edit.Print(result_image); + + // 帧计数器更新 + frame_count = (frame_count + 1) % debug_freq; + + // 按ESC退出 + if (waitKey(1) == 27) break; + } + + cap.release(); + return 0; +} \ No newline at end of file diff --git a/Cpp_example/D14_fatigue_detection/images/result.mp4 b/Cpp_example/D14_fatigue_detection/images/result.mp4 new file mode 100755 index 0000000000000000000000000000000000000000..91db167c15c86f2a799044b72918f89d0b18e30c Binary files /dev/null and b/Cpp_example/D14_fatigue_detection/images/result.mp4 differ diff --git a/Cpp_example/D14_fatigue_detection/images/result_1.png b/Cpp_example/D14_fatigue_detection/images/result_1.png new file mode 100755 index 0000000000000000000000000000000000000000..dc06d17e0619d2ce5330402249f2de18781cf67f Binary files /dev/null and b/Cpp_example/D14_fatigue_detection/images/result_1.png differ diff --git a/Cpp_example/D14_fatigue_detection/images/result_2.png b/Cpp_example/D14_fatigue_detection/images/result_2.png new file mode 100755 index 0000000000000000000000000000000000000000..dd54955d4aaa6e51c6d29be68a61ffb99173d4c7 Binary files /dev/null and b/Cpp_example/D14_fatigue_detection/images/result_2.png differ diff --git a/Cpp_example/D14_fatigue_detection/images/test.mp4 b/Cpp_example/D14_fatigue_detection/images/test.mp4 new file mode 100755 index 0000000000000000000000000000000000000000..bc2ffb18f9ea15a3dcd4d5c945f265b82b39c648 Binary files /dev/null and b/Cpp_example/D14_fatigue_detection/images/test.mp4 differ diff --git a/Cpp_example/F01_diansai/CMakeLists.txt b/Cpp_example/F01_diansai/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..2d519a7b3a3b1d337e5059fb9121f3fde146fbea --- /dev/null +++ b/Cpp_example/F01_diansai/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.10) + +project(D01_test_detection) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 定义项目根目录路径 +set(PROJECT_ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../..") +message("PROJECT_ROOT_PATH = " ${PROJECT_ROOT_PATH}) + +include("${PROJECT_ROOT_PATH}/toolchains/arm-rockchip830-linux-uclibcgnueabihf.toolchain.cmake") + +# 定义 OpenCV SDK 路径 +set(OpenCV_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/opencv-mobile-4.10.0-lockzhiner-vision-module") +set(OpenCV_DIR "${OpenCV_ROOT_PATH}/lib/cmake/opencv4") +find_package(OpenCV REQUIRED) +set(OPENCV_LIBRARIES "${OpenCV_LIBS}") + +# 定义 LockzhinerVisionModule SDK 路径 +set(LockzhinerVisionModule_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/lockzhiner_vision_module_sdk") +set(LockzhinerVisionModule_DIR "${LockzhinerVisionModule_ROOT_PATH}/lib/cmake/lockzhiner_vision_module") +find_package(LockzhinerVisionModule REQUIRED) + +add_executable(Test-detection test_detection.cc) +target_include_directories(Test-detection PRIVATE ${LOCKZHINER_VISION_MODULE_INCLUDE_DIRS}) +target_link_libraries(Test-detection PRIVATE ${OPENCV_LIBRARIES} ${LOCKZHINER_VISION_MODULE_LIBRARIES}) + +install( + TARGETS Test-detection + RUNTIME DESTINATION . +) \ No newline at end of file diff --git a/Cpp_example/F01_diansai/data/1.0.png b/Cpp_example/F01_diansai/data/1.0.png new file mode 100755 index 0000000000000000000000000000000000000000..92096d38dfc0d92ded54b96cd62afba4dbef36be Binary files /dev/null and b/Cpp_example/F01_diansai/data/1.0.png differ diff --git a/Cpp_example/F01_diansai/data/1.1.png b/Cpp_example/F01_diansai/data/1.1.png new file mode 100755 index 0000000000000000000000000000000000000000..ad620117369c9706ed5f1040548c8308ff10bf80 Binary files /dev/null and b/Cpp_example/F01_diansai/data/1.1.png differ diff --git a/Cpp_example/F01_diansai/data/1.2.png b/Cpp_example/F01_diansai/data/1.2.png new file mode 100755 index 0000000000000000000000000000000000000000..d5f3a5d4e89c7d88a82cdec60cd72bcee581cd54 Binary files /dev/null and b/Cpp_example/F01_diansai/data/1.2.png differ diff --git a/Cpp_example/F01_diansai/data/1.3.png b/Cpp_example/F01_diansai/data/1.3.png new file mode 100755 index 0000000000000000000000000000000000000000..41683a9dc81aa9c73e755ed205271b5c24637c9a Binary files /dev/null and b/Cpp_example/F01_diansai/data/1.3.png differ diff --git a/Cpp_example/F01_diansai/data/1.4.png b/Cpp_example/F01_diansai/data/1.4.png new file mode 100755 index 0000000000000000000000000000000000000000..101fb149d1cc9d6ff932fae1493719828999990b Binary files /dev/null and b/Cpp_example/F01_diansai/data/1.4.png differ diff --git a/Cpp_example/F01_diansai/data/1.5.png b/Cpp_example/F01_diansai/data/1.5.png new file mode 100755 index 0000000000000000000000000000000000000000..46175c605d352ede30c88a2b8e0b6358aedcde7b Binary files /dev/null and b/Cpp_example/F01_diansai/data/1.5.png differ diff --git a/Cpp_example/F01_diansai/data/1.6.png b/Cpp_example/F01_diansai/data/1.6.png new file mode 100755 index 0000000000000000000000000000000000000000..6c18feadf411aa2f0b839680f0bd9bebba94148d Binary files /dev/null and b/Cpp_example/F01_diansai/data/1.6.png differ diff --git a/Cpp_example/F01_diansai/data/1.7.png b/Cpp_example/F01_diansai/data/1.7.png new file mode 100755 index 0000000000000000000000000000000000000000..4ffe6f1de863e2c5e8354159aafda7e5bd52c418 Binary files /dev/null and b/Cpp_example/F01_diansai/data/1.7.png differ diff --git a/Cpp_example/F01_diansai/data/1.8.png b/Cpp_example/F01_diansai/data/1.8.png new file mode 100755 index 0000000000000000000000000000000000000000..f40960d606e35caeb2cc44aa5523dd4de7d83da1 Binary files /dev/null and b/Cpp_example/F01_diansai/data/1.8.png differ diff --git a/Cpp_example/F01_diansai/data/1.9.png b/Cpp_example/F01_diansai/data/1.9.png new file mode 100755 index 0000000000000000000000000000000000000000..9ee36c25984638fe3d750c932ad2b92da5ea67a5 Binary files /dev/null and b/Cpp_example/F01_diansai/data/1.9.png differ diff --git a/Cpp_example/F01_diansai/data/2.0.png b/Cpp_example/F01_diansai/data/2.0.png new file mode 100755 index 0000000000000000000000000000000000000000..df3e5d6ed3de95d7b374d11564df3c28782a2a9a Binary files /dev/null and b/Cpp_example/F01_diansai/data/2.0.png differ diff --git a/Cpp_example/F01_diansai/test_detection.cc b/Cpp_example/F01_diansai/test_detection.cc new file mode 100644 index 0000000000000000000000000000000000000000..62339056156100c0c92d4bd12a30aa811f04f4ca --- /dev/null +++ b/Cpp_example/F01_diansai/test_detection.cc @@ -0,0 +1,175 @@ +#include +#include +#include +#include +#include +#include + +using namespace std::chrono; + +void printUsage(const char* programName) { + std::cerr << "Usage: " << programName << " [image_path]" << std::endl; + std::cerr << "Modes:" << std::endl; + std::cerr << " -c, --camera : Use camera for detection" << std::endl; + std::cerr << " -i, --image : Detect objects in an image" << std::endl; + std::cerr << "Examples:" << std::endl; + std::cerr << " " << programName << " -c model_path # Camera detection mode" << std::endl; + std::cerr << " " << programName << " -i model_path image.jpg # Image detection mode" << std::endl; +} + +int cameraDetectionMode(const std::string& modelPath) { + // 初始化模型 + lockzhiner_vision_module::vision::PaddleDet model; + if (!model.Initialize(modelPath)) { + std::cout << "Failed to initialize model." << std::endl; + return 1; + } + + // 初始化编辑模块 + lockzhiner_vision_module::edit::Edit edit; + if (!edit.StartAndAcceptConnection()) { + std::cerr << "Error: Failed to start and accept connection." << std::endl; + return EXIT_FAILURE; + } + std::cout << "Device connected successfully." << std::endl; + + // 打开摄像头 + cv::VideoCapture cap; + cap.set(cv::CAP_PROP_FRAME_WIDTH, 640); + cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480); + cap.open(0); + + if (!cap.isOpened()) { + std::cerr << "Error: Could not open camera." << std::endl; + return 1; + } + + cv::Mat input_mat; + while (true) { + cap >> input_mat; + if (input_mat.empty()) { + std::cerr << "Warning: Captured an empty frame." << std::endl; + continue; + } + + // 执行检测 + high_resolution_clock::time_point start_time = high_resolution_clock::now(); + auto results = model.Predict(input_mat); + high_resolution_clock::time_point end_time = high_resolution_clock::now(); + + // 计算推理时间 + auto time_span = duration_cast(end_time - start_time); + std::cout << "Inference time: " << time_span.count() << " ms, " + << "Detected objects: " << results.size() << std::endl; + + // 计算并输出每个检测框的面积 + for (size_t i = 0; i < results.size(); ++i) { + const auto& bbox = results[i].bbox; + float width = bbox[2] - bbox[0]; + float height = bbox[3] - bbox[1]; + float area = width * height; + std::cout << "Object " << i + 1 << ": " + << "Area = " << area << " pixels" + << " (Position: [" << bbox[0] << ", " << bbox[1] << ", " << bbox[2] << ", " << bbox[3] << "])" + << std::endl; + } + + // 可视化结果 + cv::Mat output_image; + lockzhiner_vision_module::vision::Visualize(input_mat, output_image, results); + edit.Print(output_image); + + // 按ESC退出 + if (cv::waitKey(1) == 27) + break; + } + + cap.release(); + return 0; +} + +int imageDetectionMode(const std::string& modelPath, const std::string& imagePath) { + // 初始化模型 + lockzhiner_vision_module::vision::PaddleDet model; + if (!model.Initialize(modelPath)) { + std::cout << "Failed to initialize model." << std::endl; + return 1; + } + + // 读取图片 + cv::Mat input_mat = cv::imread(imagePath); + if (input_mat.empty()) { + std::cerr << "Error: Could not read image at " << imagePath << std::endl; + return 1; + } + + // 执行检测 + high_resolution_clock::time_point start_time = high_resolution_clock::now(); + auto results = model.Predict(input_mat); + high_resolution_clock::time_point end_time = high_resolution_clock::now(); + + // 计算推理时间 + auto time_span = duration_cast(end_time - start_time); + std::cout << "Inference time: " << time_span.count() << " ms" << std::endl; + std::cout << "Detected objects: " << results.size() << std::endl; + + // 计算并输出每个检测框的面积 + for (size_t i = 0; i < results.size(); ++i) { + const auto& bbox = results[i].bbox; + float width = bbox[2] - bbox[0]; + float height = bbox[3] - bbox[1]; + float area = width * height; + std::cout << "Object " << i + 1 << ": " + << "Area = " << area << " pixels" + << " (Position: [" << bbox[0] << ", " << bbox[1] << ", " << bbox[2] << ", " << bbox[3] << "])" + << std::endl; + } + + // 可视化结果 + cv::Mat output_image; + lockzhiner_vision_module::vision::Visualize(input_mat, output_image, results); + + // 显示结果 + cv::imshow("Detection Result", output_image); + cv::waitKey(0); + + return 0; +} + +int main(int argc, char *argv[]) +{ + // 检查参数数量 + if (argc < 3) { + printUsage(argv[0]); + return 1; + } + + // 解析模式 + std::string mode = argv[1]; + std::string modelPath = argv[2]; + + if (mode == "-c" || mode == "--camera") { + // 摄像头模式 + if (argc != 3) { + std::cerr << "Error: Camera mode requires exactly 3 arguments." << std::endl; + printUsage(argv[0]); + return 1; + } + return cameraDetectionMode(modelPath); + } + else if (mode == "-i" || mode == "--image") { + // 图片模式 + if (argc != 4) { + std::cerr << "Error: Image mode requires exactly 4 arguments." << std::endl; + printUsage(argv[0]); + return 1; + } + std::string imagePath = argv[3]; + return imageDetectionMode(modelPath, imagePath); + } + else { + std::cerr << "Error: Invalid mode specified." << std::endl; + printUsage(argv[0]); + return 1; + } +} \ No newline at end of file diff --git a/Cpp_example/F02_yingshe/CMakeLists.txt b/Cpp_example/F02_yingshe/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..4c4bbe6475eacc1b62672ccbd56f21b98ad07fe3 --- /dev/null +++ b/Cpp_example/F02_yingshe/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.10) + +project(D01_test_detection) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 定义项目根目录路径 +set(PROJECT_ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../..") +message("PROJECT_ROOT_PATH = " ${PROJECT_ROOT_PATH}) + +include("${PROJECT_ROOT_PATH}/toolchains/arm-rockchip830-linux-uclibcgnueabihf.toolchain.cmake") + +# 定义 OpenCV SDK 路径 +set(OpenCV_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/opencv-mobile-4.10.0-lockzhiner-vision-module") +set(OpenCV_DIR "${OpenCV_ROOT_PATH}/lib/cmake/opencv4") +find_package(OpenCV REQUIRED) +set(OPENCV_LIBRARIES "${OpenCV_LIBS}") + +# 定义 LockzhinerVisionModule SDK 路径 +set(LockzhinerVisionModule_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/lockzhiner_vision_module_sdk") +set(LockzhinerVisionModule_DIR "${LockzhinerVisionModule_ROOT_PATH}/lib/cmake/lockzhiner_vision_module") +find_package(LockzhinerVisionModule REQUIRED) + +add_executable(Test-capture test_capture.cc) +target_include_directories(Test-capture PRIVATE ${LOCKZHINER_VISION_MODULE_INCLUDE_DIRS}) +target_link_libraries(Test-capture PRIVATE ${OPENCV_LIBRARIES} ${LOCKZHINER_VISION_MODULE_LIBRARIES}) + +install( + TARGETS Test-capture + RUNTIME DESTINATION . +) \ No newline at end of file diff --git a/Cpp_example/F02_yingshe/Readme.md b/Cpp_example/F02_yingshe/Readme.md new file mode 100644 index 0000000000000000000000000000000000000000..d4773f7df235e8fdb790485a850abf24c8d78fd4 --- /dev/null +++ b/Cpp_example/F02_yingshe/Readme.md @@ -0,0 +1,135 @@ +# 2025年电赛C题视觉部分实现 + +## 1. 视觉测量整体方案 +本视觉测量系统采用单目视觉技术实现目标物距离和几何尺寸的双重测量。系统首先通过深度学习模型(PaddleDetection)快速定位目标物(带黑框的A4纸),利用目标物在图像中的像素高度与物理距离的二次函数关系,解算目标到基准线的距离D。该函数模型通过实验标定获得,测量范围100-200cm,精度达±2cm。 + +对于内部几何尺寸测量,系统创新性地采用中心区域聚焦策略:在检测到的目标区域内,自动裁剪中心80%区域排除边框干扰,通过自适应阈值处理和形态学滤波增强几何特征。随后提取四边形轮廓,基于图像中心优选算法锁定目标几何图形,最后利用A4纸标准尺寸(210×297mm)与像素尺寸的比例关系,精确换算内部几何图形的实际边长x,测量误差≤0.6cm。 + +系统严格满足竞赛实时性要求,从图像采集到结果显示完整流程耗时<1s。关键创新点包括:1)动态阈值窗口技术,根据目标尺寸自动调整处理参数;2)几何特征三级过滤机制(顶点数/宽高比/中心距离);3)松耦合模块化设计,为后续扩展三角形/圆形测量预留接口。整套方案在200-1000lux照度下稳定工作,已通过基本要求三项测试验证。 + + +## 2. 核心算法实现 + +### 2.1 目标检测模块 +- 模型选择:采用PaddleDetection深度学习框架。 +- 检测对象:A4纸目标物(带2cm黑色边框)。 +- 关键参数:model.SetThreshold(0.7, 0.3); +- 输出:检测到的A4纸位置。 + +### 2.2 距离测量算法 +距离测量基于目标物高度像素值与实际距离的二次函数关系,具体的实现代码如下: +```cpp +double reverseToDistance(double y) { + // 构造方程:a*x^2 + b*x + (c - y) = 0 + double A = a; + double B = b; + double C = c - y; + + double discriminant = B * B - 4 * A * C; + + // 判别式小于0,无实数解 + if (discriminant < 0) { + return -1.0; // 表示无效解 + } + + double sqrt_d = std::sqrt(discriminant); + double x1 = (-B + sqrt_d) / (2 * A); + double x2 = (-B - sqrt_d) / (2 * A); + + // 选择在合理范围 [1.0, 2.0] 内的解 + bool x1_valid = (x1 >= 1.0 && x1 <= 2.0); + bool x2_valid = (x2 >= 1.0 && x2 <= 2.0); + + if (x1_valid && x2_valid) { + // 如果两个解都有效,选择更接近趋势的(通常 x2 更小,x1 更大,看哪个更合理) + // 实际上由于 a > 0, b < 0,通常 x2 是更合理的(较小的根) + // 但根据抛物线形状,我们取更靠近数据趋势的 + // 可以打印调试,但通常取接近 1.0~2.0 之间的 + return x2; // 通常 x2 是物理意义正确的(递减趋势) + } else if (x1_valid) { + return x1; + } else if (x2_valid) { + return x2; + } else { + return -1.0; // 无有效解 + } +} +``` +- 物理原理:目标物表观尺寸与距离成反比。 +- 标定方式:通过实验数据拟合参数 + +## 3. 几何特征提取流程 + +几何特征提取是视觉测量系统的核心环节,采用多级处理流程确保测量精度。系统首先在目标检测获取的A4纸区域内,动态裁剪中心80%区域以排除2cm黑色边框干扰,随后进行自适应二值化处理:基于目标尺寸自动计算阈值窗口(取目标短边的1/4且保证为奇数),采用高斯加权模式增强内部几何图形与背景的对比度。该自适应策略能有效应对100-200cm距离范围内目标表观尺寸的变化。 + +轮廓处理阶段采用三级过滤机制:先通过形态学闭运算消除噪点,再提取所有潜在轮廓;然后执行严格几何验证:1) 多边形逼近顶点数必须为4(四边形) 2) 宽高比限制在0.5-2.0范围 3) 轮廓面积大于40像素。最终通过中心优选算法:计算各轮廓点到图像中心的欧氏距离,选取距离平方和最小的轮廓作为目标几何图形,该策略可有效减少透视畸变影响,确保在物面平行于成像平面时获得最优测量结果。 + +**部分核心代码如下:** +```cpp +// 提取 ROI +cv::Mat roi_frame = input_mat(safe_box); +if (roi_frame.empty()) continue; + +// 转灰度并二值化提取黑色区域 +cv::Mat roi_gray, roi_binary; +cv::cvtColor(roi_frame, roi_gray, cv::COLOR_BGR2GRAY); + +// 关键修改1:使用自适应阈值处理中心区域 +int block_size = std::min(safe_box.width, safe_box.height) / 4; +if (block_size % 2 == 0) block_size++; // 确保为奇数 +cv::adaptiveThreshold(roi_gray, roi_binary, 255, + cv::ADAPTIVE_THRESH_GAUSSIAN_C, + cv::THRESH_BINARY_INV, block_size, 5); + +// 关键修改2:创建中心区域ROI(忽略边缘黑边) +int margin = + std::min(safe_box.width, safe_box.height) / 5; // 忽略20%的边缘区域 +cv::Rect center_roi(margin, margin, roi_binary.cols - 2 * margin, + roi_binary.rows - 2 * margin); + +if (center_roi.width <= 0 || center_roi.height <= 0) continue; +cv::Mat roi_center = roi_binary(center_roi); + +// 形态学操作去噪 +cv::Mat kernel = + cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5)); +cv::morphologyEx(roi_center, roi_center, cv::MORPH_CLOSE, kernel); + +// 查找轮廓(仅在中心区域) +std::vector> contours; +cv::findContours(roi_center, contours, cv::RETR_EXTERNAL, + cv::CHAIN_APPROX_SIMPLE); +``` + +## 4. 尺寸计算原理 +目标物的尺寸计算原理是基于相似理论的比例来进行换算的,核心代码如下: +```cpp +// A4纸实际尺寸(毫米) +const double A4_WIDTH_MM = 210.0; +const double A4_HEIGHT_MM = 297.0; + +cv::Rect a4_rect = results[0].box; +double a4_width_px = a4_rect.width; +double a4_height_px = a4_rect.height; + +// 计算实际尺寸比例 +double scale_x = A4_WIDTH_MM / a4_width_px; +double scale_y = A4_HEIGHT_MM / a4_height_px; + +// 计算内部矩形的实际尺寸(单位:毫米) +double inner_width_mm = inner_width_px * scale_x; +double inner_height_mm = inner_height_px * scale_x; + +double inner_length_mm = ((inner_width_mm + inner_height_mm) / 2) + 5; +inner_length_cm = inner_length_mm / 10.0; // 转换为厘米 + +``` + +## 5. 性能评估 +- 处理速度:平均推理时间:<100ms,端到端延迟:<1s +- 测量精度: + +| 参数 | 要求精度 | 实测精度 | +| ----- | ----------- | ------- | +| 距离D | <= 5cm | <= 2cm | +| 边长x | <= 1cm | <= 0.6cm| \ No newline at end of file diff --git a/Cpp_example/F02_yingshe/test_capture.cc b/Cpp_example/F02_yingshe/test_capture.cc new file mode 100644 index 0000000000000000000000000000000000000000..0192bd9adb3b722e5515473a866055964cb4d066 --- /dev/null +++ b/Cpp_example/F02_yingshe/test_capture.cc @@ -0,0 +1,346 @@ +#include +#include +#include + +#include +#include +#include +#include + +using namespace std::chrono; + +// 形状类型枚举 +enum ShapeType { + UNKNOWN, + RECTANGLE, + TRIANGLE, + CIRCLE // 新增圆形类型 +}; + +// 函数定义 +double reverseToDistance(double y) { + // 拟合参数(来自之前的拟合) + const double a = 42.9873; + const double b = -207.432; + const double c = 331.412; + + // 构造方程:a*x^2 + b*x + (c - y) = 0 + double A = a; + double B = b; + double C = c - y; + + double discriminant = B * B - 4 * A * C; + + // 判别式小于0,无实数解 + if (discriminant < 0) { + return -1.0; // 表示无效解 + } + + double sqrt_d = std::sqrt(discriminant); + double x1 = (-B + sqrt_d) / (2 * A); + double x2 = (-B - sqrt_d) / (2 * A); + + // 选择在合理范围 [1.0, 2.05] 内的解 + bool x1_valid = (x1 >= 1.0 && x1 <= 2.05); + bool x2_valid = (x2 >= 1.0 && x2 <= 2.05); + + if (x1_valid && x2_valid) { + return x2; // 通常 x2 是物理意义正确的(递减趋势) + } else if (x1_valid) { + return x1; + } else if (x2_valid) { + return x2; + } else { + return -1.0; // 无有效解 + } +} + +// 实际A4纸尺寸(单位:毫米) +const double A4_WIDTH_MM = 210.0; +const double A4_HEIGHT_MM = 297.0; + +int main(int argc, char* argv[]) { + if (argc != 2) { + std::cerr << "Usage: Test-PaddleDet model_path" << std::endl; + return 1; + } + + // 初始化模型 + lockzhiner_vision_module::vision::PaddleDet model; + if (!model.Initialize(argv[1])) { + std::cout << "Failed to initialize model." << std::endl; + return 1; + } + model.SetThreshold(0.7, 0.3); // 置信度阈值0.7,NMS阈值0.3 + + // 初始化 edit 通信 + lockzhiner_vision_module::edit::Edit edit; + if (!edit.StartAndAcceptConnection()) { + std::cerr << "Error: Failed to start and accept connection." << std::endl; + return EXIT_FAILURE; + } + std::cout << "Device connected successfully." << std::endl; + + // 打开摄像头 + cv::VideoCapture cap; + cap.set(cv::CAP_PROP_FRAME_WIDTH, 640); + cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480); + cap.open(0); + + if (!cap.isOpened()) { + std::cerr << "Error: Could not open camera." << std::endl; + return 1; + } + + cv::Mat input_mat; + while (true) { + // 捕获一帧图像 + cap >> input_mat; + if (input_mat.empty()) { + std::cerr << "Warning: Captured an empty frame." << std::endl; + continue; + } + + // 调用模型进行预测 + high_resolution_clock::time_point start_time = high_resolution_clock::now(); + auto results = model.Predict(input_mat); + high_resolution_clock::time_point end_time = high_resolution_clock::now(); + + // 计算推理时间 + auto time_span = duration_cast(end_time - start_time); + std::cout << "Inference time: " << time_span.count() << " ms" << std::endl; + + // 可视化模型结果(绿色外框) + cv::Mat output_image; + lockzhiner_vision_module::vision::Visualize(input_mat, output_image, + results); + + // 存储所有符合条件的轮廓及其全局位置和类型 + struct ContourInfo { + std::vector contour; + ShapeType type; + cv::Point2f center; // 圆心坐标 + float radius; // 圆半径 + }; + std::vector candidate_contours; // 全局坐标和类型 + + // 图像中心点 + cv::Point2f image_center(input_mat.cols / 2.0f, input_mat.rows / 2.0f); + + // 初始化显示变量 + double distant_cm = -1.0; + double min_square_size_cm = -1.0; // 最小正方形边长(厘米) + + // 遍历每个检测结果 + int idx = 0; // 添加索引标识第一个检测结果 + for (const auto& result : results) { + double distant = reverseToDistance(result.box.height); + std::cout << "distant:" << distant*100 << "cm" << std::endl; + cv::Rect box = result.box; + cv::Rect safe_box = box & cv::Rect(0, 0, input_mat.cols, input_mat.rows); + if (safe_box.empty()) continue; + + // 保存距离值(转换为厘米) + if (idx == 0) { + distant_cm = distant * 100.0; + } + idx++; + + // 提取 ROI + cv::Mat roi_frame = input_mat(safe_box); + cv::imwrite("roi_frame.png", roi_frame); + if (roi_frame.empty()) continue; + + // 转灰度并二值化提取黑色区域 + cv::Mat roi_gray, roi_binary; + cv::cvtColor(roi_frame, roi_gray, cv::COLOR_BGR2GRAY); + cv::imwrite("roi_gray.png",roi_gray); + + // 关键修改1:使用自适应阈值处理中心区域 + int block_size = std::min(safe_box.width, safe_box.height) / 4; + if (block_size % 2 == 0) block_size++; // 确保为奇数 + cv::adaptiveThreshold(roi_gray, roi_binary, 255, + cv::ADAPTIVE_THRESH_GAUSSIAN_C, + cv::THRESH_BINARY_INV, block_size, 5); + cv::imwrite("roi_binary.png",roi_binary); + + // 关键修改2:创建中心区域ROI(忽略边缘黑边) + int margin = + std::min(safe_box.width, safe_box.height) / 8; // 忽略12.5%的边缘区域 + cv::Rect center_roi(margin, margin, roi_binary.cols - 2 * margin, + roi_binary.rows - 2 * margin); + + if (center_roi.width <= 0 || center_roi.height <= 0) continue; + cv::Mat roi_center = roi_binary(center_roi); + cv::imwrite("roi_center.png",roi_center); + + // 查找轮廓(仅在中心区域) + std::vector> contours; + cv::findContours(roi_center, contours, cv::RETR_EXTERNAL, + cv::CHAIN_APPROX_SIMPLE); + + // 转换轮廓到全局坐标并识别形状 + for (const auto& contour : contours) { + if (contour.size() < 5) continue; // 减少顶点数要求 + double area = cv::contourArea(contour); + if (area < 40) continue; + + // 多边形逼近 + std::vector approx; + double epsilon = 0.02 * cv::arcLength(contour, true); + cv::approxPolyDP(contour, approx, epsilon, true); + + // ==== 圆形检测 ==== + if (approx.size() >= 5) { // 顶点数较多时可能是圆 + cv::Point2f center; + float radius; + cv::minEnclosingCircle(contour, center, radius); + + // 计算圆度:轮廓面积与外接圆面积之比 + double circleArea = CV_PI * radius * radius; + double circularity = area / circleArea; + + // 圆度阈值判断 (0.7-1.0) + if (circularity > 0.7 && circularity < 1.0 && radius > 5) { + // 转换到全局坐标 + cv::Point2f global_center( + center.x + safe_box.x + center_roi.x, + center.y + safe_box.y + center_roi.y + ); + + // 存储圆形信息 + ContourInfo info; + info.type = CIRCLE; + info.center = global_center; + info.radius = radius; + candidate_contours.push_back(info); + } + } + // ==== 三角形检测 ==== + else if (approx.size() == 3) { + // 计算三角形边长 + double side1 = cv::norm(approx[0] - approx[1]); + double side2 = cv::norm(approx[1] - approx[2]); + double side3 = cv::norm(approx[2] - approx[0]); + + // 检查是否等边三角形(边长差异<15%) + double maxSide = std::max(std::max(side1, side2), side3); + double minSide = std::min(std::min(side1, side2), side3); + if (maxSide - minSide < 0.15 * (side1 + side2 + side3) / 3.0) { + // 转换到全局坐标 + std::vector global_contour; + for (auto pt : approx) { + global_contour.push_back(cv::Point( + pt.x + safe_box.x + center_roi.x, + pt.y + safe_box.y + center_roi.y + )); + } + ContourInfo info; + info.type = TRIANGLE; + info.contour = global_contour; + candidate_contours.push_back(info); + } + } + // ==== 矩形检测 ==== + else if (approx.size() == 4) { + // 计算轮廓的宽高比 + cv::Rect bound = cv::boundingRect(approx); + double aspect_ratio = static_cast(bound.width) / bound.height; + + // 过滤掉过于细长或扁平的轮廓 + if (aspect_ratio < 0.5 || aspect_ratio > 2.0) continue; + + // 转换到全局坐标 + std::vector global_contour; + for (auto pt : approx) { + global_contour.push_back(cv::Point( + pt.x + safe_box.x + center_roi.x, + pt.y + safe_box.y + center_roi.y + )); + } + ContourInfo info; + info.type = RECTANGLE; + info.contour = global_contour; + candidate_contours.push_back(info); + } + } + } + + // 寻找最小面积的正方形 + ContourInfo min_square_contour; + double min_square_area = std::numeric_limits::max(); + bool min_square_found = false; + + // 遍历所有候选轮廓,找出面积最小的正方形 + for (const auto& contour_info : candidate_contours) { + if (contour_info.type == RECTANGLE) { + double area = cv::contourArea(contour_info.contour); + if (area < min_square_area) { + min_square_area = area; + min_square_contour = contour_info; + min_square_found = true; + } + } + } + + // 计算最小正方形的边长 + if (min_square_found && !results.empty()) { + cv::Rect a4_rect = results[0].box; // 假设第一个检测结果是A4纸 + double a4_width_px = a4_rect.width; + double scale_x = A4_WIDTH_MM / a4_width_px; + + cv::Rect inner_rect = cv::boundingRect(min_square_contour.contour); + double inner_width_px = inner_rect.width; + double inner_height_px = inner_rect.height; + + // 因为是正方形,我们取平均值 + double inner_length_mm = ((inner_width_px + inner_height_px) / 2.0) * scale_x; + min_square_size_cm = inner_length_mm / 10.0; + + // 用紫色绘制最小正方形 + cv::polylines(output_image, min_square_contour.contour, true, + cv::Scalar(255, 0, 255), 3); // 紫色:BGR(255,0,255) + } + + // 在左上角显示信息(白色背景,黑色文字) + int y_offset = 30; + int line_height = 30; + + // 绘制半透明背景(调整为110高度以容纳三行信息) + cv::Rect bg_rect(10, 10, 300, 110); + cv::Mat roi_bg = output_image(bg_rect); + cv::Mat color(roi_bg.size(), roi_bg.type(), cv::Scalar(200, 200, 200)); + cv::addWeighted(color, 0.6, roi_bg, 0.4, 0, roi_bg); + + // 显示距离信息 + if (distant_cm > 0) { + std::string distant_text = cv::format("Distance: %.1f cm", distant_cm); + cv::putText(output_image, distant_text, cv::Point(20, y_offset), + cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 0), 2); + } else { + cv::putText(output_image, "Distance: N/A", cv::Point(20, y_offset), + cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 0), 2); + } + + // 显示最小正方形边长 + y_offset += line_height; + if (min_square_found) { + std::string size_text = cv::format("Min Square: %.1f cm", min_square_size_cm); + cv::putText(output_image, size_text, cv::Point(20, y_offset), + cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 0), 2); + } else { + cv::putText(output_image, "Min Square: N/A", cv::Point(20, y_offset), + cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 0), 2); + } + + // 发送到 edit 显示 + edit.Print(output_image); + + // 按 ESC 退出 + if (cv::waitKey(1) == 27) break; + } + + cap.release(); + std::cout << "Camera released. Exiting..." << std::endl; + return 0; +} \ No newline at end of file diff --git "a/Cpp_example/F02_yingshe/\346\226\260\345\273\272 DOCX \346\226\207\346\241\243.md" "b/Cpp_example/F02_yingshe/\346\226\260\345\273\272 DOCX \346\226\207\346\241\243.md" new file mode 100755 index 0000000000000000000000000000000000000000..554d8cbaf8e410c992e3782c2d916d41172b3f0c --- /dev/null +++ "b/Cpp_example/F02_yingshe/\346\226\260\345\273\272 DOCX \346\226\207\346\241\243.md" @@ -0,0 +1,59 @@ +我们有如下数据点,对每个 $ x $ 取 $ y $ 的平均值以减少噪声: + +| $ x $ | $ \bar{y} $ | +| ----- | ----------- | +| 1.0 | 167.0 | +| 1.1 | 154.0 | +| 1.2 | 143.0 | +| 1.3 | 134.0 | +| 1.4 | 124.0 | +| 1.5 | 117.0 | +| 1.6 | 109.5 | +| 1.7 | 104.0 | +| 1.8 | 98.5 | +| 1.9 | 94.5 | +| 2.0 | 88.5 | + +观察趋势:$ y $ 随 $ x $ 增大单调递减且呈凸性,适合用**二次多项式**或**指数衰减**模型。 + +经比较,**二次多项式拟合效果更优**: + +$$ +\boxed{y = 43x^2 - 207.5x + 331.5} +$$ + +--- + +### C++ 函数:由 y 反推 x + +```cpp +#include + +double reverseToDistance(double y) { + const double a = 42.9873; + const double b = -207.432; + const double c = 331.412; + + double A = a; + double B = b; + double C = c - y; + + double discriminant = B * B - 4 * A * C; + if (discriminant < 0) return -1.0; // 无实解 + + double sqrt_d = std::sqrt(discriminant); + double x1 = (-B + sqrt_d) / (2 * A); + double x2 = (-B - sqrt_d) / (2 * A); + + // 选择 [1.0, 2.0] 范围内的有效解 + if (x2 >= 1.0 && x2 <= 2.0) return x2; + if (x1 >= 1.0 && x1 <= 2.0) return x1; + + return -1.0; // 无有效解 +} +``` + +> **说明**: +> - 使用所有原始数据最小二乘拟合得到精确参数(保留4位小数)。 +> - 因抛物线开口向上,且在 $[1.0, 2.0]$ 单调递减,通常较小根 $ x_2 $ 在范围内,优先返回。 +> - 输入 $ y $ 应在 $[88, 167]$ 附近,否则可能无解。 \ No newline at end of file diff --git a/Cpp_example/F03_huafei/CMakeLists.txt b/Cpp_example/F03_huafei/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..4c4bbe6475eacc1b62672ccbd56f21b98ad07fe3 --- /dev/null +++ b/Cpp_example/F03_huafei/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.10) + +project(D01_test_detection) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 定义项目根目录路径 +set(PROJECT_ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../..") +message("PROJECT_ROOT_PATH = " ${PROJECT_ROOT_PATH}) + +include("${PROJECT_ROOT_PATH}/toolchains/arm-rockchip830-linux-uclibcgnueabihf.toolchain.cmake") + +# 定义 OpenCV SDK 路径 +set(OpenCV_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/opencv-mobile-4.10.0-lockzhiner-vision-module") +set(OpenCV_DIR "${OpenCV_ROOT_PATH}/lib/cmake/opencv4") +find_package(OpenCV REQUIRED) +set(OPENCV_LIBRARIES "${OpenCV_LIBS}") + +# 定义 LockzhinerVisionModule SDK 路径 +set(LockzhinerVisionModule_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/lockzhiner_vision_module_sdk") +set(LockzhinerVisionModule_DIR "${LockzhinerVisionModule_ROOT_PATH}/lib/cmake/lockzhiner_vision_module") +find_package(LockzhinerVisionModule REQUIRED) + +add_executable(Test-capture test_capture.cc) +target_include_directories(Test-capture PRIVATE ${LOCKZHINER_VISION_MODULE_INCLUDE_DIRS}) +target_link_libraries(Test-capture PRIVATE ${OPENCV_LIBRARIES} ${LOCKZHINER_VISION_MODULE_LIBRARIES}) + +install( + TARGETS Test-capture + RUNTIME DESTINATION . +) \ No newline at end of file diff --git a/Cpp_example/F03_huafei/test_capture.cc b/Cpp_example/F03_huafei/test_capture.cc new file mode 100644 index 0000000000000000000000000000000000000000..f54868c91a3f4f288f970c573e161cf2cfcd0830 --- /dev/null +++ b/Cpp_example/F03_huafei/test_capture.cc @@ -0,0 +1,473 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace std::chrono; + +// 函数定义 +double reverseToDistance(double y) { + // 拟合参数(来自之前的拟合) + const double a = 42.9873; + const double b = -207.432; + const double c = 331.412; + + // 构造方程:a*x^2 + b*x + (c - y) = 0 + double A = a; + double B = b; + double C = c - y; + + double discriminant = B * B - 4 * A * C; + + // 判别式小于0,无实数解 + if (discriminant < 0) { + return -1.0; // 表示无效解 + } + + double sqrt_d = std::sqrt(discriminant); + double x1 = (-B + sqrt_d) / (2 * A); + double x2 = (-B - sqrt_d) / (2 * A); + + // 选择在合理范围 [1.0, 2.05] 内的解 + bool x1_valid = (x1 >= 1.0 && x1 <= 2.05); + bool x2_valid = (x2 >= 1.0 && x2 <= 2.05); + + if (x1_valid && x2_valid) { + return x2; // 通常 x2 是物理意义正确的(递减趋势) + } else if (x1_valid) { + return x1; + } else if (x2_valid) { + return x2; + } else { + return -1.0; // 无有效解 + } +} + +// 实际A4纸尺寸(单位:毫米) +const double A4_WIDTH_MM = 210.0; +const double A4_HEIGHT_MM = 297.0; + +// 分割重叠正方形的函数,返回最小外接正方形 +std::vector> splitOverlappingSquares(const std::vector& contour) { + std::vector> result; + + // 计算凸包 + std::vector hull_indices; + cv::convexHull(contour, hull_indices, false, false); + + // 计算凸性缺陷 + std::vector defects; + cv::convexityDefects(contour, hull_indices, defects); + + if (defects.empty()) { + return result; + } + + // 存储凹点信息 + std::vector> deep_points; // <深度, 轮廓索引> + for (const auto& defect : defects) { + float depth = defect[3] / 256.0f; + int far_idx = defect[2]; + deep_points.push_back(std::make_pair(depth, far_idx)); + } + + // 按深度降序排序 + std::sort(deep_points.begin(), deep_points.end(), + [](const std::pair& a, const std::pair& b) { + return a.first > b.first; + }); + + // 取前两个凹点 + if (deep_points.size() < 2) { + return result; + } + + int idx1 = deep_points[0].second; + int idx2 = deep_points[1].second; + + // 确保 idx1 < idx2 + if (idx1 > idx2) { + std::swap(idx1, idx2); + } + + // 分割轮廓为两个部分 + std::vector part1(contour.begin() + idx1, contour.begin() + idx2 + 1); + std::vector part2; + part2.insert(part2.end(), contour.begin() + idx2, contour.end()); + part2.insert(part2.end(), contour.begin(), contour.begin() + idx1 + 1); + + // 为每个部分计算最小外接矩形 + auto getMinAreaRectPoints = [](const std::vector& points) { + if (points.size() < 4) return std::vector(); + cv::RotatedRect rect = cv::minAreaRect(points); + cv::Point2f vertices[4]; + rect.points(vertices); + return std::vector{ + cv::Point(static_cast(vertices[0].x), static_cast(vertices[0].y)), + cv::Point(static_cast(vertices[1].x), static_cast(vertices[1].y)), + cv::Point(static_cast(vertices[2].x), static_cast(vertices[2].y)), + cv::Point(static_cast(vertices[3].x), static_cast(vertices[3].y)) + }; + }; + + std::vector rect1 = getMinAreaRectPoints(part1); + std::vector rect2 = getMinAreaRectPoints(part2); + + if (!rect1.empty()) result.push_back(rect1); + if (!rect2.empty()) result.push_back(rect2); + + return result; +} + +int main(int argc, char* argv[]) { + if (argc != 2) { + std::cerr << "Usage: Test-PaddleDet model_path" << std::endl; + return 1; + } + + // 初始化模型 + lockzhiner_vision_module::vision::PaddleDet model; + if (!model.Initialize(argv[1])) { + std::cout << "Failed to initialize model." << std::endl; + return 1; + } + model.SetThreshold(0.7, 0.3); // 置信度阈值0.7,NMS阈值0.3 + + // 初始化 edit 通信 + lockzhiner_vision_module::edit::Edit edit; + if (!edit.StartAndAcceptConnection()) { + std::cerr << "Error: Failed to start and accept connection." << std::endl; + return EXIT_FAILURE; + } + std::cout << "Device connected successfully." << std::endl; + + // 打开摄像头 + cv::VideoCapture cap; + cap.set(cv::CAP_PROP_FRAME_WIDTH, 640); + cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480); + cap.open(0); + + if (!cap.isOpened()) { + std::cerr << "Error: Could not open camera." << std::endl; + return 1; + } + + cv::Mat input_mat; + while (true) { + // 捕获一帧图像 + cap >> input_mat; + if (input_mat.empty()) { + std::cerr << "Warning: Captured an empty frame." << std::endl; + continue; + } + + // 调用模型进行预测 + high_resolution_clock::time_point start_time = high_resolution_clock::now(); + auto results = model.Predict(input_mat); + high_resolution_clock::time_point end_time = high_resolution_clock::now(); + + // 计算推理时间 + auto time_span = duration_cast(end_time - start_time); + std::cout << "Inference time: " << time_span.count() << " ms" << std::endl; + + // 可视化模型结果(绿色外框) + cv::Mat output_image; + lockzhiner_vision_module::vision::Visualize(input_mat, output_image, + results); + + // 存储所有符合条件的正方形轮廓及其全局位置 + struct ContourInfo { + std::vector contour; + int tag; // 0:原始矩形, 1:分割矩形1, 2:分割矩形2 + double area; // 轮廓面积 + double side_length; // 正方形边长(厘米) + }; + std::vector candidate_contours; // 所有检测到的正方形 + + // 初始化显示变量 + double distant_cm = -1.0; + double min_square_size_cm = -1.0; // 最小正方形边长(厘米) + + // 遍历每个检测结果 + int idx = 0; // 添加索引标识第一个检测结果 + for (const auto& result : results) { + double distant = reverseToDistance(result.box.height); + std::cout << "distant:" << distant*100 << "cm" << std::endl; + cv::Rect box = result.box; + cv::Rect safe_box = box & cv::Rect(0, 0, input_mat.cols, input_mat.rows); + if (safe_box.empty()) continue; + + // 保存距离值(转换为厘米) + if (idx == 0) { + distant_cm = distant * 100.0; + } + idx++; + + // 提取 ROI + cv::Mat roi_frame = input_mat(safe_box); + if (roi_frame.empty()) continue; + + // 转灰度并二值化提取黑色区域 + cv::Mat roi_gray, roi_binary; + cv::cvtColor(roi_frame, roi_gray, cv::COLOR_BGR2GRAY); + + // 使用自适应阈值处理中心区域 + int block_size = std::min(safe_box.width, safe_box.height) / 4; + if (block_size % 2 == 0) block_size++; // 确保为奇数 + cv::adaptiveThreshold(roi_gray, roi_binary, 255, + cv::ADAPTIVE_THRESH_GAUSSIAN_C, + cv::THRESH_BINARY_INV, block_size, 5); + + // 创建中心区域ROI(忽略边缘黑边) + int margin = + std::min(safe_box.width, safe_box.height) / 8; // 忽略12.5%的边缘区域 + cv::Rect center_roi(margin, margin, roi_binary.cols - 2 * margin, + roi_binary.rows - 2 * margin); + + if (center_roi.width <= 0 || center_roi.height <= 0) continue; + cv::Mat roi_center = roi_binary(center_roi); + + // ==== 调试可视化:显示二值化图像 ==== + cv::Mat debug_binary; + cv::cvtColor(roi_center, debug_binary, cv::COLOR_GRAY2BGR); + + // 查找轮廓(仅在中心区域) + std::vector> contours; + cv::findContours(roi_center.clone(), contours, cv::RETR_EXTERNAL, // 使用clone防止修改原图 + cv::CHAIN_APPROX_SIMPLE); + + // ==== 调试可视化:绘制所有轮廓 ==== + cv::Mat debug_contours = debug_binary.clone(); + cv::drawContours(debug_contours, contours, -1, cv::Scalar(0, 0, 255), 1); // 红色轮廓 + for (size_t i = 0; i < contours.size(); i++) { + cv::Rect r = cv::boundingRect(contours[i]); + cv::putText(debug_contours, std::to_string(i), cv::Point(r.x, r.y), + cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(0, 255, 0), 1); + } + cv::imshow("Binary ROI", roi_center); + cv::imshow("All Contours", debug_contours); + // ==== 结束调试可视化 ==== + + // 转换轮廓到全局坐标并识别形状 + for (size_t i = 0; i < contours.size(); i++) { + const auto& contour = contours[i]; + if (contour.size() < 5) continue; + double area = cv::contourArea(contour); + if (area < 40) continue; + + // ==== 调试输出 ==== + std::cout << "Contour #" << i << ": points=" << contour.size() + << ", area=" << area << std::endl; + + // 多边形逼近 + std::vector approx; + double epsilon = 0.02 * cv::arcLength(contour, true); + cv::approxPolyDP(contour, approx, epsilon, true); + + // ==== 调试输出 ==== + std::cout << " Approximated points: " << approx.size() << std::endl; + + // ==== 矩形检测 ==== + if (approx.size() == 4) { + // 计算轮廓的宽高比 + cv::Rect bound = cv::boundingRect(approx); + double aspect_ratio = static_cast(bound.width) / bound.height; + + // 过滤掉过于细长或扁平的轮廓 + if (aspect_ratio < 0.5 || aspect_ratio > 2.0) continue; + + // 转换到全局坐标 + std::vector global_contour; + for (auto pt : approx) { + global_contour.push_back(cv::Point( + pt.x + safe_box.x + center_roi.x, + pt.y + safe_box.y + center_roi.y + )); + } + + // 计算正方形边长(像素) + double side_length_px = (bound.width + bound.height) / 2.0; + + // 计算实际边长(厘米)基于A4纸尺寸 + double side_length_cm = -1.0; + if (!results.empty()) { + cv::Rect a4_rect = results[0].box; + double scale_x = A4_WIDTH_MM / a4_rect.width; + side_length_cm = side_length_px * scale_x / 10.0; + } + + ContourInfo info; + info.contour = global_contour; + info.tag = 0; // 原始矩形 + info.area = area; + info.side_length = side_length_cm; + candidate_contours.push_back(info); + + // ==== 调试输出 ==== + std::cout << " Found rectangle: " << bound.width << "x" << bound.height + << ", aspect=" << aspect_ratio << std::endl; + } + // ==== 重叠正方形处理 ==== + else if (approx.size() == 8) { + // ==== 调试输出 ==== + std::cout << " Trying to split contour with " << approx.size() << " points" << std::endl; + + // 尝试分割重叠的正方形 + auto split_contours = splitOverlappingSquares(contour); + + if (split_contours.size() == 2) { + int tag_index = 1; // 分割矩形的标记从1开始 + for (const auto& split_contour : split_contours) { + if (split_contour.size() < 4) { + tag_index++; + continue; + } + + // 对分割后的轮廓进行多边形逼近 + std::vector split_approx; + double split_epsilon = 0.02 * cv::arcLength(split_contour, true); + cv::approxPolyDP(split_contour, split_approx, split_epsilon, true); + + // 检查是否是四边形 + if (split_approx.size() != 4) { + // ==== 调试输出 ==== + std::cout << " Split contour has " << split_approx.size() << " points, skipping" << std::endl; + tag_index++; + continue; + } + + + // 计算宽高比 + cv::Rect split_bound = cv::boundingRect(split_approx); + double split_aspect_ratio = static_cast(split_bound.width) / split_bound.height; + + // 过滤掉过于细长或扁平的轮廓 + if (split_aspect_ratio < 0.8 || split_aspect_ratio > 1.2) { + // ==== 调试输出 ==== + std::cout << " Split contour aspect ratio out of range: " << split_aspect_ratio << std::endl; + tag_index++; + continue; + } + + // 转换到全局坐标 + std::vector global_contour; + for (auto pt : split_approx) { + global_contour.push_back(cv::Point( + pt.x + safe_box.x + center_roi.x, + pt.y + safe_box.y + center_roi.y + )); + } + + // 计算正方形边长(像素) + double side_length_px = (split_bound.width + split_bound.height) / 2.0; + + // 计算实际边长(厘米)基于A4纸尺寸 + double side_length_cm = -1.0; + if (!results.empty()) { + cv::Rect a4_rect = results[0].box; + double scale_x = A4_WIDTH_MM / a4_rect.width; + side_length_cm = side_length_px * scale_x / 10.0; + } + + // 存储分割出的正方形 + ContourInfo info; + info.contour = global_contour; + info.tag = tag_index; // 1或2 + info.area = cv::contourArea(split_approx); + info.side_length = side_length_cm; + candidate_contours.push_back(info); + tag_index++; + + // ==== 调试输出 ==== + std::cout << " Successfully split contour: " << split_bound.width << "x" << split_bound.height + << ", aspect=" << split_aspect_ratio << std::endl; + } + } + else { + // ==== 调试输出 ==== + std::cout << " Failed to split contour, returned " << split_contours.size() << " parts" << std::endl; + } + } + } + } + + // 寻找面积最小的正方形 + ContourInfo min_square; + double min_area = std::numeric_limits::max(); + bool min_square_found = false; + + for (const auto& contour_info : candidate_contours) { + if (contour_info.area < min_area) { + min_area = contour_info.area; + min_square = contour_info; + min_square_found = true; + } + } + + // 记录最小正方形的边长 + if (min_square_found) { + min_square_size_cm = min_square.side_length; + } + + // ==== 修改部分:只绘制最小正方形 ==== + if (min_square_found) { + // 绘制最小正方形(紫色加粗) + cv::polylines(output_image, min_square.contour, true, + cv::Scalar(255, 0, 255), 4); + + // 在正方形上方显示尺寸信息 + cv::Rect bound = cv::boundingRect(min_square.contour); + std::string size_text = cv::format("%.1f cm", min_square_size_cm); + cv::Point text_pt(bound.x, bound.y - 10); + cv::putText(output_image, size_text, text_pt, + cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 0, 255), 2); + } + + // 在左上角显示信息(白色背景,黑色文字) + int y_offset = 30; + int line_height = 30; + + // 绘制半透明背景(调整为80高度以容纳两行信息) + cv::Rect bg_rect(10, 10, 300, 80); + cv::Mat roi_bg = output_image(bg_rect); + cv::Mat color(roi_bg.size(), roi_bg.type(), cv::Scalar(200, 200, 200)); + cv::addWeighted(color, 0.6, roi_bg, 0.4, 0, roi_bg); + + // 显示距离信息 + if (distant_cm > 0) { + std::string distant_text = cv::format("Distance: %.1f cm", distant_cm); + cv::putText(output_image, distant_text, cv::Point(20, y_offset), + cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 0), 2); + } else { + cv::putText(output_image, "Distance: N/A", cv::Point(20, y_offset), + cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 0), 2); + } + + // 显示最小正方形边长 + y_offset += line_height; + if (min_square_found) { + std::string size_text = cv::format("Min Square: %.1f cm", min_square_size_cm); + cv::putText(output_image, size_text, cv::Point(20, y_offset), + cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 0), 2); + } else { + cv::putText(output_image, "Min Square: N/A", cv::Point(20, y_offset), + cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 0), 2); + } + + // 发送到 edit 显示 + edit.Print(output_image); + + // 按 ESC 退出 + if (cv::waitKey(1) == 27) break; + } + + cap.release(); + std::cout << "Camera released. Exiting..." << std::endl; + return 0; +} \ No newline at end of file diff --git a/README.md b/README.md index ee2e2d7250b539b7654d2b1be613c5965b3e6bc6..b719587d52ee53d486d19ad88e16dda7438982e0 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,12 @@ OCR(Optical Character Recognition,光学字符识别)是一种将图像中 * [凌智视觉模块车牌识别](./Cpp_example/D09_plate_recognize/README.md) +### 👍 疲劳检测案例 + +疲劳检测基于计算机视觉与生理特征分析技术,通过实时捕捉面部特征、眼部状态及头部姿态,结合深度学习算法精准识别疲劳征兆,应用于驾驶安全监控、工业安全生产及健康监护领域,有效预防事故风险并提升人身安全保障水平。 + +* [凌智视觉模块疲劳检测](./Cpp_example/D14_fatigue_detection/README.md) + ### 👍 YOLOv5目标检测 目标检测(Object Detection)是深度学习中计算机视觉领域的重要任务之一,旨在识别图像或视频中所有感兴趣的物体,并准确地定位这些物体的边界框(Bounding Box)。与目标分类不同,目标检测不仅需要预测物体的类别,还需要标注它们在图像中的位置。一般来说,目标检测任务的标注过程比较复杂,适合既需要对目标进行分类,有需要对目标进行定位的场景。该案例使用YOLOv5进行目标检测。 @@ -209,7 +215,9 @@ C++ 开发案例以A、B、C、D进行不同类别进行分类,方便初学者 | D11 | 神经网络类 | PPOCRv3 | [文字识别](./Cpp_example/D11_PPOCRv3/README.md) | | D12 | 神经网络类 | PPOCRv4-Det | [文字检测](./Cpp_example/D12_ppocrv4_det/README.md) | | D13 | 神经网络类 | target_tracking | [多目标跟踪](./Cpp_example/D13_target_tracking/README.md)| +| D14 | 神经网络类 | fatugue_detection | [疲劳检测](./Cpp_example/D14_fatigue_detection/README.md)| | E01 | 使用示例类 | Test_find_Laser | [激光跟踪](./Cpp_example/E01_find_Laser/README.md)| +| E02 | 使用示例类 | Test_find_number | [数字识别](./Cpp_example/E02_find_number/README.md)| ## 🐛 Bug反馈