Nodes/YOLO-Pose Keypoints/YOLO-Pose Keypoints
ComfyUI Node

YOLO-Pose Keypoints

The ComfyUI node that outputs numbers, not pictures

By solanex·Created 3 months ago·Updated 3 months ago· 0
YOLO-Pose Keypoints
  • image
  • keypoints_json
model_name
confidence0.25

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 .pt in ComfyUI/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.

Categorykeypoints

Inputs (3)

NameTypeDefaultDescription
imageIMAGE
model_nameCOMBO1 options: <drop a .pt in models/yolo_pose>
confidenceFLOAT0.250–1

Outputs (1)

NameTypeDescription
keypoints_jsonSTRING