diff --git a/.gitattributes b/.gitattributes index d93e4fa4f07b5ef94c94cd9ba1fa4e3486514f6f..eb41d37bed3d222f966bb43b667b0756d55e7c07 100644 --- a/.gitattributes +++ b/.gitattributes @@ -49,3 +49,47 @@ ocr_webapp/PaddleOCR/deploy/cpp_infer/docs/imgs/cmake_step3.png filter=lfs diff= ocr_webapp/PaddleOCR/deploy/cpp_infer/docs/imgs/cmake_step4.png filter=lfs diff=lfs merge=lfs -text ocr_webapp/PaddleOCR/deploy/cpp_infer/docs/imgs/result.jpg filter=lfs diff=lfs merge=lfs -text ocr_webapp/PaddleOCR/deploy/cpp_infer/docs/imgs/vs_step1.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/SampleImages/general_ocr_002.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/ios_ocr_validation_reference.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/Jetson/images/00057937.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/Jetson/images/det_res_french_0.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/paddle2onnx/images/lite_demo_onnx.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/paddle2onnx/images/lite_demo_paddle.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/paddlecloud/images/architecture.jpeg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/paddlecloud/images/paddlecloud.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/paddlejs/paddlejs_demo.gif filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/pdserving/imgs/demo.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/pdserving/imgs/pipeline_result.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/pdserving/imgs/results.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/pdserving/imgs/start_server.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/deploy/ppocr-android/ppocr-sdk/src/androidTest/res/raw/android_ocr_benchmark_reference.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/banner.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/ArT.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/captcha_demo.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/ccpd_demo.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/CDLA_demo/val_0633.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/CDLA_demo/val_0941.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/ch_street_rec_1.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/ch_street_rec_2.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/doc.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/funsd_demo/gt_train_00040534.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/funsd_demo/gt_train_00070353.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/icdar_rec.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/labelme.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/LSVT_1.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/publaynet_demo/gt_PMC3724501_00006.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/publaynet_demo/gt_PMC5086060_00002.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/roLabelImg.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/table_tal_demo/1.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/table_tal_demo/2.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/tablebank_demo/004.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/tablebank_demo/005.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/VoTT.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/wildreceipt_demo/1bbe854b8817dedb8585e0732089fd1f752d2cec.jpeg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/wildreceipt_demo/2769.jpeg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/xfund_demo/gt_zh_train_0.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/datasets/xfund_demo/gt_zh_train_1.jpg filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/demo/error.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/demo/proxy.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/deployment.png filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/doc/deployment_en.png filter=lfs diff=lfs merge=lfs -text diff --git a/ocr_webapp/PaddleOCR/deploy/Jetson/images/00057937.jpg b/ocr_webapp/PaddleOCR/deploy/Jetson/images/00057937.jpg new file mode 100644 index 0000000000000000000000000000000000000000..461e09232ff89473cefae3416a5d41aa7b7ff4bb --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/Jetson/images/00057937.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6132df199f3a548df8cccf512cbeaeb8533a2610798627b19bb2cc2a1ffca9f +size 292585 diff --git a/ocr_webapp/PaddleOCR/deploy/Jetson/images/det_res_french_0.jpg b/ocr_webapp/PaddleOCR/deploy/Jetson/images/det_res_french_0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4b6e951df8e18563b0078244428bb6eb6367e8c2 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/Jetson/images/det_res_french_0.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:777690a87047fcf90f0bd5c520853fe6d75deb4bd0bbb68d6a36ceb2d33de875 +size 223382 diff --git a/ocr_webapp/PaddleOCR/deploy/Jetson/readme.md b/ocr_webapp/PaddleOCR/deploy/Jetson/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..14c88c3c3c8aa8e4bbfe6ce04cb51c58af762351 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/Jetson/readme.md @@ -0,0 +1,84 @@ +English | [简体中文](readme_ch.md) + +# Jetson Deployment for PaddleOCR + +This section introduces the deployment of PaddleOCR on Jetson NX, TX2, nano, AGX and other series of hardware. + + +## 1. Prepare Environment + +You need to prepare a Jetson development hardware. If you need TensorRT, you need to prepare the TensorRT environment. It is recommended to use TensorRT version 7.1.3; + +1. Install PaddlePaddle in Jetson + +The PaddlePaddle download [link](https://www.paddlepaddle.org.cn/inference/user_guides/download_lib.html#python) +Please select the appropriate installation package for your Jetpack version, cuda version, and trt version. Here, we download paddlepaddle_gpu-2.3.0rc0-cp36-cp36m-linux_aarch64.whl. + +Install PaddlePaddle: +```shell +pip3 install -U paddlepaddle_gpu-2.3.0rc0-cp36-cp36m-linux_aarch64.whl +``` + + +2. Download PaddleOCR code and install dependencies + +Clone the PaddleOCR code: +``` +git clone https://github.com/PaddlePaddle/PaddleOCR +``` + +and install dependencies: +``` +cd PaddleOCR +pip3 install -r requirements.txt +``` + +*Note: Jetson hardware CPU is poor, dependency installation is slow, please wait patiently* + +## 2. Perform prediction + +Obtain the PPOCR model from the [document](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/doc/doc_en/ppocr_introduction_en.md#6-model-zoo) model library. The following takes the PP-OCRv3 model as an example to introduce the use of the PPOCR model on Jetson: + +Download and unzip the PP-OCRv3 models. +``` +wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar +wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_infer.tar +tar xf ch_PP-OCRv3_det_infer.tar +tar xf ch_PP-OCRv3_rec_infer.tar +``` + +The text detection inference: +``` +cd PaddleOCR +python3 tools/infer/predict_det.py --det_model_dir=./inference/ch_PP-OCRv2_det_infer/ --image_dir=./doc/imgs/french_0.jpg --use_gpu=True +``` + +After executing the command, the predicted information will be printed out in the terminal, and the visualization results will be saved in the `./inference_results/` directory. +![](./images/det_res_french_0.jpg) + + +The text recognition inference: +``` +python3 tools/infer/predict_det.py --rec_model_dir=./inference/ch_PP-OCRv2_rec_infer/ --image_dir=./doc/imgs_words/en/word_2.png --use_gpu=True --rec_image_shape="3,48,320" +``` + +After executing the command, the predicted information will be printed on the terminal, and the output is as follows: +``` +[2022/04/28 15:41:45] root INFO: Predicts of ./doc/imgs_words/en/word_2.png:('yourself', 0.98084533) +``` + +The text detection and text recognition inference: + +``` +python3 tools/infer/predict_system.py --det_model_dir=./inference/ch_PP-OCRv2_det_infer/ --rec_model_dir=./inference/ch_PP-OCRv2_rec_infer/ --image_dir=./doc/imgs/00057937.jpg --use_gpu=True --rec_image_shape="3,48,320" +``` + +After executing the command, the predicted information will be printed out in the terminal, and the visualization results will be saved in the `./inference_results/` directory. +![](./images/00057937.jpg) + +To enable TRT prediction, you only need to set `--use_tensorrt=True` on the basis of the above command: +``` +python3 tools/infer/predict_system.py --det_model_dir=./inference/ch_PP-OCRv2_det_infer/ --rec_model_dir=./inference/ch_PP-OCRv2_rec_infer/ --image_dir=./doc/imgs/ --rec_image_shape="3,48,320" --use_gpu=True --use_tensorrt=True +``` + +For more ppocr model predictions, please refer to[document](../../doc/doc_en/models_list_en.md) diff --git a/ocr_webapp/PaddleOCR/deploy/Jetson/readme_ch.md b/ocr_webapp/PaddleOCR/deploy/Jetson/readme_ch.md new file mode 100644 index 0000000000000000000000000000000000000000..7b0a344ffe3b4c0abefc78e50371b5c92aa25001 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/Jetson/readme_ch.md @@ -0,0 +1,86 @@ +[English](readme.md) | 简体中文 + +# Jetson部署PaddleOCR模型 + +本节介绍PaddleOCR在Jetson NX、TX2、nano、AGX等系列硬件的部署。 + + +## 1. 环境准备 + +需要准备一台Jetson开发板,如果需要TensorRT预测,需准备好TensorRT环境,建议使用7.1.3版本的TensorRT; + +1. Jetson安装PaddlePaddle + +PaddlePaddle下载[链接](https://www.paddlepaddle.org.cn/inference/user_guides/download_lib.html#python) +请选择适合的您Jetpack版本、cuda版本、trt版本的安装包。 + +安装命令: +```shell +# 安装paddle,以paddlepaddle_gpu-2.3.0rc0-cp36-cp36m-linux_aarch64.whl 为例 +pip3 install -U paddlepaddle_gpu-2.3.0rc0-cp36-cp36m-linux_aarch64.whl +``` + + +2. 下载PaddleOCR代码并安装依赖 + +首先 clone PaddleOCR 代码: +``` +git clone https://github.com/PaddlePaddle/PaddleOCR +``` + +然后,安装依赖: +``` +cd PaddleOCR +pip3 install -r requirements.txt +``` + +*注:jetson硬件CPU较差,依赖安装较慢,请耐心等待* + + +## 2. 执行预测 + +从[文档](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/doc/doc_ch/ppocr_introduction.md#6-%E6%A8%A1%E5%9E%8B%E5%BA%93) 模型库中获取PPOCR模型,下面以PP-OCRv3模型为例,介绍在PPOCR模型在jetson上的使用方式: + +下载并解压PP-OCRv3模型 +``` +wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar +wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_infer.tar +tar xf ch_PP-OCRv3_det_infer.tar +tar xf ch_PP-OCRv3_rec_infer.tar +``` + +执行文本检测预测: +``` +cd PaddleOCR +python3 tools/infer/predict_det.py --det_model_dir=./inference/ch_PP-OCRv2_det_infer/ --image_dir=./doc/imgs/french_0.jpg --use_gpu=True +``` + +执行命令后在终端会打印出预测的信息,并在 `./inference_results/` 下保存可视化结果。 +![](./images/det_res_french_0.jpg) + + +执行文本识别预测: +``` +python3 tools/infer/predict_det.py --rec_model_dir=./inference/ch_PP-OCRv2_rec_infer/ --image_dir=./doc/imgs_words/en/word_2.png --use_gpu=True --rec_image_shape="3,48,320" +``` + +执行命令后在终端会打印出预测的信息,输出如下: +``` +[2022/04/28 15:41:45] root INFO: Predicts of ./doc/imgs_words/en/word_2.png:('yourself', 0.98084533) +``` + +执行文本检测+文本识别串联预测: + +``` +python3 tools/infer/predict_system.py --det_model_dir=./inference/ch_PP-OCRv2_det_infer/ --rec_model_dir=./inference/ch_PP-OCRv2_rec_infer/ --image_dir=./doc/imgs/ --use_gpu=True --rec_image_shape="3,48,320" +``` + +执行命令后在终端会打印出预测的信息,并在 `./inference_results/` 下保存可视化结果。 +![](./images/00057937.jpg) + +开启TRT预测只需要在以上命令基础上设置`--use_tensorrt=True`即可: +``` +python3 tools/infer/predict_system.py --det_model_dir=./inference/ch_PP-OCRv2_det_infer/ --rec_model_dir=./inference/ch_PP-OCRv2_rec_infer/ --image_dir=./doc/imgs/00057937.jpg --use_gpu=True --use_tensorrt=True --rec_image_shape="3,48,320" +``` + +更多ppocr模型预测请参考[文档](../../doc/doc_ch/models_list.md) diff --git a/ocr_webapp/PaddleOCR/deploy/README.md b/ocr_webapp/PaddleOCR/deploy/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0cfb793f92547de1215277f5e2348bc157ec8503 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/README.md @@ -0,0 +1,31 @@ +English | [简体中文](README_ch.md) + +# PP-OCR Deployment + +- [Paddle Deployment Introduction](#1) +- [PP-OCR Deployment](#2) + + +## Paddle Deployment Introduction + +Paddle provides a variety of deployment schemes to meet the deployment requirements of different scenarios. Please choose according to the actual situation: + +
+ +
+ + + +## PP-OCR Deployment + +PP-OCR has supported muti deployment schemes. Click the link to get the specific tutorial. + +- [Python Inference](../doc/doc_en/inference_ppocr_en.md) +- [C++ Inference](./cpp_infer/readme.md) +- [Serving (Python/C++)](./pdserving/README.md) +- [Paddle-Lite (ARM CPU/OpenCL ARM GPU)](./lite/readme.md) +- [Paddle.js](./paddlejs/README.md) +- [Jetson Inference](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/deploy/Jetson/readme.md) +- [Paddle2ONNX](./paddle2onnx/readme.md) + +If you need the deployment tutorial of academic algorithm models other than PP-OCR, please directly enter the main page of corresponding algorithms, [entrance](../doc/doc_en/algorithm_overview_en.md)。 diff --git a/ocr_webapp/PaddleOCR/deploy/README_ch.md b/ocr_webapp/PaddleOCR/deploy/README_ch.md new file mode 100644 index 0000000000000000000000000000000000000000..1773aedc2c757499bfc1f950e468a98c477636ac --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/README_ch.md @@ -0,0 +1,31 @@ +[English](README.md) | 简体中文 + +# PP-OCR 模型推理部署 + +- [Paddle 推理部署方式简介](#1) +- [PP-OCR 推理部署](#2) + + +## Paddle 推理部署方式简介 + +飞桨提供多种部署方案,以满足不同场景的部署需求,请根据实际情况进行选择: + +
+ +
+ + + +## PP-OCR 推理部署 + +PP-OCR模型已打通多种场景部署方案,点击链接获取具体的使用教程。 + +- [Python 推理](../doc/doc_ch/inference_ppocr.md) +- [C++ 推理](./cpp_infer/readme_ch.md) +- [Serving 服务化部署(Python/C++)](./pdserving/README_CN.md) +- [Paddle-Lite 端侧部署(ARM CPU/OpenCL ARM GPU)](./lite/readme_ch.md) +- [Paddle.js 部署](./paddlejs/README_ch.md) +- [Jetson 推理](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/deploy/Jetson/readme_ch.md) +- [Paddle2ONNX 推理](./paddle2onnx/readme_ch.md) + +需要PP-OCR以外的学术算法模型的推理部署,请直接进入相应算法主页面,[入口](../doc/doc_ch/algorithm_overview.md)。 \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/deploy/fastdeploy/sophgo/README.md b/ocr_webapp/PaddleOCR/deploy/fastdeploy/sophgo/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9fd2e9563f48263cda37e98a6b4cf49ec5d53b4f --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/fastdeploy/sophgo/README.md @@ -0,0 +1,102 @@ +[English](README.md) | 简体中文 + +# PaddleOCR 模型在SOPHGO上部署方案-FastDeploy + +## 1. 说明 +PaddleOCR支持通过FastDeploy在SOPHGO上部署相关模型. + +## 2.支持模型列表 + +下表中的模型下载链接由PaddleOCR模型库提供, 详见[PP-OCR系列模型列表](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.6/doc/doc_ch/models_list.md) + +| PaddleOCR版本 | 文本框检测 | 方向分类模型 | 文字识别 |字典文件| 说明 | +|:----|:----|:----|:----|:----|:--------| +| ch_PP-OCRv3[推荐] |[ch_PP-OCRv3_det](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar) | [ch_ppocr_mobile_v2.0_cls](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) | [ch_PP-OCRv3_rec](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_infer.tar) | [ppocr_keys_v1.txt](https://bj.bcebos.com/paddlehub/fastdeploy/ppocr_keys_v1.txt) | OCRv3系列原始超轻量模型,支持中英文、多语种文本检测 | +| en_PP-OCRv3[推荐] |[en_PP-OCRv3_det](https://paddleocr.bj.bcebos.com/PP-OCRv3/english/en_PP-OCRv3_det_infer.tar) | [ch_ppocr_mobile_v2.0_cls](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) | [en_PP-OCRv3_rec](https://paddleocr.bj.bcebos.com/PP-OCRv3/english/en_PP-OCRv3_rec_infer.tar) | [en_dict.txt](https://bj.bcebos.com/paddlehub/fastdeploy/en_dict.txt) | OCRv3系列原始超轻量模型,支持英文与数字识别,除检测模型和识别模型的训练数据与中文模型不同以外,无其他区别 | +| ch_PP-OCRv2 |[ch_PP-OCRv2_det](https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_det_infer.tar) | [ch_ppocr_mobile_v2.0_cls](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) | [ch_PP-OCRv2_rec](https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_rec_infer.tar) | [ppocr_keys_v1.txt](https://bj.bcebos.com/paddlehub/fastdeploy/ppocr_keys_v1.txt) | OCRv2系列原始超轻量模型,支持中英文、多语种文本检测 | +| ch_PP-OCRv2_mobile |[ch_ppocr_mobile_v2.0_det](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar) | [ch_ppocr_mobile_v2.0_cls](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) | [ch_ppocr_mobile_v2.0_rec](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_infer.tar) | [ppocr_keys_v1.txt](https://bj.bcebos.com/paddlehub/fastdeploy/ppocr_keys_v1.txt) | OCRv2系列原始超轻量模型,支持中英文、多语种文本检测,比PPOCRv2更加轻量 | +| ch_PP-OCRv2_server |[ch_ppocr_server_v2.0_det](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_det_infer.tar) | [ch_ppocr_mobile_v2.0_cls](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) | [ch_ppocr_server_v2.0_rec](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_rec_infer.tar) |[ppocr_keys_v1.txt](https://bj.bcebos.com/paddlehub/fastdeploy/ppocr_keys_v1.txt) | OCRv2服务器系列模型, 支持中英文、多语种文本检测,比超轻量模型更大,但效果更好| + +## 3. 准备PP-OCR推理模型以及转换模型 + +PP-OCRv3包括文本检测模型(ch_PP-OCRv3_det)、方向分类模型(ch_ppocr_mobile_v2.0_cls)、文字识别模型(ch_PP-OCRv3_rec) +SOPHGO-TPU部署模型前需要将以上Paddle模型转换成bmodel模型,我们以ch_PP-OCRv3_det模型为例,具体步骤如下: +- 下载Paddle模型[ch_PP-OCRv3_det](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar) +- Pddle模型转换为ONNX模型,请参考[Paddle2ONNX](https://github.com/PaddlePaddle/Paddle2ONNX) +- ONNX模型转换bmodel模型的过程,请参考[TPU-MLIR](https://github.com/sophgo/tpu-mlir) +下面我们提供一个example, 供用户参考,完成模型的转换. + +### 3.1 下载ch_PP-OCRv3_det模型,并转换为ONNX模型 +```shell +wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar +tar xvf ch_PP-OCRv3_det_infer.tar + +# 修改ch_PP-OCRv3_det模型的输入shape,由动态输入变成固定输入 +python paddle_infer_shape.py --model_dir ch_PP-OCRv3_det_infer \ + --model_filename inference.pdmodel \ + --params_filename inference.pdiparams \ + --save_dir ch_PP-OCRv3_det_infer_fix \ + --input_shape_dict="{'x':[1,3,960,608]}" + +# 请用户自行安装最新发布版本的paddle2onnx, 转换模型到ONNX格式的模型 +paddle2onnx --model_dir ch_PP-OCRv3_det_infer_fix \ + --model_filename inference.pdmodel \ + --params_filename inference.pdiparams \ + --save_file ch_PP-OCRv3_det_infer_fix.onnx \ + --enable_dev_version True +``` + +### 3.2 导出bmodel模型 + +以转换BM1684x的bmodel模型为例子,我们需要下载[TPU-MLIR](https://github.com/sophgo/tpu-mlir)工程,安装过程具体参见[TPU-MLIR文档](https://github.com/sophgo/tpu-mlir/blob/master/README.md)。 +#### 3.2.1 安装 +``` shell +docker pull sophgo/tpuc_dev:latest + +# myname1234是一个示例,也可以设置其他名字 +docker run --privileged --name myname1234 -v $PWD:/workspace -it sophgo/tpuc_dev:latest + +source ./envsetup.sh +./build.sh +``` + +#### 3.2.2 ONNX模型转换为bmodel模型 +``` shell +mkdir ch_PP-OCRv3_det && cd ch_PP-OCRv3_det + +#在该文件中放入测试图片,同时将上一步转换的ch_PP-OCRv3_det_infer_fix.onnx放入该文件夹中 +cp -rf ${REGRESSION_PATH}/dataset/COCO2017 . +cp -rf ${REGRESSION_PATH}/image . +#放入onnx模型文件ch_PP-OCRv3_det_infer_fix.onnx + +mkdir workspace && cd workspace + +#将ONNX模型转换为mlir模型,其中参数--output_names可以通过NETRON查看 +model_transform.py \ + --model_name ch_PP-OCRv3_det \ + --model_def ../ch_PP-OCRv3_det_infer_fix.onnx \ + --input_shapes [[1,3,960,608]] \ + --mean 0.0,0.0,0.0 \ + --scale 0.0039216,0.0039216,0.0039216 \ + --keep_aspect_ratio \ + --pixel_format rgb \ + --output_names sigmoid_0.tmp_0 \ + --test_input ../image/dog.jpg \ + --test_result ch_PP-OCRv3_det_top_outputs.npz \ + --mlir ch_PP-OCRv3_det.mlir + +#将mlir模型转换为BM1684x的F32 bmodel模型 +model_deploy.py \ + --mlir ch_PP-OCRv3_det.mlir \ + --quantize F32 \ + --chip bm1684x \ + --test_input ch_PP-OCRv3_det_in_f32.npz \ + --test_reference ch_PP-OCRv3_det_top_outputs.npz \ + --model ch_PP-OCRv3_det_1684x_f32.bmodel +``` +最终获得可以在BM1684x上能够运行的bmodel模型ch_PP-OCRv3_det_1684x_f32.bmodel。按照上面同样的方法,可以将ch_ppocr_mobile_v2.0_cls,ch_PP-OCRv3_rec转换为bmodel的格式。如果需要进一步对模型进行加速,可以将ONNX模型转换为INT8 bmodel,具体步骤参见[TPU-MLIR文档](https://github.com/sophgo/tpu-mlir/blob/master/README.md)。 + + +## 4. 详细部署的部署示例 +- [Python部署](python) +- [C++部署](cpp) diff --git a/ocr_webapp/PaddleOCR/deploy/fastdeploy/sophgo/python/README.md b/ocr_webapp/PaddleOCR/deploy/fastdeploy/sophgo/python/README.md new file mode 100644 index 0000000000000000000000000000000000000000..27dbe2694cd7ca71b9f16960937fde8564a5b4d4 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/fastdeploy/sophgo/python/README.md @@ -0,0 +1,52 @@ +[English](README.md) | 简体中文 +# PP-OCRv3 SOPHGO Python部署示例 +本目录下提供`infer.py`快速完成 PP-OCRv3 在SOPHGO TPU上部署的示例。 + +## 1. 部署环境准备 + +在部署前,需自行编译基于算能硬件的FastDeploy python wheel包并安装,参考文档[算能硬件部署环境](https://github.com/PaddlePaddle/FastDeploy/blob/develop/docs/cn/build_and_install#算能硬件部署环境) + + +## 2.运行部署示例 + +### 2.1 模型准备 +将Paddle模型转换为SOPHGO bmodel模型, 转换步骤参考[文档](../README.md) + +### 2.2 开始部署 +```bash +# 下载部署示例代码 +git clone https://github.com/PaddlePaddle/FastDeploy.git +cd FastDeploy/examples/vision/ocr/PP-OCR/sophgo/python + +# 如果您希望从PaddleOCR下载示例代码,请运行 +git clone https://github.com/PaddlePaddle/PaddleOCR.git +# 注意:如果当前分支找不到下面的fastdeploy测试代码,请切换到dygraph分支 +git checkout dygraph +cd PaddleOCR/deploy/fastdeploy/sophgo/python + +# 下载图片 +wget https://gitee.com/paddlepaddle/PaddleOCR/raw/release/2.6/doc/imgs/12.jpg + +#下载字典文件 +wget https://gitee.com/paddlepaddle/PaddleOCR/raw/release/2.6/ppocr/utils/ppocr_keys_v1.txt + +# 推理 +python3 infer.py --det_model ocr_bmodel/ch_PP-OCRv3_det_1684x_f32.bmodel \ + --cls_model ocr_bmodel/ch_ppocr_mobile_v2.0_cls_1684x_f32.bmodel \ + --rec_model ocr_bmodel/ch_PP-OCRv3_rec_1684x_f32.bmodel \ + --rec_label_file ../ppocr_keys_v1.txt \ + --image ../12.jpg + +# 运行完成后返回结果如下所示 +det boxes: [[42,413],[483,391],[484,428],[43,450]]rec text: 上海斯格威铂尔大酒店 rec score:0.952958 cls label: 0 cls score: 1.000000 +det boxes: [[187,456],[399,448],[400,480],[188,488]]rec text: 打浦路15号 rec score:0.897335 cls label: 0 cls score: 1.000000 +det boxes: [[23,507],[513,488],[515,529],[24,548]]rec text: 绿洲仕格维花园公寓 rec score:0.994589 cls label: 0 cls score: 1.000000 +det boxes: [[74,553],[427,542],[428,571],[75,582]]rec text: 打浦路252935号 rec score:0.900663 cls label: 0 cls score: 1.000000 + +可视化结果保存在sophgo_result.jpg中 +``` + +## 3. 其它文档 +- [PP-OCRv3 C++部署](../cpp) +- [转换 PP-OCRv3 SOPHGO模型文档](../README.md) +- 如果用户想要调整前后处理超参数、单独使用文字检测识别模型、使用其他模型等,更多详细文档与说明请参考[PP-OCR系列在CPU/GPU上的部署](../../cpu-gpu/cpp/README.md) diff --git a/ocr_webapp/PaddleOCR/deploy/fastdeploy/web/README.md b/ocr_webapp/PaddleOCR/deploy/fastdeploy/web/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5ca9628d30630d538479e6df90c744a1eea52261 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/fastdeploy/web/README.md @@ -0,0 +1,33 @@ +[English](README.md) | 简体中文 +# PP-OCRv3 前端部署示例 + +本节介绍部署PaddleOCR的PP-OCRv3模型在浏览器中运行,以及@paddle-js-models/ocr npm包中的js接口。 + + +## 1. 前端部署PP-OCRv3模型 +PP-OCRv3模型web demo使用[**参考文档**](https://github.com/PaddlePaddle/FastDeploy/tree/develop/examples/application/js/web_demo) + +## 2. PP-OCRv3 js接口 + +``` +import * as ocr from "@paddle-js-models/ocr"; +await ocr.init(detConfig, recConfig); +const res = await ocr.recognize(img, option, postConfig); +``` +ocr模型加载和初始化,其中模型为Paddle.js模型格式,js模型转换方式参考[文档](https://github.com/PaddlePaddle/FastDeploy/tree/develop/examples/application/js/web_demo/README.md) + +**init函数参数** + +> * **detConfig**(dict): 文本检测模型配置参数,默认值为 {modelPath: 'https://js-models.bj.bcebos.com/PaddleOCR/PP-OCRv3/ch_PP-OCRv3_det_infer_js_960/model.json', fill: '#fff', mean: [0.485, 0.456, 0.406],std: [0.229, 0.224, 0.225]}; 其中,modelPath为文本检测模型路径,fill 为图像预处理padding的值,mean和std分别为预处理的均值和标准差 +> * **recConfig**(dict)): 文本识别模型配置参数,默认值为 {modelPath: 'https://js-models.bj.bcebos.com/PaddleOCR/PP-OCRv3/ch_PP-OCRv3_rec_infer_js/model.json', fill: '#000', mean: [0.5, 0.5, 0.5], std: [0.5, 0.5, 0.5]}; 其中,modelPath为文本检测模型路径,fill 为图像预处理padding的值,mean和std分别为预处理的均值和标准差 + + +**recognize函数参数** + +> * **img**(HTMLImageElement): 输入图像参数,类型为HTMLImageElement。 +> * **option**(dict): 可视化文本检测框的canvas参数,可不用设置。 +> * **postConfig**(dict): 文本检测后处理参数,默认值为:{shape: 960, thresh: 0.3, box_thresh: 0.6, unclip_ratio:1.5}; thresh是输出预测图的二值化阈值;box_thresh是输出框的阈值,低于此值的预测框会被丢弃,unclip_ratio是输出框扩大的比例。 + + +## 其它文档 +- [PP-OCRv3 微信小程序部署文档](https://github.com/PaddlePaddle/FastDeploy/tree/develop/examples/application/js/mini_program) diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 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. \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/config.json new file mode 100644 index 0000000000000000000000000000000000000000..b93a5f7161f6d612c90cf4c7160dcf050a31beb3 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/config.json @@ -0,0 +1,16 @@ +{ + "modules_info": { + "kie_ser": { + "init_args": { + "version": "1.0.0", + "use_gpu": true + }, + "predict_args": { + } + } + }, + "port": 8871, + "use_multiprocess": false, + "workers": 2 +} + diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/module.py new file mode 100644 index 0000000000000000000000000000000000000000..f0ef3585d81bddca328d7c643687ef123ebccad4 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/module.py @@ -0,0 +1,145 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +sys.path.insert(0, ".") +import copy + +import time +import paddlehub +from paddlehub.common.logger import logger +from paddlehub.module.module import moduleinfo, runnable, serving +import cv2 +import numpy as np +import paddlehub as hub + +from tools.infer.utility import base64_to_cv2 +from ppstructure.kie.predict_kie_token_ser import SerPredictor +from ppstructure.utility import parse_args + +from deploy.hubserving.kie_ser.params import read_params + + +@moduleinfo( + name="kie_ser", + version="1.0.0", + summary="kie ser service", + author="paddle-dev", + author_email="paddle-dev@baidu.com", + type="cv/KIE_SER") +class KIESer(hub.Module): + def _initialize(self, use_gpu=False, enable_mkldnn=False): + """ + initialize with the necessary elements + """ + cfg = self.merge_configs() + + cfg.use_gpu = use_gpu + if use_gpu: + try: + _places = os.environ["CUDA_VISIBLE_DEVICES"] + int(_places[0]) + print("use gpu: ", use_gpu) + print("CUDA_VISIBLE_DEVICES: ", _places) + cfg.gpu_mem = 8000 + except: + raise RuntimeError( + "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id." + ) + cfg.ir_optim = True + cfg.enable_mkldnn = enable_mkldnn + + self.ser_predictor = SerPredictor(cfg) + + def merge_configs(self, ): + # deafult cfg + backup_argv = copy.deepcopy(sys.argv) + sys.argv = sys.argv[:1] + cfg = parse_args() + + update_cfg_map = vars(read_params()) + + for key in update_cfg_map: + cfg.__setattr__(key, update_cfg_map[key]) + + sys.argv = copy.deepcopy(backup_argv) + return cfg + + def read_images(self, paths=[]): + images = [] + for img_path in paths: + assert os.path.isfile( + img_path), "The {} isn't a valid file.".format(img_path) + img = cv2.imread(img_path) + if img is None: + logger.info("error in loading image:{}".format(img_path)) + continue + images.append(img) + return images + + def predict(self, images=[], paths=[]): + """ + Get the chinese texts in the predicted images. + Args: + images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths + paths (list[str]): The paths of images. If paths not images + Returns: + res (list): The result of chinese texts and save path of images. + """ + if images != [] and isinstance(images, list) and paths == []: + predicted_data = images + elif images == [] and isinstance(paths, list) and paths != []: + predicted_data = self.read_images(paths) + else: + raise TypeError("The input data is inconsistent with expectations.") + + assert predicted_data != [], "There is not any image to be predicted. Please check the input data." + + all_results = [] + for img in predicted_data: + if img is None: + logger.info("error in loading image") + all_results.append([]) + continue + starttime = time.time() + ser_res, _, elapse = self.ser_predictor(img) + elapse = time.time() - starttime + logger.info("Predict time: {}".format(elapse)) + all_results.append(ser_res) + return all_results + + @serving + def serving_method(self, images, **kwargs): + """ + Run as a service. + """ + images_decode = [base64_to_cv2(image) for image in images] + results = self.predict(images_decode, **kwargs) + return results + + +if __name__ == '__main__': + ocr = OCRSystem() + ocr._initialize() + image_path = [ + './doc/imgs/11.jpg', + './doc/imgs/12.jpg', + ] + res = ocr.predict(paths=image_path) + print(res) diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/params.py new file mode 100644 index 0000000000000000000000000000000000000000..bdd04c72e5d68ef6b2f992d9797d2f3bb63399ac --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/params.py @@ -0,0 +1,38 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from deploy.hubserving.ocr_system.params import read_params as pp_ocr_read_params + + +class Config(object): + pass + + +def read_params(): + cfg = pp_ocr_read_params() + + # SER params + cfg.kie_algorithm = "LayoutXLM" + cfg.use_visual_backbone = False + + cfg.ser_model_dir = "./inference/ser_vi_layoutxlm_xfund_infer" + cfg.ser_dict_path = "train_data/XFUND/class_list_xfun.txt" + cfg.vis_font_path = "./doc/fonts/simfang.ttf" + cfg.ocr_order_method = "tb-yx" + + return cfg diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 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. \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/config.json new file mode 100644 index 0000000000000000000000000000000000000000..4d796a860159541d73f9f799c9d3a249b48e479e --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/config.json @@ -0,0 +1,16 @@ +{ + "modules_info": { + "kie_ser_re": { + "init_args": { + "version": "1.0.0", + "use_gpu": true + }, + "predict_args": { + } + } + }, + "port": 8872, + "use_multiprocess": false, + "workers": 2 +} + diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/module.py new file mode 100644 index 0000000000000000000000000000000000000000..5a63a8a1f18822db84600764e00309e4c5c993e7 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/module.py @@ -0,0 +1,147 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +sys.path.insert(0, ".") +import copy + +import time +import paddlehub +from paddlehub.common.logger import logger +from paddlehub.module.module import moduleinfo, runnable, serving +import cv2 +import numpy as np +import paddlehub as hub + +from tools.infer.utility import base64_to_cv2 +from ppstructure.kie.predict_kie_token_ser_re import SerRePredictor +from ppstructure.utility import parse_args + +from deploy.hubserving.kie_ser_re.params import read_params + + +@moduleinfo( + name="kie_ser_re", + version="1.0.0", + summary="kie ser re service", + author="paddle-dev", + author_email="paddle-dev@baidu.com", + type="cv/KIE_SER_RE") +class KIESerRE(hub.Module): + def _initialize(self, use_gpu=False, enable_mkldnn=False): + """ + initialize with the necessary elements + """ + cfg = self.merge_configs() + + cfg.use_gpu = use_gpu + if use_gpu: + try: + _places = os.environ["CUDA_VISIBLE_DEVICES"] + int(_places[0]) + print("use gpu: ", use_gpu) + print("CUDA_VISIBLE_DEVICES: ", _places) + cfg.gpu_mem = 8000 + except: + raise RuntimeError( + "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id." + ) + cfg.ir_optim = True + cfg.enable_mkldnn = enable_mkldnn + + self.ser_re_predictor = SerRePredictor(cfg) + + def merge_configs(self, ): + # deafult cfg + backup_argv = copy.deepcopy(sys.argv) + sys.argv = sys.argv[:1] + cfg = parse_args() + + update_cfg_map = vars(read_params()) + + for key in update_cfg_map: + cfg.__setattr__(key, update_cfg_map[key]) + + sys.argv = copy.deepcopy(backup_argv) + return cfg + + def read_images(self, paths=[]): + images = [] + for img_path in paths: + assert os.path.isfile( + img_path), "The {} isn't a valid file.".format(img_path) + img = cv2.imread(img_path) + if img is None: + logger.info("error in loading image:{}".format(img_path)) + continue + images.append(img) + return images + + def predict(self, images=[], paths=[]): + """ + Get the chinese texts in the predicted images. + Args: + images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths + paths (list[str]): The paths of images. If paths not images + Returns: + res (list): The result of chinese texts and save path of images. + """ + if images != [] and isinstance(images, list) and paths == []: + predicted_data = images + elif images == [] and isinstance(paths, list) and paths != []: + predicted_data = self.read_images(paths) + else: + raise TypeError("The input data is inconsistent with expectations.") + + assert predicted_data != [], "There is not any image to be predicted. Please check the input data." + + all_results = [] + for img in predicted_data: + if img is None: + logger.info("error in loading image") + all_results.append([]) + continue + print(img.shape) + starttime = time.time() + re_res, _ = self.ser_re_predictor(img) + print(re_res) + elapse = time.time() - starttime + logger.info("Predict time: {}".format(elapse)) + all_results.append(re_res) + return all_results + + @serving + def serving_method(self, images, **kwargs): + """ + Run as a service. + """ + images_decode = [base64_to_cv2(image) for image in images] + results = self.predict(images_decode, **kwargs) + return results + + +if __name__ == '__main__': + ocr = OCRSystem() + ocr._initialize() + image_path = [ + './doc/imgs/11.jpg', + './doc/imgs/12.jpg', + ] + res = ocr.predict(paths=image_path) + print(res) diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/params.py new file mode 100644 index 0000000000000000000000000000000000000000..8c3214a5a78547d56cb0cc848bdbc35142193e84 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/params.py @@ -0,0 +1,40 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from deploy.hubserving.ocr_system.params import read_params as pp_ocr_read_params + + +class Config(object): + pass + + +def read_params(): + cfg = pp_ocr_read_params() + + # SER params + cfg.kie_algorithm = "LayoutXLM" + cfg.use_visual_backbone = False + + cfg.ser_model_dir = "./inference/ser_vi_layoutxlm_xfund_infer" + cfg.re_model_dir = "./inference/re_vi_layoutxlm_xfund_infer" + + cfg.ser_dict_path = "train_data/XFUND/class_list_xfun.txt" + cfg.vis_font_path = "./doc/fonts/simfang.ttf" + cfg.ocr_order_method = "tb-yx" + + return cfg diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 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. \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/config.json new file mode 100644 index 0000000000000000000000000000000000000000..2ced861b57efe3b9a426c129ff8a3f56e9d83a60 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/config.json @@ -0,0 +1,15 @@ +{ + "modules_info": { + "ocr_cls": { + "init_args": { + "version": "1.0.0", + "use_gpu": true + }, + "predict_args": { + } + } + }, + "port": 8866, + "use_multiprocess": false, + "workers": 2 +} diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/module.py new file mode 100644 index 0000000000000000000000000000000000000000..8b70f0376e5ebf5a960a73115c65a7a1d3d0011e --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/module.py @@ -0,0 +1,151 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +sys.path.insert(0, ".") +import copy +import paddlehub +from paddlehub.common.logger import logger +from paddlehub.module.module import moduleinfo, runnable, serving +import cv2 +import paddlehub as hub + +from tools.infer.utility import base64_to_cv2 +from tools.infer.predict_cls import TextClassifier +from tools.infer.utility import parse_args +from deploy.hubserving.ocr_cls.params import read_params + + +@moduleinfo( + name="ocr_cls", + version="1.0.0", + summary="ocr angle cls service", + author="paddle-dev", + author_email="paddle-dev@baidu.com", + type="cv/text_angle_cls") +class OCRCls(hub.Module): + def _initialize(self, use_gpu=False, enable_mkldnn=False): + """ + initialize with the necessary elements + """ + cfg = self.merge_configs() + + cfg.use_gpu = use_gpu + if use_gpu: + try: + _places = os.environ["CUDA_VISIBLE_DEVICES"] + int(_places[0]) + print("use gpu: ", use_gpu) + print("CUDA_VISIBLE_DEVICES: ", _places) + cfg.gpu_mem = 8000 + except: + raise RuntimeError( + "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id." + ) + cfg.ir_optim = True + cfg.enable_mkldnn = enable_mkldnn + + self.text_classifier = TextClassifier(cfg) + + def merge_configs(self, ): + # deafult cfg + backup_argv = copy.deepcopy(sys.argv) + sys.argv = sys.argv[:1] + cfg = parse_args() + + update_cfg_map = vars(read_params()) + + for key in update_cfg_map: + cfg.__setattr__(key, update_cfg_map[key]) + + sys.argv = copy.deepcopy(backup_argv) + return cfg + + def read_images(self, paths=[]): + images = [] + for img_path in paths: + assert os.path.isfile( + img_path), "The {} isn't a valid file.".format(img_path) + img = cv2.imread(img_path) + if img is None: + logger.info("error in loading image:{}".format(img_path)) + continue + images.append(img) + return images + + def predict(self, images=[], paths=[]): + """ + Get the text angle in the predicted images. + Args: + images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths + paths (list[str]): The paths of images. If paths not images + Returns: + res (list): The result of text detection box and save path of images. + """ + + if images != [] and isinstance(images, list) and paths == []: + predicted_data = images + elif images == [] and isinstance(paths, list) and paths != []: + predicted_data = self.read_images(paths) + else: + raise TypeError("The input data is inconsistent with expectations.") + + assert predicted_data != [], "There is not any image to be predicted. Please check the input data." + + img_list = [] + for img in predicted_data: + if img is None: + continue + img_list.append(img) + + rec_res_final = [] + try: + img_list, cls_res, predict_time = self.text_classifier(img_list) + for dno in range(len(cls_res)): + angle, score = cls_res[dno] + rec_res_final.append({ + 'angle': angle, + 'confidence': float(score), + }) + except Exception as e: + print(e) + return [[]] + + return [rec_res_final] + + @serving + def serving_method(self, images, **kwargs): + """ + Run as a service. + """ + images_decode = [base64_to_cv2(image) for image in images] + results = self.predict(images_decode, **kwargs) + return results + + +if __name__ == '__main__': + ocr = OCRCls() + ocr._initialize() + image_path = [ + './doc/imgs_words/ch/word_1.jpg', + './doc/imgs_words/ch/word_2.jpg', + './doc/imgs_words/ch/word_3.jpg', + ] + res = ocr.predict(paths=image_path) + print(res) diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/params.py new file mode 100644 index 0000000000000000000000000000000000000000..fe4e84843a434e2ca712cdb68d026520de8bf635 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/params.py @@ -0,0 +1,37 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + + +class Config(object): + pass + + +def read_params(): + cfg = Config() + + #params for text classifier + cfg.cls_model_dir = "./inference/ch_ppocr_mobile_v2.0_cls_infer/" + cfg.cls_image_shape = "3, 48, 192" + cfg.label_list = ['0', '180'] + cfg.cls_batch_num = 30 + cfg.cls_thresh = 0.9 + + cfg.use_pdserving = False + cfg.use_tensorrt = False + + return cfg diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 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. \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/config.json new file mode 100644 index 0000000000000000000000000000000000000000..6080d1c53ad065f47739a77e95bd15f792c63da0 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/config.json @@ -0,0 +1,15 @@ +{ + "modules_info": { + "ocr_det": { + "init_args": { + "version": "1.0.0", + "use_gpu": true + }, + "predict_args": { + } + } + }, + "port": 8865, + "use_multiprocess": false, + "workers": 2 +} diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/module.py new file mode 100644 index 0000000000000000000000000000000000000000..3dbaf161cdfd59528a36b5d5f645656cade2f1fc --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/module.py @@ -0,0 +1,148 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +sys.path.insert(0, ".") + +import copy +import paddlehub +from paddlehub.common.logger import logger +from paddlehub.module.module import moduleinfo, runnable, serving +import cv2 +import numpy as np +import paddlehub as hub + +from tools.infer.utility import base64_to_cv2 +from tools.infer.predict_det import TextDetector +from tools.infer.utility import parse_args +from deploy.hubserving.ocr_system.params import read_params + + +@moduleinfo( + name="ocr_det", + version="1.0.0", + summary="ocr detection service", + author="paddle-dev", + author_email="paddle-dev@baidu.com", + type="cv/text_detection") +class OCRDet(hub.Module): + def _initialize(self, use_gpu=False, enable_mkldnn=False): + """ + initialize with the necessary elements + """ + cfg = self.merge_configs() + + cfg.use_gpu = use_gpu + if use_gpu: + try: + _places = os.environ["CUDA_VISIBLE_DEVICES"] + int(_places[0]) + print("use gpu: ", use_gpu) + print("CUDA_VISIBLE_DEVICES: ", _places) + cfg.gpu_mem = 8000 + except: + raise RuntimeError( + "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id." + ) + cfg.ir_optim = True + cfg.enable_mkldnn = enable_mkldnn + + self.text_detector = TextDetector(cfg) + + def merge_configs(self, ): + # deafult cfg + backup_argv = copy.deepcopy(sys.argv) + sys.argv = sys.argv[:1] + cfg = parse_args() + + update_cfg_map = vars(read_params()) + + for key in update_cfg_map: + cfg.__setattr__(key, update_cfg_map[key]) + + sys.argv = copy.deepcopy(backup_argv) + return cfg + + def read_images(self, paths=[]): + images = [] + for img_path in paths: + assert os.path.isfile( + img_path), "The {} isn't a valid file.".format(img_path) + img = cv2.imread(img_path) + if img is None: + logger.info("error in loading image:{}".format(img_path)) + continue + images.append(img) + return images + + def predict(self, images=[], paths=[]): + """ + Get the text box in the predicted images. + Args: + images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths + paths (list[str]): The paths of images. If paths not images + Returns: + res (list): The result of text detection box and save path of images. + """ + + if images != [] and isinstance(images, list) and paths == []: + predicted_data = images + elif images == [] and isinstance(paths, list) and paths != []: + predicted_data = self.read_images(paths) + else: + raise TypeError("The input data is inconsistent with expectations.") + + assert predicted_data != [], "There is not any image to be predicted. Please check the input data." + + all_results = [] + for img in predicted_data: + if img is None: + logger.info("error in loading image") + all_results.append([]) + continue + dt_boxes, elapse = self.text_detector(img) + logger.info("Predict time : {}".format(elapse)) + + rec_res_final = [] + for dno in range(len(dt_boxes)): + rec_res_final.append({ + 'text_region': dt_boxes[dno].astype(np.int32).tolist() + }) + all_results.append(rec_res_final) + return all_results + + @serving + def serving_method(self, images, **kwargs): + """ + Run as a service. + """ + images_decode = [base64_to_cv2(image) for image in images] + results = self.predict(images_decode, **kwargs) + return results + + +if __name__ == '__main__': + ocr = OCRDet() + ocr._initialize() + image_path = [ + './doc/imgs/11.jpg', + './doc/imgs/12.jpg', + ] + res = ocr.predict(paths=image_path) + print(res) diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/params.py new file mode 100644 index 0000000000000000000000000000000000000000..c4aa13817f2dadb8225ec0112415094cce5165ad --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/params.py @@ -0,0 +1,48 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + + +class Config(object): + pass + + +def read_params(): + cfg = Config() + + #params for text detector + cfg.det_algorithm = "DB" + cfg.det_model_dir = "./inference/ch_PP-OCRv3_det_infer/" + cfg.det_limit_side_len = 960 + cfg.det_limit_type = 'max' + + #DB parmas + cfg.det_db_thresh = 0.3 + cfg.det_db_box_thresh = 0.6 + cfg.det_db_unclip_ratio = 1.5 + cfg.use_dilation = False + cfg.det_db_score_mode = "fast" + + # #EAST parmas + # cfg.det_east_score_thresh = 0.8 + # cfg.det_east_cover_thresh = 0.1 + # cfg.det_east_nms_thresh = 0.2 + + cfg.use_pdserving = False + cfg.use_tensorrt = False + + return cfg diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 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. \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/config.json new file mode 100644 index 0000000000000000000000000000000000000000..59d54e6c7545f300f134c9c2efaf546308b9c360 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/config.json @@ -0,0 +1,15 @@ +{ + "modules_info": { + "ocr_rec": { + "init_args": { + "version": "1.0.0", + "use_gpu": true + }, + "predict_args": { + } + } + }, + "port": 8867, + "use_multiprocess": false, + "workers": 2 +} diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/module.py new file mode 100644 index 0000000000000000000000000000000000000000..9fae54e2a317a7020543675d49fe0b7e07b4f7cd --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/module.py @@ -0,0 +1,151 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +sys.path.insert(0, ".") +import copy +import paddlehub +from paddlehub.common.logger import logger +from paddlehub.module.module import moduleinfo, runnable, serving +import cv2 +import paddlehub as hub + +from tools.infer.utility import base64_to_cv2 +from tools.infer.predict_rec import TextRecognizer +from tools.infer.utility import parse_args +from deploy.hubserving.ocr_rec.params import read_params + + +@moduleinfo( + name="ocr_rec", + version="1.0.0", + summary="ocr recognition service", + author="paddle-dev", + author_email="paddle-dev@baidu.com", + type="cv/text_recognition") +class OCRRec(hub.Module): + def _initialize(self, use_gpu=False, enable_mkldnn=False): + """ + initialize with the necessary elements + """ + cfg = self.merge_configs() + + cfg.use_gpu = use_gpu + if use_gpu: + try: + _places = os.environ["CUDA_VISIBLE_DEVICES"] + int(_places[0]) + print("use gpu: ", use_gpu) + print("CUDA_VISIBLE_DEVICES: ", _places) + cfg.gpu_mem = 8000 + except: + raise RuntimeError( + "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id." + ) + cfg.ir_optim = True + cfg.enable_mkldnn = enable_mkldnn + + self.text_recognizer = TextRecognizer(cfg) + + def merge_configs(self, ): + # deafult cfg + backup_argv = copy.deepcopy(sys.argv) + sys.argv = sys.argv[:1] + cfg = parse_args() + + update_cfg_map = vars(read_params()) + + for key in update_cfg_map: + cfg.__setattr__(key, update_cfg_map[key]) + + sys.argv = copy.deepcopy(backup_argv) + return cfg + + def read_images(self, paths=[]): + images = [] + for img_path in paths: + assert os.path.isfile( + img_path), "The {} isn't a valid file.".format(img_path) + img = cv2.imread(img_path) + if img is None: + logger.info("error in loading image:{}".format(img_path)) + continue + images.append(img) + return images + + def predict(self, images=[], paths=[]): + """ + Get the text box in the predicted images. + Args: + images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths + paths (list[str]): The paths of images. If paths not images + Returns: + res (list): The result of text detection box and save path of images. + """ + + if images != [] and isinstance(images, list) and paths == []: + predicted_data = images + elif images == [] and isinstance(paths, list) and paths != []: + predicted_data = self.read_images(paths) + else: + raise TypeError("The input data is inconsistent with expectations.") + + assert predicted_data != [], "There is not any image to be predicted. Please check the input data." + + img_list = [] + for img in predicted_data: + if img is None: + continue + img_list.append(img) + + rec_res_final = [] + try: + rec_res, predict_time = self.text_recognizer(img_list) + for dno in range(len(rec_res)): + text, score = rec_res[dno] + rec_res_final.append({ + 'text': text, + 'confidence': float(score), + }) + except Exception as e: + print(e) + return [[]] + + return [rec_res_final] + + @serving + def serving_method(self, images, **kwargs): + """ + Run as a service. + """ + images_decode = [base64_to_cv2(image) for image in images] + results = self.predict(images_decode, **kwargs) + return results + + +if __name__ == '__main__': + ocr = OCRRec() + ocr._initialize() + image_path = [ + './doc/imgs_words/ch/word_1.jpg', + './doc/imgs_words/ch/word_2.jpg', + './doc/imgs_words/ch/word_3.jpg', + ] + res = ocr.predict(paths=image_path) + print(res) diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/params.py new file mode 100644 index 0000000000000000000000000000000000000000..b8854c8c0718ddade7415e1d52b3deb0b2659868 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/params.py @@ -0,0 +1,41 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + + +class Config(object): + pass + + +def read_params(): + cfg = Config() + + #params for text recognizer + cfg.rec_algorithm = "CRNN" + cfg.rec_model_dir = "./inference/ch_PP-OCRv3_rec_infer/" + + cfg.rec_image_shape = "3, 48, 320" + cfg.rec_batch_num = 6 + cfg.max_text_length = 25 + + cfg.rec_char_dict_path = "./ppocr/utils/ppocr_keys_v1.txt" + cfg.use_space_char = True + + cfg.use_pdserving = False + cfg.use_tensorrt = False + + return cfg diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 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. \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/config.json new file mode 100644 index 0000000000000000000000000000000000000000..48e7e15425022e63a6622c6fe6394b8b249726cd --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/config.json @@ -0,0 +1,16 @@ +{ + "modules_info": { + "ocr_system": { + "init_args": { + "version": "1.0.0", + "use_gpu": true + }, + "predict_args": { + } + } + }, + "port": 8868, + "use_multiprocess": false, + "workers": 2 +} + diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/module.py new file mode 100644 index 0000000000000000000000000000000000000000..192fff9650901df7889ae7b0620beea1d8b03e81 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/module.py @@ -0,0 +1,156 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +sys.path.insert(0, ".") +import copy + +import time +import paddlehub +from paddlehub.common.logger import logger +from paddlehub.module.module import moduleinfo, runnable, serving +import cv2 +import numpy as np +import paddlehub as hub + +from tools.infer.utility import base64_to_cv2 +from tools.infer.predict_system import TextSystem +from tools.infer.utility import parse_args +from deploy.hubserving.ocr_system.params import read_params + + +@moduleinfo( + name="ocr_system", + version="1.0.0", + summary="ocr system service", + author="paddle-dev", + author_email="paddle-dev@baidu.com", + type="cv/PP-OCR_system") +class OCRSystem(hub.Module): + def _initialize(self, use_gpu=False, enable_mkldnn=False): + """ + initialize with the necessary elements + """ + cfg = self.merge_configs() + + cfg.use_gpu = use_gpu + if use_gpu: + try: + _places = os.environ["CUDA_VISIBLE_DEVICES"] + int(_places[0]) + print("use gpu: ", use_gpu) + print("CUDA_VISIBLE_DEVICES: ", _places) + cfg.gpu_mem = 8000 + except: + raise RuntimeError( + "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id." + ) + cfg.ir_optim = True + cfg.enable_mkldnn = enable_mkldnn + + self.text_sys = TextSystem(cfg) + + def merge_configs(self, ): + # deafult cfg + backup_argv = copy.deepcopy(sys.argv) + sys.argv = sys.argv[:1] + cfg = parse_args() + + update_cfg_map = vars(read_params()) + + for key in update_cfg_map: + cfg.__setattr__(key, update_cfg_map[key]) + + sys.argv = copy.deepcopy(backup_argv) + return cfg + + def read_images(self, paths=[]): + images = [] + for img_path in paths: + assert os.path.isfile( + img_path), "The {} isn't a valid file.".format(img_path) + img = cv2.imread(img_path) + if img is None: + logger.info("error in loading image:{}".format(img_path)) + continue + images.append(img) + return images + + def predict(self, images=[], paths=[]): + """ + Get the chinese texts in the predicted images. + Args: + images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths + paths (list[str]): The paths of images. If paths not images + Returns: + res (list): The result of chinese texts and save path of images. + """ + + if images != [] and isinstance(images, list) and paths == []: + predicted_data = images + elif images == [] and isinstance(paths, list) and paths != []: + predicted_data = self.read_images(paths) + else: + raise TypeError("The input data is inconsistent with expectations.") + + assert predicted_data != [], "There is not any image to be predicted. Please check the input data." + + all_results = [] + for img in predicted_data: + if img is None: + logger.info("error in loading image") + all_results.append([]) + continue + starttime = time.time() + dt_boxes, rec_res, _ = self.text_sys(img) + elapse = time.time() - starttime + logger.info("Predict time: {}".format(elapse)) + + dt_num = len(dt_boxes) + rec_res_final = [] + + for dno in range(dt_num): + text, score = rec_res[dno] + rec_res_final.append({ + 'text': text, + 'confidence': float(score), + 'text_region': dt_boxes[dno].astype(np.int32).tolist() + }) + all_results.append(rec_res_final) + return all_results + + @serving + def serving_method(self, images, **kwargs): + """ + Run as a service. + """ + images_decode = [base64_to_cv2(image) for image in images] + results = self.predict(images_decode, **kwargs) + return results + + +if __name__ == '__main__': + ocr = OCRSystem() + ocr._initialize() + image_path = [ + './doc/imgs/11.jpg', + './doc/imgs/12.jpg', + ] + res = ocr.predict(paths=image_path) + print(res) diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/params.py new file mode 100644 index 0000000000000000000000000000000000000000..4df1979a103345c3d2089cd0b1872bad49dcb944 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/params.py @@ -0,0 +1,68 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + + +class Config(object): + pass + + +def read_params(): + cfg = Config() + + #params for text detector + cfg.det_algorithm = "DB" + cfg.det_model_dir = "./inference/ch_PP-OCRv3_det_infer/" + cfg.det_limit_side_len = 960 + cfg.det_limit_type = 'max' + + #DB parmas + cfg.det_db_thresh = 0.3 + cfg.det_db_box_thresh = 0.5 + cfg.det_db_unclip_ratio = 1.6 + cfg.use_dilation = False + cfg.det_db_score_mode = "fast" + + #EAST parmas + cfg.det_east_score_thresh = 0.8 + cfg.det_east_cover_thresh = 0.1 + cfg.det_east_nms_thresh = 0.2 + + #params for text recognizer + cfg.rec_algorithm = "CRNN" + cfg.rec_model_dir = "./inference/ch_PP-OCRv3_rec_infer/" + + cfg.rec_image_shape = "3, 48, 320" + cfg.rec_batch_num = 6 + cfg.max_text_length = 25 + + cfg.rec_char_dict_path = "./ppocr/utils/ppocr_keys_v1.txt" + cfg.use_space_char = True + + #params for text classifier + cfg.use_angle_cls = True + cfg.cls_model_dir = "./inference/ch_ppocr_mobile_v2.0_cls_infer/" + cfg.cls_image_shape = "3, 48, 192" + cfg.label_list = ['0', '180'] + cfg.cls_batch_num = 30 + cfg.cls_thresh = 0.9 + + cfg.use_pdserving = False + cfg.use_tensorrt = False + cfg.drop_score = 0.5 + + return cfg diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/readme.md b/ocr_webapp/PaddleOCR/deploy/hubserving/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..9302bad97edee5aeb6bc26888da0a9d1ad285325 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/readme.md @@ -0,0 +1,250 @@ +[English](readme_en.md) | 简体中文 + +- [基于PaddleHub Serving的服务部署](#基于paddlehub-serving的服务部署) + - [1. 近期更新](#1-近期更新) + - [2. 快速启动服务](#2-快速启动服务) + - [2.1 安装PaddleHub](#21-安装PaddleHub) + - [2.2 下载推理模型](#22-下载推理模型) + - [2.3 安装服务模块](#23-安装服务模块) + - [2.4 启动服务](#24-启动服务) + - [2.4.1. 命令行命令启动(仅支持CPU)](#241-命令行命令启动仅支持cpu) + - [2.4.2 配置文件启动(支持CPU、GPU)](#242-配置文件启动支持cpugpu) + - [3. 发送预测请求](#3-发送预测请求) + - [4. 返回结果格式说明](#4-返回结果格式说明) + - [5. 自定义修改服务模块](#5-自定义修改服务模块) + + +PaddleOCR提供2种服务部署方式: +- 基于PaddleHub Serving的部署:代码路径为`./deploy/hubserving`,按照本教程使用; +- 基于PaddleServing的部署:代码路径为`./deploy/pdserving`,使用方法参考[文档](../../deploy/pdserving/README_CN.md)。 + +# 基于PaddleHub Serving的服务部署 + +hubserving服务部署目录下包括文本检测、文本方向分类,文本识别、文本检测+文本方向分类+文本识别3阶段串联,版面分析、表格识别和PP-Structure七种服务包,请根据需求选择相应的服务包进行安装和启动。目录结构如下: +``` +deploy/hubserving/ + └─ ocr_cls 文本方向分类模块服务包 + └─ ocr_det 文本检测模块服务包 + └─ ocr_rec 文本识别模块服务包 + └─ ocr_system 文本检测+文本方向分类+文本识别串联服务包 + └─ structure_layout 版面分析服务包 + └─ structure_table 表格识别服务包 + └─ structure_system PP-Structure服务包 + └─ kie_ser 关键信息抽取-SER服务包 + └─ kie_ser_re 关键信息抽取-SER+RE服务包 +``` + +每个服务包下包含3个文件。以2阶段串联服务包为例,目录如下: +``` +deploy/hubserving/ocr_system/ + └─ __init__.py 空文件,必选 + └─ config.json 配置文件,可选,使用配置启动服务时作为参数传入 + └─ module.py 主模块,必选,包含服务的完整逻辑 + └─ params.py 参数文件,必选,包含模型路径、前后处理参数等参数 +``` +## 1. 近期更新 + +* 2022.10.09 新增关键信息抽取服务。 +* 2022.08.23 新增版面分析服务。 +* 2022.05.05 新增PP-OCRv3检测和识别模型。 +* 2022.03.30 新增PP-Structure和表格识别两种服务。 + +## 2. 快速启动服务 +以下步骤以检测+识别2阶段串联服务为例,如果只需要检测服务或识别服务,替换相应文件路径即可。 +### 2.1 安装PaddleHub +paddlehub 需要 python>3.6.2 +```bash +pip3 install paddlehub==2.1.0 --upgrade -i https://mirror.baidu.com/pypi/simple +``` + +### 2.2 下载推理模型 +安装服务模块前,需要准备推理模型并放到正确路径。默认使用的是PP-OCRv3模型,默认模型路径为: +| 模型 | 路径 | +| ------- | - | +| 检测模型 | `./inference/ch_PP-OCRv3_det_infer/` | +| 识别模型 | `./inference/ch_PP-OCRv3_rec_infer/` | +| 方向分类器 | `./inference/ch_ppocr_mobile_v2.0_cls_infer/` | +| 版面分析模型 | `./inference/picodet_lcnet_x1_0_fgd_layout_infer/` | +| 表格结构识别模型 | `./inference/ch_ppstructure_mobile_v2.0_SLANet_infer/` | +| 关键信息抽取SER模型 | `./inference/ser_vi_layoutxlm_xfund_infer/` | +| 关键信息抽取RE模型 | `./inference/re_vi_layoutxlm_xfund_infer/` | + +**模型路径可在`params.py`中查看和修改。** + +更多模型可以从PaddleOCR提供的模型库[PP-OCR](../../doc/doc_ch/models_list.md)和[PP-Structure](../../ppstructure/docs/models_list.md)下载,也可以替换成自己训练转换好的模型。 + +### 2.3 安装服务模块 +PaddleOCR提供5种服务模块,根据需要安装所需模块。 + +在Linux环境(Windows环境请将`/`替换为`\`)下,安装模块命令如下表: +| 服务模块 | 命令 | +| ------- | - | +| 检测 | `hub install deploy/hubserving/ocr_det` | +| 分类 | `hub install deploy/hubserving/ocr_cls` | +| 识别 | `hub install deploy/hubserving/ocr_rec` | +| 检测+识别串联 | `hub install deploy/hubserving/ocr_system` | +| 表格识别 | `hub install deploy/hubserving/structure_table` | +| PP-Structure | `hub install deploy/hubserving/structure_system` | +| 版面分析 | `hub install deploy/hubserving/structure_layout` | +| 关键信息抽取SER | `hub install deploy/hubserving/kie_ser` | +| 关键信息抽取SER+RE | `hub install deploy/hubserving/kie_ser_re` | + +### 2.4 启动服务 +#### 2.4.1. 命令行命令启动(仅支持CPU) +**启动命令:** +```bash +hub serving start --modules Module1==Version1, Module2==Version2, ... \ + --port 8866 \ + --use_multiprocess \ + --workers \ +``` + +**参数:** +|参数|用途| +|---|---| +|`--modules`/`-m`|PaddleHub Serving预安装模型,以多个Module==Version键值对的形式列出
**当不指定Version时,默认选择最新版本**| +|`--port`/`-p`|服务端口,默认为8866| +|`--use_multiprocess`|是否启用并发方式,默认为单进程方式,推荐多核CPU机器使用此方式
**Windows操作系统只支持单进程方式**| +|`--workers`|在并发方式下指定的并发任务数,默认为`2*cpu_count-1`,其中`cpu_count`为CPU核数| + +如启动串联服务: +```bash +hub serving start -m ocr_system +``` + +这样就完成了一个服务化API的部署,使用默认端口号8866。 + +#### 2.4.2 配置文件启动(支持CPU、GPU) +**启动命令:** +```bash +hub serving start -c config.json +``` + +其中,`config.json`格式如下: +```json +{ + "modules_info": { + "ocr_system": { + "init_args": { + "version": "1.0.0", + "use_gpu": true + }, + "predict_args": { + } + } + }, + "port": 8868, + "use_multiprocess": false, + "workers": 2 +} +``` + +- `init_args`中的可配参数与`module.py`中的`_initialize`函数接口一致。 + + **当`use_gpu`为`true`时,表示使用GPU启动服务。** +- `predict_args`中的可配参数与`module.py`中的`predict`函数接口一致。 + +**注意:** +- 使用配置文件启动服务时,其他参数会被忽略。 +- 如果使用GPU预测(即,`use_gpu`置为`true`),则需要在启动服务之前,设置CUDA_VISIBLE_DEVICES环境变量,如: + ```bash + export CUDA_VISIBLE_DEVICES=0 + ``` +- **`use_gpu`不可与`use_multiprocess`同时为`true`**。 + +如,使用GPU 3号卡启动串联服务: +```bash +export CUDA_VISIBLE_DEVICES=3 +hub serving start -c deploy/hubserving/ocr_system/config.json +``` + +## 3. 发送预测请求 +配置好服务端,可使用以下命令发送预测请求,获取预测结果: +```bash +python tools/test_hubserving.py --server_url=server_url --image_dir=image_path +``` + +需要给脚本传递2个参数: +- `server_url`:服务地址,格式为`http://[ip_address]:[port]/predict/[module_name]` + + 例如,如果使用配置文件启动分类,检测、识别,检测+分类+识别3阶段,表格识别和PP-Structure服务 + + 并为每个服务修改了port,那么发送请求的url将分别是: + ``` + http://127.0.0.1:8865/predict/ocr_det + http://127.0.0.1:8866/predict/ocr_cls + http://127.0.0.1:8867/predict/ocr_rec + http://127.0.0.1:8868/predict/ocr_system + http://127.0.0.1:8869/predict/structure_table + http://127.0.0.1:8870/predict/structure_system + http://127.0.0.1:8870/predict/structure_layout + http://127.0.0.1:8871/predict/kie_ser + http://127.0.0.1:8872/predict/kie_ser_re + ``` +- `image_dir`:测试图像路径,可以是单张图片路径,也可以是图像集合目录路径 +- `visualize`:是否可视化结果,默认为False +- `output`:可视化结果保存路径,默认为`./hubserving_result` + +访问示例: +```bash +python tools/test_hubserving.py --server_url=http://127.0.0.1:8868/predict/ocr_system --image_dir=./doc/imgs/ --visualize=false +``` + +## 4. 返回结果格式说明 +返回结果为列表(list),列表中的每一项为词典(dict),词典一共可能包含3种字段,信息如下: +|字段名称|数据类型|意义| +|---|---|---| +|angle|str|文本角度| +|text|str|文本内容| +|confidence|float| 文本识别置信度或文本角度分类置信度| +|text_region|list|文本位置坐标| +|html|str|表格的html字符串| +|regions|list|版面分析+表格识别+OCR的结果,每一项为一个list
包含表示区域坐标的`bbox`,区域类型的`type`和区域结果的`res`三个字段| +|layout|list|版面分析的结果,每一项一个dict,包含版面区域坐标的`bbox`,区域类型的`label`| + +不同模块返回的字段不同,如,文本识别服务模块返回结果不含`text_region`字段,具体信息如下: +|字段名/模块名 |ocr_det |ocr_cls |ocr_rec |ocr_system |structure_table |structure_system |structure_layout |kie_ser |kie_re | +|--- |--- |--- |--- |--- |--- |--- |--- |--- |--- | +|angle | |✔ | |✔ | | | | +|text | | |✔ |✔ | |✔ | |✔ |✔ | +|confidence | |✔ |✔ |✔ | |✔ | |✔ |✔ | +|text_region |✔ | | |✔ | |✔ | |✔ |✔ | +|html | | | | |✔ |✔ | | | | +|regions | | | | |✔ |✔ | | | | +|layout | | | | | | |✔ | | | +|ser_res | | | | | | | |✔ | | +|re_res | | | | | | | | |✔ | + +**说明:** 如果需要增加、删除、修改返回字段,可在相应模块的`module.py`文件中进行修改,完整流程参考下一节自定义修改服务模块。 + +## 5. 自定义修改服务模块 +如果需要修改服务逻辑,一般需要操作以下步骤(以修改`deploy/hubserving/ocr_system`为例): + +1. 停止服务: + ```bash + hub serving stop --port/-p XXXX + ``` +2. 到`deploy/hubserving/ocr_system`下的`module.py`和`params.py`等文件中根据实际需求修改代码。 + + 例如,如果需要替换部署服务所用模型,则需要到`params.py`中修改模型路径参数`det_model_dir`和`rec_model_dir`,如果需要关闭文本方向分类器,则将参数`use_angle_cls`置为`False` + + 当然,同时可能还需要修改其他相关参数,请根据实际情况修改调试。 + + **强烈建议修改后先直接运行`module.py`调试,能正确运行预测后再启动服务测试。** + + **注意:** PPOCR-v3识别模型使用的图片输入shape为`3,48,320`,因此需要修改`params.py`中的`cfg.rec_image_shape = "3, 48, 320"`,如果不使用PPOCR-v3识别模型,则无需修改该参数。 +3. (可选)如果想要重命名模块需要更改`module.py`文件中的以下行: + - [`from deploy.hubserving.ocr_system.params import read_params`中的`ocr_system`](https://github.com/PaddlePaddle/PaddleOCR/blob/a923f35de57b5e378f8dd16e54d0a3e4f51267fd/deploy/hubserving/ocr_system/module.py#L35) + - [`name="ocr_system",`中的`ocr_system`](https://github.com/PaddlePaddle/PaddleOCR/blob/a923f35de57b5e378f8dd16e54d0a3e4f51267fd/deploy/hubserving/ocr_system/module.py#L39) +4. (可选)可能需要删除`__pycache__`目录以强制刷新CPython缓存: + ```bash + find deploy/hubserving/ocr_system -name '__pycache__' -exec rm -r {} \; + ``` +5. 安装修改后的新服务包: + ```bash + hub install deploy/hubserving/ocr_system + ``` +6. 重新启动服务: + ```bash + hub serving start -m ocr_system + ``` diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/readme_en.md b/ocr_webapp/PaddleOCR/deploy/hubserving/readme_en.md new file mode 100644 index 0000000000000000000000000000000000000000..034e2786ce5e45186a7c3257a8e63a5872de144a --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/readme_en.md @@ -0,0 +1,249 @@ +English | [简体中文](readme.md) + +- [Service deployment based on PaddleHub Serving](#service-deployment-based-on-paddlehub-serving) + - [1. Update](#1-update) + - [2. Quick start service](#2-quick-start-service) + - [2.1 Install PaddleHub](#21-install-paddlehub) + - [2.2 Download inference model](#22-download-inference-model) + - [2.3 Install Service Module](#23-install-service-module) + - [2.4 Start service](#24-start-service) + - [2.4.1 Start with command line parameters (CPU only)](#241-start-with-command-line-parameters-cpu-only) + - [2.4.2 Start with configuration file(CPU and GPU)](#242-start-with-configuration-filecpugpu) + - [3. Send prediction requests](#3-send-prediction-requests) + - [4. Returned result format](#4-returned-result-format) + - [5. User-defined service module modification](#5-user-defined-service-module-modification) + +PaddleOCR provides 2 service deployment methods: +- Based on **PaddleHub Serving**: Code path is `./deploy/hubserving`. Please follow this tutorial. +- Based on **PaddleServing**: Code path is `./deploy/pdserving`. Please refer to the [tutorial](../../deploy/pdserving/README.md) for usage. + +# Service deployment based on PaddleHub Serving + +The hubserving service deployment directory includes seven service packages: text detection, text angle class, text recognition, text detection+text angle class+text recognition three-stage series connection, layout analysis, table recognition, and PP-Structure. Please select the corresponding service package to install and start the service according to your needs. The directory is as follows: +``` +deploy/hubserving/ + └─ ocr_det text detection module service package + └─ ocr_cls text angle class module service package + └─ ocr_rec text recognition module service package + └─ ocr_system text detection+text angle class+text recognition three-stage series connection service package + └─ structure_layout layout analysis service package + └─ structure_table table recognition service package + └─ structure_system PP-Structure service package + └─ kie_ser KIE(SER) service package + └─ kie_ser_re KIE(SER+RE) service package +``` + +Each service pack contains 3 files. Take the 2-stage series connection service package as an example, the directory is as follows: +``` +deploy/hubserving/ocr_system/ + └─ __init__.py Empty file, required + └─ config.json Configuration file, optional, passed in as a parameter when using configuration to start the service + └─ module.py Main module file, required, contains the complete logic of the service + └─ params.py Parameter file, required, including parameters such as model path, pre and post-processing parameters +``` +## 1. Update + +* 2022.10.09 add KIE services. +* 2022.08.23 add layout analysis services. +* 2022.03.30 add PP-Structure and table recognition services. +* 2022.05.05 add PP-OCRv3 text detection and recognition services. + +## 2. Quick start service +The following steps take the 2-stage series service as an example. If only the detection service or recognition service is needed, replace the corresponding file path. + +### 2.1 Install PaddleHub +```bash +pip3 install paddlehub==2.1.0 --upgrade +``` + +### 2.2 Download inference model +Before installing the service module, you need to prepare the inference model and put it in the correct path. By default, the PP-OCRv3 models are used, and the default model path is: +| Model | Path | +| ------- | - | +| text detection model | ./inference/ch_PP-OCRv3_det_infer/ | +| text recognition model | ./inference/ch_PP-OCRv3_rec_infer/ | +| text angle classifier | ./inference/ch_ppocr_mobile_v2.0_cls_infer/ | +| layout parse model | ./inference/picodet_lcnet_x1_0_fgd_layout_infer/ | +| tanle recognition | ./inference/ch_ppstructure_mobile_v2.0_SLANet_infer/ | +| KIE(SER) | ./inference/ser_vi_layoutxlm_xfund_infer/ | +| KIE(SER+RE) | ./inference/re_vi_layoutxlm_xfund_infer/ | + +**The model path can be found and modified in `params.py`.** +More models provided by PaddleOCR can be obtained from the [model library](../../doc/doc_en/models_list_en.md). You can also use models trained by yourself. + +### 2.3 Install Service Module +PaddleOCR provides 5 kinds of service modules, install the required modules according to your needs. + +* On the Linux platform(replace `/` with `\` if using Windows), the examples are as the following table: +| Service model | Command | +| text detection | `hub install deploy/hubserving/ocr_det` | +| text angle class: | `hub install deploy/hubserving/ocr_cls` | +| text recognition: | `hub install deploy/hubserving/ocr_rec` | +| 2-stage series: | `hub install deploy/hubserving/ocr_system` | +| table recognition | `hub install deploy/hubserving/structure_table` | +| PP-Structure | `hub install deploy/hubserving/structure_system` | +| KIE(SER) | `hub install deploy/hubserving/kie_ser` | +| KIE(SER+RE) | `hub install deploy/hubserving/kie_ser_re` | + +### 2.4 Start service +#### 2.4.1 Start with command line parameters (CPU only) +**start command:** +```bash +hub serving start --modules Module1==Version1, Module2==Version2, ... \ + --port 8866 \ + --use_multiprocess \ + --workers \ +``` + +**Parameters:** +|parameters|usage| +|---|---| +|`--modules`/`-m`|PaddleHub Serving pre-installed model, listed in the form of multiple Module==Version key-value pairs
**When Version is not specified, the latest version is selected by default**| +|`--port`/`-p`|Service port, default is 8866| +|`--use_multiprocess`|Enable concurrent mode, by default using the single-process mode, this mode is recommended for multi-core CPU machines
**Windows operating system only supports single-process mode**| +|`--workers`|The number of concurrent tasks specified in concurrent mode, the default is `2*cpu_count-1`, where `cpu_count` is the number of CPU cores| + +For example, start the 2-stage series service: +```bash +hub serving start -m ocr_system +``` + +This completes the deployment of a service API, using the default port number 8866. + +#### 2.4.2 Start with configuration file(CPU and GPU) +**start command:** +```bash +hub serving start --config/-c config.json +``` + +In which the format of `config.json` is as follows: +```json +{ + "modules_info": { + "ocr_system": { + "init_args": { + "version": "1.0.0", + "use_gpu": true + }, + "predict_args": { + } + } + }, + "port": 8868, + "use_multiprocess": false, + "workers": 2 +} +``` +- The configurable parameters in `init_args` are consistent with the `_initialize` function interface in `module.py`. + + **When `use_gpu` is `true`, it means that the GPU is used to start the service**. +- The configurable parameters in `predict_args` are consistent with the `predict` function interface in `module.py`. + + **Note:** + - When using the configuration file to start the service, other parameters will be ignored. + - If you use GPU prediction (that is, `use_gpu` is set to `true`), you need to set the environment variable CUDA_VISIBLE_DEVICES before starting the service, such as: + ```bash + export CUDA_VISIBLE_DEVICES=0 + ``` + - **`use_gpu` and `use_multiprocess` cannot be `true` at the same time.** + +For example, use GPU card No. 3 to start the 2-stage series service: +```bash +export CUDA_VISIBLE_DEVICES=3 +hub serving start -c deploy/hubserving/ocr_system/config.json +``` + +## 3. Send prediction requests +After the service starts, you can use the following command to send a prediction request to obtain the prediction result: +```bash +python tools/test_hubserving.py --server_url=server_url --image_dir=image_path +``` + +Two parameters need to be passed to the script: +- **server_url**:service address, the format of which is +`http://[ip_address]:[port]/predict/[module_name]` + + For example, if using the configuration file to start the text angle classification, text detection, text recognition, detection+classification+recognition 3 stages, table recognition and PP-Structure service, + + also modified the port for each service, then the `server_url` to send the request will be: + + ``` + http://127.0.0.1:8865/predict/ocr_det + http://127.0.0.1:8866/predict/ocr_cls + http://127.0.0.1:8867/predict/ocr_rec + http://127.0.0.1:8868/predict/ocr_system + http://127.0.0.1:8869/predict/structure_table + http://127.0.0.1:8870/predict/structure_system + http://127.0.0.1:8870/predict/structure_layout + http://127.0.0.1:8871/predict/kie_ser + http://127.0.0.1:8872/predict/kie_ser_re + ``` +- **image_dir**:Test image path, which can be a single image path or an image directory path +- **visualize**:Whether to visualize the results, the default value is False +- **output**:The folder to save the Visualization result, the default value is `./hubserving_result` + +Example: +```bash +python tools/test_hubserving.py --server_url=http://127.0.0.1:8868/predict/ocr_system --image_dir=./doc/imgs/ --visualize=false` +``` + +## 4. Returned result format +The returned result is a list. Each item in the list is a dictionary which may contain three fields. The information is as follows: + +|field name|data type|description| +|----|----|----| +|angle|str|angle| +|text|str|text content| +|confidence|float|text recognition confidence| +|text_region|list|text location coordinates| +|html|str|table HTML string| +|regions|list|The result of layout analysis + table recognition + OCR, each item is a list
including `bbox` indicating area coordinates, `type` of area type and `res` of area results| +|layout|list|The result of layout analysis, each item is a dict, including `bbox` indicating area coordinates, `label` of area type| + +The fields returned by different modules are different. For example, the results returned by the text recognition service module do not contain `text_region`, detailed table is as follows: + +|field name/module name |ocr_det |ocr_cls |ocr_rec |ocr_system |structure_table |structure_system |structure_layout |kie_ser |kie_re | +|--- |--- |--- |--- |--- |--- |--- |--- |--- |--- | +|angle | |✔ | |✔ | | | | +|text | | |✔ |✔ | |✔ | |✔ |✔ | +|confidence | |✔ |✔ |✔ | |✔ | |✔ |✔ | +|text_region |✔ | | |✔ | |✔ | |✔ |✔ | +|html | | | | |✔ |✔ | | | | +|regions | | | | |✔ |✔ | | | | +|layout | | | | | | |✔ | | | +|ser_res | | | | | | | |✔ | | +|re_res | | | | | | | | |✔ | + +**Note:** If you need to add, delete or modify the returned fields, you can modify the file `module.py` of the corresponding module. For the complete process, refer to the user-defined modification service module in the next section. + +## 5. User-defined service module modification +If you need to modify the service logic, the following steps are generally required (take the modification of `deploy/hubserving/ocr_system` for example): + +1. Stop service: +```bash +hub serving stop --port/-p XXXX +``` +2. Modify the code in the corresponding files under `deploy/hubserving/ocr_system`, such as `module.py` and `params.py`, to your actual needs. + + For example, if you need to replace the model used by the deployed service, you need to modify model path parameters `det_model_dir` and `rec_model_dir` in `params.py`. If you want to turn off the text direction classifier, set the parameter `use_angle_cls` to `False`. + + Of course, other related parameters may need to be modified at the same time. Please modify and debug according to the actual situation. + + **It is suggested to run `module.py` directly for debugging after modification before starting the service test.** + + **Note** The image input shape used by the PPOCR-v3 recognition model is `3, 48, 320`, so you need to modify `cfg.rec_image_shape = "3, 48, 320"` in `params.py`, if you do not use the PPOCR-v3 recognition model, then there is no need to modify this parameter. +3. (Optional) If you want to rename the module, the following lines should be modified: + - [`ocr_system` within `from deploy.hubserving.ocr_system.params import read_params`](https://github.com/PaddlePaddle/PaddleOCR/blob/a923f35de57b5e378f8dd16e54d0a3e4f51267fd/deploy/hubserving/ocr_system/module.py#L35) + - [`ocr_system` within `name="ocr_system",`](https://github.com/PaddlePaddle/PaddleOCR/blob/a923f35de57b5e378f8dd16e54d0a3e4f51267fd/deploy/hubserving/ocr_system/module.py#L39) +4. (Optional) It may require you to delete the directory `__pycache__` to force flush build cache of CPython: + ```bash + find deploy/hubserving/ocr_system -name '__pycache__' -exec rm -r {} \; + ``` +5. Install modified service module: + ```bash + hub install deploy/hubserving/ocr_system/ + ``` +6. Restart service: + ```bash + hub serving start -m ocr_system + ``` diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 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. \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/config.json new file mode 100644 index 0000000000000000000000000000000000000000..bc52c1ab603d5659f90a5ed8a72cdb06638fb9e5 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/config.json @@ -0,0 +1,16 @@ +{ + "modules_info": { + "structure_layout": { + "init_args": { + "version": "1.0.0", + "use_gpu": true + }, + "predict_args": { + } + } + }, + "port": 8871, + "use_multiprocess": false, + "workers": 2 +} + diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/module.py new file mode 100644 index 0000000000000000000000000000000000000000..7091f123fc0039e4886d8763096952d7c445184c --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/module.py @@ -0,0 +1,143 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +sys.path.insert(0, ".") +import copy + +import time +import paddlehub +from paddlehub.common.logger import logger +from paddlehub.module.module import moduleinfo, runnable, serving +import cv2 +import paddlehub as hub + +from tools.infer.utility import base64_to_cv2 +from ppstructure.layout.predict_layout import LayoutPredictor as _LayoutPredictor +from ppstructure.utility import parse_args +from deploy.hubserving.structure_layout.params import read_params + + +@moduleinfo( + name="structure_layout", + version="1.0.0", + summary="PP-Structure layout service", + author="paddle-dev", + author_email="paddle-dev@baidu.com", + type="cv/structure_layout") +class LayoutPredictor(hub.Module): + def _initialize(self, use_gpu=False, enable_mkldnn=False): + """ + initialize with the necessary elements + """ + cfg = self.merge_configs() + cfg.use_gpu = use_gpu + if use_gpu: + try: + _places = os.environ["CUDA_VISIBLE_DEVICES"] + int(_places[0]) + print("use gpu: ", use_gpu) + print("CUDA_VISIBLE_DEVICES: ", _places) + cfg.gpu_mem = 8000 + except: + raise RuntimeError( + "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id." + ) + cfg.ir_optim = True + cfg.enable_mkldnn = enable_mkldnn + + self.layout_predictor = _LayoutPredictor(cfg) + + def merge_configs(self): + # deafult cfg + backup_argv = copy.deepcopy(sys.argv) + sys.argv = sys.argv[:1] + cfg = parse_args() + + update_cfg_map = vars(read_params()) + + for key in update_cfg_map: + cfg.__setattr__(key, update_cfg_map[key]) + + sys.argv = copy.deepcopy(backup_argv) + return cfg + + def read_images(self, paths=[]): + images = [] + for img_path in paths: + assert os.path.isfile( + img_path), "The {} isn't a valid file.".format(img_path) + img = cv2.imread(img_path) + if img is None: + logger.info("error in loading image:{}".format(img_path)) + continue + images.append(img) + return images + + def predict(self, images=[], paths=[]): + """ + Get the chinese texts in the predicted images. + Args: + images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths + paths (list[str]): The paths of images. If paths not images + Returns: + res (list): The layout results of images. + """ + + if images != [] and isinstance(images, list) and paths == []: + predicted_data = images + elif images == [] and isinstance(paths, list) and paths != []: + predicted_data = self.read_images(paths) + else: + raise TypeError("The input data is inconsistent with expectations.") + + assert predicted_data != [], "There is not any image to be predicted. Please check the input data." + + all_results = [] + for img in predicted_data: + if img is None: + logger.info("error in loading image") + all_results.append([]) + continue + starttime = time.time() + res, _ = self.layout_predictor(img) + elapse = time.time() - starttime + logger.info("Predict time: {}".format(elapse)) + + for item in res: + item['bbox'] = item['bbox'].tolist() + all_results.append({'layout': res}) + return all_results + + @serving + def serving_method(self, images, **kwargs): + """ + Run as a service. + """ + images_decode = [base64_to_cv2(image) for image in images] + results = self.predict(images_decode, **kwargs) + return results + + +if __name__ == '__main__': + layout = LayoutPredictor() + layout._initialize() + image_path = ['./ppstructure/docs/table/1.png'] + res = layout.predict(paths=image_path) + print(res) diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/params.py new file mode 100644 index 0000000000000000000000000000000000000000..448b66ac42dac555f084299f525ee9e91ad481d8 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/params.py @@ -0,0 +1,32 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + + +class Config(object): + pass + + +def read_params(): + cfg = Config() + + # params for layout analysis + cfg.layout_model_dir = './inference/picodet_lcnet_x1_0_fgd_layout_infer/' + cfg.layout_dict_path = './ppocr/utils/dict/layout_dict/layout_publaynet_dict.txt' + cfg.layout_score_threshold = 0.5 + cfg.layout_nms_threshold = 0.5 + return cfg diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 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. \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/config.json new file mode 100644 index 0000000000000000000000000000000000000000..642aa94a2a25759469f74280f6aab9a2495f493f --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/config.json @@ -0,0 +1,16 @@ +{ + "modules_info": { + "structure_system": { + "init_args": { + "version": "1.0.0", + "use_gpu": true + }, + "predict_args": { + } + } + }, + "port": 8870, + "use_multiprocess": false, + "workers": 2 +} + diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/module.py new file mode 100644 index 0000000000000000000000000000000000000000..61c93bb146ab11998bc7ed3350cb2686b73e3d3b --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/module.py @@ -0,0 +1,149 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +sys.path.insert(0, ".") +import copy + +import time +import paddlehub +from paddlehub.common.logger import logger +from paddlehub.module.module import moduleinfo, runnable, serving +import cv2 +import numpy as np +import paddlehub as hub + +from tools.infer.utility import base64_to_cv2 +from ppstructure.predict_system import StructureSystem as PPStructureSystem +from ppstructure.predict_system import save_structure_res +from ppstructure.utility import parse_args +from deploy.hubserving.structure_system.params import read_params + + +@moduleinfo( + name="structure_system", + version="1.0.0", + summary="PP-Structure system service", + author="paddle-dev", + author_email="paddle-dev@baidu.com", + type="cv/structure_system") +class StructureSystem(hub.Module): + def _initialize(self, use_gpu=False, enable_mkldnn=False): + """ + initialize with the necessary elements + """ + cfg = self.merge_configs() + + cfg.use_gpu = use_gpu + if use_gpu: + try: + _places = os.environ["CUDA_VISIBLE_DEVICES"] + int(_places[0]) + print("use gpu: ", use_gpu) + print("CUDA_VISIBLE_DEVICES: ", _places) + cfg.gpu_mem = 8000 + except: + raise RuntimeError( + "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id." + ) + cfg.ir_optim = True + cfg.enable_mkldnn = enable_mkldnn + + self.table_sys = PPStructureSystem(cfg) + + def merge_configs(self): + # deafult cfg + backup_argv = copy.deepcopy(sys.argv) + sys.argv = sys.argv[:1] + cfg = parse_args() + + update_cfg_map = vars(read_params()) + + for key in update_cfg_map: + cfg.__setattr__(key, update_cfg_map[key]) + + sys.argv = copy.deepcopy(backup_argv) + return cfg + + def read_images(self, paths=[]): + images = [] + for img_path in paths: + assert os.path.isfile( + img_path), "The {} isn't a valid file.".format(img_path) + img = cv2.imread(img_path) + if img is None: + logger.info("error in loading image:{}".format(img_path)) + continue + images.append(img) + return images + + def predict(self, images=[], paths=[]): + """ + Get the chinese texts in the predicted images. + Args: + images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths + paths (list[str]): The paths of images. If paths not images + Returns: + res (list): The result of chinese texts and save path of images. + """ + + if images != [] and isinstance(images, list) and paths == []: + predicted_data = images + elif images == [] and isinstance(paths, list) and paths != []: + predicted_data = self.read_images(paths) + else: + raise TypeError("The input data is inconsistent with expectations.") + + assert predicted_data != [], "There is not any image to be predicted. Please check the input data." + + all_results = [] + for img in predicted_data: + if img is None: + logger.info("error in loading image") + all_results.append([]) + continue + starttime = time.time() + res, _ = self.table_sys(img) + elapse = time.time() - starttime + logger.info("Predict time: {}".format(elapse)) + + # parse result + res_final = [] + for region in res: + region.pop('img') + res_final.append(region) + all_results.append({'regions': res_final}) + return all_results + + @serving + def serving_method(self, images, **kwargs): + """ + Run as a service. + """ + images_decode = [base64_to_cv2(image) for image in images] + results = self.predict(images_decode, **kwargs) + return results + + +if __name__ == '__main__': + structure_system = StructureSystem() + structure_system._initialize() + image_path = ['./ppstructure/docs/table/1.png'] + res = structure_system.predict(paths=image_path) + print(res) diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/params.py new file mode 100644 index 0000000000000000000000000000000000000000..fe691fbc2d172cc1ad32115abd5a4ee850d8ab2e --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/params.py @@ -0,0 +1,33 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from deploy.hubserving.structure_table.params import read_params as table_read_params + + +def read_params(): + cfg = table_read_params() + + # params for layout parser model + cfg.layout_model_dir = '' + cfg.layout_dict_path = './ppocr/utils/dict/layout_publaynet_dict.txt' + cfg.layout_score_threshold = 0.5 + cfg.layout_nms_threshold = 0.5 + + cfg.mode = 'structure' + cfg.output = './output' + return cfg diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 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. \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/config.json new file mode 100644 index 0000000000000000000000000000000000000000..d0e3cb1523daaec85cc0dc6099969fcfb380184b --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/config.json @@ -0,0 +1,16 @@ +{ + "modules_info": { + "structure_table": { + "init_args": { + "version": "1.0.0", + "use_gpu": true + }, + "predict_args": { + } + } + }, + "port": 8869, + "use_multiprocess": false, + "workers": 2 +} + diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/module.py new file mode 100644 index 0000000000000000000000000000000000000000..b4432b2d7b8764bc0327e7b12fe7887530e825c4 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/module.py @@ -0,0 +1,143 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +sys.path.insert(0, ".") +import copy + +import time +import paddlehub +from paddlehub.common.logger import logger +from paddlehub.module.module import moduleinfo, runnable, serving +import cv2 +import numpy as np +import paddlehub as hub + +from tools.infer.utility import base64_to_cv2 +from ppstructure.table.predict_table import TableSystem as _TableSystem +from ppstructure.predict_system import save_structure_res +from ppstructure.utility import parse_args +from deploy.hubserving.structure_table.params import read_params + + +@moduleinfo( + name="structure_table", + version="1.0.0", + summary="PP-Structure table service", + author="paddle-dev", + author_email="paddle-dev@baidu.com", + type="cv/structure_table") +class TableSystem(hub.Module): + def _initialize(self, use_gpu=False, enable_mkldnn=False): + """ + initialize with the necessary elements + """ + cfg = self.merge_configs() + cfg.use_gpu = use_gpu + if use_gpu: + try: + _places = os.environ["CUDA_VISIBLE_DEVICES"] + int(_places[0]) + print("use gpu: ", use_gpu) + print("CUDA_VISIBLE_DEVICES: ", _places) + cfg.gpu_mem = 8000 + except: + raise RuntimeError( + "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id." + ) + cfg.ir_optim = True + cfg.enable_mkldnn = enable_mkldnn + + self.table_sys = _TableSystem(cfg) + + def merge_configs(self): + # deafult cfg + backup_argv = copy.deepcopy(sys.argv) + sys.argv = sys.argv[:1] + cfg = parse_args() + + update_cfg_map = vars(read_params()) + + for key in update_cfg_map: + cfg.__setattr__(key, update_cfg_map[key]) + + sys.argv = copy.deepcopy(backup_argv) + return cfg + + def read_images(self, paths=[]): + images = [] + for img_path in paths: + assert os.path.isfile( + img_path), "The {} isn't a valid file.".format(img_path) + img = cv2.imread(img_path) + if img is None: + logger.info("error in loading image:{}".format(img_path)) + continue + images.append(img) + return images + + def predict(self, images=[], paths=[]): + """ + Get the chinese texts in the predicted images. + Args: + images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths + paths (list[str]): The paths of images. If paths not images + Returns: + res (list): The result of chinese texts and save path of images. + """ + + if images != [] and isinstance(images, list) and paths == []: + predicted_data = images + elif images == [] and isinstance(paths, list) and paths != []: + predicted_data = self.read_images(paths) + else: + raise TypeError("The input data is inconsistent with expectations.") + + assert predicted_data != [], "There is not any image to be predicted. Please check the input data." + + all_results = [] + for img in predicted_data: + if img is None: + logger.info("error in loading image") + all_results.append([]) + continue + starttime = time.time() + res, _ = self.table_sys(img) + elapse = time.time() - starttime + logger.info("Predict time: {}".format(elapse)) + + all_results.append({'html': res['html']}) + return all_results + + @serving + def serving_method(self, images, **kwargs): + """ + Run as a service. + """ + images_decode = [base64_to_cv2(image) for image in images] + results = self.predict(images_decode, **kwargs) + return results + + +if __name__ == '__main__': + table_system = TableSystem() + table_system._initialize() + image_path = ['./ppstructure/docs/table/table.jpg'] + res = table_system.predict(paths=image_path) + print(res) diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/params.py new file mode 100644 index 0000000000000000000000000000000000000000..9632c2f70b794854d191e9f088f3f2e301a5dbb3 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/params.py @@ -0,0 +1,30 @@ +# Copyright (c) 2022 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from deploy.hubserving.ocr_system.params import read_params as pp_ocr_read_params + + +def read_params(): + cfg = pp_ocr_read_params() + + # params for table structure model + cfg.table_max_len = 488 + cfg.table_model_dir = './inference/en_ppocr_mobile_v2.0_table_structure_infer/' + cfg.table_char_dict_path = './ppocr/utils/dict/table_structure_dict.txt' + cfg.show_log = False + return cfg diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/.gitignore b/ocr_webapp/PaddleOCR/deploy/ios_demo/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..66434d5ef4dce7cab1a3ed721df4e8e5bd529e8e --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/.gitignore @@ -0,0 +1,21 @@ +# CocoaPods +Pods/ + +# Xcode +PaddleOCRDemo.xcworkspace/xcuserdata/ +PaddleOCRDemo.xcodeproj/xcuserdata/ +PaddleOCRDemo.xcodeproj/project.xcworkspace/xcuserdata/ +*.xcuserstate +DerivedData/ +build/ + +# fetch_ios_demo_models.sh download cache +.fetch_ios_demo_models_work/ + +# Models — ignore all files under Models/; track only `.gitkeep` so det/, rec/, … stay in the tree +PaddleOCRDemo/Models/** +!PaddleOCRDemo/Models/*/ +!PaddleOCRDemo/Models/*/.gitkeep + +# Local validation reports +out/ diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/NOTICE b/ocr_webapp/PaddleOCR/deploy/ios_demo/NOTICE new file mode 100644 index 0000000000000000000000000000000000000000..6545fc413c0f1ce4522205cd798698f88486d948 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/NOTICE @@ -0,0 +1,25 @@ +PaddleOCR iOS demo — third-party notices +=============================================================================== + +This demo application is part of the PaddleOCR project (Apache License 2.0). +It may bundle or link third-party components in addition to the PaddleOCR +repository LICENSE. The following components are included in **source** form +in this tree: + +Clipper / polyclipping (polygon clipping and offsetting) +-------------------------------------------------------------------------------------------- + • Upstream: polyclipping clipper 6.4.2 (Angus Johnson) + • Bundled: `PaddleOCRDemo/ThirdParty/Clipper1/` (clipper.cpp, clipper.hpp) + • SPDX: BSL-1.0 (Boost Software License 1.0) + • Full text: `PaddleOCRDemo/ThirdParty/Clipper1/LICENSE` + +CocoaPods dependencies (onnxruntime-objc, OpenCV, Yams, etc.) +------------------------------------------------------------- + • Versions and upstream packages are recorded in `Podfile.lock` after + `pod install`. Each pod is subject to its own license as published by + the respective project; consult the Pods project and upstream repos + for notices required by those licenses. + +If you distribute binaries built from this demo, ensure your product’s +open-source / third-party attribution (e.g. in-app “Licenses” or +distribution packaging) satisfies your legal and policy requirements. diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcodeproj/project.pbxproj b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000000000000000000000000000000000000..a30617317d915c46dc5fa63ee598937457ce248f --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcodeproj/project.pbxproj @@ -0,0 +1,822 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 6F50EE661821421A98371746 /* Pods_PaddleOCRDemoTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A24555277519FE15EF10A5D /* Pods_PaddleOCRDemoTests.framework */; }; + A1000001 /* PaddleOCRDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000001 /* PaddleOCRDemoApp.swift */; }; + A1000002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000002 /* ContentView.swift */; }; + A1000003 /* Models in Resources */ = {isa = PBXBuildFile; fileRef = A2000006 /* Models */; }; + A1000004 /* ModelConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000011 /* ModelConfig.swift */; }; + A1000005 /* ORTSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000012 /* ORTSessionManager.swift */; }; + A10000F0 /* ORTProfilingBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = A20000F1 /* ORTProfilingBridge.mm */; }; + A1000007 /* InferenceConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000015 /* InferenceConfig.swift */; }; + A1000008 /* Preprocessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000016 /* Preprocessing.swift */; }; + A1000011 /* ORTSessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000014 /* ORTSessionManagerTests.swift */; }; + A1000021 /* DBPostProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000021 /* DBPostProcess.swift */; }; + A1000022 /* OpenCVDBBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = A2000023 /* OpenCVDBBridge.mm */; }; + A1000023 /* OpenCVImageBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = A2000025 /* OpenCVImageBridge.mm */; }; + A10000E1 /* clipper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A20000E1 /* clipper.cpp */; }; + A10000E2 /* PDBPolygonOffsetBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = A20000E2 /* PDBPolygonOffsetBridge.mm */; }; + A10000C0 /* DemoSectionChrome.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000C0 /* DemoSectionChrome.swift */; }; + A1000030 /* DetectionEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000030 /* DetectionEngine.swift */; }; + A1000040 /* RecPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000040 /* RecPreprocessor.swift */; }; + A1000050 /* CTCDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000050 /* CTCDecoder.swift */; }; + A1000060 /* RecognitionEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000060 /* RecognitionEngine.swift */; }; + A1000070 /* OCREngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000070 /* OCREngine.swift */; }; + A1000071 /* BoxSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000071 /* BoxSorter.swift */; }; + A1000072 /* QuadTextCrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000072 /* QuadTextCrop.swift */; }; + A1000074 /* EncodedImageCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000074 /* EncodedImageCodec.swift */; }; + A1000080 /* OCRViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000080 /* OCRViewModel.swift */; }; + A1000090 /* ImagePickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000090 /* ImagePickerSection.swift */; }; + A1000091 /* SampleImages in Resources */ = {isa = PBXBuildFile; fileRef = A2000091 /* SampleImages */; }; + A10000A0 /* ResultImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A0 /* ResultImageView.swift */; }; + A10000A1 /* TimingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A1 /* TimingView.swift */; }; + A10000A2 /* ResultsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A2 /* ResultsListView.swift */; }; + A10000A3 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A3 /* ErrorView.swift */; }; + A10000A4 /* OCRParametersPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A4 /* OCRParametersPanel.swift */; }; + A10000A5 /* OnnxRuntimeSettingsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A5 /* OnnxRuntimeSettingsPanel.swift */; }; + A10000A6 /* EngineSettingsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A6 /* EngineSettingsSheet.swift */; }; + A10000B3 /* OCRParameterResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000B3 /* OCRParameterResolverTests.swift */; }; + A10000C5 /* OCRBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000C5 /* OCRBenchmarkTests.swift */; }; + A10000C6 /* BenchmarkPayloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000C6 /* BenchmarkPayloads.swift */; }; + A10000D3 /* BenchmarkFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000D3 /* BenchmarkFixtures.swift */; }; + A10000C7 /* Fixtures in Resources */ = {isa = PBXBuildFile; fileRef = A20000C7 /* Fixtures */; }; + A10000B1 /* OCRParameterResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000B1 /* OCRParameterResolver.swift */; }; + A10000D0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A20000D0 /* Assets.xcassets */; }; + A10000D1 /* Bundle+BundledImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000D1 /* Bundle+BundledImage.swift */; }; + A10000D2 /* DemoAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000D2 /* DemoAssets.swift */; }; + F9888FA77A2AF8AF45B50193 /* Pods_PaddleOCRDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D0FFA11A4E43E5BD0F80219 /* Pods_PaddleOCRDemo.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + A5000001 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A3000001 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A4000001; + remoteInfo = PaddleOCRDemo; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0A24555277519FE15EF10A5D /* Pods_PaddleOCRDemoTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PaddleOCRDemoTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3D0FFA11A4E43E5BD0F80219 /* Pods_PaddleOCRDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PaddleOCRDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 49C7CEE60686CC98BE2EEE45 /* Pods-PaddleOCRDemoTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PaddleOCRDemoTests.release.xcconfig"; path = "Target Support Files/Pods-PaddleOCRDemoTests/Pods-PaddleOCRDemoTests.release.xcconfig"; sourceTree = ""; }; + 520BC3C496ED955BBB302DC4 /* Pods-PaddleOCRDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PaddleOCRDemo.debug.xcconfig"; path = "Target Support Files/Pods-PaddleOCRDemo/Pods-PaddleOCRDemo.debug.xcconfig"; sourceTree = ""; }; + 54F64B7C830508805D832F8A /* Pods-PaddleOCRDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PaddleOCRDemo.release.xcconfig"; path = "Target Support Files/Pods-PaddleOCRDemo/Pods-PaddleOCRDemo.release.xcconfig"; sourceTree = ""; }; + 72FF5C269BF861629F89D6B5 /* Pods-PaddleOCRDemoTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PaddleOCRDemoTests.debug.xcconfig"; path = "Target Support Files/Pods-PaddleOCRDemoTests/Pods-PaddleOCRDemoTests.debug.xcconfig"; sourceTree = ""; }; + A2000001 /* PaddleOCRDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddleOCRDemoApp.swift; sourceTree = ""; }; + A2000002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + A2000003 /* PaddleOCRDemo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PaddleOCRDemo-Bridging-Header.h"; sourceTree = ""; }; + A2000004 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A2000005 /* PaddleOCRDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PaddleOCRDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A2000006 /* Models */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Models; sourceTree = ""; }; + A2000009 /* PaddleOCRDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PaddleOCRDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + A2000011 /* ModelConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelConfig.swift; sourceTree = ""; }; + A2000012 /* ORTSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ORTSessionManager.swift; sourceTree = ""; }; + A20000F0 /* ORTProfilingBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ORTProfilingBridge.h; sourceTree = ""; }; + A20000F1 /* ORTProfilingBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ORTProfilingBridge.mm; sourceTree = ""; }; + A2000014 /* ORTSessionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ORTSessionManagerTests.swift; sourceTree = ""; }; + A2000015 /* InferenceConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InferenceConfig.swift; sourceTree = ""; }; + A2000016 /* Preprocessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preprocessing.swift; sourceTree = ""; }; + A2000021 /* DBPostProcess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPostProcess.swift; sourceTree = ""; }; + A2000022 /* OpenCVDBBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OpenCVDBBridge.h; sourceTree = ""; }; + A2000023 /* OpenCVDBBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OpenCVDBBridge.mm; sourceTree = ""; }; + A2000024 /* OpenCVImageBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OpenCVImageBridge.h; sourceTree = ""; }; + A2000025 /* OpenCVImageBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OpenCVImageBridge.mm; sourceTree = ""; }; + A20000E1 /* clipper.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = clipper.cpp; sourceTree = ""; }; + A20000E2 /* PDBPolygonOffsetBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PDBPolygonOffsetBridge.mm; sourceTree = ""; }; + A20000E3 /* PDBPolygonOffsetBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PDBPolygonOffsetBridge.h; sourceTree = ""; }; + A2000030 /* DetectionEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionEngine.swift; sourceTree = ""; }; + A2000040 /* RecPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecPreprocessor.swift; sourceTree = ""; }; + A2000050 /* CTCDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CTCDecoder.swift; sourceTree = ""; }; + A2000060 /* RecognitionEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecognitionEngine.swift; sourceTree = ""; }; + A2000070 /* OCREngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCREngine.swift; sourceTree = ""; }; + A2000071 /* BoxSorter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxSorter.swift; sourceTree = ""; }; + A2000072 /* QuadTextCrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadTextCrop.swift; sourceTree = ""; }; + A2000074 /* EncodedImageCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodedImageCodec.swift; sourceTree = ""; }; + A2000080 /* OCRViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCRViewModel.swift; sourceTree = ""; }; + A2000090 /* ImagePickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerSection.swift; sourceTree = ""; }; + A2000091 /* SampleImages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = SampleImages; sourceTree = ""; }; + A20000A0 /* ResultImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultImageView.swift; sourceTree = ""; }; + A20000A1 /* TimingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimingView.swift; sourceTree = ""; }; + A20000A2 /* ResultsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultsListView.swift; sourceTree = ""; }; + A20000A3 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + A20000A4 /* OCRParametersPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCRParametersPanel.swift; sourceTree = ""; }; + A20000A5 /* OnnxRuntimeSettingsPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnnxRuntimeSettingsPanel.swift; sourceTree = ""; }; + A20000A6 /* EngineSettingsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EngineSettingsSheet.swift; sourceTree = ""; }; + A20000B3 /* OCRParameterResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCRParameterResolverTests.swift; sourceTree = ""; }; + A20000C5 /* OCRBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCRBenchmarkTests.swift; sourceTree = ""; }; + A20000C6 /* BenchmarkPayloads.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkPayloads.swift; sourceTree = ""; }; + A20000D3 /* BenchmarkFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkFixtures.swift; sourceTree = ""; }; + A20000C7 /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = ""; }; + A20000B1 /* OCRParameterResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCRParameterResolver.swift; sourceTree = ""; }; + A20000C0 /* DemoSectionChrome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoSectionChrome.swift; sourceTree = ""; }; + A20000D0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A20000D1 /* Bundle+BundledImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+BundledImage.swift"; sourceTree = ""; }; + A20000D2 /* DemoAssets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoAssets.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A6000001 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F9888FA77A2AF8AF45B50193 /* Pods_PaddleOCRDemo.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A6000002 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6F50EE661821421A98371746 /* Pods_PaddleOCRDemoTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2D2CDE3432BBDFC183DD82E7 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3D0FFA11A4E43E5BD0F80219 /* Pods_PaddleOCRDemo.framework */, + 0A24555277519FE15EF10A5D /* Pods_PaddleOCRDemoTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 3E7A5973A61F2EB2D5542DE3 /* Pods */ = { + isa = PBXGroup; + children = ( + 520BC3C496ED955BBB302DC4 /* Pods-PaddleOCRDemo.debug.xcconfig */, + 54F64B7C830508805D832F8A /* Pods-PaddleOCRDemo.release.xcconfig */, + 72FF5C269BF861629F89D6B5 /* Pods-PaddleOCRDemoTests.debug.xcconfig */, + 49C7CEE60686CC98BE2EEE45 /* Pods-PaddleOCRDemoTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + A7000001 = { + isa = PBXGroup; + children = ( + A7000002 /* PaddleOCRDemo */, + A7000005 /* PaddleOCRDemoTests */, + A7000003 /* Products */, + 3E7A5973A61F2EB2D5542DE3 /* Pods */, + 2D2CDE3432BBDFC183DD82E7 /* Frameworks */, + ); + sourceTree = ""; + }; + A7000002 /* PaddleOCRDemo */ = { + isa = PBXGroup; + children = ( + A7000004 /* App */, + A7000007 /* Engine */, + A7000008 /* ViewModels */, + A7000009 /* Views */, + A7000006 /* Supporting */, + A7000010 /* Resources */, + A700000D /* ThirdParty */, + A2000006 /* Models */, + ); + path = PaddleOCRDemo; + sourceTree = ""; + }; + A7000003 /* Products */ = { + isa = PBXGroup; + children = ( + A2000005 /* PaddleOCRDemo.app */, + A2000009 /* PaddleOCRDemoTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + A7000004 /* App */ = { + isa = PBXGroup; + children = ( + A20000D1 /* Bundle+BundledImage.swift */, + A20000D2 /* DemoAssets.swift */, + A2000001 /* PaddleOCRDemoApp.swift */, + A2000002 /* ContentView.swift */, + A20000C0 /* DemoSectionChrome.swift */, + ); + path = App; + sourceTree = ""; + }; + A7000005 /* PaddleOCRDemoTests */ = { + isa = PBXGroup; + children = ( + A2000014 /* ORTSessionManagerTests.swift */, + A20000B3 /* OCRParameterResolverTests.swift */, + A20000C5 /* OCRBenchmarkTests.swift */, + A20000C6 /* BenchmarkPayloads.swift */, + A20000D3 /* BenchmarkFixtures.swift */, + A20000C7 /* Fixtures */, + ); + path = PaddleOCRDemoTests; + sourceTree = ""; + }; + A7000006 /* Supporting */ = { + isa = PBXGroup; + children = ( + A2000003 /* PaddleOCRDemo-Bridging-Header.h */, + A2000004 /* Info.plist */, + ); + path = Supporting; + sourceTree = ""; + }; + A7000007 /* Engine */ = { + isa = PBXGroup; + children = ( + A2000071 /* BoxSorter.swift */, + A2000050 /* CTCDecoder.swift */, + A2000021 /* DBPostProcess.swift */, + A2000022 /* OpenCVDBBridge.h */, + A2000023 /* OpenCVDBBridge.mm */, + A2000024 /* OpenCVImageBridge.h */, + A2000025 /* OpenCVImageBridge.mm */, + A20000E2 /* PDBPolygonOffsetBridge.mm */, + A20000E3 /* PDBPolygonOffsetBridge.h */, + A2000030 /* DetectionEngine.swift */, + A2000015 /* InferenceConfig.swift */, + A20000B1 /* OCRParameterResolver.swift */, + A2000011 /* ModelConfig.swift */, + A2000070 /* OCREngine.swift */, + A2000012 /* ORTSessionManager.swift */, + A20000F0 /* ORTProfilingBridge.h */, + A20000F1 /* ORTProfilingBridge.mm */, + A2000072 /* QuadTextCrop.swift */, + A2000074 /* EncodedImageCodec.swift */, + A2000016 /* Preprocessing.swift */, + A2000040 /* RecPreprocessor.swift */, + A2000060 /* RecognitionEngine.swift */, + ); + path = Engine; + sourceTree = ""; + }; + A7000008 /* ViewModels */ = { + isa = PBXGroup; + children = ( + A2000080 /* OCRViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + A7000009 /* Views */ = { + isa = PBXGroup; + children = ( + A20000A3 /* ErrorView.swift */, + A2000090 /* ImagePickerSection.swift */, + A20000A5 /* OnnxRuntimeSettingsPanel.swift */, + A20000A4 /* OCRParametersPanel.swift */, + A20000A6 /* EngineSettingsSheet.swift */, + A20000A0 /* ResultImageView.swift */, + A20000A2 /* ResultsListView.swift */, + A20000A1 /* TimingView.swift */, + ); + path = Views; + sourceTree = ""; + }; + A700000D /* ThirdParty */ = { + isa = PBXGroup; + children = ( + A700000E /* Clipper1 */, + ); + path = ThirdParty; + sourceTree = ""; + }; + A700000E /* Clipper1 */ = { + isa = PBXGroup; + children = ( + A20000E1 /* clipper.cpp */, + ); + path = Clipper1; + sourceTree = ""; + }; + A7000010 /* Resources */ = { + isa = PBXGroup; + children = ( + A20000D0 /* Assets.xcassets */, + A2000091 /* SampleImages */, + ); + path = Resources; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A4000001 /* PaddleOCRDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = A9000003 /* Build configuration list for PBXNativeTarget "PaddleOCRDemo" */; + buildPhases = ( + A9D5C3BAF4FDCBE19D45BCA7 /* [CP] Check Pods Manifest.lock */, + A8000001 /* Sources */, + A6000001 /* Frameworks */, + A8000003 /* Resources */, + 6530B8008C9EFD4F162C3E86 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PaddleOCRDemo; + productName = PaddleOCRDemo; + productReference = A2000005 /* PaddleOCRDemo.app */; + productType = "com.apple.product-type.application"; + }; + A4000002 /* PaddleOCRDemoTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = A9000006 /* Build configuration list for PBXNativeTarget "PaddleOCRDemoTests" */; + buildPhases = ( + 9FD4B8CB3C3E1C27991BDE0B /* [CP] Check Pods Manifest.lock */, + A8000002 /* Sources */, + A6000002 /* Frameworks */, + A8000004 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + A5000002 /* PBXTargetDependency */, + ); + name = PaddleOCRDemoTests; + productName = PaddleOCRDemoTests; + productReference = A2000009 /* PaddleOCRDemoTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A3000001 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1540; + LastUpgradeCheck = 2640; + TargetAttributes = { + A4000001 = { + CreatedOnToolsVersion = 15.4; + }; + A4000002 = { + CreatedOnToolsVersion = 15.4; + TestTargetID = A4000001; + }; + }; + }; + buildConfigurationList = A9000001 /* Build configuration list for PBXProject "PaddleOCRDemo" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A7000001; + productRefGroup = A7000003 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A4000001 /* PaddleOCRDemo */, + A4000002 /* PaddleOCRDemoTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A8000003 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A10000D0 /* Assets.xcassets in Resources */, + A1000003 /* Models in Resources */, + A1000091 /* SampleImages in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A8000004 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A10000C7 /* Fixtures in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6530B8008C9EFD4F162C3E86 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-PaddleOCRDemo/Pods-PaddleOCRDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-PaddleOCRDemo/Pods-PaddleOCRDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PaddleOCRDemo/Pods-PaddleOCRDemo-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9FD4B8CB3C3E1C27991BDE0B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-PaddleOCRDemoTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + A9D5C3BAF4FDCBE19D45BCA7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-PaddleOCRDemo-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A8000001 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A1000001 /* PaddleOCRDemoApp.swift in Sources */, + A10000D1 /* Bundle+BundledImage.swift in Sources */, + A10000D2 /* DemoAssets.swift in Sources */, + A1000002 /* ContentView.swift in Sources */, + A10000C0 /* DemoSectionChrome.swift in Sources */, + A1000021 /* DBPostProcess.swift in Sources */, + A1000022 /* OpenCVDBBridge.mm in Sources */, + A1000023 /* OpenCVImageBridge.mm in Sources */, + A10000E1 /* clipper.cpp in Sources */, + A10000E2 /* PDBPolygonOffsetBridge.mm in Sources */, + A1000030 /* DetectionEngine.swift in Sources */, + A1000004 /* ModelConfig.swift in Sources */, + A1000005 /* ORTSessionManager.swift in Sources */, + A10000F0 /* ORTProfilingBridge.mm in Sources */, + A1000007 /* InferenceConfig.swift in Sources */, + A10000B1 /* OCRParameterResolver.swift in Sources */, + A1000008 /* Preprocessing.swift in Sources */, + A1000040 /* RecPreprocessor.swift in Sources */, + A1000050 /* CTCDecoder.swift in Sources */, + A1000060 /* RecognitionEngine.swift in Sources */, + A1000070 /* OCREngine.swift in Sources */, + A1000071 /* BoxSorter.swift in Sources */, + A1000072 /* QuadTextCrop.swift in Sources */, + A1000074 /* EncodedImageCodec.swift in Sources */, + A1000080 /* OCRViewModel.swift in Sources */, + A1000090 /* ImagePickerSection.swift in Sources */, + A10000A0 /* ResultImageView.swift in Sources */, + A10000A1 /* TimingView.swift in Sources */, + A10000A2 /* ResultsListView.swift in Sources */, + A10000A3 /* ErrorView.swift in Sources */, + A10000A5 /* OnnxRuntimeSettingsPanel.swift in Sources */, + A10000A6 /* EngineSettingsSheet.swift in Sources */, + A10000A4 /* OCRParametersPanel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A8000002 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A1000011 /* ORTSessionManagerTests.swift in Sources */, + A10000B3 /* OCRParameterResolverTests.swift in Sources */, + A10000C5 /* OCRBenchmarkTests.swift in Sources */, + A10000C6 /* BenchmarkPayloads.swift in Sources */, + A10000D3 /* BenchmarkFixtures.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + A5000002 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A4000001 /* PaddleOCRDemo */; + targetProxy = A5000001 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + AA000001 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + AA000002 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AA000003 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 520BC3C496ED955BBB302DC4 /* Pods-PaddleOCRDemo.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 76MLN428C9; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PaddleOCRDemo/Supporting/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "PaddleOCR"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl,-no_warn_duplicate_libraries", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.paddleocr.demo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "PaddleOCRDemo/Supporting/PaddleOCRDemo-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + USER_HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/PaddleOCRDemo/Engine", + "$(SRCROOT)/PaddleOCRDemo/ThirdParty/Clipper1", + "$(PODS_ROOT)/onnxruntime-objc/objectivec", + "$(PODS_ROOT)/onnxruntime-objc/objectivec/include", + ); + }; + name = Debug; + }; + AA000004 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 54F64B7C830508805D832F8A /* Pods-PaddleOCRDemo.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 76MLN428C9; + ENABLE_TESTABILITY = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PaddleOCRDemo/Supporting/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "PaddleOCR"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl,-no_warn_duplicate_libraries", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.paddleocr.demo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "PaddleOCRDemo/Supporting/PaddleOCRDemo-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + USER_HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/PaddleOCRDemo/Engine", + "$(SRCROOT)/PaddleOCRDemo/ThirdParty/Clipper1", + "$(PODS_ROOT)/onnxruntime-objc/objectivec", + "$(PODS_ROOT)/onnxruntime-objc/objectivec/include", + ); + }; + name = Release; + }; + AA000005 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 72FF5C269BF861629F89D6B5 /* Pods-PaddleOCRDemoTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 76MLN428C9; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.paddleocr.demo.tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PaddleOCRDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/PaddleOCRDemo"; + }; + name = Debug; + }; + AA000006 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 49C7CEE60686CC98BE2EEE45 /* Pods-PaddleOCRDemoTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 76MLN428C9; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.paddleocr.demo.tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PaddleOCRDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/PaddleOCRDemo"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A9000001 /* Build configuration list for PBXProject "PaddleOCRDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA000001 /* Debug */, + AA000002 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A9000003 /* Build configuration list for PBXNativeTarget "PaddleOCRDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA000003 /* Debug */, + AA000004 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A9000006 /* Build configuration list for PBXNativeTarget "PaddleOCRDemoTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA000005 /* Debug */, + AA000006 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A3000001 /* Project object */; +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcodeproj/xcshareddata/xcschemes/PaddleOCRDemo.xcscheme b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcodeproj/xcshareddata/xcschemes/PaddleOCRDemo.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..2cae7330ba23a7ec1d8d5c4440114666a3a38ea7 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcodeproj/xcshareddata/xcschemes/PaddleOCRDemo.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcworkspace/contents.xcworkspacedata b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000000000000000000000000000000000..c0bcb46a77c59930e73ae0dbae56d1100d0b16c7 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/Bundle+BundledImage.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/Bundle+BundledImage.swift new file mode 100644 index 0000000000000000000000000000000000000000..7d4c4ccaaec79233be2212b7c7605d9283a01c66 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/Bundle+BundledImage.swift @@ -0,0 +1,48 @@ +// Copyright (c) 2026 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. + +import Foundation + +extension Bundle { + /// Raster extensions tried when resolving a bundled image by stem, or after an explicit extension misses. + static let bundledRasterImageExtensions: [String] = [ + "jpg", "jpeg", "png", "heic", "heif", "webp", "tiff", "tif", "bmp", "gif", + ] + + /// Resolves a filesystem path for an image under `subdirectory`. + /// + /// `name` may be a resource stem (e.g. `photo`) or include a suffix (e.g. `photo.jpg`). + /// Extensions are matched case-insensitively when provided; otherwise each entry in + /// ``bundledRasterImageExtensions`` is tried in order. + func path(forBundledImageNamed name: String, subdirectory: String) -> String? { + let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + let ns = trimmed as NSString + let extGiven = ns.pathExtension.lowercased() + let stem = ns.deletingPathExtension + let resourceStem = stem.isEmpty ? trimmed : stem + + if !extGiven.isEmpty { + if let path = path(forResource: resourceStem, ofType: extGiven, inDirectory: subdirectory) { + return path + } + } + for ext in Self.bundledRasterImageExtensions where ext != extGiven { + if let path = path(forResource: resourceStem, ofType: ext, inDirectory: subdirectory) { + return path + } + } + return nil + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/ContentView.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/ContentView.swift new file mode 100644 index 0000000000000000000000000000000000000000..e728c161474934832e23107820ff32c3ec61fcf7 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/ContentView.swift @@ -0,0 +1,321 @@ +// Copyright (c) 2026 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. + +import PhotosUI +import SwiftUI + +/// Root view: routes `AppState` and lays out **input (photo/sample)** vs **output (preview + text)**; engine tuning lives in ``EngineSettingsSheet``. +struct ContentView: View { + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @StateObject private var viewModel = OCRViewModel() + @State private var selectedItem: PhotosPickerItem? + @State private var showEngineSettings = false + + private var navigationTitleDisplayMode: NavigationBarItem.TitleDisplayMode { + switch viewModel.state { + case .ready: return .large + case .error(let err) where !err.isModelError: return .large + default: return .inline + } + } + + private var showsAdvancedToolbarItem: Bool { + switch viewModel.state { + case .loadingModels, .processing: return false + case .ready, .results, .error: return true + } + } + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 0) { + contentForState + } + .padding(.horizontal, 20) + .padding(.vertical, 20) + } + .background(Color(.systemGroupedBackground)) + .navigationTitle("PaddleOCR Demo") + .navigationBarTitleDisplayMode(navigationTitleDisplayMode) + .toolbar { + if showsAdvancedToolbarItem { + ToolbarItem(placement: .topBarTrailing) { + Button { + showEngineSettings = true + } label: { + Image(systemName: "slider.horizontal.3") + } + .accessibilityLabel("Advanced settings") + } + } + } + } + .sheet(isPresented: $showEngineSettings) { + EngineSettingsSheet(viewModel: viewModel) + } + .task { + await viewModel.loadModels() + } + .onChange(of: selectedItem) { newItem in + guard let item = newItem else { return } + Task { + guard let data = try? await item.loadTransferable(type: Data.self), + let uiImage = UIImage(data: data) else { + viewModel.reportImageLoadFailed() + return + } + selectedItem = nil + await viewModel.processImage(uiImage) + } + } + } + + private func imageInputSection(title: String, subtitle: String) -> some View { + VStack(alignment: .leading, spacing: 10) { + DemoSectionHeader(title: title, subtitle: subtitle) + ImagePickerSection( + selectedItem: $selectedItem, + sampleImageNames: viewModel.sampleImageNames, + onSampleSelected: { name in + Task { await viewModel.selectSampleImage(named: name) } + } + ) + } + } + + // MARK: - State Router + + @ViewBuilder + private var contentForState: some View { + switch viewModel.state { + case .loadingModels: + loadingModelsView + case .ready: + readyView + case .processing(let image): + processingView(image: image) + case .results(let result, let image, _): + resultsView(result: result, image: image) + case .error(let error): + errorView(error: error) + } + } + + // MARK: - Loading Models + + private var loadingModelsView: some View { + VStack(spacing: 16) { + ProgressView() + .scaleEffect(1.3) + .tint(.accentColor) + Text("Loading models") + .font(.title3.weight(.semibold)) + Text("Preparing detection and recognition…") + .font(.subheadline) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .padding(.top, 72) + .padding(.bottom, 40) + } + + // MARK: - Ready + + private var readyView: some View { + VStack(spacing: 22) { + statusPill(icon: "checkmark.circle.fill", tint: .green) { + VStack(alignment: .leading, spacing: 4) { + Text("Ready") + .font(.title3.weight(.semibold)) + Text("Choose a photo or open a sample. Engine and OCR tuning are under Advanced in the toolbar.") + .font(.subheadline) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + + imageInputSection( + title: "Image", + subtitle: "Choose from your library or a bundled sample." + ) + } + } + + // MARK: - Processing + + private func processingView(image: UIImage) -> some View { + VStack(alignment: .leading, spacing: 16) { + Text("Running OCR") + .font(.title3.weight(.semibold)) + Text("Hold on — detection and recognition are in progress.") + .font(.subheadline) + .foregroundStyle(.secondary) + + ZStack { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 360) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(.ultraThinMaterial) + + VStack(spacing: 10) { + ProgressView() + .scaleEffect(1.25) + .tint(.accentColor) + Text("Working…") + .font(.headline) + } + } + .frame(maxWidth: .infinity) + } + } + + // MARK: - Results + + private func resultsView(result: OCRRunResult, image: UIImage) -> some View { + // Only use two columns when explicitly regular (e.g. iPad); treat nil as compact. + let isCompact = horizontalSizeClass != .regular + return VStack(spacing: 22) { + if isCompact { + previewBlock(result: result, image: image) + TimingView(result: result) + .padding(.vertical, 4) + } else { + TimingView(result: result) + .padding(.vertical, 4) + HStack(alignment: .top, spacing: 16) { + previewBlock(result: result, image: image) + .frame(maxWidth: .infinity) + .layoutPriority(1) + resultsListBlock(result: result) + .frame(minWidth: 280, maxWidth: .infinity) + } + } + + if isCompact { + resultsListBlock(result: result) + } + + imageInputSection( + title: "New image", + subtitle: "Choose another photo or sample. Re-run uses the current image and OCR parameters." + ) + + rerunOCRButton() + } + } + + @ViewBuilder + private func rerunOCRButton() -> some View { + if viewModel.hasPendingOcrChanges { + Button { + Task { await viewModel.rerunOCR() } + } label: { + Label("Re-run OCR on this image", systemImage: "arrow.clockwise.circle.fill") + .frame(maxWidth: .infinity) + .padding(.vertical, 4) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + } else { + Button { + Task { await viewModel.rerunOCR() } + } label: { + Label("Re-run OCR on this image", systemImage: "arrow.clockwise.circle.fill") + .frame(maxWidth: .infinity) + .padding(.vertical, 4) + } + .buttonStyle(.bordered) + .controlSize(.large) + } + } + + private func previewBlock(result: OCRRunResult, image: UIImage) -> some View { + VStack(alignment: .leading, spacing: 10) { + DemoSectionHeader( + title: "Preview", + subtitle: "Accent overlay shows each detected text region." + ) + DemoCard { + ResultImageView(image: image, results: result.results) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + } + } + } + + private func resultsListBlock(result: OCRRunResult) -> some View { + VStack(alignment: .leading, spacing: 10) { + DemoSectionHeader(title: "Results", subtitle: nil) + DemoCard { + ResultsListView( + results: result.results, + copiedFeedback: viewModel.copiedFeedback, + onCopy: { viewModel.copyResultsToClipboard() } + ) + } + } + } + + // MARK: - Error + + private func errorView(error: AppError) -> some View { + VStack(spacing: 22) { + ErrorView(error: error, onRetry: { + Task { await viewModel.retry() } + }) + + if !error.isModelError { + VStack(alignment: .leading, spacing: 10) { + DemoSectionHeader(title: "Try another image", subtitle: nil) + ImagePickerSection( + selectedItem: $selectedItem, + sampleImageNames: viewModel.sampleImageNames, + onSampleSelected: { name in + Task { await viewModel.selectSampleImage(named: name) } + } + ) + } + } + } + .padding(.top, 24) + } + + // MARK: - Small chrome + + private func statusPill(icon: String, tint: Color, @ViewBuilder content: () -> Content) -> some View { + HStack(alignment: .top, spacing: 14) { + Image(systemName: icon) + .font(.system(size: 28)) + .foregroundStyle(tint) + .symbolRenderingMode(.hierarchical) + content() + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color(.secondarySystemGroupedBackground)) + } + .overlay { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .strokeBorder(Color(.separator).opacity(0.35), lineWidth: 0.5) + } + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/DemoAssets.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/DemoAssets.swift new file mode 100644 index 0000000000000000000000000000000000000000..e21a33d6a7991128e3256bd87f91aa408646b7a3 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/DemoAssets.swift @@ -0,0 +1,21 @@ +// Copyright (c) 2026 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. + +import Foundation + +/// Built-in demo images (bundle resource stems). +enum DemoAssets { + /// Default bundled sample for the image picker. + static let defaultSampleImageStem: String = "general_ocr_002" +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/DemoSectionChrome.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/DemoSectionChrome.swift new file mode 100644 index 0000000000000000000000000000000000000000..6230d0623190329db9989ad76701b8ae0251367e --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/DemoSectionChrome.swift @@ -0,0 +1,62 @@ +// Copyright (c) 2026 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. + +import SwiftUI + +// MARK: - Section chrome (shared by ContentView + panels) + +/// Vertical section title + optional subtitle for scan-friendly hierarchy. +struct DemoSectionHeader: View { + let title: String + var subtitle: String? + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.footnote.weight(.semibold)) + .foregroundStyle(.secondary) + .textCase(.uppercase) + .tracking(0.4) + if let subtitle { + Text(subtitle) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +/// Grouped card matching system settings style. +struct DemoCard: View { + let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + content + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color(.secondarySystemGroupedBackground)) + } + .overlay { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .strokeBorder(Color(.separator).opacity(0.35), lineWidth: 0.5) + } + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/PaddleOCRDemoApp.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/PaddleOCRDemoApp.swift new file mode 100644 index 0000000000000000000000000000000000000000..1f293e8d34d773863e437adf17aad704784669fb --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/PaddleOCRDemoApp.swift @@ -0,0 +1,24 @@ +// Copyright (c) 2026 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. + +import SwiftUI + +@main +struct PaddleOCRDemoApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/BoxSorter.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/BoxSorter.swift new file mode 100644 index 0000000000000000000000000000000000000000..a326daddd65cb9c11873eb77fbd0d29f322867d5 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/BoxSorter.swift @@ -0,0 +1,72 @@ +// Copyright (c) 2026 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. + +import Foundation + +// MARK: - BoxSorter + +/// Sorts detection boxes in natural reading order: top-to-bottom, then +/// left-to-right within the same text line. +/// +/// Two-pass: sort by top-left (y then x), then swap same-line neighbors when +/// the y-gap is below `yThreshold` but x is out of order. +struct BoxSorter { + + /// Y-coordinate threshold for same-line detection. Boxes whose top-left y + /// coordinates differ by less than this value are considered on the same line + /// and sorted left-to-right. Default: 10 pixels. + static let yThreshold: Int32 = 10 + + /// Sort detection boxes in reading order: top-to-bottom, then left-to-right + /// within the same line (defined by yThreshold). + /// + /// Algorithm: + /// 1. Sort all boxes by top-left corner: first by y, then by x. + /// 2. Insertion-sort pass: for each box, walk backward and swap with the + /// previous box if they are on the same line (y-difference < 10) and + /// the current box's x is smaller. + /// + /// - Parameter boxes: Unordered detection boxes from DBPostProcessor. + /// - Returns: Boxes sorted in reading order. + static func sortInReadingOrder(_ boxes: [DetectionBox]) -> [DetectionBox] { + guard boxes.count > 1 else { return boxes } + + // Step 1: Sort by top-left point -- first by y, then by x + var sorted = boxes.sorted { a, b in + let ay = a.points[0][1], ax = a.points[0][0] + let by = b.points[0][1], bx = b.points[0][0] + if ay != by { return ay < by } + return ax < bx + } + + // Step 2: Backward insertion sort for same-line boxes + for i in 0..<(sorted.count - 1) { + var j = i + while j >= 0 { + let nextY = sorted[j + 1].points[0][1] + let currY = sorted[j].points[0][1] + let nextX = sorted[j + 1].points[0][0] + let currX = sorted[j].points[0][0] + if abs(nextY - currY) < yThreshold && nextX < currX { + sorted.swapAt(j, j + 1) + j -= 1 + } else { + break + } + } + } + + return sorted + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/CTCDecoder.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/CTCDecoder.swift new file mode 100644 index 0000000000000000000000000000000000000000..2bf7867c449289169ab9c18d2baf80799065640a --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/CTCDecoder.swift @@ -0,0 +1,199 @@ +// Copyright (c) 2026 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. + +import Foundation + +// MARK: - Errors + +enum CTCDecoderError: LocalizedError { + case emptyDictionary + case invalidOutputShape([Int]) + + var errorDescription: String? { + switch self { + case .emptyDictionary: + return "Character dictionary is empty" + case .invalidOutputShape(let shape): + return "Invalid recognition output shape: \(shape), expected [1, T, C]" + } + } +} + +// MARK: - Recognition Result + +/// A single text recognition result from CTC decoding. +struct RecognitionResult { + /// The decoded text string. + let text: String + /// Average confidence across all decoded characters (0.0 to 1.0). + let confidence: Float +} + +// MARK: - CTCDecoder + +/// CTC label decoding for recognition output +/// +/// Greedy CTC decoding with blank index 0 and duplicate collapse. +/// Per timestep: take the best class and its score, then merge duplicates and drop blanks, +/// then average the kept scores. Logits need not be softmax probabilities; +/// `max` over the class dimension is used for confidence on exported logits. +/// +/// The CTC (Connectionist Temporal Classification) decoding algorithm: +/// 1. Argmax across the class dimension to get per-timestep predicted indices +/// 2. Max across the class dimension to get per-timestep prediction probabilities +/// 3. Remove consecutive duplicate indices (CTC merge rule) +/// 4. Remove blank tokens (index 0) +/// 5. Map remaining indices to characters via the dictionary +/// 6. Confidence = mean of probabilities at selected positions +/// +struct CTCDecoder { + private let characters: [String] + + /// The blank token index (always 0 for CTC). + private let blankIndex: Int = 0 + + /// Creates a CTCDecoder from a recognition model's InferenceConfig. + /// + /// Reads `character_dict` from the model config file and prepends the blank token. + /// + /// - Parameter config: A parsed InferenceConfig from the recognition model's config file. + /// - Throws: `CTCDecoderError.emptyDictionary` if no character dict is found. + init(config: InferenceConfig) throws { + guard let dict = config.postProcess.characterDict, !dict.isEmpty else { + throw CTCDecoderError.emptyDictionary + } + + // Build character list: dict from config, trailing space (default-on), blank at index 0. + var chars = ["blank"] + chars.append(contentsOf: dict) + chars.append(" ") + self.characters = chars + } + + /// Decodes the raw recognition model output into text with confidence. + /// + /// The output tensor has shape `[1, T, C]` where: + /// - T = number of timesteps (sequence length) + /// - C = number of classes (character vocabulary size + 1 for blank) + /// + /// - Parameters: + /// - outputData: Flat float32 array of the model output. + /// - outputShape: Shape of the output tensor, expected `[1, T, C]`. + /// - Returns: A `RecognitionResult` with decoded text and confidence score. + func decode(outputData: [Float], outputShape: [Int]) throws -> RecognitionResult { + guard outputShape.count == 3, outputShape[0] == 1 else { + throw CTCDecoderError.invalidOutputShape(outputShape) + } + + let timesteps = outputShape[1] + let numClasses = outputShape[2] + + // Step 1 & 2: Argmax and max across the class dimension for each timestep + var predIndices = [Int](repeating: 0, count: timesteps) + var predProbs = [Float](repeating: 0, count: timesteps) + + for t in 0.. maxVal { + maxVal = val + maxIdx = c + } + } + + predIndices[t] = maxIdx + predProbs[t] = maxVal + } + + // Step 3: Build selection mask + // - Remove consecutive duplicates (keep first of each run) + // - Remove blank tokens (index 0) + // + // Drop consecutive duplicate indices, then drop blank (0). + var selection = [Bool](repeating: true, count: timesteps) + + // Remove consecutive duplicates + for t in 1.. [RecognitionResult] { + guard outputShape.count == 3 else { + throw CTCDecoderError.invalidOutputShape(outputShape) + } + let batchCount = outputShape[0] + let timesteps = outputShape[1] + let numClasses = outputShape[2] + guard batchCount >= 1, timesteps >= 1, numClasses >= 1 else { + throw CTCDecoderError.invalidOutputShape(outputShape) + } + let rowElements = timesteps * numClasses + let expected = batchCount * rowElements + guard outputData.count == expected else { + throw CTCDecoderError.invalidOutputShape(outputShape) + } + var results: [RecognitionResult] = [] + results.reserveCapacity(batchCount) + for b in 0.. [DetectionBox] { + // 1. Extract probability map: pred[0, 0, :, :] from shape [1, 1, H, W] + let mapSize = tensorHeight * tensorWidth + let pred: [Float] + if outputTensor.count >= mapSize { + pred = Array(outputTensor.prefix(mapSize)) + } else { + return [] + } + + // 2. Binary threshold: create mask where prob > thresh + var binaryMask = [UInt8](repeating: 0, count: mapSize) + for i in 0.. thresh ? 1 : 0 + } + + // 3. Find contours on the binary mask + let contours = findContours( + binaryMask: binaryMask, + width: tensorWidth, + height: tensorHeight + ) + + // 4. Process each contour + let numContours = min(contours.count, maxCandidates) + var results: [DetectionBox] = [] + + for i in 0.. score { + continue + } + + // 4c. Expand polygon (unclip) via integer-grid offset. + let nsMini = miniBox.map { NSValue(cgPoint: CGPoint(x: CGFloat($0.x), y: CGFloat($0.y))) } + let area = Double(PDBOpenCVDBBridge.contourArea(fromPoints: nsMini)) + let length = Double(PDBOpenCVDBBridge.arcLength(fromPoints: nsMini, closed: true)) + guard length > 0 else { continue } + let distance = area * Double(unclipRatio) / length + + let rings = PDBPolygonOffsetBridge.inflateClosedPolygon(with: nsMini, distance: distance) as [[NSValue]] + guard rings.count == 1 else { + continue + } + let firstRing = rings[0] + + // 4d. Second getMiniBoxes on the expanded polygon + let expandedFloats = firstRing.map { FloatPoint(x: Float($0.cgPointValue.x), y: Float($0.cgPointValue.y)) } + guard let (finalBox, finalSside) = getMiniBoxes(contour: expandedFloats) else { + continue + } + if finalSside < minSize + 2 { + continue + } + + // 4e. Scale to original image dimensions + let w = Float(tensorWidth) + let h = Float(tensorHeight) + let dw = Float(originalWidth) + let dh = Float(originalHeight) + + var scaledPoints: [[Int32]] = [] + for pt in finalBox { + let sx = Int32(min(max((pt.x / w * dw).rounded(), 0), dw)) + let sy = Int32(min(max((pt.y / h * dh).rounded(), 0), dh)) + scaledPoints.append([sx, sy]) + } + + results.append(DetectionBox(points: scaledPoints, score: score)) + } + + return results + } +} + +// MARK: - Internal Point Type + +/// Float 2D point for `DBPostProcessor` geometry helpers. +internal struct FloatPoint { + var x: Float + var y: Float +} + +// MARK: - OpenCV (CocoaPods OpenCV 4.3.x) + +extension DBPostProcessor { + + /// Foreground pixels as 255 on an 8-bit mask; runs `findContours` with `RETR_LIST` and `CHAIN_APPROX_SIMPLE`. + func findContours(binaryMask: [UInt8], width: Int, height: Int) -> [[FloatPoint]] { + let data = Data(binaryMask) + let raw = PDBOpenCVDBBridge.findContours( + binaryMask: data, + width: width, + height: height, + maxCandidates: maxCandidates + ) + return raw.map { contour in + contour.map { v in + let p = v.cgPointValue + return FloatPoint(x: Float(p.x), y: Float(p.y)) + } + } + } + + /// Minimum-area rectangle: `minAreaRect` on contour points, `boxPoints` for corners, then TL/TR/BR/BL ordering. + func getMiniBoxes(contour: [FloatPoint]) -> (box: [FloatPoint], minSide: Float)? { + guard contour.count >= 2 else { return nil } + let values = contour.map { NSValue(cgPoint: CGPoint(x: CGFloat($0.x), y: CGFloat($0.y))) } + guard let result = PDBOpenCVDBBridge.miniBox(fromPoints: values) else { return nil } + let corners = result.corners.map { v in + let p = v.cgPointValue + return FloatPoint(x: Float(p.x), y: Float(p.y)) + } + return (corners, result.minSide) + } +} + +// MARK: - Box Scoring + +extension DBPostProcessor { + + /// Compute the mean probability score within a box region of the probability map. + /// + /// Mean probability inside the quad: `fillPoly` mask on the score map, then `mean` over the masked region. + func boxScoreFast( + pred: [Float], + predWidth: Int, + predHeight: Int, + box: [[Float]] + ) -> Float { + let predData = pred.withUnsafeBufferPointer { Data(buffer: $0) } + let boxNS: [[NSNumber]] = box.map { [NSNumber(value: $0[0]), NSNumber(value: $0[1])] } + return PDBOpenCVImageBridge.meanPredInQuad( + predData, + width: predWidth, + height: predHeight, + box: boxNS + ) + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/DetectionEngine.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/DetectionEngine.swift new file mode 100644 index 0000000000000000000000000000000000000000..6c2ded783fac14233a920135b854d82d886aaf4b --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/DetectionEngine.swift @@ -0,0 +1,168 @@ +// Copyright (c) 2026 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. + +import CoreGraphics +import Foundation + +// MARK: - Detection Result + +/// The result of running text detection on an image. +/// +/// Contains the detected text boxes and per-stage timing metrics for performance analysis. +struct DetectionResult { + /// Detected text region bounding polygons with confidence scores. + let boxes: [DetectionBox] + /// Seconds spent in preprocessing (resize, normalize, HWC-to-CHW). + let preprocessTime: TimeInterval + /// Seconds spent in ONNX Runtime inference. + let inferenceTime: TimeInterval + /// Seconds spent in DB postprocessing (threshold, contours, polygon offset / unclip). + let postprocessTime: TimeInterval + /// Total detection time (preprocess + inference + postprocess). + let totalTime: TimeInterval + /// Detection model input tensor shape, e.g. [1, 3, H, W]. + let inputTensorShape: [Int] +} + +// MARK: - Detection Engine Errors + +enum DetectionEngineError: LocalizedError { + case noOutputTensor + case unexpectedOutputShape([Int]) + + var errorDescription: String? { + switch self { + case .noOutputTensor: + return "Detection model produced no output tensor" + case .unexpectedOutputShape(let shape): + return "Unexpected detection output shape: \(shape), expected [1, 1, H, W]" + } + } +} + +// MARK: - DetectionEngine + +/// Orchestrates text detection: CGImage → preprocess → ORT inference → postprocess. +/// +/// This is the integration layer that composes `DetPreprocessor`, `ORTSessionManager`, and +/// `DBPostProcessor` into a single callable unit. +/// +/// Usage: +/// ```swift +/// let engine = try DetectionEngine(sessionManager: manager) +/// let result = try await engine.detect(cgImage) +/// print("Found \(result.boxes.count) text regions in \(result.totalTime)s") +/// ``` +class DetectionEngine { + private let sessionManager: ORTSessionManager + private let preprocessor: DetPreprocessor + private(set) var modelConfig: InferenceConfig + + /// Initialize with an existing ORTSessionManager (models must already be loaded). + /// + /// Loads the detection model config file from the bundle to configure + /// the preprocessor and postprocessor. + /// + /// - Parameter sessionManager: A loaded ORTSessionManager with detection model ready. + /// - Throws: If the model config cannot be loaded or required fields are missing. + init(sessionManager: ORTSessionManager) throws { + self.sessionManager = sessionManager + + // Load detection model config + let detPaths = try ModelConfig.detection() + let cfg = try InferenceConfig.load(from: detPaths.configPath) + self.modelConfig = cfg + self.preprocessor = try DetPreprocessor(config: cfg) + } + + private func makePostProcessor(effective: EffectiveOCRParams) -> DBPostProcessor { + DBPostProcessor( + thresh: effective.detThresh, + boxThresh: effective.detBoxThresh, + maxCandidates: modelConfig.postProcess.maxCandidates, + unclipRatio: effective.detUnclipRatio + ) + } + + /// Run detection on a CGImage, returning bounding polygons with confidence scores. + /// + /// Steps: DetResizeForTest → NormalizeImage → ToCHW → ORT inference → DBPostProcess + /// + /// - Parameters: + /// - image: The input image to detect text regions in. + /// - runtimeParams: Detection postprocess / resize overrides (see ``OCRRuntimeParams``). + /// - Returns: A `DetectionResult` with boxes and per-stage timing metrics. + func detect(_ image: CGImage, runtimeParams: OCRRuntimeParams = .noOverrides) async throws -> DetectionResult { + let eff = OCRParameterResolver.effective(modelConfig: modelConfig, runtime: runtimeParams) + return try await detect(effective: eff) { + try preprocessor.preprocess(image, detResize: eff.detResize) + } + } + + private func detect( + effective: EffectiveOCRParams, + preprocess: () throws -> PreprocessResult + ) async throws -> DetectionResult { + let postprocessor = makePostProcessor(effective: effective) + // Step 1: Preprocess + let preprocessStart = CFAbsoluteTimeGetCurrent() + let preprocessed = try preprocess() + let preprocessTime = CFAbsoluteTimeGetCurrent() - preprocessStart + + // Step 2: Run ORT inference + let inferenceStart = CFAbsoluteTimeGetCurrent() + let outputs = try await sessionManager.runDetection( + inputData: preprocessed.tensorData, + shape: preprocessed.tensorShape + ) + let inferenceTime = CFAbsoluteTimeGetCurrent() - inferenceStart + + // Step 3: Extract the probability map from ORT output + // The det model outputs a single tensor with shape [1, 1, H, W] + guard let firstOutput = outputs.values.first else { + throw DetectionEngineError.noOutputTensor + } + let outputData = firstOutput.data + let outputShape = firstOutput.shape + + // Output shape should be [1, 1, H, W] + guard outputShape.count == 4 else { + throw DetectionEngineError.unexpectedOutputShape(outputShape) + } + let tensorHeight = outputShape[2] + let tensorWidth = outputShape[3] + + // Step 4: Postprocess + let postprocessStart = CFAbsoluteTimeGetCurrent() + let boxes = postprocessor.process( + outputTensor: outputData, + tensorHeight: tensorHeight, + tensorWidth: tensorWidth, + originalWidth: preprocessed.originalSize.width, + originalHeight: preprocessed.originalSize.height + ) + let postprocessTime = CFAbsoluteTimeGetCurrent() - postprocessStart + + let totalTime = preprocessTime + inferenceTime + postprocessTime + + return DetectionResult( + boxes: boxes, + preprocessTime: preprocessTime, + inferenceTime: inferenceTime, + postprocessTime: postprocessTime, + totalTime: totalTime, + inputTensorShape: preprocessed.tensorShape + ) + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/EncodedImageCodec.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/EncodedImageCodec.swift new file mode 100644 index 0000000000000000000000000000000000000000..b4d704da03d3b355e30dedcfea211a8719a14f4c --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/EncodedImageCodec.swift @@ -0,0 +1,51 @@ +// Copyright (c) 2026 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. + +import CoreGraphics +import Foundation +import ImageIO + +enum EncodedImageCodecError: LocalizedError { + case emptyData + case createImageSourceFailed + case createImageFailed + + var errorDescription: String? { + switch self { + case .emptyData: + return "Image data is empty" + case .createImageSourceFailed: + return "Could not create image source from data" + case .createImageFailed: + return "Could not decode image from data" + } + } +} + +/// Decodes PNG/JPEG/WebP and other formats supported by Image I/O into a ``CGImage``. +enum EncodedImageCodec { + static func cgImage(fromEncodedData data: Data) throws -> CGImage { + guard !data.isEmpty else { + throw EncodedImageCodecError.emptyData + } + guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { + throw EncodedImageCodecError.createImageSourceFailed + } + let options: [CFString: Any] = [kCGImageSourceShouldCache: true] + guard let cgImage = CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary) else { + throw EncodedImageCodecError.createImageFailed + } + return cgImage + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/InferenceConfig.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/InferenceConfig.swift new file mode 100644 index 0000000000000000000000000000000000000000..2568ef5e736d1b501ff6ae03d3c104c914a1ec44 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/InferenceConfig.swift @@ -0,0 +1,322 @@ +// Copyright (c) 2026 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. + +import Foundation +import Yams + +// MARK: - Errors + +enum InferenceConfigError: LocalizedError { + case fileNotFound(String) + case parseError(String) + case missingField(String) + + var errorDescription: String? { + switch self { + case .fileNotFound(let path): + return "Model config file not found: \(path)" + case .parseError(let detail): + return "Failed to parse model config: \(detail)" + case .missingField(let field): + return "Missing required field in model config: \(field)" + } + } +} + +// MARK: - Transform Operations + +/// Channel order implied by `DecodeImage` in the model config file (`img_mode`: BGR or RGB). +/// When `DecodeImage` is absent, defaults to **BGR** (typical decode path for three-channel inputs). +enum InferenceImageChannelOrder: Equatable, Sendable { + case bgr + case rgb + + /// Parses `DecodeImage.img_mode`; empty/missing defaults to **BGR**. + static func fromDecodeImage(imgMode: String?) -> InferenceImageChannelOrder { + guard let raw = imgMode?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { + return .bgr + } + switch raw.lowercased() { + case "rgb": return .rgb + case "bgr": return .bgr + default: + return .bgr + } + } +} + +/// Parameters for `DetResizeForTest` from the model config file. +struct DetResizeParams: Equatable { + var limitSideLen: Int + var limitType: String + var maxSideLimit: Int +} + +/// Represents a single preprocessing transform operation parsed from the model config file. +/// Each case carries associated parameters read from the model config file. +enum TransformOp { + /// `DecodeImage` in the model config file — selects BGR vs RGB for decoded pixels. + case decodeImage(channelOrder: InferenceImageChannelOrder) + case detResizeForTest(DetResizeParams) + case normalizeImage(scale: Float, mean: [Float], std: [Float], order: String) + case toCHWImage + case recResizeImg(imageShape: [Int]) + case unknown(name: String) +} + +// MARK: - Config Structures + +struct PreProcessConfig { + let transformOps: [TransformOp] +} + +struct PostProcessConfig { + let name: String + let thresh: Float + let boxThresh: Float + let maxCandidates: Int + let unclipRatio: Float + let characterDict: [String]? +} + +struct InferenceConfig { + let modelName: String + let preProcess: PreProcessConfig + let postProcess: PostProcessConfig + + // MARK: - Loading + + /// Loads and parses a model config file into a typed `InferenceConfig`. + /// + /// - Parameter path: Absolute filesystem path to the model config file. + /// - Returns: A fully parsed `InferenceConfig` with typed transform operations. + static func load(from path: String) throws -> InferenceConfig { + guard FileManager.default.fileExists(atPath: path) else { + throw InferenceConfigError.fileNotFound(path) + } + + let configFileText: String + do { + configFileText = try String(contentsOfFile: path, encoding: .utf8) + } catch { + throw InferenceConfigError.parseError("Cannot read file: \(error.localizedDescription)") + } + + guard let root = try Yams.load(yaml: configFileText) as? [String: Any] else { + throw InferenceConfigError.parseError("Root element is not a dictionary") + } + + // Parse Global.model_name + guard let global = root["Global"] as? [String: Any], + let modelName = global["model_name"] as? String else { + throw InferenceConfigError.missingField("Global.model_name") + } + + // Parse PreProcess + guard let preProcessDict = root["PreProcess"] as? [String: Any], + let transformOpsRaw = preProcessDict["transform_ops"] as? [[String: Any?]] else { + throw InferenceConfigError.missingField("PreProcess.transform_ops") + } + + let transformOps = transformOpsRaw.map { parseTransformOp($0) } + let preProcess = PreProcessConfig(transformOps: transformOps) + + // Parse PostProcess + guard let postProcessDict = root["PostProcess"] as? [String: Any] else { + throw InferenceConfigError.missingField("PostProcess") + } + let postProcess = parsePostProcess(postProcessDict) + + return InferenceConfig( + modelName: modelName, + preProcess: preProcess, + postProcess: postProcess + ) + } +} + +extension InferenceConfig { + // MARK: - Private Parsing Helpers + + private enum ParseDefaults { + static let postThresh: Float = 0.3 + static let postBoxThresh: Float = 0.6 + static let postMaxCandidates: Int = 1000 + static let postUnclipRatio: Float = 2.0 + static let detResizeLimitSideLen: Int = 960 + static let detResizeLimitType: String = "max" + static let detResizeMaxSideLimit: Int = 4000 + } + + private static let defaultDetResizeParams = DetResizeParams( + limitSideLen: ParseDefaults.detResizeLimitSideLen, + limitType: ParseDefaults.detResizeLimitType, + maxSideLimit: ParseDefaults.detResizeMaxSideLimit + ) + + /// Parses a single transform operation dictionary (one key = op name, value = params or null). + private static func parseTransformOp(_ dict: [String: Any?]) -> TransformOp { + guard let opName = dict.keys.first else { + return .unknown(name: "empty") + } + + switch opName { + case "DecodeImage": + let params = dict[opName] as? [String: Any] ?? [:] + let imgMode = params["img_mode"] as? String + return .decodeImage(channelOrder: InferenceImageChannelOrder.fromDecodeImage(imgMode: imgMode)) + + case "DetResizeForTest": + let params = dict[opName] as? [String: Any] ?? [:] + let limitSideLen = + optionalIntValue(params["resize_long"]) + ?? optionalIntValue(params["limit_side_len"]) + ?? ParseDefaults.detResizeLimitSideLen + let limitType = params["limit_type"] as? String ?? ParseDefaults.detResizeLimitType + let maxSideLimit = + intValueForKeyIfPresent(params, key: "max_side_limit") + ?? ParseDefaults.detResizeMaxSideLimit + return .detResizeForTest( + DetResizeParams( + limitSideLen: limitSideLen, + limitType: limitType, + maxSideLimit: maxSideLimit + ) + ) + + case "NormalizeImage": + let params = dict[opName] as? [String: Any] ?? [:] + let scale = parseScale(params["scale"]) + let mean = parseFloatArray(params["mean"]) ?? [0.485, 0.456, 0.406] + let std = parseFloatArray(params["std"]) ?? [0.229, 0.224, 0.225] + let order = params["order"] as? String ?? "hwc" + return .normalizeImage(scale: scale, mean: mean, std: std, order: order) + + case "ToCHWImage": + return .toCHWImage + + case "RecResizeImg": + let params = dict[opName] as? [String: Any] ?? [:] + let imageShape = (params["image_shape"] as? [Any])?.compactMap { toInt($0) } ?? [3, 48, 320] + return .recResizeImg(imageShape: imageShape) + + default: + return .unknown(name: opName) + } + } + + /// Parses the `scale` field which may be a numeric value or a fraction string like `"1./255."`. + /// Handles string division expressions by splitting on "/" and computing the result. + private static func parseScale(_ value: Any?) -> Float { + if let floatVal = value as? Double { + return Float(floatVal) + } + if let stringVal = value as? String { + return parseScaleString(stringVal) + } + // Default scale: 1/255 + return 1.0 / 255.0 + } + + /// Evaluates a fraction string by splitting on `"/"` and dividing numerator by denominator. + private static func parseScaleString(_ s: String) -> Float { + if s.contains("/") { + let parts = s.split(separator: "/") + if parts.count == 2, + let numerator = Double(parts[0].trimmingCharacters(in: .init(charactersIn: "."))), + let denominator = Double(parts[1].trimmingCharacters(in: .init(charactersIn: "."))) { + return Float(numerator / denominator) + } + let rawParts = s.split(separator: "/") + if rawParts.count == 2, + let num = Double(rawParts[0]), + let den = Double(rawParts[1]) { + return Float(num / den) + } + } + if let val = Double(s) { + return Float(val) + } + return 1.0 / 255.0 + } + + private static func parseFloatArray(_ value: Any?) -> [Float]? { + guard let array = value as? [Any] else { return nil } + return array.compactMap { element -> Float? in + if let d = element as? Double { return Float(d) } + if let i = element as? Int { return Float(i) } + return nil + } + } + + private static func parsePostProcess(_ dict: [String: Any]) -> PostProcessConfig { + let name = dict["name"] as? String ?? "Unknown" + let thresh = (dict["thresh"] as? Double).map { Float($0) } ?? ParseDefaults.postThresh + let boxThresh = (dict["box_thresh"] as? Double).map { Float($0) } ?? ParseDefaults.postBoxThresh + let maxCandidates = dict["max_candidates"] as? Int ?? ParseDefaults.postMaxCandidates + let unclipRatio = (dict["unclip_ratio"] as? Double).map { Float($0) } ?? ParseDefaults.postUnclipRatio + let characterDict = (dict["character_dict"] as? [Any])?.compactMap { $0 as? String } + + return PostProcessConfig( + name: name, + thresh: thresh, + boxThresh: boxThresh, + maxCandidates: maxCandidates, + unclipRatio: unclipRatio, + characterDict: characterDict + ) + } + + private static func toInt(_ value: Any) -> Int? { + if let i = value as? Int { return i } + if let d = value as? Double { return Int(d) } + return nil + } + + private static func optionalIntValue(_ value: Any?) -> Int? { + guard let value else { return nil } + return toInt(value) + } + + /// Only returns a value when the key exists in `params` (a missing key yields `nil`). + private static func stringValueForKeyIfPresent(_ params: [String: Any], key: String) -> String? { + guard params.keys.contains(key) else { return nil } + return params[key] as? String + } + + private static func intValueForKeyIfPresent(_ params: [String: Any], key: String) -> Int? { + guard params.keys.contains(key) else { return nil } + return optionalIntValue(params[key]) + } + + /// First `DetResizeForTest` block in the model config file next to the ONNX weights, if present. + var detResizeFromModel: DetResizeParams? { + for op in preProcess.transformOps { + if case .detResizeForTest(let p) = op { return p } + } + return nil + } + + var detResizeForMerge: DetResizeParams { + detResizeFromModel ?? Self.defaultDetResizeParams + } + + /// First `DecodeImage` entry in `transform_ops` and its `img_mode` (BGR or RGB). If absent, **BGR**. + var decodeImageChannelOrder: InferenceImageChannelOrder { + for op in preProcess.transformOps { + if case .decodeImage(let order) = op { return order } + } + return .bgr + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ModelConfig.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ModelConfig.swift new file mode 100644 index 0000000000000000000000000000000000000000..8242098cbdb9d7d85fca13c696553d771d9fd52c --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ModelConfig.swift @@ -0,0 +1,107 @@ +// Copyright (c) 2026 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. + +import Foundation + +/// Resolves exported ONNX or ORT-format models and their **model config files** in the app bundle (`Models/det`, `Models/rec`). +/// Resource lookup uses the conventional shared basename for the weight file and the model config file in each folder. +/// Weights: prefers `inference*.ort` in the model directory (see ``bundledOrtPath``), otherwise `inference.onnx`. +struct ModelConfig { + let modelPath: String + let configPath: String + /// From `Global.model_name` in the model config file (same directory as the ONNX weights). + let name: String + + /// Human-readable label from `Global.model_name` in the model config file; `fallback` if the file cannot be read. + private static func displayName(fromModelConfigPath configPath: String, fallback: String) -> String { + guard let cfg = try? InferenceConfig.load(from: configPath) else { + return fallback + } + return cfg.modelName + } + + /// Bundled model weights: any `inference*.ort` in the subdirectory (``ortBundledFileMatchOrder``), else `inference.onnx`. + /// + /// ONNX Runtime’s `convert_onnx_models_to_ort` may emit `inference.with_runtime_opt.ort` instead of `inference.ort` + /// when using Runtime optimization style; the bundle cannot resolve that with `forResource: "inference"`. + private static let ortResourceStems: [String] = [ + "inference", + "inference.with_runtime_opt", + ] + + private static func bundledOrtPath(inDirectory directory: String) -> String? { + for stem in ortResourceStems { + if let p = Bundle.main.path(forResource: stem, ofType: "ort", inDirectory: directory) { + return p + } + } + // Other ORT export suffixes (e.g. non-default optim levels): one `inference*.ort` in the folder. + guard let urls = Bundle.main.urls(forResourcesWithExtension: "ort", subdirectory: directory) else { + return nil + } + let matches = urls.filter { url in + let c = url.lastPathComponent + return c.hasPrefix("inference") && c.hasSuffix(".ort") + } + guard !matches.isEmpty else { return nil } + if matches.count == 1 { return matches[0].path } + // If multiple, prefer lexicographic first (deterministic); avoid shipping several exports in one dir. + return matches.sorted { $0.lastPathComponent < $1.lastPathComponent }.first?.path + } + + private static func bundledWeightsPath(inDirectory directory: String, notFoundMessage: String) throws -> String { + if let ort = bundledOrtPath(inDirectory: directory) { + return ort + } + if let onnx = Bundle.main.path(forResource: "inference", ofType: "onnx", inDirectory: directory) { + return onnx + } + throw ModelConfigError.modelNotFound(notFoundMessage) + } + + static func detection() throws -> ModelConfig { + let modelPath = try Self.bundledWeightsPath( + inDirectory: "Models/det", + notFoundMessage: "det/inference.ort or det/inference.onnx" + ) + guard let configPath = Bundle.main.path(forResource: "inference", ofType: "yml", inDirectory: "Models/det") else { + throw ModelConfigError.modelNotFound("det model config file") + } + let name = displayName(fromModelConfigPath: configPath, fallback: "text_detection") + return ModelConfig(modelPath: modelPath, configPath: configPath, name: name) + } + + static func recognition() throws -> ModelConfig { + let modelPath = try Self.bundledWeightsPath( + inDirectory: "Models/rec", + notFoundMessage: "rec/inference.ort or rec/inference.onnx" + ) + guard let configPath = Bundle.main.path(forResource: "inference", ofType: "yml", inDirectory: "Models/rec") else { + throw ModelConfigError.modelNotFound("rec model config file") + } + let name = displayName(fromModelConfigPath: configPath, fallback: "text_recognition") + return ModelConfig(modelPath: modelPath, configPath: configPath, name: name) + } +} + +enum ModelConfigError: LocalizedError { + case modelNotFound(String) + + var errorDescription: String? { + switch self { + case .modelNotFound(let path): + return "Model file not found in bundle: \(path)" + } + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OCREngine.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OCREngine.swift new file mode 100644 index 0000000000000000000000000000000000000000..ec6cee768b835d1320c5c5caa688741169edf618 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OCREngine.swift @@ -0,0 +1,293 @@ +// Copyright (c) 2026 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. + +import CoreGraphics +import Foundation + +// MARK: - OCR run result types + +/// A single OCR result: one detected text region with its recognized text. +struct OCRResult { + /// Four corner points of the bounding polygon [x, y] in original image coordinates. + /// Order: top-left, top-right, bottom-right, bottom-left. + let polygon: [[Int32]] + /// The recognized text string from CTC decoding. + let text: String + /// Recognition confidence score (0.0 to 1.0). + let confidence: Float +} + +/// Result of one full OCR run on an image, with per-stage timing. +struct OCRRunResult { + /// All detected and recognized text regions, in reading order. + let results: [OCRResult] + /// Total time spent in the detection stage (preprocess + inference + postprocess). + let detectionTime: TimeInterval + /// Seconds in detection preprocessing (resize, normalize, HWC-to-CHW). + let detectionPreprocessTime: TimeInterval + /// Seconds in detection ONNX inference. + let detectionInferenceTime: TimeInterval + /// Seconds in detection postprocessing (DB map, contours, polygons). + let detectionPostprocessTime: TimeInterval + /// Total time spent recognizing all text regions (sum of per-line totals). + let recognitionTime: TimeInterval + /// Sum of recognition preprocess time across all lines (batched work split per line). + let recognitionPreprocessTime: TimeInterval + /// Sum of recognition inference time across all lines. + let recognitionInferenceTime: TimeInterval + /// Sum of recognition postprocess (CTC decode) time across all lines. + let recognitionPostprocessTime: TimeInterval + /// Wall-clock time for the entire run (detect + sort + crop + recognize). + let totalTime: TimeInterval + /// Time not attributed to detection or recognition totals. + let pipelineOverheadTime: TimeInterval + /// Number of text lines from detection sent through recognition. + let recognitionLineCount: Int + /// Detection model input tensor shape for this OCR run, e.g. [1, 3, H, W]. + let detectionInputTensorShape: [Int] + /// Recognition model input tensor shapes for each non-empty recognition batch. + let recognitionInputTensorShapes: [[Int]] + /// Per-line recognition ONNX inference times in **seconds**. + let lineRecognitionInferenceTimes: [TimeInterval] + /// Per-line recognition preprocess times in **seconds**. + let lineRecognitionPreprocessTimes: [TimeInterval] + /// Per-line recognition postprocess times in **seconds**. + let lineRecognitionPostprocessTimes: [TimeInterval] +} + +// MARK: - OCR Engine Errors + +enum OCREngineError: LocalizedError { + case quadTextCropFailed(boxIndex: Int, underlying: Error) + case recognitionBatchSizeMismatch(expected: Int, actual: Int) + + var errorDescription: String? { + switch self { + case .quadTextCropFailed(let idx, let err): + return "Quad text crop failed for box \(idx): \(err.localizedDescription)" + case .recognitionBatchSizeMismatch(let expected, let actual): + return "Recognition returned \(actual) results but \(expected) crops were sent" + } + } +} + +// MARK: - OCREngine + +/// End-to-end OCR: detect → sort → crop → recognize. +/// +/// Composes `DetectionEngine`, `BoxSorter`, `QuadTextCrop`, and `RecognitionEngine` +/// into a single `run(CGImage)` call. +/// +/// Runs entirely via async/await. Since `DetectionEngine` and +/// `RecognitionEngine` delegate to `ORTSessionManager` (a Swift actor), all ORT +/// calls are off the main thread. +/// +/// Usage: +/// ```swift +/// let manager = ORTSessionManager() +/// try await manager.loadModels() +/// let engine = try OCREngine(sessionManager: manager) +/// let result = try await engine.run(cgImage, params: .noOverrides) +/// for item in result.results { +/// print("\(item.text) (\(item.confidence))") +/// } +/// ``` +class OCREngine { + private let detectionEngine: DetectionEngine + private let recognitionEngine: RecognitionEngine + + /// Initialize with an existing ORTSessionManager (models must already be loaded). + /// + /// Creates both DetectionEngine and RecognitionEngine. + /// + /// - Parameter sessionManager: A loaded ORTSessionManager. + /// - Throws: If either engine's model config cannot be loaded. + init(sessionManager: ORTSessionManager) throws { + self.detectionEngine = try DetectionEngine(sessionManager: sessionManager) + self.recognitionEngine = try RecognitionEngine(sessionManager: sessionManager) + } + + func baselineRuntimeDefaults() -> ResolvedOCRRuntimeParams { + ResolvedOCRRuntimeParams.fromModelConfig(detectionEngine.modelConfig) + } + + /// Run full OCR on an image. + /// + /// End-to-end OCR flow (detect → sort → crop → recognize): + /// 1. **Detect**: Run detection to get bounding polygons + /// 2. **Sort**: Sort boxes in reading order (top-to-bottom, left-to-right) + /// 3. **Crop + Recognize**: Crop each region, order crops by ascending width/height for batched + /// recognition, then map results back to reading order + /// + /// - Parameters: + /// - image: The input `CGImage` to process. + /// - params: Optional runtime parameter overrides (see ``OCRRuntimeParams``). + /// - Returns: `OCRRunResult` with all line results and timing. + func run(_ image: CGImage, params: OCRRuntimeParams = .noOverrides) async throws -> OCRRunResult { + let runStart = CFAbsoluteTimeGetCurrent() + + let resolved = params.resolved(detectionEngine.modelConfig) + + let detResult = try await detectionEngine.detect(image, runtimeParams: params) + let sortedBoxes = BoxSorter.sortInReadingOrder(detResult.boxes) + let (ocrResults, recTiming, perLine) = try await recognizeSortedBoxes( + sortedBoxes, + sourceImage: image, + resolved: resolved + ) + + let totalTime = CFAbsoluteTimeGetCurrent() - runStart + let overhead = totalTime - detResult.totalTime - recTiming.total + + return OCRRunResult( + results: ocrResults, + detectionTime: detResult.totalTime, + detectionPreprocessTime: detResult.preprocessTime, + detectionInferenceTime: detResult.inferenceTime, + detectionPostprocessTime: detResult.postprocessTime, + recognitionTime: recTiming.total, + recognitionPreprocessTime: recTiming.preprocess, + recognitionInferenceTime: recTiming.inference, + recognitionPostprocessTime: recTiming.postprocess, + totalTime: totalTime, + pipelineOverheadTime: max(0, overhead), + recognitionLineCount: perLine.count, + detectionInputTensorShape: detResult.inputTensorShape, + recognitionInputTensorShapes: perLine.inputTensorShapes, + lineRecognitionInferenceTimes: perLine.inference, + lineRecognitionPreprocessTimes: perLine.preprocess, + lineRecognitionPostprocessTimes: perLine.postprocess + ) + } + + /// Aggregated recognition timing across all lines (sums of per-line split batch times). + private struct RecognitionTimingAggregate { + let preprocess: TimeInterval + let inference: TimeInterval + let postprocess: TimeInterval + let total: TimeInterval + } + + /// Per-line rec timings + private struct PerLineRecognitionTimes { + let count: Int + let inputTensorShapes: [[Int]] + let inference: [TimeInterval] + let preprocess: [TimeInterval] + let postprocess: [TimeInterval] + } + + private func recognizeSortedBoxes( + _ sortedBoxes: [DetectionBox], + sourceImage: CGImage, + resolved: ResolvedOCRRuntimeParams + ) async throws -> ([OCRResult], RecognitionTimingAggregate, PerLineRecognitionTimes) { + struct LineCrop { + let lineIndex: Int + let aspectRatio: Float + let crop: CGImage + let polygon: [[Int32]] + } + + var lines: [LineCrop] = [] + lines.reserveCapacity(sortedBoxes.count) + + for (index, box) in sortedBoxes.enumerated() { + let croppedImage: CGImage + do { + croppedImage = try QuadTextCrop.crop(sourceImage, polygon: box.points) + } catch { + throw OCREngineError.quadTextCropFailed(boxIndex: index, underlying: error) + } + let h = max(croppedImage.height, 1) + let aspect = Float(croppedImage.width) / Float(h) + lines.append(LineCrop(lineIndex: index, aspectRatio: aspect, crop: croppedImage, polygon: box.points)) + } + + let sortedForInference = lines.sorted { a, b in + if a.aspectRatio != b.aspectRatio { + return a.aspectRatio < b.aspectRatio + } + return a.lineIndex < b.lineIndex + } + + let batchSize = max(1, resolved.textRecBatchSize) + var perLine: [RecognitionEngineResult?] = Array(repeating: nil, count: lines.count) + var batchInputShapes: [[Int]] = [] + + var chunkStart = 0 + while chunkStart < sortedForInference.count { + let chunkEnd = min(chunkStart + batchSize, sortedForInference.count) + let chunk = Array(sortedForInference[chunkStart.. ResolvedOCRRuntimeParams { + let base = ResolvedOCRRuntimeParams.fromModelConfig(modelConfig) + return ResolvedOCRRuntimeParams( + textDetLimitSideLen: textDetLimitSideLen ?? base.textDetLimitSideLen, + textDetLimitType: textDetLimitType ?? base.textDetLimitType, + textDetMaxSideLimit: textDetMaxSideLimit ?? base.textDetMaxSideLimit, + textDetThresh: textDetThresh ?? base.textDetThresh, + textDetBoxThresh: textDetBoxThresh ?? base.textDetBoxThresh, + textDetUnclipRatio: textDetUnclipRatio ?? base.textDetUnclipRatio, + textRecBatchSize: textRecBatchSize ?? base.textRecBatchSize, + textRecScoreThresh: textRecScoreThresh ?? base.textRecScoreThresh + ) + } +} + +extension ResolvedOCRRuntimeParams { + /// Baseline with no UI overrides, using ``OCRParameterResolver`` app-tier defaults. + static func fromModelConfig(_ modelConfig: InferenceConfig) -> ResolvedOCRRuntimeParams { + let o = OCRParameterResolver.effective(modelConfig: modelConfig, runtime: .noOverrides) + return ResolvedOCRRuntimeParams( + textDetLimitSideLen: o.mergedLimitSideLen, + textDetLimitType: o.mergedLimitType, + textDetMaxSideLimit: o.mergedMaxSideLimit, + textDetThresh: o.detThresh, + textDetBoxThresh: o.detBoxThresh, + textDetUnclipRatio: o.detUnclipRatio, + textRecBatchSize: o.textRecBatchSize, + textRecScoreThresh: o.recScoreThresh + ) + } +} + +// MARK: - Effective parameters (single merge pass) + +struct EffectiveOCRParams: Equatable, Sendable { + // Detection — preprocess + var detResize: DetResizeParams + var mergedLimitSideLen: Int + var mergedLimitType: String + var mergedMaxSideLimit: Int + // Detection — postprocess + var detThresh: Float + var detBoxThresh: Float + var detUnclipRatio: Float + // Recognition + var textRecBatchSize: Int + var recScoreThresh: Float +} + +// MARK: - Resolver + +enum OCRParameterResolver { + + // MARK: App tier + + // Detection — preprocess + static let textDetLimitSideLenAppDefault: Int = 64 + static let textDetLimitTypeAppDefault: String = "min" + static let textDetMaxSideLimitAppDefault: Int = 4000 + // Detection — postprocess + static let textDetThreshAppDefault: Float = 0.3 + static let textDetBoxThreshAppDefault: Float = 0.6 + static let textDetUnclipRatioAppDefault: Float = 1.5 + // Recognition + static let textRecBatchSizeAppDefault: Int = 1 + static let textRecScoreThreshAppDefault: Float = 0.0 + + // MARK: Merge helpers + + /// UI override if set; otherwise the app-tier default. + private static func mergeFloat(ui: Float?, app: Float) -> Float { + ui ?? app + } + + private static func mergeInt(ui: Int?, app: Int) -> Int { + ui ?? app + } + + private static func mergeString(ui: String?, app: String) -> String { + ui ?? app + } + + private static func mergedLimitSideFields(runtime: OCRRuntimeParams) -> ( + sideLen: Int, limitType: String, maxSide: Int + ) { + let sideLen = mergeInt( + ui: runtime.textDetLimitSideLen, + app: Self.textDetLimitSideLenAppDefault + ) + let limitType = mergeString( + ui: runtime.textDetLimitType, + app: Self.textDetLimitTypeAppDefault + ) + let maxSide = mergeInt( + ui: runtime.textDetMaxSideLimit, + app: Self.textDetMaxSideLimitAppDefault + ) + return (sideLen, limitType, maxSide) + } + + private static func limitSideParams(runtime: OCRRuntimeParams) -> DetResizeParams { + let m = mergedLimitSideFields(runtime: runtime) + return DetResizeParams( + limitSideLen: m.sideLen, + limitType: m.limitType, + maxSideLimit: m.maxSide + ) + } + + /// Merges ``OCRRuntimeParams`` with app-tier defaults (UI → app). The `modelConfig` argument is + /// unused at this moment. + static func effective(modelConfig _: InferenceConfig, runtime: OCRRuntimeParams) -> EffectiveOCRParams { + let detResize = limitSideParams(runtime: runtime) + let merged = mergedLimitSideFields(runtime: runtime) + + let detThresh = mergeFloat( + ui: runtime.textDetThresh, + app: Self.textDetThreshAppDefault + ) + let detBoxThresh = mergeFloat( + ui: runtime.textDetBoxThresh, + app: Self.textDetBoxThreshAppDefault + ) + let detUnclipRatio = mergeFloat( + ui: runtime.textDetUnclipRatio, + app: Self.textDetUnclipRatioAppDefault + ) + + let textRecBatchSize = mergeInt( + ui: runtime.textRecBatchSize, + app: Self.textRecBatchSizeAppDefault + ) + let recScoreThresh = runtime.textRecScoreThresh ?? Self.textRecScoreThreshAppDefault + + return EffectiveOCRParams( + detResize: detResize, + mergedLimitSideLen: merged.sideLen, + mergedLimitType: merged.limitType, + mergedMaxSideLimit: merged.maxSide, + detThresh: detThresh, + detBoxThresh: detBoxThresh, + detUnclipRatio: detUnclipRatio, + textRecBatchSize: textRecBatchSize, + recScoreThresh: recScoreThresh + ) + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTProfilingBridge.h b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTProfilingBridge.h new file mode 100644 index 0000000000000000000000000000000000000000..f52d78a41854c9c6fae8bdcb0c2448ed73b309c9 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTProfilingBridge.h @@ -0,0 +1,45 @@ +// Copyright (c) 2026 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. + +#import + +@class ORTSession; +@class ORTSessionOptions; + +NS_ASSUME_NONNULL_BEGIN + +/// Bridges ONNX Runtime C++ profiling APIs that are not exposed on +/// `onnxruntime-objc`'s public surface. +/// +/// - Warning: `endProfilingOnSession` reads the private `_session` ivar on +/// `ORTSession` (layout depends on the +/// installed `onnxruntime-objc` version). Re-verify after pod upgrades. +@interface ORTProfilingBridge : NSObject + ++ (BOOL)enableProfilingOnSessionOptions:(ORTSessionOptions *)options + pathPrefix:(NSString *)pathPrefix + error:(NSError *_Nullable __autoreleasing *) + error + NS_SWIFT_NAME(enableProfiling(sessionOptions:pathPrefix:)); + +/// Ends profiling and returns the path to the written JSON (see ORT +/// `Session::EndProfilingAllocated`). ++ (nullable NSString *) + endProfilingOnSession:(ORTSession *)session + error:(NSError *_Nullable __autoreleasing *)error + NS_SWIFT_NAME(endProfiling(session:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTProfilingBridge.mm b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTProfilingBridge.mm new file mode 100644 index 0000000000000000000000000000000000000000..046857d6b09da74fbcbe0a5a3f8f7052d0ee1da6 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTProfilingBridge.mm @@ -0,0 +1,72 @@ +// Copyright (c) 2026 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. + +#import "ORTProfilingBridge.h" + +#import "ort_session_internal.h" + +#import "cxx_api.h" +#import "error_utils.h" + +#import + +#include + +@implementation ORTProfilingBridge + ++ (BOOL)enableProfilingOnSessionOptions:(ORTSessionOptions *)options + pathPrefix:(NSString *)pathPrefix + error:(NSError **)error { + if (options == nil || pathPrefix.length == 0) { + ORTSaveCodeAndDescriptionToError(ORT_INVALID_ARGUMENT, @"ORTProfilingBridge: options and non-empty pathPrefix required", error); + return NO; + } + try { + [options CXXAPIOrtSessionOptions].EnableProfiling(pathPrefix.fileSystemRepresentation); + return YES; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error); +} + ++ (NSString *)endProfilingOnSession:(ORTSession *)session error:(NSError **)error { + if (session == nil) { + ORTSaveCodeAndDescriptionToError(ORT_INVALID_ARGUMENT, @"ORTProfilingBridge: session is nil", error); + return nil; + } + try { + Ivar ivar = class_getInstanceVariable([ORTSession class], "_session"); + if (ivar == nullptr) { + ORTSaveCodeAndDescriptionToError(ORT_FAIL, + @"ORTProfilingBridge: ORTSession has no _session ivar — onnxruntime-objc mismatch?", + error); + return nil; + } + uintptr_t base = reinterpret_cast(session); + auto *opt = reinterpret_cast *>(base + ivar_getOffset(ivar)); + if (opt == nullptr || !opt->has_value()) { + ORTSaveCodeAndDescriptionToError(ORT_FAIL, @"ORTProfilingBridge: ORTSession is not ready", error); + return nil; + } + Ort::AllocatorWithDefaultOptions allocator; + Ort::AllocatedStringPtr path = opt->value().EndProfilingAllocated(allocator); + const char *cstr = path.get(); + if (cstr == nullptr) { + return nil; + } + return [NSString stringWithUTF8String:cstr]; + } + ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error); +} + +@end diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTSessionManager.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTSessionManager.swift new file mode 100644 index 0000000000000000000000000000000000000000..842e5ada2ba3bfd7c75af60c727a6da10b6d5155 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTSessionManager.swift @@ -0,0 +1,338 @@ +// Copyright (c) 2026 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. + +import Foundation + +/// Preset for which ONNX Runtime execution provider is registered first on the session chain in this demo. +enum ORTPrimaryExecutionProvider: String, CaseIterable, Identifiable, Sendable { + /// Default CPU execution. Default for this demo. + case cpu = "cpu" + /// Registers the XNNPACK EP; nodes it cannot run still use the CPU EP. + case xnnpack = "xnnpack" + /// Registers the Core ML EP; remaining subgraphs use other registered EPs (commonly CPU). + case coreML = "core_ml" + + var id: String { rawValue } + + var displayTitle: String { + switch self { + case .cpu: return "CPU" + case .xnnpack: return "XNNPACK" + case .coreML: return "Core ML" + } + } +} + +enum ORTCoreMLProviderOption: String, CaseIterable, Identifiable, Sendable { + case enableOnSubgraphs + case createMLProgram + case staticInputShapes + case cpuOnly + case cpuAndGPU + case aneOnly + + var id: String { rawValue } + + var displayTitle: String { + switch self { + case .enableOnSubgraphs: return "Enable on subgraphs" + case .createMLProgram: return "Use ML program" + case .staticInputShapes: return "Fixed tensor shapes only" + case .cpuOnly: return "Core ML CPU only" + case .cpuAndGPU: return "Core ML CPU and GPU" + case .aneOnly: return "Only on devices with ANE" + } + } + + var detailCaption: String { + switch self { + case .enableOnSubgraphs: return "Compile subgraphs independently for Core ML." + case .createMLProgram: return "Prefer MLProgram over NeuralNetwork (when supported)." + case .staticInputShapes: + return "Core ML tries to run only ops whose input tensors have fixed shapes at inference time; parts with varying dimensions typically stay on the CPU EP instead." + case .cpuOnly: return "Forces Core ML compute on CPU. Mutually exclusive with CPU and GPU." + case .cpuAndGPU: return "Allows Core ML to use CPU and GPU. Mutually exclusive with CPU only." + case .aneOnly: return "Only enable Core ML EP on devices that have Apple Neural Engine hardware." + } + } +} + +enum ORTSessionManagerError: LocalizedError { + case modelNotFound(String) + case sessionCreationFailed(String) + case inferenceFailed(String) + case inputTensorElementCountMismatch(expected: Int, actual: Int, modelName: String) + case outputContainsNaN(String) + case ortProfilingFailed(String) + + var errorDescription: String? { + switch self { + case .modelNotFound(let name): return "Model not found: \(name)" + case .sessionCreationFailed(let detail): return "Session creation failed: \(detail)" + case .inferenceFailed(let detail): return "Inference failed: \(detail)" + case .inputTensorElementCountMismatch(let expected, let actual, let modelName): + return "\(modelName): input tensor size mismatch (expected \(expected) floats, got \(actual))" + case .outputContainsNaN(let name): return "Output tensor '\(name)' contains NaN values" + case .ortProfilingFailed(let detail): return "ONNX Runtime profiling: \(detail)" + } + } +} + +/// File URLs of ONNX Runtime JSON profiles after ``ORTSessionManager/finalizeORTProfiling()`` (separate file per sub-model). +struct ORTProfilingOutput: Sendable { + var detectionProfileJSON: URL + var recognitionProfileJSON: URL +} + +/// Tuning options for ORT session creation. +struct ORTSessionTuningOptions: Sendable { + /// Session-level intra-op thread count. 0 = ORT default. + var intraOpThreads: Int = 0 + /// XNNPACK EP thread count. 0 = ORT default. + var xnnpackThreads: Int = 0 + /// CoreML EP flags: enableOnSubgraphs, createMLProgram, staticInputShapes, cpuOnly, cpuAndGPU, aneOnly. + var coreMLFlags: Set = [] + + static let `default` = ORTSessionTuningOptions() +} + +extension ORTSessionTuningOptions: Equatable {} + +/// At most one placement-related Core ML EP option is applied. Priority matches +/// ``OnnxRuntimeSettingsPanel`` when several keys appear (e.g. benchmark env / JSON). +private func applyCoreMLPlacementFlags(from flags: Set, to coreml: ORTCoreMLExecutionProviderOptions) { + if flags.contains(ORTCoreMLProviderOption.cpuOnly.rawValue) { + coreml.useCPUOnly = true + } else if flags.contains(ORTCoreMLProviderOption.cpuAndGPU.rawValue) { + coreml.useCPUAndGPU = true + } else if flags.contains(ORTCoreMLProviderOption.aneOnly.rawValue) { + coreml.onlyEnableForDevicesWithANE = true + } +} + +actor ORTSessionManager { + private struct SessionIONames { + let inputName: String + let outputNames: Set + } + + private var env: ORTEnv? + private var detSession: ORTSession? + private var recSession: ORTSession? + private var detIO: SessionIONames? + private var recIO: SessionIONames? + private var ortProfilingPendingFinalize: Bool = false + + /// Load both models. Creates one ORTEnv (per ORT docs: one per process), + /// configures execution providers per ``ORTPrimaryExecutionProvider``, + /// and creates sessions for detection and recognition models. + /// + /// - Parameter ortProfiling: When `true`, enables ONNX Runtime session profiling (written as JSON on finalize). + /// Profiling adds overhead; do not treat wall-clock times from the same run as a clean latency benchmark. + /// - Parameter tuning: Session tuning options (threads, EP flags). Default reads from environment. + func loadModels(executionProvider: ORTPrimaryExecutionProvider = .cpu, ortProfiling: Bool = false, tuning: ORTSessionTuningOptions = .default) async throws { + let env = try ORTEnv(loggingLevel: .warning) + self.env = env + ortProfilingPendingFinalize = ortProfiling + + let detConfig = try ModelConfig.detection() + let recConfig = try ModelConfig.recognition() + + if ortProfiling { + let baseDir = try ortProfilingDirectoryURL() + let detPrefix = baseDir.appendingPathComponent("paddle_ort_det", isDirectory: false).path + let recPrefix = baseDir.appendingPathComponent("paddle_ort_rec", isDirectory: false).path + let detOptions = try makeSessionOptions(executionProvider: executionProvider, ortProfilePathPrefix: detPrefix, tuning: tuning) + let recOptions = try makeSessionOptions(executionProvider: executionProvider, ortProfilePathPrefix: recPrefix, tuning: tuning) + detSession = try ORTSession(env: env, modelPath: detConfig.modelPath, sessionOptions: detOptions) + recSession = try ORTSession(env: env, modelPath: recConfig.modelPath, sessionOptions: recOptions) + } else { + let options = try makeSessionOptions(executionProvider: executionProvider, ortProfilePathPrefix: nil, tuning: tuning) + detSession = try ORTSession(env: env, modelPath: detConfig.modelPath, sessionOptions: options) + recSession = try ORTSession(env: env, modelPath: recConfig.modelPath, sessionOptions: options) + } + if let detSession { + detIO = try makeSessionIONames(session: detSession, modelName: "det") + } + if let recSession { + recIO = try makeSessionIONames(session: recSession, modelName: "rec") + } + } + + /// Flushes ORT profiling and returns the JSON file paths. Call once after the last inference when + /// ``loadModels(ortProfiling:)`` was `true`. Safe to call when profiling was disabled (returns `nil`). + func finalizeORTProfiling() throws -> ORTProfilingOutput? { + guard ortProfilingPendingFinalize else { return nil } + ortProfilingPendingFinalize = false + guard let det = detSession, let rec = recSession else { + throw ORTSessionManagerError.sessionCreationFailed("Cannot finalize ORT profiling: session missing") + } + let detPath = try ORTProfilingBridge.endProfiling(session: det) + let recPath = try ORTProfilingBridge.endProfiling(session: rec) + if detPath.isEmpty || recPath.isEmpty { + throw ORTSessionManagerError.ortProfilingFailed("EndProfiling returned an empty path") + } + return ORTProfilingOutput( + detectionProfileJSON: URL(fileURLWithPath: detPath), + recognitionProfileJSON: URL(fileURLWithPath: recPath) + ) + } + + private func ortProfilingDirectoryURL() throws -> URL { + let base = try FileManager.default.url( + for: .cachesDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) + let dir = base.appendingPathComponent("PaddleOCRORTProfiling", isDirectory: true) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + return dir + } + + private func makeSessionOptions(executionProvider: ORTPrimaryExecutionProvider, ortProfilePathPrefix: String?, tuning: ORTSessionTuningOptions) throws -> ORTSessionOptions { + let options = try ORTSessionOptions() + try options.setGraphOptimizationLevel(.all) + if tuning.intraOpThreads > 0 { + try options.setIntraOpNumThreads(Int32(tuning.intraOpThreads)) + } + if let prefix = ortProfilePathPrefix { + try ORTProfilingBridge.enableProfiling(sessionOptions: options, pathPrefix: prefix) + } + switch executionProvider { + case .cpu: + break + case .xnnpack: + let xnnpackOptions = ORTXnnpackExecutionProviderOptions() + if tuning.xnnpackThreads > 0 { + xnnpackOptions.intra_op_num_threads = Int32(tuning.xnnpackThreads) + } + try options.appendXnnpackExecutionProvider(with: xnnpackOptions) + case .coreML: + let coremlOptions = ORTCoreMLExecutionProviderOptions() + let flags = tuning.coreMLFlags + if flags.contains("enableOnSubgraphs") { coremlOptions.enableOnSubgraphs = true } + if flags.contains("createMLProgram") { coremlOptions.createMLProgram = true } + if flags.contains("staticInputShapes") { coremlOptions.onlyAllowStaticInputShapes = true } + applyCoreMLPlacementFlags(from: flags, to: coremlOptions) + try options.appendCoreMLExecutionProvider(with: coremlOptions) + } + return options + } + + private func makeSessionIONames(session: ORTSession, modelName: String) throws -> SessionIONames { + let inputNames = try session.inputNames() + let outputNamesList = try session.outputNames() + guard let firstInputName = inputNames.first else { + throw ORTSessionManagerError.inferenceFailed("\(modelName): no input names found") + } + return SessionIONames(inputName: firstInputName, outputNames: Set(outputNamesList)) + } + + /// Run detection inference with real preprocessed input data. + /// + /// - Parameters: + /// - inputData: Float32 array in CHW layout; length must equal the product of `shape`. + /// - shape: Tensor shape, e.g. [1, 3, 896, 960]. + /// - Returns: Dictionary mapping output name to (data as [Float], shape as [Int]). + func runDetection(inputData: [Float], shape: [Int]) async throws -> [String: (data: [Float], shape: [Int])] { + guard let session = detSession else { + throw ORTSessionManagerError.sessionCreationFailed("Detection session not loaded") + } + guard let io = detIO else { + throw ORTSessionManagerError.sessionCreationFailed("Detection session IO names not loaded") + } + return try runInference(session: session, io: io, modelName: "det", inputData: inputData, shape: shape) + } + + /// Run recognition inference with preprocessed input data. + /// + /// The recognition model accepts dynamic batch and width: shape `[N, 3, H, W]` with `N ≥ 1`, + /// fixed `H` (e.g. 48), and `W` shared after padding within each batch. A single line uses `N = 1`. + /// + /// - Parameters: + /// - inputData: Float32 row-major tensor; length must equal the product of `shape`. + /// - shape: e.g. `[1, 3, 48, 320]` or `[6, 3, 48, 640]` for a batch of six lines. + /// - Returns: Dictionary mapping output name to (data as [Float], shape as [Int]). + func runRecognition(inputData: [Float], shape: [Int]) async throws -> [String: (data: [Float], shape: [Int])] { + guard let session = recSession else { + throw ORTSessionManagerError.sessionCreationFailed("Recognition session not loaded") + } + guard let io = recIO else { + throw ORTSessionManagerError.sessionCreationFailed("Recognition session IO names not loaded") + } + return try runInference(session: session, io: io, modelName: "rec", inputData: inputData, shape: shape) + } + + /// Shared inference logic used by both detection and recognition. + /// + /// Discovers input/output names at runtime, creates a float32 tensor from the + /// provided data and shape, runs the session, extracts output arrays, and + /// validates that no output contains NaN values. + private func runInference( + session: ORTSession, + io: SessionIONames, + modelName: String, + inputData: [Float], + shape: [Int] + ) throws -> [String: (data: [Float], shape: [Int])] { + let expectedElements = shape.reduce(1, *) + guard expectedElements == inputData.count else { + throw ORTSessionManagerError.inputTensorElementCountMismatch( + expected: expectedElements, + actual: inputData.count, + modelName: modelName + ) + } + + // Create ORT tensor from input data + let nsShape = shape.map { NSNumber(value: $0) } + var data = inputData + let tensorData = NSMutableData( + bytes: &data, + length: inputData.count * MemoryLayout.stride + ) + let inputTensor = try ORTValue( + tensorData: tensorData, + elementType: .float, + shape: nsShape + ) + + let inputs: [String: ORTValue] = [io.inputName: inputTensor] + let outputs = try session.run( + withInputs: inputs, + outputNames: io.outputNames, + runOptions: nil + ) + + // Extract output tensors as Float arrays + var result: [String: (data: [Float], shape: [Int])] = [:] + for (name, value) in outputs { + let info = try value.tensorTypeAndShapeInfo() + let outputShape = info.shape.map { $0.intValue } + let outputData = try value.tensorData() as Data + let floats: [Float] = outputData.withUnsafeBytes { buffer in + Array(buffer.bindMemory(to: Float.self)) + } + + if floats.contains(where: \.isNaN) { + throw ORTSessionManagerError.outputContainsNaN(name) + } + + result[name] = (data: floats, shape: outputShape) + } + + return result + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVDBBridge.h b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVDBBridge.h new file mode 100644 index 0000000000000000000000000000000000000000..2ed51159549e581fb023ad1432973c11a83ab534 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVDBBridge.h @@ -0,0 +1,45 @@ +// Copyright (c) 2026 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. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PDBMiniBoxResult : NSObject +@property(nonatomic) NSArray *corners; +@property(nonatomic) float minSide; +@end + +@interface PDBOpenCVDBBridge : NSObject + ++ (NSArray *> *)findContoursWithBinaryMask:(NSData *)data + width:(NSInteger)width + height:(NSInteger)height + maxCandidates: + (NSInteger)maxCandidates + NS_SWIFT_NAME(findContours(binaryMask:width:height:maxCandidates:)); + ++ (nullable PDBMiniBoxResult *)miniBoxFromPoints:(NSArray *)points + NS_SWIFT_NAME(miniBox(fromPoints:)); + ++ (float)contourAreaFromPoints:(NSArray *)points + NS_SWIFT_NAME(contourArea(fromPoints:)); + ++ (float)arcLengthFromPoints:(NSArray *)points + closed:(BOOL)closed + NS_SWIFT_NAME(arcLength(fromPoints:closed:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVDBBridge.mm b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVDBBridge.mm new file mode 100644 index 0000000000000000000000000000000000000000..55cfcceabb032bf659f85431845a04c36bdca443 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVDBBridge.mm @@ -0,0 +1,147 @@ +// Copyright (c) 2026 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. + +#import "OpenCVDBBridge.h" +// Include imgproc before UIKit: Objective-C's `NO` macro breaks OpenCV enums named `NO` in opencv.hpp. +#import +#import +#import +#include + +@implementation PDBMiniBoxResult +@end + +@implementation PDBOpenCVDBBridge + ++ (NSArray *> *)findContoursWithBinaryMask:(NSData *)data + width:(NSInteger)width + height:(NSInteger)height + maxCandidates:(NSInteger)maxCandidates { + if (width <= 0 || height <= 0 || data.length < (NSUInteger)(width * height)) { + return @[]; + } + + const uint8_t *src = static_cast(data.bytes); + cv::Mat mat((int)height, (int)width, CV_8UC1); + for (int y = 0; y < (int)height; y++) { + uint8_t *row = mat.ptr(y); + for (int x = 0; x < (int)width; x++) { + row[x] = src[y * (int)width + x] ? 255 : 0; + } + } + + std::vector> contours; + cv::findContours(mat, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE); + + const NSInteger n = std::min((NSInteger)contours.size(), maxCandidates); + NSMutableArray *out = [NSMutableArray arrayWithCapacity:(NSUInteger)n]; + for (NSInteger i = 0; i < n; i++) { + const auto &c = contours[(size_t)i]; + NSMutableArray *pts = [NSMutableArray arrayWithCapacity:c.size()]; + for (const auto &pt : c) { + [pts addObject:[NSValue valueWithCGPoint:CGPointMake(pt.x, pt.y)]]; + } + [out addObject:pts]; + } + return out; +} + ++ (nullable PDBMiniBoxResult *)miniBoxFromPoints:(NSArray *)points { + if (points.count < 2) { + return nil; + } + + std::vector pts; + pts.reserve(points.count); + for (NSValue *v in points) { + CGPoint p = v.CGPointValue; + pts.emplace_back((float)p.x, (float)p.y); + } + + cv::RotatedRect rr = cv::minAreaRect(pts); + float w = rr.size.width; + float h = rr.size.height; + if (w <= 0 && h <= 0) { + return nil; + } + + cv::Point2f cvPts[4]; + rr.points(cvPts); + + // Stable sort by x only (min-area box corner order). + int ord[4] = {0, 1, 2, 3}; + std::stable_sort(ord, ord + 4, [&](int a, int b) { return cvPts[a].x < cvPts[b].x; }); + cv::Point2f sortedPts[4]; + for (int i = 0; i < 4; i++) { + sortedPts[i] = cvPts[ord[i]]; + } + + NSInteger index1, index2, index3, index4; + cv::Point2f p0 = sortedPts[0]; + cv::Point2f p1 = sortedPts[1]; + if (p1.y > p0.y) { + index1 = 0; + index4 = 1; + } else { + index1 = 1; + index4 = 0; + } + cv::Point2f p2 = sortedPts[2]; + cv::Point2f p3 = sortedPts[3]; + if (p3.y > p2.y) { + index2 = 2; + index3 = 3; + } else { + index2 = 3; + index3 = 2; + } + + PDBMiniBoxResult *res = [[PDBMiniBoxResult alloc] init]; + res.corners = @[ + [NSValue valueWithCGPoint:CGPointMake(sortedPts[index1].x, sortedPts[index1].y)], + [NSValue valueWithCGPoint:CGPointMake(sortedPts[index2].x, sortedPts[index2].y)], + [NSValue valueWithCGPoint:CGPointMake(sortedPts[index3].x, sortedPts[index3].y)], + [NSValue valueWithCGPoint:CGPointMake(sortedPts[index4].x, sortedPts[index4].y)], + ]; + res.minSide = std::min(w, h); + return res; +} + ++ (float)contourAreaFromPoints:(NSArray *)points { + if (points.count < 2) { + return 0; + } + std::vector pts; + pts.reserve(points.count); + for (NSValue *v in points) { + CGPoint p = v.CGPointValue; + pts.emplace_back((float)p.x, (float)p.y); + } + return cv::contourArea(pts); +} + ++ (float)arcLengthFromPoints:(NSArray *)points closed:(BOOL)closed { + if (points.count < 2) { + return 0; + } + std::vector pts; + pts.reserve(points.count); + for (NSValue *v in points) { + CGPoint p = v.CGPointValue; + pts.emplace_back((float)p.x, (float)p.y); + } + return (float)cv::arcLength(pts, closed); +} + +@end diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVImageBridge.h b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVImageBridge.h new file mode 100644 index 0000000000000000000000000000000000000000..d66dde7e3916d73c5d56f93b5111873f098a8138 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVImageBridge.h @@ -0,0 +1,63 @@ +// Copyright (c) 2026 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. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PDBPerspectiveCropOutput : NSObject +@property(nonatomic) NSData *rgbData; +@property(nonatomic) NSInteger width; +@property(nonatomic) NSInteger height; +@end + +@interface PDBOpenCVImageBridge : NSObject + +/// `cv::resize(..., INTER_LINEAR)` on 8-bit **3-channel** row-major data +/// (`CV_8UC3`; stores BGR channel order for OpenCV). ++ (NSData *)resizeRGBU8:(NSData *)data + srcWidth:(NSInteger)srcWidth + srcHeight:(NSInteger)srcHeight + dstWidth:(NSInteger)dstWidth + dstHeight:(NSInteger)dstHeight NS_SWIFT_NAME(resizeRGBU8(_:srcWidth:srcHeight:dstWidth:dstHeight:)); + +/// Pad RGB image to `padWidth` x `padHeight` with zeros on bottom/right. ++ (NSData *)padRGBU8:(NSData *)data + width:(NSInteger)width + height:(NSInteger)height + padWidth:(NSInteger)padWidth + padHeight:(NSInteger)padHeight + NS_SWIFT_NAME(padRGBU8(_:width:height:padWidth:padHeight:)); + +/// Minimum-area rectangle, corner ordering, perspective warp (`INTER_CUBIC`, +/// `BORDER_REPLICATE`), then 90° CCW rotation when height/width ≥ 1.5. Input +/// buffer is row-major **BGR**. ++ (nullable PDBPerspectiveCropOutput *) + quadTextLineCropBGR:(NSData *)src + srcWidth:(NSInteger)srcWidth + srcHeight:(NSInteger)srcHeight + quad:(NSArray *)quad + NS_SWIFT_NAME(quadTextLineCropBGR(_:srcWidth:srcHeight:quad:)); + +/// Mean of the float probability map inside the quadrilateral: `fillPoly` mask +/// then `cv::mean`. ++ (float)meanPredInQuad:(NSData *)pred + width:(NSInteger)width + height:(NSInteger)height + box:(NSArray *> *)box + NS_SWIFT_NAME(meanPredInQuad(_:width:height:box:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVImageBridge.mm b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVImageBridge.mm new file mode 100644 index 0000000000000000000000000000000000000000..d608aa73b499a86ca8cb6f047fa95fbaab67a306 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVImageBridge.mm @@ -0,0 +1,205 @@ +// Copyright (c) 2026 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. + +#import "OpenCVImageBridge.h" +#import +#import +#import +#import +#import +#import + +@implementation PDBPerspectiveCropOutput +@end + +@implementation PDBOpenCVImageBridge + ++ (NSData *)resizeRGBU8:(NSData *)data + srcWidth:(NSInteger)srcWidth + srcHeight:(NSInteger)srcHeight + dstWidth:(NSInteger)dstWidth + dstHeight:(NSInteger)dstHeight { + if (srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) { + return [NSData data]; + } + if (data.length < (NSUInteger)(srcWidth * srcHeight * 3)) { + return [NSData data]; + } + cv::Mat src((int)srcHeight, (int)srcWidth, CV_8UC3, (void *)data.bytes); + cv::Mat dst; + cv::resize(src, dst, cv::Size((int)dstWidth, (int)dstHeight), 0, 0, cv::INTER_LINEAR); + return [NSData dataWithBytes:dst.data length:(NSUInteger)(dst.rows * dst.cols * 3)]; +} + ++ (NSData *)padRGBU8:(NSData *)data + width:(NSInteger)width + height:(NSInteger)height + padWidth:(NSInteger)padWidth + padHeight:(NSInteger)padHeight { + if (width <= 0 || height <= 0 || padWidth < width || padHeight < height) { + return data ?: [NSData data]; + } + if (data.length < (NSUInteger)(width * height * 3)) { + return [NSData data]; + } + cv::Mat src((int)height, (int)width, CV_8UC3, (void *)data.bytes); + int bot = (int)(padHeight - height); + int right = (int)(padWidth - width); + cv::Mat dst; + cv::copyMakeBorder(src, dst, 0, bot, 0, right, cv::BORDER_CONSTANT, cv::Scalar(0, 0, 0)); + return [NSData dataWithBytes:dst.data length:(NSUInteger)(dst.rows * dst.cols * 3)]; +} + ++ (nullable PDBPerspectiveCropOutput *)quadTextLineCropBGR:(NSData *)src + srcWidth:(NSInteger)srcWidth + srcHeight:(NSInteger)srcHeight + quad:(NSArray *)quad { + if (srcWidth <= 0 || srcHeight <= 0) { + return nil; + } + if (quad.count != 4 || src.length < (NSUInteger)(srcWidth * srcHeight * 3)) { + return nil; + } + + cv::Mat im((int)srcHeight, (int)srcWidth, CV_8UC3, (void *)src.bytes); + + std::vector raw(4); + for (int i = 0; i < 4; i++) { + CGPoint p = [quad[i] CGPointValue]; + raw[i] = cv::Point2f((float)(int32_t)p.x, (float)(int32_t)p.y); + } + + cv::RotatedRect rr = cv::minAreaRect(raw); + cv::Point2f boxPts[4]; + rr.points(boxPts); + std::vector sorted(boxPts, boxPts + 4); + std::sort(sorted.begin(), sorted.end(), + [](const cv::Point2f &a, const cv::Point2f &b) { return a.x < b.x; }); + + int index_a, index_b, index_c, index_d; + if (sorted[1].y > sorted[0].y) { + index_a = 0; + index_d = 1; + } else { + index_a = 1; + index_d = 0; + } + if (sorted[3].y > sorted[2].y) { + index_b = 2; + index_c = 3; + } else { + index_b = 3; + index_c = 2; + } + + cv::Point2f srcPts[4] = {sorted[index_a], sorted[index_b], sorted[index_c], sorted[index_d]}; + + double w1 = cv::norm(srcPts[0] - srcPts[1]); + double w2 = cv::norm(srcPts[2] - srcPts[3]); + double h1 = cv::norm(srcPts[0] - srcPts[3]); + double h2 = cv::norm(srcPts[1] - srcPts[2]); + + int img_crop_width = (int)std::max(w1, w2); + int img_crop_height = (int)std::max(h1, h2); + if (img_crop_width <= 0 || img_crop_height <= 0) { + return nil; + } + + cv::Point2f dstPts[4] = { + cv::Point2f(0, 0), + cv::Point2f((float)img_crop_width, 0), + cv::Point2f((float)img_crop_width, (float)img_crop_height), + cv::Point2f(0, (float)img_crop_height), + }; + + cv::Mat M = cv::getPerspectiveTransform(srcPts, dstPts); + cv::Mat warped; + cv::warpPerspective(im, warped, M, cv::Size(img_crop_width, img_crop_height), cv::INTER_CUBIC, + cv::BORDER_REPLICATE); + + cv::Mat outMat; + double ratio = (double)warped.rows / (double)warped.cols; + if (ratio >= 1.5) { + cv::rotate(warped, outMat, cv::ROTATE_90_COUNTERCLOCKWISE); + } else { + outMat = warped; + } + + PDBPerspectiveCropOutput *result = [[PDBPerspectiveCropOutput alloc] init]; + result.rgbData = [NSData dataWithBytes:outMat.data length:(NSUInteger)(outMat.rows * outMat.cols * 3)]; + result.width = outMat.cols; + result.height = outMat.rows; + return result; +} + ++ (float)meanPredInQuad:(NSData *)pred + width:(NSInteger)width + height:(NSInteger)height + box:(NSArray *> *)box { + if (width <= 0 || height <= 0 || box.count != 4) { + return 0; + } + size_t need = (size_t)(width * height) * sizeof(float); + if (pred.length < need) { + return 0; + } + + float xminF = FLT_MAX, xmaxF = -FLT_MAX, yminF = FLT_MAX, ymaxF = -FLT_MAX; + for (NSArray *pt in box) { + if (pt.count < 2) { + return 0; + } + float x = [pt[0] floatValue]; + float y = [pt[1] floatValue]; + xminF = std::min(xminF, x); + xmaxF = std::max(xmaxF, x); + yminF = std::min(yminF, y); + ymaxF = std::max(ymaxF, y); + } + + int w = (int)width; + int h = (int)height; + // Clamp ROI to bitmap bounds before masking and mean (avoids out-of-range reads). + int xmin = std::max(0, std::min((int)std::floor(xminF), w - 1)); + int xmax = std::max(0, std::min((int)std::ceil(xmaxF), w - 1)); + int ymin = std::max(0, std::min((int)std::floor(yminF), h - 1)); + int ymax = std::max(0, std::min((int)std::ceil(ymaxF), h - 1)); + if (xmax < xmin || ymax < ymin) { + return 0; + } + + const float *p = (const float *)pred.bytes; + cv::Mat bitmap(h, w, CV_32FC1, (void *)const_cast(p)); + cv::Rect roi(xmin, ymin, xmax - xmin + 1, ymax - ymin + 1); + cv::Mat crop = bitmap(roi); + + cv::Mat mask = cv::Mat::zeros(crop.rows, crop.cols, CV_8UC1); + std::vector pts; + pts.reserve(4); + for (NSArray *pt in box) { + float fx = [pt[0] floatValue] - static_cast(xmin); + float fy = [pt[1] floatValue] - static_cast(ymin); + int px = static_cast(fx); + int py = static_cast(fy); + pts.push_back(cv::Point(px, py)); + } + std::vector> contours; + contours.push_back(pts); + cv::fillPoly(mask, contours, cv::Scalar(1)); + + cv::Scalar m = cv::mean(crop, mask); + return (float)m[0]; +} + +@end diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/PDBPolygonOffsetBridge.h b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/PDBPolygonOffsetBridge.h new file mode 100644 index 0000000000000000000000000000000000000000..d8967f114767ed67a5da5149867a0f7213097800 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/PDBPolygonOffsetBridge.h @@ -0,0 +1,29 @@ +// Copyright (c) 2026 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. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Polygon inflate (unclip) via Clipper 6.x (`ClipperLib`) for closed paths. +@interface PDBPolygonOffsetBridge : NSObject + ++ (NSArray *> *)inflateClosedPolygonWith: + (NSArray *)points + distance:(double)distance + NS_SWIFT_NAME(inflateClosedPolygon(with:distance:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/PDBPolygonOffsetBridge.mm b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/PDBPolygonOffsetBridge.mm new file mode 100644 index 0000000000000000000000000000000000000000..ff6933dc6f774c044f6ab4f7b7826e0cdb0ccd7f --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/PDBPolygonOffsetBridge.mm @@ -0,0 +1,49 @@ +// Copyright (c) 2026 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. + +#import "PDBPolygonOffsetBridge.h" +#import +#include +#include "clipper.hpp" +@implementation PDBPolygonOffsetBridge ++ (NSArray *> *)inflateClosedPolygonWith:(NSArray *)points + distance:(double)distance { + if (points.count < 2) { + return @[]; + } + using namespace ClipperLib; + // Default miter limit and arc tolerance for ClipperOffset (round join). + ClipperOffset co(2.0, 0.25); + Path path; + path.reserve(points.count); + for (NSValue *v in points) { + CGPoint p = v.CGPointValue; + cInt ix = static_cast(std::lround((double)p.x)); + cInt iy = static_cast(std::lround((double)p.y)); + path.push_back(IntPoint(ix, iy)); + } + co.AddPath(path, jtRound, etClosedPolygon); + Paths solution; + co.Execute(solution, distance); + if (solution.empty()) { + return @[]; + } + const Path &out = solution[0]; + NSMutableArray *ring = [NSMutableArray arrayWithCapacity:out.size()]; + for (const IntPoint &pt : out) { + [ring addObject:[NSValue valueWithCGPoint:CGPointMake((CGFloat)pt.X, (CGFloat)pt.Y)]]; + } + return @[ ring ]; +} +@end diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/Preprocessing.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/Preprocessing.swift new file mode 100644 index 0000000000000000000000000000000000000000..7e2358af63d2b2932167a128b159f8cd539cc64f --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/Preprocessing.swift @@ -0,0 +1,386 @@ +// Copyright (c) 2026 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. + +import CoreGraphics +import Foundation + +// MARK: - Errors + +enum DetPreprocessorError: LocalizedError { + case invalidImage + case configMissing(String) + case pixelExtractionFailed + + var errorDescription: String? { + switch self { + case .invalidImage: + return "Invalid input image: width or height is zero" + case .configMissing(let detail): + return "Required preprocessing config missing: \(detail)" + case .pixelExtractionFailed: + return "Failed to extract pixel data from image" + } + } +} + +// MARK: - Preprocessing Result + +/// The output of detection preprocessing: a float32 tensor and metadata needed for postprocessing. +struct PreprocessResult { + /// Flat CHW array of shape [1, 3, resizedH, resizedW]. + let tensorData: [Float] + /// Tensor dimensions: [1, 3, resizedH, resizedW]. + let tensorShape: [Int] + /// Original image dimensions before any resizing. + let originalSize: (width: Int, height: Int) + /// Image dimensions after resize and padding to stride multiples. + let resizedSize: (width: Int, height: Int) + /// Vertical resize ratio: resizedH / originalH. + let ratioH: Float + /// Horizontal resize ratio: resizedW / originalW. + let ratioW: Float +} + +// MARK: - DetPreprocessor + +/// Implements detection preprocessing: **DetResizeForTest** → +/// **NormalizeImage** (from the model config file) -> **ToCHWImage**. +/// +/// Rasterization uses CoreGraphics; resize/pad use OpenCV (`INTER_LINEAR`, `copyMakeBorder`). +/// HWC channel order follows `DecodeImage.img_mode` (`InferenceConfig.decodeImageChannelOrder`). +struct DetPreprocessor { + private enum ResizeRule { + case limitSide(len: Int, limitType: String, maxSideLimit: Int) + case longEdge(resizeLong: Int) + } + + private let scale: Float + private let mean: [Float] + private let std: [Float] + private let channelOrder: InferenceImageChannelOrder + + init(config: InferenceConfig) throws { + var foundScale: Float? + var foundMean: [Float]? + var foundStd: [Float]? + + for op in config.preProcess.transformOps { + switch op { + case .decodeImage: + break + case .detResizeForTest(_): + break + case .normalizeImage(let s, let m, let st, _): + foundScale = s + foundMean = m + foundStd = st + case .toCHWImage, .recResizeImg, .unknown: + break + } + } + + guard let scale = foundScale, let mean = foundMean, let std = foundStd else { + throw DetPreprocessorError.configMissing("NormalizeImage with scale, mean, std") + } + guard mean.count == 3, std.count == 3 else { + throw DetPreprocessorError.configMissing("NormalizeImage mean/std must have exactly 3 values") + } + + self.scale = scale + self.mean = mean + self.std = std + self.channelOrder = config.decodeImageChannelOrder + } + + private static func resizeRule(from params: DetResizeParams) throws -> ResizeRule { + .limitSide( + len: params.limitSideLen, + limitType: params.limitType, + maxSideLimit: params.maxSideLimit + ) + } + + /// Rasterizes ``CGImage`` to HWC uint8 (`BGR` or `RGB` per `DecodeImage.img_mode`). + func preprocess(_ image: CGImage, detResize: DetResizeParams) throws -> PreprocessResult { + let originalW = image.width + let originalH = image.height + + guard originalW > 0, originalH > 0 else { + throw DetPreprocessorError.invalidImage + } + + let pixelBytes = try rasterizeImageIntoHWCBuffer( + from: image, width: originalW, height: originalH, order: channelOrder + ) + return try preprocessFromHWCPixelBuffer( + pixelBytes: pixelBytes, + originalWidth: originalW, + originalHeight: originalH, + detResize: detResize + ) + } + + private func preprocessFromHWCPixelBuffer( + pixelBytes initialPixels: [UInt8], + originalWidth: Int, + originalHeight: Int, + detResize: DetResizeParams + ) throws -> PreprocessResult { + let resizeRule = try Self.resizeRule(from: detResize) + var pixelBytes = initialPixels + var currentW = originalWidth + var currentH = originalHeight + + if currentH + currentW < 64 { + let paddedH = max(32, currentH) + let paddedW = max(32, currentW) + pixelBytes = padImage(pixelBytes, fromW: currentW, fromH: currentH, toW: paddedW, toH: paddedH) + currentW = paddedW + currentH = paddedH + } + + let (resizeW, resizeH, ratioW, ratioH): (Int, Int, Float, Float) + switch resizeRule { + case .limitSide(let len, let limitType, let maxSideLimit): + (resizeW, resizeH, ratioW, ratioH) = computeResizeLimitSide( + srcW: currentW, + srcH: currentH, + limitSideLen: len, + limitType: limitType, + maxSideLimit: maxSideLimit + ) + case .longEdge(let resizeLong): + (resizeW, resizeH, ratioW, ratioH) = computeResizeLongEdge( + srcW: currentW, + srcH: currentH, + resizeLong: resizeLong + ) + } + + let resizedPixels = resizeImageBuffer( + pixelBytes, srcW: currentW, srcH: currentH, dstW: resizeW, dstH: resizeH + ) + + let normalizedHWC = normalizePixels(resizedPixels, width: resizeW, height: resizeH) + let chwData = hwcToCHW(normalizedHWC, width: resizeW, height: resizeH) + + return PreprocessResult( + tensorData: chwData, + tensorShape: [1, 3, resizeH, resizeW], + originalSize: (width: originalWidth, height: originalHeight), + resizedSize: (width: resizeW, height: resizeH), + ratioH: ratioH, + ratioW: ratioW + ) + } + + // MARK: - Rasterization + + /// Row-major HWC uint8: **BGR** or **RGB** per `DecodeImage.img_mode` in the model config file. + private func rasterizeImageIntoHWCBuffer( + from image: CGImage, + width: Int, + height: Int, + order: InferenceImageChannelOrder + ) throws -> [UInt8] { + let bytesPerPixel = 4 + let bytesPerRow = width * bytesPerPixel + var rgbaData = [UInt8](repeating: 0, count: height * bytesPerRow) + + guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB), + let context = CGContext( + data: &rgbaData, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: bytesPerRow, + space: colorSpace, + bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue + ) else { + throw DetPreprocessorError.pixelExtractionFailed + } + + context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height)) + + var out = [UInt8](repeating: 0, count: height * width * 3) + for i in 0..<(height * width) { + let r = rgbaData[i * 4] + let g = rgbaData[i * 4 + 1] + let b = rgbaData[i * 4 + 2] + switch order { + case .bgr: + out[i * 3] = b + out[i * 3 + 1] = g + out[i * 3 + 2] = r + case .rgb: + out[i * 3] = r + out[i * 3 + 1] = g + out[i * 3 + 2] = b + } + } + return out + } + + private func padImage(_ pixels: [UInt8], fromW: Int, fromH: Int, toW: Int, toH: Int) -> [UInt8] { + if fromW == toW && fromH == toH { + return pixels + } + let out = PDBOpenCVImageBridge.padRGBU8( + Data(pixels), width: fromW, height: fromH, padWidth: toW, padHeight: toH + ) + let expected = toW * toH * 3 + guard out.count == expected else { + return [UInt8](repeating: 0, count: expected) + } + return [UInt8](out) + } + + // MARK: - Resize geometry + + /// Limit-side resize with multiple-of-32 dimensions, optional cap on the larger side. + private func computeResizeLimitSide( + srcW: Int, + srcH: Int, + limitSideLen: Int, + limitType: String, + maxSideLimit: Int + ) -> (width: Int, height: Int, ratioW: Float, ratioH: Float) { + let h = Float(srcH) + let w = Float(srcW) + let ratio: Float + switch limitType { + case "max": + if max(srcH, srcW) > limitSideLen { + if srcH > srcW { + ratio = Float(limitSideLen) / h + } else { + ratio = Float(limitSideLen) / w + } + } else { + ratio = 1.0 + } + case "min": + if min(srcH, srcW) < limitSideLen { + if srcH < srcW { + ratio = Float(limitSideLen) / h + } else { + ratio = Float(limitSideLen) / w + } + } else { + ratio = 1.0 + } + case "resize_long": + ratio = Float(limitSideLen) / Float(max(srcH, srcW)) + default: + ratio = 1.0 + } + + var resizeH = Int(Float(srcH) * ratio) + var resizeW = Int(Float(srcW) * ratio) + + if max(resizeH, resizeW) > maxSideLimit { + let shrink = Float(maxSideLimit) / Float(max(resizeH, resizeW)) + resizeH = Int(Float(resizeH) * shrink) + resizeW = Int(Float(resizeW) * shrink) + } + + resizeH = max(Int((Double(resizeH) / 32.0).rounded(.toNearestOrEven)) * 32, 32) + resizeW = max(Int((Double(resizeW) / 32.0).rounded(.toNearestOrEven)) * 32, 32) + + if resizeH == srcH && resizeW == srcW { + return (srcW, srcH, 1.0, 1.0) + } + + let ratioH = Float(resizeH) / Float(srcH) + let ratioW = Float(resizeW) / Float(srcW) + return (resizeW, resizeH, ratioW, ratioH) + } + + /// Long-edge resize with stride 128 on both sides. + private func computeResizeLongEdge( + srcW: Int, + srcH: Int, + resizeLong: Int + ) -> (width: Int, height: Int, ratioW: Float, ratioH: Float) { + let ratio: Float + if srcH > srcW { + ratio = Float(resizeLong) / Float(srcH) + } else { + ratio = Float(resizeLong) / Float(srcW) + } + + var resizeH = Int(Float(srcH) * ratio) + var resizeW = Int(Float(srcW) * ratio) + + let stride = 128 + resizeH = ((resizeH + stride - 1) / stride) * stride + resizeW = ((resizeW + stride - 1) / stride) * stride + + let ratioH = Float(resizeH) / Float(srcH) + let ratioW = Float(resizeW) / Float(srcW) + return (resizeW, resizeH, ratioW, ratioH) + } + + private func resizeImageBuffer( + _ pixels: [UInt8], srcW: Int, srcH: Int, dstW: Int, dstH: Int + ) -> [UInt8] { + if srcW == dstW && srcH == dstH { + return pixels + } + let out = PDBOpenCVImageBridge.resizeRGBU8( + Data(pixels), srcWidth: srcW, srcHeight: srcH, dstWidth: dstW, dstHeight: dstH + ) + let expected = dstW * dstH * 3 + guard out.count == expected else { + return [UInt8](repeating: 0, count: expected) + } + return [UInt8](out) + } + + // MARK: - NormalizeImage + + private func normalizePixels(_ pixels: [UInt8], width: Int, height: Int) -> [Float] { + let pixelCount = width * height + var result = [Float](repeating: 0, count: pixelCount * 3) + + let scaleOverStd = [scale / std[0], scale / std[1], scale / std[2]] + let meanOverStd = [mean[0] / std[0], mean[1] / std[1], mean[2] / std[2]] + + for i in 0.. [Float] { + let channelSize = height * width + var chwData = [Float](repeating: 0, count: 3 * channelSize) + + for y in 0.. CGImage { + let bytesPerPixel = 4 + let bytesPerRow = width * bytesPerPixel + + let pixelCount = width * height + guard pixels.count == pixelCount * 3 else { + throw QuadTextCropError.imageCreationFailed + } + + var rgbaData = [UInt8](repeating: 255, count: pixelCount * bytesPerPixel) + for i in 0.. CGImage { + guard polygon.count == 4, polygon.allSatisfy({ $0.count == 2 }) else { + throw QuadTextCropError.invalidPolygon( + "Expected 4 points with 2 coordinates each, got \(polygon.count) points" + ) + } + + let srcPixels = try extractBGRPixels(from: image) + let srcW = image.width + let srcH = image.height + + let polyValues: [NSValue] = polygon.map { + NSValue(cgPoint: CGPoint(x: CGFloat($0[0]), y: CGFloat($0[1]))) + } + guard let out = PDBOpenCVImageBridge.quadTextLineCropBGR( + Data(srcPixels), + srcWidth: srcW, + srcHeight: srcH, + quad: polyValues + ) else { + throw QuadTextCropError.openCVFailed + } + + let bytes = [UInt8](out.rgbData) + return try BGRImageCodec.makeCGImageFromBGR(pixels: bytes, width: Int(out.width), height: Int(out.height)) + } +} + +// MARK: - Pixel Extraction & Image Creation + +private extension QuadTextCrop { + + /// Row-major HWC **BGR** + static func extractBGRPixels(from image: CGImage) throws -> [UInt8] { + let width = image.width + let height = image.height + let bytesPerPixel = 4 + let bytesPerRow = width * bytesPerPixel + var rgbaData = [UInt8](repeating: 0, count: height * bytesPerRow) + + guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB), + let context = CGContext( + data: &rgbaData, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: bytesPerRow, + space: colorSpace, + bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue + ) else { + throw QuadTextCropError.imageCreationFailed + } + + context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height)) + + let pixelCount = width * height + var bgrData = [UInt8](repeating: 0, count: pixelCount * 3) + for i in 0..= 3 else { + throw RecPreprocessorError.configMissing("RecResizeImg with image_shape") + } + + self.imgC = imageShape[0] + self.imgH = imageShape[1] + self.imgW = imageShape[2] + self.channelOrder = config.decodeImageChannelOrder + } + + /// Runs recognition preprocessing on a `CGImage`. + /// + /// Steps: extract pixels → compute dimensions → resize → normalize → HWC-to-CHW → pad + /// + /// - Parameter image: The input image (typically a cropped text region). + /// - Returns: A `RecPreprocessResult` with the float32 CHW tensor and metadata. + func preprocess(_ image: CGImage) throws -> RecPreprocessResult { + let originalW = image.width + let originalH = image.height + + guard originalW > 0, originalH > 0 else { + throw RecPreprocessorError.invalidImage + } + + let pixelBytes = try extractHWCPixels(from: image, width: originalW, height: originalH, order: channelOrder) + let (paddedData, canvasW, resizedW) = try lineResizeNormalizeCHW( + pixelBytes: pixelBytes, + originalW: originalW, + originalH: originalH, + alignCanvasTo8: Self.isRecognitionCanvasWidthAlignedTo8Enabled() + ) + + return RecPreprocessResult( + tensorData: paddedData, + tensorShape: [1, imgC, imgH, canvasW], + originalSize: (width: originalW, height: originalH), + resizedWidth: resizedW + ) + } + + /// Builds a batched tensor: each line is resized and padded to its canvas width, then all rows + /// are padded on the right to the maximum canvas width in the batch (shape `[N, C, H, W]`). + func preprocessBatch(_ images: [CGImage]) throws -> RecBatchPreprocessResult { + guard !images.isEmpty else { + throw RecPreprocessorError.invalidImage + } + + let align8 = Self.isRecognitionCanvasWidthAlignedTo8Enabled() + var rowCHW: [[Float]] = [] + var canvasWidths: [Int] = [] + + for image in images { + let originalW = image.width + let originalH = image.height + guard originalW > 0, originalH > 0 else { + throw RecPreprocessorError.invalidImage + } + let pixelBytes = try extractHWCPixels(from: image, width: originalW, height: originalH, order: channelOrder) + let (chw, canvasW, _) = try lineResizeNormalizeCHW( + pixelBytes: pixelBytes, + originalW: originalW, + originalH: originalH, + alignCanvasTo8: align8 + ) + rowCHW.append(chw) + canvasWidths.append(canvasW) + } + + guard let maxCanvasW = canvasWidths.max() else { + throw RecPreprocessorError.invalidImage + } + + let n = rowCHW.count + let channelSize = imgH * maxCanvasW + var flat = [Float](repeating: 0, count: n * imgC * channelSize) + + for (b, row) in rowCHW.enumerated() { + let w = canvasWidths[b] + let paddedRow: [Float] + if w == maxCanvasW { + paddedRow = row + } else { + let contentW = w + paddedRow = padToTargetWidth( + row, + contentWidth: contentW, + targetWidth: maxCanvasW, + height: imgH, + channels: imgC + ) + } + let batchOffset = b * imgC * channelSize + for i in 0.. Bool { + let v = ProcessInfo.processInfo.environment["PADDLEOCR_REC_ALIGN_WIDTH_TO_8"]?.lowercased() + return v == "1" || v == "true" || v == "yes" + } + + /// One text line: HWC uint8 → resize → normalize → CHW, padded to the computed canvas width. + private func lineResizeNormalizeCHW( + pixelBytes: [UInt8], + originalW: Int, + originalH: Int, + alignCanvasTo8: Bool + ) throws -> (chw: [Float], canvasW: Int, resizedW: Int) { + let w = originalW + let h = originalH + let whRatio = Float(w) / Float(h) + let defaultRatio = Float(imgW) / Float(imgH) + let maxWhRatio = max(defaultRatio, whRatio) + var canvasW = Int(Float(imgH) * maxWhRatio) + let resizedW: Int + + if canvasW > maxImgW { + canvasW = maxImgW + resizedW = maxImgW + } else { + let ratio = Float(w) / Float(h) + if Int(ceil(Float(imgH) * ratio)) > canvasW { + resizedW = canvasW + } else { + resizedW = Int(ceil(Float(imgH) * ratio)) + } + } + + if alignCanvasTo8 { + canvasW = min(max(roundUpToMultipleOf(canvasW, 8), resizedW), maxImgW) + } + + let resizedPixels = resizeWithOpenCV(pixelBytes, srcW: w, srcH: h, dstW: resizedW, dstH: imgH) + let normalizedHWC = normalizePixels(resizedPixels, width: resizedW, height: imgH) + let chwData = hwcToCHW(normalizedHWC, width: resizedW, height: imgH) + let paddedData = padToTargetWidth( + chwData, + contentWidth: resizedW, + targetWidth: canvasW, + height: imgH, + channels: imgC + ) + return (paddedData, canvasW, resizedW) + } + + // MARK: - Step 0: Pixel extraction (BGR / RGB) + + private func extractHWCPixels( + from image: CGImage, + width: Int, + height: Int, + order: InferenceImageChannelOrder + ) throws -> [UInt8] { + let bytesPerPixel = 4 + let bytesPerRow = width * bytesPerPixel + var rgbaData = [UInt8](repeating: 0, count: height * bytesPerRow) + + guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB), + let context = CGContext( + data: &rgbaData, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: bytesPerRow, + space: colorSpace, + bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue + ) else { + throw RecPreprocessorError.pixelExtractionFailed + } + + context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height)) + + var out = [UInt8](repeating: 0, count: height * width * 3) + for i in 0..<(height * width) { + let r = rgbaData[i * 4] + let g = rgbaData[i * 4 + 1] + let b = rgbaData[i * 4 + 2] + switch order { + case .bgr: + out[i * 3] = b + out[i * 3 + 1] = g + out[i * 3 + 2] = r + case .rgb: + out[i * 3] = r + out[i * 3 + 1] = g + out[i * 3 + 2] = b + } + } + return out + } + + // MARK: - Resize (OpenCV) + + private func resizeWithOpenCV( + _ pixels: [UInt8], srcW: Int, srcH: Int, dstW: Int, dstH: Int + ) -> [UInt8] { + if srcW == dstW && srcH == dstH { + return pixels + } + let out = PDBOpenCVImageBridge.resizeRGBU8( + Data(pixels), srcWidth: srcW, srcHeight: srcH, dstWidth: dstW, dstHeight: dstH + ) + let expected = dstW * dstH * 3 + guard out.count == expected else { + return [UInt8](repeating: 0, count: expected) + } + return [UInt8](out) + } + + // MARK: - Normalize (recognition) + + /// Maps pixel values from [0, 255] to [-1, 1] via `pixel / 127.5 - 1.0`. + private func normalizePixels(_ pixels: [UInt8], width: Int, height: Int) -> [Float] { + let count = width * height * 3 + var result = [Float](repeating: 0, count: count) + + for i in 0.. [Float] { + let channelSize = height * width + var chwData = [Float](repeating: 0, count: 3 * channelSize) + + for y in 0.. [Float] { + if contentWidth == targetWidth { + return chwData + } + + let targetChannelSize = height * targetWidth + let sourceChannelSize = height * contentWidth + var padded = [Float](repeating: 0, count: channels * targetChannelSize) + + for c in 0.. Int { + guard multiple > 0 else { return value } + return (value + multiple - 1) / multiple * multiple +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/RecognitionEngine.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/RecognitionEngine.swift new file mode 100644 index 0000000000000000000000000000000000000000..5865ce238bc1d442cda3d8f0ab14c904658cd65a --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/RecognitionEngine.swift @@ -0,0 +1,165 @@ +// Copyright (c) 2026 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. + +import CoreGraphics +import Foundation + +// MARK: - Recognition Engine Result + +/// The result of running text recognition on a cropped text image. +/// +/// Contains the decoded text, confidence score, and per-stage timing metrics. +struct RecognitionEngineResult { + /// The decoded text string from CTC decoding. + let text: String + /// Average confidence across all decoded characters (0.0 to 1.0). + let confidence: Float + /// Seconds spent in preprocessing (resize, normalize, HWC-to-CHW, pad). + let preprocessTime: TimeInterval + /// Seconds spent in ONNX Runtime inference. + let inferenceTime: TimeInterval + /// Seconds spent in CTC decoding (best-class pick, dedup, char mapping). + let postprocessTime: TimeInterval + /// Total recognition time (preprocess + inference + postprocess). + let totalTime: TimeInterval + /// Recognition model input tensor shape for the batch this line belonged to. + let inputTensorShape: [Int] +} + +// MARK: - Recognition Engine Errors + +enum RecognitionEngineError: LocalizedError { + case noOutputTensor + case unexpectedOutputShape([Int]) + case batchOutputCountMismatch(expectedBatch: Int, shape: [Int]) + + var errorDescription: String? { + switch self { + case .noOutputTensor: + return "Recognition model produced no output tensor" + case .unexpectedOutputShape(let shape): + return "Unexpected recognition output shape: \(shape), expected [1, T, C] or [N, T, C]" + case .batchOutputCountMismatch(let expectedBatch, let shape): + return "Recognition batch size mismatch: expected batch dimension \(expectedBatch), got shape \(shape)" + } + } +} + +// MARK: - RecognitionEngine + +/// Orchestrates text recognition: +/// CGImage → RecPreprocessor → ORT inference → CTCDecoder. +/// +/// This is the integration layer that composes `RecPreprocessor`, `ORTSessionManager`, +/// and `CTCDecoder` into a single callable unit. All preprocessing parameters are read +/// from the recognition model config file at initialization time. +/// +/// Usage: +/// ```swift +/// let engine = try RecognitionEngine(sessionManager: manager) +/// let result = try await engine.recognize(croppedTextImage) +/// print("Text: \(result.text) (confidence: \(result.confidence))") +/// ``` +class RecognitionEngine { + private let sessionManager: ORTSessionManager + private let preprocessor: RecPreprocessor + private let decoder: CTCDecoder + private(set) var modelConfig: InferenceConfig + + /// Initialize with an existing ORTSessionManager (models must already be loaded). + /// + /// Loads the recognition model config file from the bundle to configure + /// the preprocessor and CTC decoder. + /// + /// - Parameter sessionManager: A loaded ORTSessionManager with recognition model ready. + /// - Throws: If the model config cannot be loaded or required fields are missing. + init(sessionManager: ORTSessionManager) throws { + self.sessionManager = sessionManager + + let recPaths = try ModelConfig.recognition() + let cfg = try InferenceConfig.load(from: recPaths.configPath) + self.modelConfig = cfg + + self.preprocessor = try RecPreprocessor(config: cfg) + self.decoder = try CTCDecoder(config: cfg) + } + + /// Run recognition on a cropped text region CGImage, returning decoded text with confidence. + /// + /// Steps: RecPreprocessor → ORT inference → CTCDecoder + /// + /// - Parameter image: A cropped text region image. + /// - Returns: A `RecognitionEngineResult` with text, confidence, and per-stage timing. + func recognize(_ image: CGImage) async throws -> RecognitionEngineResult { + let batch = try await recognizeBatch([image]) + guard let first = batch.first else { + throw RecognitionEngineError.noOutputTensor + } + return first + } + + /// Runs recognition on multiple cropped lines in one batched inference call. + /// + /// - Parameter images: Cropped text regions in inference order (caller controls ordering). + /// - Returns: One result per input image, same order. + func recognizeBatch(_ images: [CGImage]) async throws -> [RecognitionEngineResult] { + guard !images.isEmpty else { return [] } + + let preprocessStart = CFAbsoluteTimeGetCurrent() + let batchPreprocessed = try preprocessor.preprocessBatch(images) + let preprocessTime = CFAbsoluteTimeGetCurrent() - preprocessStart + + let inferenceStart = CFAbsoluteTimeGetCurrent() + let outputs = try await sessionManager.runRecognition( + inputData: batchPreprocessed.tensorData, + shape: batchPreprocessed.tensorShape + ) + let inferenceTime = CFAbsoluteTimeGetCurrent() - inferenceStart + + guard let firstOutput = outputs.values.first else { + throw RecognitionEngineError.noOutputTensor + } + let outputData = firstOutput.data + let outputShape = firstOutput.shape + + guard outputShape.count == 3, outputShape[0] == images.count else { + if outputShape.count == 3 { + throw RecognitionEngineError.batchOutputCountMismatch(expectedBatch: images.count, shape: outputShape) + } + throw RecognitionEngineError.unexpectedOutputShape(outputShape) + } + + let postprocessStart = CFAbsoluteTimeGetCurrent() + let decoded = try decoder.decodeBatch(outputData: outputData, outputShape: outputShape) + let postprocessTime = CFAbsoluteTimeGetCurrent() - postprocessStart + + let n = Double(images.count) + let preprocessPer = preprocessTime / n + let inferencePer = inferenceTime / n + let postPer = postprocessTime / n + let totalPer = (preprocessTime + inferenceTime + postprocessTime) / n + + return decoded.map { d in + RecognitionEngineResult( + text: d.text, + confidence: d.confidence, + preprocessTime: preprocessPer, + inferenceTime: inferencePer, + postprocessTime: postPer, + totalTime: totalPer, + inputTensorShape: batchPreprocessed.tensorShape + ) + } + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..38004313122eb2687ddf7123c4038c54a29bd712 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.753", + "green" : "0.396", + "red" : "0.082" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..a65675bd40156303d975a8e87119db452526d6d9 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,62 @@ +{ + "images": [ + { + "filename": "Icon-20@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "20x20" + }, + { + "filename": "Icon-20@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "20x20" + }, + { + "filename": "Icon-29@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "29x29" + }, + { + "filename": "Icon-29@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "29x29" + }, + { + "filename": "Icon-40@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "40x40" + }, + { + "filename": "Icon-40@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "40x40" + }, + { + "filename": "Icon-60@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "60x60" + }, + { + "filename": "Icon-60@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "60x60" + }, + { + "filename": "Icon-1024.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..5cbc32ee10a372e8f01a854f712fa77960acc209 Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png differ diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8de1117ee6f654116f60089c250cf7da6cbaf125 Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png differ diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3aa6a12a5eb61b28bbc0418d9fd845ea695e49c6 Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png differ diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..78920df7975a6584c8918c7c21b27528c0084b8c Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png differ diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..51615399542a8eb3bdb3d900c9e1761b4dbb8c56 Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png differ diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3fb3e0eb8143c2a13a0b2a7411a732bb0661acba Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png differ diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..efdf5d18959c1065b980525484c6fd25ec8cd5f0 Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png differ diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..efdf5d18959c1065b980525484c6fd25ec8cd5f0 Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..01e2fc97ee32e65ef927d7f0ce8b9130e73519f0 Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png differ diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/Contents.json b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..b2cf3951c5db3652a71b3c9a8735aa44423a4f26 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/SampleImages/general_ocr_002.jpg b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/SampleImages/general_ocr_002.jpg new file mode 100644 index 0000000000000000000000000000000000000000..70f5b428c2c21a775fdfbba0668506c4a770dfd6 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/SampleImages/general_ocr_002.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4425af33dd163cf73bdff502bd35ee527e9bdd5725501db1da78bfdae9f538f4 +size 128713 diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Supporting/Info.plist b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Supporting/Info.plist new file mode 100644 index 0000000000000000000000000000000000000000..004308c3e9e21f8db04215119e687cb1a5e5c437 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Supporting/Info.plist @@ -0,0 +1,37 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleDisplayName + PaddleOCR + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Supporting/PaddleOCRDemo-Bridging-Header.h b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Supporting/PaddleOCRDemo-Bridging-Header.h new file mode 100644 index 0000000000000000000000000000000000000000..a444bbc1a28b03467529bdc3d441b638e27c8e71 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Supporting/PaddleOCRDemo-Bridging-Header.h @@ -0,0 +1,24 @@ +// Copyright (c) 2026 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. + +#ifndef BridgingHeader_h +#define BridgingHeader_h + +#import "ORTProfilingBridge.h" +#import "OpenCVDBBridge.h" +#import "OpenCVImageBridge.h" +#import "PDBPolygonOffsetBridge.h" +#import + +#endif diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/LICENSE b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..b3cf87ee31169994e14e679f54f7ba890a7e8330 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/LICENSE @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/clipper.cpp b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/clipper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d5d83764579216924e08bbb467dc414b8f4e0777 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/clipper.cpp @@ -0,0 +1,4377 @@ +/******************************************************************************* + * * + * Author : Angus Johnson * Version : 6.4.2 * Date : 27 February + *2017 * Website : + *http://www.angusj.com * Copyright : + *Angus Johnson 2010-2017 * + * * + * License: * Use, modification & distribution is subject to Boost Software + *License Ver 1. * http://www.boost.org/LICENSE_1_0.txt * + * * + * Attributions: * The code in this library is an extension of Bala Vatti's + *clipping algorithm: * "A generic solution to polygon clipping" * + * Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * + * http://portal.acm.org/citation.cfm?id=129906 * + * * + * Computer graphics and geometric modeling: implementation and algorithms * By + *Max K. Agoston * + * Springer; 1 edition (January 4, 2005) * + * http://books.google.com/books?q=vatti+clipping+agoston * + * * + * See also: * "Polygon Offsetting by Computing Winding Numbers" * Paper no. + *DETC2005-85513 pp. 565-575 * ASME 2005 + *International Design Engineering Technical Conferences * and + *Computers and Information in Engineering Conference (IDETC/CIE2005) * + * September 24-28, 2005 , Long Beach, California, USA * + * http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * + * * + *******************************************************************************/ + +/******************************************************************************* + * * + * This is a translation of the Delphi Clipper library and the naming style * + * used has retained a Delphi flavour. * + * * + *******************************************************************************/ + +#include "clipper.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ClipperLib { + +static double const pi = 3.141592653589793238; +static double const two_pi = pi * 2; +static double const def_arc_tolerance = 0.25; + +enum Direction { dRightToLeft, dLeftToRight }; + +static int const Unassigned = -1; // edge not currently 'owning' a solution +static int const Skip = -2; // edge that would otherwise close a path + +#define HORIZONTAL (-1.0E+40) +#define TOLERANCE (1.0e-20) +#define NEAR_ZERO(val) (((val) > -TOLERANCE) && ((val) < TOLERANCE)) + +struct TEdge { + IntPoint Bot; + IntPoint Curr; // current (updated for every new scanbeam) + IntPoint Top; + double Dx; + PolyType PolyTyp; + EdgeSide Side; // side only refers to current side of solution poly + int WindDelta; // 1 or -1 depending on winding direction + int WindCnt; + int WindCnt2; // winding count of the opposite polytype + int OutIdx; + TEdge *Next; + TEdge *Prev; + TEdge *NextInLML; + TEdge *NextInAEL; + TEdge *PrevInAEL; + TEdge *NextInSEL; + TEdge *PrevInSEL; +}; + +struct IntersectNode { + TEdge *Edge1; + TEdge *Edge2; + IntPoint Pt; +}; + +struct LocalMinimum { + cInt Y; + TEdge *LeftBound; + TEdge *RightBound; +}; + +struct OutPt; + +// OutRec: contains a path in the clipping solution. Edges in the AEL will +// carry a pointer to an OutRec when they are part of the clipping solution. +struct OutRec { + int Idx; + bool IsHole; + bool IsOpen; + OutRec *FirstLeft; // see comments in clipper.pas + PolyNode *PolyNd; + OutPt *Pts; + OutPt *BottomPt; +}; + +struct OutPt { + int Idx; + IntPoint Pt; + OutPt *Next; + OutPt *Prev; +}; + +struct Join { + OutPt *OutPt1; + OutPt *OutPt2; + IntPoint OffPt; +}; + +struct LocMinSorter { + inline bool operator()(const LocalMinimum &locMin1, + const LocalMinimum &locMin2) { + return locMin2.Y < locMin1.Y; + } +}; + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ + +inline cInt Round(double val) { + if ((val < 0)) + return static_cast(val - 0.5); + else + return static_cast(val + 0.5); +} +//------------------------------------------------------------------------------ + +inline cInt Abs(cInt val) { return val < 0 ? -val : val; } + +//------------------------------------------------------------------------------ +// PolyTree methods ... +//------------------------------------------------------------------------------ + +void PolyTree::Clear() { + for (PolyNodes::size_type i = 0; i < AllNodes.size(); ++i) + delete AllNodes[i]; + AllNodes.resize(0); + Childs.resize(0); +} +//------------------------------------------------------------------------------ + +PolyNode *PolyTree::GetFirst() const { + if (!Childs.empty()) + return Childs[0]; + else + return 0; +} +//------------------------------------------------------------------------------ + +int PolyTree::Total() const { + int result = (int)AllNodes.size(); + // with negative offsets, ignore the hidden outer polygon ... + if (result > 0 && Childs[0] != AllNodes[0]) + result--; + return result; +} + +//------------------------------------------------------------------------------ +// PolyNode methods ... +//------------------------------------------------------------------------------ + +PolyNode::PolyNode() : Parent(0), Index(0), m_IsOpen(false) {} +//------------------------------------------------------------------------------ + +int PolyNode::ChildCount() const { return (int)Childs.size(); } +//------------------------------------------------------------------------------ + +void PolyNode::AddChild(PolyNode &child) { + unsigned cnt = (unsigned)Childs.size(); + Childs.push_back(&child); + child.Parent = this; + child.Index = cnt; +} +//------------------------------------------------------------------------------ + +PolyNode *PolyNode::GetNext() const { + if (!Childs.empty()) + return Childs[0]; + else + return GetNextSiblingUp(); +} +//------------------------------------------------------------------------------ + +PolyNode *PolyNode::GetNextSiblingUp() const { + if (!Parent) // protects against PolyTree.GetNextSiblingUp() + return 0; + else if (Index == Parent->Childs.size() - 1) + return Parent->GetNextSiblingUp(); + else + return Parent->Childs[Index + 1]; +} +//------------------------------------------------------------------------------ + +bool PolyNode::IsHole() const { + bool result = true; + PolyNode *node = Parent; + while (node) { + result = !result; + node = node->Parent; + } + return result; +} +//------------------------------------------------------------------------------ + +bool PolyNode::IsOpen() const { return m_IsOpen; } +//------------------------------------------------------------------------------ + +#ifndef use_int32 + +//------------------------------------------------------------------------------ +// Int128 class (enables safe math on signed 64bit integers) +// eg Int128 val1((long64)9223372036854775807); //ie 2^63 -1 +// Int128 val2((long64)9223372036854775807); +// Int128 val3 = val1 * val2; +// val3.AsString => "85070591730234615847396907784232501249" (8.5e+37) +//------------------------------------------------------------------------------ + +class Int128 { +public: + ulong64 lo; + long64 hi; + + Int128(long64 _lo = 0) { + lo = (ulong64)_lo; + if (_lo < 0) + hi = -1; + else + hi = 0; + } + + Int128(const Int128 &val) : lo(val.lo), hi(val.hi) {} + + Int128(const long64 &_hi, const ulong64 &_lo) : lo(_lo), hi(_hi) {} + + Int128 &operator=(const long64 &val) { + lo = (ulong64)val; + if (val < 0) + hi = -1; + else + hi = 0; + return *this; + } + + bool operator==(const Int128 &val) const { + return (hi == val.hi && lo == val.lo); + } + + bool operator!=(const Int128 &val) const { return !(*this == val); } + + bool operator>(const Int128 &val) const { + if (hi != val.hi) + return hi > val.hi; + else + return lo > val.lo; + } + + bool operator<(const Int128 &val) const { + if (hi != val.hi) + return hi < val.hi; + else + return lo < val.lo; + } + + bool operator>=(const Int128 &val) const { return !(*this < val); } + + bool operator<=(const Int128 &val) const { return !(*this > val); } + + Int128 &operator+=(const Int128 &rhs) { + hi += rhs.hi; + lo += rhs.lo; + if (lo < rhs.lo) + hi++; + return *this; + } + + Int128 operator+(const Int128 &rhs) const { + Int128 result(*this); + result += rhs; + return result; + } + + Int128 &operator-=(const Int128 &rhs) { + *this += -rhs; + return *this; + } + + Int128 operator-(const Int128 &rhs) const { + Int128 result(*this); + result -= rhs; + return result; + } + + Int128 operator-() const // unary negation + { + if (lo == 0) + return Int128(-hi, 0); + else + return Int128(~hi, ~lo + 1); + } + + operator double() const { + const double shift64 = 18446744073709551616.0; // 2^64 + if (hi < 0) { + if (lo == 0) + return (double)hi * shift64; + else + return -(double)(~lo + ~hi * shift64); + } else + return (double)(lo + hi * shift64); + } +}; +//------------------------------------------------------------------------------ + +Int128 Int128Mul(long64 lhs, long64 rhs) { + bool negate = (lhs < 0) != (rhs < 0); + + if (lhs < 0) + lhs = -lhs; + ulong64 int1Hi = ulong64(lhs) >> 32; + ulong64 int1Lo = ulong64(lhs & 0xFFFFFFFF); + + if (rhs < 0) + rhs = -rhs; + ulong64 int2Hi = ulong64(rhs) >> 32; + ulong64 int2Lo = ulong64(rhs & 0xFFFFFFFF); + + // nb: see comments in clipper.pas + ulong64 a = int1Hi * int2Hi; + ulong64 b = int1Lo * int2Lo; + ulong64 c = int1Hi * int2Lo + int1Lo * int2Hi; + + Int128 tmp; + tmp.hi = long64(a + (c >> 32)); + tmp.lo = long64(c << 32); + tmp.lo += long64(b); + if (tmp.lo < b) + tmp.hi++; + if (negate) + tmp = -tmp; + return tmp; +}; +#endif + +//------------------------------------------------------------------------------ +// Miscellaneous global functions +//------------------------------------------------------------------------------ + +bool Orientation(const Path &poly) { return Area(poly) >= 0; } +//------------------------------------------------------------------------------ + +double Area(const Path &poly) { + int size = (int)poly.size(); + if (size < 3) + return 0; + + double a = 0; + for (int i = 0, j = size - 1; i < size; ++i) { + a += ((double)poly[j].X + poly[i].X) * ((double)poly[j].Y - poly[i].Y); + j = i; + } + return -a * 0.5; +} +//------------------------------------------------------------------------------ + +double Area(const OutPt *op) { + const OutPt *startOp = op; + if (!op) + return 0; + double a = 0; + do { + a += (double)(op->Prev->Pt.X + op->Pt.X) * + (double)(op->Prev->Pt.Y - op->Pt.Y); + op = op->Next; + } while (op != startOp); + return a * 0.5; +} +//------------------------------------------------------------------------------ + +double Area(const OutRec &outRec) { return Area(outRec.Pts); } +//------------------------------------------------------------------------------ + +bool PointIsVertex(const IntPoint &Pt, OutPt *pp) { + OutPt *pp2 = pp; + do { + if (pp2->Pt == Pt) + return true; + pp2 = pp2->Next; + } while (pp2 != pp); + return false; +} +//------------------------------------------------------------------------------ + +// See "The Point in Polygon Problem for Arbitrary Polygons" by Hormann & +// Agathos +// http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf +int PointInPolygon(const IntPoint &pt, const Path &path) { + // returns 0 if false, +1 if true, -1 if pt ON polygon boundary + int result = 0; + size_t cnt = path.size(); + if (cnt < 3) + return 0; + IntPoint ip = path[0]; + for (size_t i = 1; i <= cnt; ++i) { + IntPoint ipNext = (i == cnt ? path[0] : path[i]); + if (ipNext.Y == pt.Y) { + if ((ipNext.X == pt.X) || + (ip.Y == pt.Y && ((ipNext.X > pt.X) == (ip.X < pt.X)))) + return -1; + } + if ((ip.Y < pt.Y) != (ipNext.Y < pt.Y)) { + if (ip.X >= pt.X) { + if (ipNext.X > pt.X) + result = 1 - result; + else { + double d = (double)(ip.X - pt.X) * (ipNext.Y - pt.Y) - + (double)(ipNext.X - pt.X) * (ip.Y - pt.Y); + if (!d) + return -1; + if ((d > 0) == (ipNext.Y > ip.Y)) + result = 1 - result; + } + } else { + if (ipNext.X > pt.X) { + double d = (double)(ip.X - pt.X) * (ipNext.Y - pt.Y) - + (double)(ipNext.X - pt.X) * (ip.Y - pt.Y); + if (!d) + return -1; + if ((d > 0) == (ipNext.Y > ip.Y)) + result = 1 - result; + } + } + } + ip = ipNext; + } + return result; +} +//------------------------------------------------------------------------------ + +int PointInPolygon(const IntPoint &pt, OutPt *op) { + // returns 0 if false, +1 if true, -1 if pt ON polygon boundary + int result = 0; + OutPt *startOp = op; + for (;;) { + if (op->Next->Pt.Y == pt.Y) { + if ((op->Next->Pt.X == pt.X) || + (op->Pt.Y == pt.Y && ((op->Next->Pt.X > pt.X) == (op->Pt.X < pt.X)))) + return -1; + } + if ((op->Pt.Y < pt.Y) != (op->Next->Pt.Y < pt.Y)) { + if (op->Pt.X >= pt.X) { + if (op->Next->Pt.X > pt.X) + result = 1 - result; + else { + double d = (double)(op->Pt.X - pt.X) * (op->Next->Pt.Y - pt.Y) - + (double)(op->Next->Pt.X - pt.X) * (op->Pt.Y - pt.Y); + if (!d) + return -1; + if ((d > 0) == (op->Next->Pt.Y > op->Pt.Y)) + result = 1 - result; + } + } else { + if (op->Next->Pt.X > pt.X) { + double d = (double)(op->Pt.X - pt.X) * (op->Next->Pt.Y - pt.Y) - + (double)(op->Next->Pt.X - pt.X) * (op->Pt.Y - pt.Y); + if (!d) + return -1; + if ((d > 0) == (op->Next->Pt.Y > op->Pt.Y)) + result = 1 - result; + } + } + } + op = op->Next; + if (startOp == op) + break; + } + return result; +} +//------------------------------------------------------------------------------ + +bool Poly2ContainsPoly1(OutPt *OutPt1, OutPt *OutPt2) { + OutPt *op = OutPt1; + do { + // nb: PointInPolygon returns 0 if false, +1 if true, -1 if pt on polygon + int res = PointInPolygon(op->Pt, OutPt2); + if (res >= 0) + return res > 0; + op = op->Next; + } while (op != OutPt1); + return true; +} +//---------------------------------------------------------------------- + +bool SlopesEqual(const TEdge &e1, const TEdge &e2, bool UseFullInt64Range) { +#ifndef use_int32 + if (UseFullInt64Range) + return Int128Mul(e1.Top.Y - e1.Bot.Y, e2.Top.X - e2.Bot.X) == + Int128Mul(e1.Top.X - e1.Bot.X, e2.Top.Y - e2.Bot.Y); + else +#endif + return (e1.Top.Y - e1.Bot.Y) * (e2.Top.X - e2.Bot.X) == + (e1.Top.X - e1.Bot.X) * (e2.Top.Y - e2.Bot.Y); +} +//------------------------------------------------------------------------------ + +bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, const IntPoint pt3, + bool UseFullInt64Range) { +#ifndef use_int32 + if (UseFullInt64Range) + return Int128Mul(pt1.Y - pt2.Y, pt2.X - pt3.X) == + Int128Mul(pt1.X - pt2.X, pt2.Y - pt3.Y); + else +#endif + return (pt1.Y - pt2.Y) * (pt2.X - pt3.X) == + (pt1.X - pt2.X) * (pt2.Y - pt3.Y); +} +//------------------------------------------------------------------------------ + +bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, const IntPoint pt3, + const IntPoint pt4, bool UseFullInt64Range) { +#ifndef use_int32 + if (UseFullInt64Range) + return Int128Mul(pt1.Y - pt2.Y, pt3.X - pt4.X) == + Int128Mul(pt1.X - pt2.X, pt3.Y - pt4.Y); + else +#endif + return (pt1.Y - pt2.Y) * (pt3.X - pt4.X) == + (pt1.X - pt2.X) * (pt3.Y - pt4.Y); +} +//------------------------------------------------------------------------------ + +inline bool IsHorizontal(TEdge &e) { return e.Dx == HORIZONTAL; } +//------------------------------------------------------------------------------ + +inline double GetDx(const IntPoint pt1, const IntPoint pt2) { + return (pt1.Y == pt2.Y) ? HORIZONTAL + : (double)(pt2.X - pt1.X) / (pt2.Y - pt1.Y); +} +//--------------------------------------------------------------------------- + +inline void SetDx(TEdge &e) { + cInt dy = (e.Top.Y - e.Bot.Y); + if (dy == 0) + e.Dx = HORIZONTAL; + else + e.Dx = (double)(e.Top.X - e.Bot.X) / dy; +} +//--------------------------------------------------------------------------- + +inline void SwapSides(TEdge &Edge1, TEdge &Edge2) { + EdgeSide Side = Edge1.Side; + Edge1.Side = Edge2.Side; + Edge2.Side = Side; +} +//------------------------------------------------------------------------------ + +inline void SwapPolyIndexes(TEdge &Edge1, TEdge &Edge2) { + int OutIdx = Edge1.OutIdx; + Edge1.OutIdx = Edge2.OutIdx; + Edge2.OutIdx = OutIdx; +} +//------------------------------------------------------------------------------ + +inline cInt TopX(TEdge &edge, const cInt currentY) { + return (currentY == edge.Top.Y) + ? edge.Top.X + : edge.Bot.X + Round(edge.Dx * (currentY - edge.Bot.Y)); +} +//------------------------------------------------------------------------------ + +void IntersectPoint(TEdge &Edge1, TEdge &Edge2, IntPoint &ip) { +#ifdef use_xyz + ip.Z = 0; +#endif + + double b1, b2; + if (Edge1.Dx == Edge2.Dx) { + ip.Y = Edge1.Curr.Y; + ip.X = TopX(Edge1, ip.Y); + return; + } else if (Edge1.Dx == 0) { + ip.X = Edge1.Bot.X; + if (IsHorizontal(Edge2)) + ip.Y = Edge2.Bot.Y; + else { + b2 = Edge2.Bot.Y - (Edge2.Bot.X / Edge2.Dx); + ip.Y = Round(ip.X / Edge2.Dx + b2); + } + } else if (Edge2.Dx == 0) { + ip.X = Edge2.Bot.X; + if (IsHorizontal(Edge1)) + ip.Y = Edge1.Bot.Y; + else { + b1 = Edge1.Bot.Y - (Edge1.Bot.X / Edge1.Dx); + ip.Y = Round(ip.X / Edge1.Dx + b1); + } + } else { + b1 = Edge1.Bot.X - Edge1.Bot.Y * Edge1.Dx; + b2 = Edge2.Bot.X - Edge2.Bot.Y * Edge2.Dx; + double q = (b2 - b1) / (Edge1.Dx - Edge2.Dx); + ip.Y = Round(q); + if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) + ip.X = Round(Edge1.Dx * q + b1); + else + ip.X = Round(Edge2.Dx * q + b2); + } + + if (ip.Y < Edge1.Top.Y || ip.Y < Edge2.Top.Y) { + if (Edge1.Top.Y > Edge2.Top.Y) + ip.Y = Edge1.Top.Y; + else + ip.Y = Edge2.Top.Y; + if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) + ip.X = TopX(Edge1, ip.Y); + else + ip.X = TopX(Edge2, ip.Y); + } + // finally, don't allow 'ip' to be BELOW curr.Y (ie bottom of scanbeam) ... + if (ip.Y > Edge1.Curr.Y) { + ip.Y = Edge1.Curr.Y; + // use the more vertical edge to derive X ... + if (std::fabs(Edge1.Dx) > std::fabs(Edge2.Dx)) + ip.X = TopX(Edge2, ip.Y); + else + ip.X = TopX(Edge1, ip.Y); + } +} +//------------------------------------------------------------------------------ + +void ReversePolyPtLinks(OutPt *pp) { + if (!pp) + return; + OutPt *pp1, *pp2; + pp1 = pp; + do { + pp2 = pp1->Next; + pp1->Next = pp1->Prev; + pp1->Prev = pp2; + pp1 = pp2; + } while (pp1 != pp); +} +//------------------------------------------------------------------------------ + +void DisposeOutPts(OutPt *&pp) { + if (pp == 0) + return; + pp->Prev->Next = 0; + while (pp) { + OutPt *tmpPp = pp; + pp = pp->Next; + delete tmpPp; + } +} +//------------------------------------------------------------------------------ + +inline void InitEdge(TEdge *e, TEdge *eNext, TEdge *ePrev, const IntPoint &Pt) { + std::memset(e, 0, sizeof(TEdge)); + e->Next = eNext; + e->Prev = ePrev; + e->Curr = Pt; + e->OutIdx = Unassigned; +} +//------------------------------------------------------------------------------ + +void InitEdge2(TEdge &e, PolyType Pt) { + if (e.Curr.Y >= e.Next->Curr.Y) { + e.Bot = e.Curr; + e.Top = e.Next->Curr; + } else { + e.Top = e.Curr; + e.Bot = e.Next->Curr; + } + SetDx(e); + e.PolyTyp = Pt; +} +//------------------------------------------------------------------------------ + +TEdge *RemoveEdge(TEdge *e) { + // removes e from double_linked_list (but without removing from memory) + e->Prev->Next = e->Next; + e->Next->Prev = e->Prev; + TEdge *result = e->Next; + e->Prev = 0; // flag as removed (see ClipperBase.Clear) + return result; +} +//------------------------------------------------------------------------------ + +inline void ReverseHorizontal(TEdge &e) { + // swap horizontal edges' Top and Bottom x's so they follow the natural + // progression of the bounds - ie so their xbots will align with the + // adjoining lower edge. [Helpful in the ProcessHorizontal() method.] + std::swap(e.Top.X, e.Bot.X); +#ifdef use_xyz + std::swap(e.Top.Z, e.Bot.Z); +#endif +} +//------------------------------------------------------------------------------ + +void SwapPoints(IntPoint &pt1, IntPoint &pt2) { + IntPoint tmp = pt1; + pt1 = pt2; + pt2 = tmp; +} +//------------------------------------------------------------------------------ + +bool GetOverlapSegment(IntPoint pt1a, IntPoint pt1b, IntPoint pt2a, + IntPoint pt2b, IntPoint &pt1, IntPoint &pt2) { + // precondition: segments are Collinear. + if (Abs(pt1a.X - pt1b.X) > Abs(pt1a.Y - pt1b.Y)) { + if (pt1a.X > pt1b.X) + SwapPoints(pt1a, pt1b); + if (pt2a.X > pt2b.X) + SwapPoints(pt2a, pt2b); + if (pt1a.X > pt2a.X) + pt1 = pt1a; + else + pt1 = pt2a; + if (pt1b.X < pt2b.X) + pt2 = pt1b; + else + pt2 = pt2b; + return pt1.X < pt2.X; + } else { + if (pt1a.Y < pt1b.Y) + SwapPoints(pt1a, pt1b); + if (pt2a.Y < pt2b.Y) + SwapPoints(pt2a, pt2b); + if (pt1a.Y < pt2a.Y) + pt1 = pt1a; + else + pt1 = pt2a; + if (pt1b.Y > pt2b.Y) + pt2 = pt1b; + else + pt2 = pt2b; + return pt1.Y > pt2.Y; + } +} +//------------------------------------------------------------------------------ + +bool FirstIsBottomPt(const OutPt *btmPt1, const OutPt *btmPt2) { + OutPt *p = btmPt1->Prev; + while ((p->Pt == btmPt1->Pt) && (p != btmPt1)) + p = p->Prev; + double dx1p = std::fabs(GetDx(btmPt1->Pt, p->Pt)); + p = btmPt1->Next; + while ((p->Pt == btmPt1->Pt) && (p != btmPt1)) + p = p->Next; + double dx1n = std::fabs(GetDx(btmPt1->Pt, p->Pt)); + + p = btmPt2->Prev; + while ((p->Pt == btmPt2->Pt) && (p != btmPt2)) + p = p->Prev; + double dx2p = std::fabs(GetDx(btmPt2->Pt, p->Pt)); + p = btmPt2->Next; + while ((p->Pt == btmPt2->Pt) && (p != btmPt2)) + p = p->Next; + double dx2n = std::fabs(GetDx(btmPt2->Pt, p->Pt)); + + if (std::max(dx1p, dx1n) == std::max(dx2p, dx2n) && + std::min(dx1p, dx1n) == std::min(dx2p, dx2n)) + return Area(btmPt1) > 0; // if otherwise identical use orientation + else + return (dx1p >= dx2p && dx1p >= dx2n) || (dx1n >= dx2p && dx1n >= dx2n); +} +//------------------------------------------------------------------------------ + +OutPt *GetBottomPt(OutPt *pp) { + OutPt *dups = 0; + OutPt *p = pp->Next; + while (p != pp) { + if (p->Pt.Y > pp->Pt.Y) { + pp = p; + dups = 0; + } else if (p->Pt.Y == pp->Pt.Y && p->Pt.X <= pp->Pt.X) { + if (p->Pt.X < pp->Pt.X) { + dups = 0; + pp = p; + } else { + if (p->Next != pp && p->Prev != pp) + dups = p; + } + } + p = p->Next; + } + if (dups) { + // there appears to be at least 2 vertices at BottomPt so ... + while (dups != p) { + if (!FirstIsBottomPt(p, dups)) + pp = dups; + dups = dups->Next; + while (dups->Pt != pp->Pt) + dups = dups->Next; + } + } + return pp; +} +//------------------------------------------------------------------------------ + +bool Pt2IsBetweenPt1AndPt3(const IntPoint pt1, const IntPoint pt2, + const IntPoint pt3) { + if ((pt1 == pt3) || (pt1 == pt2) || (pt3 == pt2)) + return false; + else if (pt1.X != pt3.X) + return (pt2.X > pt1.X) == (pt2.X < pt3.X); + else + return (pt2.Y > pt1.Y) == (pt2.Y < pt3.Y); +} +//------------------------------------------------------------------------------ + +bool HorzSegmentsOverlap(cInt seg1a, cInt seg1b, cInt seg2a, cInt seg2b) { + if (seg1a > seg1b) + std::swap(seg1a, seg1b); + if (seg2a > seg2b) + std::swap(seg2a, seg2b); + return (seg1a < seg2b) && (seg2a < seg1b); +} + +//------------------------------------------------------------------------------ +// ClipperBase class methods ... +//------------------------------------------------------------------------------ + +ClipperBase::ClipperBase() // constructor +{ + m_CurrentLM = m_MinimaList.begin(); // begin() == end() here + m_UseFullRange = false; +} +//------------------------------------------------------------------------------ + +ClipperBase::~ClipperBase() // destructor +{ + Clear(); +} +//------------------------------------------------------------------------------ + +void RangeTest(const IntPoint &Pt, bool &useFullRange) { + if (useFullRange) { + if (Pt.X > hiRange || Pt.Y > hiRange || -Pt.X > hiRange || -Pt.Y > hiRange) + throw clipperException("Coordinate outside allowed range"); + } else if (Pt.X > loRange || Pt.Y > loRange || -Pt.X > loRange || + -Pt.Y > loRange) { + useFullRange = true; + RangeTest(Pt, useFullRange); + } +} +//------------------------------------------------------------------------------ + +TEdge *FindNextLocMin(TEdge *E) { + for (;;) { + while (E->Bot != E->Prev->Bot || E->Curr == E->Top) + E = E->Next; + if (!IsHorizontal(*E) && !IsHorizontal(*E->Prev)) + break; + while (IsHorizontal(*E->Prev)) + E = E->Prev; + TEdge *E2 = E; + while (IsHorizontal(*E)) + E = E->Next; + if (E->Top.Y == E->Prev->Bot.Y) + continue; // ie just an intermediate horz. + if (E2->Prev->Bot.X < E->Bot.X) + E = E2; + break; + } + return E; +} +//------------------------------------------------------------------------------ + +TEdge *ClipperBase::ProcessBound(TEdge *E, bool NextIsForward) { + TEdge *Result = E; + TEdge *Horz = 0; + + if (E->OutIdx == Skip) { + // if edges still remain in the current bound beyond the skip edge then + // create another LocMin and call ProcessBound once more + if (NextIsForward) { + while (E->Top.Y == E->Next->Bot.Y) + E = E->Next; + // don't include top horizontals when parsing a bound a second time, + // they will be contained in the opposite bound ... + while (E != Result && IsHorizontal(*E)) + E = E->Prev; + } else { + while (E->Top.Y == E->Prev->Bot.Y) + E = E->Prev; + while (E != Result && IsHorizontal(*E)) + E = E->Next; + } + + if (E == Result) { + if (NextIsForward) + Result = E->Next; + else + Result = E->Prev; + } else { + // there are more edges in the bound beyond result starting with E + if (NextIsForward) + E = Result->Next; + else + E = Result->Prev; + MinimaList::value_type locMin; + locMin.Y = E->Bot.Y; + locMin.LeftBound = 0; + locMin.RightBound = E; + E->WindDelta = 0; + Result = ProcessBound(E, NextIsForward); + m_MinimaList.push_back(locMin); + } + return Result; + } + + TEdge *EStart; + + if (IsHorizontal(*E)) { + // We need to be careful with open paths because this may not be a + // true local minima (ie E may be following a skip edge). + // Also, consecutive horz. edges may start heading left before going right. + if (NextIsForward) + EStart = E->Prev; + else + EStart = E->Next; + if (IsHorizontal(*EStart)) // ie an adjoining horizontal skip edge + { + if (EStart->Bot.X != E->Bot.X && EStart->Top.X != E->Bot.X) + ReverseHorizontal(*E); + } else if (EStart->Bot.X != E->Bot.X) + ReverseHorizontal(*E); + } + + EStart = E; + if (NextIsForward) { + while (Result->Top.Y == Result->Next->Bot.Y && Result->Next->OutIdx != Skip) + Result = Result->Next; + if (IsHorizontal(*Result) && Result->Next->OutIdx != Skip) { + // nb: at the top of a bound, horizontals are added to the bound + // only when the preceding edge attaches to the horizontal's left vertex + // unless a Skip edge is encountered when that becomes the top divide + Horz = Result; + while (IsHorizontal(*Horz->Prev)) + Horz = Horz->Prev; + if (Horz->Prev->Top.X > Result->Next->Top.X) + Result = Horz->Prev; + } + while (E != Result) { + E->NextInLML = E->Next; + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Prev->Top.X) + ReverseHorizontal(*E); + E = E->Next; + } + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Prev->Top.X) + ReverseHorizontal(*E); + Result = Result->Next; // move to the edge just beyond current bound + } else { + while (Result->Top.Y == Result->Prev->Bot.Y && Result->Prev->OutIdx != Skip) + Result = Result->Prev; + if (IsHorizontal(*Result) && Result->Prev->OutIdx != Skip) { + Horz = Result; + while (IsHorizontal(*Horz->Next)) + Horz = Horz->Next; + if (Horz->Next->Top.X == Result->Prev->Top.X || + Horz->Next->Top.X > Result->Prev->Top.X) + Result = Horz->Next; + } + + while (E != Result) { + E->NextInLML = E->Prev; + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Next->Top.X) + ReverseHorizontal(*E); + E = E->Prev; + } + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Next->Top.X) + ReverseHorizontal(*E); + Result = Result->Prev; // move to the edge just beyond current bound + } + + return Result; +} +//------------------------------------------------------------------------------ + +bool ClipperBase::AddPath(const Path &pg, PolyType PolyTyp, bool Closed) { +#ifdef use_lines + if (!Closed && PolyTyp == ptClip) + throw clipperException("AddPath: Open paths must be subject."); +#else + if (!Closed) + throw clipperException("AddPath: Open paths have been disabled."); +#endif + + int highI = (int)pg.size() - 1; + if (Closed) + while (highI > 0 && (pg[highI] == pg[0])) + --highI; + while (highI > 0 && (pg[highI] == pg[highI - 1])) + --highI; + if ((Closed && highI < 2) || (!Closed && highI < 1)) + return false; + + // create a new edge array ... + TEdge *edges = new TEdge[highI + 1]; + + bool IsFlat = true; + // 1. Basic (first) edge initialization ... + try { + edges[1].Curr = pg[1]; + RangeTest(pg[0], m_UseFullRange); + RangeTest(pg[highI], m_UseFullRange); + InitEdge(&edges[0], &edges[1], &edges[highI], pg[0]); + InitEdge(&edges[highI], &edges[0], &edges[highI - 1], pg[highI]); + for (int i = highI - 1; i >= 1; --i) { + RangeTest(pg[i], m_UseFullRange); + InitEdge(&edges[i], &edges[i + 1], &edges[i - 1], pg[i]); + } + } catch (...) { + delete[] edges; + throw; // range test fails + } + TEdge *eStart = &edges[0]; + + // 2. Remove duplicate vertices, and (when closed) collinear edges ... + TEdge *E = eStart, *eLoopStop = eStart; + for (;;) { + // nb: allows matching start and end points when not Closed ... + if (E->Curr == E->Next->Curr && (Closed || E->Next != eStart)) { + if (E == E->Next) + break; + if (E == eStart) + eStart = E->Next; + E = RemoveEdge(E); + eLoopStop = E; + continue; + } + if (E->Prev == E->Next) + break; // only two vertices + else if (Closed && + SlopesEqual(E->Prev->Curr, E->Curr, E->Next->Curr, + m_UseFullRange) && + (!m_PreserveCollinear || + !Pt2IsBetweenPt1AndPt3(E->Prev->Curr, E->Curr, E->Next->Curr))) { + // Collinear edges are allowed for open paths but in closed paths + // the default is to merge adjacent collinear edges into a single edge. + // However, if the PreserveCollinear property is enabled, only overlapping + // collinear edges (ie spikes) will be removed from closed paths. + if (E == eStart) + eStart = E->Next; + E = RemoveEdge(E); + E = E->Prev; + eLoopStop = E; + continue; + } + E = E->Next; + if ((E == eLoopStop) || (!Closed && E->Next == eStart)) + break; + } + + if ((!Closed && (E == E->Next)) || (Closed && (E->Prev == E->Next))) { + delete[] edges; + return false; + } + + if (!Closed) { + m_HasOpenPaths = true; + eStart->Prev->OutIdx = Skip; + } + + // 3. Do second stage of edge initialization ... + E = eStart; + do { + InitEdge2(*E, PolyTyp); + E = E->Next; + if (IsFlat && E->Curr.Y != eStart->Curr.Y) + IsFlat = false; + } while (E != eStart); + + // 4. Finally, add edge bounds to LocalMinima list ... + + // Totally flat paths must be handled differently when adding them + // to LocalMinima list to avoid endless loops etc ... + if (IsFlat) { + if (Closed) { + delete[] edges; + return false; + } + E->Prev->OutIdx = Skip; + MinimaList::value_type locMin; + locMin.Y = E->Bot.Y; + locMin.LeftBound = 0; + locMin.RightBound = E; + locMin.RightBound->Side = esRight; + locMin.RightBound->WindDelta = 0; + for (;;) { + if (E->Bot.X != E->Prev->Top.X) + ReverseHorizontal(*E); + if (E->Next->OutIdx == Skip) + break; + E->NextInLML = E->Next; + E = E->Next; + } + m_MinimaList.push_back(locMin); + m_edges.push_back(edges); + return true; + } + + m_edges.push_back(edges); + bool leftBoundIsForward; + TEdge *EMin = 0; + + // workaround to avoid an endless loop in the while loop below when + // open paths have matching start and end points ... + if (E->Prev->Bot == E->Prev->Top) + E = E->Next; + + for (;;) { + E = FindNextLocMin(E); + if (E == EMin) + break; + else if (!EMin) + EMin = E; + + // E and E.Prev now share a local minima (left aligned if horizontal). + // Compare their slopes to find which starts which bound ... + MinimaList::value_type locMin; + locMin.Y = E->Bot.Y; + if (E->Dx < E->Prev->Dx) { + locMin.LeftBound = E->Prev; + locMin.RightBound = E; + leftBoundIsForward = false; // Q.nextInLML = Q.prev + } else { + locMin.LeftBound = E; + locMin.RightBound = E->Prev; + leftBoundIsForward = true; // Q.nextInLML = Q.next + } + + if (!Closed) + locMin.LeftBound->WindDelta = 0; + else if (locMin.LeftBound->Next == locMin.RightBound) + locMin.LeftBound->WindDelta = -1; + else + locMin.LeftBound->WindDelta = 1; + locMin.RightBound->WindDelta = -locMin.LeftBound->WindDelta; + + E = ProcessBound(locMin.LeftBound, leftBoundIsForward); + if (E->OutIdx == Skip) + E = ProcessBound(E, leftBoundIsForward); + + TEdge *E2 = ProcessBound(locMin.RightBound, !leftBoundIsForward); + if (E2->OutIdx == Skip) + E2 = ProcessBound(E2, !leftBoundIsForward); + + if (locMin.LeftBound->OutIdx == Skip) + locMin.LeftBound = 0; + else if (locMin.RightBound->OutIdx == Skip) + locMin.RightBound = 0; + m_MinimaList.push_back(locMin); + if (!leftBoundIsForward) + E = E2; + } + return true; +} +//------------------------------------------------------------------------------ + +bool ClipperBase::AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed) { + bool result = false; + for (Paths::size_type i = 0; i < ppg.size(); ++i) + if (AddPath(ppg[i], PolyTyp, Closed)) + result = true; + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::Clear() { + DisposeLocalMinimaList(); + for (EdgeList::size_type i = 0; i < m_edges.size(); ++i) { + TEdge *edges = m_edges[i]; + delete[] edges; + } + m_edges.clear(); + m_UseFullRange = false; + m_HasOpenPaths = false; +} +//------------------------------------------------------------------------------ + +void ClipperBase::Reset() { + m_CurrentLM = m_MinimaList.begin(); + if (m_CurrentLM == m_MinimaList.end()) + return; // ie nothing to process + std::sort(m_MinimaList.begin(), m_MinimaList.end(), LocMinSorter()); + + m_Scanbeam = ScanbeamList(); // clears/resets priority_queue + // reset all edges ... + for (MinimaList::iterator lm = m_MinimaList.begin(); lm != m_MinimaList.end(); + ++lm) { + InsertScanbeam(lm->Y); + TEdge *e = lm->LeftBound; + if (e) { + e->Curr = e->Bot; + e->Side = esLeft; + e->OutIdx = Unassigned; + } + + e = lm->RightBound; + if (e) { + e->Curr = e->Bot; + e->Side = esRight; + e->OutIdx = Unassigned; + } + } + m_ActiveEdges = 0; + m_CurrentLM = m_MinimaList.begin(); +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeLocalMinimaList() { + m_MinimaList.clear(); + m_CurrentLM = m_MinimaList.begin(); +} +//------------------------------------------------------------------------------ + +bool ClipperBase::PopLocalMinima(cInt Y, const LocalMinimum *&locMin) { + if (m_CurrentLM == m_MinimaList.end() || (*m_CurrentLM).Y != Y) + return false; + locMin = &(*m_CurrentLM); + ++m_CurrentLM; + return true; +} +//------------------------------------------------------------------------------ + +IntRect ClipperBase::GetBounds() { + IntRect result; + MinimaList::iterator lm = m_MinimaList.begin(); + if (lm == m_MinimaList.end()) { + result.left = result.top = result.right = result.bottom = 0; + return result; + } + result.left = lm->LeftBound->Bot.X; + result.top = lm->LeftBound->Bot.Y; + result.right = lm->LeftBound->Bot.X; + result.bottom = lm->LeftBound->Bot.Y; + while (lm != m_MinimaList.end()) { + // todo - needs fixing for open paths + result.bottom = std::max(result.bottom, lm->LeftBound->Bot.Y); + TEdge *e = lm->LeftBound; + for (;;) { + TEdge *bottomE = e; + while (e->NextInLML) { + if (e->Bot.X < result.left) + result.left = e->Bot.X; + if (e->Bot.X > result.right) + result.right = e->Bot.X; + e = e->NextInLML; + } + result.left = std::min(result.left, e->Bot.X); + result.right = std::max(result.right, e->Bot.X); + result.left = std::min(result.left, e->Top.X); + result.right = std::max(result.right, e->Top.X); + result.top = std::min(result.top, e->Top.Y); + if (bottomE == lm->LeftBound) + e = lm->RightBound; + else + break; + } + ++lm; + } + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::InsertScanbeam(const cInt Y) { m_Scanbeam.push(Y); } +//------------------------------------------------------------------------------ + +bool ClipperBase::PopScanbeam(cInt &Y) { + if (m_Scanbeam.empty()) + return false; + Y = m_Scanbeam.top(); + m_Scanbeam.pop(); + while (!m_Scanbeam.empty() && Y == m_Scanbeam.top()) { + m_Scanbeam.pop(); + } // Pop duplicates. + return true; +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeAllOutRecs() { + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + DisposeOutRec(i); + m_PolyOuts.clear(); +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeOutRec(PolyOutList::size_type index) { + OutRec *outRec = m_PolyOuts[index]; + if (outRec->Pts) + DisposeOutPts(outRec->Pts); + delete outRec; + m_PolyOuts[index] = 0; +} +//------------------------------------------------------------------------------ + +void ClipperBase::DeleteFromAEL(TEdge *e) { + TEdge *AelPrev = e->PrevInAEL; + TEdge *AelNext = e->NextInAEL; + if (!AelPrev && !AelNext && (e != m_ActiveEdges)) + return; // already deleted + if (AelPrev) + AelPrev->NextInAEL = AelNext; + else + m_ActiveEdges = AelNext; + if (AelNext) + AelNext->PrevInAEL = AelPrev; + e->NextInAEL = 0; + e->PrevInAEL = 0; +} +//------------------------------------------------------------------------------ + +OutRec *ClipperBase::CreateOutRec() { + OutRec *result = new OutRec; + result->IsHole = false; + result->IsOpen = false; + result->FirstLeft = 0; + result->Pts = 0; + result->BottomPt = 0; + result->PolyNd = 0; + m_PolyOuts.push_back(result); + result->Idx = (int)m_PolyOuts.size() - 1; + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::SwapPositionsInAEL(TEdge *Edge1, TEdge *Edge2) { + // check that one or other edge hasn't already been removed from AEL ... + if (Edge1->NextInAEL == Edge1->PrevInAEL || + Edge2->NextInAEL == Edge2->PrevInAEL) + return; + + if (Edge1->NextInAEL == Edge2) { + TEdge *Next = Edge2->NextInAEL; + if (Next) + Next->PrevInAEL = Edge1; + TEdge *Prev = Edge1->PrevInAEL; + if (Prev) + Prev->NextInAEL = Edge2; + Edge2->PrevInAEL = Prev; + Edge2->NextInAEL = Edge1; + Edge1->PrevInAEL = Edge2; + Edge1->NextInAEL = Next; + } else if (Edge2->NextInAEL == Edge1) { + TEdge *Next = Edge1->NextInAEL; + if (Next) + Next->PrevInAEL = Edge2; + TEdge *Prev = Edge2->PrevInAEL; + if (Prev) + Prev->NextInAEL = Edge1; + Edge1->PrevInAEL = Prev; + Edge1->NextInAEL = Edge2; + Edge2->PrevInAEL = Edge1; + Edge2->NextInAEL = Next; + } else { + TEdge *Next = Edge1->NextInAEL; + TEdge *Prev = Edge1->PrevInAEL; + Edge1->NextInAEL = Edge2->NextInAEL; + if (Edge1->NextInAEL) + Edge1->NextInAEL->PrevInAEL = Edge1; + Edge1->PrevInAEL = Edge2->PrevInAEL; + if (Edge1->PrevInAEL) + Edge1->PrevInAEL->NextInAEL = Edge1; + Edge2->NextInAEL = Next; + if (Edge2->NextInAEL) + Edge2->NextInAEL->PrevInAEL = Edge2; + Edge2->PrevInAEL = Prev; + if (Edge2->PrevInAEL) + Edge2->PrevInAEL->NextInAEL = Edge2; + } + + if (!Edge1->PrevInAEL) + m_ActiveEdges = Edge1; + else if (!Edge2->PrevInAEL) + m_ActiveEdges = Edge2; +} +//------------------------------------------------------------------------------ + +void ClipperBase::UpdateEdgeIntoAEL(TEdge *&e) { + if (!e->NextInLML) + throw clipperException("UpdateEdgeIntoAEL: invalid call"); + + e->NextInLML->OutIdx = e->OutIdx; + TEdge *AelPrev = e->PrevInAEL; + TEdge *AelNext = e->NextInAEL; + if (AelPrev) + AelPrev->NextInAEL = e->NextInLML; + else + m_ActiveEdges = e->NextInLML; + if (AelNext) + AelNext->PrevInAEL = e->NextInLML; + e->NextInLML->Side = e->Side; + e->NextInLML->WindDelta = e->WindDelta; + e->NextInLML->WindCnt = e->WindCnt; + e->NextInLML->WindCnt2 = e->WindCnt2; + e = e->NextInLML; + e->Curr = e->Bot; + e->PrevInAEL = AelPrev; + e->NextInAEL = AelNext; + if (!IsHorizontal(*e)) + InsertScanbeam(e->Top.Y); +} +//------------------------------------------------------------------------------ + +bool ClipperBase::LocalMinimaPending() { + return (m_CurrentLM != m_MinimaList.end()); +} + +//------------------------------------------------------------------------------ +// TClipper methods ... +//------------------------------------------------------------------------------ + +Clipper::Clipper(int initOptions) + : ClipperBase() // constructor +{ + m_ExecuteLocked = false; + m_UseFullRange = false; + m_ReverseOutput = ((initOptions & ioReverseSolution) != 0); + m_StrictSimple = ((initOptions & ioStrictlySimple) != 0); + m_PreserveCollinear = ((initOptions & ioPreserveCollinear) != 0); + m_HasOpenPaths = false; +#ifdef use_xyz + m_ZFill = 0; +#endif +} +//------------------------------------------------------------------------------ + +#ifdef use_xyz +void Clipper::ZFillFunction(ZFillCallback zFillFunc) { m_ZFill = zFillFunc; } +//------------------------------------------------------------------------------ +#endif + +bool Clipper::Execute(ClipType clipType, Paths &solution, + PolyFillType fillType) { + return Execute(clipType, solution, fillType, fillType); +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, PolyTree &polytree, + PolyFillType fillType) { + return Execute(clipType, polytree, fillType, fillType); +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, Paths &solution, + PolyFillType subjFillType, PolyFillType clipFillType) { + if (m_ExecuteLocked) + return false; + if (m_HasOpenPaths) + throw clipperException( + "Error: PolyTree struct is needed for open path clipping."); + m_ExecuteLocked = true; + solution.resize(0); + m_SubjFillType = subjFillType; + m_ClipFillType = clipFillType; + m_ClipType = clipType; + m_UsingPolyTree = false; + bool succeeded = ExecuteInternal(); + if (succeeded) + BuildResult(solution); + DisposeAllOutRecs(); + m_ExecuteLocked = false; + return succeeded; +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, PolyTree &polytree, + PolyFillType subjFillType, PolyFillType clipFillType) { + if (m_ExecuteLocked) + return false; + m_ExecuteLocked = true; + m_SubjFillType = subjFillType; + m_ClipFillType = clipFillType; + m_ClipType = clipType; + m_UsingPolyTree = true; + bool succeeded = ExecuteInternal(); + if (succeeded) + BuildResult2(polytree); + DisposeAllOutRecs(); + m_ExecuteLocked = false; + return succeeded; +} +//------------------------------------------------------------------------------ + +void Clipper::FixHoleLinkage(OutRec &outrec) { + // skip OutRecs that (a) contain outermost polygons or + //(b) already have the correct owner/child linkage ... + if (!outrec.FirstLeft || + (outrec.IsHole != outrec.FirstLeft->IsHole && outrec.FirstLeft->Pts)) + return; + + OutRec *orfl = outrec.FirstLeft; + while (orfl && ((orfl->IsHole == outrec.IsHole) || !orfl->Pts)) + orfl = orfl->FirstLeft; + outrec.FirstLeft = orfl; +} +//------------------------------------------------------------------------------ + +bool Clipper::ExecuteInternal() { + bool succeeded = true; + try { + Reset(); + m_Maxima = MaximaList(); + m_SortedEdges = 0; + + succeeded = true; + cInt botY, topY; + if (!PopScanbeam(botY)) + return false; + InsertLocalMinimaIntoAEL(botY); + while (PopScanbeam(topY) || LocalMinimaPending()) { + ProcessHorizontals(); + ClearGhostJoins(); + if (!ProcessIntersections(topY)) { + succeeded = false; + break; + } + ProcessEdgesAtTopOfScanbeam(topY); + botY = topY; + InsertLocalMinimaIntoAEL(botY); + } + } catch (...) { + succeeded = false; + } + + if (succeeded) { + // fix orientations ... + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + if (!outRec->Pts || outRec->IsOpen) + continue; + if ((outRec->IsHole ^ m_ReverseOutput) == (Area(*outRec) > 0)) + ReversePolyPtLinks(outRec->Pts); + } + + if (!m_Joins.empty()) + JoinCommonEdges(); + + // unfortunately FixupOutPolygon() must be done after JoinCommonEdges() + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + if (!outRec->Pts) + continue; + if (outRec->IsOpen) + FixupOutPolyline(*outRec); + else + FixupOutPolygon(*outRec); + } + + if (m_StrictSimple) + DoSimplePolygons(); + } + + ClearJoins(); + ClearGhostJoins(); + return succeeded; +} +//------------------------------------------------------------------------------ + +void Clipper::SetWindingCount(TEdge &edge) { + TEdge *e = edge.PrevInAEL; + // find the edge of the same polytype that immediately preceeds 'edge' in AEL + while (e && ((e->PolyTyp != edge.PolyTyp) || (e->WindDelta == 0))) + e = e->PrevInAEL; + if (!e) { + if (edge.WindDelta == 0) { + PolyFillType pft = + (edge.PolyTyp == ptSubject ? m_SubjFillType : m_ClipFillType); + edge.WindCnt = (pft == pftNegative ? -1 : 1); + } else + edge.WindCnt = edge.WindDelta; + edge.WindCnt2 = 0; + e = m_ActiveEdges; // ie get ready to calc WindCnt2 + } else if (edge.WindDelta == 0 && m_ClipType != ctUnion) { + edge.WindCnt = 1; + edge.WindCnt2 = e->WindCnt2; + e = e->NextInAEL; // ie get ready to calc WindCnt2 + } else if (IsEvenOddFillType(edge)) { + // EvenOdd filling ... + if (edge.WindDelta == 0) { + // are we inside a subj polygon ... + bool Inside = true; + TEdge *e2 = e->PrevInAEL; + while (e2) { + if (e2->PolyTyp == e->PolyTyp && e2->WindDelta != 0) + Inside = !Inside; + e2 = e2->PrevInAEL; + } + edge.WindCnt = (Inside ? 0 : 1); + } else { + edge.WindCnt = edge.WindDelta; + } + edge.WindCnt2 = e->WindCnt2; + e = e->NextInAEL; // ie get ready to calc WindCnt2 + } else { + // nonZero, Positive or Negative filling ... + if (e->WindCnt * e->WindDelta < 0) { + // prev edge is 'decreasing' WindCount (WC) toward zero + // so we're outside the previous polygon ... + if (Abs(e->WindCnt) > 1) { + // outside prev poly but still inside another. + // when reversing direction of prev poly use the same WC + if (e->WindDelta * edge.WindDelta < 0) + edge.WindCnt = e->WindCnt; + // otherwise continue to 'decrease' WC ... + else + edge.WindCnt = e->WindCnt + edge.WindDelta; + } else + // now outside all polys of same polytype so set own WC ... + edge.WindCnt = (edge.WindDelta == 0 ? 1 : edge.WindDelta); + } else { + // prev edge is 'increasing' WindCount (WC) away from zero + // so we're inside the previous polygon ... + if (edge.WindDelta == 0) + edge.WindCnt = (e->WindCnt < 0 ? e->WindCnt - 1 : e->WindCnt + 1); + // if wind direction is reversing prev then use same WC + else if (e->WindDelta * edge.WindDelta < 0) + edge.WindCnt = e->WindCnt; + // otherwise add to WC ... + else + edge.WindCnt = e->WindCnt + edge.WindDelta; + } + edge.WindCnt2 = e->WindCnt2; + e = e->NextInAEL; // ie get ready to calc WindCnt2 + } + + // update WindCnt2 ... + if (IsEvenOddAltFillType(edge)) { + // EvenOdd filling ... + while (e != &edge) { + if (e->WindDelta != 0) + edge.WindCnt2 = (edge.WindCnt2 == 0 ? 1 : 0); + e = e->NextInAEL; + } + } else { + // nonZero, Positive or Negative filling ... + while (e != &edge) { + edge.WindCnt2 += e->WindDelta; + e = e->NextInAEL; + } + } +} +//------------------------------------------------------------------------------ + +bool Clipper::IsEvenOddFillType(const TEdge &edge) const { + if (edge.PolyTyp == ptSubject) + return m_SubjFillType == pftEvenOdd; + else + return m_ClipFillType == pftEvenOdd; +} +//------------------------------------------------------------------------------ + +bool Clipper::IsEvenOddAltFillType(const TEdge &edge) const { + if (edge.PolyTyp == ptSubject) + return m_ClipFillType == pftEvenOdd; + else + return m_SubjFillType == pftEvenOdd; +} +//------------------------------------------------------------------------------ + +bool Clipper::IsContributing(const TEdge &edge) const { + PolyFillType pft, pft2; + if (edge.PolyTyp == ptSubject) { + pft = m_SubjFillType; + pft2 = m_ClipFillType; + } else { + pft = m_ClipFillType; + pft2 = m_SubjFillType; + } + + switch (pft) { + case pftEvenOdd: + // return false if a subj line has been flagged as inside a subj polygon + if (edge.WindDelta == 0 && edge.WindCnt != 1) + return false; + break; + case pftNonZero: + if (Abs(edge.WindCnt) != 1) + return false; + break; + case pftPositive: + if (edge.WindCnt != 1) + return false; + break; + default: // pftNegative + if (edge.WindCnt != -1) + return false; + } + + switch (m_ClipType) { + case ctIntersection: + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 != 0); + case pftPositive: + return (edge.WindCnt2 > 0); + default: + return (edge.WindCnt2 < 0); + } + break; + case ctUnion: + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 == 0); + case pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + break; + case ctDifference: + if (edge.PolyTyp == ptSubject) + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 == 0); + case pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + else + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 != 0); + case pftPositive: + return (edge.WindCnt2 > 0); + default: + return (edge.WindCnt2 < 0); + } + break; + case ctXor: + if (edge.WindDelta == 0) // XOr always contributing unless open + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 == 0); + case pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + else + return true; + break; + default: + return true; + } +} +//------------------------------------------------------------------------------ + +OutPt *Clipper::AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) { + OutPt *result; + TEdge *e, *prevE; + if (IsHorizontal(*e2) || (e1->Dx > e2->Dx)) { + result = AddOutPt(e1, Pt); + e2->OutIdx = e1->OutIdx; + e1->Side = esLeft; + e2->Side = esRight; + e = e1; + if (e->PrevInAEL == e2) + prevE = e2->PrevInAEL; + else + prevE = e->PrevInAEL; + } else { + result = AddOutPt(e2, Pt); + e1->OutIdx = e2->OutIdx; + e1->Side = esRight; + e2->Side = esLeft; + e = e2; + if (e->PrevInAEL == e1) + prevE = e1->PrevInAEL; + else + prevE = e->PrevInAEL; + } + + if (prevE && prevE->OutIdx >= 0 && prevE->Top.Y < Pt.Y && e->Top.Y < Pt.Y) { + cInt xPrev = TopX(*prevE, Pt.Y); + cInt xE = TopX(*e, Pt.Y); + if (xPrev == xE && (e->WindDelta != 0) && (prevE->WindDelta != 0) && + SlopesEqual(IntPoint(xPrev, Pt.Y), prevE->Top, IntPoint(xE, Pt.Y), + e->Top, m_UseFullRange)) { + OutPt *outPt = AddOutPt(prevE, Pt); + AddJoin(result, outPt, e->Top); + } + } + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) { + AddOutPt(e1, Pt); + if (e2->WindDelta == 0) + AddOutPt(e2, Pt); + if (e1->OutIdx == e2->OutIdx) { + e1->OutIdx = Unassigned; + e2->OutIdx = Unassigned; + } else if (e1->OutIdx < e2->OutIdx) + AppendPolygon(e1, e2); + else + AppendPolygon(e2, e1); +} +//------------------------------------------------------------------------------ + +void Clipper::AddEdgeToSEL(TEdge *edge) { + // SEL pointers in PEdge are reused to build a list of horizontal edges. + // However, we don't need to worry about order with horizontal edge + // processing. + if (!m_SortedEdges) { + m_SortedEdges = edge; + edge->PrevInSEL = 0; + edge->NextInSEL = 0; + } else { + edge->NextInSEL = m_SortedEdges; + edge->PrevInSEL = 0; + m_SortedEdges->PrevInSEL = edge; + m_SortedEdges = edge; + } +} +//------------------------------------------------------------------------------ + +bool Clipper::PopEdgeFromSEL(TEdge *&edge) { + if (!m_SortedEdges) + return false; + edge = m_SortedEdges; + DeleteFromSEL(m_SortedEdges); + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::CopyAELToSEL() { + TEdge *e = m_ActiveEdges; + m_SortedEdges = e; + while (e) { + e->PrevInSEL = e->PrevInAEL; + e->NextInSEL = e->NextInAEL; + e = e->NextInAEL; + } +} +//------------------------------------------------------------------------------ + +void Clipper::AddJoin(OutPt *op1, OutPt *op2, const IntPoint OffPt) { + Join *j = new Join; + j->OutPt1 = op1; + j->OutPt2 = op2; + j->OffPt = OffPt; + m_Joins.push_back(j); +} +//------------------------------------------------------------------------------ + +void Clipper::ClearJoins() { + for (JoinList::size_type i = 0; i < m_Joins.size(); i++) + delete m_Joins[i]; + m_Joins.resize(0); +} +//------------------------------------------------------------------------------ + +void Clipper::ClearGhostJoins() { + for (JoinList::size_type i = 0; i < m_GhostJoins.size(); i++) + delete m_GhostJoins[i]; + m_GhostJoins.resize(0); +} +//------------------------------------------------------------------------------ + +void Clipper::AddGhostJoin(OutPt *op, const IntPoint OffPt) { + Join *j = new Join; + j->OutPt1 = op; + j->OutPt2 = 0; + j->OffPt = OffPt; + m_GhostJoins.push_back(j); +} +//------------------------------------------------------------------------------ + +void Clipper::InsertLocalMinimaIntoAEL(const cInt botY) { + const LocalMinimum *lm; + while (PopLocalMinima(botY, lm)) { + TEdge *lb = lm->LeftBound; + TEdge *rb = lm->RightBound; + + OutPt *Op1 = 0; + if (!lb) { + // nb: don't insert LB into either AEL or SEL + InsertEdgeIntoAEL(rb, 0); + SetWindingCount(*rb); + if (IsContributing(*rb)) + Op1 = AddOutPt(rb, rb->Bot); + } else if (!rb) { + InsertEdgeIntoAEL(lb, 0); + SetWindingCount(*lb); + if (IsContributing(*lb)) + Op1 = AddOutPt(lb, lb->Bot); + InsertScanbeam(lb->Top.Y); + } else { + InsertEdgeIntoAEL(lb, 0); + InsertEdgeIntoAEL(rb, lb); + SetWindingCount(*lb); + rb->WindCnt = lb->WindCnt; + rb->WindCnt2 = lb->WindCnt2; + if (IsContributing(*lb)) + Op1 = AddLocalMinPoly(lb, rb, lb->Bot); + InsertScanbeam(lb->Top.Y); + } + + if (rb) { + if (IsHorizontal(*rb)) { + AddEdgeToSEL(rb); + if (rb->NextInLML) + InsertScanbeam(rb->NextInLML->Top.Y); + } else + InsertScanbeam(rb->Top.Y); + } + + if (!lb || !rb) + continue; + + // if any output polygons share an edge, they'll need joining later ... + if (Op1 && IsHorizontal(*rb) && m_GhostJoins.size() > 0 && + (rb->WindDelta != 0)) { + for (JoinList::size_type i = 0; i < m_GhostJoins.size(); ++i) { + Join *jr = m_GhostJoins[i]; + // if the horizontal Rb and a 'ghost' horizontal overlap, then convert + // the 'ghost' join to a real join ready for later ... + if (HorzSegmentsOverlap(jr->OutPt1->Pt.X, jr->OffPt.X, rb->Bot.X, + rb->Top.X)) + AddJoin(jr->OutPt1, Op1, jr->OffPt); + } + } + + if (lb->OutIdx >= 0 && lb->PrevInAEL && + lb->PrevInAEL->Curr.X == lb->Bot.X && lb->PrevInAEL->OutIdx >= 0 && + SlopesEqual(lb->PrevInAEL->Bot, lb->PrevInAEL->Top, lb->Curr, lb->Top, + m_UseFullRange) && + (lb->WindDelta != 0) && (lb->PrevInAEL->WindDelta != 0)) { + OutPt *Op2 = AddOutPt(lb->PrevInAEL, lb->Bot); + AddJoin(Op1, Op2, lb->Top); + } + + if (lb->NextInAEL != rb) { + + if (rb->OutIdx >= 0 && rb->PrevInAEL->OutIdx >= 0 && + SlopesEqual(rb->PrevInAEL->Curr, rb->PrevInAEL->Top, rb->Curr, + rb->Top, m_UseFullRange) && + (rb->WindDelta != 0) && (rb->PrevInAEL->WindDelta != 0)) { + OutPt *Op2 = AddOutPt(rb->PrevInAEL, rb->Bot); + AddJoin(Op1, Op2, rb->Top); + } + + TEdge *e = lb->NextInAEL; + if (e) { + while (e != rb) { + // nb: For calculating winding counts etc, IntersectEdges() assumes + // that param1 will be to the Right of param2 ABOVE the intersection + // ... + IntersectEdges(rb, e, lb->Curr); // order important here + e = e->NextInAEL; + } + } + } + } +} +//------------------------------------------------------------------------------ + +void Clipper::DeleteFromSEL(TEdge *e) { + TEdge *SelPrev = e->PrevInSEL; + TEdge *SelNext = e->NextInSEL; + if (!SelPrev && !SelNext && (e != m_SortedEdges)) + return; // already deleted + if (SelPrev) + SelPrev->NextInSEL = SelNext; + else + m_SortedEdges = SelNext; + if (SelNext) + SelNext->PrevInSEL = SelPrev; + e->NextInSEL = 0; + e->PrevInSEL = 0; +} +//------------------------------------------------------------------------------ + +#ifdef use_xyz +void Clipper::SetZ(IntPoint &pt, TEdge &e1, TEdge &e2) { + if (pt.Z != 0 || !m_ZFill) + return; + else if (pt == e1.Bot) + pt.Z = e1.Bot.Z; + else if (pt == e1.Top) + pt.Z = e1.Top.Z; + else if (pt == e2.Bot) + pt.Z = e2.Bot.Z; + else if (pt == e2.Top) + pt.Z = e2.Top.Z; + else + (*m_ZFill)(e1.Bot, e1.Top, e2.Bot, e2.Top, pt); +} +//------------------------------------------------------------------------------ +#endif + +void Clipper::IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &Pt) { + bool e1Contributing = (e1->OutIdx >= 0); + bool e2Contributing = (e2->OutIdx >= 0); + +#ifdef use_xyz + SetZ(Pt, *e1, *e2); +#endif + +#ifdef use_lines + // if either edge is on an OPEN path ... + if (e1->WindDelta == 0 || e2->WindDelta == 0) { + // ignore subject-subject open path intersections UNLESS they + // are both open paths, AND they are both 'contributing maximas' ... + if (e1->WindDelta == 0 && e2->WindDelta == 0) + return; + + // if intersecting a subj line with a subj poly ... + else if (e1->PolyTyp == e2->PolyTyp && e1->WindDelta != e2->WindDelta && + m_ClipType == ctUnion) { + if (e1->WindDelta == 0) { + if (e2Contributing) { + AddOutPt(e1, Pt); + if (e1Contributing) + e1->OutIdx = Unassigned; + } + } else { + if (e1Contributing) { + AddOutPt(e2, Pt); + if (e2Contributing) + e2->OutIdx = Unassigned; + } + } + } else if (e1->PolyTyp != e2->PolyTyp) { + // toggle subj open path OutIdx on/off when Abs(clip.WndCnt) == 1 ... + if ((e1->WindDelta == 0) && abs(e2->WindCnt) == 1 && + (m_ClipType != ctUnion || e2->WindCnt2 == 0)) { + AddOutPt(e1, Pt); + if (e1Contributing) + e1->OutIdx = Unassigned; + } else if ((e2->WindDelta == 0) && (abs(e1->WindCnt) == 1) && + (m_ClipType != ctUnion || e1->WindCnt2 == 0)) { + AddOutPt(e2, Pt); + if (e2Contributing) + e2->OutIdx = Unassigned; + } + } + return; + } +#endif + + // update winding counts... + // assumes that e1 will be to the Right of e2 ABOVE the intersection + if (e1->PolyTyp == e2->PolyTyp) { + if (IsEvenOddFillType(*e1)) { + int oldE1WindCnt = e1->WindCnt; + e1->WindCnt = e2->WindCnt; + e2->WindCnt = oldE1WindCnt; + } else { + if (e1->WindCnt + e2->WindDelta == 0) + e1->WindCnt = -e1->WindCnt; + else + e1->WindCnt += e2->WindDelta; + if (e2->WindCnt - e1->WindDelta == 0) + e2->WindCnt = -e2->WindCnt; + else + e2->WindCnt -= e1->WindDelta; + } + } else { + if (!IsEvenOddFillType(*e2)) + e1->WindCnt2 += e2->WindDelta; + else + e1->WindCnt2 = (e1->WindCnt2 == 0) ? 1 : 0; + if (!IsEvenOddFillType(*e1)) + e2->WindCnt2 -= e1->WindDelta; + else + e2->WindCnt2 = (e2->WindCnt2 == 0) ? 1 : 0; + } + + PolyFillType e1FillType, e2FillType, e1FillType2, e2FillType2; + if (e1->PolyTyp == ptSubject) { + e1FillType = m_SubjFillType; + e1FillType2 = m_ClipFillType; + } else { + e1FillType = m_ClipFillType; + e1FillType2 = m_SubjFillType; + } + if (e2->PolyTyp == ptSubject) { + e2FillType = m_SubjFillType; + e2FillType2 = m_ClipFillType; + } else { + e2FillType = m_ClipFillType; + e2FillType2 = m_SubjFillType; + } + + cInt e1Wc, e2Wc; + switch (e1FillType) { + case pftPositive: + e1Wc = e1->WindCnt; + break; + case pftNegative: + e1Wc = -e1->WindCnt; + break; + default: + e1Wc = Abs(e1->WindCnt); + } + switch (e2FillType) { + case pftPositive: + e2Wc = e2->WindCnt; + break; + case pftNegative: + e2Wc = -e2->WindCnt; + break; + default: + e2Wc = Abs(e2->WindCnt); + } + + if (e1Contributing && e2Contributing) { + if ((e1Wc != 0 && e1Wc != 1) || (e2Wc != 0 && e2Wc != 1) || + (e1->PolyTyp != e2->PolyTyp && m_ClipType != ctXor)) { + AddLocalMaxPoly(e1, e2, Pt); + } else { + AddOutPt(e1, Pt); + AddOutPt(e2, Pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } else if (e1Contributing) { + if (e2Wc == 0 || e2Wc == 1) { + AddOutPt(e1, Pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } else if (e2Contributing) { + if (e1Wc == 0 || e1Wc == 1) { + AddOutPt(e2, Pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } else if ((e1Wc == 0 || e1Wc == 1) && (e2Wc == 0 || e2Wc == 1)) { + // neither edge is currently contributing ... + + cInt e1Wc2, e2Wc2; + switch (e1FillType2) { + case pftPositive: + e1Wc2 = e1->WindCnt2; + break; + case pftNegative: + e1Wc2 = -e1->WindCnt2; + break; + default: + e1Wc2 = Abs(e1->WindCnt2); + } + switch (e2FillType2) { + case pftPositive: + e2Wc2 = e2->WindCnt2; + break; + case pftNegative: + e2Wc2 = -e2->WindCnt2; + break; + default: + e2Wc2 = Abs(e2->WindCnt2); + } + + if (e1->PolyTyp != e2->PolyTyp) { + AddLocalMinPoly(e1, e2, Pt); + } else if (e1Wc == 1 && e2Wc == 1) + switch (m_ClipType) { + case ctIntersection: + if (e1Wc2 > 0 && e2Wc2 > 0) + AddLocalMinPoly(e1, e2, Pt); + break; + case ctUnion: + if (e1Wc2 <= 0 && e2Wc2 <= 0) + AddLocalMinPoly(e1, e2, Pt); + break; + case ctDifference: + if (((e1->PolyTyp == ptClip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || + ((e1->PolyTyp == ptSubject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) + AddLocalMinPoly(e1, e2, Pt); + break; + case ctXor: + AddLocalMinPoly(e1, e2, Pt); + } + else + SwapSides(*e1, *e2); + } +} +//------------------------------------------------------------------------------ + +void Clipper::SetHoleState(TEdge *e, OutRec *outrec) { + TEdge *e2 = e->PrevInAEL; + TEdge *eTmp = 0; + while (e2) { + if (e2->OutIdx >= 0 && e2->WindDelta != 0) { + if (!eTmp) + eTmp = e2; + else if (eTmp->OutIdx == e2->OutIdx) + eTmp = 0; + } + e2 = e2->PrevInAEL; + } + if (!eTmp) { + outrec->FirstLeft = 0; + outrec->IsHole = false; + } else { + outrec->FirstLeft = m_PolyOuts[eTmp->OutIdx]; + outrec->IsHole = !outrec->FirstLeft->IsHole; + } +} +//------------------------------------------------------------------------------ + +OutRec *GetLowermostRec(OutRec *outRec1, OutRec *outRec2) { + // work out which polygon fragment has the correct hole state ... + if (!outRec1->BottomPt) + outRec1->BottomPt = GetBottomPt(outRec1->Pts); + if (!outRec2->BottomPt) + outRec2->BottomPt = GetBottomPt(outRec2->Pts); + OutPt *OutPt1 = outRec1->BottomPt; + OutPt *OutPt2 = outRec2->BottomPt; + if (OutPt1->Pt.Y > OutPt2->Pt.Y) + return outRec1; + else if (OutPt1->Pt.Y < OutPt2->Pt.Y) + return outRec2; + else if (OutPt1->Pt.X < OutPt2->Pt.X) + return outRec1; + else if (OutPt1->Pt.X > OutPt2->Pt.X) + return outRec2; + else if (OutPt1->Next == OutPt1) + return outRec2; + else if (OutPt2->Next == OutPt2) + return outRec1; + else if (FirstIsBottomPt(OutPt1, OutPt2)) + return outRec1; + else + return outRec2; +} +//------------------------------------------------------------------------------ + +bool OutRec1RightOfOutRec2(OutRec *outRec1, OutRec *outRec2) { + do { + outRec1 = outRec1->FirstLeft; + if (outRec1 == outRec2) + return true; + } while (outRec1); + return false; +} +//------------------------------------------------------------------------------ + +OutRec *Clipper::GetOutRec(int Idx) { + OutRec *outrec = m_PolyOuts[Idx]; + while (outrec != m_PolyOuts[outrec->Idx]) + outrec = m_PolyOuts[outrec->Idx]; + return outrec; +} +//------------------------------------------------------------------------------ + +void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) { + // get the start and ends of both output polygons ... + OutRec *outRec1 = m_PolyOuts[e1->OutIdx]; + OutRec *outRec2 = m_PolyOuts[e2->OutIdx]; + + OutRec *holeStateRec; + if (OutRec1RightOfOutRec2(outRec1, outRec2)) + holeStateRec = outRec2; + else if (OutRec1RightOfOutRec2(outRec2, outRec1)) + holeStateRec = outRec1; + else + holeStateRec = GetLowermostRec(outRec1, outRec2); + + // get the start and ends of both output polygons and + // join e2 poly onto e1 poly and delete pointers to e2 ... + + OutPt *p1_lft = outRec1->Pts; + OutPt *p1_rt = p1_lft->Prev; + OutPt *p2_lft = outRec2->Pts; + OutPt *p2_rt = p2_lft->Prev; + + // join e2 poly onto e1 poly and delete pointers to e2 ... + if (e1->Side == esLeft) { + if (e2->Side == esLeft) { + // z y x a b c + ReversePolyPtLinks(p2_lft); + p2_lft->Next = p1_lft; + p1_lft->Prev = p2_lft; + p1_rt->Next = p2_rt; + p2_rt->Prev = p1_rt; + outRec1->Pts = p2_rt; + } else { + // x y z a b c + p2_rt->Next = p1_lft; + p1_lft->Prev = p2_rt; + p2_lft->Prev = p1_rt; + p1_rt->Next = p2_lft; + outRec1->Pts = p2_lft; + } + } else { + if (e2->Side == esRight) { + // a b c z y x + ReversePolyPtLinks(p2_lft); + p1_rt->Next = p2_rt; + p2_rt->Prev = p1_rt; + p2_lft->Next = p1_lft; + p1_lft->Prev = p2_lft; + } else { + // a b c x y z + p1_rt->Next = p2_lft; + p2_lft->Prev = p1_rt; + p1_lft->Prev = p2_rt; + p2_rt->Next = p1_lft; + } + } + + outRec1->BottomPt = 0; + if (holeStateRec == outRec2) { + if (outRec2->FirstLeft != outRec1) + outRec1->FirstLeft = outRec2->FirstLeft; + outRec1->IsHole = outRec2->IsHole; + } + outRec2->Pts = 0; + outRec2->BottomPt = 0; + outRec2->FirstLeft = outRec1; + + int OKIdx = e1->OutIdx; + int ObsoleteIdx = e2->OutIdx; + + e1->OutIdx = + Unassigned; // nb: safe because we only get here via AddLocalMaxPoly + e2->OutIdx = Unassigned; + + TEdge *e = m_ActiveEdges; + while (e) { + if (e->OutIdx == ObsoleteIdx) { + e->OutIdx = OKIdx; + e->Side = e1->Side; + break; + } + e = e->NextInAEL; + } + + outRec2->Idx = outRec1->Idx; +} +//------------------------------------------------------------------------------ + +OutPt *Clipper::AddOutPt(TEdge *e, const IntPoint &pt) { + if (e->OutIdx < 0) { + OutRec *outRec = CreateOutRec(); + outRec->IsOpen = (e->WindDelta == 0); + OutPt *newOp = new OutPt; + outRec->Pts = newOp; + newOp->Idx = outRec->Idx; + newOp->Pt = pt; + newOp->Next = newOp; + newOp->Prev = newOp; + if (!outRec->IsOpen) + SetHoleState(e, outRec); + e->OutIdx = outRec->Idx; + return newOp; + } else { + OutRec *outRec = m_PolyOuts[e->OutIdx]; + // OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most' + OutPt *op = outRec->Pts; + + bool ToFront = (e->Side == esLeft); + if (ToFront && (pt == op->Pt)) + return op; + else if (!ToFront && (pt == op->Prev->Pt)) + return op->Prev; + + OutPt *newOp = new OutPt; + newOp->Idx = outRec->Idx; + newOp->Pt = pt; + newOp->Next = op; + newOp->Prev = op->Prev; + newOp->Prev->Next = newOp; + op->Prev = newOp; + if (ToFront) + outRec->Pts = newOp; + return newOp; + } +} +//------------------------------------------------------------------------------ + +OutPt *Clipper::GetLastOutPt(TEdge *e) { + OutRec *outRec = m_PolyOuts[e->OutIdx]; + if (e->Side == esLeft) + return outRec->Pts; + else + return outRec->Pts->Prev; +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessHorizontals() { + TEdge *horzEdge; + while (PopEdgeFromSEL(horzEdge)) + ProcessHorizontal(horzEdge); +} +//------------------------------------------------------------------------------ + +inline bool IsMinima(TEdge *e) { + return e && (e->Prev->NextInLML != e) && (e->Next->NextInLML != e); +} +//------------------------------------------------------------------------------ + +inline bool IsMaxima(TEdge *e, const cInt Y) { + return e && e->Top.Y == Y && !e->NextInLML; +} +//------------------------------------------------------------------------------ + +inline bool IsIntermediate(TEdge *e, const cInt Y) { + return e->Top.Y == Y && e->NextInLML; +} +//------------------------------------------------------------------------------ + +TEdge *GetMaximaPair(TEdge *e) { + if ((e->Next->Top == e->Top) && !e->Next->NextInLML) + return e->Next; + else if ((e->Prev->Top == e->Top) && !e->Prev->NextInLML) + return e->Prev; + else + return 0; +} +//------------------------------------------------------------------------------ + +TEdge *GetMaximaPairEx(TEdge *e) { + // as GetMaximaPair() but returns 0 if MaxPair isn't in AEL (unless it's + // horizontal) + TEdge *result = GetMaximaPair(e); + if (result && + (result->OutIdx == Skip || + (result->NextInAEL == result->PrevInAEL && !IsHorizontal(*result)))) + return 0; + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::SwapPositionsInSEL(TEdge *Edge1, TEdge *Edge2) { + if (!(Edge1->NextInSEL) && !(Edge1->PrevInSEL)) + return; + if (!(Edge2->NextInSEL) && !(Edge2->PrevInSEL)) + return; + + if (Edge1->NextInSEL == Edge2) { + TEdge *Next = Edge2->NextInSEL; + if (Next) + Next->PrevInSEL = Edge1; + TEdge *Prev = Edge1->PrevInSEL; + if (Prev) + Prev->NextInSEL = Edge2; + Edge2->PrevInSEL = Prev; + Edge2->NextInSEL = Edge1; + Edge1->PrevInSEL = Edge2; + Edge1->NextInSEL = Next; + } else if (Edge2->NextInSEL == Edge1) { + TEdge *Next = Edge1->NextInSEL; + if (Next) + Next->PrevInSEL = Edge2; + TEdge *Prev = Edge2->PrevInSEL; + if (Prev) + Prev->NextInSEL = Edge1; + Edge1->PrevInSEL = Prev; + Edge1->NextInSEL = Edge2; + Edge2->PrevInSEL = Edge1; + Edge2->NextInSEL = Next; + } else { + TEdge *Next = Edge1->NextInSEL; + TEdge *Prev = Edge1->PrevInSEL; + Edge1->NextInSEL = Edge2->NextInSEL; + if (Edge1->NextInSEL) + Edge1->NextInSEL->PrevInSEL = Edge1; + Edge1->PrevInSEL = Edge2->PrevInSEL; + if (Edge1->PrevInSEL) + Edge1->PrevInSEL->NextInSEL = Edge1; + Edge2->NextInSEL = Next; + if (Edge2->NextInSEL) + Edge2->NextInSEL->PrevInSEL = Edge2; + Edge2->PrevInSEL = Prev; + if (Edge2->PrevInSEL) + Edge2->PrevInSEL->NextInSEL = Edge2; + } + + if (!Edge1->PrevInSEL) + m_SortedEdges = Edge1; + else if (!Edge2->PrevInSEL) + m_SortedEdges = Edge2; +} +//------------------------------------------------------------------------------ + +TEdge *GetNextInAEL(TEdge *e, Direction dir) { + return dir == dLeftToRight ? e->NextInAEL : e->PrevInAEL; +} +//------------------------------------------------------------------------------ + +void GetHorzDirection(TEdge &HorzEdge, Direction &Dir, cInt &Left, + cInt &Right) { + if (HorzEdge.Bot.X < HorzEdge.Top.X) { + Left = HorzEdge.Bot.X; + Right = HorzEdge.Top.X; + Dir = dLeftToRight; + } else { + Left = HorzEdge.Top.X; + Right = HorzEdge.Bot.X; + Dir = dRightToLeft; + } +} +//------------------------------------------------------------------------ + +/******************************************************************************* + * Notes: Horizontal edges (HEs) at scanline intersections (ie at the Top or * + * Bottom of a scanbeam) are processed as if layered. The order in which HEs * + * are processed doesn't matter. HEs intersect with other HE Bot.Xs only [#] * + * (or they could intersect with Top.Xs only, ie EITHER Bot.Xs OR Top.Xs), * and + *with other non-horizontal edges [*]. Once these intersections are * + * processed, intermediate HEs then 'promote' the Edge above (NextInLML) into * + * the AEL. These 'promoted' edges may in turn intersect [%] with other HEs. * + *******************************************************************************/ + +void Clipper::ProcessHorizontal(TEdge *horzEdge) { + Direction dir; + cInt horzLeft, horzRight; + bool IsOpen = (horzEdge->WindDelta == 0); + + GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); + + TEdge *eLastHorz = horzEdge, *eMaxPair = 0; + while (eLastHorz->NextInLML && IsHorizontal(*eLastHorz->NextInLML)) + eLastHorz = eLastHorz->NextInLML; + if (!eLastHorz->NextInLML) + eMaxPair = GetMaximaPair(eLastHorz); + + MaximaList::const_iterator maxIt; + MaximaList::const_reverse_iterator maxRit; + if (m_Maxima.size() > 0) { + // get the first maxima in range (X) ... + if (dir == dLeftToRight) { + maxIt = m_Maxima.begin(); + while (maxIt != m_Maxima.end() && *maxIt <= horzEdge->Bot.X) + maxIt++; + if (maxIt != m_Maxima.end() && *maxIt >= eLastHorz->Top.X) + maxIt = m_Maxima.end(); + } else { + maxRit = m_Maxima.rbegin(); + while (maxRit != m_Maxima.rend() && *maxRit > horzEdge->Bot.X) + maxRit++; + if (maxRit != m_Maxima.rend() && *maxRit <= eLastHorz->Top.X) + maxRit = m_Maxima.rend(); + } + } + + OutPt *op1 = 0; + + for (;;) // loop through consec. horizontal edges + { + + bool IsLastHorz = (horzEdge == eLastHorz); + TEdge *e = GetNextInAEL(horzEdge, dir); + while (e) { + + // this code block inserts extra coords into horizontal edges (in output + // polygons) whereever maxima touch these horizontal edges. This helps + //'simplifying' polygons (ie if the Simplify property is set). + if (m_Maxima.size() > 0) { + if (dir == dLeftToRight) { + while (maxIt != m_Maxima.end() && *maxIt < e->Curr.X) { + if (horzEdge->OutIdx >= 0 && !IsOpen) + AddOutPt(horzEdge, IntPoint(*maxIt, horzEdge->Bot.Y)); + maxIt++; + } + } else { + while (maxRit != m_Maxima.rend() && *maxRit > e->Curr.X) { + if (horzEdge->OutIdx >= 0 && !IsOpen) + AddOutPt(horzEdge, IntPoint(*maxRit, horzEdge->Bot.Y)); + maxRit++; + } + } + }; + + if ((dir == dLeftToRight && e->Curr.X > horzRight) || + (dir == dRightToLeft && e->Curr.X < horzLeft)) + break; + + // Also break if we've got to the end of an intermediate horizontal edge + // ... nb: Smaller Dx's are to the right of larger Dx's ABOVE the + // horizontal. + if (e->Curr.X == horzEdge->Top.X && horzEdge->NextInLML && + e->Dx < horzEdge->NextInLML->Dx) + break; + + if (horzEdge->OutIdx >= 0 && !IsOpen) // note: may be done multiple times + { +#ifdef use_xyz + if (dir == dLeftToRight) + SetZ(e->Curr, *horzEdge, *e); + else + SetZ(e->Curr, *e, *horzEdge); +#endif + op1 = AddOutPt(horzEdge, e->Curr); + TEdge *eNextHorz = m_SortedEdges; + while (eNextHorz) { + if (eNextHorz->OutIdx >= 0 && + HorzSegmentsOverlap(horzEdge->Bot.X, horzEdge->Top.X, + eNextHorz->Bot.X, eNextHorz->Top.X)) { + OutPt *op2 = GetLastOutPt(eNextHorz); + AddJoin(op2, op1, eNextHorz->Top); + } + eNextHorz = eNextHorz->NextInSEL; + } + AddGhostJoin(op1, horzEdge->Bot); + } + + // OK, so far we're still in range of the horizontal Edge but make sure + // we're at the last of consec. horizontals when matching with eMaxPair + if (e == eMaxPair && IsLastHorz) { + if (horzEdge->OutIdx >= 0) + AddLocalMaxPoly(horzEdge, eMaxPair, horzEdge->Top); + DeleteFromAEL(horzEdge); + DeleteFromAEL(eMaxPair); + return; + } + + if (dir == dLeftToRight) { + IntPoint Pt = IntPoint(e->Curr.X, horzEdge->Curr.Y); + IntersectEdges(horzEdge, e, Pt); + } else { + IntPoint Pt = IntPoint(e->Curr.X, horzEdge->Curr.Y); + IntersectEdges(e, horzEdge, Pt); + } + TEdge *eNext = GetNextInAEL(e, dir); + SwapPositionsInAEL(horzEdge, e); + e = eNext; + } // end while(e) + + // Break out of loop if HorzEdge.NextInLML is not also horizontal ... + if (!horzEdge->NextInLML || !IsHorizontal(*horzEdge->NextInLML)) + break; + + UpdateEdgeIntoAEL(horzEdge); + if (horzEdge->OutIdx >= 0) + AddOutPt(horzEdge, horzEdge->Bot); + GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); + + } // end for (;;) + + if (horzEdge->OutIdx >= 0 && !op1) { + op1 = GetLastOutPt(horzEdge); + TEdge *eNextHorz = m_SortedEdges; + while (eNextHorz) { + if (eNextHorz->OutIdx >= 0 && + HorzSegmentsOverlap(horzEdge->Bot.X, horzEdge->Top.X, + eNextHorz->Bot.X, eNextHorz->Top.X)) { + OutPt *op2 = GetLastOutPt(eNextHorz); + AddJoin(op2, op1, eNextHorz->Top); + } + eNextHorz = eNextHorz->NextInSEL; + } + AddGhostJoin(op1, horzEdge->Top); + } + + if (horzEdge->NextInLML) { + if (horzEdge->OutIdx >= 0) { + op1 = AddOutPt(horzEdge, horzEdge->Top); + UpdateEdgeIntoAEL(horzEdge); + if (horzEdge->WindDelta == 0) + return; + // nb: HorzEdge is no longer horizontal here + TEdge *ePrev = horzEdge->PrevInAEL; + TEdge *eNext = horzEdge->NextInAEL; + if (ePrev && ePrev->Curr.X == horzEdge->Bot.X && + ePrev->Curr.Y == horzEdge->Bot.Y && ePrev->WindDelta != 0 && + (ePrev->OutIdx >= 0 && ePrev->Curr.Y > ePrev->Top.Y && + SlopesEqual(*horzEdge, *ePrev, m_UseFullRange))) { + OutPt *op2 = AddOutPt(ePrev, horzEdge->Bot); + AddJoin(op1, op2, horzEdge->Top); + } else if (eNext && eNext->Curr.X == horzEdge->Bot.X && + eNext->Curr.Y == horzEdge->Bot.Y && eNext->WindDelta != 0 && + eNext->OutIdx >= 0 && eNext->Curr.Y > eNext->Top.Y && + SlopesEqual(*horzEdge, *eNext, m_UseFullRange)) { + OutPt *op2 = AddOutPt(eNext, horzEdge->Bot); + AddJoin(op1, op2, horzEdge->Top); + } + } else + UpdateEdgeIntoAEL(horzEdge); + } else { + if (horzEdge->OutIdx >= 0) + AddOutPt(horzEdge, horzEdge->Top); + DeleteFromAEL(horzEdge); + } +} +//------------------------------------------------------------------------------ + +bool Clipper::ProcessIntersections(const cInt topY) { + if (!m_ActiveEdges) + return true; + try { + BuildIntersectList(topY); + size_t IlSize = m_IntersectList.size(); + if (IlSize == 0) + return true; + if (IlSize == 1 || FixupIntersectionOrder()) + ProcessIntersectList(); + else + return false; + } catch (...) { + m_SortedEdges = 0; + DisposeIntersectNodes(); + throw clipperException("ProcessIntersections error"); + } + m_SortedEdges = 0; + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::DisposeIntersectNodes() { + for (size_t i = 0; i < m_IntersectList.size(); ++i) + delete m_IntersectList[i]; + m_IntersectList.clear(); +} +//------------------------------------------------------------------------------ + +void Clipper::BuildIntersectList(const cInt topY) { + if (!m_ActiveEdges) + return; + + // prepare for sorting ... + TEdge *e = m_ActiveEdges; + m_SortedEdges = e; + while (e) { + e->PrevInSEL = e->PrevInAEL; + e->NextInSEL = e->NextInAEL; + e->Curr.X = TopX(*e, topY); + e = e->NextInAEL; + } + + // bubblesort ... + bool isModified; + do { + isModified = false; + e = m_SortedEdges; + while (e->NextInSEL) { + TEdge *eNext = e->NextInSEL; + IntPoint Pt; + if (e->Curr.X > eNext->Curr.X) { + IntersectPoint(*e, *eNext, Pt); + if (Pt.Y < topY) + Pt = IntPoint(TopX(*e, topY), topY); + IntersectNode *newNode = new IntersectNode; + newNode->Edge1 = e; + newNode->Edge2 = eNext; + newNode->Pt = Pt; + m_IntersectList.push_back(newNode); + + SwapPositionsInSEL(e, eNext); + isModified = true; + } else + e = eNext; + } + if (e->PrevInSEL) + e->PrevInSEL->NextInSEL = 0; + else + break; + } while (isModified); + m_SortedEdges = 0; // important +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessIntersectList() { + for (size_t i = 0; i < m_IntersectList.size(); ++i) { + IntersectNode *iNode = m_IntersectList[i]; + { + IntersectEdges(iNode->Edge1, iNode->Edge2, iNode->Pt); + SwapPositionsInAEL(iNode->Edge1, iNode->Edge2); + } + delete iNode; + } + m_IntersectList.clear(); +} +//------------------------------------------------------------------------------ + +bool IntersectListSort(IntersectNode *node1, IntersectNode *node2) { + return node2->Pt.Y < node1->Pt.Y; +} +//------------------------------------------------------------------------------ + +inline bool EdgesAdjacent(const IntersectNode &inode) { + return (inode.Edge1->NextInSEL == inode.Edge2) || + (inode.Edge1->PrevInSEL == inode.Edge2); +} +//------------------------------------------------------------------------------ + +bool Clipper::FixupIntersectionOrder() { + // pre-condition: intersections are sorted Bottom-most first. + // Now it's crucial that intersections are made only between adjacent edges, + // so to ensure this the order of intersections may need adjusting ... + CopyAELToSEL(); + std::sort(m_IntersectList.begin(), m_IntersectList.end(), IntersectListSort); + size_t cnt = m_IntersectList.size(); + for (size_t i = 0; i < cnt; ++i) { + if (!EdgesAdjacent(*m_IntersectList[i])) { + size_t j = i + 1; + while (j < cnt && !EdgesAdjacent(*m_IntersectList[j])) + j++; + if (j == cnt) + return false; + std::swap(m_IntersectList[i], m_IntersectList[j]); + } + SwapPositionsInSEL(m_IntersectList[i]->Edge1, m_IntersectList[i]->Edge2); + } + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::DoMaxima(TEdge *e) { + TEdge *eMaxPair = GetMaximaPairEx(e); + if (!eMaxPair) { + if (e->OutIdx >= 0) + AddOutPt(e, e->Top); + DeleteFromAEL(e); + return; + } + + TEdge *eNext = e->NextInAEL; + while (eNext && eNext != eMaxPair) { + IntersectEdges(e, eNext, e->Top); + SwapPositionsInAEL(e, eNext); + eNext = e->NextInAEL; + } + + if (e->OutIdx == Unassigned && eMaxPair->OutIdx == Unassigned) { + DeleteFromAEL(e); + DeleteFromAEL(eMaxPair); + } else if (e->OutIdx >= 0 && eMaxPair->OutIdx >= 0) { + if (e->OutIdx >= 0) + AddLocalMaxPoly(e, eMaxPair, e->Top); + DeleteFromAEL(e); + DeleteFromAEL(eMaxPair); + } +#ifdef use_lines + else if (e->WindDelta == 0) { + if (e->OutIdx >= 0) { + AddOutPt(e, e->Top); + e->OutIdx = Unassigned; + } + DeleteFromAEL(e); + + if (eMaxPair->OutIdx >= 0) { + AddOutPt(eMaxPair, e->Top); + eMaxPair->OutIdx = Unassigned; + } + DeleteFromAEL(eMaxPair); + } +#endif + else + throw clipperException("DoMaxima error"); +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) { + TEdge *e = m_ActiveEdges; + while (e) { + // 1. process maxima, treating them as if they're 'bent' horizontal edges, + // but exclude maxima with horizontal edges. nb: e can't be a horizontal. + bool IsMaximaEdge = IsMaxima(e, topY); + + if (IsMaximaEdge) { + TEdge *eMaxPair = GetMaximaPairEx(e); + IsMaximaEdge = (!eMaxPair || !IsHorizontal(*eMaxPair)); + } + + if (IsMaximaEdge) { + if (m_StrictSimple) + m_Maxima.push_back(e->Top.X); + TEdge *ePrev = e->PrevInAEL; + DoMaxima(e); + if (!ePrev) + e = m_ActiveEdges; + else + e = ePrev->NextInAEL; + } else { + // 2. promote horizontal edges, otherwise update Curr.X and Curr.Y ... + if (IsIntermediate(e, topY) && IsHorizontal(*e->NextInLML)) { + UpdateEdgeIntoAEL(e); + if (e->OutIdx >= 0) + AddOutPt(e, e->Bot); + AddEdgeToSEL(e); + } else { + e->Curr.X = TopX(*e, topY); + e->Curr.Y = topY; +#ifdef use_xyz + e->Curr.Z = + topY == e->Top.Y ? e->Top.Z : (topY == e->Bot.Y ? e->Bot.Z : 0); +#endif + } + + // When StrictlySimple and 'e' is being touched by another edge, then + // make sure both edges have a vertex here ... + if (m_StrictSimple) { + TEdge *ePrev = e->PrevInAEL; + if ((e->OutIdx >= 0) && (e->WindDelta != 0) && ePrev && + (ePrev->OutIdx >= 0) && (ePrev->Curr.X == e->Curr.X) && + (ePrev->WindDelta != 0)) { + IntPoint pt = e->Curr; +#ifdef use_xyz + SetZ(pt, *ePrev, *e); +#endif + OutPt *op = AddOutPt(ePrev, pt); + OutPt *op2 = AddOutPt(e, pt); + AddJoin(op, op2, pt); // StrictlySimple (type-3) join + } + } + + e = e->NextInAEL; + } + } + + // 3. Process horizontals at the Top of the scanbeam ... + m_Maxima.sort(); + ProcessHorizontals(); + m_Maxima.clear(); + + // 4. Promote intermediate vertices ... + e = m_ActiveEdges; + while (e) { + if (IsIntermediate(e, topY)) { + OutPt *op = 0; + if (e->OutIdx >= 0) + op = AddOutPt(e, e->Top); + UpdateEdgeIntoAEL(e); + + // if output polygons share an edge, they'll need joining later ... + TEdge *ePrev = e->PrevInAEL; + TEdge *eNext = e->NextInAEL; + if (ePrev && ePrev->Curr.X == e->Bot.X && ePrev->Curr.Y == e->Bot.Y && + op && ePrev->OutIdx >= 0 && ePrev->Curr.Y > ePrev->Top.Y && + SlopesEqual(e->Curr, e->Top, ePrev->Curr, ePrev->Top, + m_UseFullRange) && + (e->WindDelta != 0) && (ePrev->WindDelta != 0)) { + OutPt *op2 = AddOutPt(ePrev, e->Bot); + AddJoin(op, op2, e->Top); + } else if (eNext && eNext->Curr.X == e->Bot.X && + eNext->Curr.Y == e->Bot.Y && op && eNext->OutIdx >= 0 && + eNext->Curr.Y > eNext->Top.Y && + SlopesEqual(e->Curr, e->Top, eNext->Curr, eNext->Top, + m_UseFullRange) && + (e->WindDelta != 0) && (eNext->WindDelta != 0)) { + OutPt *op2 = AddOutPt(eNext, e->Bot); + AddJoin(op, op2, e->Top); + } + } + e = e->NextInAEL; + } +} +//------------------------------------------------------------------------------ + +void Clipper::FixupOutPolyline(OutRec &outrec) { + OutPt *pp = outrec.Pts; + OutPt *lastPP = pp->Prev; + while (pp != lastPP) { + pp = pp->Next; + if (pp->Pt == pp->Prev->Pt) { + if (pp == lastPP) + lastPP = pp->Prev; + OutPt *tmpPP = pp->Prev; + tmpPP->Next = pp->Next; + pp->Next->Prev = tmpPP; + delete pp; + pp = tmpPP; + } + } + + if (pp == pp->Prev) { + DisposeOutPts(pp); + outrec.Pts = 0; + return; + } +} +//------------------------------------------------------------------------------ + +void Clipper::FixupOutPolygon(OutRec &outrec) { + // FixupOutPolygon() - removes duplicate points and simplifies consecutive + // parallel edges by removing the middle vertex. + OutPt *lastOK = 0; + outrec.BottomPt = 0; + OutPt *pp = outrec.Pts; + bool preserveCol = m_PreserveCollinear || m_StrictSimple; + + for (;;) { + if (pp->Prev == pp || pp->Prev == pp->Next) { + DisposeOutPts(pp); + outrec.Pts = 0; + return; + } + + // test for duplicate points and collinear edges ... + if ((pp->Pt == pp->Next->Pt) || (pp->Pt == pp->Prev->Pt) || + (SlopesEqual(pp->Prev->Pt, pp->Pt, pp->Next->Pt, m_UseFullRange) && + (!preserveCol || + !Pt2IsBetweenPt1AndPt3(pp->Prev->Pt, pp->Pt, pp->Next->Pt)))) { + lastOK = 0; + OutPt *tmp = pp; + pp->Prev->Next = pp->Next; + pp->Next->Prev = pp->Prev; + pp = pp->Prev; + delete tmp; + } else if (pp == lastOK) + break; + else { + if (!lastOK) + lastOK = pp; + pp = pp->Next; + } + } + outrec.Pts = pp; +} +//------------------------------------------------------------------------------ + +int PointCount(OutPt *Pts) { + if (!Pts) + return 0; + int result = 0; + OutPt *p = Pts; + do { + result++; + p = p->Next; + } while (p != Pts); + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::BuildResult(Paths &polys) { + polys.reserve(m_PolyOuts.size()); + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + if (!m_PolyOuts[i]->Pts) + continue; + Path pg; + OutPt *p = m_PolyOuts[i]->Pts->Prev; + int cnt = PointCount(p); + if (cnt < 2) + continue; + pg.reserve(cnt); + for (int i = 0; i < cnt; ++i) { + pg.push_back(p->Pt); + p = p->Prev; + } + polys.push_back(pg); + } +} +//------------------------------------------------------------------------------ + +void Clipper::BuildResult2(PolyTree &polytree) { + polytree.Clear(); + polytree.AllNodes.reserve(m_PolyOuts.size()); + // add each output polygon/contour to polytree ... + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) { + OutRec *outRec = m_PolyOuts[i]; + int cnt = PointCount(outRec->Pts); + if ((outRec->IsOpen && cnt < 2) || (!outRec->IsOpen && cnt < 3)) + continue; + FixHoleLinkage(*outRec); + PolyNode *pn = new PolyNode(); + // nb: polytree takes ownership of all the PolyNodes + polytree.AllNodes.push_back(pn); + outRec->PolyNd = pn; + pn->Parent = 0; + pn->Index = 0; + pn->Contour.reserve(cnt); + OutPt *op = outRec->Pts->Prev; + for (int j = 0; j < cnt; j++) { + pn->Contour.push_back(op->Pt); + op = op->Prev; + } + } + + // fixup PolyNode links etc ... + polytree.Childs.reserve(m_PolyOuts.size()); + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) { + OutRec *outRec = m_PolyOuts[i]; + if (!outRec->PolyNd) + continue; + if (outRec->IsOpen) { + outRec->PolyNd->m_IsOpen = true; + polytree.AddChild(*outRec->PolyNd); + } else if (outRec->FirstLeft && outRec->FirstLeft->PolyNd) + outRec->FirstLeft->PolyNd->AddChild(*outRec->PolyNd); + else + polytree.AddChild(*outRec->PolyNd); + } +} +//------------------------------------------------------------------------------ + +void SwapIntersectNodes(IntersectNode &int1, IntersectNode &int2) { + // just swap the contents (because fIntersectNodes is a single-linked-list) + IntersectNode inode = int1; // gets a copy of Int1 + int1.Edge1 = int2.Edge1; + int1.Edge2 = int2.Edge2; + int1.Pt = int2.Pt; + int2.Edge1 = inode.Edge1; + int2.Edge2 = inode.Edge2; + int2.Pt = inode.Pt; +} +//------------------------------------------------------------------------------ + +inline bool E2InsertsBeforeE1(TEdge &e1, TEdge &e2) { + if (e2.Curr.X == e1.Curr.X) { + if (e2.Top.Y > e1.Top.Y) + return e2.Top.X < TopX(e1, e2.Top.Y); + else + return e1.Top.X > TopX(e2, e1.Top.Y); + } else + return e2.Curr.X < e1.Curr.X; +} +//------------------------------------------------------------------------------ + +bool GetOverlap(const cInt a1, const cInt a2, const cInt b1, const cInt b2, + cInt &Left, cInt &Right) { + if (a1 < a2) { + if (b1 < b2) { + Left = std::max(a1, b1); + Right = std::min(a2, b2); + } else { + Left = std::max(a1, b2); + Right = std::min(a2, b1); + } + } else { + if (b1 < b2) { + Left = std::max(a2, b1); + Right = std::min(a1, b2); + } else { + Left = std::max(a2, b2); + Right = std::min(a1, b1); + } + } + return Left < Right; +} +//------------------------------------------------------------------------------ + +inline void UpdateOutPtIdxs(OutRec &outrec) { + OutPt *op = outrec.Pts; + do { + op->Idx = outrec.Idx; + op = op->Prev; + } while (op != outrec.Pts); +} +//------------------------------------------------------------------------------ + +void Clipper::InsertEdgeIntoAEL(TEdge *edge, TEdge *startEdge) { + if (!m_ActiveEdges) { + edge->PrevInAEL = 0; + edge->NextInAEL = 0; + m_ActiveEdges = edge; + } else if (!startEdge && E2InsertsBeforeE1(*m_ActiveEdges, *edge)) { + edge->PrevInAEL = 0; + edge->NextInAEL = m_ActiveEdges; + m_ActiveEdges->PrevInAEL = edge; + m_ActiveEdges = edge; + } else { + if (!startEdge) + startEdge = m_ActiveEdges; + while (startEdge->NextInAEL && + !E2InsertsBeforeE1(*startEdge->NextInAEL, *edge)) + startEdge = startEdge->NextInAEL; + edge->NextInAEL = startEdge->NextInAEL; + if (startEdge->NextInAEL) + startEdge->NextInAEL->PrevInAEL = edge; + edge->PrevInAEL = startEdge; + startEdge->NextInAEL = edge; + } +} +//---------------------------------------------------------------------- + +OutPt *DupOutPt(OutPt *outPt, bool InsertAfter) { + OutPt *result = new OutPt; + result->Pt = outPt->Pt; + result->Idx = outPt->Idx; + if (InsertAfter) { + result->Next = outPt->Next; + result->Prev = outPt; + outPt->Next->Prev = result; + outPt->Next = result; + } else { + result->Prev = outPt->Prev; + result->Next = outPt; + outPt->Prev->Next = result; + outPt->Prev = result; + } + return result; +} +//------------------------------------------------------------------------------ + +bool JoinHorz(OutPt *op1, OutPt *op1b, OutPt *op2, OutPt *op2b, + const IntPoint Pt, bool DiscardLeft) { + Direction Dir1 = (op1->Pt.X > op1b->Pt.X ? dRightToLeft : dLeftToRight); + Direction Dir2 = (op2->Pt.X > op2b->Pt.X ? dRightToLeft : dLeftToRight); + if (Dir1 == Dir2) + return false; + + // When DiscardLeft, we want Op1b to be on the Left of Op1, otherwise we + // want Op1b to be on the Right. (And likewise with Op2 and Op2b.) + // So, to facilitate this while inserting Op1b and Op2b ... + // when DiscardLeft, make sure we're AT or RIGHT of Pt before adding Op1b, + // otherwise make sure we're AT or LEFT of Pt. (Likewise with Op2b.) + if (Dir1 == dLeftToRight) { + while (op1->Next->Pt.X <= Pt.X && op1->Next->Pt.X >= op1->Pt.X && + op1->Next->Pt.Y == Pt.Y) + op1 = op1->Next; + if (DiscardLeft && (op1->Pt.X != Pt.X)) + op1 = op1->Next; + op1b = DupOutPt(op1, !DiscardLeft); + if (op1b->Pt != Pt) { + op1 = op1b; + op1->Pt = Pt; + op1b = DupOutPt(op1, !DiscardLeft); + } + } else { + while (op1->Next->Pt.X >= Pt.X && op1->Next->Pt.X <= op1->Pt.X && + op1->Next->Pt.Y == Pt.Y) + op1 = op1->Next; + if (!DiscardLeft && (op1->Pt.X != Pt.X)) + op1 = op1->Next; + op1b = DupOutPt(op1, DiscardLeft); + if (op1b->Pt != Pt) { + op1 = op1b; + op1->Pt = Pt; + op1b = DupOutPt(op1, DiscardLeft); + } + } + + if (Dir2 == dLeftToRight) { + while (op2->Next->Pt.X <= Pt.X && op2->Next->Pt.X >= op2->Pt.X && + op2->Next->Pt.Y == Pt.Y) + op2 = op2->Next; + if (DiscardLeft && (op2->Pt.X != Pt.X)) + op2 = op2->Next; + op2b = DupOutPt(op2, !DiscardLeft); + if (op2b->Pt != Pt) { + op2 = op2b; + op2->Pt = Pt; + op2b = DupOutPt(op2, !DiscardLeft); + }; + } else { + while (op2->Next->Pt.X >= Pt.X && op2->Next->Pt.X <= op2->Pt.X && + op2->Next->Pt.Y == Pt.Y) + op2 = op2->Next; + if (!DiscardLeft && (op2->Pt.X != Pt.X)) + op2 = op2->Next; + op2b = DupOutPt(op2, DiscardLeft); + if (op2b->Pt != Pt) { + op2 = op2b; + op2->Pt = Pt; + op2b = DupOutPt(op2, DiscardLeft); + }; + }; + + if ((Dir1 == dLeftToRight) == DiscardLeft) { + op1->Prev = op2; + op2->Next = op1; + op1b->Next = op2b; + op2b->Prev = op1b; + } else { + op1->Next = op2; + op2->Prev = op1; + op1b->Prev = op2b; + op2b->Next = op1b; + } + return true; +} +//------------------------------------------------------------------------------ + +bool Clipper::JoinPoints(Join *j, OutRec *outRec1, OutRec *outRec2) { + OutPt *op1 = j->OutPt1, *op1b; + OutPt *op2 = j->OutPt2, *op2b; + + // There are 3 kinds of joins for output polygons ... + // 1. Horizontal joins where Join.OutPt1 & Join.OutPt2 are vertices anywhere + // along (horizontal) collinear edges (& Join.OffPt is on the same + // horizontal). + // 2. Non-horizontal joins where Join.OutPt1 & Join.OutPt2 are at the same + // location at the Bottom of the overlapping segment (& Join.OffPt is above). + // 3. StrictSimple joins where edges touch but are not collinear and where + // Join.OutPt1, Join.OutPt2 & Join.OffPt all share the same point. + bool isHorizontal = (j->OutPt1->Pt.Y == j->OffPt.Y); + + if (isHorizontal && (j->OffPt == j->OutPt1->Pt) && + (j->OffPt == j->OutPt2->Pt)) { + // Strictly Simple join ... + if (outRec1 != outRec2) + return false; + op1b = j->OutPt1->Next; + while (op1b != op1 && (op1b->Pt == j->OffPt)) + op1b = op1b->Next; + bool reverse1 = (op1b->Pt.Y > j->OffPt.Y); + op2b = j->OutPt2->Next; + while (op2b != op2 && (op2b->Pt == j->OffPt)) + op2b = op2b->Next; + bool reverse2 = (op2b->Pt.Y > j->OffPt.Y); + if (reverse1 == reverse2) + return false; + if (reverse1) { + op1b = DupOutPt(op1, false); + op2b = DupOutPt(op2, true); + op1->Prev = op2; + op2->Next = op1; + op1b->Next = op2b; + op2b->Prev = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } else { + op1b = DupOutPt(op1, true); + op2b = DupOutPt(op2, false); + op1->Next = op2; + op2->Prev = op1; + op1b->Prev = op2b; + op2b->Next = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } + } else if (isHorizontal) { + // treat horizontal joins differently to non-horizontal joins since with + // them we're not yet sure where the overlapping is. OutPt1.Pt & OutPt2.Pt + // may be anywhere along the horizontal edge. + op1b = op1; + while (op1->Prev->Pt.Y == op1->Pt.Y && op1->Prev != op1b && + op1->Prev != op2) + op1 = op1->Prev; + while (op1b->Next->Pt.Y == op1b->Pt.Y && op1b->Next != op1 && + op1b->Next != op2) + op1b = op1b->Next; + if (op1b->Next == op1 || op1b->Next == op2) + return false; // a flat 'polygon' + + op2b = op2; + while (op2->Prev->Pt.Y == op2->Pt.Y && op2->Prev != op2b && + op2->Prev != op1b) + op2 = op2->Prev; + while (op2b->Next->Pt.Y == op2b->Pt.Y && op2b->Next != op2 && + op2b->Next != op1) + op2b = op2b->Next; + if (op2b->Next == op2 || op2b->Next == op1) + return false; // a flat 'polygon' + + cInt Left, Right; + // Op1 --> Op1b & Op2 --> Op2b are the extremites of the horizontal edges + if (!GetOverlap(op1->Pt.X, op1b->Pt.X, op2->Pt.X, op2b->Pt.X, Left, Right)) + return false; + + // DiscardLeftSide: when overlapping edges are joined, a spike will created + // which needs to be cleaned up. However, we don't want Op1 or Op2 caught up + // on the discard Side as either may still be needed for other joins ... + IntPoint Pt; + bool DiscardLeftSide; + if (op1->Pt.X >= Left && op1->Pt.X <= Right) { + Pt = op1->Pt; + DiscardLeftSide = (op1->Pt.X > op1b->Pt.X); + } else if (op2->Pt.X >= Left && op2->Pt.X <= Right) { + Pt = op2->Pt; + DiscardLeftSide = (op2->Pt.X > op2b->Pt.X); + } else if (op1b->Pt.X >= Left && op1b->Pt.X <= Right) { + Pt = op1b->Pt; + DiscardLeftSide = op1b->Pt.X > op1->Pt.X; + } else { + Pt = op2b->Pt; + DiscardLeftSide = (op2b->Pt.X > op2->Pt.X); + } + j->OutPt1 = op1; + j->OutPt2 = op2; + return JoinHorz(op1, op1b, op2, op2b, Pt, DiscardLeftSide); + } else { + // nb: For non-horizontal joins ... + // 1. Jr.OutPt1.Pt.Y == Jr.OutPt2.Pt.Y + // 2. Jr.OutPt1.Pt > Jr.OffPt.Y + + // make sure the polygons are correctly oriented ... + op1b = op1->Next; + while ((op1b->Pt == op1->Pt) && (op1b != op1)) + op1b = op1b->Next; + bool Reverse1 = ((op1b->Pt.Y > op1->Pt.Y) || + !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange)); + if (Reverse1) { + op1b = op1->Prev; + while ((op1b->Pt == op1->Pt) && (op1b != op1)) + op1b = op1b->Prev; + if ((op1b->Pt.Y > op1->Pt.Y) || + !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange)) + return false; + }; + op2b = op2->Next; + while ((op2b->Pt == op2->Pt) && (op2b != op2)) + op2b = op2b->Next; + bool Reverse2 = ((op2b->Pt.Y > op2->Pt.Y) || + !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange)); + if (Reverse2) { + op2b = op2->Prev; + while ((op2b->Pt == op2->Pt) && (op2b != op2)) + op2b = op2b->Prev; + if ((op2b->Pt.Y > op2->Pt.Y) || + !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange)) + return false; + } + + if ((op1b == op1) || (op2b == op2) || (op1b == op2b) || + ((outRec1 == outRec2) && (Reverse1 == Reverse2))) + return false; + + if (Reverse1) { + op1b = DupOutPt(op1, false); + op2b = DupOutPt(op2, true); + op1->Prev = op2; + op2->Next = op1; + op1b->Next = op2b; + op2b->Prev = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } else { + op1b = DupOutPt(op1, true); + op2b = DupOutPt(op2, false); + op1->Next = op2; + op2->Prev = op1; + op1b->Prev = op2b; + op2b->Next = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } + } +} +//---------------------------------------------------------------------- + +static OutRec *ParseFirstLeft(OutRec *FirstLeft) { + while (FirstLeft && !FirstLeft->Pts) + FirstLeft = FirstLeft->FirstLeft; + return FirstLeft; +} +//------------------------------------------------------------------------------ + +void Clipper::FixupFirstLefts1(OutRec *OldOutRec, OutRec *NewOutRec) { + // tests if NewOutRec contains the polygon before reassigning FirstLeft + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + OutRec *firstLeft = ParseFirstLeft(outRec->FirstLeft); + if (outRec->Pts && firstLeft == OldOutRec) { + if (Poly2ContainsPoly1(outRec->Pts, NewOutRec->Pts)) + outRec->FirstLeft = NewOutRec; + } + } +} +//---------------------------------------------------------------------- + +void Clipper::FixupFirstLefts2(OutRec *InnerOutRec, OutRec *OuterOutRec) { + // A polygon has split into two such that one is now the inner of the other. + // It's possible that these polygons now wrap around other polygons, so check + // every polygon that's also contained by OuterOutRec's FirstLeft container + //(including 0) to see if they've become inner to the new inner polygon ... + OutRec *orfl = OuterOutRec->FirstLeft; + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + + if (!outRec->Pts || outRec == OuterOutRec || outRec == InnerOutRec) + continue; + OutRec *firstLeft = ParseFirstLeft(outRec->FirstLeft); + if (firstLeft != orfl && firstLeft != InnerOutRec && + firstLeft != OuterOutRec) + continue; + if (Poly2ContainsPoly1(outRec->Pts, InnerOutRec->Pts)) + outRec->FirstLeft = InnerOutRec; + else if (Poly2ContainsPoly1(outRec->Pts, OuterOutRec->Pts)) + outRec->FirstLeft = OuterOutRec; + else if (outRec->FirstLeft == InnerOutRec || + outRec->FirstLeft == OuterOutRec) + outRec->FirstLeft = orfl; + } +} +//---------------------------------------------------------------------- +void Clipper::FixupFirstLefts3(OutRec *OldOutRec, OutRec *NewOutRec) { + // reassigns FirstLeft WITHOUT testing if NewOutRec contains the polygon + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + OutRec *firstLeft = ParseFirstLeft(outRec->FirstLeft); + if (outRec->Pts && firstLeft == OldOutRec) + outRec->FirstLeft = NewOutRec; + } +} +//---------------------------------------------------------------------- + +void Clipper::JoinCommonEdges() { + for (JoinList::size_type i = 0; i < m_Joins.size(); i++) { + Join *join = m_Joins[i]; + + OutRec *outRec1 = GetOutRec(join->OutPt1->Idx); + OutRec *outRec2 = GetOutRec(join->OutPt2->Idx); + + if (!outRec1->Pts || !outRec2->Pts) + continue; + if (outRec1->IsOpen || outRec2->IsOpen) + continue; + + // get the polygon fragment with the correct hole state (FirstLeft) + // before calling JoinPoints() ... + OutRec *holeStateRec; + if (outRec1 == outRec2) + holeStateRec = outRec1; + else if (OutRec1RightOfOutRec2(outRec1, outRec2)) + holeStateRec = outRec2; + else if (OutRec1RightOfOutRec2(outRec2, outRec1)) + holeStateRec = outRec1; + else + holeStateRec = GetLowermostRec(outRec1, outRec2); + + if (!JoinPoints(join, outRec1, outRec2)) + continue; + + if (outRec1 == outRec2) { + // instead of joining two polygons, we've just created a new one by + // splitting one polygon into two. + outRec1->Pts = join->OutPt1; + outRec1->BottomPt = 0; + outRec2 = CreateOutRec(); + outRec2->Pts = join->OutPt2; + + // update all OutRec2.Pts Idx's ... + UpdateOutPtIdxs(*outRec2); + + if (Poly2ContainsPoly1(outRec2->Pts, outRec1->Pts)) { + // outRec1 contains outRec2 ... + outRec2->IsHole = !outRec1->IsHole; + outRec2->FirstLeft = outRec1; + + if (m_UsingPolyTree) + FixupFirstLefts2(outRec2, outRec1); + + if ((outRec2->IsHole ^ m_ReverseOutput) == (Area(*outRec2) > 0)) + ReversePolyPtLinks(outRec2->Pts); + + } else if (Poly2ContainsPoly1(outRec1->Pts, outRec2->Pts)) { + // outRec2 contains outRec1 ... + outRec2->IsHole = outRec1->IsHole; + outRec1->IsHole = !outRec2->IsHole; + outRec2->FirstLeft = outRec1->FirstLeft; + outRec1->FirstLeft = outRec2; + + if (m_UsingPolyTree) + FixupFirstLefts2(outRec1, outRec2); + + if ((outRec1->IsHole ^ m_ReverseOutput) == (Area(*outRec1) > 0)) + ReversePolyPtLinks(outRec1->Pts); + } else { + // the 2 polygons are completely separate ... + outRec2->IsHole = outRec1->IsHole; + outRec2->FirstLeft = outRec1->FirstLeft; + + // fixup FirstLeft pointers that may need reassigning to OutRec2 + if (m_UsingPolyTree) + FixupFirstLefts1(outRec1, outRec2); + } + + } else { + // joined 2 polygons together ... + + outRec2->Pts = 0; + outRec2->BottomPt = 0; + outRec2->Idx = outRec1->Idx; + + outRec1->IsHole = holeStateRec->IsHole; + if (holeStateRec == outRec2) + outRec1->FirstLeft = outRec2->FirstLeft; + outRec2->FirstLeft = outRec1; + + if (m_UsingPolyTree) + FixupFirstLefts3(outRec2, outRec1); + } + } +} + +//------------------------------------------------------------------------------ +// ClipperOffset support functions ... +//------------------------------------------------------------------------------ + +DoublePoint GetUnitNormal(const IntPoint &pt1, const IntPoint &pt2) { + if (pt2.X == pt1.X && pt2.Y == pt1.Y) + return DoublePoint(0, 0); + + double Dx = (double)(pt2.X - pt1.X); + double dy = (double)(pt2.Y - pt1.Y); + double f = 1 * 1.0 / std::sqrt(Dx * Dx + dy * dy); + Dx *= f; + dy *= f; + return DoublePoint(dy, -Dx); +} + +//------------------------------------------------------------------------------ +// ClipperOffset class +//------------------------------------------------------------------------------ + +ClipperOffset::ClipperOffset(double miterLimit, double arcTolerance) { + this->MiterLimit = miterLimit; + this->ArcTolerance = arcTolerance; + m_lowest.X = -1; +} +//------------------------------------------------------------------------------ + +ClipperOffset::~ClipperOffset() { Clear(); } +//------------------------------------------------------------------------------ + +void ClipperOffset::Clear() { + for (int i = 0; i < m_polyNodes.ChildCount(); ++i) + delete m_polyNodes.Childs[i]; + m_polyNodes.Childs.clear(); + m_lowest.X = -1; +} +//------------------------------------------------------------------------------ + +void ClipperOffset::AddPath(const Path &path, JoinType joinType, + EndType endType) { + int highI = (int)path.size() - 1; + if (highI < 0) + return; + PolyNode *newNode = new PolyNode(); + newNode->m_jointype = joinType; + newNode->m_endtype = endType; + + // strip duplicate points from path and also get index to the lowest point ... + if (endType == etClosedLine || endType == etClosedPolygon) + while (highI > 0 && path[0] == path[highI]) + highI--; + newNode->Contour.reserve(highI + 1); + newNode->Contour.push_back(path[0]); + int j = 0, k = 0; + for (int i = 1; i <= highI; i++) + if (newNode->Contour[j] != path[i]) { + j++; + newNode->Contour.push_back(path[i]); + if (path[i].Y > newNode->Contour[k].Y || + (path[i].Y == newNode->Contour[k].Y && + path[i].X < newNode->Contour[k].X)) + k = j; + } + if (endType == etClosedPolygon && j < 2) { + delete newNode; + return; + } + m_polyNodes.AddChild(*newNode); + + // if this path's lowest pt is lower than all the others then update m_lowest + if (endType != etClosedPolygon) + return; + if (m_lowest.X < 0) + m_lowest = IntPoint(m_polyNodes.ChildCount() - 1, k); + else { + IntPoint ip = m_polyNodes.Childs[(int)m_lowest.X]->Contour[(int)m_lowest.Y]; + if (newNode->Contour[k].Y > ip.Y || + (newNode->Contour[k].Y == ip.Y && newNode->Contour[k].X < ip.X)) + m_lowest = IntPoint(m_polyNodes.ChildCount() - 1, k); + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::AddPaths(const Paths &paths, JoinType joinType, + EndType endType) { + for (Paths::size_type i = 0; i < paths.size(); ++i) + AddPath(paths[i], joinType, endType); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::FixOrientations() { + // fixup orientations of all closed paths if the orientation of the + // closed path with the lowermost vertex is wrong ... + if (m_lowest.X >= 0 && + !Orientation(m_polyNodes.Childs[(int)m_lowest.X]->Contour)) { + for (int i = 0; i < m_polyNodes.ChildCount(); ++i) { + PolyNode &node = *m_polyNodes.Childs[i]; + if (node.m_endtype == etClosedPolygon || + (node.m_endtype == etClosedLine && Orientation(node.Contour))) + ReversePath(node.Contour); + } + } else { + for (int i = 0; i < m_polyNodes.ChildCount(); ++i) { + PolyNode &node = *m_polyNodes.Childs[i]; + if (node.m_endtype == etClosedLine && !Orientation(node.Contour)) + ReversePath(node.Contour); + } + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::Execute(Paths &solution, double delta) { + solution.clear(); + FixOrientations(); + DoOffset(delta); + + // now clean up 'corners' ... + Clipper clpr; + clpr.AddPaths(m_destPolys, ptSubject, true); + if (delta > 0) { + clpr.Execute(ctUnion, solution, pftPositive, pftPositive); + } else { + IntRect r = clpr.GetBounds(); + Path outer(4); + outer[0] = IntPoint(r.left - 10, r.bottom + 10); + outer[1] = IntPoint(r.right + 10, r.bottom + 10); + outer[2] = IntPoint(r.right + 10, r.top - 10); + outer[3] = IntPoint(r.left - 10, r.top - 10); + + clpr.AddPath(outer, ptSubject, true); + clpr.ReverseSolution(true); + clpr.Execute(ctUnion, solution, pftNegative, pftNegative); + if (solution.size() > 0) + solution.erase(solution.begin()); + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::Execute(PolyTree &solution, double delta) { + solution.Clear(); + FixOrientations(); + DoOffset(delta); + + // now clean up 'corners' ... + Clipper clpr; + clpr.AddPaths(m_destPolys, ptSubject, true); + if (delta > 0) { + clpr.Execute(ctUnion, solution, pftPositive, pftPositive); + } else { + IntRect r = clpr.GetBounds(); + Path outer(4); + outer[0] = IntPoint(r.left - 10, r.bottom + 10); + outer[1] = IntPoint(r.right + 10, r.bottom + 10); + outer[2] = IntPoint(r.right + 10, r.top - 10); + outer[3] = IntPoint(r.left - 10, r.top - 10); + + clpr.AddPath(outer, ptSubject, true); + clpr.ReverseSolution(true); + clpr.Execute(ctUnion, solution, pftNegative, pftNegative); + // remove the outer PolyNode rectangle ... + if (solution.ChildCount() == 1 && solution.Childs[0]->ChildCount() > 0) { + PolyNode *outerNode = solution.Childs[0]; + solution.Childs.reserve(outerNode->ChildCount()); + solution.Childs[0] = outerNode->Childs[0]; + solution.Childs[0]->Parent = outerNode->Parent; + for (int i = 1; i < outerNode->ChildCount(); ++i) + solution.AddChild(*outerNode->Childs[i]); + } else + solution.Clear(); + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoOffset(double delta) { + m_destPolys.clear(); + m_delta = delta; + + // if Zero offset, just copy any CLOSED polygons to m_p and return ... + if (NEAR_ZERO(delta)) { + m_destPolys.reserve(m_polyNodes.ChildCount()); + for (int i = 0; i < m_polyNodes.ChildCount(); i++) { + PolyNode &node = *m_polyNodes.Childs[i]; + if (node.m_endtype == etClosedPolygon) + m_destPolys.push_back(node.Contour); + } + return; + } + + // see offset_triginometry3.svg in the documentation folder ... + if (MiterLimit > 2) + m_miterLim = 2 / (MiterLimit * MiterLimit); + else + m_miterLim = 0.5; + + double y; + if (ArcTolerance <= 0.0) + y = def_arc_tolerance; + else if (ArcTolerance > std::fabs(delta) * def_arc_tolerance) + y = std::fabs(delta) * def_arc_tolerance; + else + y = ArcTolerance; + // see offset_triginometry2.svg in the documentation folder ... + double steps = pi / std::acos(1 - y / std::fabs(delta)); + if (steps > std::fabs(delta) * pi) + steps = std::fabs(delta) * pi; // ie excessive precision check + m_sin = std::sin(two_pi / steps); + m_cos = std::cos(two_pi / steps); + m_StepsPerRad = steps / two_pi; + if (delta < 0.0) + m_sin = -m_sin; + + m_destPolys.reserve(m_polyNodes.ChildCount() * 2); + for (int i = 0; i < m_polyNodes.ChildCount(); i++) { + PolyNode &node = *m_polyNodes.Childs[i]; + m_srcPoly = node.Contour; + + int len = (int)m_srcPoly.size(); + if (len == 0 || + (delta <= 0 && (len < 3 || node.m_endtype != etClosedPolygon))) + continue; + + m_destPoly.clear(); + if (len == 1) { + if (node.m_jointype == jtRound) { + double X = 1.0, Y = 0.0; + for (cInt j = 1; j <= steps; j++) { + m_destPoly.push_back(IntPoint(Round(m_srcPoly[0].X + X * delta), + Round(m_srcPoly[0].Y + Y * delta))); + double X2 = X; + X = X * m_cos - m_sin * Y; + Y = X2 * m_sin + Y * m_cos; + } + } else { + double X = -1.0, Y = -1.0; + for (int j = 0; j < 4; ++j) { + m_destPoly.push_back(IntPoint(Round(m_srcPoly[0].X + X * delta), + Round(m_srcPoly[0].Y + Y * delta))); + if (X < 0) + X = 1; + else if (Y < 0) + Y = 1; + else + X = -1; + } + } + m_destPolys.push_back(m_destPoly); + continue; + } + // build m_normals ... + m_normals.clear(); + m_normals.reserve(len); + for (int j = 0; j < len - 1; ++j) + m_normals.push_back(GetUnitNormal(m_srcPoly[j], m_srcPoly[j + 1])); + if (node.m_endtype == etClosedLine || node.m_endtype == etClosedPolygon) + m_normals.push_back(GetUnitNormal(m_srcPoly[len - 1], m_srcPoly[0])); + else + m_normals.push_back(DoublePoint(m_normals[len - 2])); + + if (node.m_endtype == etClosedPolygon) { + int k = len - 1; + for (int j = 0; j < len; ++j) + OffsetPoint(j, k, node.m_jointype); + m_destPolys.push_back(m_destPoly); + } else if (node.m_endtype == etClosedLine) { + int k = len - 1; + for (int j = 0; j < len; ++j) + OffsetPoint(j, k, node.m_jointype); + m_destPolys.push_back(m_destPoly); + m_destPoly.clear(); + // re-build m_normals ... + DoublePoint n = m_normals[len - 1]; + for (int j = len - 1; j > 0; j--) + m_normals[j] = DoublePoint(-m_normals[j - 1].X, -m_normals[j - 1].Y); + m_normals[0] = DoublePoint(-n.X, -n.Y); + k = 0; + for (int j = len - 1; j >= 0; j--) + OffsetPoint(j, k, node.m_jointype); + m_destPolys.push_back(m_destPoly); + } else { + int k = 0; + for (int j = 1; j < len - 1; ++j) + OffsetPoint(j, k, node.m_jointype); + + IntPoint pt1; + if (node.m_endtype == etOpenButt) { + int j = len - 1; + pt1 = IntPoint((cInt)Round(m_srcPoly[j].X + m_normals[j].X * delta), + (cInt)Round(m_srcPoly[j].Y + m_normals[j].Y * delta)); + m_destPoly.push_back(pt1); + pt1 = IntPoint((cInt)Round(m_srcPoly[j].X - m_normals[j].X * delta), + (cInt)Round(m_srcPoly[j].Y - m_normals[j].Y * delta)); + m_destPoly.push_back(pt1); + } else { + int j = len - 1; + k = len - 2; + m_sinA = 0; + m_normals[j] = DoublePoint(-m_normals[j].X, -m_normals[j].Y); + if (node.m_endtype == etOpenSquare) + DoSquare(j, k); + else + DoRound(j, k); + } + + // re-build m_normals ... + for (int j = len - 1; j > 0; j--) + m_normals[j] = DoublePoint(-m_normals[j - 1].X, -m_normals[j - 1].Y); + m_normals[0] = DoublePoint(-m_normals[1].X, -m_normals[1].Y); + + k = len - 1; + for (int j = k - 1; j > 0; --j) + OffsetPoint(j, k, node.m_jointype); + + if (node.m_endtype == etOpenButt) { + pt1 = IntPoint((cInt)Round(m_srcPoly[0].X - m_normals[0].X * delta), + (cInt)Round(m_srcPoly[0].Y - m_normals[0].Y * delta)); + m_destPoly.push_back(pt1); + pt1 = IntPoint((cInt)Round(m_srcPoly[0].X + m_normals[0].X * delta), + (cInt)Round(m_srcPoly[0].Y + m_normals[0].Y * delta)); + m_destPoly.push_back(pt1); + } else { + k = 1; + m_sinA = 0; + if (node.m_endtype == etOpenSquare) + DoSquare(0, 1); + else + DoRound(0, 1); + } + m_destPolys.push_back(m_destPoly); + } + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::OffsetPoint(int j, int &k, JoinType jointype) { + // cross product ... + m_sinA = (m_normals[k].X * m_normals[j].Y - m_normals[j].X * m_normals[k].Y); + if (std::fabs(m_sinA * m_delta) < 1.0) { + // dot product ... + double cosA = + (m_normals[k].X * m_normals[j].X + m_normals[j].Y * m_normals[k].Y); + if (cosA > 0) // angle => 0 degrees + { + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + m_normals[k].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[k].Y * m_delta))); + return; + } + // else angle => 180 degrees + } else if (m_sinA > 1.0) + m_sinA = 1.0; + else if (m_sinA < -1.0) + m_sinA = -1.0; + + if (m_sinA * m_delta < 0) { + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + m_normals[k].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[k].Y * m_delta))); + m_destPoly.push_back(m_srcPoly[j]); + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + m_normals[j].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[j].Y * m_delta))); + } else + switch (jointype) { + case jtMiter: { + double r = 1 + (m_normals[j].X * m_normals[k].X + + m_normals[j].Y * m_normals[k].Y); + if (r >= m_miterLim) + DoMiter(j, k, r); + else + DoSquare(j, k); + break; + } + case jtSquare: + DoSquare(j, k); + break; + case jtRound: + DoRound(j, k); + break; + } + k = j; +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoSquare(int j, int k) { + double dx = std::tan(std::atan2(m_sinA, m_normals[k].X * m_normals[j].X + + m_normals[k].Y * m_normals[j].Y) / + 4); + m_destPoly.push_back(IntPoint( + Round(m_srcPoly[j].X + m_delta * (m_normals[k].X - m_normals[k].Y * dx)), + Round(m_srcPoly[j].Y + + m_delta * (m_normals[k].Y + m_normals[k].X * dx)))); + m_destPoly.push_back(IntPoint( + Round(m_srcPoly[j].X + m_delta * (m_normals[j].X + m_normals[j].Y * dx)), + Round(m_srcPoly[j].Y + + m_delta * (m_normals[j].Y - m_normals[j].X * dx)))); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoMiter(int j, int k, double r) { + double q = m_delta / r; + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + (m_normals[k].X + m_normals[j].X) * q), + Round(m_srcPoly[j].Y + (m_normals[k].Y + m_normals[j].Y) * q))); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoRound(int j, int k) { + double a = std::atan2(m_sinA, m_normals[k].X * m_normals[j].X + + m_normals[k].Y * m_normals[j].Y); + int steps = std::max((int)Round(m_StepsPerRad * std::fabs(a)), 1); + + double X = m_normals[k].X, Y = m_normals[k].Y, X2; + for (int i = 0; i < steps; ++i) { + m_destPoly.push_back(IntPoint(Round(m_srcPoly[j].X + X * m_delta), + Round(m_srcPoly[j].Y + Y * m_delta))); + X2 = X; + X = X * m_cos - m_sin * Y; + Y = X2 * m_sin + Y * m_cos; + } + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + m_normals[j].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[j].Y * m_delta))); +} + +//------------------------------------------------------------------------------ +// Miscellaneous public functions +//------------------------------------------------------------------------------ + +void Clipper::DoSimplePolygons() { + PolyOutList::size_type i = 0; + while (i < m_PolyOuts.size()) { + OutRec *outrec = m_PolyOuts[i++]; + OutPt *op = outrec->Pts; + if (!op || outrec->IsOpen) + continue; + do // for each Pt in Polygon until duplicate found do ... + { + OutPt *op2 = op->Next; + while (op2 != outrec->Pts) { + if ((op->Pt == op2->Pt) && op2->Next != op && op2->Prev != op) { + // split the polygon into two ... + OutPt *op3 = op->Prev; + OutPt *op4 = op2->Prev; + op->Prev = op4; + op4->Next = op; + op2->Prev = op3; + op3->Next = op2; + + outrec->Pts = op; + OutRec *outrec2 = CreateOutRec(); + outrec2->Pts = op2; + UpdateOutPtIdxs(*outrec2); + if (Poly2ContainsPoly1(outrec2->Pts, outrec->Pts)) { + // OutRec2 is contained by OutRec1 ... + outrec2->IsHole = !outrec->IsHole; + outrec2->FirstLeft = outrec; + if (m_UsingPolyTree) + FixupFirstLefts2(outrec2, outrec); + } else if (Poly2ContainsPoly1(outrec->Pts, outrec2->Pts)) { + // OutRec1 is contained by OutRec2 ... + outrec2->IsHole = outrec->IsHole; + outrec->IsHole = !outrec2->IsHole; + outrec2->FirstLeft = outrec->FirstLeft; + outrec->FirstLeft = outrec2; + if (m_UsingPolyTree) + FixupFirstLefts2(outrec, outrec2); + } else { + // the 2 polygons are separate ... + outrec2->IsHole = outrec->IsHole; + outrec2->FirstLeft = outrec->FirstLeft; + if (m_UsingPolyTree) + FixupFirstLefts1(outrec, outrec2); + } + op2 = op; // ie get ready for the Next iteration + } + op2 = op2->Next; + } + op = op->Next; + } while (op != outrec->Pts); + } +} +//------------------------------------------------------------------------------ + +void ReversePath(Path &p) { std::reverse(p.begin(), p.end()); } +//------------------------------------------------------------------------------ + +void ReversePaths(Paths &p) { + for (Paths::size_type i = 0; i < p.size(); ++i) + ReversePath(p[i]); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygon(const Path &in_poly, Paths &out_polys, + PolyFillType fillType) { + Clipper c; + c.StrictlySimple(true); + c.AddPath(in_poly, ptSubject, true); + c.Execute(ctUnion, out_polys, fillType, fillType); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygons(const Paths &in_polys, Paths &out_polys, + PolyFillType fillType) { + Clipper c; + c.StrictlySimple(true); + c.AddPaths(in_polys, ptSubject, true); + c.Execute(ctUnion, out_polys, fillType, fillType); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygons(Paths &polys, PolyFillType fillType) { + SimplifyPolygons(polys, polys, fillType); +} +//------------------------------------------------------------------------------ + +inline double DistanceSqrd(const IntPoint &pt1, const IntPoint &pt2) { + double Dx = ((double)pt1.X - pt2.X); + double dy = ((double)pt1.Y - pt2.Y); + return (Dx * Dx + dy * dy); +} +//------------------------------------------------------------------------------ + +double DistanceFromLineSqrd(const IntPoint &pt, const IntPoint &ln1, + const IntPoint &ln2) { + // The equation of a line in general form (Ax + By + C = 0) + // given 2 points (x¹,y¹) & (x²,y²) is ... + //(y¹ - y²)x + (x² - x¹)y + (y² - y¹)x¹ - (x² - x¹)y¹ = 0 + // A = (y¹ - y²); B = (x² - x¹); C = (y² - y¹)x¹ - (x² - x¹)y¹ + // perpendicular distance of point (x³,y³) = (Ax³ + By³ + C)/Sqrt(A² + B²) + // see http://en.wikipedia.org/wiki/Perpendicular_distance + double A = double(ln1.Y - ln2.Y); + double B = double(ln2.X - ln1.X); + double C = A * ln1.X + B * ln1.Y; + C = A * pt.X + B * pt.Y - C; + return (C * C) / (A * A + B * B); +} +//--------------------------------------------------------------------------- + +bool SlopesNearCollinear(const IntPoint &pt1, const IntPoint &pt2, + const IntPoint &pt3, double distSqrd) { + // this function is more accurate when the point that's geometrically + // between the other 2 points is the one that's tested for distance. + // ie makes it more likely to pick up 'spikes' ... + if (Abs(pt1.X - pt2.X) > Abs(pt1.Y - pt2.Y)) { + if ((pt1.X > pt2.X) == (pt1.X < pt3.X)) + return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; + else if ((pt2.X > pt1.X) == (pt2.X < pt3.X)) + return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; + else + return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; + } else { + if ((pt1.Y > pt2.Y) == (pt1.Y < pt3.Y)) + return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; + else if ((pt2.Y > pt1.Y) == (pt2.Y < pt3.Y)) + return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; + else + return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; + } +} +//------------------------------------------------------------------------------ + +bool PointsAreClose(IntPoint pt1, IntPoint pt2, double distSqrd) { + double Dx = (double)pt1.X - pt2.X; + double dy = (double)pt1.Y - pt2.Y; + return ((Dx * Dx) + (dy * dy) <= distSqrd); +} +//------------------------------------------------------------------------------ + +OutPt *ExcludeOp(OutPt *op) { + OutPt *result = op->Prev; + result->Next = op->Next; + op->Next->Prev = result; + result->Idx = 0; + return result; +} +//------------------------------------------------------------------------------ + +void CleanPolygon(const Path &in_poly, Path &out_poly, double distance) { + // distance = proximity in units/pixels below which vertices + // will be stripped. Default ~= sqrt(2). + + size_t size = in_poly.size(); + + if (size == 0) { + out_poly.clear(); + return; + } + + OutPt *outPts = new OutPt[size]; + for (size_t i = 0; i < size; ++i) { + outPts[i].Pt = in_poly[i]; + outPts[i].Next = &outPts[(i + 1) % size]; + outPts[i].Next->Prev = &outPts[i]; + outPts[i].Idx = 0; + } + + double distSqrd = distance * distance; + OutPt *op = &outPts[0]; + while (op->Idx == 0 && op->Next != op->Prev) { + if (PointsAreClose(op->Pt, op->Prev->Pt, distSqrd)) { + op = ExcludeOp(op); + size--; + } else if (PointsAreClose(op->Prev->Pt, op->Next->Pt, distSqrd)) { + ExcludeOp(op->Next); + op = ExcludeOp(op); + size -= 2; + } else if (SlopesNearCollinear(op->Prev->Pt, op->Pt, op->Next->Pt, + distSqrd)) { + op = ExcludeOp(op); + size--; + } else { + op->Idx = 1; + op = op->Next; + } + } + + if (size < 3) + size = 0; + out_poly.resize(size); + for (size_t i = 0; i < size; ++i) { + out_poly[i] = op->Pt; + op = op->Next; + } + delete[] outPts; +} +//------------------------------------------------------------------------------ + +void CleanPolygon(Path &poly, double distance) { + CleanPolygon(poly, poly, distance); +} +//------------------------------------------------------------------------------ + +void CleanPolygons(const Paths &in_polys, Paths &out_polys, double distance) { + out_polys.resize(in_polys.size()); + for (Paths::size_type i = 0; i < in_polys.size(); ++i) + CleanPolygon(in_polys[i], out_polys[i], distance); +} +//------------------------------------------------------------------------------ + +void CleanPolygons(Paths &polys, double distance) { + CleanPolygons(polys, polys, distance); +} +//------------------------------------------------------------------------------ + +void Minkowski(const Path &poly, const Path &path, Paths &solution, bool isSum, + bool isClosed) { + int delta = (isClosed ? 1 : 0); + size_t polyCnt = poly.size(); + size_t pathCnt = path.size(); + Paths pp; + pp.reserve(pathCnt); + if (isSum) + for (size_t i = 0; i < pathCnt; ++i) { + Path p; + p.reserve(polyCnt); + for (size_t j = 0; j < poly.size(); ++j) + p.push_back(IntPoint(path[i].X + poly[j].X, path[i].Y + poly[j].Y)); + pp.push_back(p); + } + else + for (size_t i = 0; i < pathCnt; ++i) { + Path p; + p.reserve(polyCnt); + for (size_t j = 0; j < poly.size(); ++j) + p.push_back(IntPoint(path[i].X - poly[j].X, path[i].Y - poly[j].Y)); + pp.push_back(p); + } + + solution.clear(); + solution.reserve((pathCnt + delta) * (polyCnt + 1)); + for (size_t i = 0; i < pathCnt - 1 + delta; ++i) + for (size_t j = 0; j < polyCnt; ++j) { + Path quad; + quad.reserve(4); + quad.push_back(pp[i % pathCnt][j % polyCnt]); + quad.push_back(pp[(i + 1) % pathCnt][j % polyCnt]); + quad.push_back(pp[(i + 1) % pathCnt][(j + 1) % polyCnt]); + quad.push_back(pp[i % pathCnt][(j + 1) % polyCnt]); + if (!Orientation(quad)) + ReversePath(quad); + solution.push_back(quad); + } +} +//------------------------------------------------------------------------------ + +void MinkowskiSum(const Path &pattern, const Path &path, Paths &solution, + bool pathIsClosed) { + Minkowski(pattern, path, solution, true, pathIsClosed); + Clipper c; + c.AddPaths(solution, ptSubject, true); + c.Execute(ctUnion, solution, pftNonZero, pftNonZero); +} +//------------------------------------------------------------------------------ + +void TranslatePath(const Path &input, Path &output, const IntPoint delta) { + // precondition: input != output + output.resize(input.size()); + for (size_t i = 0; i < input.size(); ++i) + output[i] = IntPoint(input[i].X + delta.X, input[i].Y + delta.Y); +} +//------------------------------------------------------------------------------ + +void MinkowskiSum(const Path &pattern, const Paths &paths, Paths &solution, + bool pathIsClosed) { + Clipper c; + for (size_t i = 0; i < paths.size(); ++i) { + Paths tmp; + Minkowski(pattern, paths[i], tmp, true, pathIsClosed); + c.AddPaths(tmp, ptSubject, true); + if (pathIsClosed) { + Path tmp2; + TranslatePath(paths[i], tmp2, pattern[0]); + c.AddPath(tmp2, ptClip, true); + } + } + c.Execute(ctUnion, solution, pftNonZero, pftNonZero); +} +//------------------------------------------------------------------------------ + +void MinkowskiDiff(const Path &poly1, const Path &poly2, Paths &solution) { + Minkowski(poly1, poly2, solution, false, true); + Clipper c; + c.AddPaths(solution, ptSubject, true); + c.Execute(ctUnion, solution, pftNonZero, pftNonZero); +} +//------------------------------------------------------------------------------ + +enum NodeType { ntAny, ntOpen, ntClosed }; + +void AddPolyNodeToPaths(const PolyNode &polynode, NodeType nodetype, + Paths &paths) { + bool match = true; + if (nodetype == ntClosed) + match = !polynode.IsOpen(); + else if (nodetype == ntOpen) + return; + + if (!polynode.Contour.empty() && match) + paths.push_back(polynode.Contour); + for (int i = 0; i < polynode.ChildCount(); ++i) + AddPolyNodeToPaths(*polynode.Childs[i], nodetype, paths); +} +//------------------------------------------------------------------------------ + +void PolyTreeToPaths(const PolyTree &polytree, Paths &paths) { + paths.resize(0); + paths.reserve(polytree.Total()); + AddPolyNodeToPaths(polytree, ntAny, paths); +} +//------------------------------------------------------------------------------ + +void ClosedPathsFromPolyTree(const PolyTree &polytree, Paths &paths) { + paths.resize(0); + paths.reserve(polytree.Total()); + AddPolyNodeToPaths(polytree, ntClosed, paths); +} +//------------------------------------------------------------------------------ + +void OpenPathsFromPolyTree(PolyTree &polytree, Paths &paths) { + paths.resize(0); + paths.reserve(polytree.Total()); + // Open paths are top level only, so ... + for (int i = 0; i < polytree.ChildCount(); ++i) + if (polytree.Childs[i]->IsOpen()) + paths.push_back(polytree.Childs[i]->Contour); +} +//------------------------------------------------------------------------------ + +std::ostream &operator<<(std::ostream &s, const IntPoint &p) { + s << "(" << p.X << "," << p.Y << ")"; + return s; +} +//------------------------------------------------------------------------------ + +std::ostream &operator<<(std::ostream &s, const Path &p) { + if (p.empty()) + return s; + Path::size_type last = p.size() - 1; + for (Path::size_type i = 0; i < last; i++) + s << "(" << p[i].X << "," << p[i].Y << "), "; + s << "(" << p[last].X << "," << p[last].Y << ")\n"; + return s; +} +//------------------------------------------------------------------------------ + +std::ostream &operator<<(std::ostream &s, const Paths &p) { + for (Paths::size_type i = 0; i < p.size(); i++) + s << p[i]; + s << "\n"; + return s; +} +//------------------------------------------------------------------------------ + +} // namespace ClipperLib diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/clipper.hpp b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/clipper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..68bc80bdc77fa810f99ae68487feb6a5af9f4a91 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/clipper.hpp @@ -0,0 +1,419 @@ +/******************************************************************************* + * * + * Author : Angus Johnson * Version : 6.4.2 * Date : 27 February + *2017 * Website : + *http://www.angusj.com * Copyright : + *Angus Johnson 2010-2017 * + * * + * License: * Use, modification & distribution is subject to Boost Software + *License Ver 1. * http://www.boost.org/LICENSE_1_0.txt * + * * + * Attributions: * The code in this library is an extension of Bala Vatti's + *clipping algorithm: * "A generic solution to polygon clipping" * + * Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * + * http://portal.acm.org/citation.cfm?id=129906 * + * * + * Computer graphics and geometric modeling: implementation and algorithms * By + *Max K. Agoston * + * Springer; 1 edition (January 4, 2005) * + * http://books.google.com/books?q=vatti+clipping+agoston * + * * + * See also: * "Polygon Offsetting by Computing Winding Numbers" * Paper no. + *DETC2005-85513 pp. 565-575 * ASME 2005 + *International Design Engineering Technical Conferences * and + *Computers and Information in Engineering Conference (IDETC/CIE2005) * + * September 24-28, 2005 , Long Beach, California, USA * + * http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * + * * + *******************************************************************************/ + +#ifndef clipper_hpp +#define clipper_hpp + +#define CLIPPER_VERSION "6.4.2" + +// use_int32: When enabled 32bit ints are used instead of 64bit ints. This +// improve performance but coordinate values are limited to the range +/- 46340 +// #define use_int32 + +// use_xyz: adds a Z member to IntPoint. Adds a minor cost to perfomance. +// #define use_xyz + +// use_lines: Enables line clipping. Adds a very minor cost to performance. +#define use_lines + +// use_deprecated: Enables temporary support for the obsolete functions +// #define use_deprecated + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ClipperLib { + +enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor }; +enum PolyType { ptSubject, ptClip }; +// By far the most widely used winding rules for polygon filling are +// EvenOdd & NonZero (GDI, GDI+, XLib, OpenGL, Cairo, AGG, Quartz, SVG, Gr32) +// Others rules include Positive, Negative and ABS_GTR_EQ_TWO (only in OpenGL) +// see http://glprogramming.com/red/chapter11.html +enum PolyFillType { pftEvenOdd, pftNonZero, pftPositive, pftNegative }; + +#ifdef use_int32 +typedef int cInt; +static cInt const loRange = 0x7FFF; +static cInt const hiRange = 0x7FFF; +#else +typedef signed long long cInt; +static cInt const loRange = 0x3FFFFFFF; +static cInt const hiRange = 0x3FFFFFFFFFFFFFFFLL; +typedef signed long long long64; // used by Int128 class +typedef unsigned long long ulong64; + +#endif + +struct IntPoint { + cInt X; + cInt Y; +#ifdef use_xyz + cInt Z; + IntPoint(cInt x = 0, cInt y = 0, cInt z = 0) : X(x), Y(y), Z(z){}; +#else + IntPoint(cInt x = 0, cInt y = 0) : X(x), Y(y){}; +#endif + + friend inline bool operator==(const IntPoint &a, const IntPoint &b) { + return a.X == b.X && a.Y == b.Y; + } + friend inline bool operator!=(const IntPoint &a, const IntPoint &b) { + return a.X != b.X || a.Y != b.Y; + } +}; +//------------------------------------------------------------------------------ + +typedef std::vector Path; +typedef std::vector Paths; + +inline Path &operator<<(Path &poly, const IntPoint &p) { + poly.push_back(p); + return poly; +} +inline Paths &operator<<(Paths &polys, const Path &p) { + polys.push_back(p); + return polys; +} + +std::ostream &operator<<(std::ostream &s, const IntPoint &p); +std::ostream &operator<<(std::ostream &s, const Path &p); +std::ostream &operator<<(std::ostream &s, const Paths &p); + +struct DoublePoint { + double X; + double Y; + DoublePoint(double x = 0, double y = 0) : X(x), Y(y) {} + DoublePoint(IntPoint ip) : X((double)ip.X), Y((double)ip.Y) {} +}; +//------------------------------------------------------------------------------ + +#ifdef use_xyz +typedef void (*ZFillCallback)(IntPoint &e1bot, IntPoint &e1top, IntPoint &e2bot, + IntPoint &e2top, IntPoint &pt); +#endif + +enum InitOptions { + ioReverseSolution = 1, + ioStrictlySimple = 2, + ioPreserveCollinear = 4 +}; +enum JoinType { jtSquare, jtRound, jtMiter }; +enum EndType { + etClosedPolygon, + etClosedLine, + etOpenButt, + etOpenSquare, + etOpenRound +}; + +class PolyNode; +typedef std::vector PolyNodes; + +class PolyNode { +public: + PolyNode(); + virtual ~PolyNode(){}; + Path Contour; + PolyNodes Childs; + PolyNode *Parent; + PolyNode *GetNext() const; + bool IsHole() const; + bool IsOpen() const; + int ChildCount() const; + +private: + // PolyNode& operator =(PolyNode& other); + unsigned Index; // node index in Parent.Childs + bool m_IsOpen; + JoinType m_jointype; + EndType m_endtype; + PolyNode *GetNextSiblingUp() const; + void AddChild(PolyNode &child); + friend class Clipper; // to access Index + friend class ClipperOffset; +}; + +class PolyTree : public PolyNode { +public: + ~PolyTree() { Clear(); }; + PolyNode *GetFirst() const; + void Clear(); + int Total() const; + +private: + // PolyTree& operator =(PolyTree& other); + PolyNodes AllNodes; + friend class Clipper; // to access AllNodes +}; + +bool Orientation(const Path &poly); +double Area(const Path &poly); +int PointInPolygon(const IntPoint &pt, const Path &path); + +void SimplifyPolygon(const Path &in_poly, Paths &out_polys, + PolyFillType fillType = pftEvenOdd); +void SimplifyPolygons(const Paths &in_polys, Paths &out_polys, + PolyFillType fillType = pftEvenOdd); +void SimplifyPolygons(Paths &polys, PolyFillType fillType = pftEvenOdd); + +void CleanPolygon(const Path &in_poly, Path &out_poly, double distance = 1.415); +void CleanPolygon(Path &poly, double distance = 1.415); +void CleanPolygons(const Paths &in_polys, Paths &out_polys, + double distance = 1.415); +void CleanPolygons(Paths &polys, double distance = 1.415); + +void MinkowskiSum(const Path &pattern, const Path &path, Paths &solution, + bool pathIsClosed); +void MinkowskiSum(const Path &pattern, const Paths &paths, Paths &solution, + bool pathIsClosed); +void MinkowskiDiff(const Path &poly1, const Path &poly2, Paths &solution); + +void PolyTreeToPaths(const PolyTree &polytree, Paths &paths); +void ClosedPathsFromPolyTree(const PolyTree &polytree, Paths &paths); +void OpenPathsFromPolyTree(PolyTree &polytree, Paths &paths); + +void ReversePath(Path &p); +void ReversePaths(Paths &p); + +struct IntRect { + cInt left; + cInt top; + cInt right; + cInt bottom; +}; + +// enums that are used internally ... +enum EdgeSide { esLeft = 1, esRight = 2 }; + +// forward declarations (for stuff used internally) ... +struct TEdge; +struct IntersectNode; +struct LocalMinimum; +struct OutPt; +struct OutRec; +struct Join; + +typedef std::vector PolyOutList; +typedef std::vector EdgeList; +typedef std::vector JoinList; +typedef std::vector IntersectList; + +//------------------------------------------------------------------------------ + +// ClipperBase is the ancestor to the Clipper class. It should not be +// instantiated directly. This class simply abstracts the conversion of sets of +// polygon coordinates into edge objects that are stored in a LocalMinima list. +class ClipperBase { +public: + ClipperBase(); + virtual ~ClipperBase(); + virtual bool AddPath(const Path &pg, PolyType PolyTyp, bool Closed); + bool AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed); + virtual void Clear(); + IntRect GetBounds(); + bool PreserveCollinear() { return m_PreserveCollinear; }; + void PreserveCollinear(bool value) { m_PreserveCollinear = value; }; + +protected: + void DisposeLocalMinimaList(); + TEdge *AddBoundsToLML(TEdge *e, bool IsClosed); + virtual void Reset(); + TEdge *ProcessBound(TEdge *E, bool IsClockwise); + void InsertScanbeam(const cInt Y); + bool PopScanbeam(cInt &Y); + bool LocalMinimaPending(); + bool PopLocalMinima(cInt Y, const LocalMinimum *&locMin); + OutRec *CreateOutRec(); + void DisposeAllOutRecs(); + void DisposeOutRec(PolyOutList::size_type index); + void SwapPositionsInAEL(TEdge *edge1, TEdge *edge2); + void DeleteFromAEL(TEdge *e); + void UpdateEdgeIntoAEL(TEdge *&e); + + typedef std::vector MinimaList; + MinimaList::iterator m_CurrentLM; + MinimaList m_MinimaList; + + bool m_UseFullRange; + EdgeList m_edges; + bool m_PreserveCollinear; + bool m_HasOpenPaths; + PolyOutList m_PolyOuts; + TEdge *m_ActiveEdges; + + typedef std::priority_queue ScanbeamList; + ScanbeamList m_Scanbeam; +}; +//------------------------------------------------------------------------------ + +class Clipper : public virtual ClipperBase { +public: + Clipper(int initOptions = 0); + bool Execute(ClipType clipType, Paths &solution, + PolyFillType fillType = pftEvenOdd); + bool Execute(ClipType clipType, Paths &solution, PolyFillType subjFillType, + PolyFillType clipFillType); + bool Execute(ClipType clipType, PolyTree &polytree, + PolyFillType fillType = pftEvenOdd); + bool Execute(ClipType clipType, PolyTree &polytree, PolyFillType subjFillType, + PolyFillType clipFillType); + bool ReverseSolution() { return m_ReverseOutput; }; + void ReverseSolution(bool value) { m_ReverseOutput = value; }; + bool StrictlySimple() { return m_StrictSimple; }; + void StrictlySimple(bool value) { m_StrictSimple = value; }; + // set the callback function for z value filling on intersections (otherwise + // Z is 0) +#ifdef use_xyz + void ZFillFunction(ZFillCallback zFillFunc); +#endif +protected: + virtual bool ExecuteInternal(); + +private: + JoinList m_Joins; + JoinList m_GhostJoins; + IntersectList m_IntersectList; + ClipType m_ClipType; + typedef std::list MaximaList; + MaximaList m_Maxima; + TEdge *m_SortedEdges; + bool m_ExecuteLocked; + PolyFillType m_ClipFillType; + PolyFillType m_SubjFillType; + bool m_ReverseOutput; + bool m_UsingPolyTree; + bool m_StrictSimple; +#ifdef use_xyz + ZFillCallback m_ZFill; // custom callback +#endif + void SetWindingCount(TEdge &edge); + bool IsEvenOddFillType(const TEdge &edge) const; + bool IsEvenOddAltFillType(const TEdge &edge) const; + void InsertLocalMinimaIntoAEL(const cInt botY); + void InsertEdgeIntoAEL(TEdge *edge, TEdge *startEdge); + void AddEdgeToSEL(TEdge *edge); + bool PopEdgeFromSEL(TEdge *&edge); + void CopyAELToSEL(); + void DeleteFromSEL(TEdge *e); + void SwapPositionsInSEL(TEdge *edge1, TEdge *edge2); + bool IsContributing(const TEdge &edge) const; + bool IsTopHorz(const cInt XPos); + void DoMaxima(TEdge *e); + void ProcessHorizontals(); + void ProcessHorizontal(TEdge *horzEdge); + void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); + OutPt *AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); + OutRec *GetOutRec(int idx); + void AppendPolygon(TEdge *e1, TEdge *e2); + void IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &pt); + OutPt *AddOutPt(TEdge *e, const IntPoint &pt); + OutPt *GetLastOutPt(TEdge *e); + bool ProcessIntersections(const cInt topY); + void BuildIntersectList(const cInt topY); + void ProcessIntersectList(); + void ProcessEdgesAtTopOfScanbeam(const cInt topY); + void BuildResult(Paths &polys); + void BuildResult2(PolyTree &polytree); + void SetHoleState(TEdge *e, OutRec *outrec); + void DisposeIntersectNodes(); + bool FixupIntersectionOrder(); + void FixupOutPolygon(OutRec &outrec); + void FixupOutPolyline(OutRec &outrec); + bool IsHole(TEdge *e); + bool FindOwnerFromSplitRecs(OutRec &outRec, OutRec *&currOrfl); + void FixHoleLinkage(OutRec &outrec); + void AddJoin(OutPt *op1, OutPt *op2, const IntPoint offPt); + void ClearJoins(); + void ClearGhostJoins(); + void AddGhostJoin(OutPt *op, const IntPoint offPt); + bool JoinPoints(Join *j, OutRec *outRec1, OutRec *outRec2); + void JoinCommonEdges(); + void DoSimplePolygons(); + void FixupFirstLefts1(OutRec *OldOutRec, OutRec *NewOutRec); + void FixupFirstLefts2(OutRec *InnerOutRec, OutRec *OuterOutRec); + void FixupFirstLefts3(OutRec *OldOutRec, OutRec *NewOutRec); +#ifdef use_xyz + void SetZ(IntPoint &pt, TEdge &e1, TEdge &e2); +#endif +}; +//------------------------------------------------------------------------------ + +class ClipperOffset { +public: + ClipperOffset(double miterLimit = 2.0, double roundPrecision = 0.25); + ~ClipperOffset(); + void AddPath(const Path &path, JoinType joinType, EndType endType); + void AddPaths(const Paths &paths, JoinType joinType, EndType endType); + void Execute(Paths &solution, double delta); + void Execute(PolyTree &solution, double delta); + void Clear(); + double MiterLimit; + double ArcTolerance; + +private: + Paths m_destPolys; + Path m_srcPoly; + Path m_destPoly; + std::vector m_normals; + double m_delta, m_sinA, m_sin, m_cos; + double m_miterLim, m_StepsPerRad; + IntPoint m_lowest; + PolyNode m_polyNodes; + + void FixOrientations(); + void DoOffset(double delta); + void OffsetPoint(int j, int &k, JoinType jointype); + void DoSquare(int j, int k); + void DoMiter(int j, int k, double r); + void DoRound(int j, int k); +}; +//------------------------------------------------------------------------------ + +class clipperException : public std::exception { +public: + clipperException(const char *description) : m_descr(description) {} + virtual ~clipperException() throw() {} + virtual const char *what() const throw() { return m_descr.c_str(); } + +private: + std::string m_descr; +}; +//------------------------------------------------------------------------------ + +} // namespace ClipperLib + +#endif // clipper_hpp diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ViewModels/OCRViewModel.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ViewModels/OCRViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..5e405a8142045889992111a3a2e60b04e077a77a --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ViewModels/OCRViewModel.swift @@ -0,0 +1,275 @@ +// Copyright (c) 2026 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. + +import Foundation +import UIKit + +// MARK: - App State + +/// The complete set of states the app can be in. +/// Drives all UI rendering via a single `@Published` property on `OCRViewModel`. +enum AppState: Equatable { + /// Models are being loaded from the bundle. + case loadingModels + /// Models are loaded and ready; waiting for the user to select an image. + case ready + /// An image is being processed by OCR. + case processing(UIImage) + /// OCR completed successfully with results and the processed image. + /// `runId` distinguishes successive successful runs for ``Equatable`` without requiring ``OCRRunResult`` to conform. + case results(OCRRunResult, UIImage, runId: UUID) + /// An error occurred during model loading or inference. + case error(AppError) + + static func == (lhs: AppState, rhs: AppState) -> Bool { + switch (lhs, rhs) { + case (.loadingModels, .loadingModels): return true + case (.ready, .ready): return true + case (.processing, .processing): return true + case (.results(_, _, let runIdL), .results(_, _, let runIdR)): return runIdL == runIdR + case (.error(let a), .error(let b)): return a == b + default: return false + } + } +} + +// MARK: - App Error + +/// User-facing errors with recovery guidance. +enum AppError: Equatable, LocalizedError { + case modelLoadFailed(String) + case inferenceFailed(String) + case imageLoadFailed + + var errorDescription: String? { + switch self { + case .modelLoadFailed(let msg): return msg + case .inferenceFailed(let msg): return msg + case .imageLoadFailed: return "Could not load the selected image. Try a different one." + } + } + + /// Whether this error is recoverable by retrying model load (vs just selecting a new image). + var isModelError: Bool { + if case .modelLoadFailed = self { return true } + return false + } +} + +// MARK: - OCRViewModel + +/// Central view model that manages the complete app lifecycle. +/// +/// State transitions: +/// ``` +/// loadingModels --> ready --> processing --> results +/// | ^ | | +/// v | v v +/// error ----retry----> error ready (reset) +/// ``` +/// +/// All state mutations happen on `@MainActor` through the single +/// `@Published var state` property, giving SwiftUI a single source of truth. +@MainActor +class OCRViewModel: ObservableObject { + @Published var state: AppState = .loadingModels + @Published var copiedFeedback: Bool = false + @Published var runtimeParams = OCRRuntimeParams.noOverrides + /// ONNX Runtime presets and tuning shown in UI (draft). Use **Apply & reload** in Advanced settings to reload sessions (`loadModels()`). + @Published var ortPrimaryExecutionProvider: ORTPrimaryExecutionProvider = .cpu + @Published var ortSessionTuning = ORTSessionTuningOptions.default + /// Snapshot of Ort settings actually used by the loaded ``ocrEngine``. + private var ortLoadedPrimaryExecutionProvider: ORTPrimaryExecutionProvider = .cpu + private var ortLoadedSessionTuning: ORTSessionTuningOptions = .default + @Published private(set) var resolvedRuntimeBaseline: ResolvedOCRRuntimeParams? + + private var sessionManager: ORTSessionManager? + private var ocrEngine: OCREngine? + /// Last image run through OCR (normalized orientation), for **Re-run OCR** without re-picking. + private var lastNormalizedImage: UIImage? + /// OCR runtime parameters used for the last successful run (drives ``hasPendingOcrChanges``). + private var lastRunRuntimeParams: OCRRuntimeParams? + + /// Bundled sample resource names (stem or `stem.ext`) for ``selectSampleImage(named:)``. + let sampleImageNames: [String] = [DemoAssets.defaultSampleImageStem] + + /// Draft Ort settings differ from the configuration used by loaded sessions. + var hasUnappliedOrtSettings: Bool { + ortPrimaryExecutionProvider != ortLoadedPrimaryExecutionProvider + || ortSessionTuning != ortLoadedSessionTuning + } + + /// `runtimeParams` differs from the last successful OCR run (only meaningful in ``AppState/results``). + var hasPendingOcrChanges: Bool { + guard case .results = state else { return false } + guard let last = lastRunRuntimeParams else { return false } + return runtimeParams != last + } + + /// When `false`, disable Apply/Revert (reload would conflict with loading or OCR in flight). + var allowsOrtApplyControls: Bool { + switch state { + case .loadingModels, .processing: return false + case .ready, .results, .error: return true + } + } + + /// Restores ONNX Runtime controls to match the currently loaded sessions. + func revertOrtDraftToLoaded() { + ortPrimaryExecutionProvider = ortLoadedPrimaryExecutionProvider + ortSessionTuning = ortLoadedSessionTuning + } + + /// Photo picker or file load produced unusable image data. + func reportImageLoadFailed() { + state = .error(.imageLoadFailed) + } + + // MARK: - Lifecycle + + /// Load ORT models from the app bundle. + /// + /// Creates an `ORTSessionManager`, loads both detection and recognition models, + /// then creates the `OCREngine` for end-to-end OCR. + func loadModels() async { + state = .loadingModels + do { + let manager = ORTSessionManager() + try await manager.loadModels(executionProvider: ortPrimaryExecutionProvider, tuning: ortSessionTuning) + let engine = try OCREngine(sessionManager: manager) + self.sessionManager = manager + self.ocrEngine = engine + self.resolvedRuntimeBaseline = engine.baselineRuntimeDefaults() + ortLoadedPrimaryExecutionProvider = ortPrimaryExecutionProvider + ortLoadedSessionTuning = ortSessionTuning + state = .ready + } catch { + state = .error(.modelLoadFailed(error.localizedDescription)) + } + } + + // MARK: - Image Processing + + /// Run full OCR on a user-selected image. + /// + /// Normalizes orientation first (camera photos often have EXIF rotation), + /// then delegates to `OCREngine.run(_:)`. + func processImage(_ uiImage: UIImage) async { + let normalized = normalizeOrientation(uiImage) + lastNormalizedImage = normalized + state = .processing(normalized) + guard let cgImage = normalized.cgImage else { + state = .error(.imageLoadFailed) + return + } + guard let engine = ocrEngine else { + state = .error(.modelLoadFailed("OCR engine is not ready.")) + return + } + do { + let result = try await engine.run(cgImage, params: runtimeParams) + lastRunRuntimeParams = runtimeParams + state = .results(result, normalized, runId: UUID()) + } catch { + lastNormalizedImage = nil + state = .error(.inferenceFailed(error.localizedDescription)) + } + } + + /// Runs OCR again on the **last image** using the current `runtimeParams` (e.g. after tuning sliders). + func rerunOCR() async { + guard let image = lastNormalizedImage else { return } + await processImage(image) + } + + /// Load a bundled sample image by name and run OCR on it. + func selectSampleImage(named name: String) async { + guard let path = Bundle.main.path(forBundledImageNamed: name, subdirectory: "SampleImages") else { + state = .error(.imageLoadFailed) + return + } + do { + guard let uiFromFile = UIImage(contentsOfFile: path) else { + state = .error(.imageLoadFailed) + return + } + let normalized = normalizeOrientation(uiFromFile) + lastNormalizedImage = normalized + state = .processing(normalized) + guard let cgImage = normalized.cgImage else { + state = .error(.imageLoadFailed) + return + } + guard let engine = ocrEngine else { + state = .error(.modelLoadFailed("OCR engine is not ready.")) + return + } + let result = try await engine.run(cgImage, params: runtimeParams) + lastRunRuntimeParams = runtimeParams + state = .results(result, normalized, runId: UUID()) + } catch { + lastNormalizedImage = nil + state = .error(.inferenceFailed(error.localizedDescription)) + } + } + + // MARK: - Result Actions + + /// Copy all recognized text to the system clipboard, with brief visual feedback. + func copyResultsToClipboard() { + guard case .results(let result, _, _) = state else { return } + let allText = result.results.map(\.text).joined(separator: "\n") + UIPasteboard.general.string = allText + copiedFeedback = true + Task { + try? await Task.sleep(nanoseconds: 2_000_000_000) + copiedFeedback = false + } + } + + // MARK: - Error Recovery + + /// Retry after an error. Model errors reload models; other errors return to ready state. + func retry() async { + guard case .error(let error) = state else { return } + if error.isModelError { + await loadModels() + } else { + state = .ready + } + } + + /// Return to the ready state (e.g. after viewing results, user wants to pick a new image). + func reset() { + lastNormalizedImage = nil + lastRunRuntimeParams = nil + state = .ready + } +} + +// MARK: - Image Orientation + +/// Normalize EXIF orientation by redrawing the image in `.up` orientation. +/// +/// Camera photos from the photo picker often carry non-`.up` orientation metadata. +/// CoreGraphics ignores EXIF orientation, so we must flatten it before OCR processing. +private func normalizeOrientation(_ image: UIImage) -> UIImage { + guard image.imageOrientation != .up else { return image } + let format = UIGraphicsImageRendererFormat() + format.scale = image.scale + let renderer = UIGraphicsImageRenderer(size: image.size, format: format) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: image.size)) + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/EngineSettingsSheet.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/EngineSettingsSheet.swift new file mode 100644 index 0000000000000000000000000000000000000000..3796d2d3067f6026fa993dbb0ca36a41c67c4e9d --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/EngineSettingsSheet.swift @@ -0,0 +1,106 @@ +// Copyright (c) 2026 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. + +import SwiftUI + +/// Advanced ONNX Runtime and OCR parameters, presented as a sheet from the main screen toolbar. +struct EngineSettingsSheet: View { + @Environment(\.dismiss) private var dismiss + @ObservedObject var viewModel: OCRViewModel + @State private var ortSessionReloadConfirmed = false + + var body: some View { + NavigationStack { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + DemoSectionHeader( + title: "ONNX Runtime", + subtitle: "Choose execution provider and options, then reload to apply." + ) + DemoCard { + OnnxRuntimeSettingsPanel( + primaryExecutionProvider: $viewModel.ortPrimaryExecutionProvider, + tuning: $viewModel.ortSessionTuning + ) + } + + DemoSectionHeader( + title: "OCR parameters", + subtitle: "These apply to the next OCR run." + ) + DemoCard { + OCRParametersPanel( + params: $viewModel.runtimeParams, + resolvedBaseline: viewModel.resolvedRuntimeBaseline ?? .fallbackForUI + ) + } + } + .padding(20) + .padding(.bottom, viewModel.hasUnappliedOrtSettings ? 120 : 24) + } + .background(Color(.systemGroupedBackground)) + .navigationTitle("Engine & parameters") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + dismiss() + } + } + } + .safeAreaInset(edge: .bottom, spacing: 0) { + if viewModel.hasUnappliedOrtSettings { + VStack(alignment: .leading, spacing: 12) { + Divider() + Text("Sessions still use the previous ONNX Runtime choices until you reload.") + .font(.footnote) + .foregroundStyle(Color.orange.opacity(0.95)) + .fixedSize(horizontal: false, vertical: true) + + HStack(spacing: 12) { + Button("Revert draft") { + viewModel.revertOrtDraftToLoaded() + } + .disabled(!viewModel.allowsOrtApplyControls || !viewModel.hasUnappliedOrtSettings) + + Button { + ortSessionReloadConfirmed = true + } label: { + Label("Apply & reload", systemImage: "arrow.clockwise.circle") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .disabled(!viewModel.allowsOrtApplyControls || !viewModel.hasUnappliedOrtSettings) + } + } + .padding(.horizontal, 20) + .padding(.vertical, 12) + .background(.ultraThinMaterial) + } + } + } + .confirmationDialog( + "Rebuild ONNX Runtime sessions?", + isPresented: $ortSessionReloadConfirmed, + titleVisibility: .visible + ) { + Button("Reload models") { + Task { await viewModel.loadModels() } + } + Button("Cancel", role: .cancel) {} + } message: { + Text("Sessions are recreated from the bundle. Expect a short pause.") + } + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ErrorView.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ErrorView.swift new file mode 100644 index 0000000000000000000000000000000000000000..529c87bf95fa8f37fa69b60502bf0f7cd8c738a5 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ErrorView.swift @@ -0,0 +1,60 @@ +// Copyright (c) 2026 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. + +import SwiftUI + +/// Displays an error state with icon, message, and retry button. +struct ErrorView: View { + let error: AppError + let onRetry: () -> Void + + var body: some View { + VStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 48)) + .symbolRenderingMode(.hierarchical) + .foregroundStyle(errorSeverityColor) + + Text(errorTitle) + .font(.title2) + .fontWeight(.semibold) + + Text(error.localizedDescription) + .font(.body) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + + Button(action: onRetry) { + Label("Retry", systemImage: "arrow.clockwise") + } + .buttonStyle(.borderedProminent) + .padding(.top, 8) + } + } + + private var errorTitle: String { + switch error { + case .modelLoadFailed: return "Model Loading Failed" + case .inferenceFailed: return "OCR Failed" + case .imageLoadFailed: return "Image Error" + } + } + + private var errorSeverityColor: Color { + switch error { + case .imageLoadFailed: return Color(.systemOrange) + case .modelLoadFailed, .inferenceFailed: return Color(.systemRed) + } + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ImagePickerSection.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ImagePickerSection.swift new file mode 100644 index 0000000000000000000000000000000000000000..259406c3799b87110b81c5766eef68f40fae2bbb --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ImagePickerSection.swift @@ -0,0 +1,83 @@ +// Copyright (c) 2026 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. + +import PhotosUI +import SwiftUI + +/// Image selection UI: a prominent "Choose Photo" button backed by PhotosPicker, +/// plus a row of sample image thumbnails for quick demo access. +struct ImagePickerSection: View { + @Binding var selectedItem: PhotosPickerItem? + let sampleImageNames: [String] + let onSampleSelected: (String) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + // Primary CTA: system photo picker + PhotosPicker( + selection: $selectedItem, + matching: .images, + photoLibrary: .shared() + ) { + Label("Choose Photo", systemImage: "photo.on.rectangle") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + + // Sample images row + if !sampleImageNames.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("Or try a sample:") + .font(.subheadline) + .foregroundStyle(.secondary) + + HStack(spacing: 8) { + ForEach(sampleImageNames, id: \.self) { name in + Button { + onSampleSelected(name) + } label: { + sampleThumbnail(named: name) + } + .buttonStyle(.plain) + .frame(minWidth: 44, minHeight: 44) + .contentShape(Rectangle()) + .accessibilityLabel("Sample image: \(name)") + } + } + } + } + } + } + + @ViewBuilder + private func sampleThumbnail(named name: String) -> some View { + if let path = Bundle.main.path(forBundledImageNamed: name, subdirectory: "SampleImages"), + let uiImage = UIImage(contentsOfFile: path) { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 60, height: 60) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } else { + // Fallback for when image can't be loaded + RoundedRectangle(cornerRadius: 8) + .fill(Color(.tertiarySystemFill)) + .frame(width: 60, height: 60) + .overlay( + Image(systemName: "photo") + .foregroundStyle(.secondary) + ) + } + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/OCRParametersPanel.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/OCRParametersPanel.swift new file mode 100644 index 0000000000000000000000000000000000000000..942e6a13446615a25f6efaf27798003acc78f350 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/OCRParametersPanel.swift @@ -0,0 +1,226 @@ +// Copyright (c) 2026 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. + +import SwiftUI + +/// Sliders for detection / recognition runtime parameters (content only; wrap with `DemoCard` in parent). +struct OCRParametersPanel: View { + @Binding var params: OCRRuntimeParams + let resolvedBaseline: ResolvedOCRRuntimeParams + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + groupTitle("Detection preprocess") + intParamRow( + title: "Limit side length", + caption: "Side length limit for detection resize", + range: 32...2048, + value: intOverrideBinding(\.textDetLimitSideLen, fallback: resolvedBaseline.textDetLimitSideLen) + ) + limitTypeRow( + selection: stringOverrideBinding(\.textDetLimitType, fallback: resolvedBaseline.textDetLimitType) + ) + intParamRow( + title: "Max side length cap", + caption: "After limit-side resize, caps the longer side if it is still above this", + range: 500...8000, + value: intOverrideBinding(\.textDetMaxSideLimit, fallback: resolvedBaseline.textDetMaxSideLimit) + ) + + groupTitle("Detection postprocess") + paramRow( + title: "Map threshold", + caption: "Binarization on the detector heatmap", + range: 0.05...0.95, + value: floatOverrideBinding(\.textDetThresh, fallback: resolvedBaseline.textDetThresh) + ) + paramRow( + title: "Box score threshold", + caption: "Minimum score to keep a box", + range: 0.1...0.95, + value: floatOverrideBinding(\.textDetBoxThresh, fallback: resolvedBaseline.textDetBoxThresh) + ) + paramRow( + title: "Unclip ratio", + caption: "Expand recovered polygons", + range: 0.5...3.0, + value: floatOverrideBinding(\.textDetUnclipRatio, fallback: resolvedBaseline.textDetUnclipRatio) + ) + + Divider() + .padding(.vertical, 12) + + groupTitle("Recognition") + intParamRow( + title: "Recognition batch size", + caption: "Number of text lines per recognition run", + range: 1...32, + value: intOverrideBinding(\.textRecBatchSize, fallback: resolvedBaseline.textRecBatchSize) + ) + paramRow( + title: "Line confidence", + caption: "Discard lines below this score", + range: 0...1, + value: floatOverrideBinding(\.textRecScoreThresh, fallback: resolvedBaseline.textRecScoreThresh) + ) + + HStack { + Spacer() + Button("Reset to defaults") { + params = .noOverrides + } + .font(.subheadline.weight(.medium)) + } + .padding(.top, 4) + } + } + + private func floatOverrideBinding( + _ keyPath: WritableKeyPath, + fallback: Float + ) -> Binding { + Binding( + get: { params[keyPath: keyPath] ?? fallback }, + set: { newValue in + var next = params + next[keyPath: keyPath] = newValue + params = next + } + ) + } + + private func intOverrideBinding( + _ keyPath: WritableKeyPath, + fallback: Int + ) -> Binding { + Binding( + get: { params[keyPath: keyPath] ?? fallback }, + set: { newValue in + var next = params + next[keyPath: keyPath] = newValue + params = next + } + ) + } + + private func stringOverrideBinding( + _ keyPath: WritableKeyPath, + fallback: String + ) -> Binding { + Binding( + get: { params[keyPath: keyPath] ?? fallback }, + set: { newValue in + var next = params + next[keyPath: keyPath] = newValue + params = next + } + ) + } + + private func groupTitle(_ text: String) -> some View { + Text(text) + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.primary) + .padding(.bottom, 8) + } + + private func paramRow( + title: String, + caption: String, + range: ClosedRange, + value: Binding + ) -> some View { + VStack(alignment: .leading, spacing: 6) { + HStack(alignment: .firstTextBaseline) { + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.subheadline) + Text(caption) + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + Text(String(format: "%.3f", value.wrappedValue)) + .font(.caption.monospacedDigit()) + .foregroundStyle(.secondary) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color(.tertiarySystemFill), in: Capsule()) + } + Slider( + value: Binding( + get: { Double(value.wrappedValue) }, + set: { value.wrappedValue = Float($0) } + ), + in: Double(range.lowerBound)...Double(range.upperBound) + ) + .tint(Color.accentColor) + } + .padding(.bottom, 14) + } + + private func intParamRow( + title: String, + caption: String, + range: ClosedRange, + value: Binding + ) -> some View { + VStack(alignment: .leading, spacing: 6) { + HStack(alignment: .firstTextBaseline) { + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.subheadline) + Text(caption) + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + Text("\(value.wrappedValue)") + .font(.caption.monospacedDigit()) + .foregroundStyle(.secondary) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color(.tertiarySystemFill), in: Capsule()) + } + Slider( + value: Binding( + get: { Double(value.wrappedValue) }, + set: { value.wrappedValue = Int($0.rounded()) } + ), + in: Double(range.lowerBound)...Double(range.upperBound), + step: 1 + ) + .tint(Color.accentColor) + } + .padding(.bottom, 14) + } + + private func limitTypeRow(selection: Binding) -> some View { + VStack(alignment: .leading, spacing: 6) { + VStack(alignment: .leading, spacing: 2) { + Text("Limit type") + .font(.subheadline) + Text("min: shortest side ≥ limit length; max: longest side ≤ limit length") + .font(.caption) + .foregroundStyle(.secondary) + } + Picker("Limit type", selection: selection) { + Text("min").tag("min") + Text("max").tag("max") + } + .pickerStyle(.segmented) + } + .padding(.bottom, 14) + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/OnnxRuntimeSettingsPanel.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/OnnxRuntimeSettingsPanel.swift new file mode 100644 index 0000000000000000000000000000000000000000..30f416444f804b30eec39fef53b54e5dad013eed --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/OnnxRuntimeSettingsPanel.swift @@ -0,0 +1,225 @@ +// Copyright (c) 2026 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. + +import SwiftUI + +private enum CoreMLComputeMode: String, CaseIterable, Identifiable { + case automatic + case cpuOnly + case cpuAndGPU + case devicesWithANE + + var id: String { rawValue } + + var segmentLabel: String { + switch self { + case .automatic: return "Auto" + case .cpuOnly: return "CPU" + case .cpuAndGPU: return "CPU+GPU" + case .devicesWithANE: return "ANE" + } + } + + static func from(flags: Set) -> CoreMLComputeMode { + let cpuOnly = ORTCoreMLProviderOption.cpuOnly.rawValue + let cpuAndGPU = ORTCoreMLProviderOption.cpuAndGPU.rawValue + let aneOnly = ORTCoreMLProviderOption.aneOnly.rawValue + if flags.contains(cpuOnly) { return .cpuOnly } + if flags.contains(cpuAndGPU) { return .cpuAndGPU } + if flags.contains(aneOnly) { return .devicesWithANE } + return .automatic + } + + func merge(into flags: inout Set) { + let cpuOnly = ORTCoreMLProviderOption.cpuOnly.rawValue + let cpuAndGPU = ORTCoreMLProviderOption.cpuAndGPU.rawValue + let aneOnly = ORTCoreMLProviderOption.aneOnly.rawValue + flags.remove(cpuOnly) + flags.remove(cpuAndGPU) + flags.remove(aneOnly) + switch self { + case .automatic: break + case .cpuOnly: flags.insert(cpuOnly) + case .cpuAndGPU: flags.insert(cpuAndGPU) + case .devicesWithANE: flags.insert(aneOnly) + } + } +} + +/// ONNX Runtime: preferred execution provider (EP) preset plus session tuning. Card content only. +struct OnnxRuntimeSettingsPanel: View { + private static let coreMLExtraToggleOptions: [ORTCoreMLProviderOption] = + [.enableOnSubgraphs, .createMLProgram, .staticInputShapes] + + @Binding var primaryExecutionProvider: ORTPrimaryExecutionProvider + @Binding var tuning: ORTSessionTuningOptions + + var body: some View { + VStack(alignment: .leading, spacing: 14) { + Text("All inference uses ONNX Runtime. Choose which EP is preferred first for this demo preset; unsupported operators still run on the CPU EP.") + .font(.caption) + .foregroundStyle(.secondary) + + Picker("Execution provider", selection: $primaryExecutionProvider) { + ForEach(ORTPrimaryExecutionProvider.allCases) { provider in + Text(provider.displayTitle).tag(provider) + } + } + .pickerStyle(.segmented) + + Divider() + .padding(.vertical, 2) + + Text("Session options") + .font(.subheadline.weight(.semibold)) + + labeledStepper( + title: "Intra-op threads", + caption: "Session-wide CPU parallelism. Use 0 to leave ONNX Runtime’s default (no explicit limit).", + value: intraOpThreadsBinding, + range: 0...8 + ) + + if primaryExecutionProvider == .xnnpack { + labeledStepper( + title: "XNNPACK intra-op threads", + caption: "0 leaves the XNNPACK EP default.", + value: xnnpackThreadsBinding, + range: 0...8 + ) + } + + if primaryExecutionProvider == .coreML { + VStack(alignment: .leading, spacing: 10) { + Text("Core ML EP options") + .font(.subheadline.weight(.semibold)) + .padding(.top, 4) + + VStack(alignment: .leading, spacing: 6) { + Text("Core ML placement") + .font(.body) + Picker("Core ML placement", selection: coreMLComputeModeBinding) { + ForEach(CoreMLComputeMode.allCases) { mode in + Text(mode.segmentLabel).tag(mode) + } + } + .pickerStyle(.segmented) + Text(coreMLComputeModeCaption) + .font(.caption) + .foregroundStyle(.secondary) + } + + ForEach(Self.coreMLExtraToggleOptions) { option in + Toggle(isOn: coreMLFlagBinding(option)) { + VStack(alignment: .leading, spacing: 2) { + Text(option.displayTitle) + Text(option.detailCaption) + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } + } + + HStack { + Spacer() + Button("Reset session options") { + tuning = .default + } + .font(.subheadline.weight(.medium)) + } + .padding(.top, 4) + } + } + + private var coreMLComputeModeCaption: String { + switch CoreMLComputeMode.from(flags: tuning.coreMLFlags) { + case .automatic: + return "ORT defaults — no cpuOnly/cpuAndGPU/device ANE restriction." + case .cpuOnly: + return "useCPUOnly — Core ML constrained to CPU." + case .cpuAndGPU: + return "useCPUAndGPU — CPU and GPU paths; excludes ANE in Core ML." + case .devicesWithANE: + return "Only enable Core ML EP on devices that have a Neural Engine." + } + } + + private var intraOpThreadsBinding: Binding { + Binding( + get: { tuning.intraOpThreads }, + set: { newValue in + var next = tuning + next.intraOpThreads = newValue + tuning = next + } + ) + } + + private var xnnpackThreadsBinding: Binding { + Binding( + get: { tuning.xnnpackThreads }, + set: { newValue in + var next = tuning + next.xnnpackThreads = newValue + tuning = next + } + ) + } + + private var coreMLComputeModeBinding: Binding { + Binding( + get: { CoreMLComputeMode.from(flags: tuning.coreMLFlags) }, + set: { mode in + var next = tuning + mode.merge(into: &next.coreMLFlags) + tuning = next + } + ) + } + + private func coreMLFlagBinding(_ option: ORTCoreMLProviderOption) -> Binding { + let key = option.rawValue + return Binding( + get: { tuning.coreMLFlags.contains(key) }, + set: { on in + var next = tuning + if on { + next.coreMLFlags.insert(key) + } else { + next.coreMLFlags.remove(key) + } + tuning = next + } + ) + } + + private func labeledStepper(title: String, caption: String, value: Binding, range: ClosedRange) -> some View { + VStack(alignment: .leading, spacing: 6) { + HStack(alignment: .firstTextBaseline) { + Text(title) + Spacer() + Stepper(value: value, in: range) { + Text("\(value.wrappedValue)") + .font(.body.monospacedDigit()) + .frame(minWidth: 24, alignment: .trailing) + } + } + Text(caption) + .font(.caption) + .foregroundStyle(.secondary) + } + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ResultImageView.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ResultImageView.swift new file mode 100644 index 0000000000000000000000000000000000000000..3a90afac00542366c396d309412151e233376e63 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ResultImageView.swift @@ -0,0 +1,79 @@ +// Copyright (c) 2026 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. + +import SwiftUI + +/// Displays the source image with bounding box polygon overlays drawn on a Canvas. +/// +/// Polygon coordinates from `OCRResult.polygon` are in original image pixel space. +/// This view computes the scale factor and offset to map them to the displayed +/// image area, accounting for `.aspectFit` rendering within the available frame. +struct ResultImageView: View { + let image: UIImage + let results: [OCRResult] + + var body: some View { + GeometryReader { geo in + let imageSize = image.size + let imageAspect = imageSize.width / imageSize.height + let viewAspect = geo.size.width / geo.size.height + + // Compute the actual rendered image rect within the GeometryReader frame + let fitSize: CGSize = imageAspect > viewAspect + ? CGSize(width: geo.size.width, height: geo.size.width / imageAspect) + : CGSize(width: geo.size.height * imageAspect, height: geo.size.height) + let offset = CGPoint( + x: (geo.size.width - fitSize.width) / 2, + y: (geo.size.height - fitSize.height) / 2 + ) + let scale = fitSize.width / imageSize.width + + ZStack { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + + Canvas { context, size in + for result in results { + let points = result.polygon.compactMap { coords -> CGPoint? in + guard coords.count >= 2 else { return nil } + return CGPoint( + x: CGFloat(coords[0]) * scale + offset.x, + y: CGFloat(coords[1]) * scale + offset.y + ) + } + guard points.count >= 4 else { continue } + + var path = Path() + path.move(to: points[0]) + for pt in points.dropFirst() { + path.addLine(to: pt) + } + path.closeSubpath() + + let strokeColor = Color.accentColor.opacity(0.9) + let fillColor = Color.accentColor.opacity(0.12) + context.stroke(path, with: .color(strokeColor), lineWidth: 2) + context.fill(path, with: .color(fillColor)) + } + } + } + .frame(width: geo.size.width, height: geo.size.height) + } + .aspectRatio(image.size.width / image.size.height, contentMode: .fit) + .frame(maxHeight: 400) + .accessibilityElement(children: .combine) + .accessibilityLabel("Image preview with \(results.count) detected text regions") + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ResultsListView.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ResultsListView.swift new file mode 100644 index 0000000000000000000000000000000000000000..93b78ed9b65cca81574c998b918e546061323a37 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ResultsListView.swift @@ -0,0 +1,98 @@ +// Copyright (c) 2026 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. + +import SwiftUI + +/// Recognized lines + copy action (intended to sit inside `DemoCard` from the parent). +struct ResultsListView: View { + let results: [OCRResult] + let copiedFeedback: Bool + let onCopy: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .firstTextBaseline) { + Text("Recognized text") + .font(.headline) + Spacer() + Button(action: onCopy) { + Label( + copiedFeedback ? "Copied" : "Copy all", + systemImage: copiedFeedback ? "checkmark.circle.fill" : "doc.on.doc" + ) + .labelStyle(.titleAndIcon) + } + .font(.subheadline.weight(.medium)) + .buttonStyle(.bordered) + .controlSize(.small) + .disabled(results.isEmpty) + } + .padding(.bottom, 12) + + if results.isEmpty { + emptyState + } else { + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(results.enumerated()), id: \.offset) { index, result in + resultRow(index: index, result: result) + if index < results.count - 1 { + Divider() + .padding(.vertical, 10) + } + } + } + } + } + } + + private var emptyState: some View { + VStack(spacing: 12) { + Image(systemName: "text.magnifyingglass") + .font(.system(size: 32)) + .foregroundStyle(.secondary) + .symbolRenderingMode(.hierarchical) + Text("No text to show") + .font(.subheadline.weight(.semibold)) + Text("Try lowering thresholds or use an image with clearer text.") + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 20) + } + + private func resultRow(index: Int, result: OCRResult) -> some View { + HStack(alignment: .top, spacing: 12) { + Text("\(index + 1)") + .font(.caption.weight(.bold)) + .foregroundStyle(.secondary) + .frame(width: 22, height: 22) + .background(Color(.tertiarySystemFill), in: Circle()) + + Text(result.text) + .font(.body) + .foregroundStyle(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(5) + + Text(String(format: "%.0f%%", result.confidence * 100)) + .font(.caption.monospacedDigit()) + .foregroundStyle(.secondary) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color(.tertiarySystemFill), in: Capsule()) + } + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/TimingView.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/TimingView.swift new file mode 100644 index 0000000000000000000000000000000000000000..f4f277db183fe74409e83713d2ce82fb56616aa0 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/TimingView.swift @@ -0,0 +1,54 @@ +// Copyright (c) 2026 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. + +import SwiftUI + +/// Compact per-stage timing chips (fits the “output” section below the preview). +struct TimingView: View { + let result: OCRRunResult + + var body: some View { + HStack(spacing: 10) { + chip(title: "Detect", ms: result.detectionTime) + chip(title: "Recognize", ms: result.recognitionTime) + chip(title: "Total", ms: result.totalTime, emphasized: true) + } + .frame(maxWidth: .infinity) + } + + private func chip(title: String, ms: TimeInterval, emphasized: Bool = false) -> some View { + VStack(spacing: 6) { + Text(title) + .font(.caption2.weight(.semibold)) + .foregroundStyle(.secondary) + Text(String(format: "%.0f", ms * 1000)) + .font(.system(.body, design: .rounded).weight(emphasized ? .semibold : .regular)) + .monospacedDigit() + Text("ms") + .font(.caption2) + .foregroundStyle(.tertiary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .padding(.horizontal, 8) + .background { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(emphasized ? Color.accentColor.opacity(0.12) : Color(.tertiarySystemFill)) + } + .overlay { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .strokeBorder(Color(.separator).opacity(0.25), lineWidth: 0.5) + } + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/BenchmarkFixtures.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/BenchmarkFixtures.swift new file mode 100644 index 0000000000000000000000000000000000000000..13cb6265fd04400e26526d7aa04d8d59e5474557 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/BenchmarkFixtures.swift @@ -0,0 +1,25 @@ +// Copyright (c) 2026 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. + +import Foundation + +/// Default bundled image file names for benchmark tests. +enum BenchmarkFixtures { + static let defaultReferenceImageStem: String = "ios_ocr_benchmark_reference" + static let defaultReferenceImageFileExtension: String = "jpg" + + static var defaultReferenceImageFileName: String { + "\(defaultReferenceImageStem).\(defaultReferenceImageFileExtension)" + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/BenchmarkPayloads.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/BenchmarkPayloads.swift new file mode 100644 index 0000000000000000000000000000000000000000..7380e17bb1aade8b8bb36c84f368106f8915f84d --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/BenchmarkPayloads.swift @@ -0,0 +1,95 @@ +// Copyright (c) 2026 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. + +import Foundation + +@testable import PaddleOCRDemo + +/// Names for JSON written as `XCTAttachment`s; keep in sync with whatever exports attachments from test results. +enum BenchmarkArtifact: String { + case iOSExport = "ios-ocr-export.json" + case onDevicePerformance = "on-device-performance.json" +} + +// MARK: - Accuracy export payload + +struct OCRExportPayload: Codable { + var schemaVersion: Int = 1 + var source: String + var items: [OCRExportItem] +} + +struct OCRExportItem: Codable { + var polygon: [[Int]] + var text: String + var score: Double? +} + +// MARK: - On-device performance payload + +struct TimingSummary: Codable { + var mean: Double + var stdev: Double + var p90: Double +} + +/// Pooled: one sample per det line per measured run (all lines concatenated, ms). +struct RecognitionPerLinePooledMs: Codable { + var count: Int + var inferenceMs: TimingSummary + var preprocessMs: TimingSummary + var postprocessMs: TimingSummary + var totalMs: TimingSummary +} + +struct RecognitionPerLineBlock: Codable { + var pooled: RecognitionPerLinePooledMs +} + +struct BenchmarkTensorShapeSample: Codable { + var shape: [Int] + var count: Int +} + +struct BenchmarkInputShapeDistribution: Codable { + var detection: [BenchmarkTensorShapeSample] + var recognition: [BenchmarkTensorShapeSample] +} + +struct OCRDeviceBenchmarkPayload: Codable { + var schemaVersion: Int = 1 + var buildConfiguration: String + var deviceModel: String + var osVersion: String + var isSimulator: Bool + var ortExecutionProvider: String + var ortProfilingEnabled: Bool + var warmupIterations: Int + var measuredIterations: Int + var inputShapeDistribution: BenchmarkInputShapeDistribution + var firstMeasuredLineCount: Int + var coldLoadTimeMs: Double + var totalTimeMs: TimingSummary + var detectionTimeMs: TimingSummary + var detectionPreprocessTimeMs: TimingSummary + var detectionInferenceTimeMs: TimingSummary + var detectionPostprocessTimeMs: TimingSummary + var recognitionTimeMs: TimingSummary + var recognitionPreprocessTimeMs: TimingSummary + var recognitionInferenceTimeMs: TimingSummary + var recognitionPostprocessTimeMs: TimingSummary + var recognitionPerLine: RecognitionPerLineBlock? + var pipelineOverheadTimeMs: TimingSummary + var memoryFootprintBeforeLoadBytes: UInt64? + var memoryFootprintAfterLoadBytes: UInt64 + var thermalState: String? +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/.gitignore b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3a4a7f7e62d4c6159278e300874f3e7c2d520e82 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/.gitignore @@ -0,0 +1,2 @@ +# Ad-hoc copies from ./scripts/run_validation.sh --image (local-*); not committed. +local-* diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/ios_ocr_validation_reference.jpg b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/ios_ocr_validation_reference.jpg new file mode 100644 index 0000000000000000000000000000000000000000..70f5b428c2c21a775fdfbba0668506c4a770dfd6 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/ios_ocr_validation_reference.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4425af33dd163cf73bdff502bd35ee527e9bdd5725501db1da78bfdae9f538f4 +size 128713 diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/OCRBenchmarkTests.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/OCRBenchmarkTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..fbf3631bc8a9995d4186c4432f676840f944f106 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/OCRBenchmarkTests.swift @@ -0,0 +1,562 @@ +// Copyright (c) 2026 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. + +import Darwin +import Foundation + +import XCTest + +@testable import PaddleOCRDemo + +import UIKit + +// MARK: - Tests + +final class OCRBenchmarkTests: XCTestCase { + + /// `PADDLEOCR_BENCHMARK_IMAGE_NAME`: bundled image stem or `stem.ext` (optional). + private static let imageNameEnvKey = "PADDLEOCR_BENCHMARK_IMAGE_NAME" + + /// Optional non-negative int; default `3`. Used only by `testOnDevicePerformanceMetrics`. + private static let warmupIterationsEnvKey = "PADDLEOCR_BENCHMARK_WARMUP_ITERATIONS" + + /// Optional non-negative int; default `10`. Used only by `testOnDevicePerformanceMetrics`. + private static let measuredIterationsEnvKey = "PADDLEOCR_BENCHMARK_MEASURED_ITERATIONS" + + /// `CPU` (default), `XNNPACK`, or `CORE_ML` — ONNX Runtime execution provider preset for benchmarks. + private static let ortExecutionProviderEnvKey = "PADDLEOCR_BENCHMARK_ORT_EXECUTION_PROVIDER" + + /// `1` / `true` / `yes` / `on` enables ONNX Runtime JSON profiling (see ``ORTSessionManager/finalizeORTProfiling()``). + /// When set, on-device performance timings are not representative of a clean latency benchmark. + private static let ortProfilingEnvKey = "PADDLEOCR_BENCHMARK_ORT_PROFILING" + + private static let buildConfigurationEnvKey = "PADDLEOCR_BENCHMARK_BUILD_CONFIGURATION" + + private static let intraOpThreadsEnvKey = "PADDLEOCR_BENCHMARK_INTRA_OP_THREADS" + private static let xnnpackThreadsEnvKey = "PADDLEOCR_BENCHMARK_XNNPACK_THREADS" + private static let coremlOptionsEnvKey = "PADDLEOCR_BENCHMARK_COREML_OPTIONS" + private static let recBatchSizeEnvKey = "PADDLEOCR_BENCHMARK_REC_BATCH_SIZE" + + func testOCRExportJSONSchema() async throws { + let cgImage = try resolveBenchmarkImage() + let executionProvider = try resolveBenchmarkExecutionProvider() + let manager = ORTSessionManager() + try await manager.loadModels(executionProvider: executionProvider, ortProfiling: resolveORTProfilingEnabled(), tuning: resolveSessionTuningOptions()) + let engine = try OCREngine(sessionManager: manager) + let run = try await engine.run(cgImage, params: .noOverrides) + + let payload = OCRExportPayload( + source: "ios_ocr_demo", + items: run.results.map { r in + OCRExportItem( + polygon: r.polygon.map { $0.map { Int($0) } }, + text: r.text, + score: Double(r.confidence) + ) + } + ) + + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + let data = try encoder.encode(payload) + _ = try JSONDecoder().decode(OCRExportPayload.self, from: data) + attachJSON(data, artifact: .iOSExport) + + if resolveORTProfilingEnabled(), let out = try await manager.finalizeORTProfiling() { + attachProfileFile(out.detectionProfileJSON, name: "ort_profile_detection") + attachProfileFile(out.recognitionProfileJSON, name: "ort_profile_recognition") + } + } + + func testOnDeviceLatencyBenchmark() async throws { + let executionProvider = try resolveBenchmarkExecutionProvider() + let ortProfiling = resolveORTProfilingEnabled() + let context = try await makeBenchmarkContext(executionProvider: executionProvider, ortProfiling: ortProfiling) + let warmup = try parseNonNegativeIntEnv(Self.warmupIterationsEnvKey, defaultValue: 3) + let iterations = max(try parseNonNegativeIntEnv(Self.measuredIterationsEnvKey, defaultValue: 10), 1) + try await runWarmup(engine: context.engine, image: context.cgImage, iterations: warmup) + + var totals: [Double] = [] + var dets: [Double] = [] + var detPre: [Double] = [] + var detInf: [Double] = [] + var detPost: [Double] = [] + var recs: [Double] = [] + var recPre: [Double] = [] + var recInf: [Double] = [] + var recPost: [Double] = [] + var overheads: [Double] = [] + totals.reserveCapacity(iterations) + dets.reserveCapacity(iterations) + detPre.reserveCapacity(iterations) + detInf.reserveCapacity(iterations) + detPost.reserveCapacity(iterations) + recs.reserveCapacity(iterations) + recPre.reserveCapacity(iterations) + recInf.reserveCapacity(iterations) + recPost.reserveCapacity(iterations) + overheads.reserveCapacity(iterations) + + var lineInferenceMsPooled: [Double] = [] + var linePreprocessMsPooled: [Double] = [] + var linePostprocessMsPooled: [Double] = [] + var lineTotalMsPooled: [Double] = [] + var firstMeasuredLineCount = 0 + var detectionInputShapes: [[Int]] = [] + var recognitionInputShapes: [[Int]] = [] + + let benchmarkParams = resolveBenchmarkRuntimeParams() + + for index in 0.. BenchmarkContext { + let cgImage = try resolveBenchmarkImage() + let memoryBeforeLoad = physicalFootprintBytes() + let manager = ORTSessionManager() + let loadStart = CFAbsoluteTimeGetCurrent() + try await manager.loadModels(executionProvider: executionProvider, ortProfiling: ortProfiling, tuning: resolveSessionTuningOptions()) + let coldLoadTime = CFAbsoluteTimeGetCurrent() - loadStart + let memoryAfterLoad = physicalFootprintBytes() + let engine = try OCREngine(sessionManager: manager) + return BenchmarkContext( + cgImage: cgImage, + manager: manager, + engine: engine, + memoryBeforeLoad: memoryBeforeLoad, + memoryAfterLoad: memoryAfterLoad, + coldLoadTime: coldLoadTime + ) + } + + private func summarizeShapes(_ shapes: [[Int]]) -> [BenchmarkTensorShapeSample] { + let counts = shapes.reduce(into: [[Int]: Int]()) { partial, shape in + partial[shape, default: 0] += 1 + } + return counts + .map { BenchmarkTensorShapeSample(shape: $0.key, count: $0.value) } + .sorted { lhs, rhs in + if lhs.shape != rhs.shape { + return lhs.shape.lexicographicallyPrecedes(rhs.shape) + } + return lhs.count > rhs.count + } + } + + private func runWarmup(engine: OCREngine, image: CGImage, iterations: Int) async throws { + let params = resolveBenchmarkRuntimeParams() + for _ in 0.. 0 { + let infMs = run.lineRecognitionInferenceTimes.map { $0 * 1000.0 } + let preMs = run.lineRecognitionPreprocessTimes.map { $0 * 1000.0 } + let postMs = run.lineRecognitionPostprocessTimes.map { $0 * 1000.0 } + lineInferenceMsPooled.append(contentsOf: infMs) + linePreprocessMsPooled.append(contentsOf: preMs) + linePostprocessMsPooled.append(contentsOf: postMs) + for i in 0.. String { + let raw = + ProcessInfo.processInfo.environment[Self.buildConfigurationEnvKey]? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return raw.isEmpty ? "Release" : raw + } + + private func isRunningOnSimulator() -> Bool { + #if targetEnvironment(simulator) + return true + #else + return false + #endif + } + + private func deviceModelName() -> String { + var systemInfo = utsname() + uname(&systemInfo) + let mirror = Mirror(reflecting: systemInfo.machine) + return mirror.children.reduce(into: "") { identifier, element in + guard let value = element.value as? Int8, value != 0 else { return } + identifier.append(String(UnicodeScalar(UInt8(value)))) + } + } + + private func resolveORTProfilingEnabled() -> Bool { + let raw = + ProcessInfo.processInfo.environment[Self.ortProfilingEnvKey]? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if raw.isEmpty { return false } + switch raw.lowercased() { + case "1", "true", "yes", "on": + return true + default: + return false + } + } + + private func resolveSessionTuningOptions() -> ORTSessionTuningOptions { + let env = ProcessInfo.processInfo.environment + var opts = ORTSessionTuningOptions() + if let v = env[Self.intraOpThreadsEnvKey], let n = Int(v), n > 0 { + opts.intraOpThreads = n + } + if let v = env[Self.xnnpackThreadsEnvKey], let n = Int(v), n > 0 { + opts.xnnpackThreads = n + } + if let v = env[Self.coremlOptionsEnvKey], !v.isEmpty { + opts.coreMLFlags = Set(v.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) }) + } + return opts + } + + private func attachProfileFile(_ url: URL, name: String) { + let attachment = XCTAttachment(contentsOfFile: url) + attachment.name = name + attachment.lifetime = XCTAttachment.Lifetime.keepAlways + add(attachment) + } + + private func resolveBenchmarkExecutionProvider() throws -> ORTPrimaryExecutionProvider { + let raw = + ProcessInfo.processInfo.environment[Self.ortExecutionProviderEnvKey]? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if raw.isEmpty { + return .cpu + } + if let exact = ORTPrimaryExecutionProvider(rawValue: raw) { + return exact + } + switch raw.lowercased() { + case "core_ml": + return .coreML + case "xnnpack": + return .xnnpack + case "cpu": + return .cpu + default: + throw NSError( + domain: "OCRBenchmarkTests", + code: 5, + userInfo: [ + NSLocalizedDescriptionKey: + "Invalid \(Self.ortExecutionProviderEnvKey): \"\(raw)\". " + + "Use CPU (default), XNNPACK, CORE_ML, or Swift raw values " + + "\(ORTPrimaryExecutionProvider.cpu.rawValue) / " + + "\(ORTPrimaryExecutionProvider.xnnpack.rawValue) / " + + "\(ORTPrimaryExecutionProvider.coreML.rawValue).", + ] + ) + } + } + + private func resolveBenchmarkImage() throws -> CGImage { + let envRaw = + ProcessInfo.processInfo.environment[Self.imageNameEnvKey]? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let raw = envRaw.isEmpty ? BenchmarkFixtures.defaultReferenceImageStem : envRaw + let bundle = Bundle(for: Self.self) + guard let path = bundle.path(forBundledImageNamed: raw, subdirectory: "Fixtures") else { + throw NSError( + domain: "OCRBenchmarkTests", + code: 3, + userInfo: [ + NSLocalizedDescriptionKey: + "Image \"\(raw)\" not found in the test bundle. Check the name and that the file is included in the test target resources (e.g. \"\(BenchmarkFixtures.defaultReferenceImageStem)\").", + ] + ) + } + let url = URL(fileURLWithPath: path) + guard let ui = UIImage(contentsOfFile: url.path), + let cg = normalizeOrientation(ui).cgImage + else { + throw NSError( + domain: "OCRBenchmarkTests", + code: 4, + userInfo: [NSLocalizedDescriptionKey: "Could not decode image at \(url.path)."] + ) + } + return cg + } + + private func attachJSON(_ data: Data, artifact: BenchmarkArtifact) { + let attachment = XCTAttachment(data: data, uniformTypeIdentifier: "public.json") + attachment.name = artifact.rawValue + attachment.lifetime = .keepAlways + add(attachment) + } + + private func waitForAsync(_ operation: @escaping () async throws -> T) throws -> T { + let semaphore = DispatchSemaphore(value: 0) + let lock = NSLock() + var capturedResult: Result? + Task { + do { + let value = try await operation() + lock.lock() + capturedResult = .success(value) + lock.unlock() + } catch { + lock.lock() + capturedResult = .failure(error) + lock.unlock() + } + semaphore.signal() + } + semaphore.wait() + lock.lock() + let result = capturedResult + lock.unlock() + switch result { + case .success(let value): + return value + case .failure(let error): + throw error + case .none: + throw NSError( + domain: "OCRBenchmarkTests", + code: 6, + userInfo: [NSLocalizedDescriptionKey: "Async operation completed without a result."] + ) + } + } + + /// Parses a non-negative integer from the environment, or returns `defaultValue` when unset/blank. + private func parseNonNegativeIntEnv(_ key: String, defaultValue: Int) throws -> Int { + let raw = + ProcessInfo.processInfo.environment[key]?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if raw.isEmpty { + return defaultValue + } + guard let n = Int(raw), n >= 0 else { + throw NSError( + domain: "OCRBenchmarkTests", + code: 3, + userInfo: [ + NSLocalizedDescriptionKey: + "Invalid \(key): expected a non-negative integer (default \(defaultValue) if unset), got \(raw).", + ] + ) + } + return n + } + + private func resolveBenchmarkRuntimeParams() -> OCRRuntimeParams { + let env = ProcessInfo.processInfo.environment + if let v = env[Self.recBatchSizeEnvKey], let n = Int(v), n >= 1 { + return OCRRuntimeParams( + textDetLimitSideLen: nil, + textDetLimitType: nil, + textDetMaxSideLimit: nil, + textDetThresh: nil, + textDetBoxThresh: nil, + textDetUnclipRatio: nil, + textRecBatchSize: n, + textRecScoreThresh: nil + ) + } + return .noOverrides + } + + /// Physical memory footprint (bytes) — matches Xcode Memory gauge more closely than RSS alone. + /// Uses `task_vm_info_data_t.phys_footprint` via `task_info(TASK_VM_INFO, ...)`. + private func physicalFootprintBytes() -> UInt64 { + var info = task_vm_info_data_t() + var count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + let kerr = withUnsafeMutablePointer(to: &info) { p in + p.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { + task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count) + } + } + guard kerr == KERN_SUCCESS else { return 0 } + return UInt64(info.phys_footprint) + } + + private func summarizeMs(_ samples: [Double]) -> TimingSummary { + let sorted = samples.sorted() + let mean = samples.reduce(0, +) / Double(max(samples.count, 1)) + let variance = + samples.isEmpty + ? 0 + : samples.map { pow($0 - mean, 2) }.reduce(0, +) / Double(samples.count) + let stdev = sqrt(variance) + let p90Idx = max(0, min(sorted.count - 1, Int(floor(0.9 * Double(max(sorted.count - 1, 0)))))) + let p90 = sorted.isEmpty ? 0 : sorted[p90Idx] + return TimingSummary(mean: mean, stdev: stdev, p90: p90) + } +} + +// MARK: - Orientation (match OCRViewModel) + +private func normalizeOrientation(_ image: UIImage) -> UIImage { + guard image.imageOrientation != .up else { return image } + let format = UIGraphicsImageRendererFormat() + format.scale = image.scale + let renderer = UIGraphicsImageRenderer(size: image.size, format: format) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: image.size)) + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/OCRParameterResolverTests.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/OCRParameterResolverTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..d75839484881d7aea2bd7f585e4274d8b6a38e4b --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/OCRParameterResolverTests.swift @@ -0,0 +1,118 @@ +// Copyright (c) 2026 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. + +import XCTest +@testable import PaddleOCRDemo + +/// Verifies ``OCRParameterResolver`` +final class OCRParameterResolverTests: XCTestCase { + + func testDetResizeMergePrefersAppDefaultsOverModelWhenUINil() throws { + let dir = FileManager.default.temporaryDirectory + let url = dir.appendingPathComponent("merge_test_det_\(UUID().uuidString).yaml") + let yaml = """ + Global: + model_name: PP-OCRv5_mobile_det + PreProcess: + transform_ops: + - DetResizeForTest: + resize_long: 960 + PostProcess: + name: DBPostProcess + """ + try yaml.write(to: url, atomically: true, encoding: .utf8) + defer { try? FileManager.default.removeItem(at: url) } + + let det = try InferenceConfig.load(from: url.path) + let m = det.detResizeFromModel + XCTAssertNotNil(m) + XCTAssertEqual(m?.limitSideLen, 960) + + let eff = OCRParameterResolver.effective(modelConfig: det, runtime: .noOverrides) + let app = OCRParameterResolver.textDetLimitSideLenAppDefault + XCTAssertEqual(eff.detResize.limitSideLen, app) + XCTAssertEqual(eff.mergedLimitSideLen, app) + } + + func testEffectiveDetPostprocessMergesThresholdsOnly() throws { + let dir = FileManager.default.temporaryDirectory + let url = dir.appendingPathComponent("merge_test_post_\(UUID().uuidString).yaml") + let yaml = """ + Global: + model_name: PP-OCRv5_mobile_det + PreProcess: + transform_ops: + - DetResizeForTest: + resize_long: 960 + PostProcess: + name: DBPostProcess + thresh: 0.99 + box_thresh: 0.99 + unclip_ratio: 9.9 + max_candidates: 7 + """ + try yaml.write(to: url, atomically: true, encoding: .utf8) + defer { try? FileManager.default.removeItem(at: url) } + + let det = try InferenceConfig.load(from: url.path) + let post = OCRParameterResolver.effective(modelConfig: det, runtime: .noOverrides) + XCTAssertEqual(post.detThresh, 0.3, accuracy: 0.0001) + XCTAssertEqual(post.detBoxThresh, 0.6, accuracy: 0.0001) + XCTAssertEqual(post.detUnclipRatio, 1.5, accuracy: 0.0001) + XCTAssertEqual(det.postProcess.maxCandidates, 7) + } + + func testDecodeImageImgModeParsedAsRGB() throws { + let dir = FileManager.default.temporaryDirectory + let url = dir.appendingPathComponent("decode_rgb_\(UUID().uuidString).yaml") + let yaml = """ + Global: + model_name: PP-OCRv5_mobile_det + PreProcess: + transform_ops: + - DecodeImage: + channel_first: false + img_mode: RGB + - DetResizeForTest: + resize_long: 960 + PostProcess: + name: DBPostProcess + """ + try yaml.write(to: url, atomically: true, encoding: .utf8) + defer { try? FileManager.default.removeItem(at: url) } + + let cfg = try InferenceConfig.load(from: url.path) + XCTAssertEqual(cfg.decodeImageChannelOrder, .rgb) + } + + func testDecodeImageMissingDefaultsToBGR() throws { + let dir = FileManager.default.temporaryDirectory + let url = dir.appendingPathComponent("decode_default_bgr_\(UUID().uuidString).yaml") + let yaml = """ + Global: + model_name: PP-OCRv5_mobile_det + PreProcess: + transform_ops: + - DetResizeForTest: + resize_long: 960 + PostProcess: + name: DBPostProcess + """ + try yaml.write(to: url, atomically: true, encoding: .utf8) + defer { try? FileManager.default.removeItem(at: url) } + + let cfg = try InferenceConfig.load(from: url.path) + XCTAssertEqual(cfg.decodeImageChannelOrder, .bgr) + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/ORTSessionManagerTests.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/ORTSessionManagerTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..f940823ae7008f45bf529c372398b63137b21fdc --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/ORTSessionManagerTests.swift @@ -0,0 +1,55 @@ +// Copyright (c) 2026 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. + +import XCTest +@testable import PaddleOCRDemo + +final class ORTSessionManagerTests: XCTestCase { + + func testLoadModels() async throws { + let manager = ORTSessionManager() + try await manager.loadModels(executionProvider: .coreML) + } + + func testLoadModelsCPU() async throws { + let manager = ORTSessionManager() + try await manager.loadModels(executionProvider: .cpu) + } + + func testModelConfigResolution() throws { + do { + let detConfig = try ModelConfig.detection() + XCTAssertFalse(detConfig.modelPath.isEmpty, "Det model path should not be empty") + let expected = try InferenceConfig.load(from: detConfig.configPath).modelName + XCTAssertEqual(detConfig.name, expected, "Display name should equal Global.model_name in the det model config") + } catch let error as ModelConfigError { + switch error { + case .modelNotFound(let path): + XCTAssertTrue(path.contains("det"), "Error should reference det model path") + } + } + + do { + let recConfig = try ModelConfig.recognition() + XCTAssertFalse(recConfig.modelPath.isEmpty, "Rec model path should not be empty") + let expected = try InferenceConfig.load(from: recConfig.configPath).modelName + XCTAssertEqual(recConfig.name, expected, "Display name should equal Global.model_name in the rec model config") + } catch let error as ModelConfigError { + switch error { + case .modelNotFound(let path): + XCTAssertTrue(path.contains("rec"), "Error should reference rec model path") + } + } + } +} diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/Podfile b/ocr_webapp/PaddleOCR/deploy/ios_demo/Podfile new file mode 100644 index 0000000000000000000000000000000000000000..b94d6792170cf52cc12737d7f2a132210421f5fa --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/Podfile @@ -0,0 +1,30 @@ +platform :ios, '16.0' + +target 'PaddleOCRDemo' do + use_frameworks! + + # ONNX Runtime with ObjC API — includes Core ML EP + XNNPACK EP pre-built + pod 'onnxruntime-objc', '~> 1.24' + + # YAML parsing for model config files + pod 'Yams', '~> 5.0' + + # OpenCV (imgproc): preprocessing resize/pad, recognition resize, quad crop, DB contours — see OpenCV*Bridge. + pod 'OpenCV', '~> 4.3.0' + + # Unit tests `@testable import PaddleOCRDemo` need the same Swift / header search paths + # as the app without duplicating pod linkage. + target 'PaddleOCRDemoTests' do + inherit! :search_paths + end +end + +# onnxruntime-objc ships headers that use #include "..." inside framework headers; newer Xcode +# treats that as an error (CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER). Allow Pods to build. +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER'] = 'NO' + end + end +end diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/Podfile.lock b/ocr_webapp/PaddleOCR/deploy/ios_demo/Podfile.lock new file mode 100644 index 0000000000000000000000000000000000000000..ded7f54f62f1379d80b58d7d4a71a38ba0716c0c --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/Podfile.lock @@ -0,0 +1,30 @@ +PODS: + - onnxruntime-c (1.24.3) + - onnxruntime-objc (1.24.3): + - onnxruntime-objc/Core (= 1.24.3) + - onnxruntime-objc/Core (1.24.3): + - onnxruntime-c (= 1.24.3) + - OpenCV (4.3.0) + - Yams (5.0.6) + +DEPENDENCIES: + - onnxruntime-objc (~> 1.24) + - OpenCV (~> 4.3.0) + - Yams (~> 5.0) + +SPEC REPOS: + trunk: + - onnxruntime-c + - onnxruntime-objc + - OpenCV + - Yams + +SPEC CHECKSUMS: + onnxruntime-c: 4a1a1a81da2087869dc9b6357f182952db2df29c + onnxruntime-objc: f9310465f0a284682811d071b7eb8e93f10517b8 + OpenCV: 681e0451c1e54c5930a8922277f817ed5d5ab459 + Yams: e10dae147f517ed57ecae37c5e8681bdf8fcab65 + +PODFILE CHECKSUM: 24111b5e94db1b6a72362b62148f587abc1a06be + +COCOAPODS: 1.16.2 diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/README.md b/ocr_webapp/PaddleOCR/deploy/ios_demo/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3000495e328eb396304001fff686cf9c4cbc8ab5 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/README.md @@ -0,0 +1,3 @@ +# ios Demo + +参考 https://github.com/PaddlePaddle/Paddle-Lite-Demo/blob/develop/ocr/ios/ppocr_demo/ppocr_demo/README.md diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/README_en.md b/ocr_webapp/PaddleOCR/deploy/ios_demo/README_en.md new file mode 100644 index 0000000000000000000000000000000000000000..540dbd6baa5201fcf2f84a5b45531902a5339bbc --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/README_en.md @@ -0,0 +1,5 @@ +# PaddleOCR iOS Demo + +[中文](./README.md) | English + +Please refer to the [documentation](../../docs/version3.x/inference_deployment/cross_platform/ios_deployment.en.md). diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-accuracy.txt b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-accuracy.txt new file mode 100644 index 0000000000000000000000000000000000000000..302908ec31e34e9184e48263c6c47b5e2ba85a3b --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-accuracy.txt @@ -0,0 +1,2 @@ +PyYAML>=6.0 +shapely>=2.0 diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-build-calib.txt b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-build-calib.txt new file mode 100644 index 0000000000000000000000000000000000000000..917552454c9d8d2532b10ab5dc3a3aa5afc8ca02 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-build-calib.txt @@ -0,0 +1,4 @@ +numpy +paddlex>=3.5,<3.6 +onnxruntime>=1.18 +opencv-contrib-python>=4.5 diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-onnx-convert-ort.txt b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-onnx-convert-ort.txt new file mode 100644 index 0000000000000000000000000000000000000000..a58c03cd1be779e60d95f21b0c8ba3610565185b --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-onnx-convert-ort.txt @@ -0,0 +1,3 @@ +numpy>=1.20 +onnx>=1.16,<1.21 +onnxruntime>=1.18 diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-onnx-quantize.txt b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-onnx-quantize.txt new file mode 100644 index 0000000000000000000000000000000000000000..dc405192303302dd29bd5d4e21e70f87b19778a3 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-onnx-quantize.txt @@ -0,0 +1,5 @@ +# Keep onnx and onnxruntime within a range that works together (avoids IR / schema skew on older CPU wheels). +# For best parity with the app, install a onnxruntime major version close to onnxruntime-objc in the Podfile. +numpy>=1.20 +onnx>=1.16,<1.21 +onnxruntime>=1.18 diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/build_onnx_calib_npy.py b/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/build_onnx_calib_npy.py new file mode 100644 index 0000000000000000000000000000000000000000..d3cf1d0fd1a41e226c67ac6cd4c46a201c84a245 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/build_onnx_calib_npy.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 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. + +"""Build float32 NCHW ``.npy`` tensors for static ONNX quantization. + +Note: + Uses undocumented PaddleX predictor internals (e.g. ``pre_tfs``). May break when + upgrading PaddleX. + +Examples: + + # Detection model + python3 scripts/build_onnx_calib_npy.py --task det \\ + --model-dir PaddleOCRDemo/Models/det --image-dir /path/to/images --output-dir calib_det + + # Recognition model (one tensor per full image) + python3 scripts/build_onnx_calib_npy.py --task rec \\ + --model-dir PaddleOCRDemo/Models/rec --image-dir /path/to/images --output-dir calib_rec + + # Recognition: full-page images — run det to crop line boxes, one tensor per crop + python3 scripts/build_onnx_calib_npy.py --task rec \\ + --model-dir PaddleOCRDemo/Models/rec --det-model-dir PaddleOCRDemo/Models/det \\ + --image-dir /path/to/pages --output-dir calib_rec_crops +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path +from typing import Any, Iterator + +import cv2 +import numpy as np +from paddlex import create_predictor + +_script_dir = Path(__file__).resolve().parent +if str(_script_dir) not in sys.path: + sys.path.insert(0, str(_script_dir)) + +from utils import die + + +def _read_model_name(model_dir: Path) -> str: + try: + import yaml + except ImportError as e: + die(f"PyYAML is required: {e}") + p = model_dir / "inference.yml" + if not p.is_file(): + die(f"missing {p}") + with p.open("r", encoding="utf-8") as f: + data = yaml.safe_load(f) + name = (data or {}).get("Global", {}).get("model_name") + if not name: + die("Global.model_name not found in inference.yml") + return str(name) + + +def _collect_images(image_dir: Path) -> list[Path]: + exts = {".png", ".jpg", ".jpeg", ".bmp", ".webp", ".tif", ".tiff"} + out: list[Path] = [] + for p in sorted(image_dir.iterdir()): + if p.is_file() and p.suffix.lower() in exts: + out.append(p) + if not out: + die(f"no images found under {image_dir}") + return out + + +def _safe_stem(p: Path, idx: int) -> str: + s = p.stem + s = re.sub(r"[^\w\-.]+", "_", s)[:80] + return f"{idx:04d}_{s}" if s else f"{idx:04d}" + + +def _iter_text_boxes(dt_polys: Any) -> Iterator[np.ndarray]: + """Turn ``dt_polys`` from a TextDetResult into a sequence of (N,2) point arrays.""" + if dt_polys is None: + return + if isinstance(dt_polys, np.ndarray): + if dt_polys.size == 0: + return + if dt_polys.ndim == 2 and dt_polys.shape[0] >= 3: + yield np.array(dt_polys, dtype=np.float32, copy=True) + elif dt_polys.ndim == 3: + for i in range(dt_polys.shape[0]): + box = np.array(dt_polys[i], dtype=np.float32, copy=True) + if box.shape[0] >= 3: + yield box + return + for box in dt_polys: + b = np.array(box, dtype=np.float32, copy=True) + if b.size >= 6 and b.ndim == 2 and b.shape[0] >= 3: + yield b + + +def _minarea_rect_rotate_crop(img: np.ndarray, points: np.ndarray) -> np.ndarray: + """ + Min-area rect crop aligned with PP-OCR (see PaddleX CropByPolys, quad). + *img* is RGB (same as TextDet Read output). + """ + tmp_box = np.array(points, dtype=np.float32, copy=True) + bounding_box = cv2.minAreaRect(tmp_box.astype(np.int32)) + pts = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0]) + if pts[1][1] > pts[0][1]: + index_a, index_d = 0, 1 + else: + index_a, index_d = 1, 0 + if pts[3][1] > pts[2][1]: + index_b, index_c = 2, 3 + else: + index_b, index_c = 3, 2 + box = [pts[index_a], pts[index_b], pts[index_c], pts[index_d]] + pts4 = [np.array(p, dtype=np.float32) for p in box] + assert len(pts4) == 4, "shape of points must be 4*2" + img_crop_width = int( + max( + np.linalg.norm(pts4[0] - pts4[1]), + np.linalg.norm(pts4[2] - pts4[3]), + ) + ) + img_crop_height = int( + max( + np.linalg.norm(pts4[0] - pts4[3]), + np.linalg.norm(pts4[1] - pts4[2]), + ) + ) + if img_crop_width < 1 or img_crop_height < 1: + raise ValueError("degenerate text crop (zero width/height)") + pts_std = np.float32( + [ + [0, 0], + [img_crop_width, 0], + [img_crop_width, img_crop_height], + [0, img_crop_height], + ] + ) + pstack = np.stack(pts4, axis=0) + m = cv2.getPerspectiveTransform(pstack, pts_std) + dst = cv2.warpPerspective( + img, + m, + (img_crop_width, img_crop_height), + borderMode=cv2.BORDER_REPLICATE, + flags=cv2.INTER_CUBIC, + ) + dh, dw = dst.shape[0:2] + if dh * 1.0 / max(dw, 1) >= 1.5: + dst = np.rot90(dst) + return dst + + +def _tensor_det(pred, image_path: str) -> np.ndarray: + """Mirror TextDetRunnerPredictor.process() preprocess, return (1,C,H,W) float32.""" + batches = list(pred.batch_sampler([image_path])) + b = batches[0] + batch_raw_imgs = pred.pre_tfs["Read"](imgs=b.instances) + batch_imgs, _ = pred.pre_tfs["Resize"]( + imgs=batch_raw_imgs, + limit_side_len=pred.limit_side_len, + limit_type=pred.limit_type, + max_side_limit=pred.max_side_limit, + ) + batch_imgs = pred.pre_tfs["Normalize"](imgs=batch_imgs) + batch_imgs = pred.pre_tfs["ToCHW"](imgs=batch_imgs) + x = pred.pre_tfs["ToBatch"](imgs=batch_imgs) + if not x: + die("ToBatch returned empty (det)") + return np.ascontiguousarray(x[0], dtype=np.float32) + + +def _tensor_rec(pred, image_path: str) -> np.ndarray: + from paddlex.inference.models.text_recognition.processors import ( + validate_text_rec_image_array, + ) + + batches = list(pred.batch_sampler([image_path])) + b = batches[0] + batch_raw_imgs = pred.pre_tfs["Read"](imgs=b.instances) + for i, img in enumerate(batch_raw_imgs): + validate_text_rec_image_array(img, index=i) + batch_imgs = pred.pre_tfs["ReisizeNorm"](imgs=batch_raw_imgs) + x = pred.pre_tfs["ToBatch"](imgs=batch_imgs) + if not x: + die("ToBatch returned empty (rec)") + return np.ascontiguousarray(x[0], dtype=np.float32) + + +def _tensor_rec_from_rgb_ndarray(rec_pred, img_rgb: np.ndarray) -> np.ndarray: + """Same preprocessing as *path* input, for an RGB array (e.g. det crop on full page).""" + from paddlex.inference.models.text_recognition.processors import ( + validate_text_rec_image_array, + ) + + bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR) + batch_raw_imgs = rec_pred.pre_tfs["Read"](imgs=[bgr]) + for i, img in enumerate(batch_raw_imgs): + validate_text_rec_image_array(img, index=i) + batch_imgs = rec_pred.pre_tfs["ReisizeNorm"](imgs=batch_raw_imgs) + x = rec_pred.pre_tfs["ToBatch"](imgs=batch_imgs) + if not x: + die("ToBatch returned empty (rec, ndarray)") + return np.ascontiguousarray(x[0], dtype=np.float32) + + +def main() -> None: + ap = argparse.ArgumentParser( + description="Build float32 NCHW .npy files for static ONNX quantize." + ) + ap.add_argument( + "--image-dir", + type=Path, + required=True, + help="Directory of input images.", + ) + ap.add_argument( + "--output-dir", + type=Path, + required=True, + help="Output directory for .npy files (created if missing).", + ) + ap.add_argument( + "--model-dir", + type=Path, + required=True, + help="Model directory.", + ) + ap.add_argument( + "--task", + choices=("det", "rec"), + required=True, + help="Model role: det = text detection, rec = text recognition.", + ) + ap.add_argument( + "--det-model-dir", + type=Path, + default=None, + help=( + "For --task rec only: text-detection model directory. " + "When set, each full image is run through det; each text box is cropped and one " + "rec calibration tensor is written per crop (instead of one tensor per file)." + ), + ) + ap.add_argument( + "--max-crops-per-image", + type=int, + default=0, + help="With --det-model-dir, cap how many box crops to use per input image (0 = no cap).", + ) + ap.add_argument("--device", default="cpu", help="Device string, e.g. cpu or gpu:0.") + args = ap.parse_args() + + if not args.image_dir.is_dir(): + die(f"not a directory: {args.image_dir}") + if not (args.model_dir / "inference.onnx").is_file(): + die(f"missing {args.model_dir / 'inference.onnx'}") + + if args.det_model_dir is not None: + if args.task != "rec": + die("--det-model-dir is only valid with --task rec") + if not (args.det_model_dir / "inference.onnx").is_file(): + die(f"missing {args.det_model_dir / 'inference.onnx'}") + + model_name = _read_model_name(args.model_dir) + if args.task == "det" and "_rec" in model_name and "_det" not in model_name: + print( + "warning: --task det but model_name looks like recognition; check --model-dir", + file=sys.stderr, + ) + if args.task == "rec" and "_det" in model_name and "_rec" not in model_name: + print( + "warning: --task rec but model_name looks like detection; check --model-dir", + file=sys.stderr, + ) + + try: + pred = create_predictor( + model_name, + model_dir=str(args.model_dir.resolve()), + device=args.device, + engine="onnxruntime", + batch_size=1, + ) + except Exception as e: + die(f"create_predictor failed: {e}") + + det_pred = None + det_name = None + if args.det_model_dir is not None: + det_name = _read_model_name(args.det_model_dir) + if "_rec" in det_name and "_det" not in det_name: + print( + "warning: --det-model-dir model_name looks like recognition; check path", + file=sys.stderr, + ) + try: + det_pred = create_predictor( + det_name, + model_dir=str(args.det_model_dir.resolve()), + device=args.device, + engine="onnxruntime", + batch_size=1, + ) + except Exception as e: + die(f"create_predictor (det) failed: {e}") + + out_dir = args.output_dir + out_dir.mkdir(parents=True, exist_ok=True) + + images = _collect_images(args.image_dir) + written = 0 + for idx, path in enumerate(images): + pstr = str(path.resolve()) + if args.task == "det": + try: + arr = _tensor_det(pred, pstr) + except Exception as e: + die(f"preprocess failed for {path}: {e}") + if arr.dtype != np.float32: + arr = arr.astype(np.float32) + name = _safe_stem(path, idx) + ".npy" + out_path = out_dir / name + np.save(str(out_path), arr) + print(f"wrote {out_path} shape={arr.shape} dtype={arr.dtype}") + written += 1 + continue + + # rec + if det_pred is None: + try: + arr = _tensor_rec(pred, pstr) + except Exception as e: + die(f"preprocess failed for {path}: {e}") + if arr.dtype != np.float32: + arr = arr.astype(np.float32) + out_path = out_dir / (_safe_stem(path, idx) + ".npy") + np.save(str(out_path), arr) + print(f"wrote {out_path} shape={arr.shape} dtype={arr.dtype}") + written += 1 + continue + + # rec + det: crops from full page + try: + det_res = next(det_pred(pstr)) # type: ignore[union-attr] + except Exception as e: + die(f"detection failed for {path}: {e}") + polys = list(_iter_text_boxes(det_res["dt_polys"])) + if not polys: + print( + f"warning: no text boxes for {path}; skipping (no .npy written)", + file=sys.stderr, + ) + continue + if args.max_crops_per_image > 0: + polys = polys[: args.max_crops_per_image] + full_rgb = det_res["input_img"] + if isinstance(full_rgb, (list, tuple)) and len(full_rgb) == 1: + full_rgb = full_rgb[0] + for box_i, quad in enumerate(polys): + try: + line_rgb = _minarea_rect_rotate_crop(full_rgb, quad) + except Exception as e: + print( + f"warning: skip box {box_i} in {path}: {e}", + file=sys.stderr, + ) + continue + try: + arr = _tensor_rec_from_rgb_ndarray(pred, line_rgb) + except Exception as e: + die(f"rec preprocess failed for {path} box {box_i}: {e}") + if arr.dtype != np.float32: + arr = arr.astype(np.float32) + out_path = out_dir / (f"{_safe_stem(path, idx)}_box{box_i:03d}.npy") + np.save(str(out_path), arr) + print(f"wrote {out_path} shape={arr.shape} dtype={arr.dtype}") + written += 1 + + print(f"done. {written} .npy file(s) -> {out_dir}") + + +if __name__ == "__main__": + main() diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/compare_ocr_json.py b/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/compare_ocr_json.py new file mode 100644 index 0000000000000000000000000000000000000000..ad5d536f95f9239e8a271650d6986a03f2d1d914 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/compare_ocr_json.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 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. + +"""Compare two OCR JSON files (reference vs device) using polygon IoU + CER. + +Default thresholds (see argparse defaults) target a stricter CI gate than a loose +0.5 IoU baseline: tighter box alignment, ~95% char-level headroom on mean CER, +and limited reference-side misses. + +Example: + python compare_ocr_json.py ref.json ios.json \\ + --iou-threshold 0.65 --cer-threshold 0.05 --max-unmatched-ratio 0.1 +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any, Dict, List, Sequence, Tuple + + +def _levenshtein(a: str, b: str) -> int: + if a == b: + return 0 + if not a: + return len(b) + if not b: + return len(a) + prev = list(range(len(b) + 1)) + for i, ca in enumerate(a, 1): + cur = [i] + for j, cb in enumerate(b, 1): + ins = cur[j - 1] + 1 + delete = prev[j] + 1 + sub = prev[j - 1] + (ca != cb) + cur.append(min(ins, delete, sub)) + prev = cur + return prev[-1] + + +def _cer(ref: str, hyp: str) -> float: + if not ref and not hyp: + return 0.0 + if not ref: + return 1.0 + return _levenshtein(ref, hyp) / max(len(ref), 1) + + +def _polygon_iou( + poly_a: Sequence[Sequence[float]], poly_b: Sequence[Sequence[float]] +) -> float: + try: + from shapely.geometry import Polygon + except ImportError as exc: + raise RuntimeError( + "compare_ocr_json.py requires `pip install shapely`" + ) from exc + + def _to_poly(p: Sequence[Sequence[float]]) -> Polygon: + pts = [(float(x), float(y)) for x, y in p] + if len(pts) < 3: + return Polygon() + if pts[0] != pts[-1]: + pts = pts + [pts[0]] + return Polygon(pts) + + p1 = _to_poly(poly_a) + p2 = _to_poly(poly_b) + if p1.is_empty or p2.is_empty or not p1.is_valid or not p2.is_valid: + return 0.0 + inter = p1.intersection(p2).area + union = p1.union(p2).area + if union <= 0: + return 0.0 + return float(inter / union) + + +def _load_items(path: Path) -> List[Dict[str, Any]]: + with path.open("r", encoding="utf-8") as f: + data = json.load(f) + items = data.get("items") + if not isinstance(items, list): + raise ValueError(f"{path}: missing 'items' array") + return items + + +def _greedy_match( + ref_items: List[Dict[str, Any]], + hyp_items: List[Dict[str, Any]], + iou_threshold: float, +) -> Tuple[List[Tuple[int, int, float]], List[int], List[int]]: + """Return (pairs as ref_idx, hyp_idx, iou), unmatched_ref, unmatched_hyp.""" + candidates: List[Tuple[float, int, int]] = [] + for i, ri in enumerate(ref_items): + ra = ri.get("polygon") + if not isinstance(ra, list): + continue + for j, hj in enumerate(hyp_items): + ha = hj.get("polygon") + if not isinstance(ha, list): + continue + iou = _polygon_iou(ra, ha) + candidates.append((iou, i, j)) + + candidates.sort(key=lambda t: t[0], reverse=True) + used_r = set() + used_h = set() + pairs: List[Tuple[int, int, float]] = [] + for iou, i, j in candidates: + if iou < iou_threshold: + break + if i in used_r or j in used_h: + continue + used_r.add(i) + used_h.add(j) + pairs.append((i, j, iou)) + + unmatched_r = [i for i in range(len(ref_items)) if i not in used_r] + unmatched_h = [j for j in range(len(hyp_items)) if j not in used_h] + return pairs, unmatched_r, unmatched_h + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Compare OCR JSON outputs") + parser.add_argument( + "reference", type=Path, help="Reference JSON (e.g. paddleocr_reference)" + ) + parser.add_argument("hypothesis", type=Path, help="Device / second JSON") + parser.add_argument( + "--iou-threshold", + type=float, + default=0.65, + help="Min IoU to pair quadrilateral boxes (stricter than common 0.5 VOC-style cutoff)", + ) + parser.add_argument( + "--cer-threshold", + type=float, + default=0.05, + help="Fail if mean CER on matched pairs exceeds this (e.g. 0.05 ≈ 95%% char accuracy headroom)", + ) + parser.add_argument( + "--max-unmatched-ratio", + type=float, + default=0.1, + help="Fail if unmatched ref lines / len(ref) exceeds this", + ) + parser.add_argument( + "--json-summary-out", + type=Path, + default=None, + help="Write the same JSON as stdout to this path (PASS or FAIL) for generate_benchmark_report.py", + ) + args = parser.parse_args(list(argv) if argv is not None else None) + + ref_items = _load_items(args.reference) + hyp_items = _load_items(args.hypothesis) + + pairs, unmatched_r, unmatched_h = _greedy_match( + ref_items, hyp_items, args.iou_threshold + ) + + cers: List[float] = [] + for ri, hj, _ in pairs: + rt = str(ref_items[ri].get("text", "")) + ht = str(hyp_items[hj].get("text", "")) + cers.append(_cer(rt, ht)) + + mean_cer = sum(cers) / len(cers) if cers else 0.0 + nref = max(len(ref_items), 1) + unmatched_ratio = len(unmatched_r) / nref + + report = { + "matched_pairs": len(pairs), + "reference_count": len(ref_items), + "hypothesis_count": len(hyp_items), + "unmatched_reference": len(unmatched_r), + "unmatched_hypothesis": len(unmatched_h), + "unmatched_reference_ratio": unmatched_ratio, + "mean_cer_matched": mean_cer, + "iou_threshold": args.iou_threshold, + "cer_threshold": args.cer_threshold, + "max_unmatched_ratio": args.max_unmatched_ratio, + } + failed = False + if mean_cer > args.cer_threshold: + print(f"FAIL: mean CER {mean_cer:.4f} > {args.cer_threshold}", file=sys.stderr) + failed = True + if unmatched_ratio > args.max_unmatched_ratio: + print( + f"FAIL: unmatched ref ratio {unmatched_ratio:.4f} > {args.max_unmatched_ratio}", + file=sys.stderr, + ) + failed = True + + report["pass"] = not failed + out_txt = json.dumps(report, indent=2, ensure_ascii=False) + print(out_txt) + + if args.json_summary_out is not None: + args.json_summary_out.parent.mkdir(parents=True, exist_ok=True) + args.json_summary_out.write_text(out_txt + "\n", encoding="utf-8") + + if not failed: + print("PASS", file=sys.stderr) + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/convert_onnx_to_ort.sh b/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/convert_onnx_to_ort.sh new file mode 100644 index 0000000000000000000000000000000000000000..3ccd8be21daa8d4d15f61a318978e38896cb7a36 --- /dev/null +++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/convert_onnx_to_ort.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 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 +# +# Convert ONNX models to ORT format for ONNX Runtime. +# See: https://onnxruntime.ai/docs/performance/model-optimizations/ort-format-models.html +# +# Requires: python3 +# +# Usage: +# ./scripts/convert_onnx_to_ort.sh +# ./scripts/convert_onnx_to_ort.sh --output-dir ./out/ort_bundles +# ./scripts/convert_onnx_to_ort.sh --input-dir /path/to/models --output-dir /tmp/ort +# ./scripts/convert_onnx_to_ort.sh --optimization-style Runtime + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +IOS_DEMO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +DEFAULT_INPUT_DIR="${IOS_DEMO_ROOT}/PaddleOCRDemo/Models" + +INPUT_DIR="${DEFAULT_INPUT_DIR}" +ORT_OUTPUT_DIR="" +KEEP_OPERATOR_CONFIG=false +EXPLICIT_OPTIMIZATION_STYLE=false +OPTIMIZATION_STYLE_VALUES=() + +usage() { + cat < Root directory to scan for *.onnx (recursive). Default: PaddleOCRDemo/Models. + --output-dir If set, write .ort files under this directory, mirroring relative paths from + the input tree; also copies inference.yml beside each converted bundle (same + layout as under --input-dir) so you can swap in the whole output folder. If + omitted, write next to each .onnx in-place. + --keep-operator-config + Keep required_operators*.config files emitted by the ORT converter (for + minimal ORT builds). By default these are removed after a successful run. + --optimization-style