typora-copy-images-to: upload



部署环境, windows10,camke 4.2 freetype 2.14.0 ,harbuzz11.0 文档未写完,推荐onnx部署简单不用编译

qt部署和onnx模型转化直接看第7章

1.安装

1.1 paddle框架安装

conda 创建一个python =3.10的基础环境

根据自己的环境安装paddle框架链接

image-20251105092542387

安装完成后进入环境,输入

python

import paddle

paddle.utils.run_check()

如果看到以下输出则没有问题,输入exit()退出

image-20251105103829851

1.2 paddleOCR安装

pip install paddleocr出现以下说明安装成功,不用指定版本,指定了还出错

image-20251105104449485

2.制作数据集

2.1工具下载

下载数据集制作工具下载地址

解压后,进入环境,cd到解压目录

pip3 install "paddlex[ocr]" -i https://pypi.tuna.tsinghua.edu.cn/simple/安装必要库

启动插件python ./PPOCRLabel.py,此时会一直下载模型,然后就会打开软件

image-20251105131825720

2.2 数据集制作

打开图片文件夹,矩形框标注,可以更改识别结果,标注完成点击确定

image-20251126153009710

制作完成后,点击文件→导出标记结果,点击文件→导出识别结果,得到四个文件

image-20251105160843782

2.3 划分训练集和测试集

打开conda终端进入环境,cd进入PPOCRLabel文件夹,把图片文件夹复制到data文件夹下,执行划分命令

python gen_ocr_train_val_test.py --trainValTestRatio 6:2:2 --datasetRootPath ./data/Tire_data

–trainValTestRatio 6:2:2 #训练集、验证集和测试集的比例

–datasetRootPath #数据集路径

image-20251111104603659

运行完成后再代码的上一级文件夹就会有train_data,里边就是划分好的数据集

image-20251111105011221

3.训练模型

3.1 代码下载

克隆也行,下载也行,下载地址

解压后cd进入,安装必要库pip install -r requirements.txt

image-20251111105735037

3.2 预训练模型下载

https://www.paddleocr.ai/main/version3.x/module_usage/text_detection.html#411下载文本检测和识别的预训练模型

image-20251111110722645

3.3 文本检测训练

windows 端训练 终端输入指令

python tools/train.py -c configs/det/PP-OCRv5/PP-OCRv5_server_det.yml \ # 配置文件路径
    -o Global.pretrained_model=./model/PP-OCRv5_server_det_pretrained.pdparams \ #模型路径
    Train.dataset.data_dir=../train_data/det \							# 数据集路径
    Train.dataset.label_file_list='[../train_data/det/train.txt]' \		# train.txt路径
    Eval.dataset.data_dir=../train_data/det \							# 数据集路径
    Eval.dataset.label_file_list='[../train_data/det/val.txt]'			# val.txt路径

python tools/train.py -c configs/det/PP-OCRv5/PP-OCRv5_server_det.yml -o Global.pretrained_model=./model/PP-OCRv5_server_det_pretrained.pdparams Train.dataset.data_dir=../train_data/det Train.dataset.label_file_list=[../train_data/det/train.txt] Eval.dataset.data_dir=../train_data/det Eval.dataset.label_file_list=[../train_data/det/val.txt]

3.4 文本识别模型训练

同理

4.验证模型

4.1 验证文本检测模型

进入终端,环境,cd到目录

python3 tools/eval.py -c configs/det/PP-OCRv5/PP-OCRv5_server_det.yml \
    -o Global.pretrained_model=output/PP-OCRv5_server_det/latest.pdparams \ # 保存的模型地址
    Eval.dataset.data_dir=./ocr_det_dataset_examples \						# 数据集路径
    Eval.dataset.label_file_list='[./ocr_det_dataset_examples/val.txt]'		#valtext 路径

python tools/eval.py -c configs/det/PP-OCRv5/PP-OCRv5_server_det.yml -o Global.pretrained_model=output/PP-OCRv5_server_det/latest.pdparams Eval.dataset.data_dir=../train_data/det Eval.dataset.label_file_list=[../train_data/det/val.txt]

4.3 模型导出

在训练的output找到你最优的模型

python tools/export_model.py -c configs/det/PP-OCRv5/PP-OCRv5_server_det.yml -o \   # 配置文件
    Global.pretrained_model=output/PP-OCRv5_server_det/latest.pdparams \			# 模型参数路径
    Global.save_inference_dir="./PP-OCRv5_server_det_infer/"						#导出地址

python tools/export_model.py -c configs/det/PP-OCRv5/PP-OCRv5_server_det.yml -o Global.pretrained_model=output/PP-OCRv5_server_det/latest.pdparams Global.save_inference_dir="./PP-OCRv5_server_det_infer/"

会得到这三个文件,然后就可以推理

image-20251111140745172

4.2 推理可视化

python tools\infer\predict_det.py \
    --image_dir "../train_data/det/test/" \				# 预测图像路径
    --det_model_dir "./output/PP-OCRv5_server_det/" \	# 模型路径
    --use_gpu true

python tools\infer\predict_det.py --image_dir "../train_data/det/test/" --det_model_dir "./PP-OCRv5_server_det_infer/" --use_gpu true

在这个路径下就能看到识别后的可视化结果

image-20251111141053745

4.3 文本识别模型

同理

4.4 检测和识别一同验证可视化

python tools/infer/predict_system.py  
    --det_model_dir=./ch_PP-OCRv2_det_infer/ \         # 检测模型目录
    --rec_model_dir=./ch_PP-OCRv2_rec_infer/  \        # 识别模型目录
    --image_dir=./datasets/img_dir/ \                  # 测试图片目录
    --draw_img_save_dir=./ch_PP-OCRv2_results/ \       # 可视化结果保存目录
    --is_visualize=True

5 部署c++

5.1 编译Opencv

下载cmake安装下载链接

image-20251112175052596

下载opencv原码和对应的第三方库下载 opencv-4.7.0 下载 opencv_contrib-4.7.0(版本可以自己选择我这边是官方教程的版本)

下载pkg-config解压 pkg-config 后添加其 bin 目录到系统 PATH 环境变量。

5.2.1 freetype2.14.0 编译

下载freetype2下载解压后打开cmake,我选择2.14版本,选择路径和编译导出路径,点击config,选择你的vs版本,x64,最后点击Generate,完成后,点击 Open Project 按钮,打开 VS ,编译。 VS里ALL_BUILD, INSTALL. 会在构建文件夹的 install 目录下生成所需的 include 和 lib 文件。

image-20251112105116472

image-20251112102158188

debug和release都运行一下install生成,然后将 freetype 的install路径添加至系统环境变量,重启电脑.

5.3.1 harfbuzz11.0编译

下载harfbuzz

image-20251112104404752

