# PaddleOCR车牌识别

车牌识别(Vehicle License Plate Recognition，VLPR) 是计算机视频图像识别技术在车辆牌照识别中的一种应用。车牌识别技术要求能够将运动中的汽车牌照从复杂背景中提取并识别出来，在高速公路车辆管理，停车场管理和城市交通中得到广泛应用。本份文档主要介绍如何基于百度开源PaddleOCR车牌识别模型构建MIGraphX C++推理示例。

## 模型简介

PaddleOCR车牌识别包括本文检测和文本识别两部分内容，其中使用DBnet作为文本检测模型，SVTR作为文本识别模型。DBnet是一种基于分割的文本检测方法，相比传统分割方法需要设定固定阈值，该模型将二值化操作插入到分割网络中进行联合优化，通过网络学习可以自适应的预测图像中每一个像素点的阈值，能够在像素水平很好的检测自然场景下不同形状的文字。SVTR是一种端到端的文本识别模型，通过单个视觉模型就可以一站式解决特征提取和文本转录两个任务，同时也保证了更快的推理速度。百度PaddleOCR开源项目提供了车牌识别的预训练模型，本示例使用PaddleOCR提供的蓝绿黄牌识别模型进行推理，车牌识别过程：输入->图像预处理->文字检测->文本识别->输出。

PaddleOCR车牌识别预训练模型下载链接：https://pan.baidu.com/s/1aeIZpgOSnh52RlztGAHctw , 提取码：hs6u 

## 模型转换

由于MIGraphX只支持onnx模型作为输入，所以本节介绍如何将PaddleOCR模型转换为onnx模型。

1. 下载PaddleOCR代码

```
git clone  -b release/2.6 https://github.com/PaddlePaddle/PaddleOCR.git
cd PaddleOCR && python3.7 setup.py install
```

2. 安装DTK版PaddlePaddle，下载地址：https://cancon.hpccube.com:65024/4/main/paddle
3. 安装Paddle2ONNX

```
python3.7 -m pip install paddle2onnx
```

4. 将下载的车牌识别模型压缩文件放入到inference文件夹下进行解压

```
mkdir inference
tar -xvzf CCPD.tar
```

5. 模型转换

```
paddle2onnx --model_dir ./inference/CCPD/det/infer \
--model_filename inference.pdmodel \
--params_filename inference.pdiparams \
--save_file ./inference/CCPD/det_onnx/model.onnx \
--opset_version 10 \
--input_shape_dict="{'x':[-1,3,-1,-1]}" \
--enable_onnx_checker True

paddle2onnx --model_dir ./inference/CCPD/det/infer \
--model_filename inference.pdmodel \
--params_filename inference.pdiparams \
--save_file ./inference/CCPD/rec_onnx/model.onnx \
--opset_version 10 \
--input_shape_dict="{'x':[-1,3,-1,-1]}" \
--enable_onnx_checker True
```

执行上述操作之后，onnx模型被保存在`./inference/CCPD/det_onnx/`, `./inference/CCPD/rec_onnx/`。

## 预处理

### DBnet预处理

待检测的车牌图像输入到DBnet模型前，需要做如下预处理：

- 数据标准化，输入图像每个像素值执行乘以缩放比例scale，然后减去均值mean，最后除以标准差std。
- 数据排布转换为NCHW
- resize图像尺寸HW维度为32的倍数

DBnet模型预处理过程定义在DB::Infer()函数中：

