| | """ |
| | 图像处理工具模块 |
| | 无状态的图像处理函数 |
| | """ |
| | 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 |
| |
|