YOLO-Pose Keypoints
The ComfyUI node that outputs numbers, not pictures
- image
- keypoints_json
Most ComfyUI nodes end in a picture: an image, a mask, a latent. YOLO-Pose Keypoints is the exception that proves the rule. Feed it an image and it hands you a JSON blob of detected pose keypoints - x/y coordinates and confidence scores for every detected instance - and then it stops. No preview, no image out. Just numbers on a wire.
That's not a limitation, it's the whole design. This node is built for the person whose workflow doesn't end inside ComfyUI: you're running an orchestrator script, you need 2D joint positions out of a frame to feed a solvePnP camera-pose solver or a UV bake, and you want them over the HTTP API without scraping a file out of an output folder. If that's not your use case, this node is probably not for you - most of the pose talk in the ComfyUI community is OpenPose, which injects a skeleton into a generation to control it. This is the reverse direction, and a much narrower niche.
How it works
Under the hood it's a thin wrapper around an ultralytics YOLO-Pose model. Your image (a ComfyUI IMAGE tensor) is converted to uint8, flipped RGB→BGR the way ultralytics expects, and pushed through .predict() with your confidence threshold. The model outputs per-instance bounding boxes and per-joint [x, y, conf] keypoints, and the node serializes them to JSON. It caches the loaded model per path, so a second run doesn't pay the load cost again.
The trick that makes it useful outside the graph: it's declared an output node, so ComfyUI always executes it and surfaces its text output in /history - the same "PreviewAny" pattern used for Florence bboxes. Your external code reads outputs[<node_id>].text[0] and gets the JSON with no sidecar file.
hist = requests.get(f"{COMFYUI_URL}/history/{prompt_id}").json()
text = hist[prompt_id]["outputs"][NODE_ID]["text"][0]
kp = json.loads(text)["instances"][0]["keypoints"]
The inputs that matter
There are only three, which is the point:
image- the still you're detecting on. Only frame 0 of a batch is used, so don't feed it a video expecting per-frame keypoints.model_name- a dropdown of every.ptinComfyUI/models/yolo_pose/. Empty until you put weights there; nothing ships with the pack.confidence- the detection threshold, default 0.25. Lower it to catch weak detections (and more false positives); raise it if you're getting junk.
The single output is keypoints_json, a STRING. Wire it to a Show Text node if you want to eyeball it in the graph, or just let the /history path carry it.
Install
Three steps, no registry needed:
cd ComfyUI/custom_nodes
git clone https://github.com/solanex/comfyui-yolo-pose
pip install -r comfyui-yolo-pose/requirements.txt # just: ultralytics
Restart ComfyUI (or refresh), then drop your trained pose weights into ComfyUI/models/yolo_pose/ - the folder is created on first load. That's the whole dependency story: one requirement, ultralytics. You can also find it in ComfyUI Manager by searching "YOLO-Pose Keypoints".
Where people get burned
The weights are yours. The dropdown will be a single placeholder (<drop a .pt in models/yolo_pose>) until you add a pose model. And it must be a pose model - a plain detection YOLO has no keypoint head, so you'll get boxes with empty keypoints lists and wonder what broke.
The Ultralytics license is real. The YOLO path is AGPL-3.0, and ultralytics has one demonstrated supply-chain compromise behind it (a cryptominer shipped in a poisoned December 2024 release that reached ComfyUI users through a detailing pack). For a node whose whole audience is people building product pipelines, that's worth knowing before you ship anything - MIT here doesn't launder the AGPL runtime underneath.
Don't trust instance zero. Instances come back sorted best-first by box confidence, but "best" isn't "the one you want." If there can be several people in frame, match against your downstream object (e.g. best overlap with a segmentation mask) rather than blindly taking index 0.
Also worth knowing: keypoints are in the input image's pixel coordinates, not normalized, and rounded to two decimals. If your pipeline assumes normalized 0–1 space, convert before you hit the solver - the README's own sample shows raw pixel values and expects you to handle semantics downstream. This node deliberately knows nothing about what the keypoints mean; that's your code's job.
Inputs (3)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| model_name | COMBO | 1 options: <drop a .pt in models/yolo_pose> | |
| confidence | FLOAT | 0.250–1 | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| keypoints_json | STRING | — |