Files
liyang ae28d55f81 Update to 2026-09-17 pipeline snapshot; add weights, L20 assets and recording via Git LFS
Source: RGB-D -> Dyn-HaMR -> L20 retargeting -> FoundationPose -> reference repair -> SPIDER,
documented in docs/PIPELINE_LATEST.md and docs/SETUP_AND_WEIGHTS.md. Adds FoundationPose and
nvdiffrast upstream snapshots, requirements/pipeline_venv.txt and the FoundationPose weight
manifest/downloader.

Assets (Git LFS): weights/ (WiLoR detector, HandFlow denoiser, UniDepth-L), FoundationPose
checkpoints, HaMeR checkpoint, Dyn-HaMR HMP model and BMC constraints, L20 URDF/meshes, the
20260915_171525 D405 recording and the two box CADs. MANO models are not redistributed
(third_party/hamer/_DATA/data/mano/README.txt). Environments, caches and run outputs excluded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:43:37 +08:00

81 lines
4.8 KiB
Python

"""Offline, rotation-safe smoothing and conservative visible-surface RGB-D fit."""
import sys,json
from pathlib import Path
import numpy as np
import torch,cv2
from scipy.spatial.transform import Rotation
from scipy.ndimage import gaussian_filter1d
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/'third_party/Dyn-HaMR/dyn-hamr'))
from body_model import MANO,run_mano
BASE=ROOT/'output/20260915_171525_dynhamr'; OUT=BASE/'corrected'; OUT.mkdir(exist_ok=True)
SRC=ROOT/'docs/20260915_171525'; meta=json.loads((SRC/'intrinsics.json').read_text())
a=dict(np.load(BASE/'optimization/prior/20260915_171525_000000_world_results.npz')); N=352
torch.set_num_threads(4)
model=MANO(model_path=str(ROOT/'third_party/Dyn-HaMR/_DATA/data/mano'),batch_size=704,pose2rot=True)
def geometry(d):
with torch.no_grad():
o=run_mano(model,*[torch.tensor(d[k]).float() for k in ['trans','root_orient','pose_body','is_right','betas']])
return o['joints'].numpy(),o['vertices'].numpy()
def smooth_rotation(v,sigma=1.2):
shape=v.shape; v=v.reshape(N,-1,3); out=np.empty_like(v)
for j in range(v.shape[1]):
r=Rotation.from_rotvec(v[:,j])
for t in range(N):
ix=np.arange(max(0,t-4),min(N,t+5)); w=np.exp(-.5*((ix-t)/sigma)**2)
out[t,j]=r[ix].mean(weights=w).as_rotvec()
return out.reshape(shape)
j0,v0=geometry(a); d={k:v.copy() for k,v in a.items()}
for b in range(2):
d['root_orient'][b]=smooth_rotation(a['root_orient'][b]); d['pose_body'][b]=smooth_rotation(a['pose_body'][b])
# Correct translations in world coordinates; left MANO translation has reflected X.
R=a['cam_R'][0]; ct=a['cam_t'][0]; K=np.array([meta[k] for k in ['fx','fy','cx','cy']])
vc=np.einsum('tij,btvj->btvi',R,v0)+ct[None,:,None,:]
wc=np.einsum('tij,btj->bti',R,j0[:,:,0])+ct[None,:,:]
raw=np.zeros((2,N)); valid=np.zeros((2,N),bool); counts=np.zeros((2,N),int)
cap=cv2.VideoCapture(str(SRC/'color.mp4'))
for t in range(N):
ok,c=cap.read(); assert ok
dep=cv2.imread(str(SRC/'depth'/f'{t:06d}.png'),-1)*meta['depth_scale_m']
skin=cv2.inRange(cv2.cvtColor(c,cv2.COLOR_BGR2YCrCb),np.array([0,133,77]),np.array([255,173,127]))>0
for b in range(2):
v=vc[b,t]; uv=np.rint(v[:,:2]/v[:,2,None]*K[:2]+K[2:]).astype(int); closest={}
for i in np.flatnonzero((v[:,2]>.1)&(uv[:,0]>=2)&(uv[:,0]<846)&(uv[:,1]>=2)&(uv[:,1]<478)):
x,y=uv[i]; key=(x//4,y//4)
if key not in closest or v[i,2]<v[closest[key],2]: closest[key]=i
errors=[]
for i in closest.values():
x,y=uv[i]
if not skin[y,x]:continue
patch=dep[y-1:y+2,x-1:x+2]; z=patch[(patch>.1)&(patch<.85)]
if len(z)>=5 and np.ptp(z)<.02:errors.append(np.median(z)-v[i,2])
counts[b,t]=len(errors)
if errors:
dz=np.median(errors); mad=np.median(np.abs(np.array(errors)-dz));raw[b,t]=dz
valid[b,t]=len(errors)>=25 and mad<.01 and abs(dz)<.025
cap.release()
# Only directly supported neighborhoods receive a depth update. No long-gap extrapolation.
shifts=np.zeros_like(raw)
for b in range(2):
numerator=gaussian_filter1d(raw[b]*valid[b],2); support=gaussian_filter1d(valid[b].astype(float),2)
shifts[b]=np.where(support>.6,numerator/np.maximum(support,1e-6),0)
shifts[b]=gaussian_filter1d(shifts[b],1)
delta_c=wc[b]/wc[b,:,2,None]*shifts[b,:,None]
delta_w=np.einsum('tji,tj->ti',R,delta_c)
delta_w[:,0]*=2*a['is_right'][b]-1
d['trans'][b]=gaussian_filter1d(a['trans'][b]+delta_w,1.5,axis=0)
j1,v1=geometry(d)
report={'frames':N,'method':'Rotation weighted means on SO(3), sigma 1.2 frames; translation Gaussian sigma 1.5 frames; conservative skin-depth residual <=25mm and MAD<10mm, support-weighted local correction','offline_uses_future_frames':True,'depth_fit_is_surface_estimate_not_ground_truth':True,'hands':{}}
for b in range(2):
side='right' if a['is_right'][b,0]>.5 else 'left'
def rms2(x):return float(np.sqrt(np.mean(np.sum(np.diff(x,n=2,axis=0)**2,axis=-1)))*1000)
deviation=np.linalg.norm(j1[b,:,0]-j0[b,:,0],axis=-1)*1000
angles=(Rotation.from_rotvec(a['root_orient'][b]).inv()*Rotation.from_rotvec(d['root_orient'][b])).magnitude()*180/np.pi
report['hands'][side]={'depth_accepted_frames':int(valid[b].sum()),'depth_shift_max_mm':float(abs(shifts[b]).max()*1000),'wrist_second_difference_rms_mm_before':rms2(j0[b,:,0]),'wrist_second_difference_rms_mm_after':rms2(j1[b,:,0]),'wrist_change_max_mm':float(deviation.max()),'wrist_change_p95_mm':float(np.percentile(deviation,95)),'root_change_max_deg':float(angles.max())}
assert np.isfinite(j1[b]).all() and deviation.max()<40 and angles.max()<15
phase=OUT/'prior';phase.mkdir(exist_ok=True)
np.savez_compressed(phase/'20260915_171525_000000_world_results.npz',**d)
np.savez_compressed(OUT/'depth_fit.npz',raw_shift=raw,accepted=valid,applied_shift=shifts,sample_count=counts)
np.savez_compressed(OUT/'comparison_joints.npz',before=j0,after=j1)
(OUT/'validation.json').write_text(json.dumps(report,indent=2));print(json.dumps(report,indent=2))