```
ErrorCode DB::Infer(const cv::Mat &img, std::vector<cv::Mat> &imgList)
{
    
    ...

    cv::Mat srcImage;
    cv::Mat resizeImg;
    img.copyTo(srcImage);

    int w = srcImage.cols;
    int h = srcImage.rows;
    float ratio = 1.f;
    int maxWH = std::max(h, w);
    if (maxWH > dbParameter.LimitSideLen)
    {
        if (h > w)
        {
            ratio = float(dbParameter.LimitSideLen) / float(h);
        }
        else
        {
            ratio = float(dbParameter.LimitSideLen) / float(w);
        }
    }  

    int resizeH = int(float(h) * ratio);
    int resizeW = int(float(w) * ratio);
    resizeH = std::max(int(round(float(resizeH) / 32) * 32), 32);
    resizeW = std::max(int(round(float(resizeW) / 32) * 32), 32);
    cv::resize(srcImage, resizeImg, cv::Size(resizeW, resizeH));

    float ratioH = float(resizeH) / float(h);
    float ratioW = float(resizeW) / float(w);

    resizeImg.convertTo(resizeImg, CV_32FC3, 1.0/255.0);
    std::vector<cv::Mat> bgrChannels(3);
    cv::split(resizeImg, bgrChannels);

    std::vector<float> mean = {0.485f, 0.456f, 0.406f};
    std::vector<float> scale = {1 / 0.229f, 1 / 0.224f, 1 / 0.225f};
    for (auto i = 0; i < bgrChannels.size(); i++)
    {
        bgrChannels[i].convertTo(bgrChannels[i], CV_32FC1, 1.0 * scale[i],
                              (0.0 - mean[i]) * scale[i]);
    }
    cv::merge(bgrChannels, resizeImg);
    int rh = resizeImg.rows;
    int rw = resizeImg.cols;
    cv::Mat inputBlob;
    inputBlob = cv::dnn::blobFromImage(resizeImg);
    
    ...
```

### SVTR预处理

SVTR模型的输入图像是DB模型检测输出裁剪的车牌区域，将裁剪图像输入到识别模型前，需要做如下预处理：

- 数据排布转换为NCHW
- resize输入图像尺寸为[1,3,48,imgW]
- 数据标准化，输入图像每个像素值执行乘以缩放比例scale，然后减去均值mean，最后除以标准差std。

SVTR模型输入预处理过程定义在SVTR::Infer()函数中：

```
ErrorCode SVTR::Infer(cv::Mat &img, std::string &resultsChar, float &resultsdScore, float &maxWHRatio)
{
    
    ...

    cv::Mat srcImage;
    cv::Mat resizeImg;
    img.copyTo(srcImage);

    float ratio = 1.f;
    int imgC = 3, imgH = 48;
    int resizeW;
    int imgW = int((48 * maxWHRatio));
    ratio = float(srcImage.cols) / float(srcImage.rows);
    if (ceil(imgH * ratio) > imgW)
    {
        resizeW = imgW;
    }
    else
    {
        resizeW = int(ceil(imgH * ratio));
    }
    cv::resize(srcImage, resizeImg, cv::Size(resizeW, imgH));
    cv::copyMakeBorder(resizeImg, resizeImg, 0, 0, 0,
                     int(imgW - resizeImg.cols), cv::BORDER_CONSTANT,
                     {127, 127, 127});

    resizeImg.convertTo(resizeImg, CV_32FC3, 1.0/255.0);
    std::vector<cv::Mat> bgrChannels(3);
    cv::split(resizeImg, bgrChannels);
    std::vector<float> mean = {0.485f, 0.456f, 0.406f};
    std::vector<float> scale = {1 / 0.229f, 1 / 0.224f, 1 / 0.225f};
    for (auto i = 0; i < bgrChannels.size(); i++)
    {
        bgrChannels[i].convertTo(bgrChannels[i], CV_32FC1, 1.0 * scale[i],
                              (0.0 - mean[i]) * scale[i]);
    }
    cv::merge(bgrChannels, resizeImg);
    cv::Mat inputBlob = cv::dnn::blobFromImage(resizeImg);
    
    ...
    
```

## 推理

### DBnet推理

DBnet模型采用动态shape推理，每次输入图像的尺寸{1,3,rh,rw}，获取推理结果后，将其转化为一维vector。

```
ErrorCode DB::Infer(const cv::Mat &img, std::vector<cv::Mat> &imgList)
{
    
    ...

    std::vector<std::size_t> inputShapeOfInfer={1,3,rh,rw};
    // 输入数据
    std::unordered_map<std::string, migraphx::shape> inputData;
    inputData[inputName]= migraphx::argument{migraphx::shape(inputShape.type(),inputShapeOfInfer), (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> inferenceResults = net.eval(inputData);
    // 如果想要指定输出节点，可以给eval()函数中提供outputNames参数来实现
    //std::vector<std::string> outputNames = {"sigmoid_0.tmp_0"};
    //std::vector<migraphx::argument> inferenceResults = net.eval(inputData, outputNames);
    
    // 获取推理结果
    migraphx::argument result = inferenceResults[0]; 

    // 转换为vector
    migraphx::shape outputShape = result.get_shape();
    int shape[]={outputShape.lens()[0],outputShape.lens()[1],outputShape.lens()[2],outputShape.lens()[3]};
    int n2 = outputShape.lens()[2];
    int n3 = outputShape.lens()[3];
    int n = n2 * n3;
    std::vector<float> out(n);
    memcpy(out.data(),result.data(),sizeof(float)*outputShape.elements());   
    out.resize(n);
    
    ...
}
```

