From 722d2a5438027f970f58e16126c537f9654ab70d Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Wed, 29 Jul 2026 17:51:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=8C=E5=BC=95=E6=93=8EOCR=E5=B9=B6?= =?UTF-8?q?=E5=8F=91:=20Windows=20OCR=20+=20EasyOCR=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E8=B7=91=E6=9C=80=E5=A4=9A=E7=AD=8910=E7=A7=92=EF=BC=8C?= =?UTF-8?q?=E8=B0=81=E5=85=88=E5=87=BA=E7=94=A8=E8=B0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前: Windows OCR串行等完→再串行等EasyOCR(微信端甚至无超时)。 EasyOCR子进程挂起=OCR整条链路都卡死。 改为 _ocr_dual_engine 方法: - Windows OCR + EasyOCR 通过 ThreadPoolExecutor 同时跑 - concurrent.futures.wait(timeout=10.0, ALL_COMPLETED) 等最多10秒 - 两引擎都在10秒内出结果→_pick_ocr_text质量对比选优 - 只有一个在10秒内出结果→直接用那个 - 都超时→返回空 - Windows OCR bad allocation 自动降级到原图重试(在worker线程内处理) 两个平台调用点(_ocr_read_qq / _ocr_read_wechat)都改为调_ocr_dual_engine。 微信端同时补上im_original备份(之前缺bad allocation降级)。 --- monitor.py | 175 +++++++++++++++++++++++++---------------------------- 1 file changed, 84 insertions(+), 91 deletions(-) diff --git a/monitor.py b/monitor.py index c98f7ee..a0e68b7 100644 --- a/monitor.py +++ b/monitor.py @@ -1049,6 +1049,85 @@ class MessageMonitor: ) return (win_text, chosen) if chosen == "windows" else (easy_text, chosen) + def _ocr_dual_engine(self, im_processed: Image.Image, im_original: Image.Image, platform: str) -> tuple: + """双引擎并发 OCR:Windows OCR + EasyOCR 同时跑,最多等 10 秒。 + + 哪個先出用哪個,兩個都出時 _pick_ocr_text 質量對比選優。 + + im_processed: 预处理后的图(2x resize + sharpen + contrast, RGBA) + im_original: 原始截图(bad allocation 降级用) + + 返回 (raw_text, engine_name) 含義同 _pick_ocr_text。 + """ + import concurrent.futures + + engine = self._ocr_ensure_engine() + if not engine: + return ("", "") + + win_text: list[str] = [""] + easy_text: list[str] = [""] + + def _win_worker(): + try: + im = im_processed + if im.mode != 'RGBA': + im = im.convert('RGBA') + sb = SoftwareBitmap(BitmapPixelFormat.RGBA8, im.width, im.height) + sb.copy_from_buffer(im.tobytes()) + text = engine.recognize_async(sb).get().text.strip() + win_text[0] = text + except (RuntimeError, OSError) as e: + estr = str(e) + if "bad allocation" in estr or "memory" in estr.lower(): + try: + im2 = im_original + if im2.mode != 'RGBA': + im2 = im2.convert('RGBA') + sb2 = SoftwareBitmap(BitmapPixelFormat.RGBA8, im2.width, im2.height) + sb2.copy_from_buffer(im2.tobytes()) + text2 = engine.recognize_async(sb2).get().text.strip() + win_text[0] = text2 + except Exception: + pass + # 其他异常静默处理 + + def _easy_worker(): + if not _HAS_EASYOCR or self._easyocr_broken: + return + img_np = _np.array(im_processed) + text = self._call_easyocr_safe(img_np, timeout=10) + if text: + easy_text[0] = text + + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as ex: + win_fut = ex.submit(_win_worker) + easy_fut = ex.submit(_easy_worker) + done, not_done = concurrent.futures.wait( + [win_fut, easy_fut], timeout=10.0, + return_when=concurrent.futures.ALL_COMPLETED + ) + for f in not_done: + f.cancel() + + wt = win_text[0] + et = easy_text[0] + + # 日志(原始输出逐行记录,用於長期對比兩引擎能力) + if wt: + for i, l in enumerate(wt.split('\n')): + if l.strip(): + logger.info(f"[ocr] [windows/原始] line[{i}]: {l.strip()[:120]!r}") + if et: + for i, l in enumerate(et.split('\n')): + if l.strip(): + logger.info(f"[ocr] [easyocr/原始] line[{i}]: {l.strip()[:120]!r}") + + if not wt and not et: + return ("", "") + + return self._pick_ocr_text(wt, et, im_processed) + def _ocr_read_qq(self) -> list: """OCR 截屏 QQ 消息区域 -> 双引擎(Windows OCR + EasyOCR)提取文本。 包含窗口验证 + 垃圾过滤 + 连续垃圾清缓存。 @@ -1179,64 +1258,8 @@ class MessageMonitor: except Exception as e: logger.debug(f"[ocr] 预处理失败(跳过): {e}") - # ── 双引擎 OCR ────────────────────────────────────── - # Windows OCR - if im.mode != 'RGBA': - im = im.convert('RGBA') - logger.debug("[ocr] Windows OCR 开始...") - try: - sb = SoftwareBitmap(BitmapPixelFormat.RGBA8, im.width, im.height) - sb.copy_from_buffer(im.tobytes()) - win_result = engine.recognize_async(sb).get() - win_text = win_result.text.strip() - logger.debug(f"[ocr] Windows OCR 完成: {len(win_text)} chars") - except (RuntimeError, OSError) as e: - estr = str(e) - if "bad allocation" in estr or "memory" in estr.lower(): - logger.debug(f"[ocr] Windows OCR bad allocation({im.width}x{im.height}),降级到原图重试") - # 用原图(跳过 2x resize)重试 - try: - im2 = im_original - if im2.mode != 'RGBA': - im2 = im2.convert('RGBA') - sb2 = SoftwareBitmap(BitmapPixelFormat.RGBA8, im2.width, im2.height) - sb2.copy_from_buffer(im2.tobytes()) - win_result2 = engine.recognize_async(sb2).get() - win_text = win_result2.text.strip() - im = im2 # EasyOCR 也用原图 - logger.debug(f"[ocr] Windows OCR 降级重试完成: {len(win_text)} chars") - except Exception as e2: - logger.debug(f"[ocr] Windows OCR 降级也失败: {e2}") - win_text = "" - else: - logger.debug(f"[ocr] Windows OCR 异常: {e}") - win_text = "" - - # EasyOCR 备用(GPU crash 隔离:通过子进程调用) - easy_text = "" - if _HAS_EASYOCR and not self._easyocr_broken: - logger.debug("[ocr] EasyOCR 子进程...") - img_np = _np.array(im) - easy_text = self._call_easyocr_safe(img_np, timeout=8) - if easy_text is None: - # None = 子进程崩溃或超时;超时已自动杀子进程,下次自动重启 - # 只有真正的崩溃(BrokenPipeError / OSError)才永久禁用 - easy_text = "" - logger.debug("[ocr] EasyOCR 无结果(超时或崩溃),使用 Windows OCR 结果") - elif easy_text: - logger.debug(f"[ocr] EasyOCR 识别完成: {len(easy_text)} chars") - - # 引擎原始输出逐行标记日志(用于长期对比两引擎能力) - if win_text: - for i, l in enumerate(win_text.split('\n')): - if l.strip(): - logger.info(f"[ocr] [windows/原始] line[{i}]: {l.strip()[:120]!r}") - if easy_text: - for i, l in enumerate(easy_text.split('\n')): - if l.strip(): - logger.info(f"[ocr] [easyocr/原始] line[{i}]: {l.strip()[:120]!r}") - - raw_text, chosen_engine = self._pick_ocr_text(win_text, easy_text, im) + # ── 双引擎并发 OCR(Windows OCR + EasyOCR 同时跑,最多等 10 秒)── + raw_text, chosen_engine = self._ocr_dual_engine(im, im_original, "qq") if not raw_text: return [] self._last_chosen_engine["qq"] = chosen_engine @@ -1325,6 +1348,7 @@ class MessageMonitor: self._ocr_unchanged.pop("wechat", None) # 图像预处理(同 QQ) + im_original = im # 保留原图,bad allocation 时降级 try: im = im.resize((im.width * 2, im.height * 2), Image.LANCZOS) im = ImageEnhance.Sharpness(im).enhance(1.8) @@ -1332,39 +1356,8 @@ class MessageMonitor: except Exception as e: logger.debug(f"[wechat] OCR 预处理失败(跳过): {e}") - # ── 双引擎 OCR ── - if im.mode != 'RGBA': - im = im.convert('RGBA') - logger.debug("[wechat] Windows OCR 开始...") - sb = SoftwareBitmap(BitmapPixelFormat.RGBA8, im.width, im.height) - sb.copy_from_buffer(im.tobytes()) - win_result = engine.recognize_async(sb).get() - win_text = win_result.text.strip() - logger.debug(f"[wechat] Windows OCR 完成: {len(win_text)} chars") - - easy_text = "" - if _HAS_EASYOCR and not self._easyocr_broken: - logger.debug("[wechat] EasyOCR 子进程...") - img_np = _np.array(im) - easy_text = self._call_easyocr_safe(img_np) - if easy_text is None: - self._easyocr_broken = True - easy_text = "" - logger.warning("[wechat] EasyOCR 子进程崩溃,已禁用(本次会话不再尝试)") - elif easy_text: - logger.debug(f"[wechat] EasyOCR 识别完成: {len(easy_text)} chars") - - # 引擎原始输出逐行标记日志(用于长期对比两引擎能力) - if win_text: - for i, l in enumerate(win_text.split('\n')): - if l.strip(): - logger.info(f"[wechat] [windows/原始] line[{i}]: {l.strip()[:120]!r}") - if easy_text: - for i, l in enumerate(easy_text.split('\n')): - if l.strip(): - logger.info(f"[wechat] [easyocr/原始] line[{i}]: {l.strip()[:120]!r}") - - raw_text, chosen_engine = self._pick_ocr_text(win_text, easy_text, im) + # ── 双引擎并发 OCR(Windows OCR + EasyOCR 同时跑,最多等 10 秒)── + raw_text, chosen_engine = self._ocr_dual_engine(im, im_original, "wechat") if not raw_text: return [] self._last_chosen_engine["wechat"] = chosen_engine