跳转至

LPR

1 概述

1.1 背景介绍

本次开源的车牌识别算法来源于LPRNet_Pytorch仓库, 该算法目前仅支持同时识别蓝牌和绿牌等中国车牌 仓库地址:

https://github.com/sirius-ai/LPRNet_Pytorch

模型下载地址:

https://github.com/sirius-ai/LPRNet_Pytorch/blob/master/weights/Final_LPRNet_model.pth

1.2 使用说明

Linux SDK-alkaid中默认带了已经预先转换好的离线模型及板端示例, 相关文件路径如下:

  • 板端示例程序路径

    Linux_SDK/sdk/verify/opendla/source/ocr/lpr
    
  • 板端离线模型路径

    Linux_SDK/project/board/${chip}/dla_file/ipu_open_models/ocr/lprnet_94x24.img
    
  • 板端测试图像路径

    Linux_SDK/sdk/verify/opendla/source/resource/license_plate.jpg
    

如果用户不需要转换模型可直接跳转至第3章节。

2 模型转换

2.1 onnx模型转换

  • python环境搭建

    $conda create -n lpr python==3.10
    $conda activate lpr
    $git clone https://github.com/sirius-ai/LPRNet_Pytorch.git
    $cd LPRNet_Pytorch
    

    注意:这里提供的python环境搭建, 仅作为参考示例, 具体搭建过程请参考官方源码运行教程:

    https://github.com/sirius-ai/LPRNet_Pytorch/blob/master/README.md
    
  • 模型测试

    • 运行模型测试脚本, 确保lpr环境配置正确。
      $python test_LPR.py
      
  • 模型导出

    • 安装依赖包

      $pip install onnx -i https://pypi.tuna.tsinghua.edu.cn/simple
      $pip install onnx-simplifier -i https://pypi.tuna.tsinghua.edu.cn/simple
      
    • 编写模型转换脚本export.py:

      from data.load_data import CHARS, CHARS_DICT, LPRDataLoader
      from PIL import Image, ImageDraw, ImageFont
      from model.LPRNet import build_lprnet
      # import torch.backends.cudnn as cudnn
      from torch.autograd import Variable
      import torch.nn.functional as F
      from torch.utils.data import *
      from torch import optim
      import torch.nn as nn
      import numpy as np
      import argparse
      import torch
      import time
      import cv2
      import os
      
      import onnx
      from onnxsim import simplify
      
      def get_parser():
          parser = argparse.ArgumentParser(description='parameters to train net')
          parser.add_argument('--img_size', default=[94, 24], help='the image size')
          parser.add_argument('--test_img_dirs', default="./data/test", help='the test images path')
          parser.add_argument('--dropout_rate', default=0, help='dropout rate.')
          parser.add_argument('--lpr_max_len', default=8, help='license plate number max length.')
          parser.add_argument('--test_batch_size', default=1, help='testing batch size.')
          parser.add_argument('--phase_train', default=False, type=bool, help='train or test phase flag.')
          parser.add_argument('--num_workers', default=8, type=int, help='Number of workers used in dataloading')
          parser.add_argument('--cuda', default=True, type=bool, help='Use cuda to train model')
          parser.add_argument('--show', default=False, type=bool, help='show test image and its predict result or not.')
          parser.add_argument('--pretrained_model', default='./weights/Final_LPRNet_model.pth', help='pretrained base model')
      
          args = parser.parse_args()
      
          return args
      
      def collate_fn(batch):
          imgs = []
          labels = []
          lengths = []
          for _, sample in enumerate(batch):
              img, label, length = sample
              imgs.append(torch.from_numpy(img))
              labels.extend(label)
              lengths.append(length)
          labels = np.asarray(labels).flatten().astype(np.float32)
      
          return (torch.stack(imgs, 0), torch.from_numpy(labels), lengths)
      
      def test():
          args = get_parser()
      
          lprnet = build_lprnet(lpr_max_len=args.lpr_max_len, phase=args.phase_train, class_num=len(CHARS), dropout_rate=args.dropout_rate)
          device = torch.device("cuda:0" if args.cuda else "cpu")
          lprnet.to(device)
          print("Successful to build network!")
      
          # load pretrained model
          if args.pretrained_model:
              lprnet.load_state_dict(torch.load(args.pretrained_model))
              print("load pretrained model successful!")
          else:
              print("[Error] Can't found pretrained mode, please check!")
              return False
      
          test_img_dirs = os.path.expanduser(args.test_img_dirs)
          test_dataset = LPRDataLoader(test_img_dirs.split(','), args.img_size, args.lpr_max_len)
          try:
              Greedy_Decode_Eval(lprnet, test_dataset, args)
          finally:
              cv2.destroyAllWindows()
      
      def Greedy_Decode_Eval(Net, datasets, args):
          # TestNet = Net.eval()
          epoch_size = len(datasets) // args.test_batch_size
          batch_iterator = iter(DataLoader(datasets, args.test_batch_size, shuffle=True, num_workers=args.num_workers, collate_fn=collate_fn))
      
          Tp = 0
          Tn_1 = 0
          Tn_2 = 0
          t1 = time.time()
          for i in range(epoch_size):
              # load train data
              images, labels, lengths = next(batch_iterator)
              start = 0
              targets = []
              for length in lengths:
                  label = labels[start:start+length]
                  targets.append(label)
                  start += length
              targets = np.array([el.numpy() for el in targets])
              imgs = images.numpy().copy()
      
              if args.cuda:
                  images = Variable(images.cuda())
              else:
                  images = Variable(images)
      
              # forward
              prebs = Net(torch.tensor(images))
      
              input_names = ['images']
              output_names = ['output']
      
              torch.onnx.export(
                  Net,
                  args=(images),
                  f='./opendla/lprnet.onnx',
                  input_names=input_names,
                  output_names=output_names,
                  export_params=True,
                  opset_version=13)
      
              model = onnx.load('./opendla/lprnet.onnx')
              model_simp, check = simplify(model)
              export_name = './opendla/lprnet_sim.onnx'
              onnx.save(model_simp, export_name)
              exit(1)
      
      if __name__ == "__main__":
          test()
      
    • 运行模型转换脚本

      $python export.py
      