设置好上面两项后,再次点击 Configure 按钮,选择 Advanced Options ,填写 freetype 安装路径, 再次点击Configure 按钮,最后点击Generate,完成后,点击 Open Project 按钮,打开 VS ,install 生成。

image-20251112111230524

image-20251112111536117

然后将 harbuzz 的install路径添加至系统环境变量,重启电脑.

5.1.3 opencv编译

修改 opencv_contrib-4.7.0 下的 modules/freetype/CMakeLists.txt

set(the_description "FreeType module. It enables to draw strings with outlines and mono-bitmaps/gray-bitmaps.")

find_package(Freetype REQUIRED)

# find_package(HarfBuzz) is not included in cmake
set(HARFBUZZ_DIR "$ENV{HARFBUZZ_DIR}" CACHE PATH "HarfBuzz directory")
find_path(HARFBUZZ_INCLUDE_DIRS
    NAMES hb-ft.h PATH_SUFFIXES harfbuzz
    HINTS ${HARFBUZZ_DIR}/include)
find_library(HARFBUZZ_LIBRARIES
    NAMES harfbuzz
    HINTS ${HARFBUZZ_DIR}/lib)
find_package_handle_standard_args(HARFBUZZ
    DEFAULT_MSG HARFBUZZ_LIBRARIES HARFBUZZ_INCLUDE_DIRS)

if(NOT FREETYPE_FOUND)
  message(STATUS "freetype2:   NO")
else()
  message(STATUS "freetype2:   YES")
endif()

if(NOT HARFBUZZ_FOUND)
  message(STATUS "harfbuzz:   NO")
else()
  message(STATUS "harfbuzz:   YES")
endif()

if(FREETYPE_FOUND AND HARFBUZZ_FOUND)
  ocv_define_module(freetype opencv_core opencv_imgproc PRIVATE_REQUIRED ${FREETYPE_LIBRARIES} ${HARFBUZZ_LIBRARIES} WRAP python)
  ocv_include_directories(${FREETYPE_INCLUDE_DIRS} ${HARFBUZZ_INCLUDE_DIRS})
else()
  ocv_module_disable(freetype)
endif()
  • 设置 OPENCV_EXTRA_MODULES_PATH 项,填入 opencv-contrib-4.7.0 的目录下的 modules 目录。
  • 勾选 WITH_FREETYPE 项,必须先编译 freetype 和 harfbuzz。再次点击config

image-20251112131347096

确定freetype的路径正确

填入harfbuzz路径

image-20251112132054144

打开qt,world,opengl,nonfree

image-20251112132305239

取消勾选test,wechat_qrcode,java,js,python,cvv的所有勾选

完成后,再次在 Cmake 界面,点击 configure, 确定没报错后,点击 Generate,最后点击 Open Project,打开 Visual studio,将 Debug 切换为 Release, 找到 INSTALL 右键 Build。没有报错即为编译成功

image-20251112163756392

opencv部分库下载不下,看问题4,5

5.2 编译Paddle Inference

参考官方教材,直接安装编译包就行,省去编译过程官方教程

image-20251111151456291

5.3 编译程序

source code填入PaddleOcr的deploy的cpp_infer路径

build填入任意地址即可,存放编译的文件,完成后点击configure

image-20251112175215438

VS的版本根据你安装的来,平台选择x64

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第一次点击 Configure 报错是正常的,在后续弹出的编译选项中,添加 OpenCV 的安装路径和 Paddle Inference 预测库路径。

image-20251111165958478

  • OPENCV_DIR:填写 OpenCV 安装路径。
  • OpenCV_DIR:同 OPENCV_DIR。
  • PADDLE_LIB:Paddle Inference 预测库路径。

image-20251111170249868

两个库编译完,opencv编译完,后边应该没啥问题,直接参考官方后续的步骤官方步骤

image-20251113090229057

6.模型转onnx,部署更简单(新版本目前有bug,不用看了)

进入conda环境,执行如下命令,通过 PaddleX CLI 安装 PaddleX 的 Paddle2ONNX 插件:

# Windows 用户需使用以下命令安装 paddlepaddle dev版本
pip install --pre paddlepaddle -i https://www.paddlepaddle.org.cn/packages/nightly/cpu/
paddlex --install paddle2onnx

ERROR: Could not find a version that satisfies the requirement onnx<=1.17.0,>=1.16 (from paddle2onnx) (from versions: none)
ERROR: No matching distribution found for onnx<=1.17.0,>=1.16
Installation failed

遇到报错onnx版本不对,直接卸载安装

pip uninstall paddle2onnx onnx onnxruntime -y

pip install paddle2onnx onnx==1.17.0 onnxruntime==1.17.0 -i https://pypi.tuna.tsinghua.edu.cn/simple

执行如下命令完成模型转换:需要使用导出后的推理模型,训练的不可以

paddlex \
    --paddle2onnx \  # 使用paddle2onnx功能
    --paddle_model_dir /your/paddle_model/dir \  # 指定 Paddle 模型所在的目录
    --onnx_model_dir /your/onnx_model/output/dir \  # 指定转换后 ONNX 模型的输出目录
    --opset_version 7  # 指定要使用的 ONNX opset 版本

paddlex --paddle2onnx --paddle_model_dir ./PP-OCRv5_server_det_infer --onnx_model_dir ./output/PP-OCRv5_server_det --opset_version 11

7 paddle3.1转换onnx模型,QT部署

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.1 文本检测模型转onnx

卸载pip uninstall paddlepaddle-gpu

安装python -m pip install paddlepaddle-gpu==3.1.1 -i https://www.paddlepaddle.org.cn/packages/stable/cu118/

pip install paddleocr==3.1.0 -i https://pypi.tuna.tsinghua.edu.cn/simple

如果安装ocr过程中报错,某些库冲突找不到,就单独pip先把那些库安装了

pip install paddle2onnx onnx==1.17.0 onnxruntime==1.18.0 -i https://pypi.tuna.tsinghua.edu.cn/simple

下载3.1版本的PaddleOCR代码,替换原来的PaddleOCR-main下载PaddleOCR3.1

完成后依旧是用之前的代码训练一个小的模型先试试能不能转换

python tools/train.py -c configs/det/PP-OCRv5/PP-OCRv5_mobile_det.yml -o Global.pretrained_model=./model/PP-OCRv5_mobile_det_pretrained.pdparams Train.dataset.data_dir=../train_data/det Train.dataset.label_file_list=[../train_data/det/train.txt] Eval.dataset.data_dir=../train_data/det Eval.dataset.label_file_list=[../train_data/det/val.txt]

导出可视化验证模型,没有问题

python tools/export_model.py -c configs/det/PP-OCRv5/PP-OCRv5_mobile_det.yml -o Global.pretrained_model=output/PP-OCRv5_mobile_det/latest.pdparams Global.save_inference_dir="./PP-OCRv5_mobile_det_infer/
python tools\infer\predict_det.py --image_dir "../train_data/det/test/" --det_model_dir "./PP-OCRv5_mobile_det_infer/" --use_gpu true

