From 6f2222339418d5a38253985dc7b63bd1f27497eb Mon Sep 17 00:00:00 2001 From: messypy Date: Wed, 3 Jun 2026 21:49:55 +0900 Subject: [PATCH] Improve OCR speed and preprocessing --- VRWT_Tool/VRC_OSC/.env.example | 8 +++ VRWT_Tool/VRC_OSC/readme.txt | 29 +++++++++ VRWT_Tool/VRC_OSC/src/common/env_config.py | 56 ++++++++++++++++ VRWT_Tool/VRC_OSC/src/ocr/ocr_actions.py | 64 ++++++++++++++++--- .../VRC_OSC/src/screen/screen_actions.py | 51 ++++++++++++++- 5 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 VRWT_Tool/VRC_OSC/src/common/env_config.py diff --git a/VRWT_Tool/VRC_OSC/.env.example b/VRWT_Tool/VRC_OSC/.env.example index 9576eb2..7874620 100644 --- a/VRWT_Tool/VRC_OSC/.env.example +++ b/VRWT_Tool/VRC_OSC/.env.example @@ -1,3 +1,11 @@ DISCORD_WEBHOOK_URL= VRCHAT_USERNAME= VRCHAT_PASSWORD= + +OCR_CROP_LEFT=0.05 +OCR_CROP_TOP=0.35 +OCR_CROP_RIGHT=0.95 +OCR_CROP_BOTTOM=0.95 +OCR_PREPROCESS=1 +OCR_SCALE=1.5 +OCR_USE_ANGLE_CLS=0 diff --git a/VRWT_Tool/VRC_OSC/readme.txt b/VRWT_Tool/VRC_OSC/readme.txt index 764e756..f120ef8 100644 --- a/VRWT_Tool/VRC_OSC/readme.txt +++ b/VRWT_Tool/VRC_OSC/readme.txt @@ -61,12 +61,39 @@ missing_count = 3 DISCORD_WEBHOOK_URL= VRCHAT_USERNAME= VRCHAT_PASSWORD= + +OCR_CROP_LEFT=0.05 +OCR_CROP_TOP=0.35 +OCR_CROP_RIGHT=0.95 +OCR_CROP_BOTTOM=0.95 +OCR_PREPROCESS=1 +OCR_SCALE=1.5 +OCR_USE_ANGLE_CLS=0 ``` `DISCORD_WEBHOOK_URL` は VRC ログ結果の Discord Webhook 通知に使う。 `VRCHAT_USERNAME` / `VRCHAT_PASSWORD` は `src/getUser.py` を使う場合だけ必要。 +OCR 設定は速度と精度の調整用。 + +`OCR_CROP_*` は VRChat ウィンドウ内の相対座標。`0.0` から `1.0` で指定する。デフォルトは画面下側の中央寄りだけを読む。 + +フル画面 OCR に戻す場合。 + +```env +OCR_CROP_LEFT=0 +OCR_CROP_TOP=0 +OCR_CROP_RIGHT=1 +OCR_CROP_BOTTOM=1 +``` + +`OCR_PREPROCESS=1` はグレースケール化、コントラスト補正、シャープ化、拡大を行う。 + +`OCR_SCALE` は前処理時の拡大率。大きいほど小さい文字に強くなるが遅くなる。 + +`OCR_USE_ANGLE_CLS=0` は角度分類を切って高速化する。縦書きや回転文字を読む必要がある場合だけ `1` にする。 + ## OSC パラメータ Avatar Parameters 側の名前と一致させる。 @@ -123,6 +150,8 @@ notice 条件に入った場合。 `translation` は OCR 結果を翻訳し、翻訳結果をクリップボードへコピーする。 +OCR はデフォルトで画面全体ではなく、画面下側の中央寄りを切り出して読む。会話ログや字幕を読む用途では速く、余計な UI 文字を拾いにくい。 + ## runtime 新規の実行ログは基本的に以下へまとめる。 diff --git a/VRWT_Tool/VRC_OSC/src/common/env_config.py b/VRWT_Tool/VRC_OSC/src/common/env_config.py new file mode 100644 index 0000000..e5fb774 --- /dev/null +++ b/VRWT_Tool/VRC_OSC/src/common/env_config.py @@ -0,0 +1,56 @@ +import os + +from common.project_paths import ENV_FILE + + +def readDotEnvValue(key): + if not ENV_FILE.exists(): + return "" + + for line in ENV_FILE.read_text(encoding="utf-8-sig", errors="replace").splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + if not stripped.startswith(key + "="): + continue + + value = stripped.split("=", 1)[1].strip() + return value.strip('"').strip("'") + + return "" + + +def getEnvValue(key, default=""): + value = os.environ.get(key, "").strip() + if value: + return value + + value = readDotEnvValue(key).strip() + if value: + return value + + return default + + +def getEnvBool(key, default=False): + value = getEnvValue(key, "").strip().lower() + if not value: + return default + + if value in ("1", "true", "yes", "on"): + return True + if value in ("0", "false", "no", "off"): + return False + + return default + + +def getEnvFloat(key, default=0.0): + value = getEnvValue(key, "").strip() + if not value: + return default + + try: + return float(value) + except ValueError: + return default diff --git a/VRWT_Tool/VRC_OSC/src/ocr/ocr_actions.py b/VRWT_Tool/VRC_OSC/src/ocr/ocr_actions.py index a9d106d..5edf1be 100644 --- a/VRWT_Tool/VRC_OSC/src/ocr/ocr_actions.py +++ b/VRWT_Tool/VRC_OSC/src/ocr/ocr_actions.py @@ -1,12 +1,6 @@ import os from datetime import datetime -import pyperclip -from paddleocr import PaddleOCR - -from common.runtime_log import appendRuntimeLog -from screen.screen_actions import captureOcrRegion - os.environ["FLAGS_enable_pir_api"] = "0" os.environ["FLAGS_use_onednn"] = "0" os.environ["FLAGS_use_mkldnn"] = "0" @@ -14,6 +8,17 @@ os.environ["FLAGS_use_onednn_bfloat16"] = "0" os.environ["MKLDNN_DISABLE_WORKSPACE"] = "1" os.environ["ONEDNN_VERBOSE"] = "0" +import pyperclip +from paddleocr import PaddleOCR +from PIL import Image +from PIL import ImageFilter +from PIL import ImageOps + +from common.env_config import getEnvBool +from common.env_config import getEnvFloat +from common.runtime_log import appendRuntimeLog +from screen.screen_actions import captureOcrRegion + _ocr_engine = None @@ -27,10 +32,17 @@ def getOcrEngine(): if _ocr_engine is None: log("INFO", "PaddleOCR initializing") - _ocr_engine = PaddleOCR( - lang="japan", - use_angle_cls=True, - ) + options = { + "lang": "japan", + "use_angle_cls": getEnvBool("OCR_USE_ANGLE_CLS", False), + "show_log": False, + } + + try: + _ocr_engine = PaddleOCR(**options) + except (TypeError, ValueError): + options.pop("show_log", None) + _ocr_engine = PaddleOCR(**options) return _ocr_engine @@ -99,6 +111,37 @@ def printOcrResult(text): print(line, flush=True) +def preprocessOcrImage(image_path): + enabled = getEnvBool("OCR_PREPROCESS", True) + if not enabled: + return image_path + + try: + image = Image.open(image_path) + image = image.convert("L") + image = ImageOps.autocontrast(image) + image = image.filter(ImageFilter.SHARPEN) + + scale = max(1.0, min(2.0, getEnvFloat("OCR_SCALE", 1.5))) + if scale > 1.0: + width, height = image.size + resampling = getattr( + getattr(Image, "Resampling", Image), + "LANCZOS", + ) + image = image.resize( + (int(width * scale), int(height * scale)), + resampling, + ) + + image.save(image_path) + log("INFO", f"OCR image preprocessed={image_path}") + except Exception as e: + log("ERROR", f"OCR preprocess failed detail={e}") + + return image_path + + def runPaddleOcr(engine, image_path): if hasattr(engine, "predict"): return engine.predict(str(image_path)) @@ -109,6 +152,7 @@ def runOcrFromImage(image_path, show_result=True): log("ACTION", f"PaddleOCR start image={image_path}") try: + image_path = preprocessOcrImage(image_path) engine = getOcrEngine() result = runPaddleOcr(engine, image_path) except Exception as e: diff --git a/VRWT_Tool/VRC_OSC/src/screen/screen_actions.py b/VRWT_Tool/VRC_OSC/src/screen/screen_actions.py index 65a9d2d..c738094 100644 --- a/VRWT_Tool/VRC_OSC/src/screen/screen_actions.py +++ b/VRWT_Tool/VRC_OSC/src/screen/screen_actions.py @@ -1,6 +1,7 @@ import pyautogui import pygetwindow as gw +from common.env_config import getEnvFloat from common.project_paths import RUNTIME_DIR SCREENSHOT_PATH = RUNTIME_DIR / "latest_screenshot.png" @@ -93,10 +94,44 @@ def getVrchatRegion(): return left, top, width, height -def captureVrchatWindowTo(image_path): +def clampRatio(value): + return max(0.0, min(1.0, value)) + + +def getOcrRegion(): + left, top, width, height = getVrchatRegion() + + # Default to the lower central area. It is much faster than OCRing the + # whole VRChat window and usually contains chat/subtitle text. + crop_left = clampRatio(getEnvFloat("OCR_CROP_LEFT", 0.05)) + crop_top = clampRatio(getEnvFloat("OCR_CROP_TOP", 0.35)) + crop_right = clampRatio(getEnvFloat("OCR_CROP_RIGHT", 0.95)) + crop_bottom = clampRatio(getEnvFloat("OCR_CROP_BOTTOM", 0.95)) + + if crop_right <= crop_left: + crop_left = 0.0 + crop_right = 1.0 + + if crop_bottom <= crop_top: + crop_top = 0.0 + crop_bottom = 1.0 + + region_left = left + int(width * crop_left) + region_top = top + int(height * crop_top) + region_width = max(1, int(width * (crop_right - crop_left))) + region_height = max(1, int(height * (crop_bottom - crop_top))) + + log( + "INFO", + f"OCR crop left={region_left} top={region_top} width={region_width} height={region_height}", + ) + return region_left, region_top, region_width, region_height + + +def captureRegionTo(image_path, region): image_path.parent.mkdir(parents=True, exist_ok=True) - left, top, width, height = getVrchatRegion() + left, top, width, height = region image = pyautogui.screenshot(region=(left, top, width, height)) image.save(image_path) @@ -105,12 +140,22 @@ def captureVrchatWindowTo(image_path): return image_path +def captureVrchatWindowTo(image_path): + return captureRegionTo( + image_path, + getVrchatRegion(), + ) + + def captureVrchatWindow(): return captureVrchatWindowTo(SCREENSHOT_PATH) def captureOcrRegion(): - return captureVrchatWindowTo(OCR_SCREENSHOT_PATH) + return captureRegionTo( + OCR_SCREENSHOT_PATH, + getOcrRegion(), + ) def captureScreen():