Improve OCR speed and preprocessing
This commit is contained in:
@@ -1,3 +1,11 @@
|
|||||||
DISCORD_WEBHOOK_URL=
|
DISCORD_WEBHOOK_URL=
|
||||||
VRCHAT_USERNAME=
|
VRCHAT_USERNAME=
|
||||||
VRCHAT_PASSWORD=
|
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
|
||||||
|
|||||||
@@ -61,12 +61,39 @@ missing_count = 3
|
|||||||
DISCORD_WEBHOOK_URL=
|
DISCORD_WEBHOOK_URL=
|
||||||
VRCHAT_USERNAME=
|
VRCHAT_USERNAME=
|
||||||
VRCHAT_PASSWORD=
|
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 通知に使う。
|
`DISCORD_WEBHOOK_URL` は VRC ログ結果の Discord Webhook 通知に使う。
|
||||||
|
|
||||||
`VRCHAT_USERNAME` / `VRCHAT_PASSWORD` は `src/getUser.py` を使う場合だけ必要。
|
`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 パラメータ
|
## OSC パラメータ
|
||||||
|
|
||||||
Avatar Parameters 側の名前と一致させる。
|
Avatar Parameters 側の名前と一致させる。
|
||||||
@@ -123,6 +150,8 @@ notice 条件に入った場合。
|
|||||||
|
|
||||||
`translation` は OCR 結果を翻訳し、翻訳結果をクリップボードへコピーする。
|
`translation` は OCR 結果を翻訳し、翻訳結果をクリップボードへコピーする。
|
||||||
|
|
||||||
|
OCR はデフォルトで画面全体ではなく、画面下側の中央寄りを切り出して読む。会話ログや字幕を読む用途では速く、余計な UI 文字を拾いにくい。
|
||||||
|
|
||||||
## runtime
|
## runtime
|
||||||
|
|
||||||
新規の実行ログは基本的に以下へまとめる。
|
新規の実行ログは基本的に以下へまとめる。
|
||||||
|
|||||||
56
VRWT_Tool/VRC_OSC/src/common/env_config.py
Normal file
56
VRWT_Tool/VRC_OSC/src/common/env_config.py
Normal 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
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime
|
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_enable_pir_api"] = "0"
|
||||||
os.environ["FLAGS_use_onednn"] = "0"
|
os.environ["FLAGS_use_onednn"] = "0"
|
||||||
os.environ["FLAGS_use_mkldnn"] = "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["MKLDNN_DISABLE_WORKSPACE"] = "1"
|
||||||
os.environ["ONEDNN_VERBOSE"] = "0"
|
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
|
_ocr_engine = None
|
||||||
|
|
||||||
|
|
||||||
@@ -27,10 +32,17 @@ def getOcrEngine():
|
|||||||
|
|
||||||
if _ocr_engine is None:
|
if _ocr_engine is None:
|
||||||
log("INFO", "PaddleOCR initializing")
|
log("INFO", "PaddleOCR initializing")
|
||||||
_ocr_engine = PaddleOCR(
|
options = {
|
||||||
lang="japan",
|
"lang": "japan",
|
||||||
use_angle_cls=True,
|
"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
|
return _ocr_engine
|
||||||
|
|
||||||
@@ -99,6 +111,37 @@ def printOcrResult(text):
|
|||||||
print(line, flush=True)
|
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):
|
def runPaddleOcr(engine, image_path):
|
||||||
if hasattr(engine, "predict"):
|
if hasattr(engine, "predict"):
|
||||||
return engine.predict(str(image_path))
|
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}")
|
log("ACTION", f"PaddleOCR start image={image_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
image_path = preprocessOcrImage(image_path)
|
||||||
engine = getOcrEngine()
|
engine = getOcrEngine()
|
||||||
result = runPaddleOcr(engine, image_path)
|
result = runPaddleOcr(engine, image_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import pyautogui
|
import pyautogui
|
||||||
import pygetwindow as gw
|
import pygetwindow as gw
|
||||||
|
|
||||||
|
from common.env_config import getEnvFloat
|
||||||
from common.project_paths import RUNTIME_DIR
|
from common.project_paths import RUNTIME_DIR
|
||||||
|
|
||||||
SCREENSHOT_PATH = RUNTIME_DIR / "latest_screenshot.png"
|
SCREENSHOT_PATH = RUNTIME_DIR / "latest_screenshot.png"
|
||||||
@@ -93,10 +94,44 @@ def getVrchatRegion():
|
|||||||
return left, top, width, height
|
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)
|
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 = pyautogui.screenshot(region=(left, top, width, height))
|
||||||
image.save(image_path)
|
image.save(image_path)
|
||||||
@@ -105,12 +140,22 @@ def captureVrchatWindowTo(image_path):
|
|||||||
return image_path
|
return image_path
|
||||||
|
|
||||||
|
|
||||||
|
def captureVrchatWindowTo(image_path):
|
||||||
|
return captureRegionTo(
|
||||||
|
image_path,
|
||||||
|
getVrchatRegion(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def captureVrchatWindow():
|
def captureVrchatWindow():
|
||||||
return captureVrchatWindowTo(SCREENSHOT_PATH)
|
return captureVrchatWindowTo(SCREENSHOT_PATH)
|
||||||
|
|
||||||
|
|
||||||
def captureOcrRegion():
|
def captureOcrRegion():
|
||||||
return captureVrchatWindowTo(OCR_SCREENSHOT_PATH)
|
return captureRegionTo(
|
||||||
|
OCR_SCREENSHOT_PATH,
|
||||||
|
getOcrRegion(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def captureScreen():
|
def captureScreen():
|
||||||
|
|||||||
Reference in New Issue
Block a user