导出onnx模型

paddlex --paddle2onnx --paddle_model_dir ./PP-OCRv5_mobile_det_infer --onnx_model_dir ./output/PP-OCRv5_mobile_det --opset_version 13

报错numpy不兼容,安装旧版本numpypip install "numpy<2" -i https://pypi.tuna.tsinghua.edu.cn/simple

成功导出模型,并且模型大小也是正确的,使用以下代码验证onnx模型的有效性,结果正确

import os
import cv2
import numpy as np
import onnxruntime
import pyclipper

# ================= 配置 =================
MODEL_PATH = "./trainModel/inference.onnx"
IMG_PATH = "./tools/1.jpg"
SAVE_PATH = "./result.jpg"

INPUT_SIZE = (640, 640)
BINARY_THRESH_MIN = 0.2  # 最小阈值
BINARY_THRESH_MAX = 0.6  # 最大阈值
MIN_AREA = 100           # 最小文字区域面积
UNCLIP_RATIO = 1.5       # 膨胀比例
# =======================================

# ---------- unclip 膨胀 ----------
def unclip(box, unclip_ratio=1.5):
    area = cv2.contourArea(box)
    if area <= 0:
        return box
    perimeter = cv2.arcLength(box, True)
    distance = area * unclip_ratio / max(perimeter, 1e-6)
    offset = pyclipper.PyclipperOffset()
    pts = box.astype(np.int32).tolist()
    offset.AddPath(pts, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)
    expanded = offset.Execute(distance)
    if len(expanded) == 0:
        return box
    expanded = np.array(expanded[0]).reshape(-1, 2)
    return expanded

# ---------- ONNX 推理 ----------
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
session = onnxruntime.InferenceSession(MODEL_PATH, providers=providers)
input_name = session.get_inputs()[0].name

# ---------- 读取图片 ----------
img = cv2.imread(IMG_PATH)
if img is None:
    raise FileNotFoundError(f"图片不存在: {IMG_PATH}")
orig_img = img.copy()
orig_h, orig_w = img.shape[:2]

# ---------- 预处理 ----------
img_resized = cv2.resize(img, INPUT_SIZE)
img_norm = img_resized.astype(np.float32) / 255.0
img_norm = img_norm.transpose(2,0,1)  # HWC -> CHW
img_input = np.expand_dims(img_norm, axis=0).astype(np.float32)

# ---------- 推理 ----------
pred_map = session.run(None, {input_name: img_input})[0]
pred_map = 1 / (1 + np.exp(-pred_map))  # sigmoid
pred_map_resized = cv2.resize(pred_map[0,0], (orig_w, orig_h))

# ---------- 自适应二值化 ----------
mean_val = pred_map_resized.mean()
binary_thresh = np.clip(mean_val*1.5, BINARY_THRESH_MIN, BINARY_THRESH_MAX)
binary_map = (pred_map_resized > binary_thresh).astype(np.uint8)

