# CRNN文本识别

本示例基于CRNN文本识别模型实现了两种MIGraphX推理：静态推理和动态shape推理。静态推理时CRNN模型只接受固定尺寸的图像输入，而动态shape推理则可以输入不同尺寸的图像进行识别。本份文档首先对CRNN算法进行简介，然后介绍了如何导出静态和动态的CRNN onnx格式模型，最后介绍了两种模式下的推理流程，并给出静态推理和动态推理的结果。

## 模型简介

CRNN是文本识别领域的一种经典算法。该算法的主要思想是认为文本识别需要对序列进行预测，所以采用了预测序列常用的RNN网络。算法通过CNN提取图片特征，然后采用RNN对序列进行预测，最终使用CTC方法得到最终结果。模型的主要结构包括基于CNN的图像特征提取模块以及基于双向LSTM的文字序列特征提取模块，网络结构如下图所示。

<img src="./CRNN_01.jpg" alt="CRNN_01"  />

本示例采用了如下的开源实现：https://github.com/meijieru/crnn.pytorch, 作者提供了CRNN的预训练模型，下载CRNN的预训练模型保存到CRNN工程的weights目录。

## 模型转换

本示例采用的开源实现未提供CRNN导出onnx模型的程序，所以需要在CRNN工程下创建export.py文件，在文件中添加导出crnn.onnx模型的程序，该程序根据参数输入的不同，可以分别导出满足CRNN动态和静态两种推理所需的onnx模型，代码如下。

```
import torch
import models.crnn as crnn
import argparse

def export_onnx(modelpath, is_dynamic):
    model = crnn.CRNN(32, 1, 37, 256)
    model.load_state_dict(torch.load(modelpath))
    dummy_input = torch.randn(1, 1, 32, 100)
    input_names = ["input"]
    output_names = ["output"]

    if is_dynamic:
        dynamic_axes = {"input": [3]}
        torch.onnx.export(model,
                          dummy_input,
                          "crnn_dynamic.onnx",
                          verbose=True,
                          dynamic_axes=dynamic_axes,
                          input_names=input_names,
                          output_names=output_names)
    else:
        torch.onnx.export(model,
                          dummy_input,
                          "crnn.onnx",
                          verbose=True,
                          dynamic_axes=None,
                          input_names=input_names,
                          output_names=output_names)

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('--weights', nargs='+', type=str, default='./weights/crnn.pth', help='model.pt path(s)')
    parser.add_argument('--dynamic', action="store_true", help='ONNX/TF/TensorRT: dynamic axes')
    opt = parser.parse_args()

    export_onnx(opt.weights, opt.dynamic)
```

export.py程序中dummy_input表示onnx模型的输入尺寸，数据排布为NCHW。在导出静态模型时，模型的输入尺寸是固定的，本示例为了和开源实现中提供的demo示例保持一致设为(1, 1, 32, 100)。在导出用于动态shape推理的模型时，将CRNN模型的NCH维度固定(1, 1, 32)，W维度设为动态维度dynamic_axes。

通过添加导出onnx模型程序后，按照如下步骤可将crnn.pth转化为onnx格式：

1. 安装DTK版的Pytorch，下载地址：https://cancon.hpccube.com:65024/4/main/pytorch

2. 安装依赖

```
# 进入migraphx samples工程根目录
cd <path_to_migraphx_samples>

# 进入示例程序目录
cd Python/Ocr/CRNN

# 环境配置
pip install -r requirements.txt
```

3. 静态模型转换

```
python export.py --weights ./weights/crnn.pth
```

4. 动态模型转换

```
python export.py --weights ./weights/crnn.pth --dynamic
```

模型导出后，可以通过netron工具：https://netron.app/ 进行模型可视化，检查模型的输入输出。**动态模型通过设置既可以执行动态推理，也可以执行静态推理，本示例中的静态推理和动态shape推理则是基于导出的动态模型实现。**

## 模型初始化

