# PaddleOCR车牌识别

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

## 模型简介

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/`。

注：模型转换时的环境配置与程序运行所需环境一致，因此运行示例时只需安装MIGraphX。

## 模型初始化

模型初始化目的主要时对模型解析、获取模型的输入属性以及编译。由于本示例中采用动态shape推理进行车牌识别，因此解析模型时需要指定推理的最大shape。DB检测模型最大输入shape设为[1,3,2496,2496]，SVTR识别模型最大输入shape设为[1,3,48,320]。

```
class det_rec_functions(object):

    ...

        # 解析检测模型
        detInput = {"x":[1,3,2496,2496]}
        self.modelDet = migraphx.parse_onnx(self.det_file, map_input_dims=detInput)
        self.inputName = self.modelDet.get_parameter_names()[0]
        self.inputShape = self.modelDet.get_parameter_shapes()[self.inputName].lens()

        print("DB inputName:{0} \nDB inputShape:{1}".format(self.inputName, self.inputShape))

        # 模型编译
        self.modelDet.compile(t=migraphx.get_target("gpu"), device_id=0)  # device_id: 设置GPU设备，默认为0号
        print("Success to compile DB")

        # 解析识别模型
        recInput = {"x":[1,3,48,320]}
        self.modelRec = migraphx.parse_onnx(self.rec_file, map_input_dims=recInput)
        self.inputName = self.modelRec.get_parameter_names()[0]
        self.inputShape = self.modelRec.get_parameter_shapes()[self.inputName].lens()
        
        print("SVTR inputName:{0} \nSVTR inputShape:{1}".format(self.inputName, self.inputShape))

        # 模型编译
        self.modelRec.compile(t=migraphx.get_target("gpu"), device_id=0)  # device_id: 设置GPU设备，默认为0号
        print("Success to compile SVTR")
        
    ...
```

## 预处理

### DBnet预处理

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

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

检测图像预处理上述操作分别定义在NormalizeImage、ToCHWImage、DetResizeForTest类中，输入图像尺寸resize相关代码如下：

```
class DetResizeForTest(object):
    
    ...
    
    def resize_image_type0(self, img):
        """
        resize image to a size multiple of 32 which is required by the network
        args:
            img(array): array with shape [h, w, c]
        return(tuple):
            img, (ratio_h, ratio_w)
        """
        limit_side_len = self.limit_side_len
        h, w, _ = img.shape

        # limit the max side
        if max(h, w) > limit_side_len:
            if h > w:
                ratio = float(limit_side_len) / h
            else:
                ratio = float(limit_side_len) / w
        else:
            ratio = 1.
        resize_h = int(h * ratio)
        resize_w = int(w * ratio)


        resize_h = int(round(resize_h / 32) * 32)
        resize_w = int(round(resize_w / 32) * 32)

        try:
            if int(resize_w) <= 0 or int(resize_h) <= 0:
                return None, (None, None)
            img = cv2.resize(img, (int(resize_w), int(resize_h)))
        except:
            print(img.shape, resize_w, resize_h)
            sys.exit(0)
        ratio_h = resize_h / float(h)
        ratio_w = resize_w / float(w)
        
        return img, [ratio_h, ratio_w]
```

其中limit_side_len参数主要用来限制最大输入图像的尺寸。

### SVTR预处理

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

- 像素值归一化到[-1, 1]
- 数据排布转换为NCHW
- resize输入图像尺寸为[1,3,48,imgW]

其中resize输入图像的W维度尺寸大小取决于 max_wh_ratio参数设置，同时W维度为SVTR模型推理的动态shape维度。

```
class det_rec_functions(object):

	...
	
    def resize_norm_img(self, img, max_wh_ratio):
        imgC, imgH, imgW = [int(v) for v in "3, 48, 320".split(",")]
        assert imgC == img.shape[2]
        imgW = int((48 * max_wh_ratio))
        h, w = img.shape[:2]
        ratio = w / float(h)
        if math.ceil(imgH * ratio) > imgW:
           resized_w = imgW
        else:
           resized_w = int(math.ceil(imgH * ratio))
        resized_image = cv2.resize(img, (resized_w, imgH))
        
        resized_image = resized_image.astype('float32')
        resized_image = resized_image.transpose((2, 0, 1)) / 255
        resized_image -= 0.5
        resized_image /= 0.5
        padding_im = np.zeros((imgC, imgH, imgW), dtype=np.float32)
        padding_im[:, :, 0:resized_w] = resized_image
        
        return padding_im
        
    ...
