ComfyUI Node

Draw ViT Pose (V2)

Turn detected poses into the skeleton image your Wan-Animate sampler actually sees

By Code2Collapse·Created 8 months ago·Updated 8 days ago· 17
Draw ViT Pose (V2)
  • pose_data
  • face_images
  • pose_images
  • face_video
  • face_cfg_scale
  • critic_report_json
  • worst_aus_csv
  • overall_mae
width832
height480
retarget_padding16
body_stick_width-1
hand_stick_width-1
draw_headtrue
pose_draw_threshold0.30
draw_iristrue
draw_gazetrue
iris_radius4
gaze_arrow_len30
iris_min_confidence0.05
iris_colorwhite
face_cfg_scale1.0
enforce_512_facetrue
reference_expression_coeffs_json
segment_length77
top_k_aus10
apply_pose_edits_to_facewarp

Wan Animate is driven by a stick figure. The model's pose conditioning channel is a rendered skeleton image, and this node is what renders it. DrawViTPoseV2 takes the pose_data from PoseAndFaceDetectionV2 and draws the whole-body skeleton - bones, hands, face, even the iris and gaze arrows if you want them - onto a clean black canvas at your target latent resolution. Whatever you see in its pose_images output is, pixel for pixel, the pose the sampler is going to animate against. So get that skeleton right.

What you set

The required inputs are almost all self-explanatory, and two of them are the ones that matter:

  • width / height - render canvas in pixels, and they should match your sampler latent size (832×480 default). The pose is retargeted onto this canvas, so if it doesn't match, you get a skeleton that's the wrong size for the generation.
  • retarget_padding (16) - padding around the body bbox when fitting. Larger = more headroom so big motions don't clip the frame edges. Bump it to 32–64 if limbs get cut off.
  • pose_draw_threshold (0.3) - per-keypoint confidence for drawing. This is separate from the detection node's pose_threshold: detection stores everything, this decides what gets rendered. Lower to 0.1 if limbs keep disappearing from the output.
  • body_stick_width / hand_stick_width - both default to -1 (auto from canvas size). 0 on hands means don't draw them at all, which is handy if hand tracking is noisy.
  • draw_head - whether the face/head skeleton renders.

The genuinely interesting stuff lives in the optional inputs. face_images (wire in face_images_512 from the detection node) makes this node a one-stop shop: it validates frame-count parity, force-resizes to 512×512, and forwards the crop on its face_video output, so a single DrawViTPoseV2 can feed both the pose and face inputs of your Wan-Animate sampler. That's the wiring you actually want for a clean graph.

The outputs beyond the skeleton

pose_images is the main event. But there are two things here that no other pose-render node does:

face_video + face_cfg_scale - passthroughs. face_cfg_scale is connection-only, deliberately: it used to be a second editable widget that could silently diverge from the detection node's value, so the author made this one read-only. Unconnected it's 1.0, a no-op. And as the detection node's tooltip hammers home, Kijai's wrapper has no face-CFG input today - use WanVideoAnimateEmbeds.face_strength for real expression control.

The closed-loop critic. Wire reference_expression_coeffs_json from a source-video run of the detection node (with export_expression_coeffs=True) and this node will measure ARKit-52 blendshapes from its own pose_data - i.e. the generated side - and report per-AU error against the source. You get critic_report_json, worst_aus_csv and overall_mae (0.0 = perfect). It's a numeric fidelity signal instead of eyeballing frames, and it's zero cost when left unwired. It's overkill for most people, but if you're chasing a specific expression that keeps getting lost, it tells you which action unit to blame.

One knob that quietly matters

apply_pose_edits_to_face (default "warp") is the delivery mechanism for expression edits made upstream (Wan Face Controller 3D's expression dials, dragged landmarks, or the detection node's force_eyes_open). The reason it exists: Wan Animate's face conditioning is 100% pixel-driven - landmarks only place the crop, the LIA motion encoder reads raw crop pixels. So editing landmarks alone changes nothing visible; this node warps the actual face-crop pixels to match the edited landmarks. If you set force_eyes_open upstream and see no effect, check this is still on warp - with "off", your edits silently do nothing.

Install-wise it's just part of the pack: ComfyUI Manager (search "WanAnimatePreprocessV2") or git clone https://github.com/Code2Collapse/ComfyUI-WanAnimatePreprocessV2 into custom_nodes, then pip install -r requirements.txt and restart. No extra model downloads for this node itself - the heavy lifting happens in the detection node upstream.

CategoryWanAnimatePreprocess_V2

Inputs (21)