2.2 离线模型转换

2.2.1 预&后处理说明

  • 预处理

    转换成功的lpr_sim.onnx模型输入信息如下图所示, 要求输入图像的尺寸为 (1, 3, 94, 24), 此外需要将像素值归一化到 [0, 1] 范围内。

  • 后处理

    本次对应开源的示例demo仅用于评估模型推理时间, 因此未提供后处理。模型输出信息如下图所示

2.2.2 offline模型转换流程

注意:1)OpenDLAModel对应的是压缩包image-dev_model_convert.tar解压之后的smodel文件。2)转换命令需要在docker环境下运行, 请先根据Docker开发环境教程, 加载SGS Docker环境。

  • 拷贝onnx模型到转换代码目录

    $cp opendla/lprnet_sim.onnx OpenDLAModel/ocr/lpr/onnx
    
  • 转换命令

    $cd IPU_SDK_Release/docker
    $bash run_docker.sh
    #进入到docker环境下的OpenDLAModel目录
    $cd /work/SGS_XXX/OpenDLAModel
    $bash convert.sh -a ocr/lpr -c config/ocr_lpr -p SGS_IPU_Toolchain(绝对路径) -s false
    
  • 最终生成的模型地址

    output/${chip}_${时间}/lprnet_sim_94x24.img
    output/${chip}_${时间}/lprnet_sim_94x24_fixed.sim
    output/${chip}_${时间}/lprnet_sim_94x24_float.sim
    

2.2.3 关键脚本参数解析