# ---------- 找轮廓 + unclip ----------
contours, _ = cv2.findContours(binary_map, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
boxes = []

for cnt in contours:
    if cv2.contourArea(cnt) < MIN_AREA:
        continue
    epsilon = 0.01 * cv2.arcLength(cnt, True)
    approx = cv2.approxPolyDP(cnt, epsilon, True)
    expanded = unclip(approx[:,0,:], UNCLIP_RATIO)
    if expanded.shape[0] < 4:
        continue
    expanded[:,0] = np.clip(expanded[:,0], 0, orig_w-1)
    expanded[:,1] = np.clip(expanded[:,1], 0, orig_h-1)
    boxes.append(expanded)

# ---------- 绘制矩形 ----------
for box in boxes:
    x, y, w, h = cv2.boundingRect(box)  # 最小外接矩形
    cv2.rectangle(orig_img, (x,y), (x+w, y+h), (0,255,0), 2)

# ---------- 保存 ----------
cv2.imwrite(SAVE_PATH, orig_img)
print(f"检测到文本框数量: {len(boxes)}")
print(f"结果已保存到: {os.path.abspath(SAVE_PATH)}")

cv2.imshow("Detected Text Boxes", orig_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

image-20251114130414266

7.2 QT c++版本部署文本检测模型

环境配置参考qt运行onnxRuntime模型,加载模型生成环境异常

ONNX Runtime Exception: Load model from ./inference.onnx failed:D:\a_work\1\s\onnxruntime\core\graph\model.cc:111 onnxruntime::Model::Model Unknown model file format version.

我的c++的onnxRuntime是1.8.1版本的,导出模型的版本是1.18.0,估计是太老了,升级一下,直接统一换到1.18.0,升级后程序报错,找到错误删除constexpr

image-20251113161434099

image-20251113161502027

使用以下代码完成文本检测和显示,可以看到结果还不错,检测时间cpu170ms, gpu25ms

#pragma once
#include <opencv2/opencv.hpp>
// ONNX Runtime C++ API
#define ORT_DISABLE_FP16
#include <onnxruntime_cxx_api.h>   // C++ 封装接口
#include <chrono>   // 用于高精度计时
#include <iostream>
#include <QObject>


// 辅助函数:简单的 Sigmoid
static float sigmoid(float x) {
	return 1.0f / (1.0f + std::exp(-x));
}

void opencvManger::getFontDect(const std::string& IMG_PATH)
{
	

	const std::string SAVE_PATH = "./result.jpg";
	const std::wstring MODEL_PATH = L"./inference.onnx"; // 确保路径正确

	// 与 Python 配置保持一致
	const int INPUT_W = 640;
	const int INPUT_H = 640;
	const float MIN_AREA = 300.0f;
	const float BINARY_THRESH_MIN = 0.2f;
	const float BINARY_THRESH_MAX = 0.6f;

	// C++模拟 Unclip 的膨胀系数
	// Python 是多边形偏移,C++ 这里用形态学膨胀模拟,数值可能需要微调
	// 如果 Python ratio 是 1.5,这里 kernel size 设为 3~5 左右通常效果接近
	const int UNCLIP_DILATE_ITERATIONS = 15;

	try {
		// ==================== 1. 初始化 ORT ====================
		Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "DBNet");
		Ort::SessionOptions session_options;
		session_options.SetIntraOpNumThreads(4);
		session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_BASIC);
		try {
			OrtSessionOptionsAppendExecutionProvider_CUDA(session_options, 0);
		}
		catch (...) {
			std::cout << "⚠️ CUDA not available, fallback to CPU" << std::endl;
		}

		Ort::Session session(env, MODEL_PATH.c_str(), session_options);
		Ort::AllocatorWithDefaultOptions allocator;

		

		// 获取输入输出节点名
		auto input_name_ptr = session.GetInputNameAllocated(0, allocator);
		auto output_name_ptr = session.GetOutputNameAllocated(0, allocator);
		const char* input_names[] = { input_name_ptr.get() };
		const char* output_names[] = { output_name_ptr.get() };

		// ==================== 2. 图片读取与预处理 (对齐 Python) ====================
		cv::Mat image = cv::imread(IMG_PATH); // BGR 格式
		if (image.empty()) {
			std::cerr << "Error: Image not found." << std::endl;
			return;
		}
		int orig_h = image.rows;
		int orig_w = image.cols;

		// Resize
		cv::Mat resized_img;
		cv::resize(image, resized_img, cv::Size(INPUT_W, INPUT_H));

		// 归一化 (0-1) & HWC -> CHW
		// 注意:Python代码并没有减均值除方差,只是 / 255.0,且保持 BGR
		std::vector<float> input_tensor_values;
		input_tensor_values.reserve(INPUT_W * INPUT_H * 3);

		// 遍历顺序:Channel -> Height -> Width (CHW)
		// OpenCV 默认是 BGR,分别提取 B, G, R 通道
		for (int c = 0; c < 3; c++) {
			for (int h = 0; h < INPUT_H; h++) {
				for (int w = 0; w < INPUT_W; w++) {
					// at<cv::Vec3b> 返回的是 BGR
					float pixel = resized_img.at<cv::Vec3b>(h, w)[c];
					input_tensor_values.push_back(pixel / 255.0f);
				}
			}
		}

		std::array<int64_t, 4> input_shape = { 1, 3, INPUT_H, INPUT_W };
		Ort::MemoryInfo memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
		Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
			memory_info, input_tensor_values.data(), input_tensor_values.size(), input_shape.data(), input_shape.size()
			);

		//计时
		//auto output_tensors = session.Run(Ort::RunOptions{ nullptr }, input_names, &input_tensor, 1, output_names, 1);
		auto start_e2e = std::chrono::high_resolution_clock::now();
		// ==================== 3. 推理 ====================
		auto output_tensors = session.Run(Ort::RunOptions{ nullptr }, input_names, &input_tensor, 1, output_names, 1);

		// ==================== 4. 后处理 ====================
		float* floatarr = output_tensors[0].GetTensorMutableData<float>();
		// DBNet 输出通常是 1x1xHxW
		cv::Mat pred_map(INPUT_H, INPUT_W, CV_32F, floatarr);

		// 4.1 Sigmoid 处理
		for (int i = 0; i < pred_map.rows * pred_map.cols; ++i) {
			pred_map.at<float>(i) = sigmoid(pred_map.at<float>(i));
		}

		// 4.2 Resize 回原图尺寸
		cv::Mat pred_resized;
		cv::resize(pred_map, pred_resized, cv::Size(orig_w, orig_h));

		// 4.3 自适应阈值 (对齐 Python 逻辑)
		// Python: mean_val = pred_map_resized.mean()
		// Python: binary_thresh = clip(mean_val * 1.5, 0.2, 0.6)
		cv::Scalar mean_scalar = cv::mean(pred_resized);
		float mean_val = static_cast<float>(mean_scalar[0]);
		float binary_thresh = std::max(BINARY_THRESH_MIN, std::min(BINARY_THRESH_MAX, mean_val * 1.5f));

		// 二值化
		cv::Mat binary_map;
		// 注意:OpenCV threshold 需要 0-255 的输入或者 CV_32F 对比
		// 这里 pred_resized 是 CV_32F (0.0-1.0),binary_thresh 也是 float
		cv::threshold(pred_resized, binary_map, binary_thresh, 255, cv::THRESH_BINARY);
		binary_map.convertTo(binary_map, CV_8U);

		// 4.4 Unclip 模拟 (形态学膨胀)
		// Python 使用多边形偏移,C++ 简单版使用 mask 膨胀
		cv::Mat dilated_map;
		cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
		cv::dilate(binary_map, dilated_map, kernel, cv::Point(-1, -1), UNCLIP_DILATE_ITERATIONS);

		// 4.5 查找轮廓
		std::vector<std::vector<cv::Point>> contours;
		cv::findContours(dilated_map, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);

		// ==================== 4.6 查找最大轮廓 ====================
		double max_area = 0.0;
		std::vector<cv::Point> max_contour; // 用于存储最大轮廓

		for (const auto& cnt : contours)
		{
			double area = cv::contourArea(cnt);

			//过滤小于最小面积的轮廓
			if (area < MIN_AREA) continue;

			//如果当前轮廓比已知的最大面积还大
			if (area > max_area)
			{
				max_area = area;
				max_contour = cnt;	 //最大轮廓
			}
		}

		// ==================== 4.7 绘制最大轮廓的最小外接矩形 ====================
		int box_count = 0;
		if (!max_contour.empty()) //有一个有效的轮廓
		{
			//使用minAreaRect,会找到包裹面积的旋转矩形
			cv::RotatedRect rotated_rect = cv::minAreaRect(max_contour);

			//获取旋转矩形的4个顶点
			cv::Point2f vertices[4];
			rotated_rect.points(vertices);

			//准备绘制多边形
			std:vector<cv::Point> box_points;
			for (int i = 0; i < 4; i++)
			{
				box_points.push_back(vertices[i]);//自动从Point2f转为Point
			}

			//绘制多边形(使用polylines而不是rectangle)
			cv::polylines(image, box_points, true, cv::Scalar(0, 255, 0), 2);
		}
		auto end_e2e = std::chrono::high_resolution_clock::now();
		std::chrono::duration<double, std::milli> e2e_ms = end_e2e - start_e2e;
		// ==================== 5. 保存结果 ====================
		cv::imwrite(SAVE_PATH, image);
		
		std::cout << "time" << e2e_ms.count() << "ms" << endl;
		std::cout << "Result saved to " << SAVE_PATH << std::endl;

	}
	catch (const Ort::Exception& e) {
		std::cerr << "ONNX Runtime Error: " << e.what() << std::endl;
	}
	catch (const std::exception& e) {
		std::cerr << "Error: " << e.what() << std::endl;
	}
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.3 文本识别模型转onnx

训练模型

python tools/train.py -c configs/rec/PP-OCRv5/PP-OCRv5_mobile_rec.yml -o Global.pretrained_model=./model/PP-OCRv5_mobile_rec_pretrained.pdparams Train.dataset.data_dir=../train_data/rec Train.dataset.label_file_list=[../train_data/rec/train.txt] Eval.dataset.data_dir=../train_data/rec Eval.dataset.label_file_list=[../train_data/rec/val.txt]

生成推理模型

python tools/export_model.py -c configs/rec/PP-OCRv5/PP-OCRv5_mobile_rec.yml -o Global.pretrained_model=output/PP-OCRv5_mobile_rec/latest.pdparams Global.save_inference_dir="./PP-OCRv5_mobile_rec_infer/"

可视化结果验证,终端查看结果

