import copy import os import json from PIL import Image from pptx import Presentation from pptx.chart.data import CategoryChartData from pptx.dml.color import RGBColor from pptx.enum.chart import XL_CHART_TYPE, XL_LABEL_POSITION from pptx.enum.shapes import MSO_SHAPE from pptx.enum.text import MSO_ANCHOR, PP_ALIGN from pptx.oxml.ns import qn from pptx.util import Inches, Pt A = "/tmp/claude-1000/-home-mm-Downloads-vkgs-sim/8bf0e66c-975d-4257-af27-61bc25ee4316/scratchpad/deck/" NAVY, BLUE, LIGHT, PALE, GRAY, ORANGE, WHITE = (RGBColor(0x1F, 0x3B, 0x70), RGBColor(0x44, 0x72, 0xC4), RGBColor(0xDC, 0xE6, 0xF4), RGBColor(0xF2, 0xF5, 0xFA), RGBColor(0x40, 0x40, 0x40), RGBColor(0xED, 0x7D, 0x31), RGBColor(0xFF, 0xFF, 0xFF)) FONT = "맑은 고딕" KEEP = {"슬라이드 번호 개체 틀 1", "제목 1", "그룹 6", "TextBox 9", "TextBox 4"} def shape(sl, name): for sh in sl.shapes: if sh.name == name: return sh raise KeyError(name) def set_runs_text(tf, text): """첫 문단 첫 run 의 서식으로 text 만 남긴다.""" p0 = tf.paragraphs[0] for p in tf.paragraphs[1:]: p._p.getparent().remove(p._p) runs = p0.runs if not runs: p0.text = text return runs[0].text = text for r in runs[1:]: r._r.getparent().remove(r._r) def set_paragraphs(tf, items): """첫 문단을 본으로 문단마다 복사 (글머리표·간격 유지).""" p0 = tf.paragraphs[0]._p for p in tf.paragraphs[1:]: p._p.getparent().remove(p._p) parent = p0.getparent() proto = copy.deepcopy(p0) parent.remove(p0) for it in items: p = copy.deepcopy(proto) rs = p.findall(qn("a:r")) for r in rs[1:]: p.remove(r) if rs: rs[0].find(qn("a:t")).text = it parent.append(p) def body(sl, tab, heading, bullets): for sh in list(sl.shapes): if sh.name not in KEEP: sh._element.getparent().remove(sh._element) set_runs_text(shape(sl, "제목 1").text_frame, "VKGS-Sim") set_runs_text(shape(sl, "TextBox 4").text_frame, tab) grp = shape(sl, "그룹 6") for c in grp.shapes: if c.has_text_frame and c.text_frame.text.strip(): set_runs_text(c.text_frame, heading) set_paragraphs(shape(sl, "TextBox 9").text_frame, bullets) return 1.82 + 0.28 * sum(1 + len(b) // 95 for b in bullets) + 0.15 # 본문 다음 y (12pt, 줄간격 150%) def notes(sl, text): sl.notes_slide.notes_text_frame.text = text def tb(sl, x, y, w, h, text, size=12, bold=False, color=GRAY, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, font=FONT): t = sl.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h)) tf = t.text_frame tf.word_wrap = True tf.margin_left = tf.margin_right = Inches(0.04) tf.margin_top = tf.margin_bottom = Inches(0.02) tf.vertical_anchor = anchor for i, line in enumerate(text.split("\n")): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = align r = p.add_run() r.text = line r.font.size, r.font.bold, r.font.color.rgb, r.font.name = Pt(size), bold, color, font return t def box(sl, x, y, w, h, text, fill=LIGHT, color=GRAY, size=12, bold=False, line=None, align=PP_ALIGN.CENTER, title=None): s = sl.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h)) s.adjustments[0] = 0.08 s.fill.solid() s.fill.fore_color.rgb = fill if line: s.line.color.rgb = line s.line.width = Pt(1.25) else: s.line.fill.background() s.shadow.inherit = False tf = s.text_frame tf.word_wrap = True tf.margin_left = tf.margin_right = Inches(0.1) tf.margin_top = tf.margin_bottom = Inches(0.06) tf.vertical_anchor = MSO_ANCHOR.MIDDLE if title is None else MSO_ANCHOR.TOP lines = ([(title, True, size + 1)] if title else []) + [(l, bold, size) for l in text.split("\n") if l] for i, (l, b, sz) in enumerate(lines): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = PP_ALIGN.CENTER if (title and i == 0) else align p.space_after = Pt(3) r = p.add_run() r.text = l r.font.size, r.font.bold, r.font.color.rgb, r.font.name = Pt(sz), b, color, FONT return s def arrow(sl, x, y, w=0.36, h=0.24, color=BLUE): s = sl.shapes.add_shape(MSO_SHAPE.RIGHT_ARROW, Inches(x), Inches(y), Inches(w), Inches(h)) s.fill.solid() s.fill.fore_color.rgb = color s.line.fill.background() s.shadow.inherit = False return s def image(sl, path, x, y, w=None, h=None): iw, ih = Image.open(path).size if w and h: # 상자 안에 맞춘다 (contain), 가운데 s = min(w / iw, h / ih) ww, hh = iw * s, ih * s return sl.shapes.add_picture(path, Inches(x + (w - ww) / 2), Inches(y + (h - hh) / 2), Inches(ww), Inches(hh)) if w: return sl.shapes.add_picture(path, Inches(x), Inches(y), Inches(w), Inches(w * ih / iw)) return sl.shapes.add_picture(path, Inches(x), Inches(y), Inches(h * iw / ih), Inches(h)) def table(sl, x, y, w, rows, colw, size=11, rowh=0.3, align_right=()): t = sl.shapes.add_table(len(rows), len(rows[0]), Inches(x), Inches(y), Inches(w), Inches(rowh * len(rows))).table for j, cw in enumerate(colw): t.columns[j].width = Inches(w * cw) for i, row in enumerate(rows): t.rows[i].height = Inches(rowh) for j, v in enumerate(row): c = t.cell(i, j) c.margin_left = c.margin_right = Inches(0.08) c.margin_top = c.margin_bottom = Inches(0.03) c.vertical_anchor = MSO_ANCHOR.MIDDLE c.fill.solid() c.fill.fore_color.rgb = NAVY if i == 0 else (PALE if i % 2 == 0 else WHITE) tf = c.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.text = "" p.alignment = PP_ALIGN.RIGHT if (j in align_right and i > 0) else PP_ALIGN.LEFT r = p.add_run() r.text = str(v) r.font.size, r.font.name = Pt(size), FONT r.font.bold = i == 0 r.font.color.rgb = WHITE if i == 0 else GRAY return t def kpi(sl, x, y, w, num, label, color=NAVY): s = box(sl, x, y, w, 1.05, "", fill=PALE, line=LIGHT) tb(sl, x + 0.1, y + 0.08, w - 0.2, 0.55, num, size=26, bold=True, color=color, align=PP_ALIGN.CENTER) tb(sl, x + 0.1, y + 0.64, w - 0.2, 0.35, label, size=11, color=GRAY, align=PP_ALIGN.CENTER) return s def caption(sl, x, y, w, text): return tb(sl, x, y, w, 0.32, text, size=12, bold=True, color=GRAY, align=PP_ALIGN.CENTER) import sys prs = Presentation(sys.argv[1]) slides = list(prs.slides) # ------------------------------------------------------------------ 1 표지 s = slides[0] for sh in s.shapes: if sh.name == "제목 1": set_runs_text(sh.text_frame, "VKGS-Sim: 3D Gaussian Splatting 기반 Vulkan 주행 시뮬레이터") elif sh.name == "부제목 2": sh.text_frame.text = "nuScenes 싱가포르 실주행 로그로 만든 closed-loop 시뮬레이터" for r in sh.text_frame.paragraphs[0].runs: r.font.size = Pt(16) elif sh.name == "텍스트 개체 틀 3": set_runs_text(sh.text_frame, "2026. 10. 12.") elif sh.name == "텍스트 개체 틀 6": set_paragraphs(sh.text_frame, ["국민대학교 차량지능연구실", "kimjunhan"]) notes(s, "VKGS-Sim 진행 보고. 실제 주행 로그로 만든 3D 장면 위에서 주행 모델이 운전하고, 그 결과가 다음 관측을 바꾸는 시뮬레이터를 소개한다.") # ------------------------------------------------------------------ 2 목차 s = slides[1] for sh in s.shapes: if sh.name == "텍스트 개체 틀 2": set_paragraphs(sh.text_frame, ["연구 배경 및 목표", "시뮬레이션 엔진 아키텍처", "멀티뷰 시점 변환 모델 아키텍처", "데이터 및 학습", "시뮬레이션 결과", "진행 현황 및 계획"]) notes(s, "다섯 부분으로 발표한다.") def divider(sl, title, note): for sh in sl.shapes: if sh.name == "제목 1": set_runs_text(sh.text_frame, title) notes(sl, note) divider(slides[2], "연구 배경 및 목표", "먼저 왜 closed-loop 시뮬레이터가 필요한지와 3DGS 를 설명한다.") # ------------------------------------------------------------------ 4 연구 배경 s = slides[3] y = body(s, "연구 배경", "연구 배경 — open-loop 평가의 한계와 closed-loop 시뮬레이션", [ "기존 주행 모델 평가는 기록된 영상을 입력하고 정답 궤적과 비교하는 open-loop 방식임", "모델이 다르게 운전해도 다음 영상은 바뀌지 않으므로, 실수가 누적되는 상황을 평가할 수 없음", "본 연구는 실주행 로그로 3D 장면을 재구성하고, 모델의 제어로 움직인 자차 위치에서 새 6캠 영상을 렌더링하는 closed-loop 시뮬레이터를 개발함", "목표: 단일 RTX 3060 Ti (8 GB) 에서 싱가포르 낮 맵 전체 (284씬, 30 km) 를 실시간으로 렌더링", ]) y = max(y, 3.55) labels = ["6캠 관측", "VLM / VLA", "조향 · 가속", "자차 적분 · 충돌", "3DGS 렌더"] bw, gap, x0 = 2.0, 0.55, 0.85 for i, l in enumerate(labels): x = x0 + i * (bw + gap) box(s, x, y + 0.35, bw, 0.75, l, fill=NAVY if i == 4 else LIGHT, color=WHITE if i == 4 else NAVY, size=14, bold=True) if i < 4: arrow(s, x + bw + 0.1, y + 0.6) back = s.shapes.add_shape(MSO_SHAPE.U_TURN_ARROW, Inches(x0 + 0.3), Inches(y + 1.25), Inches(4 * (bw + gap) + bw - 0.6), Inches(0.55)) back.rotation = 180 back.fill.solid() back.fill.fore_color.rgb = LIGHT back.line.fill.background() back.shadow.inherit = False tb(s, x0, y + 1.95, 4 * (bw + gap) + bw, 0.32, "렌더 결과가 다음 관측이 된다 — 제어가 장면을 바꾸는 루프", size=12, bold=True, color=NAVY, align=PP_ALIGN.CENTER) box(s, 0.85, y + 2.45, 5.6, 0.95, "기록 영상을 재생 → 정답 궤적과 비교\n모델이 벗어나도 영상은 그대로", title="open-loop (기존)", fill=PALE, line=LIGHT, size=11) box(s, 6.9, y + 2.45, 5.6, 0.95, "모델의 조향 · 가속으로 자차 이동\n그 위치에서 6캠 영상을 새로 렌더", title="closed-loop (VKGS-Sim)", fill=PALE, line=BLUE, size=11) notes(s, "핵심 난점은 원래 궤적에서 벗어나면 학습 카메라가 본 적 없는 시점이 된다는 것이다. 그래서 시점 변환 증강과 구간 분할 같은 작업이 이어진다.") # ------------------------------------------------------------------ 5 3DGS s = slides[4] y = body(s, "연구 배경", "3D Gaussian Splatting 개요", [ "장면을 수백만 개의 3D 가우시안 (위치 · 공분산 · 불투명도 · 구면조화 색) 으로 표현하는 명시적 장면 표현", "카메라로 투영 → 깊이순 정렬 → 앞에서부터 알파 블렌딩하여 이미지를 생성: C = Σ cᵢ αᵢ Π(1 − αⱼ)", "미분 가능하여 실제 사진과의 오차로 파라미터를 학습하고, 학습 후에는 래스터화로 실시간 렌더링이 가능함", ]) table(s, 0.85, y + 0.2, 6.1, [ ["파라미터", "의미", "바이트"], ["위치 μ (xyz)", "가우시안 중심", "12"], ["스케일 + 회전 (쿼터니언)", "공분산 Σ = R S Sᵀ Rᵀ", "28"], ["불투명도 α", "얼마나 가리는가", "4"], ["구면조화 계수 (3차, 48개)", "보는 방향에 따른 색", "192"], ["합계", "구간 모델당 200만 개 ≈ 0.5 GB", "236"], ], [0.38, 0.47, 0.15], size=12, rowh=0.38, align_right=(2,)) steps = [("① 투영", "Σ → 화면 2D 공분산\n보는 방향의 SH 색"), ("② 정렬", "보이는 가우시안만\nGPU radix 정렬"), ("③ 블렌딩", "앞에서부터 누적\n가까운 것이 가림")] for i, (t, d) in enumerate(steps): x = 7.35 + i * 1.85 box(s, x, y + 0.2, 1.55, 2.25, d, title=t, fill=PALE, line=BLUE, size=11) if i < 2: arrow(s, x + 1.57, y + 1.2, w=0.26, h=0.2) tb(s, 7.35, y + 2.6, 5.3, 0.6, "학습: PyTorch + gsplat (CUDA) / 시뮬레이터: Vulkan 으로 같은 식을 재구현 — 두 결과가 PSNR 56 dB 로 일치", size=11, bold=True, color=NAVY) notes(s, "NeRF 처럼 신경망이 아니라 명시적인 가우시안 집합이다. 2D 공분산은 Σ' = J W Σ Wᵀ Jᵀ. 블렌딩 순서가 결과를 정하므로 정렬이 핵심 비용이다.") # ------------------------------------------------------------------ 7 전체 아키텍처 s = slides[5] y = body(s, "연구 배경", "전체 시스템 아키텍처", [ "데이터 계층 → 재구성 (CUDA) → 렌더링 (Vulkan) → closed-loop 시뮬레이션의 4단계로 구성됨", "학습은 미분이 필요하여 PyTorch + gsplat 을, 시뮬레이터는 메시 · 조명 · 후처리를 한 프레임에 섞기 위해 Vulkan 을 사용함", "모든 단계가 단일 RTX 3060 Ti 8 GB 에서 동작하며, 조정값은 configs/ (sim.json, 학습 프리셋, 작업 JSON) 에 둠", ]) stages = [("① 데이터 계층", "NFS 위 nuScenes 를 읽기 전용으로\n한 번 훑어 씬 인덱스 캐시\n(6캠 · 포즈 · 캘리브 · 라이다 · 박스)\n인덱스 35.8초 · 씬 로드 7 ms"), ("② 재구성 (CUDA)", "PyTorch + gsplat 3DGS 학습\n라이다 초기화 · 깊이 감독\n차량·사람 가림 · 하늘 큐브맵\n60 m 구간 단위"), ("③ 렌더링 (Vulkan)", "vkgs_viewer: bindless\n+ 렌더 그래프 (JSON)\nGPU radix 정렬 · 6캠 · BEV\n구간 스트리밍 · 오프스크린"), ("④ 시뮬레이션", "자전거 모델 자차\n장애물 · 연석 충돌\n라이다 · 레이더 레이캐스트\n스크립트 제어 → 관측 저장")] bw = 2.75 for i, (t, d) in enumerate(stages): x = 0.85 + i * (bw + 0.38) box(s, x, y + 0.25, bw, 1.75, d, title=t, fill=NAVY if i == 3 else PALE, color=WHITE if i == 3 else GRAY, line=None if i == 3 else BLUE, size=11) if i < 3: arrow(s, x + bw + 0.04, y + 1.01, w=0.3, h=0.22) tb(s, 0.85, y + 2.15, 11.8, 0.32, "결과물: outputs/gs_day//seg (구간 모델) → 렌더 / 증강 (outputs/aug/map · outputs/aug/aug_artfixer) / 시뮬레이터", size=11, bold=True, color=NAVY, align=PP_ALIGN.CENTER) notes(s, "학습 VRAM 약 2.3~3 GB. 여러 단계 작업은 configs/jobs/<이름>.json + nusim run 으로 돌린다.") # ------------------------------------------------------------------ 9 렌더러 성능 s = slides[11] y = body(s, "엔진 구조", "Vulkan 렌더러 성능", [ "gsplat 1.4.0 과 같은 셰이더 수식을 Vulkan 으로 구현하여 학습기 렌더와 PSNR 56 dB 로 일치함", "정렬이 프레임의 58% 를 차지하여, 보이는 가우시안만 압축한 뒤 GPU radix 정렬로 교체함", ]) for i, (n, l) in enumerate([("56 dB", "학습기 렌더와의 PSNR"), ("13×", "정렬 31~35 ms → 2.7 ms"), ("16 ms", "1600×900 한 장 (오프스크린)"), ("0.19 s", "구간 모델 교체 (200만 개)")]): kpi(s, 0.85 + i * 3.0, y + 0.2, 2.75, n, l) table(s, 0.85, y + 1.55, 11.75, [ ["구성", "내용"], ["bindless 리소스", "셰이더가 슬롯 번호로 버퍼 · 이미지를 고른다 — 새 패스에 디스크립터 코드가 필요 없음"], ["렌더 그래프", "프레임 구성을 JSON 이 정하고 배리어는 그래프가 자동으로 삽입 (검증 레이어 동기화 검증)"], ["정확도", "RGBA16F 래스터 (32비트 대비 64 dB), 거리 누적 r32f, 학습기 비교 시 FXAA 끔"], ["렌더 타겟 · 정렬", "16/32비트 · radix/bitonic · FXAA 를 그래프 JSON 과 sim.json 으로 선택"], ], [0.22, 0.78], size=12, rowh=0.4) tb(s, 0.85, y + 3.7, 11.75, 0.3, "* 수치는 학습과 GPU 를 함께 쓰는 상태에서 측정", size=10, color=GRAY) notes(s, "56 dB 는 nusim render compare 로 같은 카메라에서 학습기 렌더와 비교한 값. 800×450 에서는 장당 6.8 ms.") divider(slides[18], "데이터 및 학습", "nuScenes 데이터, 학습 방법, 재구성 결과, 증강 결과를 설명한다.") # ------------------------------------------------------------------ 12 데이터셋 s = slides[19] y = body(s, "데이터", "nuScenes 싱가포르 데이터셋", [ "nuScenes 850씬 중 싱가포르 383씬을 사용 (보스턴 467씬 제외) — 6캠 1600×900 약 11.7 Hz, 이미지 532,157장, 32채널 라이다 + 레이더 5대", "밤 영상은 너무 어두워 재구성 품질이 낮으므로 낮 284씬 (주행 경로 30 km) 으로 맵을 구성함 — 그중 77% 가 3 m/s 이상의 빠른 주행", ]) table(s, 0.85, y + 0.2, 3.9, [["지역", "전체", "낮"], ["one-north", "183", "183"], ["queenstown", "115", "82"], ["holland village", "85", "19"], ["합계", "383", "284"]], [0.52, 0.24, 0.24], size=12, rowh=0.4, align_right=(1, 2)) tb(s, 0.85, y + 2.35, 3.9, 0.9, "키프레임 (어노테이션) 2 Hz, 그 사이 sweep 까지 학습에 사용. 원본은 NFS 공용 볼륨에서 읽기 전용.", size=11, color=GRAY) image(s, A + "gt_6cam.jpg", 5.05, y + 0.2, w=7.6, h=2.85) caption(s, 5.05, y + 3.1, 7.6, "실제 6캠 사진 (scene-0402) — 위: 앞왼쪽 · 앞 · 앞오른쪽 / 아래: 뒤오른쪽 · 뒤 · 뒤왼쪽") notes(s, "CAM_BACK 만 HFOV 89° (fx 809), 나머지 5대는 약 64° (fx 약 1260). 밤 → 낮 변환 (CycleGAN-Turbo) 은 센서 잡음을 키워 쓸 수 없었다.") # ------------------------------------------------------------------ 13 학습 방법 s = slides[20] y = body(s, "학습", "학습 방법 및 구간 분할", [ "라이다 누적으로 가우시안을 초기화하고 깊이 감독 (λ = 0.1), gsplat MCMC 밀도 제어 (최대 200만 개), 하늘 큐브맵 분리를 적용함", "차량 · 사람은 3D 박스로 가려 환경 전용 맵을 학습하고, 카메라별 색 아핀 · 외부 파라미터를 함께 보정함 — 구간당 15k 스텝, 약 20분", "빠른 씬은 통째로 학습하면 적합 자체가 실패하여 60 m 구간 (10 m 겹침) 으로 나눠 학습함 — 284씬 → 696 학습 단위", ]) cd = CategoryChartData() cd.categories = ["씬 통째", "구간 1", "구간 2", "구간 3", "구간 4", "구간 5"] cd.add_series("정적 PSNR (dB)", (16.23, 23.78, 22.76, 23.04, 23.02, 22.70)) gf = s.shapes.add_chart(XL_CHART_TYPE.COLUMN_CLUSTERED, Inches(0.85), Inches(y + 0.1), Inches(7.2), Inches(3.3), cd) ch = gf.chart ch.has_legend = False ch.has_title = True ch.chart_title.text_frame.text = "scene-0402 (11.9 m/s, 235 m) 정적 PSNR (dB)" ch.chart_title.text_frame.paragraphs[0].runs[0].font.size = Pt(12) ch.chart_title.text_frame.paragraphs[0].runs[0].font.bold = True pl = ch.plots[0] pl.gap_width = 60 pl.has_data_labels = True pl.data_labels.font.size = Pt(11) pl.data_labels.number_format = "0.00" pl.data_labels.number_format_is_linked = False pl.data_labels.position = XL_LABEL_POSITION.OUTSIDE_END ser = pl.series[0] ser.format.fill.solid() ser.format.fill.fore_color.rgb = BLUE pt0 = ser.points[0] pt0.format.fill.solid() pt0.format.fill.fore_color.rgb = RGBColor(0xA5, 0xA5, 0xA5) va = ch.value_axis va.minimum_scale, va.maximum_scale = 0, 30 va.has_major_gridlines = True va.major_gridlines.format.line.color.rgb = RGBColor(0xE0, 0xE0, 0xE0) va.tick_labels.font.size = Pt(10) ch.category_axis.tick_labels.font.size = Pt(11) box(s, 8.35, y + 0.15, 4.3, 3.2, "통째 학습은 학습 손실부터 높았다\n(L1 0.135 vs 0.05) — 일반화가 아니라 적합 실패\n\n원인은 장면 길이\n(전략 · 깊이 감독 · 카메라 보정은 원인 아님)\n\n구간 경계 프레임 4쌍에서\n색 · 기하 이음매 없음", title="+7.5 dB (16.23 → 23.78)", fill=PALE, line=BLUE, size=11) notes(s, "낮 284씬 중 77% (218씬) 가 3 m/s 를 넘어 구간 분할이 필수다. 구간은 같은 길이로 나누되 60 m 이하, 이웃 10 m 겹침. 1단계 기준선은 scene-0003 6만 스텝 PSNR 25.62 / SSIM 0.804.") # ------------------------------------------------------------------ 14 재구성 결과 s = slides[21] y = body(s, "학습", "재구성 결과", [ "학습 시점 재현: 같은 카메라 · 같은 순간의 실제 사진과 재구성 렌더를 비교 (scene-0008) — 렌더에서는 주차 차량이 제거됨 (환경 전용 맵)", "구간 정적 PSNR 중앙값 24.7 dB (84 단위), 하위 10% 22.1 dB, 학습 실패 0건", ]) image(s, A + "recon_vs_gt.jpg", 0.85, y + 0.15, w=11.8, h=3.3) caption(s, 0.85, y + 3.5, 5.9, "실제 사진 ↑") caption(s, 6.75, y + 3.5, 5.9, "3DGS 렌더 (차량 제거) ↑") notes(s, "이 그림은 학습에 쓴 시점의 재현이다. 학습에서 뺀 옆 차선 주행의 실제 사진으로 재면 약 16.5 dB 로 크게 떨어진다 — 새 시점 품질이 남은 핵심 과제다.") # ------------------------------------------------------------------ 15 증강 결과 s = slides[22] y = body(s, "증강", "시점 변환 데이터 증강 결과", [ "변형 9종: 좌우 0.5 · 1 m 이동, 높이 +0.4 / −0.3 m, 아래로 3°, 좌우 4° 회전 — 키프레임 2 Hz, 차량 미포함 (환경 전용 맵)", "현재 32씬 완료: 70,824장 (14 GB) — 284씬 완료 시 약 61만 장으로 낮 데이터가 9배가 됨", ]) pic = image(s, A + "aug_grid.jpg", 0.85, y + 0.15, w=11.8) gh = pic.height / 914400 names = ["원본 (실제)", "left 1 m", "left 0.5 m", "right 0.5 m", "right 1 m", "high 0.4 m", "low 0.3 m", "pitch down 3°", "yaw left 4°", "yaw right 4°"] cw = 11.8 / 5 for i, n in enumerate(names): r, c = divmod(i, 5) t = tb(s, 0.85 + c * cw + 0.05, y + 0.15 + r * gh / 2 + 0.04, 1.3, 0.26, n, size=10, bold=True, color=WHITE) t.fill.solid() t.fill.fore_color.rgb = ORANGE if i == 0 else NAVY table(s, 0.85, y + 0.3 + gh, 11.8, [["항목", "내용"], ["결과 경로", "outputs/aug/map////<원본 파일명>.jpg + meta.json (바뀐 calibrated_sensor, 원본 파일 · 포즈)"], ["한계", "옆 1 m 에서 노면 번짐 · 지운 차량 자리 (오른쪽 승합차) 가 흐릿하게 남음"]], [0.15, 0.85], size=11, rowh=0.34) notes(s, "새 씬이 늘어나는 것이 아니라 같은 씬의 '다른 카메라 배치' 버전이 9개 생긴다. 학습이 씬을 끝내면 바로 이어서 증강한다 (nusim run day_map).") divider(slides[23], "시뮬레이션 결과", "시뮬레이터 구성, 구간 스트리밍과 충돌, 센서, 시각화를 보인다.") # ------------------------------------------------------------------ 17 시뮬레이션 구성 s = slides[24] y = body(s, "시뮬레이션", "시뮬레이션 구성", [ "재구성 장면 위에 자차 · 물리 · 센서를 얹어, 모델의 제어 → 다음 관측의 루프를 구성함", ]) table(s, 0.85, y + 0.15, 11.8, [ ["구성 요소", "구현"], ["자차", "키네마틱 자전거 모델 (조향 · 가속 · 후진), glTF 차량 (Suzuki Carry) 바퀴 구름 · 서스펜션, 정지 상태로 시작"], ["충돌", "정적 라이다로 만든 장애물 지도 (0.2 m) + 주행가능영역 경계 (연석). 스침 (2점 이하) 과 빠져나가는 움직임은 허용"], ["맵 로더", "구간 모델 스트리밍 — 자차 위치의 구간을 그리고 이웃 구간을 미리 읽어 경계에서 0.19초 만에 교체"], ["센서", "6캠 (학습과 같은 캘리브) + 레이캐스트 라이다 1대 · 레이더 5대, 패널에서 보기 켜기"], ["제어 인터페이스", "키보드 또는 스크립트 (--controls, 50 ms 고정 스텝) → 구간마다 6캠 · 센서 관측 저장 (VLA 연결 자리)"], ["기록", "drive_log.csv: 위치 · 속도 · 도로 이탈 · 원래 궤적에서 벗어난 거리 · 충돌 종류"], ], [0.18, 0.82], size=12, rowh=0.52) notes(s, "합성 날씨 (비 · 안개 · 밤, 슬라이더 UI) 도 구현돼 있지만 논문 범위에서 뺐다. 다른 교통 참가자는 아직 없다.") # ------------------------------------------------------------------ 18 스트리밍 · 충돌 s = slides[25] y = body(s, "시뮬레이션", "구간 스트리밍 및 충돌 판정 개선", [ "구간 모델 스트리밍: 버퍼를 가장 큰 구간 크기로 한 번 잡고 경계에서 내용만 교체, 3 m 히스테리시스로 깜빡임 방지", "장애물 지도 개선: 긴 씬의 라이다 번짐으로 지면이 낮게 잡히고 가로수 아래 노면이 '구조물' 로 남던 문제 수정 — 노면 위 칸 11,091 → 83", ]) image(s, A + "segrun_seams.jpg", 0.85, y + 0.15, w=6.2, h=3.2) caption(s, 0.85, y + 3.38, 6.2, "구간 경계 4곳 (행) 의 경계 전후 프레임 — 이음매 없음") image(s, A + "obst_cmp.png", 7.35, y + 0.15, w=5.3, h=3.2) caption(s, 7.35, y + 3.38, 5.3, "장애물 지도 개선 전 / 후 (파랑 원래 경로, 빨강 충돌 지점)") notes(s, "지면 = 첫 빽빽한 10 cm 층, 노면에서 남기는 칸 = 지면 위 2~3 m 에 점이 있는 칸 (기둥 · 줄기 · 벽). 스침 판정과 빠져나가기 판정으로 '보이지 않는 물체에 갇히는' 문제도 해결했다.") # ------------------------------------------------------------------ 19 센서 s = slides[26] y = body(s, "센서", "레이캐스트 라이다 · 레이더 센서", [ "카메라처럼 자차에 장착하는 센서 (CARLA ray-cast 방식) — 3DGS 깊이가 아니라 실제 라이다 누적 복셀 월드 (지면 0.5 m 높이 지도) 에 광선을 쏨", "빔이 늘 지나가는 칸을 지우는 빈 공간 깎기로 지운 차량의 잔해를 제거 — 실제 라이다 대비 같은 방향 거리 오차 중앙값 24% → 3.9~5.9%", ]) image(s, A + "sensors_sim.png", 0.85, y + 0.15, w=4.3, h=3.2) caption(s, 0.85, y + 3.38, 4.3, "시뮬 라이다 + 레이더 5대 (10 m/s 주행)") image(s, A + "sensors_real.png", 5.35, y + 0.15, w=4.3, h=3.2) caption(s, 5.35, y + 3.38, 4.3, "실제 nuScenes 라이다 (같은 순간)") table(s, 9.9, y + 0.15, 2.75, [["거리 오차", "중앙값"], ["처음", "24%"], ["scene-0402", "5.9%"], ["scene-0003", "3.9%"]], [0.6, 0.4], size=11, rowh=0.38, align_right=(1,)) tb(s, 9.9, y + 1.8, 2.75, 1.6, "라이다 32채널 한 바퀴 4 ms\n레이더: 도플러 vr · 보정 속도\n노면 반사 제거\nO 키로 nuScenes 형식 .bin 저장\n\n레이더 색: 빨강 다가옴 · 파랑 멀어짐 · 노랑 정지", size=10, color=GRAY) notes(s, "설정은 sim.json sensors (HDL-32E 근사, ARS 408 근사, nuScenes 장착 위치 근사). 실제 쪽 가까운 덩어리는 그 순간의 실제 차 · 사람으로 환경 전용 맵에는 없다. 남은 것: 다른 차량의 실제 높이 · 속도, 라이다 세기.") # ------------------------------------------------------------------ 20 시각화 s = slides[27] y = body(s, "시각화", "시뮬레이터 시각화 — 실제 6캠 vs 재구성 6캠", [ "scene-0402 (235 m) 를 구간 5개로 이어 6캠으로 렌더 — 238프레임 × 6캠을 13초에 렌더링 (장당 6.8 ms, 800×450), 구간 교체 4회", ]) image(s, A + "gt_6cam.jpg", 0.85, y + 0.15, w=5.85, h=2.25) caption(s, 0.85, y + 2.45, 5.85, "실제 nuScenes 6캠 ↑") vw, vh = 5.85, 5.85 * 720 / 1920 # 영상 1920×720 — 위 사진과 같은 너비 import os if os.environ.get("NOVIDEO"): image(s, A + "drive_6cam_still.jpg", 6.8, y + 0.15, w=5.85, h=2.25) else: mv = s.shapes.add_movie(A + "drive_6cam.mp4", Inches(6.8), Inches(y + 0.15 + (2.25 - vh) / 2), Inches(vw), Inches(vh), poster_frame_image=A + "drive_6cam_still.jpg", mime_type="video/mp4") mv.name = "주행 영상" caption(s, 6.8, y + 2.45, 5.85, "VKGS-Sim 재구성 주행 영상 (클릭하면 재생) ↑") box(s, 0.85, y + 2.95, 11.8, 0.75, "주행 영상: 20초, 12 fps, 6캠 모자이크 — 위: 앞왼쪽 · 앞 · 앞오른쪽 / 아래: 뒤오른쪽 · 뒤 · 뒤왼쪽\n시뮬레이터에서 직접 주행: nusim render view scene-0402 --model outputs/gs_day/scene-0402", fill=PALE, line=LIGHT, size=11) notes(s, "오른쪽 영상을 클릭하면 재생된다. 재생 전 표시 화면은 왼쪽 실제 사진과 같은 순간(120번째 프레임)이다. 차량 · 사람은 지운 환경 전용 맵이다.") divider(slides[34], "진행 현황 및 계획", "싱가포르 낮 맵 구축 현황과 향후 계획.") # ------------------------------------------------------------------ 22 진행 현황 s = slides[36] y = body(s, "진행 현황", "싱가포르 낮 맵 구축 현황 (2026-10-12)", [ "학습 단위 696개 중 253개 (36%) 완료 — 새 도로를 많이 덮는 씬 순서로 학습 중, 정적 PSNR 중앙값 24.67 dB", "최근 48시간 속도 (시간당 약 2.6 단위) 기준 남은 443단위에 약 7일 — 10월 19일 무렵 완료 예상", ]) for i, (n, l) in enumerate([("253 / 696", "학습 단위 (36%)"), ("75", "증강이 끝난 씬 (변형 9종)"), ("161k", "증강 이미지 (32 GB)"), ("51씬", "one-north 지역 맵 (구간 149개)")]): kpi(s, 0.85 + i * 3.0, y + 0.15, 2.75, n, l) table(s, 0.85, y + 1.5, 11.75, [["항목", "10월 10일", "10월 12일"], ["완료 학습 단위", "84", "253"], ["증강 씬 · 이미지", "32씬 · 70.8k", "75씬 · 160.9k"], ["지역 맵", "씬 하나 안의 구간만", "one-north 51씬 · 구간 149개 · 센서 51/51"], ["다시 학습할 단위", "0", "72 (NAS 장애 때 실패 — 다음 회차에 자동 재학습)"]], [0.3, 0.25, 0.45], size=12, rowh=0.4) notes(s, "실패 72개는 10월 10일 저녁 NAS 장애(약 40분) 동안 데이터셋을 못 읽은 것으로, 대기 · 재시도 로직을 넣은 뒤로 새 실패는 없다. 디스크 여유 641 GB.") # ------------------------------------------------------------------ 21b 지역 맵 (새) s = slides[28] y = body(s, "시뮬레이션", "지역 맵 — 씬 사이를 이어 달리기 (one-north)", [ "학습된 60개 씬 · 179개 구간을 공통 원점 하나로 묶음 — 자차에 가장 가까운 구간을 그리고 가까운 3개를 미리 읽어 씬 경계를 넘어 계속 주행", "라이다 · 레이더 복셀 월드는 씬마다 따로 두고 (지역 전체 한 격자는 1 GB 초과) 달리는 구간의 씬 것으로 교체 — 179개 구간 모두 센서 데이터 확인", ]) image(s, A + "region_onenorth.png", 0.85, y + 0.15, h=3.3) caption(s, 0.85, y + 3.48, 3.4, "one-north 지역 맵: 회색 = 복셀 월드, 색 선 = 구간, ★ 출발") image(s, A + "strip_all.png", 4.45, y + 0.15, w=5.87, h=3.3) caption(s, 4.45, y + 3.48, 5.87, "주행 화면 — 전방 카메라 3대 · BEV 를 화면 위쪽 한 줄에 (V · N)") table(s, 10.5, y + 0.15, 2.15, [["항목", "값"], ["씬", "60"], ["구간", "179"], ["복셀 월드", "60 / 60"], ["출발", "0042"]], [0.55, 0.45], size=11, rowh=0.38, align_right=(1,)) tb(s, 10.5, y + 2.2, 2.15, 1.4, "휠 · 가운데 버튼 드래그로 차 뒤 시점 거리 0.4~5배\n\n충돌은 아직 연석만 (지역 단위 장애물 지도는 다음 단계)", size=10, color=GRAY) notes(s, "nusim render map singapore-onenorth. 센서가 일부 구간에서 꺼지던 원인: 씬의 구간이 하나 더 학습될 때마다 복셀 월드를 '낡음' 으로 보고 껐다. 복셀은 씬 전체 라이다로 만들어 기하가 그대로이므로, 만든 원점을 기록해 그대로 쓰게 고쳤다.") # ------------------------------------------------------------------ 21d 센서 주행 영상 (새) V = "/home/mm/Downloads/vkgs-sim/outputs/videos/" def movie(sl, name, x, y, w, h): """영상 (NOVIDEO=1 이면 표지 화면만 — PDF 용). 1280×720 을 상자 안에 맞춘다.""" vh = min(h, w * 9 / 16) vw = vh * 16 / 9 xx, yy = x + (w - vw) / 2, y + (h - vh) / 2 if os.environ.get("NOVIDEO"): return image(sl, V + name + "_poster.jpg", xx, yy, w=vw, h=vh) mv = sl.shapes.add_movie(V + name + ".mp4", Inches(xx), Inches(yy), Inches(vw), Inches(vh), poster_frame_image=V + name + "_poster.jpg", mime_type="video/mp4") mv.name = name return mv s = slides[29] y = body(s, "시뮬레이션", "주행 영상 — 카메라 · BEV · 라이다 · 레이더를 모두 켠 자동 주행", [ "자동 주행 (T 키 — 기록 궤적을 pure pursuit 로 따라가며 20 km/h) 으로 one-north 지역 맵을 달리며 창 화면 그대로 녹화", "위쪽 줄 = 전방 카메라 3대 · BEV (V · N), 장면 위 = 라이다 32채널 (높이별 색) · 레이더 5대 (빨강 다가옴 · 파랑 멀어짐 · 노랑 정지)", ]) movie(s, "sensors_all", 0.85, y + 0.12, 8.6, 4.0) caption(s, 0.85, y + 4.15, 8.6, "클릭하면 재생 (20초) — nusim run demo_videos") table(s, 9.7, y + 0.15, 2.95, [["항목", "값"], ["녹화", "20 fps 고정 스텝"], ["길이", "400프레임 · 20초"], ["라이다", "HDL-32E 근사"], ["레이더", "ARS 408 근사 × 5"], ["센서 1회", "약 4 ms (CPU)"]], [0.42, 0.58], size=10, rowh=0.34) tb(s, 9.7, y + 2.35, 2.95, 1.7, "창 화면 녹화: --record-screen DIR\n(UI · 미리보기 줄 포함)\n\n자동 주행: --autopilot 또는 T,\n화살표를 누르면 손으로", size=10, color=GRAY) notes(s, "센서는 3DGS 깊이가 아니라 라이다 누적 복셀 월드에 레이캐스트한다. 지역 맵에서는 달리는 구간의 씬 복셀 월드로 바꿔 끼운다.") # ------------------------------------------------------------------ 21e 날씨 영상 (새) s = slides[30] y = body(s, "시뮬레이션", "날씨 주행 영상 — 아침 · 비 · 눈", [ "같은 자동 주행 경로를 날씨 프리셋만 바꿔 녹화 — 날씨는 3DGS 렌더 뒤 후처리 (weather.comp) 와 자차 주변 월드 공간 강수 입자 (precip)", ]) for i, (nm, t) in enumerate([("weather_morning", "아침 — 조도 0.6 · 옅은 안개"), ("weather_rain", "비 — 강수 0.5 · 젖은 노면"), ("weather_snow", "눈 — 강설 0.5 · 쌓인 눈")]): movie(s, nm, 0.85 + i * 3.97, y + 0.15, 3.85, 2.17) caption(s, 0.85 + i * 3.97, y + 2.35, 3.85, t) table(s, 0.85, y + 2.85, 11.75, [["프리셋", "조도", "구름", "비", "눈", "안개", "효과"], ["아침 (morning)", "0.6", "0.15", "-", "-", "0.15", "낮은 해 · 아침 안개"], ["비 (rain)", "0.75", "0.8", "0.5", "-", "-", "빗줄기 모션 블러 · 젖은 노면 반사 · 렌즈 물방울"], ["눈 (snow)", "0.8", "-", "-", "0.5", "-", "눈송이 초점 흐림 · 노면 눈 덮임 0.4"]], [0.16, 0.08, 0.08, 0.08, 0.08, 0.08, 0.44], size=10, rowh=0.32) notes(s, "세 영상을 각각 클릭해 재생. 날씨 조정값은 configs/sim.json weather (프리셋 11개, 창의 날씨 패널에서 바로 바꿈).") # ------------------------------------------------------------------ 21f 카메라 종류 (새) s = slides[31] y = body(s, "센서", "카메라 센서 종류 — RGB · 깊이 · 시맨틱 세그멘테이션 · 흑백", [ "날씨 패널의 '카메라' 버튼 (또는 F3) 으로 바꿈 — 화면 · 전방 3대 · BEV 미리보기 · 녹화에 함께 적용, 6캠 관측 저장(O)은 늘 RGB", ]) image(s, A + "camera_modes.jpg", 0.85, y + 0.12, w=7.4, h=4.2) rows = [("깊이", "렌더할 때 쌓는 거리 버퍼 (Σzα / Σα) 의 표면 거리를 로그 눈금 색으로 — 추가 렌더 없음"), ("시맨틱", "가우시안마다 가까운 lidarseg 라이다 점의 다수결 라벨 (11종, Cityscapes 색) → SH 색 대신 라벨 색으로 스플랫"), ("흑백", "날씨 · ISP 를 거친 최종 영상의 루마"), ("구현", "렌더 그래프 camera_mode 패스 1개 + 전처리 셰이더 분기 — 같은 프레임에서 바꿈, 다시 학습 없음")] for i, (t, d) in enumerate(rows): box(s, 8.45, y + 0.12 + i * 1.06, 4.2, 0.96, d, title=t, fill=PALE, line=BLUE, size=10) notes(s, "시맨틱 라벨: nusim recon semantic (모델 옆 semantic.rgba). 라이다가 안 닿는 높은 건물 윗부분은 이웃 라벨에서 번지고, 그래도 없으면 건물, 하늘 구는 하늘로 칠한다. 반투명 파사드(창문)는 덮임으로 나눠 라벨 색 그대로 둔다.") # ------------------------------------------------------------------ 21g 성능 비교 — 품질 (새) def highlight(t, rows): """표에서 우리 행을 강조 (연한 파랑 배경 · 굵게 · 남색 글자).""" for i in rows: for j in range(len(t.columns)): c = t.cell(i, j) c.fill.solid() c.fill.fore_color.rgb = LIGHT for r in c.text_frame.paragraphs[0].runs: r.font.bold, r.font.color.rgb = True, NAVY def metric_boxes(sl, y, items): w = (11.8 - 0.15 * (len(items) - 1)) / len(items) for i, (t, d) in enumerate(items): box(sl, 0.85 + i * (w + 0.15), y, w, 0.76, d, title=t, fill=PALE, line=BLUE, size=9) s = slides[32] y = body(s, "성능 비교", "nuScenes 렌더링 품질 비교 — 관련 논문이 nuScenes 에서 보고한 값", [ "논문마다 장면 · 해상도 · 평가 분할이 달라 묶음(평가 조건)끼리는 직접 비교할 수 없음 — 같은 묶음 안에서만 순위가 의미 있음", ]) metric_boxes(s, y + 0.05, [ ("PSNR (dB) ↑", "렌더 영상과 실제 사진의 픽셀 오차를 로그로 잰 값.\n1 dB 오르면 RMS 오차가 약 11% 줄어든다"), ("SSIM (0~1) ↑", "밝기 · 대비 · 구조의 지역 유사도.\n윤곽과 질감이 얼마나 보존됐는지"), ("LPIPS (0~1) ↓", "딥러닝 특징 공간의 거리.\n사람이 느끼는 흐림 · 왜곡에 가장 가깝다"), ]) rows = [["평가 조건 (출처)", "방법", "PSNR ↑", "SSIM ↑", "LPIPS ↓"], ["SplatAD 표 1 — 10장면, 1600×900, 한 장 걸러 평가, 화면 전체", "UniSim", "22.66", "0.743", "0.414"], ["", "PVG", "24.94", "0.768", "0.497"], ["", "OmniRe", "25.50", "0.798", "0.386"], ["", "Street Gaussians", "25.54", "0.799", "0.375"], ["", "NeuRAD", "26.17", "0.790", "0.312"], ["", "SplatAD", "27.54", "0.849", "0.302"], ["NeuRAD 표 1 — 4장면, 4장마다 평가", "S-NeRF", "26.21", "0.831", "0.228"], ["", "NeuRAD-2x", "27.13", "0.820", "0.205"], ["DrivingGaussian 표 1 — 6장면, 1600×900, 5장마다", "3D-GS", "26.08", "0.717", "0.298"], ["", "EmerNeRF", "26.75", "0.760", "0.311"], ["", "DrivingGaussian-L", "28.74", "0.865", "0.237"], ["HUGSIM 표 V — Waymo 2 + nuScenes 2 시퀀스", "HUGSIM", "25.36", "0.776", "0.171"], ["VKGS-Sim (우리) — 800×450, 8장마다, 정적 영역 *", "학습 단위 258개 중앙값", "24.65", "0.766", "미측정"], ["", "scene-0039 (SplatAD 장면)", "24.86", "0.799", "미측정"]] t = table(s, 0.85, y + 0.92, 11.8, rows, [0.40, 0.24, 0.12, 0.12, 0.12], size=9, rowh=0.22, align_right=(2, 3, 4)) highlight(t, [13, 14]) tb(s, 0.85, y + 0.96 + 0.22 * len(rows), 11.8, 0.3, "* 차량 · 사람을 지운 환경 전용 모델이라 정적 영역만 잼 (화면 전체 23.41 dB). 출처 표는 2026-10-12 arXiv 원문에서 확인.", size=9, color=GRAY) notes(s, "같은 묶음 안에서만 비교한다. 우리 행은 프로토콜이 달라 참고값이다. 공정 비교는 SplatAD 프로토콜(10장면, 원 해상도, 화면 전체, LPIPS) 재현이 남은 일 — docs/paper/nuscenes_comparison.md.") # ------------------------------------------------------------------ 21h 성능 비교 — 속도 × 품질 (새) s = slides[33] y = body(s, "성능 비교", "렌더링 속도 × 품질 — 같은 표에 둘 다 보고한 논문", [ "VKGS-Sim 은 2026-10-12 측정 (scene-0039, 6캠 240장 평균, GPU 단독). 1600×900 한 장 15.5 ms 중 86% 가 스플랫 래스터", ]) metric_boxes(s, y + 0.05, [ ("FPS ↑", "초당 렌더하는 영상 장수.\n해상도 · GPU 에 따라 크게 달라진다"), ("MP/s ↑", "초당 렌더하는 픽셀 수 (백만) = FPS × 가로 × 세로.\n해상도가 달라도 비교할 수 있다"), ("PSNR ↑", "품질 (앞 장). 속도와 함께 봐야 한다 —\n가우시안 수를 줄이면 빨라지고 품질이 떨어진다"), ]) rows = [["출처", "방법", "PSNR ↑", "속도 ↑", "하드웨어 · 비고"], ["SplatAD 표 1 (nuScenes 10장면)", "UniSim", "22.66", "13.3 MP/s", "GPU 표기 없음 (학습은 A100)"], ["", "NeuRAD", "26.17", "11.8 MP/s", ""], ["", "PVG", "24.94", "18.8 MP/s", ""], ["", "Street Gaussians", "25.54", "21.5 MP/s", ""], ["", "OmniRe", "25.50", "16.3 MP/s", ""], ["", "SplatAD", "27.54", "106.1 MP/s", ""], ["HUGSIM 표 V (Waymo 2 + nuScenes 2)", "NeuRAD", "24.49", "2.61 FPS", "RTX 3090, 해상도 표기 없음"], ["", "Street Gaussians", "—", "66.5 FPS", "가우시안 685만"], ["", "HUGSIM", "25.36", "89.15 FPS", "가우시안 445만"], ["VKGS-Sim (우리, Vulkan)", "1600×900", "24.65 *", "64 FPS · 93 MP/s", "RTX 3060 Ti 8 GB, 가우시안 200만"], ["", "800×450", "24.65 *", "188 FPS · 68 MP/s", "같은 조건"]] t = table(s, 0.85, y + 0.92, 11.8, rows, [0.27, 0.17, 0.1, 0.18, 0.28], size=9.5, rowh=0.25, align_right=(2, 3)) highlight(t, [10, 11]) tb(s, 0.85, y + 0.96 + 0.25 * len(rows), 11.8, 0.5, "* PSNR 은 우리 프로토콜 (800×450 · 8장마다 · 정적 영역, 앞 장). 1600×900 한 장 = 전처리 1.23 · 정렬 0.63 · 래스터 13.38 ms. " "학습과 GPU 를 같이 쓰면 17.1 ms.", size=9, color=GRAY) notes(s, "SplatAD 는 MP/s 로, HUGSIM 은 FPS 로 냈다. 우리 GPU(RTX 3060 Ti) 는 HUGSIM 의 RTX 3090 보다 약하다. 고해상도에서는 블렌딩 채움률이 병목이라 800×450 에서는 래스터가 3.5 ms 로 준다.") # ------------------------------------------------------------------ 21c 안정성 개선 (새) s = slides[35] y = body(s, "진행 현황", "안정성 개선 — 10월 10일 ~ 12일에 찾은 문제와 해결", [ "사흘째 도는 일괄 학습에서 드러난 실패를 원인까지 찾아 고침 — 같은 일이 다시 생겨도 사람 손 없이 이어지도록 함", ]) table(s, 0.85, y + 0.2, 11.8, [["문제", "원인", "해결"], ["학습 단위 79개 실패", "NAS 일시 장애 (10/10 18:44–19:25, 입출력 오류)", "데이터셋이 돌아올 때까지 기다린 뒤 다시 시도, 최대 3회차 재학습"], ["증강 2씬 실패 (PNG 손상)", "메모리 비트 1개 뒤집힘 (ECC 없음) — 다시 렌더한 것과 1비트 차이", "PNG 를 모두 열어 검사하고 깨졌으면 그 구간을 다시 렌더"], ["지역 맵 일부에서 라이다 · 레이더 꺼짐", "복셀 월드가 없거나, 구간이 하나 더 학습되면 '낡음' 으로 보고 꺼 버림", "씬별 복셀 월드 교체 · 기하가 같으면 그대로 사용 · 없는 씬은 맵을 열 때 생성"], ["차가 움직이지 않음", "창이 키보드 포커스를 못 받음 · 한글 입력기 (ibus) 가 키 이벤트를 가로챔", "포커스 요청 + 화면 경고, 뷰어에서 입력기를 끔"], ["지면 모델 (HUGSIM 다중 평면) 실험", "차선 변경 시점 PSNR +0.08 dB — 효과 미미", "맵 배치에는 넣지 않음"]], [0.22, 0.4, 0.38], size=11, rowh=0.55) notes(s, "PNG 손상: 렌더는 결정적이라 세 번 다시 렌더해도 같은 파일이 나왔고, 깨진 파일은 그것과 정확히 1비트 달랐다. ECC 없는 메모리의 일시 오류로 보며 memtest86 점검을 권한다.") # ------------------------------------------------------------------ 23 향후 계획 s = slides[37] y = body(s, "향후 계획", "향후 연구 계획", [ "지역 맵을 완성하고, 주행 모델을 태우고, 새 시점 품질을 개선하는 순서로 진행함", ]) plans = [("지역 단위 장애물 지도", "지역 맵은 아직 연석 충돌만 — 센서처럼 씬별 장애물 지도를 바꿔 끼워 건물 · 기둥과의 충돌까지"), ("VLA 연결 · 평가", "closed-loop 연결 (--serve + Gym) 과 HD-Score 지표는 구현됨 — VLM/VLA 정책을 지역 맵 위에서 평가"), ("새 시점 품질 (핵심 과제)", "옆 차선 실제 사진 기준 약 16.5 dB (자기 경로 약 23 dB) — 지면 모델은 +0.08 dB 로 효과 미미, 다른 방법 검토"), ("교통 참가자", "3DRealCar 차량 에셋 삽입 (높이 · 속도 → 레이더 도플러), queenstown · holland village 지역 맵")] for i, (t, d) in enumerate(plans): r, c = divmod(i, 2) box(s, 0.85 + c * 6.0, y + 0.2 + r * 1.75, 5.75, 1.55, d, title=t, fill=NAVY if i == 2 else PALE, color=WHITE if i == 2 else GRAY, line=None if i == 2 else BLUE, size=12) notes(s, "여러 주행 합치기 (타일): 같은 길을 다른 차선으로 지나간 주행을 함께 학습하면 옆 차선이 실제 사진으로 감독된다. 주행 1~2개는 정상, 6개에서 3k 스텝 이후 무너진다.") for sh in slides[38].shapes: if sh.name == "텍스트 개체 틀 6": for para in sh.text_frame.paragraphs: rs = para.runs if not rs: continue if "설 재 우" in rs[0].text: rs[0].text = "kimjunhan" if rs[0].text == "Email": rs[1].text = " : kimjunhan1605@gmail.com" notes(slides[38], "질의응답.") notes(slides[39], "감사합니다.") # ================================================================== Ⅱ. 시뮬레이션 엔진 아키텍처 divider(slides[6], "시뮬레이션 엔진 아키텍처", "Vulkan 기반 시뮬레이션 엔진의 전체 구조, 한 프레임의 렌더 그래프, 시뮬레이션 루프, Python 과의 데이터 인터페이스를 설명한다.") def module(sl, x, y, w, h, title, desc, fill=WHITE, line=LIGHT, tcolor=NAVY, dcolor=GRAY, size=10): box(sl, x, y, w, h, "", fill=fill, line=line) tb(sl, x + 0.06, y + 0.04, w - 0.12, 0.26, title, size=size + 1, bold=True, color=tcolor) tb(sl, x + 0.06, y + 0.3, w - 0.12, h - 0.32, desc, size=size, color=dcolor) def figure(sl, path, y, w=11.8, h=4.4, x=0.85): im = Image.open(path) s_ = min(w / im.width, h / im.height) ww, hh = im.width * s_, im.height * s_ return sl.shapes.add_picture(path, Inches(x + (w - ww) / 2), Inches(y), Inches(ww), Inches(hh)) def down_arrow(sl, x, y, w=0.26, h=0.26, color=BLUE): s_ = sl.shapes.add_shape(MSO_SHAPE.DOWN_ARROW, Inches(x), Inches(y), Inches(w), Inches(h)) s_.fill.solid() s_.fill.fore_color.rgb = color s_.line.fill.background() s_.shadow.inherit = False # ------------------------------------------------------------------ 8 엔진 전체 구조도 s = slides[7] y = body(s, "엔진 구조", "시뮬레이션 엔진 전체 구조도", [ "(a) CPU 시뮬레이션 루프가 매 프레임 제어 · 자차 동역학 · 충돌 · 구간 스트리밍 · 센서 · 기록을 처리하고, (b) GPU 렌더 그래프가 가우시안 → 정렬 → 래스터 → 후처리로 영상을 만듦 (그림의 각 단계는 scene-0402 실제 데이터)", ]) figure(s, "/home/mm/Downloads/vkgs-sim/docs/presentation/figures/out/fig_engine_pipeline.png", y + 0.02, h=6.98 - (y + 0.02)) notes(s, "참고한 도식 형식: 3DGS (Kerbl 2023) 최적화 파이프라인, HUGSIM · OmniRe 개요도 — 단계마다 실제 중간 결과를 보여 준다. (a) 의 장애물 지도 · 구간 · 라이다 · 속도 기록, (b) 의 가우시안 · 화면 타원 · 정렬 순서 · 하늘 · 색 · 거리는 모두 scene-0402 구간 1 모델과 실제 주행 로그에서 만든 그림이다. 엔진은 하나의 실행 파일(vkgs_viewer)이고, 창 모드(시뮬레이터)와 오프스크린 모드(데이터 렌더 · 증강)가 같은 렌더러를 쓴다.") # ------------------------------------------------------------------ 9 렌더 그래프 파이프라인 s = slides[8] y = body(s, "엔진 구조", "렌더 그래프 — 한 프레임의 패스 파이프라인", [ "프레임 구성은 configs/render_graph.json 이 정함 — 패스마다 쓰는 자원과 용도 (uses) 를 선언하면 그래프가 배리어 · 레이아웃 전환을 자동으로 삽입함", "가우시안 SSBO → preprocess (투영 · SH · 보이는 것만 압축) → GPU radix 정렬 → 하늘 배경 → 스플랫 래스터 (RGBA16F 색 + r32f 거리) → 후처리 — 800×450 장당 약 6.8 ms", ]) figure(s, "/home/mm/Downloads/vkgs-sim/docs/presentation/figures/out/fig_engine_pipeline_b.png", y + 0.1, h=6.95 - (y + 0.1)) notes(s, "실제 패스 순서: clear_draw_args → preprocess → sort → readback_count → background → scene → precip → weather → fxaa → fxaa_resolve → to_present (camera_isp 꺼짐). 측정(800×450): preprocess 2.02 ms, sort 0.44 ms (이전 bitonic 31~35 ms 대비 13배), scene 4.38 ms. preprocess 가 200만 개 중 보이는 약 25~46만 개만 압축해 정렬 대상을 줄인다. RGBA16F 블렌딩은 32비트 대비 64 dB, 거리 누적은 센서 · 날씨 · 반사에 쓴다.") # ------------------------------------------------------------------ 10 시뮬레이션 루프 s = slides[9] y = body(s, "엔진 구조", "시뮬레이션 루프 — 한 프레임에 일어나는 일", [ "제어 입력 → 자차 적분 · 충돌 → 구간 선택 → 센서 → 렌더 → 기록의 순서로 매 프레임을 처리하며, 스크립트 제어는 50 ms 고정 스텝이라 재현 가능함", "VLA 모델은 '제어 입력' 자리에 들어와 6캠 (+ 센서) 관측을 받고 조향 · 가속을 돌려주는 구조 — 기록된 제어는 drive_log.csv 로 남음 (오른쪽: 실제 시험 주행의 속도)", ]) pic = figure(s, "/home/mm/Downloads/vkgs-sim/docs/presentation/figures/out/fig_engine_pipeline_a.png", y + 0.05, h=2.75) yy = y + 0.2 + pic.height / 914400 table(s, 0.85, yy, 11.8, [["단계", "핵심 처리"], ["③ 충돌", "차체 샘플 0.1 m 격자 · 깊이 = 닿은 점 − 스침 (2) · 같은 방향 0.3 m 앞이 더 깊지 않으면 허용 (갇힘 방지)"], ["④ 구간", "담당 궤적이 가장 가까운 구간 · 3 m 히스테리시스 · 가까운 3구간 비동기 로드 · 교체 0.19초"], ["⑤ 센서", "네 바퀴 자리 지면 높이로 z · pitch · roll → 라이다 32 × 1,091 광선 DDA 4 ms · 레이더 5대 도플러"]], [0.1, 0.9], size=10, rowh=0.29) notes(s, "③ 장애물 지도의 외톨이 칸이나 화단 가장자리 한 칸에 차체 옆이 스치기만 해도 막혀 갇히던 문제를 스침 허용과 탈출 판정으로 해결했다. ⑤ 기울어진 길을 무시하면 낮은 빔의 노면 거리가 20% 넘게 짧아져 노면에 앉힌다.") # ------------------------------------------------------------------ 11 데이터 인터페이스 s = slides[10] y = body(s, "엔진 구조", "Python ↔ 엔진 데이터 인터페이스", [ "엔진과 Python 은 사람이 읽을 수 있는 파일로만 연결됨 — 각 파일은 무엇으로 만들었는지 export.json 에 남기고, 모델 · 알고리즘이 바뀌면 자동으로 다시 만듦", ]) table(s, 0.85, y + 0.15, 11.8, [ ["파일", "만드는 곳 (Python)", "내용", "엔진에서 쓰는 곳"], ["point_cloud.ply (구간마다)", "recon train", "가우시안 + 원점 주석 (nusim_origin)", "렌더러 · SegmentStreamer"], ["segments.txt", "render/segmap.py", "구간 원점 차 · 담당 궤적 점", "SegmentMap (구간 선택)"], ["rig.txt", "render/export.py", "프레임별 6캠 w2c · 자차 포즈 · 객체 박스", "재생 · 운전 카메라 · 증강"], ["map.txt · drivable.txt", "render/export.py", "맵 선 · 주행가능영역 폴리곤", "오버레이 · 연석 판정"], ["obstacles.pgm", "sim/obstacles.py", "0.2 m 장애물 격자 (정적 라이다)", "충돌 판정"], ["world.vox", "sim/voxels.py + vkgs_voxcarve", "0.2 m 복셀 + 0.5 m 지면 높이 지도", "라이다 · 레이더 · 노면 정착"], ["sky.ktx2 · IBL", "recon/sky.py · render/ibl.py", "학습 하늘 큐브맵 · 환경광 (BC6H)", "background · glTF 조명"], ["sim.json · render_graph.json", "configs/", "날씨 · 센서 · 충돌 · 카메라 · 패스 구성", "엔진 전체 설정"], ], [0.22, 0.24, 0.32, 0.22], size=10, rowh=0.42) notes(s, "하드코딩을 줄이기 위해 조정값은 모두 configs/ 에 있다. 좌표계는 nusim/data/rig.py 의 docstring 이 유일한 출처다. 모든 파일은 그 모델의 원점 기준 좌표를 쓴다.") # ================================================================== Ⅲ. 멀티뷰 시점 변환 모델 아키텍처 divider(slides[12], "멀티뷰 시점 변환 모델 아키텍처", "6캠 멀티뷰 영상을 새로운 카메라 배치로 변환하는 모델 — 구간별 3DGS 재구성 모델의 구조, 모듈, 손실 함수, 리그 변환을 설명한다.") # ------------------------------------------------------------------ 14 모델 전체 구조도 s = slides[13] y = body(s, "모델 구조", "멀티뷰 시점 변환 모델 전체 구조도", [ "구간마다 멀티뷰 사진 · 라이다로 3DGS 장면을 학습: 라이다 초기화 → 3D 가우시안 → 미분 래스터라이저 → 렌더 (색 · 깊이) → 사진 · 라이다 · 마스크 감독 → 역전파", "학습된 장면을 리그 변형 T 를 준 6캠으로 다시 렌더링해 새 시점 데이터를 생성 (그림의 각 단계는 scene-0402 실제 데이터)", ]) figure(s, "/home/mm/Downloads/vkgs-sim/docs/presentation/figures/out/fig_viewpoint_model_a.png", y + 0.15, h=6.9 - (y + 0.15)) notes(s, "참고한 도식 형식: 3DGS (Kerbl 2023) Fig. 2 최적화 파이프라인 — 초기 점 → 3D 가우시안 → 투영 · 타일 래스터 → 이미지, 그리고 gradient 되돌림. 여기서는 SfM 점 대신 라이다 누적으로 초기화하고, 사진 손실에 라이다 깊이 · 하늘 · 정규화를 더했다. (b) 는 시점 변환이 학습이 아니라 '새 카메라 배치로 다시 찍기' 임을 보인다. 오른쪽 9장은 같은 순간의 앞 카메라 변형 9종이다.") # ------------------------------------------------------------------ 15 장면 표현 · 미분 렌더러 s = slides[14] y = body(s, "모델 구조", "모듈 ① 장면 표현 및 미분 래스터라이저", [ "장면 모델은 신경망이 아닌 명시적 가우시안 집합이며, 라이다로 초기화하고 3DGS-MCMC 로 개수 (최대 200만) 를 유지하며 위치를 재배치함", "미분 래스터라이저 (gsplat 1.4.0) 가 색 · 불투명도 · 깊이를 함께 렌더링하고, 그 gradient 로 모든 가우시안 파라미터를 갱신함", ]) box(s, 0.85, y + 0.15, 5.75, 3.55, "", fill=PALE, line=BLUE) tb(s, 0.95, y + 0.2, 5.5, 0.3, "가우시안 장면 G", size=13, bold=True, color=NAVY) table(s, 1.0, y + 0.58, 5.45, [["항목", "설정"], ["초기화", "정적 라이다 누적 (약 400 sweep) → 복셀 → 사진 투영 색"], ["파라미터", "μ · 스케일 · 쿼터니언 · α · SH (3차, 1,000 스텝마다 차수 증가)"], ["밀도 제어", "MCMC: 상한 200만, 낮은 α 재배치, 위치 잡음 (lr 5e5)"], ["정규화", "불투명도 · 스케일 평균 각 0.01 (보이는 것만 옵션)"], ["가림", "차량 · 사람 3D 박스 → 사진 · 라이다에서 제외"]], [0.25, 0.75], size=10, rowh=0.44) box(s, 6.9, y + 0.15, 5.75, 3.55, "", fill=PALE, line=BLUE) tb(s, 7.0, y + 0.2, 5.5, 0.3, "미분 래스터라이저 (gsplat)", size=13, bold=True, color=NAVY) rs = [("투영", "Σ′ = J W Σ Wᵀ Jᵀ, 보는 방향으로 SH 색 계산"), ("타일 정렬", "16×16 타일마다 겹치는 가우시안을 깊이순 정렬"), ("알파 블렌딩", "C = Σ cᵢ αᵢ Π(1 − αⱼ), α 누적 · 기대 깊이 D"), ("역전파", "μ · Σ · α · SH 로 gradient (packed 모드)"), ("카메라 행렬", "view gradient 끔 (2.2배 느려짐) → Δ 는 별도 모듈")] for k, (t, d) in enumerate(rs): box(s, 7.05, y + 0.6 + k * 0.6, 1.5, 0.5, t, fill=NAVY, color=WHITE, size=10, bold=True) tb(s, 8.62, y + 0.62 + k * 0.6, 3.95, 0.48, d, size=10, color=GRAY, anchor=MSO_ANCHOR.MIDDLE) notes(s, "Vulkan 렌더러는 이 래스터라이저와 같은 수식(gs_common.glsl)을 쓰고, 학습기 렌더와 PSNR 56 dB 로 일치한다. 학습은 2배 다운스케일(800×450)에서 한다.") # ------------------------------------------------------------------ 16 보정 · 하늘 s = slides[15] y = body(s, "모델 구조", "모듈 ② 카메라 보정 및 하늘 모델", [ "6대 카메라의 색 · 장착 오차와 하늘을 별도 모듈로 분리하여, 가우시안이 카메라 차이나 하늘을 흡수해 생기는 floater 를 막음", ]) cards = [("색 보정 (ColorCorrection)", "카메라별 3×4 아핀 + 프레임별 RGB log 게인\n\naffine_c = [I|0] + d_c − mean(d)\nlog_gain_v = g_v − mean(g)\n\n중심화 → 보정 없이 그린 ply 가 곧\n평균 카메라 색 (증강 영상의 색 기준)"), ("외부 파라미터 보정 (CameraAdjust)", "카메라별 SE(3) Δ (회전벡터 3 + 이동 3)\n\nw2c′ = Δ · w2c\ncam→ego′ = cam→ego · Δ⁻¹\n\nlr 1e-4, 4 스텝마다, 3만 스텝까지\n사전 정규화 5e-4\n결과: 회전 0.1~0.4°, 이동 약 1 cm"), ("하늘 모델 (SkyCube)", "학습 큐브맵 256² (lr 0.01)\n\nÎ_final = Î + (1 − α) · S(광선 방향)\n\nBCE: 하늘 픽셀 α → 0,\n라이다 표면 픽셀 α → 1 (λ 0.05)\n\n저장: sky.ktx2 (RGBA16F 선형)\n→ Vulkan background 패스")] for k, (t, d) in enumerate(cards): box(s, 0.85 + k * 4.0, y + 0.2, 3.8, 2.45, d, title=t, fill=PALE, line=BLUE, size=11, align=PP_ALIGN.LEFT) why = [("분리하지 않으면", "카메라마다 다른 색 · 노출을 가우시안 색이 흡수 → 새 시점에서 얼룩"), ("분리하지 않으면", "장착 오차 (0.1~0.4°) 만큼 같은 물체가 겹쳐 흐려짐"), ("분리하지 않으면", "하늘이 9 m 거리의 밝은 가우시안 덩어리로 구워져 floater 가 됨")] for k, (t, d) in enumerate(why): x = 0.85 + k * 4.0 down_arrow(s, x + 1.77, y + 2.72, h=0.24, color=ORANGE) box(s, x, y + 3.02, 3.8, 0.85, d, title=t, fill=WHITE, line=ORANGE, size=10) notes(s, "하늘을 분리하지 않으면 하늘 일부가 가까운 불투명 가우시안으로 구워져(9 m 거리의 밝은 덩어리) 새 시점에서 떠다닌다. '하늘 아님' 전체에 α→1 을 걸면 나뭇잎 사이로 floater 가 생겨 라이다가 표면을 확인한 픽셀에만 건다.") # ------------------------------------------------------------------ 17 손실 · 학습 설정 s = slides[16] y = body(s, "모델 구조", "손실 함수 및 학습 설정", [ "사진 재구성 손실에 라이다 깊이 · 하늘 분리 · MCMC 정규화 · 카메라 보정 사전을 더해 학습함 — 사진 손실은 차량 · 사람 마스크 밖 픽셀에서만 계산", ]) box(s, 0.85, y + 0.15, 11.8, 0.62, "L = 0.8 · L1 + 0.2 · (1 − SSIM) + 0.1 · L_depth + 0.05 · (L_sky + L_surf) + 0.01 · mean(α) + 0.01 · mean(s) + 5·10⁻⁴ · R(Δ)", fill=NAVY, color=WHITE, size=14, bold=True) table(s, 0.85, y + 0.95, 6.3, [["항", "정의"], ["L1 · D-SSIM", "렌더 Î 와 실제 사진, 객체 마스크 밖 픽셀 (SSIM λ 0.2)"], ["L_depth", "|D − D_lidar| / D_lidar, 1/4 해상도, 차량 픽셀 제외"], ["L_sky · L_surf", "하늘 픽셀 −log(1−α), 라이다 표면 픽셀 −log α"], ["mean(α) · mean(s)", "MCMC 불투명도 · 스케일 정규화"], ["R(Δ)", "카메라 외부 파라미터 보정의 크기 사전"]], [0.3, 0.7], size=10, rowh=0.44) table(s, 7.4, y + 0.95, 5.25, [["학습 설정 (day_map)", "값"], ["스텝 / 구간", "15,000 (약 20분)"], ["해상도", "800×450 (2배 축소)"], ["가우시안 상한", "2,000,000"], ["lr 위치 / 스케일", "1.6e-4 (→ ×0.01) / 5e-3"], ["lr 회전 / 불투명도 / 색", "1e-3 / 5e-2 / 1e-3"], ["재배치 구간", "500 ~ 15,000 스텝"]], [0.55, 0.45], size=10, rowh=0.37) notes(s, "L_depth 는 같은 정적 라이다를 그 카메라에 투영한 깊이 지도와의 상대 오차다. 차량 픽셀의 라이다 깊이는 차 뒤 배경이라 뺀다. GPU 텐서 값을 파이썬 if 로 검사하면 매 스텝 동기화가 걸려 학습 루프에서 피한다.") # ------------------------------------------------------------------ 18 리그 변환 s = slides[17] y = body(s, "모델 구조", "모듈 ③ 멀티뷰 리그 변환 및 렌더링", [ "6대 카메라 전체에 같은 변형 T 를 주어 리그 기하를 유지한 채 '다른 차에 단 카메라' 를 만듦 — 자차 궤적 · 시각은 원본 그대로", "프레임마다 그 시각을 맡은 구간 모델로 렌더링하여 구간 경계에서도 색 · 기하 이음매 없이 이어짐 (scene-0402: 구간 5개)", ]) pic = figure(s, "/home/mm/Downloads/vkgs-sim/docs/presentation/figures/out/fig_viewpoint_model_b.png", y + 0.1, h=2.9) yy = y + 0.2 + pic.height / 914400 table(s, 0.85, yy, 11.8, [["단계", "내용"], ["1 · 변형 정의", "shift [x 앞, y 왼쪽, z 위] m · rot [roll, pitch, yaw]° — configs/aug/rig_variants.json (9종)"], ["2 · 캘리브 변환", "R′ = Rz(yaw) · R · Rx(−pitch) · Rz(roll), t′ = t + shift → w2c′ = (ego → world · cam → ego′)⁻¹ · Δ"], ["3 · 담당 구간", "CAM_FRONT 시각 ∈ [own0, own1) 인 구간 모델 (겹침 10 m 의 가운데에서 자름)"], ["4 · 렌더 · 출력", "구간마다 rig.txt → 오프스크린 6캠 (FXAA 끔, 하늘 큐브맵) → //*.jpg + meta.json"]], [0.16, 0.84], size=10, rowh=0.31) notes(s, "변환 공식은 nusim/aug/rig_aug.py transform_calib 그대로다. pitch + 는 카메라가 아래로 숙이는 방향, yaw + 는 왼쪽으로 돌리는 방향. 리그 전체에 같은 T 를 주므로 6캠 사이의 상대 기하가 실제 차량처럼 일관된다.") # ================================================================== 10-14 추가: ArtiFixer · 차선 후처리 · 되먹임 · 원인 분석 · 노면 텍스처 from pptx.chart.data import CategoryChartData as _CCD from pptx.enum.chart import XL_LEGEND_POSITION for sh in slides[0].shapes: if sh.name == "텍스트 개체 틀 3": set_runs_text(sh.text_frame, "2026. 10. 14.") divider(slides[12], "멀티뷰 시점 변환 모델 아키텍처", "6캠 멀티뷰 영상을 새로운 카메라 배치로 변환하는 모델 — 구간별 3DGS 재구성 모델의 구조, 모듈, 손실, 리그 변환, " "그리고 렌더를 고치는 ArtiFixer 확산 보정 · 노면 차선 후처리 · 3DGS 되먹임 미세조정을 설명한다.") def line_chart(sl, x, y, w, h, cats, series, title, colors, ymin=None, ymax=None, fmt='0.0'): cd = _CCD() cd.categories = cats for n, v in series: cd.add_series(n, v) gf = sl.shapes.add_chart(XL_CHART_TYPE.LINE_MARKERS, Inches(x), Inches(y), Inches(w), Inches(h), cd) ch = gf.chart ch.has_title = True ch.chart_title.text_frame.text = title tp = ch.chart_title.text_frame.paragraphs[0] tp.runs[0].font.size, tp.runs[0].font.bold, tp.runs[0].font.name, tp.runs[0].font.color.rgb = Pt(11), True, FONT, NAVY ch.has_legend = True ch.legend.position = XL_LEGEND_POSITION.BOTTOM ch.legend.include_in_layout = False ch.legend.font.size, ch.legend.font.name = Pt(9), FONT va = ch.value_axis if ymin is not None: va.minimum_scale = ymin if ymax is not None: va.maximum_scale = ymax va.has_major_gridlines = True va.major_gridlines.format.line.color.rgb = LIGHT va.tick_labels.font.size = Pt(9) va.format.line.fill.background() ch.category_axis.tick_labels.font.size = Pt(9) ch.category_axis.tick_labels.font.name = FONT for k, s_ in enumerate(ch.plots[0].series): s_.format.line.color.rgb = colors[k] s_.format.line.width = Pt(2.25) s_.marker.format.fill.solid() s_.marker.format.fill.fore_color.rgb = colors[k] s_.marker.format.line.color.rgb = colors[k] s_.smooth = False pl = ch.plots[0] pl.has_data_labels = True dl = pl.data_labels dl.number_format, dl.number_format_is_linked = fmt, False dl.position = XL_LABEL_POSITION.ABOVE dl.font.size, dl.font.name = Pt(8), FONT return ch def flow(sl, y, steps, h=0.95, fills=None, size=10): """가로 단계 상자 + 화살표.""" n = len(steps) aw = 0.3 w = (11.8 - aw * (n - 1) - 0.06 * 2 * (n - 1)) / n for i, (t, d) in enumerate(steps): x = 0.85 + i * (w + aw + 0.12) f = (fills or {}).get(i) box(sl, x, y, w, h, d, title=t, fill=f or PALE, color=WHITE if f == NAVY else GRAY, line=None if f == NAVY else BLUE, size=size) if i < n - 1: arrow(sl, x + w + 0.06, y + h / 2 - 0.12, w=aw) # ------------------------------------------------------------------ 모듈 ④ ArtiFixer (새) s = slides[40] y = body(s, "모델 구조", "모듈 ④ ArtiFixer 확산 보정 — 새 시점 렌더의 결함 복원", [ "새 시점 (옆 1 m 등) 에서 3DGS 렌더는 흐림 · floater · 빈 곳이 생김 → 비디오 확산 모델이 같은 순간의 실제 사진을 참조해 결함을 고친 영상을 생성함", "ArtiFixer (NVIDIA, SIGGRAPH 2026) 사전학습 1.3B 모델을 그대로 사용 — 학습 없이 추론만, 정면 카메라 · 리그 변형 9종 · 키프레임", ]) flow(s, y + 0.15, [ ("입력 ① 3DGS 렌더", "구간 모델 × 변형 자세\n640×360 연속 프레임\n+ 불투명도 지도 (빈 곳 표시)"), ("입력 ② 참조 뷰", "같은 시각의 원래 카메라\n실제 사진 + 자세\n(transforms.json)"), ("ArtiFixer 1.3B", "Wan2.1-T2V-1.3B 기반\n자기회귀 비디오 확산\n참조 뷰 3장 · 국소 주의 14"), ("출력", "보정 영상 (키프레임)\n→ 모듈 ⑤ 차선 후처리\n→ 모듈 ⑥ 3DGS 되먹임"), ], h=1.25, fills={2: NAVY}) table(s, 0.85, y + 1.65, 7.1, [["설정", "값 (configs/aug/artifixer.json)"], ["체크포인트", "artifixer-1.3b.pt (Wan-AI/Wan2.1-T2V-1.3B-Diffusers)"], ["추론 인자", "--num_views 3 · --max_neighbors_per_encode 1 · --local_attn_size 14"], ["궤적", "--render_trajectory trajectory (변형 자세 순서대로)"], ["실행 환경", "별도 artifixer 환경 하위 프로세스 (torch 버전 다름), 씬당 모델 1회 로드"], ["VRAM · 시간", "약 7.7 GB (8 GB 거의 전부) · 씬당 26~38분 (추론 약 25분)"]], [0.32, 0.68], size=10, rowh=0.36) box(s, 8.15, y + 1.65, 4.5, 2.16, "• 3DGS 는 학습 궤적 근처에서만 선명 — 옆 차선은 감독이 없다\n• 확산 모델은 '그럴듯한' 질감을 채우지만 차선 같은 기하 정보는 흐려지거나 끊김\n" "→ 차선은 모듈 ⑤ 에서 실제 사진으로 되돌린다", title="왜 필요한가 · 한계", fill=WHITE, line=ORANGE, size=10, align=PP_ALIGN.LEFT) notes(s, "참고: ArtiFixer (de Lutio et al., SIGGRAPH 2026, nv-tlabs/ArtiFixer), Difix3D+ (Wu et al., CVPR 2025) — 둘 다 3D 재구성 렌더를 확산 모델로 고치고, 고친 영상을 다시 3D 에 증류하는 방식. " "GPU 가 8 GB 라 ArtiFixer 를 돌리는 동안 뷰어 · 학습을 함께 돌릴 수 없다.") # ------------------------------------------------------------------ 모듈 ⑤ 차선 후처리 (새) s = slides[41] y = body(s, "모델 구조", "모듈 ⑤ 노면 평면 호모그래피 차선 후처리", [ "같은 시각 · 같은 자차에서 카메라만 옮긴 것이므로, 노면을 평면 (자차 z = 0) 으로 보면 변형 카메라 픽셀을 원래 카메라의 실제 사진 (1600×900) 에 정확히 대응시킬 수 있음 (역원근 변환, IPM)", "노면만 실제 사진 픽셀로 바꾸고 나머지는 ArtiFixer 결과를 유지 → 차선 점선 간격 · 폭 · 색이 실제와 같음", ]) box(s, 0.85, y + 0.12, 11.8, 0.55, "x_o ≃ K_o (R − t nᵀ / d) K_v⁻¹ x_v (n, d: 자차 노면 평면, R, t: 변형 → 원래 카메라)", fill=NAVY, color=WHITE, size=14, bold=True) flow(s, y + 0.85, [ ("1 · 광선 → 노면", "변형 카메라 픽셀마다\n노면 평면 교점 X\n(35 m 넘으면 제외)"), ("2 · 원래 카메라로", "X 를 실제 사진에 투영\n양선형 샘플\n(사진 밖이면 제외)"), ("3 · 마스크", "SegFormer-B5 (Cityscapes)\n차도만 · 차량 · 사람\n9 px 팽창해 제외"), ("4 · 색 맞춤", "차도 (밝은 상위 20% 제외)\n평균 · 표준편차를\nArtiFixer 쪽으로 (대비 0.75)"), ("5 · 합성", "영역 안쪽 12 px\n거리 기반 페더\n+ 가우시안 블러 1.5"), ], h=1.25) table(s, 0.85, y + 2.3, 11.8, [["항목", "내용"], ["모드", "road (권장) = 차도 전체를 실제 사진으로 · band = HD 맵 차선 (lane · road divider, 횡단보도, 정지선) ±0.6 m 띠만"], ["보도를 빼는 이유", "보도는 약 15 cm 높아 평면 가정이 깨짐 — 옆 1 m 시점에서 몇 픽셀씩 어긋남"], ["맵을 직접 그리지 않는 이유", "HD 맵에는 점선 간격 · 폭이 없고 영상과 10~30 cm 어긋남 → 맵은 '어디에' 만, 모양은 실제 사진에서"], ["구현", "nusim/aug/lane_fix.py (LaneFixConfig), nusim aug artifixer 의 lanefix 단계"]], [0.2, 0.8], size=10, rowh=0.34) notes(s, "참고: 역원근 변환(IPM) — Mallot et al. 1991, Bertozzi & Broggi GOLD 1998. 세그멘테이션 — SegFormer (Xie et al., NeurIPS 2021), Cityscapes (Cordts et al., CVPR 2016). " "평면 호모그래피는 평평한 노면에서 기하가 정확하므로 학습 없이 실제 사진의 해상도를 그대로 가져온다.") # ------------------------------------------------------------------ 모듈 ⑥ 3DGS 되먹임 (새) s = slides[42] y = body(s, "모델 구조", "모듈 ⑥ 3DGS 되먹임 미세조정 (ArtiFixer3D 식 증류)", [ "보정 영상을 변형 자세의 의사 정답으로 삼아 구간 3DGS 모델을 이어 학습 → 시뮬레이터가 실시간 렌더하는 3D 모델 자체를 고침", "실제 뷰 감독 (사진 · 라이다 깊이 · 하늘 마스크) 은 그대로 두고, 의사 뷰를 30% 확률 · 가중 0.5 로 섞음", ]) table(s, 0.85, y + 0.15, 5.6, [["설정 (scripts/artifixer_distill.py)", "값"], ["시작점", "구간 모델 (하늘 · 색 보정 · 외부 보정 이어받음)"], ["의사 뷰", "lanefix 키프레임 640×360 → 학습 해상도로 업스케일"], ["스텝 · 확률 · 가중", "3,000 · 0.3 · 0.5"], ["lr 위치", "1.6e-4 (기하도 움직이도록)"], ["재배치", "500 ~ 1,500 스텝, 상한 200만 유지"], ["결과", "7씬 18구간 → outputs/gs_day_afx"], ["시간 · VRAM", "구간당 약 3.7분 · 2.3 GB"], ["정적 PSNR (중앙값)", "23.92 → 23.65 dB (원래 궤적, −0.27)"]], [0.42, 0.58], size=10, rowh=0.36) image(s, A + "afx_distill.jpg", 6.65, y + 0.15, w=6.0, h=3.4) caption(s, 6.65, y + 3.6, 6.0, "옆 1 m 시점 — 위: 원래 모델, 아래: 되먹임 후 (scene-0001)") notes(s, "원래 궤적 PSNR 은 0.27 dB 내려가지만 옆 차선 시점에서 차 잔상 · floater 가 사라진다. 다만 차선 선명도는 거의 그대로 — 원인 분석은 진행 현황의 '차선 품질 원인 분석' 장.") # ------------------------------------------------------------------ ArtiFixer 증강 결과 (새) s = slides[43] y = body(s, "증강", "ArtiFixer + 차선 후처리 증강 결과", [ "옆 1 m 변형 시점 (scene-0001) — 기존 3DGS 증강의 번짐 · 차 잔상 · 끊긴 차선이 ArtiFixer 로 정리되고, 노면은 실제 사진의 차선으로 바뀜", ]) pic = image(s, A + "afx_compare.jpg", 0.85, y + 0.12, h=3.2) pw = pic.width / 914400 for k, n in enumerate(["기존 3DGS 증강", "ArtiFixer", "ArtiFixer + 차선 후처리"]): caption(s, 0.85 + k * pw / 3, y + 3.35, pw / 3, n + " ↑") bx = 0.85 + pw + 0.2 bw = 12.65 - bx for k, (t, d) in enumerate([("진행", "10씬 완료 · 3,584장\n정면 카메라 · 변형 9종 · 키프레임\n남은 약 84씬 (약 2일)"), ("결과 경로", "outputs/aug/aug_artfixer/\n_lanefix//CAM_FRONT/"), ("한계", "지운 차량 자리에 회색 안개 (아래 줄)\n정면 카메라만 (6캠은 시간 6배)")]): box(s, bx, y + 0.12 + k * 1.1, bw, 1.0, d, title=t, fill=PALE if k < 2 else WHITE, line=BLUE if k < 2 else ORANGE, size=10) notes(s, "기존 증강(outputs/aug/map, 94씬 · 20만 장)은 그대로 두고, ArtiFixer 증강을 별도 경로에 만든다. 끝난 씬은 done.json 으로 표시해 끊었다 다시 돌려도 이어진다.") # ------------------------------------------------------------------ 차선 품질 원인 분석 (새) s = slides[44] y = body(s, "진행 현황", "차선 품질 원인 분석 — 시뮬 렌더가 2D 후처리보다 흐린 이유", [ "2D 후처리는 실제 사진 픽셀 (1600×900) 을 정확한 노면 평면으로 옮기지만, 시뮬은 3DGS 가우시안이 그린 노면을 렌더 — 학습 자세에서도 노면 고주파가 사진의 17~18% 뿐", ]) fx = [("85 cm", "노면 픽셀을 그리는 가우시안\n큰축 (가중 중앙값)"), ("3.8%", "200만 개 중 노면 몫\n(건물 51 · 하늘 19 · 식생 18%)"), ("−45 cm", "노면 가우시안 중심 높이\n(실제 노면 −0.6 cm)"), ("0.17", "노면 선명도 렌더/사진\n(학습 자세에서도)")] for i, (n, l) in enumerate(fx): kpi(s, 0.85 + i * 3.0, y + 0.12, 2.75, n, l, color=ORANGE if i == 0 else NAVY) table(s, 0.85, y + 1.38, 6.0, [["관찰 (scene-0001 seg0)", "의미"], ["30 cm 넘는 가우시안이 노면 기여 95~96%", "10~15 cm 폭 차선을 표현 못 함"], ["|cos(짧은축, 법선)| 0.46~0.52", "노면에 납작하지 않고 기울어진 덩어리"], ["중심 −45 cm 인데 깊이는 평면과 1~2% 일치", "여러 덩어리 합으로 '평균 깊이' 만 맞춤"], ["앞 차선 약 25개/m²", "3 cm 해상도 (1,100개/m²) 의 2%"], ["되먹임 미세조정으로 안 고쳐짐", "상한이 이미 차 있고 3k 스텝 · 640×360 목표"]], [0.55, 0.45], size=9.5, rowh=0.36) line_chart(s, 7.05, y + 1.38, 5.6, 2.6, ["−1.0 m", "0 m", "+0.5 m", "+1.0 m", "+1.5 m"], [("원래 모델", (24.46, 26.73, 25.81, 25.14, 24.86)), ("되먹임 후 (afx)", (25.47, 27.63, 26.94, 26.22, 25.76))], "노면 PSNR (dB) vs 옆 이동 — 실제 사진 평면 워프 기준", [BLUE, ORANGE], ymin=23.5, ymax=28.5, fmt='0.0') notes(s, "측정: 키프레임 8장, 차도(SegFormer) 중 평면 워프가 유효한 픽셀만. 되먹임은 PSNR 을 약 1 dB 올리지만 선명도(라플라시안 비)는 0.15~0.17 로 그대로다 — 색은 맞추지만 차선 모양은 못 만든다. " "결론: 노면을 가우시안에 맡기지 말고 따로 표현해야 한다 (다음 장).") # ------------------------------------------------------------------ 노면 텍스처 레이어 시험 (새) s = slides[45] y = body(s, "진행 현황", "해결 방향 — 노면 텍스처 레이어 시험 (scene-0001 seg0)", [ "정면 카메라 142장 (스윕 포함) 을 평면 호모그래피로 노면 정사영상 (3 cm/px, 2,257 m²) 에 모으고, 노면 · 땅속 가우시안을 빼고 그 아래 텍스처 평면을 깔아 렌더", ]) image(s, A + "roadtex_bev.jpg", 0.85, y + 0.1, w=4.6, h=1.75) caption(s, 0.85, y + 1.86, 4.6, "노면 텍스처 (BEV, 셀마다 가장 가까이서 본 사진)") pp_ = image(s, A + "roadtex_panel.jpg", 5.6, y + 0.35, w=7.05) cw3 = 7.05 / 3 for k, n in enumerate(["3DGS (+1 m)", "3DGS + 노면 텍스처 (+1 m)", "실제 사진 평면 워프"]): caption(s, 5.6 + k * cw3, y + 0.38 + pp_.height / 914400, cw3, n) line_chart(s, 0.85, y + 2.25, 5.85, 2.15, ["−1.0 m", "0 m", "+0.5 m", "+1.0 m", "+1.5 m"], [("3DGS", (0.17, 0.18, 0.17, 0.16, 0.16)), ("3DGS + 노면 텍스처", (0.96, 1.02, 1.00, 0.93, 0.89))], "노면 선명도 (렌더 / 사진) vs 옆 이동", [BLUE, ORANGE], ymin=0, ymax=1.2, fmt='0.00') table(s, 6.85, y + 2.3, 5.8, [["", "3DGS", "+ 노면 텍스처"], ["노면 PSNR, −1 m", "24.46", "25.63"], ["노면 PSNR, 0 m", "26.73", "25.87"], ["노면 PSNR, +1 m", "25.14", "26.13"], ["뺀 가우시안", "—", "67만 (34%)"], [" 그중 땅속 1.5 m 아래", "—", "43만 (22%)"]], [0.46, 0.24, 0.3], size=10, rowh=0.32, align_right=(1, 2)) notes(s, "가우시안을 빼는 기준: 텍스처 위 노면보다 0.3 m 아래, 차도 셀 위 2 m 아래, 텍스처 둘레 15 m 안 0.5 m 아래, 그리고 가장 가까운 자차 노면보다 1.5 m 넘게 아래. " "땅속 가우시안 43만 개(예산의 22%)는 노면을 아래에서 덮어 차선을 흐리던 것이다 — 엔진에서는 깊이 테스트하는 노면 메시가 가린다. " "0 m 에서 PSNR 이 0.9 dB 낮은 것은 프레임 사이 노출 이음매 · 1~2 px 정렬 오차. 다음: 엔진 메시 패스로 이식, 6캠 · 전 씬 텍스처.") # ------------------------------------------------------------------ 기존 장 갱신 s = slides[22] for gf in s.shapes: if gf.has_table: c = gf.table.cell(2, 1) c.text_frame.paragraphs[0].runs[0].text = "옆 1 m 에서 노면 번짐 · 지운 차량 자리가 흐릿하게 남음 → ArtiFixer + 차선 후처리 증강으로 보완 (다음 장)" s = slides[22] for sh in s.shapes: if sh.name == "TextBox 9": for para in sh.text_frame.paragraphs: if para.runs and "32씬" in "".join(r.text for r in para.runs): para.runs[0].text = "현재 94씬 완료: 205,331장 (40 GB) — 284씬 완료 시 약 61만 장으로 낮 데이터가 9배가 됨" for r in para.runs[1:]: r.text = "" s = slides[36] for sh in list(s.shapes): if sh.name not in KEEP: sh._element.getparent().remove(sh._element) y = body(s, "진행 현황", "싱가포르 낮 맵 구축 현황 (2026-10-14)", [ "학습 단위 696개 중 313개 (45%) 완료 — 10월 13일부터 일괄 학습을 잠시 멈추고 ArtiFixer 증강을 먼저 진행 (GPU 8 GB 를 함께 못 씀)", "ArtiFixer 증강이 끝난 씬까지 마치면 같은 작업이 day_map 일괄 학습을 자동 재개함 (nusim run aug_artifixer) — 전체 맵 학습은 계속", ]) for i, (n, l) in enumerate([("313 / 696", "학습 단위 (45%)"), ("94", "3DGS 증강 씬 (205k장 · 40 GB)"), ("10씬", "ArtiFixer + 차선 후처리 (3.6k장)"), ("313", "시맨틱 라벨 (모든 구간)")]): kpi(s, 0.85 + i * 3.0, y + 0.15, 2.75, n, l) table(s, 0.85, y + 1.5, 11.75, [["항목", "10월 12일", "10월 14일"], ["완료 학습 단위", "253", "313"], ["3DGS 증강 씬 · 이미지", "75씬 · 160.9k", "94씬 · 205.3k"], ["ArtiFixer 증강 · 되먹임", "—", "10씬 3,584장 · 7씬 18구간 미세조정 (gs_day_afx)"], ["시맨틱 카메라", "일부 구간만 라벨 (나머지 검게 보임)", "모든 구간 라벨 · render view/map 이 없으면 자동 생성"]], [0.3, 0.3, 0.4], size=12, rowh=0.4) notes(s, "시맨틱 카메라가 흑백으로 보이던 것은 313개 중 170개 구간에 semantic.rgba 가 없어서였다 (라벨이 없으면 하늘만 파랗고 나머지가 검다). 지금은 모두 채웠다.") s = slides[37] for sh in list(s.shapes): if sh.name not in KEEP: sh._element.getparent().remove(sh._element) y = body(s, "향후 계획", "향후 연구 계획", [ "노면 텍스처 레이어를 엔진에 넣어 차선 품질을 시뮬에서도 확보하고, 지역 맵을 완성한 뒤 주행 모델을 태우는 순서로 진행함", ]) plans = [("노면 텍스처 레이어 (핵심 과제)", "시험에서 차선 선명도 0.16 → 0.93 (옆 1 m) — 렌더 그래프에 노면 메시 패스 추가, 6캠 · 전 씬 텍스처, 땅속 가우시안 정리"), ("ArtiFixer 증강 · 맵 완성", "남은 약 84씬 ArtiFixer 증강 (약 2일) → day_map 일괄 학습 재개 (383단위 남음)"), ("VLA 연결 · 평가", "closed-loop 연결 (--serve + Gym) 과 HD-Score 지표는 구현됨 — VLM/VLA 정책을 지역 맵 위에서 평가"), ("교통 참가자 · 장애물", "3DRealCar 차량 에셋 삽입, 지역 단위 장애물 지도 (건물 · 기둥 충돌), queenstown · holland village 지역 맵")] for i, (t, d) in enumerate(plans): r, c = divmod(i, 2) box(s, 0.85 + c * 6.0, y + 0.2 + r * 1.75, 5.75, 1.55, d, title=t, fill=NAVY if i == 0 else PALE, color=WHITE if i == 0 else GRAY, line=None if i == 0 else BLUE, size=12) notes(s, "노면 텍스처는 프레임 사이 노출 이음매와 차량 가림 구멍 처리가 남았다. 대안으로는 라이다 지면에 납작한 노면 가우시안을 따로 두고 다시 학습하는 방법(2DGS · RoGS 식)이 있다.") # ------------------------------------------------------------------ 순서: 새 장을 제자리로 lst = prs.slides._sldIdLst ids = list(lst) order = list(range(0, 18)) + [40, 41, 42] + list(range(18, 23)) + [43] + list(range(23, 37)) + [44, 45] + list(range(37, 40)) assert sorted(order) == list(range(len(ids))) for e in ids: lst.remove(e) for k in order: lst.append(ids[k]) prs.save(sys.argv[2]) print("saved", sys.argv[2], len(prs.slides))