-   input_config.ini

        [INPUT_CONFIG]
        inputs = input;                     #onnx 输入节点名称, 如果有多个需以“,”隔开;
        training_input_formats = RGB;       #模型训练时的输入格式, 通常都是RGB;
        input_formats = BGRA;               #板端输入格式, 可以根据情况选择BGRA或者YUV_NV12;
        quantizations = TRUE;               #打开输入量化, 不需要修改;
        mean_red = 127.5;                   #均值, 跟模型预处理相关, 根据实际情况配置;
        mean_green = 127.5;                 #均值, 跟模型预处理相关, 根据实际情况配置;
        mean_blue = 127.5;                  #均值, 跟模型预处理相关, 根据实际情况配置;
        std_value = 128;                    #方差, 跟模型预处理相关, 根据实际情况配置;

        [OUTPUT_CONFIG]
        outputs = output;                   #onnx 输出节点名称, 如果有多个需以“,”隔开;
        dequantizations = FALSE;            #是否开启反量化, 根据实际需求填写, 建议为TRUE。设为False, 输出为int16; 设为True, 输出为float32

-   ocr_lpr.cfg

        [LPR]
        CHIP_LIST=pcupid                            #平台名称, 必须和板端平台一致, 否则模型无法运行
        Model_LIST=lprnet_sim                       #输入onnx模型名称
        INPUT_SIZE_LIST=94x24                       #模型输入分辨率
        INPUT_INI_LIST=input_config.ini             #配置文件
        CLASS_NUM_LIST=0                            #填0即可
        SAVE_NAME_LIST=lprnet_sim_94x24.img         #输出模型名称
        QUANT_DATA_PATH=quant_data                  #量化图片路径

2.3 模型仿真

  • 获取float/fixed/offline模型输出

    $bash convert.sh -a ocr/lpr -c config/ocr_lpr -p SGS_IPU_Toolchain(绝对路径) -s true
    

    执行上述命令后, 会默认将float模型的输出tensor保存到ocr/lpr/log/output路径下的txt文件中。此外, 在ocr/lpr/convert.sh脚本中也提供了fixedoffline的仿真示例, 用户在运行时可以通过打开注释代码块, 分别获取fixedoffline模型输出。

  • 模型精度对比

    在保证输入和上述模型相同的情况下, 进入2.1章节搭建好的环境, 在LPRNet_Pytorch/test_LPRNet.py文件的第106行处添加打印:

    print(prebs)
    

    获取pytorch模型对应节点的输出tensor, 进而和float、fixed、offline模型进行对比。此外需要特别注意的是, 原始模型的输出格式是NCHW, 而float/fixed/offline模型输出的格式是NHWC

3 板端部署

3.1 程序编译

示例程序编译之前需要先根据板子(nand/nor/emmc, ddr型号等)选择deconfig进行sdk整包编译, 具体可以参考alkaid sdk sigdoc《开发环境搭建》文档。

  • 编译板端lpr示例。

    $cd sdk/verify/opendla
    $make clean && make source/ocr/lpr -j8
    
  • 最终生成的可执行文件地址

    sdk/verify/opendla/out/${AARCH}/app/prog_ocr_lpr
    

3.2 运行文件

运行程序时, 需要先将以下几个文件拷贝到板端

  • prog_ocr_lpr
  • license_plate.jpg
  • lprnet_94x24.img

3.3 运行说明

  • Usage: ./prog_ocr_lpr -i image -m model [-t threshold](执行文件使用命令)

  • Required Input:

    • image: 图像文件夹/单张图像路径
    • model: 需要测试的offline模型路径
  • Optional Input:

    • threshold: 检测阈值(0.0~1.0, 默认为0.5)
  • Typical Output:

    ./prog_ocr_lpr -i license_plate.jpg -m models/lprnet_94x24.img
    
    inputs: license_plate.jpg
    model path: models/lprnet_94x24.img
    threshold: 0.500000
    client [1016] connected, module:ipu
    unknown element format 7
    found 1 images!
    the input image: ./license_plate.jpg
    fillbuffer processing...
    net input width: 94, net input height: 24
    client [1016] disconnected, module:ipu
    model invoke time: 2.732000 ms