< BACK_TO_INDEX

Firefly ROC-RK3566-PC 板卡上配置 PaddleOCR v4 到 NPU 实现图片文字识别

PUBLISHED:

Firefly ROC-RK3566-PC 板卡上配置 PaddleOCR v4 到 NPU 实现图片文字识别

摘要:本文记录了如何在资源极度受限的 Firefly ROC-RK3566-PC 开发板(1GB RAM + 8GB ROM, Kylin OS)上,为自研局域网聊天软件 ZFeiQ 成功部署离线中文 OCR 功能。通过 FP16 推理 + Pillow 替代 OpenCV + 极简依赖管理,在仅剩 3GB 存储空间的板子上实现了 NPU 硬件加速,中文识别准确率 > 95%,且运行时内存占用较低(约 10 MB)。

一、 挑战与最终解决方案速览

在开发 ZFeiQ 的“截图文字识别”功能时,我遇到了嵌入式 AI 部署的三个“拦路虎”:

  1. 存储危机:板子仅剩 3.3GB 存储。如果使用标准的 apt install python3-opencv,会拉取 X11、GTK 等数百 MB 的图形界面依赖,直接撑爆存储空间。
  2. 量化崩盘:标准的 INT8 量化(Quantization)需要高质量的校准数据集。如果使用简单的白底黑字图校准,NPU 运行复杂图片时会出现数值溢出,导致中文识别全是乱码。
  3. 内存捉襟见肘:板子 RAM 仅 1GB,UKUI 已占用大半。必须极致压缩 Python 库的运行时内存开销。

最终技术选型:

  • 模型策略:放弃 INT8,改用 FP16 (半精度浮点)。RK3566 NPU 原生支持 FP16,虽比 INT8 略慢,但零精度损失,且无需校准,完美避开量化坑。
  • 图像处理:主程序尽量摆脱对 OpenCV 的依赖,改用 Pillow (PIL) + Numpy,安装包仅几 MB。
  • 推理引擎:使用 rknn_toolkit_lite2 直接调用 NPU 硬件。

PP-OVR v3 与 ZFeiQ 联合演示:

软件与 NPU 分别跑通


二、 PC 端准备

为了避免在板子上进行繁重的编译工作,所有的模型转换都在 PC (WSL2 Ubuntu 20.04) 上完成。 以下略去 WSL Ubuntu 20.04.6 LTS 与 Firefly RK3566 SDK 的部署,可参照:

1. 环境安装

我们需要安装 paddle2onnx 及其依赖来导出模型,以及 rknn-toolkit2 来编译 NPU 模型。

# 1. 安装 Paddle 转换工具 (需先安装 paddlepaddle cpu 版)
pip3 install paddlepaddle paddle2onnx onnx

如何找到 rknn-toolkit2 安装包:

zhangzw0170@LAPTOP-5DE9621H:~/proj/firefly_rk3566_SDK/external$ find . -name "*cp38*.whl*"
./rknn-toolkit2/rknn_toolkit_lite2/packages/rknn_toolkit_lite2-1.6.0-cp38-cp38-linux_aarch64.whl
./rknn-toolkit2/rknn-toolkit2/docker/docker_file/ubuntu_20_04_cp38/rknn_toolkit2-1.6.0+81f21f4d-cp38-cp38-linux_x86_64.whl
./rknn-toolkit2/rknn-toolkit2/packages/rknn_toolkit2-1.6.0+81f21f4d-cp38-cp38-linux_x86_64.whl
zhangzw0170@LAPTOP-5DE9621H:~/proj/firefly_rk3566_SDK/external$

注意:请根据你的 PC Python 版本选择对应的 whl,这里的 cp3x 对应 Python 3.x 版本,我用的是 Python 3.8。

上面的 2 个包分别是:

  • rknn_toolkit_lite2-1.6.0-cp38-cp38-linux_aarch64.whl:复制出来备用,在 3.2 节安装到板卡上用
  • rknn_toolkit2-1.6.0+81f21f4d-cp38-cp38-linux_x86_64.whl:执行下面的命令安装到 WSL,这个包可以在 x64 的机子上模拟 RK3566 的 NPU 工作
