diff --git a/.gitignore b/.gitignore index fdac37d..5aefa29 100755 --- a/.gitignore +++ b/.gitignore @@ -141,4 +141,14 @@ dmypy.json *.onnx *.ttf -*.ttc \ No newline at end of file +*.ttc + +long1.jpg + +*.bin +*.mapping +*.xml + +*.pdiparams +*.pdiparams.info +*.pdmodel \ No newline at end of file diff --git a/README.md b/README.md index e8c9b9b..e4eaead 100755 --- a/README.md +++ b/README.md @@ -21,9 +21,12 @@ 目录 - [RapidOCR (捷智OCR)](#rapidocr-捷智ocr) + - [TODO](#todo) + - [Openvino](#openvino) - [简介](#简介) - [近期更新](#近期更新) - - [🔉2022-02-24 update](#2022-02-24-update) + - [🎧2022-04-04 update](#2022-04-04-update) + - [2022-02-24 update](#2022-02-24-update) - [2021-12-18 update](#2021-12-18-update) - [2021-11-28 update](#2021-11-28-update) - [之前更新记录](#之前更新记录) @@ -57,10 +60,10 @@ ## TODO -- [ ] 测试openvino推理速度,给出详细设备上推理性能对比表格 +- [x] 测试openvino推理速度,给出详细设备上推理性能对比表格 - [ ] 基于openvino的推理部署方案 -## Openvino +## Openvino https://blog.csdn.net/weixin_47567401/article/details/123525014 @@ -74,10 +77,15 @@ https://blog.csdn.net/weixin_47567401/article/details/123525014 - 基于百度的开源PaddleOCR 模型及训练,任何人可以使用本推理库,也可以根据自己的需求使用百度的paddlepaddle框架进行模型优化。 ## 近期更新 -#### 🔉2022-02-24 update +#### 🎧2022-04-04 update +- 增加python下的基于OpenVINO推理引擎的支持 +- 给出OpenVINO和ONNXRuntime的性能对比表格 +- 详情参加[python/onnxruntime/README](./python/README.md) + +#### 2022-02-24 update - 优化python目录下的推理代码 - 添加调用不同语言模型的推理代码示例 -- 详情参见:[python/README](./python/README.md) +- 详情参见:[python/README](./python/onnxruntime_infer/README.md) #### 2021-12-18 update - 添加[Google Colab Demo](https://colab.research.google.com/github/RapidAI/RapidOCR/blob/main/RapidOCRDemo.ipynb) diff --git a/python/README.md b/python/README.md index d317d9c..520f97b 100755 --- a/python/README.md +++ b/python/README.md @@ -1,148 +1,60 @@ -### 使用步骤 -1. 下载相应模型和用于显示的字体文件 - - [百度网盘 提取码:30jv](https://pan.baidu.com/s/1qkqWK4wRdMjqGGbzR-FyWg) | [Google Drive](https://drive.google.com/drive/folders/1x_a9KpCo_1blxH1xFOfgKVkw1HYRVywY?usp=sharing) +### 目录说明 +- `onnxruntime_infer`: 基于ONNXRuntime推理引擎推理 +- `openvino_infer`: 基于OpenVINO推理引擎推理 - - 下载之后模型和相应字体文件放在`fonts`和`models`下,最终目录结构如下: - ```text - models - |-- ch_PP-OCRv2_det_infer.onnx - |-- ch_ppocr_mobile_v2.0_cls_infer.onnx - |-- ch_ppocr_mobile_v2.0_det_infer.onnx - |-- ch_ppocr_server_v2.0_det_infer.onnx - |-- ch_ppocr_server_v2.0_rec_infer.onnx - |-- en_number_mobile_v2.0_rec_infer.onnx - |-- korean_mobile_v2.0_rec_infer.onnx - `-- japan_rec_crnn.onnx +### TODO +- [ ] 模型转INT8,尚未找到转换代码 +- [ ] 其他推理模型代码整理 +- [ ] 模型转换代码整理 - fonts - |-- msyh.ttc - `-- korean.ttf - ``` +### 关于OpenVINO +- OpenVINO可以直接推理ONNX模型,可以不用转换直接使用之前ONNX模型 +- OpenVINO推理速度更快,但是从对比来看,占用内存更多,目前正在排查原因 -2. 运行 - - 接口调用方式运行 - - 如果想要使用其他语言的识别模型,可以只对`rec_model_path`和`keys_path`作对应更改即可 - - **!!!各个不同语言的推理代码区别只在于模型和字典文件!!!** - ```python - from rapid_ocr_api import TextSystem, visualize +### OpenVINO与ONNXRuntime性能对比 +- 推理设备:`Windows 64位 Intel(R) Core(TM) i5-4210M CPU @ 2.60GHz 2.59 GHz` +- 测试图像宽高: `12119x810` - det_model_path = 'models/ch_ppocr_mobile_v2.0_det_infer.onnx' - cls_model_path = 'models/ch_ppocr_mobile_v2.0_cls_infer.onnx' - - # 中英文识别 - rec_model_path = 'models/ch_ppocr_mobile_v2.0_rec_infer.onnx' - keys_path = 'rec_dict/ppocr_keys_v1.txt' - - text_sys = TextSystem(det_model_path, - rec_model_path, - use_angle_cls=True, - cls_model_path=cls_model_path, - keys_path=keys_path) - - image_path = r'test_images/det_images/ch_en_num.jpg' - dt_boxes, rec_res = text_sys(image_path) - visualize(image_path, dt_boxes, rec_res) +| 测试模型 | 推理框架 | 占用内存(3次平均) | 推理时间(3次平均) | +| ------------------------------------ | -------------------- | ----------------- | ----------------- | +| `ch_PP-OCRv2_det_infer.onnx` | `ONNXRuntime=1.10.0` | 0.8G | 5.354s | +| `ch_PP-OCRv2_det_infer.onnx` | `openvino=2022.1.0` | 3.225G | 2.53s | +| `ch_PP-OCRv2_det_infer.xml` FP32 | `openvino=2022.1.0` | 3.175G | 2.0455s | - # 只有中英文和数字识别 - rec_model_path = 'models/en_number_mobile_v2.0_rec_infer.onnx' - keys_path = 'rec_dict/en_dict.txt' +### OpenVINO与ONNXRuntime推理代码写法对比 +NOTE: 以`ch_ppocr_mobile_v2_det`中推理代码为例子 - text_sys = TextSystem(det_model_path, - rec_model_path, - use_angle_cls=True, - cls_model_path=cls_model_path, - keys_path=keys_path) +- ONNXRuntime +```python +import onnxruntime - image_path = r'test_images/det_images/en_num.png' - dt_boxes, rec_res = text_sys(image_path) - visualize(image_path, dt_boxes, rec_res) +# 声明 +sess_opt = onnxruntime.SessionOptions() +sess_opt.log_severity_level = 4 +sess_opt.enable_cpu_mem_arena = False +self.session = onnxruntime.InferenceSession(det_model_path, sess_opt) +self.input_name = self.session.get_inputs()[0].name +self.output_name = self.session.get_outputs()[0].name +# 推理 +preds = self.session.run([self.output_name], + {self.input_name: img}) +``` - # 日语识别 - rec_model_path = 'models/japan_rec_crnn.onnx' - keys_path = 'rec_dict/japan_dict.txt' +- OpenVINO +```python +from openvino.runtime import Core - text_sys = TextSystem(det_model_path, - rec_model_path, - use_angle_cls=True, - cls_model_path=cls_model_path, - keys_path=keys_path) +# 初始化 +ie = Core() +model_onnx = ie.read_model(det_model_path) +compile_model = ie.compile_model(model=model_onnx, + device_name='CPU') +self.vino_session = compile_model.create_infer_request() - image_path = r'test_images/det_images/japan.png' - dt_boxes, rec_res = text_sys(image_path) - visualize(image_path, dt_boxes, rec_res) +# 推理 +self.vino_session.infer(inputs=[img]) +vino_preds = self.vino_session.get_output_tensor().data +``` - - # 韩语识别 - rec_model_path = 'models/korean_mobile_v2.0_rec_infer.onnx' - keys_path = 'rec_dict/korean_dict.txt' - font_path = 'fonts/korean.ttf' - - text_sys = TextSystem(det_model_path, - rec_model_path, - use_angle_cls=True, - cls_model_path=cls_model_path, - keys_path=keys_path) - - image_path = r'test_images/det_images/korean_1.jpg' - dt_boxes, rec_res = text_sys(image_path) - visualize(image_path, dt_boxes, rec_res, font_path) - ``` - - - 命令行运行 - ```shell - $ bash test_demo.sh - ``` - -### 相关调节参数 -|参数名称|作用|建议取值范围|默认值|代码位置| -|:---:|:---:|:---:|:---:|:---:| -|box_thresh|文本检测所得框是否保留的阈值,值越大,召回率越低|[0, 1]|0.5|[text_detect.py](https://github.com/RapidAI/RapidOCR/blob/6aa79aa390c9c9e8f41df0f0c35f3dca97e6dc93/python/ch_ppocr_mobile_v2_det/text_detect.py?_pjax=%23js-repo-pjax-container%2C%20div%5Bitemtype%3D%22http%3A%2F%2Fschema.org%2FSoftwareSourceCode%22%5D%20main%2C%20%5Bdata-pjax-container%5D#L55)| -|unclip_ratio|控制文本检测框的大小,值越大,检测框整体越大|[1.6, 2.0]|1.6|[text_detect.py](https://github.com/RapidAI/RapidOCR/blob/6aa79aa390c9c9e8f41df0f0c35f3dca97e6dc93/python/ch_ppocr_mobile_v2_det/text_detect.py?_pjax=%23js-repo-pjax-container%2C%20div%5Bitemtype%3D%22http%3A%2F%2Fschema.org%2FSoftwareSourceCode%22%5D%20main%2C%20%5Bdata-pjax-container%5D#L57)| -|text_score|文本识别结果置信度,值越大,把握越大|[0, 1]|0.5|[rapidOCR.py](https://github.com/RapidAI/RapidOCR/blob/6aa79aa390c9c9e8f41df0f0c35f3dca97e6dc93/python/rapidOCR.py?_pjax=%23js-repo-pjax-container%2C%20div%5Bitemtype%3D%22http%3A%2F%2Fschema.org%2FSoftwareSourceCode%22%5D%20main%2C%20%5Bdata-pjax-container%5D#L270)| - - -### onnxruntime-gpu版推理配置 - -1. ONNXRuntime-gpu需要严格按照与cuda、cudnn版本对应来安装,具体参考[文档](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html#requirements), **这一步关乎后面是否可以成功调用GPU** -2. 推理代码中,加载onnx模型部分,用以下对应语言代码替换即可,详细参见:[官方教程](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html) - - python版本 - ```python - import onnxruntime as ort - - model_path = '' - - providers = [ - ('CUDAExecutionProvider', { - 'device_id': 0, - 'arena_extend_strategy': 'kNextPowerOfTwo', - 'gpu_mem_limit': 2 * 1024 * 1024 * 1024, - 'cudnn_conv_algo_search': 'EXHAUSTIVE', - 'do_copy_in_default_stream': True, - }), - 'CPUExecutionProvider', - ] - - session = ort.InferenceSession(model_path, providers=providers) - ``` - - C/C++版本 - ```c++ - OrtSessionOptions* session_options = /* ... */; - - OrtCUDAProviderOptions options; - options.device_id = 0; - options.arena_extend_strategy = 0; - options.gpu_mem_limit = 2 * 1024 * 1024 * 1024; - options.cudnn_conv_algo_search = OrtCudnnConvAlgoSearch::EXHAUSTIVE; - options.do_copy_in_default_stream = 1; - - SessionOptionsAppendExecutionProvider_CUDA(session_options, &options); - ``` -3. 推理时间粗略对比(跑完RapidOCR整个项目) - - |推理方式|推理图像数目|耗费时间| - |:---:|:---:|:---:| - |CPU|46张图像|191 s| - |GPU|46张图像|52.38 s| - |||| \ No newline at end of file diff --git a/python/onnxruntime_infer/README.md b/python/onnxruntime_infer/README.md new file mode 100644 index 0000000..22b78cf --- /dev/null +++ b/python/onnxruntime_infer/README.md @@ -0,0 +1,148 @@ +### 使用步骤 +1. 下载相应模型和用于显示的字体文件 + - [百度网盘 提取码:30jv](https://pan.baidu.com/s/1qkqWK4wRdMjqGGbzR-FyWg) | [Google Drive](https://drive.google.com/drive/folders/1x_a9KpCo_1blxH1xFOfgKVkw1HYRVywY?usp=sharing) + + - 下载之后模型和相应字体文件放在`fonts`和`models`下,最终目录结构如下: + ```text + models + |-- ch_PP-OCRv2_det_infer.onnx + |-- ch_ppocr_mobile_v2.0_cls_infer.onnx + |-- ch_ppocr_mobile_v2.0_det_infer.onnx + |-- ch_ppocr_server_v2.0_det_infer.onnx + |-- ch_ppocr_server_v2.0_rec_infer.onnx + |-- en_number_mobile_v2.0_rec_infer.onnx + |-- korean_mobile_v2.0_rec_infer.onnx + `-- japan_rec_crnn.onnx + + fonts + |-- msyh.ttc + `-- korean.ttf + ``` + +2. 运行 + - 接口调用方式运行 + - 如果想要使用其他语言的识别模型,可以只对`rec_model_path`和`keys_path`作对应更改即可 + - **!!!各个不同语言的推理代码区别只在于模型和字典文件!!!** + ```python + from rapid_ocr_api import TextSystem, visualize + + det_model_path = 'models/ch_ppocr_mobile_v2.0_det_infer.onnx' + cls_model_path = 'models/ch_ppocr_mobile_v2.0_cls_infer.onnx' + + # 中英文识别 + rec_model_path = 'models/ch_ppocr_mobile_v2.0_rec_infer.onnx' + keys_path = 'rec_dict/ppocr_keys_v1.txt' + + text_sys = TextSystem(det_model_path, + rec_model_path, + use_angle_cls=True, + cls_model_path=cls_model_path, + keys_path=keys_path) + + image_path = r'test_images/det_images/ch_en_num.jpg' + dt_boxes, rec_res = text_sys(image_path) + visualize(image_path, dt_boxes, rec_res) + + + # 只有中英文和数字识别 + rec_model_path = 'models/en_number_mobile_v2.0_rec_infer.onnx' + keys_path = 'rec_dict/en_dict.txt' + + text_sys = TextSystem(det_model_path, + rec_model_path, + use_angle_cls=True, + cls_model_path=cls_model_path, + keys_path=keys_path) + + image_path = r'test_images/det_images/en_num.png' + dt_boxes, rec_res = text_sys(image_path) + visualize(image_path, dt_boxes, rec_res) + + + # 日语识别 + rec_model_path = 'models/japan_rec_crnn.onnx' + keys_path = 'rec_dict/japan_dict.txt' + + text_sys = TextSystem(det_model_path, + rec_model_path, + use_angle_cls=True, + cls_model_path=cls_model_path, + keys_path=keys_path) + + image_path = r'test_images/det_images/japan.png' + dt_boxes, rec_res = text_sys(image_path) + visualize(image_path, dt_boxes, rec_res) + + + # 韩语识别 + rec_model_path = 'models/korean_mobile_v2.0_rec_infer.onnx' + keys_path = 'rec_dict/korean_dict.txt' + font_path = 'fonts/korean.ttf' + + text_sys = TextSystem(det_model_path, + rec_model_path, + use_angle_cls=True, + cls_model_path=cls_model_path, + keys_path=keys_path) + + image_path = r'test_images/det_images/korean_1.jpg' + dt_boxes, rec_res = text_sys(image_path) + visualize(image_path, dt_boxes, rec_res, font_path) + ``` + + - 命令行运行 + ```shell + $ bash test_demo.sh + ``` + +### 相关调节参数 +|参数名称|作用|建议取值范围|默认值|代码位置| +|:---:|:---:|:---:|:---:|:---:| +|box_thresh|文本检测所得框是否保留的阈值,值越大,召回率越低|[0, 1]|0.5|[text_detect.py](https://github.com/RapidAI/RapidOCR/blob/6aa79aa390c9c9e8f41df0f0c35f3dca97e6dc93/python/ch_ppocr_mobile_v2_det/text_detect.py?_pjax=%23js-repo-pjax-container%2C%20div%5Bitemtype%3D%22http%3A%2F%2Fschema.org%2FSoftwareSourceCode%22%5D%20main%2C%20%5Bdata-pjax-container%5D#L55)| +|unclip_ratio|控制文本检测框的大小,值越大,检测框整体越大|[1.6, 2.0]|1.6|[text_detect.py](https://github.com/RapidAI/RapidOCR/blob/6aa79aa390c9c9e8f41df0f0c35f3dca97e6dc93/python/ch_ppocr_mobile_v2_det/text_detect.py?_pjax=%23js-repo-pjax-container%2C%20div%5Bitemtype%3D%22http%3A%2F%2Fschema.org%2FSoftwareSourceCode%22%5D%20main%2C%20%5Bdata-pjax-container%5D#L57)| +|text_score|文本识别结果置信度,值越大,把握越大|[0, 1]|0.5|[rapidOCR.py](https://github.com/RapidAI/RapidOCR/blob/6aa79aa390c9c9e8f41df0f0c35f3dca97e6dc93/python/rapidOCR.py?_pjax=%23js-repo-pjax-container%2C%20div%5Bitemtype%3D%22http%3A%2F%2Fschema.org%2FSoftwareSourceCode%22%5D%20main%2C%20%5Bdata-pjax-container%5D#L270)| + + +### onnxruntime-gpu版推理配置 + +1. ONNXRuntime-gpu需要严格按照与cuda、cudnn版本对应来安装,具体参考[文档](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html#requirements), **这一步关乎后面是否可以成功调用GPU** +2. 推理代码中,加载onnx模型部分,用以下对应语言代码替换即可,详细参见:[官方教程](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html) + - python版本 + ```python + import onnxruntime as ort + + model_path = '' + + providers = [ + ('CUDAExecutionProvider', { + 'device_id': 0, + 'arena_extend_strategy': 'kNextPowerOfTwo', + 'gpu_mem_limit': 2 * 1024 * 1024 * 1024, + 'cudnn_conv_algo_search': 'EXHAUSTIVE', + 'do_copy_in_default_stream': True, + }), + 'CPUExecutionProvider', + ] + + session = ort.InferenceSession(model_path, providers=providers) + ``` + - C/C++版本 + ```c++ + OrtSessionOptions* session_options = /* ... */; + + OrtCUDAProviderOptions options; + options.device_id = 0; + options.arena_extend_strategy = 0; + options.gpu_mem_limit = 2 * 1024 * 1024 * 1024; + options.cudnn_conv_algo_search = OrtCudnnConvAlgoSearch::EXHAUSTIVE; + options.do_copy_in_default_stream = 1; + + SessionOptionsAppendExecutionProvider_CUDA(session_options, &options); + ``` +3. 推理时间粗略对比(跑完RapidOCR整个项目) + + |推理方式|推理图像数目|耗费时间| + |:---:|:---:|:---:| + |CPU|46张图像|191 s| + |GPU|46张图像|52.38 s| + |||| \ No newline at end of file diff --git a/python/ch_ppocr_mobile_v2_cls/__init__.py b/python/onnxruntime_infer/ch_ppocr_mobile_v2_cls/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_mobile_v2_cls/__init__.py rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_cls/__init__.py diff --git a/python/ch_ppocr_mobile_v2_cls/run.sh b/python/onnxruntime_infer/ch_ppocr_mobile_v2_cls/run.sh similarity index 100% rename from python/ch_ppocr_mobile_v2_cls/run.sh rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_cls/run.sh diff --git a/python/ch_ppocr_mobile_v2_cls/text_cls.py b/python/onnxruntime_infer/ch_ppocr_mobile_v2_cls/text_cls.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_mobile_v2_cls/text_cls.py rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_cls/text_cls.py diff --git a/python/ch_ppocr_mobile_v2_cls/utils.py b/python/onnxruntime_infer/ch_ppocr_mobile_v2_cls/utils.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_mobile_v2_cls/utils.py rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_cls/utils.py diff --git a/python/ch_ppocr_mobile_v2_det/__init__.py b/python/onnxruntime_infer/ch_ppocr_mobile_v2_det/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_mobile_v2_det/__init__.py rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_det/__init__.py diff --git a/python/ch_ppocr_mobile_v2_det/run.sh b/python/onnxruntime_infer/ch_ppocr_mobile_v2_det/run.sh similarity index 100% rename from python/ch_ppocr_mobile_v2_det/run.sh rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_det/run.sh diff --git a/python/ch_ppocr_mobile_v2_det/text_detect.py b/python/onnxruntime_infer/ch_ppocr_mobile_v2_det/text_detect.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_mobile_v2_det/text_detect.py rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_det/text_detect.py diff --git a/python/ch_ppocr_mobile_v2_det/utils.py b/python/onnxruntime_infer/ch_ppocr_mobile_v2_det/utils.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_mobile_v2_det/utils.py rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_det/utils.py diff --git a/python/ch_ppocr_mobile_v2_rec/__init__.py b/python/onnxruntime_infer/ch_ppocr_mobile_v2_rec/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_mobile_v2_rec/__init__.py rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_rec/__init__.py diff --git a/python/ch_ppocr_mobile_v2_rec/ppocr_keys_v1.txt b/python/onnxruntime_infer/ch_ppocr_mobile_v2_rec/ppocr_keys_v1.txt old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_mobile_v2_rec/ppocr_keys_v1.txt rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_rec/ppocr_keys_v1.txt diff --git a/python/ch_ppocr_mobile_v2_rec/run.sh b/python/onnxruntime_infer/ch_ppocr_mobile_v2_rec/run.sh similarity index 100% rename from python/ch_ppocr_mobile_v2_rec/run.sh rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_rec/run.sh diff --git a/python/ch_ppocr_mobile_v2_rec/text_recognize.py b/python/onnxruntime_infer/ch_ppocr_mobile_v2_rec/text_recognize.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_mobile_v2_rec/text_recognize.py rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_rec/text_recognize.py diff --git a/python/ch_ppocr_mobile_v2_rec/utils.py b/python/onnxruntime_infer/ch_ppocr_mobile_v2_rec/utils.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_mobile_v2_rec/utils.py rename to python/onnxruntime_infer/ch_ppocr_mobile_v2_rec/utils.py diff --git a/python/ch_ppocr_server_v2_det/__init__.py b/python/onnxruntime_infer/ch_ppocr_server_v2_det/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_server_v2_det/__init__.py rename to python/onnxruntime_infer/ch_ppocr_server_v2_det/__init__.py diff --git a/python/ch_ppocr_server_v2_det/run.sh b/python/onnxruntime_infer/ch_ppocr_server_v2_det/run.sh similarity index 100% rename from python/ch_ppocr_server_v2_det/run.sh rename to python/onnxruntime_infer/ch_ppocr_server_v2_det/run.sh diff --git a/python/ch_ppocr_server_v2_det/text_detect.py b/python/onnxruntime_infer/ch_ppocr_server_v2_det/text_detect.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_server_v2_det/text_detect.py rename to python/onnxruntime_infer/ch_ppocr_server_v2_det/text_detect.py diff --git a/python/ch_ppocr_server_v2_det/utils.py b/python/onnxruntime_infer/ch_ppocr_server_v2_det/utils.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_server_v2_det/utils.py rename to python/onnxruntime_infer/ch_ppocr_server_v2_det/utils.py diff --git a/python/ch_ppocr_server_v2_rec/__init__.py b/python/onnxruntime_infer/ch_ppocr_server_v2_rec/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_server_v2_rec/__init__.py rename to python/onnxruntime_infer/ch_ppocr_server_v2_rec/__init__.py diff --git a/python/ch_ppocr_server_v2_rec/ppocr_keys_v1.txt b/python/onnxruntime_infer/ch_ppocr_server_v2_rec/ppocr_keys_v1.txt old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_server_v2_rec/ppocr_keys_v1.txt rename to python/onnxruntime_infer/ch_ppocr_server_v2_rec/ppocr_keys_v1.txt diff --git a/python/ch_ppocr_server_v2_rec/run.sh b/python/onnxruntime_infer/ch_ppocr_server_v2_rec/run.sh similarity index 100% rename from python/ch_ppocr_server_v2_rec/run.sh rename to python/onnxruntime_infer/ch_ppocr_server_v2_rec/run.sh diff --git a/python/ch_ppocr_server_v2_rec/text_recognize.py b/python/onnxruntime_infer/ch_ppocr_server_v2_rec/text_recognize.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_server_v2_rec/text_recognize.py rename to python/onnxruntime_infer/ch_ppocr_server_v2_rec/text_recognize.py diff --git a/python/ch_ppocr_server_v2_rec/utils.py b/python/onnxruntime_infer/ch_ppocr_server_v2_rec/utils.py old mode 100755 new mode 100644 similarity index 100% rename from python/ch_ppocr_server_v2_rec/utils.py rename to python/onnxruntime_infer/ch_ppocr_server_v2_rec/utils.py diff --git a/python/fonts/.gitkeep b/python/onnxruntime_infer/fonts/.gitkeep similarity index 100% rename from python/fonts/.gitkeep rename to python/onnxruntime_infer/fonts/.gitkeep diff --git a/python/models/模型位置.md b/python/onnxruntime_infer/models/.gitkeep old mode 100755 new mode 100644 similarity index 100% rename from python/models/模型位置.md rename to python/onnxruntime_infer/models/.gitkeep diff --git a/python/rapid_ocr_api.py b/python/onnxruntime_infer/rapid_ocr_api.py similarity index 100% rename from python/rapid_ocr_api.py rename to python/onnxruntime_infer/rapid_ocr_api.py diff --git a/python/rec_dict/en_dict.txt b/python/onnxruntime_infer/rec_dict/en_dict.txt similarity index 100% rename from python/rec_dict/en_dict.txt rename to python/onnxruntime_infer/rec_dict/en_dict.txt diff --git a/python/rec_dict/japan_dict.txt b/python/onnxruntime_infer/rec_dict/japan_dict.txt similarity index 100% rename from python/rec_dict/japan_dict.txt rename to python/onnxruntime_infer/rec_dict/japan_dict.txt diff --git a/python/rec_dict/korean_dict.txt b/python/onnxruntime_infer/rec_dict/korean_dict.txt similarity index 100% rename from python/rec_dict/korean_dict.txt rename to python/onnxruntime_infer/rec_dict/korean_dict.txt diff --git a/python/rec_dict/ppocr_keys_v1.txt b/python/onnxruntime_infer/rec_dict/ppocr_keys_v1.txt similarity index 100% rename from python/rec_dict/ppocr_keys_v1.txt rename to python/onnxruntime_infer/rec_dict/ppocr_keys_v1.txt diff --git a/python/requirements.txt b/python/onnxruntime_infer/requirements.txt old mode 100755 new mode 100644 similarity index 75% rename from python/requirements.txt rename to python/onnxruntime_infer/requirements.txt index a2384eb..3bccfbb --- a/python/requirements.txt +++ b/python/onnxruntime_infer/requirements.txt @@ -4,4 +4,5 @@ onnxruntime==1.7.0 opencv_python==4.5.1.48 six==1.15.0 numpy==1.19.2 -Pillow==8.3.0 \ No newline at end of file +Pillow==8.3.0 +openvino==2022.1.0 \ No newline at end of file diff --git a/python/test_demo.py b/python/onnxruntime_infer/test_demo.py similarity index 100% rename from python/test_demo.py rename to python/onnxruntime_infer/test_demo.py diff --git a/python/test_demo.sh b/python/onnxruntime_infer/test_demo.sh similarity index 100% rename from python/test_demo.sh rename to python/onnxruntime_infer/test_demo.sh diff --git a/python/test_images/det_images/ch_en_num.jpg b/python/onnxruntime_infer/test_images/det_images/ch_en_num.jpg similarity index 100% rename from python/test_images/det_images/ch_en_num.jpg rename to python/onnxruntime_infer/test_images/det_images/ch_en_num.jpg diff --git a/python/test_images/det_images/en_num.png b/python/onnxruntime_infer/test_images/det_images/en_num.png similarity index 100% rename from python/test_images/det_images/en_num.png rename to python/onnxruntime_infer/test_images/det_images/en_num.png diff --git a/python/test_images/det_images/japan.png b/python/onnxruntime_infer/test_images/det_images/japan.png similarity index 100% rename from python/test_images/det_images/japan.png rename to python/onnxruntime_infer/test_images/det_images/japan.png diff --git a/python/test_images/det_images/korean_1.jpg b/python/onnxruntime_infer/test_images/det_images/korean_1.jpg similarity index 100% rename from python/test_images/det_images/korean_1.jpg rename to python/onnxruntime_infer/test_images/det_images/korean_1.jpg diff --git a/python/test_images/rec_images/imgs_words_ch/1.png b/python/onnxruntime_infer/test_images/rec_images/imgs_words_ch/1.png similarity index 100% rename from python/test_images/rec_images/imgs_words_ch/1.png rename to python/onnxruntime_infer/test_images/rec_images/imgs_words_ch/1.png diff --git a/python/test_images/rec_images/imgs_words_ch/2.png b/python/onnxruntime_infer/test_images/rec_images/imgs_words_ch/2.png similarity index 100% rename from python/test_images/rec_images/imgs_words_ch/2.png rename to python/onnxruntime_infer/test_images/rec_images/imgs_words_ch/2.png diff --git a/python/test_images/rec_images/imgs_words_en/word_10.png b/python/onnxruntime_infer/test_images/rec_images/imgs_words_en/word_10.png similarity index 100% rename from python/test_images/rec_images/imgs_words_en/word_10.png rename to python/onnxruntime_infer/test_images/rec_images/imgs_words_en/word_10.png diff --git a/python/test_images/rec_images/imgs_words_japan/1.jpg b/python/onnxruntime_infer/test_images/rec_images/imgs_words_japan/1.jpg similarity index 100% rename from python/test_images/rec_images/imgs_words_japan/1.jpg rename to python/onnxruntime_infer/test_images/rec_images/imgs_words_japan/1.jpg diff --git a/python/test_images/rec_images/imgs_words_korean/1.jpg b/python/onnxruntime_infer/test_images/rec_images/imgs_words_korean/1.jpg similarity index 100% rename from python/test_images/rec_images/imgs_words_korean/1.jpg rename to python/onnxruntime_infer/test_images/rec_images/imgs_words_korean/1.jpg diff --git a/python/openvino_infer/README.md b/python/openvino_infer/README.md new file mode 100644 index 0000000..f679cc7 --- /dev/null +++ b/python/openvino_infer/README.md @@ -0,0 +1,6 @@ +#### 安装 +```bash +# Windows端 +pip install openvino==2022.1.0 +pip install openvino-dev==2022.1.0 +``` \ No newline at end of file diff --git a/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_ir/__init__.py b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_ir/__init__.py new file mode 100644 index 0000000..d7a3e0b --- /dev/null +++ b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_ir/__init__.py @@ -0,0 +1,6 @@ +# !/usr/bin/env python +# -*- encoding: utf-8 -*- +# @File: __init__.py +# @Time: 2021/01/23 15:24:27 +# @Author: Max +from .text_detect import TextDetector \ No newline at end of file diff --git a/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_ir/run.sh b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_ir/run.sh new file mode 100644 index 0000000..99fc806 --- /dev/null +++ b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_ir/run.sh @@ -0,0 +1,9 @@ +#! /bin/bash +set -e errexit + +root_path=$(cd $(dirname $0); pwd) +image_path=${root_path}"/test_images/det_images/1.jpg" +model_path=${root_path}"/models/ch_ppocr_mobile_v2.0_det_infer.onnx" + +python text_detect.py --image_path ${image_path} \ + --model_path ${model_path} \ No newline at end of file diff --git a/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_ir/text_detect.py b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_ir/text_detect.py new file mode 100644 index 0000000..2816f12 --- /dev/null +++ b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_ir/text_detect.py @@ -0,0 +1,153 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# !/usr/bin/env python +# -*- encoding: utf-8 -*- +import argparse +import time + +import cv2 +import numpy as np +from openvino.runtime import Core + +try: + from .utils import (DBPostProcess, check_and_read_gif, create_operators, + draw_text_det_res, transform) +except: + from utils import (DBPostProcess, check_and_read_gif, create_operators, + draw_text_det_res, transform) + + +class TextDetector(object): + def __init__(self, det_model_path): + pre_process_list = [{ + 'DetResizeForTest': { + 'limit_side_len': 736, + 'limit_type': 'min' + } + }, { + 'NormalizeImage': { + 'std': [0.229, 0.224, 0.225], + 'mean': [0.485, 0.456, 0.406], + 'scale': '1./255.', + 'order': 'hwc' + } + }, { + 'ToCHWImage': None + }, { + 'KeepKeys': { + 'keep_keys': ['image', 'shape'] + } + }] + + self.preprocess_op = create_operators(pre_process_list) + self.postprocess_op = DBPostProcess(thresh=0.3, + box_thresh=0.5, + max_candidates=1000, + unclip_ratio=1.6, + use_dilation=True) + ie = Core() + model_onnx = ie.read_model(det_model_path) + compile_model = ie.compile_model(model=model_onnx, + device_name='CPU') + self.vino_session = compile_model.create_infer_request() + + def order_points_clockwise(self, pts): + """ + reference from: https://github.com/jrosebr1/imutils/blob/master/imutils/perspective.py + # sort the points based on their x-coordinates + """ + xSorted = pts[np.argsort(pts[:, 0]), :] + + # grab the left-most and right-most points from the sorted + # x-roodinate points + leftMost = xSorted[:2, :] + rightMost = xSorted[2:, :] + + # now, sort the left-most coordinates according to their + # y-coordinates so we can grab the top-left and bottom-left + # points, respectively + leftMost = leftMost[np.argsort(leftMost[:, 1]), :] + (tl, bl) = leftMost + + rightMost = rightMost[np.argsort(rightMost[:, 1]), :] + (tr, br) = rightMost + + rect = np.array([tl, tr, br, bl], dtype="float32") + return rect + + def clip_det_res(self, points, img_height, img_width): + for pno in range(points.shape[0]): + points[pno, 0] = int(min(max(points[pno, 0], 0), img_width - 1)) + points[pno, 1] = int(min(max(points[pno, 1], 0), img_height - 1)) + return points + + 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 __call__(self, img): + ori_im = img.copy() + data = {'image': img} + data = transform(data, self.preprocess_op) + img, shape_list = data + if img is None: + return None, 0 + img = np.expand_dims(img, axis=0) + img = img.astype(np.float32) + shape_list = np.expand_dims(shape_list, axis=0) + + start_time = time.time() + self.vino_session.infer(inputs=[img]) + vino_preds = self.vino_session.get_output_tensor().data + + post_result = self.postprocess_op(vino_preds, shape_list) + dt_boxes = post_result[0]['points'] + dt_boxes = self.filter_tag_det_res(dt_boxes, ori_im.shape) + elapse = time.time() - start_time + + src_im = draw_text_det_res(dt_boxes, r'E:\PythonProjects\RapidOCR\python\test_images\long1.jpg') + cv2.imwrite('det_results.jpg', src_im) + return dt_boxes, elapse + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--image_path', type=str, help='image_path|image_dir') + parser.add_argument('--model_path', type=str, help='model_path') + args = parser.parse_args() + + text_detector = TextDetector(args.model_path) + + img, flag = check_and_read_gif(args.image_path) + if not flag: + img = cv2.imread(args.image_path) + if img is None: + raise ValueError(f"error in loading image:{args.image_path}") + + dt_boxes, elapse = text_detector(img) + + src_im = draw_text_det_res(dt_boxes, args.image_path) + cv2.imwrite('det_results.jpg', src_im) + print(f'图像已经保存为det_results.jpg了\t{elapse:.6f}s') + diff --git a/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_ir/utils.py b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_ir/utils.py new file mode 100644 index 0000000..dfbea6c --- /dev/null +++ b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_ir/utils.py @@ -0,0 +1,414 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +# coding:utf-8 +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import os +import sys + +import cv2 +import numpy as np +import pyclipper +import six +from shapely.geometry import Polygon + + +class DecodeImage(object): + """ decode image """ + + def __init__(self, img_mode='RGB', channel_first=False, **kwargs): + self.img_mode = img_mode + self.channel_first = channel_first + + def __call__(self, data): + img = data['image'] + if six.PY2: + assert type(img) is str and len( + img) > 0, "invalid input 'img' in DecodeImage" + else: + assert type(img) is bytes and len( + img) > 0, "invalid input 'img' in DecodeImage" + img = np.frombuffer(img, dtype='uint8') + img = cv2.imdecode(img, 1) + if img is None: + return None + if self.img_mode == 'GRAY': + img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) + elif self.img_mode == 'RGB': + assert img.shape[2] == 3, 'invalid shape of image[%s]' % ( + img.shape) + img = img[:, :, ::-1] + + if self.channel_first: + img = img.transpose((2, 0, 1)) + + data['image'] = img + return data + + +class NormalizeImage(object): + """ normalize image such as substract mean, divide std + """ + + def __init__(self, scale=None, mean=None, std=None, order='chw', **kwargs): + if isinstance(scale, str): + scale = eval(scale) + self.scale = np.float32(scale if scale is not None else 1.0 / 255.0) + mean = mean if mean is not None else [0.485, 0.456, 0.406] + std = std if std is not None else [0.229, 0.224, 0.225] + + shape = (3, 1, 1) if order == 'chw' else (1, 1, 3) + self.mean = np.array(mean).reshape(shape).astype('float32') + self.std = np.array(std).reshape(shape).astype('float32') + + def __call__(self, data): + img = data['image'] + from PIL import Image + if isinstance(img, Image.Image): + img = np.array(img) + + assert isinstance(img, + np.ndarray), "invalid input 'img' in NormalizeImage" + data['image'] = ( + img.astype('float32') * self.scale - self.mean) / self.std + return data + + +class ToCHWImage(object): + """ convert hwc image to chw image + """ + + def __init__(self, **kwargs): + pass + + def __call__(self, data): + img = data['image'] + from PIL import Image + if isinstance(img, Image.Image): + img = np.array(img) + data['image'] = img.transpose((2, 0, 1)) + return data + + +class KeepKeys(object): + def __init__(self, keep_keys, **kwargs): + self.keep_keys = keep_keys + + def __call__(self, data): + data_list = [] + for key in self.keep_keys: + data_list.append(data[key]) + return data_list + + +class DetResizeForTest(object): + def __init__(self, **kwargs): + super(DetResizeForTest, self).__init__() + self.resize_type = 0 + if 'image_shape' in kwargs: + self.image_shape = kwargs['image_shape'] + self.resize_type = 1 + elif 'limit_side_len' in kwargs: + self.limit_side_len = kwargs['limit_side_len'] + self.limit_type = kwargs.get('limit_type', 'min') + + if 'resize_long' in kwargs: + self.resize_type = 2 + self.resize_long = kwargs.get('resize_long', 960) + else: + self.limit_side_len = 736 + self.limit_type = 'min' + + def __call__(self, data): + img = data['image'] + src_h, src_w, _ = img.shape + + if self.resize_type == 0: + # img, shape = self.resize_image_type0(img) + img, [ratio_h, ratio_w] = self.resize_image_type0(img) + elif self.resize_type == 2: + img, [ratio_h, ratio_w] = self.resize_image_type2(img) + else: + # img, shape = self.resize_image_type1(img) + img, [ratio_h, ratio_w] = self.resize_image_type1(img) + data['image'] = img + data['shape'] = np.array([src_h, src_w, ratio_h, ratio_w]) + return data + + def resize_image_type1(self, img): + resize_h, resize_w = self.image_shape + ori_h, ori_w = img.shape[:2] # (h, w, c) + ratio_h = float(resize_h) / ori_h + ratio_w = float(resize_w) / ori_w + img = cv2.resize(img, (int(resize_w), int(resize_h))) + # return img, np.array([ori_h, ori_w]) + return img, [ratio_h, ratio_w] + + 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 self.limit_type == 'max': + 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. + else: + if min(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] + + def resize_image_type2(self, img): + h, w, _ = img.shape + + resize_w = w + resize_h = h + + # Fix the longer side + if resize_h > resize_w: + ratio = float(self.resize_long) / resize_h + else: + ratio = float(self.resize_long) / resize_w + + resize_h = int(resize_h * ratio) + resize_w = int(resize_w * ratio) + + max_stride = 128 + resize_h = (resize_h + max_stride - 1) // max_stride * max_stride + resize_w = (resize_w + max_stride - 1) // max_stride * max_stride + img = cv2.resize(img, (int(resize_w), int(resize_h))) + ratio_h = resize_h / float(h) + ratio_w = resize_w / float(w) + + return img, [ratio_h, ratio_w] + + +def transform(data, ops=None): + """ transform """ + if ops is None: + ops = [] + for op in ops: + data = op(data) + if data is None: + return None + return data + + +def check_and_read_gif(img_path): + if os.path.basename(img_path)[-3:] in ['gif', 'GIF']: + gif = cv2.VideoCapture(img_path) + ret, frame = gif.read() + if not ret: + print("Cannot read {}. This gif image maybe corrupted.") + return None, False + if len(frame.shape) == 2 or frame.shape[-1] == 1: + frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2RGB) + imgvalue = frame[:, :, ::-1] + return imgvalue, True + return None, False + + +def create_operators(op_param_list, global_config=None): + """ + create operators based on the config + + Args: + params(list): a dict list, used to create some operators + """ + assert isinstance( + op_param_list, list), ('operator config should be a list') + ops = [] + for operator in op_param_list: + assert isinstance(operator, + dict) and len(operator) == 1, "yaml format error" + op_name = list(operator)[0] + param = {} if operator[op_name] is None else operator[op_name] + if global_config is not None: + param.update(global_config) + op = eval(op_name)(**param) + ops.append(op) + return ops + + +def draw_text_det_res(dt_boxes, img_path): + src_im = cv2.imread(img_path) + for box in dt_boxes: + box = np.array(box).astype(np.int32).reshape(-1, 2) + cv2.polylines(src_im, [box], True, + color=(255, 255, 0), thickness=2) + return src_im + + +class DBPostProcess(object): + """ + The post process for Differentiable Binarization (DB). + """ + + def __init__(self, + thresh=0.3, + box_thresh=0.7, + max_candidates=1000, + unclip_ratio=2.0, + use_dilation=False, + **kwargs): + self.thresh = thresh + self.box_thresh = box_thresh + self.max_candidates = max_candidates + self.unclip_ratio = unclip_ratio + self.min_size = 3 + self.dilation_kernel = None if not use_dilation else np.array( + [[1, 1], [1, 1]]) + + 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 + + def unclip(self, box): + unclip_ratio = self.unclip_ratio + poly = Polygon(box) + distance = poly.area * unclip_ratio / poly.length + offset = pyclipper.PyclipperOffset() + offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) + expanded = np.array(offset.Execute(distance)) + return expanded + + def get_mini_boxes(self, contour): + bounding_box = cv2.minAreaRect(contour) + points = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0]) + + index_1, index_2, index_3, index_4 = 0, 1, 2, 3 + if points[1][1] > points[0][1]: + index_1 = 0 + index_4 = 1 + else: + index_1 = 1 + index_4 = 0 + if points[3][1] > points[2][1]: + index_2 = 2 + index_3 = 3 + else: + index_2 = 3 + index_3 = 2 + + box = [ + points[index_1], points[index_2], points[index_3], points[index_4] + ] + return box, min(bounding_box[1]) + + def box_score_fast(self, bitmap, _box): + h, w = bitmap.shape[:2] + box = _box.copy() + xmin = np.clip(np.floor(box[:, 0].min()).astype(np.int), 0, w - 1) + xmax = np.clip(np.ceil(box[:, 0].max()).astype(np.int), 0, w - 1) + ymin = np.clip(np.floor(box[:, 1].min()).astype(np.int), 0, h - 1) + ymax = np.clip(np.ceil(box[:, 1].max()).astype(np.int), 0, h - 1) + + mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8) + box[:, 0] = box[:, 0] - xmin + box[:, 1] = box[:, 1] - ymin + cv2.fillPoly(mask, box.reshape(1, -1, 2).astype(np.int32), 1) + return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0] + + def __call__(self, pred, shape_list): + pred = pred[:, 0, :, :] + segmentation = pred > self.thresh + + boxes_batch = [] + for batch_index in range(pred.shape[0]): + src_h, src_w, ratio_h, ratio_w = shape_list[batch_index] + if self.dilation_kernel is not None: + mask = cv2.dilate( + np.array(segmentation[batch_index]).astype(np.uint8), + self.dilation_kernel) + else: + mask = segmentation[batch_index] + boxes, scores = self.boxes_from_bitmap(pred[batch_index], mask, + src_w, src_h) + + boxes_batch.append({'points': boxes}) + return boxes_batch diff --git a/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_onnx/__init__.py b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_onnx/__init__.py new file mode 100644 index 0000000..d7a3e0b --- /dev/null +++ b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_onnx/__init__.py @@ -0,0 +1,6 @@ +# !/usr/bin/env python +# -*- encoding: utf-8 -*- +# @File: __init__.py +# @Time: 2021/01/23 15:24:27 +# @Author: Max +from .text_detect import TextDetector \ No newline at end of file diff --git a/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_onnx/run.sh b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_onnx/run.sh new file mode 100644 index 0000000..99fc806 --- /dev/null +++ b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_onnx/run.sh @@ -0,0 +1,9 @@ +#! /bin/bash +set -e errexit + +root_path=$(cd $(dirname $0); pwd) +image_path=${root_path}"/test_images/det_images/1.jpg" +model_path=${root_path}"/models/ch_ppocr_mobile_v2.0_det_infer.onnx" + +python text_detect.py --image_path ${image_path} \ + --model_path ${model_path} \ No newline at end of file diff --git a/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_onnx/text_detect.py b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_onnx/text_detect.py new file mode 100644 index 0000000..1029971 --- /dev/null +++ b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_onnx/text_detect.py @@ -0,0 +1,150 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# !/usr/bin/env python +# -*- encoding: utf-8 -*- +import argparse +import time + +import cv2 +import numpy as np +from openvino.runtime import Core + +try: + from .utils import (DBPostProcess, check_and_read_gif, create_operators, + draw_text_det_res, transform) +except: + from utils import (DBPostProcess, check_and_read_gif, create_operators, + draw_text_det_res, transform) + + +class TextDetector(object): + def __init__(self, det_model_path): + pre_process_list = [{ + 'DetResizeForTest': { + 'limit_side_len': 736, + 'limit_type': 'min' + } + }, { + 'NormalizeImage': { + 'std': [0.229, 0.224, 0.225], + 'mean': [0.485, 0.456, 0.406], + 'scale': '1./255.', + 'order': 'hwc' + } + }, { + 'ToCHWImage': None + }, { + 'KeepKeys': { + 'keep_keys': ['image', 'shape'] + } + }] + + self.preprocess_op = create_operators(pre_process_list) + self.postprocess_op = DBPostProcess(thresh=0.3, + box_thresh=0.5, + max_candidates=1000, + unclip_ratio=1.6, + use_dilation=True) + ie = Core() + model_onnx = ie.read_model(det_model_path) + compile_model = ie.compile_model(model=model_onnx, + device_name='CPU') + self.vino_session = compile_model.create_infer_request() + + def order_points_clockwise(self, pts): + """ + reference from: https://github.com/jrosebr1/imutils/blob/master/imutils/perspective.py + # sort the points based on their x-coordinates + """ + xSorted = pts[np.argsort(pts[:, 0]), :] + + # grab the left-most and right-most points from the sorted + # x-roodinate points + leftMost = xSorted[:2, :] + rightMost = xSorted[2:, :] + + # now, sort the left-most coordinates according to their + # y-coordinates so we can grab the top-left and bottom-left + # points, respectively + leftMost = leftMost[np.argsort(leftMost[:, 1]), :] + (tl, bl) = leftMost + + rightMost = rightMost[np.argsort(rightMost[:, 1]), :] + (tr, br) = rightMost + + rect = np.array([tl, tr, br, bl], dtype="float32") + return rect + + def clip_det_res(self, points, img_height, img_width): + for pno in range(points.shape[0]): + points[pno, 0] = int(min(max(points[pno, 0], 0), img_width - 1)) + points[pno, 1] = int(min(max(points[pno, 1], 0), img_height - 1)) + return points + + 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 __call__(self, img): + ori_im = img.copy() + data = {'image': img} + data = transform(data, self.preprocess_op) + img, shape_list = data + if img is None: + return None, 0 + img = np.expand_dims(img, axis=0) + img = img.astype(np.float32) + shape_list = np.expand_dims(shape_list, axis=0) + + start_time = time.time() + self.vino_session.infer(inputs=[img]) + vino_preds = self.vino_session.get_output_tensor().data + + post_result = self.postprocess_op(vino_preds, shape_list) + dt_boxes = post_result[0]['points'] + dt_boxes = self.filter_tag_det_res(dt_boxes, ori_im.shape) + elapse = time.time() - start_time + return dt_boxes, elapse + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--image_path', type=str, help='image_path|image_dir') + parser.add_argument('--model_path', type=str, help='model_path') + args = parser.parse_args() + + text_detector = TextDetector(args.model_path) + + img, flag = check_and_read_gif(args.image_path) + if not flag: + img = cv2.imread(args.image_path) + if img is None: + raise ValueError(f"error in loading image:{args.image_path}") + + dt_boxes, elapse = text_detector(img) + + src_im = draw_text_det_res(dt_boxes, args.image_path) + cv2.imwrite('det_results.jpg', src_im) + print(f'图像已经保存为det_results.jpg了\t{elapse:.6f}s') + diff --git a/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_onnx/utils.py b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_onnx/utils.py new file mode 100644 index 0000000..dfbea6c --- /dev/null +++ b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_onnx/utils.py @@ -0,0 +1,414 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +# coding:utf-8 +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import os +import sys + +import cv2 +import numpy as np +import pyclipper +import six +from shapely.geometry import Polygon + + +class DecodeImage(object): + """ decode image """ + + def __init__(self, img_mode='RGB', channel_first=False, **kwargs): + self.img_mode = img_mode + self.channel_first = channel_first + + def __call__(self, data): + img = data['image'] + if six.PY2: + assert type(img) is str and len( + img) > 0, "invalid input 'img' in DecodeImage" + else: + assert type(img) is bytes and len( + img) > 0, "invalid input 'img' in DecodeImage" + img = np.frombuffer(img, dtype='uint8') + img = cv2.imdecode(img, 1) + if img is None: + return None + if self.img_mode == 'GRAY': + img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) + elif self.img_mode == 'RGB': + assert img.shape[2] == 3, 'invalid shape of image[%s]' % ( + img.shape) + img = img[:, :, ::-1] + + if self.channel_first: + img = img.transpose((2, 0, 1)) + + data['image'] = img + return data + + +class NormalizeImage(object): + """ normalize image such as substract mean, divide std + """ + + def __init__(self, scale=None, mean=None, std=None, order='chw', **kwargs): + if isinstance(scale, str): + scale = eval(scale) + self.scale = np.float32(scale if scale is not None else 1.0 / 255.0) + mean = mean if mean is not None else [0.485, 0.456, 0.406] + std = std if std is not None else [0.229, 0.224, 0.225] + + shape = (3, 1, 1) if order == 'chw' else (1, 1, 3) + self.mean = np.array(mean).reshape(shape).astype('float32') + self.std = np.array(std).reshape(shape).astype('float32') + + def __call__(self, data): + img = data['image'] + from PIL import Image + if isinstance(img, Image.Image): + img = np.array(img) + + assert isinstance(img, + np.ndarray), "invalid input 'img' in NormalizeImage" + data['image'] = ( + img.astype('float32') * self.scale - self.mean) / self.std + return data + + +class ToCHWImage(object): + """ convert hwc image to chw image + """ + + def __init__(self, **kwargs): + pass + + def __call__(self, data): + img = data['image'] + from PIL import Image + if isinstance(img, Image.Image): + img = np.array(img) + data['image'] = img.transpose((2, 0, 1)) + return data + + +class KeepKeys(object): + def __init__(self, keep_keys, **kwargs): + self.keep_keys = keep_keys + + def __call__(self, data): + data_list = [] + for key in self.keep_keys: + data_list.append(data[key]) + return data_list + + +class DetResizeForTest(object): + def __init__(self, **kwargs): + super(DetResizeForTest, self).__init__() + self.resize_type = 0 + if 'image_shape' in kwargs: + self.image_shape = kwargs['image_shape'] + self.resize_type = 1 + elif 'limit_side_len' in kwargs: + self.limit_side_len = kwargs['limit_side_len'] + self.limit_type = kwargs.get('limit_type', 'min') + + if 'resize_long' in kwargs: + self.resize_type = 2 + self.resize_long = kwargs.get('resize_long', 960) + else: + self.limit_side_len = 736 + self.limit_type = 'min' + + def __call__(self, data): + img = data['image'] + src_h, src_w, _ = img.shape + + if self.resize_type == 0: + # img, shape = self.resize_image_type0(img) + img, [ratio_h, ratio_w] = self.resize_image_type0(img) + elif self.resize_type == 2: + img, [ratio_h, ratio_w] = self.resize_image_type2(img) + else: + # img, shape = self.resize_image_type1(img) + img, [ratio_h, ratio_w] = self.resize_image_type1(img) + data['image'] = img + data['shape'] = np.array([src_h, src_w, ratio_h, ratio_w]) + return data + + def resize_image_type1(self, img): + resize_h, resize_w = self.image_shape + ori_h, ori_w = img.shape[:2] # (h, w, c) + ratio_h = float(resize_h) / ori_h + ratio_w = float(resize_w) / ori_w + img = cv2.resize(img, (int(resize_w), int(resize_h))) + # return img, np.array([ori_h, ori_w]) + return img, [ratio_h, ratio_w] + + 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 self.limit_type == 'max': + 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. + else: + if min(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] + + def resize_image_type2(self, img): + h, w, _ = img.shape + + resize_w = w + resize_h = h + + # Fix the longer side + if resize_h > resize_w: + ratio = float(self.resize_long) / resize_h + else: + ratio = float(self.resize_long) / resize_w + + resize_h = int(resize_h * ratio) + resize_w = int(resize_w * ratio) + + max_stride = 128 + resize_h = (resize_h + max_stride - 1) // max_stride * max_stride + resize_w = (resize_w + max_stride - 1) // max_stride * max_stride + img = cv2.resize(img, (int(resize_w), int(resize_h))) + ratio_h = resize_h / float(h) + ratio_w = resize_w / float(w) + + return img, [ratio_h, ratio_w] + + +def transform(data, ops=None): + """ transform """ + if ops is None: + ops = [] + for op in ops: + data = op(data) + if data is None: + return None + return data + + +def check_and_read_gif(img_path): + if os.path.basename(img_path)[-3:] in ['gif', 'GIF']: + gif = cv2.VideoCapture(img_path) + ret, frame = gif.read() + if not ret: + print("Cannot read {}. This gif image maybe corrupted.") + return None, False + if len(frame.shape) == 2 or frame.shape[-1] == 1: + frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2RGB) + imgvalue = frame[:, :, ::-1] + return imgvalue, True + return None, False + + +def create_operators(op_param_list, global_config=None): + """ + create operators based on the config + + Args: + params(list): a dict list, used to create some operators + """ + assert isinstance( + op_param_list, list), ('operator config should be a list') + ops = [] + for operator in op_param_list: + assert isinstance(operator, + dict) and len(operator) == 1, "yaml format error" + op_name = list(operator)[0] + param = {} if operator[op_name] is None else operator[op_name] + if global_config is not None: + param.update(global_config) + op = eval(op_name)(**param) + ops.append(op) + return ops + + +def draw_text_det_res(dt_boxes, img_path): + src_im = cv2.imread(img_path) + for box in dt_boxes: + box = np.array(box).astype(np.int32).reshape(-1, 2) + cv2.polylines(src_im, [box], True, + color=(255, 255, 0), thickness=2) + return src_im + + +class DBPostProcess(object): + """ + The post process for Differentiable Binarization (DB). + """ + + def __init__(self, + thresh=0.3, + box_thresh=0.7, + max_candidates=1000, + unclip_ratio=2.0, + use_dilation=False, + **kwargs): + self.thresh = thresh + self.box_thresh = box_thresh + self.max_candidates = max_candidates + self.unclip_ratio = unclip_ratio + self.min_size = 3 + self.dilation_kernel = None if not use_dilation else np.array( + [[1, 1], [1, 1]]) + + 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 + + def unclip(self, box): + unclip_ratio = self.unclip_ratio + poly = Polygon(box) + distance = poly.area * unclip_ratio / poly.length + offset = pyclipper.PyclipperOffset() + offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) + expanded = np.array(offset.Execute(distance)) + return expanded + + def get_mini_boxes(self, contour): + bounding_box = cv2.minAreaRect(contour) + points = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0]) + + index_1, index_2, index_3, index_4 = 0, 1, 2, 3 + if points[1][1] > points[0][1]: + index_1 = 0 + index_4 = 1 + else: + index_1 = 1 + index_4 = 0 + if points[3][1] > points[2][1]: + index_2 = 2 + index_3 = 3 + else: + index_2 = 3 + index_3 = 2 + + box = [ + points[index_1], points[index_2], points[index_3], points[index_4] + ] + return box, min(bounding_box[1]) + + def box_score_fast(self, bitmap, _box): + h, w = bitmap.shape[:2] + box = _box.copy() + xmin = np.clip(np.floor(box[:, 0].min()).astype(np.int), 0, w - 1) + xmax = np.clip(np.ceil(box[:, 0].max()).astype(np.int), 0, w - 1) + ymin = np.clip(np.floor(box[:, 1].min()).astype(np.int), 0, h - 1) + ymax = np.clip(np.ceil(box[:, 1].max()).astype(np.int), 0, h - 1) + + mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8) + box[:, 0] = box[:, 0] - xmin + box[:, 1] = box[:, 1] - ymin + cv2.fillPoly(mask, box.reshape(1, -1, 2).astype(np.int32), 1) + return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0] + + def __call__(self, pred, shape_list): + pred = pred[:, 0, :, :] + segmentation = pred > self.thresh + + boxes_batch = [] + for batch_index in range(pred.shape[0]): + src_h, src_w, ratio_h, ratio_w = shape_list[batch_index] + if self.dilation_kernel is not None: + mask = cv2.dilate( + np.array(segmentation[batch_index]).astype(np.uint8), + self.dilation_kernel) + else: + mask = segmentation[batch_index] + boxes, scores = self.boxes_from_bitmap(pred[batch_index], mask, + src_w, src_h) + + boxes_batch.append({'points': boxes}) + return boxes_batch diff --git a/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_paddle/__init__.py b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_paddle/__init__.py new file mode 100644 index 0000000..d7a3e0b --- /dev/null +++ b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_paddle/__init__.py @@ -0,0 +1,6 @@ +# !/usr/bin/env python +# -*- encoding: utf-8 -*- +# @File: __init__.py +# @Time: 2021/01/23 15:24:27 +# @Author: Max +from .text_detect import TextDetector \ No newline at end of file diff --git a/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_paddle/run.sh b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_paddle/run.sh new file mode 100644 index 0000000..99fc806 --- /dev/null +++ b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_paddle/run.sh @@ -0,0 +1,9 @@ +#! /bin/bash +set -e errexit + +root_path=$(cd $(dirname $0); pwd) +image_path=${root_path}"/test_images/det_images/1.jpg" +model_path=${root_path}"/models/ch_ppocr_mobile_v2.0_det_infer.onnx" + +python text_detect.py --image_path ${image_path} \ + --model_path ${model_path} \ No newline at end of file diff --git a/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_paddle/text_detect.py b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_paddle/text_detect.py new file mode 100644 index 0000000..1029971 --- /dev/null +++ b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_paddle/text_detect.py @@ -0,0 +1,150 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# !/usr/bin/env python +# -*- encoding: utf-8 -*- +import argparse +import time + +import cv2 +import numpy as np +from openvino.runtime import Core + +try: + from .utils import (DBPostProcess, check_and_read_gif, create_operators, + draw_text_det_res, transform) +except: + from utils import (DBPostProcess, check_and_read_gif, create_operators, + draw_text_det_res, transform) + + +class TextDetector(object): + def __init__(self, det_model_path): + pre_process_list = [{ + 'DetResizeForTest': { + 'limit_side_len': 736, + 'limit_type': 'min' + } + }, { + 'NormalizeImage': { + 'std': [0.229, 0.224, 0.225], + 'mean': [0.485, 0.456, 0.406], + 'scale': '1./255.', + 'order': 'hwc' + } + }, { + 'ToCHWImage': None + }, { + 'KeepKeys': { + 'keep_keys': ['image', 'shape'] + } + }] + + self.preprocess_op = create_operators(pre_process_list) + self.postprocess_op = DBPostProcess(thresh=0.3, + box_thresh=0.5, + max_candidates=1000, + unclip_ratio=1.6, + use_dilation=True) + ie = Core() + model_onnx = ie.read_model(det_model_path) + compile_model = ie.compile_model(model=model_onnx, + device_name='CPU') + self.vino_session = compile_model.create_infer_request() + + def order_points_clockwise(self, pts): + """ + reference from: https://github.com/jrosebr1/imutils/blob/master/imutils/perspective.py + # sort the points based on their x-coordinates + """ + xSorted = pts[np.argsort(pts[:, 0]), :] + + # grab the left-most and right-most points from the sorted + # x-roodinate points + leftMost = xSorted[:2, :] + rightMost = xSorted[2:, :] + + # now, sort the left-most coordinates according to their + # y-coordinates so we can grab the top-left and bottom-left + # points, respectively + leftMost = leftMost[np.argsort(leftMost[:, 1]), :] + (tl, bl) = leftMost + + rightMost = rightMost[np.argsort(rightMost[:, 1]), :] + (tr, br) = rightMost + + rect = np.array([tl, tr, br, bl], dtype="float32") + return rect + + def clip_det_res(self, points, img_height, img_width): + for pno in range(points.shape[0]): + points[pno, 0] = int(min(max(points[pno, 0], 0), img_width - 1)) + points[pno, 1] = int(min(max(points[pno, 1], 0), img_height - 1)) + return points + + 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 __call__(self, img): + ori_im = img.copy() + data = {'image': img} + data = transform(data, self.preprocess_op) + img, shape_list = data + if img is None: + return None, 0 + img = np.expand_dims(img, axis=0) + img = img.astype(np.float32) + shape_list = np.expand_dims(shape_list, axis=0) + + start_time = time.time() + self.vino_session.infer(inputs=[img]) + vino_preds = self.vino_session.get_output_tensor().data + + post_result = self.postprocess_op(vino_preds, shape_list) + dt_boxes = post_result[0]['points'] + dt_boxes = self.filter_tag_det_res(dt_boxes, ori_im.shape) + elapse = time.time() - start_time + return dt_boxes, elapse + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--image_path', type=str, help='image_path|image_dir') + parser.add_argument('--model_path', type=str, help='model_path') + args = parser.parse_args() + + text_detector = TextDetector(args.model_path) + + img, flag = check_and_read_gif(args.image_path) + if not flag: + img = cv2.imread(args.image_path) + if img is None: + raise ValueError(f"error in loading image:{args.image_path}") + + dt_boxes, elapse = text_detector(img) + + src_im = draw_text_det_res(dt_boxes, args.image_path) + cv2.imwrite('det_results.jpg', src_im) + print(f'图像已经保存为det_results.jpg了\t{elapse:.6f}s') + diff --git a/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_paddle/utils.py b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_paddle/utils.py new file mode 100644 index 0000000..dfbea6c --- /dev/null +++ b/python/openvino_infer/ch_ppocr_mobile_v2_det_openvino_paddle/utils.py @@ -0,0 +1,414 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +# coding:utf-8 +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import os +import sys + +import cv2 +import numpy as np +import pyclipper +import six +from shapely.geometry import Polygon + + +class DecodeImage(object): + """ decode image """ + + def __init__(self, img_mode='RGB', channel_first=False, **kwargs): + self.img_mode = img_mode + self.channel_first = channel_first + + def __call__(self, data): + img = data['image'] + if six.PY2: + assert type(img) is str and len( + img) > 0, "invalid input 'img' in DecodeImage" + else: + assert type(img) is bytes and len( + img) > 0, "invalid input 'img' in DecodeImage" + img = np.frombuffer(img, dtype='uint8') + img = cv2.imdecode(img, 1) + if img is None: + return None + if self.img_mode == 'GRAY': + img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) + elif self.img_mode == 'RGB': + assert img.shape[2] == 3, 'invalid shape of image[%s]' % ( + img.shape) + img = img[:, :, ::-1] + + if self.channel_first: + img = img.transpose((2, 0, 1)) + + data['image'] = img + return data + + +class NormalizeImage(object): + """ normalize image such as substract mean, divide std + """ + + def __init__(self, scale=None, mean=None, std=None, order='chw', **kwargs): + if isinstance(scale, str): + scale = eval(scale) + self.scale = np.float32(scale if scale is not None else 1.0 / 255.0) + mean = mean if mean is not None else [0.485, 0.456, 0.406] + std = std if std is not None else [0.229, 0.224, 0.225] + + shape = (3, 1, 1) if order == 'chw' else (1, 1, 3) + self.mean = np.array(mean).reshape(shape).astype('float32') + self.std = np.array(std).reshape(shape).astype('float32') + + def __call__(self, data): + img = data['image'] + from PIL import Image + if isinstance(img, Image.Image): + img = np.array(img) + + assert isinstance(img, + np.ndarray), "invalid input 'img' in NormalizeImage" + data['image'] = ( + img.astype('float32') * self.scale - self.mean) / self.std + return data + + +class ToCHWImage(object): + """ convert hwc image to chw image + """ + + def __init__(self, **kwargs): + pass + + def __call__(self, data): + img = data['image'] + from PIL import Image + if isinstance(img, Image.Image): + img = np.array(img) + data['image'] = img.transpose((2, 0, 1)) + return data + + +class KeepKeys(object): + def __init__(self, keep_keys, **kwargs): + self.keep_keys = keep_keys + + def __call__(self, data): + data_list = [] + for key in self.keep_keys: + data_list.append(data[key]) + return data_list + + +class DetResizeForTest(object): + def __init__(self, **kwargs): + super(DetResizeForTest, self).__init__() + self.resize_type = 0 + if 'image_shape' in kwargs: + self.image_shape = kwargs['image_shape'] + self.resize_type = 1 + elif 'limit_side_len' in kwargs: + self.limit_side_len = kwargs['limit_side_len'] + self.limit_type = kwargs.get('limit_type', 'min') + + if 'resize_long' in kwargs: + self.resize_type = 2 + self.resize_long = kwargs.get('resize_long', 960) + else: + self.limit_side_len = 736 + self.limit_type = 'min' + + def __call__(self, data): + img = data['image'] + src_h, src_w, _ = img.shape + + if self.resize_type == 0: + # img, shape = self.resize_image_type0(img) + img, [ratio_h, ratio_w] = self.resize_image_type0(img) + elif self.resize_type == 2: + img, [ratio_h, ratio_w] = self.resize_image_type2(img) + else: + # img, shape = self.resize_image_type1(img) + img, [ratio_h, ratio_w] = self.resize_image_type1(img) + data['image'] = img + data['shape'] = np.array([src_h, src_w, ratio_h, ratio_w]) + return data + + def resize_image_type1(self, img): + resize_h, resize_w = self.image_shape + ori_h, ori_w = img.shape[:2] # (h, w, c) + ratio_h = float(resize_h) / ori_h + ratio_w = float(resize_w) / ori_w + img = cv2.resize(img, (int(resize_w), int(resize_h))) + # return img, np.array([ori_h, ori_w]) + return img, [ratio_h, ratio_w] + + 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 self.limit_type == 'max': + 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. + else: + if min(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] + + def resize_image_type2(self, img): + h, w, _ = img.shape + + resize_w = w + resize_h = h + + # Fix the longer side + if resize_h > resize_w: + ratio = float(self.resize_long) / resize_h + else: + ratio = float(self.resize_long) / resize_w + + resize_h = int(resize_h * ratio) + resize_w = int(resize_w * ratio) + + max_stride = 128 + resize_h = (resize_h + max_stride - 1) // max_stride * max_stride + resize_w = (resize_w + max_stride - 1) // max_stride * max_stride + img = cv2.resize(img, (int(resize_w), int(resize_h))) + ratio_h = resize_h / float(h) + ratio_w = resize_w / float(w) + + return img, [ratio_h, ratio_w] + + +def transform(data, ops=None): + """ transform """ + if ops is None: + ops = [] + for op in ops: + data = op(data) + if data is None: + return None + return data + + +def check_and_read_gif(img_path): + if os.path.basename(img_path)[-3:] in ['gif', 'GIF']: + gif = cv2.VideoCapture(img_path) + ret, frame = gif.read() + if not ret: + print("Cannot read {}. This gif image maybe corrupted.") + return None, False + if len(frame.shape) == 2 or frame.shape[-1] == 1: + frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2RGB) + imgvalue = frame[:, :, ::-1] + return imgvalue, True + return None, False + + +def create_operators(op_param_list, global_config=None): + """ + create operators based on the config + + Args: + params(list): a dict list, used to create some operators + """ + assert isinstance( + op_param_list, list), ('operator config should be a list') + ops = [] + for operator in op_param_list: + assert isinstance(operator, + dict) and len(operator) == 1, "yaml format error" + op_name = list(operator)[0] + param = {} if operator[op_name] is None else operator[op_name] + if global_config is not None: + param.update(global_config) + op = eval(op_name)(**param) + ops.append(op) + return ops + + +def draw_text_det_res(dt_boxes, img_path): + src_im = cv2.imread(img_path) + for box in dt_boxes: + box = np.array(box).astype(np.int32).reshape(-1, 2) + cv2.polylines(src_im, [box], True, + color=(255, 255, 0), thickness=2) + return src_im + + +class DBPostProcess(object): + """ + The post process for Differentiable Binarization (DB). + """ + + def __init__(self, + thresh=0.3, + box_thresh=0.7, + max_candidates=1000, + unclip_ratio=2.0, + use_dilation=False, + **kwargs): + self.thresh = thresh + self.box_thresh = box_thresh + self.max_candidates = max_candidates + self.unclip_ratio = unclip_ratio + self.min_size = 3 + self.dilation_kernel = None if not use_dilation else np.array( + [[1, 1], [1, 1]]) + + 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 + + def unclip(self, box): + unclip_ratio = self.unclip_ratio + poly = Polygon(box) + distance = poly.area * unclip_ratio / poly.length + offset = pyclipper.PyclipperOffset() + offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) + expanded = np.array(offset.Execute(distance)) + return expanded + + def get_mini_boxes(self, contour): + bounding_box = cv2.minAreaRect(contour) + points = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0]) + + index_1, index_2, index_3, index_4 = 0, 1, 2, 3 + if points[1][1] > points[0][1]: + index_1 = 0 + index_4 = 1 + else: + index_1 = 1 + index_4 = 0 + if points[3][1] > points[2][1]: + index_2 = 2 + index_3 = 3 + else: + index_2 = 3 + index_3 = 2 + + box = [ + points[index_1], points[index_2], points[index_3], points[index_4] + ] + return box, min(bounding_box[1]) + + def box_score_fast(self, bitmap, _box): + h, w = bitmap.shape[:2] + box = _box.copy() + xmin = np.clip(np.floor(box[:, 0].min()).astype(np.int), 0, w - 1) + xmax = np.clip(np.ceil(box[:, 0].max()).astype(np.int), 0, w - 1) + ymin = np.clip(np.floor(box[:, 1].min()).astype(np.int), 0, h - 1) + ymax = np.clip(np.ceil(box[:, 1].max()).astype(np.int), 0, h - 1) + + mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8) + box[:, 0] = box[:, 0] - xmin + box[:, 1] = box[:, 1] - ymin + cv2.fillPoly(mask, box.reshape(1, -1, 2).astype(np.int32), 1) + return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0] + + def __call__(self, pred, shape_list): + pred = pred[:, 0, :, :] + segmentation = pred > self.thresh + + boxes_batch = [] + for batch_index in range(pred.shape[0]): + src_h, src_w, ratio_h, ratio_w = shape_list[batch_index] + if self.dilation_kernel is not None: + mask = cv2.dilate( + np.array(segmentation[batch_index]).astype(np.uint8), + self.dilation_kernel) + else: + mask = segmentation[batch_index] + boxes, scores = self.boxes_from_bitmap(pred[batch_index], mask, + src_w, src_h) + + boxes_batch.append({'points': boxes}) + return boxes_batch diff --git a/python/openvino_infer/test_openvino.py b/python/openvino_infer/test_openvino.py new file mode 100644 index 0000000..308838b --- /dev/null +++ b/python/openvino_infer/test_openvino.py @@ -0,0 +1,59 @@ +# !/usr/bin/env python +# -*- encoding: utf-8 -*- +# @File: test_openvino.py +# @Author: SWHL +# @Contact: liekkaskono@163.com +import numpy as np + +# Infer ONNXRuntime +# import cv2 + +# from ch_ppocr_mobile_v2_det import TextDetector + +# det_model_path = 'models/ch_PP-OCRv2_det_infer.onnx' + +# img_path = 'test_images/long1.jpg' +# img = cv2.imread(img_path) + +# text_detect_onnx = TextDetector(det_model_path) +# dt_boxes, elapse = text_detect_onnx(img) +# print(f'onnxruntime: {elapse}') + + +# # Infer ONNX +# import cv2 +# from ch_ppocr_mobile_v2_det_openvino_onnx import TextDetector as TextDetectorVino + +# det_model_path = 'models/ch_PP-OCRv2_det_infer.onnx' +# img_path = 'test_images/det_images/ch_en_num.jpg' +# img = cv2.imread(img_path) + +# text_detect_vino = TextDetectorVino(det_model_path) +# dt_boxes, elapse = text_detect_vino(img) +# print(f'openvino ONNX: {elapse}\tdt_boxes:{np.mean(dt_boxes)}') + +# # Infer IR +import cv2 +from ch_ppocr_mobile_v2_det_openvino_ir import TextDetector as TextDetectorIR + +det_model_path = 'models/IR/FP16/ch_PP-OCRv2_det_infer.xml' +img_path = 'test_images/long1.jpg' +img = cv2.imread(img_path) + +text_detect_ir = TextDetectorIR(det_model_path) +dt_boxes, elapse = text_detect_ir(img) +print(f'FP16 openvino IR: {elapse}\tdt_boxes:{np.mean(dt_boxes)}') + +# Infer paddle 有误 +# RuntimeError: Can't SetBlob with name: x, because model input (shape={?,3,960,960}) and blob (shape=(1.3.12128.800)) are incompatible +# import cv2 +# from ch_ppocr_mobile_v2_det_openvino_paddle import TextDetector as TextDetectorPaddle + +# det_model_path = 'models/ch_PP-OCRv2_det_infer/ch_PP-OCRv2_det_infer/inference.pdmodel' +# img_path = 'test_images/long1.jpg' +# img = cv2.imread(img_path) + +# text_detect_ir = TextDetectorPaddle(det_model_path) +# dt_boxes, elapse = text_detect_ir(img) +# print(f'openvino IR: {elapse}') +