| from __future__ import annotations |
|
|
| from pathlib import Path |
|
|
| from PIL import Image, ImageDraw |
|
|
| from driftwm.utils import ensure_dir |
|
|
|
|
| OUT_DIR = Path("experiments/reports/figures/task_schematics") |
| WORKSPACE = (0.0, 10.0, 0.0, 10.0) |
|
|
| BG = (247, 250, 252) |
| BORDER = (45, 62, 80) |
| START = (35, 91, 140) |
| GOAL = (203, 62, 74) |
| ACTIVE = (30, 139, 88) |
| PASSIVE = (145, 153, 163) |
| FLOW = (68, 135, 205) |
| SWITCH = (238, 184, 72) |
|
|
|
|
| def w2p(x: float, y: float, size: int = 640, pad: int = 58) -> tuple[int, int]: |
| xmin, xmax, ymin, ymax = WORKSPACE |
| px = int(pad + (x - xmin) / (xmax - xmin) * (size - 2 * pad)) |
| py = int(size - pad - (y - ymin) / (ymax - ymin) * (size - 2 * pad)) |
| return px, py |
|
|
|
|
| def draw_arrow( |
| draw: ImageDraw.ImageDraw, |
| p0: tuple[int, int], |
| p1: tuple[int, int], |
| color: tuple[int, int, int], |
| width: int = 6, |
| head: int = 18, |
| ) -> None: |
| import math |
|
|
| draw.line((p0, p1), fill=color, width=width) |
| dx = p1[0] - p0[0] |
| dy = p1[1] - p0[1] |
| angle = math.atan2(dy, dx) |
| for delta in (2.55, -2.55): |
| a = angle + delta |
| q = (int(p1[0] + head * math.cos(a)), int(p1[1] + head * math.sin(a))) |
| draw.line((p1, q), fill=color, width=width) |
|
|
|
|
| def draw_polyline_arrow( |
| draw: ImageDraw.ImageDraw, |
| pts: list[tuple[int, int]], |
| color: tuple[int, int, int], |
| width: int = 6, |
| ) -> None: |
| if len(pts) < 2: |
| return |
| draw.line(pts, fill=color, width=width, joint="curve") |
| draw_arrow(draw, pts[-2], pts[-1], color, width=width) |
|
|
|
|
| def circle(draw: ImageDraw.ImageDraw, center: tuple[int, int], radius: int, color: tuple[int, int, int], width: int = 0) -> None: |
| box = (center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius) |
| if width: |
| draw.ellipse(box, outline=color, width=width) |
| else: |
| draw.ellipse(box, fill=color) |
|
|
|
|
| def base(size: int = 640) -> tuple[Image.Image, ImageDraw.ImageDraw]: |
| img = Image.new("RGB", (size, size), BG) |
| draw = ImageDraw.Draw(img) |
| draw.rectangle((58, 58, size - 58, size - 58), outline=BORDER, width=3) |
| return img, draw |
|
|
|
|
| def draw_flow_arrows(draw: ImageDraw.ImageDraw, arrows: list[tuple[tuple[float, float], tuple[float, float]]]) -> None: |
| for start, end in arrows: |
| draw_arrow(draw, w2p(*start), w2p(*end), FLOW, width=5, head=15) |
|
|
|
|
| def save(img: Image.Image, name: str) -> None: |
| ensure_dir(OUT_DIR / "clean") |
| path = OUT_DIR / "clean" / name |
| img.save(path) |
| print(path) |
|
|
|
|
| def reach_target() -> Image.Image: |
| img, draw = base() |
| start = w2p(2.0, 2.0) |
| goal = w2p(8.0, 8.0) |
| draw_arrow(draw, start, goal, ACTIVE) |
| circle(draw, start, 13, START) |
| circle(draw, goal, 18, GOAL, width=6) |
| return img |
|
|
|
|
| def station_keeping() -> Image.Image: |
| img, draw = base() |
| center = w2p(5.0, 5.0) |
| circle(draw, center, 72, GOAL, width=6) |
| draw.line((center[0] - 22, center[1], center[0] + 22, center[1]), fill=GOAL, width=4) |
| draw.line((center[0], center[1] - 22, center[0], center[1] + 22), fill=GOAL, width=4) |
| draw_flow_arrows(draw, [((6.8, 6.8), (5.85, 5.95)), ((6.6, 3.3), (5.8, 4.2)), ((3.5, 6.8), (4.25, 5.9))]) |
| pts = [w2p(4.55, 4.25), w2p(5.2, 4.45), w2p(5.45, 5.15), w2p(4.85, 5.45), w2p(4.65, 4.8), w2p(5.0, 5.02)] |
| draw_polyline_arrow(draw, pts, ACTIVE, width=6) |
| circle(draw, pts[0], 12, START) |
| return img |
|
|
|
|
| def waypoint_square() -> Image.Image: |
| img, draw = base() |
| start = w2p(2.0, 2.0) |
| pts = [w2p(2.5, 2.5), w2p(7.5, 2.5), w2p(7.5, 7.5), w2p(2.5, 7.5)] |
| draw_polyline_arrow(draw, [start] + pts, ACTIVE, width=6) |
| circle(draw, start, 13, START) |
| for p in pts: |
| circle(draw, p, 13, GOAL) |
| return img |
|
|
|
|
| def waypoint_zigzag() -> Image.Image: |
| img, draw = base() |
| start = w2p(2.0, 2.0) |
| pts = [w2p(2.5, 7.0), w2p(4.2, 3.0), w2p(5.8, 7.0), w2p(7.5, 3.0)] |
| draw_polyline_arrow(draw, [start] + pts, ACTIVE, width=6) |
| circle(draw, start, 13, START) |
| for p in pts: |
| circle(draw, p, 13, GOAL) |
| return img |
|
|
|
|
| def make_contact_sheet(names: list[str]) -> None: |
| ensure_dir(OUT_DIR) |
| thumbs = [Image.open(OUT_DIR / "clean" / name).resize((240, 240), Image.Resampling.LANCZOS) for name in names] |
| sheet = Image.new("RGB", (2 * 280 + 40, 2 * 300 + 40), (246, 249, 251)) |
| draw = ImageDraw.Draw(sheet) |
| labels = [name.removeprefix("task_").removesuffix(".png") for name in names] |
| for i, (thumb, label) in enumerate(zip(thumbs, labels, strict=True)): |
| row, col = divmod(i, 2) |
| x = 20 + col * 280 |
| y = 20 + row * 300 |
| sheet.paste(thumb, (x, y)) |
| draw.text((x, y + 250), label, fill=(35, 45, 58)) |
| path = OUT_DIR / "task_schematics_contact_sheet.png" |
| sheet.save(path) |
| print(path) |
|
|
|
|
| def main() -> None: |
| tasks = { |
| "task_reach_target.png": reach_target(), |
| "task_station_keeping.png": station_keeping(), |
| "task_waypoint_square.png": waypoint_square(), |
| "task_waypoint_zigzag.png": waypoint_zigzag(), |
| } |
| for name, img in tasks.items(): |
| save(img, name) |
| make_contact_sheet( |
| [ |
| "task_reach_target.png", |
| "task_station_keeping.png", |
| "task_waypoint_square.png", |
| "task_waypoint_zigzag.png", |
| ] |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|