NameTypeDefaultDescription
pose_dataPOSEDATAFrom Pose and Face Detection (V2).
widthINT83264–2048Render canvas width (px). Match the sampler latent size.
heightINT48064–2048Render canvas height (px). Match the sampler latent size.
retarget_paddingINT160–512Padding (px) added around the body bbox when retargeting. Larger = more headroom for big motions.
body_stick_widthINT-1-1–20Body skeleton stick width in px. -1 = auto from canvas size.
hand_stick_widthINT-1-1–20Hand skeleton stick width in px. -1 = auto.
draw_headBOOLEANtrueDraw the head/face skeleton (eyes, nose, ears).
pose_draw_thresholdFLOAT0.300–1Per-keypoint score threshold for drawing.
draw_irisoptBOOLEANtrueDraw iris/pupil markers from MediaPipe iris_data.
draw_gazeoptBOOLEANtrueDraw gaze direction arrows from iris_data.
iris_radiusoptINT41–20Pupil circle radius in pixels.
gaze_arrow_lenoptINT304–200Length of gaze direction arrow in pixels.
iris_min_confidenceoptFLOAT0.050–1Skip iris frames whose detection confidence is below this.
iris_coloroptCOMBOwhiteColor of the drawn pupil; magenta gives strongest sampler signal.
face_imagesoptIMAGEOPTIONAL face crop IMAGE batch (typically the face_images_512 output of PoseAndFaceDetectionV2). When wired, the node validates frame-count parity with the pose batch, optionally force-resizes to 512x512, and forwards it on the 'face_video' output so a single DrawViTPoseV2 can feed the Wan-Animate sampler's pose+face inputs in one place.
face_cfg_scaleoptFLOAT1.01–10Passthrough face CFG scale, CONNECTION-ONLY (forceInput) so there is exactly one source of truth: PoseAndFaceDetectionV2.face_cfg_scale. It used to be a second independently-editable widget with the same default, so you could set 2.0 upstream, leave 1.0 here, and get no warning that they had diverged. Unconnected = 1.0 (no-op), which matches the old default. NOTE: Kijai's ComfyUI-WanVideoWrapper has no face-CFG input to wire this into today — for real control over expression adherence use WanVideoAnimateEmbeds.face_strength (spec 2.2's stronger, more direct block-scale lever) instead.
enforce_512_faceoptBOOLEANtrueIf True and 'face_images' is provided at a non-512 size, force-resize each frame to 512x512 (bilinear) before forwarding. Default True so the encoder always sees the trained input shape.
reference_expression_coeffs_jsonoptSTRINGWan-Animate spec 3.1 (closed-loop critic): wire in the 'expression_coeffs_json' output of a PoseAndFaceDetectionV2 run (export_expression_coeffs=True) on the SOURCE driving video. When non-empty, this node measures ARKit-52 blendshapes from ITS OWN pose_data.iris_data (i.e. the GENERATED Wan-Animate output side, since this node is downstream of the generation pass) and reports per-AU + per-segment error against the reference — a numeric fidelity signal instead of eyeballing frames. Leave empty to skip entirely (zero extra cost).
segment_lengthoptINT771–100000Frames per segment for the critic's worst-segment breakdown — match WanVideoAnimateEmbeds.frame_window_size (default 77) so segments line up with Wan-Animate's own splice boundaries (spec 2.5/3.5). Only used when reference_expression_coeffs_json is wired.
top_k_ausoptINT101–52How many worst-tracked AUs the critic reports, worst-first. Only used when reference_expression_coeffs_json is wired.
apply_pose_edits_to_faceoptCOMBOwarpExpression-edit DELIVERY (2026-07-24). When pose_data carries edited face landmarks (WanFaceController3DV2 expression dials / dragged landmarks) AND face_images is wired, 'warp' moves the ACTUAL face-crop pixels from the original landmark positions to the edited ones (same Delaunay piecewise-affine engine as FC3D's preview), so the Wan-Animate face encoder sees the edit. Without this, landmark edits only change the drawn skeleton — the photographic face crop stays neutral and the sampler follows the crop, i.e. your expression edits silently do nothing. No-op when landmarks are unedited (zero cost), so the default stays 'warp'.

Outputs (6)

NameTypeDescription
pose_imagesIMAGERendered skeleton IMAGE batch. Feed into your Wan 2.2 Animate sampler.
face_videoIMAGEPassthrough face IMAGE batch (512x512 if enforce_512_face). Empty single-frame zero tensor if 'face_images' was not wired.
face_cfg_scaleFLOATPassthrough face_cfg_scale (Wan-Animate paper Sec. 4.3). 1.0 = CFG off.
critic_report_jsonSTRINGWan-Animate spec 3.1 closed-loop critic report (JSON): per-AU mean-absolute-error, per-frame error curve, per-segment breakdown worst-first. '{}' when reference_expression_coeffs_json was not wired.
worst_aus_csvSTRINGCSV 'name,mae' for the top_k_aus worst-tracked AUs, worst first. Empty string when the critic did not run.
overall_maeFLOATMean of all per-AU MAE values (0.0 = perfect match to the reference). 0.0 when the critic did not run.