From 330b79288b81e0b9badc6630e252e7cd85e15f69 Mon Sep 17 00:00:00 2001 From: Sivarius Date: Fri, 3 Jul 2026 16:59:26 +0800 Subject: [PATCH] Initial: Tesseract Web GUI with Flask + start.bat --- README.md | 59 ++++++ app.py | 148 ++++++++++++++ start.bat | 36 ++++ templates/index.html | 291 +++++++++++++++++++++++++++ uploads/47b13fb4_20260703_165917.png | Bin 0 -> 4109 bytes uploads/proc_ba6fc1d7.png | Bin 0 -> 6360 bytes 6 files changed, 534 insertions(+) create mode 100644 README.md create mode 100644 app.py create mode 100644 start.bat create mode 100644 templates/index.html create mode 100644 uploads/47b13fb4_20260703_165917.png create mode 100644 uploads/proc_ba6fc1d7.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..db0849c --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Tesseract Web GUI + +Простой локальный веб-сервис для OCR через Tesseract. + +## Структура + +``` +tesseract-webgui/ +├── app.py # Flask сервер +├── start.bat # Запуск на Windows +├── templates/ +│ └── index.html # Веб-интерфейс +└── uploads/ # Сюда сохраняются загруженные и обработанные картинки +``` + +## Запуск + +```bash +start.bat +``` + +Или вручную: + +```bash +cd tesseract-webgui +python -m pip install flask pillow pytesseract +python app.py +``` + +Затем открывай в браузере: **http://127.0.0.1:5000/** + +## Возможности + +- Загрузка картинки drag-&-drop или через кнопку +- Настройка языка (`rus+eng`, `eng`, и т.д.) +- PSM режимы (авто, блок текста, одна строка и др.) +- Предобработка: увеличение, контраст, резкость, бинаризация +- Whitelist символов +- Предпросмотр загруженной картинки +- Сохранение всех параметров OCR в выводе + +## Требования + +- Python 3.10+ +- Tesseract OCR (должен быть в `C:\Program Files\Tesseract-OCR\tesseract.exe`) +- Русский языковой пакет: `rus.traineddata` в `C:\Program Files\Tesseract-OCR\tessdata\` + +## Установка Tesseract + +```bash +winget install UB-Mannheim.TesseractOCR +``` + +Русский язык (если не установился): + +```bash +curl -L -o "%TEMP%\rus.traineddata" "https://github.com/tesseract-ocr/tessdata/raw/main/rus.traineddata" +copy "%TEMP%\rus.traineddata" "C:\Program Files\Tesseract-OCR\tessdata\" +``` diff --git a/app.py b/app.py new file mode 100644 index 0000000..65380ec --- /dev/null +++ b/app.py @@ -0,0 +1,148 @@ +""" +Tesseract Web GUI +Локальный веб-сервис для распознавания текста с картинок. +Запуск: start.bat +""" + +import os +import io +import uuid +import base64 +from pathlib import Path +from datetime import datetime + +from PIL import Image, ImageEnhance, ImageFilter, ImageOps +import pytesseract +from flask import Flask, render_template, request, jsonify, send_from_directory +from werkzeug.utils import secure_filename + +# ── Конфиг ───────────────────────────────────────────────────────────── +BASE_DIR = Path(__file__).resolve().parent +UPLOAD_DIR = BASE_DIR / "uploads" +UPLOAD_DIR.mkdir(exist_ok=True) + +# Путь к Tesseract (Windows) +TESSERACT_CMD = r"C:\Program Files\Tesseract-OCR\tesseract.exe" +if Path(TESSERACT_CMD).exists(): + pytesseract.pytesseract.tesseract_cmd = TESSERACT_CMD + +ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "bmp", "tiff", "webp", "gif"} + +app = Flask(__name__) +app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 16 MB + +# ── Хелперы ──────────────────────────────────────────────────────────── + +def allowed_file(filename: str) -> bool: + return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS + + +def preprocess_image(img: Image.Image, scale: int, contrast: float, sharpen: bool, binary: bool, binary_thresh: int) -> Image.Image: + """Предобработка перед OCR.""" + if scale > 1: + w, h = img.size + img = img.resize((w * scale, h * scale), Image.LANCZOS) + + img = img.convert("L") + + if contrast != 1.0: + img = ImageEnhance.Contrast(img).enhance(contrast) + + if sharpen: + img = img.filter(ImageFilter.SHARPEN) + + if binary: + img = img.point(lambda x: 0 if x < binary_thresh else 255) + + return img + + +def do_ocr(image_path: Path, lang: str, psm: int, + scale: int, contrast: float, sharpen: bool, + binary: bool, binary_thresh: int, + whitelist: str = "") -> dict: + """Выполняет OCR и возвращает словарь с результатом.""" + img = Image.open(image_path) + + # Для preview сохраняем оригинал как base64 + buf = io.BytesIO() + img.convert("RGB").save(buf, format="PNG") + preview_b64 = base64.b64encode(buf.getvalue()).decode("utf-8") + + # Предобработка + proc = preprocess_image(img, scale, contrast, sharpen, binary, binary_thresh) + + config = f"--psm {psm}" + if whitelist: + config += f' -c tessedit_char_whitelist="{whitelist}"' + + text = pytesseract.image_to_string(proc, lang=lang, config=config) + + # Сохраняем обработанную картинку + proc_id = uuid.uuid4().hex[:8] + proc_path = UPLOAD_DIR / f"proc_{proc_id}.png" + proc.save(proc_path) + + return { + "text": text, + "preview": preview_b64, + "proc_path": str(proc_path), + "config_used": f"lang={lang}, psm={psm}, scale={scale}, contrast={contrast}, sharpen={sharpen}, binary={binary}, thresh={binary_thresh}, whitelist={whitelist or 'none'}", + } + + +# ── Роуты ────────────────────────────────────────────────────────────── + +@app.route("/") +def index(): + return render_template("index.html") + + +@app.route("/ocr", methods=["POST"]) +def ocr(): + if "file" not in request.files: + return jsonify({"error": "Файл не загружен"}), 400 + + file = request.files["file"] + if file.filename == "" or not allowed_file(file.filename): + return jsonify({"error": "Неверный или пустой файл (разрешены PNG, JPG, JPEG, BMP, TIFF, WEBP, GIF)"}), 400 + + # Параметры из формы + lang = request.form.get("lang", "rus+eng").strip() + psm = int(request.form.get("psm", 6)) + scale = int(request.form.get("scale", 2)) + contrast = float(request.form.get("contrast", 2.0)) + sharpen = request.form.get("sharpen", "true").lower() in ("true", "1", "on") + binary = request.form.get("binary", "false").lower() in ("true", "1", "on") + binary_thresh = int(request.form.get("binary_thresh", 140)) + whitelist = request.form.get("whitelist", "").strip() + + # Сохраняем оригинал + ext = secure_filename(file.filename).rsplit(".", 1)[1].lower() + uid = uuid.uuid4().hex[:8] + original_path = UPLOAD_DIR / f"{uid}_{datetime.now():%Y%m%d_%H%M%S}.{ext}" + file.save(original_path) + + try: + result = do_ocr( + original_path, lang, psm, + scale, contrast, sharpen, + binary, binary_thresh, whitelist + ) + return jsonify(result) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/uploads/") +def uploads(filename): + return send_from_directory(UPLOAD_DIR, filename) + + +# ── Запуск ───────────────────────────────────────────────────────────── + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 5000)) + print(f"[Tesseract Web GUI] http://127.0.0.1:{port}/") + # debug=False, потому что иначе батник не закроется нормально + app.run(host="127.0.0.1", port=port, debug=False) diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..f25dc39 --- /dev/null +++ b/start.bat @@ -0,0 +1,36 @@ +@echo off +chcp 65001 > nul +title Tesseract Web GUI + +:: Путь к папке проекта +set APP_DIR=%~dp0 +cd /d "%APP_DIR%" + +:: Проверяем Python +where python > nul 2> nul +if errorlevel 1 ( + echo [Ошибка] Python не найден. Установи Python 3.10+ и добавь в PATH. + pause + exit /b 1 +) + +:: Установка зависимостей (пропустит если уже стоят) +echo [1/3] Проверка зависимостей... +python -m pip install -q flask pillow pytesseract + +:: Проверяем Tesseract +if not exist "C:\Program Files\Tesseract-OCR\tesseract.exe" ( + echo [Предупреждение] Tesseract не найден по пути C:\Program Files\Tesseract-OCR\tesseract.exe + echo Установи: winget install UB-Mannheim.TesseractOCR + pause + exit /b 1 +) + +:: Запуск +echo [2/3] Запуск веб-сервиса... +echo [3/3] Открой браузер: http://127.0.0.1:5000/ +echo. + +python app.py + +pause diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..823ebee --- /dev/null +++ b/templates/index.html @@ -0,0 +1,291 @@ + + + + + + Tesseract Web GUI + + + +
+

