diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8da8c24..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -model \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a724ee7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Darklighture + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 27286bd..0000000 --- a/Pipfile +++ /dev/null @@ -1,16 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -googletrans = "*" -pillow = "*" -easyocr = "*" -deep-translator = "*" -black = "*" - -[dev-packages] - -[requires] -python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 2605fab..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,943 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "9ffe7377bad955208aed949ff554deae938ffe9207d17c301763c3253c3b07e1" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "beautifulsoup4": { - "hashes": [ - "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", - "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==4.12.3" - }, - "black": { - "hashes": [ - "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", - "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", - "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", - "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0", - "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9", - "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", - "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213", - "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d", - "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7", - "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837", - "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f", - "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395", - "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995", - "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", - "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597", - "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959", - "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5", - "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb", - "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", - "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7", - "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd", - "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==24.3.0" - }, - "certifi": { - "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" - ], - "markers": "python_version >= '3.6'", - "version": "==2024.2.2" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "charset-normalizer": { - "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" - }, - "click": { - "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.7" - }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "markers": "platform_system == 'Windows'", - "version": "==0.4.6" - }, - "deep-translator": { - "hashes": [ - "sha256:801260c69231138707ea88a0955e484db7d40e210c9e0ae0f77372ffda5f4bf5", - "sha256:d635df037e23fa35d12fd42dab72a0b55c9dd19e6292009ee7207e3f30b9e60a" - ], - "index": "pypi", - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==1.11.4" - }, - "easyocr": { - "hashes": [ - "sha256:5b0a2e7cfdfc6c1ec99d9583663e570e4189dca6fbf373f074b21b8809e44d2b" - ], - "index": "pypi", - "version": "==1.7.1" - }, - "filelock": { - "hashes": [ - "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e", - "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c" - ], - "markers": "python_version >= '3.8'", - "version": "==3.13.1" - }, - "fsspec": { - "extras": [ - "http" - ], - "hashes": [ - "sha256:918d18d41bf73f0e2b261824baeb1b124bcf771767e3a26425cd7dec3332f512", - "sha256:f39780e282d7d117ffb42bb96992f8a90795e4d0fb0f661a70ca39fe9c43ded9" - ], - "markers": "python_version >= '3.8'", - "version": "==2024.3.1" - }, - "googletrans": { - "hashes": [ - "sha256:74df47b092e2d566522019d149e3f1d75732570ad76eaf8e14aebeffc126c372" - ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==4.0.0rc1" - }, - "h11": { - "hashes": [ - "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1", - "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1" - ], - "version": "==0.9.0" - }, - "h2": { - "hashes": [ - "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5", - "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14" - ], - "version": "==3.2.0" - }, - "hpack": { - "hashes": [ - "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89", - "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2" - ], - "version": "==3.0.0" - }, - "hstspreload": { - "hashes": [ - "sha256:14c4dd20f352a5ef42320740700d364f841c8a3b2026d77e3e6f16f8c1ad2afb", - "sha256:bd192b68d69683b0390816946c0fd705a9ba6d5a4c2cd34b8433a53141a378ea" - ], - "markers": "python_version >= '3.6'", - "version": "==2024.3.1" - }, - "httpcore": { - "hashes": [ - "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0", - "sha256:ecc5949310d9dae4de64648a4ce529f86df1f232ce23dcfefe737c24d21dfbe9" - ], - "markers": "python_version >= '3.6'", - "version": "==0.9.1" - }, - "httpx": { - "hashes": [ - "sha256:32d930858eab677bc29a742aaa4f096de259f1c78c68a90ad11f5c3c04f08335", - "sha256:3642bd13e90b80ba8a243a730275eb10a4c26ec96f5fc16b87e458d4ab21efae" - ], - "markers": "python_version >= '3.6'", - "version": "==0.13.3" - }, - "hyperframe": { - "hashes": [ - "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40", - "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f" - ], - "version": "==5.2.0" - }, - "idna": { - "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" - }, - "imageio": { - "hashes": [ - "sha256:08082bf47ccb54843d9c73fe9fc8f3a88c72452ab676b58aca74f36167e8ccba", - "sha256:ae9732e10acf807a22c389aef193f42215718e16bd06eed0c5bb57e1034a4d53" - ], - "markers": "python_version >= '3.8'", - "version": "==2.34.0" - }, - "jinja2": { - "hashes": [ - "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", - "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.3" - }, - "lazy-loader": { - "hashes": [ - "sha256:1e9e76ee8631e264c62ce10006718e80b2cfc74340d17d1031e0f84af7478554", - "sha256:3b68898e34f5b2a29daaaac172c6555512d0f32074f147e2254e4a6d9d838f37" - ], - "markers": "python_version >= '3.7'", - "version": "==0.3" - }, - "markupsafe": { - "hashes": [ - "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", - "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", - "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", - "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", - "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", - "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", - "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", - "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", - "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", - "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", - "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", - "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", - "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", - "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", - "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", - "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", - "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", - "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", - "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", - "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", - "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", - "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", - "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", - "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", - "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", - "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", - "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", - "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", - "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", - "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", - "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", - "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", - "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", - "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", - "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", - "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", - "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", - "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", - "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", - "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", - "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", - "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", - "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", - "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", - "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", - "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", - "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", - "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", - "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", - "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", - "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", - "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", - "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", - "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", - "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", - "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", - "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", - "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", - "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", - "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.5" - }, - "mpmath": { - "hashes": [ - "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", - "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c" - ], - "version": "==1.3.0" - }, - "mypy-extensions": { - "hashes": [ - "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", - "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.0" - }, - "networkx": { - "hashes": [ - "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6", - "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2" - ], - "markers": "python_version >= '3.9'", - "version": "==3.2.1" - }, - "ninja": { - "hashes": [ - "sha256:18302d96a5467ea98b68e1cae1ae4b4fb2b2a56a82b955193c637557c7273dbd", - "sha256:185e0641bde601e53841525c4196278e9aaf4463758da6dd1e752c0a0f54136a", - "sha256:376889c76d87b95b5719fdd61dd7db193aa7fd4432e5d52d2e44e4c497bdbbee", - "sha256:3e0f9be5bb20d74d58c66cc1c414c3e6aeb45c35b0d0e41e8d739c2c0d57784f", - "sha256:73b93c14046447c7c5cc892433d4fae65d6364bec6685411cb97a8bcf815f93a", - "sha256:7563ce1d9fe6ed5af0b8dd9ab4a214bf4ff1f2f6fd6dc29f480981f0f8b8b249", - "sha256:76482ba746a2618eecf89d5253c0d1e4f1da1270d41e9f54dfbd91831b0f6885", - "sha256:84502ec98f02a037a169c4b0d5d86075eaf6afc55e1879003d6cab51ced2ea4b", - "sha256:95da904130bfa02ea74ff9c0116b4ad266174fafb1c707aa50212bc7859aebf1", - "sha256:9d793b08dd857e38d0b6ffe9e6b7145d7c485a42dcfea04905ca0cdb6017cc3c", - "sha256:9df724344202b83018abb45cb1efc22efd337a1496514e7e6b3b59655be85205", - "sha256:aad34a70ef15b12519946c5633344bc775a7656d789d9ed5fdb0d456383716ef", - "sha256:d491fc8d89cdcb416107c349ad1e3a735d4c4af5e1cb8f5f727baca6350fdaea", - "sha256:ecf80cf5afd09f14dcceff28cb3f11dc90fb97c999c89307aea435889cb66877", - "sha256:fa2ba9d74acfdfbfbcf06fad1b8282de8a7a8c481d9dee45c859a8c93fcc1082" - ], - "version": "==1.11.1.1" - }, - "numpy": { - "hashes": [ - "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", - "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", - "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", - "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", - "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", - "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", - "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea", - "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c", - "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", - "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", - "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be", - "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", - "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", - "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", - "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", - "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd", - "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c", - "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", - "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0", - "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c", - "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", - "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", - "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", - "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6", - "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", - "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", - "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30", - "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", - "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", - "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", - "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", - "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", - "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764", - "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", - "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", - "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f" - ], - "markers": "python_version >= '3.9'", - "version": "==1.26.4" - }, - "opencv-python-headless": { - "hashes": [ - "sha256:11e3849d83e6651d4e7699aadda9ec7ed7c38957cbbcb99db074f2a2d2de9670", - "sha256:2ea8a2edc4db87841991b2fbab55fc07b97ecb602e0f47d5d485bd75cee17c1a", - "sha256:57ce2865e8fec431c6f97a81e9faaf23fa5be61011d0a75ccf47a3c0d65fa73d", - "sha256:71a4cd8cf7c37122901d8e81295db7fb188730e33a0e40039a4e59c1030b0958", - "sha256:976656362d68d9f40a5c66f83901430538002465f7db59142784f3893918f3df", - "sha256:a8056c2cb37cd65dfcdf4153ca16f7362afcf3a50d600d6bb69c660fc61ee29c", - "sha256:e0ee54e27be493e8f7850847edae3128e18b540dac1d7b2e4001b8944e11e1c6" - ], - "markers": "python_version >= '3.6'", - "version": "==4.9.0.80" - }, - "packaging": { - "hashes": [ - "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" - ], - "markers": "python_version >= '3.7'", - "version": "==24.0" - }, - "pathspec": { - "hashes": [ - "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", - "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" - ], - "markers": "python_version >= '3.8'", - "version": "==0.12.1" - }, - "pillow": { - "hashes": [ - "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8", - "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39", - "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac", - "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869", - "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e", - "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04", - "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9", - "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e", - "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe", - "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef", - "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56", - "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa", - "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f", - "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f", - "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e", - "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a", - "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2", - "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2", - "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5", - "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a", - "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2", - "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213", - "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563", - "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591", - "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c", - "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2", - "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb", - "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757", - "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0", - "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452", - "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad", - "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01", - "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f", - "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5", - "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61", - "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e", - "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b", - "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068", - "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9", - "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588", - "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483", - "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f", - "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67", - "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7", - "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311", - "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6", - "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72", - "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6", - "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129", - "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13", - "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67", - "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c", - "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516", - "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e", - "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e", - "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364", - "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023", - "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1", - "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04", - "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d", - "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a", - "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7", - "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb", - "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4", - "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e", - "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1", - "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48", - "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==10.2.0" - }, - "platformdirs": { - "hashes": [ - "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", - "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" - ], - "markers": "python_version >= '3.8'", - "version": "==4.2.0" - }, - "pyclipper": { - "hashes": [ - "sha256:010ee13d40d924341cc41b6d9901d763175040c68753939f140bc0cc714f18bb", - "sha256:02a98d09af9b60bcf8e9480d153c0839e20b92689f5602f87242a4933842fecd", - "sha256:0589b80f2da1ad322345a93c053b5d46dc692def5a188351be01f34bcf041218", - "sha256:0f516fd69aa61a9698a3ce3ba2f7edda5ac6aafc8d964ee3bc60897906947fcb", - "sha256:0f78a1c18ff4f9276f78d9353d6ed4309c3886a9d0172437e48328aef499165e", - "sha256:1158a2b13d59bdfab33d1d928f7b72c8c7fb8a76e7d2283839cb45d7c0ff2140", - "sha256:2ce6e0a6ab32182c26537965cf521822cd11a28a7ffcef48635a94c6ca8559ef", - "sha256:36d456fdf32a6410a87bd7af8ebc4c01f19b4e3b839104b3072558cad0d8bf4c", - "sha256:39ccd920b192a4f8096589a2a1f8faaf6aaaadb7a163b5ce913d03faac2449bb", - "sha256:3c45f99b8180dd4df4c86642657ca92b7d5289a5e3724521822e0f9461961fe2", - "sha256:46499b361ae067662b22578401d83d57716f3cc0071d592feb07d504b439fea7", - "sha256:5237282f906049c307e6c90333c7d56f6b8712bf087ef97b141830c40b09ca0a", - "sha256:567ffd419a0bdc3727fa4562cfa1f18484691817a2bc0bc675750aa28ed98bd4", - "sha256:59c8c75661a6d87e98b1655851578a2917d3c8859912c9a4f1956b9830940fd9", - "sha256:5a041f1a7982b17cf92fd3be349ec41ff1901792149c166bf283f469567b52d6", - "sha256:5f3484b4dffa64f0e3a43b63165a5c0f507c5850e70b9cc2eaa82474d7746393", - "sha256:5f445a2d03690faa23a1b90e32dfb4352a60b23437323de87388c6c611d3d1e3", - "sha256:741910bfd7b0bd40f027869f4bf86bdd9678ae7f74e8dabcf62d170269f6191d", - "sha256:847f1e2fc3994bb498fe675f55c98129b95dc26a5c92304ba4cf0ab40721ea3d", - "sha256:85ca06f382f999903d809380e4c01ec127d3eb26431402e9b3f01facaec68b80", - "sha256:87efec9795744cef786f2f8cab17d6dc07f57dfce5e3b7f3be96eb79a4ce5794", - "sha256:8bb9cd95fd4bd88fb1590d1763a52e3ea6a1095e11b3e885ff164da1313aae79", - "sha256:a496efa146d2d88b59350021739e4685e439dc569b6654e9e6d5e42e9a0b1666", - "sha256:a678999d728023f1f3988a14a2e6d89d6f1ed4d0786d5992c1bffb4c1ab30318", - "sha256:aca8635573646b65c054399433fb3493637f1445db942de8a52fca9ef493ba3d", - "sha256:b7a983ae019932bfa0a1971a2dc8c856704add5f3d567bed8fac02dbc0e7f0bf", - "sha256:ba692cf11873886085a0445dcfc362b24ca35bcb997ad9e9b5685854a290d8ff", - "sha256:bb2fb22927c3ac3191e555efd335c6efa819aa1ff4d0901979673ab5a18eb740", - "sha256:bf3a2ccd6e4e078250b0a31a12c519b0be6d1bc160acfceee62407dbd68558f6", - "sha256:c0239f928e0bf78a3efc2f2f615a10bfcdb9f33012d46d64c8d1225b4bde7096", - "sha256:c9c1fdf4ecae6b55033ede3f4e931156ffc969334300f44f8bf1b356ec0a3d63", - "sha256:cfea42972e90954b3c89da9216993373a2270a5103d4916fd543a1109528ed4c", - "sha256:d5c77e39ab05a6cf277c819639968b21e6959e996ea1a074afc24236541708ff", - "sha256:d8760075c395b924f894aa16ee06e8c040c6f9b63e0903e49de3cc8d82d9e637", - "sha256:d8a9e3e46aa50e4c3667db9a816d59ae4f9c62b05f997abb8a9b3f3afe6d94a4", - "sha256:da30e59c684eea198f6e19244e9a41e855a23a416cc708821fd4eb8f5f18626c", - "sha256:dd3c4b312a931e668a7a291d4bd5b10bacb0687bd163220a9f0418c7e23169e2", - "sha256:e346e7adba43e40f5f5f293b6b6a45de5a6a3bdc74e437dedd948c5d74de9405", - "sha256:e36f018303656ea4a629d2fba0d0d4c74960eacec7119fe2ab3c658ce84c494b", - "sha256:e4ea61ca5899d3346c614951342c506f119601ed0a1f4889a9cc236558afec6b", - "sha256:ead0f3ecd1961005f61d50c896e33442138b4e7c9e0c035784d3525068dd2b10", - "sha256:eb9d1cb2999bc1ea8ad1c3a031ba33b0a89a5ace25d33df7529d3ff18c16604c", - "sha256:ee1c4797b1dc982ae9d60333269536ea03ddc0baa1c3383a6d5b741dbbb12675", - "sha256:f0b84fcf5230aca2de06ddb7920459daa858853835f8774739ca30dd516e7d37" - ], - "version": "==1.3.0.post5" - }, - "pytesseract": { - "hashes": [ - "sha256:8f22cc98f765bf13517ead0c70effedb46c153540d25783e04014f28b55a5fc6", - "sha256:f1c3a8b0f07fd01a1085d451f5b8315be6eec1d5577a6796d46dc7a62bd4120f" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==0.3.10" - }, - "python-bidi": { - "hashes": [ - "sha256:50eef6f6a0bbdd685f9e8c207f3c9050f5b578d0a46e37c76a9c4baea2cc2e13", - "sha256:5347f71e82b3e9976dc657f09ded2bfe39ba8d6777ca81a5b2c56c30121c496e" - ], - "version": "==0.4.2" - }, - "pyyaml": { - "hashes": [ - "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", - "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0.1" - }, - "requests": { - "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" - ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" - }, - "rfc3986": { - "hashes": [ - "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", - "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" - ], - "version": "==1.5.0" - }, - "scikit-image": { - "hashes": [ - "sha256:003ca2274ac0fac252280e7179ff986ff783407001459ddea443fe7916e38cff", - "sha256:018d734df1d2da2719087d15f679d19285fce97cd37695103deadfaef2873236", - "sha256:22318b35044cfeeb63ee60c56fc62450e5fe516228138f1d06c7a26378248a86", - "sha256:2bcb74adb0634258a67f66c2bb29978c9a3e222463e003b67ba12056c003971b", - "sha256:2c6ef454a85f569659b813ac2a93948022b0298516b757c9c6c904132be327e2", - "sha256:3663d063d8bf2fb9bdfb0ca967b9ee3b6593139c860c7abc2d2351a8a8863938", - "sha256:3b7a6c89e8d6252332121b58f50e1625c35f7d6a85489c0b6b7ee4f5155d547a", - "sha256:5071b8f6341bfb0737ab05c8ab4ac0261f9e25dbcc7b5d31e5ed230fd24a7929", - "sha256:6a92dca3d95b1301442af055e196a54b5a5128c6768b79fc0a4098f1d662dee6", - "sha256:722b970aa5da725dca55252c373b18bbea7858c1cdb406e19f9b01a4a73b30b2", - "sha256:74ec5c1d4693506842cc7c9487c89d8fc32aed064e9363def7af08b8f8cbb31d", - "sha256:95d6da2d8a44a36ae04437c76d32deb4e3c993ffc846b394b9949fd8ded73cb2", - "sha256:9e801c44a814afdadeabf4dffdffc23733e393767958b82319706f5fa3e1eaa9", - "sha256:a05ae4fe03d802587ed8974e900b943275548cde6a6807b785039d63e9a7a5ff", - "sha256:be79d7493f320a964f8fcf603121595ba82f84720de999db0fcca002266a549a", - "sha256:c472a1fb3665ec5c00423684590631d95f9afcbc97f01407d348b821880b2cb3", - "sha256:c5c378db54e61b491b9edeefff87e49fcf7fdf729bb93c777d7a5f15d36f743e", - "sha256:cf3c0c15b60ae3e557a0c7575fbd352f0c3ce0afca562febfe3ab80efbeec0e9", - "sha256:e87872f067444ee90a00dd49ca897208308645382e8a24bd3e76f301af2352cd", - "sha256:ebdbdc901bae14dab637f8d5c99f6d5cc7aaf4a3b6f4003194e003e9f688a6fc", - "sha256:f5b23908dd4d120e6aecb1ed0277563e8cbc8d6c0565bdc4c4c6475d53608452" - ], - "markers": "python_version >= '3.9'", - "version": "==0.22.0" - }, - "scipy": { - "hashes": [ - "sha256:196ebad3a4882081f62a5bf4aeb7326aa34b110e533aab23e4374fcccb0890dc", - "sha256:408c68423f9de16cb9e602528be4ce0d6312b05001f3de61fe9ec8b1263cad08", - "sha256:4bf5abab8a36d20193c698b0f1fc282c1d083c94723902c447e5d2f1780936a3", - "sha256:4c1020cad92772bf44b8e4cdabc1df5d87376cb219742549ef69fc9fd86282dd", - "sha256:5adfad5dbf0163397beb4aca679187d24aec085343755fcdbdeb32b3679f254c", - "sha256:5e32847e08da8d895ce09d108a494d9eb78974cf6de23063f93306a3e419960c", - "sha256:6546dc2c11a9df6926afcbdd8a3edec28566e4e785b915e849348c6dd9f3f490", - "sha256:730badef9b827b368f351eacae2e82da414e13cf8bd5051b4bdfd720271a5371", - "sha256:75ea2a144096b5e39402e2ff53a36fecfd3b960d786b7efd3c180e29c39e53f2", - "sha256:78e4402e140879387187f7f25d91cc592b3501a2e51dfb320f48dfb73565f10b", - "sha256:8b8066bce124ee5531d12a74b617d9ac0ea59245246410e19bca549656d9a40a", - "sha256:8bee4993817e204d761dba10dbab0774ba5a8612e57e81319ea04d84945375ba", - "sha256:913d6e7956c3a671de3b05ccb66b11bc293f56bfdef040583a7221d9e22a2e35", - "sha256:95e5c750d55cf518c398a8240571b0e0782c2d5a703250872f36eaf737751338", - "sha256:9c39f92041f490422924dfdb782527a4abddf4707616e07b021de33467f917bc", - "sha256:a24024d45ce9a675c1fb8494e8e5244efea1c7a09c60beb1eeb80373d0fecc70", - "sha256:a7ebda398f86e56178c2fa94cad15bf457a218a54a35c2a7b4490b9f9cb2676c", - "sha256:b360f1b6b2f742781299514e99ff560d1fe9bd1bff2712894b52abe528d1fd1e", - "sha256:bba1b0c7256ad75401c73e4b3cf09d1f176e9bd4248f0d3112170fb2ec4db067", - "sha256:c3003652496f6e7c387b1cf63f4bb720951cfa18907e998ea551e6de51a04467", - "sha256:e53958531a7c695ff66c2e7bb7b79560ffdc562e2051644c5576c39ff8efb563", - "sha256:e646d8571804a304e1da01040d21577685ce8e2db08ac58e543eaca063453e1c", - "sha256:e7e76cc48638228212c747ada851ef355c2bb5e7f939e10952bc504c11f4e372", - "sha256:f5f00ebaf8de24d14b8449981a2842d404152774c1a1d880c901bf454cb8e2a1", - "sha256:f7ce148dffcd64ade37b2df9315541f9adad6efcaa86866ee7dd5db0c8f041c3" - ], - "markers": "python_version >= '3.9'", - "version": "==1.12.0" - }, - "shapely": { - "hashes": [ - "sha256:083d026e97b6c1f4a9bd2a9171c7692461092ed5375218170d91705550eecfd5", - "sha256:13cb37d3826972a82748a450328fe02a931dcaed10e69a4d83cc20ba021bc85f", - "sha256:18bddb8c327f392189a8d5d6b9a858945722d0bb95ccbd6a077b8e8fc4c7890d", - "sha256:27b6e1910094d93e9627f2664121e0e35613262fc037051680a08270f6058daf", - "sha256:300d203b480a4589adefff4c4af0b13919cd6d760ba3cbb1e56275210f96f654", - "sha256:31a40b6e3ab00a4fd3a1d44efb2482278642572b8e0451abdc8e0634b787173e", - "sha256:42bceb9bceb3710a774ce04908fda0f28b291323da2688f928b3f213373b5aee", - "sha256:442f4dcf1eb58c5a4e3428d88e988ae153f97ab69a9f24e07bf4af8038536325", - "sha256:4d279e56bbb68d218d63f3efc80c819cedcceef0e64efbf058a1df89dc57201b", - "sha256:4d65d0aa7910af71efa72fd6447e02a8e5dd44da81a983de9d736d6e6ccbe674", - "sha256:5026b30433a70911979d390009261b8c4021ff87c7c3cbd825e62bb2ffa181bc", - "sha256:54d925c9a311e4d109ec25f6a54a8bd92cc03481a34ae1a6a92c1fe6729b7e01", - "sha256:56cee3e4e8159d6f2ce32e421445b8e23154fd02a0ac271d6a6c0b266a8e3cce", - "sha256:58afbba12c42c6ed44c4270bc0e22f3dadff5656d711b0ad335c315e02d04707", - "sha256:59b16976c2473fec85ce65cc9239bef97d4205ab3acead4e6cdcc72aee535679", - "sha256:601c5c0058a6192df704cb889439f64994708563f57f99574798721e9777a44b", - "sha256:619232c8276fded09527d2a9fd91a7885ff95c0ff9ecd5e3cb1e34fbb676e2ae", - "sha256:64c5013dacd2d81b3bb12672098a0b2795c1bf8190cfc2980e380f5ef9d9e4d9", - "sha256:6b464f2666b13902835f201f50e835f2f153f37741db88f68c7f3b932d3505fa", - "sha256:6dfdc077a6fcaf74d3eab23a1ace5abc50c8bce56ac7747d25eab582c5a2990e", - "sha256:6f555fe3304a1f40398977789bc4fe3c28a11173196df9ece1e15c5bc75a48db", - "sha256:705efbce1950a31a55b1daa9c6ae1c34f1296de71ca8427974ec2f27d57554e3", - "sha256:71b2de56a9e8c0e5920ae5ddb23b923490557ac50cb0b7fa752761bf4851acde", - "sha256:71eb736ef2843f23473c6e37f6180f90f0a35d740ab284321548edf4e55d9a52", - "sha256:881eb9dbbb4a6419667e91fcb20313bfc1e67f53dbb392c6840ff04793571ed1", - "sha256:882fb1ffc7577e88c1194f4f1757e277dc484ba096a3b94844319873d14b0f2d", - "sha256:88566d01a30f0453f7d038db46bc83ce125e38e47c5f6bfd4c9c287010e9bf74", - "sha256:9302d7011e3e376d25acd30d2d9e70d315d93f03cc748784af19b00988fc30b1", - "sha256:98040462b36ced9671e266b95c326b97f41290d9d17504a1ee4dc313a7667b9c", - "sha256:99abad1fd1303b35d991703432c9481e3242b7b3a393c186cfb02373bf604004", - "sha256:a983cc418c1fa160b7d797cfef0e0c9f8c6d5871e83eae2c5793fce6a837fad9", - "sha256:af7e9abe180b189431b0f490638281b43b84a33a960620e6b2e8d3e3458b61a1", - "sha256:b2a7d256db6f5b4b407dc0c98dd1b2fcf1c9c5814af9416e5498d0a2e4307a4b", - "sha256:b9f2d93bff2ea52fa93245798cddb479766a18510ea9b93a4fb9755c79474889", - "sha256:bd45d456983dc60a42c4db437496d3f08a4201fbf662b69779f535eb969660af", - "sha256:c91981c99ade980fc49e41a544629751a0ccd769f39794ae913e53b07b2f78b9", - "sha256:d8c2a2989222c6062f7a0656e16276c01bb308bc7e5d999e54bf4e294ce62e76", - "sha256:e45f0c8cd4583647db3216d965d49363e6548c300c23fd7e57ce17a03f824034", - "sha256:e86e7cb8e331a4850e0c2a8b2d66dc08d7a7b301b8d1d34a13060e3a5b4b3b55", - "sha256:f10d2ccf0554fc0e39fad5886c839e47e207f99fdf09547bc687a2330efda35b", - "sha256:f24ecbb90a45c962b3b60d8d9a387272ed50dc010bfe605f1d16dfc94772d8a1" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.3" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "sniffio": { - "hashes": [ - "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", - "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" - }, - "soupsieve": { - "hashes": [ - "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", - "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7" - ], - "markers": "python_version >= '3.8'", - "version": "==2.5" - }, - "sympy": { - "hashes": [ - "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5", - "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8" - ], - "markers": "python_version >= '3.8'", - "version": "==1.12" - }, - "tifffile": { - "hashes": [ - "sha256:4920a3ec8e8e003e673d3c6531863c99eedd570d1b8b7e141c072ed78ff8030d", - "sha256:870998f82fbc94ff7c3528884c1b0ae54863504ff51dbebea431ac3fa8fb7c21" - ], - "markers": "python_version >= '3.9'", - "version": "==2024.2.12" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, - "torch": { - "hashes": [ - "sha256:0952549bcb43448c8d860d5e3e947dd18cbab491b14638e21750cb3090d5ad3e", - "sha256:0e8bdd4c77ac2584f33ee14c6cd3b12767b4da508ec4eed109520be7212d1069", - "sha256:26bd2272ec46fc62dcf7d24b2fb284d44fcb7be9d529ebf336b9860350d674ed", - "sha256:2d9e7e5ecbb002257cf98fae13003abbd620196c35f85c9e34c2adfb961321ec", - "sha256:46085e328d9b738c261f470231e987930f4cc9472d9ffb7087c7a1343826ac51", - "sha256:5297f13370fdaca05959134b26a06a7f232ae254bf2e11a50eddec62525c9006", - "sha256:5c0c83aa7d94569997f1f474595e808072d80b04d34912ce6f1a0e1c24b0c12a", - "sha256:5f5dee8433798888ca1415055f5e3faf28a3bad660e4c29e1014acd3275ab11a", - "sha256:6a21bcd7076677c97ca7db7506d683e4e9db137e8420eb4a68fb67c3668232a7", - "sha256:6ab3ea2e29d1aac962e905142bbe50943758f55292f1b4fdfb6f4792aae3323e", - "sha256:77e990af75fb1675490deb374d36e726f84732cd5677d16f19124934b2409ce9", - "sha256:79848f46196750367dcdf1d2132b722180b9d889571e14d579ae82d2f50596c5", - "sha256:7ee804847be6be0032fbd2d1e6742fea2814c92bebccb177f0d3b8e92b2d2b18", - "sha256:84b2fb322ab091039fdfe74e17442ff046b258eb5e513a28093152c5b07325a7", - "sha256:8d3bad336dd2c93c6bcb3268e8e9876185bda50ebde325ef211fb565c7d15273", - "sha256:8f93ddf3001ecec16568390b507652644a3a103baa72de3ad3b9c530e3277098", - "sha256:91a1b598055ba06b2c386415d2e7f6ac818545e94c5def597a74754940188513", - "sha256:ada53aebede1c89570e56861b08d12ba4518a1f8b82d467c32665ec4d1f4b3c8", - "sha256:b6d78338acabf1fb2e88bf4559d837d30230cf9c3e4337261f4d83200df1fcbe", - "sha256:be21d4c41ecebed9e99430dac87de1439a8c7882faf23bba7fea3fea7b906ac1", - "sha256:c47bc25744c743f3835831a20efdcfd60aeb7c3f9804a213f61e45803d16c2a5", - "sha256:d6227060f268894f92c61af0a44c0d8212e19cb98d05c20141c73312d923bc0a", - "sha256:d86664ec85902967d902e78272e97d1aff1d331f7619d398d3ffab1c9b8e9157", - "sha256:ed9e29eb94cd493b36bca9cb0b1fd7f06a0688215ad1e4b3ab4931726e0ec092", - "sha256:f1b90ac61f862634039265cd0f746cc9879feee03ff962c803486301b778714b" - ], - "markers": "python_full_version >= '3.8.0'", - "version": "==2.2.1" - }, - "torchvision": { - "hashes": [ - "sha256:06418880212b66e45e855dd39f536e7fd48b4e6b034a11dd9fe9e2384afb51ec", - "sha256:0ecc9a58171bd555aed583bf2f72e7fd6cc4f767c14f8b80b6a8725eacf4ceb1", - "sha256:2621097065fa1c827885e2b52102e839a3541b933b7a90e0fa3c42c3de1bc3cf", - "sha256:32dc5de86d2ade399e11087095674ca08a1649fb322cfe69336d28add467edcb", - "sha256:33d65d0c7fdcb3f7bc1dd8ed30ea3cd7e0587b4ad1b104b5677c8191a8bad9f1", - "sha256:429d63eb7551aa4d8f6cdf08d109b5570c20cbcce36d9cb95b24556418e4dc82", - "sha256:524405457dd97d9ab0e48df502f819d0f41a113ce8f00470bb9926d9d36efcf1", - "sha256:54902877410ffb5458ee52b6d0de4b25cf01496bee736d6825301a5f0398536e", - "sha256:58299a724b37b893c7ce4d0b32ea1480c30e467cc114167964b45f6013f6c2d3", - "sha256:5966936c669a08870f6547cd0a90d08b157aeda03293f79e2adbb934687175ed", - "sha256:5ce76466af2b5a30573939cae1e6e62e29316ceb3ee748091002f312ab0912f6", - "sha256:5d241d2a5fb4e608677fccf6f80b34a124446d324ee40c7814ce54bce888275b", - "sha256:5f427ebee15521edcd836bfe05e86feb5189b5c943b9e3999ed0e3f391fbaa1d", - "sha256:8a1b17fb158b2b881f2c8796fe1839a624e49d5fd07aa61f6dae60ba4819421a", - "sha256:9106e32c9f1e70afa8172cf1b064cf9c2998d8dff0769ec69d537b20209ee43d", - "sha256:9d4d45a996f4313e9c5db4da71d31508d44f7ccfbf29d3442bdcc2ad13e0b6f3", - "sha256:a2109c1a1dcf71e8940d43e91f78c4dd5bf0fcefb3a0a42244102752009f5862", - "sha256:aaefef2be6a02f206085ce4bb6c0078b03ebf48cb6ff82bd762ff6248475e08e", - "sha256:bd5dcd14a32945c72f5c19341add94aa7c23dd7bca2bafde44d0f3c4344d17ed", - "sha256:cc22c1ed0f1aba3f98fd72b6f60021f57aec1d2f6af518522e8a0a83848de3a8", - "sha256:dca22795cc02ca0d5ddc08c1422ff620bc9899f63d15dc36f71ef37250e17b75", - "sha256:e0fe98d9d92c23d2262ff82f973242951b9357fb640f8888ac50848bd00f5b45", - "sha256:e74f5a26ef8190eab0c38b3f63914fea94e58e3b2f0e5466611c9f63bd91a80b", - "sha256:ea2ccdbf5974e0bf27fd6644a33b19cb0700297cf397bb0469e762c11c6c4105", - "sha256:ebe5fdb466aff8a8e8e755de84a843418b6f8d500624752c05eaa638d7700f3d" - ], - "markers": "python_version >= '3.8'", - "version": "==0.17.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", - "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" - ], - "markers": "python_version < '3.11'", - "version": "==4.10.0" - }, - "urllib3": { - "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" - ], - "markers": "python_version >= '3.8'", - "version": "==2.2.1" - } - }, - "develop": {} -} diff --git a/README.md b/README.md index bf6847d..5cb78fe 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,247 @@ -# Image Translator +# 图像翻译器 (Image Translator GUI) -This project utilizes optical character recognition (OCR) and translation to translate text within images from one language to another. It performs the following steps: +一个功能强大的图像文字翻译工具,支持自动OCR识别、交互式文本框选择、样式自定义和多语言翻译。 -1. **OCR Processing:** The project extracts text and its bounding boxes from input images using the EasyOCR library. -2. **Translation:** It translates the extracted text using the Google Translator API. -3. **Text Replacement:** The translated text is then overlaid onto the image, replacing the original text while maintaining its position and style. -4. **Output:** Finally, the modified image with translated text is saved to an output folder. +![Python](https://img.shields.io/badge/Python-3.8+-blue.svg) +![License](https://img.shields.io/badge/License-MIT-green.svg) -## Setup +## ✨ 主要功能 -### Installation +### 🎯 核心功能 +- **OCR文字识别** - 自动识别图片中的文字(支持中文、英文等多种语言) +- **多语言翻译** - 支持9种语言互译(英文、简体中文、繁体中文、日文、韩文、法文、德文、西班牙文、俄文) +- **交互式选择** - 可视化选择需要翻译的文本框 +- **手动框选** - 支持手动框选文字区域进行识别 +- **样式自定义** - 每个文本框可独立设置字体大小、字体类型、文字颜色 -1. Clone this repository to your local machine. -2. Install the required Python dependencies using `pip install pipenv && pipenv install`. +### 🖼️ 图形界面特色 +- **醒目的文本框标注** - 荧光绿/红色边框 + 彩色序号背景 +- **双向联动** - 图片点击与列表自动同步 +- **实时编辑** - 译文、字体、颜色实时调整 +- **批量操作** - 全选/全不选/反选快捷按钮 +- **自动保存** - 编辑后自动保存,无需手动点击 -## Usage +### 📦 两种使用模式 +1. **GUI模式** - 图形界面,适合单张图片精细处理 +2. **CLI模式** - 命令行批量处理,适合批量翻译整个文件夹 -1. Place your input images in the `input` folder. -2. Run the script `main.py`. -3. Translated images will be saved in the `output` folder. +## 📸 功能展示 -## Notes +### 主界面 +- **左侧**:图片预览区,带醒目的文本框标注和序号 +- **右侧**:设置面板、文本框列表、样式编辑器、运行日志 -- Supported languages for OCR can be seen [here](https://www.jaided.ai/easyocr/) -- Supported languages for Google Translate can be obtained using the following code: - ```python - from deep_translator.constants import GOOGLE_LANGUAGES_TO_CODES - print(GOOGLE_LANGUAGES_TO_CODES) - ``` -- Adjustments to text languages, recognition thresholds, translation services, or image processing parameters can be made within the script. +### 核心特性 +- ✅ 自动OCR识别文字 +- ✅ 手动框选补充识别 +- ✅ 可视化选择翻译区域 +- ✅ 每个文本框独立样式设置 +- ✅ 字体大小:自动或10-300号 +- ✅ 7种字体类型选择 +- ✅ 9种预设颜色+自定义颜色 +- ✅ 图片与列表实时联动 -## Examples +## 🚀 快速开始 -![image-1](https://github.com/boysugi20/python-image-translator/assets/53815726/cc2a52b3-2627-4f08-a428-c0dba4341bda) -![image-1-translated](https://github.com/boysugi20/python-image-translator/assets/53815726/3ecafe2e-df19-4ca2-aeff-b05cc89394db) +### 安装依赖 +```bash +pip install -r requirements.txt +``` -## Acknowledgments +### 运行程序 -- [EasyOCR](https://github.com/JaidedAI/EasyOCR) - For OCR processing. -- [Google Translator](https://pypi.org/project/deep-translator/) - For text translation. -- [Pillow (PIL Fork)](https://python-pillow.org/) - For image manipulation. +#### 方式1:图形界面(推荐) +```bash +python image_translator_gui.py +``` + +#### 方式2:命令行批量处理 +```bash +python image_translator_optimized.py +``` + +## 📖 使用指南 + +### GUI模式使用步骤 + +#### 基础流程 +1. **选择图片** - 点击"选择图片"按钮 +2. **识别文本** - 点击"识别文本"进行OCR识别 +3. **选择文本框** - 在图片或列表中选择要翻译的文本框 + - 图片上点击:切换选择状态(绿色=选中,红色=未选) + - 列表中点击☑:切换选择状态 + - 批量操作:全选/全不选/反选 +4. **设置语言** - 选择源语言和目标语言(如:英文 → 简体中文) +5. **翻译文本** - 点击"翻译选中的文本" +6. **编辑调整**(可选) + - 点击列表中的文本框进入编辑模式 + - 修改译文、调整字体大小、选择字体类型、设置颜色 +7. **保存图片** - 点击"应用翻译并保存" + +#### 手动框选模式 +如果自动识别遗漏了某些文字: +1. 勾选工具栏的 **"手动框选模式"** +2. 鼠标变为十字准星 +3. 在图片上**拖拽框选**要识别的文字区域 +4. 松开鼠标后自动识别并添加到列表 +5. 可继续框选其他区域 +6. 取消勾选退出手动框选模式 + +### 样式自定义 + +选中文本框后,可在下方编辑区调整: + +#### 字体大小 +- **自动** - 自动适应文本框大小(推荐) +- **10-300** - 手动指定字体大小(支持超大字体) + +#### 字体类型 +- 默认 / 微软雅黑 / 黑体 / 宋体 / 楷体 / Arial / Times New Roman + +#### 文字颜色 +- **自动** - 根据背景自动选择黑/白 +- **预设颜色** - 黑、白、红、绿、蓝、黄、橙、紫 +- **自定义颜色** - 点击"自定义颜色..."选择任意RGB颜色 + +### CLI模式使用 + +将图片放入 `input` 文件夹,运行: +```bash +python image_translator_optimized.py +``` + +翻译后的图片会保存在 `output` 文件夹中。 + +## ⚙️ 配置文件 + +`config.json` 包含所有配置选项: + +```json +{ + "ocr_languages": ["ch_sim", "en"], + "model_storage_directory": "model", + "ocr_width_threshold": 0.8, + "ocr_confidence_threshold": 0.4, + "ocr_decoder": "wordbeamsearch", + "source_language": "en", + "target_language": "zh-CN", + "input_folder": "input", + "output_folder": "output", + "background_margin": 10, + "color_discoloration_strength": 40, + "max_font_size": 500, + "font_path": "C:/Windows/Fonts/msyh.ttc", + "max_workers": 4 +} +``` + +### 主要配置项说明 + +| 配置项 | 说明 | 默认值 | +|--------|------|--------| +| `source_language` | 源语言代码 | `en` (英文) | +| `target_language` | 目标语言代码 | `zh-CN` (简体中文) | +| `ocr_confidence_threshold` | OCR置信度阈值 | `0.4` | +| `max_font_size` | 最大字体大小 | `500` | +| `font_path` | 字体文件路径 | `C:/Windows/Fonts/msyh.ttc` | +| `max_workers` | 并发处理数 | `4` | + +### 支持的语言代码 + +| 语言 | 代码 | +|------|------| +| 英文 | `en` | +| 简体中文 | `zh-CN` | +| 繁体中文 | `zh-TW` | +| 日文 | `ja` | +| 韩文 | `ko` | +| 法文 | `fr` | +| 德文 | `de` | +| 西班牙文 | `es` | +| 俄文 | `ru` | + +## 🛠️ 技术栈 + +- **Python 3.8+** +- **OCR引擎** - EasyOCR +- **翻译服务** - Google Translator (deep-translator) +- **图像处理** - Pillow (PIL) +- **GUI框架** - Tkinter + +## 📁 项目结构 + +``` +pic/ +├── image_translator_gui.py # 图形界面程序 +├── image_translator_optimized.py # 核心引擎(CLI模式) +├── config.json # 配置文件 +├── requirements.txt # 依赖列表 +├── README.md # 说明文档 +├── input/ # 输入图片文件夹 +├── output/ # 输出图片文件夹 +└── model/ # OCR模型文件夹(自动下载) +``` + +## 💡 使用技巧 + +### 提高识别准确度 +- 上传清晰的图片 +- 调整 OCR 置信度阈值(`ocr_confidence_threshold`) +- 使用手动框选补充遗漏的文字 + +### 提高翻译质量 +- 确保网络连接正常(使用在线翻译服务) +- 翻译后可手动编辑译文 +- 对于专业术语,建议手动修正 + +### 优化显示效果 +- 根据图片背景选择合适的文字颜色 +- 对于小文本框,使用"自动"字体大小 +- 对于标题,可手动设置大字体(如 48-128 号) +- 使用"黑体"字体显示更粗壮醒目 + +### 批量处理 +- 使用 CLI 模式批量处理多张图片 +- 调整 `max_workers` 参数控制并发数 +- 网络不稳定时建议降低并发数 + +## 🐛 常见问题 + +### Q: 提示 "ConnectionResetError" 怎么办? +**A:** 这是网络连接问题。解决方法: +1. 检查网络连接 +2. 降低并发数(修改 `max_workers` 为 1) +3. 重新运行程序 + +### Q: 翻译结果显示为方框(□□□)? +**A:** 字体不支持中文。解决方法: +1. 确保 `font_path` 指向正确的中文字体 +2. 使用微软雅黑:`C:/Windows/Fonts/msyh.ttc` +3. 或在 GUI 中选择其他中文字体 + +### Q: OCR 识别不准确怎么办? +**A:** 可以尝试: +1. 调高 OCR 置信度阈值(但可能漏掉部分文字) +2. 使用手动框选模式补充识别 +3. 确保图片清晰,文字对比度高 + +### Q: 如何只翻译部分文字? +**A:** 两种方法: +1. 识别后,在列表中取消不需要翻译的文本框(点击☑变为☐) +2. 跳过自动识别,直接使用手动框选模式,只框选需要翻译的区域 + +### Q: 首次运行很慢? +**A:** 首次运行需要下载 OCR 模型(约 200-300MB),这是正常现象。后续运行会快很多。 + +## 📝 更新日志 + +### v1.0.0 (2026-02-02) +- ✨ 初始版本发布 +- ✅ 支持自动 OCR 识别 +- ✅ 支持手动框选文字 +- ✅ 多语言翻译 +- ✅ 交互式文本框选择 +- ✅ 样式自定义(字体大小、类型、颜色) +- ✅ GUI 和 CLI 两种模式 +- ✅ 图片与列表双向联动 diff --git a/config.json b/config.json new file mode 100644 index 0000000..b1b2338 --- /dev/null +++ b/config.json @@ -0,0 +1,19 @@ +{ + "ocr_languages": [ + "ch_sim", + "en" + ], + "model_storage_directory": "model", + "ocr_width_threshold": 0.8, + "ocr_confidence_threshold": 0.4, + "ocr_decoder": "wordbeamsearch", + "source_language": "en", + "target_language": "zh-CN", + "input_folder": "input", + "output_folder": "output", + "background_margin": 10, + "color_discoloration_strength": 40, + "max_font_size": 500, + "font_path": "C:/Windows/Fonts/msyh.ttc", + "max_workers": 4 +} \ No newline at end of file diff --git a/image_translator_gui.py b/image_translator_gui.py new file mode 100644 index 0000000..c65cdc6 --- /dev/null +++ b/image_translator_gui.py @@ -0,0 +1,1294 @@ +import os +import tkinter as tk +from tkinter import ttk, filedialog, messagebox, scrolledtext +from pathlib import Path +import threading +from PIL import Image, ImageTk, ImageDraw, ImageFont +import logging +from typing import List, Tuple, Optional + +from image_translator_optimized import ( + Config, ImageTranslator, OCRProcessor, Translator, ImageProcessor +) + + +class TextBoxItem: + """文本框项""" + def __init__(self, index, coordinates, text, selected=True): + self.index = index + self.coordinates = coordinates + self.text = text + self.selected = selected + self.translated_text = None + # 样式设置 + self.font_size = None # None表示自动计算 + self.font_path = None # None表示使用默认字体 + self.text_color = None # None表示自动计算(黑/白) + + +class ImageTranslatorGUI: + """图像翻译器图形界面""" + + def __init__(self, root): + self.root = root + self.root.title("图像翻译器 - 交互式选择") + self.root.geometry("1400x800") + + # 配置 + self.config = Config.from_file() if os.path.exists("config.json") else Config.get_default() + self.translator = None + self.processing = False + + # 当前图片相关 + self.current_image_path = None + self.current_image = None + self.display_image = None + self.photo_image = None + self.text_boxes = [] # List[TextBoxItem] + self.scale_factor = 1.0 + self.current_edit_index = None # 当前正在编辑的文本框索引 + + # 手动框选相关 + self.manual_select_mode = False # 是否处于手动框选模式 + self.select_start_x = None + self.select_start_y = None + self.select_rect_id = None # 当前绘制的矩形框ID + + # 设置样式 + self.setup_styles() + + # 创建界面 + self.create_widgets() + + # 设置日志处理器 + self.setup_gui_logging() + + def setup_styles(self): + """设置界面样式""" + style = ttk.Style() + style.theme_use('clam') + + # 配置按钮样式 + style.configure('Primary.TButton', padding=10, font=('Microsoft YaHei', 10)) + style.configure('Success.TButton', padding=8, font=('Microsoft YaHei', 9)) + style.configure('TLabel', font=('Microsoft YaHei', 9)) + style.configure('Title.TLabel', font=('Microsoft YaHei', 12, 'bold')) + + def create_widgets(self): + """创建所有界面组件""" + # 主容器 - 使用PanedWindow分割左右 + main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL) + main_paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 左侧面板 - 图片预览 + left_frame = ttk.Frame(main_paned) + main_paned.add(left_frame, weight=2) + + # 右侧面板 - 控制和文本框列表 + right_frame = ttk.Frame(main_paned) + main_paned.add(right_frame, weight=1) + + # 创建左侧内容 + self.create_left_panel(left_frame) + + # 创建右侧内容 + self.create_right_panel(right_frame) + + def create_left_panel(self, parent): + """创建左侧图片预览面板""" + parent.columnconfigure(0, weight=1) + parent.rowconfigure(1, weight=1) + + # 顶部工具栏 + toolbar = ttk.Frame(parent, padding="5") + toolbar.grid(row=0, column=0, sticky=(tk.W, tk.E)) + + ttk.Button(toolbar, text="选择图片", command=self.select_image).pack(side=tk.LEFT, padx=5) + ttk.Button(toolbar, text="识别文本", command=self.detect_text).pack(side=tk.LEFT, padx=5) + + # 手动框选模式切换按钮 + self.manual_mode_var = tk.BooleanVar(value=False) + manual_check = ttk.Checkbutton( + toolbar, + text="手动框选模式", + variable=self.manual_mode_var, + command=self.toggle_manual_mode + ) + manual_check.pack(side=tk.LEFT, padx=5) + + ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, padx=5, fill=tk.Y) + + ttk.Button(toolbar, text="全选", command=self.select_all).pack(side=tk.LEFT, padx=5) + ttk.Button(toolbar, text="全不选", command=self.deselect_all).pack(side=tk.LEFT, padx=5) + ttk.Button(toolbar, text="反选", command=self.invert_selection).pack(side=tk.LEFT, padx=5) + + # 图片显示区域 + image_frame = ttk.LabelFrame(parent, text="图片预览(点击文本框切换选择状态)", padding="10") + image_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5) + image_frame.columnconfigure(0, weight=1) + image_frame.rowconfigure(0, weight=1) + + # 创建Canvas用于显示图片 + self.canvas = tk.Canvas(image_frame, bg='gray', cursor='cross') + self.canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # 添加滚动条 + h_scrollbar = ttk.Scrollbar(image_frame, orient=tk.HORIZONTAL, command=self.canvas.xview) + h_scrollbar.grid(row=1, column=0, sticky=(tk.W, tk.E)) + v_scrollbar = ttk.Scrollbar(image_frame, orient=tk.VERTICAL, command=self.canvas.yview) + v_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + + self.canvas.configure(xscrollcommand=h_scrollbar.set, yscrollcommand=v_scrollbar.set) + + # 绑定鼠标事件 + self.canvas.bind('', self.on_canvas_mouse_down) + self.canvas.bind('', self.on_canvas_mouse_drag) + self.canvas.bind('', self.on_canvas_mouse_up) + + def create_right_panel(self, parent): + """创建右侧控制面板""" + parent.columnconfigure(0, weight=1) + parent.rowconfigure(1, weight=1) + parent.rowconfigure(2, weight=1) + + # 1. 翻译设置和高级设置(合并为一行) + self.create_settings_section(parent) + + # 2. 文本框列表 + self.create_textbox_list(parent) + + # 3. 日志输出 + self.create_log_section(parent) + + # 4. 控制按钮 + self.create_control_section(parent) + + def create_settings_section(self, parent): + """创建设置区域(翻译+高级设置合并)""" + frame = ttk.LabelFrame(parent, text="设置", padding="10") + frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5) + + # 语言字典 + languages = { + "英文": "en", + "简体中文": "zh-CN", + "繁体中文": "zh-TW", + "日文": "ja", + "韩文": "ko", + "法文": "fr", + "德文": "de", + "西班牙文": "es", + "俄文": "ru" + } + self.languages_dict = languages + + # 第一行:语言设置 + row1 = ttk.Frame(frame) + row1.pack(fill=tk.X, pady=2) + + ttk.Label(row1, text="源语言:", width=8).pack(side=tk.LEFT, padx=5) + self.source_lang_var = tk.StringVar(value=self.get_language_name(self.config.source_language, languages)) + source_combo = ttk.Combobox(row1, textvariable=self.source_lang_var, values=list(languages.keys()), state='readonly', width=10) + source_combo.pack(side=tk.LEFT, padx=5) + + ttk.Label(row1, text="→", font=('Arial', 14)).pack(side=tk.LEFT, padx=2) + ttk.Button(row1, text="⇄", command=self.swap_languages, width=3).pack(side=tk.LEFT, padx=2) + + ttk.Label(row1, text="目标语言:", width=8).pack(side=tk.LEFT, padx=(10, 5)) + self.target_lang_var = tk.StringVar(value=self.get_language_name(self.config.target_language, languages)) + target_combo = ttk.Combobox(row1, textvariable=self.target_lang_var, values=list(languages.keys()), state='readonly', width=10) + target_combo.pack(side=tk.LEFT, padx=5) + + # 第二行:高级设置 + row2 = ttk.Frame(frame) + row2.pack(fill=tk.X, pady=2) + + ttk.Label(row2, text="OCR置信度:", width=10).pack(side=tk.LEFT, padx=5) + self.confidence_var = tk.DoubleVar(value=self.config.ocr_confidence_threshold) + confidence_scale = ttk.Scale(row2, from_=0.0, to=1.0, variable=self.confidence_var, orient=tk.HORIZONTAL, length=100) + confidence_scale.pack(side=tk.LEFT, padx=5) + self.confidence_label = ttk.Label(row2, text=f"{self.confidence_var.get():.2f}", width=4) + self.confidence_label.pack(side=tk.LEFT, padx=2) + self.confidence_var.trace_add('write', lambda *args: self.confidence_label.config(text=f"{self.confidence_var.get():.2f}")) + + ttk.Label(row2, text="最大字体:", width=8).pack(side=tk.LEFT, padx=(10, 5)) + self.font_size_var = tk.IntVar(value=self.config.max_font_size) + ttk.Spinbox(row2, from_=10, to=1000, textvariable=self.font_size_var, width=8).pack(side=tk.LEFT, padx=5) + + def create_translation_section(self, parent): + """创建翻译设置区域""" + frame = ttk.LabelFrame(parent, text="翻译设置", padding="10") + frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5) + + # 语言选择 + languages = { + "英文": "en", + "简体中文": "zh-CN", + "繁体中文": "zh-TW", + "日文": "ja", + "韩文": "ko", + "法文": "fr", + "德文": "de", + "西班牙文": "es", + "俄文": "ru" + } + + # 源语言 + ttk.Label(frame, text="源语言:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) + self.source_lang_var = tk.StringVar(value=self.get_language_name(self.config.source_language, languages)) + source_combo = ttk.Combobox(frame, textvariable=self.source_lang_var, values=list(languages.keys()), state='readonly', width=12) + source_combo.grid(row=0, column=1, sticky=tk.W, padx=5) + + # 箭头和切换按钮 + ttk.Label(frame, text="→", font=('Arial', 16)).grid(row=0, column=2, padx=5) + ttk.Button(frame, text="⇄", command=self.swap_languages, width=3).grid(row=0, column=3, padx=2) + + # 目标语言 + ttk.Label(frame, text="目标语言:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5) + self.target_lang_var = tk.StringVar(value=self.get_language_name(self.config.target_language, languages)) + target_combo = ttk.Combobox(frame, textvariable=self.target_lang_var, values=list(languages.keys()), state='readonly', width=12) + target_combo.grid(row=1, column=1, sticky=tk.W, padx=5) + + self.languages_dict = languages + + def create_advanced_section(self, parent): + """创建高级设置区域""" + frame = ttk.LabelFrame(parent, text="高级设置", padding="10") + frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=5) + + # OCR 置信度阈值 + ttk.Label(frame, text="OCR置信度:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) + self.confidence_var = tk.DoubleVar(value=self.config.ocr_confidence_threshold) + confidence_scale = ttk.Scale(frame, from_=0.0, to=1.0, variable=self.confidence_var, orient=tk.HORIZONTAL, length=150) + confidence_scale.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5) + self.confidence_label = ttk.Label(frame, text=f"{self.confidence_var.get():.2f}") + self.confidence_label.grid(row=0, column=2, padx=5) + self.confidence_var.trace_add('write', lambda *args: self.confidence_label.config(text=f"{self.confidence_var.get():.2f}")) + + # 最大字体大小 + ttk.Label(frame, text="最大字体:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5) + self.font_size_var = tk.IntVar(value=self.config.max_font_size) + ttk.Spinbox(frame, from_=10, to=1000, textvariable=self.font_size_var, width=8).grid(row=1, column=1, sticky=tk.W, padx=5) + + frame.columnconfigure(1, weight=1) + + def create_textbox_list(self, parent): + """创建文本框列表""" + frame = ttk.LabelFrame(parent, text="识别到的文本框", padding="10") + frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + # 创建Treeview + columns = ('#', '☑', '原文', '译文') + self.tree = ttk.Treeview(frame, columns=columns, show='headings', height=10) + self.tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # 设置列 + self.tree.heading('#', text='#') + self.tree.heading('☑', text='☑') + self.tree.heading('原文', text='原文') + self.tree.heading('译文', text='译文') + + self.tree.column('#', width=35, anchor='center') + self.tree.column('☑', width=35, anchor='center') + self.tree.column('原文', width=180) + self.tree.column('译文', width=180) + + # 滚动条 + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=self.tree.yview) + scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + self.tree.configure(yscrollcommand=scrollbar.set) + + # 单击切换选择状态 + self.tree.bind('', self.on_tree_click) + + # 编辑框区域 + edit_frame = ttk.LabelFrame(frame, text="编辑选中的文本框", padding="10") + edit_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5) + + # 第一行:译文编辑 + row1 = ttk.Frame(edit_frame) + row1.pack(fill=tk.X, pady=2) + ttk.Label(row1, text="译文:", width=10).pack(side=tk.LEFT, padx=5) + self.edit_var = tk.StringVar() + self.edit_entry = ttk.Entry(row1, textvariable=self.edit_var) + self.edit_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) + ttk.Label(row1, text="(按回车保存)", font=('Microsoft YaHei', 8)).pack(side=tk.LEFT) + + # 绑定回车键自动保存 + self.edit_entry.bind('', lambda e: self.apply_edit()) + # 绑定失去焦点自动保存 + self.edit_entry.bind('', lambda e: self.apply_edit_auto()) + + # 第二行:字体大小 + row2 = ttk.Frame(edit_frame) + row2.pack(fill=tk.X, pady=2) + ttk.Label(row2, text="字体大小:", width=10).pack(side=tk.LEFT, padx=5) + self.font_size_edit_var = tk.StringVar(value="自动") + self.font_size_combo = ttk.Combobox( + row2, + textvariable=self.font_size_edit_var, + values=['自动', '10', '12', '14', '16', '18', '20', '24', '28', '32', '36', '40', '48', '56', '64', '72', '80', '96', '128', '150', '200', '250', '300'], + width=10, + state='readonly' + ) + self.font_size_combo.pack(side=tk.LEFT, padx=5) + self.font_size_combo.bind('<>', self.on_style_changed) + + # 字体选择 + ttk.Label(row2, text="字体:", width=8).pack(side=tk.LEFT, padx=(20, 5)) + self.font_edit_var = tk.StringVar(value="默认") + self.font_combo = ttk.Combobox( + row2, + textvariable=self.font_edit_var, + values=['默认', '微软雅黑', '黑体', '宋体', '楷体', 'Arial', 'Times New Roman'], + width=15, + state='readonly' + ) + self.font_combo.pack(side=tk.LEFT, padx=5) + self.font_combo.bind('<>', self.on_style_changed) + + # 第三行:文字颜色 + row3 = ttk.Frame(edit_frame) + row3.pack(fill=tk.X, pady=2) + ttk.Label(row3, text="文字颜色:", width=10).pack(side=tk.LEFT, padx=5) + self.color_edit_var = tk.StringVar(value="自动") + self.color_combo = ttk.Combobox( + row3, + textvariable=self.color_edit_var, + values=['自动', '黑色', '白色', '红色', '绿色', '蓝色', '黄色', '橙色', '紫色'], + width=10, + state='readonly' + ) + self.color_combo.pack(side=tk.LEFT, padx=5) + self.color_combo.bind('<>', self.on_style_changed) + + # 自定义颜色按钮 + ttk.Button(row3, text="自定义颜色...", command=self.choose_custom_color).pack(side=tk.LEFT, padx=5) + + # 重置按钮 + ttk.Button(row3, text="重置样式", command=self.reset_style).pack(side=tk.LEFT, padx=(20, 5)) + + def create_log_section(self, parent): + """创建日志输出区域""" + frame = ttk.LabelFrame(parent, text="运行日志", padding="10") + frame.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + # 日志文本框 + self.log_text = scrolledtext.ScrolledText( + frame, + height=8, + wrap=tk.WORD, + font=('Consolas', 8) + ) + self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + def create_control_section(self, parent): + """创建控制按钮区域""" + frame = ttk.Frame(parent, padding="10") + frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=5) + + # 翻译选中文本按钮 + self.translate_button = ttk.Button( + frame, + text="翻译选中的文本", + command=self.translate_selected, + style='Primary.TButton' + ) + self.translate_button.pack(fill=tk.X, pady=2) + + # 应用翻译按钮 + self.apply_button = ttk.Button( + frame, + text="应用翻译并保存", + command=self.apply_translation, + style='Success.TButton', + state=tk.DISABLED + ) + self.apply_button.pack(fill=tk.X, pady=2) + + # 保存配置 + ttk.Button(frame, text="保存配置", command=self.save_config).pack(fill=tk.X, pady=2) + + def setup_gui_logging(self): + """设置GUI日志处理器""" + class GUILogHandler(logging.Handler): + def __init__(self, text_widget): + super().__init__() + self.text_widget = text_widget + + def emit(self, record): + msg = self.format(record) + def append(): + self.text_widget.insert(tk.END, msg + '\n') + self.text_widget.see(tk.END) + self.text_widget.after(0, append) + + # 配置日志 + logger = logging.getLogger() + logger.setLevel(logging.INFO) + logger.handlers.clear() + + # 添加GUI处理器 + gui_handler = GUILogHandler(self.log_text) + gui_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + logger.addHandler(gui_handler) + + # 添加文件处理器 + file_handler = logging.FileHandler('translator_gui.log', encoding='utf-8') + file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + logger.addHandler(file_handler) + + def get_language_name(self, code, languages): + """根据语言代码获取语言名称""" + for name, lang_code in languages.items(): + if lang_code == code: + return name + return "英文" + + def swap_languages(self): + """交换源语言和目标语言""" + source = self.source_lang_var.get() + target = self.target_lang_var.get() + self.source_lang_var.set(target) + self.target_lang_var.set(source) + + def select_image(self): + """选择图片""" + filename = filedialog.askopenfilename( + title="选择图片", + filetypes=[ + ("图片文件", "*.jpg *.jpeg *.png *.webp *.bmp"), + ("所有文件", "*.*") + ] + ) + if filename: + self.load_image(filename) + + def load_image(self, image_path): + """加载图片""" + try: + self.current_image_path = image_path + self.current_image = Image.open(image_path) + self.text_boxes = [] + + # 显示图片 + self.display_image_on_canvas() + + logging.info(f"已加载图片: {image_path}") + except Exception as e: + messagebox.showerror("错误", f"加载图片失败: {e}") + logging.error(f"加载图片失败: {e}") + + def display_image_on_canvas(self, draw_boxes=False): + """在Canvas上显示图片""" + if self.current_image is None: + return + + # 复制图片用于显示 + display_img = self.current_image.copy() + + # 如果需要绘制文本框 + if draw_boxes and self.text_boxes: + draw = ImageDraw.Draw(display_img, 'RGBA') + + for box_item in self.text_boxes: + coords = box_item.coordinates + color = 'lime' if box_item.selected else 'red' + width = 3 if box_item.selected else 2 + + # 绘制矩形 + points = [(int(p[0]), int(p[1])) for p in coords] + draw.polygon(points, outline=color, width=width) + + # 绘制序号 + x_min = min(p[0] for p in coords) + y_min = min(p[1] for p in coords) + + # 序号文字 + number_text = str(box_item.index + 1) + + try: + number_font = ImageFont.truetype("C:/Windows/Fonts/msyhbd.ttc", 28) + except: + try: + number_font = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 28) + except: + number_font = ImageFont.load_default() + + # 计算文字边界框 + bbox = draw.textbbox((0, 0), number_text, font=number_font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + + # 序号位置 + number_x = x_min + number_y = max(5, y_min - text_height - 8) + + # 绘制序号背景(半透明矩形) + padding = 4 + bg_color = (0, 255, 0, 100) if box_item.selected else (255, 0, 0, 100) # RGBA + draw.rectangle( + [number_x - padding, number_y - padding, + number_x + text_width + padding, number_y + text_height + padding], + fill=bg_color + ) + + # 绘制白色序号文字 + draw.text((number_x, number_y), number_text, fill='white', font=number_font) + + # 调整图片大小以适应Canvas + canvas_width = self.canvas.winfo_width() + canvas_height = self.canvas.winfo_height() + + if canvas_width <= 1 or canvas_height <= 1: + canvas_width = 800 + canvas_height = 600 + + img_width, img_height = display_img.size + scale_x = canvas_width / img_width + scale_y = canvas_height / img_height + self.scale_factor = min(scale_x, scale_y, 1.0) + + new_width = int(img_width * self.scale_factor) + new_height = int(img_height * self.scale_factor) + + display_img = display_img.resize((new_width, new_height), Image.Resampling.LANCZOS) + + # 转换为PhotoImage + self.photo_image = ImageTk.PhotoImage(display_img) + + # 显示在Canvas上 + self.canvas.delete('all') + self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo_image) + self.canvas.config(scrollregion=(0, 0, new_width, new_height)) + + def detect_text(self): + """识别文本""" + if self.current_image_path is None: + messagebox.showwarning("警告", "请先选择图片") + return + + def detect(): + try: + logging.info("正在初始化OCR...") + self.update_config_from_gui() + + ocr = OCRProcessor(self.config) + ocr.initialize() + + logging.info("正在识别文本...") + text_boxes = ocr.extract_text_boxes(self.current_image_path) + + if not text_boxes: + messagebox.showinfo("提示", "未识别到文本") + return + + # 创建TextBoxItem列表 + self.text_boxes = [ + TextBoxItem(i, coords, text, selected=True) + for i, (coords, text) in enumerate(text_boxes) + ] + + logging.info(f"识别到 {len(self.text_boxes)} 个文本框") + + # 更新显示 + self.root.after(0, self.update_display) + + except Exception as e: + logging.error(f"文本识别失败: {e}", exc_info=True) + messagebox.showerror("错误", f"文本识别失败: {e}") + + # 在后台线程运行 + thread = threading.Thread(target=detect) + thread.daemon = True + thread.start() + + def update_display(self): + """更新显示(图片和列表)""" + # 更新图片显示 + self.display_image_on_canvas(draw_boxes=True) + + # 更新文本框列表 + self.tree.delete(*self.tree.get_children()) + for i, box_item in enumerate(self.text_boxes): + # 更新索引(防止删除后索引不连续) + box_item.index = i + + # 序号 + number = str(box_item.index + 1) + # 复选框符号 + checkbox = '☑' if box_item.selected else '☐' + # 译文 + translated = box_item.translated_text if box_item.translated_text else '' + + self.tree.insert('', tk.END, values=(number, checkbox, box_item.text, translated), tags=(str(box_item.index),)) + + def toggle_manual_mode(self): + """切换手动框选模式""" + self.manual_select_mode = self.manual_mode_var.get() + if self.manual_select_mode: + self.canvas.config(cursor='crosshair') + logging.info("已进入手动框选模式 - 拖拽鼠标框选文字区域") + else: + self.canvas.config(cursor='arrow') + logging.info("已退出手动框选模式") + + def on_canvas_mouse_down(self, event): + """鼠标按下事件""" + if self.manual_select_mode: + # 手动框选模式:开始绘制矩形 + self.select_start_x = event.x + self.select_start_y = event.y + else: + # 普通模式:处理点击 + self.on_canvas_click(event) + + def on_canvas_mouse_drag(self, event): + """鼠标拖拽事件""" + if not self.manual_select_mode or self.select_start_x is None: + return + + # 删除之前的临时矩形 + if self.select_rect_id: + self.canvas.delete(self.select_rect_id) + + # 绘制新的矩形 + self.select_rect_id = self.canvas.create_rectangle( + self.select_start_x, self.select_start_y, + event.x, event.y, + outline='yellow', + width=3, + dash=(5, 5) + ) + + def on_canvas_mouse_up(self, event): + """鼠标释放事件""" + if not self.manual_select_mode or self.select_start_x is None: + return + + # 删除临时矩形 + if self.select_rect_id: + self.canvas.delete(self.select_rect_id) + self.select_rect_id = None + + # 计算实际坐标(考虑缩放) + x1 = min(self.select_start_x, event.x) / self.scale_factor + y1 = min(self.select_start_y, event.y) / self.scale_factor + x2 = max(self.select_start_x, event.x) / self.scale_factor + y2 = max(self.select_start_y, event.y) / self.scale_factor + + # 检查框选区域是否太小 + if abs(x2 - x1) < 10 or abs(y2 - y1) < 10: + logging.warning("框选区域太小,请重新框选") + self.select_start_x = None + self.select_start_y = None + return + + # 重置起点 + self.select_start_x = None + self.select_start_y = None + + # 对框选区域进行OCR识别 + self.recognize_manual_region(x1, y1, x2, y2) + + def recognize_manual_region(self, x1, y1, x2, y2): + """对手动框选的区域进行OCR识别""" + if not self.current_image_path: + return + + def recognize(): + try: + logging.info(f"正在识别框选区域: ({int(x1)}, {int(y1)}) - ({int(x2)}, {int(y2)})") + + # 裁剪图片区域 + image = Image.open(self.current_image_path) + cropped = image.crop((int(x1), int(y1), int(x2), int(y2))) + + # 保存临时图片 + temp_path = "temp_crop.png" + cropped.save(temp_path) + + # OCR识别 + self.update_config_from_gui() + ocr = OCRProcessor(self.config) + if not hasattr(ocr, 'reader') or ocr.reader is None: + ocr.initialize() + + # 识别裁剪区域 + result = ocr.reader.readtext( + temp_path, + width_ths=self.config.ocr_width_threshold, + decoder=self.config.ocr_decoder + ) + + # 删除临时文件 + if os.path.exists(temp_path): + os.remove(temp_path) + + if not result: + logging.warning("未在框选区域识别到文字") + messagebox.showinfo("提示", "未在框选区域识别到文字") + return + + # 提取文字 + texts = [entry[1] for entry in result if entry[2] > self.config.ocr_confidence_threshold] + if not texts: + logging.warning("框选区域文字置信度过低") + messagebox.showinfo("提示", "识别到的文字置信度过低") + return + + combined_text = ' '.join(texts) + logging.info(f"识别到文字: {combined_text}") + + # 创建文本框坐标(矩形四个角) + coordinates = [ + [x1, y1], + [x2, y1], + [x2, y2], + [x1, y2] + ] + + # 添加到文本框列表 + new_index = len(self.text_boxes) + new_box = TextBoxItem(new_index, coordinates, combined_text, selected=True) + self.text_boxes.append(new_box) + + logging.info(f"已添加手动框选的文本框 #{new_index + 1}: {combined_text}") + + # 更新显示 + self.root.after(0, self.update_display) + + # 自动跳转到新添加的项 + self.root.after(100, lambda: self.jump_to_item_in_tree(new_index)) + self.root.after(100, lambda: self.show_in_editor(new_index)) + + except Exception as e: + logging.error(f"识别框选区域失败: {e}", exc_info=True) + messagebox.showerror("错误", f"识别失败: {e}") + + # 在后台线程运行 + thread = threading.Thread(target=recognize) + thread.daemon = True + thread.start() + + def on_canvas_click(self, event): + """Canvas点击事件 - 切换文本框选择状态(仅在非手动框选模式下)""" + if not self.text_boxes: + return + + # 获取点击位置(考虑缩放) + x = event.x / self.scale_factor + y = event.y / self.scale_factor + + # 检查点击是否在某个文本框内 + for box_item in self.text_boxes: + coords = box_item.coordinates + x_min = min(p[0] for p in coords) + y_min = min(p[1] for p in coords) + x_max = max(p[0] for p in coords) + y_max = max(p[1] for p in coords) + + if x_min <= x <= x_max and y_min <= y <= y_max: + # 切换选择状态 + box_item.selected = not box_item.selected + logging.info(f"文本框 #{box_item.index + 1} {'选中' if box_item.selected else '取消选中'}: {box_item.text}") + + # 更新显示 + self.update_display() + + # 在右侧列表中定位到该项 + self.jump_to_item_in_tree(box_item.index) + + # 在编辑框中显示 + self.show_in_editor(box_item.index) + + break + + def jump_to_item_in_tree(self, index): + """在Treeview中跳转到指定项""" + # 获取所有项 + items = self.tree.get_children() + + if index < len(items): + item_id = items[index] + + # 选中该项 + self.tree.selection_set(item_id) + + # 滚动到该项使其可见 + self.tree.see(item_id) + + # 设置焦点 + self.tree.focus(item_id) + + def on_tree_click(self, event): + """Treeview点击事件""" + # 获取点击的区域 + region = self.tree.identify_region(event.x, event.y) + + if region == "cell": + # 获取点击的列 + column = self.tree.identify_column(event.x) + item = self.tree.identify_row(event.y) + + if not item: + return + + index = int(self.tree.item(item, 'tags')[0]) + + # 如果点击的是第二列(复选框列),切换选择状态 + if column == '#2': # 复选框列 + self.text_boxes[index].selected = not self.text_boxes[index].selected + self.update_display() + logging.info(f"文本框 #{index + 1} {'选中' if self.text_boxes[index].selected else '取消选中'}") + else: + # 其他列,选中该项并显示在编辑框中 + self.tree.selection_set(item) + self.show_in_editor(index) + + def show_in_editor(self, index): + """在编辑框中显示选中的文本""" + # 先保存之前的编辑 + if hasattr(self, 'current_edit_index') and self.current_edit_index is not None: + self.apply_edit_auto() + + box_item = self.text_boxes[index] + + # 显示译文 + if box_item.translated_text: + self.edit_var.set(box_item.translated_text) + else: + self.edit_var.set(box_item.text) + + # 显示字体大小 + if box_item.font_size is None: + self.font_size_edit_var.set("自动") + else: + self.font_size_edit_var.set(str(box_item.font_size)) + + # 显示字体 + if box_item.font_path is None: + self.font_edit_var.set("默认") + else: + # 根据路径推断字体名称 + font_name = self.get_font_name_from_path(box_item.font_path) + self.font_edit_var.set(font_name) + + # 显示颜色 + if box_item.text_color is None: + self.color_edit_var.set("自动") + else: + color_name = self.get_color_name(box_item.text_color) + self.color_edit_var.set(color_name) + + # 保存当前编辑的索引 + self.current_edit_index = index + + def get_font_name_from_path(self, font_path): + """根据字体路径获取字体名称""" + font_map = { + 'msyh.ttc': '微软雅黑', + 'msyhbd.ttc': '微软雅黑', + 'simhei.ttf': '黑体', + 'simsun.ttc': '宋体', + 'simkai.ttf': '楷体', + 'arial.ttf': 'Arial', + 'times.ttf': 'Times New Roman' + } + for key, value in font_map.items(): + if key in font_path.lower(): + return value + return "默认" + + def get_color_name(self, color): + """根据颜色值获取颜色名称""" + color_map = { + 'black': '黑色', + 'white': '白色', + 'red': '红色', + 'green': '绿色', + 'blue': '蓝色', + 'yellow': '黄色', + 'orange': '橙色', + 'purple': '紫色' + } + return color_map.get(color, color if isinstance(color, str) else '自定义') + + def on_style_changed(self, event=None): + """样式改变时的处理""" + if not hasattr(self, 'current_edit_index') or self.current_edit_index is None: + return + + box_item = self.text_boxes[self.current_edit_index] + + # 更新字体大小 + size_text = self.font_size_edit_var.get() + if size_text == "自动": + box_item.font_size = None + else: + try: + box_item.font_size = int(size_text) + except: + box_item.font_size = None + + # 更新字体 + font_name = self.font_edit_var.get() + box_item.font_path = self.get_font_path_from_name(font_name) + + # 更新颜色 + color_name = self.color_edit_var.get() + box_item.text_color = self.get_color_from_name(color_name) + + logging.info(f"更新文本框 #{self.current_edit_index + 1} 样式: 大小={size_text}, 字体={font_name}, 颜色={color_name}") + + def get_font_path_from_name(self, font_name): + """根据字体名称获取字体路径""" + font_paths = { + '默认': None, + '微软雅黑': 'C:/Windows/Fonts/msyh.ttc', + '黑体': 'C:/Windows/Fonts/simhei.ttf', + '宋体': 'C:/Windows/Fonts/simsun.ttc', + '楷体': 'C:/Windows/Fonts/simkai.ttf', + 'Arial': 'C:/Windows/Fonts/arial.ttf', + 'Times New Roman': 'C:/Windows/Fonts/times.ttf' + } + return font_paths.get(font_name, None) + + def get_color_from_name(self, color_name): + """根据颜色名称获取颜色值""" + color_values = { + '自动': None, + '黑色': 'black', + '白色': 'white', + '红色': 'red', + '绿色': 'green', + '蓝色': 'blue', + '黄色': 'yellow', + '橙色': 'orange', + '紫色': 'purple' + } + return color_values.get(color_name, None) + + def choose_custom_color(self): + """选择自定义颜色""" + from tkinter import colorchooser + + if not hasattr(self, 'current_edit_index') or self.current_edit_index is None: + messagebox.showwarning("警告", "请先选择一个文本框") + return + + color = colorchooser.askcolor(title="选择文字颜色") + if color[1]: # color[1] 是十六进制颜色值 + box_item = self.text_boxes[self.current_edit_index] + box_item.text_color = color[1] + self.color_edit_var.set(f"自定义({color[1]})") + logging.info(f"设置文本框 #{self.current_edit_index + 1} 颜色为: {color[1]}") + + def reset_style(self): + """重置样式为自动""" + if not hasattr(self, 'current_edit_index') or self.current_edit_index is None: + messagebox.showwarning("警告", "请先选择一个文本框") + return + + box_item = self.text_boxes[self.current_edit_index] + box_item.font_size = None + box_item.font_path = None + box_item.text_color = None + + self.font_size_edit_var.set("自动") + self.font_edit_var.set("默认") + self.color_edit_var.set("自动") + + logging.info(f"重置文本框 #{self.current_edit_index + 1} 样式为自动") + + def apply_edit(self): + """应用编辑(手动调用或回车触发)""" + if not hasattr(self, 'current_edit_index') or self.current_edit_index is None: + return + + new_text = self.edit_var.get().strip() + if not new_text: + return + + # 更新翻译文本 + box_item = self.text_boxes[self.current_edit_index] + + # 只有内容真的改变了才更新 + if box_item.translated_text != new_text: + box_item.translated_text = new_text + # 更新显示 + self.update_display() + logging.info(f"已修改文本框 #{self.current_edit_index + 1} 的译文: {new_text}") + + def apply_edit_auto(self): + """自动应用编辑(失去焦点时)""" + if not hasattr(self, 'current_edit_index') or self.current_edit_index is None: + return + + new_text = self.edit_var.get().strip() + if not new_text: + return + + # 更新翻译文本 + box_item = self.text_boxes[self.current_edit_index] + + # 只有内容真的改变了才更新 + if box_item.translated_text != new_text: + box_item.translated_text = new_text + # 更新显示 + self.update_display() + + def select_all(self): + """全选""" + for box_item in self.text_boxes: + box_item.selected = True + self.update_display() + logging.info("已全选所有文本框") + + def deselect_all(self): + """全不选""" + for box_item in self.text_boxes: + box_item.selected = False + self.update_display() + logging.info("已取消选择所有文本框") + + def invert_selection(self): + """反选""" + for box_item in self.text_boxes: + box_item.selected = not box_item.selected + self.update_display() + logging.info("已反选所有文本框") + + def translate_selected(self): + """翻译选中的文本""" + if not self.text_boxes: + messagebox.showwarning("警告", "请先识别文本") + return + + selected_boxes = [box for box in self.text_boxes if box.selected] + if not selected_boxes: + messagebox.showwarning("警告", "请至少选择一个文本框") + return + + def translate(): + try: + logging.info(f"正在翻译 {len(selected_boxes)} 个文本框...") + self.update_config_from_gui() + + translator = Translator(self.config) + + for i, box_item in enumerate(selected_boxes): + translated = translator.translate(box_item.text) + box_item.translated_text = translated + logging.info(f"[{i+1}/{len(selected_boxes)}] '{box_item.text}' -> '{translated}'") + + logging.info("翻译完成!") + self.root.after(0, self.on_translation_complete) + + except Exception as e: + logging.error(f"翻译失败: {e}", exc_info=True) + messagebox.showerror("错误", f"翻译失败: {e}") + + # 在后台线程运行 + thread = threading.Thread(target=translate) + thread.daemon = True + thread.start() + + def on_translation_complete(self): + """翻译完成后的处理""" + self.update_display() + self.apply_button.config(state=tk.NORMAL) + messagebox.showinfo("完成", "翻译完成!点击'应用翻译并保存'生成图片") + + def apply_translation(self): + """应用翻译并保存图片""" + if not self.current_image_path: + return + + try: + logging.info("正在生成翻译后的图片...") + self.update_config_from_gui() + + # 直接使用自定义的图像处理逻辑 + result_image = self.render_translated_image() + + # 保存图片 + output_path = self.get_output_path() + result_image.save(output_path) + + logging.info(f"图片已保存: {output_path}") + messagebox.showinfo("完成", f"图片已保存至:\n{output_path}") + + # 询问是否打开 + if messagebox.askyesno("打开图片", "是否打开翻译后的图片?"): + os.startfile(output_path) + + except Exception as e: + logging.error(f"应用翻译失败: {e}", exc_info=True) + messagebox.showerror("错误", f"应用翻译失败: {e}") + + def render_translated_image(self): + """渲染翻译后的图片(支持自定义样式)""" + from PIL import Image, ImageDraw, ImageFont + from collections import Counter + + image = Image.open(self.current_image_path) + draw = ImageDraw.Draw(image) + + for box_item in self.text_boxes: + # 跳过未选中或没有翻译的文本框 + if not box_item.selected or not box_item.translated_text: + continue + + coordinates = box_item.coordinates + translated_text = box_item.translated_text + + # 获取边界框 + x_min = min(p[0] for p in coordinates) + y_min = min(p[1] for p in coordinates) + x_max = max(p[0] for p in coordinates) + y_max = max(p[1] for p in coordinates) + + # 获取背景颜色 + image_processor = ImageProcessor(self.config) + bg_color = image_processor.get_background_color(image, x_min, y_min, x_max, y_max) + + # 覆盖原文本 + draw.rectangle(((x_min, y_min), (x_max, y_max)), fill=bg_color) + + # 计算字体和位置 + width = x_max - x_min + height = y_max - y_min + + # 使用自定义样式或自动计算 + if box_item.font_size is not None: + # 使用指定的字体大小 + font_size = box_item.font_size + font_path = box_item.font_path or self.config.font_path + + try: + font = ImageFont.truetype(font_path, size=font_size) + except: + font = ImageFont.load_default(size=font_size) + + # 计算文字位置(居中) + bbox = draw.textbbox((0, 0), translated_text, font=font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + + x_offset = (width - text_width) // 2 - bbox[0] + y_offset = (height - text_height) // 2 - bbox[1] + else: + # 自动计算字体大小 + font, x_offset, y_offset = self.calculate_font_size_custom( + image, translated_text, width, height, box_item.font_path + ) + + # 确定文字颜色 + if box_item.text_color is not None: + text_color = box_item.text_color + else: + # 自动计算 + text_color = image_processor.get_text_color(bg_color) + + # 绘制翻译文本 + if font: + draw.text( + (x_min + x_offset, y_min + y_offset), + translated_text, + fill=text_color, + font=font + ) + + return image + + def calculate_font_size_custom(self, image, text, width, height, font_path=None): + """计算合适的字体大小(自定义字体路径)""" + from PIL import ImageDraw, ImageFont + + draw = ImageDraw.Draw(image) + font = None + x, y = 0, 0 + + # 使用指定字体路径或默认字体 + if font_path is None: + font_path = self.config.font_path + + for size in range(1, self.config.max_font_size): + try: + new_font = ImageFont.truetype(font_path, size=size) + except: + new_font = ImageFont.load_default(size=size) + + new_box = draw.textbbox((0, 0), text, font=new_font) + + new_w = new_box[2] - new_box[0] + new_h = new_box[3] - new_box[1] + + if new_w > width or new_h > height: + break + + font = new_font + w, h = new_w, new_h + box = new_box + x = (width - w) // 2 - box[0] + y = (height - h) // 2 - box[1] + + return font, x, y + + def get_output_path(self): + """生成输出路径""" + if not self.current_image_path: + return "output.jpg" + + path = Path(self.current_image_path) + output_dir = Path(self.config.output_folder) + output_dir.mkdir(exist_ok=True) + + output_name = f"{path.stem}-translated{path.suffix}" + return str(output_dir / output_name) + + def swap_languages(self): + """交换源语言和目标语言""" + source = self.source_lang_var.get() + target = self.target_lang_var.get() + self.source_lang_var.set(target) + self.target_lang_var.set(source) + + def save_config(self): + """保存配置到文件""" + try: + self.update_config_from_gui() + self.config.save() + messagebox.showinfo("成功", "配置已保存到 config.json") + logging.info("配置已保存") + except Exception as e: + messagebox.showerror("错误", f"保存配置失败: {e}") + logging.error(f"保存配置失败: {e}") + + def update_config_from_gui(self): + """从GUI更新配置对象""" + # 更新语言设置 + source_name = self.source_lang_var.get() + target_name = self.target_lang_var.get() + self.config.source_language = self.languages_dict.get(source_name, "en") + self.config.target_language = self.languages_dict.get(target_name, "zh-CN") + + # 更新高级设置 + self.config.ocr_confidence_threshold = self.confidence_var.get() + self.config.max_font_size = self.font_size_var.get() + + +def main(): + """主函数""" + root = tk.Tk() + app = ImageTranslatorGUI(root) + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/image_translator_optimized.py b/image_translator_optimized.py new file mode 100644 index 0000000..6d43b6c --- /dev/null +++ b/image_translator_optimized.py @@ -0,0 +1,534 @@ +import os +import json +import logging +from pathlib import Path +from typing import List, Tuple, Optional, Dict +from dataclasses import dataclass +from concurrent.futures import ThreadPoolExecutor, as_completed +from collections import Counter + +from PIL import Image, ImageDraw, ImageFont +from deep_translator import GoogleTranslator +import easyocr + + +# ==================== 配置管理 ==================== +@dataclass +class Config: + """配置类""" + # OCR配置 + ocr_languages: List[str] + model_storage_directory: str + ocr_width_threshold: float + ocr_confidence_threshold: float + ocr_decoder: str + + # 翻译配置 + source_language: str + target_language: str + + # 目录配置 + input_folder: str + output_folder: str + + # 图像处理配置 + background_margin: int + color_discoloration_strength: int + max_font_size: int + font_path: str # 字体文件路径 + + # 性能配置 + max_workers: int + + @classmethod + def from_file(cls, config_path: str = "config.json") -> 'Config': + """从配置文件加载配置""" + if os.path.exists(config_path): + with open(config_path, 'r', encoding='utf-8') as f: + config_data = json.load(f) + return cls(**config_data) + else: + # 默认配置 + return cls.get_default() + + @classmethod + def get_default(cls) -> 'Config': + """获取默认配置""" + return cls( + ocr_languages=["ch_sim", "en"], + model_storage_directory="model", + ocr_width_threshold=0.8, + ocr_confidence_threshold=0.4, + ocr_decoder="wordbeamsearch", + source_language="zh-CN", + target_language="en", + input_folder="input", + output_folder="output", + background_margin=10, + color_discoloration_strength=40, + max_font_size=500, + font_path="C:/Windows/Fonts/msyh.ttc", # 微软雅黑字体 + max_workers=4 + ) + + def save(self, config_path: str = "config.json"): + """保存配置到文件""" + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(self.__dict__, f, indent=4, ensure_ascii=False) + + +# ==================== 日志配置 ==================== +def setup_logging(log_file: str = "translator.log"): + """配置日志""" + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_file, encoding='utf-8'), + logging.StreamHandler() + ] + ) + + +# ==================== OCR处理器 ==================== +class OCRProcessor: + """OCR文字识别处理器""" + + def __init__(self, config: Config): + self.config = config + self.reader = None + + def initialize(self): + """初始化OCR读取器""" + try: + logging.info(f"初始化OCR读取器,支持语言: {self.config.ocr_languages}") + self.reader = easyocr.Reader( + self.config.ocr_languages, + model_storage_directory=self.config.model_storage_directory, + gpu=False, + download_enabled=True, + verbose=True + ) + logging.info("OCR读取器初始化成功") + except Exception as e: + logging.error(f"OCR初始化失败: {e}") + raise + + def extract_text_boxes(self, image_path: str) -> List[Tuple[List, str]]: + """ + 从图像中提取文字和位置 + + Args: + image_path: 图像路径 + + Returns: + [(坐标框, 文字), ...] + """ + if self.reader is None: + raise RuntimeError("OCR读取器未初始化,请先调用initialize()") + + try: + result = self.reader.readtext( + image_path, + width_ths=self.config.ocr_width_threshold, + decoder=self.config.ocr_decoder + ) + + # 过滤低置信度结果 + text_boxes = [ + (entry[0], entry[1]) + for entry in result + if entry[2] > self.config.ocr_confidence_threshold + ] + + logging.info(f"识别到 {len(text_boxes)} 个文本框") + return text_boxes + + except Exception as e: + logging.error(f"OCR识别失败: {e}") + return [] + + +# ==================== 翻译器 ==================== +class Translator: + """文本翻译器""" + + def __init__(self, config: Config): + self.config = config + self.translator = GoogleTranslator( + source=config.source_language, + target=config.target_language + ) + + def translate(self, text: str) -> Optional[str]: + """ + 翻译文本 + + Args: + text: 待翻译文本 + + Returns: + 翻译后的文本,失败返回None + """ + if not text or not text.strip(): + return None + + try: + translated = self.translator.translate(text) + logging.debug(f"翻译: '{text}' -> '{translated}'") + return translated + except Exception as e: + logging.error(f"翻译失败: {text}, 错误: {e}") + return None + + def translate_batch(self, texts: List[str]) -> List[Optional[str]]: + """批量翻译文本""" + return [self.translate(text) for text in texts] + + +# ==================== 图像处理器 ==================== +class ImageProcessor: + """图像处理器""" + + def __init__(self, config: Config): + self.config = config + + def calculate_font_size( + self, + image: Image.Image, + text: str, + width: int, + height: int + ) -> Tuple[Optional[ImageFont.FreeTypeFont], int, int]: + """ + 计算合适的字体大小 + + Returns: + (字体对象, x坐标, y坐标) + """ + draw = ImageDraw.Draw(image) + font = None + x, y = 0, 0 + + # 尝试加载指定的字体文件 + font_path = self.config.font_path + if not os.path.exists(font_path): + # 如果指定字体不存在,尝试常见的中文字体 + fallback_fonts = [ + "C:/Windows/Fonts/msyh.ttc", # 微软雅黑 + "C:/Windows/Fonts/simhei.ttf", # 黑体 + "C:/Windows/Fonts/simsun.ttc", # 宋体 + "C:/Windows/Fonts/arial.ttf", # Arial + ] + for fb_font in fallback_fonts: + if os.path.exists(fb_font): + font_path = fb_font + logging.info(f"使用备用字体: {font_path}") + break + + for size in range(1, self.config.max_font_size): + try: + new_font = ImageFont.truetype(font_path, size=size) + except Exception: + # 如果字体加载失败,使用默认字体 + new_font = ImageFont.load_default(size=size) + + new_box = draw.textbbox((0, 0), text, font=new_font) + + new_w = new_box[2] - new_box[0] + new_h = new_box[3] - new_box[1] + + if new_w > width or new_h > height: + break + + font = new_font + w, h = new_w, new_h + box = new_box + x = (width - w) // 2 - box[0] + y = (height - h) // 2 - box[1] + + return font, x, y + + def adjust_color(self, color: Tuple[int, int, int], strength: int) -> Tuple[int, int, int]: + """调整颜色亮度""" + r, g, b = color[:3] + r = max(0, min(255, r + strength)) + g = max(0, min(255, g + strength)) + b = max(0, min(255, b + strength)) + + # 避免纯白色 + if r == 255 and g == 255 and b == 255: + r, g, b = 245, 245, 245 + + return (r, g, b) + + def get_background_color( + self, + image: Image.Image, + x_min: int, + y_min: int, + x_max: int, + y_max: int + ) -> Tuple[int, int, int]: + """获取背景颜色""" + image = image.convert('RGBA') + margin = self.config.background_margin + + edge_region = image.crop(( + max(x_min - margin, 0), + max(y_min - margin, 0), + min(x_max + margin, image.width), + min(y_max + margin, image.height), + )) + + pixels = list(edge_region.getdata()) + opaque_pixels = [pixel[:3] for pixel in pixels if pixel[3] > 0] + + if not opaque_pixels: + background_color = (255, 255, 255) + else: + most_common = Counter(opaque_pixels).most_common(1)[0][0] + background_color = most_common + + background_color = self.adjust_color( + background_color, + self.config.color_discoloration_strength + ) + return background_color + + def get_text_color(self, background_color: Tuple[int, int, int]) -> str: + """根据背景颜色计算文字颜色""" + luminance = ( + 0.299 * background_color[0] + + 0.587 * background_color[1] + + 0.114 * background_color[2] + ) / 255 + + return "black" if luminance > 0.5 else "white" + + def get_bounding_box(self, coordinates: List[List[int]]) -> Tuple[int, int, int, int]: + """计算文本框的边界""" + x_min = min(coord[0] for coord in coordinates) + y_min = min(coord[1] for coord in coordinates) + x_max = max(coord[0] for coord in coordinates) + y_max = max(coord[1] for coord in coordinates) + return x_min, y_min, x_max, y_max + + def replace_text( + self, + image_path: str, + text_boxes: List[Tuple[List, str]], + translations: List[Optional[str]] + ) -> Image.Image: + """ + 在图像上替换文本 + + Args: + image_path: 图像路径 + text_boxes: OCR识别的文本框 + translations: 翻译后的文本 + + Returns: + 处理后的图像 + """ + image = Image.open(image_path) + draw = ImageDraw.Draw(image) + + for (coordinates, original_text), translated_text in zip(text_boxes, translations): + if translated_text is None: + continue + + # 获取边界框 + x_min, y_min, x_max, y_max = self.get_bounding_box(coordinates) + + # 获取背景颜色 + bg_color = self.get_background_color(image, x_min, y_min, x_max, y_max) + + # 覆盖原文本 + draw.rectangle(((x_min, y_min), (x_max, y_max)), fill=bg_color) + + # 计算字体和位置 + width, height = x_max - x_min, y_max - y_min + font, x_offset, y_offset = self.calculate_font_size( + image, translated_text, width, height + ) + + # 绘制翻译文本 + if font: + text_color = self.get_text_color(bg_color) + draw.text( + (x_min + x_offset, y_min + y_offset), + translated_text, + fill=text_color, + font=font + ) + + return image + + +# ==================== 主处理器 ==================== +class ImageTranslator: + """图像翻译主处理器""" + + def __init__(self, config: Config): + self.config = config + self.ocr = OCRProcessor(config) + self.translator = Translator(config) + self.image_processor = ImageProcessor(config) + + def initialize(self): + """初始化所有组件""" + self.ocr.initialize() + + # 确保输出目录存在 + os.makedirs(self.config.output_folder, exist_ok=True) + + def process_single_image(self, image_path: str, output_path: str) -> bool: + """ + 处理单张图片 + + Args: + image_path: 输入图片路径 + output_path: 输出图片路径 + + Returns: + 是否处理成功 + """ + try: + logging.info(f"开始处理: {image_path}") + + # OCR识别 + text_boxes = self.ocr.extract_text_boxes(image_path) + + if not text_boxes: + logging.warning(f"未识别到文本: {image_path}") + return False + + # 翻译 + texts = [text for _, text in text_boxes] + translations = self.translator.translate_batch(texts) + + # 替换文本 + result_image = self.image_processor.replace_text( + image_path, text_boxes, translations + ) + + # 保存 + result_image.save(output_path) + logging.info(f"处理完成,保存至: {output_path}") + + return True + + except Exception as e: + logging.error(f"处理失败 {image_path}: {e}", exc_info=True) + return False + + def process_folder(self, use_parallel: bool = True) -> Dict[str, int]: + """ + 批量处理文件夹中的图片 + + Args: + use_parallel: 是否使用并行处理 + + Returns: + 处理统计信息 + """ + input_path = Path(self.config.input_folder) + output_path = Path(self.config.output_folder) + + # 获取所有图片文件 + image_extensions = {'.jpg', '.jpeg', '.png', '.webp', '.bmp'} + image_files = [ + f for f in input_path.iterdir() + if f.is_file() and f.suffix.lower() in image_extensions + ] + + if not image_files: + logging.warning(f"未找到图片文件: {input_path}") + return {"total": 0, "success": 0, "failed": 0} + + logging.info(f"找到 {len(image_files)} 个图片文件") + + stats = {"total": len(image_files), "success": 0, "failed": 0} + + if use_parallel and len(image_files) > 1: + # 并行处理 + with ThreadPoolExecutor(max_workers=self.config.max_workers) as executor: + futures = {} + + for img_file in image_files: + output_file = output_path / f"{img_file.stem}-translated{img_file.suffix}" + future = executor.submit( + self.process_single_image, + str(img_file), + str(output_file) + ) + futures[future] = img_file.name + + for future in as_completed(futures): + filename = futures[future] + try: + success = future.result() + if success: + stats["success"] += 1 + else: + stats["failed"] += 1 + except Exception as e: + logging.error(f"处理失败 {filename}: {e}") + stats["failed"] += 1 + else: + # 串行处理 + for img_file in image_files: + output_file = output_path / f"{img_file.stem}-translated{img_file.suffix}" + success = self.process_single_image(str(img_file), str(output_file)) + + if success: + stats["success"] += 1 + else: + stats["failed"] += 1 + + return stats + + +# ==================== 主函数 ==================== +def main(): + """主函数""" + # 设置日志 + setup_logging() + + # 加载配置 + config = Config.get_default() + + # 如果配置文件不存在,创建默认配置 + if not os.path.exists("config.json"): + logging.info("创建默认配置文件: config.json") + config.save() + else: + config = Config.from_file() + + # 创建翻译器 + translator = ImageTranslator(config) + + try: + # 初始化 + translator.initialize() + + # 处理图片 + stats = translator.process_folder(use_parallel=True) + + # 输出统计 + logging.info(f"\n{'='*50}") + logging.info(f"处理完成!") + logging.info(f"总计: {stats['total']} 张") + logging.info(f"成功: {stats['success']} 张") + logging.info(f"失败: {stats['failed']} 张") + logging.info(f"{'='*50}") + + except KeyboardInterrupt: + logging.info("用户中断处理") + except Exception as e: + logging.error(f"程序异常: {e}", exc_info=True) + + +if __name__ == "__main__": + main() diff --git a/input/image-1.jpg b/input/image-1.jpg deleted file mode 100644 index 5325d34..0000000 Binary files a/input/image-1.jpg and /dev/null differ diff --git a/main.py b/main.py deleted file mode 100644 index 89e8e69..0000000 --- a/main.py +++ /dev/null @@ -1,197 +0,0 @@ -from PIL import Image, ImageDraw, ImageFont -from deep_translator import GoogleTranslator -import os, easyocr - - -def perform_ocr(image_path, reader): - # Perform OCR on the image - result = reader.readtext(image_path, width_ths = 0.8, decoder = 'wordbeamsearch') - - # Extract text and bounding boxes from the OCR result - extracted_text_boxes = [(entry[0], entry[1]) for entry in result if entry[2] > 0.4] - - return extracted_text_boxes - - -def get_font(image, text, width, height): - - # Default values at start - font_size = None # For font size - font = None # For object truetype with correct font size - box = None # For version 8.0.0 - x = 0 - y = 0 - - draw = ImageDraw.Draw(image) # Create a draw object - - # Test for different font sizes - for size in range(1, 500): - - # Create new font - new_font = ImageFont.load_default(size=font_size) - - # Calculate bbox for version 8.0.0 - new_box = draw.textbbox((0, 0), text, font=new_font) - - # Calculate width and height - new_w = new_box[2] - new_box[0] # Bottom - Top - new_h = new_box[3] - new_box[1] # Right - Left - - # If too big then exit with previous values - if new_w > width or new_h > height: - break - - # Set new current values as current values - font_size = size - font = new_font - box = new_box - w = new_w - h = new_h - - # Calculate position (minus margins in box) - x = (width - w) // 2 - box[0] # Minus left margin - y = (height - h) // 2 - box[1] # Minus top margin - - return font, x, y - - -def add_discoloration(color, strength): - r, g, b = color[:3] - r = max(0, min(255, r + strength)) - g = max(0, min(255, g + strength)) - b = max(0, min(255, b + strength)) - - if r == 255 and g == 255 and b == 255: - r, g, b = 245, 245, 245 - - return (r, g, b) - - -def get_background_color(image, x_min, y_min, x_max, y_max): - image = image.convert('RGBA') # Handle transparency - - margin = 10 - edge_region = image.crop(( - max(x_min - margin, 0), - max(y_min - margin, 0), - min(x_max + margin, image.width), - min(y_max + margin, image.height), - )) - - pixels = list(edge_region.getdata()) - opaque_pixels = [pixel[:3] for pixel in pixels if pixel[3] > 0] - - if not opaque_pixels: - background_color = (255, 255, 255) # fallback if all pixels are transparent - else: - from collections import Counter - most_common = Counter(opaque_pixels).most_common(1)[0][0] - background_color = most_common - - background_color = add_discoloration(background_color, 40) - return background_color - - -def get_text_fill_color(background_color): - # Calculate the luminance of the background color - luminance = ( - 0.299 * background_color[0] - + 0.587 * background_color[1] - + 0.114 * background_color[2] - ) / 255 - - # Determine the text color based on the background luminance - if luminance > 0.5: - return "black" # Use black text for light backgrounds - else: - return "white" # Use white text for dark backgrounds - - -def replace_text_with_translation(image_path, translated_texts, text_boxes): - # Open the image - image = Image.open(image_path) - draw = ImageDraw.Draw(image) - - # Load a font - font = ImageFont.load_default() - - # Replace each text box with translated text - for text_box, translated in zip(text_boxes, translated_texts): - - if translated is None: - continue - - # Set initial values - x_min, y_min = text_box[0][0][0], text_box[0][0][1] - x_max, y_max = text_box[0][0][0], text_box[0][0][1] - - for coordinate in text_box[0]: - - x, y = coordinate - - if x < x_min: - x_min = x - elif x > x_max: - x_max = x - if y < y_min: - y_min = y - elif y > y_max: - y_max = y - - # Find the most common color in the text region - background_color = get_background_color(image, x_min, y_min, x_max, y_max) - - # Draw a rectangle to cover the text region with the original background color - draw.rectangle(((x_min, y_min), (x_max, y_max)), fill=background_color) - - # Calculate font size, box - font, x, y = get_font(image, translated, x_max - x_min, y_max - y_min) - - # Draw the translated text within the box - draw.text( - (x_min + x, y_min + y), - translated, - fill=get_text_fill_color(background_color), - font=font, - ) - - return image - - -# Initialize the OCR reader -reader = easyocr.Reader(["ch_sim", "en"], model_storage_directory = 'model') - -# Initialize the Translator -translator = GoogleTranslator(source="zh-CN", target="en") - -# Define input and output location -input_folder = "input" -output_folder = "output" - -# Process each image file from input -files = os.listdir(input_folder) -image_files = [file for file in files if file.endswith((".jpg", ".jpeg", ".png"))] -for filename in image_files: - - print(f'[INFO] Processing {filename}...') - - image_path = os.path.join(input_folder, filename) - - # Extract text and location - extracted_text_boxes = perform_ocr(image_path, reader) - - # Translate texts - translated_texts = [] - for text_box, text in extracted_text_boxes: - translated_texts.append(translator.translate(text)) - - # Replace text with translated text - image = replace_text_with_translation(image_path, translated_texts, extracted_text_boxes) - - # Save modified image - base_filename, extension = os.path.splitext(filename) - output_filename = f"{base_filename}-translated{extension}" - output_path = os.path.join(output_folder, output_filename) - image.save(output_path) - - print(f'[INFO] Saved as {output_filename}...') diff --git a/model/craft_mlt_25k.pth b/model/craft_mlt_25k.pth new file mode 100644 index 0000000..7461d59 Binary files /dev/null and b/model/craft_mlt_25k.pth differ diff --git a/model/zh_sim_g2.pth b/model/zh_sim_g2.pth new file mode 100644 index 0000000..e7d9dad Binary files /dev/null and b/model/zh_sim_g2.pth differ diff --git a/output/image-1-translated.jpg b/output/image-1-translated.jpg deleted file mode 100644 index a7fa467..0000000 Binary files a/output/image-1-translated.jpg and /dev/null differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7dbb39e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,22 @@ +# Image Translator Requirements +# Python 3.8+ + +# OCR - 文字识别 +easyocr>=1.7.0 + +# Translation - 翻译服务 +deep-translator>=1.11.4 + +# Image Processing - 图像处理 +Pillow>=10.0.0 + +# Additional dependencies installed with EasyOCR: +# - torch (PyTorch) +# - torchvision +# - opencv-python-headless +# - scipy +# - scikit-image +# - python-bidi +# - pyyaml + +# Note: tkinter comes with Python standard library (no need to install)