为了得到裁剪的车牌区域图像，需要进行一系列后处理。首先对推理结果进行二值化处理获得二值化图像bitMap，然后从二值图像中获取车牌区域boxes信息，boxes信息中主要存储车牌区域矩形框的坐标，该过程定义在DBPostProcessor类中，具体代码参考BoxesFromBitmap()函数。最后对boxes信息进行过滤，过滤过程定义FilterTagDetRes()函数中，保留符合条件的boxes信息并根据坐标信息对原图进行裁剪。

```
ErrorCode DB::Infer(const cv::Mat &img, std::vector<cv::Mat> &imgList)
{
    
    ...
    
    std::vector<float> pred(n, 0.0);
    std::vector<unsigned char> cbuf(n, ' ');
    for (int i = 0; i < n; i++)
    {
        pred[i] = (float)(out[i]);
        cbuf[i] = (unsigned char)((out[i]) * 255);
    }

    cv::Mat cbufMap(n2, n3, CV_8UC1, (unsigned char *)cbuf.data());
    cv::Mat predMap(n2, n3, CV_32F, (float *)pred.data());
    const double threshold = dbParameter.BinaryThreshold * 255;
    const double maxvalue = 255;
    cv::Mat bitMap;
    cv::threshold(cbufMap, bitMap, threshold, maxvalue, cv::THRESH_BINARY);
    cv::imwrite("./det_box.jpg", bitMap);

    std::vector<std::vector<std::vector<int>>> boxes;
    DBPostProcessor postProcessor;
    boxes = postProcessor.BoxesFromBitmap(predMap, bitMap, dbParameter.BoxThreshold, dbParameter.UnclipRatio, dbParameter.ScoreMode);
    boxes = postProcessor.FilterTagDetRes(boxes, ratioH, ratioW, srcImage);

    std::vector<migraphxSamples::OCRPredictResult> ocrResults; 
    for (int i = 0; i < boxes.size(); i++) 
    {
        OCRPredictResult res;
        res.box = boxes[i];
        ocrResults.push_back(res);
    }
    Utility::sorted_boxes(ocrResults);

    for (int j = 0; j < ocrResults.size(); j++)
    {
        cv::Mat cropImg;
        cropImg = Utility::GetRotateCropImage(img, ocrResults[j].box);
        imgList.push_back(cropImg);
    }
}
```

### SVTR推理

根据SVTR输入图像预处理的结果可以看出，识别模型的输入不是固定尺寸，因此SVTR模型和DBnet模型一样需要执行动态推理，并将结果保存在一维vector中，为了得到最终的车牌识别结果，需要对out数据进行后处理，主要过程有两步：

- 第一步首先获取预测类别最大概率对应的字符索引argmaxIdx和对应最大概率maxValue
- 第二步根据字符索引信息获取对应位置的字符，然后去除重复和特殊字符#

后处理具体代码如下：

```
ErrorCode SVTR::Infer(cv::Mat &img, std::string &resultsChar, float &resultsdScore, float &maxWHRatio)
{
    
    ...

    int argmaxIdx;
    int lastIndex = 0;
    float score = 0.f;
    int count = 0;
    float maxValue = 0.0f;
    for (int j = 0; j < n2; j++)
    {
        argmaxIdx = int(std::distance(&out[(j) * n3], 
                std::max_element(&out[(j) * n3], &out[(j + 1) * n3])));
        maxValue = float(*std::max_element(&out[(j) * n3], 
                &out[(j + 1) * n3]));

        if (argmaxIdx > 0 && (!(n > 0 && argmaxIdx == lastIndex))) 
            {
                score += maxValue;
                count += 1;
                resultsChar += charactorDict[argmaxIdx];
            }
        lastIndex = argmaxIdx;
    }
    resultsdScore = score / count;

    return SUCCESS;
}
```
