| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- """
- OCR 识别类 — 纯识别,不操作手机
- 默认 RapidOCR (rapidocr_onnxruntime),本地免费,用于导航大字检测
- engine="cloud" 切换百度云 OCR(标准版,用于说明书等小字页面),失败自动回退本地
- """
- import json
- import re
- from pathlib import Path
- from typing import Optional, Union
- import cv2
- import numpy as np
- from paddleocr import PaddleOCR
- from rapidocr_onnxruntime import RapidOCR
- class OCR:
- """RapidOCR 封装,只负责图片文字识别,不参与设备操作"""
- def __init__(self):
- import os as _os
- _os.environ.setdefault("OMP_NUM_THREADS", "1")
- self._engine = RapidOCR(text_det_thresh=0.3, text_det_box_thresh=0.3)
- self._baidu_client = None # 百度OCR客户端,惰性初始化
- self.oom = False # 内存崩溃熔断标志:本地引擎OOM后置True,上层据此跳过依赖OCR的步骤
- # ── 百度云 OCR ──────────────────────────────────────────
- def _load_baidu_keys(self) -> dict:
- """从 config.json 或环境变量读取百度 OCR 密钥"""
- import os as _os
- cfg_path = Path(__file__).parent.parent / "config.json"
- cfg = {}
- if cfg_path.exists():
- try:
- with open(cfg_path, encoding="utf-8") as f:
- cfg = json.load(f)
- except Exception:
- pass
- ocr_cfg = cfg.get("baidu_ocr", {})
- return {
- "app_id": _os.environ.get("BAIDU_OCR_APP_ID") or ocr_cfg.get("app_id", ""),
- "api_key": _os.environ.get("BAIDU_OCR_API_KEY") or ocr_cfg.get("api_key", ""),
- "secret_key": _os.environ.get("BAIDU_OCR_SECRET_KEY") or ocr_cfg.get("secret_key", ""),
- }
- def _get_baidu_client(self):
- if self._baidu_client is None:
- from aip import AipOcr
- k = self._load_baidu_keys()
- if not (k["app_id"] and k["api_key"] and k["secret_key"]):
- raise RuntimeError("config.json 缺少 baidu_ocr 密钥")
- self._baidu_client = AipOcr(k["app_id"], k["api_key"], k["secret_key"])
- return self._baidu_client
- @staticmethod
- def _baidu_to_standard(words_result: list) -> list:
- """百度 words_result → 项目统一格式 [{text, confidence, bbox, box}]"""
- results = []
- for item in words_result or []:
- loc = item.get("location", {})
- l, t = loc.get("left", 0), loc.get("top", 0)
- w, h = loc.get("width", 0), loc.get("height", 0)
- prob = item.get("probability") or {}
- conf = prob.get("average") if isinstance(prob, dict) and prob.get("average") is not None else 1.0
- results.append({
- "text": item.get("words", ""),
- "confidence": round(float(conf), 4),
- # 四角点:左上、右上、右下、左下
- "bbox": [[l, t], [l + w, t], [l + w, t + h], [l, t + h]],
- # 简化坐标:[左上x, 左上y, 右下x, 右下y],方便直接查看
- "box": [l, t, l + w, t + h],
- })
- return results
- def recognize_cloud(
- self,
- image: Union[str, Path, np.ndarray],
- rect: Optional[list] = None,
- confidence: float = 0.1,
- pattern: Optional[str] = None,
- detail: str = "all",
- ) -> list:
- """百度云 OCR(通用文字识别标准版),返回格式与 recognize 一致"""
- if isinstance(image, (str, Path)):
- img = cv2.imread(str(image))
- if img is None:
- raise FileNotFoundError(f"无法读取图片: {image}")
- else:
- img = image
- if rect:
- l, t, r, b = rect
- img = img[t:b, l:r]
- client = self._get_baidu_client()
- # JPEG 压缩发送,减小体积(百度推荐);标准版上限 4M/4096px
- ok, enc = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 90])
- if not ok:
- raise RuntimeError("图片编码失败")
- # 标准版(高精度版 accurate 配额小、天天触发日限 error_code=17,已弃用;
- # 生僻字场景已由 PP-OCRv6 + 交叉验证覆盖)
- res = client.basicGeneral(enc.tobytes())
- words_result = res.get("words_result", []) if isinstance(res, dict) else []
- if isinstance(res, dict) and res.get("error_code"):
- print(f"[ocr] 标准版受限(error_code={res.get('error_code')}: {res.get('error_msg')})")
- words_result = []
- results = self._baidu_to_standard(words_result)
- if confidence:
- results = [r for r in results if r["confidence"] >= confidence]
- if pattern:
- results = [r for r in results if re.search(pattern, r["text"])]
- if detail == "text":
- return [r["text"] for r in results]
- elif detail == "first":
- return results[0]["text"] if results else None
- return results
- def recognize_license(self, image: Union[str, Path, np.ndarray], rect: Optional[list] = None) -> dict:
- """
- 百度营业执照专用接口(美团同款 /rest/2.0/ocr/v1/business_license)。
- 返回结构化字段 dict,如 {"注册号": "...", "公司名称": "...", "地址": "...", ...}
- 失败返回 {}
- Args:
- image: 图片路径或 numpy 数组
- rect: 裁剪区域 [left, top, right, bottom],不传则整图
- """
- if isinstance(image, (str, Path)):
- img = cv2.imread(str(image))
- if img is None:
- raise FileNotFoundError(f"无法读取图片: {image}")
- else:
- img = image
- if rect:
- l, t, r, b = rect
- img = img[t:b, l:r]
- ok, enc = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 90])
- if not ok:
- return {}
- client = self._get_baidu_client()
- try:
- res = client.businessLicense(enc.tobytes())
- except Exception as e:
- print(f"[ocr] 百度营业执照OCR失败: {e}")
- return {}
- words_result = res.get("words_result", {}) if isinstance(res, dict) else {}
- return {k: v.get("words", "") for k, v in (words_result or {}).items()}
- # ── 核心识别 ────────────────────────────────────────────
- def recognize(
- self,
- image: Union[str, Path, np.ndarray],
- rect: Optional[list] = None,
- confidence: float = 0.1,
- pattern: Optional[str] = None,
- detail: str = "all",
- engine: str = "local",
- ) -> list:
- """
- 对图片执行 OCR 识别
- Args:
- image: 图片路径(str/Path) 或 numpy 数组
- rect: 裁剪区域 [left, top, right, bottom],不传则全图
- confidence: 置信度阈值,默认 0.1
- pattern: 正则过滤,只返回匹配的文字
- detail: 'all'=返回完整结果, 'text'=只返回文字列表, 'first'=只返回第一个匹配文字
- engine: 'local'=RapidOCR(默认), 'cloud'=百度云OCR(失败自动回退本地)
- Returns:
- detail='all'时:
- [{'text': str, 'confidence': float,
- 'bbox': [[x1,y1],[x2,y1],[x2,y2],[x1,y2]]}, ...]
- detail='text'时:
- ['文字1', '文字2', ...]
- detail='first'时:
- str 或 None
- """
- if engine == "cloud":
- try:
- return self.recognize_cloud(image, rect, confidence, pattern, detail)
- except Exception as e:
- print(f"[ocr] 百度OCR失败,回退本地: {e}")
- # 1. 读取图片
- if isinstance(image, (str, Path)):
- img = cv2.imread(str(image))
- if img is None:
- raise FileNotFoundError(f"无法读取图片: {image}")
- img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
- elif isinstance(image, np.ndarray):
- img = image
- else:
- raise TypeError(f"不支持的图片类型: {type(image)}")
- # 2. 裁剪区域(在压缩前,用原始坐标)
- crop_offset_l, crop_offset_t = 0, 0
- if rect:
- l, t, r, b = rect
- img = img[t:b, l:r]
- crop_offset_l, crop_offset_t = l, t
- # 3. 压缩大图,避免ONNX OOM
- orig_h, orig_w = img.shape[:2]
- if orig_h < 1 or orig_w < 1:
- return [] if detail != "first" else ""
- scale = 1.0
- max_size = 960
- if max(orig_h, orig_w) > max_size:
- scale = max_size / max(orig_h, orig_w)
- img = cv2.resize(img, (max(1, int(orig_w * scale)), max(1, int(orig_h * scale))))
- # 4. 执行 OCR(内存崩溃时置熔断标志后抛出,由上层跳过依赖OCR的步骤)
- try:
- raw, _ = self._engine(img)
- except Exception as e:
- if any(k in str(e).lower() for k in ("bad allocation", "out of memory", "insufficient memory", "unable to allocate")):
- self.oom = True
- print("[ocr] 本地OCR内存崩溃,已熔断后续OCR步骤")
- raise
- # 5. 结构化 & 过滤,把坐标映射回原始图
- results = []
- for bbox, text, conf in (raw or []):
- if conf < confidence:
- continue
- if pattern and not re.search(pattern, text):
- continue
- pts = [[int(p[0] / scale) + crop_offset_l, int(p[1] / scale) + crop_offset_t] for p in bbox]
- results.append({
- "text": text,
- "confidence": round(float(conf), 4),
- # 四角点:左上、右上、右下、左下
- "bbox": pts,
- # 简化坐标:[左上x, 左上y, 右下x, 右下y],方便直接查看
- "box": [pts[0][0], pts[0][1], pts[2][0], pts[2][1]],
- })
- # 5. 按需返回格式
- if detail == "text":
- return [r["text"] for r in results]
- elif detail == "first":
- return results[0]["text"] if results else None
- return results # detail='all'
- # ── 快捷方法 ────────────────────────────────────────────
- def recognize_file(self, path: str, **kwargs) -> list:
- """从文件路径识别"""
- return self.recognize(path, **kwargs)
- def recognize_array(self, arr: np.ndarray, **kwargs) -> list:
- """从 numpy 数组识别"""
- return self.recognize(arr, **kwargs)
- if __name__ == "__main__":
- """
- 验证 OCR 对指定图片的识别效果(本地RapidOCR / 百度云高精度 / PaddleOCR 三引擎对比)。
- 用法: python steps/ocr.py <图片路径> [关键词]
- 例: python steps/ocr.py "C:/Users/18357/Downloads/image (5).png" 胃舒
- """
- import sys as _sys
- img_path = _sys.argv[1] if len(_sys.argv) > 1 else r"C:\Users\18357\Downloads\image.png"
- keyword = _sys.argv[2] if len(_sys.argv) > 2 else ""
- print(f"验证图片: {img_path}")
- print(f"过滤关键词: {keyword}\n")
- _ocr = OCR()
- # 1. 本地 RapidOCR
- print("=== 本地 RapidOCR ===")
- try:
- raw = _ocr.recognize(img_path, detail="all")
- hits = [r for r in raw if keyword in r["text"]]
- if not hits:
- print(" (未识别到含关键词的文字)")
- for r in hits:
- print(f" conf={r['confidence']:.2f} box={r['box']} [{r['text']}]")
- except Exception as e:
- print(f" 识别失败: {e}")
- # 2. 百度云标准版
- print("=== 百度云标准版 ===")
- try:
- raw_c = _ocr.recognize(img_path, detail="all", engine="cloud")
- hits = [r for r in raw_c if keyword in r["text"]]
- if not hits:
- print(" (未识别到含关键词的文字)")
- for r in hits:
- print(f" conf={r['confidence']:.2f} box={r['box']} [{r['text']}]")
- except Exception as e:
- print(f" 云端失败: {e}")
- # 3. PaddleOCR
- print("=== PaddleOCR ===")
- try:
- import cv2 as _cv2
- img = _cv2.imread(img_path)
- img = _cv2.cvtColor(img, _cv2.COLOR_BGR2RGB)
- _paddle = PaddleOCR(lang="ch", show_log=False)
- res = _paddle.ocr(img)
- hits = [line[1] for line in (res[0] or []) if keyword in line[1][0]]
- if not hits:
- print(" (未识别到含关键词的文字)")
- for text, conf in hits:
- print(f" conf={conf:.2f} [{text}]")
- except Exception as e:
- print(f" PaddleOCR失败: {e}")
|