本示例使用导出的动态CRNN模型同时执行静态和动态推理。模型加载成功后，模型初始化主要调用MIGraphX的parse_onnx()、get_parameter_shapes()、compile()函数实现对模型的加载解析、获取模型输入属性和编译处理。静态推理和动态推理在这个过程中主要区别在于parse_onnx()函数的输入不同。

1. 静态推理


   ```
   ErrorCode Crnn::Initialize(InitializationParameterOfOcr initializationParameterOfOcr, bool dynamic)
   {		
           ...
           
           migraphx::onnx_options onnx_options;
           onnx_options.map_input_dims["input"]={1,1,32,100};
           
           net = migraphx::parse_onnx(modelPath, onnx_options);       
   
           ...
   }
   ```

静态推理将模型的输入设为一个固定的尺寸{1,1,32,100}，每次执行静态推理都必须将输入图像resize到设定的尺寸。

2. 动态shape推理

   ```
   ErrorCode Crnn::Initialize(InitializationParameterOfOcr initializationParameterOfOcr, bool dynamic)
   {
           ...
           
           migraphx::onnx_options onnx_options;
           onnx_options.map_input_dims["input"]={1,1,32,512};
   
           net = migraphx::parse_onnx(modelPath, onnx_options);       
   
           ...
   }
   ```

动态shape推理需要设定一个最大shape，本示例设为{1,1,32,512}，并在parse_onnx()函数对crnn动态模型解析时作为参数输入。注意每次输入图像的尺寸可以不像静态推理一样需要固定，但是输入尺寸必须要小于设定的最大shape。

   ## 预处理

静态推理将待识别的文本图像输入模型前，需要对图像做如下预处理：

   - 转换为单通道图像

   - resize到模型初始化设定的尺寸(100,  32)

   - 将像素值归一化到[-1, 1]

   - 转换数据排布为NCHW

动态推理将待识别的文本图像输入模型前，需要对图像做如下预处理：

   - 转换为单通道图像
   - resize到(widthRaw, 32)，其中widthRaw为待识别图像原宽度
   - 将像素值归一化到[-1, 1]
   - 转换数据排布为NCHW

   ```
   ErrorCode Crnn::Infer(const cv::Mat &srcImage, std::vector<char> &resultsChar, bool raw, bool dynamic)
   {
       ...
   
       cv::Mat inputImage, inputBlob;
       cv::cvtColor(srcImage, inputImage, CV_BGR2GRAY);
       
       int height, width, widthRaw;
       widthRaw = inputImage.cols;
       if(dynamic)
       {
           cv::resize(inputImage, inputImage, cv::Size(widthRaw, 32));
           height = inputImage.rows, width = inputImage.cols;
       }
       else
       {
           cv::resize(inputImage, inputImage, cv::Size(100, 32));
           height = inputImage.rows, width = inputImage.cols;
       }
       
       // 数据排布转换
       inputBlob = cv::dnn::blobFromImage(inputImage);
       
       for(int i=0; i<width * height; i++)
       {
           *((float*)inputBlob.data+i) = ((*((float*)inputBlob.data+i))/255.f - 0.5)/0.5;
       }
       
       ...
       
   }
   ```

对比两种推理模式的预处理方式可以看出，动态推理的输入图像的尺寸是不固定的，静态推理需要固定尺寸的输入。

   ## 推理

   ### 静态推理

静态推理只需在文本图像经过预处理之后，输入到模型中执行推理，就可以得到推理结果。

   ```
   ErrorCode Crnn::Infer(const cv::Mat &srcImage, std::vector<char> &resultsChar, bool raw)
   {
       ...
           
       // 输入数据
       std::unordered_map<std::string, migraphx::shape> inputData;
       inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};
   
       // 推理
       std::vector<migraphx::argument> inferenceResults = net.eval(inputData);
   
       // 获取推理结果
       std::vector<cv::Mat> outs;
       migraphx::argument result = inferenceResults[0]; 
   
       // 转换为cv::Mat
       migraphx::shape outputShape = result.get_shape();
       int shape[]={outputShape.lens()[0],outputShape.lens()[1],outputShape.lens()[2]};
       cv::Mat out(3,shape,CV_32F);
       memcpy(out.data,result.data(),sizeof(float)*outputShape.elements());
       outs.push_back(out);
       
       ...
   }
   ```