python tools\infer\predict_rec.py --image_dir "../train_data/rec/test/" --rec_model_dir "./PP-OCRv5_mobile_rec_infer/" --use_gpu true

导出onnx模型

paddlex --paddle2onnx --paddle_model_dir ./PP-OCRv5_mobile_rec_infer --onnx_model_dir ./output/PP-OCRv5_mobile_rec --opset_version 13

文本识别同时会在推理文件夹导出一个字典文件,我们到时候需要使用它

image-20251121141458349

查看配置文件,输入尺寸统一为[3, 48, 320],使用python代码验证onnx模型效果,检测正确

import cv2
import numpy as np
import onnxruntime
import os

# ================= 配置 =================
MODEL_PATH = "./tools/rec_inference.onnx"   # ONNX 模型路径,改成你的路径
DICT_PATH  = "./tools/ppocr_keys.txt"       # 字典路径(每行一个字符),改成你的路径
IMG_PATH   = "./tools/2.jpg"                # 待识别图片路径,改成你的路径

# PaddleOCR v5_mobile_rec 的默认输入尺寸
IMG_H = 48
IMG_W = 320
# =========================================

# ---------- 加载字典 ----------
def load_dict(dict_path):
    """
    加载字符字典文件,返回包含 blank 在内的字符列表。
    PaddleOCR 的 CTC 解码中通常把 index=0 作为 blank。
    因此我们把字典组织为:["blank", char1, char2, ...]
    """
    with open(dict_path, 'r', encoding="utf-8") as f:
        keys = [line.strip() for line in f.readlines() if line.strip() != ""]
    # 在最前面加一个占位 'blank',对应类别 0
    dict_character = ["blank"] + keys
    return dict_character

dict_character = load_dict(DICT_PATH)


# ---------- CTC 解码(Best path decoding) ----------
def ctc_decode(preds):
    """
    对网络输出做简单的 best-path CTC 解码(去掉 blank,合并重复)
    preds: numpy array, shape = [seq_len, num_classes] (即单条样本)
    返回:解码后的字符串(unicode)
    说明:
      - preds[t] 是长度为 num_classes 的向量(通常未经过 softmax 或 已经是概率)
      - 我们使用 argmax 取每个时间步最可能的类别索引
      - index 0 被视为 blank,跳过不输出
      - 合并连续重复的字符(重复字符只输出一次)
    """
    # 每个时间步取得分最大的类别索引
    preds_idx = preds.argmax(axis=1)  # shape: [seq_len]
    prev = -1
    result = ""

    for idx in preds_idx:
        # 跳过 blank(0),并且合并重复
        if idx != prev and idx != 0:
            # 直接用 dict_character[idx]:因为我们在字典最前面加了 'blank' -> index 对齐
            result += dict_character[idx-1]
        prev = idx
    return result


# ---------- 预处理(复现 PaddleOCR RecResizeImg 行为) ----------
def preprocess(img, imgH=IMG_H, imgW=IMG_W):
    """
    输入:BGR 图像(H, W, 3)
    步骤:
      1. 计算等比例缩放到高度 imgH,宽度按比例计算,限制不超过 imgW
      2. 将缩放结果放在左上角,右侧用 255 填充到宽度 imgW(Paddle 的 padding 策略)
      3. 转为 float -> 归一化 (img/255 -> (img-0.5)/0.5)
      4. HWC -> CHW, 增加 batch 维度,返回 float32 张量,shape = [1,3,imgH,imgW]
    """
    # 原始尺寸
    h, w = img.shape[:2]
    ratio = w / float(h)
    # 目标宽度 = min(int(imgH * ratio), imgW)
    target_w = min(int(imgH * ratio), imgW)

    # 等比例 resize 到 (target_w, imgH)
    resized = cv2.resize(img, (target_w, imgH))

    # 创建一个全白图(Paddle 用 255 填充)
    padded = np.ones((imgH, imgW, 3), dtype=np.float32) * 255.0
    # 将缩放后的图像放在左上角
    padded[:, :target_w, :] = resized

    # 转 float 并做归一化:/255 -> (x - 0.5) / 0.5
    padded = padded.astype(np.float32) / 255.0
    padded = (padded - 0.5) / 0.5

    # HWC -> CHW
    padded = padded.transpose(2, 0, 1)

    # 增加 batch 维度
    input_tensor = np.expand_dims(padded, axis=0).astype(np.float32)

    # 调试信息(确认输入 shape)
    # 期望: (1, 3, IMG_H, IMG_W)
    print("Preprocess: input tensor shape =", input_tensor.shape)
    return input_tensor


# ---------- 初始化 ONNX 运行时 ----------
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
session = onnxruntime.InferenceSession(MODEL_PATH, providers=providers)

# 注意:有的模型输入名不是 "x",可以打印 session.get_inputs() 确认
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
print("ONNX model input name:", input_name, "output name:", output_name)


# ---------- 主流程 ----------
if __name__ == "__main__":
    # 读取图片(BGR)
    img = cv2.imread(IMG_PATH)
    if img is None:
        raise FileNotFoundError(f"图片不存在: {IMG_PATH}")

    # 预处理 -> 得到 shape 为 (1,3,IMG_H,IMG_W) 的张量
    img_input = preprocess(img, IMG_H, IMG_W)

    # 推理(ONNXRuntime)
    # 注意:确保传入的数据类型为 np.float32,shape 与模型期望相同
    out = session.run(None, {input_name: img_input})
    # 通常输出是 shape [1, seq_len, num_classes](但也有模型输出 [1, num_classes, seq_len] 的变体)
    pred = out[0]
    print("Raw model output shape:", pred.shape)

    # 如果输出是 [1, num_classes, seq_len],需要转置为 [1, seq_len, num_classes]
    if pred.ndim == 3 and pred.shape[1] == len(dict_character):
        # 很少见:输出为 [B, C, T] -> 转为 [B, T, C]
        pred = pred.transpose(0, 2, 1)
        print("Transposed model output to shape:", pred.shape)

    # 去掉 batch 维度 -> [seq_len, num_classes]
    pred_single = pred[0]

    #(可选)如果模型输出是 logits 而不是 softmax,ctc_decode 使用 argmax 也可行;
    # 如需更稳健可先做 softmax: probs = softmax(pred_single, axis=1)
    # 但 argmax 不受 softmax 单调性影响,故此处不必强制 softmax。

    # 解码
    result = ctc_decode(pred_single)
    print("识别结果:", result)

image-20251121160229382

7.4 QT C++部署文本识别模型

需要把opencv的HWC转换为CHW格式,并且把输出的一维tensor的变成二维的[0.1,0.2,0.3,0.4] -> [0.1,0.2],[0.3,0.4],方便计算最大概率得到class

