kimjunhan commited on
Commit
18dfaa2
ยท
verified ยท
1 Parent(s): 5170de3

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes. ย  See raw diff
Files changed (50) hide show
  1. configs/jobs/demo_camera_modes.json +18 -0
  2. configs/jobs/ground_ablation.json +11 -0
  3. configs/jobs/veh_60k_v2.json +14 -0
  4. src/nusim/nusim/__init__.py +33 -0
  5. src/nusim/nusim/aug/__init__.py +1 -0
  6. src/nusim/nusim/aug/artifixer_prep.py +128 -0
  7. src/nusim/nusim/aug/artifixer_run.py +257 -0
  8. src/nusim/nusim/aug/lane_check.py +145 -0
  9. src/nusim/nusim/aug/lane_fix.py +219 -0
  10. src/nusim/nusim/aug/map_aug.py +115 -0
  11. src/nusim/nusim/aug/rig_aug.py +170 -0
  12. src/nusim/nusim/aug/translate.py +117 -0
  13. src/nusim/nusim/cli/__init__.py +1 -0
  14. src/nusim/nusim/cli/doctor.py +255 -0
  15. src/nusim/nusim/cli/main.py +1487 -0
  16. src/nusim/nusim/config.py +57 -0
  17. src/nusim/nusim/data/__init__.py +5 -0
  18. src/nusim/nusim/data/index.py +459 -0
  19. src/nusim/nusim/data/rig.py +204 -0
  20. src/nusim/nusim/io/__init__.py +5 -0
  21. src/nusim/nusim/io/jsonstream.py +103 -0
  22. src/nusim/nusim/io/ktx2.py +139 -0
  23. src/nusim/nusim/io/ply.py +170 -0
  24. src/nusim/nusim/jobs.py +116 -0
  25. src/nusim/nusim/log.py +67 -0
  26. src/nusim/nusim/paths.py +154 -0
  27. src/nusim/nusim/render/__init__.py +9 -0
  28. src/nusim/pyproject.toml +25 -0
  29. src/vkgs/CMakeLists.txt +168 -0
  30. src/vkgs/shaders/bindless.glsl +20 -0
  31. src/vkgs/shaders/depth_common.glsl +19 -0
  32. src/vkgs/shaders/env.frag +21 -0
  33. src/vkgs/shaders/env.vert +14 -0
  34. src/vkgs/shaders/fxaa.comp +80 -0
  35. src/vkgs/shaders/gs_common.glsl +32 -0
  36. src/vkgs/shaders/gs_preprocess.comp +145 -0
  37. src/vkgs/shaders/gs_radix_common.glsl +19 -0
  38. src/vkgs/shaders/gs_radix_hist.comp +19 -0
  39. src/vkgs/shaders/gs_radix_scan.comp +30 -0
  40. src/vkgs/shaders/gs_radix_scatter.comp +53 -0
  41. src/vkgs/shaders/gs_sort.comp +32 -0
  42. src/vkgs/shaders/gs_splat.frag +21 -0
  43. src/vkgs/shaders/gs_splat.vert +33 -0
  44. src/vkgs/shaders/ibl_common.glsl +36 -0
  45. src/vkgs/shaders/line.frag +7 -0
  46. src/vkgs/shaders/line.vert +17 -0
  47. src/vkgs/shaders/nodes/isp.glsl +82 -0
  48. src/vkgs/shaders/pbr.frag +45 -0
  49. src/vkgs/shaders/pbr.vert +25 -0
  50. src/vkgs/shaders/pbr_gltf.frag +128 -0
configs/jobs/demo_camera_modes.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_doc": "๋ฐœํ‘œ์šฉ ์นด๋ฉ”๋ผ ๋ชจ๋“œ ์ฃผํ–‰ ์˜์ƒ (2026-10-12). demo_videos ์™€ ๊ฐ™์€ ์ž๋™ ์ฃผํ–‰ 20์ดˆ ์ฐฝ ํ™”๋ฉด ๋…นํ™”๋ฅผ ์นด๋ฉ”๋ผ ๋ชจ๋“œ(--camera-mode)๋งŒ ๋ฐ”๊ฟ” ๊นŠ์ด ยท ์‹œ๋งจํ‹ฑ ยท ํ‘๋ฐฑ์œผ๋กœ โ€” ๋ชจ๋“œ๋Š” ์œ„์ชฝ ์ค„ ์นด๋ฉ”๋ผ ์„ผ์„œ ์˜์ƒ์—๋งŒ ๊ฑธ๋ฆฌ๊ณ  ๋ฉ”์ธ ์ฃผํ–‰ ํ™”๋ฉด ยท BEV ๋Š” RGB ๊ทธ๋Œ€๋กœ. ์‹œ๋งจํ‹ฑ์€ ๋ชจ๋ธ ์˜† semantic.rgba ๊ฐ€ ์žˆ์–ด์•ผ ํ•œ๋‹ค (nusim recon semantic --location singapore-onenorth). ์ฐฝ์ด ํ™”๋ฉด์— ๋– ์„œ ํ˜ผ์ž ๋‹ฌ๋ฆฐ๋‹ค โ€” ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฏธ๋ฆฌ ์•Œ๋ฆด ๊ฒƒ. ๊ฒฐ๊ณผ: outputs/videos/camera_<๋ชจ๋“œ>.mp4 (+ _poster.jpg).",
3
+ "name": "demo_camera_modes",
4
+ "vars": {
5
+ "map": "render map singapore-onenorth --no-sensors --",
6
+ "common": "--autopilot --front-cams --bev-pip --hide-panels --autoshot 400",
7
+ "dir": "outputs/videos",
8
+ "enc": "render encode --bitrate 2M"
9
+ },
10
+ "steps": [
11
+ {"name": "rec_depth", "run": "${map} ${common} --camera-mode depth --record-screen ${dir}/camera_depth/frames"},
12
+ {"name": "enc_depth", "run": "${enc} ${dir}/camera_depth/frames ${dir}/camera_depth.mp4 --poster ${dir}/camera_depth_poster.jpg --caption '๊นŠ์ด ์นด๋ฉ”๋ผ โ€” ๋กœ๊ทธ ๊ฑฐ๋ฆฌ turbo ์ƒ‰ (0~80 m)'"},
13
+ {"name": "rec_semantic", "run": "${map} ${common} --camera-mode semantic --record-screen ${dir}/camera_semantic/frames"},
14
+ {"name": "enc_semantic", "run": "${enc} ${dir}/camera_semantic/frames ${dir}/camera_semantic.mp4 --poster ${dir}/camera_semantic_poster.jpg --caption '์‹œ๋งจํ‹ฑ ์„ธ๊ทธ๋ฉ˜ํ…Œ์ด์…˜ ์นด๋ฉ”๋ผ โ€” nuScenes-lidarseg ๋ผ๋ฒจ (Cityscapes ์ƒ‰)'"},
15
+ {"name": "rec_gray", "run": "${map} ${common} --camera-mode gray --record-screen ${dir}/camera_gray/frames"},
16
+ {"name": "enc_gray", "run": "${enc} ${dir}/camera_gray/frames ${dir}/camera_gray.mp4 --poster ${dir}/camera_gray_poster.jpg --caption 'ํ‘๋ฐฑ ์นด๋ฉ”๋ผ โ€” Rec.601 ๋ฃจ๋งˆ'"}
17
+ ]
18
+ }
configs/jobs/ground_ablation.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_doc": "์ง€๋ฉด ๋ชจ๋ธ (HUGSIM ๋‹ค์ค‘ ํ‰๋ฉด ์ง€๋ฉด, nusim/recon/ground.py) ํšจ๊ณผ ์ ๊ฒ€ (2026-10-10). scene-0003 ์„ ๋งต ๋ฐฐ์น˜์™€ ๊ฐ™์€ ํ”„๋ฆฌ์…‹(day_map, 15k)์œผ๋กœ ์ง€๋ฉด ๋ชจ๋ธ ์—†์ด / ฮป 10 / ฮป 100 ํ•™์Šตํ•˜๊ณ , ์›๋ž˜ ๊ฒฝ๋กœ ํ‰๊ฐ€(eval)์™€ ์‹ค์ œ ์ฐจ์„  ๋ณ€๊ฒฝ ์‹œ์ (onenorth_0003 ํƒ€์ผ์—์„œ ๋บ€ ์ฃผํ–‰ 0127ยท0969, ์˜† ์•ฝ 4 m โ€” tile-eval)์„ ๋น„๊ตํ•œ๋‹ค. L_ground ๋Š” mยฒ ๋‹จ์œ„ ๋ถ„์‚ฐ์ด๋ผ ๋…ธ๋ฉด ์žก์Œ 0.05 m ์ด๋ฉด 0.0025 โ€” ์ธก๊ด‘ ์†์‹ค(~0.1)๊ณผ ๋งž์ถ”๋ ค๋ฉด ฮป 10~100. ๋งต ๋ฐฐ์น˜(day_map)๊ฐ€ ๋๋‚œ ๋’ค์— ๋Œ๋ฆฐ๋‹ค (์‚ฌ์šฉ์ž ๊ฒฐ์ • 2026-10-10: GPU ์‹คํ—˜์€ ๋ฐฐ์น˜ ๋’ค).",
3
+ "name": "ground_ablation",
4
+ "vars": {"base": "recon train scene-0003 --preset day_map", "out": "outputs/gs_exp/ground"},
5
+ "steps": [
6
+ {"name": "base", "run": "${base} --out ${out}/base"},
7
+ {"name": "g10", "run": "${base} --ground-lambda 10 --out ${out}/g10"},
8
+ {"name": "g100", "run": "${base} --ground-lambda 100 --out ${out}/g100"},
9
+ {"name": "lane", "run": "recon tile-eval onenorth_0003 --dir ${out}/base --dir ${out}/g10 --dir ${out}/g100 --out ${out}/tile_eval"}
10
+ ]
11
+ }
configs/jobs/veh_60k_v2.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_doc": "์ฐจ๋Ÿ‰ ๋™์‹œ ํ•™์Šต 60k ์žฌํ•™์Šต (2026-10-06). veh_60k ๋Š” MCMC ์žฌ๋ฐฐ์น˜๊ฐ€ ๊ฐ์ฒด ๊ฐ€์šฐ์‹œ์•ˆ์„ ๋‹ค๋ฅธ ๊ณณ์œผ๋กœ ์˜ฎ๊ฒจ ์ฐจ๋Ÿ‰ 35๋Œ€ ์ค‘ 17๋Œ€๋งŒ ๋‚จ์•˜๋‹ค โ†’ ์žฌ๋ฐฐ์น˜ ํŒ์ • ์ง์ „์— ๊ฐ์ฒด ๊ฐ€์šฐ์‹œ์•ˆ ๋ถˆํˆฌ๋ช…๋„ ํ•˜ํ•œ(train.py). 3k ์ ๊ฒ€์—์„œ 35๋Œ€ ์ „๋ถ€ ๋‚จ์Œ.",
3
+ "name": "veh_60k_v2",
4
+ "steps": [
5
+ {
6
+ "name": "train",
7
+ "run": "recon train scene-0003 --strategy mcmc --depth-lambda 0.1 --objects vehicles --eval-every 5000 --out outputs/gs/scene-0003_veh60p"
8
+ },
9
+ {
10
+ "name": "eval",
11
+ "run": "recon eval scene-0003 --dir outputs/gs/scene-0003_veh60p --geom"
12
+ }
13
+ ]
14
+ }
src/nusim/nusim/__init__.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """nuScenes ์‹ฑ๊ฐ€ํฌ๋ฅด ๋งต ๊ธฐ๋ฐ˜ Vulkan 3DGS closed-loop ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ.
2
+
3
+ ๋ ˆ์ด์–ด ๊ตฌ์กฐ (docs/architecture.md ์ฐธ๊ณ ):
4
+
5
+ data/ nuScenes ์ฝ๊ธฐ โ€” scene ์ธ๋ฑ์Šค, ์นด๋ฉ”๋ผ ๋ฆฌ๊ทธ, ego ๊ถค์ 
6
+ maps/ ๋งต ํ™•์žฅ(map expansion) โ€” ์ฐจ์„  ๊ทธ๋ž˜ํ”„, ์ฃผํ–‰๊ฐ€๋Šฅ์˜์—ญ
7
+ recon/ 3DGS ์žฌ๊ตฌ์„ฑ(ํ•™์Šต) โ€” gsplat ๊ธฐ๋ฐ˜
8
+ io/ .ply / .splat ์ง๋ ฌํ™” โ€” ํ•™์Šต๊ธฐ์™€ Vulkan ๋ Œ๋”๋Ÿฌ ์‚ฌ์ด์˜ ๊ตํ™˜ ํฌ๋งท
9
+ sim/ closed-loop ํ™˜๊ฒฝ โ€” ego ์ƒํƒœ ์ ๋ถ„, 6์นด๋ฉ”๋ผ ๊ด€์ธก ์ƒ์„ฑ
10
+ cli/ ์ปค๋งจ๋“œ๋ผ์ธ ์ง„์ž…์ 
11
+ """
12
+
13
+ __version__ = "0.1.0"
14
+
15
+ # 6๊ฐœ ์นด๋ฉ”๋ผ. nuScenes ์˜ ํ‘œ์ค€ ์ด๋ฆ„์ด์ž ์ด ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ ์“ฐ๋Š” ์ •์ค€ ์ˆœ์„œ๋‹ค.
16
+ # ์ˆœ์„œ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ๋ฉ€ํ‹ฐ๋ทฐ ํ…์„œ์˜ ์ถ• ์˜๋ฏธ๊ฐ€ ๋‹ฌ๋ผ์ง€๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ๋งŒ ์ •์˜ํ•œ๋‹ค.
17
+ CAMERAS = (
18
+ "CAM_FRONT_LEFT",
19
+ "CAM_FRONT",
20
+ "CAM_FRONT_RIGHT",
21
+ "CAM_BACK_RIGHT",
22
+ "CAM_BACK",
23
+ "CAM_BACK_LEFT",
24
+ )
25
+
26
+ # ์‹ฑ๊ฐ€ํฌ๋ฅด ๋งต 3์ข…. boston-seaport ๋Š” ์ด ํ”„๋กœ์ ํŠธ์˜ ๋ฒ”์œ„ ๋ฐ–์ด๋‹ค.
27
+ SINGAPORE_MAPS = (
28
+ "singapore-onenorth",
29
+ "singapore-queenstown",
30
+ "singapore-hollandvillage",
31
+ )
32
+
33
+ __all__ = ["CAMERAS", "SINGAPORE_MAPS", "__version__"]
src/nusim/nusim/aug/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """๋ฐ์ดํ„ฐ์…‹ ๋‹ค์–‘์„ฑ ์ฆ๊ฐ• โ€” ์žฌ๊ตฌ์„ฑ ์žฅ๋ฉด์œผ๋กœ ์ƒˆ ์นด๋ฉ”๋ผ ์‹œ์ (rig_aug), ์ƒ์„ฑ ๋ชจ๋ธ๋กœ ๋ฐคยท๋น„ โ†’ ๋‚ฎ(translate)."""
src/nusim/nusim/aug/artifixer_prep.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ArtiFixer(nv-tlabs/artifixer, SIGGRAPH 2026) ์ž…๋ ฅ ๋งŒ๋“ค๊ธฐ โ€” ์‹œ์  ๋ณ€ํ˜• ๋ Œ๋”๋ฅผ ํ™•์‚ฐ ๋ชจ๋ธ๋กœ ๊ณ ์น˜๋Š” ์‹คํ—˜ (nusim aug artifixer-prep).
2
+
3
+ ArtiFixer ๋Š” 3D ์žฌ๊ตฌ์„ฑ ๋ Œ๋”(+๋ถˆํˆฌ๋ช…๋„)์™€ ์‹ค์ œ ์ฐธ์กฐ ์‚ฌ์ง„ ๋ช‡ ์žฅ, ์นด๋ฉ”๋ผ ์ž์„ธ๋ฅผ ๋ฐ›์•„ ๋ Œ๋”์˜ ํ๋ฆผ ยท ๊ตฌ๋ฉ ยท ๋– ๋‹ค๋‹ˆ๋Š” ์กฐ๊ฐ์„
4
+ ์ž๊ธฐํšŒ๊ท€ ๋น„๋””์˜ค ํ™•์‚ฐ(Wan2.1)์œผ๋กœ ๊ณ ์นœ๋‹ค. ์›๋ž˜ ํŒŒ์ดํ”„๋ผ์ธ์€ COLMAP โ†’ 3DGRUT ๋ฅผ ์ƒˆ๋กœ ํ•™์Šตํ•˜์ง€๋งŒ, ์—ฌ๊ธฐ์„œ๋Š” ์šฐ๋ฆฌ ๊ตฌ๊ฐ„ ๋ชจ๋ธ
5
+ (outputs/gs_day/<์”ฌ>/seg<k>)์˜ gsplat ๋ Œ๋”๋ฅผ ๊ทธ๋Œ€๋กœ ๋„ฃ๋Š”๋‹ค โ€” model_eval/datasets/reconstructed_colmap_eval.py ํ˜•์‹:
6
+
7
+ <out>/split.json {"test": {<id>: {transforms_path, image_root, render_dir, opacity_dir,
8
+ selected_indices_path, target_indices_path, prompt_path, camera_scale, has_gt}}}
9
+ <out>/transforms.json OPENCV ๋‚ด๋ถ€ ํŒŒ๋ผ๋ฏธํ„ฐ + frames[i].transform_matrix (NeRFStudio/OpenGL c2w, ๋ฏธํ„ฐ)
10
+ <out>/images/ ์ฐธ์กฐ ์‚ฌ์ง„ (์›๋ž˜ ์ž์„ธ์˜ ์‹ค์ œ nuScenes ์‚ฌ์ง„, ๋ Œ๋” ํ•ด์ƒ๋„๋กœ ์ค„์ž„)
11
+ <out>/renders/{i:05d}.png ๋ Œ๋” (์ƒ‰ ๋ณด์ • ยท ํ•˜๋Š˜ ํ•ฉ์„ฑ = nusim recon eval ๊ณผ ๊ฐ™์€ ์˜์ƒ)
12
+ <out>/opacity/{i:05d}.png ๋ Œ๋” ๋ถˆํˆฌ๋ช…๋„ ฮฑ (ํ•˜๋Š˜ ํ•ฉ์„ฑ ์ „)
13
+ <out>/selected_indices.json ์ฐธ์กฐ(์‹ค์ œ ์‚ฌ์ง„) ํ”„๋ ˆ์ž„ = 0..N-1
14
+ <out>/target_indices.json ๊ณ ์น  ํ”„๋ ˆ์ž„ = ๋ณ€ํ˜• ์ž์„ธ N..2N-1
15
+ <out>/caption.txt ์บก์…˜ โ€” ์ž„๋ฒ ๋”ฉ(caption.h5)์€ artifixer ํ™˜๊ฒฝ์—์„œ UMT5 ๋กœ (scripts/artifixer_caption.py)
16
+
17
+ ์ž์„ธ๋Š” ํ•™์Šต์ด ๋ณด์ •ํ•œ ์™ธ๋ถ€ ํŒŒ๋ผ๋ฏธํ„ฐ(camera_adjust)๋ฅผ ์ ์šฉํ•œ ๋ Œ๋” ์ž์„ธ ๊ทธ๋Œ€๋กœ๋‹ค. ๋ฏธํ„ฐ ๋‹จ์œ„๋ผ metric_scale = 1,
18
+ camera_scale = 0.01 (ArtiFixer prepare_colmap_artifixer_inputs ์˜ DEFAULT_CAMERA_CONDITIONING_SCALE).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import logging
25
+ from dataclasses import replace
26
+ from pathlib import Path
27
+
28
+ import numpy as np
29
+
30
+ LOG = logging.getLogger(__name__)
31
+ CAMERA_CONDITIONING_SCALE = 0.01
32
+ OPENCV_TO_OPENGL = np.diag([1.0, -1.0, -1.0, 1.0])
33
+ DEFAULT_CAPTION = ("A daytime dashcam video driving along a tree-lined street in Singapore, asphalt road with white "
34
+ "lane markings, curbs, sidewalks, trees and buildings, clear sky, realistic photo.")
35
+
36
+
37
+ def prepare(scene_name: str, model_dir: Path, variant: dict, out: Path, cam: str = "CAM_FRONT",
38
+ width: int | None = None, max_frames: int | None = None, timestamps: set | None = None, start: int = 0, caption: str = DEFAULT_CAPTION,
39
+ tag: str = "", log=LOG.info) -> dict:
40
+ import torch
41
+ from PIL import Image
42
+
43
+ from nusim.aug.rig_aug import transform_calib
44
+ from nusim.cli.main import _model_data, _model_objects, _model_views
45
+ from nusim.io.ply import read_ply
46
+ from nusim.recon.dataset import build_views
47
+ from nusim.recon.train import CameraAdjust, ColorCorrection, SkyCube, _to_params, composite_sky, render
48
+
49
+ model_dir = Path(model_dir)
50
+ out.mkdir(parents=True, exist_ok=True)
51
+ for d in ("images", "renders", "opacity"):
52
+ (out / d).mkdir(exist_ok=True)
53
+ g = read_ply(model_dir / "point_cloud.ply")
54
+ vk = _model_views(model_dir)
55
+ sub, ds = _model_data(scene_name, model_dir)
56
+ kw = dict(downscale=vk["downscale"], frame_stride=vk["frame_stride"], keyframes_only=vk["keyframes_only"],
57
+ objects=_model_objects(model_dir), load_images=False, mask_dynamic=False, log=LOG.debug)
58
+ # ํ•™์Šต๊ณผ ๊ฐ™์€ ๋ทฐ ๋ฒˆํ˜ธ (์ƒ‰ ๋ณด์ •์˜ ํ”„๋ ˆ์ž„๋ณ„ ๊ฒŒ์ธ์ด ์ด ๋ฒˆํ˜ธ) โ€” ๊ทธ์ค‘ ํ•œ ์นด๋ฉ”๋ผ๋งŒ ์“ด๋‹ค
59
+ real = build_views(sub, ds, g.origin, **kw)
60
+ vsub = replace(sub, calib={c: (transform_calib(k, variant.get("shift_m", (0, 0, 0)), variant.get("rot_deg", (0, 0, 0)))
61
+ if c == cam else k) for c, k in sub.calib.items()})
62
+ var = build_views(vsub, ds, g.origin, **kw)
63
+ from nusim import CAMERAS
64
+
65
+ cid = CAMERAS.index(cam)
66
+ rv = [v for v in real.all if v.cam_id == cid][start:]
67
+ tv = [v for v in var.all if v.cam_id == cid][start:]
68
+ if timestamps is not None: # ๊ตฌ๊ฐ„ ๋ชจ๋ธ์ด '๋งก๋Š”' ํ”„๋ ˆ์ž„๋งŒ (nusim.aug.map_aug.assign_front_frames)
69
+ keep = [i for i, v in enumerate(rv) if v.pose.timestamp in timestamps]
70
+ rv, tv = [rv[i] for i in keep], [tv[i] for i in keep]
71
+ if max_frames:
72
+ rv, tv = rv[:max_frames], tv[:max_frames]
73
+ if width and width != rv[0].width: # 8 GB VRAM: ArtiFixer ํ† ํฐ ์ˆ˜๊ฐ€ ํ•ด์ƒ๋„์— ๋น„๋ก€ โ€” ๋ Œ๋” ํ•ด์ƒ๋„ ์ž์ฒด๋ฅผ ์ค„์ธ๋‹ค
74
+ s = width / rv[0].width
75
+
76
+ def small(v):
77
+ K = v.K.copy()
78
+ K[:2] *= s
79
+ return replace(v, K=K, width=width, height=round(v.height * s))
80
+ rv, tv = [small(v) for v in rv], [small(v) for v in tv]
81
+ assert [v.pose.timestamp for v in rv] == [v.pose.timestamp for v in tv]
82
+ n = len(rv)
83
+ dev = torch.device("cuda")
84
+ params = _to_params(g, dev)
85
+ color = ColorCorrection.load(model_dir / "color_correction.pt", 6, len(real.all), dev)
86
+ adj = None
87
+ if (model_dir / "camera_adjust.pt").exists():
88
+ adj = CameraAdjust(6).to(dev)
89
+ adj.load_state_dict(torch.load(model_dir / "camera_adjust.pt", weights_only=True))
90
+ sky = SkyCube.from_state(torch.load(model_dir / "sky.pt", weights_only=True)) if (model_dir / "sky.pt").exists() else None
91
+
92
+ frames = []
93
+ w, h = rv[0].width, rv[0].height
94
+ K = rv[0].K
95
+ for i, v in enumerate(rv + tv):
96
+ with torch.no_grad():
97
+ img, alpha, info = render(params, v, g.sh_degree, cam_adjust=adj, cam_grad=False)
98
+ img = composite_sky(img, alpha, sky, v, info["viewmat"])
99
+ img = color(img, v.cam_id, rv[i % n].idx).clamp(0, 1) # ๋ณ€ํ˜• ํ”„๋ ˆ์ž„๋„ ๊ฐ™์€ ์‹œ๊ฐ ์‹ค์ œ ํ”„๋ ˆ์ž„์˜ ๊ฒŒ์ธ
100
+ Image.fromarray((img[0].cpu().numpy() * 255 + 0.5).astype(np.uint8)).save(out / "renders" / f"{i:05d}.png")
101
+ Image.fromarray((alpha[0, ..., 0].clamp(0, 1).cpu().numpy() * 255 + 0.5).astype(np.uint8)).save(
102
+ out / "opacity" / f"{i:05d}.png")
103
+ w2c = info["viewmat"][0].double().cpu().numpy() if info["viewmat"].dim() == 3 else info["viewmat"].double().cpu().numpy()
104
+ c2w = np.linalg.inv(w2c) @ OPENCV_TO_OPENGL
105
+ fr = rv[i % n].pose
106
+ name = f"images/{i % n:05d}.jpg"
107
+ if i < n: # ์ฐธ์กฐ = ์›๋ž˜ ์ž์„ธ์˜ ์‹ค์ œ ์‚ฌ์ง„
108
+ Image.open(ds.root / fr.filename).convert("RGB").resize((w, h), Image.BICUBIC).save(out / name, quality=95)
109
+ frames.append({"file_path": name, "transform_matrix": c2w.tolist(), "timestamp": int(fr.timestamp),
110
+ "original_filename": fr.filename, "role": "reference" if i < n else "target"})
111
+ tr = {"camera_model": "OPENCV", "w": int(w), "h": int(h), "fl_x": float(K[0, 0]), "fl_y": float(K[1, 1]),
112
+ "cx": float(K[0, 2]), "cy": float(K[1, 2]), "k1": 0.0, "k2": 0.0, "p1": 0.0, "p2": 0.0, "frames": frames}
113
+ (out / "transforms.json").write_text(json.dumps(tr, indent=1))
114
+ (out / "selected_indices.json").write_text(json.dumps(list(range(n))))
115
+ (out / "target_indices.json").write_text(json.dumps(list(range(n, 2 * n))))
116
+ (out / "caption.txt").write_text(caption)
117
+ sid = f"{scene_name}_{model_dir.name}_{cam}_{variant['name']}{tag}" # tag: ํ•œ ๊ตฌ๊ฐ„์„ ๋‚˜๋ˆˆ ์กฐ๊ฐ (artifixer_run.prep_scene)
118
+ split = {"test": {sid: {"scene_id": sid, "transforms_path": "transforms.json", "image_root": ".",
119
+ "render_dir": "renders", "opacity_dir": "opacity",
120
+ "selected_indices_path": "selected_indices.json", "target_indices_path": "target_indices.json",
121
+ "prompt_path": "caption.h5", "metric_scale": 1.0,
122
+ "camera_scale": CAMERA_CONDITIONING_SCALE, "has_gt": False}}}
123
+ (out / "split.json").write_text(json.dumps(split, indent=1))
124
+ meta = {"scene": scene_name, "model": str(model_dir), "cam": cam, "variant": variant, "frames": n,
125
+ "size": [int(w), int(h)], "scene_id": sid}
126
+ (out / "prep.json").write_text(json.dumps(meta, indent=1))
127
+ log(f" {sid}: ์ฐธ์กฐ {n}์žฅ + ๊ณ ์น  ํ”„๋ ˆ์ž„ {n}์žฅ ({w}ร—{h}) โ†’ {out}")
128
+ return meta
src/nusim/nusim/aug/artifixer_run.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ArtiFixer + ์ฐจ์„  ํ›„์ฒ˜๋ฆฌ ์ฆ๊ฐ•์„ ์”ฌ ๋‹จ์œ„๋กœ ๋๊นŒ์ง€ (nusim aug artifixer) โ€” ์„ค์ • configs/aug/artifixer.json.
2
+
3
+ ์”ฌ ํ•˜๋‚˜, ์นด๋ฉ”๋ผ ํ•˜๋‚˜๋งˆ๋‹ค:
4
+ 1. prep ๊ตฌ๊ฐ„ ๋ชจ๋ธ(outputs/gs_day/<์”ฌ>/seg<k>) ร— ๋ฆฌ๊ทธ ๋ณ€ํ˜•(๋‚ ์”จ ์—†๋Š” 9์ข…)๋งˆ๋‹ค ArtiFixer ์ž…๋ ฅ
5
+ (nusim.aug.artifixer_prep.prepare โ€” ๊ตฌ๊ฐ„์ด '๋งก๋Š”' ํ”„๋ ˆ์ž„๋งŒ, nusim.aug.map_aug.assign_front_frames)
6
+ โ†’ outputs/artifixer/<์”ฌ>_<CAM>/<๊ตฌ๊ฐ„>_<๋ณ€ํ˜•>/ + ํ•ฉ์นœ split.json
7
+ 2. infer artifixer ํ™˜๊ฒฝ์—์„œ model_eval.run_inference ํ•œ ๋ฒˆ (๋ชจ๋ธ ๋กœ๋“œ ํ•œ ๋ฒˆ์— ์ž…๋ ฅ ์ „๋ถ€) โ†’ <๊ฐ™์€ ๊ณณ>/run
8
+ 3. collect ๊ณ ์นœ ํ”„๋ ˆ์ž„ ์ค‘ ํ‚คํ”„๋ ˆ์ž„ โ†’ outputs/aug/aug_artfixer/<์”ฌ>/<๋ณ€ํ˜•>/<CAM>/<์›๋ณธ ํŒŒ์ผ๋ช…>.jpg + meta.json
9
+ 4. lanefix ๋…ธ๋ฉด์„ ์‹ค์ œ ์‚ฌ์ง„์˜ ํ‰๋ฉด ํ˜ธ๋ชจ๊ทธ๋ž˜ํ”ผ๋กœ (nusim.aug.lane_fix, road ๋ชจ๋“œ)
10
+ โ†’ outputs/aug/aug_artfixer/<์”ฌ>_lanefix/<๋ณ€ํ˜•>/<CAM>/ + meta.json
11
+ 5. ์ค‘๊ฐ„ ํŒŒ์ผ ์ •๋ฆฌ (๋ Œ๋” ยท ๋ถˆํˆฌ๋ช…๋„ ยท ์ถ”๋ก  ํ”„๋ ˆ์ž„, ์”ฌ๋‹น ์•ฝ 2.8 GB) โ€” keep_intermediate ๋กœ ๋‚จ๊ธธ ์ˆ˜ ์žˆ๋‹ค
12
+ ๋๋‚œ ํ‘œ์‹œ๋Š” <์”ฌ>_lanefix/done.json (์นด๋ฉ”๋ผ๋ณ„). ์žˆ์œผ๋ฉด ๊ฑด๋„ˆ๋›ฐ๋ฏ€๋กœ ๋Š์—ˆ๋‹ค ๋‹ค์‹œ ๋Œ๋ ค๋„ ๋œ๋‹ค.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import shutil
20
+ import subprocess
21
+ import time
22
+ from dataclasses import asdict
23
+ from pathlib import Path
24
+
25
+ from nusim.config import configs_dir
26
+ from nusim.paths import output_dir, workspace_root
27
+
28
+
29
+ def artifixer_config() -> dict:
30
+ return json.loads((configs_dir() / "aug" / "artifixer.json").read_text())
31
+
32
+
33
+ def _variants() -> list[dict]:
34
+ from nusim.aug.rig_aug import load_variants
35
+
36
+ return [v for v in load_variants(configs_dir() / "aug" / "rig_variants.json") if not v.get("weather")]
37
+
38
+
39
+ def done_cams(scene: str) -> set[str]:
40
+ p = output_dir() / "aug" / "aug_artfixer" / f"{scene}_lanefix" / "done.json"
41
+ return set(json.loads(p.read_text()).get("cams", {})) if p.exists() else set()
42
+
43
+
44
+ def finished_scenes(root: Path | None = None) -> list[str]:
45
+ """ํ•™์Šต ยท ๊ธฐ์กด ์ฆ๊ฐ•(aug map)๊นŒ์ง€ ๋๋‚œ ์”ฌ โ€” ๊ธฐ์กด ์ฆ๊ฐ•์ด ๋๋‚œ ์ˆœ์„œ(= ๋„๋กœ ์ปค๋ฒ„๋ฆฌ์ง€ ์šฐ์„  ํ•™์Šต ์ˆœ์„œ)๋Œ€๋กœ."""
46
+ n_var = len(_variants())
47
+ base = output_dir() / "aug" / "map"
48
+ out = []
49
+ for d in base.glob("scene-*"):
50
+ metas = list(d.glob("*/meta.json"))
51
+ if len(metas) >= n_var:
52
+ out.append((max(m.stat().st_mtime for m in metas), d.name))
53
+ return [s for _, s in sorted(out)]
54
+
55
+
56
+ def prep_scene(scene: str, cam: str, cfg: dict, root: Path, log=print) -> Path:
57
+ from nusim.aug.artifixer_prep import prepare
58
+ from nusim.aug.map_aug import assign_front_frames
59
+ from nusim.data.index import load_scene
60
+ from nusim.recon.batch import segment_dirs
61
+
62
+ base = output_dir() / "artifixer" / f"{scene}_{cam}"
63
+ segs = segment_dirs(root, scene)
64
+ if not segs:
65
+ raise RuntimeError(f"{scene}: ํ•™์Šต๋œ ๊ตฌ๊ฐ„ ๋ชจ๋ธ์ด ์—†์Œ ({root})")
66
+ sc = load_scene(scene)
67
+ owned = assign_front_frames(sc, [s for s, _ in segs])
68
+ cap = workspace_root() / cfg["caption_h5"]
69
+ split = {"test": {}}
70
+ for (info, d), fr in zip(segs, owned):
71
+ # ๊ตฌ๊ฐ„์ด ๋งก๋Š” ์‹œ๊ฐ โ€” ์ •๋ฉด ์นด๋ฉ”๋ผ ์‹œ๊ฐ ๊ธฐ์ค€์ด๋ผ ๋‹ค๋ฅธ ์นด๋ฉ”๋ผ๋Š” ๊ตฌ๊ฐ„ ๋ชจ๋ธ์˜ ํ•™์Šต ๋ทฐ ์ „๋ถ€
72
+ ts = {f.timestamp for f in fr} if cam == "CAM_FRONT" else None
73
+ # 8 GB: ์ž…๋ ฅ์ด ๊ธธ๋ฉด ArtiFixer ๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ ๋ถ€์กฑ โ€” 60 m ๋ณด๋‹ค ์งง์•„ ํ•œ ๊ตฌ๊ฐ„์œผ๋กœ ํ•™์Šต๋œ ์”ฌ(์•ฝ 230ํ”„๋ ˆ์ž„)์ด ๊ทธ๋žฌ๋‹ค.
74
+ # ์—ฐ์†ํ•œ ์กฐ๊ฐ์œผ๋กœ ๋‚˜๋ˆ  ๋”ฐ๋กœ ๋„ฃ๋Š”๋‹ค (์กฐ๊ฐ ๊ฒฝ๊ณ„์—์„œ ์‹œ๊ฐ„ ์ผ๊ด€์„ฑ์€ ๋Š๊ธด๋‹ค)
75
+ chunks = [ts]
76
+ mx = cfg.get("max_frames")
77
+ if ts is not None and mx and len(ts) > mx:
78
+ order, n = sorted(ts), -(-len(ts) // mx)
79
+ chunks = [set(order[j * len(order) // n:(j + 1) * len(order) // n]) for j in range(n)]
80
+ for c, cts in enumerate(chunks):
81
+ tag = f"_c{c}" if len(chunks) > 1 else ""
82
+ for v in _variants():
83
+ out = base / f"{d.name}_{v['name']}{tag}"
84
+ meta = prepare(scene, d, v, out, cam=cam, width=cfg["width"], timestamps=cts, tag=tag, log=log)
85
+ shutil.copy(cap, out / "caption.h5")
86
+ ent = json.loads((out / "split.json").read_text())["test"][meta["scene_id"]]
87
+ for k in ("transforms_path", "image_root", "render_dir", "opacity_dir", "selected_indices_path",
88
+ "target_indices_path", "prompt_path"):
89
+ ent[k] = str((out / ent[k]).resolve())
90
+ split["test"][meta["scene_id"]] = {**ent, "prep_dir": str(out.resolve())}
91
+ (base / "split.json").write_text(json.dumps(split, indent=1))
92
+ log(f" prep: ์ž…๋ ฅ {len(split['test'])}๊ฐœ โ†’ {base / 'split.json'}")
93
+ return base
94
+
95
+
96
+ def _collected(scene: str, cam: str, base: Path) -> dict[str, list] | None:
97
+ """split.json ์˜ ๋ณ€ํ˜•๋งˆ๋‹ค ์ˆ˜์ง‘๋œ ArtiFixer ๊ฒฐ๊ณผ(meta.json ์˜ ์ด ์นด๋ฉ”๋ผ ํ‘œ๋ณธ) โ€” ํ•˜๋‚˜๋ผ๋„ ์—†์œผ๋ฉด None."""
98
+ sp = base / "split.json"
99
+ if not sp.exists():
100
+ return None
101
+ want = {json.loads((Path(e["prep_dir"]) / "prep.json").read_text())["variant"]["name"]
102
+ for e in json.loads(sp.read_text())["test"].values()}
103
+ root = output_dir() / "aug" / "aug_artfixer" / scene
104
+ out = {}
105
+ for var in want:
106
+ m = root / var / "meta.json"
107
+ smp = [x for x in json.loads(m.read_text())["samples"] if x["filename"].startswith(cam + "/")] if m.exists() else []
108
+ if not smp:
109
+ return None
110
+ out[var] = smp
111
+ return out
112
+
113
+
114
+ def _inferred(base: Path) -> bool:
115
+ """split.json ์˜ ์ž…๋ ฅ๋งˆ๋‹ค ArtiFixer ๊ฒฐ๊ณผ(pred)๊ฐ€ ์žˆ๋‚˜ โ€” ์ง€๊ธˆ ์„ค์ •(max_frames)์œผ๋กœ ๋งŒ๋“  split ์ด์–ด์•ผ ํ•œ๋‹ค."""
116
+ sp = base / "split.json"
117
+ if not sp.exists():
118
+ return False
119
+ sids = json.loads(sp.read_text())["test"]
120
+ return bool(sids) and all(next((base / "run").glob(f"*/*/{sid}/frames/batch_0000/pred"), None) for sid in sids)
121
+
122
+
123
+ def infer(base: Path, cfg: dict, log=print) -> Path:
124
+ """artifixer ํ™˜๊ฒฝ ํ•˜์œ„ ํ”„๋กœ์„ธ์Šค โ€” vkgs ํ™˜๊ฒฝ๊ณผ torch ๋ฒ„์ „์ด ๋‹ฌ๋ผ ๊ฐ™์€ ํ”„๋กœ์„ธ์Šค์—์„œ ๋ชป ๋Œ๋ฆฐ๋‹ค."""
125
+ repo = workspace_root() / cfg["repo"]
126
+ run = base / "run"
127
+ env = {**os.environ, "PYTHONPATH": str(repo / "thirdparty" / "3DGRUT-ArtiFixer"),
128
+ "PYTORCH_CUDA_ALLOC_CONF": "expandable_segments:True"}
129
+ cmd = [os.path.expanduser(cfg["python"]), "-m", "model_eval.run_inference", "--evalset", "reconstructed_colmap",
130
+ "--checkpoint_pt", str(workspace_root() / cfg["checkpoint"]), "--model_id", cfg["model_id"],
131
+ "--save_dir", str(run), "--split_path", str(base / "split.json"), *cfg["infer_args"]]
132
+ t = time.time()
133
+ with open(base / "run.log", "w") as f:
134
+ r = subprocess.run(cmd, cwd=repo, env=env, stdout=f, stderr=subprocess.STDOUT)
135
+ if r.returncode:
136
+ raise RuntimeError(f"ArtiFixer ์ถ”๋ก  ์‹คํŒจ (์ฝ”๋“œ {r.returncode}) โ€” {base / 'run.log'}")
137
+ log(f" infer: {(time.time() - t) / 60:.1f}๋ถ„ โ†’ {run}")
138
+ return run
139
+
140
+
141
+ def collect(scene: str, cam: str, base: Path, log=print) -> dict[str, list]:
142
+ from PIL import Image
143
+
144
+ split = json.loads((base / "split.json").read_text())["test"]
145
+ dst_root = output_dir() / "aug" / "aug_artfixer" / scene
146
+ per_var: dict[str, list] = {}
147
+ size = None
148
+ for sid, ent in split.items():
149
+ prep_dir = Path(ent["prep_dir"])
150
+ pm = json.loads((prep_dir / "prep.json").read_text())
151
+ tr = json.loads((prep_dir / "transforms.json").read_text())
152
+ pred = next((base / "run").glob(f"*/*/{sid}/frames/batch_0000/pred"), None)
153
+ if pred is None:
154
+ raise RuntimeError(f"{sid}: ArtiFixer ๊ฒฐ๊ณผ ์—†์Œ โ€” ์ถ”๋ก ์ด ์ค‘๊ฐ„์— ๋Š๊ฒผ๋‹ค")
155
+ var = pm["variant"]["name"]
156
+ out = dst_root / var / cam
157
+ out.mkdir(parents=True, exist_ok=True)
158
+ size = Image.open(prep_dir / "renders/00000.png").size # ArtiFixer ๋Š” 16 ๋ฐฐ์ˆ˜๋กœ ๋ฐ”๊ฟ” ๋‚ด๋ฏ€๋กœ ๋ Œ๋” ํฌ๊ธฐ๋กœ ๋˜๋Œ๋ฆฐ๋‹ค
159
+ for i, f in enumerate(tr["frames"]):
160
+ if f["role"] != "target" or not f["original_filename"].startswith("samples/"): # ํ‚คํ”„๋ ˆ์ž„๋งŒ
161
+ continue
162
+ src = pred / f"{i:05d}.png"
163
+ if not src.exists():
164
+ continue
165
+ name = Path(f["original_filename"]).name
166
+ Image.open(src).convert("RGB").resize(size, Image.BICUBIC).save(out / name, quality=95)
167
+ per_var.setdefault(var, []).append({"segment": Path(pm["model"]).name, "original_filename": f["original_filename"],
168
+ "filename": f"{cam}/{name}", "timestamp": f["timestamp"]})
169
+ for var, samples in per_var.items():
170
+ p = dst_root / var / "meta.json"
171
+ old = json.loads(p.read_text()) if p.exists() else {}
172
+ keep = [s for s in old.get("samples", []) if not s["filename"].startswith(cam + "/")]
173
+ (dst_root / var / "meta.json").write_text(json.dumps({
174
+ "scene": scene, "variant": var, "cams": sorted(set(old.get("cams", [])) | {cam}),
175
+ "method": "ArtiFixer 1.3B (nv-tlabs/artifixer) on VKGS-Sim 3DGS renders",
176
+ "size": list(size), "keyframes_only": True,
177
+ "samples": sorted(keep + samples, key=lambda s: (s["filename"].split("/")[0], s["timestamp"]))}, indent=1))
178
+ log(f" collect: {sum(map(len, per_var.values()))}์žฅ โ†’ {dst_root}")
179
+ return per_var
180
+
181
+
182
+ def lanefix(scene: str, cam: str, base: Path, log=print) -> int:
183
+ from scipy.spatial import cKDTree
184
+
185
+ from nusim.aug.lane_fix import LaneFixConfig, Segmenter, fix_prep_dir, map_points
186
+ from nusim.data.index import load_scene
187
+
188
+ lcfg = LaneFixConfig()
189
+ sc = load_scene(scene)
190
+ tree = cKDTree(map_points(sc, lcfg))
191
+ seg = Segmenter()
192
+ src_root = output_dir() / "aug" / "aug_artfixer" / scene
193
+ dst_root = output_dir() / "aug" / "aug_artfixer" / f"{scene}_lanefix"
194
+ n = 0
195
+ preps = sorted({Path(e["prep_dir"]) for e in json.loads((base / "split.json").read_text())["test"].values()})
196
+ for prep in preps: # ๊ตฌ๊ฐ„ ์—†๋Š” ์”ฌ์€ scene-XXXX_<๋ณ€ํ˜•> โ€” seg* ๋งŒ ๋ณด๋ฉด 0์žฅ์ด ๋œ๋‹ค
197
+ var = json.loads((prep / "prep.json").read_text())["variant"]["name"]
198
+ n += fix_prep_dir(prep, src_root / var, dst_root / var, seg, sc, tree, lcfg)
199
+ for var_dir in src_root.iterdir():
200
+ if not (var_dir / "meta.json").exists() or not (dst_root / var_dir.name).exists():
201
+ continue
202
+ m = json.loads((var_dir / "meta.json").read_text())
203
+ m["method"] += " + ๋…ธ๋ฉด ํ‰๋ฉด ํ˜ธ๋ชจ๊ทธ๋ž˜ํ”ผ ์ฐจ์„  ํ›„์ฒ˜๋ฆฌ (nusim.aug.lane_fix)"
204
+ m["lanefix"] = asdict(lcfg)
205
+ (dst_root / var_dir.name / "meta.json").write_text(json.dumps(m, indent=1, ensure_ascii=False))
206
+ del seg
207
+ import torch
208
+
209
+ torch.cuda.empty_cache()
210
+ log(f" lanefix: {n}์žฅ โ†’ {dst_root}")
211
+ return n
212
+
213
+
214
+ def cleanup(base: Path) -> None:
215
+ """๋ฌด๊ฑฐ์šด ์ค‘๊ฐ„ ํŒŒ์ผ๋งŒ ์ง€์šด๋‹ค โ€” prep.json ยท transforms.json ยท split.json ์€ ๋‚จ๊ฒจ ๋‹ค์‹œ ํ›„์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ."""
216
+ shutil.rmtree(base / "run", ignore_errors=True)
217
+ for prep in (d for d in base.iterdir() if d.is_dir() and d.name != "run"): # ๊ตฌ๊ฐ„ ์—†๋Š” ์”ฌ์€ scene-XXXX_<๋ณ€ํ˜•>
218
+ for d in ("images", "renders", "opacity"):
219
+ shutil.rmtree(prep / d, ignore_errors=True)
220
+
221
+
222
+ def aug_scene(scene: str, cfg: dict | None = None, root: Path | None = None, cams=None, force: bool = False,
223
+ log=print) -> dict:
224
+ cfg = cfg or artifixer_config()
225
+ root = root or output_dir() / "gs_day"
226
+ res = {}
227
+ for cam in cams or cfg["cams"]:
228
+ if cam in done_cams(scene) and not force:
229
+ log(f" {cam}: ์ด๋ฏธ ๋๋‚จ โ€” ๊ฑด๋„ˆ๋œ€")
230
+ continue
231
+ t = time.time()
232
+ base = output_dir() / "artifixer" / f"{scene}_{cam}"
233
+ per_var = None if force else _collected(scene, cam, base)
234
+ if per_var: # ์ถ”๋ก  ยท ์ˆ˜์ง‘๊นŒ์ง€ ๋๋‚˜๊ณ  ์ฐจ์„  ํ›„์ฒ˜๋ฆฌ์—์„œ ์‹คํŒจํ•œ ์”ฌ โ€” 20๋ถ„์งœ๋ฆฌ ์ถ”๋ก ์„ ๋‹ค์‹œ ๋Œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค
235
+ log(f" {cam}: ArtiFixer ๊ฒฐ๊ณผ {sum(map(len, per_var.values()))}์žฅ ์žˆ์Œ โ€” ์ฐจ์„  ํ›„์ฒ˜๋ฆฌ๋ถ€ํ„ฐ")
236
+ elif not force and _inferred(base):
237
+ log(f" {cam}: ์ถ”๋ก  ๊ฒฐ๊ณผ ์žˆ์Œ โ€” ์ˆ˜์ง‘๋ถ€ํ„ฐ")
238
+ else:
239
+ base = prep_scene(scene, cam, cfg, root, log=log)
240
+ import torch
241
+
242
+ torch.cuda.empty_cache() # ArtiFixer ๊ฐ€ 8 GB ๊ฑฐ์˜ ์ „๋ถ€๋ฅผ ์“ด๋‹ค
243
+ infer(base, cfg, log=log)
244
+ if not per_var:
245
+ per_var = collect(scene, cam, base, log=log)
246
+ n = lanefix(scene, cam, base, log=log)
247
+ if not cfg.get("keep_intermediate"):
248
+ cleanup(base)
249
+ p = output_dir() / "aug" / "aug_artfixer" / f"{scene}_lanefix" / "done.json"
250
+ p.parent.mkdir(parents=True, exist_ok=True)
251
+ d = json.loads(p.read_text()) if p.exists() else {"scene": scene, "cams": {}}
252
+ d["cams"][cam] = {"images": n, "variants": sorted(per_var), "minutes": round((time.time() - t) / 60, 1),
253
+ "finished": time.strftime("%Y-%m-%d %H:%M")}
254
+ p.write_text(json.dumps(d, indent=1))
255
+ res[cam] = d["cams"][cam]
256
+ log(f" {cam}: ์™„๋ฃŒ {n}์žฅ, {d['cams'][cam]['minutes']}๋ถ„")
257
+ return res
src/nusim/nusim/aug/lane_check.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """์ฆ๊ฐ• ์˜์ƒ์˜ ์ฐจ์„  ๋ณด์ž„ ๊ฒ€์‚ฌ (nusim aug lanecheck) โ€” ์ฐจ์„  ๊ฒ€์ถœ๊ธฐ ์—†์ด, nuScenes ๋งต์˜ ์ฐจ์„ ์œผ๋กœ ์žฐ๋‹ค.
2
+
3
+ ๋งต์˜ lane_divider ยท road_divider ํด๋ฆฌ๋ผ์ธ์„ ์ง€๋ฉด(์ž์ฐจ ๋†’์ด)์— ๋†“๊ณ  ์นด๋ฉ”๋ผ์— ํˆฌ์˜ํ•œ ๋’ค, ํ‘œ๋ณธ์ ๋งˆ๋‹ค ์„ ์— ์ˆ˜์ง์ธ
4
+ ๋ฐ๊ธฐ ๋‹จ๋ฉด์„ ๋ณธ๋‹ค. ๋‹จ๋ฉด ๊ฐ€์šด๋ฐ ์ฐฝ(ยฑsearch_m, ๋งต ยท ํฌ์ฆˆ ์˜ค์ฐจ ๋ชซ) ์•ˆ์— ์ฃผ๋ณ€(์ค‘์•™๊ฐ’)๋ณด๋‹ค contrast ์ด์ƒ ๋ฐ์€ ๋Šฅ์„ ์ด ์žˆ์œผ๋ฉด
5
+ '๋ณด์ž„'. ์‹ค์ œ ์‚ฌ์ง„ ยท ์›๋ž˜ ์œ„์น˜ ๋ Œ๋” ยท ๋ณ€ํ˜• ๋ Œ๋”๋ฅผ ๊ฐ™์€ ํ‘œ๋ณธ์ ์œผ๋กœ ์žฌ๋ฏ€๋กœ, ์ฐจ์ด๊ฐ€ ๊ณง ์žฌ๊ตฌ์„ฑ ยท ์‹œ์  ์ด๋™์˜ ์†์‹ค์ด๋‹ค.
6
+ ๋Šฅ์„ ์ด ๋‘˜ ์ด์ƒ(์‚ฌ์ด์— ๊ณจ)์ด๋ฉด '์ด์ค‘์„ ' โ€” ์‹œ์  ์ด๋™์—์„œ ๋…ธ๋ฉด ๊ธฐํ•˜๊ฐ€ ํ‹€๋ ค ์ฐจ์„ ์ด ๊ฐˆ๋ผ์ง„ ๊ฒƒ.
7
+
8
+ ํ‘œ๋ณธ์ : ์นด๋ฉ”๋ผ ์•ž near_m~far_m, 0.5 m ๊ฐ„๊ฒฉ, ์˜์ƒ ์•ˆ. ์‚ฌ์ง„์—์„œ ์ฐจ๋Ÿ‰์ด ๊ฐ€๋ฆฐ ์ ์€ ์‚ฌ์ง„์—์„œ๋„ ์•ˆ ๋ณด์ด๋ฏ€๋กœ
9
+ ๋ณด์ž„ ๋น„์œจ์€ ์‚ฌ์ง„ ๋Œ€๋น„๋กœ ์ฝ๋Š”๋‹ค (recall_vs_photo = ๋ Œ๋”๋„ ๋ณด์ด๊ณ  ์‚ฌ์ง„๋„ ๋ณด์ธ ์  / ์‚ฌ์ง„์—์„œ ๋ณด์ธ ์ ).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+
18
+ import numpy as np
19
+
20
+
21
+ @dataclass
22
+ class LaneCheckConfig:
23
+ near_m: float = 5.0
24
+ far_m: float = 25.0
25
+ step_m: float = 0.5
26
+ profile_m: float = 1.2 # ๋‹จ๋ฉด ๋ฐ˜ํญ (์ง€๋ฉด m)
27
+ search_m: float = 0.35 # ๋Šฅ์„ ์„ ์ฐพ๋Š” ๊ฐ€์šด๋ฐ ์ฐฝ ๋ฐ˜ํญ
28
+ contrast: float = 0.06 # ๋Šฅ์„  โˆ’ ์ฃผ๋ณ€ ์ค‘์•™๊ฐ’ (0~1 ๋ฐ๊ธฐ)
29
+ cams: tuple = ("CAM_FRONT",)
30
+ layers: tuple = ("lane_divider", "road_divider")
31
+
32
+
33
+ def _gray(path: Path) -> np.ndarray:
34
+ from PIL import Image
35
+
36
+ return np.asarray(Image.open(path).convert("L"), np.float32) / 255
37
+
38
+
39
+ def map_lines(scene, margin: float = 40.0) -> list[np.ndarray]:
40
+ from nusim.maps.expansion import BBox, MapExpansion
41
+ from nusim.paths import dataset
42
+
43
+ ego = np.array([f.ego_translation for f in scene.frames["CAM_FRONT"]])
44
+ box = BBox(ego[:, 0].min() - margin, ego[:, 1].min() - margin, ego[:, 0].max() + margin, ego[:, 1].max() + margin)
45
+ feats = MapExpansion(scene.location, dataset()).features(box)
46
+ out = []
47
+ for layer in LaneCheckConfig.layers:
48
+ for pl in feats.get(layer, []):
49
+ pl = np.asarray(pl, np.float64)[:, :2]
50
+ d = np.r_[0, np.cumsum(np.linalg.norm(np.diff(pl, axis=0), axis=1))]
51
+ t = np.arange(0, d[-1], 0.5)
52
+ if len(t) >= 2:
53
+ out.append(np.c_[np.interp(t, d, pl[:, 0]), np.interp(t, d, pl[:, 1])])
54
+ return out
55
+
56
+
57
+ def sample_points(pose, lines: list[np.ndarray], ground_z: float, cfg: LaneCheckConfig):
58
+ """(u, v, ๋‹จ๋ฉด ๋ฐฉํ–ฅ (du, dv) ํ”ฝ์…€/m) โ€” ์ง€๋ฉด ์œ„ ์ฐจ์„  ํ‘œ๋ณธ์  ์ค‘ ๋ณด์ด๋Š” ๊ฒƒ."""
59
+ w2c, K = pose.w2c, pose.K
60
+ out = []
61
+ for pl in lines:
62
+ tang = np.gradient(pl, axis=0)
63
+ tang /= np.linalg.norm(tang, axis=1, keepdims=True) + 1e-9
64
+ nrm = np.c_[-tang[:, 1], tang[:, 0]]
65
+ P = np.c_[pl, np.full(len(pl), ground_z), np.ones(len(pl))]
66
+ Pn = np.c_[pl + 0.1 * nrm, np.full(len(pl), ground_z), np.ones(len(pl))]
67
+ c, cn = (w2c @ P.T).T, (w2c @ Pn.T).T
68
+ ok = (c[:, 2] > cfg.near_m) & (c[:, 2] < cfg.far_m)
69
+ for i in np.nonzero(ok)[0]:
70
+ uv = K @ c[i, :3]
71
+ uvn = K @ cn[i, :3]
72
+ u, v = uv[:2] / uv[2]
73
+ un, vn = uvn[:2] / uvn[2]
74
+ if 2 <= u < pose.width - 2 and 2 <= v < pose.height - 2:
75
+ out.append((u, v, (un - u) / 0.1, (vn - v) / 0.1))
76
+ return out
77
+
78
+
79
+ def ridge(img: np.ndarray, u, v, du, dv, cfg: LaneCheckConfig) -> tuple[bool, int]:
80
+ """(๊ฐ€์šด๋ฐ ์ฐฝ์— ๋Šฅ์„ ์ด ์žˆ๋‚˜, ์ฐฝ ์•ˆ ๋Šฅ์„  ์ˆ˜)."""
81
+ s = np.linspace(-cfg.profile_m, cfg.profile_m, 49)
82
+ x, y = u + s * du, v + s * dv
83
+ h, w = img.shape
84
+ keep = (x >= 0) & (x < w - 1) & (y >= 0) & (y < h - 1)
85
+ if keep.sum() < 30:
86
+ return False, 0
87
+ xi, yi = np.clip(x, 0, w - 1).astype(int), np.clip(y, 0, h - 1).astype(int)
88
+ prof = img[yi, xi]
89
+ base = np.median(prof[keep])
90
+ mid = keep & (np.abs(s) <= cfg.search_m)
91
+ lift = prof - base
92
+ hit = mid & (lift > cfg.contrast)
93
+ if not hit.any():
94
+ return False, 0
95
+ runs = np.diff(np.r_[0, hit.astype(int), 0])
96
+ return True, int((runs == 1).sum())
97
+
98
+
99
+ def check_scene(scene_name: str, sources: dict[str, Path], variants: dict[str, dict], cfg: LaneCheckConfig | None = None,
100
+ keyframes: bool = True) -> dict:
101
+ """sources: ์ด๋ฆ„ โ†’ ์˜์ƒ ๋””๋ ‰ํ„ฐ๋ฆฌ (<dir>/<CAM>/<์›๋ณธ ํŒŒ์ผ๋ช…>), 'photo' ๋Š” ๋ฐ์ดํ„ฐ์…‹ ์›๋ณธ. variants: ์ด๋ฆ„ โ†’ ๋ฆฌ๊ทธ ๋ณ€ํ˜•
102
+ (๊ทธ ์†Œ์Šค์˜ ์นด๋ฉ”๋ผ ํฌ์ฆˆ). ๊ฒฐ๊ณผ: ์†Œ์Šค๋งˆ๋‹ค ํ‘œ๋ณธ์  ์ˆ˜ ยท ๋ณด์ž„ ๋น„์œจ ยท ์‚ฌ์ง„ ๋Œ€๋น„ ์žฌํ˜„์œจ ยท ์ด์ค‘์„  ๋น„์œจ."""
103
+ from nusim.aug.rig_aug import transform_calib
104
+ from nusim.data.index import load_scene
105
+ from nusim.data.rig import camera_pose
106
+ from nusim.paths import dataset
107
+
108
+ cfg = cfg or LaneCheckConfig()
109
+ scene = load_scene(scene_name)
110
+ ds = dataset()
111
+ lines = map_lines(scene)
112
+ res = {}
113
+ for name, d in sources.items():
114
+ var = variants.get(name, {})
115
+ st = {"points": 0, "visible": 0, "double": 0, "photo_visible": 0, "both": 0}
116
+ for cam in cfg.cams:
117
+ calib = transform_calib(scene.calib[cam], var.get("shift_m", (0, 0, 0)), var.get("rot_deg", (0, 0, 0)))
118
+ calib0 = scene.calib[cam]
119
+ for fr in scene.frames[cam]:
120
+ if keyframes and not fr.is_keyframe:
121
+ continue
122
+ p = (ds.root / fr.filename) if d is None else d / cam / Path(fr.filename).name
123
+ if not p.exists():
124
+ continue
125
+ img = _gray(p)
126
+ pose = camera_pose(fr, calib, cam)
127
+ gz = fr.ego_translation[2]
128
+ pts = sample_points(pose, lines, gz, cfg)
129
+ photo = None
130
+ if d is not None and not var: # ๊ฐ™์€ ํฌ์ฆˆ์˜ ์‚ฌ์ง„๊ณผ ์ ๋งˆ๋‹ค ์ง์ง€์–ด ์žฌํ˜„์œจ
131
+ photo = _gray(ds.root / fr.filename)
132
+ for u, v, du, dv in pts:
133
+ vis, n = ridge(img, u, v, du, dv, cfg)
134
+ st["points"] += 1
135
+ st["visible"] += vis
136
+ st["double"] += n >= 2
137
+ if photo is not None:
138
+ pv, _ = ridge(photo, u, v, du, dv, cfg)
139
+ st["photo_visible"] += pv
140
+ st["both"] += pv and vis
141
+ n = max(st["points"], 1)
142
+ res[name] = {**st, "visible_rate": st["visible"] / n, "double_rate": st["double"] / max(st["visible"], 1)}
143
+ if st["photo_visible"]:
144
+ res[name]["recall_vs_photo"] = st["both"] / st["photo_visible"]
145
+ return res
src/nusim/nusim/aug/lane_fix.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """์ฐจ์„  ํ›„์ฒ˜๋ฆฌ โ€” ArtiFixer ์ฆ๊ฐ• ์˜์ƒ์˜ ๋Š๊ธฐ๊ณ  ํ๋ฆฐ ์ฐจ์„ ์„ ์‹ค์ œ ์‚ฌ์ง„์˜ ์ฐจ์„ ์œผ๋กœ ๋ฐ”๊พผ๋‹ค (scripts/artifixer_lanefix.py).
2
+
3
+ ์‹œ์  ๋ณ€ํ˜•์€ ๊ฐ™์€ ์‹œ๊ฐ ยท ๊ฐ™์€ ์ž์ฐจ์—์„œ ์นด๋ฉ”๋ผ๋งŒ ์˜ฎ๊ธด ๊ฒƒ์ด๋‹ค. ๋…ธ๋ฉด์„ ํ‰๋ฉด(์ž์ฐจ ์ขŒํ‘œ z = 0)์œผ๋กœ ๋ณด๋ฉด, ๋ณ€ํ˜• ์นด๋ฉ”๋ผ์˜ ํ”ฝ์…€๋งˆ๋‹ค
4
+ ๊ด‘์„  โ†’ ๋…ธ๋ฉด ๊ต์  โ†’ ์›๋ž˜ ์นด๋ฉ”๋ผ๋กœ ํˆฌ์˜ํ•ด **์‹ค์ œ ์‚ฌ์ง„**(1600ร—900)์„ ๊ทธ ์ž๋ฆฌ์— ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค (ํ‰๋ฉด ํ˜ธ๋ชจ๊ทธ๋ž˜ํ”ผ โ€” ํ‰ํ‰ํ•œ ๋…ธ๋ฉด์ด๋ฉด
5
+ ๊ธฐํ•˜๊ฐ€ ์ •ํ™•ํ•˜๋‹ค). ๊ทธ์ค‘ nuScenes HD ๋งต์˜ lane_divider ยท road_divider ยท ped_crossing ยท stop_line ์ฃผ๋ณ€(ยฑband_m)๋งŒ ๋ฐ”๊พผ๋‹ค:
6
+ ๋งต์€ '์ฐจ์„ ์ด ์–ด๋”” ์žˆ๋‚˜' ๋งŒ ์ฃผ๊ณ , ์ฐจ์„ ์˜ ๋ชจ์–‘(์ ์„  ๊ฐ„๊ฒฉ ยท ํญ ยท ์ƒ‰)์€ ์‹ค์ œ ์‚ฌ์ง„์—์„œ ์˜จ๋‹ค (๋งต์—๋Š” ์ ์„  ๊ฐ„๊ฒฉ ยท ํญ์ด ์—†๊ณ 
7
+ ์˜์ƒ๊ณผ 10~30 cm ์–ด๊ธ‹๋‚˜์„œ, ๋งต ์„ ์„ ์ง์ ‘ ๊ทธ๋ฆฌ๋ฉด ์‹ค์ œ์™€ ์•ˆ ๋งž๋Š”๋‹ค).
8
+
9
+ ๋นผ๋Š” ๊ณณ: ๋…ธ๋ฉด ๊ต์ ์ด max_dist_m ๋ณด๋‹ค ๋จผ ๊ณณ(์ž‘์€ ๊ฐ๋„ ์˜ค์ฐจ๊ฐ€ ํฌ๊ฒŒ ๋ฒˆ์ง„๋‹ค), ์›๋ž˜ ์นด๋ฉ”๋ผ ์˜์ƒ ๋ฐ–, ์‹ค์ œ ์‚ฌ์ง„์—์„œ ์ฐจ๋Ÿ‰ ยท ์‚ฌ๋žŒ์ด
10
+ ๊ฐ€๋ฆฐ ๊ณณ (SegFormer-B5 Cityscapes โ€” nusim.recon.sky ์™€ ๊ฐ™์€ ๋ชจ๋ธ), ์‹ค์ œ ์‚ฌ์ง„์—์„œ ๋…ธ๋ฉด ยท ๋ณด๋„๊ฐ€ ์•„๋‹Œ ๊ณณ.
11
+ ํ•ฉ์„ฑ: ๋  ์•ˆ ๋…ธ๋ฉด(์ฐจ์„  ์ œ์™ธ)์˜ ํ‰๊ท  ยท ํ‘œ์ค€ํŽธ์ฐจ๋กœ ์‹ค์ œ ์‚ฌ์ง„ ์ƒ‰์„ ArtiFixer ์˜์ƒ ์ชฝ์œผ๋กœ ๋งž์ถ”๊ณ , ๊ฐ€์žฅ์ž๋ฆฌ๋ฅผ ํ๋ ค ์„ž๋Š”๋‹ค.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+
20
+ import numpy as np
21
+
22
+ OPENGL_TO_OPENCV = np.diag([1.0, -1.0, -1.0, 1.0])
23
+ MOVERS = ("car", "truck", "bus", "train", "motorcycle", "bicycle", "person", "rider")
24
+ GROUND = ("road", "sidewalk")
25
+
26
+
27
+ @dataclass
28
+ class LaneFixConfig:
29
+ band_m: float = 0.6 # ๋งต ์„  ๋‘˜๋ ˆ ๋ฐ”๊ฟ€ ๋  ๋ฐ˜ํญ
30
+ max_dist_m: float = 35.0 # ์ด๋ณด๋‹ค ๋จผ ๋…ธ๋ฉด์€ ๊ทธ๋Œ€๋กœ (ํ˜ธ๋ชจ๊ทธ๋ž˜ํ”ผ ์˜ค์ฐจ๊ฐ€ ์ปค์ง„๋‹ค)
31
+ feather_px: float = 12.0 # ํ•ฉ์„ฑ ๊ฐ€์žฅ์ž๋ฆฌ ์„ž๋Š” ํญ (์˜์—ญ ์•ˆ์ชฝ์œผ๋กœ, ์ถœ๋ ฅ ํ•ด์ƒ๋„ ํ”ฝ์…€)
32
+ contrast: float = 0.75 # ์‹ค์ œ ์‚ฌ์ง„ ๋…ธ๋ฉด ๋Œ€๋น„๋ฅผ ArtiFixer ์ชฝ์œผ๋กœ ๋งž์ถ”๋Š” ์ •๋„ (1 = ์™„์ „ํžˆ)
33
+ mover_dilate_px: int = 9 # ์‹ค์ œ ์‚ฌ์ง„ ์ฐจ๋Ÿ‰ ๋งˆ์Šคํฌ ํŒฝ์ฐฝ (์›๋ณธ ํ•ด์ƒ๋„)
34
+ layers: tuple = ("lane_divider", "road_divider", "ped_crossing", "stop_line")
35
+ mode: str = "road" # road = ๋…ธ๋ฉด ์ „์ฒด๋ฅผ ์‹ค์ œ ์‚ฌ์ง„์œผ๋กœ (๊ถŒ์žฅ) ยท band = ๋งต ์ฐจ์„  ๋ ๋งŒ
36
+
37
+
38
+ def map_points(scene, cfg: LaneFixConfig, step: float = 0.1, margin: float = 60.0) -> np.ndarray:
39
+ """๋งต ์ฐจ์„  ์š”์†Œ๋ฅผ ์ „์—ญ xy ์ ๋“ค๋กœ (์„ ์€ 0.1 m ๊ฐ„๊ฒฉ, ๋‹ค๊ฐํ˜•์€ ํ…Œ๋‘๋ฆฌ + ๋‚ด๋ถ€ ๊ฒฉ์ž)."""
40
+ from matplotlib.path import Path as MPath
41
+
42
+ from nusim.maps.expansion import BBox, MapExpansion
43
+ from nusim.paths import dataset
44
+
45
+ ego = np.array([f.ego_translation for f in scene.frames["CAM_FRONT"]])
46
+ box = BBox(ego[:, 0].min() - margin, ego[:, 1].min() - margin, ego[:, 0].max() + margin, ego[:, 1].max() + margin)
47
+ feats = MapExpansion(scene.location, dataset()).features(box)
48
+ pts = []
49
+ for layer in cfg.layers:
50
+ for pl in feats.get(layer, []):
51
+ pl = np.asarray(pl, np.float64)[:, :2]
52
+ d = np.r_[0, np.cumsum(np.linalg.norm(np.diff(pl, axis=0), axis=1))]
53
+ if d[-1] <= 0:
54
+ continue
55
+ t = np.arange(0, d[-1], step)
56
+ pts.append(np.c_[np.interp(t, d, pl[:, 0]), np.interp(t, d, pl[:, 1])])
57
+ if layer in ("ped_crossing", "stop_line") and len(pl) >= 3: # ๋‹ค๊ฐํ˜• ๋‚ด๋ถ€๋„ (ํšก๋‹จ๋ณด๋„ ์ค„๋ฌด๋Šฌ)
58
+ lo, hi = pl.min(0), pl.max(0)
59
+ gx, gy = np.meshgrid(np.arange(lo[0], hi[0], 0.3), np.arange(lo[1], hi[1], 0.3))
60
+ g = np.c_[gx.ravel(), gy.ravel()]
61
+ pts.append(g[MPath(pl).contains_points(g)])
62
+ return np.concatenate(pts) if pts else np.zeros((0, 2))
63
+
64
+
65
+ class Segmenter:
66
+ def __init__(self):
67
+ import torch
68
+ from transformers import SegformerForSemanticSegmentation
69
+
70
+ from nusim.recon.sky import MODEL
71
+
72
+ self.torch = torch
73
+ self.model = SegformerForSemanticSegmentation.from_pretrained(MODEL).half().cuda().eval()
74
+ ids = {v.lower(): int(k) for k, v in self.model.config.id2label.items()}
75
+ self.movers = [ids[k] for k in MOVERS if k in ids]
76
+ self.ground = [ids[k] for k in GROUND if k in ids]
77
+ self.mean = torch.tensor([0.485, 0.456, 0.406], device="cuda")[:, None, None]
78
+ self.std = torch.tensor([0.229, 0.224, 0.225], device="cuda")[:, None, None]
79
+
80
+ def __call__(self, im: np.ndarray) -> np.ndarray:
81
+ """(H,W,3) uint8 โ†’ ํด๋ž˜์Šค ๋ฒˆํ˜ธ (H,W)."""
82
+ import torch.nn.functional as F
83
+
84
+ t = self.torch.as_tensor(im, device="cuda").permute(2, 0, 1).float() / 255
85
+ h, w = im.shape[:2]
86
+ t = F.interpolate(t[None], size=(576, 1024), mode="bilinear", align_corners=False)
87
+ with self.torch.no_grad():
88
+ lg = self.model(pixel_values=((t - self.mean) / self.std).half()).logits
89
+ lg = F.interpolate(lg.float(), size=(h, w), mode="bilinear", align_corners=False)[0]
90
+ return lg.argmax(0).cpu().numpy()
91
+
92
+
93
+ def ground_lookup(c2w_v: np.ndarray, Kv: np.ndarray, size_v, plane_n: np.ndarray, plane_p: np.ndarray):
94
+ """๋ณ€ํ˜• ์นด๋ฉ”๋ผ ํ”ฝ์…€๋งˆ๋‹ค ๋…ธ๋ฉด ๊ต์  (H,W,3) ๊ณผ ๊ฑฐ๋ฆฌ (H,W). ๋…ธ๋ฉด์— ์•ˆ ๋‹ฟ์œผ๋ฉด nan."""
95
+ w, h = size_v
96
+ u, v = np.meshgrid(np.arange(w) + 0.5, np.arange(h) + 0.5)
97
+ d_cam = np.stack([(u - Kv[0, 2]) / Kv[0, 0], (v - Kv[1, 2]) / Kv[1, 1], np.ones_like(u)], -1)
98
+ R, o = c2w_v[:3, :3], c2w_v[:3, 3]
99
+ d = d_cam @ R.T
100
+ denom = d @ plane_n
101
+ t = ((plane_p - o) @ plane_n) / np.where(np.abs(denom) < 1e-9, np.nan, denom)
102
+ t = np.where(t > 0, t, np.nan)
103
+ X = o + d * t[..., None]
104
+ dist = t * np.linalg.norm(d_cam, axis=-1)
105
+ return X, dist
106
+
107
+
108
+ def project(X: np.ndarray, c2w: np.ndarray, K: np.ndarray):
109
+ w2c = np.linalg.inv(c2w)
110
+ Xc = X @ w2c[:3, :3].T + w2c[:3, 3]
111
+ z = Xc[..., 2]
112
+ uv = (Xc[..., :2] / z[..., None]) * np.array([K[0, 0], K[1, 1]]) + np.array([K[0, 2], K[1, 2]])
113
+ return uv, z
114
+
115
+
116
+ def bilinear(img: np.ndarray, uv: np.ndarray) -> np.ndarray:
117
+ import cv2
118
+
119
+ m = uv.astype(np.float32)
120
+ return cv2.remap(img, m[..., 0] - 0.5, m[..., 1] - 0.5, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
121
+
122
+
123
+ def fix_image(art: np.ndarray, photo: np.ndarray, seg: np.ndarray, seg_ids: tuple, c2w_v, c2w_o, Kv, Ko,
124
+ plane_n, plane_p, origin_xy, tree, cfg: LaneFixConfig) -> tuple[np.ndarray, np.ndarray]:
125
+ """(ํ›„์ฒ˜๋ฆฌ ์˜์ƒ, ๋ฐ”๊พผ ๊ฐ€์ค‘์น˜ ฮฑ) โ€” art (h,w,3) ์ถœ๋ ฅ ํ•ด์ƒ๋„, photo (H,W,3) ์›๋ณธ ํ•ด์ƒ๋„."""
126
+ import cv2
127
+
128
+ h, w = art.shape[:2]
129
+ X, dist = ground_lookup(c2w_v, Kv, (w, h), plane_n, plane_p)
130
+ ok = np.isfinite(dist) & (dist < cfg.max_dist_m)
131
+ Xs = np.where(ok[..., None], X, 0)
132
+ if cfg.mode == "band":
133
+ dmap, _ = tree.query((Xs[..., :2] + origin_xy).reshape(-1, 2), distance_upper_bound=cfg.band_m)
134
+ ok &= np.isfinite(dmap.reshape(h, w))
135
+ uv, z = project(Xs, c2w_o, Ko)
136
+ H, W = photo.shape[:2]
137
+ ok &= (z > 0.5) & (uv[..., 0] >= 0) & (uv[..., 0] < W - 1) & (uv[..., 1] >= 0) & (uv[..., 1] < H - 1)
138
+ movers, ground = seg_ids
139
+ bad = np.isin(seg, movers).astype(np.uint8)
140
+ bad = cv2.dilate(bad, np.ones((cfg.mover_dilate_px, cfg.mover_dilate_px), np.uint8)) > 0
141
+ good = np.isin(seg, ground if cfg.mode == "band" else ground[:1]) & ~bad # road ๋ชจ๋“œ: ์ฐจ๋„๋งŒ (๋ณด๋„๋Š” 15 cm ๋†’์•„ ํ‰๋ฉด์ด ์•„๋‹ˆ๋‹ค)
142
+ uvc = np.clip(np.nan_to_num(uv), 0, [W - 1, H - 1])
143
+ ok &= bilinear(good.astype(np.float32), uvc) > 0.99
144
+ if ok.sum() < 50:
145
+ return art, np.zeros((h, w), np.float32)
146
+ warped = bilinear(photo.astype(np.float32), uvc)
147
+ # ์ƒ‰ ๋งž์ถค: ๋  ์•ˆ ๋…ธ๋ฉด(์ฐจ์„  ํŽ˜์ธํŠธ = ๋ฐ์€ ์ƒ์œ„ 20% ์ œ์™ธ)์—์„œ ์ฑ„๋„๋ณ„ ํ‰๊ท  ยท ํ‘œ์ค€ํŽธ์ฐจ
148
+ lum_w, lum_a = warped.mean(-1), art.astype(np.float32).mean(-1)
149
+ road = ok & (lum_w < np.percentile(lum_w[ok], 80))
150
+ out = art.astype(np.float32).copy()
151
+ fixed = warped.copy()
152
+ for c in range(3):
153
+ mw, sw = warped[..., c][road].mean(), warped[..., c][road].std() + 1e-3
154
+ ma, sa = out[..., c][road].mean(), out[..., c][road].std() + 1e-3
155
+ k = cfg.contrast * (sa / sw) + (1 - cfg.contrast) # ๋Œ€๋น„๋ฅผ ๋‹ค ๋งž์ถ”๋ฉด ์ฐจ์„ ์ด ํ๋ ค์ง„๋‹ค โ€” ์ผ๋ถ€๋งŒ
156
+ fixed[..., c] = (warped[..., c] - mw) * k + ma
157
+ # ์˜์—ญ ๊ฒฝ๊ณ„(์›๋ž˜ ์นด๋ฉ”๋ผ ์‹œ์•ผ ๋ ยท ์—ฐ์„ ยท ์ฐจ๋Ÿ‰)์—์„œ ์•ˆ์ชฝ์œผ๋กœ feather_px ์— ๊ฑธ์ณ ์„ž๋Š”๋‹ค
158
+ inside = cv2.distanceTransform(ok.astype(np.uint8), cv2.DIST_L2, 5)
159
+ a = cv2.GaussianBlur(np.clip(inside / cfg.feather_px, 0, 1).astype(np.float32), (0, 0), 1.5) * ok
160
+ out = out * (1 - a[..., None]) + fixed * a[..., None]
161
+ _ = lum_a
162
+ return np.clip(out, 0, 255).astype(np.uint8), a
163
+
164
+
165
+ def plane_of(frame, origin: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
166
+ """์ž์ฐจ ์ขŒํ‘œ z = 0 ํ‰๋ฉด (์”ฌ ์›์  ๊ธฐ์ค€ ์ขŒํ‘œ์˜ ๋ฒ•์„  ยท ํ•œ ์ )."""
167
+ from nusim.data.rig import quat_to_matrix
168
+
169
+ R = quat_to_matrix(frame.ego_rotation)
170
+ return R[:, 2], np.asarray(frame.ego_translation, np.float64) - origin
171
+
172
+
173
+ def fix_prep_dir(prep_dir: Path, art_dir: Path, out_dir: Path, seg: Segmenter, scene, tree, cfg: LaneFixConfig,
174
+ keyframes_only: bool = True, debug_dir: Path | None = None) -> int:
175
+ """์ค€๋น„ ๋””๋ ‰ํ„ฐ๋ฆฌ ํ•˜๋‚˜(๊ตฌ๊ฐ„ ร— ๋ณ€ํ˜•)์˜ ๊ณ ์น  ํ”„๋ ˆ์ž„๋งˆ๋‹ค. art_dir = ๊ทธ ๋ณ€ํ˜•์˜ ArtiFixer ๊ฒฐ๊ณผ (<CAM>/<์›๋ณธ ํŒŒ์ผ๋ช…>.jpg)."""
176
+ from PIL import Image
177
+
178
+ from nusim.io.ply import read_origin
179
+ from nusim.paths import dataset
180
+
181
+ pm = json.loads((prep_dir / "prep.json").read_text())
182
+ tr = json.loads((prep_dir / "transforms.json").read_text())
183
+ origin = read_origin(Path(pm["model"]) / "point_cloud.ply")
184
+ frames = tr["frames"]
185
+ n = len(frames) // 2
186
+ Kv = np.array([[tr["fl_x"], 0, tr["cx"]], [0, tr["fl_y"], tr["cy"]], [0, 0, 1]], np.float64)
187
+ by_fn = {f.filename: f for f in scene.frames[pm["cam"]]}
188
+ calib = scene.calib[pm["cam"]]
189
+ Ko = np.asarray(calib.intrinsic, np.float64)
190
+ ds = dataset()
191
+ cnt = 0
192
+ for i in range(n):
193
+ ref, tgt = frames[i], frames[i + n]
194
+ fn = ref["original_filename"]
195
+ if keyframes_only and not fn.startswith("samples/"):
196
+ continue
197
+ src = art_dir / pm["cam"] / Path(fn).name
198
+ if not src.exists():
199
+ continue
200
+ art = np.asarray(Image.open(src).convert("RGB"))
201
+ photo = np.asarray(Image.open(ds.root / fn).convert("RGB"))
202
+ s = photo.shape[1] / tr["w"] # ์›๋ž˜ ์นด๋ฉ”๋ผ๋Š” ์›๋ณธ ํ•ด์ƒ๋„๋กœ โ€” ์ž์„ธ๋Š” ๊ฐ™๊ณ  K ๋งŒ ํ‚ค์šด๋‹ค
203
+ Ko_s = Kv.copy()
204
+ Ko_s[:2] *= s
205
+ c2w_v = np.asarray(tgt["transform_matrix"]) @ OPENGL_TO_OPENCV
206
+ c2w_o = np.asarray(ref["transform_matrix"]) @ OPENGL_TO_OPENCV
207
+ pn, pp = plane_of(by_fn[fn], origin)
208
+ lab = seg(photo)
209
+ out, a = fix_image(art, photo, lab, (seg.movers, seg.ground), c2w_v, c2w_o, Kv, Ko_s, pn, pp,
210
+ origin[:2], tree, cfg)
211
+ dst = out_dir / pm["cam"] / Path(fn).name
212
+ dst.parent.mkdir(parents=True, exist_ok=True)
213
+ Image.fromarray(out).save(dst, quality=95)
214
+ if debug_dir is not None:
215
+ debug_dir.mkdir(parents=True, exist_ok=True)
216
+ Image.fromarray((a * 255).astype(np.uint8)).save(debug_dir / (Path(fn).stem + "_alpha.png"))
217
+ cnt += 1
218
+ _ = Ko
219
+ return cnt
src/nusim/nusim/aug/map_aug.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """๋งต ์ „์ฒด ์‹œ์  ๋ณ€ํ˜• ์ฆ๊ฐ• โ€” ์ผ๊ด„ ์žฌ๊ตฌ์„ฑ(nusim recon batch)ํ•œ ๋‚ฎ ์”ฌ๋“ค์„ ๋ฆฌ๊ทธ ๋ณ€ํ˜•์œผ๋กœ ๋‹ค์‹œ ์ฐ๋Š”๋‹ค.
2
+
3
+ nusim aug rig ์€ ์”ฌ ํ•˜๋‚˜๋ฅผ ๋ชจ๋ธ ํ•˜๋‚˜๋กœ ๊ทธ๋ฆฐ๋‹ค. ๋งต ์ผ๊ด„ ์žฌ๊ตฌ์„ฑ์—์„œ๋Š” ๊ธด ์”ฌ์ด ๊ตฌ๊ฐ„(seg<k>)์œผ๋กœ ๋‚˜๋‰˜์–ด ์žˆ์œผ๋ฏ€๋กœ
4
+ (nusim.recon.batch.plan_segments), CAM_FRONT ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๊ทธ ์‹œ๊ฐ์„ '๋งก๋Š”' ๊ตฌ๊ฐ„์˜ ๋ชจ๋ธ๋กœ ๊ทธ๋ฆฐ๋‹ค. ๋งก๋Š” ๊ตฌ๊ฐ„์€ ์ด์›ƒ๊ณผ
5
+ ๊ฒน์น˜์ง€ ์•Š๋Š”๋‹ค (๊ฒน์นจ 10 m ์˜ ๊ฐ€์šด๋ฐ์—์„œ ์ž๋ฆ„) โ€” ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์ •ํ™•ํžˆ ํ•œ ๋ชจ๋ธ์ด๊ณ , ๊ฐ ๋ชจ๋ธ์€ ์ž๊ธฐ ํ•™์Šต ๊ตฌ๊ฐ„ ์•ˆ์ชฝ๋งŒ ๊ทธ๋ฆฐ๋‹ค.
6
+
7
+ ๊ฒฐ๊ณผ: <out>/<scene>/<variant>/<CAM>/<์›๋ณธ ํŒŒ์ผ๋ช…>.jpg + meta.json (nusim aug rig ๊ณผ ๊ฐ™์€ ํ˜•์‹ + segmentsยทkeyframes_only).
8
+ ๊ฐ™์€ ๋ชจ๋ธยท์„ค์ •์œผ๋กœ ๋๋‚œ ๋ณ€ํ˜•์€ ๊ฑด๋„ˆ๋›ด๋‹ค (meta.json ์˜ model ๋ชฉ๋ก ๋น„๊ต) โ€” ์ผ๊ด„ ํ•™์Šต์„ ๋”ฐ๋ผ๊ฐ€๋ฉฐ ์—ฌ๋Ÿฌ ๋ฒˆ ๋ถˆ๋Ÿฌ๋„ ๋œ๋‹ค.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ import shutil
16
+ from dataclasses import replace
17
+ from pathlib import Path
18
+
19
+ LOG = logging.getLogger(__name__)
20
+
21
+
22
+ def assign_front_frames(scene, segs: list[dict]) -> list[list]:
23
+ """๊ตฌ๊ฐ„๋งˆ๋‹ค ๋งก๋Š” CAM_FRONT ํ”„๋ ˆ์ž„ โ€” ๋ฐ˜์—ด๋ฆฐ ๊ตฌ๊ฐ„ [own0, own1) (๋งˆ์ง€๋ง‰์€ ๋๊นŒ์ง€), ์”ฌ ์‹œ์ž‘ ๊ธฐ์ค€ ๋งˆ์ดํฌ๋กœ์ดˆ๋กœ ๋น„๊ต."""
24
+ start = min(f.timestamp for fs in scene.frames.values() for f in fs[:1])
25
+ front = scene.frames["CAM_FRONT"]
26
+ out = []
27
+ for i, s in enumerate(segs):
28
+ lo = -1 if s.get("own0") is None or i == 0 else start + round(s["own0"] * 1e6)
29
+ hi = None if s.get("own1") is None or i == len(segs) - 1 else start + round(s["own1"] * 1e6)
30
+ out.append([f for f in front if f.timestamp >= lo and (hi is None or f.timestamp < hi)])
31
+ return out
32
+
33
+
34
+ def _stamp(d: Path) -> str:
35
+ p = d / "point_cloud.ply"
36
+ return f"{d}@{int(p.stat().st_mtime)}"
37
+
38
+
39
+ def ensure_ibl(scene_name: str, segs: list[tuple[dict, Path]], log=LOG.info) -> None:
40
+ """glTF ์ฐจ๋Ÿ‰์„ ๋‹ค์‹œ ๋„ฃ์œผ๋ ค๋ฉด ์”ฌ์˜ IBL(ํ™˜๊ฒฝ ์กฐ๋ช…)์ด ์žˆ์–ด์•ผ ํ•œ๋‹ค (_ibl_gltf_args). ์—†์œผ๋ฉด ๊ฐ€์šด๋ฐ ํ”„๋ ˆ์ž„์„ ๋งก๋Š” ๊ตฌ๊ฐ„ ๋ชจ๋ธ๋กœ ๊ตฝ๋Š”๋‹ค."""
41
+ from nusim.data.index import load_scene
42
+ from nusim.paths import output_dir
43
+ from nusim.render.ibl import bake_ibl
44
+
45
+ out = output_dir() / "ibl" / scene_name
46
+ if (out / "env_specular.ktx2").exists():
47
+ return
48
+ scene = load_scene(scene_name)
49
+ mid = len(scene.frames["CAM_FRONT"]) // 2
50
+ owned = assign_front_frames(scene, [s for s, _ in segs])
51
+ tmid = scene.frames["CAM_FRONT"][mid].timestamp
52
+ d = next((d for (s, d), fr in zip(segs, owned) if any(f.timestamp == tmid for f in fr)), segs[0][1])
53
+ # ๊ตฌ๊ฐ„ ๋ชจ๋ธ์˜ ์ƒ‰ ๋ณด์ •์€ ๊ทธ ํ•™์Šต ๊ตฌ๊ฐ„์˜ ๋ทฐ ์ˆ˜๋งŒํผ์ด๋‹ค โ€” ๊ฐ™์€ ๊ตฌ๊ฐ„์œผ๋กœ ์ž๋ฅธ ์”ฌ์„ ๋„˜๊ธด๋‹ค
54
+ from nusim.cli.main import _model_data
55
+
56
+ sub, _ = _model_data(scene_name, d)
57
+ front = sub.frames["CAM_FRONT"]
58
+ k = min(range(len(front)), key=lambda i: abs(front[i].timestamp - tmid))
59
+ log(f" IBL ๊ตฝ๊ธฐ (๋ชจ๋ธ {d})")
60
+ bake_ibl(sub, d, out, frame=k)
61
+
62
+
63
+ def aug_scene(scene_name: str, root: Path, variants: list[dict], out_root: Path, viewer: Path,
64
+ keyframes: bool = True, agents: bool = True, log=LOG.info) -> dict:
65
+ from nusim.aug.rig_aug import keyframes_only, render_frames, variant_meta
66
+ from nusim.cli.main import _ibl_gltf_args
67
+ from nusim.data.index import load_scene
68
+ from nusim.recon.batch import segment_dirs
69
+
70
+ segs = segment_dirs(root, scene_name)
71
+ if not segs:
72
+ raise FileNotFoundError(f"{root / scene_name} ์— ํ•™์Šต๋œ ๋ชจ๋ธ์ด ์—†๋‹ค")
73
+ if len(segs) != segs[0][0]["n"]:
74
+ raise RuntimeError(f"{scene_name}: ๊ตฌ๊ฐ„ {segs[0][0]['n']}๊ฐœ ์ค‘ {len(segs)}๊ฐœ๋งŒ ํ•™์Šต๋จ")
75
+ if agents:
76
+ ensure_ibl(scene_name, segs, log)
77
+ extra = _ibl_gltf_args(scene_name, agents=True) if agents else []
78
+ scene = load_scene(scene_name)
79
+ owned = assign_front_frames(scene, [s for s, _ in segs])
80
+ base = keyframes_only(scene) if keyframes else scene
81
+ models = [_stamp(d) for _, d in segs]
82
+ summary = {}
83
+ for v in variants:
84
+ out = out_root / scene_name / v["name"]
85
+ meta_path = out / "meta.json"
86
+ if meta_path.exists():
87
+ old = json.loads(meta_path.read_text())
88
+ if old.get("model") == models and old.get("keyframes_only") == keyframes and old.get("variant") == v \
89
+ and old.get("agents") == agents:
90
+ summary[v["name"]] = len(old["samples"])
91
+ continue
92
+ if out.exists():
93
+ shutil.rmtree(out)
94
+ samples, seg_meta, offset = [], [], 0
95
+ for (s, d), fr in zip(segs, owned):
96
+ keep = {f.timestamp for f in fr}
97
+ front = [f for f in base.frames["CAM_FRONT"] if f.timestamp in keep]
98
+ if not front:
99
+ continue
100
+ sub = replace(base, frames={**base.frames, "CAM_FRONT": front})
101
+ got = render_frames(sub, d, v, out, viewer, extra)
102
+ (out / "rig.txt").rename(out / f"rig_seg{s['k']}.txt")
103
+ for x in got:
104
+ x["frame"] += offset
105
+ x["segment"] = s["k"]
106
+ offset += len(front)
107
+ samples += got
108
+ seg_meta.append({"k": s["k"], "own0": s.get("own0"), "own1": s.get("own1"), "model": str(d),
109
+ "front_frames": len(front), "images": len(got)})
110
+ meta = variant_meta(scene, v, models, samples)
111
+ meta.update(keyframes_only=keyframes, agents=agents, segments=seg_meta)
112
+ meta_path.write_text(json.dumps(meta, indent=1, ensure_ascii=False))
113
+ summary[v["name"]] = len(samples)
114
+ log(f" {v['name']:<14} {len(samples):,}์žฅ (๊ตฌ๊ฐ„ {len(seg_meta)}๊ฐœ) โ†’ {out}")
115
+ return summary
src/nusim/nusim/aug/rig_aug.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """์นด๋ฉ”๋ผ ๋ฆฌ๊ทธ ๋ณ€ํ˜• ์ฆ๊ฐ• โ€” ์žฌ๊ตฌ์„ฑํ•œ ๋‚ฎ ์žฅ๋ฉด์„ 6์บ  ์‹œ์ ์„ ๋ฐ”๊ฟ” ๋‹ค์‹œ ์ฐ๋Š”๋‹ค.
2
+
3
+ ๊ฐ™์€ ์ฃผํ–‰(๊ฐ™์€ ์‹œ๊ฐยท๊ฐ™์€ ์ž์ฐจ ๊ถค์ )์„ ์นด๋ฉ”๋ผ ์žฅ์ฐฉ ์œ„์น˜ยท์ž์„ธ๋งŒ ๋ฐ”๊ฟ” ๋ Œ๋”ํ•œ๋‹ค: ์˜† ์ฐจ์„  ์ชฝ์œผ๋กœ ์ด๋™, ๋” ๋†’์€/๋‚ฎ์€
4
+ ์ฐจ, ์•„๋ž˜๋กœ ์ˆ™์ž„, ์ขŒ์šฐ๋กœ ๋Œ๋ฆผ (configs/aug/rig_variants.json). 6์บ  ์ „์ฒด์— ๊ฐ™์€ ๋ณ€ํ˜•์„ ์ฃผ๋ฏ€๋กœ ๋ฆฌ๊ทธ ๊ธฐํ•˜๊ฐ€ ์ผ๊ด€๋œ๋‹ค.
5
+
6
+ ๋ฐฉ๋ฒ•: ์”ฌ ์ธ๋ฑ์Šค์˜ ์นด๋ฉ”๋ผ ์บ˜๋ฆฌ๋ธŒ๋ ˆ์ด์…˜(cam โ†’ ego)๋งŒ ๋ฐ”๊พผ ์‚ฌ๋ณธ์œผ๋กœ ๋ทฐ์–ด ๋ฆฌ๊ทธ(rig.txt)๋ฅผ ๋งŒ๋“ค๊ณ 
7
+ (nusim.render.export.write_rig โ€” ํ•™์Šต์ด ๋ณด์ •ํ•œ ์™ธ๋ถ€ ํŒŒ๋ผ๋ฏธํ„ฐ๋„ ๊ทธ๋Œ€๋กœ ์ ์šฉ), vkgs_viewer --offscreen ์œผ๋กœ ๋ Œ๋”ํ•œ๋‹ค.
8
+ ๊ฒฐ๊ณผ: outputs/aug/rig/<scene>/<variant>/
9
+ <CAM>/<์›๋ณธ ํŒŒ์ผ๋ช…>.jpg ์›๋ณธ๊ณผ ๊ฐ™์€ ์ด๋ฆ„ โ€” CAM_FRONT ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๊ฐ ์นด๋ฉ”๋ผ์˜ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์žฅ (rig.txt ์™€ ๊ฐ™์€ ๋Œ€์‘).
10
+ ์›๋ณธ sweep ์ „๋ถ€๋Š” ์•„๋‹ˆ๋‹ค (์นด๋ฉ”๋ผ๋งˆ๋‹ค ์•ฝ 12 Hz ์ค‘ CAM_FRONT ์‹œ๊ฐ์— ๊ฐ€๊นŒ์šด ๊ฒƒ๋งŒ)
11
+ meta.json ๋ณ€ํ˜•, ๋ฐ”๋€ calibrated_sensor (cam โ†’ ego, nuScenes ํ˜•์‹), ์žฅ๋งˆ๋‹ค ์›๋ณธ ํŒŒ์ผยท์‹œ๊ฐยท์ž์ฐจ ํฌ์ฆˆ
12
+ rig.txt ๋ Œ๋”์— ์“ด ๋ฆฌ๊ทธ
13
+ ์›๋ณธ ๋ฐ์ดํ„ฐ์…‹์—๋Š” ์“ฐ์ง€ ์•Š๋Š”๋‹ค.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import logging
20
+ import shutil
21
+ import subprocess
22
+ from dataclasses import replace
23
+ from pathlib import Path
24
+
25
+ import numpy as np
26
+
27
+ from nusim.data.rig import matrix_to_quat, quat_to_matrix
28
+
29
+ LOG = logging.getLogger(__name__)
30
+ RENDER_ATTEMPTS = 3 # ๊นจ์ง„ PNG ๊ฐ€ ๋‚˜์˜ค๋ฉด ๊ตฌ๊ฐ„์„ ๋‹ค์‹œ ๋ Œ๋”ํ•˜๋Š” ํšŸ์ˆ˜ (render_frames)
31
+
32
+
33
+ def _broken_pngs(d: Path) -> list[Path]:
34
+ """d ์˜ PNG ์ค‘ ๋๊นŒ์ง€ ๋””์ฝ”๋”ฉ๋˜์ง€ ์•Š๋Š” ๊ฒƒ (zlib ๊ฒ€์‚ฌํ•ฉ ํฌํ•จ)."""
35
+ from PIL import Image
36
+
37
+ bad = []
38
+ for p in sorted(d.glob("*.png")):
39
+ try:
40
+ with Image.open(p) as im:
41
+ im.load()
42
+ except OSError:
43
+ bad.append(p)
44
+ return bad
45
+
46
+
47
+ def _rx(a: float) -> np.ndarray:
48
+ c, s = np.cos(a), np.sin(a)
49
+ return np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
50
+
51
+
52
+ def _rz(a: float) -> np.ndarray:
53
+ c, s = np.cos(a), np.sin(a)
54
+ return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
55
+
56
+
57
+ def transform_calib(calib, shift_m=(0, 0, 0), rot_deg=(0, 0, 0)):
58
+ """CameraCalib (cam โ†’ ego) ์— ๋ฆฌ๊ทธ ๋ณ€ํ˜•์„ ์ค€๋‹ค.
59
+ shift_m: ego ์ขŒํ‘œ [x ์•ž, y ์™ผ์ชฝ, z ์œ„] ์ด๋™. rot_deg: [roll, pitch, yaw] โ€”
60
+ pitch + = ์นด๋ฉ”๋ผ๊ฐ€ ์•„๋ž˜๋กœ ์ˆ™์ธ๋‹ค (์นด๋ฉ”๋ผ ์ž์‹ ์˜ ๊ฐ€๋กœ์ถ•), yaw + = ์™ผ์ชฝ์œผ๋กœ (ego z ์ถ•, ๋ฆฌ๊ทธ ์ „์ฒด๊ฐ€ ๊ฐ™์ด),
61
+ roll = ์นด๋ฉ”๋ผ ๊ด‘์ถ• ๋‘˜๋ ˆ. ์นด๋ฉ”๋ผ ์ขŒํ‘œ๋Š” OpenCV (x ์˜ค๋ฅธ์ชฝ, y ์•„๋ž˜, z ์•ž) โ€” rig.py ์™€ ๊ฐ™๋‹ค."""
62
+ roll, pitch, yaw = np.radians(np.asarray(rot_deg, dtype=np.float64))
63
+ R = quat_to_matrix(calib.rotation)
64
+ # ์นด๋ฉ”๋ผ x ์ถ• ๋‘˜๋ ˆ +ฮธ ๋Š” ๊ด‘์ถ•(z)์„ ์œ„(-y)๋กœ ๋“ ๋‹ค โ†’ ์ˆ™์ด๋ ค๋ฉด -pitch
65
+ R2 = _rz(yaw) @ R @ _rx(-pitch) @ _rz(roll)
66
+ t2 = np.asarray(calib.translation, dtype=np.float64) + np.asarray(shift_m, dtype=np.float64)
67
+ return replace(calib, rotation=matrix_to_quat(R2), translation=tuple(float(x) for x in t2))
68
+
69
+
70
+ def load_variants(path: Path, names: list[str] | None = None) -> list[dict]:
71
+ vs = [v for v in json.loads(path.read_text())["variants"]]
72
+ if names:
73
+ missing = set(names) - {v["name"] for v in vs}
74
+ if missing:
75
+ raise ValueError(f"๋ณ€ํ˜•์ด ์—†์Œ: {sorted(missing)} (์žˆ๋Š” ๊ฒƒ: {[v['name'] for v in vs]})")
76
+ vs = [v for v in vs if v["name"] in names]
77
+ return vs
78
+
79
+
80
+ def keyframes_only(scene):
81
+ """ํ‚คํ”„๋ ˆ์ž„(2 Hz sample)๋งŒ ๋‚จ๊ธด ์‚ฌ๋ณธ. rig_frames ๋Š” CAM_FRONT ์‹œ๊ฐ์— ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์žฅ์„ ๊ณ ๋ฅด๋ฏ€๋กœ, ๋ชจ๋“  ์นด๋ฉ”๋ผ๋ฅผ
82
+ ํ‚คํ”„๋ ˆ์ž„์œผ๋กœ ์ค„์ด๋ฉด ๊ฐ™์€ sample ์˜ 6์žฅ์ด ์ง์ง€์–ด์ง„๋‹ค."""
83
+ return replace(scene, frames={c: [f for f in fs if f.is_keyframe] for c, fs in scene.frames.items()})
84
+
85
+
86
+ def render_frames(scene, model_dir: Path, variant: dict, out: Path, viewer: Path,
87
+ extra_args: list[str] | None = None, jpeg_quality: int = 95) -> list[dict]:
88
+ """scene(๊ตฌ๊ฐ„ยทํ‚คํ”„๋ ˆ์ž„์œผ๋กœ ์ค„์ธ ์‚ฌ๋ณธ์ด์–ด๋„ ๋จ)์˜ CAM_FRONT ํ”„๋ ˆ์ž„๋งˆ๋‹ค 6์บ ์„ ๋ฆฌ๊ทธ ๋ณ€ํ˜•์œผ๋กœ ๋ Œ๋”ํ•ด
89
+ out/<CAM>/<์›๋ณธ ํŒŒ์ผ๋ช…>.jpg ๋กœ ์“ด๋‹ค. ์žฅ๋งˆ๋‹ค ๋ฉ”ํƒ€ ๋ชฉ๋ก์„ ๋Œ๋ ค์ค€๋‹ค (frame ์€ ์ด ํ˜ธ์ถœ ์•ˆ์˜ ๋ฒˆํ˜ธ)."""
90
+ from PIL import Image
91
+
92
+ from nusim.cli.main import _model_objects
93
+ from nusim.io.ply import read_origin
94
+ from nusim.render.export import load_camera_adjust, rig_frames, write_rig
95
+
96
+ vscene = replace(scene, calib={cam: transform_calib(c, variant.get("shift_m", (0, 0, 0)), variant.get("rot_deg", (0, 0, 0)))
97
+ for cam, c in scene.calib.items()})
98
+ out.mkdir(parents=True, exist_ok=True)
99
+ origin = read_origin(model_dir / "point_cloud.ply")
100
+ write_rig(out / "rig.txt", vscene, origin, load_camera_adjust(model_dir), objects=_model_objects(model_dir))
101
+ tmp = out / "_png"
102
+ # ๋ฐ์ดํ„ฐ์…‹์šฉ: ํ™”๋ฉด ๊ณต๊ฐ„ AA ์—†์Œ โ€” ์‹ค์ œ ์นด๋ฉ”๋ผ ์˜์ƒ์ฒ˜๋Ÿผ. ํ•˜๋Š˜: ๋‚ ์”จ๊ฐ€ ์—†์œผ๋ฉด ํ•™์Šต๊ธฐ์™€ ๊ฐ™์€ ํ•˜๋Š˜(ํ•˜๋Š˜ ๊ฐ€์šฐ์‹œ์•ˆ ์œ ์ง€,
103
+ # ํ•˜๋Š˜ ํ๋ธŒ๋งต์„ ํ•™์Šตํ•œ ๋ชจ๋ธ์ด๋ฉด ๊ทธ ํ๋ธŒ๋งต), ๋‚ ์”จ ๋ณ€ํ˜•์ด๋ฉด ๋‚ฎ ํ•˜๋Š˜ ๊ฐ€์šฐ์‹œ์•ˆ์„ ๋นผ๊ณ  ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ•˜๋Š˜
104
+ cmd = [str(viewer), str(model_dir / "point_cloud.ply"), "--cams", str(out / "rig.txt"), "--offscreen",
105
+ "--out", str(tmp), "--pass", "fxaa=off"]
106
+ if (model_dir / "objects" / "objects.json").exists(): # ์ฐจ๋Ÿ‰์„ ํ•จ๊ป˜ ํ•™์Šตํ•œ ๋ชจ๋ธ โ€” ์ฐจ๋Ÿ‰ ์—์…‹๋„ ๊ฐ™์€ ์‹œ์ ์—์„œ
107
+ cmd += ["--objects", str(model_dir / "objects")]
108
+ if variant.get("weather"):
109
+ cmd += ["--weather", variant["weather"]]
110
+ else:
111
+ cmd += ["--keep-sky-gaussians"]
112
+ if (model_dir / "sky.ktx2").exists():
113
+ cmd += ["--sky", str(model_dir / "sky.ktx2")]
114
+ cmd += extra_args or []
115
+ # ๋ Œ๋”๋Š” ๊ฒฐ์ •์ ์ธ๋ฐ PNG ๊ฐ€ ๊ฐ€๋” ๊นจ์ง„๋‹ค โ€” ๋‹ค์‹œ ๋ Œ๋”ํ•œ ๊ฒƒ๊ณผ 1๋น„ํŠธ ์ฐจ์ด (2026-10-12, scene-0017ยท0036: ECC ์—†๋Š” ๋ฉ”๋ชจ๋ฆฌ์˜
116
+ # ๋น„ํŠธ ๋’ค์ง‘ํž˜์œผ๋กœ ๋ณธ๋‹ค). zlib ๊ฒ€์‚ฌํ•ฉ์ด ์žก์œผ๋ฏ€๋กœ PNG ๋ฅผ ๋‹ค ์—ด์–ด ๋ณด๊ณ , ๊นจ์กŒ์œผ๋ฉด ๊ทธ ๊ตฌ๊ฐ„์„ ํ•œ ๋ฒˆ ๋” ๋ Œ๋”ํ•œ๋‹ค
117
+ for attempt in range(RENDER_ATTEMPTS):
118
+ if tmp.exists():
119
+ shutil.rmtree(tmp)
120
+ r = subprocess.run(cmd, capture_output=True, text=True)
121
+ if r.returncode != 0:
122
+ raise RuntimeError(f"vkgs_viewer ์‹คํŒจ:\n{r.stdout[-2000:]}\n{r.stderr[-2000:]}")
123
+ bad = _broken_pngs(tmp)
124
+ if not bad:
125
+ break
126
+ LOG.warning(f" ๊นจ์ง„ PNG {len(bad)}์žฅ ({bad[0].name}) โ€” ๋‹ค์‹œ ๋ Œ๋” ({attempt + 2}/{RENDER_ATTEMPTS})")
127
+ else:
128
+ raise RuntimeError(f"PNG ๊ฐ€ {RENDER_ATTEMPTS}๋ฒˆ ๋‹ค ๊นจ์ง: {bad[:3]}")
129
+ samples, done = [], set()
130
+ for k, cam, fr in rig_frames(scene):
131
+ src = tmp / f"f{k:04d}_{cam}.png"
132
+ # rig ๋Š” CAM_FRONT ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๊ฐ ์นด๋ฉ”๋ผ์˜ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์žฅ์„ ๊ณ ๋ฅด๋ฏ€๋กœ ๊ฐ™์€ ์žฅ์ด ๋‘ ๋ฒˆ ๋ฝ‘ํž ์ˆ˜ ์žˆ๋‹ค โ€” ํ•œ ๋ฒˆ๋งŒ
133
+ if not src.exists() or fr.filename in done:
134
+ continue
135
+ done.add(fr.filename)
136
+ dst = out / cam / Path(fr.filename).name
137
+ dst.parent.mkdir(parents=True, exist_ok=True)
138
+ Image.open(src).convert("RGB").save(dst, quality=jpeg_quality)
139
+ samples.append({"frame": k, "cam": cam, "timestamp": fr.timestamp, "is_keyframe": fr.is_keyframe,
140
+ "original_filename": fr.filename, "filename": str(dst.relative_to(out)),
141
+ "ego_translation": list(fr.ego_translation), "ego_rotation": list(fr.ego_rotation)})
142
+ shutil.rmtree(tmp)
143
+ return samples
144
+
145
+
146
+ def variant_meta(scene, variant: dict, models, samples: list[dict]) -> dict:
147
+ vscene_calib = {cam: transform_calib(c, variant.get("shift_m", (0, 0, 0)), variant.get("rot_deg", (0, 0, 0)))
148
+ for cam, c in scene.calib.items()}
149
+ return {
150
+ "scene": scene.name, "variant": variant, "model": models,
151
+ "calibrated_sensor": {cam: {"translation": list(c.translation), "rotation": list(c.rotation),
152
+ "camera_intrinsic": [list(r) for r in c.intrinsic]}
153
+ for cam, c in vscene_calib.items()},
154
+ "note": "calibrated_sensor = ๋ฐ”๋€ cam โ†’ ego (nuScenes ํ˜•์‹, ํ•™์Šต ๋ณด์ • ์ „). ์ž์ฐจ ํฌ์ฆˆ๋Š” ์›๋ณธ ๊ทธ๋Œ€๋กœ.",
155
+ "samples": samples,
156
+ }
157
+
158
+
159
+ def render_variant(scene_name: str, model_dir: Path, variant: dict, out_root: Path, viewer: Path,
160
+ extra_args: list[str] | None = None, jpeg_quality: int = 95, log=LOG.info) -> dict:
161
+ """์”ฌ ์ „์ฒด๋ฅผ ๋ชจ๋ธ ํ•˜๋‚˜๋กœ (nusim aug rig). ๊ตฌ๊ฐ„ ๋ชจ๋ธ๋กœ ๋‚˜๋ˆˆ ์”ฌ์€ nusim.aug.map_aug."""
162
+ from nusim.data.index import load_scene
163
+
164
+ scene = load_scene(scene_name)
165
+ out = out_root / scene_name / variant["name"]
166
+ samples = render_frames(scene, model_dir, variant, out, viewer, extra_args, jpeg_quality)
167
+ meta = variant_meta(scene, variant, str(model_dir), samples)
168
+ (out / "meta.json").write_text(json.dumps(meta, indent=1, ensure_ascii=False))
169
+ log(f" {variant['name']:<20} {len(samples):,}์žฅ โ†’ {out}")
170
+ return meta
src/nusim/nusim/aug/translate.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """๋ฐคยท๋น„ โ†’ ๋‚ฎ ์˜์ƒ ๋ณ€ํ™˜ โ€” ๋‚ฎ ์”ฌ๋งŒ์œผ๋กœ๋Š” ๋ชป ๋ฎ๋Š” ์ง€์—ญ(์‹ฑ๊ฐ€ํฌ๋ฅด ์•ผ๊ฐ„ยท์šฐ์ฒœ ์”ฌ)์„ ์žฌ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ.
2
+
3
+ ๋ชจ๋ธ: CycleGAN-Turbo (img2img-turbo, Parmar et al. 2024, MIT ์ฝ”๋“œ / SD-Turbo ๊ฐ€์ค‘์น˜ ์—ฐ๊ตฌ์šฉ) ์˜ BDD100K ์ฃผํ–‰ ์˜์ƒ
4
+ ์‚ฌ์ „ํ•™์Šต ์ฒดํฌํฌ์ธํŠธ night_to_day, rainy_to_clear. third_party/img2img-turbo, ์ฒดํฌํฌ์ธํŠธ๋Š” cache/models/img2img-turbo
5
+ (cache/models/img2img-turbo/fetch.sh ๋กœ ๋ณ‘๋ ฌ ๋‹ค์šด๋กœ๋“œ โ€” ๊ฐœ๋‹น 1.7 GB).
6
+
7
+ ๊ฒฐ๊ณผ๋Š” ์›๋ณธ๊ณผ ๊ฐ™์€ ์ƒ๋Œ€ ๊ฒฝ๋กœ๋กœ outputs/aug/translate/<model>/samples|sweeps/... ์— ์“ด๋‹ค. ์žฌ๊ตฌ์„ฑ์€
8
+ `nusim recon train <scene> --image-root outputs/aug/translate/<model>` โ€” ์ด๋ฏธ์ง€๋งŒ ๋ฐ”๋€Œ๊ณ  ๋ผ์ด๋‹คยทํฌ์ฆˆ๋Š” ์›๋ณธ์ด๋‹ค.
9
+ ์›๋ณธ ๋ฐ์ดํ„ฐ์…‹์—๋Š” ์“ฐ์ง€ ์•Š๋Š”๋‹ค.
10
+
11
+ ์•Œ๊ณ  ์“ฐ์ž: ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๋”ฐ๋กœ ๋ณ€ํ™˜ํ•˜๋ฏ€๋กœ ์‹œ๊ฐ„ ๊นœ๋นก์ž„ยท์นด๋ฉ”๋ผ ์‚ฌ์ด ๋ถˆ์ผ์น˜๊ฐ€ ์žˆ๋‹ค. 3DGS ๋Š” ์—ฌ๋Ÿฌ ๋ทฐ์˜ ํ‰๊ท ์„ ๋ฐฐ์šฐ๋ฏ€๋กœ
12
+ ์กฐ๊ธˆ์€ ๊ฒฌ๋””์ง€๋งŒ, ๊ฒฐ๊ณผ๋Š” ์žฌ๊ตฌ์„ฑ PSNRยท์œก์•ˆ์œผ๋กœ ํ™•์ธํ•œ๋‹ค.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import sys
19
+ import time
20
+ from pathlib import Path
21
+
22
+ import numpy as np
23
+
24
+ LOG = logging.getLogger(__name__)
25
+ ROOT = Path(__file__).resolve().parents[4]
26
+ CKPT_DIR = ROOT / "cache" / "models" / "img2img-turbo"
27
+ MODELS = {"night_to_day": "night2day.pkl", "rainy_to_clear": "rainy2clear.pkl"}
28
+
29
+
30
+ def load_model(name: str):
31
+ import torch
32
+
33
+ if name not in MODELS:
34
+ raise ValueError(f"๋ชจ๋ฅด๋Š” ๋ณ€ํ™˜ '{name}' (์žˆ๋Š” ๊ฒƒ: {list(MODELS)})")
35
+ if not (CKPT_DIR / MODELS[name]).exists():
36
+ raise FileNotFoundError(f"{CKPT_DIR / MODELS[name]} ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค: bash {CKPT_DIR / 'fetch.sh'} {MODELS[name][:-4]}")
37
+ src = str(ROOT / "third_party" / "img2img-turbo" / "src")
38
+ if src not in sys.path:
39
+ sys.path.append(src) # nusim ๋ณด๋‹ค ๋’ค โ€” ๊ทธ์ชฝ ๋ชจ๋“ˆ ์ด๋ฆ„๊ณผ ๊ฒน์น˜์ง€ ์•Š๊ฒŒ
40
+ from cyclegan_turbo import CycleGAN_Turbo
41
+
42
+ # ์›๋ž˜ ์ฝ”๋“œ๋Š” fp32 ๋กœ GPU ์— ์˜ฌ๋ฆฐ ๋’ค ์ค„์ธ๋‹ค (4.4 GB) โ€” 8 GB GPU ์—์„œ ํ•™์Šต๊ณผ ๊ฐ™์ด ๋ชป ๋ˆ๋‹ค.
43
+ # ์ƒ์„ฑ ์ค‘ Module.cuda ๋ฅผ ์ž ์‹œ ๋ง‰์•„ CPU ์—์„œ ๋งŒ๋“ค๊ณ , fp16 ์œผ๋กœ ๋ฐ”๊พผ ๋’ค ์˜ฌ๋ฆฐ๋‹ค (์•ฝ 2 GB).
44
+ # ์ฒดํฌํฌ์ธํŠธ(1.7 GB fp32)๋„ GPU ๋กœ ์ €์žฅ๋ผ ์žˆ์–ด torch.load ๊ฐ€ ๋ฐ”๋กœ ์˜ฌ๋ฆฐ๋‹ค โ†’ CPU ๋กœ ์ฝ๊ฒŒ.
45
+ real_cuda, real_load = torch.nn.Module.cuda, torch.load
46
+ torch.nn.Module.cuda = lambda self, *a, **k: self
47
+ torch.load = lambda f, *a, **k: real_load(f, *a, **{**k, "map_location": "cpu", "weights_only": False})
48
+ try:
49
+ m = CycleGAN_Turbo(pretrained_name=name, ckpt_folder=str(CKPT_DIR))
50
+ finally:
51
+ torch.nn.Module.cuda, torch.load = real_cuda, real_load
52
+ m = m.eval().half().cuda()
53
+ m.requires_grad_(False)
54
+ return m
55
+
56
+
57
+ def translate_image(model, img, size: tuple[int, int]):
58
+ """PIL ์ด๋ฏธ์ง€ โ†’ ๋ณ€ํ™˜ํ•œ PIL ์ด๋ฏธ์ง€ (์›๋ž˜ ํฌ๊ธฐ๋กœ). size = ๋ชจ๋ธ ์ž…๋ ฅ (8์˜ ๋ฐฐ์ˆ˜)."""
59
+ import torch
60
+ from PIL import Image
61
+ from torchvision import transforms
62
+
63
+ x = transforms.ToTensor()(img.resize(size, Image.LANCZOS))
64
+ x = (x * 2 - 1).unsqueeze(0).cuda().half()
65
+ with torch.no_grad(): # ์ „์—ญ grad ์„ค์ •์€ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค (๊ฐ™์€ ํ”„๋กœ์„ธ์Šค์˜ ํ•™์Šต์„ ๋ง์น˜์ง€ ์•Š๊ฒŒ)
66
+ y = model(x)
67
+ out = ((y[0].float().clamp(-1, 1) + 1) * 127.5).round().byte().permute(1, 2, 0).cpu().numpy()
68
+ return Image.fromarray(out).resize(img.size, Image.LANCZOS)
69
+
70
+
71
+ def translate_scene(scene_name: str, models: list[str], out_root: Path, size=(912, 512), cameras=None,
72
+ stride: int = 1, keyframes_only: bool = False, quality: int = 95, log=LOG.info) -> dict:
73
+ """์”ฌ์˜ ์นด๋ฉ”๋ผ ์˜์ƒ์„ models ์ˆœ์„œ๋Œ€๋กœ ๋ณ€ํ™˜ (์˜ˆ: rainy_to_clear ๋‹ค์Œ night_to_day). ์ด๋ฏธ ์žˆ๋Š” ํŒŒ์ผ์€ ๊ฑด๋„ˆ๋›ด๋‹ค."""
74
+ from PIL import Image
75
+
76
+ from nusim import CAMERAS
77
+ from nusim.data.index import load_scene
78
+ from nusim.paths import dataset
79
+
80
+ scene, ds = load_scene(scene_name), dataset()
81
+ cams = cameras or CAMERAS
82
+ frames = []
83
+ for cam in cams:
84
+ fr = scene.keyframes(cam) if keyframes_only else scene.frames[cam]
85
+ frames += fr[::stride]
86
+ tag = "+".join(models)
87
+ out = out_root / tag
88
+ todo = [f for f in frames if not (out / f.filename).exists()]
89
+ log(f" {scene_name}: {len(frames):,}์žฅ ์ค‘ {len(todo):,}์žฅ ๋ณ€ํ™˜ ({tag}, ์ž…๋ ฅ {size[0]}x{size[1]}) โ†’ {out}")
90
+ if not todo:
91
+ return {"n": len(frames), "done": 0, "out": str(out)}
92
+ # ๋ชจ๋ธ์€ ํ•˜๋‚˜์”ฉ๋งŒ ์˜ฌ๋ฆฐ๋‹ค (fp16 ํ•˜๋‚˜์— ์•ฝ 2.2 GB โ€” 8 GB GPU ์—์„œ ํ•™์Šต๊ณผ ๊ฐ™์ด ๋Œ๊ฒŒ). ์—ฐ์‡„ ๋ณ€ํ™˜์€ ๋‹จ๊ณ„๋ณ„๋กœ:
93
+ # ์•ž ๋‹จ๊ณ„ ๊ฒฐ๊ณผ๋ฅผ out/_stage<i>/ ์— ๋‘๊ณ  ๋‹ค์Œ ๋‹จ๊ณ„๊ฐ€ ์ฝ๋Š”๋‹ค.
94
+ import shutil
95
+
96
+ import torch
97
+
98
+ t0 = time.time()
99
+ src_root = ds.root
100
+ for si, name in enumerate(models):
101
+ last = si == len(models) - 1
102
+ dst_root = out if last else out / f"_stage{si}_{scene_name}" # ์”ฌ๋งˆ๋‹ค ๋”ฐ๋กœ (๋™์‹œ์— ๋Œ๋ ค๋„ ์„œ๋กœ ์•ˆ ์ง€์šฐ๊ฒŒ)
103
+ net = load_model(name)
104
+ for i, f in enumerate(todo):
105
+ img = Image.open(src_root / f.filename).convert("RGB") # ์›๋ณธ์€ ์ฝ๊ธฐ๋งŒ
106
+ img = translate_image(net, img, size)
107
+ dst = dst_root / f.filename
108
+ dst.parent.mkdir(parents=True, exist_ok=True)
109
+ img.save(dst, quality=quality if last else 98)
110
+ if i % 200 == 0:
111
+ log(f" [{name}] {i:,}/{len(todo):,} {(time.time() - t0) / max(i + si * len(todo), 1):.2f}s/์žฅ")
112
+ del net
113
+ torch.cuda.empty_cache()
114
+ if si > 0:
115
+ shutil.rmtree(src_root) # ์•ž ๋‹จ๊ณ„ ์ž„์‹œ ๊ฒฐ๊ณผ
116
+ src_root = dst_root
117
+ return {"n": len(frames), "done": len(todo), "out": str(out), "s_per_image": (time.time() - t0) / len(todo)}
src/nusim/nusim/cli/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """์ปค๋งจ๋“œ๋ผ์ธ ์ง„์ž…์ ."""
src/nusim/nusim/cli/doctor.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """vkgs-doctor โ€” ์ปจํ…Œ์ด๋„ˆ ํ™˜๊ฒฝ์ด ์‹ค์ œ๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ์ƒํƒœ์ธ์ง€ ํ•œ ๋ฒˆ์— ์ ๊ฒ€ํ•œ๋‹ค.
2
+
3
+ ์ด๊ฒŒ ๋”ฐ๋กœ ์žˆ๋Š” ์ด์œ : ์ด ์Šคํƒ์€ ์กฐ์šฉํžˆ ๋ฐ˜์ฏค ๋ง๊ฐ€์ง€๋Š” ๋ฐฉ์‹์ด ์—ฌ๋Ÿฟ์ด๋‹ค.
4
+ - nvidia-container-toolkit ์€ ์žˆ๋Š”๋ฐ DRIVER_CAPABILITIES ์— graphics ๊ฐ€ ๋น ์ง€๋ฉด
5
+ CUDA ๋Š” ๋˜๊ณ  Vulkan ๋งŒ GPU ๋ฅผ 0๊ฐœ๋กœ ๋ณธ๋‹ค. ์—๋Ÿฌ ์—†์ด ๊ฒ€์€ ํ™”๋ฉด๋งŒ ๋‚˜์˜จ๋‹ค.
6
+ - NFS ๊ฐ€ ์ž ๊น ๋Š๊ธฐ๋ฉด ๋ฐ์ดํ„ฐ์…‹ ๊ฒฝ๋กœ๊ฐ€ '์—†๋Š” ๊ฒƒ์ฒ˜๋Ÿผ' ๋ณด์ธ๋‹ค.
7
+ - gsplat ์˜ CUDA ํ™•์žฅ์ด ๋นŒ๋“œ๋Š” ๋๋Š”๋ฐ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ์•ˆ ๋งž์œผ๋ฉด ์ฒซ backward ์—์„œ์•ผ ํ„ฐ์ง„๋‹ค.
8
+ ํ•™์Šต์„ 40๋ถ„ ๋Œ๋ฆฐ ๋’ค์— ์•„๋Š” ๊ฒƒ๋ณด๋‹ค ์—ฌ๊ธฐ์„œ ์•„๋Š” ๊ฒŒ ๋‚ซ๋‹ค.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import platform
15
+ import shutil
16
+ import subprocess
17
+ import sys
18
+
19
+ _OK, _WARN, _FAIL = "โœ”", "!", "โœ˜"
20
+ _C = {"โœ”": "\033[1;32m", "!": "\033[1;33m", "โœ˜": "\033[1;31m"}
21
+ _RESET = "\033[0m"
22
+
23
+ _failures = 0
24
+ _warnings = 0
25
+
26
+
27
+ def _line(status: str, label: str, detail: str = "") -> None:
28
+ global _failures, _warnings
29
+ if status == _FAIL:
30
+ _failures += 1
31
+ elif status == _WARN:
32
+ _warnings += 1
33
+ color = _C.get(status, "")
34
+ print(f" {color}{status}{_RESET} {label:<22} {detail}")
35
+
36
+
37
+ def _head(title: str) -> None:
38
+ print(f"\n\033[1;36m{title}\033[0m")
39
+
40
+
41
+ def _run(cmd: list[str], timeout: int = 20) -> tuple[int, str]:
42
+ try:
43
+ p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
44
+ return p.returncode, (p.stdout or "") + (p.stderr or "")
45
+ except FileNotFoundError:
46
+ return 127, "์‹คํ–‰ํŒŒ์ผ ์—†์Œ"
47
+ except subprocess.TimeoutExpired:
48
+ return 124, "์‹œ๊ฐ„ ์ดˆ๊ณผ"
49
+
50
+
51
+ # --------------------------------------------------------------------------- ๊ฒ€์‚ฌ๋“ค
52
+ def check_runtime() -> None:
53
+ _head("๋Ÿฐํƒ€์ž„")
54
+ _line(_OK, "python", f"{platform.python_version()} ({sys.executable})")
55
+ _line(_OK, "platform", f"{platform.system()} {platform.release()}")
56
+ in_docker = os.path.exists("/.dockerenv")
57
+ in_local = os.environ.get("CONDA_DEFAULT_ENV") == "vkgs"
58
+ if in_docker:
59
+ _line(_OK, "์‹คํ–‰ ํ™˜๊ฒฝ", "Docker ์ปจํ…Œ์ด๋„ˆ")
60
+ elif in_local:
61
+ _line(_OK, "์‹คํ–‰ ํ™˜๊ฒฝ", f"๋กœ์ปฌ micromamba 'vkgs' ({os.environ.get('CONDA_PREFIX', '')})")
62
+ else:
63
+ _line(_WARN, "์‹คํ–‰ ํ™˜๊ฒฝ", "์ปจํ…Œ์ด๋„ˆ๋„ vkgs ํ™˜๊ฒฝ๋„ ์•„๋‹˜ โ€” source scripts/local_env.sh ๋ฅผ ํ–ˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”")
64
+
65
+
66
+ def check_cuda() -> None:
67
+ _head("CUDA / PyTorch")
68
+ try:
69
+ import torch
70
+ except ImportError as e:
71
+ _line(_FAIL, "torch", f"์ž„ํฌํŠธ ์‹คํŒจ: {e}")
72
+ return
73
+ _line(_OK, "torch", torch.__version__)
74
+
75
+ if not torch.cuda.is_available():
76
+ _line(_FAIL, "cuda", "torch.cuda.is_available() == False "
77
+ "โ†’ --gpus all ์—†์ด ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋„์› ๊ฑฐ๋‚˜ ๋“œ๋ผ์ด๋ฒ„ ๋ถˆ์ผ์น˜")
78
+ return
79
+ n = torch.cuda.device_count()
80
+ _line(_OK, "cuda", f"{torch.version.cuda}, ์žฅ์น˜ {n}๊ฐœ")
81
+ for i in range(n):
82
+ p = torch.cuda.get_device_properties(i)
83
+ cap = f"sm_{p.major}{p.minor}"
84
+ vram = p.total_memory / 1024**3
85
+ note = ""
86
+ if vram < 10:
87
+ note = " โ† 8 GB ๊ธ‰: 3DGS ํ•™์Šต ์‹œ ๋‹ค์šด์Šค์ผ€์ผ/๊ฐ€์šฐ์‹œ์•ˆ ์ˆ˜ ์ œํ•œ ํ•„์š”"
88
+ _line(_OK, f"gpu[{i}]", f"{p.name} {vram:.1f} GB {cap}{note}")
89
+
90
+ # ์‹ค์ œ๋กœ ์ปค๋„์ด ๋„๋Š”์ง€. is_available() ๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•˜๋‹ค.
91
+ try:
92
+ a = torch.randn(256, 256, device="cuda")
93
+ torch.cuda.synchronize()
94
+ _line(_OK, "cuda ์—ฐ์‚ฐ", f"๊ฒ€์ฆ ํ†ต๊ณผ ({float((a @ a.T).sum()):.3e})")
95
+ except Exception as e:
96
+ _line(_FAIL, "cuda ์—ฐ์‚ฐ", f"{type(e).__name__}: {e}")
97
+
98
+
99
+ def check_gsplat() -> None:
100
+ _head("3DGS ํ•™์Šต ๋ฐฑ์—”๋“œ")
101
+ try:
102
+ import gsplat
103
+ except ImportError as e:
104
+ _line(_FAIL, "gsplat", f"์ž„ํฌํŠธ ์‹คํŒจ: {e}")
105
+ return
106
+ _line(_OK, "gsplat", getattr(gsplat, "__version__", "?"))
107
+ try:
108
+ import torch
109
+ from gsplat import rasterization
110
+ if not torch.cuda.is_available():
111
+ _line(_WARN, "gsplat ๋ž˜์Šคํ„ฐํ™”", "CUDA ๊ฐ€ ์—†์–ด ๊ฑด๋„ˆ๋œ€")
112
+ return
113
+ # ๊ฐ€์šฐ์‹œ์•ˆ 1๊ฐœ์งœ๋ฆฌ ์ตœ์†Œ ๋ž˜์Šคํ„ฐํ™”. CUDA ํ™•์žฅ์ด ์ด GPU ์•„ํ‚คํ…์ฒ˜๋กœ
114
+ # ์‹ค์ œ ๋นŒ๋“œ๋๋Š”์ง€ ์—ฌ๊ธฐ์„œ ๋“œ๋Ÿฌ๋‚œ๋‹ค.
115
+ dev = "cuda"
116
+ means = torch.zeros(1, 3, device=dev); means[0, 2] = 5.0
117
+ quats = torch.tensor([[1.0, 0, 0, 0]], device=dev)
118
+ scales = torch.full((1, 3), 0.3, device=dev)
119
+ opac = torch.ones(1, device=dev)
120
+ colors = torch.ones(1, 3, device=dev)
121
+ viewmat = torch.eye(4, device=dev)[None]
122
+ K = torch.tensor([[[100.0, 0, 32.0], [0, 100.0, 32.0], [0, 0, 1.0]]], device=dev)
123
+ img, _, _ = rasterization(means, quats, scales, opac, colors, viewmat, K, 64, 64)
124
+ torch.cuda.synchronize()
125
+ _line(_OK, "gsplat ๋ž˜์Šคํ„ฐํ™”", f"์ถœ๋ ฅ {tuple(img.shape)}, ์ตœ๋Œ€๊ฐ’ {float(img.max()):.3f}")
126
+ except Exception as e:
127
+ _line(_FAIL, "gsplat ๋ž˜์Šคํ„ฐํ™”", f"{type(e).__name__}: {e}")
128
+
129
+
130
+ def check_vulkan() -> None:
131
+ _head("Vulkan")
132
+ # ํ˜ธ์ŠคํŠธ๋Š” /usr/share (๋“œ๋ผ์ด๋ฒ„ ํŒจํ‚ค์ง€), ์ปจํ…Œ์ด๋„ˆ ์ด๋ฏธ์ง€๋Š” /etc ์— ๋‘”๋‹ค (docker/Dockerfile). VK_DRIVER_FILES ๊ฐ€ ์šฐ์„ .
133
+ cands = [f for f in os.environ.get("VK_DRIVER_FILES", "").split(":") if f]
134
+ for icd_dir in ("/usr/share/vulkan/icd.d", "/etc/vulkan/icd.d"):
135
+ if os.path.isdir(icd_dir):
136
+ cands += [f"{icd_dir}/{f}" for f in sorted(os.listdir(icd_dir))]
137
+ nvidia_icd = [f for f in cands if "nvidia" in os.path.basename(f).lower() and os.path.isfile(f)]
138
+ if nvidia_icd:
139
+ _line(_OK, "NVIDIA ICD", nvidia_icd[0])
140
+ else:
141
+ _line(_FAIL, "NVIDIA ICD", "nvidia ICD ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค (/usr/share ยท /etc vulkan/icd.d) โ€” "
142
+ "NVIDIA_DRIVER_CAPABILITIES ์— graphics ๊ฐ€ ๋น ์กŒ์„ ๊ฐ€๋Šฅ์„ฑ์ด ํฝ๋‹ˆ๋‹ค")
143
+
144
+ if not shutil.which("vulkaninfo"):
145
+ _line(_WARN, "vulkaninfo", "์„ค์น˜๋˜์ง€ ์•Š์Œ (vulkan-tools)")
146
+ return
147
+ rc, out = _run(["vulkaninfo", "--summary"])
148
+ if rc != 0:
149
+ _line(_FAIL, "vulkaninfo", f"์ข…๋ฃŒ์ฝ”๋“œ {rc}: {out.strip().splitlines()[-1][:120] if out.strip() else ''}")
150
+ return
151
+
152
+ gpus = [l.strip() for l in out.splitlines() if "deviceName" in l]
153
+ api = next((l.strip() for l in out.splitlines() if "apiVersion" in l), "")
154
+ if gpus:
155
+ _line(_OK, "Vulkan ์žฅ์น˜", f"{len(gpus)}๊ฐœ")
156
+ for g in gpus:
157
+ _line(_OK, "", g)
158
+ if api:
159
+ _line(_OK, "apiVersion", api.split("=")[-1].strip())
160
+ else:
161
+ _line(_FAIL, "Vulkan ์žฅ์น˜", "0๊ฐœ โ€” ICD ๋Š” ์žˆ๋Š”๋ฐ ๋ฌผ๋ฆฌ์žฅ์น˜๊ฐ€ ์•ˆ ์žกํž™๋‹ˆ๋‹ค")
162
+
163
+ for tool, pkg in (("glslangValidator", "glslang-tools"), ("glslc", "glslc")):
164
+ if shutil.which(tool):
165
+ _line(_OK, f"์…ฐ์ด๋” ์ปดํŒŒ์ผ๋Ÿฌ", tool)
166
+ break
167
+ else:
168
+ _line(_FAIL, "์…ฐ์ด๋” ์ปดํŒŒ์ผ๋Ÿฌ", "glslangValidator / glslc ๋‘˜ ๋‹ค ์—†์Œ")
169
+
170
+ if os.environ.get("DISPLAY"):
171
+ _line(_OK, "DISPLAY", os.environ["DISPLAY"] + " (์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋ทฐ์–ด ๊ฐ€๋Šฅ)")
172
+ else:
173
+ _line(_WARN, "DISPLAY", "์—†์Œ โ€” ํ—ค๋“œ๋ฆฌ์Šค. ์˜คํ”„๋ผ์ธ ๋ฐฐ์น˜ ๋ Œ๋”๋ง์€ ์ •์ƒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค")
174
+
175
+
176
+ def check_dataset() -> None:
177
+ _head("nuScenes ๋ฐ์ดํ„ฐ์…‹")
178
+ try:
179
+ from nusim.paths import dataset
180
+ ds = dataset()
181
+ except Exception as e:
182
+ _line(_FAIL, "๋ฐ์ดํ„ฐ์…‹ ๋ฃจํŠธ", str(e).splitlines()[0])
183
+ return
184
+ _line(_OK, "๋ฃจํŠธ", str(ds.root))
185
+ for name, (exists, path) in ds.check().items():
186
+ _line(_OK if exists else _FAIL, name, str(path) if not exists else "")
187
+
188
+ # NFS ๋Š” ๋งˆ์šดํŠธ๋ผ ๋ณด์—ฌ๋„ ์‘๋‹ต์ด ์ฃฝ์–ด ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค. ์‹ค์ œ๋กœ ํ•œ ํŒŒ์ผ์„ ์ฝ์–ด ๋ณธ๋‹ค.
189
+ try:
190
+ import time
191
+ t = time.time()
192
+ (ds.meta / "scene.json").read_bytes()
193
+ _line(_OK, "์ฝ๊ธฐ ์‹œํ—˜", f"scene.json {(time.time()-t)*1000:.0f} ms")
194
+ except Exception as e:
195
+ _line(_FAIL, "์ฝ๊ธฐ ์‹œํ—˜", f"{type(e).__name__}: {e}")
196
+
197
+
198
+ def check_index() -> None:
199
+ _head("์”ฌ ์ธ๋ฑ์Šค")
200
+ try:
201
+ from nusim.data.index import load_catalog
202
+ cat = load_catalog()
203
+ except FileNotFoundError:
204
+ _line(_WARN, "์นดํƒˆ๋กœ๊ทธ", "์•„์ง ์—†์Œ โ€” nusim index build ๋ฅผ ์‹คํ–‰ํ•˜์„ธ์š”")
205
+ return
206
+ except Exception as e:
207
+ _line(_FAIL, "์นดํƒˆ๋กœ๊ทธ", f"{type(e).__name__}: {e}")
208
+ return
209
+ import collections
210
+ by = collections.Counter(s["location"] for s in cat["scenes"])
211
+ total = sum(s["n_images"] for s in cat["scenes"])
212
+ _line(_OK, "์นดํƒˆ๋กœ๊ทธ", f"์”ฌ {len(cat['scenes'])}๊ฐœ / ์ด๋ฏธ์ง€ {total:,}์žฅ (์ƒ์„ฑ {cat['built_at']})")
213
+ for loc, n in sorted(by.items()):
214
+ _line(_OK, "", f"{loc:26s} {n:4d} ์”ฌ")
215
+ if cat.get("dataset_root"):
216
+ from nusim.paths import nuscenes_root
217
+ cur = str(nuscenes_root())
218
+ if cat["dataset_root"] != cur:
219
+ _line(_WARN, "๋ฃจํŠธ ๋ถˆ์ผ์น˜",
220
+ f"์ธ๋ฑ์Šค๋Š” {cat['dataset_root']} ๊ธฐ์ค€์œผ๋กœ ๋งŒ๋“ค์–ด์กŒ๋Š”๋ฐ ํ˜„์žฌ๋Š” {cur} ์ž…๋‹ˆ๋‹ค. "
221
+ "ํŒŒ์ผ ์ƒ๋Œ€๊ฒฝ๋กœ๋Š” ๊ฐ™์œผ๋ฏ€๋กœ ๋ณดํ†ต ๋ฌดํ•ดํ•˜์ง€๋งŒ, ์ด์ƒํ•˜๋ฉด --force ๋กœ ์žฌ์ƒ์„ฑํ•˜์„ธ์š”.")
222
+
223
+
224
+ def check_workspace() -> None:
225
+ _head("์ž‘์—…๊ณต๊ฐ„")
226
+ from nusim.paths import cache_dir, output_dir, workspace_root
227
+ _line(_OK, "workspace", str(workspace_root()))
228
+ for label, d in (("cache", cache_dir()), ("outputs", output_dir())):
229
+ writable = os.access(d, os.W_OK)
230
+ _line(_OK if writable else _FAIL, label,
231
+ str(d) if writable else f"{d} โ† ์“ฐ๊ธฐ ๋ถˆ๊ฐ€. uid ๋ถˆ์ผ์น˜์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค (HOST_UID/.env ํ™•์ธ)")
232
+
233
+
234
+ def main() -> int:
235
+ print("\033[1mvkgs-sim ํ™˜๊ฒฝ ์ง„๋‹จ\033[0m")
236
+ for fn in (check_runtime, check_cuda, check_gsplat, check_vulkan,
237
+ check_dataset, check_index, check_workspace):
238
+ try:
239
+ fn()
240
+ except Exception as e: # ํ•œ ํ•ญ๋ชฉ์ด ํ„ฐ์ ธ๋„ ๋‚˜๋จธ์ง€๋Š” ๋ด์•ผ ํ•œ๋‹ค
241
+ _line(_FAIL, fn.__name__, f"๊ฒ€์‚ฌ ์ž์ฒด๊ฐ€ ์‹คํŒจ: {type(e).__name__}: {e}")
242
+
243
+ print()
244
+ if _failures:
245
+ print(f"\033[1;31m{_failures}๊ฐœ ์‹คํŒจ\033[0m, {_warnings}๊ฐœ ๊ฒฝ๊ณ ")
246
+ return 1
247
+ if _warnings:
248
+ print(f"\033[1;33m๋ฌธ์ œ ์—†์Œ ({_warnings}๊ฐœ ๊ฒฝ๊ณ )\033[0m")
249
+ return 0
250
+ print("\033[1;32m๋ชจ๋‘ ์ •์ƒ\033[0m")
251
+ return 0
252
+
253
+
254
+ if __name__ == "__main__":
255
+ raise SystemExit(main())
src/nusim/nusim/cli/main.py ADDED
@@ -0,0 +1,1487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """nusim โ€” ํ”„๋กœ์ ํŠธ ์ปค๋งจ๋“œ๋ผ์ธ.
2
+
3
+ typer/rich ๊ฐ€ ์•„๋‹ˆ๋ผ argparse ๋กœ ์“ด ์ด์œ : ์ด CLI ๋Š” ์ปจํ…Œ์ด๋„ˆ ๋ฐ– ํ˜ธ์ŠคํŠธ์—์„œ๋„
4
+ (์˜์กด์„ฑ ์„ค์น˜ ์—†์ด) ๋Œ์•„์•ผ ์ง„๋‹จ์— ์“ธ๋ชจ๊ฐ€ ์žˆ๋‹ค. ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ ์“ด๋‹ค.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import logging
11
+ import sys
12
+ from pathlib import Path
13
+
14
+
15
+ LOG = logging.getLogger("nusim.cli")
16
+
17
+
18
+ def _cmd_index_build(a: argparse.Namespace) -> int:
19
+ from nusim import SINGAPORE_MAPS
20
+ from nusim.data.index import build_index
21
+
22
+ locations = a.locations or list(SINGAPORE_MAPS)
23
+ build_index(
24
+ locations=locations,
25
+ include_sweeps=not a.no_sweeps,
26
+ force=a.force,
27
+ )
28
+ return 0
29
+
30
+
31
+ def _cmd_index_list(a: argparse.Namespace) -> int:
32
+ from nusim.data.index import load_catalog
33
+
34
+ cat = load_catalog()
35
+ rows = cat["scenes"]
36
+ if a.location:
37
+ rows = [r for r in rows if r["location"] == a.location]
38
+ if a.grep:
39
+ g = a.grep.lower()
40
+ rows = [r for r in rows if g in r["description"].lower() or g in r["name"]]
41
+ if a.limit:
42
+ rows = rows[: a.limit]
43
+
44
+ print(f"{'scene':<12} {'location':<26} {'imgs':>6} {'sec':>6} description")
45
+ print("-" * 100)
46
+ for r in rows:
47
+ print(f"{r['name']:<12} {r['location']:<26} {r['n_images']:>6} "
48
+ f"{r['duration_s']:>6.1f} {r['description'][:44]}")
49
+ print(f"\n{len(rows)}๊ฐœ ์”ฌ / ์ด๋ฏธ์ง€ {sum(r['n_images'] for r in rows):,}์žฅ")
50
+ return 0
51
+
52
+
53
+ def _cmd_scene_show(a: argparse.Namespace) -> int:
54
+ from nusim.data.index import load_scene
55
+ from nusim.paths import dataset
56
+
57
+ si = load_scene(a.name)
58
+ ds = dataset()
59
+ print(f"\n\033[1m{si.name}\033[0m {si.location}")
60
+ print(f" {si.description}")
61
+ print(f" ์ฐจ๋Ÿ‰ {si.vehicle} / ์ดฌ์˜์ผ {si.date_captured} / {si.duration_s():.1f}์ดˆ")
62
+ print(f" ํ‚คํ”„๋ ˆ์ž„ {si.nbr_samples}๊ฐœ / ์ „์ฒด ์ด๋ฏธ์ง€ {si.n_images:,}์žฅ\n")
63
+ print(f" {'camera':<17} {'frames':>7} {'keyfr':>6} {'Hz':>6} {'fx':>8} {'HFOV':>6}")
64
+ for cam, frames in si.frames.items():
65
+ if not frames:
66
+ continue
67
+ span = (frames[-1].timestamp - frames[0].timestamp) / 1e6
68
+ hz = (len(frames) - 1) / span if span > 0 else 0.0
69
+ c = si.calib[cam]
70
+ import math
71
+ fx = c.intrinsic[0][0]
72
+ hfov = math.degrees(2 * math.atan(c.width / (2 * fx)))
73
+ print(f" {cam:<17} {len(frames):>7} {len(si.keyframes(cam)):>6} {hz:>6.1f} {fx:>8.1f} {hfov:>5.1f}ยฐ")
74
+
75
+ f0 = si.frames["CAM_FRONT"][0]
76
+ print(f"\n ์ฒซ ํ”„๋ ˆ์ž„: {f0.filename}")
77
+ print(f" ์กด์žฌ ์—ฌ๋ถ€: {f0.path(ds).exists()}")
78
+ return 0
79
+
80
+
81
+ def _cmd_recon_init(a: argparse.Namespace) -> int:
82
+ from nusim.data.index import load_scene
83
+ from nusim.io.ply import write_ply
84
+ from nusim.paths import dataset, output_dir
85
+ from nusim.recon.init import InitConfig, init_gaussians
86
+
87
+ import json
88
+
89
+ from nusim.recon.init import init_meta
90
+
91
+ cfg = InitConfig(voxel=a.voxel, max_points=a.max_points)
92
+ print(f"\033[1m{a.name}\033[0m ์ดˆ๊ธฐํ™”")
93
+ g = init_gaussians(load_scene(a.name), dataset(), cfg)
94
+ path = write_ply(output_dir() / "gs" / a.name / "init.ply", g)
95
+ path.with_suffix(".json").write_text(json.dumps(init_meta(cfg), indent=2))
96
+ print(f" โ†’ {path}")
97
+ return 0
98
+
99
+
100
+ def _train_config(a: argparse.Namespace, **extra):
101
+ """recon train ์˜ต์…˜ โ†’ TrainConfig (๋‹จ์ผ ์”ฌยทํƒ€์ผ ๊ณต์šฉ)."""
102
+ from nusim.recon.train import TrainConfig
103
+
104
+ mcmc = a.strategy == "mcmc"
105
+ return TrainConfig(iters=a.iters, max_gaussians=a.max_gaussians, depth_lambda=a.depth_lambda,
106
+ depth_scale=a.depth_scale, strategy=a.strategy, sky=a.sky, objects=a.objects,
107
+ downscale=a.downscale, frame_stride=a.stride, keyframes_only=a.keyframes_only,
108
+ aniso_reg=a.aniso_reg, aniso_max=a.aniso_max,
109
+ ground_lambda=getattr(a, "ground_lambda", 0.0),
110
+ opacity_reg=a.opacity_reg if a.opacity_reg is not None else (0.01 if mcmc else 0.0),
111
+ scale_reg=a.scale_reg if a.scale_reg is not None else (0.01 if mcmc else 0.0),
112
+ # MCMC ๋Š” cap ์•ˆ์—์„œ ์žฌ๋ฐฐ์น˜๋งŒ ํ•˜๋ฏ€๋กœ ์˜ค๋ž˜ ์ผœ ๋‘”๋‹ค (gsplat ๊ธฐ๋ณธ 25k/30k โ‰ˆ 0.8)
113
+ refine_stop=a.refine_stop or (int(0.8 * a.iters) if mcmc else min(15_000, a.iters // 2)),
114
+ eval_every=a.eval_every, absgrad=a.absgrad,
115
+ grow_grad2d=a.grow_grad2d or (0.0008 if a.absgrad else 0.0002),
116
+ lr_camera=0.0 if a.no_camera_adjust else TrainConfig.lr_camera,
117
+ time_window=a.time_window, **extra, image_root=str(Path(a.image_root).resolve()) if a.image_root else None)
118
+
119
+
120
+ def _train_tile(a: argparse.Namespace, out: Path) -> int:
121
+ """์—ฌ๋Ÿฌ ์ฃผํ–‰์„ ํ•œ ๋ชจ๋ธ๋กœ (recon.tile). a.name ์€ ๋ฌด์‹œํ•˜๊ณ  ํƒ€์ผ์˜ anchor ๋ฅผ ์“ด๋‹ค."""
122
+ import json
123
+
124
+ from nusim.io.ply import read_ply, write_ply
125
+ from nusim.log import run_log
126
+ from nusim.paths import dataset
127
+ from nusim.recon.init import InitConfig, init_meta
128
+ from nusim.recon.tile import ALIGN_VERSION, Tile, tile_init, tile_lidar, tile_scenes, tile_views
129
+ from nusim.recon.train import train
130
+
131
+ if a.objects == "vehicles":
132
+ raise SystemExit("ํƒ€์ผ ํ•™์Šต์€ ์•„์ง --objects vehicles ๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค (์ฃผํ–‰๋งˆ๋‹ค ์ฐจ๋Ÿ‰์ด ๋‹ค๋ฅด๋‹ค) โ€” agents ๋กœ")
133
+ tile, ds = Tile.load(a.tile), dataset()
134
+ if a.name != tile.anchor:
135
+ print(f" ํƒ€์ผ {tile.name} ์˜ ๊ธฐ์ค€ ์”ฌ์€ {tile.anchor} โ€” {a.name} ๋Œ€์‹  ๊ทธ๊ฑธ ์“ด๋‹ค")
136
+ scenes = tile_scenes(tile)
137
+ icfg = InitConfig(sky_points=0 if a.sky else 30_000, objects=a.objects)
138
+ init_ply = out / "init.ply"
139
+ want = {**init_meta(icfg), "tile": json.loads(json.dumps(
140
+ {"train": tile.train, "center": tile.center.tolist(), "half_m": tile.half_m,
141
+ "align": ALIGN_VERSION if tile.align else 0}))}
142
+ meta_path = init_ply.with_suffix(".json")
143
+ out.mkdir(parents=True, exist_ok=True)
144
+ if not init_ply.exists() or not meta_path.exists() or json.loads(meta_path.read_text()) != want:
145
+ write_ply(init_ply, tile_init(tile, scenes, ds, icfg, max_total=icfg.max_points))
146
+ meta_path.write_text(json.dumps(want, indent=2))
147
+ init = read_ply(init_ply)
148
+ views = tile_views(scenes, ds, init.origin, objects=a.objects, downscale=a.downscale,
149
+ frame_stride=a.stride, keyframes_only=a.keyframes_only)
150
+ pts = None
151
+ if a.depth_lambda > 0:
152
+ from nusim.recon.depth import lidar_depth_maps
153
+
154
+ pts = tile_lidar(scenes, ds, InitConfig(objects=a.objects), init.origin)
155
+ lidar_depth_maps(pts, views.train, scale=a.depth_scale)
156
+ if a.sky:
157
+ # ํ•˜๋Š˜ = ํ๋ธŒ๋งต + ํ•˜๋Š˜ ํ”ฝ์…€์—์„œ ์Šคํ”Œ๋žซ ฮฑโ†’0. ๋‚ ๋งˆ๋‹ค ํ•˜๋Š˜ ๋ฐ๊ธฐ๊ฐ€ ๋‹ฌ๋ผ, ํ•˜๋Š˜์„ ๋”ฐ๋กœ ๋‘์ง€ ์•Š์œผ๋ฉด ๊ฐ€์šฐ์‹œ์•ˆ์ด ๋ถ€ํ’€์–ด
158
+ # ํ•˜๋Š˜์„ ๋ฎ๋‹ค ์นด๋ฉ”๋ผ๊นŒ์ง€ ๊ฐ์ŒŒ๋‹ค (6์ฃผํ–‰ 15k: 10 m ๋„˜๋Š” ๊ฐ€์šฐ์‹œ์•ˆ 685๊ฐœ, ์ž๊ธฐ ์ฃผํ–‰ ํ‰๊ฐ€ 12.7 dB)
159
+ from nusim.recon.sky import sky_masks
160
+
161
+ for k, s in enumerate(scenes):
162
+ sky_masks([v for v in views.train if v.cam_id // 6 == k], ds, sky_mask_cache(s.name, ds))
163
+ cfg = _train_config(a, tile=tile.name, reg_visible=True)
164
+ with run_log(out / "run.log"):
165
+ LOG.info("ํƒ€์ผ ํ•™์Šต %s (%d์ฃผํ–‰: %s, ๋บ€ ์ฃผํ–‰ %s) โ†’ %s", tile.name, len(scenes), ", ".join(tile.train),
166
+ ", ".join(tile.heldout) or "์—†์Œ", out)
167
+ train(views, init, out, cfg, depth_points=pts)
168
+ return 0
169
+
170
+
171
+ def _cmd_recon_tile_eval(a: argparse.Namespace) -> int:
172
+ import json
173
+
174
+ from nusim.log import run_log
175
+ from nusim.paths import dataset, output_dir
176
+ from nusim.recon.tile import Tile, tile_eval
177
+
178
+ tile = Tile.load(a.tile)
179
+ out = Path(a.out) if a.out else output_dir() / "tiles" / tile.name
180
+ out.mkdir(parents=True, exist_ok=True)
181
+ with run_log(out / "eval.log"):
182
+ res = tile_eval(tile, [Path(d) for d in a.dir], dataset(), stride=a.stride, save_dir=out / "renders")
183
+ (out / "eval.json").write_text(json.dumps(res, indent=2, ensure_ascii=False))
184
+ print(f"\n ๋บ€ ์ฃผํ–‰ {', '.join(tile.heldout)} ์˜ ์‹ค์ œ ์‚ฌ์ง„ (์ƒ‰ ์•„ํ•€ ๋งž์ถค ํ›„, ์‚ฌ๋žŒยท์ฐจ๋Ÿ‰ ๊ฐ€๋ฆผ)")
185
+ print(f" ์ฐจ์„  ๋ณ€๊ฒฝ ์‹œ์  = {tile.anchor} ์˜ ๊ฐ™์€ ๋ฐฉํ–ฅ ์นด๋ฉ”๋ผ๊ฐ€ ์˜† 6 m ์•ˆ์— ์žˆ๋Š” ๋ทฐ")
186
+ for d, r in res["models"].items():
187
+ dist = " ".join(f"{k} {v['psnr']:.2f}/{v['ssim']:.3f}({v['n']})" for k, v in r["by_dist"].items() if v["n"])
188
+ print(f" {Path(d).parent.name + '/' + Path(d).name:<36} PSNR {r['psnr']:.2f} SSIM {r['ssim']:.4f} "
189
+ f"| ์˜† ๊ฑฐ๋ฆฌ๋ณ„ PSNR/SSIM: {dist} | ์ „์ฒด {r['all']['psnr']:.2f}")
190
+ print(f" โ†’ {out / 'eval.json'}")
191
+ return 0
192
+
193
+
194
+ def _cmd_recon_train(a: argparse.Namespace) -> int:
195
+ from nusim.data.index import load_scene
196
+ from nusim.io.ply import read_ply, write_ply
197
+ from nusim.paths import dataset, output_dir
198
+ from nusim.recon.dataset import build_views
199
+ from nusim.recon.init import init_gaussians
200
+ from nusim.recon.train import TrainConfig, train
201
+
202
+ out = Path(a.out) if a.out else output_dir() / "gs" / a.name
203
+ if a.tile:
204
+ return _train_tile(a, out)
205
+ scene, ds = load_scene(a.name), dataset()
206
+ if a.image_root: # ์นด๋ฉ”๋ผ ์ด๋ฏธ์ง€๋งŒ ๋‹ค๋ฅธ ๊ณณ์—์„œ (๋ฐคยท๋น„ โ†’ ๋‚ฎ ๋ณ€ํ™˜ ๋“ฑ, ๊ฐ™์€ ์ƒ๋Œ€ ๊ฒฝ๋กœ)
207
+ from dataclasses import replace as _replace
208
+
209
+ ds = _replace(ds, image_root=Path(a.image_root))
210
+ print(f" ์นด๋ฉ”๋ผ ์ด๋ฏธ์ง€ ๋ฃจํŠธ {ds.images}")
211
+ if a.time_window:
212
+ t0, t1 = (float(x) for x in a.time_window.split(":"))
213
+ scene = scene.window(t0, t1)
214
+ print(f" ์‹œ๊ฐ„ ๊ตฌ๊ฐ„ {t0:g}~{t1:g} ์ดˆ โ€” ์นด๋ฉ”๋ผ {scene.n_images}์žฅ, ๋ผ์ด๋‹ค {len(scene.lidar)}์žฅ")
215
+ # ์ดˆ๊ธฐํ™”๋Š” ๊ฒฐ๊ณผ ๋””๋ ‰ํ„ฐ๋ฆฌ๋งˆ๋‹ค ๋”ฐ๋กœ ๋‘”๋‹ค โ€” ์„ค์ •(sweep ์—ฌ๋ถ€ ๋“ฑ)์ด ๋‹ค๋ฅธ ์‹คํ—˜์ด ์„œ๋กœ์˜ init ์„ ์“ฐ์ง€ ์•Š๊ฒŒ
216
+ init_ply = out / "init.ply"
217
+ print(f"\033[1m{a.name}\033[0m {scene.description}")
218
+ import json
219
+
220
+ from nusim.recon.init import InitConfig, init_meta
221
+
222
+ # ํ•˜๋Š˜์„ ํ๋ธŒ๋งต์œผ๋กœ ๋ถ„๋ฆฌํ•˜๋ฉด ๋จผ ๊ตฌ๋ฉด ๊ฐ€์šฐ์‹œ์•ˆ(ํ•˜๋Š˜ ๊ตฌ)์€ ํ•„์š” ์—†๋‹ค โ€” ์˜คํžˆ๋ ค floater ์›์ธ
223
+ # vehicles: ๋ฐฐ๊ฒฝ ์ดˆ๊ธฐํ™”ยท๊นŠ์ด ์ง€๋„๋Š” ์ฐจ๋Ÿ‰๊นŒ์ง€ ๋บ€ ๋ผ์ด๋‹ค๋กœ (์ฐจ๋Ÿ‰์€ ๊ฐ์ฒด ๊ฐ€์šฐ์‹œ์•ˆ์ด ๋”ฐ๋กœ ๋งก๋Š”๋‹ค โ€” recon.objects)
224
+ bg_objects = "agents" if a.objects == "vehicles" else a.objects
225
+ icfg = InitConfig(sky_points=0 if a.sky else 30_000, objects=bg_objects)
226
+ meta_path = init_ply.with_suffix(".json")
227
+ old_meta = json.loads(meta_path.read_text()) if meta_path.exists() else None
228
+ want_meta = {**init_meta(icfg), **({"time_window": a.time_window} if a.time_window else {}),
229
+ **({"image_root": str(ds.images)} if a.image_root else {})} # ์ƒ‰์€ ์ด ์ด๋ฏธ์ง€์—์„œ
230
+ if not init_ply.exists() or old_meta != want_meta:
231
+ if init_ply.exists():
232
+ print(" init.ply ๋ฅผ ๋‹ค์‹œ ๋งŒ๋“ญ๋‹ˆ๋‹ค (์„ค์ • ๋˜๋Š” ์ดˆ๊ธฐํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜ ํŒ์ด ๋‹ค๋ฆ„)")
233
+ write_ply(init_ply, init_gaussians(scene, ds, icfg))
234
+ meta_path.write_text(json.dumps(want_meta, indent=2))
235
+ init = read_ply(init_ply)
236
+ views = build_views(scene, ds, init.origin, downscale=a.downscale,
237
+ frame_stride=a.stride, keyframes_only=a.keyframes_only, objects=a.objects)
238
+ if a.depth_lambda > 0:
239
+ from nusim.recon.depth import lidar_depth_maps
240
+ from nusim.recon.init import InitConfig, accumulate_lidar
241
+
242
+ pts = accumulate_lidar(scene, ds, InitConfig(objects=bg_objects)) - init.origin # ๊ฐ€๋ฆฐ ๊ฐ์ฒด์˜ ์ ์€ ๋บ€๋‹ค
243
+ lidar_depth_maps(pts, views.train, scale=a.depth_scale)
244
+ else:
245
+ pts = None
246
+ if a.sky:
247
+ from nusim.paths import cache_dir
248
+ from nusim.recon.sky import sky_masks
249
+
250
+ sky_masks(views.train, ds, sky_mask_cache(a.name, ds))
251
+ cfg = _train_config(a)
252
+ from nusim.log import run_log
253
+
254
+ with run_log(out / "run.log"):
255
+ LOG.info("ํ•™์Šต ์‹œ์ž‘ %s โ†’ %s (strategy=%s, sky=%s, depth_lambda=%s, objects=%s)", a.name, out, a.strategy,
256
+ a.sky, a.depth_lambda, a.objects)
257
+ objs = obj_init = None
258
+ if a.objects == "vehicles":
259
+ from nusim.recon.objects import ObjectSet, init_objects
260
+
261
+ objs = ObjectSet.from_scene(scene, init.origin)
262
+ obj_init = init_objects(objs, scene, ds, icfg, log=print)
263
+ gmask = None
264
+ if cfg.ground_lambda > 0:
265
+ from nusim.recon.ground import ground_mask
266
+
267
+ gmask = ground_mask(init.means, scene, init.origin, ds)
268
+ train(views, init, out, cfg, depth_points=pts, objects=objs, obj_init=obj_init, ground=gmask)
269
+ return 0
270
+
271
+
272
+ def _model_views(d: Path) -> dict:
273
+ """๋ชจ๋ธ์„ ํ•™์Šตํ•œ ๋ทฐ ์ง‘ํ•ฉ ์„ค์ • (report.json ์˜ config). ์˜› ๋ชจ๋ธ์€ ๊ธฐ๋ณธ๊ฐ’."""
274
+ import json
275
+
276
+ out = {"downscale": 2, "frame_stride": 1, "keyframes_only": False}
277
+ try:
278
+ cfg = json.loads((d / "report.json").read_text())["config"]
279
+ out.update({k: cfg[k] for k in out if k in cfg})
280
+ except (OSError, KeyError, ValueError):
281
+ pass
282
+ return out
283
+
284
+
285
+ def _model_data(name: str, d: Path):
286
+ """(์”ฌ, ๋ฐ์ดํ„ฐ ๊ฒฝ๋กœ) โ€” ๋ชจ๋ธ์„ ํ•™์Šตํ•œ ๋ฐ์ดํ„ฐ ๋ฒ”์œ„ ๊ทธ๋Œ€๋กœ (report.json config ์˜ time_windowยทimage_root).
287
+ ํ‰๊ฐ€ยท์ฆ๋ฅ˜ยท์ •๋ฆฌยทํ•˜๋Š˜ ํ‘œ์‹œยท์‚ฌ์ดํด์ด ๋ชจ๋‘ ์ด๊ฑธ๋กœ โ€” ๊ตฌ๊ฐ„ ๋ชจ๋ธ์— ์”ฌ ์ „์ฒด ๋ทฐ๋ฅผ ์ฃผ๋ฉด ์ƒ‰ ๋ณด์ • ํฌ๊ธฐ๊ฐ€ ์•ˆ ๋งž๊ณ ,
288
+ ๋ณ€ํ™˜ ์˜์ƒ์œผ๋กœ ํ•™์Šตํ•œ ๋ชจ๋ธ์„ ์›๋ณธ(๋ฐค) ์‚ฌ์ง„์œผ๋กœ ํ‰๊ฐ€ํ•˜๊ฒŒ ๋œ๋‹ค (2026-10-05 ๊ฒ€ํ† )."""
289
+ import json
290
+ from dataclasses import replace
291
+
292
+ from nusim.data.index import load_scene
293
+ from nusim.paths import dataset
294
+
295
+ try:
296
+ cfg = json.loads((d / "report.json").read_text())["config"]
297
+ except (OSError, KeyError, ValueError):
298
+ cfg = {}
299
+ scene, ds = load_scene(name), dataset()
300
+ if cfg.get("time_window"):
301
+ t0, t1 = (float(x) for x in cfg["time_window"].split(":"))
302
+ scene = scene.window(t0, t1)
303
+ if cfg.get("image_root"):
304
+ ds = replace(ds, image_root=Path(cfg["image_root"]))
305
+ return scene, ds
306
+
307
+
308
+ def sky_mask_cache(name: str, ds) -> Path:
309
+ """ํ•˜๋Š˜ ๋งˆ์Šคํฌ ์บ์‹œ โ€” ์ด๋ฏธ์ง€ ๋ฃจํŠธ๋งˆ๋‹ค ๋”ฐ๋กœ (์›๋ณธ ๋ฐค ์‚ฌ์ง„๊ณผ ๋ณ€ํ™˜ํ•œ ๋‚ฎ ์˜์ƒ์˜ ๋งˆ์Šคํฌ๋ฅผ ์„ž์ง€ ์•Š๊ฒŒ)."""
310
+ from nusim.paths import cache_dir
311
+
312
+ tag = name if ds.image_root is None else f"{name}@{Path(ds.image_root).name}"
313
+ return cache_dir() / "sky_masks" / tag
314
+
315
+
316
+ def _model_objects(d: Path) -> str:
317
+ """๋ชจ๋ธ์ด ํ•™์Šต ๋•Œ ๊ฐ€๋ฆฐ ๊ฐ์ฒด ์ข…๋ฅ˜ (report.json ์˜ config.objects). ์˜› ๋ชจ๋ธ์€ dynamic."""
318
+ import json
319
+
320
+ try:
321
+ return json.loads((d / "report.json").read_text())["config"].get("objects", "dynamic")
322
+ except (OSError, KeyError, ValueError):
323
+ return "dynamic"
324
+
325
+
326
+ def _cmd_recon_eval(a: argparse.Namespace) -> int:
327
+ import json
328
+
329
+ import torch
330
+
331
+ from nusim.data.index import load_scene
332
+ from nusim.io.ply import read_ply
333
+ from nusim.paths import dataset, output_dir
334
+ from nusim.recon.dataset import build_views
335
+ from nusim.recon.train import CameraAdjust, ColorCorrection, _to_params, evaluate
336
+
337
+ d = Path(a.dir) if a.dir else output_dir() / "gs" / a.name
338
+ g = read_ply(d / "point_cloud.ply")
339
+ objects = _model_objects(d)
340
+ vk = _model_views(d)
341
+ scene, ds = _model_data(a.name, d)
342
+ views = build_views(scene, ds, g.origin, downscale=a.downscale or vk["downscale"],
343
+ frame_stride=vk["frame_stride"], keyframes_only=vk["keyframes_only"], objects=objects)
344
+ color = ColorCorrection.load(d / "color_correction.pt", 6, len(views.train) + len(views.test), "cuda")
345
+ adj = None
346
+ if (d / "camera_adjust.pt").exists():
347
+ adj = CameraAdjust(6).cuda()
348
+ adj.load_state_dict(torch.load(d / "camera_adjust.pt", weights_only=True))
349
+ sky = None
350
+ if (d / "sky.pt").exists():
351
+ from nusim.recon.train import SkyCube
352
+
353
+ try:
354
+ sky = SkyCube.from_state(torch.load(d / "sky.pt", weights_only=True))
355
+ except ValueError as e:
356
+ LOG.warning("ํ•˜๋Š˜ ํ๋ธŒ๋งต์„ ๋ชป ์ฝ์Œ: %s", e)
357
+ params = _to_params(g, "cuda")
358
+ objposes = None
359
+ if objects == "vehicles" and (d / "objects" / "objects.json").exists():
360
+ # ์ฐจ๋Ÿ‰ ๋™์‹œ ํ•™์Šต ๋ชจ๋ธ: ๋ฐฐ๊ฒฝ ply ์— ์ฐจ๋Ÿ‰ ์—์…‹์„ ๋‹ค์‹œ ๋ถ™์—ฌ ํ•™์Šต ๋•Œ์ฒ˜๋Ÿผ ๊ทธ๋ฆฐ๋‹ค
361
+ from nusim.recon.objects import load_objects
362
+
363
+ arrays, ids, objs = load_objects(d, scene, g.origin)
364
+ if len(ids):
365
+ for key, val in arrays.items():
366
+ params[key] = torch.nn.Parameter(torch.cat([params[key].data, val], 0))
367
+ params["obj"] = torch.nn.Parameter(torch.cat([torch.full((len(g),), -1.0, device="cuda"),
368
+ torch.as_tensor(ids, device="cuda")]))
369
+ cache: dict[int, tuple] = {}
370
+
371
+ def objposes(v):
372
+ t = int(v.pose.timestamp)
373
+ if t not in cache:
374
+ cache[t] = tuple(torch.as_tensor(x, device="cuda", dtype=torch.float32) for x in objs.poses_at(t))
375
+ return cache[t]
376
+ print(f" ์ฐจ๋Ÿ‰ ์—์…‹ {len(set(ids.tolist()))}๋Œ€, ๊ฐ€์šฐ์‹œ์•ˆ {len(ids):,}๊ฐœ๋ฅผ ๋ถ™์—ฌ ํ‰๊ฐ€")
377
+ ev = evaluate(params, color, views, g.sh_degree, cam_adjust=adj, sky=sky, objposes=objposes, with_lpips=a.lpips)
378
+ if a.geom:
379
+ from nusim.paths import cache_dir
380
+ from nusim.recon.depth import lidar_depth_maps
381
+ from nusim.recon.init import InitConfig, accumulate_lidar
382
+ from nusim.recon.metrics import geometry_metrics
383
+ from nusim.recon.sky import sky_masks
384
+
385
+ pts = accumulate_lidar(scene, ds, InitConfig(objects=objects)) - g.origin
386
+ if adj is not None: # ๋ Œ๋”์™€ ๊ฐ™์€ (๋ณด์ •๋œ) ์นด๋ฉ”๋ผ๋กœ ํˆฌ์˜ํ•œ๋‹ค
387
+ from nusim.recon.train import _refresh_depth
388
+
389
+ _refresh_depth(views.test, adj, pts, 4)
390
+ else:
391
+ lidar_depth_maps(pts, views.test, scale=4)
392
+ sky_masks(views.test, ds, sky_mask_cache(a.name, ds))
393
+ ev.update(geometry_metrics(params, views.test, g.sh_degree, cam_adjust=adj))
394
+ prev = json.loads((d / "eval.json").read_text()) if (d / "eval.json").exists() else {}
395
+ (d / "eval.json").write_text(json.dumps({**prev, **ev}, indent=2)) # ์ด์ „ --geom ๋“ฑ ๋‹ค๋ฅธ ํ‚ค๋Š” ๋‚จ๊ธด๋‹ค
396
+ print(json.dumps(ev, indent=2))
397
+ return 0
398
+
399
+
400
+ def _cmd_run(a: argparse.Namespace) -> int:
401
+ from nusim.config import resolve
402
+ from nusim.jobs import run_job
403
+ from nusim.log import run_log
404
+ from nusim.paths import output_dir
405
+
406
+ path = resolve("jobs", a.job)
407
+ with run_log(output_dir() / "jobs" / f"{path.stem}.log"):
408
+ return run_job(path, dry=a.dry_run)
409
+
410
+
411
+ def _cmd_recon_batch(a: argparse.Namespace) -> int:
412
+ from nusim.paths import output_dir
413
+ from nusim.recon.batch import day_scenes, run_batch
414
+
415
+ if a.scenes:
416
+ scenes = [s.strip() for s in a.scenes.split(",") if s.strip()]
417
+ else:
418
+ scenes = day_scenes(a.location)
419
+ if a.order == "coverage":
420
+ from nusim.recon.batch import coverage_order
421
+
422
+ scenes = coverage_order(scenes)
423
+ if a.limit:
424
+ scenes = scenes[: a.limit]
425
+ root = Path(a.root) if a.root else output_dir() / "gs_day"
426
+ print(f"์ผ๊ด„ ์žฌ๊ตฌ์„ฑ {len(scenes)}์”ฌ (ํ”„๋ฆฌ์…‹ {a.preset}) โ†’ {root}")
427
+ from nusim.log import run_log
428
+
429
+ with run_log(root / "batch.log"):
430
+ status = run_batch(scenes, root, a.preset, iters=a.iters, sky=False if a.no_sky else None,
431
+ segment_m=a.segment_m, overlap_m=a.overlap_m, after=a.after, keep_init=a.keep_init)
432
+ units = [v for k, v in status.items() if not k.startswith("_")]
433
+ return 0 if all(v.get("state") == "done" for v in units) else 1
434
+
435
+
436
+ def _cmd_recon_cycle(a: argparse.Namespace) -> int:
437
+ import json
438
+
439
+ from nusim.log import run_log
440
+ from nusim.paths import output_dir
441
+ from nusim.recon.cycle import bench_metrics, build_cycle
442
+
443
+ base = Path(a.base) if a.base else output_dir() / "gs_day" / a.name
444
+ with run_log(base / "cycle" / "run.log"):
445
+ cdir = build_cycle(a.name, base, a.iters, a.shift)
446
+ res = {k: bench_metrics(cdir / "bench", k) for k in ("render", "base")}
447
+ (cdir / "bench_metrics.json").write_text(json.dumps(res, indent=2))
448
+ print(json.dumps(res, indent=2))
449
+ return 0
450
+
451
+
452
+ def _cmd_recon_prune(a: argparse.Namespace) -> int:
453
+ from nusim.log import run_log
454
+ from nusim.recon.prune import prune_model
455
+
456
+ src = Path(a.dir) if a.dir else _render_paths(a.name)
457
+ out = Path(a.out) if a.out else src.parent / f"{src.name}_pruned"
458
+ with run_log(out / "prune.log"):
459
+ prune_model(a.name, src, out, margin=a.margin, ratio=a.ratio, min_obs=a.min_obs)
460
+ return 0
461
+
462
+
463
+ def _cmd_recon_bench(a: argparse.Namespace) -> int:
464
+ import json
465
+
466
+ from nusim.recon.cycle import bench_metrics, run_fixer_bench
467
+
468
+ bench = Path(a.bench)
469
+ if a.run_difix:
470
+ run_fixer_bench(bench)
471
+ res = {k: bench_metrics(bench, k) for k in a.pred.split(",")}
472
+ print(json.dumps(res, indent=2))
473
+ return 0
474
+
475
+
476
+ def _cmd_recon_skylabel(a: argparse.Namespace) -> int:
477
+ from nusim.log import run_log
478
+ from nusim.recon.sky_label import label_sky
479
+
480
+ d = Path(a.dir) if a.dir else _render_paths(a.name)
481
+ with run_log(d / "sky_label.log"):
482
+ label_sky(a.name, d, ratio=a.ratio, min_obs=a.min_obs)
483
+ return 0
484
+
485
+
486
+ def _cmd_recon_semantic(a: argparse.Namespace) -> int:
487
+ from nusim.data.index import load_scene
488
+ from nusim.paths import dataset, output_dir
489
+ from nusim.recon.batch import segment_dirs
490
+ from nusim.recon.semantic import is_current, semantic_config, write_semantic
491
+
492
+ root = Path(a.root) if a.root else output_dir() / "gs_day"
493
+ names = list(a.names)
494
+ if a.location:
495
+ from nusim.render.regionmap import trained_scenes
496
+
497
+ names += [s for s, _ in trained_scenes(root, a.location)]
498
+ if not names:
499
+ raise SystemExit("์”ฌ ์ด๋ฆ„์ด๋‚˜ --location ์„ ์ค€๋‹ค")
500
+ cfg, paths, n = semantic_config(), dataset(), 0
501
+ for i, name in enumerate(names):
502
+ dirs = [m for _, m in segment_dirs(root, name)] or ([Path(a.dir)] if a.dir else [_render_paths(name)])
503
+ todo = [m for m in dirs if a.force or not is_current(m)]
504
+ if not todo:
505
+ continue
506
+ print(f"[{i + 1}/{len(names)}] {name}: ๋ชจ๋ธ {len(todo)}๊ฐœ")
507
+ sc, cache = load_scene(name), {}
508
+ for m in todo:
509
+ write_semantic(m, sc, paths, cfg, log=print, cache=cache)
510
+ n += 1
511
+ print(f"์‹œ๋งจํ‹ฑ ๋ผ๋ฒจ {n}๊ฐœ ๋ชจ๋ธ ์ƒˆ๋กœ ๋งŒ๋“ฆ")
512
+ return 0
513
+
514
+
515
+ def _ensure_sky_label(name: str, d: Path) -> None:
516
+ """ํ•˜๋Š˜์„ ๋ถ„๋ฆฌํ•˜์ง€ ์•Š๊ณ  ํ•™์Šตํ•œ ๋ชจ๋ธ์ด๋ฉด ํ•˜๋Š˜ ๊ฐ€์šฐ์‹œ์•ˆ์„ ํ‘œ์‹œํ•ด ๋‘”๋‹ค (๋ทฐ์–ด๊ฐ€ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ•˜๋Š˜ ๋ชจ๋“œ์—์„œ ๋บ€๋‹ค).
517
+ ํ•˜๋Š˜ ํ๋ธŒ๋งต(sky.ktx2)์ด ์žˆ๋Š” ๋ชจ๋ธ์€ ํ•˜๋Š˜์ด ์ด๋ฏธ ๋”ฐ๋กœ๋ผ ํ•„์š” ์—†๋‹ค."""
518
+ from nusim.recon.sky_label import is_fresh, label_sky
519
+
520
+ if (d / "sky.ktx2").exists() or is_fresh(d):
521
+ return
522
+ print(" ํ•˜๋Š˜ ๊ฐ€์šฐ์‹œ์•ˆ ํ‘œ์‹œ (ํ•œ ๋ฒˆ๋งŒ, ํ•˜๋Š˜ ๋งˆ์Šคํฌ ์บ์‹œ ์‚ฌ์šฉ)โ€ฆ")
523
+ label_sky(name, d, log=print)
524
+
525
+
526
+ def _cmd_aug_rig(a: argparse.Namespace) -> int:
527
+ from nusim.aug.rig_aug import load_variants, render_variant
528
+ from nusim.config import configs_dir
529
+ from nusim.log import run_log
530
+ from nusim.paths import output_dir
531
+ from nusim.render.compare import viewer_binary
532
+
533
+ d = _render_paths(a.name, a.model)
534
+ _ensure_export(a.name, d, 1)
535
+ variants = load_variants(Path(a.variants) if a.variants else configs_dir() / "aug" / "rig_variants.json",
536
+ a.only.split(",") if a.only else None)
537
+ out_root = Path(a.out) if a.out else output_dir() / "aug" / "rig"
538
+ extra = _ibl_gltf_args(a.name, agents=True) if a.agents else []
539
+ with run_log(out_root / a.name / "aug.log"):
540
+ print(f"\033[1m{a.name}\033[0m ๋ฆฌ๊ทธ ๋ณ€ํ˜• {len(variants)}๊ฐœ (๋ชจ๋ธ {d})")
541
+ for v in variants:
542
+ render_variant(a.name, d, v, out_root, viewer_binary(), extra, log=print)
543
+ return 0
544
+
545
+
546
+ def _cmd_aug_map(a: argparse.Namespace) -> int:
547
+ from nusim.aug.map_aug import aug_scene
548
+ from nusim.aug.rig_aug import load_variants
549
+ from nusim.config import configs_dir
550
+ from nusim.log import run_log
551
+ from nusim.paths import output_dir
552
+ from nusim.render.compare import viewer_binary
553
+
554
+ variants = load_variants(Path(a.variants) if a.variants else configs_dir() / "aug" / "rig_variants.json",
555
+ a.only.split(",") if a.only else None)
556
+ if not a.only: # ๊ธฐ๋ณธ์€ ๋‚ ์”จ ์—†๋Š” ๋ณ€ํ˜•๋งŒ โ€” ๋‚ ์”จ ํ•ฉ์„ฑ์€ ๋…ผ๋ฌธ ๋ฒ”์œ„ ๋ฐ– (2026-10-06)
557
+ variants = [v for v in variants if not v.get("weather")]
558
+ root = Path(a.root) if a.root else output_dir() / "gs_day"
559
+ out_root = Path(a.out) if a.out else output_dir() / "aug" / "map"
560
+ with run_log(out_root / a.name / "aug.log"):
561
+ print(f"\033[1m{a.name}\033[0m ๋ฆฌ๊ทธ ๋ณ€ํ˜• {len(variants)}๊ฐœ (๋ชจ๋ธ {root / a.name}, "
562
+ f"{'ํ‚คํ”„๋ ˆ์ž„' if a.keyframes_only else '์ „์ฒด ํ”„๋ ˆ์ž„'}, ์ฐจ๋Ÿ‰ {'glTF' if a.agents else '์—†์Œ'})")
563
+ aug_scene(a.name, root, variants, out_root, viewer_binary(), keyframes=a.keyframes_only, agents=a.agents,
564
+ log=print)
565
+ return 0
566
+
567
+
568
+ def _cmd_aug_artifixer(a: argparse.Namespace) -> int:
569
+ from nusim.aug.artifixer_run import aug_scene, artifixer_config, done_cams, finished_scenes
570
+ from nusim.log import run_log
571
+ from nusim.paths import output_dir
572
+
573
+ cfg = artifixer_config()
574
+ cams = a.cams.split(",") if a.cams else cfg["cams"]
575
+ names = list(a.names) or finished_scenes()
576
+ if a.limit:
577
+ names = [n for n in names if a.force or not set(cams) <= done_cams(n)][:a.limit]
578
+ log_dir = output_dir() / "aug" / "aug_artfixer"
579
+ log_dir.mkdir(parents=True, exist_ok=True)
580
+ failed = []
581
+ with run_log(log_dir / "artifixer.log"):
582
+ for i, name in enumerate(names):
583
+ if not a.force and set(cams) <= done_cams(name):
584
+ continue
585
+ print(f"\033[1m[{i + 1}/{len(names)}] {name}\033[0m ArtiFixer + ์ฐจ์„  ํ›„์ฒ˜๋ฆฌ ({','.join(cams)})", flush=True)
586
+ try:
587
+ aug_scene(name, cfg, Path(a.root) if a.root else None, cams, force=a.force)
588
+ except Exception as e: # ์”ฌ ํ•˜๋‚˜๊ฐ€ ๊นจ์ ธ๋„ (PNG ์†์ƒ ยท NFS ๋Š๊น€) ๋‚˜๋จธ์ง€๋Š” ๊ณ„์†
589
+ print(f" ์‹คํŒจ: {e}")
590
+ failed.append(name)
591
+ if failed:
592
+ print(f"์‹คํŒจ {len(failed)}์”ฌ: {' '.join(failed)}")
593
+ return 1 if failed else 0
594
+
595
+
596
+ def _cmd_aug_translate(a: argparse.Namespace) -> int:
597
+ from nusim.aug.translate import translate_scene
598
+ from nusim.log import run_log
599
+ from nusim.paths import output_dir
600
+
601
+ out_root = Path(a.out) if a.out else output_dir() / "aug" / "translate"
602
+ w, h = (int(x) for x in a.size.split("x"))
603
+ with run_log(out_root / "translate.log"):
604
+ for name in a.names:
605
+ translate_scene(name, a.models.split(","), out_root, size=(w, h), stride=a.stride,
606
+ keyframes_only=a.keyframes_only, log=print)
607
+ return 0
608
+
609
+
610
+ def _is_seg(d: Path) -> bool:
611
+ from nusim.render.segmap import is_segmented
612
+
613
+ return is_segmented(d)
614
+
615
+
616
+ def _render_paths(name: str, model: str | None = None, segmented: bool = False):
617
+ """๋ Œ๋”์— ์“ธ ๋ชจ๋ธ ๋””๋ ‰ํ„ฐ๋ฆฌ. ๊ธฐ๋ณธ์€ outputs/gs/<scene>_env (ํ™˜๊ฒฝ ์ „์šฉ), ๋‹ค์Œ outputs/gs/<scene>_depth (๋ผ์ด๋‹ค ๊นŠ์ด ๊ฐ๋…),
618
+ ์—†์œผ๋ฉด outputs/gs/<scene>. ์ฐจ์„  ๋ณ€๊ฒฝ ๊ฐ™์€ ์ƒˆ ์‹œ์ ์—์„œ ๊นŠ์ด ๊ฐ๋… ๋ชจ๋ธ์ด ํ™•์—ฐํžˆ ๋‚ซ๋‹ค
619
+ (docs/roadmap.md 1๋‹จ๊ณ„ ๋ชจ๋ธ ๋น„๊ต). segmented: ๊ตฌ๊ฐ„์œผ๋กœ ๋‚˜๋ˆˆ ์ผ๊ด„ ์žฌ๊ตฌ์„ฑ ์”ฌ ๋””๋ ‰ํ„ฐ๋ฆฌ(seg<k>/)๋„ ๋ฐ›๋Š”๋‹ค (render view)."""
620
+ from nusim.paths import output_dir
621
+
622
+ if model:
623
+ d = Path(model)
624
+ else:
625
+ # ํ™˜๊ฒฝ ์ „์šฉ(_env: ์ฐจ๋Ÿ‰ยท์‚ฌ๋žŒ ์ „๋ถ€ ์ง€์›€) > ์ผ๊ด„ ์žฌ๊ตฌ์„ฑ(gs_day, ํ™˜๊ฒฝ ์ „์šฉ) > ๊นŠ์ด ๊ฐ๋…(_depth) > ๊ธฐ๋ณธ ์ˆœ
626
+ cands = [output_dir() / "gs" / f"{name}_env", output_dir() / "gs_day" / name,
627
+ output_dir() / "gs" / f"{name}_depth", output_dir() / "gs" / name]
628
+ for d in cands:
629
+ if (d / "point_cloud.ply").exists() or (segmented and _is_seg(d)):
630
+ break
631
+ if not (d / "point_cloud.ply").exists() and not (segmented and _is_seg(d)):
632
+ raise FileNotFoundError(f"{d / 'point_cloud.ply'} ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋จผ์ €: nusim recon train {name}")
633
+ return d
634
+
635
+
636
+ def _ibl_gltf_args(name: str, agents: bool = False) -> list[str]:
637
+ """IBL ์ด ๊ตฌ์›Œ์ ธ ์žˆ์œผ๋ฉด ํ™˜๊ฒฝ ์กฐ๋ช…์„ ์ผ ๋‹ค.
638
+ ๋‹ค๋ฅธ ์ฐจ๋Ÿ‰(glTF)์€ ๊ธฐ๋ณธ์œผ๋กœ ๋„ฃ์ง€ ์•Š๋Š”๋‹ค โ€” ๋ Œ๋”๋ง์€ ์ฃผํ–‰ ํ™˜๊ฒฝ(๋งต)๋งŒ ๊ตฌ์„ฑํ•˜๊ณ , ์ฐจ๋Ÿ‰ยท์‚ฌ๋žŒ ๊ฐ™์€ ๊ฐ์ฒด๋Š”
639
+ ์ดํ›„ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ๊ฐ€ ๋”ฐ๋กœ ๋„ฃ๋Š” ๋ฐฉํ–ฅ์ด๋‹ค. agents=True ๋ฉด nuScenes ๋กœ๊ทธ์˜ ์›€์ง์ด๋Š” ์ฐจ ๊ถค์ ์— glTF ์ฐจ๋Ÿ‰์„ ๋†“๋Š”๋‹ค."""
640
+ from nusim.paths import output_dir, workspace_root
641
+
642
+ args = []
643
+ ibl = output_dir() / "ibl" / name
644
+ if (ibl / "env_specular.ktx2").exists():
645
+ args += ["--ibl", str(ibl)]
646
+ car = workspace_root() / "assets" / "gltf" / "CarConcept.glb"
647
+ if agents and car.exists():
648
+ args += ["--gltf", str(car)]
649
+ return args
650
+
651
+
652
+ def _write_obstacles(name: str, d: Path, origin) -> None:
653
+ """์ถฉ๋Œ์šฉ ์žฅ์• ๋ฌผ ์ง€๋„ (์ •์  ๋ผ์ด๋‹ค, ๋ชจ๋ธ๊ณผ ๊ฐ™์€ objects ์„ค์ •์œผ๋กœ ์ฐจ๋Ÿ‰ยท์‚ฌ๋žŒ ์ œ๊ฑฐ) โ†’ d/obstacles.pgm"""
654
+ from nusim.data.index import load_scene
655
+ from nusim.paths import dataset
656
+ from nusim.sim.obstacles import write_obstacles
657
+
658
+ info = write_obstacles(d / "obstacles.pgm", load_scene(name), dataset(), origin, objects=_model_objects(d))
659
+ print(f" ์žฅ์• ๋ฌผ ์ง€๋„ {info['cells']:,}์นธ ({info['res']} m; ๋…ธ๋ฉด ์•ˆ์ชฝ์—์„œ ๋น„์šด ์นธ {info['cleared_on_road']}, "
660
+ f"์›๋ž˜ ๊ฒฝ๋กœ์—์„œ ๋” ๋น„์šด ์นธ {info['carved_by_log_path']}) โ†’ {d / 'obstacles.pgm'}")
661
+
662
+
663
+ EXPORT_VERSION = 2 # ๋‚ด๋ณด๋‚ด๊ธฐ ํŒ (๋ฐ”๋€Œ๋ฉด ๋‹ค์‹œ ๋งŒ๋“ ๋‹ค). 2: ํ™˜๊ฒฝ ์ „์šฉ ๋ชจ๋ธ์˜ A ์ค„์— ์ฃผ์ฐจ ์ฐจ๋Ÿ‰
664
+
665
+
666
+ def _model_stamp(d: Path) -> str:
667
+ """๋ชจ๋ธ์ด ๋ฐ”๋€Œ์—ˆ๋Š”์ง€ โ€” ply ์™€ ์™ธ๋ถ€ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณด์ •์˜ ์ˆ˜์ • ์‹œ๊ฐ."""
668
+ parts = [str(int((d / "point_cloud.ply").stat().st_mtime))]
669
+ if (d / "camera_adjust.json").exists():
670
+ parts.append(str(int((d / "camera_adjust.json").stat().st_mtime)))
671
+ return ":".join(parts)
672
+
673
+
674
+ def _ensure_export(name: str, d: Path, downscale: int, margin: float = 60.0, force: bool = False) -> None:
675
+ """๋ทฐ์–ด๊ฐ€ ์ฝ๋Š” ํŒŒ์ผ(rig.txt, map.txt, drivable.txt, obstacles.pgm)์„ ํ•„์š”ํ•œ ๊ฒƒ๋งŒ ๋‹ค์‹œ ๋งŒ๋“ ๋‹ค.
676
+ ์˜ˆ์ „์—๋Š” ํŒŒ์ผ์ด ์žˆ๊ธฐ๋งŒ ํ•˜๋ฉด ๊ทธ๋Œ€๋กœ ์จ์„œ, ๋ชจ๋ธ์„ ๋‹ค์‹œ ํ•™์Šตํ•˜๊ฑฐ๋‚˜ --downscaleยทobjectsยท์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๋ฐ”๊ฟ”๋„
677
+ ๋‚ก์€ ๋ฆฌ๊ทธยท์ง€๋„๋กœ ๊ทธ๋ ธ๋‹ค (2026-10-02 ๋ฆฌ๋ทฐ). ๋ฌด์—‡์œผ๋กœ ๋งŒ๋“ค์—ˆ๋Š”์ง€ export.json ์— ๋‚จ๊ธฐ๊ณ  ๋น„๊ตํ•œ๋‹ค."""
678
+ import json
679
+
680
+ from nusim.data.index import load_scene
681
+ from nusim.io.ply import read_origin
682
+ from nusim.render.export import load_camera_adjust, write_drivable, write_map_lines, write_rig
683
+ from nusim.sim.obstacles import OBSTACLES_VERSION
684
+ from nusim.sim.voxels import VOXELS_VERSION
685
+
686
+ state_path = d / "export.json"
687
+ state = json.loads(state_path.read_text()) if state_path.exists() and not force else {}
688
+ stamp = _model_stamp(d)
689
+ want = {
690
+ "rig": {"model": stamp, "downscale": downscale, "version": EXPORT_VERSION},
691
+ "map": {"model": stamp, "version": EXPORT_VERSION, "margin": margin},
692
+ "obstacles": {"model": stamp, "version": OBSTACLES_VERSION, "objects": _model_objects(d)},
693
+ "voxels": {"model": stamp, "version": VOXELS_VERSION, "objects": _model_objects(d)},
694
+ }
695
+ files = {"rig": ["rig.txt"], "map": ["map.txt", "drivable.txt"], "obstacles": ["obstacles.pgm"],
696
+ "voxels": ["world.vox"]}
697
+ todo = [k for k in want if state.get(k) != want[k] or not all((d / f).exists() for f in files[k])]
698
+ if not todo:
699
+ return
700
+ scene, _ = _model_data(name, d) # ๊ตฌ๊ฐ„ ๋ชจ๋ธ(--time-window)์ด๋ฉด ๊ทธ ํ•™์Šต ๊ตฌ๊ฐ„๋งŒ ์žฌ์ƒยทํ‘œ์‹œ
701
+ origin = read_origin(d / "point_cloud.ply")
702
+ if "rig" in todo:
703
+ adjust = load_camera_adjust(d)
704
+ n = write_rig(d / "rig.txt", scene, origin, adjust, downscale=downscale, objects=_model_objects(d))
705
+ print(f" ์นด๋ฉ”๋ผ ๋ฆฌ๊ทธ {n}ํ”„๋ ˆ์ž„ ร— 6์บ  โ†’ {d / 'rig.txt'}" + (" (์™ธ๋ถ€ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณด์ • ๋ฐ˜์˜)" if adjust else ""))
706
+ if "map" in todo:
707
+ counts = write_map_lines(d / "map.txt", scene, origin, margin=margin)
708
+ n_poly = write_drivable(d / "drivable.txt", scene, origin)
709
+ print(f" ์ฃผํ–‰๊ฐ€๋Šฅ์˜์—ญ {n_poly}๊ฐœ ํด๋ฆฌ๊ณค โ†’ {d / 'drivable.txt'}")
710
+ print(f" ๋งต ์„  โ†’ {d / 'map.txt'}")
711
+ for k, v in counts.items():
712
+ print(f" {k:<16} {v:>5}")
713
+ if "obstacles" in todo:
714
+ _write_obstacles(name, d, origin)
715
+ if "voxels" in todo: # ๋ผ์ด๋‹คยท๋ ˆ์ด๋” ๋ ˆ์ด์บ์ŠคํŠธ์šฉ (nusim/sim/voxels.py)
716
+ from nusim.paths import dataset
717
+ from nusim.sim.voxels import write_voxels
718
+
719
+ info = write_voxels(d / "world.vox", scene, dataset(), origin, objects=_model_objects(d))
720
+ print(f" ๋ณต์…€ ์›”๋“œ {info['occupied']:,}์นธ ({info['res']} m, {info['mb']} MB) โ†’ {d / 'world.vox'}")
721
+ state.update({k: want[k] for k in todo})
722
+ state_path.write_text(json.dumps(state, indent=2))
723
+
724
+
725
+ def _cmd_render_export(a: argparse.Namespace) -> int:
726
+ d = _render_paths(a.name, getattr(a, "model", None))
727
+ _ensure_export(a.name, d, a.downscale, margin=a.margin, force=True)
728
+ return 0
729
+
730
+
731
+ def _cmd_render_compare(a: argparse.Namespace) -> int:
732
+ import json
733
+
734
+ from nusim.data.index import load_scene
735
+ from nusim.io.ply import read_origin
736
+ from nusim.paths import dataset
737
+ from nusim.recon.dataset import build_views
738
+ from nusim.render.compare import compare, run_offscreen
739
+ from nusim.render.export import load_camera_adjust, write_views
740
+
741
+ d = _render_paths(a.name, getattr(a, "model", None))
742
+ origin = read_origin(d / "point_cloud.ply")
743
+ vk = _model_views(d)
744
+ scene, ds = _model_data(a.name, d)
745
+ views = build_views(scene, ds, origin, load_images=False, mask_dynamic=False,
746
+ frame_stride=vk["frame_stride"], keyframes_only=vk["keyframes_only"]).test
747
+ views = views[:: max(1, len(views) // a.n)][: a.n]
748
+ adjust = load_camera_adjust(d)
749
+ write_views(d / "test_views.txt", views, adjust)
750
+ vk_dir = d / "vk_test"
751
+ for old in vk_dir.glob("*.png"):
752
+ old.unlink()
753
+ print(run_offscreen(d / "point_cloud.ply", d / "test_views.txt", vk_dir).rstrip())
754
+ res = compare(d, views, vk_dir, adjust)
755
+ (d / "vk_compare.json").write_text(json.dumps(res, indent=2))
756
+ ok = res["mean_abs_diff_pct"] < 2.0
757
+ print(f"\n Vulkan vs gsplat ({res['n_views']}๋ทฐ): PSNR {res['psnr_vk_vs_gsplat']:.2f} dB "
758
+ f"ํ‰๊ท  ์ฐจ์ด {res['mean_abs_diff_pct']:.3f}% 2% ๋„˜๋Š” ํ”ฝ์…€ {res['pixels_over_2pct']:.2f}% "
759
+ f"์ตœ์•… ๋ทฐ {res['worst_view_psnr']:.2f} dB โ†’ {'ํ•ฉ๊ฒฉ' if ok else '๋ถˆํ•ฉ๊ฒฉ'} (๊ธฐ์ค€: ํ‰๊ท  ์ฐจ์ด < 2%)")
760
+ return 0 if ok else 1
761
+
762
+
763
+ def _cmd_render_view(a: argparse.Namespace) -> int:
764
+ import os
765
+
766
+ from nusim.render.compare import viewer_binary
767
+
768
+ d = _render_paths(a.name, getattr(a, "model", None), segmented=True)
769
+ sky_dir = d
770
+ if _is_seg(d): # ์ผ๊ด„ ์žฌ๊ตฌ์„ฑ์ด ๊ตฌ๊ฐ„์œผ๋กœ ๋‚˜๋ˆˆ ์”ฌ โ€” ๊ตฌ๊ฐ„์„ ์ด์–ด์„œ (์ž์ฐจ ์œ„์น˜์˜ ๊ตฌ๊ฐ„์„ ๊ทธ๋ฆฐ๋‹ค)
771
+ from nusim.render.segmap import write_segment_map
772
+
773
+ segs = write_segment_map(a.name, d, log=print)
774
+ ply, sky_dir = segs[0][1] / "point_cloud.ply", segs[0][1]
775
+ from nusim.aug.map_aug import ensure_ibl
776
+
777
+ ensure_ibl(a.name, segs, log=print) # glTF ์ž์ฐจ์˜ ์กฐ๋ช… โ€” ์—†์œผ๋ฉด 3DGS ์ž์ฐจ ์—์…‹์œผ๋กœ ๋Œ€์‹ ํ•ด ๊ฒ€์€ ๋ฉ์–ด๋ฆฌ๊ฐ€ ๋‚˜์™”๋‹ค
778
+ seg_args = ["--segments", str(d / "segments.txt")]
779
+ model_dirs = [m for _, m in segs]
780
+ else:
781
+ _ensure_export(a.name, d, a.downscale)
782
+ _ensure_sky_label(a.name, d)
783
+ ply, seg_args, model_dirs = d / "point_cloud.ply", [], [d]
784
+ from nusim.recon.semantic import ensure_semantic
785
+
786
+ ensure_semantic(a.name, model_dirs, log=print) # ์—†์œผ๋ฉด ์‹œ๋งจํ‹ฑ ์นด๋ฉ”๋ผ(F3)๊ฐ€ ํ•˜๋Š˜๋งŒ ํŒŒ๋ž—๊ณ  ๋‚˜๋จธ์ง€๋Š” ๊ฒ€๋‹ค
787
+ cmd = [str(viewer_binary()), str(ply.resolve()), "--cams", str(d / "rig.txt"),
788
+ "--lines", str(d / "map.txt"), "--shot-dir", str(d / "shots"), *seg_args]
789
+ if (d / "drivable.txt").exists():
790
+ cmd += ["--drivable", str(d / "drivable.txt")] # ์ง์ ‘ ์šด์ „(G) ๋•Œ ๋„๋กœ ์ดํƒˆ ํŒ์ • + ์—ฐ์„ ์ถฉ๋Œ
791
+ cmd += ["--obstacles", str(d / "obstacles.pgm")] # ๊ฑด๋ฌผยท๊ธฐ๋‘ฅยท๋ฒฝ ์ถฉ๋Œ
792
+ if (d / "objects" / "objects.json").exists(): # ์ฐจ๋Ÿ‰์„ ํ•จ๊ป˜ ํ•™์Šตํ•œ ๋ชจ๋ธ โ€” ์ฐจ๋Ÿ‰ ์—์…‹์„ ๊ฐ์ž์˜ ๊ถค์ ์œผ๋กœ (K: ํŽธ์ง‘)
793
+ cmd += ["--objects", str(d / "objects")]
794
+ from nusim.paths import output_dir, workspace_root
795
+
796
+ cmd += _ibl_gltf_args(a.name, agents=a.agents)
797
+ if (sky_dir / "sky.ktx2").exists():
798
+ cmd += ["--sky", str(sky_dir / "sky.ktx2")]
799
+ if (d / "world.vox").exists():
800
+ cmd += ["--voxels", str(d / "world.vox")] # ๋ผ์ด๋‹คยท๋ ˆ์ด๋” (configs/sim.json sensors, ์ฐฝ์˜ '์„ผ์„œ' ํŒจ๋„)
801
+ from nusim.config import sim_config, sim_config_path
802
+
803
+ if sim_config_path().exists():
804
+ cmd += ["--config", str(sim_config_path())] # ๋‚ ์”จ ํ”„๋ฆฌ์…‹ยทํ•˜๋Š˜ยท3์ธ์นญ ์นด๋ฉ”๋ผยท์ถฉ๋Œ (configs/sim.json)
805
+ ego_cfg = sim_config().get("ego", {})
806
+ ego_gltf = workspace_root() / ego_cfg.get("gltf", "assets/gltf/ego_car.glb")
807
+ ego = workspace_root() / ego_cfg.get("fallback_3dgs_asset", "outputs/assets/ego_car.ply")
808
+ if ego_gltf.exists() and "--ibl" in cmd:
809
+ cmd += ["--ego-gltf", str(ego_gltf)] # ์ž์ฐจ = ๋จธํ‹ฐ๋ฆฌ์–ผ ์žˆ๋Š” ์ฐจ๋Ÿ‰ ๋ฉ”์‹œ (PBR + IBL)
810
+ if "length_m" in ego_cfg:
811
+ cmd += ["--ego-length", str(float(ego_cfg["length_m"]))]
812
+ elif ego.exists():
813
+ cmd += ["--ego-asset", str(ego)] # ์—†์œผ๋ฉด nuScenes ์ฃผ์ฐจ ์ฐจ๋Ÿ‰ 3DGS ์—์…‹
814
+ if a.autoshot:
815
+ cmd += ["--autoshot", str(a.autoshot)]
816
+ elif not a.replay:
817
+ cmd += ["--drive"] # ์šด์ „ ๋ชจ๋“œ(์ฐจ ๋’ค ์‹œ์ )๋กœ ์‹œ์ž‘. G ๋ฅผ ๋ˆ„๋ฅด๋ฉด ๋กœ๊ทธ ์žฌ์ƒ(์ฐจ์— ๋‹ฌ๋ฆฐ ์นด๋ฉ”๋ผ ์‹œ์ )
818
+ if a.weather:
819
+ cmd += ["--weather", a.weather] # ํ•ฉ์„ฑ ๋‚ ์”จ ์‹œ์ž‘๊ฐ’ โ€” ์ฐฝ ์•ˆ ์Šฌ๋ผ์ด๋”(F1)๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค
820
+ os.execv(cmd[0], cmd)
821
+
822
+
823
+ def _cmd_render_map(a: argparse.Namespace) -> int:
824
+ import os
825
+
826
+ from nusim.config import sim_config, sim_config_path
827
+ from nusim.paths import output_dir, workspace_root
828
+ from nusim.render.compare import viewer_binary
829
+ from nusim.render.regionmap import write_region_map
830
+
831
+ root = Path(a.root) if a.root else output_dir() / "gs_day"
832
+ out = root / "_map" / a.location
833
+ if not a.no_sensors: # ์—†๋Š” ์”ฌ ๊ฒƒ๋งŒ (--sensors: ํ•™์Šต๋œ ๊ตฌ๊ฐ„์ด ๋ฐ”๋€ ์”ฌ๋„ ๋‹ค์‹œ)
834
+ from nusim.render.regionmap import build_sensor_worlds
835
+
836
+ n = build_sensor_worlds(a.location, root, log=print, stale=a.sensors)
837
+ if n:
838
+ print(f" ์„ผ์„œ ๋ณต์…€ ์›”๋“œ {n}๊ฐœ ์”ฌ ์ƒˆ๋กœ ๋งŒ๋“ฆ")
839
+ info = write_region_map(a.location, root, out, start=a.start, log=print)
840
+ if a.no_view:
841
+ return 0
842
+ from nusim.recon.semantic import ensure_semantic
843
+ from nusim.render.regionmap import trained_scenes
844
+
845
+ for s, segs in ([] if a.no_semantic else trained_scenes(root, a.location)): # ์‹œ๋งจํ‹ฑ ์นด๋ฉ”๋ผ(F3) ๋ผ๋ฒจ โ€” ์ƒˆ๋กœ ํ•™์Šต๋œ ๊ตฌ๊ฐ„๋งŒ (๋ชจ๋ธ๋‹น ์ˆ˜ ์ดˆ)
846
+ ensure_semantic(s, [m for _, m in segs], log=print)
847
+ from nusim.aug.map_aug import ensure_ibl
848
+ from nusim.recon.batch import segment_dirs
849
+
850
+ ensure_ibl(info["anchor"], segment_dirs(root, info["anchor"]), log=print) # ์—†์œผ๋ฉด glTF ์ž์ฐจ ๋Œ€์‹  3DGS ์ž์ฐจ(๊ฒ€์€ ๋ฉ์–ด๋ฆฌ)๊ฐ€ ๋‚˜์˜จ๋‹ค
851
+ cmd = [str(viewer_binary()), info["first_ply"], "--cams", str(out / "rig.txt"), "--segments", str(out / "segments.txt"),
852
+ "--lines", str(out / "map.txt"), "--drivable", str(out / "drivable.txt"), "--shot-dir", str(out / "shots")]
853
+ cmd += _ibl_gltf_args(info["anchor"])
854
+ if Path(info["sky"]).exists():
855
+ cmd += ["--sky", info["sky"]]
856
+ if sim_config_path().exists():
857
+ cmd += ["--config", str(sim_config_path())]
858
+ ego_cfg = sim_config().get("ego", {})
859
+ ego_gltf = workspace_root() / ego_cfg.get("gltf", "assets/gltf/ego_car.glb")
860
+ ego = workspace_root() / ego_cfg.get("fallback_3dgs_asset", "outputs/assets/ego_car.ply")
861
+ if ego_gltf.exists() and "--ibl" in cmd:
862
+ cmd += ["--ego-gltf", str(ego_gltf)]
863
+ if "length_m" in ego_cfg:
864
+ cmd += ["--ego-length", str(float(ego_cfg["length_m"]))]
865
+ elif ego.exists():
866
+ cmd += ["--ego-asset", str(ego)]
867
+ if not a.replay:
868
+ cmd += ["--drive"]
869
+ cmd += a.viewer_args # nusim render map <์ง€์—ญ> -- --autopilot ... (๋ทฐ์–ด ์˜ต์…˜ ๊ทธ๋Œ€๋กœ)
870
+ print(f" ์ง€์—ญ ๋งต {info['location']}: ์”ฌ {info['scenes']}๊ฐœ ยท ๊ตฌ๊ฐ„ {info['segments']}๊ฐœ โ€” ์„ผ์„œ ๋ณต์…€ ์›”๋“œ "
871
+ f"{info['sensor_worlds']}/{info['scenes']}์”ฌ"
872
+ + ("" if info["sensor_worlds"] == info["scenes"] else " (๋‚˜๋จธ์ง€๋Š” ์„ผ์„œ ๊บผ์ง โ€” --no-sensors ๋ฅผ ๋นผ๊ณ  ๋‹ค์‹œ)")
873
+ + " ยท ์žฅ์• ๋ฌผ ์ง€๋„๋Š” ์•„์ง ์ง€์—ญ ๋‹จ์œ„๋กœ ์—†์Œ (์—ฐ์„ ์ถฉ๋Œ๋งŒ)")
874
+ os.execv(cmd[0], cmd)
875
+
876
+
877
+ def _cmd_render_encode(a: argparse.Namespace) -> int:
878
+ """PNG ํ”„๋ ˆ์ž„ ๋””๋ ‰ํ„ฐ๋ฆฌ (vkgs_viewer --record-screen / --record) โ†’ mp4. ์™ผ์ชฝ ์•„๋ž˜์— ์„ค๋ช… ๊ธ€์ž๋ฅผ ์–น๋Š”๋‹ค."""
879
+ import imageio.v2 as imageio
880
+ import numpy as np
881
+ from PIL import Image, ImageDraw, ImageFont
882
+
883
+ frames = sorted(Path(a.frames).glob("frame_*.png"))
884
+ if not frames:
885
+ raise SystemExit(f"{a.frames} ์— frame_*.png ๊ฐ€ ์—†๋‹ค")
886
+ font = None
887
+ for f in ("/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf", "/usr/share/fonts/truetype/nanum/NanumSquareRoundB.ttf"):
888
+ if Path(f).exists():
889
+ font = ImageFont.truetype(f, a.font_size)
890
+ break
891
+ out = Path(a.out)
892
+ out.parent.mkdir(parents=True, exist_ok=True)
893
+ with imageio.get_writer(out, fps=a.fps, codec="libx264", quality=None, bitrate=a.bitrate, macro_block_size=1,
894
+ pixelformat="yuv420p") as w:
895
+ for fp in frames:
896
+ im = Image.open(fp).convert("RGB")
897
+ if a.width and im.width != a.width:
898
+ im = im.resize((a.width, round(im.height * a.width / im.width / 2) * 2), Image.LANCZOS)
899
+ if a.caption:
900
+ d = ImageDraw.Draw(im, "RGBA")
901
+ x0, y0, x1, y1 = d.textbbox((0, 0), a.caption, font=font)
902
+ m = 10
903
+ d.rectangle((m, im.height - m - (y1 - y0) - 14, m + x1 - x0 + 16, im.height - m), fill=(0, 0, 0, 160))
904
+ d.text((m + 8 - x0, im.height - m - (y1 - y0) - 7 - y0), a.caption, font=font, fill=(255, 255, 255, 255))
905
+ w.append_data(np.asarray(im))
906
+ if a.poster:
907
+ Image.open(frames[len(frames) // 2]).convert("RGB").save(a.poster, quality=90)
908
+ print(f" {len(frames)}ํ”„๋ ˆ์ž„ โ†’ {out} ({out.stat().st_size / 2**20:.1f} MB)")
909
+ return 0
910
+
911
+
912
+ def _cmd_render_video(a: argparse.Namespace) -> int:
913
+ from nusim.render.video import compose, render_frames, write_video
914
+
915
+ d = _render_paths(a.name, getattr(a, "model", None))
916
+ _ensure_export(a.name, d, 1)
917
+ _ensure_sky_label(a.name, d)
918
+ fdir = d / "sim_frames"
919
+ print(render_frames(d, fdir, a.frames, bev_height=a.bev_height).rstrip())
920
+ out = d / f"{a.name}_sim.mp4"
921
+ n = write_video(fdir, out, a.name, fps=a.fps)
922
+ # ์ฒซ ํ”„๋ ˆ์ž„ ์ •์ง€ ์ด๋ฏธ์ง€๋„ ๋‚จ๊ธด๋‹ค (์˜์ƒ์„ ๋ชป ์—ฌ๋Š” ๊ณณ์—์„œ ๋ณด๋ ค๊ณ )
923
+ from PIL import Image
924
+
925
+ first = sorted(fdir.glob("f*_BEV.png"))[len(sorted(fdir.glob("f*_BEV.png"))) // 2]
926
+ Image.fromarray(compose(fdir, int(first.name[1:5]), a.name)).save(d / f"{a.name}_sim.jpg", quality=92)
927
+ print(f" ์˜์ƒ {n}ํ”„๋ ˆ์ž„ โ†’ {out}\n ์ •์ง€ ์ด๋ฏธ์ง€ โ†’ {d / (a.name + '_sim.jpg')}")
928
+ return 0
929
+
930
+
931
+ def _cmd_recon_distill(a: argparse.Namespace) -> int:
932
+ from nusim.data.index import load_scene
933
+ from nusim.io.ply import read_ply
934
+ from nusim.paths import dataset, output_dir
935
+ from nusim.recon.dataset import build_views
936
+ from nusim.recon.distill import make_pseudo_views
937
+ from nusim.recon.train import TrainConfig, train
938
+
939
+ base = Path(a.base) if a.base else output_dir() / "gs" / a.name
940
+ out = Path(a.out) if a.out else base.parent / f"{base.name}_difix"
941
+ scene, ds = _model_data(a.name, base)
942
+ if (base / "sky.pt").exists():
943
+ # ํ•˜๋Š˜ ํ๋ธŒ๋งต์„ ์ด์–ด๋ฐ›๋Š” ๋ฏธ์„ธ์กฐ์ •์€ ์•„์ง ์—†๋‹ค. ๊ทธ๋ƒฅ ๋Œ๋ฆฌ๋ฉด ํ•˜๋Š˜์ด ๋น ์ง„ ๋ชจ๋ธ(ํ•˜๋Š˜ ๊ตฌ ๊ฐ€์šฐ์‹œ์•ˆ๋„ ์—†์Œ)์ด
944
+ # ์ƒ๊ธฐ๊ณ , ์˜์‚ฌ ๋ทฐ์˜ ๊ฒ€์€ ํ•˜๋Š˜์„ Difix ๊ฐ€ ๊ณ ์นœ ๊ฑธ ๊ฐ€์šฐ์‹œ์•ˆ์ด ๋ฐฐ์›Œ ํ•˜๋Š˜ floater ๊ฐ€ ๋œ๋‹ค.
945
+ print(f"์˜ค๋ฅ˜: {base} ๋Š” ํ•˜๋Š˜์„ ํ๋ธŒ๋งต์œผ๋กœ ๋ถ„๋ฆฌํ•œ ๋ชจ๋ธ(sky.pt) โ€” distill ์€ ์•„์ง ํ•˜๋Š˜ ๋ถ„๋ฆฌ ๋ชจ๋ธ์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
946
+ return 1
947
+ g = read_ply(base / "point_cloud.ply")
948
+ print(f"\033[1m{a.name}\033[0m Difix ์˜์‚ฌ ์ •๋‹ต โ†’ 3DGS ๋ฏธ์„ธ์กฐ์ • ({base} โ†’ {out})")
949
+ objects = _model_objects(base)
950
+ vk = _model_views(base)
951
+ views = build_views(scene, ds, g.origin, downscale=vk["downscale"], frame_stride=vk["frame_stride"],
952
+ keyframes_only=vk["keyframes_only"], objects=objects)
953
+ if a.depth_lambda > 0: # ๋ฏธ์„ธ์กฐ์ • ์ค‘์—๋„ ๊ธฐํ•˜๋ฅผ ๋ถ™์žก๋Š”๋‹ค (์˜์‚ฌ ๋ทฐ์—๋Š” ๊นŠ์ด๊ฐ€ ์—†๋‹ค)
954
+ from nusim.recon.depth import lidar_depth_maps
955
+ from nusim.recon.init import InitConfig, accumulate_lidar
956
+
957
+ import torch
958
+
959
+ from nusim.recon.train import CameraAdjust, _refresh_depth
960
+
961
+ pts = accumulate_lidar(scene, ds, InitConfig(objects=objects)) - g.origin
962
+ adj = CameraAdjust(6).cuda() # ๋ Œ๋”์™€ ๊ฐ™์€ (๋ณด์ •๋œ) ์นด๋ฉ”๋ผ๋กœ ๊นŠ์ด ์ง€๋„๋ฅผ ๋งŒ๋“ ๋‹ค
963
+ if (base / "camera_adjust.pt").exists():
964
+ adj.load_state_dict(torch.load(base / "camera_adjust.pt", weights_only=True))
965
+ _refresh_depth(views.train, adj, pts, 4)
966
+ del adj
967
+
968
+ def cfg_for(iters: int):
969
+ return TrainConfig(depth_lambda=a.depth_lambda,
970
+ iters=iters, sh_degree_every=0, lr_camera=0.0,
971
+ lr_means=1.6e-5, lr_means_final_ratio=0.1, # ์ด๋ฏธ ์ˆ˜๋ ดํ•œ ์œ„์น˜๋ฅผ ํฌ๊ฒŒ ํ”๋“ค์ง€ ์•Š๋Š”๋‹ค
972
+ refine_start=500, refine_stop=iters // 2, reset_every=10**9, # ๋ถˆํˆฌ๋ช…๋„ ๋ฆฌ์…‹ ์—†์Œ (step 0 ์€ train ์ด ๊ฑด๋„ˆ๋œ€)
973
+ max_gaussians=a.max_gaussians, eval_every=a.eval_every, objects=objects,
974
+ downscale=vk["downscale"], frame_stride=vk["frame_stride"], keyframes_only=vk["keyframes_only"],
975
+ pseudo_prob=a.pseudo_prob, pseudo_weight=a.pseudo_weight)
976
+
977
+ def pseudo_from(model: Path, shifts):
978
+ # ์˜์‚ฌ ๋ทฐ ์บ์‹œ๋Š” ๊ธฐ๋ฐ˜ ๋ชจ๋ธยท๊ฐ€๋ฆผ ์„ค์ •ยท๊ฐ„๊ฒฉ๋งˆ๋‹ค ๋”ฐ๋กœ โ€” ์žฌํ•™์Šตํ•œ ๋ชจ๋ธ์— ๋‚ก์€ Difix ๊ฒฐ๊ณผ๋ฅผ ์“ฐ์ง€ ์•Š๊ฒŒ
979
+ stamp = f"{objects}_every{a.every}_{int((model / 'point_cloud.ply').stat().st_mtime)}"
980
+ return make_pseudo_views(model, scene, views, model / "pseudo" / stamp, shifts, a.every, objects=objects,
981
+ downscale=vk["downscale"])
982
+
983
+ if not a.progressive:
984
+ pseudo = pseudo_from(base, tuple(float(x) for x in a.shifts.split(",")))
985
+ train(views, g, out, cfg_for(a.iters), resume_dir=base, pseudo=pseudo)
986
+ return 0
987
+
988
+ # ์ ์ง„ ์ฆ๋ฅ˜ (Difix3D+): ๊ฐ€๊นŒ์šด ์ด๋™๋ถ€ํ„ฐ. ๋‹จ๊ณ„๋งˆ๋‹ค ์ง์ „ ๋‹จ๊ณ„ ๋ชจ๋ธ๋กœ ยฑs ์‹œ์ ์„ ๋ Œ๋” โ†’ Difix โ†’ ์˜์‚ฌ ๋ทฐ๋ฅผ ์Œ“๊ณ  ์งง๊ฒŒ
989
+ # ๋ฏธ์„ธ์กฐ์ •. ์ถœ๋ฐœ์ ์ด ์ด๋ฏธ ์ข‹์•„์ ธ์„œ Difix ๊ฒฐ๊ณผ๊ฐ€ ์‹œ์ ๋งˆ๋‹ค ์ผ๊ด€๋˜๊ณ , ํ•œ ๋ฒˆ์— 3 m ๋ฅผ ๊ณ ์น  ๋•Œ์˜ ํ๋ ค์ง์„ ์ค„์ธ๋‹ค.
990
+ steps = [float(x) for x in a.progressive.split(",")]
991
+ pseudo, cur = [], base
992
+ for k, s in enumerate(steps):
993
+ print(f"\n โ”€โ”€ ์ ์ง„ {k + 1}/{len(steps)}: ยฑ{s:g} m (์ถœ๋ฐœ {cur})")
994
+ pseudo += pseudo_from(cur, (-s, s))
995
+ stage = out / f"stage{k + 1}_{s:g}m" if k < len(steps) - 1 else out
996
+ g_cur = read_ply(cur / "point_cloud.ply")
997
+ train(views, g_cur, stage, cfg_for(a.iters_per_stage), resume_dir=cur, pseudo=pseudo)
998
+ cur = stage
999
+ return 0
1000
+
1001
+
1002
+ def _cmd_asset_scan(a: argparse.Namespace) -> int:
1003
+ from nusim.data.index import iter_scenes
1004
+ from nusim.recon.asset import scan_parked_cars
1005
+
1006
+ rows = scan_parked_cars(iter_scenes(), max_dist=a.max_dist)
1007
+ print(f"{'๋ฐฉ์œ„(/8)':>8} {'์ตœ์†Œ๊ฑฐ๋ฆฌ':>8} {'scene':<12} {'inst':>4} ํฌ๊ธฐ (w,l,h)")
1008
+ for r in rows[: a.limit]:
1009
+ print(f"{r[0]:>8} {r[1]:>7.1f}m {r[2]:<12} {r[3]:>4} {r[4]}")
1010
+ return 0
1011
+
1012
+
1013
+ def _cmd_asset_extract(a: argparse.Namespace) -> int:
1014
+ from nusim.data.index import load_scene
1015
+ from nusim.io.ply import read_ply, write_ply
1016
+ from nusim.paths import output_dir
1017
+ from nusim.recon.asset import extract_vehicle
1018
+
1019
+ model = Path(a.model) if a.model else output_dir() / "gs" / f"{a.name}_depth"
1020
+ g = extract_vehicle(read_ply(model / "point_cloud.ply"), load_scene(a.name), a.instance, mirror=not a.no_mirror)
1021
+ out = Path(a.out) if a.out else output_dir() / "assets" / f"car_{a.name}_{a.instance}.ply"
1022
+ write_ply(out, g)
1023
+ print(f" ์—์…‹ ๊ฐ€์šฐ์‹œ์•ˆ {len(g):,}๊ฐœ โ†’ {out}")
1024
+ if a.as_ego:
1025
+ ego = output_dir() / "assets" / "ego_car.ply"
1026
+ ego.unlink(missing_ok=True)
1027
+ ego.symlink_to(out.name)
1028
+ print(f" ์ž์ฐจ ์—์…‹์œผ๋กœ ์ง€์ • โ†’ {ego} (nusim render view ๊ฐ€ ์ž๋™์œผ๋กœ ์“ด๋‹ค)")
1029
+ return 0
1030
+
1031
+
1032
+ def _cmd_render_ibl(a: argparse.Namespace) -> int:
1033
+ from nusim.data.index import load_scene
1034
+ from nusim.paths import output_dir
1035
+ from nusim.render.ibl import bake_ibl
1036
+
1037
+ d = _render_paths(a.name, a.model)
1038
+ out = output_dir() / "ibl" / a.name
1039
+ print(f"\033[1m{a.name}\033[0m IBL ๊ตฝ๊ธฐ (๋ชจ๋ธ {d})")
1040
+ bake_ibl(load_scene(a.name), d, out, frame=a.frame, size=a.size, samples=a.samples, fill=a.fill)
1041
+ return 0
1042
+
1043
+
1044
+ def _cmd_doctor(a: argparse.Namespace) -> int:
1045
+ from nusim.cli.doctor import main as doctor_main
1046
+ return doctor_main()
1047
+
1048
+
1049
+
1050
+ # ------------------------------------------------------------------ closed-loop ํ‰๊ฐ€ (nusim.sim.closed_loop, HUGSIM ์‹)
1051
+ def _cmd_sim_run(a: argparse.Namespace) -> int:
1052
+ from nusim.paths import output_dir
1053
+ from nusim.sim.closed_loop import run_episode
1054
+ from nusim.sim.policies import make_policy
1055
+ from nusim.sim.scenario import Scenario, closed_loop_config
1056
+
1057
+ scn = Scenario.load(a.scenario)
1058
+ over = dict(scn.overrides)
1059
+ if a.size:
1060
+ w, h = a.size.lower().split("x")
1061
+ over.setdefault("render", {}).update(width=int(w), height=int(h))
1062
+ if a.max_duration:
1063
+ scn.max_duration_s = a.max_duration
1064
+ cfg = closed_loop_config(over)
1065
+ out = Path(a.out) if a.out else output_dir() / "closed_loop" / scn.name / a.policy
1066
+ print(f" ์‹œ๋‚˜๋ฆฌ์˜ค {scn.name} ({scn.difficulty}, {scn.scene}) ยท ์ •์ฑ… {a.policy} โ†’ {out}")
1067
+ res = run_episode(scn, make_policy(a.policy), out, cfg, save_front=not a.no_front, video=not a.no_video)
1068
+ return 0 if res["hd_score"] >= 0 else 1
1069
+
1070
+
1071
+ def _cmd_sim_score(a: argparse.Namespace) -> int:
1072
+ import json
1073
+
1074
+ from nusim.sim.hdscore import format_score, score_episode
1075
+
1076
+ for ep in a.episodes:
1077
+ res = score_episode(Path(ep))
1078
+ print(f" {ep} [{res.get('scenario')} ยท {res.get('policy')} ยท {res.get('termination')}]")
1079
+ print(f" {format_score(res)}")
1080
+ if a.verbose:
1081
+ for p in res["per_step"]:
1082
+ print(f" t={p['t']:5.1f} NC {p['nc']:.1f} DAC {p['dac']:.0f} TTC {p['ttc']:.0f} COM {p['com']:.0f}"
1083
+ f" โ†’ {p['hd']:.3f}" + (f" (์Šน์ฐจ๊ฐ: {','.join(p['com_fail'])})" if p["com_fail"] else ""))
1084
+ if a.json:
1085
+ print(json.dumps({k: v for k, v in res.items() if k != "per_step"}, indent=1))
1086
+ return 0
1087
+
1088
+
1089
+ def _cmd_sim_make_scenarios(a: argparse.Namespace) -> int:
1090
+ import numpy as np
1091
+
1092
+ from nusim.data.index import load_scene
1093
+ from nusim.paths import output_dir, workspace_root
1094
+ from nusim.recon.batch import segment_dirs
1095
+ from nusim.sim.scenario import closed_loop_config, scenarios_dir, write_scenario
1096
+
1097
+ cfg = closed_loop_config()["scenarios"]
1098
+ root = Path(a.root) if a.root else output_dir() / "gs_day"
1099
+ names = a.scenes or sorted(p.name for p in root.iterdir() if p.is_dir() and p.name.startswith("scene-"))
1100
+ out_dir = Path(a.out) if a.out else scenarios_dir()
1101
+ n_ok = 0
1102
+ for name in names:
1103
+ segs = segment_dirs(root, name)
1104
+ if not segs or len(segs) < segs[0][0].get("n", 1):
1105
+ print(f" {name}: ๊ฑด๋„ˆ๋œ€ โ€” ๊ตฌ๊ฐ„ {len(segs)}/{segs[0][0].get('n', 1) if segs else '?'}๊ฐœ๋งŒ ํ•™์Šต๋จ")
1106
+ continue
1107
+ fr = load_scene(name).frames["CAM_FRONT"]
1108
+ xy = np.array([f.ego_translation[:2] for f in fr])
1109
+ ts = np.array([f.timestamp for f in fr]) * 1e-6
1110
+ dist = float(np.linalg.norm(np.diff(xy, axis=0), axis=1).sum())
1111
+ route_m = float(a.route_m if a.route_m is not None else cfg["route_m"])
1112
+ use_m = min(dist, route_m) if route_m > 0 else dist
1113
+ if use_m < cfg["min_route_m"]:
1114
+ print(f" {name}: ๊ฑด๋„ˆ๋œ€ โ€” ๊ฒฝ๋กœ {dist:.1f} m < {cfg['min_route_m']} m (์ •์ฐจ ์”ฌ)")
1115
+ continue
1116
+ dur = float(ts[-1] - ts[0]) * (use_m / max(dist, 1e-6))
1117
+ model = root / name
1118
+ try:
1119
+ model = model.relative_to(workspace_root())
1120
+ except ValueError:
1121
+ pass
1122
+ d = {"name": f"{name}_easy", "difficulty": "easy", "scene": name, "model": str(model),
1123
+ "start": {"frame": 0, "speed": cfg["start_speed"]},
1124
+ "route": {"to_frame": None, "max_m": route_m},
1125
+ "max_duration_s": round(dur * cfg["duration_factor"] + cfg["duration_pad_s"], 1),
1126
+ "actors": [], "overrides": {},
1127
+ "info": {"route_m": round(use_m, 1), "log_duration_s": round(dur, 1), "segments": len(segs)}}
1128
+ write_scenario(out_dir / f"{name}_easy.json", d)
1129
+ n_ok += 1
1130
+ print(f" {name}_easy: ๊ฒฝ๋กœ {use_m:.0f} m, ๊ธฐ๋ก {dur:.1f}์ดˆ, ๊ตฌ๊ฐ„ {len(segs)}๊ฐœ")
1131
+ print(f" ์‹œ๋‚˜๋ฆฌ์˜ค {n_ok}๊ฐœ โ†’ {out_dir}")
1132
+ return 0
1133
+
1134
+
1135
+ def build_parser() -> argparse.ArgumentParser:
1136
+ p = argparse.ArgumentParser(prog="nusim", description=__doc__.splitlines()[0])
1137
+ sub = p.add_subparsers(dest="cmd", required=True)
1138
+
1139
+ idx = sub.add_parser("index", help="์”ฌ ์ธ๋ฑ์Šค ์ƒ์„ฑ/์กฐํšŒ").add_subparsers(dest="sub", required=True)
1140
+
1141
+ b = idx.add_parser("build", help="์›๋ณธ ํ…Œ์ด๋ธ”์„ ํ›‘์–ด ์”ฌ ์ธ๋ฑ์Šค๋ฅผ ์บ์‹œ์— ๋งŒ๋“ ๋‹ค (์•ฝ 40์ดˆ)")
1142
+ b.add_argument("--force", action="store_true", help="์ด๋ฏธ ์žˆ์–ด๋„ ๋‹ค์‹œ ๋งŒ๋“ ๋‹ค")
1143
+ b.add_argument("--no-sweeps", action="store_true",
1144
+ help="ํ‚คํ”„๋ ˆ์ž„(2 Hz)๋งŒ ๋‹ด๋Š”๋‹ค. 12๋ฐฐ ์ž‘์ง€๋งŒ ์žฌ๊ตฌ์„ฑ ํ’ˆ์งˆ์ด ๋–จ์–ด์ง„๋‹ค")
1145
+ b.add_argument("--locations", nargs="+", metavar="LOC",
1146
+ help="๊ธฐ๋ณธ๊ฐ’์€ ์‹ฑ๊ฐ€ํฌ๋ฅด 3์ข…. ์˜ˆ: --locations boston-seaport")
1147
+ b.set_defaults(func=_cmd_index_build)
1148
+
1149
+ l = idx.add_parser("list", help="์ธ๋ฑ์‹ฑ๋œ ์”ฌ ๋ชฉ๋ก")
1150
+ l.add_argument("--location", help="singapore-onenorth ๋“ฑ์œผ๋กœ ํ•„ํ„ฐ")
1151
+ l.add_argument("--grep", help="์ด๋ฆ„/์„ค๋ช…์— ์ด ๋ฌธ์ž์—ด์ด ๋“  ์”ฌ๋งŒ")
1152
+ l.add_argument("--limit", type=int, help="์ƒ์œ„ N๊ฐœ๋งŒ")
1153
+ l.set_defaults(func=_cmd_index_list)
1154
+
1155
+ s = sub.add_parser("scene", help="์”ฌ ์ƒ์„ธ").add_subparsers(dest="sub", required=True)
1156
+ sh = s.add_parser("show", help="์”ฌ ํ•˜๋‚˜์˜ ์นด๋ฉ”๋ผ/ํ”„๋ ˆ์ž„ ์š”์•ฝ")
1157
+ sh.add_argument("name", help="์˜ˆ: scene-0003")
1158
+ sh.set_defaults(func=_cmd_scene_show)
1159
+
1160
+ rn = sub.add_parser("run", help="์ž‘์—… ์ •์˜(configs/jobs/<์ด๋ฆ„>.json) ์‹คํ–‰ โ€” ์—ฌ๋Ÿฌ ๋‹จ๊ณ„๋ฅผ ์ˆœ์„œ๋Œ€๋กœ, ๋๋‚œ ๋‹จ๊ณ„๋Š” ๊ฑด๋„ˆ๋›ด๋‹ค")
1161
+ rn.add_argument("job", help="์ž‘์—… ์ด๋ฆ„ ๋˜๋Š” JSON ๊ฒฝ๋กœ")
1162
+ rn.add_argument("--dry-run", action="store_true", help="์‹คํ–‰ํ•˜์ง€ ์•Š๊ณ  ํŽผ์นœ ๋‹จ๊ณ„๋งŒ ๋ณด์—ฌ ์ค€๋‹ค")
1163
+ rn.set_defaults(func=_cmd_run)
1164
+
1165
+ r = sub.add_parser("recon", help="3DGS ์žฌ๊ตฌ์„ฑ").add_subparsers(dest="sub", required=True)
1166
+ ri = r.add_parser("init", help="LIDAR_TOP โ†’ ์ดˆ๊ธฐ ๊ฐ€์šฐ์‹œ์•ˆ (outputs/gs/<scene>/init.ply)")
1167
+ ri.add_argument("name")
1168
+ ri.add_argument("--voxel", type=float, default=0.15, help="๋ณต์…€ ํฌ๊ธฐ m (๊ธฐ๋ณธ 0.15)")
1169
+ ri.add_argument("--max-points", type=int, default=1_500_000)
1170
+ ri.set_defaults(func=_cmd_recon_init)
1171
+
1172
+ rt = r.add_parser("train", help="gsplat ํ•™์Šต โ†’ point_cloud.ply + report.json")
1173
+ p._nusim_train = rt
1174
+ rt.add_argument("name")
1175
+ rt.add_argument("--iters", type=int, default=60_000,
1176
+ help="๊ธฐ๋ณธ 6๋งŒ. ์”ฌ๋‹น ๋ทฐ๊ฐ€ 1,200์žฅ ๋‚จ์ง“์ด๋ผ 3๋งŒ์ด๋ฉด ๋ทฐ๋‹น 25ํšŒ๋ฟ โ€” ๋œ ํ•™์Šต๋œ๋‹ค")
1177
+ rt.add_argument("--downscale", type=int, default=2, help="์ด๋ฏธ์ง€ ์ถ•์†Œ ๋น„์œจ (๊ธฐ๋ณธ 2 โ†’ 800x450)")
1178
+ rt.add_argument("--stride", type=int, default=1, help="ํ”„๋ ˆ์ž„ ๊ฐ„๊ฒฉ (2 ๋ฉด ์•ฝ 6 Hz)")
1179
+ rt.add_argument("--keyframes-only", action="store_true", help="2 Hz ํ‚คํ”„๋ ˆ์ž„๋งŒ")
1180
+ rt.add_argument("--image-root", default=None,
1181
+ help="์นด๋ฉ”๋ผ ์ด๋ฏธ์ง€๋ฅผ ์ด ๋ฃจํŠธ์—์„œ (์›๋ณธ๊ณผ ๊ฐ™์€ ์ƒ๋Œ€ ๊ฒฝ๋กœ) โ€” nusim aug translate ๊ฒฐ๊ณผ ๋“ฑ")
1182
+ rt.add_argument("--time-window", default=None, metavar="T0:T1",
1183
+ help="์”ฌ ์‹œ์ž‘๋ถ€ํ„ฐ T0~T1 ์ดˆ๋งŒ ํ•™์Šต (๋น ๋ฅธ ์”ฌ์„ ๊ตฌ๊ฐ„๋ณ„๋กœ ๋‚˜๋ˆŒ ๋•Œ)")
1184
+ rt.add_argument("--max-gaussians", type=int, default=2_000_000)
1185
+ rt.add_argument("--eval-every", type=int, default=5000)
1186
+ rt.add_argument("--refine-stop", type=int, help="densify ์ข…๋ฃŒ ์Šคํ… (๊ธฐ๋ณธ min(15000, iters/2))")
1187
+ rt.add_argument("--absgrad", action="store_true", help="์ ˆ๋Œ“๊ฐ’ gradient ๋กœ densify (์žŽยท์„ธ๋ถ€์— ์œ ๋ฆฌ)")
1188
+ rt.add_argument("--grow-grad2d", type=float, help="densify ์ž„๊ณ„๊ฐ’ (๊ธฐ๋ณธ 0.0002, absgrad ๋ฉด 0.0008)")
1189
+ rt.add_argument("--depth-lambda", type=float, default=0.0, help="๋ผ์ด๋‹ค ๊นŠ์ด ๊ฐ๋… ๊ฐ€์ค‘์น˜ (0 ์ด๋ฉด ๋”)")
1190
+ rt.add_argument("--strategy", choices=["default", "mcmc"], default="default",
1191
+ help="densify ์ „๋žต. mcmc = 3DGS-MCMC (cap = --max-gaussians, ๋ถˆํˆฌ๋ช…๋„ยท์Šค์ผ€์ผ ์ •๊ทœํ™” 0.01)")
1192
+ rt.add_argument("--sky", action="store_true", help="ํ•˜๋Š˜์„ ํ•™์Šต ๊ฐ€๋Šฅํ•œ ํ๋ธŒ๋งต์œผ๋กœ ๋ถ„๋ฆฌ (ํ•˜๋Š˜ ๋งˆ์Šคํฌ BCE)")
1193
+ rt.add_argument("--no-sky", dest="sky", action="store_false", help="ํ•˜๋Š˜ ๋ถ„๋ฆฌ ๋„๊ธฐ (ํ”„๋ฆฌ์…‹์ด ์ผ  ๊ฒƒ์„ ๋Œ ๋•Œ)")
1194
+ rt.add_argument("--preset", help="ํ•™์Šต ํ”„๋ฆฌ์…‹ (configs/train/<์ด๋ฆ„>.json ๋˜๋Š” ๊ฒฝ๋กœ) โ€” ๋ช…๋ น์ค„ ๊ฐ’์ด ์šฐ์„ ")
1195
+ rt.add_argument("--objects", choices=("dynamic", "agents", "vehicles"), default="dynamic",
1196
+ help="ํ•™์Šต์—์„œ ์ง€์šธ ๊ฐ์ฒด. dynamic: ์›€์ง์ด๋Š” ๊ฒƒ๋งŒ (์ฃผ์ฐจ ์ฐจ๋Ÿ‰์€ ๋ฐฐ๊ฒฝ), "
1197
+ "agents: ์ฐจ๋Ÿ‰ยท์‚ฌ๋žŒยท์ž์ „๊ฑฐ ์ „๋ถ€ โ€” ์ฃผํ–‰ ๋งต๋งŒ ๋‚จ๊ธฐ๋Š” ํ™˜๊ฒฝ ์ „์šฉ ์žฌ๊ตฌ์„ฑ, "
1198
+ "vehicles: ์ฐจ๋Ÿ‰์€ ๊ฐ์ฒด ๊ฐ€์šฐ์‹œ์•ˆ์œผ๋กœ ํ•จ๊ป˜ ํ•™์Šต(์—์…‹์œผ๋กœ ๋‚ด๋ณด๋ƒ„), ์‚ฌ๋žŒยท์ž์ „๊ฑฐ๋งŒ ์ง€์›€")
1199
+ rt.add_argument("--opacity-reg", type=float, help="๋ถˆํˆฌ๋ช…๋„ ์ •๊ทœํ™” (mcmc ๊ธฐ๋ณธ 0.01)")
1200
+ rt.add_argument("--aniso-reg", type=float, default=0.0, help="๋ฐ”๋Š˜ ์–ต์ œ ๊ฐ€์ค‘์น˜ (๊ฐ€์žฅ ๊ธด ์ถ•/๋‘ ๋ฒˆ์งธ ์ถ• ๋น„ ๋ฒŒ์ , 0 = ๋”)")
1201
+ rt.add_argument("--aniso-max", type=float, default=6.0, help="๋ฐ”๋Š˜ ์–ต์ œ๊ฐ€ ์‹œ์ž‘๋˜๋Š” ์ถ• ๋น„")
1202
+ rt.add_argument("--ground-lambda", type=float, default=0.0,
1203
+ help="์ง€๋ฉด ๋ชจ๋ธ (HUGSIM ๋‹ค์ค‘ ํ‰๋ฉด): ๋…ธ๋ฉด ๊ฐ€์šฐ์‹œ์•ˆ์„ ์›ํŒ์œผ๋กœ ๊ณ ์ • + ์นธ๋ณ„ ๋†’์ด ๋ถ„์‚ฐ ๊ฐ€์ค‘์น˜ (0 ์ด๋ฉด ๋”)")
1204
+ rt.add_argument("--scale-reg", type=float, help="์Šค์ผ€์ผ ์ •๊ทœํ™” (mcmc ๊ธฐ๋ณธ 0.01)")
1205
+ rt.add_argument("--depth-scale", type=int, default=2,
1206
+ help="๊นŠ์ด ์ง€๋„ ์ถ•์†Œ ๋น„์œจ (sweep ๋ˆ„์ ์ด๋ฉด 1/2 ์—์„œ๋„ 79%% ์ฑ„์›Œ์ง„๋‹ค)")
1207
+ rt.add_argument("--no-camera-adjust", action="store_true", help="์นด๋ฉ”๋ผ ์™ธ๋ถ€ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณด์ • ๋„๊ธฐ")
1208
+ rt.add_argument("--out", help="๊ฒฐ๊ณผ ๋””๋ ‰ํ„ฐ๋ฆฌ (๊ธฐ๋ณธ outputs/gs/<scene>). ์‹คํ—˜์„ ๋‚˜๋ž€ํžˆ ๋Œ๋ฆด ๏ฟฝ๏ฟฝ")
1209
+ rt.add_argument("--tile", help="์—ฌ๋Ÿฌ ์ฃผํ–‰์„ ํ•œ ๋ชจ๋ธ๋กœ (configs/tiles/<์ด๋ฆ„>.json, recon.tile). name ๋Œ€์‹  ํƒ€์ผ์˜ anchor")
1210
+ rt.set_defaults(func=_cmd_recon_train)
1211
+
1212
+ rte = r.add_parser("tile-eval", help="ํƒ€์ผ์—์„œ ๋บ€ ์ฃผํ–‰(์˜† ์ฐจ์„ )์˜ ์‹ค์ œ ์‚ฌ์ง„์œผ๋กœ ๋ชจ๋ธ๋“ค์„ ๋น„๊ต")
1213
+ rte.add_argument("tile")
1214
+ rte.add_argument("--dir", action="append", required=True, help="๋ชจ๋ธ ๋””๋ ‰ํ„ฐ๋ฆฌ (์—ฌ๋Ÿฌ ๋ฒˆ)")
1215
+ rte.add_argument("--stride", type=int, default=4, help="๋บ€ ์ฃผํ–‰์˜ ํ”„๋ ˆ์ž„ ๊ฐ„๊ฒฉ")
1216
+ rte.add_argument("--out", help="๊ธฐ๋ณธ outputs/tiles/<ํƒ€์ผ>")
1217
+ rte.set_defaults(func=_cmd_recon_tile_eval)
1218
+
1219
+ rds = r.add_parser("distill", help="Difix ๋กœ ๊ณ ์นœ ์ƒˆ ์‹œ์ (์ฐจ์„  ์ด๋™)์„ ์˜์‚ฌ ์ •๋‹ต์œผ๋กœ 3DGS ๋ฏธ์„ธ์กฐ์ •")
1220
+ rds.add_argument("name")
1221
+ rds.add_argument("--shifts", default="-3,-1.5,1.5,3", help="์ž์ฐจ ์ขŒ์šฐ ์ด๋™๋Ÿ‰ m (์‰ผํ‘œ)")
1222
+ rds.add_argument("--every", type=int, default=6, help="์นด๋ฉ”๋ผ๋ณ„ ํ•™์Šต ํ”„๋ ˆ์ž„ ๊ฐ„๊ฒฉ")
1223
+ rds.add_argument("--iters", type=int, default=15000)
1224
+ rds.add_argument("--pseudo-prob", type=float, default=0.3)
1225
+ rds.add_argument("--pseudo-weight", type=float, default=0.5)
1226
+ rds.add_argument("--max-gaussians", type=int, default=2_600_000)
1227
+ rds.add_argument("--eval-every", type=int, default=5000)
1228
+ rds.add_argument("--base", help="์ถœ๋ฐœ ๋ชจ๋ธ ๋””๋ ‰ํ„ฐ๋ฆฌ (๊ธฐ๋ณธ outputs/gs/<scene>)")
1229
+ rds.add_argument("--depth-lambda", type=float, default=0.0, help="๋ผ์ด๋‹ค ๊นŠ์ด ๊ฐ๋… ์œ ์ง€ ๊ฐ€์ค‘์น˜")
1230
+ rds.add_argument("--progressive", help="์ ์ง„ ์ฆ๋ฅ˜: ์ด๋™๋Ÿ‰ ๋‹จ๊ณ„ (์‰ผํ‘œ, m) โ€” ์˜ˆ 0.5,1,2,3. ์ฃผ๋ฉด --shiftsยท--iters ๋Œ€์‹ ")
1231
+ rds.add_argument("--iters-per-stage", type=int, default=3000, help="์ ์ง„ ์ฆ๋ฅ˜ ๋‹จ๊ณ„๋‹น ๋ฏธ์„ธ์กฐ์ • ์Šคํ…")
1232
+ rds.add_argument("--out", help="๊ธฐ๋ณธ <base>_difix")
1233
+ rds.set_defaults(func=_cmd_recon_distill)
1234
+
1235
+ rb = r.add_parser("batch", help="์—ฌ๋Ÿฌ ์”ฌ ์ผ๊ด„ ์žฌ๊ตฌ์„ฑ (๊ธฐ๋ณธ: ์ฃผ๊ฐ„ ์”ฌ ์ „์ฒด โ†’ outputs/gs_day/<scene>, ์ด์–ด ํ•˜๊ธฐ ๊ฐ€๋Šฅ)")
1236
+ rb.add_argument("--scenes", help="์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•œ ์”ฌ ์ด๋ฆ„ (๊ธฐ๋ณธ: ์„ค๋ช…์— night/rain ์ด ์—†๋Š” ์”ฌ ์ „๋ถ€)")
1237
+ rb.add_argument("--location", help="singapore-onenorth ๋“ฑ์œผ๋กœ ์ œํ•œ")
1238
+ rb.add_argument("--limit", type=int, help="์•ž์—์„œ N ์”ฌ๋งŒ")
1239
+ rb.add_argument("--order", choices=["location", "coverage"], default="location",
1240
+ help="location: ์ง€์—ญยท์”ฌ ๋ฒˆํ˜ธ ์ˆœ (๊ธฐ๋ณธ). coverage: ์ƒˆ ๋„๋กœ๋ฅผ ๋งŽ์ด ๋ฎ๋Š” ์”ฌ๋ถ€ํ„ฐ โ€” ๋งต์ด ๋นจ๋ฆฌ ์ฑ„์›Œ์ง„๋‹ค")
1241
+ rb.add_argument("--preset", default="day_map", help="ํ•™์Šต ํ”„๋ฆฌ์…‹ (configs/train/<์ด๋ฆ„>.json)")
1242
+ rb.add_argument("--iters", type=int, help="ํ”„๋ฆฌ์…‹์˜ iters ๋ฅผ ๋ฎ์–ด์“ด๋‹ค")
1243
+ rb.add_argument("--root", help="๊ฒฐ๊ณผ ๋ฃจํŠธ (๊ธฐ๋ณธ outputs/gs_day)")
1244
+ rb.add_argument("--no-sky", action="store_true", help="ํ”„๋ฆฌ์…‹๊ณผ ์ƒ๊ด€์—†์ด ํ•˜๋Š˜ ๋ถ„๋ฆฌ ์—†์ด")
1245
+ rb.add_argument("--segment-m", type=float, default=60.0,
1246
+ help="๊ตฌ๊ฐ„ ๊ธธ์ด ์ƒํ•œ (์ž์ฐจ ์ด๋™ m). ๋” ๊ธด ์”ฌ์€ ๊ฐ™์€ ๊ธธ์ด ๊ตฌ๊ฐ„์œผ๋กœ ๋‚˜๋ˆ  ๋”ฐ๋กœ ํ•™์Šต (๊ธฐ๋ณธ 60)")
1247
+ rb.add_argument("--overlap-m", type=float, default=10.0, help="์ด์›ƒ ๊ตฌ๊ฐ„ ๊ฒน์นจ (m, ๊ธฐ๋ณธ 10)")
1248
+ rb.add_argument("--after", action="append", metavar="CMD",
1249
+ help="์”ฌ์˜ ๊ตฌ๊ฐ„์ด ๋‹ค ๋๋‚˜๋ฉด ๋Œ๋ฆด nusim ๋ช…๋ น ({scene}ยท{root} ์น˜ํ™˜, ์—ฌ๋Ÿฌ ๋ฒˆ). ์˜ˆ: 'aug map {scene} --root {root}'")
1250
+ rb.add_argument("--keep-init", action="store_true", help="init.ply ๋ฅผ ์ง€์šฐ์ง€ ์•Š๋Š”๋‹ค (๊ธฐ๋ณธ: ํ•™์Šต ๋’ค ์ง€์›€, ๋””์Šคํฌ ์ ˆ์•ฝ)")
1251
+ rb.set_defaults(func=_cmd_recon_batch)
1252
+
1253
+ rc = r.add_parser("cycle", help="์‚ฌ์ดํด ๋ Œ๋” ๋ฒค์น˜๋งˆํฌ: ์˜†์œผ๋กœ ์˜ฎ๊ธด ๋ Œ๋”๋กœ ๋ชจ๋ธ์„ ๋‹ค์‹œ ํ•™์Šตํ•ด ์›๋ž˜ ์‹œ์ (์ •๋‹ต ์‚ฌ์ง„)์—์„œ ์žฐ๋‹ค")
1254
+ rc.add_argument("name")
1255
+ rc.add_argument("--base", help="์‹ค์ œ ์‚ฌ์ง„์œผ๋กœ ํ•™์Šตํ•œ ๋ชจ๋ธ (๊ธฐ๋ณธ outputs/gs_day/<scene>)")
1256
+ rc.add_argument("--iters", type=int, default=15_000)
1257
+ rc.add_argument("--shift", type=float, default=1.0, help="์˜†์œผ๋กœ ์˜ฎ๊ธฐ๋Š” ๊ฑฐ๋ฆฌ m (๊ธฐ๋ณธ 1, ๋…ผ๋ฌธ๊ณผ ๊ฐ™์Œ)")
1258
+ rc.set_defaults(func=_cmd_recon_cycle)
1259
+ rp = r.add_parser("prune", help="floater ์ •๋ฆฌ: ๋ผ์ด๋‹ค๊ฐ€ ๋น„์–ด ์žˆ๋‹ค๊ณ  ๋ณธ ๊ณต๊ฐ„์— ๋–  ์žˆ๋Š” ๊ฐ€์šฐ์‹œ์•ˆ์„ ์ง€์šด๋‹ค")
1260
+ rp.add_argument("name")
1261
+ rp.add_argument("--dir", help="๋ชจ๋ธ ๋””๋ ‰ํ„ฐ๋ฆฌ (๊ธฐ๋ณธ: render ๊ฐ€ ์“ฐ๋Š” ๋ชจ๋ธ)")
1262
+ rp.add_argument("--out", help="๊ฒฐ๊ณผ (๊ธฐ๋ณธ <dir>_pruned)")
1263
+ rp.add_argument("--margin", type=float, default=0.15, help="๋ผ์ด๋‹ค ํ‘œ๋ฉด๋ณด๋‹ค ์ด ๋น„์œจ ์ด์ƒ ์•ž์ด๋ฉด ์œ„๋ฐ˜")
1264
+ rp.add_argument("--ratio", type=float, default=0.5, help="์œ„๋ฐ˜ ๋น„์œจ์ด ์ด๋ณด๋‹ค ํฌ๋ฉด ์ง€์›€")
1265
+ rp.add_argument("--min-obs", type=int, default=3, help="ํŒ์ •์— ํ•„์š”ํ•œ ์ตœ์†Œ ๊ด€์ธก ํšŸ์ˆ˜")
1266
+ rp.set_defaults(func=_cmd_recon_prune)
1267
+ rsl = r.add_parser("skylabel", help="ํ•˜๋Š˜ ๊ฐ€์šฐ์‹œ์•ˆ ํ‘œ์‹œ: ํ•˜๋Š˜ ๋งˆ์Šคํฌ์— ๋–จ์–ด์ง€๋Š” ๊ฐ€์šฐ์‹œ์•ˆ (๋ฐค์— ํ•˜๋Š˜์ด ํ•˜์–—๊ฒŒ ๋‚จ๋Š” ๋ฌธ์ œ)")
1268
+ rsl.add_argument("name")
1269
+ rsl.add_argument("--dir", help="๋ชจ๋ธ ๋””๋ ‰ํ„ฐ๋ฆฌ (๊ธฐ๋ณธ: render ๊ฐ€ ์“ฐ๋Š” ๋ชจ๋ธ)")
1270
+ rsl.add_argument("--ratio", type=float, default=0.4, help="ํ•˜๋Š˜ ํ”ฝ์…€ ๋น„์œจ์ด ์ด๋ณด๋‹ค ํฌ๋ฉด ํ•˜๋Š˜")
1271
+ rsl.add_argument("--min-obs", type=int, default=3, help="ํŒ์ •์— ํ•„์š”ํ•œ ์ตœ์†Œ ๊ด€์ธก ํšŸ์ˆ˜")
1272
+ rsl.set_defaults(func=_cmd_recon_skylabel)
1273
+ rse = r.add_parser("semantic", help="๊ฐ€์šฐ์‹œ์•ˆ ์‹œ๋งจํ‹ฑ ๋ผ๋ฒจ (lidarseg ๋‹ค์ˆ˜๊ฒฐ) โ†’ ๋ชจ๋ธ ์˜† semantic.rgba โ€” ๋ทฐ์–ด์˜ ์‹œ๋งจํ‹ฑ ์นด๋ฉ”๋ผ")
1274
+ rse.add_argument("names", nargs="*", help="์”ฌ (๊ตฌ๊ฐ„ ๋ชจ๋ธ์ด๋ฉด ๊ตฌ๊ฐ„ ์ „๋ถ€)")
1275
+ rse.add_argument("--location", help="์ด ์ง€์—ญ์˜ ํ•™์Šต๋œ ์”ฌ ์ „๋ถ€ (singapore-onenorth ๋“ฑ)")
1276
+ rse.add_argument("--root", help="์ผ๊ด„ ์žฌ๊ตฌ์„ฑ ๋ฃจํŠธ (๊ธฐ๋ณธ outputs/gs_day)")
1277
+ rse.add_argument("--dir", help="๊ตฌ๊ฐ„ ๋ชจ๋ธ์ด ์•„๋‹Œ ์”ฌ์˜ ๋ชจ๋ธ ๋””๋ ‰ํ„ฐ๋ฆฌ (๊ธฐ๋ณธ: render ๊ฐ€ ์“ฐ๋Š” ๋ชจ๋ธ)")
1278
+ rse.add_argument("--force", action="store_true", help="์žˆ์–ด๋„ ๋‹ค์‹œ")
1279
+ rse.set_defaults(func=_cmd_recon_semantic)
1280
+ rbn = r.add_parser("bench", help="๋ฒค์น˜ ์„ธํŠธ(<cycle>/bench)์˜ ์˜ˆ์ธก์„ ์ •๋‹ต๊ณผ ๋น„๊ต (PSNRยทSSIMยทLPIPSยทDISTS)")
1281
+ rbn.add_argument("bench")
1282
+ rbn.add_argument("--pred", default="render,base", help="๋น„๊ตํ•  ์ ‘๋ฏธ์‚ฌ๋“ค (render = ์‚ฌ์ดํด ๋ Œ๋”, base = ์› ๋ชจ๋ธ, "
1283
+ "difix = Difix ์ „์ฒด ๋ณด์ •, difixm = ์‹ ๋ขฐ๋„ ๋งˆ์Šคํฌ ๋ธ”๋ Œ๋”ฉ)")
1284
+ rbn.add_argument("--run-difix", action="store_true", help="๋จผ์ € Difix ๋ฅผ ๋Œ๋ ค *_difix / *_difixm ์„ ๋งŒ๋“ ๋‹ค")
1285
+ rbn.set_defaults(func=_cmd_recon_bench)
1286
+
1287
+ re_ = r.add_parser("eval", help="ํ•™์Šต๋œ ply ๋ฅผ hold-out ๋ทฐ๋กœ ๋‹ค์‹œ ํ‰๊ฐ€ (์ •์ /์ „์ฒด PSNR)")
1288
+ re_.add_argument("name")
1289
+ re_.add_argument("--dir", help="point_cloud.ply ์™€ color_correction.pt ๊ฐ€ ์žˆ๋Š” ๊ณณ (๊ธฐ๋ณธ outputs/gs/<scene>)")
1290
+ re_.add_argument("--downscale", type=int, default=None, help="๊ธฐ๋ณธ: ํ•™์Šต ๋•Œ ๊ฐ’ (report.json)")
1291
+ re_.add_argument("--geom", action="store_true",
1292
+ help="๊ธฐํ•˜ยทfloater ์ง€ํ‘œ๋„: ์ •์ /๊ตฌ๋ฉ(๋™์  ๋งˆ์Šคํฌ) ๊นŠ์ด ์˜ค์ฐจ, ํ•˜๋Š˜ ฮฑ (nusim.recon.metrics)")
1293
+ re_.add_argument("--lpips", action="store_true", help="LPIPS (AlexNet) ๋„ โ€” ์ „์ฒด ์˜์ƒ ยท ์ •์  ์˜์—ญ")
1294
+ re_.set_defaults(func=_cmd_recon_eval)
1295
+
1296
+ rd = sub.add_parser("render", help="Vulkan ๋ Œ๋”๋Ÿฌ (src/vkgs) ์—ฐ๋™").add_subparsers(dest="sub", required=True)
1297
+ ex = rd.add_parser("export", help="์นด๋ฉ”๋ผ ๋ฆฌ๊ทธ + ๋งต ์„  ํŒŒ์ผ โ†’ outputs/gs/<scene>/{rig,map}.txt")
1298
+ ex.add_argument("name")
1299
+ ex.add_argument("--model", help="๋ชจ๋ธ ๋””๋ ‰ํ„ฐ๋ฆฌ (๊ธฐ๋ณธ outputs/gs/<scene>_depth, ์—†์œผ๋ฉด <scene>)")
1300
+ ex.add_argument("--downscale", type=int, default=1, help="๋ฆฌ๊ทธ ํ•ด์ƒ๋„ ์ถ•์†Œ (๊ธฐ๋ณธ 1 = 1600x900)")
1301
+ ex.add_argument("--margin", type=float, default=60.0, help="๊ถค์  ์ฃผ๋ณ€ ๋งต ๋ฒ”์œ„ m")
1302
+ ex.set_defaults(func=_cmd_render_export)
1303
+ cp = rd.add_parser("compare", help="ํ‰๊ฐ€ ๋ทฐ์—์„œ Vulkan ๋ Œ๋” vs gsplat ๋ Œ๋” ํ”ฝ์…€ ๋น„๊ต")
1304
+ cp.add_argument("name")
1305
+ cp.add_argument("--model", help="๋ชจ๋ธ ๋””๋ ‰ํ„ฐ๋ฆฌ (๊ธฐ๋ณธ outputs/gs/<scene>_depth, ์—†์œผ๋ฉด <scene>)")
1306
+ cp.add_argument("-n", type=int, default=24, help="๋น„๊ตํ•  ๋ทฐ ์ˆ˜")
1307
+ cp.set_defaults(func=_cmd_render_compare)
1308
+ vw = rd.add_parser("view", help="์ฐฝ ๋ชจ๋“œ ๋ทฐ์–ด (์ž์œ  ์นด๋ฉ”๋ผ / 6์บ  ์ถ”์ข… / ๊ถค์  ์žฌ์ƒ / BEV / ๋งต)")
1309
+ vw.add_argument("name")
1310
+ vw.add_argument("--model", help="๋ชจ๋ธ ๋””๋ ‰ํ„ฐ๋ฆฌ (๊ธฐ๋ณธ outputs/gs/<scene>_depth, ์—†์œผ๋ฉด <scene>)")
1311
+ vw.add_argument("--downscale", type=int, default=1)
1312
+ vw.add_argument("--autoshot", type=int, default=0, help="N ํ”„๋ ˆ์ž„ ๋’ค ์Šคํฌ๋ฆฐ์ƒท ์ฐ๊ณ  ์ข…๋ฃŒ (์ ๊ฒ€์šฉ)")
1313
+ vw.add_argument("--weather", help="ํ•ฉ์„ฑ ๋‚ ์”จ ์‹œ์ž‘๊ฐ’: clear|overcast|dusk|night|rain|heavy_rain|fog|night_rain "
1314
+ "๋˜๋Š” daylight=0.3,rain=0.5,... (์ฐฝ ์•ˆ ์Šฌ๋ผ์ด๋”๋กœ๋„ ์กฐ์ ˆ, F1 ํŒจ๋„)")
1315
+ vw.add_argument("--replay", action="store_true", help="๋กœ๊ทธ ์žฌ์ƒ ๋ชจ๋“œ(์ฐจ์— ๋‹ฌ๋ฆฐ CAM_FRONT ์‹œ์ )๋กœ ์‹œ์ž‘ (๊ธฐ๋ณธ: ์šด์ „ ๋ชจ๋“œ, ์ฐจ ๋’ค ์‹œ์ )")
1316
+ vw.add_argument("--agents", action="store_true",
1317
+ help="nuScenes ๋กœ๊ทธ์˜ ์›€์ง์ด๋Š” ์ฐจ ๊ถค์ ์— glTF ์ฐจ๋Ÿ‰์„ ๋„ฃ๋Š”๋‹ค (๊ธฐ๋ณธ: ํ™˜๊ฒฝ๋งŒ)")
1318
+ vw.set_defaults(func=_cmd_render_view)
1319
+ rm = rd.add_parser("map", help="์ง€์—ญ ๋งต: ํ•™์Šต๋œ ์”ฌ ๊ตฌ๊ฐ„๋“ค์„ ์ด์–ด ํ•œ ๊ณต๊ฐ„์—์„œ ์ฃผํ–‰ (singapore-onenorth ๋“ฑ)")
1320
+ rm.add_argument("location", help="singapore-onenorth | singapore-queenstown | singapore-hollandvillage")
1321
+ rm.add_argument("--start", help="์ถœ๋ฐœ ์”ฌ (๊ธฐ๋ณธ: ๊ตฌ๊ฐ„์ด ๊ฐ€์žฅ ๋งŽ์€ ์”ฌ)")
1322
+ rm.add_argument("--root", help="์ผ๊ด„ ์žฌ๊ตฌ์„ฑ ๋ฃจํŠธ (๊ธฐ๋ณธ outputs/gs_day)")
1323
+ rm.add_argument("--replay", action="store_true", help="์ถœ๋ฐœ ์”ฌ์˜ ๋กœ๊ทธ ์žฌ์ƒ ๋ชจ๋“œ๋กœ ์‹œ์ž‘")
1324
+ rm.add_argument("--sensors", action="store_true",
1325
+ help="ํ•™์Šต๋œ ๊ตฌ๊ฐ„์ด ๋ฐ”๋€ ์”ฌ์˜ ๋ผ์ด๋‹คยท๋ ˆ์ด๋” ๋ณต์…€ ์›”๋“œ๋„ ๋‹ค์‹œ ๋งŒ๋“ ๋‹ค (์—†๋Š” ์”ฌ ๊ฒƒ๏ฟฝ๏ฟฝ ๊ธฐ๋ณธ์œผ๋กœ ๋งŒ๋“ ๋‹ค, ์”ฌ๋‹น 1~2๋ถ„)")
1326
+ rm.add_argument("--no-sensors", action="store_true", help="์—†๋Š” ๋ณต์…€ ์›”๋“œ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค (๊ทธ ์”ฌ์€ ์„ผ์„œ ๊บผ์ง)")
1327
+ rm.add_argument("--no-view", action="store_true", help="ํŒŒ์ผ๋งŒ ๋งŒ๋“ค๊ณ  ์ฐฝ์€ ๋„์šฐ์ง€ ์•Š๋Š”๋‹ค")
1328
+ rm.add_argument("--no-semantic", action="store_true",
1329
+ help="์—†๋Š” ์‹œ๋งจํ‹ฑ ๋ผ๋ฒจ์„ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค (๊ทธ ๊ตฌ๊ฐ„์€ F3 ์‹œ๋งจํ‹ฑ ์นด๋ฉ”๋ผ๊ฐ€ ๋น„์–ด ๋ณด์ธ๋‹ค, ๋‚˜์ค‘์— nusim recon semantic)")
1330
+ rm.set_defaults(viewer_args=[]) # '--' ๋’ค๋Š” vkgs_viewer ์˜ต์…˜ ๊ทธ๋Œ€๋กœ (main ์ด ๋–ผ์–ด ๋‘”๋‹ค): -- --autopilot --show-sensors
1331
+ rm.set_defaults(func=_cmd_render_map)
1332
+
1333
+ ve = rd.add_parser("encode", help="PNG ํ”„๋ ˆ์ž„ (vkgs_viewer --record-screen) โ†’ mp4, ์„ค๋ช… ๊ธ€์ž ์–น๊ธฐ")
1334
+ ve.add_argument("frames", help="frame_*.png ๋””๋ ‰ํ„ฐ๋ฆฌ")
1335
+ ve.add_argument("out", help="์ถœ๋ ฅ .mp4")
1336
+ ve.add_argument("--fps", type=float, default=20.0, help="๋…นํ™”๋Š” ๊ณ ์ • 50 ms ์Šคํ… = 20 fps")
1337
+ ve.add_argument("--width", type=int, default=1280, help="๊ฐ€๋กœ ํ”ฝ์…€๋กœ ์ค„์ธ๋‹ค (0 = ๊ทธ๋Œ€๋กœ)")
1338
+ ve.add_argument("--bitrate", default="3M", help="libx264 ๋น„ํŠธ๋ ˆ์ดํŠธ (๋ฐœํ‘œ์ž๋ฃŒ์— ๋„ฃ์„ ํฌ๊ธฐ: 3M ร— 20์ดˆ โ‰ˆ 7.5 MB)")
1339
+ ve.add_argument("--caption", default="", help="์™ผ์ชฝ ์•„๋ž˜ ๊ธ€์ž")
1340
+ ve.add_argument("--font-size", type=int, default=24)
1341
+ ve.add_argument("--poster", help="๊ฐ€์šด๋ฐ ํ”„๋ ˆ์ž„์„ ์ด jpg ๋กœ (๋ฐœํ‘œ์ž๋ฃŒ ํ‘œ์ง€ ํ™”๋ฉด)")
1342
+ ve.set_defaults(func=_cmd_render_encode)
1343
+ vd = rd.add_parser("video", help="6์บ  + BEV(๋งต ์˜ค๋ฒ„๋ ˆ์ด) ์žฌ์ƒ ์˜์ƒ โ†’ outputs/gs/<scene>/<scene>_sim.mp4")
1344
+ vd.add_argument("name")
1345
+ vd.add_argument("--model", help="๋ชจ๋ธ ๋””๋ ‰ํ„ฐ๋ฆฌ (๊ธฐ๋ณธ outputs/gs/<scene>_depth, ์—†์œผ๋ฉด <scene>)")
1346
+ vd.add_argument("--frames", help="ํ”„๋ ˆ์ž„ ๋ฒ”์œ„ A:B (๊ธฐ๋ณธ ์ „์ฒด)")
1347
+ vd.add_argument("--fps", type=float, default=12.0, help="๊ธฐ๋ณธ 12 (nuScenes ์นด๋ฉ”๋ผ ์•ฝ 12 Hz)")
1348
+ vd.add_argument("--bev-height", type=float, default=70.0)
1349
+ vd.set_defaults(func=_cmd_render_video)
1350
+
1351
+ asp = sub.add_parser("asset", help="nuScenes ์‹ค์ œ ์ฐจ๋Ÿ‰ โ†’ 3DGS ์—์…‹").add_subparsers(dest="sub", required=True)
1352
+ sc_ = asp.add_parser("scan", help="์ž์ฐจ๊ฐ€ ๊ฐ€๊นŒ์ด ์ง€๋‚˜๊ฐ€๋ฉฐ ์—ฌ๋Ÿฌ ๋ฐฉ์œ„์—์„œ ๋ณธ ์ฃผ์ฐจ ์ฐจ๋Ÿ‰ ์ฐพ๊ธฐ (์ฃผ๊ฐ„ ์”ฌ)")
1353
+ sc_.add_argument("--max-dist", type=float, default=5.0)
1354
+ sc_.add_argument("--limit", type=int, default=15)
1355
+ sc_.set_defaults(func=_cmd_asset_scan)
1356
+ xt = asp.add_parser("extract", help="ํ•™์Šต๋œ ์”ฌ์—์„œ ์ฃผ์ฐจ ์ฐจ๋Ÿ‰์„ ์ž˜๋ผ ๋ฐ•์Šค ๋กœ์ปฌ 3DGS ์—์…‹์œผ๋กœ")
1357
+ xt.add_argument("name", help="์”ฌ (์˜ˆ: scene-0916)")
1358
+ xt.add_argument("--instance", type=int, required=True)
1359
+ xt.add_argument("--model", help="์”ฌ ๋ชจ๋ธ ๋””๋ ‰ํ„ฐ๋ฆฌ (๊ธฐ๋ณธ outputs/gs/<scene>_depth)")
1360
+ xt.add_argument("--no-mirror", action="store_true", help="์ขŒ์šฐ ๋Œ€์นญ ๋ณต์‚ฌ ๋„๊ธฐ")
1361
+ xt.add_argument("--out")
1362
+ xt.add_argument("--as-ego", action="store_true", help="outputs/assets/ego_car.ply ๋กœ ์ง€์ •")
1363
+ xt.set_defaults(func=_cmd_asset_extract)
1364
+
1365
+ ib = rd.add_parser("ibl", help="6์บ  ํŒŒ๋…ธ๋ผ๋งˆ โ†’ HDR ํ๋ธŒ๋งต โ†’ prefiltered diffuse/specular + BRDF LUT โ†’ BC6H KTX2")
1366
+ ib.add_argument("name")
1367
+ ib.add_argument("--model")
1368
+ ib.add_argument("--frame", type=int, help="CAM_FRONT ํ”„๋ ˆ์ž„ (๊ธฐ๋ณธ ๊ฐ€์šด๋ฐ)")
1369
+ ib.add_argument("--size", type=int, default=512)
1370
+ ib.add_argument("--samples", type=int, default=512, help="GGX ์ƒ˜ํ”Œ ์ˆ˜")
1371
+ ib.add_argument("--fill", choices=["extrap", "gs"], default="extrap",
1372
+ help="์‚ฌ์ง„์ด ์—†๋Š” ์œ„ยท์•„๋ž˜ ์ฑ„์›€: extrap = ๋  ๊ฐ€์žฅ์ž๋ฆฌ ์ƒ‰ ์™ธ์‚ฝ (๊ธฐ๋ณธ), gs = 3DGS ๋ Œ๋”")
1373
+ ib.set_defaults(func=_cmd_render_ibl)
1374
+
1375
+ ag = sub.add_parser("aug", help="๋ฐ์ดํ„ฐ์…‹ ๋‹ค์–‘์„ฑ ์ฆ๊ฐ•").add_subparsers(dest="sub", required=True)
1376
+ agr = ag.add_parser("rig", help="์žฌ๊ตฌ์„ฑ ์žฅ๋ฉด์„ 6์บ  ์‹œ์ (์žฅ์ฐฉ ์œ„์น˜ยท์ž์„ธ)์„ ๋ฐ”๊ฟ” ๋‹ค์‹œ ๋ Œ๋” โ†’ outputs/aug/rig")
1377
+ agr.add_argument("name")
1378
+ agr.add_argument("--only", help="๋ณ€ํ˜• ์ด๋ฆ„๋“ค (์‰ผํ‘œ). ๊ธฐ๋ณธ: ์ „๋ถ€")
1379
+ agr.add_argument("--variants", help="๋ณ€ํ˜• ์ •์˜ JSON (๊ธฐ๋ณธ configs/aug/rig_variants.json)")
1380
+ agr.add_argument("--model", help="๋ชจ๋ธ ๋””๋ ‰ํ„ฐ๋ฆฌ (๊ธฐ๋ณธ: render ๊ฐ€ ์“ฐ๋Š” ๋ชจ๋ธ)")
1381
+ agr.add_argument("--agents", action="store_true", help="์ง€์šด ์ฐจ๋Ÿ‰์„ glTF ๋กœ ๋‹ค์‹œ ๋„ฃ๋Š”๋‹ค (ํ™˜๊ฒฝ ์ „์šฉ ๋ชจ๋ธ์ผ ๋•Œ)")
1382
+ agr.add_argument("--out", help="๊ธฐ๋ณธ outputs/aug/rig")
1383
+ agr.set_defaults(func=_cmd_aug_rig)
1384
+ agm = ag.add_parser("map", help="์ผ๊ด„ ์žฌ๊ตฌ์„ฑ(๊ตฌ๊ฐ„ ๋ชจ๋ธ) ์”ฌ์„ ๋ฆฌ๊ทธ ๋ณ€ํ˜•์œผ๋กœ ๋‹ค์‹œ ๋ Œ๋” โ†’ outputs/aug/map")
1385
+ agm.add_argument("name")
1386
+ agm.add_argument("--root", help="์ผ๊ด„ ์žฌ๊ตฌ์„ฑ ๋ฃจํŠธ (๊ธฐ๋ณธ outputs/gs_day)")
1387
+ agm.add_argument("--only", help="๋ณ€ํ˜• ์ด๋ฆ„๋“ค (์‰ผํ‘œ). ๊ธฐ๋ณธ: ๋‚ ์”จ ์—†๋Š” ๋ณ€ํ˜• ์ „๋ถ€")
1388
+ agm.add_argument("--variants", help="๋ณ€ํ˜• ์ •์˜ JSON (๊ธฐ๋ณธ configs/aug/rig_variants.json)")
1389
+ agm.add_argument("--keyframes-only", action="store_true", help="ํ‚คํ”„๋ ˆ์ž„(2 Hz sample)๋งŒ")
1390
+ agm.add_argument("--agents", action="store_true", help="์ง€์šด ์ฐจ๋Ÿ‰์„ glTF ๋กœ ๋‹ค์‹œ ๋„ฃ๋Š”๋‹ค (IBL ์ด ์—†์œผ๋ฉด ๊ตฝ๋Š”๋‹ค)")
1391
+ agm.add_argument("--out", help="๊ธฐ๋ณธ outputs/aug/map")
1392
+ agm.set_defaults(func=_cmd_aug_map)
1393
+ agx = ag.add_parser("artifixer", help="ArtiFixer ํ™•์‚ฐ ๋ณด์ • + ์ฐจ์„  ํ›„์ฒ˜๋ฆฌ ์ฆ๊ฐ• (configs/aug/artifixer.json) โ†’ outputs/aug/aug_artfixer/<์”ฌ>_lanefix")
1394
+ agx.add_argument("names", nargs="*", help="์”ฌ (๊ธฐ๋ณธ: ํ•™์Šต ยท aug map ์ด ๋๋‚œ ์”ฌ ์ „๋ถ€, ๋๋‚œ ์ˆœ์„œ๋Œ€๋กœ)")
1395
+ agx.add_argument("--cams", help="์นด๋ฉ”๋ผ๋“ค (์‰ผํ‘œ, ๊ธฐ๋ณธ ์„ค์ • cams = CAM_FRONT)")
1396
+ agx.add_argument("--root", help="์ผ๊ด„ ์žฌ๊ตฌ์„ฑ ๋ฃจํŠธ (๊ธฐ๋ณธ outputs/gs_day)")
1397
+ agx.add_argument("--limit", type=int, help="์•ˆ ๋๋‚œ ์”ฌ ์ค‘ ์•ž์—์„œ N๊ฐœ๋งŒ")
1398
+ agx.add_argument("--force", action="store_true", help="๋๋‚ฌ์–ด๋„ ๋‹ค์‹œ")
1399
+ agx.set_defaults(func=_cmd_aug_artifixer)
1400
+ agt = ag.add_parser("translate", help="๋ฐคยท๋น„ ์˜์ƒ โ†’ ๋‚ฎ (CycleGAN-Turbo) โ†’ outputs/aug/translate/<๋ชจ๋ธ>/ (์›๋ณธ๊ณผ ๊ฐ™์€ ์ƒ๋Œ€ ๊ฒฝ๋กœ)")
1401
+ agt.add_argument("names", nargs="+")
1402
+ agt.add_argument("--models", default="night_to_day",
1403
+ help="์ฐจ๋ก€๋กœ ์ ์šฉํ•  ๋ณ€ํ™˜ (์‰ผํ‘œ): night_to_day, rainy_to_clear โ€” ์˜ˆ: rainy_to_clear,night_to_day")
1404
+ agt.add_argument("--size", default="912x512", help="๋ชจ๋ธ ์ž…๋ ฅ ํฌ๊ธฐ (8์˜ ๋ฐฐ์ˆ˜). ์›๋ณธ ๋น„์œจ 16:9")
1405
+ agt.add_argument("--stride", type=int, default=1)
1406
+ agt.add_argument("--keyframes-only", action="store_true")
1407
+ agt.add_argument("--out", help="๊ธฐ๋ณธ outputs/aug/translate")
1408
+ agt.set_defaults(func=_cmd_aug_translate)
1409
+ sm = sub.add_parser("sim", help="closed-loop ํ‰๊ฐ€ (HUGSIM ์‹: ์‹œ๋‚˜๋ฆฌ์˜ค ยท ์ •์ฑ… ยท HD-Score)").add_subparsers(dest="sub", required=True)
1410
+ smr = sm.add_parser("run", help="์‹œ๋‚˜๋ฆฌ์˜ค ํ•˜๋‚˜๋ฅผ ์ •์ฑ…์œผ๋กœ ๋๊นŒ์ง€ (์ฐฝ ์—†์Œ, vkgs_viewer --serve) โ†’ ์—ํ”ผ์†Œ๋“œ ๋กœ๊ทธ + HD-Score")
1411
+ smr.add_argument("scenario", help="configs/scenarios/<์ด๋ฆ„>.json ์˜ ์ด๋ฆ„ ๋˜๋Š” ๊ฒฝ๋กœ")
1412
+ smr.add_argument("--policy", default="replay", help="replay (๊ธฐ๋ก ๊ถค์  ์ „๋ฌธ๊ฐ€) | constant (์ผ์ • ์†๋„ ์ง์ง„)")
1413
+ smr.add_argument("--out", help="๊ธฐ๋ณธ outputs/closed_loop/<์‹œ๋‚˜๋ฆฌ์˜ค>/<์ •์ฑ…>")
1414
+ smr.add_argument("--size", help="๊ด€์ธก ํ•ด์ƒ๋„ WxH (๊ธฐ๋ณธ configs/closed_loop.json render)")
1415
+ smr.add_argument("--max-duration", type=float, help="์ œํ•œ ์‹œ๊ฐ„ ๋ฎ์–ด์“ฐ๊ธฐ (์ดˆ)")
1416
+ smr.add_argument("--no-front", action="store_true", help="์ „๋ฐฉ ์นด๋ฉ”๋ผ jpg ๋ฅผ ๋‚จ๊ธฐ์ง€ ์•Š๋Š”๋‹ค")
1417
+ smr.add_argument("--no-video", action="store_true", help="front.mp4 ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค")
1418
+ smr.set_defaults(func=_cmd_sim_run)
1419
+ sms = sm.add_parser("score", help="์—ํ”ผ์†Œ๋“œ ๋กœ๊ทธ โ†’ NC/DAC/TTC/COM/Rc/HD-Score")
1420
+ sms.add_argument("episodes", nargs="+", help="์—ํ”ผ์†Œ๋“œ ๋””๋ ‰ํ„ฐ๋ฆฌ (episode.json + steps.csv)")
1421
+ sms.add_argument("-v", "--verbose", action="store_true", help="๊ฒฐ์ • ์‹œ์ ๋งˆ๋‹ค")
1422
+ sms.add_argument("--json", action="store_true")
1423
+ sms.set_defaults(func=_cmd_sim_score)
1424
+ smm = sm.add_parser("make-scenarios", help="์ผ๊ด„ ์žฌ๊ตฌ์„ฑ ์”ฌ(๊ตฌ๊ฐ„์ด ๋‹ค ๋๋‚œ ๊ฒƒ)๋งˆ๋‹ค Easy ์‹œ๋‚˜๋ฆฌ์˜ค โ†’ configs/scenarios/")
1425
+ smm.add_argument("scenes", nargs="*", help="๊ธฐ๋ณธ: --root ์˜ ๋ชจ๋“  ์”ฌ")
1426
+ smm.add_argument("--root", help="๊ธฐ๋ณธ outputs/gs_day")
1427
+ smm.add_argument("--route-m", type=float, help="๊ฒฝ๋กœ ๊ธธ์ด ์ƒํ•œ (๊ธฐ๋ณธ configs/closed_loop.json scenarios.route_m, 0 = ๋๊นŒ์ง€)")
1428
+ smm.add_argument("--out", help="๊ธฐ๋ณธ configs/scenarios")
1429
+ smm.set_defaults(func=_cmd_sim_make_scenarios)
1430
+ d = sub.add_parser("doctor", help="ํ™˜๊ฒฝ ์ง„๋‹จ (vkgs-doctor ์™€ ๋™์ผ)")
1431
+ d.set_defaults(func=_cmd_doctor)
1432
+
1433
+ return p
1434
+
1435
+
1436
+ def _preset_defaults(argv: list[str] | None) -> dict:
1437
+ """`recon train --preset X` ์˜ ํ”„๋ฆฌ์…‹ ๊ฐ’ (configs/train/X.json). ๋ช…๋ น์ค„ ๊ฐ’์ด ์šฐ์„ ํ•˜๋„๋ก ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ๋งŒ ์“ด๋‹ค."""
1438
+ av = sys.argv[1:] if argv is None else argv
1439
+ if "--preset" not in av or av[:2] != ["recon", "train"]:
1440
+ return {}
1441
+ from nusim.config import train_preset
1442
+
1443
+ return train_preset(av[av.index("--preset") + 1])
1444
+
1445
+
1446
+ def main(argv: list[str] | None = None) -> int:
1447
+ parser = build_parser()
1448
+ preset = _preset_defaults(argv)
1449
+ if preset:
1450
+ rt = parser._nusim_train # recon train ํ•˜์œ„ ํŒŒ์„œ
1451
+ known = {a.dest for a in rt._actions}
1452
+ bad = sorted(set(preset) - known)
1453
+ if bad:
1454
+ print(f"\033[1;31m์˜ค๋ฅ˜:\033[0m ํ”„๋ฆฌ์…‹์— ๋ชจ๋ฅด๋Š” ํ‚ค: {bad} (nusim recon train --help)", file=sys.stderr)
1455
+ return 1
1456
+ rt.set_defaults(**preset)
1457
+ argv = list(sys.argv[1:] if argv is None else argv)
1458
+ tail = []
1459
+ if "--" in argv and argv[:2] == ["render", "map"]: # render map ์˜ '--' ๋’ค = ๋ทฐ์–ด ์˜ต์…˜
1460
+ k = argv.index("--")
1461
+ argv, tail = argv[:k], argv[k + 1:]
1462
+ args = parser.parse_args(argv)
1463
+ if tail:
1464
+ args.viewer_args = tail
1465
+ try:
1466
+ from nusim.log import setup
1467
+ from nusim.paths import output_dir
1468
+
1469
+ tag = "_".join(x for x in (args.cmd, getattr(args, "sub", None)) if x)
1470
+ path = setup(output_dir() / "logs", tag=tag)
1471
+ LOG.debug("๋ช…๋ น %s (๋กœ๊ทธ %s)", " ".join(sys.argv), path)
1472
+ except Exception: # ๋กœ๊ทธ ์„ค์ • ์‹คํŒจ๊ฐ€ ๋ช…๋ น์„ ๋ง‰์œผ๋ฉด ์•ˆ ๋œ๋‹ค (์˜ˆ: outputs ์“ฐ๊ธฐ ๋ถˆ๊ฐ€)
1473
+ pass
1474
+ try:
1475
+ return args.func(args)
1476
+ except (FileNotFoundError, RuntimeError, ValueError) as e:
1477
+ print(f"\033[1;31m์˜ค๋ฅ˜:\033[0m {e}", file=sys.stderr)
1478
+ return 1
1479
+ except KeyboardInterrupt:
1480
+ return 130
1481
+ except BrokenPipeError: # `nusim index list | head` ์ฒ˜๋Ÿผ ํŒŒ์ดํ”„๊ฐ€ ๋จผ์ € ๋‹ซํžŒ ๊ฒฝ์šฐ
1482
+ sys.stderr.close()
1483
+ return 0
1484
+
1485
+
1486
+ if __name__ == "__main__":
1487
+ raise SystemExit(main())
src/nusim/nusim/config.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """์„ค์ • ํŒŒ์ผ (configs/*.json) โ€” ์ฝ”๋“œ์— ๋ฐ•์•„ ๋‘๋˜ ์กฐ์ •๊ฐ’์„ ํ•œ๊ณณ์— ๋ชจ์€๋‹ค.
2
+
3
+ configs/sim.json ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ(๋ทฐ์–ด): ๋‚ ์”จ ํ”„๋ฆฌ์…‹, ํ•˜๋Š˜, 3์ธ์นญ ์นด๋ฉ”๋ผ, ์ถฉ๋Œ, ์ž์ฐจ (viewer.cpp ๋„ ์ฝ๋Š”๋‹ค)
4
+ configs/train/<์ด๋ฆ„>.json ํ•™์Šต ํ”„๋ฆฌ์…‹ โ€” `nusim recon train --preset <์ด๋ฆ„|๊ฒฝ๋กœ>` (๋ช…๋ น์ค„ ๊ฐ’์ด ์šฐ์„ )
5
+ configs/jobs/<์ด๋ฆ„>.json ์ž‘์—… ์ •์˜ โ€” `nusim run <์ด๋ฆ„|๊ฒฝ๋กœ>` (nusim.jobs)
6
+
7
+ JSON ์˜ "_doc" ํ‚ค๋Š” ์„ค๋ช…์šฉ์ด๋ผ ๋ฌด์‹œํ•œ๋‹ค.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from functools import lru_cache
14
+ from pathlib import Path
15
+
16
+ from nusim.paths import workspace_root
17
+
18
+
19
+ def configs_dir() -> Path:
20
+ return workspace_root() / "configs"
21
+
22
+
23
+ def resolve(kind: str, name_or_path: str) -> Path:
24
+ """'day_map' โ†’ configs/<kind>/day_map.json, ๊ฒฝ๋กœ๋ฉด ๊ทธ๋Œ€๋กœ."""
25
+ p = Path(name_or_path)
26
+ if p.suffix == ".json" or p.exists():
27
+ return p if p.is_absolute() or p.exists() else workspace_root() / p
28
+ return configs_dir() / kind / f"{name_or_path}.json"
29
+
30
+
31
+ def load(path: Path) -> dict:
32
+ if not path.exists():
33
+ raise FileNotFoundError(f"์„ค์ • ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค: {path}")
34
+ return _strip_doc(json.loads(path.read_text()))
35
+
36
+
37
+ def _strip_doc(x):
38
+ if isinstance(x, dict):
39
+ return {k: _strip_doc(v) for k, v in x.items() if not k.startswith("_doc")}
40
+ if isinstance(x, list):
41
+ return [_strip_doc(v) for v in x]
42
+ return x
43
+
44
+
45
+ def sim_config_path() -> Path:
46
+ return configs_dir() / "sim.json"
47
+
48
+
49
+ @lru_cache(maxsize=1)
50
+ def sim_config() -> dict:
51
+ p = sim_config_path()
52
+ return load(p) if p.exists() else {}
53
+
54
+
55
+ def train_preset(name_or_path: str) -> dict:
56
+ """ํ•™์Šต ํ”„๋ฆฌ์…‹ โ†’ argparse dest ์ด๋ฆ„์˜ ๊ธฐ๋ณธ๊ฐ’ dict (์˜ˆ: {"iters": 15000, "strategy": "mcmc", ...})."""
57
+ return load(resolve("train", name_or_path))
src/nusim/nusim/data/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """nuScenes ์ฝ๊ธฐ ๊ณ„์ธต โ€” ์”ฌ ์ธ๋ฑ์Šค, ์นด๋ฉ”๋ผ ๋ฆฌ๊ทธ, ego ๊ถค์ ."""
2
+
3
+ from nusim.data.index import SceneIndex, build_index, load_catalog, load_scene
4
+
5
+ __all__ = ["SceneIndex", "build_index", "load_catalog", "load_scene"]
src/nusim/nusim/data/index.py ADDED
@@ -0,0 +1,459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """์‹ฑ๊ฐ€ํฌ๋ฅด ์”ฌ ์ธ๋ฑ์Šค โ€” ์›๋ณธ ํ…Œ์ด๋ธ”์„ ํ•œ ๋ฒˆ๋งŒ ํ›‘์–ด ์บ์‹œ๋กœ ๊ตณํžŒ๋‹ค.
2
+
3
+ ์™œ ์ด ๊ณ„์ธต์ด ํ•„์š”ํ•œ๊ฐ€:
4
+ nuscenes-devkit ์˜ NuScenes() ๋Š” ๋ชจ๋“  ํ…Œ์ด๋ธ”์„ ํ†ต์งธ๋กœ ๋ฉ”๋ชจ๋ฆฌ์— ์˜ฌ๋ฆฐ๋‹ค
5
+ (trainval ๊ธฐ์ค€ 2~3๋ถ„, 10 GB ์ด์ƒ). ํŒŒ์ผ ๋ชฉ๋ก ํ•˜๋‚˜ ๋ณด์ž๊ณ  ๋งค๋ฒˆ ๋‚ผ ๋น„์šฉ์ด ์•„๋‹ˆ๋‹ค.
6
+ ๊ฒŒ๋‹ค๊ฐ€ ์›๋ณธ์€ NFS ์œ„์— ์žˆ์–ด ๋ฐ˜๋ณต ์ ‘๊ทผ์ด ํŠนํžˆ ๋น„์‹ธ๋‹ค.
7
+
8
+ ์—ฌ๊ธฐ์„œ๋Š” ํ…Œ์ด๋ธ”์„ ์ŠคํŠธ๋ฆฌ๋ฐ์œผ๋กœ ๋”ฑ ํ•œ ๋ฒˆ ํ›‘์–ด ์šฐ๋ฆฌ๊ฐ€ ์“ฐ๋Š” ๊ฒƒ๋งŒ ์”ฌ๋ณ„๋กœ
9
+ ์ž˜๋ผ ๋กœ์ปฌ ์บ์‹œ์— ์ €์žฅํ•œ๋‹ค. ์ดํ›„ ์กฐํšŒ๋Š” ์”ฌ ํ•˜๋‚˜๋‹น ์ˆ˜ MB ๋ฅผ ์ฝ๋Š” ์ผ์ด ๋œ๋‹ค.
10
+
11
+ devkit ์„ ๋Œ€์ฒดํ•˜๋ ค๋Š” ๊ฒŒ ์•„๋‹ˆ๋‹ค โ€” ์–ด๋…ธํ…Œ์ด์…˜/๋งต API ๊ฐ€ ํ•„์š”ํ•˜๋ฉด devkit ์„ ์“ด๋‹ค.
12
+ ์ด๊ฑด ์žฌ๊ตฌ์„ฑยท์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ฃจํ”„๊ฐ€ ๋งค๋ฒˆ ๋‘๋“œ๋ฆฌ๋Š” '๋œจ๊ฑฐ์šด ๊ฒฝ๋กœ' ๋งŒ ๋œ์–ด๋‚ด๋Š” ๊ฒƒ์ด๋‹ค.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import gzip
19
+ import json
20
+ import time
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+ from typing import Iterable, Iterator, Sequence
24
+
25
+ from nusim import CAMERAS, SINGAPORE_MAPS
26
+ from nusim.io import iter_json_array
27
+ from nusim.paths import DatasetPaths, cache_dir, dataset
28
+
29
+ INDEX_VERSION = 4 # 2: LIDAR_TOP ํ‚คํ”„๋ ˆ์ž„, 3: 3D ๋ฐ•์Šค ์–ด๋…ธํ…Œ์ด์…˜, 4: LIDAR_TOP sweep(20 Hz)
30
+ _INDEX_DIRNAME = "scene_index"
31
+ LIDAR = "LIDAR_TOP"
32
+
33
+
34
+ # --------------------------------------------------------------------------- ์ž๋ฃŒ๊ตฌ์กฐ
35
+ _log = logging.getLogger(__name__).info # ์ง„ํ–‰ ๋ฉ”์‹œ์ง€ ๊ธฐ๋ณธ ์ถœ๋ ฅ โ†’ nusim.log (์ฝ˜์†” + ๋กœ๊ทธ ํŒŒ์ผ)
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class CameraCalib:
40
+ """์ฐจ์ฒด(ego) ์ขŒํ‘œ๊ณ„ ๊ธฐ์ค€ ์นด๋ฉ”๋ผ ์™ธ๋ถ€/๋‚ด๋ถ€ ํŒŒ๋ผ๋ฏธํ„ฐ.
41
+
42
+ translation/rotation ์€ nuScenes ์˜ calibrated_sensor ๊ทธ๋Œ€๋กœ๋‹ค:
43
+ ์นด๋ฉ”๋ผ โ†’ ego ๋ณ€ํ™˜์ด๋ฉฐ rotation ์€ wxyz ์ฟผํ„ฐ๋‹ˆ์–ธ.
44
+ """
45
+
46
+ translation: tuple[float, float, float]
47
+ rotation: tuple[float, float, float, float] # w, x, y, z
48
+ intrinsic: tuple[tuple[float, float, float], ...] # 3x3
49
+ width: int
50
+ height: int
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class Frame:
55
+ """์นด๋ฉ”๋ผ ํ•œ ๋Œ€์˜ ํ•œ ์žฅ."""
56
+
57
+ timestamp: int # ๋งˆ์ดํฌ๋กœ์ดˆ
58
+ filename: str # ๋ฐ์ดํ„ฐ์…‹ ๋ฃจํŠธ ๊ธฐ์ค€ ์ƒ๋Œ€๊ฒฝ๋กœ
59
+ ego_translation: tuple[float, float, float]
60
+ ego_rotation: tuple[float, float, float, float] # w, x, y, z, ์ „์—ญ(๋งต) โ†’ ego
61
+ is_keyframe: bool
62
+
63
+ def path(self, paths: DatasetPaths) -> Path:
64
+ return paths.root / self.filename
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class Box:
69
+ """ํ‚คํ”„๋ ˆ์ž„ ํ•˜๋‚˜์—์„œ ๊ฐ์ฒด ํ•˜๋‚˜์˜ 3D ๋ฐ•์Šค. ์ „์—ญ(๋งต) ์ขŒํ‘œ.
70
+
71
+ nuScenes sample_annotation ๊ทธ๋Œ€๋กœ๋‹ค: size ๋Š” (w, l, h) โ€” ๋ฐ•์Šค ๋กœ์ปฌ x ๊ฐ€ ๊ธธ์ด(l),
72
+ y ๊ฐ€ ํญ(w). ๊ผญ์ง“์  ๊ณ„์‚ฐ์€ nusim.data.rig.box_corners ๋ฅผ ์“ด๋‹ค.
73
+ """
74
+
75
+ timestamp: int # ๋งˆ์ดํฌ๋กœ์ดˆ (ํ‚คํ”„๋ ˆ์ž„ sample ์˜ ์‹œ๊ฐ)
76
+ instance: int # SceneIndex.instances ์˜ ๋ฒˆํ˜ธ
77
+ translation: tuple[float, float, float]
78
+ size: tuple[float, float, float] # w, l, h
79
+ rotation: tuple[float, float, float, float] # w, x, y, z
80
+ attribute: str # 'vehicle.moving' ๋“ฑ. ์—†์œผ๋ฉด ''
81
+
82
+
83
+ @dataclass
84
+ class SceneIndex:
85
+ """ํ•œ ์”ฌ์˜ ์ „์ฒด ๋ฉ€ํ‹ฐ๋ทฐ ์ธ๋ฑ์Šค."""
86
+
87
+ token: str
88
+ name: str # 'scene-0001'
89
+ location: str # 'singapore-onenorth'
90
+ description: str
91
+ log_token: str
92
+ date_captured: str
93
+ vehicle: str
94
+ nbr_samples: int
95
+ calib: dict[str, CameraCalib] = field(default_factory=dict)
96
+ frames: dict[str, list[Frame]] = field(default_factory=dict)
97
+ # LIDAR_TOP ์ „๋ถ€ (sweep 20 Hz, ์”ฌ๋‹น ์•ฝ 400์žฅ). ํ‚คํ”„๋ ˆ์ž„(2 Hz)์€ is_keyframe ์œผ๋กœ ๊ตฌ๋ถ„ํ•œ๋‹ค.
98
+ # 3DGS ์ดˆ๊ธฐํ™”์™€ ๊นŠ์ด ๊ฐ๋…(์นด๋ฉ”๋ผ-๋ผ์ด๋‹ค ์œตํ•ฉ)์— ์“ด๋‹ค.
99
+ # lidar_calib ์˜ intrinsic ์€ ๋น„์–ด ์žˆ๋‹ค (nuScenes ๊ฐ€ ๋ผ์ด๋‹ค์— ์ฃผ์ง€ ์•Š์Œ).
100
+ lidar_calib: CameraCalib | None = None
101
+ lidar: list[Frame] = field(default_factory=list)
102
+
103
+ def lidar_keyframes(self) -> list[Frame]:
104
+ return [f for f in self.lidar if f.is_keyframe]
105
+ # 3D ๋ฐ•์Šค (ํ‚คํ”„๋ ˆ์ž„์—๋งŒ ์žˆ๋‹ค). instances[i] = (instance_token, category_name)
106
+ instances: list[tuple[str, str]] = field(default_factory=list)
107
+ boxes: list[Box] = field(default_factory=list)
108
+
109
+ @property
110
+ def n_images(self) -> int:
111
+ return sum(len(v) for v in self.frames.values())
112
+
113
+ def window(self, t0: float, t1: float) -> "SceneIndex":
114
+ """์”ฌ ์‹œ์ž‘๋ถ€ํ„ฐ [t0, t1] ์ดˆ ๊ตฌ๊ฐ„๋งŒ ๋‚จ๊ธด ์‚ฌ๋ณธ (์นด๋ฉ”๋ผยท๋ผ์ด๋‹ค ํ”„๋ ˆ์ž„). ๋ฐ•์Šค๋Š” ๊ทธ๋Œ€๋กœ โ€”
115
+ ๋น ๋ฅธ ์”ฌ(์ˆ˜๋ฐฑ m)์„ ๊ตฌ๊ฐ„๋ณ„๋กœ ๋‚˜๋ˆ  ์žฌ๊ตฌ์„ฑํ•˜๊ฑฐ๋‚˜ ์ง„๋‹จํ•  ๋•Œ ์“ด๋‹ค."""
116
+ from dataclasses import replace
117
+
118
+ start = min(f.timestamp for fr in self.frames.values() for f in fr[:1])
119
+ lo, hi = start + int(t0 * 1e6), start + int(t1 * 1e6)
120
+ keep = lambda fr: [f for f in fr if lo <= f.timestamp <= hi]
121
+ return replace(self, frames={c: keep(fr) for c, fr in self.frames.items()}, lidar=keep(self.lidar))
122
+
123
+ def duration_s(self) -> float:
124
+ ts = [f.timestamp for v in self.frames.values() for f in v]
125
+ return (max(ts) - min(ts)) / 1e6 if ts else 0.0
126
+
127
+ def keyframes(self, camera: str) -> list[Frame]:
128
+ return [f for f in self.frames.get(camera, ()) if f.is_keyframe]
129
+
130
+ # ---- ์ง๋ ฌํ™” (์บ์‹œ๋Š” ์‚ฌ๋žŒ์ด ์ฝ์„ ์ˆ˜ ์žˆ๋Š” gzip JSON) ----
131
+ def to_json(self) -> dict:
132
+ return {
133
+ "index_version": INDEX_VERSION,
134
+ "scene": {
135
+ "token": self.token, "name": self.name, "location": self.location,
136
+ "description": self.description, "log_token": self.log_token,
137
+ "date_captured": self.date_captured, "vehicle": self.vehicle,
138
+ "nbr_samples": self.nbr_samples,
139
+ },
140
+ "calib": {
141
+ cam: {
142
+ "translation": list(c.translation), "rotation": list(c.rotation),
143
+ "intrinsic": [list(r) for r in c.intrinsic],
144
+ "width": c.width, "height": c.height,
145
+ }
146
+ for cam, c in self.calib.items()
147
+ },
148
+ # ํ”„๋ ˆ์ž„์€ ๋ฆฌ์ŠคํŠธ-of-๋ฆฌ์ŠคํŠธ๋กœ ์ €์žฅํ•œ๋‹ค. ํ‚ค ์ด๋ฆ„์„ 60๋งŒ ๋ฒˆ ๋ฐ˜๋ณตํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค
149
+ # ํŒŒ์ผ์ด 3๋ฐฐ ์ž‘๊ณ  ํŒŒ์‹ฑ๋„ ๊ทธ๋งŒํผ ๋น ๋ฅด๋‹ค.
150
+ "frame_schema": ["timestamp", "filename", "ex", "ey", "ez", "qw", "qx", "qy", "qz", "is_keyframe"],
151
+ "frames": {
152
+ cam: [
153
+ [f.timestamp, f.filename, *f.ego_translation, *f.ego_rotation, int(f.is_keyframe)]
154
+ for f in fs
155
+ ]
156
+ for cam, fs in self.frames.items()
157
+ },
158
+ "lidar_calib": None if self.lidar_calib is None else {
159
+ "translation": list(self.lidar_calib.translation),
160
+ "rotation": list(self.lidar_calib.rotation),
161
+ },
162
+ "lidar": [
163
+ [f.timestamp, f.filename, *f.ego_translation, *f.ego_rotation, int(f.is_keyframe)]
164
+ for f in self.lidar
165
+ ],
166
+ "instances": [list(i) for i in self.instances],
167
+ "box_schema": ["timestamp", "instance", "tx", "ty", "tz", "w", "l", "h",
168
+ "qw", "qx", "qy", "qz", "attribute"],
169
+ "boxes": [
170
+ [b.timestamp, b.instance, *b.translation, *b.size, *b.rotation, b.attribute]
171
+ for b in self.boxes
172
+ ],
173
+ }
174
+
175
+ @classmethod
176
+ def from_json(cls, d: dict) -> "SceneIndex":
177
+ if d.get("index_version") != INDEX_VERSION:
178
+ raise ValueError(
179
+ f"์ธ๋ฑ์Šค ๋ฒ„์ „ ๋ถˆ์ผ์น˜ ({d.get('index_version')} != {INDEX_VERSION}). "
180
+ "'nusim index build --force' ๋กœ ๋‹ค์‹œ ๋งŒ๋“œ์„ธ์š”."
181
+ )
182
+ s = d["scene"]
183
+ obj = cls(**s)
184
+ obj.calib = {
185
+ cam: CameraCalib(
186
+ translation=tuple(c["translation"]), rotation=tuple(c["rotation"]),
187
+ intrinsic=tuple(tuple(r) for r in c["intrinsic"]),
188
+ width=c["width"], height=c["height"],
189
+ )
190
+ for cam, c in d["calib"].items()
191
+ }
192
+ obj.frames = {
193
+ cam: [
194
+ Frame(timestamp=r[0], filename=r[1],
195
+ ego_translation=(r[2], r[3], r[4]),
196
+ ego_rotation=(r[5], r[6], r[7], r[8]),
197
+ is_keyframe=bool(r[9]))
198
+ for r in rows
199
+ ]
200
+ for cam, rows in d["frames"].items()
201
+ }
202
+ if lc := d.get("lidar_calib"):
203
+ obj.lidar_calib = CameraCalib(
204
+ translation=tuple(lc["translation"]), rotation=tuple(lc["rotation"]),
205
+ intrinsic=(), width=0, height=0,
206
+ )
207
+ obj.lidar = [
208
+ Frame(timestamp=r[0], filename=r[1],
209
+ ego_translation=(r[2], r[3], r[4]),
210
+ ego_rotation=(r[5], r[6], r[7], r[8]),
211
+ is_keyframe=bool(r[9]))
212
+ for r in d.get("lidar", ())
213
+ ]
214
+ obj.instances = [tuple(i) for i in d.get("instances", ())]
215
+ obj.boxes = [
216
+ Box(timestamp=r[0], instance=r[1], translation=(r[2], r[3], r[4]),
217
+ size=(r[5], r[6], r[7]), rotation=(r[8], r[9], r[10], r[11]), attribute=r[12])
218
+ for r in d.get("boxes", ())
219
+ ]
220
+ return obj
221
+
222
+
223
+ # --------------------------------------------------------------------------- ๋นŒ๋“œ
224
+ def _index_dir() -> Path:
225
+ d = cache_dir() / _INDEX_DIRNAME
226
+ d.mkdir(parents=True, exist_ok=True)
227
+ return d
228
+
229
+
230
+ def build_index(
231
+ paths: DatasetPaths | None = None,
232
+ locations: Sequence[str] = SINGAPORE_MAPS,
233
+ cameras: Sequence[str] = CAMERAS,
234
+ include_sweeps: bool = True,
235
+ force: bool = False,
236
+ log=_log,
237
+ ) -> dict:
238
+ """ํ…Œ์ด๋ธ”์„ ํ›‘์–ด ์”ฌ๋ณ„ ์ธ๋ฑ์Šค๋ฅผ ์บ์‹œ์— ์“ด๋‹ค. ์นดํƒˆ๋กœ๊ทธ(dict)๋ฅผ ๋Œ๋ ค์ค€๋‹ค.
239
+
240
+ include_sweeps=False ๋ฉด ํ‚คํ”„๋ ˆ์ž„(2 Hz)๋งŒ ๋‹ด๋Š”๋‹ค. ์žฌ๊ตฌ์„ฑ ํ’ˆ์งˆ์€ ๋–จ์–ด์ง€์ง€๋งŒ
241
+ ์ธ๋ฑ์Šค๊ฐ€ 12๋ฐฐ ์ž‘์•„์ ธ ๋น ๋ฅธ ์‹คํ—˜์— ์“ธ ๋งŒํ•˜๋‹ค.
242
+ """
243
+ paths = paths or dataset()
244
+ out = _index_dir()
245
+ catalog_path = out / "catalog.json"
246
+
247
+ if catalog_path.exists() and not force:
248
+ log(f"์นดํƒˆ๋กœ๊ทธ๊ฐ€ ์ด๋ฏธ ์žˆ์Šต๋‹ˆ๋‹ค: {catalog_path} (--force ๋กœ ์žฌ์ƒ์„ฑ)")
249
+ return json.loads(catalog_path.read_text())
250
+
251
+ t0 = time.time()
252
+
253
+ # --- 1. ์ž‘์€ ํ…Œ์ด๋ธ”๋“ค (ํ•ฉ์ณ์„œ 8 MB ๋‚จ์ง“) --------------------------------
254
+ log("1/6 log / scene / sensor / calibrated_sensor ์ฝ๋Š” ์ค‘")
255
+ logs = {r["token"]: r for r in iter_json_array(paths.meta / "log.json")}
256
+ sensors = {r["token"]: r for r in iter_json_array(paths.meta / "sensor.json")}
257
+ calibs = {r["token"]: r for r in iter_json_array(paths.meta / "calibrated_sensor.json")}
258
+
259
+ # calibrated_sensor โ†’ ์นด๋ฉ”๋ผ ์ฑ„๋„ ์ด๋ฆ„. sample_data ํ•„ํ„ฐ๋ง์— ์ด๊ฑธ ์“ด๋‹ค.
260
+ cam_set = set(cameras)
261
+ calib_channel = {
262
+ tok: sensors[c["sensor_token"]]["channel"]
263
+ for tok, c in calibs.items()
264
+ if sensors[c["sensor_token"]]["channel"] in cam_set
265
+ }
266
+ lidar_calibs = {
267
+ tok for tok, c in calibs.items() if sensors[c["sensor_token"]]["channel"] == LIDAR
268
+ }
269
+
270
+ wanted_loc = set(locations)
271
+ scenes: dict[str, dict] = {}
272
+ for r in iter_json_array(paths.meta / "scene.json"):
273
+ lg = logs[r["log_token"]]
274
+ if lg["location"] in wanted_loc:
275
+ scenes[r["token"]] = {**r, "_log": lg}
276
+ log(f" ๋Œ€์ƒ ์”ฌ {len(scenes)}๊ฐœ ({', '.join(sorted(wanted_loc))})")
277
+ if not scenes:
278
+ raise RuntimeError(f"location {sorted(wanted_loc)} ์— ํ•ด๋‹นํ•˜๋Š” ์”ฌ์ด ์—†์Šต๋‹ˆ๋‹ค.")
279
+
280
+ # --- 2. sample โ†’ scene (7 MB) -------------------------------------------
281
+ log("2/6 sample.json ์ฝ๋Š” ์ค‘")
282
+ sample_scene: dict[str, str] = {}
283
+ sample_ts: dict[str, int] = {}
284
+ for r in iter_json_array(paths.meta / "sample.json"):
285
+ if r["scene_token"] in scenes:
286
+ sample_scene[r["token"]] = r["scene_token"]
287
+ sample_ts[r["token"]] = r["timestamp"]
288
+ log(f" ํ‚คํ”„๋ ˆ์ž„ ์ƒ˜ํ”Œ {len(sample_scene):,}๊ฐœ")
289
+
290
+ # --- 3. sample_data ์ŠคํŠธ๋ฆฌ๋ฐ (1.35 GB) ----------------------------------
291
+ # ๊ฐ€์žฅ ๋น„์‹ผ ๋‹จ๊ณ„. ํ•œ ๋ฒˆ๋งŒ ํ›‘์œผ๋ฉด์„œ ํ•„์š”ํ•œ ํ–‰๋งŒ ๊ฑด์ง„๋‹ค.
292
+ log("3/6 sample_data.json ์ŠคํŠธ๋ฆฌ๋ฐ (1.35 GB โ€” 30์ดˆ ๋‚ด์™ธ)")
293
+ per_scene: dict[str, dict[str, list]] = {t: {c: [] for c in cameras} for t in scenes}
294
+ need_pose: set[str] = set()
295
+ scene_calib: dict[str, dict[str, str]] = {t: {} for t in scenes}
296
+ per_scene_lidar: dict[str, list] = {t: [] for t in scenes}
297
+ scene_lidar_calib: dict[str, str] = {}
298
+ seen = kept = 0
299
+
300
+ for r in iter_json_array(paths.meta / "sample_data.json"):
301
+ seen += 1
302
+ cs = r["calibrated_sensor_token"]
303
+ chan = calib_channel.get(cs)
304
+ if chan is None:
305
+ # ๋ผ์ด๋‹ค๋Š” sweep ๊นŒ์ง€ ์ „๋ถ€ ๊ฑด์ง„๋‹ค (RADAR ๋Š” ๋ฒ„๋ฆฐ๋‹ค).
306
+ if cs in lidar_calibs:
307
+ st = sample_scene.get(r["sample_token"])
308
+ if st is not None:
309
+ per_scene_lidar[st].append(
310
+ (r["timestamp"], r["filename"], r["ego_pose_token"], bool(r["is_key_frame"])))
311
+ need_pose.add(r["ego_pose_token"])
312
+ scene_lidar_calib.setdefault(st, cs)
313
+ continue
314
+ st = sample_scene.get(r["sample_token"])
315
+ if st is None: # ์‹ฑ๊ฐ€ํฌ๋ฅด ์”ฌ์ด ์•„๋‹˜
316
+ continue
317
+ if not include_sweeps and not r["is_key_frame"]:
318
+ continue
319
+ per_scene[st][chan].append(
320
+ (r["timestamp"], r["filename"], r["ego_pose_token"], r["is_key_frame"])
321
+ )
322
+ need_pose.add(r["ego_pose_token"])
323
+ scene_calib[st].setdefault(chan, r["calibrated_sensor_token"])
324
+ kept += 1
325
+ log(f" {seen:,}ํ–‰ ์ค‘ {kept:,}ํ–‰ ์ฑ„ํƒ, ego_pose {len(need_pose):,}๊ฐœ ํ•„์š”")
326
+
327
+ # --- 4. ego_pose ์ŠคํŠธ๋ฆฌ๋ฐ (645 MB) --------------------------------------
328
+ log("4/6 ego_pose.json ์ŠคํŠธ๋ฆฌ๋ฐ (645 MB)")
329
+ poses: dict[str, tuple] = {}
330
+ for r in iter_json_array(paths.meta / "ego_pose.json"):
331
+ if r["token"] in need_pose:
332
+ t = r["translation"]
333
+ q = r["rotation"]
334
+ poses[r["token"]] = (t[0], t[1], t[2], q[0], q[1], q[2], q[3])
335
+ missing = len(need_pose) - len(poses)
336
+ if missing:
337
+ raise RuntimeError(f"ego_pose {missing}๊ฐœ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค โ€” ๋ฐ์ดํ„ฐ์…‹์ด ๋ถˆ์™„์ „ํ•ฉ๋‹ˆ๋‹ค.")
338
+
339
+ # --- 5. 3D ๋ฐ•์Šค (sample_annotation 583 MB) ------------------------------
340
+ log("5/6 sample_annotation.json ์ŠคํŠธ๋ฆฌ๋ฐ (583 MB)")
341
+ categories = {r["token"]: r["name"] for r in iter_json_array(paths.meta / "category.json")}
342
+ attributes = {r["token"]: r["name"] for r in iter_json_array(paths.meta / "attribute.json")}
343
+ inst_cat = {r["token"]: categories[r["category_token"]]
344
+ for r in iter_json_array(paths.meta / "instance.json")}
345
+ per_scene_inst: dict[str, dict[str, int]] = {t: {} for t in scenes}
346
+ per_scene_boxes: dict[str, list] = {t: [] for t in scenes}
347
+ n_box = 0
348
+ for r in iter_json_array(paths.meta / "sample_annotation.json"):
349
+ st = sample_scene.get(r["sample_token"])
350
+ if st is None:
351
+ continue
352
+ ids = per_scene_inst[st]
353
+ inst = ids.setdefault(r["instance_token"], len(ids))
354
+ attr = attributes[r["attribute_tokens"][0]] if r["attribute_tokens"] else ""
355
+ per_scene_boxes[st].append(Box(
356
+ timestamp=sample_ts[r["sample_token"]], instance=inst,
357
+ translation=tuple(r["translation"]), size=tuple(r["size"]),
358
+ rotation=tuple(r["rotation"]), attribute=attr,
359
+ ))
360
+ n_box += 1
361
+ log(f" ๋ฐ•์Šค {n_box:,}๊ฐœ")
362
+
363
+ # --- 6. ์”ฌ๋ณ„๋กœ ์“ฐ๊ธฐ ------------------------------------------------------
364
+ log("6/6 ์”ฌ๋ณ„ ์ธ๋ฑ์Šค ๊ธฐ๋ก")
365
+ catalog = {
366
+ "index_version": INDEX_VERSION,
367
+ "built_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
368
+ "dataset_root": str(paths.root),
369
+ "version": paths.version,
370
+ "locations": sorted(wanted_loc),
371
+ "cameras": list(cameras),
372
+ "include_sweeps": include_sweeps,
373
+ "scenes": [],
374
+ }
375
+
376
+ for tok, sc in sorted(scenes.items(), key=lambda kv: kv[1]["name"]):
377
+ si = SceneIndex(
378
+ token=tok, name=sc["name"], location=sc["_log"]["location"],
379
+ description=sc["description"], log_token=sc["log_token"],
380
+ date_captured=sc["_log"]["date_captured"], vehicle=sc["_log"]["vehicle"],
381
+ nbr_samples=sc["nbr_samples"],
382
+ )
383
+ for cam, cs_tok in scene_calib[tok].items():
384
+ c = calibs[cs_tok]
385
+ # ์ด ์ฑ„๋„์˜ ์ฒซ ํ”„๋ ˆ์ž„์—์„œ ํ•ด์ƒ๋„๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค (์”ฌ ์•ˆ์—์„œ๋Š” ๋ถˆ๋ณ€).
386
+ si.calib[cam] = CameraCalib(
387
+ translation=tuple(c["translation"]),
388
+ rotation=tuple(c["rotation"]),
389
+ intrinsic=tuple(tuple(row) for row in c["camera_intrinsic"]),
390
+ width=1600, height=900, # nuScenes ์นด๋ฉ”๋ผ๋Š” ์ „๋ถ€ 1600x900
391
+ )
392
+ for cam in cameras:
393
+ rows = sorted(per_scene[tok][cam])
394
+ si.frames[cam] = [
395
+ Frame(timestamp=ts, filename=fn,
396
+ ego_translation=poses[ep][:3], ego_rotation=poses[ep][3:],
397
+ is_keyframe=bool(kf))
398
+ for ts, fn, ep, kf in rows
399
+ ]
400
+ if cs_tok := scene_lidar_calib.get(tok):
401
+ c = calibs[cs_tok]
402
+ si.lidar_calib = CameraCalib(
403
+ translation=tuple(c["translation"]), rotation=tuple(c["rotation"]),
404
+ intrinsic=(), width=0, height=0,
405
+ )
406
+ si.lidar = [
407
+ Frame(timestamp=ts, filename=fn,
408
+ ego_translation=poses[ep][:3], ego_rotation=poses[ep][3:],
409
+ is_keyframe=kf)
410
+ for ts, fn, ep, kf in sorted(per_scene_lidar[tok])
411
+ ]
412
+ si.instances = [(it, inst_cat[it]) for it, _ in
413
+ sorted(per_scene_inst[tok].items(), key=lambda kv: kv[1])]
414
+ si.boxes = sorted(per_scene_boxes[tok], key=lambda b: (b.timestamp, b.instance))
415
+ _write_scene(si)
416
+ catalog["scenes"].append({
417
+ "name": si.name, "token": si.token, "location": si.location,
418
+ "n_images": si.n_images, "nbr_samples": si.nbr_samples,
419
+ "duration_s": round(si.duration_s(), 2), "description": si.description,
420
+ })
421
+
422
+ catalog_path.write_text(json.dumps(catalog, indent=2, ensure_ascii=False))
423
+ total_img = sum(s["n_images"] for s in catalog["scenes"])
424
+ log(f"์™„๋ฃŒ: ์”ฌ {len(catalog['scenes'])}๊ฐœ / ์ด๋ฏธ์ง€ {total_img:,}์žฅ / {time.time()-t0:.1f}์ดˆ")
425
+ log(f" โ†’ {out}")
426
+ return catalog
427
+
428
+
429
+ def _write_scene(si: SceneIndex) -> Path:
430
+ p = _index_dir() / f"{si.name}.json.gz"
431
+ with gzip.open(p, "wt", encoding="utf-8") as f:
432
+ json.dump(si.to_json(), f, ensure_ascii=False)
433
+ return p
434
+
435
+
436
+ # --------------------------------------------------------------------------- ์กฐํšŒ
437
+ def load_catalog() -> dict:
438
+ p = _index_dir() / "catalog.json"
439
+ if not p.exists():
440
+ raise FileNotFoundError(
441
+ f"์”ฌ ์ธ๋ฑ์Šค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค ({p}).\n๋จผ์ € ๋งŒ๋“œ์„ธ์š”: nusim index build"
442
+ )
443
+ return json.loads(p.read_text())
444
+
445
+
446
+ def load_scene(name: str) -> SceneIndex:
447
+ """'scene-0001' ๊ฐ™์€ ์ด๋ฆ„์œผ๋กœ ์”ฌ ์ธ๋ฑ์Šค๋ฅผ ์ฝ๋Š”๋‹ค."""
448
+ p = _index_dir() / f"{name}.json.gz"
449
+ if not p.exists():
450
+ raise FileNotFoundError(f"{name} ์ธ๋ฑ์Šค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค ({p}). 'nusim index build' ๋ฅผ ๋จผ์ € ์‹คํ–‰ํ•˜์„ธ์š”.")
451
+ with gzip.open(p, "rt", encoding="utf-8") as f:
452
+ return SceneIndex.from_json(json.load(f))
453
+
454
+
455
+ def iter_scenes(location: str | None = None) -> Iterator[SceneIndex]:
456
+ for entry in load_catalog()["scenes"]:
457
+ if location and entry["location"] != location:
458
+ continue
459
+ yield load_scene(entry["name"])
src/nusim/nusim/data/rig.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """์นด๋ฉ”๋ผ ๋ฆฌ๊ทธ ์ˆ˜ํ•™ โ€” nuScenes ํฌ์ฆˆ๋ฅผ ๋ Œ๋”๋Ÿฌ๊ฐ€ ์“ฐ๋Š” ํ–‰๋ ฌ๋กœ ๋ฐ”๊พผ๋‹ค.
2
+
3
+ ์ขŒํ‘œ๊ณ„ ์•ฝ์† (์ด ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ ๊ณ ์ •):
4
+
5
+ global / map nuScenes ๋งต ์ขŒํ‘œ. x=๋™, y=๋ถ, z=์œ„. ๋ฏธํ„ฐ.
6
+ ego ์ฐจ์ฒด. x=์ „๋ฐฉ, y=์ขŒ, z=์ƒ.
7
+ camera OpenCV ๊ด€๋ก€. x=์šฐ, y=ํ•˜, z=์ „๋ฐฉ. โ† nuScenes intrinsic ์ด ์ด๊ฑธ ๊ฐ€์ •ํ•œ๋‹ค
8
+ clip Vulkan ๊ด€๋ก€. NDC x,y โˆˆ [-1,1], y ์•„๋ž˜๊ฐ€ +, z โˆˆ [0,1].
9
+
10
+ nuScenes ์›๋ณธ์˜ ์˜๋ฏธ:
11
+ ego_pose .translation/.rotation = ego โ†’ global
12
+ calibrated_sensor .translation/.rotation = camera โ†’ ego
13
+ sample_annotation .translation/.rotation = box โ†’ global, ๋ฐ•์Šค ์ค‘์‹ฌ ๊ธฐ์ค€
14
+ .size = (w, l, h). ๋ฐ•์Šค ๋กœ์ปฌ x ๊ฐ€ ๊ธธ์ด(l), y ๊ฐ€ ํญ(w), z ๊ฐ€ ๋†’์ด
15
+ ์ฟผํ„ฐ๋‹ˆ์–ธ์€ ์ „๋ถ€ (w, x, y, z) ์ˆœ์„œ๋‹ค.
16
+
17
+ ๋”ฐ๋ผ์„œ camera โ†’ global = ego_pose โˆ˜ calib ์ด๊ณ ,
18
+ ๋ Œ๋”๋Ÿฌ๊ฐ€ ํ•„์š”๋กœ ํ•˜๋Š” view ํ–‰๋ ฌ์€ ๊ทธ ์—ญ์ด๋‹ค.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+
25
+ import numpy as np
26
+
27
+ from nusim.data.index import CameraCalib, Frame
28
+
29
+
30
+ def quat_to_matrix(q: tuple[float, float, float, float]) -> np.ndarray:
31
+ """(w,x,y,z) ์ฟผํ„ฐ๋‹ˆ์–ธ โ†’ 3x3 ํšŒ์ „ํ–‰๋ ฌ.
32
+
33
+ pyquaternion ์„ ์“ฐ์ง€ ์•Š๋Š” ์ด์œ : ์ด ํ•จ์ˆ˜๋Š” ํ”„๋ ˆ์ž„๋งˆ๋‹ค 6๋ฒˆ์”ฉ ์ˆ˜์‹ญ๋งŒ ๋ฒˆ ๋ถˆ๋ฆฐ๋‹ค.
34
+ ์˜์กด์„ฑ ์—†๋Š” ์ˆœ์ˆ˜ numpy ์ชฝ์ด 10๋ฐฐ ์ด์ƒ ๋น ๋ฅด๊ณ , ์‹๋„ ์งง๋‹ค.
35
+ """
36
+ w, x, y, z = q
37
+ n = w * w + x * x + y * y + z * z
38
+ if n < 1e-12:
39
+ return np.eye(3)
40
+ s = 2.0 / n
41
+ wx, wy, wz = s * w * x, s * w * y, s * w * z
42
+ xx, xy, xz = s * x * x, s * x * y, s * x * z
43
+ yy, yz, zz = s * y * y, s * y * z, s * z * z
44
+ return np.array([
45
+ [1.0 - (yy + zz), xy - wz, xz + wy],
46
+ [xy + wz, 1.0 - (xx + zz), yz - wx],
47
+ [xz - wy, yz + wx, 1.0 - (xx + yy)],
48
+ ])
49
+
50
+
51
+ def matrix_to_quat(r: np.ndarray) -> tuple[float, float, float, float]:
52
+ """3x3 ํšŒ์ „ํ–‰๋ ฌ โ†’ (w,x,y,z) ์ฟผํ„ฐ๋‹ˆ์–ธ (quat_to_matrix ์˜ ์—ญ, w โ‰ฅ 0). Shepperd ๋ฐฉ๋ฒ•."""
53
+ m = np.asarray(r, dtype=np.float64)
54
+ tr = m[0, 0] + m[1, 1] + m[2, 2]
55
+ if tr > 0:
56
+ s = 2.0 * np.sqrt(tr + 1.0)
57
+ q = (0.25 * s, (m[2, 1] - m[1, 2]) / s, (m[0, 2] - m[2, 0]) / s, (m[1, 0] - m[0, 1]) / s)
58
+ elif m[0, 0] > m[1, 1] and m[0, 0] > m[2, 2]:
59
+ s = 2.0 * np.sqrt(1.0 + m[0, 0] - m[1, 1] - m[2, 2])
60
+ q = ((m[2, 1] - m[1, 2]) / s, 0.25 * s, (m[0, 1] + m[1, 0]) / s, (m[0, 2] + m[2, 0]) / s)
61
+ elif m[1, 1] > m[2, 2]:
62
+ s = 2.0 * np.sqrt(1.0 + m[1, 1] - m[0, 0] - m[2, 2])
63
+ q = ((m[0, 2] - m[2, 0]) / s, (m[0, 1] + m[1, 0]) / s, 0.25 * s, (m[1, 2] + m[2, 1]) / s)
64
+ else:
65
+ s = 2.0 * np.sqrt(1.0 + m[2, 2] - m[0, 0] - m[1, 1])
66
+ q = ((m[1, 0] - m[0, 1]) / s, (m[0, 2] + m[2, 0]) / s, (m[1, 2] + m[2, 1]) / s, 0.25 * s)
67
+ q = np.asarray(q)
68
+ q = q / np.linalg.norm(q)
69
+ if q[0] < 0:
70
+ q = -q
71
+ return tuple(float(x) for x in q)
72
+
73
+
74
+ def rt_to_matrix(r: np.ndarray, t) -> np.ndarray:
75
+ """3x3 ํšŒ์ „ + 3๋ฒกํ„ฐ โ†’ 4x4 ๋™์ฐจ๋ณ€ํ™˜."""
76
+ m = np.eye(4)
77
+ m[:3, :3] = r
78
+ m[:3, 3] = t
79
+ return m
80
+
81
+
82
+ def box_corners(translation, size, rotation, pad: float = 0.0) -> np.ndarray:
83
+ """3D ๋ฐ•์Šค โ†’ ์ „์—ญ ์ขŒํ‘œ ๊ผญ์ง“์  (8,3). devkit Box.corners() ์™€ ๊ฐ™์€ ์ˆœ์„œ.
84
+
85
+ pad ๋Š” ๊ฐ ๋ฐฉํ–ฅ์œผ๋กœ ๋”ํ•˜๋Š” ์—ฌ์œ (m). ์–ด๋…ธํ…Œ์ด์…˜ ๋ฐ•์Šค๋Š” ๊ฐ์ฒด์— ๋”ฑ ๋ถ™์–ด ์žˆ์–ด์„œ
86
+ ๊ทธ๋ฆผ์žยท๋ฐฑ๋ฏธ๋Ÿฌยท๋ณดํ–‰์ž ํŒ”๋‹ค๋ฆฌ๊ฐ€ ์‚์ ธ๋‚˜์˜จ๋‹ค.
87
+ """
88
+ w, l, h = (np.asarray(size, dtype=np.float64) + 2 * pad)
89
+ x = l / 2 * np.array([1, 1, 1, 1, -1, -1, -1, -1])
90
+ y = w / 2 * np.array([1, -1, -1, 1, 1, -1, -1, 1])
91
+ z = h / 2 * np.array([1, 1, -1, -1, 1, 1, -1, -1])
92
+ return np.stack([x, y, z], axis=1) @ quat_to_matrix(rotation).T + np.asarray(translation)
93
+
94
+
95
+ def points_in_box(pts: np.ndarray, translation, size, rotation, pad: float = 0.0) -> np.ndarray:
96
+ """(N,3) ์ „์—ญ ์ขŒํ‘œ ์  ์ค‘ ๋ฐ•์Šค ์•ˆ์— ์žˆ๋Š” ๊ฒƒ์˜ bool ๋งˆ์Šคํฌ."""
97
+ local = (pts - np.asarray(translation)) @ quat_to_matrix(rotation) # R^T ๋ฅผ ๊ณฑํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™๋‹ค
98
+ w, l, h = (np.asarray(size, dtype=np.float64) + 2 * pad) / 2
99
+ return (np.abs(local[:, 0]) <= l) & (np.abs(local[:, 1]) <= w) & (np.abs(local[:, 2]) <= h)
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class CameraPose:
104
+ """ํ•œ ์‹œ์ ์˜ ์นด๋ฉ”๋ผ ํ•˜๋‚˜. ๋ Œ๋”๋Ÿฌ์— ๋„˜๊ธธ ๊ฒƒ์ด ์ „๋ถ€ ๋“ค์–ด์žˆ๋‹ค."""
105
+
106
+ channel: str
107
+ timestamp: int
108
+ c2w: np.ndarray # 4x4, camera โ†’ global
109
+ K: np.ndarray # 3x3 intrinsic
110
+ width: int
111
+ height: int
112
+ filename: str = ""
113
+
114
+ @property
115
+ def w2c(self) -> np.ndarray:
116
+ """view ํ–‰๋ ฌ. ํšŒ์ „์€ ์ „์น˜, ํ‰ํ–‰์ด๋™์€ ๊ทธ์— ๋งž์ถฐ ๋‹ค์‹œ ๊ณ„์‚ฐํ•œ๋‹ค
117
+ (์ผ๋ฐ˜ ์—ญํ–‰๋ ฌ์„ ์“ฐ๋ฉด ์ˆ˜์น˜์˜ค์ฐจ๊ฐ€ ๋ˆ„์ ๋œ๋‹ค)."""
118
+ r = self.c2w[:3, :3]
119
+ m = np.eye(4)
120
+ m[:3, :3] = r.T
121
+ m[:3, 3] = -r.T @ self.c2w[:3, 3]
122
+ return m
123
+
124
+ @property
125
+ def position(self) -> np.ndarray:
126
+ return self.c2w[:3, 3].copy()
127
+
128
+ def projection(self, znear: float = 0.1, zfar: float = 200.0) -> np.ndarray:
129
+ """intrinsic โ†’ Vulkan clip ๊ณต๊ฐ„ 4x4 ํˆฌ์˜ํ–‰๋ ฌ.
130
+
131
+ OpenCV ์นด๋ฉ”๋ผ(y ์•„๋ž˜, z ์ „๋ฐฉ)์™€ Vulkan NDC(y ์•„๋ž˜, z โˆˆ [0,1]) ๋Š” ์ถ• ๋ฐฉํ–ฅ์ด ๊ฐ™๋‹ค.
132
+ ๊ทธ๋ž˜์„œ OpenGL ์‹๊ณผ ๋‹ฌ๋ฆฌ y ๋’ค์ง‘๊ธฐ๋„, z ๋ถ€ํ˜ธ ๋ฐ˜์ „๋„ ์—†๋‹ค. ์ •ํ™•ํžˆ
133
+ ndc_x = 2u/w - 1, ndc_y = 2v/h - 1 (u,v = K ๋กœ ํˆฌ์˜ํ•œ ํ”ฝ์…€ ์ขŒํ‘œ)
134
+ ๊ฐ€ ์„ฑ๋ฆฝํ•˜๊ณ , tests/test_rig.py ๊ฐ€ ์ด๊ฑธ ํ”ฝ์…€ ๋‹จ์œ„๋กœ ๊ฒ€์ฆํ•œ๋‹ค.
135
+ """
136
+ fx, fy = self.K[0, 0], self.K[1, 1]
137
+ cx, cy = self.K[0, 2], self.K[1, 2]
138
+ w, h = self.width, self.height
139
+
140
+ p = np.zeros((4, 4))
141
+ p[0, 0] = 2.0 * fx / w
142
+ p[1, 1] = 2.0 * fy / h
143
+ # ์ฃผ์  ์˜คํ”„์…‹. u = fxยทx/z + cx ์ด๋ฏ€๋กœ 2u/w-1 = (2fx/w)ยทx/z + (2cx/w - 1).
144
+ p[0, 2] = 2.0 * cx / w - 1.0
145
+ p[1, 2] = 2.0 * cy / h - 1.0
146
+ # ๊นŠ์ด [znear, zfar] โ†’ [0, 1]
147
+ p[2, 2] = zfar / (zfar - znear)
148
+ p[2, 3] = -zfar * znear / (zfar - znear)
149
+ p[3, 2] = 1.0
150
+ return p
151
+
152
+ def fov_deg(self) -> tuple[float, float]:
153
+ fx, fy = self.K[0, 0], self.K[1, 1]
154
+ return (
155
+ float(np.degrees(2 * np.arctan(self.width / (2 * fx)))),
156
+ float(np.degrees(2 * np.arctan(self.height / (2 * fy)))),
157
+ )
158
+
159
+
160
+ def camera_pose(frame: Frame, calib: CameraCalib, channel: str) -> CameraPose:
161
+ """ํ•œ ํ”„๋ ˆ์ž„ + ๊ทธ ์ฑ„๋„์˜ ์บ˜๋ฆฌ๋ธŒ โ†’ CameraPose."""
162
+ ego2global = rt_to_matrix(quat_to_matrix(frame.ego_rotation), frame.ego_translation)
163
+ cam2ego = rt_to_matrix(quat_to_matrix(calib.rotation), calib.translation)
164
+ return CameraPose(
165
+ channel=channel,
166
+ timestamp=frame.timestamp,
167
+ c2w=ego2global @ cam2ego,
168
+ K=np.asarray(calib.intrinsic, dtype=np.float64),
169
+ width=calib.width,
170
+ height=calib.height,
171
+ filename=frame.filename,
172
+ )
173
+
174
+
175
+ def scene_poses(scene, cameras=None, keyframes_only: bool = False) -> list[CameraPose]:
176
+ """์”ฌ ์ „์ฒด์˜ ์นด๋ฉ”๋ผ ํฌ์ฆˆ๋ฅผ ์‹œ๊ฐ„์ˆœ์œผ๋กœ. 3DGS ํ•™์Šต๊ธฐ์˜ ์ž…๋ ฅ์ด ๋˜๋Š” ํ˜•ํƒœ๋‹ค."""
177
+ from nusim import CAMERAS
178
+
179
+ out: list[CameraPose] = []
180
+ for cam in cameras or CAMERAS:
181
+ calib = scene.calib.get(cam)
182
+ if calib is None:
183
+ continue
184
+ frames = scene.keyframes(cam) if keyframes_only else scene.frames.get(cam, ())
185
+ out.extend(camera_pose(f, calib, cam) for f in frames)
186
+ out.sort(key=lambda p: (p.timestamp, p.channel))
187
+ return out
188
+
189
+
190
+ def ego_trajectory(scene, channel: str = "CAM_FRONT") -> np.ndarray:
191
+ """(N, 8) ๋ฐฐ์—ด: t[s], x, y, z, qw, qx, qy, qz.
192
+
193
+ closed-loop ์‹œ์ž‘ ์ƒํƒœ์™€ ์›๋ณธ ๊ถค์  ๋น„๊ต(์˜คํ”ˆ๋ฃจํ”„ ๊ธฐ์ค€์„ )์— ์“ด๋‹ค.
194
+ """
195
+ frames = scene.frames.get(channel, [])
196
+ if not frames:
197
+ raise ValueError(f"{scene.name} ์— {channel} ํ”„๋ ˆ์ž„์ด ์—†์Šต๋‹ˆ๋‹ค")
198
+ t0 = frames[0].timestamp
199
+ return np.array(
200
+ [
201
+ [(f.timestamp - t0) / 1e6, *f.ego_translation, *f.ego_rotation]
202
+ for f in frames
203
+ ]
204
+ )
src/nusim/nusim/io/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """์ง๋ ฌํ™” ๊ณ„์ธต โ€” ๋Œ€์šฉ๋Ÿ‰ JSON ์ŠคํŠธ๋ฆฌ๋ฐ, 3DGS .ply ์ž…์ถœ๋ ฅ."""
2
+
3
+ from nusim.io.jsonstream import iter_json_array, load_table
4
+
5
+ __all__ = ["iter_json_array", "load_table"]
src/nusim/nusim/io/jsonstream.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """nuScenes ํ…Œ์ด๋ธ”์„ ์œ„ํ•œ ์ŠคํŠธ๋ฆฌ๋ฐ JSON ๋ฆฌ๋”.
2
+
3
+ ์™œ ํ•„์š”ํ•œ๊ฐ€:
4
+ v1.0-trainval ์˜ sample_data.json ์€ 1.3 GB, ego_pose.json ์€ 645 MB ๋‹ค.
5
+ json.load() ๋กœ ์ฝ์œผ๋ฉด ํŒŒ์ด์ฌ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„๊ฐ€ ์›๋ณธ์˜ 4~5๋ฐฐ๋กœ ๋ถ€ํ’€์–ด
6
+ sample_data ํ•˜๋‚˜์— 6 GB ์•ˆํŒŽ์„ ๋จน๋Š”๋‹ค. ๊ฒŒ๋‹ค๊ฐ€ ์ด ํŒŒ์ผ๋“ค์€ NFS ์œ„์— ์žˆ์–ด
7
+ ์ „์ฒด๋ฅผ ์ฝ๋Š” ๊ฒƒ ์ž์ฒด๊ฐ€ ๋น„์‹ธ๋‹ค.
8
+
9
+ ์—ฌ๊ธฐ์„œ๋Š” ์ตœ์ƒ์œ„ ๋ฐฐ์—ด์˜ ์›์†Œ๋ฅผ ํ•˜๋‚˜์”ฉ ํ˜๋ ค๋ณด๋‚ธ๋‹ค. ํ˜ธ์ถœ์ž๊ฐ€ ํ•„์š”ํ•œ ๊ฒƒ๋งŒ
10
+ ๊ณจ๋ผ ๋‹ด์œผ๋ฉด ํ”ผํฌ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ '๋ฒ„ํผ ํฌ๊ธฐ + ๊ณ ๋ฅธ ๊ฒƒ' ์œผ๋กœ ๋–จ์–ด์ง„๋‹ค.
11
+ ์‹ค์ธก: sample_data.json ์ „์ฒด ์ˆœํšŒ ์‹œ ํ”ผํฌ ~60 MB.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import re
18
+ from pathlib import Path
19
+ from typing import Any, Callable, Iterator
20
+
21
+ # ์›์†Œ ์‚ฌ์ด์˜ ๊ณต๋ฐฑ๊ณผ ์‰ผํ‘œ. raw_decode ๋Š” idx ๊ฐ€ ๊ฐ’์˜ ์ฒซ ๊ธ€์ž๋ฅผ ๊ฐ€๋ฆฌ์ผœ์•ผ ํ•œ๋‹ค.
22
+ _GAP = re.compile(r"[\s,]*")
23
+
24
+ _CHUNK = 1 << 22 # 4 MiB
25
+ # ๋ฒ„ํผ ์•ž์ชฝ์˜ ์†Œ๋น„๋œ ๋ถ€๋ถ„์„ ์ž˜๋ผ๋‚ด๋Š” ์ž„๊ณ„์น˜. ๋งค ์›์†Œ๋งˆ๋‹ค ์ž๋ฅด๋ฉด ์Šฌ๋ผ์ด์Šค ๋ณต์‚ฌ๊ฐ€
26
+ # O(nยฒ) ๊ฐ€ ๋˜๊ณ , ์•„์˜ˆ ์•ˆ ์ž๋ฅด๋ฉด ๋ฒ„ํผ๊ฐ€ ํŒŒ์ผ ํฌ๊ธฐ๋งŒํผ ์ž๋ž€๋‹ค.
27
+ _COMPACT_AT = 1 << 21 # 2 MiB
28
+
29
+
30
+ def iter_json_array(path: str | Path, chunk_size: int = _CHUNK) -> Iterator[Any]:
31
+ """์ตœ์ƒ์œ„๊ฐ€ JSON ๋ฐฐ์—ด์ธ ํŒŒ์ผ์˜ ์›์†Œ๋ฅผ ํ•˜๋‚˜์”ฉ ๋‚ด๋†“๋Š”๋‹ค.
32
+
33
+ nuScenes ์˜ ๋ชจ๋“  ํ…Œ์ด๋ธ”์ด ์ด ๋ชจ์–‘์ด๋‹ค. ๋‹ค๋ฅธ ๋ชจ์–‘์ด๋ฉด ValueError ๋ฅผ ๋‚ธ๋‹ค.
34
+ """
35
+ path = Path(path)
36
+ dec = json.JSONDecoder()
37
+
38
+ with path.open("r", encoding="utf-8") as f:
39
+ buf = f.read(chunk_size)
40
+ if not buf:
41
+ return
42
+
43
+ start = buf.find("[")
44
+ if start < 0:
45
+ raise ValueError(f"{path}: ์ตœ์ƒ์œ„ JSON ๋ฐฐ์—ด์ด ์•„๋‹™๋‹ˆ๋‹ค")
46
+ pos = start + 1
47
+
48
+ while True:
49
+ pos = _GAP.match(buf, pos).end()
50
+
51
+ # ๋ฒ„ํผ๊ฐ€ ๋ฐ”๋‹ฅ๋‚ฌ์œผ๋ฉด ๋” ์ฝ๋Š”๋‹ค.
52
+ if pos >= len(buf):
53
+ more = f.read(chunk_size)
54
+ if not more:
55
+ return # ๋‹ซ๋Š” ']' ์—†์ด ๋๋‚จ โ€” ์ž˜๋ฆฐ ํŒŒ์ผ์ด์ง€๋งŒ ์–ป์€ ๊ฑด ์ด๋ฏธ ๋ƒˆ๋‹ค
56
+ buf = buf[pos:] + more
57
+ pos = 0
58
+ continue
59
+
60
+ if buf[pos] == "]":
61
+ return
62
+
63
+ try:
64
+ obj, pos = dec.raw_decode(buf, pos)
65
+ except ValueError:
66
+ # ์›์†Œ๊ฐ€ ๋ฒ„ํผ ๊ฒฝ๊ณ„์— ๊ฑธ์ณค๋‹ค. ๋” ์ฝ์–ด์„œ ๋‹ค์‹œ ์‹œ๋„ํ•œ๋‹ค.
67
+ more = f.read(chunk_size)
68
+ if not more:
69
+ raise ValueError(f"{path}: ํŒŒ์ผ ๋์—์„œ JSON ์ด ์ž˜๋ ธ์Šต๋‹ˆ๋‹ค") from None
70
+ buf += more
71
+ continue
72
+
73
+ yield obj
74
+
75
+ if pos > _COMPACT_AT:
76
+ buf = buf[pos:]
77
+ pos = 0
78
+
79
+
80
+ def load_table(
81
+ path: str | Path,
82
+ keep: Callable[[dict], bool] | None = None,
83
+ fields: tuple[str, ...] | None = None,
84
+ ) -> list[dict]:
85
+ """ํ…Œ์ด๋ธ”์„ ์ฝ๋˜ ์›ํ•˜๋Š” ํ–‰๊ณผ ์—ด๋งŒ ๋‚จ๊ธด๋‹ค.
86
+
87
+ keep: ํ–‰ ํ•„ํ„ฐ. None ์ด๋ฉด ์ „๋ถ€.
88
+ fields: ๋‚จ๊ธธ ํ‚ค. None ์ด๋ฉด ์ „๋ถ€. ํฐ ํ…Œ์ด๋ธ”์—์„œ๋Š” ์ด๊ฑธ ์ฃผ๋Š” ๊ฒŒ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ํฌ๊ฒŒ ์ค„์ธ๋‹ค.
89
+ """
90
+ out: list[dict] = []
91
+ for row in iter_json_array(path):
92
+ if keep is not None and not keep(row):
93
+ continue
94
+ out.append({k: row[k] for k in fields} if fields else row)
95
+ return out
96
+
97
+
98
+ def index_table(path: str | Path, key: str = "token", fields: tuple[str, ...] | None = None) -> dict[str, dict]:
99
+ """ํ…Œ์ด๋ธ”์„ key(๊ธฐ๋ณธ 'token') ๋กœ ์ฐพ์„ ์ˆ˜ ์žˆ๋Š” dict ๋กœ ๋งŒ๋“ ๋‹ค."""
100
+ out: dict[str, dict] = {}
101
+ for row in iter_json_array(path):
102
+ out[row[key]] = {k: row[k] for k in fields} if fields else row
103
+ return out
src/nusim/nusim/io/ktx2.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """KTX2 (Khronos Texture 2.0) ์“ฐ๊ธฐ/์ฝ๊ธฐ โ€” Vulkan ๋ Œ๋”๋Ÿฌ์— ํ๋ธŒ๋งตยทLUT ๋ฅผ ๋„˜๊ธฐ๋Š” ํฌ๋งท.
2
+
3
+ https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html
4
+ ํ•„์š”ํ•œ ๊ฒƒ๋งŒ: 2D / ํ๋ธŒ๋งต, mip ์ฒด์ธ, ์ดˆ์••์ถ• ์—†์Œ(supercompression 0).
5
+ ๋ธ”๋ก ์••์ถ•(BC6H) ๋ฐ์ดํ„ฐ๋Š” ์ด๋ฏธ ์••์ถ•๋œ ๋ฐ”์ดํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋‹ด๋Š”๋‹ค.
6
+
7
+ ํŒŒ์ผ ์•ˆ ๋ฐ์ดํ„ฐ ์ˆœ์„œ๋Š” ์ŠคํŽ™๋Œ€๋กœ **์ž‘์€ mip ๋ถ€ํ„ฐ** ํฐ mip ์ˆœ์ด๊ณ , ๊ฐ mip ์•ˆ์—์„œ๋Š” ๋ฉด 0~5 ์ˆœ์„œ๋‹ค.
8
+ ๋ ˆ๋ฒจ ์ธ๋ฑ์Šค๋Š” mip 0(๊ฐ€์žฅ ํฐ ๊ฒƒ)๋ถ€ํ„ฐ ์ ๋Š”๋‹ค.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import math
14
+ import struct
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ IDENTIFIER = b"\xabKTX 20\xbb\r\n\x1a\n"
19
+
20
+ # ์“ฐ๋Š” VkFormat
21
+ VK_FORMAT_R16G16_SFLOAT = 83
22
+ VK_FORMAT_R16G16B16A16_SFLOAT = 97
23
+ VK_FORMAT_BC6H_UFLOAT_BLOCK = 143
24
+
25
+ _BLOCK = {VK_FORMAT_BC6H_UFLOAT_BLOCK: (4, 4, 16)} # ๋ธ”๋ก ๊ฐ€๋กœ, ์„ธ๋กœ, ๋ฐ”์ดํŠธ
26
+ _TEXEL = {VK_FORMAT_R16G16_SFLOAT: 4, VK_FORMAT_R16G16B16A16_SFLOAT: 8}
27
+
28
+
29
+ def level_size(vk_format: int, w: int, h: int) -> int:
30
+ if vk_format in _BLOCK:
31
+ bw, bh, nb = _BLOCK[vk_format]
32
+ return ((w + bw - 1) // bw) * ((h + bh - 1) // bh) * nb
33
+ return w * h * _TEXEL[vk_format]
34
+
35
+
36
+ def _dfd(vk_format: int) -> bytes:
37
+ """๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ํ˜•์‹ ์„œ์ˆ ์ž(DFD). ๋ทฐ์–ด๋Š” vkFormat ๋งŒ ๋ณด์ง€๋งŒ ์™ธ๋ถ€ ๋„๊ตฌ ํ˜ธํ™˜์„ ์œ„ํ•ด ๋„ฃ๋Š”๋‹ค."""
38
+ # KHR_DF_MODEL: RGBSDA=1, BC6H=131 / ์ „์†ก ํ•จ์ˆ˜ LINEAR=1 / ์›์ƒ‰ BT709=1
39
+ if vk_format == VK_FORMAT_BC6H_UFLOAT_BLOCK:
40
+ model, dims, plane0 = 133, (3, 3, 0, 0), 16 # KHR_DF_MODEL_BC6H (131 ์€ BC4)
41
+ # ์ƒ˜ํ”Œ 1๊ฐœ: 128 ๋น„ํŠธ ๋ธ”๋ก ์ „์ฒด, ์ฑ„๋„ COLOR(0), FLOAT ํ”Œ๋ž˜๊ทธ(0x80)
42
+ samples = [(0, 127, 0x80, 0, 0x3F800000)] # sampleUpper = 1.0f (ktx validate ๊ธฐ์ค€)
43
+ elif vk_format in (VK_FORMAT_R16G16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT):
44
+ n = 2 if vk_format == VK_FORMAT_R16G16_SFLOAT else 4
45
+ model, dims, plane0 = 1, (0, 0, 0, 0), 2 * n
46
+ chan = [0, 1, 2, 15][:n] # R, G, B, A
47
+ # FLOAT(0x80) | SIGNED(0x40). ๋ฒ”์œ„ -1..1 ์˜ float ๋น„ํŠธ
48
+ samples = [(16 * i, 15, 0xC0, chan[i], (0xBF800000, 0x3F800000)) for i in range(n)]
49
+ else:
50
+ raise ValueError(f"DFD ๋ฏธ์ง€์› vkFormat {vk_format}")
51
+ body = b""
52
+ for s in samples:
53
+ off, length, flags, ch, rng = s if len(s) == 5 else (*s, 0)
54
+ lower, upper = rng if isinstance(rng, tuple) else (0, rng)
55
+ body += struct.pack("<HBBBBBBII", off, length, (flags | ch) & 0xFF, 0, 0, 0, 0, lower, upper)
56
+ block_size = 24 + len(body)
57
+ header = struct.pack("<IHH", 0, 2, block_size) # vendorId KHRONOS(0)+descriptorType 0, version 2, ๋ธ”๋ก ํฌ๊ธฐ
58
+ header += struct.pack("<BBBB", model, 1, 1, 0) # colorModel, primaries BT709, transfer LINEAR, flags
59
+ header += struct.pack("<BBBB", *dims)
60
+ header += struct.pack("<8B", plane0, 0, 0, 0, 0, 0, 0, 0)
61
+ block = header + body
62
+ return struct.pack("<I", 4 + len(block)) + block
63
+
64
+
65
+ @dataclass
66
+ class Ktx2:
67
+ vk_format: int
68
+ width: int
69
+ height: int
70
+ faces: int
71
+ levels: list[list[bytes]] # levels[mip][face] โ€” mip 0 ์ด ๊ฐ€์žฅ ํฌ๋‹ค
72
+ kv: dict[str, str] | None = None
73
+
74
+
75
+ def write_ktx2(path: str | Path, tex: Ktx2) -> Path:
76
+ path = Path(path)
77
+ nlev = len(tex.levels)
78
+ for m, lv in enumerate(tex.levels):
79
+ w, h = max(1, tex.width >> m), max(1, tex.height >> m)
80
+ for f in lv:
81
+ if len(f) != level_size(tex.vk_format, w, h):
82
+ raise ValueError(f"mip {m} ํฌ๊ธฐ {len(f)} != ๊ธฐ๋Œ€ {level_size(tex.vk_format, w, h)}")
83
+ dfd = _dfd(tex.vk_format)
84
+ kvd = b""
85
+ for k, v in (tex.kv or {}).items():
86
+ kv = k.encode() + b"\0" + v.encode() + b"\0"
87
+ kvd += struct.pack("<I", len(kv)) + kv + b"\0" * ((-len(kv)) % 4)
88
+
89
+ type_size = 1 if tex.vk_format in _BLOCK else 2
90
+ # ์ŠคํŽ™: mip ๋ฐ์ดํ„ฐ ์ •๋ ฌ์€ ์ •ํ™•ํžˆ lcm(ํ…์…€/๋ธ”๋ก ๋ฐ”์ดํŠธ, 4). ๋” ํฌ๊ฒŒ ์žก์œผ๋ฉด ktx validate ๊ฐ€ ๊ฑฐ๋ถ€ํ•œ๋‹ค.
91
+ unit = _BLOCK[tex.vk_format][2] if tex.vk_format in _BLOCK else _TEXEL[tex.vk_format]
92
+ align = unit * 4 // math.gcd(unit, 4)
93
+ head_len = 12 + 4 * 9 + 4 * 4 + 8 * 2 + 24 * nlev
94
+ dfd_off = head_len
95
+ kvd_off = dfd_off + len(dfd)
96
+ data_start = kvd_off + len(kvd)
97
+ data_start += (-data_start) % align
98
+ # ์ž‘์€ mip ๋ถ€ํ„ฐ ๋ฐฐ์น˜
99
+ offsets = [0] * nlev
100
+ blob = b""
101
+ pos = data_start
102
+ for m in reversed(range(nlev)):
103
+ pad = (-pos) % align
104
+ blob += b"\0" * pad
105
+ pos += pad
106
+ offsets[m] = pos
107
+ lvl = b"".join(tex.levels[m])
108
+ blob += lvl
109
+ pos += len(lvl)
110
+
111
+ out = IDENTIFIER
112
+ out += struct.pack("<9I", tex.vk_format, type_size, tex.width, tex.height, 0, 0, tex.faces, nlev, 0)
113
+ out += struct.pack("<4I", dfd_off, len(dfd), kvd_off if kvd else 0, len(kvd))
114
+ out += struct.pack("<2Q", 0, 0)
115
+ for m in range(nlev):
116
+ n = len(b"".join(tex.levels[m]))
117
+ out += struct.pack("<3Q", offsets[m], n, n)
118
+ out += dfd + kvd
119
+ out += b"\0" * (data_start - len(out))
120
+ out += blob
121
+ path.parent.mkdir(parents=True, exist_ok=True)
122
+ path.write_bytes(out)
123
+ return path
124
+
125
+
126
+ def read_ktx2(path: str | Path) -> Ktx2:
127
+ b = Path(path).read_bytes()
128
+ if b[:12] != IDENTIFIER:
129
+ raise ValueError(f"{path}: KTX2 ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค")
130
+ vk_format, _ts, w, h, _d, _layers, faces, nlev, sc = struct.unpack_from("<9I", b, 12)
131
+ if sc:
132
+ raise ValueError(f"{path}: ์ดˆ์••์ถ•(supercompression {sc}) ๋ฏธ์ง€์›")
133
+ nlev = max(nlev, 1)
134
+ levels = []
135
+ for m in range(nlev):
136
+ off, n, _ = struct.unpack_from("<3Q", b, 12 + 36 + 16 + 16 + 24 * m)
137
+ per = n // faces
138
+ levels.append([b[off + f * per: off + (f + 1) * per] for f in range(faces)])
139
+ return Ktx2(vk_format, w, h, faces, levels)
src/nusim/nusim/io/ply.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ํ‘œ์ค€ 3DGS .ply ์ž…์ถœ๋ ฅ โ€” ํ•™์Šต๊ธฐ์™€ Vulkan ๋ Œ๋”๋Ÿฌ ์‚ฌ์ด์˜ ์œ ์ผํ•œ ์ ‘์ .
2
+
3
+ ํฌ๋งท์€ ์›๋ณธ 3DGS (Kerbl et al. 2023) ๊ตฌํ˜„์ด ์“ฐ๋Š” ๊ทธ๋Œ€๋กœ๋‹ค. ๊ทธ๋ž˜์„œ SIBR ๋ทฐ์–ด,
4
+ SuperSplat ๊ฐ™์€ ์™ธ๋ถ€ ๋„๊ตฌ๋กœ๋„ ์—ด๋ฆฐ๋‹ค. binary_little_endian, vertex ํ•˜๋‚˜์— float32:
5
+
6
+ x y z ์œ„์น˜ (์”ฌ ์›์  ๊ธฐ์ค€, ์•„๋ž˜ ์ฐธ๊ณ )
7
+ nx ny nz ํ•ญ์ƒ 0. ์›๋ณธ ํฌ๋งท์— ์ž๋ฆฌ๋งŒ ์žˆ๋‹ค
8
+ f_dc_0..2 SH 0์ฐจ ๊ณ„์ˆ˜ (RGB)
9
+ f_rest_0..3(K-1)-1 SH ๊ณ ์ฐจ ๊ณ„์ˆ˜. **์ฑ„๋„ ์šฐ์„ ** โ€” R ์˜ K-1 ๊ฐœ, G, B ์ˆœ์„œ
10
+ opacity logit (sigmoid ์ „)
11
+ scale_0..2 log (exp ์ „)
12
+ rot_0..3 ์ฟผํ„ฐ๋‹ˆ์–ธ (w,x,y,z), ์ •๊ทœํ™” ์ „
13
+
14
+ ์”ฌ ์›์ : nuScenes ์ „์—ญ ์ขŒํ‘œ๋Š” ์ˆ˜์ฒœ m ๋‹จ์œ„๋ผ float32 ๋กœ ๋‹ด์œผ๋ฉด cm ์ดํ•˜๊ฐ€ ๋ญ‰๊ฐœ์ง„๋‹ค.
15
+ ๊ทธ๋ž˜์„œ ์œ„์น˜๋Š” ์”ฌ ์›์ ์„ ๋บ€ ๊ฐ’์œผ๋กœ ์ €์žฅํ•˜๊ณ , ์›์ ์€ ํ—ค๋”์˜ `comment nusim_origin`
16
+ ์— float64 ๋กœ ๋‚จ๊ธด๋‹ค. ํ‘œ์ค€ ๋ฆฌ๋”๋Š” comment ๋ฅผ ๋ฌด์‹œํ•˜๋ฏ€๋กœ ํ˜ธํ™˜์„ฑ์ด ๊นจ์ง€์ง€ ์•Š๋Š”๋‹ค.
17
+ ์ „์—ญ ์ขŒํ‘œ = ์ €์žฅ๋œ ์œ„์น˜ + origin.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+
25
+ import numpy as np
26
+
27
+ _ORIGIN_TAG = "nusim_origin"
28
+
29
+
30
+ @dataclass
31
+ class Gaussians:
32
+ """๊ฐ€์šฐ์‹œ์•ˆ ์ง‘ํ•ฉ. ์ „๋ถ€ ํ™œ์„ฑํ™” ์ „(raw) ํŒŒ๋ผ๋ฏธํ„ฐ๋‹ค โ€” ํ•™์Šต๊ธฐ ๋‚ด๋ถ€ ํ‘œํ˜„๊ณผ ๊ฐ™๋‹ค."""
33
+
34
+ means: np.ndarray # (N,3) ์”ฌ ์›์  ๊ธฐ์ค€
35
+ scales: np.ndarray # (N,3) log
36
+ quats: np.ndarray # (N,4) wxyz
37
+ opacities: np.ndarray # (N,) logit
38
+ sh0: np.ndarray # (N,1,3)
39
+ shN: np.ndarray # (N,K-1,3), K = (deg+1)^2. deg 0 ์ด๋ฉด (N,0,3)
40
+ origin: np.ndarray = field(default_factory=lambda: np.zeros(3)) # float64, ์ „์—ญ ์ขŒํ‘œ
41
+
42
+ def __len__(self) -> int:
43
+ return len(self.means)
44
+
45
+ @property
46
+ def sh_degree(self) -> int:
47
+ k = 1 + self.shN.shape[1]
48
+ deg = int(round(np.sqrt(k))) - 1
49
+ if (deg + 1) ** 2 != k:
50
+ raise ValueError(f"SH ๊ณ„์ˆ˜ ๊ฐœ์ˆ˜ {k} ๊ฐ€ (deg+1)^2 ๊ผด์ด ์•„๋‹™๋‹ˆ๋‹ค")
51
+ return deg
52
+
53
+
54
+ def _attr_names(n_rest: int) -> list[str]:
55
+ return (
56
+ ["x", "y", "z", "nx", "ny", "nz"]
57
+ + [f"f_dc_{i}" for i in range(3)]
58
+ + [f"f_rest_{i}" for i in range(n_rest)]
59
+ + ["opacity"]
60
+ + [f"scale_{i}" for i in range(3)]
61
+ + [f"rot_{i}" for i in range(4)]
62
+ )
63
+
64
+
65
+ def write_ply(path: str | Path, g: Gaussians) -> Path:
66
+ path = Path(path)
67
+ path.parent.mkdir(parents=True, exist_ok=True)
68
+ n = len(g)
69
+ # (N,K-1,3) โ†’ (N,3,K-1) โ†’ ํ‰ํƒ„ํ™”. ์›๋ณธ 3DGS ์˜ f_rest ์ˆœ์„œ๊ฐ€ ์ด๊ฒƒ์ด๋‹ค.
70
+ rest = np.ascontiguousarray(g.shN.transpose(0, 2, 1)).reshape(n, -1)
71
+ cols = [
72
+ g.means, np.zeros((n, 3)), g.sh0.reshape(n, 3), rest,
73
+ g.opacities.reshape(n, 1), g.scales, g.quats,
74
+ ]
75
+ data = np.concatenate([np.asarray(c, dtype=np.float32) for c in cols], axis=1)
76
+ names = _attr_names(rest.shape[1])
77
+ assert data.shape[1] == len(names)
78
+
79
+ o = np.asarray(g.origin, dtype=np.float64)
80
+ header = "\n".join(
81
+ ["ply", "format binary_little_endian 1.0",
82
+ f"comment {_ORIGIN_TAG} {o[0]!r} {o[1]!r} {o[2]!r}",
83
+ f"element vertex {n}"]
84
+ + [f"property float {a}" for a in names]
85
+ + ["end_header"]
86
+ ) + "\n"
87
+
88
+ tmp = path.with_suffix(path.suffix + ".tmp")
89
+ with open(tmp, "wb") as f:
90
+ f.write(header.encode("ascii"))
91
+ f.write(data.astype("<f4", copy=False).tobytes())
92
+ tmp.replace(path) # ๋ Œ๋”๋Ÿฌ๊ฐ€ ๋ฐ˜์ฏค ์“ฐ์ธ ํŒŒ์ผ์„ ์ฝ์ง€ ์•Š๊ฒŒ
93
+ return path
94
+
95
+
96
+ def read_origin(path: str | Path) -> np.ndarray:
97
+ """ํ—ค๋”๋งŒ ์ฝ์–ด ์”ฌ ์›์ ์„ ๋Œ๋ ค์ค€๋‹ค. ๊ฐ€์šฐ์‹œ์•ˆ 200๋งŒ ๊ฐœ์งœ๋ฆฌ ply ๋ฅผ ๋‹ค ์ฝ์ง€ ์•Š์œผ๋ ค๊ณ ."""
98
+ with open(path, "rb") as f:
99
+ for raw in f:
100
+ tok = raw.decode("ascii", "replace").split()
101
+ if tok[:2] == ["comment", _ORIGIN_TAG] and len(tok) == 5:
102
+ return np.array([float(t) for t in tok[2:5]])
103
+ if tok[:1] == ["end_header"]:
104
+ break
105
+ return np.zeros(3)
106
+
107
+
108
+ def read_count(path: str | Path) -> int:
109
+ """ํ—ค๋”๋งŒ ์ฝ์–ด ๊ฐ€์šฐ์‹œ์•ˆ ์ˆ˜ (element vertex)."""
110
+ with open(path, "rb") as f:
111
+ for raw in f:
112
+ tok = raw.decode("ascii", "replace").split()
113
+ if tok[:2] == ["element", "vertex"]:
114
+ return int(tok[2])
115
+ if tok[:1] == ["end_header"]:
116
+ break
117
+ raise ValueError(f"{path}: element vertex ๊ฐ€ ์—†๋‹ค")
118
+
119
+
120
+ def read_ply(path: str | Path) -> Gaussians:
121
+ path = Path(path)
122
+ with open(path, "rb") as f:
123
+ if f.readline().strip() != b"ply":
124
+ raise ValueError(f"{path}: ply ํŒŒ์ผ์ด ์•„๋‹™๋‹ˆ๋‹ค")
125
+ names: list[str] = []
126
+ n = None
127
+ origin = np.zeros(3)
128
+ while True:
129
+ line = f.readline()
130
+ if not line:
131
+ raise ValueError(f"{path}: end_header ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค")
132
+ tok = line.decode("ascii").split()
133
+ if not tok:
134
+ continue
135
+ if tok[0] == "format" and tok[1] != "binary_little_endian":
136
+ raise ValueError(f"{path}: {tok[1]} ๋Š” ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
137
+ elif tok[0] == "comment" and len(tok) == 5 and tok[1] == _ORIGIN_TAG:
138
+ origin = np.array([float(t) for t in tok[2:5]])
139
+ elif tok[0] == "element":
140
+ if tok[1] != "vertex":
141
+ raise ValueError(f"{path}: vertex ์™ธ element({tok[1]}) ๋Š” ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
142
+ n = int(tok[2])
143
+ elif tok[0] == "property":
144
+ if tok[1] != "float":
145
+ raise ValueError(f"{path}: float ๊ฐ€ ์•„๋‹Œ ์†์„ฑ {tok[1]} {tok[2]}")
146
+ names.append(tok[2])
147
+ elif tok[0] == "end_header":
148
+ break
149
+ if n is None:
150
+ raise ValueError(f"{path}: vertex ๊ฐœ์ˆ˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค")
151
+ data = np.frombuffer(f.read(n * len(names) * 4), dtype="<f4").reshape(n, len(names))
152
+
153
+ col = {a: i for i, a in enumerate(names)}
154
+
155
+ def pick(prefix: str, k: int) -> np.ndarray:
156
+ return data[:, [col[f"{prefix}{i}"] for i in range(k)]]
157
+
158
+ n_rest = sum(1 for a in names if a.startswith("f_rest_"))
159
+ if n_rest % 3:
160
+ raise ValueError(f"{path}: f_rest ๊ฐœ์ˆ˜ {n_rest} ๊ฐ€ 3์˜ ๋ฐฐ์ˆ˜๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค")
161
+ rest = pick("f_rest_", n_rest).reshape(n, 3, n_rest // 3).transpose(0, 2, 1)
162
+ return Gaussians(
163
+ means=data[:, [col["x"], col["y"], col["z"]]].copy(),
164
+ scales=pick("scale_", 3).copy(),
165
+ quats=pick("rot_", 4).copy(),
166
+ opacities=data[:, col["opacity"]].copy(),
167
+ sh0=pick("f_dc_", 3).reshape(n, 1, 3).copy(),
168
+ shN=np.ascontiguousarray(rest),
169
+ origin=origin,
170
+ )
src/nusim/nusim/jobs.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """์ž‘์—… ์ •์˜(JSON) ์‹คํ–‰๊ธฐ โ€” `nusim run <configs/jobs/์ด๋ฆ„.json>`.
2
+
3
+ ์‹คํ—˜๋งˆ๋‹ค ์…ธ ๋Œ€๊ธฐ์—ด ์Šคํฌ๋ฆฝํŠธ(cache/queue_*.sh)๋ฅผ ์ƒˆ๋กœ ์“ฐ๋˜ ๊ฒƒ์„ ๋Œ€์‹ ํ•œ๋‹ค. ์ž‘์—… ํŒŒ์ผ ํ˜•์‹:
4
+
5
+ {
6
+ "name": "quality_2026-10-05",
7
+ "vars": {"iters": "15000", "base": "--strategy mcmc --depth-lambda 0.1 --objects agents"},
8
+ "steps": [
9
+ {"name": "pilot-${s}", "foreach": {"s": ["scene-0184", "scene-0402"]},
10
+ "clean": ["outputs/gs_day/${s}"],
11
+ "run": "recon batch --scenes ${s} --no-sky --iters ${iters}"},
12
+ {"name": "exp-fullres", "run": "recon train scene-0003 ${base} --iters ${iters} --downscale 1 --out outputs/gs_exp/fullres"}
13
+ ]
14
+ }
15
+
16
+ - run: nusim ํ•˜์œ„ ๋ช…๋ น (๋ฌธ์ž์—ด์ด๋ฉด ๊ณต๋ฐฑ์œผ๋กœ ๋‚˜๋ˆˆ๋‹ค, ๋ชฉ๋ก๋„ ๋œ๋‹ค). ${๋ณ€์ˆ˜} ๋Š” varsยทforeach ๊ฐ’์œผ๋กœ ๋ฐ”๊พผ๋‹ค.
17
+ - foreach: ๋ณ€์ˆ˜๋งˆ๋‹ค ๊ฐ’ ๋ชฉ๋ก โ€” ๋‹จ๊ณ„๊ฐ€ ๊ฐ’ ์ˆ˜๋งŒํผ ํŽผ์ณ์ง„๋‹ค (์—ฌ๋Ÿฌ ๋ณ€์ˆ˜๋ฉด ๊ณฑ). ๊ฐ’์ด ๊ฐ์ฒด๋ฉด ${e.id}, ${e.args} ์ฒ˜๋Ÿผ.
18
+ - clean: ์‹คํ–‰ ์ „์— ์ง€์šธ ๋””๋ ‰ํ„ฐ๋ฆฌ (๋‹ค์‹œ ๋งŒ๋“ค ๊ฒฐ๊ณผ).
19
+ - continue_on_error: ์‹คํŒจํ•ด๋„ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ (๊ธฐ๋ณธ true โ€” ๋ฐค์ƒˆ ๋„๋Š” ๋Œ€๊ธฐ์—ด์ด ํ•œ ๋‹จ๊ณ„ ๋•Œ๋ฌธ์— ๋ฉˆ์ถ”์ง€ ์•Š๊ฒŒ).
20
+ ์ƒํƒœ๋Š” outputs/jobs/<name>/state.json โ€” ๊ฐ™์€ ์ด๋ฆ„ยท๊ฐ™์€ ๋ช…๋ น์œผ๋กœ ๋๋‚œ ๋‹จ๊ณ„๋Š” ๋‹ค์‹œ ์‹คํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค (์ด์–ด ํ•˜๊ธฐ).
21
+ ๋‹จ๊ณ„๋งˆ๋‹ค ๋กœ๊ทธ outputs/jobs/<name>/<๋‹จ๊ณ„>.log.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import itertools
27
+ import json
28
+ import logging
29
+ import re
30
+ import shlex
31
+ import shutil
32
+ import subprocess
33
+ import time
34
+ from pathlib import Path
35
+
36
+ LOG = logging.getLogger(__name__)
37
+ _VAR = re.compile(r"\$\{(\w+(?:\.\w+)*)\}")
38
+
39
+
40
+ def _subst(x, env: dict):
41
+ if isinstance(x, str):
42
+ def one(m):
43
+ v = env
44
+ for part in m.group(1).split("."): # ${e.args} โ€” foreach ๊ฐ’์ด ๊ฐ์ฒด์ผ ๋•Œ
45
+ if not isinstance(v, dict) or part not in v:
46
+ raise ValueError(f"์ •์˜๋˜์ง€ ์•Š์€ ๋ณ€์ˆ˜: ${{{m.group(1)}}}")
47
+ v = v[part]
48
+ return str(v)
49
+ return _VAR.sub(one, x)
50
+ if isinstance(x, list):
51
+ return [_subst(v, env) for v in x]
52
+ if isinstance(x, dict):
53
+ return {k: _subst(v, env) for k, v in x.items()}
54
+ return x
55
+
56
+
57
+ def expand(job: dict) -> list[dict]:
58
+ """foreach ๋ฅผ ํŽผ์น˜๊ณ  ๋ณ€์ˆ˜๋ฅผ ์ฑ„์šด ๋‹จ๊ณ„ ๋ชฉ๋ก."""
59
+ base_env = {k: _subst(v, {}) if isinstance(v, str) and "${" not in v else v for k, v in job.get("vars", {}).items()}
60
+ # vars ๋ผ๋ฆฌ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ ๋ฒˆ ๋”
61
+ base_env = {k: _subst(v, base_env) for k, v in base_env.items()}
62
+ out = []
63
+ for st in job["steps"]:
64
+ loops = st.get("foreach", {})
65
+ keys = list(loops)
66
+ for combo in itertools.product(*(loops[k] for k in keys)) if keys else [()]:
67
+ env = dict(base_env, **dict(zip(keys, combo)))
68
+ s = _subst({k: v for k, v in st.items() if k != "foreach"}, env)
69
+ run = s["run"]
70
+ s["run"] = shlex.split(run) if isinstance(run, str) else list(run)
71
+ s.setdefault("name", " ".join(s["run"][:3]))
72
+ out.append(s)
73
+ names = [s["name"] for s in out]
74
+ dup = {n for n in names if names.count(n) > 1}
75
+ if dup:
76
+ raise ValueError(f"๋‹จ๊ณ„ ์ด๋ฆ„์ด ๊ฒน์นฉ๋‹ˆ๋‹ค (์ƒํƒœ ๊ธฐ๋ก ํ‚ค): {sorted(dup)}")
77
+ return out
78
+
79
+
80
+ def run_job(path: Path, dry: bool = False, log=LOG.info) -> int:
81
+ from nusim.config import load
82
+ from nusim.paths import output_dir, workspace_root
83
+
84
+ job = load(path)
85
+ steps = expand(job)
86
+ wdir = output_dir() / "jobs" / job["name"]
87
+ wdir.mkdir(parents=True, exist_ok=True)
88
+ state_path = wdir / "state.json"
89
+ state = json.loads(state_path.read_text()) if state_path.exists() else {}
90
+ fails = 0
91
+ for k, st in enumerate(steps):
92
+ name, cmd = st["name"], st["run"]
93
+ if state.get(name, {}).get("state") == "done" and state[name].get("run") == cmd:
94
+ log(f"[{k + 1}/{len(steps)}] {name} โ€” ์ด๋ฏธ ๋๋‚จ, ๊ฑด๋„ˆ๋œ€")
95
+ continue
96
+ log(f"[{k + 1}/{len(steps)}] {name}: nusim {' '.join(cmd)}")
97
+ if dry:
98
+ continue
99
+ for c in st.get("clean", []):
100
+ p = Path(c) if Path(c).is_absolute() else workspace_root() / c
101
+ if p.exists():
102
+ shutil.rmtree(p)
103
+ t0 = time.time()
104
+ safe = re.sub(r"[^\w.-]+", "_", name)
105
+ with open(wdir / f"{safe}.log", "w") as f:
106
+ r = subprocess.run(["nusim", *cmd], stdout=f, stderr=subprocess.STDOUT, cwd=workspace_root())
107
+ rec = {"run": cmd, "state": "done" if r.returncode == 0 else "failed", "returncode": r.returncode,
108
+ "seconds": round(time.time() - t0)}
109
+ state[name] = rec
110
+ state_path.write_text(json.dumps(state, indent=2, ensure_ascii=False))
111
+ log(f" {rec['state']} ({rec['seconds'] / 60:.0f}๋ถ„)")
112
+ if r.returncode != 0:
113
+ fails += 1
114
+ if not st.get("continue_on_error", job.get("continue_on_error", True)):
115
+ break
116
+ return 1 if fails else 0
src/nusim/nusim/log.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ํ”„๋กœ์ ํŠธ ๊ณต์šฉ ๋กœ๊ทธ.
2
+
3
+ ๋ชจ๋“  ๋ชจ๋“ˆ์€ `logging.getLogger(__name__)` (= "nusim.recon.train" ๊ฐ™์€ ์ด๋ฆ„)๋กœ ๋‚จ๊ธด๋‹ค.
4
+ `setup()` ์ด ํ•œ ๋ฒˆ ๋ถˆ๋ฆฌ๋ฉด:
5
+ ์ฝ˜์†” HH:MM:SS ๋ฉ”์‹œ์ง€ (์‚ฌ๋žŒ์ด ๋ณด๋Š” ์ง„ํ–‰ ์ƒํ™ฉ)
6
+ ํŒŒ์ผ ๋‚ ์งœ ์‹œ๊ฐ.ms ๋ ˆ๋ฒจ ๋ชจ๋“ˆ | ๋ฉ”์‹œ์ง€ (outputs/logs/<๋ช…๋ น>_<์‹œ๊ฐ>.log โ€” ๋‚˜์ค‘์— ์ถ”์ ์šฉ)
7
+ `run_log(path)` ๋Š” ํ•™์Šต ๊ฐ™์€ ๊ธด ์ž‘์—…์˜ ๊ฒฐ๊ณผ ๋””๋ ‰ํ„ฐ๋ฆฌ์— ๊ฐ™์€ ๋กœ๊ทธ๋ฅผ ํ•œ ๋ฒŒ ๋” ๋‚จ๊ธด๋‹ค.
8
+
9
+ ํ•จ์ˆ˜๋“ค์ด ๋ฐ›๋Š” `log=` ์ธ์ž(์ง„ํ–‰ ๋ฉ”์‹œ์ง€ ์ฝœ๋ฐฑ)์˜ ๊ธฐ๋ณธ๊ฐ’์€ ๋ชจ๋“ˆ ๋กœ๊ฑฐ์˜ info ๋‹ค โ€” print ๊ฐ€ ์•„๋‹ˆ๋‹ค.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import contextlib
15
+ import logging
16
+ import sys
17
+ import time
18
+ from pathlib import Path
19
+
20
+ ROOT = "nusim"
21
+ _FILE_FMT = "%(asctime)s.%(msecs)03d %(levelname)-5s %(name)s | %(message)s"
22
+ _CONSOLE_FMT = "%(asctime)s %(message)s"
23
+
24
+
25
+ def get(name: str) -> logging.Logger:
26
+ return logging.getLogger(name if name.startswith(ROOT) else f"{ROOT}.{name}")
27
+
28
+
29
+ def setup(log_dir: Path | None = None, tag: str = "nusim", level: int = logging.INFO,
30
+ debug_file: bool = True) -> Path | None:
31
+ """์ฝ˜์†” + ํŒŒ์ผ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๊ฑด๋‹ค. ๋‘ ๋ฒˆ ๋ถˆ๋ ค๋„ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์ค‘๋ณต๋˜์ง€ ์•Š๋Š”๋‹ค. ๋กœ๊ทธ ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ๋Œ๋ ค์ค€๋‹ค."""
32
+ root = logging.getLogger(ROOT)
33
+ root.setLevel(logging.DEBUG if debug_file else level)
34
+ root.propagate = False
35
+ for h in list(root.handlers):
36
+ root.removeHandler(h)
37
+ con = logging.StreamHandler(sys.stdout)
38
+ con.setLevel(level)
39
+ con.setFormatter(logging.Formatter(_CONSOLE_FMT, "%H:%M:%S"))
40
+ root.addHandler(con)
41
+ path = None
42
+ if log_dir is not None:
43
+ log_dir.mkdir(parents=True, exist_ok=True)
44
+ path = log_dir / f"{tag}_{time.strftime('%Y%m%d-%H%M%S')}.log"
45
+ fh = logging.FileHandler(path, encoding="utf-8")
46
+ fh.setLevel(logging.DEBUG if debug_file else level)
47
+ fh.setFormatter(logging.Formatter(_FILE_FMT, "%Y-%m-%d %H:%M:%S"))
48
+ root.addHandler(fh)
49
+ return path
50
+
51
+
52
+ @contextlib.contextmanager
53
+ def run_log(path: Path):
54
+ """with run_log(out_dir / "run.log"): ... โ€” ๊ทธ ์•ˆ์˜ nusim ๋กœ๊ทธ๋ฅผ ์ด ํŒŒ์ผ์—๋„ (DEBUG ํฌํ•จ) ๋‚จ๊ธด๋‹ค."""
55
+ path.parent.mkdir(parents=True, exist_ok=True)
56
+ fh = logging.FileHandler(path, encoding="utf-8")
57
+ fh.setLevel(logging.DEBUG)
58
+ fh.setFormatter(logging.Formatter(_FILE_FMT, "%Y-%m-%d %H:%M:%S"))
59
+ root = logging.getLogger(ROOT)
60
+ if not root.handlers: # setup() ์—†์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ์“ฐ์ธ ๊ฒฝ์šฐ์—๋„ ์ฝ˜์†”์—๋Š” ๋‚˜์˜ค๊ฒŒ
61
+ setup()
62
+ root.addHandler(fh)
63
+ try:
64
+ yield path
65
+ finally:
66
+ root.removeHandler(fh)
67
+ fh.close()
src/nusim/nusim/paths.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """๋ฐ์ดํ„ฐ์…‹/์ž‘์—…๊ณต๊ฐ„ ๊ฒฝ๋กœ ํ•ด์„.
2
+
3
+ ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ๋Š” nuScenes ๊ฐ€ /data/nuscenes ์— ์ฝ๊ธฐ์ „์šฉ์œผ๋กœ ๋งˆ์šดํŠธ๋˜๊ณ ,
4
+ ์ปจํ…Œ์ด๋„ˆ ๋ฐ–(ํ˜ธ์ŠคํŠธ)์—์„œ๋Š” NAS ์›๋ณธ ๊ฒฝ๋กœ๋ฅผ ๊ทธ๋Œ€๋กœ ์“ด๋‹ค.
5
+ ์ฝ”๋“œ๊ฐ€ ๋‘ ๊ฒฝ์šฐ๋ฅผ ๋ชจ๋‘ ๊ฒฌ๋””๋„๋ก ์—ฌ๊ธฐ ํ•œ ๊ณณ์—์„œ๋งŒ ๊ฒฐ์ •ํ•œ๋‹ค.
6
+
7
+ ์šฐ์„ ์ˆœ์œ„: ๋ช…์‹œ ์ธ์ž > ํ™˜๊ฒฝ๋ณ€์ˆ˜ NUSCENES_ROOT > config/paths.yaml > ์•Œ๋ ค์ง„ ํ›„๋ณด
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+ # ์ปจํ…Œ์ด๋„ˆ ๋งˆ์šดํŠธ ๊ฒฝ๋กœ๊ฐ€ ๋จผ์ €, ๊ทธ ๋‹ค์Œ์ด ํ˜ธ์ŠคํŠธ NAS ๊ฒฝ๋กœ.
17
+ _ROOT_CANDIDATES = (
18
+ "/data/nuscenes",
19
+ "/home/mm/nas2/nuscenes_original_junhan/nuscenes/original/full",
20
+ )
21
+
22
+ # ๋ฃจํŠธ๊ฐ€ ๋งž๋Š”์ง€ ํŒ์ •ํ•˜๋Š” ๊ธฐ์ค€. ์ด ์„ธ ๊ฐ€์ง€๊ฐ€ ์ „๋ถ€ ์žˆ์–ด์•ผ ์šฐ๋ฆฌ๊ฐ€ ์“ฐ๋Š” ํŒŒ์ดํ”„๋ผ์ธ์ด ๋ˆ๋‹ค.
23
+ _ROOT_MARKERS = ("v1.0-trainval", "samples", "maps")
24
+
25
+
26
+ def workspace_root() -> Path:
27
+ """ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ. ์ปจํ…Œ์ด๋„ˆ์—์„œ๋Š” /workspace, ๋ฐ–์—์„œ๋Š” ์†Œ์Šค ํŠธ๋ฆฌ๋ฅผ ๊ฑฐ์Šฌ๋Ÿฌ ์ฐพ๋Š”๋‹ค."""
28
+ if env := os.environ.get("VKGS_WORKSPACE"):
29
+ return Path(env)
30
+ # .../src/nusim/nusim/paths.py โ†’ 3๋‹จ๊ณ„ ์œ„๊ฐ€ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ
31
+ return Path(__file__).resolve().parents[3]
32
+
33
+
34
+ def _looks_like_nuscenes(p: Path) -> bool:
35
+ return all((p / m).exists() for m in _ROOT_MARKERS)
36
+
37
+
38
+ def nuscenes_root(explicit: str | os.PathLike | None = None) -> Path:
39
+ """nuScenes ๋ฐ์ดํ„ฐ์…‹ ๋ฃจํŠธ๋ฅผ ๋Œ๋ ค์ค€๋‹ค. ๋ชป ์ฐพ์œผ๋ฉด ๋ฌด์—‡์„ ์ฐพ์•˜๋Š”์ง€ ์•Œ๋ ค์ฃผ๋ฉฐ ์‹คํŒจํ•œ๋‹ค."""
40
+ tried: list[str] = []
41
+
42
+ for cand in (explicit, os.environ.get("NUSCENES_ROOT")):
43
+ if not cand:
44
+ continue
45
+ p = Path(cand)
46
+ if _looks_like_nuscenes(p):
47
+ return p
48
+ tried.append(f"{p} (์ง€์ •๋์œผ๋‚˜ {_ROOT_MARKERS} ์ค‘ ์ผ๋ถ€๊ฐ€ ์—†์Œ)")
49
+
50
+ cfg = workspace_root() / "config" / "paths.yaml"
51
+ if cfg.exists():
52
+ try:
53
+ import yaml
54
+
55
+ data = yaml.safe_load(cfg.read_text()) or {}
56
+ if cand := data.get("nuscenes_root"):
57
+ p = Path(cand)
58
+ if _looks_like_nuscenes(p):
59
+ return p
60
+ tried.append(f"{p} (config/paths.yaml)")
61
+ except Exception as e: # yaml ์ด ์—†๊ฑฐ๋‚˜ ๊นจ์ง„ ๊ฒฝ์šฐ โ€” ์น˜๋ช…์ ์ด์ง€ ์•Š๋‹ค
62
+ tried.append(f"{cfg} (์ฝ๊ธฐ ์‹คํŒจ: {e})")
63
+
64
+ for cand in _ROOT_CANDIDATES:
65
+ p = Path(cand)
66
+ if _looks_like_nuscenes(p):
67
+ return p
68
+ tried.append(str(p))
69
+
70
+ raise FileNotFoundError(
71
+ "nuScenes ๋ฃจํŠธ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์‹œ๋„ํ•œ ๊ฒฝ๋กœ:\n "
72
+ + "\n ".join(tried)
73
+ + "\n\n๋ฃจํŠธ์—๋Š” ๋‹ค์Œ์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: "
74
+ + ", ".join(_ROOT_MARKERS)
75
+ + "\nํ™˜๊ฒฝ๋ณ€์ˆ˜ NUSCENES_ROOT ๋กœ ์ง€์ •ํ•˜๊ฑฐ๋‚˜ config/paths.yaml ์„ ๊ณ ์น˜์„ธ์š”."
76
+ )
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class DatasetPaths:
81
+ """nuScenes ๋ฃจํŠธ ์•„๋ž˜์˜ ํ•˜์œ„ ๊ฒฝ๋กœ ๋ชจ์Œ. ๊ฒฝ๋กœ ๋ฌธ์ž์—ด์„ ์ฝ”๋“œ ๊ณณ๊ณณ์— ํฉ์ง€ ์•Š๊ธฐ ์œ„ํ•จ."""
82
+
83
+ root: Path
84
+ version: str = "v1.0-trainval"
85
+ # ์นด๋ฉ”๋ผ ์ด๋ฏธ์ง€๋งŒ ๋‹ค๋ฅธ ๊ณณ์—์„œ ์ฝ์„ ๋•Œ (๊ฐ™์€ ์ƒ๋Œ€ ๊ฒฝ๋กœ) โ€” ์˜ˆ: ๋ฐคยท๋น„ โ†’ ๋‚ฎ์œผ๋กœ ๋ฐ”๊พผ ์˜์ƒ (nusim.aug.translate).
86
+ # ๋ผ์ด๋‹คยท๋ฉ”ํƒ€ยท์ง€๋„๋Š” ๊ทธ๋Œ€๋กœ root ์—์„œ.
87
+ image_root: Path | None = None
88
+
89
+ @property
90
+ def images(self) -> Path:
91
+ return self.image_root or self.root
92
+
93
+ @property
94
+ def meta(self) -> Path:
95
+ """scene.json / sample_data.json ๋“ฑ ํ…Œ์ด๋ธ”์ด ์žˆ๋Š” ๊ณณ."""
96
+ return self.root / self.version
97
+
98
+ @property
99
+ def samples(self) -> Path:
100
+ """ํ‚คํ”„๋ ˆ์ž„ ์„ผ์„œ ๋ฐ์ดํ„ฐ (2 Hz)."""
101
+ return self.root / "samples"
102
+
103
+ @property
104
+ def sweeps(self) -> Path:
105
+ """๋น„ํ‚คํ”„๋ ˆ์ž„ ์„ผ์„œ ๋ฐ์ดํ„ฐ (์นด๋ฉ”๋ผ 12 Hz). 3DGS ์žฌ๊ตฌ์„ฑ์— ํ•ต์‹ฌ์ด๋‹ค โ€”
106
+ ํ‚คํ”„๋ ˆ์ž„๋งŒ ์“ฐ๋ฉด ๋ทฐ๊ฐ€ ๋„ˆ๋ฌด ์„ฑ๊ฒจ์„œ ๊ฐ€์šฐ์‹œ์•ˆ์ด ์ˆ˜๋ ดํ•˜์ง€ ์•Š๋Š”๋‹ค."""
107
+ return self.root / "sweeps"
108
+
109
+ @property
110
+ def maps(self) -> Path:
111
+ return self.root / "maps"
112
+
113
+ @property
114
+ def map_expansion(self) -> Path:
115
+ """singapore-onenorth.json ๋“ฑ ๋ฒกํ„ฐ ๋งต. ์ฐจ์„  ๊ทธ๋ž˜ํ”„์˜ ์ถœ์ฒ˜๋‹ค."""
116
+ return self.root / "maps" / "expansion"
117
+
118
+ @property
119
+ def can_bus(self) -> Path:
120
+ """scene-XXXX_pose.json ๋“ฑ. ego_pose.json ๋ณด๋‹ค ํ›จ์”ฌ ์กฐ๋ฐ€ํ•œ ์ž์ฐจ ์ƒํƒœ๋กœ,
121
+ closed-loop ์ฐจ๋Ÿ‰ ๋ชจ๋ธ ํŠœ๋‹์— ์“ด๋‹ค."""
122
+ return self.root / "can_bus"
123
+
124
+ def check(self) -> dict[str, tuple[bool, Path]]:
125
+ """๊ฐ ํ•˜์œ„ ๊ฒฝ๋กœ์˜ ์กด์žฌ ์—ฌ๋ถ€. ์ง„๋‹จ(vkgs-doctor)์—์„œ ์“ด๋‹ค."""
126
+ return {
127
+ name: (path.exists(), path)
128
+ for name, path in (
129
+ ("meta", self.meta),
130
+ ("samples", self.samples),
131
+ ("sweeps", self.sweeps),
132
+ ("maps", self.maps),
133
+ ("map_expansion", self.map_expansion),
134
+ ("can_bus", self.can_bus),
135
+ )
136
+ }
137
+
138
+
139
+ def dataset(explicit: str | os.PathLike | None = None, version: str = "v1.0-trainval") -> DatasetPaths:
140
+ return DatasetPaths(root=nuscenes_root(explicit), version=version)
141
+
142
+
143
+ def cache_dir() -> Path:
144
+ """ํŒŒ์ƒ ์‚ฐ์ถœ๋ฌผ(์”ฌ ์ธ๋ฑ์Šค ๋“ฑ). NAS ๊ฐ€ ์•„๋‹ˆ๋ผ ๋กœ์ปฌ ๋””์Šคํฌ์—ฌ์•ผ ํ•œ๋‹ค โ€”
145
+ NFS ์™•๋ณต์ด ์ธ๋ฑ์Šค ์กฐํšŒ๋ณด๋‹ค ๋น„์‹ธ๋ฉด ์บ์‹œ์˜ ์˜๋ฏธ๊ฐ€ ์—†๋‹ค."""
146
+ d = Path(os.environ.get("VKGS_CACHE", workspace_root() / "cache"))
147
+ d.mkdir(parents=True, exist_ok=True)
148
+ return d
149
+
150
+
151
+ def output_dir() -> Path:
152
+ d = Path(os.environ.get("VKGS_OUTPUT", workspace_root() / "outputs"))
153
+ d.mkdir(parents=True, exist_ok=True)
154
+ return d
src/nusim/nusim/render/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """Vulkan ๋ Œ๋”๋Ÿฌ(src/vkgs/vkgs_viewer) ์™€์˜ ์ ‘์ .
2
+
3
+ export.py ์นด๋ฉ”๋ผ ๋ฆฌ๊ทธยท๋งต ์„ ยทํ‰๊ฐ€ ๋ทฐ๋ฅผ ๋ทฐ์–ด๊ฐ€ ์ฝ๋Š” ํ…์ŠคํŠธ ํŒŒ์ผ๋กœ
4
+ compare.py ๊ฐ™์€ ์นด๋ฉ”๋ผ์—์„œ ํ•™์Šต๊ธฐ(gsplat) ๋ Œ๋” vs Vulkan ๋ Œ๋” ํ”ฝ์…€ ๋น„๊ต (roadmap 2๋‹จ๊ณ„ ๊ธฐ์ค€)
5
+
6
+ nusim render export scene-0003 # rig.txt / map.txt / test_views.txt
7
+ nusim render compare scene-0003 # ์˜คํ”„์Šคํฌ๋ฆฐ ๋ Œ๋” ํ›„ PSNR / ํ‰๊ท  ์˜ค์ฐจ
8
+ nusim render view scene-0003 # ์ฐฝ ๋ชจ๋“œ ๋ทฐ์–ด ์‹คํ–‰
9
+ """
src/nusim/pyproject.toml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nusim"
7
+ version = "0.1.0"
8
+ description = "nuScenes ์‹ฑ๊ฐ€ํฌ๋ฅด ๋งต ๊ธฐ๋ฐ˜ Vulkan 3DGS closed-loop ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ"
9
+ requires-python = ">=3.10"
10
+
11
+ # ์‹ค์ œ ์„ค์น˜๋Š” docker/requirements.txt ๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค (Dockerfile ์ด --no-deps ๋กœ ๊ฑด๋‹ค).
12
+ # ์—ฌ๊ธฐ ๋ชฉ๋ก์€ ์ปจํ…Œ์ด๋„ˆ ๋ฐ–์—์„œ ์“ธ ๋•Œ๋ฅผ ์œ„ํ•œ ๋ฌธ์„œ ๊ฒธ ์‚ฌ์–‘์ด๋‹ค.
13
+ dependencies = [
14
+ "numpy<2.0", "scipy", "Pillow", "opencv-python-headless",
15
+ "pyquaternion", "shapely<2.0", "cachetools", "matplotlib",
16
+ "plyfile", "pyyaml", "typer", "rich", "tqdm",
17
+ ]
18
+
19
+ [project.scripts]
20
+ vkgs-doctor = "nusim.cli.doctor:main"
21
+ nusim = "nusim.cli.main:main"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["."]
25
+ include = ["nusim*"]
src/vkgs/CMakeLists.txt ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cmake_minimum_required(VERSION 3.22)
2
+ project(vkgs LANGUAGES C CXX)
3
+
4
+ # 3DGS compute ๋ž˜์Šคํ„ฐ๋ผ์ด์ €๋Š” C++20 ์˜ designated initializer ์™€ <bit> ๋ฅผ ์“ด๋‹ค.
5
+ set(CMAKE_CXX_STANDARD 20)
6
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
7
+ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # clangd ์šฉ
8
+
9
+ if(NOT CMAKE_BUILD_TYPE)
10
+ set(CMAKE_BUILD_TYPE RelWithDebInfo)
11
+ endif()
12
+
13
+ find_package(Vulkan REQUIRED)
14
+ message(STATUS "Vulkan: ${Vulkan_LIBRARY} (ํ—ค๋” ${Vulkan_INCLUDE_DIR})")
15
+
16
+ # ---------------------------------------------------------------- ์…ฐ์ด๋” ์ปดํŒŒ์ผ
17
+ # glslc(shaderc) ๋ฅผ ์„ ํ˜ธํ•˜๋˜ ์—†์œผ๋ฉด glslangValidator ๋กœ ํด๋ฐฑํ•œ๋‹ค.
18
+ # jammy ์—์„œ glslc ๋Š” ๋ณ„๋„ ํŒจํ‚ค์ง€๋ผ ํ•ญ์ƒ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•  ์ˆ˜ ์—†๋‹ค.
19
+ # FindVulkan ์ด glslc ๋ฅผ ์ฐพ์•„ ์ฃผ๋ฉด ๊ทธ๊ฑธ ์“ฐ๊ณ , ์•„๋‹ˆ๋ฉด PATH ์—์„œ ์ฐพ๋Š”๋‹ค.
20
+ if(Vulkan_GLSLC_EXECUTABLE)
21
+ set(GLSLC ${Vulkan_GLSLC_EXECUTABLE})
22
+ else()
23
+ find_program(GLSLC glslc)
24
+ endif()
25
+ find_program(GLSLANG glslangValidator)
26
+
27
+ if(GLSLC)
28
+ # -I: gs_common.glsl ๊ฐ™์€ ๊ณตํ†ต ํ—ค๋”๋ฅผ #include ํ•˜๊ธฐ ์œ„ํ•จ
29
+ set(SHADER_CMD ${GLSLC} --target-env=vulkan1.2 -O -I ${CMAKE_CURRENT_SOURCE_DIR}/shaders)
30
+ message(STATUS "์…ฐ์ด๋” ์ปดํŒŒ์ผ๋Ÿฌ: ${GLSLC}")
31
+ elseif(GLSLANG)
32
+ # -V ๋ฅผ ๋ฐ˜๋“œ์‹œ ์ค€๋‹ค. --target-env ๋งŒ์œผ๋กœ SPIR-V ๊ฐ€ ๋‚˜์˜จ๋‹ค๊ณ  ๊ธฐ๋Œ€ํ•˜๋ฉด ์•ˆ ๋œ๋‹ค โ€”
33
+ # ๋ฒ„์ „์— ๋”ฐ๋ผ ๊ฒ€์ฆ๋งŒ ํ•˜๊ณ  -o ๋ฅผ ๋ฌด์‹œํ•ด ๋นˆ ํŒŒ์ผ์ด ๋‚จ๋Š”๋‹ค.
34
+ set(SHADER_CMD ${GLSLANG} -V --target-env vulkan1.2 -I${CMAKE_CURRENT_SOURCE_DIR}/shaders)
35
+ message(STATUS "์…ฐ์ด๋” ์ปดํŒŒ์ผ๋Ÿฌ: ${GLSLANG} (glslc ์—†์Œ)")
36
+ else()
37
+ message(FATAL_ERROR "glslc ๋„ glslangValidator ๋„ ์—†์Šต๋‹ˆ๋‹ค. glslang-tools ๋ฅผ ์„ค์น˜ํ•˜์„ธ์š”.")
38
+ endif()
39
+ set(SHADER_OUT_FLAG -o)
40
+
41
+ set(SPIRV_DIR ${CMAKE_BINARY_DIR}/shaders)
42
+ file(MAKE_DIRECTORY ${SPIRV_DIR})
43
+
44
+ # GLSL ํ•˜๋‚˜๋ฅผ SPIR-V ๋กœ ๊ตฝ๋Š”๋‹ค. ๊ฒฐ๊ณผ๋Š” ๋นŒ๋“œ ๋””๋ ‰ํ„ฐ๋ฆฌ์˜ shaders/ ์•„๋ž˜์— ๋†“์ธ๋‹ค.
45
+ function(vkgs_add_shader SRC)
46
+ get_filename_component(NAME ${SRC} NAME)
47
+ set(OUT ${SPIRV_DIR}/${NAME}.spv)
48
+ add_custom_command(
49
+ OUTPUT ${OUT}
50
+ COMMAND ${SHADER_CMD} ${CMAKE_CURRENT_SOURCE_DIR}/${SRC} ${SHADER_OUT_FLAG} ${OUT}
51
+ # ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ์กฐ์šฉํžˆ ๋นˆ ํŒŒ์ผ์„ ๋‚จ๊ธฐ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์–ด ์—ฌ๊ธฐ์„œ ๊ฑธ๋Ÿฌ๋‚ธ๋‹ค.
52
+ COMMAND ${CMAKE_COMMAND} -E cat ${OUT} > /dev/null
53
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${SRC} ${CMAKE_CURRENT_SOURCE_DIR}/shaders/gs_common.glsl ${CMAKE_CURRENT_SOURCE_DIR}/shaders/ibl_common.glsl
54
+ ${CMAKE_CURRENT_SOURCE_DIR}/shaders/depth_common.glsl ${CMAKE_CURRENT_SOURCE_DIR}/shaders/bindless.glsl
55
+ ${CMAKE_CURRENT_SOURCE_DIR}/shaders/gs_radix_common.glsl ${CMAKE_CURRENT_SOURCE_DIR}/shaders/weather_common.glsl
56
+ COMMENT "SPIR-V: ${NAME}"
57
+ VERBATIM)
58
+ list(APPEND VKGS_SPIRV ${OUT})
59
+ set(VKGS_SPIRV ${VKGS_SPIRV} PARENT_SCOPE)
60
+ endfunction()
61
+
62
+ vkgs_add_shader(shaders/smoke.comp)
63
+ vkgs_add_shader(shaders/gs_preprocess.comp)
64
+ vkgs_add_shader(shaders/gs_sort.comp)
65
+ vkgs_add_shader(shaders/gs_radix_hist.comp)
66
+ vkgs_add_shader(shaders/gs_radix_scan.comp)
67
+ vkgs_add_shader(shaders/gs_radix_scatter.comp)
68
+ vkgs_add_shader(shaders/gs_splat.vert)
69
+ vkgs_add_shader(shaders/gs_splat.frag)
70
+ vkgs_add_shader(shaders/line.vert)
71
+ vkgs_add_shader(shaders/line.frag)
72
+ vkgs_add_shader(shaders/env.vert)
73
+ vkgs_add_shader(shaders/env.frag)
74
+ vkgs_add_shader(shaders/pbr.vert)
75
+ vkgs_add_shader(shaders/pbr.frag)
76
+ vkgs_add_shader(shaders/weather.comp)
77
+ vkgs_add_shader(shaders/fxaa.comp)
78
+ vkgs_add_shader(shaders/camera_mode.comp)
79
+ vkgs_add_shader(shaders/precip.vert)
80
+ vkgs_add_shader(shaders/precip.frag)
81
+ vkgs_add_shader(shaders/sky_sim.frag)
82
+ vkgs_add_shader(shaders/pbr_gltf.vert)
83
+ vkgs_add_shader(shaders/pbr_gltf.frag)
84
+ vkgs_add_shader(shaders/shadow.vert)
85
+ vkgs_add_shader(shaders/shadow.frag)
86
+
87
+ # ์…ฐ์ด๋” ๊ทธ๋ž˜ํ”„ (configs/shader_graphs/*.json) โ†’ scripts/shader_graph.py ๊ฐ€ GLSL ๋กœ ํŽผ์นจ โ†’ SPIR-V.
88
+ # ๊ทธ๋ž˜ํ”„ JSONยท๋…ธ๋“œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(shaders/nodes/*.glsl)ยท์ƒ์„ฑ๊ธฐ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ๋‹ค์‹œ ๋งŒ๋“ ๋‹ค. ๋งค๊ฐœ๋ณ€์ˆ˜ ํ‘œ(.params.json)๋Š”
89
+ # SPIR-V ์˜†์— ๋†“์—ฌ ๋ทฐ์–ด๊ฐ€ ์Šฌ๋ผ์ด๋”ยท์ดˆ๊นƒ๊ฐ’์— ์“ด๋‹ค.
90
+ find_program(VKGS_PYTHON NAMES python3 python REQUIRED)
91
+ set(GRAPH_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../configs/shader_graphs)
92
+ set(GRAPH_GEN ${CMAKE_CURRENT_SOURCE_DIR}/../../scripts/shader_graph.py)
93
+ file(GLOB VKGS_SHADER_GRAPHS CONFIGURE_DEPENDS ${GRAPH_DIR}/*.json)
94
+ file(GLOB VKGS_NODE_LIBS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/shaders/nodes/*.glsl)
95
+ foreach(G ${VKGS_SHADER_GRAPHS})
96
+ get_filename_component(GNAME ${G} NAME_WE)
97
+ set(GSRC ${SPIRV_DIR}/gen/${GNAME}.comp)
98
+ set(GOUT ${SPIRV_DIR}/${GNAME}.comp.spv)
99
+ add_custom_command(
100
+ OUTPUT ${GSRC} ${SPIRV_DIR}/${GNAME}.params.json
101
+ COMMAND ${VKGS_PYTHON} ${GRAPH_GEN} ${G} --shaders ${CMAKE_CURRENT_SOURCE_DIR}/shaders --out ${SPIRV_DIR}/gen
102
+ COMMAND ${CMAKE_COMMAND} -E copy ${SPIRV_DIR}/gen/${GNAME}.params.json ${SPIRV_DIR}/${GNAME}.params.json
103
+ DEPENDS ${G} ${GRAPH_GEN} ${VKGS_NODE_LIBS}
104
+ COMMENT "์…ฐ์ด๋” ๊ทธ๋ž˜ํ”„: ${GNAME}"
105
+ VERBATIM)
106
+ add_custom_command(
107
+ OUTPUT ${GOUT}
108
+ COMMAND ${SHADER_CMD} ${GSRC} ${SHADER_OUT_FLAG} ${GOUT}
109
+ DEPENDS ${GSRC} ${CMAKE_CURRENT_SOURCE_DIR}/shaders/bindless.glsl
110
+ COMMENT "SPIR-V: ${GNAME}.comp (์…ฐ์ด๋” ๊ทธ๋ž˜ํ”„)"
111
+ VERBATIM)
112
+ list(APPEND VKGS_SPIRV ${GOUT} ${SPIRV_DIR}/${GNAME}.params.json)
113
+ endforeach()
114
+ add_custom_target(vkgs_shaders ALL DEPENDS ${VKGS_SPIRV})
115
+
116
+ # ---------------------------------------------------------------- ์‹คํ–‰ํŒŒ์ผ
117
+ # vkgs_probe: ์ปจํ…Œ์ด๋„ˆ์˜ Vulkan ๊ฒฝ๋กœ๊ฐ€ ์ง„์งœ๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ๋๊นŒ์ง€ ํ™•์ธํ•œ๋‹ค.
118
+ # ์žฅ์น˜ ์—ด๊ฑฐ โ†’ ํ ์ƒ์„ฑ โ†’ SPIR-V ๋กœ๋“œ โ†’ ์ปดํ“จํŠธ ๋””์ŠคํŒจ์น˜ โ†’ ๊ฒฐ๊ณผ ์ฝ๊ธฐ
119
+ # ์ด ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์กฐ์šฉํžˆ ์‹คํŒจํ•˜๋ฉด ๋ Œ๋”๋Ÿฌ๋Š” ๊ฒ€์€ ํ™”๋ฉด๋งŒ ๋‚ธ๋‹ค.
120
+ add_executable(vkgs_probe src/probe.cpp)
121
+ target_link_libraries(vkgs_probe PRIVATE Vulkan::Vulkan)
122
+ target_compile_definitions(vkgs_probe PRIVATE VKGS_SPIRV_DIR="${SPIRV_DIR}")
123
+ add_dependencies(vkgs_probe vkgs_shaders)
124
+
125
+ # vkgs_voxcarve: ๋ ˆ์ด์บ์ŠคํŠธ ์„ผ์„œ์šฉ ๋ณต์…€ ์›”๋“œ์˜ ๋นˆ ๊ณต๊ฐ„ ๊นŽ๊ธฐ (nusim/sim/voxels.py ๊ฐ€ ๋ถ€๋ฅธ๋‹ค)
126
+ add_executable(vkgs_voxcarve src/voxcarve.cpp)
127
+ find_package(Threads REQUIRED)
128
+ target_link_libraries(vkgs_voxcarve PRIVATE Threads::Threads)
129
+
130
+ # vkgs_viewer: 3DGS .ply ๋ฅผ ๊ทธ๋ฆฐ๋‹ค (์˜คํ”„์Šคํฌ๋ฆฐ PNG / GLFW ์ฐฝ). roadmap 2a.
131
+ find_package(glfw3 3.3 REQUIRED)
132
+ # Dear ImGui (MIT) โ€” ๋ทฐ์–ด ์•ˆ ์Šฌ๋ผ์ด๋” UI (ํ•ฉ์„ฑ ๋‚ ์”จยท์กฐ๋„). third_party/imgui (v1.91.9)
133
+ set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/imgui)
134
+ # SPIRV-Reflect (Khronos, Apache-2.0) โ€” ์…ฐ์ด๋” ๋ฆฌํ”Œ๋ ‰์…˜ (src/shader_reflect.*)
135
+ set(SPVR_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/SPIRV-Reflect)
136
+ add_executable(vkgs_viewer src/viewer.cpp src/ply.cpp src/gltf.cpp src/shader_reflect.cpp src/render_graph.cpp ${SPVR_DIR}/spirv_reflect.c
137
+ ${IMGUI_DIR}/imgui.cpp ${IMGUI_DIR}/imgui_draw.cpp ${IMGUI_DIR}/imgui_tables.cpp
138
+ ${IMGUI_DIR}/imgui_widgets.cpp ${IMGUI_DIR}/backends/imgui_impl_glfw.cpp
139
+ ${IMGUI_DIR}/backends/imgui_impl_vulkan.cpp)
140
+ # GLFW ํ—ค๋”๊ฐ€ OpenGL ํ—ค๋”(GL/gl.h)๋ฅผ ๋Œ์–ด์˜ค์ง€ ์•Š๊ฒŒ โ€” ์šฐ๋ฆฌ๋Š” Vulkan ๋งŒ ์“ด๋‹ค
141
+ set_source_files_properties(${IMGUI_DIR}/backends/imgui_impl_glfw.cpp PROPERTIES COMPILE_DEFINITIONS GLFW_INCLUDE_NONE)
142
+ target_include_directories(vkgs_viewer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/stb
143
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/cgltf
144
+ ${IMGUI_DIR} ${IMGUI_DIR}/backends
145
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party
146
+ ${SPVR_DIR})
147
+ target_link_libraries(vkgs_viewer PRIVATE Vulkan::Vulkan glfw)
148
+ target_compile_definitions(vkgs_viewer PRIVATE VKGS_SPIRV_DIR="${SPIRV_DIR}"
149
+ VKGS_CONFIG_DEFAULT="${CMAKE_CURRENT_SOURCE_DIR}/../../configs/sim.json"
150
+ VKGS_RENDER_GRAPH_DEFAULT="${CMAKE_CURRENT_SOURCE_DIR}/../../configs/render_graph.json"
151
+ VKGS_TRACY_PROFILER_DEFAULT="${CMAKE_BINARY_DIR}/tracy-profiler/tracy-profiler")
152
+ add_dependencies(vkgs_viewer vkgs_shaders)
153
+
154
+ # Tracy ํ”„๋กœํŒŒ์ผ๋Ÿฌ ํด๋ผ์ด์–ธํŠธ (third_party/tracy v0.11.1, BSD-3). ON_DEMAND: ํ”„๋กœํŒŒ์ผ๋Ÿฌ๊ฐ€ ๋ถ™์–ด ์žˆ์„ ๋•Œ๋งŒ ๊ธฐ๋ก โ€”
155
+ # ํ‰์†Œ ์‹คํ–‰ ๋น„์šฉ์€ ๊ฑฐ์˜ ์—†๋‹ค. ํ”„๋กœํŒŒ์ผ๋Ÿฌ GUIยท์บก์ฒ˜ ๋„๊ตฌ ๋นŒ๋“œ๋Š” scripts/build_tracy.sh.
156
+ option(VKGS_TRACY "Tracy ํ”„๋กœํŒŒ์ผ๋ง (CPUยทGPU ๊ตฌ๊ฐ„)" ON)
157
+ if(VKGS_TRACY)
158
+ set(TRACY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/tracy)
159
+ target_sources(vkgs_viewer PRIVATE ${TRACY_DIR}/public/TracyClient.cpp)
160
+ target_include_directories(vkgs_viewer PRIVATE ${TRACY_DIR}/public)
161
+ target_compile_definitions(vkgs_viewer PRIVATE TRACY_ENABLE TRACY_ON_DEMAND)
162
+ target_link_libraries(vkgs_viewer PRIVATE dl pthread)
163
+ else()
164
+ target_include_directories(vkgs_viewer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/tracy/public)
165
+ endif()
166
+
167
+ install(TARGETS vkgs_probe vkgs_viewer vkgs_voxcarve RUNTIME DESTINATION bin)
168
+ install(DIRECTORY ${SPIRV_DIR}/ DESTINATION share/vkgs/shaders)
src/vkgs/shaders/bindless.glsl ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ์ „์—ญ bindless descriptor set (set 0) โ€” viewer.cpp ์˜ Bindless ์™€ ๊ฐ™์€ bindingยท์Šฌ๋กฏ ๋ฒˆํ˜ธ.
2
+ // ๋ชจ๋“  ํŒŒ์ดํ”„๋ผ์ธ์ด ์ด set ํ•˜๋‚˜์™€ ๊ฐ™์€ push constant ๋ฒ”์œ„๋ฅผ ์“ด๋‹ค. ๋ฆฌ์†Œ์Šค๋Š” '๋ฐฐ์—ด + ๋ฒˆํ˜ธ' ๋กœ ๊ณ ๋ฅธ๋‹ค:
3
+ // binding 0 SSBO[] ์…ฐ์ด๋”๋งˆ๋‹ค ํ•„์š”ํ•œ ํƒ€์ž…์œผ๋กœ ๊ฐ™์€ binding ์„ ๋ณ„์นญ ์„ ์–ธํ•œ๋‹ค
4
+ // binding 1 UBO[]
5
+ // binding 2 sampler2D[] glTF ์žฌ์งˆ ํ…์Šค์ฒ˜, BRDF LUT
6
+ // binding 3 samplerCube[] IBL specularยทirradiance, ํ•™์Šต๋œ ํ•˜๋Š˜
7
+ // binding 4 image2D[] 4์ฑ„๋„ ๋ Œ๋” ํƒ€๊ฒŸยท๋‚ ์”จ ๊ฒฐ๊ณผยท๋ฐฐ๊ฒฝยท๊ฐ•์ˆ˜ (ํ˜•์‹ ์—†๋Š” storage โ€” ํ˜•์‹์€ ๋ Œ๋” ๊ทธ๋ž˜ํ”„ JSON)
8
+ // binding 5 image2D[] 1~2์ฑ„๋„ ๊ฑฐ๋ฆฌ ๋ˆ„์  (RG: ฮฃzฮฑ, ฮฃฮฑ โ€” depth_common.glsl)
9
+ // ํ•˜๋‚˜๋ฟ์ธ ๋ฆฌ์†Œ์Šค๋Š” ๊ณ ์ • ์Šฌ๋กฏ (์•„๋ž˜ ์ƒ์ˆ˜), ์—ฌ๋Ÿฌ ๊ฐœ์ธ ๋ฆฌ์†Œ์Šค(์žฌ์งˆยทํ…์Šค์ฒ˜ยทํ๋ธŒ)๋Š” push constant ๋‚˜ ์ „์—ญ ์ƒ์ˆ˜์˜ ๋ฒˆํ˜ธ.
10
+ #extension GL_EXT_nonuniform_qualifier : require
11
+
12
+ const uint kSsboGlobals = 0u, kSsboGauss = 1u, kSsboSplats = 2u, kSsboKeys = 3u, kSsboDraw = 4u;
13
+ const uint kUboCamera = 0u, kUboWeather = 1u;
14
+ const uint kImgTarget = 0u, kImgPost = 1u; // rgba32f
15
+ const uint kImgDepthAcc = 0u; // r32f
16
+
17
+ layout(set = 0, binding = 2) uniform sampler2D gTex2D[];
18
+ layout(set = 0, binding = 3) uniform samplerCube gCube[];
19
+ // ์ „์—ญ ์ƒ์ˆ˜ (SSBO ์Šฌ๋กฏ 0): ibl = (specular ํ๋ธŒ, irradiance ํ๋ธŒ, BRDF LUT 2D, ํ•™์Šต๋œ ํ•˜๋Š˜ ํ๋ธŒ) ๋ฒˆํ˜ธ
20
+ layout(std430, set = 0, binding = 0) readonly buffer GlobalsBuf { ivec4 ibl; ivec4 pad; } gGlobals[];
src/vkgs/shaders/depth_common.glsl ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ๊ฑฐ๋ฆฌ ๋ˆ„์  ๋ฒ„ํผ (๋ Œ๋” ํŒจ์Šค ์ƒ‰ ์ฒจ๋ถ€ 1, RG32F) โ€” ๋‚ ์”จ ํ›„์ฒ˜๋ฆฌ(weather.comp)ยท๊ฐ•์ˆ˜ ์ž…์ž(precip.frag)๊ฐ€ ์“ด๋‹ค.
2
+ // ๊ทธ๋ฆฌ๋Š” ๊ฒƒ๋งˆ๋‹ค (zยทฮฑ, ฮฑ) ๋ฅผ ์ƒ‰๊ณผ ๊ฐ™์€ premultiplied over ๋กœ ์Œ“๋Š”๋‹ค:
3
+ // R = ฮฃ zยทฮฑยทT (์นด๋ฉ”๋ผ z, m), G = ฮฃ ฮฑยทT = ๋ฐฐ๊ฒฝ์„ ๋บ€ ๋ฎ์ž„ ์ •๋„ (0 = ๋ฐฐ๊ฒฝ๋งŒ, 1 = ๋ถˆํˆฌ๋ช…ํ•œ ๊ฒƒ์ด ๋‹ค ๊ฐ€๋ฆผ)
4
+ // ๋ฐฐ๊ฒฝ(ํ•˜๋Š˜ยทํ™˜๊ฒฝ๋งต)์€ (0, 0) ์„ ฮฑ = 1 ๋กœ ์“ด๋‹ค โ€” ์•ž์˜ ๋ˆ„์ ์„ ์ง€์šฐ๊ณ  ๋ฎ์ž„ 0 ์—์„œ ์‹œ์ž‘. ์ง€์šด ๊ฐ’๋„ 0.
5
+ // ํ‘œ๋ฉด ๊นŠ์ด = R / G, ๋ฐฐ๊ฒฝ์ด ๋ณด์ด๋Š” ๋น„์œจ = 1 โˆ’ G (accum_depth / accum_cover).
6
+ // (2026-10-05 ์ „๊นŒ์ง€๋Š” R ํ•˜๋‚˜์— ๋ฐฐ๊ฒฝ 1000 m ๋ฅผ ์„ž์–ด '๊ธฐ๋Œ€ ๊นŠ์ด' ๋กœ ์ผ๋‹ค โ€” ๋ฐ˜ํˆฌ๋ช…์ด 3%๋งŒ ๋‚จ์•„๋„ 30 m ๊ฐ€ ๋”ํ•ด์ ธ
7
+ // ๋น—๋ฐฉ์šธ ๊ฐ€๋ฆผยท์•ˆ๊ฐœยท๋…ธ๋ฉด ํŒ์ •์ด ํ‹€๋ ธ๋‹ค.)
8
+ const float kNear = 0.2, kFar = 2000.0; // viewer.cpp RenderCam ๊ธฐ๋ณธ๊ฐ’๊ณผ ๊ฐ™๋‹ค
9
+ const float kSkyDepth = 1000.0;
10
+
11
+ // Vulkan [0,1] ๊นŠ์ด(gl_FragCoord.z) โ†’ ์นด๋ฉ”๋ผ z (m). viewer.cpp projection() ์˜ ์—ญ.
12
+ float linear_depth(float zndc) {
13
+ return kFar * kNear / (kFar - zndc * (kFar - kNear));
14
+ }
15
+
16
+ // ๋ˆ„์  ๋ฒ„ํผ ๊ฐ’ (R, G) โ†’ ํ‘œ๋ฉด ์นด๋ฉ”๋ผ z (m). ๋ฎ์ž„์ด ๊ฑฐ์˜ ์—†์œผ๋ฉด kSkyDepth.
17
+ float accum_depth(vec2 rg) { return rg.y > 0.02 ? rg.x / rg.y : kSkyDepth; }
18
+ float accum_cover(vec2 rg) { return clamp(rg.y, 0.0, 1.0); }
19
+
src/vkgs/shaders/env.frag ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ #extension GL_GOOGLE_include_directive : require
3
+ // ํ™˜๊ฒฝ๋งต ๋ฐฐ๊ฒฝ. 3DGS ๊ฐ€ ๋น„์–ด ์žˆ๋Š”(์•ŒํŒŒ < 1) ๊ณณ์œผ๋กœ ๋ณด์ธ๋‹ค โ€” ์Šคํ”Œ๋žซ์ด ๋’ค์— ๊ทธ๋ ค์ง€๋ฉฐ ๋ฎ๋Š”๋‹ค.
4
+ #include "ibl_common.glsl"
5
+ #include "depth_common.glsl"
6
+ layout(location = 0) in vec2 vPix;
7
+ layout(location = 0) out vec4 outColor;
8
+ layout(push_constant) uniform PC {
9
+ mat4 c2w_rot;
10
+ vec4 intr;
11
+ vec4 size;
12
+ ivec4 idx; // x = ๋ฐฐ๊ฒฝ์œผ๋กœ ์“ธ ํ๋ธŒ ๋ฒˆํ˜ธ (IBL specular ๋˜๋Š” ํ•™์Šต๋œ ํ•˜๋Š˜)
13
+ } pc;
14
+ void main() {
15
+ vec3 dc = vec3((vPix.x - pc.intr.z) / pc.intr.x, (vPix.y - pc.intr.w) / pc.intr.y, 1.0);
16
+ vec3 dw = normalize(mat3(pc.c2w_rot) * dc);
17
+ vec3 lin = textureLod(gCube[pc.idx.x], dw, pc.size.z).rgb;
18
+ // size.w = 1: ํ•™์Šต๋œ ํ•˜๋Š˜(sky.ktx2) โ€” LDR ์ •์ค€ ์ƒ‰์„ ์„ ํ˜•์œผ๋กœ ๋‹ด์•˜์„ ๋ฟ์ด๋ผ ํ•˜์ด๋ผ์ดํŠธ ์••์ถ•์„ ํ•˜์ง€ ์•Š๋Š”๋‹ค
19
+ vec3 c = pc.size.w > 0.5 ? lin : compress_highlights(lin);
20
+ outColor = vec4(linear_to_srgb(c), 1.0);
21
+ }
src/vkgs/shaders/env.vert ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ // ํ™”๋ฉด์„ ๋ฎ๋Š” ์‚ผ๊ฐํ˜• ํ•˜๋‚˜. ํ”ฝ์…€ โ†’ ์‹œ์„  ๋ฐฉํ–ฅ์€ ํ”„๋ž˜๊ทธ๋จผํŠธ์—์„œ.
3
+ layout(location = 0) out vec2 vPix;
4
+ layout(push_constant) uniform PC {
5
+ mat4 c2w_rot; // ์นด๋ฉ”๋ผ โ†’ ์”ฌ ํšŒ์ „ (OpenCV ์ถ•)
6
+ vec4 intr; // fx fy cx cy
7
+ vec4 size; // w h lod -
8
+ ivec4 idx; // env.frag ์ฐธ๊ณ 
9
+ } pc;
10
+ void main() {
11
+ vec2 uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
12
+ gl_Position = vec4(uv * 2.0 - 1.0, 1.0, 1.0);
13
+ vPix = uv * pc.size.xy;
14
+ }
src/vkgs/shaders/fxaa.comp ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ #extension GL_GOOGLE_include_directive : require
3
+ #extension GL_EXT_shader_image_load_formatted : require
4
+ // FXAA (Lottes 3.11 ํ’ˆ์งˆ ํ”„๋ฆฌ์…‹์„ ์ค„์ธ ๊ฒƒ) โ€” ํ™”๋ฉด ๊ณต๊ฐ„ ์•ˆํ‹ฐ์—์ผ๋ฆฌ์–ด์‹ฑ, ์ปดํ“จํŠธ ํ•œ ํŒจ์Šค.
5
+ // MSAA ๋Š” ์Šคํ”Œ๋žซ ๋ธ”๋ Œ๋”ฉ์„ ์ƒ˜ํ”Œ ์ˆ˜๋งŒํผ ๊ณฑํ•˜๋ฏ€๋กœ(4ร— ์ด๋ฉด ๋ž˜์Šคํ„ฐ 4๋ฐฐ) ์“ฐ์ง€ ์•Š๋Š”๋‹ค. 3DGS ๋Š” ์›๋ž˜ ๋ถ€๋“œ๋Ÿฝ๊ณ ,
6
+ // ๊ณ„๋‹จ์€ ๋ฉ”์‹œ(์ฐจ๋Ÿ‰)ยท๋งต ์„  ๊ฐ€์žฅ์ž๋ฆฌ์— ์ƒ๊ธด๋‹ค โ€” ๊ทธ ๊ฐ€์žฅ์ž๋ฆฌ๋งŒ ๋ฃจ๋งˆ ๋Œ€๋น„๋กœ ์ฐพ์•„ ๊ทธ ๋ฐฉํ–ฅ์œผ๋กœ ์„ž๋Š”๋‹ค.
7
+ // ์ž…๋ ฅ post(sRGB) โ†’ ์ถœ๋ ฅ color(์ด ์‹œ์ ์—” ๋‹ค ์“ด ์žฅ๋ฉด ์ƒ‰ ์ด๋ฏธ์ง€๋ฅผ ๋นŒ๋ ค ์“ด๋‹ค) โ†’ ๋ Œ๋” ๊ทธ๋ž˜ํ”„๊ฐ€ ๋‹ค์‹œ post ๋กœ ๋ณต์‚ฌ.
8
+ layout(local_size_x = 8, local_size_y = 8) in;
9
+ #include "bindless.glsl"
10
+ layout(set = 0, binding = 4) uniform image2D gImgF4[];
11
+ #define uIn gImgF4[kImgPost]
12
+ #define uOut gImgF4[kImgTarget]
13
+
14
+ const float EDGE_MIN = 1.0 / 24.0; // ์–ด๋‘์šด ๊ณณ์—์„œ ์ด๋ณด๋‹ค ์ž‘์€ ๋Œ€๋น„๋Š” ๋ฌด์‹œ
15
+ const float EDGE_REL = 1.0 / 8.0; // ์ฃผ๋ณ€ ์ตœ๋Œ€ ๋ฃจ๋งˆ ๋Œ€๋น„ ์ƒ๋Œ€ ์ž„๊ณ„๊ฐ’
16
+ const float SUBPIX = 0.75; // ํ”ฝ์…€ ์ดํ•˜ ๊ณ„๋‹จ ์™„ํ™” ์ •๋„
17
+ const int STEPS = 8;
18
+
19
+ ivec2 gSize;
20
+ vec3 at(ivec2 p) { return imageLoad(uIn, clamp(p, ivec2(0), gSize - 1)).rgb; }
21
+ // ์–‘์„ ํ˜• ์ƒ˜ํ”Œ (storage image ๋ผ ์ง์ ‘ ์„ž๋Š”๋‹ค)
22
+ vec3 bilerp(vec2 p) {
23
+ p -= 0.5;
24
+ ivec2 i = ivec2(floor(p));
25
+ vec2 f = p - vec2(i);
26
+ return mix(mix(at(i), at(i + ivec2(1, 0)), f.x), mix(at(i + ivec2(0, 1)), at(i + ivec2(1, 1)), f.x), f.y);
27
+ }
28
+ float luma(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }
29
+
30
+ void main() {
31
+ gSize = imageSize(uIn);
32
+ ivec2 p = ivec2(gl_GlobalInvocationID.xy);
33
+ if (p.x >= gSize.x || p.y >= gSize.y) return;
34
+ vec4 src = imageLoad(uIn, p);
35
+ float lM = luma(src.rgb);
36
+ float lN = luma(at(p + ivec2(0, -1))), lS = luma(at(p + ivec2(0, 1)));
37
+ float lW = luma(at(p + ivec2(-1, 0))), lE = luma(at(p + ivec2(1, 0)));
38
+ float lo = min(lM, min(min(lN, lS), min(lW, lE))), hi = max(lM, max(max(lN, lS), max(lW, lE)));
39
+ float range = hi - lo;
40
+ if (range < max(EDGE_MIN, hi * EDGE_REL)) { imageStore(uOut, p, src); return; }
41
+
42
+ float lNW = luma(at(p + ivec2(-1, -1))), lNE = luma(at(p + ivec2(1, -1)));
43
+ float lSW = luma(at(p + ivec2(-1, 1))), lSE = luma(at(p + ivec2(1, 1)));
44
+ // ํ”ฝ์…€ ์ดํ•˜ ํ˜ผํ•ฉ๋Ÿ‰
45
+ float avg = (2.0 * (lN + lS + lW + lE) + lNW + lNE + lSW + lSE) / 12.0;
46
+ float sub = clamp(abs(avg - lM) / range, 0.0, 1.0);
47
+ sub = smoothstep(0.0, 1.0, sub);
48
+ sub = sub * sub * SUBPIX;
49
+ // ๊ฐ€์žฅ์ž๋ฆฌ ๋ฐฉํ–ฅ: ๊ฐ€๋กœ(์œ„์•„๋ž˜๋กœ ๋ฐ”๋€œ) ๋˜๋Š” ์„ธ๋กœ
50
+ float eH = abs(lNW + lNE - 2.0 * lN) + 2.0 * abs(lW + lE - 2.0 * lM) + abs(lSW + lSE - 2.0 * lS);
51
+ float eV = abs(lNW + lSW - 2.0 * lW) + 2.0 * abs(lN + lS - 2.0 * lM) + abs(lNE + lSE - 2.0 * lE);
52
+ bool horz = eH >= eV;
53
+ float l1 = horz ? lN : lW, l2 = horz ? lS : lE;
54
+ float g1 = abs(l1 - lM), g2 = abs(l2 - lM);
55
+ bool steep1 = g1 >= g2;
56
+ float grad = 0.25 * max(g1, g2);
57
+ vec2 stepN = horz ? vec2(0.0, steep1 ? -1.0 : 1.0) : vec2(steep1 ? -1.0 : 1.0, 0.0);
58
+ float lEdge = 0.5 * (lM + (steep1 ? l1 : l2));
59
+ vec2 c = vec2(p) + 0.5 + 0.5 * stepN;
60
+ vec2 along = horz ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
61
+ // ๊ฐ€์žฅ์ž๋ฆฌ๋ฅผ ๋”ฐ๋ผ ์–‘์ชฝ ๋ ์ฐพ๊ธฐ
62
+ vec2 a = c - along, b = c + along;
63
+ float da = luma(bilerp(a)) - lEdge, db = luma(bilerp(b)) - lEdge;
64
+ bool doneA = abs(da) >= grad, doneB = abs(db) >= grad;
65
+ for (int k = 0; k < STEPS && !(doneA && doneB); ++k) {
66
+ float s = k < 3 ? 1.0 : 2.0;
67
+ if (!doneA) { a -= along * s; da = luma(bilerp(a)) - lEdge; doneA = abs(da) >= grad; }
68
+ if (!doneB) { b += along * s; db = luma(bilerp(b)) - lEdge; doneB = abs(db) >= grad; }
69
+ }
70
+ vec2 here = vec2(p) + 0.5;
71
+ float distA = horz ? here.x - a.x : here.y - a.y, distB = horz ? b.x - here.x : b.y - here.y;
72
+ bool nearA = distA < distB;
73
+ float dist = min(distA, distB), len = distA + distB;
74
+ bool smaller = lM - lEdge < 0.0;
75
+ bool ok = ((nearA ? da : db) < 0.0) != smaller;
76
+ float off = ok ? 0.5 - dist / len : 0.0;
77
+ off = max(off, sub);
78
+ vec3 col = bilerp(here + stepN * off);
79
+ imageStore(uOut, p, vec4(col, src.a));
80
+ }
src/vkgs/shaders/gs_common.glsl ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 3DGS ์…ฐ์ด๋” ๊ณตํ†ต ์ •์˜. ์ˆ˜์‹๊ณผ ์ƒ์ˆ˜๋Š” gsplat 1.4.0 (ํ•™์Šต๊ธฐ)๊ณผ ๋˜‘๊ฐ™์ด ๋งž์ถ˜๋‹ค โ€”
2
+ // ๋‹ค๋ฅด๋ฉด ํ•™์Šต๊ธฐ ๋ Œ๋”์™€ Vulkan ๋ Œ๋”๊ฐ€ ํ”ฝ์…€ ๋‹จ์œ„๋กœ ์–ด๊ธ‹๋‚œ๋‹ค (roadmap 2๋‹จ๊ณ„ ํ•ฉ๊ฒฉ ๊ธฐ์ค€ < 2%).
3
+
4
+ // ๊ฐ€์šฐ์‹œ์•ˆ ํ•˜๋‚˜ = vec4 15๊ฐœ (240 B). 3D ๊ณต๋ถ„์‚ฐ์€ ๋กœ๋“œ ์‹œ CPU ์—์„œ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•œ๋‹ค.
5
+ struct Gaussian {
6
+ vec4 pos_opacity; // xyz = ์”ฌ ์›์  ๊ธฐ์ค€ ์œ„์น˜, w = sigmoid ์ ์šฉ๋œ ๋ถˆํˆฌ๋ช…๋„
7
+ vec4 cov_a; // ฮฃ xx, xy, xz, yy
8
+ vec4 cov_b; // ฮฃ yz, zz, -, -
9
+ vec4 sh[12]; // SH ๊ณ„์ˆ˜ 16๊ฐœ ร— RGB = 48 float. sh ๊ณ„์ˆ˜ k ์˜ ์ฑ„๋„ c ๋Š” [k*3+c]
10
+ };
11
+
12
+ // ํ™”๋ฉด๊ณต๊ฐ„ ์Šคํ”Œ๋žซ = vec4 3๊ฐœ
13
+ // [0] = (u, v, radius, opacity) u,v ๋Š” ํ”ฝ์…€ ์ขŒํ‘œ (gsplat ๊ณผ ๊ฐ™์ด ํ”ฝ์…€ ์ค‘์‹ฌ์ด +0.5)
14
+ // [1] = (conic.xyz, -) 2D ๊ณต๋ถ„์‚ฐ์˜ ์—ญํ–‰๋ ฌ (a, b, c)
15
+ // [2] = (rgb, -)
16
+
17
+ #include "bindless.glsl"
18
+
19
+ layout(std140, set = 0, binding = 1) uniform Camera {
20
+ mat4 w2c; // ์”ฌ ์›์  ๊ธฐ์ค€ world โ†’ camera (OpenCV: x ์šฐ, y ํ•˜, z ์ „๋ฐฉ)
21
+ vec4 intr; // fx, fy, cx, cy (ํ”ฝ์…€)
22
+ vec4 size; // width, height, near, far
23
+ vec4 campos; // xyz = ์นด๋ฉ”๋ผ ์œ„์น˜ (์”ฌ ์›์  ๊ธฐ์ค€), w = ๋†’์ด ํด๋ฆฝ (์ด๋ณด๋‹ค ๋†’์€ ๊ฐ€์šฐ์‹œ์•ˆ์€ ์•ˆ ๊ทธ๋ฆผ)
24
+ uvec4 counts; // N, N_pow2, sh_degree, -
25
+ mat4 obj_xform; // ๊ฐ์ฒด ์—์…‹(์ž์ฐจ ๋“ฑ) ๋กœ์ปฌ โ†’ ์”ฌ ์ขŒํ‘œ. obj.x ๋ฒˆ์งธ ๊ฐ€์šฐ์‹œ์•ˆ๋ถ€ํ„ฐ ์ ์šฉ
26
+ uvec4 obj; // x = ๊ฐ์ฒด ์‹œ์ž‘ ์ธ๋ฑ์Šค (์—†์œผ๋ฉด N), y = ๋ณด์ด๊ธฐ(1)/์ˆจ๊ธฐ๊ธฐ(0)
27
+ uvec4 objs; // ์žฅ๋ฉด ๊ฐ์ฒด(์ฐจ๋Ÿ‰ ์—์…‹ ๋“ฑ, ๋ณ„๋„ ๋ฒ„ํผ): x = ๊ฐ€์šฐ์‹œ์•ˆ SSBO ์Šฌ๋กฏ, y = ๊ฐ์ฒด ๋ฒˆํ˜ธ SSBO ์Šฌ๋กฏ,
28
+ // z = ๊ฐ์ฒด ํ‘œ SSBO ์Šฌ๋กฏ, w = ๊ฐ์ฒด ๊ฐ€์šฐ์‹œ์•ˆ ์ˆ˜. ์ „์—ญ ๋ฒˆํ˜ธ counts.x + j ๊ฐ€ ๊ฐ์ฒด ๊ฐ€์šฐ์‹œ์•ˆ j
29
+ uvec4 mode; // ์นด๋ฉ”๋ผ ๋ชจ๋“œ (camera_mode.comp): x = 0 RGB ยท 1 ๊นŠ์ด ยท 2 ์‹œ๋งจํ‹ฑ ยท 3 ํ‘๋ฐฑ,
30
+ // y = ์‹œ๋งจํ‹ฑ ์ฐจ๋Ÿ‰ ์ƒ‰ (RGBA8, ๊ฐ์ฒด ์—์…‹), z = ์‹œ๋งจํ‹ฑ ํ•˜๋Š˜ ์ƒ‰ (RGBA8), w = ๊นŠ์ด ์ตœ๋Œ€ ๊ฑฐ๋ฆฌ m (float ๋น„ํŠธ)
31
+ } gCamera[];
32
+ #define cam gCamera[kUboCamera]
src/vkgs/shaders/gs_preprocess.comp ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ #extension GL_GOOGLE_include_directive : require
3
+ // ๊ฐ€์šฐ์‹œ์•ˆ๋งˆ๋‹ค: ํˆฌ์˜ โ†’ 2D ๊ณต๋ถ„์‚ฐ โ†’ conic/๋ฐ˜์ง€๋ฆ„ โ†’ SH ์ƒ‰ โ†’ ์ •๋ ฌ ํ‚ค.
4
+ // gsplat fully_fused_projection_fwd.cu / spherical_harmonics.cuh ๋ฅผ ๊ทธ๋Œ€๋กœ ์˜ฎ๊ฒผ๋‹ค.
5
+
6
+ layout(local_size_x = 256) in;
7
+
8
+ #include "gs_common.glsl"
9
+
10
+ layout(std430, set = 0, binding = 0) readonly buffer Gaussians { Gaussian g[]; } bGauss[];
11
+ layout(std430, set = 0, binding = 0) writeonly buffer Splats { vec4 splat[]; } bSplats[];
12
+ layout(std430, set = 0, binding = 0) writeonly buffer Keys { uvec2 keys[]; } bKeys[];
13
+ layout(std430, set = 0, binding = 0) buffer DrawArgs {
14
+ uint vertexCount; uint instanceCount; uint firstVertex; uint firstInstance;
15
+ } bDraw[];
16
+ #define g bGauss[kSsboGauss].g
17
+ // ์žฅ๋ฉด ๊ฐ์ฒด (์ฐจ๋Ÿ‰ ์—์…‹): ๊ฐ์ฒด ๊ฐ€์šฐ์‹œ์•ˆ j ์˜ ๊ฐ์ฒด ๋ฒˆํ˜ธ, ๊ฐ์ฒด ํ‘œ (๋ฐ•์Šค ๋กœ์ปฌ โ†’ ์”ฌ, ๋ณด์ด๊ธฐ)
18
+ layout(std430, set = 0, binding = 0) readonly buffer ObjGaussians { Gaussian og[]; } bObjGauss[];
19
+ layout(std430, set = 0, binding = 0) readonly buffer ObjIds { uint id[]; } bObjIds[];
20
+ struct ObjInstance { mat4 xform; vec4 flags; }; // flags.x = ๋ณด์ด๊ธฐ
21
+ layout(std430, set = 0, binding = 0) readonly buffer ObjTable { ObjInstance inst[]; } bObjTable[];
22
+ #define splat bSplats[kSsboSplats].splat
23
+ #define keys bKeys[kSsboKeys].keys
24
+ #define draw bDraw[kSsboDraw]
25
+
26
+ const float EPS2D = 0.3; // gsplat rasterization(eps2d=0.3) โ€” ์ €์—ญ ํ†ต๊ณผ ๋ธ”๋Ÿฌ
27
+
28
+ vec3 sh_color(in Gaussian G, vec3 dir, uint degree) {
29
+ // sh_coeffs_to_color_fast ์™€ ๊ฐ™์€ ์ˆœ์„œ/์ƒ์ˆ˜
30
+ vec3 c[16];
31
+ for (int k = 0; k < 16; ++k) {
32
+ int f = k * 3;
33
+ c[k] = vec3(G.sh[f / 4][f % 4], G.sh[(f + 1) / 4][(f + 1) % 4], G.sh[(f + 2) / 4][(f + 2) % 4]);
34
+ }
35
+ vec3 r = 0.2820947917738781 * c[0];
36
+ if (degree < 1) return r;
37
+ vec3 d = normalize(dir);
38
+ float x = d.x, y = d.y, z = d.z;
39
+ r += 0.48860251190292 * (-y * c[1] + z * c[2] - x * c[3]);
40
+ if (degree < 2) return r;
41
+ float z2 = z * z;
42
+ float fTmp0B = -1.092548430592079 * z;
43
+ float fC1 = x * x - y * y;
44
+ float fS1 = 2.0 * x * y;
45
+ r += (0.5462742152960395 * fS1) * c[4] + (fTmp0B * y) * c[5]
46
+ + (0.9461746957575601 * z2 - 0.3153915652525201) * c[6]
47
+ + (fTmp0B * x) * c[7] + (0.5462742152960395 * fC1) * c[8];
48
+ if (degree < 3) return r;
49
+ float fTmp0C = -2.285228997322329 * z2 + 0.4570457994644658;
50
+ float fTmp1B = 1.445305721320277 * z;
51
+ float fC2 = x * fC1 - y * fS1;
52
+ float fS2 = x * fS1 + y * fC1;
53
+ r += (-0.5900435899266435 * fS2) * c[9] + (fTmp1B * fS1) * c[10]
54
+ + (fTmp0C * y) * c[11] + (z * (1.865881662950577 * z2 - 1.119528997770346)) * c[12]
55
+ + (fTmp0C * x) * c[13] + (fTmp1B * fC1) * c[14] + (-0.5900435899266435 * fC2) * c[15];
56
+ return r;
57
+ }
58
+
59
+ void main() {
60
+ uint i = gl_GlobalInvocationID.x;
61
+ uint n = cam.counts.x;
62
+ if (i >= n + cam.objs.w) return;
63
+ // ์ „์—ญ ๋ฒˆํ˜ธ i: [0, n) ๋ฐฐ๊ฒฝ (+ ์ž์ฐจ ์—์…‹), [n, n + objs.w) ์žฅ๋ฉด ๊ฐ์ฒด (๋ณ„๋„ ๋ฒ„ํผ, ๊ฐ์ฒด๋งˆ๋‹ค ์ž์„ธ)
64
+ Gaussian G;
65
+ mat4 X = cam.obj_xform;
66
+ bool is_obj = false;
67
+ if (i >= n) {
68
+ uint j = i - n;
69
+ G = bObjGauss[cam.objs.x].og[j];
70
+ ObjInstance oi = bObjTable[cam.objs.z].inst[bObjIds[cam.objs.y].id[j]];
71
+ if (oi.flags.x < 0.5) return; // ์ง€์› ๊ฑฐ๋‚˜ ์ด ์‹œ๊ฐ์— ์—†๋Š” ๊ฐ์ฒด
72
+ X = oi.xform;
73
+ is_obj = true;
74
+ } else {
75
+ G = g[i];
76
+ // ๊ฐ์ฒด ์—์…‹(์ž์ฐจ): ๋ฐ•์Šค ๋กœ์ปฌ ์ขŒํ‘œ โ†’ ์ด๋ฒˆ ํ”„๋ ˆ์ž„์˜ ์ž์ฐจ ํฌ์ฆˆ
77
+ is_obj = i >= cam.obj.x;
78
+ if (is_obj && cam.obj.y == 0u) return;
79
+ }
80
+ vec3 p = G.pos_opacity.xyz;
81
+ if (is_obj) p = (X * vec4(p, 1.0)).xyz;
82
+ // BEV ์—์„œ ๋‚˜๋ฌด ์บ๋…ธํ”ผ๊ฐ€ ์ง€๋ฉด์„ ๋ฎ์ง€ ์•Š๋„๋ก ์ผ์ • ๋†’์ด ์œ„๋Š” ๋บ€๋‹ค
83
+ if (p.z > cam.campos.w) return;
84
+ vec3 t = (cam.w2c * vec4(p, 1.0)).xyz;
85
+ float near = cam.size.z, far = cam.size.w;
86
+ if (t.z < near || t.z > far) return;
87
+
88
+ // world ๊ณต๋ถ„์‚ฐ โ†’ camera ๊ณต๋ถ„์‚ฐ
89
+ vec4 a = G.cov_a, b = G.cov_b;
90
+ mat3 S = mat3(a.x, a.y, a.z,
91
+ a.y, a.w, b.x,
92
+ a.z, b.x, b.y);
93
+ if (is_obj) {
94
+ mat3 Ro = mat3(X);
95
+ S = Ro * S * transpose(Ro);
96
+ }
97
+ mat3 W = mat3(cam.w2c);
98
+ mat3 Sc = W * S * transpose(W);
99
+
100
+ // persp_proj: ํ™”๋ฉด ๋ฐ– ๋ฉ€๋ฆฌ ์žˆ๋Š” ๊ฐ€์šฐ์‹œ์•ˆ์˜ ์•ผ์ฝ”๋น„์•ˆ์ด ํญ์ฃผํ•˜์ง€ ์•Š๊ฒŒ x/z ๋ฅผ ์ž๋ฅธ๋‹ค
101
+ float fx = cam.intr.x, fy = cam.intr.y, cx = cam.intr.z, cy = cam.intr.w;
102
+ float w = cam.size.x, h = cam.size.y;
103
+ float tan_fovx = 0.5 * w / fx, tan_fovy = 0.5 * h / fy;
104
+ float lim_x_pos = (w - cx) / fx + 0.3 * tan_fovx;
105
+ float lim_x_neg = cx / fx + 0.3 * tan_fovx;
106
+ float lim_y_pos = (h - cy) / fy + 0.3 * tan_fovy;
107
+ float lim_y_neg = cy / fy + 0.3 * tan_fovy;
108
+ float rz = 1.0 / t.z, rz2 = rz * rz;
109
+ float tx = t.z * min(lim_x_pos, max(-lim_x_neg, t.x * rz));
110
+ float ty = t.z * min(lim_y_pos, max(-lim_y_neg, t.y * rz));
111
+ vec3 j0 = vec3(fx * rz, 0.0, -fx * tx * rz2);
112
+ vec3 j1 = vec3(0.0, fy * rz, -fy * ty * rz2);
113
+ float c00 = dot(j0, Sc * j0) + EPS2D;
114
+ float c01 = dot(j0, Sc * j1);
115
+ float c11 = dot(j1, Sc * j1) + EPS2D;
116
+
117
+ float det = c00 * c11 - c01 * c01;
118
+ if (det <= 0.0) return;
119
+ vec3 conic = vec3(c11, -c01, c00) / det;
120
+
121
+ float bb = 0.5 * (c00 + c11);
122
+ float v1 = bb + sqrt(max(0.01, bb * bb - det));
123
+ float radius = ceil(3.0 * sqrt(v1));
124
+ vec2 mean2d = vec2(fx * t.x * rz + cx, fy * t.y * rz + cy);
125
+ if (mean2d.x + radius <= 0.0 || mean2d.x - radius >= w ||
126
+ mean2d.y + radius <= 0.0 || mean2d.y - radius >= h) return;
127
+
128
+ vec3 rgb;
129
+ if (cam.mode.x == 2u) { // ์‹œ๋งจํ‹ฑ ์นด๋ฉ”๋ผ: ๋ผ๋ฒจ ์ƒ‰ (ply.cpp ๊ฐ€ semantic.rgba ๋ฅผ cov_b.w ์— ๋„ฃ๋Š”๋‹ค). ๊ฐ์ฒด ์—์…‹ = ์ฐจ๋Ÿ‰ ์ƒ‰
130
+ rgb = is_obj ? unpackUnorm4x8(cam.mode.y).rgb : unpackUnorm4x8(floatBitsToUint(G.cov_b.w)).rgb;
131
+ } else {
132
+ rgb = sh_color(G, p - cam.campos.xyz, cam.counts.z);
133
+ rgb = max(rgb + 0.5, vec3(0.0)); // gsplat: clamp_min(colors + 0.5, 0)
134
+ }
135
+
136
+ splat[3 * i + 0] = vec4(mean2d, radius, G.pos_opacity.w);
137
+ splat[3 * i + 1] = vec4(conic, 0.0);
138
+ splat[3 * i + 2] = vec4(rgb, t.z); // w = ์นด๋ฉ”๋ผ ๊นŠ์ด โ€” ๋ฉ”์‹œ(PBR)์™€ ๊นŠ์ด ํ•ฉ์„ฑ์šฉ
139
+
140
+ // ๋’ค์—์„œ ์•ž์œผ๋กœ ๊ทธ๋ฆฌ๋ฏ€๋กœ ๋จผ ๊ฒƒ์ด ๋จผ์ € ์˜ค๋„๋ก ๊นŠ์ด๋ฅผ ๋’ค์ง‘๋Š”๋‹ค.
141
+ // ์–‘์ˆ˜ float ์˜ ๋น„ํŠธ ํŒจํ„ด์€ ๊ฐ’๊ณผ ๋‹จ์กฐ ๊ด€๊ณ„๋ผ uint ๋น„๊ต๋กœ ์ •๋ ฌ๋œ๋‹ค.
142
+ // ๋ณด์ด๋Š” ๊ฒƒ๋งŒ ์•ž์ชฝ์— ๋ชจ์€๋‹ค (์ •๋ ฌ์€ [0, ๋ณด์ด๋Š” ์ˆ˜) ๋งŒ ํ•œ๋‹ค โ€” gs_radix_*.comp, bitonic ์€ ๋’ค๋ฅผ ์ฑ„์›Œ์„œ)
143
+ uint slot = atomicAdd(draw.instanceCount, 1u);
144
+ keys[slot] = uvec2(0xFFFFFFFEu - floatBitsToUint(t.z), i);
145
+ }
src/vkgs/shaders/gs_radix_common.glsl ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // GPU radix ์ •๋ ฌ (LSD, 8๋น„ํŠธ ์ž๋ฆฟ์ˆ˜ ร— 4ํŒจ์Šค) ๊ณตํ†ต โ€” gs_radix_hist / scan / scatter.comp.
2
+ // ์ •๋ ฌ ๋Œ€์ƒ: keys[0, ๋ณด์ด๋Š” ์ˆ˜) ์˜ (key, index). ๋ณด์ด๋Š” ์ˆ˜๋Š” GPU ์˜ ์ธ๋‹ค์ด๋ ‰ํŠธ ์ธ์ž์—์„œ ์ฝ๋Š”๋‹ค (CPU ์™•๋ณต ์—†์Œ).
3
+ // ์›Œํฌ๊ทธ๋ฃน ์ˆ˜ G ๋Š” ๊ณ ์ •์ด๊ณ , ์›Œํฌ๊ทธ๋ฃน g ๋Š” ์—ฐ์† ๋ธ”๋ก [gยทB, (g+1)ยทB) ๋ฅผ ๋งก๋Š”๋‹ค (B = ๋ณด์ด๋Š” ์ˆ˜ / G ๋ฅผ 256 ๋ฐฐ์ˆ˜๋กœ ์˜ฌ๋ฆผ).
4
+ // ํŒจ์Šค๋งˆ๋‹ค ์›๋ณธ โ†” ์ž„์‹œ ๋ฒ„ํผ๋ฅผ ์˜ค๊ฐ€๋ฏ€๋กœ 4ํŒจ์Šค ๋’ค ๊ฒฐ๊ณผ๋Š” ๋‹ค์‹œ keys ์— ์žˆ๋‹ค. ์•ˆ์ • ์ •๋ ฌ์ด๋‹ค.
5
+ #include "bindless.glsl"
6
+ layout(std430, set = 0, binding = 0) buffer Keys { uvec2 keys[]; } bKeys[];
7
+ layout(std430, set = 0, binding = 0) buffer Hist { uint h[]; } bHist[];
8
+ layout(std430, set = 0, binding = 0) readonly buffer DrawArgs {
9
+ uint vertexCount; uint instanceCount; uint firstVertex; uint firstInstance;
10
+ } bDraw[];
11
+ // shift = ์ด๋ฒˆ ์ž๋ฆฟ์ˆ˜ ์œ„์น˜, flip = 0 ์ด๋ฉด keys โ†’ tmp, 1 ์ด๋ฉด tmp โ†’ keys, groups = G, slots = (keys, tmp, hist)
12
+ layout(push_constant) uniform Radix { uint shift; uint flip; uint groups; uint pad; uvec4 slots; } pc;
13
+ #define SRC bKeys[pc.flip == 0u ? pc.slots.x : pc.slots.y].keys
14
+ #define DST bKeys[pc.flip == 0u ? pc.slots.y : pc.slots.x].keys
15
+ #define HIST bHist[pc.slots.z].h
16
+ const uint RADIX = 256u;
17
+
18
+ uint radix_count() { return bDraw[kSsboDraw].instanceCount; }
19
+ uint radix_block(uint count) { return ((count + pc.groups - 1u) / pc.groups + 255u) / 256u * 256u; }
src/vkgs/shaders/gs_radix_hist.comp ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ #extension GL_GOOGLE_include_directive : require
3
+ #extension GL_EXT_nonuniform_qualifier : require
4
+ // radix 1/3: ์›Œํฌ๊ทธ๋ฃน๋งˆ๋‹ค ์ž๊ธฐ ๋ธ”๋ก์˜ ์ž๋ฆฟ์ˆ˜ ํžˆ์Šคํ† ๊ทธ๋žจ โ†’ HIST[gยท256 + d]
5
+ layout(local_size_x = 256) in;
6
+ #include "gs_radix_common.glsl"
7
+
8
+ shared uint s_hist[RADIX];
9
+
10
+ void main() {
11
+ uint t = gl_LocalInvocationID.x, g = gl_WorkGroupID.x;
12
+ s_hist[t] = 0u;
13
+ barrier();
14
+ uint count = radix_count(), block = radix_block(count);
15
+ uint b0 = min(g * block, count), b1 = min(b0 + block, count);
16
+ for (uint i = b0 + t; i < b1; i += 256u) atomicAdd(s_hist[(SRC[i].x >> pc.shift) & 255u], 1u);
17
+ barrier();
18
+ HIST[g * RADIX + t] = s_hist[t];
19
+ }
src/vkgs/shaders/gs_radix_scan.comp ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ #extension GL_GOOGLE_include_directive : require
3
+ #extension GL_EXT_nonuniform_qualifier : require
4
+ // radix 2/3 (์›Œํฌ๊ทธ๋ฃน ํ•˜๋‚˜): HIST[gยท256 + d] โ†’ ๊ทธ (์ž๋ฆฟ์ˆ˜, ์›Œํฌ๊ทธ๋ฃน) ์˜ ์ถœ๋ ฅ ์‹œ์ž‘ ์œ„์น˜.
5
+ // ์ˆœ์„œ๋Š” ์ž๋ฆฟ์ˆ˜ ๋จผ์ €, ๊ฐ™์€ ์ž๋ฆฟ์ˆ˜ ์•ˆ์—์„œ ์›Œํฌ๊ทธ๋ฃน ์ˆœ โ€” ๊ทธ๋ž˜์„œ ์•ˆ์ • ์ •๋ ฌ์ด ๋œ๋‹ค.
6
+ layout(local_size_x = 256) in;
7
+ #include "gs_radix_common.glsl"
8
+
9
+ shared uint s_sum[RADIX];
10
+
11
+ void main() {
12
+ uint d = gl_LocalInvocationID.x;
13
+ uint total = 0u;
14
+ for (uint g = 0u; g < pc.groups; ++g) total += HIST[g * RADIX + d];
15
+ s_sum[d] = total;
16
+ barrier();
17
+ // ์ž๋ฆฟ์ˆ˜ ํ•ฉ์˜ ๋ฐฐํƒ€์  ๋ˆ„์  (Hillisโ€“Steele)
18
+ for (uint off = 1u; off < RADIX; off <<= 1u) {
19
+ uint v = d >= off ? s_sum[d - off] : 0u;
20
+ barrier();
21
+ s_sum[d] += v;
22
+ barrier();
23
+ }
24
+ uint run = s_sum[d] - total;
25
+ for (uint g = 0u; g < pc.groups; ++g) {
26
+ uint c = HIST[g * RADIX + d];
27
+ HIST[g * RADIX + d] = run;
28
+ run += c;
29
+ }
30
+ }
src/vkgs/shaders/gs_radix_scatter.comp ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ #extension GL_GOOGLE_include_directive : require
3
+ #extension GL_EXT_nonuniform_qualifier : require
4
+ #extension GL_KHR_shader_subgroup_basic : require
5
+ #extension GL_KHR_shader_subgroup_ballot : require
6
+ // radix 3/3: ๋ธ”๋ก์„ 256๊ฐœ์”ฉ ์ฐจ๋ก€๋กœ, ๊ฐ™์€ ์ž๋ฆฟ์ˆ˜๋ผ๋ฆฌ์˜ ์ˆœ์œ„(์•ˆ์ •)๋ฅผ ๋งค๊ฒจ ์ œ์ž๋ฆฌ๋กœ ๋ณด๋‚ธ๋‹ค.
7
+ // ์ˆœ์œ„: subgroup ballot 8๋ฒˆ์œผ๋กœ '๊ฐ™์€ ์ž๋ฆฟ์ˆ˜์ธ ๋ ˆ์ธ' ๋งˆ์Šคํฌ๋ฅผ ๋งŒ๋“ค๊ณ  (WLMS), ๊ทธ ์•ˆ์—์„œ ์•ž์„  ๋ ˆ์ธ ์ˆ˜.
8
+ // subgroup ์‚ฌ์ด๋Š” ๊ณต์œ  ๋ฉ”๋ชจ๋ฆฌ์˜ (subgroup, ์ž๋ฆฟ์ˆ˜) ๊ฐœ์ˆ˜ ๋ˆ„์ ์œผ๋กœ ์ž‡๋Š”๋‹ค. subgroup ํฌ๊ธฐ 16 ์ด์ƒ์ด ํ•„์š”ํ•˜๋‹ค (๋ทฐ์–ด๊ฐ€ ๊ฒ€์‚ฌ).
9
+ layout(local_size_x = 256) in;
10
+ #include "gs_radix_common.glsl"
11
+
12
+ const uint MAX_SG = 16u; // 256 / 16
13
+ shared uint s_off[RADIX]; // ์ด ์›Œํฌ๊ทธ๋ฃน์˜ ์ž๋ฆฟ์ˆ˜๋ณ„ ๋‹ค์Œ ์ถœ๋ ฅ ์œ„์น˜
14
+ shared uint s_sg[MAX_SG][RADIX]; // (subgroup, ์ž๋ฆฟ์ˆ˜) ๊ฐœ์ˆ˜ โ†’ ๋ฐฐํƒ€์  ๋ˆ„์ 
15
+
16
+ void main() {
17
+ uint t = gl_LocalInvocationID.x, g = gl_WorkGroupID.x;
18
+ uint nsg = gl_NumSubgroups, sg = gl_SubgroupID;
19
+ s_off[t] = HIST[g * RADIX + t];
20
+ uint count = radix_count(), block = radix_block(count);
21
+ uint b0 = min(g * block, count), b1 = min(b0 + block, count);
22
+ // ์›์†Œ ์ˆœ์„œ = (subgroup, ๋ ˆ์ธ) ์ˆœ์„œ โ€” ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด ๋กœ๋“œ ์œ„์น˜๋ฅผ ๊ทธ ์ˆœ์„œ๋กœ ์ •ํ•œ๋‹ค
23
+ uint lane_index = sg * gl_SubgroupSize + gl_SubgroupInvocationID;
24
+ for (uint c = b0; c < b1; c += 256u) {
25
+ for (uint s = 0u; s < nsg; ++s) s_sg[s][t] = 0u;
26
+ barrier();
27
+ uint i = c + lane_index;
28
+ bool valid = i < b1;
29
+ uvec2 kv = valid ? SRC[i] : uvec2(0u);
30
+ uint d = (kv.x >> pc.shift) & 255u;
31
+ uvec4 peers = subgroupBallot(valid);
32
+ for (uint b = 0u; b < 8u; ++b) {
33
+ bool bit = ((d >> b) & 1u) != 0u;
34
+ uvec4 bal = subgroupBallot(bit);
35
+ peers &= bit ? bal : ~bal;
36
+ }
37
+ uint rank = subgroupBallotExclusiveBitCount(peers);
38
+ if (valid && subgroupBallotFindLSB(peers) == gl_SubgroupInvocationID)
39
+ s_sg[sg][d] = subgroupBallotBitCount(peers);
40
+ barrier();
41
+ uint run = 0u; // ์Šค๋ ˆ๋“œ t = ์ž๋ฆฟ์ˆ˜ t
42
+ for (uint s = 0u; s < nsg; ++s) {
43
+ uint v = s_sg[s][t];
44
+ s_sg[s][t] = run;
45
+ run += v;
46
+ }
47
+ barrier();
48
+ if (valid) DST[s_off[d] + s_sg[sg][d] + rank] = kv;
49
+ barrier();
50
+ s_off[t] += run;
51
+ barrier();
52
+ }
53
+ }
src/vkgs/shaders/gs_sort.comp ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ #extension GL_GOOGLE_include_directive : require
3
+ // bitonic ์ •๋ ฌ ํ•œ ๋‹จ๊ณ„ (k, j). (key, index) ์Œ์„ key ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ โ€” ๊ธฐ์ค€ ๊ตฌํ˜„ (๊ธฐ๋ณธ์€ radix, gs_radix_*.comp).
4
+ // ์ „์ฒ˜๋ฆฌ๊ฐ€ ๋ณด์ด๋Š” ๊ฒƒ๋งŒ [0, ๋ณด์ด๋Š” ์ˆ˜) ์— ๋ชจ์œผ๋ฏ€๋กœ, k = 0 ์ธ '์ฑ„์šฐ๊ธฐ' ๋‹จ๊ณ„๊ฐ€ ๋จผ์ € ๋’ค์ชฝ์„ ์•ˆ ๋ณด์ž„ ํ‚ค๋กœ ์ฑ„์šด๋‹ค.
5
+ // N=2^21 ์ด๋ฉด 231 ๋ฒˆ ๋””์ŠคํŒจ์น˜.
6
+
7
+ layout(local_size_x = 256) in;
8
+
9
+ #include "bindless.glsl"
10
+ layout(std430, set = 0, binding = 0) buffer Keys { uvec2 keys[]; } bKeys[];
11
+ layout(std430, set = 0, binding = 0) readonly buffer DrawArgs {
12
+ uint vertexCount; uint instanceCount; uint firstVertex; uint firstInstance;
13
+ } bDraw[];
14
+ #define keys bKeys[kSsboKeys].keys
15
+ layout(push_constant) uniform Step { uint j; uint k; uint n; } pc;
16
+
17
+ void main() {
18
+ uint i = gl_GlobalInvocationID.x;
19
+ if (i >= pc.n) return;
20
+ if (pc.k == 0u) { // ์ฑ„์šฐ๊ธฐ: ๋ณด์ด์ง€ ์•Š๋Š” ์ž๋ฆฌ โ†’ ์ •๋ ฌ ํ›„ ๋งจ ๋’ค
21
+ if (i >= bDraw[kSsboDraw].instanceCount) keys[i] = uvec2(0xFFFFFFFFu, 0u);
22
+ return;
23
+ }
24
+ uint l = i ^ pc.j;
25
+ if (l <= i) return;
26
+ uvec2 a = keys[i], b = keys[l];
27
+ bool ascending = (i & pc.k) == 0u;
28
+ if (ascending ? (a.x > b.x) : (a.x < b.x)) {
29
+ keys[i] = b;
30
+ keys[l] = a;
31
+ }
32
+ }
src/vkgs/shaders/gs_splat.frag ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ // gsplat rasterize_to_pixels_fwd.cu ์˜ ํ”ฝ์…€ ๊ธฐ์—ฌ ๊ณ„์‚ฐ๊ณผ ๊ฐ™๋‹ค.
3
+ // ๋’คโ†’์•ž ์ˆœ์„œ๋กœ premultiplied over ๋ธ”๋ Œ๋”ฉ (src ONE, dst ONE_MINUS_SRC_ALPHA).
4
+ // ์•žโ†’๋’ค๋กœ ํ•ฉ์น˜๋Š” gsplat ๊ณผ ์ˆ˜ํ•™์ ์œผ๋กœ ๊ฐ™๊ณ , ๋‹ค๋ฅธ ์ ์€ gsplat ์˜ ์กฐ๊ธฐ ์ข…๋ฃŒ(T โ‰ค 1e-4) ๋ฟ์ด๋‹ค.
5
+
6
+ layout(location = 0) flat in vec2 vCenter;
7
+ layout(location = 1) flat in vec3 vConic;
8
+ layout(location = 2) flat in vec4 vColor;
9
+ layout(location = 3) flat in float vZ;
10
+ layout(location = 0) out vec4 outColor;
11
+ layout(location = 1) out vec4 outDepth; // ๊ฑฐ๋ฆฌ ๋ˆ„์  (depth_common.glsl)
12
+
13
+ void main() {
14
+ vec2 d = vCenter - gl_FragCoord.xy; // gl_FragCoord ๋Š” ํ”ฝ์…€ ์ค‘์‹ฌ(+0.5) โ€” gsplat ์˜ px ์™€ ๊ฐ™๋‹ค
15
+ float sigma = 0.5 * (vConic.x * d.x * d.x + vConic.z * d.y * d.y) + vConic.y * d.x * d.y;
16
+ if (sigma < 0.0) discard;
17
+ float alpha = min(0.999, vColor.a * exp(-sigma));
18
+ if (alpha < 1.0 / 255.0) discard;
19
+ outColor = vec4(vColor.rgb * alpha, alpha);
20
+ outDepth = vec4(vZ * alpha, alpha, 0.0, alpha);
21
+ }
src/vkgs/shaders/gs_splat.vert ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ #extension GL_GOOGLE_include_directive : require
3
+ #include "bindless.glsl"
4
+ // ์ •๋ ฌ๋œ ์ˆœ์„œ๋Œ€๋กœ ์Šคํ”Œ๋žซ๋งˆ๋‹ค ํ™”๋ฉด์ •๋ ฌ ์ฟผ๋“œ ํ•˜๋‚˜ (3ฯƒ ๋ฐ˜์ง€๋ฆ„). ์ธ์Šคํ„ด์Šค = ๊ฐ€์šฐ์‹œ์•ˆ.
5
+
6
+ layout(std430, set = 0, binding = 0) readonly buffer Splats { vec4 splat[]; } bSplats[];
7
+ layout(std430, set = 0, binding = 0) readonly buffer Keys { uvec2 keys[]; } bKeys[];
8
+ #define splat bSplats[kSsboSplats].splat
9
+ #define keys bKeys[kSsboKeys].keys
10
+ layout(push_constant) uniform Viewport { vec2 size; vec2 near_far; } pc;
11
+
12
+ layout(location = 0) flat out vec2 vCenter;
13
+ layout(location = 1) flat out vec3 vConic;
14
+ layout(location = 2) flat out vec4 vColor; // rgb, opacity
15
+ layout(location = 3) flat out float vZ; // ๊ฐ€์šฐ์‹œ์•ˆ ์ค‘์‹ฌ์˜ ์นด๋ฉ”๋ผ z (๊ฑฐ๋ฆฌ ๋ˆ„์  ๋ฒ„ํผ์šฉ)
16
+
17
+ void main() {
18
+ uint idx = keys[gl_InstanceIndex].y;
19
+ vec4 s0 = splat[3 * idx + 0];
20
+ vCenter = s0.xy;
21
+ vConic = splat[3 * idx + 1].xyz;
22
+ vColor = vec4(splat[3 * idx + 2].rgb, s0.w);
23
+
24
+ vec2 corner = vec2(float(gl_VertexIndex & 1) * 2.0 - 1.0, float(gl_VertexIndex >> 1) * 2.0 - 1.0);
25
+ vec2 pix = s0.xy + corner * s0.z;
26
+ // ํ”ฝ์…€ โ†’ Vulkan NDC. y ๊ฐ€ ์•„๋ž˜๋กœ + ๋ผ ๋’ค์ง‘์ง€ ์•Š๋Š”๋‹ค (nusim.data.rig ์ฐธ๊ณ ).
27
+ // ๊นŠ์ด = ๊ฐ€์šฐ์‹œ์•ˆ ์ค‘์‹ฌ ๊นŠ์ด๋ฅผ Vulkan [0,1] ๋กœ (nusim.data.rig projection ๊ณผ ๊ฐ™์€ ์‹).
28
+ // ๋ฉ”์‹œ(PBR ๊ตฌ, glTF)๊ฐ€ ๋จผ์ € ๊นŠ์ด๋ฅผ ์“ฐ๋ฉด ๊ทธ ๋’ค์˜ ์Šคํ”Œ๋žซ์ด ๊นŠ์ด ํ…Œ์ŠคํŠธ๋กœ ๊ฐ€๋ ค์ง„๋‹ค.
29
+ float n = pc.near_far.x, f = pc.near_far.y, z = splat[3 * idx + 2].w;
30
+ vZ = z;
31
+ float zndc = f / (f - n) - f * n / ((f - n) * z);
32
+ gl_Position = vec4(pix / pc.size * 2.0 - 1.0, zndc, 1.0);
33
+ }
src/vkgs/shaders/ibl_common.glsl ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // IBL ๊ณตํ†ต โ€” nusim/render/ibl.py ์™€ ์ง.
2
+ // ํ๋ธŒ๋งต์€ ์”ฌ ์ขŒํ‘œ(z ์œ„) ๋ฐฉํ–ฅ ๋ฒกํ„ฐ๋กœ ๋ฐ”๋กœ ์ƒ˜ํ”Œ๋งํ•œ๋‹ค (๊ตฝ๋Š” ์ชฝ์ด Vulkan ๋ฉด ํ‘œ๋ฅผ ๊ทธ๋Œ€๋กœ ์ผ๋‹ค).
3
+ // ์ƒ‰ ๊ณต๊ฐ„: ํ๋ธŒ๋งต์€ ์„ ํ˜• HDR(๊ทผ์‚ฌ), 3DGS ๋Š” ์นด๋ฉ”๋ผ sRGB(์ •์ค€ ์ƒ‰). ํ™”๋ฉด์€ 3DGS ์ชฝ์— ๋งž์ถ˜๋‹ค.
4
+
5
+ #include "bindless.glsl"
6
+ #define uSpecular gCube[gGlobals[kSsboGlobals].ibl.x] // GGX ํ”„๋ฆฌํ•„ํ„ฐ, mip = ๊ฑฐ์น ๊ธฐ ร— (mips-1)
7
+ #define uIrradiance gCube[gGlobals[kSsboGlobals].ibl.y] // ๋ณต์‚ฌ์กฐ๋„ / ฯ€
8
+ #define uBrdfLut gTex2D[gGlobals[kSsboGlobals].ibl.z] // (NdotV, ๊ฑฐ์น ๊ธฐ) โ†’ (scale, bias)
9
+
10
+ vec3 linear_to_srgb(vec3 c) {
11
+ c = max(c, vec3(0.0));
12
+ return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(vec3(0.0031308), c));
13
+ }
14
+
15
+ // ๊ตฝ๊ธฐ ๋•Œ์˜ ํ•˜์ด๋ผ์ดํŠธ ํ™•์žฅ(nusim/render/ibl.py expand_highlights: knee 0.75, gain 6)์„ ๋˜๋Œ๋ ค ์นด๋ฉ”๋ผ๊ฐ€ ์ฐ์—ˆ์„
16
+ // ๋ฐ๊ธฐ๋กœ โ€” ๋ฐฐ๊ฒฝ ํ‘œ์‹œ์šฉ. ์ •๋ฐฉํ–ฅ L = lยท(1 + 6xยฒ), x = clamp((l โˆ’ 0.75)/0.25, 0, 1) ์€ l ์— ๋Œ€ํ•ด ๋‹จ์กฐ ์ฆ๊ฐ€๋ผ
17
+ // ๊ตฌ๊ฐ„ [0.75, 1] ์—์„œ ์ด๋ถ„๋ฒ•์œผ๋กœ ์ •ํ™•ํžˆ ํ‘ผ๋‹ค (l โ‰ฅ 1 ์ด๋ฉด L = 7l).
18
+ // ์˜ˆ์ „ ๊ณ ์ •์  ๋ฐ˜๋ณต l โ† L/(1+6x(l)ยฒ) ์€ L ๊ณผ L/7 ์‚ฌ์ด๋ฅผ ์˜ค๊ฐ€๋ฉฐ ๋ฐœ์‚ฐํ–ˆ๊ณ , ์ง์ˆ˜ ๋ฒˆ(4ํšŒ) ๋Œ์•„ ์ž…๋ ฅ์„ ๊ทธ๋Œ€๋กœ
19
+ // ๋Œ๋ ค์คฌ๋‹ค โ€” ์›๋ž˜ ํœ˜๋„ 0.8~1 ์ธ ํ•˜๋Š˜ยท๋ฐ˜์‚ฌ๊ฐ€ ์ตœ๋Œ€ 7๋ฐฐ ๋ฐ๊ฒŒ ํ•˜์–—๊ฒŒ ๋‚ ์•„๊ฐ”๋‹ค (2026-10-02 ๋ฆฌ๋ทฐ).
20
+ vec3 compress_highlights(vec3 lin) {
21
+ float L = dot(lin, vec3(0.2126, 0.7152, 0.0722));
22
+ if (L <= 0.75) return lin;
23
+ float Li;
24
+ if (L >= 7.0) {
25
+ Li = L / 7.0;
26
+ } else {
27
+ float lo = 0.75, hi = 1.0;
28
+ for (int k = 0; k < 14; ++k) {
29
+ float m = 0.5 * (lo + hi);
30
+ float x = (m - 0.75) / 0.25;
31
+ if (m * (1.0 + 6.0 * x * x) < L) lo = m; else hi = m;
32
+ }
33
+ Li = 0.5 * (lo + hi);
34
+ }
35
+ return lin * (Li / L);
36
+ }
src/vkgs/shaders/line.frag ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ layout(location = 0) in vec4 vColor;
3
+ layout(location = 0) out vec4 outColor;
4
+
5
+ void main() {
6
+ outColor = vec4(vColor.rgb * vColor.a, vColor.a); // premultiplied
7
+ }
src/vkgs/shaders/line.vert ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ #extension GL_GOOGLE_include_directive : require
3
+ // ๋งต ์˜ค๋ฒ„๋ ˆ์ด(์ฐจ์„ , ์ฃผํ–‰๊ฐ€๋Šฅ์˜์—ญ ๊ฒฝ๊ณ„, ๊ถค์ ). 3DGS ์œ„์— ๊ฒน์ณ ๊ทธ๋ฆฐ๋‹ค (๊นŠ์ด ํ…Œ์ŠคํŠธ ์—†์Œ).
4
+ // ํˆฌ์˜์€ nusim.data.rig.CameraPose.projection ๊ณผ ๊ฐ™์€ Vulkan clip โ€” ๊ทผํ‰๋ฉด ํด๋ฆฌํ•‘์€ ํ•˜๋“œ์›จ์–ด๊ฐ€ ํ•œ๋‹ค.
5
+
6
+ #include "bindless.glsl"
7
+ // ์ •์ ์€ bindless SSBO ์—์„œ ์ง์ ‘ ์ฝ๋Š”๋‹ค (vertex pulling): LineVertex = ์œ„์น˜ xyz(์”ฌ ์›์  ๊ธฐ์ค€) + ์ƒ‰ rgba = float 7๊ฐœ
8
+ layout(std430, set = 0, binding = 0) readonly buffer LineVerts { float lv[]; } gLineVerts[];
9
+ layout(push_constant) uniform PC { mat4 viewProj; ivec4 idx; } pc; // idx.x = ์ •์  SSBO ์Šฌ๋กฏ
10
+ layout(location = 0) out vec4 vColor;
11
+
12
+ void main() {
13
+ uint b = uint(gl_VertexIndex) * 7u, s = uint(pc.idx.x);
14
+ vec3 inPos = vec3(gLineVerts[s].lv[b], gLineVerts[s].lv[b + 1u], gLineVerts[s].lv[b + 2u]);
15
+ vColor = vec4(gLineVerts[s].lv[b + 3u], gLineVerts[s].lv[b + 4u], gLineVerts[s].lv[b + 5u], gLineVerts[s].lv[b + 6u]);
16
+ gl_Position = pc.viewProj * vec4(inPos, 1.0);
17
+ }
src/vkgs/shaders/nodes/isp.glsl ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ์…ฐ์ด๋” ๊ทธ๋ž˜ํ”„ ๋…ธ๋“œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ โ€” ์นด๋ฉ”๋ผ ISP (scripts/shader_graph.py ๊ฐ€ ์ฝ๋Š”๋‹ค).
2
+ //
3
+ // ํ•จ์ˆ˜ ํ•˜๋‚˜ = ๋…ธ๋“œ ํ•˜๋‚˜. ์ด๋ฆ„์ด n_<op> ์ธ ํ•จ์ˆ˜๋งŒ ๋…ธ๋“œ๋‹ค. ์‹œ๊ทธ๋‹ˆ์ฒ˜๊ฐ€ ๊ณง ๋…ธ๋“œ ์ •์˜๋‹ค:
4
+ // ๋ฐ˜ํ™˜ํ˜• n_<op>(์ž…๋ ฅ..., Ctx x, ๋งค๊ฐœ๋ณ€์ˆ˜...)
5
+ // - Ctx ์•ž์˜ ์ธ์ž = ์ž…๋ ฅ ํฌํŠธ (๋‹ค๋ฅธ ๋…ธ๋“œ์˜ ์ถœ๋ ฅ๊ณผ ํƒ€์ž…์ด ๋งž์•„์•ผ ํ•œ๋‹ค)
6
+ // - Ctx ๋’ค์˜ float ์ธ์ž = ๋งค๊ฐœ๋ณ€์ˆ˜ (์‹คํ–‰ ์ค‘ ์Šฌ๋ผ์ด๋”๋กœ ๋ฐ”๊พผ๋‹ค โ€” ๋งค๊ฐœ๋ณ€์ˆ˜ ๋ฒ„ํผ์—์„œ ์ฝ๋Š”๋‹ค)
7
+ // - Ctx ๊ฐ€ ์—†์œผ๋ฉด ์ž…๋ ฅ๋งŒ ๋ฐ›๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜
8
+ // ๋งค๊ฐœ๋ณ€์ˆ˜์˜ ๊ธฐ๋ณธ๊ฐ’ยท๋ฒ”์œ„๋Š” ๋ฐ”๋กœ ์œ„ ์ฃผ์„ '// @param ์ด๋ฆ„ ๊ธฐ๋ณธ๊ฐ’ ์ตœ์†Ÿ๊ฐ’ ์ตœ๋Œ“๊ฐ’' ์œผ๋กœ ์ค€๋‹ค (์—†์œผ๋ฉด 0, 0~1).
9
+ // ๋…ธ๋“œ๋Š” ํ”ฝ์…€ ํ•˜๋‚˜๋งŒ ๋ณธ๋‹ค (์ด์›ƒ ํ”ฝ์…€์„ ์ฝ์ง€ ์•Š๋Š”๋‹ค) โ€” ๊ทธ๋ž˜์„œ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ™์€ ์ด๋ฏธ์ง€์— ๋ฎ์–ด์จ๋„ ์•ˆ์ „ํ•˜๋‹ค.
10
+
11
+ struct Ctx {
12
+ ivec2 px; // ํ”ฝ์…€
13
+ vec2 uv; // 0~1
14
+ vec2 size; // ํญ, ๋†’์ด
15
+ float time; // ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ๊ฐ s
16
+ uint frame; // ํ”„๋ ˆ์ž„ ๋ฒˆํ˜ธ (๋…ธ์ด์ฆˆ ์”จ์•—)
17
+ };
18
+
19
+ // sRGB ๋ถ€ํ˜ธํ™” โ†” ์„ ํ˜•. ๋ Œ๋” ๊ฒฐ๊ณผ(post)๋Š” sRGB ๋กœ ๋ถ€ํ˜ธํ™”๋ผ ์žˆ๋‹ค (ํ•™์Šต ์ด๋ฏธ์ง€๊ฐ€ JPEG).
20
+ vec3 n_to_linear(vec3 c) { return mix(c / 12.92, pow((max(c, 0.0) + 0.055) / 1.055, vec3(2.4)), step(vec3(0.04045), c)); }
21
+ vec3 n_to_srgb(vec3 c) {
22
+ c = max(c, vec3(0.0));
23
+ return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(vec3(0.0031308), c));
24
+ }
25
+
26
+ // ๋…ธ์ถœ (EV, ์„ ํ˜• ์ƒ‰์—)
27
+ // @param ev 0 -3 3
28
+ vec3 n_exposure(vec3 c, Ctx x, float ev) { return c * exp2(ev); }
29
+
30
+ // ํ™”์ดํŠธ๋ฐธ๋Ÿฐ์Šค โ€” ์ƒ‰์˜จ๋„(+ ๋”ฐ๋œปํ•˜๊ฒŒ / - ์ฐจ๊ฐ‘๊ฒŒ)ยทํ‹ดํŠธ(+ ๋งˆ์  ํƒ€ / - ์ดˆ๋ก) ์ด๋“ (์„ ํ˜• ์ƒ‰์—)
31
+ // @param temperature 0 -1 1
32
+ // @param tint 0 -1 1
33
+ vec3 n_white_balance(vec3 c, Ctx x, float temperature, float tint) {
34
+ vec3 g = vec3(1.0 + 0.3 * temperature, 1.0 - 0.2 * tint, 1.0 - 0.3 * temperature);
35
+ return c * g / dot(g, vec3(0.2126, 0.7152, 0.0722)); // ๋ฐ๊ธฐ๋Š” ์œ ์ง€
36
+ }
37
+
38
+ // ์ฑ„๋„ (1 = ๊ทธ๋Œ€๋กœ, 0 = ํ‘๋ฐฑ)
39
+ // @param saturation 1 0 2
40
+ vec3 n_saturation(vec3 c, Ctx x, float saturation) {
41
+ float y = dot(c, vec3(0.2126, 0.7152, 0.0722));
42
+ return max(mix(vec3(y), c, saturation), 0.0);
43
+ }
44
+
45
+ // ๋Œ€๋น„ (์ค‘๊ฐ„ ํšŒ์ƒ‰ ๊ธฐ์ค€, sRGB ๋ถ€ํ˜ธํ™” ์ƒ‰์—)
46
+ // @param contrast 1 0.5 1.5
47
+ vec3 n_contrast(vec3 c, Ctx x, float contrast) { return max((c - 0.5) * contrast + 0.5, 0.0); }
48
+
49
+ // ๋น„๋„คํŒ… โ€” ๋ Œ์ฆˆ ์ฃผ๋ณ€ ๊ฐ๊ด‘ cos^4 (์„ ํ˜• ์ƒ‰์—). ์‹œ์•ผ๊ฐ์€ ํ™”๋ฉด ๋Œ€๊ฐ ๊ธฐ์ค€ ๊ทผ์‚ฌ
50
+ // @param vignette 0 0 1
51
+ vec3 n_vignette(vec3 c, Ctx x, float vignette) {
52
+ vec2 d = (x.uv - 0.5) * vec2(x.size.x / x.size.y, 1.0);
53
+ float cos_t = inversesqrt(1.0 + dot(d, d) * 1.2);
54
+ return c * mix(1.0, pow(cos_t, 4.0), vignette);
55
+ }
56
+
57
+ // ์„ผ์„œ ๋…ธ์ด์ฆˆ โ€” ์ƒท ๋…ธ์ด์ฆˆ(๋ฐ๊ธฐ์— ๋น„๋ก€) + ์ฝ๊ธฐ ๋…ธ์ด์ฆˆ (์„ ํ˜• ์ƒ‰์—). ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๋‹ค๋ฅด๋‹ค
58
+ // @param shot_noise 0 0 0.05
59
+ // @param read_noise 0 0 0.02
60
+ vec3 n_sensor_noise(vec3 c, Ctx x, float shot_noise, float read_noise) {
61
+ uvec3 s = uvec3(uvec2(x.px), x.frame);
62
+ // PCG ํ•ด์‹œ 3๊ฐœ โ†’ ๊ทผ์‚ฌ ์ •๊ทœ๋ถ„ํฌ (๊ท ๋“ฑ ๋‘˜์˜ ํ•ฉ)
63
+ vec3 n = vec3(0.0);
64
+ for (int k = 0; k < 2; ++k) {
65
+ s = s * 1664525u + 1013904223u;
66
+ s.x += s.y * s.z; s.y += s.z * s.x; s.z += s.x * s.y;
67
+ s ^= s >> 16u;
68
+ s.x += s.y * s.z; s.y += s.z * s.x; s.z += s.x * s.y;
69
+ n += vec3(s) * (1.0 / 4294967296.0) - 0.5;
70
+ }
71
+ n *= 2.449; // ๋ถ„์‚ฐ 1 ๋กœ
72
+ return max(c + n * (shot_noise * sqrt(max(c, 0.0)) + read_noise), 0.0);
73
+ }
74
+
75
+ // ๋‘ ๊ฐˆ๋ž˜ ์„ž๊ธฐ
76
+ // @param amount 0.5 0 1
77
+ vec3 n_mix(vec3 a, vec3 b, Ctx x, float amount) { return mix(a, b, amount); }
78
+
79
+ // ๋ฐ๊ธฐ (float ์ถœ๋ ฅ โ€” ํƒ€์ž… ๊ฒ€์‚ฌ ์˜ˆ์‹œยท๋งˆ์Šคํฌ์šฉ)
80
+ float n_luma(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }
81
+ // ํšŒ์ƒ‰์œผ๋กœ (float โ†’ vec3)
82
+ vec3 n_gray(float y) { return vec3(y); }
src/vkgs/shaders/pbr.frag ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ #extension GL_GOOGLE_include_directive : require
3
+ // split-sum IBL (Karis 2013): ํ™•์‚ฐ = ๋ณต์‚ฌ์กฐ๋„ยท์•Œ๋ฒ ๋„, ์ •๋ฐ˜์‚ฌ = ํ”„๋ฆฌํ•„ํ„ฐ(R, ๊ฑฐ์น ๊ธฐ)ยท(F0ยทA + B)
4
+ #include "ibl_common.glsl"
5
+ #include "depth_common.glsl"
6
+ layout(location = 0) in vec3 vWorld;
7
+ layout(location = 1) in vec3 vNormal;
8
+ layout(location = 0) out vec4 outColor;
9
+ layout(location = 1) out vec4 outDepth; // ๊ฑฐ๋ฆฌ ๋ˆ„์  (depth_common.glsl)
10
+ layout(push_constant) uniform PC {
11
+ mat4 viewProj;
12
+ vec4 center_radius;
13
+ vec4 albedo_rough;
14
+ vec4 metal;
15
+ vec4 campos;
16
+ ivec4 idx;
17
+ } pc;
18
+
19
+ vec3 fresnel_schlick_roughness(float cosT, vec3 F0, float r) {
20
+ return F0 + (max(vec3(1.0 - r), F0) - F0) * pow(1.0 - cosT, 5.0);
21
+ }
22
+
23
+ void main() {
24
+ vec3 N = normalize(vNormal);
25
+ vec3 V = normalize(pc.campos.xyz - vWorld);
26
+ vec3 R = reflect(-V, N);
27
+ float NdotV = max(dot(N, V), 1e-4);
28
+ vec3 albedo = pc.albedo_rough.rgb;
29
+ float rough = clamp(pc.albedo_rough.a, 0.0, 1.0);
30
+ float metallic = pc.metal.x;
31
+ vec3 F0 = mix(vec3(0.04), albedo, metallic);
32
+
33
+ float mips = float(textureQueryLevels(uSpecular));
34
+ vec3 pref = textureLod(uSpecular, R, rough * (mips - 1.0)).rgb;
35
+ vec2 ab = texture(uBrdfLut, vec2(NdotV, rough)).rg;
36
+ vec3 spec = pref * (F0 * ab.x + ab.y);
37
+
38
+ vec3 kS = fresnel_schlick_roughness(NdotV, F0, rough);
39
+ vec3 kD = (1.0 - kS) * (1.0 - metallic);
40
+ vec3 diffuse = texture(uIrradiance, N).rgb * albedo;
41
+ vec3 lin = kD * diffuse + spec;
42
+ // 3DGS ์™€ ๊ฐ™์€ ์นด๋ฉ”๋ผ sRGB ๊ณต๊ฐ„์œผ๋กœ. ํ•˜์ด๋ผ์ดํŠธ ํ™•์žฅ๋„ ๋˜๋Œ๋ ค ์žฅ๋ฉด ๋ฐ๊ธฐ์™€ ๋งž์ถ˜๋‹ค.
43
+ outColor = vec4(linear_to_srgb(compress_highlights(lin)), 1.0);
44
+ outDepth = vec4(linear_depth(gl_FragCoord.z), 1.0, 0.0, 1.0);
45
+ }
src/vkgs/shaders/pbr.vert ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ #extension GL_GOOGLE_include_directive : require
3
+ #include "bindless.glsl"
4
+ // IBL ๊ฒ€์ฆ์šฉ PBR ๋ฉ”์‹œ (๊ตฌ). glTF ์ฐจ๋Ÿ‰๋„ ๊ฐ™์€ ์…ฐ์ด๋” ๊ฒฝ๋กœ๋กœ ๋“ค์–ด์˜ฌ ๊ฒƒ์ด๋‹ค.
5
+ // ์ •์ ์€ bindless SSBO ์—์„œ (vertex pulling): ์œ„์น˜ xyz + ๋ฒ•์„  xyz = float 6๊ฐœ
6
+ layout(std430, set = 0, binding = 0) readonly buffer SphereVerts { float sv[]; } gSphereVerts[];
7
+ layout(push_constant) uniform PC {
8
+ mat4 viewProj;
9
+ vec4 center_radius; // ๊ตฌ ์ค‘์‹ฌ(์”ฌ ์ขŒํ‘œ), ๋ฐ˜์ง€๋ฆ„
10
+ vec4 albedo_rough; // rgb ์•Œ๋ฒ ๋„(์„ ํ˜•), ๊ฑฐ์น ๊ธฐ
11
+ vec4 metal; // x ๊ธˆ์†์„ฑ
12
+ vec4 campos; // ์นด๋ฉ”๋ผ ์œ„์น˜
13
+ ivec4 idx; // x = ์ •์  SSBO ์Šฌ๋กฏ
14
+ } pc;
15
+ layout(location = 0) out vec3 vWorld;
16
+ layout(location = 1) out vec3 vNormal;
17
+ void main() {
18
+ uint b = uint(gl_VertexIndex) * 6u, s = uint(pc.idx.x);
19
+ vec3 inPos = vec3(gSphereVerts[s].sv[b], gSphereVerts[s].sv[b + 1u], gSphereVerts[s].sv[b + 2u]);
20
+ vec3 inNormal = vec3(gSphereVerts[s].sv[b + 3u], gSphereVerts[s].sv[b + 4u], gSphereVerts[s].sv[b + 5u]);
21
+ vec3 w = pc.center_radius.xyz + inPos * pc.center_radius.w;
22
+ vWorld = w;
23
+ vNormal = inNormal;
24
+ gl_Position = pc.viewProj * vec4(w, 1.0);
25
+ }
src/vkgs/shaders/pbr_gltf.frag ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 450
2
+ #extension GL_GOOGLE_include_directive : require
3
+ // glTF metallic-roughness + clearcoat + transmission(๊ทผ์‚ฌ), split-sum IBL.
4
+ #include "ibl_common.glsl"
5
+ #include "depth_common.glsl"
6
+
7
+ layout(location = 0) in vec3 vWorld;
8
+ layout(location = 1) in vec3 vNormal;
9
+ layout(location = 2) in vec2 vUV0;
10
+ layout(location = 3) in vec2 vUV1;
11
+ layout(location = 4) in vec4 vTangent;
12
+ layout(location = 0) out vec4 outColor;
13
+ layout(location = 1) out vec4 outDepth; // ๊ฑฐ๋ฆฌ ๋ˆ„์  (depth_common.glsl)
14
+
15
+ layout(push_constant) uniform PC {
16
+ mat4 model;
17
+ mat4 viewProj;
18
+ vec4 campos;
19
+ ivec4 idx; // x = ์žฌ์งˆ SSBO ์Šฌ๋กฏ, y = ์žฌ์งˆ ๋ฒˆํ˜ธ
20
+ } pc;
21
+
22
+ struct Material { // std430 โ€” viewer.cpp add_gltf ์˜ MatGPU ์™€ ๊ฐ™์€ ์ˆœ์„œ (304 B)
23
+ vec4 base; // ๊ธฐ๋ณธ์ƒ‰ ๊ณ„์ˆ˜ (์„ ํ˜•)
24
+ vec4 emissive; // rgb ร— emissive_strength
25
+ vec4 mrno; // metallic, roughness, normal_scale, occlusion_strength
26
+ vec4 alpha; // cutoff, mode(0 ๋ถˆํˆฌ๋ช… 1 ๋งˆ์Šคํฌ 2 ๋ธ”๋ Œ๋“œ), transmission, has_normal_tex
27
+ vec4 cc; // clearcoat, clearcoat_roughness, -, -
28
+ ivec4 tc0; // texcoord ๋ฒˆํ˜ธ: base, metal-rough, normal, occlusion
29
+ ivec4 tc1; // emissive, -, -, -
30
+ vec4 xf[10]; // KHR_texture_transform: ์Šฌ๋กฏ๋งˆ๋‹ค 2ํ–‰ (a b c), (d e f)
31
+ ivec4 tex0; // 2D ํ…์Šค์ฒ˜ ๋ฒˆํ˜ธ: base, metal-rough, normal, occlusion (์—†์œผ๋ฉด ํฐ์ƒ‰)
32
+ ivec4 tex1; // emissive, -, -, -
33
+ };
34
+ layout(std430, set = 0, binding = 0) readonly buffer Materials { Material m[]; } gMaterials[];
35
+ Material mat;
36
+ #define tBase gTex2D[mat.tex0.x]
37
+ #define tMetalRough gTex2D[mat.tex0.y]
38
+ #define tNormal gTex2D[mat.tex0.z]
39
+ #define tOcclusion gTex2D[mat.tex0.w]
40
+ #define tEmissive gTex2D[mat.tex1.x]
41
+
42
+ vec2 uv_of(int slot) {
43
+ int tc = slot < 4 ? mat.tc0[slot] : mat.tc1.x;
44
+ vec3 uv = vec3(tc == 0 ? vUV0 : vUV1, 1.0);
45
+ return vec2(dot(mat.xf[2 * slot].xyz, uv), dot(mat.xf[2 * slot + 1].xyz, uv));
46
+ }
47
+
48
+ vec3 fresnel_schlick_roughness(float cosT, vec3 F0, float r) {
49
+ return F0 + (max(vec3(1.0 - r), F0) - F0) * pow(1.0 - cosT, 5.0);
50
+ }
51
+
52
+ vec3 ibl_specular(vec3 R, float rough, vec3 F0, float NdotV) {
53
+ float mips = float(textureQueryLevels(uSpecular));
54
+ vec3 pref = textureLod(uSpecular, R, rough * (mips - 1.0)).rgb;
55
+ vec2 ab = texture(uBrdfLut, vec2(NdotV, rough)).rg;
56
+ return pref * (F0 * ab.x + ab.y);
57
+ }
58
+
59
+ void main() {
60
+ mat = gMaterials[pc.idx.x].m[pc.idx.y];
61
+ vec4 base = mat.base * texture(tBase, uv_of(0)); // sRGB ๋ทฐ๋ผ ์ƒ˜ํ”Œ๋ง์ด ์„ ํ˜•์œผ๋กœ ๋Œ๋ ค์ค€๋‹ค
62
+ if (mat.alpha.y == 1.0 && base.a < mat.alpha.x) discard;
63
+
64
+ vec4 mr = texture(tMetalRough, uv_of(1));
65
+ float metallic = clamp(mat.mrno.x * mr.b, 0.0, 1.0);
66
+ float rough = clamp(mat.mrno.y * mr.g, 0.03, 1.0);
67
+ float occ = 1.0 + mat.mrno.w * (texture(tOcclusion, uv_of(3)).r - 1.0);
68
+ vec3 emissive = mat.emissive.rgb * texture(tEmissive, uv_of(4)).rgb;
69
+
70
+ vec3 N = normalize(vNormal);
71
+ if (!gl_FrontFacing) N = -N; // ์–‘๋ฉด ์žฌ์งˆ (์ฐจ ๋ฉ”์‹œ๋Š” ์ปฌ๋ง ์—†์ด ๊ทธ๋ฆฐ๋‹ค)
72
+ vec3 V = normalize(pc.campos.xyz - vWorld);
73
+ if (mat.alpha.w > 0.5) {
74
+ vec3 T, B;
75
+ if (abs(vTangent.w) > 0.5) {
76
+ T = normalize(vTangent.xyz - N * dot(N, vTangent.xyz));
77
+ B = cross(N, T) * vTangent.w;
78
+ } else { // ํƒ„์  ํŠธ๊ฐ€ ์—†์œผ๋ฉด ํ™”๋ฉด ๋ฏธ๋ถ„์œผ๋กœ (cotangent frame)
79
+ vec3 dp1 = dFdx(vWorld), dp2 = dFdy(vWorld);
80
+ vec2 uv = uv_of(2);
81
+ vec2 du1 = dFdx(uv), du2 = dFdy(uv);
82
+ vec3 dp2perp = cross(dp2, N), dp1perp = cross(N, dp1);
83
+ T = dp2perp * du1.x + dp1perp * du2.x;
84
+ B = dp2perp * du1.y + dp1perp * du2.y;
85
+ float inv = inversesqrt(max(dot(T, T), dot(B, B)) + 1e-12);
86
+ T *= inv;
87
+ B *= inv;
88
+ }
89
+ vec3 nt = texture(tNormal, uv_of(2)).xyz * 2.0 - 1.0;
90
+ nt.xy *= mat.mrno.z;
91
+ N = normalize(mat3(T, B, N) * nt);
92
+ }
93
+
94
+ float NdotV = max(dot(N, V), 1e-4);
95
+ vec3 R = reflect(-V, N);
96
+ vec3 F0 = mix(vec3(0.04), base.rgb, metallic);
97
+ vec3 spec = ibl_specular(R, rough, F0, NdotV);
98
+ vec3 kS = fresnel_schlick_roughness(NdotV, F0, rough);
99
+ vec3 kD = (1.0 - kS) * (1.0 - metallic);
100
+ vec3 diffuse = texture(uIrradiance, N).rgb * base.rgb;
101
+
102
+ // ํˆฌ๊ณผ(์œ ๋ฆฌ) ๊ทผ์‚ฌ: ๋’ค์ชฝ ์žฅ๋ฉด ๋Œ€์‹  ํ™˜๊ฒฝ๋งต์„ ์‹œ์„  ๋ฐฉํ–ฅ์œผ๋กœ ๋น„์ถ˜๋‹ค + ๋ฐ˜ํˆฌ๋ช… ๋ธ”๋ Œ๋”ฉ
103
+ float tr = mat.alpha.z;
104
+ vec3 body = kD * diffuse * (1.0 - tr);
105
+ if (tr > 0.0) {
106
+ float mips = float(textureQueryLevels(uSpecular));
107
+ body += tr * (1.0 - kS) * textureLod(uSpecular, -V, rough * (mips - 1.0)).rgb * base.rgb;
108
+ }
109
+ vec3 lin = (body + spec) * occ + emissive;
110
+
111
+ // ํด๋ฆฌ์–ด์ฝ”ํŠธ: ๊ฑฐ์น ๊ธฐ ๋‚ฎ์€ ์œ ์ „์ฒด(F0 0.04) ์ธต์ด ์œ„์— ํ•œ ๊ฒน ๋” (์ฐจ ๋„์žฅ)
112
+ float cc = mat.cc.x;
113
+ if (cc > 0.0) {
114
+ float ccr = clamp(mat.cc.y, 0.03, 1.0);
115
+ vec3 Fc = fresnel_schlick_roughness(NdotV, vec3(0.04), ccr);
116
+ lin = lin * (1.0 - cc * Fc) + cc * ibl_specular(reflect(-V, normalize(vNormal) * (gl_FrontFacing ? 1.0 : -1.0)),
117
+ ccr, vec3(0.04), NdotV);
118
+ }
119
+
120
+ lin *= pc.campos.w;
121
+ vec3 c = linear_to_srgb(compress_highlights(lin));
122
+ float a = 1.0;
123
+ if (mat.alpha.y == 2.0) a = base.a;
124
+ if (tr > 0.0) a = mix(1.0, 0.35, tr) + 0.5 * kS.g * tr; // ์œ ๋ฆฌ: ์ •๋ฉด์€ ๋น„์น˜๊ณ  ๋น„์Šค๋“ฌํ•˜๋ฉด ๋ฐ˜์‚ฌ
125
+ a = clamp(a, 0.0, 1.0);
126
+ outColor = vec4(c * a, a); // premultiplied
127
+ outDepth = vec4(linear_depth(gl_FragCoord.z) * a, a, 0.0, a);
128
+ }