# 2. 安装 RKNN 工具 (从 Firefly SDK 或瑞芯微 GitHub 获取 whl)
pip3 install rknn_toolkit2-1.6.0-cp38-cp38-linux_x86_64.whl

2. 原料获取:模型与字典

我们选择 PP-OCRv4 Slim 系列模型,体积极小且中文识别率高。

mkdir -p ~/PPOCRv4 && cd ~/PPOCRv4

# 下载检测模型 (Det)
wget https://paddleocr.bj.bcebos.com/PP-OCRv4/chinese/ch_PP-OCRv4_det_infer.tar
tar xf ch_PP-OCRv4_det_infer.tar

# 下载识别模型 (Rec)
wget https://paddleocr.bj.bcebos.com/PP-OCRv4/chinese/ch_PP-OCRv4_rec_infer.tar
tar xf ch_PP-OCRv4_rec_infer.tar

# 下载字典文件 (必需)
wget https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/release/2.6/ppocr/utils/ppocr_keys_v1.txt

查阅算子支持列表:<SDK 目录>/external/rknn-toolkit2/doc/RKNN-Toolkit2_OP_Support-1.6.0.md。

读者可能会问,为什么要在这里查?因为笔者一开始部署的是 PP-OCR v3,依赖的最低级算子库是 Opset 11,此时还没有加入 HardSmish 算子;而现在的 PP-OCR v4,依赖的最低级算子库是 Opset 14。笔者当时还觉得很遗憾,以为没办法上更新的模型,但实际上 RKNN ToolKit2 1.6.0 是支持 HardSmish 算子的。所以学会查文档真的很重要啊!!

3. 模型构建:ONNX 固化与 FP16 编译

这是最核心的一步。由于 NPU 需要固定输入形状,且我们要避开 INT8 量化的精度损失,我们编写了一个 Python 脚本 build_fp16_v4.py 来自动完成 ONNX 导出 -> Shape 修改 -> RKNN 编译 的全过程。

# build_fp16_v4.py
import onnx
from rknn.api import RKNN
import os
import sys

# --- 配置 ---
# 目标平台
PLATFORM = 'rk3566'
# 输出文件夹
OUT_DIR = './build_output'

# 检测模型输入尺寸 (960x960)
DET_SHAPE = [1, 3, 960, 960]
# 识别模型输入尺寸 (标准值为 1, 3, 48, 320)
# 此处加大识别尺寸是为了处理单行长文本
REC_SHAPE = [1, 3, 48, 640]

def fix_onnx_shape(src_path, dst_path, input_shape):
    """
    彻底固化 ONNX 维度,包括 Batch Size
    """
    print(f"--> Fixing shape for {src_path} to {input_shape}...")
    if not os.path.exists(src_path):
        print(f"Error: Source file {src_path} not found!")
        sys.exit(1)
        
    model = onnx.load(src_path)
    input_tensor = model.graph.input[0]
    
    # 1. 强制修改所有维度 (Batch, Channel, Height, Width)
    for i in range(4):
        input_tensor.type.tensor_type.shape.dim[i].dim_value = input_shape[i]
    
    # 2. 清除动态参数名 (dim_param),防止干扰
    for i in [0, 2, 3]:
        if input_tensor.type.tensor_type.shape.dim[i].dim_param:
            input_tensor.type.tensor_type.shape.dim[i].dim_param = ""

    onnx.save(model, dst_path)
    print(f"    Saved static model to {dst_path}")

def build_rknn(onnx_path, rknn_path, mean_values, std_values):
    print(f"--> Building RKNN: {rknn_path}")
    rknn = RKNN(verbose=False)
    rknn.config(mean_values=[mean_values], std_values=[std_values], target_platform=PLATFORM)
    
    # 加载
    if rknn.load_onnx(model=onnx_path) != 0:
        print('Load ONNX failed!')
        sys.exit(1)
        
    # 构建 (FP16 模式,关闭量化)
    print('--> Building (FP16 Mode)...')
    if rknn.build(do_quantization=False) != 0:
        print('Build RKNN failed!')
        sys.exit(1)
        
    # 导出
    if rknn.export_rknn(rknn_path) != 0:
        print('Export RKNN failed!')
        sys.exit(1)
    print('    Success!')

