diff --git a/comfy_extras/nodes_sam3d_body.py b/comfy_extras/nodes_sam3d_body.py index 77bad110d..9b57f7399 100644 --- a/comfy_extras/nodes_sam3d_body.py +++ b/comfy_extras/nodes_sam3d_body.py @@ -144,8 +144,7 @@ class SAM3DBody_Predict(io.ComfyNode): "bboxes", optional=True, force_input=True, tooltip=( "Per-frame person boxes (e.g. RT-DETR Detect with class_name='person'). " - "Used when no SAM3 track is wired — gives the top-down model a tight, " - "person-centered crop. Multi-person supported (one box = one person)." + "Use for better detection as alternative to SAM3 tracks." ), ), io.Boolean.Input( @@ -154,7 +153,7 @@ class SAM3DBody_Predict(io.ComfyNode): io.Float.Input( "fov_degrees", default=0.0, min=0.0, max=170.0, step=0.5, - tooltip=( + tooltip=( #TODO: get FoV from moge another way? "Vertical FOV in degrees. Affects predicted depth (cam_t.z) and " "absolute scale. 0 = use moge_geometry or fall back to ~53° (16:9). " "Any non-zero value overrides moge_geometry." @@ -164,7 +163,7 @@ class SAM3DBody_Predict(io.ComfyNode): "moge_geometry", optional=True, tooltip=( - "MoGe geometry (from MoGeInference), used to calculate camera field of view." + "MoGe geometry, used to calculate camera field of view." "For batches choose the most representative frame, or leave unset" ), ), @@ -345,10 +344,9 @@ class SAM3DBody_FaceExpression(io.ComfyNode): crop_factor = 1.2 # Auto-pick full-frame vs per-person crops. BlazeFace full-range needs - # ≥32px face in its 192px input; below that we escalate to per-person - # crops. Face height ≈ 20% of body-bbox height (rough but stable). + # ≥32px face in its 192px input; below that we escalate to per-person crops H_img0, W_img0 = img_np.shape[1], img_np.shape[2] - min_bbox_px = 32.0 * max(H_img0, W_img0) / (192.0 * 0.20) + min_bbox_px = 32.0 * max(H_img0, W_img0) / (192.0 * 0.20) # Face height ≈ 20% of body-bbox height. use_per_person_crops = any( (p["bbox"][3] - p["bbox"][1]) < min_bbox_px for persons in new_frames for p in persons @@ -400,10 +398,9 @@ class SAM3DBody_FaceExpression(io.ComfyNode): pbar.update(1) - # Baseline subtraction. MP has subject-specific rest bias (e.g. - # naturally-raised brow at 0.15); without subtraction, strength - # multipliers bake that into every frame. Per-clip needs ~30 frames - # or it would zero out the expression. + # Baseline subtraction. MP has subject-specific rest bias, without subtraction, strength + # multipliers bake that into every frame. + # Per-clip needs ~30 frames or it would zero out the expression. BASELINE_MIN_FRAMES = 30 if n_total_frames_with_persons >= BASELINE_MIN_FRAMES: for pid in range(max_persons): @@ -417,7 +414,7 @@ class SAM3DBody_FaceExpression(io.ComfyNode): f"got {n_total_frames_with_persons}. Skipping subtraction." ) - # Smooth raw signal AFTER baseline subtraction but BEFORE gap fill — + # Smooth raw signal AFTER baseline subtraction but BEFORE gap fill # MP's per-frame noise gets averaged out at the source. bs_win = int(blendshape_smooth_window) if bs_win > 1: @@ -484,9 +481,8 @@ class SAM3DBody_Smooth(io.ComfyNode): options=["gaussian", "savgol"], default="gaussian", advanced=True, tooltip=( - "'gaussian': symmetric weighted average — phase-preserving " - "(no time-shift), best general-purpose smoother. " - "'savgol': sliding polynomial fit — preserves sharp peaks " + "'gaussian': symmetric weighted average, best general-purpose smoother. " + "'savgol': sliding polynomial fit, preserves sharp peaks." ), ), io.Int.Input( @@ -498,10 +494,9 @@ class SAM3DBody_Smooth(io.ComfyNode): "rotation_threshold_deg", default=15.0, min=0.0, max=45.0, step=1.0, advanced=True, tooltip=( - "Geometry smoothing drops to RAW above this root-rotation " - "rate (deg/frame) to preserve fast spins. 15° suits most " - "content; low values trigger on ordinary jitter and " - "silently sabotage smoothing. 0 = disable backoff." + "Disables smoothing for this root-rotation rate (deg/frame) to preserve fast spins. " + "15° suits most content, low values trigger on ordinary jitter and " + "silently sabotage smoothing. 0 = disable." ), ), ], @@ -908,12 +903,10 @@ class SAM3DBody_Render(io.ComfyNode): ], tooltip=( "'mesh' = 3D MHR mesh rasterized through the camera. " - "'silhouette' = binary mask of the mesh (white-on-black, " - "background ignored). 'openpose_2d' = flat 2D skeleton " - "from pred_keypoints_2d (DWPose look, ControlNet-ready). " - "'openpose_3d' = same skeleton as flat-shaded 3D capsules " - "(camera-aware, proper depth). 'scail' = SCAIL 3D capsules " - "via torch SDF ray-march (proper occlusion / depth)." + "'silhouette' = binary mask of the mesh. " + "'openpose_2d' = flat 2D skeleton " + "'openpose_3d' = openpose skeleton as flat-shaded 3D model " + "'scail' = SCAIL 3D capsules " ), ), ], diff --git a/comfy_extras/nodes_save_3d.py b/comfy_extras/nodes_save_3d.py index 1f67f91cd..402c049ca 100644 --- a/comfy_extras/nodes_save_3d.py +++ b/comfy_extras/nodes_save_3d.py @@ -425,6 +425,21 @@ def rainbow_tilt_inputs(): ] +def camera_translation_input(): + """Shared camera_translation combo (BuildPoseGLB + SavePoseBVH).""" + return IO.Combo.Input( + "camera_translation", + options=["off", "centered", "absolute"], + default="off", + tooltip=( + "Bake pred_cam_t into the root's translation " + "'off' = bind position " + "'centered' = delta from frame 0 " + "'absolute' = raw (Z is camera depth — usually meters away)." + ), + ) + + class BuildPoseGLB(IO.ComfyNode): """Convert pose_data to an in-memory animated GLB""" @@ -438,24 +453,13 @@ class BuildPoseGLB(IO.ComfyNode): inputs=[ IO.MultiType.Input( "pose_data", types=[MHRPoseData, KimodoPoseData], - tooltip=( - "MHR pose data from SAM3DBody_Predict, or external-rig " - "pose data from Kimodo (`_skeleton_override`-augmented)." - ), + tooltip=("MHR pose data from SAM3DBody_Predict, Kimodo. "), ), SAM3DBodyModel.Input("sam3d_body_model", optional=True), IO.DynamicCombo.Input( "mesh_style", options=[ IO.DynamicCombo.Option("body_mesh", [ - IO.Int.Input( - "bone_smooth_window", - default=0, min=0, max=51, step=2, - tooltip=( - "Gaussian window on per-bone rotation keyframes. 0 = off. " - "7-15 helps cartwheels/spins where upstream Smooth misses spikes." - ), - ), IO.DynamicCombo.Input( "bone_vis", options=[ @@ -485,11 +489,7 @@ class BuildPoseGLB(IO.ComfyNode): ), ]), ], - tooltip=( - "Bone vis shape, rigidly skinned to each joint. " - "'octahedrons' = Blender-style directional bones (joint → " - "primary child); 'sticks' = thin lines." - ), + tooltip=("Bone vis shape, rigidly skinned to each joint. "), ), IO.DynamicCombo.Input( "shader", @@ -500,7 +500,7 @@ class BuildPoseGLB(IO.ComfyNode): IO.Float.Input( "person_palette_falloff", default=0.6, min=0.1, max=1.0, step=0.05, - tooltip="Per-person desaturation: track k gets (1 - falloff^k) pastel mix.", + tooltip="Per-person desaturation: each track gets (1 - falloff^k) pastel mix.", ), ]), IO.DynamicCombo.Option("rainbow_face_normal", [ @@ -508,7 +508,7 @@ class BuildPoseGLB(IO.ComfyNode): IO.Float.Input( "person_palette_falloff", default=0.6, min=0.1, max=1.0, step=0.05, - tooltip="Per-person desaturation: track k gets (1 - falloff^k) pastel mix.", + tooltip="Per-person desaturation: each track gets (1 - falloff^k) pastel mix.", ), ]), IO.DynamicCombo.Option("rainbow_face_semantic", [ @@ -516,7 +516,7 @@ class BuildPoseGLB(IO.ComfyNode): IO.Float.Input( "person_palette_falloff", default=0.6, min=0.1, max=1.0, step=0.05, - tooltip="Per-person desaturation: track k gets (1 - falloff^k) pastel mix.", + tooltip="Per-person desaturation: each track gets (1 - falloff^k) pastel mix.", ), ]), ], @@ -527,14 +527,6 @@ class BuildPoseGLB(IO.ComfyNode): ), ]), IO.DynamicCombo.Option("bones_only", [ - IO.Int.Input( - "bone_smooth_window", - default=0, min=0, max=51, step=2, - tooltip=( - "Gaussian window on per-bone rotation keyframes. 0 = off. " - "7-15 helps cartwheels/spins where upstream Smooth misses spikes." - ), - ), IO.DynamicCombo.Input( "bone_vis", options=[ @@ -571,14 +563,6 @@ class BuildPoseGLB(IO.ComfyNode): ), ]), IO.DynamicCombo.Option("openpose", [ - IO.Int.Input( - "bone_smooth_window", - default=0, min=0, max=51, step=2, - tooltip=( - "Gaussian window on keypoint tracks. 0 = off. " - "7-15 calms jitter where upstream Smooth misses spikes." - ), - ), IO.Float.Input( "marker_radius_m", default=0.010, min=0.005, max=0.1, step=0.001, advanced=True, tooltip="Sphere radius in m.", @@ -618,14 +602,6 @@ class BuildPoseGLB(IO.ComfyNode): ), ]), IO.DynamicCombo.Option("scail", [ - IO.Int.Input( - "bone_smooth_window", - default=0, min=0, max=51, step=2, - tooltip=( - "Gaussian window on keypoint tracks. 0 = off. " - "7-15 calms jitter where upstream Smooth misses spikes." - ), - ), IO.Float.Input( "stick_radius_m", default=0.022, min=0.002, max=0.1, step=0.001, advanced=True, tooltip=( @@ -667,28 +643,25 @@ class BuildPoseGLB(IO.ComfyNode): ]), ], tooltip=( - "'body_mesh' = real Armature (127 bones, skinning, TRS " - "keyframes, 72 face morphs; needs model). " + "'body_mesh' = real Armature (127 bones, skinning, TRS keyframes, 72 face morphs; needs model). " "'bones_only' = bone-shape primitives at each joint (preview armature). " "'openpose' = OpenPose-18 3D skeleton from keypoints " - "(no model needed). 'scail' = SCAIL 3D capsule rig (open " - "cylinders capped flush by joint spheres)." + "'scail' = SCAIL 3D capsule rig (open cylinders capped flush by joint spheres)." + ), + ), + IO.Int.Input( + "bone_smooth_window", + default=0, min=0, max=51, step=2, + tooltip=( + "Gaussian smoothing window on per-bone rotation keyframes / keypoint " + "tracks. 0 = off. 7-15 calms spins/jitter where upstream Smooth misses spikes." ), ), IO.Float.Input( "fps", default=24.0, min=1.0, max=240.0, step=1.0, tooltip="Animation frame rate.", ), - IO.Combo.Input( - "camera_translation", - options=["off", "centered", "absolute"], - default="off", - tooltip=( - "Bake pred_cam_t into per-track root translation. " - "'off' = origin; 'centered' = delta from frame 0; " - "'absolute' = raw (Z is camera depth — usually meters away)." - ), - ), + camera_translation_input(), IO.Int.Input( "track_index", default=-1, min=-1, max=15, tooltip="-1 = all tracks; ≥0 = single track.", @@ -698,7 +671,7 @@ class BuildPoseGLB(IO.ComfyNode): ) @classmethod - def execute(cls, pose_data, mesh_style, sam3d_body_model=None, fps=24.0, camera_translation="off", track_index=-1) -> IO.NodeOutput: + def execute(cls, pose_data, mesh_style, sam3d_body_model=None, bone_smooth_window=0, fps=24.0, camera_translation="off", track_index=-1) -> IO.NodeOutput: mesh_style = mesh_style or {"mesh_style": "body_mesh"} mode_key = mesh_style["mesh_style"] # `shader` is nested in body_mesh; absent for bones_only. @@ -730,7 +703,7 @@ class BuildPoseGLB(IO.ComfyNode): bone_vis_color = str(bone_vis_dict.get("bone_vis_color", "white")) glb_bytes = build_glb_skeletal( pose_data, sam3d_body_model, - bone_smooth_window=int(mesh_style.get("bone_smooth_window", 0)), + bone_smooth_window=int(bone_smooth_window), bone_vis=bone_vis, bone_vis_radius_m=bone_vis_radius_m, bone_vis_color=bone_vis_color, @@ -754,7 +727,7 @@ class BuildPoseGLB(IO.ComfyNode): face_marker_radius_m=float(mesh_style.get("face_marker_radius_m", 0.0)), palette="openpose", shape="ellipsoid", - bone_smooth_window=int(mesh_style.get("bone_smooth_window", 0)), + bone_smooth_window=int(bone_smooth_window), ) elif mode_key == "scail": # SCAIL rig: open cylinders capped flush by joint spheres (sphere @@ -781,7 +754,7 @@ class BuildPoseGLB(IO.ComfyNode): # inside of the open cylinders shades sensibly at grazing angles. material_roughness=float(mesh_style.get("material_roughness", 0.3)), material_double_sided=True, - bone_smooth_window=int(mesh_style.get("bone_smooth_window", 0)), + bone_smooth_window=int(bone_smooth_window), ) else: raise ValueError(f"BuildPoseGLB: unknown mesh_style {mode_key!r}") @@ -813,16 +786,7 @@ class SavePoseBVH(IO.ComfyNode): "fps", default=24.0, min=1.0, max=240.0, step=1.0, tooltip="Animation frame rate (BVH `Frame Time`).", ), - IO.Combo.Input( - "camera_translation", - options=["off", "centered", "absolute"], - default="off", - tooltip=( - "Bake pred_cam_t into the root's position channels. " - "'off' = bind position; 'centered' = delta from frame 0; " - "'absolute' = raw (Z is camera depth — usually meters away)." - ), - ), + camera_translation_input(), IO.Combo.Input( "units", options=["cm", "m"],