diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..695801a0a46a96e45fdd431a08190c37e27e8c38 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,12 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +ocr_webapp/data/output/rec_cham_inference/inference.pdiparams filter=lfs diff=lfs merge=lfs -text +ocr_webapp/data/output/rec_cham_inference_latest/inference.pdiparams filter=lfs diff=lfs merge=lfs -text +ocr_webapp/data/output/rec_cham_inference_v18/inference.pdiparams filter=lfs diff=lfs merge=lfs -text +ocr_webapp/data/output/rec_cham_inference_v21/inference.pdiparams filter=lfs diff=lfs merge=lfs -text +ocr_webapp/data/output/rec_cham_inference_v22/inference.pdiparams filter=lfs diff=lfs merge=lfs -text +ocr_webapp/data/output/rec_cham_inference_v23/inference.pdiparams filter=lfs diff=lfs merge=lfs -text +ocr_webapp/data/output/rec_cham_inference_v23/inference.pdiparams.info filter=lfs diff=lfs merge=lfs -text +ocr_webapp/data/output/rec_cham_inference_v23/inference.pdmodel filter=lfs diff=lfs merge=lfs -text +ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/imgs/paper/db.jpg filter=lfs diff=lfs merge=lfs -text diff --git a/ocr_webapp/PaddleOCR/.clang_format.hook b/ocr_webapp/PaddleOCR/.clang_format.hook new file mode 100644 index 0000000000000000000000000000000000000000..fdc3c054c0be41a1bda613fc572af0d9cf6f3c13 --- /dev/null +++ b/ocr_webapp/PaddleOCR/.clang_format.hook @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +readonly VERSION="13.0.0" + +version=$(clang-format -version) + +if ! [[ $(python -V 2>&1 | awk '{print $2}' | awk -F '.' '{print $1$2}') -ge 36 ]]; then + echo "clang-format installation by pip need python version great equal 3.6, + please change the default python to higher version." + exit 1 +fi + +if ! [[ $version == *"$VERSION"* ]]; then + # low version of pip may not have the source of clang-format whl + pip install --upgrade pip + pip install clang-format==13.0.0 +fi + +clang-format $@ diff --git a/ocr_webapp/PaddleOCR/.github/ISSUE_TEMPLATE/bug-report.yml b/ocr_webapp/PaddleOCR/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000000000000000000000000000000000000..084cbeb5d4c328406ea1c75063c077374a1dcdaf --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,62 @@ +# PaddleOCR + +name: 🐛 Bug Report +description: Problems with PaddleOCR +body: + - type: markdown + attributes: + value: | + Thank you for submitting a PaddleOCR 🐛 Bug Report! + + - type: checkboxes + attributes: + label: 🔎 Search before asking + description: > + Please search the PaddleOCR [Docs](https://paddlepaddle.github.io/PaddleOCR/), [Issues](https://github.com/PaddlePaddle/PaddleOCR/issues) and [Discussions](https://github.com/PaddlePaddle/PaddleOCR/discussions) to see if a similar bug report already exists. + options: + - label: I have searched the PaddleOCR [Docs](https://paddlepaddle.github.io/PaddleOCR/) and found no similar bug report. + required: true + - label: I have searched the PaddleOCR [Issues](https://github.com/PaddlePaddle/PaddleOCR/issues) and found no similar bug report. + required: true + - label: I have searched the PaddleOCR [Discussions](https://github.com/PaddlePaddle/PaddleOCR/discussions) and found no similar bug report. + required: true + + - type: textarea + attributes: + label: 🐛 Bug (问题描述) + description: Provide console output with error messages and/or screenshots of the bug. (请提供详细报错信息或者截图) + placeholder: | + 💡 ProTip! Include as much information as possible (screenshots, logs, tracebacks etc.) to receive the most helpful response. + validations: + required: true + + - type: textarea + attributes: + label: 🏃‍♂️ Environment (运行环境) + description: Please specify the software and hardware you used to produce the bug. (请给出详细依赖包信息,便于复现问题) + placeholder: | + ```bash + OS macOS-13.5.2 + Environment Jupyter + Python 3.11.2 + PaddleOCR 2.8.1 + Install git + RAM 16.00 GB + CPU Apple M2 + CUDA None + ``` + validations: + required: true + + - type: textarea + attributes: + label: 🌰 Minimal Reproducible Example (最小可复现问题的Demo) + description: > + When asking a question, people will be better able to provide help if you provide code that they can easily understand and use to **reproduce** the problem. + This is referred to by community members as creating a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). (请务必提供该Demo,这样节省大家时间) + placeholder: | + ```bash + # Code to reproduce your issue here + ``` + validations: + required: true diff --git a/ocr_webapp/PaddleOCR/.github/ISSUE_TEMPLATE/config.yml b/ocr_webapp/PaddleOCR/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..520777db85ee7a084670ce19871bcbacaa86554f --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: 🙏 Q&A + url: https://github.com/PaddlePaddle/PaddleOCR/discussions/categories/q-a + about: Ask the community for help + - name: 💡 Feature requests and ideas + url: https://github.com/PaddlePaddle/PaddleOCR/discussions/categories/ideas + about: Share ideas for new features + - name: 🙌 Show and tell + url: https://github.com/PaddlePaddle/PaddleOCR/discussions/categories/show-and-tell + about: Show off something you've made diff --git a/ocr_webapp/PaddleOCR/.github/ISSUE_TEMPLATE/custom.md b/ocr_webapp/PaddleOCR/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000000000000000000000000000000000000..46039fdcf323ddef1cab77131b5e6efed39579aa --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,17 @@ +--- +name: Issue template +about: Issue template for code error. +title: '' +labels: '' +assignees: '' + +--- + +请提供下述完整信息以便快速定位问题/Please provide the following information to quickly locate the problem + +- 系统环境/System Environment: +- 版本号/Version:Paddle: PaddleOCR: 问题相关组件/Related components: +- 运行指令/Command Code: +- 完整报错/Complete Error Message: + +请尽量不要包含图片在问题中/Please try to not include the image in the issue. diff --git a/ocr_webapp/PaddleOCR/.github/ISSUE_TEMPLATE/newfeature.md b/ocr_webapp/PaddleOCR/.github/ISSUE_TEMPLATE/newfeature.md new file mode 100644 index 0000000000000000000000000000000000000000..4ffcbbb564952c811bddf62d9294ffb095bd90fd --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/ISSUE_TEMPLATE/newfeature.md @@ -0,0 +1,17 @@ +--- +name: New Feature Issue template +about: Issue template for new features. +title: '' +labels: 'Code PR is needed' +assignees: 'shiyutang' + +--- + +## 背景 + +经过需求征集https://github.com/PaddlePaddle/PaddleOCR/issues/10334 和每周技术研讨会 https://github.com/PaddlePaddle/PaddleOCR/issues/10223 讨论,我们确定了XXXX任务。 + +## 解决步骤 +1. 根据开源代码进行网络结构、评估指标转换。代码链接:XXXX +2. 结合[论文复现指南](https://github.com/PaddlePaddle/models/blob/release%2F2.2/tutorials/article-implementation/ArticleReproduction_CV.md),进行前反向对齐等操作,达到论文Table.1中的指标。 +3. 参考[PR提交规范](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.6/doc/doc_ch/code_and_doc.md)提交代码PR到ppocr中。 diff --git a/ocr_webapp/PaddleOCR/.github/actions/detect-docs-only/action.yml b/ocr_webapp/PaddleOCR/.github/actions/detect-docs-only/action.yml new file mode 100644 index 0000000000000000000000000000000000000000..93e5fc1fb7d4c2df91b64716689b378dbd63597c --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/actions/detect-docs-only/action.yml @@ -0,0 +1,55 @@ +name: Detect Docs-Only Change +description: > + Output docs_only=true if every changed file in the current pull_request + matches docs/**, **/*.md, or .github/**. On push or workflow_dispatch, + always output docs_only=false. +outputs: + docs_only: + description: "true if change is docs-only, otherwise false" + value: ${{ steps.compute.outputs.docs_only }} +runs: + using: composite + steps: + - id: compute + shell: bash + env: + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_BASE_REF: ${{ github.base_ref }} + run: | + set -euo pipefail + if [ "${GITHUB_EVENT_NAME}" != "pull_request" ]; then + echo "docs_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + git fetch origin "${GITHUB_BASE_REF}" --depth=1 >/dev/null 2>&1 || true + CHANGED_FILES="$(git diff --name-only "origin/${GITHUB_BASE_REF}...HEAD")" + # >>> matcher-begin + shopt -s globstar extglob nullglob + is_docs_only() { + local files="$1" + if [ -z "$files" ]; then + echo "false"; return 0 + fi + while IFS= read -r f; do + [ -z "$f" ] && continue + case "$f" in + docs/*) ;; + .github/*) ;; + *.md) ;; + *) + # also accept nested */**/*.md via case glob + case "$f" in + */*.md) ;; + *) echo "false"; return 0 ;; + esac + ;; + esac + done <<< "$files" + echo "true" + } + result="$(is_docs_only "${CHANGED_FILES}")" + if [ -n "${GITHUB_OUTPUT:-}" ] && [ "${GITHUB_OUTPUT}" != "/dev/null" ]; then + echo "docs_only=${result}" >> "$GITHUB_OUTPUT" + fi + echo "${result}" + # <<< matcher-end diff --git a/ocr_webapp/PaddleOCR/.github/dependabot.yml b/ocr_webapp/PaddleOCR/.github/dependabot.yml new file mode 100644 index 0000000000000000000000000000000000000000..3bb32a93a303a8c098eac7e0043d51702d022a38 --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: weekly diff --git a/ocr_webapp/PaddleOCR/.github/pull_request_template.md b/ocr_webapp/PaddleOCR/.github/pull_request_template.md new file mode 100644 index 0000000000000000000000000000000000000000..65f32f2cb7275cf1d10a088f45e2eed848cef637 --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/pull_request_template.md @@ -0,0 +1,15 @@ +### PR 类型 PR types + + +### PR 变化内容类型 PR changes + + +### 描述 Description + + +### 提PR之前的检查 Check-list + +- [ ] 这个 PR 是提交到dygraph分支或者是一个cherry-pick,否则请先提交到dygarph分支。 + This PR is pushed to the dygraph branch or cherry-picked from the dygraph branch. Otherwise, please push your changes to the dygraph branch. +- [ ] 这个PR清楚描述了功能,帮助评审能提升效率。This PR have fully described what it does such that reviewers can speedup. +- [ ] 这个PR已经经过本地测试。This PR can be covered by existing tests or locally verified. diff --git a/ocr_webapp/PaddleOCR/.github/workflows/build_publish_develop_docs.yml b/ocr_webapp/PaddleOCR/.github/workflows/build_publish_develop_docs.yml new file mode 100644 index 0000000000000000000000000000000000000000..d14f3004b6dcf5a7f5af9b439cc39e43beeec735 --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/workflows/build_publish_develop_docs.yml @@ -0,0 +1,39 @@ +name: Build/Publish Develop Docs +on: + push: + branches: + - master + - main +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: actions/setup-python@v6 + with: + python-version: 3.x + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v5 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - run: pip install mike mkdocs-material jieba mkdocs-git-revision-date-localized-plugin mkdocs-git-committers-plugin-2 mkdocs-static-i18n markdown-callouts + - name: Check docs GitHub source links + run: python tools/check_docs_github_links.py --repo-slug PaddlePaddle/PaddleOCR --forbidden-ref main --forbidden-ref master + - name: Resolve docs GitHub source refs + run: python tools/resolve_doc_github_refs.py --placeholder '{{PADDLEOCR_GITHUB_REF}}' --source-ref "${REF_NAME}" + env: + REF_NAME: ${{ github.ref_name }} + - run: | + git fetch origin gh-pages --depth=1 + mike deploy --push --update-aliases main latest + env: + DOCS_EDIT_URI: edit/${{ github.ref_name }}/docs/ diff --git a/ocr_webapp/PaddleOCR/.github/workflows/build_publish_release_docs.yml b/ocr_webapp/PaddleOCR/.github/workflows/build_publish_release_docs.yml new file mode 100644 index 0000000000000000000000000000000000000000..d03317387ac4a08ce819fc52a16aa82b4979c62e --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/workflows/build_publish_release_docs.yml @@ -0,0 +1,41 @@ +name: Build/Publish Release Docs +on: + push: + tags: + - v* + +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email github-actions[bot]@users.noreply.github.com + - uses: actions/setup-python@v6 + with: + python-version: 3.x + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v5 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - run: pip install mike mkdocs-material jieba mkdocs-git-revision-date-localized-plugin mkdocs-git-committers-plugin-2 mkdocs-static-i18n markdown-callouts + - name: Check docs GitHub source links + run: python tools/check_docs_github_links.py --repo-slug PaddlePaddle/PaddleOCR --forbidden-ref main --forbidden-ref master + - name: Resolve docs GitHub source refs + run: python tools/resolve_doc_github_refs.py --placeholder '{{PADDLEOCR_GITHUB_REF}}' --source-ref "${REF_NAME}" + env: + REF_NAME: ${{ github.ref_name }} + - run: | + git fetch origin gh-pages --depth=1 + mike deploy --push "${REF_NAME}" + + env: + REF_NAME: ${{ github.ref_name }} + DOCS_EDIT_URI: blob/${{ github.ref_name }}/docs/ diff --git a/ocr_webapp/PaddleOCR/.github/workflows/close_inactive_issues.yml b/ocr_webapp/PaddleOCR/.github/workflows/close_inactive_issues.yml new file mode 100644 index 0000000000000000000000000000000000000000..065bfa519b542548aefd9ed29c5ec9a9f2b5482b --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/workflows/close_inactive_issues.yml @@ -0,0 +1,23 @@ +name: Close inactive issues +on: + schedule: + - cron: "30 1 * * *" + +jobs: + close-issues: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v10 + with: + days-before-issue-stale: 90 + days-before-issue-close: 14 + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 90 days with no activity." + close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." + days-before-pr-stale: -1 + days-before-pr-close: -1 + repo-token: ${{ secrets.GITHUB_TOKEN }} + diff --git a/ocr_webapp/PaddleOCR/.github/workflows/codestyle.yml b/ocr_webapp/PaddleOCR/.github/workflows/codestyle.yml new file mode 100644 index 0000000000000000000000000000000000000000..42ec5061beffff0c024422d1238c0193e512238c --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/workflows/codestyle.yml @@ -0,0 +1,41 @@ +name: PaddleOCR Code Style Check + +# NOTE: Job name `check-code-style` is the required status check context +# configured in branch protection. Do not rename without updating settings. + +on: + pull_request: {} + push: + branches: ['main', 'release/*'] + +jobs: + check-code-style: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.ref }} + fetch-depth: 0 + - id: detect + uses: ./.github/actions/detect-docs-only + - uses: actions/setup-python@v6 + if: steps.detect.outputs.docs_only != 'true' + with: + python-version: '3.10' + - name: Cache Python dependencies + if: steps.detect.outputs.docs_only != 'true' + uses: actions/cache@v5 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Install Dependencies for Python + if: steps.detect.outputs.docs_only != 'true' + run: | + python -m pip install --upgrade pip + pip install "clang-format==13.0.0" + - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 + if: steps.detect.outputs.docs_only != 'true' + with: + extra_args: '--all-files' diff --git a/ocr_webapp/PaddleOCR/.github/workflows/docs_anchor_check.yml b/ocr_webapp/PaddleOCR/.github/workflows/docs_anchor_check.yml new file mode 100644 index 0000000000000000000000000000000000000000..5be996fa3d08d2c22059f48e49f664906737c8be --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/workflows/docs_anchor_check.yml @@ -0,0 +1,39 @@ +name: Docs Anchor Link Check + +on: + pull_request: + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'mkdocs-ci.yml' + - 'overrides/**' + +jobs: + check-anchor-links: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - uses: actions/setup-python@v6 + with: + python-version: '3.x' + + - uses: actions/cache@v5 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-mkdocs-${{ hashFiles('mkdocs.yml') }} + restore-keys: | + ${{ runner.os }}-pip-mkdocs- + + - name: Install dependencies + run: pip install mike mkdocs-material jieba mkdocs-git-revision-date-localized-plugin mkdocs-git-committers-plugin-2 mkdocs-static-i18n markdown-callouts + + - name: Resolve docs GitHub source refs + run: python tools/resolve_doc_github_refs.py --placeholder '{{PADDLEOCR_GITHUB_REF}}' --source-ref main + + - name: Check for broken anchor links + env: + ENABLE_GIT_PLUGINS: 'false' + run: mkdocs build -f mkdocs-ci.yml diff --git a/ocr_webapp/PaddleOCR/.github/workflows/link_check.yml b/ocr_webapp/PaddleOCR/.github/workflows/link_check.yml new file mode 100644 index 0000000000000000000000000000000000000000..41415604293e5a8d6ecc164fed6419013d79799d --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/workflows/link_check.yml @@ -0,0 +1,42 @@ +name: Link Checker + +on: + repository_dispatch: + workflow_dispatch: + # push: + # branches: + # - main + # schedule: + # - cron: "00 18 * * 6" + +jobs: + linkChecker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: '3.x' + + - name: Resolve docs GitHub source refs + run: python tools/resolve_doc_github_refs.py --placeholder '{{PADDLEOCR_GITHUB_REF}}' --source-ref main + + - name: Link Checker + id: lychee + uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2 + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + with: + args: --exclude 'docs/index/*.md' 'docs/update/*.md' --verbose --no-progress --max-redirects 8 'docs/**/*.md' + format: markdown + fail: false + output: lychee/results.md + + - name: Create Issue From File + if: steps.lychee.outputs.exit_code != 0 + uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6 + with: + title: Link Checker Report + content-filepath: ./lychee/results.md + labels: report, automated issue diff --git a/ocr_webapp/PaddleOCR/.github/workflows/python-publish.yml b/ocr_webapp/PaddleOCR/.github/workflows/python-publish.yml new file mode 100644 index 0000000000000000000000000000000000000000..a35a1836f7b6f9012adbbdeb54741636b8b4f195 --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/workflows/python-publish.yml @@ -0,0 +1,41 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + pip install setuptools + pip install wheel + - name: Build package + run: python setup.py bdist_wheel + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/ocr_webapp/PaddleOCR/.github/workflows/python_publish.yml b/ocr_webapp/PaddleOCR/.github/workflows/python_publish.yml new file mode 100644 index 0000000000000000000000000000000000000000..07d5331c3f56699ecd830b033fffc0cb52ac323c --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/workflows/python_publish.yml @@ -0,0 +1,39 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build==1.2.2 + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/ocr_webapp/PaddleOCR/.github/workflows/test_gpu.yml b/ocr_webapp/PaddleOCR/.github/workflows/test_gpu.yml new file mode 100644 index 0000000000000000000000000000000000000000..5f4d766c3da67bac253070c80b37ea02b3fd69ac --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/workflows/test_gpu.yml @@ -0,0 +1,117 @@ +name: PaddleOCR PR Tests GPU + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: +env: + PR_ID: ${{ github.event.pull_request.number }} + COMMIT_ID: ${{ github.event.pull_request.head.sha || github.sha }} + work_dir: /workspace/PaddleOCR + PADDLENLP_ROOT: /workspace/PaddleOCR + TASK: paddleocr-CI-${{ github.event.pull_request.number }} + BRANCH: ${{ github.event.pull_request.base.ref }} + AGILE_COMPILE_BRANCH: ${{ github.event.pull_request.base.ref }} + DIR_NAME: ${{ github.repository }} +permissions: + contents: read + +jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + docs_only: ${{ steps.detect.outputs.docs_only }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - id: detect + uses: ./.github/actions/detect-docs-only + + test-pr-gpu-impl: + runs-on: [self-hosted, GPU-2Card-OCR] + needs: detect-changes + if: needs.detect-changes.outputs.docs_only != 'true' + steps: + - name: run test + env: + py_version: "3.10" + paddle_whl: https://paddle-qa.bj.bcebos.com/paddle-pipeline/Develop-GpuSome-LinuxCentos-Gcc82-Cuda118-Cudnn86-Trt85-Py310-CINN-Compile/latest/paddlepaddle_gpu-0.0.0-cp310-cp310-linux_x86_64.whl + docker_image: ccr-2vdh3abv-pub.cnc.bj.baidubce.com/paddlepaddle/paddle:latest-dev-cuda11.8-cudnn8.6-trt8.5-gcc82 + run: | + work_dir=$RANDOM + mkdir $work_dir + cd $work_dir + git clone --depth=1 https://github.com/PaddlePaddle/PaddleOCR.git -b main + cd PaddleOCR + if [ -n "${PR_ID}" ]; then + git fetch origin pull/${PR_ID}/head:ci_build + git checkout ci_build + else + git fetch --depth=1 origin "${COMMIT_ID}" + git checkout "${COMMIT_ID}" + fi + docker run --gpus all --rm -i --name PaddleOCR_CI_$RANDOM \ + --shm-size=128g --net=host \ + -v $PWD:/workspace -w /workspace \ + -e "py_version=${py_version}" \ + -e "paddle_whl=${paddle_whl}" \ + ${docker_image} /bin/bash -c ' + ldconfig; + nvidia-smi + df -hl + + echo ${py_version} + rm -rf run_env + mkdir run_env + ln -s $(which python${py_version}) run_env/python + ln -s $(which python${py_version}) run_env/python3 + ln -s $(which pip${py_version}) run_env/pip + export PATH=$PWD/run_env:${PATH} + + git config --global --add safe.directory /workspace + + python -m pip install paddlepaddle-gpu==3.1.0 -i https://www.paddlepaddle.org.cn/packages/stable/cu118/ + python -c "import paddle; paddle.version.show()" + python -m pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple + python -m pip install pytest + if [ -f requirements.txt ]; then python -m pip install -r requirements.txt; fi + PADDLEX_SERIES=$(grep -oE "paddlex\[[^]]+\]>=[0-9]+\.[0-9]+" pyproject.toml | head -1 | grep -oE "[0-9]+\.[0-9]+") + if [ -z "$PADDLEX_SERIES" ]; then + echo "Failed to determine PaddleX version requirement from pyproject.toml" >&2 + exit 1 + fi + PADDLEX_BRANCH="release/${PADDLEX_SERIES}" + echo "Installing PaddleX from branch: ${PADDLEX_BRANCH}" + python -m pip install -e ".[all]" "paddlex@git+https://github.com/PaddlePaddle/PaddleX.git@${PADDLEX_BRANCH}" + python -c "import paddlex; print(f\"Installed paddlex version: {paddlex.__version__}\")" + python -m pytest --verbose tests/ + ' + + # Aggregator: produces a single check named `test-pr-gpu` so branch + # protection (which requires the literal context `test-pr-gpu`) is satisfied + # even when the heavy GPU job is skipped (e.g. docs-only changes). Without + # this, a SKIPPED conclusion on the GPU job leaves the required check + # unsatisfied and blocks merge. + test-pr-gpu: + runs-on: ubuntu-latest + needs: [detect-changes, test-pr-gpu-impl] + if: always() + steps: + - name: Verify required jobs + run: | + if [ "${{ needs.detect-changes.result }}" != "success" ]; then + echo "detect-changes did not succeed: ${{ needs.detect-changes.result }}" + exit 1 + fi + if [ "${{ needs.detect-changes.outputs.docs_only }}" = "true" ]; then + echo "Docs-only change; treating GPU tests as not required." + exit 0 + fi + if [ "${{ needs.test-pr-gpu-impl.result }}" != "success" ]; then + echo "test-pr-gpu-impl concluded with: ${{ needs.test-pr-gpu-impl.result }}" + exit 1 + fi + echo "GPU tests passed." diff --git a/ocr_webapp/PaddleOCR/.github/workflows/tests.yml b/ocr_webapp/PaddleOCR/.github/workflows/tests.yml new file mode 100644 index 0000000000000000000000000000000000000000..98e26b2cab347476afabc76a0344883d4c33be34 --- /dev/null +++ b/ocr_webapp/PaddleOCR/.github/workflows/tests.yml @@ -0,0 +1,108 @@ +name: PaddleOCR PR Tests + +on: + push: + branches: ["main", "release/*"] + pull_request: + branches: ["main", "release/*"] + +permissions: + contents: read + +jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + docs_only: ${{ steps.detect.outputs.docs_only }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - id: detect + uses: ./.github/actions/detect-docs-only + + test-pr-python: + runs-on: ubuntu-latest + needs: detect-changes + if: needs.detect-changes.outputs.docs_only != 'true' + strategy: + matrix: + python-version: ["3.8", "3.9", "3.13"] + + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache dependencies + uses: actions/cache@v5 + with: + path: | + ~/.cache/pip + ~/.local/lib/python${{ matrix.python-version }}/site-packages + ~/.paddleocr/ + key: ${{ runner.os }}-dependencies-${{ matrix.python-version }}-${{ hashFiles('**/requirements.txt', 'pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-dependencies- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + # On py3.8 several paddlex transitive deps require py3.9+, so only + # paddleocr[doc2md] is installable. See installation.md for the + # supported extras-by-Python-version matrix. + if [[ "${{ matrix.python-version }}" == "3.8" ]]; then + python -m pip install paddlepaddle==3.0.0 -i https://www.paddlepaddle.org.cn/packages/stable/cpu/ + PADDLEOCR_EXTRAS="[doc2md]" + else + python -m pip install paddlepaddle==3.1.0 -i https://www.paddlepaddle.org.cn/packages/stable/cpu/ + PADDLEOCR_EXTRAS="[all]" + fi + PADDLEX_SERIES=$(grep -oE 'paddlex\[[^]]+\]>=[0-9]+\.[0-9]+' pyproject.toml | head -1 | grep -oE '[0-9]+\.[0-9]+') + if [ -z "$PADDLEX_SERIES" ]; then + echo "Failed to determine PaddleX version requirement from pyproject.toml" >&2 + exit 1 + fi + + python -m pip install "paddlex>=3.7.0,<3.8.0" + PADDLEX_BRANCH="release/${PADDLEX_SERIES}" + echo "Installing PaddleX from branch: ${PADDLEX_BRANCH}" + python -m pip install -e ".${PADDLEOCR_EXTRAS}" "paddlex@git+https://github.com/PaddlePaddle/PaddleX.git@${PADDLEX_BRANCH}" + python -c "import paddlex; print(f'Installed paddlex version: {paddlex.__version__}')" + - name: Test with pytest + run: | + # Skip py38_incompatible tests on py3.8. + if [[ "${{ matrix.python-version }}" == "3.8" ]]; then + pytest --verbose tests/ -m "not resource_intensive and not py38_incompatible" + else + pytest --verbose tests/ + fi + + # Aggregator: produces a single check named `test-pr` so branch protection + # (which requires the literal context `test-pr`) is satisfied. Without this, + # the matrix above only emits `test-pr (3.x)` checks and the required + # `test-pr` context never reports. + test-pr: + runs-on: ubuntu-latest + needs: [detect-changes, test-pr-python] + if: always() + steps: + - name: Verify required jobs + run: | + if [ "${{ needs.detect-changes.result }}" != "success" ]; then + echo "detect-changes did not succeed: ${{ needs.detect-changes.result }}" + exit 1 + fi + if [ "${{ needs.detect-changes.outputs.docs_only }}" = "true" ]; then + echo "Docs-only change; skipping python tests." + exit 0 + fi + if [ "${{ needs.test-pr-python.result }}" != "success" ]; then + echo "test-pr-python concluded with: ${{ needs.test-pr-python.result }}" + exit 1 + fi + echo "All python matrix variants passed." diff --git a/ocr_webapp/PaddleOCR/.gitignore b/ocr_webapp/PaddleOCR/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3a05fb74687f2b12790f2f73fc96cf8a6abb2bd3 --- /dev/null +++ b/ocr_webapp/PaddleOCR/.gitignore @@ -0,0 +1,34 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +.ipynb_checkpoints/ +*.py[cod] +*$py.class + +# C extensions +*.so + +inference/ +inference_results/ +output/ +train_data/ +log/ +*.DS_Store +*.vs +*.user +*~ +*.vscode +*.idea + +*.log +.clang-format +.clang_format.hook + +build/ +dist/ +paddleocr.egg-info/ +/deploy/android_demo/app/OpenCV/ +/deploy/android_demo/app/PaddleLite/ +/deploy/android_demo/app/.cxx/ +/deploy/android_demo/app/cache/ +test_tipc/web/models/ +test_tipc/web/node_modules/ \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/.lycheeignore b/ocr_webapp/PaddleOCR/.lycheeignore new file mode 100644 index 0000000000000000000000000000000000000000..444fe9de0a57988d1269a02a5d04ee67309bdaa2 --- /dev/null +++ b/ocr_webapp/PaddleOCR/.lycheeignore @@ -0,0 +1,5 @@ +zhuanlan.zhihu.com/* +https://demo.doctrp.top/ +http://127.0.0.1:8001/ +http://localhost:9003 +https://rrc.cvc.uab.es/ diff --git a/ocr_webapp/PaddleOCR/.pre-commit-config.yaml b/ocr_webapp/PaddleOCR/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..94c450b3e4e4081c4e50a01ae99770ce6eb1237a --- /dev/null +++ b/ocr_webapp/PaddleOCR/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: a11d9314b22d8f8c7556443875b731ef05965464 + hooks: + - id: check-merge-conflict + - id: check-symlinks + - id: detect-private-key + files: (?!.*paddle)^.*$ + - id: end-of-file-fixer + files: \.md$ + - id: trailing-whitespace + files: \.md$ +- repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.0.1 + hooks: + - id: forbid-crlf + files: \.md$ + - id: remove-crlf + files: \.md$ + - id: forbid-tabs + files: \.md$ + - id: remove-tabs + files: \.md$ +- repo: local + hooks: + - id: clang-format + name: clang-format + description: Format files with ClangFormat + entry: bash .clang_format.hook -i + language: system + files: \.(c|cc|cxx|cpp|cu|h|hpp|hxx|cuh|proto)$ +# For Python files +- repo: https://github.com/psf/black.git + rev: 23.3.0 + hooks: + - id: black + files: (.*\.(py|pyi|bzl)|BUILD|.*\.BUILD|WORKSPACE)$ +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.0 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix, --no-cache] diff --git a/ocr_webapp/PaddleOCR/.style.yapf b/ocr_webapp/PaddleOCR/.style.yapf new file mode 100644 index 0000000000000000000000000000000000000000..4741fb4f3bbc6681088cf9e960321e7b857a93a8 --- /dev/null +++ b/ocr_webapp/PaddleOCR/.style.yapf @@ -0,0 +1,3 @@ +[style] +based_on_style = pep8 +column_limit = 80 diff --git a/ocr_webapp/PaddleOCR/CNAME b/ocr_webapp/PaddleOCR/CNAME new file mode 100644 index 0000000000000000000000000000000000000000..c5334e1fad0323498dc8cc3f848be9d608cf22a1 --- /dev/null +++ b/ocr_webapp/PaddleOCR/CNAME @@ -0,0 +1 @@ +www.paddleocr.ai diff --git a/ocr_webapp/PaddleOCR/__init__.py b/ocr_webapp/PaddleOCR/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a7c32e9629d2e5ff04dc2ca45c6317caac8fa631 --- /dev/null +++ b/ocr_webapp/PaddleOCR/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .paddleocr import * + +__version__ = paddleocr.VERSION +__all__ = [ + 'PaddleOCR', 'PPStructure', 'draw_ocr', 'draw_structure_result', + 'save_structure_res', 'download_with_progressbar', 'sorted_layout_boxes', + 'convert_info_docx', 'to_excel' +] diff --git a/ocr_webapp/PaddleOCR/api_sdk/README.md b/ocr_webapp/PaddleOCR/api_sdk/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d86e28e1be3a49935b7880d5c7f41dfed5e4a91e --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/README.md @@ -0,0 +1,50 @@ +# PaddleOCR official API SDKs + +English | [简体中文](README_cn.md) + +This directory contains source-adjacent maintainer files for the PaddleOCR +official API SDKs. The SDKs call hosted PaddleOCR official API services; they do +not run local PaddleOCR inference or load local models. + +The official user documentation: + +- [Overview](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/overview.md) +- [Python SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/python.md) +- [TypeScript SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.md) +- [Go SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.md) +- [CLI](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/cli.md) + +## Maintainer Files + +| File | Purpose | +| --- | --- | +| [`typescript/README.md`](typescript/README.md) | Package-level README for the TypeScript SDK. | +| [`go/README.md`](go/README.md) | Package-level README for the Go SDK. | + +The Python SDK is part of the main `paddleocr` package. + +## Package Locations + +| Language | Source location | User docs | +| --- | --- | --- | +| Python | [`../paddleocr`](../paddleocr) | [Python SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/python.md) | +| TypeScript | [`typescript`](typescript) | [TypeScript SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.md) | +| Go | [`go`](go) | [Go SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.md) | + +## Validation + +Run these from the PaddleOCR repo root directory: + +```bash +# Python +python -m pytest tests/api_client/ + +# TypeScript +cd api_sdk/typescript +npm run lint +npm test + +# Go +cd ../go +go test ./... +``` diff --git a/ocr_webapp/PaddleOCR/api_sdk/README_cn.md b/ocr_webapp/PaddleOCR/api_sdk/README_cn.md new file mode 100644 index 0000000000000000000000000000000000000000..b24f16d46dd95cb09b957ea47f8c67b2240b161b --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/README_cn.md @@ -0,0 +1,48 @@ +# PaddleOCR 官方 API SDK + +[English](README.md) | 简体中文 + +本目录包含 PaddleOCR 官方 API SDK 的源码相邻维护文档。SDK 调用 PaddleOCR 官方 API 托管服务;它们不在本地执行 PaddleOCR 推理,也不加载本地模型。 + +正式用户文档: + +- [总览](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/overview.md) +- [Python SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/python.md) +- [TypeScript SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.md) +- [Go SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.md) +- [CLI](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/cli.md) + +## 维护者文件 + +| 文件 | 作用 | +| --- | --- | +| [`typescript/README_cn.md`](typescript/README_cn.md) | TypeScript SDK 的包级 README。 | +| [`go/README_cn.md`](go/README_cn.md) | Go SDK 的包级 README。 | + +Python SDK 是主 `paddleocr` 包的一部分。 + +## 包位置 + +| 语言 | 源码位置 | 用户文档 | +| --- | --- | --- | +| Python | [`../paddleocr`](../paddleocr) | [Python SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/python.md) | +| TypeScript | [`typescript`](typescript) | [TypeScript SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.md) | +| Go | [`go`](go) | [Go SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.md) | + +## 验证 + +在 PaddleOCR 仓库根目录执行: + +```bash +# Python +python -m pytest tests/api_client/ + +# TypeScript +cd api_sdk/typescript +npm run lint +npm test + +# Go +cd ../go +go test ./... +``` diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/README.md b/ocr_webapp/PaddleOCR/api_sdk/go/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7c69bb0089298ab42b584936ac61d9d4475626b2 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/README.md @@ -0,0 +1,72 @@ +# PaddleOCR Go SDK + +English | [简体中文](README_cn.md) + +Go client for the PaddleOCR official API. It submits OCR and document parsing +jobs to hosted PaddleOCR services; it does not run local PaddleOCR inference or +load local models. + +Official user docs: + +- [Go SDK](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.md) +- [Go SDK (English)](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.en.md) + +## Install + +```bash +go get github.com/PaddlePaddle/PaddleOCR/api_sdk/go +``` + +Versioned releases use submodule tags such as `api_sdk/go/v0.1.0`. + +## Minimal Usage + +Set `PADDLEOCR_ACCESS_TOKEN` or pass `WithToken` when constructing the client: + +```bash +export PADDLEOCR_ACCESS_TOKEN="your-access-token" +``` + +```go +client, err := paddleocr.NewClient() +if err != nil { + return err +} + +result, err := client.OCR(ctx, &paddleocr.OCRRequest{ + Model: paddleocr.PPOCRv5, + FileURL: "https://example.com/invoice.pdf", +}) +if err != nil { + return err +} +fmt.Println(result.JobID, len(result.Pages)) +``` + +Set `Model: paddleocr.PPOCRv6` (or `"PP-OCRv6"`) to use the PP-OCRv6 hosted OCR model. +Set `Model: paddleocr.PPOCRv5Latin` (or `"PP-OCRv5-latin"`) to use the PP-OCRv5 Latin-script hosted OCR model. + +Document parsing defaults to PaddleOCR-VL-1.6: + +```go +doc, err := client.ParseDocument(ctx, &paddleocr.DocParsingRequest{ + FilePath: "./report.pdf", + Options: &paddleocr.PaddleOCRVLOptions{ + UseChartRecognition: paddleocr.Bool(true), + }, +}) +if err != nil { + return err +} +fmt.Println(doc.JobID, len(doc.Pages)) +``` + +## Build And Test + +```bash +go test ./... +go vet ./... +go test -race ./... +``` + +`go test -race ./...` is recommended before public release. diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/README_cn.md b/ocr_webapp/PaddleOCR/api_sdk/go/README_cn.md new file mode 100644 index 0000000000000000000000000000000000000000..0b19689e15c76238b29ad8ee475be6c03db00cd1 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/README_cn.md @@ -0,0 +1,70 @@ +# PaddleOCR Go SDK + +[English](README.md) | 简体中文 + +面向 PaddleOCR 官方 API 的 Go 客户端。它会把 OCR 和文档解析任务提交到 PaddleOCR 官方托管服务;不会运行本地 PaddleOCR 推理,也不会加载本地模型。 + +正式用户文档: + +- [Go SDK](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.md) +- [Go SDK(英文)](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.en.md) + +## 安装 + +```bash +go get github.com/PaddlePaddle/PaddleOCR/api_sdk/go +``` + +版本化发布使用 `api_sdk/go/v0.1.0` 这类子目录 module tag。 + +## 最小示例 + +设置 `PADDLEOCR_ACCESS_TOKEN`,或在构造客户端时传入 `WithToken`: + +```bash +export PADDLEOCR_ACCESS_TOKEN="your-access-token" +``` + +```go +client, err := paddleocr.NewClient() +if err != nil { + return err +} + +result, err := client.OCR(ctx, &paddleocr.OCRRequest{ + Model: paddleocr.PPOCRv5, + FileURL: "https://example.com/invoice.pdf", +}) +if err != nil { + return err +} +fmt.Println(result.JobID, len(result.Pages)) +``` + +将 `Model` 设为 `paddleocr.PPOCRv6`(或 `"PP-OCRv6"`)可使用 PP-OCRv6 云端 OCR 模型。 +将 `Model` 设为 `paddleocr.PPOCRv5Latin`(或 `"PP-OCRv5-latin"`)可使用 PP-OCRv5 拉丁语系云端 OCR 模型。 + +文档解析默认使用 PaddleOCR-VL-1.6: + +```go +doc, err := client.ParseDocument(ctx, &paddleocr.DocParsingRequest{ + FilePath: "./report.pdf", + Options: &paddleocr.PaddleOCRVLOptions{ + UseChartRecognition: paddleocr.Bool(true), + }, +}) +if err != nil { + return err +} +fmt.Println(doc.JobID, len(doc.Pages)) +``` + +## 构建与测试 + +```bash +go test ./... +go vet ./... +go test -race ./... +``` + +公开发布前建议运行 `go test -race ./...`。 diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/client.go b/ocr_webapp/PaddleOCR/api_sdk/go/client.go new file mode 100644 index 0000000000000000000000000000000000000000..93d02f122f9180b5630b4c4a55f818762e4ceb1d --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/client.go @@ -0,0 +1,66 @@ +// 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. + +package paddleocr + +import ( + "net/http" + "os" + "strings" + "time" +) + +type Client struct { + token string + baseURL string + jobsURL string + requestTimeout time.Duration + pollTimeout time.Duration + clientPlatform string + httpClient *http.Client +} + +func NewClient(opts ...ClientOption) (*Client, error) { + c := &Client{ + requestTimeout: 5 * time.Minute, + pollTimeout: 10 * time.Minute, + } + for _, opt := range opts { + opt(c) + } + if c.token == "" { + c.token = os.Getenv("PADDLEOCR_ACCESS_TOKEN") + } + if c.token == "" { + return nil, &AuthError{PaddleOCRAPIError{Message: "Token is required. Set PADDLEOCR_ACCESS_TOKEN or use WithToken()."}} + } + if c.baseURL == "" { + c.baseURL = os.Getenv("PADDLEOCR_BASE_URL") + } + if c.baseURL == "" { + c.baseURL = DefaultBaseURL + } + c.baseURL = strings.TrimRight(c.baseURL, "/") + c.jobsURL = c.baseURL + apiPath + if c.httpClient == nil { + c.httpClient = &http.Client{Timeout: c.requestTimeout} + } + return c, nil +} + +func (c *Client) setClientPlatformHeader(req *http.Request) { + if c.clientPlatform != "" { + req.Header.Set("Client-Platform", c.clientPlatform) + } +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/client_test.go b/ocr_webapp/PaddleOCR/api_sdk/go/client_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b322848eae5cb3e15943b2f4dc7da6221f719752 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/client_test.go @@ -0,0 +1,235 @@ +package paddleocr + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestClientPlatformHeader(t *testing.T) { + var got string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got = r.Header.Get("Client-Platform") + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "code": 0, + "data": map[string]string{"jobId": "job-1"}, + }) + })) + defer server.Close() + + client, err := NewClient( + WithToken("token"), + WithBaseURL(server.URL), + WithClientPlatform("my-app"), + ) + if err != nil { + t.Fatalf("NewClient() error = %v", err) + } + + job, err := client.SubmitOCR(context.Background(), &OCRRequest{ + FileURL: "https://example.test/input.pdf", + }) + if err != nil { + t.Fatalf("SubmitOCR() error = %v", err) + } + if job.JobID != "job-1" { + t.Fatalf("JobID = %q, want job-1", job.JobID) + } + if got != "my-app" { + t.Fatalf("Client-Platform = %q, want my-app", got) + } +} + +func TestDocumentParsingOptionsIncludeCurrentAndFutureServiceParameters(t *testing.T) { + trueValue := true + falseValue := false + var got map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body struct { + OptionalPayload map[string]interface{} `json:"optionalPayload"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("Decode request body error = %v", err) + } + got = body.OptionalPayload + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "code": 0, + "data": map[string]string{"jobId": "job-doc"}, + }) + })) + defer server.Close() + + client, err := NewClient( + WithToken("token"), + WithBaseURL(server.URL), + ) + if err != nil { + t.Fatalf("NewClient() error = %v", err) + } + + _, err = client.SubmitDocumentParsing(context.Background(), &DocParsingRequest{ + Model: PaddleOCRVL16, + FileURL: "https://example.test/doc.pdf", + Options: &PaddleOCRVLOptions{ + UseOcrForImageBlock: &trueValue, + FormatBlockContent: &trueValue, + MarkdownIgnoreLabels: []string{"image"}, + VlmExtraArgs: map[string]interface{}{"temperature": 0.1}, + ReturnMarkdownImages: &falseValue, + OutputFormats: []string{"docx"}, + ExtraOptions: map[string]interface{}{"futureOption": "enabled"}, + }, + }) + if err != nil { + t.Fatalf("SubmitDocumentParsing() error = %v", err) + } + + if got["useOcrForImageBlock"] != true || got["formatBlockContent"] != true { + t.Fatalf("got boolean options %#v", got) + } + if got["returnMarkdownImages"] != false || got["futureOption"] != "enabled" { + t.Fatalf("got passthrough options %#v", got) + } +} + +func TestResultParsersPreserveRawFieldsAndDataInfo(t *testing.T) { + ocrLine := map[string]interface{}{ + "result": map[string]interface{}{ + "dataInfo": map[string]interface{}{"numPages": float64(1)}, + "ocrResults": []interface{}{ + map[string]interface{}{ + "prunedResult": map[string]interface{}{"text": "hello"}, + "ocrImage": "ocr.png", + "docPreprocessingImage": "pre.png", + "inputImage": "input.png", + }, + }, + }, + } + ocrResult, err := parseOCRResult("job-ocr", []map[string]interface{}{ocrLine}) + if err != nil { + t.Fatalf("parseOCRResult() error = %v", err) + } + if ocrResult.DataInfo["numPages"] != float64(1) { + t.Fatalf("OCR metadata not preserved: %#v", ocrResult) + } + if ocrResult.Pages[0].DocPreprocessingImageURL != "pre.png" || ocrResult.Pages[0].InputImageURL != "input.png" { + t.Fatalf("OCR page image URLs not preserved: %#v", ocrResult.Pages[0]) + } + if ocrResult.Pages[0].Raw["ocrImage"] != "ocr.png" { + t.Fatalf("OCR raw page not preserved: %#v", ocrResult.Pages[0].Raw) + } + + docPage := map[string]interface{}{ + "prunedResult": map[string]interface{}{"blocks": []interface{}{map[string]interface{}{"label": "text"}}}, + "markdown": map[string]interface{}{"text": "hello", "images": map[string]interface{}{"figure.png": "figure-url"}, "isStart": true}, + "outputImages": map[string]interface{}{"page.png": "page-url"}, + "inputImage": "input.png", + "exports": map[string]interface{}{"docx": "docx-url"}, + } + docLine := map[string]interface{}{ + "result": map[string]interface{}{ + "dataInfo": map[string]interface{}{"numPages": float64(1)}, + "layoutParsingResults": []interface{}{docPage}, + }, + } + docResult, err := parseDocParsingResult("job-doc", []map[string]interface{}{docLine}) + if err != nil { + t.Fatalf("parseDocParsingResult() error = %v", err) + } + if docResult.DataInfo["numPages"] != float64(1) { + t.Fatalf("document metadata not preserved: %#v", docResult) + } + if docResult.Pages[0].PrunedResult == nil || docResult.Pages[0].Raw["inputImage"] != "input.png" { + t.Fatalf("document page raw fields not preserved: %#v", docResult.Pages[0]) + } + if docResult.Pages[0].Exports["docx"] != "docx-url" || docResult.Pages[0].Markdown["isStart"] != true { + t.Fatalf("document structured fields not preserved: %#v", docResult.Pages[0]) + } +} + +func TestSubmitOCRAcceptsOfficialModelNameString(t *testing.T) { + var got string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body struct { + Model string `json:"model"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("Decode request body error = %v", err) + } + got = body.Model + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "code": 0, + "data": map[string]string{"jobId": "job-1"}, + }) + })) + defer server.Close() + + client, err := NewClient( + WithToken("token"), + WithBaseURL(server.URL), + ) + if err != nil { + t.Fatalf("NewClient() error = %v", err) + } + + job, err := client.SubmitOCR(context.Background(), &OCRRequest{ + Model: "PP-OCRv5", + FileURL: "https://example.test/input.pdf", + }) + if err != nil { + t.Fatalf("SubmitOCR() error = %v", err) + } + if job.Model != "PP-OCRv5" { + t.Fatalf("Job model = %q, want PP-OCRv5", job.Model) + } + if got != "PP-OCRv5" { + t.Fatalf("Request model = %q, want PP-OCRv5", got) + } +} + +func TestSubmitOCRAcceptsPPOCRv5LatinModelNameString(t *testing.T) { + var got string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body struct { + Model string `json:"model"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("Decode request body error = %v", err) + } + got = body.Model + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "code": 0, + "data": map[string]string{"jobId": "job-latin"}, + }) + })) + defer server.Close() + + client, err := NewClient( + WithToken("token"), + WithBaseURL(server.URL), + ) + if err != nil { + t.Fatalf("NewClient() error = %v", err) + } + + job, err := client.SubmitOCR(context.Background(), &OCRRequest{ + Model: PPOCRv5Latin, + FileURL: "https://example.test/latin.pdf", + }) + if err != nil { + t.Fatalf("SubmitOCR() error = %v", err) + } + if job.Model != PPOCRv5Latin { + t.Fatalf("Job model = %q, want %s", job.Model, PPOCRv5Latin) + } + if got != PPOCRv5Latin { + t.Fatalf("Request model = %q, want %s", got, PPOCRv5Latin) + } +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/doc.go b/ocr_webapp/PaddleOCR/api_sdk/go/doc.go new file mode 100644 index 0000000000000000000000000000000000000000..c5200abb60da70f32d9eeb48910a7903d7ade64a --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/doc.go @@ -0,0 +1,30 @@ +// 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. + +// Package paddleocr provides a Go client for the PaddleOCR official API. +// +// Create a client with NewClient and authenticate with WithToken or the +// PADDLEOCR_ACCESS_TOKEN environment variable. Use OCR for supported OCR models +// and ParseDocument for document parsing models. +// SubmitOCR and SubmitDocumentParsing return an Operation for non-blocking +// status checks with Poll or typed waits with WaitOCR and WaitDocumentParsing. +// SaveResource downloads one result resource URL. SaveOCRResultResources and +// SaveDocumentParsingResultResources save resources from typed result objects +// into an existing directory. +// +// Request timeout and polling timeout are configured separately with +// WithRequestTimeout and WithPollTimeout. Errors are exposed as typed values, +// such as AuthError, InvalidRequestError, APIError, ResponseFormatError, and +// ResultParseError, and are suitable for errors.As. +package paddleocr diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/errors.go b/ocr_webapp/PaddleOCR/api_sdk/go/errors.go new file mode 100644 index 0000000000000000000000000000000000000000..cbbd0a58e4f98ebfca473ac2da42e59595ce300c --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/errors.go @@ -0,0 +1,103 @@ +// 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. + +package paddleocr + +import "fmt" + +type PaddleOCRAPIError struct { + Message string + Cause error +} + +func (e *PaddleOCRAPIError) Error() string { + if e.Message == "" && e.Cause != nil { + return e.Cause.Error() + } + return e.Message +} + +func (e *PaddleOCRAPIError) Unwrap() error { + return e.Cause +} + +type AuthError struct { + PaddleOCRAPIError +} + +type InvalidRequestError struct { + PaddleOCRAPIError +} + +type APIError struct { + StatusCode int + PaddleOCRAPIError +} + +func (e *APIError) Error() string { + return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message) +} + +type RateLimitError struct { + APIError +} + +type ServiceUnavailableError struct { + APIError +} + +type JobFailedError struct { + JobID string + ErrorMsg string + PaddleOCRAPIError +} + +func (e *JobFailedError) Error() string { + return fmt.Sprintf("Job %s failed: %s", e.JobID, e.ErrorMsg) +} + +type RequestTimeoutError struct { + PaddleOCRAPIError +} + +type PollTimeoutError struct { + JobID string + Elapsed float64 + PaddleOCRAPIError +} + +func (e *PollTimeoutError) Error() string { + return fmt.Sprintf("Timed out after %.1fs waiting for job %s", e.Elapsed, e.JobID) +} + +type NetworkError struct { + PaddleOCRAPIError +} + +type FileNotFoundError struct { + Path string + PaddleOCRAPIError +} + +func (e *FileNotFoundError) Error() string { + return fmt.Sprintf("File not found: %s", e.Path) +} + +type ResponseFormatError struct { + PaddleOCRAPIError +} + +type ResultParseError struct { + PaddleOCRAPIError +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/examples/doc_parsing_file/main.go b/ocr_webapp/PaddleOCR/api_sdk/go/examples/doc_parsing_file/main.go new file mode 100644 index 0000000000000000000000000000000000000000..22bb093717836707c4f0e43bfc2463e8270c073a --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/examples/doc_parsing_file/main.go @@ -0,0 +1,61 @@ +// 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. + +package main + +import ( + "context" + "fmt" + "log" + + paddleocr "github.com/PaddlePaddle/PaddleOCR/api_sdk/go" +) + +func main() { + client, err := paddleocr.NewClient() + if err != nil { + log.Fatal(err) + } + ctx := context.Background() + + // Convenience method (blocks until done) + result, err := client.ParseDocument(ctx, &paddleocr.DocParsingRequest{ + Model: paddleocr.PPStructureV3, + FilePath: "./sample.pdf", + Options: &paddleocr.PPStructureV3Options{UseChartRecognition: paddleocr.Bool(true)}, + }) + if err != nil { + log.Fatal(err) + } + for i, page := range result.Pages { + fmt.Printf("Page %d:\n%s\n", i+1, page.MarkdownText) + } + + // Manual control with typed job metadata and typed wait methods. + ocrJob, _ := client.SubmitOCR(ctx, &paddleocr.OCRRequest{FileURL: "https://example.com/f1.pdf"}) + docJob, _ := client.SubmitDocumentParsing(ctx, &paddleocr.DocParsingRequest{ + Model: paddleocr.PPStructureV3, FilePath: "./sample.pdf", + }) + + ocrResult, err := client.WaitOCRResult(ctx, ocrJob.JobID) + if err != nil { + log.Printf("OCR job error: %v", err) + } + docResult, err := client.WaitDocumentParsingResult(ctx, docJob.JobID) + if err != nil { + log.Printf("document parsing job error: %v", err) + } + fmt.Printf("OCR done: %v\n", ocrResult) + fmt.Printf("Document parsing done: %v\n", docResult) +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/examples/ocr_url/main.go b/ocr_webapp/PaddleOCR/api_sdk/go/examples/ocr_url/main.go new file mode 100644 index 0000000000000000000000000000000000000000..f19c9ef2d210fc96ee868c2065dac2e946270bdf --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/examples/ocr_url/main.go @@ -0,0 +1,43 @@ +// 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. + +package main + +import ( + "context" + "fmt" + "log" + + paddleocr "github.com/PaddlePaddle/PaddleOCR/api_sdk/go" +) + +func main() { + client, err := paddleocr.NewClient() + if err != nil { + log.Fatal(err) + } + ctx := context.Background() + + result, err := client.OCR(ctx, &paddleocr.OCRRequest{ + FileURL: "https://example.com/invoice.pdf", + }) + if err != nil { + log.Fatal(err) + } + + for i, page := range result.Pages { + fmt.Printf("Page %d: %v\n", i+1, page.PrunedResult) + fmt.Printf(" Image URL: %s\n", page.OCRImageURL) + } +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/go.mod b/ocr_webapp/PaddleOCR/api_sdk/go/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..bfeb193737669c8466b6a53dc806010251ff116a --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/go.mod @@ -0,0 +1,3 @@ +module github.com/PaddlePaddle/PaddleOCR/api_sdk/go + +go 1.21 diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/models.go b/ocr_webapp/PaddleOCR/api_sdk/go/models.go new file mode 100644 index 0000000000000000000000000000000000000000..6bad7c4d86a6bffe1e228a5aa713e81cc9a4cab4 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/models.go @@ -0,0 +1,159 @@ +// 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. + +package paddleocr + +const ( + PPOCRv5 = "PP-OCRv5" + PPOCRv5Latin = "PP-OCRv5-latin" + PPOCRv6 = "PP-OCRv6" + PPStructureV3 = "PP-StructureV3" + PaddleOCRVL = "PaddleOCR-VL" + PaddleOCRVL15 = "PaddleOCR-VL-1.5" + PaddleOCRVL16 = "PaddleOCR-VL-1.6" +) + +// IsOCRModel reports whether model is supported by OCR APIs. +func IsOCRModel(model string) bool { + return model == PPOCRv5 || model == PPOCRv5Latin || model == PPOCRv6 +} + +// IsDocumentParsingModel reports whether model is supported by document parsing APIs. +func IsDocumentParsingModel(model string) bool { + switch model { + case PPStructureV3, PaddleOCRVL, PaddleOCRVL15, PaddleOCRVL16: + return true + default: + return false + } +} + +// IsVLModel reports whether model is a PaddleOCR-VL family model. +func IsVLModel(model string) bool { + switch model { + case PaddleOCRVL, PaddleOCRVL15, PaddleOCRVL16: + return true + default: + return false + } +} + +type OCROptions struct { + UseDocOrientationClassify *bool `json:"useDocOrientationClassify,omitempty"` + UseDocUnwarping *bool `json:"useDocUnwarping,omitempty"` + UseTextlineOrientation *bool `json:"useTextlineOrientation,omitempty"` + TextDetLimitSideLen *int `json:"textDetLimitSideLen,omitempty"` + TextDetLimitType *string `json:"textDetLimitType,omitempty"` + TextDetThresh *float64 `json:"textDetThresh,omitempty"` + TextDetBoxThresh *float64 `json:"textDetBoxThresh,omitempty"` + TextDetUnclipRatio *float64 `json:"textDetUnclipRatio,omitempty"` + TextRecScoreThresh *float64 `json:"textRecScoreThresh,omitempty"` + Visualize *bool `json:"visualize,omitempty"` + ExtraOptions map[string]interface{} `json:"-"` +} + +type PPStructureV3Options struct { + UseDocOrientationClassify *bool `json:"useDocOrientationClassify,omitempty"` + UseDocUnwarping *bool `json:"useDocUnwarping,omitempty"` + UseTextlineOrientation *bool `json:"useTextlineOrientation,omitempty"` + UseSealRecognition *bool `json:"useSealRecognition,omitempty"` + UseTableRecognition *bool `json:"useTableRecognition,omitempty"` + UseFormulaRecognition *bool `json:"useFormulaRecognition,omitempty"` + UseChartRecognition *bool `json:"useChartRecognition,omitempty"` + UseRegionDetection *bool `json:"useRegionDetection,omitempty"` + LayoutThreshold interface{} `json:"layoutThreshold,omitempty"` + LayoutNms *bool `json:"layoutNms,omitempty"` + LayoutUnclipRatio interface{} `json:"layoutUnclipRatio,omitempty"` + LayoutMergeBboxesMode interface{} `json:"layoutMergeBboxesMode,omitempty"` + FormatBlockContent *bool `json:"formatBlockContent,omitempty"` + TextDetLimitSideLen *int `json:"textDetLimitSideLen,omitempty"` + TextDetLimitType *string `json:"textDetLimitType,omitempty"` + TextDetThresh *float64 `json:"textDetThresh,omitempty"` + TextDetBoxThresh *float64 `json:"textDetBoxThresh,omitempty"` + TextDetUnclipRatio *float64 `json:"textDetUnclipRatio,omitempty"` + TextRecScoreThresh *float64 `json:"textRecScoreThresh,omitempty"` + UseWiredTableCellsTransToHtml *bool `json:"useWiredTableCellsTransToHtml,omitempty"` + UseWirelessTableCellsTransToHtml *bool `json:"useWirelessTableCellsTransToHtml,omitempty"` + UseTableOrientationClassify *bool `json:"useTableOrientationClassify,omitempty"` + UseOcrResultsWithTableCells *bool `json:"useOcrResultsWithTableCells,omitempty"` + UseE2eWiredTableRecModel *bool `json:"useE2eWiredTableRecModel,omitempty"` + UseE2eWirelessTableRecModel *bool `json:"useE2eWirelessTableRecModel,omitempty"` + MarkdownIgnoreLabels []string `json:"markdownIgnoreLabels,omitempty"` + PrettifyMarkdown *bool `json:"prettifyMarkdown,omitempty"` + ShowFormulaNumber *bool `json:"showFormulaNumber,omitempty"` + ReturnMarkdownImages *bool `json:"returnMarkdownImages,omitempty"` + OutputFormats []string `json:"outputFormats,omitempty"` + Visualize *bool `json:"visualize,omitempty"` + ExtraOptions map[string]interface{} `json:"-"` +} + +type PaddleOCRVLOptions struct { + UseDocOrientationClassify *bool `json:"useDocOrientationClassify,omitempty"` + UseDocUnwarping *bool `json:"useDocUnwarping,omitempty"` + UseLayoutDetection *bool `json:"useLayoutDetection,omitempty"` + UseChartRecognition *bool `json:"useChartRecognition,omitempty"` + UseSealRecognition *bool `json:"useSealRecognition,omitempty"` + UseOcrForImageBlock *bool `json:"useOcrForImageBlock,omitempty"` + LayoutThreshold interface{} `json:"layoutThreshold,omitempty"` + LayoutNms *bool `json:"layoutNms,omitempty"` + LayoutUnclipRatio interface{} `json:"layoutUnclipRatio,omitempty"` + LayoutMergeBboxesMode interface{} `json:"layoutMergeBboxesMode,omitempty"` + LayoutShapeMode *string `json:"layoutShapeMode,omitempty"` + PromptLabel *string `json:"promptLabel,omitempty"` + FormatBlockContent *bool `json:"formatBlockContent,omitempty"` + RepetitionPenalty *float64 `json:"repetitionPenalty,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopP *float64 `json:"topP,omitempty"` + MinPixels *int `json:"minPixels,omitempty"` + MaxPixels *int `json:"maxPixels,omitempty"` + MaxNewTokens *int `json:"maxNewTokens,omitempty"` + VlmExtraArgs map[string]interface{} `json:"vlmExtraArgs,omitempty"` + MergeLayoutBlocks *bool `json:"mergeLayoutBlocks,omitempty"` + MarkdownIgnoreLabels []string `json:"markdownIgnoreLabels,omitempty"` + PrettifyMarkdown *bool `json:"prettifyMarkdown,omitempty"` + ShowFormulaNumber *bool `json:"showFormulaNumber,omitempty"` + RestructurePages *bool `json:"restructurePages,omitempty"` + MergeTables *bool `json:"mergeTables,omitempty"` + RelevelTitles *bool `json:"relevelTitles,omitempty"` + ReturnMarkdownImages *bool `json:"returnMarkdownImages,omitempty"` + OutputFormats []string `json:"outputFormats,omitempty"` + Visualize *bool `json:"visualize,omitempty"` + ExtraOptions map[string]interface{} `json:"-"` +} + +// DocParsingOptionsProvider marks document parsing option structs. +type DocParsingOptionsProvider interface { + isDocParsingOptions() +} + +func (*PPStructureV3Options) isDocParsingOptions() {} +func (*PaddleOCRVLOptions) isDocParsingOptions() {} + +type OCRRequest struct { + Model string + FileURL string + FilePath string + PageRanges string + BatchID string + Options *OCROptions +} + +type DocParsingRequest struct { + Model string + FileURL string + FilePath string + PageRanges string + BatchID string + Options DocParsingOptionsProvider +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/ocr.go b/ocr_webapp/PaddleOCR/api_sdk/go/ocr.go new file mode 100644 index 0000000000000000000000000000000000000000..bb38a7a892f0799b218340097cf816d2c67a6108 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/ocr.go @@ -0,0 +1,317 @@ +// 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. + +package paddleocr + +import ( + "context" + "encoding/json" +) + +func (c *Client) OCR(ctx context.Context, req *OCRRequest) (*OCRResult, error) { + job, err := c.SubmitOCR(ctx, req) + if err != nil { + return nil, err + } + return c.WaitOCRResult(ctx, job.JobID) +} + +// ParseDocument performs document parsing. Blocks until result is ready. +func (c *Client) ParseDocument(ctx context.Context, req *DocParsingRequest) (*DocParsingResult, error) { + job, err := c.SubmitDocumentParsing(ctx, req) + if err != nil { + return nil, err + } + return c.WaitDocumentParsingResult(ctx, job.JobID) +} + +// SubmitOCR submits an OCR job and returns job metadata for tracking. +func (c *Client) SubmitOCR(ctx context.Context, req *OCRRequest) (*Job, error) { + if req == nil { + return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "OCR request is nil"}} + } + model := req.Model + if model == "" { + model = PPOCRv6 + } + if !IsOCRModel(model) { + return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "model is not an OCR model: " + model}} + } + jobID, err := c.submit(ctx, model, req.FileURL, req.FilePath, req.Options, req.PageRanges, req.BatchID) + if err != nil { + return nil, err + } + return &Job{JobID: jobID, Model: model, Task: "ocr", PageRanges: req.PageRanges, BatchID: req.BatchID}, nil +} + +// SubmitDocumentParsing submits a document parsing job and returns job metadata for tracking. +func (c *Client) SubmitDocumentParsing(ctx context.Context, req *DocParsingRequest) (*Job, error) { + if req == nil { + return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "document parsing request is nil"}} + } + model := req.Model + if model == "" { + model = PaddleOCRVL16 + } + if !IsDocumentParsingModel(model) { + return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "model is not a document parsing model: " + model}} + } + jobID, err := c.submit(ctx, model, req.FileURL, req.FilePath, req.Options, req.PageRanges, req.BatchID) + if err != nil { + return nil, err + } + return &Job{JobID: jobID, Model: model, Task: "document_parsing", PageRanges: req.PageRanges, BatchID: req.BatchID}, nil +} + +func (c *Client) WaitOCRResult(ctx context.Context, jobID string) (*OCRResult, error) { + jsonlData, err := c.pollUntilDone(ctx, jobID) + if err != nil { + return nil, err + } + return parseOCRResult(jobID, jsonlData) +} + +func (c *Client) WaitDocumentParsingResult(ctx context.Context, jobID string) (*DocParsingResult, error) { + jsonlData, err := c.pollUntilDone(ctx, jobID) + if err != nil { + return nil, err + } + return parseDocParsingResult(jobID, jsonlData) +} + +func (c *Client) GetStatus(ctx context.Context, jobID string) (*JobStatus, error) { + status, err := c.getJobStatus(ctx, jobID) + if err != nil { + return nil, err + } + return normalizeStatus(jobID, status) +} + +func (c *Client) GetBatchStatus(ctx context.Context, batchID string) (*BatchStatus, error) { + if batchID == "" { + return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "batchID is required"}} + } + return c.getBatchStatus(ctx, batchID) +} + +func (c *Client) submit(ctx context.Context, model, fileURL, filePath string, options interface{}, pageRanges, batchID string) (string, error) { + if fileURL == "" && filePath == "" { + return "", &InvalidRequestError{PaddleOCRAPIError{Message: "Either FileURL or FilePath is required."}} + } + if fileURL != "" && filePath != "" { + return "", &InvalidRequestError{PaddleOCRAPIError{Message: "FileURL and FilePath are mutually exclusive."}} + } + + payload := defaultPayload(model, options) + + if fileURL != "" { + return c.submitURL(ctx, model, fileURL, payload, pageRanges, batchID) + } + return c.submitFile(ctx, model, filePath, payload, pageRanges, batchID) +} + +func defaultPayload(model string, options interface{}) interface{} { + switch typed := options.(type) { + case *OCROptions: + if typed != nil { + return payloadWithExtraOptions(typed) + } + case *PPStructureV3Options: + if typed != nil { + return payloadWithExtraOptions(typed) + } + case *PaddleOCRVLOptions: + if typed != nil { + return payloadWithExtraOptions(typed) + } + default: + if options != nil { + return payloadWithExtraOptions(options) + } + } + if IsOCRModel(model) { + return payloadWithExtraOptions(&OCROptions{}) + } + if IsVLModel(model) { + return payloadWithExtraOptions(&PaddleOCRVLOptions{}) + } + return payloadWithExtraOptions(&PPStructureV3Options{}) +} + +func payloadWithExtraOptions(options interface{}) interface{} { + payloadBytes, _ := json.Marshal(options) + payload := map[string]interface{}{} + _ = json.Unmarshal(payloadBytes, &payload) + + var extraOptions map[string]interface{} + switch typed := options.(type) { + case *OCROptions: + extraOptions = typed.ExtraOptions + case *PPStructureV3Options: + extraOptions = typed.ExtraOptions + case *PaddleOCRVLOptions: + extraOptions = typed.ExtraOptions + } + for key, value := range extraOptions { + payload[key] = value + } + return payload +} + +func parseOCRResult(jobID string, jsonlData []map[string]interface{}) (*OCRResult, error) { + result := &OCRResult{JobID: jobID, DataInfo: map[string]interface{}{}} + for _, lineObj := range jsonlData { + resultData, ok := lineObj["result"].(map[string]interface{}) + if !ok { + return nil, &ResultParseError{PaddleOCRAPIError{Message: "OCR result item is missing result"}} + } + if dataInfo, ok := resultData["dataInfo"].(map[string]interface{}); ok { + for key, value := range dataInfo { + result.DataInfo[key] = value + } + } + ocrResults, ok := resultData["ocrResults"].([]interface{}) + if !ok { + return nil, &ResultParseError{PaddleOCRAPIError{Message: "OCR result item is missing ocrResults"}} + } + for _, item := range ocrResults { + itemMap, ok := item.(map[string]interface{}) + if !ok { + return nil, &ResultParseError{PaddleOCRAPIError{Message: "OCR result page is malformed"}} + } + if _, ok := itemMap["prunedResult"]; !ok { + return nil, &ResultParseError{PaddleOCRAPIError{Message: "OCR result page is missing prunedResult"}} + } + page := OCRPage{ + PrunedResult: itemMap["prunedResult"], + OCRImageURL: getString(itemMap, "ocrImage"), + DocPreprocessingImageURL: getString(itemMap, "docPreprocessingImage"), + InputImageURL: getString(itemMap, "inputImage"), + Raw: itemMap, + } + result.Pages = append(result.Pages, page) + } + } + return result, nil +} + +func parseDocParsingResult(jobID string, jsonlData []map[string]interface{}) (*DocParsingResult, error) { + result := &DocParsingResult{JobID: jobID, DataInfo: map[string]interface{}{}} + for _, lineObj := range jsonlData { + resultData, ok := lineObj["result"].(map[string]interface{}) + if !ok { + return nil, &ResultParseError{PaddleOCRAPIError{Message: "document parsing result item is missing result"}} + } + if dataInfo, ok := resultData["dataInfo"].(map[string]interface{}); ok { + for key, value := range dataInfo { + result.DataInfo[key] = value + } + } + lpResults, ok := resultData["layoutParsingResults"].([]interface{}) + if !ok { + return nil, &ResultParseError{PaddleOCRAPIError{Message: "document parsing result item is missing layoutParsingResults"}} + } + for _, item := range lpResults { + itemMap, ok := item.(map[string]interface{}) + if !ok { + return nil, &ResultParseError{PaddleOCRAPIError{Message: "document parsing result page is malformed"}} + } + markdown, ok := itemMap["markdown"].(map[string]interface{}) + if !ok || getString(markdown, "text") == "" { + return nil, &ResultParseError{PaddleOCRAPIError{Message: "document parsing result page is missing markdown.text"}} + } + page := DocParsingPage{ + MarkdownText: getString(markdown, "text"), + MarkdownImages: getStringMap(markdown, "images"), + OutputImages: getStringMap(itemMap, "outputImages"), + PrunedResult: itemMap["prunedResult"], + InputImageURL: getString(itemMap, "inputImage"), + Exports: getMap(itemMap, "exports"), + Markdown: markdown, + Raw: itemMap, + } + result.Pages = append(result.Pages, page) + } + } + return result, nil +} + +func getString(m map[string]interface{}, key string) string { + if m == nil { + return "" + } + v, _ := m[key].(string) + return v +} + +func getMap(m map[string]interface{}, key string) map[string]interface{} { + value, ok := m[key].(map[string]interface{}) + if !ok { + return map[string]interface{}{} + } + return value +} + +func getStringMap(m map[string]interface{}, key string) map[string]string { + if m == nil { + return nil + } + raw, ok := m[key] + if !ok { + return nil + } + switch v := raw.(type) { + case map[string]interface{}: + result := make(map[string]string, len(v)) + for k, val := range v { + if s, ok := val.(string); ok { + result[k] = s + } + } + return result + default: + b, _ := json.Marshal(raw) + var result map[string]string + json.Unmarshal(b, &result) + return result + } +} + +func normalizeStatus(jobID string, status *jobStatusResponse) (*JobStatus, error) { + if status == nil || status.State == "" { + return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "status response is missing state"}} + } + if status.State != "pending" && status.State != "running" && status.State != "done" && status.State != "failed" { + return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "unknown job state: " + status.State}} + } + result := &JobStatus{ + JobID: jobID, + State: status.State, + ResultURL: status.ResultURL, + ErrorMsg: status.ErrorMsg, + } + if len(status.ExtractProgress) != 0 && string(status.ExtractProgress) != "null" { + var progress extractProgress + if err := json.Unmarshal(status.ExtractProgress, &progress); err != nil { + return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "status progress is malformed", Cause: err}} + } + result.Progress = &Progress{ + TotalPages: progress.TotalPages, + ExtractedPages: progress.ExtractedPages, + StartTime: progress.StartTime, + EndTime: progress.EndTime, + } + } + return result, nil +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/operation.go b/ocr_webapp/PaddleOCR/api_sdk/go/operation.go new file mode 100644 index 0000000000000000000000000000000000000000..dced9e70cbdd8f4b95bc7c4f0f58302ab53126e0 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/operation.go @@ -0,0 +1,69 @@ +// 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. + +package paddleocr + +import ( + "context" + "encoding/json" +) + +// Operation represents an in-progress job. Use Wait() to block until done, +// or Poll() to check status without blocking. +type Operation struct { + client *Client + JobID string + model string +} + +// Wait blocks until the job completes and returns the parsed result. +func (op *Operation) Wait(ctx context.Context) (interface{}, error) { + jsonlData, err := op.client.pollUntilDone(ctx, op.JobID) + if err != nil { + return nil, err + } + if IsOCRModel(op.model) { + return parseOCRResult(op.JobID, jsonlData) + } + return parseDocParsingResult(op.JobID, jsonlData) +} + +// Poll checks the current job status without waiting. +// Returns the status, whether the job is done, and any error. +func (op *Operation) Poll(ctx context.Context) (*JobStatus, bool, error) { + status, err := op.client.getJobStatus(ctx, op.JobID) + if err != nil { + return nil, false, err + } + + js := &JobStatus{ + JobID: op.JobID, + State: status.State, + ErrorMsg: status.ErrorMsg, + } + + if status.ExtractProgress != nil { + var ep extractProgress + if err := json.Unmarshal(status.ExtractProgress, &ep); err == nil { + js.Progress = &Progress{ + TotalPages: ep.TotalPages, + ExtractedPages: ep.ExtractedPages, + StartTime: ep.StartTime, + EndTime: ep.EndTime, + } + } + } + + return js, status.State == "done", nil +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/options.go b/ocr_webapp/PaddleOCR/api_sdk/go/options.go new file mode 100644 index 0000000000000000000000000000000000000000..de7832e2832e26f96d2ecfc56325166905cb865f --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/options.go @@ -0,0 +1,73 @@ +// 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. + +package paddleocr + +import ( + "net/http" + "time" +) + +const DefaultBaseURL = "https://paddleocr.aistudio-app.com" + +const apiPath = "/api/v2/ocr/jobs" + +type ClientOption func(*Client) + +func WithToken(token string) ClientOption { + return func(c *Client) { + c.token = token + } +} + +func WithBaseURL(url string) ClientOption { + return func(c *Client) { + c.baseURL = url + } +} + +func WithTimeout(d time.Duration) ClientOption { + return func(c *Client) { + c.requestTimeout = d + c.pollTimeout = d + } +} + +func WithRequestTimeout(d time.Duration) ClientOption { + return func(c *Client) { + c.requestTimeout = d + } +} + +func WithPollTimeout(d time.Duration) ClientOption { + return func(c *Client) { + c.pollTimeout = d + } +} + +func WithClientPlatform(clientPlatform string) ClientOption { + return func(c *Client) { + c.clientPlatform = clientPlatform + } +} + +func WithHTTPClient(hc *http.Client) ClientOption { + return func(c *Client) { + c.httpClient = hc + } +} + +func Bool(v bool) *bool { + return &v +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/poller.go b/ocr_webapp/PaddleOCR/api_sdk/go/poller.go new file mode 100644 index 0000000000000000000000000000000000000000..975b4beaac5ae51da81ed056632aa24386ffc99d --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/poller.go @@ -0,0 +1,90 @@ +// 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. + +package paddleocr + +import ( + "context" + "fmt" + "time" +) + +const ( + initialInterval = 3 * time.Second + multiplier = 1.5 + maxInterval = 15 * time.Second +) + +func (c *Client) pollUntilDone(ctx context.Context, jobID string) ([]map[string]interface{}, error) { + interval := initialInterval + deadline := time.Now().Add(c.pollTimeout) + pollCtx, cancel := context.WithDeadline(ctx, deadline) + defer cancel() + + for time.Now().Before(deadline) { + remaining := time.Until(deadline) + if remaining <= 0 { + break + } + if interval > remaining { + interval = remaining + } + timer := time.NewTimer(interval) + select { + case <-pollCtx.Done(): + timer.Stop() + if ctx.Err() != nil { + return nil, ctx.Err() + } + return nil, &PollTimeoutError{ + JobID: jobID, + Elapsed: c.pollTimeout.Seconds(), + PaddleOCRAPIError: PaddleOCRAPIError{Message: fmt.Sprintf("Timed out after %.1fs", c.pollTimeout.Seconds())}, + } + case <-timer.C: + } + + status, err := c.getJobStatus(pollCtx, jobID) + if err != nil { + return nil, err + } + + switch status.State { + case "done": + jsonURL := status.ResultURL["jsonUrl"] + if jsonURL == "" { + return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "done job response is missing resultUrl.jsonUrl"}} + } + return c.fetchJSONL(pollCtx, jsonURL) + case "failed": + return nil, &JobFailedError{ + JobID: jobID, + ErrorMsg: status.ErrorMsg, + PaddleOCRAPIError: PaddleOCRAPIError{Message: status.ErrorMsg}, + } + } + + next := time.Duration(float64(interval) * multiplier) + if next > maxInterval { + next = maxInterval + } + interval = next + } + + return nil, &PollTimeoutError{ + JobID: jobID, + Elapsed: c.pollTimeout.Seconds(), + PaddleOCRAPIError: PaddleOCRAPIError{Message: fmt.Sprintf("Timed out after %.1fs", c.pollTimeout.Seconds())}, + } +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/resource.go b/ocr_webapp/PaddleOCR/api_sdk/go/resource.go new file mode 100644 index 0000000000000000000000000000000000000000..81b4cb9a85899ddc81bc200691da5681f6a683ba --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/resource.go @@ -0,0 +1,267 @@ +// 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. + +package paddleocr + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "sort" + "strings" +) + +type SaveResourceOption func(*saveResourceOptions) + +type saveResourceOptions struct { + overwrite bool +} + +func WithOverwrite(overwrite bool) SaveResourceOption { + return func(o *saveResourceOptions) { + o.overwrite = overwrite + } +} + +// SaveResource downloads one result resource URL. +func (c *Client) SaveResource(ctx context.Context, resourceURL, dest string, opts ...SaveResourceOption) (string, error) { + if resourceURL == "" { + return "", &InvalidRequestError{PaddleOCRAPIError{Message: "resource URL is required"}} + } + if dest == "" { + return "", &InvalidRequestError{PaddleOCRAPIError{Message: "destination path is required"}} + } + options := saveResourceOptions{} + for _, opt := range opts { + opt(&options) + } + savedPath, err := resolveSavePath(resourceURL, dest) + if err != nil { + return "", err + } + parent := filepath.Dir(savedPath) + if st, err := os.Stat(parent); err != nil { + if errors.Is(err, os.ErrNotExist) { + return "", &FileNotFoundError{Path: parent, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination parent not found: " + parent, Cause: err}} + } + return "", err + } else if !st.IsDir() { + return "", &FileNotFoundError{Path: parent, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination parent is not a directory: " + parent}} + } + if !options.overwrite { + if _, err := os.Stat(savedPath); err == nil { + return "", &InvalidRequestError{PaddleOCRAPIError{Message: "destination already exists: " + savedPath}} + } else if err != nil && !errors.Is(err, os.ErrNotExist) { + return "", err + } + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, resourceURL, nil) + if err != nil { + return "", &NetworkError{PaddleOCRAPIError{Message: err.Error(), Cause: err}} + } + resp, err := c.httpClient.Do(req) + if err != nil { + return "", classifyHTTPError(err) + } + defer resp.Body.Close() + if err := raiseForResourceResponse(resp); err != nil { + return "", err + } + + tmp, err := os.CreateTemp(parent, "."+filepath.Base(savedPath)+".tmp-*") + if err != nil { + return "", err + } + tmpPath := tmp.Name() + cleanupTemp := true + defer func() { + if cleanupTemp { + _ = os.Remove(tmpPath) + } + }() + if _, err := io.Copy(tmp, resp.Body); err != nil { + _ = tmp.Close() + return "", err + } + if err := tmp.Close(); err != nil { + return "", err + } + if options.overwrite { + if err := os.Rename(tmpPath, savedPath); err != nil { + return "", err + } + } else { + if err := os.Link(tmpPath, savedPath); err != nil { + if errors.Is(err, os.ErrExist) { + return "", &InvalidRequestError{PaddleOCRAPIError{Message: "destination already exists: " + savedPath, Cause: err}} + } + return "", err + } + if err := os.Remove(tmpPath); err != nil { + return "", err + } + } + cleanupTemp = false + return savedPath, nil +} + +func (c *Client) SaveOCRResultResources(ctx context.Context, result *OCRResult, destDir string, opts ...SaveResourceOption) ([]string, error) { + if result == nil { + return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "OCR result is required"}} + } + if err := requireExistingDirectory(destDir); err != nil { + return nil, err + } + saved := make([]string, 0, len(result.Pages)) + for i, page := range result.Pages { + if page.OCRImageURL == "" { + continue + } + name := fmt.Sprintf("ocr-page-%d%s", i+1, safeResourceExtension(page.OCRImageURL)) + if err := validateResultResourceFilename(name); err != nil { + return nil, err + } + path, err := c.SaveResource(ctx, page.OCRImageURL, filepath.Join(destDir, name), opts...) + if err != nil { + return nil, err + } + saved = append(saved, path) + } + return saved, nil +} + +func (c *Client) SaveDocumentParsingResultResources(ctx context.Context, result *DocParsingResult, destDir string, opts ...SaveResourceOption) ([]string, error) { + if result == nil { + return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "document parsing result is required"}} + } + if err := requireExistingDirectory(destDir); err != nil { + return nil, err + } + var saved []string + for _, page := range result.Pages { + paths, err := c.saveNamedResourceMap(ctx, page.MarkdownImages, destDir, opts...) + if err != nil { + return nil, err + } + saved = append(saved, paths...) + paths, err = c.saveNamedResourceMap(ctx, page.OutputImages, destDir, opts...) + if err != nil { + return nil, err + } + saved = append(saved, paths...) + } + return saved, nil +} + +func (c *Client) saveNamedResourceMap(ctx context.Context, resources map[string]string, destDir string, opts ...SaveResourceOption) ([]string, error) { + keys := make([]string, 0, len(resources)) + for key := range resources { + keys = append(keys, key) + } + sort.Strings(keys) + saved := make([]string, 0, len(keys)) + for _, key := range keys { + resourceURL := resources[key] + if resourceURL == "" { + continue + } + name, err := resultResourceFilename(key) + if err != nil { + return nil, err + } + path, err := c.SaveResource(ctx, resourceURL, filepath.Join(destDir, name), opts...) + if err != nil { + return nil, err + } + saved = append(saved, path) + } + return saved, nil +} + +func resolveSavePath(resourceURL, dest string) (string, error) { + if st, err := os.Stat(dest); err == nil && st.IsDir() { + name := "resource" + u, parseErr := url.Parse(resourceURL) + if parseErr == nil { + if base := path.Base(u.Path); base != "." && base != "/" && base != "" { + name = base + } + } + return filepath.Join(dest, name), nil + } else if err != nil && !errors.Is(err, os.ErrNotExist) { + return "", err + } + return dest, nil +} + +func raiseForResourceResponse(resp *http.Response) error { + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return nil + } + body, _ := io.ReadAll(resp.Body) + return &APIError{StatusCode: resp.StatusCode, PaddleOCRAPIError: PaddleOCRAPIError{Message: string(body)}} +} + +func requireExistingDirectory(destDir string) error { + if destDir == "" { + return &InvalidRequestError{PaddleOCRAPIError{Message: "destination directory is required"}} + } + st, err := os.Stat(destDir) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return &FileNotFoundError{Path: destDir, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination directory not found: " + destDir, Cause: err}} + } + return err + } + if !st.IsDir() { + return &FileNotFoundError{Path: destDir, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination is not a directory: " + destDir}} + } + return nil +} + +func resultResourceFilename(key string) (string, error) { + if err := validateResultResourceFilename(key); err != nil { + return "", err + } + return key, nil +} + +func validateResultResourceFilename(name string) error { + if name == "" || name == "." || name == ".." || filepath.IsAbs(name) || path.IsAbs(name) || strings.ContainsAny(name, `/\`) { + return &InvalidRequestError{PaddleOCRAPIError{Message: "unsafe resource filename: " + name}} + } + return nil +} + +func safeResourceExtension(resourceURL string) string { + u, err := url.Parse(resourceURL) + if err != nil { + return "" + } + ext := path.Ext(path.Base(u.Path)) + if len(ext) <= 1 { + return "" + } + if err := validateResultResourceFilename("resource" + ext); err != nil { + return "" + } + return ext +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/results.go b/ocr_webapp/PaddleOCR/api_sdk/go/results.go new file mode 100644 index 0000000000000000000000000000000000000000..a2e071d6efb6ca2cb93e26d77b3465bb26a71ade --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/results.go @@ -0,0 +1,74 @@ +// 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. + +package paddleocr + +type OCRPage struct { + PrunedResult interface{} `json:"prunedResult"` + OCRImageURL string `json:"ocrImageUrl,omitempty"` + DocPreprocessingImageURL string `json:"docPreprocessingImageUrl,omitempty"` + InputImageURL string `json:"inputImageUrl,omitempty"` + Raw map[string]interface{} `json:"raw,omitempty"` +} + +type DocParsingPage struct { + MarkdownText string `json:"markdownText"` + MarkdownImages map[string]string `json:"markdownImages"` + OutputImages map[string]string `json:"outputImages"` + PrunedResult interface{} `json:"prunedResult,omitempty"` + InputImageURL string `json:"inputImageUrl,omitempty"` + Exports map[string]interface{} `json:"exports,omitempty"` + Markdown map[string]interface{} `json:"markdown,omitempty"` + Raw map[string]interface{} `json:"raw,omitempty"` +} + +type OCRResult struct { + JobID string `json:"jobId"` + Pages []OCRPage `json:"pages"` + DataInfo map[string]interface{} `json:"dataInfo,omitempty"` +} + +type DocParsingResult struct { + JobID string `json:"jobId"` + Pages []DocParsingPage `json:"pages"` + DataInfo map[string]interface{} `json:"dataInfo,omitempty"` +} + +type Progress struct { + TotalPages int `json:"totalPages"` + ExtractedPages int `json:"extractedPages"` + StartTime string `json:"startTime,omitempty"` + EndTime string `json:"endTime,omitempty"` +} + +type Job struct { + JobID string `json:"jobId"` + Model string `json:"model"` + Task string `json:"task"` + PageRanges string `json:"pageRanges,omitempty"` + BatchID string `json:"batchId,omitempty"` +} + +type JobStatus struct { + JobID string `json:"jobId"` + State string `json:"state"` + Progress *Progress `json:"progress,omitempty"` + ResultURL map[string]string `json:"resultUrl,omitempty"` + ErrorMsg string `json:"errorMsg,omitempty"` +} + +type BatchStatus struct { + BatchID string `json:"batchId"` + Jobs []*JobStatus `json:"jobs"` +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/go/transport.go b/ocr_webapp/PaddleOCR/api_sdk/go/transport.go new file mode 100644 index 0000000000000000000000000000000000000000..92887c9c6b96dbb532bfc2206adca2303bc6d1df --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/go/transport.go @@ -0,0 +1,329 @@ +// 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. + +package paddleocr + +import ( + "bytes" + "context" + "encoding/json" + "io" + "mime/multipart" + "net" + "net/http" + "os" + "path/filepath" + "strings" +) + +type apiResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data json.RawMessage `json:"data"` +} + +type submitResponse struct { + JobID string `json:"jobId"` +} + +type jobStatusResponse struct { + JobID string `json:"jobId"` + State string `json:"state"` + ExtractProgress json.RawMessage `json:"extractProgress"` + ResultURL map[string]string `json:"resultUrl"` + ErrorMsg string `json:"errorMsg"` +} + +type extractProgress struct { + TotalPages int `json:"totalPages"` + ExtractedPages int `json:"extractedPages"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` +} + +func (c *Client) submitURL(ctx context.Context, model, fileURL string, payload interface{}, pageRanges, batchID string) (string, error) { + body := map[string]interface{}{ + "fileUrl": fileURL, + "model": model, + "optionalPayload": payload, + } + if pageRanges != "" { + body["pageRanges"] = pageRanges + } + if batchID != "" { + body["batchId"] = batchID + } + jsonBody, err := json.Marshal(body) + if err != nil { + return "", &ResponseFormatError{PaddleOCRAPIError{Message: "failed to encode submit request", Cause: err}} + } + + req, err := http.NewRequestWithContext(ctx, "POST", c.jobsURL, bytes.NewReader(jsonBody)) + if err != nil { + return "", &NetworkError{PaddleOCRAPIError{Message: err.Error()}} + } + req.Header.Set("Authorization", "Bearer "+c.token) + req.Header.Set("Content-Type", "application/json") + c.setClientPlatformHeader(req) + + resp, err := c.httpClient.Do(req) + if err != nil { + return "", classifyHTTPError(err) + } + defer resp.Body.Close() + + if err := raiseForResponse(resp); err != nil { + return "", err + } + + apiResp, err := decodeAPIResponse(resp) + if err != nil { + return "", err + } + var sr submitResponse + if err := json.Unmarshal(apiResp.Data, &sr); err != nil { + return "", &ResponseFormatError{PaddleOCRAPIError{Message: "submit response is malformed", Cause: err}} + } + if sr.JobID == "" { + return "", &ResponseFormatError{PaddleOCRAPIError{Message: "submit response is missing jobId"}} + } + return sr.JobID, nil +} + +func (c *Client) submitFile(ctx context.Context, model, filePath string, payload interface{}, pageRanges, batchID string) (string, error) { + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return "", &FileNotFoundError{Path: filePath, PaddleOCRAPIError: PaddleOCRAPIError{Message: "File not found: " + filePath}} + } + + var buf bytes.Buffer + w := multipart.NewWriter(&buf) + + _ = w.WriteField("model", model) + payloadJSON, err := json.Marshal(payload) + if err != nil { + return "", &ResponseFormatError{PaddleOCRAPIError{Message: "failed to encode optionalPayload", Cause: err}} + } + if err := w.WriteField("optionalPayload", string(payloadJSON)); err != nil { + return "", &ResponseFormatError{PaddleOCRAPIError{Message: "failed to prepare multipart body", Cause: err}} + } + if pageRanges != "" { + _ = w.WriteField("pageRanges", pageRanges) + } + if batchID != "" { + _ = w.WriteField("batchId", batchID) + } + + file, err := os.Open(filePath) + if err != nil { + return "", &FileNotFoundError{Path: filePath, PaddleOCRAPIError: PaddleOCRAPIError{Message: "Failed to open file: " + filePath, Cause: err}} + } + defer file.Close() + + fw, err := w.CreateFormFile("file", filepath.Base(filePath)) + if err != nil { + return "", &ResponseFormatError{PaddleOCRAPIError{Message: "failed to prepare multipart body", Cause: err}} + } + if _, err := io.Copy(fw, file); err != nil { + return "", &NetworkError{PaddleOCRAPIError{Message: "failed to read file content", Cause: err}} + } + if err := w.Close(); err != nil { + return "", &ResponseFormatError{PaddleOCRAPIError{Message: "failed to finalize multipart body", Cause: err}} + } + + req, err := http.NewRequestWithContext(ctx, "POST", c.jobsURL, &buf) + if err != nil { + return "", &NetworkError{PaddleOCRAPIError{Message: err.Error()}} + } + req.Header.Set("Authorization", "Bearer "+c.token) + req.Header.Set("Content-Type", w.FormDataContentType()) + c.setClientPlatformHeader(req) + + resp, err := c.httpClient.Do(req) + if err != nil { + return "", classifyHTTPError(err) + } + defer resp.Body.Close() + + if err := raiseForResponse(resp); err != nil { + return "", err + } + + apiResp, err := decodeAPIResponse(resp) + if err != nil { + return "", err + } + var sr submitResponse + if err := json.Unmarshal(apiResp.Data, &sr); err != nil { + return "", &ResponseFormatError{PaddleOCRAPIError{Message: "submit response is malformed", Cause: err}} + } + if sr.JobID == "" { + return "", &ResponseFormatError{PaddleOCRAPIError{Message: "submit response is missing jobId"}} + } + return sr.JobID, nil +} + +func (c *Client) getJobStatus(ctx context.Context, jobID string) (*jobStatusResponse, error) { + req, err := http.NewRequestWithContext(ctx, "GET", c.jobsURL+"/"+jobID, nil) + if err != nil { + return nil, &NetworkError{PaddleOCRAPIError{Message: err.Error()}} + } + req.Header.Set("Authorization", "Bearer "+c.token) + c.setClientPlatformHeader(req) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, classifyHTTPError(err) + } + defer resp.Body.Close() + + if err := raiseForResponse(resp); err != nil { + return nil, err + } + + apiResp, err := decodeAPIResponse(resp) + if err != nil { + return nil, err + } + var status jobStatusResponse + if err := json.Unmarshal(apiResp.Data, &status); err != nil { + return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "status response is malformed", Cause: err}} + } + if status.State == "" { + return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "status response is missing state"}} + } + return &status, nil +} + +func (c *Client) getBatchStatus(ctx context.Context, batchID string) (*BatchStatus, error) { + req, err := http.NewRequestWithContext(ctx, "GET", c.jobsURL+"/batch/"+batchID, nil) + if err != nil { + return nil, &NetworkError{PaddleOCRAPIError{Message: err.Error(), Cause: err}} + } + req.Header.Set("Authorization", "Bearer "+c.token) + c.setClientPlatformHeader(req) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, classifyHTTPError(err) + } + defer resp.Body.Close() + + if err := raiseForResponse(resp); err != nil { + return nil, err + } + apiResp, err := decodeAPIResponse(resp) + if err != nil { + return nil, err + } + var payload struct { + ExtractResult []jobStatusResponse `json:"extractResult"` + } + if err := json.Unmarshal(apiResp.Data, &payload); err != nil { + return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "batch response is malformed", Cause: err}} + } + result := &BatchStatus{BatchID: batchID} + for _, item := range payload.ExtractResult { + jobID := item.JobID + if jobID == "" { + return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "batch response item is missing jobId"}} + } + status, err := normalizeStatus(jobID, &item) + if err != nil { + return nil, err + } + result.Jobs = append(result.Jobs, status) + } + return result, nil +} + +func (c *Client) fetchJSONL(ctx context.Context, url string) ([]map[string]interface{}, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, &NetworkError{PaddleOCRAPIError{Message: err.Error()}} + } + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, classifyHTTPError(err) + } + defer resp.Body.Close() + if err := raiseForResponse(resp); err != nil { + return nil, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + lines := strings.Split(strings.TrimSpace(string(body)), "\n") + var results []map[string]interface{} + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + var obj map[string]interface{} + if err := json.Unmarshal([]byte(line), &obj); err != nil { + return nil, &ResultParseError{PaddleOCRAPIError{Message: "failed to parse JSONL result payload", Cause: err}} + } + results = append(results, obj) + } + return results, nil +} + +func raiseForResponse(resp *http.Response) error { + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return nil + } + body, _ := io.ReadAll(resp.Body) + msg := string(body) + + switch { + case resp.StatusCode == 401 || resp.StatusCode == 403: + return &AuthError{PaddleOCRAPIError{Message: "Authentication failed: " + msg}} + case resp.StatusCode == 400: + return &InvalidRequestError{PaddleOCRAPIError{Message: "Bad request: " + msg}} + case resp.StatusCode == 429: + return &RateLimitError{APIError{StatusCode: 429, PaddleOCRAPIError: PaddleOCRAPIError{Message: "Rate limit exceeded: " + msg}}} + case resp.StatusCode == 503 || resp.StatusCode == 504: + return &ServiceUnavailableError{APIError{StatusCode: resp.StatusCode, PaddleOCRAPIError: PaddleOCRAPIError{Message: "Service unavailable: " + msg}}} + default: + return &APIError{StatusCode: resp.StatusCode, PaddleOCRAPIError: PaddleOCRAPIError{Message: msg}} + } +} + +func decodeAPIResponse(resp *http.Response) (*apiResponse, error) { + var apiResp apiResponse + if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { + return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "expected a JSON response body", Cause: err}} + } + if apiResp.Code != 0 { + msg := apiResp.Msg + if msg == "" { + msg = "PaddleOCR official API request failed" + } + return nil, &APIError{StatusCode: resp.StatusCode, PaddleOCRAPIError: PaddleOCRAPIError{Message: msg}} + } + if len(apiResp.Data) == 0 || string(apiResp.Data) == "null" { + return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "response body is missing data"}} + } + return &apiResp, nil +} + +func classifyHTTPError(err error) error { + if ne, ok := err.(net.Error); ok && ne.Timeout() { + return &RequestTimeoutError{PaddleOCRAPIError: PaddleOCRAPIError{Message: err.Error(), Cause: err}} + } + return &NetworkError{PaddleOCRAPIError{Message: err.Error(), Cause: err}} +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/.gitignore b/ocr_webapp/PaddleOCR/api_sdk/typescript/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..aea33c4060e9b512adb0f19fe8755e840fd3b475 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +.vite/ +coverage/ +.nyc_output/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/README.md b/ocr_webapp/PaddleOCR/api_sdk/typescript/README.md new file mode 100644 index 0000000000000000000000000000000000000000..484ce1863a813f70cab0d1e006134fef27c3be17 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/README.md @@ -0,0 +1,72 @@ +# PaddleOCR TypeScript SDK + +English | [简体中文](README_cn.md) + +TypeScript client for the PaddleOCR official API. It submits OCR and document +parsing jobs to hosted PaddleOCR services; it does not perform local OCR +inference. + +Official user docs: + +- [TypeScript SDK](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.md) +- [TypeScript SDK (English)](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.en.md) + +## Install + +```bash +npm install @paddleocr/api-sdk +``` + +This package follows SemVer and is published as a public scoped npm package. + +For local development: + +```bash +npm install +npm run build +``` + +## Minimal Usage + +Set `PADDLEOCR_ACCESS_TOKEN` or pass `token` to the client: + +```bash +export PADDLEOCR_ACCESS_TOKEN="your-access-token" +``` + +```ts +import { Model, PaddleOCRClient } from "@paddleocr/api-sdk"; + +const client = new PaddleOCRClient(); +const result = await client.ocr({ + model: Model.PPOCRv5, + fileUrl: "https://example.com/invoice.pdf", +}); + +console.log(result.jobId, result.pages.length); +``` + +Specify `model: Model.PPOCRv6` (or `"PP-OCRv6"`) to use the PP-OCRv6 hosted OCR model. +Use `Model.PPOCRv5Latin` (or `"PP-OCRv5-latin"`) for the PP-OCRv5 Latin-script hosted OCR model. + +Document parsing defaults to PaddleOCR-VL-1.6: + +```ts +const doc = await client.parseDocument({ + filePath: "./report.pdf", + options: { + useChartRecognition: true, + }, +}); + +console.log(doc.jobId, doc.pages.length); +``` + +## Build And Test + +```bash +npm run lint +npm run build +npm test +npm audit --audit-level=moderate +``` diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/README_cn.md b/ocr_webapp/PaddleOCR/api_sdk/typescript/README_cn.md new file mode 100644 index 0000000000000000000000000000000000000000..ad2ebacb23e0d7fb03c2c3c2fa0ce2b795eaf144 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/README_cn.md @@ -0,0 +1,70 @@ +# PaddleOCR TypeScript SDK + +[English](README.md) | 简体中文 + +面向 PaddleOCR 官方 API 的 TypeScript 客户端。它会把 OCR 和文档解析任务提交到 PaddleOCR 官方托管服务;不会在本地执行 OCR 推理。 + +正式用户文档: + +- [TypeScript SDK](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.md) +- [TypeScript SDK(英文)](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.en.md) + +## 安装 + +```bash +npm install @paddleocr/api-sdk +``` + +该包遵循语义化版本,并作为公开 scoped npm 包发布。 + +本地开发: + +```bash +npm install +npm run build +``` + +## 最小示例 + +设置 `PADDLEOCR_ACCESS_TOKEN`,或在构造客户端时传入 `token`: + +```bash +export PADDLEOCR_ACCESS_TOKEN="your-access-token" +``` + +```ts +import { Model, PaddleOCRClient } from "@paddleocr/api-sdk"; + +const client = new PaddleOCRClient(); +const result = await client.ocr({ + model: Model.PPOCRv5, + fileUrl: "https://example.com/invoice.pdf", +}); + +console.log(result.jobId, result.pages.length); +``` + +通过 `model: Model.PPOCRv6`(或 `"PP-OCRv6"`)可指定 PP-OCRv6 云端 OCR 模型。 +使用 `Model.PPOCRv5Latin`(或 `"PP-OCRv5-latin"`)可指定 PP-OCRv5 拉丁语系云端 OCR 模型。 + +文档解析默认使用 PaddleOCR-VL-1.6: + +```ts +const doc = await client.parseDocument({ + filePath: "./report.pdf", + options: { + useChartRecognition: true, + }, +}); + +console.log(doc.jobId, doc.pages.length); +``` + +## 构建与测试 + +```bash +npm run lint +npm run build +npm test +npm audit --audit-level=moderate +``` diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/examples/doc-parsing-file.ts b/ocr_webapp/PaddleOCR/api_sdk/typescript/examples/doc-parsing-file.ts new file mode 100644 index 0000000000000000000000000000000000000000..99c39ec39bbe0d38c22083bd0db2f2c49f391318 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/examples/doc-parsing-file.ts @@ -0,0 +1,47 @@ +// 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 { PaddleOCRClient, Model } from "@paddleocr/api-sdk"; + +async function main() { + const client = new PaddleOCRClient(); + + // Convenience: doc parsing with local file + const result = await client.parseDocument({ + model: Model.PPStructureV3, + filePath: "./sample.pdf", + options: { useChartRecognition: true }, + }); + + for (const page of result.pages) { + console.log(page.markdownText); + } + + // Manual control: submit + concurrent wait + const job1 = await client.submitOcr({ fileUrl: "https://example.com/f1.pdf" }); + const job2 = await client.submitDocumentParsing({ + model: Model.PPStructureV3, + filePath: "./sample.pdf", + }); + + const [r1, r2] = await Promise.all([ + client.waitOcrResult(job1.jobId), + client.waitDocumentParsingResult(job2.jobId), + ]); + + console.log("Job1 done:", r1); + console.log("Job2 done:", r2); +} + +main().catch(console.error); diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/examples/ocr-url.ts b/ocr_webapp/PaddleOCR/api_sdk/typescript/examples/ocr-url.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7c15f0a0eb1788f425d2165d9143013a448f065 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/examples/ocr-url.ts @@ -0,0 +1,31 @@ +// 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 { PaddleOCRClient, Model } from "@paddleocr/api-sdk"; + +async function main() { + const client = new PaddleOCRClient(); + + // Convenience: OCR with URL + const ocrResult = await client.ocr({ + fileUrl: "https://example.com/invoice.pdf", + }); + + for (const page of ocrResult.pages) { + console.log("Result:", page.prunedResult); + console.log("Image:", page.ocrImageUrl); + } +} + +main().catch(console.error); diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/package-lock.json b/ocr_webapp/PaddleOCR/api_sdk/typescript/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..73b09ede139cf419d67eafc6504bb24dbf194edb --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/package-lock.json @@ -0,0 +1,2640 @@ +{ + "name": "@paddleocr/api-sdk", + "version": "0.2.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@paddleocr/api-sdk", + "version": "0.2.2", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^25.9.1", + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^4.1.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", + "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", + "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", + "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", + "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", + "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", + "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", + "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", + "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.7", + "@vitest/browser-preview": "4.1.7", + "@vitest/browser-webdriverio": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/ui": "4.1.7", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz", + "integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/package.json b/ocr_webapp/PaddleOCR/api_sdk/typescript/package.json new file mode 100644 index 0000000000000000000000000000000000000000..250926801b56e70fedf998bde475cdc8cb289b79 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/package.json @@ -0,0 +1,62 @@ +{ + "name": "@paddleocr/api-sdk", + "version": "0.2.3", + "description": "TypeScript SDK for the PaddleOCR official API", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "files": [ + "dist", + "README.md", + "README_cn.md" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/PaddlePaddle/PaddleOCR.git", + "directory": "api_sdk/typescript" + }, + "homepage": "https://github.com/PaddlePaddle/PaddleOCR/tree/main/api_sdk/typescript", + "bugs": { + "url": "https://github.com/PaddlePaddle/PaddleOCR/issues" + }, + "keywords": [ + "paddleocr", + "ocr", + "document-parsing", + "api-sdk", + "typescript", + "official-api" + ], + "scripts": { + "build": "tsup", + "test": "vitest run", + "lint": "tsc --noEmit", + "prepublishOnly": "npm run lint && npm run build && npm test" + }, + "engines": { + "node": ">=18" + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^25.9.1", + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^4.1.7" + } +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/src/client.ts b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/client.ts new file mode 100644 index 0000000000000000000000000000000000000000..7fa4c7bfb149ace0d357bb02c31b103690b63386 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/client.ts @@ -0,0 +1,394 @@ +// 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 { stat, writeFile } from "node:fs/promises"; +import { basename, dirname, extname, join } from "node:path"; +import { AuthError, FileNotFoundError, InvalidRequestError, ResultParseError } from "./errors.js"; +import { HttpClient } from "./internal/http.js"; +import { Poller } from "./internal/poller.js"; +import type { ClientOptions, DocParsingRequest, OCRRequest, SaveResourceOptions } from "./models.js"; +import { isDocumentParsingModel, isOCRModel, Model } from "./models.js"; +import type { BatchStatus, DocParsingResult, Job, JobStatus, OCRResult } from "./results.js"; + +const DEFAULT_BASE_URL = "https://paddleocr.aistudio-app.com"; + +interface ResourceSavePlan { + resourceUrl: string; + filename: string; +} + +export class PaddleOCRClient { + private http: HttpClient; + private poller: Poller; + + constructor(options: ClientOptions = {}) { + const token = options.token || process.env.PADDLEOCR_ACCESS_TOKEN || ""; + if (!token) { + throw new AuthError("Token is required. Set PADDLEOCR_ACCESS_TOKEN or pass token option."); + } + const baseUrl = options.baseUrl || process.env.PADDLEOCR_BASE_URL || DEFAULT_BASE_URL; + const requestTimeout = options.requestTimeout || options.timeout || 300000; + const pollTimeout = options.pollTimeout || options.timeout || 600000; + + this.http = new HttpClient( + token, + baseUrl, + requestTimeout, + options.fetch, + options.clientPlatform, + ); + this.poller = new Poller(this.http, pollTimeout); + } + + async ocr(req: OCRRequest, options?: { signal?: AbortSignal }): Promise { + const job = await this.submitOcr(req, options); + return this.waitOcrResult(job, options); + } + + async parseDocument(req: DocParsingRequest, options?: { signal?: AbortSignal }): Promise { + const job = await this.submitDocumentParsing(req, options); + return this.waitDocumentParsingResult(job, options); + } + + async submitOcr(req: OCRRequest, options?: { signal?: AbortSignal }): Promise { + const model = req.model ?? Model.PPOCRv6; + const jobId = await this.submit(model, "ocr", req, options?.signal); + return { jobId, model, task: "ocr", pageRanges: req.pageRanges, batchId: req.batchId }; + } + + async submitDocumentParsing(req: DocParsingRequest, options?: { signal?: AbortSignal }): Promise { + const model = req.model ?? Model.PaddleOCRVL16; + if (!isDocumentParsingModel(model)) { + throw new InvalidRequestError(`Model ${model} is not a document parsing model.`); + } + const jobId = await this.submit(model, "document_parsing", req, options?.signal); + return { jobId, model, task: "document_parsing", pageRanges: req.pageRanges, batchId: req.batchId }; + } + + async waitOcrResult(job: Job | string, options?: { signal?: AbortSignal }): Promise { + const resolved = this.resolveJob(job, "ocr"); + const jsonlData = await this.poller.pollUntilDone(resolved.jobId, options?.signal); + return this.parseOCRResult(resolved.jobId, jsonlData); + } + + async waitDocumentParsingResult(job: Job | string, options?: { signal?: AbortSignal }): Promise { + const resolved = this.resolveJob(job, "document_parsing"); + const jsonlData = await this.poller.pollUntilDone(resolved.jobId, options?.signal); + return this.parseDocParsingResult(resolved.jobId, jsonlData); + } + + async getStatus(jobId: string, options?: { signal?: AbortSignal }): Promise { + return this.poller.getStatus(jobId, options?.signal); + } + + async getBatchStatus(batchId: string, options?: { signal?: AbortSignal }): Promise { + return this.poller.getBatchStatus(batchId, options?.signal); + } + + async saveResource( + resourceUrl: string, + destination: string, + options: SaveResourceOptions = {}, + ): Promise { + return this.saveResourceUrl(resourceUrl, destination, options); + } + + async saveOcrResultResources( + result: OCRResult, + destination: string, + options: SaveResourceOptions = {}, + ): Promise { + return this.saveResultResources(result, destination, options); + } + + async saveDocumentParsingResultResources( + result: DocParsingResult, + destination: string, + options: SaveResourceOptions = {}, + ): Promise { + return this.saveResultResources(result, destination, options); + } + + private async saveResourceUrl(resourceUrl: string, destination: string, options: SaveResourceOptions): Promise { + if (!resourceUrl) { + throw new InvalidRequestError("resourceUrl is required."); + } + let url: URL; + try { + url = new URL(resourceUrl); + } catch (error) { + throw new InvalidRequestError(`Invalid resource URL: ${resourceUrl}`, { cause: error }); + } + const target = await this.resolveDestination(url, destination, options); + const content = await this.http.fetchResource(resourceUrl); + await writeFile(target, Buffer.from(content), { flag: options.overwrite ? "w" : "wx" }); + return target; + } + + private async saveResultResources( + result: OCRResult | DocParsingResult, + destination: string, + options: SaveResourceOptions, + ): Promise { + await this.requireExistingDirectory(destination); + const plans = this.collectResultResourcePlans(result); + const targets = plans.map((plan) => join(destination, plan.filename)); + for (const target of targets) { + await this.requireWritableTarget(target, options); + } + this.requireUniqueTargets(targets, options); + const savedPaths: string[] = []; + for (const [index, plan] of plans.entries()) { + await this.saveResourceUrl(plan.resourceUrl, targets[index], options); + savedPaths.push(targets[index]); + } + return savedPaths; + } + + private collectResultResourcePlans(result: OCRResult | DocParsingResult): ResourceSavePlan[] { + if (isDocParsingResult(result)) { + return result.pages.flatMap((page) => [ + ...this.collectMappedResourcePlans(page.markdownImages), + ...this.collectMappedResourcePlans(page.outputImages), + ]); + } + return result.pages.flatMap((page, index) => { + if (!page.ocrImageUrl) { + return []; + } + return [{ + resourceUrl: page.ocrImageUrl, + filename: `ocr-page-${index + 1}${resourceExtension(page.ocrImageUrl)}`, + }]; + }); + } + + private collectMappedResourcePlans(resources: Record): ResourceSavePlan[] { + return Object.keys(resources) + .sort() + .map((key) => ({ + resourceUrl: resources[key], + filename: safeMapKeyFilename(key), + })); + } + + private async requireExistingDirectory(destination: string): Promise { + let destinationStat; + try { + destinationStat = await stat(destination); + } catch { + throw new FileNotFoundError(destination); + } + if (!destinationStat.isDirectory()) { + throw new InvalidRequestError(`Destination must be an existing directory: ${destination}`); + } + } + + private async requireWritableTarget(target: string, options: SaveResourceOptions): Promise { + try { + await stat(target); + } catch { + return; + } + if (!options.overwrite) { + throw new InvalidRequestError(`Destination already exists: ${target}`); + } + } + + private requireUniqueTargets(targets: string[], options: SaveResourceOptions): void { + if (options.overwrite) { + return; + } + const seen = new Set(); + for (const target of targets) { + if (seen.has(target)) { + throw new InvalidRequestError(`Destination already exists: ${target}`); + } + seen.add(target); + } + } + + private async resolveDestination(url: URL, destination: string, options: SaveResourceOptions): Promise { + let destinationStat; + try { + destinationStat = await stat(destination); + } catch { + destinationStat = undefined; + } + const target = destinationStat?.isDirectory() ? join(destination, safeUrlBasename(url)) : destination; + await this.requireWritableTarget(target, options); + const parent = dirname(target); + try { + const parentStat = await stat(parent); + if (!parentStat.isDirectory()) { + throw new InvalidRequestError(`Destination parent must be a directory: ${parent}`); + } + } catch (error) { + if (error instanceof InvalidRequestError) { + throw error; + } + throw new FileNotFoundError(parent, { cause: error }); + } + return target; + } + + private async submit( + model: string, + task: Job["task"], + req: { fileUrl?: string; filePath?: string; pageRanges?: string; batchId?: string; options?: object }, + signal?: AbortSignal, + ): Promise { + if (!req.fileUrl && !req.filePath) { + throw new InvalidRequestError("Either fileUrl or filePath is required."); + } + if (req.fileUrl && req.filePath) { + throw new InvalidRequestError("fileUrl and filePath are mutually exclusive."); + } + + this.validateModelForTask(model, task); + const payload = req.options || {}; + + if (req.fileUrl) { + return this.http.submitUrl(model, req.fileUrl, payload, { + pageRanges: req.pageRanges, + batchId: req.batchId, + signal, + }); + } + return this.http.submitFile(model, req.filePath!, payload, { + pageRanges: req.pageRanges, + batchId: req.batchId, + signal, + }); + } + + private parseOCRResult(jobId: string, jsonlData: unknown[]): OCRResult { + const dataInfo: Record = {}; + const pages = jsonlData.flatMap((lineObj) => { + if (!isRecord(lineObj) || !isRecord(lineObj.result) || !Array.isArray(lineObj.result.ocrResults)) { + throw new ResultParseError("OCR result item is missing result.ocrResults."); + } + if (isRecord(lineObj.result.dataInfo)) { + Object.assign(dataInfo, lineObj.result.dataInfo); + } + return lineObj.result.ocrResults.map((item) => { + if (!isRecord(item) || !("prunedResult" in item)) { + throw new ResultParseError("OCR result page is missing prunedResult."); + } + return { + prunedResult: item.prunedResult, + ocrImageUrl: typeof item.ocrImage === "string" ? item.ocrImage : undefined, + docPreprocessingImageUrl: typeof item.docPreprocessingImage === "string" ? item.docPreprocessingImage : undefined, + inputImageUrl: typeof item.inputImage === "string" ? item.inputImage : undefined, + raw: item, + }; + }); + }); + return { jobId, pages, dataInfo }; + } + + private parseDocParsingResult(jobId: string, jsonlData: unknown[]): DocParsingResult { + const dataInfo: Record = {}; + const pages = jsonlData.flatMap((lineObj) => { + if (!isRecord(lineObj) || !isRecord(lineObj.result) || !Array.isArray(lineObj.result.layoutParsingResults)) { + throw new ResultParseError("Document parsing result item is missing result.layoutParsingResults."); + } + if (isRecord(lineObj.result.dataInfo)) { + Object.assign(dataInfo, lineObj.result.dataInfo); + } + return lineObj.result.layoutParsingResults.map((item) => { + if (!isRecord(item) || !isRecord(item.markdown) || typeof item.markdown.text !== "string") { + throw new ResultParseError("Document parsing result page is missing markdown.text."); + } + return { + markdownText: item.markdown.text, + markdownImages: isRecord(item.markdown.images) ? stringMap(item.markdown.images) : {}, + outputImages: isRecord(item.outputImages) ? stringMap(item.outputImages) : {}, + prunedResult: item.prunedResult, + inputImageUrl: typeof item.inputImage === "string" ? item.inputImage : undefined, + exports: isRecord(item.exports) ? item.exports : {}, + markdown: item.markdown, + raw: item, + }; + }); + }); + return { jobId, pages, dataInfo }; + } + + private resolveJob(job: Job | string, expectedTask: Job["task"]): Job { + if (typeof job === "string") { + return { + jobId: job, + model: expectedTask === "ocr" ? Model.PPOCRv6 : Model.PaddleOCRVL16, + task: expectedTask, + }; + } + if (job.task !== expectedTask) { + throw new InvalidRequestError(`Job ${job.jobId} is a ${job.task} job, not a ${expectedTask} job.`); + } + this.validateModelForTask(job.model, expectedTask); + return job; + } + + private validateModelForTask(model: string, task: Job["task"]): void { + if (task === "ocr" && !isOCRModel(model)) { + throw new InvalidRequestError(`Model ${model} is not an OCR model.`); + } + if (task === "document_parsing" && !isDocumentParsingModel(model)) { + throw new InvalidRequestError(`Model ${model} is not a document parsing model.`); + } + } +} + +function stringMap(value: Record): Record { + const result: Record = {}; + for (const [key, val] of Object.entries(value)) { + if (typeof val === "string") { + result[key] = val; + } + } + return result; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function isDocParsingResult(result: OCRResult | DocParsingResult): result is DocParsingResult { + return result.pages.some((page) => "markdownText" in page); +} + +function safeMapKeyFilename(key: string): string { + if (!key || key === "." || key === ".." || key.includes("/") || key.includes("\\") || key.startsWith(".")) { + throw new InvalidRequestError(`Unsafe resource filename: ${key}`); + } + return key; +} + +function safeUrlBasename(url: URL): string { + const name = basename(url.pathname) || "resource"; + if (name === "." || name === "..") { + return "resource"; + } + return name; +} + +function resourceExtension(resourceUrl: string): string { + try { + const url = new URL(resourceUrl); + return extname(url.pathname); + } catch { + return ""; + } +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/src/errors.ts b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/errors.ts new file mode 100644 index 0000000000000000000000000000000000000000..149e5577cbb063c333376e4326c3d8fa437d0034 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/errors.ts @@ -0,0 +1,119 @@ +// 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. + +export class PaddleOCRAPIError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message); + this.name = "PaddleOCRAPIError"; + this.cause = options?.cause; + } +} + +export class AuthError extends PaddleOCRAPIError { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "AuthError"; + } +} + +export class InvalidRequestError extends PaddleOCRAPIError { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "InvalidRequestError"; + } +} + +export class APIError extends PaddleOCRAPIError { + statusCode: number; + constructor(statusCode: number, message: string, options?: ErrorOptions) { + super(`HTTP ${statusCode}: ${message}`, options); + this.name = "APIError"; + this.statusCode = statusCode; + } +} + +export class RateLimitError extends APIError { + constructor(message: string, options?: ErrorOptions) { + super(429, message, options); + this.name = "RateLimitError"; + } +} + +export class ServiceUnavailableError extends APIError { + constructor(statusCode: number, message: string, options?: ErrorOptions) { + super(statusCode, message, options); + this.name = "ServiceUnavailableError"; + } +} + +export class JobFailedError extends PaddleOCRAPIError { + jobId: string; + errorMsg: string; + constructor(jobId: string, errorMsg: string, options?: ErrorOptions) { + super(`Job ${jobId} failed: ${errorMsg}`, options); + this.name = "JobFailedError"; + this.jobId = jobId; + this.errorMsg = errorMsg; + } +} + +export class RequestTimeoutError extends PaddleOCRAPIError { + timeoutMs: number; + constructor(timeoutMs: number, options?: ErrorOptions) { + super(`Request timed out after ${timeoutMs}ms`, options); + this.name = "RequestTimeoutError"; + this.timeoutMs = timeoutMs; + } +} + +export class PollTimeoutError extends PaddleOCRAPIError { + jobId: string; + timeoutMs: number; + constructor(jobId: string, timeoutMs: number, options?: ErrorOptions) { + super(`Timed out after ${timeoutMs}ms waiting for job ${jobId}`, options); + this.name = "PollTimeoutError"; + this.jobId = jobId; + this.timeoutMs = timeoutMs; + } +} + +export class NetworkError extends PaddleOCRAPIError { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "NetworkError"; + } +} + +export class FileNotFoundError extends PaddleOCRAPIError { + path: string; + constructor(path: string, options?: ErrorOptions) { + super(`File not found: ${path}`, options); + this.name = "FileNotFoundError"; + this.path = path; + } +} + +export class ResponseFormatError extends PaddleOCRAPIError { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "ResponseFormatError"; + } +} + +export class ResultParseError extends PaddleOCRAPIError { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "ResultParseError"; + } +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/src/index.ts b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0eb08973548724622c3d8e218d5846327468cff6 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/index.ts @@ -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. + +export { PaddleOCRClient } from "./client.js"; +export { Model, isDocumentParsingModel, isOCRModel, isVLModel } from "./models.js"; +export type { + ClientOptions, + DocParsingOptions, + DocParsingRequest, + OCROptions, + OCRRequest, + PaddleOCRVLOptions, + PPStructureV3Options, + SaveResourceOptions, +} from "./models.js"; +export type { + BatchStatus, + DocParsingPage, + DocParsingResult, + Job, + JobStatus, + OCRPage, + OCRResult, + Progress, +} from "./results.js"; +export { + APIError, + AuthError, + FileNotFoundError, + InvalidRequestError, + JobFailedError, + NetworkError, + PaddleOCRAPIError, + PollTimeoutError, + RateLimitError, + RequestTimeoutError, + ResponseFormatError, + ResultParseError, + ServiceUnavailableError, +} from "./errors.js"; diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/src/internal/abort.ts b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/internal/abort.ts new file mode 100644 index 0000000000000000000000000000000000000000..2d1cab14a6a484f5187124ae99c4f82ef5a602f4 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/internal/abort.ts @@ -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. + +/** Value to reject/throw when an operation was aborted via `AbortSignal` (aligns with `fetch` + `DOMException`). */ +export function userAbortReason(signal: AbortSignal): unknown { + return signal.reason !== undefined + ? signal.reason + : new DOMException("The user aborted a request.", "AbortError"); +} + +export function throwIfAborted(signal?: AbortSignal): void { + if (!signal?.aborted) return; + throw userAbortReason(signal); +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/src/internal/http.ts b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/internal/http.ts new file mode 100644 index 0000000000000000000000000000000000000000..be0778d6b5fa015c4b90cae8ba9d45fd74c73df1 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/internal/http.ts @@ -0,0 +1,255 @@ +// 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 { + APIError, + AuthError, + InvalidRequestError, + NetworkError, + RateLimitError, + RequestTimeoutError, + ResponseFormatError, + ResultParseError, + ServiceUnavailableError, +} from "../errors.js"; +import { userAbortReason } from "./abort.js"; + +const DEFAULT_BASE_URL = "https://paddleocr.aistudio-app.com"; +const API_PATH = "/api/v2/ocr/jobs"; + +interface SubmitOptions { + pageRanges?: string; + batchId?: string; + signal?: AbortSignal; +} + +interface APIResponse { + code?: number; + msg?: string; + data: T; +} + +interface SubmitResponse { + jobId: string; +} + +export class HttpClient { + private baseUrl: string; + private jobsUrl: string; + private token: string; + private requestTimeout: number; + private fetchImpl: typeof fetch; + private clientPlatform?: string; + + constructor( + token: string, + baseUrl: string = DEFAULT_BASE_URL, + requestTimeout: number = 300000, + fetchImpl: typeof fetch = fetch, + clientPlatform?: string, + ) { + this.token = token; + this.baseUrl = baseUrl.replace(/\/+$/, ""); + this.jobsUrl = `${this.baseUrl}${API_PATH}`; + this.requestTimeout = requestTimeout; + this.fetchImpl = fetchImpl; + this.clientPlatform = clientPlatform; + } + + async submitUrl(model: string, fileUrl: string, optionalPayload: object, options: SubmitOptions = {}): Promise { + const body: Record = { fileUrl, model, optionalPayload }; + if (options.pageRanges !== undefined) { + body.pageRanges = options.pageRanges; + } + if (options.batchId !== undefined) { + body.batchId = options.batchId; + } + const data = await this.fetchJson(this.jobsUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }, options.signal); + return requireJobId(data); + } + + async submitFile(model: string, filePath: string, optionalPayload: object, options: SubmitOptions = {}): Promise { + const fs = await import("fs"); + const path = await import("path"); + + if (!fs.existsSync(filePath)) { + const { FileNotFoundError } = await import("../errors.js"); + throw new FileNotFoundError(filePath); + } + + const form = new FormData(); + form.append("model", model); + form.append("optionalPayload", JSON.stringify(optionalPayload)); + if (options.pageRanges !== undefined) { + form.append("pageRanges", options.pageRanges); + } + if (options.batchId !== undefined) { + form.append("batchId", options.batchId); + } + + const fileBuffer = fs.readFileSync(filePath); + const blob = new Blob([fileBuffer]); + form.append("file", blob, path.basename(filePath)); + + const data = await this.fetchJson(this.jobsUrl, { + method: "POST", + body: form, + }, options.signal); + return requireJobId(data); + } + + async getJobStatus(jobId: string, signal?: AbortSignal, timeoutMs?: number): Promise { + return this.fetchJson( + `${this.jobsUrl}/${encodeURIComponent(jobId)}`, + { method: "GET" }, + signal, + true, + timeoutMs, + ); + } + + async getBatchStatus(batchId: string, signal?: AbortSignal, timeoutMs?: number): Promise { + return this.fetchJson( + `${this.jobsUrl}/batch/${encodeURIComponent(batchId)}`, + { method: "GET" }, + signal, + true, + timeoutMs, + ); + } + + async fetchJsonl(url: string, signal?: AbortSignal, timeoutMs?: number): Promise { + const resp = await this.fetch(url, { method: "GET" }, signal, false, timeoutMs); + const text = await resp.text(); + try { + return text + .trim() + .split("\n") + .filter((line) => line.trim()) + .map((line) => JSON.parse(line) as unknown); + } catch (error) { + throw new ResultParseError("Failed to parse JSONL result payload.", { cause: error }); + } + } + + async fetchResource(url: string, signal?: AbortSignal, timeoutMs?: number): Promise { + const resp = await this.fetch(url, { method: "GET" }, signal, false, timeoutMs); + return resp.arrayBuffer(); + } + + private async fetchJson( + url: string, + init: RequestInit, + signal?: AbortSignal, + withAuth: boolean = true, + timeoutMs?: number, + ): Promise { + const resp = await this.fetch(url, init, signal, withAuth, timeoutMs); + let payload: APIResponse; + try { + payload = await resp.json() as APIResponse; + } catch (error) { + throw new ResponseFormatError("Expected a JSON response body.", { cause: error }); + } + if (payload.code !== undefined && payload.code !== 0) { + throw new APIError(resp.status, payload.msg || "PaddleOCR official API request failed."); + } + if (!payload || typeof payload !== "object" || !("data" in payload)) { + throw new ResponseFormatError("Response body is missing data."); + } + return payload.data; + } + + private async fetch( + url: string, + init: RequestInit, + signal?: AbortSignal, + withAuth: boolean = true, + timeoutMs?: number, + ): Promise { + const headers: Record = { + ...(init.headers as Record || {}), + }; + if (withAuth) { + headers.Authorization = `Bearer ${this.token}`; + if (this.clientPlatform) { + headers["Client-Platform"] = this.clientPlatform; + } + } + + let resp: Response; + const timeoutController = new AbortController(); + const effectiveTimeout = Math.max(0, Math.min(this.requestTimeout, timeoutMs ?? this.requestTimeout)); + const timeoutID = setTimeout(() => timeoutController.abort(), effectiveTimeout); + const abortController = new AbortController(); + const abort = () => abortController.abort(); + timeoutController.signal.addEventListener("abort", abort, { once: true }); + if (signal?.aborted) { + abort(); + } else { + signal?.addEventListener("abort", abort, { once: true }); + } + try { + resp = await this.fetchImpl(url, { + ...init, + headers, + signal: abortController.signal, + }); + } catch (e: unknown) { + if (signal?.aborted) { + throw userAbortReason(signal); + } + if (timeoutController.signal.aborted) { + throw new RequestTimeoutError(effectiveTimeout, { cause: e }); + } + const message = e instanceof Error ? e.message : String(e); + throw new NetworkError(`Connection failed: ${message}`); + } finally { + clearTimeout(timeoutID); + signal?.removeEventListener("abort", abort); + } + + if (resp.ok) return resp; + + let text = await resp.text(); + try { + const payload = JSON.parse(text) as { msg?: string; message?: string; errorMsg?: string }; + text = payload.msg || payload.message || payload.errorMsg || text; + } catch { + // Keep raw response text. + } + if (resp.status === 401 || resp.status === 403) { + throw new AuthError(`Authentication failed: ${text}`); + } else if (resp.status === 400) { + throw new InvalidRequestError(`Bad request: ${text}`); + } else if (resp.status === 429) { + throw new RateLimitError(`Rate limit exceeded: ${text}`); + } else if (resp.status === 503 || resp.status === 504) { + throw new ServiceUnavailableError(resp.status, `Service unavailable: ${text}`); + } else { + throw new APIError(resp.status, text); + } + } +} + +function requireJobId(data: SubmitResponse): string { + if (!data || typeof data.jobId !== "string" || data.jobId.length === 0) { + throw new ResponseFormatError("Submit response is missing jobId."); + } + return data.jobId; +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/src/internal/poller.ts b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/internal/poller.ts new file mode 100644 index 0000000000000000000000000000000000000000..51b1f3cf4167f116fab418898ee0c44c36ce3ddb --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/internal/poller.ts @@ -0,0 +1,162 @@ +// 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 { JobFailedError, PollTimeoutError, RequestTimeoutError, ResponseFormatError } from "../errors.js"; +import type { BatchStatus, JobStatus, Progress } from "../results.js"; +import { throwIfAborted, userAbortReason } from "./abort.js"; +import { HttpClient } from "./http.js"; + +const INITIAL_INTERVAL = 3000; +const MULTIPLIER = 1.5; +const MAX_INTERVAL = 15000; +const MAX_WAIT_TIME = 600000; + +export class Poller { + private http: HttpClient; + private maxWaitTime: number; + + constructor(http: HttpClient, maxWaitTime: number = MAX_WAIT_TIME) { + this.http = http; + this.maxWaitTime = maxWaitTime; + } + + async pollUntilDone(jobId: string, signal?: AbortSignal): Promise { + let interval = INITIAL_INTERVAL; + const deadline = Date.now() + this.maxWaitTime; + + while (Date.now() < deadline) { + throwIfAborted(signal); + + const remaining = deadline - Date.now(); + const data = await this.withPollTimeout(jobId, remaining, () => this.http.getJobStatus(jobId, signal, remaining)); + const status = normalizeStatus(jobId, data); + + if (status.state === "done") { + const jsonUrl = resultJsonUrl(data); + const resultRemaining = deadline - Date.now(); + return await this.withPollTimeout(jobId, resultRemaining, () => this.http.fetchJsonl(jsonUrl, signal, resultRemaining)); + } + + if (status.state === "failed") { + throw new JobFailedError(jobId, status.errorMsg || "Unknown error"); + } + + await this.sleep(Math.min(interval, Math.max(0, deadline - Date.now())), signal); + interval = Math.min(interval * MULTIPLIER, MAX_INTERVAL); + } + + throw new PollTimeoutError(jobId, this.maxWaitTime); + } + + async getStatus(jobId: string, signal?: AbortSignal): Promise { + return normalizeStatus(jobId, await this.http.getJobStatus(jobId, signal)); + } + + async getBatchStatus(batchId: string, signal?: AbortSignal): Promise { + const data = await this.http.getBatchStatus(batchId, signal); + if (!isRecord(data) || !Array.isArray(data.extractResult)) { + throw new ResponseFormatError("Batch response is missing extractResult."); + } + return { + batchId, + jobs: data.extractResult.map((item) => { + if (!isRecord(item) || typeof item.jobId !== "string") { + throw new ResponseFormatError("Batch extractResult item is missing jobId."); + } + return normalizeStatus(item.jobId, item); + }), + }; + } + + private sleep(ms: number, signal?: AbortSignal): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(resolve, ms); + if (!signal) return; + signal.addEventListener( + "abort", + () => { + clearTimeout(timer); + reject(userAbortReason(signal)); + }, + { once: true }, + ); + }); + } + + private async withPollTimeout(jobId: string, remainingMs: number, operation: () => Promise): Promise { + if (remainingMs <= 0) { + throw new PollTimeoutError(jobId, this.maxWaitTime); + } + try { + return await operation(); + } catch (error) { + if (error instanceof RequestTimeoutError && error.timeoutMs === remainingMs) { + throw new PollTimeoutError(jobId, this.maxWaitTime, { cause: error }); + } + throw error; + } + } +} + +function normalizeStatus(jobId: string, data: unknown): JobStatus { + if (!isRecord(data) || typeof data.state !== "string") { + throw new ResponseFormatError("Status response is missing state."); + } + if (!["pending", "running", "done", "failed"].includes(data.state)) { + throw new ResponseFormatError(`Unknown job state: ${data.state}`); + } + return { + jobId, + state: data.state as JobStatus["state"], + progress: normalizeProgress(data.extractProgress), + resultUrl: isRecord(data.resultUrl) ? stringMap(data.resultUrl) : undefined, + errorMsg: typeof data.errorMsg === "string" ? data.errorMsg : undefined, + }; +} + +function normalizeProgress(progress: unknown): Progress | undefined { + if (progress === undefined || progress === null) { + return undefined; + } + if (!isRecord(progress)) { + throw new ResponseFormatError("Status progress must be an object."); + } + return { + totalPages: typeof progress.totalPages === "number" ? progress.totalPages : 0, + extractedPages: typeof progress.extractedPages === "number" ? progress.extractedPages : 0, + startTime: typeof progress.startTime === "string" ? progress.startTime : undefined, + endTime: typeof progress.endTime === "string" ? progress.endTime : undefined, + }; +} + +function resultJsonUrl(data: unknown): string { + if (!isRecord(data) || !isRecord(data.resultUrl) || typeof data.resultUrl.jsonUrl !== "string") { + throw new ResponseFormatError("Done job response is missing resultUrl.jsonUrl."); + } + return data.resultUrl.jsonUrl; +} + +function stringMap(value: Record): Record { + const result: Record = {}; + for (const [key, val] of Object.entries(value)) { + if (typeof val === "string") { + result[key] = val; + } + } + return result; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/src/models.ts b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/models.ts new file mode 100644 index 0000000000000000000000000000000000000000..b42ebde0d1e4e67647d176ebafe473b3b7ae11da --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/models.ts @@ -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. + +export enum Model { + PPOCRv5 = "PP-OCRv5", + PPOCRv5Latin = "PP-OCRv5-latin", + PPOCRv6 = "PP-OCRv6", + PPStructureV3 = "PP-StructureV3", + PaddleOCRVL = "PaddleOCR-VL", + PaddleOCRVL15 = "PaddleOCR-VL-1.5", + PaddleOCRVL16 = "PaddleOCR-VL-1.6", +} + +const OCR_MODELS = new Set([Model.PPOCRv5, Model.PPOCRv5Latin, Model.PPOCRv6]); +const DOCUMENT_PARSING_MODELS = new Set([ + Model.PPStructureV3, + Model.PaddleOCRVL, + Model.PaddleOCRVL15, + Model.PaddleOCRVL16, +]); +const VL_MODELS = new Set([ + Model.PaddleOCRVL, + Model.PaddleOCRVL15, + Model.PaddleOCRVL16, +]); + +export function isOCRModel( + model: string +): model is Model.PPOCRv5 | Model.PPOCRv5Latin | Model.PPOCRv6 { + return OCR_MODELS.has(model); +} + +export function isDocumentParsingModel(model: string): boolean { + return DOCUMENT_PARSING_MODELS.has(model); +} + +export function isVLModel(model: string): boolean { + return VL_MODELS.has(model); +} + +export interface OCROptions { + useDocOrientationClassify?: boolean; + useDocUnwarping?: boolean; + useTextlineOrientation?: boolean; + textDetLimitSideLen?: number; + textDetLimitType?: string; + textDetThresh?: number; + textDetBoxThresh?: number; + textDetUnclipRatio?: number; + textRecScoreThresh?: number; + visualize?: boolean; + [key: string]: unknown; +} + +export interface PPStructureV3Options { + useDocOrientationClassify?: boolean; + useDocUnwarping?: boolean; + useTextlineOrientation?: boolean; + useSealRecognition?: boolean; + useTableRecognition?: boolean; + useFormulaRecognition?: boolean; + useChartRecognition?: boolean; + useRegionDetection?: boolean; + layoutThreshold?: number | Record; + layoutNms?: boolean; + layoutUnclipRatio?: number | number[] | Record; + layoutMergeBboxesMode?: string | Record; + formatBlockContent?: boolean; + textDetLimitSideLen?: number; + textDetLimitType?: string; + textDetThresh?: number; + textDetBoxThresh?: number; + textDetUnclipRatio?: number; + textRecScoreThresh?: number; + useWiredTableCellsTransToHtml?: boolean; + useWirelessTableCellsTransToHtml?: boolean; + useTableOrientationClassify?: boolean; + useOcrResultsWithTableCells?: boolean; + useE2eWiredTableRecModel?: boolean; + useE2eWirelessTableRecModel?: boolean; + markdownIgnoreLabels?: string[]; + prettifyMarkdown?: boolean; + showFormulaNumber?: boolean; + returnMarkdownImages?: boolean; + outputFormats?: string[]; + visualize?: boolean; + [key: string]: unknown; +} + +export interface PaddleOCRVLOptions { + useDocOrientationClassify?: boolean; + useDocUnwarping?: boolean; + useLayoutDetection?: boolean; + useChartRecognition?: boolean; + useSealRecognition?: boolean; + useOcrForImageBlock?: boolean; + layoutThreshold?: number | Record; + layoutNms?: boolean; + layoutUnclipRatio?: number | number[] | Record; + layoutMergeBboxesMode?: string | Record; + layoutShapeMode?: "rect" | "quad" | "poly" | "auto"; + promptLabel?: "ocr" | "formula" | "table" | "chart" | "seal" | "spotting"; + formatBlockContent?: boolean; + repetitionPenalty?: number; + temperature?: number; + topP?: number; + minPixels?: number; + maxPixels?: number; + maxNewTokens?: number; + vlmExtraArgs?: Record; + mergeLayoutBlocks?: boolean; + markdownIgnoreLabels?: string[]; + prettifyMarkdown?: boolean; + showFormulaNumber?: boolean; + restructurePages?: boolean; + mergeTables?: boolean; + relevelTitles?: boolean; + returnMarkdownImages?: boolean; + outputFormats?: string[]; + visualize?: boolean; + [key: string]: unknown; +} + +export type DocParsingOptions = PPStructureV3Options | PaddleOCRVLOptions; + +export interface OCRRequest { + model?: Model | string; + fileUrl?: string; + filePath?: string; + pageRanges?: string; + batchId?: string; + options?: OCROptions; +} + +export interface DocParsingRequest { + model?: Model | string; + fileUrl?: string; + filePath?: string; + pageRanges?: string; + batchId?: string; + options?: DocParsingOptions; +} + +export interface ClientOptions { + token?: string; + baseUrl?: string; + timeout?: number; + requestTimeout?: number; + pollTimeout?: number; + clientPlatform?: string; + fetch?: typeof fetch; +} + +export interface SaveResourceOptions { + overwrite?: boolean; + filename?: string; +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/src/results.ts b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/results.ts new file mode 100644 index 0000000000000000000000000000000000000000..d39ef2ce17e620c2e627343ae3546ba29d4373ee --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/src/results.ts @@ -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. + +export interface OCRPage { + prunedResult: unknown; + ocrImageUrl?: string; + docPreprocessingImageUrl?: string; + inputImageUrl?: string; + raw?: unknown; +} + +export interface DocParsingPage { + markdownText: string; + markdownImages: Record; + outputImages: Record; + prunedResult?: unknown; + inputImageUrl?: string; + exports?: Record; + markdown?: Record; + raw?: unknown; +} + +export interface OCRResult { + jobId: string; + pages: OCRPage[]; + dataInfo?: Record; +} + +export interface DocParsingResult { + jobId: string; + pages: DocParsingPage[]; + dataInfo?: Record; +} + +export interface Progress { + totalPages: number; + extractedPages: number; + startTime?: string; + endTime?: string; +} + +export interface Job { + jobId: string; + model: string; + task: "ocr" | "document_parsing"; + pageRanges?: string; + batchId?: string; +} + +export interface JobStatus { + jobId: string; + state: "pending" | "running" | "done" | "failed"; + progress?: Progress; + resultUrl?: Record; + errorMsg?: string; +} + +export interface BatchStatus { + batchId: string; + jobs: JobStatus[]; +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/tests/client.test.ts b/ocr_webapp/PaddleOCR/api_sdk/typescript/tests/client.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..7fa24b9cc886c27dac4d709d8973a3efa3a6bb31 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/tests/client.test.ts @@ -0,0 +1,703 @@ +import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { + APIError, + AuthError, + FileNotFoundError, + InvalidRequestError, + JobFailedError, + Model, + PaddleOCRClient, + PollTimeoutError, + RequestTimeoutError, + ResponseFormatError, + ResultParseError, +} from "../src/index.js"; +import type { DocParsingResult, Job, OCRResult } from "../src/index.js"; + +type FetchCall = { + url: string; + init: RequestInit; +}; + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +function textResponse(body: string, status = 200): Response { + return new Response(body, { status }); +} + +function createClient(fetchImpl: typeof fetch, options: Partial[0]> = {}) { + return new PaddleOCRClient({ + token: "test-token", + baseUrl: "https://api.example.test", + requestTimeout: 100, + pollTimeout: 100, + fetch: fetchImpl, + ...options, + }); +} + +function captureFetch(responses: Array>): { fetch: typeof fetch; calls: FetchCall[] } { + const calls: FetchCall[] = []; + const fetchImpl = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + calls.push({ url: String(input), init: init ?? {} }); + const next = responses.shift(); + if (next instanceof Error) { + throw next; + } + if (!next) { + throw new Error("No mocked response remaining"); + } + return next; + }) as unknown as typeof fetch; + return { fetch: fetchImpl, calls }; +} + +afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); +}); + +describe("PaddleOCRClient public contract", () => { + test("requires token at construction and reads PADDLEOCR_ACCESS_TOKEN fallback", () => { + const previous = process.env.PADDLEOCR_ACCESS_TOKEN; + delete process.env.PADDLEOCR_ACCESS_TOKEN; + expect(() => new PaddleOCRClient()).toThrow(AuthError); + + process.env.PADDLEOCR_ACCESS_TOKEN = "env-token"; + const { fetch } = captureFetch([jsonResponse({ data: { state: "running" } })]); + expect(() => new PaddleOCRClient({ baseUrl: "https://api.example.test", fetch })).not.toThrow(); + + if (previous === undefined) { + delete process.env.PADDLEOCR_ACCESS_TOKEN; + } else { + process.env.PADDLEOCR_ACCESS_TOKEN = previous; + } + }); + + test("exposes contract names and omits unpublished draft aliases", () => { + const { fetch } = captureFetch([]); + const client = createClient(fetch); + expect(client.getStatus).toBeTypeOf("function"); + expect(client.parseDocument).toBeTypeOf("function"); + expect(client.submitDocumentParsing).toBeTypeOf("function"); + expect(client.waitOcrResult).toBeTypeOf("function"); + expect(client.waitDocumentParsingResult).toBeTypeOf("function"); + expect(client.saveResource).toBeTypeOf("function"); + expect(client.saveOcrResultResources).toBeTypeOf("function"); + expect(client.saveDocumentParsingResultResources).toBeTypeOf("function"); + + expect("getResult" in client).toBe(false); + expect("waitForResult" in client).toBe(false); + expect("docParsing" in client).toBe(false); + expect("submitDocParsing" in client).toBe(false); + }); + + test("submitOcr sends contract body and returns job metadata", async () => { + const { fetch, calls } = captureFetch([jsonResponse({ data: { jobId: "job-1" } })]); + const client = createClient(fetch); + + const job = await client.submitOcr({ + fileUrl: "https://files.example.test/invoice.pdf", + pageRanges: "1-2", + batchId: "batch-1", + options: { visualize: true }, + }); + + expect(job).toEqual({ + jobId: "job-1", + model: Model.PPOCRv6, + task: "ocr", + pageRanges: "1-2", + batchId: "batch-1", + }); + expect(calls[0].url).toBe("https://api.example.test/api/v2/ocr/jobs"); + expect(calls[0].init.method).toBe("POST"); + expect(calls[0].init.headers).toMatchObject({ + Authorization: "Bearer test-token", + "Content-Type": "application/json", + }); + expect(JSON.parse(String(calls[0].init.body))).toEqual({ + fileUrl: "https://files.example.test/invoice.pdf", + model: Model.PPOCRv6, + optionalPayload: { visualize: true }, + pageRanges: "1-2", + batchId: "batch-1", + }); + }); + + test("clientPlatform is sent as Client-Platform header on API requests", async () => { + const { fetch, calls } = captureFetch([jsonResponse({ data: { jobId: "job-1" } })]); + const client = createClient(fetch, { clientPlatform: "my-app" }); + + await client.submitOcr({ + fileUrl: "https://files.example.test/invoice.pdf", + }); + + expect(calls[0].init.headers).toMatchObject({ + "Client-Platform": "my-app", + }); + }); + + test("submitOcr propagates explicit OCR model and defaults to PP-OCRv6", async () => { + const { fetch, calls } = captureFetch([ + jsonResponse({ data: { jobId: "job-explicit" } }), + jsonResponse({ data: { jobId: "job-default" } }), + ]); + const client = createClient(fetch); + + const explicit = await client.submitOcr({ + model: Model.PPOCRv5, + fileUrl: "https://files.example.test/explicit.pdf", + }); + const implicit = await client.submitOcr({ + fileUrl: "https://files.example.test/default.pdf", + }); + + expect(explicit.model).toBe(Model.PPOCRv5); + expect(implicit.model).toBe(Model.PPOCRv6); + expect(JSON.parse(String(calls[0].init.body)).model).toBe(Model.PPOCRv5); + expect(JSON.parse(String(calls[1].init.body)).model).toBe(Model.PPOCRv6); + }); + + test("submitOcr accepts PP-OCRv6 model name", async () => { + const { fetch, calls } = captureFetch([jsonResponse({ data: { jobId: "job-v6" } })]); + const client = createClient(fetch); + + const job = await client.submitOcr({ + model: Model.PPOCRv6, + fileUrl: "https://files.example.test/v6.pdf", + }); + + expect(job.model).toBe(Model.PPOCRv6); + expect(JSON.parse(String(calls[0].init.body)).model).toBe(Model.PPOCRv6); + }); + + test("submitOcr accepts PP-OCRv5-latin model name", async () => { + const { fetch, calls } = captureFetch([jsonResponse({ data: { jobId: "job-latin" } })]); + const client = createClient(fetch); + + const job = await client.submitOcr({ + model: Model.PPOCRv5Latin, + fileUrl: "https://files.example.test/latin.pdf", + }); + + expect(job.model).toBe(Model.PPOCRv5Latin); + expect(JSON.parse(String(calls[0].init.body)).model).toBe(Model.PPOCRv5Latin); + }); + + test("submitOcr and submitDocumentParsing accept official model name strings", async () => { + const { fetch, calls } = captureFetch([ + jsonResponse({ data: { jobId: "job-ocr" } }), + jsonResponse({ data: { jobId: "job-doc" } }), + ]); + const client = createClient(fetch); + + const ocrJob = await client.submitOcr({ + model: "PP-OCRv5", + fileUrl: "https://files.example.test/ocr.pdf", + }); + const docJob = await client.submitDocumentParsing({ + model: "PaddleOCR-VL-1.6", + fileUrl: "https://files.example.test/doc.pdf", + }); + + expect(ocrJob.model).toBe("PP-OCRv5"); + expect(docJob.model).toBe("PaddleOCR-VL-1.6"); + expect(JSON.parse(String(calls[0].init.body)).model).toBe("PP-OCRv5"); + expect(JSON.parse(String(calls[1].init.body)).model).toBe("PaddleOCR-VL-1.6"); + }); + + test("model helpers classify current OCR and document parsing models", async () => { + const mod = await import("../src/index.js"); + + expect(mod.isOCRModel(Model.PPOCRv5)).toBe(true); + expect(mod.isOCRModel(Model.PPOCRv5Latin)).toBe(true); + expect(mod.isOCRModel(Model.PPOCRv6)).toBe(true); + expect(mod.isOCRModel(Model.PPStructureV3)).toBe(false); + expect(mod.isOCRModel("future-unknown-model")).toBe(false); + expect(mod.isDocumentParsingModel(Model.PaddleOCRVL)).toBe(true); + expect(mod.isDocumentParsingModel(Model.PaddleOCRVL16)).toBe(true); + expect(mod.isDocumentParsingModel(Model.PPOCRv5)).toBe(false); + }); + + test("submitDocumentParsing passes current and future document parsing options", async () => { + const { fetch, calls } = captureFetch([jsonResponse({ data: { jobId: "job-doc" } })]); + const client = createClient(fetch); + + await client.submitDocumentParsing({ + model: Model.PaddleOCRVL16, + fileUrl: "https://files.example.test/doc.pdf", + options: { + useOcrForImageBlock: true, + formatBlockContent: true, + markdownIgnoreLabels: ["image"], + vlmExtraArgs: { temperature: 0.1 }, + returnMarkdownImages: false, + outputFormats: ["docx"], + futureOption: "enabled", + }, + }); + + expect(JSON.parse(String(calls[0].init.body)).optionalPayload).toEqual({ + useOcrForImageBlock: true, + formatBlockContent: true, + markdownIgnoreLabels: ["image"], + vlmExtraArgs: { temperature: 0.1 }, + returnMarkdownImages: false, + outputFormats: ["docx"], + futureOption: "enabled", + }); + }); + + test("submitDocumentParsing defaults to PaddleOCR-VL-1.6", async () => { + const { fetch, calls } = captureFetch([jsonResponse({ data: { jobId: "job-doc" } })]); + const client = createClient(fetch); + + const job = await client.submitDocumentParsing({ + fileUrl: "https://files.example.test/doc.pdf", + }); + + expect(job.model).toBe(Model.PaddleOCRVL16); + expect(JSON.parse(String(calls[0].init.body)).model).toBe(Model.PaddleOCRVL16); + }); + + test("submitOcr rejects non-OCR models before network calls", async () => { + const { fetch } = captureFetch([]); + const client = createClient(fetch); + + await expect( + client.submitOcr({ + model: Model.PPStructureV3, + fileUrl: "https://files.example.test/not-ocr.pdf", + }), + ).rejects.toThrow(InvalidRequestError); + expect(fetch).not.toHaveBeenCalled(); + }); + + test("validates submit requests before network calls", async () => { + const { fetch } = captureFetch([]); + const client = createClient(fetch); + + await expect(client.submitOcr({})).rejects.toThrow(InvalidRequestError); + await expect( + client.submitOcr({ fileUrl: "https://files.example.test/a.pdf", filePath: "./a.pdf" }), + ).rejects.toThrow(InvalidRequestError); + expect(fetch).not.toHaveBeenCalled(); + }); + + test("getStatus performs one non-blocking status request", async () => { + const { fetch, calls } = captureFetch([ + jsonResponse({ + data: { + state: "running", + extractProgress: { totalPages: 3, extractedPages: 1 }, + }, + }), + ]); + const client = createClient(fetch); + + await expect(client.getStatus("job-1")).resolves.toEqual({ + jobId: "job-1", + state: "running", + progress: { totalPages: 3, extractedPages: 1 }, + errorMsg: undefined, + }); + expect(calls).toHaveLength(1); + expect(calls[0].url).toBe("https://api.example.test/api/v2/ocr/jobs/job-1"); + }); + + test("normalizes multiple trailing baseUrl slashes", async () => { + const { fetch, calls } = captureFetch([jsonResponse({ data: { state: "running" } })]); + const client = createClient(fetch, { baseUrl: "https://api.example.test///" }); + + await client.getStatus("job-1"); + + expect(calls[0].url).toBe("https://api.example.test/api/v2/ocr/jobs/job-1"); + }); + + test("typed wait accepts bare jobId, fetches JSONL without Authorization, and parses OCR results", async () => { + const { fetch, calls } = captureFetch([ + jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job-1.jsonl" } } }), + textResponse( + JSON.stringify({ + result: { + dataInfo: { numPages: 1 }, + ocrResults: [ + { + prunedResult: { text: "hello" }, + ocrImage: "img.png", + docPreprocessingImage: "pre.png", + inputImage: "input.png", + }, + ], + }, + }), + ), + ]); + const client = createClient(fetch); + + await expect(client.waitOcrResult("job-1")).resolves.toMatchObject({ + jobId: "job-1", + dataInfo: { numPages: 1 }, + pages: [ + { + prunedResult: { text: "hello" }, + ocrImageUrl: "img.png", + docPreprocessingImageUrl: "pre.png", + inputImageUrl: "input.png", + raw: { + prunedResult: { text: "hello" }, + ocrImage: "img.png", + docPreprocessingImage: "pre.png", + inputImage: "input.png", + }, + }, + ], + }); + expect(calls[1].url).toBe("https://storage.example.test/job-1.jsonl"); + expect((calls[1].init.headers as Record).Authorization).toBeUndefined(); + }); + + test("typed waits reject mismatched Job task before polling", async () => { + const { fetch } = captureFetch([]); + const client = createClient(fetch); + const docJob: Job = { jobId: "job-1", model: Model.PPStructureV3, task: "document_parsing" }; + + await expect(client.waitOcrResult(docJob)).rejects.toThrow(InvalidRequestError); + expect(fetch).not.toHaveBeenCalled(); + }); + + test("typed waits reject mismatched Job model before polling", async () => { + const { fetch } = captureFetch([]); + const client = createClient(fetch); + const ocrTaskWithDocumentModel: Job = { jobId: "job-1", model: Model.PPStructureV3, task: "ocr" }; + const documentTaskWithOcrModel: Job = { jobId: "job-2", model: Model.PPOCRv5, task: "document_parsing" }; + + await expect(client.waitOcrResult(ocrTaskWithDocumentModel)).rejects.toThrow(InvalidRequestError); + await expect(client.waitDocumentParsingResult(documentTaskWithOcrModel)).rejects.toThrow(InvalidRequestError); + expect(fetch).not.toHaveBeenCalled(); + }); + + test("polling maps failed, timeout, unknown state, and missing result URL", async () => { + const failed = createClient(captureFetch([jsonResponse({ data: { state: "failed", errorMsg: "boom" } })]).fetch); + await expect(failed.waitOcrResult("job-1")).rejects.toThrow(JobFailedError); + + const unknown = createClient(captureFetch([jsonResponse({ data: { state: "paused" } })]).fetch); + await expect(unknown.waitOcrResult("job-1")).rejects.toThrow(ResponseFormatError); + + const missingResult = createClient(captureFetch([jsonResponse({ data: { state: "done" } })]).fetch); + await expect(missingResult.waitOcrResult("job-1")).rejects.toThrow(ResponseFormatError); + + vi.useFakeTimers(); + const timeoutFetch = vi.fn(async () => jsonResponse({ data: { state: "running" } })) as unknown as typeof fetch; + const timeout = createClient(timeoutFetch, { pollTimeout: 1 }); + const promise = expect(timeout.waitOcrResult("job-1")).rejects.toThrow(PollTimeoutError); + await vi.runAllTimersAsync(); + await promise; + }); + + test("malformed successful responses and malformed JSONL use dedicated errors", async () => { + const malformedSubmit = createClient(captureFetch([jsonResponse({ data: {} })]).fetch); + await expect(malformedSubmit.submitOcr({ fileUrl: "https://files.example.test/a.pdf" })).rejects.toThrow( + ResponseFormatError, + ); + + const malformedJsonl = createClient( + captureFetch([ + jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job.jsonl" } } }), + textResponse("{not-json}\n"), + ]).fetch, + ); + await expect(malformedJsonl.waitOcrResult("job-1")).rejects.toThrow(ResultParseError); + }); + + test("malformed fetched OCR result records use ResultParseError", async () => { + const client = createClient( + captureFetch([ + jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job.jsonl" } } }), + textResponse(JSON.stringify({ notResult: {} })), + ]).fetch, + ); + + await expect(client.waitOcrResult("job-1")).rejects.toThrow(ResultParseError); + }); + + test.each([ + ["missing ocrResults", { result: {} }], + ["missing prunedResult", { result: { ocrResults: [{ ocrImage: "img.png" }] } }], + ])("malformed OCR payload with %s uses ResultParseError", async (_name, payload) => { + const client = createClient( + captureFetch([ + jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job.jsonl" } } }), + textResponse(JSON.stringify(payload)), + ]).fetch, + ); + + await expect(client.waitOcrResult("job-1")).rejects.toThrow(ResultParseError); + }); + + test("waitDocumentParsingResult preserves raw page data and dataInfo", async () => { + const page = { + prunedResult: { blocks: [{ label: "text", content: "hello" }] }, + markdown: { text: "hello", images: { "figure.png": "https://example.test/figure.png" }, isStart: true, isEnd: true }, + outputImages: { "page.png": "https://example.test/page.png" }, + inputImage: "https://example.test/input.png", + exports: { docx: "https://example.test/result.docx" }, + }; + const { fetch } = captureFetch([ + jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job.jsonl" } } }), + textResponse(JSON.stringify({ result: { dataInfo: { numPages: 1 }, layoutParsingResults: [page] } })), + ]); + const client = createClient(fetch); + + await expect(client.waitDocumentParsingResult("job-1")).resolves.toMatchObject({ + jobId: "job-1", + dataInfo: { numPages: 1 }, + pages: [ + { + markdownText: "hello", + markdownImages: { "figure.png": "https://example.test/figure.png" }, + outputImages: { "page.png": "https://example.test/page.png" }, + prunedResult: { blocks: [{ label: "text", content: "hello" }] }, + inputImageUrl: "https://example.test/input.png", + exports: { docx: "https://example.test/result.docx" }, + markdown: page.markdown, + raw: page, + }, + ], + }); + }); + + test("malformed fetched document result records use ResultParseError", async () => { + const client = createClient( + captureFetch([ + jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job.jsonl" } } }), + textResponse(JSON.stringify({ notResult: {} })), + ]).fetch, + ); + + await expect(client.waitDocumentParsingResult("job-1")).rejects.toThrow(ResultParseError); + }); + + test.each([ + ["missing layoutParsingResults", { result: {} }], + ["missing markdown", { result: { layoutParsingResults: [{}] } }], + ["missing markdown.text", { result: { layoutParsingResults: [{ markdown: { images: {} } }] } }], + ])("malformed document payload with %s uses ResultParseError", async (_name, payload) => { + const client = createClient( + captureFetch([ + jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job.jsonl" } } }), + textResponse(JSON.stringify(payload)), + ]).fetch, + ); + + await expect(client.waitDocumentParsingResult("job-1")).rejects.toThrow(ResultParseError); + }); + + test("HTTP status and timeout failures map to contract errors", async () => { + await expect(createClient(captureFetch([textResponse("bad auth", 401)]).fetch).getStatus("job-1")).rejects.toThrow( + AuthError, + ); + await expect(createClient(captureFetch([textResponse("bad request", 400)]).fetch).getStatus("job-1")).rejects.toThrow( + InvalidRequestError, + ); + await expect(createClient(captureFetch([textResponse("server error", 500)]).fetch).getStatus("job-1")).rejects.toThrow( + APIError, + ); + + const timeoutFetch = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError"))); + }), + ) as unknown as typeof fetch; + await expect(createClient(timeoutFetch, { requestTimeout: 1 }).getStatus("job-1")).rejects.toThrow( + RequestTimeoutError, + ); + }); + + test("submitFile reports missing local files before network calls", async () => { + const { fetch } = captureFetch([]); + const client = createClient(fetch); + + await expect(client.submitOcr({ filePath: "/definitely/missing/file.pdf" })).rejects.toThrow(FileNotFoundError); + expect(fetch).not.toHaveBeenCalled(); + }); + + test("saveResource writes URL downloads without auth and requires overwrite opt-in", async () => { + const dir = await mkdtemp(join(tmpdir(), "paddleocr-api-sdk-")); + try { + const { fetch, calls } = captureFetch([textResponse("content"), textResponse("replacement")]); + const client = createClient(fetch); + + const saved = await client.saveResource("https://storage.example.test/a/b/c.png", dir); + expect(saved).toBe(join(dir, "c.png")); + await expect(readFile(join(dir, "c.png"), "utf8")).resolves.toBe("content"); + expect((calls[0].init.headers as Record).Authorization).toBeUndefined(); + + await expect(client.saveResource("https://storage.example.test/a/b/c.png", dir)).rejects.toThrow( + InvalidRequestError, + ); + + await client.saveResource("https://storage.example.test/a/b/c.png", dir, { overwrite: true }); + await expect(readFile(join(dir, "c.png"), "utf8")).resolves.toBe("replacement"); + await expect(stat(join(dir, "c.png"))).resolves.toBeTruthy(); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + test("saveOcrResultResources saves OCR result image URLs with stable page filenames", async () => { + const dir = await mkdtemp(join(tmpdir(), "paddleocr-api-sdk-")); + try { + const { fetch, calls } = captureFetch([textResponse("page 1"), textResponse("page 2")]); + const client = createClient(fetch); + const result: OCRResult = { + jobId: "job-ocr", + pages: [ + { prunedResult: { text: "one" }, ocrImageUrl: "https://storage.example.test/results/page-a.png?sig=opaque" }, + { prunedResult: { text: "two" }, ocrImageUrl: "https://storage.example.test/results/page-b.jpg" }, + ], + }; + + const saved = await client.saveOcrResultResources(result, dir); + + expect(saved).toEqual([join(dir, "ocr-page-1.png"), join(dir, "ocr-page-2.jpg")]); + await expect(readFile(join(dir, "ocr-page-1.png"), "utf8")).resolves.toBe("page 1"); + await expect(readFile(join(dir, "ocr-page-2.jpg"), "utf8")).resolves.toBe("page 2"); + expect(calls).toHaveLength(2); + expect(calls.map((call) => (call.init.headers as Record).Authorization)).toEqual([ + undefined, + undefined, + ]); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + test("saveDocumentParsingResultResources preserves safe document parsing map keys over opaque URL basenames", async () => { + const dir = await mkdtemp(join(tmpdir(), "paddleocr-api-sdk-")); + try { + const { fetch } = captureFetch([textResponse("markdown image"), textResponse("output image")]); + const client = createClient(fetch); + const result: DocParsingResult = { + jobId: "job-doc", + pages: [ + { + markdownText: "![figure](figure 1.png)", + markdownImages: { + "figure 1.png": "https://storage.example.test/download?id=markdown-image", + }, + outputImages: { + "rendered-page.jpg": "https://storage.example.test/blob?id=output-image", + }, + }, + ], + }; + + const saved = await client.saveDocumentParsingResultResources(result, dir); + + expect(saved).toEqual([join(dir, "figure 1.png"), join(dir, "rendered-page.jpg")]); + await expect(readFile(join(dir, "figure 1.png"), "utf8")).resolves.toBe("markdown image"); + await expect(readFile(join(dir, "rendered-page.jpg"), "utf8")).resolves.toBe("output image"); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + test("saveDocumentParsingResultResources sorts document parsing map keys for deterministic output", async () => { + const dir = await mkdtemp(join(tmpdir(), "paddleocr-api-sdk-")); + try { + const { fetch, calls } = captureFetch([textResponse("alpha"), textResponse("bravo"), textResponse("charlie")]); + const client = createClient(fetch); + const result: DocParsingResult = { + jobId: "job-doc", + pages: [ + { + markdownText: "", + markdownImages: { + "charlie.png": "https://storage.example.test/charlie", + "alpha.png": "https://storage.example.test/alpha", + "bravo.png": "https://storage.example.test/bravo", + }, + outputImages: {}, + }, + ], + }; + + const saved = await client.saveDocumentParsingResultResources(result, dir); + + expect(saved).toEqual([join(dir, "alpha.png"), join(dir, "bravo.png"), join(dir, "charlie.png")]); + expect(calls.map((call) => call.url)).toEqual([ + "https://storage.example.test/alpha", + "https://storage.example.test/bravo", + "https://storage.example.test/charlie", + ]); + await expect(readFile(join(dir, "alpha.png"), "utf8")).resolves.toBe("alpha"); + await expect(readFile(join(dir, "bravo.png"), "utf8")).resolves.toBe("bravo"); + await expect(readFile(join(dir, "charlie.png"), "utf8")).resolves.toBe("charlie"); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + test("saveOcrResultResources requires overwrite opt-in for bulk result files", async () => { + const dir = await mkdtemp(join(tmpdir(), "paddleocr-api-sdk-")); + try { + const result: OCRResult = { + jobId: "job-ocr", + pages: [{ prunedResult: {}, ocrImageUrl: "https://storage.example.test/page.png" }], + }; + + await writeFile(join(dir, "ocr-page-1.png"), "existing"); + const blocked = captureFetch([textResponse("unexpected")]); + await expect(createClient(blocked.fetch).saveOcrResultResources(result, dir)).rejects.toThrow(InvalidRequestError); + expect(blocked.calls).toHaveLength(0); + + const replacement = captureFetch([textResponse("replacement")]); + await createClient(replacement.fetch).saveOcrResultResources(result, dir, { overwrite: true }); + await expect(readFile(join(dir, "ocr-page-1.png"), "utf8")).resolves.toBe("replacement"); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + test.each([ + ["empty", ""], + ["absolute", "/absolute.png"], + ["parent traversal", "../escape.png"], + ["traversal segment", ".."], + ["forward separator", "nested/escape.png"], + ["backslash separator", "nested\\escape.png"], + ])("saveDocumentParsingResultResources rejects unsafe document parsing map key: %s", async (_name, unsafeKey) => { + const dir = await mkdtemp(join(tmpdir(), "paddleocr-api-sdk-")); + try { + const { fetch } = captureFetch([]); + const client = createClient(fetch); + const result: DocParsingResult = { + jobId: "job-doc", + pages: [ + { + markdownText: "", + markdownImages: { + [unsafeKey]: "https://storage.example.test/escape.png", + }, + outputImages: {}, + }, + ], + }; + + await expect(client.saveDocumentParsingResultResources(result, dir)).rejects.toThrow(InvalidRequestError); + expect(fetch).not.toHaveBeenCalled(); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/tsconfig.json b/ocr_webapp/PaddleOCR/api_sdk/typescript/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..a121c551a0a43b9310462e512c1cde3ea58cd0a3 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "types": ["node"], + "outDir": "./dist", + "declaration": true, + "strict": true, + "esModuleInterop": true, + "baseUrl": ".", + "paths": { + "@paddleocr/api-sdk": ["src/index.ts"] + }, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests", "examples"] +} diff --git a/ocr_webapp/PaddleOCR/api_sdk/typescript/tsup.config.ts b/ocr_webapp/PaddleOCR/api_sdk/typescript/tsup.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..dfec24f1f63b719d3662f6cd426c2b6facacabd6 --- /dev/null +++ b/ocr_webapp/PaddleOCR/api_sdk/typescript/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm", "cjs"], + dts: true, + clean: true, + splitting: false, +}); diff --git "a/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/PCB\345\255\227\347\254\246\350\257\206\345\210\253.md" "b/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/PCB\345\255\227\347\254\246\350\257\206\345\210\253.md" new file mode 100644 index 0000000000000000000000000000000000000000..4b4efe4730ff0bfb0c05ee1f3f8b95ed04b29c5e --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/PCB\345\255\227\347\254\246\350\257\206\345\210\253.md" @@ -0,0 +1,652 @@ +# 基于PP-OCRv3的PCB字符识别 + +- [1. 项目介绍](#1-项目介绍) +- [2. 安装说明](#2-安装说明) +- [3. 数据准备](#3-数据准备) +- [4. 文本检测](#4-文本检测) + - [4.1 预训练模型直接评估](#41-预训练模型直接评估) + - [4.2 预训练模型+验证集padding直接评估](#42-预训练模型验证集padding直接评估) + - [4.3 预训练模型+fine-tune](#43-预训练模型fine-tune) +- [5. 文本识别](#5-文本识别) + - [5.1 预训练模型直接评估](#51-预训练模型直接评估) + - [5.2 三种fine-tune方案](#52-三种fine-tune方案) +- [6. 模型导出](#6-模型导出) +- [7. 端对端评测](#7-端对端评测) +- [8. Jetson部署](#8-Jetson部署) +- [9. 总结](#9-总结) +- [更多资源](#更多资源) + +# 1. 项目介绍 + +印刷电路板(PCB)是电子产品中的核心器件,对于板件质量的测试与监控是生产中必不可少的环节。在一些场景中,通过PCB中信号灯颜色和文字组合可以定位PCB局部模块质量问题,PCB文字识别中存在如下难点: + +- 裁剪出的PCB图片宽高比例较小 +- 文字区域整体面积也较小 +- 包含垂直、水平多种方向文本 + +针对本场景,PaddleOCR基于全新的PP-OCRv3通过合成数据、微调以及其他场景适配方法完成小字符文本识别任务,满足企业上线要求。PCB检测、识别效果如 **图1** 所示: + +
+
图1 PCB检测识别效果
+ +注:欢迎在AIStudio领取免费算力体验线上实训,项目链接: [基于PP-OCRv3实现PCB字符识别](https://aistudio.baidu.com/aistudio/projectdetail/4008973) + +# 2. 安装说明 + + +下载PaddleOCR源码,安装依赖环境。 + + +```python +# 如仍需安装or安装更新,可以执行以下步骤 +git clone https://github.com/PaddlePaddle/PaddleOCR.git +# git clone https://gitee.com/PaddlePaddle/PaddleOCR +``` + + +```python +# 安装依赖包 +pip install -r /home/aistudio/PaddleOCR/requirements.txt +``` + +# 3. 数据准备 + +我们通过图片合成工具生成 **图2** 所示的PCB图片,整图只有高25、宽150左右、文字区域高9、宽45左右,包含垂直和水平2种方向的文本: + +
+
图2 数据集示例
+ +暂时不开源生成的PCB数据集,但是通过更换背景,通过如下代码生成数据即可: + +``` +cd gen_data +python3 gen.py --num_img=10 +``` + +生成图片参数解释: + +``` +num_img:生成图片数量 +font_min_size、font_max_size:字体最大、最小尺寸 +bg_path:文字区域背景存放路径 +det_bg_path:整图背景存放路径 +fonts_path:字体路径 +corpus_path:语料路径 +output_dir:生成图片存储路径 +``` + +这里生成 **100张** 相同尺寸和文本的图片,如 **图3** 所示,方便大家跑通实验。通过如下代码解压数据集: + +
+
图3 案例提供数据集示例
+ + +```python +tar xf ./data/data148165/dataset.tar -C ./ +``` + +在生成数据集的时需要生成检测和识别训练需求的格式: + + +- **文本检测** + +标注文件格式如下,中间用'\t'分隔: + +``` +" 图像文件名 json.dumps编码的图像标注信息" +ch4_test_images/img_61.jpg [{"transcription": "MASA", "points": [[310, 104], [416, 141], [418, 216], [312, 179]]}, {...}] +``` + +json.dumps编码前的图像标注信息是包含多个字典的list,字典中的 `points` 表示文本框的四个点的坐标(x, y),从左上角的点开始顺时针排列。 `transcription` 表示当前文本框的文字,***当其内容为“###”时,表示该文本框无效,在训练时会跳过。*** + +- **文本识别** + +标注文件的格式如下, txt文件中默认请将图片路径和图片标签用'\t'分割,如用其他方式分割将造成训练报错。 + +``` +" 图像文件名 图像标注信息 " + +train_data/rec/train/word_001.jpg 简单可依赖 +train_data/rec/train/word_002.jpg 用科技让复杂的世界更简单 +... +``` + + +# 4. 文本检测 + +选用飞桨OCR开发套件[PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR)中的PP-OCRv3模型进行文本检测和识别。针对检测模型和识别模型,进行了共计9个方面的升级: + +- PP-OCRv3检测模型对PP-OCRv2中的CML协同互学习文本检测蒸馏策略进行了升级,分别针对教师模型和学生模型进行进一步效果优化。其中,在对教师模型优化时,提出了大感受野的PAN结构LK-PAN和引入了DML蒸馏策略;在对学生模型优化时,提出了残差注意力机制的FPN结构RSE-FPN。 + +- PP-OCRv3的识别模块是基于文本识别算法SVTR优化。SVTR不再采用RNN结构,通过引入Transformers结构更加有效地挖掘文本行图像的上下文信息,从而提升文本识别能力。PP-OCRv3通过轻量级文本识别网络SVTR_LCNet、Attention损失指导CTC损失训练策略、挖掘文字上下文信息的数据增广策略TextConAug、TextRotNet自监督预训练模型、UDML联合互学习策略、UIM无标注数据挖掘方案,6个方面进行模型加速和效果提升。 + +更多细节请参考PP-OCRv3[技术报告](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/PP-OCRv3_introduction.md)。 + + +我们使用 **3种方案** 进行检测模型的训练、评估: +- **PP-OCRv3英文超轻量检测预训练模型直接评估** +- PP-OCRv3英文超轻量检测预训练模型 + **验证集padding**直接评估 +- PP-OCRv3英文超轻量检测预训练模型 + **fine-tune** + +## **4.1 预训练模型直接评估** + +我们首先通过PaddleOCR提供的预训练模型在验证集上进行评估,如果评估指标能满足效果,可以直接使用预训练模型,不再需要训练。 + +使用预训练模型直接评估步骤如下: + +**1)下载预训练模型** + + +PaddleOCR已经提供了PP-OCR系列模型,部分模型展示如下表所示: + +| 模型简介 | 模型名称 | 推荐场景 | 检测模型 | 方向分类器 | 识别模型 | +| ------------------------------------- | ----------------------- | --------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 中英文超轻量PP-OCRv3模型(16.2M) | ch_PP-OCRv3_xx | 移动端&服务器端 | [推理模型](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar) / [训练模型](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_distill_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) / [训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_infer.tar) / [训练模型](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_train.tar) | +| 英文超轻量PP-OCRv3模型(13.4M) | en_PP-OCRv3_xx | 移动端&服务器端 | [推理模型](https://paddleocr.bj.bcebos.com/PP-OCRv3/english/en_PP-OCRv3_det_infer.tar) / [训练模型](https://paddleocr.bj.bcebos.com/PP-OCRv3/english/en_PP-OCRv3_det_distill_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) / [训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/PP-OCRv3/english/en_PP-OCRv3_rec_infer.tar) / [训练模型](https://paddleocr.bj.bcebos.com/PP-OCRv3/english/en_PP-OCRv3_rec_train.tar) | +| 中英文超轻量PP-OCRv2模型(13.0M) | ch_PP-OCRv2_xx | 移动端&服务器端 | [推理模型](https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_det_infer.tar) / [训练模型](https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_det_distill_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_rec_infer.tar) / [训练模型](https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_rec_train.tar) | +| 中英文超轻量PP-OCR mobile模型(9.4M) | ch_ppocr_mobile_v2.0_xx | 移动端&服务器端 | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_pre.tar) | +| 中英文通用PP-OCR server模型(143.4M) | ch_ppocr_server_v2.0_xx | 服务器端 | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_det_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_det_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_rec_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_rec_pre.tar) | + +更多模型下载(包括多语言),可以参[考PP-OCR系列模型下载](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/models_list.md) + +这里我们使用PP-OCRv3英文超轻量检测模型,下载并解压预训练模型: + + + + +```python +# 如果更换其他模型,更新下载链接和解压指令就可以 +cd /home/aistudio/PaddleOCR +mkdir pretrain_models +cd pretrain_models +# 下载英文预训练模型 +wget https://paddleocr.bj.bcebos.com/PP-OCRv3/english/en_PP-OCRv3_det_distill_train.tar +tar xf en_PP-OCRv3_det_distill_train.tar && rm -rf en_PP-OCRv3_det_distill_train.tar +%cd .. +``` + +**模型评估** + + +首先修改配置文件`configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml`中的以下字段: +``` +Eval.dataset.data_dir:指向验证集图片存放目录,'/home/aistudio/dataset' +Eval.dataset.label_file_list:指向验证集标注文件,'/home/aistudio/dataset/det_gt_val.txt' +Eval.dataset.transforms.DetResizeForTest: 尺寸 + limit_side_len: 48 + limit_type: 'min' +``` + +然后在验证集上进行评估,具体代码如下: + + + +```python +cd /home/aistudio/PaddleOCR +python tools/eval.py \ + -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml \ + -o Global.checkpoints="./pretrain_models/en_PP-OCRv3_det_distill_train/best_accuracy" +``` + +## **4.2 预训练模型+验证集padding直接评估** + +考虑到PCB图片比较小,宽度只有25左右、高度只有140-170左右,我们在原图的基础上进行padding,再进行检测评估,padding前后效果对比如 **图4** 所示: + +
+
图4 padding前后对比图
+ +将图片都padding到300*300大小,因为坐标信息发生了变化,我们同时要修改标注文件,在`/home/aistudio/dataset`目录里也提供了padding之后的图片,大家也可以尝试训练和评估: + +同上,我们需要修改配置文件`configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml`中的以下字段: +``` +Eval.dataset.data_dir:指向验证集图片存放目录,'/home/aistudio/dataset' +Eval.dataset.label_file_list:指向验证集标注文件,/home/aistudio/dataset/det_gt_padding_val.txt +Eval.dataset.transforms.DetResizeForTest: 尺寸 + limit_side_len: 1100 + limit_type: 'min' +``` + +如需获取已训练模型,请加入PaddleX官方交流频道,获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 + +将下载或训练完成的模型放置在对应目录下即可完成模型推理 + + +```python +cd /home/aistudio/PaddleOCR +python tools/eval.py \ + -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml \ + -o Global.checkpoints="./pretrain_models/en_PP-OCRv3_det_distill_train/best_accuracy" +``` + +## **4.3 预训练模型+fine-tune** + + +基于预训练模型,在生成的1500图片上进行fine-tune训练和评估,其中train数据1200张,val数据300张,修改配置文件`configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml`中的以下字段: +``` +Global.epoch_num: 这里设置为1,方便快速跑通,实际中根据数据量调整该值 +Global.save_model_dir:模型保存路径 +Global.pretrained_model:指向预训练模型路径,'./pretrain_models/en_PP-OCRv3_det_distill_train/student.pdparams' +Optimizer.lr.learning_rate:调整学习率,本实验设置为0.0005 +Train.dataset.data_dir:指向训练集图片存放目录,'/home/aistudio/dataset' +Train.dataset.label_file_list:指向训练集标注文件,'/home/aistudio/dataset/det_gt_train.txt' +Train.dataset.transforms.EastRandomCropData.size:训练尺寸改为[480,64] +Eval.dataset.data_dir:指向验证集图片存放目录,'/home/aistudio/dataset/' +Eval.dataset.label_file_list:指向验证集标注文件,'/home/aistudio/dataset/det_gt_val.txt' +Eval.dataset.transforms.DetResizeForTest:评估尺寸,添加如下参数 + limit_side_len: 64 + limit_type:'min' +``` +执行下面命令启动训练: + + +```python +cd /home/aistudio/PaddleOCR/ +python tools/train.py \ + -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml +``` + +**模型评估** + + +使用训练好的模型进行评估,更新模型路径`Global.checkpoints`: + + +```python +cd /home/aistudio/PaddleOCR/ +python3 tools/eval.py \ + -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml \ + -o Global.checkpoints="./output/ch_PP-OCR_V3_det/latest" +``` + +使用训练好的模型进行评估,指标如下所示: + + +| 序号 | 方案 | hmean | 效果提升 | 实验分析 | +| -------- | -------- | -------- | -------- | -------- | +| 1 | PP-OCRv3英文超轻量检测预训练模型 | 64.64% | - | 提供的预训练模型具有泛化能力 | +| 2 | PP-OCRv3英文超轻量检测预训练模型 + 验证集padding | 72.13% |+7.49% | padding可以提升尺寸较小图片的检测效果| +| 3 | PP-OCRv3英文超轻量检测预训练模型 + fine-tune | 100.00% | +27.87% | fine-tune会提升垂类场景效果 | + + +``` +注:上述实验结果均是在1500张图片(1200张训练集,300张测试集)上训练、评估的得到,AIstudio只提供了100张数据,所以指标有所差异属于正常,只要策略有效、规律相同即可。 +``` + +# 5. 文本识别 + +我们分别使用如下4种方案进行训练、评估: + +- **方案1**:**PP-OCRv3中英文超轻量识别预训练模型直接评估** +- **方案2**:PP-OCRv3中英文超轻量检测预训练模型 + **fine-tune** +- **方案3**:PP-OCRv3中英文超轻量检测预训练模型 + fine-tune + **公开通用识别数据集** +- **方案4**:PP-OCRv3中英文超轻量检测预训练模型 + fine-tune + **增加PCB图像数量** + + +## **5.1 预训练模型直接评估** + +同检测模型,我们首先使用PaddleOCR提供的识别预训练模型在PCB验证集上进行评估。 + +使用预训练模型直接评估步骤如下: + +**1)下载预训练模型** + + +我们使用PP-OCRv3中英文超轻量文本识别模型,下载并解压预训练模型: + + +```python +# 如果更换其他模型,更新下载链接和解压指令就可以 +cd /home/aistudio/PaddleOCR/pretrain_models/ +wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_train.tar +tar xf ch_PP-OCRv3_rec_train.tar && rm -rf ch_PP-OCRv3_rec_train.tar +cd .. +``` + +**模型评估** + + +首先修改配置文件`configs/det/ch_PP-OCRv3/ch_PP-OCRv2_rec_distillation.yml`中的以下字段: + +``` +Metric.ignore_space: True:忽略空格 +Eval.dataset.data_dir:指向验证集图片存放目录,'/home/aistudio/dataset' +Eval.dataset.label_file_list:指向验证集标注文件,'/home/aistudio/dataset/rec_gt_val.txt' +``` + +我们使用下载的预训练模型进行评估: + + +```python +cd /home/aistudio/PaddleOCR +python3 tools/eval.py \ + -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml \ + -o Global.checkpoints=pretrain_models/ch_PP-OCRv3_rec_train/best_accuracy + +``` + +## **5.2 三种fine-tune方案** + +方案2、3、4训练和评估方式是相同的,因此在我们了解每个技术方案之后,再具体看修改哪些参数是相同,哪些是不同的。 + +**方案介绍:** + +1) **方案2**:预训练模型 + **fine-tune** + +- 在预训练模型的基础上进行fine-tune,使用1500张PCB进行训练和评估,其中训练集1200张,验证集300张。 + + +2) **方案3**:预训练模型 + fine-tune + **公开通用识别数据集** + +- 当识别数据比较少的情况,可以考虑添加公开通用识别数据集。在方案2的基础上,添加公开通用识别数据集,如lsvt、rctw等。 + +3)**方案4**:预训练模型 + fine-tune + **增加PCB图像数量** + +- 如果能够获取足够多真实场景,我们可以通过增加数据量提升模型效果。在方案2的基础上,增加PCB的数量到2W张左右。 + + +**参数修改:** + +接着我们看需要修改的参数,以上方案均需要修改配置文件`configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml`的参数,**修改一次即可**: + +``` +Global.pretrained_model:指向预训练模型路径,'pretrain_models/ch_PP-OCRv3_rec_train/best_accuracy' +Optimizer.lr.values:学习率,本实验设置为0.0005 +Train.loader.batch_size_per_card: batch size,默认128,因为数据量小于128,因此我们设置为8,数据量大可以按默认的训练 +Eval.loader.batch_size_per_card: batch size,默认128,设置为4 +Metric.ignore_space: 忽略空格,本实验设置为True +``` + +**更换不同的方案**每次需要修改的参数: +``` +Global.epoch_num: 这里设置为1,方便快速跑通,实际中根据数据量调整该值 +Global.save_model_dir:指向模型保存路径 +Train.dataset.data_dir:指向训练集图片存放目录 +Train.dataset.label_file_list:指向训练集标注文件 +Eval.dataset.data_dir:指向验证集图片存放目录 +Eval.dataset.label_file_list:指向验证集标注文件 +``` + +同时**方案3**修改以下参数 +``` +Eval.dataset.label_file_list:添加公开通用识别数据标注文件 +Eval.dataset.ratio_list:数据和公开通用识别数据每次采样比例,按实际修改即可 +``` +如 **图5** 所示: +
+
图5 添加公开通用识别数据配置文件示例
+ + +我们提取Student模型的参数,在PCB数据集上进行fine-tune,可以参考如下代码: + + +```python +import paddle +# 加载预训练模型 +all_params = paddle.load("./pretrain_models/ch_PP-OCRv3_rec_train/best_accuracy.pdparams") +# 查看权重参数的keys +print(all_params.keys()) +# 学生模型的权重提取 +s_params = {key[len("student_model."):]: all_params[key] for key in all_params if "student_model." in key} +# 查看学生模型权重参数的keys +print(s_params.keys()) +# 保存 +paddle.save(s_params, "./pretrain_models/ch_PP-OCRv3_rec_train/student.pdparams") +``` + +修改参数后,**每个方案**都执行如下命令启动训练: + + + +```python +cd /home/aistudio/PaddleOCR/ +python3 tools/train.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml +``` + + +使用训练好的模型进行评估,更新模型路径`Global.checkpoints`: + + +```python +cd /home/aistudio/PaddleOCR/ +python3 tools/eval.py \ + -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml \ + -o Global.checkpoints=./output/rec_ppocr_v3/latest +``` + +所有方案评估指标如下: + +| 序号 | 方案 | acc | 效果提升 | 实验分析 | +| -------- | -------- | -------- | -------- | -------- | +| 1 | PP-OCRv3中英文超轻量识别预训练模型直接评估 | 46.67% | - | 提供的预训练模型具有泛化能力 | +| 2 | PP-OCRv3中英文超轻量识别预训练模型 + fine-tune | 42.02% |-4.65% | 在数据量不足的情况,反而比预训练模型效果低(也可以通过调整超参数再试试)| +| 3 | PP-OCRv3中英文超轻量识别预训练模型 + fine-tune + 公开通用识别数据集 | 77.00% | +30.33% | 在数据量不足的情况下,可以考虑补充公开数据训练 | +| 4 | PP-OCRv3中英文超轻量识别预训练模型 + fine-tune + 增加PCB图像数量 | 99.99% | +22.99% | 如果能获取更多数据量的情况,可以通过增加数据量提升效果 | + +``` +注:上述实验结果均是在1500张图片(1200张训练集,300张测试集)、2W张图片、添加公开通用识别数据集上训练、评估的得到,AIstudio只提供了100张数据,所以指标有所差异属于正常,只要策略有效、规律相同即可。 +``` + +# 6. 模型导出 + +inference 模型(paddle.jit.save保存的模型) 一般是模型训练,把模型结构和模型参数保存在文件中的固化模型,多用于预测部署场景。 训练过程中保存的模型是checkpoints模型,保存的只有模型的参数,多用于恢复训练等。 与checkpoints模型相比,inference 模型会额外保存模型的结构信息,在预测部署、加速推理上性能优越,灵活方便,适合于实际系统集成。 + + +```python +# 导出检测模型 +python3 tools/export_model.py \ + -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml \ + -o Global.pretrained_model="./output/ch_PP-OCR_V3_det/latest" \ + Global.save_inference_dir="./inference_model/ch_PP-OCR_V3_det/" +``` + +因为上述模型只训练了1个epoch,因此我们使用训练最优的模型进行预测,存储在`/home/aistudio/best_models/`目录下,解压即可 + + +```python +cd /home/aistudio/best_models/ +wget https://paddleocr.bj.bcebos.com/fanliku/PCB/det_ppocr_v3_en_infer_PCB.tar +tar xf /home/aistudio/best_models/det_ppocr_v3_en_infer_PCB.tar -C /home/aistudio/PaddleOCR/pretrain_models/ +``` + + +```python +# 检测模型inference模型预测 +cd /home/aistudio/PaddleOCR/ +python3 tools/infer/predict_det.py \ + --image_dir="/home/aistudio/dataset/imgs/0000.jpg" \ + --det_algorithm="DB" \ + --det_model_dir="./pretrain_models/det_ppocr_v3_en_infer_PCB/" \ + --det_limit_side_len=48 \ + --det_limit_type='min' \ + --det_db_unclip_ratio=2.5 \ + --use_gpu=True +``` + +结果存储在`inference_results`目录下,检测如下图所示: +
+
图6 检测结果
+ + +同理,导出识别模型并进行推理。 + +```python +# 导出识别模型 +python3 tools/export_model.py \ + -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml \ + -o Global.pretrained_model="./output/rec_ppocr_v3/latest" \ + Global.save_inference_dir="./inference_model/rec_ppocr_v3/" + +``` + +同检测模型,识别模型也只训练了1个epoch,因此我们使用训练最优的模型进行预测,存储在`/home/aistudio/best_models/`目录下,解压即可 + + +```python +cd /home/aistudio/best_models/ +wget https://paddleocr.bj.bcebos.com/fanliku/PCB/rec_ppocr_v3_ch_infer_PCB.tar +tar xf /home/aistudio/best_models/rec_ppocr_v3_ch_infer_PCB.tar -C /home/aistudio/PaddleOCR/pretrain_models/ +``` + + +```python +# 识别模型inference模型预测 +cd /home/aistudio/PaddleOCR/ +python3 tools/infer/predict_rec.py \ + --image_dir="../test_imgs/0000_rec.jpg" \ + --rec_model_dir="./pretrain_models/rec_ppocr_v3_ch_infer_PCB" \ + --rec_image_shape="3, 48, 320" \ + --use_space_char=False \ + --use_gpu=True +``` + +```python +# 检测+识别模型inference模型预测 +cd /home/aistudio/PaddleOCR/ +python3 tools/infer/predict_system.py \ + --image_dir="../test_imgs/0000.jpg" \ + --det_model_dir="./pretrain_models/det_ppocr_v3_en_infer_PCB" \ + --det_limit_side_len=48 \ + --det_limit_type='min' \ + --det_db_unclip_ratio=2.5 \ + --rec_model_dir="./pretrain_models/rec_ppocr_v3_ch_infer_PCB" \ + --rec_image_shape="3, 48, 320" \ + --draw_img_save_dir=./det_rec_infer/ \ + --use_space_char=False \ + --use_angle_cls=False \ + --use_gpu=True + +``` + +端到端预测结果存储在`det_res_infer`文件夹内,结果如下图所示: +
+
图7 检测+识别结果
+ +# 7. 端对端评测 + +接下来介绍文本检测+文本识别的端对端指标评估方式。主要分为三步: + +1)首先运行`tools/infer/predict_system.py`,将`image_dir`改为需要评估的数据文件家,得到保存的结果: + + +```python +# 检测+识别模型inference模型预测 +python3 tools/infer/predict_system.py \ + --image_dir="../dataset/imgs/" \ + --det_model_dir="./pretrain_models/det_ppocr_v3_en_infer_PCB" \ + --det_limit_side_len=48 \ + --det_limit_type='min' \ + --det_db_unclip_ratio=2.5 \ + --rec_model_dir="./pretrain_models/rec_ppocr_v3_ch_infer_PCB" \ + --rec_image_shape="3, 48, 320" \ + --draw_img_save_dir=./det_rec_infer/ \ + --use_space_char=False \ + --use_angle_cls=False \ + --use_gpu=True +``` + +得到保存结果,文本检测识别可视化图保存在`det_rec_infer/`目录下,预测结果保存在`det_rec_infer/system_results.txt`中,格式如下:`0018.jpg [{"transcription": "E295", "points": [[88, 33], [137, 33], [137, 40], [88, 40]]}]` + +2)然后将步骤一保存的数据转换为端对端评测需要的数据格式: 修改 `tools/end2end/convert_ppocr_label.py`中的代码,convert_label函数中设置输入标签路径,Mode,保存标签路径等,对预测数据的GTlabel和预测结果的label格式进行转换。 +``` +ppocr_label_gt = "/home/aistudio/dataset/det_gt_val.txt" +convert_label(ppocr_label_gt, "gt", "./save_gt_label/") + +ppocr_label_gt = "/home/aistudio/PaddleOCR/PCB_result/det_rec_infer/system_results.txt" +convert_label(ppocr_label_gt, "pred", "./save_PPOCRV2_infer/") +``` + +运行`convert_ppocr_label.py`: + + +```python + python3 tools/end2end/convert_ppocr_label.py +``` + +得到如下结果: +``` +├── ./save_gt_label/ +├── ./save_PPOCRV2_infer/ +``` + +3) 最后,执行端对端评测,运行`tools/end2end/eval_end2end.py`计算端对端指标,运行方式如下: + + +```python +pip install editdistance +python3 tools/end2end/eval_end2end.py ./save_gt_label/ ./save_PPOCRV2_infer/ +``` + +使用`预训练模型+fine-tune'检测模型`、`预训练模型 + 2W张PCB图片funetune`识别模型,在300张PCB图片上评估得到如下结果,fmeasure为主要关注的指标: +
+
图8 端到端评估指标
+ +``` +注: 使用上述命令不能跑出该结果,因为数据集不相同,可以更换为自己训练好的模型,按上述流程运行 +``` + +# 8. Jetson部署 + +我们只需要以下步骤就可以完成Jetson nano部署模型,简单易操作: + +**1、在Jetson nano开发版上环境准备:** + +* 安装PaddlePaddle + +* 下载PaddleOCR并安装依赖 + +**2、执行预测** + +* 将推理模型下载到jetson + +* 执行检测、识别、串联预测即可 + +详细[参考流程](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/deploy/Jetson/readme_ch.md)。 + +# 9. 总结 + +检测实验分别使用PP-OCRv3预训练模型在PCB数据集上进行了直接评估、验证集padding、 fine-tune 3种方案,识别实验分别使用PP-OCRv3预训练模型在PCB数据集上进行了直接评估、 fine-tune、添加公开通用识别数据集、增加PCB图片数量4种方案,指标对比如下: + +* 检测 + + +| 序号 | 方案 | hmean | 效果提升 | 实验分析 | +| ---- | -------------------------------------------------------- | ------ | -------- | ------------------------------------- | +| 1 | PP-OCRv3英文超轻量检测预训练模型直接评估 | 64.64% | - | 提供的预训练模型具有泛化能力 | +| 2 | PP-OCRv3英文超轻量检测预训练模型 + 验证集padding直接评估 | 72.13% | +7.49% | padding可以提升尺寸较小图片的检测效果 | +| 3 | PP-OCRv3英文超轻量检测预训练模型 + fine-tune | 100.00% | +27.87% | fine-tune会提升垂类场景效果 | + +* 识别 + +| 序号 | 方案 | acc | 效果提升 | 实验分析 | +| ---- | ------------------------------------------------------------ | ------ | -------- | ------------------------------------------------------------ | +| 1 | PP-OCRv3中英文超轻量识别预训练模型直接评估 | 46.67% | - | 提供的预训练模型具有泛化能力 | +| 2 | PP-OCRv3中英文超轻量识别预训练模型 + fine-tune | 42.02% | -4.65% | 在数据量不足的情况,反而比预训练模型效果低(也可以通过调整超参数再试试) | +| 3 | PP-OCRv3中英文超轻量识别预训练模型 + fine-tune + 公开通用识别数据集 | 77.00% | +30.33% | 在数据量不足的情况下,可以考虑补充公开数据训练 | +| 4 | PP-OCRv3中英文超轻量识别预训练模型 + fine-tune + 增加PCB图像数量 | 99.99% | +22.99% | 如果能获取更多数据量的情况,可以通过增加数据量提升效果 | + +* 端到端 + +| det | rec | fmeasure | +| --------------------------------------------- | ------------------------------------------------------------ | -------- | +| PP-OCRv3英文超轻量检测预训练模型 + fine-tune | PP-OCRv3中英文超轻量识别预训练模型 + fine-tune + 增加PCB图像数量 | 93.30% | + +*结论* + +PP-OCRv3的检测模型在未经过fine-tune的情况下,在PCB数据集上也有64.64%的精度,说明具有泛化能力。验证集padding之后,精度提升7.5%,在图片尺寸较小的情况,我们可以通过padding的方式提升检测效果。经过 fine-tune 后能够极大的提升检测效果,精度达到100%。 + +PP-OCRv3的识别模型方案1和方案2对比可以发现,当数据量不足的情况,预训练模型精度可能比fine-tune效果还要高,所以我们可以先尝试预训练模型直接评估。如果在数据量不足的情况下想进一步提升模型效果,可以通过添加公开通用识别数据集,识别效果提升30%,非常有效。最后如果我们能够采集足够多的真实场景数据集,可以通过增加数据量提升模型效果,精度达到99.99%。 + +# 更多资源 + +- 更多深度学习知识、产业案例、面试宝典等,请参考:[awesome-DeepLearning](https://github.com/paddlepaddle/awesome-DeepLearning) + +- 更多PaddleOCR使用教程,请参考:[PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR/tree/dygraph) + + +- 飞桨框架相关资料,请参考:[飞桨深度学习平台](https://www.paddlepaddle.org.cn/?fr=paddleEdu_aistudio) + +# 参考 + +* 数据生成代码库:https://github.com/zcswdt/Color_OCR_image_generator diff --git "a/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/background/bg.jpg" "b/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/background/bg.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..3cb6eab819c3b7d4f68590d2cdc9d36351590197 Binary files /dev/null and "b/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/background/bg.jpg" differ diff --git "a/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/corpus/text.txt" "b/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/corpus/text.txt" new file mode 100644 index 0000000000000000000000000000000000000000..8b8cb793ef6755bf00ed8c154e24638b07a519c2 --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/corpus/text.txt" @@ -0,0 +1,30 @@ +5ZQ +I4UL +PWL +SNOG +ZL02 +1C30 +O3H +YHRS +N03S +1U5Y +JTK +EN4F +YKJ +DWNH +R42W +X0V +4OF5 +08AM +Y93S +GWE2 +0KR +9U2A +DBQ +Y6J +ROZ +K06 +KIEY +NZQJ +UN1B +6X4 \ No newline at end of file diff --git "a/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/det_background/1.png" "b/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/det_background/1.png" new file mode 100644 index 0000000000000000000000000000000000000000..8a49eaa6862113044e05d17e32941a0a20911426 Binary files /dev/null and "b/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/det_background/1.png" differ diff --git "a/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/det_background/2.png" "b/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/det_background/2.png" new file mode 100644 index 0000000000000000000000000000000000000000..c3fcc0c92826b97b5f6abd970f1a0580eede0f5d Binary files /dev/null and "b/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/det_background/2.png" differ diff --git "a/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/gen.py" "b/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/gen.py" new file mode 100644 index 0000000000000000000000000000000000000000..0eb00cd1efc218ff72de7e1a7747ebd1154caff0 --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/PCB\345\255\227\347\254\246\350\257\206\345\210\253/gen_data/gen.py" @@ -0,0 +1,270 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# 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. +""" +This code is refer from: +https://github.com/zcswdt/Color_OCR_image_generator +""" +import os +import random +import PIL +from PIL import Image, ImageDraw, ImageFont +import json +import argparse + + +def get_char_lines(txt_root_path): + """ + desc:get corpus line + """ + txt_files = os.listdir(txt_root_path) + char_lines = [] + for txt in txt_files: + f = open(os.path.join(txt_root_path, txt), mode='r', encoding='utf-8') + lines = f.readlines() + f.close() + for line in lines: + char_lines.append(line.strip()) + return char_lines + + +def get_horizontal_text_picture(image_file, chars, fonts_list, cf): + """ + desc:gen horizontal text picture + """ + img = Image.open(image_file) + if img.mode != 'RGB': + img = img.convert('RGB') + img_w, img_h = img.size + + # random choice font + font_path = random.choice(fonts_list) + # random choice font size + font_size = random.randint(cf.font_min_size, cf.font_max_size) + font = ImageFont.truetype(font_path, font_size) + + ch_w = [] + ch_h = [] + for ch in chars: + if int(PIL.__version__.split('.')[0]) < 10: + wt, ht = font.getsize(ch) + else: + left, top, right, bottom = font.getbbox(ch) + wt, ht = right - left, bottom - top + ch_w.append(wt) + ch_h.append(ht) + f_w = sum(ch_w) + f_h = max(ch_h) + + # add space + char_space_width = max(ch_w) + f_w += (char_space_width * (len(chars) - 1)) + + x1 = random.randint(0, img_w - f_w) + y1 = random.randint(0, img_h - f_h) + x2 = x1 + f_w + y2 = y1 + f_h + + crop_y1 = y1 + crop_x1 = x1 + crop_y2 = y2 + crop_x2 = x2 + + best_color = (0, 0, 0) + draw = ImageDraw.Draw(img) + for i, ch in enumerate(chars): + draw.text((x1, y1), ch, best_color, font=font) + x1 += (ch_w[i] + char_space_width) + crop_img = img.crop((crop_x1, crop_y1, crop_x2, crop_y2)) + return crop_img, chars + + +def get_vertical_text_picture(image_file, chars, fonts_list, cf): + """ + desc:gen vertical text picture + """ + img = Image.open(image_file) + if img.mode != 'RGB': + img = img.convert('RGB') + img_w, img_h = img.size + # random choice font + font_path = random.choice(fonts_list) + # random choice font size + font_size = random.randint(cf.font_min_size, cf.font_max_size) + font = ImageFont.truetype(font_path, font_size) + + ch_w = [] + ch_h = [] + for ch in chars: + if int(PIL.__version__.split('.')[0]) < 10: + wt, ht = font.getsize(ch) + else: + left, top, right, bottom = font.getbbox(ch) + wt, ht = right - left, bottom - top + ch_w.append(wt) + ch_h.append(ht) + f_w = max(ch_w) + f_h = sum(ch_h) + + x1 = random.randint(0, img_w - f_w) + y1 = random.randint(0, img_h - f_h) + x2 = x1 + f_w + y2 = y1 + f_h + + crop_y1 = y1 + crop_x1 = x1 + crop_y2 = y2 + crop_x2 = x2 + + best_color = (0, 0, 0) + draw = ImageDraw.Draw(img) + i = 0 + for ch in chars: + draw.text((x1, y1), ch, best_color, font=font) + y1 = y1 + ch_h[i] + i = i + 1 + crop_img = img.crop((crop_x1, crop_y1, crop_x2, crop_y2)) + crop_img = crop_img.transpose(Image.ROTATE_90) + return crop_img, chars + + +def get_fonts(fonts_path): + """ + desc: get all fonts + """ + font_files = os.listdir(fonts_path) + fonts_list=[] + for font_file in font_files: + font_path=os.path.join(fonts_path, font_file) + fonts_list.append(font_path) + return fonts_list + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--num_img', type=int, default=30, help="Number of images to generate") + parser.add_argument('--font_min_size', type=int, default=11) + parser.add_argument('--font_max_size', type=int, default=12, + help="Help adjust the size of the generated text and the size of the picture") + parser.add_argument('--bg_path', type=str, default='./background', + help='The generated text pictures will be pasted onto the pictures of this folder') + parser.add_argument('--det_bg_path', type=str, default='./det_background', + help='The generated text pictures will use the pictures of this folder as the background') + parser.add_argument('--fonts_path', type=str, default='../../StyleText/fonts', + help='The font used to generate the picture') + parser.add_argument('--corpus_path', type=str, default='./corpus', + help='The corpus used to generate the text picture') + parser.add_argument('--output_dir', type=str, default='./output/', help='Images save dir') + + + cf = parser.parse_args() + # save path + if not os.path.exists(cf.output_dir): + os.mkdir(cf.output_dir) + + # get corpus + txt_root_path = cf.corpus_path + char_lines = get_char_lines(txt_root_path=txt_root_path) + + # get all fonts + fonts_path = cf.fonts_path + fonts_list = get_fonts(fonts_path) + + # rec bg + img_root_path = cf.bg_path + imnames=os.listdir(img_root_path) + + # det bg + det_bg_path = cf.det_bg_path + bg_pics = os.listdir(det_bg_path) + + # OCR det files + det_val_file = open(cf.output_dir + 'det_gt_val.txt', 'w', encoding='utf-8') + det_train_file = open(cf.output_dir + 'det_gt_train.txt', 'w', encoding='utf-8') + # det imgs + det_save_dir = 'imgs/' + if not os.path.exists(cf.output_dir + det_save_dir): + os.mkdir(cf.output_dir + det_save_dir) + det_val_save_dir = 'imgs_val/' + if not os.path.exists(cf.output_dir + det_val_save_dir): + os.mkdir(cf.output_dir + det_val_save_dir) + + # OCR rec files + rec_val_file = open(cf.output_dir + 'rec_gt_val.txt', 'w', encoding='utf-8') + rec_train_file = open(cf.output_dir + 'rec_gt_train.txt', 'w', encoding='utf-8') + # rec imgs + rec_save_dir = 'rec_imgs/' + if not os.path.exists(cf.output_dir + rec_save_dir): + os.mkdir(cf.output_dir + rec_save_dir) + rec_val_save_dir = 'rec_imgs_val/' + if not os.path.exists(cf.output_dir + rec_val_save_dir): + os.mkdir(cf.output_dir + rec_val_save_dir) + + + val_ratio = cf.num_img * 0.2 # val dataset ratio + + print('start generating...') + for i in range(0, cf.num_img): + imname = random.choice(imnames) + img_path = os.path.join(img_root_path, imname) + + rnd = random.random() + # gen horizontal text picture + if rnd < 0.5: + gen_img, chars = get_horizontal_text_picture(img_path, char_lines[i], fonts_list, cf) + ori_w, ori_h = gen_img.size + gen_img = gen_img.crop((0, 3, ori_w, ori_h)) + # gen vertical text picture + else: + gen_img, chars = get_vertical_text_picture(img_path, char_lines[i], fonts_list, cf) + ori_w, ori_h = gen_img.size + gen_img = gen_img.crop((3, 0, ori_w, ori_h)) + + ori_w, ori_h = gen_img.size + + # rec imgs + save_img_name = str(i).zfill(4) + '.jpg' + if i < val_ratio: + save_dir = os.path.join(rec_val_save_dir, save_img_name) + line = save_dir + '\t' + char_lines[i] + '\n' + rec_val_file.write(line) + else: + save_dir = os.path.join(rec_save_dir, save_img_name) + line = save_dir + '\t' + char_lines[i] + '\n' + rec_train_file.write(line) + gen_img.save(cf.output_dir + save_dir, quality = 95, subsampling=0) + + # det img + # random choice bg + bg_pic = random.sample(bg_pics, 1)[0] + det_img = Image.open(os.path.join(det_bg_path, bg_pic)) + # the PCB position is fixed, modify it according to your own scenario + if bg_pic == '1.png': + x1 = 38 + y1 = 3 + else: + x1 = 34 + y1 = 1 + + det_img.paste(gen_img, (x1, y1)) + # text pos + chars_pos = [[x1, y1], [x1 + ori_w, y1], [x1 + ori_w, y1 + ori_h], [x1, y1 + ori_h]] + label = [{"transcription":char_lines[i], "points":chars_pos}] + if i < val_ratio: + save_dir = os.path.join(det_val_save_dir, save_img_name) + det_val_file.write(save_dir + '\t' + json.dumps( + label, ensure_ascii=False) + '\n') + else: + save_dir = os.path.join(det_save_dir, save_img_name) + det_train_file.write(save_dir + '\t' + json.dumps( + label, ensure_ascii=False) + '\n') + det_img.save(cf.output_dir + save_dir, quality = 95, subsampling=0) diff --git a/ocr_webapp/PaddleOCR/applications/README.md b/ocr_webapp/PaddleOCR/applications/README.md new file mode 100644 index 0000000000000000000000000000000000000000..896f1c4be5b73d4b624fa9ca980f9351fde5f1ce --- /dev/null +++ b/ocr_webapp/PaddleOCR/applications/README.md @@ -0,0 +1,77 @@ +[English](README_en.md) | 简体中文 + +# 场景应用 + +PaddleOCR场景应用覆盖通用,制造、金融、交通行业的主要OCR垂类应用,在PP-OCR、PP-Structure的通用能力基础之上,以notebook的形式展示利用场景数据微调、模型优化方法、数据增广等内容,为开发者快速落地OCR应用提供示范与启发。 + +- [教程文档](#1) + - [通用](#11) + - [制造](#12) + - [金融](#13) + - [交通](#14) + +- [模型下载](#2) + + + +## 教程文档 + + + +### 通用 + +| 类别 | 亮点 | 模型下载 | 教程 | 示例图 | +| ---------------------- | ------------------------------------------------------------ | -------------- | --------------------------------------- | ------------------------------------------------------------ | +| 高精度中文识别模型SVTR | 比PP-OCRv3识别模型精度高3%,
可用于数据挖掘或对预测效率要求不高的场景。 | [模型下载](#2) | [中文](./高精度中文识别模型.md)/English | | +| 手写体识别 | 新增字形支持 | [模型下载](#2) | [中文](./手写文字识别.md)/English | | + + + +### 制造 + +| 类别 | 亮点 | 模型下载 | 教程 | 示例图 | +| -------------- | ------------------------------ | -------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 数码管识别 | 数码管数据合成、漏识别调优 | [模型下载](#2) | [中文](./光功率计数码管字符识别/光功率计数码管字符识别.md)/English | | +| 液晶屏读数识别 | 检测模型蒸馏、Serving部署 | [模型下载](#2) | [中文](./液晶屏读数识别.md)/English | | +| 包装生产日期 | 点阵字符合成、过曝过暗文字识别 | [模型下载](#2) | [中文](./包装生产日期识别.md)/English | | +| PCB文字识别 | 小尺寸文本检测与识别 | [模型下载](#2) | [中文](./PCB字符识别/PCB字符识别.md)/English | | +| 电表识别 | 大分辨率图像检测调优 | [模型下载](#2) | | | +| 液晶屏缺陷检测 | 非文字字符识别 | | | | + + + +### 金融 + +| 类别 | 亮点 | 模型下载 | 教程 | 示例图 | +| -------------- | ----------------------------- | -------------- | ----------------------------------------- | ------------------------------------------------------------ | +| 表单VQA | 多模态通用表单结构化提取 | [模型下载](#2) | [中文](./多模态表单识别.md)/English | | +| 增值税发票 | 关键信息抽取,SER、RE任务训练 | [模型下载](#2) | [中文](./发票关键信息抽取.md)/English | | +| 印章检测与识别 | 端到端弯曲文本识别 | [模型下载](#2) | [中文](./印章弯曲文字识别.md)/English | | +| 通用卡证识别 | 通用结构化提取 | [模型下载](#2) | [中文](./快速构建卡证类OCR.md)/English | | +| 身份证识别 | 结构化提取、图像阴影 | | | | +| 合同比对 | 密集文本检测、NLP关键信息抽取 | [模型下载](#2) | [中文](./扫描合同关键信息提取.md)/English | | + + + +### 交通 + +| 类别 | 亮点 | 模型下载 | 教程 | 示例图 | +| ----------------- | ------------------------------ | -------------- | ----------------------------------- | ------------------------------------------------------------ | +| 车牌识别 | 多角度图像、轻量模型、端侧部署 | [模型下载](#2) | [中文](./轻量级车牌识别.md)/English | | +| 驾驶证/行驶证识别 | 尽请期待 | | | | +| 快递单识别 | 尽请期待 | | | | + + + +## 模型下载 + +如需下载上述场景中已经训练好的垂类模型,可以加入PaddleX官方交流频道获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 + + +如果您是企业开发者且未在上述场景中找到合适的方案,可以填写[OCR应用合作调研问卷](https://paddle.wjx.cn/vj/QwF7GKw.aspx),免费与官方团队展开不同层次的合作,包括但不限于问题抽象、确定技术方案、项目答疑、共同研发等。如果您已经使用PaddleOCR落地项目,也可以填写此问卷,与飞桨平台共同宣传推广,提升企业技术品宣。期待您的提交! + + +traffic + diff --git a/ocr_webapp/PaddleOCR/applications/README_en.md b/ocr_webapp/PaddleOCR/applications/README_en.md new file mode 100644 index 0000000000000000000000000000000000000000..df18465e55253e98a3caf750bee203ff451b6ac9 --- /dev/null +++ b/ocr_webapp/PaddleOCR/applications/README_en.md @@ -0,0 +1,79 @@ +English| [简体中文](README.md) + +# Application + +PaddleOCR scene application covers general, manufacturing, finance, transportation industry of the main OCR vertical applications, on the basis of the general capabilities of PP-OCR, PP-Structure, in the form of notebook to show the use of scene data fine-tuning, model optimization methods, data augmentation and other content, for developers to quickly land OCR applications to provide demonstration and inspiration. + +- [Tutorial](#1) + - [General](#11) + - [Manufacturing](#12) + - [Finance](#13) + - [Transportation](#14) + +- [Model Download](#2) + + + +## Tutorial + + + +### General + +| Case | Feature | Model Download | Tutorial | Example | +| ---------------------------------------------- | ---------------- | -------------------- | --------------------------------------- | ------------------------------------------------------------ | +| High-precision Chineses recognition model SVTR | New model | [Model Download](#2) | [中文](./高精度中文识别模型.md)/English | | +| Chinese handwriting recognition | New font support | [Model Download](#2) | [中文](./手写文字识别.md)/English | | + + + +### Manufacturing + +| Case | Feature | Model Download | Tutorial | Example | +| ------------------------------ | ------------------------------------------------------------ | -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| Digital tube | Digital tube data sythesis, recognition model fine-tuning | [Model Download](#2) | [中文](./光功率计数码管字符识别/光功率计数码管字符识别.md)/English | | +| LCD screen | Detection model distillation, serving deployment | [Model Download](#2) | [中文](./液晶屏读数识别.md)/English | | +| Packaging production data | Dot matrix character synthesis, overexposure and overdark text recognition | [Model Download](#2) | [中文](./包装生产日期识别.md)/English | | +| PCB text recognition | Small size text detection and recognition | [Model Download](#2) | [中文](./PCB字符识别/PCB字符识别.md)/English | | +| Meter text recognition | High-resolution image detection fine-tuning | [Model Download](#2) | | | +| LCD character defect detection | Non-text character recognition | | | | + + + +### Finance + +| Case | Feature | Model Download | Tutorial | Example | +| ----------------------------------- | -------------------------------------------------- | -------------------- | ----------------------------------------- | ------------------------------------------------------------ | +| Form visual question and answer | Multimodal general form structured extraction | [Model Download](#2) | [中文](./多模态表单识别.md)/English | | +| VAT invoice | Key information extraction, SER, RE task fine-tune | [Model Download](#2) | [中文](./发票关键信息抽取.md)/English | | +| Seal detection and recognition | End-to-end curved text recognition | [Model Download](#2) | [中文](./印章弯曲文字识别.md)/English | | +| Universal card recognition | Universal structured extraction | [Model Download](#2) | [中文](./快速构建卡证类OCR.md)/English | | +| ID card recognition | Structured extraction, image shading | | | | +| Contract key information extraction | Dense text detection, NLP concatenation | [Model Download](#2) | [中文](./扫描合同关键信息提取.md)/English | | + + + +### Transportation + +| Case | Feature | Model Download | Tutorial | Example | +| ----------------------------------------------- | ------------------------------------------------------------ | -------------------- | ----------------------------------- | ------------------------------------------------------------ | +| License plate recognition | Multi-angle images, lightweight models, edge-side deployment | [Model Download](#2) | [中文](./轻量级车牌识别.md)/English | | +| Driver's license/driving license identification | coming soon | | | | +| Express text recognition | coming soon | | | | + + + +## Model Download + +- For international developers: We're building a way to download these trained models, and since the current tutorials are Chinese, if you are good at both Chinese and English, or willing to polish English documents, please let us know in [discussion](https://github.com/PaddlePaddle/PaddleOCR/discussions). +- For Chinese developer: If you want to download the trained application model in the above scenarios, scan the QR code below with your WeChat, follow the PaddlePaddle official account to fill in the questionnaire, and join the PaddleOCR official group to get the 20G OCR learning materials (including "Dive into OCR" e-book, course video, application models and other materials) + +
+ +
+ + If you are an enterprise developer and have not found a suitable solution in the above scenarios, you can fill in the [OCR Application Cooperation Survey Questionnaire](https://paddle.wjx.cn/vj/QwF7GKw.aspx) to carry out different levels of cooperation with the official team **for free**, including but not limited to problem abstraction, technical solution determination, project Q&A, joint research and development, etc. If you have already used paddleOCR in your project, you can also fill out this questionnaire to jointly promote with the PaddlePaddle and enhance the technical publicity of enterprises. Looking forward to your submission! + + +trackgit-views + diff --git "a/ocr_webapp/PaddleOCR/applications/\344\270\255\346\226\207\350\241\250\346\240\274\350\257\206\345\210\253.md" "b/ocr_webapp/PaddleOCR/applications/\344\270\255\346\226\207\350\241\250\346\240\274\350\257\206\345\210\253.md" new file mode 100644 index 0000000000000000000000000000000000000000..d61514ff2d65703aadfb81b8dc6f52167ab953fe --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\344\270\255\346\226\207\350\241\250\346\240\274\350\257\206\345\210\253.md" @@ -0,0 +1,472 @@ +# 智能运营:通用中文表格识别 + +- [1. 背景介绍](#1-背景介绍) +- [2. 中文表格识别](#2-中文表格识别) +- [2.1 环境准备](#21-环境准备) +- [2.2 准备数据集](#22-准备数据集) + - [2.2.1 划分训练测试集](#221-划分训练测试集) + - [2.2.2 查看数据集](#222-查看数据集) +- [2.3 训练](#23-训练) +- [2.4 验证](#24-验证) +- [2.5 训练引擎推理](#25-训练引擎推理) +- [2.6 模型导出](#26-模型导出) +- [2.7 预测引擎推理](#27-预测引擎推理) +- [2.8 表格识别](#28-表格识别) +- [3. 表格属性识别](#3-表格属性识别) +- [3.1 代码、环境、数据准备](#31-代码环境数据准备) + - [3.1.1 代码准备](#311-代码准备) + - [3.1.2 环境准备](#312-环境准备) + - [3.1.3 数据准备](#313-数据准备) +- [3.2 表格属性识别训练](#32-表格属性识别训练) +- [3.3 表格属性识别推理和部署](#33-表格属性识别推理和部署) + - [3.3.1 模型转换](#331-模型转换) + - [3.3.2 模型推理](#332-模型推理) + +## 1. 背景介绍 + +中文表格识别在金融行业有着广泛的应用,如保险理赔、财报分析和信息录入等领域。当前,金融行业的表格识别主要以手动录入为主,开发一种自动表格识别成为丞待解决的问题。 +![](https://ai-studio-static-online.cdn.bcebos.com/d1e7780f0c7745ada4be540decefd6288e4d59257d8141f6842682a4c05d28b6) + + +在金融行业中,表格图像主要有清单类的单元格密集型表格,申请表类的大单元格表格,拍照表格和倾斜表格四种主要形式。 + +![](https://ai-studio-static-online.cdn.bcebos.com/da82ae8ef8fd479aaa38e1049eb3a681cf020dc108fa458eb3ec79da53b45fd1) +![](https://ai-studio-static-online.cdn.bcebos.com/5ffff2093a144a6993a75eef71634a52276015ee43a04566b9c89d353198c746) + + +当前的表格识别算法不能很好的处理这些场景下的表格图像。在本例中,我们使用PP-StructureV2最新发布的表格识别模型SLANet来演示如何进行中文表格是识别。同时,为了方便作业流程,我们使用表格属性识别模型对表格图像的属性进行识别,对表格的难易程度进行判断,加快人工进行校对速度。 + +本项目AI Studio链接:https://aistudio.baidu.com/aistudio/projectdetail/4588067 + +## 2. 中文表格识别 +### 2.1 环境准备 + + +```python +# 下载PaddleOCR代码 +! git clone -b dygraph https://gitee.com/paddlepaddle/PaddleOCR +``` + + +```python +# 安装PaddleOCR环境 +! pip install -r PaddleOCR/requirements.txt --force-reinstall +! pip install protobuf==3.19 +``` + +### 2.2 准备数据集 + +本例中使用的数据集采用表格[生成工具](https://github.com/WenmuZhou/TableGeneration)制作。 + +使用如下命令对数据集进行解压,并查看数据集大小 + + +```python +! cd data/data165849 && tar -xf table_gen_dataset.tar && cd - +! wc -l data/data165849/table_gen_dataset/gt.txt +``` + +#### 2.2.1 划分训练测试集 + +使用下述命令将数据集划分为训练集和测试集, 这里将90%划分为训练集,10%划分为测试集 + + +```python +import random +with open('/home/aistudio/data/data165849/table_gen_dataset/gt.txt') as f: + lines = f.readlines() +random.shuffle(lines) +train_len = int(len(lines)*0.9) +train_list = lines[:train_len] +val_list = lines[train_len:] + +# 保存结果 +with open('/home/aistudio/train.txt','w',encoding='utf-8') as f: + f.writelines(train_list) +with open('/home/aistudio/val.txt','w',encoding='utf-8') as f: + f.writelines(val_list) +``` + +划分完成后,数据集信息如下 + +|类型|数量|图片地址|标注文件路径| +|---|---|---|---| +|训练集|18000|/home/aistudio/data/data165849/table_gen_dataset|/home/aistudio/train.txt| +|测试集|2000|/home/aistudio/data/data165849/table_gen_dataset|/home/aistudio/val.txt| + +#### 2.2.2 查看数据集 + + +```python +import cv2 +import os, json +import numpy as np +from matplotlib import pyplot as plt +%matplotlib inline + +def parse_line(data_dir, line): + data_line = line.strip("\n") + info = json.loads(data_line) + file_name = info['filename'] + cells = info['html']['cells'].copy() + structure = info['html']['structure']['tokens'].copy() + + img_path = os.path.join(data_dir, file_name) + if not os.path.exists(img_path): + print(img_path) + return None + data = { + 'img_path': img_path, + 'cells': cells, + 'structure': structure, + 'file_name': file_name + } + return data + +def draw_bbox(img_path, points, color=(255, 0, 0), thickness=2): + if isinstance(img_path, str): + img_path = cv2.imread(img_path) + img_path = img_path.copy() + for point in points: + cv2.polylines(img_path, [point.astype(int)], True, color, thickness) + return img_path + + +def rebuild_html(data): + html_code = data['structure'] + cells = data['cells'] + to_insert = [i for i, tag in enumerate(html_code) if tag in ('', '>')] + + for i, cell in zip(to_insert[::-1], cells[::-1]): + if cell['tokens']: + text = ''.join(cell['tokens']) + # skip empty text + sp_char_list = ['', '', '\u2028', ' ', '', ''] + text_remove_style = skip_char(text, sp_char_list) + if len(text_remove_style) == 0: + continue + html_code.insert(i + 1, text) + + html_code = ''.join(html_code) + return html_code + + +def skip_char(text, sp_char_list): + """ + skip empty cell + @param text: text in cell + @param sp_char_list: style char and special code + @return: + """ + for sp_char in sp_char_list: + text = text.replace(sp_char, '') + return text + +save_dir = '/home/aistudio/vis' +os.makedirs(save_dir, exist_ok=True) +image_dir = '/home/aistudio/data/data165849/' +html_str = '' + +# 解析标注信息并还原html表格 +data = parse_line(image_dir, val_list[0]) + +img = cv2.imread(data['img_path']) +img_name = ''.join(os.path.basename(data['file_name']).split('.')[:-1]) +img_save_name = os.path.join(save_dir, img_name) +boxes = [np.array(x['bbox']) for x in data['cells']] +show_img = draw_bbox(data['img_path'], boxes) +cv2.imwrite(img_save_name + '_show.jpg', show_img) + +html = rebuild_html(data) +html_str += html +html_str += '
' + +# 显示标注的html字符串 +from IPython.core.display import display, HTML +display(HTML(html_str)) +# 显示单元格坐标 +plt.figure(figsize=(15,15)) +plt.imshow(show_img) +plt.show() +``` + +### 2.3 训练 + +这里选用PP-StructureV2中的表格识别模型[SLANet](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/configs/table/SLANet.yml) + +SLANet是PP-StructureV2全新推出的表格识别模型,相比PP-StructureV1中TableRec-RARE,在速度不变的情况下精度提升4.7%。TEDS提升2% + + +|算法|Acc|[TEDS(Tree-Edit-Distance-based Similarity)](https://github.com/ibm-aur-nlp/PubTabNet/tree/master/src)|Speed| +| --- | --- | --- | ---| +| EDD[2] |x| 88.30% |x| +| TableRec-RARE(ours) | 71.73%| 93.88% |779ms| +| SLANet(ours) | 76.31%| 95.89%|766ms| + +进行训练之前先使用如下命令下载预训练模型 + + +```python +# 进入PaddleOCR工作目录 +os.chdir('/home/aistudio/PaddleOCR') +# 下载英文预训练模型 +! wget -nc -P ./pretrain_models/ https://paddleocr.bj.bcebos.com/ppstructure/models/slanet/en_ppstructure_mobile_v2.0_SLANet_train.tar --no-check-certificate +! cd ./pretrain_models/ && tar xf en_ppstructure_mobile_v2.0_SLANet_train.tar && cd ../ +``` + +使用如下命令即可启动训练,需要修改的配置有 + +|字段|修改值|含义| +|---|---|---| +|Global.pretrained_model|./pretrain_models/en_ppstructure_mobile_v2.0_SLANet_train/best_accuracy.pdparams|指向英文表格预训练模型地址| +|Global.eval_batch_step|562|模型多少step评估一次,一般设置为一个epoch总的step数| +|Optimizer.lr.name|Const|学习率衰减器 | +|Optimizer.lr.learning_rate|0.0005|学习率设为之前的0.05倍 | +|Train.dataset.data_dir|/home/aistudio/data/data165849|指向训练集图片存放目录 | +|Train.dataset.label_file_list|/home/aistudio/data/data165849/table_gen_dataset/train.txt|指向训练集标注文件 | +|Train.loader.batch_size_per_card|32|训练时每张卡的batch_size | +|Train.loader.num_workers|1|训练集多进程数据读取的进程数,在aistudio中需要设为1 | +|Eval.dataset.data_dir|/home/aistudio/data/data165849|指向测试集图片存放目录 | +|Eval.dataset.label_file_list|/home/aistudio/data/data165849/table_gen_dataset/val.txt|指向测试集标注文件 | +|Eval.loader.batch_size_per_card|32|测试时每张卡的batch_size | +|Eval.loader.num_workers|1|测试集多进程数据读取的进程数,在aistudio中需要设为1 | + + +已经修改好的配置存储在 `/home/aistudio/SLANet_ch.yml` + + +```python +import os +os.chdir('/home/aistudio/PaddleOCR') +! python3 tools/train.py -c /home/aistudio/SLANet_ch.yml +``` + +大约在7个epoch后达到最高精度 97.49% + +### 2.4 验证 + +训练完成后,可使用如下命令在测试集上评估最优模型的精度 + + +```python +! python3 tools/eval.py -c /home/aistudio/SLANet_ch.yml -o Global.checkpoints=/home/aistudio/PaddleOCR/output/SLANet_ch/best_accuracy.pdparams +``` + +### 2.5 训练引擎推理 +使用如下命令可使用训练引擎对单张图片进行推理 + + +```python +import os;os.chdir('/home/aistudio/PaddleOCR') +! python3 tools/infer_table.py -c /home/aistudio/SLANet_ch.yml -o Global.checkpoints=/home/aistudio/PaddleOCR/output/SLANet_ch/best_accuracy.pdparams Global.infer_img=/home/aistudio/data/data165849/table_gen_dataset/img/no_border_18298_G7XZH93DDCMATGJQ8RW2.jpg +``` + + +```python +import cv2 +from matplotlib import pyplot as plt +%matplotlib inline + +# 显示原图 +show_img = cv2.imread('/home/aistudio/data/data165849/table_gen_dataset/img/no_border_18298_G7XZH93DDCMATGJQ8RW2.jpg') +plt.figure(figsize=(15,15)) +plt.imshow(show_img) +plt.show() + +# 显示预测的单元格 +show_img = cv2.imread('/home/aistudio/PaddleOCR/output/infer/no_border_18298_G7XZH93DDCMATGJQ8RW2.jpg') +plt.figure(figsize=(15,15)) +plt.imshow(show_img) +plt.show() +``` + +### 2.6 模型导出 + +使用如下命令可将模型导出为inference模型 + + +```python +! python3 tools/export_model.py -c /home/aistudio/SLANet_ch.yml -o Global.checkpoints=/home/aistudio/PaddleOCR/output/SLANet_ch/best_accuracy.pdparams Global.save_inference_dir=/home/aistudio/SLANet_ch/infer +``` + +### 2.7 预测引擎推理 +使用如下命令可使用预测引擎对单张图片进行推理 + + + +```python +os.chdir('/home/aistudio/PaddleOCR/ppstructure') +! python3 table/predict_structure.py \ + --table_model_dir=/home/aistudio/SLANet_ch/infer \ + --table_char_dict_path=../ppocr/utils/dict/table_structure_dict.txt \ + --image_dir=/home/aistudio/data/data165849/table_gen_dataset/img/no_border_18298_G7XZH93DDCMATGJQ8RW2.jpg \ + --output=../output/inference +``` + + +```python +# 显示原图 +show_img = cv2.imread('/home/aistudio/data/data165849/table_gen_dataset/img/no_border_18298_G7XZH93DDCMATGJQ8RW2.jpg') +plt.figure(figsize=(15,15)) +plt.imshow(show_img) +plt.show() + +# 显示预测的单元格 +show_img = cv2.imread('/home/aistudio/PaddleOCR/output/inference/no_border_18298_G7XZH93DDCMATGJQ8RW2.jpg') +plt.figure(figsize=(15,15)) +plt.imshow(show_img) +plt.show() +``` + +### 2.8 表格识别 + +在表格结构模型训练完成后,可结合OCR检测识别模型,对表格内容进行识别。 + +首先下载PP-OCRv3文字检测识别模型 + + +```python +# 下载PP-OCRv3文本检测识别模型并解压 +! wget -nc -P ./inference/ https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_slim_infer.tar --no-check-certificate +! wget -nc -P ./inference/ https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_slim_infer.tar --no-check-certificate +! cd ./inference/ && tar xf ch_PP-OCRv3_det_slim_infer.tar && tar xf ch_PP-OCRv3_rec_slim_infer.tar && cd ../ +``` + +模型下载完成后,使用如下命令进行表格识别 + + +```python +import os;os.chdir('/home/aistudio/PaddleOCR/ppstructure') +! python3 table/predict_table.py \ + --det_model_dir=inference/ch_PP-OCRv3_det_slim_infer \ + --rec_model_dir=inference/ch_PP-OCRv3_rec_slim_infer \ + --table_model_dir=/home/aistudio/SLANet_ch/infer \ + --rec_char_dict_path=../ppocr/utils/ppocr_keys_v1.txt \ + --table_char_dict_path=../ppocr/utils/dict/table_structure_dict.txt \ + --image_dir=/home/aistudio/data/data165849/table_gen_dataset/img/no_border_18298_G7XZH93DDCMATGJQ8RW2.jpg \ + --output=../output/table +``` + + +```python +# 显示原图 +show_img = cv2.imread('/home/aistudio/data/data165849/table_gen_dataset/img/no_border_18298_G7XZH93DDCMATGJQ8RW2.jpg') +plt.figure(figsize=(15,15)) +plt.imshow(show_img) +plt.show() + +# 显示预测结果 +from IPython.core.display import display, HTML +display(HTML('
alleadersh不贰过,推从自己参与浙江数。另一方
AnSha自己越共商共建工作协商w.east 抓好改革试点任务
EdimeImisesElec怀天下”。22.26 31.614.30 794.94
ip Profundi:2019年12月1Horspro444.482.41 87679.98
iehaiTrain组长蒋蕊Toafterdec203.4323.54 44266.62
Tyint roudlyRol谢您的好意,我知道ErChows48.9010316
NaFlint一辈的aterreclam7823.869829.237.96 3068
家上下游企业,5Tr景象。当地球上的我们Urelaw799.62354.9612.9833
赛事( uestCh复制的业务模式并Listicjust9.239253.22
Ca Iskole扶贫"之名引导 Papua 7191.901.653.6248
避讳ir但由于Fficeof0.226.377.173397.75
ndaTurk百处遗址gMa1288.342053.662.29885.45
')) +``` + +## 3. 表格属性识别 +### 3.1 代码、环境、数据准备 +#### 3.1.1 代码准备 +首先,我们需要准备训练表格属性的代码,PaddleClas集成了PULC方案,该方案可以快速获得一个在CPU上用时2ms的属性识别模型。PaddleClas代码可以clone下载得到。获取方式如下: + + + +```python +! git clone -b develop https://gitee.com/paddlepaddle/PaddleClas +``` + +#### 3.1.2 环境准备 +其次,我们需要安装训练PaddleClas相关的依赖包 + + +```python +! pip install -r PaddleClas/requirements.txt --force-reinstall +! pip install protobuf==3.20.0 +``` + + +#### 3.1.3 数据准备 + +最后,准备训练数据。在这里,我们一共定义了表格的6个属性,分别是表格来源、表格数量、表格颜色、表格清晰度、表格有无干扰、表格角度。其可视化如下: + +![](https://user-images.githubusercontent.com/45199522/190587903-ccdfa6fb-51e8-42de-b08b-a127cb04e304.png) + +这里,我们提供了一个表格属性的demo子集,可以快速迭代体验。下载方式如下: + + +```python +%cd PaddleClas/dataset +!wget https://paddleclas.bj.bcebos.com/data/PULC/table_attribute.tar +!tar -xf table_attribute.tar +%cd ../PaddleClas/dataset +%cd ../ +``` + +### 3.2 表格属性识别训练 +表格属性训练整体pipelinie如下: + +![](https://user-images.githubusercontent.com/45199522/190599426-3415b38e-e16e-4e68-9253-2ff531b1b5ca.png) + +1.训练过程中,图片经过预处理之后,送入到骨干网络之中,骨干网络将抽取表格图片的特征,最终该特征连接输出的FC层,FC层经过Sigmoid激活函数后和真实标签做交叉熵损失函数,优化器通过对该损失函数做梯度下降来更新骨干网络的参数,经过多轮训练后,骨干网络的参数可以对为止图片做很好的预测; + +2.推理过程中,图片经过预处理之后,送入到骨干网络之中,骨干网络加载学习好的权重后对该表格图片做出预测,预测的结果为一个6维向量,该向量中的每个元素反映了每个属性对应的概率值,通过对该值进一步卡阈值之后,得到最终的输出,最终的输出描述了该表格的6个属性。 + +当准备好相关的数据之后,可以一键启动表格属性的训练,训练代码如下: + + +```python + +!python tools/train.py -c ./ppcls/configs/PULC/table_attribute/PPLCNet_x1_0.yaml -o Global.device=cpu -o Global.epochs=10 +``` + +### 3.3 表格属性识别推理和部署 +#### 3.3.1 模型转换 +当训练好模型之后,需要将模型转换为推理模型进行部署。转换脚本如下: + + +```python +!python tools/export_model.py -c ppcls/configs/PULC/table_attribute/PPLCNet_x1_0.yaml -o Global.pretrained_model=output/PPLCNet_x1_0/best_model +``` + +执行以上命令之后,会在当前目录上生成`inference`文件夹,该文件夹中保存了当前精度最高的推理模型。 + +#### 3.3.2 模型推理 +安装推理需要的paddleclas包, 此时需要通过下载安装paddleclas的develop的whl包 + + + +```python +!pip install https://paddleclas.bj.bcebos.com/whl/paddleclas-0.0.0-py3-none-any.whl +``` + +进入`deploy`目录下即可对模型进行推理 + + +```python +%cd deploy/ +``` + +推理命令如下: + + +```python +!python python/predict_cls.py -c configs/PULC/table_attribute/inference_table_attribute.yaml -o Global.inference_model_dir="../inference" -o Global.infer_imgs="../dataset/table_attribute/Table_val/val_9.jpg" +!python python/predict_cls.py -c configs/PULC/table_attribute/inference_table_attribute.yaml -o Global.inference_model_dir="../inference" -o Global.infer_imgs="../dataset/table_attribute/Table_val/val_3253.jpg" +``` + +推理的表格图片: + +![](https://user-images.githubusercontent.com/45199522/190596141-74f4feda-b082-46d7-908d-b0bd5839b430.png) + +预测结果如下: +``` +val_9.jpg: {'attributes': ['Scanned', 'Little', 'Black-and-White', 'Clear', 'Without-Obstacles', 'Horizontal'], 'output': [1, 1, 1, 1, 1, 1]} +``` + + +推理的表格图片: + +![](https://user-images.githubusercontent.com/45199522/190597086-2e685200-22d0-4042-9e46-f61f24e02e4e.png) + +预测结果如下: +``` +val_3253.jpg: {'attributes': ['Photo', 'Little', 'Black-and-White', 'Blurry', 'Without-Obstacles', 'Tilted'], 'output': [0, 1, 1, 0, 1, 0]} +``` + +对比两张图片可以发现,第一张图片比较清晰,表格属性的结果也偏向于比较容易识别,我们可以更相信表格识别的结果,第二张图片比较模糊,且存在倾斜现象,表格识别可能存在错误,需要我们人工进一步校验。通过表格的属性识别能力,可以进一步将“人工”和“智能”很好的结合起来,为表格识别能力的落地的精度提供保障。 diff --git "a/ocr_webapp/PaddleOCR/applications/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253/corpus/digital.txt" "b/ocr_webapp/PaddleOCR/applications/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253/corpus/digital.txt" new file mode 100644 index 0000000000000000000000000000000000000000..26b06e784e779bdf97dfaa9b3ba8d4c0155b0718 --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253/corpus/digital.txt" @@ -0,0 +1,43 @@ +46.39 +40.08 +89.52 +-71.93 +23.19 +-81.02 +-34.09 +05.87 +-67.80 +-51.56 +-34.58 +37.91 +56.98 +29.01 +-90.13 +35.55 +66.07 +-90.35 +-50.93 +42.42 +21.40 +-30.99 +-71.78 +25.60 +-48.69 +-72.28 +-17.55 +-99.93 +-47.35 +-64.89 +-31.28 +-90.01 +05.17 +30.91 +30.56 +-06.90 +79.05 +67.74 +-32.31 +94.22 +28.75 +51.03 +-58.96 diff --git "a/ocr_webapp/PaddleOCR/applications/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253/fonts/DS-DIGI.TTF" "b/ocr_webapp/PaddleOCR/applications/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253/fonts/DS-DIGI.TTF" new file mode 100644 index 0000000000000000000000000000000000000000..09258773c7219ad9e60b92b918e3c50b58f43c9e Binary files /dev/null and "b/ocr_webapp/PaddleOCR/applications/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253/fonts/DS-DIGI.TTF" differ diff --git "a/ocr_webapp/PaddleOCR/applications/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253/fonts/DS-DIGIB.TTF" "b/ocr_webapp/PaddleOCR/applications/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253/fonts/DS-DIGIB.TTF" new file mode 100644 index 0000000000000000000000000000000000000000..064ad478a510a9c581bb16d295f659256e1f920a Binary files /dev/null and "b/ocr_webapp/PaddleOCR/applications/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253/fonts/DS-DIGIB.TTF" differ diff --git "a/ocr_webapp/PaddleOCR/applications/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253.md" "b/ocr_webapp/PaddleOCR/applications/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253.md" new file mode 100644 index 0000000000000000000000000000000000000000..95fc81479e5e362ee8808598aacbb2093f324e5a --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253/\345\205\211\345\212\237\347\216\207\350\256\241\346\225\260\347\240\201\347\256\241\345\255\227\347\254\246\350\257\206\345\210\253.md" @@ -0,0 +1,467 @@ +# 光功率计数码管字符识别 + +本案例将使用OCR技术自动识别光功率计显示屏文字,通过本章您可以掌握: + +- PaddleOCR快速使用 +- 数据合成方法 +- 数据挖掘方法 +- 基于现有数据微调 + +## 1. 背景介绍 + +光功率计(optical power meter )是指用于测量绝对光功率或通过一段光纤的光功率相对损耗的仪器。在光纤系统中,测量光功率是最基本的,非常像电子学中的万用表;在光纤测量中,光功率计是重负荷常用表。 + + + +目前光功率计缺少将数据直接输出的功能,需要人工读数。这一项工作单调重复,如果可以使用机器替代人工,将节约大量成本。针对上述问题,希望通过摄像头拍照->智能读数的方式高效地完成此任务。 + +为实现智能读数,通常会采取文本检测+文本识别的方案: + +第一步,使用文本检测模型定位出光功率计中的数字部分; + +第二步,使用文本识别模型获得准确的数字和单位信息。 + +本项目主要介绍如何完成第二步文本识别部分,包括:真实评估集的建立、训练数据的合成、基于 PP-OCRv3 和 SVTR_Tiny 两个模型进行训练,以及评估和推理。 + +本项目难点如下: + +- 光功率计数码管字符数据较少,难以获取。 +- 数码管中小数点占像素较少,容易漏识别。 + +针对以上问题, 本例选用 PP-OCRv3 和 SVTR_Tiny 两个高精度模型训练,同时提供了真实数据挖掘案例和数据合成案例。基于 PP-OCRv3 模型,在构建的真实评估集上精度从 52% 提升至 72%,SVTR_Tiny 模型精度可达到 78.9%。 + +aistudio项目链接: [光功率计数码管字符识别](https://aistudio.baidu.com/aistudio/projectdetail/4049044?contributionType=1) + +## 2. PaddleOCR 快速使用 + +PaddleOCR 旨在打造一套丰富、领先、且实用的OCR工具库,助力开发者训练出更好的模型,并应用落地。 + +![](https://github.com/PaddlePaddle/PaddleOCR/raw/release/2.5/doc/imgs_results/ch_ppocr_mobile_v2.0/test_add_91.jpg) + + +官方提供了适用于通用场景的高精轻量模型,首先使用官方提供的 PP-OCRv3 模型预测图片,验证下当前模型在光功率计场景上的效果。 + +- 准备环境 + +``` +python3 -m pip install -U pip +python3 -m pip install paddleocr +``` + + +- 测试效果 + +测试图: + +![](https://ai-studio-static-online.cdn.bcebos.com/8dca91f016884e16ad9216d416da72ea08190f97d87b4be883f15079b7ebab9a) + + +``` +paddleocr --lang=ch --det=Fase --image_dir=data +``` + +得到如下测试结果: + +``` +('.7000', 0.6885431408882141) +``` + +发现数字识别较准,然而对负号和小数点识别不准确。 由于PP-OCRv3的训练数据大多为通用场景数据,在特定的场景上效果可能不够好。因此需要基于场景数据进行微调。 + +下面就主要介绍如何在光功率计(数码管)场景上微调训练。 + + +## 3. 开始训练 + +### 3.1 数据准备 + +特定的工业场景往往很难获取开源的真实数据集,光功率计也是如此。在实际工业场景中,可以通过摄像头采集的方法收集大量真实数据,本例中重点介绍数据合成方法和真实数据挖掘方法,如何利用有限的数据优化模型精度。 + +数据集分为两个部分:合成数据,真实数据, 其中合成数据由 text_renderer 工具批量生成得到, 真实数据通过爬虫等方式在百度图片中搜索并使用 PPOCRLabel 标注得到。 + + +- 合成数据 + +本例中数据合成工具使用的是 [text_renderer](https://github.com/Sanster/text_renderer), 该工具可以合成用于文本识别训练的文本行数据: + +![](https://github.com/oh-my-ocr/text_renderer/raw/master/example_data/effect_layout_image/char_spacing_compact.jpg) + +![](https://github.com/oh-my-ocr/text_renderer/raw/master/example_data/effect_layout_image/color_image.jpg) + + +``` +export https_proxy=http://172.19.57.45:3128 +git clone https://github.com/oh-my-ocr/text_renderer +``` + +``` +import os +python3 setup.py develop +python3 -m pip install -r docker/requirements.txt +python3 main.py \ + --config example_data/example.py \ + --dataset img \ + --num_processes 2 \ + --log_period 10 +``` + +给定字体和语料,就可以合成较为丰富样式的文本行数据。 光功率计识别场景,目标是正确识别数码管文本,因此需要收集部分数码管字体,训练语料,用于合成文本识别数据。 + +将收集好的语料存放在 example_data 路径下: + +``` +ln -s ./fonts/DS* text_renderer/example_data/font/ +ln -s ./corpus/digital.txt text_renderer/example_data/text/ +``` + +修改 text_renderer/example_data/font_list/font_list.txt ,选择需要的字体开始合成: + +``` +python3 main.py \ + --config example_data/digital_example.py \ + --dataset img \ + --num_processes 2 \ + --log_period 10 +``` + +合成图片会被存在目录 text_renderer/example_data/digital/chn_data 下 + +查看合成的数据样例: + +![](https://ai-studio-static-online.cdn.bcebos.com/7d5774a273f84efba5b9ce7fd3f86e9ef24b6473e046444db69fa3ca20ac0986) + + +- 真实数据挖掘 + +模型训练需要使用真实数据作为评价指标,否则很容易过拟合到简单的合成数据中。没有开源数据的情况下,可以利用部分无标注数据+标注工具获得真实数据。 + + +1. 数据搜集 + +使用[爬虫工具](https://github.com/Joeclinton1/google-images-download.git)获得无标注数据 + +2. [PPOCRLabel](https://github.com/PaddlePaddle/PaddleOCR/tree/release/2.5/PPOCRLabel) 完成半自动标注 + +PPOCRLabel是一款适用于OCR领域的半自动化图形标注工具,内置PP-OCR模型对数据自动标注和重新识别。使用Python3和PyQT5编写,支持矩形框标注、表格标注、不规则文本标注、关键信息标注模式,导出格式可直接用于PaddleOCR检测和识别模型的训练。 + +![](https://github.com/PaddlePaddle/PaddleOCR/raw/release/2.5/PPOCRLabel/data/gif/steps_en.gif) + + +收集完数据后就可以进行分配了,验证集中一般都是真实数据,训练集中包含合成数据+真实数据。本例中标注了155张图片,其中训练集和验证集的数目为100和55。 + + +最终 `data` 文件夹应包含以下几部分: + +``` +|-data + |- synth_train.txt + |- real_train.txt + |- real_eval.txt + |- synthetic_data + |- word_001.png + |- word_002.jpg + |- word_003.jpg + | ... + |- real_data + |- word_001.png + |- word_002.jpg + |- word_003.jpg + | ... + ... +``` + +### 3.2 模型选择 + +本案例提供了2种文本识别模型:PP-OCRv3 识别模型 和 SVTR_Tiny: + +[PP-OCRv3 识别模型](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/PP-OCRv3_introduction.md):PP-OCRv3的识别模块是基于文本识别算法SVTR优化。SVTR不再采用RNN结构,通过引入Transformers结构更加有效地挖掘文本行图像的上下文信息,从而提升文本识别能力。并进行了一系列结构改进加速模型预测。 + +[SVTR_Tiny](https://arxiv.org/abs/2205.00159):SVTR提出了一种用于场景文本识别的单视觉模型,该模型在patch-wise image tokenization框架内,完全摒弃了序列建模,在精度具有竞争力的前提下,模型参数量更少,速度更快。 + +以上两个策略在自建中文数据集上的精度和速度对比如下: + +| ID | 策略 | 模型大小 | 精度 | 预测耗时(CPU + MKLDNN)| +|-----|-----|--------|----| --- | +| 01 | PP-OCRv2 | 8M | 74.80% | 8.54ms | +| 02 | SVTR_Tiny | 21M | 80.10% | 97.00ms | +| 03 | SVTR_LCNet(h32) | 12M | 71.90% | 6.60ms | +| 04 | SVTR_LCNet(h48) | 12M | 73.98% | 7.60ms | +| 05 | + GTC | 12M | 75.80% | 7.60ms | +| 06 | + TextConAug | 12M | 76.30% | 7.60ms | +| 07 | + TextRotNet | 12M | 76.90% | 7.60ms | +| 08 | + UDML | 12M | 78.40% | 7.60ms | +| 09 | + UIM | 12M | 79.40% | 7.60ms | + + +### 3.3 开始训练 + +首先下载 PaddleOCR 代码库 + +``` +git clone -b release/2.5 https://github.com/PaddlePaddle/PaddleOCR.git +``` + +PaddleOCR提供了训练脚本、评估脚本和预测脚本,本节将以 PP-OCRv3 中文识别模型为例: + +**Step1:下载预训练模型** + +首先下载 pretrain model,您可以下载训练好的模型在自定义数据上进行finetune + +``` +cd PaddleOCR/ +# 下载PP-OCRv3 中文预训练模型 +wget -P ./pretrain_models/ https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_train.tar +# 解压模型参数 +cd pretrain_models +tar -xf ch_PP-OCRv3_rec_train.tar && rm -rf ch_PP-OCRv3_rec_train.tar +``` + +**Step2:自定义字典文件** + +接下来需要提供一个字典({word_dict_name}.txt),使模型在训练时,可以将所有出现的字符映射为字典的索引。 + +因此字典需要包含所有希望被正确识别的字符,{word_dict_name}.txt需要写成如下格式,并以 `utf-8` 编码格式保存: + +``` +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +- +. +``` + +word_dict.txt 每行有一个单字,将字符与数字索引映射在一起,“3.14” 将被映射成 [3, 11, 1, 4] + +* 内置字典 + +PaddleOCR内置了一部分字典,可以按需使用。 + +`ppocr/utils/ppocr_keys_v1.txt` 是一个包含6623个字符的中文字典 + +`ppocr/utils/ic15_dict.txt` 是一个包含36个字符的英文字典 + +* 自定义字典 + +内置字典面向通用场景,具体的工业场景中,可能需要识别特殊字符,或者只需识别某几个字符,此时自定义字典会更提升模型精度。例如在光功率计场景中,需要识别数字和单位。 + +遍历真实数据标签中的字符,制作字典`digital_dict.txt`如下所示: + +``` +- +. +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +B +E +F +H +L +N +T +W +d +k +m +n +o +z +``` + + + + +**Step3:修改配置文件** + +为了更好的使用预训练模型,训练推荐使用[ch_PP-OCRv3_rec_distillation.yml](../../configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml)配置文件,并参考下列说明修改配置文件: + +以 `ch_PP-OCRv3_rec_distillation.yml` 为例: +``` +Global: + ... + # 添加自定义字典,如修改字典请将路径指向新字典 + character_dict_path: ppocr/utils/dict/digital_dict.txt + ... + # 识别空格 + use_space_char: True + + +Optimizer: + ... + # 添加学习率衰减策略 + lr: + name: Cosine + learning_rate: 0.001 + ... + +... + +Train: + dataset: + # 数据集格式,支持LMDBDataSet以及SimpleDataSet + name: SimpleDataSet + # 数据集路径 + data_dir: ./data/ + # 训练集标签文件 + label_file_list: + - ./train_data/digital_img/digital_train.txt #11w + - ./train_data/digital_img/real_train.txt #100 + - ./train_data/digital_img/dbm_img/dbm.txt #3w + ratio_list: + - 0.3 + - 1.0 + - 1.0 + transforms: + ... + - RecResizeImg: + # 修改 image_shape 以适应长文本 + image_shape: [3, 48, 320] + ... + loader: + ... + # 单卡训练的batch_size + batch_size_per_card: 256 + ... + +Eval: + dataset: + # 数据集格式,支持LMDBDataSet以及SimpleDataSet + name: SimpleDataSet + # 数据集路径 + data_dir: ./data + # 验证集标签文件 + label_file_list: + - ./train_data/digital_img/real_val.txt + transforms: + ... + - RecResizeImg: + # 修改 image_shape 以适应长文本 + image_shape: [3, 48, 320] + ... + loader: + # 单卡验证的batch_size + batch_size_per_card: 256 + ... +``` +**注意,训练/预测/评估时的配置文件请务必与训练一致。** + +**Step4:启动训练** + +*如果您安装的是cpu版本,请将配置文件中的 `use_gpu` 字段修改为false* + +``` +# GPU训练 支持单卡,多卡训练 +# 训练数码管数据 训练日志会自动保存为 "{save_model_dir}" 下的train.log + +#单卡训练(训练周期长,不建议) +python3 tools/train.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml -o Global.pretrained_model=./pretrain_models/ch_PP-OCRv3_rec_train/best_accuracy + +#多卡训练,通过--gpus参数指定卡号 +python3 -m paddle.distributed.launch --gpus '0,1,2,3' tools/train.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml -o Global.pretrained_model=./pretrain_models/en_PP-OCRv3_rec_train/best_accuracy +``` + + +PaddleOCR支持训练和评估交替进行, 可以在 `configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml` 中修改 `eval_batch_step` 设置评估频率,默认每500个iter评估一次。评估过程中默认将最佳acc模型,保存为 `output/ch_PP-OCRv3_rec_distill/best_accuracy` 。 + +如果验证集很大,测试将会比较耗时,建议减少评估次数,或训练完再进行评估。 + +### SVTR_Tiny 训练 + +SVTR_Tiny 训练步骤与上面一致,SVTR支持的配置和模型训练权重可以参考[算法介绍文档](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/algorithm_rec_svtr.md) + +**Step1:下载预训练模型** + +``` +# 下载 SVTR_Tiny 中文识别预训练模型和配置文件 +wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/rec_svtr_tiny_none_ctc_ch_train.tar +# 解压模型参数 +tar -xf rec_svtr_tiny_none_ctc_ch_train.tar && rm -rf rec_svtr_tiny_none_ctc_ch_train.tar +``` +**Step2:自定义字典文件** + +字典依然使用自定义的 digital_dict.txt + +**Step3:修改配置文件** + +配置文件中对应修改字典路径和数据路径 + +**Step4:启动训练** + +``` +## 单卡训练 +python tools/train.py -c rec_svtr_tiny_none_ctc_ch_train/rec_svtr_tiny_6local_6global_stn_ch.yml \ + -o Global.pretrained_model=./rec_svtr_tiny_none_ctc_ch_train/best_accuracy +``` + +### 3.4 验证效果 + +如需获取已训练模型,请加入PaddleX官方交流频道,获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 + +将下载或训练完成的模型放置在对应目录下即可完成模型推理 + +* 指标评估 + +训练中模型参数默认保存在`Global.save_model_dir`目录下。在评估指标时,需要设置`Global.checkpoints`指向保存的参数文件。评估数据集可以通过 `configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml` 修改Eval中的 `label_file_path` 设置。 + +``` +# GPU 评估, Global.checkpoints 为待测权重 +python3 -m paddle.distributed.launch --gpus '0' tools/eval.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml -o Global.checkpoints={path/to/weights}/best_accuracy +``` + +* 测试识别效果 + +使用 PaddleOCR 训练好的模型,可以通过以下脚本进行快速预测。 + +默认预测图片存储在 `infer_img` 里,通过 `-o Global.checkpoints` 加载训练好的参数文件: + +根据配置文件中设置的 `save_model_dir` 和 `save_epoch_step` 字段,会有以下几种参数被保存下来: + +``` +output/rec/ +├── best_accuracy.pdopt +├── best_accuracy.pdparams +├── best_accuracy.states +├── config.yml +├── iter_epoch_3.pdopt +├── iter_epoch_3.pdparams +├── iter_epoch_3.states +├── latest.pdopt +├── latest.pdparams +├── latest.states +└── train.log +``` + +其中 best_accuracy.* 是评估集上的最优模型;iter_epoch_x.* 是以 `save_epoch_step` 为间隔保存下来的模型;latest.* 是最后一个epoch的模型。 + +``` +# 预测英文结果 +python3 tools/infer_rec.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml -o Global.pretrained_model={path/to/weights}/best_accuracy Global.infer_img=test_digital.png +``` + +预测图片: + +![](https://ai-studio-static-online.cdn.bcebos.com/8dca91f016884e16ad9216d416da72ea08190f97d87b4be883f15079b7ebab9a) + + +得到输入图像的预测结果: + +``` +infer_img: test_digital.png + result: ('-70.00', 0.9998967) +``` diff --git "a/ocr_webapp/PaddleOCR/applications/\345\214\205\350\243\205\347\224\237\344\272\247\346\227\245\346\234\237\350\257\206\345\210\253.md" "b/ocr_webapp/PaddleOCR/applications/\345\214\205\350\243\205\347\224\237\344\272\247\346\227\245\346\234\237\350\257\206\345\210\253.md" new file mode 100644 index 0000000000000000000000000000000000000000..670ec9cda3f5b66d074d30733bce21b5701db3c9 --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\345\214\205\350\243\205\347\224\237\344\272\247\346\227\245\346\234\237\350\257\206\345\210\253.md" @@ -0,0 +1,687 @@ +# 一种基于PaddleOCR的产品包装生产日期识别模型 + +- [1. 项目介绍](#1-项目介绍) +- [2. 环境搭建](#2-环境搭建) +- [3. 数据准备](#3-数据准备) +- [4. 直接使用PP-OCRv3模型评估](#4-直接使用PPOCRv3模型评估) +- [5. 基于合成数据finetune](#5-基于合成数据finetune) + - [5.1 Text Renderer数据合成方法](#51-TextRenderer数据合成方法) + - [5.1.1 下载Text Renderer代码](#511-下载TextRenderer代码) + - [5.1.2 准备背景图片](#512-准备背景图片) + - [5.1.3 准备语料](#513-准备语料) + - [5.1.4 下载字体](#514-下载字体) + - [5.1.5 运行数据合成命令](#515-运行数据合成命令) + - [5.2 模型训练](#52-模型训练) +- [6. 基于真实数据finetune](#6-基于真实数据finetune) + - [6.1 python爬虫获取数据](#61-python爬虫获取数据) + - [6.2 数据挖掘](#62-数据挖掘) + - [6.3 模型训练](#63-模型训练) +- [7. 基于合成+真实数据finetune](#7-基于合成+真实数据finetune) + + +## 1. 项目介绍 + +产品包装生产日期是计算机视觉图像识别技术在工业场景中的一种应用。产品包装生产日期识别技术要求能够将产品生产日期从复杂背景中提取并识别出来,在物流管理、物资管理中得到广泛应用。 + +![](https://ai-studio-static-online.cdn.bcebos.com/d9e0533cc1df47ffa3bbe99de9e42639a3ebfa5bce834bafb1ca4574bf9db684) + + +- 项目难点 + +1. 没有训练数据 +2. 图像质量层次不齐: 角度倾斜、图片模糊、光照不足、过曝等问题严重 + +针对以上问题, 本例选用PP-OCRv3这一开源超轻量OCR系统进行包装产品生产日期识别系统的开发。直接使用PP-OCRv3进行评估的精度为62.99%。为提升识别精度,我们首先使用数据合成工具合成了3k数据,基于这部分数据进行finetune,识别精度提升至73.66%。由于合成数据与真实数据之间的分布存在差异,为进一步提升精度,我们使用网络爬虫配合数据挖掘策略得到了1k带标签的真实数据,基于真实数据finetune的精度为71.33%。最后,我们综合使用合成数据和真实数据进行finetune,将识别精度提升至86.99%。各策略的精度提升效果如下: + +| 策略 | 精度| +| :--------------- | :-------- | +| PP-OCRv3评估 | 62.99| +| 合成数据finetune | 73.66| +| 真实数据finetune | 71.33| +| 真实+合成数据finetune | 86.99| + +AIStudio项目链接: [一种基于PaddleOCR的包装生产日期识别方法](https://aistudio.baidu.com/aistudio/projectdetail/4287736) + +## 2. 环境搭建 + +本任务基于Aistudio完成, 具体环境如下: + +- 操作系统: Linux +- PaddlePaddle: 2.3 +- PaddleOCR: Release/2.5 +- text_renderer: master + +下载PaddlleOCR代码并安装依赖库: +```bash +git clone -b dygraph https://gitee.com/paddlepaddle/PaddleOCR + +# 安装依赖库 +cd PaddleOCR +pip install -r PaddleOCR/requirements.txt +``` + +## 3. 数据准备 + +本项目使用人工预标注的300张图像作为测试集。 + +部分数据示例如下: + +![](https://ai-studio-static-online.cdn.bcebos.com/39ff30e0ab0442579712255e6a9ea6b5271169c98e624e6eb2b8781f003bfea0) + + +标签文件格式如下: +```txt +数据路径 标签(中间以制表符分隔) +``` + +|数据集类型|数量| +|---|---| +|测试集| 300| + +数据集[下载链接](https://aistudio.baidu.com/aistudio/datasetdetail/149770),下载后可以通过下方命令解压: + +```bash +tar -xvf data.tar +mv data ${PaddleOCR_root} +``` + +数据解压后的文件结构如下: + +```shell +PaddleOCR +├── data +│ ├── mining_images # 挖掘的真实数据示例 +│ ├── mining_train.list # 挖掘的真实数据文件列表 +│ ├── render_images # 合成数据示例 +│ ├── render_train.list # 合成数据文件列表 +│ ├── val # 测试集数据 +│ └── val.list # 测试集数据文件列表 +| ├── bg # 合成数据所需背景图像 +│ └── corpus # 合成数据所需语料 +``` + +## 4. 直接使用PP-OCRv3模型评估 + +准备好测试数据后,可以使用PaddleOCR的PP-OCRv3模型进行识别。 + +- 下载预训练模型 + +首先需要下载PP-OCR v3中英文识别模型文件,下载链接可以在https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/ppocr_introduction.md#6 获取,下载命令: + +```bash +cd ${PaddleOCR_root} +mkdir ckpt +wget -nc -P ckpt https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_train.tar +pushd ckpt/ +tar -xvf ch_PP-OCRv3_rec_train.tar +popd +``` + +- 模型评估 + +使用以下命令进行PP-OCRv3评估: + +```bash +python tools/eval.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml \ + -o Global.checkpoints=ckpt/ch_PP-OCRv3_rec_train/best_accuracy \ + Eval.dataset.data_dir=./data \ + Eval.dataset.label_file_list=["./data/val.list"] + +``` + +其中各参数含义如下: + +```bash +-c: 指定使用的配置文件,ch_PP-OCRv3_rec_distillation.yml对应于OCRv3识别模型。 +-o: 覆盖配置文件中参数 +Global.checkpoints: 指定评估使用的模型文件路径 +Eval.dataset.data_dir: 指定评估数据集路径 +Eval.dataset.label_file_list: 指定评估数据集文件列表 +``` + +## 5. 基于合成数据finetune + +### 5.1 Text Renderer数据合成方法 + +#### 5.1.1 下载Text Renderer代码 + +首先从github或gitee下载Text Renderer代码,并安装相关依赖。 + +```bash +git clone https://gitee.com/wowowoll/text_renderer.git + +# 安装依赖库 +cd text_renderer +pip install -r requirements.txt +``` + +使用text renderer合成数据之前需要准备好背景图片、语料以及字体库,下面将逐一介绍各个步骤。 + +#### 5.1.2 准备背景图片 + +观察日常生活中常见的包装生产日期图片,我们可以发现其背景相对简单。为此我们可以从网上找一下图片,截取部分图像块作为背景图像。 + +本项目已准备了部分图像作为背景图片,在第3部分完成数据准备后,可以得到我们准备好的背景图像,示例如下: + +![](https://ai-studio-static-online.cdn.bcebos.com/456ae2acb27d4a94896c478812aee0bc3551c703d7bd40c9be4dc983c7b3fc8a) + + + +背景图像存放于如下位置: + +```shell +PaddleOCR +├── data +| ├── bg # 合成数据所需背景图像 +``` + +#### 5.1.3 准备语料 + +观察测试集生产日期图像,我们可以知道如下数据有如下特点: +1. 由年月日组成,中间可能以“/”、“-”、“:”、“.”或者空格间隔,也可能以汉字年月日分隔 +2. 有些生产日期包含在产品批号中,此时可能包含具体时间、英文字母或数字标识 + +基于以上两点,我们编写语料生成脚本: + +```python +import random +from random import choice +import os + +cropus_num = 2000 #设置语料数量 + +def get_cropus(f): + # 随机生成年份 + year = random.randint(0, 22) + # 随机生成月份 + month = random.randint(1, 12) + # 随机生成日期 + day_dict = {31: [1,3,5,7,8,10,12], 30: [4,6,9,11], 28: [2]} + for item in day_dict: + if month in day_dict[item]: + day = random.randint(0, item) + # 随机生成小时 + hours = random.randint(0, 24) + # 随机生成分钟 + minute = random.randint(0, 60) + # 随机生成秒数 + second = random.randint(0, 60) + + # 随机生成产品标识字符 + length = random.randint(0, 6) + file_id = [] + flag = 0 + my_dict = [i for i in range(48,58)] + [j for j in range(40, 42)] + [k for k in range(65,90)] # 大小写字母 + 括号 + + for i in range(1, length): + if flag: + if i == flag+2: #括号匹配 + file_id.append(')') + flag = 0 + continue + sel = choice(my_dict) + if sel == 41: + continue + if sel == 40: + if i == 1 or i > length-3: + continue + flag = i + my_ascii = chr(sel) + file_id.append(my_ascii) + file_id_str = ''.join(file_id) + + #随机生成产品标识字符 + file_id2 = random.randint(0, 9) + + rad = random.random() + if rad < 0.3: + f.write('20{:02d}{:02d}{:02d} {}'.format(year, month, day, file_id_str)) + elif 0.3 < rad < 0.5: + f.write('20{:02d}年{:02d}月{:02d}日'.format(year, month, day)) + elif 0.5 < rad < 0.7: + f.write('20{:02d}/{:02d}/{:02d}'.format(year, month, day)) + elif 0.7 < rad < 0.8: + f.write('20{:02d}-{:02d}-{:02d}'.format(year, month, day)) + elif 0.8 < rad < 0.9: + f.write('20{:02d}.{:02d}.{:02d}'.format(year, month, day)) + else: + f.write('{:02d}:{:02d}:{:02d} {:02d}'.format(hours, minute, second, file_id2)) + +if __name__ == "__main__": + file_path = '/home/aistudio/text_renderer/my_data/cropus' + if not os.path.exists(file_path): + os.makedirs(file_path) + file_name = os.path.join(file_path, 'books.txt') + f = open(file_name, 'w') + for i in range(cropus_num): + get_cropus(f) + if i < cropus_num-1: + f.write('\n') + + f.close() +``` + +本项目已准备了部分语料,在第3部分完成数据准备后,可以得到我们准备好的语料库,默认位置如下: + +```shell +PaddleOCR +├── data +│ └── corpus #合成数据所需语料 +``` + +#### 5.1.4 下载字体 + +观察包装生产日期,我们可以发现其使用的字体为点阵体。字体可以在如下网址下载: +https://www.fonts.net.cn/fonts-en/tag-dianzhen-1.html + +本项目已准备了部分字体,在第3部分完成数据准备后,可以得到我们准备好的字体,默认位置如下: + +```shell +PaddleOCR +├── data +│ └── fonts #合成数据所需字体 +``` + +下载好字体后,还需要在list文件中指定字体文件存放路径,脚本如下: + +```bash +cd text_renderer/my_data/ +touch fonts.list +ls /home/aistudio/PaddleOCR/data/fonts/* > fonts.list +``` + +#### 5.1.5 运行数据合成命令 + +完成数据准备后,my_data文件结构如下: + +```shell +my_data/ +├── cropus +│ └── books.txt #语料库 +├── eng.txt #字符列表 +└── fonts.list #字体列表 +``` + +在运行合成数据命令之前,还有两处细节需要手动修改: +1. 将默认配置文件`text_renderer/configs/default.yaml`中第9行enable的值设为`true`,即允许合成彩色图像。否则合成的都是灰度图。 + +```yaml + # color boundary is in R,G,B format + font_color: ++ enable: true #false +``` + +2. 将`text_renderer/textrenderer/renderer.py`第184行作如下修改,取消padding。否则图片两端会有一些空白。 + +```python +padding = random.randint(s_bbox_width // 10, s_bbox_width // 8) #修改前 +padding = 0 #修改后 +``` + +运行数据合成命令: + +```bash +cd /home/aistudio/text_renderer/ +python main.py --num_img=3000 \ + --fonts_list='./my_data/fonts.list' \ + --corpus_dir "./my_data/cropus" \ + --corpus_mode "list" \ + --bg_dir "/home/aistudio/PaddleOCR/data/bg/" \ + --img_width 0 +``` + +合成好的数据默认保存在`text_renderer/output`目录下,可进入该目录查看合成的数据。 + + +合成数据示例如下 +![](https://ai-studio-static-online.cdn.bcebos.com/d686a48d465a43d09fbee51924fdca42ee21c50e676646da8559fb9967b94185) + +数据合成好后,还需要生成如下格式的训练所需的标注文件, +``` +图像路径 标签 +``` + +使用如下脚本即可生成标注文件: + +```python +import random + +abspath = '/home/aistudio/text_renderer/output/default/' + +#标注文件生成路径 +fout = open('./render_train.list', 'w', encoding='utf-8') + +with open('./output/default/tmp_labels.txt','r') as f: + lines = f.readlines() + for item in lines: + label = item[9:] + filename = item[:8] + '.jpg' + fout.write(abspath + filename + '\t' + label) + + fout.close() +``` + +经过以上步骤,我们便完成了包装生产日期数据合成。 +数据位于`text_renderer/output`,标注文件位于`text_renderer/render_train.list`。 + +本项目提供了生成好的数据供大家体验,完成步骤3的数据准备后,可得数据路径位于: + +```shell +PaddleOCR +├── data +│ ├── render_images # 合成数据示例 +│ ├── render_train.list #合成数据文件列表 +``` + +### 5.2 模型训练 + +准备好合成数据后,我们可以使用以下命令,利用合成数据进行finetune: +```bash +cd ${PaddleOCR_root} +python tools/train.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml \ + -o Global.pretrained_model=./ckpt/ch_PP-OCRv3_rec_train/best_accuracy \ + Global.epoch_num=20 \ + Global.eval_batch_step='[0, 20]' \ + Train.dataset.data_dir=./data \ + Train.dataset.label_file_list=['./data/render_train.list'] \ + Train.loader.batch_size_per_card=64 \ + Eval.dataset.data_dir=./data \ + Eval.dataset.label_file_list=["./data/val.list"] \ + Eval.loader.batch_size_per_card=64 + +``` + +其中各参数含义如下: + +```txt +-c: 指定使用的配置文件,ch_PP-OCRv3_rec_distillation.yml对应于OCRv3识别模型。 +-o: 覆盖配置文件中参数 +Global.pretrained_model: 指定finetune使用的预训练模型 +Global.epoch_num: 指定训练的epoch数 +Global.eval_batch_step: 间隔多少step做一次评估 +Train.dataset.data_dir: 训练数据集路径 +Train.dataset.label_file_list: 训练集文件列表 +Train.loader.batch_size_per_card: 训练单卡batch size +Eval.dataset.data_dir: 评估数据集路径 +Eval.dataset.label_file_list: 评估数据集文件列表 +Eval.loader.batch_size_per_card: 评估单卡batch size +``` + +## 6. 基于真实数据finetune + + +使用合成数据finetune能提升我们模型的识别精度,但由于合成数据和真实数据之间的分布可能有一定差异,因此作用有限。为进一步提高识别精度,本节介绍如何挖掘真实数据进行模型finetune。 + +数据挖掘的整体思路如下: +1. 使用python爬虫从网上获取大量无标签数据 +2. 使用模型从大量无标签数据中构建出有效训练集 + +### 6.1 python爬虫获取数据 + +- 推荐使用[爬虫工具](https://github.com/Joeclinton1/google-images-download)获取无标签图片。 + +图片获取后,可按如下目录格式组织: + +```txt +sprider +├── file.list +├── data +│ ├── 00000.jpg +│ ├── 00001.jpg +... +``` + +### 6.2 数据挖掘 + +我们使用PaddleOCR对获取到的图片进行挖掘,具体步骤如下: +1. 使用 PP-OCRv3检测模型+svtr-tiny识别模型,对每张图片进行预测。 +2. 使用数据挖掘策略,得到有效图片。 +3. 将有效图片对应的图像区域和标签提取出来,构建训练集。 + + +首先下载预训练模型,PP-OCRv3检测模型下载链接:https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar + +如需获取已训练模型,请加入PaddleX官方交流频道,获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 + + +完成下载后,可将模型存储于如下位置: + +```shell +PaddleOCR +├── data +│ ├── rec_vit_sub_64_363_all/ # svtr_tiny高精度识别模型 +``` + +```bash +# 下载解压PP-OCRv3检测模型 +cd ${PaddleOCR_root} +wget -nc -P ckpt https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar +pushd ckpt +tar -xvf ch_PP-OCRv3_det_infer.tar +popd ckpt +``` + +在使用PPOCRv3检测模型+svtr-tiny识别模型进行预测之前,有如下两处细节需要手动修改: +1. 将`tools/infer/predict_rec.py`中第110行`imgW`修改为`320` + +```python +#imgW = int((imgH * max_wh_ratio)) +imgW = 320 +``` + +2. 将`tools/infer/predict_system.py`第169行添加如下一行,将预测分数也写入结果文件中。 + +```python +"scores": rec_res[idx][1], +``` + +模型预测命令: +```bash +python tools/infer/predict_system.py \ + --image_dir="/home/aistudio/sprider/data" \ + --det_model_dir="./ckpt/ch_PP-OCRv3_det_infer/" \ + --rec_model_dir="/home/aistudio/PaddleOCR/data/rec_vit_sub_64_363_all/" \ + --rec_image_shape="3,32,320" +``` + +获得预测结果后,我们使用数据挖掘策略得到有效图片。具体挖掘策略如下: +1. 预测置信度高于95% +2. 识别结果包含字符‘20’,即年份 +3. 没有中文,或者有中文并且‘日’和'月'同时在识别结果中 + +```python +# 获取有效预测 + +import json +import re + +zh_pattern = re.compile(u'[\u4e00-\u9fa5]+') #正则表达式,筛选字符是否包含中文 + +file_path = '/home/aistudio/PaddleOCR/inference_results/system_results.txt' +out_path = '/home/aistudio/PaddleOCR/selected_results.txt' +f_out = open(out_path, 'w') + +with open(file_path, "r", encoding='utf-8') as fin: + lines = fin.readlines() + + +for line in lines: + flag = False + # 读取文件内容 + file_name, json_file = line.strip().split('\t') + preds = json.loads(json_file) + res = [] + for item in preds: + transcription = item['transcription'] #获取识别结果 + scores = item['scores'] #获取识别得分 + # 挖掘策略 + if scores > 0.95: + if '20' in transcription and len(transcription) > 4 and len(transcription) < 12: + word = transcription + if not(zh_pattern.search(word) and ('日' not in word or '月' not in word)): + flag = True + res.append(item) + save_pred = file_name + "\t" + json.dumps( + res, ensure_ascii=False) + "\n" + if flag ==True: + f_out.write(save_pred) + +f_out.close() +``` + +然后将有效预测对应的图像区域和标签提取出来,构建训练集。具体实现脚本如下: + +```python +import cv2 +import json +import numpy as np + +PATH = '/home/aistudio/PaddleOCR/inference_results/' #数据原始路径 +SAVE_PATH = '/home/aistudio/mining_images/' #裁剪后数据保存路径 +file_list = '/home/aistudio/PaddleOCR/selected_results.txt' #数据预测结果 +label_file = '/home/aistudio/mining_images/mining_train.list' #输出真实数据训练集标签list + +if not os.path.exists(SAVE_PATH): + os.mkdir(SAVE_PATH) + +f_label = open(label_file, 'w') + + +def get_rotate_crop_image(img, points): + """ + 根据检测结果points,从输入图像img中裁剪出相应的区域 + """ + assert len(points) == 4, "shape of points must be 4*2" + img_crop_width = int( + max( + np.linalg.norm(points[0] - points[1]), + np.linalg.norm(points[2] - points[3]))) + img_crop_height = int( + max( + np.linalg.norm(points[0] - points[3]), + np.linalg.norm(points[1] - points[2]))) + pts_std = np.float32([[0, 0], [img_crop_width, 0], + [img_crop_width, img_crop_height], + [0, img_crop_height]]) + M = cv2.getPerspectiveTransform(points, pts_std) + # 形变或倾斜,会做透视变换,reshape成矩形 + dst_img = cv2.warpPerspective( + img, + M, (img_crop_width, img_crop_height), + borderMode=cv2.BORDER_REPLICATE, + flags=cv2.INTER_CUBIC) + dst_img_height, dst_img_width = dst_img.shape[0:2] + if dst_img_height * 1.0 / dst_img_width >= 1.5: + dst_img = np.rot90(dst_img) + return dst_img + +def crop_and_get_filelist(file_list): + with open(file_list, "r", encoding='utf-8') as fin: + lines = fin.readlines() + + img_num = 0 + for line in lines: + img_name, json_file = line.strip().split('\t') + preds = json.loads(json_file) + for item in preds: + transcription = item['transcription'] + points = item['points'] + points = np.array(points).astype('float32') + #print('processing {}...'.format(img_name)) + + img = cv2.imread(PATH+img_name) + dst_img = get_rotate_crop_image(img, points) + h, w, c = dst_img.shape + newWidth = int((32. / h) * w) + newImg = cv2.resize(dst_img, (newWidth, 32)) + new_img_name = '{:05d}.jpg'.format(img_num) + cv2.imwrite(SAVE_PATH+new_img_name, dst_img) + f_label.write(SAVE_PATH+new_img_name+'\t'+transcription+'\n') + img_num += 1 + + +crop_and_get_filelist(file_list) +f_label.close() +``` + +### 6.3 模型训练 + +通过数据挖掘,我们得到了真实场景数据和对应的标签。接下来使用真实数据finetune,观察精度提升效果。 + + +利用真实数据进行finetune: + +```bash +cd ${PaddleOCR_root} +python tools/train.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml \ + -o Global.pretrained_model=./ckpt/ch_PP-OCRv3_rec_train/best_accuracy \ + Global.epoch_num=20 \ + Global.eval_batch_step='[0, 20]' \ + Train.dataset.data_dir=./data \ + Train.dataset.label_file_list=['./data/mining_train.list'] \ + Train.loader.batch_size_per_card=64 \ + Eval.dataset.data_dir=./data \ + Eval.dataset.label_file_list=["./data/val.list"] \ + Eval.loader.batch_size_per_card=64 +``` + +各参数含义参考第6部分合成数据finetune,只需要对训练数据路径做相应的修改: + +```txt +Train.dataset.data_dir: 训练数据集路径 +Train.dataset.label_file_list: 训练集文件列表 +``` + +示例使用我们提供的真实数据进行finetune,如想换成自己的数据,只需要相应的修改`Train.dataset.data_dir`和`Train.dataset.label_file_list`参数即可。 + +由于数据量不大,这里仅训练20个epoch即可。训练完成后,可以得到合成数据finetune后的精度为best acc=**71.33%**。 + +由于数量比较少,精度会比合成数据finetue的略低。 + + +## 7. 基于合成+真实数据finetune + +为进一步提升模型精度,我们结合使用合成数据和挖掘到的真实数据进行finetune。 + +利用合成+真实数据进行finetune,各参数含义参考第6部分合成数据finetune,只需要对训练数据路径做相应的修改: + +```txt +Train.dataset.data_dir: 训练数据集路径 +Train.dataset.label_file_list: 训练集文件列表 +``` + +生成训练list文件: +```bash +# 生成训练集文件list +cat /home/aistudio/PaddleOCR/data/render_train.list /home/aistudio/PaddleOCR/data/mining_train.list > /home/aistudio/PaddleOCR/data/render_mining_train.list +``` + +启动训练: +```bash +cd ${PaddleOCR_root} +python tools/train.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml \ + -o Global.pretrained_model=./ckpt/ch_PP-OCRv3_rec_train/best_accuracy \ + Global.epoch_num=40 \ + Global.eval_batch_step='[0, 20]' \ + Train.dataset.data_dir=./data \ + Train.dataset.label_file_list=['./data/render_mining_train.list'] \ + Train.loader.batch_size_per_card=64 \ + Eval.dataset.data_dir=./data \ + Eval.dataset.label_file_list=["./data/val.list"] \ + Eval.loader.batch_size_per_card=64 +``` + +示例使用我们提供的真实+合成数据进行finetune,如想换成自己的数据,只需要相应的修改Train.dataset.data_dir和Train.dataset.label_file_list参数即可。 + +由于数据量不大,这里仅训练40个epoch即可。训练完成后,可以得到合成数据finetune后的精度为best acc=**86.99%**。 + +可以看到,相较于原始PP-OCRv3的识别精度62.99%,使用合成数据+真实数据finetune后,识别精度能提升24%。 + +如需获取已训练模型,请加入PaddleX官方交流频道,获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 + + +模型的推理部署方法可以参考repo文档: https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/deploy/README_ch.md diff --git "a/ocr_webapp/PaddleOCR/applications/\345\215\260\347\253\240\345\274\257\346\233\262\346\226\207\345\255\227\350\257\206\345\210\253.md" "b/ocr_webapp/PaddleOCR/applications/\345\215\260\347\253\240\345\274\257\346\233\262\346\226\207\345\255\227\350\257\206\345\210\253.md" new file mode 100644 index 0000000000000000000000000000000000000000..fd1532fe3383d432b70d75c2470ab332ccc0836b --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\345\215\260\347\253\240\345\274\257\346\233\262\346\226\207\345\255\227\350\257\206\345\210\253.md" @@ -0,0 +1,1033 @@ +# 印章弯曲文字识别 + +- [1. 项目介绍](#1-----) +- [2. 环境搭建](#2-----) + * [2.1 准备PaddleDetection环境](#21---paddledetection--) + * [2.2 准备PaddleOCR环境](#22---paddleocr--) +- [3. 数据集准备](#3------) + * [3.1 数据标注](#31-----) + * [3.2 数据处理](#32-----) +- [4. 印章检测实践](#4-------) +- [5. 印章文字识别实践](#5---------) + * [5.1 端对端印章文字识别实践](#51------------) + * [5.2 两阶段印章文字识别实践](#52------------) + + [5.2.1 印章文字检测](#521-------) + + [5.2.2 印章文字识别](#522-------) + + +# 1. 项目介绍 + +弯曲文字识别在OCR任务中有着广泛的应用,比如:自然场景下的招牌,艺术文字,以及常见的印章文字识别。 + +在本项目中,将以印章识别任务为例,介绍如何使用PaddleDetection和PaddleOCR完成印章检测和印章文字识别任务。 + +项目难点: +1. 缺乏训练数据 +2. 图像质量参差不齐,图像模糊,文字不清晰 + +针对以上问题,本项目选用PaddleOCR里的PPOCRLabel工具完成数据标注。基于PaddleDetection完成印章区域检测,然后通过PaddleOCR里的端对端OCR算法和两阶段OCR算法分别完成印章文字识别任务。不同任务的精度效果如下: + + +| 任务 | 训练数据数量 | 精度 | +| -------- | - | -------- | +| 印章检测 | 1000 | 95.00% | +| 印章文字识别-端对端OCR方法 | 700 | 47.00% | +| 印章文字识别-两阶段OCR方法 | 700 | 55.00% | + +点击进入 [AI Studio 项目](https://aistudio.baidu.com/aistudio/projectdetail/4586113) + +# 2. 环境搭建 + +本项目需要准备PaddleDetection和PaddleOCR的项目运行环境,其中PaddleDetection用于实现印章检测任务,PaddleOCR用于实现文字识别任务 + + +## 2.1 准备PaddleDetection环境 + +下载PaddleDetection代码: +``` +!git clone https://github.com/PaddlePaddle/PaddleDetection.git +# 如果克隆github代码较慢,请从gitee上克隆代码 +#git clone https://gitee.com/PaddlePaddle/PaddleDetection.git +``` + +安装PaddleDetection依赖 +``` +!cd PaddleDetection && pip install -r requirements.txt +``` + +## 2.2 准备PaddleOCR环境 + +下载PaddleOCR代码: +``` +!git clone https://github.com/PaddlePaddle/PaddleOCR.git +# 如果克隆github代码较慢,请从gitee上克隆代码 +#git clone https://gitee.com/PaddlePaddle/PaddleOCR.git +``` + +安装PaddleOCR依赖 +``` +!cd PaddleOCR && git checkout dygraph && pip install -r requirements.txt +``` + +# 3. 数据集准备 + +## 3.1 数据标注 + +本项目中使用[PPOCRLabel](https://github.com/PaddlePaddle/PaddleOCR/tree/release/2.6/PPOCRLabel)工具标注印章检测数据,标注内容包括印章的位置以及印章中文字的位置和文字内容。 + + +注:PPOCRLabel的使用方法参考[文档](https://github.com/PaddlePaddle/PaddleOCR/tree/release/2.6/PPOCRLabel)。 + +PPOCRlabel标注印章数据步骤: +- 打开数据集所在文件夹 +- 按下快捷键Q进行4点(多点)标注——针对印章文本识别, + - 印章弯曲文字包围框采用偶数点标注(比如4点,8点,16点),按照阅读顺序,以16点标注为例,从文字左上方开始标注->到文字右上方标注8个点->到文字右下方->文字左下方8个点,一共8个点,形成包围曲线,参考下图。如果文字弯曲程度不高,为了减小标注工作量,可以采用4点、8点标注,需要注意的是,文字上下点数相同。(总点数尽量不要超过18个) + - 对于需要识别的印章中非弯曲文字,采用4点框标注即可 + - 对应包围框的文字部分默认是”待识别”,需要修改为包围框内的具体文字内容 +- 快捷键W进行矩形标注——针对印章区域检测,印章检测区域保证标注框包围整个印章,包围框对应文字可以设置为'印章区域',方便后续处理。 +- 针对印章中的水平文字可以视情况考虑矩形或四点标注:保证按行标注即可。如果背景文字与印章文字比较接近,标注时尽量避开背景文字。 +- 标注完成后修改右侧文本结果,确认无误后点击下方check(或CTRL+V),确认本张图片的标注。 +- 所有图片标注完成后,在顶部菜单栏点击File -> Export Label导出label.txt。 + +标注完成后,可视化效果如下: +![](https://ai-studio-static-online.cdn.bcebos.com/f5acbc4f50dd401a8f535ed6a263f94b0edff82c1aed4285836a9ead989b9c13) + +数据标注完成后,标签中包含印章检测的标注和印章文字识别的标注,如下所示: +``` +img/1.png [{"transcription": "印章区域", "points": [[87, 245], [214, 245], [214, 369], [87, 369]], "difficult": false}, {"transcription": "国家税务总局泸水市税务局第二税务分局", "points": [[110, 314], [116, 290], [131, 275], [152, 273], [170, 277], [181, 289], [186, 303], [186, 312], [201, 311], [198, 289], [189, 272], [175, 259], [152, 252], [124, 257], [100, 280], [94, 312]], "difficult": false}, {"transcription": "征税专用章", "points": [[117, 334], [183, 334], [183, 352], [117, 352]], "difficult": false}] +``` +标注中包含表示'印章区域'的坐标和'印章文字'坐标以及文字内容。 + + + +## 3.2 数据处理 + +标注时为了方便标注,没有区分印章区域的标注框和文字区域的标注框,可以通过python代码完成标签的划分。 + +在本项目的'/home/aistudio/work/seal_labeled_datas'目录下,存放了标注的数据示例,如下: + + +![](https://ai-studio-static-online.cdn.bcebos.com/3d762970e2184177a2c633695a31029332a4cd805631430ea797309492e45402) + +标签文件'/home/aistudio/work/seal_labeled_datas/Label.txt'中的标注内容如下: + +``` +img/test1.png [{"transcription": "待识别", "points": [[408, 232], [537, 232], [537, 352], [408, 352]], "difficult": false}, {"transcription": "电子回单", "points": [[437, 305], [504, 305], [504, 322], [437, 322]], "difficult": false}, {"transcription": "云南省农村信用社", "points": [[417, 290], [434, 295], [438, 281], [446, 267], [455, 261], [472, 258], [489, 264], [498, 277], [502, 295], [526, 289], [518, 267], [503, 249], [475, 232], [446, 239], [429, 255], [418, 275]], "difficult": false}, {"transcription": "专用章", "points": [[437, 319], [503, 319], [503, 338], [437, 338]], "difficult": false}] +``` + + +为了方便训练,我们需要通过python代码将用于训练印章检测和训练印章文字识别的标注区分开。 + + +``` +import numpy as np +import json +import cv2 +import os +from shapely.geometry import Polygon + + +def poly2box(poly): + xmin = np.min(np.array(poly)[:, 0]) + ymin = np.min(np.array(poly)[:, 1]) + xmax = np.max(np.array(poly)[:, 0]) + ymax = np.max(np.array(poly)[:, 1]) + return np.array([[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]]) + + +def draw_text_det_res(dt_boxes, src_im, color=(255, 255, 0)): + for box in dt_boxes: + box = np.array(box).astype(np.int32).reshape(-1, 2) + cv2.polylines(src_im, [box], True, color=color, thickness=2) + return src_im + +class LabelDecode(object): + def __init__(self, **kwargs): + pass + + def __call__(self, data): + label = json.loads(data['label']) + + nBox = len(label) + seal_boxes = self.get_seal_boxes(label) + + gt_label = [] + + for seal_box in seal_boxes: + seal_anno = {'seal_box': seal_box} + boxes, txts, txt_tags = [], [], [] + + for bno in range(0, nBox): + box = label[bno]['points'] + txt = label[bno]['transcription'] + try: + ints = self.get_intersection(box, seal_box) + except Exception as E: + print(E) + continue + + if abs(Polygon(box).area - self.get_intersection(box, seal_box)) < 1e-3 and \ + abs(Polygon(box).area - self.get_union(box, seal_box)) > 1e-3: + + boxes.append(box) + txts.append(txt) + if txt in ['*', '###', '待识别']: + txt_tags.append(True) + else: + txt_tags.append(False) + + seal_anno['polys'] = boxes + seal_anno['texts'] = txts + seal_anno['ignore_tags'] = txt_tags + + gt_label.append(seal_anno) + + return gt_label + + def get_seal_boxes(self, label): + + nBox = len(label) + seal_box = [] + for bno in range(0, nBox): + box = label[bno]['points'] + if len(box) == 4: + seal_box.append(box) + + if len(seal_box) == 0: + return None + + seal_box = self.valid_seal_box(seal_box) + return seal_box + + + def is_seal_box(self, box, boxes): + is_seal = True + for poly in boxes: + if list(box.shape()) != list(box.shape.shape()): + if abs(Polygon(box).area - self.get_intersection(box, poly)) < 1e-3: + return False + else: + if np.sum(np.array(box) - np.array(poly)) < 1e-3: + # continue when the box is same with poly + continue + if abs(Polygon(box).area - self.get_intersection(box, poly)) < 1e-3: + return False + return is_seal + + + def valid_seal_box(self, boxes): + if len(boxes) == 1: + return boxes + + new_boxes = [] + flag = True + for k in range(0, len(boxes)): + flag = True + tmp_box = boxes[k] + for i in range(0, len(boxes)): + if k == i: continue + if abs(Polygon(tmp_box).area - self.get_intersection(tmp_box, boxes[i])) < 1e-3: + flag = False + continue + if flag: + new_boxes.append(tmp_box) + + return new_boxes + + + def get_union(self, pD, pG): + return Polygon(pD).union(Polygon(pG)).area + + def get_intersection_over_union(self, pD, pG): + return get_intersection(pD, pG) / get_union(pD, pG) + + def get_intersection(self, pD, pG): + return Polygon(pD).intersection(Polygon(pG)).area + + def expand_points_num(self, boxes): + max_points_num = 0 + for box in boxes: + if len(box) > max_points_num: + max_points_num = len(box) + ex_boxes = [] + for box in boxes: + ex_box = box + [box[-1]] * (max_points_num - len(box)) + ex_boxes.append(ex_box) + return ex_boxes + + +def gen_extract_label(data_dir, label_file, seal_gt, seal_ppocr_gt): + label_decode_func = LabelDecode() + gts = open(label_file, "r").readlines() + + seal_gt_list = [] + seal_ppocr_list = [] + + for idx, line in enumerate(gts): + img_path, label = line.strip().split("\t") + data = {'label': label, 'img_path':img_path} + res = label_decode_func(data) + src_img = cv2.imread(os.path.join(data_dir, img_path)) + if res is None: + print("ERROR! res is None!") + continue + + anno = [] + for i, gt in enumerate(res): + # print(i, box, type(box), ) + anno.append({'polys': gt['seal_box'], 'cls':1}) + + seal_gt_list.append(f"{img_path}\t{json.dumps(anno)}\n") + seal_ppocr_list.append(f"{img_path}\t{json.dumps(res)}\n") + + if not os.path.exists(os.path.dirname(seal_gt)): + os.makedirs(os.path.dirname(seal_gt)) + if not os.path.exists(os.path.dirname(seal_ppocr_gt)): + os.makedirs(os.path.dirname(seal_ppocr_gt)) + + with open(seal_gt, "w") as f: + f.writelines(seal_gt_list) + f.close() + + with open(seal_ppocr_gt, 'w') as f: + f.writelines(seal_ppocr_list) + f.close() + +def vis_seal_ppocr(data_dir, label_file, save_dir): + + datas = open(label_file, 'r').readlines() + for idx, line in enumerate(datas): + img_path, label = line.strip().split('\t') + img_path = os.path.join(data_dir, img_path) + + label = json.loads(label) + src_im = cv2.imread(img_path) + if src_im is None: + continue + + for anno in label: + seal_box = anno['seal_box'] + txt_boxes = anno['polys'] + + # vis seal box + src_im = draw_text_det_res([seal_box], src_im, color=(255, 255, 0)) + src_im = draw_text_det_res(txt_boxes, src_im, color=(255, 0, 0)) + + save_path = os.path.join(save_dir, os.path.basename(img_path)) + if not os.path.exists(save_dir): + os.makedirs(save_dir) + # print(src_im.shape) + cv2.imwrite(save_path, src_im) + + +def draw_html(img_dir, save_name): + import glob + + images_dir = glob.glob(img_dir + "/*") + print(len(images_dir)) + + html_path = save_name + with open(html_path, 'w') as html: + html.write('\n\n') + html.write('\n') + html.write("") + + html.write("\n") + html.write(f'\n") + html.write(f'' % (base)) + html.write("\n") + + html.write('\n') + html.write('
\n GT') + + for i, filename in enumerate(sorted(images_dir)): + if filename.endswith("txt"): continue + print(filename) + + base = "{}".format(filename) + if True: + html.write("
{filename}\n GT') + html.write('GT 310\n
\n') + html.write('\n\n') + print("ok") + + +def crop_seal_from_img(label_file, data_dir, save_dir, save_gt_path): + + if not os.path.exists(save_dir): + os.makedirs(save_dir) + + datas = open(label_file, 'r').readlines() + all_gts = [] + count = 0 + for idx, line in enumerate(datas): + img_path, label = line.strip().split('\t') + img_path = os.path.join(data_dir, img_path) + + label = json.loads(label) + src_im = cv2.imread(img_path) + if src_im is None: + continue + + for c, anno in enumerate(label): + seal_poly = anno['seal_box'] + txt_boxes = anno['polys'] + txts = anno['texts'] + ignore_tags = anno['ignore_tags'] + + box = poly2box(seal_poly) + img_crop = src_im[box[0][1]:box[2][1], box[0][0]:box[2][0], :] + + save_path = os.path.join(save_dir, f"{idx}_{c}.jpg") + cv2.imwrite(save_path, np.array(img_crop)) + + img_gt = [] + for i in range(len(txts)): + txt_boxes_crop = np.array(txt_boxes[i]) + txt_boxes_crop[:, 1] -= box[0, 1] + txt_boxes_crop[:, 0] -= box[0, 0] + img_gt.append({'transcription': txts[i], "points": txt_boxes_crop.tolist(), "ignore_tag": ignore_tags[i]}) + + if len(img_gt) >= 1: + count += 1 + save_gt = f"{os.path.basename(save_path)}\t{json.dumps(img_gt)}\n" + + all_gts.append(save_gt) + + print(f"The num of all image: {len(all_gts)}, and the number of useful image: {count}") + if not os.path.exists(os.path.dirname(save_gt_path)): + os.makedirs(os.path.dirname(save_gt_path)) + + with open(save_gt_path, "w") as f: + f.writelines(all_gts) + f.close() + print("Done") + + + +if __name__ == "__main__": + + # 数据处理 + gen_extract_label("./seal_labeled_datas", "./seal_labeled_datas/Label.txt", "./seal_ppocr_gt/seal_det_img.txt", "./seal_ppocr_gt/seal_ppocr_img.txt") + vis_seal_ppocr("./seal_labeled_datas", "./seal_ppocr_gt/seal_ppocr_img.txt", "./seal_ppocr_gt/seal_ppocr_vis/") + draw_html("./seal_ppocr_gt/seal_ppocr_vis/", "./vis_seal_ppocr.html") + seal_ppocr_img_label = "./seal_ppocr_gt/seal_ppocr_img.txt" + crop_seal_from_img(seal_ppocr_img_label, "./seal_labeled_datas/", "./seal_img_crop", "./seal_img_crop/label.txt") + +``` + +处理完成后,生成的文件如下: +``` +├── seal_img_crop/ +│ ├── 0_0.jpg +│ ├── ... +│ └── label.txt +├── seal_ppocr_gt/ +│ ├── seal_det_img.txt +│ ├── seal_ppocr_img.txt +│ └── seal_ppocr_vis/ +│ ├── test1.png +│ ├── ... +└── vis_seal_ppocr.html + +``` +其中`seal_img_crop/label.txt`文件为印章识别标签文件,其内容格式为: +``` +0_0.jpg [{"transcription": "\u7535\u5b50\u56de\u5355", "points": [[29, 73], [96, 73], [96, 90], [29, 90]], "ignore_tag": false}, {"transcription": "\u4e91\u5357\u7701\u519c\u6751\u4fe1\u7528\u793e", "points": [[9, 58], [26, 63], [30, 49], [38, 35], [47, 29], [64, 26], [81, 32], [90, 45], [94, 63], [118, 57], [110, 35], [95, 17], [67, 0], [38, 7], [21, 23], [10, 43]], "ignore_tag": false}, {"transcription": "\u4e13\u7528\u7ae0", "points": [[29, 87], [95, 87], [95, 106], [29, 106]], "ignore_tag": false}] +``` +可以直接用于PaddleOCR的PGNet算法的训练。 + +`seal_ppocr_gt/seal_det_img.txt`为印章检测标签文件,其内容格式为: +``` +img/test1.png [{"polys": [[408, 232], [537, 232], [537, 352], [408, 352]], "cls": 1}] +``` +为了使用PaddleDetection工具完成印章检测模型的训练,需要将`seal_det_img.txt`转换为COCO或者VOC的数据标注格式。 + +可以直接使用下述代码将印章检测标注转换成VOC格式。 + + +``` +import numpy as np +import json +import cv2 +import os +from shapely.geometry import Polygon + +seal_train_gt = "./seal_ppocr_gt/seal_det_img.txt" +# 注:仅用于示例,实际使用中需要分别转换训练集和测试集的标签 +seal_valid_gt = "./seal_ppocr_gt/seal_det_img.txt" + +def gen_main_train_txt(mode='train'): + if mode == "train": + file_path = seal_train_gt + if mode in ['valid', 'test']: + file_path = seal_valid_gt + + save_path = f"./seal_VOC/ImageSets/Main/{mode}.txt" + save_train_path = f"./seal_VOC/{mode}.txt" + if not os.path.exists(os.path.dirname(save_path)): + os.makedirs(os.path.dirname(save_path)) + + datas = open(file_path, 'r').readlines() + img_names = [] + train_names = [] + for line in datas: + img_name = line.strip().split('\t')[0] + img_name = os.path.basename(img_name) + (i_name, extension) = os.path.splitext(img_name) + t_name = 'JPEGImages/'+str(img_name)+' '+'Annotations/'+str(i_name)+'.xml\n' + train_names.append(t_name) + img_names.append(i_name + "\n") + + with open(save_train_path, "w") as f: + f.writelines(train_names) + f.close() + + with open(save_path, "w") as f: + f.writelines(img_names) + f.close() + + print(f"{mode} save done") + + +def gen_xml_label(mode='train'): + if mode == "train": + file_path = seal_train_gt + if mode in ['valid', 'test']: + file_path = seal_valid_gt + + datas = open(file_path, 'r').readlines() + img_names = [] + train_names = [] + anno_path = "./seal_VOC/Annotations" + img_path = "./seal_VOC/JPEGImages" + + if not os.path.exists(anno_path): + os.makedirs(anno_path) + if not os.path.exists(img_path): + os.makedirs(img_path) + + for idx, line in enumerate(datas): + img_name, label = line.strip().split('\t') + img = cv2.imread(os.path.join("./seal_labeled_datas", img_name)) + cv2.imwrite(os.path.join(img_path, os.path.basename(img_name)), img) + height, width, c = img.shape + img_name = os.path.basename(img_name) + (i_name, extension) = os.path.splitext(img_name) + label = json.loads(label) + + xml_file = open(("./seal_VOC/Annotations" + '/' + i_name + '.xml'), 'w') + xml_file.write('\n') + xml_file.write(' seal_VOC\n') + xml_file.write(' ' + str(img_name) + '\n') + xml_file.write(' ' + 'Annotations/' + str(img_name) + '\n') + xml_file.write(' \n') + xml_file.write(' ' + str(width) + '\n') + xml_file.write(' ' + str(height) + '\n') + xml_file.write(' 3\n') + xml_file.write(' \n') + xml_file.write(' 0\n') + + for anno in label: + poly = anno['polys'] + if anno['cls'] == 1: + gt_cls = 'redseal' + xmin = np.min(np.array(poly)[:, 0]) + ymin = np.min(np.array(poly)[:, 1]) + xmax = np.max(np.array(poly)[:, 0]) + ymax = np.max(np.array(poly)[:, 1]) + xmin,ymin,xmax,ymax= int(xmin),int(ymin),int(xmax),int(ymax) + xml_file.write(' \n') + xml_file.write(' '+str(gt_cls)+'\n') + xml_file.write(' Unspecified\n') + xml_file.write(' 0\n') + xml_file.write(' 0\n') + xml_file.write(' \n') + xml_file.write(' '+str(xmin)+'\n') + xml_file.write(' '+str(ymin)+'\n') + xml_file.write(' '+str(xmax)+'\n') + xml_file.write(' '+str(ymax)+'\n') + xml_file.write(' \n') + xml_file.write(' \n') + xml_file.write('') + xml_file.close() + print(f'{mode} xml save done!') + + +gen_main_train_txt() +gen_main_train_txt('valid') +gen_xml_label('train') +gen_xml_label('valid') + +``` + +数据处理完成后,转换为VOC格式的印章检测数据存储在~/data/seal_VOC目录下,目录组织结构为: + +``` +├── Annotations/ +├── ImageSets/ +│   └── Main/ +│   ├── train.txt +│   └── valid.txt +├── JPEGImages/ +├── train.txt +└── valid.txt +└── label_list.txt +``` + +Annotations下为数据的标签,JPEGImages目录下为图像文件,label_list.txt为标注检测框类别标签文件。 + +在接下来一节中,将介绍如何使用PaddleDetection工具库完成印章检测模型的训练。 + +# 4. 印章检测实践 + +在实际应用中,印章多是出现在合同,发票,公告等场景中,印章文字识别的任务需要排除图像中背景文字的影响,因此需要先检测出图像中的印章区域。 + + +借助PaddleDetection目标检测库可以很容易的实现印章检测任务,使用PaddleDetection训练印章检测任务流程如下: + +- 选择算法 +- 修改数据集配置路径 +- 启动训练 + + +**算法选择** + +PaddleDetection中有许多检测算法可以选择,考虑到每条数据中印章区域较为清晰,且考虑到性能需求。在本项目中,我们采用mobilenetv3为backbone的ppyolo算法完成印章检测任务,对应的配置文件是:configs/ppyolo/ppyolo_mbv3_large.yml + + + +**修改配置文件** + +配置文件中的默认数据路径是COCO, +需要修改为印章检测的数据路径,主要修改如下: +在配置文件'configs/ppyolo/ppyolo_mbv3_large.yml'末尾增加如下内容: +``` +metric: VOC +map_type: 11point +num_classes: 2 + +TrainDataset: + !VOCDataSet + dataset_dir: dataset/seal_VOC + anno_path: train.txt + label_list: label_list.txt + data_fields: ['image', 'gt_bbox', 'gt_class', 'difficult'] + +EvalDataset: + !VOCDataSet + dataset_dir: dataset/seal_VOC + anno_path: test.txt + label_list: label_list.txt + data_fields: ['image', 'gt_bbox', 'gt_class', 'difficult'] + +TestDataset: + !ImageFolder + anno_path: dataset/seal_VOC/label_list.txt +``` + +配置文件中设置的数据路径在PaddleDetection/dataset目录下,我们可以将处理后的印章检测训练数据移动到PaddleDetection/dataset目录下或者创建一个软连接。 + +``` +!ln -s seal_VOC ./PaddleDetection/dataset/ +``` + +另外图象中印章数量比较少,可以调整NMS后处理的检测框数量,即keep_top_k,nms_top_k 从100,1000,调整为10,100。在配置文件'configs/ppyolo/ppyolo_mbv3_large.yml'末尾增加如下内容完成后处理参数的调整 +``` +BBoxPostProcess: + decode: + name: YOLOBox + conf_thresh: 0.005 + downsample_ratio: 32 + clip_bbox: true + scale_x_y: 1.05 + nms: + name: MultiClassNMS + keep_top_k: 10 # 修改前100 + nms_threshold: 0.45 + nms_top_k: 100 # 修改前1000 + score_threshold: 0.005 +``` + + +修改完成后,需要在PaddleDetection中增加印章数据的处理代码,即在PaddleDetection/ppdet/data/source/目录下创建seal.py文件,文件中填充如下代码: +``` +import os +import numpy as np +from ppdet.core.workspace import register, serializable +from .dataset import DetDataset +import cv2 +import json + +from ppdet.utils.logger import setup_logger +logger = setup_logger(__name__) + + +@register +@serializable +class SealDataSet(DetDataset): + """ + Load dataset with COCO format. + + Args: + dataset_dir (str): root directory for dataset. + image_dir (str): directory for images. + anno_path (str): coco annotation file path. + data_fields (list): key name of data dictionary, at least have 'image'. + sample_num (int): number of samples to load, -1 means all. + load_crowd (bool): whether to load crowded ground-truth. + False as default + allow_empty (bool): whether to load empty entry. False as default + empty_ratio (float): the ratio of empty record number to total + record's, if empty_ratio is out of [0. ,1.), do not sample the + records and use all the empty entries. 1. as default + """ + + def __init__(self, + dataset_dir=None, + image_dir=None, + anno_path=None, + data_fields=['image'], + sample_num=-1, + load_crowd=False, + allow_empty=False, + empty_ratio=1.): + super(SealDataSet, self).__init__(dataset_dir, image_dir, anno_path, + data_fields, sample_num) + self.load_image_only = False + self.load_semantic = False + self.load_crowd = load_crowd + self.allow_empty = allow_empty + self.empty_ratio = empty_ratio + + def _sample_empty(self, records, num): + # if empty_ratio is out of [0. ,1.), do not sample the records + if self.empty_ratio < 0. or self.empty_ratio >= 1.: + return records + import random + sample_num = min( + int(num * self.empty_ratio / (1 - self.empty_ratio)), len(records)) + records = random.sample(records, sample_num) + return records + + def parse_dataset(self): + anno_path = os.path.join(self.dataset_dir, self.anno_path) + image_dir = os.path.join(self.dataset_dir, self.image_dir) + + records = [] + empty_records = [] + ct = 0 + + assert anno_path.endswith('.txt'), \ + 'invalid seal_gt file: ' + anno_path + + all_datas = open(anno_path, 'r').readlines() + + for idx, line in enumerate(all_datas): + im_path, label = line.strip().split('\t') + img_path = os.path.join(image_dir, im_path) + label = json.loads(label) + im_h, im_w, im_c = cv2.imread(img_path).shape + + coco_rec = { + 'im_file': img_path, + 'im_id': np.array([idx]), + 'h': im_h, + 'w': im_w, + } if 'image' in self.data_fields else {} + + if not self.load_image_only: + bboxes = [] + for anno in label: + poly = anno['polys'] + # poly to box + x1 = np.min(np.array(poly)[:, 0]) + y1 = np.min(np.array(poly)[:, 1]) + x2 = np.max(np.array(poly)[:, 0]) + y2 = np.max(np.array(poly)[:, 1]) + eps = 1e-5 + if x2 - x1 > eps and y2 - y1 > eps: + clean_box = [ + round(float(x), 3) for x in [x1, y1, x2, y2] + ] + anno = {'clean_box': clean_box, 'gt_cls':int(anno['cls'])} + bboxes.append(anno) + else: + logger.info("invalid box") + + num_bbox = len(bboxes) + if num_bbox <= 0: + continue + + gt_bbox = np.zeros((num_bbox, 4), dtype=np.float32) + gt_class = np.zeros((num_bbox, 1), dtype=np.int32) + is_crowd = np.zeros((num_bbox, 1), dtype=np.int32) + # gt_poly = [None] * num_bbox + + for i, box in enumerate(bboxes): + gt_class[i][0] = box['gt_cls'] + gt_bbox[i, :] = box['clean_box'] + is_crowd[i][0] = 0 + + gt_rec = { + 'is_crowd': is_crowd, + 'gt_class': gt_class, + 'gt_bbox': gt_bbox, + # 'gt_poly': gt_poly, + } + + for k, v in gt_rec.items(): + if k in self.data_fields: + coco_rec[k] = v + + records.append(coco_rec) + ct += 1 + if self.sample_num > 0 and ct >= self.sample_num: + break + self.roidbs = records +``` + +**启动训练** + +启动单卡训练的命令为: +``` +!python3 tools/train.py -c configs/ppyolo/ppyolo_mbv3_large.yml --eval + +# 分布式训练命令为: +!python3 -m paddle.distributed.launch --gpus 0,1,2,3,4,5,6,7 tools/train.py -c configs/ppyolo/ppyolo_mbv3_large.yml --eval +``` + +训练完成后,日志中会打印模型的精度: + +``` +[07/05 11:42:09] ppdet.engine INFO: Eval iter: 0 +[07/05 11:42:14] ppdet.metrics.metrics INFO: Accumulating evaluatation results... +[07/05 11:42:14] ppdet.metrics.metrics INFO: mAP(0.50, 11point) = 99.31% +[07/05 11:42:14] ppdet.engine INFO: Total sample number: 112, averge FPS: 26.45840794253432 +[07/05 11:42:14] ppdet.engine INFO: Best test bbox ap is 0.996. +``` + + +我们可以使用训练好的模型观察预测结果: +``` +!python3 tools/infer.py -c configs/ppyolo/ppyolo_mbv3_large.yml -o weights=./output/ppyolo_mbv3_large/model_final.pdparams --img_dir=./test.jpg +``` +预测结果如下: + +![](https://ai-studio-static-online.cdn.bcebos.com/0f650c032b0f4d56bd639713924768cc820635e9977845008d233f465291a29e) + +# 5. 印章文字识别实践 + +在使用ppyolo检测到印章区域后,接下来借助PaddleOCR里的文字识别能力,完成印章中文字的识别。 + +PaddleOCR中的OCR算法包含文字检测算法,文字识别算法以及OCR端对端算法。 + +文字检测算法负责检测到图像中的文字,再由文字识别模型识别出检测到的文字,进而实现OCR的任务。文字检测+文字识别串联完成OCR任务的架构称为两阶段的OCR算法。相对应的端对端的OCR方法可以用一个算法同时完成文字检测和识别的任务。 + + +| 文字检测 | 文字识别 | 端对端算法 | +| -------- | -------- | -------- | +| DB\DB++\EAST\SAST\PSENet | SVTR\CRNN\NRTN\Abinet\SAR\... | PGNet | + + +本节中将分别介绍端对端的文字检测识别算法以及两阶段的文字检测识别算法在印章检测识别任务上的实践。 + + +## 5.1 端对端印章文字识别实践 + +本节介绍使用PaddleOCR里的PGNet算法完成印章文字识别。 + +PGNet属于端对端的文字检测识别算法,在PaddleOCR中的配置文件为: +[PaddleOCR/configs/e2e/e2e_r50_vd_pg.yml](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.6/configs/e2e/e2e_r50_vd_pg.yml) + +使用PGNet完成文字检测识别任务的步骤为: +- 修改配置文件 +- 启动训练 + +PGNet默认配置文件的数据路径为totaltext数据集路径,本次训练中,需要修改为上一节数据处理后得到的标签文件和数据目录: + +训练数据配置修改后如下: +``` +Train: + dataset: + name: PGDataSet + data_dir: ./train_data/seal_ppocr + label_file_list: [./train_data/seal_ppocr/seal_ppocr_img.txt] + ratio_list: [1.0] +``` +测试数据集配置修改后如下: +``` +Eval: + dataset: + name: PGDataSet + data_dir: ./train_data/seal_ppocr_test + label_file_list: [./train_data/seal_ppocr_test/seal_ppocr_img.txt] +``` + +启动训练的命令为: +``` +!python3 tools/train.py -c configs/e2e/e2e_r50_vd_pg.yml +``` +模型训练完成后,可以得到最终的精度为47.4%。数据量较少,以及数据质量较差会影响模型的训练精度,如果有更多的数据参与训练,精度将进一步提升。 + +如需获取已训练模型,请点击文末的链接,加入官方交流群获取全部OCR垂类模型下载链接、《动手学OCR》电子书等全套OCR学习资料🎁 + + +## 5.2 两阶段印章文字识别实践 + +上一节介绍了使用PGNet实现印章识别任务的训练流程。本小节将介绍使用PaddleOCR里的文字检测和文字识别算法分别完成印章文字的检测和识别。 + +### 5.2.1 印章文字检测 + +PaddleOCR中包含丰富的文字检测算法,包含DB,DB++,EAST,SAST,PSENet等等。其中DB,DB++,PSENet均支持弯曲文字检测,本项目中,使用DB++作为印章弯曲文字检测算法。 + +PaddleOCR中发布的db++文字检测算法模型是英文文本检测模型,因此需要重新训练模型。 + + +修改[DB++配置文件](DB++的默认配置文件位于[configs/det/det_r50_db++_icdar15.yml](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.6/configs/det/det_r50_db%2B%2B_icdar15.yml) +中的数据路径: + + +``` +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/seal_ppocr + label_file_list: [./train_data/seal_ppocr/seal_ppocr_img.txt] + ratio_list: [1.0] +``` +测试数据集配置修改后如下: +``` +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/seal_ppocr_test + label_file_list: [./train_data/seal_ppocr_test/seal_ppocr_img.txt] +``` + + +启动训练: +``` +!python3 tools/train.py -c configs/det/det_r50_db++_icdar15.yml -o Global.epoch_num=100 +``` + +考虑到数据较少,通过Global.epoch_num设置仅训练100个epoch。 +模型训练完成后,在测试集上预测的可视化效果如下: + +![](https://ai-studio-static-online.cdn.bcebos.com/498119182f0a414ab86ae2de752fa31c9ddc3a74a76847049cc57884602cb269) + + +如需获取已训练模型,请点击文末的链接,加入官方交流群获取全部OCR垂类模型下载链接、《动手学OCR》电子书等全套OCR学习资料🎁 + + +### 5.2.2 印章文字识别 + +上一节中完成了印章文字的检测模型训练,本节介绍印章文字识别模型的训练。识别模型采用SVTR算法,SVTR算法是IJCAI收录的文字识别算法,SVTR模型具备超轻量高精度的特点。 + +在启动训练之前,需要准备印章文字识别需要的数据集,需要使用如下代码,将印章中的文字区域剪切出来构建训练集。 + +``` +import cv2 +import numpy as np + +def get_rotate_crop_image(img, points): + ''' + img_height, img_width = img.shape[0:2] + left = int(np.min(points[:, 0])) + right = int(np.max(points[:, 0])) + top = int(np.min(points[:, 1])) + bottom = int(np.max(points[:, 1])) + img_crop = img[top:bottom, left:right, :].copy() + points[:, 0] = points[:, 0] - left + points[:, 1] = points[:, 1] - top + ''' + assert len(points) == 4, "shape of points must be 4*2" + img_crop_width = int( + max( + np.linalg.norm(points[0] - points[1]), + np.linalg.norm(points[2] - points[3]))) + img_crop_height = int( + max( + np.linalg.norm(points[0] - points[3]), + np.linalg.norm(points[1] - points[2]))) + pts_std = np.float32([[0, 0], [img_crop_width, 0], + [img_crop_width, img_crop_height], + [0, img_crop_height]]) + M = cv2.getPerspectiveTransform(points, pts_std) + dst_img = cv2.warpPerspective( + img, + M, (img_crop_width, img_crop_height), + borderMode=cv2.BORDER_REPLICATE, + flags=cv2.INTER_CUBIC) + dst_img_height, dst_img_width = dst_img.shape[0:2] + if dst_img_height * 1.0 / dst_img_width >= 1.5: + dst_img = np.rot90(dst_img) + return dst_img + + +def run(data_dir, label_file, save_dir): + datas = open(label_file, 'r').readlines() + for idx, line in enumerate(datas): + img_path, label = line.strip().split('\t') + img_path = os.path.join(data_dir, img_path) + + label = json.loads(label) + src_im = cv2.imread(img_path) + if src_im is None: + continue + + for anno in label: + seal_box = anno['seal_box'] + txt_boxes = anno['polys'] + crop_im = get_rotate_crop_image(src_im, text_boxes) + + save_path = os.path.join(save_dir, f'{idx}.png') + if not os.path.exists(save_dir): + os.makedirs(save_dir) + # print(src_im.shape) + cv2.imwrite(save_path, crop_im) + +``` + + +数据处理完成后,即可配置训练的配置文件。SVTR配置文件选择[configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.6/configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml) +修改SVTR配置文件中的训练数据部分如下: + +``` +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/seal_ppocr_crop/ + label_file_list: + - ./train_data/seal_ppocr_crop/train_list.txt +``` + +修改预测部分配置文件: +``` +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/seal_ppocr_crop/ + label_file_list: + - ./train_data/seal_ppocr_crop_test/train_list.txt +``` + +启动训练: + +``` +!python3 tools/train.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml + +``` + +训练完成后可以发现测试集指标达到了61%。 +由于数据较少,训练时会发现在训练集上的acc指标远大于测试集上的acc指标,即出现过拟合现象。通过补充数据和一些数据增强可以缓解这个问题。 + + + +如需获取已训练模型,请加入PaddleX官方交流频道,获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 diff --git "a/ocr_webapp/PaddleOCR/applications/\345\217\221\347\245\250\345\205\263\351\224\256\344\277\241\346\201\257\346\212\275\345\217\226.md" "b/ocr_webapp/PaddleOCR/applications/\345\217\221\347\245\250\345\205\263\351\224\256\344\277\241\346\201\257\346\212\275\345\217\226.md" new file mode 100644 index 0000000000000000000000000000000000000000..c463956d9e3634eabc18824b1aaf4be78a208088 --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\345\217\221\347\245\250\345\205\263\351\224\256\344\277\241\346\201\257\346\212\275\345\217\226.md" @@ -0,0 +1,342 @@ + +# 基于VI-LayoutXLM的发票关键信息抽取 + +- [1. 项目背景及意义](#1-项目背景及意义) +- [2. 项目内容](#2-项目内容) +- [3. 安装环境](#3-安装环境) +- [4. 关键信息抽取](#4-关键信息抽取) + - [4.1 文本检测](#41-文本检测) + - [4.2 文本识别](#42-文本识别) + - [4.3 语义实体识别](#43-语义实体识别) + - [4.4 关系抽取](#44-关系抽取) + + + +## 1. 项目背景及意义 + +关键信息抽取在文档场景中被广泛使用,如身份证中的姓名、住址信息抽取,快递单中的姓名、联系方式等关键字段内容的抽取。传统基于模板匹配的方案需要针对不同的场景制定模板并进行适配,较为繁琐,不够鲁棒。基于该问题,我们借助飞桨提供的PaddleOCR套件中的关键信息抽取方案,实现对增值税发票场景的关键信息抽取。 + +## 2. 项目内容 + +本项目基于PaddleOCR开源套件,以VI-LayoutXLM多模态关键信息抽取模型为基础,针对增值税发票场景进行适配,提取该场景的关键信息。 + +## 3. 安装环境 + +```bash +# 首先git官方的PaddleOCR项目,安装需要的依赖 +# 第一次运行打开该注释 +git clone https://gitee.com/PaddlePaddle/PaddleOCR.git +cd PaddleOCR +# 安装PaddleOCR的依赖 +pip install -r requirements.txt +# 安装关键信息抽取任务的依赖 +pip install -r ./ppstructure/kie/requirements.txt +``` + +## 4. 关键信息抽取 + +基于文档图像的关键信息抽取包含3个部分:(1)文本检测(2)文本识别(3)关键信息抽取方法,包括语义实体识别或者关系抽取,下面分别进行介绍。 + +### 4.1 文本检测 + + +本文重点关注发票的关键信息抽取模型训练与预测过程,因此在关键信息抽取过程中,直接使用标注的文本检测与识别标注信息进行测试,如果你希望自定义该场景的文本检测模型,完成端到端的关键信息抽取部分,请参考[文本检测模型训练教程](../doc/doc_ch/detection.md),按照训练数据格式准备数据,并完成该场景下垂类文本检测模型的微调过程。 + + +### 4.2 文本识别 + +本文重点关注发票的关键信息抽取模型训练与预测过程,因此在关键信息抽取过程中,直接使用提供的文本检测与识别标注信息进行测试,如果你希望自定义该场景的文本检测模型,完成端到端的关键信息抽取部分,请参考[文本识别模型训练教程](../doc/doc_ch/recognition.md),按照训练数据格式准备数据,并完成该场景下垂类文本识别模型的微调过程。 + +### 4.3 语义实体识别 (Semantic Entity Recognition) + +语义实体识别指的是给定一段文本行,确定其类别(如`姓名`、`住址`等类别)。PaddleOCR中提供了基于VI-LayoutXLM的多模态语义实体识别方法,融合文本、位置与版面信息,相比LayoutXLM多模态模型,去除了其中的视觉骨干网络特征提取部分,引入符合阅读顺序的文本行排序方法,同时使用UDML联合互蒸馏方法进行训练,最终在精度与速度方面均超越LayoutXLM。更多关于VI-LayoutXLM的算法介绍与精度指标,请参考:[VI-LayoutXLM算法介绍](../doc/doc_ch/algorithm_kie_vi_layoutxlm.md)。 + +#### 4.3.1 准备数据 + +发票场景为例,我们首先需要标注出其中的关键字段,我们将其标注为`问题-答案`的key-value pair,如下,编号No为12270830,则`No`字段标注为question,`12270830`字段标注为answer。如下图所示。 + +
+ +
+ +**注意:** + +* 如果文本检测模型数据标注过程中,没有标注 **非关键信息内容** 的检测框,那么在标注关键信息抽取任务的时候,也不需要标注该部分,如上图所示;如果标注的过程,如果同时标注了**非关键信息内容** 的检测框,那么我们需要将该部分的label记为other。 +* 标注过程中,需要以文本行为单位进行标注,无需标注单个字符的位置信息。 + + +已经处理好的增值税发票数据集从这里下载:[增值税发票数据集下载链接](https://aistudio.baidu.com/aistudio/datasetdetail/165561)。 + +下载好发票数据集,并解压在train_data目录下,目录结构如下所示。 + +``` +train_data + |--zzsfp + |---class_list.txt + |---imgs/ + |---train.json + |---val.json +``` + +其中`class_list.txt`是包含`other`, `question`, `answer`,3个种类的的类别列表(不区分大小写),`imgs`目录底下,`train.json`与`val.json`分别表示训练与评估集合的标注文件。训练集中包含30张图片,验证集中包含8张图片。部分标注如下所示。 + +```py +b33.jpg [{"transcription": "No", "label": "question", "points": [[2882, 472], [3026, 472], [3026, 588], [2882, 588]], }, {"transcription": "12269563", "label": "answer", "points": [[3066, 448], [3598, 448], [3598, 576], [3066, 576]], ]}] +``` + +相比于OCR检测的标注,仅多了`label`字段。 + + +#### 4.3.2 开始训练 + + +VI-LayoutXLM的配置为[ser_vi_layoutxlm_xfund_zh_udml.yml](../configs/kie/vi_layoutxlm/ser_vi_layoutxlm_xfund_zh_udml.yml),需要修改数据、类别数目以及配置文件。 + +```yml +Architecture: + model_type: &model_type "kie" + name: DistillationModel + algorithm: Distillation + Models: + Teacher: + pretrained: + freeze_params: false + return_all_feats: true + model_type: *model_type + algorithm: &algorithm "LayoutXLM" + Transform: + Backbone: + name: LayoutXLMForSer + pretrained: True + # one of base or vi + mode: vi + checkpoints: + # 定义类别数目 + num_classes: &num_classes 5 + ... + +PostProcess: + name: DistillationSerPostProcess + model_name: ["Student", "Teacher"] + key: backbone_out + # 定义类别文件 + class_path: &class_path train_data/zzsfp/class_list.txt + +Train: + dataset: + name: SimpleDataSet + # 定义训练数据目录与标注文件 + data_dir: train_data/zzsfp/imgs + label_file_list: + - train_data/zzsfp/train.json + ... + +Eval: + dataset: + # 定义评估数据目录与标注文件 + name: SimpleDataSet + data_dir: train_data/zzsfp/imgs + label_file_list: + - train_data/zzsfp/val.json + ... +``` + +LayoutXLM与VI-LayoutXLM针对该场景的训练结果如下所示。 + +| 模型 | 迭代轮数 | Hmean | +| :---: | :---: | :---: | +| LayoutXLM | 50 | 100.00% | +| VI-LayoutXLM | 50 | 100.00% | + +可以看出,由于当前数据量较少,场景比较简单,因此2个模型的Hmean均达到了100%。 + + +#### 4.3.3 模型评估 + +模型训练过程中,使用的是知识蒸馏的策略,最终保留了学生模型的参数,在评估时,我们需要针对学生模型的配置文件进行修改: [ser_vi_layoutxlm_xfund_zh.yml](../configs/kie/vi_layoutxlm/ser_vi_layoutxlm_xfund_zh.yml),修改内容与训练配置相同,包括**类别数、类别映射文件、数据目录**。 + +修改完成后,执行下面的命令完成评估过程。 + +```bash +# 注意:需要根据你的配置文件地址与保存的模型地址,对评估命令进行修改 +python3 tools/eval.py -c ./fapiao/ser_vi_layoutxlm.yml -o Architecture.Backbone.checkpoints=fapiao/models/ser_vi_layoutxlm_fapiao_udml/best_accuracy +``` + +输出结果如下所示。 + +``` +[2022/08/18 08:49:58] ppocr INFO: metric eval *************** +[2022/08/18 08:49:58] ppocr INFO: precision:1.0 +[2022/08/18 08:49:58] ppocr INFO: recall:1.0 +[2022/08/18 08:49:58] ppocr INFO: hmean:1.0 +[2022/08/18 08:49:58] ppocr INFO: fps:1.9740402401574881 +``` + +#### 4.3.4 模型预测 + +使用下面的命令进行预测。 + +```bash +python3 tools/infer_kie_token_ser.py -c fapiao/ser_vi_layoutxlm.yml -o Architecture.Backbone.checkpoints=fapiao/models/ser_vi_layoutxlm_fapiao_udml/best_accuracy Global.infer_img=./train_data/XFUND/zh_val/val.json Global.infer_mode=False +``` + +预测结果会保存在配置文件中的`Global.save_res_path`目录中。 + +部分预测结果如下所示。 + +
+ +
+ + +* 注意:在预测时,使用的文本检测与识别结果为标注的结果,直接从json文件里面进行读取。 + +如果希望使用OCR引擎结果得到的结果进行推理,则可以使用下面的命令进行推理。 + + +```bash +python3 tools/infer_kie_token_ser.py -c fapiao/ser_vi_layoutxlm.yml -o Architecture.Backbone.checkpoints=fapiao/models/ser_vi_layoutxlm_fapiao_udml/best_accuracy Global.infer_img=./train_data/zzsfp/imgs/b25.jpg Global.infer_mode=True +``` + +结果如下所示。 + +
+ +
+ +它会使用PP-OCRv3的文本检测与识别模型进行获取文本位置与内容信息。 + +可以看出,由于训练的过程中,没有标注额外的字段为other类别,所以大多数检测出来的字段被预测为question或者answer。 + +如果希望构建基于你在垂类场景训练得到的OCR检测与识别模型,可以使用下面的方法传入检测与识别的inference 模型路径,即可完成OCR文本检测与识别以及SER的串联过程。 + +```bash +python3 tools/infer_kie_token_ser.py -c fapiao/ser_vi_layoutxlm.yml -o Architecture.Backbone.checkpoints=fapiao/models/ser_vi_layoutxlm_fapiao_udml/best_accuracy Global.infer_img=./train_data/zzsfp/imgs/b25.jpg Global.infer_mode=True Global.kie_rec_model_dir="your_rec_model" Global.kie_det_model_dir="your_det_model" +``` + +### 4.4 关系抽取(Relation Extraction) + +使用SER模型,可以获取图像中所有的question与answer的字段,继续这些字段的类别,我们需要进一步获取question与answer之间的连接,因此需要进一步训练关系抽取模型,解决该问题。本文也基于VI-LayoutXLM多模态预训练模型,进行下游RE任务的模型训练。 + +#### 4.4.1 准备数据 + +以发票场景为例,相比于SER任务,RE中还需要标记每个文本行的id信息以及链接关系linking,如下所示。 + +
+ +
+ + +标注文件的部分内容如下所示。 + +```py +b33.jpg [{"transcription": "No", "label": "question", "points": [[2882, 472], [3026, 472], [3026, 588], [2882, 588]], "id": 0, "linking": [[0, 1]]}, {"transcription": "12269563", "label": "answer", "points": [[3066, 448], [3598, 448], [3598, 576], [3066, 576]], "id": 1, "linking": [[0, 1]]}] +``` + +相比与SER的标注,多了`id`与`linking`的信息,分别表示唯一标识以及连接关系。 + +已经处理好的增值税发票数据集从这里下载:[增值税发票数据集下载链接](https://aistudio.baidu.com/aistudio/datasetdetail/165561)。 + +#### 4.4.2 开始训练 + +基于VI-LayoutXLM的RE任务配置为[re_vi_layoutxlm_xfund_zh_udml.yml](../configs/kie/vi_layoutxlm/re_vi_layoutxlm_xfund_zh_udml.yml),需要修改**数据路径、类别列表文件**。 + +```yml +Train: + dataset: + name: SimpleDataSet + # 定义训练数据目录与标注文件 + data_dir: train_data/zzsfp/imgs + label_file_list: + - train_data/zzsfp/train.json + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - VQATokenLabelEncode: # Class handling label + contains_re: True + algorithm: *algorithm + class_path: &class_path train_data/zzsfp/class_list.txt + ... + +Eval: + dataset: + # 定义评估数据目录与标注文件 + name: SimpleDataSet + data_dir: train_data/zzsfp/imgs + label_file_list: + - train_data/zzsfp/val.json + ... + +``` + +LayoutXLM与VI-LayoutXLM针对该场景的训练结果如下所示。 + +| 模型 | 迭代轮数 | Hmean | +| :---: | :---: | :---: | +| LayoutXLM | 50 | 98.00% | +| VI-LayoutXLM | 50 | 99.30% | + +可以看出,对于VI-LayoutXLM相比LayoutXLM的Hmean高了1.3%。 + +如需获取已训练模型,请加入PaddleX官方交流频道,获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 + + + +#### 4.4.3 模型评估 + +模型训练过程中,使用的是知识蒸馏的策略,最终保留了学生模型的参数,在评估时,我们需要针对学生模型的配置文件进行修改: [re_vi_layoutxlm_xfund_zh.yml](../configs/kie/vi_layoutxlm/re_vi_layoutxlm_xfund_zh.yml),修改内容与训练配置相同,包括**类别映射文件、数据目录**。 + +修改完成后,执行下面的命令完成评估过程。 + +```bash +# 注意:需要根据你的配置文件地址与保存的模型地址,对评估命令进行修改 +python3 tools/eval.py -c ./fapiao/re_vi_layoutxlm.yml -o Architecture.Backbone.checkpoints=fapiao/models/re_vi_layoutxlm_fapiao_udml/best_accuracy +``` + +输出结果如下所示。 + +```py +[2022/08/18 12:17:14] ppocr INFO: metric eval *************** +[2022/08/18 12:17:14] ppocr INFO: precision:1.0 +[2022/08/18 12:17:14] ppocr INFO: recall:0.9873417721518988 +[2022/08/18 12:17:14] ppocr INFO: hmean:0.9936305732484078 +[2022/08/18 12:17:14] ppocr INFO: fps:2.765963539771157 +``` + +#### 4.4.4 模型预测 + +使用下面的命令进行预测。 + +```bash +# -c 后面的是RE任务的配置文件 +# -o 后面的字段是RE任务的配置 +# -c_ser 后面的是SER任务的配置文件 +# -c_ser 后面的字段是SER任务的配置 +python3 tools/infer_kie_token_ser_re.py -c fapiao/re_vi_layoutxlm.yml -o Architecture.Backbone.checkpoints=fapiao/models/re_vi_layoutxlm_fapiao_trained/best_accuracy Global.infer_img=./train_data/zzsfp/val.json Global.infer_mode=False -c_ser fapiao/ser_vi_layoutxlm.yml -o_ser Architecture.Backbone.checkpoints=fapiao/models/ser_vi_layoutxlm_fapiao_trained/best_accuracy +``` + +预测结果会保存在配置文件中的`Global.save_res_path`目录中。 + +部分预测结果如下所示。 + +
+ +
+ + +* 注意:在预测时,使用的文本检测与识别结果为标注的结果,直接从json文件里面进行读取。 + +如果希望使用OCR引擎结果得到的结果进行推理,则可以使用下面的命令进行推理。 + +```bash +python3 tools/infer_kie_token_ser_re.py -c fapiao/re_vi_layoutxlm.yml -o Architecture.Backbone.checkpoints=fapiao/models/re_vi_layoutxlm_fapiao_udml/best_accuracy Global.infer_img=./train_data/zzsfp/val.json Global.infer_mode=True -c_ser fapiao/ser_vi_layoutxlm.yml -o_ser Architecture.Backbone.checkpoints=fapiao/models/ser_vi_layoutxlm_fapiao_udml/best_accuracy +``` + +如果希望构建基于你在垂类场景训练得到的OCR检测与识别模型,可以使用下面的方法传入,即可完成SER + RE的串联过程。 + +```bash +python3 tools/infer_kie_token_ser_re.py -c fapiao/re_vi_layoutxlm.yml -o Architecture.Backbone.checkpoints=fapiao/models/re_vi_layoutxlm_fapiao_udml/best_accuracy Global.infer_img=./train_data/zzsfp/val.json Global.infer_mode=True -c_ser fapiao/ser_vi_layoutxlm.yml -o_ser Architecture.Backbone.checkpoints=fapiao/models/ser_vi_layoutxlm_fapiao_udml/best_accuracy Global.kie_rec_model_dir="your_rec_model" Global.kie_det_model_dir="your_det_model" +``` diff --git "a/ocr_webapp/PaddleOCR/applications/\345\244\232\346\250\241\346\200\201\350\241\250\345\215\225\350\257\206\345\210\253.md" "b/ocr_webapp/PaddleOCR/applications/\345\244\232\346\250\241\346\200\201\350\241\250\345\215\225\350\257\206\345\210\253.md" new file mode 100644 index 0000000000000000000000000000000000000000..59aaf72b77723bbc61746455ac2bf258e094dfd0 --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\345\244\232\346\250\241\346\200\201\350\241\250\345\215\225\350\257\206\345\210\253.md" @@ -0,0 +1,898 @@ +# 多模态表单识别 +- [多模态表单识别](#多模态表单识别) + - [1 项目说明](#1-项目说明) + - [2 安装说明](#2-安装说明) + - [3 数据准备](#3-数据准备) + - [3.1 下载处理好的数据集](#31-下载处理好的数据集) + - [3.2 转换为PaddleOCR检测和识别格式](#32-转换为paddleocr检测和识别格式) + - [4 OCR](#4-ocr) + - [4.1 文本检测](#41-文本检测) + - [4.1.1 方案1:预训练模型](#411-方案1预训练模型) + - [4.1.2 方案2:XFUND数据集+fine-tune](#412-方案2xfund数据集fine-tune) + - [4.2 文本识别](#42-文本识别) + - [4.2.1 方案1:预训练模型](#421-方案1预训练模型) + - [4.2.2 方案2:XFUND数据集+finetune](#422-方案2xfund数据集finetune) + - [4.2.3 方案3:XFUND数据集+finetune+真实通用识别数据](#423-方案3xfund数据集finetune真实通用识别数据) + - [5 文档视觉问答(DOC-VQA)](#5-文档视觉问答doc-vqa) + - [5.1 SER](#51-ser) + - [5.1.1 模型训练](#511-模型训练) + - [5.1.2 模型评估](#512-模型评估) + - [5.1.3 模型预测](#513-模型预测) + - [5.2 RE](#52-re) + - [5.2.1 模型训练](#521-模型训练) + - [5.2.2 模型评估](#522-模型评估) + - [5.2.3 模型预测](#523-模型预测) + - [6 导出Excel](#6-导出excel) + - [获得模型](#获得模型) + - [更多资源](#更多资源) + - [参考链接](#参考链接) + +## 1 项目说明 + +计算机视觉在金融领域的应用覆盖文字识别、图像识别、视频识别等,其中文字识别(OCR)是金融领域中的核心AI能力,其应用覆盖客户服务、风险防控、运营管理等各项业务,针对的对象包括通用卡证票据识别(银行卡、身份证、营业执照等)、通用文本表格识别(印刷体、多语言、手写体等)以及一些金融特色票据凭证。通过因此如果能够在结构化信息提取时同时利用文字、页面布局等信息,便可增强不同版式下的泛化性。 + +表单识别旨在识别各种具有表格性质的证件、房产证、营业执照、个人信息表、发票等关键键值对(如姓名-张三),其广泛应用于银行、证券、公司财务等领域,具有很高的商业价值。本次范例项目开源了全流程表单识别方案,能够在多个场景快速实现迁移能力。表单识别通常存在以下难点: + +- 人工摘录工作效率低; +- 国内常见表单版式多; +- 传统技术方案泛化效果不满足。 + + +表单识别包含两大阶段:OCR阶段和文档视觉问答阶段。 + +其中,OCR阶段选取了PaddleOCR的PP-OCRv2模型,主要由文本检测和文本识别两个模块组成。DOC-VQA文档视觉问答阶段基于PaddleNLP自然语言处理算法库实现的LayoutXLM模型,支持基于多模态方法的语义实体识别(Semantic Entity Recognition, SER)以及关系抽取(Relation Extraction, RE)任务。本案例流程如 **图1** 所示: + +
+
图1 多模态表单识别流程图
+ +注:欢迎再AIStudio领取免费算力体验线上实训,项目链接: [多模态表单识别](https://aistudio.baidu.com/aistudio/projectdetail/3884375?contributionType=1) + +## 2 安装说明 + + +下载PaddleOCR源码,上述AIStudio项目中已经帮大家打包好的PaddleOCR(已经修改好配置文件),无需下载解压即可,只需安装依赖环境~ + + +```python +unzip -q PaddleOCR.zip +``` + + +```python +# 如仍需安装or安装更新,可以执行以下步骤 +# git clone https://github.com/PaddlePaddle/PaddleOCR.git -b dygraph +# git clone https://gitee.com/PaddlePaddle/PaddleOCR +``` + + +```python +# 安装依赖包 +pip install -U pip +pip install -r /home/aistudio/PaddleOCR/requirements.txt +pip install paddleocr + +pip install yacs gnureadline paddlenlp==2.2.1 +pip install xlsxwriter +``` + +## 3 数据准备 + +这里使用[XFUN数据集](https://github.com/doc-analysis/XFUND)做为实验数据集。 XFUN数据集是微软提出的一个用于KIE任务的多语言数据集,共包含七个数据集,每个数据集包含149张训练集和50张验证集 + +分别为:ZH(中文)、JA(日语)、ES(西班牙)、FR(法语)、IT(意大利)、DE(德语)、PT(葡萄牙) + +本次实验选取中文数据集作为我们的演示数据集。法语数据集作为实践课程的数据集,数据集样例图如 **图2** 所示。 + +
+
图2 数据集样例,左中文,右法语
+ +### 3.1 下载处理好的数据集 + + +处理好的XFUND中文数据集下载地址:[https://paddleocr.bj.bcebos.com/dataset/XFUND.tar](https://paddleocr.bj.bcebos.com/dataset/XFUND.tar) ,可以运行如下指令完成中文数据集下载和解压。 + +
+
图3 下载数据集
+ + +```python +wget https://paddleocr.bj.bcebos.com/dataset/XFUND.tar +tar -xf XFUND.tar + +# XFUN其他数据集使用下面的代码进行转换 +# 代码链接:https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppstructure/vqa/helper/trans_xfun_data.py +# %cd PaddleOCR +# python3 ppstructure/vqa/tools/trans_xfun_data.py --ori_gt_path=path/to/json_path --output_path=path/to/save_path +# %cd ../ +``` + +运行上述指令后在 /home/aistudio/PaddleOCR/ppstructure/vqa/XFUND 目录下有2个文件夹,目录结构如下所示: + +```bash +/home/aistudio/PaddleOCR/ppstructure/vqa/XFUND + └─ zh_train/ 训练集 + ├── image/ 图片存放文件夹 + ├── xfun_normalize_train.json 标注信息 + └─ zh_val/ 验证集 + ├── image/ 图片存放文件夹 + ├── xfun_normalize_val.json 标注信息 + +``` + +该数据集的标注格式为 + +```bash +{ + "height": 3508, # 图像高度 + "width": 2480, # 图像宽度 + "ocr_info": [ + { + "text": "邮政地址:", # 单个文本内容 + "label": "question", # 文本所属类别 + "bbox": [261, 802, 483, 859], # 单个文本框 + "id": 54, # 文本索引 + "linking": [[54, 60]], # 当前文本和其他文本的关系 [question, answer] + "words": [] + }, + { + "text": "湖南省怀化市市辖区", + "label": "answer", + "bbox": [487, 810, 862, 859], + "id": 60, + "linking": [[54, 60]], + "words": [] + } + ] +} +``` + +### 3.2 转换为PaddleOCR检测和识别格式 + +使用XFUND训练PaddleOCR检测和识别模型,需要将数据集格式改为训练需求的格式。 + +
+
图4 转换为OCR格式
+ +- **文本检测** 标注文件格式如下,中间用'\t'分隔: + +" 图像文件名 json.dumps编码的图像标注信息" +ch4_test_images/img_61.jpg [{"transcription": "MASA", "points": [[310, 104], [416, 141], [418, 216], [312, 179]]}, {...}] + +json.dumps编码前的图像标注信息是包含多个字典的list,字典中的 `points` 表示文本框的四个点的坐标(x, y),从左上角的点开始顺时针排列。 `transcription` 表示当前文本框的文字,***当其内容为“###”时,表示该文本框无效,在训练时会跳过。*** + +- **文本识别** 标注文件的格式如下, txt文件中默认请将图片路径和图片标签用'\t'分割,如用其他方式分割将造成训练报错。 + +``` +" 图像文件名 图像标注信息 " + +train_data/rec/train/word_001.jpg 简单可依赖 +train_data/rec/train/word_002.jpg 用科技让复杂的世界更简单 +... +``` + + + + +```python +unzip -q /home/aistudio/data/data140302/XFUND_ori.zip -d /home/aistudio/data/data140302/ +``` + +已经提供转换脚本,执行如下代码即可转换成功: + + +```python +%cd /home/aistudio/ +python trans_xfund_data.py +``` + +## 4 OCR + +选用飞桨OCR开发套件[PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/README_ch.md)中的PP-OCRv2模型进行文本检测和识别。PP-OCRv2在PP-OCR的基础上,进一步在5个方面重点优化,检测模型采用CML协同互学习知识蒸馏策略和CopyPaste数据增广策略;识别模型采用LCNet轻量级骨干网络、UDML 改进知识蒸馏策略和[Enhanced CTC loss](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/doc/doc_ch/enhanced_ctc_loss.md)损失函数改进,进一步在推理速度和预测效果上取得明显提升。更多细节请参考PP-OCRv2[技术报告](https://arxiv.org/abs/2109.03144)。 + +### 4.1 文本检测 + +我们使用2种方案进行训练、评估: +- **PP-OCRv2中英文超轻量检测预训练模型** +- **XFUND数据集+fine-tune** + +#### 4.1.1 方案1:预训练模型 + +**1)下载预训练模型** + +
+
图5 文本检测方案1-下载预训练模型
+ + +PaddleOCR已经提供了PP-OCR系列模型,部分模型展示如下表所示: + +| 模型简介 | 模型名称 | 推荐场景 | 检测模型 | 方向分类器 | 识别模型 | +| ------------------------------------- | ----------------------- | --------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 中英文超轻量PP-OCRv2模型(13.0M) | ch_PP-OCRv2_xx | 移动端&服务器端 | [推理模型](https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_det_infer.tar) / [训练模型](https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_det_distill_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_rec_infer.tar) / [训练模型](https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_rec_train.tar) | +| 中英文超轻量PP-OCR mobile模型(9.4M) | ch_ppocr_mobile_v2.0_xx | 移动端&服务器端 | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_pre.tar) | +| 中英文通用PP-OCR server模型(143.4M) | ch_ppocr_server_v2.0_xx | 服务器端 | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_det_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_det_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_train.tar) | [推理模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_rec_infer.tar) / [预训练模型](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_rec_pre.tar) | + +更多模型下载(包括多语言),可以参考[PP-OCR 系列模型下载](./doc/doc_ch/models_list.md) + + +这里我们使用PP-OCRv2中英文超轻量检测模型,下载并解压预训练模型: + + + + +```python +%cd /home/aistudio/PaddleOCR/pretrain/ +wget https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_det_distill_train.tar +tar -xf ch_PP-OCRv2_det_distill_train.tar && rm -rf ch_PP-OCRv2_det_distill_train.tar +% cd .. +``` + +**2)模型评估** + +
+
图6 文本检测方案1-模型评估
+ +接着使用下载的超轻量检测模型在XFUND验证集上进行评估,由于蒸馏需要包含多个网络,甚至多个Student网络,在计算指标的时候只需要计算一个Student网络的指标即可,key字段设置为Student则表示只计算Student网络的精度。 + +``` +Metric: + name: DistillationMetric + base_metric_name: DetMetric + main_indicator: hmean + key: "Student" +``` +首先修改配置文件`configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_distill.yml`中的以下字段: +``` +Eval.dataset.data_dir:指向验证集图片存放目录 +Eval.dataset.label_file_list:指向验证集标注文件 +``` + + +然后在XFUND验证集上进行评估,具体代码如下: + + +```python +%cd /home/aistudio/PaddleOCR +python tools/eval.py \ + -c configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_distill.yml \ + -o Global.checkpoints="./pretrain_models/ch_PP-OCRv2_det_distill_train/best_accuracy" +``` + +使用预训练模型进行评估,指标如下所示: + +| 方案 | hmeans | +| -------- | -------- | +| PP-OCRv2中英文超轻量检测预训练模型 | 77.26% | + +使用文本检测预训练模型在XFUND验证集上评估,达到77%左右,充分说明ppocr提供的预训练模型具有泛化能力。 + +#### 4.1.2 方案2:XFUND数据集+fine-tune + +PaddleOCR提供的蒸馏预训练模型包含了多个模型的参数,我们提取Student模型的参数,在XFUND数据集上进行finetune,可以参考如下代码: + +```python +import paddle +# 加载预训练模型 +all_params = paddle.load("pretrain/ch_PP-OCRv2_det_distill_train/best_accuracy.pdparams") +# 查看权重参数的keys +# print(all_params.keys()) +# 学生模型的权重提取 +s_params = {key[len("student_model."):]: all_params[key] for key in all_params if "student_model." in key} +# 查看学生模型权重参数的keys +print(s_params.keys()) +# 保存 +paddle.save(s_params, "pretrain/ch_PP-OCRv2_det_distill_train/student.pdparams") +``` + +**1)模型训练** + +
+
图7 文本检测方案2-模型训练
+ + +修改配置文件`configs/det/ch_PP-OCRv2_det_student.yml`中的以下字段: +``` +Global.pretrained_model:指向预训练模型路径 +Train.dataset.data_dir:指向训练集图片存放目录 +Train.dataset.label_file_list:指向训练集标注文件 +Eval.dataset.data_dir:指向验证集图片存放目录 +Eval.dataset.label_file_list:指向验证集标注文件 +Optimizer.lr.learning_rate:调整学习率,本实验设置为0.005 +Train.dataset.transforms.EastRandomCropData.size:训练尺寸改为[1600, 1600] +Eval.dataset.transforms.DetResizeForTest:评估尺寸,添加如下参数 + limit_side_len: 1600 + limit_type: 'min' + +``` +执行下面命令启动训练: + + +```python +CUDA_VISIBLE_DEVICES=0 python tools/train.py \ + -c configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_student.yml +``` + +**2)模型评估** + +
+
图8 文本检测方案2-模型评估
+ +使用训练好的模型进行评估,更新模型路径`Global.checkpoints`。 + +如需获取已训练模型,请加入PaddleX官方交流频道,获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 + +将下载或训练完成的模型放置在对应目录下即可完成模型评估 + + +```python +%cd /home/aistudio/PaddleOCR/ +python tools/eval.py \ + -c configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_student.yml \ + -o Global.checkpoints="pretrain/ch_db_mv3-student1600-finetune/best_accuracy" +``` + +同时我们提供了未finetuen的模型,配置文件参数(`pretrained_model`设置为空,`learning_rate` 设置为0.001) + + +```python +%cd /home/aistudio/PaddleOCR/ +python tools/eval.py \ + -c configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_student.yml \ + -o Global.checkpoints="pretrain/ch_db_mv3-student1600/best_accuracy" +``` + +使用训练好的模型进行评估,指标如下所示: + +| 方案 | hmeans | +| -------- | -------- | +| XFUND数据集 | 79.27% | +| XFUND数据集+fine-tune | 85.24% | + +对比仅使用XFUND数据集训练的模型,使用XFUND数据集+finetune训练,在验证集上评估达到85%左右,说明 finetune会提升垂类场景效果。 + +**3)导出模型** + +
+
图9 文本检测方案2-模型导出
+ +在模型训练过程中保存的模型文件是包含前向预测和反向传播的过程,在实际的工业部署则不需要反向传播,因此需要将模型进行导成部署需要的模型格式。 执行下面命令,即可导出模型。 + + +```python +# 加载配置文件`ch_PP-OCRv2_det_student.yml`,从`pretrain/ch_db_mv3-student1600-finetune`目录下加载`best_accuracy`模型 +# inference模型保存在`./output/det_db_inference`目录下 +%cd /home/aistudio/PaddleOCR/ +python tools/export_model.py \ + -c configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_student.yml \ + -o Global.pretrained_model="pretrain/ch_db_mv3-student1600-finetune/best_accuracy" \ + Global.save_inference_dir="./output/det_db_inference/" +``` + +转换成功后,在目录下有三个文件: +``` +/inference/rec_crnn/ + ├── inference.pdiparams # 识别inference模型的参数文件 + ├── inference.pdiparams.info # 识别inference模型的参数信息,可忽略 + └── inference.pdmodel # 识别inference模型的program文件 +``` + +**4)模型预测** + +
+
图10 文本检测方案2-模型预测
+ +加载上面导出的模型,执行如下命令对验证集或测试集图片进行预测: + +``` +det_model_dir:预测模型 +image_dir:测试图片路径 +use_gpu:是否使用GPU +``` + +检测可视化结果保存在`/home/aistudio/inference_results/`目录下,查看检测效果。 + + +```python +%pwd +!python tools/infer/predict_det.py \ + --det_algorithm="DB" \ + --det_model_dir="./output/det_db_inference/" \ + --image_dir="./doc/vqa/input/zh_val_21.jpg" \ + --use_gpu=True +``` + +总结,我们分别使用PP-OCRv2中英文超轻量检测预训练模型、XFUND数据集+finetune2种方案进行评估、训练等,指标对比如下: + +| 方案 | hmeans | 结果分析 | +| -------- | -------- | -------- | +| PP-OCRv2中英文超轻量检测预训练模型 | 77.26% | ppocr提供的预训练模型有泛化能力 | +| XFUND数据集 | 79.27% | | +| XFUND数据集+finetune | 85.24% | finetune会提升垂类场景效果 | + +### 4.2 文本识别 + +我们分别使用如下3种方案进行训练、评估: + +- PP-OCRv2中英文超轻量识别预训练模型 +- XFUND数据集+fine-tune +- XFUND数据集+fine-tune+真实通用识别数据 + +#### 4.2.1 方案1:预训练模型 + +**1)下载预训练模型** + +
+ +
图11 文本识别方案1-下载预训练模型
+ +我们使用PP-OCRv2中英文超轻量文本识别模型,下载并解压预训练模型: + + +```python +%cd /home/aistudio/PaddleOCR/pretrain/ +wget https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_rec_train.tar +tar -xf ch_PP-OCRv2_rec_train.tar && rm -rf ch_PP-OCRv2_rec_train.tar +% cd .. +``` + +**2)模型评估** + +
+ +
图12 文本识别方案1-模型评估
+ +首先修改配置文件`configs/det/ch_PP-OCRv2/ch_PP-OCRv2_rec_distillation.yml`中的以下字段: + +``` +Eval.dataset.data_dir:指向验证集图片存放目录 +Eval.dataset.label_file_list:指向验证集标注文件 +``` + +我们使用下载的预训练模型进行评估: + + +```python +%cd /home/aistudio/PaddleOCR +CUDA_VISIBLE_DEVICES=0 python tools/eval.py \ + -c configs/rec/ch_PP-OCRv2/ch_PP-OCRv2_rec_distillation.yml \ + -o Global.checkpoints=./pretrain/ch_PP-OCRv2_rec_train/best_accuracy +``` + +使用预训练模型进行评估,指标如下所示: + +| 方案 | acc | +| -------- | -------- | +| PP-OCRv2中英文超轻量识别预训练模型 | 67.48% | + +使用文本预训练模型在XFUND验证集上评估,acc达到67%左右,充分说明ppocr提供的预训练模型具有泛化能力。 + +#### 4.2.2 方案2:XFUND数据集+finetune + +同检测模型,我们提取Student模型的参数,在XFUND数据集上进行finetune,可以参考如下代码: + + +```python +import paddle +# 加载预训练模型 +all_params = paddle.load("pretrain/ch_PP-OCRv2_rec_train/best_accuracy.pdparams") +# 查看权重参数的keys +print(all_params.keys()) +# 学生模型的权重提取 +s_params = {key[len("Student."):]: all_params[key] for key in all_params if "Student." in key} +# 查看学生模型权重参数的keys +print(s_params.keys()) +# 保存 +paddle.save(s_params, "pretrain/ch_PP-OCRv2_rec_train/student.pdparams") +``` + +**1)模型训练** + +
+
图13 文本识别方案2-模型训练
+ +修改配置文件`configs/rec/ch_PP-OCRv2/ch_PP-OCRv2_rec.yml`中的以下字段: + +``` +Global.pretrained_model:指向预训练模型路径 +Global.character_dict_path: 字典路径 +Optimizer.lr.values:学习率 +Train.dataset.data_dir:指向训练集图片存放目录 +Train.dataset.label_file_list:指向训练集标注文件 +Eval.dataset.data_dir:指向验证集图片存放目录 +Eval.dataset.label_file_list:指向验证集标注文件 +``` +执行如下命令启动训练: + +```python +%cd /home/aistudio/PaddleOCR/ +CUDA_VISIBLE_DEVICES=0 python tools/train.py \ + -c configs/rec/ch_PP-OCRv2/ch_PP-OCRv2_rec.yml +``` + +**2)模型评估** + +
+ +
图14 文本识别方案2-模型评估
+ +使用训练好的模型进行评估,更新模型路径`Global.checkpoints`,这里为大家提供训练好的模型`./pretrain/rec_mobile_pp-OCRv2-student-finetune/best_accuracy` + + +```python +%cd /home/aistudio/PaddleOCR/ +CUDA_VISIBLE_DEVICES=0 python tools/eval.py \ + -c configs/rec/ch_PP-OCRv2/ch_PP-OCRv2_rec.yml \ + -o Global.checkpoints=./pretrain/rec_mobile_pp-OCRv2-student-finetune/best_accuracy +``` + +使用预训练模型进行评估,指标如下所示: + +| 方案 | acc | +| -------- | -------- | +| XFUND数据集+finetune | 72.33% | + +使用XFUND数据集+finetune训练,在验证集上评估达到72%左右,说明 finetune会提升垂类场景效果。 + +#### 4.2.3 方案3:XFUND数据集+finetune+真实通用识别数据 + +接着我们在上述`XFUND数据集+finetune`实验的基础上,添加真实通用识别数据,进一步提升识别效果。首先准备真实通用识别数据,并上传到AIStudio: + +**1)模型训练** + +
+ +
图15 文本识别方案3-模型训练
+ +在上述`XFUND数据集+finetune`实验中修改配置文件`configs/rec/ch_PP-OCRv2/ch_PP-OCRv2_rec.yml`的基础上,继续修改以下字段: + +``` +Train.dataset.label_file_list:指向真实识别训练集图片存放目录 +Train.dataset.ratio_list:动态采样 +``` +执行如下命令启动训练: + + + +```python +%cd /home/aistudio/PaddleOCR/ +CUDA_VISIBLE_DEVICES=0 python tools/train.py \ + -c configs/rec/ch_PP-OCRv2/ch_PP-OCRv2_rec.yml +``` + +**2)模型评估** + +
+ +
图16 文本识别方案3-模型评估
+ +使用训练好的模型进行评估,更新模型路径`Global.checkpoints`。 + + +```python +CUDA_VISIBLE_DEVICES=0 python tools/eval.py \ + -c configs/rec/ch_PP-OCRv2/ch_PP-OCRv2_rec.yml \ + -o Global.checkpoints=./pretrain/rec_mobile_pp-OCRv2-student-realdata/best_accuracy +``` + +使用预训练模型进行评估,指标如下所示: + +| 方案 | acc | +| -------- | -------- | +| XFUND数据集+fine-tune+真实通用识别数据 | 85.29% | + +使用XFUND数据集+finetune训练,在验证集上评估达到85%左右,说明真实通用识别数据对于性能提升很有帮助。 + +**3)导出模型** + +
+
图17 文本识别方案3-导出模型
+ +导出模型只保留前向预测的过程: + + +```python +!python tools/export_model.py \ + -c configs/rec/ch_PP-OCRv2/ch_PP-OCRv2_rec.yml \ + -o Global.pretrained_model=pretrain/rec_mobile_pp-OCRv2-student-realdata/best_accuracy \ + Global.save_inference_dir=./output/rec_crnn_inference/ +``` + +**4)模型预测** + +
+ +
图18 文本识别方案3-模型预测
+ +加载上面导出的模型,执行如下命令对验证集或测试集图片进行预测,检测可视化结果保存在`/home/aistudio/inference_results/`目录下,查看检测、识别效果。需要通过`--rec_char_dict_path`指定使用的字典路径 + + +```python +python tools/infer/predict_system.py \ + --image_dir="./doc/vqa/input/zh_val_21.jpg" \ + --det_model_dir="./output/det_db_inference/" \ + --rec_model_dir="./output/rec_crnn_inference/" \ + --rec_image_shape="3, 32, 320" \ + --rec_char_dict_path="/home/aistudio/XFUND/word_dict.txt" +``` + +总结,我们分别使用PP-OCRv2中英文超轻量检测预训练模型、XFUND数据集+finetune2种方案进行评估、训练等,指标对比如下: + +| 方案 | acc | 结果分析 | +| -------- | -------- | -------- | +| PP-OCRv2中英文超轻量识别预训练模型 | 67.48% | ppocr提供的预训练模型具有泛化能力 | +| XFUND数据集+fine-tune |72.33% | finetune会提升垂类场景效果 | +| XFUND数据集+fine-tune+真实通用识别数据 | 85.29% | 真实通用识别数据对于性能提升很有帮助 | + +## 5 文档视觉问答(DOC-VQA) + +VQA指视觉问答,主要针对图像内容进行提问和回答,DOC-VQA是VQA任务中的一种,DOC-VQA主要针对文本图像的文字内容提出问题。 + +PaddleOCR中DOC-VQA系列算法基于PaddleNLP自然语言处理算法库实现LayoutXLM论文,支持基于多模态方法的 **语义实体识别 (Semantic Entity Recognition, SER)** 以及 **关系抽取 (Relation Extraction, RE)** 任务。 + +如果希望直接体验预测过程,可以下载我们提供的预训练模型,跳过训练过程,直接预测即可。 + + +```python +%cd pretrain +#下载SER模型 +wget https://paddleocr.bj.bcebos.com/pplayout/ser_LayoutXLM_xfun_zh.tar && tar -xvf ser_LayoutXLM_xfun_zh.tar +#下载RE模型 +wget https://paddleocr.bj.bcebos.com/pplayout/re_LayoutXLM_xfun_zh.tar && tar -xvf re_LayoutXLM_xfun_zh.tar +%cd ../ +``` + +### 5.1 SER + +SER: 语义实体识别 (Semantic Entity Recognition), 可以完成对图像中的文本识别与分类。 + +
+
图19 SER测试效果图
+ +**图19** 中不同颜色的框表示不同的类别,对于XFUND数据集,有QUESTION, ANSWER, HEADER 3种类别 + +- 深紫色:HEADER +- 浅紫色:QUESTION +- 军绿色:ANSWER + +在OCR检测框的左上方也标出了对应的类别和OCR识别结果。 + +#### 5.1.1 模型训练 + +
+ +
图20 SER-模型训练
+ +启动训练之前,需要修改配置文件 `configs/vqa/ser/layoutxlm.yml` 以下四个字段: + + 1. Train.dataset.data_dir:指向训练集图片存放目录 + 2. Train.dataset.label_file_list:指向训练集标注文件 + 3. Eval.dataset.data_dir:指指向验证集图片存放目录 + 4. Eval.dataset.label_file_list:指向验证集标注文件 + + + +```python +%cd /home/aistudio/PaddleOCR/ +CUDA_VISIBLE_DEVICES=0 python tools/train.py -c configs/vqa/ser/layoutxlm.yml +``` + +最终会打印出`precision`, `recall`, `hmean`等指标。 在`./output/ser_layoutxlm/`文件夹中会保存训练日志,最优的模型和最新epoch的模型。 + +#### 5.1.2 模型评估 + +
+ +
图21 SER-模型评估
+ +我们使用下载的预训练模型进行评估,如果使用自己训练好的模型进行评估,将待评估的模型所在文件夹路径赋值给 `Architecture.Backbone.checkpoints` 字段即可。 + + + + +```python +CUDA_VISIBLE_DEVICES=0 python tools/eval.py \ + -c configs/vqa/ser/layoutxlm.yml \ + -o Architecture.Backbone.checkpoints=pretrain/ser_LayoutXLM_xfun_zh/ +``` + +最终会打印出`precision`, `recall`, `hmean`等指标,预训练模型评估指标如下: + +
+
图 SER预训练模型评估指标
+ +#### 5.1.3 模型预测 + +
+ +
图22 SER-模型预测
+ +使用如下命令即可完成`OCR引擎 + SER`的串联预测, 以SER预训练模型为例: + + +```python +CUDA_VISIBLE_DEVICES=0 python tools/infer_vqa_token_ser.py \ + -c configs/vqa/ser/layoutxlm.yml \ + -o Architecture.Backbone.checkpoints=pretrain/ser_LayoutXLM_xfun_zh/ \ + Global.infer_img=doc/vqa/input/zh_val_42.jpg +``` + +最终会在`config.Global.save_res_path`字段所配置的目录下保存预测结果可视化图像以及预测结果文本文件,预测结果文本文件名为`infer_results.txt`。通过如下命令查看预测图片: + + +```python +import cv2 +from matplotlib import pyplot as plt +# 在notebook中使用matplotlib.pyplot绘图时,需要添加该命令进行显示 +%matplotlib inline + +img = cv2.imread('output/ser/zh_val_42_ser.jpg') +plt.figure(figsize=(48,24)) +plt.imshow(img) +``` + +### 5.2 RE + +基于 RE 任务,可以完成对图象中的文本内容的关系提取,如判断问题对(pair)。 + +
+
图23 RE预测效果图
+ +图中红色框表示问题,蓝色框表示答案,问题和答案之间使用绿色线连接。在OCR检测框的左上方也标出了对应的类别和OCR识别结果。 + +#### 5.2.1 模型训练 + +
+ +
图24 RE-模型训练
+ +启动训练之前,需要修改配置文件`configs/vqa/re/layoutxlm.yml`中的以下四个字段 + + Train.dataset.data_dir:指向训练集图片存放目录 + Train.dataset.label_file_list:指向训练集标注文件 + Eval.dataset.data_dir:指指向验证集图片存放目录 + Eval.dataset.label_file_list:指向验证集标注文件 + + + +```python +CUDA_VISIBLE_DEVICES=0 python3 tools/train.py -c configs/vqa/re/layoutxlm.yml +``` + +最终会打印出`precision`, `recall`, `hmean`等指标。 在`./output/re_layoutxlm/`文件夹中会保存训练日志,最优的模型和最新epoch的模型 + +#### 5.2.2 模型评估 + +
+
图25 RE-模型评估
+ + +我们使用下载的预训练模型进行评估,如果使用自己训练好的模型进行评估,将待评估的模型所在文件夹路径赋值给 `Architecture.Backbone.checkpoints` 字段即可。 + + +```python +CUDA_VISIBLE_DEVICES=0 python3 tools/eval.py \ + -c configs/vqa/re/layoutxlm.yml \ + -o Architecture.Backbone.checkpoints=pretrain/re_LayoutXLM_xfun_zh/ +``` + +最终会打印出`precision`, `recall`, `hmean`等指标,预训练模型评估指标如下: + +
+
图 RE预训练模型评估指标
+ +#### 5.2.3 模型预测 + +
+ +
图26 RE-模型预测
+ +使用如下命令即可完成OCR引擎 + SER + RE的串联预测, 以预训练SER和RE模型为例, + +最终会在config.Global.save_res_path字段所配置的目录下保存预测结果可视化图像以及预测结果文本文件,预测结果文本文件名为infer_results.txt。 + + +```python +cd /home/aistudio/PaddleOCR +CUDA_VISIBLE_DEVICES=0 python3 tools/infer_vqa_token_ser_re.py \ + -c configs/vqa/re/layoutxlm.yml \ + -o Architecture.Backbone.checkpoints=pretrain/re_LayoutXLM_xfun_zh/ \ + Global.infer_img=test_imgs/ \ + -c_ser configs/vqa/ser/layoutxlm.yml \ + -o_ser Architecture.Backbone.checkpoints=pretrain/ser_LayoutXLM_xfun_zh/ +``` + +最终会在config.Global.save_res_path字段所配置的目录下保存预测结果可视化图像以及预测结果文本文件,预测结果文本文件名为infer_results.txt, 每一行表示一张图片的结果,每张图片的结果如下所示,前面表示测试图片路径,后面为测试结果:key字段及对应的value字段。 + +``` +test_imgs/t131.jpg {"政治面税": "群众", "性别": "男", "籍贯": "河北省邯郸市", "婚姻状况": "亏末婚口已婚口已娇", "通讯地址": "邯郸市阳光苑7号楼003", "民族": "汉族", "毕业院校": "河南工业大学", "户口性质": "口农村城镇", "户口地址": "河北省邯郸市", "联系电话": "13288888888", "健康状况": "健康", "姓名": "小六", "好高cm": "180", "出生年月": "1996年8月9日", "文化程度": "本科", "身份证号码": "458933777777777777"} +```` + +展示预测结果 + +```python +import cv2 +from matplotlib import pyplot as plt +%matplotlib inline + +img = cv2.imread('./output/re/t131_ser.jpg') +plt.figure(figsize=(48,24)) +plt.imshow(img) +``` + +## 6 导出Excel + +
+
图27 导出Excel
+ +为了输出信息匹配对,我们修改`tools/infer_vqa_token_ser_re.py`文件中的`line 194-197`。 +``` + fout.write(img_path + "\t" + json.dumps( + { + "ser_result": result, + }, ensure_ascii=False) + "\n") + +``` +更改为 +``` +result_key = {} +for ocr_info_head, ocr_info_tail in result: + result_key[ocr_info_head['text']] = ocr_info_tail['text'] + +fout.write(img_path + "\t" + json.dumps( + result_key, ensure_ascii=False) + "\n") +``` + +同时将输出结果导出到Excel中,效果如 图28 所示: + +
+
图28 Excel效果图
+ + +```python +import json +import xlsxwriter as xw + +workbook = xw.Workbook('output/re/infer_results.xlsx') +format1 = workbook.add_format({ + 'align': 'center', + 'valign': 'vcenter', + 'text_wrap': True, +}) +worksheet1 = workbook.add_worksheet('sheet1') +worksheet1.activate() +title = ['姓名', '性别', '民族', '文化程度', '身份证号码', '联系电话', '通讯地址'] +worksheet1.write_row('A1', title) +i = 2 + +with open('output/re/infer_results.txt', 'r', encoding='utf-8') as fin: + lines = fin.readlines() + for line in lines: + img_path, result = line.strip().split('\t') + result_key = json.loads(result) + # 写入Excel + row_data = [result_key['姓名'], result_key['性别'], result_key['民族'], result_key['文化程度'], result_key['身份证号码'], + result_key['联系电话'], result_key['通讯地址']] + row = 'A' + str(i) + worksheet1.write_row(row, row_data, format1) + i+=1 +workbook.close() +``` + +## 更多资源 + +- 更多深度学习知识、产业案例、面试宝典等,请参考:[awesome-DeepLearning](https://github.com/paddlepaddle/awesome-DeepLearning) + +- 更多PaddleOCR使用教程,请参考:[PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR/tree/dygraph) + +- 更多PaddleNLP使用教程,请参考:[PaddleNLP](https://github.com/PaddlePaddle/PaddleNLP) + +- 飞桨框架相关资料,请参考:[飞桨深度学习平台](https://www.paddlepaddle.org.cn/?fr=paddleEdu_aistudio) + +## 参考链接 + +- LayoutXLM: Multimodal Pre-training for Multilingual Visually-rich Document Understanding, https://arxiv.org/pdf/2104.08836.pdf + +- microsoft/unilm/layoutxlm, https://github.com/microsoft/unilm/tree/master/layoutxlm + +- XFUND dataset, https://github.com/doc-analysis/XFUND diff --git "a/ocr_webapp/PaddleOCR/applications/\345\277\253\351\200\237\346\236\204\345\273\272\345\215\241\350\257\201\347\261\273OCR.md" "b/ocr_webapp/PaddleOCR/applications/\345\277\253\351\200\237\346\236\204\345\273\272\345\215\241\350\257\201\347\261\273OCR.md" new file mode 100644 index 0000000000000000000000000000000000000000..79266c6c2054f5e4124e542397f1532af7445da5 --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\345\277\253\351\200\237\346\236\204\345\273\272\345\215\241\350\257\201\347\261\273OCR.md" @@ -0,0 +1,775 @@ +# 快速构建卡证类OCR + + +- [快速构建卡证类OCR](#快速构建卡证类ocr) + - [1. 金融行业卡证识别应用](#1-金融行业卡证识别应用) + - [1.1 金融行业中的OCR相关技术](#11-金融行业中的ocr相关技术) + - [1.2 金融行业中的卡证识别场景介绍](#12-金融行业中的卡证识别场景介绍) + - [1.3 OCR落地挑战](#13-ocr落地挑战) + - [2. 卡证识别技术解析](#2-卡证识别技术解析) + - [2.1 卡证分类模型](#21-卡证分类模型) + - [2.2 卡证识别模型](#22-卡证识别模型) + - [3. OCR技术拆解](#3-ocr技术拆解) + - [3.1技术流程](#31技术流程) + - [3.2 OCR技术拆解---卡证分类](#32-ocr技术拆解---卡证分类) + - [卡证分类:数据、模型准备](#卡证分类数据模型准备) + - [卡证分类---修改配置文件](#卡证分类---修改配置文件) + - [卡证分类---训练](#卡证分类---训练) + - [3.2 OCR技术拆解---卡证识别](#32-ocr技术拆解---卡证识别) + - [身份证识别:检测+分类](#身份证识别检测分类) + - [数据标注](#数据标注) + - [4 . 项目实践](#4--项目实践) + - [4.1 环境准备](#41-环境准备) + - [4.2 配置文件修改](#42-配置文件修改) + - [4.3 代码修改](#43-代码修改) + - [4.3.1 数据读取](#431-数据读取) + - [4.3.2 head修改](#432--head修改) + - [4.3.3 修改loss](#433-修改loss) + - [4.3.4 后处理](#434-后处理) + - [4.4. 模型启动](#44-模型启动) + - [5 总结](#5-总结) + - [References](#references) + +## 1. 金融行业卡证识别应用 + +### 1.1 金融行业中的OCR相关技术 + +* 《“十四五”数字经济发展规划》指出,2020年我国数字经济核心产业增加值占GDP比重达7.8%,随着数字经济迈向全面扩展,到2025年该比例将提升至10%。 + +* 在过去数年的跨越发展与积累沉淀中,数字金融、金融科技已在对金融业的重塑与再造中充分印证了其自身价值。 + +* 以智能为目标,提升金融数字化水平,实现业务流程自动化,降低人力成本。 + + +![](https://ai-studio-static-online.cdn.bcebos.com/8bb381f164c54ea9b4043cf66fc92ffdea8aaf851bab484fa6e19bd2f93f154f) + + + +### 1.2 金融行业中的卡证识别场景介绍 + +应用场景:身份证、银行卡、营业执照、驾驶证等。 + +应用难点:由于数据的采集来源多样,以及实际采集数据各种噪声:反光、褶皱、模糊、倾斜等各种问题干扰。 + +![](https://ai-studio-static-online.cdn.bcebos.com/981640e17d05487e961162f8576c9e11634ca157f79048d4bd9d3bc21722afe8) + + + +### 1.3 OCR落地挑战 + + +![](https://ai-studio-static-online.cdn.bcebos.com/a5973a8ddeff4bd7ac082f02dc4d0c79de21e721b41641cbb831f23c2cb8fce2) + + + + + +## 2. 卡证识别技术解析 + + +![](https://ai-studio-static-online.cdn.bcebos.com/d7f96effc2434a3ca2d4144ff33c50282b830670c892487d8d7dec151921cce7) + + +### 2.1 卡证分类模型 + +卡证分类:基于PPLCNet + +与其他轻量级模型相比在CPU环境下ImageNet数据集上的表现 + +![](https://ai-studio-static-online.cdn.bcebos.com/cbda3390cb994f98a3c8a9ba88c90c348497763f6c9f4b4797f7d63d84da5f63) + +![](https://ai-studio-static-online.cdn.bcebos.com/dedab7b7fd6543aa9e7f625132b24e3ba3f200e361fa468dac615f7814dfb98d) + + + +* 模型来自模型库PaddleClas,它是一个图像识别和图像分类任务的工具集,助力使用者训练出更好的视觉模型和应用落地。 + +### 2.2 卡证识别模型 + +* 检测:DBNet 识别:SVRT + +![](https://ai-studio-static-online.cdn.bcebos.com/9a7a4e19edc24310b46620f2ee7430f918223b93d4f14a15a52973c096926bad) + + +* PPOCRv3在文本检测、识别进行了一系列改进优化,在保证精度的同时提升预测效率 + + +![](https://ai-studio-static-online.cdn.bcebos.com/6afdbb77e8db4aef9b169e4e94c5d90a9764cfab4f2c4c04aa9afdf4f54d7680) + + +![](https://ai-studio-static-online.cdn.bcebos.com/c1a7d197847a4f168848c59b8e625d1d5e8066b778144395a8b9382bb85dc364) + + +## 3. OCR技术拆解 + +### 3.1技术流程 + +![](https://ai-studio-static-online.cdn.bcebos.com/89ba046177864d8783ced6cb31ba92a66ca2169856a44ee59ac2bb18e44a6c4b) + + +### 3.2 OCR技术拆解---卡证分类 + +#### 卡证分类:数据、模型准备 + + +A 使用爬虫获取无标注数据,将相同类别的放在同一文件夹下,文件名从0开始命名。具体格式如下图所示。 + +​ 注:卡证类数据,建议每个类别数据量在500张以上 +![](https://ai-studio-static-online.cdn.bcebos.com/6f875b6e695e4fe5aedf427beb0d4ce8064ad7cc33c44faaad59d3eb9732639d) + + +B 一行命令生成标签文件 + +``` +tree -r -i -f | grep -E "jpg|JPG|jpeg|JPEG|png|PNG|webp" | awk -F "/" '{print $0" "$2}' > train_list.txt +``` + +C [下载预训练模型 ](https://github.com/PaddlePaddle/PaddleClas/blob/release/2.4/docs/zh_CN/models/PP-LCNet.md) + + + +#### 卡证分类---修改配置文件 + + +配置文件主要修改三个部分: + + 全局参数:预训练模型路径/训练轮次/图像尺寸 + + 模型结构:分类数 + + 数据处理:训练/评估数据路径 + + + ![](https://ai-studio-static-online.cdn.bcebos.com/e0dc05039c7444c5ab1260ff550a408748df8d4cfe864223adf390e51058dbd5) + +#### 卡证分类---训练 + + +指定配置文件启动训练: + +``` +!python /home/aistudio/work/PaddleClas/tools/train.py -c /home/aistudio/work/PaddleClas/ppcls/configs/PULC/text_image_orientation/PPLCNet_x1_0.yaml +``` +![](https://ai-studio-static-online.cdn.bcebos.com/06af09bde845449ba0a676410f4daa1cdc3983ac95034bdbbafac3b7fd94042f) + +​ 注:日志中显示了训练结果和评估结果(训练时可以设置固定轮数评估一次) + + +### 3.2 OCR技术拆解---卡证识别 + +卡证识别(以身份证检测为例) +存在的困难及问题: + + * 在自然场景下,由于各种拍摄设备以及光线、角度不同等影响导致实际得到的证件影像千差万别。 + + * 如何快速提取需要的关键信息 + + * 多行的文本信息,检测结果如何正确拼接 + + ![](https://ai-studio-static-online.cdn.bcebos.com/4f8f5533a2914e0a821f4a639677843c32ec1f08a1b1488d94c0b8bfb6e72d2d) + + + +* OCR技术拆解---OCR工具库 + + PaddleOCR是一个丰富、领先且实用的OCR工具库,助力开发者训练出更好的模型并应用落地 + + +身份证识别:用现有的方法识别 + +![](https://ai-studio-static-online.cdn.bcebos.com/12d402e6a06d482a88f979e0ebdfb39f4d3fc8b80517499689ec607ddb04fbf3) + + + + +#### 身份证识别:检测+分类 + +> 方法:基于现有的dbnet检测模型,加入分类方法。检测同时进行分类,从一定程度上优化识别流程 + +![](https://ai-studio-static-online.cdn.bcebos.com/e1e798c87472477fa0bfca0da12bb0c180845a3e167a4761b0d26ff4330a5ccb) + + +![](https://ai-studio-static-online.cdn.bcebos.com/23a5a19c746441309864586e467f995ec8a551a3661640e493fc4d77520309cd) + +#### 数据标注 + +使用PaddleOCRLable进行快速标注 + +![](https://ai-studio-static-online.cdn.bcebos.com/a73180425fa14f919ce52d9bf70246c3995acea1831843cca6c17d871b8f5d95) + + +* 修改PPOCRLabel.py,将下图中的kie参数设置为True + + +![](https://ai-studio-static-online.cdn.bcebos.com/d445cf4d850e4063b9a7fc6a075c12204cf912ff23ec471fa2e268b661b3d693) + + +* 数据标注踩坑分享 + +![](https://ai-studio-static-online.cdn.bcebos.com/89f42eccd600439fa9e28c97ccb663726e4e54ce3a854825b4c3b7d554ea21df) + +​ 注:两者只有标注有差别,训练参数数据集都相同 + +## 4 . 项目实践 + +AIStudio项目链接:[快速构建卡证类OCR](https://aistudio.baidu.com/aistudio/projectdetail/4459116) + +### 4.1 环境准备 + +1)拉取[paddleocr](https://github.com/PaddlePaddle/PaddleOCR)项目,如果从github上拉取速度慢可以选择从gitee上获取。 +``` +!git clone https://github.com/PaddlePaddle/PaddleOCR.git -b release/2.6 /home/aistudio/work/ +``` + +2)获取并解压预训练模型,如果要使用其他模型可以从模型库里自主选择合适模型。 +``` +!wget -P work/pre_trained/ https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_distill_train.tar +!tar -vxf /home/aistudio/work/pre_trained/ch_PP-OCRv3_det_distill_train.tar -C /home/aistudio/work/pre_trained +``` +3) 安装必要依赖 +``` +!pip install -r /home/aistudio/work/requirements.txt +``` + +### 4.2 配置文件修改 + +修改配置文件 *work/configs/det/detmv3db.yml* + +具体修改说明如下: + +![](https://ai-studio-static-online.cdn.bcebos.com/fcdf517af5a6466294d72db7450209378d8efd9b77764e329d3f2aff3579a20c) + + 注:在上述的配置文件的Global变量中需要添加以下两个参数: + +​ label_list 为标签表 +​ num_classes 为分类数 +​ 上述两个参数根据实际的情况配置即可 + + +![](https://ai-studio-static-online.cdn.bcebos.com/0b056be24f374812b61abf43305774767ae122c8479242f98aa0799b7bfc81d4) + +其中lable_list内容如下例所示,***建议第一个参数设置为 background,不要设置为实际要提取的关键信息种类***: + +![](https://ai-studio-static-online.cdn.bcebos.com/9fc78bbcdf754898b9b2c7f000ddf562afac786482ab4f2ab063e2242faa542a) + +配置文件中的其他设置说明 + +![](https://ai-studio-static-online.cdn.bcebos.com/c7fc5e631dd44bc8b714630f4e49d9155a831d9e56c64e2482ded87081d0db22) + +![](https://ai-studio-static-online.cdn.bcebos.com/8d1022ac25d9474daa4fb236235bd58760039d58ad46414f841559d68e0d057f) + +![](https://ai-studio-static-online.cdn.bcebos.com/ee927ad9ebd442bb96f163a7ebbf4bc95e6bedee97324a51887cf82de0851fd3) + + + + +### 4.3 代码修改 + + +#### 4.3.1 数据读取 + + + +* 修改 PaddleOCR/ppocr/data/imaug/label_ops.py中的DetLabelEncode + + +```python +class DetLabelEncode(object): + + # 修改检测标签的编码处,新增了参数分类数:num_classes,重写初始化方法,以及分类标签的读取 + + def __init__(self, label_list, num_classes=8, **kwargs): + self.num_classes = num_classes + self.label_list = [] + if label_list: + if isinstance(label_list, str): + with open(label_list, 'r+', encoding='utf-8') as f: + for line in f.readlines(): + self.label_list.append(line.replace("\n", "")) + else: + self.label_list = label_list + else: + assert ' please check label_list whether it is none or config is right' + + if num_classes != len(self.label_list): # 校验分类数和标签的一致性 + assert 'label_list length is not equal to the num_classes' + + def __call__(self, data): + label = data['label'] + label = json.loads(label) + nBox = len(label) + boxes, txts, txt_tags, classes = [], [], [], [] + for bno in range(0, nBox): + box = label[bno]['points'] + txt = label[bno]['key_cls'] # 此处将kie中的参数作为分类读取 + boxes.append(box) + txts.append(txt) + + if txt in ['*', '###']: + txt_tags.append(True) + if self.num_classes > 1: + classes.append(-2) + else: + txt_tags.append(False) + if self.num_classes > 1: # 将KIE内容的key标签作为分类标签使用 + classes.append(int(self.label_list.index(txt))) + + if len(boxes) == 0: + + return None + boxes = self.expand_points_num(boxes) + boxes = np.array(boxes, dtype=np.float32) + txt_tags = np.array(txt_tags, dtype=np.bool_) + classes = classes + data['polys'] = boxes + data['texts'] = txts + data['ignore_tags'] = txt_tags + if self.num_classes > 1: + data['classes'] = classes + return data +``` + +* 修改 PaddleOCR/ppocr/data/imaug/make_shrink_map.py中的MakeShrinkMap类。这里需要注意的是,如果我们设置的label_list中的第一个参数为要检测的信息那么会得到如下的mask, + +举例说明: +这是检测的mask图,图中有四个mask那么实际对应的分类应该是4类 + +![](https://ai-studio-static-online.cdn.bcebos.com/42d2188d3d6b498880952e12c3ceae1efabf135f8d9f4c31823f09ebe02ba9d2) + + + +label_list中第一个为关键分类,则得到的分类Mask实际如下,与上图相比,少了一个box: + +![](https://ai-studio-static-online.cdn.bcebos.com/864604967256461aa7c5d32cd240645e9f4c70af773341d5911f22d5a3e87b5f) + + + +```python +class MakeShrinkMap(object): + r''' + Making binary mask from detection data with ICDAR format. + Typically following the process of class `MakeICDARData`. + ''' + + def __init__(self, min_text_size=8, shrink_ratio=0.4, num_classes=8, **kwargs): + self.min_text_size = min_text_size + self.shrink_ratio = shrink_ratio + self.num_classes = num_classes # 添加了分类 + + def __call__(self, data): + image = data['image'] + text_polys = data['polys'] + ignore_tags = data['ignore_tags'] + if self.num_classes > 1: + classes = data['classes'] + + h, w = image.shape[:2] + text_polys, ignore_tags = self.validate_polygons(text_polys, + ignore_tags, h, w) + gt = np.zeros((h, w), dtype=np.float32) + mask = np.ones((h, w), dtype=np.float32) + gt_class = np.zeros((h, w), dtype=np.float32) # 新增分类 + for i in range(len(text_polys)): + polygon = text_polys[i] + height = max(polygon[:, 1]) - min(polygon[:, 1]) + width = max(polygon[:, 0]) - min(polygon[:, 0]) + if ignore_tags[i] or min(height, width) < self.min_text_size: + cv2.fillPoly(mask, + polygon.astype(np.int32)[np.newaxis, :, :], 0) + ignore_tags[i] = True + else: + polygon_shape = Polygon(polygon) + subject = [tuple(l) for l in polygon] + padding = pyclipper.PyclipperOffset() + padding.AddPath(subject, pyclipper.JT_ROUND, + pyclipper.ET_CLOSEDPOLYGON) + shrinked = [] + + # Increase the shrink ratio every time we get multiple polygon returned back + possible_ratios = np.arange(self.shrink_ratio, 1, + self.shrink_ratio) + np.append(possible_ratios, 1) + for ratio in possible_ratios: + distance = polygon_shape.area * ( + 1 - np.power(ratio, 2)) / polygon_shape.length + shrinked = padding.Execute(-distance) + if len(shrinked) == 1: + break + + if shrinked == []: + cv2.fillPoly(mask, + polygon.astype(np.int32)[np.newaxis, :, :], 0) + ignore_tags[i] = True + continue + + for each_shirnk in shrinked: + shirnk = np.array(each_shirnk).reshape(-1, 2) + cv2.fillPoly(gt, [shirnk.astype(np.int32)], 1) + if self.num_classes > 1: # 绘制分类的mask + cv2.fillPoly(gt_class, polygon.astype(np.int32)[np.newaxis, :, :], classes[i]) + + + data['shrink_map'] = gt + + if self.num_classes > 1: + data['class_mask'] = gt_class + + data['shrink_mask'] = mask + return data +``` + +由于在训练数据中会对数据进行resize设置,yml中的操作为:EastRandomCropData,所以需要修改PaddleOCR/ppocr/data/imaug/random_crop_data.py中的EastRandomCropData + + +```python +class EastRandomCropData(object): + def __init__(self, + size=(640, 640), + max_tries=10, + min_crop_side_ratio=0.1, + keep_ratio=True, + num_classes=8, + **kwargs): + self.size = size + self.max_tries = max_tries + self.min_crop_side_ratio = min_crop_side_ratio + self.keep_ratio = keep_ratio + self.num_classes = num_classes + + def __call__(self, data): + img = data['image'] + text_polys = data['polys'] + ignore_tags = data['ignore_tags'] + texts = data['texts'] + if self.num_classes > 1: + classes = data['classes'] + all_care_polys = [ + text_polys[i] for i, tag in enumerate(ignore_tags) if not tag + ] + # 计算crop区域 + crop_x, crop_y, crop_w, crop_h = crop_area( + img, all_care_polys, self.min_crop_side_ratio, self.max_tries) + # crop 图片 保持比例填充 + scale_w = self.size[0] / crop_w + scale_h = self.size[1] / crop_h + scale = min(scale_w, scale_h) + h = int(crop_h * scale) + w = int(crop_w * scale) + if self.keep_ratio: + padimg = np.zeros((self.size[1], self.size[0], img.shape[2]), + img.dtype) + padimg[:h, :w] = cv2.resize( + img[crop_y:crop_y + crop_h, crop_x:crop_x + crop_w], (w, h)) + img = padimg + else: + img = cv2.resize( + img[crop_y:crop_y + crop_h, crop_x:crop_x + crop_w], + tuple(self.size)) + # crop 文本框 + text_polys_crop = [] + ignore_tags_crop = [] + texts_crop = [] + classes_crop = [] + for poly, text, tag,class_index in zip(text_polys, texts, ignore_tags,classes): + poly = ((poly - (crop_x, crop_y)) * scale).tolist() + if not is_poly_outside_rect(poly, 0, 0, w, h): + text_polys_crop.append(poly) + ignore_tags_crop.append(tag) + texts_crop.append(text) + if self.num_classes > 1: + classes_crop.append(class_index) + data['image'] = img + data['polys'] = np.array(text_polys_crop) + data['ignore_tags'] = ignore_tags_crop + data['texts'] = texts_crop + if self.num_classes > 1: + data['classes'] = classes_crop + return data +``` + +#### 4.3.2 head修改 + + + +主要修改 ppocr/modeling/heads/det_db_head.py,将Head类中的最后一层的输出修改为实际的分类数,同时在DBHead中新增分类的head。 + +![](https://ai-studio-static-online.cdn.bcebos.com/0e25da2ccded4af19e95c85c3d3287ab4d53e31a4eed4607b6a4cb637c43f6d3) + + + +#### 4.3.3 修改loss + + +修改PaddleOCR/ppocr/losses/det_db_loss.py中的DBLoss类,分类采用交叉熵损失函数进行计算。 + +![](https://ai-studio-static-online.cdn.bcebos.com/dc10a070018d4d27946c26ec24a2a85bc3f16422f4964f72a9b63c6170d954e1) + + +#### 4.3.4 后处理 + + + +由于涉及到eval以及后续推理能否正常使用,我们需要修改后处理的相关代码,修改位置 PaddleOCR/ppocr/postprocess/db_postprocess.py中的DBPostProcess类 + + +```python +class DBPostProcess(object): + """ + The post process for Differentiable Binarization (DB). + """ + + def __init__(self, + thresh=0.3, + box_thresh=0.7, + max_candidates=1000, + unclip_ratio=2.0, + use_dilation=False, + score_mode="fast", + **kwargs): + self.thresh = thresh + self.box_thresh = box_thresh + self.max_candidates = max_candidates + self.unclip_ratio = unclip_ratio + self.min_size = 3 + self.score_mode = score_mode + assert score_mode in [ + "slow", "fast" + ], "Score mode must be in [slow, fast] but got: {}".format(score_mode) + + self.dilation_kernel = None if not use_dilation else np.array( + [[1, 1], [1, 1]]) + + def boxes_from_bitmap(self, pred, _bitmap, classes, dest_width, dest_height): + """ + _bitmap: single map with shape (1, H, W), + whose values are binarized as {0, 1} + """ + + bitmap = _bitmap + height, width = bitmap.shape + + outs = cv2.findContours((bitmap * 255).astype(np.uint8), cv2.RETR_LIST, + cv2.CHAIN_APPROX_SIMPLE) + if len(outs) == 3: + img, contours, _ = outs[0], outs[1], outs[2] + elif len(outs) == 2: + contours, _ = outs[0], outs[1] + + num_contours = min(len(contours), self.max_candidates) + + boxes = [] + scores = [] + class_indexes = [] + class_scores = [] + for index in range(num_contours): + contour = contours[index] + points, sside = self.get_mini_boxes(contour) + if sside < self.min_size: + continue + points = np.array(points) + if self.score_mode == "fast": + score, class_index, class_score = self.box_score_fast(pred, points.reshape(-1, 2), classes) + else: + score, class_index, class_score = self.box_score_slow(pred, contour, classes) + if self.box_thresh > score: + continue + + box = self.unclip(points).reshape(-1, 1, 2) + box, sside = self.get_mini_boxes(box) + if sside < self.min_size + 2: + continue + box = np.array(box) + + box[:, 0] = np.clip( + np.round(box[:, 0] / width * dest_width), 0, dest_width) + box[:, 1] = np.clip( + np.round(box[:, 1] / height * dest_height), 0, dest_height) + + boxes.append(box.astype(np.int16)) + scores.append(score) + + class_indexes.append(class_index) + class_scores.append(class_score) + + if classes is None: + return np.array(boxes, dtype=np.int16), scores + else: + return np.array(boxes, dtype=np.int16), scores, class_indexes, class_scores + + def unclip(self, box): + unclip_ratio = self.unclip_ratio + poly = Polygon(box) + distance = poly.area * unclip_ratio / poly.length + offset = pyclipper.PyclipperOffset() + offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) + expanded = np.array(offset.Execute(distance)) + return expanded + + def get_mini_boxes(self, contour): + bounding_box = cv2.minAreaRect(contour) + points = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0]) + + index_1, index_2, index_3, index_4 = 0, 1, 2, 3 + if points[1][1] > points[0][1]: + index_1 = 0 + index_4 = 1 + else: + index_1 = 1 + index_4 = 0 + if points[3][1] > points[2][1]: + index_2 = 2 + index_3 = 3 + else: + index_2 = 3 + index_3 = 2 + + box = [ + points[index_1], points[index_2], points[index_3], points[index_4] + ] + return box, min(bounding_box[1]) + + def box_score_fast(self, bitmap, _box, classes): + ''' + box_score_fast: use bbox mean score as the mean score + ''' + h, w = bitmap.shape[:2] + box = _box.copy() + xmin = np.clip(np.floor(box[:, 0].min()).astype(np.int32), 0, w - 1) + xmax = np.clip(np.ceil(box[:, 0].max()).astype(np.int32), 0, w - 1) + ymin = np.clip(np.floor(box[:, 1].min()).astype(np.int32), 0, h - 1) + ymax = np.clip(np.ceil(box[:, 1].max()).astype(np.int32), 0, h - 1) + + mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8) + box[:, 0] = box[:, 0] - xmin + box[:, 1] = box[:, 1] - ymin + cv2.fillPoly(mask, box.reshape(1, -1, 2).astype(np.int32), 1) + + if classes is None: + return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0], None, None + else: + k = 999 + class_mask = np.full((ymax - ymin + 1, xmax - xmin + 1), k, dtype=np.int32) + + cv2.fillPoly(class_mask, box.reshape(1, -1, 2).astype(np.int32), 0) + classes = classes[ymin:ymax + 1, xmin:xmax + 1] + + new_classes = classes + class_mask + a = new_classes.reshape(-1) + b = np.where(a >= k) + classes = np.delete(a, b[0].tolist()) + + class_index = np.argmax(np.bincount(classes)) + class_score = np.sum(classes == class_index) / len(classes) + + return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0], class_index, class_score + + def box_score_slow(self, bitmap, contour, classes): + """ + box_score_slow: use polyon mean score as the mean score + """ + h, w = bitmap.shape[:2] + contour = contour.copy() + contour = np.reshape(contour, (-1, 2)) + + xmin = np.clip(np.min(contour[:, 0]), 0, w - 1) + xmax = np.clip(np.max(contour[:, 0]), 0, w - 1) + ymin = np.clip(np.min(contour[:, 1]), 0, h - 1) + ymax = np.clip(np.max(contour[:, 1]), 0, h - 1) + + mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8) + + contour[:, 0] = contour[:, 0] - xmin + contour[:, 1] = contour[:, 1] - ymin + + cv2.fillPoly(mask, contour.reshape(1, -1, 2).astype(np.int32), 1) + + if classes is None: + return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0], None, None + else: + k = 999 + class_mask = np.full((ymax - ymin + 1, xmax - xmin + 1), k, dtype=np.int32) + + cv2.fillPoly(class_mask, contour.reshape(1, -1, 2).astype(np.int32), 0) + classes = classes[ymin:ymax + 1, xmin:xmax + 1] + + new_classes = classes + class_mask + a = new_classes.reshape(-1) + b = np.where(a >= k) + classes = np.delete(a, b[0].tolist()) + + class_index = np.argmax(np.bincount(classes)) + class_score = np.sum(classes == class_index) / len(classes) + + return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0], class_index, class_score + + def __call__(self, outs_dict, shape_list): + pred = outs_dict['maps'] + if isinstance(pred, paddle.Tensor): + pred = pred.numpy() + pred = pred[:, 0, :, :] + segmentation = pred > self.thresh + + if "classes" in outs_dict: + classes = outs_dict['classes'] + if isinstance(classes, paddle.Tensor): + classes = classes.numpy() + classes = classes[:, 0, :, :] + + else: + classes = None + + boxes_batch = [] + for batch_index in range(pred.shape[0]): + src_h, src_w, ratio_h, ratio_w = shape_list[batch_index] + if self.dilation_kernel is not None: + mask = cv2.dilate( + np.array(segmentation[batch_index]).astype(np.uint8), + self.dilation_kernel) + else: + mask = segmentation[batch_index] + + if classes is None: + boxes, scores = self.boxes_from_bitmap(pred[batch_index], mask, None, + src_w, src_h) + boxes_batch.append({'points': boxes}) + else: + boxes, scores, class_indexes, class_scores = self.boxes_from_bitmap(pred[batch_index], mask, + classes[batch_index], + src_w, src_h) + boxes_batch.append({'points': boxes, "classes": class_indexes, "class_scores": class_scores}) + + return boxes_batch +``` + +### 4.4. 模型启动 + +在完成上述步骤后我们就可以正常启动训练 + +``` +!python /home/aistudio/work/PaddleOCR/tools/train.py -c /home/aistudio/work/PaddleOCR/configs/det/det_mv3_db.yml +``` + +其他命令: +``` +!python /home/aistudio/work/PaddleOCR/tools/eval.py -c /home/aistudio/work/PaddleOCR/configs/det/det_mv3_db.yml +!python /home/aistudio/work/PaddleOCR/tools/infer_det.py -c /home/aistudio/work/PaddleOCR/configs/det/det_mv3_db.yml +``` +模型推理 +``` +!python /home/aistudio/work/PaddleOCR/tools/infer/predict_det.py --image_dir="/home/aistudio/work/test_img/" --det_model_dir="/home/aistudio/work/PaddleOCR/output/infer" +``` + +## 5 总结 + +1. 分类+检测在一定程度上能够缩短用时,具体的模型选取要根据业务场景恰当选择。 +2. 数据标注需要多次进行测试调整标注方法,一般进行检测模型微调,需要标注至少上百张。 +3. 设置合理的batch_size以及resize大小,同时注意lr设置。 + + +## References + +1 https://github.com/PaddlePaddle/PaddleOCR + +2 https://github.com/PaddlePaddle/PaddleClas + +3 https://blog.csdn.net/YY007H/article/details/124491217 diff --git "a/ocr_webapp/PaddleOCR/applications/\346\211\213\345\206\231\346\226\207\345\255\227\350\257\206\345\210\253.md" "b/ocr_webapp/PaddleOCR/applications/\346\211\213\345\206\231\346\226\207\345\255\227\350\257\206\345\210\253.md" new file mode 100644 index 0000000000000000000000000000000000000000..b2bfdb3aaed9fb27b3794e33c7018d640ce7852a --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\346\211\213\345\206\231\346\226\207\345\255\227\350\257\206\345\210\253.md" @@ -0,0 +1,251 @@ +# 基于PP-OCRv3的手写文字识别 + +- [1. 项目背景及意义](#1-项目背景及意义) +- [2. 项目内容](#2-项目内容) +- [3. PP-OCRv3识别算法介绍](#3-PP-OCRv3识别算法介绍) +- [4. 安装环境](#4-安装环境) +- [5. 数据准备](#5-数据准备) +- [6. 模型训练](#6-模型训练) + - [6.1 下载预训练模型](#61-下载预训练模型) + - [6.2 修改配置文件](#62-修改配置文件) + - [6.3 开始训练](#63-开始训练) +- [7. 模型评估](#7-模型评估) +- [8. 模型导出推理](#8-模型导出推理) + - [8.1 模型导出](#81-模型导出) + - [8.2 模型推理](#82-模型推理) + + +## 1. 项目背景及意义 +目前光学字符识别(OCR)技术在我们的生活当中被广泛使用,但是大多数模型在通用场景下的准确性还有待提高。针对于此我们借助飞桨提供的PaddleOCR套件较容易的实现了在垂类场景下的应用。手写体在日常生活中较为常见,然而手写体的识别却存在着很大的挑战,因为每个人的手写字体风格不一样,这对于视觉模型来说还是相当有挑战的。因此训练一个手写体识别模型具有很好的现实意义。下面给出一些手写体的示例图: + +![example](https://ai-studio-static-online.cdn.bcebos.com/7a8865b2836f42d382e7c3fdaedc4d307d797fa2bcd0466e9f8b7705efff5a7b) + +## 2. 项目内容 +本项目基于PaddleOCR套件,以PP-OCRv3识别模型为基础,针对手写文字识别场景进行优化。 + +Aistudio项目链接:[OCR手写文字识别](https://aistudio.baidu.com/aistudio/projectdetail/4330587) + +## 3. PP-OCRv3识别算法介绍 +PP-OCRv3的识别模块是基于文本识别算法[SVTR](https://arxiv.org/abs/2205.00159)优化。SVTR不再采用RNN结构,通过引入Transformers结构更加有效地挖掘文本行图像的上下文信息,从而提升文本识别能力。如下图所示,PP-OCRv3采用了6个优化策略。 + +![v3_rec](https://ai-studio-static-online.cdn.bcebos.com/d4f5344b5b854d50be738671598a89a45689c6704c4d481fb904dd7cf72f2a1a) + +优化策略汇总如下: + +* SVTR_LCNet:轻量级文本识别网络 +* GTC:Attention指导CTC训练策略 +* TextConAug:挖掘文字上下文信息的数据增广策略 +* TextRotNet:自监督的预训练模型 +* UDML:联合互学习策略 +* UIM:无标注数据挖掘方案 + +详细优化策略描述请参考[PP-OCRv3优化策略](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/PP-OCRv3_introduction.md#3-%E8%AF%86%E5%88%AB%E4%BC%98%E5%8C%96) + +## 4. 安装环境 + + +```python +# 首先git官方的PaddleOCR项目,安装需要的依赖 +git clone https://github.com/PaddlePaddle/PaddleOCR.git +cd PaddleOCR +pip install -r requirements.txt +``` + +## 5. 数据准备 +本项目使用公开的手写文本识别数据集,包含Chinese OCR, 中科院自动化研究所-手写中文数据集[CASIA-HWDB2.x](http://www.nlpr.ia.ac.cn/databases/handwriting/Download.html),以及由中科院手写数据和网上开源数据合并组合的[数据集](https://aistudio.baidu.com/aistudio/datasetdetail/102884/0)等,该项目已经挂载处理好的数据集,可直接下载使用进行训练。 + + +```python +下载并解压数据 +tar -xf hw_data.tar +``` + +## 6. 模型训练 +### 6.1 下载预训练模型 +首先需要下载我们需要的PP-OCRv3识别预训练模型,更多选择请自行选择其他的[文字识别模型](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/models_list.md#2-%E6%96%87%E6%9C%AC%E8%AF%86%E5%88%AB%E6%A8%A1%E5%9E%8B) + + +```python +# 使用该指令下载需要的预训练模型 +wget -P ./pretrained_models/ https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_train.tar +# 解压预训练模型文件 +tar -xf ./pretrained_models/ch_PP-OCRv3_rec_train.tar -C pretrained_models +``` + +### 6.2 修改配置文件 +我们使用`configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml`,主要修改训练轮数和学习率参相关参数,设置预训练模型路径,设置数据集路径。 另外,batch_size可根据自己机器显存大小进行调整。 具体修改如下几个地方: + +``` + epoch_num: 100 # 训练epoch数 + save_model_dir: ./output/ch_PP-OCR_v3_rec + save_epoch_step: 10 + eval_batch_step: [0, 100] # 评估间隔,每隔100step评估一次 + pretrained_model: ./pretrained_models/ch_PP-OCRv3_rec_train/best_accuracy # 预训练模型路径 + + + lr: + name: Cosine # 修改学习率衰减策略为Cosine + learning_rate: 0.0001 # 修改fine-tune的学习率 + warmup_epoch: 2 # 修改warmup轮数 + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data # 训练集图片路径 + ext_op_transform_idx: 1 + label_file_list: + - ./train_data/chineseocr-data/rec_hand_line_all_label_train.txt # 训练集标签 + - ./train_data/handwrite/HWDB2.0Train_label.txt + - ./train_data/handwrite/HWDB2.1Train_label.txt + - ./train_data/handwrite/HWDB2.2Train_label.txt + - ./train_data/handwrite/hwdb_ic13/handwriting_hwdb_train_labels.txt + - ./train_data/handwrite/HW_Chinese/train_hw.txt + ratio_list: + - 0.1 + - 1.0 + - 1.0 + - 1.0 + - 0.02 + - 1.0 + loader: + shuffle: true + batch_size_per_card: 64 + drop_last: true + num_workers: 4 +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data # 测试集图片路径 + label_file_list: + - ./train_data/chineseocr-data/rec_hand_line_all_label_val.txt # 测试集标签 + - ./train_data/handwrite/HWDB2.0Test_label.txt + - ./train_data/handwrite/HWDB2.1Test_label.txt + - ./train_data/handwrite/HWDB2.2Test_label.txt + - ./train_data/handwrite/hwdb_ic13/handwriting_hwdb_val_labels.txt + - ./train_data/handwrite/HW_Chinese/test_hw.txt + loader: + shuffle: false + drop_last: false + batch_size_per_card: 64 + num_workers: 4 +``` +由于数据集大多是长文本,因此需要**注释**掉下面的数据增广策略,以便训练出更好的模型。 +``` +- RecConAug: + prob: 0.5 + ext_data_num: 2 + image_shape: [48, 320, 3] +``` + + +### 6.3 开始训练 +我们使用上面修改好的配置文件`configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml`,预训练模型,数据集路径,学习率,训练轮数等都已经设置完毕后,可以使用下面命令开始训练。 + + +```python +# 开始训练识别模型 +python tools/train.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml + +``` + +## 7. 模型评估 +在训练之前,我们可以直接使用下面命令来评估预训练模型的效果: + + + +```python +# 评估预训练模型 +python tools/eval.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml -o Global.pretrained_model="./pretrained_models/ch_PP-OCRv3_rec_train/best_accuracy" +``` +``` +[2022/07/14 10:46:22] ppocr INFO: load pretrain successful from ./pretrained_models/ch_PP-OCRv3_rec_train/best_accuracy +eval model:: 100%|████████████████████████████| 687/687 [03:29<00:00, 3.27it/s] +[2022/07/14 10:49:52] ppocr INFO: metric eval *************** +[2022/07/14 10:49:52] ppocr INFO: acc:0.03724954461811258 +[2022/07/14 10:49:52] ppocr INFO: norm_edit_dis:0.4859541065843199 +[2022/07/14 10:49:52] ppocr INFO: Teacher_acc:0.0371584699368947 +[2022/07/14 10:49:52] ppocr INFO: Teacher_norm_edit_dis:0.48718814890536477 +[2022/07/14 10:49:52] ppocr INFO: fps:947.8562684823883 +``` + +可以看出,直接加载预训练模型进行评估,效果较差,因为预训练模型并不是基于手写文字进行单独训练的,所以我们需要基于预训练模型进行finetune。 +训练完成后,可以进行测试评估,评估命令如下: + + + +```python +# 评估finetune效果 +python tools/eval.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml -o Global.pretrained_model="./output/ch_PP-OCR_v3_rec/best_accuracy" + +``` + +评估结果如下,可以看出识别准确率为54.3%。 +``` +[2022/07/14 10:54:06] ppocr INFO: metric eval *************** +[2022/07/14 10:54:06] ppocr INFO: acc:0.5430100180913 +[2022/07/14 10:54:06] ppocr INFO: norm_edit_dis:0.9203322593158589 +[2022/07/14 10:54:06] ppocr INFO: Teacher_acc:0.5401183969626324 +[2022/07/14 10:54:06] ppocr INFO: Teacher_norm_edit_dis:0.919827504507755 +[2022/07/14 10:54:06] ppocr INFO: fps:928.948733797251 +``` + +如需获取已训练模型,请加入PaddleX官方交流频道,获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 + +将下载或训练完成的模型放置在对应目录下即可完成模型推理 + +## 8. 模型导出推理 +训练完成后,可以将训练模型转换成inference模型。inference 模型会额外保存模型的结构信息,在预测部署、加速推理上性能优越,灵活方便,适合于实际系统集成。 + + +### 8.1 模型导出 +导出命令如下: + + + +```python +# 转化为推理模型 +python tools/export_model.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml -o Global.pretrained_model="./output/ch_PP-OCR_v3_rec/best_accuracy" Global.save_inference_dir="./inference/rec_ppocrv3/" + +``` + +### 8.2 模型推理 +导出模型后,可以使用如下命令进行推理预测: + + + +```python +# 推理预测 +python tools/infer/predict_rec.py --image_dir="train_data/handwrite/HWDB2.0Test_images/104-P16_4.jpg" --rec_model_dir="./inference/rec_ppocrv3/Student" +``` + +``` +[2022/07/14 10:55:56] ppocr INFO: In PP-OCRv3, rec_image_shape parameter defaults to '3, 48, 320', if you are using recognition model with PP-OCRv2 or an older version, please set --rec_image_shape='3,32,320 +[2022/07/14 10:55:58] ppocr INFO: Predicts of train_data/handwrite/HWDB2.0Test_images/104-P16_4.jpg:('品结构,差异化的多品牌渗透使欧莱雅确立了其在中国化妆', 0.9904912114143372) +``` + + +```python +# 可视化文字识别图片 +from PIL import Image +import matplotlib.pyplot as plt +import numpy as np +import os + + +img_path = 'train_data/handwrite/HWDB2.0Test_images/104-P16_4.jpg' + +def vis(img_path): + plt.figure() + image = Image.open(img_path) + plt.imshow(image) + plt.show() + # image = image.resize([208, 208]) + + +vis(img_path) +``` + + +![res](https://ai-studio-static-online.cdn.bcebos.com/ad7c02745491498d82e0ce95f4a274f9b3920b2f467646858709359b7af9d869) diff --git "a/ocr_webapp/PaddleOCR/applications/\346\211\253\346\217\217\345\220\210\345\220\214\345\205\263\351\224\256\344\277\241\346\201\257\346\217\220\345\217\226.md" "b/ocr_webapp/PaddleOCR/applications/\346\211\253\346\217\217\345\220\210\345\220\214\345\205\263\351\224\256\344\277\241\346\201\257\346\217\220\345\217\226.md" new file mode 100644 index 0000000000000000000000000000000000000000..be4574e64c2b9bcb88bc75ddf58179a0f3390d3d --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\346\211\253\346\217\217\345\220\210\345\220\214\345\205\263\351\224\256\344\277\241\346\201\257\346\217\220\345\217\226.md" @@ -0,0 +1,282 @@ +# 金融智能核验:扫描合同关键信息抽取 + +本案例将使用OCR技术和通用信息抽取技术,实现合同关键信息审核和比对。通过本章的学习,你可以快速掌握: + +1. 使用PaddleOCR提取扫描文本内容 +2. 使用PaddleNLP抽取自定义信息 + +点击进入 [AI Studio 项目](https://aistudio.baidu.com/aistudio/projectdetail/4545772) + +## 1. 项目背景 +合同审核广泛应用于大中型企业、上市公司、证券、基金公司中,是规避风险的重要任务。 +- 合同内容对比:合同审核场景中,快速找出不同版本合同修改区域、版本差异;如合同盖章归档场景中有效识别实际签署的纸质合同、电子版合同差异。 + +- 合规性检查:法务人员进行合同审核,如合同完备性检查、大小写金额检查、签约主体一致性检查、双方权利和义务对等性分析等。 + +- 风险点识别:通过合同审核可识别事实倾向型风险点和数值计算型风险点等,例如交付地点约定不明、合同总价款不一致、重要条款缺失等风险点。 + + +![](https://ai-studio-static-online.cdn.bcebos.com/d5143df967fa4364a38868793fe7c57b0c0b1213930243babd6ae01423dcbc4d) + +传统业务中大多使用人工进行纸质版合同审核,存在成本高,工作量大,效率低的问题,且一旦出错将造成巨额损失。 + + +本项目针对以上场景,使用PaddleOCR+PaddleNLP快速提取文本内容,经过少量数据微调即可准确抽取关键信息,**高效完成合同内容对比、合规性检查、风险点识别等任务,提高效率,降低风险**。 + +![](https://ai-studio-static-online.cdn.bcebos.com/54f3053e6e1b47a39b26e757006fe2c44910d60a3809422ab76c25396b92e69b) + + +## 2. 解决方案 + +### 2.1 扫描合同文本内容提取 + +使用PaddleOCR开源的模型可以快速完成扫描文档的文本内容提取,在清晰文档上识别准确率可达到95%+。下面来快速体验一下: + +#### 2.1.1 环境准备 + +[PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR)提供了适用于通用场景的高精轻量模型,提供数据预处理-模型推理-后处理全流程,支持pip安装: + +``` +python -m pip install paddleocr +``` + +#### 2.1.2 效果测试 + +使用一张合同图片作为测试样本,感受ppocrv3模型效果: + + + +使用中文检测+识别模型提取文本,实例化PaddleOCR类: + +``` +from paddleocr import PaddleOCR, draw_ocr + +# paddleocr目前支持中英文、英文、法语、德语、韩语、日语等80个语种,可以通过修改lang参数进行切换 +ocr = PaddleOCR(use_angle_cls=False, lang="ch") # need to run only once to download and load model into memory +``` + +一行命令启动预测,预测结果包括`检测框`和`文本识别内容`: + +``` +img_path = "./test_img/hetong2.jpg" +result = ocr.ocr(img_path, cls=False) +for line in result: + print(line) + +# 可视化结果 +from PIL import Image + +image = Image.open(img_path).convert('RGB') +boxes = [line[0] for line in result] +txts = [line[1][0] for line in result] +scores = [line[1][1] for line in result] +im_show = draw_ocr(image, boxes, txts, scores, font_path='./simfang.ttf') +im_show = Image.fromarray(im_show) +im_show.show() +``` + +#### 2.1.3 图片预处理 + +通过上图可视化结果可以看到,印章部分造成的文本遮盖,影响了文本识别结果,因此可以考虑通道提取,去除图片中的红色印章: + +``` +import cv2 +import numpy as np +import matplotlib.pyplot as plt + +#读入图像,三通道 +image=cv2.imread("./test_img/hetong2.jpg",cv2.IMREAD_COLOR) #timg.jpeg + +#获得三个通道 +Bch,Gch,Rch=cv2.split(image) + +#保存三通道图片 +cv2.imwrite('blue_channel.jpg',Bch) +cv2.imwrite('green_channel.jpg',Gch) +cv2.imwrite('red_channel.jpg',Rch) +``` +#### 2.1.4 合同文本信息提取 + +经过2.1.3的预处理后,合同照片的红色通道被分离,获得了一张相对更干净的图片,此时可以再次使用ppocr模型提取文本内容: + +``` +import numpy as np +import cv2 + + +img_path = './red_channel.jpg' +result = ocr.ocr(img_path, cls=False) + +# 可视化结果 +from PIL import Image + +image = Image.open(img_path).convert('RGB') +boxes = [line[0] for line in result] +txts = [line[1][0] for line in result] +scores = [line[1][1] for line in result] +im_show = draw_ocr(image, boxes, txts, scores, font_path='./simfang.ttf') +im_show = Image.fromarray(im_show) +vis = np.array(im_show) +im_show.show() +``` + +忽略检测框内容,提取完整的合同文本: + +``` +txts = [line[1][0] for line in result] +all_context = "\n".join(txts) +print(all_context) +``` + +通过以上环节就完成了扫描合同关键信息抽取的第一步:文本内容提取,接下来可以基于识别出的文本内容抽取关键信息 + +### 2.2 合同关键信息抽取 + +#### 2.2.1 环境准备 + +安装PaddleNLP + + +``` +pip install --upgrade pip +pip install --upgrade paddlenlp +``` + +#### 2.2.2 合同关键信息抽取 + +PaddleNLP 使用 Taskflow 统一管理多场景任务的预测功能,其中`information_extraction` 通过大量的有标签样本进行训练,在通用的场景中一般可以直接使用,只需更换关键字即可。例如在合同信息抽取中,我们重新定义抽取关键字: + +甲方、乙方、币种、金额、付款方式 + + +将使用OCR提取好的文本作为输入,使用三行命令可以对上文中提取到的合同文本进行关键信息抽取: + +``` +from paddlenlp import Taskflow +schema = ["甲方","乙方","总价"] +ie = Taskflow('information_extraction', schema=schema) +ie.set_schema(schema) +ie(all_context) +``` + +可以看到UIE模型可以准确的提取出关键信息,用于后续的信息比对或审核。 + +## 3.效果优化 + +### 3.1 文本识别后处理调优 + +实际图片采集过程中,可能出现部分图片弯曲等问题,导致使用默认参数识别文本时存在漏检,影响关键信息获取。 + +例如下图: + + + + +直接进行预测: + +``` +img_path = "./test_img/hetong3.jpg" +# 预测结果 +result = ocr.ocr(img_path, cls=False) +# 可视化结果 +from PIL import Image + +image = Image.open(img_path).convert('RGB') +boxes = [line[0] for line in result] +txts = [line[1][0] for line in result] +scores = [line[1][1] for line in result] +im_show = draw_ocr(image, boxes, txts, scores, font_path='./simfang.ttf') +im_show = Image.fromarray(im_show) +im_show.show() +``` + +可视化结果可以看到,弯曲图片存在漏检,一般来说可以通过调整后处理参数解决,无需重新训练模型。漏检问题往往是因为检测模型获得的分割图太小,生成框的得分过低被过滤掉了,通常有两种方式调整参数: +- 开启`use_dilatiion=True` 膨胀分割区域 +- 调小`det_db_box_thresh`阈值 + +``` +# 重新实例化 PaddleOCR +ocr = PaddleOCR(use_angle_cls=False, lang="ch", det_db_box_thresh=0.3, use_dilation=True) + +# 预测并可视化 +img_path = "./test_img/hetong3.jpg" +# 预测结果 +result = ocr.ocr(img_path, cls=False) +# 可视化结果 +image = Image.open(img_path).convert('RGB') +boxes = [line[0] for line in result] +txts = [line[1][0] for line in result] +scores = [line[1][1] for line in result] +im_show = draw_ocr(image, boxes, txts, scores, font_path='./simfang.ttf') +im_show = Image.fromarray(im_show) +im_show.show() +``` + +可以看到漏检问题被很好的解决,提取完整的文本内容: + +``` +txts = [line[1][0] for line in result] +context = "\n".join(txts) +print(context) +``` + +### 3.2 关键信息提取调优 + +UIE通过大量有标签样本进行训练,得到了一个开箱即用的高精模型。 然而针对不同场景,可能会出现部分实体无法被抽取的情况。通常来说有以下几个方法进行效果调优: + + +- 修改 schema +- 添加正则方法 +- 标注小样本微调模型 + +**修改schema** + +Prompt和原文描述越像,抽取效果越好,例如 +``` +三:合同价格:总价为人民币大写:参拾玖万捌仟伍佰 +元,小写:398500.00元。总价中包括站房工程建设、安装 +及相关避雷、消防、接地、电力、材料费、检验费、安全、 +验收等所需费用及其他相关费用和税金。 +``` +schema = ["总金额"] 时无法准确抽取,与原文描述差异较大。 修改 schema = ["总价"] 再次尝试: + +``` +from paddlenlp import Taskflow +# schema = ["总金额"] +schema = ["总价"] +ie = Taskflow('information_extraction', schema=schema) +ie.set_schema(schema) +ie(all_context) +``` + + +**模型微调** + +UIE的建模方式主要是通过 `Prompt` 方式来建模, `Prompt` 在小样本上进行微调效果非常有效。详细的数据标注+模型微调步骤可以参考项目: + +[PaddleNLP信息抽取技术重磅升级!](https://aistudio.baidu.com/aistudio/projectdetail/3914778?channelType=0&channel=0) + +[工单信息抽取](https://aistudio.baidu.com/aistudio/projectdetail/3914778?contributionType=1) + +[快递单信息抽取](https://aistudio.baidu.com/aistudio/projectdetail/4038499?contributionType=1) + + +## 总结 + +扫描合同的关键信息提取可以使用 PaddleOCR + PaddleNLP 组合实现,两个工具均有以下优势: + +* 使用简单:whl包一键安装,3行命令调用 +* 效果领先:优秀的模型效果可覆盖几乎全部的应用场景 +* 调优成本低:OCR模型可通过后处理参数的调整适配略有偏差的扫描文本, UIE模型可以通过极少的标注样本微调,成本很低。 + +## 作业 + +尝试自己解析出 `test_img/homework.png` 扫描合同中的 [甲方、乙方] 关键词: + + + + + +更多场景下的垂类模型获取,请加入PaddleX官方交流频道,获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 diff --git "a/ocr_webapp/PaddleOCR/applications/\346\266\262\346\231\266\345\261\217\350\257\273\346\225\260\350\257\206\345\210\253.md" "b/ocr_webapp/PaddleOCR/applications/\346\266\262\346\231\266\345\261\217\350\257\273\346\225\260\350\257\206\345\210\253.md" new file mode 100644 index 0000000000000000000000000000000000000000..9e11407e0cb47a0854393823ca337e9c64849443 --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\346\266\262\346\231\266\345\261\217\350\257\273\346\225\260\350\257\206\345\210\253.md" @@ -0,0 +1,616 @@ +# 基于PP-OCRv3的液晶屏读数识别 + +- [1. 项目背景及意义](#1-项目背景及意义) +- [2. 项目内容](#2-项目内容) +- [3. 安装环境](#3-安装环境) +- [4. 文字检测](#4-文字检测) + - [4.1 PP-OCRv3检测算法介绍](#41-PP-OCRv3检测算法介绍) + - [4.2 数据准备](#42-数据准备) + - [4.3 模型训练](#43-模型训练) + - [4.3.1 预训练模型直接评估](#431-预训练模型直接评估) + - [4.3.2 预训练模型直接finetune](#432-预训练模型直接finetune) + - [4.3.3 基于预训练模型Finetune_student模型](#433-基于预训练模型Finetune_student模型) + - [4.3.4 基于预训练模型Finetune_teacher模型](#434-基于预训练模型Finetune_teacher模型) + - [4.3.5 采用CML蒸馏进一步提升student模型精度](#435-采用CML蒸馏进一步提升student模型精度) + - [4.3.6 模型导出推理](#436-4.3.6-模型导出推理) +- [5. 文字识别](#5-文字识别) + - [5.1 PP-OCRv3识别算法介绍](#51-PP-OCRv3识别算法介绍) + - [5.2 数据准备](#52-数据准备) + - [5.3 模型训练](#53-模型训练) + - [5.4 模型导出推理](#54-模型导出推理) +- [6. 系统串联](#6-系统串联) + - [6.1 后处理](#61-后处理) +- [7. PaddleServing部署](#7-PaddleServing部署) + + +## 1. 项目背景及意义 +目前光学字符识别(OCR)技术在我们的生活当中被广泛使用,但是大多数模型在通用场景下的准确性还有待提高,针对于此我们借助飞桨提供的PaddleOCR套件较容易的实现了在垂类场景下的应用。 + +该项目以国家质量基础(NQI)为准绳,充分利用大数据、云计算、物联网等高新技术,构建覆盖计量端、实验室端、数据端和硬件端的完整计量解决方案,解决传统计量校准中存在的难题,拓宽计量检测服务体系和服务领域;解决无数传接口或数传接口不统一、不公开的计量设备,以及计量设备所处的环境比较恶劣,不适合人工读取数据。通过OCR技术实现远程计量,引领计量行业向智慧计量转型和发展。 + +## 2. 项目内容 +本项目基于PaddleOCR开源套件,以PP-OCRv3检测和识别模型为基础,针对液晶屏读数识别场景进行优化。 + +Aistudio项目链接:[OCR液晶屏读数识别](https://aistudio.baidu.com/aistudio/projectdetail/4080130) + +## 3. 安装环境 + +```python +# 首先git官方的PaddleOCR项目,安装需要的依赖 +# 第一次运行打开该注释 +# git clone https://gitee.com/PaddlePaddle/PaddleOCR.git +cd PaddleOCR +pip install -r requirements.txt +``` + +## 4. 文字检测 +文本检测的任务是定位出输入图像中的文字区域。近年来学术界关于文本检测的研究非常丰富,一类方法将文本检测视为目标检测中的一个特定场景,基于通用目标检测算法进行改进适配,如TextBoxes[1]基于一阶段目标检测器SSD[2]算法,调整目标框使之适合极端长宽比的文本行,CTPN[3]则是基于Faster RCNN[4]架构改进而来。但是文本检测与目标检测在目标信息以及任务本身上仍存在一些区别,如文本一般长宽比较大,往往呈“条状”,文本行之间可能比较密集,弯曲文本等,因此又衍生了很多专用于文本检测的算法。本项目基于PP-OCRv3算法进行优化。 + +### 4.1 PP-OCRv3检测算法介绍 +PP-OCRv3检测模型是对PP-OCRv2中的CML(Collaborative Mutual Learning) 协同互学习文本检测蒸馏策略进行了升级。如下图所示,CML的核心思想结合了①传统的Teacher指导Student的标准蒸馏与 ②Students网络之间的DML互学习,可以让Students网络互学习的同时,Teacher网络予以指导。PP-OCRv3分别针对教师模型和学生模型进行进一步效果优化。其中,在对教师模型优化时,提出了大感受野的PAN结构LK-PAN和引入了DML(Deep Mutual Learning)蒸馏策略;在对学生模型优化时,提出了残差注意力机制的FPN结构RSE-FPN。 +![](https://ai-studio-static-online.cdn.bcebos.com/c306b2f028364805a55494d435ab553a76cf5ae5dd3f4649a948ea9aeaeb28b8) + +详细优化策略描述请参考[PP-OCRv3优化策略](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/PP-OCRv3_introduction.md#2) + +### 4.2 数据准备 +[计量设备屏幕字符检测数据集](https://aistudio.baidu.com/aistudio/datasetdetail/127845)数据来源于实际项目中各种计量设备的数显屏,以及在网上搜集的一些其他数显屏,包含训练集755张,测试集355张。 + +```python +# 在PaddleOCR下创建新的文件夹train_data +mkdir train_data +# 下载数据集并解压到指定路径下 +unzip icdar2015.zip -d train_data +``` + +```python +# 随机查看文字检测数据集图片 +from PIL import Image +import matplotlib.pyplot as plt +import numpy as np +import os + + +train = './train_data/icdar2015/text_localization/test' +# 从指定目录中选取一张图片 +def get_one_image(train): + plt.figure() + files = os.listdir(train) + n = len(files) + ind = np.random.randint(0,n) + img_dir = os.path.join(train,files[ind]) + image = Image.open(img_dir) + plt.imshow(image) + plt.show() + image = image.resize([208, 208]) + +get_one_image(train) +``` +![det_png](https://ai-studio-static-online.cdn.bcebos.com/0639da09b774458096ae577e82b2c59e89ced6a00f55458f946997ab7472a4f8) + +### 4.3 模型训练 + +#### 4.3.1 预训练模型直接评估 +下载我们需要的PP-OCRv3检测预训练模型,更多选择请自行选择其他的[文字检测模型](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/models_list.md#1-%E6%96%87%E6%9C%AC%E6%A3%80%E6%B5%8B%E6%A8%A1%E5%9E%8B) + +```python +#使用该指令下载需要的预训练模型 +wget -P ./pretrained_models/ https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_distill_train.tar +# 解压预训练模型文件 +tar -xf ./pretrained_models/ch_PP-OCRv3_det_distill_train.tar -C pretrained_models +``` + +在训练之前,我们可以直接使用下面命令来评估预训练模型的效果: + +```python +# 评估预训练模型 +python tools/eval.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml -o Global.pretrained_model="./pretrained_models/ch_PP-OCRv3_det_distill_train/best_accuracy" +``` + +结果如下: + +| | 方案 |hmeans| +|---|---------------------------|---| +| 0 | PP-OCRv3中英文超轻量检测预训练模型直接预测 |47.50%| + +#### 4.3.2 预训练模型直接finetune +##### 修改配置文件 +我们使用configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml,主要修改训练轮数和学习率参相关参数,设置预训练模型路径,设置数据集路径。 另外,batch_size可根据自己机器显存大小进行调整。 具体修改如下几个地方: +``` +epoch:100 +save_epoch_step:10 +eval_batch_step:[0, 50] +save_model_dir: ./output/ch_PP-OCR_v3_det/ +pretrained_model: ./pretrained_models/ch_PP-OCRv3_det_distill_train/best_accuracy +learning_rate: 0.00025 +num_workers: 0 # 如果单卡训练,建议将Train和Eval的loader部分的num_workers设置为0,否则会出现`/dev/shm insufficient`的报错 +``` + +##### 开始训练 +使用我们上面修改的配置文件configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml,训练命令如下: + +```python +# 开始训练模型 +python tools/train.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml -o Global.pretrained_model=./pretrained_models/ch_PP-OCRv3_det_distill_train/best_accuracy +``` + +评估训练好的模型: + +```python +# 评估训练好的模型 +python tools/eval.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml -o Global.pretrained_model="./output/ch_PP-OCR_v3_det/best_accuracy" +``` + +结果如下: +| | 方案 |hmeans| +|---|---------------------------|---| +| 0 | PP-OCRv3中英文超轻量检测预训练模型直接预测 |47.50%| +| 1 | PP-OCRv3中英文超轻量检测预训练模型fintune |65.20%| + +#### 4.3.3 基于预训练模型Finetune_student模型 + +我们使用configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml,主要修改训练轮数和学习率参相关参数,设置预训练模型路径,设置数据集路径。 另外,batch_size可根据自己机器显存大小进行调整。 具体修改如下几个地方: +``` +epoch:100 +save_epoch_step:10 +eval_batch_step:[0, 50] +save_model_dir: ./output/ch_PP-OCR_v3_det_student/ +pretrained_model: ./pretrained_models/ch_PP-OCRv3_det_distill_train/student +learning_rate: 0.00025 +num_workers: 0 # 如果单卡训练,建议将Train和Eval的loader部分的num_workers设置为0,否则会出现`/dev/shm insufficient`的报错 +``` + +训练命令如下: + +```python +python tools/train.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml -o Global.pretrained_model=./pretrained_models/ch_PP-OCRv3_det_distill_train/student +``` + +评估训练好的模型: + +```python +# 评估训练好的模型 +python tools/eval.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml -o Global.pretrained_model="./output/ch_PP-OCR_v3_det_student/best_accuracy" +``` + +结果如下: +| | 方案 |hmeans| +|---|---------------------------|---| +| 0 | PP-OCRv3中英文超轻量检测预训练模型直接预测 |47.50%| +| 1 | PP-OCRv3中英文超轻量检测预训练模型fintune |65.20%| +| 2 | PP-OCRv3中英文超轻量检测预训练模型fintune学生模型 |80.00%| + +#### 4.3.4 基于预训练模型Finetune_teacher模型 + +首先需要从提供的预训练模型best_accuracy.pdparams中提取teacher参数,组合成适合dml训练的初始化模型,提取代码如下: + +```python +cd ./pretrained_models/ +# transform teacher params in best_accuracy.pdparams into teacher_dml.paramers +import paddle + +# load pretrained model +all_params = paddle.load("ch_PP-OCRv3_det_distill_train/best_accuracy.pdparams") +# print(all_params.keys()) + +# keep teacher params +t_params = {key[len("Teacher."):]: all_params[key] for key in all_params if "Teacher." in key} + +# print(t_params.keys()) + +s_params = {"Student." + key: t_params[key] for key in t_params} +s2_params = {"Student2." + key: t_params[key] for key in t_params} +s_params = {**s_params, **s2_params} +# print(s_params.keys()) + +paddle.save(s_params, "ch_PP-OCRv3_det_distill_train/teacher_dml.pdparams") + +``` + +我们使用configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_dml.yml,主要修改训练轮数和学习率参相关参数,设置预训练模型路径,设置数据集路径。 另外,batch_size可根据自己机器显存大小进行调整。 具体修改如下几个地方: +``` +epoch:100 +save_epoch_step:10 +eval_batch_step:[0, 50] +save_model_dir: ./output/ch_PP-OCR_v3_det_teacher/ +pretrained_model: ./pretrained_models/ch_PP-OCRv3_det_distill_train/teacher_dml +learning_rate: 0.00025 +num_workers: 0 # 如果单卡训练,建议将Train和Eval的loader部分的num_workers设置为0,否则会出现`/dev/shm insufficient`的报错 +``` + +训练命令如下: + +```python +python tools/train.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_dml.yml -o Global.pretrained_model=./pretrained_models/ch_PP-OCRv3_det_distill_train/teacher_dml +``` + +评估训练好的模型: + +```python +# 评估训练好的模型 +python tools/eval.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_dml.yml -o Global.pretrained_model="./output/ch_PP-OCR_v3_det_teacher/best_accuracy" +``` + +结果如下: +| | 方案 |hmeans| +|---|---------------------------|---| +| 0 | PP-OCRv3中英文超轻量检测预训练模型直接预测 |47.50%| +| 1 | PP-OCRv3中英文超轻量检测预训练模型fintune |65.20%| +| 2 | PP-OCRv3中英文超轻量检测预训练模型fintune学生模型 |80.00%| +| 3 | PP-OCRv3中英文超轻量检测预训练模型fintune教师模型 |84.80%| + +#### 4.3.5 采用CML蒸馏进一步提升student模型精度 + +需要从4.3.3和4.3.4训练得到的best_accuracy.pdparams中提取各自代表student和teacher的参数,组合成适合cml训练的初始化模型,提取代码如下: + +```python +# transform teacher params and student parameters into cml model +import paddle + +all_params = paddle.load("./pretrained_models/ch_PP-OCRv3_det_distill_train/best_accuracy.pdparams") +# print(all_params.keys()) + +t_params = paddle.load("./output/ch_PP-OCR_v3_det_teacher/best_accuracy.pdparams") +# print(t_params.keys()) + +s_params = paddle.load("./output/ch_PP-OCR_v3_det_student/best_accuracy.pdparams") +# print(s_params.keys()) + +for key in all_params: + # teacher is OK + if "Teacher." in key: + new_key = key.replace("Teacher", "Student") + #print("{} >> {}\n".format(key, new_key)) + assert all_params[key].shape == t_params[new_key].shape + all_params[key] = t_params[new_key] + + if "Student." in key: + new_key = key.replace("Student.", "") + #print("{} >> {}\n".format(key, new_key)) + assert all_params[key].shape == s_params[new_key].shape + all_params[key] = s_params[new_key] + + if "Student2." in key: + new_key = key.replace("Student2.", "") + print("{} >> {}\n".format(key, new_key)) + assert all_params[key].shape == s_params[new_key].shape + all_params[key] = s_params[new_key] + +paddle.save(all_params, "./pretrained_models/ch_PP-OCRv3_det_distill_train/teacher_cml_student.pdparams") +``` + +训练命令如下: + +```python +python tools/train.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml -o Global.pretrained_model=./pretrained_models/ch_PP-OCRv3_det_distill_train/teacher_cml_student Global.save_model_dir=./output/ch_PP-OCR_v3_det_finetune/ +``` + +评估训练好的模型: + +```python +# 评估训练好的模型 +python tools/eval.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml -o Global.pretrained_model="./output/ch_PP-OCR_v3_det_finetune/best_accuracy" +``` + +结果如下: +| | 方案 |hmeans| +|---|---------------------------|---| +| 0 | PP-OCRv3中英文超轻量检测预训练模型直接预测 |47.50%| +| 1 | PP-OCRv3中英文超轻量检测预训练模型fintune |65.20%| +| 2 | PP-OCRv3中英文超轻量检测预训练模型fintune学生模型 |80.00%| +| 3 | PP-OCRv3中英文超轻量检测预训练模型fintune教师模型 |84.80%| +| 4 | 基于2和3训练好的模型fintune |82.70%| + +如需获取已训练模型,请加入PaddleX官方交流频道,获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 + +将下载或训练完成的模型放置在对应目录下即可完成模型推理 + +#### 4.3.6 模型导出推理 +训练完成后,可以将训练模型转换成inference模型。inference 模型会额外保存模型的结构信息,在预测部署、加速推理上性能优越,灵活方便,适合于实际系统集成。 +##### 4.3.6.1 模型导出 +导出命令如下: + +```python +# 转化为推理模型 +python tools/export_model.py \ +-c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml \ +-o Global.pretrained_model=./output/ch_PP-OCR_v3_det_finetune/best_accuracy \ +-o Global.save_inference_dir="./inference/det_ppocrv3" + +``` + +##### 4.3.6.2 模型推理 +导出模型后,可以使用如下命令进行推理预测: + +```python +# 推理预测 +python tools/infer/predict_det.py --image_dir="train_data/icdar2015/text_localization/test/1.jpg" --det_model_dir="./inference/det_ppocrv3/Student" +``` + +## 5. 文字识别 +文本识别的任务是识别出图像中的文字内容,一般输入来自于文本检测得到的文本框截取出的图像文字区域。文本识别一般可以根据待识别文本形状分为规则文本识别和不规则文本识别两大类。规则文本主要指印刷字体、扫描文本等,文本大致处在水平线位置;不规则文本往往不在水平位置,存在弯曲、遮挡、模糊等问题。不规则文本场景具有很大的挑战性,也是目前文本识别领域的主要研究方向。本项目基于PP-OCRv3算法进行优化。 + +### 5.1 PP-OCRv3识别算法介绍 +PP-OCRv3的识别模块是基于文本识别算法[SVTR](https://arxiv.org/abs/2205.00159)优化。SVTR不再采用RNN结构,通过引入Transformers结构更加有效地挖掘文本行图像的上下文信息,从而提升文本识别能力。如下图所示,PP-OCRv3采用了6个优化策略。 +![](https://ai-studio-static-online.cdn.bcebos.com/d4f5344b5b854d50be738671598a89a45689c6704c4d481fb904dd7cf72f2a1a) + +优化策略汇总如下: +* SVTR_LCNet:轻量级文本识别网络 +* GTC:Attention指导CTC训练策略 +* TextConAug:挖掘文字上下文信息的数据增广策略 +* TextRotNet:自监督的预训练模型 +* UDML:联合互学习策略 +* UIM:无标注数据挖掘方案 + +详细优化策略描述请参考[PP-OCRv3优化策略](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/PP-OCRv3_introduction.md#3-%E8%AF%86%E5%88%AB%E4%BC%98%E5%8C%96) + +### 5.2 数据准备 +[计量设备屏幕字符识别数据集](https://aistudio.baidu.com/aistudio/datasetdetail/128714)数据来源于实际项目中各种计量设备的数显屏,以及在网上搜集的一些其他数显屏,包含训练集19912张,测试集4099张。 + +```python +# 解压下载的数据集到指定路径下 +unzip ic15_data.zip -d train_data +``` + +```python +# 随机查看文字检测数据集图片 +from PIL import Image +import matplotlib.pyplot as plt +import numpy as np +import os + +train = './train_data/ic15_data/train' +# 从指定目录中选取一张图片 +def get_one_image(train): + plt.figure() + files = os.listdir(train) + n = len(files) + ind = np.random.randint(0,n) + img_dir = os.path.join(train,files[ind]) + image = Image.open(img_dir) + plt.imshow(image) + plt.show() + image = image.resize([208, 208]) + +get_one_image(train) +``` + +![rec_png](https://ai-studio-static-online.cdn.bcebos.com/3de0d475c69746d0a184029001ef07c85fd68816d66d4beaa10e6ef60030f9b4) + +### 5.3 模型训练 +#### 下载预训练模型 +下载我们需要的PP-OCRv3识别预训练模型,更多选择请自行选择其他的[文字识别模型](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.5/doc/doc_ch/models_list.md#2-%E6%96%87%E6%9C%AC%E8%AF%86%E5%88%AB%E6%A8%A1%E5%9E%8B) + +```python +# 使用该指令下载需要的预训练模型 +wget -P ./pretrained_models/ https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_train.tar +# 解压预训练模型文件 +tar -xf ./pretrained_models/ch_PP-OCRv3_rec_train.tar -C pretrained_models +``` + +#### 修改配置文件 +我们使用configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml,主要修改训练轮数和学习率参相关参数,设置预训练模型路径,设置数据集路径。 另外,batch_size可根据自己机器显存大小进行调整。 具体修改如下几个地方: +``` + epoch_num: 100 # 训练epoch数 + save_model_dir: ./output/ch_PP-OCR_v3_rec + save_epoch_step: 10 + eval_batch_step: [0, 100] # 评估间隔,每隔100step评估一次 + cal_metric_during_train: true + pretrained_model: ./pretrained_models/ch_PP-OCRv3_rec_train/best_accuracy # 预训练模型路径 + character_dict_path: ppocr/utils/ppocr_keys_v1.txt + use_space_char: true # 使用空格 + + lr: + name: Cosine # 修改学习率衰减策略为Cosine + learning_rate: 0.0002 # 修改fine-tune的学习率 + warmup_epoch: 2 # 修改warmup轮数 + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/ic15_data/ # 训练集图片路径 + ext_op_transform_idx: 1 + label_file_list: + - ./train_data/ic15_data/rec_gt_train.txt # 训练集标签 + ratio_list: + - 1.0 + loader: + shuffle: true + batch_size_per_card: 64 + drop_last: true + num_workers: 4 +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/ic15_data/ # 测试集图片路径 + label_file_list: + - ./train_data/ic15_data/rec_gt_test.txt # 测试集标签 + ratio_list: + - 1.0 + loader: + shuffle: false + drop_last: false + batch_size_per_card: 64 + num_workers: 4 +``` + +在训练之前,我们可以直接使用下面命令来评估预训练模型的效果: + +```python +# 评估预训练模型 +python tools/eval.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml -o Global.pretrained_model="./pretrained_models/ch_PP-OCRv3_rec_train/best_accuracy" +``` + +结果如下: +| | 方案 |accuracy| +|---|---------------------------|---| +| 0 | PP-OCRv3中英文超轻量识别预训练模型直接预测 |70.40%| + +#### 开始训练 +我们使用上面修改好的配置文件configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml,预训练模型,数据集路径,学习率,训练轮数等都已经设置完毕后,可以使用下面命令开始训练。 + +```python +# 开始训练识别模型 +python tools/train.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml +``` + +训练完成后,可以对训练模型中最好的进行测试,评估命令如下: + +```python +# 评估finetune效果 +python tools/eval.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml -o Global.checkpoints="./output/ch_PP-OCR_v3_rec/best_accuracy" +``` + +结果如下: +| | 方案 |accuracy| +|---|---------------------------|---| +| 0 | PP-OCRv3中英文超轻量识别预训练模型直接预测 |70.40%| +| 1 | PP-OCRv3中英文超轻量识别预训练模型finetune |82.20%| + +如需获取已训练模型,请扫码填写问卷,加入PaddleOCR官方交流群获取全部OCR垂类模型下载链接、《动手学OCR》电子书等全套OCR学习资料🎁 +
+ +
+将下载或训练完成的模型放置在对应目录下即可完成模型推理。 + +### 5.4 模型导出推理 +训练完成后,可以将训练模型转换成inference模型。inference 模型会额外保存模型的结构信息,在预测部署、加速推理上性能优越,灵活方便,适合于实际系统集成。 +#### 模型导出 +导出命令如下: + +```python +# 转化为推理模型 +python tools/export_model.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml -o Global.pretrained_model="./output/ch_PP-OCR_v3_rec/best_accuracy" Global.save_inference_dir="./inference/rec_ppocrv3/" +``` + +#### 模型推理 +导出模型后,可以使用如下命令进行推理预测 + +```python +# 推理预测 +python tools/infer/predict_rec.py --image_dir="train_data/ic15_data/test/1_crop_0.jpg" --rec_model_dir="./inference/rec_ppocrv3/Student" +``` + +## 6. 系统串联 +我们将上面训练好的检测和识别模型进行系统串联测试,命令如下: + +```python +#串联测试 +python3 tools/infer/predict_system.py --image_dir="./train_data/icdar2015/text_localization/test/142.jpg" --det_model_dir="./inference/det_ppocrv3/Student" --rec_model_dir="./inference/rec_ppocrv3/Student" +``` + +测试结果保存在`./inference_results/`目录下,可以用下面代码进行可视化 + +```python +%cd /home/aistudio/PaddleOCR +# 显示结果 +import matplotlib.pyplot as plt +from PIL import Image +img_path= "./inference_results/142.jpg" +img = Image.open(img_path) +plt.figure("test_img", figsize=(30,30)) +plt.imshow(img) +plt.show() +``` + +![sys_res_png](https://ai-studio-static-online.cdn.bcebos.com/901ab741cb46441ebec510b37e63b9d8d1b7c95f63cc4e5e8757f35179ae6373) + +### 6.1 后处理 +如果需要获取key-value信息,可以基于启发式的规则,将识别结果与关键字库进行匹配;如果匹配上了,则取该字段为key, 后面一个字段为value。 + +```python +def postprocess(rec_res): + keys = ["型号", "厂家", "版本号", "检定校准分类", "计量器具编号", "烟尘流量", + "累积体积", "烟气温度", "动压", "静压", "时间", "试验台编号", "预测流速", + "全压", "烟温", "流速", "工况流量", "标杆流量", "烟尘直读嘴", "烟尘采样嘴", + "大气压", "计前温度", "计前压力", "干球温度", "湿球温度", "流量", "含湿量"] + key_value = [] + if len(rec_res) > 1: + for i in range(len(rec_res) - 1): + rec_str, _ = rec_res[i] + for key in keys: + if rec_str in key: + key_value.append([rec_str, rec_res[i + 1][0]]) + break + return key_value +key_value = postprocess(filter_rec_res) +``` + +## 7. PaddleServing部署 +首先需要安装PaddleServing部署相关的环境 + +```python +python -m pip install paddle-serving-server-gpu +python -m pip install paddle_serving_client +python -m pip install paddle-serving-app +``` + +### 7.1 转化检测模型 + +```python +cd deploy/pdserving/ +python -m paddle_serving_client.convert --dirname ../../inference/det_ppocrv3/Student/ \ + --model_filename inference.pdmodel \ + --params_filename inference.pdiparams \ + --serving_server ./ppocr_det_v3_serving/ \ + --serving_client ./ppocr_det_v3_client/ +``` + +### 7.2 转化识别模型 + +```python +python -m paddle_serving_client.convert --dirname ../../inference/rec_ppocrv3/Student \ + --model_filename inference.pdmodel \ + --params_filename inference.pdiparams \ + --serving_server ./ppocr_rec_v3_serving/ \ + --serving_client ./ppocr_rec_v3_client/ +``` + + +### 7.3 启动服务 +首先可以将后处理代码加入到web_service.py中,具体修改如下: +``` +# 代码153行后面增加下面代码 +def _postprocess(rec_res): + keys = ["型号", "厂家", "版本号", "检定校准分类", "计量器具编号", "烟尘流量", + "累积体积", "烟气温度", "动压", "静压", "时间", "试验台编号", "预测流速", + "全压", "烟温", "流速", "工况流量", "标杆流量", "烟尘直读嘴", "烟尘采样嘴", + "大气压", "计前温度", "计前压力", "干球温度", "湿球温度", "流量", "含湿量"] + key_value = [] + if len(rec_res) > 1: + for i in range(len(rec_res) - 1): + rec_str, _ = rec_res[i] + for key in keys: + if rec_str in key: + key_value.append([rec_str, rec_res[i + 1][0]]) + break + return key_value +key_value = _postprocess(rec_list) +res = {"result": str(key_value)} +# res = {"result": str(result_list)} +``` + +启动服务端 +```python +python web_service.py 2>&1 >log.txt +``` + +### 7.4 发送请求 +然后再开启一个新的终端,运行下面的客户端代码 + +```python +python pipeline_http_client.py --image_dir ../../train_data/icdar2015/text_localization/test/142.jpg +``` + +可以获取到最终的key-value结果: +``` +大气压, 100.07kPa +干球温度, 0000℃ +计前温度, 0000℃ +湿球温度, 0000℃ +计前压力, -0000kPa +流量, 00.0L/min +静压, 00000kPa +含湿量, 00.0 % +``` diff --git "a/ocr_webapp/PaddleOCR/applications/\350\275\273\351\207\217\347\272\247\350\275\246\347\211\214\350\257\206\345\210\253.md" "b/ocr_webapp/PaddleOCR/applications/\350\275\273\351\207\217\347\272\247\350\275\246\347\211\214\350\257\206\345\210\253.md" new file mode 100644 index 0000000000000000000000000000000000000000..297cda07c0365d1c375abace3ceab57a22559d28 --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\350\275\273\351\207\217\347\272\247\350\275\246\347\211\214\350\257\206\345\210\253.md" @@ -0,0 +1,832 @@ +# 一种基于PaddleOCR的轻量级车牌识别模型 + +- [1. 项目介绍](#1-项目介绍) +- [2. 环境搭建](#2-环境搭建) +- [3. 数据集准备](#3-数据集准备) + - [3.1 数据集标注规则](#31-数据集标注规则) + - [3.2 制作符合PP-OCR训练格式的标注文件](#32-制作符合pp-ocr训练格式的标注文件) +- [4. 实验](#4-实验) + - [4.1 检测](#41-检测) + - [4.1.1 预训练模型直接预测](#411-预训练模型直接预测) + - [4.1.2 CCPD车牌数据集fine-tune](#412-ccpd车牌数据集fine-tune) + - [4.1.3 CCPD车牌数据集fine-tune+量化训练](#413-ccpd车牌数据集fine-tune量化训练) + - [4.1.4 模型导出](#414-模型导出) + - [4.2 识别](#42-识别) + - [4.2.1 预训练模型直接预测](#421-预训练模型直接预测) + - [4.2.2 预训练模型直接预测+改动后处理](#422-预训练模型直接预测改动后处理) + - [4.2.3 CCPD车牌数据集fine-tune](#423-ccpd车牌数据集fine-tune) + - [4.2.4 CCPD车牌数据集fine-tune+量化训练](#424-ccpd车牌数据集fine-tune量化训练) + - [4.2.5 模型导出](#425-模型导出) + - [4.3 计算End2End指标](#43-计算End2End指标) + - [4.4 部署](#44-部署) + - [4.5 实验总结](#45-实验总结) + +## 1. 项目介绍 + +车牌识别(Vehicle License Plate Recognition,VLPR) 是计算机视频图像识别技术在车辆牌照识别中的一种应用。车牌识别技术要求能够将运动中的汽车牌照从复杂背景中提取并识别出来,在高速公路车辆管理,停车场管理和城市交通中得到广泛应用。 + +本项目难点如下: + +1. 车牌在图像中的尺度差异大、在车辆上的悬挂位置不固定 +2. 车牌图像质量层次不齐: 角度倾斜、图片模糊、光照不足、过曝等问题严重 +3. 边缘和端测场景应用对模型大小有限制,推理速度有要求 + +针对以上问题, 本例选用 PP-OCRv3 这一开源超轻量OCR系统进行车牌识别系统的开发。基于PP-OCRv3模型,在CCPD数据集达到99%的检测和94%的识别精度,模型大小12.8M(2.5M+10.3M)。基于量化对模型体积进行进一步压缩到5.8M(1M+4.8M), 同时推理速度提升25%。 + + + +aistudio项目链接: [基于PaddleOCR的轻量级车牌识别范例](https://aistudio.baidu.com/aistudio/projectdetail/3919091?contributionType=1) + +## 2. 环境搭建 + +本任务基于Aistudio完成, 具体环境如下: + +- 操作系统: Linux +- PaddlePaddle: 2.3 +- paddleslim: 2.2.2 +- PaddleOCR: Release/2.5 + +下载 PaddleOCR代码 + +```bash +git clone -b dygraph https://github.com/PaddlePaddle/PaddleOCR +``` + +安装依赖库 + +```bash +pip install -r PaddleOCR/requirements.txt +``` + +## 3. 数据集准备 + +所使用的数据集为 CCPD2020 新能源车牌数据集,该数据集为 + +该数据集分布如下: + +|数据集类型|数量| +|---|---| +|训练集| 5769| +|验证集| 1001| +|测试集| 5006| + +数据集图片示例如下: +![](https://ai-studio-static-online.cdn.bcebos.com/3bce057a8e0c40a0acbd26b2e29e4e2590a31bc412764be7b9e49799c69cb91c) + +数据集可以从这里下载 https://aistudio.baidu.com/aistudio/datasetdetail/101595 + +下载好数据集后对数据集进行解压 + +```bash +unzip -d /home/aistudio/data /home/aistudio/data/data101595/CCPD2020.zip +``` + +### 3.1 数据集标注规则 + +CPPD数据集的图片文件名具有特殊规则,详细可查看:https://github.com/detectRecog/CCPD + +具体规则如下: + +例如: 025-95_113-154&383_386&473-386&473_177&454_154&383_363&402-0_0_22_27_27_33_16-37-15.jpg + +每个名称可以分为七个字段,以-符号作为分割。这些字段解释如下。 + +- 025:车牌面积与整个图片区域的面积比。025 (25%) + +- 95_113:水平倾斜程度和垂直倾斜度。水平 95度 垂直 113度 + +- 154&383_386&473:左上和右下顶点的坐标。左上(154,383) 右下(386,473) + +- 386&473_177&454_154&383_363&402:整个图像中车牌的四个顶点的精确(x,y)坐标。这些坐标从右下角顶点开始。(386,473) (177,454) (154,383) (363,402) + +- 0_0_22_27_27_33_16:CCPD中的每个图像只有一个车牌。每个车牌号码由一个汉字,一个字母和五个字母或数字组成。有效的中文车牌由七个字符组成:省(1个字符),字母(1个字符),字母+数字(5个字符)。“ 0_0_22_27_27_33_16”是每个字符的索引。这三个数组定义如下。每个数组的最后一个字符是字母O,而不是数字0。我们将O用作“无字符”的符号,因为中文车牌字符中没有O。因此以上车牌拼起来即为 皖AY339S + +- 37:牌照区域的亮度。 37 (37%) + +- 15:车牌区域的模糊度。15 (15%) + +```python +provinces = ["皖", "沪", "津", "渝", "冀", "晋", "蒙", "辽", "吉", "黑", "苏", "浙", "京", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂", "琼", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新", "警", "学", "O"] +alphabets = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W','X', 'Y', 'Z', 'O'] +ads = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X','Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'O'] +``` + +### 3.2 制作符合PP-OCR训练格式的标注文件 + +在开始训练之前,可使用如下代码制作符合PP-OCR训练格式的标注文件。 + + +```python +import cv2 +import os +import json +from tqdm import tqdm +import numpy as np + +provinces = ["皖", "沪", "津", "渝", "冀", "晋", "蒙", "辽", "吉", "黑", "苏", "浙", "京", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂", "琼", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新", "警", "学", "O"] +alphabets = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'O'] +ads = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'O'] + +def make_label(img_dir, save_gt_folder, phase): + crop_img_save_dir = os.path.join(save_gt_folder, phase, 'crop_imgs') + os.makedirs(crop_img_save_dir, exist_ok=True) + + f_det = open(os.path.join(save_gt_folder, phase, 'det.txt'), 'w', encoding='utf-8') + f_rec = open(os.path.join(save_gt_folder, phase, 'rec.txt'), 'w', encoding='utf-8') + + i = 0 + for filename in tqdm(os.listdir(os.path.join(img_dir, phase))): + str_list = filename.split('-') + if len(str_list) < 5: + continue + coord_list = str_list[3].split('_') + txt_list = str_list[4].split('_') + boxes = [] + for coord in coord_list: + boxes.append([int(x) for x in coord.split("&")]) + boxes = [boxes[2], boxes[3], boxes[0], boxes[1]] + lp_number = provinces[int(txt_list[0])] + alphabets[int(txt_list[1])] + ''.join([ads[int(x)] for x in txt_list[2:]]) + + # det + det_info = [{'points':boxes, 'transcription':lp_number}] + f_det.write('{}\t{}\n'.format(os.path.join(phase, filename), json.dumps(det_info, ensure_ascii=False))) + + # rec + boxes = np.float32(boxes) + img = cv2.imread(os.path.join(img_dir, phase, filename)) + # crop_img = img[int(boxes[:,1].min()):int(boxes[:,1].max()),int(boxes[:,0].min()):int(boxes[:,0].max())] + crop_img = get_rotate_crop_image(img, boxes) + crop_img_save_filename = '{}_{}.jpg'.format(i,'_'.join(txt_list)) + crop_img_save_path = os.path.join(crop_img_save_dir, crop_img_save_filename) + cv2.imwrite(crop_img_save_path, crop_img) + f_rec.write('{}/crop_imgs/{}\t{}\n'.format(phase, crop_img_save_filename, lp_number)) + i+=1 + f_det.close() + f_rec.close() + +def get_rotate_crop_image(img, points): + ''' + img_height, img_width = img.shape[0:2] + left = int(np.min(points[:, 0])) + right = int(np.max(points[:, 0])) + top = int(np.min(points[:, 1])) + bottom = int(np.max(points[:, 1])) + img_crop = img[top:bottom, left:right, :].copy() + points[:, 0] = points[:, 0] - left + points[:, 1] = points[:, 1] - top + ''' + assert len(points) == 4, "shape of points must be 4*2" + img_crop_width = int( + max( + np.linalg.norm(points[0] - points[1]), + np.linalg.norm(points[2] - points[3]))) + img_crop_height = int( + max( + np.linalg.norm(points[0] - points[3]), + np.linalg.norm(points[1] - points[2]))) + pts_std = np.float32([[0, 0], [img_crop_width, 0], + [img_crop_width, img_crop_height], + [0, img_crop_height]]) + M = cv2.getPerspectiveTransform(points, pts_std) + dst_img = cv2.warpPerspective( + img, + M, (img_crop_width, img_crop_height), + borderMode=cv2.BORDER_REPLICATE, + flags=cv2.INTER_CUBIC) + dst_img_height, dst_img_width = dst_img.shape[0:2] + if dst_img_height * 1.0 / dst_img_width >= 1.5: + dst_img = np.rot90(dst_img) + return dst_img + +img_dir = '/home/aistudio/data/CCPD2020/ccpd_green' +save_gt_folder = '/home/aistudio/data/CCPD2020/PPOCR' +# phase = 'train' # change to val and test to make val dataset and test dataset +for phase in ['train','val','test']: + make_label(img_dir, save_gt_folder, phase) +``` + +通过上述命令可以完成了`训练集`,`验证集`和`测试集`的制作,制作完成的数据集信息如下: + +| 类型 | 数据集 | 图片地址 | 标签地址 | 图片数量 | +| --- | --- | --- | --- | --- | +| 检测 | 训练集 | /home/aistudio/data/CCPD2020/ccpd_green/train | /home/aistudio/data/CCPD2020/PPOCR/train/det.txt | 5769 | +| 检测 | 验证集 | /home/aistudio/data/CCPD2020/ccpd_green/val | /home/aistudio/data/CCPD2020/PPOCR/val/det.txt | 1001 | +| 检测 | 测试集 | /home/aistudio/data/CCPD2020/ccpd_green/test | /home/aistudio/data/CCPD2020/PPOCR/test/det.txt | 5006 | +| 识别 | 训练集 | /home/aistudio/data/CCPD2020/PPOCR/train/crop_imgs | /home/aistudio/data/CCPD2020/PPOCR/train/rec.txt | 5769 | +| 识别 | 验证集 | /home/aistudio/data/CCPD2020/PPOCR/val/crop_imgs | /home/aistudio/data/CCPD2020/PPOCR/val/rec.txt | 1001 | +| 识别 | 测试集 | /home/aistudio/data/CCPD2020/PPOCR/test/crop_imgs | /home/aistudio/data/CCPD2020/PPOCR/test/rec.txt | 5006 | + +在普遍的深度学习流程中,都是在训练集训练,在验证集选择最优模型后在测试集上进行测试。在本例中,我们省略中间步骤,直接在训练集训练,在测试集选择最优模型,因此我们只使用训练集和测试集。 + +## 4. 实验 + +由于数据集比较少,为了模型更好和更快的收敛,这里选用 PaddleOCR 中的 PP-OCRv3 模型进行文本检测和识别,并且使用 PP-OCRv3 模型参数作为预训练模型。PP-OCRv3在PP-OCRv2的基础上,中文场景端到端Hmean指标相比于PP-OCRv2提升5%, 英文数字模型端到端效果提升11%。详细优化细节请参考[PP-OCRv3](../doc/doc_ch/PP-OCRv3_introduction.md)技术报告。 + +由于车牌场景均为端侧设备部署,因此对速度和模型大小有比较高的要求,因此还需要采用量化训练的方式进行模型大小的压缩和模型推理速度的加速。模型量化可以在基本不损失模型的精度的情况下,将FP32精度的模型参数转换为Int8精度,减小模型参数大小并加速计算,使用量化后的模型在移动端等部署时更具备速度优势。 + +因此,本实验中对于车牌检测和识别有如下3种方案: + +1. PP-OCRv3中英文超轻量预训练模型直接预测 +2. CCPD车牌数据集在PP-OCRv3模型上fine-tune +3. CCPD车牌数据集在PP-OCRv3模型上fine-tune后量化 + +### 4.1 检测 +#### 4.1.1 预训练模型直接预测 + +从下表中下载PP-OCRv3文本检测预训练模型 + +|模型名称|模型简介|配置文件|推理模型大小|下载地址| +| --- | --- | --- | --- | --- | +|ch_PP-OCRv3_det| 【最新】原始超轻量模型,支持中英文、多语种文本检测 |[ch_PP-OCRv3_det_cml.yml](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml)| 3.8M |[推理模型](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar) / [训练模型](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_distill_train.tar)| + +使用如下命令下载预训练模型 + +```bash +mkdir models +cd models +wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_distill_train.tar +tar -xf ch_PP-OCRv3_det_distill_train.tar +cd /home/aistudio/PaddleOCR +``` + +预训练模型下载完成后,我们使用[ch_PP-OCRv3_det_student.yml](../configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml) 配置文件进行后续实验,在开始评估之前需要对配置文件中部分字段进行设置,具体如下: + +1. 模型存储和训练相关: + 1. Global.pretrained_model: 指向PP-OCRv3文本检测预训练模型地址 +2. 数据集相关 + 1. Eval.dataset.data_dir:指向测试集图片存放目录 + 2. Eval.dataset.label_file_list:指向测试集标注文件 + +上述字段均为必须修改的字段,可以通过修改配置文件的方式改动,也可在不需要修改配置文件的情况下,改变训练的参数。这里使用不改变配置文件的方式 。使用如下命令进行PP-OCRv3文本检测预训练模型的评估 + + +```bash +python tools/eval.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml -o \ + Global.pretrained_model=models/ch_PP-OCRv3_det_distill_train/student.pdparams \ + Eval.dataset.data_dir=/home/aistudio/data/CCPD2020/ccpd_green \ + Eval.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/test/det.txt] +``` +上述指令中,通过-c 选择训练使用配置文件,通过-o参数在不需要修改配置文件的情况下,改变训练的参数。 + +使用预训练模型进行评估,指标如下所示: + +| 方案 |hmeans| +|---------------------------|---| +| PP-OCRv3中英文超轻量检测预训练模型直接预测 |76.12%| + +#### 4.1.2 CCPD车牌数据集fine-tune + +**训练** + +为了进行fine-tune训练,我们需要在配置文件中设置需要使用的预训练模型地址,学习率和数据集等参数。 具体如下: + +1. 模型存储和训练相关: + 1. Global.pretrained_model: 指向PP-OCRv3文本检测预训练模型地址 + 2. Global.eval_batch_step: 模型多少step评估一次,这里设为从第0个step开始没隔772个step评估一次,772为一个epoch总的step数。 +2. 优化器相关: + 1. Optimizer.lr.name: 学习率衰减器设为常量 Const + 2. Optimizer.lr.learning_rate: 做 fine-tune 实验,学习率需要设置的比较小,此处学习率设为配置文件中的0.05倍 + 3. Optimizer.lr.warmup_epoch: warmup_epoch设为0 +3. 数据集相关: + 1. Train.dataset.data_dir:指向训练集图片存放目录 + 2. Train.dataset.label_file_list:指向训练集标注文件 + 3. Eval.dataset.data_dir:指向测试集图片存放目录 + 4. Eval.dataset.label_file_list:指向测试集标注文件 + +使用如下代码即可启动在CCPD车牌数据集上的fine-tune。 + +```bash +python tools/train.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml -o \ + Global.pretrained_model=models/ch_PP-OCRv3_det_distill_train/student.pdparams \ + Global.save_model_dir=output/CCPD/det \ + Global.eval_batch_step="[0, 772]" \ + Optimizer.lr.name=Const \ + Optimizer.lr.learning_rate=0.0005 \ + Optimizer.lr.warmup_epoch=0 \ + Train.dataset.data_dir=/home/aistudio/data/CCPD2020/ccpd_green \ + Train.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/train/det.txt] \ + Eval.dataset.data_dir=/home/aistudio/data/CCPD2020/ccpd_green \ + Eval.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/test/det.txt] +``` + +在上述命令中,通过`-o`的方式修改了配置文件中的参数。 + + +**评估** + +训练完成后使用如下命令进行评估 + + +```bash +python tools/eval.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml -o \ + Global.pretrained_model=output/CCPD/det/best_accuracy.pdparams \ + Eval.dataset.data_dir=/home/aistudio/data/CCPD2020/ccpd_green \ + Eval.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/test/det.txt] +``` + +使用预训练模型和CCPD车牌数据集fine-tune,指标分别如下: + +|方案|hmeans| +|---|---| +|PP-OCRv3中英文超轻量检测预训练模型直接预测|76.12%| +|PP-OCRv3中英文超轻量检测预训练模型 fine-tune|99.00%| + +可以看到进行fine-tune能显著提升车牌检测的效果。 + +#### 4.1.3 CCPD车牌数据集fine-tune+量化训练 + +此处采用 PaddleOCR 中提供好的[量化教程](../deploy/slim/quantization/README.md)对模型进行量化训练。 + +量化训练可通过如下命令启动: + +```bash +python3.7 deploy/slim/quantization/quant.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml -o \ + Global.pretrained_model=output/CCPD/det/best_accuracy.pdparams \ + Global.save_model_dir=output/CCPD/det_quant \ + Global.eval_batch_step="[0, 772]" \ + Optimizer.lr.name=Const \ + Optimizer.lr.learning_rate=0.0005 \ + Optimizer.lr.warmup_epoch=0 \ + Train.dataset.data_dir=/home/aistudio/data/CCPD2020/ccpd_green \ + Train.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/train/det.txt] \ + Eval.dataset.data_dir=/home/aistudio/data/CCPD2020/ccpd_green \ + Eval.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/test/det.txt] +``` + +量化后指标对比如下 + +|方案|hmeans| 模型大小 | 预测速度(lite) | +|---|---|------|------------| +|PP-OCRv3中英文超轻量检测预训练模型 fine-tune|99.00%| 2.5M | 223ms | +|PP-OCRv3中英文超轻量检测预训练模型 fine-tune+量化|98.91%| 1.0M | 189ms | + +可以看到通过量化训练在精度几乎无损的情况下,降低模型体积60%并且推理速度提升15%。 + +速度测试基于[PaddleOCR lite教程](../deploy/lite/readme_ch.md)完成。 + +#### 4.1.4 模型导出 + +使用如下命令可以将训练好的模型进行导出 + +* 非量化模型 +```bash +python tools/export_model.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml -o \ + Global.pretrained_model=output/CCPD/det/best_accuracy.pdparams \ + Global.save_inference_dir=output/det/infer +``` +* 量化模型 +```bash +python deploy/slim/quantization/export_model.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml -o \ + Global.pretrained_model=output/CCPD/det_quant/best_accuracy.pdparams \ + Global.save_inference_dir=output/det/infer +``` + +### 4.2 识别 +#### 4.2.1 预训练模型直接预测 + +从下表中下载PP-OCRv3文本识别预训练模型 + +|模型名称|模型简介|配置文件|推理模型大小|下载地址| +| --- | --- | --- | --- | --- | +|ch_PP-OCRv3_rec|【最新】原始超轻量模型,支持中英文、数字识别|[ch_PP-OCRv3_rec_distillation.yml](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/configs/rec/PP-OCRv3/ch_PP-OCRv3_rec_distillation.yml)| 12.4M |[推理模型](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_infer.tar) / [训练模型](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_train.tar) | + +使用如下命令下载预训练模型 + +```bash +mkdir models +cd models +wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_train.tar +tar -xf ch_PP-OCRv3_rec_train.tar +cd /home/aistudio/PaddleOCR +``` + +PaddleOCR提供的PP-OCRv3识别模型采用蒸馏训练策略,因此提供的预训练模型中会包含`Teacher`和`Student`模型的参数,详细信息可参考[knowledge_distillation.md](../doc/doc_ch/knowledge_distillation.md)。 因此,模型下载完成后需要使用如下代码提取`Student`模型的参数: + +```python +import paddle +# 加载预训练模型 +all_params = paddle.load("models/ch_PP-OCRv3_rec_train/best_accuracy.pdparams") +# 查看权重参数的keys +print(all_params.keys()) +# 学生模型的权重提取 +s_params = {key[len("Student."):]: all_params[key] for key in all_params if "Student." in key} +# 查看学生模型权重参数的keys +print(s_params.keys()) +# 保存 +paddle.save(s_params, "models/ch_PP-OCRv3_rec_train/student.pdparams") +``` + +预训练模型下载完成后,我们使用[ch_PP-OCRv3_rec.yml](../configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml) 配置文件进行后续实验,在开始评估之前需要对配置文件中部分字段进行设置,具体如下: + +1. 模型存储和训练相关: + 1. Global.pretrained_model: 指向PP-OCRv3文本识别预训练模型地址 +2. 数据集相关 + 1. Eval.dataset.data_dir:指向测试集图片存放目录 + 2. Eval.dataset.label_file_list:指向测试集标注文件 + +使用如下命令进行PP-OCRv3文本识别预训练模型的评估 + +```bash +python tools/eval.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml -o \ + Global.pretrained_model=models/ch_PP-OCRv3_rec_train/student.pdparams \ + Eval.dataset.data_dir=/home/aistudio/data/CCPD2020/PPOCR \ + Eval.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/test/rec.txt] +``` + +如需获取已训练模型,请加入PaddleX官方交流频道,获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 + + + +评估部分日志如下: +```bash +[2022/05/12 19:52:02] ppocr INFO: load pretrain successful from models/ch_PP-OCRv3_rec_train/best_accuracy +eval model:: 100%|██████████████████████████████| 40/40 [00:15<00:00, 2.57it/s] +[2022/05/12 19:52:17] ppocr INFO: metric eval *************** +[2022/05/12 19:52:17] ppocr INFO: acc:0.0 +[2022/05/12 19:52:17] ppocr INFO: norm_edit_dis:0.8656084923002452 +[2022/05/12 19:52:17] ppocr INFO: Teacher_acc:0.000399520574511545 +[2022/05/12 19:52:17] ppocr INFO: Teacher_norm_edit_dis:0.8657902943394548 +[2022/05/12 19:52:17] ppocr INFO: fps:1443.1801978719905 + +``` +使用预训练模型进行评估,指标如下所示: + +|方案|acc| +|---|---| +|PP-OCRv3中英文超轻量识别预训练模型直接预测|0%| + +从评估日志中可以看到,直接使用PP-OCRv3预训练模型进行评估,acc非常低,但是norm_edit_dis很高。因此,我们猜测是模型大部分文字识别是对的,只有少部分文字识别错误。使用如下命令进行infer查看模型的推理结果进行验证: + + +```bash +python tools/infer_rec.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml -o \ + Global.pretrained_model=models/ch_PP-OCRv3_rec_train/student.pdparams \ + Global.infer_img=/home/aistudio/data/CCPD2020/PPOCR/test/crop_imgs/0_0_0_3_32_30_31_30_30.jpg +``` + +输出部分日志如下: +```bash +[2022/05/01 08:51:57] ppocr INFO: train with paddle 2.2.2 and device CUDAPlace(0) +W0501 08:51:57.127391 11326 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.0, Runtime API Version: 10.1 +W0501 08:51:57.132315 11326 device_context.cc:465] device: 0, cuDNN Version: 7.6. +[2022/05/01 08:52:00] ppocr INFO: load pretrain successful from models/ch_PP-OCRv3_rec_train/student +[2022/05/01 08:52:00] ppocr INFO: infer_img: /home/aistudio/data/CCPD2020/PPOCR/test/crop_imgs/0_0_3_32_30_31_30_30.jpg +[2022/05/01 08:52:00] ppocr INFO: result: {"Student": {"label": "皖A·D86766", "score": 0.9552637934684753}, "Teacher": {"label": "皖A·D86766", "score": 0.9917094707489014}} +[2022/05/01 08:52:00] ppocr INFO: success! +``` + +从infer结果可以看到,车牌中的文字大部分都识别正确,只是多识别出了一个`·`。针对这种情况,有如下两种方案: +1. 直接通过后处理去掉多识别的`·`。 +2. 进行 fine-tune。 + +#### 4.2.2 预训练模型直接预测+改动后处理 + +直接通过后处理去掉多识别的`·`,在后处理的改动比较简单,只需在 [ppocr/postprocess/rec_postprocess.py](../ppocr/postprocess/rec_postprocess.py) 文件的76行添加如下代码: +```python +text = text.replace('·','') +``` + +改动前后指标对比: + +|方案|acc| +|---|---| +|PP-OCRv3中英文超轻量识别预训练模型直接预测|0.20%| +|PP-OCRv3中英文超轻量识别预训练模型直接预测+后处理去掉多识别的`·`|90.97%| + +可以看到,去掉多余的`·`能大幅提高精度。 + +#### 4.2.3 CCPD车牌数据集fine-tune + +**训练** + +为了进行fine-tune训练,我们需要在配置文件中设置需要使用的预训练模型地址,学习率和数据集等参数。 具体如下: + +1. 模型存储和训练相关: + 1. Global.pretrained_model: 指向PP-OCRv3文本识别预训练模型地址 + 2. Global.eval_batch_step: 模型多少step评估一次,这里设为从第0个step开始没隔45个step评估一次,45为一个epoch总的step数。 +2. 优化器相关 + 1. Optimizer.lr.name: 学习率衰减器设为常量 Const + 2. Optimizer.lr.learning_rate: 做 fine-tune 实验,学习率需要设置的比较小,此处学习率设为配置文件中的0.05倍 + 3. Optimizer.lr.warmup_epoch: warmup_epoch设为0 +3. 数据集相关 + 1. Train.dataset.data_dir:指向训练集图片存放目录 + 2. Train.dataset.label_file_list:指向训练集标注文件 + 3. Eval.dataset.data_dir:指向测试集图片存放目录 + 4. Eval.dataset.label_file_list:指向测试集标注文件 + +使用如下命令启动 fine-tune + +```bash +python tools/train.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml -o \ + Global.pretrained_model=models/ch_PP-OCRv3_rec_train/student.pdparams \ + Global.save_model_dir=output/CCPD/rec/ \ + Global.eval_batch_step="[0, 90]" \ + Optimizer.lr.name=Const \ + Optimizer.lr.learning_rate=0.0005 \ + Optimizer.lr.warmup_epoch=0 \ + Train.dataset.data_dir=/home/aistudio/data/CCPD2020/PPOCR \ + Train.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/train/rec.txt] \ + Eval.dataset.data_dir=/home/aistudio/data/CCPD2020/PPOCR \ + Eval.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/test/rec.txt] +``` + +**评估** + +训练完成后使用如下命令进行评估 + +```bash +python tools/eval.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml -o \ + Global.pretrained_model=output/CCPD/rec/best_accuracy.pdparams \ + Eval.dataset.data_dir=/home/aistudio/data/CCPD2020/PPOCR \ + Eval.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/test/rec.txt] +``` + +使用预训练模型和CCPD车牌数据集fine-tune,指标分别如下: + +|方案| acc | +|---|--------| +|PP-OCRv3中英文超轻量识别预训练模型直接预测| 0.00% | +|PP-OCRv3中英文超轻量识别预训练模型直接预测+后处理去掉多识别的`·`| 90.97% | +|PP-OCRv3中英文超轻量识别预训练模型 fine-tune| 94.54% | + +可以看到进行fine-tune能显著提升车牌识别的效果。 + +#### 4.2.4 CCPD车牌数据集fine-tune+量化训练 + +此处采用 PaddleOCR 中提供好的[量化教程](../deploy/slim/quantization/README.md)对模型进行量化训练。 + +量化训练可通过如下命令启动: + +```bash +python3.7 deploy/slim/quantization/quant.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml -o \ + Global.pretrained_model=output/CCPD/rec/best_accuracy.pdparams \ + Global.save_model_dir=output/CCPD/rec_quant/ \ + Global.eval_batch_step="[0, 90]" \ + Optimizer.lr.name=Const \ + Optimizer.lr.learning_rate=0.0005 \ + Optimizer.lr.warmup_epoch=0 \ + Train.dataset.data_dir=/home/aistudio/data/CCPD2020/PPOCR \ + Train.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/train/rec.txt] \ + Eval.dataset.data_dir=/home/aistudio/data/CCPD2020/PPOCR \ + Eval.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/test/rec.txt] +``` + +量化后指标对比如下 + +|方案| acc | 模型大小 | 预测速度(lite) | +|---|--------|-------|------------| +|PP-OCRv3中英文超轻量识别预训练模型 fine-tune| 94.54% | 10.3M | 4.2ms | +|PP-OCRv3中英文超轻量识别预训练模型 fine-tune + 量化| 93.40% | 4.8M | 1.8ms | + +可以看到量化后能降低模型体积53%并且推理速度提升57%,但是由于识别数据过少,量化带来了1%的精度下降。 + +速度测试基于[PaddleOCR lite教程](../deploy/lite/readme_ch.md)完成。 + +#### 4.2.5 模型导出 + +使用如下命令可以将训练好的模型进行导出。 + +* 非量化模型 +```bash +python tools/export_model.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml -o \ + Global.pretrained_model=output/CCPD/rec/best_accuracy.pdparams \ + Global.save_inference_dir=output/CCPD/rec/infer +``` +* 量化模型 +```bash +python deploy/slim/quantization/export_model.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml -o \ + Global.pretrained_model=output/CCPD/rec_quant/best_accuracy.pdparams \ + Global.save_inference_dir=output/CCPD/rec_quant/infer +``` + +### 4.3 计算End2End指标 + +端到端指标可通过 [PaddleOCR内置脚本](../tools/end2end/readme.md) 进行计算,具体步骤如下: + +1. 导出模型 + +通过如下命令进行模型的导出。注意,量化模型导出时,需要配置eval数据集 + +```bash +# 检测模型 + +# 预训练模型 +python tools/export_model.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml -o \ + Global.pretrained_model=models/ch_PP-OCRv3_det_distill_train/student.pdparams \ + Global.save_inference_dir=output/ch_PP-OCRv3_det_distill_train/infer + +# 非量化模型 +python tools/export_model.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml -o \ + Global.pretrained_model=output/CCPD/det/best_accuracy.pdparams \ + Global.save_inference_dir=output/CCPD/det/infer + +# 量化模型 +python deploy/slim/quantization/export_model.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml -o \ + Global.pretrained_model=output/CCPD/det_quant/best_accuracy.pdparams \ + Global.save_inference_dir=output/CCPD/det_quant/infer \ + Eval.dataset.data_dir=/home/aistudio/data/CCPD2020/ccpd_green \ + Eval.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/test/det.txt] \ + Eval.loader.num_workers=0 + +# 识别模型 + +# 预训练模型 +python tools/export_model.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml -o \ + Global.pretrained_model=models/ch_PP-OCRv3_rec_train/student.pdparams \ + Global.save_inference_dir=output/ch_PP-OCRv3_rec_train/infer + +# 非量化模型 +python tools/export_model.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml -o \ + Global.pretrained_model=output/CCPD/rec/best_accuracy.pdparams \ + Global.save_inference_dir=output/CCPD/rec/infer + +# 量化模型 +python deploy/slim/quantization/export_model.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml -o \ + Global.pretrained_model=output/CCPD/rec_quant/best_accuracy.pdparams \ + Global.save_inference_dir=output/CCPD/rec_quant/infer \ + Eval.dataset.data_dir=/home/aistudio/data/CCPD2020/PPOCR \ + Eval.dataset.label_file_list=[/home/aistudio/data/CCPD2020/PPOCR/test/rec.txt] +``` + +2. 用导出的模型对测试集进行预测 + +此处,分别使用PP-OCRv3预训练模型,fintune模型和量化模型对测试集的所有图像进行预测,命令如下: + +```bash +# PP-OCRv3中英文超轻量检测预训练模型,PP-OCRv3中英文超轻量识别预训练模型 +python3 tools/infer/predict_system.py --det_model_dir=models/ch_PP-OCRv3_det_distill_train/infer --rec_model_dir=models/ch_PP-OCRv3_rec_train/infer --det_limit_side_len=736 --det_limit_type=min --image_dir=/home/aistudio/data/CCPD2020/ccpd_green/test/ --draw_img_save_dir=infer/pretrain --use_dilation=true + +# PP-OCRv3中英文超轻量检测预训练模型+fine-tune,PP-OCRv3中英文超轻量识别预训练模型+fine-tune +python3 tools/infer/predict_system.py --det_model_dir=output/CCPD/det/infer --rec_model_dir=output/CCPD/rec/infer --det_limit_side_len=736 --det_limit_type=min --image_dir=/home/aistudio/data/CCPD2020/ccpd_green/test/ --draw_img_save_dir=infer/fine-tune --use_dilation=true + +# PP-OCRv3中英文超轻量检测预训练模型 fine-tune +量化,PP-OCRv3中英文超轻量识别预训练模型 fine-tune +量化 结果转换和评估 +python3 tools/infer/predict_system.py --det_model_dir=output/CCPD/det_quant/infer --rec_model_dir=output/CCPD/rec_quant/infer --det_limit_side_len=736 --det_limit_type=min --image_dir=/home/aistudio/data/CCPD2020/ccpd_green/test/ --draw_img_save_dir=infer/quant --use_dilation=true +``` + +3. 转换label并计算指标 + +将gt和上一步保存的预测结果转换为端对端评测需要的数据格式,并根据转换后的数据进行端到端指标计算 + +```bash +python3 tools/end2end/convert_ppocr_label.py --mode=gt --label_path=/home/aistudio/data/CCPD2020/PPOCR/test/det.txt --save_folder=end2end/gt + +# PP-OCRv3中英文超轻量检测预训练模型,PP-OCRv3中英文超轻量识别预训练模型 结果转换和评估 +python3 tools/end2end/convert_ppocr_label.py --mode=pred --label_path=infer/pretrain/system_results.txt --save_folder=end2end/pretrain +python3 tools/end2end/eval_end2end.py end2end/gt end2end/pretrain + +# PP-OCRv3中英文超轻量检测预训练模型,PP-OCRv3中英文超轻量识别预训练模型+后处理去掉多识别的`·` 结果转换和评估 +# 需手动修改后处理函数 +python3 tools/end2end/convert_ppocr_label.py --mode=pred --label_path=infer/post/system_results.txt --save_folder=end2end/post +python3 tools/end2end/eval_end2end.py end2end/gt end2end/post + +# PP-OCRv3中英文超轻量检测预训练模型 fine-tune,PP-OCRv3中英文超轻量识别预训练模型 fine-tune 结果转换和评估 +python3 tools/end2end/convert_ppocr_label.py --mode=pred --label_path=infer/fine-tune/system_results.txt --save_folder=end2end/fine-tune +python3 tools/end2end/eval_end2end.py end2end/gt end2end/fine-tune + +# PP-OCRv3中英文超轻量检测预训练模型 fine-tune +量化,PP-OCRv3中英文超轻量识别预训练模型 fine-tune +量化 结果转换和评估 +python3 tools/end2end/convert_ppocr_label.py --mode=pred --label_path=infer/quant/system_results.txt --save_folder=end2end/quant +python3 tools/end2end/eval_end2end.py end2end/gt end2end/quant +``` + +日志如下: +```bash +The convert label saved in end2end/gt +The convert label saved in end2end/pretrain +start testing... +hit, dt_count, gt_count 2 5988 5006 +character_acc: 70.42% +avg_edit_dist_field: 2.37 +avg_edit_dist_img: 2.37 +precision: 0.03% +recall: 0.04% +fmeasure: 0.04% +The convert label saved in end2end/post +start testing... +hit, dt_count, gt_count 4224 5988 5006 +character_acc: 81.59% +avg_edit_dist_field: 1.47 +avg_edit_dist_img: 1.47 +precision: 70.54% +recall: 84.38% +fmeasure: 76.84% +The convert label saved in end2end/fine-tune +start testing... +hit, dt_count, gt_count 4286 4898 5006 +character_acc: 94.16% +avg_edit_dist_field: 0.47 +avg_edit_dist_img: 0.47 +precision: 87.51% +recall: 85.62% +fmeasure: 86.55% +The convert label saved in end2end/quant +start testing... +hit, dt_count, gt_count 4349 4951 5006 +character_acc: 94.13% +avg_edit_dist_field: 0.47 +avg_edit_dist_img: 0.47 +precision: 87.84% +recall: 86.88% +fmeasure: 87.36% +``` + +各个方案端到端指标如下: + +|模型| 指标 | +|---|--------| +|PP-OCRv3中英文超轻量检测预训练模型
PP-OCRv3中英文超轻量识别预训练模型| 0.04% | +|PP-OCRv3中英文超轻量检测预训练模型
PP-OCRv3中英文超轻量识别预训练模型 + 后处理去掉多识别的`·`| 78.27% | +|PP-OCRv3中英文超轻量检测预训练模型+fine-tune
PP-OCRv3中英文超轻量识别预训练模型+fine-tune| 87.14% | +|PP-OCRv3中英文超轻量检测预训练模型+fine-tune+量化
PP-OCRv3中英文超轻量识别预训练模型+fine-tune+量化| 88.00% | + +从结果中可以看到对预训练模型不做修改,只根据场景下的具体情况进行后处理的修改就能大幅提升端到端指标到78.27%,在CCPD数据集上进行 fine-tune 后指标进一步提升到87.14%, 在经过量化训练之后,由于检测模型的recall变高,指标进一步提升到88%。但是这个结果仍旧不符合检测模型+识别模型的真实性能(99%*94%=93%),因此我们需要对 base case 进行具体分析。 + +在之前的端到端预测结果中,可以看到很多不符合车牌标注的文字被识别出来, 因此可以进行简单的过滤来提升precision + +为了快速评估,我们在 ` tools/end2end/convert_ppocr_label.py` 脚本的 58 行加入如下代码,对非8个字符的结果进行过滤 +```python +if len(txt) != 8: # 车牌字符串长度为8 + continue +``` + +此外,通过可视化box可以发现有很多框都是竖直翻转之后的框,并且没有完全框住车牌边界,因此需要进行框的竖直翻转以及轻微扩大,示意图如下: + +![](https://ai-studio-static-online.cdn.bcebos.com/59ab0411c8eb4dfd917fb2b6e5b69a17ee7ca48351444aec9ac6104b79ff1028) + +修改前后个方案指标对比如下: + + +各个方案端到端指标如下: + +|模型|base|A:识别结果过滤|B:use_dilation|C:flip_box|best| +|---|---|---|---|---|---| +|PP-OCRv3中英文超轻量检测预训练模型
PP-OCRv3中英文超轻量识别预训练模型|0.04%|0.08%|0.02%|0.05%|0.00%(A)| +|PP-OCRv3中英文超轻量检测预训练模型
PP-OCRv3中英文超轻量识别预训练模型 + 后处理去掉多识别的`·`|78.27%|90.84%|78.61%|79.43%|91.66%(A+B+C)| +|PP-OCRv3中英文超轻量检测预训练模型+fine-tune
PP-OCRv3中英文超轻量识别预训练模型+fine-tune|87.14%|90.40%|87.66%|89.98%|92.50%(A+B+C)| +|PP-OCRv3中英文超轻量检测预训练模型+fine-tune+量化
PP-OCRv3中英文超轻量识别预训练模型+fine-tune+量化|88.00%|90.54%|88.50%|89.46%|92.02%(A+B+C)| + + +从结果中可以看到对预训练模型不做修改,只根据场景下的具体情况进行后处理的修改就能大幅提升端到端指标到91.66%,在CCPD数据集上进行 fine-tune 后指标进一步提升到92.5%, 在经过量化训练之后,指标变为92.02%。 + +### 4.4 部署 + +- 基于 Paddle Inference 的python推理 + +检测模型和识别模型分别 fine-tune 并导出为inference模型之后,可以使用如下命令基于 Paddle Inference 进行端到端推理并对结果进行可视化。 + +```bash +python tools/infer/predict_system.py \ + --det_model_dir=output/CCPD/det/infer/ \ + --rec_model_dir=output/CCPD/rec/infer/ \ + --image_dir="/home/aistudio/data/CCPD2020/ccpd_green/test/04131106321839081-92_258-159&509_530&611-527&611_172&599_159&509_530&525-0_0_3_32_30_31_30_30-109-106.jpg" \ + --rec_image_shape=3,48,320 +``` +推理结果如下 + +![](https://ai-studio-static-online.cdn.bcebos.com/76b6a0939c2c4cf49039b6563c4b28e241e11285d7464e799e81c58c0f7707a7) + +- 端侧部署 + +端侧部署我们采用基于 PaddleLite 的 cpp 推理。Paddle Lite是飞桨轻量化推理引擎,为手机、IOT端提供高效推理能力,并广泛整合跨平台硬件,为端侧部署及应用落地问题提供轻量化的部署方案。具体可参考 [PaddleOCR lite教程](../deploy/lite/readme_ch.md) + + +### 4.5 实验总结 + +我们分别使用PP-OCRv3中英文超轻量预训练模型在车牌数据集上进行了直接评估和 fine-tune 和 fine-tune +量化3种方案的实验,并基于[PaddleOCR lite教程](../deploy/lite/readme_ch.md)进行了速度测试,指标对比如下: + +- 检测 + +|方案|hmeans| 模型大小 | 预测速度(lite) | +|---|---|------|------------| +|PP-OCRv3中英文超轻量检测预训练模型直接预测|76.12%|2.5M| 233ms | +|PP-OCRv3中英文超轻量检测预训练模型 fine-tune|99.00%| 2.5M | 233ms | +|PP-OCRv3中英文超轻量检测预训练模型 fine-tune + 量化|98.91%| 1.0M | 189ms |fine-tune + +- 识别 + +|方案| acc | 模型大小 | 预测速度(lite) | +|---|--------|-------|------------| +|PP-OCRv3中英文超轻量识别预训练模型直接预测| 0.00% |10.3M| 4.2ms | +|PP-OCRv3中英文超轻量识别预训练模型直接预测+后处理去掉多识别的`·`| 90.97% |10.3M| 4.2ms | +|PP-OCRv3中英文超轻量识别预训练模型 fine-tune| 94.54% | 10.3M | 4.2ms | +|PP-OCRv3中英文超轻量识别预训练模型 fine-tune + 量化| 93.40% | 4.8M | 1.8ms | + + +- 端到端指标如下: + +|方案|fmeasure|模型大小|预测速度(lite) | +|---|---|---|---| +|PP-OCRv3中英文超轻量检测预训练模型
PP-OCRv3中英文超轻量识别预训练模型|0.08%|12.8M|298ms| +|PP-OCRv3中英文超轻量检测预训练模型
PP-OCRv3中英文超轻量识别预训练模型 + 后处理去掉多识别的`·`|91.66%|12.8M|298ms| +|PP-OCRv3中英文超轻量检测预训练模型+fine-tune
PP-OCRv3中英文超轻量识别预训练模型+fine-tune|92.50%|12.8M|298ms| +|PP-OCRv3中英文超轻量检测预训练模型+fine-tune+量化
PP-OCRv3中英文超轻量识别预训练模型+fine-tune+量化|92.02%|5.80M|224ms| + + +**结论** + +PP-OCRv3的检测模型在未经过fine-tune的情况下,在车牌数据集上也有一定的精度,经过 fine-tune 后能够极大的提升检测效果,精度达到99%。在使用量化训练后检测模型的精度几乎无损,并且模型大小压缩60%。 + +PP-OCRv3的识别模型在未经过fine-tune的情况下,在车牌数据集上精度为0,但是经过分析可以知道,模型大部分字符都预测正确,但是会多预测一个特殊字符,去掉这个特殊字符后,精度达到90%。PP-OCRv3识别模型在经过 fine-tune 后识别精度进一步提升,达到94.4%。在使用量化训练后识别模型大小压缩53%,但是由于数据量多少,带来了1%的精度损失。 + +从端到端结果中可以看到对预训练模型不做修改,只根据场景下的具体情况进行后处理的修改就能大幅提升端到端指标到91.66%,在CCPD数据集上进行 fine-tune 后指标进一步提升到92.5%, 在经过量化训练之后,指标轻微下降到92.02%但模型大小降低54%。 diff --git "a/ocr_webapp/PaddleOCR/applications/\351\253\230\347\262\276\345\272\246\344\270\255\346\226\207\350\257\206\345\210\253\346\250\241\345\236\213.md" "b/ocr_webapp/PaddleOCR/applications/\351\253\230\347\262\276\345\272\246\344\270\255\346\226\207\350\257\206\345\210\253\346\250\241\345\236\213.md" new file mode 100644 index 0000000000000000000000000000000000000000..b16d82f1db3bf0cb7b44f0c914728598cb4a9e1d --- /dev/null +++ "b/ocr_webapp/PaddleOCR/applications/\351\253\230\347\262\276\345\272\246\344\270\255\346\226\207\350\257\206\345\210\253\346\250\241\345\236\213.md" @@ -0,0 +1,107 @@ +# 高精度中文场景文本识别模型SVTR + +## 1. 简介 + +PP-OCRv3是百度开源的超轻量级场景文本检测识别模型库,其中超轻量的场景中文识别模型SVTR_LCNet使用了SVTR算法结构。为了保证速度,SVTR_LCNet将SVTR模型的Local Blocks替换为LCNet,使用两层Global Blocks。在中文场景中,PP-OCRv3识别主要使用如下优化策略([详细技术报告](../doc/doc_ch/PP-OCRv3_introduction.md)): +- GTC:Attention指导CTC训练策略; +- TextConAug:挖掘文字上下文信息的数据增广策略; +- TextRotNet:自监督的预训练模型; +- UDML:联合互学习策略; +- UIM:无标注数据挖掘方案。 + +其中 *UIM:无标注数据挖掘方案* 使用了高精度的SVTR中文模型进行无标注文件的刷库,该模型在PP-OCRv3识别的数据集上训练,精度对比如下表。 + +|中文识别算法|模型|UIM|精度| +| --- | --- | --- |--- | +|PP-OCRv3|SVTR_LCNet| w/o |78.40%| +|PP-OCRv3|SVTR_LCNet| w |79.40%| +|SVTR|SVTR-Tiny|-|82.50%| + +aistudio项目链接: [高精度中文场景文本识别模型SVTR](https://aistudio.baidu.com/aistudio/projectdetail/4263032) + +## 2. SVTR中文模型使用 + +### 环境准备 + + +本任务基于Aistudio完成, 具体环境如下: + +- 操作系统: Linux +- PaddlePaddle: 2.3 +- PaddleOCR: dygraph + +下载 PaddleOCR代码 + +```bash +git clone -b dygraph https://github.com/PaddlePaddle/PaddleOCR +``` + +安装依赖库 + +```bash +pip install -r PaddleOCR/requirements.txt -i https://mirror.baidu.com/pypi/simple +``` + +### 快速使用 + +获取SVTR中文模型文件,请加入PaddleX官方交流频道,获取20G OCR学习大礼包(内含《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料) + +- PaddleX官方交流频道:https://aistudio.baidu.com/community/channel/610 + + +```bash +# 解压模型文件 +tar xf svtr_ch_high_accuracy.tar +``` + +预测中文文本,以下图为例: +![](../doc/imgs_words/ch/word_1.jpg) + +预测命令: + +```bash +# CPU预测 +python tools/infer_rec.py -c configs/rec/rec_svtrnet_ch.yml -o Global.pretrained_model=./svtr_ch_high_accuracy/best_accuracy Global.infer_img=./doc/imgs_words/ch/word_1.jpg Global.use_gpu=False + +# GPU预测 +#python tools/infer_rec.py -c configs/rec/rec_svtrnet_ch.yml -o Global.pretrained_model=./svtr_ch_high_accuracy/best_accuracy Global.infer_img=./doc/imgs_words/ch/word_1.jpg Global.use_gpu=True +``` + +可以看到最后打印结果为 +- result: 韩国小馆 0.9853458404541016 + +0.9853458404541016为预测置信度。 + +### 推理模型导出与预测 + +inference 模型(paddle.jit.save保存的模型) 一般是模型训练,把模型结构和模型参数保存在文件中的固化模型,多用于预测部署场景。 训练过程中保存的模型是checkpoints模型,保存的只有模型的参数,多用于恢复训练等。 与checkpoints模型相比,inference 模型会额外保存模型的结构信息,在预测部署、加速推理上性能优越,灵活方便,适合于实际系统集成。 + +运行识别模型转inference模型命令,如下: + +```bash +python tools/export_model.py -c configs/rec/rec_svtrnet_ch.yml -o Global.pretrained_model=./svtr_ch_high_accuracy/best_accuracy Global.save_inference_dir=./inference/svtr_ch +``` + +转换成功后,在目录下有三个文件: +```shell +inference/svtr_ch/ + ├── inference.pdiparams # 识别inference模型的参数文件 + ├── inference.pdiparams.info # 识别inference模型的参数信息,可忽略 + └── inference.pdmodel # 识别inference模型的program文件 +``` + +inference模型预测,命令如下: + +```bash +# CPU预测 +python3 tools/infer/predict_rec.py --image_dir="./doc/imgs_words/ch/word_1.jpg" --rec_algorithm='SVTR' --rec_model_dir=./inference/svtr_ch/ --rec_image_shape='3, 32, 320' --rec_char_dict_path=ppocr/utils/ppocr_keys_v1.txt --use_gpu=False + +# GPU预测 +#python3 tools/infer/predict_rec.py --image_dir="./doc/imgs_words/ch/word_1.jpg" --rec_algorithm='SVTR' --rec_model_dir=./inference/svtr_ch/ --rec_image_shape='3, 32, 320' --rec_char_dict_path=ppocr/utils/ppocr_keys_v1.txt --use_gpu=True +``` + +**注意** + +- 使用SVTR算法时,需要指定--rec_algorithm='SVTR' +- 如果使用自定义字典训练的模型,需要将--rec_char_dict_path=ppocr/utils/ppocr_keys_v1.txt修改为自定义的字典 +- --rec_image_shape='3, 32, 320' 该参数不能去掉 diff --git a/ocr_webapp/PaddleOCR/awesome_projects.md b/ocr_webapp/PaddleOCR/awesome_projects.md new file mode 100644 index 0000000000000000000000000000000000000000..2e51ba5ed8422d249e40616f5503a0bb1dd264b8 --- /dev/null +++ b/ocr_webapp/PaddleOCR/awesome_projects.md @@ -0,0 +1,30 @@ +## 😃 Awesome projects based on PaddleOCR +💗 PaddleOCR wouldn’t be where it is today without its incredible community! A massive 🙌 thank you 🙌 to all our longtime partners, new collaborators, and everyone who’s poured their passion into PaddleOCR — whether we’ve named you or not. Your support fuels our fire! 🔥 +| Project Name | Description | +| ------------ | ----------- | +| [Umi-OCR](https://github.com/hiroi-sora/Umi-OCR) |Free, Open-source, Batch Offline OCR Software.| +| [LearnOpenCV](http://github.com/spmallick/learnopencv) | code for Computer Vision, Deep learning, and AI research articles.| +| [OmniParser](https://github.com/microsoft/OmniParser) |OmniParser: Screen Parsing tool for Pure Vision Based GUI Agent.| +| [QAnything](https://github.com/netease-youdao/QAnything) |Question and Answer based on Anything.| +| [PaddleHub](https://github.com/PaddlePaddle/PaddleHub) |400+ AI Models: Rich, high-quality AI models, including CV, NLP, Speech, Video and Cross-Modal.| +| [PaddleNLP](https://github.com/PaddlePaddle/PaddleNLP) |A Large Language Model (LLM) development suite based on the PaddlePaddle.| +| [Rerun](https://github.com/rerun-io/rerun) | Rerun is building the multimodal data stack to model, ingest, store, query and view robotics-style data | +| [Dango-Translator](https://github.com/PantsuDango/Dango-Translator) | Recognize text on the screen, translate it and show the translation results in real time.| +| [PDF-Extract-Kit](https://github.com/opendatalab/PDF-Extract-Kit) | PDF-Extract-Kit is a powerful open-source toolkit designed to efficiently extract high-quality content from complex and diverse PDF documents. | +| [manga-image-translator](https://github.com/zyddnys/manga-image-translator) | Translate texts in manga/images.| +| [March7thAssistant](https://github.com/moesnow/March7thAssistant) | Daily Tasks: Stamina recovery, daily training, claiming rewards, commissions, and farming. | +| [PaddlePaddle/models](https://github.com/PaddlePaddle/models) |PaddlePaddle's industrial-grade model zoo.| +| [katanaml/sparrow](https://github.com/katanaml/sparrow) | Sparrow is an innovative open-source solution for efficient data extraction and processing from various documents and images. | +| [RapidOCR](https://github.com/RapidAI/RapidOCR) | Awesome OCR multiple programming languages toolkits based on ONNXRuntime, OpenVINO, PaddlePaddle and PyTorch | +| [autoMate](https://github.com/yuruotong1/autoMate) | AI-Powered Local Automation Tool & Let Your Computer Work for You. | +| [Agent-S](https://github.com/simular-ai/Agent-S) | A Compositional Generalist-Specialist Framework for Computer Use Agents. | +| [pdf-craft](https://github.com/oomol-lab/pdf-craft) | PDF Craft can convert PDF files into various other formats. | +| [VV](https://github.com/Cicada000/VV) | Zhang Weiwei Quotations Search Project. | +| [docetl](https://github.com/ucbepic/docetl) | DocETL is a tool for creating and executing data processing pipelines, especially suited for complex document processing tasks. | +| [ZenlessZoneZero-Auto](https://github.com/sMythicalBird/ZenlessZoneZero-Auto) | Zenless Zone Zero Automation Framework. | +| [Yuxi-Know](https://github.com/xerrors/Yuxi-Know) | Knowledge graph question answering system based on LLMs. | +| [PaddleSharp](https://github.com/sdcb/PaddleSharp) |.NET/C# binding for Baidu paddle inference library and PaddleOCR | +| [python-office](https://github.com/CoderWanFeng/python-office) | Python tool for office works. | +| [OnnxOCR](https://github.com/jingsongliujing/OnnxOCR) |A lightweight OCR system based on PaddleOCR, decoupled from the PaddlePaddle deep learning training framework, with ultra-fast inference speed | +| [Frigate](https://github.com/blakeblackshear/frigate) | Real-time NVR system with AI-powered object detection and License Plate Recognition (LPR) using PaddleOCR. | +| ... |... | diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/.gitattributes b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..8543e0a71603c693e83841b4a29a04c54a24d2a0 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/.gitattributes @@ -0,0 +1,2 @@ +*.html linguist-language=python +*.ipynb linguist-language=python \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/.gitignore b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..cef1c73b393daf2b192bef1aafd9c612517247a4 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/.gitignore @@ -0,0 +1,16 @@ +.DS_Store +*.pth +*.pyc +*.pyo +*.log +*.tmp +*.pkl +__pycache__/ +.idea/ +output/ +test/*.jpg +datasets/ +index/ +train_log/ +log/ +profiling_log/ \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/LICENSE.md b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..b09cd7856d58590578ee1a4f3ad45d1310a97f87 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/LICENSE.md @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/README.MD b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/README.MD new file mode 100644 index 0000000000000000000000000000000000000000..dbc07faafb1fbf519ad64bb6b6a801889fd3042e --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/README.MD @@ -0,0 +1,132 @@ +# Real-time Scene Text Detection with Differentiable Binarization + +**note**: some code is inherited from [WenmuZhou/DBNet.pytorch](https://github.com/WenmuZhou/DBNet.pytorch) + +[中文解读](https://zhuanlan.zhihu.com/p/94677957) + +![network](imgs/paper/db.jpg) + +## update +2020-06-07: 添加灰度图训练,训练灰度图时需要在配置里移除`dataset.args.transforms.Normalize` + +## Install Using Conda +``` +conda env create -f environment.yml +git clone https://github.com/WenmuZhou/DBNet.paddle.git +cd DBNet.paddle/ +``` + +or +## Install Manually +```bash +conda create -n dbnet python=3.6 +conda activate dbnet + +conda install ipython pip + +# python dependencies +pip install -r requirement.txt + +# clone repo +git clone https://github.com/WenmuZhou/DBNet.paddle.git +cd DBNet.paddle/ + +``` + +## Requirements +* paddlepaddle 2.4+ + +## Download + +TBD + +## Data Preparation + +Training data: prepare a text `train.txt` in the following format, use '\t' as a separator +``` +./datasets/train/img/001.jpg ./datasets/train/gt/001.txt +``` + +Validation data: prepare a text `test.txt` in the following format, use '\t' as a separator +``` +./datasets/test/img/001.jpg ./datasets/test/gt/001.txt +``` +- Store images in the `img` folder +- Store groundtruth in the `gt` folder + +The groundtruth can be `.txt` files, with the following format: +``` +x1, y1, x2, y2, x3, y3, x4, y4, annotation +``` + + +## Train +1. config the `dataset['train']['dataset'['data_path']'`,`dataset['validate']['dataset'['data_path']`in [config/icdar2015_resnet18_fpn_DBhead_polyLR.yaml](cconfig/icdar2015_resnet18_fpn_DBhead_polyLR.yaml) +* . single gpu train +```bash +bash singlel_gpu_train.sh +``` +* . Multi-gpu training +```bash +bash multi_gpu_train.sh +``` +## Test + +[eval.py](tools/eval.py) is used to test model on test dataset + +1. config `model_path` in [eval.sh](eval.sh) +2. use following script to test +```bash +bash eval.sh +``` + +## Predict +[predict.py](tools/predict.py) Can be used to inference on all images in a folder +1. config `model_path`,`input_folder`,`output_folder` in [predict.sh](predict.sh) +2. use following script to predict +``` +bash predict.sh +``` +You can change the `model_path` in the `predict.sh` file to your model location. + +tips: if result is not good, you can change `thre` in [predict.sh](predict.sh) + +## Export Model + +[export_model.py](tools/export_model.py) Can be used to inference on all images in a folder + +use following script to export inference model +``` +python tools/export_model.py --config_file config/icdar2015_resnet50_FPN_DBhead_polyLR.yaml -o trainer.resume_checkpoint=model_best.pth trainer.output_dir=output/infer +``` + +## Paddle Inference infer + +[infer.py](tools/infer.py) Can be used to inference on all images in a folder + +use following script to export inference model +``` +python tools/infer.py --model-dir=output/infer/ --img-path imgs/paper/db.jpg +``` + +

Performance

+ +### [ICDAR 2015](http://rrc.cvc.uab.es/?ch=4) +only train on ICDAR2015 dataset + +| Method | image size (short size) |learning rate | Precision (%) | Recall (%) | F-measure (%) | FPS | +|:--------------------------:|:-------:|:--------:|:--------:|:------------:|:---------------:|:-----:| +| ImageNet-resnet50-FPN-DBHead(torch) |736 |1e-3|90.19 | 78.14 | 83.88 | 27 | +| ImageNet-resnet50-FPN-DBHead(paddle) |736 |1e-3| 89.47 | 79.03 | 83.92 | 27 | +| ImageNet-resnet50-FPN-DBHead(paddle_amp) |736 |1e-3| 88.62 | 79.95 | 84.06 | 27 | + + +### examples +TBD + + +### reference +1. https://arxiv.org/pdf/1911.08947.pdf +2. https://github.com/WenmuZhou/DBNet.pytorch + +**If this repository helps you,please star it. Thanks.** diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/base/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/base/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..223e9e02d7ab1fbc2819f9ff33e04a2bd0d0303c --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/base/__init__.py @@ -0,0 +1,2 @@ +from .base_trainer import BaseTrainer +from .base_dataset import BaseDataSet \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/base/base_dataset.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/base/base_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..4a839a8ffbc34a2f671e905f8750269eb5af0371 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/base/base_dataset.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/12/4 13:12 +# @Author : zhoujun +import copy +from paddle.io import Dataset +from data_loader.modules import * + + +class BaseDataSet(Dataset): + def __init__(self, + data_path: str, + img_mode, + pre_processes, + filter_keys, + ignore_tags, + transform=None, + target_transform=None): + assert img_mode in ['RGB', 'BRG', 'GRAY'] + self.ignore_tags = ignore_tags + self.data_list = self.load_data(data_path) + item_keys = [ + 'img_path', 'img_name', 'text_polys', 'texts', 'ignore_tags' + ] + for item in item_keys: + assert item in self.data_list[ + 0], 'data_list from load_data must contains {}'.format( + item_keys) + self.img_mode = img_mode + self.filter_keys = filter_keys + self.transform = transform + self.target_transform = target_transform + self._init_pre_processes(pre_processes) + + def _init_pre_processes(self, pre_processes): + self.aug = [] + if pre_processes is not None: + for aug in pre_processes: + if 'args' not in aug: + args = {} + else: + args = aug['args'] + if isinstance(args, dict): + cls = eval(aug['type'])(**args) + else: + cls = eval(aug['type'])(args) + self.aug.append(cls) + + def load_data(self, data_path: str) -> list: + """ + 把数据加载为一个list: + :params data_path: 存储数据的文件夹或者文件 + return a dict ,包含了,'img_path','img_name','text_polys','texts','ignore_tags' + """ + raise NotImplementedError + + def apply_pre_processes(self, data): + for aug in self.aug: + data = aug(data) + return data + + def __getitem__(self, index): + try: + data = copy.deepcopy(self.data_list[index]) + im = cv2.imread(data['img_path'], 1 + if self.img_mode != 'GRAY' else 0) + if self.img_mode == 'RGB': + im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB) + data['img'] = im + data['shape'] = [im.shape[0], im.shape[1]] + data = self.apply_pre_processes(data) + + if self.transform: + data['img'] = self.transform(data['img']) + data['text_polys'] = data['text_polys'].tolist() + if len(self.filter_keys): + data_dict = {} + for k, v in data.items(): + if k not in self.filter_keys: + data_dict[k] = v + return data_dict + else: + return data + except: + return self.__getitem__(np.random.randint(self.__len__())) + + def __len__(self): + return len(self.data_list) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/base/base_trainer.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/base/base_trainer.py new file mode 100644 index 0000000000000000000000000000000000000000..82c308d361ca0b1da274aefdef6b626a81ae3cc7 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/base/base_trainer.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/23 21:50 +# @Author : zhoujun + +import os +import pathlib +import shutil +from pprint import pformat + +import anyconfig +import paddle +import numpy as np +import random +from paddle.jit import to_static +from paddle.static import InputSpec + +from utils import setup_logger + + +class BaseTrainer: + def __init__(self, + config, + model, + criterion, + train_loader, + validate_loader, + metric_cls, + post_process=None): + config['trainer']['output_dir'] = os.path.join( + str(pathlib.Path(os.path.abspath(__name__)).parent), + config['trainer']['output_dir']) + config['name'] = config['name'] + '_' + model.name + self.save_dir = config['trainer']['output_dir'] + self.checkpoint_dir = os.path.join(self.save_dir, 'checkpoint') + + os.makedirs(self.checkpoint_dir, exist_ok=True) + + self.global_step = 0 + self.start_epoch = 0 + self.config = config + self.criterion = criterion + # logger and tensorboard + self.visualdl_enable = self.config['trainer'].get('visual_dl', False) + self.epochs = self.config['trainer']['epochs'] + self.log_iter = self.config['trainer']['log_iter'] + if paddle.distributed.get_rank() == 0: + anyconfig.dump(config, os.path.join(self.save_dir, 'config.yaml')) + self.logger = setup_logger(os.path.join(self.save_dir, 'train.log')) + self.logger_info(pformat(self.config)) + + self.model = self.apply_to_static(model) + + # device + if paddle.device.cuda.device_count( + ) > 0 and paddle.device.is_compiled_with_cuda(): + self.with_cuda = True + random.seed(self.config['trainer']['seed']) + np.random.seed(self.config['trainer']['seed']) + paddle.seed(self.config['trainer']['seed']) + else: + self.with_cuda = False + self.logger_info('train with and paddle {}'.format(paddle.__version__)) + # metrics + self.metrics = { + 'recall': 0, + 'precision': 0, + 'hmean': 0, + 'train_loss': float('inf'), + 'best_model_epoch': 0 + } + + self.train_loader = train_loader + if validate_loader is not None: + assert post_process is not None and metric_cls is not None + self.validate_loader = validate_loader + self.post_process = post_process + self.metric_cls = metric_cls + self.train_loader_len = len(train_loader) + + if self.validate_loader is not None: + self.logger_info( + 'train dataset has {} samples,{} in dataloader, validate dataset has {} samples,{} in dataloader'. + format( + len(self.train_loader.dataset), self.train_loader_len, + len(self.validate_loader.dataset), + len(self.validate_loader))) + else: + self.logger_info( + 'train dataset has {} samples,{} in dataloader'.format( + len(self.train_loader.dataset), self.train_loader_len)) + + self._initialize_scheduler() + + self._initialize_optimizer() + + # resume or finetune + if self.config['trainer']['resume_checkpoint'] != '': + self._load_checkpoint( + self.config['trainer']['resume_checkpoint'], resume=True) + elif self.config['trainer']['finetune_checkpoint'] != '': + self._load_checkpoint( + self.config['trainer']['finetune_checkpoint'], resume=False) + + if self.visualdl_enable and paddle.distributed.get_rank() == 0: + from visualdl import LogWriter + self.writer = LogWriter(self.save_dir) + + # 混合精度训练 + self.amp = self.config.get('amp', None) + if self.amp == 'None': + self.amp = None + if self.amp: + self.amp['scaler'] = paddle.amp.GradScaler( + init_loss_scaling=self.amp.get("scale_loss", 1024), + use_dynamic_loss_scaling=self.amp.get( + 'use_dynamic_loss_scaling', True)) + self.model, self.optimizer = paddle.amp.decorate( + models=self.model, + optimizers=self.optimizer, + level=self.amp.get('amp_level', 'O2')) + + # 分布式训练 + if paddle.device.cuda.device_count() > 1: + self.model = paddle.DataParallel(self.model) + # make inverse Normalize + self.UN_Normalize = False + for t in self.config['dataset']['train']['dataset']['args'][ + 'transforms']: + if t['type'] == 'Normalize': + self.normalize_mean = t['args']['mean'] + self.normalize_std = t['args']['std'] + self.UN_Normalize = True + + def apply_to_static(self, model): + support_to_static = self.config['trainer'].get('to_static', False) + if support_to_static: + specs = None + print('static') + specs = [InputSpec([None, 3, -1, -1])] + model = to_static(model, input_spec=specs) + self.logger_info( + "Successfully to apply @to_static with specs: {}".format(specs)) + return model + + def train(self): + """ + Full training logic + """ + for epoch in range(self.start_epoch + 1, self.epochs + 1): + self.epoch_result = self._train_epoch(epoch) + self._on_epoch_finish() + if paddle.distributed.get_rank() == 0 and self.visualdl_enable: + self.writer.close() + self._on_train_finish() + + def _train_epoch(self, epoch): + """ + Training logic for an epoch + + :param epoch: Current epoch number + """ + raise NotImplementedError + + def _eval(self, epoch): + """ + eval logic for an epoch + + :param epoch: Current epoch number + """ + raise NotImplementedError + + def _on_epoch_finish(self): + raise NotImplementedError + + def _on_train_finish(self): + raise NotImplementedError + + def _save_checkpoint(self, epoch, file_name): + """ + Saving checkpoints + + :param epoch: current epoch number + :param log: logging information of the epoch + :param save_best: if True, rename the saved checkpoint to 'model_best.pth.tar' + """ + state_dict = self.model.state_dict() + state = { + 'epoch': epoch, + 'global_step': self.global_step, + 'state_dict': state_dict, + 'optimizer': self.optimizer.state_dict(), + 'config': self.config, + 'metrics': self.metrics + } + filename = os.path.join(self.checkpoint_dir, file_name) + paddle.save(state, filename) + + def _load_checkpoint(self, checkpoint_path, resume): + """ + Resume from saved checkpoints + :param checkpoint_path: Checkpoint path to be resumed + """ + self.logger_info("Loading checkpoint: {} ...".format(checkpoint_path)) + checkpoint = paddle.load(checkpoint_path) + self.model.set_state_dict(checkpoint['state_dict']) + if resume: + self.global_step = checkpoint['global_step'] + self.start_epoch = checkpoint['epoch'] + self.config['lr_scheduler']['args']['last_epoch'] = self.start_epoch + # self.scheduler.load_state_dict(checkpoint['scheduler']) + self.optimizer.set_state_dict(checkpoint['optimizer']) + if 'metrics' in checkpoint: + self.metrics = checkpoint['metrics'] + self.logger_info("resume from checkpoint {} (epoch {})".format( + checkpoint_path, self.start_epoch)) + else: + self.logger_info("finetune from checkpoint {}".format( + checkpoint_path)) + + def _initialize(self, name, module, *args, **kwargs): + module_name = self.config[name]['type'] + module_args = self.config[name].get('args', {}) + assert all([k not in module_args for k in kwargs + ]), 'Overwriting kwargs given in config file is not allowed' + module_args.update(kwargs) + return getattr(module, module_name)(*args, **module_args) + + def _initialize_scheduler(self): + self.lr_scheduler = self._initialize('lr_scheduler', + paddle.optimizer.lr) + + def _initialize_optimizer(self): + self.optimizer = self._initialize( + 'optimizer', + paddle.optimizer, + parameters=self.model.parameters(), + learning_rate=self.lr_scheduler) + + def inverse_normalize(self, batch_img): + if self.UN_Normalize: + batch_img[:, 0, :, :] = batch_img[:, 0, :, :] * self.normalize_std[ + 0] + self.normalize_mean[0] + batch_img[:, 1, :, :] = batch_img[:, 1, :, :] * self.normalize_std[ + 1] + self.normalize_mean[1] + batch_img[:, 2, :, :] = batch_img[:, 2, :, :] * self.normalize_std[ + 2] + self.normalize_mean[2] + + def logger_info(self, s): + if paddle.distributed.get_rank() == 0: + self.logger.info(s) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/SynthText.yaml b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/SynthText.yaml new file mode 100644 index 0000000000000000000000000000000000000000..61d5da7d3b4b8ec9da0bd79020d95c694ecef513 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/SynthText.yaml @@ -0,0 +1,40 @@ +name: DBNet +dataset: + train: + dataset: + type: SynthTextDataset # 数据集类型 + args: + data_path: ''# SynthTextDataset 根目录 + pre_processes: # 数据的预处理过程,包含augment和标签制作 + - type: IaaAugment # 使用imgaug进行变换 + args: + - {'type':Fliplr, 'args':{'p':0.5}} + - {'type': Affine, 'args':{'rotate':[-10,10]}} + - {'type':Resize,'args':{'size':[0.5,3]}} + - type: EastRandomCropData + args: + size: [640,640] + max_tries: 50 + keep_ratio: true + - type: MakeBorderMap + args: + shrink_ratio: 0.4 + - type: MakeShrinkMap + args: + shrink_ratio: 0.4 + min_text_size: 8 + transforms: # 对图片进行的变换方式 + - type: ToTensor + args: {} + - type: Normalize + args: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + img_mode: RGB + filter_keys: ['img_path','img_name','text_polys','texts','ignore_tags','shape'] # 返回数据之前,从数据字典里删除的key + ignore_tags: ['*', '###'] + loader: + batch_size: 1 + shuffle: true + num_workers: 0 + collate_fn: '' \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/SynthText_resnet18_FPN_DBhead_polyLR.yaml b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/SynthText_resnet18_FPN_DBhead_polyLR.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a665e94a7fbaa531b67a385ceeeae3be843e97b6 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/SynthText_resnet18_FPN_DBhead_polyLR.yaml @@ -0,0 +1,65 @@ +name: DBNet +base: ['config/SynthText.yaml'] +arch: + type: Model + backbone: + type: resnet18 + pretrained: true + neck: + type: FPN + inner_channels: 256 + head: + type: DBHead + out_channels: 2 + k: 50 +post_processing: + type: SegDetectorRepresenter + args: + thresh: 0.3 + box_thresh: 0.7 + max_candidates: 1000 + unclip_ratio: 1.5 # from paper +metric: + type: QuadMetric + args: + is_output_polygon: false +loss: + type: DBLoss + alpha: 1 + beta: 10 + ohem_ratio: 3 +optimizer: + type: Adam + args: + lr: 0.001 + weight_decay: 0 + amsgrad: true +lr_scheduler: + type: WarmupPolyLR + args: + warmup_epoch: 3 +trainer: + seed: 2 + epochs: 1200 + log_iter: 10 + show_images_iter: 50 + resume_checkpoint: '' + finetune_checkpoint: '' + output_dir: output + visual_dl: false +amp: + scale_loss: 1024 + amp_level: O2 + custom_white_list: [] + custom_black_list: ['exp', 'sigmoid', 'concat'] +dataset: + train: + dataset: + args: + data_path: ./datasets/SynthText + img_mode: RGB + loader: + batch_size: 2 + shuffle: true + num_workers: 6 + collate_fn: '' \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015.yaml b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4551b14b24acecf079b677699dceed01f7a68c12 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015.yaml @@ -0,0 +1,69 @@ +name: DBNet +dataset: + train: + dataset: + type: ICDAR2015Dataset # 数据集类型 + args: + data_path: # 一个存放 img_path \t gt_path的文件 + - '' + pre_processes: # 数据的预处理过程,包含augment和标签制作 + - type: IaaAugment # 使用imgaug进行变换 + args: + - {'type':Fliplr, 'args':{'p':0.5}} + - {'type': Affine, 'args':{'rotate':[-10,10]}} + - {'type':Resize,'args':{'size':[0.5,3]}} + - type: EastRandomCropData + args: + size: [640,640] + max_tries: 50 + keep_ratio: true + - type: MakeBorderMap + args: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - type: MakeShrinkMap + args: + shrink_ratio: 0.4 + min_text_size: 8 + transforms: # 对图片进行的变换方式 + - type: ToTensor + args: {} + - type: Normalize + args: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + img_mode: RGB + filter_keys: [img_path,img_name,text_polys,texts,ignore_tags,shape] # 返回数据之前,从数据字典里删除的key + ignore_tags: ['*', '###'] + loader: + batch_size: 1 + shuffle: true + num_workers: 0 + collate_fn: '' + validate: + dataset: + type: ICDAR2015Dataset + args: + data_path: + - '' + pre_processes: + - type: ResizeShortSize + args: + short_size: 736 + resize_text_polys: false + transforms: + - type: ToTensor + args: {} + - type: Normalize + args: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + img_mode: RGB + filter_keys: [] + ignore_tags: ['*', '###'] + loader: + batch_size: 1 + shuffle: true + num_workers: 0 + collate_fn: ICDARCollectFN \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015_dcn_resnet18_FPN_DBhead_polyLR.yaml b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015_dcn_resnet18_FPN_DBhead_polyLR.yaml new file mode 100644 index 0000000000000000000000000000000000000000..608ef42c1a2085a7450cf1662071f26cd7d472ae --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015_dcn_resnet18_FPN_DBhead_polyLR.yaml @@ -0,0 +1,82 @@ +name: DBNet +base: ['config/icdar2015.yaml'] +arch: + type: Model + backbone: + type: deformable_resnet18 + pretrained: true + neck: + type: FPN + inner_channels: 256 + head: + type: DBHead + out_channels: 2 + k: 50 +post_processing: + type: SegDetectorRepresenter + args: + thresh: 0.3 + box_thresh: 0.7 + max_candidates: 1000 + unclip_ratio: 1.5 # from paper +metric: + type: QuadMetric + args: + is_output_polygon: false +loss: + type: DBLoss + alpha: 1 + beta: 10 + ohem_ratio: 3 +optimizer: + type: Adam + args: + lr: 0.001 + weight_decay: 0 + amsgrad: true +lr_scheduler: + type: WarmupPolyLR + args: + warmup_epoch: 3 +trainer: + seed: 2 + epochs: 1200 + log_iter: 10 + show_images_iter: 50 + resume_checkpoint: '' + finetune_checkpoint: '' + output_dir: output + visual_dl: false +amp: + scale_loss: 1024 + amp_level: O2 + custom_white_list: [] + custom_black_list: ['exp', 'sigmoid', 'concat'] +dataset: + train: + dataset: + args: + data_path: + - ./datasets/train.txt + img_mode: RGB + loader: + batch_size: 1 + shuffle: true + num_workers: 6 + collate_fn: '' + validate: + dataset: + args: + data_path: + - ./datasets/test.txt + pre_processes: + - type: ResizeShortSize + args: + short_size: 736 + resize_text_polys: false + img_mode: RGB + loader: + batch_size: 1 + shuffle: true + num_workers: 6 + collate_fn: ICDARCollectFN \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015_resnet18_FPN_DBhead_polyLR.yaml b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015_resnet18_FPN_DBhead_polyLR.yaml new file mode 100644 index 0000000000000000000000000000000000000000..62c392b9ce67e754a212fa219fdc51e685d52f32 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015_resnet18_FPN_DBhead_polyLR.yaml @@ -0,0 +1,82 @@ +name: DBNet +base: ['config/icdar2015.yaml'] +arch: + type: Model + backbone: + type: resnet18 + pretrained: true + neck: + type: FPN + inner_channels: 256 + head: + type: DBHead + out_channels: 2 + k: 50 +post_processing: + type: SegDetectorRepresenter + args: + thresh: 0.3 + box_thresh: 0.7 + max_candidates: 1000 + unclip_ratio: 1.5 # from paper +metric: + type: QuadMetric + args: + is_output_polygon: false +loss: + type: DBLoss + alpha: 1 + beta: 10 + ohem_ratio: 3 +optimizer: + type: Adam + args: + lr: 0.001 + weight_decay: 0 + amsgrad: true +lr_scheduler: + type: WarmupPolyLR + args: + warmup_epoch: 3 +trainer: + seed: 2 + epochs: 1200 + log_iter: 10 + show_images_iter: 50 + resume_checkpoint: '' + finetune_checkpoint: '' + output_dir: output + visual_dl: false +amp: + scale_loss: 1024 + amp_level: O2 + custom_white_list: [] + custom_black_list: ['exp', 'sigmoid', 'concat'] +dataset: + train: + dataset: + args: + data_path: + - ./datasets/train.txt + img_mode: RGB + loader: + batch_size: 1 + shuffle: true + num_workers: 6 + collate_fn: '' + validate: + dataset: + args: + data_path: + - ./datasets/test.txt + pre_processes: + - type: ResizeShortSize + args: + short_size: 736 + resize_text_polys: false + img_mode: RGB + loader: + batch_size: 1 + shuffle: true + num_workers: 6 + collate_fn: ICDARCollectFN diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015_resnet18_FPN_DBhead_polyLR_finetune.yaml b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015_resnet18_FPN_DBhead_polyLR_finetune.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9b018d5cdd8877055f0c0372f313ab5d1beeb881 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015_resnet18_FPN_DBhead_polyLR_finetune.yaml @@ -0,0 +1,83 @@ +name: DBNet +base: ['config/icdar2015.yaml'] +arch: + type: Model + backbone: + type: resnet18 + pretrained: true + neck: + type: FPN + inner_channels: 256 + head: + type: DBHead + out_channels: 2 + k: 50 +post_processing: + type: SegDetectorRepresenter + args: + thresh: 0.3 + box_thresh: 0.7 + max_candidates: 1000 + unclip_ratio: 1.5 # from paper +metric: + type: QuadMetric + args: + is_output_polygon: false +loss: + type: DBLoss + alpha: 1 + beta: 10 + ohem_ratio: 3 +optimizer: + type: Adam + args: + lr: 0.001 + weight_decay: 0 + amsgrad: true +lr_scheduler: + type: StepLR + args: + step_size: 10 + gama: 0.8 +trainer: + seed: 2 + epochs: 500 + log_iter: 10 + show_images_iter: 50 + resume_checkpoint: '' + finetune_checkpoint: '' + output_dir: output + visual_dl: false +amp: + scale_loss: 1024 + amp_level: O2 + custom_white_list: [] + custom_black_list: ['exp', 'sigmoid', 'concat'] +dataset: + train: + dataset: + args: + data_path: + - ./datasets/train.txt + img_mode: RGB + loader: + batch_size: 1 + shuffle: true + num_workers: 6 + collate_fn: '' + validate: + dataset: + args: + data_path: + - ./datasets/test.txt + pre_processes: + - type: ResizeShortSize + args: + short_size: 736 + resize_text_polys: false + img_mode: RGB + loader: + batch_size: 1 + shuffle: true + num_workers: 6 + collate_fn: ICDARCollectFN diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015_resnet50_FPN_DBhead_polyLR.yaml b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015_resnet50_FPN_DBhead_polyLR.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2a870fd7c75de41c6f7cf20854029dfd7ed38e16 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/icdar2015_resnet50_FPN_DBhead_polyLR.yaml @@ -0,0 +1,79 @@ +name: DBNet +base: ['config/icdar2015.yaml'] +arch: + type: Model + backbone: + type: resnet50 + pretrained: true + neck: + type: FPN + inner_channels: 256 + head: + type: DBHead + out_channels: 2 + k: 50 +post_processing: + type: SegDetectorRepresenter + args: + thresh: 0.3 + box_thresh: 0.7 + max_candidates: 1000 + unclip_ratio: 1.5 # from paper +metric: + type: QuadMetric + args: + is_output_polygon: false +loss: + type: DBLoss + alpha: 1 + beta: 10 + ohem_ratio: 3 +optimizer: + type: Adam +lr_scheduler: + type: Polynomial + args: + learning_rate: 0.001 + warmup_epoch: 3 +trainer: + seed: 2 + epochs: 1200 + log_iter: 10 + show_images_iter: 50 + resume_checkpoint: '' + finetune_checkpoint: '' + output_dir: output/fp16_o2 + visual_dl: false +amp: + scale_loss: 1024 + amp_level: O2 + custom_white_list: [] + custom_black_list: ['exp', 'sigmoid', 'concat'] +dataset: + train: + dataset: + args: + data_path: + - ./datasets/train.txt + img_mode: RGB + loader: + batch_size: 16 + shuffle: true + num_workers: 6 + collate_fn: '' + validate: + dataset: + args: + data_path: + - ./datasets/test.txt + pre_processes: + - type: ResizeShortSize + args: + short_size: 736 + resize_text_polys: false + img_mode: RGB + loader: + batch_size: 1 + shuffle: true + num_workers: 6 + collate_fn: ICDARCollectFN diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/open_dataset.yaml b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/open_dataset.yaml new file mode 100644 index 0000000000000000000000000000000000000000..97267586c28fbd0145a8b14ed39c5520d6716bee --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/open_dataset.yaml @@ -0,0 +1,73 @@ +name: DBNet +dataset: + train: + dataset: + type: DetDataset # 数据集类型 + args: + data_path: # 一个存放 img_path \t gt_path的文件 + - '' + pre_processes: # 数据的预处理过程,包含augment和标签制作 + - type: IaaAugment # 使用imgaug进行变换 + args: + - {'type':Fliplr, 'args':{'p':0.5}} + - {'type': Affine, 'args':{'rotate':[-10,10]}} + - {'type':Resize,'args':{'size':[0.5,3]}} + - type: EastRandomCropData + args: + size: [640,640] + max_tries: 50 + keep_ratio: true + - type: MakeBorderMap + args: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - type: MakeShrinkMap + args: + shrink_ratio: 0.4 + min_text_size: 8 + transforms: # 对图片进行的变换方式 + - type: ToTensor + args: {} + - type: Normalize + args: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + img_mode: RGB + load_char_annotation: false + expand_one_char: false + filter_keys: [img_path,img_name,text_polys,texts,ignore_tags,shape] # 返回数据之前,从数据字典里删除的key + ignore_tags: ['*', '###'] + loader: + batch_size: 1 + shuffle: true + num_workers: 0 + collate_fn: '' + validate: + dataset: + type: DetDataset + args: + data_path: + - '' + pre_processes: + - type: ResizeShortSize + args: + short_size: 736 + resize_text_polys: false + transforms: + - type: ToTensor + args: {} + - type: Normalize + args: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + img_mode: RGB + load_char_annotation: false # 是否加载字符级标注 + expand_one_char: false # 是否对只有一个字符的框进行宽度扩充,扩充后w = w+h + filter_keys: [] + ignore_tags: ['*', '###'] + loader: + batch_size: 1 + shuffle: true + num_workers: 0 + collate_fn: ICDARCollectFN \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/open_dataset_dcn_resnet50_FPN_DBhead_polyLR.yaml b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/open_dataset_dcn_resnet50_FPN_DBhead_polyLR.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6c81738720e4de47ff2e528653eb0069b19dffdd --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/open_dataset_dcn_resnet50_FPN_DBhead_polyLR.yaml @@ -0,0 +1,86 @@ +name: DBNet +base: ['config/open_dataset.yaml'] +arch: + type: Model + backbone: + type: deformable_resnet18 + pretrained: true + neck: + type: FPN + inner_channels: 256 + head: + type: DBHead + out_channels: 2 + k: 50 +post_processing: + type: SegDetectorRepresenter + args: + thresh: 0.3 + box_thresh: 0.7 + max_candidates: 1000 + unclip_ratio: 1.5 # from paper +metric: + type: QuadMetric + args: + is_output_polygon: false +loss: + type: DBLoss + alpha: 1 + beta: 10 + ohem_ratio: 3 +optimizer: + type: Adam + args: + lr: 0.001 + weight_decay: 0 + amsgrad: true +lr_scheduler: + type: WarmupPolyLR + args: + warmup_epoch: 3 +trainer: + seed: 2 + epochs: 1200 + log_iter: 1 + show_images_iter: 1 + resume_checkpoint: '' + finetune_checkpoint: '' + output_dir: output + visual_dl: false +amp: + scale_loss: 1024 + amp_level: O2 + custom_white_list: [] + custom_black_list: ['exp', 'sigmoid', 'concat'] +dataset: + train: + dataset: + args: + data_path: + - ./datasets/train.json + img_mode: RGB + load_char_annotation: false + expand_one_char: false + loader: + batch_size: 2 + shuffle: true + num_workers: 6 + collate_fn: '' + validate: + dataset: + args: + data_path: + - ./datasets/test.json + pre_processes: + - type: ResizeShortSize + args: + short_size: 736 + resize_text_polys: false + img_mode: RGB + load_char_annotation: false + expand_one_char: false + loader: + batch_size: 1 + shuffle: true + num_workers: 6 + collate_fn: ICDARCollectFN diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/open_dataset_resnest50_FPN_DBhead_polyLR.yaml b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/open_dataset_resnest50_FPN_DBhead_polyLR.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d47ab06eddfebdd45376a7d60a771923215b932d --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/open_dataset_resnest50_FPN_DBhead_polyLR.yaml @@ -0,0 +1,86 @@ +name: DBNet +base: ['config/open_dataset.yaml'] +arch: + type: Model + backbone: + type: resnest50 + pretrained: true + neck: + type: FPN + inner_channels: 256 + head: + type: DBHead + out_channels: 2 + k: 50 +post_processing: + type: SegDetectorRepresenter + args: + thresh: 0.3 + box_thresh: 0.7 + max_candidates: 1000 + unclip_ratio: 1.5 # from paper +metric: + type: QuadMetric + args: + is_output_polygon: false +loss: + type: DBLoss + alpha: 1 + beta: 10 + ohem_ratio: 3 +optimizer: + type: Adam + args: + lr: 0.001 + weight_decay: 0 + amsgrad: true +lr_scheduler: + type: WarmupPolyLR + args: + warmup_epoch: 3 +trainer: + seed: 2 + epochs: 1200 + log_iter: 1 + show_images_iter: 1 + resume_checkpoint: '' + finetune_checkpoint: '' + output_dir: output + visual_dl: false +amp: + scale_loss: 1024 + amp_level: O2 + custom_white_list: [] + custom_black_list: ['exp', 'sigmoid', 'concat'] +dataset: + train: + dataset: + args: + data_path: + - ./datasets/train.json + img_mode: RGB + load_char_annotation: false + expand_one_char: false + loader: + batch_size: 2 + shuffle: true + num_workers: 6 + collate_fn: '' + validate: + dataset: + args: + data_path: + - ./datasets/test.json + pre_processes: + - type: ResizeShortSize + args: + short_size: 736 + resize_text_polys: false + img_mode: RGB + load_char_annotation: false + expand_one_char: false + loader: + batch_size: 1 + shuffle: true + num_workers: 6 + collate_fn: ICDARCollectFN diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/open_dataset_resnet18_FPN_DBhead_polyLR.yaml b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/open_dataset_resnet18_FPN_DBhead_polyLR.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ff16ddb26dc90b62dabde59630b2450de4fdf326 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/config/open_dataset_resnet18_FPN_DBhead_polyLR.yaml @@ -0,0 +1,93 @@ +name: DBNet +base: ['config/open_dataset.yaml'] +arch: + type: Model + backbone: + type: resnet18 + pretrained: true + neck: + type: FPN + inner_channels: 256 + head: + type: DBHead + out_channels: 2 + k: 50 +post_processing: + type: SegDetectorRepresenter + args: + thresh: 0.3 + box_thresh: 0.7 + max_candidates: 1000 + unclip_ratio: 1.5 # from paper +metric: + type: QuadMetric + args: + is_output_polygon: false +loss: + type: DBLoss + alpha: 1 + beta: 10 + ohem_ratio: 3 +optimizer: + type: Adam + args: + lr: 0.001 + weight_decay: 0 + amsgrad: true +lr_scheduler: + type: WarmupPolyLR + args: + warmup_epoch: 3 +trainer: + seed: 2 + epochs: 1200 + log_iter: 1 + show_images_iter: 1 + resume_checkpoint: '' + finetune_checkpoint: '' + output_dir: output + visual_dl: false +amp: + scale_loss: 1024 + amp_level: O2 + custom_white_list: [] + custom_black_list: ['exp', 'sigmoid', 'concat'] +dataset: + train: + dataset: + args: + data_path: + - ./datasets/train.json + transforms: # 对图片进行的变换方式 + - type: ToTensor + args: {} + - type: Normalize + args: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + img_mode: RGB + load_char_annotation: false + expand_one_char: false + loader: + batch_size: 2 + shuffle: true + num_workers: 6 + collate_fn: '' + validate: + dataset: + args: + data_path: + - ./datasets/test.json + pre_processes: + - type: ResizeShortSize + args: + short_size: 736 + resize_text_polys: false + img_mode: RGB + load_char_annotation: false + expand_one_char: false + loader: + batch_size: 1 + shuffle: true + num_workers: 6 + collate_fn: ICDARCollectFN diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..afc6e56b51948a3ec237967b9cf360eea984d625 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/__init__.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/23 21:52 +# @Author : zhoujun +import copy + +import PIL +import numpy as np +import paddle +from paddle.io import DataLoader, DistributedBatchSampler, BatchSampler + +from paddle.vision import transforms + + +def get_dataset(data_path, module_name, transform, dataset_args): + """ + 获取训练dataset + :param data_path: dataset文件列表,每个文件内以如下格式存储 ‘path/to/img\tlabel’ + :param module_name: 所使用的自定义dataset名称,目前只支持data_loaders.ImageDataset + :param transform: 该数据集使用的transforms + :param dataset_args: module_name的参数 + :return: 如果data_path列表不为空,返回对于的ConcatDataset对象,否则None + """ + from . import dataset + s_dataset = getattr(dataset, module_name)(transform=transform, + data_path=data_path, + **dataset_args) + return s_dataset + + +def get_transforms(transforms_config): + tr_list = [] + for item in transforms_config: + if 'args' not in item: + args = {} + else: + args = item['args'] + cls = getattr(transforms, item['type'])(**args) + tr_list.append(cls) + tr_list = transforms.Compose(tr_list) + return tr_list + + +class ICDARCollectFN: + def __init__(self, *args, **kwargs): + pass + + def __call__(self, batch): + data_dict = {} + to_tensor_keys = [] + for sample in batch: + for k, v in sample.items(): + if k not in data_dict: + data_dict[k] = [] + if isinstance(v, (np.ndarray, paddle.Tensor, PIL.Image.Image)): + if k not in to_tensor_keys: + to_tensor_keys.append(k) + data_dict[k].append(v) + for k in to_tensor_keys: + data_dict[k] = paddle.stack(data_dict[k], 0) + return data_dict + + +def get_dataloader(module_config, distributed=False): + if module_config is None: + return None + config = copy.deepcopy(module_config) + dataset_args = config['dataset']['args'] + if 'transforms' in dataset_args: + img_transfroms = get_transforms(dataset_args.pop('transforms')) + else: + img_transfroms = None + # 创建数据集 + dataset_name = config['dataset']['type'] + data_path = dataset_args.pop('data_path') + if data_path == None: + return None + + data_path = [x for x in data_path if x is not None] + if len(data_path) == 0: + return None + if 'collate_fn' not in config['loader'] or config['loader'][ + 'collate_fn'] is None or len(config['loader']['collate_fn']) == 0: + config['loader']['collate_fn'] = None + else: + config['loader']['collate_fn'] = eval(config['loader']['collate_fn'])() + + _dataset = get_dataset( + data_path=data_path, + module_name=dataset_name, + transform=img_transfroms, + dataset_args=dataset_args) + sampler = None + if distributed: + # 3)使用DistributedSampler + batch_sampler = DistributedBatchSampler( + dataset=_dataset, + batch_size=config['loader'].pop('batch_size'), + shuffle=config['loader'].pop('shuffle')) + else: + batch_sampler = BatchSampler( + dataset=_dataset, + batch_size=config['loader'].pop('batch_size'), + shuffle=config['loader'].pop('shuffle')) + loader = DataLoader( + dataset=_dataset, batch_sampler=batch_sampler, **config['loader']) + return loader diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/dataset.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..29d3954fe6b89c4585435d0221fa25c28cc8adef --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/dataset.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/23 21:54 +# @Author : zhoujun +import pathlib +import os +import cv2 +import numpy as np +import scipy.io as sio +from tqdm.auto import tqdm + +from base import BaseDataSet +from utils import order_points_clockwise, get_datalist, load, expand_polygon + + +class ICDAR2015Dataset(BaseDataSet): + def __init__(self, + data_path: str, + img_mode, + pre_processes, + filter_keys, + ignore_tags, + transform=None, + **kwargs): + super().__init__(data_path, img_mode, pre_processes, filter_keys, + ignore_tags, transform) + + def load_data(self, data_path: str) -> list: + data_list = get_datalist(data_path) + t_data_list = [] + for img_path, label_path in data_list: + data = self._get_annotation(label_path) + if len(data['text_polys']) > 0: + item = { + 'img_path': img_path, + 'img_name': pathlib.Path(img_path).stem + } + item.update(data) + t_data_list.append(item) + else: + print('there is no suit bbox in {}'.format(label_path)) + return t_data_list + + def _get_annotation(self, label_path: str) -> dict: + boxes = [] + texts = [] + ignores = [] + with open(label_path, encoding='utf-8', mode='r') as f: + for line in f.readlines(): + params = line.strip().strip('\ufeff').strip( + '\xef\xbb\xbf').split(',') + try: + box = order_points_clockwise( + np.array(list(map(float, params[:8]))).reshape(-1, 2)) + if cv2.contourArea(box) > 0: + boxes.append(box) + label = params[8] + texts.append(label) + ignores.append(label in self.ignore_tags) + except: + print('load label failed on {}'.format(label_path)) + data = { + 'text_polys': np.array(boxes), + 'texts': texts, + 'ignore_tags': ignores, + } + return data + + +class DetDataset(BaseDataSet): + def __init__(self, + data_path: str, + img_mode, + pre_processes, + filter_keys, + ignore_tags, + transform=None, + **kwargs): + self.load_char_annotation = kwargs['load_char_annotation'] + self.expand_one_char = kwargs['expand_one_char'] + super().__init__(data_path, img_mode, pre_processes, filter_keys, + ignore_tags, transform) + + def load_data(self, data_path: str) -> list: + """ + 从json文件中读取出 文本行的坐标和gt,字符的坐标和gt + :param data_path: + :return: + """ + data_list = [] + for path in data_path: + content = load(path) + for gt in tqdm( + content['data_list'], desc='read file {}'.format(path)): + img_path = os.path.join(content['data_root'], gt['img_name']) + polygons = [] + texts = [] + illegibility_list = [] + language_list = [] + for annotation in gt['annotations']: + if len(annotation['polygon']) == 0 or len(annotation[ + 'text']) == 0: + continue + if len(annotation['text']) > 1 and self.expand_one_char: + annotation['polygon'] = expand_polygon(annotation[ + 'polygon']) + polygons.append(annotation['polygon']) + texts.append(annotation['text']) + illegibility_list.append(annotation['illegibility']) + language_list.append(annotation['language']) + if self.load_char_annotation: + for char_annotation in annotation['chars']: + if len(char_annotation['polygon']) == 0 or len( + char_annotation['char']) == 0: + continue + polygons.append(char_annotation['polygon']) + texts.append(char_annotation['char']) + illegibility_list.append(char_annotation[ + 'illegibility']) + language_list.append(char_annotation['language']) + data_list.append({ + 'img_path': img_path, + 'img_name': gt['img_name'], + 'text_polys': np.array(polygons), + 'texts': texts, + 'ignore_tags': illegibility_list + }) + return data_list + + +class SynthTextDataset(BaseDataSet): + def __init__(self, + data_path: str, + img_mode, + pre_processes, + filter_keys, + transform=None, + **kwargs): + self.transform = transform + self.dataRoot = pathlib.Path(data_path) + if not self.dataRoot.exists(): + raise FileNotFoundError('Dataset folder is not exist.') + + self.targetFilePath = self.dataRoot / 'gt.mat' + if not self.targetFilePath.exists(): + raise FileExistsError('Target file is not exist.') + targets = {} + sio.loadmat( + self.targetFilePath, + targets, + squeeze_me=True, + struct_as_record=False, + variable_names=['imnames', 'wordBB', 'txt']) + + self.imageNames = targets['imnames'] + self.wordBBoxes = targets['wordBB'] + self.transcripts = targets['txt'] + super().__init__(data_path, img_mode, pre_processes, filter_keys, + transform) + + def load_data(self, data_path: str) -> list: + t_data_list = [] + for imageName, wordBBoxes, texts in zip( + self.imageNames, self.wordBBoxes, self.transcripts): + item = {} + wordBBoxes = np.expand_dims( + wordBBoxes, axis=2) if (wordBBoxes.ndim == 2) else wordBBoxes + _, _, numOfWords = wordBBoxes.shape + text_polys = wordBBoxes.reshape( + [8, numOfWords], order='F').T # num_words * 8 + text_polys = text_polys.reshape(numOfWords, 4, + 2) # num_of_words * 4 * 2 + transcripts = [word for line in texts for word in line.split()] + if numOfWords != len(transcripts): + continue + item['img_path'] = str(self.dataRoot / imageName) + item['img_name'] = (self.dataRoot / imageName).stem + item['text_polys'] = text_polys + item['texts'] = transcripts + item['ignore_tags'] = [x in self.ignore_tags for x in transcripts] + t_data_list.append(item) + return t_data_list diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bc055dae5d44876463c5eb1edfea18b96c319ca8 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/12/4 10:53 +# @Author : zhoujun +from .iaa_augment import IaaAugment +from .augment import * +from .random_crop_data import EastRandomCropData, PSERandomCrop +from .make_border_map import MakeBorderMap +from .make_shrink_map import MakeShrinkMap diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/augment.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/augment.py new file mode 100644 index 0000000000000000000000000000000000000000..e81bc123d914a7f2eb143d3c2504fab0e0f8d63f --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/augment.py @@ -0,0 +1,304 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/23 21:52 +# @Author : zhoujun + +import math +import numbers +import random + +import cv2 +import numpy as np +from skimage.util import random_noise + + +class RandomNoise: + def __init__(self, random_rate): + self.random_rate = random_rate + + def __call__(self, data: dict): + """ + 对图片加噪声 + :param data: {'img':,'text_polys':,'texts':,'ignore_tags':} + :return: + """ + if random.random() > self.random_rate: + return data + data['img'] = (random_noise( + data['img'], mode='gaussian', clip=True) * 255).astype(im.dtype) + return data + + +class RandomScale: + def __init__(self, scales, random_rate): + """ + :param scales: 尺度 + :param ramdon_rate: 随机系数 + :return: + """ + self.random_rate = random_rate + self.scales = scales + + def __call__(self, data: dict) -> dict: + """ + 从scales中随机选择一个尺度,对图片和文本框进行缩放 + :param data: {'img':,'text_polys':,'texts':,'ignore_tags':} + :return: + """ + if random.random() > self.random_rate: + return data + im = data['img'] + text_polys = data['text_polys'] + + tmp_text_polys = text_polys.copy() + rd_scale = float(np.random.choice(self.scales)) + im = cv2.resize(im, dsize=None, fx=rd_scale, fy=rd_scale) + tmp_text_polys *= rd_scale + + data['img'] = im + data['text_polys'] = tmp_text_polys + return data + + +class RandomRotateImgBox: + def __init__(self, degrees, random_rate, same_size=False): + """ + :param degrees: 角度,可以是一个数值或者list + :param ramdon_rate: 随机系数 + :param same_size: 是否保持和原图一样大 + :return: + """ + if isinstance(degrees, numbers.Number): + if degrees < 0: + raise ValueError( + "If degrees is a single number, it must be positive.") + degrees = (-degrees, degrees) + elif isinstance(degrees, list) or isinstance( + degrees, tuple) or isinstance(degrees, np.ndarray): + if len(degrees) != 2: + raise ValueError( + "If degrees is a sequence, it must be of len 2.") + degrees = degrees + else: + raise Exception( + 'degrees must in Number or list or tuple or np.ndarray') + self.degrees = degrees + self.same_size = same_size + self.random_rate = random_rate + + def __call__(self, data: dict) -> dict: + """ + 从scales中随机选择一个尺度,对图片和文本框进行缩放 + :param data: {'img':,'text_polys':,'texts':,'ignore_tags':} + :return: + """ + if random.random() > self.random_rate: + return data + im = data['img'] + text_polys = data['text_polys'] + + # ---------------------- 旋转图像 ---------------------- + w = im.shape[1] + h = im.shape[0] + angle = np.random.uniform(self.degrees[0], self.degrees[1]) + + if self.same_size: + nw = w + nh = h + else: + # 角度变弧度 + rangle = np.deg2rad(angle) + # 计算旋转之后图像的w, h + nw = (abs(np.sin(rangle) * h) + abs(np.cos(rangle) * w)) + nh = (abs(np.cos(rangle) * h) + abs(np.sin(rangle) * w)) + # 构造仿射矩阵 + rot_mat = cv2.getRotationMatrix2D((nw * 0.5, nh * 0.5), angle, 1) + # 计算原图中心点到新图中心点的偏移量 + rot_move = np.dot(rot_mat, + np.array([(nw - w) * 0.5, (nh - h) * 0.5, 0])) + # 更新仿射矩阵 + rot_mat[0, 2] += rot_move[0] + rot_mat[1, 2] += rot_move[1] + # 仿射变换 + rot_img = cv2.warpAffine( + im, + rot_mat, (int(math.ceil(nw)), int(math.ceil(nh))), + flags=cv2.INTER_LANCZOS4) + + # ---------------------- 矫正bbox坐标 ---------------------- + # rot_mat是最终的旋转矩阵 + # 获取原始bbox的四个中点,然后将这四个点转换到旋转后的坐标系下 + rot_text_polys = list() + for bbox in text_polys: + point1 = np.dot(rot_mat, np.array([bbox[0, 0], bbox[0, 1], 1])) + point2 = np.dot(rot_mat, np.array([bbox[1, 0], bbox[1, 1], 1])) + point3 = np.dot(rot_mat, np.array([bbox[2, 0], bbox[2, 1], 1])) + point4 = np.dot(rot_mat, np.array([bbox[3, 0], bbox[3, 1], 1])) + rot_text_polys.append([point1, point2, point3, point4]) + data['img'] = rot_img + data['text_polys'] = np.array(rot_text_polys) + return data + + +class RandomResize: + def __init__(self, size, random_rate, keep_ratio=False): + """ + :param input_size: resize尺寸,数字或者list的形式,如果为list形式,就是[w,h] + :param ramdon_rate: 随机系数 + :param keep_ratio: 是否保持长宽比 + :return: + """ + if isinstance(size, numbers.Number): + if size < 0: + raise ValueError( + "If input_size is a single number, it must be positive.") + size = (size, size) + elif isinstance(size, list) or isinstance(size, tuple) or isinstance( + size, np.ndarray): + if len(size) != 2: + raise ValueError( + "If input_size is a sequence, it must be of len 2.") + size = (size[0], size[1]) + else: + raise Exception( + 'input_size must in Number or list or tuple or np.ndarray') + self.size = size + self.keep_ratio = keep_ratio + self.random_rate = random_rate + + def __call__(self, data: dict) -> dict: + """ + 从scales中随机选择一个尺度,对图片和文本框进行缩放 + :param data: {'img':,'text_polys':,'texts':,'ignore_tags':} + :return: + """ + if random.random() > self.random_rate: + return data + im = data['img'] + text_polys = data['text_polys'] + + if self.keep_ratio: + # 将图片短边pad到和长边一样 + h, w, c = im.shape + max_h = max(h, self.size[0]) + max_w = max(w, self.size[1]) + im_padded = np.zeros((max_h, max_w, c), dtype=np.uint8) + im_padded[:h, :w] = im.copy() + im = im_padded + text_polys = text_polys.astype(np.float32) + h, w, _ = im.shape + im = cv2.resize(im, self.size) + w_scale = self.size[0] / float(w) + h_scale = self.size[1] / float(h) + text_polys[:, :, 0] *= w_scale + text_polys[:, :, 1] *= h_scale + + data['img'] = im + data['text_polys'] = text_polys + return data + + +def resize_image(img, short_size): + height, width, _ = img.shape + if height < width: + new_height = short_size + new_width = new_height / height * width + else: + new_width = short_size + new_height = new_width / width * height + new_height = int(round(new_height / 32) * 32) + new_width = int(round(new_width / 32) * 32) + resized_img = cv2.resize(img, (new_width, new_height)) + return resized_img, (new_width / width, new_height / height) + + +class ResizeShortSize: + def __init__(self, short_size, resize_text_polys=True): + """ + :param size: resize尺寸,数字或者list的形式,如果为list形式,就是[w,h] + :return: + """ + self.short_size = short_size + self.resize_text_polys = resize_text_polys + + def __call__(self, data: dict) -> dict: + """ + 对图片和文本框进行缩放 + :param data: {'img':,'text_polys':,'texts':,'ignore_tags':} + :return: + """ + im = data['img'] + text_polys = data['text_polys'] + + h, w, _ = im.shape + short_edge = min(h, w) + if short_edge < self.short_size: + # 保证短边 >= short_size + scale = self.short_size / short_edge + im = cv2.resize(im, dsize=None, fx=scale, fy=scale) + scale = (scale, scale) + # im, scale = resize_image(im, self.short_size) + if self.resize_text_polys: + # text_polys *= scale + text_polys[:, 0] *= scale[0] + text_polys[:, 1] *= scale[1] + + data['img'] = im + data['text_polys'] = text_polys + return data + + +class HorizontalFlip: + def __init__(self, random_rate): + """ + + :param random_rate: 随机系数 + """ + self.random_rate = random_rate + + def __call__(self, data: dict) -> dict: + """ + 从scales中随机选择一个尺度,对图片和文本框进行缩放 + :param data: {'img':,'text_polys':,'texts':,'ignore_tags':} + :return: + """ + if random.random() > self.random_rate: + return data + im = data['img'] + text_polys = data['text_polys'] + + flip_text_polys = text_polys.copy() + flip_im = cv2.flip(im, 1) + h, w, _ = flip_im.shape + flip_text_polys[:, :, 0] = w - flip_text_polys[:, :, 0] + + data['img'] = flip_im + data['text_polys'] = flip_text_polys + return data + + +class VerticallFlip: + def __init__(self, random_rate): + """ + + :param random_rate: 随机系数 + """ + self.random_rate = random_rate + + def __call__(self, data: dict) -> dict: + """ + 从scales中随机选择一个尺度,对图片和文本框进行缩放 + :param data: {'img':,'text_polys':,'texts':,'ignore_tags':} + :return: + """ + if random.random() > self.random_rate: + return data + im = data['img'] + text_polys = data['text_polys'] + + flip_text_polys = text_polys.copy() + flip_im = cv2.flip(im, 0) + h, w, _ = flip_im.shape + flip_text_polys[:, :, 1] = h - flip_text_polys[:, :, 1] + data['img'] = flip_im + data['text_polys'] = flip_text_polys + return data diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/iaa_augment.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/iaa_augment.py new file mode 100644 index 0000000000000000000000000000000000000000..1cf891bbd6fd5763aacbd60749d4dd9b6de89681 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/iaa_augment.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/12/4 18:06 +# @Author : zhoujun +import numpy as np +import imgaug +import imgaug.augmenters as iaa + + +class AugmenterBuilder(object): + def __init__(self): + pass + + def build(self, args, root=True): + if args is None or len(args) == 0: + return None + elif isinstance(args, list): + if root: + sequence = [self.build(value, root=False) for value in args] + return iaa.Sequential(sequence) + else: + return getattr( + iaa, + args[0])(* [self.to_tuple_if_list(a) for a in args[1:]]) + elif isinstance(args, dict): + cls = getattr(iaa, args['type']) + return cls(**{ + k: self.to_tuple_if_list(v) + for k, v in args['args'].items() + }) + else: + raise RuntimeError('unknown augmenter arg: ' + str(args)) + + def to_tuple_if_list(self, obj): + if isinstance(obj, list): + return tuple(obj) + return obj + + +class IaaAugment(): + def __init__(self, augmenter_args): + self.augmenter_args = augmenter_args + self.augmenter = AugmenterBuilder().build(self.augmenter_args) + + def __call__(self, data): + image = data['img'] + shape = image.shape + + if self.augmenter: + aug = self.augmenter.to_deterministic() + data['img'] = aug.augment_image(image) + data = self.may_augment_annotation(aug, data, shape) + return data + + def may_augment_annotation(self, aug, data, shape): + if aug is None: + return data + + line_polys = [] + for poly in data['text_polys']: + new_poly = self.may_augment_poly(aug, shape, poly) + line_polys.append(new_poly) + data['text_polys'] = np.array(line_polys) + return data + + def may_augment_poly(self, aug, img_shape, poly): + keypoints = [imgaug.Keypoint(p[0], p[1]) for p in poly] + keypoints = aug.augment_keypoints( + [imgaug.KeypointsOnImage( + keypoints, shape=img_shape)])[0].keypoints + poly = [(p.x, p.y) for p in keypoints] + return poly diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/make_border_map.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/make_border_map.py new file mode 100644 index 0000000000000000000000000000000000000000..2985f3c8a01c67efb71b5279edf95dd3f9fe5680 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/make_border_map.py @@ -0,0 +1,143 @@ +import cv2 +import numpy as np +np.seterr(divide='ignore', invalid='ignore') +import pyclipper +from shapely.geometry import Polygon + + +class MakeBorderMap(): + def __init__(self, shrink_ratio=0.4, thresh_min=0.3, thresh_max=0.7): + self.shrink_ratio = shrink_ratio + self.thresh_min = thresh_min + self.thresh_max = thresh_max + + def __call__(self, data: dict) -> dict: + """ + 从scales中随机选择一个尺度,对图片和文本框进行缩放 + :param data: {'img':,'text_polys':,'texts':,'ignore_tags':} + :return: + """ + im = data['img'] + text_polys = data['text_polys'] + ignore_tags = data['ignore_tags'] + + canvas = np.zeros(im.shape[:2], dtype=np.float32) + mask = np.zeros(im.shape[:2], dtype=np.float32) + + for i in range(len(text_polys)): + if ignore_tags[i]: + continue + self.draw_border_map(text_polys[i], canvas, mask=mask) + canvas = canvas * (self.thresh_max - self.thresh_min) + self.thresh_min + + data['threshold_map'] = canvas + data['threshold_mask'] = mask + return data + + def draw_border_map(self, polygon, canvas, mask): + polygon = np.array(polygon) + assert polygon.ndim == 2 + assert polygon.shape[1] == 2 + + polygon_shape = Polygon(polygon) + if polygon_shape.area <= 0: + return + distance = polygon_shape.area * ( + 1 - np.power(self.shrink_ratio, 2)) / polygon_shape.length + subject = [tuple(l) for l in polygon] + padding = pyclipper.PyclipperOffset() + padding.AddPath(subject, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) + + padded_polygon = np.array(padding.Execute(distance)[0]) + cv2.fillPoly(mask, [padded_polygon.astype(np.int32)], 1.0) + + xmin = padded_polygon[:, 0].min() + xmax = padded_polygon[:, 0].max() + ymin = padded_polygon[:, 1].min() + ymax = padded_polygon[:, 1].max() + width = xmax - xmin + 1 + height = ymax - ymin + 1 + + polygon[:, 0] = polygon[:, 0] - xmin + polygon[:, 1] = polygon[:, 1] - ymin + + xs = np.broadcast_to( + np.linspace( + 0, width - 1, num=width).reshape(1, width), (height, width)) + ys = np.broadcast_to( + np.linspace( + 0, height - 1, num=height).reshape(height, 1), (height, width)) + + distance_map = np.zeros( + (polygon.shape[0], height, width), dtype=np.float32) + for i in range(polygon.shape[0]): + j = (i + 1) % polygon.shape[0] + absolute_distance = self.distance(xs, ys, polygon[i], polygon[j]) + distance_map[i] = np.clip(absolute_distance / distance, 0, 1) + distance_map = distance_map.min(axis=0) + + xmin_valid = min(max(0, xmin), canvas.shape[1] - 1) + xmax_valid = min(max(0, xmax), canvas.shape[1] - 1) + ymin_valid = min(max(0, ymin), canvas.shape[0] - 1) + ymax_valid = min(max(0, ymax), canvas.shape[0] - 1) + canvas[ymin_valid:ymax_valid + 1, xmin_valid:xmax_valid + 1] = np.fmax( + 1 - distance_map[ymin_valid - ymin:ymax_valid - ymax + height, + xmin_valid - xmin:xmax_valid - xmax + width], + canvas[ymin_valid:ymax_valid + 1, xmin_valid:xmax_valid + 1]) + + def distance(self, xs, ys, point_1, point_2): + ''' + compute the distance from point to a line + ys: coordinates in the first axis + xs: coordinates in the second axis + point_1, point_2: (x, y), the end of the line + ''' + height, width = xs.shape[:2] + square_distance_1 = np.square(xs - point_1[0]) + np.square(ys - point_1[ + 1]) + square_distance_2 = np.square(xs - point_2[0]) + np.square(ys - point_2[ + 1]) + square_distance = np.square(point_1[0] - point_2[0]) + np.square( + point_1[1] - point_2[1]) + + cosin = (square_distance - square_distance_1 - square_distance_2) / ( + 2 * np.sqrt(square_distance_1 * square_distance_2)) + square_sin = 1 - np.square(cosin) + square_sin = np.nan_to_num(square_sin) + + result = np.sqrt(square_distance_1 * square_distance_2 * square_sin / + square_distance) + result[cosin < + 0] = np.sqrt(np.fmin(square_distance_1, square_distance_2))[cosin + < 0] + # self.extend_line(point_1, point_2, result) + return result + + def extend_line(self, point_1, point_2, result): + ex_point_1 = (int( + round(point_1[0] + (point_1[0] - point_2[0]) * ( + 1 + self.shrink_ratio))), int( + round(point_1[1] + (point_1[1] - point_2[1]) * ( + 1 + self.shrink_ratio)))) + cv2.line( + result, + tuple(ex_point_1), + tuple(point_1), + 4096.0, + 1, + lineType=cv2.LINE_AA, + shift=0) + ex_point_2 = (int( + round(point_2[0] + (point_2[0] - point_1[0]) * ( + 1 + self.shrink_ratio))), int( + round(point_2[1] + (point_2[1] - point_1[1]) * ( + 1 + self.shrink_ratio)))) + cv2.line( + result, + tuple(ex_point_2), + tuple(point_2), + 4096.0, + 1, + lineType=cv2.LINE_AA, + shift=0) + return ex_point_1, ex_point_2 diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/make_shrink_map.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/make_shrink_map.py new file mode 100644 index 0000000000000000000000000000000000000000..3f268b9dead349538e9f47d5b960feea27f90c51 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/make_shrink_map.py @@ -0,0 +1,133 @@ +import numpy as np +import cv2 + + +def shrink_polygon_py(polygon, shrink_ratio): + """ + 对框进行缩放,返回去的比例为1/shrink_ratio 即可 + """ + cx = polygon[:, 0].mean() + cy = polygon[:, 1].mean() + polygon[:, 0] = cx + (polygon[:, 0] - cx) * shrink_ratio + polygon[:, 1] = cy + (polygon[:, 1] - cy) * shrink_ratio + return polygon + + +def shrink_polygon_pyclipper(polygon, shrink_ratio): + from shapely.geometry import Polygon + import pyclipper + polygon_shape = Polygon(polygon) + distance = polygon_shape.area * ( + 1 - np.power(shrink_ratio, 2)) / polygon_shape.length + subject = [tuple(l) for l in polygon] + padding = pyclipper.PyclipperOffset() + padding.AddPath(subject, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) + shrinked = padding.Execute(-distance) + if shrinked == []: + shrinked = np.array(shrinked) + else: + shrinked = np.array(shrinked[0]).reshape(-1, 2) + return shrinked + + +class MakeShrinkMap(): + r''' + Making binary mask from detection data with ICDAR format. + Typically following the process of class `MakeICDARData`. + ''' + + def __init__(self, + min_text_size=8, + shrink_ratio=0.4, + shrink_type='pyclipper'): + shrink_func_dict = { + 'py': shrink_polygon_py, + 'pyclipper': shrink_polygon_pyclipper + } + self.shrink_func = shrink_func_dict[shrink_type] + self.min_text_size = min_text_size + self.shrink_ratio = shrink_ratio + + def __call__(self, data: dict) -> dict: + """ + 从scales中随机选择一个尺度,对图片和文本框进行缩放 + :param data: {'img':,'text_polys':,'texts':,'ignore_tags':} + :return: + """ + image = data['img'] + text_polys = data['text_polys'] + ignore_tags = data['ignore_tags'] + + h, w = image.shape[:2] + text_polys, ignore_tags = self.validate_polygons(text_polys, + ignore_tags, h, w) + gt = np.zeros((h, w), dtype=np.float32) + mask = np.ones((h, w), dtype=np.float32) + for i in range(len(text_polys)): + polygon = text_polys[i] + height = max(polygon[:, 1]) - min(polygon[:, 1]) + width = max(polygon[:, 0]) - min(polygon[:, 0]) + if ignore_tags[i] or min(height, width) < self.min_text_size: + cv2.fillPoly(mask, + polygon.astype(np.int32)[np.newaxis, :, :], 0) + ignore_tags[i] = True + else: + shrinked = self.shrink_func(polygon, self.shrink_ratio) + if shrinked.size == 0: + cv2.fillPoly(mask, + polygon.astype(np.int32)[np.newaxis, :, :], 0) + ignore_tags[i] = True + continue + cv2.fillPoly(gt, [shrinked.astype(np.int32)], 1) + + data['shrink_map'] = gt + data['shrink_mask'] = mask + return data + + def validate_polygons(self, polygons, ignore_tags, h, w): + ''' + polygons (numpy.array, required): of shape (num_instances, num_points, 2) + ''' + if len(polygons) == 0: + return polygons, ignore_tags + assert len(polygons) == len(ignore_tags) + for polygon in polygons: + polygon[:, 0] = np.clip(polygon[:, 0], 0, w - 1) + polygon[:, 1] = np.clip(polygon[:, 1], 0, h - 1) + + for i in range(len(polygons)): + area = self.polygon_area(polygons[i]) + if abs(area) < 1: + ignore_tags[i] = True + if area > 0: + polygons[i] = polygons[i][::-1, :] + return polygons, ignore_tags + + def polygon_area(self, polygon): + return cv2.contourArea(polygon) + # edge = 0 + # for i in range(polygon.shape[0]): + # next_index = (i + 1) % polygon.shape[0] + # edge += (polygon[next_index, 0] - polygon[i, 0]) * (polygon[next_index, 1] - polygon[i, 1]) + # + # return edge / 2. + + +if __name__ == '__main__': + from shapely.geometry import Polygon + import pyclipper + + polygon = np.array([[0, 0], [100, 10], [100, 100], [10, 90]]) + a = shrink_polygon_py(polygon, 0.4) + print(a) + print(shrink_polygon_py(a, 1 / 0.4)) + b = shrink_polygon_pyclipper(polygon, 0.4) + print(b) + poly = Polygon(b) + distance = poly.area * 1.5 / poly.length + offset = pyclipper.PyclipperOffset() + offset.AddPath(b, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) + expanded = np.array(offset.Execute(distance)) + bounding_box = cv2.minAreaRect(expanded) + points = cv2.boxPoints(bounding_box) + print(points) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/random_crop_data.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/random_crop_data.py new file mode 100644 index 0000000000000000000000000000000000000000..fac2e4c07cfca966e6c17231f2ab4bce702f191f --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/data_loader/modules/random_crop_data.py @@ -0,0 +1,206 @@ +import random + +import cv2 +import numpy as np + + +# random crop algorithm similar to https://github.com/argman/EAST +class EastRandomCropData(): + def __init__(self, + size=(640, 640), + max_tries=50, + min_crop_side_ratio=0.1, + require_original_image=False, + keep_ratio=True): + self.size = size + self.max_tries = max_tries + self.min_crop_side_ratio = min_crop_side_ratio + self.require_original_image = require_original_image + self.keep_ratio = keep_ratio + + def __call__(self, data: dict) -> dict: + """ + 从scales中随机选择一个尺度,对图片和文本框进行缩放 + :param data: {'img':,'text_polys':,'texts':,'ignore_tags':} + :return: + """ + im = data['img'] + text_polys = data['text_polys'] + ignore_tags = data['ignore_tags'] + texts = data['texts'] + all_care_polys = [ + text_polys[i] for i, tag in enumerate(ignore_tags) if not tag + ] + # 计算crop区域 + crop_x, crop_y, crop_w, crop_h = self.crop_area(im, all_care_polys) + # crop 图片 保持比例填充 + scale_w = self.size[0] / crop_w + scale_h = self.size[1] / crop_h + scale = min(scale_w, scale_h) + h = int(crop_h * scale) + w = int(crop_w * scale) + if self.keep_ratio: + if len(im.shape) == 3: + padimg = np.zeros((self.size[1], self.size[0], im.shape[2]), + im.dtype) + else: + padimg = np.zeros((self.size[1], self.size[0]), im.dtype) + padimg[:h, :w] = cv2.resize( + im[crop_y:crop_y + crop_h, crop_x:crop_x + crop_w], (w, h)) + img = padimg + else: + img = cv2.resize(im[crop_y:crop_y + crop_h, crop_x:crop_x + crop_w], + tuple(self.size)) + # crop 文本框 + text_polys_crop = [] + ignore_tags_crop = [] + texts_crop = [] + for poly, text, tag in zip(text_polys, texts, ignore_tags): + poly = ((poly - (crop_x, crop_y)) * scale).tolist() + if not self.is_poly_outside_rect(poly, 0, 0, w, h): + text_polys_crop.append(poly) + ignore_tags_crop.append(tag) + texts_crop.append(text) + data['img'] = img + data['text_polys'] = np.float32(text_polys_crop) + data['ignore_tags'] = ignore_tags_crop + data['texts'] = texts_crop + return data + + def is_poly_in_rect(self, poly, x, y, w, h): + poly = np.array(poly) + if poly[:, 0].min() < x or poly[:, 0].max() > x + w: + return False + if poly[:, 1].min() < y or poly[:, 1].max() > y + h: + return False + return True + + def is_poly_outside_rect(self, poly, x, y, w, h): + poly = np.array(poly) + if poly[:, 0].max() < x or poly[:, 0].min() > x + w: + return True + if poly[:, 1].max() < y or poly[:, 1].min() > y + h: + return True + return False + + def split_regions(self, axis): + regions = [] + min_axis = 0 + for i in range(1, axis.shape[0]): + if axis[i] != axis[i - 1] + 1: + region = axis[min_axis:i] + min_axis = i + regions.append(region) + return regions + + def random_select(self, axis, max_size): + xx = np.random.choice(axis, size=2) + xmin = np.min(xx) + xmax = np.max(xx) + xmin = np.clip(xmin, 0, max_size - 1) + xmax = np.clip(xmax, 0, max_size - 1) + return xmin, xmax + + def region_wise_random_select(self, regions, max_size): + selected_index = list(np.random.choice(len(regions), 2)) + selected_values = [] + for index in selected_index: + axis = regions[index] + xx = int(np.random.choice(axis, size=1)) + selected_values.append(xx) + xmin = min(selected_values) + xmax = max(selected_values) + return xmin, xmax + + def crop_area(self, im, text_polys): + h, w = im.shape[:2] + h_array = np.zeros(h, dtype=np.int32) + w_array = np.zeros(w, dtype=np.int32) + for points in text_polys: + points = np.round(points, decimals=0).astype(np.int32) + minx = np.min(points[:, 0]) + maxx = np.max(points[:, 0]) + w_array[minx:maxx] = 1 + miny = np.min(points[:, 1]) + maxy = np.max(points[:, 1]) + h_array[miny:maxy] = 1 + # ensure the cropped area not across a text + h_axis = np.where(h_array == 0)[0] + w_axis = np.where(w_array == 0)[0] + + if len(h_axis) == 0 or len(w_axis) == 0: + return 0, 0, w, h + + h_regions = self.split_regions(h_axis) + w_regions = self.split_regions(w_axis) + + for i in range(self.max_tries): + if len(w_regions) > 1: + xmin, xmax = self.region_wise_random_select(w_regions, w) + else: + xmin, xmax = self.random_select(w_axis, w) + if len(h_regions) > 1: + ymin, ymax = self.region_wise_random_select(h_regions, h) + else: + ymin, ymax = self.random_select(h_axis, h) + + if xmax - xmin < self.min_crop_side_ratio * w or ymax - ymin < self.min_crop_side_ratio * h: + # area too small + continue + num_poly_in_rect = 0 + for poly in text_polys: + if not self.is_poly_outside_rect(poly, xmin, ymin, xmax - xmin, + ymax - ymin): + num_poly_in_rect += 1 + break + + if num_poly_in_rect > 0: + return xmin, ymin, xmax - xmin, ymax - ymin + + return 0, 0, w, h + + +class PSERandomCrop(): + def __init__(self, size): + self.size = size + + def __call__(self, data): + imgs = data['imgs'] + + h, w = imgs[0].shape[0:2] + th, tw = self.size + if w == tw and h == th: + return imgs + + # label中存在文本实例,并且按照概率进行裁剪,使用threshold_label_map控制 + if np.max(imgs[2]) > 0 and random.random() > 3 / 8: + # 文本实例的左上角点 + tl = np.min(np.where(imgs[2] > 0), axis=1) - self.size + tl[tl < 0] = 0 + # 文本实例的右下角点 + br = np.max(np.where(imgs[2] > 0), axis=1) - self.size + br[br < 0] = 0 + # 保证选到右下角点时,有足够的距离进行crop + br[0] = min(br[0], h - th) + br[1] = min(br[1], w - tw) + + for _ in range(50000): + i = random.randint(tl[0], br[0]) + j = random.randint(tl[1], br[1]) + # 保证shrink_label_map有文本 + if imgs[1][i:i + th, j:j + tw].sum() <= 0: + continue + else: + break + else: + i = random.randint(0, h - th) + j = random.randint(0, w - tw) + + # return i, j, th, tw + for idx in range(len(imgs)): + if len(imgs[idx].shape) == 3: + imgs[idx] = imgs[idx][i:i + th, j:j + tw, :] + else: + imgs[idx] = imgs[idx][i:i + th, j:j + tw] + data['imgs'] = imgs + return data diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/environment.yml b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/environment.yml new file mode 100644 index 0000000000000000000000000000000000000000..571dbf2a0462a842e17420cbd2bdba2b56b62131 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/environment.yml @@ -0,0 +1,21 @@ +name: dbnet +channels: + - conda-forge + - defaults +dependencies: + - anyconfig==0.9.10 + - future==0.18.2 + - imgaug==0.4.0 + - matplotlib==3.1.2 + - numpy==1.17.4 + - opencv + - pyclipper + - PyYAML==5.2 + - scikit-image==0.16.2 + - Shapely==1.6.4 + - tensorboard=2 + - tqdm==4.40.1 + - ipython + - pip + - pip: + - polygon3 diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/eval.sh b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/eval.sh new file mode 100644 index 0000000000000000000000000000000000000000..b3bf46818610caedca6690a95713a020b678e2bf --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/eval.sh @@ -0,0 +1 @@ +CUDA_VISIBLE_DEVICES=0 python3 tools/eval.py --model_path '' \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/generate_lists.sh b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/generate_lists.sh new file mode 100644 index 0000000000000000000000000000000000000000..84f408c64330f0aefb684ed738fa2dcdbc4af6da --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/generate_lists.sh @@ -0,0 +1,17 @@ +#Only use if your file names of the images and txts are identical +rm ./datasets/train_img.txt +rm ./datasets/train_gt.txt +rm ./datasets/test_img.txt +rm ./datasets/test_gt.txt +rm ./datasets/train.txt +rm ./datasets/test.txt +ls ./datasets/train/img/*.jpg > ./datasets/train_img.txt +ls ./datasets/train/gt/*.txt > ./datasets/train_gt.txt +ls ./datasets/test/img/*.jpg > ./datasets/test_img.txt +ls ./datasets/test/gt/*.txt > ./datasets/test_gt.txt +paste ./datasets/train_img.txt ./datasets/train_gt.txt > ./datasets/train.txt +paste ./datasets/test_img.txt ./datasets/test_gt.txt > ./datasets/test.txt +rm ./datasets/train_img.txt +rm ./datasets/train_gt.txt +rm ./datasets/test_img.txt +rm ./datasets/test_gt.txt diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/imgs/paper/db.jpg b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/imgs/paper/db.jpg new file mode 100644 index 0000000000000000000000000000000000000000..86c9b6be18bcd35d20f35122d4e191cc5938692a --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/imgs/paper/db.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70e7c3667907d08caa79d79cc15addd2975b044eebdb79019d3d6eb76f5a3051 +size 194472 diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..26ff73ff690bf56c8d5cfb64a37bbffb706da7e2 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/23 21:55 +# @Author : zhoujun +import copy +from .model import Model +from .losses import build_loss + +__all__ = ['build_loss', 'build_model'] +support_model = ['Model'] + + +def build_model(config): + """ + get architecture model class + """ + copy_config = copy.deepcopy(config) + arch_type = copy_config.pop('type') + assert arch_type in support_model, f'{arch_type} is not developed yet!, only {support_model} are support now' + arch_model = eval(arch_type)(copy_config) + return arch_model diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/backbone/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/backbone/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..740c8d5ff09311def6ca465aae40c34261518102 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/backbone/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/23 21:54 +# @Author : zhoujun + +from .resnet import * + +__all__ = ['build_backbone'] + +support_backbone = [ + 'resnet18', 'deformable_resnet18', 'deformable_resnet50', 'resnet50', + 'resnet34', 'resnet101', 'resnet152' +] + + +def build_backbone(backbone_name, **kwargs): + assert backbone_name in support_backbone, f'all support backbone is {support_backbone}' + backbone = eval(backbone_name)(**kwargs) + return backbone diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/backbone/resnet.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/backbone/resnet.py new file mode 100644 index 0000000000000000000000000000000000000000..9b30b382d98fec00d396dabc4f12f20ad8c77389 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/backbone/resnet.py @@ -0,0 +1,375 @@ +import math +import paddle +from paddle import nn + +BatchNorm2d = nn.BatchNorm2D + +__all__ = [ + 'ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101', + 'deformable_resnet18', 'deformable_resnet50', 'resnet152' +] + +model_urls = { + 'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth', + 'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth', + 'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth', + 'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth', + 'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth', +} + + +def constant_init(module, constant, bias=0): + module.weight = paddle.create_parameter( + shape=module.weight.shape, + dtype='float32', + default_initializer=paddle.nn.initializer.Constant(constant)) + if hasattr(module, 'bias'): + module.bias = paddle.create_parameter( + shape=module.bias.shape, + dtype='float32', + default_initializer=paddle.nn.initializer.Constant(bias)) + + +def conv3x3(in_planes, out_planes, stride=1): + """3x3 convolution with padding""" + return nn.Conv2D( + in_planes, + out_planes, + kernel_size=3, + stride=stride, + padding=1, + bias_attr=False) + + +class BasicBlock(nn.Layer): + expansion = 1 + + def __init__(self, inplanes, planes, stride=1, downsample=None, dcn=None): + super(BasicBlock, self).__init__() + self.with_dcn = dcn is not None + self.conv1 = conv3x3(inplanes, planes, stride) + self.bn1 = BatchNorm2d(planes, momentum=0.1) + self.relu = nn.ReLU() + self.with_modulated_dcn = False + if not self.with_dcn: + self.conv2 = nn.Conv2D( + planes, planes, kernel_size=3, padding=1, bias_attr=False) + else: + from paddle.version.ops import DeformConv2D + deformable_groups = dcn.get('deformable_groups', 1) + offset_channels = 18 + self.conv2_offset = nn.Conv2D( + planes, + deformable_groups * offset_channels, + kernel_size=3, + padding=1) + self.conv2 = DeformConv2D( + planes, planes, kernel_size=3, padding=1, bias_attr=False) + self.bn2 = BatchNorm2d(planes, momentum=0.1) + self.downsample = downsample + self.stride = stride + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + # out = self.conv2(out) + if not self.with_dcn: + out = self.conv2(out) + else: + offset = self.conv2_offset(out) + out = self.conv2(out, offset) + out = self.bn2(out) + + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class Bottleneck(nn.Layer): + expansion = 4 + + def __init__(self, inplanes, planes, stride=1, downsample=None, dcn=None): + super(Bottleneck, self).__init__() + self.with_dcn = dcn is not None + self.conv1 = nn.Conv2D(inplanes, planes, kernel_size=1, bias_attr=False) + self.bn1 = BatchNorm2d(planes, momentum=0.1) + self.with_modulated_dcn = False + if not self.with_dcn: + self.conv2 = nn.Conv2D( + planes, + planes, + kernel_size=3, + stride=stride, + padding=1, + bias_attr=False) + else: + deformable_groups = dcn.get('deformable_groups', 1) + from paddle.vision.ops import DeformConv2D + offset_channels = 18 + self.conv2_offset = nn.Conv2D( + planes, + deformable_groups * offset_channels, + stride=stride, + kernel_size=3, + padding=1) + self.conv2 = DeformConv2D( + planes, + planes, + kernel_size=3, + padding=1, + stride=stride, + bias_attr=False) + self.bn2 = BatchNorm2d(planes, momentum=0.1) + self.conv3 = nn.Conv2D( + planes, planes * 4, kernel_size=1, bias_attr=False) + self.bn3 = BatchNorm2d(planes * 4, momentum=0.1) + self.relu = nn.ReLU() + self.downsample = downsample + self.stride = stride + self.dcn = dcn + self.with_dcn = dcn is not None + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + # out = self.conv2(out) + if not self.with_dcn: + out = self.conv2(out) + else: + offset = self.conv2_offset(out) + out = self.conv2(out, offset) + out = self.bn2(out) + out = self.relu(out) + + out = self.conv3(out) + out = self.bn3(out) + + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class ResNet(nn.Layer): + def __init__(self, block, layers, in_channels=3, dcn=None): + self.dcn = dcn + self.inplanes = 64 + super(ResNet, self).__init__() + self.out_channels = [] + self.conv1 = nn.Conv2D( + in_channels, + 64, + kernel_size=7, + stride=2, + padding=3, + bias_attr=False) + self.bn1 = BatchNorm2d(64, momentum=0.1) + self.relu = nn.ReLU() + self.maxpool = nn.MaxPool2D(kernel_size=3, stride=2, padding=1) + self.layer1 = self._make_layer(block, 64, layers[0]) + self.layer2 = self._make_layer(block, 128, layers[1], stride=2, dcn=dcn) + self.layer3 = self._make_layer(block, 256, layers[2], stride=2, dcn=dcn) + self.layer4 = self._make_layer(block, 512, layers[3], stride=2, dcn=dcn) + + if self.dcn is not None: + for m in self.modules(): + if isinstance(m, Bottleneck) or isinstance(m, BasicBlock): + if hasattr(m, 'conv2_offset'): + constant_init(m.conv2_offset, 0) + + def _make_layer(self, block, planes, blocks, stride=1, dcn=None): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2D( + self.inplanes, + planes * block.expansion, + kernel_size=1, + stride=stride, + bias_attr=False), + BatchNorm2d( + planes * block.expansion, momentum=0.1), ) + + layers = [] + layers.append(block(self.inplanes, planes, stride, downsample, dcn=dcn)) + self.inplanes = planes * block.expansion + for i in range(1, blocks): + layers.append(block(self.inplanes, planes, dcn=dcn)) + self.out_channels.append(planes * block.expansion) + return nn.Sequential(*layers) + + def forward(self, x): + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + x = self.maxpool(x) + + x2 = self.layer1(x) + x3 = self.layer2(x2) + x4 = self.layer3(x3) + x5 = self.layer4(x4) + + return x2, x3, x4, x5 + + +def load_torch_params(paddle_model, torch_patams): + paddle_params = paddle_model.state_dict() + + fc_names = ['classifier'] + for key, torch_value in torch_patams.items(): + if 'num_batches_tracked' in key: + continue + key = key.replace("running_var", "_variance").replace( + "running_mean", "_mean").replace("module.", "") + torch_value = torch_value.detach().cpu().numpy() + if key in paddle_params: + flag = [i in key for i in fc_names] + if any(flag) and "weight" in key: # ignore bias + new_shape = [1, 0] + list(range(2, torch_value.ndim)) + print( + f"name: {key}, ori shape: {torch_value.shape}, new shape: {torch_value.transpose(new_shape).shape}" + ) + torch_value = torch_value.transpose(new_shape) + paddle_params[key] = torch_value + else: + print(f'{key} not in paddle') + paddle_model.set_state_dict(paddle_params) + + +def load_models(model, model_name): + import torch.utils.model_zoo as model_zoo + torch_patams = model_zoo.load_url(model_urls[model_name]) + load_torch_params(model, torch_patams) + + +def resnet18(pretrained=True, **kwargs): + """Constructs a ResNet-18 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs) + if pretrained: + assert kwargs.get( + 'in_channels', + 3) == 3, 'in_channels must be 3 whem pretrained is True' + print('load from imagenet') + load_models(model, 'resnet18') + return model + + +def deformable_resnet18(pretrained=True, **kwargs): + """Constructs a ResNet-18 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet( + BasicBlock, [2, 2, 2, 2], dcn=dict(deformable_groups=1), **kwargs) + if pretrained: + assert kwargs.get( + 'in_channels', + 3) == 3, 'in_channels must be 3 whem pretrained is True' + print('load from imagenet') + model.load_state_dict( + model_zoo.load_url(model_urls['resnet18']), strict=False) + return model + + +def resnet34(pretrained=True, **kwargs): + """Constructs a ResNet-34 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet(BasicBlock, [3, 4, 6, 3], **kwargs) + if pretrained: + assert kwargs.get( + 'in_channels', + 3) == 3, 'in_channels must be 3 whem pretrained is True' + model.load_state_dict( + model_zoo.load_url(model_urls['resnet34']), strict=False) + return model + + +def resnet50(pretrained=True, **kwargs): + """Constructs a ResNet-50 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet(Bottleneck, [3, 4, 6, 3], **kwargs) + if pretrained: + assert kwargs.get( + 'in_channels', + 3) == 3, 'in_channels must be 3 whem pretrained is True' + load_models(model, 'resnet50') + return model + + +def deformable_resnet50(pretrained=True, **kwargs): + """Constructs a ResNet-50 model with deformable conv. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet( + Bottleneck, [3, 4, 6, 3], dcn=dict(deformable_groups=1), **kwargs) + if pretrained: + assert kwargs.get( + 'in_channels', + 3) == 3, 'in_channels must be 3 whem pretrained is True' + model.load_state_dict( + model_zoo.load_url(model_urls['resnet50']), strict=False) + return model + + +def resnet101(pretrained=True, **kwargs): + """Constructs a ResNet-101 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet(Bottleneck, [3, 4, 23, 3], **kwargs) + if pretrained: + assert kwargs.get( + 'in_channels', + 3) == 3, 'in_channels must be 3 whem pretrained is True' + model.load_state_dict( + model_zoo.load_url(model_urls['resnet101']), strict=False) + return model + + +def resnet152(pretrained=True, **kwargs): + """Constructs a ResNet-152 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet(Bottleneck, [3, 8, 36, 3], **kwargs) + if pretrained: + assert kwargs.get( + 'in_channels', + 3) == 3, 'in_channels must be 3 whem pretrained is True' + model.load_state_dict( + model_zoo.load_url(model_urls['resnet152']), strict=False) + return model + + +if __name__ == '__main__': + + x = paddle.zeros([2, 3, 640, 640]) + net = resnet50(pretrained=True) + y = net(x) + for u in y: + print(u.shape) + + print(net.out_channels) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/basic.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/basic.py new file mode 100644 index 0000000000000000000000000000000000000000..f661878df7e1a7a9cbadc2f58d532c3af4949589 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/basic.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/12/6 11:19 +# @Author : zhoujun +from paddle import nn + + +class ConvBnRelu(nn.Layer): + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + padding=0, + dilation=1, + groups=1, + bias=True, + padding_mode='zeros', + inplace=True): + super().__init__() + self.conv = nn.Conv2D( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + groups=groups, + bias_attr=bias, + padding_mode=padding_mode) + self.bn = nn.BatchNorm2D(out_channels) + self.relu = nn.ReLU() + + def forward(self, x): + x = self.conv(x) + x = self.bn(x) + x = self.relu(x) + return x diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/head/DBHead.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/head/DBHead.py new file mode 100644 index 0000000000000000000000000000000000000000..29277cec9d3eb60929e2bda2e78d08856008f0b4 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/head/DBHead.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/12/4 14:54 +# @Author : zhoujun +import paddle +from paddle import nn, ParamAttr + + +class DBHead(nn.Layer): + def __init__(self, in_channels, out_channels, k=50): + super().__init__() + self.k = k + self.binarize = nn.Sequential( + nn.Conv2D( + in_channels, + in_channels // 4, + 3, + padding=1, + weight_attr=ParamAttr( + initializer=nn.initializer.KaimingNormal())), + nn.BatchNorm2D( + in_channels // 4, + weight_attr=ParamAttr(initializer=nn.initializer.Constant(1)), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(1e-4))), + nn.ReLU(), + nn.Conv2DTranspose( + in_channels // 4, + in_channels // 4, + 2, + 2, + weight_attr=ParamAttr( + initializer=nn.initializer.KaimingNormal())), + nn.BatchNorm2D( + in_channels // 4, + weight_attr=ParamAttr(initializer=nn.initializer.Constant(1)), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(1e-4))), + nn.ReLU(), + nn.Conv2DTranspose( + in_channels // 4, + 1, + 2, + 2, + weight_attr=nn.initializer.KaimingNormal()), + nn.Sigmoid()) + + self.thresh = self._init_thresh(in_channels) + + def forward(self, x): + shrink_maps = self.binarize(x) + threshold_maps = self.thresh(x) + if self.training: + binary_maps = self.step_function(shrink_maps, threshold_maps) + y = paddle.concat( + (shrink_maps, threshold_maps, binary_maps), axis=1) + else: + y = paddle.concat((shrink_maps, threshold_maps), axis=1) + return y + + def _init_thresh(self, + inner_channels, + serial=False, + smooth=False, + bias=False): + in_channels = inner_channels + if serial: + in_channels += 1 + self.thresh = nn.Sequential( + nn.Conv2D( + in_channels, + inner_channels // 4, + 3, + padding=1, + bias_attr=bias, + weight_attr=ParamAttr( + initializer=nn.initializer.KaimingNormal())), + nn.BatchNorm2D( + inner_channels // 4, + weight_attr=ParamAttr(initializer=nn.initializer.Constant(1)), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(1e-4))), + nn.ReLU(), + self._init_upsample( + inner_channels // 4, + inner_channels // 4, + smooth=smooth, + bias=bias), + nn.BatchNorm2D( + inner_channels // 4, + weight_attr=ParamAttr(initializer=nn.initializer.Constant(1)), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(1e-4))), + nn.ReLU(), + self._init_upsample( + inner_channels // 4, 1, smooth=smooth, bias=bias), + nn.Sigmoid()) + return self.thresh + + def _init_upsample(self, + in_channels, + out_channels, + smooth=False, + bias=False): + if smooth: + inter_out_channels = out_channels + if out_channels == 1: + inter_out_channels = in_channels + module_list = [ + nn.Upsample( + scale_factor=2, mode='nearest'), nn.Conv2D( + in_channels, + inter_out_channels, + 3, + 1, + 1, + bias_attr=bias, + weight_attr=ParamAttr( + initializer=nn.initializer.KaimingNormal())) + ] + if out_channels == 1: + module_list.append( + nn.Conv2D( + in_channels, + out_channels, + kernel_size=1, + stride=1, + padding=1, + bias_attr=True, + weight_attr=ParamAttr( + initializer=nn.initializer.KaimingNormal()))) + return nn.Sequential(module_list) + else: + return nn.Conv2DTranspose( + in_channels, + out_channels, + 2, + 2, + weight_attr=ParamAttr( + initializer=nn.initializer.KaimingNormal())) + + def step_function(self, x, y): + return paddle.reciprocal(1 + paddle.exp(-self.k * (x - y))) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/head/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/head/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5610c69754ebd1d8d7aa1b69773e91672481e418 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/head/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# @Time : 2020/6/5 11:35 +# @Author : zhoujun +from .DBHead import DBHead + +__all__ = ['build_head'] +support_head = ['DBHead'] + + +def build_head(head_name, **kwargs): + assert head_name in support_head, f'all support head is {support_head}' + head = eval(head_name)(**kwargs) + return head \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/losses/DB_loss.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/losses/DB_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..74d240c17b11ae784203a3916b06925a0a2f3af6 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/losses/DB_loss.py @@ -0,0 +1,49 @@ +import paddle +from models.losses.basic_loss import BalanceCrossEntropyLoss, MaskL1Loss, DiceLoss + + +class DBLoss(paddle.nn.Layer): + def __init__(self, + alpha=1.0, + beta=10, + ohem_ratio=3, + reduction='mean', + eps=1e-06): + """ + Implement PSE Loss. + :param alpha: binary_map loss 前面的系数 + :param beta: threshold_map loss 前面的系数 + :param ohem_ratio: OHEM的比例 + :param reduction: 'mean' or 'sum'对 batch里的loss 算均值或求和 + """ + super().__init__() + assert reduction in ['mean', 'sum'], " reduction must in ['mean','sum']" + self.alpha = alpha + self.beta = beta + self.bce_loss = BalanceCrossEntropyLoss(negative_ratio=ohem_ratio) + self.dice_loss = DiceLoss(eps=eps) + self.l1_loss = MaskL1Loss(eps=eps) + self.ohem_ratio = ohem_ratio + self.reduction = reduction + + def forward(self, pred, batch): + shrink_maps = pred[:, 0, :, :] + threshold_maps = pred[:, 1, :, :] + binary_maps = pred[:, 2, :, :] + loss_shrink_maps = self.bce_loss(shrink_maps, batch['shrink_map'], + batch['shrink_mask']) + loss_threshold_maps = self.l1_loss( + threshold_maps, batch['threshold_map'], batch['threshold_mask']) + metrics = dict( + loss_shrink_maps=loss_shrink_maps, + loss_threshold_maps=loss_threshold_maps) + if pred.shape[1] > 2: + loss_binary_maps = self.dice_loss(binary_maps, batch['shrink_map'], + batch['shrink_mask']) + metrics['loss_binary_maps'] = loss_binary_maps + loss_all = (self.alpha * loss_shrink_maps + self.beta * + loss_threshold_maps + loss_binary_maps) + metrics['loss'] = loss_all + else: + metrics['loss'] = loss_shrink_maps + return metrics diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/losses/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/losses/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9dc0f1033b66999bb9cc7edb3c39d80836963c56 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/losses/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# @Time : 2020/6/5 11:36 +# @Author : zhoujun +import copy +from .DB_loss import DBLoss + +__all__ = ['build_loss'] +support_loss = ['DBLoss'] + + +def build_loss(config): + copy_config = copy.deepcopy(config) + loss_type = copy_config.pop('type') + assert loss_type in support_loss, f'all support loss is {support_loss}' + criterion = eval(loss_type)(**copy_config) + return criterion diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/losses/basic_loss.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/losses/basic_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..8e68cb172aea81afc5bd6c5d75dedd6841c59456 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/losses/basic_loss.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/12/4 14:39 +# @Author : zhoujun +import paddle +import paddle.nn as nn + + +class BalanceCrossEntropyLoss(nn.Layer): + ''' + Balanced cross entropy loss. + Shape: + - Input: :math:`(N, 1, H, W)` + - GT: :math:`(N, 1, H, W)`, same shape as the input + - Mask: :math:`(N, H, W)`, same spatial shape as the input + - Output: scalar. + + ''' + + def __init__(self, negative_ratio=3.0, eps=1e-6): + super(BalanceCrossEntropyLoss, self).__init__() + self.negative_ratio = negative_ratio + self.eps = eps + + def forward(self, + pred: paddle.Tensor, + gt: paddle.Tensor, + mask: paddle.Tensor, + return_origin=False): + ''' + Args: + pred: shape :math:`(N, 1, H, W)`, the prediction of network + gt: shape :math:`(N, 1, H, W)`, the target + mask: shape :math:`(N, H, W)`, the mask indicates positive regions + ''' + positive = (gt * mask) + negative = ((1 - gt) * mask) + positive_count = int(positive.sum()) + negative_count = min( + int(negative.sum()), int(positive_count * self.negative_ratio)) + loss = nn.functional.binary_cross_entropy(pred, gt, reduction='none') + positive_loss = loss * positive + negative_loss = loss * negative + negative_loss, _ = negative_loss.reshape([-1]).topk(negative_count) + + balance_loss = (positive_loss.sum() + negative_loss.sum()) / ( + positive_count + negative_count + self.eps) + + if return_origin: + return balance_loss, loss + return balance_loss + + +class DiceLoss(nn.Layer): + ''' + Loss function from https://arxiv.org/abs/1707.03237, + where iou computation is introduced heatmap manner to measure the + diversity bwtween tow heatmaps. + ''' + + def __init__(self, eps=1e-6): + super(DiceLoss, self).__init__() + self.eps = eps + + def forward(self, pred: paddle.Tensor, gt, mask, weights=None): + ''' + pred: one or two heatmaps of shape (N, 1, H, W), + the losses of tow heatmaps are added together. + gt: (N, 1, H, W) + mask: (N, H, W) + ''' + return self._compute(pred, gt, mask, weights) + + def _compute(self, pred, gt, mask, weights): + if len(pred.shape) == 4: + pred = pred[:, 0, :, :] + gt = gt[:, 0, :, :] + assert pred.shape == gt.shape + assert pred.shape == mask.shape + if weights is not None: + assert weights.shape == mask.shape + mask = weights * mask + intersection = (pred * gt * mask).sum() + + union = (pred * mask).sum() + (gt * mask).sum() + self.eps + loss = 1 - 2.0 * intersection / union + assert loss <= 1 + return loss + + +class MaskL1Loss(nn.Layer): + def __init__(self, eps=1e-6): + super(MaskL1Loss, self).__init__() + self.eps = eps + + def forward(self, pred: paddle.Tensor, gt, mask): + loss = (paddle.abs(pred - gt) * mask).sum() / (mask.sum() + self.eps) + return loss diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/model.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/model.py new file mode 100644 index 0000000000000000000000000000000000000000..ee24ff5b3d6b5a6c30b64de56a0aa83e9960001e --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/model.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/23 21:57 +# @Author : zhoujun +from addict import Dict +from paddle import nn +import paddle.nn.functional as F + +from models.backbone import build_backbone +from models.neck import build_neck +from models.head import build_head + + +class Model(nn.Layer): + def __init__(self, model_config: dict): + """ + PANnet + :param model_config: 模型配置 + """ + super().__init__() + model_config = Dict(model_config) + backbone_type = model_config.backbone.pop('type') + neck_type = model_config.neck.pop('type') + head_type = model_config.head.pop('type') + self.backbone = build_backbone(backbone_type, **model_config.backbone) + self.neck = build_neck( + neck_type, + in_channels=self.backbone.out_channels, + **model_config.neck) + self.head = build_head( + head_type, in_channels=self.neck.out_channels, **model_config.head) + self.name = f'{backbone_type}_{neck_type}_{head_type}' + + def forward(self, x): + _, _, H, W = x.shape + backbone_out = self.backbone(x) + neck_out = self.neck(backbone_out) + y = self.head(neck_out) + y = F.interpolate(y, size=(H, W), mode='bilinear', align_corners=True) + return y diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/neck/FPN.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/neck/FPN.py new file mode 100644 index 0000000000000000000000000000000000000000..53a3fa4b80c556ba87fa559494771f72acae0788 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/neck/FPN.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/9/13 10:29 +# @Author : zhoujun +import paddle +import paddle.nn.functional as F +from paddle import nn + +from models.basic import ConvBnRelu + + +class FPN(nn.Layer): + def __init__(self, in_channels, inner_channels=256, **kwargs): + """ + :param in_channels: 基础网络输出的维度 + :param kwargs: + """ + super().__init__() + inplace = True + self.conv_out = inner_channels + inner_channels = inner_channels // 4 + # reduce layers + self.reduce_conv_c2 = ConvBnRelu( + in_channels[0], inner_channels, kernel_size=1, inplace=inplace) + self.reduce_conv_c3 = ConvBnRelu( + in_channels[1], inner_channels, kernel_size=1, inplace=inplace) + self.reduce_conv_c4 = ConvBnRelu( + in_channels[2], inner_channels, kernel_size=1, inplace=inplace) + self.reduce_conv_c5 = ConvBnRelu( + in_channels[3], inner_channels, kernel_size=1, inplace=inplace) + # Smooth layers + self.smooth_p4 = ConvBnRelu( + inner_channels, + inner_channels, + kernel_size=3, + padding=1, + inplace=inplace) + self.smooth_p3 = ConvBnRelu( + inner_channels, + inner_channels, + kernel_size=3, + padding=1, + inplace=inplace) + self.smooth_p2 = ConvBnRelu( + inner_channels, + inner_channels, + kernel_size=3, + padding=1, + inplace=inplace) + + self.conv = nn.Sequential( + nn.Conv2D( + self.conv_out, + self.conv_out, + kernel_size=3, + padding=1, + stride=1), + nn.BatchNorm2D(self.conv_out), + nn.ReLU()) + self.out_channels = self.conv_out + + def forward(self, x): + c2, c3, c4, c5 = x + # Top-down + p5 = self.reduce_conv_c5(c5) + p4 = self._upsample_add(p5, self.reduce_conv_c4(c4)) + p4 = self.smooth_p4(p4) + p3 = self._upsample_add(p4, self.reduce_conv_c3(c3)) + p3 = self.smooth_p3(p3) + p2 = self._upsample_add(p3, self.reduce_conv_c2(c2)) + p2 = self.smooth_p2(p2) + + x = self._upsample_cat(p2, p3, p4, p5) + x = self.conv(x) + return x + + def _upsample_add(self, x, y): + return F.interpolate(x, size=y.shape[2:]) + y + + def _upsample_cat(self, p2, p3, p4, p5): + h, w = p2.shape[2:] + p3 = F.interpolate(p3, size=(h, w)) + p4 = F.interpolate(p4, size=(h, w)) + p5 = F.interpolate(p5, size=(h, w)) + return paddle.concat([p2, p3, p4, p5], axis=1) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/neck/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/neck/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..76553413784df2d5d87824e71d8de9b95ce7ce7e --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/models/neck/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# @Time : 2020/6/5 11:34 +# @Author : zhoujun +from .FPN import FPN + +__all__ = ['build_neck'] +support_neck = ['FPN'] + + +def build_neck(neck_name, **kwargs): + assert neck_name in support_neck, f'all support neck is {support_neck}' + neck = eval(neck_name)(**kwargs) + return neck diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/multi_gpu_train.sh b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/multi_gpu_train.sh new file mode 100644 index 0000000000000000000000000000000000000000..b49a73f1581d8b40c20aeb01fd6ccc37fd6ab24b --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/multi_gpu_train.sh @@ -0,0 +1,2 @@ +# export NCCL_P2P_DISABLE=1 +CUDA_VISIBLE_DEVICES=0,1,2,3 python3 -m paddle.distributed.launch tools/train.py --config_file "config/icdar2015_resnet50_FPN_DBhead_polyLR.yaml" \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/post_processing/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/post_processing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2f8e43223dfaf509c1bec58e828dd98a4630eb90 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/post_processing/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/12/5 15:17 +# @Author : zhoujun + +from .seg_detector_representer import SegDetectorRepresenter + + +def get_post_processing(config): + try: + cls = eval(config['type'])(**config['args']) + return cls + except: + return None \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/post_processing/seg_detector_representer.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/post_processing/seg_detector_representer.py new file mode 100644 index 0000000000000000000000000000000000000000..f1273dcfcce0a73cc76f841354aed2623219a8d8 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/post_processing/seg_detector_representer.py @@ -0,0 +1,192 @@ +import cv2 +import numpy as np +import pyclipper +import paddle +from shapely.geometry import Polygon + + +class SegDetectorRepresenter(): + def __init__(self, + thresh=0.3, + box_thresh=0.7, + max_candidates=1000, + unclip_ratio=1.5): + self.min_size = 3 + self.thresh = thresh + self.box_thresh = box_thresh + self.max_candidates = max_candidates + self.unclip_ratio = unclip_ratio + + def __call__(self, batch, pred, is_output_polygon=False): + ''' + batch: (image, polygons, ignore_tags + batch: a dict produced by dataloaders. + image: tensor of shape (N, C, H, W). + polygons: tensor of shape (N, K, 4, 2), the polygons of objective regions. + ignore_tags: tensor of shape (N, K), indicates whether a region is ignorable or not. + shape: the original shape of images. + filename: the original filenames of images. + pred: + binary: text region segmentation map, with shape (N, H, W) + thresh: [if exists] thresh hold prediction with shape (N, H, W) + thresh_binary: [if exists] binarized with threshhold, (N, H, W) + ''' + if isinstance(pred, paddle.Tensor): + pred = pred.numpy() + pred = pred[:, 0, :, :] + segmentation = self.binarize(pred) + boxes_batch = [] + scores_batch = [] + for batch_index in range(pred.shape[0]): + height, width = batch['shape'][batch_index] + if is_output_polygon: + boxes, scores = self.polygons_from_bitmap( + pred[batch_index], segmentation[batch_index], width, height) + else: + boxes, scores = self.boxes_from_bitmap( + pred[batch_index], segmentation[batch_index], width, height) + boxes_batch.append(boxes) + scores_batch.append(scores) + return boxes_batch, scores_batch + + def binarize(self, pred): + return pred > self.thresh + + def polygons_from_bitmap(self, pred, _bitmap, dest_width, dest_height): + ''' + _bitmap: single map with shape (H, W), + whose values are binarized as {0, 1} + ''' + + assert len(_bitmap.shape) == 2 + bitmap = _bitmap # The first channel + height, width = bitmap.shape + boxes = [] + scores = [] + + contours, _ = cv2.findContours((bitmap * 255).astype(np.uint8), + cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) + + for contour in contours[:self.max_candidates]: + epsilon = 0.005 * cv2.arcLength(contour, True) + approx = cv2.approxPolyDP(contour, epsilon, True) + points = approx.reshape((-1, 2)) + if points.shape[0] < 4: + continue + # _, sside = self.get_mini_boxes(contour) + # if sside < self.min_size: + # continue + score = self.box_score_fast(pred, contour.squeeze(1)) + if self.box_thresh > score: + continue + + if points.shape[0] > 2: + box = self.unclip(points, unclip_ratio=self.unclip_ratio) + if len(box) > 1: + continue + else: + continue + box = box.reshape(-1, 2) + _, sside = self.get_mini_boxes(box.reshape((-1, 1, 2))) + if sside < self.min_size + 2: + continue + + if not isinstance(dest_width, int): + dest_width = dest_width.item() + dest_height = dest_height.item() + + box[:, 0] = np.clip( + np.round(box[:, 0] / width * dest_width), 0, dest_width) + box[:, 1] = np.clip( + np.round(box[:, 1] / height * dest_height), 0, dest_height) + boxes.append(box) + scores.append(score) + return boxes, scores + + def boxes_from_bitmap(self, pred, _bitmap, dest_width, dest_height): + ''' + _bitmap: single map with shape (H, W), + whose values are binarized as {0, 1} + ''' + + assert len(_bitmap.shape) == 2 + bitmap = _bitmap # The first channel + height, width = bitmap.shape + contours, _ = cv2.findContours((bitmap * 255).astype(np.uint8), + cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) + num_contours = min(len(contours), self.max_candidates) + boxes = np.zeros((num_contours, 4, 2), dtype=np.int16) + scores = np.zeros((num_contours, ), dtype=np.float32) + + for index in range(num_contours): + contour = contours[index].squeeze(1) + points, sside = self.get_mini_boxes(contour) + if sside < self.min_size: + continue + points = np.array(points) + score = self.box_score_fast(pred, contour) + if self.box_thresh > score: + continue + + box = self.unclip( + points, unclip_ratio=self.unclip_ratio).reshape(-1, 1, 2) + box, sside = self.get_mini_boxes(box) + if sside < self.min_size + 2: + continue + box = np.array(box) + if not isinstance(dest_width, int): + dest_width = dest_width.item() + dest_height = dest_height.item() + + box[:, 0] = np.clip( + np.round(box[:, 0] / width * dest_width), 0, dest_width) + box[:, 1] = np.clip( + np.round(box[:, 1] / height * dest_height), 0, dest_height) + boxes[index, :, :] = box.astype(np.int16) + scores[index] = score + return boxes, scores + + def unclip(self, box, unclip_ratio=1.5): + poly = Polygon(box) + distance = poly.area * unclip_ratio / poly.length + offset = pyclipper.PyclipperOffset() + offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) + expanded = np.array(offset.Execute(distance)) + return expanded + + def get_mini_boxes(self, contour): + bounding_box = cv2.minAreaRect(contour) + points = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0]) + + index_1, index_2, index_3, index_4 = 0, 1, 2, 3 + if points[1][1] > points[0][1]: + index_1 = 0 + index_4 = 1 + else: + index_1 = 1 + index_4 = 0 + if points[3][1] > points[2][1]: + index_2 = 2 + index_3 = 3 + else: + index_2 = 3 + index_3 = 2 + + box = [ + points[index_1], points[index_2], points[index_3], points[index_4] + ] + return box, min(bounding_box[1]) + + def box_score_fast(self, bitmap, _box): + h, w = bitmap.shape[:2] + box = _box.copy() + xmin = np.clip(np.floor(box[:, 0].min()).astype(np.int), 0, w - 1) + xmax = np.clip(np.ceil(box[:, 0].max()).astype(np.int), 0, w - 1) + ymin = np.clip(np.floor(box[:, 1].min()).astype(np.int), 0, h - 1) + ymax = np.clip(np.ceil(box[:, 1].max()).astype(np.int), 0, h - 1) + + mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8) + box[:, 0] = box[:, 0] - xmin + box[:, 1] = box[:, 1] - ymin + cv2.fillPoly(mask, box.reshape(1, -1, 2).astype(np.int32), 1) + return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0] diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/predict.sh b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/predict.sh new file mode 100644 index 0000000000000000000000000000000000000000..37ab148283024c2360b7b13df24e5cc5b5cdaa4f --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/predict.sh @@ -0,0 +1 @@ +CUDA_VISIBLE_DEVICES=0 python tools/predict.py --model_path model_best.pth --input_folder ./input --output_folder ./output --thre 0.7 --polygon --show --save_result \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/requirement.txt b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/requirement.txt new file mode 100644 index 0000000000000000000000000000000000000000..191819f32c8e5e6d318201f56a84b1faa5e3bce7 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/requirement.txt @@ -0,0 +1,13 @@ +anyconfig +future +imgaug +matplotlib +numpy +opencv-python +Polygon3 +pyclipper +PyYAML +scikit-image +Shapely +tqdm +addict \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/single_gpu_train.sh b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/single_gpu_train.sh new file mode 100644 index 0000000000000000000000000000000000000000..7d46cf0359a20b7579e1bf7747c4055e6d820a5c --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/single_gpu_train.sh @@ -0,0 +1 @@ +CUDA_VISIBLE_DEVICES=0 python3 tools/train.py --config_file "config/icdar2015_resnet50_FPN_DBhead_polyLR.yaml" diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/singlel_gpu_train.sh b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/singlel_gpu_train.sh new file mode 100644 index 0000000000000000000000000000000000000000..f8b9f0e89b25e4e00e129cdfb3c0a771846d8c56 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/singlel_gpu_train.sh @@ -0,0 +1 @@ +CUDA_VISIBLE_DEVICES=0 python3 tools/train.py --config_file "config/icdar2015_resnet50_FPN_DBhead_polyLR.yaml" \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test/README.MD b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test/README.MD new file mode 100644 index 0000000000000000000000000000000000000000..b43c6e9a1990ab290f07bca1187e4cdefd46ff1e --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test/README.MD @@ -0,0 +1,8 @@ +Place the images that you want to detect here. You better named them as such: +img_10.jpg +img_11.jpg +img_{img_id}.jpg + +For predicting single images, you can change the `img_path` in the `/tools/predict.py` to your image number. + +The result will be saved in the output_folder(default is test/output) you give in predict.sh \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/benchmark_train.sh b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/benchmark_train.sh new file mode 100644 index 0000000000000000000000000000000000000000..d94dac2f43bed428236b265e8c7336d98a03c980 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/benchmark_train.sh @@ -0,0 +1,287 @@ +#!/bin/bash +source test_tipc/common_func.sh + +# run benchmark sh +# Usage: +# bash run_benchmark_train.sh config.txt params +# or +# bash run_benchmark_train.sh config.txt + +function func_parser_params(){ + strs=$1 + IFS="=" + array=(${strs}) + tmp=${array[1]} + echo ${tmp} +} + +function set_dynamic_epoch(){ + string=$1 + num=$2 + _str=${string:1:6} + IFS="C" + arr=(${_str}) + M=${arr[0]} + P=${arr[1]} + ep=`expr $num \* $M \* $P` + echo $ep +} + +function func_sed_params(){ + filename=$1 + line=$2 + param_value=$3 + params=`sed -n "${line}p" $filename` + IFS=":" + array=(${params}) + key=${array[0]} + value=${array[1]} + + new_params="${key}:${param_value}" + IFS=";" + cmd="sed -i '${line}s/.*/${new_params}/' '${filename}'" + eval $cmd +} + +function set_gpu_id(){ + string=$1 + _str=${string:1:6} + IFS="C" + arr=(${_str}) + M=${arr[0]} + P=${arr[1]} + gn=`expr $P - 1` + gpu_num=`expr $gn / $M` + seq=`seq -s "," 0 $gpu_num` + echo $seq +} + +function get_repo_name(){ + IFS=";" + cur_dir=$(pwd) + IFS="/" + arr=(${cur_dir}) + echo ${arr[-1]} +} + +FILENAME=$1 +# copy FILENAME as new +new_filename="./test_tipc/benchmark_train.txt" +cmd=`yes|cp $FILENAME $new_filename` +FILENAME=$new_filename +# MODE must be one of ['benchmark_train'] +MODE=$2 +PARAMS=$3 + +to_static="" +# parse "to_static" options and modify trainer into "to_static_trainer" +if [[ $PARAMS =~ "dynamicTostatic" ]] ;then + to_static="d2sT_" + sed -i 's/trainer:norm_train/trainer:to_static_train/g' $FILENAME + # clear PARAM contents + if [ $PARAMS = "to_static" ] ;then + PARAMS="" + fi +fi +# bash test_tipc/benchmark_train.sh test_tipc/configs/det_mv3_db_v2_0/train_benchmark.txt benchmark_train dynamic_bs8_fp32_DP_N1C8 +# bash test_tipc/benchmark_train.sh test_tipc/configs/det_mv3_db_v2_0/train_benchmark.txt benchmark_train dynamicTostatic_bs8_fp32_DP_N1C8 +# bash test_tipc/benchmark_train.sh test_tipc/configs/det_mv3_db_v2_0/train_benchmark.txt benchmark_train dynamic_bs8_null_DP_N1C1 +IFS=$'\n' +# parser params from train_benchmark.txt +dataline=`cat $FILENAME` +# parser params +IFS=$'\n' +lines=(${dataline}) +model_name=$(func_parser_value "${lines[1]}") +python_name=$(func_parser_value "${lines[2]}") + +# set env +python=${python_name} +export str_tmp=$(echo `pip list|grep paddlepaddle-gpu|awk -F ' ' '{print $2}'`) +export frame_version=${str_tmp%%.post*} +export frame_commit=$(echo `${python} -c "import paddle;print(paddle.version.commit)"`) + +# 获取benchmark_params所在的行数 +line_num=`grep -n -w "train_benchmark_params" $FILENAME | cut -d ":" -f 1` +# for train log parser +batch_size=$(func_parser_value "${lines[line_num]}") +line_num=`expr $line_num + 1` +fp_items=$(func_parser_value "${lines[line_num]}") +line_num=`expr $line_num + 1` +epoch=$(func_parser_value "${lines[line_num]}") + +line_num=`expr $line_num + 1` +profile_option_key=$(func_parser_key "${lines[line_num]}") +profile_option_params=$(func_parser_value "${lines[line_num]}") +profile_option="${profile_option_key}:${profile_option_params}" + +line_num=`expr $line_num + 1` +flags_value=$(func_parser_value "${lines[line_num]}") +# set flags +IFS=";" +flags_list=(${flags_value}) +for _flag in ${flags_list[*]}; do + cmd="export ${_flag}" + eval $cmd +done + +# set log_name +repo_name=$(get_repo_name ) +SAVE_LOG=${BENCHMARK_LOG_DIR:-$(pwd)} # */benchmark_log +mkdir -p "${SAVE_LOG}/benchmark_log/" +status_log="${SAVE_LOG}/benchmark_log/results.log" + +# The number of lines in which train params can be replaced. +line_python=3 +line_gpuid=4 +line_precision=6 +line_epoch=7 +line_batchsize=9 +line_profile=13 +line_eval_py=24 +line_export_py=30 + +func_sed_params "$FILENAME" "${line_eval_py}" "null" +func_sed_params "$FILENAME" "${line_export_py}" "null" +func_sed_params "$FILENAME" "${line_python}" "$python" + +# if params +if [ ! -n "$PARAMS" ] ;then + # PARAMS input is not a word. + IFS="|" + batch_size_list=(${batch_size}) + fp_items_list=(${fp_items}) + device_num_list=(N1C4) + run_mode="DP" +elif [[ ${PARAMS} = "dynamicTostatic" ]];then + IFS="|" + model_type=$PARAMS + batch_size_list=(${batch_size}) + fp_items_list=(${fp_items}) + device_num_list=(N1C4) + run_mode="DP" +else + # parser params from input: modeltype_bs${bs_item}_${fp_item}_${run_mode}_${device_num} + IFS="_" + params_list=(${PARAMS}) + model_type=${params_list[0]} + batch_size=${params_list[1]} + batch_size=`echo ${batch_size} | tr -cd "[0-9]" ` + precision=${params_list[2]} + run_mode=${params_list[3]} + device_num=${params_list[4]} + IFS=";" + + if [ ${precision} = "fp16" ];then + precision="amp" + fi + + epoch=$(set_dynamic_epoch $device_num $epoch) + fp_items_list=($precision) + batch_size_list=($batch_size) + device_num_list=($device_num) +fi + +IFS="|" +for batch_size in ${batch_size_list[*]}; do + for train_precision in ${fp_items_list[*]}; do + for device_num in ${device_num_list[*]}; do + # sed batchsize and precision + if [ ${train_precision} = "amp" ];then + precision="fp16" + else + precision="fp32" + fi + + func_sed_params "$FILENAME" "${line_precision}" "$train_precision" + func_sed_params "$FILENAME" "${line_batchsize}" "$MODE=$batch_size" + func_sed_params "$FILENAME" "${line_epoch}" "$MODE=$epoch" + gpu_id=$(set_gpu_id $device_num) + + if [ ${#gpu_id} -le 1 ];then + log_path="$SAVE_LOG/profiling_log" + mkdir -p $log_path + log_name="${repo_name}_${model_name}_bs${batch_size}_${precision}_${run_mode}_${device_num}_${to_static}profiling" + func_sed_params "$FILENAME" "${line_gpuid}" "0" # sed used gpu_id + # set profile_option params + tmp=`sed -i "${line_profile}s/.*/${profile_option}/" "${FILENAME}"` + + # run test_train_inference_python.sh + cmd="bash test_tipc/test_train_inference_python.sh ${FILENAME} benchmark_train > ${log_path}/${log_name} 2>&1 " + echo $cmd + eval $cmd + eval "cat ${log_path}/${log_name}" + + # without profile + log_path="$SAVE_LOG/train_log" + speed_log_path="$SAVE_LOG/index" + mkdir -p $log_path + mkdir -p $speed_log_path + log_name="${repo_name}_${model_name}_bs${batch_size}_${precision}_${run_mode}_${device_num}_${to_static}log" + speed_log_name="${repo_name}_${model_name}_bs${batch_size}_${precision}_${run_mode}_${device_num}_${to_static}speed" + func_sed_params "$FILENAME" "${line_profile}" "null" # sed profile_id as null + cmd="bash test_tipc/test_train_inference_python.sh ${FILENAME} benchmark_train > ${log_path}/${log_name} 2>&1 " + echo $cmd + job_bt=`date '+%Y%m%d%H%M%S'` + eval $cmd + job_et=`date '+%Y%m%d%H%M%S'` + export model_run_time=$((${job_et}-${job_bt})) + eval "cat ${log_path}/${log_name}" + + # parser log + _model_name="${model_name}_bs${batch_size}_${precision}_${run_mode}" + cmd="${python} ${BENCHMARK_ROOT}/scripts/analysis.py --filename ${log_path}/${log_name} \ + --speed_log_file '${speed_log_path}/${speed_log_name}' \ + --model_name ${_model_name} \ + --base_batch_size ${batch_size} \ + --run_mode ${run_mode} \ + --fp_item ${precision} \ + --keyword ips: \ + --skip_steps 2 \ + --device_num ${device_num} \ + --speed_unit samples/s \ + --convergence_key loss: " + echo $cmd + eval $cmd + last_status=${PIPESTATUS[0]} + status_check $last_status "${cmd}" "${status_log}" + else + IFS=";" + unset_env=`unset CUDA_VISIBLE_DEVICES` + log_path="$SAVE_LOG/train_log" + speed_log_path="$SAVE_LOG/index" + mkdir -p $log_path + mkdir -p $speed_log_path + log_name="${repo_name}_${model_name}_bs${batch_size}_${precision}_${run_mode}_${device_num}_${to_static}log" + speed_log_name="${repo_name}_${model_name}_bs${batch_size}_${precision}_${run_mode}_${device_num}_${to_static}speed" + func_sed_params "$FILENAME" "${line_gpuid}" "$gpu_id" # sed used gpu_id + func_sed_params "$FILENAME" "${line_profile}" "null" # sed --profile_option as null + cmd="bash test_tipc/test_train_inference_python.sh ${FILENAME} benchmark_train > ${log_path}/${log_name} 2>&1 " + echo $cmd + job_bt=`date '+%Y%m%d%H%M%S'` + eval $cmd + job_et=`date '+%Y%m%d%H%M%S'` + export model_run_time=$((${job_et}-${job_bt})) + eval "cat ${log_path}/${log_name}" + # parser log + _model_name="${model_name}_bs${batch_size}_${precision}_${run_mode}" + + cmd="${python} ${BENCHMARK_ROOT}/scripts/analysis.py --filename ${log_path}/${log_name} \ + --speed_log_file '${speed_log_path}/${speed_log_name}' \ + --model_name ${_model_name} \ + --base_batch_size ${batch_size} \ + --run_mode ${run_mode} \ + --fp_item ${precision} \ + --keyword ips: \ + --skip_steps 2 \ + --device_num ${device_num} \ + --speed_unit images/s \ + --convergence_key loss: " + echo $cmd + eval $cmd + last_status=${PIPESTATUS[0]} + status_check $last_status "${cmd}" "${status_log}" + fi + done + done +done diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/common_func.sh b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/common_func.sh new file mode 100644 index 0000000000000000000000000000000000000000..c123d3cf6e6487ea6b0d5ef1a108e0994a7f1eb4 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/common_func.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +function func_parser_key(){ + strs=$1 + IFS=":" + array=(${strs}) + tmp=${array[0]} + echo ${tmp} +} + +function func_parser_value(){ + strs=$1 + IFS=":" + array=(${strs}) + tmp=${array[1]} + echo ${tmp} +} + +function func_set_params(){ + key=$1 + value=$2 + if [ ${key}x = "null"x ];then + echo " " + elif [[ ${value} = "null" ]] || [[ ${value} = " " ]] || [ ${#value} -le 0 ];then + echo " " + else + echo "${key}=${value}" + fi +} + +function func_parser_params(){ + strs=$1 + MODE=$2 + IFS=":" + array=(${strs}) + key=${array[0]} + tmp=${array[1]} + IFS="|" + res="" + for _params in ${tmp[*]}; do + IFS="=" + array=(${_params}) + mode=${array[0]} + value=${array[1]} + if [[ ${mode} = ${MODE} ]]; then + IFS="|" + #echo $(func_set_params "${mode}" "${value}") + echo $value + break + fi + IFS="|" + done + echo ${res} +} + +function status_check(){ + last_status=$1 # the exit code + run_command=$2 + run_log=$3 + model_name=$4 + log_path=$5 + if [ $last_status -eq 0 ]; then + echo -e "\033[33m Run successfully with command - ${model_name} - ${run_command} - ${log_path} \033[0m" | tee -a ${run_log} + else + echo -e "\033[33m Run failed with command - ${model_name} - ${run_command} - ${log_path} \033[0m" | tee -a ${run_log} + fi +} \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/configs/det_res50_db/train_infer_python.txt b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/configs/det_res50_db/train_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..7dc3da35fe6fdef0ebfaa43609d28650667d6e62 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/configs/det_res50_db/train_infer_python.txt @@ -0,0 +1,61 @@ +===========================train_params=========================== +model_name:det_res50_db +python:python +gpu_list:0|0,1 +trainer.use_gpu:True|True +amp:null +trainer.epochs:lite_train_lite_infer=1|whole_train_whole_infer=300 +trainer.output_dir:./output/ +dataset.train.loader.batch_size:lite_train_lite_infer=8|whole_train_lite_infer=8 +trainer.finetune_checkpoint:null +train_model_name:checkpoint/model_latest.pth +train_infer_img_dir:imgs/paper/db.jpg +null:null +## +trainer:norm_train +norm_train:tools/train.py --config_file config/icdar2015_resnet50_FPN_DBhead_polyLR.yaml -o trainer.log_iter=1 trainer.enable_eval=False dataset.train.loader.shuffle=false arch.backbone.pretrained=False +quant_export:null +fpgm_export:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:null +null:null +## +===========================infer_params=========================== +trainer.output_dir:./output/ +trainer.resume_checkpoint: +norm_export:tools/export_model.py --config_file config/icdar2015_resnet50_FPN_DBhead_polyLR.yaml -o +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +## +train_model:./inference/det_r50_vd_db_v2.0_train/best_accuracy +infer_export:tools/export_model.py --config_file config/icdar2015_resnet50_FPN_DBhead_polyLR.yaml -o +infer_quant:False +inference:tools/infer.py +--use_gpu:True|False +--enable_mkldnn:False +--cpu_threads:6 +--batch_size:1 +--use_tensorrt:False +--precision:fp32 +--model_dir: +--img_path:imgs/paper/db.jpg +--save_log_path:null +--benchmark:True +null:null +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[3,640,640]}];[{float32,[3,960,960]}] +===========================train_benchmark_params========================== +batch_size:8 +fp_items:fp32|fp16 +epoch:2 +--profiler_options:batch_range=[10,20];state=GPU;tracer_option=Default;profile_path=model.profile +flags:FLAGS_eager_delete_tensor_gb=0.0;FLAGS_fraction_of_gpu_memory_to_use=0.98;FLAGS_conv_workspace_size_limit=4096 +===========================to_static_train_benchmark_params=========================== +to_static_train:trainer.to_static=true diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/prepare.sh b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/prepare.sh new file mode 100644 index 0000000000000000000000000000000000000000..cd8f56fd7d3bc07b2d35d1239bf723bb3de1e136 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/prepare.sh @@ -0,0 +1,54 @@ +#!/bin/bash +source test_tipc/common_func.sh + +FILENAME=$1 + +# MODE be one of ['lite_train_lite_infer' 'lite_train_whole_infer' 'whole_train_whole_infer', +# 'whole_infer', 'klquant_whole_infer', +# 'cpp_infer', 'serving_infer'] + +MODE=$2 + +dataline=$(cat ${FILENAME}) + +# parser params +IFS=$'\n' +lines=(${dataline}) + +# The training params +model_name=$(func_parser_value "${lines[1]}") + +trainer_list=$(func_parser_value "${lines[14]}") + +if [ ${MODE} = "lite_train_lite_infer" ];then + python_name_list=$(func_parser_value "${lines[2]}") + array=(${python_name_list}) + python_name=${array[0]} + ${python_name} -m pip install -r requirement.txt + if [[ ${model_name} =~ "det_res50_db" ]];then + wget -nc https://paddle-wheel.bj.bcebos.com/benchmark/resnet50-19c8e357.pth -O /root/.cache/torch/hub/checkpoints/resnet50-19c8e357.pth + + # 下载数据集并解压 + rm -rf datasets + wget -nc https://paddleocr.bj.bcebos.com/dygraph_v2.0/test/benchmark_train/datasets.tar + tar xf datasets.tar + fi +elif [ ${MODE} = "benchmark_train" ];then + python_name_list=$(func_parser_value "${lines[2]}") + array=(${python_name_list}) + python_name=${array[0]} + ${python_name} -m pip install -r requirement.txt + if [[ ${model_name} =~ "det_res50_db" ]];then + wget -nc https://paddle-wheel.bj.bcebos.com/benchmark/resnet50-19c8e357.pth -O /root/.cache/torch/hub/checkpoints/resnet50-19c8e357.pth + + # 下载数据集并解压 + rm -rf datasets + wget -nc https://paddleocr.bj.bcebos.com/dygraph_v2.0/test/benchmark_train/datasets.tar + tar xf datasets.tar + # expand gt.txt 2 times + # cd ./train_data/icdar2015/text_localization + # for i in `seq 2`;do cp train_icdar2015_label.txt dup$i.txt;done + # cat dup* > train_icdar2015_label.txt && rm -rf dup* + # cd ../../../ + fi +fi \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/test_train_inference_python.sh b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/test_train_inference_python.sh new file mode 100644 index 0000000000000000000000000000000000000000..a54591a60cbd68c101cecb60ad6f6e4acd758872 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/test_tipc/test_train_inference_python.sh @@ -0,0 +1,343 @@ +#!/bin/bash +source test_tipc/common_func.sh + +FILENAME=$1 +# MODE be one of ['lite_train_lite_infer' 'lite_train_whole_infer' 'whole_train_whole_infer', 'whole_infer'] +MODE=$2 + +dataline=$(awk 'NR>=1{print}' $FILENAME) + +# parser params +IFS=$'\n' +lines=(${dataline}) + +# The training params +model_name=$(func_parser_value "${lines[1]}") +python=$(func_parser_value "${lines[2]}") +gpu_list=$(func_parser_value "${lines[3]}") +train_use_gpu_key=$(func_parser_key "${lines[4]}") +train_use_gpu_value=$(func_parser_value "${lines[4]}") +autocast_list=$(func_parser_value "${lines[5]}") +autocast_key=$(func_parser_key "${lines[5]}") +epoch_key=$(func_parser_key "${lines[6]}") +epoch_num=$(func_parser_params "${lines[6]}" "${MODE}") +save_model_key=$(func_parser_key "${lines[7]}") +train_batch_key=$(func_parser_key "${lines[8]}") +train_batch_value=$(func_parser_params "${lines[8]}" "${MODE}") +pretrain_model_key=$(func_parser_key "${lines[9]}") +pretrain_model_value=$(func_parser_value "${lines[9]}") +train_model_name=$(func_parser_value "${lines[10]}") +train_infer_img_dir=$(func_parser_value "${lines[11]}") +train_param_key1=$(func_parser_key "${lines[12]}") +train_param_value1=$(func_parser_value "${lines[12]}") + +trainer_list=$(func_parser_value "${lines[14]}") +trainer_norm=$(func_parser_key "${lines[15]}") +norm_trainer=$(func_parser_value "${lines[15]}") +pact_key=$(func_parser_key "${lines[16]}") +pact_trainer=$(func_parser_value "${lines[16]}") +fpgm_key=$(func_parser_key "${lines[17]}") +fpgm_trainer=$(func_parser_value "${lines[17]}") +distill_key=$(func_parser_key "${lines[18]}") +distill_trainer=$(func_parser_value "${lines[18]}") +trainer_key1=$(func_parser_key "${lines[19]}") +trainer_value1=$(func_parser_value "${lines[19]}") +trainer_key2=$(func_parser_key "${lines[20]}") +trainer_value2=$(func_parser_value "${lines[20]}") + +eval_py=$(func_parser_value "${lines[23]}") +eval_key1=$(func_parser_key "${lines[24]}") +eval_value1=$(func_parser_value "${lines[24]}") + +save_infer_key=$(func_parser_key "${lines[27]}") +export_weight=$(func_parser_key "${lines[28]}") +norm_export=$(func_parser_value "${lines[29]}") +pact_export=$(func_parser_value "${lines[30]}") +fpgm_export=$(func_parser_value "${lines[31]}") +distill_export=$(func_parser_value "${lines[32]}") +export_key1=$(func_parser_key "${lines[33]}") +export_value1=$(func_parser_value "${lines[33]}") +export_key2=$(func_parser_key "${lines[34]}") +export_value2=$(func_parser_value "${lines[34]}") +inference_dir=$(func_parser_value "${lines[35]}") + +# parser inference model +infer_model_dir_list=$(func_parser_value "${lines[36]}") +infer_export_list=$(func_parser_value "${lines[37]}") +infer_is_quant=$(func_parser_value "${lines[38]}") +# parser inference +inference_py=$(func_parser_value "${lines[39]}") +use_gpu_key=$(func_parser_key "${lines[40]}") +use_gpu_list=$(func_parser_value "${lines[40]}") +use_mkldnn_key=$(func_parser_key "${lines[41]}") +use_mkldnn_list=$(func_parser_value "${lines[41]}") +cpu_threads_key=$(func_parser_key "${lines[42]}") +cpu_threads_list=$(func_parser_value "${lines[42]}") +batch_size_key=$(func_parser_key "${lines[43]}") +batch_size_list=$(func_parser_value "${lines[43]}") +use_trt_key=$(func_parser_key "${lines[44]}") +use_trt_list=$(func_parser_value "${lines[44]}") +precision_key=$(func_parser_key "${lines[45]}") +precision_list=$(func_parser_value "${lines[45]}") +infer_model_key=$(func_parser_key "${lines[46]}") +image_dir_key=$(func_parser_key "${lines[47]}") +infer_img_dir=$(func_parser_value "${lines[47]}") +save_log_key=$(func_parser_key "${lines[48]}") +benchmark_key=$(func_parser_key "${lines[49]}") +benchmark_value=$(func_parser_value "${lines[49]}") +infer_key1=$(func_parser_key "${lines[50]}") +infer_value1=$(func_parser_value "${lines[50]}") + +LOG_PATH="./test_tipc/output/${model_name}/${MODE}" +mkdir -p ${LOG_PATH} +status_log="${LOG_PATH}/results_python.log" + +line_num=`grep -n -w "to_static_train_benchmark_params" $FILENAME | cut -d ":" -f 1` +to_static_key=$(func_parser_key "${lines[line_num]}") +to_static_trainer=$(func_parser_value "${lines[line_num]}") + +function func_inference(){ + IFS='|' + _python=$1 + _script=$2 + _model_dir=$3 + _log_path=$4 + _img_dir=$5 + _flag_quant=$6 + _gpu=$7 + # inference + for use_gpu in ${use_gpu_list[*]}; do + if [ ${use_gpu} = "False" ] || [ ${use_gpu} = "cpu" ]; then + for use_mkldnn in ${use_mkldnn_list[*]}; do + # if [ ${use_mkldnn} = "False" ] && [ ${_flag_quant} = "True" ]; then + # continue + # fi + for threads in ${cpu_threads_list[*]}; do + for batch_size in ${batch_size_list[*]}; do + for precision in ${precision_list[*]}; do + if [ ${use_mkldnn} = "False" ] && [ ${precision} = "fp16" ]; then + continue + fi # skip when enable fp16 but disable mkldnn + if [ ${_flag_quant} = "True" ] && [ ${precision} != "int8" ]; then + continue + fi # skip when quant model inference but precision is not int8 + set_precision=$(func_set_params "${precision_key}" "${precision}") + + _save_log_path="${_log_path}/python_infer_cpu_gpus_${_gpu}_usemkldnn_${use_mkldnn}_threads_${threads}_precision_${precision}_batchsize_${batch_size}.log" + set_infer_data=$(func_set_params "${image_dir_key}" "${_img_dir}") + set_benchmark=$(func_set_params "${benchmark_key}" "${benchmark_value}") + set_batchsize=$(func_set_params "${batch_size_key}" "${batch_size}") + set_mkldnn=$(func_set_params "${use_mkldnn_key}" "${use_mkldnn}") + set_cpu_threads=$(func_set_params "${cpu_threads_key}" "${threads}") + set_model_dir=$(func_set_params "${infer_model_key}" "${_model_dir}") + set_infer_params0=$(func_set_params "${save_log_key}" "${save_log_value}") + set_infer_params1=$(func_set_params "${infer_key1}" "${infer_value1}") + command="${_python} ${_script} ${use_gpu_key}=${use_gpu} ${set_mkldnn} ${set_cpu_threads} ${set_model_dir} ${set_batchsize} ${set_infer_params0} ${set_infer_data} ${set_benchmark} ${set_precision} ${set_infer_params1} > ${_save_log_path} 2>&1 " + eval $command + last_status=${PIPESTATUS[0]} + eval "cat ${_save_log_path}" + status_check $last_status "${command}" "${status_log}" "${model_name}" "${_save_log_path}" + done + done + done + done + elif [ ${use_gpu} = "True" ] || [ ${use_gpu} = "gpu" ]; then + for use_trt in ${use_trt_list[*]}; do + for precision in ${precision_list[*]}; do + if [[ ${_flag_quant} = "False" ]] && [[ ${precision} =~ "int8" ]]; then + continue + fi + if [[ ${precision} =~ "fp16" || ${precision} =~ "int8" ]] && [ ${use_trt} = "False" ]; then + continue + fi + if [[ ${use_trt} = "False" && ${precision} =~ "int8" ]] && [ ${_flag_quant} = "True" ]; then + continue + fi + for batch_size in ${batch_size_list[*]}; do + _save_log_path="${_log_path}/python_infer_gpu_gpus_${_gpu}_usetrt_${use_trt}_precision_${precision}_batchsize_${batch_size}.log" + set_infer_data=$(func_set_params "${image_dir_key}" "${_img_dir}") + set_benchmark=$(func_set_params "${benchmark_key}" "${benchmark_value}") + set_batchsize=$(func_set_params "${batch_size_key}" "${batch_size}") + set_tensorrt=$(func_set_params "${use_trt_key}" "${use_trt}") + set_precision=$(func_set_params "${precision_key}" "${precision}") + set_model_dir=$(func_set_params "${infer_model_key}" "${_model_dir}") + set_infer_params0=$(func_set_params "${save_log_key}" "${save_log_value}") + set_infer_params1=$(func_set_params "${infer_key1}" "${infer_value1}") + command="${_python} ${_script} ${use_gpu_key}=${use_gpu} ${set_tensorrt} ${set_precision} ${set_model_dir} ${set_batchsize} ${set_infer_data} ${set_benchmark} ${set_infer_params1} ${set_infer_params0} > ${_save_log_path} 2>&1 " + eval $command + last_status=${PIPESTATUS[0]} + eval "cat ${_save_log_path}" + status_check $last_status "${command}" "${status_log}" "${model_name}" "${_save_log_path}" + + done + done + done + else + echo "Does not support hardware other than CPU and GPU Currently!" + fi + done +} + +if [ ${MODE} = "whole_infer" ]; then + GPUID=$3 + if [ ${#GPUID} -le 0 ];then + env=" " + else + env="export CUDA_VISIBLE_DEVICES=${GPUID}" + fi + # set CUDA_VISIBLE_DEVICES + eval $env + export Count=0 + gpu=0 + IFS="|" + infer_run_exports=(${infer_export_list}) + infer_quant_flag=(${infer_is_quant}) + for infer_model in ${infer_model_dir_list[*]}; do + # run export + if [ ${infer_run_exports[Count]} != "null" ];then + save_infer_dir="${infer_model}" + set_export_weight=$(func_set_params "${export_weight}" "${infer_model}") + set_save_infer_key=$(func_set_params "${save_infer_key}" "${save_infer_dir}") + export_log_path="${LOG_PATH}_export_${Count}.log" + export_cmd="${python} ${infer_run_exports[Count]} ${set_export_weight} ${set_save_infer_key} > ${export_log_path} 2>&1 " + echo ${infer_run_exports[Count]} + echo $export_cmd + eval $export_cmd + status_export=$? + status_check $status_export "${export_cmd}" "${status_log}" "${model_name}" "${export_log_path}" + else + save_infer_dir=${infer_model} + fi + #run inference + is_quant=${infer_quant_flag[Count]} + func_inference "${python}" "${inference_py}" "${save_infer_dir}" "${LOG_PATH}" "${infer_img_dir}" ${is_quant} "${gpu}" + Count=$(($Count + 1)) + done +else + IFS="|" + export Count=0 + USE_GPU_KEY=(${train_use_gpu_value}) + for gpu in ${gpu_list[*]}; do + train_use_gpu=${USE_GPU_KEY[Count]} + Count=$(($Count + 1)) + ips="" + if [ ${gpu} = "-1" ];then + env="" + elif [ ${#gpu} -le 1 ];then + env="export CUDA_VISIBLE_DEVICES=${gpu}" + elif [ ${#gpu} -le 15 ];then + IFS="," + array=(${gpu}) + env="export CUDA_VISIBLE_DEVICES=${array[0]}" + IFS="|" + else + IFS=";" + array=(${gpu}) + ips=${array[0]} + gpu=${array[1]} + IFS="|" + env=" " + fi + for autocast in ${autocast_list[*]}; do + if [ ${autocast} = "amp" ]; then + set_amp_config="amp.scale_loss=1024.0 amp.use_dynamic_loss_scaling=True amp.amp_level=O2" + else + set_amp_config="amp=None" + fi + for trainer in ${trainer_list[*]}; do + flag_quant=False + if [ ${trainer} = ${pact_key} ]; then + run_train=${pact_trainer} + run_export=${pact_export} + flag_quant=True + elif [ ${trainer} = "${fpgm_key}" ]; then + run_train=${fpgm_trainer} + run_export=${fpgm_export} + elif [ ${trainer} = "${distill_key}" ]; then + run_train=${distill_trainer} + run_export=${distill_export} + elif [ ${trainer} = "${to_static_key}" ]; then + run_train="${norm_trainer} ${to_static_trainer}" + run_export=${norm_export} + elif [[ ${trainer} = ${trainer_key2} ]]; then + run_train=${trainer_value2} + run_export=${export_value2} + else + run_train=${norm_trainer} + run_export=${norm_export} + fi + + if [ ${run_train} = "null" ]; then + continue + fi + + set_epoch=$(func_set_params "${epoch_key}" "${epoch_num}") + set_pretrain=$(func_set_params "${pretrain_model_key}" "${pretrain_model_value}") + set_batchsize=$(func_set_params "${train_batch_key}" "${train_batch_value}") + set_train_params1=$(func_set_params "${train_param_key1}" "${train_param_value1}") + set_use_gpu=$(func_set_params "${train_use_gpu_key}" "${train_use_gpu}") + # if length of ips >= 15, then it is seen as multi-machine + # 15 is the min length of ips info for multi-machine: 0.0.0.0,0.0.0.0 + if [ ${#ips} -le 15 ];then + save_log="${LOG_PATH}/${trainer}_gpus_${gpu}_autocast_${autocast}" + nodes=1 + else + IFS="," + ips_array=(${ips}) + IFS="|" + nodes=${#ips_array[@]} + save_log="${LOG_PATH}/${trainer}_gpus_${gpu}_autocast_${autocast}_nodes_${nodes}" + fi + + + set_save_model=$(func_set_params "${save_model_key}" "${save_log}") + if [ ${#gpu} -le 2 ];then # train with cpu or single gpu + cmd="${python} ${run_train} ${set_use_gpu} ${set_save_model} ${set_epoch} ${set_pretrain} ${set_batchsize} ${set_amp_config} ${set_train_params1}" + elif [ ${#ips} -le 15 ];then # train with multi-gpu + cmd="${python} -m paddle.distributed.launch --gpus=${gpu} ${run_train} ${set_use_gpu} ${set_save_model} ${set_epoch} ${set_pretrain} ${set_batchsize} ${set_amp_config} ${set_train_params1}" + else # train with multi-machine + cmd="${python} -m paddle.distributed.launch --ips=${ips} --gpus=${gpu} ${run_train} ${set_use_gpu} ${set_save_model} ${set_pretrain} ${set_epoch} ${set_batchsize} ${set_amp_config} ${set_train_params1}" + fi + # run train + eval $cmd + eval "cat ${save_log}/train.log >> ${save_log}.log" + status_check $? "${cmd}" "${status_log}" "${model_name}" "${save_log}.log" + + set_eval_pretrain=$(func_set_params "${pretrain_model_key}" "${save_log}/${train_model_name}") + + # run eval + if [ ${eval_py} != "null" ]; then + eval ${env} + set_eval_params1=$(func_set_params "${eval_key1}" "${eval_value1}") + eval_log_path="${LOG_PATH}/${trainer}_gpus_${gpu}_autocast_${autocast}_nodes_${nodes}_eval.log" + eval_cmd="${python} ${eval_py} ${set_eval_pretrain} ${set_use_gpu} ${set_eval_params1} > ${eval_log_path} 2>&1 " + eval $eval_cmd + status_check $? "${eval_cmd}" "${status_log}" "${model_name}" "${eval_log_path}" + fi + # run export model + if [ ${run_export} != "null" ]; then + # run export model + save_infer_path="${save_log}" + export_log_path="${LOG_PATH}/${trainer}_gpus_${gpu}_autocast_${autocast}_nodes_${nodes}_export.log" + set_export_weight=$(func_set_params "${export_weight}" "${save_log}/${train_model_name}") + set_save_infer_key=$(func_set_params "${save_infer_key}" "${save_infer_path}") + export_cmd="${python} ${run_export} ${set_export_weight} ${set_save_infer_key} > ${export_log_path} 2>&1 " + eval $export_cmd + status_check $? "${export_cmd}" "${status_log}" "${model_name}" "${export_log_path}" + + #run inference + eval $env + save_infer_path="${save_log}" + if [[ ${inference_dir} != "null" ]] && [[ ${inference_dir} != '##' ]]; then + infer_model_dir="${save_infer_path}/${inference_dir}" + else + infer_model_dir=${save_infer_path} + fi + func_inference "${python}" "${inference_py}" "${infer_model_dir}" "${LOG_PATH}" "${train_infer_img_dir}" "${flag_quant}" "${gpu}" + + eval "unset CUDA_VISIBLE_DEVICES" + fi + done # done with: for trainer in ${trainer_list[*]}; do + done # done with: for autocast in ${autocast_list[*]}; do + done # done with: for gpu in ${gpu_list[*]}; do +fi # end if [ ${MODE} = "infer" ]; then \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7cbf835d7e1654d050df9fca997a774f3d7947ad --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/12/8 13:14 +# @Author : zhoujun \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/eval.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/eval.py new file mode 100644 index 0000000000000000000000000000000000000000..fe514ddc0d4f1e09e784f346374e0e5aa2b998bc --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/eval.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# @Time : 2018/6/11 15:54 +# @Author : zhoujun +import os +import sys +import pathlib +__dir__ = pathlib.Path(os.path.abspath(__file__)) +sys.path.append(str(__dir__)) +sys.path.append(str(__dir__.parent.parent)) + +import argparse +import time +import paddle +from tqdm.auto import tqdm + + +class EVAL(): + def __init__(self, model_path, gpu_id=0): + from models import build_model + from data_loader import get_dataloader + from post_processing import get_post_processing + from utils import get_metric + self.gpu_id = gpu_id + if self.gpu_id is not None and isinstance( + self.gpu_id, int) and paddle.device.is_compiled_with_cuda(): + paddle.device.set_device("gpu:{}".format(self.gpu_id)) + else: + paddle.device.set_device("cpu") + checkpoint = paddle.load(model_path) + config = checkpoint['config'] + config['arch']['backbone']['pretrained'] = False + + self.validate_loader = get_dataloader(config['dataset']['validate'], + config['distributed']) + + self.model = build_model(config['arch']) + self.model.set_state_dict(checkpoint['state_dict']) + + self.post_process = get_post_processing(config['post_processing']) + self.metric_cls = get_metric(config['metric']) + + def eval(self): + self.model.eval() + raw_metrics = [] + total_frame = 0.0 + total_time = 0.0 + for i, batch in tqdm( + enumerate(self.validate_loader), + total=len(self.validate_loader), + desc='test model'): + with paddle.no_grad(): + start = time.time() + preds = self.model(batch['img']) + boxes, scores = self.post_process( + batch, + preds, + is_output_polygon=self.metric_cls.is_output_polygon) + total_frame += batch['img'].shape[0] + total_time += time.time() - start + raw_metric = self.metric_cls.validate_measure(batch, + (boxes, scores)) + raw_metrics.append(raw_metric) + metrics = self.metric_cls.gather_measure(raw_metrics) + print('FPS:{}'.format(total_frame / total_time)) + return { + 'recall': metrics['recall'].avg, + 'precision': metrics['precision'].avg, + 'fmeasure': metrics['fmeasure'].avg + } + + +def init_args(): + parser = argparse.ArgumentParser(description='DBNet.paddle') + parser.add_argument( + '--model_path', + required=False, + default='output/DBNet_resnet18_FPN_DBHead/checkpoint/1.pth', + type=str) + args = parser.parse_args() + return args + + +if __name__ == '__main__': + args = init_args() + eval = EVAL(args.model_path) + result = eval.eval() + print(result) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/export_model.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/export_model.py new file mode 100644 index 0000000000000000000000000000000000000000..59a318a196a6480f904c7ddaed7eab49cbf3f80c --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/export_model.py @@ -0,0 +1,57 @@ +import os +import sys + +__dir__ = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(__dir__) +sys.path.insert(0, os.path.abspath(os.path.join(__dir__, ".."))) + +import argparse + +import paddle +from paddle.jit import to_static + +from models import build_model +from utils import Config, ArgsParser + + +def init_args(): + parser = ArgsParser() + args = parser.parse_args() + return args + + +def load_checkpoint(model, checkpoint_path): + """ + load checkpoints + :param checkpoint_path: Checkpoint path to be loaded + """ + checkpoint = paddle.load(checkpoint_path) + model.set_state_dict(checkpoint['state_dict']) + print('load checkpoint from {}'.format(checkpoint_path)) + + +def main(config): + model = build_model(config['arch']) + load_checkpoint(model, config['trainer']['resume_checkpoint']) + model.eval() + + save_path = config["trainer"]["output_dir"] + save_path = os.path.join(save_path, "inference") + infer_shape = [3, -1, -1] + model = to_static( + model, + input_spec=[ + paddle.static.InputSpec( + shape=[None] + infer_shape, dtype="float32") + ]) + + paddle.jit.save(model, save_path) + print("inference model is saved to {}".format(save_path)) + + +if __name__ == "__main__": + args = init_args() + assert os.path.exists(args.config_file) + config = Config(args.config_file) + config.merge_dict(args.opt) + main(config.cfg) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/infer.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/infer.py new file mode 100644 index 0000000000000000000000000000000000000000..24e919c33f2352e7e45ea2c2503fa0f94bb9cf58 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/infer.py @@ -0,0 +1,298 @@ +# Copyright (c) 2021 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 os +import sys +import pathlib +__dir__ = pathlib.Path(os.path.abspath(__file__)) +sys.path.append(str(__dir__)) +sys.path.append(str(__dir__.parent.parent)) + +import cv2 +import paddle +from paddle import inference +import numpy as np +from PIL import Image + +from paddle.vision import transforms +from tools.predict import resize_image +from post_processing import get_post_processing +from utils.util import draw_bbox, save_result + + +class InferenceEngine(object): + """InferenceEngine + + Inference engina class which contains preprocess, run, postprocess + """ + + def __init__(self, args): + """ + Args: + args: Parameters generated using argparser. + Returns: None + """ + super().__init__() + self.args = args + + # init inference engine + self.predictor, self.config, self.input_tensor, self.output_tensor = self.load_predictor( + os.path.join(args.model_dir, "inference.pdmodel"), + os.path.join(args.model_dir, "inference.pdiparams")) + + # build transforms + self.transforms = transforms.Compose([ + transforms.ToTensor(), transforms.Normalize( + mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) + ]) + + # wamrup + if self.args.warmup > 0: + for idx in range(args.warmup): + print(idx) + x = np.random.rand(1, 3, self.args.crop_size, + self.args.crop_size).astype("float32") + self.input_tensor.copy_from_cpu(x) + self.predictor.run() + self.output_tensor.copy_to_cpu() + + self.post_process = get_post_processing({ + 'type': 'SegDetectorRepresenter', + 'args': { + 'thresh': 0.3, + 'box_thresh': 0.7, + 'max_candidates': 1000, + 'unclip_ratio': 1.5 + } + }) + + def load_predictor(self, model_file_path, params_file_path): + """load_predictor + initialize the inference engine + Args: + model_file_path: inference model path (*.pdmodel) + model_file_path: inference parmaeter path (*.pdiparams) + Return: + predictor: Predictor created using Paddle Inference. + config: Configuration of the predictor. + input_tensor: Input tensor of the predictor. + output_tensor: Output tensor of the predictor. + """ + args = self.args + config = inference.Config(model_file_path, params_file_path) + if args.use_gpu: + config.enable_use_gpu(1000, 0) + if args.use_tensorrt: + config.enable_tensorrt_engine( + workspace_size=1 << 30, + precision_mode=precision, + max_batch_size=args.max_batch_size, + min_subgraph_size=args. + min_subgraph_size, # skip the minmum trt subgraph + use_calib_mode=False) + + # collect shape + trt_shape_f = os.path.join(model_dir, "_trt_dynamic_shape.txt") + + if not os.path.exists(trt_shape_f): + config.collect_shape_range_info(trt_shape_f) + logger.info( + f"collect dynamic shape info into : {trt_shape_f}") + try: + config.enable_tuned_tensorrt_dynamic_shape(trt_shape_f, + True) + except Exception as E: + logger.info(E) + logger.info("Please keep your paddlepaddle-gpu >= 2.3.0!") + else: + config.disable_gpu() + # The thread num should not be greater than the number of cores in the CPU. + if args.enable_mkldnn: + # cache 10 different shapes for mkldnn to avoid memory leak + config.set_mkldnn_cache_capacity(10) + config.enable_mkldnn() + if args.precision == "fp16": + config.enable_mkldnn_bfloat16() + if hasattr(args, "cpu_threads"): + config.set_cpu_math_library_num_threads(args.cpu_threads) + else: + # default cpu threads as 10 + config.set_cpu_math_library_num_threads(10) + + # enable memory optim + config.enable_memory_optim() + config.disable_glog_info() + + config.switch_use_feed_fetch_ops(False) + config.switch_ir_optim(True) + + # create predictor + predictor = inference.create_predictor(config) + + # get input and output tensor property + input_names = predictor.get_input_names() + input_tensor = predictor.get_input_handle(input_names[0]) + + output_names = predictor.get_output_names() + output_tensor = predictor.get_output_handle(output_names[0]) + + return predictor, config, input_tensor, output_tensor + + def preprocess(self, img_path, short_size): + """preprocess + Preprocess to the input. + Args: + img_path: Image path. + Returns: Input data after preprocess. + """ + img = cv2.imread(img_path, 1) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + h, w = img.shape[:2] + img = resize_image(img, short_size) + img = self.transforms(img) + img = np.expand_dims(img, axis=0) + shape_info = {'shape': [(h, w)]} + return img, shape_info + + def postprocess(self, x, shape_info, is_output_polygon): + """postprocess + Postprocess to the inference engine output. + Args: + x: Inference engine output. + Returns: Output data after argmax. + """ + box_list, score_list = self.post_process( + shape_info, x, is_output_polygon=is_output_polygon) + box_list, score_list = box_list[0], score_list[0] + if len(box_list) > 0: + if is_output_polygon: + idx = [x.sum() > 0 for x in box_list] + box_list = [box_list[i] for i, v in enumerate(idx) if v] + score_list = [score_list[i] for i, v in enumerate(idx) if v] + else: + idx = box_list.reshape(box_list.shape[0], -1).sum( + axis=1) > 0 # 去掉全为0的框 + box_list, score_list = box_list[idx], score_list[idx] + else: + box_list, score_list = [], [] + return box_list, score_list + + def run(self, x): + """run + Inference process using inference engine. + Args: + x: Input data after preprocess. + Returns: Inference engine output + """ + self.input_tensor.copy_from_cpu(x) + self.predictor.run() + output = self.output_tensor.copy_to_cpu() + return output + + +def get_args(add_help=True): + """ + parse args + """ + import argparse + + def str2bool(v): + return v.lower() in ("true", "t", "1") + + parser = argparse.ArgumentParser( + description="PaddlePaddle Classification Training", add_help=add_help) + + parser.add_argument("--model_dir", default=None, help="inference model dir") + parser.add_argument("--batch_size", type=int, default=1) + parser.add_argument( + "--short_size", default=1024, type=int, help="short size") + parser.add_argument("--img_path", default="./images/demo.jpg") + + parser.add_argument( + "--benchmark", default=False, type=str2bool, help="benchmark") + parser.add_argument("--warmup", default=0, type=int, help="warmup iter") + parser.add_argument( + '--polygon', action='store_true', help='output polygon or box') + + parser.add_argument("--use_gpu", type=str2bool, default=True) + parser.add_argument("--use_tensorrt", type=str2bool, default=False) + parser.add_argument("--precision", type=str, default="fp32") + parser.add_argument("--gpu_mem", type=int, default=500) + parser.add_argument("--gpu_id", type=int, default=0) + parser.add_argument("--enable_mkldnn", type=str2bool, default=False) + parser.add_argument("--cpu_threads", type=int, default=10) + + args = parser.parse_args() + return args + + +def main(args): + """ + Main inference function. + Args: + args: Parameters generated using argparser. + Returns: + class_id: Class index of the input. + prob: : Probability of the input. + """ + inference_engine = InferenceEngine(args) + + # init benchmark + if args.benchmark: + import auto_log + autolog = auto_log.AutoLogger( + model_name="db", + batch_size=args.batch_size, + inference_config=inference_engine.config, + gpu_ids="auto" if args.use_gpu else None) + + # enable benchmark + if args.benchmark: + autolog.times.start() + + # preprocess + img, shape_info = inference_engine.preprocess(args.img_path, + args.short_size) + + if args.benchmark: + autolog.times.stamp() + + output = inference_engine.run(img) + + if args.benchmark: + autolog.times.stamp() + + # postprocess + box_list, score_list = inference_engine.postprocess(output, shape_info, + args.polygon) + + if args.benchmark: + autolog.times.stamp() + autolog.times.end(stamp=True) + autolog.report() + + img = draw_bbox(cv2.imread(args.img_path)[:, :, ::-1], box_list) + # 保存结果到路径 + os.makedirs('output', exist_ok=True) + img_path = pathlib.Path(args.img_path) + output_path = os.path.join('output', img_path.stem + '_infer_result.jpg') + cv2.imwrite(output_path, img[:, :, ::-1]) + save_result( + output_path.replace('_infer_result.jpg', '.txt'), box_list, score_list, + args.polygon) + + +if __name__ == "__main__": + args = get_args() + main(args) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/predict.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/predict.py new file mode 100644 index 0000000000000000000000000000000000000000..51beffd1706c0c12100a0d7fea98c7532b1272b6 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/predict.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/24 12:06 +# @Author : zhoujun + +import os +import sys +import pathlib +__dir__ = pathlib.Path(os.path.abspath(__file__)) +sys.path.append(str(__dir__)) +sys.path.append(str(__dir__.parent.parent)) + +import time +import cv2 +import paddle + +from data_loader import get_transforms +from models import build_model +from post_processing import get_post_processing + + +def resize_image(img, short_size): + height, width, _ = img.shape + if height < width: + new_height = short_size + new_width = new_height / height * width + else: + new_width = short_size + new_height = new_width / width * height + new_height = int(round(new_height / 32) * 32) + new_width = int(round(new_width / 32) * 32) + resized_img = cv2.resize(img, (new_width, new_height)) + return resized_img + + +class PaddleModel: + def __init__(self, model_path, post_p_thre=0.7, gpu_id=None): + ''' + 初始化模型 + :param model_path: 模型地址(可以是模型的参数或者参数和计算图一起保存的文件) + :param gpu_id: 在哪一块gpu上运行 + ''' + self.gpu_id = gpu_id + + if self.gpu_id is not None and isinstance( + self.gpu_id, int) and paddle.device.is_compiled_with_cuda(): + paddle.device.set_device("gpu:{}".format(self.gpu_id)) + else: + paddle.device.set_device("cpu") + checkpoint = paddle.load(model_path) + + config = checkpoint['config'] + config['arch']['backbone']['pretrained'] = False + self.model = build_model(config['arch']) + self.post_process = get_post_processing(config['post_processing']) + self.post_process.box_thresh = post_p_thre + self.img_mode = config['dataset']['train']['dataset']['args'][ + 'img_mode'] + self.model.set_state_dict(checkpoint['state_dict']) + self.model.eval() + + self.transform = [] + for t in config['dataset']['train']['dataset']['args']['transforms']: + if t['type'] in ['ToTensor', 'Normalize']: + self.transform.append(t) + self.transform = get_transforms(self.transform) + + def predict(self, + img_path: str, + is_output_polygon=False, + short_size: int=1024): + ''' + 对传入的图像进行预测,支持图像地址,opecv 读取图片,偏慢 + :param img_path: 图像地址 + :param is_numpy: + :return: + ''' + assert os.path.exists(img_path), 'file is not exists' + img = cv2.imread(img_path, 1 if self.img_mode != 'GRAY' else 0) + if self.img_mode == 'RGB': + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + h, w = img.shape[:2] + img = resize_image(img, short_size) + # 将图片由(w,h)变为(1,img_channel,h,w) + tensor = self.transform(img) + tensor = tensor.unsqueeze_(0) + + batch = {'shape': [(h, w)]} + with paddle.no_grad(): + start = time.time() + preds = self.model(tensor) + box_list, score_list = self.post_process( + batch, preds, is_output_polygon=is_output_polygon) + box_list, score_list = box_list[0], score_list[0] + if len(box_list) > 0: + if is_output_polygon: + idx = [x.sum() > 0 for x in box_list] + box_list = [box_list[i] for i, v in enumerate(idx) if v] + score_list = [score_list[i] for i, v in enumerate(idx) if v] + else: + idx = box_list.reshape(box_list.shape[0], -1).sum( + axis=1) > 0 # 去掉全为0的框 + box_list, score_list = box_list[idx], score_list[idx] + else: + box_list, score_list = [], [] + t = time.time() - start + return preds[0, 0, :, :].detach().cpu().numpy(), box_list, score_list, t + + +def save_depoly(net, input, save_path): + input_spec = [ + paddle.static.InputSpec( + shape=[None, 3, None, None], dtype="float32") + ] + net = paddle.jit.to_static(net, input_spec=input_spec) + + # save static model for inference directly + paddle.jit.save(net, save_path) + + +def init_args(): + import argparse + parser = argparse.ArgumentParser(description='DBNet.paddle') + parser.add_argument('--model_path', default=r'model_best.pth', type=str) + parser.add_argument( + '--input_folder', + default='./test/input', + type=str, + help='img path for predict') + parser.add_argument( + '--output_folder', + default='./test/output', + type=str, + help='img path for output') + parser.add_argument('--gpu', default=0, type=int, help='gpu for inference') + parser.add_argument( + '--thre', default=0.3, type=float, help='the thresh of post_processing') + parser.add_argument( + '--polygon', action='store_true', help='output polygon or box') + parser.add_argument('--show', action='store_true', help='show result') + parser.add_argument( + '--save_result', + action='store_true', + help='save box and score to txt file') + args = parser.parse_args() + return args + + +if __name__ == '__main__': + import pathlib + from tqdm import tqdm + import matplotlib.pyplot as plt + from utils.util import show_img, draw_bbox, save_result, get_image_file_list + + args = init_args() + print(args) + # 初始化网络 + model = PaddleModel(args.model_path, post_p_thre=args.thre, gpu_id=args.gpu) + img_folder = pathlib.Path(args.input_folder) + for img_path in tqdm(get_image_file_list(args.input_folder)): + preds, boxes_list, score_list, t = model.predict( + img_path, is_output_polygon=args.polygon) + img = draw_bbox(cv2.imread(img_path)[:, :, ::-1], boxes_list) + if args.show: + show_img(preds) + show_img(img, title=os.path.basename(img_path)) + plt.show() + # 保存结果到路径 + os.makedirs(args.output_folder, exist_ok=True) + img_path = pathlib.Path(img_path) + output_path = os.path.join(args.output_folder, + img_path.stem + '_result.jpg') + pred_path = os.path.join(args.output_folder, + img_path.stem + '_pred.jpg') + cv2.imwrite(output_path, img[:, :, ::-1]) + cv2.imwrite(pred_path, preds * 255) + save_result( + output_path.replace('_result.jpg', '.txt'), boxes_list, score_list, + args.polygon) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/train.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/train.py new file mode 100644 index 0000000000000000000000000000000000000000..403d6185fc28c5037bcb29d5f188ec0692c9499b --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/tools/train.py @@ -0,0 +1,61 @@ +import os +import sys +import pathlib +__dir__ = pathlib.Path(os.path.abspath(__file__)) +sys.path.append(str(__dir__)) +sys.path.append(str(__dir__.parent.parent)) + +import paddle +import paddle.distributed as dist +from utils import Config, ArgsParser + + +def init_args(): + parser = ArgsParser() + args = parser.parse_args() + return args + + +def main(config, profiler_options): + from models import build_model, build_loss + from data_loader import get_dataloader + from trainer import Trainer + from post_processing import get_post_processing + from utils import get_metric + if paddle.device.cuda.device_count() > 1: + dist.init_parallel_env() + config['distributed'] = True + else: + config['distributed'] = False + train_loader = get_dataloader(config['dataset']['train'], + config['distributed']) + assert train_loader is not None + if 'validate' in config['dataset']: + validate_loader = get_dataloader(config['dataset']['validate'], False) + else: + validate_loader = None + criterion = build_loss(config['loss']) + config['arch']['backbone']['in_channels'] = 3 if config['dataset']['train'][ + 'dataset']['args']['img_mode'] != 'GRAY' else 1 + model = build_model(config['arch']) + # set @to_static for benchmark, skip this by default. + post_p = get_post_processing(config['post_processing']) + metric = get_metric(config['metric']) + trainer = Trainer( + config=config, + model=model, + criterion=criterion, + train_loader=train_loader, + post_process=post_p, + metric_cls=metric, + validate_loader=validate_loader, + profiler_options=profiler_options) + trainer.train() + + +if __name__ == '__main__': + args = init_args() + assert os.path.exists(args.config_file) + config = Config(args.config_file) + config.merge_dict(args.opt) + main(config.cfg, args.profiler_options) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/trainer/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/trainer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..76c7392d142c96d5ec715b528ea47e9001cbec4b --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/trainer/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/23 21:58 +# @Author : zhoujun +from .trainer import Trainer \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/trainer/trainer.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/trainer/trainer.py new file mode 100644 index 0000000000000000000000000000000000000000..34b259f3d182672d727fb50eebf14c27870cb836 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/trainer/trainer.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/23 21:58 +# @Author : zhoujun +import time + +import paddle +from tqdm import tqdm + +from base import BaseTrainer +from utils import runningScore, cal_text_score, Polynomial, profiler + + +class Trainer(BaseTrainer): + def __init__(self, + config, + model, + criterion, + train_loader, + validate_loader, + metric_cls, + post_process=None, + profiler_options=None): + super(Trainer, self).__init__(config, model, criterion, train_loader, + validate_loader, metric_cls, post_process) + self.profiler_options = profiler_options + self.enable_eval = config['trainer'].get('enable_eval', True) + + def _train_epoch(self, epoch): + self.model.train() + total_samples = 0 + train_reader_cost = 0.0 + train_batch_cost = 0.0 + reader_start = time.time() + epoch_start = time.time() + train_loss = 0. + running_metric_text = runningScore(2) + + for i, batch in enumerate(self.train_loader): + profiler.add_profiler_step(self.profiler_options) + if i >= self.train_loader_len: + break + self.global_step += 1 + lr = self.optimizer.get_lr() + + cur_batch_size = batch['img'].shape[0] + + train_reader_cost += time.time() - reader_start + if self.amp: + with paddle.amp.auto_cast( + enable='gpu' in paddle.device.get_device(), + custom_white_list=self.amp.get('custom_white_list', []), + custom_black_list=self.amp.get('custom_black_list', []), + level=self.amp.get('level', 'O2')): + preds = self.model(batch['img']) + loss_dict = self.criterion(preds.astype(paddle.float32), batch) + scaled_loss = self.amp['scaler'].scale(loss_dict['loss']) + scaled_loss.backward() + self.amp['scaler'].minimize(self.optimizer, scaled_loss) + else: + preds = self.model(batch['img']) + loss_dict = self.criterion(preds, batch) + # backward + loss_dict['loss'].backward() + self.optimizer.step() + self.lr_scheduler.step() + self.optimizer.clear_grad() + + train_batch_time = time.time() - reader_start + train_batch_cost += train_batch_time + total_samples += cur_batch_size + + # acc iou + score_shrink_map = cal_text_score( + preds[:, 0, :, :], + batch['shrink_map'], + batch['shrink_mask'], + running_metric_text, + thred=self.config['post_processing']['args']['thresh']) + + # loss 和 acc 记录到日志 + loss_str = 'loss: {:.4f}, '.format(loss_dict['loss'].item()) + for idx, (key, value) in enumerate(loss_dict.items()): + loss_dict[key] = value.item() + if key == 'loss': + continue + loss_str += '{}: {:.4f}'.format(key, loss_dict[key]) + if idx < len(loss_dict) - 1: + loss_str += ', ' + + train_loss += loss_dict['loss'] + acc = score_shrink_map['Mean Acc'] + iou_shrink_map = score_shrink_map['Mean IoU'] + + if self.global_step % self.log_iter == 0: + self.logger_info( + '[{}/{}], [{}/{}], global_step: {}, ips: {:.1f} samples/sec, avg_reader_cost: {:.5f} s, avg_batch_cost: {:.5f} s, avg_samples: {}, acc: {:.4f}, iou_shrink_map: {:.4f}, {}lr:{:.6}, time:{:.2f}'. + format(epoch, self.epochs, i + 1, self.train_loader_len, + self.global_step, total_samples / train_batch_cost, + train_reader_cost / self.log_iter, train_batch_cost / + self.log_iter, total_samples / self.log_iter, acc, + iou_shrink_map, loss_str, lr, train_batch_cost)) + total_samples = 0 + train_reader_cost = 0.0 + train_batch_cost = 0.0 + + if self.visualdl_enable and paddle.distributed.get_rank() == 0: + # write tensorboard + for key, value in loss_dict.items(): + self.writer.add_scalar('TRAIN/LOSS/{}'.format(key), value, + self.global_step) + self.writer.add_scalar('TRAIN/ACC_IOU/acc', acc, + self.global_step) + self.writer.add_scalar('TRAIN/ACC_IOU/iou_shrink_map', + iou_shrink_map, self.global_step) + self.writer.add_scalar('TRAIN/lr', lr, self.global_step) + reader_start = time.time() + return { + 'train_loss': train_loss / self.train_loader_len, + 'lr': lr, + 'time': time.time() - epoch_start, + 'epoch': epoch + } + + def _eval(self, epoch): + self.model.eval() + raw_metrics = [] + total_frame = 0.0 + total_time = 0.0 + for i, batch in tqdm( + enumerate(self.validate_loader), + total=len(self.validate_loader), + desc='test model'): + with paddle.no_grad(): + start = time.time() + if self.amp: + with paddle.amp.auto_cast( + enable='gpu' in paddle.device.get_device(), + custom_white_list=self.amp.get('custom_white_list', + []), + custom_black_list=self.amp.get('custom_black_list', + []), + level=self.amp.get('level', 'O2')): + preds = self.model(batch['img']) + preds = preds.astype(paddle.float32) + else: + preds = self.model(batch['img']) + boxes, scores = self.post_process( + batch, + preds, + is_output_polygon=self.metric_cls.is_output_polygon) + total_frame += batch['img'].shape[0] + total_time += time.time() - start + raw_metric = self.metric_cls.validate_measure(batch, + (boxes, scores)) + raw_metrics.append(raw_metric) + metrics = self.metric_cls.gather_measure(raw_metrics) + self.logger_info('FPS:{}'.format(total_frame / total_time)) + return metrics['recall'].avg, metrics['precision'].avg, metrics[ + 'fmeasure'].avg + + def _on_epoch_finish(self): + self.logger_info('[{}/{}], train_loss: {:.4f}, time: {:.4f}, lr: {}'. + format(self.epoch_result['epoch'], self.epochs, self. + epoch_result['train_loss'], self.epoch_result[ + 'time'], self.epoch_result['lr'])) + net_save_path = '{}/model_latest.pth'.format(self.checkpoint_dir) + net_save_path_best = '{}/model_best.pth'.format(self.checkpoint_dir) + + if paddle.distributed.get_rank() == 0: + self._save_checkpoint(self.epoch_result['epoch'], net_save_path) + save_best = False + if self.validate_loader is not None and self.metric_cls is not None and self.enable_eval: # 使用f1作为最优模型指标 + recall, precision, hmean = self._eval(self.epoch_result[ + 'epoch']) + + if self.visualdl_enable: + self.writer.add_scalar('EVAL/recall', recall, + self.global_step) + self.writer.add_scalar('EVAL/precision', precision, + self.global_step) + self.writer.add_scalar('EVAL/hmean', hmean, + self.global_step) + self.logger_info( + 'test: recall: {:.6f}, precision: {:.6f}, hmean: {:.6f}'. + format(recall, precision, hmean)) + + if hmean >= self.metrics['hmean']: + save_best = True + self.metrics['train_loss'] = self.epoch_result['train_loss'] + self.metrics['hmean'] = hmean + self.metrics['precision'] = precision + self.metrics['recall'] = recall + self.metrics['best_model_epoch'] = self.epoch_result[ + 'epoch'] + else: + if self.epoch_result['train_loss'] <= self.metrics[ + 'train_loss']: + save_best = True + self.metrics['train_loss'] = self.epoch_result['train_loss'] + self.metrics['best_model_epoch'] = self.epoch_result[ + 'epoch'] + best_str = 'current best, ' + for k, v in self.metrics.items(): + best_str += '{}: {:.6f}, '.format(k, v) + self.logger_info(best_str) + if save_best: + import shutil + shutil.copy(net_save_path, net_save_path_best) + self.logger_info("Saving current best: {}".format( + net_save_path_best)) + else: + self.logger_info("Saving checkpoint: {}".format(net_save_path)) + + def _on_train_finish(self): + if self.enable_eval: + for k, v in self.metrics.items(): + self.logger_info('{}:{}'.format(k, v)) + self.logger_info('finish train') + + def _initialize_scheduler(self): + if self.config['lr_scheduler']['type'] == 'Polynomial': + self.config['lr_scheduler']['args']['epochs'] = self.config[ + 'trainer']['epochs'] + self.config['lr_scheduler']['args']['step_each_epoch'] = len( + self.train_loader) + self.lr_scheduler = Polynomial( + **self.config['lr_scheduler']['args'])() + else: + self.lr_scheduler = self._initialize('lr_scheduler', + paddle.optimizer.lr) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..194e0b82ff6576c3880914fad0492fde276cff33 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/23 21:58 +# @Author : zhoujun +from .util import * +from .metrics import * +from .schedulers import * +from .cal_recall.script import cal_recall_precison_f1 +from .ocr_metric import get_metric diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/cal_recall/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/cal_recall/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0db38a8a37f0a3d8fbd8c12a3e54457e41cf9360 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/cal_recall/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# @Time : 1/16/19 6:40 AM +# @Author : zhoujun +from .script import cal_recall_precison_f1 +__all__ = ['cal_recall_precison_f1'] diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/cal_recall/rrc_evaluation_funcs.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/cal_recall/rrc_evaluation_funcs.py new file mode 100644 index 0000000000000000000000000000000000000000..4e12ee66a07118c07559eebd655f5173e046696e --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/cal_recall/rrc_evaluation_funcs.py @@ -0,0 +1,479 @@ +#!/usr/bin/env python2 +#encoding: UTF-8 +import json +import sys +sys.path.append('./') +import zipfile +import re +import sys +import os +import codecs +import traceback +import numpy as np +from utils import order_points_clockwise + + +def print_help(): + sys.stdout.write( + 'Usage: python %s.py -g= -s= [-o= -p=]' + % sys.argv[0]) + sys.exit(2) + + +def load_zip_file_keys(file, fileNameRegExp=''): + """ + Returns an array with the entries of the ZIP file that match with the regular expression. + The key's are the names or the file or the capturing group definied in the fileNameRegExp + """ + try: + archive = zipfile.ZipFile(file, mode='r', allowZip64=True) + except: + raise Exception('Error loading the ZIP archive.') + + pairs = [] + + for name in archive.namelist(): + addFile = True + keyName = name + if fileNameRegExp != "": + m = re.match(fileNameRegExp, name) + if m == None: + addFile = False + else: + if len(m.groups()) > 0: + keyName = m.group(1) + + if addFile: + pairs.append(keyName) + + return pairs + + +def load_zip_file(file, fileNameRegExp='', allEntries=False): + """ + Returns an array with the contents (filtered by fileNameRegExp) of a ZIP file. + The key's are the names or the file or the capturing group definied in the fileNameRegExp + allEntries validates that all entries in the ZIP file pass the fileNameRegExp + """ + try: + archive = zipfile.ZipFile(file, mode='r', allowZip64=True) + except: + raise Exception('Error loading the ZIP archive') + + pairs = [] + for name in archive.namelist(): + addFile = True + keyName = name + if fileNameRegExp != "": + m = re.match(fileNameRegExp, name) + if m == None: + addFile = False + else: + if len(m.groups()) > 0: + keyName = m.group(1) + + if addFile: + pairs.append([keyName, archive.read(name)]) + else: + if allEntries: + raise Exception('ZIP entry not valid: %s' % name) + + return dict(pairs) + + +def load_folder_file(file, fileNameRegExp='', allEntries=False): + """ + Returns an array with the contents (filtered by fileNameRegExp) of a ZIP file. + The key's are the names or the file or the capturing group definied in the fileNameRegExp + allEntries validates that all entries in the ZIP file pass the fileNameRegExp + """ + pairs = [] + for name in os.listdir(file): + addFile = True + keyName = name + if fileNameRegExp != "": + m = re.match(fileNameRegExp, name) + if m == None: + addFile = False + else: + if len(m.groups()) > 0: + keyName = m.group(1) + + if addFile: + pairs.append([keyName, open(os.path.join(file, name)).read()]) + else: + if allEntries: + raise Exception('ZIP entry not valid: %s' % name) + + return dict(pairs) + + +def decode_utf8(raw): + """ + Returns a Unicode object on success, or None on failure + """ + try: + raw = codecs.decode(raw, 'utf-8', 'replace') + #extracts BOM if exists + raw = raw.encode('utf8') + if raw.startswith(codecs.BOM_UTF8): + raw = raw.replace(codecs.BOM_UTF8, '', 1) + return raw.decode('utf-8') + except: + return None + + +def validate_lines_in_file(fileName, + file_contents, + CRLF=True, + LTRB=True, + withTranscription=False, + withConfidence=False, + imWidth=0, + imHeight=0): + """ + This function validates that all lines of the file calling the Line validation function for each line + """ + utf8File = decode_utf8(file_contents) + if (utf8File is None): + raise Exception("The file %s is not UTF-8" % fileName) + + lines = utf8File.split("\r\n" if CRLF else "\n") + for line in lines: + line = line.replace("\r", "").replace("\n", "") + if (line != ""): + try: + validate_tl_line(line, LTRB, withTranscription, withConfidence, + imWidth, imHeight) + except Exception as e: + raise Exception( + ("Line in sample not valid. Sample: %s Line: %s Error: %s" % + (fileName, line, str(e))).encode('utf-8', 'replace')) + + +def validate_tl_line(line, + LTRB=True, + withTranscription=True, + withConfidence=True, + imWidth=0, + imHeight=0): + """ + Validate the format of the line. If the line is not valid an exception will be raised. + If maxWidth and maxHeight are specified, all points must be inside the imgage bounds. + Posible values are: + LTRB=True: xmin,ymin,xmax,ymax[,confidence][,transcription] + LTRB=False: x1,y1,x2,y2,x3,y3,x4,y4[,confidence][,transcription] + """ + get_tl_line_values(line, LTRB, withTranscription, withConfidence, imWidth, + imHeight) + + +def get_tl_line_values(line, + LTRB=True, + withTranscription=False, + withConfidence=False, + imWidth=0, + imHeight=0): + """ + Validate the format of the line. If the line is not valid an exception will be raised. + If maxWidth and maxHeight are specified, all points must be inside the imgage bounds. + Posible values are: + LTRB=True: xmin,ymin,xmax,ymax[,confidence][,transcription] + LTRB=False: x1,y1,x2,y2,x3,y3,x4,y4[,confidence][,transcription] + Returns values from a textline. Points , [Confidences], [Transcriptions] + """ + confidence = 0.0 + transcription = "" + points = [] + + numPoints = 4 + + if LTRB: + + numPoints = 4 + + if withTranscription and withConfidence: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-1].?[0-9]*)\s*,(.*)$', + line) + if m == None: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-1].?[0-9]*)\s*,(.*)$', + line) + raise Exception( + "Format incorrect. Should be: xmin,ymin,xmax,ymax,confidence,transcription" + ) + elif withConfidence: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-1].?[0-9]*)\s*$', + line) + if m == None: + raise Exception( + "Format incorrect. Should be: xmin,ymin,xmax,ymax,confidence" + ) + elif withTranscription: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,(.*)$', + line) + if m == None: + raise Exception( + "Format incorrect. Should be: xmin,ymin,xmax,ymax,transcription" + ) + else: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,?\s*$', + line) + if m == None: + raise Exception( + "Format incorrect. Should be: xmin,ymin,xmax,ymax") + + xmin = int(m.group(1)) + ymin = int(m.group(2)) + xmax = int(m.group(3)) + ymax = int(m.group(4)) + if (xmax < xmin): + raise Exception("Xmax value (%s) not valid (Xmax < Xmin)." % (xmax)) + if (ymax < ymin): + raise Exception("Ymax value (%s) not valid (Ymax < Ymin)." % + (ymax)) + + points = [float(m.group(i)) for i in range(1, (numPoints + 1))] + + if (imWidth > 0 and imHeight > 0): + validate_point_inside_bounds(xmin, ymin, imWidth, imHeight) + validate_point_inside_bounds(xmax, ymax, imWidth, imHeight) + + else: + + numPoints = 8 + + if withTranscription and withConfidence: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-1].?[0-9]*)\s*,(.*)$', + line) + if m == None: + raise Exception( + "Format incorrect. Should be: x1,y1,x2,y2,x3,y3,x4,y4,confidence,transcription" + ) + elif withConfidence: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*([0-1].?[0-9]*)\s*$', + line) + if m == None: + raise Exception( + "Format incorrect. Should be: x1,y1,x2,y2,x3,y3,x4,y4,confidence" + ) + elif withTranscription: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,(.*)$', + line) + if m == None: + raise Exception( + "Format incorrect. Should be: x1,y1,x2,y2,x3,y3,x4,y4,transcription" + ) + else: + m = re.match( + r'^\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*,\s*(-?[0-9]+)\s*$', + line) + if m == None: + raise Exception( + "Format incorrect. Should be: x1,y1,x2,y2,x3,y3,x4,y4") + + points = [float(m.group(i)) for i in range(1, (numPoints + 1))] + + points = order_points_clockwise(np.array(points).reshape(-1, + 2)).reshape(-1) + validate_clockwise_points(points) + + if (imWidth > 0 and imHeight > 0): + validate_point_inside_bounds(points[0], points[1], imWidth, + imHeight) + validate_point_inside_bounds(points[2], points[3], imWidth, + imHeight) + validate_point_inside_bounds(points[4], points[5], imWidth, + imHeight) + validate_point_inside_bounds(points[6], points[7], imWidth, + imHeight) + + if withConfidence: + try: + confidence = float(m.group(numPoints + 1)) + except ValueError: + raise Exception("Confidence value must be a float") + + if withTranscription: + posTranscription = numPoints + (2 if withConfidence else 1) + transcription = m.group(posTranscription) + m2 = re.match(r'^\s*\"(.*)\"\s*$', transcription) + if m2 != None: #Transcription with double quotes, we extract the value and replace escaped characters + transcription = m2.group(1).replace("\\\\", "\\").replace("\\\"", + "\"") + + return points, confidence, transcription + + +def validate_point_inside_bounds(x, y, imWidth, imHeight): + if (x < 0 or x > imWidth): + raise Exception("X value (%s) not valid. Image dimensions: (%s,%s)" % + (xmin, imWidth, imHeight)) + if (y < 0 or y > imHeight): + raise Exception( + "Y value (%s) not valid. Image dimensions: (%s,%s) Sample: %s Line:%s" + % (ymin, imWidth, imHeight)) + + +def validate_clockwise_points(points): + """ + Validates that the points that the 4 points that dlimite a polygon are in clockwise order. + """ + + if len(points) != 8: + raise Exception("Points list not valid." + str(len(points))) + + point = [[int(points[0]), int(points[1])], + [int(points[2]), int(points[3])], + [int(points[4]), int(points[5])], + [int(points[6]), int(points[7])]] + edge = [(point[1][0] - point[0][0]) * (point[1][1] + point[0][1]), + (point[2][0] - point[1][0]) * (point[2][1] + point[1][1]), + (point[3][0] - point[2][0]) * (point[3][1] + point[2][1]), + (point[0][0] - point[3][0]) * (point[0][1] + point[3][1])] + + summatory = edge[0] + edge[1] + edge[2] + edge[3] + if summatory > 0: + raise Exception( + "Points are not clockwise. The coordinates of bounding quadrilaterals have to be given in clockwise order. Regarding the correct interpretation of 'clockwise' remember that the image coordinate system used is the standard one, with the image origin at the upper left, the X axis extending to the right and Y axis extending downwards." + ) + + +def get_tl_line_values_from_file_contents(content, + CRLF=True, + LTRB=True, + withTranscription=False, + withConfidence=False, + imWidth=0, + imHeight=0, + sort_by_confidences=True): + """ + Returns all points, confindences and transcriptions of a file in lists. Valid line formats: + xmin,ymin,xmax,ymax,[confidence],[transcription] + x1,y1,x2,y2,x3,y3,x4,y4,[confidence],[transcription] + """ + pointsList = [] + transcriptionsList = [] + confidencesList = [] + + lines = content.split("\r\n" if CRLF else "\n") + for line in lines: + line = line.replace("\r", "").replace("\n", "") + if (line != ""): + points, confidence, transcription = get_tl_line_values( + line, LTRB, withTranscription, withConfidence, imWidth, + imHeight) + pointsList.append(points) + transcriptionsList.append(transcription) + confidencesList.append(confidence) + + if withConfidence and len(confidencesList) > 0 and sort_by_confidences: + import numpy as np + sorted_ind = np.argsort(-np.array(confidencesList)) + confidencesList = [confidencesList[i] for i in sorted_ind] + pointsList = [pointsList[i] for i in sorted_ind] + transcriptionsList = [transcriptionsList[i] for i in sorted_ind] + + return pointsList, confidencesList, transcriptionsList + + +def main_evaluation(p, + default_evaluation_params_fn, + validate_data_fn, + evaluate_method_fn, + show_result=True, + per_sample=True): + """ + This process validates a method, evaluates it and if it succed generates a ZIP file with a JSON entry for each sample. + Params: + p: Dictionary of parmeters with the GT/submission locations. If None is passed, the parameters send by the system are used. + default_evaluation_params_fn: points to a function that returns a dictionary with the default parameters used for the evaluation + validate_data_fn: points to a method that validates the corrct format of the submission + evaluate_method_fn: points to a function that evaluated the submission and return a Dictionary with the results + """ + evalParams = default_evaluation_params_fn() + if 'p' in p.keys(): + evalParams.update(p['p'] if isinstance(p['p'], dict) else json.loads(p[ + 'p'][1:-1])) + + resDict = { + 'calculated': True, + 'Message': '', + 'method': '{}', + 'per_sample': '{}' + } + try: + # validate_data_fn(p['g'], p['s'], evalParams) + evalData = evaluate_method_fn(p['g'], p['s'], evalParams) + resDict.update(evalData) + + except Exception as e: + traceback.print_exc() + resDict['Message'] = str(e) + resDict['calculated'] = False + + if 'o' in p: + if not os.path.exists(p['o']): + os.makedirs(p['o']) + + resultsOutputname = p['o'] + '/results.zip' + outZip = zipfile.ZipFile(resultsOutputname, mode='w', allowZip64=True) + + del resDict['per_sample'] + if 'output_items' in resDict.keys(): + del resDict['output_items'] + + outZip.writestr('method.json', json.dumps(resDict)) + + if not resDict['calculated']: + if show_result: + sys.stderr.write('Error!\n' + resDict['Message'] + '\n\n') + if 'o' in p: + outZip.close() + return resDict + + if 'o' in p: + if per_sample == True: + for k, v in evalData['per_sample'].iteritems(): + outZip.writestr(k + '.json', json.dumps(v)) + + if 'output_items' in evalData.keys(): + for k, v in evalData['output_items'].iteritems(): + outZip.writestr(k, v) + + outZip.close() + + if show_result: + sys.stdout.write("Calculated!") + sys.stdout.write(json.dumps(resDict['method'])) + + return resDict + + +def main_validation(default_evaluation_params_fn, validate_data_fn): + """ + This process validates a method + Params: + default_evaluation_params_fn: points to a function that returns a dictionary with the default parameters used for the evaluation + validate_data_fn: points to a method that validates the corrct format of the submission + """ + try: + p = dict([s[1:].split('=') for s in sys.argv[1:]]) + evalParams = default_evaluation_params_fn() + if 'p' in p.keys(): + evalParams.update(p['p'] if isinstance(p['p'], dict) else + json.loads(p['p'][1:-1])) + + validate_data_fn(p['g'], p['s'], evalParams) + print('SUCCESS') + sys.exit(0) + except Exception as e: + print(str(e)) + sys.exit(101) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/cal_recall/script.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/cal_recall/script.py new file mode 100644 index 0000000000000000000000000000000000000000..3b2f3916f62f191ae1b6b658edad9963243babf7 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/cal_recall/script.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from collections import namedtuple +from . import rrc_evaluation_funcs +import Polygon as plg +import numpy as np + + +def default_evaluation_params(): + """ + default_evaluation_params: Default parameters to use for the validation and evaluation. + """ + return { + 'IOU_CONSTRAINT': 0.5, + 'AREA_PRECISION_CONSTRAINT': 0.5, + 'GT_SAMPLE_NAME_2_ID': 'gt_img_([0-9]+).txt', + 'DET_SAMPLE_NAME_2_ID': 'res_img_([0-9]+).txt', + 'LTRB': + False, # LTRB:2points(left,top,right,bottom) or 4 points(x1,y1,x2,y2,x3,y3,x4,y4) + 'CRLF': False, # Lines are delimited by Windows CRLF format + 'CONFIDENCES': + False, # Detections must include confidence value. AP will be calculated + 'PER_SAMPLE_RESULTS': + True # Generate per sample results and produce data for visualization + } + + +def validate_data(gtFilePath, submFilePath, evaluationParams): + """ + Method validate_data: validates that all files in the results folder are correct (have the correct name contents). + Validates also that there are no missing files in the folder. + If some error detected, the method raises the error + """ + gt = rrc_evaluation_funcs.load_folder_file( + gtFilePath, evaluationParams['GT_SAMPLE_NAME_2_ID']) + + subm = rrc_evaluation_funcs.load_folder_file( + submFilePath, evaluationParams['DET_SAMPLE_NAME_2_ID'], True) + + # Validate format of GroundTruth + for k in gt: + rrc_evaluation_funcs.validate_lines_in_file( + k, gt[k], evaluationParams['CRLF'], evaluationParams['LTRB'], True) + + # Validate format of results + for k in subm: + if (k in gt) == False: + raise Exception("The sample %s not present in GT" % k) + + rrc_evaluation_funcs.validate_lines_in_file( + k, subm[k], evaluationParams['CRLF'], evaluationParams['LTRB'], + False, evaluationParams['CONFIDENCES']) + + +def evaluate_method(gtFilePath, submFilePath, evaluationParams): + """ + Method evaluate_method: evaluate method and returns the results + Results. Dictionary with the following values: + - method (required) Global method metrics. Ex: { 'Precision':0.8,'Recall':0.9 } + - samples (optional) Per sample metrics. Ex: {'sample1' : { 'Precision':0.8,'Recall':0.9 } , 'sample2' : { 'Precision':0.8,'Recall':0.9 } + """ + + def polygon_from_points(points): + """ + Returns a Polygon object to use with the Polygon2 class from a list of 8 points: x1,y1,x2,y2,x3,y3,x4,y4 + """ + resBoxes = np.empty([1, 8], dtype='int32') + resBoxes[0, 0] = int(points[0]) + resBoxes[0, 4] = int(points[1]) + resBoxes[0, 1] = int(points[2]) + resBoxes[0, 5] = int(points[3]) + resBoxes[0, 2] = int(points[4]) + resBoxes[0, 6] = int(points[5]) + resBoxes[0, 3] = int(points[6]) + resBoxes[0, 7] = int(points[7]) + pointMat = resBoxes[0].reshape([2, 4]).T + return plg.Polygon(pointMat) + + def rectangle_to_polygon(rect): + resBoxes = np.empty([1, 8], dtype='int32') + resBoxes[0, 0] = int(rect.xmin) + resBoxes[0, 4] = int(rect.ymax) + resBoxes[0, 1] = int(rect.xmin) + resBoxes[0, 5] = int(rect.ymin) + resBoxes[0, 2] = int(rect.xmax) + resBoxes[0, 6] = int(rect.ymin) + resBoxes[0, 3] = int(rect.xmax) + resBoxes[0, 7] = int(rect.ymax) + + pointMat = resBoxes[0].reshape([2, 4]).T + + return plg.Polygon(pointMat) + + def rectangle_to_points(rect): + points = [ + int(rect.xmin), int(rect.ymax), int(rect.xmax), int(rect.ymax), + int(rect.xmax), int(rect.ymin), int(rect.xmin), int(rect.ymin) + ] + return points + + def get_union(pD, pG): + areaA = pD.area() + areaB = pG.area() + return areaA + areaB - get_intersection(pD, pG) + + def get_intersection_over_union(pD, pG): + try: + return get_intersection(pD, pG) / get_union(pD, pG) + except: + return 0 + + def get_intersection(pD, pG): + pInt = pD & pG + if len(pInt) == 0: + return 0 + return pInt.area() + + def compute_ap(confList, matchList, numGtCare): + correct = 0 + AP = 0 + if len(confList) > 0: + confList = np.array(confList) + matchList = np.array(matchList) + sorted_ind = np.argsort(-confList) + confList = confList[sorted_ind] + matchList = matchList[sorted_ind] + for n in range(len(confList)): + match = matchList[n] + if match: + correct += 1 + AP += float(correct) / (n + 1) + + if numGtCare > 0: + AP /= numGtCare + + return AP + + perSampleMetrics = {} + + matchedSum = 0 + + Rectangle = namedtuple('Rectangle', 'xmin ymin xmax ymax') + + gt = rrc_evaluation_funcs.load_folder_file( + gtFilePath, evaluationParams['GT_SAMPLE_NAME_2_ID']) + subm = rrc_evaluation_funcs.load_folder_file( + submFilePath, evaluationParams['DET_SAMPLE_NAME_2_ID'], True) + + numGlobalCareGt = 0 + numGlobalCareDet = 0 + + arrGlobalConfidences = [] + arrGlobalMatches = [] + + for resFile in gt: + + gtFile = gt[resFile] # rrc_evaluation_funcs.decode_utf8(gt[resFile]) + recall = 0 + precision = 0 + hmean = 0 + + detMatched = 0 + + iouMat = np.empty([1, 1]) + + gtPols = [] + detPols = [] + + gtPolPoints = [] + detPolPoints = [] + + # Array of Ground Truth Polygons' keys marked as don't Care + gtDontCarePolsNum = [] + # Array of Detected Polygons' matched with a don't Care GT + detDontCarePolsNum = [] + + pairs = [] + detMatchedNums = [] + + arrSampleConfidences = [] + arrSampleMatch = [] + sampleAP = 0 + + evaluationLog = "" + + pointsList, _, transcriptionsList = rrc_evaluation_funcs.get_tl_line_values_from_file_contents( + gtFile, evaluationParams['CRLF'], evaluationParams['LTRB'], True, + False) + for n in range(len(pointsList)): + points = pointsList[n] + transcription = transcriptionsList[n] + dontCare = transcription == "###" + if evaluationParams['LTRB']: + gtRect = Rectangle(*points) + gtPol = rectangle_to_polygon(gtRect) + else: + gtPol = polygon_from_points(points) + gtPols.append(gtPol) + gtPolPoints.append(points) + if dontCare: + gtDontCarePolsNum.append(len(gtPols) - 1) + + evaluationLog += "GT polygons: " + str(len(gtPols)) + ( + " (" + str(len(gtDontCarePolsNum)) + " don't care)\n" + if len(gtDontCarePolsNum) > 0 else "\n") + + if resFile in subm: + + detFile = subm[ + resFile] # rrc_evaluation_funcs.decode_utf8(subm[resFile]) + + pointsList, confidencesList, _ = rrc_evaluation_funcs.get_tl_line_values_from_file_contents( + detFile, evaluationParams['CRLF'], evaluationParams['LTRB'], + False, evaluationParams['CONFIDENCES']) + for n in range(len(pointsList)): + points = pointsList[n] + + if evaluationParams['LTRB']: + detRect = Rectangle(*points) + detPol = rectangle_to_polygon(detRect) + else: + detPol = polygon_from_points(points) + detPols.append(detPol) + detPolPoints.append(points) + if len(gtDontCarePolsNum) > 0: + for dontCarePol in gtDontCarePolsNum: + dontCarePol = gtPols[dontCarePol] + intersected_area = get_intersection(dontCarePol, detPol) + pdDimensions = detPol.area() + precision = 0 if pdDimensions == 0 else intersected_area / pdDimensions + if (precision > + evaluationParams['AREA_PRECISION_CONSTRAINT']): + detDontCarePolsNum.append(len(detPols) - 1) + break + + evaluationLog += "DET polygons: " + str(len(detPols)) + ( + " (" + str(len(detDontCarePolsNum)) + " don't care)\n" + if len(detDontCarePolsNum) > 0 else "\n") + + if len(gtPols) > 0 and len(detPols) > 0: + # Calculate IoU and precision matrixs + outputShape = [len(gtPols), len(detPols)] + iouMat = np.empty(outputShape) + gtRectMat = np.zeros(len(gtPols), np.int8) + detRectMat = np.zeros(len(detPols), np.int8) + for gtNum in range(len(gtPols)): + for detNum in range(len(detPols)): + pG = gtPols[gtNum] + pD = detPols[detNum] + iouMat[gtNum, detNum] = get_intersection_over_union(pD, + pG) + + for gtNum in range(len(gtPols)): + for detNum in range(len(detPols)): + if gtRectMat[gtNum] == 0 and detRectMat[ + detNum] == 0 and gtNum not in gtDontCarePolsNum and detNum not in detDontCarePolsNum: + if iouMat[gtNum, detNum] > evaluationParams[ + 'IOU_CONSTRAINT']: + gtRectMat[gtNum] = 1 + detRectMat[detNum] = 1 + detMatched += 1 + pairs.append({'gt': gtNum, 'det': detNum}) + detMatchedNums.append(detNum) + evaluationLog += "Match GT #" + str( + gtNum) + " with Det #" + str(detNum) + "\n" + + if evaluationParams['CONFIDENCES']: + for detNum in range(len(detPols)): + if detNum not in detDontCarePolsNum: + # we exclude the don't care detections + match = detNum in detMatchedNums + + arrSampleConfidences.append(confidencesList[detNum]) + arrSampleMatch.append(match) + + arrGlobalConfidences.append(confidencesList[detNum]) + arrGlobalMatches.append(match) + + numGtCare = (len(gtPols) - len(gtDontCarePolsNum)) + numDetCare = (len(detPols) - len(detDontCarePolsNum)) + if numGtCare == 0: + recall = float(1) + precision = float(0) if numDetCare > 0 else float(1) + sampleAP = precision + else: + recall = float(detMatched) / numGtCare + precision = 0 if numDetCare == 0 else float(detMatched) / numDetCare + if evaluationParams['CONFIDENCES'] and evaluationParams[ + 'PER_SAMPLE_RESULTS']: + sampleAP = compute_ap(arrSampleConfidences, arrSampleMatch, + numGtCare) + + hmean = 0 if (precision + recall) == 0 else 2.0 * precision * recall / ( + precision + recall) + + matchedSum += detMatched + numGlobalCareGt += numGtCare + numGlobalCareDet += numDetCare + + if evaluationParams['PER_SAMPLE_RESULTS']: + perSampleMetrics[resFile] = { + 'precision': precision, + 'recall': recall, + 'hmean': hmean, + 'pairs': pairs, + 'AP': sampleAP, + 'iouMat': [] if len(detPols) > 100 else iouMat.tolist(), + 'gtPolPoints': gtPolPoints, + 'detPolPoints': detPolPoints, + 'gtDontCare': gtDontCarePolsNum, + 'detDontCare': detDontCarePolsNum, + 'evaluationParams': evaluationParams, + 'evaluationLog': evaluationLog + } + + # Compute MAP and MAR + AP = 0 + if evaluationParams['CONFIDENCES']: + AP = compute_ap(arrGlobalConfidences, arrGlobalMatches, numGlobalCareGt) + + methodRecall = 0 if numGlobalCareGt == 0 else float( + matchedSum) / numGlobalCareGt + methodPrecision = 0 if numGlobalCareDet == 0 else float( + matchedSum) / numGlobalCareDet + methodHmean = 0 if methodRecall + methodPrecision == 0 else 2 * methodRecall * methodPrecision / ( + methodRecall + methodPrecision) + + methodMetrics = { + 'precision': methodPrecision, + 'recall': methodRecall, + 'hmean': methodHmean, + 'AP': AP + } + + resDict = { + 'calculated': True, + 'Message': '', + 'method': methodMetrics, + 'per_sample': perSampleMetrics + } + + return resDict + + +def cal_recall_precison_f1(gt_path, result_path, show_result=False): + p = {'g': gt_path, 's': result_path} + result = rrc_evaluation_funcs.main_evaluation(p, default_evaluation_params, + validate_data, + evaluate_method, show_result) + return result['method'] diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/compute_mean_std.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/compute_mean_std.py new file mode 100644 index 0000000000000000000000000000000000000000..5d0ab5cd23d66e4070c336a93abebbedad6028b1 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/compute_mean_std.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/12/7 14:46 +# @Author : zhoujun + +import numpy as np +import cv2 +import os +import random +from tqdm import tqdm +# calculate means and std +train_txt_path = './train_val_list.txt' + +CNum = 10000 # 挑选多少图片进行计算 + +img_h, img_w = 640, 640 +imgs = np.zeros([img_w, img_h, 3, 1]) +means, stdevs = [], [] + +with open(train_txt_path, 'r') as f: + lines = f.readlines() + random.shuffle(lines) # shuffle , 随机挑选图片 + + for i in tqdm(range(CNum)): + img_path = lines[i].split('\t')[0] + + img = cv2.imread(img_path) + img = cv2.resize(img, (img_h, img_w)) + img = img[:, :, :, np.newaxis] + + imgs = np.concatenate((imgs, img), axis=3) +# print(i) + +imgs = imgs.astype(np.float32) / 255. + +for i in tqdm(range(3)): + pixels = imgs[:, :, i, :].ravel() # 拉成一行 + means.append(np.mean(pixels)) + stdevs.append(np.std(pixels)) + +# cv2 读取的图像格式为BGR,PIL/Skimage读取到的都是RGB不用转 +means.reverse() # BGR --> RGB +stdevs.reverse() + +print("normMean = {}".format(means)) +print("normStd = {}".format(stdevs)) +print('transforms.Normalize(normMean = {}, normStd = {})'.format(means, stdevs)) \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/make_trainfile.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/make_trainfile.py new file mode 100644 index 0000000000000000000000000000000000000000..9b7ae70ff7aeea46b7b36ab4a177baa0108d72c3 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/make_trainfile.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/24 12:06 +# @Author : zhoujun +import os +import glob +import pathlib + +data_path = r'test' +# data_path/img 存放图片 +# data_path/gt 存放标签文件 + +f_w = open(os.path.join(data_path, 'test.txt'), 'w', encoding='utf8') +for img_path in glob.glob(data_path + '/img/*.jpg', recursive=True): + d = pathlib.Path(img_path) + label_path = os.path.join(data_path, 'gt', ('gt_' + str(d.stem) + '.txt')) + if os.path.exists(img_path) and os.path.exists(label_path): + print(img_path, label_path) + else: + print('不存在', img_path, label_path) + f_w.write('{}\t{}\n'.format(img_path, label_path)) +f_w.close() \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/metrics.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..e9c54b8d2e8d6acc01aef62a43c27f018f333435 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/metrics.py @@ -0,0 +1,58 @@ +# Adapted from score written by wkentaro +# https://github.com/wkentaro/pytorch-fcn/blob/master/torchfcn/utils.py + +import numpy as np + + +class runningScore(object): + def __init__(self, n_classes): + self.n_classes = n_classes + self.confusion_matrix = np.zeros((n_classes, n_classes)) + + def _fast_hist(self, label_true, label_pred, n_class): + mask = (label_true >= 0) & (label_true < n_class) + + if np.sum((label_pred[mask] < 0)) > 0: + print(label_pred[label_pred < 0]) + hist = np.bincount( + n_class * label_true[mask].astype(int) + label_pred[mask], + minlength=n_class**2).reshape(n_class, n_class) + return hist + + def update(self, label_trues, label_preds): + # print label_trues.dtype, label_preds.dtype + for lt, lp in zip(label_trues, label_preds): + try: + self.confusion_matrix += self._fast_hist(lt.flatten(), + lp.flatten(), + self.n_classes) + except: + pass + + def get_scores(self): + """Returns accuracy score evaluation result. + - overall accuracy + - mean accuracy + - mean IU + - fwavacc + """ + hist = self.confusion_matrix + acc = np.diag(hist).sum() / (hist.sum() + 0.0001) + acc_cls = np.diag(hist) / (hist.sum(axis=1) + 0.0001) + acc_cls = np.nanmean(acc_cls) + iu = np.diag(hist) / ( + hist.sum(axis=1) + hist.sum(axis=0) - np.diag(hist) + 0.0001) + mean_iu = np.nanmean(iu) + freq = hist.sum(axis=1) / (hist.sum() + 0.0001) + fwavacc = (freq[freq > 0] * iu[freq > 0]).sum() + cls_iu = dict(zip(range(self.n_classes), iu)) + + return { + 'Overall Acc': acc, + 'Mean Acc': acc_cls, + 'FreqW Acc': fwavacc, + 'Mean IoU': mean_iu, + }, cls_iu + + def reset(self): + self.confusion_matrix = np.zeros((self.n_classes, self.n_classes)) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3e7c51cf0651a25b83e29016f4126461828ff887 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/12/5 15:36 +# @Author : zhoujun +from .icdar2015 import QuadMetric + + +def get_metric(config): + try: + if 'args' not in config: + args = {} + else: + args = config['args'] + if isinstance(args, dict): + cls = eval(config['type'])(**args) + else: + cls = eval(config['type'])(args) + return cls + except: + return None \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..375ae557e9fdf1120fa79b412de21434c1d71896 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/12/5 15:36 +# @Author : zhoujun + +from .quad_metric import QuadMetric \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/__init__.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/deteval.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/deteval.py new file mode 100644 index 0000000000000000000000000000000000000000..c5dcfc4b96a9802200e08467cbd937483960a0fc --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/deteval.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import math +from collections import namedtuple +import numpy as np +from shapely.geometry import Polygon + + +class DetectionDetEvalEvaluator(object): + def __init__(self, + area_recall_constraint=0.8, + area_precision_constraint=0.4, + ev_param_ind_center_diff_thr=1, + mtype_oo_o=1.0, + mtype_om_o=0.8, + mtype_om_m=1.0): + + self.area_recall_constraint = area_recall_constraint + self.area_precision_constraint = area_precision_constraint + self.ev_param_ind_center_diff_thr = ev_param_ind_center_diff_thr + self.mtype_oo_o = mtype_oo_o + self.mtype_om_o = mtype_om_o + self.mtype_om_m = mtype_om_m + + def evaluate_image(self, gt, pred): + def get_union(pD, pG): + return Polygon(pD).union(Polygon(pG)).area + + def get_intersection_over_union(pD, pG): + return get_intersection(pD, pG) / get_union(pD, pG) + + def get_intersection(pD, pG): + return Polygon(pD).intersection(Polygon(pG)).area + + def one_to_one_match(row, col): + cont = 0 + for j in range(len(recallMat[0])): + if recallMat[row, + j] >= self.area_recall_constraint and precisionMat[ + row, j] >= self.area_precision_constraint: + cont = cont + 1 + if (cont != 1): + return False + cont = 0 + for i in range(len(recallMat)): + if recallMat[ + i, col] >= self.area_recall_constraint and precisionMat[ + i, col] >= self.area_precision_constraint: + cont = cont + 1 + if (cont != 1): + return False + + if recallMat[row, + col] >= self.area_recall_constraint and precisionMat[ + row, col] >= self.area_precision_constraint: + return True + return False + + def num_overlaps_gt(gtNum): + cont = 0 + for detNum in range(len(detRects)): + if detNum not in detDontCareRectsNum: + if recallMat[gtNum, detNum] > 0: + cont = cont + 1 + return cont + + def num_overlaps_det(detNum): + cont = 0 + for gtNum in range(len(recallMat)): + if gtNum not in gtDontCareRectsNum: + if recallMat[gtNum, detNum] > 0: + cont = cont + 1 + return cont + + def is_single_overlap(row, col): + if num_overlaps_gt(row) == 1 and num_overlaps_det(col) == 1: + return True + else: + return False + + def one_to_many_match(gtNum): + many_sum = 0 + detRects = [] + for detNum in range(len(recallMat[0])): + if gtRectMat[gtNum] == 0 and detRectMat[ + detNum] == 0 and detNum not in detDontCareRectsNum: + if precisionMat[gtNum, + detNum] >= self.area_precision_constraint: + many_sum += recallMat[gtNum, detNum] + detRects.append(detNum) + if round(many_sum, 4) >= self.area_recall_constraint: + return True, detRects + else: + return False, [] + + def many_to_one_match(detNum): + many_sum = 0 + gtRects = [] + for gtNum in range(len(recallMat)): + if gtRectMat[gtNum] == 0 and detRectMat[ + detNum] == 0 and gtNum not in gtDontCareRectsNum: + if recallMat[gtNum, detNum] >= self.area_recall_constraint: + many_sum += precisionMat[gtNum, detNum] + gtRects.append(gtNum) + if round(many_sum, 4) >= self.area_precision_constraint: + return True, gtRects + else: + return False, [] + + def center_distance(r1, r2): + return ((np.mean(r1, axis=0) - np.mean(r2, axis=0))**2).sum()**0.5 + + def diag(r): + r = np.array(r) + return ((r[:, 0].max() - r[:, 0].min())**2 + + (r[:, 1].max() - r[:, 1].min())**2)**0.5 + + perSampleMetrics = {} + + recall = 0 + precision = 0 + hmean = 0 + recallAccum = 0. + precisionAccum = 0. + gtRects = [] + detRects = [] + gtPolPoints = [] + detPolPoints = [] + gtDontCareRectsNum = [ + ] #Array of Ground Truth Rectangles' keys marked as don't Care + detDontCareRectsNum = [ + ] #Array of Detected Rectangles' matched with a don't Care GT + pairs = [] + evaluationLog = "" + + recallMat = np.empty([1, 1]) + precisionMat = np.empty([1, 1]) + + for n in range(len(gt)): + points = gt[n]['points'] + # transcription = gt[n]['text'] + dontCare = gt[n]['ignore'] + + if not Polygon(points).is_valid or not Polygon(points).is_simple: + continue + + gtRects.append(points) + gtPolPoints.append(points) + if dontCare: + gtDontCareRectsNum.append(len(gtRects) - 1) + + evaluationLog += "GT rectangles: " + str(len(gtRects)) + ( + " (" + str(len(gtDontCareRectsNum)) + " don't care)\n" + if len(gtDontCareRectsNum) > 0 else "\n") + + for n in range(len(pred)): + points = pred[n]['points'] + + if not Polygon(points).is_valid or not Polygon(points).is_simple: + continue + + detRect = points + detRects.append(detRect) + detPolPoints.append(points) + if len(gtDontCareRectsNum) > 0: + for dontCareRectNum in gtDontCareRectsNum: + dontCareRect = gtRects[dontCareRectNum] + intersected_area = get_intersection(dontCareRect, detRect) + rdDimensions = Polygon(detRect).area + if (rdDimensions == 0): + precision = 0 + else: + precision = intersected_area / rdDimensions + if (precision > self.area_precision_constraint): + detDontCareRectsNum.append(len(detRects) - 1) + break + + evaluationLog += "DET rectangles: " + str(len(detRects)) + ( + " (" + str(len(detDontCareRectsNum)) + " don't care)\n" + if len(detDontCareRectsNum) > 0 else "\n") + + if len(gtRects) == 0: + recall = 1 + precision = 0 if len(detRects) > 0 else 1 + + if len(detRects) > 0: + #Calculate recall and precision matrixs + outputShape = [len(gtRects), len(detRects)] + recallMat = np.empty(outputShape) + precisionMat = np.empty(outputShape) + gtRectMat = np.zeros(len(gtRects), np.int8) + detRectMat = np.zeros(len(detRects), np.int8) + for gtNum in range(len(gtRects)): + for detNum in range(len(detRects)): + rG = gtRects[gtNum] + rD = detRects[detNum] + intersected_area = get_intersection(rG, rD) + rgDimensions = Polygon(rG).area + rdDimensions = Polygon(rD).area + recallMat[ + gtNum, + detNum] = 0 if rgDimensions == 0 else intersected_area / rgDimensions + precisionMat[ + gtNum, + detNum] = 0 if rdDimensions == 0 else intersected_area / rdDimensions + + # Find one-to-one matches + evaluationLog += "Find one-to-one matches\n" + for gtNum in range(len(gtRects)): + for detNum in range(len(detRects)): + if gtRectMat[gtNum] == 0 and detRectMat[ + detNum] == 0 and gtNum not in gtDontCareRectsNum and detNum not in detDontCareRectsNum: + match = one_to_one_match(gtNum, detNum) + if match is True: + #in deteval we have to make other validation before mark as one-to-one + if is_single_overlap(gtNum, detNum) is True: + rG = gtRects[gtNum] + rD = detRects[detNum] + normDist = center_distance(rG, rD) + normDist /= diag(rG) + diag(rD) + normDist *= 2.0 + if normDist < self.ev_param_ind_center_diff_thr: + gtRectMat[gtNum] = 1 + detRectMat[detNum] = 1 + recallAccum += self.mtype_oo_o + precisionAccum += self.mtype_oo_o + pairs.append({ + 'gt': gtNum, + 'det': detNum, + 'type': 'OO' + }) + evaluationLog += "Match GT #" + str( + gtNum) + " with Det #" + str( + detNum) + "\n" + else: + evaluationLog += "Match Discarded GT #" + str( + gtNum) + " with Det #" + str( + detNum) + " normDist: " + str( + normDist) + " \n" + else: + evaluationLog += "Match Discarded GT #" + str( + gtNum) + " with Det #" + str( + detNum) + " not single overlap\n" + # Find one-to-many matches + evaluationLog += "Find one-to-many matches\n" + for gtNum in range(len(gtRects)): + if gtNum not in gtDontCareRectsNum: + match, matchesDet = one_to_many_match(gtNum) + if match is True: + evaluationLog += "num_overlaps_gt=" + str( + num_overlaps_gt(gtNum)) + #in deteval we have to make other validation before mark as one-to-one + if num_overlaps_gt(gtNum) >= 2: + gtRectMat[gtNum] = 1 + recallAccum += (self.mtype_oo_o + if len(matchesDet) == 1 else + self.mtype_om_o) + precisionAccum += (self.mtype_oo_o + if len(matchesDet) == 1 else + self.mtype_om_o * + len(matchesDet)) + pairs.append({ + 'gt': gtNum, + 'det': matchesDet, + 'type': 'OO' if len(matchesDet) == 1 else 'OM' + }) + for detNum in matchesDet: + detRectMat[detNum] = 1 + evaluationLog += "Match GT #" + str( + gtNum) + " with Det #" + str(matchesDet) + "\n" + else: + evaluationLog += "Match Discarded GT #" + str( + gtNum) + " with Det #" + str( + matchesDet) + " not single overlap\n" + + # Find many-to-one matches + evaluationLog += "Find many-to-one matches\n" + for detNum in range(len(detRects)): + if detNum not in detDontCareRectsNum: + match, matchesGt = many_to_one_match(detNum) + if match is True: + #in deteval we have to make other validation before mark as one-to-one + if num_overlaps_det(detNum) >= 2: + detRectMat[detNum] = 1 + recallAccum += (self.mtype_oo_o + if len(matchesGt) == 1 else + self.mtype_om_m * len(matchesGt)) + precisionAccum += (self.mtype_oo_o + if len(matchesGt) == 1 else + self.mtype_om_m) + pairs.append({ + 'gt': matchesGt, + 'det': detNum, + 'type': 'OO' if len(matchesGt) == 1 else 'MO' + }) + for gtNum in matchesGt: + gtRectMat[gtNum] = 1 + evaluationLog += "Match GT #" + str( + matchesGt) + " with Det #" + str(detNum) + "\n" + else: + evaluationLog += "Match Discarded GT #" + str( + matchesGt) + " with Det #" + str( + detNum) + " not single overlap\n" + + numGtCare = (len(gtRects) - len(gtDontCareRectsNum)) + if numGtCare == 0: + recall = float(1) + precision = float(0) if len(detRects) > 0 else float(1) + else: + recall = float(recallAccum) / numGtCare + precision = float(0) if ( + len(detRects) - len(detDontCareRectsNum) + ) == 0 else float(precisionAccum) / ( + len(detRects) - len(detDontCareRectsNum)) + hmean = 0 if (precision + recall + ) == 0 else 2.0 * precision * recall / ( + precision + recall) + + numGtCare = len(gtRects) - len(gtDontCareRectsNum) + numDetCare = len(detRects) - len(detDontCareRectsNum) + + perSampleMetrics = { + 'precision': precision, + 'recall': recall, + 'hmean': hmean, + 'pairs': pairs, + 'recallMat': [] if len(detRects) > 100 else recallMat.tolist(), + 'precisionMat': [] + if len(detRects) > 100 else precisionMat.tolist(), + 'gtPolPoints': gtPolPoints, + 'detPolPoints': detPolPoints, + 'gtCare': numGtCare, + 'detCare': numDetCare, + 'gtDontCare': gtDontCareRectsNum, + 'detDontCare': detDontCareRectsNum, + 'recallAccum': recallAccum, + 'precisionAccum': precisionAccum, + 'evaluationLog': evaluationLog + } + + return perSampleMetrics + + def combine_results(self, results): + numGt = 0 + numDet = 0 + methodRecallSum = 0 + methodPrecisionSum = 0 + + for result in results: + numGt += result['gtCare'] + numDet += result['detCare'] + methodRecallSum += result['recallAccum'] + methodPrecisionSum += result['precisionAccum'] + + methodRecall = 0 if numGt == 0 else methodRecallSum / numGt + methodPrecision = 0 if numDet == 0 else methodPrecisionSum / numDet + methodHmean = 0 if methodRecall + methodPrecision == 0 else 2 * methodRecall * methodPrecision / ( + methodRecall + methodPrecision) + + methodMetrics = { + 'precision': methodPrecision, + 'recall': methodRecall, + 'hmean': methodHmean + } + + return methodMetrics + + +if __name__ == '__main__': + evaluator = DetectionDetEvalEvaluator() + gts = [[{ + 'points': [(0, 0), (1, 0), (1, 1), (0, 1)], + 'text': 1234, + 'ignore': False, + }, { + 'points': [(2, 2), (3, 2), (3, 3), (2, 3)], + 'text': 5678, + 'ignore': True, + }]] + preds = [[{ + 'points': [(0.1, 0.1), (1, 0), (1, 1), (0, 1)], + 'text': 123, + 'ignore': False, + }]] + results = [] + for gt, pred in zip(gts, preds): + results.append(evaluator.evaluate_image(gt, pred)) + metrics = evaluator.combine_results(results) + print(metrics) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/icdar2013.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/icdar2013.py new file mode 100644 index 0000000000000000000000000000000000000000..7e8c86aae334dfdc1f35db91772e09c164e29d22 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/icdar2013.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import math +from collections import namedtuple +import numpy as np +from shapely.geometry import Polygon + + +class DetectionICDAR2013Evaluator(object): + def __init__(self, + area_recall_constraint=0.8, + area_precision_constraint=0.4, + ev_param_ind_center_diff_thr=1, + mtype_oo_o=1.0, + mtype_om_o=0.8, + mtype_om_m=1.0): + + self.area_recall_constraint = area_recall_constraint + self.area_precision_constraint = area_precision_constraint + self.ev_param_ind_center_diff_thr = ev_param_ind_center_diff_thr + self.mtype_oo_o = mtype_oo_o + self.mtype_om_o = mtype_om_o + self.mtype_om_m = mtype_om_m + + def evaluate_image(self, gt, pred): + def get_union(pD, pG): + return Polygon(pD).union(Polygon(pG)).area + + def get_intersection_over_union(pD, pG): + return get_intersection(pD, pG) / get_union(pD, pG) + + def get_intersection(pD, pG): + return Polygon(pD).intersection(Polygon(pG)).area + + def one_to_one_match(row, col): + cont = 0 + for j in range(len(recallMat[0])): + if recallMat[row, + j] >= self.area_recall_constraint and precisionMat[ + row, j] >= self.area_precision_constraint: + cont = cont + 1 + if (cont != 1): + return False + cont = 0 + for i in range(len(recallMat)): + if recallMat[ + i, col] >= self.area_recall_constraint and precisionMat[ + i, col] >= self.area_precision_constraint: + cont = cont + 1 + if (cont != 1): + return False + + if recallMat[row, + col] >= self.area_recall_constraint and precisionMat[ + row, col] >= self.area_precision_constraint: + return True + return False + + def one_to_many_match(gtNum): + many_sum = 0 + detRects = [] + for detNum in range(len(recallMat[0])): + if gtRectMat[gtNum] == 0 and detRectMat[ + detNum] == 0 and detNum not in detDontCareRectsNum: + if precisionMat[gtNum, + detNum] >= self.area_precision_constraint: + many_sum += recallMat[gtNum, detNum] + detRects.append(detNum) + if round(many_sum, 4) >= self.area_recall_constraint: + return True, detRects + else: + return False, [] + + def many_to_one_match(detNum): + many_sum = 0 + gtRects = [] + for gtNum in range(len(recallMat)): + if gtRectMat[gtNum] == 0 and detRectMat[ + detNum] == 0 and gtNum not in gtDontCareRectsNum: + if recallMat[gtNum, detNum] >= self.area_recall_constraint: + many_sum += precisionMat[gtNum, detNum] + gtRects.append(gtNum) + if round(many_sum, 4) >= self.area_precision_constraint: + return True, gtRects + else: + return False, [] + + def center_distance(r1, r2): + return ((np.mean(r1, axis=0) - np.mean(r2, axis=0))**2).sum()**0.5 + + def diag(r): + r = np.array(r) + return ((r[:, 0].max() - r[:, 0].min())**2 + + (r[:, 1].max() - r[:, 1].min())**2)**0.5 + + perSampleMetrics = {} + + recall = 0 + precision = 0 + hmean = 0 + recallAccum = 0. + precisionAccum = 0. + gtRects = [] + detRects = [] + gtPolPoints = [] + detPolPoints = [] + gtDontCareRectsNum = [ + ] #Array of Ground Truth Rectangles' keys marked as don't Care + detDontCareRectsNum = [ + ] #Array of Detected Rectangles' matched with a don't Care GT + pairs = [] + evaluationLog = "" + + recallMat = np.empty([1, 1]) + precisionMat = np.empty([1, 1]) + + for n in range(len(gt)): + points = gt[n]['points'] + # transcription = gt[n]['text'] + dontCare = gt[n]['ignore'] + + if not Polygon(points).is_valid or not Polygon(points).is_simple: + continue + + gtRects.append(points) + gtPolPoints.append(points) + if dontCare: + gtDontCareRectsNum.append(len(gtRects) - 1) + + evaluationLog += "GT rectangles: " + str(len(gtRects)) + ( + " (" + str(len(gtDontCareRectsNum)) + " don't care)\n" + if len(gtDontCareRectsNum) > 0 else "\n") + + for n in range(len(pred)): + points = pred[n]['points'] + + if not Polygon(points).is_valid or not Polygon(points).is_simple: + continue + + detRect = points + detRects.append(detRect) + detPolPoints.append(points) + if len(gtDontCareRectsNum) > 0: + for dontCareRectNum in gtDontCareRectsNum: + dontCareRect = gtRects[dontCareRectNum] + intersected_area = get_intersection(dontCareRect, detRect) + rdDimensions = Polygon(detRect).area + if (rdDimensions == 0): + precision = 0 + else: + precision = intersected_area / rdDimensions + if (precision > self.area_precision_constraint): + detDontCareRectsNum.append(len(detRects) - 1) + break + + evaluationLog += "DET rectangles: " + str(len(detRects)) + ( + " (" + str(len(detDontCareRectsNum)) + " don't care)\n" + if len(detDontCareRectsNum) > 0 else "\n") + + if len(gtRects) == 0: + recall = 1 + precision = 0 if len(detRects) > 0 else 1 + + if len(detRects) > 0: + #Calculate recall and precision matrixs + outputShape = [len(gtRects), len(detRects)] + recallMat = np.empty(outputShape) + precisionMat = np.empty(outputShape) + gtRectMat = np.zeros(len(gtRects), np.int8) + detRectMat = np.zeros(len(detRects), np.int8) + for gtNum in range(len(gtRects)): + for detNum in range(len(detRects)): + rG = gtRects[gtNum] + rD = detRects[detNum] + intersected_area = get_intersection(rG, rD) + rgDimensions = Polygon(rG).area + rdDimensions = Polygon(rD).area + recallMat[ + gtNum, + detNum] = 0 if rgDimensions == 0 else intersected_area / rgDimensions + precisionMat[ + gtNum, + detNum] = 0 if rdDimensions == 0 else intersected_area / rdDimensions + + # Find one-to-one matches + evaluationLog += "Find one-to-one matches\n" + for gtNum in range(len(gtRects)): + for detNum in range(len(detRects)): + if gtRectMat[gtNum] == 0 and detRectMat[ + detNum] == 0 and gtNum not in gtDontCareRectsNum and detNum not in detDontCareRectsNum: + match = one_to_one_match(gtNum, detNum) + if match is True: + #in deteval we have to make other validation before mark as one-to-one + rG = gtRects[gtNum] + rD = detRects[detNum] + normDist = center_distance(rG, rD) + normDist /= diag(rG) + diag(rD) + normDist *= 2.0 + if normDist < self.ev_param_ind_center_diff_thr: + gtRectMat[gtNum] = 1 + detRectMat[detNum] = 1 + recallAccum += self.mtype_oo_o + precisionAccum += self.mtype_oo_o + pairs.append({ + 'gt': gtNum, + 'det': detNum, + 'type': 'OO' + }) + evaluationLog += "Match GT #" + str( + gtNum) + " with Det #" + str(detNum) + "\n" + else: + evaluationLog += "Match Discarded GT #" + str( + gtNum) + " with Det #" + str( + detNum) + " normDist: " + str( + normDist) + " \n" + # Find one-to-many matches + evaluationLog += "Find one-to-many matches\n" + for gtNum in range(len(gtRects)): + if gtNum not in gtDontCareRectsNum: + match, matchesDet = one_to_many_match(gtNum) + if match is True: + evaluationLog += "num_overlaps_gt=" + str( + num_overlaps_gt(gtNum)) + gtRectMat[gtNum] = 1 + recallAccum += (self.mtype_oo_o if len(matchesDet) == 1 + else self.mtype_om_o) + precisionAccum += (self.mtype_oo_o + if len(matchesDet) == 1 else + self.mtype_om_o * len(matchesDet)) + pairs.append({ + 'gt': gtNum, + 'det': matchesDet, + 'type': 'OO' if len(matchesDet) == 1 else 'OM' + }) + for detNum in matchesDet: + detRectMat[detNum] = 1 + evaluationLog += "Match GT #" + str( + gtNum) + " with Det #" + str(matchesDet) + "\n" + + # Find many-to-one matches + evaluationLog += "Find many-to-one matches\n" + for detNum in range(len(detRects)): + if detNum not in detDontCareRectsNum: + match, matchesGt = many_to_one_match(detNum) + if match is True: + detRectMat[detNum] = 1 + recallAccum += (self.mtype_oo_o if len(matchesGt) == 1 + else self.mtype_om_m * len(matchesGt)) + precisionAccum += (self.mtype_oo_o + if len(matchesGt) == 1 else + self.mtype_om_m) + pairs.append({ + 'gt': matchesGt, + 'det': detNum, + 'type': 'OO' if len(matchesGt) == 1 else 'MO' + }) + for gtNum in matchesGt: + gtRectMat[gtNum] = 1 + evaluationLog += "Match GT #" + str( + matchesGt) + " with Det #" + str(detNum) + "\n" + + numGtCare = (len(gtRects) - len(gtDontCareRectsNum)) + if numGtCare == 0: + recall = float(1) + precision = float(0) if len(detRects) > 0 else float(1) + else: + recall = float(recallAccum) / numGtCare + precision = float(0) if ( + len(detRects) - len(detDontCareRectsNum) + ) == 0 else float(precisionAccum) / ( + len(detRects) - len(detDontCareRectsNum)) + hmean = 0 if (precision + recall + ) == 0 else 2.0 * precision * recall / ( + precision + recall) + + numGtCare = len(gtRects) - len(gtDontCareRectsNum) + numDetCare = len(detRects) - len(detDontCareRectsNum) + + perSampleMetrics = { + 'precision': precision, + 'recall': recall, + 'hmean': hmean, + 'pairs': pairs, + 'recallMat': [] if len(detRects) > 100 else recallMat.tolist(), + 'precisionMat': [] + if len(detRects) > 100 else precisionMat.tolist(), + 'gtPolPoints': gtPolPoints, + 'detPolPoints': detPolPoints, + 'gtCare': numGtCare, + 'detCare': numDetCare, + 'gtDontCare': gtDontCareRectsNum, + 'detDontCare': detDontCareRectsNum, + 'recallAccum': recallAccum, + 'precisionAccum': precisionAccum, + 'evaluationLog': evaluationLog + } + + return perSampleMetrics + + def combine_results(self, results): + numGt = 0 + numDet = 0 + methodRecallSum = 0 + methodPrecisionSum = 0 + + for result in results: + numGt += result['gtCare'] + numDet += result['detCare'] + methodRecallSum += result['recallAccum'] + methodPrecisionSum += result['precisionAccum'] + + methodRecall = 0 if numGt == 0 else methodRecallSum / numGt + methodPrecision = 0 if numDet == 0 else methodPrecisionSum / numDet + methodHmean = 0 if methodRecall + methodPrecision == 0 else 2 * methodRecall * methodPrecision / ( + methodRecall + methodPrecision) + + methodMetrics = { + 'precision': methodPrecision, + 'recall': methodRecall, + 'hmean': methodHmean + } + + return methodMetrics + + +if __name__ == '__main__': + evaluator = DetectionICDAR2013Evaluator() + gts = [[{ + 'points': [(0, 0), (1, 0), (1, 1), (0, 1)], + 'text': 1234, + 'ignore': False, + }, { + 'points': [(2, 2), (3, 2), (3, 3), (2, 3)], + 'text': 5678, + 'ignore': True, + }]] + preds = [[{ + 'points': [(0.1, 0.1), (1, 0), (1, 1), (0, 1)], + 'text': 123, + 'ignore': False, + }]] + results = [] + for gt, pred in zip(gts, preds): + results.append(evaluator.evaluate_image(gt, pred)) + metrics = evaluator.combine_results(results) + print(metrics) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/iou.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/iou.py new file mode 100644 index 0000000000000000000000000000000000000000..5f9533b3c37e7d11ce2aaa276211baae14d1fb97 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/iou.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from collections import namedtuple +import numpy as np +from shapely.geometry import Polygon +import cv2 + + +def iou_rotate(box_a, box_b, method='union'): + rect_a = cv2.minAreaRect(box_a) + rect_b = cv2.minAreaRect(box_b) + r1 = cv2.rotatedRectangleIntersection(rect_a, rect_b) + if r1[0] == 0: + return 0 + else: + inter_area = cv2.contourArea(r1[1]) + area_a = cv2.contourArea(box_a) + area_b = cv2.contourArea(box_b) + union_area = area_a + area_b - inter_area + if union_area == 0 or inter_area == 0: + return 0 + if method == 'union': + iou = inter_area / union_area + elif method == 'intersection': + iou = inter_area / min(area_a, area_b) + else: + raise NotImplementedError + return iou + + +class DetectionIoUEvaluator(object): + def __init__(self, + is_output_polygon=False, + iou_constraint=0.5, + area_precision_constraint=0.5): + self.is_output_polygon = is_output_polygon + self.iou_constraint = iou_constraint + self.area_precision_constraint = area_precision_constraint + + def evaluate_image(self, gt, pred): + def get_union(pD, pG): + return Polygon(pD).union(Polygon(pG)).area + + def get_intersection_over_union(pD, pG): + return get_intersection(pD, pG) / get_union(pD, pG) + + def get_intersection(pD, pG): + return Polygon(pD).intersection(Polygon(pG)).area + + def compute_ap(confList, matchList, numGtCare): + correct = 0 + AP = 0 + if len(confList) > 0: + confList = np.array(confList) + matchList = np.array(matchList) + sorted_ind = np.argsort(-confList) + confList = confList[sorted_ind] + matchList = matchList[sorted_ind] + for n in range(len(confList)): + match = matchList[n] + if match: + correct += 1 + AP += float(correct) / (n + 1) + + if numGtCare > 0: + AP /= numGtCare + + return AP + + perSampleMetrics = {} + + matchedSum = 0 + + Rectangle = namedtuple('Rectangle', 'xmin ymin xmax ymax') + + numGlobalCareGt = 0 + numGlobalCareDet = 0 + + arrGlobalConfidences = [] + arrGlobalMatches = [] + + recall = 0 + precision = 0 + hmean = 0 + + detMatched = 0 + + iouMat = np.empty([1, 1]) + + gtPols = [] + detPols = [] + + gtPolPoints = [] + detPolPoints = [] + + # Array of Ground Truth Polygons' keys marked as don't Care + gtDontCarePolsNum = [] + # Array of Detected Polygons' matched with a don't Care GT + detDontCarePolsNum = [] + + pairs = [] + detMatchedNums = [] + + arrSampleConfidences = [] + arrSampleMatch = [] + + evaluationLog = "" + + for n in range(len(gt)): + points = gt[n]['points'] + # transcription = gt[n]['text'] + dontCare = gt[n]['ignore'] + + if not Polygon(points).is_valid or not Polygon(points).is_simple: + continue + + gtPol = points + gtPols.append(gtPol) + gtPolPoints.append(points) + if dontCare: + gtDontCarePolsNum.append(len(gtPols) - 1) + + evaluationLog += "GT polygons: " + str(len(gtPols)) + ( + " (" + str(len(gtDontCarePolsNum)) + " don't care)\n" + if len(gtDontCarePolsNum) > 0 else "\n") + + for n in range(len(pred)): + points = pred[n]['points'] + if not Polygon(points).is_valid or not Polygon(points).is_simple: + continue + + detPol = points + detPols.append(detPol) + detPolPoints.append(points) + if len(gtDontCarePolsNum) > 0: + for dontCarePol in gtDontCarePolsNum: + dontCarePol = gtPols[dontCarePol] + intersected_area = get_intersection(dontCarePol, detPol) + pdDimensions = Polygon(detPol).area + precision = 0 if pdDimensions == 0 else intersected_area / pdDimensions + if (precision > self.area_precision_constraint): + detDontCarePolsNum.append(len(detPols) - 1) + break + + evaluationLog += "DET polygons: " + str(len(detPols)) + ( + " (" + str(len(detDontCarePolsNum)) + " don't care)\n" + if len(detDontCarePolsNum) > 0 else "\n") + + if len(gtPols) > 0 and len(detPols) > 0: + # Calculate IoU and precision matrixs + outputShape = [len(gtPols), len(detPols)] + iouMat = np.empty(outputShape) + gtRectMat = np.zeros(len(gtPols), np.int8) + detRectMat = np.zeros(len(detPols), np.int8) + if self.is_output_polygon: + for gtNum in range(len(gtPols)): + for detNum in range(len(detPols)): + pG = gtPols[gtNum] + pD = detPols[detNum] + iouMat[gtNum, detNum] = get_intersection_over_union(pD, + pG) + else: + # gtPols = np.float32(gtPols) + # detPols = np.float32(detPols) + for gtNum in range(len(gtPols)): + for detNum in range(len(detPols)): + pG = np.float32(gtPols[gtNum]) + pD = np.float32(detPols[detNum]) + iouMat[gtNum, detNum] = iou_rotate(pD, pG) + for gtNum in range(len(gtPols)): + for detNum in range(len(detPols)): + if gtRectMat[gtNum] == 0 and detRectMat[ + detNum] == 0 and gtNum not in gtDontCarePolsNum and detNum not in detDontCarePolsNum: + if iouMat[gtNum, detNum] > self.iou_constraint: + gtRectMat[gtNum] = 1 + detRectMat[detNum] = 1 + detMatched += 1 + pairs.append({'gt': gtNum, 'det': detNum}) + detMatchedNums.append(detNum) + evaluationLog += "Match GT #" + \ + str(gtNum) + " with Det #" + str(detNum) + "\n" + + numGtCare = (len(gtPols) - len(gtDontCarePolsNum)) + numDetCare = (len(detPols) - len(detDontCarePolsNum)) + if numGtCare == 0: + recall = float(1) + precision = float(0) if numDetCare > 0 else float(1) + else: + recall = float(detMatched) / numGtCare + precision = 0 if numDetCare == 0 else float(detMatched) / numDetCare + + hmean = 0 if (precision + recall) == 0 else 2.0 * \ + precision * recall / (precision + recall) + + matchedSum += detMatched + numGlobalCareGt += numGtCare + numGlobalCareDet += numDetCare + + perSampleMetrics = { + 'precision': precision, + 'recall': recall, + 'hmean': hmean, + 'pairs': pairs, + 'iouMat': [] if len(detPols) > 100 else iouMat.tolist(), + 'gtPolPoints': gtPolPoints, + 'detPolPoints': detPolPoints, + 'gtCare': numGtCare, + 'detCare': numDetCare, + 'gtDontCare': gtDontCarePolsNum, + 'detDontCare': detDontCarePolsNum, + 'detMatched': detMatched, + 'evaluationLog': evaluationLog + } + + return perSampleMetrics + + def combine_results(self, results): + numGlobalCareGt = 0 + numGlobalCareDet = 0 + matchedSum = 0 + for result in results: + numGlobalCareGt += result['gtCare'] + numGlobalCareDet += result['detCare'] + matchedSum += result['detMatched'] + + methodRecall = 0 if numGlobalCareGt == 0 else float( + matchedSum) / numGlobalCareGt + methodPrecision = 0 if numGlobalCareDet == 0 else float( + matchedSum) / numGlobalCareDet + methodHmean = 0 if methodRecall + methodPrecision == 0 else 2 * \ + methodRecall * methodPrecision / ( + methodRecall + methodPrecision) + + methodMetrics = { + 'precision': methodPrecision, + 'recall': methodRecall, + 'hmean': methodHmean + } + + return methodMetrics + + +if __name__ == '__main__': + evaluator = DetectionIoUEvaluator() + preds = [[{ + 'points': [(0.1, 0.1), (0.5, 0), (0.5, 1), (0, 1)], + 'text': 1234, + 'ignore': False, + }, { + 'points': [(0.5, 0.1), (1, 0), (1, 1), (0.5, 1)], + 'text': 5678, + 'ignore': False, + }]] + gts = [[{ + 'points': [(0.1, 0.1), (1, 0), (1, 1), (0, 1)], + 'text': 123, + 'ignore': False, + }]] + results = [] + for gt, pred in zip(gts, preds): + results.append(evaluator.evaluate_image(gt, pred)) + metrics = evaluator.combine_results(results) + print(metrics) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/mtwi2018.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/mtwi2018.py new file mode 100644 index 0000000000000000000000000000000000000000..8e319aacf5a395a121e94bd2e9d123cec9279e7e --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/detection/mtwi2018.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import math +from collections import namedtuple +import numpy as np +from shapely.geometry import Polygon + + +class DetectionMTWI2018Evaluator(object): + def __init__( + self, + area_recall_constraint=0.7, + area_precision_constraint=0.7, + ev_param_ind_center_diff_thr=1, ): + + self.area_recall_constraint = area_recall_constraint + self.area_precision_constraint = area_precision_constraint + self.ev_param_ind_center_diff_thr = ev_param_ind_center_diff_thr + + def evaluate_image(self, gt, pred): + def get_union(pD, pG): + return Polygon(pD).union(Polygon(pG)).area + + def get_intersection_over_union(pD, pG): + return get_intersection(pD, pG) / get_union(pD, pG) + + def get_intersection(pD, pG): + return Polygon(pD).intersection(Polygon(pG)).area + + def one_to_one_match(row, col): + cont = 0 + for j in range(len(recallMat[0])): + if recallMat[row, + j] >= self.area_recall_constraint and precisionMat[ + row, j] >= self.area_precision_constraint: + cont = cont + 1 + if (cont != 1): + return False + cont = 0 + for i in range(len(recallMat)): + if recallMat[ + i, col] >= self.area_recall_constraint and precisionMat[ + i, col] >= self.area_precision_constraint: + cont = cont + 1 + if (cont != 1): + return False + + if recallMat[row, + col] >= self.area_recall_constraint and precisionMat[ + row, col] >= self.area_precision_constraint: + return True + return False + + def one_to_many_match(gtNum): + many_sum = 0 + detRects = [] + for detNum in range(len(recallMat[0])): + if gtRectMat[gtNum] == 0 and detRectMat[ + detNum] == 0 and detNum not in detDontCareRectsNum: + if precisionMat[gtNum, + detNum] >= self.area_precision_constraint: + many_sum += recallMat[gtNum, detNum] + detRects.append(detNum) + if round(many_sum, 4) >= self.area_recall_constraint: + return True, detRects + else: + return False, [] + + def many_to_one_match(detNum): + many_sum = 0 + gtRects = [] + for gtNum in range(len(recallMat)): + if gtRectMat[gtNum] == 0 and detRectMat[ + detNum] == 0 and gtNum not in gtDontCareRectsNum: + if recallMat[gtNum, detNum] >= self.area_recall_constraint: + many_sum += precisionMat[gtNum, detNum] + gtRects.append(gtNum) + if round(many_sum, 4) >= self.area_precision_constraint: + return True, gtRects + else: + return False, [] + + def center_distance(r1, r2): + return ((np.mean(r1, axis=0) - np.mean(r2, axis=0))**2).sum()**0.5 + + def diag(r): + r = np.array(r) + return ((r[:, 0].max() - r[:, 0].min())**2 + + (r[:, 1].max() - r[:, 1].min())**2)**0.5 + + perSampleMetrics = {} + + recall = 0 + precision = 0 + hmean = 0 + recallAccum = 0. + precisionAccum = 0. + gtRects = [] + detRects = [] + gtPolPoints = [] + detPolPoints = [] + gtDontCareRectsNum = [ + ] #Array of Ground Truth Rectangles' keys marked as don't Care + detDontCareRectsNum = [ + ] #Array of Detected Rectangles' matched with a don't Care GT + pairs = [] + evaluationLog = "" + + recallMat = np.empty([1, 1]) + precisionMat = np.empty([1, 1]) + + for n in range(len(gt)): + points = gt[n]['points'] + # transcription = gt[n]['text'] + dontCare = gt[n]['ignore'] + + if not Polygon(points).is_valid or not Polygon(points).is_simple: + continue + + gtRects.append(points) + gtPolPoints.append(points) + if dontCare: + gtDontCareRectsNum.append(len(gtRects) - 1) + + evaluationLog += "GT rectangles: " + str(len(gtRects)) + ( + " (" + str(len(gtDontCareRectsNum)) + " don't care)\n" + if len(gtDontCareRectsNum) > 0 else "\n") + + for n in range(len(pred)): + points = pred[n]['points'] + + if not Polygon(points).is_valid or not Polygon(points).is_simple: + continue + + detRect = points + detRects.append(detRect) + detPolPoints.append(points) + if len(gtDontCareRectsNum) > 0: + for dontCareRectNum in gtDontCareRectsNum: + dontCareRect = gtRects[dontCareRectNum] + intersected_area = get_intersection(dontCareRect, detRect) + rdDimensions = Polygon(detRect).area + if (rdDimensions == 0): + precision = 0 + else: + precision = intersected_area / rdDimensions + if (precision > 0.5): + detDontCareRectsNum.append(len(detRects) - 1) + break + + evaluationLog += "DET rectangles: " + str(len(detRects)) + ( + " (" + str(len(detDontCareRectsNum)) + " don't care)\n" + if len(detDontCareRectsNum) > 0 else "\n") + + if len(gtRects) == 0: + recall = 1 + precision = 0 if len(detRects) > 0 else 1 + + if len(detRects) > 0: + #Calculate recall and precision matrixs + outputShape = [len(gtRects), len(detRects)] + recallMat = np.empty(outputShape) + precisionMat = np.empty(outputShape) + gtRectMat = np.zeros(len(gtRects), np.int8) + detRectMat = np.zeros(len(detRects), np.int8) + for gtNum in range(len(gtRects)): + for detNum in range(len(detRects)): + rG = gtRects[gtNum] + rD = detRects[detNum] + intersected_area = get_intersection(rG, rD) + rgDimensions = Polygon(rG).area + rdDimensions = Polygon(rD).area + recallMat[ + gtNum, + detNum] = 0 if rgDimensions == 0 else intersected_area / rgDimensions + precisionMat[ + gtNum, + detNum] = 0 if rdDimensions == 0 else intersected_area / rdDimensions + + # Find one-to-one matches + evaluationLog += "Find one-to-one matches\n" + for gtNum in range(len(gtRects)): + for detNum in range(len(detRects)): + if gtRectMat[gtNum] == 0 and detRectMat[ + detNum] == 0 and gtNum not in gtDontCareRectsNum and detNum not in detDontCareRectsNum: + match = one_to_one_match(gtNum, detNum) + if match is True: + #in deteval we have to make other validation before mark as one-to-one + rG = gtRects[gtNum] + rD = detRects[detNum] + normDist = center_distance(rG, rD) + normDist /= diag(rG) + diag(rD) + normDist *= 2.0 + if normDist < self.ev_param_ind_center_diff_thr: + gtRectMat[gtNum] = 1 + detRectMat[detNum] = 1 + recallAccum += 1.0 + precisionAccum += 1.0 + pairs.append({ + 'gt': gtNum, + 'det': detNum, + 'type': 'OO' + }) + evaluationLog += "Match GT #" + str( + gtNum) + " with Det #" + str(detNum) + "\n" + else: + evaluationLog += "Match Discarded GT #" + str( + gtNum) + " with Det #" + str( + detNum) + " normDist: " + str( + normDist) + " \n" + # Find one-to-many matches + evaluationLog += "Find one-to-many matches\n" + for gtNum in range(len(gtRects)): + if gtNum not in gtDontCareRectsNum: + match, matchesDet = one_to_many_match(gtNum) + if match is True: + gtRectMat[gtNum] = 1 + recallAccum += 1.0 + precisionAccum += len(matchesDet) / ( + 1 + math.log(len(matchesDet))) + pairs.append({ + 'gt': gtNum, + 'det': matchesDet, + 'type': 'OO' if len(matchesDet) == 1 else 'OM' + }) + for detNum in matchesDet: + detRectMat[detNum] = 1 + evaluationLog += "Match GT #" + str( + gtNum) + " with Det #" + str(matchesDet) + "\n" + + # Find many-to-one matches + evaluationLog += "Find many-to-one matches\n" + for detNum in range(len(detRects)): + if detNum not in detDontCareRectsNum: + match, matchesGt = many_to_one_match(detNum) + if match is True: + detRectMat[detNum] = 1 + recallAccum += len(matchesGt) / ( + 1 + math.log(len(matchesGt))) + precisionAccum += 1.0 + pairs.append({ + 'gt': matchesGt, + 'det': detNum, + 'type': 'OO' if len(matchesGt) == 1 else 'MO' + }) + for gtNum in matchesGt: + gtRectMat[gtNum] = 1 + evaluationLog += "Match GT #" + str( + matchesGt) + " with Det #" + str(detNum) + "\n" + + numGtCare = (len(gtRects) - len(gtDontCareRectsNum)) + if numGtCare == 0: + recall = float(1) + precision = float(0) if len(detRects) > 0 else float(1) + else: + recall = float(recallAccum) / numGtCare + precision = float(0) if ( + len(detRects) - len(detDontCareRectsNum) + ) == 0 else float(precisionAccum) / ( + len(detRects) - len(detDontCareRectsNum)) + hmean = 0 if (precision + recall + ) == 0 else 2.0 * precision * recall / ( + precision + recall) + + numGtCare = len(gtRects) - len(gtDontCareRectsNum) + numDetCare = len(detRects) - len(detDontCareRectsNum) + + perSampleMetrics = { + 'precision': precision, + 'recall': recall, + 'hmean': hmean, + 'pairs': pairs, + 'recallMat': [] if len(detRects) > 100 else recallMat.tolist(), + 'precisionMat': [] + if len(detRects) > 100 else precisionMat.tolist(), + 'gtPolPoints': gtPolPoints, + 'detPolPoints': detPolPoints, + 'gtCare': numGtCare, + 'detCare': numDetCare, + 'gtDontCare': gtDontCareRectsNum, + 'detDontCare': detDontCareRectsNum, + 'recallAccum': recallAccum, + 'precisionAccum': precisionAccum, + 'evaluationLog': evaluationLog + } + + return perSampleMetrics + + def combine_results(self, results): + numGt = 0 + numDet = 0 + methodRecallSum = 0 + methodPrecisionSum = 0 + + for result in results: + numGt += result['gtCare'] + numDet += result['detCare'] + methodRecallSum += result['recallAccum'] + methodPrecisionSum += result['precisionAccum'] + + methodRecall = 0 if numGt == 0 else methodRecallSum / numGt + methodPrecision = 0 if numDet == 0 else methodPrecisionSum / numDet + methodHmean = 0 if methodRecall + methodPrecision == 0 else 2 * methodRecall * methodPrecision / ( + methodRecall + methodPrecision) + + methodMetrics = { + 'precision': methodPrecision, + 'recall': methodRecall, + 'hmean': methodHmean + } + + return methodMetrics + + +if __name__ == '__main__': + evaluator = DetectionICDAR2013Evaluator() + gts = [[{ + 'points': [(0, 0), (1, 0), (1, 1), (0, 1)], + 'text': 1234, + 'ignore': False, + }, { + 'points': [(2, 2), (3, 2), (3, 3), (2, 3)], + 'text': 5678, + 'ignore': True, + }]] + preds = [[{ + 'points': [(0.1, 0.1), (1, 0), (1, 1), (0, 1)], + 'text': 123, + 'ignore': False, + }]] + results = [] + for gt, pred in zip(gts, preds): + results.append(evaluator.evaluate_image(gt, pred)) + metrics = evaluator.combine_results(results) + print(metrics) diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/quad_metric.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/quad_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..e7e403a31c16af6b2e0e533139f2257cf6135c8a --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/ocr_metric/icdar2015/quad_metric.py @@ -0,0 +1,98 @@ +import numpy as np + +from .detection.iou import DetectionIoUEvaluator + + +class AverageMeter(object): + """Computes and stores the average and current value""" + + def __init__(self): + self.reset() + + def reset(self): + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + return self + + +class QuadMetric(): + def __init__(self, is_output_polygon=False): + self.is_output_polygon = is_output_polygon + self.evaluator = DetectionIoUEvaluator( + is_output_polygon=is_output_polygon) + + def measure(self, batch, output, box_thresh=0.6): + ''' + batch: (image, polygons, ignore_tags + batch: a dict produced by dataloaders. + image: tensor of shape (N, C, H, W). + polygons: tensor of shape (N, K, 4, 2), the polygons of objective regions. + ignore_tags: tensor of shape (N, K), indicates whether a region is ignorable or not. + shape: the original shape of images. + filename: the original filenames of images. + output: (polygons, ...) + ''' + results = [] + gt_polyons_batch = batch['text_polys'] + ignore_tags_batch = batch['ignore_tags'] + pred_polygons_batch = np.array(output[0]) + pred_scores_batch = np.array(output[1]) + for polygons, pred_polygons, pred_scores, ignore_tags in zip( + gt_polyons_batch, pred_polygons_batch, pred_scores_batch, + ignore_tags_batch): + gt = [ + dict( + points=np.int64(polygons[i]), ignore=ignore_tags[i]) + for i in range(len(polygons)) + ] + if self.is_output_polygon: + pred = [ + dict(points=pred_polygons[i]) + for i in range(len(pred_polygons)) + ] + else: + pred = [] + # print(pred_polygons.shape) + for i in range(pred_polygons.shape[0]): + if pred_scores[i] >= box_thresh: + # print(pred_polygons[i,:,:].tolist()) + pred.append( + dict(points=pred_polygons[i, :, :].astype(np.int))) + # pred = [dict(points=pred_polygons[i,:,:].tolist()) if pred_scores[i] >= box_thresh for i in range(pred_polygons.shape[0])] + results.append(self.evaluator.evaluate_image(gt, pred)) + return results + + def validate_measure(self, batch, output, box_thresh=0.6): + return self.measure(batch, output, box_thresh) + + def evaluate_measure(self, batch, output): + return self.measure(batch, output), np.linspace( + 0, batch['image'].shape[0]).tolist() + + def gather_measure(self, raw_metrics): + raw_metrics = [ + image_metrics + for batch_metrics in raw_metrics for image_metrics in batch_metrics + ] + + result = self.evaluator.combine_results(raw_metrics) + + precision = AverageMeter() + recall = AverageMeter() + fmeasure = AverageMeter() + + precision.update(result['precision'], n=len(raw_metrics)) + recall.update(result['recall'], n=len(raw_metrics)) + fmeasure_score = 2 * precision.val * recall.val / ( + precision.val + recall.val + 1e-8) + fmeasure.update(fmeasure_score) + + return {'precision': precision, 'recall': recall, 'fmeasure': fmeasure} diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/profiler.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/profiler.py new file mode 100644 index 0000000000000000000000000000000000000000..e64afd6a0d8cfd860920916acfbf168d58dfff2d --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/profiler.py @@ -0,0 +1,110 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# 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 sys +import paddle + +# A global variable to record the number of calling times for profiler +# functions. It is used to specify the tracing range of training steps. +_profiler_step_id = 0 + +# A global variable to avoid parsing from string every time. +_profiler_options = None + + +class ProfilerOptions(object): + ''' + Use a string to initialize a ProfilerOptions. + The string should be in the format: "key1=value1;key2=value;key3=value3". + For example: + "profile_path=model.profile" + "batch_range=[50, 60]; profile_path=model.profile" + "batch_range=[50, 60]; tracer_option=OpDetail; profile_path=model.profile" + ProfilerOptions supports following key-value pair: + batch_range - a integer list, e.g. [100, 110]. + state - a string, the optional values are 'CPU', 'GPU' or 'All'. + sorted_key - a string, the optional values are 'calls', 'total', + 'max', 'min' or 'ave. + tracer_option - a string, the optional values are 'Default', 'OpDetail', + 'AllOpDetail'. + profile_path - a string, the path to save the serialized profile data, + which can be used to generate a timeline. + exit_on_finished - a boolean. + ''' + + def __init__(self, options_str): + assert isinstance(options_str, str) + + self._options = { + 'batch_range': [10, 20], + 'state': 'All', + 'sorted_key': 'total', + 'tracer_option': 'Default', + 'profile_path': '/tmp/profile', + 'exit_on_finished': True + } + self._parse_from_string(options_str) + + def _parse_from_string(self, options_str): + for kv in options_str.replace(' ', '').split(';'): + key, value = kv.split('=') + if key == 'batch_range': + value_list = value.replace('[', '').replace(']', '').split(',') + value_list = list(map(int, value_list)) + if len(value_list) >= 2 and value_list[0] >= 0 and value_list[ + 1] > value_list[0]: + self._options[key] = value_list + elif key == 'exit_on_finished': + self._options[key] = value.lower() in ("yes", "true", "t", "1") + elif key in [ + 'state', 'sorted_key', 'tracer_option', 'profile_path' + ]: + self._options[key] = value + + def __getitem__(self, name): + if self._options.get(name, None) is None: + raise ValueError( + "ProfilerOptions does not have an option named %s." % name) + return self._options[name] + + +def add_profiler_step(options_str=None): + ''' + Enable the operator-level timing using PaddlePaddle's profiler. + The profiler uses a independent variable to count the profiler steps. + One call of this function is treated as a profiler step. + + Args: + profiler_options - a string to initialize the ProfilerOptions. + Default is None, and the profiler is disabled. + ''' + if options_str is None: + return + + global _profiler_step_id + global _profiler_options + + if _profiler_options is None: + _profiler_options = ProfilerOptions(options_str) + + if _profiler_step_id == _profiler_options['batch_range'][0]: + paddle.utils.profiler.start_profiler(_profiler_options['state'], + _profiler_options['tracer_option']) + elif _profiler_step_id == _profiler_options['batch_range'][1]: + paddle.utils.profiler.stop_profiler(_profiler_options['sorted_key'], + _profiler_options['profile_path']) + if _profiler_options['exit_on_finished']: + sys.exit(0) + + _profiler_step_id += 1 diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/schedulers.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/schedulers.py new file mode 100644 index 0000000000000000000000000000000000000000..1b6fb7d285594c06ec146c301bc5deb26d4e9c26 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/schedulers.py @@ -0,0 +1,64 @@ +from paddle.optimizer import lr +import logging +__all__ = ['Polynomial'] + + +class Polynomial(object): + """ + Polynomial learning rate decay + Args: + learning_rate (float): The initial learning rate. It is a python float number. + epochs(int): The decay epoch size. It determines the decay cycle, when by_epoch is set to true, it will change to epochs=epochs*step_each_epoch. + step_each_epoch: all steps in each epoch. + end_lr(float, optional): The minimum final learning rate. Default: 0.0001. + power(float, optional): Power of polynomial. Default: 1.0. + warmup_epoch(int): The epoch numbers for LinearWarmup. Default: 0, , when by_epoch is set to true, it will change to warmup_epoch=warmup_epoch*step_each_epoch. + warmup_start_lr(float): Initial learning rate of warm up. Default: 0.0. + last_epoch (int, optional): The index of last epoch. Can be set to restart training. Default: -1, means initial learning rate. + by_epoch: Whether the set parameter is based on epoch or iter, when set to true,, epochs and warmup_epoch will be automatically multiplied by step_each_epoch. Default: True + """ + + def __init__(self, + learning_rate, + epochs, + step_each_epoch, + end_lr=0.0, + power=1.0, + warmup_epoch=0, + warmup_start_lr=0.0, + last_epoch=-1, + by_epoch=True, + **kwargs): + super().__init__() + if warmup_epoch >= epochs: + msg = f"When using warm up, the value of \"epochs\" must be greater than value of \"Optimizer.lr.warmup_epoch\". The value of \"Optimizer.lr.warmup_epoch\" has been set to {epochs}." + logging.warning(msg) + warmup_epoch = epochs + self.learning_rate = learning_rate + self.epochs = epochs + self.end_lr = end_lr + self.power = power + self.last_epoch = last_epoch + self.warmup_epoch = warmup_epoch + self.warmup_start_lr = warmup_start_lr + + if by_epoch: + self.epochs *= step_each_epoch + self.warmup_epoch = int(self.warmup_epoch * step_each_epoch) + + def __call__(self): + learning_rate = lr.PolynomialDecay( + learning_rate=self.learning_rate, + decay_steps=self.epochs, + end_lr=self.end_lr, + power=self.power, + last_epoch=self. + last_epoch) if self.epochs > 0 else self.learning_rate + if self.warmup_epoch > 0: + learning_rate = lr.LinearWarmup( + learning_rate=learning_rate, + warmup_steps=self.warmup_epoch, + start_lr=self.warmup_start_lr, + end_lr=self.learning_rate, + last_epoch=self.last_epoch) + return learning_rate diff --git a/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/util.py b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/util.py new file mode 100644 index 0000000000000000000000000000000000000000..39bae764092fdca068a1792e065350209b45191a --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/PaddleOCR_DBNet/utils/util.py @@ -0,0 +1,367 @@ +# -*- coding: utf-8 -*- +# @Time : 2019/8/23 21:59 +# @Author : zhoujun +import json +import pathlib +import time +import os +import glob +import cv2 +import yaml +from typing import Mapping +import matplotlib.pyplot as plt +import numpy as np + +from argparse import ArgumentParser, RawDescriptionHelpFormatter + + +def _check_image_file(path): + img_end = {'jpg', 'bmp', 'png', 'jpeg', 'rgb', 'tif', 'tiff', 'gif', 'pdf'} + return any([path.lower().endswith(e) for e in img_end]) + + +def get_image_file_list(img_file): + imgs_lists = [] + if img_file is None or not os.path.exists(img_file): + raise Exception("not found any img file in {}".format(img_file)) + + img_end = {'jpg', 'bmp', 'png', 'jpeg', 'rgb', 'tif', 'tiff', 'gif', 'pdf'} + if os.path.isfile(img_file) and _check_image_file(img_file): + imgs_lists.append(img_file) + elif os.path.isdir(img_file): + for single_file in os.listdir(img_file): + file_path = os.path.join(img_file, single_file) + if os.path.isfile(file_path) and _check_image_file(file_path): + imgs_lists.append(file_path) + if len(imgs_lists) == 0: + raise Exception("not found any img file in {}".format(img_file)) + imgs_lists = sorted(imgs_lists) + return imgs_lists + + +def setup_logger(log_file_path: str=None): + import logging + logging._warn_preinit_stderr = 0 + logger = logging.getLogger('DBNet.paddle') + formatter = logging.Formatter( + '%(asctime)s %(name)s %(levelname)s: %(message)s') + ch = logging.StreamHandler() + ch.setFormatter(formatter) + logger.addHandler(ch) + if log_file_path is not None: + file_handle = logging.FileHandler(log_file_path) + file_handle.setFormatter(formatter) + logger.addHandler(file_handle) + logger.setLevel(logging.DEBUG) + return logger + + +# --exeTime +def exe_time(func): + def newFunc(*args, **args2): + t0 = time.time() + back = func(*args, **args2) + print("{} cost {:.3f}s".format(func.__name__, time.time() - t0)) + return back + + return newFunc + + +def load(file_path: str): + file_path = pathlib.Path(file_path) + func_dict = {'.txt': _load_txt, '.json': _load_json, '.list': _load_txt} + assert file_path.suffix in func_dict + return func_dict[file_path.suffix](file_path) + + +def _load_txt(file_path: str): + with open(file_path, 'r', encoding='utf8') as f: + content = [ + x.strip().strip('\ufeff').strip('\xef\xbb\xbf') + for x in f.readlines() + ] + return content + + +def _load_json(file_path: str): + with open(file_path, 'r', encoding='utf8') as f: + content = json.load(f) + return content + + +def save(data, file_path): + file_path = pathlib.Path(file_path) + func_dict = {'.txt': _save_txt, '.json': _save_json} + assert file_path.suffix in func_dict + return func_dict[file_path.suffix](data, file_path) + + +def _save_txt(data, file_path): + """ + 将一个list的数组写入txt文件里 + :param data: + :param file_path: + :return: + """ + if not isinstance(data, list): + data = [data] + with open(file_path, mode='w', encoding='utf8') as f: + f.write('\n'.join(data)) + + +def _save_json(data, file_path): + with open(file_path, 'w', encoding='utf-8') as json_file: + json.dump(data, json_file, ensure_ascii=False, indent=4) + + +def show_img(imgs: np.ndarray, title='img'): + color = (len(imgs.shape) == 3 and imgs.shape[-1] == 3) + imgs = np.expand_dims(imgs, axis=0) + for i, img in enumerate(imgs): + plt.figure() + plt.title('{}_{}'.format(title, i)) + plt.imshow(img, cmap=None if color else 'gray') + plt.show() + + +def draw_bbox(img_path, result, color=(255, 0, 0), thickness=2): + if isinstance(img_path, str): + img_path = cv2.imread(img_path) + # img_path = cv2.cvtColor(img_path, cv2.COLOR_BGR2RGB) + img_path = img_path.copy() + for point in result: + point = point.astype(int) + cv2.polylines(img_path, [point], True, color, thickness) + return img_path + + +def cal_text_score(texts, + gt_texts, + training_masks, + running_metric_text, + thred=0.5): + training_masks = training_masks.numpy() + pred_text = texts.numpy() * training_masks + pred_text[pred_text <= thred] = 0 + pred_text[pred_text > thred] = 1 + pred_text = pred_text.astype(np.int32) + gt_text = gt_texts.numpy() * training_masks + gt_text = gt_text.astype(np.int32) + running_metric_text.update(gt_text, pred_text) + score_text, _ = running_metric_text.get_scores() + return score_text + + +def order_points_clockwise(pts): + rect = np.zeros((4, 2), dtype="float32") + s = pts.sum(axis=1) + rect[0] = pts[np.argmin(s)] + rect[2] = pts[np.argmax(s)] + diff = np.diff(pts, axis=1) + rect[1] = pts[np.argmin(diff)] + rect[3] = pts[np.argmax(diff)] + return rect + + +def order_points_clockwise_list(pts): + pts = pts.tolist() + pts.sort(key=lambda x: (x[1], x[0])) + pts[:2] = sorted(pts[:2], key=lambda x: x[0]) + pts[2:] = sorted(pts[2:], key=lambda x: -x[0]) + pts = np.array(pts) + return pts + + +def get_datalist(train_data_path): + """ + 获取训练和验证的数据list + :param train_data_path: 训练的dataset文件列表,每个文件内以如下格式存储 ‘path/to/img\tlabel’ + :return: + """ + train_data = [] + for p in train_data_path: + with open(p, 'r', encoding='utf-8') as f: + for line in f.readlines(): + line = line.strip('\n').replace('.jpg ', '.jpg\t').split('\t') + if len(line) > 1: + img_path = pathlib.Path(line[0].strip(' ')) + label_path = pathlib.Path(line[1].strip(' ')) + if img_path.exists() and img_path.stat( + ).st_size > 0 and label_path.exists() and label_path.stat( + ).st_size > 0: + train_data.append((str(img_path), str(label_path))) + return train_data + + +def save_result(result_path, box_list, score_list, is_output_polygon): + if is_output_polygon: + with open(result_path, 'wt') as res: + for i, box in enumerate(box_list): + box = box.reshape(-1).tolist() + result = ",".join([str(int(x)) for x in box]) + score = score_list[i] + res.write(result + ',' + str(score) + "\n") + else: + with open(result_path, 'wt') as res: + for i, box in enumerate(box_list): + score = score_list[i] + box = box.reshape(-1).tolist() + result = ",".join([str(int(x)) for x in box]) + res.write(result + ',' + str(score) + "\n") + + +def expand_polygon(polygon): + """ + 对只有一个字符的框进行扩充 + """ + (x, y), (w, h), angle = cv2.minAreaRect(np.float32(polygon)) + if angle < -45: + w, h = h, w + angle += 90 + new_w = w + h + box = ((x, y), (new_w, h), angle) + points = cv2.boxPoints(box) + return order_points_clockwise(points) + + +def _merge_dict(config, merge_dct): + """ Recursive dict merge. Inspired by :meth:``dict.update()``, instead of + updating only top-level keys, dict_merge recurses down into dicts nested + to an arbitrary depth, updating keys. The ``merge_dct`` is merged into + ``dct``. + Args: + config: dict onto which the merge is executed + merge_dct: dct merged into config + Returns: dct + """ + for key, value in merge_dct.items(): + sub_keys = key.split('.') + key = sub_keys[0] + if key in config and len(sub_keys) > 1: + _merge_dict(config[key], {'.'.join(sub_keys[1:]): value}) + elif key in config and isinstance(config[key], dict) and isinstance( + value, Mapping): + _merge_dict(config[key], value) + else: + config[key] = value + return config + + +def print_dict(cfg, print_func=print, delimiter=0): + """ + Recursively visualize a dict and + indenting acrrording by the relationship of keys. + """ + for k, v in sorted(cfg.items()): + if isinstance(v, dict): + print_func("{}{} : ".format(delimiter * " ", str(k))) + print_dict(v, print_func, delimiter + 4) + elif isinstance(v, list) and len(v) >= 1 and isinstance(v[0], dict): + print_func("{}{} : ".format(delimiter * " ", str(k))) + for value in v: + print_dict(value, print_func, delimiter + 4) + else: + print_func("{}{} : {}".format(delimiter * " ", k, v)) + + +class Config(object): + def __init__(self, config_path, BASE_KEY='base'): + self.BASE_KEY = BASE_KEY + self.cfg = self._load_config_with_base(config_path) + + def _load_config_with_base(self, file_path): + """ + Load config from file. + Args: + file_path (str): Path of the config file to be loaded. + Returns: global config + """ + _, ext = os.path.splitext(file_path) + assert ext in ['.yml', '.yaml'], "only support yaml files for now" + + with open(file_path) as f: + file_cfg = yaml.load(f, Loader=yaml.Loader) + + # NOTE: cfgs outside have higher priority than cfgs in _BASE_ + if self.BASE_KEY in file_cfg: + all_base_cfg = dict() + base_ymls = list(file_cfg[self.BASE_KEY]) + for base_yml in base_ymls: + with open(base_yml) as f: + base_cfg = self._load_config_with_base(base_yml) + all_base_cfg = _merge_dict(all_base_cfg, base_cfg) + + del file_cfg[self.BASE_KEY] + file_cfg = _merge_dict(all_base_cfg, file_cfg) + file_cfg['filename'] = os.path.splitext(os.path.split(file_path)[-1])[0] + return file_cfg + + def merge_dict(self, args): + self.cfg = _merge_dict(self.cfg, args) + + def print_cfg(self, print_func=print): + """ + Recursively visualize a dict and + indenting acrrording by the relationship of keys. + """ + print_func('----------- Config -----------') + print_dict(self.cfg, print_func) + print_func('---------------------------------------------') + + def save(self, p): + with open(p, 'w') as f: + yaml.dump( + dict(self.cfg), f, default_flow_style=False, sort_keys=False) + + +class ArgsParser(ArgumentParser): + def __init__(self): + super(ArgsParser, self).__init__( + formatter_class=RawDescriptionHelpFormatter) + self.add_argument( + "-c", "--config_file", help="configuration file to use") + self.add_argument( + "-o", "--opt", nargs='*', help="set configuration options") + self.add_argument( + '-p', + '--profiler_options', + type=str, + default=None, + help='The option of profiler, which should be in format ' \ + '\"key1=value1;key2=value2;key3=value3\".' + ) + + def parse_args(self, argv=None): + args = super(ArgsParser, self).parse_args(argv) + assert args.config_file is not None, \ + "Please specify --config_file=configure_file_path." + args.opt = self._parse_opt(args.opt) + return args + + def _parse_opt(self, opts): + config = {} + if not opts: + return config + for s in opts: + s = s.strip() + k, v = s.split('=', 1) + if '.' not in k: + config[k] = yaml.load(v, Loader=yaml.Loader) + else: + keys = k.split('.') + if keys[0] not in config: + config[keys[0]] = {} + cur = config[keys[0]] + for idx, key in enumerate(keys[1:]): + if idx == len(keys) - 2: + cur[key] = yaml.load(v, Loader=yaml.Loader) + else: + cur[key] = {} + cur = cur[key] + return config + + +if __name__ == '__main__': + img = np.zeros((1, 3, 640, 640)) + show_img(img[0][0]) + plt.show() diff --git a/ocr_webapp/PaddleOCR/benchmark/analysis.py b/ocr_webapp/PaddleOCR/benchmark/analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..7322f00ace94ff25e8aba38106471d32a5e8223d --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/analysis.py @@ -0,0 +1,346 @@ +# copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# 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 print_function + +import argparse +import json +import os +import re +import traceback + + +def parse_args(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--filename", type=str, help="The name of log which need to analysis.") + parser.add_argument( + "--log_with_profiler", + type=str, + help="The path of train log with profiler") + parser.add_argument( + "--profiler_path", type=str, help="The path of profiler timeline log.") + parser.add_argument( + "--keyword", type=str, help="Keyword to specify analysis data") + parser.add_argument( + "--separator", + type=str, + default=None, + help="Separator of different field in log") + parser.add_argument( + '--position', type=int, default=None, help='The position of data field') + parser.add_argument( + '--range', + type=str, + default="", + help='The range of data field to intercept') + parser.add_argument( + '--base_batch_size', type=int, help='base_batch size on gpu') + parser.add_argument( + '--skip_steps', + type=int, + default=0, + help='The number of steps to be skipped') + parser.add_argument( + '--model_mode', + type=int, + default=-1, + help='Analysis mode, default value is -1') + parser.add_argument('--ips_unit', type=str, default=None, help='IPS unit') + parser.add_argument( + '--model_name', + type=str, + default=0, + help='training model_name, transformer_base') + parser.add_argument( + '--mission_name', type=str, default=0, help='training mission name') + parser.add_argument( + '--direction_id', type=int, default=0, help='training direction_id') + parser.add_argument( + '--run_mode', + type=str, + default="sp", + help='multi process or single process') + parser.add_argument( + '--index', + type=int, + default=1, + help='{1: speed, 2:mem, 3:profiler, 6:max_batch_size}') + parser.add_argument( + '--gpu_num', type=int, default=1, help='nums of training gpus') + args = parser.parse_args() + args.separator = None if args.separator == "None" else args.separator + return args + + +def _is_number(num): + pattern = re.compile(r'^[-+]?[-0-9]\d*\.\d*|[-+]?\.?[0-9]\d*$') + result = pattern.match(num) + if result: + return True + else: + return False + + +class TimeAnalyzer(object): + def __init__(self, + filename, + keyword=None, + separator=None, + position=None, + range="-1"): + if filename is None: + raise Exception("Please specify the filename!") + + if keyword is None: + raise Exception("Please specify the keyword!") + + self.filename = filename + self.keyword = keyword + self.separator = separator + self.position = position + self.range = range + self.records = None + self._distil() + + def _distil(self): + self.records = [] + with open(self.filename, "r") as f_object: + lines = f_object.readlines() + for line in lines: + if self.keyword not in line: + continue + try: + result = None + + # Distil the string from a line. + line = line.strip() + line_words = line.split( + self.separator) if self.separator else line.split() + if args.position: + result = line_words[self.position] + else: + # Distil the string following the keyword. + for i in range(len(line_words) - 1): + if line_words[i] == self.keyword: + result = line_words[i + 1] + break + + # Distil the result from the picked string. + if not self.range: + result = result[0:] + elif _is_number(self.range): + result = result[0:int(self.range)] + else: + result = result[int(self.range.split(":")[0]):int( + self.range.split(":")[1])] + self.records.append(float(result)) + except Exception as exc: + print("line is: {}; separator={}; position={}".format( + line, self.separator, self.position)) + + print("Extract {} records: separator={}; position={}".format( + len(self.records), self.separator, self.position)) + + def _get_fps(self, + mode, + batch_size, + gpu_num, + avg_of_records, + run_mode, + unit=None): + if mode == -1 and run_mode == 'sp': + assert unit, "Please set the unit when mode is -1." + fps = gpu_num * avg_of_records + elif mode == -1 and run_mode == 'mp': + assert unit, "Please set the unit when mode is -1." + fps = gpu_num * avg_of_records #temporarily, not used now + print("------------this is mp") + elif mode == 0: + # s/step -> samples/s + fps = (batch_size * gpu_num) / avg_of_records + unit = "samples/s" + elif mode == 1: + # steps/s -> steps/s + fps = avg_of_records + unit = "steps/s" + elif mode == 2: + # s/step -> steps/s + fps = 1 / avg_of_records + unit = "steps/s" + elif mode == 3: + # steps/s -> samples/s + fps = batch_size * gpu_num * avg_of_records + unit = "samples/s" + elif mode == 4: + # s/epoch -> s/epoch + fps = avg_of_records + unit = "s/epoch" + else: + ValueError("Unsupported analysis mode.") + + return fps, unit + + def analysis(self, + batch_size, + gpu_num=1, + skip_steps=0, + mode=-1, + run_mode='sp', + unit=None): + if batch_size <= 0: + print("base_batch_size should larger than 0.") + return 0, '' + + if len( + self.records + ) <= skip_steps: # to address the condition which item of log equals to skip_steps + print("no records") + return 0, '' + + sum_of_records = 0 + sum_of_records_skipped = 0 + skip_min = self.records[skip_steps] + skip_max = self.records[skip_steps] + + count = len(self.records) + for i in range(count): + sum_of_records += self.records[i] + if i >= skip_steps: + sum_of_records_skipped += self.records[i] + if self.records[i] < skip_min: + skip_min = self.records[i] + if self.records[i] > skip_max: + skip_max = self.records[i] + + avg_of_records = sum_of_records / float(count) + avg_of_records_skipped = sum_of_records_skipped / float(count - + skip_steps) + + fps, fps_unit = self._get_fps(mode, batch_size, gpu_num, avg_of_records, + run_mode, unit) + fps_skipped, _ = self._get_fps(mode, batch_size, gpu_num, + avg_of_records_skipped, run_mode, unit) + if mode == -1: + print("average ips of %d steps, skip 0 step:" % count) + print("\tAvg: %.3f %s" % (avg_of_records, fps_unit)) + print("\tFPS: %.3f %s" % (fps, fps_unit)) + if skip_steps > 0: + print("average ips of %d steps, skip %d steps:" % + (count, skip_steps)) + print("\tAvg: %.3f %s" % (avg_of_records_skipped, fps_unit)) + print("\tMin: %.3f %s" % (skip_min, fps_unit)) + print("\tMax: %.3f %s" % (skip_max, fps_unit)) + print("\tFPS: %.3f %s" % (fps_skipped, fps_unit)) + elif mode == 1 or mode == 3: + print("average latency of %d steps, skip 0 step:" % count) + print("\tAvg: %.3f steps/s" % avg_of_records) + print("\tFPS: %.3f %s" % (fps, fps_unit)) + if skip_steps > 0: + print("average latency of %d steps, skip %d steps:" % + (count, skip_steps)) + print("\tAvg: %.3f steps/s" % avg_of_records_skipped) + print("\tMin: %.3f steps/s" % skip_min) + print("\tMax: %.3f steps/s" % skip_max) + print("\tFPS: %.3f %s" % (fps_skipped, fps_unit)) + elif mode == 0 or mode == 2: + print("average latency of %d steps, skip 0 step:" % count) + print("\tAvg: %.3f s/step" % avg_of_records) + print("\tFPS: %.3f %s" % (fps, fps_unit)) + if skip_steps > 0: + print("average latency of %d steps, skip %d steps:" % + (count, skip_steps)) + print("\tAvg: %.3f s/step" % avg_of_records_skipped) + print("\tMin: %.3f s/step" % skip_min) + print("\tMax: %.3f s/step" % skip_max) + print("\tFPS: %.3f %s" % (fps_skipped, fps_unit)) + + return round(fps_skipped, 3), fps_unit + + +if __name__ == "__main__": + args = parse_args() + run_info = dict() + run_info["log_file"] = args.filename + run_info["model_name"] = args.model_name + run_info["mission_name"] = args.mission_name + run_info["direction_id"] = args.direction_id + run_info["run_mode"] = args.run_mode + run_info["index"] = args.index + run_info["gpu_num"] = args.gpu_num + run_info["FINAL_RESULT"] = 0 + run_info["JOB_FAIL_FLAG"] = 0 + + try: + if args.index == 1: + if args.gpu_num == 1: + run_info["log_with_profiler"] = args.log_with_profiler + run_info["profiler_path"] = args.profiler_path + analyzer = TimeAnalyzer(args.filename, args.keyword, args.separator, + args.position, args.range) + run_info["FINAL_RESULT"], run_info["UNIT"] = analyzer.analysis( + batch_size=args.base_batch_size, + gpu_num=args.gpu_num, + skip_steps=args.skip_steps, + mode=args.model_mode, + run_mode=args.run_mode, + unit=args.ips_unit) + try: + if int(os.getenv('job_fail_flag')) == 1 or int(run_info[ + "FINAL_RESULT"]) == 0: + run_info["JOB_FAIL_FLAG"] = 1 + except: + pass + elif args.index == 3: + run_info["FINAL_RESULT"] = {} + records_fo_total = TimeAnalyzer(args.filename, 'Framework overhead', + None, 3, '').records + records_fo_ratio = TimeAnalyzer(args.filename, 'Framework overhead', + None, 5).records + records_ct_total = TimeAnalyzer(args.filename, 'Computation time', + None, 3, '').records + records_gm_total = TimeAnalyzer(args.filename, + 'GpuMemcpy Calls', + None, 4, '').records + records_gm_ratio = TimeAnalyzer(args.filename, + 'GpuMemcpy Calls', + None, 6).records + records_gmas_total = TimeAnalyzer(args.filename, + 'GpuMemcpyAsync Calls', + None, 4, '').records + records_gms_total = TimeAnalyzer(args.filename, + 'GpuMemcpySync Calls', + None, 4, '').records + run_info["FINAL_RESULT"]["Framework_Total"] = records_fo_total[ + 0] if records_fo_total else 0 + run_info["FINAL_RESULT"]["Framework_Ratio"] = records_fo_ratio[ + 0] if records_fo_ratio else 0 + run_info["FINAL_RESULT"][ + "ComputationTime_Total"] = records_ct_total[ + 0] if records_ct_total else 0 + run_info["FINAL_RESULT"]["GpuMemcpy_Total"] = records_gm_total[ + 0] if records_gm_total else 0 + run_info["FINAL_RESULT"]["GpuMemcpy_Ratio"] = records_gm_ratio[ + 0] if records_gm_ratio else 0 + run_info["FINAL_RESULT"][ + "GpuMemcpyAsync_Total"] = records_gmas_total[ + 0] if records_gmas_total else 0 + run_info["FINAL_RESULT"]["GpuMemcpySync_Total"] = records_gms_total[ + 0] if records_gms_total else 0 + else: + print("Not support!") + except Exception: + traceback.print_exc() + print("{}".format(json.dumps(run_info)) + ) # it's required, for the log file path insert to the database diff --git a/ocr_webapp/PaddleOCR/benchmark/readme.md b/ocr_webapp/PaddleOCR/benchmark/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..d90d21468e7c9d0c9068a273ae704c0c8a086eab --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/readme.md @@ -0,0 +1,30 @@ + +# PaddleOCR DB/EAST/PSE 算法训练benchmark测试 + +PaddleOCR/benchmark目录下的文件用于获取并分析训练日志。 +训练采用icdar2015数据集,包括1000张训练图像和500张测试图像。模型配置采用resnet18_vd作为backbone,分别训练batch_size=8和batch_size=16的情况。 + +## 运行训练benchmark + +benchmark/run_det.sh 中包含了三个过程: +- 安装依赖 +- 下载数据 +- 执行训练 +- 日志分析获取IPS + +在执行训练部分,会执行单机单卡(默认0号卡)单机多卡训练,并分别执行batch_size=8和batch_size=16的情况。所以执行完后,每种模型会得到4个日志文件。 + +run_det.sh 执行方式如下: + +``` +# cd PaddleOCR/ +bash benchmark/run_det.sh +``` + +以DB为例,将得到四个日志文件,如下: +``` +det_res18_db_v2.0_sp_bs16_fp32_1 +det_res18_db_v2.0_sp_bs8_fp32_1 +det_res18_db_v2.0_mp_bs16_fp32_1 +det_res18_db_v2.0_mp_bs8_fp32_1 +``` diff --git a/ocr_webapp/PaddleOCR/benchmark/run_benchmark_det.sh b/ocr_webapp/PaddleOCR/benchmark/run_benchmark_det.sh new file mode 100644 index 0000000000000000000000000000000000000000..9f5b46cde1da58ccc3fbb128e52aa1cfe4f3dd53 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/run_benchmark_det.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# 运行示例:CUDA_VISIBLE_DEVICES=0 bash run_benchmark.sh ${run_mode} ${bs_item} ${fp_item} 500 ${model_mode} +# 参数说明 +function _set_params(){ + run_mode=${1:-"sp"} # 单卡sp|多卡mp + batch_size=${2:-"64"} + fp_item=${3:-"fp32"} # fp32|fp16 + max_epoch=${4:-"10"} # 可选,如果需要修改代码提前中断 + model_item=${5:-"model_item"} + run_log_path=${TRAIN_LOG_DIR:-$(pwd)} # TRAIN_LOG_DIR 后续QA设置该参数 +# 日志解析所需参数 + base_batch_size=${batch_size} + mission_name="OCR" + direction_id="0" + ips_unit="images/sec" + skip_steps=2 # 解析日志,有些模型前几个step耗时长,需要跳过 (必填) + keyword="ips:" # 解析日志,筛选出数据所在行的关键字 (必填) + index="1" + model_name=${model_item}_bs${batch_size}_${fp_item} # model_item 用于yml文件名匹配,model_name 用于数据入库前端展示 +# 以下不用修改 + device=${CUDA_VISIBLE_DEVICES//,/ } + arr=(${device}) + num_gpu_devices=${#arr[*]} + log_file=${run_log_path}/${model_item}_${run_mode}_bs${batch_size}_${fp_item}_${num_gpu_devices} +} +function _train(){ + echo "Train on ${num_gpu_devices} GPUs" + echo "current CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES, gpus=$num_gpu_devices, batch_size=$batch_size" + + train_cmd="-c configs/det/${model_item}.yml -o Train.loader.batch_size_per_card=${batch_size} Global.epoch_num=${max_epoch} Global.eval_batch_step=[0,20000] Global.print_batch_step=2" + case ${run_mode} in + sp) + train_cmd="python tools/train.py "${train_cmd}"" + ;; + mp) + rm -rf ./mylog + train_cmd="python -m paddle.distributed.launch --log_dir=./mylog --gpus=$CUDA_VISIBLE_DEVICES tools/train.py ${train_cmd}" + ;; + *) echo "choose run_mode(sp or mp)"; exit 1; + esac +# 以下不用修改 + echo ${train_cmd} + timeout 15m ${train_cmd} > ${log_file} 2>&1 + if [ $? -ne 0 ];then + echo -e "${model_name}, FAIL" + export job_fail_flag=1 + else + echo -e "${model_name}, SUCCESS" + export job_fail_flag=0 + fi + + if [ $run_mode = "mp" -a -d mylog ]; then + rm ${log_file} + cp mylog/workerlog.0 ${log_file} + fi +} + +source ${BENCHMARK_ROOT}/scripts/run_model.sh # 在该脚本中会对符合benchmark规范的log使用analysis.py 脚本进行性能数据解析;该脚本在连调时可从benchmark repo中下载https://github.com/PaddlePaddle/benchmark/blob/master/scripts/run_model.sh;如果不联调只想要产出训练log可以注掉本行,提交时需打开 +_set_params $@ +#_train # 如果只想产出训练log,不解析,可取消注释 +_run # 该函数在run_model.sh中,执行时会调用_train; 如果不联调只想要产出训练log可以注掉本行,提交时需打开 + diff --git a/ocr_webapp/PaddleOCR/benchmark/run_det.sh b/ocr_webapp/PaddleOCR/benchmark/run_det.sh new file mode 100644 index 0000000000000000000000000000000000000000..981510c9ae80698dae7f4c8b342dc50442aa7913 --- /dev/null +++ b/ocr_webapp/PaddleOCR/benchmark/run_det.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# 提供可稳定复现性能的脚本,默认在标准docker环境内py37执行: paddlepaddle/paddle:latest-gpu-cuda10.1-cudnn7 paddle=2.1.2 py=37 +# 执行目录: ./PaddleOCR +# 1 安装该模型需要的依赖 (如需开启优化策略请注明) +log_path=${LOG_PATH_INDEX_DIR:-$(pwd)} +python -m pip install -r requirements.txt +# 2 拷贝该模型需要数据、预训练模型 +wget -P ./train_data/ https://paddleocr.bj.bcebos.com/dygraph_v2.0/test/icdar2015.tar && cd train_data && tar xf icdar2015.tar && cd ../ +wget -P ./pretrain_models/ https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ResNet50_vd_pretrained.pdparams +wget -P ./pretrain_models/ https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ResNet18_vd_pretrained.pdparams +wget -P ./pretrain_models/ https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ResNet50_vd_ssld_pretrained.pdparams +# 3 批量运行(如不方便批量,1,2需放到单个模型中) + +model_mode_list=(det_res18_db_v2.0 det_r50_vd_east det_r50_vd_pse) +fp_item_list=(fp32) +for model_mode in ${model_mode_list[@]}; do + for fp_item in ${fp_item_list[@]}; do + if [ ${model_mode} == "det_r50_vd_east" ]; then + bs_list=(16) + else + bs_list=(8 16) + fi + for bs_item in ${bs_list[@]}; do + echo "index is speed, 1gpus, begin, ${model_name}" + run_mode=sp + log_name=ocr_${model_mode}_bs${bs_item}_${fp_item}_${run_mode} + CUDA_VISIBLE_DEVICES=0 bash benchmark/run_benchmark_det.sh ${run_mode} ${bs_item} ${fp_item} 1 ${model_mode} | tee ${log_path}/${log_name}_speed_1gpus 2>&1 # (5min) + sleep 60 + echo "index is speed, 8gpus, run_mode is multi_process, begin, ${model_name}" + run_mode=mp + log_name=ocr_${model_mode}_bs${bs_item}_${fp_item}_${run_mode} + CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 bash benchmark/run_benchmark_det.sh ${run_mode} ${bs_item} ${fp_item} 2 ${model_mode} | tee ${log_path}/${log_name}_speed_8gpus8p 2>&1 + sleep 60 + done + done +done + + + diff --git a/ocr_webapp/PaddleOCR/configs/cls/ch_PP-OCRv3/ch_PP-OCRv3_rotnet.yml b/ocr_webapp/PaddleOCR/configs/cls/ch_PP-OCRv3/ch_PP-OCRv3_rotnet.yml new file mode 100644 index 0000000000000000000000000000000000000000..f7e327d1e0378ed75b03b1f866414ef34e197c47 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/cls/ch_PP-OCRv3/ch_PP-OCRv3_rotnet.yml @@ -0,0 +1,98 @@ +Global: + debug: false + use_gpu: true + epoch_num: 100 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/rec_ppocr_v3_rotnet + save_epoch_step: 3 + eval_batch_step: [0, 2000] + cal_metric_during_train: true + pretrained_model: null + checkpoints: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_words/ch/word_1.jpg + character_dict_path: ppocr/utils/ppocr_keys_v1.txt + max_text_length: 25 + infer_mode: false + use_space_char: true + save_res_path: ./output/rec/predicts_chinese_lite_v2.0.txt +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + regularizer: + name: L2 + factor: 1.0e-05 +Architecture: + model_type: cls + algorithm: CLS + Transform: null + Backbone: + name: MobileNetV1Enhance + scale: 0.5 + last_conv_stride: [1, 2] + last_pool_type: avg + Neck: + Head: + name: ClsHead + class_dim: 4 + +Loss: + name: ClsLoss + main_indicator: acc + +PostProcess: + name: ClsPostProcess + +Metric: + name: ClsMetric + main_indicator: acc + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data + label_file_list: + - ./train_data/train_list.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - BaseDataAugmentation: + - RandAugment: + - SSLRotateResize: + image_shape: [3, 48, 320] + - KeepKeys: + keep_keys: ["image", "label"] + loader: + collate_fn: "SSLRotateCollate" + shuffle: true + batch_size_per_card: 32 + drop_last: true + num_workers: 8 +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data + label_file_list: + - ./train_data/val_list.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - SSLRotateResize: + image_shape: [3, 48, 320] + - KeepKeys: + keep_keys: ["image", "label"] + loader: + collate_fn: "SSLRotateCollate" + shuffle: false + drop_last: false + batch_size_per_card: 64 + num_workers: 8 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/cls/cls_mv3.yml b/ocr_webapp/PaddleOCR/configs/cls/cls_mv3.yml new file mode 100644 index 0000000000000000000000000000000000000000..0c46ff560277d9e318587432671de09f0d13cf35 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/cls/cls_mv3.yml @@ -0,0 +1,94 @@ +Global: + use_gpu: true + epoch_num: 100 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/cls/mv3/ + save_epoch_step: 3 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [0, 1000] + cal_metric_during_train: True + pretrained_model: + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_words_en/word_10.png + label_list: ['0','180'] + +Architecture: + model_type: cls + algorithm: CLS + Transform: + Backbone: + name: MobileNetV3 + scale: 0.35 + model_name: small + Neck: + Head: + name: ClsHead + class_dim: 2 + +Loss: + name: ClsLoss + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: ClsPostProcess + +Metric: + name: ClsMetric + main_indicator: acc + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/cls + label_file_list: + - ./train_data/cls/train.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - ClsLabelEncode: # Class handling label + - BaseDataAugmentation: + - RandAugment: + - ClsResizeImg: + image_shape: [3, 48, 192] + - KeepKeys: + keep_keys: ['image', 'label'] # dataloader will return list in this order + loader: + shuffle: True + batch_size_per_card: 512 + drop_last: True + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/cls + label_file_list: + - ./train_data/cls/test.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - ClsLabelEncode: # Class handling label + - ClsResizeImg: + image_shape: [3, 48, 192] + - KeepKeys: + keep_keys: ['image', 'label'] # dataloader will return list in this order + loader: + shuffle: False + drop_last: False + batch_size_per_card: 512 + num_workers: 4 diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv3/PP-OCRv3_det_cml.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv3/PP-OCRv3_det_cml.yml new file mode 100644 index 0000000000000000000000000000000000000000..148260818bad402cee5ba6bc36ceb533583f472b --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv3/PP-OCRv3_det_cml.yml @@ -0,0 +1,226 @@ +Global: + debug: false + use_gpu: true + epoch_num: 500 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/ch_PP-OCR_v3_det/ + save_epoch_step: 100 + eval_batch_step: + - 0 + - 400 + cal_metric_during_train: false + pretrained_model: null + checkpoints: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + distributed: true + d2s_train_image_shape: [3, -1, -1] + amp_dtype: bfloat16 + +Architecture: + name: DistillationModel + algorithm: Distillation + model_type: det + Models: + Student: + pretrained: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: true + Neck: + name: RSEFPN + out_channels: 96 + shortcut: True + Head: + name: DBHead + k: 50 + Student2: + pretrained: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: true + Neck: + name: RSEFPN + out_channels: 96 + shortcut: True + Head: + name: DBHead + k: 50 + Teacher: + freeze_params: true + return_all_feats: false + model_type: det + algorithm: DB + Backbone: + name: ResNet_vd + in_channels: 3 + layers: 50 + Neck: + name: LKPAN + out_channels: 256 + Head: + name: DBHead + kernel_list: [7,2,2] + k: 50 + +Loss: + name: CombinedLoss + loss_config_list: + - DistillationDilaDBLoss: + weight: 1.0 + model_name_pairs: + - ["Student", "Teacher"] + - ["Student2", "Teacher"] + key: maps + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + - DistillationDMLLoss: + model_name_pairs: + - ["Student", "Student2"] + maps_name: "thrink_maps" + weight: 1.0 + model_name_pairs: ["Student", "Student2"] + key: maps + - DistillationDBLoss: + weight: 1.0 + model_name_list: ["Student", "Student2"] + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: L2 + factor: 5.0e-05 + +PostProcess: + name: DistillationDBPostProcess + model_name: ["Student"] + key: head_out + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DistillationMetric + base_metric_name: DetMetric + main_indicator: hmean + key: "Student" + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 960 + - 960 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv3/PP-OCRv3_det_dml.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv3/PP-OCRv3_det_dml.yml new file mode 100644 index 0000000000000000000000000000000000000000..6dbf5ffd897ccb75d1c116c09fbf02e27393b5d5 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv3/PP-OCRv3_det_dml.yml @@ -0,0 +1,174 @@ +Global: + use_gpu: true + epoch_num: 1200 + log_smooth_window: 20 + print_batch_step: 2 + save_model_dir: ./output/ch_db_mv3/ + save_epoch_step: 1200 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [3000, 2000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/MobileNetV3_large_x0_5_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_db/predicts_db.txt + d2s_train_image_shape: [3, -1, -1] + +Architecture: + name: DistillationModel + algorithm: Distillation + model_type: det + Models: + Student: + return_all_feats: false + model_type: det + algorithm: DB + Backbone: + name: ResNet_vd + in_channels: 3 + layers: 50 + Neck: + name: LKPAN + out_channels: 256 + Head: + name: DBHead + kernel_list: [7,2,2] + k: 50 + Student2: + return_all_feats: false + model_type: det + algorithm: DB + Backbone: + name: ResNet_vd + in_channels: 3 + layers: 50 + Neck: + name: LKPAN + out_channels: 256 + Head: + name: DBHead + kernel_list: [7,2,2] + k: 50 + + +Loss: + name: CombinedLoss + loss_config_list: + - DistillationDMLLoss: + model_name_pairs: + - ["Student", "Student2"] + maps_name: "thrink_maps" + weight: 1.0 + # act: None + model_name_pairs: ["Student", "Student2"] + key: maps + - DistillationDBLoss: + weight: 1.0 + model_name_list: ["Student", "Student2"] + # key: maps + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: DistillationDBPostProcess + model_name: ["Student", "Student2"] + key: head_out + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DistillationMetric + base_metric_name: DetMetric + main_indicator: hmean + key: "Student" + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - CopyPaste: + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [-10, 10] } } + - { 'type': Resize, 'args': { 'size': [0.5, 3] } } + - EastRandomCropData: + size: [960, 960] + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'threshold_map', 'threshold_mask', 'shrink_map', 'shrink_mask'] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: +# image_shape: [736, 1280] + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv3/PP-OCRv3_mobile_det.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv3/PP-OCRv3_mobile_det.yml new file mode 100644 index 0000000000000000000000000000000000000000..eafc44bb77a843ee9a629946d65d6d513bbd95ab --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv3/PP-OCRv3_mobile_det.yml @@ -0,0 +1,165 @@ +Global: + model_name: PP-OCRv3_mobile_det # To use static model for inference. + debug: false + use_gpu: true + epoch_num: 500 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/PP-OCRv3_mobile_det/ + save_epoch_step: 100 + eval_batch_step: + - 0 + - 400 + cal_metric_during_train: false + pretrained_model: https://paddleocr.bj.bcebos.com/pretrained/MobileNetV3_large_x0_5_pretrained.pdparams + checkpoints: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + distributed: true + d2s_train_image_shape: [3, -1, -1] + +Architecture: + model_type: det + algorithm: DB + Transform: + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: True + Neck: + name: RSEFPN + out_channels: 96 + shortcut: True + Head: + name: DBHead + k: 50 + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: L2 + factor: 5.0e-05 +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 +Metric: + name: DetMetric + main_indicator: hmean +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 960 + - 960 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 4 +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: null + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv3/PP-OCRv3_server_det.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv3/PP-OCRv3_server_det.yml new file mode 100644 index 0000000000000000000000000000000000000000..1bd7c8e41ae6c2ef90767b4486c27dde54968cb6 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv3/PP-OCRv3_server_det.yml @@ -0,0 +1,160 @@ +Global: + model_name: PP-OCRv3_server_det # To use static model for inference. + debug: false + use_gpu: true + epoch_num: 500 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/PP-OCRv3_server_det/ + save_epoch_step: 100 + eval_batch_step: + - 0 + - 400 + cal_metric_during_train: false + pretrained_model: https://paddleocr.bj.bcebos.com/pretrained/ResNet50_vd_ssld_pretrained.pdparams + checkpoints: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + distributed: true + d2s_train_image_shape: [3, -1, -1] + amp_dtype: bfloat16 + +Architecture: + model_type: det + algorithm: DB + Backbone: + name: ResNet_vd + in_channels: 3 + layers: 50 + Neck: + name: LKPAN + out_channels: 256 + Head: + name: DBHead + kernel_list: [7,2,2] + k: 50 + + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: L2 + factor: 5.0e-05 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 960 + - 960 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_det_cml.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_det_cml.yml new file mode 100644 index 0000000000000000000000000000000000000000..b316ceffc98134704f0be71a0735691cb615d36f --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_det_cml.yml @@ -0,0 +1,240 @@ +Global: + debug: false + use_gpu: true + epoch_num: 500 + log_smooth_window: 20 + print_batch_step: 20 + save_model_dir: ./output/ch_PP-OCRv4 + save_epoch_step: 50 + eval_batch_step: + - 0 + - 1000 + cal_metric_during_train: false + checkpoints: null + pretrained_model: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + d2s_train_image_shape: [3, 640, 640] + distributed: true +Architecture: + name: DistillationModel + algorithm: Distillation + model_type: det + Models: + Student: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPLCNetV3 + scale: 0.75 + pretrained: false + det: true + Neck: + name: RSEFPN + out_channels: 96 + shortcut: true + Head: + name: DBHead + k: 50 + Student2: + pretrained: null + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPLCNetV3 + scale: 0.75 + pretrained: true + det: true + Neck: + name: RSEFPN + out_channels: 96 + shortcut: true + Head: + name: DBHead + k: 50 + Teacher: + pretrained: https://paddleocr.bj.bcebos.com/PP-OCRv4/chinese/ch_PP-OCRv4_det_cml_teacher_pretrained/teacher.pdparams + freeze_params: true + return_all_feats: false + model_type: det + algorithm: DB + Backbone: + name: ResNet_vd + in_channels: 3 + layers: 50 + Neck: + name: LKPAN + out_channels: 256 + Head: + name: DBHead + kernel_list: + - 7 + - 2 + - 2 + k: 50 +Loss: + name: CombinedLoss + loss_config_list: + - DistillationDilaDBLoss: + weight: 1.0 + model_name_pairs: + - - Student + - Teacher + - - Student2 + - Teacher + key: maps + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + - DistillationDMLLoss: + model_name_pairs: + - Student + - Student2 + maps_name: thrink_maps + weight: 1.0 + key: maps + - DistillationDBLoss: + weight: 1.0 + model_name_list: + - Student + - Student2 + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: L2 + factor: 5.0e-05 +PostProcess: + name: DistillationDBPostProcess + model_name: + - Student + key: head_out + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 +Metric: + name: DistillationMetric + base_metric_name: DetMetric + main_indicator: hmean + key: Student +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: 500 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + total_epoch: 500 + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 16 + num_workers: 8 +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: + limit_side_len: 960 + limit_type: max + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_mobile_det.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_mobile_det.yml new file mode 100644 index 0000000000000000000000000000000000000000..c5791ce0d8fd45478b77db15373a223980115ff7 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_mobile_det.yml @@ -0,0 +1,174 @@ +Global: + model_name: PP-OCRv4_mobile_det # To use static model for inference. + debug: false + use_gpu: true + epoch_num: &epoch_num 500 + log_smooth_window: 20 + print_batch_step: 100 + save_model_dir: ./output/PP-OCRv4_mobile_det + save_epoch_step: 10 + eval_batch_step: + - 0 + - 1500 + cal_metric_during_train: false + checkpoints: + pretrained_model: https://paddleocr.bj.bcebos.com/pretrained/PPLCNetV3_x0_75_ocr_det.pdparams + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + d2s_train_image_shape: [3, 640, 640] + distributed: true + +Architecture: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPLCNetV3 + scale: 0.75 + det: True + Neck: + name: RSEFPN + out_channels: 96 + shortcut: True + Head: + name: DBHead + k: 50 + fix_nan: True + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 #(8*8c) + warmup_epoch: 2 + regularizer: + name: L2 + factor: 5.0e-05 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: *epoch_num + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + total_epoch: *epoch_num + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_mobile_seal_det.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_mobile_seal_det.yml new file mode 100644 index 0000000000000000000000000000000000000000..9788c9124e586ec9fc9dd53c37bb8e44440f5b3e --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_mobile_seal_det.yml @@ -0,0 +1,171 @@ +Global: + model_name: PP-OCRv4_mobile_seal_det # To use static model for inference. + debug: false + use_gpu: true + epoch_num: 100 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: output + save_epoch_step: 1 + eval_batch_step: + - 0 + - 100 + cal_metric_during_train: false + checkpoints: + pretrained_model: https://paddleocr.bj.bcebos.com/pretrained/PPLCNetV3_x0_75_ocr_det.pdparams + save_inference_dir: null + use_visualdl: false + distributed: true + d2s_train_image_shape: [3, 640, 640] + +Architecture: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPLCNetV3 + scale: 0.75 + det: True + Neck: + name: RSEFPN + out_channels: 96 + shortcut: True + Head: + name: DBHead + k: 50 + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: L2 + factor: 1e-6 + +PostProcess: + name: DBPostProcess + thresh: 0.2 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 0.5 + box_type: "poly" + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: TextDetDataset + data_dir: datasets/ICDAR2015 + label_file_list: + - datasets/ICDAR2015/train.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.8 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: 500 + - MakeShrinkMap: + shrink_ratio: 0.8 + min_text_size: 8 + total_epoch: 500 + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 3 + +Eval: + dataset: + name: TextDetDataset + data_dir: datasets/ICDAR2015 + label_file_list: + - datasets/ICDAR2015/val.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: + resize_long: 736 + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 0 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_server_det.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_server_det.yml new file mode 100644 index 0000000000000000000000000000000000000000..93e5992929aa5e0eff25c21f55093615790dcd42 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_server_det.yml @@ -0,0 +1,175 @@ +Global: + model_name: PP-OCRv4_server_det # To use static model for inference. + debug: false + use_gpu: true + epoch_num: &epoch_num 500 + log_smooth_window: 20 + print_batch_step: 100 + save_model_dir: ./output/PP-OCRv4_server_det + save_epoch_step: 10 + eval_batch_step: + - 0 + - 1500 + cal_metric_during_train: false + checkpoints: + pretrained_model: https://paddleocr.bj.bcebos.com/pretrained/PPHGNet_small_ocr_det.pdparams + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + d2s_train_image_shape: [3, 640, 640] + distributed: true + +Architecture: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPHGNet_small + det: True + Neck: + name: LKPAN + out_channels: 256 + intracl: true + Head: + name: PFHeadLocal + k: 50 + mode: "large" + fix_nan: True + + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 #(8*8c) + warmup_epoch: 2 + regularizer: + name: L2 + factor: 1e-6 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: *epoch_num + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + total_epoch: *epoch_num + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_server_seal_det.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_server_seal_det.yml new file mode 100644 index 0000000000000000000000000000000000000000..20c73223a547ef7fbc410c2400df50c499d12ae7 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv4/PP-OCRv4_server_seal_det.yml @@ -0,0 +1,171 @@ +Global: + model_name: PP-OCRv4_server_seal_det # To use static model for inference. + debug: false + use_gpu: true + epoch_num: 100 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: output + save_epoch_step: 1 + eval_batch_step: + - 0 + - 100 + cal_metric_during_train: false + checkpoints: + pretrained_model: https://paddleocr.bj.bcebos.com/pretrained/PPHGNet_small_ocr_det.pdparams + save_inference_dir: null + use_visualdl: false + distributed: true + d2s_train_image_shape: [3, 640, 640] + +Architecture: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPHGNet_small + det: True + Neck: + name: LKPAN + out_channels: 256 + intracl: true + Head: + name: PFHeadLocal + k: 50 + mode: "large" + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: L2 + factor: 1e-6 + +PostProcess: + name: DBPostProcess + thresh: 0.2 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 0.5 + box_type: "poly" + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: TextDetDataset + data_dir: datasets/ICDAR2015 + label_file_list: + - datasets/ICDAR2015/train.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.8 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: 500 + - MakeShrinkMap: + shrink_ratio: 0.8 + min_text_size: 8 + total_epoch: 500 + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 4 + num_workers: 3 + +Eval: + dataset: + name: TextDetDataset + data_dir: datasets/ICDAR2015 + label_file_list: + - datasets/ICDAR2015/val.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: + resize_long: 736 + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 0 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv5/PP-OCRv5_mobile_det.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv5/PP-OCRv5_mobile_det.yml new file mode 100644 index 0000000000000000000000000000000000000000..b15fcb8a17c5a5e0ae342bfda96e5aaac5dacc54 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv5/PP-OCRv5_mobile_det.yml @@ -0,0 +1,174 @@ +Global: + model_name: PP-OCRv5_mobile_det # To use static model for inference. + debug: false + use_gpu: true + epoch_num: &epoch_num 500 + log_smooth_window: 20 + print_batch_step: 100 + save_model_dir: ./output/PP-OCRv5_mobile_det + save_epoch_step: 10 + eval_batch_step: + - 0 + - 1500 + cal_metric_during_train: false + checkpoints: + pretrained_model: https://paddleocr.bj.bcebos.com/pretrained/PPLCNetV3_x0_75_ocr_det.pdparams + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + d2s_train_image_shape: [3, 640, 640] + distributed: true + +Architecture: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPLCNetV3 + scale: 0.75 + det: True + Neck: + name: RSEFPN + out_channels: 96 + shortcut: True + Head: + name: DBHead + k: 50 + fix_nan: True + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 #(8*8c) + warmup_epoch: 2 + regularizer: + name: L2 + factor: 5.0e-05 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: *epoch_num + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + total_epoch: *epoch_num + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv5/PP-OCRv5_server_det.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv5/PP-OCRv5_server_det.yml new file mode 100644 index 0000000000000000000000000000000000000000..6b14dc67ec603b66c97a85f60e84532229b34c22 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv5/PP-OCRv5_server_det.yml @@ -0,0 +1,173 @@ +Global: + model_name: PP-OCRv5_server_det # To use static model for inference. + debug: false + use_gpu: true + epoch_num: &epoch_num 500 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/PP-OCRv5_server_det + save_epoch_step: 10 + eval_batch_step: + - 0 + - 1500 + cal_metric_during_train: false + checkpoints: + pretrained_model: https://paddle-model-ecology.bj.bcebos.com/paddlex/official_pretrained_model/PPHGNetV2_B4_ocr_det.pdparams + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + distributed: true + +Architecture: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPHGNetV2_B4 + det: True + Neck: + name: LKPAN + out_channels: 256 + intracl: true + Head: + name: PFHeadLocal + k: 50 + mode: "large" + + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 #(8*8c) + warmup_epoch: 2 + regularizer: + name: L2 + factor: 1e-6 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: *epoch_num + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + total_epoch: *epoch_num + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv6/PP-OCRv6_medium_det.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv6/PP-OCRv6_medium_det.yml new file mode 100644 index 0000000000000000000000000000000000000000..cfdb169a4291e5d180642e65cc6ac84dac206634 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv6/PP-OCRv6_medium_det.yml @@ -0,0 +1,195 @@ +Global: + model_name: PP-OCRv6_medium_det # To use static model for inference. + debug: false + use_gpu: true + use_ema: true + ema_decay: 0.9996 + ema_decay_type: threshold + epoch_num: &epoch_num 500 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/PP-OCRv6_medium_det + save_epoch_step: 10 + eval_batch_step: + - 0 + - 1500 + cal_metric_during_train: false + checkpoints: null + pretrained_model: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + d2s_train_image_shape: [3, 640, 640] + distributed: true + +Architecture: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPLCNetV4 + det: true + model_size: medium + Neck: + name: RepLKPAN + out_channels: 256 + intracl: true + Head: + name: DBHead + k: 50 + fix_nan: true + aux_in_channels: 256 + +Loss: + name: DBLoss + main_loss_type: DiceFocalLoss + alpha: 5 + beta: 10 + focal_alpha: 0.25 + focal_gamma: 2.5 + aux_weight_p4: 0.2 + aux_weight_p3: 0.3 + aux_weight_p2: 0.4 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 #(8*8c) + warmup_epoch: 2 + regularizer: + name: L2 + factor: 1.0e-05 + +PostProcess: + name: DBPostProcess + thresh: 0.2 + box_thresh: 0.45 + max_candidates: 3000 + unclip_ratio: 1.4 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: null + - ColorJitter: + brightness: 0.4 + contrast: 0.4 + saturation: 0.7 + hue: 0.1 + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + p: 0.5 + rotate: + - -45 + - 45 + fit_output: true + - type: Resize + args: + size: + - 0.1 + - 2 + - RandomPerspective: + prob: 0.3 + scale: 0.0 + shear: 20.0 + perspective: 0.001 + fit_output: true + fill_value: [0, 0, 0] + - RandomCrop: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: *epoch_num + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + total_epoch: *epoch_num + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: null + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv6/PP-OCRv6_small_det.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv6/PP-OCRv6_small_det.yml new file mode 100644 index 0000000000000000000000000000000000000000..a709edc5c9059792500b9e0bbd9b93ab6045221f --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv6/PP-OCRv6_small_det.yml @@ -0,0 +1,184 @@ +Global: + model_name: PP-OCRv6_small_det # To use static model for inference. + debug: false + use_gpu: true + use_ema: true + ema_decay: 0.9997 + ema_decay_type: threshold + epoch_num: &epoch_num 500 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/PP-OCRv6_small_det + save_epoch_step: 10 + eval_batch_step: + - 0 + - 1500 + cal_metric_during_train: false + checkpoints: null + pretrained_model: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + d2s_train_image_shape: [3, 640, 640] + distributed: true + +Architecture: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPLCNetV4 + det: true + model_size: small + Neck: + name: RepLKFPN + out_channels: 96 + dilated_kernel_size: 7 + shortcut: true + Head: + name: DBHead + k: 50 + fix_nan: true + aux_in_channels: 96 + +Loss: + name: DBLoss + main_loss_type: DiceFocalLoss + alpha: 5 + beta: 10 + focal_alpha: 0.25 + focal_gamma: 2.5 + aux_weight_p4: 0.2 + aux_weight_p3: 0.3 + aux_weight_p2: 0.4 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 #(8*8c) + warmup_epoch: 2 + regularizer: + name: L2 + factor: 1.0e-05 + +PostProcess: + name: DBPostProcess + thresh: 0.2 + box_thresh: 0.45 + max_candidates: 3000 + unclip_ratio: 1.4 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + p: 0.5 + rotate: + - -45 + - 45 + fit_output: true + - type: Resize + args: + size: + - 0.1 + - 2 + - RandomCrop: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: *epoch_num + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + total_epoch: *epoch_num + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: null + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/PP-OCRv6/PP-OCRv6_tiny_det.yml b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv6/PP-OCRv6_tiny_det.yml new file mode 100644 index 0000000000000000000000000000000000000000..f9b0ee0875abb7593d2db6b75d5b6f3dd4000974 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/PP-OCRv6/PP-OCRv6_tiny_det.yml @@ -0,0 +1,189 @@ +Global: + model_name: PP-OCRv6_tiny_det # To use static model for inference. + debug: false + use_gpu: true + use_ema: true + ema_decay: 0.9998 + ema_decay_type: threshold + epoch_num: &epoch_num 500 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/PP-OCRv6_tiny_det + save_epoch_step: 10 + eval_batch_step: + - 0 + - 1500 + cal_metric_during_train: false + checkpoints: null + pretrained_model: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + d2s_train_image_shape: [3, 640, 640] + distributed: true + +Architecture: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPLCNetV4 + det: true + model_size: tiny + Neck: + name: RepLKFPN + out_channels: 64 + dilated_kernel_size: 5 + shortcut: true + Head: + name: DBHead + k: 50 + fix_nan: true + aux_in_channels: 64 + +Loss: + name: DBLoss + main_loss_type: DiceFocalLoss + alpha: 5 + beta: 10 + focal_alpha: 0.25 + focal_gamma: 2.5 + aux_weight_p4: 0.2 + aux_weight_p3: 0.3 + aux_weight_p2: 0.4 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 #(8*8c) + warmup_epoch: 2 + regularizer: + name: L2 + factor: 1.0e-06 + +PostProcess: + name: DBPostProcess + thresh: 0.2 + box_thresh: 0.4 + max_candidates: 3000 + unclip_ratio: 1.4 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: null + - ColorJitter: + brightness: 0.4 + contrast: 0.4 + saturation: 0.7 + hue: 0.1 + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + p: 0.5 + rotate: + - -45 + - 45 + fit_output: true + - type: Resize + args: + size: + - 0.3 + - 2 + - RandomCrop: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: *epoch_num + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + total_epoch: *epoch_num + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: null + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_cml.yml b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_cml.yml new file mode 100644 index 0000000000000000000000000000000000000000..d35e08ae26eec65be1a8931cf76361b768e80db6 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_cml.yml @@ -0,0 +1,206 @@ +Global: + use_gpu: true + epoch_num: 1200 + log_smooth_window: 20 + print_batch_step: 2 + save_model_dir: ./output/ch_db_mv3/ + save_epoch_step: 1200 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [3000, 2000] + cal_metric_during_train: False + pretrained_model: + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_db/predicts_db.txt + use_amp: False + amp_level: O2 + amp_dtype: bfloat16 + +Architecture: + name: DistillationModel + algorithm: Distillation + model_type: det + Models: + Teacher: + pretrained: ./pretrain_models/ch_ppocr_server_v2.0_det_train/best_accuracy + freeze_params: true + return_all_feats: false + model_type: det + algorithm: DB + Transform: + Backbone: + name: ResNet_vd + layers: 18 + Neck: + name: DBFPN + out_channels: 256 + Head: + name: DBHead + k: 50 + Student: + pretrained: + freeze_params: false + return_all_feats: false + model_type: det + algorithm: DB + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: True + Neck: + name: DBFPN + out_channels: 96 + Head: + name: DBHead + k: 50 + Student2: + pretrained: + freeze_params: false + return_all_feats: false + model_type: det + algorithm: DB + Transform: + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: True + Neck: + name: DBFPN + out_channels: 96 + Head: + name: DBHead + k: 50 + +Loss: + name: CombinedLoss + loss_config_list: + - DistillationDilaDBLoss: + weight: 1.0 + model_name_pairs: + - ["Student", "Teacher"] + - ["Student2", "Teacher"] + key: maps + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + - DistillationDMLLoss: + model_name_pairs: + - ["Student", "Student2"] + maps_name: "thrink_maps" + weight: 1.0 + # act: None + model_name_pairs: ["Student", "Student2"] + key: maps + - DistillationDBLoss: + weight: 1.0 + model_name_list: ["Student", "Student2"] + # key: maps + # name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: DistillationDBPostProcess + model_name: ["Student", "Student2", "Teacher"] + # key: maps + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DistillationMetric + base_metric_name: DetMetric + main_indicator: hmean + key: "Student" + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - CopyPaste: + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [-10, 10] } } + - { 'type': Resize, 'args': { 'size': [0.5, 3] } } + - EastRandomCropData: + size: [960, 960] + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'threshold_map', 'threshold_mask', 'shrink_map', 'shrink_mask'] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_distill.yml b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_distill.yml new file mode 100644 index 0000000000000000000000000000000000000000..d24ee11f3dee9d17a587d6d4d0765abfe459fbc9 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_distill.yml @@ -0,0 +1,175 @@ +Global: + use_gpu: true + epoch_num: 1200 + log_smooth_window: 20 + print_batch_step: 2 + save_model_dir: ./output/ch_db_mv3/ + save_epoch_step: 1200 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [3000, 2000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/MobileNetV3_large_x0_5_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_db/predicts_db.txt + +Architecture: + name: DistillationModel + algorithm: Distillation + model_type: det + Models: + Student: + pretrained: ./pretrain_models/MobileNetV3_large_x0_5_pretrained + freeze_params: false + return_all_feats: false + model_type: det + algorithm: DB + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: True + Neck: + name: DBFPN + out_channels: 96 + Head: + name: DBHead + k: 50 + Teacher: + pretrained: ./pretrain_models/ch_ppocr_server_v2.0_det_train/best_accuracy + freeze_params: true + return_all_feats: false + model_type: det + algorithm: DB + Transform: + Backbone: + name: ResNet_vd + layers: 18 + Neck: + name: DBFPN + out_channels: 256 + Head: + name: DBHead + k: 50 + +Loss: + name: CombinedLoss + loss_config_list: + - DistillationDilaDBLoss: + weight: 1.0 + model_name_pairs: + - ["Student", "Teacher"] + key: maps + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + - DistillationDBLoss: + weight: 1.0 + model_name_list: ["Student"] + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: DistillationDBPostProcess + model_name: ["Student"] + key: head_out + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DistillationMetric + base_metric_name: DetMetric + main_indicator: hmean + key: "Student" + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - CopyPaste: + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [-10, 10] } } + - { 'type': Resize, 'args': { 'size': [0.5, 3] } } + - EastRandomCropData: + size: [960, 960] + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'threshold_map', 'threshold_mask', 'shrink_map', 'shrink_mask'] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: +# image_shape: [736, 1280] + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_dml.yml b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_dml.yml new file mode 100644 index 0000000000000000000000000000000000000000..8b160f63538d51dc57b08ba83f7ebf019e3c9dbb --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_dml.yml @@ -0,0 +1,178 @@ +Global: + use_gpu: true + epoch_num: 1200 + log_smooth_window: 20 + print_batch_step: 2 + save_model_dir: ./output/ch_db_mv3/ + save_epoch_step: 1200 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [3000, 2000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/MobileNetV3_large_x0_5_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_db/predicts_db.txt + +Architecture: + name: DistillationModel + algorithm: Distillation + model_type: det + Models: + Student: + pretrained: ./pretrain_models/MobileNetV3_large_x0_5_pretrained + freeze_params: false + return_all_feats: false + model_type: det + algorithm: DB + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: True + Neck: + name: DBFPN + out_channels: 96 + Head: + name: DBHead + k: 50 + Teacher: + pretrained: ./pretrain_models/MobileNetV3_large_x0_5_pretrained + freeze_params: false + return_all_feats: false + model_type: det + algorithm: DB + Transform: + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: True + Neck: + name: DBFPN + out_channels: 96 + Head: + name: DBHead + k: 50 + + +Loss: + name: CombinedLoss + loss_config_list: + - DistillationDMLLoss: + model_name_pairs: + - ["Student", "Teacher"] + maps_name: "thrink_maps" + weight: 1.0 + # act: None + model_name_pairs: ["Student", "Teacher"] + key: maps + - DistillationDBLoss: + weight: 1.0 + model_name_list: ["Student", "Teacher"] + # key: maps + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: DistillationDBPostProcess + model_name: ["Student", "Teacher"] + key: head_out + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DistillationMetric + base_metric_name: DetMetric + main_indicator: hmean + key: "Student" + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - CopyPaste: + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [-10, 10] } } + - { 'type': Resize, 'args': { 'size': [0.5, 3] } } + - EastRandomCropData: + size: [960, 960] + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'threshold_map', 'threshold_mask', 'shrink_map', 'shrink_mask'] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: +# image_shape: [736, 1280] + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_student.yml b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_student.yml new file mode 100644 index 0000000000000000000000000000000000000000..cca2a596ce73d7f66a14e5967e5926c5ee36295c --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv2/ch_PP-OCRv2_det_student.yml @@ -0,0 +1,132 @@ +Global: + use_gpu: true + epoch_num: 1200 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/ch_db_mv3/ + save_epoch_step: 1200 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [0, 400] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/student.pdparams + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_db/predicts_db.txt + +Architecture: + model_type: det + algorithm: DB + Transform: + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: True + Neck: + name: DBFPN + out_channels: 96 + Head: + name: DBHead + k: 50 + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [-10, 10] } } + - { 'type': Resize, 'args': { 'size': [0.5, 3] } } + - EastRandomCropData: + size: [960, 960] + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'threshold_map', 'threshold_mask', 'shrink_map', 'shrink_mask'] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: +# image_shape: [736, 1280] + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml new file mode 100644 index 0000000000000000000000000000000000000000..252d1599776a893b882723a9f7329c99458f3dc4 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml @@ -0,0 +1,226 @@ +Global: + debug: false + use_gpu: true + epoch_num: 500 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/ch_PP-OCR_v3_det/ + save_epoch_step: 100 + eval_batch_step: + - 0 + - 400 + cal_metric_during_train: false + pretrained_model: null + checkpoints: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + distributed: true + d2s_train_image_shape: [3, -1, -1] + amp_dtype: bfloat16 + +Architecture: + name: DistillationModel + algorithm: Distillation + model_type: det + Models: + Student: + pretrained: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: true + Neck: + name: RSEFPN + out_channels: 96 + shortcut: True + Head: + name: DBHead + k: 50 + Student2: + pretrained: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: true + Neck: + name: RSEFPN + out_channels: 96 + shortcut: True + Head: + name: DBHead + k: 50 + Teacher: + freeze_params: true + return_all_feats: false + model_type: det + algorithm: DB + Backbone: + name: ResNet_vd + in_channels: 3 + layers: 50 + Neck: + name: LKPAN + out_channels: 256 + Head: + name: DBHead + kernel_list: [7,2,2] + k: 50 + +Loss: + name: CombinedLoss + loss_config_list: + - DistillationDilaDBLoss: + weight: 1.0 + model_name_pairs: + - ["Student", "Teacher"] + - ["Student2", "Teacher"] + key: maps + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + - DistillationDMLLoss: + model_name_pairs: + - ["Student", "Student2"] + maps_name: "thrink_maps" + weight: 1.0 + model_name_pairs: ["Student", "Student2"] + key: maps + - DistillationDBLoss: + weight: 1.0 + model_name_list: ["Student", "Student2"] + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: L2 + factor: 5.0e-05 + +PostProcess: + name: DistillationDBPostProcess + model_name: ["Student"] + key: head_out + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DistillationMetric + base_metric_name: DetMetric + main_indicator: hmean + key: "Student" + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 960 + - 960 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_dml.yml b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_dml.yml new file mode 100644 index 0000000000000000000000000000000000000000..e85127a2ee37fc4a6c59066e465318792f9a0dd3 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_dml.yml @@ -0,0 +1,173 @@ +Global: + use_gpu: true + epoch_num: 1200 + log_smooth_window: 20 + print_batch_step: 2 + save_model_dir: ./output/ch_db_mv3/ + save_epoch_step: 1200 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [3000, 2000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/MobileNetV3_large_x0_5_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_db/predicts_db.txt + +Architecture: + name: DistillationModel + algorithm: Distillation + model_type: det + Models: + Student: + return_all_feats: false + model_type: det + algorithm: DB + Backbone: + name: ResNet_vd + in_channels: 3 + layers: 50 + Neck: + name: LKPAN + out_channels: 256 + Head: + name: DBHead + kernel_list: [7,2,2] + k: 50 + Student2: + return_all_feats: false + model_type: det + algorithm: DB + Backbone: + name: ResNet_vd + in_channels: 3 + layers: 50 + Neck: + name: LKPAN + out_channels: 256 + Head: + name: DBHead + kernel_list: [7,2,2] + k: 50 + + +Loss: + name: CombinedLoss + loss_config_list: + - DistillationDMLLoss: + model_name_pairs: + - ["Student", "Student2"] + maps_name: "thrink_maps" + weight: 1.0 + # act: None + model_name_pairs: ["Student", "Student2"] + key: maps + - DistillationDBLoss: + weight: 1.0 + model_name_list: ["Student", "Student2"] + # key: maps + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: DistillationDBPostProcess + model_name: ["Student", "Student2"] + key: head_out + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DistillationMetric + base_metric_name: DetMetric + main_indicator: hmean + key: "Student" + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - CopyPaste: + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [-10, 10] } } + - { 'type': Resize, 'args': { 'size': [0.5, 3] } } + - EastRandomCropData: + size: [960, 960] + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'threshold_map', 'threshold_mask', 'shrink_map', 'shrink_mask'] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: +# image_shape: [736, 1280] + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml new file mode 100644 index 0000000000000000000000000000000000000000..083383a00f5fcdaa43a06a6d13954ac24fdc636c --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml @@ -0,0 +1,163 @@ +Global: + debug: false + use_gpu: true + epoch_num: 500 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/ch_PP-OCR_V3_det/ + save_epoch_step: 100 + eval_batch_step: + - 0 + - 400 + cal_metric_during_train: false + pretrained_model: https://paddleocr.bj.bcebos.com/pretrained/MobileNetV3_large_x0_5_pretrained.pdparams + checkpoints: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + distributed: true + +Architecture: + model_type: det + algorithm: DB + Transform: + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: True + Neck: + name: RSEFPN + out_channels: 96 + shortcut: True + Head: + name: DBHead + k: 50 + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: L2 + factor: 5.0e-05 +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 +Metric: + name: DetMetric + main_indicator: hmean +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 960 + - 960 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 4 +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: null + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv4/ch_PP-OCRv4_det_cml.yml b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv4/ch_PP-OCRv4_det_cml.yml new file mode 100644 index 0000000000000000000000000000000000000000..fe582ba5b76208459a47563b9d02bf6aa2593b06 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv4/ch_PP-OCRv4_det_cml.yml @@ -0,0 +1,235 @@ +Global: + debug: false + use_gpu: true + epoch_num: 500 + log_smooth_window: 20 + print_batch_step: 20 + save_model_dir: ./output/ch_PP-OCRv4 + save_epoch_step: 50 + eval_batch_step: + - 0 + - 1000 + cal_metric_during_train: true + checkpoints: null + pretrained_model: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + distributed: true +Architecture: + name: DistillationModel + algorithm: Distillation + model_type: det + Models: + Student: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPLCNetNew + scale: 0.75 + pretrained: false + Neck: + name: RSEFPN + out_channels: 96 + shortcut: true + Head: + name: DBHead + k: 50 + Student2: + pretrained: null + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPLCNetNew + scale: 0.75 + pretrained: true + Neck: + name: RSEFPN + out_channels: 96 + shortcut: true + Head: + name: DBHead + k: 50 + Teacher: + pretrained: https://paddleocr.bj.bcebos.com/PP-OCRv4/chinese/ch_PP-OCRv4_det_cml_teacher_pretrained/teacher.pdparams + freeze_params: true + return_all_feats: false + model_type: det + algorithm: DB + Backbone: + name: ResNet_vd + in_channels: 3 + layers: 50 + Neck: + name: LKPAN + out_channels: 256 + Head: + name: DBHead + kernel_list: + - 7 + - 2 + - 2 + k: 50 +Loss: + name: CombinedLoss + loss_config_list: + - DistillationDilaDBLoss: + weight: 1.0 + model_name_pairs: + - - Student + - Teacher + - - Student2 + - Teacher + key: maps + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + - DistillationDMLLoss: + model_name_pairs: + - Student + - Student2 + maps_name: thrink_maps + weight: 1.0 + key: maps + - DistillationDBLoss: + weight: 1.0 + model_name_list: + - Student + - Student2 + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: L2 + factor: 5.0e-05 +PostProcess: + name: DistillationDBPostProcess + model_name: + - Student + key: head_out + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 +Metric: + name: DistillationMetric + base_metric_name: DetMetric + main_indicator: hmean + key: Student +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: 500 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + total_epoch: 500 + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 16 + num_workers: 8 +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: null + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv4/ch_PP-OCRv4_det_student.yml b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv4/ch_PP-OCRv4_det_student.yml new file mode 100644 index 0000000000000000000000000000000000000000..39b260cf3ad0a0bb9664a5165f3262962bba072e --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv4/ch_PP-OCRv4_det_student.yml @@ -0,0 +1,171 @@ +Global: + debug: false + use_gpu: true + epoch_num: &epoch_num 500 + log_smooth_window: 20 + print_batch_step: 100 + save_model_dir: ./output/ch_PP-OCRv4 + save_epoch_step: 10 + eval_batch_step: + - 0 + - 1500 + cal_metric_during_train: false + checkpoints: + pretrained_model: https://paddleocr.bj.bcebos.com/pretrained/PPLCNetV3_x0_75_ocr_det.pdparams + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + distributed: true + +Architecture: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPLCNetV3 + scale: 0.75 + det: True + Neck: + name: RSEFPN + out_channels: 96 + shortcut: True + Head: + name: DBHead + k: 50 + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 #(8*8c) + warmup_epoch: 2 + regularizer: + name: L2 + factor: 5.0e-05 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: *epoch_num + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + total_epoch: *epoch_num + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv4/ch_PP-OCRv4_det_teacher.yml b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv4/ch_PP-OCRv4_det_teacher.yml new file mode 100644 index 0000000000000000000000000000000000000000..b58af1cc9be9c20372caa1fc94d233c7e14c0773 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/ch_PP-OCRv4/ch_PP-OCRv4_det_teacher.yml @@ -0,0 +1,172 @@ +Global: + debug: false + use_gpu: true + epoch_num: &epoch_num 500 + log_smooth_window: 20 + print_batch_step: 100 + save_model_dir: ./output/ch_PP-OCRv4 + save_epoch_step: 10 + eval_batch_step: + - 0 + - 1500 + cal_metric_during_train: false + checkpoints: + pretrained_model: https://paddleocr.bj.bcebos.com/pretrained/PPHGNet_small_ocr_det.pdparams + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + distributed: true + +Architecture: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPHGNet_small + det: True + Neck: + name: LKPAN + out_channels: 256 + intracl: true + Head: + name: PFHeadLocal + k: 50 + mode: "large" + + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 #(8*8c) + warmup_epoch: 2 + regularizer: + name: L2 + factor: 1e-6 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: *epoch_num + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + total_epoch: *epoch_num + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/ch_ppocr_v2.0/ch_det_mv3_db_v2.0.yml b/ocr_webapp/PaddleOCR/configs/det/ch_ppocr_v2.0/ch_det_mv3_db_v2.0.yml new file mode 100644 index 0000000000000000000000000000000000000000..87718cad68743880dbfcd6b4c574bb09b9f53204 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/ch_ppocr_v2.0/ch_det_mv3_db_v2.0.yml @@ -0,0 +1,132 @@ +Global: + use_gpu: true + epoch_num: 1200 + log_smooth_window: 20 + print_batch_step: 2 + save_model_dir: ./output/ch_db_mv3/ + save_epoch_step: 1200 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [3000, 2000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/MobileNetV3_large_x0_5_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_db/predicts_db.txt + +Architecture: + model_type: det + algorithm: DB + Transform: + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + disable_se: True + Neck: + name: DBFPN + out_channels: 96 + Head: + name: DBHead + k: 50 + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [-10, 10] } } + - { 'type': Resize, 'args': { 'size': [0.5, 3] } } + - EastRandomCropData: + size: [960, 960] + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'threshold_map', 'threshold_mask', 'shrink_map', 'shrink_mask'] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: +# image_shape: [736, 1280] + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/ch_ppocr_v2.0/ch_det_res18_db_v2.0.yml b/ocr_webapp/PaddleOCR/configs/det/ch_ppocr_v2.0/ch_det_res18_db_v2.0.yml new file mode 100644 index 0000000000000000000000000000000000000000..e983c221e2cea87e35b1c5f0a67daed0a55c4257 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/ch_ppocr_v2.0/ch_det_res18_db_v2.0.yml @@ -0,0 +1,131 @@ +Global: + use_gpu: true + epoch_num: 1200 + log_smooth_window: 20 + print_batch_step: 2 + save_model_dir: ./output/ch_db_res18/ + save_epoch_step: 1200 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [3000, 2000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/ResNet18_vd_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_db/predicts_db.txt + +Architecture: + model_type: det + algorithm: DB + Transform: + Backbone: + name: ResNet_vd + layers: 18 + disable_se: True + Neck: + name: DBFPN + out_channels: 256 + Head: + name: DBHead + k: 50 + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [-10, 10] } } + - { 'type': Resize, 'args': { 'size': [0.5, 3] } } + - EastRandomCropData: + size: [960, 960] + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'threshold_map', 'threshold_mask', 'shrink_map', 'shrink_mask'] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: +# image_shape: [736, 1280] + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/det_mv3_db.yml b/ocr_webapp/PaddleOCR/configs/det/det_mv3_db.yml new file mode 100644 index 0000000000000000000000000000000000000000..8f5685ec2a314c4b6a00c6c636f36b9c9c5daf00 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_mv3_db.yml @@ -0,0 +1,133 @@ +Global: + use_gpu: true + use_xpu: false + use_mlu: false + epoch_num: 1200 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/db_mv3/ + save_epoch_step: 1200 + # evaluation is run every 2000 iterations + eval_batch_step: [0, 2000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/MobileNetV3_large_x0_5_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_db/predicts_db.txt + +Architecture: + model_type: det + algorithm: DB + Transform: + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + Neck: + name: DBFPN + out_channels: 256 + Head: + name: DBHead + k: 50 + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + learning_rate: 0.001 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [-10, 10] } } + - { 'type': Resize, 'args': { 'size': [0.5, 3] } } + - EastRandomCropData: + size: [640, 640] + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'threshold_map', 'threshold_mask', 'shrink_map', 'shrink_mask'] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 16 + num_workers: 8 + use_shared_memory: True + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: + image_shape: [736, 1280] + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 8 + use_shared_memory: True diff --git a/ocr_webapp/PaddleOCR/configs/det/det_mv3_east.yml b/ocr_webapp/PaddleOCR/configs/det/det_mv3_east.yml new file mode 100644 index 0000000000000000000000000000000000000000..4ae32ab004684f040b5939be7f348d468b4b8024 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_mv3_east.yml @@ -0,0 +1,109 @@ +Global: + use_gpu: true + epoch_num: 10000 + log_smooth_window: 20 + print_batch_step: 2 + save_model_dir: ./output/east_mv3/ + save_epoch_step: 1000 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [4000, 5000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/MobileNetV3_large_x0_5_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: + save_res_path: ./output/det_east/predicts_east.txt + +Architecture: + model_type: det + algorithm: EAST + Transform: + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + Neck: + name: EASTFPN + model_name: small + Head: + name: EASTHead + model_name: small + +Loss: + name: EASTLoss + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + # name: Cosine + learning_rate: 0.001 + # warmup_epoch: 0 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: EASTPostProcess + score_thresh: 0.8 + cover_thresh: 0.1 + nms_thresh: 0.2 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - EASTProcessTrain: + image_shape: [512, 512] + background_ratio: 0.125 + min_crop_side_ratio: 0.1 + min_text_size: 10 + - KeepKeys: + keep_keys: ['image', 'score_map', 'geo_map', 'training_mask'] # dataloader will return list in this order + loader: + shuffle: True + drop_last: False + batch_size_per_card: 16 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: + limit_side_len: 2400 + limit_type: max + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/configs/det/det_mv3_pse.yml b/ocr_webapp/PaddleOCR/configs/det/det_mv3_pse.yml new file mode 100644 index 0000000000000000000000000000000000000000..f80180ce7c1604cfda42ede36930e1bd9fdb8e21 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_mv3_pse.yml @@ -0,0 +1,135 @@ +Global: + use_gpu: true + epoch_num: 600 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/det_mv3_pse/ + save_epoch_step: 600 + # evaluation is run every 63 iterations + eval_batch_step: [ 0,63 ] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/MobileNetV3_large_x0_5_pretrained + checkpoints: #./output/det_r50_vd_pse_batch8_ColorJitter/best_accuracy + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_pse/predicts_pse.txt + +Architecture: + model_type: det + algorithm: PSE + Transform: null + Backbone: + name: MobileNetV3 + scale: 0.5 + model_name: large + Neck: + name: FPN + out_channels: 96 + Head: + name: PSEHead + hidden_dim: 96 + out_channels: 7 + +Loss: + name: PSELoss + alpha: 0.7 + ohem_ratio: 3 + kernel_sample_mask: pred + reduction: none + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Step + learning_rate: 0.001 + step_size: 200 + gamma: 0.1 + regularizer: + name: 'L2' + factor: 0.0005 + +PostProcess: + name: PSEPostProcess + thresh: 0 + box_thresh: 0.85 + min_area: 16 + box_type: quad # 'quad' or 'poly' + scale: 1 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [ 1.0 ] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - ColorJitter: + brightness: 0.12549019607843137 + saturation: 0.5 + - IaaAugment: + augmenter_args: + - { 'type': Resize, 'args': { 'size': [ 0.5, 3 ] } } + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [ -10, 10 ] } } + - MakePseGt: + kernel_num: 7 + min_shrink_ratio: 0.4 + size: 640 + - RandomCropImgMask: + size: [ 640,640 ] + main_key: gt_text + crop_keys: [ 'image', 'gt_text', 'gt_kernels', 'mask' ] + - NormalizeImage: + scale: 1./255. + mean: [ 0.485, 0.456, 0.406 ] + std: [ 0.229, 0.224, 0.225 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'image', 'gt_text', 'gt_kernels', 'mask' ] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 16 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + ratio_list: [ 1.0 ] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: + limit_side_len: 736 + limit_type: min + - NormalizeImage: + scale: 1./255. + mean: [ 0.485, 0.456, 0.406 ] + std: [ 0.229, 0.224, 0.225 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'image', 'shape', 'polys', 'ignore_tags' ] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 8 \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/configs/det/det_r18_vd_ct.yml b/ocr_webapp/PaddleOCR/configs/det/det_r18_vd_ct.yml new file mode 100644 index 0000000000000000000000000000000000000000..42922dfd22c0e49d20d50534c76fedae16b27a4a --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_r18_vd_ct.yml @@ -0,0 +1,107 @@ +Global: + use_gpu: true + epoch_num: 600 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/det_ct/ + save_epoch_step: 10 + # evaluation is run every 2000 iterations + eval_batch_step: [0,1000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/ResNet18_vd_pretrained.pdparams + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img623.jpg + save_res_path: ./output/det_ct/predicts_ct.txt + +Architecture: + model_type: det + algorithm: CT + Transform: + Backbone: + name: ResNet_vd + layers: 18 + Neck: + name: CTFPN + Head: + name: CT_Head + in_channels: 512 + hidden_dim: 128 + num_classes: 3 + +Loss: + name: CTLoss + +Optimizer: + name: Adam + lr: #PolynomialDecay + name: Linear + learning_rate: 0.001 + end_lr: 0. + epochs: 600 + step_each_epoch: 1254 + power: 0.9 + +PostProcess: + name: CTPostProcess + box_type: poly + +Metric: + name: CTMetric + main_indicator: f_score + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/total_text/train + label_file_list: + - ./train_data/total_text/train/train.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: RGB + channel_first: False + - CTLabelEncode: # Class handling label + - RandomScale: + - MakeShrink: + - GroupRandomHorizontalFlip: + - GroupRandomRotate: + - GroupRandomCropPadding: + - MakeCentripetalShift: + - ColorJitter: + brightness: 0.125 + saturation: 0.5 + - ToCHWImage: + - NormalizeImage: + - KeepKeys: + keep_keys: ['image', 'gt_kernel', 'training_mask', 'gt_instance', 'gt_kernel_instance', 'training_mask_distance', 'gt_distance'] # the order of the dataloader list + loader: + shuffle: True + drop_last: True + batch_size_per_card: 4 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/total_text/test + label_file_list: + - ./train_data/total_text/test/test.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: RGB + channel_first: False + - CTLabelEncode: # Class handling label + - ScaleAlignedShort: + - NormalizeImage: + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'texts'] # the order of the dataloader list + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/det_r50_db++_icdar15.yml b/ocr_webapp/PaddleOCR/configs/det/det_r50_db++_icdar15.yml new file mode 100644 index 0000000000000000000000000000000000000000..c4353477a651c8fb79e2250831743e11533c43be --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_r50_db++_icdar15.yml @@ -0,0 +1,164 @@ +Global: + debug: false + use_gpu: true + epoch_num: 1000 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/det_r50_icdar15/ + save_epoch_step: 200 + eval_batch_step: + - 0 + - 2000 + cal_metric_during_train: false + pretrained_model: ./pretrain_models/ResNet50_dcn_asf_synthtext_pretrained + checkpoints: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt +Architecture: + model_type: det + algorithm: DB++ + Transform: null + Backbone: + name: ResNet + layers: 50 + dcn_stage: [False, True, True, True] + Neck: + name: DBFPN + out_channels: 256 + use_asf: True + Head: + name: DBHead + k: 50 +Loss: + name: DBLoss + balance_loss: true + main_loss_type: BCELoss + alpha: 5 + beta: 10 + ohem_ratio: 3 +Optimizer: + name: Momentum + momentum: 0.9 + lr: + name: DecayLearningRate + learning_rate: 0.007 + epochs: 1000 + factor: 0.9 + end_lr: 0 + weight_decay: 0.0001 +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + box_type: 'quad' # 'quad' or 'poly' +Metric: + name: DetMetric + main_indicator: hmean +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: + - 1.0 + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 10 + keep_ratio: true + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - NormalizeImage: + scale: 1./255. + mean: + - 0.48109378172549 + - 0.45752457890196 + - 0.40787054090196 + std: + - 1.0 + - 1.0 + - 1.0 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 4 + num_workers: 8 +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: + image_shape: + - 1152 + - 2048 + - NormalizeImage: + scale: 1./255. + mean: + - 0.48109378172549 + - 0.45752457890196 + - 0.40787054090196 + std: + - 1.0 + - 1.0 + - 1.0 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/det_r50_db++_td_tr.yml b/ocr_webapp/PaddleOCR/configs/det/det_r50_db++_td_tr.yml new file mode 100644 index 0000000000000000000000000000000000000000..e50eec1f9124441ca0e5805f615735be3fde5ee4 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_r50_db++_td_tr.yml @@ -0,0 +1,167 @@ +Global: + debug: false + use_gpu: true + epoch_num: 1000 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/det_r50_td_tr/ + save_epoch_step: 200 + eval_batch_step: + - 0 + - 2000 + cal_metric_during_train: false + pretrained_model: ./pretrain_models/ResNet50_dcn_asf_synthtext_pretrained + checkpoints: null + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt +Architecture: + model_type: det + algorithm: DB++ + Transform: null + Backbone: + name: ResNet + layers: 50 + dcn_stage: [False, True, True, True] + Neck: + name: DBFPN + out_channels: 256 + use_asf: True + Head: + name: DBHead + k: 50 +Loss: + name: DBLoss + balance_loss: true + main_loss_type: BCELoss + alpha: 5 + beta: 10 + ohem_ratio: 3 +Optimizer: + name: Momentum + momentum: 0.9 + lr: + name: DecayLearningRate + learning_rate: 0.007 + epochs: 1000 + factor: 0.9 + end_lr: 0 + weight_decay: 0.0001 +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.5 + max_candidates: 1000 + unclip_ratio: 1.5 + box_type: 'quad' # 'quad' or 'poly' +Metric: + name: DetMetric + main_indicator: hmean +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/ + label_file_list: + - ./train_data/TD_TR/TD500/train_gt_labels.txt + - ./train_data/TD_TR/TR400/gt_labels.txt + ratio_list: + - 1.0 + - 1.0 + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 10 + keep_ratio: true + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - NormalizeImage: + scale: 1./255. + mean: + - 0.48109378172549 + - 0.45752457890196 + - 0.40787054090196 + std: + - 1.0 + - 1.0 + - 1.0 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 4 + num_workers: 8 +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/ + label_file_list: + - ./train_data/TD_TR/TD500/test_gt_labels.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: + image_shape: + - 736 + - 736 + keep_ratio: True + - NormalizeImage: + scale: 1./255. + mean: + - 0.48109378172549 + - 0.45752457890196 + - 0.40787054090196 + std: + - 1.0 + - 1.0 + - 1.0 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/det_r50_drrg_ctw.yml b/ocr_webapp/PaddleOCR/configs/det/det_r50_drrg_ctw.yml new file mode 100644 index 0000000000000000000000000000000000000000..f67c926f3a8294a41df0751357061c69a895549e --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_r50_drrg_ctw.yml @@ -0,0 +1,133 @@ +Global: + use_gpu: true + epoch_num: 1200 + log_smooth_window: 20 + print_batch_step: 5 + save_model_dir: ./output/det_r50_drrg_ctw/ + save_epoch_step: 100 + # evaluation is run every 1260 iterations + eval_batch_step: [37800, 1260] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/ResNet50_vd_ssld_pretrained.pdparams + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_drrg/predicts_drrg.txt + + +Architecture: + model_type: det + algorithm: DRRG + Transform: + Backbone: + name: ResNet_vd + layers: 50 + Neck: + name: FPN_UNet + in_channels: [256, 512, 1024, 2048] + out_channels: 32 + Head: + name: DRRGHead + in_channels: 32 + text_region_thr: 0.3 + center_region_thr: 0.4 +Loss: + name: DRRGLoss + +Optimizer: + name: Momentum + momentum: 0.9 + lr: + name: DecayLearningRate + learning_rate: 0.028 + epochs: 1200 + factor: 0.9 + end_lr: 0.0000001 + weight_decay: 0.0001 + +PostProcess: + name: DRRGPostprocess + link_thr: 0.8 + +Metric: + name: DetFCEMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/ctw1500/imgs/ + label_file_list: + - ./train_data/ctw1500/imgs/training.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + ignore_orientation: True + - DetLabelEncode: # Class handling label + - ColorJitter: + brightness: 0.12549019607843137 + saturation: 0.5 + - RandomScaling: + - RandomCropFlip: + crop_ratio: 0.5 + - RandomCropPolyInstances: + crop_ratio: 0.8 + min_side_ratio: 0.3 + - RandomRotatePolyInstances: + rotate_ratio: 0.5 + max_angle: 60 + pad_with_fixed_color: False + - SquareResizePad: + target_size: 800 + pad_ratio: 0.6 + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - DRRGTargets: + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'gt_text_mask', 'gt_center_region_mask', 'gt_mask', + 'gt_top_height_map', 'gt_bot_height_map', 'gt_sin_map', + 'gt_cos_map', 'gt_comp_attribs'] # dataloader will return list in this order + loader: + shuffle: True + drop_last: False + batch_size_per_card: 4 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/ctw1500/imgs/ + label_file_list: + - ./train_data/ctw1500/imgs/test.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + ignore_orientation: True + - DetLabelEncode: # Class handling label + - DetResizeForTest: + limit_type: 'min' + limit_side_len: 640 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - Pad: + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_db.yml b/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_db.yml new file mode 100644 index 0000000000000000000000000000000000000000..288dcc8c1a5934b35d329acc38fa85451ebaeb19 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_db.yml @@ -0,0 +1,128 @@ +Global: + use_gpu: true + epoch_num: 1200 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/det_r50_vd/ + save_epoch_step: 1200 + # evaluation is run every 2000 iterations + eval_batch_step: [0,2000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/ResNet50_vd_ssld_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_db/predicts_db.txt + +Architecture: + model_type: det + algorithm: DB + Transform: + Backbone: + name: ResNet_vd + layers: 50 + Neck: + name: DBFPN + out_channels: 256 + Head: + name: DBHead + k: 50 + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + learning_rate: 0.001 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.7 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [-10, 10] } } + - { 'type': Resize, 'args': { 'size': [0.5, 3] } } + - EastRandomCropData: + size: [640, 640] + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'threshold_map', 'threshold_mask', 'shrink_map', 'shrink_mask'] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 16 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: + image_shape: [736, 1280] + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 8 diff --git a/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_dcn_fce_ctw.yml b/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_dcn_fce_ctw.yml new file mode 100644 index 0000000000000000000000000000000000000000..3a4075b322a173796f26ebdbe5b83ba98e98e72d --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_dcn_fce_ctw.yml @@ -0,0 +1,139 @@ +Global: + use_gpu: true + epoch_num: 1500 + log_smooth_window: 20 + print_batch_step: 20 + save_model_dir: ./output/det_r50_dcn_fce_ctw/ + save_epoch_step: 100 + # evaluation is run every 835 iterations + eval_batch_step: [0, 835] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/ResNet50_vd_ssld_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_fce/predicts_fce.txt + + +Architecture: + model_type: det + algorithm: FCE + Transform: + Backbone: + name: ResNet_vd + layers: 50 + dcn_stage: [False, True, True, True] + out_indices: [1,2,3] + Neck: + name: FCEFPN + out_channels: 256 + has_extra_convs: False + extra_stage: 0 + Head: + name: FCEHead + fourier_degree: 5 +Loss: + name: FCELoss + fourier_degree: 5 + num_sample: 50 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + learning_rate: 0.0001 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: FCEPostProcess + scales: [8, 16, 32] + alpha: 1.0 + beta: 1.0 + fourier_degree: 5 + box_type: 'poly' + +Metric: + name: DetFCEMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/ctw1500/imgs/ + label_file_list: + - ./train_data/ctw1500/imgs/training.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + ignore_orientation: True + - DetLabelEncode: # Class handling label + - ColorJitter: + brightness: 0.142 + saturation: 0.5 + contrast: 0.5 + - RandomScaling: + - RandomCropFlip: + crop_ratio: 0.5 + - RandomCropPolyInstances: + crop_ratio: 0.8 + min_side_ratio: 0.3 + - RandomRotatePolyInstances: + rotate_ratio: 0.5 + max_angle: 30 + pad_with_fixed_color: False + - SquareResizePad: + target_size: 800 + pad_ratio: 0.6 + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - FCENetTargets: + fourier_degree: 5 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'p3_maps', 'p4_maps', 'p5_maps'] # dataloader will return list in this order + loader: + shuffle: True + drop_last: False + batch_size_per_card: 6 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/ctw1500/imgs/ + label_file_list: + - ./train_data/ctw1500/imgs/test.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + ignore_orientation: True + - DetLabelEncode: # Class handling label + - DetResizeForTest: + limit_type: 'min' + limit_side_len: 736 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - Pad: + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_east.yml b/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_east.yml new file mode 100644 index 0000000000000000000000000000000000000000..af90ef0adb929955141ef31779e917e4d73057ee --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_east.yml @@ -0,0 +1,108 @@ +Global: + use_gpu: true + epoch_num: 10000 + log_smooth_window: 20 + print_batch_step: 2 + save_model_dir: ./output/east_r50_vd/ + save_epoch_step: 1000 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [4000, 5000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/ResNet50_vd_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: + save_res_path: ./output/det_east/predicts_east.txt + +Architecture: + model_type: det + algorithm: EAST + Transform: + Backbone: + name: ResNet_vd + layers: 50 + Neck: + name: EASTFPN + model_name: large + Head: + name: EASTHead + model_name: large + +Loss: + name: EASTLoss + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + # name: Cosine + learning_rate: 0.001 + # warmup_epoch: 0 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: EASTPostProcess + score_thresh: 0.8 + cover_thresh: 0.1 + nms_thresh: 0.2 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - EASTProcessTrain: + image_shape: [512, 512] + background_ratio: 0.125 + min_crop_side_ratio: 0.1 + min_text_size: 10 + - KeepKeys: + keep_keys: ['image', 'score_map', 'geo_map', 'training_mask'] # dataloader will return list in this order + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: + limit_side_len: 2400 + limit_type: max + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_pse.yml b/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_pse.yml new file mode 100644 index 0000000000000000000000000000000000000000..1a971564fda2ce89bf091808dedb361f1caeddc3 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_pse.yml @@ -0,0 +1,134 @@ +Global: + use_gpu: true + epoch_num: 600 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/det_r50_vd_pse/ + save_epoch_step: 600 + # evaluation is run every 125 iterations + eval_batch_step: [ 0,125 ] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/ResNet50_vd_ssld_pretrained + checkpoints: #./output/det_r50_vd_pse_batch8_ColorJitter/best_accuracy + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_pse/predicts_pse.txt + +Architecture: + model_type: det + algorithm: PSE + Transform: + Backbone: + name: ResNet_vd + layers: 50 + Neck: + name: FPN + out_channels: 256 + Head: + name: PSEHead + hidden_dim: 256 + out_channels: 7 + +Loss: + name: PSELoss + alpha: 0.7 + ohem_ratio: 3 + kernel_sample_mask: pred + reduction: none + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Step + learning_rate: 0.0001 + step_size: 200 + gamma: 0.1 + regularizer: + name: 'L2' + factor: 0.0005 + +PostProcess: + name: PSEPostProcess + thresh: 0 + box_thresh: 0.85 + min_area: 16 + box_type: quad # 'quad' or 'poly' + scale: 1 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [ 1.0 ] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - ColorJitter: + brightness: 0.12549019607843137 + saturation: 0.5 + - IaaAugment: + augmenter_args: + - { 'type': Resize, 'args': { 'size': [ 0.5, 3 ] } } + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [ -10, 10 ] } } + - MakePseGt: + kernel_num: 7 + min_shrink_ratio: 0.4 + size: 640 + - RandomCropImgMask: + size: [ 640,640 ] + main_key: gt_text + crop_keys: [ 'image', 'gt_text', 'gt_kernels', 'mask' ] + - NormalizeImage: + scale: 1./255. + mean: [ 0.485, 0.456, 0.406 ] + std: [ 0.229, 0.224, 0.225 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'image', 'gt_text', 'gt_kernels', 'mask' ] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + ratio_list: [ 1.0 ] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: + limit_side_len: 736 + limit_type: min + - NormalizeImage: + scale: 1./255. + mean: [ 0.485, 0.456, 0.406 ] + std: [ 0.229, 0.224, 0.225 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'image', 'shape', 'polys', 'ignore_tags' ] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 8 \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_sast_icdar15.yml b/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_sast_icdar15.yml new file mode 100644 index 0000000000000000000000000000000000000000..e1bf6fad0c4f526c2b635494560446e779bdb572 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_sast_icdar15.yml @@ -0,0 +1,109 @@ +Global: + use_gpu: true + epoch_num: 5000 + log_smooth_window: 20 + print_batch_step: 2 + save_model_dir: ./output/sast_r50_vd_ic15/ + save_epoch_step: 1000 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [4000, 5000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/ResNet50_vd_ssld_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: + save_res_path: ./output/sast_r50_vd_ic15/predicts_sast.txt + + +Architecture: + model_type: det + algorithm: SAST + Transform: + Backbone: + name: ResNet_SAST + layers: 50 + Neck: + name: SASTFPN + with_cab: True + Head: + name: SASTHead + +Loss: + name: SASTLoss + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + # name: Cosine + learning_rate: 0.001 + # warmup_epoch: 0 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: SASTPostProcess + score_thresh: 0.5 + sample_pts_num: 2 + nms_thresh: 0.2 + expand_scale: 1.0 + shrink_ratio_of_width: 0.3 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/ + label_file_list: [./train_data/icdar2013/train_label_json.txt, ./train_data/icdar2015/train_label_json.txt, ./train_data/icdar17_mlt_latin/train_label_json.txt, ./train_data/coco_text_icdar_4pts/train_label_json.txt] + ratio_list: [0.1, 0.45, 0.3, 0.15] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - SASTProcessTrain: + image_shape: [512, 512] + min_crop_side_ratio: 0.3 + min_crop_size: 24 + min_text_size: 4 + max_text_size: 512 + - KeepKeys: + keep_keys: ['image', 'score_map', 'border_map', 'training_mask', 'tvo_map', 'tco_map'] # dataloader will return list in this order + loader: + shuffle: True + drop_last: False + batch_size_per_card: 4 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: + resize_long: 1536 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_sast_totaltext.yml b/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_sast_totaltext.yml new file mode 100644 index 0000000000000000000000000000000000000000..44a0766b1bf2790a9633602bc637932529046e34 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_r50_vd_sast_totaltext.yml @@ -0,0 +1,108 @@ +Global: + use_gpu: true + epoch_num: 5000 + log_smooth_window: 20 + print_batch_step: 2 + save_model_dir: ./output/sast_r50_vd_tt/ + save_epoch_step: 1000 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [4000, 5000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/ResNet50_vd_ssld_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: + save_res_path: ./output/sast_r50_vd_tt/predicts_sast.txt + +Architecture: + model_type: det + algorithm: SAST + Transform: + Backbone: + name: ResNet_SAST + layers: 50 + Neck: + name: SASTFPN + with_cab: True + Head: + name: SASTHead + +Loss: + name: SASTLoss + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + # name: Cosine + learning_rate: 0.001 + # warmup_epoch: 0 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: SASTPostProcess + score_thresh: 0.5 + sample_pts_num: 6 + nms_thresh: 0.2 + expand_scale: 1.2 + shrink_ratio_of_width: 0.2 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/ + label_file_list: [./train_data/art_latin_icdar_14pt/train_no_tt_test/train_label_json.txt, ./train_data/total_text_icdar_14pt/train_label_json.txt] + ratio_list: [0.5, 0.5] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - SASTProcessTrain: + image_shape: [512, 512] + min_crop_side_ratio: 0.3 + min_crop_size: 24 + min_text_size: 4 + max_text_size: 512 + - KeepKeys: + keep_keys: ['image', 'score_map', 'border_map', 'training_mask', 'tvo_map', 'tco_map'] # dataloader will return list in this order + loader: + shuffle: True + drop_last: False + batch_size_per_card: 4 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/ + label_file_list: + - ./train_data/total_text_icdar_14pt/test_label_json.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: + resize_long: 768 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 \ No newline at end of file diff --git a/ocr_webapp/PaddleOCR/configs/det/det_repsvtr_db.yml b/ocr_webapp/PaddleOCR/configs/det/det_repsvtr_db.yml new file mode 100644 index 0000000000000000000000000000000000000000..b772057815017fd920a6727fed1e59b28738ab2f --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_repsvtr_db.yml @@ -0,0 +1,169 @@ +Global: + debug: false + use_gpu: true + epoch_num: &epoch_num 500 + log_smooth_window: 20 + print_batch_step: 100 + save_model_dir: ./output/det_repsvtr_db + save_epoch_step: 10 + eval_batch_step: + - 0 + - 1000 + cal_metric_during_train: false + checkpoints: + pretrained_model: + save_inference_dir: null + use_visualdl: false + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./checkpoints/det_db/predicts_db.txt + distributed: true + +Architecture: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: RepSVTR_det + Neck: + name: RSEFPN + out_channels: 96 + shortcut: True + Head: + name: DBHead + k: 50 + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 #(8*8c) + warmup_epoch: 2 + regularizer: + name: L2 + factor: 5.0e-05 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - CopyPaste: null + - IaaAugment: + augmenter_args: + - type: Fliplr + args: + p: 0.5 + - type: Affine + args: + rotate: + - -10 + - 10 + - type: Resize + args: + size: + - 0.5 + - 3 + - EastRandomCropData: + size: + - 640 + - 640 + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + total_epoch: *epoch_num + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + total_epoch: *epoch_num + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - threshold_map + - threshold_mask + - shrink_map + - shrink_mask + loader: + shuffle: true + drop_last: false + batch_size_per_card: 8 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: + img_mode: BGR + channel_first: false + - DetLabelEncode: null + - DetResizeForTest: + - NormalizeImage: + scale: 1./255. + mean: + - 0.485 + - 0.456 + - 0.406 + std: + - 0.229 + - 0.224 + - 0.225 + order: hwc + - ToCHWImage: null + - KeepKeys: + keep_keys: + - image + - shape + - polys + - ignore_tags + loader: + shuffle: false + drop_last: false + batch_size_per_card: 1 + num_workers: 2 +profiler_options: null diff --git a/ocr_webapp/PaddleOCR/configs/det/det_res18_db_v2.0.yml b/ocr_webapp/PaddleOCR/configs/det/det_res18_db_v2.0.yml new file mode 100644 index 0000000000000000000000000000000000000000..e983c221e2cea87e35b1c5f0a67daed0a55c4257 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/det/det_res18_db_v2.0.yml @@ -0,0 +1,131 @@ +Global: + use_gpu: true + epoch_num: 1200 + log_smooth_window: 20 + print_batch_step: 2 + save_model_dir: ./output/ch_db_res18/ + save_epoch_step: 1200 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [3000, 2000] + cal_metric_during_train: False + pretrained_model: ./pretrain_models/ResNet18_vd_pretrained + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: doc/imgs_en/img_10.jpg + save_res_path: ./output/det_db/predicts_db.txt + +Architecture: + model_type: det + algorithm: DB + Transform: + Backbone: + name: ResNet_vd + layers: 18 + disable_se: True + Neck: + name: DBFPN + out_channels: 256 + Head: + name: DBHead + k: 50 + +Loss: + name: DBLoss + balance_loss: true + main_loss_type: DiceLoss + alpha: 5 + beta: 10 + ohem_ratio: 3 + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 2 + regularizer: + name: 'L2' + factor: 0 + +PostProcess: + name: DBPostProcess + thresh: 0.3 + box_thresh: 0.6 + max_candidates: 1000 + unclip_ratio: 1.5 + +Metric: + name: DetMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/train_icdar2015_label.txt + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - IaaAugment: + augmenter_args: + - { 'type': Fliplr, 'args': { 'p': 0.5 } } + - { 'type': Affine, 'args': { 'rotate': [-10, 10] } } + - { 'type': Resize, 'args': { 'size': [0.5, 3] } } + - EastRandomCropData: + size: [960, 960] + max_tries: 50 + keep_ratio: true + - MakeBorderMap: + shrink_ratio: 0.4 + thresh_min: 0.3 + thresh_max: 0.7 + - MakeShrinkMap: + shrink_ratio: 0.4 + min_text_size: 8 + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'threshold_map', 'threshold_mask', 'shrink_map', 'shrink_mask'] # the order of the dataloader list + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/icdar2015/text_localization/ + label_file_list: + - ./train_data/icdar2015/text_localization/test_icdar2015_label.txt + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - DetLabelEncode: # Class handling label + - DetResizeForTest: +# image_shape: [736, 1280] + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: ['image', 'shape', 'polys', 'ignore_tags'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/e2e/e2e_r50_vd_pg.yml b/ocr_webapp/PaddleOCR/configs/e2e/e2e_r50_vd_pg.yml new file mode 100644 index 0000000000000000000000000000000000000000..4642f544868f720d413f7f5242740705bc9fd0a5 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/e2e/e2e_r50_vd_pg.yml @@ -0,0 +1,121 @@ +Global: + use_gpu: True + epoch_num: 600 + log_smooth_window: 20 + print_batch_step: 10 + save_model_dir: ./output/pgnet_r50_vd_totaltext/ + save_epoch_step: 10 + # evaluation is run every 0 iterationss after the 1000th iteration + eval_batch_step: [ 0, 1000 ] + cal_metric_during_train: False + pretrained_model: + checkpoints: + save_inference_dir: + use_visualdl: False + infer_img: + infer_visual_type: EN # two mode: EN is for english datasets, CN is for chinese datasets + valid_set: totaltext # two mode: totaltext valid curved words, partvgg valid non-curved words + save_res_path: ./output/pgnet_r50_vd_totaltext/predicts_pgnet.txt + character_dict_path: ppocr/utils/ic15_dict.txt + character_type: EN + max_text_length: 50 # the max length in seq + max_text_nums: 30 # the max seq nums in a pic + tcl_len: 64 + +Architecture: + model_type: e2e + algorithm: PGNet + Transform: + Backbone: + name: ResNet + layers: 50 + Neck: + name: PGFPN + Head: + name: PGHead + character_dict_path: ppocr/utils/ic15_dict.txt # the same as Global:character_dict_path + +Loss: + name: PGLoss + tcl_bs: 64 + max_text_length: 50 # the same as Global: max_text_length + max_text_nums: 30 # the same as Global:max_text_nums + pad_num: 36 # the length of dict for pad + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Cosine + learning_rate: 0.001 + warmup_epoch: 50 + regularizer: + name: 'L2' + factor: 0.0001 + +PostProcess: + name: PGPostProcess + score_thresh: 0.5 + mode: fast # fast or slow two ways + point_gather_mode: align # same as PGProcessTrain: point_gather_mode + +Metric: + name: E2EMetric + mode: A # two ways for eval, A: label from txt, B: label from gt_mat + gt_mat_dir: ./train_data/total_text/gt # the dir of gt_mat + character_dict_path: ppocr/utils/ic15_dict.txt + main_indicator: f_score_e2e + +Train: + dataset: + name: PGDataSet + data_dir: ./train_data/total_text/train + label_file_list: [./train_data/total_text/train/train.txt] + ratio_list: [1.0] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - E2ELabelEncodeTrain: + - PGProcessTrain: + batch_size: 14 # same as loader: batch_size_per_card + use_resize: True + use_random_crop: False + min_crop_size: 24 + min_text_size: 4 + max_text_size: 512 + point_gather_mode: align # two mode: align and none, align mode is better than none mode + - KeepKeys: + keep_keys: [ 'images', 'tcl_maps', 'tcl_label_maps', 'border_maps','direction_maps', 'training_masks', 'label_list', 'pos_list', 'pos_mask' ] # dataloader will return list in this order + loader: + shuffle: True + drop_last: True + batch_size_per_card: 14 + num_workers: 16 + +Eval: + dataset: + name: PGDataSet + data_dir: ./train_data/total_text/test + label_file_list: [./train_data/total_text/test/test.txt] + transforms: + - DecodeImage: # load image + img_mode: BGR + channel_first: False + - E2ELabelEncodeTest: + - E2EResizeForTest: + max_side_len: 768 + - NormalizeImage: + scale: 1./255. + mean: [ 0.485, 0.456, 0.406 ] + std: [ 0.229, 0.224, 0.225 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'image', 'shape', 'polys', 'texts', 'ignore_tags', 'img_id'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 2 diff --git a/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/re_layoutlmv2_xfund_zh.yml b/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/re_layoutlmv2_xfund_zh.yml new file mode 100644 index 0000000000000000000000000000000000000000..3e3578d8cac1aadd484f583dbe0955f7c47fca73 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/re_layoutlmv2_xfund_zh.yml @@ -0,0 +1,123 @@ +Global: + use_gpu: True + epoch_num: &epoch_num 200 + log_smooth_window: 10 + print_batch_step: 10 + save_model_dir: ./output/re_layoutlmv2_xfund_zh + save_epoch_step: 2000 + # evaluation is run every 10 iterations after the 0th iteration + eval_batch_step: [ 0, 19 ] + cal_metric_during_train: False + save_inference_dir: + use_visualdl: False + seed: 2022 + infer_img: ppstructure/docs/kie/input/zh_val_21.jpg + save_res_path: ./output/re_layoutlmv2_xfund_zh/res/ + +Architecture: + model_type: kie + algorithm: &algorithm "LayoutLMv2" + Transform: + Backbone: + name: LayoutLMv2ForRe + pretrained: True + checkpoints: + +Loss: + name: LossFromOutput + key: loss + reduction: mean + +Optimizer: + name: AdamW + beta1: 0.9 + beta2: 0.999 + clip_norm: 10 + lr: + learning_rate: 0.00005 + warmup_epoch: 10 + regularizer: + name: L2 + factor: 0.00000 + +PostProcess: + name: VQAReTokenLayoutLMPostProcess + +Metric: + name: VQAReTokenMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: train_data/XFUND/zh_train/image + label_file_list: + - train_data/XFUND/zh_train/train.json + ratio_list: [ 1.0 ] + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - VQATokenLabelEncode: # Class handling label + contains_re: True + algorithm: *algorithm + class_path: &class_path train_data/XFUND/class_list_xfun.txt + - VQATokenPad: + max_seq_len: &max_seq_len 512 + return_attention_mask: True + - VQAReTokenRelation: + - VQAReTokenChunk: + max_seq_len: *max_seq_len + - Resize: + size: [224,224] + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'input_ids', 'bbox', 'attention_mask', 'token_type_ids','image', 'entities', 'relations'] # dataloader will return list in this order + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 8 + collate_fn: ListCollator + +Eval: + dataset: + name: SimpleDataSet + data_dir: train_data/XFUND/zh_val/image + label_file_list: + - train_data/XFUND/zh_val/val.json + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - VQATokenLabelEncode: # Class handling label + contains_re: True + algorithm: *algorithm + class_path: *class_path + - VQATokenPad: + max_seq_len: *max_seq_len + return_attention_mask: True + - VQAReTokenRelation: + - VQAReTokenChunk: + max_seq_len: *max_seq_len + - Resize: + size: [224,224] + - NormalizeImage: + scale: 1./255. + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'input_ids', 'bbox', 'attention_mask', 'token_type_ids', 'image','entities', 'relations'] # dataloader will return list in this order + loader: + shuffle: False + drop_last: False + batch_size_per_card: 8 + num_workers: 8 + collate_fn: ListCollator diff --git a/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/re_layoutxlm_xfund_zh.yml b/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/re_layoutxlm_xfund_zh.yml new file mode 100644 index 0000000000000000000000000000000000000000..99dc771d150b15847486c096529a2828b9c0c05a --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/re_layoutxlm_xfund_zh.yml @@ -0,0 +1,123 @@ +Global: + use_gpu: True + epoch_num: &epoch_num 130 + log_smooth_window: 10 + print_batch_step: 10 + save_model_dir: ./output/re_layoutxlm_xfund_zh + save_epoch_step: 2000 + # evaluation is run every 10 iterations after the 0th iteration + eval_batch_step: [ 0, 19 ] + cal_metric_during_train: False + save_inference_dir: + use_visualdl: False + seed: 2022 + infer_img: ppstructure/docs/kie/input/zh_val_21.jpg + save_res_path: ./output/re_layoutxlm_xfund_zh/res/ + +Architecture: + model_type: kie + algorithm: &algorithm "LayoutXLM" + Transform: + Backbone: + name: LayoutXLMForRe + pretrained: True + checkpoints: + +Loss: + name: LossFromOutput + key: loss + reduction: mean + +Optimizer: + name: AdamW + beta1: 0.9 + beta2: 0.999 + clip_norm: 10 + lr: + learning_rate: 0.00005 + warmup_epoch: 10 + regularizer: + name: L2 + factor: 0.00000 + +PostProcess: + name: VQAReTokenLayoutLMPostProcess + +Metric: + name: VQAReTokenMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: train_data/XFUND/zh_train/image + label_file_list: + - train_data/XFUND/zh_train/train.json + ratio_list: [ 1.0 ] + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - VQATokenLabelEncode: # Class handling label + contains_re: True + algorithm: *algorithm + class_path: &class_path train_data/XFUND/class_list_xfun.txt + - VQATokenPad: + max_seq_len: &max_seq_len 512 + return_attention_mask: True + - VQAReTokenRelation: + - VQAReTokenChunk: + max_seq_len: *max_seq_len + - TensorizeEntitiesRelations: + - Resize: + size: [224,224] + - NormalizeImage: + scale: 1 + mean: [ 123.675, 116.28, 103.53 ] + std: [ 58.395, 57.12, 57.375 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'input_ids', 'bbox','attention_mask', 'token_type_ids', 'image', 'entities', 'relations'] # dataloader will return list in this order + loader: + shuffle: True + drop_last: False + batch_size_per_card: 2 + num_workers: 8 + +Eval: + dataset: + name: SimpleDataSet + data_dir: train_data/XFUND/zh_val/image + label_file_list: + - train_data/XFUND/zh_val/val.json + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - VQATokenLabelEncode: # Class handling label + contains_re: True + algorithm: *algorithm + class_path: *class_path + - VQATokenPad: + max_seq_len: *max_seq_len + return_attention_mask: True + - VQAReTokenRelation: + - VQAReTokenChunk: + max_seq_len: *max_seq_len + - TensorizeEntitiesRelations: + - Resize: + size: [224,224] + - NormalizeImage: + scale: 1 + mean: [ 123.675, 116.28, 103.53 ] + std: [ 58.395, 57.12, 57.375 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'input_ids', 'bbox', 'attention_mask', 'token_type_ids', 'image', 'entities', 'relations'] # dataloader will return list in this order + loader: + shuffle: False + drop_last: False + batch_size_per_card: 8 + num_workers: 8 diff --git a/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/ser_layoutlm_xfund_zh.yml b/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/ser_layoutlm_xfund_zh.yml new file mode 100644 index 0000000000000000000000000000000000000000..5424d7e1fb5ab472644ec5b00225921670941025 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/ser_layoutlm_xfund_zh.yml @@ -0,0 +1,121 @@ +Global: + use_gpu: True + epoch_num: &epoch_num 200 + log_smooth_window: 10 + print_batch_step: 10 + save_model_dir: ./output/ser_layoutlm_xfund_zh + save_epoch_step: 2000 + # evaluation is run every 10 iterations after the 0th iteration + eval_batch_step: [ 0, 19 ] + cal_metric_during_train: False + save_inference_dir: + use_visualdl: False + seed: 2022 + infer_img: ppstructure/docs/kie/input/zh_val_42.jpg + save_res_path: ./output/re_layoutlm_xfund_zh/res + +Architecture: + model_type: kie + algorithm: &algorithm "LayoutLM" + Transform: + Backbone: + name: LayoutLMForSer + pretrained: True + checkpoints: + num_classes: &num_classes 7 + +Loss: + name: VQASerTokenLayoutLMLoss + num_classes: *num_classes + +Optimizer: + name: AdamW + beta1: 0.9 + beta2: 0.999 + lr: + name: Linear + learning_rate: 0.00005 + epochs: *epoch_num + warmup_epoch: 2 + regularizer: + name: L2 + factor: 0.00000 + +PostProcess: + name: VQASerTokenLayoutLMPostProcess + class_path: &class_path train_data/XFUND/class_list_xfun.txt + +Metric: + name: VQASerTokenMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: train_data/XFUND/zh_train/image + label_file_list: + - train_data/XFUND/zh_train/train.json + ratio_list: [ 1.0 ] + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - VQATokenLabelEncode: # Class handling label + contains_re: False + algorithm: *algorithm + class_path: *class_path + - VQATokenPad: + max_seq_len: &max_seq_len 512 + return_attention_mask: True + - VQASerTokenChunk: + max_seq_len: *max_seq_len + - Resize: + size: [224,224] + - NormalizeImage: + scale: 1 + mean: [ 123.675, 116.28, 103.53 ] + std: [ 58.395, 57.12, 57.375 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'input_ids', 'bbox', 'attention_mask', 'token_type_ids', 'image', 'labels'] # dataloader will return list in this order + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 16 + +Eval: + dataset: + name: SimpleDataSet + data_dir: train_data/XFUND/zh_val/image + label_file_list: + - train_data/XFUND/zh_val/val.json + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - VQATokenLabelEncode: # Class handling label + contains_re: False + algorithm: *algorithm + class_path: *class_path + - VQATokenPad: + max_seq_len: *max_seq_len + return_attention_mask: True + - VQASerTokenChunk: + max_seq_len: *max_seq_len + - Resize: + size: [224,224] + - NormalizeImage: + scale: 1 + mean: [ 123.675, 116.28, 103.53 ] + std: [ 58.395, 57.12, 57.375 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'input_ids', 'bbox', 'attention_mask', 'token_type_ids', 'image', 'labels'] # dataloader will return list in this order + loader: + shuffle: False + drop_last: False + batch_size_per_card: 8 + num_workers: 4 diff --git a/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/ser_layoutlmv2_xfund_zh.yml b/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/ser_layoutlmv2_xfund_zh.yml new file mode 100644 index 0000000000000000000000000000000000000000..c5e833524011b03110db3bd6f4bf845db8473922 --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/ser_layoutlmv2_xfund_zh.yml @@ -0,0 +1,122 @@ +Global: + use_gpu: True + epoch_num: &epoch_num 200 + log_smooth_window: 10 + print_batch_step: 10 + save_model_dir: ./output/ser_layoutlmv2_xfund_zh/ + save_epoch_step: 2000 + # evaluation is run every 10 iterations after the 0th iteration + eval_batch_step: [ 0, 19 ] + cal_metric_during_train: False + save_inference_dir: + use_visualdl: False + seed: 2022 + infer_img: ppstructure/docs/kie/input/zh_val_42.jpg + save_res_path: ./output/ser_layoutlmv2_xfund_zh/res/ + +Architecture: + model_type: kie + algorithm: &algorithm "LayoutLMv2" + Transform: + Backbone: + name: LayoutLMv2ForSer + pretrained: True + checkpoints: + num_classes: &num_classes 7 + +Loss: + name: VQASerTokenLayoutLMLoss + num_classes: *num_classes + key: "backbone_out" + +Optimizer: + name: AdamW + beta1: 0.9 + beta2: 0.999 + lr: + name: Linear + learning_rate: 0.00005 + epochs: *epoch_num + warmup_epoch: 2 + regularizer: + + name: L2 + factor: 0.00000 + +PostProcess: + name: VQASerTokenLayoutLMPostProcess + class_path: &class_path train_data/XFUND/class_list_xfun.txt + +Metric: + name: VQASerTokenMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: train_data/XFUND/zh_train/image + label_file_list: + - train_data/XFUND/zh_train/train.json + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - VQATokenLabelEncode: # Class handling label + contains_re: False + algorithm: *algorithm + class_path: *class_path + - VQATokenPad: + max_seq_len: &max_seq_len 512 + return_attention_mask: True + - VQASerTokenChunk: + max_seq_len: *max_seq_len + - Resize: + size: [224,224] + - NormalizeImage: + scale: 1 + mean: [ 123.675, 116.28, 103.53 ] + std: [ 58.395, 57.12, 57.375 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'input_ids', 'bbox', 'attention_mask', 'token_type_ids', 'image', 'labels'] # dataloader will return list in this order + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: train_data/XFUND/zh_val/image + label_file_list: + - train_data/XFUND/zh_val/val.json + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - VQATokenLabelEncode: # Class handling label + contains_re: False + algorithm: *algorithm + class_path: *class_path + - VQATokenPad: + max_seq_len: *max_seq_len + return_attention_mask: True + - VQASerTokenChunk: + max_seq_len: *max_seq_len + - Resize: + size: [224,224] + - NormalizeImage: + scale: 1 + mean: [ 123.675, 116.28, 103.53 ] + std: [ 58.395, 57.12, 57.375 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'input_ids', 'bbox', 'attention_mask', 'token_type_ids', 'image', 'labels'] # dataloader will return list in this order + loader: + shuffle: False + drop_last: False + batch_size_per_card: 8 + num_workers: 4 diff --git a/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/ser_layoutxlm_xfund_zh.yml b/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/ser_layoutxlm_xfund_zh.yml new file mode 100644 index 0000000000000000000000000000000000000000..abcfec2d16f13d4b4266633dbb509e0fba6d931f --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/kie/layoutlm_series/ser_layoutxlm_xfund_zh.yml @@ -0,0 +1,122 @@ +Global: + use_gpu: True + epoch_num: &epoch_num 200 + log_smooth_window: 10 + print_batch_step: 10 + save_model_dir: ./output/ser_layoutxlm_xfund_zh + save_epoch_step: 2000 + # evaluation is run every 10 iterations after the 0th iteration + eval_batch_step: [ 0, 19 ] + cal_metric_during_train: False + save_inference_dir: + use_visualdl: False + seed: 2022 + infer_img: ppstructure/docs/kie/input/zh_val_42.jpg + save_res_path: ./output/ser_layoutxlm_xfund_zh/res + +Architecture: + model_type: kie + algorithm: &algorithm "LayoutXLM" + Transform: + Backbone: + name: LayoutXLMForSer + pretrained: True + checkpoints: + num_classes: &num_classes 7 + +Loss: + name: VQASerTokenLayoutLMLoss + num_classes: *num_classes + key: "backbone_out" + +Optimizer: + name: AdamW + beta1: 0.9 + beta2: 0.999 + lr: + name: Linear + learning_rate: 0.00005 + epochs: *epoch_num + warmup_epoch: 2 + regularizer: + name: L2 + factor: 0.00000 + +PostProcess: + name: VQASerTokenLayoutLMPostProcess + class_path: &class_path train_data/XFUND/class_list_xfun.txt + +Metric: + name: VQASerTokenMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: train_data/XFUND/zh_train/image + label_file_list: + - train_data/XFUND/zh_train/train.json + ratio_list: [ 1.0 ] + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - VQATokenLabelEncode: # Class handling label + contains_re: False + algorithm: *algorithm + class_path: *class_path + - VQATokenPad: + max_seq_len: &max_seq_len 512 + return_attention_mask: True + - VQASerTokenChunk: + max_seq_len: *max_seq_len + - Resize: + size: [224,224] + - NormalizeImage: + scale: 1 + mean: [ 123.675, 116.28, 103.53 ] + std: [ 58.395, 57.12, 57.375 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'input_ids', 'bbox', 'attention_mask', 'token_type_ids', 'image', 'labels'] # dataloader will return list in this order + loader: + shuffle: True + drop_last: False + batch_size_per_card: 8 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: train_data/XFUND/zh_val/image + label_file_list: + - train_data/XFUND/zh_val/val.json + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - VQATokenLabelEncode: # Class handling label + contains_re: False + algorithm: *algorithm + class_path: *class_path + - VQATokenPad: + max_seq_len: *max_seq_len + return_attention_mask: True + - VQASerTokenChunk: + max_seq_len: *max_seq_len + - Resize: + size: [224,224] + - NormalizeImage: + scale: 1 + mean: [ 123.675, 116.28, 103.53 ] + std: [ 58.395, 57.12, 57.375 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'input_ids', 'bbox', 'attention_mask', 'token_type_ids', 'image', 'labels'] # dataloader will return list in this order + loader: + shuffle: False + drop_last: False + batch_size_per_card: 8 + num_workers: 4 diff --git a/ocr_webapp/PaddleOCR/configs/kie/sdmgr/kie_unet_sdmgr.yml b/ocr_webapp/PaddleOCR/configs/kie/sdmgr/kie_unet_sdmgr.yml new file mode 100644 index 0000000000000000000000000000000000000000..da2e4fda504fcbff280788b0baf6d803cf75fe4b --- /dev/null +++ b/ocr_webapp/PaddleOCR/configs/kie/sdmgr/kie_unet_sdmgr.yml @@ -0,0 +1,111 @@ +Global: + use_gpu: True + epoch_num: 60 + log_smooth_window: 20 + print_batch_step: 50 + save_model_dir: ./output/kie_5/ + save_epoch_step: 50 + # evaluation is run every 5000 iterations after the 4000th iteration + eval_batch_step: [ 0, 80 ] + # 1. If pretrained_model is saved in static mode, such as classification pretrained model + # from static branch, load_static_weights must be set as True. + # 2. If you want to finetune the pretrained models we provide in the docs, + # you should set load_static_weights as False. + load_static_weights: False + cal_metric_during_train: False + pretrained_model: + checkpoints: + save_inference_dir: + use_visualdl: False + class_path: &class_path ./train_data/wildreceipt/class_list.txt + infer_img: ./train_data/wildreceipt/1.txt + save_res_path: ./output/sdmgr_kie/predicts_kie.txt + img_scale: [ 1024, 512 ] + +Architecture: + model_type: kie + algorithm: SDMGR + Transform: + Backbone: + name: Kie_backbone + Head: + name: SDMGRHead + +Loss: + name: SDMGRLoss + +Optimizer: + name: Adam + beta1: 0.9 + beta2: 0.999 + lr: + name: Piecewise + learning_rate: 0.001 + decay_epochs: [ 60, 80, 100] + values: [ 0.001, 0.0001, 0.00001] + warmup_epoch: 2 + regularizer: + name: 'L2' + factor: 0.00005 + +PostProcess: + name: None + +Metric: + name: KIEMetric + main_indicator: hmean + +Train: + dataset: + name: SimpleDataSet + data_dir: ./train_data/wildreceipt/ + label_file_list: [ './train_data/wildreceipt/wildreceipt_train.txt' ] + ratio_list: [ 1.0 ] + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - NormalizeImage: + scale: 1 + mean: [ 123.675, 116.28, 103.53 ] + std: [ 58.395, 57.12, 57.375 ] + order: 'hwc' + - KieLabelEncode: # Class handling label + character_dict_path: ./train_data/wildreceipt/dict.txt + class_path: *class_path + - KieResize: + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'image', 'relations', 'texts', 'points', 'labels', 'tag', 'shape'] # dataloader will return list in this order + loader: + shuffle: True + drop_last: False + batch_size_per_card: 4 + num_workers: 4 + +Eval: + dataset: + name: SimpleDataSet + data_dir: ./train_data/wildreceipt + label_file_list: + - ./train_data/wildreceipt/wildreceipt_test.txt + transforms: + - DecodeImage: # load image + img_mode: RGB + channel_first: False + - KieLabelEncode: # Class handling label + character_dict_path: ./train_data/wildreceipt/dict.txt + - KieResize: + - NormalizeImage: + scale: 1 + mean: [ 123.675, 116.28, 103.53 ] + std: [ 58.395, 57.12, 57.375 ] + order: 'hwc' + - ToCHWImage: + - KeepKeys: + keep_keys: [ 'image', 'relations', 'texts', 'points', 'labels', 'tag', 'ori_image', 'ori_boxes', 'shape'] + loader: + shuffle: False + drop_last: False + batch_size_per_card: 1 # must be 1 + num_workers: 4 diff --git a/ocr_webapp/app.py b/ocr_webapp/app.py new file mode 100644 index 0000000000000000000000000000000000000000..3539aa9097ca1b360dffa8752dec8b954fa35ae7 --- /dev/null +++ b/ocr_webapp/app.py @@ -0,0 +1,1108 @@ +import os +import sys + +# Force single-threaded execution for OpenMP and MKL to prevent conflicts between OpenCV and PaddlePaddle on Windows +os.environ["OMP_NUM_THREADS"] = "1" +os.environ["MKL_NUM_THREADS"] = "1" +os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" + +import json +import base64 +import cv2 +import re +import numpy as np + +# Monkeypatch NumPy 2.x to support legacy symbols removed/deprecated for PaddleOCR and imgaug +if not hasattr(np, 'sctypes'): + np.sctypes = { + 'int': [np.int8, np.int16, np.int32, np.int64], + 'uint': [np.uint8, np.uint16, np.uint32, np.uint64], + 'float': [np.float16, np.float32, np.float64], + 'complex': [np.complex64, np.complex128], + 'others': [bool, object, bytes, str] + } +if not hasattr(np, 'bool'): + np.bool = bool +if not hasattr(np, 'int'): + np.int = int +if not hasattr(np, 'float'): + np.float = float +if not hasattr(np, 'typeDict'): + np.typeDict = {} + +from http.server import BaseHTTPRequestHandler, HTTPServer +import urllib.parse + +# Force UTF-8 stdout encoding on Windows +if sys.stdout and sys.stdout.encoding != 'utf-8': + try: + sys.stdout.reconfigure(encoding='utf-8') + except AttributeError: + pass + +PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) +paddleocr_dir = os.path.join(PROJECT_ROOT, "PaddleOCR") +if paddleocr_dir not in sys.path: + # Use append instead of insert(0) to prevent shadowing PyPI paddleocr + sys.path.append(paddleocr_dir) + +# Add ocr_training to sys.path so 'scripts.generate_data' can be imported +ocr_training_dir = os.path.join(os.path.dirname(PROJECT_ROOT), "ocr_training") +if ocr_training_dir not in sys.path: + sys.path.insert(0, ocr_training_dir) + +# Global variables for OCR models +ocr_models = {} + +class OfficialPaddleOCRWrapper: + def __init__(self, lang): + self.lang = lang + self.ocr = None + + def _init_ocr(self): + if self.ocr is None: + print("DEBUG: [OfficialPaddleOCRWrapper] Importing paddleocr...") + from paddleocr import PaddleOCR + ocr_rec_batch_size = int(os.getenv("OCR_REC_BATCH_SIZE", "16")) + + import paddleocr + version_str = getattr(paddleocr, "__version__", "2.0.0") + print(f"DEBUG: [OfficialPaddleOCRWrapper] paddleocr version={version_str}") + + if version_str.startswith("3."): + # PP-OCRv6 (paddleocr 3.x) + print("DEBUG: [OfficialPaddleOCRWrapper] Instantiating PP-OCRv6...") + self.ocr = PaddleOCR( + use_textline_orientation=False, + lang=self.lang, + text_recognition_batch_size=ocr_rec_batch_size + ) + else: + # PP-OCRv4 / PP-OCRv5 (paddleocr 2.x) + print("DEBUG: [OfficialPaddleOCRWrapper] Instantiating PP-OCRv4...") + self.ocr = PaddleOCR( + use_angle_cls=False, + show_log=False, + lang=self.lang, + rec=True, + det=False, + rec_batch_num=ocr_rec_batch_size + ) + print("DEBUG: [OfficialPaddleOCRWrapper] Instantiated successfully.") + + def __call__(self, img_list): + print(f"DEBUG: [OfficialPaddleOCRWrapper] Received {len(img_list)} images") + self._init_ocr() + if not img_list: + return [], 0.0 + + try: + import paddleocr + version_str = getattr(paddleocr, "__version__", "2.0.0") + if version_str.startswith("3."): + print("DEBUG: [OfficialPaddleOCRWrapper] Calling PP-OCRv6 text_rec_model...") + rec_res = self.ocr.paddlex_pipeline._pipeline.text_rec_model(img_list) + print("DEBUG: [OfficialPaddleOCRWrapper] PP-OCRv6 prediction finished.") + results = [] + for r in rec_res: + pred_text = r['rec_text'] + conf = r['rec_score'] + results.append((pred_text, float(conf))) + return results, 0.0 + else: + # PaddleOCR 2.x batching + print("DEBUG: [OfficialPaddleOCRWrapper] Calling PP-OCRv4 text_recognizer...") + rec_res, _ = self.ocr.text_recognizer(img_list) + print("DEBUG: [OfficialPaddleOCRWrapper] PP-OCRv4 prediction finished.") + results = [] + for res in rec_res: + pred_text, conf = res if res else ("", 0.0) + results.append((pred_text, float(conf))) + return results, 0.0 + except Exception as e: + print(f"DEBUG: [OfficialPaddleOCRWrapper] Batch failed, falling back to sequential: {e}") + results = [] + for img in img_list: + try: + res = self.ocr.ocr(img, det=False, cls=False) + if res and res[0] and res[0][0]: + text, conf = res[0][0] + results.append((text, conf)) + else: + results.append(("", 0.0)) + except Exception: + results.append(("", 0.0)) + return results, 0.0 + +class AutoRoutingOCRWrapper: + def __init__(self, cham_model, viet_model): + self.cham_model = cham_model + self.viet_model = viet_model + + def __call__(self, img_list): + if not img_list: + return [], 0.0 + + # 1. Predict all images with Cham model in a single batch + print(f"DEBUG: [AutoRouting] Calling Cham model for {len(img_list)} images...") + cham_res, _ = self.cham_model(img_list) + print("DEBUG: [AutoRouting] Cham model prediction finished.") + + # 2. Identify indices requiring Vietnamese OCR + viet_indices = [] + viet_imgs = [] + for i, (cham_text, cham_conf) in enumerate(cham_res): + if cham_conf < 0.65: + viet_indices.append(i) + viet_imgs.append(img_list[i]) + + # 3. Predict those with Vietnamese model in a single batch + viet_res = [] + if viet_imgs: + print(f"DEBUG: [AutoRouting] Calling Viet model for {len(viet_imgs)} images...") + viet_batch_res, _ = self.viet_model(viet_imgs) + viet_res = viet_batch_res + print("DEBUG: [AutoRouting] Viet model prediction finished.") + + # 4. Merge results keeping router logic + final_results = [None] * len(img_list) + viet_ptr = 0 + for i in range(len(img_list)): + cham_text, cham_conf = cham_res[i] + if cham_conf >= 0.65: + final_results[i] = (cham_text, cham_conf) + else: + viet_text, viet_conf = viet_res[viet_ptr] + viet_ptr += 1 + + if cham_conf >= 0.40 and cham_conf > viet_conf - 0.15: + final_results[i] = (cham_text, cham_conf) + else: + final_results[i] = (viet_text, viet_conf) + + return final_results, 0.0 + +def get_ocr_model(version): + """Loads and caches the PaddleOCR model for the given version dynamically""" + global ocr_models + if version in ocr_models: + return ocr_models[version] + + if version in ['vi', 'en', 'fr', 'japan']: + ocr_model = OfficialPaddleOCRWrapper(lang=version) + ocr_models[version] = ocr_model + return ocr_model + + if version == 'auto': + cham_model = get_ocr_model('v23') + viet_model = get_ocr_model('vi') + ocr_model = AutoRoutingOCRWrapper(cham_model, viet_model) + ocr_models[version] = ocr_model + return ocr_model + + from tools.infer.predict_rec import TextRecognizer + import tools.infer.utility as utility + + model_dir = os.path.join(PROJECT_ROOT, "data", "output", f"rec_cham_inference_{version}") + dict_path = os.path.join(PROJECT_ROOT, "data", f"cham_dict_{version}.txt") + + if not os.path.exists(model_dir): + # Fallback to general data/output folders if version-specific folder is missing + model_dir = os.path.join(PROJECT_ROOT, "data", "output", "rec_cham_inference") + dict_path = os.path.join(PROJECT_ROOT, "data", "cham_dict.txt") + + sys_argv_backup = sys.argv + sys.argv = [sys.argv[0]] + args = utility.parse_args() + sys.argv = sys_argv_backup + + args.rec_model_dir = model_dir + args.rec_char_dict_path = dict_path + + import paddle + args.use_gpu = paddle.is_compiled_with_cuda() + args.rec_image_shape = "3, 48, 320" + args.use_space_char = True + + # Batch size config: default to 1 on CPU to prevent padding latency blowup from varying crop aspect ratios + env_batch = os.getenv("OCR_REC_BATCH_SIZE") + if env_batch: + ocr_rec_batch_size = int(env_batch) + else: + ocr_rec_batch_size = 1 if not paddle.is_compiled_with_cuda() else 16 + + args.rec_batch_num = ocr_rec_batch_size + + if args.use_gpu: + args.enable_mkldnn = False + args.ir_optim = True + args.precision = "fp32" + else: + # CPU Settings: Disable MKLDNN by default on Windows to prevent OneDNN deadlocks inside socket handlers + env_mkldnn = os.getenv("ENABLE_MKLDNN", "False").lower() in ("true", "1") + args.enable_mkldnn = env_mkldnn + args.mkldnn_cache_capacity = 32 + + env_threads = os.getenv("CPU_THREADS") + if env_threads: + args.cpu_threads = int(env_threads) + else: + # Default to 1 thread to prevent OpenMP deadlocks on Windows socket servers + args.cpu_threads = 1 + + args.ir_optim = False # Disable on CPU to prevent OneDNN crash + args.precision = "fp32" + + print(f"⚙️ Loading PaddleOCR Model {version} (GPU: {args.use_gpu}, MKLDNN: {args.enable_mkldnn}, Batch: {args.rec_batch_num}, Threads: {args.cpu_threads})...") + ocr_model = TextRecognizer(args) + + # Warmup the model with a dummy image to trigger compilation/allocator caching + try: + dummy_img = np.zeros((48, 320, 3), dtype=np.uint8) + ocr_model([dummy_img]) + print(f"🔥 Model {version} warmed up successfully.") + except Exception as e: + print(f"Failed to warmup model: {e}") + + ocr_models[version] = ocr_model + return ocr_model + +# ============================================================================== +# Line Segmentation Algorithms +# ============================================================================== + +def segment_lines_adaptive(img, threshold_pct=0.08, gap_threshold=12): + """Adaptive Projection Profile line segmentation""" + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + _, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY_INV) + hist = np.sum(thresh, axis=1) + + max_val = np.max(hist) + threshold = max_val * threshold_pct + active_rows = np.where(hist >= threshold)[0] + + if len(active_rows) == 0: + return [], [] + + line_ranges = [] + start = active_rows[0] + for i in range(1, len(active_rows)): + if active_rows[i] - active_rows[i-1] > gap_threshold: + line_ranges.append((start, active_rows[i-1])) + start = active_rows[i] + line_ranges.append((start, active_rows[-1])) + + # Return ranges and crop coordinates + cropped_lines = [] + coords = [] + pad = 8 + h, w = gray.shape + for s, e in line_ranges: + s_pad = max(0, s - pad) + e_pad = min(h, e + pad) + cropped_lines.append(img[s_pad:e_pad, :]) + coords.append((s_pad, e_pad)) + return cropped_lines, coords + +def segment_lines_valleys(img, window_size=25, min_dist=35): + """Valley Detection (local minima) line segmentation""" + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + _, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY_INV) + hist = np.sum(thresh, axis=1) + + smoothed = np.convolve(hist, np.ones(window_size)/window_size, mode='same') + + valleys = [] + radius = max(3, int(window_size / 2)) + for i in range(20, len(smoothed) - 20): + if smoothed[i] == np.min(smoothed[i-radius:i+radius+1]): + if not valleys or i - valleys[-1] > min_dist: + valleys.append(i) + + split_points = [0] + valleys + [len(smoothed)] + cropped_lines = [] + coords = [] + for idx in range(len(split_points) - 1): + s = split_points[idx] + e = split_points[idx + 1] + if e - s < 15: + continue + cropped_lines.append(img[s:e, :]) + coords.append((s, e)) + return cropped_lines, coords + +def calculate_crop_sanity_score(conf, pred_text, median_len, crop_img, ctype): + """Calculates sanity score for a crop prediction""" + if not pred_text or crop_img is None or crop_img.size == 0: + return -1.0 + + score = conf + # Penalize if prediction length is too far from median + if median_len > 0: + ratio = len(pred_text) / median_len + if ratio > 1.4 or ratio < 0.6: + score *= 0.8 + + # Penalize if there is significant ink touching the top or bottom edges + # We check the top 3 and bottom 3 rows + gray = cv2.cvtColor(crop_img, cv2.COLOR_BGR2GRAY) + _, thresh = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV) + h, w = thresh.shape + if h > 6: + # Removed edge ink penalty because it falsely penalizes tight crops that perfectly bound the diacritics. + pass + + # Boost safe slightly because it inherently suffers from resize-down confidence drops + if ctype == "safe": + score *= 1.15 + elif ctype in ["upper_rescue", "lower_rescue"]: + score *= 1.10 + elif ctype == "loose": + score *= 1.05 + + return score + +def compute_cer(pred, gt): + if len(pred) == 0 and len(gt) == 0: + return 0.0 + if len(gt) == 0: + return 1.0 + import difflib + similarity = difflib.SequenceMatcher(None, pred, gt).ratio() + return 1.0 - similarity + +def has_important_cham_sign_changes(pred1, pred2): + if pred1 == pred2: + return False + import difflib + diff = difflib.ndiff(pred1, pred2) + for d in diff: + if d.startswith('- ') or d.startswith('+ '): + char = d[2] + # Cham pre-signs / final signs range: U+A800 to U+A82F, U+A840 to U+A87F + if '\uA800' <= char <= '\uA82F' or '\uA840' <= char <= '\uA87F': + return True + return False + +def segment_lines_advanced(img, base_coords, ocr_model): + """ + Advanced Multi-Crop Strategy with Risk-Gated Batched Inference + """ + import time + import cv2 + import numpy as np + + t_start = time.time() + if not base_coords: + return [], [] + + img_h, img_w, _ = img.shape + heights = [e - s for s, e in base_coords] + median_line_height = np.median(heights) + + # [Pre-compute legacy bounds, CCA, etc., identical to original until Phase 1] + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + _, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY_INV) + num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(thresh, connectivity=8) + + line_components = [set() for _ in base_coords] + ambiguous_components = set() + cca_bounds = [] + + for i in range(1, num_labels): + x, y, w, h, area = stats[i] + if area < 5 or h < 3 or w < 3: + continue + cy = y + h / 2 + assigned_lines = [] + for l_idx, (s, e) in enumerate(base_coords): + if s - 5 <= cy <= e + 5: + assigned_lines.append(l_idx) + + if len(assigned_lines) == 1: + line_components[assigned_lines[0]].add(i) + elif len(assigned_lines) > 1: + ambiguous_components.add(i) + for l_idx in assigned_lines: + line_components[l_idx].add(i) + + true_core_coords = [] + for idx, (s, e) in enumerate(base_coords): + comps = line_components[idx] + if comps: + core_s = min(stats[i][1] for i in comps) + core_e = max(stats[i][1] + stats[i][3] for i in comps) + true_core_coords.append((core_s, core_e)) + else: + true_core_coords.append((s, e)) + + page_safe = True + for idx in range(len(base_coords) - 1): + if true_core_coords[idx+1][0] - true_core_coords[idx][1] < 0.15 * median_line_height: + page_safe = False + break + + # Phase 1: Candidate Generation (Pass 1) + t_candidate = time.time() + pass1_candidates = [] + lines_meta = [] + + for idx, (s, e) in enumerate(true_core_coords): + comps = line_components[idx] + if comps: + xmin = min(stats[i][0] for i in comps) + xmax = max(stats[i][0] + stats[i][2] for i in comps) + ymin = min(stats[i][1] for i in comps) + ymax = max(stats[i][1] + stats[i][3] for i in comps) + else: + xmin, xmax = 0, img_w + ymin, ymax = int(s), int(e) + + line_meta = { + "line_id": idx, + "core_box": [int(s), int(e)], + "bbox": [int(xmin), int(ymin), int(xmax), int(ymax)], + "needs_review": False, + "reason": [], + "candidates": {}, + "overlap_prev_px": 0, + "overlap_next_px": 0 + } + + core_top = true_core_coords[idx][0] + core_bot = true_core_coords[idx][1] + prev_bot = true_core_coords[idx-1][1] if idx > 0 else 0 + next_top = true_core_coords[idx+1][0] if idx < len(true_core_coords)-1 else img_h + + gap_prev = core_top - prev_bot if idx > 0 else 999999 + gap_next = next_top - core_bot if idx < len(true_core_coords)-1 else 999999 + + median_ink_height = median_line_height + is_gap_safe = min(gap_prev, gap_next) >= 0.35 * median_ink_height + + valley_s, valley_e = base_coords[idx] + has_edge_ink_risk = False + if int(valley_s) > 0 and core_top <= int(valley_s) + 2: + has_edge_ink_risk = True + if int(valley_e) < img_h and core_bot >= int(valley_e) - 2: + has_edge_ink_risk = True + + close_risk = False + if min(gap_prev, gap_next) < 0.25 * median_line_height: + close_risk = True + line_meta["reason"].append("very_close_lines_or_touching_diacritics") + + pad_tight_top = int(0.05 * median_line_height) + pad_tight_bot = int(0.05 * median_line_height) + + safety_margin_px = max(2, int(0.05 * median_line_height)) + + if close_risk or not page_safe: + safe_top = ymin - safety_margin_px + safe_bot = ymax + safety_margin_px + else: + safe_top = valley_s + safe_bot = valley_e + + min_top_pad = int(0.40 * median_line_height) + min_bot_pad = int(0.40 * median_line_height) + + safe_top = min(safe_top, core_top - min_top_pad) + safe_bot = max(safe_bot, core_bot + min_bot_pad) + + if idx > 0: + safe_top = max(safe_top, true_core_coords[idx-1][1] + 2) + if idx < len(true_core_coords) - 1: + safe_bot = min(safe_bot, true_core_coords[idx+1][0] - 2) + + pad_safe_top = int(core_top - safe_top) + pad_safe_bot = int(safe_bot - core_bot) + + def clamp_crop(pad_t, pad_b): + c_s = max(0, core_top - pad_t) + c_e = min(img_h, core_bot + pad_b) + ov_prev = max(0, prev_bot - c_s) if idx > 0 else 0 + ov_next = max(0, c_e - next_top) if idx < len(true_core_coords)-1 else 0 + return (int(c_s), int(c_e), int(ov_prev), int(ov_next)) + + candidates_to_run = ["safe"] + + for ctype in candidates_to_run: + if ctype == "safe": + c_s, c_e, o_p, o_n = clamp_crop(pad_safe_top, pad_safe_bot) + + c_img = img[c_s:c_e, :].copy() + if ctype == "safe" and (close_risk or not page_safe): + components_to_erase = set() + for other_idx in range(len(true_core_coords)): + if other_idx != idx: + components_to_erase.update(line_components[other_idx]) + components_to_erase.difference_update(line_components[idx]) + + # Fast LUT lookup instead of np.isin (prevents CPU freeze on large label sets) + erase_lut = np.zeros(num_labels, dtype=bool) + if components_to_erase: + erase_lut[list(components_to_erase)] = True + + label_roi = labels[c_s:c_e, :] + erase_mask = erase_lut[label_roi] + c_img[erase_mask] = [255, 255, 255] + + cand_obj = { + "line_id": idx, + "crop_type": ctype, + "crop_img": c_img, + "crop_box": [c_s, c_e], + "overlap_prev_px": o_p, + "overlap_next_px": o_n, + "prediction": "", + "confidence": 0.0, + "pred_length": 0 + } + pass1_candidates.append(cand_obj) + line_meta["candidates"][ctype] = cand_obj + + # Store metadata and flags for later + line_meta["close_risk"] = close_risk + line_meta["page_safe"] = page_safe + line_meta["is_gap_safe"] = is_gap_safe + line_meta["has_edge_ink_risk"] = has_edge_ink_risk + line_meta["has_ambiguous_cc"] = any(c in ambiguous_components for c in line_components[idx]) + lines_meta.append(line_meta) + + candidate_generation_sec = time.time() - t_candidate + + # Phase 3: Pass 1 Batch Recognition + t_pass1 = time.time() + if pass1_candidates: + imgs = [c["crop_img"] for c in pass1_candidates] + # ocr_model accepts a list of images and returns list of (text, conf) + # However, PaddleOCR's predict() might fail if empty list or invalid dims. + valid_imgs = [] + valid_indices = [] + for i, m in enumerate(imgs): + if m.size > 0 and m.shape[0] >= 8 and m.shape[1] >= 8: + # Prevent aspect ratio blowup that causes CPU hang during inference resizing + scaled_w = int(m.shape[1] * 48 / m.shape[0]) + if scaled_w > 2000: + # Downscale horizontally to protect the sequence processing layer + m = cv2.resize(m, (2000, m.shape[0])) + valid_imgs.append(m) + valid_indices.append(i) + + if valid_imgs: + try: + print(f"DEBUG: [segment_lines_advanced] Calling ocr_model for Pass 1 ({len(valid_imgs)} images)...") + results, _ = ocr_model(valid_imgs) + print("DEBUG: [segment_lines_advanced] Pass 1 prediction finished.") + for vi, res in zip(valid_indices, results): + pred_text, conf = res if res else ("", 0.0) + pred_text = pred_text.strip() + pass1_candidates[vi]["prediction"] = pred_text + pass1_candidates[vi]["confidence"] = float(conf) + pass1_candidates[vi]["pred_length"] = len(pred_text) + except Exception as e: + print(f"DEBUG: [segment_lines_advanced] Batch inference pass 1 failed: {e}") + + recognition_batch_pass1_sec = time.time() - t_pass1 + + # Phase 6: Two-Pass Fallback + t_pass2_gen = time.time() + pass2_candidates = [] + + for idx, line_meta in enumerate(lines_meta): + # Determine if we need Pass 2 + cands = line_meta["candidates"] + best_conf = max([c["confidence"] for c in cands.values()]) if cands else 0.0 + + is_clean_line = line_meta["is_gap_safe"] and not line_meta["has_edge_ink_risk"] + + needs_pass2 = False + if is_clean_line: + # Clean line: only run Pass 2 if confidence is below acceptable threshold (0.75) + if best_conf < 0.75: + needs_pass2 = True + else: + # Risky line: run Pass 2 if confidence is below high safety threshold (0.90) + if best_conf < 0.90: + needs_pass2 = True + + if needs_pass2: + core_top = true_core_coords[idx][0] + core_bot = true_core_coords[idx][1] + prev_bot = true_core_coords[idx-1][1] if idx > 0 else 0 + next_top = true_core_coords[idx+1][0] if idx < len(true_core_coords)-1 else img_h + + pad_safe_top = int(core_top - cands["safe"]["crop_box"][0]) if "safe" in cands else 0 + pad_safe_bot = int(cands["safe"]["crop_box"][1] - core_bot) if "safe" in cands else 0 + + pad_loose_top = int(0.80 * median_line_height) + pad_loose_bot = int(0.80 * median_line_height) + + pad_tight_top = int(0.05 * median_line_height) + pad_tight_bot = int(0.05 * median_line_height) + + pad_upper_rescue_top = pad_loose_top + pad_upper_rescue_bot = pad_safe_bot + + pad_lower_rescue_top = pad_safe_top + pad_lower_rescue_bot = pad_loose_bot + + def clamp_crop2(pad_t, pad_b): + c_s = max(0, core_top - pad_t) + c_e = min(img_h, core_bot + pad_b) + ov_prev = max(0, prev_bot - c_s) if idx > 0 else 0 + ov_next = max(0, c_e - next_top) if idx < len(true_core_coords)-1 else 0 + return (int(c_s), int(c_e), int(ov_prev), int(ov_next)) + + # We need to generate tight crop now since it wasn't generated in Pass 1 + tight_s, tight_e, tight_op, tight_on = clamp_crop2(pad_tight_top, pad_tight_bot) + tight_img = img[tight_s:tight_e, :].copy() + tight_cand = { + "line_id": idx, + "crop_type": "tight", + "crop_img": tight_img, + "crop_box": [tight_s, tight_e], + "overlap_prev_px": tight_op, + "overlap_next_px": tight_on, + "prediction": "", + "confidence": 0.0, + "pred_length": 0 + } + pass2_candidates.append(tight_cand) + line_meta["candidates"]["tight"] = tight_cand + + for ctype, pt, pb in [ + ("upper_rescue", pad_upper_rescue_top, pad_upper_rescue_bot), + ("lower_rescue", pad_lower_rescue_top, pad_lower_rescue_bot), + ("loose", pad_loose_top, pad_loose_bot) + ]: + c_s, c_e, o_p, o_n = clamp_crop2(pt, pb) + c_img = img[c_s:c_e, :].copy() + cand_obj = { + "line_id": idx, + "crop_type": ctype, + "crop_img": c_img, + "crop_box": [c_s, c_e], + "overlap_prev_px": o_p, + "overlap_next_px": o_n, + "prediction": "", + "confidence": 0.0, + "pred_length": 0 + } + pass2_candidates.append(cand_obj) + line_meta["candidates"][ctype] = cand_obj + + # Run Batch Pass 2 with coordinate-level deduplication + t_pass2_rec = time.time() + if pass2_candidates: + # Cache for coordinate-level deduplication: (s, e) -> (prediction, confidence) + seen_boxes = {} + + # Pre-populate seen_boxes with Pass 1 (safe) results to avoid re-running identical boxes + for line_meta in lines_meta: + if "safe" in line_meta["candidates"]: + safe_c = line_meta["candidates"]["safe"] + seen_boxes[tuple(safe_c["crop_box"])] = (safe_c["prediction"], safe_c["confidence"]) + + exec_candidates = [] # candidates that require actual OCR execution + deferred_copies = [] # candidates that will copy results from an identical box + + for cand in pass2_candidates: + c_box = tuple(cand["crop_box"]) + if c_box in seen_boxes: + res = seen_boxes[c_box] + if res[0] != "" or res[1] > 0.0: + cand["prediction"] = res[0] + cand["confidence"] = res[1] + cand["pred_length"] = len(res[0]) + else: + deferred_copies.append((cand, c_box)) + else: + exec_candidates.append(cand) + seen_boxes[c_box] = ("", 0.0) # mark as pending + + if exec_candidates: + imgs = [c["crop_img"] for c in exec_candidates] + valid_imgs = [] + valid_indices = [] + for i, m in enumerate(imgs): + if m.size > 0 and m.shape[0] >= 8 and m.shape[1] >= 8: + # Prevent aspect ratio blowup that causes CPU hang during inference resizing + scaled_w = int(m.shape[1] * 48 / m.shape[0]) + if scaled_w > 2000: + # Downscale horizontally to protect the sequence processing layer + m = cv2.resize(m, (2000, m.shape[0])) + valid_imgs.append(m) + valid_indices.append(i) + + if valid_imgs: + try: + print(f"DEBUG: [segment_lines_advanced] Calling ocr_model for Pass 2 ({len(valid_imgs)} images, deduplicated from {len(pass2_candidates)})...") + results, _ = ocr_model(valid_imgs) + print("DEBUG: [segment_lines_advanced] Pass 2 prediction finished.") + for vi, res in zip(valid_indices, results): + pred_text, conf = res if res else ("", 0.0) + pred_text = pred_text.strip() + + exec_cand = exec_candidates[vi] + exec_cand["prediction"] = pred_text + exec_cand["confidence"] = float(conf) + exec_cand["pred_length"] = len(pred_text) + + seen_boxes[tuple(exec_cand["crop_box"])] = (pred_text, float(conf)) + except Exception as e: + print(f"DEBUG: [segment_lines_advanced] Batch inference pass 2 failed: {e}") + + # Process deferred copies + for cand, c_box in deferred_copies: + pred_text, conf = seen_boxes[c_box] + cand["prediction"] = pred_text + cand["confidence"] = conf + cand["pred_length"] = len(pred_text) + + recognition_batch_pass2_sec = time.time() - t_pass2_rec + + # Selection and Finalization + t_select = time.time() + final_crops = [] + + for idx, line_meta in enumerate(lines_meta): + cand_data = line_meta["candidates"] + + # Determine best candidate + eval_order = ["safe", "tight"] + for ctype in ["upper_rescue", "lower_rescue", "loose"]: + if ctype in cand_data: + eval_order.append(ctype) + + selected_ctype = None + for ctype in ["safe", "tight"]: + if ctype in cand_data and cand_data[ctype]["confidence"] >= 0.90 and cand_data[ctype]["pred_length"] > 0: + selected_ctype = ctype + break + + if selected_ctype is not None: + best_score = cand_data[selected_ctype]["confidence"] + cand_data[selected_ctype]["sanity_score"] = best_score + else: + run_lengths = [c["pred_length"] for c in cand_data.values() if c["pred_length"] > 0] + median_len = np.median(run_lengths) if run_lengths else 0 + + best_score = -1.0 + selected_ctype = "safe" if "safe" in cand_data else "tight" + + for ctype in eval_order: + if ctype not in cand_data: + continue + data = cand_data[ctype] + score = calculate_crop_sanity_score(data["confidence"], data["prediction"], median_len, data["crop_img"], ctype) + + if ctype == "loose" and "safe" in cand_data: + safe_data = cand_data["safe"] + if safe_data["pred_length"] > 0: + ratio = data["pred_length"] / safe_data["pred_length"] + if ratio > 1.35: + score *= 0.3 + if safe_data["confidence"] - data["confidence"] > 0.10: + score *= 0.4 + tokens = data["prediction"].split() + single_chars = sum(1 for t in tokens if len(t) == 1) + if len(tokens) > 0 and single_chars / len(tokens) > 0.3: + score *= 0.5 + if len(safe_data["prediction"]) > 0: + import difflib + similarity = difflib.SequenceMatcher(None, data["prediction"], safe_data["prediction"]).ratio() + if similarity < 0.5: + score *= 0.6 + + data["sanity_score"] = score + if score > best_score: + best_score = score + selected_ctype = ctype + + if line_meta["page_safe"] and selected_ctype not in ["safe", "tight"]: + safe_score = cand_data.get("safe", {}).get("sanity_score", 0) + if safe_score > best_score * 0.9: + selected_ctype = "safe" + best_score = safe_score + + line_meta["selected_crop_type"] = selected_ctype + line_meta["crop_sanity_score"] = best_score + + sel_data = cand_data[selected_ctype] + + # Flags + needs_review_reasons = [] + if line_meta["has_ambiguous_cc"]: + needs_review_reasons.append("ambiguous_component_assignment") + + if selected_ctype not in ["safe", "tight"]: + if sel_data["overlap_prev_px"] > 5 or sel_data["overlap_next_px"] > 5: + needs_review_reasons.append("adjacent_line_contamination") + + if len(cand_data) > 1: + preds = [c["prediction"] for c in cand_data.values() if c["prediction"]] + if len(set(preds)) == len(cand_data) and len(preds) > 1: + needs_review_reasons.append("multi_crop_disagreement") + + line_meta["needs_review"] = len(needs_review_reasons) > 0 + line_meta["reason"] = needs_review_reasons + + if sel_data["confidence"] > 0.8: + line_meta["crop_confidence"] = "high" + elif sel_data["confidence"] > 0.5: + line_meta["crop_confidence"] = "medium" + else: + line_meta["crop_confidence"] = "low" + + line_meta["overlap_prev_px"] = sel_data["overlap_prev_px"] + line_meta["overlap_next_px"] = sel_data["overlap_next_px"] + + # Compatibility with downstream (app expects ["img"] in candidate) + for c in cand_data.values(): + c["img"] = c["crop_img"] + + final_crops.append(sel_data["crop_img"]) + + selection_sec = time.time() - t_select + total_sec = time.time() - t_start + + # Phase 7: Profiler + print(f""" + ⏱️ Profiler [segment_lines_advanced]: + - candidate_generation_sec: {candidate_generation_sec:.4f}s + - recognition_batch_pass1_sec: {recognition_batch_pass1_sec:.4f}s + - recognition_batch_pass2_sec: {recognition_batch_pass2_sec:.4f}s + - selection_sec: {selection_sec:.4f}s + - total_sec: {total_sec:.4f}s + - num_lines: {len(true_core_coords)} + - num_candidates_pass1: {len(pass1_candidates)} + - num_candidates_pass2: {len(pass2_candidates)} + - ocr_model_calls: { (1 if pass1_candidates else 0) + (1 if pass2_candidates else 0) } + """) + + profiler_dict = { + "candidate_generation_sec": round(candidate_generation_sec, 4), + "recognition_batch_pass1_sec": round(recognition_batch_pass1_sec, 4), + "recognition_batch_pass2_sec": round(recognition_batch_pass2_sec, 4), + "selection_sec": round(selection_sec, 4), + "total_segmentation_sec": round(total_sec, 4), + "num_lines": len(true_core_coords), + "num_candidates_pass1": len(pass1_candidates), + "num_candidates_pass2": len(pass2_candidates), + "ocr_model_calls": (1 if pass1_candidates else 0) + (1 if pass2_candidates else 0) + } + + return final_crops, lines_meta, profiler_dict + + + +# ============================================================================== +# Web Server Request Handler +# ============================================================================== + +class ChamOCRRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path == '/': + self.send_response(200) + self.send_header('Content-type', 'text/html; charset=utf-8') + self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + self.send_header('Pragma', 'no-cache') + self.send_header('Expires', '0') + self.end_headers() + + dir_path = os.path.dirname(os.path.abspath(__file__)) + html_path = os.path.join(dir_path, 'index.html') + if os.path.exists(html_path): + with open(html_path, 'r', encoding='utf-8') as f: + html_content = f.read() + else: + html_content = "

Error: index.html not found!

" + + self.wfile.write(html_content.encode('utf-8')) + else: + self.send_error(404, 'File Not Found') + + def do_POST(self): + import time + t_start = time.time() + print(f"📥 Received POST request: {self.path}...") + if self.path == '/log': + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + try: + log_data = json.loads(post_data.decode('utf-8')) + print(f"\n🚨 [FRONTEND LOG] {log_data.get('message')}\n") + except Exception as e: + print(f"Failed to parse log: {e}") + + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({"status": "success"}).encode('utf-8')) + return + + if self.path == '/ocr': + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + print(f"⏱️ Read request body in: {time.time() - t_start:.4f}s") + t_parse = time.time() + data = json.loads(post_data.decode('utf-8')) + + # Extract parameters + img_b64 = data['image'] + model_ver = data.get('model', 'v21') + method = data.get('method', 'valley') + threshold = float(data.get('threshold', 0.05)) + gap = int(data.get('gap', 12)) + win = int(data.get('window', 25)) + + # Decode image + img_data = base64.b64decode(img_b64) + nparr = np.frombuffer(img_data, np.uint8) + img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + print(f"⏱️ Parsed parameters and decoded image in: {time.time() - t_parse:.4f}s") + + if img is None: + self.send_json_response({'error': 'Không thể đọc ảnh'}, 400) + return + + # Perform segmentation + t_model = time.time() + try: + ocr = get_ocr_model(model_ver) + except Exception as e: + self.send_json_response({'error': f'Lỗi tải mô hình: {e}'}, 500) + return + print(f"⏱️ Loaded model in: {time.time() - t_model:.4f}s") + + t_seg = time.time() + if method == 'valley': + _, coords = segment_lines_valleys(img, window_size=win, min_dist=gap) + else: + _, coords = segment_lines_adaptive(img, threshold_pct=threshold, gap_threshold=gap) + base_segmentation_sec = time.time() - t_seg + print(f"⏱️ Base line segmentation took: {base_segmentation_sec:.4f}s") + + t_adv = time.time() + # Apply Advanced Segmentation and Multi-Crop + final_crops, lines_metadata, profiler_dict = segment_lines_advanced(img, coords, ocr) + print(f"⏱️ segment_lines_advanced took: {time.time() - t_adv:.4f}s") + + t_resp = time.time() + # Prepare backward-compatible response + results = [] + try: + for idx, (line_img, meta) in enumerate(zip(final_crops, lines_metadata)): + pred_text = meta['candidates'][meta['selected_crop_type']]['prediction'] + confidence = meta['candidates'][meta['selected_crop_type']]['confidence'] + + if model_ver in ['v24', 'v26']: + from scripts.generate_data import normalize_unicode + pred_text = normalize_unicode(pred_text) + + # Encode cropped line to base64 for frontend display + _, buffer = cv2.imencode('.png', line_img) + line_b64 = base64.b64encode(buffer).decode('utf-8') + + # Backward-compatible format + results.append({ + 'index': meta['line_id'] + 1, + 'text': pred_text, + 'confidence': float(confidence), + 'coords': meta['core_box'], + 'bbox': meta.get('bbox', [0, meta['core_box'][0], img.shape[1], meta['core_box'][1]]), + 'image': line_b64, + 'segmentation': { + 'overlap_prev_px': meta['overlap_prev_px'], + 'overlap_next_px': meta['overlap_next_px'], + 'crop_confidence': meta['crop_confidence'], + 'selected_crop_type': meta['selected_crop_type'], + 'crop_sanity_score': meta['crop_sanity_score'], + 'needs_review': meta['needs_review'], + 'reason': meta['reason'] + } + }) + except Exception as e: + self.send_json_response({'error': f'Lỗi hệ thống OCR: {e}'}, 500) + return + print(f"⏱️ Formatted response in: {time.time() - t_resp:.4f}s") + + t_send = time.time() + profiler_dict['backend_total_sec'] = round(time.time() - t_start, 4) + profiler_dict['base_segmentation_sec'] = round(base_segmentation_sec, 4) + self.send_json_response({'lines': results, 'profiler': profiler_dict}) + print(f"⏱️ Sent response in: {time.time() - t_send:.4f}s") + print(f"🎉 Total POST /ocr execution time: {profiler_dict['backend_total_sec']:.4f}s") + + elif self.path == '/save_correction': + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + data = json.loads(post_data.decode('utf-8')) + + text = data.get('text', '') + dest_file = os.path.join(PROJECT_ROOT, "data", "ocr_corrections.txt") + os.makedirs(os.path.dirname(dest_file), exist_ok=True) + + with open(dest_file, 'a', encoding='utf-8', newline='\n') as f: + f.write(text.strip() + '\n') + + self.send_json_response({'success': True, 'path': dest_file}) + else: + self.send_error(404, 'Endpoint Not Found') + + def send_json_response(self, data, status=200): + self.send_response(status) + self.send_header('Content-type', 'application/json; charset=utf-8') + # Allow Cross-Origin for flexibility + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + self.wfile.write(json.dumps(data).encode('utf-8')) + +# ============================================================================== +# HTML Dashboard Frontend Template +# ============================================================================== + +HTML_TEMPLATE = "" +def run_server(): + env_port = int(os.getenv("PORT", "7860")) + preferred_ports = [env_port, 7860, 8080, 8081, 8082, 8501, 8888, 0] + # Remove duplicate ports while preserving order + seen = set() + preferred_ports = [p for p in preferred_ports if not (p in seen or seen.add(p))] + + httpd = None + actual_port = None + for port in preferred_ports: + try: + server_address = ('0.0.0.0', port) + httpd = HTTPServer(server_address, ChamOCRRequestHandler) + actual_port = httpd.server_address[1] + break + except OSError: + print(f"⚠️ Port {port} is occupied, trying next...") + continue + + if httpd is None: + print("❌ Could not bind to any port.") + return + + print(f"======================================================================") + print(f"🚀 Cham OCR Diagnostic Studio is running at: http://localhost:{actual_port}") + print(f"📁 Corrections will be saved to: ocr_webapp/data/ocr_corrections.txt") + print(f"======================================================================") + + try: + get_ocr_model('v23') + except Exception as e: + print(f"⚠️ Could not pre-load model v23: {e}. It will load when requested.") + + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nStopping server...") + httpd.server_close() + +if __name__ == '__main__': + run_server() diff --git a/ocr_webapp/data/cham_dict.txt b/ocr_webapp/data/cham_dict.txt new file mode 100644 index 0000000000000000000000000000000000000000..17389c768778178e0d964d31f3ac5c32d67b130f --- /dev/null +++ b/ocr_webapp/data/cham_dict.txt @@ -0,0 +1,69 @@ +ꨀ +ꨁ +ꨄ +ꨆ +ꨇ +ꨈ +ꨌ +ꨎ +ꨏ +ꨒ +ꨓ +ꨔ +ꨕ +ꨖ +ꨗ +ꨘ +ꨙ +ꨚ +ꨛ +ꨜ +ꨝ +ꨟ +ꨠ +ꨡ +ꨢ +ꨣ +ꨤ +ꨥ +ꨧ +ꨨ +ꨩ +ꨪ +ꨫ +ꨬ +ꨭ +ꨮ +ꨯ +ꨰ +ꨱ +ꨳ +ꨴ +ꨵ +ꨶ +ꩀ +ꩂ +ꩃ +ꩄ +ꩅ +ꩆ +ꩇ +ꩈ +ꩉ +ꩊ +ꩌ +ꩍ +꩐ +꩑ +꩒ +꩓ +꩔ +꩕ +꩖ +꩗ +꩘ +꩙ +꩜ +꩝ +꩞ +꩟ diff --git a/ocr_webapp/data/cham_dict_v18.txt b/ocr_webapp/data/cham_dict_v18.txt new file mode 100644 index 0000000000000000000000000000000000000000..190fc923f130c75ce3ab02b5e7bbf3a6b5369412 --- /dev/null +++ b/ocr_webapp/data/cham_dict_v18.txt @@ -0,0 +1,69 @@ +: +ꨀ +ꨁ +ꨂ +ꨃ +ꨄ +ꨅ +ꨆ +ꨇ +ꨈ +ꨉ +ꨊ +ꨋ +ꨌ +ꨍ +ꨎ +ꨏ +ꨐ +ꨑ +ꨒ +ꨓ +ꨔ +ꨕ +ꨖ +ꨗ +ꨘ +ꨙ +ꨚ +ꨛ +ꨜ +ꨝ +ꨞ +ꨟ +ꨠ +ꨡ +ꨢ +ꨣ +ꨤ +ꨥ +ꨦ +ꨧ +ꨨ +ꨩ +ꨪ +ꨫ +ꨬ +ꨭ +ꨮ +ꨯ +ꨰ +ꨱ +ꨲ +ꨳ +ꨴ +ꨵ +ꨶ +ꩀ +ꩂ +ꩃ +ꩄ +ꩅ +ꩆ +ꩇ +ꩈ +ꩉ +ꩊ +ꩋ +ꩌ +ꩍ diff --git a/ocr_webapp/data/cham_dict_v19.txt b/ocr_webapp/data/cham_dict_v19.txt new file mode 100644 index 0000000000000000000000000000000000000000..633b6709d4febc5653bc8b5544872d327ffa0740 --- /dev/null +++ b/ocr_webapp/data/cham_dict_v19.txt @@ -0,0 +1,83 @@ +: +ꨀ +ꨁ +ꨂ +ꨃ +ꨄ +ꨅ +ꨆ +ꨇ +ꨈ +ꨉ +ꨊ +ꨋ +ꨌ +ꨍ +ꨎ +ꨏ +ꨐ +ꨑ +ꨒ +ꨓ +ꨔ +ꨕ +ꨖ +ꨗ +ꨘ +ꨙ +ꨚ +ꨛ +ꨜ +ꨝ +ꨞ +ꨟ +ꨠ +ꨡ +ꨢ +ꨣ +ꨤ +ꨥ +ꨦ +ꨧ +ꨨ +ꨩ +ꨪ +ꨫ +ꨬ +ꨭ +ꨮ +ꨯ +ꨰ +ꨱ +ꨲ +ꨳ +ꨴ +ꨵ +ꨶ +ꩀ +ꩂ +ꩃ +ꩄ +ꩅ +ꩆ +ꩇ +ꩈ +ꩉ +ꩊ +ꩋ +ꩌ +ꩍ +꩐ +꩑ +꩒ +꩓ +꩔ +꩕ +꩖ +꩗ +꩘ +꩙ +꩜ +꩝ +꩞ +꩟ diff --git a/ocr_webapp/data/cham_dict_v21.txt b/ocr_webapp/data/cham_dict_v21.txt new file mode 100644 index 0000000000000000000000000000000000000000..633b6709d4febc5653bc8b5544872d327ffa0740 --- /dev/null +++ b/ocr_webapp/data/cham_dict_v21.txt @@ -0,0 +1,83 @@ +: +ꨀ +ꨁ +ꨂ +ꨃ +ꨄ +ꨅ +ꨆ +ꨇ +ꨈ +ꨉ +ꨊ +ꨋ +ꨌ +ꨍ +ꨎ +ꨏ +ꨐ +ꨑ +ꨒ +ꨓ +ꨔ +ꨕ +ꨖ +ꨗ +ꨘ +ꨙ +ꨚ +ꨛ +ꨜ +ꨝ +ꨞ +ꨟ +ꨠ +ꨡ +ꨢ +ꨣ +ꨤ +ꨥ +ꨦ +ꨧ +ꨨ +ꨩ +ꨪ +ꨫ +ꨬ +ꨭ +ꨮ +ꨯ +ꨰ +ꨱ +ꨲ +ꨳ +ꨴ +ꨵ +ꨶ +ꩀ +ꩂ +ꩃ +ꩄ +ꩅ +ꩆ +ꩇ +ꩈ +ꩉ +ꩊ +ꩋ +ꩌ +ꩍ +꩐ +꩑ +꩒ +꩓ +꩔ +꩕ +꩖ +꩗ +꩘ +꩙ +꩜ +꩝ +꩞ +꩟ diff --git a/ocr_webapp/data/cham_dict_v22.txt b/ocr_webapp/data/cham_dict_v22.txt new file mode 100644 index 0000000000000000000000000000000000000000..633b6709d4febc5653bc8b5544872d327ffa0740 --- /dev/null +++ b/ocr_webapp/data/cham_dict_v22.txt @@ -0,0 +1,83 @@ +: +ꨀ +ꨁ +ꨂ +ꨃ +ꨄ +ꨅ +ꨆ +ꨇ +ꨈ +ꨉ +ꨊ +ꨋ +ꨌ +ꨍ +ꨎ +ꨏ +ꨐ +ꨑ +ꨒ +ꨓ +ꨔ +ꨕ +ꨖ +ꨗ +ꨘ +ꨙ +ꨚ +ꨛ +ꨜ +ꨝ +ꨞ +ꨟ +ꨠ +ꨡ +ꨢ +ꨣ +ꨤ +ꨥ +ꨦ +ꨧ +ꨨ +ꨩ +ꨪ +ꨫ +ꨬ +ꨭ +ꨮ +ꨯ +ꨰ +ꨱ +ꨲ +ꨳ +ꨴ +ꨵ +ꨶ +ꩀ +ꩂ +ꩃ +ꩄ +ꩅ +ꩆ +ꩇ +ꩈ +ꩉ +ꩊ +ꩋ +ꩌ +ꩍ +꩐ +꩑ +꩒ +꩓ +꩔ +꩕ +꩖ +꩗ +꩘ +꩙ +꩜ +꩝ +꩞ +꩟ diff --git a/ocr_webapp/data/cham_dict_v23.txt b/ocr_webapp/data/cham_dict_v23.txt new file mode 100644 index 0000000000000000000000000000000000000000..2bea71db8da5d7baa2d38a1ebf6c98b7ca926e69 --- /dev/null +++ b/ocr_webapp/data/cham_dict_v23.txt @@ -0,0 +1,82 @@ +: +ꨀ +ꨁ +ꨂ +ꨃ +ꨄ +ꨅ +ꨆ +ꨇ +ꨈ +ꨉ +ꨊ +ꨋ +ꨌ +ꨍ +ꨎ +ꨏ +ꨐ +ꨑ +ꨒ +ꨓ +ꨔ +ꨕ +ꨖ +ꨗ +ꨘ +ꨙ +ꨚ +ꨛ +ꨜ +ꨝ +ꨞ +ꨟ +ꨠ +ꨡ +ꨢ +ꨣ +ꨤ +ꨥ +ꨦ +ꨧ +ꨨ +ꨩ +ꨪ +ꨫ +ꨬ +ꨭ +ꨮ +ꨯ +ꨰ +ꨱ +ꨳ +ꨴ +ꨵ +ꨶ +ꩀ +ꩂ +ꩃ +ꩄ +ꩅ +ꩆ +ꩇ +ꩈ +ꩉ +ꩊ +ꩋ +ꩌ +ꩍ +꩐ +꩑ +꩒ +꩓ +꩔ +꩕ +꩖ +꩗ +꩘ +꩙ +꩜ +꩝ +꩞ +꩟ diff --git a/ocr_webapp/data/cham_dict_v23_1.txt b/ocr_webapp/data/cham_dict_v23_1.txt new file mode 100644 index 0000000000000000000000000000000000000000..48a2cc0b4d5e180b23c5a74cbe755da3ea94583a --- /dev/null +++ b/ocr_webapp/data/cham_dict_v23_1.txt @@ -0,0 +1,101 @@ +: +ꨀ +ꨁ +ꨂ +ꨃ +ꨄ +ꨅ +ꨆ +ꨇ +ꨈ +ꨉ +ꨊ +ꨋ +ꨌ +ꨍ +ꨎ +ꨏ +ꨐ +ꨑ +ꨒ +ꨓ +ꨔ +ꨕ +ꨖ +ꨗ +ꨘ +ꨙ +ꨚ +ꨛ +ꨜ +ꨝ +ꨞ +ꨟ +ꨠ +ꨡ +ꨢ +ꨣ +ꨤ +ꨥ +ꨦ +ꨧ +ꨨ +ꨩ +ꨪ +ꨫ +ꨬ +ꨭ +ꨮ +ꨯ +ꨰ +ꨱ +ꨳ +ꨴ +ꨵ +ꨶ +ꩀ +ꩂ +ꩃ +ꩄ +ꩅ +ꩆ +ꩇ +ꩈ +ꩉ +ꩊ +ꩋ +ꩌ +ꩍ +꩐ +꩑ +꩒ +꩓ +꩔ +꩕ +꩖ +꩗ +꩘ +꩙ +꩜ +꩝ +꩞ +꩟ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +[ +] +( +) +. +, +; +- +/ diff --git a/ocr_webapp/data/matches.txt b/ocr_webapp/data/matches.txt new file mode 100644 index 0000000000000000000000000000000000000000..146a2692e8dd0ed7931f836555bf894077d3e4b6 --- /dev/null +++ b/ocr_webapp/data/matches.txt @@ -0,0 +1,249 @@ +app.py:9: # Monkeypatch NumPy 2.x to support legacy symbols removed/deprecated for PaddleOCR and imgaug +app.py:27: from http.server import BaseHTTPRequestHandler, HTTPServer +app.py:58: from paddleocr import PaddleOCR +app.py:59: ocr_rec_batch_size = int(os.getenv("OCR_REC_BATCH_SIZE", "16")) +app.py:69: text_recognition_batch_size=ocr_rec_batch_size +app.py:79: rec_batch_num=ocr_rec_batch_size +app.py:91: # Bypass detection pipeline and run batch recognition directly on text_rec_model +app.py:100: # PaddleOCR 2.x batching +app.py:108: print(f"OfficialPaddleOCRWrapper batch failed, falling back to sequential: {e}") +app.py:123: def __init__(self, cham_model, viet_model): +app.py:124: self.cham_model = cham_model +app.py:131: # 1. Predict all images with Cham model in a single batch +app.py:132: cham_res, _ = self.cham_model(img_list) +app.py:137: for i, (cham_text, cham_conf) in enumerate(cham_res): +app.py:138: if cham_conf < 0.65: +app.py:142: # 3. Predict those with Vietnamese model in a single batch +app.py:145: viet_batch_res, _ = self.viet_model(viet_imgs) +app.py:146: viet_res = viet_batch_res +app.py:152: cham_text, cham_conf = cham_res[i] +app.py:153: if cham_conf >= 0.65: +app.py:154: final_results[i] = (cham_text, cham_conf) +app.py:159: if cham_conf >= 0.40 and cham_conf > viet_conf - 0.15: +app.py:160: final_results[i] = (cham_text, cham_conf) +app.py:167: """Loads and caches the PaddleOCR model for the given version dynamically""" +app.py:172: if version in ['vi', 'en', 'ch', 'japan']: +app.py:178: cham_model = get_ocr_model('v23') +app.py:180: ocr_model = AutoRoutingOCRWrapper(cham_model, viet_model) +app.py:184: from tools.infer.predict_rec import TextRecognizer +app.py:187: model_dir = os.path.join(PROJECT_ROOT, "data", "output", f"rec_cham_inference_{version}") +app.py:188: dict_path = os.path.join(PROJECT_ROOT, "data", f"cham_dict_{version}.txt") +app.py:192: model_dir = os.path.join(PROJECT_ROOT, "data", "output", "rec_cham_inference") +app.py:193: dict_path = os.path.join(PROJECT_ROOT, "data", "cham_dict.txt") +app.py:201: args.rec_char_dict_path = dict_path +app.py:206: args.use_space_char = True +app.py:208: # Batch size config +app.py:209: ocr_rec_batch_size = int(os.getenv("OCR_REC_BATCH_SIZE", "16")) +app.py:210: args.rec_batch_num = ocr_rec_batch_size +app.py:218: args.mkldnn_cache_capacity = 32 +app.py:223: print(f"⚙️ Loading PaddleOCR Model {version} (GPU: {args.use_gpu}, MKLDNN: {args.enable_mkldnn}, Batch: {args.rec_batch_num})...") +app.py:226: # Warmup the model with a dummy image to trigger compilation/allocator caching +app.py:307: # Penalize if prediction length is too far from median +app.py:313: # Penalize if there is significant ink touching the top or bottom edges +app.py:314: # We check the top 3 and bottom 3 rows +app.py:322: # Boost safe slightly because it inherently suffers from resize-down confidence drops +app.py:338: similarity = difflib.SequenceMatcher(None, pred, gt).ratio() +app.py:341: def has_important_cham_sign_changes(pred1, pred2): +app.py:348: char = d[2] +app.py:350: if '\uA800' <= char <= '\uA82F' or '\uA840' <= char <= '\uA87F': +app.py:356: Advanced Multi-Crop Strategy with Risk-Gated Batched Inference +app.py:460: line_meta["reason"].append("very_close_lines_or_touching_diacritics") +app.py:537: # Phase 3: Pass 1 Batch Recognition +app.py:560: print(f"Batch inference pass 1 failed: {e}") +app.py:562: recognition_batch_pass1_sec = time.time() - t_pass1 +app.py:651: # Run Batch Pass 2 +app.py:672: print(f"Batch inference pass 2 failed: {e}") +app.py:674: recognition_batch_pass2_sec = time.time() - t_pass2_rec +app.py:720: single_chars = sum(1 for t in tokens if len(t) == 1) +app.py:721: if len(tokens) > 0 and single_chars / len(tokens) > 0.3: +app.py:725: similarity = difflib.SequenceMatcher(None, data["prediction"], safe_data["prediction"]).ratio() +app.py:785: - recognition_batch_pass1_sec: {recognition_batch_pass1_sec:.4f}s +app.py:786: - recognition_batch_pass2_sec: {recognition_batch_pass2_sec:.4f}s +app.py:797: "recognition_batch_pass1_sec": round(recognition_batch_pass1_sec, 4), +app.py:798: "recognition_batch_pass2_sec": round(recognition_batch_pass2_sec, 4), +app.py:819: self.send_header('Content-type', 'text/html; charset=utf-8') +app.py:820: self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') +app.py:821: self.send_header('Pragma', 'no-cache') +app.py:873: nparr = np.frombuffer(img_data, np.uint8) +app.py:912: from scripts.generate_data import normalize_unicode +app.py:915: # Encode cropped line to base64 for frontend display +app.py:967: self.send_header('Content-type', 'application/json; charset=utf-8') +index.html:4: +index.html:464: .char-bbox-rect { +index.html:471: animation: pulse-char 1.5s infinite; +index.html:474: @keyframes pulse-char { +index.html:568: .char-span { +index.html:575: .char-span:hover { +index.html:579: .char-span.active { +index.html:612: grid-template-columns: 1fr 1fr; +index.html:624: .inspector-item span:first-child { +index.html:681: @keyframes spin { +index.html:783: +index.html:791: Khởi chạy OCR +index.html:796: +index.html:832: +index.html:836: +index.html:843:
Lịch sử & Hàng đợi quét
+index.html:849: +index.html:860: + +
+ + +
+ + +
+ ▶ Cấu hình nâng cao +
+
+
+ + +
+ +
+ + +
+
+ + +
+
+ + + +
Lịch sử & Hàng đợi quét
+
+ +
+ +
+ +
+ + + +
+
+
+ + + +
+ + 100% + +
+ +
+
+ + + + + +
+
+ + +
+ +
+ +
+ +
+
+ + + + + +
+ +

Vui lòng tải ảnh lên để bắt đầu kiểm tra và hiệu chỉnh OCR

+
+
+
+ + +
+
Trình soạn thảo văn bản liên tục
+ + +
+ +
+ + +
+
+ CHI TIẾT ĐỐI TƯỢNG ĐANG CHỌN + Chưa chọn +
+ +
+
+ Văn bản chọn: + - +
+
+ Độ tự tin: + - +
+
+ OCR gốc: + - +
+
+ Nguồn Bbox: + - +
+
+ Tọa độ Bbox: + - +
+
+ +
+ + + +
+
+ + +
+
+ + +
+ + +
+ + +
+ + + +
+
+ + + + + + + + + + + +