if __name__ == '__main__':
    # 0. 准备输出目录
    if not os.path.exists(OUT_DIR):
        os.makedirs(OUT_DIR)
        print(f"Created output directory: {OUT_DIR}")

    # 定义所有文件路径
    det_dynamic_path = os.path.join(OUT_DIR, 'ocr_det_dynamic.onnx')
    det_static_path  = os.path.join(OUT_DIR, 'ocr_det_static.onnx')
    det_rknn_path    = os.path.join(OUT_DIR, 'ocr_det_rk3566_v4.rknn')

    rec_dynamic_path = os.path.join(OUT_DIR, 'ocr_rec_dynamic.onnx')
    rec_static_path  = os.path.join(OUT_DIR, 'ocr_rec_static.onnx')
    rec_rknn_path    = os.path.join(OUT_DIR, 'ocr_rec_rk3566_v4.rknn')

    # 1. 导出动态 ONNX (如果不存在)
    if not os.path.exists(det_dynamic_path):
	# 需要 Opset >= 14 以支持 HardSwish 算子
        print("Exporting Det dynamic ONNX...")
        cmd = (f"paddle2onnx --model_dir ./ch_PP-OCRv4_det_infer "
               f"--model_filename inference.pdmodel --params_filename inference.pdiparams "
               f"--save_file {det_dynamic_path} --opset_version 14 --enable_onnx_checker True")
        os.system(cmd)

    if not os.path.exists(rec_dynamic_path):
        print("Exporting Rec dynamic ONNX...")
        cmd = (f"paddle2onnx --model_dir ./ch_PP-OCRv4_rec_infer "
               f"--model_filename inference.pdmodel --params_filename inference.pdiparams "
               f"--save_file {rec_dynamic_path} --opset_version 11 --enable_onnx_checker True")
        os.system(cmd)

    # 2. 处理检测模型 (Det)
    fix_onnx_shape(det_dynamic_path, det_static_path, DET_SHAPE)
    build_rknn(det_static_path, det_rknn_path, 
               mean_values=[123.675, 116.28, 103.53], 
               std_values=[58.395, 57.12, 57.375])

    # 3. 处理识别模型 (Rec)
    fix_onnx_shape(rec_dynamic_path, rec_static_path, REC_SHAPE)
    build_rknn(rec_static_path, rec_rknn_path, 
               mean_values=[127.5, 127.5, 127.5], 
               std_values=[127.5, 127.5, 127.5])

    print(f"\nAll done! Check {OUT_DIR} for generated files.")

4. 测试图片准备

以下脚本可用于生成测试图片:

# gen_test_img.py
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import os

# 图片保存路径
SAVE_PATH = "test.jpg"

def create_demo_image():
    # 1. 创建一张白色的背景图 (H, W, C)
    # 为了更接近真实场景,我们给它设大一点,比如 800x800
    width, height = 800, 800
    img_pil = Image.new("RGB", (width, height), (255, 255, 255))
    draw = ImageDraw.Draw(img_pil)

    # 2. 设置字体
    # WSL2 可以直接访问 Windows 的字体目录
    # 尝试加载黑体 (SimHei),如果没有则尝试微软雅黑
    font_path = "/mnt/c/Windows/Fonts/simhei.ttf"
    if not os.path.exists(font_path):
        font_path = "/mnt/c/Windows/Fonts/msyh.ttc"
    
    try:
        # 字号设大一点,方便 OCR 识别
        font = ImageFont.truetype(font_path, 40)
    except Exception as e:
        print(f"Error loading font from {font_path}: {e}")
        print("Trying default font (Chinese might fail)...")
        font = ImageFont.load_default()

    # 3. 要写入的文字内容
    texts = [
        "PaddlerOCR by ZFeiQ:",
        "Hello Neural Processing Unit NPU !!!",
        "!@#$%^&*()",
        "你好!神经处理单元!",
        "你好!麒麟类飞秋软件!"
    ]

    # 4. 绘制文字
    x, y = 50, 50
    text_color = (0, 0, 0) # 黑色

    for line in texts:
        draw.text((x, y), line, font=font, fill=text_color)
        y += 60 # 行间距

    # 5. 保存图片
    img_pil.save(SAVE_PATH)
    print(f"Success! Image saved to: {SAVE_PATH}")

