From 0cc344d88dfedb2f6aa56f864a4f4192ac5c6d66 Mon Sep 17 00:00:00 2001 From: q Date: Sun, 8 Mar 2026 13:42:59 +0300 Subject: [PATCH] update --- README.md | 31 ++- __pycache__/app.cpython-313.pyc | Bin 25816 -> 28169 bytes app.py | 196 ++++++++++++------ .../paddle_ocr_backend.cpython-313.pyc | Bin 0 -> 9268 bytes core/paddle_ocr_backend.py | 171 +++++++++++++++ requirements-paddle.txt | 3 + run_service.sh | 8 + scripts/download_models.sh | 172 +++++++++++++++ 8 files changed, 513 insertions(+), 68 deletions(-) create mode 100644 core/__pycache__/paddle_ocr_backend.cpython-313.pyc create mode 100644 core/paddle_ocr_backend.py create mode 100644 requirements-paddle.txt create mode 100755 scripts/download_models.sh diff --git a/README.md b/README.md index df235dd..e423342 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ Python 3.6-compatible Flask service that accepts an image and returns detected t - Accepts JPEG/PNG/WebP/BMP/TIFF/GIF (first frame) via multipart or base64 JSON. - Optional Google Coral EdgeTPU detection for scene context (loaded once at start). - EAST text detector (OpenCV DNN) when model exists; otherwise OpenCV contour-based text-region fallback. -- OCR via `pytesseract` with per-block confidences; configurable languages. +- OCR via `pytesseract` (default) with per-block confidences; configurable languages. +- Optional `PaddleOCR` backend (`OCR_BACKEND=paddle`) for better text-on-photo quality. - Auto-selects `rus` or `eng` (via Tesseract OSD script detection) before full OCR when `OCR_LANG=eng+rus`. - If OCR result is empty, retries with fallback language/full-frame pass to improve recall. - Hard payload cap (10MB default) and graceful degradation if Coral/EAST unavailable. @@ -32,12 +33,28 @@ sudo apt-get install -y python3-opencv pip3 install -r requirements.txt ``` +Optional PaddleOCR backend (Python 3.8+ only): +```bash +pip3 install -r requirements-paddle.txt +``` + ## Models Place your models in `./models` (paths can be overridden via env): - EdgeTPU SSD: `ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite` - Labels: `coco_labels.txt` - EAST text detector (optional): `frozen_east_text_detection.pb` +Auto-download all models (Coral + EAST + Paddle + local `tessdata_best`): +```bash +./scripts/download_models.sh +``` + +Examples: +```bash +./scripts/download_models.sh --only-paddle +./scripts/download_models.sh --no-paddle +``` + ### Better OCR models (tessdata_best) ```bash sudo apt-get install -y wget @@ -56,9 +73,17 @@ OpenCV from JetPack repos (4.x) usually ships without CUDA dnn. To use CUDA, bui - `MODEL_EDGETPU_PATH` (default `./models/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite`) - `MODEL_LABELS_PATH` (default `./models/coco_labels.txt`) - `MODEL_EAST_PATH` (default `./models/frozen_east_text_detection.pb`) +- `OCR_BACKEND` (default `tesseract`; `tesseract|paddle|auto`) - `OCR_LANG` (default `eng+rus`; request language set for OCR) - `OCR_AUTO_LANG` (default `true`; auto-pick `eng` or `rus` by script when `OCR_LANG` contains both) - `OCR_CONFIG` (default `--oem 1 --psm 6`, forwarded to Tesseract) +- `PADDLE_LANG` (default `ru`; used when `OCR_BACKEND=paddle`) +- `PADDLE_USE_GPU` (default `false`) +- `PADDLE_USE_ANGLE_CLS` (default `false`) +- `PADDLE_DET_MODEL_DIR` (default `./models/paddle/PP-OCRv5_mobile_det_infer`) +- `PADDLE_REC_MODEL_DIR` (default `./models/paddle/eslav_PP-OCRv5_mobile_rec_infer`) +- `PADDLE_CLS_MODEL_DIR` (default empty) +- `PADDLE_CPU_THREADS` (default `4`) - `TESSDATA_PREFIX` (path to tessdata / tessdata_best if you install better models) - `OCR_UPSCALE` (default `1.5`, upscale factor before OCR; max 3.0) - `OCR_CLAHE` (default `true`, contrast-limited adaptive histogram) @@ -80,7 +105,7 @@ python3 app.py --host 0.0.0.0 --port 8000 Waitress or gunicorn can also serve the app if desired (not required). ## API -- `GET /health` -> `{ "status": "ok", "coral": true/false }` +- `GET /health` -> `{ "status": "ok", "coral": true/false, "paddle": true/false, "ocr_backend": "..." }` - `POST /v1/image2text` - Multipart: field `image` - JSON: `{ "image_base64": "" }` @@ -108,6 +133,7 @@ Example JSON response (fields may vary): "meta": { "latency_ms": 123, "coral_used": true, + "ocr_backend": "tesseract", "east_used": true, "text_locator": "east", "ocr_lang": "eng", @@ -121,6 +147,7 @@ Example JSON response (fields may vary): ## Notes - If Coral is absent or fails, the service still returns OCR with `coral_used=false`. +- If `OCR_BACKEND=paddle` but PaddleOCR package/models are unavailable, service falls back to Tesseract. - If EAST model is missing, service first tries OpenCV contour text regions and only then full-image OCR. - Keep images under `MAX_IMAGE_MB` (413 returned otherwise). - Optimize Tesseract languages to those you need to speed up OCR. diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index bf2035cace211c2ad047ef62dd17280fd06efb42..7969f80bad33756c7949982ea13bc1b83ad6e6e1 100644 GIT binary patch delta 8071 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 literal 0 HcmV?d00001 diff --git a/core/paddle_ocr_backend.py b/core/paddle_ocr_backend.py new file mode 100644 index 0000000..998edb2 --- /dev/null +++ b/core/paddle_ocr_backend.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +"""Optional PaddleOCR backend wrapper.""" +import os +import sys + +try: + from paddleocr import PaddleOCR +except ImportError: # pragma: no cover + PaddleOCR = None + + +class PaddleOCRBackend(object): + def __init__(self, + enable=False, + lang='ru', + use_angle_cls=False, + use_gpu=False, + det_model_dir='', + rec_model_dir='', + cls_model_dir='', + cpu_threads=0): + self.enable = bool(enable) + self.lang = lang or 'ru' + self.use_angle_cls = bool(use_angle_cls) + self.use_gpu = bool(use_gpu) + self.det_model_dir = det_model_dir or '' + self.rec_model_dir = rec_model_dir or '' + self.cls_model_dir = cls_model_dir or '' + self.cpu_threads = int(cpu_threads) if cpu_threads else 0 + + self.available = False + self.engine = None + self.last_error = '' + self._load() + + def _load(self): + if not self.enable: + return + if PaddleOCR is None: + self.last_error = 'paddleocr_not_installed' + print('[WARN] PaddleOCR backend requested but package is not installed', file=sys.stderr) + return + kwargs = { + 'lang': self.lang, + 'use_angle_cls': self.use_angle_cls, + 'use_gpu': self.use_gpu, + 'show_log': False, + } + det_dir = self._resolve_dir(self.det_model_dir, 'det') + rec_dir = self._resolve_dir(self.rec_model_dir, 'rec') + cls_dir = self._resolve_dir(self.cls_model_dir, 'cls') + if det_dir: + kwargs['det_model_dir'] = det_dir + if rec_dir: + kwargs['rec_model_dir'] = rec_dir + if cls_dir: + kwargs['cls_model_dir'] = cls_dir + if self.cpu_threads > 0: + kwargs['cpu_threads'] = self.cpu_threads + try: + self.engine = PaddleOCR(**kwargs) + self.available = True + print('[INFO] PaddleOCR backend ready (lang={})'.format(self.lang)) + except Exception as exc: + self.last_error = str(exc) + self.available = False + print('[WARN] Failed to initialize PaddleOCR backend: {}'.format(exc), file=sys.stderr) + + def run_ocr(self, img_rgb, return_boxes=True): + if not self.available or img_rgb is None: + return '', [] + try: + if img_rgb.ndim == 3 and img_rgb.shape[2] == 3: + img_bgr = img_rgb[:, :, ::-1].copy() + else: + img_bgr = img_rgb.copy() + raw = self.engine.ocr(img_bgr, cls=self.use_angle_cls) + except Exception as exc: + print('[WARN] PaddleOCR inference failed: {}'.format(exc), file=sys.stderr) + return '', [] + return self._parse_result(raw, return_boxes=return_boxes) + + def _parse_result(self, raw, return_boxes=True): + lines = [] + self._walk_lines(raw, lines) + blocks = [] + texts = [] + for line in lines: + poly = line[0] + rec = line[1] + text = self._safe_text(rec[0] if len(rec) > 0 else '') + if not text: + continue + conf = self._safe_float(rec[1] if len(rec) > 1 else -1.0, default=-1.0) + bbox = self._poly_to_bbox(poly) + texts.append(text) + if return_boxes: + blocks.append({ + 'bbox': [bbox[0], bbox[1], bbox[2], bbox[3]], + 'text': text, + 'conf': conf, + }) + return '\n'.join(texts), blocks if return_boxes else [] + + def _walk_lines(self, node, out): + if self._is_line_entry(node): + out.append(node) + return + if isinstance(node, (list, tuple)): + for item in node: + self._walk_lines(item, out) + + def _is_line_entry(self, node): + if not isinstance(node, (list, tuple)) or len(node) < 2: + return False + poly = node[0] + rec = node[1] + return self._is_poly(poly) and self._is_rec(rec) + + def _is_poly(self, poly): + if not isinstance(poly, (list, tuple)) or len(poly) < 3: + return False + first = poly[0] + return isinstance(first, (list, tuple)) and len(first) >= 2 + + def _is_rec(self, rec): + if not isinstance(rec, (list, tuple)) or len(rec) < 1: + return False + return isinstance(rec[0], (str, bytes)) + + def _poly_to_bbox(self, poly): + xs = [] + ys = [] + for pt in poly: + if not isinstance(pt, (list, tuple)) or len(pt) < 2: + continue + xs.append(self._safe_int(pt[0], 0)) + ys.append(self._safe_int(pt[1], 0)) + if not xs or not ys: + return (0, 0, 0, 0) + return (max(0, min(xs)), max(0, min(ys)), max(xs), max(ys)) + + def _resolve_dir(self, path, name): + if not path: + return '' + if os.path.isdir(path): + return path + print('[WARN] PaddleOCR {} model dir not found: {}'.format(name, path), file=sys.stderr) + return '' + + def _safe_text(self, value): + if value is None: + return '' + if isinstance(value, bytes): + try: + value = value.decode('utf-8', 'ignore') + except Exception: + return '' + return str(value).strip() + + def _safe_float(self, value, default=0.0): + try: + return float(value) + except Exception: + return float(default) + + def _safe_int(self, value, default=0): + try: + return int(round(float(value))) + except Exception: + return int(default) diff --git a/requirements-paddle.txt b/requirements-paddle.txt new file mode 100644 index 0000000..d885cd6 --- /dev/null +++ b/requirements-paddle.txt @@ -0,0 +1,3 @@ +# Optional OCR backend dependencies. +# PaddleOCR 3.x supports Python 3.8+. +paddleocr>=3.0.0 diff --git a/run_service.sh b/run_service.sh index af308cc..6af84c3 100755 --- a/run_service.sh +++ b/run_service.sh @@ -8,9 +8,17 @@ set -euo pipefail export MODEL_EDGETPU_PATH=${MODEL_EDGETPU_PATH:-./models/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite} export MODEL_LABELS_PATH=${MODEL_LABELS_PATH:-./models/coco_labels.txt} export MODEL_EAST_PATH=${MODEL_EAST_PATH:-./models/frozen_east_text_detection.pb} +export OCR_BACKEND=${OCR_BACKEND:-tesseract} # tesseract|paddle|auto export OCR_LANG=${OCR_LANG:-eng+rus} export OCR_AUTO_LANG=${OCR_AUTO_LANG:-true} export OCR_CONFIG=${OCR_CONFIG:---oem 1 --psm 6} +export PADDLE_LANG=${PADDLE_LANG:-ru} +export PADDLE_USE_GPU=${PADDLE_USE_GPU:-false} +export PADDLE_USE_ANGLE_CLS=${PADDLE_USE_ANGLE_CLS:-false} +export PADDLE_DET_MODEL_DIR=${PADDLE_DET_MODEL_DIR:-./models/paddle/PP-OCRv5_mobile_det_infer} +export PADDLE_REC_MODEL_DIR=${PADDLE_REC_MODEL_DIR:-./models/paddle/eslav_PP-OCRv5_mobile_rec_infer} +export PADDLE_CLS_MODEL_DIR=${PADDLE_CLS_MODEL_DIR:-} +export PADDLE_CPU_THREADS=${PADDLE_CPU_THREADS:-4} # Point to tessdata_best if installed export TESSDATA_PREFIX=${TESSDATA_PREFIX:-} export MAX_IMAGE_MB=${MAX_IMAGE_MB:-10} diff --git a/scripts/download_models.sh b/scripts/download_models.sh new file mode 100755 index 0000000..02ca6a8 --- /dev/null +++ b/scripts/download_models.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +# Download OCR/object models used by the service. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +MODELS_DIR="${MODELS_DIR:-$ROOT_DIR/models}" +PADDLE_DIR="${PADDLE_DIR:-$MODELS_DIR/paddle}" +TESSDATA_LOCAL_DIR="${TESSDATA_LOCAL_DIR:-$MODELS_DIR/tessdata_best}" + +DOWNLOAD_CORAL=1 +DOWNLOAD_EAST=1 +DOWNLOAD_PADDLE=1 +DOWNLOAD_TESSDATA=1 + +usage() { + cat <<'EOF' +Usage: + ./scripts/download_models.sh [options] + +Options: + --only-coral Download only Coral model + labels + --only-east Download only EAST text detector model + --only-paddle Download only PaddleOCR models + --only-tessdata Download only local tessdata_best (eng+rus) + --no-coral Skip Coral model download + --no-east Skip EAST model download + --no-paddle Skip PaddleOCR model download + --no-tessdata Skip local tessdata_best download + -h, --help Show this help + +Environment overrides: + MODELS_DIR=/abs/path/to/models + PADDLE_DIR=/abs/path/to/models/paddle + TESSDATA_LOCAL_DIR=/abs/path/to/tessdata_best +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --only-coral) + DOWNLOAD_CORAL=1; DOWNLOAD_EAST=0; DOWNLOAD_PADDLE=0; DOWNLOAD_TESSDATA=0 + shift + ;; + --only-east) + DOWNLOAD_CORAL=0; DOWNLOAD_EAST=1; DOWNLOAD_PADDLE=0; DOWNLOAD_TESSDATA=0 + shift + ;; + --only-paddle) + DOWNLOAD_CORAL=0; DOWNLOAD_EAST=0; DOWNLOAD_PADDLE=1; DOWNLOAD_TESSDATA=0 + shift + ;; + --only-tessdata) + DOWNLOAD_CORAL=0; DOWNLOAD_EAST=0; DOWNLOAD_PADDLE=0; DOWNLOAD_TESSDATA=1 + shift + ;; + --no-coral) + DOWNLOAD_CORAL=0 + shift + ;; + --no-east) + DOWNLOAD_EAST=0 + shift + ;; + --no-paddle) + DOWNLOAD_PADDLE=0 + shift + ;; + --no-tessdata) + DOWNLOAD_TESSDATA=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "[ERROR] Unknown option: $1" >&2 + usage + exit 1 + ;; + esac +done + +mkdir -p "$MODELS_DIR" "$PADDLE_DIR" "$TESSDATA_LOCAL_DIR" + +if command -v curl >/dev/null 2>&1; then + DOWNLOADER="curl" +elif command -v wget >/dev/null 2>&1; then + DOWNLOADER="wget" +else + echo "[ERROR] curl or wget is required" >&2 + exit 1 +fi + +download_file() { + local url="$1" + local dst="$2" + mkdir -p "$(dirname "$dst")" + if [[ -s "$dst" ]]; then + echo "[SKIP] $dst already exists" + return 0 + fi + local tmp="${dst}.part" + echo "[GET] $url" + if [[ "$DOWNLOADER" == "curl" ]]; then + curl -L --fail --retry 3 --connect-timeout 20 --max-time 600 "$url" -o "$tmp" + else + wget -O "$tmp" "$url" + fi + mv "$tmp" "$dst" +} + +download_and_extract_tar() { + local url="$1" + local tag="$2" + local out_dir="$3" + local cache_dir="$MODELS_DIR/.downloads" + local archive="$cache_dir/${tag}.tar" + mkdir -p "$cache_dir" "$out_dir" + download_file "$url" "$archive" + echo "[EXTRACT] $archive -> $out_dir" + tar -xf "$archive" -C "$out_dir" +} + +# ---- URLs ---- +CORAL_MODEL_URL="https://github.com/google-coral/test_data/raw/master/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite" +CORAL_LABELS_URL="https://github.com/google-coral/test_data/raw/master/coco_labels.txt" +EAST_URL="https://github.com/argman/EAST/releases/download/model/frozen_east_text_detection.pb" +TESS_ENG_URL="https://github.com/tesseract-ocr/tessdata_best/raw/main/eng.traineddata" +TESS_RUS_URL="https://github.com/tesseract-ocr/tessdata_best/raw/main/rus.traineddata" + +# PaddleOCR v5 mobile models (detection + RU/EN recognition) +PADDLE_DET_URL="https://paddle-model-ecology.bj.bcebos.com/paddlex/official_inference_model/paddle3.0.0/PP-OCRv5_mobile_det_infer.tar" +PADDLE_REC_URL="https://paddle-model-ecology.bj.bcebos.com/paddlex/official_inference_model/paddle3.0.0/eslav_PP-OCRv5_mobile_rec_infer.tar" + +if [[ "$DOWNLOAD_CORAL" == "1" ]]; then + download_file "$CORAL_MODEL_URL" "$MODELS_DIR/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite" + download_file "$CORAL_LABELS_URL" "$MODELS_DIR/coco_labels.txt" +fi + +if [[ "$DOWNLOAD_EAST" == "1" ]]; then + download_file "$EAST_URL" "$MODELS_DIR/frozen_east_text_detection.pb" +fi + +if [[ "$DOWNLOAD_PADDLE" == "1" ]]; then + download_and_extract_tar "$PADDLE_DET_URL" "PP-OCRv5_mobile_det_infer" "$PADDLE_DIR" + download_and_extract_tar "$PADDLE_REC_URL" "eslav_PP-OCRv5_mobile_rec_infer" "$PADDLE_DIR" +fi + +if [[ "$DOWNLOAD_TESSDATA" == "1" ]]; then + download_file "$TESS_ENG_URL" "$TESSDATA_LOCAL_DIR/eng.traineddata" + download_file "$TESS_RUS_URL" "$TESSDATA_LOCAL_DIR/rus.traineddata" +fi + +cat <