File size: 6,912 Bytes
121869d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | """Live per-frame tactile features for the T3 / SITR ACT policies.
Produces the exact 3840-d vector (5 fingers x 768, finger order 0..4) that
`screw_t3_50k` / `screw_sitr5_50k` were trained on — same preprocessing and
constants as the offline encoders (tools/encode_t3.py, tools/encode_sitr.py).
Robot-loop usage:
from tools.live_features import LiveTactileEncoder
enc = LiveTactileEncoder("t3", device="cuda") # or "sitr"
feat = enc(frames5) # frames5: (5, H, W) uint8 grayscale, fingers 0..4
action = infer.step({"state": qpos22, "tactile": feat})
Third-party dependencies (NOT bundled):
t3 — repo + t3_medium weights, loaded through tactile_fusion's
load_baselines._load_t3_raw (set TACTILE_FUSION_ROOT if not default)
sitr — gsrl repo + checkpoints/SITR_B18.pth (set GSRL_ROOT if not default)
Self-test (compares against features stored in an episode HDF5):
python -m tools.live_features --kind t3 --episode episodes/screwing/episode_000_screwing_0.hdf5
python -m tools.live_features --kind sitr --episode episodes/screwing/episode_000_screwing_0.hdf5
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import numpy as np
import torch
import torch.nn.functional as F
TACTILE_FUSION_ROOT = os.environ.get(
"TACTILE_FUSION_ROOT", "/home/allenbi/projects_25/tactile_fusion")
GSRL_ROOT = os.environ.get("GSRL_ROOT", "/home/allenbi/projects_25/gsrl")
N_FINGERS = 5
FEAT_DIM = 768
# T3: per-dataset stats computed ONCE on the screwing corpus (encode_t3.py).
# Fixed at training time — never recompute at deployment.
T3_MEAN, T3_STD = 0.49611, 0.18685
# SITR: training-time stats from gsrl/dataloaders.py, applied on [0,1] input.
SITR_MEAN = (-1.2223, -1.8114, -1.7090)
SITR_STD = (11.7932, 12.7956, 13.6452)
def _strip_shadowing_paths():
"""tactile_act's `data/` package shadows tactile_fusion's — drop it before
importing tactile_fusion modules (same fix as encode_t3.py)."""
sys.path[:] = [p for p in sys.path
if "tactile_act" not in Path(p or ".").resolve().as_posix()]
for name in list(sys.modules):
if name == "data" or name.startswith("data."):
del sys.modules[name]
class LiveTactileEncoder:
def __init__(self, kind: str, device: str = "cuda"):
assert kind in ("t3", "sitr"), kind
self.kind = kind
self.device = torch.device(device)
if kind == "t3":
self._init_t3()
else:
self._init_sitr()
# ------------------------------------------------------------- T3
def _init_t3(self):
_strip_shadowing_paths()
if TACTILE_FUSION_ROOT not in sys.path:
sys.path.insert(0, TACTILE_FUSION_ROOT)
cwd = os.getcwd()
os.chdir(TACTILE_FUSION_ROOT) # relative third_party paths
try:
from load_baselines import _load_t3_raw # type: ignore
model, domain = _load_t3_raw(modality="9dtact")
finally:
os.chdir(cwd)
model = model.to(self.device).eval()
self._encoder = model.encoders[domain]
self._trunk = model.trunk
self._model = model # keep alive
m = torch.full((1, 3, 1, 1), T3_MEAN, device=self.device)
s = torch.full((1, 3, 1, 1), T3_STD, device=self.device)
self._norm = (m, s)
# ------------------------------------------------------------- SITR
def _init_sitr(self):
if GSRL_ROOT not in sys.path:
sys.path.insert(0, GSRL_ROOT)
from models.networks import SITR_base # type: ignore
model = SITR_base(num_calibration=0)
ckpt = torch.load(os.path.join(GSRL_ROOT, "checkpoints/SITR_B18.pth"),
map_location="cpu")
sd = ckpt.get("state_dict", ckpt)
model.load_state_dict(sd, strict=False)
self._model = model.to(self.device).eval()
m = torch.tensor(SITR_MEAN, device=self.device).view(1, 3, 1, 1)
s = torch.tensor(SITR_STD, device=self.device).view(1, 3, 1, 1)
self._norm = (m, s)
# ------------------------------------------------------------- common
def _preprocess(self, frames: np.ndarray) -> torch.Tensor:
"""(B, H, W) uint8 grayscale -> (B, 3, 224, 224) normalized float."""
x = torch.from_numpy(np.ascontiguousarray(frames)).to(self.device)
x = x.float() / 255.0
x = x.unsqueeze(1).expand(-1, 3, -1, -1)
x = F.interpolate(x, size=(224, 224), mode="bilinear",
align_corners=False)
m, s = self._norm
return (x - m) / s
@torch.no_grad()
def encode_frames(self, frames: np.ndarray) -> np.ndarray:
"""(B, H, W) uint8 -> (B, 768) float32."""
x = self._preprocess(frames)
if self.kind == "t3":
tokens = self._encoder(x)
tokens = self._trunk(tokens)
tokens = F.layer_norm(tokens, (tokens.shape[-1],))
out = tokens[:, 0, :]
else:
out = self._model.forward_encoder(x, c=None)[:, 0, :]
return out.float().cpu().numpy()
def __call__(self, frames5: np.ndarray) -> np.ndarray:
"""(5, H, W) uint8, fingers 0..4 -> (3840,) float32 (order preserved)."""
assert frames5.shape[0] == N_FINGERS, frames5.shape
return self.encode_frames(frames5).reshape(-1)
# ------------------------------------------------------------------ self-test
def _selftest(kind: str, episode: str, device: str, n_frames: int = 4) -> int:
import h5py
enc = LiveTactileEncoder(kind, device=device)
worst = 1.0
with h5py.File(episode, "r") as hf:
T = hf["/qpos"].shape[0]
idx = np.linspace(0, T - 1, n_frames).astype(int)
for t in idx:
frames5 = np.stack([hf[f"/images/raw_{f}"][t]
for f in range(N_FINGERS)])
live = enc(frames5).reshape(N_FINGERS, FEAT_DIM)
stored = np.stack([hf[f"/{kind}/{f}"][t] for f in range(N_FINGERS)])
cos = np.sum(live * stored, 1) / (
np.linalg.norm(live, axis=1) * np.linalg.norm(stored, axis=1))
worst = min(worst, float(cos.min()))
print(f"t={t:4d} cos(live, stored) per finger: "
+ " ".join(f"{c:.4f}" for c in cos))
ok = worst > 0.999
print(f"[selftest {kind}] worst cosine {worst:.5f} -> "
+ ("PASS" if ok else "FAIL (feature mismatch — do not deploy)"))
return 0 if ok else 1
if __name__ == "__main__":
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--kind", choices=["t3", "sitr"], required=True)
ap.add_argument("--episode", required=True)
ap.add_argument("--device",
default="cuda" if torch.cuda.is_available() else "cpu")
args = ap.parse_args()
sys.exit(_selftest(args.kind, args.episode, args.device))
|