//单张文本识别
vector<string> load_dict(const std::string& path)
{
	vector<string> dict;
	dict.push_back("blank");

	std::ifstream infile(path);
	string line;
	while (getline(infile, line))
	{
		if (!line.empty())
		{
			dict.push_back(line);
		}
	}
	return dict;
}


void opencvManger::getFontRec(const string & imgPath)
{
	const int IMG_H = 48;
	const int IMG_W = 320;
	const std::wstring strModelPath = L"./model/rec_inference.onnx";
	std::string dict_path = "./model/ppocr_keys.txt";

	// 1. 加载字典
	vector<string> dict = load_dict(dict_path);

	// 2.onnx初始化
	Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "Rec");
	Ort::SessionOptions session_options;
	session_options.SetIntraOpNumThreads(4);
	session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_BASIC);
	try
	{
		OrtSessionOptionsAppendExecutionProvider_CUDA(session_options, 0);
	}
	catch (...) {
		std::cout << "⚠️ CUDA not available, fallback to CPU" << std::endl;
	}

	Ort::Session session(env, strModelPath.c_str(), session_options);
	Ort::AllocatorWithDefaultOptions allocator;
	//获取节输入输出节点名称
	auto strInputName = session.GetInputNameAllocated(0, allocator);
	auto strOutputName = session.GetOutputNameAllocated(0, allocator);
	const char* cInputNames[] = { strInputName.get() };
	const char* cOutputNames[] = { strOutputName.get() };

	// 3. 读取图片(BGR,图像预处理
	cv::Mat img = cv::imread(imgPath);
	if (img.empty()) {
		cout << "图片读取失败!" << endl;
		return;
	}

	int h = img.rows;
	int w = img.cols;
	float ratio = w * 1.0f / h;
	int target_w = min(int(IMG_H * ratio), IMG_W);

	//resize,高度固定为48,宽度按比例
	cv::Mat resized;
	cv::resize(img, resized, cv::Size(target_w, IMG_H));

	//Paddling,初始化为255,宽度在扩充到320
	cv::Mat padded(IMG_H, IMG_W, CV_32FC3, cv::Scalar(255, 255, 255));
	resized.convertTo(resized, CV_32FC3);
	resized.copyTo(padded(cv::Rect(0, 0, target_w, IMG_H)));

	// Normalize
	padded /= 255.0f;
	padded = (padded - 0.5f) / 0.5f;

	//转换为tensor  HWC ->CHW  ,opencv img[H][W][C]
	std::vector<float> inputTensorValue;
	inputTensorValue.reserve(IMG_H * IMG_W * 3);
	int idx = 0;
	for (int c = 0; c < 3; c++)
	{
		for (int i = 0; i < IMG_H; i++)
		{
			for (int j = 0; j < IMG_W; j++)
			{
				inputTensorValue.push_back(padded.at<cv::Vec3f>(i, j)[c]);
			}
		}
	}

	// 4.模型推理   输入形状 [1,3,48,320]

	std::array<int64_t, 4> inputShape = { 1,3,IMG_H,IMG_W };
	size_t inputTensorSize = inputTensorValue.size();

	//创建ONNX输入
	Ort::MemoryInfo memoryInfo = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
	Ort::Value inputOrt = Ort::Value::CreateTensor<float>(memoryInfo, inputTensorValue.data(),inputTensorSize, inputShape.data(), inputShape.size());
	//推理
	auto outputTensor = session.Run(
		Ort::RunOptions{ nullptr },
		cInputNames,
		&inputOrt,
		1,
		cOutputNames,
		1
	);

	//5.解析输出: shape = [1,T,C]
	float* pOutput = outputTensor[0].GetTensorMutableData<float>();
	auto outShape = outputTensor[0].GetTensorTypeAndShapeInfo().GetShape();

	int T = outShape[1];
	int C = outShape[2];

	vector<vector<float>> preds(T, vector<float>(C));
	//把一维的变成二维的[0.1,0.2,0.3,0.4]  ->  [0.1,0.2],[0.3,0.4]
	for (int t = 0; t < T; t++)
	{
		for (int c = 0; c < C; c++)
		{
			preds[t][c] = pOutput[t*C + c];
		}
	}

	//6.CTC 解码
	string result;
	int prev = -1;
	for (int i = 0; i < preds.size(); i++)
	{
		int idx = max_element(preds[i].begin(), preds[i].end()) - preds[i].begin();
		
		if (idx != prev && idx != 0)
		{
			result += dict[idx];
		}
		prev = idx;
	}
	cout << result << endl;
}

7.5 文本检测与文本识别连同部署

整体代码如下

#pragma once
#include <opencv2/opencv.hpp>
// ONNX Runtime C++ API
#define ORT_DISABLE_FP16
#include <onnxruntime_cxx_api.h>   // C++ 封装接口
#include <chrono>   // 用于高精度计时
#include <iostream>
#include <QObject>
#include <vector>

using namespace std;
using namespace cv;


class ocrManger : public QObject
{
	Q_OBJECT
public:

	static ocrManger* getInstance();
	//禁止拷贝和赋值
	ocrManger(const ocrManger&) = delete;
	ocrManger& operator = (const ocrManger&) = delete;
	std::string runOCR(const std::string& imgPath);   // 整体流程:检测+识别

private:

	static ocrManger* m_pInstance;
	// ====== ONNX Runtime 基础对象(全局只能一个 Env)======
	Ort::Env env;
	Ort::SessionOptions sessionOptions;

	//========检测模型==========
	Ort::Session* m_detSession;
	std::string m_strDetInputName;
	std::string m_strDetOutputName;

	//========识别模型==========
	Ort::Session* m_recSession;
	std::string m_strRecInputName;
	std::string m_strRecOutputName;
	std::vector<std::string> m_recDict;

private:                                                                                                                  
	//构造函数私有化
	explicit ocrManger(QObject *parent = nullptr);

	//=======内部功能函数========
	cv::Mat detecTextBox(const cv::Mat& img, cv::Rect& box);
	std::string recoginizeText(const cv::Mat& roi);
	std::vector<std::string> loadDict(const std::string& path);
};


#include "ocrManger.h"
#include <string>
#include <vector>
#include <fstream>

//类外实例化
ocrManger* ocrManger::m_pInstance = nullptr;

// 与 Python 配置保持一致
const int DET_INPUT_W = 640;
const int DET_INPUT_H = 640;
const int REC_INPUT_W = 320;
const int REC_INPUT_H = 48;
const float MIN_AREA = 100.0f;
const float BINARY_THRESH_MIN = 0.2f;
const float BINARY_THRESH_MAX = 0.6f;
// 定义常用的 ImageNet 均值和方差 (PaddleOCR 和大多数 DBNet 都用这个)
const float mean_vals[3] = { 0.485f, 0.456f, 0.406f };
const float std_vals[3] = { 0.229f, 0.224f, 0.225f };

