feat(core): 可视调试器支持记录原图

This commit is contained in:
XcantloadX 2025-01-15 17:52:49 +08:00
parent 0d795a5926
commit 9d90baa2c5
7 changed files with 160 additions and 77 deletions

View File

@ -91,5 +91,9 @@ def find_rgb(
x, y, w, h = rect
# 红色圈出rect
cv2.rectangle(result_image, (x, y), (x+w, y+h), (255, 0, 0), 2)
debug_result('find_rgb', result_image, f'color={color}, rect={rect}, result={ret}')
debug_result(
'find_rgb',
[result_image, image],
f'color={color}, rect={rect}, result={ret}'
)
return ret

View File

@ -51,16 +51,14 @@ async def read_memory(key: str):
"""读取内存中的数据"""
try:
image = None
if key in vars._results:
image = vars._results[key].image
elif key in vars._images:
if key in vars._images:
image = vars._images[key]
else:
raise HTTPException(status_code=404, detail="Key not found")
# 编码图片
encode_params = [cv2.IMWRITE_JPEG_QUALITY, 85, cv2.IMWRITE_JPEG_PROGRESSIVE, 1]
_, buffer = cv2.imencode('.jpg', image, encode_params)
encode_params = [cv2.IMWRITE_PNG_COMPRESSION, 4]
_, buffer = cv2.imencode('.png', image, encode_params)
# 添加缓存控制头
headers = {
"Cache-Control": "public, max-age=3600", # 缓存1小时
@ -91,7 +89,7 @@ async def websocket_endpoint(websocket: WebSocket):
except:
await websocket.close()
def send_ws_message(title: str, image: str, text: str = '', wait: bool = False):
def send_ws_message(title: str, image: list[str], text: str = '', wait: bool = False):
"""发送 WebSocket 消息"""
message = {
"type": "visual",

View File

@ -241,6 +241,15 @@
<input class="form-check-input" type="checkbox" id="lockViewCheckbox">
<label class="form-check-label" for="lockViewCheckbox">锁定视图</label>
</div>
<div class="image-pagination ms-2 d-inline-block">
<button class="btn btn-sm btn-outline-secondary" id="prevImageBtn" disabled>
<i class="bi bi-arrow-left-short"></i>
</button>
<span id="imagePageCount" class="mx-2">1/1</span>
<button class="btn btn-sm btn-outline-secondary" id="nextImageBtn" disabled>
<i class="bi bi-arrow-right-short"></i>
</button>
</div>
</div>
<div class="slider-container mb-2">
<input type="range" class="form-range" id="pageSlider" value="0" min="0" max="0">
@ -252,7 +261,7 @@
<button class="btn btn-sm btn-outline-secondary" id="prevBtn" alt="上一张">
<i class="bi bi-chevron-left"></i>
</button>
<span id="imageCount" style="margin: 0 10px">0/0</span>
<span id="funcCount" style="margin: 0 10px">0/0</span>
<button class="btn btn-sm btn-outline-secondary" id="nextBtn" alt="下一张">
<i class="bi bi-chevron-right"></i>
</button>
@ -294,8 +303,9 @@
let startX, startY;
let translateX = 0, translateY = 0;
let isReconnecting = false;
let imageHistory = [];
let currentImageIndex = -1;
let funcHistory = []; // 服务器推送的函数执行结果
let currentFuncIndex = -1;
let currentImageIndex = 0; // 当前图片页码
let isResizing = false;
let startWidth = 0;
let minWidth = 200; // 最小宽度
@ -331,37 +341,53 @@
// 更新图片计数
function updateImageCount() {
$('#imageCount').text(`${currentImageIndex + 1}/${imageHistory.length}`);
$('#firstBtn').prop('disabled', currentImageIndex <= 0);
$('#prevBtn').prop('disabled', currentImageIndex <= 0);
$('#nextBtn').prop('disabled', currentImageIndex >= imageHistory.length - 1);
$('#lastBtn').prop('disabled', currentImageIndex >= imageHistory.length - 1);
$('#funcCount').text(`${currentFuncIndex + 1}/${funcHistory.length}`);
$('#firstBtn').prop('disabled', currentFuncIndex <= 0);
$('#prevBtn').prop('disabled', currentFuncIndex <= 0);
$('#nextBtn').prop('disabled', currentFuncIndex >= funcHistory.length - 1);
$('#lastBtn').prop('disabled', currentFuncIndex >= funcHistory.length - 1);
// 更新滑块最大值
const slider = $('#pageSlider');
slider.attr('max', Math.max(0, imageHistory.length - 1));
slider.attr('max', Math.max(0, funcHistory.length - 1));
}
// 显示历史图片
function showHistoryImage(index) {
if (index < 0 || index >= imageHistory.length) return;
currentImageIndex = index;
const imageData = imageHistory[index];
if (index < 0 || index >= funcHistory.length) return;
currentFuncIndex = index;
const imageData = funcHistory[index];
// 更新图片
const imageContainer = $('#imageViewer');
const img = $('<img>');
if (imageData.type === 'memory') {
img.attr('src', `/api/read_memory?key=${imageData.value}`);
} else if (imageData.type === 'file') {
img.attr('src', `/api/read_file?path=${imageData.value}`);
}
imageContainer.empty().append(img);
// 重置当前图片页码
currentImageIndex = 0;
showCurrentImage();
// 更新标题和详情
$('h4').text(imageData.name);
$('#customMessage').html(imageData.details);
// 更新计数和滑块
updateImageCount();
$('#pageSlider').val(currentFuncIndex);
}
// 显示当前结果中的指定页码图片
function showCurrentImage() {
if (currentFuncIndex < 0) return;
const imageData = funcHistory[currentFuncIndex].image;
const imageValues = imageData;
// 更新图片
const imageContainer = $('#imageViewer');
const img = $('<img>');
if (imageData.type === 'memory') {
img.attr('src', `/api/read_memory?key=${imageValues.value[currentImageIndex]}`);
} else if (imageData.type === 'file') {
img.attr('src', `/api/read_file?path=${imageValues.value[currentImageIndex]}`);
}
imageContainer.empty().append(img);
// 根据锁定状态决定是否保持视图
if (isViewLocked) {
currentScale = lastScale;
@ -394,9 +420,19 @@
// 初始化拖拽事件
initDragEvents();
// 更新计数和滑块
updateImageCount();
$('#pageSlider').val(currentImageIndex);
// 更新图片页码显示和按钮状态
updateImagePageCount();
}
// 更新图片页码显示
function updateImagePageCount() {
if (currentFuncIndex < 0) return;
const imageData = funcHistory[currentFuncIndex].image.value;
$('#imagePageCount').text(`${currentImageIndex + 1}/${imageData.length}`);
$('#prevImageBtn').prop('disabled', currentImageIndex <= 0);
$('#nextImageBtn').prop('disabled', currentImageIndex >= imageData.length - 1);
}
// 连接 WebSocket
@ -423,17 +459,12 @@
if (data.type === 'visual') {
// 保存图片数据到历史记录
imageHistory.push({
type: data.data.image.type,
value: data.data.image.value,
name: data.data.name,
details: data.data.details
});
funcHistory.push(data.data);
// 只有当前正在查看最后一张图片时,才自动显示新图片
if (currentImageIndex === imageHistory.length - 2 || currentImageIndex === -1) {
currentImageIndex = imageHistory.length - 1;
showHistoryImage(currentImageIndex);
if (currentFuncIndex === funcHistory.length - 2 || currentFuncIndex === -1) {
currentFuncIndex = funcHistory.length - 1;
showHistoryImage(currentFuncIndex);
} else {
// 否则只更新计数和按钮状态
updateImageCount();
@ -543,9 +574,9 @@
// 导航按钮事件
$('#firstBtn').click(() => showHistoryImage(0));
$('#prevBtn').click(() => showHistoryImage(currentImageIndex - 1));
$('#nextBtn').click(() => showHistoryImage(currentImageIndex + 1));
$('#lastBtn').click(() => showHistoryImage(imageHistory.length - 1));
$('#prevBtn').click(() => showHistoryImage(currentFuncIndex - 1));
$('#nextBtn').click(() => showHistoryImage(currentFuncIndex + 1));
$('#lastBtn').click(() => showHistoryImage(funcHistory.length - 1));
// 初始化拖拽调整宽度
function initResizeHandle() {
@ -581,25 +612,34 @@
});
}
// 下载当前图片
// 下载当前图片
function downloadCurrentImage() {
if (currentImageIndex < 0 || currentImageIndex >= imageHistory.length) return;
if (currentFuncIndex < 0 || currentFuncIndex >= funcHistory.length) return;
const imageData = funcHistory[currentFuncIndex].image;
const imageData = imageHistory[currentImageIndex];
const img = $('#imageViewer img');
const link = document.createElement('a');
// 使用原始图片URL
link.href = img.attr('src');
// 生成文件名
// 生成基础文件名
const name = funcHistory[currentFuncIndex].name;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const extension = imageData.type === 'memory' ? 'jpg' : 'png';
link.download = `${imageData.name}_${timestamp}.${extension}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 下载所有图片
imageData.value.forEach((val, index) => {
const link = document.createElement('a');
if (imageData.type === 'memory') {
link.href = `/api/read_memory?key=${val}`;
} else {
link.href = `/api/read_file?path=${val}`;
}
// 如果只有一张图片,不添加索引
const fileName = imageData.length > 1
? `${name}_${index + 1}_${timestamp}.png`
: `${name}_${timestamp}.png`;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
// 键盘控制
@ -610,12 +650,12 @@
switch(e.key) {
case 'ArrowLeft':
if (!$('#prevBtn').prop('disabled')) {
showHistoryImage(currentImageIndex - 1);
showHistoryImage(currentFuncIndex - 1);
}
break;
case 'ArrowRight':
if (!$('#nextBtn').prop('disabled')) {
showHistoryImage(currentImageIndex + 1);
showHistoryImage(currentFuncIndex + 1);
}
break;
case 'Home':
@ -625,7 +665,7 @@
break;
case 'End':
if (!$('#lastBtn').prop('disabled')) {
showHistoryImage(imageHistory.length - 1);
showHistoryImage(funcHistory.length - 1);
}
break;
}
@ -648,6 +688,22 @@
$('#lockViewCheckbox').on('change', function() {
isViewLocked = $(this).prop('checked');
});
// 初始化图片分页按钮事件
$('#prevImageBtn').click(function() {
if (currentImageIndex > 0) {
currentImageIndex--;
showCurrentImage();
}
});
$('#nextImageBtn').click(function() {
const func = funcHistory[currentFuncIndex];
if (currentImageIndex < func.image.value.length - 1) {
currentImageIndex++;
showCurrentImage();
}
});
}
// 修改初始化调用

View File

@ -11,18 +11,22 @@ from cv2.typing import MatLike
class Result(NamedTuple):
title: str
image: MatLike
text: str
image: list[str]
description: str
@dataclass
class _Vars:
"""调试变量类"""
enabled: bool = False
"""是否启用调试结果显示。"""
max_results: int = 200
"""最多保存的结果数量。"""
max_results: int = -1
"""最多保存的结果数量。-1 表示不限制。"""
wait_for_message_sent: bool = False
"""是否等待消息发送完成才继续下一个操作。"""
"""
是否等待消息发送完成才继续后续代码
启用此选项可能会降低运行速度
"""
hide_server_log: bool = True
"""是否隐藏服务器日志。"""
@ -33,6 +37,16 @@ _results: dict[str, Result] = {}
_images: dict[str, MatLike] = {}
"""存放临时图片的字典。"""
def _save_image(image: MatLike) -> str:
"""缓存图片数据到 _images 字典中。返回 key。"""
key = str(uuid.uuid4())
_images[key] = image
return key
def _save_images(images: list[MatLike]) -> list[str]:
"""缓存图片数据到 _images 字典中。返回 key 列表。"""
return [_save_image(image) for image in images]
def img(image: str | MatLike | None) -> str:
"""
用于在 `result()` 函数中嵌入图片
@ -52,7 +66,7 @@ def img(image: str | MatLike | None) -> str:
# TODO: 保存原图。原图用 PNG结果用 JPG 压缩。
def result(
title: str,
image: MatLike,
image: MatLike | list[MatLike],
text: str = ''
):
"""
@ -74,8 +88,13 @@ def result(
"""
if not debug.enabled:
return
if not isinstance(image, list):
image = [image]
key = 'result_' + title + '_' + str(time.time())
_results[key] = Result(title, image, text)
# 保存图片
saved_images = _save_images(image)
_results[key] = Result(title, saved_images, text)
if len(_results) > debug.max_results:
_results.pop(next(iter(_results)))
# 拼接消息
@ -99,5 +118,5 @@ def result(
)
# 发送 WS 消息
from .server import send_ws_message
send_ws_message(title, key, final_text, wait=debug.wait_for_message_sent)
send_ws_message(title, saved_images, final_text, wait=debug.wait_for_message_sent)

View File

@ -349,7 +349,11 @@ def find(
result_text += f"matches: {len(matches)} \n"
for match in matches:
result_text += f"score: {match.score} position: {match.position} size: {match.size} \n"
debug_result(f"image.find", result_image, result_text)
debug_result(
'image.find',
[result_image, image],
result_text
)
return matches[0] if len(matches) > 0 else None
def find_many(
@ -387,7 +391,7 @@ def find_many(
result_image = _draw_result(image, results)
debug_result(
'image.find_many',
result_image,
[result_image, image],
f"template: {img(template)} \n"
f"matches: {len(results)} \n"
)
@ -451,7 +455,7 @@ def find_any(
)
debug_result(
'image.find_any',
_draw_result(image, ret),
[_draw_result(image, ret), image],
msg
)
return ret
@ -491,7 +495,7 @@ def count(
result_image = _draw_result(image, results)
debug_result(
'image.count',
result_image,
[result_image, image],
(
f"template: {img(template)} \n"
f"mask: {img(mask)} \n"
@ -536,7 +540,7 @@ def expect(
if debug.enabled:
debug_result(
'image.expect',
_draw_result(image, ret),
[_draw_result(image, ret), image],
(
f"template: {img(template)} \n"
f"mask: {img(mask)} \n"
@ -569,8 +573,9 @@ def similar(
result_image = np.hstack([image1, image2])
debug_result(
'image.similar',
result_image,
[result_image, image1, image2],
f"result: {result} >= {threshold} == {result >= threshold} \n"
)
return result >= threshold

View File

@ -139,7 +139,7 @@ class Ocr:
result_image = _draw_result(img, ret)
debug_result(
'ocr',
result_image,
[result_image, img],
f"result: \n" + \
"<table class='result-table'><tr><th>Text</th><th>Confidence</th></tr>" + \
"\n".join([f"<tr><td>{r.text}</td><td>{r.confidence:.2f}</td></tr>" for r in ret]) + \

View File

@ -13,6 +13,7 @@ logger = getLogger(__name__)
def loading() -> bool:
"""检测是否在场景加载页面"""
img = device.screenshot()
original_img = img.copy()
# 二值化图片
_, img = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
# 裁剪上面 10%
@ -22,7 +23,7 @@ def loading() -> bool:
b,g,r = cv2.split(img)
shiftet_im = b.astype(np.int64) + 1000 * (g.astype(np.int64) + 1) + 1000 * 1000 * (r.astype(np.int64) + 1)
ret = len(np.unique(shiftet_im)) <= 2
result('tasks.actions.loading', img, f'result={ret}')
result('tasks.actions.loading', [img, original_img], f'result={ret}')
return ret
@action('等待加载开始')