if __name__ == "__main__":
    create_demo_image()

生成的测试图片效果: test.jpg

5. 检查文件

本阶段完成后目录下的文件:

zhangzw0170@LAPTOP-5DE9621H:~/proj/PPOCRv4$ tree ~/proj/PPOCRv4
/home/zhangzw0170/proj/PPOCRv4
├── build_fp16_v4.py
├── build_output
│   ├── ocr_det_dynamic.onnx
│   ├── ocr_det_rk3566_v4.rknn
│   ├── ocr_det_static.onnx
│   ├── ocr_rec_dynamic.onnx
│   ├── ocr_rec_rk3566_v4.rknn
│   └── ocr_rec_static.onnx
├── ch_PP-OCRv4_det_infer
│   ├── inference.pdiparams
│   ├── inference.pdiparams.info
│   └── inference.pdmodel
├── ch_PP-OCRv4_det_infer.tar
├── ch_PP-OCRv4_rec_infer
│   ├── inference.pdiparams
│   ├── inference.pdiparams.info
│   └── inference.pdmodel
├── ch_PP-OCRv4_rec_infer.tar
├── ppocr_keys_v1.txt
└── test.jpg

三、 板端环境部署

这是板子上的最终环境清单。我们将 ZFeiQ 的基础依赖和 OCR 的新增依赖合并,力求最小化。

1. requirements.txt 清单

# --- ZFeiQ 基础通信依赖 ---
# 两个密码学库,任选其一即可,推荐第一个
cryptography>=42.0
pycryptodome>=3.19

# --- OCR 新增轻量化依赖 ---
pillow              # 替代 OpenCV 进行图像读取与缩放 (仅几 MB)
numpy               # 矩阵运算
pyclipper           # 后处理:文本框多边形计算
shapely             # 后处理:几何计算
opencv-python       # 【妥协】仅用于 utils 内部数学计算 (pip安装,不走apt,不带GUI依赖)

# --- NPU 驱动 ---
# rknn_toolkit_lite2 (通过官方 whl 文件安装)

2. 安装命令

在 RK3566 上执行以下命令,避免使用 apt 安装庞大的 python3-opencv

# 1. 安装 Python 库 (使用 pip 安装 opencv-python 以避免拉取 X11 依赖)
pip3 install pillow numpy pyclipper shapely opencv-python cryptography pycryptodome

