Skip to content

Pose & keypoints

Keypoint detection finds the people in an image and where their body landmarks are. You pass in the pixels and get back a list: one entry per person, with a box and 17 landmarks, from the nose to the ankles.

createKeypointDetector runs YOLO26 pose by Ultralytics on the GPU.

import {
function createKeypointDetector(config?: KeypointDetectorModel, opts?: LoadOptions): Promise<KeypointDetector>
createKeypointDetector
,
function imageBufferFromImageData(img: ImageData): ImageBuffer
imageBufferFromImageData
,
const models: {
readonly embedding: {
readonly EMBEDDINGGEMMA_2: {
DEFAULT: {
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F16: {
dtype: "f16";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F32: {
dtype: "f32";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
};
readonly ALL_MINILM_L6_V2: {
DEFAULT: {
tokenizerPath: string;
} & {
modelPath: string;
};
F16: {
tokenizerPath: string;
} & {
dtype: "f16";
modelPath: string;
};
F32: {
tokenizerPath: string;
} & {
modelPath: string;
dtype: "f32";
};
};
};
... 6 more ...;
readonly imageClassification: {
readonly MOBILENETV4: {
readonly DEFAULT: {
modelPath: string;
};
readonly SMALL: {
modelPath: string;
};
};
};
}
models
} from 'runntime/zoo';
// 1. Load the model once. The weights download from the Hugging Face Hub.
const
const detector: KeypointDetector
detector
= await
function createKeypointDetector(config?: KeypointDetectorModel, opts?: LoadOptions): Promise<KeypointDetector>
createKeypointDetector
(
const models: {
readonly embedding: {
readonly EMBEDDINGGEMMA_2: {
DEFAULT: {
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F16: {
dtype: "f16";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F32: {
dtype: "f32";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
};
readonly ALL_MINILM_L6_V2: {
DEFAULT: {
tokenizerPath: string;
} & {
modelPath: string;
};
F16: {
tokenizerPath: string;
} & {
dtype: "f16";
modelPath: string;
};
F32: {
tokenizerPath: string;
} & {
modelPath: string;
dtype: "f32";
};
};
};
... 6 more ...;
readonly imageClassification: {
readonly MOBILENETV4: {
readonly DEFAULT: {
modelPath: string;
};
readonly SMALL: {
modelPath: string;
};
};
};
}
models
.
keypointDetection: {
readonly YOLO26_POSE: {
readonly DEFAULT: {
modelPath: string;
};
readonly N: {
modelPath: string;
};
readonly S: {
modelPath: string;
};
readonly M: {
modelPath: string;
};
};
}
keypointDetection
.
type YOLO26_POSE: {
readonly DEFAULT: {
modelPath: string;
};
readonly N: {
modelPath: string;
};
readonly S: {
modelPath: string;
};
readonly M: {
modelPath: string;
};
}
YOLO26_POSE
.
type DEFAULT: {
modelPath: string;
}
DEFAULT
);
// 2. Get the pixels of an image, here from a canvas the picture is drawn on.
const
const ctx: CanvasRenderingContext2D
ctx
=
const canvas: HTMLCanvasElement
canvas
.
HTMLCanvasElement.getContext(contextId: "2d", options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D | null (+5 overloads)
getContext
('2d')!;
const
const image: ImageBuffer
image
=
function imageBufferFromImageData(img: ImageData): ImageBuffer
imageBufferFromImageData
(
const ctx: CanvasRenderingContext2D
ctx
.
CanvasImageData.getImageData(sx: number, sy: number, sw: number, sh: number, settings?: ImageDataSettings): ImageData
getImageData
(0, 0,
const canvas: HTMLCanvasElement
canvas
.
HTMLCanvasElement.width: number
width
,
const canvas: HTMLCanvasElement
canvas
.
HTMLCanvasElement.height: number
height
));
// 3. Find the people.
const
const people: KeypointDetection[]
people
= await
const detector: KeypointDetector
detector
.
KeypointDetector.detectKeypoints(image: ImageBuffer, options?: DetectKeypointsOptions): Promise<KeypointDetection[]>
detectKeypoints
(
const image: ImageBuffer
image
);
// 4. Use the results.
for (const {
const confidence: number
confidence
,
const box: {
readonly format: "xyxy";
} & Readonly<{
xmin: number;
ymin: number;
xmax: number;
ymax: number;
}>
box
,
const landmarks: Readonly<Record<"nose" | "leftEye" | "rightEye" | "leftEar" | "rightEar" | "leftShoulder" | "rightShoulder" | "leftElbow" | "rightElbow" | "leftWrist" | "rightWrist" | "leftHip" | "rightHip" | "leftKnee" | "rightKnee" | "leftAnkle" | "rightAnkle", Landmark>>
landmarks
} of
const people: KeypointDetection[]
people
) {
var console: Console
console
.
Console.log(...data: any[]): void
log
(
const confidence: number
confidence
,
const box: {
readonly format: "xyxy";
} & Readonly<{
xmin: number;
ymin: number;
xmax: number;
ymax: number;
}>
box
,
const landmarks: Readonly<Record<"nose" | "leftEye" | "rightEye" | "leftEar" | "rightEar" | "leftShoulder" | "rightShoulder" | "leftElbow" | "rightElbow" | "leftWrist" | "rightWrist" | "leftHip" | "rightHip" | "leftKnee" | "rightKnee" | "leftAnkle" | "rightAnkle", Landmark>>
landmarks
.
nose: Landmark
nose
);
}
// 0.89 { format: 'xyxy', xmin: 48, ymin: 397, xmax: 241, ymax: 908 } { x: 144, y: 446, confidence: 0.99 }
// 0.87 { format: 'xyxy', xmin: 224, ymin: 404, xmax: 346, ymax: 868 } { x: 296, y: 448, confidence: 0.99 }
// ...
// 5. Free the GPU memory when done.
const detector: KeypointDetector
detector
.
KeypointDetector.dispose(): void
dispose
();

The example reads one picture off a canvas. A video or a camera works the same way: draw each frame on the canvas and call detectKeypoints again, the detector is loaded once.

The engine must be set up first, see Getting started. The model runs in half precision, so the device needs shader-f16.

Your camera, frame by frame through detectKeypoints() on your GPU. Limbs are drawn from detector.skeleton, landmarks under 0.5 confidence are left out. No camera? The sample picture goes through the same call.

detectKeypoints returns a list with one entry per person found, best first. Nobody found gives an empty list. Each entry is:

interface KeypointDetection {
box: { format: 'xyxy'; xmin: number; ymin: number; xmax: number; ymax: number };
confidence: number; // how sure the model is, 0 to 1
landmarks: Record<CocoLandmark, Landmark>; // 17 entries, 'nose' to 'rightAnkle'
}
interface Landmark {
x: number; // in pixels of the image you passed in
y: number;
confidence: number; // how sure the model is the landmark is visible, 0 to 1
}

Every person has all 17 landmarks. One the model cannot see, behind the body or outside the frame, still has a position, with a low confidence. Skip landmarks below about 0.5 when drawing.

detector.skeleton lists which landmarks connect, as pairs of names:

for (const
const person: KeypointDetection
person
of
const people: KeypointDetection[]
people
) {
for (const [
const a: "nose" | "leftEye" | "rightEye" | "leftEar" | "rightEar" | "leftShoulder" | "rightShoulder" | "leftElbow" | "rightElbow" | "leftWrist" | "rightWrist" | "leftHip" | "rightHip" | "leftKnee" | "rightKnee" | "leftAnkle" | "rightAnkle"
a
,
const b: "nose" | "leftEye" | "rightEye" | "leftEar" | "rightEar" | "leftShoulder" | "rightShoulder" | "leftElbow" | "rightElbow" | "leftWrist" | "rightWrist" | "leftHip" | "rightHip" | "leftKnee" | "rightKnee" | "leftAnkle" | "rightAnkle"
b
] of
const detector: KeypointDetector
detector
.
KeypointDetector.skeleton: readonly (readonly ["nose" | "leftEye" | "rightEye" | "leftEar" | "rightEar" | "leftShoulder" | "rightShoulder" | "leftElbow" | "rightElbow" | "leftWrist" | "rightWrist" | "leftHip" | "rightHip" | "leftKnee" | "rightKnee" | "leftAnkle" | "rightAnkle", "nose" | "leftEye" | "rightEye" | "leftEar" | "rightEar" | "leftShoulder" | "rightShoulder" | "leftElbow" | "rightElbow" | "leftWrist" | "rightWrist" | "leftHip" | "rightHip" | "leftKnee" | "rightKnee" | "leftAnkle" | "rightAnkle"])[]
skeleton
) {
const
const from: Landmark
from
=
const person: KeypointDetection
person
.
KeypointDetection.landmarks: Readonly<Record<"nose" | "leftEye" | "rightEye" | "leftEar" | "rightEar" | "leftShoulder" | "rightShoulder" | "leftElbow" | "rightElbow" | "leftWrist" | "rightWrist" | "leftHip" | "rightHip" | "leftKnee" | "rightKnee" | "leftAnkle" | "rightAnkle", Landmark>>
landmarks
[
const a: "nose" | "leftEye" | "rightEye" | "leftEar" | "rightEar" | "leftShoulder" | "rightShoulder" | "leftElbow" | "rightElbow" | "leftWrist" | "rightWrist" | "leftHip" | "rightHip" | "leftKnee" | "rightKnee" | "leftAnkle" | "rightAnkle"
a
];
const
const to: Landmark
to
=
const person: KeypointDetection
person
.
KeypointDetection.landmarks: Readonly<Record<"nose" | "leftEye" | "rightEye" | "leftEar" | "rightEar" | "leftShoulder" | "rightShoulder" | "leftElbow" | "rightElbow" | "leftWrist" | "rightWrist" | "leftHip" | "rightHip" | "leftKnee" | "rightKnee" | "leftAnkle" | "rightAnkle", Landmark>>
landmarks
[
const b: "nose" | "leftEye" | "rightEye" | "leftEar" | "rightEar" | "leftShoulder" | "rightShoulder" | "leftElbow" | "rightElbow" | "leftWrist" | "rightWrist" | "leftHip" | "rightHip" | "leftKnee" | "rightKnee" | "leftAnkle" | "rightAnkle"
b
];
if (
const from: Landmark
from
.
Landmark.confidence: number
confidence
< 0.5 ||
const to: Landmark
to
.
Landmark.confidence: number
confidence
< 0.5) continue;
const ctx: CanvasRenderingContext2D
ctx
.
CanvasPath.moveTo(x: number, y: number): void
moveTo
(
const from: Landmark
from
.
Point.x: number
x
,
const from: Landmark
from
.
Point.y: number
y
);
const ctx: CanvasRenderingContext2D
ctx
.
CanvasPath.lineTo(x: number, y: number): void
lineTo
(
const to: Landmark
to
.
Point.x: number
x
,
const to: Landmark
to
.
Point.y: number
y
);
}
}
const ctx: CanvasRenderingContext2D
ctx
.
CanvasDrawPath.stroke(): void (+1 overload)
stroke
();
function createKeypointDetector(
model?: KeypointDetectorModel,
options?: LoadOptions,
): Promise<KeypointDetector>;
interface KeypointDetector {
readonly landmarkNames: readonly CocoLandmark[];
readonly skeleton: readonly (readonly [CocoLandmark, CocoLandmark])[];
detectKeypoints(
image: ImageBuffer,
options?: DetectKeypointsOptions,
): Promise<KeypointDetection[]>;
dispose(): void;
}
interface DetectKeypointsOptions {
readonly confidenceThreshold?: number; // default 0.3
readonly maxDetections?: number; // default 300
}
type CocoLandmark =
| 'nose'
| 'leftEye'
| 'rightEye'
| 'leftEar'
| 'rightEar'
| 'leftShoulder'
| 'rightShoulder'
| 'leftElbow'
| 'rightElbow'
| 'leftWrist'
| 'rightWrist'
| 'leftHip'
| 'rightHip'
| 'leftKnee'
| 'rightKnee'
| 'leftAnkle'
| 'rightAnkle';
  • detectKeypoints(image, options?) returns the people in the image, best first. image is an ImageBuffer of any size. Build one with imageBufferFromImageData(imageData) from a canvas, or imageBuffer(bytes, width, height, format?) from raw rgba, rgb, bgra, bgr or gray bytes. confidenceThreshold drops people scored below it, maxDetections caps how many come back.
  • landmarkNames are the 17 names in model order, the same list as the exported COCO_LANDMARKS.
  • skeleton are the landmark pairs that form limbs, as name pairs.
  • dispose() frees the GPU memory.

YOLO26 picks one box per person on its own, so there is no IoU threshold.

The first argument is the model config, a plain object with the fields below. models.keypointDetection.YOLO26_POSE holds ready-made ones: N (6 MB, the DEFAULT), S (20 MB) and M (42 MB, more accurate, slower). Pass one as is, copy it with a field changed, or point at your own safetensors export of an Ultralytics YOLO26 pose checkpoint.

interface KeypointDetectorModel {
readonly modelPath?: ModelPath;
readonly variant?: 'n' | 's' | 'm' | 'l' | 'x';
readonly inputSize?: number; // default 640
readonly resizeMode?: 'letterbox' | 'stretch' | 'crop'; // default 'letterbox'
}
type ModelPath = string | RangeSource; // a URL, or bytes you already have

Examples:

import {
function createKeypointDetector(config?: KeypointDetectorModel, opts?: LoadOptions): Promise<KeypointDetector>
createKeypointDetector
,
const models: {
readonly embedding: {
readonly EMBEDDINGGEMMA_2: {
DEFAULT: {
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F16: {
dtype: "f16";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F32: {
dtype: "f32";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
};
readonly ALL_MINILM_L6_V2: {
DEFAULT: {
tokenizerPath: string;
} & {
modelPath: string;
};
F16: {
tokenizerPath: string;
} & {
dtype: "f16";
modelPath: string;
};
F32: {
tokenizerPath: string;
} & {
modelPath: string;
dtype: "f32";
};
};
};
... 6 more ...;
readonly imageClassification: {
readonly MOBILENETV4: {
readonly DEFAULT: {
modelPath: string;
};
readonly SMALL: {
modelPath: string;
};
};
};
}
models
} from 'runntime/zoo';
// The medium size.
await
function createKeypointDetector(config?: KeypointDetectorModel, opts?: LoadOptions): Promise<KeypointDetector>
createKeypointDetector
(
const models: {
readonly embedding: {
readonly EMBEDDINGGEMMA_2: {
DEFAULT: {
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F16: {
dtype: "f16";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F32: {
dtype: "f32";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
};
readonly ALL_MINILM_L6_V2: {
DEFAULT: {
tokenizerPath: string;
} & {
modelPath: string;
};
F16: {
tokenizerPath: string;
} & {
dtype: "f16";
modelPath: string;
};
F32: {
tokenizerPath: string;
} & {
modelPath: string;
dtype: "f32";
};
};
};
... 6 more ...;
readonly imageClassification: {
readonly MOBILENETV4: {
readonly DEFAULT: {
modelPath: string;
};
readonly SMALL: {
modelPath: string;
};
};
};
}
models
.
keypointDetection: {
readonly YOLO26_POSE: {
readonly DEFAULT: {
modelPath: string;
};
readonly N: {
modelPath: string;
};
readonly S: {
modelPath: string;
};
readonly M: {
modelPath: string;
};
};
}
keypointDetection
.
type YOLO26_POSE: {
readonly DEFAULT: {
modelPath: string;
};
readonly N: {
modelPath: string;
};
readonly S: {
modelPath: string;
};
readonly M: {
modelPath: string;
};
}
YOLO26_POSE
.
type M: {
modelPath: string;
}
M
);
// Faster, for a camera loop.
await
function createKeypointDetector(config?: KeypointDetectorModel, opts?: LoadOptions): Promise<KeypointDetector>
createKeypointDetector
({
...
const models: {
readonly embedding: {
readonly EMBEDDINGGEMMA_2: {
DEFAULT: {
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F16: {
dtype: "f16";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F32: {
dtype: "f32";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
};
readonly ALL_MINILM_L6_V2: {
DEFAULT: {
tokenizerPath: string;
} & {
modelPath: string;
};
F16: {
tokenizerPath: string;
} & {
dtype: "f16";
modelPath: string;
};
F32: {
tokenizerPath: string;
} & {
modelPath: string;
dtype: "f32";
};
};
};
... 6 more ...;
readonly imageClassification: {
readonly MOBILENETV4: {
readonly DEFAULT: {
modelPath: string;
};
readonly SMALL: {
modelPath: string;
};
};
};
}
models
.
keypointDetection: {
readonly YOLO26_POSE: {
readonly DEFAULT: {
modelPath: string;
};
readonly N: {
modelPath: string;
};
readonly S: {
modelPath: string;
};
readonly M: {
modelPath: string;
};
};
}
keypointDetection
.
type YOLO26_POSE: {
readonly DEFAULT: {
modelPath: string;
};
readonly N: {
modelPath: string;
};
readonly S: {
modelPath: string;
};
readonly M: {
modelPath: string;
};
}
YOLO26_POSE
.
type DEFAULT: {
modelPath: string;
}
DEFAULT
,
KeypointDetectorModel.inputSize?: number | undefined
inputSize
: 512,
KeypointDetectorModel.resizeMode?: ResizeMode | undefined
resizeMode
: 'crop',
});
// Your own copy of the file.
await
function createKeypointDetector(config?: KeypointDetectorModel, opts?: LoadOptions): Promise<KeypointDetector>
createKeypointDetector
({
KeypointDetectorModel.modelPath?: ModelPath | undefined
modelPath
: '/models/yolo26n-pose/model.safetensors' });
  • modelPath - the weights, a model.safetensors file. Default: models.keypointDetection.YOLO26_POSE.DEFAULT.modelPath.
  • variant - the model size, n, s, m, l or x. Read from the weights by default.
  • inputSize - model input width and height, a multiple of 32. Default 640. Smaller is faster and misses more small people.
  • resizeMode - how the image fits the model input. letterbox (default) pads, stretch distorts, crop cuts the centered square.

The second argument controls the download. Every field is optional.

interface LoadOptions {
cache?: WeightCache;
cacheId?: string;
onProgress?: (name: string, doneBytes: number, totalBytes: number) => void;
onBytes?: (chunkBytes: number) => void;
signal?: AbortSignal;
}

Example:

import {
function createKeypointDetector(config?: KeypointDetectorModel, opts?: LoadOptions): Promise<KeypointDetector>
createKeypointDetector
,
function createOpfsCache(namespace: string): Promise<WeightCache | undefined>
createOpfsCache
,
const models: {
readonly embedding: {
readonly EMBEDDINGGEMMA_2: {
DEFAULT: {
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F16: {
dtype: "f16";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F32: {
dtype: "f32";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
};
readonly ALL_MINILM_L6_V2: {
DEFAULT: {
tokenizerPath: string;
} & {
modelPath: string;
};
F16: {
tokenizerPath: string;
} & {
dtype: "f16";
modelPath: string;
};
F32: {
tokenizerPath: string;
} & {
modelPath: string;
dtype: "f32";
};
};
};
... 6 more ...;
readonly imageClassification: {
readonly MOBILENETV4: {
readonly DEFAULT: {
modelPath: string;
};
readonly SMALL: {
modelPath: string;
};
};
};
}
models
} from 'runntime/zoo';
const
const detector: KeypointDetector
detector
= await
function createKeypointDetector(config?: KeypointDetectorModel, opts?: LoadOptions): Promise<KeypointDetector>
createKeypointDetector
(
const models: {
readonly embedding: {
readonly EMBEDDINGGEMMA_2: {
DEFAULT: {
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F16: {
dtype: "f16";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
F32: {
dtype: "f32";
modelPath: string;
visionPath: string;
audioPath: string;
tokenizerPath: string;
};
};
readonly ALL_MINILM_L6_V2: {
DEFAULT: {
tokenizerPath: string;
} & {
modelPath: string;
};
F16: {
tokenizerPath: string;
} & {
dtype: "f16";
modelPath: string;
};
F32: {
tokenizerPath: string;
} & {
modelPath: string;
dtype: "f32";
};
};
};
... 6 more ...;
readonly imageClassification: {
readonly MOBILENETV4: {
readonly DEFAULT: {
modelPath: string;
};
readonly SMALL: {
modelPath: string;
};
};
};
}
models
.
keypointDetection: {
readonly YOLO26_POSE: {
readonly DEFAULT: {
modelPath: string;
};
readonly N: {
modelPath: string;
};
readonly S: {
modelPath: string;
};
readonly M: {
modelPath: string;
};
};
}
keypointDetection
.
type YOLO26_POSE: {
readonly DEFAULT: {
modelPath: string;
};
readonly N: {
modelPath: string;
};
readonly S: {
modelPath: string;
};
readonly M: {
modelPath: string;
};
}
YOLO26_POSE
.
type DEFAULT: {
modelPath: string;
}
DEFAULT
, {
LoadOptions.cache?: WeightCache | undefined
cache
: await
function createOpfsCache(namespace: string): Promise<WeightCache | undefined>
createOpfsCache
('my-app'),
LoadOptions.onProgress?: ((name: string, doneBytes: number, totalBytes: number) => void) | undefined
onProgress
: (
name: string
name
,
done: number
done
,
total: number
total
) =>
var console: Console
console
.
Console.log(...data: any[]): void
log
(`${
var Math: Math
Math
.
Math.round(x: number): number
round
((100 *
done: number
done
) /
total: number
total
)}%`),
});
  • cache - keeps the downloaded weights in the browser, so the next visit loads them without the network. createOpfsCache(name) is the built-in one.
  • onProgress - called while the weights upload to the GPU, with bytes done and bytes total. Use it for a progress bar.
  • signal - an AbortSignal. Abort it to cancel a load in progress.
ModelConfigLandmarksInputWeights
YOLO26 pose nmodels.keypointDetection.YOLO26_POSE.NCOCO-17640×640 (default)f16, 6 MB
YOLO26 pose smodels.keypointDetection.YOLO26_POSE.SCOCO-17640×640 (default)f16, 20 MB
YOLO26 pose mmodels.keypointDetection.YOLO26_POSE.MCOCO-17640×640 (default)f16, 42 MB

Your own export of the other sizes, s, l and x, loads through modelPath too.