样例介绍
文本识别应用,即标识图片中文本框的位置并识别文本框内的内容。
图1 文本识别样例
本例中使用的是Mindspore框架的CTPN模型与ONNX框架的SVTR模型,其中CTPN模型为文本检测模型,能够检测出文本所在区域;SVTR模型为文字识别模型,可识别文字内容。用户可以直接使用训练好的开源模型,也可以基于开源模型的源码进行修改、重新训练,还可以基于算法、框架构建适合的模型。
CTPN模型的基本介绍如下。
- 输入数据:BGR格式、任意分辨率的输入图片。
- 输出数据:图片中文本框的4个顶点的坐标。
SVTR模型的基本介绍如下。
- 输入数据:BGR格式、任意分辨率的输入图片。
- 输出数据:图片中字符的内容。
说明
图片中文本框的4个顶点的坐标为缩放后的图片中的坐标,需要映射回原图的坐标。
业务模块介绍
从功能模块介绍以及业务流程说明方面来介绍一下本案例相关的业务模块。
图2 案例流程图
- 图片输入:收集待识别的文本数据集做为输入数据。
- 图片预处理:图片通过OpenCV解码,再将图片缩放至CTPN模型要求的输入大小,并把图片进行归一化。
- 检测模型推理:CTPN模型识别图片中的文本框。
- 模型后处理:处理推理结果,获取所有文本框的位置以及对应的置信度。
- 切割文本框:将原图片根据文本框的位置切割成仅包含文本框的子图。
- 子图预处理:将图片缩放至SVTR模型要求的输入大小,并把图片进行归一化。
- 识别模型推理:SVTR模型识别图片中的文本。
- 识别模型后处理:处理推理结果,根据字典查询结果拼接出文本框中的内容。
获取代码
- 获取代码文件。
单击获取链接或使用wget命令,下载代码文件压缩包,以root用户登录开发者套件。
wget https://ascend-devkit-tool.obs.cn-south-1.myhuaweicloud.com/models/ocr_acl_sample.zip
- 将“ocr_acl_sample.zip”压缩包上传到开发者套件,解压并进入解压后的目录。
unzip ocr_acl_sample.zip cd ocr_acl_sample
代码目录结构如下所示,按照正常开发流程,需要将框架模型文件转换成昇腾AI处理器支持推理的om格式模型文件,鉴于当前是入门内容,用户可直接获取已转换好的om模型进行推理。
ocr_acl_sample ├── datas #推理图片目录 │ └── test.png ├── models #om模型放置的目录 │ ├── ctpn.om #检测模型(CTPN)的om文件 │ └── svtr.om #识别模型(SVTR)的om文件 └── src │ └── utils.py #主程序以及模型前后处理相关函数 ├── main.py # 推理主程序 ├── ppocr_keys_v1.txt # 识别模型(SVTR)所需标签文件
代码解析
开发代码过程中,在“ocr_acl_sample/main.py”文件中已包含读入数据、前处理、推理、后处理等功能,串联整个应用代码逻辑,此处仅对代码进行解析。
- 在“main.py”文件的开头有如下代码,用于导入第三方库与调用AscendCL接口推理所需文件。
import json import math import os from argparse import ArgumentParser from abc import abstractmethod, ABC import cv2 # 图片处理三方库,用于对图片进行前后处理 import numpy as np # 用于对多维数组进行计算 from PIL import Image, ImageDraw # 图像处理库,此处用于读入图片并画出结果 import acl from src.utils import get_images_from_path, img_read, detect SUCCESS = 0 # 成功状态值 FAILED = 1 # 失败状态值 ACL_MEM_MALLOC_NORMAL_ONLY = 2 # 申请内存策略, 仅申请普通页
- 获取输入的参数。
def parse_args(): parser = ArgumentParser() parser.add_argument('--image_path', type=str, required=True) # 获取推理图片/文件夹的路径 parser.add_argument('--det_model_path', type=str, required=True) # 获取检测模型(CTPN)的路径 parser.add_argument('--rec_model_path', type=str, required=True) # 获取识别模型(SVTR)的路径 parser.add_argument('--rec_model_dict', type=str, required=True) # 获取识别模型字典的路径 parser.add_argument('--device_id', type=int, required=False, default=0) # 获取推理使用的NPU的编号 return parser.parse_args()
- 资源初始化。
使用AscendCL接口开发应用时,必须先初始化AscendCL ,否则可能会导致后续系统内部资源初始化出错,进而导致其它业务异常。有初始化就要去初始化,在确定完成了AscendCL的所有调用之后,或者进程退出之前,需调用接口实现AscendCL去初始化。以下两个函数实现了acl资源的初始化和去初始化。
def init_acl(device_id): acl.init() # 初始化 acl ret = acl.rt.set_device(device_id) # 初始化 NPU if ret: raise RuntimeError(ret) context, ret = acl.rt.create_context(device_id) # 创建context if ret: raise RuntimeError(ret) print('Init ACL Successfully') return context def deinit_acl(context, device_id): ret = acl.rt.destroy_context(context) # 销毁context if ret: raise RuntimeError(ret) ret = acl.rt.reset_device(device_id) # 重置 NPU if ret: raise RuntimeError(ret) ret = acl.finalize() # 检查acl相关资源是否全部释放 if ret: raise RuntimeError(ret) print('Deinit ACL Successfully')
- 定义模型资源相关基类,承担初始化模型资源、创建输入输出数据集、执行推理、解析输出、释放模型资源等功能。之后的CTPN与SVTR模型推理可继承此类。
class Model(ABC): def __init__(self, model_path): print(f"load model {model_path}") self.model_path = model_path # 模型路径 self.model_id = None # 模型 id self.input_dataset = None # 输入数据结构 self.output_dataset = None # 输出数据结构 self.model_desc = None # 模型描述信息 self._input_num = 0 # 输入数据个数 self._output_num = 0 # 输出数据个数 self._output_info = [] # 输出信息列表 self._is_released = False # 资源是否被释放 self.model_height = None # 模型输入图片高度 self.model_width = None # 模型输入图片宽度 self.model_channel = None # 模型输入图片通道数 self._init_resource() def _init_resource(self): ''' 初始化模型、输入、输出相关资源 ''' print("Init model resource") # 加载模型文件 self.model_id, ret = acl.mdl.load_from_file(self.model_path) # 加载模型 self.model_desc = acl.mdl.create_desc() # 初始化模型信息对象 ret = acl.mdl.get_desc(self.model_desc, self.model_id) # 根据模型获取描述信息 print("[Model] Model init resource stage success") # 创建模型输出 dataset 结构 self._output_num = acl.mdl.get_num_outputs(self.model_desc) # 获取模型输出个数 self._gen_output_dataset() # 创建模型输出dataset结构 for i in range(self._output_num): dims = acl.mdl.get_output_dims(self.model_desc, i)[0]["dims"] # 获取每个输出的维度 datatype = acl.mdl.get_output_data_type(self.model_desc, i) # 获取每个输出的数据类型 self._output_info.append({"shape": tuple(dims), "type": datatype}) # 将维度和数据类型加入输出信息列表 # 获取模型输入个数, 为创建输入dataset结构做准备 self._input_num = acl.mdl.get_num_inputs(self.model_desc) # 获取模型输入的shape,为预处理做准备 dims, ret = acl.mdl.get_input_dims_v2(self.model_desc, 0) if ret: raise RuntimeError(ret) dims = dims['dims'] # 获取模型输入图片的通道数、高、宽 self.model_channel = dims[1] self.model_height = dims[2] self.model_width = dims[3] def _gen_output_dataset(self): ''' 组织输出数据的dataset结构 ''' ret = SUCCESS self.output_dataset = acl.mdl.create_dataset() # 创建输出dataset结构 for i in range(self._output_num): temp_buffer_size = acl.mdl.get_output_size_by_index(self.model_desc, i) # 获取模型输出个数 temp_buffer, ret = acl.rt.malloc(temp_buffer_size, ACL_MEM_MALLOC_NORMAL_ONLY) # 为每个输出申请device内存 dataset_buffer = acl.create_data_buffer(temp_buffer, temp_buffer_size) # 创建输出的data buffer结构,将申请的内存填入data buffer _, ret = acl.mdl.add_dataset_buffer(self.output_dataset, dataset_buffer) # 将 data buffer 加入输出dataset if ret == FAILED: self._release_dataset(self.output_dataset) # 失败时释放dataset print("[Model] create model output dataset success") def _gen_input_dataset(self, input_list): ''' 组织输入数据的dataset结构 ''' ret = SUCCESS self.input_dataset = acl.mdl.create_dataset() # 创建输入dataset结构 for i in range(self._input_num): item = input_list[i] # 获取第 i 个输入数据 data = acl.util.bytes_to_ptr(item.tobytes()) # 获取输入数据字节流 size = item.size * item.itemsize # 获取输入数据字节数 dataset_buffer = acl.create_data_buffer(data, size) # 创建输入dataset buffer结构, 填入输入数据 _, ret = acl.mdl.add_dataset_buffer(self.input_dataset, dataset_buffer) # 将dataset buffer加入dataset if ret == FAILED: self._release_dataset(self.input_dataset) # 失败时释放dataset print("[Model] create model input dataset success") def _unpack_bytes_array(self, byte_array, shape, datatype): ''' 将内存不同类型的数据解码为numpy数组 ''' np_type = None # 获取输出数据类型对应的numpy数组类型和解码标记 if datatype == 0: # ACL_FLOAT np_type = np.float32 elif datatype == 1: # ACL_FLOAT16 np_type = np.float16 elif datatype == 3: # ACL_INT32 np_type = np.int32 elif datatype == 8: # ACL_UINT32 np_type = np.uint32 elif datatype == 12: np_type = np.bool8 else: print("unsurpport datatype ", datatype) return # 将解码后的数据组织为numpy数组,并设置shape和类型 return np.frombuffer(byte_array, dtype=np_type).reshape(shape) def _output_dataset_to_numpy(self): ''' 将模型输出解码为numpy数组 ''' dataset = [] # 遍历每个输出 for i in range(self._output_num): buffer = acl.mdl.get_dataset_buffer(self.output_dataset, i) # 从输出dataset中获取buffer data = acl.get_data_buffer_addr(buffer) # 获取输出数据内存地址 size = acl.get_data_buffer_size(buffer) # 获取输出数据字节数 narray = acl.util.ptr_to_bytes(data, size) # 将指针转为字节流数据 # 根据模型输出的shape和数据类型,将内存数据解码为numpy数组 output_nparray = self._unpack_bytes_array(narray, self._output_info[i]["shape"], self._output_info[i]["type"]) dataset.append(output_nparray) return dataset def execute(self, input_list): '''创建输入dataset对象, 推理完成后, 将输出数据转换为numpy格式''' self._gen_input_dataset(input_list) # 创建模型输入dataset结构 ret = acl.mdl.execute(self.model_id, self.input_dataset, self.output_dataset) # 调用离线模型的execute推理数据 out_numpy = self._output_dataset_to_numpy() # 将推理输出的二进制数据流解码为numpy数组, 数组的shape和类型与模型输出规格一致 return out_numpy def release(self): ''' 释放模型相关资源 ''' if self._is_released: return print("Model start release...") self._release_dataset(self.input_dataset) # 释放输入数据结构 self.input_dataset = None # 将输入数据置空 self._release_dataset(self.output_dataset) # 释放输出数据结构 self.output_dataset = None # 将输出数据置空 if self.model_id: ret = acl.mdl.unload(self.model_id) # 卸载模型 if self.model_desc: ret = acl.mdl.destroy_desc(self.model_desc) # 释放模型描述信息 self._is_released = True print("Model release source success") def _release_dataset(self, dataset): ''' 释放 aclmdlDataset 类型数据 ''' if not dataset: return num = acl.mdl.get_dataset_num_buffers(dataset) # 获取数据集包含的buffer个数 for i in range(num): data_buf = acl.mdl.get_dataset_buffer(dataset, i) # 获取buffer指针 if data_buf: ret = acl.destroy_data_buffer(data_buf) # 释放buffer ret = acl.mdl.destroy_dataset(dataset) # 销毁数据集 @abstractmethod def infer(self, inputs): # 保留接口, 子类必须重写 pass
- 定义CTPN模型的具体推理功能,包含前处理、推理、后处理等功能,得到模型推理输出结果。CTPN继承自 src 文件夹下 model.py 中的Model基类,并重写了其infer功能。此处的Model基类包含了初始化模型资源、创建输入输出数据集、执行推理、解析输出、释放模型资源等功能,具体可参见步骤4。另外,此基类的定义也与目标检测应用样例中的步骤6相似(有少许不同),开发者可根据需要改变此基类的定义。
class CTPN(Model): def __init__(self, model_path): super().__init__(model_path) self.mean = np.array([123.675, 116.28, 103.53]).reshape((1, 1, 3)).astype(np.float32) # 定义均值 self.std = np.array([58.395, 57.12, 57.375]).reshape((1, 1, 3)).astype(np.float32) # 定义标准差 def infer(self, inputs): # 前处理 dst_img = cv2.resize(inputs, (int(self.model_width), int(self.model_height))).astype(np.float32) # 缩放图片至模型要求的shape,并将数据类型转为fp32 dst_img -= self.mean # 标准化 dst_img /= self.std dst_img = dst_img.transpose((2, 0, 1)) # 更改图片格式从 HWC 到 CHW dst_img = np.expand_dims(dst_img, axis=0) # 更改图片格式从 CHW to NCHW dst_img = np.ascontiguousarray(dst_img).astype(np.float32) # 转换到连续内存 # 推理 output = self.execute([dst_img, ]) # 后处理 proposal = output[0] # 获取 proposals proposal_mask = output[1] # 获取proposal的mask all_box_tmp = proposal all_mask_tmp = np.expand_dims(proposal_mask, axis=1) # 将mask扩充一维 using_boxes_mask = all_box_tmp * all_mask_tmp # proposal * mask 获取到真正的proposal textsegs = using_boxes_mask[:, 0:4].astype(np.float32) # 区分出分割信息与置信度信息 scores = using_boxes_mask[:, 4].astype(np.float32) bboxes = detect(textsegs, scores[:, np.newaxis], (self.model_height, self.model_width)) # 根据分割信息与置信度信息计算出文本框 return bboxes
- 与上一步骤相似,继承自 src 文件夹下 model.py 中的Model基类,定义SVTR模型推理功能。
class SVTR(Model): def __init__(self, model_path, dict_path): super().__init__(model_path) self.labels = [''] with open(dict_path, 'r') as f: # 逐行读入识别模型的字典文件,加入标签列表 labels = f.readlines() for char in labels: self.labels.append(char.strip()) self.labels.append(' ') self.scale = np.float32(1 / 255) # 用于缩放图片像素值域 self.mean = 0.5 # 设定均值与方差 self.std = 0.5 def infer(self,inputs): # 前处理:缩放图片至模型输入要求的shape h, w, _ = inputs.shape ratio = w / h if math.ceil(ratio * self.model_height) > self.model_width: resize_w = self.model_width else: resize_w = math.ceil(ratio * self.model_height) img = cv2.resize(inputs, (resize_w, self.model_height)) _, w, _ = img.shape padding_w = self.model_width - w # 计算图片需要padding的长度 img = cv2.copyMakeBorder(img, 0, 0, 0, padding_w, cv2.BORDER_CONSTANT, value=0.).astype(np.float32) # 使用opencv进行padding图片,并将图片转为 fp32格式 img *= self.scale img -= self.mean # 标准化 img /= self.std dst_img = img.transpose((2, 0, 1)) # 更改图片格式从 HWC 到 CHW dst_img = np.expand_dims(dst_img, axis=0) # 更改图片格式从 CHW to NCHW dst_img = np.ascontiguousarray(dst_img).astype(np.float32) # 转换到连续内存 # 推理 output = self.execute([dst_img, ]) # 后处理 output = np.argmax(output[0], axis=2).reshape(-1) # 将输出结果进行argmax处理,并reshape ans = [] # 初始化字符列表 last_char = '' for i, char in enumerate(output): # 逐个解析输出,并加入字符列表 if char and self.labels[char] != last_char: ans.append(self.labels[char]) last_char = self.labels[char] return ''.join(ans)
- 定义一个acl初始化与运行main函数。
if __name__ == '__main__': args = parse_args() # 读取输入参数 context, stream = init_acl(args.device_id) # 初始化acl相关资源 image_lst = get_images_from_path(args.image_path) # 获取输入图片路径中的所有图片 det_model = CTPN(model_path=args.det_model_path) # 创建CTPN模型对象 rec_model = SVTR(model_path=args.rec_model_path, dict_path=args.rec_model_dict) # 创建SVTR模型对象 infer_res = [] # 初始化推理结果列表 if not os.path.exists('infer_result'): # 创建保存推理结果的目录 os.makedirs('infer_result') # 对每一张图片进行推理 for image in image_lst: ans = {} # 初始化结果字典 img_src = img_read(image) # 使用opencv对图片及进行解码 basename = os.path.basename(image) # 获取图片名称(带后缀) print(f'start infer image: {basename}') name, ext = os.path.splitext(basename) # 获取图片名称与后缀 image_h, image_w = img_src.shape[:2] # 获取图片的宽高 bboxes = det_model.infer(img_src) # 利用检测模型推理,得到结果 im = Image.open(image) # 打开图片 draw = ImageDraw.Draw(im) ans['image_name'] = basename ans['result'] = [] # 初始化每张图片的推理结果 # 遍历检测模型的结果 for bbox in bboxes: bbox_detail = {} # 将文本框的坐标映射回原图的坐标 x1 = int(bbox[0] / det_model.model_width * image_w) y1 = int(bbox[1] / det_model.model_height * image_h) x2 = int(bbox[2] / det_model.model_width * image_w) y2 = int(bbox[3] / det_model.model_height * image_h) draw.line([(x1, y1), (x1, y2), (x2, y2), (x2, y1), (x1, y1)], fill='red', width=2) # 在原图上画出检测框 bbox_detail['bbox'] = [x1, y1, x1, y2, x2, y2, x2, y1] crop_img = img_src[y1:y2, x1:x2] # 切割子图 bbox_detail['text'] = rec_model.infer(crop_img) # 利用识别模型推理,得到结果 print('文字结果:',bbox_detail['text']) ans['result'].append(bbox_detail) # 保存推理结果 infer_res.append(ans) # 将本张图片推理结果加入 infer_res im.save(os.path.join('infer_result', name + '_res' + ext)) # 保存推理结果图片 det_model.release() # 释放模型资源 rec_model.release() deinit_acl(context, stream, args.device_id) # 释放acl资源 with open(os.path.join('infer_result', 'infer_result.json'), 'w') as f: # 保存推理结果至json文件 json.dump(infer_res, f, indent=4)
运行推理
在“ocr_acl_sample”目录下,执行以下命令。
python ./main.py --det_model_path=./models/ctpn.om --rec_model_path=./models/svtr.om --rec_model_dict=./ppocr_keys_v1.txt --image_path=./datas
说明
- 如果执行脚本报错ModuleNotFoundError: No module named 'PIL',则表示缺少Pillow库,请使用pip3 install Pillow --user命令安装Pillow库。
- 如果执行脚本报错ModuleNotFoundError: No module named 'cv2',则表示缺少OpenCV库,请使用pip3 install opencv-python --user命令安装OpenCV库。
- 如果Python导入OpenCV模块报错“ImportError: libGL.so.1: cannot open shared object file: No such file or directory”,可参考Python导入opencv模块报错,提示:ImportError: libGL.so.1: cannot open shared object file: No such file or directory解决。
终端上屏显的结果如下, 推理结果保存在当前路径下的“infer_result”目录下:
Init ACL Successfully load model ./models/ctpn.om Init model resource [Model] Model init resource stage success [Model] create model output dataset success load model ./models/svtr.om Init model resource [Model] Model init resource stage success [Model] create model output dataset success start infer image: test.png [Model] create model input dataset success [Model] create model input dataset success 文字结果: 开启开发者之旅 [Model] create model input dataset success 文字结果: 从入门到进阶,开启异腾开发者成长之旅 Model start release... Model release source success Model start release... Model release source success Deinit ACL Successfully
推理完成后,在当前infer_result文件夹下生成结果图片test_res.png:
样例总结与扩展
以上代码包括以下几个步骤:
1. 初始化acl资源:此样例中,包括指定计算设备、创建context、创建stream等操作。再初始化了两个模型类(CTPN、SVTR),初始化时,进行了模型的加载以及输出数据集的创建
2. 推理:读入图片,调用model.infer进行推理,其中包含数据的前处理、输入数据集结构的创建、推理、将推理结果转换为numpy、并进行后处理等操作,得到最终文本识别后的图片,保存在infer_result文件夹中。
3. 资源销毁:最后记得释放相关资源,包括卸载模型、销毁输入输出数据集、释放 Context、释放指定的计算设备、以及AscendCL去初始化等操作。
AscendCL接口分类总结:
分类 | 接口函数 | 描述 |
---|---|---|
AscendCL初始化相关 | acl.init() | pyACL初始化函数 |
acl.rt.set_device(device_id) | 指定当前进程或线程中用于运算的Device,同时隐式创建默认Context | |
acl.rt.create_context(device_id) | 在当前进程或线程中显式创建一个Context | |
模型描述信息相关 | acl.mdl.load_from_file(model_path) | 从文件加载离线模型数据(适配昇腾AI处理器的离线模型) |
acl.mdl.create_desc() | 创建aclmdlDesc类型的数据 | |
acl.mdl.get_desc(model_desc, model_id) | 根据模型ID获取该模型的aclmdlDesc类型数据 | |
acl.mdl.get_num_inputs(model_desc) | 根据aclmdlDesc类型的数据,获取模型的输入个数 | |
acl.mdl.get_num_outputs(model_desc) | 根据aclmdlDesc类型的数据,获取模型的输出个数 | |
acl.mdl.get_output_size_by_index(model_desc, i) | 根据aclmdlDesc类型的数据,获取指定输出的大小,单位为Byte | |
acl.mdl.get_output_dims(model_desc, i) | 根据模型描述信息获取指定的模型输出tensor的维度信息 | |
acl.mdl.get_output_data_type(model_desc, i) | 根据模型描述信息获取模型中指定输出的数据类型 | |
数据集结构相关 | acl.mdl.create_dataset() | 创建aclmdlDataset类型的数据 |
acl.create_data_buffer(data_addr, size) | 创建aclDataBuffer类型的数据,该数据类型用于描述内存地址、大小等内存信息 | |
acl.mdl.add_dataset_buffer(dataset, date_buffer) | 向aclmdlDataset中增加aclDataBuffer | |
acl.mdl.get_dataset_num_buffers(dataset) | 获取aclmdlDataset中aclDataBuffer的个数 | |
acl.mdl.get_dataset_buffer(dataset, i) | 获取aclmdlDataset中的第i个aclDataBuffer | |
acl.get_data_buffer_addr(buffer) | 获取aclDataBuffer类型中的数据的地址对象 | |
acl.get_data_buffer_size(buffer) | 获取aclDataBuffer类型中数据的内存大小,单位Byte | |
acl.util.bytes_to_ptr(bytes_data) | 将bytes对象转换成为void*数据,可以将转换好的数据传递给C函数直接使用 | |
acl.util.ptr_to_bytes(ptr, size) | 将void*数据转换为bytes对象,可以使python代码直接访问 | |
acl.rt.malloc(size, policy) | 申请Device上的内存 | |
推理相关 | acl.mdl.execute(model_id, input_dataset, output_dataset) | 执行模型推理,直到返回推理结果 |
销毁资源相关 | acl.destroy_data_buffer(data_buffer) | 销毁aclDataBuffer类型的数据 |
acl.mdl.destroy_dataset(dataset) | 销毁aclmdlDataset类型的数据 | |
acl.mdl.unload(model_id) | 系统完成模型推理后,可调用该接口卸载模型,释放资源 | |
acl.mdl.destroy_desc(model_desc) | 销毁aclmdlDesc类型的数据 | |
acl.rt.destroy_context(context) | 销毁一个Context,释放Context的资源 | |
acl.rt.reset_device(device_id) | 复位当前运算的Device,释放Device上的资源,包括默认Context、默认Stream以及默认Context下创建的所有Stream | |
acl.finalize() | pyACL去初始化函数,用于释放进程内的pyACL相关资源 |
理解各个接口含义后,用户可进行灵活运用。除此外,此样例中只示范了图片推理,若需要对视频流数据进行推理,可用三种方式输入视频流数据:USB摄像头、手机摄像头。具体使用方式可参考《摄像头拉流》,用户只需将前处理、推理及后处理代码放入摄像头推理代码的循环中即可,注意有些细节地方需进行相应修改,具体逻辑可参照图像分类应用中的样例总结与扩展。