// C++模拟 Unclip 的膨胀系数
// Python 是多边形偏移,C++ 这里用形态学膨胀模拟,数值可能需要微调
// 如果 Python ratio 是 1.5,这里 kernel size 设为 3~5 左右通常效果接近
const int UNCLIP_DILATE_ITERATIONS = 20;

const std::string SAVE_PATH = "./result.jpg";

// 辅助函数:Sigmoid
static float sigmoid(float x) {
	return 1.0f / (1.0f + exp(-x));
}

/***********************************************
 * @功能描述 : 构造函数,初始化onnx环境
 ***********************************************/
ocrManger::ocrManger(QObject* parent) : QObject(parent)
{
	//初始化 ONNX Runtime 环境
	this->env = Ort::Env(ORT_LOGGING_LEVEL_WARNING, "OCR");
	sessionOptions.SetIntraOpNumThreads(4);
	sessionOptions.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_BASIC);
	//尝试启用 CUDA 加速
	try
	{
		OrtSessionOptionsAppendExecutionProvider_CUDA(sessionOptions, 0);
	}
	catch (...)
	{
		std::cout << "⚠️ CUDA not available, fallback to CPU" << std::endl;
	}

	Ort::AllocatorWithDefaultOptions allocator;
	
	try
	{
		//====加载检测模型====
		m_detSession = new Ort::Session(env, L"./model/det_inference.onnx", sessionOptions);
		m_strDetInputName = m_detSession->GetInputNameAllocated(0, allocator).get();
		m_strDetOutputName = m_detSession->GetOutputNameAllocated(0, allocator).get();
	}
	catch (const std::exception& e) {
		std::cerr << "Failed to load DET model: " << e.what() << std::endl;
	}
	try
	{
		//====加载识别模型====
		m_recSession = new Ort::Session(env, L"./model/rec_inference.onnx", sessionOptions);
		m_strRecInputName = m_recSession->GetInputNameAllocated(0, allocator).get();
		m_strRecOutputName = m_recSession->GetOutputNameAllocated(0, allocator).get();
	}
	catch (const std::exception& e) {
		std::cerr << "Failed to load REC model: " << e.what() << std::endl;
	}

	//====加载字典====
	m_recDict = loadDict("./model/ppocr_keys.txt");
	if (m_recDict.empty()) std::cerr << "Warning: Dictionary is empty!" << std::endl;

	std::cout << "OCR system initialized.\n";
}


/***********************************************
 * @功能描述 : 运行ocr流程
 ***********************************************/
std::string ocrManger::runOCR(const std::string & imgPath)
{
	cv::Mat img = cv::imread(imgPath);
	if (img.empty())
	{
		return "";
	}

	// 1) 检测框 获得包含文字的矩形框 (box) 和 二值化掩码 (binmap)
	cv::Rect  box;
	cv::Mat binmap = detecTextBox(img, box);
	if (box.area() < 10)
	{
		cout << "No valid text detectd\n";
		return "";
	}
	//裁剪 ROI 根据检测框从原图中抠出文字区域
	cv::Mat roi = img(box).clone();
	cv::imshow("Debug ROI", roi);

	//2) 识别
	string text = recoginizeText(roi);
	cout << "Final ocr result" << text << endl;
	return text;
}



cv::Mat ocrManger::detecTextBox(const cv::Mat & img, cv::Rect & box)
{
	//缩放和归一化
	int originW = img.cols;
	int originH = img.rows;
	cv::Mat resizedImg;
	cv::resize(img, resizedImg, cv::Size(DET_INPUT_W, DET_INPUT_H));

	std::vector<float> inputTensorValues;
	inputTensorValues.reserve(DET_INPUT_W*DET_INPUT_H * 3);

	// 遍历顺序:Channel -> Height -> Width (CHW)
	// OpenCV 默认是 BGR,分别提取 B, G, R 通道
	for (int c = 0; c < 3; c++)
	{
		for (int i = 0; i < DET_INPUT_H; i++)
		{
			for (int j = 0; j < DET_INPUT_W; j++)
			{
				float pixel = resizedImg.at<cv::Vec3b>(i, j)[c];
				inputTensorValues.push_back((pixel / 255.0f - mean_vals[c]) / std_vals[c]);
			}
		}
	}

	//创建tensor
	std::array<int64_t, 4> inputShape = { 1,3,DET_INPUT_H,DET_INPUT_W };//数据格式
	Ort::MemoryInfo memoryInfo = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
	Ort::Value inputTensor = Ort::Value::CreateTensor<float>(
		memoryInfo,
		inputTensorValues.data(),
		inputTensorValues.size(),
		inputShape.data(),
		inputShape.size()
		);

	//开始推理,,DBNet 输出通常是 1x1xHxW
	const char* inputNames[] = { m_strDetInputName.c_str() };
	const char* outputNames[] = { m_strDetOutputName.c_str() };
	auto outputTensor = m_detSession->Run(
		Ort::RunOptions{ nullptr },
		inputNames,
		&inputTensor,
		1,
		outputNames,
		1
		);

	//后处理
	float* pOutput = outputTensor[0].GetTensorMutableData<float>();
	cv::Mat predMap(DET_INPUT_H, DET_INPUT_W, CV_32F, pOutput);

	//Sigmoid
	for (int i = 0; i < predMap.rows * predMap.cols; i++)
	{
		predMap.at<float>(i) = sigmoid(predMap.at<float>(i));
	}

	//缩放回原图
	cv::Mat predResized;
	cv::resize(predMap, predResized, cv::Size(originW, originH));
	//自适应阈值(对齐 Python 逻辑)
	// Python: mean_val = pred_map_resized.mean()
	// Python: binary_thresh = clip(mean_val * 1.5, 0.2, 0.6)
	cv::Scalar mean_scalar = cv::mean(predResized);
	float meanVal = static_cast<float>(mean_scalar[0]);
	float binary_thresh = std::max(BINARY_THRESH_MIN, std::min(BINARY_THRESH_MAX, meanVal * 1.5f));
	
	// 二值化
	cv::Mat binaryMap;
	cv::threshold(predResized, binaryMap, binary_thresh, 255, cv::THRESH_BINARY);
	binaryMap.convertTo(binaryMap, CV_8U);

	//Unclip 模拟 (形态学膨胀)
	// Python 使用多边形偏移,C++ 简单版使用 mask 膨胀
	cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
	cv::dilate(binaryMap, binaryMap, kernel, cv::Point(-1, -1), UNCLIP_DILATE_ITERATIONS);

	//查找轮廓
	std::vector<std::vector<cv::Point>> contours;
	cv::findContours(binaryMap, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);

	double maxArea = 0.0;
	box = cv::Rect(0, 0, 0, 0);

	for (const auto& cnt : contours) {
		double area = cv::contourArea(cnt);
		if (area > MIN_AREA && area > maxArea) { // 过滤太小的噪点
			maxArea = area;
			box = cv::boundingRect(cnt);
		}
	}

	// 返回二值图供外部进行更精细的操作
	return binaryMap;
}



