Initial public release of BashkirTrOCR v3 (ONNX FP16 CUDA & INT8 CPU)
Browse files- LICENSE +202 -0
- META.json +62 -0
- NOTICE +12 -0
- QUICKSTART_RU.md +33 -0
- README.md +188 -0
- SHA256SUMS +28 -0
- config.json +18 -0
- examples/line_comparison.png +0 -0
- licenses/RapidOCR-Apache-2.0.txt +203 -0
- licenses/TrOCR-MIT.txt +21 -0
- models/dbnet.onnx +3 -0
- models/fp16/decoder.onnx +3 -0
- models/fp16/encoder.onnx +3 -0
- models/int8/decoder.onnx +3 -0
- models/int8/encoder.onnx +3 -0
- preprocessor_config.json +23 -0
- requirements-common.txt +9 -0
- requirements-cpu.txt +2 -0
- requirements-gpu.txt +4 -0
- run.py +21 -0
- runtime/__init__.py +1 -0
- runtime/dbnet_utils.py +290 -0
- runtime/engine.py +141 -0
- runtime/index.html +26 -0
- runtime/line_merger.py +98 -0
- start_ocr.bat +47 -0
- studio.py +59 -0
- tokenizer.json +0 -0
- tokenizer_config.json +1 -0
LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
META.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "bashkir_trocr_base",
|
| 3 |
+
"version": "v3.0-onnx-runtime.1",
|
| 4 |
+
"type": "onnx_ocr_model_bundle",
|
| 5 |
+
"language": "ba",
|
| 6 |
+
"license": "apache-2.0",
|
| 7 |
+
"status": "published",
|
| 8 |
+
"repo_id": "failed09/bashkir-trocr-base",
|
| 9 |
+
"intended_visibility": "public",
|
| 10 |
+
"architecture": "ViT-Base + TrOCR decoder",
|
| 11 |
+
"precision": {
|
| 12 |
+
"cpu": "dynamic_int8",
|
| 13 |
+
"gpu": "mixed_fp16_fp32_io"
|
| 14 |
+
},
|
| 15 |
+
"training_data_included": false,
|
| 16 |
+
"private_user_data_included": false,
|
| 17 |
+
"quality_claim": "No independent real-book CER/WER established. Manual review required.",
|
| 18 |
+
"artifacts": [
|
| 19 |
+
{
|
| 20 |
+
"file": "models/dbnet.onnx",
|
| 21 |
+
"bytes": 2432880,
|
| 22 |
+
"sha256": "3439588c030faea393a54515f51e983d8e155b19a2e8aba7891934c1cf0de526"
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"file": "models/fp16/decoder.onnx",
|
| 26 |
+
"bytes": 598137053,
|
| 27 |
+
"sha256": "e55c720fd10ae8f290c28d06a150dc118a118d020f7405fef85a1b273d9ed7a3"
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"file": "models/fp16/encoder.onnx",
|
| 31 |
+
"bytes": 172327813,
|
| 32 |
+
"sha256": "90a4ee3d331ea59cc2b60a860064ded38a3953163a33d4d16ce34f9b640103a5"
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"file": "models/int8/decoder.onnx",
|
| 36 |
+
"bytes": 300449064,
|
| 37 |
+
"sha256": "e04bcbaaee07851e7b0a47c7b271fcd85a4d8b68623a3241e2c968c22808aecb"
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"file": "models/int8/encoder.onnx",
|
| 41 |
+
"bytes": 89817188,
|
| 42 |
+
"sha256": "c9773380fd2e85ec4ccc07f570de7c2e63c88784570d3378a6af1ae744c7ca04"
|
| 43 |
+
}
|
| 44 |
+
],
|
| 45 |
+
"validation": {
|
| 46 |
+
"cpu_clean_environment": true,
|
| 47 |
+
"gpu_clean_environment": true,
|
| 48 |
+
"pytorch_required": false,
|
| 49 |
+
"transformers_required": false,
|
| 50 |
+
"cuda_providers_confirmed": true,
|
| 51 |
+
"private_inputs_redistributed": false,
|
| 52 |
+
"public_examples": 1,
|
| 53 |
+
"independent_real_book_accuracy_established": false
|
| 54 |
+
},
|
| 55 |
+
"known_limitations": [
|
| 56 |
+
"Superscript 1 may be recognized as !; I/l/1/7 can be confused.",
|
| 57 |
+
"Capitals, Bashkir letters and punctuation can be wrong.",
|
| 58 |
+
"Dash types and word-splitting at wide-line boundaries are unreliable.",
|
| 59 |
+
"Decorative marks, columns, skew and poor scans require manual review.",
|
| 60 |
+
"No PDF rasterization, handwriting guarantee, spelling repair or independent real-book CER/WER claim."
|
| 61 |
+
]
|
| 62 |
+
}
|
NOTICE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Bashkir TrOCR v3
|
| 2 |
+
Fine-tuned from microsoft/trocr-base-stage1.
|
| 3 |
+
|
| 4 |
+
PP-OCRv3 detection model and DBNet preprocessing/postprocessing helpers:
|
| 5 |
+
Copyright (c) 2020 PaddlePaddle Authors. Apache License 2.0.
|
| 6 |
+
Selected from RapidOCR 1.2.3 (https://github.com/RapidAI/RapidOCR).
|
| 7 |
+
Modified: only detector helpers retained; no recognition/classifier models bundled.
|
| 8 |
+
|
| 9 |
+
No source books, training corpus, personal paths or training logs are redistributed.
|
| 10 |
+
|
| 11 |
+
Upstream TrOCR implementation: Copyright (c) Microsoft Corporation, MIT; see licenses/TrOCR-MIT.txt.
|
| 12 |
+
RapidOCR: Copyright (c) 2021 RapidOCR Authors, Apache-2.0; see licenses/RapidOCR-Apache-2.0.txt.
|
QUICKSTART_RU.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Быстрый запуск Bashkir OCR v3
|
| 2 |
+
|
| 3 |
+
1. Установите Python 3.11–3.13 с python.org. На Windows включите «Add Python to PATH».
|
| 4 |
+
2. Скачайте репозиторий целиком в обычную папку с правом записи.
|
| 5 |
+
3. Запустите start_ocr.bat. По умолчанию выбран режим 3 (Авто — сам определит наличие видеокарты NVIDIA через nvidia-smi и поставит GPU FP16, иначе CPU INT8), либо выберите 1 — CPU INT8 или 2 — NVIDIA GPU FP16. Первый запуск скачает зависимости в локальное окружение. Дождитесь завершения установки.
|
| 6 |
+
4. Откроется браузер. Выберите изображение, нажмите «Распознать». Переключайтесь между вкладками «По строкам» (нарезка строк и построчная правка) и «Страница и текст» (страница целиком слева и весь текст единым блоком справа).
|
| 7 |
+
5. «Скопировать текст» копирует весь распознанный текст в буфер обмена; «Сохранить TXT» выгружает его в UTF-8 файл. Документы сервером на диск не записываются.
|
| 8 |
+
6. Для остановки закройте окно запуска. Для следующего запуска снова откройте батник.
|
| 9 |
+
|
| 10 |
+
## Режимы
|
| 11 |
+
|
| 12 |
+
- CPU INT8 — обычный процессор, отдельная видеокарта не нужна.
|
| 13 |
+
- GPU FP16 — NVIDIA CUDA. AMD/Intel GPU в этом релизе не проверены. CUDA/cuDNN зависимости ставятся в отдельное окружение; нужен установленный драйвер NVIDIA.
|
| 14 |
+
- Авто (по умолчанию) — проверяет наличие драйвера NVIDIA (nvidia-smi), автоматически устанавливает и запускает GPU-окружение (с откатом на CPU при сбое инициализации CUDA), либо использует CPU.
|
| 15 |
+
|
| 16 |
+
Не смешивайте onnxruntime и onnxruntime-gpu в одном окружении. Батник создаёт отдельные .venv-cpu и .venv-gpu. Пакет не содержит PyTorch/Transformers. Если установка прервана, следующий запуск повторит её. Если Python отсутствует, установите его и перезапустите батник.
|
| 17 |
+
|
| 18 |
+
## Ограничения
|
| 19 |
+
|
| 20 |
+
Принимается одно изображение страницы (PNG/JPEG/WebP/TIFF/BMP), до 25 МБ и 25 миллионов пикселей. Для PDF сначала экспортируйте страницы в изображения. Для TIFF используется первая страница. Не подавайте целую книгу одним длинным изображением.
|
| 21 |
+
|
| 22 |
+
Проверяйте мелкие сноски 1/!, регистр, похожие буквы, тире и переносы. Расположение колонок и сильно наклонённые страницы могут обрабатываться неверно. Декоративные элементы могут стать мусорным текстом. Красивый результат не означает отсутствие ошибок.
|
| 23 |
+
|
| 24 |
+
Сервер слушает только 127.0.0.1; изображения не отправляются в облако. Не меняйте привязку на публичный адрес. В интерфейсе нет телеметрии и внешних шрифтов.
|
| 25 |
+
|
| 26 |
+
## Если не запускается
|
| 27 |
+
|
| 28 |
+
- CUDA недоступна: обновите драйвер NVIDIA или выберите CPU. FP16 не предназначен для CPU.
|
| 29 |
+
- Ошибка DLL в Windows: проверьте Microsoft Visual C++ Redistributable и установку зависимостей выбранного окружения. PyTorch устанавливать не требуется.
|
| 30 |
+
- Порт 8001 занят: закройте предыдущий OCR либо запустите studio.py с --port 8002.
|
| 31 |
+
- Текст не найден: убедитесь, что изображение содержит текст, а не иллюстрацию; попробуйте более чёткую страницу.
|
| 32 |
+
- Видно предупреждение о длине строки: вручную проверьте обрезанный конец или разделите изображение на более короткие строки.
|
| 33 |
+
|
README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language:
|
| 3 |
+
- ba
|
| 4 |
+
license: apache-2.0
|
| 5 |
+
pretty_name: BashkirTrOCR
|
| 6 |
+
library_name: onnxruntime
|
| 7 |
+
pipeline_tag: image-to-text
|
| 8 |
+
tags:
|
| 9 |
+
- bashkir
|
| 10 |
+
- trocr
|
| 11 |
+
- ocr
|
| 12 |
+
- optical-character-recognition
|
| 13 |
+
- vision-encoder-decoder
|
| 14 |
+
- text-recognition
|
| 15 |
+
- onnx
|
| 16 |
+
- onnxruntime
|
| 17 |
+
- int8
|
| 18 |
+
- fp16
|
| 19 |
+
- low-resource
|
| 20 |
+
- experimental
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
# BashkirTrOCR
|
| 24 |
+
|
| 25 |
+
> Optical Character Recognition for printed Bashkir books and documents, shipped as standalone ONNX models (FP16 CUDA GPU & dynamic INT8 CPU) with a local browser review studio.
|
| 26 |
+
|
| 27 |
+
## Overview
|
| 28 |
+
|
| 29 |
+
This model transcribes printed Bashkir text lines and full document pages. It supports the complete 42-letter Bashkir Cyrillic alphabet, including all 9 specific letters (`ә, ө, ү, ғ, ҡ, ң, һ, ҫ, ҙ`), mixed Russian-Bashkir lines, punctuation, and dialogue formatting.
|
| 30 |
+
|
| 31 |
+
The release bundles a complete zero-PyTorch ONNX Runtime pipeline: `PP-OCRv3` (DBNet) text-line detection, wide-line splitting, `ViT-Base + TrOCR` recognition in both **mixed FP16** (for NVIDIA CUDA GPUs) and **dynamic INT8** (for CPUs), a CLI runner, and **Bashkir OCR Studio** — a local web interface with two review modes (line-by-line crop comparison and side-by-side full page + full text).
|
| 32 |
+
|
| 33 |
+
| At a glance | |
|
| 34 |
+
| --- | --- |
|
| 35 |
+
| Task | Page & line-level Optical Character Recognition (`image-to-text`) |
|
| 36 |
+
| Default artifact | **`models/fp16/`** (CUDA GPU) / **`models/int8/`** (CPU) |
|
| 37 |
+
| Architecture | Vision-Encoder-Decoder (`ViT-Base` + `TrOCR` Causal LM) + `PP-OCRv3` detector |
|
| 38 |
+
| Version / license | v3.0-onnx-runtime.1 / Apache-2.0 |
|
| 39 |
+
|
| 40 |
+
## Contents
|
| 41 |
+
|
| 42 |
+
### Files and Configurations
|
| 43 |
+
|
| 44 |
+
| File / Directory | Purpose | Size |
|
| 45 |
+
| --- | --- | ---: |
|
| 46 |
+
| **`models/fp16/encoder.onnx`** (recommended for GPU) | Mixed FP16 ViT encoder for NVIDIA CUDA | 172.3 MB |
|
| 47 |
+
| **`models/fp16/decoder.onnx`** (recommended for GPU) | Mixed FP16 TrOCR decoder for NVIDIA CUDA | 598.1 MB |
|
| 48 |
+
| **`models/int8/encoder.onnx`** (recommended for CPU) | Dynamic INT8 ViT encoder for CPU | 89.8 MB |
|
| 49 |
+
| **`models/int8/decoder.onnx`** (recommended for CPU) | Dynamic INT8 TrOCR decoder for CPU | 300.4 MB |
|
| 50 |
+
| `models/dbnet.onnx` | `PP-OCRv3` text-line detector graph | 2.4 MB |
|
| 51 |
+
| `tokenizer.json`, `tokenizer_config.json` | Tokenizer vocabulary (50,283 tokens) and settings | 1.8 MB |
|
| 52 |
+
| `config.json`, `preprocessor_config.json` | Runtime contract, generation limits and image normalization | — |
|
| 53 |
+
| `start_ocr.bat`, `studio.py`, `run.py`, `runtime/` | One-click Windows launcher, local browser studio and CLI | — |
|
| 54 |
+
| `requirements-cpu.txt`, `requirements-gpu.txt` | Isolated dependency manifests for CPU and CUDA runtimes | — |
|
| 55 |
+
| `QUICKSTART_RU.md` | Quickstart guide and troubleshooting in Russian | — |
|
| 56 |
+
| `META.json` | Release passport and artifact hashes | — |
|
| 57 |
+
| `LICENSE`, `NOTICE`, `licenses/` | Full Apache-2.0 license text and upstream attributions | — |
|
| 58 |
+
| `SHA256SUMS` | Checksums of the public files | — |
|
| 59 |
+
|
| 60 |
+
Raw training corpora, rendered synthetic image archives and full PyTorch checkpoints are not included.
|
| 61 |
+
|
| 62 |
+
### Model Architecture
|
| 63 |
+
|
| 64 |
+
| Property | Value |
|
| 65 |
+
|---|---|
|
| 66 |
+
| Base architecture | Vision-Encoder-Decoder (`VisionEncoderDecoderModel`) |
|
| 67 |
+
| Encoder | Vision Transformer (`ViT-Base`, 12 layers, 12 attention heads, hidden size 768) |
|
| 68 |
+
| Decoder | TrOCR Autoregressive Transformer Decoder (12 layers, 16 heads, hidden size 1024) |
|
| 69 |
+
| Line detector | `PP-OCRv3` (DBNet) polygon detector + horizontal line merger |
|
| 70 |
+
| Parameters | ~334M |
|
| 71 |
+
| Vocabulary | 50,283 tokens |
|
| 72 |
+
| Image resolution | 384 × 384 RGB |
|
| 73 |
+
|
| 74 |
+
### Examples
|
| 75 |
+
|
| 76 |
+
Side-by-side line recognition in **Bashkir OCR Studio** (cropped book lines on the left, recognized text on the right):
|
| 77 |
+
|
| 78 |
+

|
| 79 |
+
|
| 80 |
+
## Method
|
| 81 |
+
|
| 82 |
+
```text
|
| 83 |
+
BashkirTrOCR v3 pipeline
|
| 84 |
+
├── 1. page text-line detection (PP-OCRv3 / DBNet) & horizontal box merging
|
| 85 |
+
├── 2. wide-line splitting at whitespace valleys (aspect ratio > 20:1)
|
| 86 |
+
├── 3. ViT-Base visual encoding (384 × 384 RGB) & autoregressive TrOCR decoding
|
| 87 |
+
└── 4. interactive browser review (line-by-line crops or full-page side-by-side)
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
The recognizer was adapted from [`microsoft/trocr-base-stage1`](https://huggingface.co/microsoft/trocr-base-stage1) with full Bashkir Cyrillic vocabulary coverage and fine-tuned on a multi-domain collection of printed text lines (books, vintage scans, and digital typography). The trained v3 checkpoint was exported to ONNX and converted into two deployment variants:
|
| 91 |
+
- **Mixed FP16 (`models/fp16/`)** for NVIDIA CUDA GPUs, keeping graph inputs/outputs and numerically sensitive reductions in FP32 for exact parity with the PyTorch v3 checkpoint.
|
| 92 |
+
- **Dynamic INT8 (`models/int8/`)** for standalone CPU execution without a dedicated GPU.
|
| 93 |
+
|
| 94 |
+
### Benchmark
|
| 95 |
+
|
| 96 |
+
Runtime parity and speed comparison between the bundled ONNX variants on the 133-line multi-page diagnostic suite (NVIDIA RTX 4060 vs CPU):
|
| 97 |
+
|
| 98 |
+
| Runtime variant | Precision | Lines tested | Exact match vs PyTorch v3 | Full poem page latency | 133-line suite total time |
|
| 99 |
+
| --- | --- | ---: | ---: | ---: | ---: |
|
| 100 |
+
| **NVIDIA CUDA GPU** | Mixed FP16 | 133 | **133 / 133 (100%)** | **8.8 s** | **66.9 s** |
|
| 101 |
+
| **CPU** | Dynamic INT8 | 133 | 114 / 133 (85.7%) | 49.7 s | 392.4 s |
|
| 102 |
+
|
| 103 |
+
Differences between INT8 CPU and FP16 GPU outputs consist primarily of dash variants (`—` vs `–`) and minor punctuation on degraded scans.
|
| 104 |
+
|
| 105 |
+
## Quality and Use
|
| 106 |
+
|
| 107 |
+
Use this release for assisted digitization of printed Bashkir books, articles, and dictionary pages. The bundled **Bashkir OCR Studio** (`studio.py` / `start_ocr.bat`) serves as a ready-to-run reference application for single-page interactive review with two view modes:
|
| 108 |
+
- **Line-by-line review** — inspect each detected line crop alongside its editable transcription.
|
| 109 |
+
- **Full page & text** — view the full page scan on the left and edit the complete page text as a single block on the right, with one-click clipboard copy and UTF-8 text export.
|
| 110 |
+
|
| 111 |
+
For large-scale book digitization, the standalone `OCR` engine (`runtime/engine.py`) or the raw ONNX graphs can be directly integrated into custom batch, PDF, or multi-page processing pipelines.
|
| 112 |
+
|
| 113 |
+
### Limitations
|
| 114 |
+
|
| 115 |
+
- Tiny superscript footnote digits can be confused with punctuation (e.g. `1 → !`), and visually similar glyphs (`1`, `I`, `l`, `7`) require attention.
|
| 116 |
+
- Multi-column layouts, heavy page skew, decorative dividers, and low-contrast scans can affect reading order or line boundaries.
|
| 117 |
+
- Wide-line splitting may introduce spaces or split hyphenated words across chunk boundaries.
|
| 118 |
+
- Designed for printed Cyrillic Bashkir text; not intended for handwriting or historical Arabic-script manuscripts.
|
| 119 |
+
|
| 120 |
+
## Related Resources
|
| 121 |
+
|
| 122 |
+
- [Bashkir Diacritics Restorer](https://huggingface.co/failed09/bashkir-diacritics-restorer) — character-level restorer for Bashkir-specific letters (`ә, ө, ү, ғ, ҡ, ң, һ, ҫ, ҙ`), useful for post-correcting characters confused on degraded scans.
|
| 123 |
+
- [BashkirRoBERTa](https://huggingface.co/failed09/bashkir-roberta) — pretrained Bashkir masked language model suitable for scoring OCR hypotheses and reconstructing damaged words or broken line fragments.
|
| 124 |
+
|
| 125 |
+
## Usage
|
| 126 |
+
|
| 127 |
+
> **Note:** The bundled `Bashkir OCR Studio` web interface is provided as an interactive single-page usage example. For automated multi-page or full-book processing, import `runtime.engine.OCR` (or run the ONNX sessions directly) in your own batch pipeline.
|
| 128 |
+
|
| 129 |
+
### 1. Local Browser Studio (Windows One-Click Example)
|
| 130 |
+
|
| 131 |
+
Download the repository and double-click `start_ocr.bat`. By default (`[3] Auto`), the launcher automatically detects an NVIDIA GPU via `nvidia-smi`, creates an isolated `.venv-gpu` environment (or `.venv-cpu` on CPU-only machines), installs dependencies, and opens **Bashkir OCR Studio** at `http://127.0.0.1:8001`:
|
| 132 |
+
|
| 133 |
+
```bash
|
| 134 |
+
pip install huggingface_hub
|
| 135 |
+
hf download failed09/bashkir-trocr-base --local-dir bashkir-ocr
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
### 2. Command-Line & Custom Pipeline API
|
| 139 |
+
|
| 140 |
+
```bash
|
| 141 |
+
cd bashkir-ocr
|
| 142 |
+
python -m venv .venv-gpu
|
| 143 |
+
.venv-gpu\Scripts\python -m pip install -r requirements-gpu.txt
|
| 144 |
+
|
| 145 |
+
# Transcribe a page image from CLI (use --backend auto, gpu, or cpu)
|
| 146 |
+
.venv-gpu\Scripts\python run.py page.png --backend auto --output result.txt
|
| 147 |
+
|
| 148 |
+
# Launch the web studio manually
|
| 149 |
+
.venv-gpu\Scripts\python studio.py --backend auto
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
```python
|
| 153 |
+
from PIL import Image
|
| 154 |
+
from runtime.engine import OCR
|
| 155 |
+
|
| 156 |
+
ocr = OCR(".", backend="auto") # selects CUDA FP16 if available, else CPU INT8
|
| 157 |
+
result = ocr.recognize(Image.open("page.png"))
|
| 158 |
+
print(result["full_text"])
|
| 159 |
+
```
|
| 160 |
+
|
| 161 |
+
## License
|
| 162 |
+
|
| 163 |
+
The model weights, ONNX graphs and runtime code are distributed under [Apache-2.0](https://huggingface.co/failed09/bashkir-trocr-base/blob/main/LICENSE). Upstream attributions for TrOCR (`MIT`) and RapidOCR (`Apache-2.0`) are recorded in [NOTICE](https://huggingface.co/failed09/bashkir-trocr-base/blob/main/NOTICE). Rights in original source publications remain with their respective owners; raw book scans and training texts are not redistributed.
|
| 164 |
+
|
| 165 |
+
## Citation
|
| 166 |
+
|
| 167 |
+
```bibtex
|
| 168 |
+
@software{failed09_bashkir_trocr_2026,
|
| 169 |
+
title = {BashkirTrOCR: Printed Bashkir OCR with ONNX Runtime (FP16 CUDA & INT8 CPU)},
|
| 170 |
+
author = {failed09},
|
| 171 |
+
year = {2026},
|
| 172 |
+
publisher = {Hugging Face},
|
| 173 |
+
url = {https://huggingface.co/failed09/bashkir-trocr-base},
|
| 174 |
+
note = {Optical Character Recognition model and local review studio for Bashkir}
|
| 175 |
+
}
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
## Open Bashkir Data and Sources 🐝
|
| 179 |
+
|
| 180 |
+
This release is part of an open-source effort to support the development,
|
| 181 |
+
preservation and practical use of the Bashkir language. Other related models,
|
| 182 |
+
datasets and tools are available on the author's Hugging Face profile.
|
| 183 |
+
|
| 184 |
+
The author does not claim ownership or authorship of the source texts or other
|
| 185 |
+
materials used to derive this release; rights and licensing remain with the
|
| 186 |
+
original authors, publishers and dataset providers. Source texts are not
|
| 187 |
+
redistributed in this repository, so users should follow the licenses and
|
| 188 |
+
attribution requirements of the relevant upstream resources.
|
SHA256SUMS
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
3ddf9be5c28fe27dad143a5dc76eea25222ad1dd68934a047064e56ed2fa40c5 LICENSE
|
| 2 |
+
6eb2c965307dc568082b8e63bef68828e571f37d9aadd19a1b8d9f0fbff6e6b0 META.json
|
| 3 |
+
a7269948dafa3fd519facba67890b90704d566609ba86c3555d02ce62f4c9c24 NOTICE
|
| 4 |
+
b0d20c0131758208f59219b5aac7910c08147cac9a42789ab7952b9c10f4c520 QUICKSTART_RU.md
|
| 5 |
+
8c0b2fba7bbfd16caa92bccbc50e85050a379e13a3cf8972fc4fdb30290aa290 README.md
|
| 6 |
+
6af91e2db60bc88f047eebb53414e1de8bd1015d2566e801f34ef9f5ea71e6c2 config.json
|
| 7 |
+
58fb88778d56947e0929d8fb5d85e60e8fb18485061c465b9d3b3f3ed26e4219 examples/line_comparison.png
|
| 8 |
+
3e0af25fdd06aa9586ae97adb00ea927ebe5a3805ac77d2d3a81ce5f55693333 licenses/RapidOCR-Apache-2.0.txt
|
| 9 |
+
904dc4d8749877f1dba1cda48200d2462dccbeb7c134d5e4ef6fa75e0198c8fe licenses/TrOCR-MIT.txt
|
| 10 |
+
3439588c030faea393a54515f51e983d8e155b19a2e8aba7891934c1cf0de526 models/dbnet.onnx
|
| 11 |
+
e55c720fd10ae8f290c28d06a150dc118a118d020f7405fef85a1b273d9ed7a3 models/fp16/decoder.onnx
|
| 12 |
+
90a4ee3d331ea59cc2b60a860064ded38a3953163a33d4d16ce34f9b640103a5 models/fp16/encoder.onnx
|
| 13 |
+
e04bcbaaee07851e7b0a47c7b271fcd85a4d8b68623a3241e2c968c22808aecb models/int8/decoder.onnx
|
| 14 |
+
c9773380fd2e85ec4ccc07f570de7c2e63c88784570d3378a6af1ae744c7ca04 models/int8/encoder.onnx
|
| 15 |
+
1918ad875a1cd08aa9ca0ecb3e4ae13adabe89f850a038f35f485d916bbd6e10 preprocessor_config.json
|
| 16 |
+
77feb31f7f3f15a17fd569064c23abe9fa718f4a399dde268559ab644fae6512 requirements-common.txt
|
| 17 |
+
d1fc9108e0de2d3186caf337a97971c14032ef66001c2ec0024642b51eaa40c6 requirements-cpu.txt
|
| 18 |
+
a91cea3236c184653692f95944fa2c4967a7b431f7fa84029ef6efb2f26eac75 requirements-gpu.txt
|
| 19 |
+
8492c5f8e6048e769bd2a4ed1e51b57d4cfad036e30c6e7f4ec7bd3799b23ec5 run.py
|
| 20 |
+
089c2e7f646157f37640ac8dced5f20aa417728aa24d0ffc1c5cc4e37f9de21d runtime/__init__.py
|
| 21 |
+
7447d83ba6825d95db99fb4b561a223d94dcd4f32de7c4670d4ee82df2b6af81 runtime/dbnet_utils.py
|
| 22 |
+
380642fbed945044c7ae3f18a7565b569c32146ec5f13d5ca090f7e68354b617 runtime/engine.py
|
| 23 |
+
2eae00b66dff26e0a28c7d7c7b4ad8bbe7619926cc33a4f5c4111af2c8d28dfc runtime/index.html
|
| 24 |
+
af060a2df181d242311694ee5053fe52e4b1cf2a9758a666abb0488f37403cfa runtime/line_merger.py
|
| 25 |
+
293d3851b50aa16bf4f966d48a785d66187153ca86b8011a5b2ef1a2811e2b58 start_ocr.bat
|
| 26 |
+
32ea287ac8d514ccc0dafec12c293e604d328ed69b32453754ff6340b5be1762 studio.py
|
| 27 |
+
04ac1e6dec3f09a8cd17adbcc1d4a7809dbb3bdd2f800cbc8e9f33ec4155b28a tokenizer.json
|
| 28 |
+
416c94e5774d3bdf841aeea13f526ec76fe137b18db3acd56f65e7bc4d399ea3 tokenizer_config.json
|
config.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architecture": "ViT-Base + TrOCR decoder",
|
| 3 |
+
"image_size": 384,
|
| 4 |
+
"decoder_start_token_id": 0,
|
| 5 |
+
"eos_token_id": 2,
|
| 6 |
+
"pad_token_id": 1,
|
| 7 |
+
"max_new_tokens": 256,
|
| 8 |
+
"variants": {
|
| 9 |
+
"int8": {
|
| 10 |
+
"encoder": "models/int8/encoder.onnx",
|
| 11 |
+
"decoder": "models/int8/decoder.onnx"
|
| 12 |
+
},
|
| 13 |
+
"fp16": {
|
| 14 |
+
"encoder": "models/fp16/encoder.onnx",
|
| 15 |
+
"decoder": "models/fp16/decoder.onnx"
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
}
|
examples/line_comparison.png
ADDED
|
licenses/RapidOCR-Apache-2.0.txt
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright (c) 2021 RapidOCR Authors. All rights reserved.
|
| 2 |
+
|
| 3 |
+
Apache License
|
| 4 |
+
Version 2.0, January 2004
|
| 5 |
+
http://www.apache.org/licenses/
|
| 6 |
+
|
| 7 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 8 |
+
|
| 9 |
+
1. Definitions.
|
| 10 |
+
|
| 11 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 12 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 13 |
+
|
| 14 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 15 |
+
the copyright owner that is granting the License.
|
| 16 |
+
|
| 17 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 18 |
+
other entities that control, are controlled by, or are under common
|
| 19 |
+
control with that entity. For the purposes of this definition,
|
| 20 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 21 |
+
direction or management of such entity, whether by contract or
|
| 22 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 23 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 24 |
+
|
| 25 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 26 |
+
exercising permissions granted by this License.
|
| 27 |
+
|
| 28 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 29 |
+
including but not limited to software source code, documentation
|
| 30 |
+
source, and configuration files.
|
| 31 |
+
|
| 32 |
+
"Object" form shall mean any form resulting from mechanical
|
| 33 |
+
transformation or translation of a Source form, including but
|
| 34 |
+
not limited to compiled object code, generated documentation,
|
| 35 |
+
and conversions to other media types.
|
| 36 |
+
|
| 37 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 38 |
+
Object form, made available under the License, as indicated by a
|
| 39 |
+
copyright notice that is included in or attached to the work
|
| 40 |
+
(an example is provided in the Appendix below).
|
| 41 |
+
|
| 42 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 43 |
+
form, that is based on (or derived from) the Work and for which the
|
| 44 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 45 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 46 |
+
of this License, Derivative Works shall not include works that remain
|
| 47 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 48 |
+
the Work and Derivative Works thereof.
|
| 49 |
+
|
| 50 |
+
"Contribution" shall mean any work of authorship, including
|
| 51 |
+
the original version of the Work and any modifications or additions
|
| 52 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 53 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 54 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 55 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 56 |
+
means any form of electronic, verbal, or written communication sent
|
| 57 |
+
to the Licensor or its representatives, including but not limited to
|
| 58 |
+
communication on electronic mailing lists, source code control systems,
|
| 59 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 60 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 61 |
+
excluding communication that is conspicuously marked or otherwise
|
| 62 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 63 |
+
|
| 64 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 65 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 66 |
+
subsequently incorporated within the Work.
|
| 67 |
+
|
| 68 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 69 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 70 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 71 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 72 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 73 |
+
Work and such Derivative Works in Source or Object form.
|
| 74 |
+
|
| 75 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 76 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 77 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 78 |
+
(except as stated in this section) patent license to make, have made,
|
| 79 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 80 |
+
where such license applies only to those patent claims licensable
|
| 81 |
+
by such Contributor that are necessarily infringed by their
|
| 82 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 83 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 84 |
+
institute patent litigation against any entity (including a
|
| 85 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 86 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 87 |
+
or contributory patent infringement, then any patent licenses
|
| 88 |
+
granted to You under this License for that Work shall terminate
|
| 89 |
+
as of the date such litigation is filed.
|
| 90 |
+
|
| 91 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 92 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 93 |
+
modifications, and in Source or Object form, provided that You
|
| 94 |
+
meet the following conditions:
|
| 95 |
+
|
| 96 |
+
(a) You must give any other recipients of the Work or
|
| 97 |
+
Derivative Works a copy of this License; and
|
| 98 |
+
|
| 99 |
+
(b) You must cause any modified files to carry prominent notices
|
| 100 |
+
stating that You changed the files; and
|
| 101 |
+
|
| 102 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 103 |
+
that You distribute, all copyright, patent, trademark, and
|
| 104 |
+
attribution notices from the Source form of the Work,
|
| 105 |
+
excluding those notices that do not pertain to any part of
|
| 106 |
+
the Derivative Works; and
|
| 107 |
+
|
| 108 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 109 |
+
distribution, then any Derivative Works that You distribute must
|
| 110 |
+
include a readable copy of the attribution notices contained
|
| 111 |
+
within such NOTICE file, excluding those notices that do not
|
| 112 |
+
pertain to any part of the Derivative Works, in at least one
|
| 113 |
+
of the following places: within a NOTICE text file distributed
|
| 114 |
+
as part of the Derivative Works; within the Source form or
|
| 115 |
+
documentation, if provided along with the Derivative Works; or,
|
| 116 |
+
within a display generated by the Derivative Works, if and
|
| 117 |
+
wherever such third-party notices normally appear. The contents
|
| 118 |
+
of the NOTICE file are for informational purposes only and
|
| 119 |
+
do not modify the License. You may add Your own attribution
|
| 120 |
+
notices within Derivative Works that You distribute, alongside
|
| 121 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 122 |
+
that such additional attribution notices cannot be construed
|
| 123 |
+
as modifying the License.
|
| 124 |
+
|
| 125 |
+
You may add Your own copyright statement to Your modifications and
|
| 126 |
+
may provide additional or different license terms and conditions
|
| 127 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 128 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 129 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 130 |
+
the conditions stated in this License.
|
| 131 |
+
|
| 132 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 133 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 134 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 135 |
+
this License, without any additional terms or conditions.
|
| 136 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 137 |
+
the terms of any separate license agreement you may have executed
|
| 138 |
+
with Licensor regarding such Contributions.
|
| 139 |
+
|
| 140 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 141 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 142 |
+
except as required for reasonable and customary use in describing the
|
| 143 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 144 |
+
|
| 145 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 146 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 147 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 148 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 149 |
+
implied, including, without limitation, any warranties or conditions
|
| 150 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 151 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 152 |
+
appropriateness of using or redistributing the Work and assume any
|
| 153 |
+
risks associated with Your exercise of permissions under this License.
|
| 154 |
+
|
| 155 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 156 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 157 |
+
unless required by applicable law (such as deliberate and grossly
|
| 158 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 159 |
+
liable to You for damages, including any direct, indirect, special,
|
| 160 |
+
incidental, or consequential damages of any character arising as a
|
| 161 |
+
result of this License or out of the use or inability to use the
|
| 162 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 163 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 164 |
+
other commercial damages or losses), even if such Contributor
|
| 165 |
+
has been advised of the possibility of such damages.
|
| 166 |
+
|
| 167 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 168 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 169 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 170 |
+
or other liability obligations and/or rights consistent with this
|
| 171 |
+
License. However, in accepting such obligations, You may act only
|
| 172 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 173 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 174 |
+
defend, and hold each Contributor harmless for any liability
|
| 175 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 176 |
+
of your accepting any such warranty or additional liability.
|
| 177 |
+
|
| 178 |
+
END OF TERMS AND CONDITIONS
|
| 179 |
+
|
| 180 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 181 |
+
|
| 182 |
+
To apply the Apache License to your work, attach the following
|
| 183 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 184 |
+
replaced with your own identifying information. (Don't include
|
| 185 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 186 |
+
comment syntax for the file format. We also recommend that a
|
| 187 |
+
file or class name and description of purpose be included on the
|
| 188 |
+
same "printed page" as the copyright notice for easier
|
| 189 |
+
identification within third-party archives.
|
| 190 |
+
|
| 191 |
+
Copyright (c) 2021 RapidAI. All rights reserved.
|
| 192 |
+
|
| 193 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 194 |
+
you may not use this file except in compliance with the License.
|
| 195 |
+
You may obtain a copy of the License at
|
| 196 |
+
|
| 197 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 198 |
+
|
| 199 |
+
Unless required by applicable law or agreed to in writing, software
|
| 200 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 201 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 202 |
+
See the License for the specific language governing permissions and
|
| 203 |
+
limitations under the License.
|
licenses/TrOCR-MIT.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
The MIT License (MIT)
|
| 2 |
+
|
| 3 |
+
Copyright (c) Microsoft Corporation
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
models/dbnet.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3439588c030faea393a54515f51e983d8e155b19a2e8aba7891934c1cf0de526
|
| 3 |
+
size 2432880
|
models/fp16/decoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e55c720fd10ae8f290c28d06a150dc118a118d020f7405fef85a1b273d9ed7a3
|
| 3 |
+
size 598137053
|
models/fp16/encoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:90a4ee3d331ea59cc2b60a860064ded38a3953163a33d4d16ce34f9b640103a5
|
| 3 |
+
size 172327813
|
models/int8/decoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e04bcbaaee07851e7b0a47c7b271fcd85a4d8b68623a3241e2c968c22808aecb
|
| 3 |
+
size 300449064
|
models/int8/encoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c9773380fd2e85ec4ccc07f570de7c2e63c88784570d3378a6af1ae744c7ca04
|
| 3 |
+
size 89817188
|
preprocessor_config.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"do_normalize": true,
|
| 3 |
+
"do_rescale": true,
|
| 4 |
+
"do_resize": true,
|
| 5 |
+
"image_mean": [
|
| 6 |
+
0.5,
|
| 7 |
+
0.5,
|
| 8 |
+
0.5
|
| 9 |
+
],
|
| 10 |
+
"image_processor_type": "ViTImageProcessor",
|
| 11 |
+
"image_std": [
|
| 12 |
+
0.5,
|
| 13 |
+
0.5,
|
| 14 |
+
0.5
|
| 15 |
+
],
|
| 16 |
+
"processor_class": "TrOCRProcessor",
|
| 17 |
+
"resample": 2,
|
| 18 |
+
"rescale_factor": 0.00392156862745098,
|
| 19 |
+
"size": {
|
| 20 |
+
"height": 384,
|
| 21 |
+
"width": 384
|
| 22 |
+
}
|
| 23 |
+
}
|
requirements-common.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy>=1.26,<3
|
| 2 |
+
Pillow>=11,<13
|
| 3 |
+
tokenizers>=0.22,<0.23
|
| 4 |
+
opencv-python-headless>=4.10,<5
|
| 5 |
+
pyclipper>=1.3,<2
|
| 6 |
+
shapely>=2,<3
|
| 7 |
+
fastapi>=0.115,<1
|
| 8 |
+
uvicorn>=0.30,<1
|
| 9 |
+
python-multipart>=0.0.20,<0.1
|
requirements-cpu.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-r requirements-common.txt
|
| 2 |
+
onnxruntime==1.22.0
|
requirements-gpu.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-r requirements-common.txt
|
| 2 |
+
onnxruntime-gpu[cuda,cudnn]==1.22.0
|
| 3 |
+
# ORT 1.22 convolution frontend was validated with cuDNN 9.1; 9.27 failed.
|
| 4 |
+
nvidia-cudnn-cu12==9.1.0.70
|
run.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Recognize one image locally and write UTF-8 text and JSON."""
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from PIL import Image
|
| 6 |
+
from runtime.engine import OCR
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main():
|
| 10 |
+
p=argparse.ArgumentParser(description=__doc__)
|
| 11 |
+
p.add_argument('image',type=Path)
|
| 12 |
+
p.add_argument('--backend',choices=['cpu','gpu','auto'],default='cpu')
|
| 13 |
+
p.add_argument('--output',type=Path,default=Path('result.txt'))
|
| 14 |
+
args=p.parse_args()
|
| 15 |
+
with Image.open(args.image) as image:r=OCR(backend=args.backend).recognize(image,include_images=False)
|
| 16 |
+
args.output.write_text(r['full_text']+'\n',encoding='utf-8')
|
| 17 |
+
args.output.with_suffix('.json').write_text(json.dumps(r,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
| 18 |
+
print(f'Saved {args.output}; backend={r["backend"]}; lines={len(r["lines"])}')
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if __name__=='__main__':main()
|
runtime/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Standalone Bashkir OCR; no PyTorch or Transformers dependency."""
|
runtime/dbnet_utils.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
"""
|
| 16 |
+
# -*- encoding: utf-8 -*-
|
| 17 |
+
# @Author: SWHL
|
| 18 |
+
# Modified: selected detector helpers only; no recognition/classifier runtime.
|
| 19 |
+
import cv2
|
| 20 |
+
import numpy as np
|
| 21 |
+
import pyclipper
|
| 22 |
+
from shapely.geometry import Polygon
|
| 23 |
+
|
| 24 |
+
class DetResizeForTest():
|
| 25 |
+
def __init__(self, **kwargs):
|
| 26 |
+
super(DetResizeForTest, self).__init__()
|
| 27 |
+
self.resize_type = 0
|
| 28 |
+
if 'image_shape' in kwargs:
|
| 29 |
+
self.image_shape = kwargs['image_shape']
|
| 30 |
+
self.resize_type = 1
|
| 31 |
+
elif 'limit_side_len' in kwargs:
|
| 32 |
+
self.limit_side_len = kwargs.get('limit_side_len', 736)
|
| 33 |
+
self.limit_type = kwargs.get('limit_type', 'min')
|
| 34 |
+
|
| 35 |
+
if 'resize_long' in kwargs:
|
| 36 |
+
self.resize_type = 2
|
| 37 |
+
self.resize_long = kwargs.get('resize_long', 960)
|
| 38 |
+
else:
|
| 39 |
+
self.limit_side_len = kwargs.get('limit_side_len', 736)
|
| 40 |
+
self.limit_type = kwargs.get('limit_type', 'min')
|
| 41 |
+
|
| 42 |
+
def __call__(self, data):
|
| 43 |
+
img = data['image']
|
| 44 |
+
src_h, src_w = img.shape[:2]
|
| 45 |
+
|
| 46 |
+
if self.resize_type == 0:
|
| 47 |
+
# img, shape = self.resize_image_type0(img)
|
| 48 |
+
img, [ratio_h, ratio_w] = self.resize_image_type0(img)
|
| 49 |
+
elif self.resize_type == 2:
|
| 50 |
+
img, [ratio_h, ratio_w] = self.resize_image_type2(img)
|
| 51 |
+
else:
|
| 52 |
+
# img, shape = self.resize_image_type1(img)
|
| 53 |
+
img, [ratio_h, ratio_w] = self.resize_image_type1(img)
|
| 54 |
+
data['image'] = img
|
| 55 |
+
data['shape'] = np.array([src_h, src_w, ratio_h, ratio_w])
|
| 56 |
+
return data
|
| 57 |
+
|
| 58 |
+
def resize_image_type1(self, img):
|
| 59 |
+
resize_h, resize_w = self.image_shape
|
| 60 |
+
ori_h, ori_w = img.shape[:2] # (h, w, c)
|
| 61 |
+
ratio_h = float(resize_h) / ori_h
|
| 62 |
+
ratio_w = float(resize_w) / ori_w
|
| 63 |
+
img = cv2.resize(img, (int(resize_w), int(resize_h)))
|
| 64 |
+
# return img, np.array([ori_h, ori_w])
|
| 65 |
+
return img, [ratio_h, ratio_w]
|
| 66 |
+
|
| 67 |
+
def resize_image_type0(self, img):
|
| 68 |
+
"""
|
| 69 |
+
resize image to a size multiple of 32 which is required by the network
|
| 70 |
+
args:
|
| 71 |
+
img(array): array with shape [h, w, c]
|
| 72 |
+
return(tuple):
|
| 73 |
+
img, (ratio_h, ratio_w)
|
| 74 |
+
"""
|
| 75 |
+
limit_side_len = self.limit_side_len
|
| 76 |
+
h, w = img.shape[:2]
|
| 77 |
+
|
| 78 |
+
# limit the max side
|
| 79 |
+
if self.limit_type == 'max':
|
| 80 |
+
if max(h, w) > limit_side_len:
|
| 81 |
+
if h > w:
|
| 82 |
+
ratio = float(limit_side_len) / h
|
| 83 |
+
else:
|
| 84 |
+
ratio = float(limit_side_len) / w
|
| 85 |
+
else:
|
| 86 |
+
ratio = 1.
|
| 87 |
+
else:
|
| 88 |
+
if min(h, w) < limit_side_len:
|
| 89 |
+
if h < w:
|
| 90 |
+
ratio = float(limit_side_len) / h
|
| 91 |
+
else:
|
| 92 |
+
ratio = float(limit_side_len) / w
|
| 93 |
+
else:
|
| 94 |
+
ratio = 1.
|
| 95 |
+
resize_h = int(h * ratio)
|
| 96 |
+
resize_w = int(w * ratio)
|
| 97 |
+
|
| 98 |
+
resize_h = int(round(resize_h / 32) * 32)
|
| 99 |
+
resize_w = int(round(resize_w / 32) * 32)
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
if int(resize_w) <= 0 or int(resize_h) <= 0:
|
| 103 |
+
return None, (None, None)
|
| 104 |
+
img = cv2.resize(img, (int(resize_w), int(resize_h)))
|
| 105 |
+
except:
|
| 106 |
+
print(img.shape, resize_w, resize_h)
|
| 107 |
+
sys.exit(0)
|
| 108 |
+
ratio_h = resize_h / float(h)
|
| 109 |
+
ratio_w = resize_w / float(w)
|
| 110 |
+
return img, [ratio_h, ratio_w]
|
| 111 |
+
|
| 112 |
+
def resize_image_type2(self, img):
|
| 113 |
+
h, w = img.shape[:2]
|
| 114 |
+
|
| 115 |
+
resize_w = w
|
| 116 |
+
resize_h = h
|
| 117 |
+
|
| 118 |
+
# Fix the longer side
|
| 119 |
+
if resize_h > resize_w:
|
| 120 |
+
ratio = float(self.resize_long) / resize_h
|
| 121 |
+
else:
|
| 122 |
+
ratio = float(self.resize_long) / resize_w
|
| 123 |
+
|
| 124 |
+
resize_h = int(resize_h * ratio)
|
| 125 |
+
resize_w = int(resize_w * ratio)
|
| 126 |
+
|
| 127 |
+
max_stride = 128
|
| 128 |
+
resize_h = (resize_h + max_stride - 1) // max_stride * max_stride
|
| 129 |
+
resize_w = (resize_w + max_stride - 1) // max_stride * max_stride
|
| 130 |
+
img = cv2.resize(img, (int(resize_w), int(resize_h)))
|
| 131 |
+
ratio_h = resize_h / float(h)
|
| 132 |
+
ratio_w = resize_w / float(w)
|
| 133 |
+
|
| 134 |
+
return img, [ratio_h, ratio_w]
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
class DBPostProcess():
|
| 138 |
+
"""The post process for Differentiable Binarization (DB)."""
|
| 139 |
+
|
| 140 |
+
def __init__(self,
|
| 141 |
+
thresh=0.3,
|
| 142 |
+
box_thresh=0.7,
|
| 143 |
+
max_candidates=1000,
|
| 144 |
+
unclip_ratio=2.0,
|
| 145 |
+
score_mode="fast",
|
| 146 |
+
use_dilation=False):
|
| 147 |
+
self.thresh = thresh
|
| 148 |
+
self.box_thresh = box_thresh
|
| 149 |
+
self.max_candidates = max_candidates
|
| 150 |
+
self.unclip_ratio = unclip_ratio
|
| 151 |
+
self.min_size = 3
|
| 152 |
+
self.score_mode = score_mode
|
| 153 |
+
|
| 154 |
+
if use_dilation:
|
| 155 |
+
self.dilation_kernel = np.array([[1, 1], [1, 1]])
|
| 156 |
+
else:
|
| 157 |
+
self.dilation_kernel = None
|
| 158 |
+
|
| 159 |
+
def boxes_from_bitmap(self, pred, _bitmap, dest_width, dest_height):
|
| 160 |
+
'''
|
| 161 |
+
_bitmap: single map with shape (1, H, W),
|
| 162 |
+
whose values are binarized as {0, 1}
|
| 163 |
+
'''
|
| 164 |
+
|
| 165 |
+
bitmap = _bitmap
|
| 166 |
+
height, width = bitmap.shape
|
| 167 |
+
|
| 168 |
+
outs = cv2.findContours((bitmap * 255).astype(np.uint8), cv2.RETR_LIST,
|
| 169 |
+
cv2.CHAIN_APPROX_SIMPLE)
|
| 170 |
+
if len(outs) == 3:
|
| 171 |
+
img, contours, _ = outs[0], outs[1], outs[2]
|
| 172 |
+
elif len(outs) == 2:
|
| 173 |
+
contours, _ = outs[0], outs[1]
|
| 174 |
+
|
| 175 |
+
num_contours = min(len(contours), self.max_candidates)
|
| 176 |
+
|
| 177 |
+
boxes = []
|
| 178 |
+
scores = []
|
| 179 |
+
for index in range(num_contours):
|
| 180 |
+
contour = contours[index]
|
| 181 |
+
points, sside = self.get_mini_boxes(contour)
|
| 182 |
+
if sside < self.min_size:
|
| 183 |
+
continue
|
| 184 |
+
points = np.array(points)
|
| 185 |
+
if self.score_mode == "fast":
|
| 186 |
+
score = self.box_score_fast(pred, points.reshape(-1, 2))
|
| 187 |
+
else:
|
| 188 |
+
score = self.box_score_slow(pred, contour)
|
| 189 |
+
if self.box_thresh > score:
|
| 190 |
+
continue
|
| 191 |
+
|
| 192 |
+
box = self.unclip(points).reshape(-1, 1, 2)
|
| 193 |
+
box, sside = self.get_mini_boxes(box)
|
| 194 |
+
if sside < self.min_size + 2:
|
| 195 |
+
continue
|
| 196 |
+
box = np.array(box)
|
| 197 |
+
|
| 198 |
+
box[:, 0] = np.clip(
|
| 199 |
+
np.round(box[:, 0] / width * dest_width), 0, dest_width)
|
| 200 |
+
box[:, 1] = np.clip(
|
| 201 |
+
np.round(box[:, 1] / height * dest_height), 0, dest_height)
|
| 202 |
+
boxes.append(box.astype(np.int16))
|
| 203 |
+
scores.append(score)
|
| 204 |
+
return np.array(boxes, dtype=np.int16), scores
|
| 205 |
+
|
| 206 |
+
def unclip(self, box):
|
| 207 |
+
unclip_ratio = self.unclip_ratio
|
| 208 |
+
poly = Polygon(box)
|
| 209 |
+
distance = poly.area * unclip_ratio / poly.length
|
| 210 |
+
offset = pyclipper.PyclipperOffset()
|
| 211 |
+
offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)
|
| 212 |
+
expanded = np.array(offset.Execute(distance))
|
| 213 |
+
return expanded
|
| 214 |
+
|
| 215 |
+
def get_mini_boxes(self, contour):
|
| 216 |
+
bounding_box = cv2.minAreaRect(contour)
|
| 217 |
+
points = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0])
|
| 218 |
+
|
| 219 |
+
index_1, index_2, index_3, index_4 = 0, 1, 2, 3
|
| 220 |
+
if points[1][1] > points[0][1]:
|
| 221 |
+
index_1 = 0
|
| 222 |
+
index_4 = 1
|
| 223 |
+
else:
|
| 224 |
+
index_1 = 1
|
| 225 |
+
index_4 = 0
|
| 226 |
+
if points[3][1] > points[2][1]:
|
| 227 |
+
index_2 = 2
|
| 228 |
+
index_3 = 3
|
| 229 |
+
else:
|
| 230 |
+
index_2 = 3
|
| 231 |
+
index_3 = 2
|
| 232 |
+
|
| 233 |
+
box = [
|
| 234 |
+
points[index_1], points[index_2], points[index_3], points[index_4]
|
| 235 |
+
]
|
| 236 |
+
return box, min(bounding_box[1])
|
| 237 |
+
|
| 238 |
+
def box_score_fast(self, bitmap, _box):
|
| 239 |
+
h, w = bitmap.shape[:2]
|
| 240 |
+
box = _box.copy()
|
| 241 |
+
xmin = np.clip(np.floor(box[:, 0].min()).astype(np.int32), 0, w - 1)
|
| 242 |
+
xmax = np.clip(np.ceil(box[:, 0].max()).astype(np.int32), 0, w - 1)
|
| 243 |
+
ymin = np.clip(np.floor(box[:, 1].min()).astype(np.int32), 0, h - 1)
|
| 244 |
+
ymax = np.clip(np.ceil(box[:, 1].max()).astype(np.int32), 0, h - 1)
|
| 245 |
+
|
| 246 |
+
mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8)
|
| 247 |
+
box[:, 0] = box[:, 0] - xmin
|
| 248 |
+
box[:, 1] = box[:, 1] - ymin
|
| 249 |
+
cv2.fillPoly(mask, box.reshape(1, -1, 2).astype(np.int32), 1)
|
| 250 |
+
return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0]
|
| 251 |
+
|
| 252 |
+
def box_score_slow(self, bitmap, contour):
|
| 253 |
+
'''
|
| 254 |
+
box_score_slow: use polyon mean score as the mean score
|
| 255 |
+
'''
|
| 256 |
+
h, w = bitmap.shape[:2]
|
| 257 |
+
contour = contour.copy()
|
| 258 |
+
contour = np.reshape(contour, (-1, 2))
|
| 259 |
+
|
| 260 |
+
xmin = np.clip(np.min(contour[:, 0]), 0, w - 1)
|
| 261 |
+
xmax = np.clip(np.max(contour[:, 0]), 0, w - 1)
|
| 262 |
+
ymin = np.clip(np.min(contour[:, 1]), 0, h - 1)
|
| 263 |
+
ymax = np.clip(np.max(contour[:, 1]), 0, h - 1)
|
| 264 |
+
|
| 265 |
+
mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8)
|
| 266 |
+
|
| 267 |
+
contour[:, 0] = contour[:, 0] - xmin
|
| 268 |
+
contour[:, 1] = contour[:, 1] - ymin
|
| 269 |
+
|
| 270 |
+
cv2.fillPoly(mask, contour.reshape(1, -1, 2).astype(np.int32), 1)
|
| 271 |
+
return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0]
|
| 272 |
+
|
| 273 |
+
def __call__(self, pred, shape_list):
|
| 274 |
+
pred = pred[:, 0, :, :]
|
| 275 |
+
segmentation = pred > self.thresh
|
| 276 |
+
|
| 277 |
+
boxes_batch = []
|
| 278 |
+
for batch_index in range(pred.shape[0]):
|
| 279 |
+
src_h, src_w, ratio_h, ratio_w = shape_list[batch_index]
|
| 280 |
+
if self.dilation_kernel is not None:
|
| 281 |
+
mask = cv2.dilate(
|
| 282 |
+
np.array(segmentation[batch_index]).astype(np.uint8),
|
| 283 |
+
self.dilation_kernel)
|
| 284 |
+
else:
|
| 285 |
+
mask = segmentation[batch_index]
|
| 286 |
+
boxes, scores = self.boxes_from_bitmap(pred[batch_index], mask,
|
| 287 |
+
src_w, src_h)
|
| 288 |
+
|
| 289 |
+
boxes_batch.append({'points': boxes})
|
| 290 |
+
return boxes_batch
|
runtime/engine.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Local ONNX OCR with shared detection and line assembly for CPU/GPU."""
|
| 2 |
+
import base64
|
| 3 |
+
import io
|
| 4 |
+
import json
|
| 5 |
+
import time
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import cv2
|
| 10 |
+
from PIL import Image, ImageOps
|
| 11 |
+
from tokenizers import Tokenizer
|
| 12 |
+
|
| 13 |
+
from .dbnet_utils import DBPostProcess, DetResizeForTest
|
| 14 |
+
from .line_merger import merge_line_boxes
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def pixels(image, config):
|
| 18 |
+
size=config['size']
|
| 19 |
+
image=image.convert('RGB').resize((size['width'],size['height']),Image.Resampling(config['resample']))
|
| 20 |
+
x=np.asarray(image,dtype=np.float32)*np.float32(config['rescale_factor'])
|
| 21 |
+
x=(x-np.asarray(config['image_mean'],dtype=np.float32))/np.asarray(config['image_std'],dtype=np.float32)
|
| 22 |
+
return np.ascontiguousarray(x.transpose(2,0,1)[None])
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def split_line(image, ratio=20):
|
| 26 |
+
w,h=image.size
|
| 27 |
+
if w/max(h,1)<=ratio:return [image]
|
| 28 |
+
ink=(np.asarray(image.convert('L'))<210).sum(axis=0)
|
| 29 |
+
runs=[];start=None
|
| 30 |
+
for x,value in enumerate(ink):
|
| 31 |
+
if value==0 and start is None:start=x
|
| 32 |
+
elif value!=0 and start is not None:runs.append((start,x));start=None
|
| 33 |
+
if start is not None:runs.append((start,w))
|
| 34 |
+
gaps=[(a,b) for a,b in runs if a>40 and b<w-40 and b-a>=8]
|
| 35 |
+
if gaps:
|
| 36 |
+
a,b=min(gaps,key=lambda r:abs((r[0]+r[1])/2-w/2));cut=(a+b)//2
|
| 37 |
+
else:
|
| 38 |
+
left,right=int(w*.35),int(w*.65)
|
| 39 |
+
cut=left+int(np.argmin(ink[left:right])) if right>left else w//2
|
| 40 |
+
cut=max(1,min(w-1,cut))
|
| 41 |
+
return split_line(image.crop((0,0,cut,h)),ratio)+split_line(image.crop((cut,0,w,h)),ratio)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def preview(image):
|
| 45 |
+
image=image.copy();image.thumbnail((1100,180))
|
| 46 |
+
out=io.BytesIO();image.save(out,format='PNG')
|
| 47 |
+
return 'data:image/png;base64,'+base64.b64encode(out.getvalue()).decode('ascii')
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class OCR:
|
| 51 |
+
def __init__(self, root=None, backend='cpu', threads=4):
|
| 52 |
+
import onnxruntime as ort
|
| 53 |
+
ort.disable_telemetry_events()
|
| 54 |
+
self.root=Path(root or Path(__file__).resolve().parents[1])
|
| 55 |
+
self.config=json.loads((self.root/'config.json').read_text(encoding='utf-8'))
|
| 56 |
+
self.preprocess=json.loads((self.root/'preprocessor_config.json').read_text(encoding='utf-8'))
|
| 57 |
+
self.tokenizer=Tokenizer.from_file(str(self.root/'tokenizer.json'))
|
| 58 |
+
self.tokenizer.no_padding();self.tokenizer.no_truncation()
|
| 59 |
+
requested_backend=backend
|
| 60 |
+
if backend not in ('cpu','gpu','auto'):raise ValueError('backend must be cpu, gpu or auto')
|
| 61 |
+
if backend in ('gpu','auto') and hasattr(ort,'preload_dlls'):
|
| 62 |
+
ort.preload_dlls()
|
| 63 |
+
if backend=='auto':backend='gpu' if 'CUDAExecutionProvider' in ort.get_available_providers() else 'cpu'
|
| 64 |
+
if backend=='gpu' and 'CUDAExecutionProvider' not in ort.get_available_providers():
|
| 65 |
+
raise RuntimeError('CUDA недоступна. Установите GPU-зависимости или выберите CPU.')
|
| 66 |
+
options=ort.SessionOptions();options.intra_op_num_threads=threads;options.inter_op_num_threads=1
|
| 67 |
+
if backend=='gpu':
|
| 68 |
+
variant=self.config['variants']['fp16']
|
| 69 |
+
providers=['CUDAExecutionProvider','CPUExecutionProvider']
|
| 70 |
+
try:
|
| 71 |
+
self.encoder=ort.InferenceSession(str(self.root/variant['encoder']),sess_options=options,providers=providers)
|
| 72 |
+
self.decoder=ort.InferenceSession(str(self.root/variant['decoder']),sess_options=options,providers=providers)
|
| 73 |
+
if any('CUDAExecutionProvider' not in s.get_providers() for s in (self.encoder,self.decoder)):
|
| 74 |
+
raise RuntimeError('Модель не загрузилась на CUDA. Выберите CPU; скрытая смена GPU на CPU запрещена.')
|
| 75 |
+
self.encoder.disable_fallback()
|
| 76 |
+
self.decoder.disable_fallback()
|
| 77 |
+
except Exception:
|
| 78 |
+
if requested_backend!='auto':raise
|
| 79 |
+
backend='cpu'
|
| 80 |
+
if backend=='cpu':
|
| 81 |
+
variant=self.config['variants']['int8']
|
| 82 |
+
providers=['CPUExecutionProvider']
|
| 83 |
+
self.encoder=ort.InferenceSession(str(self.root/variant['encoder']),sess_options=options,providers=providers)
|
| 84 |
+
self.decoder=ort.InferenceSession(str(self.root/variant['decoder']),sess_options=options,providers=providers)
|
| 85 |
+
self.backend=backend
|
| 86 |
+
self.detector=ort.InferenceSession(str(self.root/'models/dbnet.onnx'),sess_options=options,providers=['CPUExecutionProvider'])
|
| 87 |
+
self.resize=DetResizeForTest(limit_side_len=736,limit_type='min')
|
| 88 |
+
self.post=DBPostProcess(thresh=.2,box_thresh=.2,max_candidates=1000,unclip_ratio=1.3,use_dilation=True,score_mode='fast')
|
| 89 |
+
|
| 90 |
+
def recognize_line(self,image):
|
| 91 |
+
hidden=self.encoder.run(None,{'pixel_values':pixels(image,self.preprocess)})[0]
|
| 92 |
+
ids=np.array([[self.config['decoder_start_token_id']]],dtype=np.int64)
|
| 93 |
+
truncated=True
|
| 94 |
+
for _ in range(self.config['max_new_tokens']):
|
| 95 |
+
logits=self.decoder.run(None,{'input_ids':ids,'encoder_hidden_states':hidden})[0]
|
| 96 |
+
if not np.isfinite(logits).all():raise RuntimeError('Non-finite decoder output')
|
| 97 |
+
token=int(np.argmax(logits[0,-1]))
|
| 98 |
+
ids=np.concatenate((ids,np.array([[token]],dtype=np.int64)),axis=1)
|
| 99 |
+
if token==self.config['eos_token_id']:truncated=False;break
|
| 100 |
+
return self.tokenizer.decode(ids[0].tolist(),skip_special_tokens=True).strip(),truncated
|
| 101 |
+
|
| 102 |
+
def detect(self,image):
|
| 103 |
+
# The original detector upsizes the short side. Bound unusually narrow
|
| 104 |
+
# images so a one-line crop cannot allocate a gigantic detector tensor.
|
| 105 |
+
scale=max(1.,736/min(image.size))
|
| 106 |
+
if max(image.size)*scale>4096:
|
| 107 |
+
scale=4096/max(image.size)
|
| 108 |
+
width=max(32,round(image.width*scale/32)*32)
|
| 109 |
+
height=max(32,round(image.height*scale/32)*32)
|
| 110 |
+
data=dict(image=cv2.resize(np.asarray(image),(width,height)),
|
| 111 |
+
shape=np.array([image.height,image.width,height/image.height,width/image.width]))
|
| 112 |
+
else:data=self.resize({'image':np.asarray(image)})
|
| 113 |
+
x=np.asarray(data['image'],dtype=np.float32)*np.float32(1/255)
|
| 114 |
+
x=(x-np.array([.485,.456,.406],dtype=np.float32))/np.array([.229,.224,.225],dtype=np.float32)
|
| 115 |
+
x=np.ascontiguousarray(x.transpose(2,0,1)[None])
|
| 116 |
+
output=self.detector.run(None,{self.detector.get_inputs()[0].name:x})[0]
|
| 117 |
+
boxes=self.post(output,np.asarray([data['shape']]))[0]['points']
|
| 118 |
+
valid=[]
|
| 119 |
+
for box in boxes:
|
| 120 |
+
box=np.asarray(box,dtype=np.float32)
|
| 121 |
+
box[:,0]=np.clip(box[:,0],0,image.width-1);box[:,1]=np.clip(box[:,1],0,image.height-1)
|
| 122 |
+
if np.linalg.norm(box[0]-box[1])>3 and np.linalg.norm(box[0]-box[3])>3:valid.append(box)
|
| 123 |
+
return merge_line_boxes(valid,x_margin_ratio=3.5,y_overlap_thresh=.45)
|
| 124 |
+
|
| 125 |
+
def recognize(self,image,boxes=None,include_images=True):
|
| 126 |
+
t=time.perf_counter()
|
| 127 |
+
image=ImageOps.exif_transpose(image).convert('RGB')
|
| 128 |
+
if np.asarray(image.convert('L')).mean()<125:image=ImageOps.invert(image)
|
| 129 |
+
boxes=self.detect(image) if boxes is None else boxes
|
| 130 |
+
lines=[]
|
| 131 |
+
for box in boxes:
|
| 132 |
+
a,b,c,d=[int(v) for v in box]
|
| 133 |
+
if c-a<4 or d-b<4:continue
|
| 134 |
+
crop=image.crop((max(0,a-4),max(0,b-2),min(image.width,c+4),min(image.height,d+2)))
|
| 135 |
+
parts=[self.recognize_line(ch) for ch in split_line(crop)]
|
| 136 |
+
text=' '.join(s for s,_ in parts if s).strip()
|
| 137 |
+
line=dict(bbox=[a,b,c,d],text=text,truncated=any(end for _,end in parts),chunks=len(parts))
|
| 138 |
+
if include_images:line['image']=preview(crop)
|
| 139 |
+
lines.append(line)
|
| 140 |
+
return dict(backend=self.backend,lines=lines,full_text='\n'.join(row['text'] for row in lines),
|
| 141 |
+
seconds=time.perf_counter()-t,no_text_detected=not lines)
|
runtime/index.html
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="ru"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 3 |
+
<title>Bashkir OCR Studio</title>
|
| 4 |
+
<style>
|
| 5 |
+
:root{color-scheme:light;--ink:#183333;--muted:#637778;--border:#d8e3df;--accent:#196c56}*{box-sizing:border-box}body{margin:0;background:#f3f6f2;color:var(--ink);font:16px/1.55 system-ui,sans-serif}main{max-width:1300px;margin:auto;padding:32px 24px}header{display:flex;justify-content:space-between;align-items:start;gap:20px}h1{font-size:30px;letter-spacing:-1px;margin:0}p{max-width:850px}.muted{color:var(--muted)}.card{background:white;border:1px solid var(--border);border-radius:16px;padding:24px;margin:22px 0}.controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap}button,.file{border:1px solid var(--border);background:white;border-radius:9px;padding:11px 17px;font:inherit;color:var(--ink);cursor:pointer}button.primary{background:var(--accent);color:white;border-color:var(--accent)}button:disabled{opacity:.45;cursor:wait}input[type=file]{max-width:400px}#original{max-height:420px;max-width:100%;object-fit:contain;margin:18px 0}.tabs{display:inline-flex;background:#e8efec;padding:4px;border-radius:11px;border:1px solid var(--border);gap:4px}.tab{border:none;background:transparent;border-radius:8px;padding:8px 15px;font-weight:600;color:var(--muted);cursor:pointer}.tab.active{background:var(--accent);color:white;box-shadow:0 1px 3px rgba(0,0,0,.12)}.row{display:grid;grid-template-columns:42px minmax(0,1fr) minmax(0,1fr);gap:16px;padding:18px 0;border-bottom:1px solid var(--border);align-items:start}.row img{max-width:100%;background:#fff}.num{color:var(--muted);font-size:13px}textarea{width:100%;min-height:62px;resize:vertical;border:1px solid var(--border);border-radius:8px;padding:10px;font:17px/1.5 system-ui;color:var(--ink);background:#fcfdfb}.page-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:20px;align-items:start;margin-top:12px}.page-pane{position:sticky;top:16px}.page-pane img{max-width:100%;max-height:78vh;object-fit:contain;border:1px solid var(--border);border-radius:10px;background:#fff;display:block}#fullText{min-height:520px;height:72vh;font:17px/1.6 system-ui;padding:14px}.warning{color:#9b5b12;font-size:13px}.rowhead{font-weight:600;color:var(--muted);font-size:13px}pre{white-space:pre-wrap;font:inherit}.badge{white-space:nowrap;border:1px solid var(--border);padding:7px 12px;border-radius:30px}#message[role=alert]{color:#9b3612}@media(max-width:700px){main{padding:22px 14px}.card{padding:16px}.row{grid-template-columns:25px minmax(0,1fr)}.row textarea,.row .result{grid-column:2}.rowhead{display:none}.page-split{grid-template-columns:1fr}.page-pane{position:static}header{display:block}.badge{display:inline-block;margin-top:12px}}
|
| 6 |
+
</style>
|
| 7 |
+
<main><header><div><h1>Bashkir OCR Studio</h1><div class="muted">Распознайте. Сверьте со сканом. Сохраните текст.</div></div><span class="badge" id="backend">Загрузка…</span></header>
|
| 8 |
+
<section class="card"><div class="controls"><input id="file" type="file" accept="image/png,image/jpeg,image/webp,image/tiff,image/bmp"><button class="primary" id="recognize" disabled>Распознать</button><div style="flex:1"></div><div class="tabs" role="tablist" aria-label="Режим просмотра"><button type="button" class="tab active" id="tabLines" role="tab" aria-selected="true">По строкам</button><button type="button" class="tab" id="tabPage" role="tab" aria-selected="false">Страница и текст</button></div></div><p class="muted">Изображение обрабатывается на этом компьютере. Оно не отправляется в облако и не сохраняется сервером. Поддерживается одно изображение за раз.</p><details><summary>На что обратить внимание</summary><p class="muted">Проверьте мелкие сноски: <b>1</b> может превратиться в <b>!</b>. Возможны ошибки регистра, похожих букв и тире. Длинные строки разделяются на части; сложные колонки, наклон и декоративные элементы могут нарушать порядок и полноту текста. Результат требует вычитки.</p></details><img id="original" alt="Выбранное изображение" hidden><p id="message" aria-live="polite">Выберите изображение со страницы книги или документа.</p></section>
|
| 9 |
+
<section class="card" id="results" hidden><div class="controls"><div class="tabs" role="tablist" aria-label="Режим просмотра результатов"><button type="button" class="tab active" id="tabLines2" role="tab" aria-selected="true">По строкам</button><button type="button" class="tab" id="tabPage2" role="tab" aria-selected="false">Страница и текст</button></div><div style="flex:1"></div><button id="copyText">Скопировать текст</button><button id="saveText">Сохранить TXT</button></div><p class="muted" id="summary"></p><div id="viewLines"><div class="row rowhead"><span>№</span><span>Оригинальная строка</span><span>Распознанный текст — можно исправить</span></div><div id="rows"></div></div><div id="viewPage" class="page-split" hidden><div class="page-pane"><div class="rowhead" style="margin-bottom:8px">Страница</div><img id="pageImage" alt="Исходная страница"></div><div class="text-pane"><div class="rowhead" style="margin-bottom:8px">Весь распознанный текст — можно исправить</div><textarea id="fullText" aria-label="Весь распознанный текст"></textarea></div></div></section><footer class="muted">TrOCR v3 · Без словарной автозамены · Закройте окно запуска, чтобы остановить OCR.</footer></main>
|
| 10 |
+
<script>
|
| 11 |
+
const $=id=>document.getElementById(id);let chosen=null,objectUrl=null,answer=null,mode='lines';
|
| 12 |
+
function setMode(next){mode=next;const isPage=mode==='page';['tabLines','tabLines2'].forEach(id=>{$(id).classList.toggle('active',!isPage);$(id).setAttribute('aria-selected',String(!isPage))});['tabPage','tabPage2'].forEach(id=>{$(id).classList.toggle('active',isPage);$(id).setAttribute('aria-selected',String(isPage))});$('viewLines').hidden=isPage;$('viewPage').hidden=!isPage}
|
| 13 |
+
$('tabLines').onclick=$('tabLines2').onclick=()=>setMode('lines');
|
| 14 |
+
$('tabPage').onclick=$('tabPage2').onclick=()=>setMode('page');
|
| 15 |
+
function syncFullFromRows(){$('fullText').value=[...$('rows').querySelectorAll('textarea')].map(x=>x.value).join('\n')}
|
| 16 |
+
$('fullText').addEventListener('input',()=>{const boxes=[...$('rows').querySelectorAll('textarea')];const parts=$('fullText').value.split('\n');if(parts.length===boxes.length)boxes.forEach((b,i)=>{b.value=parts[i]})});
|
| 17 |
+
fetch('/status').then(r=>r.json()).then(x=>{$('backend').textContent=x.backend==='gpu'?'NVIDIA GPU · FP16':'CPU · INT8'}).catch(()=>{$('backend').textContent='Сервер недоступен'});
|
| 18 |
+
$('file').addEventListener('change',()=>{chosen=$('file').files[0];$('recognize').disabled=!chosen;if(objectUrl)URL.revokeObjectURL(objectUrl);if(chosen){objectUrl=URL.createObjectURL(chosen);$('original').src=objectUrl;$('pageImage').src=objectUrl;$('original').hidden=false;$('results').hidden=true;$('message').textContent='Нажмите «Распознать».'}});
|
| 19 |
+
$('recognize').onclick=async()=>{if(!chosen)return;$('recognize').disabled=true;$('message').removeAttribute('role');$('message').textContent='Идёт распознавание. На CPU это может занять несколько минут.';$('results').hidden=true;try{const data=new FormData();data.append('file',chosen);const response=await fetch('/recognize',{method:'POST',body:data});if(!response.ok){const e=await response.json();throw new Error(e.detail||'Не удалось распознать изображение')}answer=await response.json();$('rows').replaceChildren();answer.lines.forEach((line,index)=>{const row=document.createElement('div');row.className='row';const n=document.createElement('span');n.className='num';n.textContent=index+1;const img=document.createElement('img');img.src=line.image;img.alt='Строка '+(index+1);const result=document.createElement('div');result.className='result';const edit=document.createElement('textarea');edit.value=line.text;edit.setAttribute('aria-label','Текст строки '+(index+1));edit.addEventListener('input',syncFullFromRows);result.append(edit);if(line.truncated){const note=document.createElement('div');note.className='warning';note.textContent='Достигнут предел длины: проверьте, не обрезана ли строка.';result.append(note)}row.append(n,img,result);$('rows').append(row)});syncFullFromRows();$('summary').textContent=`Строк: ${answer.lines.length}. Время: ${answer.seconds.toFixed(1)} с. ${answer.backend==='gpu'?'GPU FP16':'CPU INT8'}.`;$('message').textContent=answer.no_text_detected?'Текст не обнаружен. Проверьте, есть ли он на изображении; модель не распознаёт иллюстрацию как целую строку.':'Готово. Переключайте вкладки «По строкам» и «Страница и текст» для удобной сверки.';$('results').hidden=answer.no_text_detected;if(!answer.no_text_detected)$('original').hidden=true}catch(e){$('message').setAttribute('role','alert');$('message').textContent=e.message}finally{$('recognize').disabled=!chosen}};
|
| 20 |
+
function currentText(){return($('fullText').value||[...$('rows').querySelectorAll('textarea')].map(x=>x.value).join('\n'))}
|
| 21 |
+
function save(content,type,name){const url=URL.createObjectURL(new Blob([content],{type}));const a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)}
|
| 22 |
+
$('copyText').onclick=async()=>{await navigator.clipboard.writeText(currentText());const old=$('copyText').textContent;$('copyText').textContent='Скопировано!';setTimeout(()=>{$('copyText').textContent=old},1500)};
|
| 23 |
+
$('saveText').onclick=()=>save(currentText()+'\n','text/plain;charset=utf-8','ocr-result.txt');
|
| 24 |
+
</script></html>
|
| 25 |
+
|
| 26 |
+
|
runtime/line_merger.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
def merge_line_boxes(det_res, x_margin_ratio=2.5, y_overlap_thresh=0.45):
|
| 4 |
+
"""
|
| 5 |
+
Умное объединение разрезанных боксов одной строки (Horizontal Line Merging).
|
| 6 |
+
|
| 7 |
+
:param det_res: список кортежей (box, text, score) от PaddleOCR/RapidOCR
|
| 8 |
+
:param x_margin_ratio: максимальное расстояние между словами одной строки (в долях высоты)
|
| 9 |
+
:param y_overlap_thresh: минимальный коэффициент вертикального перекрытия для принадлежности к одной строке
|
| 10 |
+
"""
|
| 11 |
+
if det_res is None or len(det_res) == 0:
|
| 12 |
+
return []
|
| 13 |
+
|
| 14 |
+
# Преобразуем в структурированные прямоугольники [xmin, ymin, xmax, ymax]
|
| 15 |
+
rects = []
|
| 16 |
+
for item in det_res:
|
| 17 |
+
if isinstance(item, (list, tuple)) and len(item) > 0 and isinstance(item[0], (list, tuple, np.ndarray)):
|
| 18 |
+
box = np.array(item[0]).astype(int)
|
| 19 |
+
else:
|
| 20 |
+
box = np.array(item).astype(int)
|
| 21 |
+
|
| 22 |
+
if box.ndim == 2 and box.shape[0] >= 4:
|
| 23 |
+
xmin = int(np.min(box[:, 0]))
|
| 24 |
+
xmax = int(np.max(box[:, 0]))
|
| 25 |
+
ymin = int(np.min(box[:, 1]))
|
| 26 |
+
ymax = int(np.max(box[:, 1]))
|
| 27 |
+
elif len(box) == 4:
|
| 28 |
+
xmin, ymin, xmax, ymax = int(box[0]), int(box[1]), int(box[2]), int(box[3])
|
| 29 |
+
else:
|
| 30 |
+
continue
|
| 31 |
+
|
| 32 |
+
h = max(1, ymax - ymin)
|
| 33 |
+
w = max(1, xmax - xmin)
|
| 34 |
+
rects.append({
|
| 35 |
+
"box": [xmin, ymin, xmax, ymax],
|
| 36 |
+
"xmin": xmin, "xmax": xmax,
|
| 37 |
+
"ymin": ymin, "ymax": ymax,
|
| 38 |
+
"h": h, "w": w,
|
| 39 |
+
"cy": (ymin + ymax) / 2.0
|
| 40 |
+
})
|
| 41 |
+
|
| 42 |
+
if not rects:
|
| 43 |
+
return []
|
| 44 |
+
|
| 45 |
+
# Сортируем сначала по вертикали Y
|
| 46 |
+
rects.sort(key=lambda r: (r["ymin"], r["xmin"]))
|
| 47 |
+
|
| 48 |
+
# Группируем боксы по строкам с проверкой близости вертикальных центров
|
| 49 |
+
lines = []
|
| 50 |
+
for r in rects:
|
| 51 |
+
placed = False
|
| 52 |
+
for line in lines:
|
| 53 |
+
line_ymin = min(x["ymin"] for x in line)
|
| 54 |
+
line_ymax = max(x["ymax"] for x in line)
|
| 55 |
+
line_cy = (line_ymin + line_ymax) / 2.0
|
| 56 |
+
line_h = max(1, line_ymax - line_ymin)
|
| 57 |
+
|
| 58 |
+
# Проверяем расстояние между центрами строк
|
| 59 |
+
center_diff = abs(r["cy"] - line_cy)
|
| 60 |
+
if center_diff <= min(r["h"], line_h) * 0.45:
|
| 61 |
+
line.append(r)
|
| 62 |
+
placed = True
|
| 63 |
+
break
|
| 64 |
+
if not placed:
|
| 65 |
+
lines.append([r])
|
| 66 |
+
|
| 67 |
+
# Внутри каждой строки сортируем слева направо и объединяем
|
| 68 |
+
merged_boxes = []
|
| 69 |
+
for line in lines:
|
| 70 |
+
line.sort(key=lambda r: r["xmin"])
|
| 71 |
+
|
| 72 |
+
cur_xmin = line[0]["xmin"]
|
| 73 |
+
cur_xmax = line[0]["xmax"]
|
| 74 |
+
cur_ymin = line[0]["ymin"]
|
| 75 |
+
cur_ymax = line[0]["ymax"]
|
| 76 |
+
|
| 77 |
+
for next_r in line[1:]:
|
| 78 |
+
avg_h = ((cur_ymax - cur_ymin) + next_r["h"]) / 2.0
|
| 79 |
+
dist_x = next_r["xmin"] - cur_xmax
|
| 80 |
+
|
| 81 |
+
# Если боксы достаточно близко по горизонтали — объединяем в один
|
| 82 |
+
if dist_x <= avg_h * x_margin_ratio:
|
| 83 |
+
cur_xmax = max(cur_xmax, next_r["xmax"])
|
| 84 |
+
cur_ymin = min(cur_ymin, next_r["ymin"])
|
| 85 |
+
cur_ymax = max(cur_ymax, next_r["ymax"])
|
| 86 |
+
else:
|
| 87 |
+
# Слишком далеко (например, две колонки текста) — сохраняем отдельно
|
| 88 |
+
merged_boxes.append([cur_xmin, cur_ymin, cur_xmax, cur_ymax])
|
| 89 |
+
cur_xmin = next_r["xmin"]
|
| 90 |
+
cur_xmax = next_r["xmax"]
|
| 91 |
+
cur_ymin = next_r["ymin"]
|
| 92 |
+
cur_ymax = next_r["ymax"]
|
| 93 |
+
|
| 94 |
+
merged_boxes.append([cur_xmin, cur_ymin, cur_xmax, cur_ymax])
|
| 95 |
+
|
| 96 |
+
# Сортируем итоговые строки строго сверху вниз
|
| 97 |
+
merged_boxes.sort(key=lambda b: (b[1], b[0]))
|
| 98 |
+
return merged_boxes
|
start_ocr.bat
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
setlocal
|
| 3 |
+
cd /d "%~dp0"
|
| 4 |
+
title Bashkir OCR Studio v3
|
| 5 |
+
echo [1] CPU - ONNX INT8
|
| 6 |
+
echo [2] NVIDIA GPU - ONNX FP16
|
| 7 |
+
echo [3] Auto - detect NVIDIA GPU (FP16) or fall back to CPU (INT8)
|
| 8 |
+
set "OCR_MODE="
|
| 9 |
+
set /p "OCR_MODE=Choose mode [1/2/3, default 3]: "
|
| 10 |
+
if "%OCR_MODE%"=="" set "OCR_MODE=3"
|
| 11 |
+
set "OCR_BACKEND=cpu"
|
| 12 |
+
set "OCR_RUN_BACKEND=cpu"
|
| 13 |
+
if "%OCR_MODE%"=="2" (
|
| 14 |
+
set "OCR_BACKEND=gpu"
|
| 15 |
+
set "OCR_RUN_BACKEND=gpu"
|
| 16 |
+
)
|
| 17 |
+
if "%OCR_MODE%"=="3" (
|
| 18 |
+
set "OCR_RUN_BACKEND=auto"
|
| 19 |
+
if exist ".venv-gpu\.ready" (
|
| 20 |
+
set "OCR_BACKEND=gpu"
|
| 21 |
+
) else (
|
| 22 |
+
nvidia-smi >nul 2>&1
|
| 23 |
+
if not errorlevel 1 set "OCR_BACKEND=gpu"
|
| 24 |
+
)
|
| 25 |
+
)
|
| 26 |
+
set "OCR_ENV=.venv-%OCR_BACKEND%"
|
| 27 |
+
if exist "%OCR_ENV%\.ready" goto LAUNCH
|
| 28 |
+
echo First start (%OCR_BACKEND%): Python 3.11-3.13 and internet access are needed to install dependencies.
|
| 29 |
+
python --version >nul 2>&1
|
| 30 |
+
if errorlevel 1 (
|
| 31 |
+
echo Install Python from python.org and enable Add Python to PATH.
|
| 32 |
+
goto FINISH
|
| 33 |
+
)
|
| 34 |
+
if not exist "%OCR_ENV%\Scripts\python.exe" python -m venv "%OCR_ENV%"
|
| 35 |
+
if errorlevel 1 goto FINISH
|
| 36 |
+
"%OCR_ENV%\Scripts\python.exe" -m pip install -r "requirements-%OCR_BACKEND%.txt"
|
| 37 |
+
if errorlevel 1 (
|
| 38 |
+
echo Installation failed. Check your connection, then run install again:
|
| 39 |
+
echo %OCR_ENV%\Scripts\python.exe -m pip install -r requirements-%OCR_BACKEND%.txt
|
| 40 |
+
goto FINISH
|
| 41 |
+
)
|
| 42 |
+
type nul > "%OCR_ENV%\.ready"
|
| 43 |
+
:LAUNCH
|
| 44 |
+
"%OCR_ENV%\Scripts\python.exe" studio.py --backend %OCR_RUN_BACKEND%
|
| 45 |
+
:FINISH
|
| 46 |
+
pause
|
| 47 |
+
|
studio.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Local browser UI. Uploaded images live only in request memory."""
|
| 2 |
+
import argparse
|
| 3 |
+
import io
|
| 4 |
+
import threading
|
| 5 |
+
import time
|
| 6 |
+
import webbrowser
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
from fastapi import FastAPI, HTTPException, UploadFile
|
| 10 |
+
from fastapi.responses import FileResponse
|
| 11 |
+
from PIL import Image, UnidentifiedImageError
|
| 12 |
+
from starlette.concurrency import run_in_threadpool
|
| 13 |
+
from runtime.engine import OCR
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def create_app(ocr):
|
| 17 |
+
app=FastAPI(title='Bashkir OCR Studio',docs_url=None,redoc_url=None)
|
| 18 |
+
lock=threading.Lock()
|
| 19 |
+
@app.get('/')
|
| 20 |
+
def index():return FileResponse(Path(__file__).parent/'runtime/index.html',headers={'Cache-Control':'no-store, no-cache, must-revalidate','Pragma':'no-cache','Expires':'0'})
|
| 21 |
+
@app.get('/status')
|
| 22 |
+
def status():return dict(ready=True,backend=ocr.backend)
|
| 23 |
+
@app.post('/recognize')
|
| 24 |
+
async def recognize(file:UploadFile):
|
| 25 |
+
data=await file.read(25*1024*1024+1)
|
| 26 |
+
if len(data)>25*1024*1024:raise HTTPException(413,'Изображение слишком большое: предел 25 МБ.')
|
| 27 |
+
try:
|
| 28 |
+
image=Image.open(io.BytesIO(data))
|
| 29 |
+
if image.width*image.height>25_000_000:raise HTTPException(413,'Предел — 25 миллионов пикселей.')
|
| 30 |
+
image.load()
|
| 31 |
+
except (UnidentifiedImageError,OSError,Image.DecompressionBombError):raise HTTPException(400,'Не удалось открыть изображение.')
|
| 32 |
+
if not lock.acquire(blocking=False):raise HTTPException(409,'Дождитесь завершения текущего изображения.')
|
| 33 |
+
try:return await run_in_threadpool(ocr.recognize,image)
|
| 34 |
+
finally:lock.release()
|
| 35 |
+
return app
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def main():
|
| 39 |
+
parser=argparse.ArgumentParser(description=__doc__)
|
| 40 |
+
parser.add_argument('--backend',choices=['cpu','gpu','auto'],default='cpu')
|
| 41 |
+
parser.add_argument('--port',type=int,default=8001)
|
| 42 |
+
parser.add_argument('--no-browser',action='store_true')
|
| 43 |
+
parser.add_argument('--model-dir',type=Path,help='Optional directory containing config.json and model files')
|
| 44 |
+
args=parser.parse_args()
|
| 45 |
+
ocr=OCR(root=args.model_dir,backend=args.backend)
|
| 46 |
+
if not args.no_browser:
|
| 47 |
+
def open_ready():
|
| 48 |
+
import urllib.request
|
| 49 |
+
for _ in range(60):
|
| 50 |
+
try:
|
| 51 |
+
with urllib.request.urlopen(f'http://127.0.0.1:{args.port}/status',timeout=1) as r:
|
| 52 |
+
if r.status==200:webbrowser.open(f'http://127.0.0.1:{args.port}/?v={int(time.time())}');return
|
| 53 |
+
except OSError:time.sleep(.5)
|
| 54 |
+
threading.Thread(target=open_ready,daemon=True).start()
|
| 55 |
+
import uvicorn
|
| 56 |
+
uvicorn.run(create_app(ocr),host='127.0.0.1',port=args.port,access_log=False)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__=='__main__':main()
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"add_prefix_space": false, "added_tokens_decoder": {"0": {"content": "<s>", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": true}, "1": {"content": "<pad>", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": true}, "2": {"content": "</s>", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": true}, "3": {"content": "<unk>", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": true}, "50264": {"content": "<mask>", "lstrip": true, "normalized": true, "rstrip": false, "single_word": false, "special": true}, "50265": {"content": "Ғ", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50266": {"content": "ғ", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50267": {"content": "Ҙ", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50268": {"content": "ҙ", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50269": {"content": "Ҡ", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50270": {"content": "ҡ", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50271": {"content": "Ң", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50272": {"content": "ң", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50273": {"content": "Ө", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50274": {"content": "ө", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50275": {"content": "Ҫ", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50276": {"content": "ҫ", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50277": {"content": "Ү", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50278": {"content": "ү", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50279": {"content": "Һ", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50280": {"content": "һ", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50281": {"content": "Ә", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}, "50282": {"content": "ә", "lstrip": false, "normalized": true, "rstrip": false, "single_word": false, "special": false}}, "backend": "tokenizers", "bos_token": "<s>", "clean_up_tokenization_spaces": false, "cls_token": "<s>", "eos_token": "</s>", "errors": "replace", "is_local": false, "local_files_only": false, "mask_token": "<mask>", "model_max_length": 512, "pad_token": "<pad>", "processor_class": "TrOCRProcessor", "sep_token": "</s>", "tokenizer_class": "RobertaTokenizer", "trim_offsets": true, "unk_token": "<unk>"}
|