双引擎OCR并发: Windows OCR + EasyOCR同时跑最多等10秒,谁先出用谁
之前: 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降级)。
This commit is contained in:
+84
-91
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user