pablovela5620 Claude Fable 5 commited on
Commit
fd37678
·
1 Parent(s): e0fb844

Show the body mesh and project it onto the source clip

Browse files

The canonical world gains the translucent SMPL-X mesh (exoego pattern:
static faces with albedo alpha 90, streamed vertices) with the joints
and bones reading through it, and the moving source camera — pose
recovered per frame by a rigid Kabsch fit of the incam body onto the
canonical one, K_fullimg as its pinhole. The source video moves under
world/camera/video so the viewer reprojects the mesh and skeleton onto
the person natively (arkitscenes technique; world content listed
explicitly in the pane's contents). Result layout matches the approved
reference: wide 3D pane and source on top, six clean views in one row.
Median reprojection error against ViTPose observations: ~10 px at
704-pixel width, with a 1e-7 m rigid-fit residual.

Implemented by Codex against the reviewed spec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

fdanyone/rerun_streaming.py CHANGED
@@ -44,6 +44,7 @@ from fdanyone.viz import (
44
  log_body,
45
  log_result,
46
  log_smplx_params,
 
47
  log_status,
48
  log_video_stream,
49
  motion_blueprint,
@@ -53,7 +54,7 @@ from fdanyone.viz import (
53
 
54
  LOGGER: logging.Logger = logging.getLogger("fdanyone.app")
55
 
56
- APPLICATION_ID: str = "4danyone-rerun-v2"
57
  """Versioned per blueprint change: the viewer persists blueprints by app id,
58
  so a stale layout from an earlier deploy would otherwise shadow a new one."""
59
 
@@ -338,7 +339,7 @@ def _motion_hook(
338
  for index, box in enumerate(boxes.numpy()[: INFERENCE.num_frames]):
339
  set_frame_on_recording(recording, index, session.spec.fps)
340
  recording.log(
341
- "source/bboxes",
342
  rr.Boxes2D(
343
  array=box.reshape(1, 4), array_format=rr.Box2DFormat.XYXY, colors=BOX_COLOR
344
  ),
@@ -348,7 +349,7 @@ def _motion_hook(
348
  for index, frame in enumerate(keypoints.numpy()[: INFERENCE.num_frames]):
349
  set_frame_on_recording(recording, index, session.spec.fps)
350
  recording.log(
351
- "source/keypoints",
352
  rr.Points2D(positions=frame[:, :2], radii=6.0, colors=KEYPOINT_COLOR),
353
  )
354
  log_status(recording, f"motion: {stage}")
@@ -448,7 +449,7 @@ def _decode_source_stills(spec: RunSpec) -> dict[int, RgbFrame]:
448
  def source_phase(recording: rr.RecordingStream, spec: RunSpec) -> str:
449
  """Log the source clip on the frame timeline; CPU only."""
450
 
451
- log_video_stream(recording, "source/video", spec.video_path)
452
  log_status(recording, "prepare: source clip logged")
453
  return "Recovering motion with GVHMR."
454
 
@@ -500,8 +501,15 @@ def motion_phase(recording: rr.RecordingStream, session: Session) -> Iterator[st
500
 
501
  body: BodyMotion = load_body_motion(session.prepared.motion_dir, device="cpu")
502
  log_body(recording, body, session.spec.fps)
 
 
 
503
  log_smplx_params(recording, session.prepared.motion_dir, session.spec.fps)
504
- log_status(recording, "motion: SMPL-X body and parameters logged")
 
 
 
 
505
  yield "Generating six views."
506
 
507
 
@@ -561,11 +569,11 @@ def smoke_motion_phase(recording: rr.RecordingStream, session: Session) -> Itera
561
  set_frame_on_recording(recording, index, session.spec.fps)
562
  x0: float = 180.0 + 2.0 * index
563
  recording.log(
564
- "source/bboxes",
565
  rr.Boxes2D(array=[[x0, 300.0, x0 + 300.0, 1100.0]], array_format=rr.Box2DFormat.XYXY),
566
  )
567
  keypoints: Float[np.ndarray, "17 2"] = rng.uniform((x0, 350.0), (x0 + 300.0, 1050.0), (17, 2))
568
- recording.log("source/keypoints", rr.Points2D(keypoints))
569
  log_status(recording, f"[smoke] motion frame {index}")
570
  time.sleep(SMOKE_DELAY)
571
  yield f"[smoke] motion frame {index}"
 
44
  log_body,
45
  log_result,
46
  log_smplx_params,
47
+ log_source_camera,
48
  log_status,
49
  log_video_stream,
50
  motion_blueprint,
 
54
 
55
  LOGGER: logging.Logger = logging.getLogger("fdanyone.app")
56
 
57
+ APPLICATION_ID: str = "4danyone-rerun-v3"
58
  """Versioned per blueprint change: the viewer persists blueprints by app id,
59
  so a stale layout from an earlier deploy would otherwise shadow a new one."""
60
 
 
339
  for index, box in enumerate(boxes.numpy()[: INFERENCE.num_frames]):
340
  set_frame_on_recording(recording, index, session.spec.fps)
341
  recording.log(
342
+ "world/camera/video/bboxes",
343
  rr.Boxes2D(
344
  array=box.reshape(1, 4), array_format=rr.Box2DFormat.XYXY, colors=BOX_COLOR
345
  ),
 
349
  for index, frame in enumerate(keypoints.numpy()[: INFERENCE.num_frames]):
350
  set_frame_on_recording(recording, index, session.spec.fps)
351
  recording.log(
352
+ "world/camera/video/keypoints",
353
  rr.Points2D(positions=frame[:, :2], radii=6.0, colors=KEYPOINT_COLOR),
354
  )
355
  log_status(recording, f"motion: {stage}")
 
449
  def source_phase(recording: rr.RecordingStream, spec: RunSpec) -> str:
450
  """Log the source clip on the frame timeline; CPU only."""
451
 
452
+ log_video_stream(recording, "world/camera/video", spec.video_path)
453
  log_status(recording, "prepare: source clip logged")
454
  return "Recovering motion with GVHMR."
455
 
 
501
 
502
  body: BodyMotion = load_body_motion(session.prepared.motion_dir, device="cpu")
503
  log_body(recording, body, session.spec.fps)
504
+ reprojection_error: float = log_source_camera(
505
+ recording, body, session.prepared.motion_dir, session.spec.fps
506
+ )
507
  log_smplx_params(recording, session.prepared.motion_dir, session.spec.fps)
508
+ log_status(
509
+ recording,
510
+ f"motion: SMPL-X body, parameters, and source camera logged "
511
+ f"(median reprojection error {reprojection_error:.2f} px)",
512
+ )
513
  yield "Generating six views."
514
 
515
 
 
569
  set_frame_on_recording(recording, index, session.spec.fps)
570
  x0: float = 180.0 + 2.0 * index
571
  recording.log(
572
+ "world/camera/video/bboxes",
573
  rr.Boxes2D(array=[[x0, 300.0, x0 + 300.0, 1100.0]], array_format=rr.Box2DFormat.XYXY),
574
  )
575
  keypoints: Float[np.ndarray, "17 2"] = rng.uniform((x0, 350.0), (x0 + 300.0, 1050.0), (17, 2))
576
+ recording.log("world/camera/video/keypoints", rr.Points2D(keypoints))
577
  log_status(recording, f"[smoke] motion frame {index}")
578
  time.sleep(SMOKE_DELAY)
579
  yield f"[smoke] motion frame {index}"
fdanyone/viz.py CHANGED
@@ -17,11 +17,11 @@ if TYPE_CHECKING:
17
  import rerun as rr
18
  import rerun.blueprint as rrb
19
  import torch
20
- from jaxtyping import Float
21
 
22
  from fdanyone.motion.body import BodyMotion
23
 
24
- APPLICATION_ID = "4danyone"
25
  FRAME_TIMELINE = "frame"
26
  TIME_TIMELINE = "time"
27
 
@@ -113,7 +113,7 @@ def log_video_stream(recording: rr.RecordingStream, entity: str, video: Path) ->
113
 
114
 
115
  def log_body(recording: rr.RecordingStream, body: BodyMotion, fps: Fraction) -> None:
116
- """Log the posed SMPL-X joints and skeleton over the whole clip."""
117
 
118
  import numpy as np
119
  import rerun as rr
@@ -127,9 +127,18 @@ def log_body(recording: rr.RecordingStream, body: BodyMotion, fps: Fraction) ->
127
  recording.log(
128
  "world/body/skeleton", rr.LineStrips3D.from_fields(radii=0.008, colors=BONE_COLOR), static=True
129
  )
 
 
 
 
 
 
 
 
130
  for index in range(min(len(body.joints), INFERENCE.num_frames)):
131
  set_frame_on_recording(recording, index, fps)
132
  joints: Float[np.ndarray, "joints 3"] = body.joints[index]
 
133
  recording.log("world/body/joints", rr.Points3D.from_fields(positions=joints))
134
  recording.log(
135
  "world/body/skeleton",
@@ -137,6 +146,114 @@ def log_body(recording: rr.RecordingStream, body: BodyMotion, fps: Fraction) ->
137
  strips=np.stack([joints[[child, parent]] for child, parent in bones], axis=0)
138
  ),
139
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
 
142
  def log_smplx_params(recording: rr.RecordingStream, motion_dir: Path, fps: Fraction) -> None:
@@ -208,8 +325,8 @@ def motion_blueprint() -> rrb.Blueprint:
208
 
209
  return rrb.Blueprint(
210
  rrb.Horizontal(
211
- rrb.Spatial2DView(origin="source", contents=["source/video"], name="Source"),
212
- rrb.Spatial2DView(origin="source", name="Detections"),
213
  rrb.TextLogView(origin="log", name="Progress"),
214
  column_shares=[1.0, 1.0, 1.0],
215
  ),
@@ -247,16 +364,20 @@ def result_blueprint(fps: Fraction) -> rrb.Blueprint:
247
  rrb.Vertical(
248
  rrb.Horizontal(
249
  rrb.Spatial3DView(origin="world", name="Canonical world"),
250
- rrb.Spatial2DView(origin="source", contents=["source/video"], name="Source"),
 
 
 
 
 
251
  ),
252
- rrb.Grid(
253
  *(
254
- rrb.Spatial2DView(origin=f"world/cameras/dense/{index:02d}/image", name=f"View {index:02d}")
255
  for index in range(VIEWS)
256
  ),
257
- grid_columns=3,
258
  ),
259
- row_shares=[1.0, 1.0],
260
  ),
261
  rrb.TimePanel(
262
  timeline=TIME_TIMELINE,
 
17
  import rerun as rr
18
  import rerun.blueprint as rrb
19
  import torch
20
+ from jaxtyping import Bool, Float, Int
21
 
22
  from fdanyone.motion.body import BodyMotion
23
 
24
+ APPLICATION_ID: str = "4danyone-rerun-v3"
25
  FRAME_TIMELINE = "frame"
26
  TIME_TIMELINE = "time"
27
 
 
113
 
114
 
115
  def log_body(recording: rr.RecordingStream, body: BodyMotion, fps: Fraction) -> None:
116
+ """Log the posed SMPL-X mesh, joints, and skeleton over the whole clip."""
117
 
118
  import numpy as np
119
  import rerun as rr
 
127
  recording.log(
128
  "world/body/skeleton", rr.LineStrips3D.from_fields(radii=0.008, colors=BONE_COLOR), static=True
129
  )
130
+ recording.log(
131
+ "world/body/mesh",
132
+ rr.Mesh3D.from_fields(
133
+ triangle_indices=body.faces,
134
+ albedo_factor=(66, 135, 245, 90),
135
+ ),
136
+ static=True,
137
+ )
138
  for index in range(min(len(body.joints), INFERENCE.num_frames)):
139
  set_frame_on_recording(recording, index, fps)
140
  joints: Float[np.ndarray, "joints 3"] = body.joints[index]
141
+ vertices: Float[np.ndarray, "vertices 3"] = body.vertices[index]
142
  recording.log("world/body/joints", rr.Points3D.from_fields(positions=joints))
143
  recording.log(
144
  "world/body/skeleton",
 
146
  strips=np.stack([joints[[child, parent]] for child, parent in bones], axis=0)
147
  ),
148
  )
149
+ recording.log("world/body/mesh", rr.Mesh3D.from_fields(vertex_positions=vertices))
150
+
151
+
152
+ def log_source_camera(
153
+ recording: rr.RecordingStream,
154
+ body: BodyMotion,
155
+ motion_dir: Path,
156
+ fps: Fraction,
157
+ ) -> float:
158
+ """Log the moving source camera and return its median 2D joint error."""
159
+
160
+ import numpy as np
161
+ import rerun as rr
162
+ import smplx
163
+ import torch
164
+
165
+ from fdanyone.config import INFERENCE
166
+ from fdanyone.motion.body import NUM_SMPLX_SKELETON_JOINTS, _smplx_model_path
167
+ from fdanyone.motion.result import MotionResult
168
+
169
+ motion: MotionResult = MotionResult.load(motion_dir)
170
+ num_frames: int = min(motion.num_frames, len(body.joints), INFERENCE.num_frames)
171
+ body_model: torch.nn.Module = smplx.create(
172
+ model_path=str(_smplx_model_path()),
173
+ model_type="smplx",
174
+ gender="neutral",
175
+ num_betas=10,
176
+ num_pca_comps=12,
177
+ flat_hand_mean=False,
178
+ use_pca=True,
179
+ batch_size=motion.num_frames,
180
+ ).to("cpu")
181
+ parameters: dict[str, torch.Tensor] = motion.smpl_params_incam
182
+ with torch.inference_mode():
183
+ incam_joints_tensor: Float[torch.Tensor, "frames all_joints 3"] = body_model(
184
+ betas=parameters["betas"],
185
+ global_orient=parameters["global_orient"],
186
+ body_pose=parameters["body_pose"],
187
+ transl=parameters["transl"],
188
+ ).joints.detach()
189
+ incam_joints: Float[np.ndarray, "frames joints 3"] = (
190
+ incam_joints_tensor[:, :NUM_SMPLX_SKELETON_JOINTS].cpu().numpy().astype(np.float64)
191
+ )
192
+ # BodyMotion is load_body_motion's global-parameter pass after canonicalization.
193
+ canonical_joints: Float[np.ndarray, "frames joints 3"] = body.joints[:num_frames].astype(np.float64)
194
+ intrinsics: Float[np.ndarray, "frames 3 3"] = motion.K_fullimg[:num_frames].numpy().astype(np.float64)
195
+ observed_keypoints: Float[np.ndarray, "frames 17 3"] = (
196
+ motion.observed_keypoints_2d[:num_frames].numpy().astype(np.float64)
197
+ )
198
+ # COCO17 body order after the five facial keypoints.
199
+ coco_body_joints: Int[np.ndarray, "12"] = np.asarray(
200
+ [16, 17, 18, 19, 20, 21, 1, 2, 4, 5, 7, 8]
201
+ )
202
+ pixel_errors: list[float] = []
203
+
204
+ recording.log(
205
+ "world/camera",
206
+ rr.Pinhole(
207
+ image_from_camera=intrinsics[0],
208
+ resolution=(motion.image_width, motion.image_height),
209
+ camera_xyz=rr.ViewCoordinates.RDF,
210
+ image_plane_distance=0.35,
211
+ ),
212
+ static=True,
213
+ )
214
+ for index in range(num_frames):
215
+ source_joints: Float[np.ndarray, "joints 3"] = incam_joints[index]
216
+ target_joints: Float[np.ndarray, "joints 3"] = canonical_joints[index]
217
+ source_center: Float[np.ndarray, "3"] = source_joints.mean(axis=0)
218
+ target_center: Float[np.ndarray, "3"] = target_joints.mean(axis=0)
219
+ source_centered: Float[np.ndarray, "joints 3"] = source_joints - source_center
220
+ target_centered: Float[np.ndarray, "joints 3"] = target_joints - target_center
221
+ covariance: Float[np.ndarray, "3 3"] = source_centered.T @ target_centered
222
+ decomposition: tuple[
223
+ Float[np.ndarray, "3 3"], Float[np.ndarray, "3"], Float[np.ndarray, "3 3"]
224
+ ] = np.linalg.svd(covariance)
225
+ left_vectors: Float[np.ndarray, "3 3"] = decomposition[0]
226
+ right_vectors_t: Float[np.ndarray, "3 3"] = decomposition[2]
227
+ determinant_fix: Float[np.ndarray, "3"] = np.ones(3, dtype=np.float64)
228
+ orientation: float = float(np.linalg.det(left_vectors @ right_vectors_t))
229
+ determinant_fix[-1] = -1.0 if orientation < 0.0 else 1.0
230
+ camera_to_world_row: Float[np.ndarray, "3 3"] = (
231
+ left_vectors @ np.diag(determinant_fix) @ right_vectors_t
232
+ )
233
+ camera_to_world: Float[np.ndarray, "3 3"] = camera_to_world_row.T
234
+ translation: Float[np.ndarray, "3"] = target_center - source_center @ camera_to_world_row
235
+
236
+ set_frame_on_recording(recording, index, fps)
237
+ recording.log(
238
+ "world/camera",
239
+ rr.Transform3D(translation=translation, mat3x3=camera_to_world),
240
+ )
241
+
242
+ body_joints: Float[np.ndarray, "12 3"] = target_joints[coco_body_joints]
243
+ camera_joints: Float[np.ndarray, "12 3"] = (body_joints - translation) @ camera_to_world
244
+ pixels_homogeneous: Float[np.ndarray, "12 3"] = camera_joints @ intrinsics[index].T
245
+ projected_pixels: Float[np.ndarray, "12 2"] = (
246
+ pixels_homogeneous[:, :2] / pixels_homogeneous[:, 2:3]
247
+ )
248
+ observed_body: Float[np.ndarray, "12 3"] = observed_keypoints[index, 5:]
249
+ frame_errors: Float[np.ndarray, "12"] = np.linalg.norm(
250
+ projected_pixels - observed_body[:, :2], axis=1
251
+ )
252
+ visible: Bool[np.ndarray, "12"] = observed_body[:, 2] > 0.0
253
+ pixel_errors.extend(float(value) for value in frame_errors[visible])
254
+
255
+ median_error: float = float(np.median(np.asarray(pixel_errors, dtype=np.float64)))
256
+ return median_error
257
 
258
 
259
  def log_smplx_params(recording: rr.RecordingStream, motion_dir: Path, fps: Fraction) -> None:
 
325
 
326
  return rrb.Blueprint(
327
  rrb.Horizontal(
328
+ rrb.Spatial2DView(origin="world/camera", contents=["$origin/video"], name="Source"),
329
+ rrb.Spatial2DView(origin="world/camera", name="Detections"),
330
  rrb.TextLogView(origin="log", name="Progress"),
331
  column_shares=[1.0, 1.0, 1.0],
332
  ),
 
364
  rrb.Vertical(
365
  rrb.Horizontal(
366
  rrb.Spatial3DView(origin="world", name="Canonical world"),
367
+ rrb.Spatial2DView(
368
+ origin="world/camera",
369
+ contents=["$origin/**", "/world/body/**"],
370
+ name="Source",
371
+ ),
372
+ column_shares=[3.0, 1.0],
373
  ),
374
+ rrb.Horizontal(
375
  *(
376
+ rrb.Spatial2DView(origin=f"world/cameras/dense/{index:02d}/image")
377
  for index in range(VIEWS)
378
  ),
 
379
  ),
380
+ row_shares=[2.0, 1.0],
381
  ),
382
  rrb.TimePanel(
383
  timeline=TIME_TIMELINE,
tests/test_app_helpers.py CHANGED
@@ -14,6 +14,7 @@ from pathlib import Path
14
  import numpy as np
15
  import pytest
16
  import rerun as rr
 
17
  import torch
18
 
19
  os.environ.setdefault("FDANYONE_SKIP_LOAD", "1")
@@ -157,11 +158,37 @@ def test_pump_reraises_a_worker_failure_on_the_caller_thread() -> None:
157
 
158
 
159
  def test_blueprints_build_for_every_phase() -> None:
160
- """A malformed blueprint only fails when it is sent, so build all three."""
 
 
 
 
 
 
 
 
 
161
 
162
- assert viz.motion_blueprint() is not None
163
  assert viz.diffusion_blueprint() is not None
164
- assert viz.result_blueprint(Fraction(25, 1)) is not None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
 
167
  # ---------------------------------------------------------------------------
 
14
  import numpy as np
15
  import pytest
16
  import rerun as rr
17
+ import rerun.blueprint as rrb
18
  import torch
19
 
20
  os.environ.setdefault("FDANYONE_SKIP_LOAD", "1")
 
158
 
159
 
160
  def test_blueprints_build_for_every_phase() -> None:
161
+ """Blueprints expose every phase in the approved camera-centered layout."""
162
+
163
+ assert streaming.APPLICATION_ID == "4danyone-rerun-v3"
164
+ assert viz.APPLICATION_ID == "4danyone-rerun-v3"
165
+
166
+ motion: rrb.Blueprint = viz.motion_blueprint()
167
+ source: rrb.Spatial2DView = motion.root_container.contents[0] # pyrefly: ignore
168
+ detections: rrb.Spatial2DView = motion.root_container.contents[1] # pyrefly: ignore
169
+ assert (source.origin, source.contents) == ("world/camera", ["$origin/video"])
170
+ assert detections.origin == "world/camera"
171
 
 
172
  assert viz.diffusion_blueprint() is not None
173
+
174
+ result: rrb.Blueprint = viz.result_blueprint(Fraction(25, 1))
175
+ top: rrb.Horizontal = result.root_container.contents[0] # pyrefly: ignore
176
+ dense: rrb.Horizontal = result.root_container.contents[1] # pyrefly: ignore
177
+ world: rrb.Spatial3DView = top.contents[0] # pyrefly: ignore
178
+ result_source: rrb.Spatial2DView = top.contents[1] # pyrefly: ignore
179
+ assert result.root_container.row_shares == [2.0, 1.0]
180
+ assert top.column_shares == [3.0, 1.0]
181
+ assert (world.origin, world.name) == ("world", "Canonical world")
182
+ assert (result_source.origin, result_source.contents) == (
183
+ "world/camera",
184
+ ["$origin/**", "/world/body/**"],
185
+ )
186
+ assert type(dense).__name__ == "Horizontal"
187
+ assert tuple(view.origin for view in dense.contents) == tuple(
188
+ f"world/cameras/dense/{index:02d}/image" for index in range(viz.VIEWS)
189
+ )
190
+ assert result.time_panel.play_state == "playing"
191
+ assert result.time_panel.loop_mode == "all"
192
 
193
 
194
  # ---------------------------------------------------------------------------