Keypoints¶
Collect named points — joints, landmarks, part locations — from a model that answers in a fixed 0-1000 space, and draw them back as dots for verification.
from annotools import KeypointObject, draw_keypoints, load_image, normalize_coordinates, preview
view = preview(load_image("photo.jpg"), max_width=768, max_height=768)
answer = [[420, 330], [500, 375]] # Gemini writes [y, x] in a 0-1000 space
points = normalize_coordinates(answer, 1000, 1000, axis_order="yx")
names = ["left_eye", "nose"]
overlay = draw_keypoints(
view,
[KeypointObject(point=point, label=name) for point, name in zip(points, names, strict=True)],
point_diameter=5,
)
points is [[0.33, 0.42], [0.375, 0.5]] — x first, whatever order the model used, because
axis_order describes the model's frame only. The base is 1000, 1000 even on a 4:3 image: the
0-1000 space is per axis, not a square pixel grid. point_diameter is in output pixels, so a
768 px view wants a larger dot than the default 3.
normalize_coordinates on the answer, then preview_image_keypoints:
{
"coordinates": [[420, 330], [500, 375]],
"base_width": 1000,
"base_height": 1000,
"axis_order": "yx"
}
{
"source": "photo.jpg",
"objects": [
{ "point": [0.33, 0.42], "label": "left_eye" },
{ "point": [0.375, 0.5], "label": "nose" }
],
"point_diameter": 5,
"max_width": 768,
"max_height": 768
}
The overlay returns the image plus the usual metadata with objects — the number of points
drawn — and grid when a grid object is passed. For a model that answers in pixels of the
preview instead, use its output_width / output_height as the base.
Then: preview_image_keypoints and
normalize_coordinates for the parameters;
draw_keypoints and
KeypointObject for the library contract.
Which points to ask for, how many correction rounds to allow, and how to catch a systematically
shifted skeleton are in
skills/localization-annotation-guide;
committing the accepted points is skills/sqlite-annotation-store.