Improve OCR speed and preprocessing

This commit is contained in:
messypy
2026-06-03 21:49:55 +09:00
committed by every_holiday
parent e8eb9463fa
commit 6f22223394
5 changed files with 195 additions and 13 deletions

View File

@@ -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

View File

@@ -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
新規の実行ログは基本的に以下へまとめる。

View File

@@ -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

View File

@@ -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:

View File

@@ -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():