feat(task): 新增跟踪功能 & 支持跟踪推荐卡检测
This commit is contained in:
parent
28a8e9d280
commit
9a45a9b04a
|
@ -13,6 +13,7 @@ tmp/
|
|||
res/sprites_compiled/
|
||||
messages/
|
||||
logs/
|
||||
traces/
|
||||
##########################
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue