poser-go

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 8, 2026 License: GPL-3.0

README

poser-go

Reusable Go library for pose normalization, similarity scoring, and mirror-aware ranking — the canonical Go home for poser's BONE pose-similarity math. Landmarks in, results out: extraction (image → 33 landmarks) is MediaPipe and stays in Python; poser-go consumes landmarks, it does not produce them.

Ships two ways from one codebase:

  • a Go library — import pose and search, no network cost;
  • a self-hosted HTTP server (poserd) so non-Go consumers (Python, JS, anything) use the same scoring/ranking over HTTP.

The server is a thin binary over the library — no logic lives only in the server.

pose and search are pure and deterministic: no I/O, no globals, no network, standard library only. Parity with the Python implementation is enforced by a golden fixture at 1e-9 (see Parity).

Status

v0.x — the exported API may change until Suratea has run it in production; then v1.0 freezes it.

Install

go get codeberg.org/datuch/poser-go

Module path: codeberg.org/datuch/poser-go. Requires Go 1.26+.

Library usage

import (
    "codeberg.org/datuch/poser-go/pose"
    "codeberg.org/datuch/poser-go/search"
)

// 1. Normalize raw MediaPipe landmarks (len 33) into a 66-float feature vector.
query := pose.Normalize(queryLandmarks)

// 2. Score two poses directly (result in [0,1], 1 == identical).
s := pose.DefaultScorer()
similarity := s.Score(query, other)

// 3. Rank a candidate set, mirror-aware, top-k, self-excluded.
cands := []search.Candidate{
    {ID: "img-1", Features: pose.Normalize(lms1)},
    {ID: "img-2", Features: pose.Normalize(lms2)},
}
results := search.RankLandmarks(s, queryLandmarks, cands, search.Options{
    Limit:     20,
    Mirror:    true,       // also match the horizontally-flipped pose
    ExcludeID: "img-self", // e.g. the query's own stored pose
})
// results is sorted by descending Similarity.

Packages:

Package Purpose Deps
pose types, Normalize, Mirror, Scorer/DefaultScorer stdlib only
search stateless Rank / RankLandmarks over candidates pose
extract optional HTTP client for a poser /landmarks svc stdlib only
httpapi stateless HTTP handlers over the above stdlib only

A consumer that only needs a score imports pose and pulls no ranking or HTTP code. Extraction is a separate extract package, so pure-landmark consumers take no net/http client dependency.

Custom scorer

DefaultScorer() is poser's golden-verified configuration. Every knob is exported and overridable:

s := pose.DefaultScorer()
s.LandmarkWeights[pose.NumLandmarks-1] = 0 // ignore a landmark
s.BoneBlend = 0.5                          // 50/50 bone vs. angle
// s.Bones, s.AngleChains are also replaceable.

Bone weights are derived as the mean of a bone's two endpoint landmark weights; each angle chain's weight is its middle (vertex) landmark's weight — so adjusting LandmarkWeights reweights bones and angles consistently.

Server usage

Build and run the static binary:

go build ./cmd/poserd
./poserd -listen 127.0.0.1:8080
# with extraction passthrough to a poser instance:
./poserd -listen 127.0.0.1:8080 -poser-url http://poser:8000

Flags mirror env vars: POSERD_LISTEN, POSERD_POSER_URL. A Docker image and a docker-compose.example.yml (poserd in front of a private poser instance) are included.

httpapi.Handler(cfg) is importable — a Go app can mount it under its own mux. All endpoints are stateless; the caller supplies candidates per request, so one process serves many independent corpora.

Endpoints
Method & path Request Response
GET /healthz {"status":"ok"}
POST /normalize {"landmarks":[33]} {"features":[66]}
POST /mirror {"landmarks":[33]} {"landmarks":[33]}
POST /score {"a":[66],"b":[66]} {"similarity":0.9}
POST /rank {"query":[33],"candidates":[…],"options":{…}} {"results":[…]}
POST /extract multipart file {"landmarks":[33]}
POST /search-image multipart file + candidates,options fields {"results":[…]}

† only mounted when -poser-url is set.

candidates are {"id":…,"features":[66]}; options are {"limit":int,"mirror":bool,"exclude_id":string}. Errors use a JSON envelope {"error":"…"} with status codes: 400 bad body, 422 no pose detected in an image, 503 extraction service unavailable.

curl -s -X POST localhost:8080/score \
  -d '{"a":[...66 floats...],"b":[...66 floats...]}'
# {"similarity":0.87}

Algorithm spec

This section defines the behavior independently of any implementation. Together with the golden fixture it is the source of truth — neither Python nor Go is authoritative.

Input. 33 MediaPipe Pose landmarks, each {x, y, z, visibility}. x/y are image-normalized to [0,1]; z is unused by scoring; visibility is occlusion confidence in [0,1].

Normalization → 66-float feature vector

For landmarks L[0..32], using only x/y:

  1. Center on the hip midpoint. With left hip = index 23, right hip = 24: cx = (x[23]+x[24])/2, cy = (y[23]+y[24])/2; subtract (cx, cy) from every landmark.
  2. Scale by torso size. torso = hypot(x[11]-x[24], y[11]-y[24]) (left shoulder to right hip). If torso < 1e-6, fall back to shoulder width hypot(x[11]-x[12], y[11]-y[12]). Then scale = torso + 1e-8.
  3. Weight by visibility. For each landmark i, emit (x[i]/scale · vis[i], y[i]/scale · vis[i]).

The result is 66 floats, interleaved x,y per landmark, in landmark index order.

Mirror

Horizontal flip without re-running inference: set x → 1-x for every landmark, then swap these left/right index pairs:

(1,4) (2,5) (3,6) (7,8) (9,10) (11,12) (13,14) (15,16)
(17,18) (19,20) (21,22) (23,24) (25,26) (27,28) (29,30) (31,32)

Mirror operates on raw landmarks; normalize afterward for a mirrored feature vector.

BONE similarity score

Given two feature vectors, the score is a weighted blend of bone-direction agreement and joint-angle agreement, in [0,1]:

score = 0.6 · boneTerm + 0.4 · angleTerm

Bone term. For each of the 14 bones (a,b) below, take the direction vectors dir = (p[b]-p[a]), normalized to unit length when non-degenerate (length > 1e-8; otherwise left as the raw difference). Per-bone similarity is (dot(dir1, dir2) + 1) / 2 (cosine remapped to [0,1]). boneTerm is the weighted mean over bones; a bone's weight is (w[a]+w[b])/2.

14 bones (landmark index pairs):

(11,13) (13,15)   left arm            (12,14) (14,16)   right arm
(23,25) (25,27)   left leg            (24,26) (26,28)   right leg
(11,23) (12,24)   torso sides         (11,12) (23,24)   shoulder & hip lines
(0,11)  (0,12)    neck/head

Angle term. For each of the 10 joint chains (j1,j2,j3) below, measure the angle at the vertex j2 (in radians, via acos of the clamped cosine; 0 when either segment is degenerate, length < 1e-8). Per-chain similarity is 1 - |angle1 - angle2| / π. angleTerm is the weighted mean; a chain's weight is w[j2].

10 angle chains:

(11,13,15) (12,14,16)   elbows
(23,25,27) (24,26,28)   knees
(11,23,25) (12,24,26)   hip flexion
(12,11,13) (11,12,14)   shoulder line vs. upper arm
(24,23,25) (23,24,26)   hip line vs. thigh
Landmark weights

w[i], from poser's landmark_weights.yaml (higher = more salient for pose matching):

idx landmark w idx landmark w
0 nose 5 17 left_pinky 1
1 left_eye_inner 1 18 right_pinky 1
2 left_eye 1 19 left_index 1
3 left_eye_outer 1 20 right_index 1
4 right_eye_inner 1 21 left_thumb 1
5 right_eye 1 22 right_thumb 1
6 right_eye_outer 1 23 left_hip 8
7 left_ear 1 24 right_hip 8
8 right_ear 1 25 left_knee 9
9 mouth_left 1 26 right_knee 9
10 mouth_right 1 27 left_ankle 7
11 left_shoulder 10 28 right_ankle 7
12 right_shoulder 10 29 left_heel 3
13 left_elbow 8 30 right_heel 3
14 right_elbow 8 31 left_foot_index 1
15 left_wrist 11 32 right_foot_index 1
16 right_wrist 11

Parity / regeneration

Behavior parity with the Python poser is enforced by a golden fixture and the conformance test; a diff means the two implementations have diverged. See docs/parity.md for the schema and how to regenerate.

Design

docs/design.md — architecture, boundaries, and locked decisions.

License

GPL-3.0-or-later. See LICENSE.

Directories

Path Synopsis
cmd
poserd command
Command poserd is a static HTTP server exposing poser-go's stateless pose scoring and ranking (and, when a poser instance is configured, an extraction passthrough).
Command poserd is a static HTTP server exposing poser-go's stateless pose scoring and ranking (and, when a poser instance is configured, an extraction passthrough).
Package extract is an optional HTTP client for a poser (https://codeberg.org/datuch/poser) instance, used purely as a stateless landmark-extraction service (image -> 33 landmarks).
Package extract is an optional HTTP client for a poser (https://codeberg.org/datuch/poser) instance, used purely as a stateless landmark-extraction service (image -> 33 landmarks).
Package httpapi is a stateless HTTP surface over pose + search (with an optional extract passthrough).
Package httpapi is a stateless HTTP surface over pose + search (with an optional extract passthrough).
Package pose is the pure, deterministic core of poser-go: pose landmark types, torso-centered normalization, horizontal mirroring, and BONE similarity scoring.
Package pose is the pure, deterministic core of poser-go: pose landmark types, torso-centered normalization, horizontal mirroring, and BONE similarity scoring.
Package search provides stateless, mirror-aware ranking of pose candidates against a query.
Package search provides stateless, mirror-aware ranking of pose candidates against a query.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL