✨ feat: workable
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
"""模板识别 + 标定工具测试(用合成图片,无需真机)。需要 opencv/numpy。"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
cv2 = pytest.importorskip("cv2")
|
||||
|
||||
from hbc.board import CellKind # noqa: E402
|
||||
from hbc.calibrate import _fit_scale, _map_box_to_original # noqa: E402
|
||||
from hbc.calibrate import main as calib_main # noqa: E402
|
||||
from hbc.config import ( # noqa: E402
|
||||
GridConfig,
|
||||
TemplateMatchConfig,
|
||||
level_layout_path,
|
||||
load_level_grid,
|
||||
)
|
||||
from hbc.recognizer import recognize_board # noqa: E402
|
||||
from hbc.templates import TemplateLibrary # noqa: E402
|
||||
|
||||
CELL = 40
|
||||
|
||||
# 合成棋盘布局:数字=正常块颜色, 'W'=墙(石), 'I'=墙(冰), '.'=空
|
||||
LAYOUT = [
|
||||
["0", "1", "2", "W"],
|
||||
["1", "I", "0", "."],
|
||||
["2", "0", "1", "2"],
|
||||
]
|
||||
|
||||
# 每种外观对应的 BGR 纯色块(足够区分即可)
|
||||
APPEARANCE = {
|
||||
"0": (60, 60, 220), # 红
|
||||
"1": (60, 200, 60), # 绿
|
||||
"2": (220, 120, 60), # 蓝
|
||||
"W": (90, 90, 90), # 石墙(灰)
|
||||
"I": (230, 220, 180), # 冰墙(浅蓝白)
|
||||
".": (10, 10, 10), # 空(近黑)
|
||||
}
|
||||
|
||||
|
||||
def _swatch(bgr):
|
||||
img = np.zeros((CELL, CELL, 3), np.uint8)
|
||||
img[:] = bgr
|
||||
return img
|
||||
|
||||
|
||||
def _build_board_image():
|
||||
rows, cols = len(LAYOUT), len(LAYOUT[0])
|
||||
img = np.zeros((rows * CELL, cols * CELL, 3), np.uint8)
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
img[r * CELL:(r + 1) * CELL, c * CELL:(c + 1) * CELL] = _swatch(APPEARANCE[LAYOUT[r][c]])
|
||||
return img
|
||||
|
||||
|
||||
def _write_templates(root):
|
||||
(root / "normal").mkdir(parents=True)
|
||||
(root / "wall").mkdir(parents=True)
|
||||
(root / "empty").mkdir(parents=True)
|
||||
cv2.imwrite(str(root / "normal" / "red.png"), _swatch(APPEARANCE["0"]))
|
||||
cv2.imwrite(str(root / "normal" / "green.png"), _swatch(APPEARANCE["1"]))
|
||||
cv2.imwrite(str(root / "normal" / "blue.png"), _swatch(APPEARANCE["2"]))
|
||||
cv2.imwrite(str(root / "wall" / "stone.png"), _swatch(APPEARANCE["W"]))
|
||||
cv2.imwrite(str(root / "wall" / "ice.png"), _swatch(APPEARANCE["I"]))
|
||||
cv2.imwrite(str(root / "empty" / "hole.png"), _swatch(APPEARANCE["."]))
|
||||
|
||||
|
||||
def test_template_recognition_multi_wall(tmp_path):
|
||||
root = tmp_path / "templates"
|
||||
_write_templates(root)
|
||||
lib = TemplateLibrary.load(TemplateMatchConfig(templates_dir=str(root), canonical_size=32))
|
||||
|
||||
img = _build_board_image()
|
||||
rows, cols = len(LAYOUT), len(LAYOUT[0])
|
||||
grid = GridConfig(x=0, y=0, w=cols * CELL, h=rows * CELL, rows=rows, cols=cols)
|
||||
board = recognize_board(img, grid, lib)
|
||||
|
||||
# 校验每格类型
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
cell = board.get(r, c)
|
||||
token = LAYOUT[r][c]
|
||||
if token in ("0", "1", "2"):
|
||||
assert cell.kind == CellKind.NORMAL, (r, c, cell)
|
||||
elif token in ("W", "I"):
|
||||
assert cell.kind == CellKind.BRICK, (r, c, cell)
|
||||
else:
|
||||
assert cell.kind == CellKind.EMPTY, (r, c, cell)
|
||||
|
||||
# 两种墙应被区分到不同 meta['wall']
|
||||
assert board.get(0, 3).meta["wall"] == "stone"
|
||||
assert board.get(1, 1).meta["wall"] == "ice"
|
||||
|
||||
# 相同外观的正常块应得到相同 color id(求解器据此判等)
|
||||
assert board.color_at(0, 0) == board.color_at(1, 2) # 两个红
|
||||
assert board.color_at(0, 1) == board.color_at(1, 0) # 两个绿
|
||||
assert board.color_at(0, 0) != board.color_at(0, 1) # 红 != 绿
|
||||
|
||||
|
||||
def test_low_confidence_becomes_ice_wall(tmp_path):
|
||||
"""置信度不足(如冰块/未登记方块)一律判为冰墙,而非空格。"""
|
||||
root = tmp_path / "templates"
|
||||
_write_templates(root) # 只有红/绿/蓝 + 墙 + 空
|
||||
lib = TemplateLibrary.load(TemplateMatchConfig(templates_dir=str(root), canonical_size=32))
|
||||
|
||||
purple = _swatch((200, 40, 200)) # 库里没有的外观 -> 低置信度
|
||||
cell = lib.classify(purple)
|
||||
assert cell.is_brick and cell.meta.get("wall") == "ice"
|
||||
|
||||
# 真正的空块(与空模板一致,高分)仍判为空格
|
||||
empty_cell = lib.classify(_swatch(APPEARANCE["."]))
|
||||
assert empty_cell.is_empty and not empty_cell.is_brick
|
||||
|
||||
|
||||
def test_layout_save_load(tmp_path):
|
||||
p = tmp_path / "layout.json"
|
||||
g = GridConfig(x=12, y=34, w=400, h=300, rows=6, cols=8)
|
||||
g.save_json(p)
|
||||
g2 = GridConfig.load_json(p)
|
||||
assert g2.to_dict() == g.to_dict()
|
||||
|
||||
|
||||
def test_level_layout_files(tmp_path):
|
||||
"""7 关网格逐渐变大:每关一个 level{N}.json,文件名带关号,统一放 layouts/。"""
|
||||
d = tmp_path / "layouts"
|
||||
GridConfig(0, 0, 200, 200, 5, 5).save_json(_mk(level_layout_path(1, d)))
|
||||
GridConfig(0, 0, 240, 240, 6, 6).save_json(level_layout_path(2, d))
|
||||
GridConfig(0, 0, 280, 280, 8, 8).save_json(level_layout_path(7, d))
|
||||
|
||||
assert level_layout_path(1, d).name == "level1.json"
|
||||
assert load_level_grid(1, d).rows == 5
|
||||
assert load_level_grid(2, d).cols == 6
|
||||
assert load_level_grid(7, d).w == 280
|
||||
# 未标定的关卡应报错
|
||||
import pytest as _pt
|
||||
with _pt.raises(FileNotFoundError):
|
||||
load_level_grid(3, d)
|
||||
|
||||
|
||||
def _mk(p):
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
|
||||
def test_roi_scale_mapping():
|
||||
"""缩放窗口里选的框,要能正确换算回原图坐标。"""
|
||||
# 原图 1800 高,限制 900 -> scale=0.5
|
||||
scale = _fit_scale(1000, 1800, 900)
|
||||
assert abs(scale - 0.5) < 1e-9
|
||||
# 在缩放图上选 (50,100,200,300) -> 原图应为两倍
|
||||
assert _map_box_to_original((50, 100, 200, 300), scale) == (100, 200, 400, 600)
|
||||
# 不需要缩放时原样返回
|
||||
assert _fit_scale(400, 300, 900) == 1.0
|
||||
assert _map_box_to_original((1, 2, 3, 4), 1.0) == (1, 2, 3, 4)
|
||||
|
||||
|
||||
def test_calibrate_export_and_preview(tmp_path):
|
||||
img_path = tmp_path / "board.png"
|
||||
cv2.imwrite(str(img_path), _build_board_image())
|
||||
layouts_dir = tmp_path / "layouts"
|
||||
rows, cols = len(LAYOUT), len(LAYOUT[0])
|
||||
GridConfig(0, 0, cols * CELL, rows * CELL, rows, cols).save_json(_mk(level_layout_path(1, layouts_dir)))
|
||||
|
||||
cells_dir = tmp_path / "cells"
|
||||
rc = calib_main(["export", "--image", str(img_path), "--level", "1",
|
||||
"--layouts-dir", str(layouts_dir), "--out", str(cells_dir)])
|
||||
assert rc == 0
|
||||
exported = list(cells_dir.glob("*.png"))
|
||||
assert len(exported) == rows * cols
|
||||
|
||||
preview = tmp_path / "preview.png"
|
||||
rc = calib_main(["preview", "--image", str(img_path), "--level", "1",
|
||||
"--layouts-dir", str(layouts_dir), "--out", str(preview)])
|
||||
assert rc == 0
|
||||
assert preview.exists()
|
||||
|
||||
|
||||
def test_calibrate_roi_with_box(tmp_path):
|
||||
"""无 GUI:用 --box 直接给坐标,写入指定关卡文件。"""
|
||||
img_path = tmp_path / "board.png"
|
||||
cv2.imwrite(str(img_path), _build_board_image())
|
||||
layouts_dir = tmp_path / "layouts"
|
||||
rc = calib_main(["roi", "--image", str(img_path), "--level", "3",
|
||||
"--rows", "5", "--cols", "5", "--box", "0,0,200,200",
|
||||
"--layouts-dir", str(layouts_dir)])
|
||||
assert rc == 0
|
||||
g = load_level_grid(3, layouts_dir)
|
||||
assert (g.x, g.y, g.w, g.h, g.rows, g.cols) == (0, 0, 200, 200, 5, 5)
|
||||
Reference in New Issue
Block a user