diff --git a/Cpp_example/D08_pp_humanseg/README.md b/Cpp_example/D08_pp_humanseg/README.md index 7deb1303787d67d49636c41de493e14138946121..766e49a89f429c2db24551a08ce33271c0765529 100644 --- a/Cpp_example/D08_pp_humanseg/README.md +++ b/Cpp_example/D08_pp_humanseg/README.md @@ -54,23 +54,6 @@ bool Run(); - true:推理执行成功。 - false:推理失败(可能原因:输入数据未准备、内存不足等)。 -#### 2.1.5 GetInputAttrs 函数 -```cpp -const std::vector& GetInputAttrs() const; -``` -- 作用:获取模型所有输入张量的属性信息(维度/形状、数据类型、量化参数等)。 -- 参数说明: - - 无 -- 返回值:常量引用形式的 rknn_tensor_attr 向量,包含输入张量属性。 - -#### 2.1.6 GetOutputAttrs 函数 -```cpp -const std::vector& GetInputMemories() const; -``` -- 作用:获取模型所有输出张量的属性信息。 -- 参数说明:无 -- 返回值:常量引用形式的 rknn_tensor_attr 向量,包含输出张量属性。 - ## 3. PP-Humanseg人像分割代码解析 ### 3.1 流程图 @@ -83,8 +66,8 @@ backend.Initialize(model_path) ``` - 获取输入输出属性 ```cpp -const auto& input_attrs = backend.GetInputAttrs(); -const auto& output_attrs = backend.GetOutputAttrs(); +const auto& input_tensor = backend.GetInputTensor(0); +const auto& output_tensor = backend.GetOutputTensor(0); ``` - 对输入图像进行推理 ```cpp @@ -93,27 +76,37 @@ backend.Run() 自定义函数说明 - pp-humanseg输入预处理 ```cpp -cv::Mat preprocess(const cv::Mat& image, const std::vector& input_dims) +cv::Mat preprocess(const cv::Mat& image, + const std::vector& input_dims, + float input_scale, + int input_zp) ``` -- 作用:对输入图像进行预处理操作,包括 ​​尺寸调整​​、​​颜色空间转换​​ 和 ​​量化处理​​,使其符合 RKNN 模型的输入要求。 +- 作用:对输入图像进行预处理操作,包括​尺寸调整​​、​​颜色空间转换​​和​​量化处理​​,使其符合RKNN模型的输入要求。 - 参数说明: - - image:输入图像(BGR 格式的 cv::Mat 对象)。 + - image:输入图像。 - input_dims:模型输入张量的维度定义(需满足 [1, H, W, 3] 的 NHWC 格式)。 + - input_scale:量化缩放因子(用于将浮点像素值转换为 INT8 数值)。 + - input_zp:量化零点偏移值(INT8 数值的偏移基准)。 - 返回值: - - 返回预处理后的量化张量(cv::Mat,数据类型为 CV_8S)。 + - 返回 HxWx3 的量化张量(cv::Mat,数据类型为 CV_8S)。 - 若输入维度不合法,返回空矩阵(cv::Mat())并报错。 + - pp-humanseg输入后处理 ```cpp -cv::Mat postprocess(const rknn_tensor_mem* output_mem, +cv::Mat postprocess(const int8_t* output_data, const std::vector& output_dims, - const cv::Size& target_size) + float output_scale, + int output_zp, + const cv::Size& target_size) ``` -- 作用:将模型输出的原始张量转换为高精度分割掩膜,包含 ​​概率解码​​、​​动态阈值分割​​、​​形态学优化​​和​​边缘增强​​等步骤,最终生成与原始图像尺寸匹配的二值化掩膜。 +- 作用:将RKNN模型的​​量化输出​​转换为​高精度分割掩模​​,通过多阶段优化(概率生成、阈值分割、形态学处理、边缘优化)生成与输入图像尺寸匹配的二进制掩模。 - 参数说明: - - output_mem:模型输出的内存指针,包含量化后的原始数据。 - - output_dims:模型输出的维度信息,需满足 [1, 2, H, W] 的 NCHW 格式。 - - target_size:目标输出尺寸。 -- 返回值:返回优化后的二值化掩膜。 + - output_data:模型输出的量化数据指针。 + - output_dims:输出张量维度,需满足[1, 2, H, W]的NCHW格式。 + - output_scale:反量化缩放因子。 + - output_zp:反量化零点偏移值。 + - taeget_size:目标输出尺寸。 +- 返回值:处理后的二进制掩模。 ### 3.3 完整代码实现 ```cpp @@ -126,14 +119,12 @@ cv::Mat postprocess(const rknn_tensor_mem* output_mem, #include using namespace std::chrono; -// 输入和输出量化的scale和zeropoint -const float INPUT_SCALE = 1.0f / 255.0f; -const int INPUT_ZP = -128; -const float OUTPUT_SCALE = 0.034558f; -const int OUTPUT_ZP = -128; // 预处理函数 -cv::Mat preprocess(const cv::Mat& image, const std::vector& input_dims) { +cv::Mat preprocess(const cv::Mat& image, + const std::vector& input_dims, + float input_scale, + int input_zp) { // 确保输入维度为NHWC [1, H, W, 3] if (input_dims.size() != 4 || input_dims[0] != 1 || input_dims[3] != 3) { std::cerr << "Invalid input dimensions" << std::endl; @@ -150,14 +141,17 @@ cv::Mat preprocess(const cv::Mat& image, const std::vector& input_dims) // 量化到INT8 cv::Mat quantized; - rgb.convertTo(quantized, CV_8S, 255.0 * INPUT_SCALE, INPUT_ZP); + float scale = 1.0f / (input_scale * 255.0f); + rgb.convertTo(quantized, CV_8S, scale, input_zp); return quantized; } // 后处理函数 -cv::Mat postprocess(const rknn_tensor_mem* output_mem, +cv::Mat postprocess(const int8_t* output_data, const std::vector& output_dims, + float output_scale, + int output_zp, const cv::Size& target_size) { // 验证输出维度为NCHW [1, 2, H, W] if (output_dims.size() != 4 || output_dims[0] != 1 || output_dims[1] != 2) { @@ -165,28 +159,24 @@ cv::Mat postprocess(const rknn_tensor_mem* output_mem, return cv::Mat(); } - const int8_t* data = static_cast(output_mem->virt_addr); const int h = output_dims[2]; const int w = output_dims[3]; // ================= 1. 概率图生成优化 ================= cv::Mat prob_map(h, w, CV_32FC1); - // 基于192x192模型的缩放补偿 float spatial_weight = 1.0f - (h * w) / (192.0f * 192.0f); for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { const int bg_idx = 0 * h * w + y * w + x; const int fg_idx = 1 * h * w + y * w + x; - // 带饱和保护的反量化 - float bg_logit = std::clamp((data[bg_idx] - OUTPUT_ZP) * OUTPUT_SCALE, -10.0f, 10.0f); - float fg_logit = std::clamp((data[fg_idx] - OUTPUT_ZP) * OUTPUT_SCALE, -10.0f, 10.0f); + + float bg_logit = std::clamp((output_data[bg_idx] - output_zp) * output_scale, -10.0f, 10.0f); + float fg_logit = std::clamp((output_data[fg_idx] - output_zp) * output_scale, -10.0f, 10.0f); - // 空间注意力加权(中心区域增强) float center_weight = 1.0f - (std::abs(x - w/2.0f)/(w/2.0f) + std::abs(y - h/2.0f)/(h/2.0f))/2.0f; fg_logit *= (1.2f + 0.3f * center_weight * spatial_weight); - // 稳健的Softmax计算 float max_logit = std::max(bg_logit, fg_logit); float exp_sum = expf(bg_logit - max_logit) + expf(fg_logit - max_logit); prob_map.at(y, x) = expf(fg_logit - max_logit) / (exp_sum + 1e-8f); @@ -195,7 +185,6 @@ cv::Mat postprocess(const rknn_tensor_mem* output_mem, // ================= 2. 自适应阈值优化 ================= cv::Mat binary_mask; - // 重点区域检测 cv::Mat prob_roi = prob_map(cv::Rect(w/4, h/4, w/2, h/2)); float center_mean = cv::mean(prob_roi)[0]; float dynamic_thresh = std::clamp(0.45f - (center_mean - 0.5f) * 0.3f, 0.25f, 0.65f); @@ -205,18 +194,14 @@ cv::Mat postprocess(const rknn_tensor_mem* output_mem, // ================= 3. 多尺度形态学处理 ================= std::vector mask_pyramid; - // 构建金字塔 cv::buildPyramid(binary_mask, mask_pyramid, 2); - // 小尺度去噪 cv::Mat kernel1 = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3,3)); cv::morphologyEx(mask_pyramid[1], mask_pyramid[1], cv::MORPH_OPEN, kernel1); - // 中尺度填充 cv::Mat kernel2 = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5,5)); cv::morphologyEx(mask_pyramid[0], mask_pyramid[0], cv::MORPH_CLOSE, kernel2); - // 金字塔重建 cv::Mat refined_mask; cv::pyrUp(mask_pyramid[1], refined_mask, mask_pyramid[0].size()); cv::bitwise_and(refined_mask, mask_pyramid[0], refined_mask); @@ -241,7 +226,6 @@ cv::Mat postprocess(const rknn_tensor_mem* output_mem, cv::Mat final_mask; cv::bilateralFilter(resized_mask, final_mask, 5, 15, 15); - // 最终保边处理 cv::Mat edge_mask_hr; cv::resize(final_edges, edge_mask_hr, target_size, 0, 0, cv::INTER_NEAREST); final_mask.setTo(255, edge_mask_hr > 128); @@ -272,25 +256,22 @@ int main(int argc, char* argv[]) { return -1; } - // 获取输入属性 - const auto& input_attrs = backend.GetInputAttrs(); - if (input_attrs.empty()) { - std::cerr << "No input attributes found" << std::endl; - return -1; - } - const auto& input_attr = input_attrs[0]; - std::vector input_dims(input_attr.dims, input_attr.dims + input_attr.n_dims); + // 获取输入Tensor + const auto& input_tensor = backend.GetInputTensor(0); + std::vector input_dims = input_tensor.GetDims(); + float input_scale = input_tensor.GetScale(); + int input_zp = input_tensor.GetZp(); // 预处理 - cv::Mat preprocessed = preprocess(image, input_dims); + cv::Mat preprocessed = preprocess(image, input_dims, input_scale, input_zp); if (preprocessed.empty()) { std::cerr << "Preprocessing failed" << std::endl; return -1; } // 验证输入数据尺寸 - const size_t expected_input_size = input_attr.size_with_stride; - const size_t actual_input_size = preprocessed.total() * preprocessed.elemSize(); + size_t expected_input_size = input_tensor.GetElemsBytes(); + size_t actual_input_size = preprocessed.total() * preprocessed.elemSize(); if (expected_input_size != actual_input_size) { std::cerr << "Input size mismatch! Expected: " << expected_input_size << ", Actual: " << actual_input_size << std::endl; @@ -298,40 +279,30 @@ int main(int argc, char* argv[]) { } // 拷贝输入数据 - const auto& input_memories = backend.GetInputMemories(); - if (input_memories.empty() || !input_memories[0]) { - std::cerr << "Invalid input memory" << std::endl; - return -1; - } - memcpy(input_memories[0]->virt_addr, preprocessed.data, actual_input_size); + void* input_data = input_tensor.GetData(); + memcpy(input_data, preprocessed.data, actual_input_size); // 执行推理 - high_resolution_clock::time_point start_time = - high_resolution_clock::now(); + high_resolution_clock::time_point start_time = high_resolution_clock::now(); if (!backend.Run()) { std::cerr << "Inference failed" << std::endl; return -1; } - // 获取输出 - const auto& output_attrs = backend.GetOutputAttrs(); - if (output_attrs.empty()) { - std::cerr << "No output attributes found" << std::endl; - return -1; - } - const auto& output_memories = backend.GetOutputMemories(); - if (output_memories.empty() || !output_memories[0]) { - std::cerr << "Invalid output memory" << std::endl; - return -1; - } + // 获取输出Tensor + const auto& output_tensor = backend.GetOutputTensor(0); + std::vector output_dims = output_tensor.GetDims(); + float output_scale = output_tensor.GetScale(); + int output_zp = output_tensor.GetZp(); + const int8_t* output_data = static_cast(output_tensor.GetData()); // 后处理 - const auto& output_attr = output_attrs[0]; - std::vector output_dims(output_attr.dims, output_attr.dims + output_attr.n_dims); - cv::Mat mask = postprocess(output_memories[0], output_dims, image.size()); high_resolution_clock::time_point end_time = high_resolution_clock::now(); auto time_span = duration_cast(end_time - start_time); + cv::Mat mask = postprocess(output_data, output_dims, output_scale, output_zp, image.size()); + std::cout << "单张图片推理时间(ms): " << time_span.count() << std::endl; + // 生成结果 cv::Mat result; cv::bitwise_and(image, image, result, mask); diff --git a/Cpp_example/D08_pp_humanseg/images/view.png b/Cpp_example/D08_pp_humanseg/images/view.png old mode 100755 new mode 100644 diff --git a/Cpp_example/D08_pp_humanseg/pp_humanseg.cc b/Cpp_example/D08_pp_humanseg/pp_humanseg.cc index a73c4749617e2933f9c7b718f0e599b02b703990..a9ad17e5b71c8f72eac1653c7bceaa0d136f5d30 100644 --- a/Cpp_example/D08_pp_humanseg/pp_humanseg.cc +++ b/Cpp_example/D08_pp_humanseg/pp_humanseg.cc @@ -7,14 +7,12 @@ #include using namespace std::chrono; -// 输入和输出量化的scale和zeropoint -const float INPUT_SCALE = 1.0f / 255.0f; -const int INPUT_ZP = -128; -const float OUTPUT_SCALE = 0.034558f; -const int OUTPUT_ZP = -128; // 预处理函数 -cv::Mat preprocess(const cv::Mat& image, const std::vector& input_dims) { +cv::Mat preprocess(const cv::Mat& image, + const std::vector& input_dims, + float input_scale, + int input_zp) { // 确保输入维度为NHWC [1, H, W, 3] if (input_dims.size() != 4 || input_dims[0] != 1 || input_dims[3] != 3) { std::cerr << "Invalid input dimensions" << std::endl; @@ -31,14 +29,17 @@ cv::Mat preprocess(const cv::Mat& image, const std::vector& input_dims) // 量化到INT8 cv::Mat quantized; - rgb.convertTo(quantized, CV_8S, 255.0 * INPUT_SCALE, INPUT_ZP); + float scale = 1.0f / (input_scale * 255.0f); + rgb.convertTo(quantized, CV_8S, scale, input_zp); return quantized; } // 后处理函数 -cv::Mat postprocess(const rknn_tensor_mem* output_mem, +cv::Mat postprocess(const int8_t* output_data, const std::vector& output_dims, + float output_scale, + int output_zp, const cv::Size& target_size) { // 验证输出维度为NCHW [1, 2, H, W] if (output_dims.size() != 4 || output_dims[0] != 1 || output_dims[1] != 2) { @@ -46,28 +47,24 @@ cv::Mat postprocess(const rknn_tensor_mem* output_mem, return cv::Mat(); } - const int8_t* data = static_cast(output_mem->virt_addr); const int h = output_dims[2]; const int w = output_dims[3]; // ================= 1. 概率图生成优化 ================= cv::Mat prob_map(h, w, CV_32FC1); - // 基于192x192模型的缩放补偿 float spatial_weight = 1.0f - (h * w) / (192.0f * 192.0f); for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { const int bg_idx = 0 * h * w + y * w + x; const int fg_idx = 1 * h * w + y * w + x; - // 带饱和保护的反量化 - float bg_logit = std::clamp((data[bg_idx] - OUTPUT_ZP) * OUTPUT_SCALE, -10.0f, 10.0f); - float fg_logit = std::clamp((data[fg_idx] - OUTPUT_ZP) * OUTPUT_SCALE, -10.0f, 10.0f); + + float bg_logit = std::clamp((output_data[bg_idx] - output_zp) * output_scale, -10.0f, 10.0f); + float fg_logit = std::clamp((output_data[fg_idx] - output_zp) * output_scale, -10.0f, 10.0f); - // 空间注意力加权(中心区域增强) float center_weight = 1.0f - (std::abs(x - w/2.0f)/(w/2.0f) + std::abs(y - h/2.0f)/(h/2.0f))/2.0f; fg_logit *= (1.2f + 0.3f * center_weight * spatial_weight); - // 稳健的Softmax计算 float max_logit = std::max(bg_logit, fg_logit); float exp_sum = expf(bg_logit - max_logit) + expf(fg_logit - max_logit); prob_map.at(y, x) = expf(fg_logit - max_logit) / (exp_sum + 1e-8f); @@ -76,7 +73,6 @@ cv::Mat postprocess(const rknn_tensor_mem* output_mem, // ================= 2. 自适应阈值优化 ================= cv::Mat binary_mask; - // 重点区域检测 cv::Mat prob_roi = prob_map(cv::Rect(w/4, h/4, w/2, h/2)); float center_mean = cv::mean(prob_roi)[0]; float dynamic_thresh = std::clamp(0.45f - (center_mean - 0.5f) * 0.3f, 0.25f, 0.65f); @@ -86,18 +82,14 @@ cv::Mat postprocess(const rknn_tensor_mem* output_mem, // ================= 3. 多尺度形态学处理 ================= std::vector mask_pyramid; - // 构建金字塔 cv::buildPyramid(binary_mask, mask_pyramid, 2); - // 小尺度去噪 cv::Mat kernel1 = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3,3)); cv::morphologyEx(mask_pyramid[1], mask_pyramid[1], cv::MORPH_OPEN, kernel1); - // 中尺度填充 cv::Mat kernel2 = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5,5)); cv::morphologyEx(mask_pyramid[0], mask_pyramid[0], cv::MORPH_CLOSE, kernel2); - // 金字塔重建 cv::Mat refined_mask; cv::pyrUp(mask_pyramid[1], refined_mask, mask_pyramid[0].size()); cv::bitwise_and(refined_mask, mask_pyramid[0], refined_mask); @@ -122,7 +114,6 @@ cv::Mat postprocess(const rknn_tensor_mem* output_mem, cv::Mat final_mask; cv::bilateralFilter(resized_mask, final_mask, 5, 15, 15); - // 最终保边处理 cv::Mat edge_mask_hr; cv::resize(final_edges, edge_mask_hr, target_size, 0, 0, cv::INTER_NEAREST); final_mask.setTo(255, edge_mask_hr > 128); @@ -153,25 +144,22 @@ int main(int argc, char* argv[]) { return -1; } - // 获取输入属性 - const auto& input_attrs = backend.GetInputAttrs(); - if (input_attrs.empty()) { - std::cerr << "No input attributes found" << std::endl; - return -1; - } - const auto& input_attr = input_attrs[0]; - std::vector input_dims(input_attr.dims, input_attr.dims + input_attr.n_dims); + // 获取输入Tensor + const auto& input_tensor = backend.GetInputTensor(0); + std::vector input_dims = input_tensor.GetDims(); + float input_scale = input_tensor.GetScale(); + int input_zp = input_tensor.GetZp(); // 预处理 - cv::Mat preprocessed = preprocess(image, input_dims); + cv::Mat preprocessed = preprocess(image, input_dims, input_scale, input_zp); if (preprocessed.empty()) { std::cerr << "Preprocessing failed" << std::endl; return -1; } // 验证输入数据尺寸 - const size_t expected_input_size = input_attr.size_with_stride; - const size_t actual_input_size = preprocessed.total() * preprocessed.elemSize(); + size_t expected_input_size = input_tensor.GetElemsBytes(); + size_t actual_input_size = preprocessed.total() * preprocessed.elemSize(); if (expected_input_size != actual_input_size) { std::cerr << "Input size mismatch! Expected: " << expected_input_size << ", Actual: " << actual_input_size << std::endl; @@ -179,40 +167,30 @@ int main(int argc, char* argv[]) { } // 拷贝输入数据 - const auto& input_memories = backend.GetInputMemories(); - if (input_memories.empty() || !input_memories[0]) { - std::cerr << "Invalid input memory" << std::endl; - return -1; - } - memcpy(input_memories[0]->virt_addr, preprocessed.data, actual_input_size); + void* input_data = input_tensor.GetData(); + memcpy(input_data, preprocessed.data, actual_input_size); // 执行推理 - high_resolution_clock::time_point start_time = - high_resolution_clock::now(); + high_resolution_clock::time_point start_time = high_resolution_clock::now(); if (!backend.Run()) { std::cerr << "Inference failed" << std::endl; return -1; } - // 获取输出 - const auto& output_attrs = backend.GetOutputAttrs(); - if (output_attrs.empty()) { - std::cerr << "No output attributes found" << std::endl; - return -1; - } - const auto& output_memories = backend.GetOutputMemories(); - if (output_memories.empty() || !output_memories[0]) { - std::cerr << "Invalid output memory" << std::endl; - return -1; - } + // 获取输出Tensor + const auto& output_tensor = backend.GetOutputTensor(0); + std::vector output_dims = output_tensor.GetDims(); + float output_scale = output_tensor.GetScale(); + int output_zp = output_tensor.GetZp(); + const int8_t* output_data = static_cast(output_tensor.GetData()); // 后处理 - const auto& output_attr = output_attrs[0]; - std::vector output_dims(output_attr.dims, output_attr.dims + output_attr.n_dims); - cv::Mat mask = postprocess(output_memories[0], output_dims, image.size()); high_resolution_clock::time_point end_time = high_resolution_clock::now(); auto time_span = duration_cast(end_time - start_time); + cv::Mat mask = postprocess(output_data, output_dims, output_scale, output_zp, image.size()); + std::cout << "单张图片推理时间(ms): " << time_span.count() << std::endl; + // 生成结果 cv::Mat result; cv::bitwise_and(image, image, result, mask); @@ -229,4 +207,4 @@ int main(int argc, char* argv[]) { cv::waitKey(0); return 0; -} +} \ No newline at end of file diff --git a/Cpp_example/D11_PPOCRv3/CMakeLists.txt b/Cpp_example/D11_PPOCRv3/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..fe0750f8dab607d2653cca1cde10e2d622d9711f --- /dev/null +++ b/Cpp_example/D11_PPOCRv3/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.10) + +project(PPOCRv3) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 定义项目根目录路径 +set(PROJECT_ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../..") +message("PROJECT_ROOT_PATH = " ${PROJECT_ROOT_PATH}) + +include("${PROJECT_ROOT_PATH}/toolchains/arm-rockchip830-linux-uclibcgnueabihf.toolchain.cmake") + +# 定义 OpenCV SDK 路径 +set(OpenCV_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/opencv-mobile-4.10.0-lockzhiner-vision-module") +set(OpenCV_DIR "${OpenCV_ROOT_PATH}/lib/cmake/opencv4") +find_package(OpenCV REQUIRED) +set(OPENCV_LIBRARIES "${OpenCV_LIBS}") + +# 定义 LockzhinerVisionModule SDK 路径 +set(LockzhinerVisionModule_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/lockzhiner_vision_module_sdk") +set(LockzhinerVisionModule_DIR "${LockzhinerVisionModule_ROOT_PATH}/lib/cmake/lockzhiner_vision_module") +find_package(LockzhinerVisionModule REQUIRED) + +# ncnn配置 +set(NCNN_ROOT_DIR "${PROJECT_ROOT_PATH}/third_party/ncnn-20240820-lockzhiner-vision-module") # 确保third_party层级存在 +message(STATUS "Checking ncnn headers in: ${NCNN_ROOT_DIR}/include/ncnn") + +# 验证头文件存在 +if(NOT EXISTS "${NCNN_ROOT_DIR}/include/ncnn/net.h") + message(FATAL_ERROR "ncnn headers not found. Confirm the directory contains ncnn: ${NCNN_ROOT_DIR}") +endif() + +set(NCNN_INCLUDE_DIRS "${NCNN_ROOT_DIR}/include") +set(NCNN_LIBRARIES "${NCNN_ROOT_DIR}/lib/libncnn.a") + +add_executable(Test-ppocrv3 ppocrv3.cc) +target_include_directories(Test-ppocrv3 PRIVATE ${LOCKZHINER_VISION_MODULE_INCLUDE_DIRS} ${NCNN_INCLUDE_DIRS}) +target_link_libraries(Test-ppocrv3 PRIVATE ${OPENCV_LIBRARIES} ${NCNN_LIBRARIES} ${LOCKZHINER_VISION_MODULE_LIBRARIES}) + +install( + TARGETS Test-ppocrv3 + RUNTIME DESTINATION . +) \ No newline at end of file diff --git a/Cpp_example/D11_PPOCRv3/README.md b/Cpp_example/D11_PPOCRv3/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f659aff3674010f0e7280238899c6295d83284a3 --- /dev/null +++ b/Cpp_example/D11_PPOCRv3/README.md @@ -0,0 +1,379 @@ +# PPOCRv3 字符识别 +本章节在 Lockzhiner Vision Module 上基于PPOCRv3字符识别模型, 实现了一个简单的字符识别系统。 + +## 1. 基本知识讲解 +### 1.1 字符识别简介 +OCR(光学字符识别)是指通过电子设备读取并转换纸质文档或图像中的文字为可编辑和处理的数字文本的技术。它涉及图像预处理、字符分割、特征提取、字符识别及后处理等步骤,以实现高准确度的文字转换。OCR技术极大提升了信息数字化的效率,广泛应用于数字化图书馆、自动化数据录入、车牌识别系统及辅助阅读工具等领域,是现代办公与生活中不可或缺的一部分。 +### 1.2 字符识别常用方法 +- 模板匹配:通过与预定义字符模板比较来识别字符,适用于固定字体和字号。 +- 特征提取:从字符中提取关键特征(如线条、端点)并使用分类器识别,适应字体变化。 +- 神经网络:利用卷积神经网络自动学习字符特征,特别适合复杂背景和多变字体,提供高准确率。 +这些方法各有优势,选择取决于具体应用需求和文档特性。随着技术发展,基于神经网络的方法因其高性能而得到广泛应用。 + +## 2. C++ API 文档 +### 2.1 Net类 +#### 2.1.1 头文件 +```cpp +#include +``` +- 作用:用于声明Net类,使得Net类可以在当前文件中使用。 + +#### 2.1.2 构造类对象 +```cpp +ncnn::Net ocr_net; +``` +- 作用:创建一个Net类型的对象实例,用于实现字符识别。 +- 参数说明: + - 无 +- 返回值: + - 无 + +#### 2.1.3 load_param函数 +```cpp +int load_param(const DataReader& dr); +``` +- 参数说明: + - dr:传入的参数文件路径。 +- 返回值: + - 返回值为0表示加载参数文件成功。 + +#### 2.1.4 load_model函数 +```cpp +int load_model(const DataReader& dr); +``` +- 参数说明: + - dr:传入的模型文件路径。 +- 返回值:返回值为0表示加载模型成功。 + +#### 2.1.5 from_pixels函数 +```cpp +ncnn::Mat::from_pixels(srcResize.data, ncnn::Mat::PIXEL_BGR, srcResize.cols, srcResize.rows); +``` +- 参数说明: + - srcResize.data:输入图像的像素数据指针。 + - ncnn::Mat::PIXEL_BGR:输入像素数据的颜色格式。 + - srcResize.cols:输入图像的宽度。 + - srcResize.rows:输入图像的高度。 +- 返回值:适配成 NCNN 所需的格式的包含图像数据的新对象。 + +### 2.2 Extractor类 +#### 2.2.1 头文件 +```cpp +#include +``` +- 作用:用于声明Extractor类,使得Extractor类可以在当前文件中使用。 + +#### 2.2.2 构造类函数 +```cpp +ncnn::Extractor extractor = net.create_extractor(); +``` +- 作用:从已经加载了神经网络模型的 net 中创建一个 Extractor 实例,用于执行文字识别的推理任务。 +- 参数说明: + - 无 +- 返回值: + - 无 + +## 3. PPOCRv3 字符识别代码解析 +### 3.1 流程图 + + + +### 3.2 核心代码解析 +#### 3.2.1 初始化模型 +```cpp +bool InitOCRModel(const string& param_path, const string& model_path, const string& dict_path) +``` +- 作用:加载OCR模型参数和权重文件,初始化字符字典,完成OCR系统初始化。 +- 参数说明: + - param_path:模型结构文件路径(.param文件)。 + - model_path:模型权重文件路径(.bin文件)。 + - dict_path:字符字典文件路径。 +- 返回值: + - true:模型和字典加载均成功。 + - false:模型加载失败 或 字典加载失败。 + +#### 3.2.2 字符识别 +```cpp +string RecognizePlate(cv::Mat plate_img) +``` +- 作用:执行图像预处理、模型推理、CTC解码全流程。 +- 参数说明: + - plate_img:待识别的区域图像(BGR格式)。 +- 返回值: + - license:解码后的识别结果(UTF-8字符串)。 + +#### 3.2.3 CTC解码 +```cpp +string decode_ctc(const ncnn::Mat& out) +``` +- 作用:对OCR模型输出的概率矩阵进行CTC解码,将时序分类结果转换为可读文本序列。 +- 参数说明: + - out:模型输出的概率矩阵。 +- 返回值: + - result:解码后的文本字符串(已过滤无效字符)。 + +#### 3.2.4 加载字符字典 +```cpp +vector load_character_dict(const string& path, bool use_space_char) +``` +- 作用:从文本文件加载字符字典,生成符合CTC模型要求的字符列表。 +- 参数说明: + - path:字典文件路径(每行一个字符)。 + - use_space_char:是否在字典末尾添加空格符。 +- 返回值: + - 结构化的字符列表,格式为:["blank", char1, char2,..., (空格)]。 + +### 3.3 完整代码实现 +```cpp +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace std::chrono; + +// OCR配置参数 +const cv::Size OCR_INPUT_SIZE(320, 48); + +const bool USE_SPACE_CHAR = true; +const float MEAN_VALS[3] = {127.5f, 127.5f, 127.5f}; +const float NORM_VALS[3] = {1.0f/127.5f, 1.0f/127.5f, 1.0f/127.5f}; + +ncnn::Net ocr_net; + + +vector char_list; // 从字典加载的字符列表 + +// 加载字符字典 +vector load_character_dict(const string& path, bool use_space_char) { + vector temp_list; + ifstream file(path); + if (!file.is_open()) { + cerr << "Failed to open dictionary file: " << path << endl; + return {}; + } + + string line; + while (getline(file, line)) { + line.erase(remove(line.begin(), line.end(), '\r'), line.end()); + line.erase(remove(line.begin(), line.end(), '\n'), line.end()); + temp_list.push_back(line); + } + + if (use_space_char) { + temp_list.push_back(" "); + } + + vector char_list{"blank"}; + char_list.insert(char_list.end(), temp_list.begin(), temp_list.end()); + return char_list; +} + +// CTC解码 +string decode_ctc(const ncnn::Mat& out) { + vector indices; + const int num_timesteps = out.h; + const int num_classes = out.w; + + for (int t = 0; t < num_timesteps; ++t) { + const float* prob = out.row(t); + int max_idx = 0; + float max_prob = prob[0]; + + for (int c = 0; c < num_classes; ++c) { + if (prob[c] > max_prob) { + max_idx = c; + max_prob = prob[c]; + } + } + indices.push_back(max_idx); + } + + string result; + int prev_idx = -1; + + for (int idx : indices) { + if (idx == 0) { + prev_idx = -1; + continue; + } + if (idx != prev_idx) { + if (idx < char_list.size()) { + result += char_list[idx]; + } + prev_idx = idx; + } + } + + return result; +} + + +// 初始化OCR模型 +bool InitOCRModel(const string& param_path, const string& model_path, const string& dict_path) { + if (!ocr_net.load_param(param_path.c_str()) && !ocr_net.load_model(model_path.c_str())) { + char_list = load_character_dict(dict_path, USE_SPACE_CHAR); + return !char_list.empty(); + } + return false; +} + +// 文字识别 +string RecognizePlate(cv::Mat plate_img) { + // 图像预处理 + cv::resize(plate_img, plate_img, OCR_INPUT_SIZE); + ncnn::Mat in = ncnn::Mat::from_pixels(plate_img.data, + ncnn::Mat::PIXEL_BGR, + plate_img.cols, + plate_img.rows); + // PP-OCR风格归一化 + in.substract_mean_normalize(MEAN_VALS, NORM_VALS); + + // 模型推理 + ncnn::Extractor ex = ocr_net.create_extractor(); + ex.input("in0", in); + + ncnn::Mat out; + ex.extract("out0", out); + + // CTC解码 + string license = decode_ctc(out); + return license; +} + +int main(int argc, char** argv) { + // 参数验证 + if (argc != 5) { + cerr << "Usage: " << argv[0] + << " [image_path]\n" + << "Example:\n" + << " Realtime: " << argv[0] << " ocr.param ocr.bin ppocr_keys_v1.txt\n" + << " Image: " << argv[0] << " ocr.param ocr.bin ppocr_keys_v1.txt test.jpg\n"; + return 1; + } + // 初始化OCR模型和字典 + if (!InitOCRModel(argv[1], argv[2], argv[3])) { + cerr << "Failed to initialize OCR system" << endl; + return 1; + } + + // 图片处理模式 + cv::Mat image = cv::imread(argv[4]); + if (image.empty()) { + cerr << "Failed to read image: " << argv[4] << endl; + return 1; + } + + auto ocr_start = std::chrono::high_resolution_clock::now(); + string result = RecognizePlate(image); + auto ocr_end = std::chrono::high_resolution_clock::now(); + std::cout << "OCR: " << std::chrono::duration(ocr_end - ocr_start).count() << "s\n"; + + cout << " 识别结果: " << result << endl; + cv::waitKey(0); +} +``` + +## 4. 编译调试 +### 4.1 编译环境搭建 +- 请确保你已经按照 [开发环境搭建指南](../../../../docs/introductory_tutorial/cpp_development_environment.md) 正确配置了开发环境。 +- 同时已经正确连接开发板。 +### 4.2 Cmake介绍 +```cmake +cmake_minimum_required(VERSION 3.10) + +project(PPOCRv3) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 定义项目根目录路径 +set(PROJECT_ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../..") +message("PROJECT_ROOT_PATH = " ${PROJECT_ROOT_PATH}) + +include("${PROJECT_ROOT_PATH}/toolchains/arm-rockchip830-linux-uclibcgnueabihf.toolchain.cmake") + +# 定义 OpenCV SDK 路径 +set(OpenCV_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/opencv-mobile-4.10.0-lockzhiner-vision-module") +set(OpenCV_DIR "${OpenCV_ROOT_PATH}/lib/cmake/opencv4") +find_package(OpenCV REQUIRED) +set(OPENCV_LIBRARIES "${OpenCV_LIBS}") + +# 定义 LockzhinerVisionModule SDK 路径 +set(LockzhinerVisionModule_ROOT_PATH "${PROJECT_ROOT_PATH}/third_party/lockzhiner_vision_module_sdk") +set(LockzhinerVisionModule_DIR "${LockzhinerVisionModule_ROOT_PATH}/lib/cmake/lockzhiner_vision_module") +find_package(LockzhinerVisionModule REQUIRED) + +# ncnn配置 +set(NCNN_ROOT_DIR "${PROJECT_ROOT_PATH}/third_party/ncnn-20240820-lockzhiner-vision-module") # 确保third_party层级存在 +message(STATUS "Checking ncnn headers in: ${NCNN_ROOT_DIR}/include/ncnn") + +# 验证头文件存在 +if(NOT EXISTS "${NCNN_ROOT_DIR}/include/ncnn/net.h") + message(FATAL_ERROR "ncnn headers not found. Confirm the directory contains ncnn: ${NCNN_ROOT_DIR}") +endif() + +set(NCNN_INCLUDE_DIRS "${NCNN_ROOT_DIR}/include") +set(NCNN_LIBRARIES "${NCNN_ROOT_DIR}/lib/libncnn.a") + +add_executable(Test-ppocrv3 ppocrv3.cc) +target_include_directories(Test-ppocrv3 PRIVATE ${LOCKZHINER_VISION_MODULE_INCLUDE_DIRS} ${NCNN_INCLUDE_DIRS}) +target_link_libraries(Test-ppocrv3 PRIVATE ${OPENCV_LIBRARIES} ${NCNN_LIBRARIES} ${LOCKZHINER_VISION_MODULE_LIBRARIES}) + +install( + TARGETS Test-ppocrv3 + RUNTIME DESTINATION . +) +``` +### 4.3 编译项目 +使用 Docker Destop 打开 LockzhinerVisionModule 容器并执行以下命令来编译项目 +```bash +# 进入Demo所在目录 +cd /LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/D10_PPOCRv3 +# 创建编译目录 +rm -rf build && mkdir build && cd build +# 配置交叉编译工具链 +export TOOLCHAIN_ROOT_PATH="/LockzhinerVisionModuleWorkSpace/arm-rockchip830-linux-uclibcgnueabihf" +# 使用cmake配置项目 +cmake .. +# 执行编译项目 +make -j8 && make install +``` + +在执行完上述命令后,会在build目录下生成可执行文件。 + + +## 5. 执行结果 +### 5.1 运行前准备 +- 请确保你已经下载了 [凌智视觉模块字符识别模型结构文件](https://gitee.com/LockzhinerAI/LockzhinerVisionModule/releases/download/v0.0.6/ppocrv3.param) +- 请确保你已经下载了 [凌智视觉模块字符识别模型权重文件](https://gitee.com/LockzhinerAI/LockzhinerVisionModule/releases/download/v0.0.6/ppocrv3.bin) +- 请确保你已经下载了 [凌智视觉模块文字识别keys文件](https://gitee.com/LockzhinerAI/LockzhinerVisionModule/releases/download/v0.0.6/ppocr_keys_v1.txt) +### 5.2 运行过程 +```shell +chmod 777 Test-ppocrv3 +./Test-ppocrv3 ppocrv3.param ppocrv3.bin ppocr_keys_v1.txt image_path +``` +### 5.3 运行效果 +#### 5.3.1 PPOCRv3字符识别 +- 测试图像 + +![title](./images/2.png) + +- 识别结果 + +![title](./images/3.png) + +#### 5.3.2 注意事项 +由于本章节只训练了一个PPOCRv3的文字识别模型,并没有训练检测模型,所有只针对包含单行文本的图像效果比较好,对于包含多行文本的识别,效果并不是很好。 + +## 6. 总结 +通过上述内容,我们成功实现了一个简单的基于PPOCRv3的字符识别系统,包括: + +- 加载识别模型和检测图像。 +- 进行图像的推理以及字符识别。 +- 将识别结果打印出来。 \ No newline at end of file diff --git a/Cpp_example/D11_PPOCRv3/images/1.png b/Cpp_example/D11_PPOCRv3/images/1.png new file mode 100644 index 0000000000000000000000000000000000000000..36372fa5ed285083aa7af456970858957da94c2d Binary files /dev/null and b/Cpp_example/D11_PPOCRv3/images/1.png differ diff --git a/Cpp_example/D11_PPOCRv3/images/10.png b/Cpp_example/D11_PPOCRv3/images/10.png new file mode 100644 index 0000000000000000000000000000000000000000..691fcfed4ffc7b0a10c2f0f840e1ac13084b3e82 Binary files /dev/null and b/Cpp_example/D11_PPOCRv3/images/10.png differ diff --git a/Cpp_example/D11_PPOCRv3/images/11.png b/Cpp_example/D11_PPOCRv3/images/11.png new file mode 100644 index 0000000000000000000000000000000000000000..c63f929027c346c94f8fbda92ec6729aa256ad87 Binary files /dev/null and b/Cpp_example/D11_PPOCRv3/images/11.png differ diff --git a/Cpp_example/D11_PPOCRv3/images/12.png b/Cpp_example/D11_PPOCRv3/images/12.png new file mode 100644 index 0000000000000000000000000000000000000000..ae189297a34c4fe2913456b13bc09265e8d375cc Binary files /dev/null and b/Cpp_example/D11_PPOCRv3/images/12.png differ diff --git a/Cpp_example/D11_PPOCRv3/images/2.png b/Cpp_example/D11_PPOCRv3/images/2.png new file mode 100644 index 0000000000000000000000000000000000000000..665dd9cd68ae536e96c5b8649d2ac51b03f0da89 Binary files /dev/null and b/Cpp_example/D11_PPOCRv3/images/2.png differ diff --git a/Cpp_example/D11_PPOCRv3/images/3.png b/Cpp_example/D11_PPOCRv3/images/3.png new file mode 100644 index 0000000000000000000000000000000000000000..bda9461f71412999b3eff93d08dd6dedcc4ce6ef Binary files /dev/null and b/Cpp_example/D11_PPOCRv3/images/3.png differ diff --git a/Cpp_example/D11_PPOCRv3/images/4.png b/Cpp_example/D11_PPOCRv3/images/4.png new file mode 100644 index 0000000000000000000000000000000000000000..7941f6ea97dbcf5aa863f16948a6ffd9be134033 Binary files /dev/null and b/Cpp_example/D11_PPOCRv3/images/4.png differ diff --git a/Cpp_example/D11_PPOCRv3/images/5.png b/Cpp_example/D11_PPOCRv3/images/5.png new file mode 100644 index 0000000000000000000000000000000000000000..7941f6ea97dbcf5aa863f16948a6ffd9be134033 Binary files /dev/null and b/Cpp_example/D11_PPOCRv3/images/5.png differ diff --git a/Cpp_example/D11_PPOCRv3/images/6.png b/Cpp_example/D11_PPOCRv3/images/6.png new file mode 100644 index 0000000000000000000000000000000000000000..f352a8d5ac526157b71385248bd0197dc38e5d18 Binary files /dev/null and b/Cpp_example/D11_PPOCRv3/images/6.png differ diff --git a/Cpp_example/D11_PPOCRv3/images/7.png b/Cpp_example/D11_PPOCRv3/images/7.png new file mode 100644 index 0000000000000000000000000000000000000000..688ad2674415176988c0c3c30e57c4fde23d8fb1 Binary files /dev/null and b/Cpp_example/D11_PPOCRv3/images/7.png differ diff --git a/Cpp_example/D11_PPOCRv3/images/8.png b/Cpp_example/D11_PPOCRv3/images/8.png new file mode 100644 index 0000000000000000000000000000000000000000..62ffdafe4e562faa01d3f646f6ecff8f3f1be778 Binary files /dev/null and b/Cpp_example/D11_PPOCRv3/images/8.png differ diff --git a/Cpp_example/D11_PPOCRv3/images/9.png b/Cpp_example/D11_PPOCRv3/images/9.png new file mode 100644 index 0000000000000000000000000000000000000000..393429cc6cf3f478aecaa2fb42cc16ed0c80622f Binary files /dev/null and b/Cpp_example/D11_PPOCRv3/images/9.png differ diff --git a/Cpp_example/D11_PPOCRv3/paddle2ncnn.md b/Cpp_example/D11_PPOCRv3/paddle2ncnn.md new file mode 100644 index 0000000000000000000000000000000000000000..d6028cf60c6dd981daea7a1b429c9c7f6ccba34d --- /dev/null +++ b/Cpp_example/D11_PPOCRv3/paddle2ncnn.md @@ -0,0 +1,68 @@ +# PPOCRv3文字识别模型转NCNN部署 +## 1. 简介 +本文档详细介绍了如何将PPOCRv3文字识别模型转换为ncnn格式的完整实现流程,通过多框架协同实现跨平台部署,核心链路为:​paddle -> pth -> pt -> pnnx -> ncnn​​。之前也尝试使用paddle → onnx → ncnn的方案,但是在转ncnn过程中,遇到了某些层不支持的问题,经过修改ncnn模型参数,可实现部署,但是修改较复杂,很不易用,不推荐先转onnx再转到ncnn格式。现在利用paddle -> pth -> pt -> pnnx -> ncnn 的方式实现ncnn模型部署,且不需要对PPOCRv3的模型进行修改,以下是主要的实现步骤: + +* paddle -> pth -> pt +* pt -> pnnx -> ncnn + +## 2. paddle -> pth -> pt +执行以下指令克隆PaddleOCR2Pytorch项目 +```shell +git clone https://gitcode.com/gh_mirrors/pa/PaddleOCR2Pytorch.git +``` + +之后执行以下指令进入PaddleOCR2Pytorch目录 +```shell +cd PaddleOCR2Pytorch +``` +进入目录以后,可以看到目录结构如下: + +![](./images/4.png) + +找到"./pytorchocr/base_ocr_v20.py"文件,对其中save_pytorch_weights函数修改 +![](./images/5.png) + +![](./images/6.png) + +找到save_pytorch_weights函数,在save_pytorch_weights函数中添加如下代码: +```python +"转torchsprit" +input_size = torch.randn(1,3,48, 320) +mod = torch.jit.trace(self.net, input_size) +torch.jit.save(mod, "ch_PP-OCRv3_rec_infer.pt") +``` +修改以后的save_pytorch_weights函数如下: +![](./images/7.png) + +在终端运行以下命令,将PPOCRv3的文字识别模型由paddle格式转为pytorch格式: +```shell +python converter/ch_ppocr_v3_rec_converter.py --src_model_path "./model/ch_PP-OCRv3_rec_train/best_accuracy" +``` +这里需要注意的是默认加载的是训练模型best_accuracy,大家可以去PaddleOCR官网下载 [PPOCRv3文字识别模型的训练模型](https://github.com/PaddlePaddle/PaddleOCR/blob/main/docs/version2.x/model/index.md) + +![](./images/8.png) +运行完以上命令后,会在PaddleOCR2Pytorch目录中生成一个.pth模型和一个.pt模型。 + +![](./images/9.png) + +## 3. pt -> pnnx -> ncnn +将.pt格式转换为ncnn格式需要用到第三方库pnnx,大家可以先去[pnnx官网](https://github.com/pnnx/pnnx/releases)下载安装包,我这边下载的版本是20240226的,大家可以根据自己的需求下载。 + +![](./images/10.png) + +将下载后的安装包放到PaddleOCR2Pytorch目录下,然后解压。解压以后会在PaddleOCR2Pytorch目录下生成一个pnnx-20240226-linux文件夹,里面包含一个pnnx可执行文件。 + +![](./images/11.png) + +之后在终端执行以下命令来生成ncnn模型文件(这里应用了2个inputshape是为了实现动态输入,H维上的值是任意设置的)。 +```shell +./pnnx-20240226-linux/pnnx ch_PP-OCRv3_rec_infer.pt inputshape=[1,3,48,320] inputshape2=[1,3,48,480] +``` +运行结束以后会在PaddleOCR2ncnn目录下生成ncnn格式的文件。 + +![](./images/12.png) + +到这里我们就实现了将PPOCRv3文字识别模型转ncnn的全部流程,并且对转换以后的模型不需要做任何修改就可以进行推理。 + +## 4. NCNN部署推理 +将以上生成的ncnn下载到本地,具体的ncnn部署demo可以查看 [PPOCRv3识别模型部署](./Cpp_example/D11_PPOCRv3/README.md) diff --git a/Cpp_example/D11_PPOCRv3/ppocrv3.cc b/Cpp_example/D11_PPOCRv3/ppocrv3.cc new file mode 100644 index 0000000000000000000000000000000000000000..c47c2ff3fbd8dc01d628deea845b83408a55d60b --- /dev/null +++ b/Cpp_example/D11_PPOCRv3/ppocrv3.cc @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace std::chrono; + +// OCR配置参数 +const cv::Size OCR_INPUT_SIZE(320, 48); + +const bool USE_SPACE_CHAR = true; +const float MEAN_VALS[3] = {127.5f, 127.5f, 127.5f}; +const float NORM_VALS[3] = {1.0f/127.5f, 1.0f/127.5f, 1.0f/127.5f}; + +ncnn::Net ocr_net; + + +vector char_list; // 从字典加载的字符列表 + +// 加载字符字典 +vector load_character_dict(const string& path, bool use_space_char) { + vector temp_list; + ifstream file(path); + if (!file.is_open()) { + cerr << "Failed to open dictionary file: " << path << endl; + return {}; + } + + string line; + while (getline(file, line)) { + line.erase(remove(line.begin(), line.end(), '\r'), line.end()); + line.erase(remove(line.begin(), line.end(), '\n'), line.end()); + temp_list.push_back(line); + } + + if (use_space_char) { + temp_list.push_back(" "); + } + + vector char_list{"blank"}; + char_list.insert(char_list.end(), temp_list.begin(), temp_list.end()); + return char_list; +} + +// CTC解码 +string decode_ctc(const ncnn::Mat& out) { + vector indices; + const int num_timesteps = out.h; + const int num_classes = out.w; + + for (int t = 0; t < num_timesteps; ++t) { + const float* prob = out.row(t); + int max_idx = 0; + float max_prob = prob[0]; + + for (int c = 0; c < num_classes; ++c) { + if (prob[c] > max_prob) { + max_idx = c; + max_prob = prob[c]; + } + } + indices.push_back(max_idx); + } + + string result; + int prev_idx = -1; + + for (int idx : indices) { + if (idx == 0) { + prev_idx = -1; + continue; + } + if (idx != prev_idx) { + if (idx < char_list.size()) { + result += char_list[idx]; + } + prev_idx = idx; + } + } + + return result; +} + + +// 初始化OCR模型 +bool InitOCRModel(const string& param_path, const string& model_path, const string& dict_path) { + if (!ocr_net.load_param(param_path.c_str()) && !ocr_net.load_model(model_path.c_str())) { + char_list = load_character_dict(dict_path, USE_SPACE_CHAR); + return !char_list.empty(); + } + return false; +} + +// 文字识别 +string RecognizePlate(cv::Mat plate_img) { + // 图像预处理 + cv::resize(plate_img, plate_img, OCR_INPUT_SIZE); + ncnn::Mat in = ncnn::Mat::from_pixels(plate_img.data, + ncnn::Mat::PIXEL_BGR, + plate_img.cols, + plate_img.rows); + // PP-OCR风格归一化 + in.substract_mean_normalize(MEAN_VALS, NORM_VALS); + + // 模型推理 + ncnn::Extractor ex = ocr_net.create_extractor(); + ex.input("in0", in); + + ncnn::Mat out; + ex.extract("out0", out); + + // CTC解码 + string license = decode_ctc(out); + return license; +} + +int main(int argc, char** argv) { + // 参数验证 + if (argc != 5) { + cerr << "Usage: " << argv[0] + << " [image_path]\n" + << "Example:\n" + << " Realtime: " << argv[0] << " ocr.param ocr.bin ppocr_keys_v1.txt\n" + << " Image: " << argv[0] << " ocr.param ocr.bin ppocr_keys_v1.txt test.jpg\n"; + return 1; + } + // 初始化OCR模型和字典 + if (!InitOCRModel(argv[1], argv[2], argv[3])) { + cerr << "Failed to initialize OCR system" << endl; + return 1; + } + + // 图片处理模式 + cv::Mat image = cv::imread(argv[4]); + if (image.empty()) { + cerr << "Failed to read image: " << argv[4] << endl; + return 1; + } + + auto ocr_start = std::chrono::high_resolution_clock::now(); + string result = RecognizePlate(image); + auto ocr_end = std::chrono::high_resolution_clock::now(); + std::cout << "OCR: " << std::chrono::duration(ocr_end - ocr_start).count() << "s\n"; + + cout << " 识别结果: " << result << endl; + cv::waitKey(0); +} \ No newline at end of file