| """ |
| 图像处理工具模块 |
| 无状态的图像处理函数 |
| """ |
| import numpy as np |
| import tempfile |
| import os |
| import traceback |
| import math |
| from pathlib import Path |
| from PIL import Image, ImageDraw, ImageFont |
| import cv2 |
| from config import VIDEO_PLAYBACK_FPS, ROUTESTICK_OVERLAY_ACTION_TEXTS, get_ui_action_text |
|
|
| |
| |
| DEPRECATED_COORDINATE_AXES_ENVS = ["PatternLock", "RouteStick", "InsertPeg", "SwingXtimes"] |
| ENABLE_DEPRECATED_COORDINATE_AXES_OVERLAY = False |
|
|
|
|
| def _video_output_dirs(): |
| """视频输出目录候选(按优先级)。""" |
| current_dir = Path(__file__).resolve().parent |
| project_root = current_dir.parent |
| env_dir = os.environ.get("ROBOMME_TEMP_DEMOS_DIR") |
|
|
| candidates = [ |
| Path(env_dir).expanduser() if env_dir else None, |
| project_root / "temp_demos", |
| current_dir / "temp_demos", |
| Path.cwd() / "temp_demos", |
| Path(tempfile.gettempdir()) / "robomme_temp_demos", |
| ] |
|
|
| result = [] |
| seen = set() |
| for path in candidates: |
| if path is None: |
| continue |
| resolved = path.resolve() |
| key = str(resolved) |
| if key in seen: |
| continue |
| seen.add(key) |
| result.append(resolved) |
| return result |
|
|
|
|
| def _write_with_opencv(path, frames): |
| """imageio 不可用时使用 OpenCV 写视频。""" |
| if not frames: |
| return False |
|
|
| h, w = frames[0].shape[:2] |
| writer = cv2.VideoWriter( |
| path, |
| cv2.VideoWriter_fourcc(*"mp4v"), |
| VIDEO_PLAYBACK_FPS, |
| (w, h), |
| ) |
| if not writer.isOpened(): |
| return False |
|
|
| try: |
| for frame in frames: |
| if frame.shape[:2] != (h, w): |
| frame = cv2.resize(frame, (w, h), interpolation=cv2.INTER_LINEAR) |
| writer.write(cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)) |
| return True |
| finally: |
| writer.release() |
|
|
|
|
| def save_video(frames, suffix=""): |
| """ |
| 视频保存函数 - 使用imageio生成视频 |
| |
| 优化点: |
| 1. 使用imageio.mimwrite,不依赖FFmpeg编码器 |
| 2. 直接处理RGB帧,无需颜色空间转换 |
| 3. 自动处理编码,简单可靠 |
| """ |
| if not frames or len(frames) == 0: |
| return None |
| |
| try: |
| |
| processed_frames = [] |
| for f in frames: |
| if not isinstance(f, np.ndarray): |
| f = np.array(f) |
| if f.dtype != np.uint8: |
| if np.max(f) <= 1.0: |
| f = (f * 255).astype(np.uint8) |
| else: |
| f = f.clip(0, 255).astype(np.uint8) |
| if len(f.shape) == 2: |
| f = np.stack([f] * 3, axis=-1) |
| elif len(f.shape) == 3 and f.shape[2] == 4: |
| f = f[:, :, :3] |
| processed_frames.append(f) |
|
|
| imageio = None |
| try: |
| import imageio as _imageio |
| imageio = _imageio |
| except Exception: |
| imageio = None |
|
|
| for temp_dir in _video_output_dirs(): |
| try: |
| temp_dir.mkdir(parents=True, exist_ok=True) |
| fd, path = tempfile.mkstemp(suffix=f"_{suffix}.mp4", dir=str(temp_dir)) |
| os.close(fd) |
|
|
| if imageio is not None: |
| imageio.mimwrite( |
| path, |
| processed_frames, |
| fps=VIDEO_PLAYBACK_FPS, |
| quality=8, |
| macro_block_size=None, |
| ) |
| else: |
| ok = _write_with_opencv(path, processed_frames) |
| if not ok: |
| raise RuntimeError("OpenCV video writer failed") |
|
|
| if os.path.exists(path) and os.path.getsize(path) > 0: |
| return path |
|
|
| raise RuntimeError(f"generated empty video: {path}") |
| except Exception as e: |
| print(f"save_video failed in {temp_dir}: {e}") |
| traceback.print_exc() |
| try: |
| if "path" in locals() and path and os.path.exists(path): |
| os.remove(path) |
| except Exception: |
| pass |
|
|
| print("Error in save_video: all video output directories failed") |
| return None |
| except Exception as e: |
| print(f"Error in save_video: {e}") |
| traceback.print_exc() |
| return None |
|
|
|
|
| def concatenate_frames_horizontally(frames1, frames2=None, env_id=None): |
| """ |
| 处理 base frames 序列,添加标注和坐标系(已移除 wrist camera) |
| |
| Args: |
| frames1: base frames 视频帧列表 |
| frames2: 已弃用,保留以保持向后兼容,但不会被使用 |
| env_id: 环境ID,用于决定是否显示坐标系(可选) |
| |
| Returns: |
| 处理后的帧列表 |
| """ |
| |
| |
| show_coordinate_axes = ( |
| ENABLE_DEPRECATED_COORDINATE_AXES_OVERLAY |
| and (env_id in DEPRECATED_COORDINATE_AXES_ENVS if env_id else False) |
| ) |
| if not frames1: |
| return [] |
| |
| concatenated_frames = [] |
| |
| for i in range(len(frames1)): |
| |
| frame1 = frames1[i] if i < len(frames1) else frames1[-1] |
| |
| |
| if frame1 is not None: |
| if not isinstance(frame1, np.ndarray): |
| frame1 = np.array(frame1) |
| if frame1.dtype != np.uint8: |
| if np.max(frame1) <= 1.0: |
| frame1 = (frame1 * 255).astype(np.uint8) |
| else: |
| frame1 = frame1.clip(0, 255).astype(np.uint8) |
| if len(frame1.shape) == 2: |
| frame1 = np.stack([frame1] * 3, axis=-1) |
| else: |
| continue |
| |
| |
| actual_h, actual_w1 = frame1.shape[:2] |
| |
| |
| left_border_width = 0 |
| right_border_width = 0 |
| if show_coordinate_axes: |
| if env_id == "RouteStick": |
| left_border_width = 200 |
| right_border_width = 0 |
| else: |
| left_border_width = 150 |
| |
| if show_coordinate_axes: |
| |
| left_border = np.zeros((actual_h, left_border_width, 3), dtype=np.uint8) |
| |
| |
| concatenated_frame = np.concatenate([left_border, frame1], axis=1) |
| |
| |
| concatenated_pil = Image.fromarray(concatenated_frame) |
| |
| |
| left_border_pil = Image.new('RGB', (left_border_width, actual_h), (0, 0, 0)) |
| if env_id == "RouteStick": |
| |
| left_border_pil = draw_coordinate_axes(left_border_pil, position="left", rotate_180=False, env_id=env_id) |
| else: |
| |
| left_border_pil = draw_coordinate_axes(left_border_pil, position="left", rotate_180=True, env_id=env_id) |
| |
| |
| concatenated_pil.paste(left_border_pil, (0, 0)) |
| |
| |
| concatenated_frame = np.array(concatenated_pil) |
| else: |
| |
| concatenated_frame = frame1 |
| |
| concatenated_frames.append(concatenated_frame) |
| |
| return concatenated_frames |
|
|
|
|
| def draw_semicircle(draw, center, radius, color, width=2, half="lower", start_pos="left", end_pos="right", arrow_position="end", arrow_size=6): |
| """ |
| DEPRECATED: 仅供旧版 RouteStick 旋转示意图绘制使用(当前默认不再调用)。 |
| |
| 绘制半圆封装函数 |
| |
| Args: |
| draw: PIL ImageDraw object |
| center: (x, y) 圆心坐标 |
| radius: 半径 |
| color: 颜色 |
| width: 线宽 |
| half: "upper" (上半圆) or "lower" (下半圆) |
| start_pos: "left" or "right" (起始位置) |
| end_pos: "left" or "right" (结束位置) |
| arrow_position: "start" (箭头在起始位置) or "end" (箭头在结束位置) or None |
| arrow_size: 箭头大小 |
| """ |
| cx, cy = center |
| |
| |
| |
| |
| |
| |
| angle_map = { |
| "lower": {"right": 0, "left": 180}, |
| "upper": {"right": 360, "left": 180} |
| } |
| |
| start_angle = angle_map[half].get(start_pos, 0) |
| end_angle = angle_map[half].get(end_pos, 180) |
| |
| |
| step = 5 |
| if start_angle > end_angle: |
| step = -5 |
| |
| points = [] |
| |
| |
| for a in range(start_angle, end_angle + (1 if step > 0 else -1), step): |
| rad = math.radians(a) |
| x = cx + radius * math.cos(rad) |
| y = cy + radius * math.sin(rad) |
| points.append((x, y)) |
| |
| if len(points) < 2: |
| return |
| |
| |
| draw.line(points, fill=color, width=width) |
| |
| |
| if arrow_position: |
| if arrow_position == "start": |
| |
| arrow_center = points[0] |
| next_pt = points[1] |
| dx = next_pt[0] - arrow_center[0] |
| dy = next_pt[1] - arrow_center[1] |
| else: |
| |
| arrow_center = points[-1] |
| prev_pt = points[-2] |
| dx = arrow_center[0] - prev_pt[0] |
| dy = arrow_center[1] - prev_pt[1] |
|
|
| angle = math.atan2(dy, dx) |
| |
| |
| arrow_len = arrow_size * 1.5 |
| arrow_wing = arrow_size |
| |
| |
| |
| tip_x = arrow_center[0] + arrow_len * math.cos(angle) |
| tip_y = arrow_center[1] + arrow_len * math.sin(angle) |
| tip_pt = (tip_x, tip_y) |
| |
| |
| bx = arrow_center[0] - arrow_len * math.cos(angle) |
| by = arrow_center[1] - arrow_len * math.sin(angle) |
| |
| |
| w1x = bx + arrow_wing * math.cos(angle + math.pi/2) |
| w1y = by + arrow_wing * math.sin(angle + math.pi/2) |
| |
| w2x = bx + arrow_wing * math.cos(angle - math.pi/2) |
| w2y = by + arrow_wing * math.sin(angle - math.pi/2) |
| |
| draw.polygon([tip_pt, (w1x, w1y), (w2x, w2y)], fill=color) |
|
|
|
|
| def draw_coordinate_axes(img, position="right", rotate_180=False, env_id=None): |
| """ |
| DEPRECATED: 历史任务特化图像叠加函数(当前默认不再调用)。 |
| |
| 在图片外的黑色区域绘制坐标系,标注 forward/backward/left/right |
| |
| Args: |
| img: PIL Image 或 numpy array |
| position: "left" 或 "right",指定在左侧还是右侧绘制 |
| rotate_180: 如果为 True,将坐标系顺时针旋转180度(用于 base camera) |
| env_id: 环境ID,用于决定是否绘制特殊示意图(如 RouteStick 的旋转方向) |
| |
| Returns: |
| PIL Image with coordinate axes drawn |
| """ |
| if isinstance(img, np.ndarray): |
| img = Image.fromarray(img) |
| |
| img = img.copy() |
| draw = ImageDraw.Draw(img) |
| |
| |
| width, height = img.size |
| |
| |
| if env_id == "RouteStick" and (position == "right" or position == "left"): |
| lcw_text, lccw_text, rcw_text, rccw_text = [ |
| get_ui_action_text("RouteStick", action_text) |
| for action_text in ROUTESTICK_OVERLAY_ACTION_TEXTS |
| ] |
|
|
| |
| |
| illustration_width = 220 |
| |
| |
| try: |
| small_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 12) |
| except: |
| try: |
| small_font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 12) |
| except: |
| small_font = ImageFont.load_default() |
| |
| line_color = (255, 255, 255) |
| semicircle_radius = 15 |
| arrow_size = 3 |
| vertical_spacing = 5 |
| line_width = 2 |
| |
| |
| |
| item_height = semicircle_radius * 2 + 20 +10 |
| total_height = 4 * item_height + 3 * vertical_spacing |
| |
| |
| |
| layout_center_x = width // 2 |
| start_y = (height - total_height) // 2 -20 |
| |
| |
| |
| lcw_center_x = layout_center_x |
| lcw_center_y = start_y + item_height // 2 |
| |
| lccw_center_x = layout_center_x |
| lccw_center_y = lcw_center_y + item_height + vertical_spacing |
| |
| rcw_center_x = layout_center_x |
| rcw_center_y = lccw_center_y + item_height + vertical_spacing |
| |
| rccw_center_x = layout_center_x |
| rccw_center_y = rcw_center_y + item_height + vertical_spacing |
| |
| |
| draw_semicircle(draw, (lcw_center_x , lcw_center_y+15), semicircle_radius, line_color, line_width, half="upper", start_pos="left", end_pos="right", arrow_position="end", arrow_size=arrow_size) |
| |
| |
| lcw_bbox = draw.textbbox((0, 0), lcw_text, font=small_font) |
| lcw_text_width = lcw_bbox[2] - lcw_bbox[0] |
| lcw_text_height = lcw_bbox[3] - lcw_bbox[1] |
| lcw_text_x = lcw_center_x - lcw_text_width // 2 |
| lcw_text_y = lcw_center_y + semicircle_radius + 5 |
| draw.rectangle( |
| [(lcw_text_x - 2, lcw_text_y - 2), |
| (lcw_text_x + lcw_text_width + 2, lcw_text_y + lcw_text_height + 2)], |
| fill=(0, 0, 0) |
| ) |
| draw.text((lcw_text_x, lcw_text_y), lcw_text, fill=line_color, font=small_font) |
| |
| |
| draw_semicircle(draw, (lccw_center_x, lccw_center_y), semicircle_radius, line_color, line_width, half="lower", start_pos="left", end_pos="right", arrow_position="end", arrow_size=arrow_size) |
|
|
| |
| lccw_bbox = draw.textbbox((0, 0), lccw_text, font=small_font) |
| lccw_text_width = lccw_bbox[2] - lccw_bbox[0] |
| lccw_text_height = lccw_bbox[3] - lccw_bbox[1] |
| lccw_text_x = lccw_center_x - lccw_text_width // 2 |
| lccw_text_y = lccw_center_y + semicircle_radius + 5 |
| draw.rectangle( |
| [(lccw_text_x - 2, lccw_text_y - 2), |
| (lccw_text_x + lccw_text_width + 2, lccw_text_y + lccw_text_height + 2)], |
| fill=(0, 0, 0) |
| ) |
| draw.text((lccw_text_x, lccw_text_y), lccw_text, fill=line_color, font=small_font) |
| |
| |
| draw_semicircle(draw, (rcw_center_x , rcw_center_y), semicircle_radius, line_color, line_width, half="lower", start_pos="right", end_pos="left", arrow_position="end", arrow_size=arrow_size) |
|
|
| |
| rcw_bbox = draw.textbbox((0, 0), rcw_text, font=small_font) |
| rcw_text_width = rcw_bbox[2] - rcw_bbox[0] |
| rcw_text_height = rcw_bbox[3] - rcw_bbox[1] |
| rcw_text_x = rcw_center_x - rcw_text_width // 2 |
| rcw_text_y = rcw_center_y + semicircle_radius + 5 |
| draw.rectangle( |
| [(rcw_text_x - 2, rcw_text_y - 2), |
| (rcw_text_x + rcw_text_width + 2, rcw_text_y + rcw_text_height + 2)], |
| fill=(0, 0, 0) |
| ) |
| draw.text((rcw_text_x, rcw_text_y), rcw_text, fill=line_color, font=small_font) |
| |
| |
| draw_semicircle(draw, (rccw_center_x , rccw_center_y+15), semicircle_radius, line_color, line_width, half="upper",start_pos="right", end_pos="left", arrow_position="end", arrow_size=arrow_size) |
|
|
| |
| rccw_bbox = draw.textbbox((0, 0), rccw_text, font=small_font) |
| rccw_text_width = rccw_bbox[2] - rccw_bbox[0] |
| rccw_text_height = rccw_bbox[3] - rccw_bbox[1] |
| rccw_text_x = rccw_center_x - rccw_text_width // 2 |
| rccw_text_y = rccw_center_y + semicircle_radius + 5 |
| draw.rectangle( |
| [(rccw_text_x - 2, rccw_text_y - 2), |
| (rccw_text_x + rccw_text_width + 2, rccw_text_y + rccw_text_height + 2)], |
| fill=(0, 0, 0) |
| ) |
| draw.text((rccw_text_x, rccw_text_y), rccw_text, fill=line_color, font=small_font) |
| |
| |
| return img |
| |
| |
| axis_size = 60 |
| |
| |
| origin_x = width // 2 - axis_size // 2 |
| origin_y = height // 2 - axis_size // 2 |
| |
| |
| try: |
| font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14) |
| except: |
| try: |
| font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 14) |
| except: |
| font = ImageFont.load_default() |
| |
| |
| axis_length = axis_size - 20 |
| center_x = origin_x + axis_size // 2 |
| center_y = origin_y + axis_size // 2 |
| |
| |
| line_color = (255, 255, 255) |
| line_width = 2 |
| |
| |
| if rotate_180: |
| |
| |
| draw.line( |
| [(center_x - axis_length // 2, center_y), |
| (center_x + axis_length // 2, center_y)], |
| fill=line_color, width=line_width |
| ) |
| |
| |
| draw.line( |
| [(center_x, center_y - axis_length // 2), |
| (center_x, center_y + axis_length // 2)], |
| fill=line_color, width=line_width |
| ) |
| |
| |
| arrow_size = 5 |
| |
| draw.polygon( |
| [(center_x, center_y + axis_length // 2), |
| (center_x - arrow_size, center_y + axis_length // 2 - arrow_size), |
| (center_x + arrow_size, center_y + axis_length // 2 - arrow_size)], |
| fill=line_color |
| ) |
| |
| draw.polygon( |
| [(center_x, center_y - axis_length // 2), |
| (center_x - arrow_size, center_y - axis_length // 2 + arrow_size), |
| (center_x + arrow_size, center_y - axis_length // 2 + arrow_size)], |
| fill=line_color |
| ) |
| |
| draw.polygon( |
| [(center_x - axis_length // 2, center_y), |
| (center_x - axis_length // 2 + arrow_size, center_y - arrow_size), |
| (center_x - axis_length // 2 + arrow_size, center_y + arrow_size)], |
| fill=line_color |
| ) |
| |
| draw.polygon( |
| [(center_x + axis_length // 2, center_y), |
| (center_x + axis_length // 2 - arrow_size, center_y - arrow_size), |
| (center_x + axis_length // 2 - arrow_size, center_y + arrow_size)], |
| fill=line_color |
| ) |
| |
| |
| text_color = (255, 255, 255) |
| |
| |
| forward_text = "forward" |
| forward_bbox = draw.textbbox((0, 0), forward_text, font=font) |
| forward_width = forward_bbox[2] - forward_bbox[0] |
| forward_x = center_x - forward_width // 2 |
| forward_y = center_y + axis_length // 2 + 5 |
| draw.rectangle( |
| [(forward_x - 2, forward_y - 2), |
| (forward_x + forward_width + 2, forward_y + (forward_bbox[3] - forward_bbox[1]) + 2)], |
| fill=(0, 0, 0) |
| ) |
| draw.text((forward_x, forward_y), forward_text, fill=text_color, font=font) |
| |
| |
| backward_text = "backward" |
| backward_bbox = draw.textbbox((0, 0), backward_text, font=font) |
| backward_width = backward_bbox[2] - backward_bbox[0] |
| backward_x = center_x - backward_width // 2 |
| backward_y = center_y - axis_length // 2 - 20 |
| draw.rectangle( |
| [(backward_x - 2, backward_y - 2), |
| (backward_x + backward_width + 2, backward_y + (backward_bbox[3] - backward_bbox[1]) + 2)], |
| fill=(0, 0, 0) |
| ) |
| draw.text((backward_x, backward_y), backward_text, fill=text_color, font=font) |
| |
| |
| right_text = "right" |
| right_bbox = draw.textbbox((0, 0), right_text, font=font) |
| right_width = right_bbox[2] - right_bbox[0] |
| right_x = center_x - axis_length // 2 - right_width - 5 |
| right_y = center_y - (right_bbox[3] - right_bbox[1]) // 2 |
| draw.rectangle( |
| [(right_x - 2, right_y - 2), |
| (right_x + right_width + 2, right_y + (right_bbox[3] - right_bbox[1]) + 2)], |
| fill=(0, 0, 0) |
| ) |
| draw.text((right_x, right_y), right_text, fill=text_color, font=font) |
| |
| |
| left_text = "left" |
| left_bbox = draw.textbbox((0, 0), left_text, font=font) |
| left_width = left_bbox[2] - left_bbox[0] |
| left_x = center_x + axis_length // 2 + 5 |
| left_y = center_y - (left_bbox[3] - left_bbox[1]) // 2 |
| draw.rectangle( |
| [(left_x - 2, left_y - 2), |
| (left_x + left_width + 2, left_y + (left_bbox[3] - left_bbox[1]) + 2)], |
| fill=(0, 0, 0) |
| ) |
| draw.text((left_x, left_y), left_text, fill=text_color, font=font) |
| else: |
| |
| |
| draw.line( |
| [(center_x - axis_length // 2, center_y), |
| (center_x + axis_length // 2, center_y)], |
| fill=line_color, width=line_width |
| ) |
| |
| |
| draw.line( |
| [(center_x, center_y - axis_length // 2), |
| (center_x, center_y + axis_length // 2)], |
| fill=line_color, width=line_width |
| ) |
| |
| |
| arrow_size = 5 |
| |
| draw.polygon( |
| [(center_x, center_y - axis_length // 2), |
| (center_x - arrow_size, center_y - axis_length // 2 + arrow_size), |
| (center_x + arrow_size, center_y - axis_length // 2 + arrow_size)], |
| fill=line_color |
| ) |
| |
| draw.polygon( |
| [(center_x, center_y + axis_length // 2), |
| (center_x - arrow_size, center_y + axis_length // 2 - arrow_size), |
| (center_x + arrow_size, center_y + axis_length // 2 - arrow_size)], |
| fill=line_color |
| ) |
| |
| draw.polygon( |
| [(center_x + axis_length // 2, center_y), |
| (center_x + axis_length // 2 - arrow_size, center_y - arrow_size), |
| (center_x + axis_length // 2 - arrow_size, center_y + arrow_size)], |
| fill=line_color |
| ) |
| |
| draw.polygon( |
| [(center_x - axis_length // 2, center_y), |
| (center_x - axis_length // 2 + arrow_size, center_y - arrow_size), |
| (center_x - axis_length // 2 + arrow_size, center_y + arrow_size)], |
| fill=line_color |
| ) |
| |
| |
| text_color = (255, 255, 255) |
| |
| |
| forward_text = "forward" |
| forward_bbox = draw.textbbox((0, 0), forward_text, font=font) |
| forward_width = forward_bbox[2] - forward_bbox[0] |
| forward_x = center_x - forward_width // 2 |
| forward_y = center_y - axis_length // 2 - 20 |
| draw.rectangle( |
| [(forward_x - 2, forward_y - 2), |
| (forward_x + forward_width + 2, forward_y + (forward_bbox[3] - forward_bbox[1]) + 2)], |
| fill=(0, 0, 0) |
| ) |
| draw.text((forward_x, forward_y), forward_text, fill=text_color, font=font) |
| |
| |
| backward_text = "backward" |
| backward_bbox = draw.textbbox((0, 0), backward_text, font=font) |
| backward_width = backward_bbox[2] - backward_bbox[0] |
| backward_x = center_x - backward_width // 2 |
| backward_y = center_y + axis_length // 2 + 5 |
| draw.rectangle( |
| [(backward_x - 2, backward_y - 2), |
| (backward_x + backward_width + 2, backward_y + (backward_bbox[3] - backward_bbox[1]) + 2)], |
| fill=(0, 0, 0) |
| ) |
| draw.text((backward_x, backward_y), backward_text, fill=text_color, font=font) |
| |
| |
| right_text = "right" |
| right_bbox = draw.textbbox((0, 0), right_text, font=font) |
| right_width = right_bbox[2] - right_bbox[0] |
| right_x = center_x + axis_length // 2 + 5 |
| right_y = center_y - (right_bbox[3] - right_bbox[1]) // 2 |
| draw.rectangle( |
| [(right_x - 2, right_y - 2), |
| (right_x + right_width + 2, right_y + (right_bbox[3] - right_bbox[1]) + 2)], |
| fill=(0, 0, 0) |
| ) |
| draw.text((right_x, right_y), right_text, fill=text_color, font=font) |
| |
| |
| left_text = "left" |
| left_bbox = draw.textbbox((0, 0), left_text, font=font) |
| left_width = left_bbox[2] - left_bbox[0] |
| left_x = center_x - axis_length // 2 - left_width - 5 |
| left_y = center_y - (left_bbox[3] - left_bbox[1]) // 2 |
| draw.rectangle( |
| [(left_x - 2, left_y - 2), |
| (left_x + left_width + 2, left_y + (left_bbox[3] - left_bbox[1]) + 2)], |
| fill=(0, 0, 0) |
| ) |
| draw.text((left_x, left_y), left_text, fill=text_color, font=font) |
| |
| return img |
|
|
|
|
| def draw_marker(img, x, y): |
| """Draws a fluorescent yellow circle and cross at (x, y).""" |
| if isinstance(img, np.ndarray): |
| img = Image.fromarray(img) |
| |
| img = img.copy() |
| draw = ImageDraw.Draw(img) |
| r = 5 |
| marker_color = (204, 255, 0) |
| |
| draw.ellipse((x-r, y-r, x+r, y+r), outline=marker_color, width=2) |
| |
| draw.line((x-r, y, x+r, y), fill=marker_color, width=2) |
| draw.line((x, y-r, x, y+r), fill=marker_color, width=2) |
| return img |
|
|