feat(core): 可视调试器支持记录原图
This commit is contained in:
parent
0d795a5926
commit
9d90baa2c5
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 修改初始化调用
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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]) + \
|
||||
|
|
|
@ -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('等待加载开始')
|
||||
|
|
Loading…
Reference in New Issue