/***********************************************
 * @功能描述 : 文本识别
 ***********************************************/
std::string ocrManger::recoginizeText(const cv::Mat & roi)
{
	if (roi.empty()) return "";

	//高度固定为 48,宽度按比例缩放,最大不超过 320
	int h = roi.rows;
	int w = roi.cols;
	float ratio = w * 1.0f / h;
	int target_w = std::min(int(REC_INPUT_H * ratio), REC_INPUT_W);

	// 保证 target_w 至少为1
	target_w = std::max(target_w, 1);

	// 创建一个 48x320 的灰色底图 (0.5f 对应归一化后的0)
	cv::Mat resized;
	cv::resize(roi, resized, cv::Size(target_w, REC_INPUT_H));
	cv::Mat padded(REC_INPUT_H, REC_INPUT_W, CV_32FC3, cv::Scalar(0.5f, 0.5f, 0.5f)); // 归一化后的0通常是灰

	// 转换为浮点并归一化
	resized.convertTo(resized, CV_32FC3);
	resized /= 255.0f;
	resized = (resized - 0.5f) / 0.5f;

	// 将缩放后的图贴到 padding 图的左边
	resized.copyTo(padded(cv::Rect(0, 0, target_w, REC_INPUT_H)));

	//HWC -> CHW 转换
	std::vector<float> inputTensorValues;
	inputTensorValues.reserve(REC_INPUT_H * REC_INPUT_W * 3);

	for (int c = 0; c < 3; c++) {
		for (int i = 0; i < REC_INPUT_H; i++) {
			for (int j = 0; j < REC_INPUT_W; j++) {
				inputTensorValues.push_back(padded.at<cv::Vec3f>(i, j)[c]);
			}
		}
	}

	std::array<int64_t, 4> inputShape = { 1, 3, REC_INPUT_H, REC_INPUT_W };
	Ort::MemoryInfo memoryInfo = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
	Ort::Value inputTensor = Ort::Value::CreateTensor<float>(
		memoryInfo, inputTensorValues.data(), inputTensorValues.size(), inputShape.data(), inputShape.size()
		);

	// 3. Inference
	const char* inputNames[] = { m_strRecInputName.c_str() };
	const char* outputNames[] = { m_strRecOutputName.c_str() };

	auto outputTensor = m_recSession->Run(
		Ort::RunOptions{ nullptr },
		inputNames,
		&inputTensor,
		1,
		outputNames,
		1
	);

	// 模型输出形状通常是 [1, TimeSteps, NumClasses]
	float* pOutput = outputTensor[0].GetTensorMutableData<float>();
	auto outShape = outputTensor[0].GetTensorTypeAndShapeInfo().GetShape(); // [1, T, C]
	int T = outShape[1];
	int C = outShape[2];

	std::string resultText;
	int prev_idx = -1;

	for (int t = 0; t < T; t++) {
		// ArgMax
		int max_idx = 0;
		float max_val = -FLT_MAX;
		for (int c = 0; c < C; c++) {
			float val = pOutput[t * C + c];
			if (val > max_val) {
				max_val = val;
				max_idx = c;
			}
		}

		// CTC Logic: Drop duplicate neighbors and blank (index 0)
		if (max_idx != prev_idx && max_idx != 0) {
			if (max_idx < m_recDict.size()) {
				resultText += m_recDict[max_idx];
			}
		}
		prev_idx = max_idx;
	}

	return resultText;
}



/***********************************************
 * @功能描述 : 从txt中读取字符对应字典
 ***********************************************/
std::vector<std::string> ocrManger::loadDict(const std::string & path)
{
	vector<std::string> dict;
	dict.push_back("blank");

	std::ifstream infile(path);
	string line;
	while (getline(infile, line))
	{
		if (!line.empty())
		{
			dict.push_back(line);
		}
	}
	return dict;
}

//获取单例对象
ocrManger* ocrManger::getInstance()
{
	if (m_pInstance == nullptr)
	{
		m_pInstance = new ocrManger();
	}
	return m_pInstance;
}

能够识别出文字,但是检测的模型训练的不太好,有时候检测不到,需要加些扰动和缩放感觉

image-20260113172356587

遇到问题

1.AssertionError: The length of ratio_list should be the same as the file_list.

去掉’','[../train_data/det/train.txt]'变为[../train_data/det/train.txt]

2.验证时候爆显存

打开配置文件,关闭使用gpuuse-gpu=false

3.OpenCV was not compiled with the freetype module (opencv_freetype) !

4.X版本的opencv都没有,需要自己编译,官方写的很详细,参考官方就行参考链接

image-20251111172749735

4.opencv部分库下载失败

打开build文件夹下,有一个download_with_curl.sh,里边是下载的链接

image-20251112135411934

使用科学上网,下载链接,把文件放入对应文件夹

image-20251112140018587

保存的时候名字前缀对应,直接替换就行

image-20251112140421192

推荐使用以下方式,复制到cmd下载

curl --proxy "http://127.0.0.1:端口号" --create-dirs --output "E:/RuiYanTech/cache/ppocrlabel-3.1.4/opencv-4.7.0/.cache/xfeatures2d/boostdesc/98ea99d399965c03d555cef3ea502a0b-boostdesc_binboost_128.i" "https://raw.githubusercontent.com/opencv/opencv_3rdparty/34e4206aef44d50e6bbcd0ab06354b52e7466d26/boostdesc_binboost_128.i"

image-20251112160359959

5.opencv编译No SOURCES given to target: ade

自行下载下载v0.1.2a.zip 替换名字放入

并将opencv-4.7.0\modules\gapi\cmake\DownloadADE.cmake的下载函数注释,再次config就能够编译通过了

image-20251112155555682

6.检测到“_ITERATOR_DEBUG_LEVEL”的不匹配项: 值“2”不匹配值“0”(algorithm.obj 中)

生成opencv时报错,需要确保所有项目(opencv_world 和 harfbuzz)都使用相同的配置(Debug 或 Release)

7.onnx模型转换失败

运行后发现模型为0kb,转换失败

image-20251112170156776

查询了相关资料,是导出模型的时候少了文件,新版的paddle改了只导出json,不使用.pdmodel,目前好像还没有修复这个bug,推荐使用小于3.0版本的paddle

image-20251113090626118

8.onnxRuntime加载onnx模型生成环境报异常

ONNX Runtime Exception: Load model from ./inference.onnx failed:D:\a_work\1\s\onnxruntime\core\graph\model.cc:111 onnxruntime::Model::Model Unknown model file format version.

我的c++的onnxRuntime是1.8.1版本的,导出模型的版本是1.18.0,估计是太老了,升级一下,直接统一换到1.18.0

升级后报错,找到错误删除constexpr

image-20251113161434099

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