Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee82b161eb | ||
|
|
3c17405a6b | ||
|
|
915b5c9f45 | ||
|
|
a70499edfa | ||
|
|
1f2024e85f |
22
run-debug.bat
Normal file
22
run-debug.bat
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
@echo off
|
||||||
|
REM Run the exe with output captured. Window does not auto-close.
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0"
|
||||||
|
echo Running sephiria_inv.exe ...
|
||||||
|
echo.
|
||||||
|
sephiria_inv.exe > sephiria_inv_console.log 2>&1
|
||||||
|
set EC=%errorlevel%
|
||||||
|
echo.
|
||||||
|
echo === sephiria_inv.exe exited with code %EC% ===
|
||||||
|
echo.
|
||||||
|
echo --- sephiria_inv_console.log ---
|
||||||
|
if exist sephiria_inv_console.log type sephiria_inv_console.log
|
||||||
|
echo --- end console.log ---
|
||||||
|
echo.
|
||||||
|
echo --- sephiria_inv_startup.log ---
|
||||||
|
if exist sephiria_inv_startup.log type sephiria_inv_startup.log
|
||||||
|
echo --- end startup.log ---
|
||||||
|
echo.
|
||||||
|
echo Press any key to close this window.
|
||||||
|
pause > nul
|
||||||
|
endlocal
|
||||||
114
run.py
114
run.py
@@ -1,6 +1,116 @@
|
|||||||
"""PyInstaller entry shim."""
|
"""PyInstaller entry shim with aggressive startup diagnostics.
|
||||||
|
|
||||||
|
Writes a log entry the moment this module is loaded (before any project
|
||||||
|
imports). Logs to two locations:
|
||||||
|
1. Next to the exe (sys.executable directory) -- usually the user's
|
||||||
|
Downloads folder; trivially findable.
|
||||||
|
2. %LOCALAPPDATA%/sephiria_inv/startup.log
|
||||||
|
|
||||||
|
Either log existing tells us Python started. Neither existing means the
|
||||||
|
PyInstaller bootloader died before Python ran (DLL/runtime issue).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def _candidate_log_paths():
|
||||||
|
paths = []
|
||||||
|
# 1. Next to the exe (most discoverable for the user)
|
||||||
|
try:
|
||||||
|
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
|
||||||
|
if exe_dir:
|
||||||
|
paths.append(os.path.join(exe_dir, "sephiria_inv_startup.log"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# 2. LOCALAPPDATA / home
|
||||||
|
try:
|
||||||
|
base = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
|
||||||
|
folder = os.path.join(base, "sephiria_inv")
|
||||||
|
os.makedirs(folder, exist_ok=True)
|
||||||
|
paths.append(os.path.join(folder, "startup.log"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def _write_log(msg: str) -> list:
|
||||||
|
written = []
|
||||||
|
stamp = datetime.now().isoformat()
|
||||||
|
for path in _candidate_log_paths():
|
||||||
|
try:
|
||||||
|
with open(path, "a", encoding="utf-8") as fh:
|
||||||
|
fh.write(f"\n=== {stamp} ===\n{msg}\n")
|
||||||
|
written.append(path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return written
|
||||||
|
|
||||||
|
|
||||||
|
def _env_dump() -> str:
|
||||||
|
lines = [
|
||||||
|
f"python: {sys.version}",
|
||||||
|
f"executable: {sys.executable}",
|
||||||
|
f"argv: {sys.argv}",
|
||||||
|
f"frozen: {getattr(sys, 'frozen', False)}",
|
||||||
|
f"_MEIPASS: {getattr(sys, '_MEIPASS', '<none>')}",
|
||||||
|
f"cwd: {os.getcwd()}",
|
||||||
|
f"PATH head: {os.environ.get('PATH', '')[:200]}",
|
||||||
|
f"LOCALAPPDATA: {os.environ.get('LOCALAPPDATA', '<none>')}",
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# === MODULE-IMPORT BREADCRUMB ===
|
||||||
|
# This runs the moment Python loads the entry script. If this never appears
|
||||||
|
# in either log, the PyInstaller bootloader failed before Python started.
|
||||||
|
_BOOT_LOGS = _write_log("BOOT: run.py loaded\n" + _env_dump())
|
||||||
|
|
||||||
|
|
||||||
|
def _show_error(title: str, body: str) -> None:
|
||||||
|
try:
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import scrolledtext
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title(title)
|
||||||
|
root.geometry("780x460")
|
||||||
|
txt = scrolledtext.ScrolledText(root, wrap="word")
|
||||||
|
txt.pack(fill="both", expand=True)
|
||||||
|
txt.insert("1.0", body)
|
||||||
|
tk.Button(root, text="Close", command=root.destroy).pack(pady=4)
|
||||||
|
root.mainloop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _main() -> int:
|
||||||
|
try:
|
||||||
from sephiria_inv.__main__ import main
|
from sephiria_inv.__main__ import main
|
||||||
|
except BaseException:
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
logs = _write_log("IMPORT FAIL\n" + tb + "\n--env--\n" + _env_dump())
|
||||||
|
_show_error(
|
||||||
|
"sephiria_inv: import failed",
|
||||||
|
f"Failed to import sephiria_inv.__main__\n\nLogs: {logs}\n\n{tb}",
|
||||||
|
)
|
||||||
|
return 11
|
||||||
|
try:
|
||||||
|
return int(main() or 0)
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except BaseException:
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
logs = _write_log("RUNTIME FAIL\n" + tb)
|
||||||
|
_show_error(
|
||||||
|
"sephiria_inv: runtime error",
|
||||||
|
f"Crashed during main()\n\nLogs: {logs}\n\n{tb}",
|
||||||
|
)
|
||||||
|
return 12
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(_main())
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ class ScreenshotFrame(ttk.Frame):
|
|||||||
def __init__(self, master, on_confirmed) -> None:
|
def __init__(self, master, on_confirmed) -> None:
|
||||||
super().__init__(master, padding=6)
|
super().__init__(master, padding=6)
|
||||||
self.on_confirmed = on_confirmed
|
self.on_confirmed = on_confirmed
|
||||||
self.slot_var = master.master.slot_var # type: ignore[attr-defined]
|
self.slot_var = master.winfo_toplevel().slot_var # type: ignore[attr-defined]
|
||||||
self.image: Optional[Image.Image] = None
|
self.image: Optional[Image.Image] = None
|
||||||
self.bbox: Optional[Tuple[int, int, int, int]] = None
|
self.bbox: Optional[Tuple[int, int, int, int]] = None
|
||||||
self.cells: List[CellResult] = []
|
self.cells: List[CellResult] = []
|
||||||
@@ -409,11 +409,12 @@ class ScreenshotFrame(ttk.Frame):
|
|||||||
|
|
||||||
ctl = ttk.Frame(self)
|
ctl = ttk.Frame(self)
|
||||||
ctl.pack(fill="x")
|
ctl.pack(fill="x")
|
||||||
ttk.Button(ctl, text="🎮 게임 창 선택…", command=self._pick_window).pack(side="left")
|
ttk.Button(ctl, text="게임 창 선택…", command=self._pick_window).pack(side="left")
|
||||||
ttk.Button(ctl, text="🖥 전체 화면 캡처", command=self._capture_screen).pack(side="left", padx=4)
|
ttk.Button(ctl, text="전체 화면 캡처", command=self._capture_screen).pack(side="left", padx=4)
|
||||||
ttk.Button(ctl, text="📂 파일 열기…", command=self._open_file).pack(side="left", padx=4)
|
ttk.Button(ctl, text="파일 열기…", command=self._open_file).pack(side="left", padx=4)
|
||||||
ttk.Button(ctl, text="🔁 영역 재지정", command=self._reselect_bbox).pack(side="left", padx=4)
|
ttk.Button(ctl, text="영역 재지정", command=self._reselect_bbox).pack(side="left", padx=4)
|
||||||
ttk.Button(ctl, text="✅ 이 구성으로 계산", command=self._confirm).pack(side="right")
|
ttk.Button(ctl, text="디버그 저장", command=self._save_debug).pack(side="left", padx=4)
|
||||||
|
ttk.Button(ctl, text="이 구성으로 계산", command=self._confirm).pack(side="right")
|
||||||
|
|
||||||
self.status = ttk.Label(
|
self.status = ttk.Label(
|
||||||
self,
|
self,
|
||||||
@@ -506,6 +507,8 @@ class ScreenshotFrame(ttk.Frame):
|
|||||||
# First call may download a lot — keep artifacts on (user wants them)
|
# First call may download a lot — keep artifacts on (user wants them)
|
||||||
warm_templates(include_artifacts=True)
|
warm_templates(include_artifacts=True)
|
||||||
self._templates_warmed = True
|
self._templates_warmed = True
|
||||||
|
from .recognizer import load_stats
|
||||||
|
self._tpl_stats = load_stats()
|
||||||
slot_num = int(round(self.slot_var.get()))
|
slot_num = int(round(self.slot_var.get()))
|
||||||
cells = recognize_image(
|
cells = recognize_image(
|
||||||
self.image, self.bbox,
|
self.image, self.bbox,
|
||||||
@@ -515,6 +518,27 @@ class ScreenshotFrame(ttk.Frame):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.after(0, lambda: messagebox.showerror("인식 실패", str(e)))
|
self.after(0, lambda: messagebox.showerror("인식 실패", str(e)))
|
||||||
|
|
||||||
|
def _save_debug(self) -> None:
|
||||||
|
if self.image is None or not self.bbox:
|
||||||
|
messagebox.showinfo("안내", "먼저 캡처 + 영역 지정을 해주세요.")
|
||||||
|
return
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from .recognizer import dump_debug
|
||||||
|
base = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
|
||||||
|
out_dir = os.path.join(base, "sephiria_inv", "debug",
|
||||||
|
datetime.now().strftime("%Y%m%d-%H%M%S"))
|
||||||
|
try:
|
||||||
|
slot_num = int(round(self.slot_var.get()))
|
||||||
|
report = dump_debug(self.image, self.bbox, out_dir, slot_num=slot_num)
|
||||||
|
messagebox.showinfo(
|
||||||
|
"디버그 저장 완료",
|
||||||
|
f"폴더에 screenshot.png, bbox_crop.png, cells/, report.txt 가 저장됨.\n\n{out_dir}\n\n"
|
||||||
|
f"report: {report}",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("디버그 저장 실패", str(e))
|
||||||
|
|
||||||
def _show_cells(self, cells: List[CellResult]) -> None:
|
def _show_cells(self, cells: List[CellResult]) -> None:
|
||||||
self.cells = cells
|
self.cells = cells
|
||||||
for c in self.preview_inner.winfo_children():
|
for c in self.preview_inner.winfo_children():
|
||||||
@@ -541,10 +565,22 @@ class ScreenshotFrame(ttk.Frame):
|
|||||||
kind_counts[effective["kind"]] = kind_counts.get(effective["kind"], 0) + 1
|
kind_counts[effective["kind"]] = kind_counts.get(effective["kind"], 0) + 1
|
||||||
self._make_cell(y, x, slot_id, effective)
|
self._make_cell(y, x, slot_id, effective)
|
||||||
|
|
||||||
|
stats = getattr(self, "_tpl_stats", None) or {}
|
||||||
|
tpl_line = ""
|
||||||
|
if stats:
|
||||||
|
total_loaded = stats.get("slabs_ok", 0) + stats.get("artifacts_ok", 0)
|
||||||
|
total_failed = stats.get("slabs_fail", 0) + stats.get("artifacts_fail", 0)
|
||||||
|
tpl_line = (
|
||||||
|
f"\n템플릿: 슬랩 {stats.get('slabs_ok',0)}/{stats.get('slabs_ok',0)+stats.get('slabs_fail',0)} · "
|
||||||
|
f"아티팩트 {stats.get('artifacts_ok',0)}/{stats.get('artifacts_ok',0)+stats.get('artifacts_fail',0)}"
|
||||||
|
)
|
||||||
|
if total_loaded == 0:
|
||||||
|
tpl_line += " (CDN 다운로드 실패 — 인터넷 연결/방화벽 확인)"
|
||||||
msg = (
|
msg = (
|
||||||
f"석판 {kind_counts.get('slab', 0)} · 아티팩트 {kind_counts.get('artifact', 0)} · "
|
f"석판 {kind_counts.get('slab', 0)} · 아티팩트 {kind_counts.get('artifact', 0)} · "
|
||||||
f"빈칸 {kind_counts.get('empty', 0)} · 합쳐진(?) {kind_counts.get('merged', 0)} · "
|
f"빈칸 {kind_counts.get('empty', 0)} · 합쳐진(?) {kind_counts.get('merged', 0)} · "
|
||||||
f"미인식 {kind_counts.get('unknown', 0)}\n"
|
f"미인식 {kind_counts.get('unknown', 0)}"
|
||||||
|
f"{tpl_line}\n"
|
||||||
"셀을 클릭하면 종류/값을 교정할 수 있습니다. 끝나면 [이 구성으로 계산]."
|
"셀을 클릭하면 종류/값을 교정할 수 있습니다. 끝나면 [이 구성으로 계산]."
|
||||||
)
|
)
|
||||||
self.status["text"] = msg
|
self.status["text"] = msg
|
||||||
|
|||||||
@@ -80,17 +80,24 @@ _TEMPLATE_CACHE: List[_Template] = []
|
|||||||
_CACHE_BUILT = False
|
_CACHE_BUILT = False
|
||||||
|
|
||||||
|
|
||||||
|
_LAST_LOAD_STATS: Dict[str, int] = {"slabs_ok": 0, "slabs_fail": 0,
|
||||||
|
"artifacts_ok": 0, "artifacts_fail": 0}
|
||||||
|
|
||||||
|
|
||||||
def _build_templates(*, include_artifacts: bool = True) -> List[_Template]:
|
def _build_templates(*, include_artifacts: bool = True) -> List[_Template]:
|
||||||
"""Build (and cache) the full template list. Lazy because download is slow."""
|
"""Build (and cache) the full template list. Lazy because download is slow."""
|
||||||
global _CACHE_BUILT
|
global _CACHE_BUILT
|
||||||
if _CACHE_BUILT and _TEMPLATE_CACHE:
|
if _CACHE_BUILT and _TEMPLATE_CACHE:
|
||||||
return _TEMPLATE_CACHE
|
return _TEMPLATE_CACHE
|
||||||
out: List[_Template] = []
|
out: List[_Template] = []
|
||||||
|
s_ok = s_fail = a_ok = a_fail = 0
|
||||||
# Slabs: 4 rotations for rotatable, 1 otherwise
|
# Slabs: 4 rotations for rotatable, 1 otherwise
|
||||||
for s in SLABS:
|
for s in SLABS:
|
||||||
img = fetch_slab_image(s.image)
|
img = fetch_slab_image(s.image)
|
||||||
if img is None:
|
if img is None:
|
||||||
|
s_fail += 1
|
||||||
continue
|
continue
|
||||||
|
s_ok += 1
|
||||||
base = _on_dark(img)
|
base = _on_dark(img)
|
||||||
rotations = (0, 1, 2, 3) if s.rotate else (0,)
|
rotations = (0, 1, 2, 3) if s.rotate else (0,)
|
||||||
for r in rotations:
|
for r in rotations:
|
||||||
@@ -100,9 +107,13 @@ def _build_templates(*, include_artifacts: bool = True) -> List[_Template]:
|
|||||||
for a in ARTIFACTS:
|
for a in ARTIFACTS:
|
||||||
img = fetch_artifact_image(a.image)
|
img = fetch_artifact_image(a.image)
|
||||||
if img is None:
|
if img is None:
|
||||||
|
a_fail += 1
|
||||||
continue
|
continue
|
||||||
|
a_ok += 1
|
||||||
base = _on_dark(img)
|
base = _on_dark(img)
|
||||||
out.append(_Template("artifact", a.value, 0, _to_feat(base)))
|
out.append(_Template("artifact", a.value, 0, _to_feat(base)))
|
||||||
|
_LAST_LOAD_STATS.update({"slabs_ok": s_ok, "slabs_fail": s_fail,
|
||||||
|
"artifacts_ok": a_ok, "artifacts_fail": a_fail})
|
||||||
_TEMPLATE_CACHE.clear()
|
_TEMPLATE_CACHE.clear()
|
||||||
_TEMPLATE_CACHE.extend(out)
|
_TEMPLATE_CACHE.extend(out)
|
||||||
_CACHE_BUILT = True
|
_CACHE_BUILT = True
|
||||||
@@ -117,24 +128,51 @@ def warm_templates(*, include_artifacts: bool = True) -> int:
|
|||||||
return len(_build_templates(include_artifacts=include_artifacts))
|
return len(_build_templates(include_artifacts=include_artifacts))
|
||||||
|
|
||||||
|
|
||||||
|
def load_stats() -> Dict[str, int]:
|
||||||
|
"""Return last template load counts: slabs_ok, slabs_fail, artifacts_ok, artifacts_fail."""
|
||||||
|
return dict(_LAST_LOAD_STATS)
|
||||||
|
|
||||||
|
|
||||||
# ---------- cell classification ----------
|
# ---------- cell classification ----------
|
||||||
|
|
||||||
def _is_empty(cell: Image.Image) -> bool:
|
def _is_empty(cell: Image.Image) -> bool:
|
||||||
"""Heuristic: empty slots are dark and ~uniform."""
|
"""Heuristic: empty slots are uniform color (any brightness).
|
||||||
|
|
||||||
|
Drops the dark-only assumption so HDR / bright-monitor captures with
|
||||||
|
pinkish slot backgrounds still detect as empty. Uniformity is the
|
||||||
|
actual invariant — empty slots have low std-dev whatever the hue.
|
||||||
|
"""
|
||||||
g = np.asarray(cell.convert("L"), dtype=np.float32)
|
g = np.asarray(cell.convert("L"), dtype=np.float32)
|
||||||
return bool(g.mean() < 60.0 and g.std() < 14.0)
|
rgb = np.asarray(cell.convert("RGB"), dtype=np.float32)
|
||||||
|
chan_std = float(rgb.reshape(-1, 3).std(axis=0).mean())
|
||||||
|
return bool(g.std() < 18.0 and chan_std < 22.0)
|
||||||
|
|
||||||
|
|
||||||
|
def _inset(cell: Image.Image, ratio: float = 0.16) -> Image.Image:
|
||||||
|
"""Trim decorative borders / corner badges before template matching.
|
||||||
|
|
||||||
|
The in-game slot has chunky frame ornaments and a stack-count badge in
|
||||||
|
a corner. Templates are clean icons. Cropping ~16% off every side
|
||||||
|
aligns the comparable inner art and removes the badge area in most
|
||||||
|
games.
|
||||||
|
"""
|
||||||
|
w, h = cell.size
|
||||||
|
dx = int(w * ratio)
|
||||||
|
dy = int(h * ratio)
|
||||||
|
return cell.crop((dx, dy, w - dx, h - dy))
|
||||||
|
|
||||||
|
|
||||||
def _classify(
|
def _classify(
|
||||||
cell: Image.Image,
|
cell: Image.Image,
|
||||||
templates: List[_Template],
|
templates: List[_Template],
|
||||||
*,
|
*,
|
||||||
min_score: float = 0.55,
|
min_score: float = 0.35,
|
||||||
) -> Tuple[str, Optional[str], int, float]:
|
) -> Tuple[str, Optional[str], int, float]:
|
||||||
"""Return (kind, value, rotation, score)."""
|
"""Return (kind, value, rotation, score)."""
|
||||||
if _is_empty(cell):
|
if _is_empty(cell):
|
||||||
return "empty", None, 0, 1.0
|
return "empty", None, 0, 1.0
|
||||||
feat = _to_feat(cell)
|
inner = _inset(cell)
|
||||||
|
feat = _to_feat(inner)
|
||||||
# Stack template features into a matrix for one big dot-product
|
# Stack template features into a matrix for one big dot-product
|
||||||
if not templates:
|
if not templates:
|
||||||
return "unknown", None, 0, 0.0
|
return "unknown", None, 0, 0.0
|
||||||
@@ -148,6 +186,27 @@ def _classify(
|
|||||||
return t.kind, t.value, t.rotation, best
|
return t.kind, t.value, t.rotation, best
|
||||||
|
|
||||||
|
|
||||||
|
def _classify_with_top(
|
||||||
|
cell: Image.Image,
|
||||||
|
templates: List[_Template],
|
||||||
|
*,
|
||||||
|
top_k: int = 3,
|
||||||
|
) -> Tuple[str, Optional[str], int, float, List[Tuple[str, str, int, float]]]:
|
||||||
|
"""Like _classify but also returns the top-k matches for debug dumps."""
|
||||||
|
if _is_empty(cell):
|
||||||
|
return "empty", None, 0, 1.0, []
|
||||||
|
if not templates:
|
||||||
|
return "unknown", None, 0, 0.0, []
|
||||||
|
feat = _to_feat(_inset(cell))
|
||||||
|
M = np.stack([t.feat for t in templates], axis=0)
|
||||||
|
scores = M @ feat
|
||||||
|
order = np.argsort(-scores)[:top_k]
|
||||||
|
top = [(templates[i].kind, templates[i].value, templates[i].rotation,
|
||||||
|
float(scores[i])) for i in order]
|
||||||
|
kind, value, rot, score = _classify(cell, templates)
|
||||||
|
return kind, value, rot, score, top
|
||||||
|
|
||||||
|
|
||||||
# ---------- public API ----------
|
# ---------- public API ----------
|
||||||
|
|
||||||
def recognize_image(
|
def recognize_image(
|
||||||
@@ -156,7 +215,7 @@ def recognize_image(
|
|||||||
*,
|
*,
|
||||||
slot_num: int = 34,
|
slot_num: int = 34,
|
||||||
include_artifacts: bool = True,
|
include_artifacts: bool = True,
|
||||||
min_score: float = 0.55,
|
min_score: float = 0.35,
|
||||||
) -> List[CellResult]:
|
) -> List[CellResult]:
|
||||||
"""Slice img[bbox] into a 6-col grid and classify each cell.
|
"""Slice img[bbox] into a 6-col grid and classify each cell.
|
||||||
|
|
||||||
@@ -184,6 +243,59 @@ def recognize_image(
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def dump_debug(
|
||||||
|
img: Image.Image,
|
||||||
|
bbox: Tuple[int, int, int, int],
|
||||||
|
out_dir: str,
|
||||||
|
*,
|
||||||
|
slot_num: int = 34,
|
||||||
|
include_artifacts: bool = True,
|
||||||
|
) -> str:
|
||||||
|
"""Save full screenshot, bbox crop, every cell crop and a top-3 match
|
||||||
|
report to out_dir. Returns the path to the report file. Used to iterate
|
||||||
|
on recognizer tuning from real captures.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
img.save(os.path.join(out_dir, "screenshot.png"))
|
||||||
|
L, T, R, B = bbox
|
||||||
|
crop = img.crop((L, T, R, B)).convert("RGB")
|
||||||
|
crop.save(os.path.join(out_dir, "bbox_crop.png"))
|
||||||
|
grid = generate_grid_config(slot_num)
|
||||||
|
if not grid:
|
||||||
|
return out_dir
|
||||||
|
rows = len(grid)
|
||||||
|
cell_w = (R - L) // GRID_COLS
|
||||||
|
cell_h = (B - T) // rows
|
||||||
|
templates = _build_templates(include_artifacts=include_artifacts)
|
||||||
|
stats = load_stats()
|
||||||
|
lines = [
|
||||||
|
f"bbox: {bbox}",
|
||||||
|
f"grid: {len(grid)} rows x {GRID_COLS} cols, slot_num={slot_num}",
|
||||||
|
f"cell px: {cell_w} x {cell_h}",
|
||||||
|
f"templates loaded: total={len(templates)} stats={stats}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
cells_dir = os.path.join(out_dir, "cells")
|
||||||
|
os.makedirs(cells_dir, exist_ok=True)
|
||||||
|
for row in grid:
|
||||||
|
y = row["rows"]
|
||||||
|
for x in range(row["cols"]):
|
||||||
|
cx0 = x * cell_w
|
||||||
|
cy0 = y * cell_h
|
||||||
|
cell = crop.crop((cx0, cy0, cx0 + cell_w, cy0 + cell_h))
|
||||||
|
cell.save(os.path.join(cells_dir, f"{y}-{x}.png"))
|
||||||
|
kind, value, rot, score, top = _classify_with_top(cell, templates)
|
||||||
|
top_s = ", ".join(f"{k}:{v}@r{r}={s:.3f}" for k, v, r, s in top)
|
||||||
|
lines.append(
|
||||||
|
f" {y}-{x}: kind={kind} value={value} rot={rot} score={score:.3f} | top: {top_s}"
|
||||||
|
)
|
||||||
|
report = os.path.join(out_dir, "report.txt")
|
||||||
|
with open(report, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write("\n".join(lines))
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
def recognize_file(
|
def recognize_file(
|
||||||
path: str,
|
path: str,
|
||||||
bbox: Tuple[int, int, int, int],
|
bbox: Tuple[int, int, int, int],
|
||||||
|
|||||||
Reference in New Issue
Block a user