graph LR
A["DIGIT RGB\n240×320"] --> B["배경 차감\nimage − blank"]
B --> C["+좌표 채널 2\n(x, y 픽셀 인덱스)"]
C --> D["TouchNet\n9-conv FCN\n≈3.9M"]
D --> E["Gx, Gy\n기울기맵 2ch"]
E --> F["DST Poisson\n적분"]
F --> G["depth (mm)\n+ contact mask"]
📝 DIGIT pretrained 5종 네트워크 구조 비교
DIGIT pretrained 5종 비교 실험 보고서의 후속. 지난 보고서가 “같은 DIGIT에 꽂았을 때 체감이 어땠나”였다면, 이번에는 왜 그런 차이가 났는지를 각 모델의 네트워크 구조 설계에서 찾는다. 다섯 레포(py3DCal, Sparsh, SITR, MidasTouch, NeuralFeels)의 실제 코드를 읽고 레이어 단위까지 정리했다. 방법론 자체의 배경은 DIGIT 2D 센서 정보 활용 참조.
1. 왜 구조를 다시 보는가
체감 비교에서 나온 관찰들 — “NeuralFeels 깊이는 절대값이 모호하다”, “SITR은 시뮬 학습인데 전이가 잘 된다”, “Sparsh는 무접촉 노이즈가 있지만 상대 변화는 안정적이다” — 는 전부 구조와 학습 목적함수에 원인이 새겨져 있었다. 예를 들어 NeuralFeels의 절대값 모호는 도메인 갭 문제이기 이전에, 애초에 scale/shift 불변 손실로 학습되어 절대 깊이를 맞출 의무가 없는 네트워크였기 때문이다. 이런 것들은 데모를 돌려서는 안 보이고 코드를 읽어야 보인다.
또 하나의 동기는 재학습이다. 내 센서로 파인튜닝하거나 모델을 개조하려면 어디에 무슨 레이어가 있고 손실이 뭔지 알아야 한다. 이 노트는 그때 다시 펴 볼 참조 문서를 겸한다.
2. 한눈 비교표
| py3DCal | Sparsh | SITR | MidasTouch | NeuralFeels | |
|---|---|---|---|---|---|
| 모델 종류 | 순수 CNN (FCN) | ViT-B + DPT 디코더 | ViT-B + Linear 디코더 | FCRN + MinkLoc3D + PF | ViT-S + DPT 디코더 |
| 백본 | TouchNet 9-conv | ViT-Base/16 (768d, 12층) | ViT-Base/16 (768d, 12층) | ResNet-50 / MinkFPN | ViT-Small/16 DINO (384d, 12층) |
| 파라미터 | ≈3.9M | ≈86M(백본)+디코더 | ≈86M+α | ≈60M대(TDN)+≈1M(TCN) | ≈22M+디코더 |
| 입력 | 5ch = RGB−bg + 좌표 2ch | 6ch = 2프레임 RGB concat | 3ch 샘플 + 54ch 캘리브 | 3ch RGB (min-max 정규화) | 3ch RGB, [−1,1] |
| 디코더 | 없음 (출력이 곧 conv9) | DPT Reassemble+Fusion | Linear(768→768)+unpatchify | UpProject ×4 / GeM pooling | DPT Reassemble+Fusion |
| 출력 | Gx,Gy → Poisson → mm depth | normal 1ch + shear 2ch | normal map 3ch + 128d 임베딩 | heightmap → 256d code → SE(3) belief | depth [0,1] 1ch |
| 학습 목적함수 | MSE (지도) | SSL(DINO 등) + photometric 자기지도 | SupCon + normal map 지도 | MSE / BatchHard Triplet | Scale-Shift-Invariant loss |
| 학습 데이터 | 실물 3D프린터 캘리브 | 실물 대규모 무라벨 | 시뮬 (domain randomization) | 시뮬 (TACTO) | 시뮬 (TACTO) |
이 표만으로도 지난 체감 순위의 절반이 설명된다. 실물 데이터로 학습된 것은 py3DCal과 Sparsh뿐이고, 시뮬 학습 3종 중 SITR만 domain randomization + 캘리브레이션 조건화라는 이중 안전장치를 갖고 있다.
3. py3DCal — TouchNet: 해상도를 버리지 않는 미니멀 CNN
구조
TouchNet은 다섯 모델 중 유일하게 풀링도 스트라이드도 없는 네트워크다. 9개 conv를 지나는 동안 공간 해상도(320×240)가 한 번도 줄지 않고, 채널만 모래시계처럼 부풀었다 줄어든다. conv1–8은 Conv→BN→ReLU→Dropout2d, 마지막 conv9만 1×1 무활성 선형 투영이다.
| # | in→out ch | kernel | dropout |
|---|---|---|---|
| conv1 | 5→32 | 7×7 | 0.2 |
| conv2 | 32→64 | 7×7 | 0.2 |
| conv3 | 64→128 | 7×7 | 0.2 |
| conv4 | 128→256 | 5×5 | 0.3 |
| conv5 | 256→256 | 5×5 | 0.3 |
| conv6 | 256→128 | 5×5 | 0.2 |
| conv7 | 128→64 | 3×3 | 0.2 |
| conv8 | 64→32 | 3×3 | 0.2 |
| conv9 | 32→2 | 1×1 | — |
파라미터를 코드에서 직접 세어 보면 conv ≈3,879,586 + BN 2,080 = ≈3.88M. skip connection도, 인코더-디코더도 없다. receptive field는 커널을 겹겹이 쌓아(7,7,7,5,5,5,3,3) 확보한다.
입력 5채널이 재밌는 부분. RGB에서 blank 이미지를 뺀 3채널에, 정규화도 안 한 절대 픽셀 인덱스(x는 1..W, y는 1..H)를 좌표 채널 2장으로 붙인다. CoordConv의 소박한 버전인데, 목적이 명확하다 — DIGIT의 조명은 위치마다 색·방향이 다르므로, 같은 눌림이라도 젤 위 어디냐에 따라 RGB 패턴이 다르다. 좌표 채널은 네트워크에 “지금 젤의 어느 위치를 보고 있는지”를 알려주는 조건화다.
출력과 후처리
출력은 깊이가 아니라 표면 기울기 (Gx, Gy) 2채널이다. 깊이는 학습 없는 후처리 — scipy DST(Discrete Sine Transform) 기반 스펙트럴 Poisson 솔버(Dirichlet 경계, 반복 없이 직접 해) — 로 적분해 얻고, clip(−depth, 0)으로 뒤집어 mm 단위 깊이맵을 만든다. 즉 “학습은 국소 기울기까지만, 전역 적분은 수학에 맡긴다”는 분업. mm 절대값이 나오는 이유는 학습 라벨 자체가 3D 프린터로 구를 정확한 (x, y, 깊이)에 눌러 만든 실측 기울기이기 때문이다.
학습
MSELoss로 기울기맵 회귀, AdamW(lr 1e-4, wd 1e-4), 60 epochs. 다섯 중 가장 단순한 지도학습이다.
코드 위치
- 모델:
py3DCal/model_training/models/touchnet.py:18 - 좌표 채널:
model_training/lib/add_coordinate_embeddings.py:3 - Poisson:
model_training/lib/fast_poisson.py:5 - 학습 루프:
model_training/lib/train_model.py:15
4. Sparsh — SSL ViT 백본 + DPT 디코더: 표현과 태스크의 분리
graph LR
A["프레임 t, t−5\nRGB×2 = 6ch"] --> B["배경 diff\n(img−bg)/255+0.5"]
B --> C["PatchEmbed\nConv 6→768, /16"]
C --> D["ViT-Base 12층\n768d (SSL, frozen)"]
D -->|"blocks 2,5,8,11"| E["Reassemble ×4\nscales 4/8/16/32"]
E --> F["Fusion ×4\nRefineNet식"]
F --> G["NormalShearHead"]
G --> H["normal 1ch (sigmoid)\nshear 2ch (tanh×20)"]
백본: 2프레임을 채널로 합쳐 한 번에 패치화
백본은 ViT-Base/16(768d, 12층 12헤드, register token 1개, sinusoidal 위치 임베딩). 입력이 특이한데, 연속 2프레임(stride 5)의 RGB를 채널 축으로 concat한 6채널 이미지를 단일 PatchEmbed(Conv 6→768, k16 s16)로 한 번에 패치화한다. 프레임별로 토큰화해서 시간 축 attention을 하는 게 아니라, 시간 정보를 패치 임베딩 conv가 처음부터 섞는다. 미끄러짐(shear·slip)은 단일 프레임에는 없고 프레임 간 변화에만 있으므로, 시간을 입력 단계에서 태워버리는 이 설계가 Sparsh의 정체성이다. normal 추정 경로는 [현재 프레임, 배경] 조합의 6채널을 쓴다.
SSL 알고리즘은 레포에 DINO(EMA teacher, 65536-d 프로토타입 head, multi-crop), DINOv2(+iBOT patch loss, KoLeo, LayerScale), MAE(mask ratio 0.75, 마스크 패치만 MSE), I-JEPA(latent 예측 L1)가 모두 구현돼 있고, 백본만 바꿔 끼울 수 있다. 체감 최고였던 DINO 백본은 라벨 없이 표현만 배운 상태로 frozen된다.
Force field 디코더: DPT를 힘 추정에 재활용
디코더는 놀랍게도 NeuralFeels와 같은 DPT 계열이다. frozen ViT의 블록 2, 5, 8, 11에 forward hook을 걸어 4개 층의 토큰을 뽑고:
- Reassemble — cls/register 토큰을 버리고(
Read_ignore) 토큰을 2D 격자로 재배열, 1×1 conv로 768→128(resample_dim), 스케일 [4,8,16,32]로 업/다운샘플 - Fusion —
ResidualConvUnit(3×3 conv 잔차) ×2 + ×2 업샘플, 거친 층부터 미세한 층으로 RefineNet식 누적 - NormalShearHead — ConvBlock 2개(skip concat 포함) 뒤 두 갈래: normal = Conv3×3→1ch→Sigmoid (0–1), shear = Conv3×3→GELU→Conv3×3→2ch→Tanh ×20 (±20 스케일 flow)
학습: 힘 라벨 없는 photometric 자기지도
digit_dino 설정 기준으로 force field 디코더조차 힘 라벨을 전혀 안 쓴다. ResNet-18 PoseEstimator가 프레임 간 모션을 추정하고, normal 맵을 disparity처럼 취급해 DIGIT 카메라 내참수로 역투영→재투영한 view synthesis 오차(SSIM 0.85 + L1 0.15) + edge-aware smoothness로 normal을, optical-flow warp의 photometric 손실로 shear를 학습한다. monodepth2의 자기지도 깊이 학습 레시피를 촉각으로 이식한 것이다.
여기서 체감 비교 때의 관찰 두 개가 바로 설명된다. (1) 무접촉 노이즈: normal이 photometric 재구성의 부산물이라 “접촉 없음 = 0”을 강제하는 항이 없다. (2) 절대값보다 상대 변화가 유효: sigmoid 0–1 출력에 물리 단위 앵커가 없다. 반대로 라벨이 필요 없으니 실물 DIGIT 데이터를 무한정 부어 학습할 수 있었고, 그게 실센서 안정성의 원천이다.
기타 헤드들은 가볍다: SlipProbe(토큰 풀링→MLP, 2클래스), ForceLinearProbe(AttentivePooler→3d 회귀, smooth L1), PoseLinearProbe(tx/ty/yaw 각 11-bin 분류) 등 — 백본이 좋으면 헤드는 선형에 가까워도 된다는 SSL 진영의 전형적 주장 구조다.
코드 위치
- 백본:
sparsh/tactile_ssl/model/vision_transformer.py:557(vit_base) - ForceField 디코더:
tactile_ssl/downstream_task/forcefield_sl.py:33, hooks:151 - DPT 레이어:
downstream_task/utils_forcefield/layers/Reassemble.py:105,Fusion.py:37,Head.py:67 - 자기지도 손실:
downstream_task/utils_forcefield/ssl_flow_loss.py:11 - 전처리:
tactile_ssl/data/digit/utils.py:48
5. SITR — 캘리브레이션 토큰으로 조건화되는 ViT
graph LR
A["샘플 RGB 3ch"] --> B["patch_embed\n→ cls+196 토큰"]
K["캘리브 18장\n54ch 스택"] --> L["c_patch_embed\nConv 54→768\n→ 196 토큰"]
B --> D["토큰 concat\n(393 토큰)"]
L --> D
D --> E["공유 ViT-B 12층\njoint self-attention"]
E --> F["캘리브 토큰\n슬라이스 제거"]
F --> G["Linear 768→768\n+ unpatchify"]
F --> I["contrastive head\n768→128, L2 norm"]
G --> H["surface normal map\n3×224×224"]
H --> J["ResNet18\n분류 16 / 포즈 3-DoF"]
인코더: “이 센서가 어떤 센서인지”를 토큰으로 주입
SITR의 핵심 질문은 “센서 광학이 제각각인데 어떻게 하나의 모델로 커버하나”이고, 답이 캘리브레이션 이미지의 토큰화다. 18장(배경 1 + 구 압입 9 + 큐브 압입 9)을 채널 축으로 스택해 54채널 텐서 하나로 만들고, 샘플용과 별개의 c_patch_embed(Conv 54→768)로 196개 토큰을 뽑는다. 이 캘리브 토큰들을 샘플 토큰 시퀀스(cls+196) 뒤에 concat해서 공유 12층 transformer를 함께 통과시킨 뒤, 끝에서 캘리브 토큰만 잘라 버린다.
cross-attention 모듈을 따로 두지 않고 joint self-attention 한 방으로 조건화한 것이 설계 포인트다. 샘플 패치가 attention으로 “우리 센서에서 구가 눌리면 이런 색이 나온다”는 참조 정보를 흡수하고, 흡수가 끝나면 캘리브 토큰은 소용을 다한다. 18장의 순서가 곧 채널 순서이므로 캘리브 배열은 항상 같은 규격(3×3 격자 압입)이어야 한다.
디코더: 화려한 인코딩, 소박한 디코딩
법선맵 헤드는 MAE식 단일 Linear(768→16²·3)를 토큰별로 적용하고 unpatchify하는 게 전부다. Sparsh·NeuralFeels가 4층 hook + Reassemble + Fusion으로 공을 들이는 것과 정반대. “인코더가 이미 센서 불변 표현을 만들었으면 픽셀 복원은 선형으로 충분하다”는 배팅이고, 실제로 통했다. 병렬로 contrastive_head(Linear 768→128 + L2 정규화)가 대조학습용 임베딩을 낸다.
분류·포즈는 2단계 구조다: frozen SITR이 어떤 센서 이미지든 표준화된 법선맵으로 “번역”하고, 그 위에서 평범한 ResNet-18이 일한다. 분류는 ResNet 특징 512d + cls 임베딩 투영을 concat해 16클래스, 포즈는 이미지 쌍의 법선맵 2장(6ch, conv1 교체)에서 3-DoF(Δx, Δy, Δyaw) 직접 회귀. 센서 불변성 문제를 인코더에 가두고 태스크 학습은 표준 CV로 환원한 깔끔한 관심사 분리다.
학습
SupConLoss(온도 0.07) — positive를 “같은 물체를 다른 시뮬 센서로 찍은 이미지”로 잡아, 표현에서 센서 정보를 지우도록 압박한다 — 에 법선맵 픽셀 지도손실을 더한다. 데이터는 Blender에서 젤 광학·FOV·거칠기·4방향 조명 색/강도를 무작위화한 가상 센서 무리로 생성(domain randomization). 실물 DIGIT은 이 무작위 분포의 한 표본으로 취급되어 zero-shot 전이가 성립한다. 단, 공개 레포는 eval 전용이라 학습 스크립트는 없다.
코드 위치
- 인코더/융합:
SITR/models/networks.py:89(SITR),:181(forward_encoder),:236(SITR_base) - 디코더:
models/networks.py:210, 분류:18, 포즈:45 - 손실:
models/losses.py:11(SupConLoss) - 데이터 생성기(vendored):
gs_blender/scripting.py(randomize),post_process.py(dmap→normal)
6. MidasTouch — 신경망 2개 + 확률 필터의 시스템 설계
graph TD
subgraph TDN["TDN — 깊이 추정 (FCRN)"]
A["DIGIT RGB\n240×320"] --> B["ResNet-50\n인코더"]
B --> C["UpProject ×4\n1024→512→256→128→64"]
C --> D["heightmap 1ch\n+ contact mask"]
end
subgraph TCN["TCN — 촉각 지문 (MinkLoc3D)"]
D --> E["역투영\n포인트클라우드 4096점"]
E --> F["MinkFPN\nsparse 3D conv"]
F --> G["GeM pooling\n→ 256d code"]
end
subgraph PF["파티클 필터"]
G --> H["codebook 조회\ncosine similarity"]
H --> I["SE(3) 파티클 50000\nbelief 갱신"]
J["6-DOF odometry"] --> I
end
TDN: ViT가 아니라 FCRN이었다
지난 노트에서 TDN을 막연히 NeuralFeels와 같은 계열로 생각했는데, 코드를 열어 보니 FCRN(Laina et al. 2016)이다 — ResNet-50 [3,4,6,3] 인코더 + 4단 UpProject 디코더(1024→512→256→128→64→1ch, 최종 320×240 bilinear 업샘플). NYU-Depth 실내 깊이 추정용으로 사전학습된 가중치에서 출발해 TACTO 시뮬 heightmap을 MSE로 회귀했다. TorchScript(jit.ScriptModule)로 짜여 있고 가중치 파일은 728MB(체크포인트 형식이라 순수 파라미터보다 큼).
전처리도 나머지와 다르다. 배경을 이미지에서 빼지 않고 per-image min-max 정규화만 하고, 배경 처리는 출력 단계에서 — 배경 heightmap 템플릿과의 diff에 quantile(0.8) 기반 threshold를 걸어 contact mask를 만드는 식으로 — 수행한다. 실센서에서는 heightmap을 시간축으로 지수가중 블렌딩(윈도 10)해 떨림을 누른다.
TCN: 깊이를 “장소 인식” 문제로 바꾸는 임베딩
TCN은 LiDAR place recognition에서 온 MinkLoc3D다. 마스크된 heightmap을 카메라 내참수로 역투영해 포인트클라우드로 만들고 → 4096점 리샘플 → [−1,1] 정규화 → sparse 양자화(1mm) → MinkFPN(sparse 3D conv, planes 32/64/64, FPN top-down 1단, lateral 256) → GeM pooling(학습되는 지수 p, 초기 3) → L2 정규화된 256d 촉각 지문 코드. 특징 입력은 전부 1인 더미 — 순수하게 접촉 기하의 모양만 인코딩한다. 학습은 BatchHard Triplet(margin 0.2): 가까운 접촉끼리 코드가 가깝도록 하는 메트릭 러닝이다.
파티클 필터: 신경망 밖에서 시간을 통합
50,000개의 SE(3) 파티클(4×4 행렬)이 물체 표면 위 접촉 위치의 belief를 표현한다. 모션 모델은 외부 odometry를 가우시안 노이즈(0.5°, 0.2mm)와 함께 우측 합성하고, 측정 모델은 각 파티클 위치에서 사전계산된 codebook(메시 표면 50,000 포즈 → 시뮬 렌더 → TDN→TCN 코드, pynanoflann KD-tree)의 최근접 코드와 라이브 코드의 cosine similarity를 softmax해 가중치로 삼는다. 표면에서 2mm 이상 이탈한 파티클 제거, 50프레임마다 DBSCAN 군집화, 분산 변화에 따른 파티클 수 annealing까지 — 고전 로보틱스 상태추정의 정석 구성이다.
설계 관점의 핵심: 다섯 모델 중 유일하게 시간 통합·불확실성 표현을 신경망 밖(파티클 필터)에 두고, 신경망은 “한 프레임 → 코드”라는 순수 함수로 좁혔다. 그래서 능력의 상한(전역 위치추정)이 가장 높지만, 대신 codebook·메시·odometry라는 시스템 의존성이 생긴다 — 지난 체감 비교에서 4위였던 이유가 구조에 그대로 있다.
코드 위치
- FCRN:
MidasTouch/midastouch/contrib/tdn_fcrn/fcrn.py:174, 래퍼tdn.py:28 - MinkLoc3D:
contrib/tcn_minkloc/minkloc.py:15, MinkFPNminkfpn.py:13, 클라우드→코드tcn.py:52 - 파티클 필터:
modules/particle_filter.py(모션:359, 유사도:449) - 메인 루프:
filter/filter.py:131, codebook:tactile_tree/build_codebook.py:32
7. NeuralFeels — DPT(ViT-S): 상대 깊이로 충분한 SLAM 프런트엔드
graph LR
A["DIGIT RGB\n→224×224, [−1,1]"] --> B["ViT-Small/16\nDINO init, 384d"]
B -->|"blocks 2,5,8,11"| C["Reassemble ×4\nCLS readout proj"]
C --> D["Fusion ×4\nRefineNet식"]
D --> E["HeadDepth\nConv→Sigmoid"]
E --> F["depth [0,1]"]
F --> G["bg 템플릿 diff\nquantile 마스킹"]
G --> H["미터 변환 → 3D 접촉점\n→ neural SDF"]
구조
촉각 프런트엔드는 정석 DPT다. timm의 vit_small_patch16_224.dino(384d, 12층, ImageNet DINO 초기화) 백본에서 블록 2, 5, 8, 11을 hook — Sparsh와 정확히 같은 위치다. Reassemble에서 Sparsh와 한 가지 다른 선택을 하는데, cls 토큰을 버리는 대신 모든 패치 토큰에 concat하고 Linear(768→384)로 접는 Read_projection 방식으로 전역 문맥을 살린다. 이후 resample_dim 128, Fusion(ResidualConvUnit ×2 + ×2 업샘플) ×4까지 Sparsh와 동일 구조, 머리만 다르다: HeadDepth = Conv3×3(128→64) → ×2 bilinear → Conv3×3(64→32) → ReLU → Conv1×1(32→1) → Sigmoid, [0,1] 단채널 깊이.
출력 [0,1]은 [0,255] heightmap으로 스케일 → 센서 해상도로 bicubic 복원 → depth_scale로 미터 변환된다. 접촉 마스크는 배경 heightmap 템플릿과의 diff에 quantile(0.9)×ratio(실센서 1.2) threshold를 걸어 만들고, depth×mask로 접촉 영역만 남겨 neural SDF(Instant-NGP 해시그리드 MLP + Theseus SE(3) 포즈 최적화)의 3D 샘플로 공급한다.
학습이 체감 문제의 답이었다
손실이 MiDaS의 Scale-and-Shift-Invariant loss다: 예측과 정답을 최소제곱으로 scale/shift 정렬한 뒤 masked MSE + 4-scale gradient 정칙화를 계산한다. 즉 이 네트워크는 애초에 절대 깊이를 맞추도록 학습되지 않았다. 상대 깊이(모양)만 맞으면 손실이 0이 될 수 있고, 절대 스케일은 뒷단의 depth_scale 상수와 SDF 최적화가 흡수해 준다는 설계다. 지난 체감 보고서에서 “포화되고 절대값이 모호하다”고 쓴 것의 절반은 TACTO 시뮬 도메인 갭이지만, 나머지 절반은 이 손실 선택의 당연한 귀결이다 — SLAM 파이프라인 안에서는 합리적 선택이, 프런트엔드만 떼어 단독 깊이 센서로 쓰려는 내 사용법과 어긋났던 것.
학습 데이터는 TACTO 시뮬 YCB 41종, Adam 이중 lr(백본 1.5e-5 / 디코더 1.5e-4). SITR 같은 광학 randomization 없이 frozen 배포되므로 실센서 갭은 그대로 노출된다.
코드 위치
- DPT:
neuralfeels/neuralfeels/contrib/tactile_transformer/dpt_model.py:19,reassemble.py:60,fusion.py:40,head.py:31 - 래퍼/전처리:
contrib/tactile_transformer/touch_vit.py:82 - SSI 손실:
contrib/tactile_transformer/loss.py:126 - 마스킹→SDF 공급:
contrib/tactile_transformer/tactile_depth.py:95,modules/sensor.py:665 - 설정:
scripts/config/main/touch_depth/vit.yaml
8. 횡단 비교 — 구조에서 읽히는 것들
8.1 같은 깊이 추정, 세 갈래 길
py3DCal(TouchNet), MidasTouch(TDN), NeuralFeels(DPT)는 모두 “RGB → 깊이”인데 설계가 3세대에 걸쳐 있다:
| py3DCal | MidasTouch TDN | NeuralFeels | |
|---|---|---|---|
| 계보 | 소형 FCN (2024, 촉각 전용) | FCRN (2016, 실내 깊이) | DPT (2021, 범용 dense 예측) |
| 해상도 전략 | 다운샘플 없음 | 1/32까지 줄였다 복원 | 패치 16 토큰화 후 복원 |
| 직접 예측 대상 | 기울기 (적분은 수학) | heightmap 직접 | 상대 깊이 (SSI) |
| 절대 스케일 근거 | 실측 캘리브 라벨 | 시뮬 heightmap 라벨 | 없음 (뒷단 상수) |
체감에서 py3DCal > NeuralFeels였던 것은 모델 용량(3.9M vs 22M+)과 무관하다. 접촉 패치는 국소 현상이라 전역 attention의 이득이 작고, 정확한 라벨과 절대 스케일 근거가 있는 쪽이 이긴다. DIGIT 젤 면적처럼 작고 구조가 일정한 도메인에서는 “커널 몇 층 + 좋은 캘리브 데이터”가 여전히 강하다.
8.2 DPT 디코더의 수렴 — Sparsh와 NeuralFeels는 형제
Sparsh forcefield 디코더와 NeuralFeels DPT는 hooks [2,5,8,11], Reassemble scales [4,8,16,32], resample_dim 128, ResidualConvUnit Fusion까지 동일하다(둘 다 Intel DPT/FocusOnDepth 계열 코드에서 유래). 차이는 셋뿐:
- Read 방식: Sparsh는 cls를 버림(
Read_ignore), NeuralFeels는 cls를 concat-투영(Read_projection) - 머리: NormalShearHead(sigmoid 1ch + tanh×20 2ch) vs HeadDepth(sigmoid 1ch)
- 백본과 학습: frozen ViT-B(촉각 SSL) + photometric 자기지도 vs 파인튜닝 ViT-S(ImageNet DINO) + SSI 지도
즉 두 모델의 체감 격차는 디코더 구조가 아니라 백본이 무엇을 보고 배웠는가(실물 촉각 vs ImageNet→TACTO)에서 왔다고 봐야 한다. 같은 그릇에 다른 물을 담은 셈이다.
8.3 조건화 전략 — “센서 특이성”을 어디서 처리하나
광학 촉각 센서는 개체마다 조명·젤이 미묘하게 달라서, 모든 모델이 어떤 식으로든 “지금 이 센서”를 조건화한다:
| 모델 | 조건화 수단 | 시점 |
|---|---|---|
| py3DCal | 좌표 채널 2ch (젤 위 위치별 조명) | 입력 |
| Sparsh | 배경 프레임을 6ch의 절반으로 | 입력 |
| SITR | 캘리브 18장 → 196 토큰, joint attention | 인코더 내부 |
| MidasTouch | 배경 heightmap과 diff | 출력 (마스크) |
| NeuralFeels | 배경 heightmap 템플릿과 diff | 출력 (마스크) |
SITR만 조건화가 학습된 연산(attention)이고 나머지는 고정 연산(빼기·붙이기)이다. 새 센서 개체에 대한 적응력이 SITR에서 가장 좋았던 게 우연이 아니다 — 조건화 용량 자체가 다르다. 반대로 py3DCal·Sparsh의 입력단 빼기는 공짜지만, 배경 이미지가 오래되면(젤 마모, 조명 드리프트) 성능이 함께 미끄러진다.
8.4 학습 패러다임 스펙트럼과 라벨 비용
지도 MSE 메트릭 러닝 대조 + 지도 자기지도
py3DCal ────── MidasTouch ────── SITR ────── Sparsh(SSL+photometric)
NeuralFeels(SSI)
높은 라벨 비용 ◄──────────────────────────────► 라벨 불필요
- py3DCal: 라벨이 가장 비싸다(3D 프린터 실측) — 대신 mm 절대값이라는 최고 품질 출력
- MidasTouch: 시뮬 라벨 + triplet. 라벨은 싸지만 codebook이라는 배포 비용으로 전가
- NeuralFeels: 시뮬 라벨 + 절대 스케일 포기(SSI). 라벨 비용을 손실 완화로 줄인 트레이드
- SITR: 시뮬 라벨 + domain randomization으로 실물 라벨 0장에 도전, 성공
- Sparsh: 실물 무라벨 대량 + photometric — 라벨 0으로 실물 도메인 정면 돌파
“시뮬 학습이라고 다 같지 않다”의 구조적 정체가 여기서 드러난다. NeuralFeels와 SITR 모두 시뮬 학습이지만, NeuralFeels는 TACTO 렌더 분포 하나에 과적합될 수 있는 반면 SITR은 학습 분포 자체가 “무작위 광학 센서들의 집합”이라 실물 DIGIT이 분포 안쪽에 들어온다. 게다가 SITR은 추론 시 캘리브 토큰이 잔여 갭을 한 번 더 흡수한다. randomization(학습 시) + 조건화(추론 시)의 이중 방어가 zero-shot 전이의 실체다.
8.5 출력 형태가 곧 용도
파라미터 수보다 중요한 게 출력의 물리적 지위다:
- 절대 물리량: py3DCal(mm) — 바로 제어에 쓸 수 있는 유일한 출력
- 상대량/정규화량: Sparsh(0–1 normal, ±20 shear), NeuralFeels([0,1] depth), SITR(normal map) — 후단 보정 또는 상대 변화 소비 전제
- 표현/코드: Sparsh latent(768d), SITR cls(128d), MidasTouch code(256d) — 다른 시스템의 입력
- 확률분포: MidasTouch SE(3) belief — 유일하게 불확실성을 내장
실용 조합으로 지난 노트에서 “Sparsh(힘) + py3DCal(깊이) + 필요시 SITR”을 꼽았는데, 구조를 보고 나니 근거가 더 분명하다: 셋은 백본도 조건화도 출력 지위도 겹치지 않는다. 같은 DIGIT RGB에서 서로 직교하는 정보를 뽑는 스택인 셈이다.
9. 정리
- TouchNet(py3DCal)은 “작아서 좋은” 게 아니라 “문제를 잘게 쪼개서” 좋다. 기울기까지만 학습하고 적분은 수학에, 위치 조건화는 좌표 채널에, 절대 스케일은 실측 라벨에 맡긴 분업 설계. 촉각처럼 도메인이 좁을 때 대형 백본 없이도 이길 수 있음을 보여준다.
- Sparsh와 NeuralFeels는 같은 DPT 디코더를 쓴다. 체감 1위와 5위의 차이는 디코더가 아니라 백본의 학습 데이터(실물 무라벨 대량 vs ImageNet→TACTO)와 손실(photometric vs SSI)에 있었다. “무엇을 배웠나”가 “어떻게 생겼나”를 압도한 사례.
- SITR의 zero-shot 전이는 randomization + 캘리브 토큰 조건화의 합작이다. 조건화를 학습된 attention으로 처리한 유일한 모델이고, 새 센서 적응이라는 내 문제의식에 구조적으로 가장 정면으로 답한다.
- NeuralFeels의 “절대값 모호”는 버그가 아니라 사양이다. SSI 손실은 SLAM 백엔드가 스케일을 흡수해 주는 전제의 선택이므로, 프런트엔드만 단독으로 쓰려면 손실부터 바꿔 재학습해야 한다 — 재학습을 하게 된다면 py3DCal식 실측 캘리브 라벨 + 절대 손실 조합이 자연스러운 출발점.
- MidasTouch는 신경망 설계보다 시스템 설계다. 시간·불확실성을 파티클 필터로 밀어내고 신경망을 순수 함수로 유지한 구조라, TDN/TCN만 최신 모델(예: py3DCal 깊이 + 더 나은 코드 임베딩)로 갈아 끼우는 개조가 오히려 쉬워 보인다.
10. 다음 단계 — NeuralFeels 깊이 프런트엔드를 어떻게 고칠 것인가
이 조사의 원래 타겟은 NeuralFeels(인핸드 자세·형상 추정)였고, 문제는 pretrained ViT의 실센서 깊이 정확도였다. 구조 분석을 마친 지금, 원인은 두 겹으로 정리된다: (1) 백본이 실물 촉각을 본 적이 없다(ImageNet DINO → TACTO 시뮬 frozen), (2) SSI 손실이라 애초에 절대 깊이를 학습하지 않았다. 해법도 이 두 겹을 각각 쳐야 한다.
10.1 “Sparsh DPT로 교체”가 왜 그대로는 성립하지 않는가
§8.2의 발견(“격차는 백본에서 왔다”)을 보면 Sparsh의 DPT를 이식하고 싶어지지만, 문자 그대로의 교체는 세 가지 이유로 무의미하다:
- 디코더는 이미 같다. hooks [2,5,8,11]·Reassemble·Fusion 동일 — 바꿔도 얻는 게 없다.
- Sparsh 디코더의 출력은 기하가 아니다. NormalShearHead의 normal 채널은 photometric 손실에서 disparity처럼 취급된 힘/눌림 맵이지, SDF가 요구하는 젤 표면 heightmap이 아니다. sigmoid [0,1]에 절대 스케일 앵커도 없어 “절대값 모호”가 그대로 남는다.
- 가중치 치수가 다르다. ViT-B(768d) ↔︎ ViT-S(384d), 백본 이식 불가.
성립하는 형태는 이것이다: Sparsh frozen 백본(실물 SSL) + NeuralFeels식 DPT depth 헤드를 새로 학습. 학습 대상이 ~86M 백본이 아니라 수 M짜리 헤드뿐이라, 원래 막혔던 “ViT 재학습이라는 큰 산”이 언덕이 된다. dpt_model.py는 embed_dim이 설정값이라 768 대응이 되고, 남는 배관은 Sparsh ViT vendoring(state_dict의 teacher_encoder.backbone 프리픽스 매핑)과 6채널 입력([프레임, 배경]) 연결이다.
10.2 라벨 문제의 답은 py3DCal
“실물 깊이 라벨을 어떻게 만드나”라는 원래 블로커에는 이제 답이 있다. py3DCal의 mm 깊이를 의사라벨(pseudo-label)로 쓰는 것이다. 지난 실험에서 py3DCal 출력을 신뢰할 수 있다고 결론냈으므로, 실물 DIGIT 프레임에 py3DCal 깊이를 붙여 헤드를 학습하면 — 실물 도메인 + 절대 스케일 + SSI가 아닌 절대 손실 — NeuralFeels의 약점 세 개를 한 번에 친다. 의사라벨의 상한(py3DCal의 오차를 물려받음)은 있지만, 백본의 표현력이 라벨 노이즈를 평균 내며 낯선 접촉 기하로 일반화해 주리라는 것이 기대 포인트다.
접합부도 이미 검증돼 있다. neuralfeels/contrib/touchnet/에 TactileDepth와 동일 인터페이스의 TouchNet 백엔드(TouchNetDepth) + build_tactile_depth() 팩토리 + 스케일 보정 스크립트(calibrate_touchnet_scale.py)가 만들어져 있어서, 0–255 intensity 규약·int64 캐스팅·부호 규약 같은 함정의 처리 패턴이 확보된 상태다. 새 백엔드는 같은 어댑터 패턴으로 들어가면 된다.
10.3 권장 로드맵
판단 기준을 먼저 세운다: 최적화할 지표는 깊이맵의 미관이 아니라 다운스트림 pose tracking 오차(feelsight_real 시퀀스의 drift/ADD-S)다. 백엔드를 갈아 끼우며 같은 시퀀스로 A/B 하는 평가 하네스가 0순위.
| 트랙 | 내용 | 비용 | 기대 |
|---|---|---|---|
| 0. 평가 하네스 | 같은 실물 시퀀스에서 깊이 백엔드별 pose 오차 비교 | 반나절 | 이후 모든 판단의 근거 |
| A. py3DCal 직결 | 기존 TouchNetDepth 어댑터의 스케일 보정을 마무리하고 NeuralFeels에 투입 | 며칠 (학습 0) | 실물+mm 절대값. 리스크: 구 압입 캘리브 도메인 vs 인핸드 모서리·다중 접촉 |
| B. Sparsh 백본 + depth 헤드 | frozen ViT-B 위에 DPT depth 헤드를 py3DCal 의사라벨(실물)로 학습 | 1–2주 | 본명. 실물 백본의 일반화 + 절대 스케일 라벨 |
| B′. 축소판 | 같은 레시피(의사라벨+절대 손실)를 기존 ViT-S에 파인튜닝 | B보다 저렴 | ViT-B가 실시간 예산(DIGIT ×4)을 초과할 때의 대안 |
| 참고 1 | Sparsh normal 필드를 스케일 보정해 heightmap처럼 꽂는 어댑터 | 반나절 (학습 0) | 백본 가설의 하한 확인. 힘≠기하라 기대치는 낮게 |
| 참고 2 | 같은 헤드를 TACTO sim 라벨로만 학습 | B와 병렬 | 의사라벨의 기여도 분리 (ablation) |
추천 순서는 0 → A → B. A가 이기면 그대로 쓰면 되고(가장 빠른 골인), A가 인핸드 접촉 도메인에서 흔들리면 그 실패 사례가 곧 B의 학습 데이터 수집 목록이 된다. B에서 ViT-B의 추론 비용(ViT-S의 ~4배, 손가락 4개 DIGIT 동시)이 실시간 루프를 깨면 B′로 후퇴한다 — 이 경우에도 §8.2의 교훈(“무엇을 배웠나가 어떻게 생겼나를 압도한다”)대로, 백본 크기보다 레시피(실물 데이터 + 절대 손실)가 본질이다.
이 노트의 레이어 수치·파일 경로는 2026-07-03 시점 각 레포의 로컬 체크아웃 기준이다. 실험 체감과의 대응은 이전 보고서를, 방법론 지형도는 자료조사 노트를 참조.