"""논문식 아키텍처 도식 두 장 — make_panels.py 의 실제 데이터 패널을 합친다. fig_viewpoint_model.png : 멀티뷰 시점 변환 모델 (구간별 3DGS 학습 → 리그 변환 렌더) fig_engine_pipeline.png : Vulkan 시뮬레이션 엔진 (CPU 시뮬레이션 루프 + GPU 렌더 그래프) 참고한 도식 형식: 3DGS (Kerbl 2023) 최적화 파이프라인, HUGSIM · OmniRe 개요도 — 단계마다 실제 데이터 그림, 데이터 흐름 화살표, gradient 되돌림 화살표. """ import json import os import sys from pathlib import Path import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt # noqa: E402 from matplotlib.patches import Circle, FancyArrowPatch, FancyBboxPatch, Polygon, Rectangle # noqa: E402 from PIL import Image # noqa: E402 P = Path(sys.argv[1]) # 패널 OUT = Path(sys.argv[2]) EXTRA = Path(sys.argv[3]) # sensors_sim.png, drive_6cam_still.jpg 가 있는 곳 ROOT = Path(__file__).resolve().parents[3] plt.rcParams["font.family"] = ["NanumBarunGothic", "DejaVu Sans"] # 수식 기호(Î, ᵀ, 아래첨자, −)는 DejaVu 로 plt.rcParams["axes.unicode_minus"] = False NAVY, BLUE, LIGHT, PALE, GRAY, ORANGE, GREEN = "#1F3B70", "#4472C4", "#DCE6F4", "#F4F7FB", "#404040", "#ED7D31", "#5B9B4C" class Canvas: def __init__(self, w, h): self.W, self.H = w, h self.fig = plt.figure(figsize=(w, h), facecolor="white") def ax(self, x, y, w, h): # 위쪽 기준 (y = 위에서 inch) a = self.fig.add_axes([x / self.W, 1 - (y + h) / self.H, w / self.W, h / self.H]) a.axis("off") return a def img(self, path, x, y, w, h, border=NAVY, lw=1.2, title=None, tsize=11, crop=None): im = Image.open(path).convert("RGB") if crop: im = im.crop(crop) a = self.ax(x, y, w, h) a.imshow(im, aspect="auto") a.set_xlim(0, im.width) a.set_ylim(im.height, 0) if border: self.rect(x, y, w, h, fc="none", ec=border, lw=lw, r=0.0) if title: self.text(x + w / 2, y - 0.12, title, size=tsize, bold=True, color=NAVY) return a def _fig_xy(self, x, y): return x / self.W, 1 - y / self.H def rect(self, x, y, w, h, fc=PALE, ec=LIGHT, lw=1.0, r=0.08, ls="-", z=0): bx = FancyBboxPatch(self._fig_xy(x, y + h), w / self.W, h / self.H, boxstyle=f"round,pad=0,rounding_size={r / self.W}" if r else "square,pad=0", transform=self.fig.transFigure, fc=fc, ec=ec, lw=lw, ls=ls, zorder=z) self.fig.add_artist(bx) return bx def text(self, x, y, s, size=11, bold=False, color=GRAY, ha="center", va="center", style="normal", z=8): self.fig.text(*self._fig_xy(x, y), s, fontsize=size, fontweight="bold" if bold else "normal", color=color, ha=ha, va=va, fontstyle=style, zorder=z) def arrow(self, x0, y0, x1, y1, color=BLUE, lw=2.2, ls="-", rad=0.0, head=14): a = FancyArrowPatch(self._fig_xy(x0, y0), self._fig_xy(x1, y1), transform=self.fig.transFigure, arrowstyle=f"-|>,head_length={head / 20},head_width={head / 34}", color=color, lw=lw, ls=ls, connectionstyle=f"arc3,rad={rad}", mutation_scale=10, zorder=5) self.fig.add_artist(a) def chip(self, x, y, s, fc=NAVY, color="white", size=10, w=None, h=0.3): w = w or (0.085 * len(s) * size / 10 + 0.22) self.rect(x - w / 2, y - h / 2, w, h, fc=fc, ec="none", r=0.12, z=6) self.text(x, y, s, size=size, bold=True, color=color) def save(self, path): self.fig.savefig(path, dpi=220, facecolor="white") plt.close(self.fig) def split(path, h_in, cut_in): """(a) · (b) 띠를 따로 저장 — 슬라이드에서 크게 보이게.""" im = Image.open(path) y = int(im.height * cut_in / h_in) im.crop((0, 0, im.width, y)).save(str(path).replace(".png", "_a.png")) im.crop((0, y, im.width, im.height)).save(str(path).replace(".png", "_b.png")) def stack(c, paths, x, y, w, h, dx=0.16, dy=0.16): """겹친 사진 묶음 (멀티뷰 입력).""" for i, p in enumerate(paths): c.img(p, x + i * dx, y + i * dy, w, h, border="white", lw=1.5) c.rect(x + i * dx, y + i * dy, w, h, fc="none", ec=NAVY, lw=0.8, r=0) # ================================================================== 그림 1: 멀티뷰 시점 변환 모델 c = Canvas(16, 8.6) c.text(0.25, 0.3, "(a) 구간별 3DGS 장면 학습 — CUDA · gsplat", size=15, bold=True, color=NAVY, ha="left") c.rect(0.15, 0.5, 15.7, 4.55, fc="#FAFBFD", ec=LIGHT, lw=1.2, r=0.12) # 입력 stack(c, [P / "gt_CAM_FRONT_LEFT.jpg", P / "gt_CAM_FRONT_RIGHT.jpg", P / "gt_CAM_FRONT.jpg"], 0.35, 0.95, 2.2, 1.24) c.text(1.5, 2.65, "멀티뷰 사진 (6캠, 800×450)", size=11, bold=True, color=NAVY) c.img(P / "lidar_init.png", 0.35, 2.95, 2.55, 1.43) c.text(1.62, 4.55, "정적 라이다 누적 (약 400 sweep)", size=11, bold=True, color=NAVY) c.text(1.62, 4.82, "차량 · 사람 박스 안 점 제거", size=9.5, color=GRAY) c.arrow(2.95, 3.65, 3.45, 2.7, color=GRAY, lw=1.6) c.chip(3.0, 3.1, "초기화", fc=GRAY, size=9) # 3D 가우시안 c.img(P / "gaussians_3d.png", 3.5, 1.05, 3.0, 1.69, title="3D 가우시안 장면 G") c.text(5.0, 3.0, "G = { μ, Σ = RSSᵀRᵀ, α, SH (3차) } × ≤ 200만", size=10, color=GRAY) c.text(5.0, 3.3, "MCMC: 낮은 α 재배치 + 위치 잡음", size=10, color=GRAY) c.arrow(6.55, 1.9, 7.25, 1.9) c.chip(6.9, 1.6, r"w2c·$\Delta$, K", fc=BLUE, size=8.5, w=0.95) # 미분 래스터라이저 c.img(P / "splats_tiles.png", 7.3, 1.05, 3.0, 1.69, title="미분 래스터라이저") c.text(8.8, 3.0, "투영 Σ′ = J W Σ Wᵀ Jᵀ", size=10, color=GRAY) c.text(8.8, 3.3, "16×16 타일 깊이 정렬 → α 블렌딩", size=10, color=GRAY) c.text(8.8, 3.6, "C = Σ cᵢ αᵢ Π (1 − αⱼ)", size=10, color=GRAY) c.arrow(10.35, 1.9, 10.85, 1.9) # 렌더 출력 c.img(P / "render_front.jpg", 10.9, 0.95, 2.1, 1.18, title="렌더 Î + (1−α)·S(d)") c.img(P / "render_depth.png", 10.9, 2.45, 2.1, 1.18) c.text(11.95, 3.83, "기대 깊이 D", size=10.5, bold=True, color=NAVY) c.arrow(13.05, 1.55, 13.45, 1.55) c.arrow(13.05, 3.05, 13.45, 3.05) # 감독 신호 + 손실 c.img(P / "masks.png", 13.5, 0.95, 2.1, 1.18, title="실제 사진 · 가림 마스크") c.img(P / "lidar_depth.png", 13.5, 2.45, 2.1, 1.18) c.text(14.55, 3.83, "라이다 깊이 D_lidar", size=10.5, bold=True, color=NAVY) c.rect(9.0, 4.15, 6.65, 0.62, fc=NAVY, ec="none", r=0.1) c.text(12.32, 4.46, "L = 0.8·L1 + 0.2·D-SSIM + 0.1·L_depth + 0.05·(L_sky + L_surf) + 0.01·(ᾱ + s̄)", size=10.5, bold=True, color="white") # gradient 되돌림 c.arrow(9.0, 4.46, 5.0, 3.55, color=ORANGE, lw=2.2, ls="--", rad=-0.08) c.text(6.7, 4.55, r"$\nabla$L (Adam) → G · $\Delta$ (카메라별 SE3) · 색 아핀 · 하늘 큐브맵", size=10.5, bold=True, color=ORANGE) c.text(14.55, 4.95, "빨강: 차량·사람 (손실 제외) 파랑: 하늘 (α → 0)", size=9, color=GRAY) # (b) 시점 변환 렌더 y0 = 5.35 c.text(0.25, y0, "(b) 멀티뷰 시점 변환 렌더 — Vulkan 오프스크린", size=15, bold=True, color=NAVY, ha="left") c.rect(0.15, y0 + 0.2, 15.7, 3.15, fc="#FFF9F4", ec="#F6D3B8", lw=1.2, r=0.12) c.img(P / "rig_transform.png", 0.45, y0 + 0.5, 2.25, 2.46, border=None) c.text(1.57, y0 + 3.08, "리그 변형 T (6캠 공통)", size=11, bold=True, color=NAVY) c.text(3.75, y0 + 1.25, "R′ = Rz(yaw)·R·Rx(−pitch)·Rz(roll)", size=10, color=GRAY) c.text(3.75, y0 + 1.55, "t′ = t + shift (cam → ego)", size=10, color=GRAY) c.text(3.75, y0 + 1.85, "자차 궤적 · 시각은 그대로", size=10, color=GRAY) c.arrow(2.75, y0 + 2.2, 4.6, y0 + 2.2, color=ORANGE) c.img(P / "segments_bev.png", 4.7, y0 + 0.45, 2.3, 2.3, border=None) c.text(5.85, y0 + 3.0, "프레임마다 담당 구간 모델", size=11, bold=True, color=NAVY) c.arrow(7.05, y0 + 1.6, 7.55, y0 + 1.6, color=ORANGE) lay = [["CAM_FRONT_LEFT", "CAM_FRONT", "CAM_FRONT_RIGHT"], ["CAM_BACK_RIGHT", "CAM_BACK", "CAM_BACK_LEFT"]] for r, row in enumerate(lay): for k, cam in enumerate(row): c.img(P / f"render_{cam}.jpg", 7.6 + k * 1.22, y0 + 0.55 + r * 0.7, 1.18, 0.66, border="white", lw=0.5) c.text(9.4, y0 + 2.15, "6캠 동시 렌더 (색 보정 없음 = 평균 카메라 색)", size=10.5, bold=True, color=NAVY) c.arrow(11.3, y0 + 1.25, 11.8, y0 + 1.25, color=ORANGE) # 변형 9종 (같은 씬 · 앞 카메라) var = ["left_1m", "left_0.5m", "right_0.5m", "right_1m", "high_0.4m", "low_0.3m", "pitch_down_3", "yaw_left_4", "yaw_right_4"] meta = json.load(open(ROOT / "outputs/aug/map/scene-0402/left_1m/meta.json")) fs = [s for s in meta["samples"] if s["cam"] == "CAM_FRONT"] smp = fs[len(fs) * 3 // 8] name = os.path.basename(smp["original_filename"]) for i, v in enumerate(var): r, k = divmod(i, 3) c.img(ROOT / f"outputs/aug/map/scene-0402/{v}/CAM_FRONT/{name}", 11.9 + k * 1.3, y0 + 0.42 + r * 0.8, 1.26, 0.71, border="white", lw=0.5) c.text(11.9 + k * 1.3 + 0.63, y0 + 0.42 + r * 0.8 + 0.62, v, size=7.5, bold=True, color="white") c.text(13.85, y0 + 2.92, "새 시점 6캠 × 9종 + meta.json (새 calibrated_sensor)", size=10.5, bold=True, color=NAVY) c.save(OUT / "fig_viewpoint_model.png") split(OUT / "fig_viewpoint_model.png", 8.6, 5.15) # ================================================================== 그림 2: Vulkan 시뮬레이션 엔진 c = Canvas(16, 8.45) c.text(0.25, 0.3, "(a) 시뮬레이션 루프 — CPU, 매 프레임 (closed-loop)", size=15, bold=True, color=NAVY, ha="left") c.rect(0.15, 0.5, 15.7, 3.25, fc="#FAFBFD", ec=LIGHT, lw=1.2, r=0.12) xs = [0.45, 3.0, 5.55, 8.1, 10.65, 13.2] yt, bw, bh = 0.95, 2.2, 1.6 titles = ["① 제어 입력", "② 자차 동역학", "③ 충돌 판정", "④ 구간 스트리밍", "⑤ 센서 레이캐스트", "⑥ 기록 · 관측"] subs = ["키보드 · 스크립트 (50 ms)\nVLA 가 들어올 자리", "키네마틱 자전거 모델\n10 ms 하위 스텝", "장애물 격자 0.2 m · 연석\n스침 2점 · 탈출 판정", "가까운 담당 궤적 · 3 m 히스테리시스\n이웃 3구간 비동기 로드", "노면 정착 → 라이다 32ch · 레이더 5대\n복셀 월드 DDA (4 ms)", "drive_log.csv · 6캠 PNG\n라이다 .bin · 레이더 .txt"] for i, x in enumerate(xs): c.rect(x, yt, bw, bh, fc="white", ec=LIGHT, lw=1.0, r=0.08) c.text(x + bw / 2, yt + bh + 0.22, titles[i], size=11.5, bold=True, color=NAVY) c.text(x + bw / 2, yt + bh + 0.62, subs[i], size=9, color=GRAY) if i < 5: c.arrow(x + bw + 0.03, yt + bh / 2, xs[i + 1] - 0.03, yt + bh / 2, lw=2) # ① 운전대 아이콘 a = c.ax(xs[0], yt, bw, bh) a.set_xlim(-1.4, 1.4) a.set_ylim(-1.0, 1.0) a.set_aspect("equal") a.add_patch(Circle((0, 0), 0.75, fill=False, lw=6, color=NAVY)) a.add_patch(Circle((0, 0), 0.18, color=NAVY)) for ang in (90, 210, 330): import numpy as np a.plot([0, 0.72 * np.cos(np.radians(ang))], [0, 0.72 * np.sin(np.radians(ang))], color=NAVY, lw=5) a.text(0, -0.93, "throttle · brake · steer", ha="center", fontsize=8.5, color=GRAY) # ② 자전거 모델 그림 a = c.ax(xs[1], yt, bw, bh) a.set_xlim(-1.6, 1.6) a.set_ylim(-1.15, 1.15) a.set_aspect("equal") a.add_patch(Rectangle((-0.45, -0.95), 0.9, 1.9, fc=LIGHT, ec=NAVY, lw=1.5)) for wx, wy, rot in ((-0.45, -0.6, 0), (0.45, -0.6, 0), (-0.45, 0.6, 18), (0.45, 0.6, 18)): t = matplotlib.transforms.Affine2D().rotate_deg_around(wx, wy, rot) + a.transData a.add_patch(Rectangle((wx - 0.08, wy - 0.22), 0.16, 0.44, fc=NAVY, ec="none", transform=t)) a.plot([0, 0], [-0.6, 0.6], color=ORANGE, lw=2) a.annotate("", xy=(0.55, 1.05), xytext=(0, 0.6), arrowprops=dict(arrowstyle="->", color=ORANGE, lw=1.8)) a.text(0.75, 0.95, "δ", fontsize=13, color=ORANGE, fontweight="bold") a.text(0.12, 0.0, "L", fontsize=12, color=ORANGE, fontweight="bold") a.text(-1.5, -1.1, "ẏaw = v·tan δ / L", fontsize=9, color=GRAY) c.img(P / "obstacles_crop.png", xs[2] + 0.3, yt + 0.02, 1.6, 1.56, border=None) c.img(P / "segments_bev.png", xs[3] + 0.3, yt + 0.02, 1.6, 1.56, border=None) c.img(EXTRA / "sensors_sim.png", xs[4] + 0.05, yt + 0.03, 2.1, 1.54, border=None) c.img(P / "drive_log.png", xs[5] + 0.05, yt + 0.2, 2.1, 1.25, border=None) c.arrow(xs[5] + bw / 2, yt - 0.02, xs[0] + bw / 2, yt - 0.02, color=ORANGE, lw=2, ls="--", rad=0.06) c.text(7.6, 0.72, "다음 프레임의 관측 → 다시 ① (모델의 제어가 다음 관측을 바꾼다)", size=10, bold=True, color=ORANGE) # (b) GPU 렌더 그래프 y0 = 4.05 c.text(0.25, y0, "(b) 렌더 그래프 — GPU, Vulkan (configs/render_graph.json, 배리어 자동)", size=15, bold=True, color=NAVY, ha="left") c.rect(0.15, y0 + 0.2, 15.7, 3.95, fc="#F7F9FD", ec=LIGHT, lw=1.2, r=0.12) c.arrow(13.2, 3.8, 13.2, y0 + 0.62, color=GRAY, lw=1.8) c.text(13.05, y0 + 0.42, "CPU → GPU: 카메라 UBO · 동적 선 (센서 점 · 자차 박스) · 구간 교체", size=9.5, color=GRAY, ha="right") bufs = [("gaussians", P / "gaussians_3d.png", "가우시안 SSBO\n(구간 모델, ≤ 200만)"), ("splats · keys", P / "splats.png", "화면 타원 · SH 색\n보이는 것만 압축"), ("keys (정렬)", P / "splats_sorted.png", "깊이 순서\n(색 = 정렬 순위)"), ("background", P / "sky_panorama.jpg", "학습 하늘 큐브맵\n· IBL"), ("color · depth_acc", P / "render_front.jpg", "RGBA16F 색\n+ rg32f 거리 누적"), ("post → 화면", EXTRA / "drive_6cam_still.jpg", "6캠 · 3인칭 · BEV\nPNG 쓰기 스레드")] passes = [("preprocess", "compute", "2.0 ms"), ("sort", "radix 8bit×4", "0.44 ms"), ("background", "raster", ""), ("scene", "raster (간접)", "4.4 ms"), ("weather · fxaa", "compute", "")] bx0, bw2, bh2, gap = 0.35, 1.8, 1.02, 0.92 for i, (nm, path, sub) in enumerate(bufs): x = bx0 + i * (bw2 + gap) c.img(path, x, y0 + 0.95, bw2, bh2, title=nm, tsize=10.5) c.text(x + bw2 / 2, y0 + 2.35, sub, size=9, color=GRAY) if i == 4: c.img(P / "render_depth.png", x, y0 + 2.75, bw2, bh2) c.text(x + bw2 / 2, y0 + 3.95, "depth_acc (rg32f 거리)", size=8.5, color=GRAY) if i < 5: pn, kind, tms = passes[i] cx = x + bw2 + gap / 2 c.arrow(x + bw2 + 0.04, y0 + 1.5, x + bw2 + gap - 0.04, y0 + 1.5, color=NAVY, lw=2) c.chip(cx, y0 + 1.2, pn, fc=NAVY, size=8, w=gap + 0.06, h=0.26) c.text(cx, y0 + 1.85, kind, size=8, bold=True, color=ORANGE) if tms: c.text(cx, y0 + 2.07, tms, size=8, color=GRAY) c.rect(0.4, y0 + 2.85, 9.9, 1.1, fc="white", ec=LIGHT, lw=1, r=0.08) c.text(0.6, y0 + 3.1, "bindless — 셰이더가 슬롯 번호로 SSBO · 이미지를 고른다 (새 패스에 디스크립터 코드 불필요)", size=9.5, color=GRAY, ha="left") c.text(0.6, y0 + 3.4, "렌더 그래프 — 패스마다 uses (자원 · 용도) 선언 → 배리어 · 레이아웃 전환 자동 삽입", size=9.5, color=GRAY, ha="left") c.text(0.6, y0 + 3.7, "셰이더 그래프 — 후처리 노드 (JSON) → GLSL 생성 · 리플렉션 · Tracy 패스별 GPU 시간", size=9.5, color=GRAY, ha="left") c.save(OUT / "fig_engine_pipeline.png") split(OUT / "fig_engine_pipeline.png", 8.45, 3.79) print("saved", OUT)