feat(task): 新增跟踪功能 & 支持跟踪推荐卡检测

This commit is contained in:
XcantloadX 2025-03-10 19:12:27 +08:00
parent 28a8e9d280
commit 9a45a9b04a
5 changed files with 157 additions and 13 deletions

1
.gitignore vendored
View File

@ -13,6 +13,7 @@ tmp/
res/sprites_compiled/
messages/
logs/
traces/
##########################
# Byte-compiled / optimized / DLL files

View File

@ -9,12 +9,12 @@ import cv2
import numpy as np
from cv2.typing import MatLike
from .. import R
from . import loading
from ..common import ProduceAction, conf
from .scenes import at_home
from ..util.trace import trace
from .commu import handle_unread_commu
from ..common import ProduceAction, conf
from kotonebot.errors import UnrecoverableError
from kotonebot.backend.context.context import use_screenshot
from .common import until_acquisition_clear, acquisitions, commut_event
@ -257,8 +257,8 @@ def detect_recommended_card(
raise ValueError(f"Unsupported card count: {card_count}")
cards.append(SKIP_POSITION)
image = use_screenshot(img)
original_image = image.copy()
results: list[CardDetectResult] = []
for x, y, w, h, return_value in cards:
outer = (max(0, x - GLOW_EXTENSION), max(0, y - GLOW_EXTENSION))
@ -318,6 +318,17 @@ def detect_recommended_card(
filtered_results[0].top_score,
filtered_results[0].bottom_score
)
trace('rec-card', original_image, {
'card_count': card_count,
'type': filtered_results[0].type,
'score': filtered_results[0].score,
'borders': (
filtered_results[0].left_score,
filtered_results[0].right_score,
filtered_results[0].top_score,
filtered_results[0].bottom_score
)
})
return filtered_results[0]
def handle_recommended_card(
@ -326,15 +337,6 @@ def handle_recommended_card(
*,
img: MatLike | None = None,
):
# cd = Countdown(seconds=timeout)
# while not cd.expired():
# result = detect_recommended_card(card_count, threshold_predicate, img=img)
# if result is not None:
# device.double_click(result)
# return result
# sleep(np.random.uniform(0.01, 0.1))
# return None
result = detect_recommended_card(card_count, threshold_predicate, img=img)
if result is not None:
device.double_click(result)

View File

@ -0,0 +1,35 @@
import os
import json
import uuid
from typing import Any, Literal, TextIO
import cv2
from cv2.typing import MatLike
from kotonebot.util import cv2_imwrite
TraceId = Literal['rec-card']
TRACE_DIR = './traces/'
_trace_files: dict[TraceId, TextIO] = {}
if not os.path.exists(TRACE_DIR):
os.makedirs(TRACE_DIR)
def trace(id: TraceId, image: MatLike, message: str | dict[str, Any]):
file = None
dir = os.path.join(TRACE_DIR, id)
if id not in _trace_files:
if not os.path.exists(dir):
os.makedirs(dir)
file = open(os.path.join(dir, id + '.log'), 'a+', encoding='utf-8')
_trace_files[id] = file
else:
file = _trace_files[id]
image_name = uuid.uuid4().hex
cv2_imwrite(os.path.join(dir, image_name + '.png'), image)
if isinstance(message, dict):
message = json.dumps(message)
message = f'{image_name}.png\n{message}\n'
file.write(message)
file.flush()

View File

@ -336,4 +336,9 @@ def cv2_imread(path: str, flags: int = cv2.IMREAD_COLOR) -> MatLike:
img = cv2.imdecode(np.fromfile(path,dtype=np.uint8), flags)
return img
def cv2_imwrite(path: str, img: MatLike):
"""
cv2.imwrite 的简单封装
支持了对带中文的路径的写入
"""
cv2.imencode('.png', img)[1].tofile(path)

101
tools/render_trance.py Normal file
View File

@ -0,0 +1,101 @@
import json
import os
from pathlib import Path
def format_json(data):
"""将 JSON 数据格式化为更易读的字符串"""
formatted = []
for key, value in data.items():
if isinstance(value, list):
value = ', '.join(f'{v:.3f}' if isinstance(v, float) else str(v) for v in value)
formatted.append(f"<b>{key}:</b> {value}")
return '<br>'.join(formatted)
def generate_html(entries):
"""生成 HTML 内容"""
html = """
<html>
<head>
<title>Image Data</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.7/viewer.min.js" integrity="sha512-lZD0JiwhtP4UkFD1mc96NiTZ14L7MjyX5Khk8PMxJszXMLvu7kjq1sp4bb0tcL6MY+/4sIuiUxubOqoueHrW4w==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.7/viewer.min.css" integrity="sha512-vRbASHFS0JiM4xwX/iqr9mrD/pXGnOP2CLdmXSSNAjLdgQVFyt4qI+BIoUW7/81uSuKRj0cWv3Dov8vVQOTHLw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
.img-container {
margin: 20px; padding: 20px; border: 1px solid #ccc;
display: inline-block;
}
img {
max-width: 100%;
max-height: 400px;
height: auto;
object-fit: contain;
}
.data { margin-top: 10px; }
</style>
</head>
<body>
"""
html += "<div id='container'>"
for image, data in entries:
html += f"""
<div class="img-container">
<img src="{image}" alt="{image}">
<div class="data">
{format_json(data)}
</div>
</div>
"""
html += "</div>"
html += """
<script>
document.addEventListener('DOMContentLoaded', function() {
const gallery = new Viewer(document.getElementById('container'));
// gallery.show()
});
</script>
</body>
</html>
"""
return html
def parse_log(log_content, base_dir):
"""解析日志内容"""
entries = []
lines = log_content.strip().split('\n')
for i in range(0, len(lines), 2):
if i+1 >= len(lines):
break
image = os.path.abspath(os.path.join(base_dir, os.path.basename(lines[i])))
image = 'file:///' + image
try:
data = json.loads(lines[i+1])
entries.append((image, data))
except json.JSONDecodeError:
continue
return entries
def main(input_file, output_file):
"""主函数"""
with open(input_file, 'r') as f:
log_content = f.read()
base_dir = os.path.dirname(input_file)
entries = parse_log(log_content, base_dir)
html_content = generate_html(entries)
with open(output_file, 'w') as f:
f.write(html_content)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Render log data to HTML')
parser.add_argument('input', help='Input log file')
parser.add_argument('output', help='Output HTML file')
args = parser.parse_args()
main(args.input, args.output)