安装刚才准备好的 RKNN Toolkit 2 库: (忘掉了?点这里

# 2. 安装 NPU 驱动 (从 Firefly SDK 获取的 whl)
# 请根据板子 Python 版本选择对应的 whl
pip3 install rknn_toolkit_lite2-1.6.0-cp38-cp38-linux_aarch64.whl

四、 PC 端模拟与板端测试

1. 提取 SDK 后处理代码

PaddleOCR 的后处理逻辑复杂,我们需要从瑞芯微的 rknn_model_zoo 中提取官方实现。 注意:必须从 PPOCR-System 路径下提取,否则会导致 API 不兼容。

# 下载 RKNN 算法示例库
git clone https://github.com/airockchip/rknn_model_zoo.git
# 提取 OCR 后处理需要的 db_postprocess.py 等核心代码
cp -r ./rknn_model_zoo/examples/PPOCR/PPOCR-System/python/utils

2. PC 端模拟脚本

# simulate.py
import os
import sys
import argparse
import numpy as np
from PIL import Image 

# --- 强力静音工具 ---
class HiddenPrints:
    def __init__(self, activated=True):
        self.activated = activated
        self._original_stdout = None
        self._original_stderr = None

    def __enter__(self):
        if self.activated:
            self._original_stdout = sys.stdout
            self._original_stderr = sys.stderr
            sys.stdout = open(os.devnull, 'w')
            sys.stderr = open(os.devnull, 'w')
            try:
                self._stdout_fd = self._original_stdout.fileno()
                self._stderr_fd = self._original_stderr.fileno()
                self._saved_stdout_fd = os.dup(self._stdout_fd)
                self._saved_stderr_fd = os.dup(self._stderr_fd)
                os.dup2(sys.stdout.fileno(), self._stdout_fd)
                os.dup2(sys.stderr.fileno(), self._stderr_fd)
            except Exception:
                pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.activated:
            try:
                os.dup2(self._saved_stdout_fd, self._stdout_fd)
                os.dup2(self._saved_stderr_fd, self._stderr_fd)
                os.close(self._saved_stdout_fd)
                os.close(self._saved_stderr_fd)
            except Exception:
                pass
            sys.stdout = self._original_stdout
            sys.stderr = self._original_stderr

# 解析参数
parser = argparse.ArgumentParser()
parser.add_argument('--debug', action='store_true', help='Show detailed logs')
args = parser.parse_args()

if args.debug:
    os.environ['RKNN_LOG_LEVEL'] = '3'
else:
    os.environ['RKNN_LOG_LEVEL'] = '0'

from rknn.api import RKNN
from utils.db_postprocess import DBPostProcess
from utils.rec_postprocess import CTCLabelDecode

# 【注意】请确保这里指向的是 v4 版本的静态 ONNX 文件
DET_MODEL_PATH = './build_output/ocr_det_static.onnx'
REC_MODEL_PATH = './build_output/ocr_rec_static.onnx'
KEY_PATH = './ppocr_keys_v1.txt'
IMG_PATH = './test.jpg' 

def init_model(path, model_type, verbose=False):
    if verbose: print(f"Loading {path}...")
    rknn = RKNN(verbose=verbose)
    
    # 预处理配置保持与 v3 一致 (Paddle 默认)
    if model_type == 'det':
        rknn.config(mean_values=[[123.675, 116.28, 103.53]], 
                    std_values=[[58.395, 57.12, 57.375]], 
                    target_platform='rk3566')
    else:
        rknn.config(mean_values=[[127.5, 127.5, 127.5]], 
                    std_values=[[127.5, 127.5, 127.5]], 
                    target_platform='rk3566')

    ret = rknn.load_onnx(model=path)
    if ret != 0: print(f"Load {path} failed!"); sys.exit(1)
    
    ret = rknn.build(do_quantization=False)
    if ret != 0: print("Build failed!"); sys.exit(1)
    
    ret = rknn.init_runtime(target=None)
    if ret != 0: print("Init runtime failed!"); sys.exit(1)
    return rknn

def main():
    msg_prefix = "[Sim v4]"
    if not args.debug:
        print(f"{msg_prefix} Quiet Mode ON. Initializing models (960x960 / 640x48)...")

    with HiddenPrints(activated=not args.debug):
        det_model = init_model(DET_MODEL_PATH, 'det', verbose=args.debug)
        rec_model = init_model(REC_MODEL_PATH, 'rec', verbose=args.debug)

    # 【参数调整】v4 模型检测能力更强,可以适当调高 box_thresh
    post_det = DBPostProcess(thresh=0.3, box_thresh=0.6, unclip_ratio=1.5)
    post_rec = CTCLabelDecode(character_dict_path=KEY_PATH, use_space_char=True)

    if not os.path.exists(IMG_PATH):
        print(f"Error: {IMG_PATH} not found.")
        return
    
    img_pil = Image.open(IMG_PATH).convert("RGB")
    img = np.array(img_pil)
    h, w = img.shape[:2]

    # ==========================
    # 3. Detection (960x960)
    # ==========================
    # 【关键修改】Det 输入尺寸改为 960x960
    img_det_pil = img_pil.resize((960, 960), Image.BILINEAR)
    img_det = np.array(img_det_pil)
    img_det = img_det[np.newaxis, ...]

    with HiddenPrints(activated=not args.debug):
        outputs = det_model.inference(inputs=[img_det], data_format='nhwc')
    
    # 【关键修改】计算比例时使用 960
    ratio_h = 960 / float(h)
    ratio_w = 960 / float(w)
    
    det_results = post_det({'maps': outputs[0]}, [[h, w, ratio_h, ratio_w]])
    dt_boxes = det_results[0]['points']

    print(f"{msg_prefix} Found {len(dt_boxes)} boxes.")

    # ==========================
    # 4. Recognition (640x48)
    # ==========================
    for i, box in enumerate(dt_boxes):
        box = sorted(box, key=lambda x: x[0])
        x_min = int(min([p[0] for p in box]))
        x_max = int(max([p[0] for p in box]))
        y_min = int(min([p[1] for p in box]))
        y_max = int(max([p[1] for p in box]))
        
        crop_img = img[y_min:y_max, x_min:x_max]
        if crop_img.size == 0: continue

        crop_pil = Image.fromarray(crop_img)
        
        # 【关键修改】Rec 输入尺寸改为 640x48
        img_rec_pil = crop_pil.resize((640, 48), Image.BILINEAR)
        img_rec = np.array(img_rec_pil)
        img_rec = img_rec[np.newaxis, ...]

        with HiddenPrints(activated=not args.debug):
            outputs = rec_model.inference(inputs=[img_rec], data_format='nhwc')
        
        rec_res = post_rec(outputs[0]) 
        if rec_res and len(rec_res) > 0:
            text, score = rec_res[0]
            print(f"   Box {i}: '{text}' (Conf: {score:.2f})")

    det_model.release()
    rec_model.release()
    print(f"{msg_prefix} Done.")

if __name__ == '__main__':
    main()

PC 端模拟结果:

zhangzw0170@LAPTOP-5DE9621H:~/proj/PPOCRv4$ python3 simulate.py
[Sim v4] Quiet Mode ON. Initializing models (960x960 / 640x48)...
[Sim v4] Found 5 boxes.
   Box 0: '你好!麒麟类飞秋软件!' (Conf: 0.99)
   Box 1: '你好!神经处理单元!' (Conf: 0.99)
   Box 2: '!@#$%&*' (Conf: 0.96)
   Box 3: 'Hello Neural Processing Unit NPU !!!' (Conf: 1.00)
   Box 4: 'PaddlerOCRbyZFeiQ:' (Conf: 0.96)
[Sim v4] Done.

与图片比较:跳转到图片

若需要更详细的输出,可使用 python3 simulate.py --debug

可以看到,中文与英文的识别结果非常准确,且置信度很高,不过还是有些瑕疵,比如 Box 2 最后的一对括号没有识别出来,Box 4 的文字没有空格。

3. 板卡端测试脚本

# main.py
import os
import sys
import argparse
import numpy as np
from PIL import Image 
from rknnlite.api import RKNNLite

from utils.db_postprocess import DBPostProcess
from utils.rec_postprocess import CTCLabelDecode

# 【关键】修改为 v4 模型路径
DET_MODEL_PATH = './build_output/ocr_det_rk3566_v4.rknn'
REC_MODEL_PATH = './build_output/ocr_rec_rk3566_v4.rknn'
KEY_PATH = './ppocr_keys_v1.txt'
IMG_PATH = './test.jpg' 

def init_model(path, verbose=False):
    if verbose: print(f"Loading {path}...")
    rknn = RKNNLite(verbose=verbose)
    
    ret = rknn.load_rknn(path)
    if ret != 0: print(f"Load {path} failed!"); sys.exit(ret)
    
    # RK3566 单核 NPU,无需 core_mask
    ret = rknn.init_runtime()
    if ret != 0: print("Init runtime failed"); sys.exit(ret)
    return rknn

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--debug', action='store_true', help='Show detailed logs')
    args = parser.parse_args()

    if args.debug:
        os.environ['RKNN_LOG_LEVEL'] = '3'
    else:
        os.environ['RKNN_LOG_LEVEL'] = '0'
    
    msg_prefix = "[Board v4]"
    if not args.debug:
        print(f"{msg_prefix} Running OCR (960x960 / 640x48)...")
    
    det_model = init_model(DET_MODEL_PATH, verbose=args.debug)
    rec_model = init_model(REC_MODEL_PATH, verbose=args.debug)

    # Det 后处理参数
    post_det = DBPostProcess(thresh=0.3, box_thresh=0.5, unclip_ratio=1.5)
    # Rec 后处理参数
    post_rec = CTCLabelDecode(character_dict_path=KEY_PATH, use_space_char=True)

    if not os.path.exists(IMG_PATH):
        print("Error: Image not found!")
        return
    
    img_pil = Image.open(IMG_PATH).convert("RGB")
    img = np.array(img_pil)
    h, w = img.shape[:2]

    # ==========================
    # 3. Detection (960x960)
    # ==========================
    # 【关键修改】Det 输入尺寸改为 960
    img_det_pil = img_pil.resize((960, 960), Image.BILINEAR)
    img_det = np.array(img_det_pil)
    img_det = img_det[np.newaxis, ...]

    # 【关键修改】指定 data_format='nhwc'
    outputs = det_model.inference(inputs=[img_det], data_format='nhwc')
    
    # 【关键修改】比例计算基数改为 960
    ratio_h = 960 / float(h)
    ratio_w = 960 / float(w)
    
    # 适配新版 API:使用字典和双层列表传参
    det_results = post_det({'maps': outputs[0]}, [[h, w, ratio_h, ratio_w]])
    dt_boxes = det_results[0]['points']

    print(f" -> Found {len(dt_boxes)} boxes.")

    # ==========================
    # 4. Recognition (640x48)
    # ==========================
    for i, box in enumerate(dt_boxes):
        box = sorted(box, key=lambda x: x[0])
        x_min = int(min([p[0] for p in box]))
        x_max = int(max([p[0] for p in box]))
        y_min = int(min([p[1] for p in box]))
        y_max = int(max([p[1] for p in box]))
        
        crop_img = img[y_min:y_max, x_min:x_max]
        if crop_img.size == 0: continue

        crop_pil = Image.fromarray(crop_img)
        
        # 【关键修改】Rec 输入宽度改为 640
        img_rec_pil = crop_pil.resize((640, 48), Image.BILINEAR)
        img_rec = np.array(img_rec_pil)
        img_rec = img_rec[np.newaxis, ...]

        # 【关键修改】指定 data_format='nhwc'
        outputs = rec_model.inference(inputs=[img_rec], data_format='nhwc')
        
        rec_res = post_rec(outputs[0]) 
        if rec_res and len(rec_res) > 0:
            text, score = rec_res[0]
            print(f"   Box {i}: '{text}' (Conf: {score:.2f})")

    det_model.release()
    rec_model.release()
    if not args.debug: print("Done.")

if __name__ == '__main__':
    main()

板卡端运行结果:

kylin@kylin:~/ZFeiQ/PPOCRv4$ python3 main.py
[Board v4] Running OCR (960x960 / 640x48)...
W Query dynamic range failed. Ret code: RKNN_ERR_MODEL_INVALID. (If it is a static shape RKNN model, please ignore the above warning message.)
W Query dynamic range failed. Ret code: RKNN_ERR_MODEL_INVALID. (If it is a static shape RKNN model, please ignore the above warning message.)
 -> Found 5 boxes.
   Box 0: '你好!麒麟类飞秋软件!' (Conf: 0.99)
   Box 1: '你好!神经处理单元!' (Conf: 0.99)
   Box 2: '!@#$%&*' (Conf: 0.96)
   Box 3: 'Hello Neural Processing Unit NPU !!!' (Conf: 1.00)
   Box 4: 'PaddlerOCRbyZFeiQ:' (Conf: 0.96)
Done.

可以看到,实际运行结果与 PC 端仿真结果相当符合,结果喜人。

NPU 调用成功


五、 PPOCR v4 与 v3 的识别比较

为了验证升级效果,我对比了 优化后的 v4 模型默认配置的 v3 模型

1. PP-OCR v4 (本方案)

  • 配置:FP16, Det: 960x960, Rec: 640x48 (宽视野)
  • 表现:长英文 Hello Neural... 识别完美,无字符粘连;中文置信度均在 0.99 左右。
  • 日志
kylin@kylin:~/ZFeiQ/PPOCRv4$ python3 main.py
[Board v4] Running OCR (960x960 / 640x48)...
W Query dynamic range failed. Ret code: RKNN_ERR_MODEL_INVALID. (If it is a static shape RKNN model, please ignore the above warning message.)
W Query dynamic range failed. Ret code: RKNN_ERR_MODEL_INVALID. (If it is a static shape RKNN model, please ignore the above warning message.)
 -> Found 5 boxes.
   Box 0: '你好!麒麟类飞秋软件!' (Conf: 0.99)
   Box 1: '你好!神经处理单元!' (Conf: 0.99)
   Box 2: '!@#$%&*' (Conf: 0.96)
   Box 3: 'Hello Neural Processing Unit NPU !!!' (Conf: 1.00)
   Box 4: 'PaddlerOCRbyZFeiQ:' (Conf: 0.96)
Done.

2. PP-OCR v3 (旧方案对照)

  • 配置:FP16, Det: 640x640, Rec: 320x48 (标准视野)
  • 表现:受限于 320 像素宽度,长文本被横向挤压,导致识别丢失字符(HellNeural...)。
  • 日志
kylin@kylin:~/ZFeiQ/zfeiq_ocr_deploy$ python3 main.py --debug
Loading ./ocr_det_rk3566.rknn...
W Verbose file path is invalid, debug info will not dump to file.
D target set by user is: None
D Starting ntp or adb, target soc is RK3566, device id is: None
W Query dynamic range failed. Ret code: RKNN_ERR_MODEL_INVALID. (If it is a static shape RKNN model, please ignore the above warning message.)
Loading ./ocr_rec_rk3566.rknn...
W Verbose file path is invalid, debug info will not dump to file.
D target set by user is: None
D Starting ntp or adb, target soc is RK3566, device id is: None
W Query dynamic range failed. Ret code: RKNN_ERR_MODEL_INVALID. (If it is a static shape RKNN model, please ignore the above warning message.)
 -> Found 5 boxes.
   Box 0: '你好!麒麟类飞秋软件!' (Conf: 0.94)
   Box 1: '你好!神经处理单元!' (Conf: 0.95)
   Box 2: '!@#$%~&*()' (Conf: 0.79)
   Box 3: 'HellNeuralProcessing UnitU!' (Conf: 0.81)
   Box 4: 'PaddlerOCR by ZFeiQ:' (Conf: 0.95)

12.PPOCRv4(左上)与PPOCRv3(左下)的对比


六、 总结与展望

本次在 Firefly RK3566 上部署 PaddleOCR v4 的实战经历,本质上是一次**在极限资源下的工程取舍(Trade-off)**过程。

  1. 打破“版本焦虑”:虽然文档推荐旧版本,但在确认算子支持(HardSwish)的前提下,大胆尝试新版 PP-OCR v4 带来了显著的精度提升。
  2. 精度与速度的平衡:放弃理论更快的 INT8 量化,选择 FP16,以极微小的速度代价换取了**“零校准成本”“PC 级的一致性精度”**,这对项目快速落地至关重要。
  3. 软件工程的价值:通过重写代码移除 OpenCV 依赖、手动固化 ONNX 维度,我们硬生生在 1GB 内存的板子上挤出了运行大模型的空间。

最终成果:ZFeiQ 聊天软件现在具备了完全离线、毫秒级响应、高达 99% 准确率的中文 OCR 能力。这证明了只要优化得当,低算力国产芯片也能承载现代化的 AI 应用。

后续优化方向

  • 将 OCR 推理放入独立线程,避免阻塞 UI 主线程。
  • 添加剪贴板监听功能,实现“复制图片即识别”。