inferenceResults是crnn.onnx模型的MIGraphX推理结果，CRNN模型包含一个输出，所以result等于inferenceResults[0]，result包含三个维度：outputShape.lens()[0]=26可以认为是对应原图的26份纵向分割，也就是这张图片要被从左到右预测的次数，每次预测输出一个特征向量，outputShape.lens()[1]=1表示特征向量的宽度，outputShape.lens()[2]=37表示特征向量的长度。其中37=10+26+1，10、26、1分别表示10个数字(0123456789)、26个英文字母(abcdefghijklmnopqrstuvwxyz)和一个空格“-”。获取MIGraphX推理结果之后需要进行后处理得到最终文本识别的结果。后处理包括两个步骤：

   1. 第一步通过判断每次预测输出的特征向量中得分最高字符对应的位置索引信息maxIdx，并将其保存在数组predChars中

   ```
   ErrorCode Crnn::Infer(const cv::Mat &srcImage, std::vector<char> &resultsChar, bool raw)
   {
       ...
       
       std::vector<int> predChars; 
       const std::string alphabet = "-0123456789abcdefghijklmnopqrstuvwxyz";
   
       //获取字符索引序列
       for(uint i = 0; i < outs[0].size[0]; i++)
       {
           cv::Mat scores = Mat(1,outs[0].size[2],CV_32F,outs[0].ptr<float>(i));
           cv::Point charIdPoint;
           double maxCharScore;
           cv::minMaxLoc(scores, 0, &maxCharScore, 0, &charIdPoint);
           int maxIdx = charIdPoint.x;
           predChars.push_back(maxIdx);
       }
       
       ...
   }
   ```

   2. 第二步根据predChars保存的字符索引信息从alphabet中提取对应的字符，并且当布尔值raw为真时，数组resultsChar保存包括空格、重复字符在内的所有输出，当布尔值raw为假时，需要做去除空格和重复字符处理，数组resultsChar保存最终的文本识别结果。

   ```
   ErrorCode Crnn::Infer(const cv::Mat &srcImage, std::vector<char> &resultsChar, bool raw)
   {
       ...
       
       //字符转录处理
       for(uint i=0; i<predChars.size(); i++)
       {
           if(raw)
           {
               resultsChar.push_back(alphabet[predChars[i]]);
           }
           else
           {
               if(predChars[i] != 0)
               {
                   if(!(i > 0 && predChars[i-1]==predChars[i]))
                   {
                       resultsChar.push_back(alphabet[predChars[i]]);
                   }
               }
           }
       }
   
       ...
   }
   ```

### 动态shape推理

动态shape推理需要处理多张图像，每次输入新图像执行与静态推理一致的操作，相关过程定义在Sample.cpp文件中，代码如下：

```
void Sample_Crnn_Dynamic()
{
    
    ...

    // 读取多张测试图像
    std::vector<cv::Mat> srcImages;
    cv::String folder = "../Resource/Images/DynamicPic";
    std::vector<cv::String> imagePathList;
    cv::glob(folder,imagePathList);
    for (int i = 0; i < imagePathList.size(); ++i)
    {
        cv:: Mat srcImage=cv::imread(imagePathList[i], 1);
        srcImages.push_back(srcImage); 
    }

    // 获取推理结果
    LOG_INFO(stdout,"========== Ocr Results ==========\n");
    for(int i=0; i<srcImages.size(); ++i)
    {
        // 执行动态shape推理
        std::vector<char> resultSim;
        crnn.Infer(srcImages[i],resultSim, false, true);

		// 打印输出结果
        for(int i = 0; i < resultSim.size(); i++)
        {
            std::cout << resultSim.at(i);
        }
        std::cout << std::endl;
    }
    
    ...
    
}
```