🔍 Tesseract Web GUI

+
Загрузи картинку — получи текст. Всё локально, без интернета.
+ +
+
+
+

1. Картинка

+
+ 📁 Перетащи сюда файл или кликни для выбора + +
+
+
+ +
+

2. Настройки OCR

+ + + + + + + + + + + + +
+ + +
+ +
+ + +
+ + + + + + + + +
+
+
+ +
+

3. Результат

+ +
+
+
+
+ + + + diff --git a/uploads/47b13fb4_20260703_165917.png b/uploads/47b13fb4_20260703_165917.png new file mode 100644 index 0000000000000000000000000000000000000000..a22147f3e91b18b397c0c2830694118386f4b0fb GIT binary patch literal 4109 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 x@;2%tJ>-E! literal 0 HcmV?d00001 diff --git a/uploads/proc_ba6fc1d7.png b/uploads/proc_ba6fc1d7.png new file mode 100644 index 0000000000000000000000000000000000000000..42f15efdd4b196a1ee9ce93ad63e0421c276eb8e GIT binary patch literal 6360 zcmd^^<6(8o^{kdW?fIO33&mJ$g$=?0}6siRx+K*|GY34tS}8Svl2uBuFOMNrOJR#rN-QIh zDXk}OQ6fmgMk-I{#Wv-jKwl8EWFKBKS0zV6a+L5Q2CsX3icO+2YLm1FxJXmcxZ*F(J_n8hhNTo*pMWHlYOH;p5n1;e!BfG}q z`&i(5Dsi%tG>1f*$a(Fm^&k#=7wPDdL0_sF_&lisihClJ$&W%J%|Q}>@PISDxma4w$n^lrBf+QAc~5vO}e zq;D*zOxx>8i=zXUu_m0B@l($LY^d}QgRaD*yTUk;0#y z3?nL#@q4IiPMIRxS@bPCvaqURFyPL=>!rC?Kw`X=LNsw4Mi=uzfEnDQg7NS2RPNRC z{EJ$Kr`f{W_!ak;a@cF7>+jBOjUWFRz*RjRX=$<(GvQDB)W5W!(Uw?R7a(OX@x_tO zUMyRt#bb%e6Sm)={VQ$sV=JUhvdzD#|GS;8BF2&tEgq73pK$y7@4{}MN@vlMAjBSN zh$C7ZaOp%k1CfV!(c*z$Ro}>?NbQqe9OCBHH9Nz3?5a(zv2_)ar2KX}hj6{YzKvh|`E$hgao!FC95GSAas(!u91drBQQ|%cPqtrmvR< z;Jx#QUCmhwF0`JRmX?I-`M0ayyu~R*PZdP(HyHTu2j+4xqS9E~;fBCZ3!=2$-*SeU zw_HVwMWmgIxo+0a3yxo^BnnW9)BRF~@QUt4(zAv}ksx&ziJ57FfG5J88xXRcbQN61#RP1Bq2H;(Ug$lJSV;&SI+@%~v>0auPaRz9?SRJH$;#Em zW{S((SCjH?3|itN-u{aLY%qcpLe;nkWdjV~!{KppCw7%3SIxw@v=6;$8m z#**≪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 literal 0 HcmV?d00001