diff --git a/Cpp_example/C06_test_qr_code_detector/CMakeLists.txt b/Cpp_example/C06_test_qr_code_detector/CMakeLists.txt index 5bc112182bad8bea270137c6426b101469f7d190..17721c6c85aecdedbc586a2bddd4c79b286f59d3 100644 --- a/Cpp_example/C06_test_qr_code_detector/CMakeLists.txt +++ b/Cpp_example/C06_test_qr_code_detector/CMakeLists.txt @@ -26,7 +26,7 @@ find_package(LockzhinerVisionModule REQUIRED) # 定义 ZXing SDK 路径 set(ZXing_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/zxing-cpp-v2.2.1-lockzhiner-vision-module") set(ZXing_INCLUDE_DIRS "${ZXing_ROOT_PATH}/include") -set(ZXing_LIBRARIES "${ZXing_ROOT_PATH}/lib/libzxing.a") +set(ZXing_LIBRARIES "${ZXing_ROOT_PATH}/lib/libZXing.a") # 基本图像处理示例 add_executable(Test-qr_code-detector test_qr_code_detector.cc) diff --git a/Cpp_example/D10_yolov5/README.md b/Cpp_example/D10_yolov5/README.md index 98ce23290acc789ba0a1b3639bf35300f3e6468c..575ff2d0d15544ff22c4e753dc6b759348b5d818 100644 --- a/Cpp_example/D10_yolov5/README.md +++ b/Cpp_example/D10_yolov5/README.md @@ -1,120 +1,165 @@ # YOLOV5目标检测 + 本章节基于 Lockzhiner Vision Module 和 YOLOv5 目标检测模型,实现实时目标检测功能。 + ## 1. 基本知识简介 + ### 1.1 目标检测简介 + 目标检测是计算机视觉领域中的一个关键任务,它不仅需要识别图像中存在哪些对象,还需要定位这些对象的位置。具体来说,目标检测算法会输出每个检测到的对象的边界框(Bounding Box)以及其所属类别的概率或置信度得分。 + - 应用场景:目标检测技术广泛应用于多个领域,包括但不限于安全监控、自动驾驶汽车、智能零售和医疗影像分析。 + ### 1.2 YOLOv5简介 + YOLOv5 是 Ultralytics 在 2020 年基于 PyTorch 开发的一种高效目标检测模型,属于 YOLO 系列。它通过一次前向传播即可预测目标的类别和位置,适用于实时检测任务。YOLOv5 提供了多种模型大小(如 YOLOv5s、m、l、x),适应不同硬件条件。其结构包括骨干网络 CSPDarknet、特征融合层和检测头,支持多尺度预测和自定义训练,广泛用于各种检测场景。 ## 2. API 文档 + ### 2.1 YOLOv5模型类 + #### 2.1.1 头文件 + ```cpp #include "yolov5.h" ``` + #### 2.1.2 模型初始化函数 + ```cpp int init_yolov5_model(const char* model_path, rknn_app_context_t* ctx); ``` + - 作用:加载YOLOv5 RKNN模型并初始化推理上下文 - 参数 - - model_path:RKNN模型文件路径 - - ctx:模型上下文指针 + - model_path:RKNN模型文件路径 + - ctx:模型上下文指针 - 返回值: - - 0:初始化成功 - - -1:初始化失败 + - 0:初始化成功 + - -1:初始化失败 + #### 2.1.3 模型推理函数 + ```cpp int inference_yolov5_model(rknn_app_context_t* ctx, object_detect_result_list* od_results); ``` + - 作用:执行模型推理并获取检测结果 - 参数: - - ctx:已初始化的模型上下文 - - od_results:检测结果存储结构体指针 + - ctx:已初始化的模型上下文 + - od_results:检测结果存储结构体指针 - 返回值: - - 0:推理成功 - - -1 :推理失败 + - 0:推理成功 + - -1 :推理失败 + #### 2.1.4 模型释放函数 + ```cpp void release_yolov5_model(rknn_app_context_t* ctx); ``` + - 作用:释放模型相关资源 - 参数: - - ctx:待释放的模型上下文 + - ctx:待释放的模型上下文 - 返回值:无 + ### 2.2 图像处理函数 + #### 2.2.1 Letterbox处理 + ```cpp cv::Mat letterbox(cv::Mat& image); ``` + - 作用:保持图像比例进行缩放,添加灰边填充 - 参数: -image:输入图像(RGB格式) - 返回值: - - 返回预处理图像 + - 返回预处理图像 + #### 2.2.2 坐标映射函数 + ```cpp void mapCoordinates(float& x, float& y); ``` + - 作用:将模型输出坐标映射回原始图像坐标系 - 参数: - - x/y:模型输出坐标(输入输出参数) + - x/y:模型输出坐标(输入输出参数) - 返回值:无 + ### 2.3 结果处理函数 + #### 2.3.1 后处理初始化 + ```cpp void init_post_process(); ``` + - 作用:加载类别标签文件 - 参数:无 - 返回值:无 + #### 2.3.2 结果绘制函数 + ```cpp void draw_detections(int count, object_detect_result* results, cv::Mat& frame, void (*mapFunc)(float&, float&)); ``` + - 作用:在图像上绘制检测框和标签 - 参数: - - count:检测结果数量 - - results:检测结果数组 - - frame:目标图像帧 - - mapFunc:坐标映射函数指针 + - count:检测结果数量 + - results:检测结果数组 + - frame:目标图像帧 + - mapFunc:坐标映射函数指针 - 返回值:无 + ## 3. 代码解析 + ### 3.1 流程图 ### 3.2核心代码解析 + - 模型初始化 + ```cpp rknn_app_context_t rknn_app_ctx; init_yolov5_model("yolov5s.rknn", &rknn_app_ctx); ``` + - 图像预处理 + ```cpp cv::Mat letterboxImage = letterbox(frame); // 保持比例缩放 memcpy(rknn_app_ctx.input_mems[0]->virt_addr, letterboxImage.data, model_width * model_height * 3); ``` + - 模型推理 + ```cpp object_detect_result_list od_results; inference_yolov5_model(&rknn_app_ctx, &od_results); ``` + - 结果可视化 + ```cpp draw_detections(od_results.count, od_results.results, frame, mapCoordinates); ``` + ### 3.3 完整代码实现 + ```cpp #include #include @@ -146,117 +191,121 @@ char *lable; int main(int argc, char *argv[]) { - if (argc != 4) - { - LOGGER_INFO("Usage: %s ./yolov5_main model_path ./label size\n ./label_txt"); - } - obj_class_num = atoi(argv[2]); - lable = argv[3]; - // Rknn model - char text[16]; - // rknn上下文结构体 - rknn_app_context_t rknn_app_ctx; - object_detect_result_list od_results; - int ret; - const char *model_path = argv[1]; - memset(&rknn_app_ctx, 0, sizeof(rknn_app_context_t)); - - // Step 1: Load RKNN model - if (init_yolov5_model(model_path, &rknn_app_ctx) != 0) - { - printf("❌ Failed to load RKNN model!\n"); - return -1; - } - printf("✅ RKNN model loaded successfully.\n"); - - // 加载标签文件 - init_post_process(); - - // 打开摄像头 - lockzhiner_vision_module::edit::Edit edit; - if (!edit.StartAndAcceptConnection()) - { - std::cerr << "Error: Failed to start and accept connection." << std::endl; - return EXIT_FAILURE; - } - - 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 frame; - // 在 while 循环外声明 start 和 end 变量 - std::chrono::steady_clock::time_point start, end; - - while (true) - { - // 记录开始时间 - start = std::chrono::steady_clock::now(); - // Step 2: Load image from command line - cap >> frame; - if (frame.empty()) - { - LOGGER_INFO("❌ Failed to read frame from camera.\n"); - continue; - } - cv::resize(frame, frame, cv::Size(width, height), 0, 0, cv::INTER_LINEAR); - cv::Mat letterboxImage = letterbox(frame); - - if (letterboxImage.empty() || letterboxImage.total() * letterboxImage.elemSize() != model_width * model_height * 3) - { - - LOGGER_ERROR("❌ Input image format or size mismatch!\n"); - release_yolov5_model(&rknn_app_ctx); - return -1; - } - - if (rknn_app_ctx.input_mems == nullptr || rknn_app_ctx.input_mems[0] == nullptr) - { - - LOGGER_ERROR("❌ RKNN input memory not allocated!\n"); - release_yolov5_model(&rknn_app_ctx); - return -1; - } - - memcpy(rknn_app_ctx.input_mems[0]->virt_addr, letterboxImage.data, model_width * model_height * 3); - - if (inference_yolov5_model(&rknn_app_ctx, &od_results) != 0) - { - LOGGER_ERROR("inference_yolov5_model failed"); - release_yolov5_model(&rknn_app_ctx); - return -1; - } - - draw_detections(od_results.count, // 传入结果数量 - od_results.results, // 传入结果数组 - frame, // 图像帧 - mapCoordinates); // 直接使用现有坐标映射函数 - - edit.Print(frame); - // 记录结束时间 - end = std::chrono::steady_clock::now(); - // 计算耗时(秒) - double elapsed_time = std::chrono::duration(end - start).count(); - printf("Frame processed in %.4f seconds\n", elapsed_time); - } - release_yolov5_model(&rknn_app_ctx); - deinit_post_process(); - cap.release(); - return 0; + if (argc != 4) + { + LOGGER_INFO("Usage: %s ./yolov5_main model_path ./label size\n ./label_txt"); + } + obj_class_num = atoi(argv[2]); + lable = argv[3]; + // Rknn model + char text[16]; + // rknn上下文结构体 + rknn_app_context_t rknn_app_ctx; + object_detect_result_list od_results; + int ret; + const char *model_path = argv[1]; + memset(&rknn_app_ctx, 0, sizeof(rknn_app_context_t)); + + // Step 1: Load RKNN model + if (init_yolov5_model(model_path, &rknn_app_ctx) != 0) + { + printf("❌ Failed to load RKNN model!\n"); + return -1; + } + printf("✅ RKNN model loaded successfully.\n"); + + // 加载标签文件 + init_post_process(); + + // 打开摄像头 + lockzhiner_vision_module::edit::Edit edit; + if (!edit.StartAndAcceptConnection()) + { + std::cerr << "Error: Failed to start and accept connection." << std::endl; + return EXIT_FAILURE; + } + + 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 frame; + // 在 while 循环外声明 start 和 end 变量 + std::chrono::steady_clock::time_point start, end; + + while (true) + { + // 记录开始时间 + start = std::chrono::steady_clock::now(); + // Step 2: Load image from command line + cap >> frame; + if (frame.empty()) + { + LOGGER_INFO("❌ Failed to read frame from camera.\n"); + continue; + } + cv::resize(frame, frame, cv::Size(width, height), 0, 0, cv::INTER_LINEAR); + cv::Mat letterboxImage = letterbox(frame); + + if (letterboxImage.empty() || letterboxImage.total() * letterboxImage.elemSize() != model_width * model_height * 3) + { + + LOGGER_ERROR("❌ Input image format or size mismatch!\n"); + release_yolov5_model(&rknn_app_ctx); + return -1; + } + + if (rknn_app_ctx.input_mems == nullptr || rknn_app_ctx.input_mems[0] == nullptr) + { + + LOGGER_ERROR("❌ RKNN input memory not allocated!\n"); + release_yolov5_model(&rknn_app_ctx); + return -1; + } + + memcpy(rknn_app_ctx.input_mems[0]->virt_addr, letterboxImage.data, model_width * model_height * 3); + + if (inference_yolov5_model(&rknn_app_ctx, &od_results) != 0) + { + LOGGER_ERROR("inference_yolov5_model failed"); + release_yolov5_model(&rknn_app_ctx); + return -1; + } + + draw_detections(od_results.count, // 传入结果数量 + od_results.results, // 传入结果数组 + frame, // 图像帧 + mapCoordinates); // 直接使用现有坐标映射函数 + + edit.Print(frame); + // 记录结束时间 + end = std::chrono::steady_clock::now(); + // 计算耗时(秒) + double elapsed_time = std::chrono::duration(end - start).count(); + printf("Frame processed in %.4f seconds\n", elapsed_time); + } + release_yolov5_model(&rknn_app_ctx); + deinit_post_process(); + cap.release(); + return 0; } ``` ## 4. 编译过程 + ### 4.1 编译环境搭建 + - 请确保你已经按照 [开发环境搭建指南](../../../../docs/introductory_tutorial/cpp_development_environment.md) 正确配置了开发环境。 - 同时以正确连接开发板。 + ### 4.2 Cmake介绍 + ```cmake # CMake最低版本要求 cmake_minimum_required(VERSION 3.10) @@ -302,8 +351,11 @@ install( RUNTIME DESTINATION . ) ``` + ### 4.3 编译项目 + 使用 Docker Destop 打开 LockzhinerVisionModule 容器并执行以下命令来编译项目 + ```bash # 进入Demo所在目录 cd /LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/D10_yolov5 @@ -320,14 +372,17 @@ make -j8 && make install 在执行完上述命令后,会在build目录下生成可执行文件。 ## 5. 例程运行示例 + ### 5.1 运行 + ```shell chmod 777 yolov5_main # 在实际应用的过程中LZ-Picodet需要替换为下载的或者你的rknn模型 ./yolov5_main ./voc320.rknn 20 label ``` + ### 5.2 结果展示 - 可以看到我们可以正确识别多种类别的 -![](./images/D10_yolov5.png) \ No newline at end of file +![](./images/D10_yolov5.png) diff --git "a/Cpp_example/D10_yolov5/\346\250\241\345\236\213\346\226\207\344\273\266\350\275\254\346\215\242.md" "b/Cpp_example/D10_yolov5/\346\250\241\345\236\213\346\226\207\344\273\266\350\275\254\346\215\242.md" new file mode 100644 index 0000000000000000000000000000000000000000..1be139b6c89a12d30b4dbf76579c5cc9f8207a0b --- /dev/null +++ "b/Cpp_example/D10_yolov5/\346\250\241\345\236\213\346\226\207\344\273\266\350\275\254\346\215\242.md" @@ -0,0 +1,223 @@ +# 模型训练以及转换 + +## 1. 模型训练 + +### 1.1 下载模型训练代码 + +```shell +git clone https://github.com/airockchip/yolov5.git +``` + +### 1.2 准备数据集 + +将自定义数据集转换为yolo格式,下面以Labelme工具标注的数据集为例,运行以下代码进行数据集的转换: + +```python +import os +import json +import shutil +import random +from pathlib import Path + +def convert_labelme_to_yolo(src_dir, dest_dir, yaml_output_path, split_ratio=0.8): + src_dir = Path(src_dir) + dest_dir = Path(dest_dir) + yaml_output_path = Path(yaml_output_path) + + # 输入路径设置 + annotations_dir = src_dir / 'annotations' + images_dir = src_dir / 'images' + + if not annotations_dir.exists(): + print(f"找不到标注文件夹: {annotations_dir}") + return + + # 1. 创建 YOLOv5 所需目录结构 + for split in ['train', 'val']: + (dest_dir / 'images' / split).mkdir(parents=True, exist_ok=True) + (dest_dir / 'labels' / split).mkdir(parents=True, exist_ok=True) + + # 获取所有 json 文件 + json_files = list(annotations_dir.glob('*.json')) + if not json_files: + print("未找到任何 JSON 标注文件!") + return + + # 2. 随机打乱用于划分训练集 (80%) 和验证集 (20%) + random.seed(42) + random.shuffle(json_files) + train_count = int(len(json_files) * split_ratio) + + # 类别映射(目前只有一个类别 fire,这里需要根据你的数据集类别进行修改) + classes = ['fire'] + class_to_id = {cls: i for i, cls in enumerate(classes)} + + print(f"总计找到 {len(json_files)} 个标注文件,开始转换...") + + success_count = 0 + for i, json_path in enumerate(json_files): + split = 'train' if i < train_count else 'val' + + with open(json_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 获取图像宽高 + img_w = data.get('imageWidth') + img_h = data.get('imageHeight') + + if not img_w or not img_h: + print(f"警告: {json_path.name} 中没有图像宽高数据,跳过。") + continue + + # 解析图像文件名 (兼容 Windows '\\' 和 Linux '/') + img_name = data.get('imagePath', '').replace('\\', '/').split('/')[-1] + if not img_name: + img_name = json_path.stem + '.png' + + src_img_path = images_dir / img_name + + # 3. 如果图片不存在,尝试其它常见的图片后缀 + if not src_img_path.exists(): + for ext in ['.jpg', '.jpeg', '.JPG', '.PNG']: + alt_img_path = images_dir / (json_path.stem + ext) + if alt_img_path.exists(): + src_img_path = alt_img_path + img_name = alt_img_path.name + break + else: + print(f"警告: 找不到对应的图像文件 {src_img_path},跳过此样本。") + continue + + # 4. 准备输出 label 内容 + yolo_labels = [] + for shape in data.get('shapes', []): + label = shape.get('label') + if label not in class_to_id: + continue # 如果存在其它意外的类别则忽略 + + class_id = class_to_id[label] + points = shape.get('points') + + # 确保是矩形框并且有两个点 + if shape.get('shape_type') == 'rectangle' and len(points) == 2: + x1, y1 = points[0] + x2, y2 = points[1] + + # 确保坐标极值正确 + x_min, x_max = min(x1, x2), max(x1, x2) + y_min, y_max = min(y1, y2), max(y1, y2) + + # 转换为 YOLO 的中心点及宽高,并且归一化到 0~1 + x_center = ((x_min + x_max) / 2.0) / img_w + y_center = ((y_min + y_max) / 2.0) / img_h + w = (x_max - x_min) / img_w + h = (y_max - y_min) / img_h + + yolo_labels.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") + + # 5. 写入 YOLO 格式的 txt 标签文件 + dest_txt_path = dest_dir / 'labels' / split / (json_path.stem + '.txt') + with open(dest_txt_path, 'w', encoding='utf-8') as f: + f.write('\n'.join(yolo_labels)) + + # 6. 复制图像文件到 YOLO 目录 + dest_img_path = dest_dir / 'images' / split / img_name + shutil.copy(src_img_path, dest_img_path) + success_count += 1 + + # 7. 生成 YOLOv8 训练所需的 YAML 文件 + yaml_content = f"""path: {dest_dir.absolute()} # 数据集根目录 +train: images/train # 训练集图片目录 (相对于 path) +val: images/val # 验证集图片目录 (相对于 path) + +# 类别定义 +names: + 0: fire +""" + yaml_output_path.parent.mkdir(parents=True, exist_ok=True) + with open(yaml_output_path, 'w', encoding='utf-8') as f: + f.write(yaml_content) + + print(f"\n转换完成!成功处理 {success_count} 个样本。") + print(f"YOLO 格式数据集保存在: {dest_dir}") + print(f"YOLO 配置文件已生成: {yaml_output_path}") + +if __name__ == '__main__': + # 原始数据集路径 + SOURCE_DIR = 'path/to/yolov5/Dataset' + # 转换后YOLO数据集的存放路径 + DEST_DIR = 'path/to/yolov5/Dataset_YOLO' + # 要求的 yaml 配置文件生成路径 + YAML_PATH = 'path/to/yolov5/data/data.yaml' + + convert_labelme_to_yolo(SOURCE_DIR, DEST_DIR, YAML_PATH) +``` + +### 1.3 开始训练 + +运行以下代码开始训练yolov5模型 + +```python +python train.py --data ./data/data.yaml --epochs 300 --weights yolov5s.pt --cfg yolov5s.yaml --batch-size 32 +``` + +训练好的模型会保存在./runs/train/weights目录下。 + +## 2. 模型转换 + +### 2.1 导出onnx模型 + +由于是在rv1106上运行,对于模型转换请参考如下文档: + +[yolov5_export_onnx](https://github.com/airockchip/yolov5/blob/master/README_rkopt.md) + +在满足 ./requirements.txt 的环境要求后,执行以下语句导出模型 + +```shell +python export.py --rknpu --weight path/to/your/xxx.pt +``` + +执行完毕后,会生成 ONNX 模型. 假如原始模型为 yolov8s.pt,则生成 yolov8s.onnx 模型。 转换后的onnx模型会保存在runs/detect/train/weights目录下。 + +### 2.2 准备校准数据集 + +在量化时,提供 20~100 张有代表性的图片给 RKNN 即可。请在你的终端中运行以下命令,从你的 Dataset_YOLO/images/train 中提取部分图片路径生成校准文件: + +```shell +cd path/to/yolov5 +# 随机选取50张训练集的图片路径,写入 dataset_subset.txt +ls /path/to/yolov5/Dataset_YOLO/images/train/*.png | head -n 50 > dataset_subset.txt +``` + +- 注意:如果你的图片后缀是 .jpg ,请将上面的 *.png 替换为*.jpg + +### 2.3 转RKNN模型 + +#### 2.3.1 克隆rknn_model_zoo + +```shell +git clone https://github.com/airockchip/rknn_model_zoo.git +cd /path/to/rknn_model_zoo/examples/yolov5/python +``` + +#### 2.3.2 修改convert.py代码 + +主要是将 DATASET_PATH 指向刚才我们生成的 dataset_subset.txt : + +```python +DATASET_PATH = 'path/to/dataset_subset.txt' +``` + +#### 2.3.3 运行convert.py + +在运行转换脚本前请确保你已经配置好了所需要的环境。并且将之前转换好的onnx模型放到以下目录: + +- /path/to/rknn_model_zoo/examples/yolov5/model/ + +执行以下命令开始转换 + +```shell +python convert.py ../model/xxxx.onnx rv1106 +``` + +执行完毕后,如果没有报错,你就会在 rknn_model_zoo/examples/yolov5/model/ 目录下得到 yolov5.rknn 文件,这个文件就可以直接放到你的 RV1106 板子上进行部署板上运行了! diff --git a/Cpp_example/D15_yolov8/CMakeLists.txt b/Cpp_example/D15_yolov8/CMakeLists.txt new file mode 100755 index 0000000000000000000000000000000000000000..289ff4cb39ea4e8fe98a36d651510d55fb0c4ba8 --- /dev/null +++ b/Cpp_example/D15_yolov8/CMakeLists.txt @@ -0,0 +1,43 @@ +# CMake最低版本要求 +cmake_minimum_required(VERSION 3.10) + +project(yolov8) + +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) + +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(yolov8_main + main.cc + postprocess.cc + yolov8.cc + yolov8.h + postprocess.h +) +target_include_directories(yolov8_main PRIVATE ${LOCKZHINER_VISION_MODULE_INCLUDE_DIRS} ${rknpu2_INCLUDE_DIRS} ${RKNPU2_BACKEND_BASE_DIR}) +target_link_libraries(yolov8_main PRIVATE ${OPENCV_LIBRARIES} ${LOCKZHINER_VISION_MODULE_LIBRARIES}) + +install( + TARGETS yolov8_main + RUNTIME DESTINATION . +) \ No newline at end of file diff --git a/Cpp_example/D15_yolov8/README.md b/Cpp_example/D15_yolov8/README.md new file mode 100644 index 0000000000000000000000000000000000000000..33a170e547d9f6252aa359d4eea364b005322604 --- /dev/null +++ b/Cpp_example/D15_yolov8/README.md @@ -0,0 +1,521 @@ +# YOLOv8 目标检测 + +本章节基于 Lockzhiner Vision Module 和 YOLOv8 目标检测模型,实现实时目标检测功能。 + +## 1. 基本知识简介 + +### 1.1 目标检测简介 + +目标检测是计算机视觉领域中的一个关键任务,它不仅需要识别图像中存在哪些对象,还需要定位这些对象的位置。具体来说,目标检测算法会输出每个检测到的对象的边界框(Bounding Box)以及其所属类别的概率或置信度得分。 + +- 应用场景:目标检测技术广泛应用于多个领域,包括但不限于安全监控、自动驾驶汽车、智能零售和医疗影像分析。 + +### 1.2 YOLOv8 简介 + +YOLOv8 是 Ultralytics 在 2023 年推出的最新一代 YOLO 系列目标检测模型。相比 YOLOv5,YOLOv8 在架构和性能上都有显著提升: + +- **Anchor-Free 设计**:摒弃了传统的 Anchor 框机制,采用更简洁的 Anchor-Free 方式,提高了检测精度和泛化能力 +- **解耦头结构**:将分类和回归任务分离,使用两个独立的分支处理,提升了模型性能 +- **新的损失函数**:采用 CIoU Loss 和 DFL (Distribution Focal Loss) 组合,边界框定位更精准 +- **多尺度特征融合**:改进的 PANet 结构,更好地融合不同尺度的特征 +- **高效的主干网络**:优化的 CSPDarknet 结构,在速度和精度之间取得更好的平衡 + +YOLOv8 同样提供了多种模型规格(YOLOv8n/s/m/l/x),适用于从边缘设备到高性能服务器的各种应用场景。 + +## 2. API 文档 + +### 2.1 YOLOv8 模型类 + +#### 2.1.1 头文件 + +```cpp +#include "yolov8.h" +``` + +#### 2.1.2 模型初始化函数 + +```cpp +int init_yolov8_model(const char* model_path, rknn_app_context_t* ctx); +``` + +- 作用:加载 YOLOv8 RKNN 模型并初始化推理上下文 +- 参数: + - model_path:RKNN 模型文件路径 + - ctx:模型上下文指针 +- 返回值: + - 0:初始化成功 + - -1:初始化失败 + +#### 2.1.3 模型推理函数 + +```cpp +int inference_yolov8_model(rknn_app_context_t* ctx, + object_detect_result_list* od_results); +``` + +- 作用:执行模型推理并获取检测结果 +- 参数: + - ctx:已初始化的模型上下文 + - od_results:检测结果存储结构体指针 +- 返回值: + - 0:推理成功 + - -1:推理失败 + +#### 2.1.4 模型释放函数 + +```cpp +int release_yolov8_model(rknn_app_context_t* ctx); +``` + +- 作用:释放模型相关资源 +- 参数: + - ctx:待释放的模型上下文 +- 返回值: + - 0:释放成功 + - -1:释放失败 + +### 2.2 图像处理函数 + +#### 2.2.1 Letterbox 处理 + +```cpp +cv::Mat letterbox(cv::Mat input); +``` + +- 作用:保持图像比例进行缩放,添加灰边填充 +- 参数: + - input:输入图像 (RGB 格式) +- 返回值: + - 返回预处理后的 640x640 图像 + +#### 2.2.2 坐标映射函数 + +```cpp +void mapCoordinates(int *x, int *y); +``` + +- 作用:将模型输出的坐标映射回原始图像坐标系 +- 参数: + - x/y:模型输出坐标指针(输入输出参数) +- 返回值:无 + +### 2.3 结果处理函数 + +#### 2.3.1 后处理初始化 + +```cpp +int init_post_process(); +``` + +- 作用:加载类别标签文件 +- 参数:无 +- 返回值: + - 0:初始化成功 + - -1:初始化失败 + +#### 2.3.2 后处理释放 + +```cpp +void deinit_post_process(); +``` + +- 作用:释放后处理相关资源 +- 参数:无 +- 返回值:无 + +#### 2.3.3 类别名称获取 + +```cpp +char *coco_cls_to_name(int cls_id); +``` + +- 作用:将类别 ID 转换为可读的类别名称 +- 参数: + - cls_id:类别 ID +- 返回值: + - 类别名称字符串 + +#### 2.3.4 结果绘制函数 + +```cpp +void draw_detections(int count, + object_detect_result* results, + cv::Mat& frame, + void (*mapCoord)(int *, int *)); +``` + +- 作用:在图像上绘制检测框和标签 +- 参数: + - count:检测结果数量 + - results:检测结果数组 + - frame:目标图像帧 + - mapCoord:坐标映射函数指针 +- 返回值:无 + +#### 2.3.5 后处理核心函数 + +```cpp +int post_process(rknn_app_context_t *app_ctx, + void *outputs, + float conf_threshold, + float nms_threshold, + object_detect_result_list *od_results); +``` + +- 作用:解码模型输出,执行 DFL 和 NMS 处理 +- 参数: + - app_ctx:模型上下文 + - outputs:模型输出内存数组 + - conf_threshold:置信度阈值(默认 0.25) + - nms_threshold:NMS 阈值(默认 0.45) + - od_results:检测结果输出 +- 返回值: + - 0:处理成功 + - -1:处理失败 + +## 3. 代码解析 + +### 3.1 流程图 + +![](./images/yolov8.png) + +### 3.2 核心代码解析 + +- **模型初始化** + +```cpp +rknn_app_context_t rknn_app_ctx; +const char *model_path = argv[1]; +if (init_yolov8_model(model_path, &rknn_app_ctx) != 0) +{ + printf("❌ Failed to load RKNN model!\n"); + return -1; +} +printf("✅ RKNN model loaded successfully.\n"); +``` + +- **图像预处理** + +```cpp +cv::resize(frame, frame, cv::Size(width, height), 0, 0, cv::INTER_LINEAR); +cv::Mat letterboxImage = letterbox(frame); // 保持比例缩放,填充灰边 + +// 复制数据到 RKNN 输入内存 +memcpy(rknn_app_ctx.input_mems[0]->virt_addr, + letterboxImage.data, + model_width * model_height * 3); +``` + +- **模型推理** + +```cpp +object_detect_result_list od_results; +if (inference_yolov8_model(&rknn_app_ctx, &od_results) != 0) +{ + LOGGER_ERROR("inference_yolov8_model failed"); + return -1; +} +``` + +- **后处理(在 inference_yolov8_model 内部调用)** + +```cpp +// 解码三个检测头输出 +// 1. Box 分支:边界框回归 +// 2. Score_Sum 分支:快速过滤 +// 3. Score 分支:类别置信度 + +// DFL (Distribution Focal Loss) 解码边界框 +compute_dfl(before_dfl, dfl_len, box); + +// NMS (非极大值抑制) 过滤重叠框 +nms(validCount, filterBoxes, classId, indexArray, c, nms_threshold); +``` + +- **结果可视化** + +```cpp +draw_detections(od_results.count, + od_results.results, + frame, + mapCoordinates); +``` + +### 3.3 完整代码实现 + +```cpp +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "yolov8.h" + +// output img size +#define DISP_WIDTH 640 +#define DISP_HEIGHT 480 + +// disp size +int width = DISP_WIDTH; +int height = DISP_HEIGHT; + +// model size +int model_width = 640; +int model_height = 640; + +int leftPadding; +int topPadding; + +// label size +extern int obj_class_num; +char *lable; + +int main(int argc, char *argv[]) +{ + if (argc != 4) + { + LOGGER_INFO("Usage: %s ./yolov8_main model_path ./label size ./label_txt", argv[0]); + return -1; + } + obj_class_num = atoi(argv[2]); + lable = argv[3]; + if (obj_class_num <= 0) + { + LOGGER_ERROR("Invalid class count: %d", obj_class_num); + return -1; + } + + // rknn 上下文结构体 + rknn_app_context_t rknn_app_ctx; + object_detect_result_list od_results; + int ret; + const char *model_path = argv[1]; + memset(&rknn_app_ctx, 0, sizeof(rknn_app_context_t)); + + // Step 1: Load RKNN model + if (init_yolov8_model(model_path, &rknn_app_ctx) != 0) + { + printf("❌ Failed to load RKNN model!\n"); + return -1; + } + printf("✅ RKNN model loaded successfully.\n"); + + // 检查模型输出分支,动态调整类别数量 + int output_per_branch = rknn_app_ctx.io_num.n_output / 3; + int score_idx = output_per_branch > 0 ? (output_per_branch - 1) : -1; + if (score_idx >= 0) + { + int model_class_num = rknn_app_ctx.output_attrs[score_idx].dims[3]; + if (model_class_num > 0 && obj_class_num != model_class_num) + { + LOGGER_INFO("Class count mismatch, use model class count %d instead of arg %d", + model_class_num, obj_class_num); + obj_class_num = model_class_num; + } + } + + // 加载标签文件 + if (init_post_process() != 0) + { + LOGGER_ERROR("init_post_process failed"); + release_yolov8_model(&rknn_app_ctx); + 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; + } + + 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 frame; + std::chrono::steady_clock::time_point start, end; + + while (true) + { + start = std::chrono::steady_clock::now(); + cap >> frame; + if (frame.empty()) + { + LOGGER_INFO("❌ Failed to read frame from camera.\n"); + continue; + } + cv::resize(frame, frame, cv::Size(width, height), 0, 0, cv::INTER_LINEAR); + cv::Mat letterboxImage = letterbox(frame); + + if (letterboxImage.empty() || + letterboxImage.total() * letterboxImage.elemSize() != model_width * model_height * 3) + { + LOGGER_ERROR("❌ Input image format or size mismatch!\n"); + release_yolov8_model(&rknn_app_ctx); + return -1; + } + + if (rknn_app_ctx.input_mems == nullptr || + rknn_app_ctx.input_mems[0] == nullptr) + { + LOGGER_ERROR("❌ RKNN input memory not allocated!\n"); + release_yolov8_model(&rknn_app_ctx); + return -1; + } + + memcpy(rknn_app_ctx.input_mems[0]->virt_addr, + letterboxImage.data, + model_width * model_height * 3); + + if (inference_yolov8_model(&rknn_app_ctx, &od_results) != 0) + { + LOGGER_ERROR("inference_yolov8_model failed"); + release_yolov8_model(&rknn_app_ctx); + return -1; + } + + draw_detections(od_results.count, + od_results.results, + frame, + mapCoordinates); + + edit.Print(frame); + + end = std::chrono::steady_clock::now(); + double elapsed_time = std::chrono::duration(end - start).count(); + printf("Frame processed in %.4f seconds\n", elapsed_time); + } + + release_yolov8_model(&rknn_app_ctx); + deinit_post_process(); + cap.release(); + return 0; +} +``` + +## 4. 编译过程 + +### 4.1 编译环境搭建 + +- 请确保你已经按照 [开发环境搭建指南](../../../../docs/introductory_tutorial/cpp_development_environment.md) 正确配置了开发环境。 +- 同时已正确连接开发板。 + +### 4.2 CMake 介绍 + +```cmake +# CMake 最低版本要求 +cmake_minimum_required(VERSION 3.10) + +project(yolov8) + +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) + +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(yolov8_main + main.cc + postprocess.cc + yolov8.cc + yolov8.h + postprocess.h +) +target_include_directories(yolov8_main PRIVATE + ${LOCKZHINER_VISION_MODULE_INCLUDE_DIRS} + ${rknpu2_INCLUDE_DIRS} + ${RKNPU2_BACKEND_BASE_DIR} +) +target_link_libraries(yolov8_main PRIVATE + ${OPENCV_LIBRARIES} + ${LOCKZHINER_VISION_MODULE_LIBRARIES} +) + +install( + TARGETS yolov8_main + RUNTIME DESTINATION . +) +``` + +### 4.3 编译项目 + +使用 Docker Desktop 打开 LockzhinerVisionModule 容器并执行以下命令来编译项目: + +```bash +# 进入 Demo 所在目录 +cd /LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/D15_yolov8 + +# 创建编译目录 +rm -rf build && mkdir build && cd build + +# 配置交叉编译工具链 +export TOOLCHAIN_ROOT_PATH="/LockzhinerVisionModuleWorkSpace/arm-rockchip830-linux-uclibcgnueabihf" + +# 使用 cmake 配置项目 +cmake .. + +# 执行编译项目 +make -j8 && make install +``` + +在执行完上述命令后,会在 build 目录下生成可执行文件 `yolov8_main`。 + +## 5. 例程运行示例 + +### 5.1 运行 + +```shell +chmod 777 yolov8_main + +# 语法格式 +./yolov8_main + +# 实际示例(使用自定义火焰数据集训练的 1 类别模型) +./yolov8_main ./yolov8_fire_rv1106.rknn 1 ./labels.txt + +``` + +**参数说明:** + +- `model_path`:YOLOv8 RKNN 模型文件路径 +- `class_count`:类别数量 +- `label_file_path`:标签文件路径,每行一个类别名称 + +### 5.2 结果展示 + +- 可以准确识别火焰 +- 实时显示检测框、类别名称和置信度 +- 支持摄像头实时视频流检测 + +![](./images/yolov8_demo.png) diff --git a/Cpp_example/D15_yolov8/images/yolov8.png b/Cpp_example/D15_yolov8/images/yolov8.png new file mode 100755 index 0000000000000000000000000000000000000000..75a71d3a0f064a4796a2e77606f085a9419b1060 Binary files /dev/null and b/Cpp_example/D15_yolov8/images/yolov8.png differ diff --git a/Cpp_example/D15_yolov8/images/yolov8_demo.png b/Cpp_example/D15_yolov8/images/yolov8_demo.png new file mode 100755 index 0000000000000000000000000000000000000000..644656ccd232e88de1109049128febbdc8cf2fff Binary files /dev/null and b/Cpp_example/D15_yolov8/images/yolov8_demo.png differ diff --git a/Cpp_example/D15_yolov8/main.cc b/Cpp_example/D15_yolov8/main.cc new file mode 100755 index 0000000000000000000000000000000000000000..5221bf1a979fb92d0dc6990206ba08c4bc3041f9 --- /dev/null +++ b/Cpp_example/D15_yolov8/main.cc @@ -0,0 +1,161 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "yolov8.h" + +// output img size +#define DISP_WIDTH 640 +#define DISP_HEIGHT 480 + +// disp size +int width = DISP_WIDTH; +int height = DISP_HEIGHT; + +// model size +int model_width = 640; +int model_height = 640; + +int leftPadding; +int topPadding; + +// label size +extern int obj_class_num; +char *lable; + +int main(int argc, char *argv[]) +{ + if (argc != 4) + { + LOGGER_INFO("Usage: %s ./yolov8_main model_path ./label size ./label_txt", argv[0]); + return -1; + } + obj_class_num = atoi(argv[2]); + lable = argv[3]; + if (obj_class_num <= 0) + { + LOGGER_ERROR("Invalid class count: %d", obj_class_num); + return -1; + } + + // rknn上下文结构体 + rknn_app_context_t rknn_app_ctx; + object_detect_result_list od_results; + int ret; + const char *model_path = argv[1]; + memset(&rknn_app_ctx, 0, sizeof(rknn_app_context_t)); + + // Step 1: Load RKNN model + if (init_yolov8_model(model_path, &rknn_app_ctx) != 0) + { + printf("❌ Failed to load RKNN model!\n"); + return -1; + } + printf("✅ RKNN model loaded successfully.\n"); + + int output_per_branch = rknn_app_ctx.io_num.n_output / 3; + int score_idx = output_per_branch > 0 ? (output_per_branch - 1) : -1; + if (score_idx >= 0) + { + int model_class_num = rknn_app_ctx.output_attrs[score_idx].dims[3]; + if (model_class_num > 0 && obj_class_num != model_class_num) + { + LOGGER_INFO("Class count mismatch, use model class count %d instead of arg %d", model_class_num, obj_class_num); + obj_class_num = model_class_num; + } + } + + // 加载标签文件 + if (init_post_process() != 0) + { + LOGGER_ERROR("init_post_process failed"); + release_yolov8_model(&rknn_app_ctx); + 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; + } + + 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 frame; + std::chrono::steady_clock::time_point start, end; + + while (true) + { + start = std::chrono::steady_clock::now(); + cap >> frame; + if (frame.empty()) + { + LOGGER_INFO("❌ Failed to read frame from camera.\n"); + continue; + } + cv::resize(frame, frame, cv::Size(width, height), 0, 0, cv::INTER_LINEAR); + cv::Mat letterboxImage = letterbox(frame); + + if (letterboxImage.empty() || letterboxImage.total() * letterboxImage.elemSize() != model_width * model_height * 3) + { + LOGGER_ERROR("❌ Input image format or size mismatch!\n"); + release_yolov8_model(&rknn_app_ctx); + return -1; + } + + if (rknn_app_ctx.input_mems == nullptr || rknn_app_ctx.input_mems[0] == nullptr) + { + LOGGER_ERROR("❌ RKNN input memory not allocated!\n"); + release_yolov8_model(&rknn_app_ctx); + return -1; + } + + memcpy(rknn_app_ctx.input_mems[0]->virt_addr, letterboxImage.data, model_width * model_height * 3); + + if (inference_yolov8_model(&rknn_app_ctx, &od_results) != 0) + { + LOGGER_ERROR("inference_yolov8_model failed"); + release_yolov8_model(&rknn_app_ctx); + return -1; + } + + // printf("draw_detections start, count=%d\n", od_results.count); + // fflush(stdout); + draw_detections(od_results.count, + od_results.results, + frame, + mapCoordinates); + // printf("draw_detections done\n"); + // fflush(stdout); + + // printf("edit.Print start\n"); + // fflush(stdout); + + edit.Print(frame); + + // printf("edit.Print done\n"); + // fflush(stdout); + end = std::chrono::steady_clock::now(); + double elapsed_time = std::chrono::duration(end - start).count(); + printf("Frame processed in %.4f seconds\n", elapsed_time); + } + release_yolov8_model(&rknn_app_ctx); + deinit_post_process(); + cap.release(); + return 0; +} \ No newline at end of file diff --git a/Cpp_example/D15_yolov8/model/fire_label.txt b/Cpp_example/D15_yolov8/model/fire_label.txt new file mode 100755 index 0000000000000000000000000000000000000000..69b21656c0ecea94332352c454ba30bf1e0586bb --- /dev/null +++ b/Cpp_example/D15_yolov8/model/fire_label.txt @@ -0,0 +1 @@ +fire \ No newline at end of file diff --git a/Cpp_example/D15_yolov8/postprocess.cc b/Cpp_example/D15_yolov8/postprocess.cc new file mode 100755 index 0000000000000000000000000000000000000000..ec6c51ce746d0f56d5972ea22b406ae75ddd863a --- /dev/null +++ b/Cpp_example/D15_yolov8/postprocess.cc @@ -0,0 +1,564 @@ +#include "yolov8.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#define LABEL_NALE_TXT_PATH "./model/coco_80_labels_list.txt" + +int OBJ_CLASS_NUM = 1; // 默认值(通过 init_post_process 修改) +static char** labels = nullptr; // 动态分配的标签数组 +int obj_class_num = 0; + + +inline static int clamp(float val, int min, int max) { return val > min ? (val < max ? val : max) : min; } +extern char *lable; +static char *readLine(FILE *fp, char *buffer, int *len) +{ + int ch; + int i = 0; + size_t buff_len = 0; + + buffer = (char *)malloc(buff_len + 1); + if (!buffer) + return NULL; // Out of memory + + while ((ch = fgetc(fp)) != '\n' && ch != EOF) + { + if (ch == '\r') continue; // 过滤 Windows 的 \r 换行符,防止 cv::putText 卡死 + buff_len++; + void *tmp = realloc(buffer, buff_len + 1); + if (tmp == NULL) + { + free(buffer); + return NULL; // Out of memory + } + buffer = (char *)tmp; + + buffer[i] = (char)ch; + i++; + } + buffer[i] = '\0'; + + *len = buff_len; + + // Detect end + if (ch == EOF && (i == 0 || ferror(fp))) + { + free(buffer); + return NULL; + } + return buffer; +} + +static int readLines(const char *fileName, char *lines[], int max_line) +{ + FILE *file = fopen(fileName, "r"); + char *s = NULL; + int i = 0; + int n = 0; + + if (file == NULL) + { + printf("Open %s fail!\n", fileName); + return -1; + } + + while ((s = readLine(file, s, &n)) != NULL) + { + lines[i++] = s; + if (i >= max_line) + break; + } + fclose(file); + return i; +} + +static int loadLabelName(const char *locationFilename, char *label[]) +{ + printf("load lable %s\n", locationFilename); + readLines(locationFilename, label, OBJ_CLASS_NUM); + return 0; +} + +static float CalculateOverlap(float xmin0, float ymin0, float xmax0, float ymax0, float xmin1, float ymin1, float xmax1, + float ymax1) +{ + float w = fmax(0.f, fmin(xmax0, xmax1) - fmax(xmin0, xmin1) + 1.0); + float h = fmax(0.f, fmin(ymax0, ymax1) - fmax(ymin0, ymin1) + 1.0); + float i = w * h; + float u = (xmax0 - xmin0 + 1.0) * (ymax0 - ymin0 + 1.0) + (xmax1 - xmin1 + 1.0) * (ymax1 - ymin1 + 1.0) - i; + return u <= 0.f ? 0.f : (i / u); +} + +static int nms(int validCount, std::vector &outputLocations, std::vector classIds, std::vector &order, + int filterId, float threshold) +{ + for (int i = 0; i < validCount; ++i) + { + int n = order[i]; + if (n == -1 || classIds[n] != filterId) + { + continue; + } + for (int j = i + 1; j < validCount; ++j) + { + int m = order[j]; + if (m == -1 || classIds[m] != filterId) + { + continue; + } + float xmin0 = outputLocations[n * 4 + 0]; + float ymin0 = outputLocations[n * 4 + 1]; + float xmax0 = outputLocations[n * 4 + 0] + outputLocations[n * 4 + 2]; + float ymax0 = outputLocations[n * 4 + 1] + outputLocations[n * 4 + 3]; + + float xmin1 = outputLocations[m * 4 + 0]; + float ymin1 = outputLocations[m * 4 + 1]; + float xmax1 = outputLocations[m * 4 + 0] + outputLocations[m * 4 + 2]; + float ymax1 = outputLocations[m * 4 + 1] + outputLocations[m * 4 + 3]; + + float iou = CalculateOverlap(xmin0, ymin0, xmax0, ymax0, xmin1, ymin1, xmax1, ymax1); + + if (iou > threshold) + { + order[j] = -1; + } + } + } + return 0; +} + +static int quick_sort_indice_inverse(std::vector &input, int left, int right, std::vector &indices) +{ + float key; + int key_index; + int low = left; + int high = right; + if (left < right) + { + key_index = indices[left]; + key = input[left]; + while (low < high) + { + while (low < high && input[high] <= key) + { + high--; + } + input[low] = input[high]; + indices[low] = indices[high]; + while (low < high && input[low] >= key) + { + low++; + } + input[high] = input[low]; + indices[high] = indices[low]; + } + input[low] = key; + indices[low] = key_index; + quick_sort_indice_inverse(input, left, low - 1, indices); + quick_sort_indice_inverse(input, low + 1, right, indices); + } + return low; +} + +static float sigmoid(float x) { return 1.0 / (1.0 + expf(-x)); } + +static float unsigmoid(float y) { return -1.0 * logf((1.0 / y) - 1.0); } + +inline static int32_t __clip(float val, float min, float max) +{ + float f = val <= min ? min : (val >= max ? max : val); + return f; +} + +static int8_t qnt_f32_to_affine(float f32, int32_t zp, float scale) +{ + float dst_val = (f32 / scale) + zp; + int8_t res = (int8_t)__clip(dst_val, -128, 127); + return res; +} + +static float deqnt_affine_to_f32(int8_t qnt, int32_t zp, float scale) { return ((float)qnt - (float)zp) * scale; } + +static void compute_dfl(float* tensor, int dfl_len, float* box){ + for (int b=0; b<4; b++){ + float exp_t[dfl_len]; + float exp_sum=0; + float acc_sum=0; + for (int i=0; i< dfl_len; i++){ + exp_t[i] = exp(tensor[i+b*dfl_len]); + exp_sum += exp_t[i]; + } + + for (int i=0; i< dfl_len; i++){ + acc_sum += exp_t[i]/exp_sum *i; + } + box[b] = acc_sum; + } +} + +static int process_i8(int8_t *box_tensor, int32_t box_zp, float box_scale, + int8_t *score_tensor, int32_t score_zp, float score_scale, + int8_t *score_sum_tensor, int32_t score_sum_zp, float score_sum_scale, + int grid_h, int grid_w, int stride, int dfl_len, + std::vector &boxes, + std::vector &objProbs, + std::vector &classId, + float threshold) +{ + int validCount = 0; + int grid_len = grid_h * grid_w; + int8_t score_thres_i8 = qnt_f32_to_affine(threshold, score_zp, score_scale); + int8_t score_sum_thres_i8 = qnt_f32_to_affine(threshold, score_sum_zp, score_sum_scale); + + for (int i = 0; i < grid_h; i++) + { + for (int j = 0; j < grid_w; j++) + { + int offset = i* grid_w + j; + int max_class_id = -1; + + // 通过 score sum 起到快速过滤的作用 + if (score_sum_tensor != nullptr){ + if (score_sum_tensor[offset] < score_sum_thres_i8){ + continue; + } + } + + int8_t max_score = -score_zp; + for (int c= 0; c< OBJ_CLASS_NUM; c++){ + if ((score_tensor[offset] > score_thres_i8) && (score_tensor[offset] > max_score)) + { + max_score = score_tensor[offset]; + max_class_id = c; + } + offset += grid_len; + } + + // compute box + if (max_score> score_thres_i8){ + offset = i* grid_w + j; + float box[4]; + float before_dfl[dfl_len*4]; + for (int k=0; k< dfl_len*4; k++){ + before_dfl[k] = deqnt_affine_to_f32(box_tensor[offset], box_zp, box_scale); + offset += grid_len; + } + compute_dfl(before_dfl, dfl_len, box); + + float x1,y1,x2,y2,w,h; + x1 = (-box[0] + j + 0.5)*stride; + y1 = (-box[1] + i + 0.5)*stride; + x2 = (box[2] + j + 0.5)*stride; + y2 = (box[3] + i + 0.5)*stride; + w = x2 - x1; + h = y2 - y1; + boxes.push_back(x1); + boxes.push_back(y1); + boxes.push_back(w); + boxes.push_back(h); + + objProbs.push_back(deqnt_affine_to_f32(max_score, score_zp, score_scale)); + classId.push_back(max_class_id); + validCount ++; + } + } + } + return validCount; +} + +static int process_fp32(float *box_tensor, float *score_tensor, float *score_sum_tensor, + int grid_h, int grid_w, int stride, int dfl_len, + std::vector &boxes, + std::vector &objProbs, + std::vector &classId, + float threshold) +{ + int validCount = 0; + int grid_len = grid_h * grid_w; + for (int i = 0; i < grid_h; i++) + { + for (int j = 0; j < grid_w; j++) + { + int offset = i* grid_w + j; + int max_class_id = -1; + + // 通过 score sum 起到快速过滤的作用 + if (score_sum_tensor != nullptr){ + if (score_sum_tensor[offset] < threshold){ + continue; + } + } + + float max_score = 0; + for (int c= 0; c< OBJ_CLASS_NUM; c++){ + if ((score_tensor[offset] > threshold) && (score_tensor[offset] > max_score)) + { + max_score = score_tensor[offset]; + max_class_id = c; + } + offset += grid_len; + } + + // compute box + if (max_score> threshold){ + offset = i* grid_w + j; + float box[4]; + float before_dfl[dfl_len*4]; + for (int k=0; k< dfl_len*4; k++){ + before_dfl[k] = box_tensor[offset]; + offset += grid_len; + } + compute_dfl(before_dfl, dfl_len, box); + + float x1,y1,x2,y2,w,h; + x1 = (-box[0] + j + 0.5)*stride; + y1 = (-box[1] + i + 0.5)*stride; + x2 = (box[2] + j + 0.5)*stride; + y2 = (box[3] + i + 0.5)*stride; + w = x2 - x1; + h = y2 - y1; + boxes.push_back(x1); + boxes.push_back(y1); + boxes.push_back(w); + boxes.push_back(h); + + objProbs.push_back(max_score); + classId.push_back(max_class_id); + validCount ++; + } + } + } + return validCount; +} + + +static int process_i8_rv1106(int8_t *box_tensor, int32_t box_zp, float box_scale, + int8_t *score_tensor, int32_t score_zp, float score_scale, + int8_t *score_sum_tensor, int32_t score_sum_zp, float score_sum_scale, + int grid_h, int grid_w, int stride, int dfl_len, int score_channels, + std::vector &boxes, + std::vector &objProbs, + std::vector &classId, + float threshold) { + int validCount = 0; + int grid_len = grid_h * grid_w; + int8_t score_thres_i8 = qnt_f32_to_affine(threshold, score_zp, score_scale); + int8_t score_sum_thres_i8 = qnt_f32_to_affine(threshold, score_sum_zp, score_sum_scale); + + for (int i = 0; i < grid_h; i++) { + for (int j = 0; j < grid_w; j++) { + int offset = i * grid_w + j; + int max_class_id = -1; + + // 通过 score sum 起到快速过滤的作用 + if (score_sum_tensor != nullptr) { + //score_sum_tensor [1, 1, 80, 80] + if (score_sum_tensor[offset] < score_sum_thres_i8) { + continue; + } + } + + int8_t max_score = -128; + int class_count = OBJ_CLASS_NUM < score_channels ? OBJ_CLASS_NUM : score_channels; + offset = offset * score_channels; + for (int c = 0; c < class_count; c++) { + int8_t score = score_tensor[offset + c]; + if ((score > score_thres_i8) && (score > max_score)) { + max_score = score; + max_class_id = c; + } + } + + // compute box + if (max_class_id >= 0 && max_score > score_thres_i8) { + offset = (i * grid_w + j) * 4 * dfl_len; + float box[4]; + float before_dfl[dfl_len*4]; + for (int k=0; k< dfl_len*4; k++){ + before_dfl[k] = deqnt_affine_to_f32(box_tensor[offset + k], box_zp, box_scale); + } + compute_dfl(before_dfl, dfl_len, box); + + float x1, y1, x2, y2, w, h; + x1 = (-box[0] + j + 0.5) * stride; + y1 = (-box[1] + i + 0.5) * stride; + x2 = (box[2] + j + 0.5) * stride; + y2 = (box[3] + i + 0.5) * stride; + w = x2 - x1; + h = y2 - y1; + boxes.push_back(x1); + boxes.push_back(y1); + boxes.push_back(w); + boxes.push_back(h); + + objProbs.push_back(deqnt_affine_to_f32(max_score, score_zp, score_scale)); + classId.push_back(max_class_id); + validCount ++; + } + } + } + return validCount; +} + +int post_process(rknn_app_context_t *app_ctx, void *outputs, float conf_threshold, float nms_threshold, object_detect_result_list *od_results) +{ + rknn_tensor_mem **_outputs = (rknn_tensor_mem **)outputs; + std::vector filterBoxes; + std::vector objProbs; + std::vector classId; + int validCount = 0; + int stride = 0; + int grid_h = 0; + int grid_w = 0; + int model_in_w = app_ctx->model_width; + int model_in_h = app_ctx->model_height; + + memset(od_results, 0, sizeof(object_detect_result_list)); + + // default 3 branch + int output_per_branch = app_ctx->io_num.n_output / 3; + for (int i = 0; i < 3; i++) + { + int box_idx = i * output_per_branch; + int dfl_len = app_ctx->output_attrs[box_idx].dims[3] / 4; + void *score_sum = nullptr; + int32_t score_sum_zp = 0; + float score_sum_scale = 1.0; + if (output_per_branch == 3) { + score_sum = _outputs[i * output_per_branch + 1]->virt_addr; // 修复:YOLOv8 中 score_sum 在索引 1 + score_sum_zp = app_ctx->output_attrs[i * output_per_branch + 1].zp; + score_sum_scale = app_ctx->output_attrs[i * output_per_branch + 1].scale; + } + int score_idx = i * output_per_branch + 2; // 修复:YOLOv8 中 score 在索引 2 + grid_h = app_ctx->output_attrs[box_idx].dims[1]; + grid_w = app_ctx->output_attrs[box_idx].dims[2]; + stride = model_in_h / grid_h; + + if (app_ctx->is_quant) { + validCount += process_i8_rv1106((int8_t *)_outputs[box_idx]->virt_addr, app_ctx->output_attrs[box_idx].zp, app_ctx->output_attrs[box_idx].scale, + (int8_t *)_outputs[score_idx]->virt_addr, app_ctx->output_attrs[score_idx].zp, + app_ctx->output_attrs[score_idx].scale, (int8_t *)score_sum, score_sum_zp, score_sum_scale, + grid_h, grid_w, stride, dfl_len, app_ctx->output_attrs[score_idx].dims[3], filterBoxes, objProbs, classId, conf_threshold); + } + else + { + printf("RV1106 only support quantization mode\n"); + return -1; + } + } + + // no object detect + if (validCount <= 0) + { + return 0; + } + std::vector indexArray; + for (int i = 0; i < validCount; ++i) + { + indexArray.push_back(i); + } + quick_sort_indice_inverse(objProbs, 0, validCount - 1, indexArray); + + std::set class_set(std::begin(classId), std::end(classId)); + + for (auto c : class_set) + { + nms(validCount, filterBoxes, classId, indexArray, c, nms_threshold); + } + + int last_count = 0; + od_results->count = 0; + + /* box valid detect target */ + for (int i = 0; i < validCount; ++i) + { + if (indexArray[i] == -1 || last_count >= OBJ_NUMB_MAX_SIZE) + { + continue; + } + int n = indexArray[i]; + + float x1 = filterBoxes[n * 4 + 0]; + float y1 = filterBoxes[n * 4 + 1]; + float x2 = x1 + filterBoxes[n * 4 + 2]; + float y2 = y1 + filterBoxes[n * 4 + 3]; + int id = classId[n]; + float obj_conf = objProbs[n]; + + od_results->results[last_count].box.left = (int)(clamp(x1, 0, model_in_w)); + od_results->results[last_count].box.top = (int)(clamp(y1, 0, model_in_h)); + od_results->results[last_count].box.right = (int)(clamp(x2, 0, model_in_w)); + od_results->results[last_count].box.bottom = (int)(clamp(y2, 0, model_in_h)); + od_results->results[last_count].prop = obj_conf; + od_results->results[last_count].cls_id = id; + last_count++; + } + od_results->count = last_count; + return 0; +} + +int init_post_process() +{ + OBJ_CLASS_NUM = obj_class_num; + + // 释放旧内存(防止内存泄漏) + if (labels != nullptr) { + deinit_post_process(); + } + + // 分配新内存并清零(防止野指针) + labels = new char*[OBJ_CLASS_NUM]; + memset(labels, 0, sizeof(char*) * OBJ_CLASS_NUM); + + int ret = 0; + ret = loadLabelName(lable, labels); + if (ret < 0) + { + printf("Load %s failed!\n", lable); + return -1; + } + return 0; +} + +char *coco_cls_to_name(int cls_id) +{ + + static char null_str[] = "null"; + + if (cls_id >= OBJ_CLASS_NUM) + { + return null_str; + } + + if (labels[cls_id]) + { + return labels[cls_id]; + } + + return null_str; +} + +void deinit_post_process() +{ + if (labels == nullptr) + { + return; + } + for (int i = 0; i < OBJ_CLASS_NUM; i++) + { + if (labels[i] != nullptr) + { + free(labels[i]); + labels[i] = nullptr; + } + } + delete[] labels; + labels = nullptr; +} \ No newline at end of file diff --git a/Cpp_example/D15_yolov8/postprocess.h b/Cpp_example/D15_yolov8/postprocess.h new file mode 100755 index 0000000000000000000000000000000000000000..d467c307692010d69e898264b4581fd21a592652 --- /dev/null +++ b/Cpp_example/D15_yolov8/postprocess.h @@ -0,0 +1,38 @@ +#ifndef _RKNN_YOLOV8_DEMO_POSTPROCESS_H_ +#define _RKNN_YOLOV8_DEMO_POSTPROCESS_H_ + +#include +#include +#include "rknpu2_backend/rknpu2_backend.h" + +#define OBJ_NAME_MAX_SIZE 64 +#define OBJ_NUMB_MAX_SIZE 128 +#define NMS_THRESH 0.45 +#define BOX_THRESH 0.25 + +typedef struct { + int left; + int top; + int right; + int bottom; +} image_rect_t; + +typedef struct { + image_rect_t box; + float prop; + int cls_id; +} object_detect_result; + +typedef struct { + int id; + int count; + object_detect_result results[OBJ_NUMB_MAX_SIZE]; +} object_detect_result_list; + +int init_post_process(); +void deinit_post_process(); +char *coco_cls_to_name(int cls_id); +int post_process(rknn_app_context_t *app_ctx, void *outputs, float conf_threshold, float nms_threshold, object_detect_result_list *od_results); + +void deinitPostProcess(); +#endif //_RKNN_YOLOV8_DEMO_POSTPROCESS_H_ \ No newline at end of file diff --git a/Cpp_example/D15_yolov8/yolov8.cc b/Cpp_example/D15_yolov8/yolov8.cc new file mode 100755 index 0000000000000000000000000000000000000000..fdb6258723d01f54640143c45eb776ea5bae799c --- /dev/null +++ b/Cpp_example/D15_yolov8/yolov8.cc @@ -0,0 +1,273 @@ +#include +#include +#include +#include +#include +#include +#include "yolov8.h" + +static void dump_tensor_attr(rknn_tensor_attr *attr) +{ + printf(" index=%d, name=%s, n_dims=%d, dims=[%d, %d, %d, %d], n_elems=%d, size=%d, fmt=%s, type=%s, qnt_type=%s, " + "zp=%d, scale=%f\n", + attr->index, attr->name, attr->n_dims, attr->dims[0], attr->dims[1], attr->dims[2], attr->dims[3], + attr->n_elems, attr->size, get_format_string(attr->fmt), get_type_string(attr->type), + get_qnt_type_string(attr->qnt_type), attr->zp, attr->scale); +} + +int init_yolov8_model(const char *model_path, rknn_app_context_t *app_ctx) +{ + int ret; + int model_len = 0; + char *model; + rknn_context ctx = 0; + + ret = rknn_init(&ctx, (char *)model_path, 0, 0, NULL); + if (ret < 0) + { + printf("rknn_init fail! ret=%d\n", ret); + return -1; + } + + // Get Model Input Output Number + rknn_input_output_num io_num; + ret = rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num)); + if (ret != RKNN_SUCC) + { + printf("rknn_query fail! ret=%d\n", ret); + return -1; + } + printf("model input num: %d, output num: %d\n", io_num.n_input, io_num.n_output); + + // Get Model Input Info + printf("input tensors:\n"); + rknn_tensor_attr input_attrs[io_num.n_input]; + memset(input_attrs, 0, sizeof(input_attrs)); + for (int i = 0; i < io_num.n_input; i++) + { + input_attrs[i].index = i; + ret = rknn_query(ctx, RKNN_QUERY_NATIVE_INPUT_ATTR, &(input_attrs[i]), sizeof(rknn_tensor_attr)); + if (ret != RKNN_SUCC) + { + printf("rknn_query fail! ret=%d\n", ret); + return -1; + } + dump_tensor_attr(&(input_attrs[i])); + } + + // Get Model Output Info + printf("output tensors:\n"); + rknn_tensor_attr output_attrs[io_num.n_output]; + memset(output_attrs, 0, sizeof(output_attrs)); + for (int i = 0; i < io_num.n_output; i++) + { + output_attrs[i].index = i; + //When using the zero-copy API interface, query the native output tensor attribute + ret = rknn_query(ctx, RKNN_QUERY_NATIVE_NHWC_OUTPUT_ATTR, &(output_attrs[i]), sizeof(rknn_tensor_attr)); + if (ret != RKNN_SUCC) + { + printf("rknn_query fail! ret=%d\n", ret); + return -1; + } + dump_tensor_attr(&(output_attrs[i])); + } + + // default input type is int8 (normalize and quantize need compute in outside) + // if set uint8, will fuse normalize and quantize to npu + input_attrs[0].type = RKNN_TENSOR_UINT8; + // default fmt is NHWC,1106 npu only support NHWC in zero copy mode + input_attrs[0].fmt = RKNN_TENSOR_NHWC; + printf("input_attrs[0].size_with_stride=%d\n", input_attrs[0].size_with_stride); + app_ctx->input_mems[0] = rknn_create_mem(ctx, input_attrs[0].size_with_stride); + + // Set input tensor memory + ret = rknn_set_io_mem(ctx, app_ctx->input_mems[0], &input_attrs[0]); + if (ret < 0) { + printf("input_mems rknn_set_io_mem fail! ret=%d\n", ret); + return -1; + } + + // Set output tensor memory + for (uint32_t i = 0; i < io_num.n_output; ++i) { + app_ctx->output_mems[i] = rknn_create_mem(ctx, output_attrs[i].size_with_stride); + ret = rknn_set_io_mem(ctx, app_ctx->output_mems[i], &output_attrs[i]); + if (ret < 0) { + printf("output_mems rknn_set_io_mem fail! ret=%d\n", ret); + return -1; + } + } + + // Set to context + app_ctx->rknn_ctx = ctx; + + // TODO + if (output_attrs[0].qnt_type == RKNN_TENSOR_QNT_AFFINE_ASYMMETRIC) + { + app_ctx->is_quant = true; + } + else + { + app_ctx->is_quant = false; + } + + app_ctx->io_num = io_num; + app_ctx->input_attrs = (rknn_tensor_attr *)malloc(io_num.n_input * sizeof(rknn_tensor_attr)); + memcpy(app_ctx->input_attrs, input_attrs, io_num.n_input * sizeof(rknn_tensor_attr)); + app_ctx->output_attrs = (rknn_tensor_attr *)malloc(io_num.n_output * sizeof(rknn_tensor_attr)); + memcpy(app_ctx->output_attrs, output_attrs, io_num.n_output * sizeof(rknn_tensor_attr)); + + if (input_attrs[0].fmt == RKNN_TENSOR_NCHW) + { + printf("model is NCHW input fmt\n"); + app_ctx->model_channel = input_attrs[0].dims[1]; + app_ctx->model_height = input_attrs[0].dims[2]; + app_ctx->model_width = input_attrs[0].dims[3]; + } else + { + printf("model is NHWC input fmt\n"); + app_ctx->model_height = input_attrs[0].dims[1]; + app_ctx->model_width = input_attrs[0].dims[2]; + app_ctx->model_channel = input_attrs[0].dims[3]; + } + + printf("model input height=%d, width=%d, channel=%d\n", + app_ctx->model_height, app_ctx->model_width, app_ctx->model_channel); + + return 0; +} + +int release_yolov8_model(rknn_app_context_t *app_ctx) +{ + if (app_ctx->input_attrs != NULL) + { + free(app_ctx->input_attrs); + app_ctx->input_attrs = NULL; + } + if (app_ctx->output_attrs != NULL) + { + free(app_ctx->output_attrs); + app_ctx->output_attrs = NULL; + } + for (int i = 0; i < app_ctx->io_num.n_input; i++) { + if (app_ctx->input_mems[i] != NULL) { + rknn_destroy_mem(app_ctx->rknn_ctx, app_ctx->input_mems[i]); + } + } + for (int i = 0; i < app_ctx->io_num.n_output; i++) { + if (app_ctx->output_mems[i] != NULL) { + rknn_destroy_mem(app_ctx->rknn_ctx, app_ctx->output_mems[i]); + } + } + if (app_ctx->rknn_ctx != 0) + { + rknn_destroy(app_ctx->rknn_ctx); + app_ctx->rknn_ctx = 0; + } + + printf("Release success\n"); + return 0; +} + +int inference_yolov8_model(rknn_app_context_t *app_ctx, object_detect_result_list *od_results) +{ + int ret; + const float nms_threshold = NMS_THRESH; + const float box_conf_threshold = BOX_THRESH; + + + memset(od_results, 0x00, sizeof(*od_results)); + + printf("rknn_run...\n"); + auto rknn_start = std::chrono::steady_clock::now(); + ret = rknn_run(app_ctx->rknn_ctx, nullptr); + auto rknn_end = std::chrono::steady_clock::now(); + double rknn_time = std::chrono::duration(rknn_end - rknn_start).count(); + printf("rknn_run done, ret=%d, cost time: %.4f seconds\n", ret, rknn_time); + if (ret < 0) { + printf("rknn_run fail! ret=%d\n", ret); + return -1; + } + + post_process(app_ctx, app_ctx->output_mems, box_conf_threshold, nms_threshold, od_results); +out: + return ret; +} + +/* +预处理 +图片填充缩放 +*/ +float scale; +extern int model_width; +extern int model_height; + +extern int width; +extern int height; + +extern int leftPadding; +extern int topPadding; +cv::Mat letterbox(cv::Mat input) +{ + float scaleX = (float)model_width / (float)width; + float scaleY = (float)model_height / (float)height; + scale = scaleX < scaleY ? scaleX : scaleY; + + int inputWidth = (int)((float)width * scale); + int inputHeight = (int)((float)height * scale); + + leftPadding = (model_width - inputWidth) / 2; + topPadding = (model_height - inputHeight) / 2; + + cv::Mat inputScale; + cv::resize(input, inputScale, cv::Size(inputWidth, inputHeight), 0, 0, cv::INTER_LINEAR); + cv::Mat letterboxImage(model_width, model_width, CV_8UC3, cv::Scalar(0, 0, 0)); + cv::Rect roi(leftPadding, topPadding, inputWidth, inputHeight); + inputScale.copyTo(letterboxImage(roi)); + + return letterboxImage; +} +/* +将经过 letterbox 处理后的坐标映射回原始图像坐标系 +*/ +void mapCoordinates(int *x, int *y) +{ + int mx = *x - leftPadding; + int my = *y - topPadding; + + *x = (int)((float)mx / scale); + *y = (int)((float)my / scale); +} + +void draw_detections(int count, + object_detect_result *results, + cv::Mat &frame, + void (*mapCoord)(int *, int *)) +{ + char text[128]; + for (int i = 0; i < count; i++) + { + object_detect_result *det = &results[i]; + + int sX = (int)det->box.left; + int sY = (int)det->box.top; + int eX = (int)det->box.right; + int eY = (int)det->box.bottom; + + mapCoord(&sX, &sY); + mapCoord(&eX, &eY); + + cv::rectangle(frame, + cv::Point(sX, sY), + cv::Point(eX, eY), + cv::Scalar(0, 255, 0), 1); + + sprintf(text, "%s %.1f%%", + coco_cls_to_name(det->cls_id), + det->prop * 100); + + cv::putText(frame, text, + cv::Point(sX, sY - 8), + cv::FONT_HERSHEY_SIMPLEX, 1, + cv::Scalar(0, 255, 0), 1); + } +} \ No newline at end of file diff --git a/Cpp_example/D15_yolov8/yolov8.h b/Cpp_example/D15_yolov8/yolov8.h new file mode 100755 index 0000000000000000000000000000000000000000..e516765004787559ff264ebe3987bc254236cc87 --- /dev/null +++ b/Cpp_example/D15_yolov8/yolov8.h @@ -0,0 +1,45 @@ +#ifndef _RKNN_DEMO_YOLOV8_H_ +#define _RKNN_DEMO_YOLOV8_H_ + +#include "rknn_api.h" + +typedef struct { + char *dma_buf_virt_addr; + int dma_buf_fd; + int size; +} rknn_dma_buf; + +typedef struct { + rknn_context rknn_ctx; + rknn_input_output_num io_num; + rknn_tensor_attr* input_attrs; + rknn_tensor_attr* output_attrs; + + rknn_tensor_mem* input_mems[1]; + rknn_tensor_mem* output_mems[9]; + rknn_dma_buf img_dma_buf; + + int model_channel; + int model_width; + int model_height; + bool is_quant; +} rknn_app_context_t; + +#include "postprocess.h" + + +int init_yolov8_model(const char* model_path, rknn_app_context_t* app_ctx); + +int release_yolov8_model(rknn_app_context_t* app_ctx); + +int inference_yolov8_model(rknn_app_context_t* app_ctx, object_detect_result_list* od_results); + +#endif //_RKNN_DEMO_YOLOV8_H_ + +cv::Mat letterbox(cv::Mat input); +void mapCoordinates(int *x, int *y); + +void draw_detections(int count, + object_detect_result *results, + cv::Mat &frame, + void (*mapCoord)(int *, int *)); \ No newline at end of file diff --git "a/Cpp_example/D15_yolov8/\346\250\241\345\236\213\346\226\207\344\273\266\350\275\254\346\215\242.md" "b/Cpp_example/D15_yolov8/\346\250\241\345\236\213\346\226\207\344\273\266\350\275\254\346\215\242.md" new file mode 100755 index 0000000000000000000000000000000000000000..5a799cebe480068ecf7329bc8292b619e3930948 --- /dev/null +++ "b/Cpp_example/D15_yolov8/\346\250\241\345\236\213\346\226\207\344\273\266\350\275\254\346\215\242.md" @@ -0,0 +1,230 @@ +# 模型训练以及转换 + +## 1. 模型训练 + +### 1.1 下载模型训练代码 + +```shell +git clone https://github.com/airockchip/ultralytics_yolov8.git +``` + +### 1.2 准备数据集 + +将自定义数据集转换为yolo格式,下面以Labelme工具标注的数据集为例,运行以下代码进行数据集的转换: + +```python +import os +import json +import shutil +import random +from pathlib import Path + +def convert_labelme_to_yolo(src_dir, dest_dir, yaml_output_path, split_ratio=0.8): + src_dir = Path(src_dir) + dest_dir = Path(dest_dir) + yaml_output_path = Path(yaml_output_path) + + # 输入路径设置 + annotations_dir = src_dir / 'annotations' + images_dir = src_dir / 'images' + + if not annotations_dir.exists(): + print(f"找不到标注文件夹: {annotations_dir}") + return + + # 1. 创建 YOLOv8 所需目录结构 + for split in ['train', 'val']: + (dest_dir / 'images' / split).mkdir(parents=True, exist_ok=True) + (dest_dir / 'labels' / split).mkdir(parents=True, exist_ok=True) + + # 获取所有 json 文件 + json_files = list(annotations_dir.glob('*.json')) + if not json_files: + print("未找到任何 JSON 标注文件!") + return + + # 2. 随机打乱用于划分训练集 (80%) 和验证集 (20%) + random.seed(42) + random.shuffle(json_files) + train_count = int(len(json_files) * split_ratio) + + # 类别映射(目前只有一个类别 fire,这里需要根据你的数据集类别进行修改) + classes = ['fire'] + class_to_id = {cls: i for i, cls in enumerate(classes)} + + print(f"总计找到 {len(json_files)} 个标注文件,开始转换...") + + success_count = 0 + for i, json_path in enumerate(json_files): + split = 'train' if i < train_count else 'val' + + with open(json_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 获取图像宽高 + img_w = data.get('imageWidth') + img_h = data.get('imageHeight') + + if not img_w or not img_h: + print(f"警告: {json_path.name} 中没有图像宽高数据,跳过。") + continue + + # 解析图像文件名 (兼容 Windows '\\' 和 Linux '/') + img_name = data.get('imagePath', '').replace('\\', '/').split('/')[-1] + if not img_name: + img_name = json_path.stem + '.png' + + src_img_path = images_dir / img_name + + # 3. 如果图片不存在,尝试其它常见的图片后缀 + if not src_img_path.exists(): + for ext in ['.jpg', '.jpeg', '.JPG', '.PNG']: + alt_img_path = images_dir / (json_path.stem + ext) + if alt_img_path.exists(): + src_img_path = alt_img_path + img_name = alt_img_path.name + break + else: + print(f"警告: 找不到对应的图像文件 {src_img_path},跳过此样本。") + continue + + # 4. 准备输出 label 内容 + yolo_labels = [] + for shape in data.get('shapes', []): + label = shape.get('label') + if label not in class_to_id: + continue # 如果存在其它意外的类别则忽略 + + class_id = class_to_id[label] + points = shape.get('points') + + # 确保是矩形框并且有两个点 + if shape.get('shape_type') == 'rectangle' and len(points) == 2: + x1, y1 = points[0] + x2, y2 = points[1] + + # 确保坐标极值正确 + x_min, x_max = min(x1, x2), max(x1, x2) + y_min, y_max = min(y1, y2), max(y1, y2) + + # 转换为 YOLO 的中心点及宽高,并且归一化到 0~1 + x_center = ((x_min + x_max) / 2.0) / img_w + y_center = ((y_min + y_max) / 2.0) / img_h + w = (x_max - x_min) / img_w + h = (y_max - y_min) / img_h + + yolo_labels.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") + + # 5. 写入 YOLO 格式的 txt 标签文件 + dest_txt_path = dest_dir / 'labels' / split / (json_path.stem + '.txt') + with open(dest_txt_path, 'w', encoding='utf-8') as f: + f.write('\n'.join(yolo_labels)) + + # 6. 复制图像文件到 YOLO 目录 + dest_img_path = dest_dir / 'images' / split / img_name + shutil.copy(src_img_path, dest_img_path) + success_count += 1 + + # 7. 生成 YOLOv8 训练所需的 YAML 文件 + yaml_content = f"""path: {dest_dir.absolute()} # 数据集根目录 +train: images/train # 训练集图片目录 (相对于 path) +val: images/val # 验证集图片目录 (相对于 path) + +# 类别定义 +names: + 0: fire +""" + yaml_output_path.parent.mkdir(parents=True, exist_ok=True) + with open(yaml_output_path, 'w', encoding='utf-8') as f: + f.write(yaml_content) + + print(f"\n转换完成!成功处理 {success_count} 个样本。") + print(f"YOLO 格式数据集保存在: {dest_dir}") + print(f"YOLO 配置文件已生成: {yaml_output_path}") + +if __name__ == '__main__': + # 原始数据集路径 + SOURCE_DIR = 'path/to/ultralytics_yolov8/Dataset' + # 转换后YOLO数据集的存放路径 + DEST_DIR = 'path/to/ultralytics_yolov8/Dataset_YOLO' + # 要求的 yaml 配置文件生成路径 + YAML_PATH = 'path/to/ultralytics_yolov8/datasets/data.yaml' + + convert_labelme_to_yolo(SOURCE_DIR, DEST_DIR, YAML_PATH) +``` + +### 1.3 开始训练 + +运行以下代码开始训练yolov8模型 + +```python +from ultralytics import YOLO +from ultralytics.utils import LOGGER +model = YOLO("yolov8n.pt") +# model = YOLO("./ultralytics/cfg/models/v8/yolov8s.yaml") +results = model.train(data="datasets/data.yaml", epochs=300, workers=8,imgsz=640, batch=32, device=0) +``` + +训练好的模型会保存在./runs/detect/train/weights目录下。 + +## 2. 模型转换 + +### 2.1 导出onnx模型 + +由于是在rv1106上运行,对于模型转换请参考如下文档: + +[RKOPT_README.zh-CN.md](https://github.com/airockchip/ultralytics_yolov8/blob/main/RKOPT_README.zh-CN.md) + +- 调整 ./ultralytics/cfg/default.yaml 中 model 文件路径,默认为 yolov8n.pt,若自己训练模型,请调接至对应的路径。支持检测、分割、姿态、旋转框检测模型。 + +在满足 ./requirements.txt 的环境要求后,执行以下语句导出模型 + +```shell +export PYTHONPATH=./ +python ./ultralytics/engine/exporter.py +``` + +执行完毕后,会生成 ONNX 模型. 假如原始模型为 yolov8n.pt,则生成 yolov8n.onnx 模型。 转换后的onnx模型会保存在runs/detect/train/weights目录下。 + +### 2.2 准备校准数据集 + +在量化时,提供 20~100 张有代表性的图片给 RKNN 即可。请在你的终端中运行以下命令,从你的 Dataset_YOLO/images/train 中提取部分图片路径生成校准文件: + +```shell +cd path/to/ultralytics_yolov8 +# 随机选取50张训练集的图片路径,写入 dataset_subset.txt +ls /path/to/ultralytics_yolov8/Dataset_YOLO/images/train/*.png | head -n 50 > dataset_subset.txt +``` + +- 注意:如果你的图片后缀是 .jpg ,请将上面的 *.png 替换为*.jpg + +### 2.3 转RKNN模型 + +#### 2.3.1 克隆rknn_model_zoo + +```shell +git clone https://github.com/airockchip/rknn_model_zoo.git +cd /path/to/rknn_model_zoo/examples/yolov8/python +``` + +#### 2.3.2 修改convert.py代码 + +主要是将 DATASET_PATH 指向刚才我们生成的 dataset_subset.txt : + +```python +DATASET_PATH = 'path/to/dataset_subset.txt' +``` + +#### 2.3.3 运行convert.py + +在运行转换脚本前请确保你已经配置好了所需要的环境。并且将之前转换好的onnx模型放到以下目录: + +- /path/to/rknn_model_zoo/examples/yolov8/model/ + +执行以下命令开始转换 + +```shell +python convert.py /path/to/xxxx.onnx rv1106 i8 ../model/yolov8.rknn +``` + +执行完毕后,如果没有报错,你就会在 rknn_model_zoo/examples/yolov8/model/ 目录下得到 yolov8.rknn 文件,这个文件就可以直接放到你的 RV1106 板子上进行部署板上运行了! diff --git a/README.md b/README.md index 06ae7ed6c6f149b9e2a523f3f36de79b4e2e68db..2a8168663c36086cd0e1ac66b11287919f3ffa4d 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ OpenCV 是一个开源的计算机视觉库,它提供了一组功能强大的 目标检测(Object Detection)是深度学习中计算机视觉领域的重要任务之一,旨在识别图像或视频中所有感兴趣的物体,并准确地定位这些物体的边界框(Bounding Box)。与目标分类不同,目标检测不仅需要预测物体的类别,还需要标注它们在图像中的位置。一般来说,目标检测任务的标注过程比较复杂,适合既需要对目标进行分类,有需要对目标进行定位的场景。 * [凌智视觉模块通用检测模型部署指南](./example/vision/detetcion) +* [YOLOv5 模型部署指南](./example/vision/yolov5) ### 👍 条码检测识别案例 @@ -185,6 +186,12 @@ OCR(Optical Character Recognition,光学字符识别)是一种将图像中 * [YOLOv5目标检测](./Cpp_example/D10_yolov5//README.md) +### 👍 YOLOv8目标检测 + +目标检测(Object Detection)是深度学习中计算机视觉领域的重要任务之一,旨在识别图像或视频中所有感兴趣的物体,并准确地定位这些物体的边界框(Bounding Box)。与目标分类不同,目标检测不仅需要预测物体的类别,还需要标注它们在图像中的位置。一般来说,目标检测任务的标注过程比较复杂,适合既需要对目标进行分类,有需要对目标进行定位的场景。该案例使用YOLOv8进行目标检测。 + +* [YOLOv8目标检测](./Cpp_example/D15_yolov8//README.md) + ## 🏀 C++ 开发案例 C++ 开发案例以A、B、C、D进行不同类别进行分类,方便初学者进行使用和二次开发。 @@ -229,6 +236,7 @@ C++ 开发案例以A、B、C、D进行不同类别进行分类,方便初学者 | 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)| +| D15 | 神经网络类 | YOLOv8 | [YOLOv8目标检测](./Cpp_example/D15_yolov8/README.md)| | E01 | 使用示例类 | Test_find_Laser | [激光跟踪](./Cpp_example/E01_find_Laser/README.md)| | E02 | 使用示例类 | Test_find_number | [数字识别](./Cpp_example/E02_find_number/README.md)| diff --git "a/docs/Linux/linux\347\256\200\346\230\216\346\225\231\347\250\213.md" "b/docs/Linux/linux\347\256\200\346\230\216\346\225\231\347\250\213.md" index 9898840b14bba37e0273e0a3c5fc6ab34f0c45f8..9aa2eba364153d9b988df7c4ad6db6e3ca050f2d 100644 --- "a/docs/Linux/linux\347\256\200\346\230\216\346\225\231\347\250\213.md" +++ "b/docs/Linux/linux\347\256\200\346\230\216\346\225\231\347\250\213.md" @@ -1,4 +1,4 @@ -

凌智视觉模块**linux**简明教程

+

凌智视觉模块 Linux 简明教程

发布版本:V0.0.0 @@ -26,7 +26,7 @@ | **日期** | **版本** | **作者** | **修改说明** | |:-----------|--------|--------|-----------| -| 2026/02/04 | 0.0.0 |钟海滨| 初始化linux教程 | +| 2026/02/07 | 0.0.0 |钟海滨| 初始化linux教程 | **注**: 此篇教程参考[菜鸟教程](https://www.runoob.com/linux/linux-tutorial.html) diff --git "a/docs/opencv-python\346\216\245\345\217\243\346\226\207\346\241\243.md" "b/docs/opencv-python\346\216\245\345\217\243\346\226\207\346\241\243.md" index 6c3b2027c2cb18bb910657c16afc8e207057fc53..25479e202cebdc8bec4db39b41356d886ae9e236 100644 --- "a/docs/opencv-python\346\216\245\345\217\243\346\226\207\346\241\243.md" +++ "b/docs/opencv-python\346\216\245\345\217\243\346\226\207\346\241\243.md" @@ -2,7 +2,7 @@ 发布版本:V0.0.6 -日期:2026/02/04 +日期:2026/02/14 文件密级:□绝密 □秘密 □内部资料 ■公开 @@ -28,6 +28,7 @@ |:-----------|--------|--------|-----------| | 2026/02/04 | 0.0.6 |钟海滨| 完善接口文档内容 | | 2026/02/06 | 0.0.6 |钟海滨| 增加大量函数封装 | +| 2026/02/14 | 0.0.6 |钟海滨| 增加部分接口 | --- ## 目录 @@ -323,6 +324,55 @@ cv2.putText(img, text, org, fontFace, fontScale, color, thickness=1, cv2.putText(img, "Hello World", (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255)) ``` +#### polylines - 绘制多段折线 +```python +# 语法 +cv2.polylines(img, pts, isClosed, color, thickness=1, lineType=cv2.LINE_8, shift=0) + +# 示例(pts 为点列表的列表) +pts = [[(10, 10), (100, 10), (100, 100)]] +cv2.polylines(img, pts, True, (0, 255, 0), 2) +``` + +#### rectangle_rect - 使用 Rect 绘制矩形 +```python +# 语法 +cv2.rectangle_rect(img, rect, color, thickness=1, lineType=cv2.LINE_8, shift=0) + +# 示例 +rect = cv2.Rect(50, 50, 100, 80) +cv2.rectangle_rect(img, rect, (255, 0, 0), 2) +``` + +#### ellipse - 绘制椭圆 +```python +# 语法 +cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness=1, lineType=cv2.LINE_8, shift=0) + +# 示例 +cv2.ellipse(img, (100, 100), (60, 30), 0, 0, 360, (255, 255, 0), 2) +``` + +#### fillPoly - 填充多边形 +```python +# 语法 +cv2.fillPoly(img, pts, color, lineType=cv2.LINE_8, shift=0) + +# 示例 +polys = [[(10, 10), (80, 10), (80, 60), (10, 60)]] +cv2.fillPoly(img, polys, (0, 0, 255)) +``` + +#### fillConvexPoly - 填充凸多边形 +```python +# 语法 +cv2.fillConvexPoly(img, pts, color, lineType=cv2.LINE_8, shift=0) + +# 示例 +pts = [(30, 30), (120, 50), (80, 120)] +cv2.fillConvexPoly(img, pts, (0, 255, 255)) +``` + ### 4.2.4 轮廓处理 #### findContours - 查找轮廓 @@ -362,6 +412,69 @@ rect = cv2.boundingRect(points) bbox = cv2.boundingRect(contour) ``` +#### arcLength - 计算轮廓周长 +```python +# 语法 +length = cv2.arcLength(curve, closed) + +# 示例 +length = cv2.arcLength(contour, True) +``` + +#### approxPolyDP - 轮廓近似 +```python +# 语法 +approx = cv2.approxPolyDP(curve, epsilon, closed) + +# 示例 +approx = cv2.approxPolyDP(contour, 2.0, True) +``` + +#### moments - 计算几何矩 +```python +# 语法 +m = cv2.moments(contour, binaryImage=False) + +# 示例 +m = cv2.moments(contour) +``` + +#### HuMoments - Hu矩 +```python +# 语法 +hu = cv2.HuMoments(contour, binaryImage=False) + +# 示例 +hu = cv2.HuMoments(contour) +``` + +#### contourCentroid - 轮廓质心 +```python +# 语法 +center = cv2.contourCentroid(contour) + +# 示例 +center = cv2.contourCentroid(contour) +``` + +#### convexHull - 轮廓凸包 +```python +# 语法 +hull = cv2.convexHull(contour, clockwise=False) + +# 示例 +hull = cv2.convexHull(contour) +``` + +#### convexityDefects - 凸包缺陷 +```python +# 语法 +defects = cv2.convexityDefects(contour) + +# 示例 +defects = cv2.convexityDefects(contour) +``` + ### 4.2.5 霍夫变换 #### HoughLines - 标准霍夫线变换 @@ -488,12 +601,48 @@ equalized = cv2.equalizeHist(gray) #### applyCLAHE - 自适应直方图均衡化 ```python # 语法 -dst = cv2.applyCLAHE(inputImage, clipLimit=40.0, tileSize=(8, 8)) +dst = cv2.applyCLAHE(inputImage, clipLimit, tileGridSize) # 示例 clahe_result = cv2.applyCLAHE(gray, 2.0, (8, 8)) ``` +#### copyMakeBorder - 扩展边界 +```python +# 语法 +dst = cv2.copyMakeBorder(img, top, bottom, left, right, borderType, value=(0, 0, 0, 0)) + +# 示例 +bordered = cv2.copyMakeBorder(img, 10, 10, 20, 20, cv2.BORDER_CONSTANT, (0, 0, 0, 0)) +``` + +#### matchTemplate - 模板匹配 +```python +# 语法 +result = cv2.matchTemplate(image, templ, method) + +# 示例 +result = cv2.matchTemplate(img, templ, cv2.TM_CCOEFF_NORMED) +``` + +#### cornerHarris - Harris 角点检测 +```python +# 语法 +dst = cv2.cornerHarris(src, blockSize, ksize, k, borderType=cv2.BORDER_DEFAULT) + +# 示例 +harris = cv2.cornerHarris(gray, 2, 3, 0.04) +``` + +#### distanceTransform - 距离变换 +```python +# 语法 +dist = cv2.distanceTransform(src, distanceType, maskSize) + +# 示例 +dist = cv2.distanceTransform(binary, cv2.DIST_L2, 3) +``` + ### 4.2.11 特征点与形状分析 #### goodFeaturesToTrack - 角点检测 @@ -524,8 +673,9 @@ merged = cv2.merge(bgr_planes) small = cv2.pyrDown(img) large = cv2.pyrUp(small) -# 直方图计算 -hist = cv2.calcHist([gray], [0], None, [256], [0, 256]) +# 直方图计算(histSize 与 ranges 必填;mask 为空时使用 Mat) +mask = cv2.Mat() +hist = cv2.calcHist([gray], [0], mask, [256], [0, 256]) # 查找表变换 result = cv2.LUT(gray, lut) @@ -557,8 +707,12 @@ affine = cv2.warpAffine(img, M_affine, (img.cols, img.rows)) M_persp = cv2.getPerspectiveTransform(src_points4, dst_points4) persp = cv2.warpPerspective(img, M_persp, (img.cols, img.rows)) -# 翻转和转置 +# 重映射 +remapped = cv2.remap(img, map1, map2, cv2.INTER_LINEAR) + +# 翻转、旋转和转置 flipped = cv2.flip(img, 1) +rotated90 = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) transposed = cv2.transpose(img) ``` @@ -638,11 +792,12 @@ cv2.imwrite('output.jpg', processed_img) #### imencode - 图像编码 ```python -# 语法(当前实现返回编码成功与字节数据) -success, buffer = cv2.imencode(extension, img, params=None) +# 语法(param 可为 None / int / list / tuple) +success, buffer = cv2.imencode(extension, img, param=None) # 示例 ret, jpg_bytes = cv2.imencode('.jpg', img) +ret, jpg_bytes = cv2.imencode('.jpg', img, 90) # jpg_bytes 类型为 bytes,可直接写入文件或通过网络发送 ``` @@ -704,6 +859,12 @@ sub_img = cv2.subtract(img1, img2) mul_img = cv2.multiply(img1, img2) div_img = cv2.divide(img1, img2) +# 标量运算 +add_s = cv2.addScalar(img, (10, 10, 10, 0)) +sub_s = cv2.subtractScalar(img, (10, 10, 10, 0)) +mul_s = cv2.multiplyScalar(img, (1.2, 1.2, 1.2, 1.0)) +div_s = cv2.divideScalar(img, (2, 2, 2, 1)) + # 线性组合与差分 scaled = cv2.scaleAdd(img1, 0.5, img2) abs_diff = cv2.absdiff(img1, img2) diff --git "a/docs/\346\216\245\345\217\243\346\226\207\346\241\243/lockzhiner_vision_module/cv2/core_functionality.py" "b/docs/\346\216\245\345\217\243\346\226\207\346\241\243/lockzhiner_vision_module/cv2/core_functionality.py" index c5a28823996c978d170523808f4d826d787be450..4455bcd5d5ef01c96e82ccb8c4e86335707a9a42 100644 --- "a/docs/\346\216\245\345\217\243\346\226\207\346\241\243/lockzhiner_vision_module/cv2/core_functionality.py" +++ "b/docs/\346\216\245\345\217\243\346\226\207\346\241\243/lockzhiner_vision_module/cv2/core_functionality.py" @@ -41,20 +41,40 @@ def add(src1, src2, mask=None): return cv2.add(src1, src2, mask) +def addScalar(src, scalar, mask=None): + if mask is None: + mask = cv2.Mat() + return cv2.addScalar(src, scalar, mask) + + def subtract(src1, src2, mask=None): if mask is None: mask = cv2.Mat() return cv2.subtract(src1, src2, mask) +def subtractScalar(src, scalar, mask=None): + if mask is None: + mask = cv2.Mat() + return cv2.subtractScalar(src, scalar, mask) + + def multiply(src1, src2, scale=1.0): return cv2.multiply(src1, src2, scale) +def multiplyScalar(src, scalar, scale=1.0): + return cv2.multiplyScalar(src, scalar, scale) + + def divide(src1, src2, scale=1.0): return cv2.divide(src1, src2, scale) +def divideScalar(src, scalar, scale=1.0): + return cv2.divideScalar(src, scalar, scale) + + def scaleAdd(src1, alpha, src2): return cv2.scaleAdd(src1, alpha, src2) @@ -92,8 +112,15 @@ def compareHist(H1, H2, method): def imencode(ext: str, img_data, param=None): if param is None: - param = [] - ret, buf = cv2.imencode(ext, img_data, param) + params = [] + elif isinstance(param, int): + quality_flag = getattr(cv2, "IMWRITE_JPEG_QUALITY", 1) + params = [quality_flag, param] + elif isinstance(param, (list, tuple)): + params = list(param) + else: + raise TypeError("param must be int, list, tuple, or None") + ret, buf = cv2.imencode(ext, img_data, params) return ret, buf diff --git "a/docs/\346\216\245\345\217\243\346\226\207\346\241\243/lockzhiner_vision_module/cv2/image_processing.py" "b/docs/\346\216\245\345\217\243\346\226\207\346\241\243/lockzhiner_vision_module/cv2/image_processing.py" index e44b6bda736c6646b88fe7bfd8dee31643710c67..20a8386344d56a5e567837bb0c51b9aa020bb85a 100644 --- "a/docs/\346\216\245\345\217\243\346\226\207\346\241\243/lockzhiner_vision_module/cv2/image_processing.py" +++ "b/docs/\346\216\245\345\217\243\346\226\207\346\241\243/lockzhiner_vision_module/cv2/image_processing.py" @@ -177,6 +177,31 @@ def contourArea(contour, oriented=False): return area +def moments(contour, binaryImage=False): + temp_contour = [convert2point(p) for p in contour] + return cv2.moments(temp_contour, binaryImage) + + +def HuMoments(contour, binaryImage=False): + temp_contour = [convert2point(p) for p in contour] + return cv2.HuMoments(temp_contour, binaryImage) + + +def contourCentroid(contour): + temp_contour = [convert2point(p) for p in contour] + return cv2.contourCentroid(temp_contour) + + +def convexHull(contour, clockwise=False): + temp_contour = [convert2point(p) for p in contour] + return cv2.convexHull(temp_contour, clockwise) + + +def convexityDefects(contour): + temp_contour = [convert2point(p) for p in contour] + return cv2.convexityDefects(temp_contour) + + def goodFeaturesToTrack(image, maxCorners, qualityLevel, minDistance, mask=None, blockSize=3, useHarrisDetector=False, k=0.04): if mask is None: mask_mat = cv2.Mat() diff --git "a/docs/\346\216\245\345\217\243\346\226\207\346\241\243/opencv\346\226\260\345\242\236\346\216\245\345\217\243\346\265\213\350\257\225\344\276\213\347\250\213/2026-0214\346\226\260\345\242\236.py" "b/docs/\346\216\245\345\217\243\346\226\207\346\241\243/opencv\346\226\260\345\242\236\346\216\245\345\217\243\346\265\213\350\257\225\344\276\213\347\250\213/2026-0214\346\226\260\345\242\236.py" new file mode 100644 index 0000000000000000000000000000000000000000..7ea5805884edc5ddc8d37b92a60c4ab6080b5beb --- /dev/null +++ "b/docs/\346\216\245\345\217\243\346\226\207\346\241\243/opencv\346\226\260\345\242\236\346\216\245\345\217\243\346\265\213\350\257\225\344\276\213\347\250\213/2026-0214\346\226\260\345\242\236.py" @@ -0,0 +1,92 @@ +import os +import sys + +import lockzhiner_vision_module.cv2 as cv2 + + + +def require_image(path): + if not os.path.isfile(path): + raise RuntimeError("Image file not found: " + path) + img = cv2.imread(path, cv2.IMREAD_COLOR) + if img.empty(): + raise RuntimeError("Failed to read image: " + path) + return img + + +def main(): + if len(sys.argv) < 2: + print("Usage: python test_new_apis_no_numpy.py ") + return 1 + + img = require_image(sys.argv[1]) + print("img:", img.cols, img.rows, img.channels(), img.type()) + + ok, encoded = cv2.imencode(".jpg", img, 80) + print("imencode ok:", ok, "bytes:", len(encoded)) + + print("at(0,0):", img.at(0, 0)) + print("ptr(0) len:", len(img.ptr(0))) + print("ptr(0,0) len:", len(img.ptr(0, 0))) + + reshaped = img.reshape(img.channels(), img.rows) + print("reshape:", reshaped.cols, reshaped.rows) + + converted = img.convertTo(img.type(), 1.0, 0.0) + print("convertTo:", converted.cols, converted.rows) + + scalar = cv2.Scalar(10, 20, 30) + add_s = cv2.addScalar(img, scalar, cv2.Mat()) + sub_s = cv2.subtractScalar(img, scalar, cv2.Mat()) + mul_s = cv2.multiplyScalar(img, scalar, 1.0) + div_s = cv2.divideScalar(img, scalar, 1.0) + print("addScalar/subtractScalar/multiplyScalar/divideScalar:", + add_s.cols, sub_s.cols, mul_s.cols, div_s.cols) + + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY, 0) + _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) + contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + print("contours:", len(contours)) + + result = img.copy() + + if contours: + c = contours[0] + area = cv2.contourArea(c, False) + perimeter = cv2.arcLength(c, True) + approx = cv2.approxPolyDP(c, 2.0, True) + + m = cv2.moments(c, False) + hu = cv2.HuMoments(c, False) + cx, cy = cv2.contourCentroid(c) + hull = cv2.convexHull(c, False) + defects = cv2.convexityDefects(c) + + print("area:", area) + print("arcLength:", perimeter) + print("approx size:", len(approx)) + print("moments m00:", m.get("m00")) + print("HuMoments:", hu) + print("centroid:", cx, cy) + print("convexHull size:", len(hull)) + print("convexityDefects:", len(defects)) + + cv2.drawContours(result, [c], -1, (0, 255, 0), 2, cv2.LINE_8, 0) + cv2.drawContours(result, [hull], -1, (0, 0, 255), 2, cv2.LINE_8, 0) + cv2.circle(result, (int(cx), int(cy)), 4, (255, 0, 0), 2, cv2.LINE_8, 0) + + rect = cv2.boundingRect(c) + cv2.rectangle_rect(result, rect, (255, 255, 0), 2, cv2.LINE_8, 0) + + text = "A:{:.2f} P:{:.2f}".format(area, perimeter) + cv2.putText(result, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2, cv2.LINE_8, False) + + out_path = os.path.join(os.path.dirname(os.path.abspath(sys.argv[1])), "result_new_apis.png") + ok = cv2.imwrite(out_path, result) + print("save:", out_path, "ok:", ok) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git "a/docs/\346\216\245\345\217\243\346\226\207\346\241\243/opencv\346\226\260\345\242\236\346\216\245\345\217\243\346\265\213\350\257\225\344\276\213\347\250\213/result_new_apis.png" "b/docs/\346\216\245\345\217\243\346\226\207\346\241\243/opencv\346\226\260\345\242\236\346\216\245\345\217\243\346\265\213\350\257\225\344\276\213\347\250\213/result_new_apis.png" new file mode 100644 index 0000000000000000000000000000000000000000..f4d1cfe7d2101d4e86eaad5a501ca21d389ba05a Binary files /dev/null and "b/docs/\346\216\245\345\217\243\346\226\207\346\241\243/opencv\346\226\260\345\242\236\346\216\245\345\217\243\346\265\213\350\257\225\344\276\213\347\250\213/result_new_apis.png" differ diff --git a/example/vision/yolov5/convert.py b/example/vision/yolov5/convert.py new file mode 100644 index 0000000000000000000000000000000000000000..cd8ce7b5150437320b3f637632e7e64b2eedea40 --- /dev/null +++ b/example/vision/yolov5/convert.py @@ -0,0 +1,75 @@ +import sys + +from rknn.api import RKNN + +DATASET_PATH = './dataset.txt' +DEFAULT_RKNN_PATH = './yolov5.rknn' +DEFAULT_QUANT = True + +def parse_arg(): + if len(sys.argv) < 3: + print("Usage: python3 {} onnx_model_path [platform] [dtype(optional)] [output_rknn_path(optional)]".format(sys.argv[0])) + print(" platform choose from [rk3562, rk3566, rk3568, rk3576, rk3588, rv1103, rv1106, rv1126b, rv1109, rv1126, rk1808]") + print(" dtype choose from [i8, fp] for [rk3562, rk3566, rk3568, rk3576, rk3588, rv1103, rv1106, rv1126b]") + print(" dtype choose from [u8, fp] for [rv1109, rv1126, rk1808]") + exit(1) + + model_path = sys.argv[1] + platform = sys.argv[2] + + do_quant = DEFAULT_QUANT + if len(sys.argv) > 3: + model_type = sys.argv[3] + if model_type not in ['i8', 'u8', 'fp']: + print("ERROR: Invalid model type: {}".format(model_type)) + exit(1) + elif model_type in ['i8', 'u8']: + do_quant = True + else: + do_quant = False + + if len(sys.argv) > 4: + output_path = sys.argv[4] + else: + output_path = DEFAULT_RKNN_PATH + + return model_path, platform, do_quant, output_path + +if __name__ == '__main__': + model_path, platform, do_quant, output_path = parse_arg() + + # Create RKNN object + rknn = RKNN(verbose=False) + + # Pre-process config + print('--> Config model') + rknn.config(mean_values=[[0, 0, 0]], std_values=[ + [255, 255, 255]], target_platform=platform) + print('done') + + # Load model + print('--> Loading model') + ret = rknn.load_onnx(model=model_path) + if ret != 0: + print('Load model failed!') + exit(ret) + print('done') + + # Build model + print('--> Building model') + ret = rknn.build(do_quantization=do_quant, dataset=DATASET_PATH) + if ret != 0: + print('Build model failed!') + exit(ret) + print('done') + + # Export rknn model + print('--> Export rknn model') + ret = rknn.export_rknn(output_path) + if ret != 0: + print('Export rknn model failed!') + exit(ret) + print('done') + + # Release + rknn.release() \ No newline at end of file diff --git a/example/vision/yolov5/data.py b/example/vision/yolov5/data.py new file mode 100644 index 0000000000000000000000000000000000000000..c93cb90cfa310a9b7cf708bf8005ffc270e0e2a9 --- /dev/null +++ b/example/vision/yolov5/data.py @@ -0,0 +1,9 @@ +import os + +# 获取目录下所有文件名 +file_list = os.listdir("./images/train2017") +path = "./images/train2017/" +# 写入文件,每行一个 +with open('dataset.txt', 'w', encoding='utf-8') as f: + for filename in file_list: + f.write(path+filename + '\n') \ No newline at end of file diff --git a/example/vision/yolov5/test_yolov5.py b/example/vision/yolov5/test_yolov5.py new file mode 100755 index 0000000000000000000000000000000000000000..d4d99a366673618d395b68571c7041d2345006bb --- /dev/null +++ b/example/vision/yolov5/test_yolov5.py @@ -0,0 +1,157 @@ +import lockzhiner_vision_module.cv2 as cv2 +import sys +import os +from lockzhiner_vision_module.vision.detection import YoloV5 +from lockzhiner_vision_module.vision.utils import visualize +from lockzhiner_vision_module.edit import Edit + +def test_yolov5_image(model_path, image_path, label_path=None, output_path='yolov5_result.jpg'): + """ + 测试 YOLOv5 模型在单张图像上的目标检测功能 + + Args: + model_path (str): YOLOv5 模型文件路径 + image_path (str): 测试图像路径 + label_path (str, optional): 标签文件路径 + output_path (str, optional): 输出图像路径,默认为 'yolov5_result.jpg' + """ + print(f"正在初始化 YOLOv5 模型...") + print(f"模型路径: {model_path}") + + model = YoloV5() + + if not model.initialize(model_path): + print("模型初始化失败!") + return False + + print("模型初始化成功!") + + if model.load_labels(label_path): + labels = model.get_labels() + print(f"Loaded labels: {labels}") + else: + labels = [] + print("No labels loaded, will use label_id") + model.set_threshold(conf_threshold=0.25, nms_threshold=0.45) + print("检测阈值已设置: conf_threshold=0.25, nms_threshold=0.45") + + print(f"\n正在读取图像: {image_path}") + image = cv2.imread(image_path) + + if image is None: + print(f"无法读取图像: {image_path}") + return False + + print(f"图像尺寸: {image.cols}x{image.rows}") + print(f"图像尺寸: {image.shape[1]}x{image.shape[0]}") + print("\n正在进行目标检测...") + results = model.predict(image) + + print(f"检测完成!共检测到 {len(results)} 个目标") + + for i, result in enumerate(results): + print(f"\n目标 {i + 1}:") + print(f" 矩形框: x={result.box.x}, y={result.box.y}, " + f"width={result.box.width}, height={result.box.height}") + print(f" 标签 ID: {result.label_id}") + print(f" 置信度: {result.score:.4f}") + + print(f"\n正在可视化检测结果...") + output_image = visualize(image, results,labels) + + print(f"正在保存结果图像到: {output_path}") + cv2.imwrite(output_path, output_image) + + print(f"\n测试完成!结果已保存到 {output_path}") + return True + + +def test_yolov5_camera(model_path, label_path=None): + """ + 测试 YOLOv5 模型在摄像头实时视频上的目标检测功能 + + Args: + model_path (str): YOLOv5 模型文件路径 + label_path (str, optional): 标签文件路径 + """ + print(f"正在初始化 YOLOv5 模型...") + print(f"模型路径: {model_path}") + width = 640 + height = 480 + edit = Edit() + edit.start_and_accept_connection() + + model = YoloV5() + + if not model.initialize(model_path): + print("模型初始化失败!") + return False + + print("模型初始化成功!") + + if label_path: + print(f"正在加载标签文件: {label_path}") + if model.load_labels(label_path): + labels = model.get_labels() + print("标签文件加载成功!") + else: + labels = [] + print("标签文件加载失败,将使用默认标签 ID") + + + model.set_threshold(conf_threshold=0.25, nms_threshold=0.45) + print("检测阈值已设置: conf_threshold=0.25, nms_threshold=0.45") + + print("\n正在打开摄像头...") + cap = cv2.VideoCapture() + + if not cap.open(0): + print("无法打开摄像头!") + return False + + print("摄像头已打开,按 'q' 键退出") + + while True: + ret, frame = cap.read() + + if not ret: + print("无法读取摄像头画面") + break + + results = model.predict(frame) + + + output_frame = visualize(frame, results,labels) + edit.print(output_frame) + + + cap.release() + cv2.destroyAllWindows() + print("\n摄像头测试结束") + return True + + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("使用方法:") + print(" 测试图像: python test_yolov5.py [label_path] [output_path]") + print(" 测试摄像头: python test_yolov5.py camera [label_path]") + print("\n参数说明:") + print(" model_path - YOLOv5 模型文件路径") + print(" image_path - 测试图像路径") + print(" camera - 使用摄像头进行实时检测") + print(" label_path - (可选) 标签文件路径") + print(" output_path - (可选) 输出图像路径,默认为 'yolov5_result.jpg'") + sys.exit(1) + + model_path = sys.argv[1] + mode = sys.argv[2] + + if mode == "camera": + label_path = sys.argv[3] if len(sys.argv) > 3 else None + test_yolov5_camera(model_path, label_path) + else: + image_path = mode + label_path = sys.argv[3] if len(sys.argv) > 3 else None + output_path = sys.argv[4] if len(sys.argv) > 4 else 'yolov5_result.jpg' + test_yolov5_image(model_path, image_path, label_path, output_path) diff --git "a/example/vision/yolov5/yolov5 \350\256\255\347\273\203\344\270\216\351\203\250\347\275\262\346\226\207\346\241\243.md" "b/example/vision/yolov5/yolov5 \350\256\255\347\273\203\344\270\216\351\203\250\347\275\262\346\226\207\346\241\243.md" new file mode 100644 index 0000000000000000000000000000000000000000..38b01dde961958c8bdc493c4d6a59c6d8369cc21 --- /dev/null +++ "b/example/vision/yolov5/yolov5 \350\256\255\347\273\203\344\270\216\351\203\250\347\275\262\346\226\207\346\241\243.md" @@ -0,0 +1,418 @@ +

凌智视觉模块 YOLOv5 目标检测模型部署指南

+ +发布版本:V0.0.0 + +日期:2026-03-04 + +文件密级:□绝密 □秘密 □内部资料 ■公开 + +--- + +**免责声明** + +本文档按**现状**提供,福州凌睿智捷电子有限公司(以下简称**本公司**)不对本文档中的任何陈述、信息和内容的准确性、可靠性、完整性、适销性、适用性及非侵权性提供任何明示或暗示的声明或保证。本文档仅作为使用指导的参考。 + +由于产品版本升级或其他原因,本文档可能在未经任何通知的情况下不定期更新或修改。 + +**读者对象** + +本教程适用于以下工程师: + +- 技术支持工程师 +- 软件开发工程师 + +**修订记录** + +| **日期** | **版本** | **作者** | **修改说明** | +|:-----------| -------- |--------| ------------ | +| 2026/03/04 | 0.0.0 | 钟海滨 | 初始版本 | + +## 1 简介 + +在深度学习中,目标检测是一项重要的计算机视觉任务,旨在在图像或视频中精确定位和识别多个目标的位置和类别。这项技术广泛应用于自动驾驶、安防监控、工业检测、智能零售等多种场景。 + +为了实现高效且准确的目标检测,我们基于 Lockzhiner Vision Module 的 YOLOv5 模型,训练了一个高性能的目标检测模型。YOLOv5(You Only Look Once version 5)是一种单阶段目标检测算法,以其出色的速度和精度平衡而闻名。 + +## 2 运行前的准备 + +* 请确保你已经下载了 [凌智视觉模块图片传输助手](https://gitee.com/LockzhinerAI/LockzhinerVisionModule/releases/download/v0.0.0/LockzhinerVisionModuleImageFetcher.exe) + +## 3 模型训练与转换 + +### 3.1 拉取 YOLOv5 仓库 + +```bash +git clone https://github.com/airockchip/yolov5.git +cd yolov5 +pip install -r requirements.txt +``` + +### 3.2 训练模型 + +```bash +python train.py --data coco.yaml --img 640 --batch 16 --epochs 300 --weights yolov5s.pt +``` + +### 3.3 导出 ONNX 模型 + +```bash +python export.py --rknpu --weights /media/lzdz/data/yolo-rk/runs/train/exp6/weights/best.pt +``` + +### 3.4 转换为 RKNN 模型 + +**构建量化数据集** + +```bash +python data.py +``` + +**安装转换工具** + +```bash +pip install rknn-toolkit2 +``` + +**转换模型** + +```bash +python convert.py /media/lzdz/data/yolo-rk/runs/train/exp8/weights/best.onnx rv1106 +``` + +## 4 在凌智视觉模块上部署模型 + +训练完模型后,请参考以下教程在凌智视觉模块上部署检测模型例程: + +* [凌智视觉模块 YOLOv5 目标检测模型 Python 部署指南](#5-python-api-文档) + +## 5 Python API 文档 + +### 5.1 Rect 类 + +Rect 类来自 cv2 模块,用于表示矩形框信息。 + +```python +from lockzhiner_vision_module.cv2 import Rect + +# Rect 类属性 +rect.x # 矩形左上角坐标点的 x 坐标 +rect.y # 矩形左上角坐标点的 y 坐标 +rect.width # 矩形的宽 +rect.height # 矩形的高 +``` + +### 5.2 DetectionResult 类 + +```python +class DetectionResult: + """ + 检测结果类,用于封装和处理目标检测结果数据。 + + 该类主要提供了一个包装层,用于访问和管理由视觉模块产生的检测结果。 + """ + + @property + def box(self): + """ + 获取目标检测模型检测结果的矩形框信息 + + Returns: + Rect: 矩形框信息 + """ + return self.detection_result.box + + @property + def score(self): + """ + 获取目标检测模型检测结果的得分信息 + + Returns: + float: 得分信息 + """ + return self.detection_result.score + + @property + def label_id(self): + """ + 获取目标检测模型检测结果的分类标签信息 + + Returns: + int: 分类标签信息 + """ + return self.detection_result.label_id +``` + +### 5.3 YoloV5 类 + +```python +from lockzhiner_vision_module.vision.detection import YoloV5 + +class YoloV5: + """ + YoloV5 类 - 用于目标检测的 YOLOv5 模型封装。 + + 该类封装了 YOLOv5 框架下的目标检测模型,提供了初始化和预测的方法。 + """ + + def __init__(self): + """ + 构造函数 - 初始化 YoloV5 对象。 + """ + self.model = vision.YoloV5() + + def initialize(self, model_path): + """ + 初始化模型 - 加载预训练的 YOLOv5 模型。 + + Args: + model_path (str): 模型文件的路径。 + + Returns: + bool: 初始化是否成功。 + """ + return self.model.initialize(model_path) + + def load_labels(self, label_path): + """ + 加载标签文件 - 加载类别标签。 + + Args: + label_path (str): 标签文件的路径。 + + Returns: + bool: 加载是否成功。 + """ + return self.model.load_labels(label_path) + + def set_threshold(self, conf_threshold=0.25, nms_threshold=0.45): + """ + 设置目标检测阈值 + + Args: + conf_threshold (float): 目标检测置信度阈值,默认为 0.25 + nms_threshold (float): 目标检测 NMS 阈值,默认为 0.45 + + """ + self.model.set_threshold(conf_threshold, nms_threshold) + + def predict(self, input_mat): + """ + 进行预测 - 使用加载的模型对输入数据进行目标检测预测。 + + Args: + input_mat (cv2.Mat): 输入的图像数据,通常是一个 cv2.Mat 变量。 + + Returns: + list(DetectionResult): 预测结果对象列表,每一个预测结果包含了矩形框、标签信息和置信度等信息。 + """ + return self.model.predict(input_mat) +``` + +## 6 项目介绍 + +为了方便大家入手,我们做了一个简易的目标检测例程。该程序可以使用摄像头进行端到端推理,也可以对单张图像进行检测。 +**注**: YOLOV5 类使用时,除了加载模型外文件外,还需要提供标签文件,该文件存放了模型所对应的标签。 +```python +import lockzhiner_vision_module.cv2 as cv2 +import sys +import os +from lockzhiner_vision_module.vision.detection import YoloV5 +from lockzhiner_vision_module.vision.utils import visualize +from lockzhiner_vision_module.edit import Edit + +def test_yolov5_image(model_path, image_path, label_path=None, output_path='yolov5_result.jpg'): + """ + 测试 YOLOv5 模型在单张图像上的目标检测功能 + + Args: + model_path (str): YOLOv5 模型文件路径 + image_path (str): 测试图像路径 + label_path (str, optional): 标签文件路径 + output_path (str, optional): 输出图像路径,默认为 'yolov5_result.jpg' + """ + print(f"正在初始化 YOLOv5 模型...") + print(f"模型路径: {model_path}") + + model = YoloV5() + + if not model.initialize(model_path): + print("模型初始化失败!") + return False + + print("模型初始化成功!") + + if label_path: + print(f"正在加载标签文件: {label_path}") + if model.load_labels(label_path): + print("标签文件加载成功!") + else: + print("标签文件加载失败,将使用默认标签 ID") + + model.set_threshold(conf_threshold=0.25, nms_threshold=0.45) + print("检测阈值已设置: conf_threshold=0.25, nms_threshold=0.45") + + print(f"\n正在读取图像: {image_path}") + image = cv2.imread(image_path) + + if image is None: + print(f"无法读取图像: {image_path}") + return False + + print(f"图像尺寸: {image.cols}x{image.rows}") + print(f"图像尺寸: {image.shape[1]}x{image.shape[0]}") + print("\n正在进行目标检测...") + results = model.predict(image) + + print(f"检测完成!共检测到 {len(results)} 个目标") + + for i, result in enumerate(results): + print(f"\n目标 {i + 1}:") + print(f" 矩形框: x={result.box.x}, y={result.box.y}, " + f"width={result.box.width}, height={result.box.height}") + print(f" 标签 ID: {result.label_id}") + print(f" 置信度: {result.score:.4f}") + + print(f"\n正在可视化检测结果...") + output_image = visualize(image, results) + + print(f"正在保存结果图像到: {output_path}") + cv2.imwrite(output_path, output_image) + + print(f"\n测试完成!结果已保存到 {output_path}") + return True + + +def test_yolov5_camera(model_path, label_path=None): + """ + 测试 YOLOv5 模型在摄像头实时视频上的目标检测功能 + + Args: + model_path (str): YOLOv5 模型文件路径 + label_path (str, optional): 标签文件路径 + """ + print(f"正在初始化 YOLOv5 模型...") + print(f"模型路径: {model_path}") + width = 640 + height = 480 + edit = Edit() + edit.start_and_accept_connection() + + model = YoloV5() + + if not model.initialize(model_path): + print("模型初始化失败!") + return False + + print("模型初始化成功!") + + if label_path: + print(f"正在加载标签文件: {label_path}") + if model.load_labels(label_path): + print("标签文件加载成功!") + else: + print("标签文件加载失败,将使用默认标签 ID") + + model.set_threshold(conf_threshold=0.25, nms_threshold=0.45) + print("检测阈值已设置: conf_threshold=0.25, nms_threshold=0.45") + + print("\n正在打开摄像头...") + cap = cv2.VideoCapture() + + if not cap.open(0): + print("无法打开摄像头!") + return False + + print("摄像头已打开,按 'q' 键退出") + + while True: + ret, frame = cap.read() + + if not ret: + print("无法读取摄像头画面") + break + + results = model.predict(frame) + + + output_frame = visualize(frame, results) + edit.print(output_frame) + + + cap.release() + cv2.destroyAllWindows() + print("\n摄像头测试结束") + return True + + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("使用方法:") + print(" 测试图像: python test_yolov5.py [label_path] [output_path]") + print(" 测试摄像头: python test_yolov5.py camera [label_path]") + print("\n参数说明:") + print(" model_path - YOLOv5 模型文件路径") + print(" image_path - 测试图像路径") + print(" camera - 使用摄像头进行实时检测") + print(" label_path - (可选) 标签文件路径") + print(" output_path - (可选) 输出图像路径,默认为 'yolov5_result.jpg'") + sys.exit(1) + + model_path = sys.argv[1] + mode = sys.argv[2] + + if mode == "camera": + label_path = sys.argv[3] if len(sys.argv) > 3 else None + test_yolov5_camera(model_path, label_path) + else: + image_path = mode + label_path = sys.argv[3] if len(sys.argv) > 3 else None + output_path = sys.argv[4] if len(sys.argv) > 4 else 'yolov5_result.jpg' + test_yolov5_image(model_path, image_path, label_path, output_path) +``` + +## 7 上传并测试 Python 程序 + +参考 [连接设备指南](../../../../docs/introductory_tutorial/connect_device_using_ssh.md) 正确连接 Lockzhiner Vision Module 设备。 + +请使用 Electerm Sftp 依次上传以下文件: + +- 进入存放 **test_yolov5.py** 脚本文件的目录,将 **test_yolov5.py** 上传到 Lockzhiner Vision Module +- 进入存放 **.rknn** 模型存放的目录(模型存放在训练模型后下载的 output 文件夹内),将 **.rknn** 模型上传到 Lockzhiner Vision Module +- (可选)如果有标签文件,将标签文件也上传到 Lockzhiner Vision Module + +请使用 Electerm Ssh 并在命令行中执行以下命令: + +**测试单张图像:** + +```bash +python test_yolov5.py [label_path] [output_path] +``` + +**测试摄像头:** + +```bash +python test_yolov5.py camera [label_path] +``` + +连接凌智视觉模块图片传输助手后,选择连接设备,运行程序后,屏幕上开始打印矩形框信息、标签信息和置信度,并在一段时间后输出 FPS 值。 + +## 8 各模型性能指标 + +以下测试数据为模型执行 Predict 函数运行 1000 次耗时的平均时间。 + +支持的模型输入尺寸为: +- 640x640:推理时间 100ms +- 416x416:推理时间 70ms +- 320x320:推理时间 50ms + +请根据需求自行调整。 + +| 目标检测模型 | 输入尺寸 | 推理时间(ms) | FPS(帧/s) | +|:------:|:--------:|:--------:|:--------:| +| YOLOv5 | 640x640 | 100 | 10 | +| YOLOv5 | 416x416 | 70 | 14 | +| YOLOv5 | 320x320 | 50 | 20 | + + + +