```

## 推理

### DBnet推理

获取MIGraphX的推理结果之后，首先需要进行如下后处理才能得到文本框boxes：

- 第一步对概率图进行二值化处理，利用DBPostProcess类中的二值化阈值thresh=0.3生成二值化图bitmap，利用cv2.findContours()函数从二值化图中寻找车牌所在区域的轮廓，其中max_candidates阈值用来限制轮廓点的数量；
- 第二步使用get_mini_boxes()函数处理获取车牌轮廓的最小外接矩形框的坐标points、矩形框的最小边长sside，通过阈值min_size筛选上一步中的矩形框，滤掉sside小于min_size的矩形框；
- 第三步保留下来的矩形框使用box_score_fast()函数进行计算预测概率score，进一步使用box_thresh阈值过滤scores小于该阈值的矩形框；
- 第四步对保留下来的矩形框进行反向shrink操作，重复进行第二步内容，并将坐标转换为原图坐标，经过上述处理获取了输入图像中车牌区域的坐标集和boxes。

具体代码如下：

```
# 检测结果后处理
class DBPostProcess(object):

	...

    def boxes_from_bitmap(self, pred, _bitmap, dest_width, dest_height):
        '''
        _bitmap: single map with shape (1, H, W),
                whose values are binarized as {0, 1}
        '''

        bitmap = _bitmap
        height, width = bitmap.shape

        outs = cv2.findContours((bitmap * 255).astype(np.uint8), cv2.RETR_LIST,
                                cv2.CHAIN_APPROX_SIMPLE)
        if len(outs) == 3:
            img, contours, _ = outs[0], outs[1], outs[2]
        elif len(outs) == 2:
            contours, _ = outs[0], outs[1]

        num_contours = min(len(contours), self.max_candidates)

        boxes = []
        scores = []
        for index in range(num_contours):
            contour = contours[index]
            points, sside = self.get_mini_boxes(contour)
            if sside < self.min_size:
                continue
            points = np.array(points)
            score = self.box_score_fast(pred, points.reshape(-1, 2))
            if self.box_thresh > score:
                continue

            box = self.unclip(points).reshape(-1, 1, 2)
            box, sside = self.get_mini_boxes(box)
            if sside < self.min_size + 2:
                continue
            box = np.array(box)

            box[:, 0] = np.clip(
                np.round(box[:, 0] / width * dest_width), 0, dest_width)
            box[:, 1] = np.clip(
                np.round(box[:, 1] / height * dest_height), 0, dest_height)
            boxes.append(box.astype(np.int16))
            scores.append(score)
        return np.array(boxes, dtype=np.int16), scores
        
        ...
```

获取了模型预测的文本boxes之后，下一步就是需要对文本boxes进行过滤，主要步骤有：

- 把boxes中点坐标按顺时针排序；
- 对点坐标进行裁剪，使其坐标不超过图片范围；
- 求矩阵的二范式，即求两点间的直线距离；
- 将两点间的直线距离小于3的过滤掉。

```
class det_rec_functions(object):
	
	...
	
	def filter_tag_det_res(self, dt_boxes, image_shape):
        img_height, img_width = image_shape[0:2]
        dt_boxes_new = []
        for box in dt_boxes:
            box = self.order_points_clockwise(box)
            box = self.clip_det_res(box, img_height, img_width)
            rect_width = int(np.linalg.norm(box[0] - box[1]))
            rect_height = int(np.linalg.norm(box[0] - box[3]))
            if rect_width <= 3 or rect_height <= 3:
                continue
            dt_boxes_new.append(box)
        dt_boxes = np.array(dt_boxes_new)
        return dt_boxes
    
    def sorted_boxes(self, dt_boxes):
        """
        Sort text boxes in order from top to bottom, left to right
        args:
            dt_boxes(array):detected text boxes with shape [4, 2]
        return:
            sorted boxes(array) with shape [4, 2]
        """
        num_boxes = dt_boxes.shape[0]
        sorted_boxes = sorted(dt_boxes, key=lambda x: (x[0][1], x[0][0]))
        _boxes = list(sorted_boxes)

        for i in range(num_boxes - 1):
            if abs(_boxes[i + 1][0][1] - _boxes[i][0][1]) < 10 and \
                    (_boxes[i + 1][0][0] < _boxes[i][0][0]):
                tmp = _boxes[i]
                _boxes[i] = _boxes[i + 1]
                _boxes[i + 1] = tmp
        return _boxes
	
	...
```

### SVTR后处理

识别模型SVTR的后处理的目的是对推理的结果进行解码，从而获取字符识别的结果。识别模型预测的字符类别保存在ppocr_keys_v1.txt文件中，共有6625个文本字符，其中第一个字符为#，最后一个字符为空格，后处理过程定义在process_pred()类中。MIGraphX的推理结果pred包含三个维度，第一个维度表示batch信息，第二个维度表示当前图像预测的字符数量，其中包括重复字符和空格，第三个维度表示预测的类别概率。因此后处理第一步首先获取预测类别最大概率对应的字符索引和对应最大概率，然后第二步根据字符索引信息获取对应位置的字符，然后去除重复和特殊字符，最后获得当前图像的字符识别结果。

```
class process_pred(object):
   
    ...

    def decode(self, text_index, text_prob=None, is_remove_duplicate=False):
        result_list = []
        ignored_tokens = [0]
        batch_size = len(text_index)
        for batch_idx in range(batch_size):
            char_list = []
            conf_list = []
            for idx in range(len(text_index[batch_idx])):
                if text_index[batch_idx][idx] in ignored_tokens:
                    continue
                if is_remove_duplicate:
                    if idx > 0 and text_index[batch_idx][idx - 1] == text_index[batch_idx][idx]:
                        continue
                char_list.append(self.character[int(text_index[batch_idx][idx])])
                if text_prob is not None:
                    conf_list.append(text_prob[batch_idx][idx])
                else:
                    conf_list.append(1)
            text = ''.join(char_list)
            result_list.append((text, np.mean(conf_list)))
        return result_list

    def __call__(self, preds, label=None):
        if not isinstance(preds, np.ndarray):
            preds = np.array(preds)
        preds_idx = preds.argmax(axis=2)
        preds_prob = preds.max(axis=2)
        text = self.decode(preds_idx, preds_prob, is_remove_duplicate=True)
        if label is None:
            return text
        label = self.decode(label)
        return text, label
```
