# SUM Parts 검토 노트 > 대상: [tudelft3d/SUM-Parts-Benchmarks](https://github.com/tudelft3d/SUM-Parts-Benchmarks) > 목적: 로컬 재현 테스트 + 한국 샘플 데이터 적용 가능성 확인 > 작성일: 2026-08-20 --- ## 1. 프로젝트 개요 **SUM Parts** — TU Delft 3D geoinformation, CVPR 2025. 도시 **텍스처 3D 메시**의 **part-level(부품 단위) 시맨틱 세그멘테이션** 벤치마크. | 항목 | 내용 | |---|---| | 규모 | 2.5 km² 도시 메시 | | 어노테이션 | 이중 트랙 — face 단위 + texture 픽셀 단위 | | 클래스 | 21종 | | 텍스처 메시 포맷 | ASCII PLY | | 시맨틱 포인트클라우드 포맷 | binary PLY | | 데이터 배포 | Hugging Face `gwxgrxhyz/SUM-Parts` (**게이트 있음**, 아래 참조) | | 논문 | [arXiv:2503.15300](https://arxiv.org/abs/2503.15300) | | 프로젝트 페이지 | https://tudelft3d.github.io/SUMParts/ | | 연락처 | gaoweixiaocuhk@gmail.com (Weixiao Gao) | ### 클래스 — README는 21이라 하지만, 코드상 트랙별로 다르다 README 전체 목록 (21종): ``` unclassified, terrain, high vegetation, water, car, boat, wall, roof surface, facade surface, chimney, dormer, balcony, roof installation, window, door, low vegetation, impervious surface, road, road marking, cycle lane, sidewalk ``` **실제 학습 코드에서는 트랙별로 분리된다** (PointNeXt `openpoints/dataset/` 확인 결과): | 트랙 | 클래스 수 | 목록 | |---|---|---| | `SUMV2_Triangle` (face/메시) | **13** | unclassified, terrain, high_vegetation, facade_surface, water, car, boat, roof_surface, chimney, dormer, balcony, roof_installation, wall | | `SUMV2_Texture` (텍스처 픽셀) | **20** | 위 + window, door, low_vegetation, road, road_marking 등 세부 클래스 | → 벤치마크 재현 시 **cfg의 `num_classes`와 데이터 트랙을 반드시 일치**시켜야 한다. `ignore_index: 0` (unclassified는 손실 계산에서 제외). ### PLY 필드 규약 | 필드 | 의미 | |---|---| | `f:color` | face 단위 색상 | | `v:color` | vertex(point) 단위 색상 | | `f:label` / `v:label` | 시맨틱 라벨 | | `h:texcoord` | 텍스처 좌표 (halfedge) | ### 리포지토리 구조 - `semantic_segmentation/` — 딥러닝 베이스라인 - KPConv (`train_UrbanMesh.py`, `UrbanMesh.py`) - PointNeXt_bundle (PointNet, PointNet++, PointNeXt, PointVector) - Open3D_ML (SparseConvUNet, RandLA-Net) - SPG - RF_MRF / SUM_RF / PSSNet → 별도 PSSNet repo의 `sumv2` 브랜치 - `interactive_annotation/` — SAM, SimpleClick 기반 텍스처 어노테이션 도구 - `assets/` — 문서 이미지, 설정 파일 ### 데이터 접근 — 게이트 걸려 있음 ⚠️ HF 데이터셋이 `gated: auto`. 토큰만으로는 안 되고 **계정별 약관 수락이 1회 필요**하다. 수락 전에는 모든 다운로드가 `HTTP 403`. **수동 절차 (브라우저 필요, 자동화 불가)** 1. https://huggingface.co/datasets/gwxgrxhyz/SUM-Parts 접속 2. 로그인 후 게이트 폼에서 CC BY-NC 4.0 수락 3. 이후 캐시된 토큰으로 자동 다운로드 가능 **배포 파일** | 파일 | 내용 | |---|---| | `demo.zip` | 소량 샘플 — 스모크 테스트용. **여기부터 시작** | | `mesh.zip` | 텍스처 메시 (ASCII PLY) | | `pcl.zip` | 시맨틱 포인트클라우드 (binary PLY) | ### 데이터 레이아웃 (PointNeXt 기준) `cfgs/sumv2_triangle/default.yaml` → `data_root: ../../data/sumv2_tri_texpcl/` ⚠️ **이 상대경로는 `main.py`의 작업 디렉토리 기준**이다. `main.py`는 `examples/segmentation/`에서 실행되므로 `../../` = **`PointNeXt_bundle/`**이지 repo 루트가 아니다. 착각하면 `Totally 0 samples in train set`으로 조용히 실패한다. ``` PointNeXt_bundle/data/sumv2_tri_texpcl/ ├── train/*.ply ├── val/*.ply ├── test/*.ply └── processed/ # presample 캐시, 자동 생성 ``` 여기서는 데이터를 `/data`에 두고 심볼릭 링크로 연결했다 ([scripts/link_data.sh](../scripts/link_data.sh)). **demo.zip 실제 구조** (train/val/test 아님 — 타일 1장짜리 쇼케이스): ``` data/ ├── mesh/ │ ├── textured_mesh/ demo.ply + demo.jpg │ ├── face_label/ demo_face_label.ply + .jpg │ ├── pixel_label/ demo_pixel_label.ply + .png │ └── full_pixel_label/ demo_full_pixel_label.ply + .png └── pcl/ ├── face_labeling_pcl/ demo_{texsp,fdcen,poisson,rand}_pcl.ply └── texture_labeling_pcl/ demo_{texsp,poisson,rand}_pcl.ply ``` cfg 디렉토리명 `sumv2_tri_texpcl` = **triangle 트랙 + texsp 샘플러** → `pcl/face_labeling_pcl/demo_texsp_pcl.ply` **PLY vertex 속성** (실측 + `read_ply_with_plyfilelib` 확인): | 속성 | 실제 배포본 | 필수 | 비고 | |---|---|---|---| | `x`, `y`, `z` | ✅ | ✅ | float32 | | `nx`, `ny`, `nz` | ✅ | ✗ | 로더가 무시 | | `r`, `g`, `b` | ✅ | ✅ | **`red,green,blue` 아님.** 로더는 둘 다 받는다 | | `label` | ✅ | ✅ | int32 | | `sp_id` | texsp 파일만 | ✗ | 슈퍼픽셀 ID | | `object_index` | ✗ | ✗ | 로더는 지원하나 demo엔 없음 | **실측 라벨 분포** | 트랙 | 라벨 값 | 클래스 수 | |---|---|---| | `face_labeling_pcl` | `{0..10, 12}` | 13 ✅ `SUMV2_Triangle` 일치 | | `texture_labeling_pcl` | `{0..9, 11..19}` | 20 ✅ `SUMV2_Texture` 일치 | **타일 규모**: 252 × 252 × 40 m, 47만 포인트 (fdcen만 21만). 좌표 원점 `(7749, 5499, -1.6)` — **저자도 전체 CRS 좌표가 아닌 로컬 미터계로 배포**한다. → **한국 데이터 변환 시 이 스키마만 맞추면 된다.** 텍스처 불필요. ### 주요 학습 하이퍼파라미터 (sumv2_triangle 기본값) | 항목 | 값 | |---|---| | `voxel_size` | 0.02 | | `voxel_max` (train) | 64000 | | `loop` | 30 (학습셋 30회 반복 → epoch 수는 적어도 됨) | | `epochs` | 100 | | `batch_size` | 2 (PointNeXt-XL 기준. PointNet: 6, PointNet++: 10) | | `ignore_index` | 0 (unclassified 제외) | | optimizer | adamw, lr 0.01, cosine | | `cls_weighed_loss` | True (클래스 불균형 보정) | 제공 모델 cfg: `pointnet.yaml`, `pointnet++msg.yaml`, `pointnext-xl.yaml`, `pointvector-xl.yaml` ### 평가 현재는 각 데이터에 내장된 GT 라벨로 자체 평가. 논문과 동일한 fine-grained 테스트셋 평가는 **예측 결과를 저자 이메일로 보내면 로컬에서 채점**해 줌. 자동 평가 코드는 HF에 추가 예정. ### 시각화 - **Mapple** (권장) — `f:color`, `v:color`, 라벨 범례 표시 가능 - **MeshLab** — face color/texture는 보이나 스칼라(라벨)는 처리 못 함 --- ## 2. 라이선스 검토 **두 개가 따로 논다. 분리해서 봐야 한다.** | 대상 | 라이선스 | 배포 조건 | |---|---|---| | 코드 (GitHub repo) | **GPL-3.0** | 파생 코드 배포 시 소스 공개 + GPL-3.0 승계. 비공개 상용 제품에 혼합 불가 | | 데이터셋 (HF SUM-Parts) | **CC BY-NC 4.0** | **비상업 한정**. 출처 표기 필수. 재배포/개변 허용 | ### 결론 - **로컬 실험/학습 목적** → 제약 없음. 그냥 쓰면 된다. - **논문 / 오픈소스 릴리즈** → 가능. 단 코드는 GPL-3.0으로 내고, SUM Parts(2025) + SUM(2021) 둘 다 인용. - **상업 이용** → 저자에게 별도 허락 필요 (gaoweixiaocuhk@gmail.com). ### 주의 3가지 1. **학습 가중치** — NC 데이터로 뽑은 weight는 NC로 취급하는 게 안전. 상업 배포 금지 쪽으로 본다. 2. **베이스라인별 개별 라이선스** — KPConv/PointNeXt/Open3D-ML은 대개 MIT, SPG는 별도 확인 필요. vendoring 시 각 LICENSE 동봉. 3. **데이터 자체를 repo에 커밋 금지** — HF 링크 + 다운로드 스크립트만 둔다. > 법률 자문 아님. 최종 확인은 repo `LICENSE` 파일 + HF 데이터셋 카드 원문. ### 인용 ```bibtex @InProceedings{Gao_2025_CVPR, author = {Gao, Weixiao and Nan, Liangliang and Ledoux, Hugo}, title = {SUM Parts: Benchmarking Part-Level Semantic Segmentation of Urban Meshes}, booktitle = {Proceedings of CVPR}, month = {June}, year = {2025}, pages = {24474-24484} } ``` --- ## 3. 실행 환경 판단 ### 왜 Windows 네이티브로 안 되나 `railway-client` 프로젝트의 SAM 3.1은 Windows에서 잘 돈다. 이유는 **순수 PyTorch, 컴파일 없음**. SUM 베이스라인은 성격이 다르다 — `nvcc + 컴파일러`로 **커스텀 CUDA 커널을 직접 빌드**해야 한다. | 하려는 것 | Windows 네이티브 | |---|---| | `interactive_annotation/` (SAM, SimpleClick) | ✅ 가능 | | PLY 로드 / 변환 / 시각화 | ✅ 가능 | | KPConv / PointNeXt / SPG 학습 | ❌ `pointnet2_ops`, cut-pursuit 등 Windows 패치 필요 | **결론**: WSL2가 이미 설치돼 있으므로 그대로 사용. 학습만 WSL, 데이터 변환/어노테이션은 Windows. `/mnt/d/`로 파일 공유되니 복사 불필요. ### 실측 환경 **Windows 호스트** | 항목 | 값 | |---|---| | OS | Windows 10 Enterprise 19045 | | GPU | RTX 3060 12GB, 드라이버 610.47 | | Python | 3.11.9 | | MSVC | Visual Studio Professional 2022 ✅ | | CUDA Toolkit (nvcc) | ❌ 없음 | | WSL | Ubuntu-22.04 (v2), docker-desktop | **WSL2 / Ubuntu-22.04** | 항목 | 값 | 상태 | |---|---|---| | GPU 패스스루 | RTX 3060 12GB 인식됨 | ✅ | | gcc / g++ | 11.4.0 | ✅ | | git | 2.34.1 | ✅ | | Python (시스템) | 3.10.12 | ✅ | | 디스크 `/` | 251G 중 192G 여유 | ✅ | | 디스크 `/mnt/d` | 7.3T 중 2.9T 여유 | ✅ | | RAM | 31G | ✅ | | nvcc | 없음 | ❌ 설치 필요 | | conda | 없음 | ❌ 설치 필요 | | **sudo** | **암호 필요 (NOPASSWD 아님)** | ⚠️ apt 사용 불가 | ### 최종 구성 (검증 완료) ``` nvcc : 11.8.89 (~/miniconda3/envs/sumparts/bin/nvcc) torch : 2.0.1+cu118 cuda : available → NVIDIA GeForce RTX 3060 numpy : 1.26.4 python : 3.10 ``` **업스트림 `install.sh`와 다르게 간 이유** 원본은 python 3.7 / torch 1.12.1 / cu113 / `cudatoolkit=11.3` 전제. py3.7은 EOL이고 `requirements.txt` 핀(`setuptools==59.5.0`, `protobuf==3.19.4`, `tensorboard==2.8.0` 등)이 py3.10에서 해결되지 않는다. 그래서: | 항목 | 원본 | 여기 | 이유 | |---|---|---|---| | python | 3.7 | 3.10 | 3.7 EOL | | torch | 1.12.1+cu113 | 2.0.1+cu118 | 드라이버 610.47 / sm_86 대응 | | 채널 | anaconda defaults | conda-forge + nvidia | ToS/상용 라이선스 회피 | | `deepspeed` | 설치 | 제외 | sumv2 학습 경로에서 미사용, 빌드 무거움 | | `mkdocs-*` | 설치 | 제외 | 문서 전용 | | `chamfer_dist`, `emd` 확장 | 빌드 | 제외 | reconstruction 태스크 전용 | | `plyfile` | **누락됨** | 추가 | 데이터 로더가 import 하는데 requirements.txt에 없음 | | `setuptools` | 59.5.0 | <80 | 80+에서 `python setup.py install` 제거됨 | 빌드 대상 CUDA 확장 3종: `pointnet2_batch`, `subsampling`, `pointops`. `TORCH_CUDA_ARCH_LIST=8.6` 고정 (RTX 3060) — 전 아키텍처 빌드 방지. ### Anaconda 채널 회피 (중요) 기본 miniconda는 `repo.anaconda.com/pkgs/main`, `pkgs/r`을 쓰는데, 이 채널은 **ToS 수락이 필요하고 일정 규모 이상 조직에는 상용 라이선스 의무**가 붙는다. `conda create` 시 `CondaToSNonInteractiveError`로 막힌다. → 수락하지 않고 우회. `--override-channels -c conda-forge` + CUDA는 `nvidia` 채널. `conda config --remove channels defaults` + `channel_priority strict`. ### sudo 제약 대응 `sudo` 암호가 필요해 `apt install cuda-toolkit`을 자동화할 수 없다. → **conda 경로로 우회**. 둘 다 sudo 없이 설치된다. - miniconda → `~/miniconda3` (홈 디렉토리, sudo 불필요) - CUDA Toolkit → `conda install -c nvidia cuda-toolkit` (env 내부 설치, sudo 불필요) ### 배치 원칙 | 대상 | 위치 | 이유 | |---|---|---| | 코드 / conda env | WSL ext4 (`~`) | `/mnt/d`는 9p 파일시스템이라 CUDA 확장 빌드가 매우 느림 | | 데이터셋 | `/mnt/d` | 용량 크고 Windows 도구와 공유 필요 | --- ## 3-1. 빌드 트러블슈팅 기록 CUDA 확장 3종(`pointnet2_batch`, `subsampling`, `pointops`) 빌드에서 걸린 것들. 전부 **최신 툴체인 × 2022년 코드베이스** 충돌이다. ### ① Anaconda ToS 차단 ``` CondaToSNonInteractiveError: Terms of Service have not been accepted for the following channels: https://repo.anaconda.com/pkgs/main, .../pkgs/r ``` 수락하면 넘어가지만 상용 라이선스 의무가 붙는다. → `--override-channels -c conda-forge` + `nvidia` 채널로 회피. defaults 미사용. ### ② ninja SIGPIPE ``` subprocess.CalledProcessError: Command '['ninja', '-v']' died with . RuntimeError: Error compiling objects for extension ``` 컴파일 에러가 아니다. torch 2.0의 `cpp_extension`이 `ninja -v` 출력을 파이프로 읽는데 **ninja ≥1.12가 거기서 SIGPIPE로 죽는다**. torch가 원인을 뭉개고 "Error compiling objects"로만 표시해 오해하기 쉽다. → `pip install ninja==1.11.1.1` ### ③ setuptools에서 distutils.msvccompiler 제거 ``` ModuleNotFoundError: No module named 'distutils.msvccompiler' ``` `openpoints/cpp/subsampling/setup.py:2` 가 `numpy.distutils.misc_util`를 import 하는데, `numpy.distutils`는 내부적으로 `distutils.msvccompiler`를 찾는다. 이 모듈은 **setuptools 74.0에서 제거**됨. (초기 핀 `<80`은 79.0.1을 뽑아서 여전히 실패) → `pip install setuptools==69.5.1` (단, `numpy<2` 도 함께 필요. numpy 2에는 `numpy.distutils` 자체가 없다.) ### ④ WSL 인스턴스 사망 ``` Wsl/Service/CreateInstance/E_FAIL ``` 컴파일 도중 WSL VM이 통째로 내려갔다. 컴파일 에러와 무관. → `wsl --shutdown` 후 재접속으로 복구. 재발 시 `MAX_JOBS` 낮춰서 병렬 컴파일 부하 감소. ### ⑤ 확장 모듈 이름 (함정) `pointnet2_batch/setup.py`가 만드는 모듈명은 **`pointnet2_batch_cuda`**다. 업스트림 PointNet++ 포크들의 `pointnet2_cuda`가 아니다. 검증 스크립트에서 이걸 틀리면 빌드는 다 성공했는데 `ModuleNotFoundError`로 실패한 것처럼 보인다. | 확장 | import 이름 | |---|---| | pointnet2_batch | `pointnet2_batch_cuda` | | pointops | `pointops_cuda` | | subsampling | `from openpoints.cpp.subsampling import grid_subsampling` | ### ⑥ `unzip` 미설치 exit 127. sudo 암호가 필요해 `apt install unzip`을 못 한다. → python `zipfile`로 압축 해제. ### ⑦ `wandb`가 모듈 스코프에서 import 됨 ``` main.py:8: import argparse, yaml, os, logging, numpy as np, csv, wandb, glob ModuleNotFoundError: No module named 'wandb' ``` `wandb.use_wandb=False`로 꺼도 소용없다. **import 자체가 조건 없이 실행**되므로 설치는 필수. → `pip install wandb` + `WANDB_MODE=disabled` (로그인 요구 차단). ### ⑧ chamfer_dist / emd는 "선택"이 아니다 ``` openpoints/cpp/chamfer_dist/__init__.py:10: import chamfer ModuleNotFoundError: No module named 'chamfer' ``` 기능상 reconstruction 전용이 맞다. 하지만 `openpoints/models/__init__.py:9`가 `.reconstruction`을 **조건 없이 import** → `maskedpointvit.py:10` → `chamfer_dist`. 세그멘테이션만 돌려도 import 체인에 걸린다. 빌드 필수. (`deepspeed` 제외는 유효 — import 체인에 없음.) ### ⑨ emd 모듈명도 다르다 `emd/setup.py`: `setup(name='emd_ext', ext_modules=[CUDAExtension(name='emd_cuda', ...)])` → import 이름은 **`emd_cuda`**. `emd`는 `openpoints/cpp/emd/__init__.py`의 파이썬 별칭일 뿐. ### ⑩ `data_root` 상대경로 기준점 ``` [SUMV2_Triangle]: Totally 0 samples in train set. ``` 예외 없이 조용히 0개. `data_root: ../../data/...`는 **`main.py`의 cwd 기준**이고, `main.py`는 `examples/segmentation/`에서 돈다 → `../../` = `PointNeXt_bundle/`. repo 루트가 아니다. → [scripts/link_data.sh](../scripts/link_data.sh)로 `PointNeXt_bundle/data` → `/data` 심볼릭 링크. ### ⑪ 샘플 수 < batch_size → 엉뚱한 예외 ``` 0it [00:00, ?it/s] AttributeError: 'int' object has no attribute 'diag' at openpoints/utils/metrics.py:84 -> return self.value.diag() ``` 메시지가 원인을 전혀 안 가리킨다. 실제 원인은 **dataloader의 `drop_last`**: 타일 1장 × `loop=1` = 샘플 1개인데 pointnet `batch_size=6` → **배치 0개** → 혼동행렬이 `int 0`으로 남아 `.diag()` 호출에서 터진다. → `dataset.train.loop`를 키우고 `batch_size=1`. 데이터가 적을 때 반드시 걸리는 함정. ### ⑫ Git Bash에서 WSL 호출 시 경로 변환 ``` bash: C:/Program Files/Git/mnt/d/.../smoke_train.sh: No such file or directory ``` Git Bash(MSYS)가 `/mnt/d/...`를 Windows 경로로 자동 변환한다. → WSL 호출은 PowerShell에서 하거나 `MSYS_NO_PATHCONV=1`. ### ⑬ numpy 제거 별칭 ``` sumv2_triangle.py:134: label = label.astype(np.long) AttributeError: module 'numpy' has no attribute 'long' ``` `np.long` / `np.int` / `np.float` / `np.bool` / `np.object` / `np.str`는 **numpy 1.24에서 제거**됐다. 원본은 numpy 1.20 전제. numpy를 내리는 건 불가 — torch 2.0.1은 `numpy<2`를 요구하고, numpy 1.20에는 py3.10 휠이 없다. → 소스 패치. [scripts/patch_numpy_aliases.sh](../scripts/patch_numpy_aliases.sh) (8개 파일) | 별칭 | 치환 | |---|---| | `np.long` | `np.int64` | | `np.int` | `int` | | `np.float` | `float` | | `np.bool` | `bool` | | `np.object` | `object` | | `np.str` | `str` | `\b` 경계 필수 — 안 그러면 `np.int32`, `np.float32`까지 망가진다. ### ⑭ collections ABC 이동 ``` point_transformer_gpu.py:282: isinstance(self.angle, collections.Iterable) AttributeError: module 'collections' has no attribute 'Iterable' ``` Python 3.10에서 `collections.abc`로 이동. 같은 패치 스크립트가 처리한다. ### ⑮ BatchNorm은 배치 1을 거부 ``` ValueError: Expected more than 1 value per channel when training, got input size torch.Size([1, 512]) ``` ⑪ 해결하려고 `batch_size=1`로 내렸더니 이번엔 BN이 막는다. **양쪽에서 조인다**: 너무 크면 `drop_last`로 배치 0개, 너무 작으면 BN 거부. → `batch_size=2` (BN 하한) + `loop`로 타일 반복해 온전한 배치 확보. ### ⑯ WSL 크래시가 남긴 0바이트 패키지 ★ 가장 찾기 어려웠던 것. ``` main.py:666: all_logits = scatter(all_logits, idx_points, dim=0, reduce='mean') TypeError: 'module' object is not callable ``` `from torch_scatter import scatter`가 함수가 아니라 서브모듈을 반환. 이유: ``` torch_scatter/__init__.py 0 bytes torch_scatter/scatter.py 0 bytes torch_scatter/_scatter_cuda.so 0 bytes ``` ④의 WSL VM 사망 당시 pip이 열고 있던 파일이 **전부 빈 껍데기로 디스크에 남았다**. `gdown/*.py` 전부, `vtkmodules/*.so` 다수도 동일. **고약한 이유**: import가 성공한다. 모듈이 비어있을 뿐이라 에러가 한참 뒤 호출 지점에서 엉뚱한 메시지로 터진다. 설치 로그에도 `Successfully installed`로 남아 있다. **탐지**: ```bash find -type f \( -name '*.py' -o -name '*.so' \) -size 0 ! -name '__init__.py' ``` `__init__.py`는 원래 빈 경우가 많으니 제외. 잔여 정상 케이스: `torch/cuda/error.py`, `torch/ao/.../observation_type.py`, `sklearn/_built_with_meson.py`. → [scripts/repair_env.sh](../scripts/repair_env.sh)로 크래시 당시 배치 전체 강제 재설치. **교훈**: VM/컨테이너가 설치 도중 죽으면 그 시점 패키지들을 무조건 재설치해라. pip은 손상을 감지하지 못한다. ### 무시해도 되는 경고 ``` warning: 'T* at::Tensor::data() const' is deprecated: Tensor.data() is deprecated. Please use Tensor.data_ptr() instead. warning: There are no g++ version bounds defined for CUDA version 11.8 ``` 전부 경고. 빌드 실패 원인 아니다. ### 재실행 전략 빌드 단계만 [scripts/build_ext.sh](../scripts/build_ext.sh)로 분리했다. 의존성 재설치 없이 컴파일만 재시도 가능하고, 이미 import 되는 확장은 건너뛴다 (`FORCE=1`이면 전부 재빌드). --- ## 3-2. 실학습 가능성 — RTX 3060 12GB 실측 (2026-08-20) ### 결론: 가능하다. 단 XL 계열은 `voxel_max`를 낮춰야 한다. ### 데이터 규모 | 항목 | 값 | |---|---| | `pcl.zip` | 4.66 GB | | `mesh.zip` | 0.52 GB | | `demo.zip` | 0.12 GB | | 타일 수 | **train 24 / val 8 / test 8** (총 40) | 디스크 192GB 여유 → 저장은 문제 없음. ### ★ WSL2의 함정 — VRAM 초과가 OOM을 내지 않는다 WSL2의 NVIDIA 드라이버는 VRAM을 넘으면 **호스트 RAM으로 흘린다**(system memory fallback). 네이티브 Linux라면 `torch.cuda.OutOfMemoryError`로 즉시 죽을 설정이 여기서는 **조용히 완주한다 — 25~100배 느리게.** ``` peak VRAM 15.47 GB on a 12 GB card <- 물리적으로 불가능한 수치가 보고된다 ``` "돌아간다"는 사실만 보고 설정을 확정하면 20일짜리 학습을 시작하게 된다. **반드시 peak VRAM을 카드 용량과 대조해야 한다.** ### `voxel_max` 스윕 (pointnext-xl, batch_size 2) | voxel_max | pts/batch | peak VRAM | s/iter | 판정 | |---|---|---|---|---| | 24000 | 48,000 | 6.17 G | **0.434** | ✅ | | 32000 | 64,000 | 8.03 G | **0.635** | ✅ | | 40000 | 80,000 | 9.88 G | **1.169** | ✅ | | 48000 | 96,000 | 11.75 G | **29.169** | ❌ 폴백 | | **64000** (cfg 기본값) | 128,000 | 15.47 G | **46.980** | ❌ 폴백 | 경계가 선명하다. 40000 → 48000 사이에서 **25배** 절벽. 정상 구간 안에서도 s/iter가 초선형으로 증가한다(0.434 → 0.635 → 1.169). ### 모델별 실측 | cfg | params | bs | 설정 | peak VRAM | s/iter | 적합 | |---|---|---|---|---|---|---| | pointnet | 3.6M | 2 | voxel_max 64000 (원본) | 6.01 G | 0.291 | ✅ | | pointnet++msg | 3.0M | 2 | voxel_max 64000 (원본) | 4.16 G | 0.675 | ✅ | | pointnext-xl | 41.6M | 2 | voxel_max 32000 | 8.03 G | 0.635 | ✅ | | pointvector-xl | 24.1M | 2 | voxel_max 24000 | 6.46 G | 0.402 | ✅ | | pointnext-xl | 41.6M | 2 | voxel_max 64000 | 15.47 G | 46.980 | ❌ | | pointvector-xl | 24.1M | 2 | voxel_max 64000 | 16.49 G | 13.113 | ❌ | > `batch_size`는 네 cfg 모두 `default.yaml`의 **2**를 그대로 쓴다. > default.yaml 주석의 "PointNet: 6, PointNet++: 10"은 실제 설정에 반영돼 있지 않다. ### 학습 시간 추정 반복 횟수 = train 24타일 × `loop` 30 ÷ `batch_size` 2 = **360 iter/epoch** × `epochs` 100 = **36,000 iter** | 모델 | 설정 | 학습 시간 (val 제외) | |---|---|---| | pointnet | 원본 | **2.9 h** | | pointnet++msg | 원본 | **6.8 h** | | pointnext-xl | voxel_max 32000 | **6.4 h** | | pointvector-xl | voxel_max 24000 | **4.0 h** | | ~~pointnext-xl~~ | ~~원본 64000~~ | ~~**470 h ≈ 20일**~~ | `val_freq: 1`이라 매 epoch 8타일 검증이 붙는다 → **1~3시간 추가**. `val_freq: 5`로 올리면 대부분 회수된다. ### 남는 제약 - **XL 계열은 논문 설정 재현 불가.** `voxel_max` 64000 → 32000은 샘플당 포인트를 절반으로 줄이는 것이고, 컨텍스트 윈도우가 좁아진다. 논문 수치와 직접 비교하면 안 된다. - **작은 모델 2개(pointnet, pointnet++msg)는 원본 설정 그대로 학습 가능하다.** 재현성이 중요하면 이쪽부터. - 측정은 5~8 iteration 평균이며 warm-up 2회는 제외했다. 장시간 학습의 열/클럭 변동은 반영 안 됨. ### 스크립트 | 파일 | 역할 | |---|---| | [scripts/bench_models.py](../scripts/bench_models.py) | 모델별 peak VRAM + s/iter 측정, VRAM 초과 여부 판정 | | [scripts/run_bench.sh](../scripts/run_bench.sh) | 벤치 실행 래퍼 | | [scripts/sweep_voxel_max.sh](../scripts/sweep_voxel_max.sh) | `voxel_max` 스윕으로 VRAM 한계 탐색 | | [scripts/check_hf_sizes.py](../scripts/check_hf_sizes.py) | HF 아카이브 크기 조회 | | [scripts/list_archive.sh](../scripts/list_archive.sh) | 아카이브 내용·타일 수 확인 (압축 해제 없이) | | [scripts/verify_archives.sh](../scripts/verify_archives.sh) | 다운로드 아카이브 무결성 검사 | | [scripts/check_alloc_conf.sh](../scripts/check_alloc_conf.sh) | `PYTORCH_CUDA_ALLOC_CONF` 유효성 사전 검증 | | [scripts/epoch_timing.sh](../scripts/epoch_timing.sh) | epoch별 소요 시간 → 감속 시작 지점 특정 | | [scripts/iter_rate.sh](../scripts/iter_rate.sh) | 초기 vs 최근 반복 속도 비교 | | [scripts/verify_speed.sh](../scripts/verify_speed.sh) | 현재 속도가 정상 범위인지 판정 | | [scripts/diag_run.sh](../scripts/diag_run.sh) | 실행 실패 시 로그 일괄 덤프 | ### 무인 실행 스크립트 | 파일 | 역할 | |---|---| | [scripts/launch_overnight.sh](../scripts/launch_overnight.sh) | `setsid nohup` 분리 실행. `RESUME_CKPT`로 이어하기 | | [scripts/train_watchdog.sh](../scripts/train_watchdog.sh) | 크래시 시 최신 체크포인트에서 자동 재개 (최대 8회) | | [scripts/check_training.sh](../scripts/check_training.sh) | 진행 epoch·GPU·best miou·워치독 이벤트 | | [scripts/stop_training.sh](../scripts/stop_training.sh) | 워치독 먼저 종료 후 학습 중단 | | [scripts/monitor_training.sh](../scripts/monitor_training.sh) | 진행·감속·크래시·완료 이벤트 스트림 | | [scripts/train_full.sh](../scripts/train_full.sh) | 단발 실학습 (워치독 없이) | | [scripts/prepare_full_split.sh](../scripts/prepare_full_split.sh) | `val` → `validate` 심볼릭 링크 | | [scripts/show_layout.sh](../scripts/show_layout.sh) | 전개된 데이터 레이아웃 확인 | --- ## 3-2-1. 실학습 중 발생한 무증상 10배 감속 ★★ 벤치마크에서 발견한 "VRAM 초과가 OOM을 안 낸다"가 **실제 학습에서 재현됐다.** 다만 원인이 예상과 달랐다. ### 증상 ``` epoch 1-10 ~103 s/epoch (벤치 예측 105 s와 일치) epoch 11 +1011 s ← 20:45부터 급변 epoch 12-20 ~1100 s/epoch ``` 반복당 속도: **0.29 s/it → 3.14 s/it (10.7배)**. 남은 80 epoch에 24시간. 에러 없음. 경고 없음. GPU 사용률 100%, 클럭 1942MHz 만빵, 온도 51°C, 스로틀 전무. **로그만 보면 완벽하게 정상이다.** ### 오진과 정정 처음엔 데스크톱 앱(브라우저·Electron)이 VRAM을 잠식했다고 봤다. `nvidia-smi`에 Brave, Edge, WebView2, VS Code 등이 잔뜩 잡혀 있었기 때문이다. **학습을 죽이고 재측정해서 뒤집혔다:** | 상태 | Dedicated (VRAM) | Shared (호스트 RAM) | |---|---|---| | 학습 중 | 11,715 MB | **18,967 MB** | | 학습 종료 후 | **1,245 MB** | 468 MB | 데스크톱 앱은 1.2GB뿐. **11.7GB + 19GB를 전부 학습 프로세스가 잡고 있었다.** 벤치 측정값 6.01GB의 약 2배로 불어난 것. > 교훈: 남 탓하기 전에 **의심 대상을 죽여보고 기준선을 재라.** 30초면 된다. ### 진단 경로 | 단계 | 관측 | 배제/확정 | |---|---|---| | GPU 스로틀 | 51°C, `HW Slowdown: Not Active` | 열/전력 배제 | | WSL 자원 | swap 0, IO wait 0, load 1.16 | 메모리·디스크 배제 | | 프로세스 | main python **CPU 99.8% 단일 스레드** | CUDA 동기화 스핀 = GPU 대기 | | **전력** | **61.7W** (3060 TDP 170W) | 연산 아님 → **전송 대기** | | 반복 속도 | 초기 0.29 s/it → 3.14 s/it | epoch 간 오버헤드 아닌 **반복 자체** 감속 | | **Perf 카운터** | **Shared Usage 18,967 MB** | **시스템 메모리 폴백 확정** | **전력 소비가 가장 빠른 판별 지표였다.** 사용률 100%인데 전력이 낮으면 연산이 아니라 대기다. ```powershell # 폴백 여부 직접 확인 (nvidia-smi로는 안 보인다) (Get-Counter '\GPU Adapter Memory(*)\Shared Usage','\GPU Adapter Memory(*)\Dedicated Usage').CounterSamples ``` ### 기여 요인 — cfg의 검증 설정 ```yaml train: { voxel_max: 64000 } val: { voxel_max: null } # ← 타일 전체(47만 포인트)를 통째로 ``` 학습 스텝은 상한이 있는데 **검증만 무제한**이다. 이 거대한 일시 할당이 캐싱 할당자 풀을 부풀린다. 감속 시작이 epoch 10 검증 직후인 것과 일치한다. **핵심**: PyTorch 캐싱 할당자는 한 번 shared로 넘어간 풀을 되돌리지 않는다. VRAM이 비어도 자동 회복 없음 → **프로세스 재시작이 유일한 복구 경로.** ### 조치 | 항목 | 값 | |---|---| | 할당자 | `PYTORCH_CUDA_ALLOC_CONF=garbage_collection_threshold:0.7,max_split_size_mb:128` | | 검증 상한 | `dataset.val.voxel_max=64000` (학습과 동일) | | 재개 | epoch 20 체크포인트 (진행분 손실 없음) | **복구 확인** ``` 반복 속도 : 3.56 it/s = 0.28 s/it (벤치 0.291과 일치) GPU 전력 : 61.7W → 140.08W ``` ### ⚠️ `expandable_segments`는 torch 2.0에서 못 쓴다 첫 시도에서 `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True`를 썼다가 즉시 크래시. ``` RuntimeError: Unrecognized CachingAllocator option: expandable_segments ``` **torch 2.1부터 지원**한다. 이 환경은 2.0.1. 게다가 traceback이 `model.to(device)`를 가리켜 모델 문제처럼 보인다. → [scripts/check_alloc_conf.sh](../scripts/check_alloc_conf.sh)로 **실행 전 2초 만에 검증**하도록 만들었다. 워치독은 이때 "재시도해도 새 체크포인트가 안 생긴다 = 학습 진입 전 실패"를 감지하고 무한 재시도 없이 정상 중단했다. 설계 의도대로 동작. ### 남는 편차 — 명시 `dataset.val.voxel_max`를 64000으로 제한했으므로 **val_miou는 서브샘플된 타일 기준**이다. 체크포인트 선택 신호로만 유효하다. 최종 test는 `test()`가 전체 타일을 슬라이딩 윈도우로 처리하므로 **보고 수치에는 영향이 없다.** ### 감시에 반영 침묵이 성공으로 오인되지 않도록 [scripts/monitor_training.sh](../scripts/monitor_training.sh)에 **속도 회귀 감지**를 추가했다. 1.5 s/it을 넘으면 GPU 메모리·전력과 함께 즉시 알린다. 크래시만 감시하면 이런 무증상 감속은 영원히 못 잡는다. --- ## 3-4. 실학습 결과 — pointnet 100 epoch (2026-08-21) ### 완주 ``` epoch 100/100 완료 01:56:58 Best ckpt @E90 val_oa 49.12 val_macc 25.47 val_miou 17.19 ``` | 항목 | 값 | |---|---| | 모델 | pointnet (3.6M params) | | 데이터 | `face_labeling/texsp_pcl`, train 24 / val 8 / test 8 | | 설정 | `voxel_max 64000`(원본), `batch_size 2`, `val_freq 5`, cosine LR 0.01→1.2e-5 | | 소요 | 20:08 시작 → 01:57 완료. 순수 학습 시간 약 3.4h (감속 사고 3h 별도) | | 속도 | 102초/epoch, 0.28 s/it (벤치 0.291과 일치) | ### 수렴 추이 | epoch | train_miou | best val_miou | |---|---|---| | 10 | 12.80 | 2.77 | | 20 | 15.79 | 8.04 | | 31 | 17.56 | 8.49 | | 40 | 17.86 | 10.40 | | 50 | 18.89 | 15.62 | | 60 | 19.22 | 16.04 | | 70 | 19.09 | 16.93 | | 80 | 19.62 | 16.93 | | **90** | — | **17.19** ← best | | 100 | 19.95 | 17.19 | train 19.95 / val 17.19 — 격차가 작아 과적합 징후 없다. ### ★ test 세트는 라벨이 없다 (블라인드) 학습 직후 test 단계에서 크래시: ``` main.py:672 cm.update(pred, label) metrics.py:74 RuntimeError: bincount only supports 1-d non-negative integral inputs ``` 원인 조사 결과 **우리 설정 문제가 아니었다**: | split | 라벨 값 | |---|---| | train 24타일 | `0 ~ 12` 정상 | | val 8타일 | `0 ~ 12` 정상 | | **test 8타일** | **전부 `-1`** (dtype int32, uniq=1) | README와 일치한다: > "For fine-grained test set evaluation consistent with the paper, > **send predictions to our email for local assessment.**" 의도적인 블라인드 테스트셋이고 `-1`은 자리표시자다. 업스트림 코드가 `if label is not None`만 검사하고 `-1`을 안 걸러서, `true × num_classes + pred`가 음수가 되어 `torch.bincount`가 거부한 것. > **로컬에서 test_miou는 원천적으로 계산 불가.** 논문 수치와 직접 대조하려면 > 예측 결과를 저자에게 보내야 한다 (gaoweixiaocuhk@gmail.com). ### 업스트림 버그 패치 2건 | # | 증상 | 원인 | 패치 | |---|---|---|---| | A | `bincount ... non-negative` | 블라인드 test의 `-1` 라벨 미처리 | [patch_unlabeled_test.sh](../scripts/patch_unlabeled_test.sh) — 전부 음수면 "정답 없음"으로 간주, 예측만 생성 | | B | `UnboundLocalError: 'epoch'` (`mode=val`) | `epoch`이 학습 루프 안에서만 바인딩되는데 val 경로가 참조 | [patch_val_mode.sh](../scripts/patch_val_mode.sh) — `epoch = best_epoch` 선바인딩 | 둘 다 멱등이고 원본은 `main.py.orig`로 보존한다. 라벨 있는 데이터의 채점 동작은 바뀌지 않는다. ### 워치독 동작 검증 학습 완주 후 test 크래시로 rc=1이 되자 워치독이 재개를 시도했고, 2회째에 **"새 체크포인트가 생기지 않음 = 학습 진입 전 실패"**를 감지해 중단했다. 무한 재시도 없이 설계대로 동작했다. ### 남는 편차 학습 중 `dataset.val.voxel_max`를 64000으로 제한했다(§3-2-1). 따라서 위 `val_miou 17.19`는 **서브샘플된 검증셋 기준**이다. `voxel_max: null`(전체 타일)로 다시 평가한 수치는 [scripts/final_eval.sh](../scripts/final_eval.sh) 결과로 별도 기록한다. --- ## 3-3. WSL2 안정화 VM이 **2회** 사망했다. | # | 시점 | 상황 | |---|---|---| | 1 | CUDA 확장 컴파일 중 | `Wsl/Service/CreateInstance/E_FAIL`. pip 진행 중 파일들이 **0바이트**로 남음 (§3-1 ⑯) | | 2 | GPU 벤치(VRAM 만재) + 4.7GB 다운로드 동시 | 두 작업 동시 exit 253 | `.wslconfig`가 없어 WSL2가 호스트 64GB의 50%(≈32GB)를 동적으로 잡고 있었다. **적용한 설정** (`C:\Users\nbright\.wslconfig`) ```ini [wsl2] memory=24GB # 동적 50% → 고정 상한 swap=16GB # 스파이크 시 VM 사망 대신 페이지아웃 processors=8 # cfg의 num_workers: 6 감안 vmIdleTimeout=60000 # 연속 실행 시 VM 부팅 비용 회피 ``` `pageReporting`은 뺐다 — 기본값이 `true`이고, 명시하면 WSL 2.7.12가 파싱 경고를 낸다. 적용 후 확인: `Mem 23GB / Swap 16GB / 8 cpus`, GPU 정상 인식, 경고 없음. **운용 규칙**: GPU 만재 작업과 대용량 다운로드를 **동시에 돌리지 않는다.** > `wsl --shutdown`은 WSL 배포판만 종료한다. Windows 네이티브 프로세스는 영향 없다. --- ## 4. 한국 데이터 적용 계획 ### 두 갈래 경로 **A. 포인트 클라우드 경로 (권장, 스모크 테스트용)** 포인트 기반 방법(KPConv/PointNeXt/RandLA-Net)은 `xyz + rgb + label`만 필요. 텍스처 불필요. ``` 한국 메시(OBJ+texture) → face 샘플링 → binary PLY (v:color, v:label) ``` - SUM으로 학습한 모델 → 한국 타일 zero-shot 추론 - 라벨 없으면 전부 `0`(unclassified)로 채워도 추론은 동작 **B. 메시 경로 (텍스처 기반 21클래스 풀 파이프라인)** `h:texcoord` + `f:label` 포함 ASCII PLY 필요. 변환 공수 큼. 후순위. ### 확보된 실데이터 — 서산 명천 (2026-08) `D:\MyProject_대용량샘플\02. 데이터\01. 서산 명천_08월` ``` ├── 01. 드론원본이미지 JPG 8,120장 (132 GB) + PPK(.obs/.nav/.MRK) │ 고고도 120m / 저고도 70m × 수직 / 경사, 1구역(미션1)·2구역(미션7) ├── 02. 본태모델 ★ OBJ 6개 (3.0 GB) + JPG 텍스처 144장 (908 MB) ├── 03. 지오이드모델 .tif 2.8 GB ├── 04. 카메라파라미터 .txt 6개 └── 05. 정사영상 .tif 25.8 GB + .tfw + .dwg ``` **본태모델 = ContextCapture 계열 OBJ, 블록 6개** | 구역 | 블록 | OBJ 크기 | |---|---|---| | 구역1(미션1) | BlockYBY / BlockYYA / BlockYYX | 645 / 433 / 464 MB | | 구역2(미션7) | BlockBXY / BlockYBA / BlockYBX | 497 / 355 / 634 MB | **`metadata.xml` — 좌표계 (결정적)** ```xml EPSG:5186+9999 154113.37472199186,466558.57182090241,151.18628846539363 155184.79128718161,469705.02542312664,149.57080945797654 ``` → **좌표가 이미 로컬 미터계 + 원점 오프셋.** SUM Parts 배포 관례와 동일. 재투영 작업 불필요. (단 원본 easting/northing이나 경위도를 그대로 넣으면 cfg의 반경·voxel 설정이 전부 미터 기준이라 조용히 망가진다.) **메시 실측** (`scripts/inspect_obj.py`) | | BlockYBA | BlockYYA | SUM Parts demo | |---|---|---|---| | vertex | 2,287,325 | 2,699,696 | — | | face | 4,553,583 | 5,377,777 | — | | UV | ✅ 3,121,900 | ✅ 3,886,962 | ✅ | | vertex color | ✗ | ✗ | — | | 머티리얼 | **17** | **21** | — | | extent | 465×487×28 m | 439×404×46 m | 252×252×40 m | | 면적 | 0.227 km² | 0.177 km² | — | | 밀도 | 20.1 face/m² | 30.3 face/m² | — | **적합 판정 근거** - 텍스처 메시 + UV 완비 → SUM Parts와 동일 모달리티 - 좌표계 관례 일치 - 블록이 SUM 타일(252m)의 약 4배 면적 → 블록당 4타일, 6블록 = **약 24타일** 확보 가능 **주의 2가지** 1. **멀티머티리얼** — 블록당 텍스처 아틀라스 17~21장. `trimesh.load(force='mesh')`로 합치면 텍스처가 전부 날아가 결과가 회색이 된다. Scene으로 받아 머티리얼별로 샘플링해야 함. 2. **vertex color 없음** — 텍스처가 유일한 색상 소스. 폴백 경로가 없다. ### 왜 PLY로 변환하나 (OBJ/GLB 아니고) 포맷 선택이 아니라 **모델 요구사항**이다. ```python # openpoints/dataset/sumv2_triangle/sumv2_triangle.py self.data_list = glob.glob(os.path.join(data_root, split, "*.ply")) plydata = PlyData.read(filename) labels = plydata['vertex']['label'] ``` - 데이터로더가 `*.ply`만 glob한다. OBJ/GLB는 스캔조차 안 함 - 네트워크 입력은 **포인트 클라우드**지 메시가 아니다 - OBJ/GLB에는 포인트별 `label` 스칼라를 담을 표준 필드가 없다. SUM Parts 저자가 텍스처 메시조차 PLY로 배포하는 이유도 같다 (`f:label`, `v:label`) 원본 OBJ는 읽기만 하고 손대지 않는다. PLY는 모델 입력용 중간 산출물이다. ``` BlockYBA.obj (원본 불변) → 면적가중 샘플링 → tile.ply → 모델 → 예측 ``` ### 다른 한국 데이터 후보 (참고) - **V-World 3D** (국토지리정보원) — 3D 건물 / 지형 - **서울 S-Map** — 3D 서울, 텍스처 메시 --- ## 4-1. POC 결과 (2026-08-20) — 통과 범위를 의도적으로 **타일 1장**으로 제한. "우리 데이터가 이 파이프라인을 통과하는가"만 확인. 성능 측정 아님. **입력**: 구역2(미션7)/BlockYBA, 중앙 252m bbox `(300, 720) ~ (552, 972)` ``` [17:10:12] loaded 17 geometry group(s), 4,553,583 faces total [17:10:13] cropped surface area: 14,609 m^2 across 283,099 faces geom 0..16 : 머티리얼별 면적 비례 샘플링 [17:10:19] extent: [137.84 178.07 19.48] m origin: [299.6 794.32 -67.58] [17:10:19] wrote seosan_BlockYBA_tile0.ply (470,000 points, label=0, colour=sum) ``` **스키마 대조** (`scripts/check_ply.py`) | | 변환 결과 | SUM demo | 판정 | |---|---|---|---| | properties | `x,y,z,r,g,b,label` | `x,y,z,nx,ny,nz,r,g,b,label,sp_id` | OK (법선·sp_id는 로더가 무시) | | colour | float32 [0,1] | float32 [0,1] | ✅ 일치 | | label dtype | int32 | int32 | ✅ | | extent | 137.8 × 178.1 × 19.5 m | 252.2 × 252.2 × 39.7 m | 노선형 vs 정사각 타일 | | density | 19.1 pts/m² | 7.4 pts/m² | 2.6× 조밀 | **추론** (`scripts/poc_infer.sh`) ``` Successful Loading the ckpt from ..._ckpt_best.pth length of test dataset: 1 Test on 0-th cloud [2]/[3]: 100% test_oa nan, test_macc 0.00, test_miou 0.00 POC INFER DONE (exit=0) ``` `test_oa = nan`은 **정상**이다. 전 포인트 라벨이 0이고 `ignore_index: 0`이라 채점 대상이 없다. **예측 산출물** (`scripts/check_pred.py`) ``` visualization/seosan_BlockYBA_tile0_pred.ply 13 MB points : 470,000 properties : ['x','y','z','red','green','blue','label'] palette fit: exact class points share wall 470,000 100.00% ``` 470,000 포인트 전부에 클래스 예측이 붙었다. 전부 `wall`인 건 1 epoch 모델(val_miou 0.21)이 사실상 단일 클래스만 뱉기 때문이며, 예상된 결과다. > **결론: 배관은 뚫렸다.** OBJ → 샘플링 → PLY → 모델 → 포인트별 예측 → 시각화 PLY. > 모델 품질에 대해서는 이 POC가 아무것도 말해주지 않는다. ### POC에서 드러난 것 3가지 **① 색상 스케일 — 조용히 망가지는 함정 ★** 첫 변환에서 `red,green,blue` uint8 [0,255]로 썼다. SUM 배포본은 `r,g,b` **float32 [0,1]**이다. 로더는 정규화하지 않는다: ```python rgb = np.stack([...'r','g','b'...]).astype(np.float32) if np.max(rgb) > 1: rgb = rgb # ← no-op. 읽으면 정규화할 것 같지만 아무것도 안 한다 ``` cfg의 활성 transform도 `[PointsToTensor, PointCloudScaling, PointCloudRotation, PointCloudJitter]` 뿐 — `NumpyChromaticNormalize`는 주석 처리돼 있다. → **파일에 든 값이 그대로 네트워크에 들어간다.** 255배 큰 색상 특징을 주게 되고, 에러 없이 결과만 무의미해진다. `--colour-style sum`이 기본값인 이유. **② 블록은 정사각형이 아니다 — 노선을 따라 뻗은 형태** 252×252m bbox로 잘랐는데 실제 표면적은 14,609 m² (bbox 면적 63,504 m²의 **23%**). 결과 extent도 137×178m. 도로 프로젝트라 노선을 따라 길게 뻗은 형태다. 도시 전역을 덮는 SUM Parts와 형상이 다르다. → 균등 격자로 타일링하면 빈 타일이 대량 발생한다. **노선 축을 따라 자르는 전략**이 필요하다. 앞서 "블록당 4타일 × 6블록 = 24타일" 추정은 낙관적이며, 실제로는 더 적다. **③ 멀티머티리얼 처리 필수** `trimesh.load(..., force='mesh')`로 합치면 머티리얼별 텍스처가 전부 유실되어 결과가 회색으로 나온다. Scene으로 받아 geometry(=머티리얼)별로 UV 샘플링해야 한다. BlockYBA는 17개, BlockYYA는 21개 그룹. ### 다음 단계에서 필요한 것 POC는 배관만 확인했다. 의미 있는 결과를 내려면: 1. **제대로 학습된 체크포인트** — SUM Parts 전체 데이터(`mesh.zip`/`pcl.zip`)로 학습. 현재는 demo 타일 1장 1 epoch짜리뿐이다. 2. **노선 축 기반 타일링** — 빈 타일 회피 3. **한국 타일 라벨링** — zero-shot 성능을 측정하려면 GT가 필요하다. 최소 2~3타일. SAM 기반 어노테이션 도구(`interactive_annotation/`)가 repo에 있다. 4. **클래스 정의 대조** — SUM 13클래스(terrain/roof/facade/chimney/dormer...)는 도시 건물 중심. 도로 프로젝트에서 필요한 건물/수목/차량/지면은 SUM 13클래스를 합치면 그대로 나온다 (§4-2). ### 예상 결과 SUM Parts는 헬싱키 항공 메시 기반. 한국 도시는 지붕 형태(평지붕 + 옥탑), 건물 밀도, 도로 표시가 다르다. → **zero-shot mIoU 큰 폭 하락 예상**. 그게 정상이며, 그 수치 자체가 저자에게 보낼 메일의 근거가 된다. 타일 2~3장 라벨링해 fine-tune 하면 크게 개선될 것. --- ## 5. 진행 순서 1. ~~환경 조사~~ ✅ 2. ~~WSL2 환경 셋업~~ ✅ (miniconda, conda env py3.10, CUDA 11.8, torch 2.0.1+cu118) 3. ~~repo clone~~ ✅ `~/sum-parts` 4. ~~PointNeXt CUDA 확장 빌드~~ ✅ (3종 전부 import 확인) 5. ~~HF 게이트 수락~~ ✅ (브라우저 수동) 6. ~~`demo.zip` 다운로드 + 압축 해제 + 스키마 검증~~ ✅ 7. ~~1 epoch 스모크 테스트~~ ✅ **파이프라인 생존 확인** 8. ~~한국 실데이터 조사~~ ✅ 서산 명천, ContextCapture OBJ 6블록 9. ~~한국 타일 → PLY 변환~~ ✅ BlockYBA 타일 1장, 470k 포인트 10. ~~POC 추론~~ ✅ **배관 관통 확인** (§4-1) 11. **제대로 된 학습** ← 다음. `pcl.zip` 전체 받아 실학습 12. 노선 축 기반 타일링 + 한국 타일 라벨링 13. Zero-shot 평가 + Mapple 시각화 14. 결과 정리 후 저자에게 메일 ### 스모크 테스트 결과 (2026-08-20) ``` cfg=pointnet epochs=1 loop=4 bs=2 Train Epoch [1/1] Loss 3.474 Acc 0.08 Best ckpt @E1 val_oa 2.48 val_macc 8.33 val_miou 0.21 Test [0]/[1] cloud Best ckpt @E1 test_oa 2.48 test_macc 8.33 test_miou 0.21 save results in log/.../ALo5iXF6AAHrWkAghKA8nv.csv exit=0 ``` **체인 전 구간 통과**: PLY 로드 → grid subsample → CUDA ops → forward/backward → 검증 → 체크포인트 저장 → 재로드 → 테스트 추론 → CSV 저장. ⚠️ **수치는 무의미하다.** 1 epoch, 타일 1장, train/val/test 모두 같은 타일. 파이프라인 생존만 확인한 것이고, 어떤 성능 주장에도 쓸 수 없다. ### 검증된 스택 ``` python 3.10 nvcc 11.8.89 torch 2.0.1+cu118 cuda available: True (RTX 3060) numpy 1.26.4 ninja 1.11.1.1 (1.12+ 금지) setuptools 69.5.1 (74+ 금지) torch-scatter 2.1.2+pt20cu118 pointnet2_batch_cuda : OK pointops_cuda : OK grid_subsampling : OK plyfile / torch_scatter : OK ``` ### 스크립트 | 파일 | 역할 | |---|---| | [scripts/setup_env.sh](../scripts/setup_env.sh) | conda env + CUDA + torch | | [scripts/setup_pointnext.sh](../scripts/setup_pointnext.sh) | python 의존성 → build_ext.sh 호출 | | [scripts/build_ext.sh](../scripts/build_ext.sh) | CUDA 확장 빌드만 (재실행 가능, `FORCE=1`로 전체 재빌드) | | [scripts/repair_env.sh](../scripts/repair_env.sh) | WSL 크래시로 0바이트가 된 패키지 재설치 | | [scripts/patch_numpy_aliases.sh](../scripts/patch_numpy_aliases.sh) | 제거된 numpy 별칭 / collections ABC 소스 패치 | | [scripts/verify_env.py](../scripts/verify_env.py) | 컴파일 확장 + import 체인 일괄 점검 | | [scripts/download_data.sh](../scripts/download_data.sh) | HF 데이터 다운로드 + 압축 해제 | | [scripts/prepare_demo_split.sh](../scripts/prepare_demo_split.sh) | demo 타일을 train/val/test로 배치 | | [scripts/link_data.sh](../scripts/link_data.sh) | `PointNeXt_bundle/data` 심볼릭 링크 | | [scripts/smoke_train.sh](../scripts/smoke_train.sh) | 1 epoch 파이프라인 생존 테스트 | | [scripts/inspect_obj.py](../scripts/inspect_obj.py) | 대용량 OBJ 스트리밍 통계 (vertex/face/UV/bbox/머티리얼) | | [scripts/mesh_to_ply.py](../scripts/mesh_to_ply.py) | 멀티머티리얼 텍스처 메시 → SUM 스키마 PLY (bbox 크롭 지원) | | [scripts/check_ply.py](../scripts/check_ply.py) | PLY를 SUM 스키마와 대조 검증 | | [scripts/poc_korea.sh](../scripts/poc_korea.sh) | 서산 명천 타일 1장 변환 + 검증 | | [scripts/poc_infer.sh](../scripts/poc_infer.sh) | 변환된 타일로 추론 | | [scripts/check_pred.py](../scripts/check_pred.py) | 예측 PLY의 클래스 분포 복원 (팔레트 역매핑) | | [scripts/poc_check_pred.sh](../scripts/poc_check_pred.sh) | 최신 예측 PLY 자동 탐색 후 검사 | ### 실행 순서 (처음부터 재현 시) ```bash bash scripts/setup_env.sh # conda env + CUDA 11.8 + torch 2.0.1 bash scripts/setup_pointnext.sh # 의존성 + CUDA 확장 빌드 bash scripts/patch_numpy_aliases.sh # 소스 현대화 패치 bash scripts/download_data.sh # HF 게이트 수락 후 bash scripts/prepare_demo_split.sh bash scripts/link_data.sh python scripts/verify_env.py # 전부 OK 확인 bash scripts/smoke_train.sh ``` WSL이 중간에 죽으면 `bash scripts/repair_env.sh` 먼저 돌리고 재개. --- ## 6. 메모 - VSCode에서 WSL 전환: 좌하단 `><` 버튼 → "Connect to WSL" - 다만 Windows 셸에서 `wsl -d Ubuntu-22.04 -- bash -lc ""` 로 직접 실행도 가능 (전환 불필요) - `Downloads`에 `교각.hmeg`, `교량.hmeg`, `sss.obj` 등 자체 3D 데이터 존재 → 한국 샘플 후보로 검토 가치 있음