Angle Score
Catching the framing drift OKS can't see
- image
- reference_image
- angle_distance
- pass_mask
- info
- reasons
The CCIP and OKS scorers in the CCIP Judge pack handle "is it the right character" and "are the joints in the right place." Angle Score goes after the quieter failure: the character is right, the pose is roughly right, but the framing and proportions are off - head tilted down, shoulders squashed, torso too long. Those don't move keypoints far enough to fail OKS, but they make the batch look wrong.
Angle Score turns that into a number by building four geometric features from the DWPose skeleton and comparing them to the reference:
- distance from face to shoulder, divided by face width
- shoulder tilt
- torso length divided by shoulder width
- distance from nose to eye line, divided by eye width
The differences get squared and combined into an RMS distance, so it's a single scalar per image. Lower is closer to the reference, and the default pass threshold is 0.5 (the widget ranges 0–5, giving you room to be picky).
The honest caveat
The README is refreshingly straight about this: Angle Score is not an independent measurement from OKS. Both are derived from the same DWPose keypoints, and both get NaN when detection fails. Think of it as a second opinion on the same skeleton, tuned to composition and proportion rather than absolute joint positions - it catches the "same pose, wrong proportions" failure mode that OKS's joint-wise math is structurally blind to. Also, this is not a camera-angle estimation model. It's a lightweight geometric heuristic built on top of DWPose. That's fine for batch filtering; just don't expect it to tell you the camera was 15 degrees to the left.
The inputs that matter
image- your batch of generated images.threshold- default0.5, lower = stricter.reference_folder/reference_image- the reference pool,reference_imagewinning if both are connected.reference_pose_json- same as OKS Score: hand it a precomputed OpenPose BODY-18 JSON so the reference pose isn't re-estimated every run.fail_score- legacy compat widget, ignored. Same trap as OKS; setting it changes nothing.
Outputs are angle_distance (per-image float), pass_mask (per-image boolean), info (summary text), and reasons (why individual images failed - e.g. missing face or shoulder joints). NaN on any image that can't produce the needed joints, and since NaN compares false against everything, failed detection means failed image, full stop.
Installing
One clone covers all six nodes in the pack:
cd ComfyUI/custom_nodes
git clone https://github.com/o-ankomochi-o/comfyui-ccip-judge.git
cd comfyui-ccip-judge
python -m pip install -r requirements.txt
Or install via ComfyUI Manager by searching "CCIP Judge". Stick to Python 3.10–3.12 (3.13 fails the dependency install), and don't install both onnxruntime and onnxruntime-gpu. The DWPose model downloads from Hugging Face the first time you run any scorer.
Where people get burned
- Face-only close-ups. No shoulders, no torso, no shoulder tilt - most features come back
None, the score goesNaN, and a perfectly good portrait fails. If your batch is headshots, you probably want OKS withkeypoint_set="portrait"and to skip this node entirely. - Multiple people in frame. It scores the largest detected person, same as OKS.
- Because Angle needs both a face and shoulders, it's the most fragile of the three scorers. That's by design - a pose measurement without shoulders isn't a pose measurement - but it means you'll see more
NaNs than with CCIP.
Wire angle_distance into the pack's Three-Stage Filter alongside the other two scores. Used alone it's a composition filter; used with the others it's the thing that keeps a "consistent character" batch from being a batch of consistent characters in twelve different framings.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| threshold | FLOAT | 0.500–5 | — |
| reference_folder | STRING | — | |
| fail_score | FLOAT | 1.000–5 | — |
| reference_imageopt | IMAGE | — | |
| reference_pose_jsonopt | STRING | — |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| angle_distance | FLOAT | — |
| pass_mask | BOOLEAN | — |
| info | STRING | — |
| reasons | STRING | — |