665 lines
25 KiB
Python
665 lines
25 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Perception and vision tool implementations for the Explorer agent.
|
|
|
|
Split out of ``artemis.agents.explorer.explorer``: the ``exec_*`` tool method
|
|
group plus its search helpers, packaged as a mixin consumed by ``Explorer``.
|
|
Patched collaborators (``settings``, ``draw_dots``, ``search_ui_func``,
|
|
``search_by_coordinates_func``, ``_run_object_detection``, ``StorageManager``,
|
|
``logger``) are resolved through the facade module at call time; see
|
|
``artemis.agents.explorer._facade``.
|
|
"""
|
|
|
|
import asyncio
|
|
import glob
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
|
|
import cv2
|
|
|
|
from artemis.agents.explorer._facade import facade
|
|
from artemis.agents.image_processor.image_processor import ImageProcessor
|
|
from artemis.data_engine.trace import trace
|
|
|
|
|
|
def load_detector_templates() -> tuple[list, object]:
|
|
"""Load object-detector prompt templates and the optional global timeout.
|
|
|
|
Shared by ``exec_detect_objects`` and the flash-mode ``run`` path; the two
|
|
call sites historically carried an identical inline copy of this block.
|
|
"""
|
|
_ex = facade()
|
|
detector_prompt_path = (
|
|
Path(__file__).parent.parent / "object_detector" / "object_detector.json"
|
|
)
|
|
global_timeout = None
|
|
try:
|
|
with open(detector_prompt_path, encoding="utf-8") as f:
|
|
detector_config = json.load(f)
|
|
detector_templates = detector_config.get("templates", [])
|
|
detector_instructions = detector_config.get("instructions", "")
|
|
global_timeout = detector_config.get("global_timeout", None)
|
|
except Exception as e:
|
|
_ex.logger.warning(f"Failed to load detector prompt config: {e}")
|
|
detector_templates = [
|
|
"Point to the following objects in the provided image: {labels_str}."
|
|
]
|
|
detector_instructions = ""
|
|
|
|
templates = [f"{t}\n\n{detector_instructions}" for t in detector_templates]
|
|
return templates, global_timeout
|
|
|
|
|
|
class PerceptionToolsMixin:
|
|
"""Perception/vision tool method group of :class:`Explorer`."""
|
|
|
|
async def _search_ui_helper(self, query: str, prefix: str = "S", color: str = "red") -> dict:
|
|
_ex = facade()
|
|
if not self.image_name:
|
|
return {
|
|
"text": (
|
|
"Error: UI data for the current screen is not found in Data"
|
|
" Engine. It might not be synced yet."
|
|
),
|
|
"image_path": None,
|
|
}
|
|
|
|
# 1. Try with high precision threshold
|
|
raw_res = _ex.search_ui_func(self.image_name, query, 0.7)
|
|
if "error" in raw_res:
|
|
return {"text": raw_res["error"], "image_path": None}
|
|
|
|
matches = raw_res.get("matches", [])
|
|
|
|
# 2. If fewer than 3 matches, try with a lower threshold and merge
|
|
if len(matches) < 3:
|
|
raw_res_low = _ex.search_ui_func(self.image_name, query, 0.4)
|
|
if "error" not in raw_res_low:
|
|
low_matches = raw_res_low.get("matches", [])
|
|
existing_keys = {
|
|
(
|
|
m.get("matched_text"),
|
|
tuple(m.get("bounds")) if m.get("bounds") else None,
|
|
)
|
|
for m in matches
|
|
}
|
|
for m in low_matches:
|
|
key = (
|
|
m.get("matched_text"),
|
|
tuple(m.get("bounds")) if m.get("bounds") else None,
|
|
)
|
|
if key not in existing_keys:
|
|
matches.append(m)
|
|
existing_keys.add(key)
|
|
|
|
if not matches:
|
|
return {"text": "No matches found.", "image_path": None}
|
|
|
|
points = []
|
|
labels = []
|
|
text_output = []
|
|
|
|
for m in matches:
|
|
m_type = m.get("type", "xml")
|
|
m_prefix = "O" if m_type == "ocr" else prefix
|
|
label_id = f"{m_prefix}{self.global_label_idx}"
|
|
self.global_label_idx += 1
|
|
bounds = m.get("bounds")
|
|
matched_text = m.get("matched_text", "")
|
|
|
|
if bounds and len(bounds) == 4:
|
|
left, top, right, bottom = bounds
|
|
cx_pixel = (left + right) // 2
|
|
cy_pixel = (top + bottom) // 2
|
|
|
|
points.append([cx_pixel, cy_pixel])
|
|
labels.append(label_id)
|
|
|
|
cx_norm = int(max(0, min(1000, cx_pixel * 1000 / self.width)))
|
|
cy_norm = int(max(0, min(1000, cy_pixel * 1000 / self.height)))
|
|
|
|
text_output.append(f"[{label_id}] '{matched_text}' at [{cx_norm},{cy_norm}]")
|
|
else:
|
|
text_output.append(f"[{label_id}] '{matched_text}' at unknown")
|
|
|
|
if not points:
|
|
return {"text": " | ".join(text_output), "image_path": None}
|
|
|
|
base_dir = _ex.settings.TRACES_PATH
|
|
images_dir = base_dir / "images"
|
|
search_ui_dir = images_dir / "search_ui"
|
|
search_ui_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
existing_files = glob.glob(str(search_ui_dir / f"{self.image_name}_*.jpg"))
|
|
max_seq = 0
|
|
for f in existing_files:
|
|
match = re.search(r"_(\d+)\.jpg$", f)
|
|
if match:
|
|
max_seq = max(max_seq, int(match.group(1)))
|
|
seq = max_seq + 1
|
|
output_path = search_ui_dir / f"{self.image_name}_{seq}.jpg"
|
|
|
|
try:
|
|
_ex.draw_dots(
|
|
self.screenshot_path,
|
|
points,
|
|
labels,
|
|
str(output_path),
|
|
color=color,
|
|
)
|
|
image_path = str(output_path)
|
|
except Exception as e:
|
|
_ex.logger.warning(f"Failed to draw dots: {e}")
|
|
image_path = None
|
|
|
|
return {"text": " | ".join(text_output), "image_path": image_path}
|
|
|
|
async def _search_by_coords_helper(
|
|
self, nx: int, ny: int, prefix: str = "X", color: str = "blue"
|
|
) -> dict:
|
|
_ex = facade()
|
|
if not self.image_name:
|
|
return {
|
|
"text": (
|
|
"Error: UI data for the current screen is not found in Data"
|
|
" Engine. It might not be synced yet."
|
|
),
|
|
"image_path": None,
|
|
}
|
|
|
|
x = int(max(0, min(self.width, nx * self.width / 1000)))
|
|
y = int(max(0, min(self.height, ny * self.height / 1000)))
|
|
raw_res = _ex.search_by_coordinates_func(self.image_name, x, y)
|
|
|
|
label_id = f"{prefix}{self.global_label_idx}"
|
|
self.global_label_idx += 1
|
|
|
|
# Draw a dot at the query coordinates
|
|
base_dir = _ex.settings.TRACES_PATH
|
|
images_dir = base_dir / "images"
|
|
coords_audit_dir = images_dir / "coords_audit"
|
|
coords_audit_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
existing_files = glob.glob(str(coords_audit_dir / f"{self.image_name}_*.jpg"))
|
|
max_seq = 0
|
|
for f in existing_files:
|
|
match = re.search(r"_(\d+)\.jpg$", f)
|
|
if match:
|
|
max_seq = max(max_seq, int(match.group(1)))
|
|
seq = max_seq + 1
|
|
output_path = coords_audit_dir / f"{self.image_name}_{seq}.jpg"
|
|
|
|
try:
|
|
_ex.draw_dots(
|
|
self.screenshot_path,
|
|
[[x, y]],
|
|
[label_id],
|
|
str(output_path),
|
|
color=color,
|
|
)
|
|
image_path = str(output_path)
|
|
except Exception as e:
|
|
_ex.logger.warning(f"Failed to draw blue dots: {e}")
|
|
image_path = None
|
|
|
|
raw_res_str = str(raw_res).replace("\n", " | ").replace(" | | ", " | ")
|
|
return {
|
|
"text": (f"Matched element at [{nx},{ny}]: [{label_id}] | {raw_res_str}"),
|
|
"image_path": image_path,
|
|
}
|
|
|
|
@trace(type="tool", name="detect_objects")
|
|
async def exec_detect_objects(
|
|
self,
|
|
queries: list[str],
|
|
target_image_id: str = "img_0",
|
|
prefix: str = "D",
|
|
color: str = "red",
|
|
) -> dict:
|
|
_ex = facade()
|
|
try:
|
|
target_info = self.image_pool.get(target_image_id)
|
|
if not target_info:
|
|
return {
|
|
"text": (f"Error: {target_image_id} not found in Image Pool."),
|
|
"image_path": None,
|
|
}
|
|
|
|
target_path = target_info["path"]
|
|
transform = target_info["transform"]
|
|
|
|
# Load object detector templates
|
|
templates, global_timeout = load_detector_templates()
|
|
|
|
result = await _ex._run_object_detection(
|
|
self.ctx,
|
|
target_path,
|
|
queries,
|
|
templates,
|
|
global_timeout=global_timeout,
|
|
)
|
|
try:
|
|
detected_items = result.get("detected", [])
|
|
failed_queries = result.get("failed", [])
|
|
|
|
if not detected_items:
|
|
text_output = ["No objects detected."]
|
|
if failed_queries:
|
|
text_output.append(
|
|
"\n[NOTE: Failed to detect the following queries:"
|
|
f" {', '.join(failed_queries)}]"
|
|
)
|
|
return {
|
|
"text": "".join(text_output),
|
|
"image_path": str(target_path),
|
|
}
|
|
|
|
base_dir = _ex.settings.TRACES_PATH
|
|
images_dir = base_dir / "images"
|
|
object_detect_dir = images_dir / "object_detect"
|
|
object_detect_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
image_name_safe = self.image_name or "temp_image"
|
|
existing_files = glob.glob(str(object_detect_dir / f"{image_name_safe}_*.jpg"))
|
|
max_seq = 0
|
|
for f in existing_files:
|
|
match = re.search(r"_(\d+)\.jpg$", f)
|
|
if match:
|
|
max_seq = max(max_seq, int(match.group(1)))
|
|
seq = max_seq + 1
|
|
output_path = object_detect_dir / f"{image_name_safe}_{seq}.jpg"
|
|
|
|
points = []
|
|
d_labels = []
|
|
text_output = []
|
|
|
|
if target_image_id != "img_0":
|
|
text_output.append(
|
|
"[NOTE: Coordinates are automatically mapped back to"
|
|
" the original 1080x2400 screen space from"
|
|
f" {target_image_id}]"
|
|
)
|
|
|
|
for item in detected_items:
|
|
label_id = f"{prefix}{self.global_label_idx}"
|
|
self.global_label_idx += 1
|
|
pos = item.get("point")
|
|
if pos and isinstance(pos, list) and len(pos) == 2:
|
|
x_norm, y_norm = pos
|
|
|
|
t_img = cv2.imread(target_path)
|
|
t_h, t_w = t_img.shape[:2]
|
|
|
|
# Pixels in target image
|
|
x_target_pixel = (x_norm / 1000) * t_w
|
|
y_target_pixel = (y_norm / 1000) * t_h
|
|
|
|
# Pixels in original image
|
|
x_orig_pixel = (x_target_pixel / transform["scale_x"]) + transform[
|
|
"offset_x"
|
|
]
|
|
y_orig_pixel = (y_target_pixel / transform["scale_y"]) + transform[
|
|
"offset_y"
|
|
]
|
|
|
|
points.append([int(x_orig_pixel), int(y_orig_pixel)])
|
|
d_labels.append(label_id)
|
|
|
|
# Original image norm
|
|
x_orig_norm = int(max(0, min(1000, x_orig_pixel * 1000 / self.width)))
|
|
y_orig_norm = int(max(0, min(1000, y_orig_pixel * 1000 / self.height)))
|
|
|
|
text_output.append(
|
|
f'[{label_id}] "{item.get("label")}" at [{x_orig_norm},{y_orig_norm}]'
|
|
)
|
|
|
|
_ex.draw_dots(
|
|
self.screenshot_path,
|
|
points,
|
|
d_labels,
|
|
str(output_path),
|
|
color=color,
|
|
)
|
|
|
|
if failed_queries:
|
|
text_output.append(
|
|
" | [NOTE: Failed to detect the following queries:"
|
|
f" {', '.join(failed_queries)}]"
|
|
)
|
|
|
|
return {
|
|
"text": " | ".join(text_output),
|
|
"image_path": str(output_path),
|
|
}
|
|
|
|
except Exception as e:
|
|
_ex.logger.warning(f"Failed to process detection output for annotation: {e}")
|
|
return {"text": str(result), "image_path": None}
|
|
except Exception as e:
|
|
return {
|
|
"text": f"Error: Object detection failed: {e}",
|
|
"image_path": None,
|
|
}
|
|
|
|
@trace(type="tool", name="ask_perception_tool")
|
|
async def exec_ask_perception_tool(
|
|
self, search_query: str, nx: int, ny: int, detect_queries: list[str]
|
|
) -> dict:
|
|
if nx is None or ny is None or not (0 <= nx <= 1000) or not (0 <= ny <= 1000):
|
|
coords_invalid = True
|
|
else:
|
|
coords_invalid = False
|
|
|
|
# Run tasks concurrently in parallel!
|
|
tasks = [
|
|
self._search_ui_helper(search_query, prefix="X", color="green"),
|
|
self._search_by_coords_helper(nx, ny, prefix="X", color="blue")
|
|
if not coords_invalid
|
|
else asyncio.sleep(
|
|
0,
|
|
result={
|
|
"text": "Error: Invalid coordinates format",
|
|
"image_path": None,
|
|
},
|
|
),
|
|
self.exec_detect_objects(
|
|
detect_queries, target_image_id="img_0", prefix="D", color="red"
|
|
),
|
|
]
|
|
|
|
results = await asyncio.gather(*tasks)
|
|
|
|
# Consolidate text results
|
|
text_parts = []
|
|
|
|
xml_text = results[0].get("text", "No matches found.")
|
|
if xml_text and xml_text != "No matches found.":
|
|
text_parts.append(f"Text Search Results are: {xml_text}")
|
|
|
|
coord_text = results[1].get("text", "No elements found.")
|
|
if coord_text and coord_text != "No elements found.":
|
|
text_parts.append(f"Coordinate Search Results are: {coord_text}")
|
|
|
|
obj_text = results[2].get("text", "No objects detected.")
|
|
if obj_text:
|
|
if obj_text.startswith("No objects detected."):
|
|
note_part = obj_text.replace("No objects detected.", "").strip()
|
|
if note_part:
|
|
text_parts.append(f"Object Detection Results are: {note_part}")
|
|
else:
|
|
text_parts.append(f"Object Detection Results are: {obj_text}")
|
|
|
|
if not text_parts:
|
|
unified_text = "No matches or objects found across any perception method."
|
|
else:
|
|
unified_text = ". ".join(text_parts) + "."
|
|
|
|
# Collect image paths
|
|
image_paths = []
|
|
for r in results:
|
|
if isinstance(r, dict) and r.get("image_path"):
|
|
image_paths.append(r["image_path"])
|
|
|
|
return {
|
|
"text": unified_text,
|
|
"image_paths": image_paths,
|
|
}
|
|
|
|
@trace(type="tool", name="ask_image_processor")
|
|
async def exec_ask_image_processor(
|
|
self, instruction: str, target_image_id: str = "img_0"
|
|
) -> dict:
|
|
coder = ImageProcessor(self.ctx)
|
|
target_info = self.image_pool.get(target_image_id)
|
|
if not target_info:
|
|
return {
|
|
"text": f"Error: {target_image_id} not found in Image Pool.",
|
|
"image_path": None,
|
|
}
|
|
|
|
target_path = target_info["path"]
|
|
result = await coder.run(instruction, target_path)
|
|
|
|
if not isinstance(result, dict) or "error" in result:
|
|
err_msg = result.get("error") if isinstance(result, dict) else "Unknown error"
|
|
return {
|
|
"text": f"Image Processor failed: {err_msg}",
|
|
"image_paths": [],
|
|
}
|
|
|
|
outputs = result.get("outputs", [])
|
|
summary = result.get("summary", "")
|
|
|
|
parent_transform = target_info["transform"]
|
|
registered_ids = []
|
|
annotations_sections = []
|
|
image_paths = []
|
|
|
|
for entry in outputs:
|
|
new_id = f"img_{self.next_img_id}"
|
|
self.next_img_id += 1
|
|
|
|
new_path = entry["path"]
|
|
image_paths.append(new_path)
|
|
transform = entry["transform"]
|
|
|
|
final_scale_x = parent_transform["scale_x"] * transform["scale_x"]
|
|
final_scale_y = parent_transform["scale_y"] * transform["scale_y"]
|
|
final_offset_x = (
|
|
transform["offset_x"] / parent_transform["scale_x"]
|
|
) + parent_transform["offset_x"]
|
|
final_offset_y = (
|
|
transform["offset_y"] / parent_transform["scale_y"]
|
|
) + parent_transform["offset_y"]
|
|
|
|
self.image_pool[new_id] = {
|
|
"path": new_path,
|
|
"transform": {
|
|
"offset_x": final_offset_x,
|
|
"offset_y": final_offset_y,
|
|
"scale_x": final_scale_x,
|
|
"scale_y": final_scale_y,
|
|
},
|
|
"description": (f"Generated by coder from {target_image_id}: {summary}"),
|
|
}
|
|
registered_ids.append(new_id)
|
|
|
|
# Parse and translate annotations
|
|
annotations = entry.get("annotations", {})
|
|
if annotations:
|
|
lines = [f"{new_id}:"]
|
|
for label, coord in sorted(annotations.items(), key=lambda item: item[0]):
|
|
px, py = coord
|
|
sx = int(round((px / final_scale_x) + final_offset_x))
|
|
sy = int(round((py / final_scale_y) + final_offset_y))
|
|
lines.append(f" - [{label}]: [{sx}, {sy}]")
|
|
annotations_sections.append("\n".join(lines))
|
|
|
|
if registered_ids:
|
|
ids_str = ", ".join(registered_ids)
|
|
text = (
|
|
"Image Processor completed successfully. New image(s) saved as"
|
|
f" {ids_str} in Image Pool. Summary: {summary}"
|
|
)
|
|
if annotations_sections:
|
|
text += "\n\nAnnotations (wrt original screenshot):\n" + "\n".join(
|
|
annotations_sections
|
|
)
|
|
return {
|
|
"text": text,
|
|
"image_paths": image_paths,
|
|
}
|
|
else:
|
|
return {
|
|
"text": f"ImageProcessor error: {err_msg}",
|
|
"image_paths": [],
|
|
}
|
|
|
|
output_path = result.get("output_path")
|
|
trans = result.get("transform")
|
|
new_image_id = f"img_{len(self.image_pool)}"
|
|
if output_path and trans:
|
|
self.image_pool[new_image_id] = {
|
|
"path": output_path,
|
|
"transform": trans,
|
|
}
|
|
|
|
text_msg = (
|
|
f"Image processed successfully. New image saved as"
|
|
f" '{new_image_id}'. You can now use '{new_image_id}' as"
|
|
" 'target_image_id' in subsequent tool calls."
|
|
)
|
|
return {
|
|
"text": text_msg,
|
|
"image_path": output_path,
|
|
"image_id": new_image_id,
|
|
}
|
|
|
|
@trace(type="tool", name="get_ocr_list")
|
|
async def exec_get_ocr_list(self, prefix: str = "O", color: str = "green") -> dict:
|
|
_ex = facade()
|
|
if not self.image_name:
|
|
return {
|
|
"text": ("Error: UI data for the current screen is not found in Data Engine."),
|
|
"image_path": None,
|
|
}
|
|
|
|
try:
|
|
db_path = _ex.settings.DATA_ENGINE_DB_PATH
|
|
base_dir = _ex.settings.TRACES_PATH
|
|
storage = _ex.StorageManager(db_path=str(db_path), base_dir=base_dir)
|
|
|
|
record = storage.get_ui_record(self.image_name)
|
|
if not record or not record.ocr_result:
|
|
return {
|
|
"text": "No text elements detected on the screen.",
|
|
"image_path": None,
|
|
}
|
|
|
|
points = []
|
|
labels = []
|
|
text_output = []
|
|
|
|
for item in record.ocr_result:
|
|
text = item.get("text", "").strip()
|
|
bounds = item.get("bounds", [])
|
|
if text and len(bounds) == 4:
|
|
left, top, right, bottom = bounds
|
|
cx = (left + right) // 2
|
|
cy = (top + bottom) // 2
|
|
if 0 <= cx <= self.width and 0 <= cy <= self.height:
|
|
label_id = f"{prefix}{self.global_label_idx}"
|
|
self.global_label_idx += 1
|
|
points.append([cx, cy])
|
|
labels.append(label_id)
|
|
|
|
cx_norm = int(max(0, min(1000, cx * 1000 / self.width)))
|
|
cy_norm = int(max(0, min(1000, cy * 1000 / self.height)))
|
|
|
|
text_output.append(f"[{label_id}] '{text}' coords: [{cx_norm},{cy_norm}]")
|
|
|
|
if not points:
|
|
return {
|
|
"text": "No valid OCR results found.",
|
|
"image_path": None,
|
|
}
|
|
|
|
images_dir = base_dir / "images"
|
|
ocr_dir = images_dir / "ocr"
|
|
ocr_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
output_path = ocr_dir / f"{self.image_name}_{self.global_label_idx}.jpg"
|
|
_ex.draw_dots(self.screenshot_path, points, labels, str(output_path), color=color)
|
|
|
|
return {
|
|
"text": "\n".join(text_output),
|
|
"image_path": str(output_path),
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"text": f"Error retrieving or processing OCR results: {e}",
|
|
"image_path": None,
|
|
}
|
|
|
|
@trace(type="tool", name="inspect_region")
|
|
async def exec_inspect_region(
|
|
self,
|
|
x_min: int,
|
|
y_min: int,
|
|
x_max: int,
|
|
y_max: int,
|
|
zoom_factor: float = 2.0,
|
|
) -> dict:
|
|
_ex = facade()
|
|
try:
|
|
px_start = int(max(0, min(self.width, x_min * self.width / 1000.0)))
|
|
py_start = int(max(0, min(self.height, y_min * self.height / 1000.0)))
|
|
px_end = int(max(0, min(self.width, x_max * self.width / 1000.0)))
|
|
py_end = int(max(0, min(self.height, y_max * self.height / 1000.0)))
|
|
|
|
if px_end <= px_start or py_end <= py_start:
|
|
return {
|
|
"text": (
|
|
f"Error: Invalid crop coordinates. x_min={x_min},"
|
|
f" x_max={x_max}, y_min={y_min}, y_max={y_max}."
|
|
" Bounding box dimensions must be strictly positive."
|
|
),
|
|
"image_path": None,
|
|
}
|
|
|
|
img = cv2.imread(self.screenshot_path)
|
|
if img is None:
|
|
return {
|
|
"text": (
|
|
f"Error: Failed to read original screenshot at {self.screenshot_path}"
|
|
),
|
|
"image_path": None,
|
|
}
|
|
|
|
cropped = img[py_start:py_end, px_start:px_end]
|
|
|
|
if zoom_factor is None:
|
|
zoom_factor = 2.0
|
|
zoom_factor = max(1.0, min(4.0, float(zoom_factor)))
|
|
|
|
c_h, c_w = cropped.shape[:2]
|
|
if c_h > 0 and c_w > 0:
|
|
new_w = int(round(c_w * zoom_factor))
|
|
new_h = int(round(c_h * zoom_factor))
|
|
if new_w > 0 and new_h > 0:
|
|
cropped = cv2.resize(cropped, (new_w, new_h))
|
|
|
|
base_dir = _ex.settings.TRACES_PATH
|
|
images_dir = base_dir / "images"
|
|
inspect_region_dir = images_dir / "inspect_region"
|
|
inspect_region_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
image_name_safe = self.image_name or "temp_image"
|
|
output_path = inspect_region_dir / f"{image_name_safe}_{self.global_label_idx}.jpg"
|
|
cv2.imwrite(str(output_path), cropped)
|
|
|
|
return {
|
|
"text": (f"Inspected region coordinates: [{x_min}, {y_min}] to [{x_max}, {y_max}]"),
|
|
"image_path": str(output_path),
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"text": f"Error: Region inspection failed: {e}",
|
|
"image_path": None,
|
|
}
|