mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-25 21:00:43 +01:00
A hand whose running left/right call wandered across 0.5 got mirrored mid-track: the palm normal turned over (the PR #3 gestures take's 90th percentile change per update, fingers moving, 94 -> 32 degrees). Now it keeps its side while the call is within 0.25 of 0.5. hands/README.md: the Skeleton section, with what replays measured, the new settings and ft-handreplay options. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
717 lines
32 KiB
C++
717 lines
32 KiB
C++
#include "skeleton.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
|
|
namespace skel {
|
|
|
|
// ------------------------------------------------------------------------------ rotations
|
|
|
|
V3 Rot::operator*(V3 v) const {
|
|
return {m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2], m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
|
|
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2]};
|
|
}
|
|
|
|
Rot Rot::operator*(const Rot &o) const {
|
|
Rot r;
|
|
for (int i = 0; i < 3; ++i)
|
|
for (int j = 0; j < 3; ++j) r.m[i][j] = m[i][0] * o.m[0][j] + m[i][1] * o.m[1][j] + m[i][2] * o.m[2][j];
|
|
return r;
|
|
}
|
|
|
|
Rot Rot::t() const {
|
|
Rot r;
|
|
for (int i = 0; i < 3; ++i)
|
|
for (int j = 0; j < 3; ++j) r.m[i][j] = m[j][i];
|
|
return r;
|
|
}
|
|
|
|
Rot exp_rot(V3 w) {
|
|
const double a = norm(w);
|
|
Rot r;
|
|
if (a < 1e-12) return r;
|
|
const V3 k = w * (1 / a);
|
|
const double c = std::cos(a), s = std::sin(a), v = 1 - c;
|
|
r.m[0][0] = c + k[0] * k[0] * v, r.m[0][1] = k[0] * k[1] * v - k[2] * s, r.m[0][2] = k[0] * k[2] * v + k[1] * s;
|
|
r.m[1][0] = k[1] * k[0] * v + k[2] * s, r.m[1][1] = c + k[1] * k[1] * v, r.m[1][2] = k[1] * k[2] * v - k[0] * s;
|
|
r.m[2][0] = k[2] * k[0] * v - k[1] * s, r.m[2][1] = k[2] * k[1] * v + k[0] * s, r.m[2][2] = c + k[2] * k[2] * v;
|
|
return r;
|
|
}
|
|
|
|
V3 log_rot(const Rot &r) {
|
|
const double c = std::clamp((r.m[0][0] + r.m[1][1] + r.m[2][2] - 1) / 2, -1.0, 1.0), a = std::acos(c);
|
|
const V3 v{r.m[2][1] - r.m[1][2], r.m[0][2] - r.m[2][0], r.m[1][0] - r.m[0][1]};
|
|
if (a < 1e-6) return v * 0.5;
|
|
if (a > M_PI - 1e-4) { // near a half turn: the axis from the diagonal
|
|
int i = 0;
|
|
for (int k = 1; k < 3; ++k)
|
|
if (r.m[k][k] > r.m[i][i]) i = k;
|
|
V3 axis{};
|
|
axis[i] = std::sqrt(std::max(0.0, (r.m[i][i] + 1) / 2));
|
|
for (int k = 0; k < 3; ++k)
|
|
if (k != i) axis[k] = (r.m[k][i] + r.m[i][k]) / (4 * axis[i]);
|
|
return unit(axis) * a;
|
|
}
|
|
return v * (a / (2 * std::sin(a)));
|
|
}
|
|
|
|
void to_quat(const Rot &r, double q[4]) {
|
|
const auto &m = r.m;
|
|
const double tr = m[0][0] + m[1][1] + m[2][2];
|
|
if (tr > 0) {
|
|
const double s = std::sqrt(tr + 1) * 2;
|
|
q[3] = s / 4, q[0] = (m[2][1] - m[1][2]) / s, q[1] = (m[0][2] - m[2][0]) / s, q[2] = (m[1][0] - m[0][1]) / s;
|
|
} else if (m[0][0] > m[1][1] && m[0][0] > m[2][2]) {
|
|
const double s = std::sqrt(1 + m[0][0] - m[1][1] - m[2][2]) * 2;
|
|
q[3] = (m[2][1] - m[1][2]) / s, q[0] = s / 4, q[1] = (m[0][1] + m[1][0]) / s, q[2] = (m[0][2] + m[2][0]) / s;
|
|
} else if (m[1][1] > m[2][2]) {
|
|
const double s = std::sqrt(1 + m[1][1] - m[0][0] - m[2][2]) * 2;
|
|
q[3] = (m[0][2] - m[2][0]) / s, q[0] = (m[0][1] + m[1][0]) / s, q[1] = s / 4, q[2] = (m[1][2] + m[2][1]) / s;
|
|
} else {
|
|
const double s = std::sqrt(1 + m[2][2] - m[0][0] - m[1][1]) * 2;
|
|
q[3] = (m[1][0] - m[0][1]) / s, q[0] = (m[0][2] + m[2][0]) / s, q[1] = (m[1][2] + m[2][1]) / s, q[2] = s / 4;
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
V3 cross(V3 a, V3 b) { return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]}; }
|
|
V3 mirror(V3 v, int side) { return {side * v[0], v[1], v[2]}; }
|
|
|
|
Rot from_cols(V3 x, V3 y, V3 z) {
|
|
Rot r;
|
|
for (int i = 0; i < 3; ++i) r.m[i][0] = x[i], r.m[i][1] = y[i], r.m[i][2] = z[i];
|
|
return r;
|
|
}
|
|
|
|
// Rotation about one of the frame's own axes.
|
|
Rot axis_rot(int axis, double a) {
|
|
Rot r;
|
|
const double c = std::cos(a), s = std::sin(a);
|
|
const int i = (axis + 1) % 3, j = (axis + 2) % 3;
|
|
r.m[i][i] = c, r.m[i][j] = -s, r.m[j][i] = s, r.m[j][j] = c;
|
|
return r;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- the hand model's tables
|
|
|
|
// The palm's landmarks (rigid), and each chain: the thumb from its CMC (1), the fingers from
|
|
// their knuckles.
|
|
const int kPalmIds[6] = {0, 1, 5, 9, 13, 17};
|
|
const int kChainBase[5] = {1, 5, 9, 13, 17};
|
|
const int kChainQ[5] = {0, 6, 11, 16, 21}; // first angle in Fit::q
|
|
|
|
struct Dof {
|
|
int joint; // 0, 1, 2 along the chain
|
|
int axis; // 0 x: flexion (positive toward the palm); 1 y: abduction (positive toward the
|
|
// thumb's side); 2 z: twist about the bone
|
|
double lo, hi;
|
|
};
|
|
// Angles in radians. Abduction is applied before flexion at a joint, so a bent finger keeps
|
|
// its spread.
|
|
constexpr double D = M_PI / 180;
|
|
// How far a chain's base (a knuckle, the thumb's CMC) may sit off the rigid palm: a prior's
|
|
// sigma and a hard limit, m.
|
|
constexpr double kBaseSigma = 0.004, kBaseMax = 0.015;
|
|
const Dof kThumb[6] = {{0, 1, -50 * D, 50 * D}, {0, 0, -50 * D, 50 * D}, {0, 2, -35 * D, 35 * D}, // CMC
|
|
{1, 1, -20 * D, 20 * D}, {1, 0, -30 * D, 80 * D}, // MCP
|
|
{2, 0, -30 * D, 90 * D}}; // IP
|
|
// Fingers get a little twist at the knuckle too: their flexion planes don't quite follow the
|
|
// palm's normal (in the 2026-10-03 gestures take, without it the fitted index tip sat a median
|
|
// 8 mm across the finger from the measured one).
|
|
const Dof kFinger[4][5] = {
|
|
{{0, 1, -25 * D, 25 * D}, {0, 0, -35 * D, 95 * D}, {0, 2, -25 * D, 25 * D}, {1, 0, -10 * D, 110 * D}, {2, 0, -20 * D, 90 * D}},
|
|
{{0, 1, -25 * D, 25 * D}, {0, 0, -35 * D, 95 * D}, {0, 2, -25 * D, 25 * D}, {1, 0, -10 * D, 110 * D}, {2, 0, -20 * D, 90 * D}},
|
|
{{0, 1, -25 * D, 25 * D}, {0, 0, -35 * D, 95 * D}, {0, 2, -25 * D, 25 * D}, {1, 0, -10 * D, 110 * D}, {2, 0, -20 * D, 90 * D}},
|
|
{{0, 1, -30 * D, 30 * D}, {0, 0, -35 * D, 95 * D}, {0, 2, -25 * D, 25 * D}, {1, 0, -10 * D, 110 * D}, {2, 0, -20 * D, 90 * D}},
|
|
};
|
|
|
|
const Dof *chain_dofs(int c, int *n) {
|
|
*n = c == 0 ? 6 : 5;
|
|
return c == 0 ? kThumb : kFinger[c - 1];
|
|
}
|
|
|
|
// The rotation a dof's angle makes about its axis: flexion turns the bone from -Z toward -Y,
|
|
// and abduction and twist are mirrored on a left hand so the same angles mean the same motion.
|
|
double dof_angle(const Dof &d, double q, int side) { return d.axis == 0 ? -q : side * q; }
|
|
double dof_sign(const Dof &d, int side) { return d.axis == 0 ? -1.0 : double(side); }
|
|
|
|
// The model's average hand (MediaPipe's world landmarks, medians over the 2026-09-30 lit
|
|
// recording, at Hand::scale 1), in the right hand's palm frame (x to the little finger,
|
|
// y out of the back, z toward the wrist), metres.
|
|
const V3 kCanonPalm[6] = {{0, 0, 0},
|
|
{-0.0149, -0.0142, -0.0158},
|
|
{-0.0172, -0.0043, -0.0657},
|
|
{0, 0, -0.0706},
|
|
{0.0162, 0, -0.0646},
|
|
{0.0286, -0.0043, -0.0559}};
|
|
const double kCanonPhalanx[15] = {0.0266, 0.0216, 0.0201, 0.0237, 0.0175, 0.0165, 0.0235, 0.0166,
|
|
0.0176, 0.0214, 0.0163, 0.0134, 0.0168, 0.0109, 0.0108};
|
|
// The thumb's rest direction from its CMC, and the way its MCP bends (same data), right hand.
|
|
const V3 kThumbRest{-0.50, -0.48, -0.70};
|
|
const V3 kThumbFlex{0.80, -0.40, -0.37};
|
|
|
|
int phalanx_bone(int k) { return kChainBase[k / 3] + k % 3; } // phalanx k -> bone index
|
|
|
|
// A chain's rest frame in the palm frame (mirrored for side): -Z along the resting bone.
|
|
Rot rest_frame(const Shape &s, int c, int side) {
|
|
V3 z, y;
|
|
if (c == 0) {
|
|
const V3 r = unit(kThumbRest), f = kThumbFlex;
|
|
z = r * -1;
|
|
y = unit(f - r * dot(f, r)) * -1;
|
|
} else {
|
|
// halfway between the metacarpal and the palm's axis, in the palm's plane
|
|
V3 m = s.palm[kChainBase[c]];
|
|
m[1] = 0;
|
|
z = unit(unit(m) + V3{0, 0, -1}) * -1;
|
|
y = {0, 1, 0};
|
|
}
|
|
z = mirror(z, side), y = mirror(y, side);
|
|
return from_cols(cross(y, z), y, z);
|
|
}
|
|
|
|
struct ChainState {
|
|
V3 base; // the chain's first landmark (on the palm, plus its offset)
|
|
V3 pts[3]; // the chain's landmarks
|
|
Rot frames[3]; // each joint's frame
|
|
V3 axis[6], pivot[6];
|
|
};
|
|
|
|
// rest: rest_frame(s, c, side), which the caller keeps for the chain's iterations
|
|
void chain_fk(const Shape &s, int c, int side, const Rot &rest, const Rot &R, V3 t, const double *q, V3 off, ChainState &st) {
|
|
int n;
|
|
const Dof *dofs = chain_dofs(c, &n);
|
|
const int base = kChainBase[c];
|
|
Rot F = R * rest;
|
|
V3 p = R * mirror(s.palm[base], side) + t + off;
|
|
st.base = p;
|
|
int d = 0;
|
|
for (int j = 0; j < 3; ++j) {
|
|
for (; d < n && dofs[d].joint == j; ++d) {
|
|
st.axis[d] = F.col(dofs[d].axis);
|
|
st.pivot[d] = p;
|
|
F = F * axis_rot(dofs[d].axis, dof_angle(dofs[d], q[d], side));
|
|
}
|
|
st.frames[j] = F;
|
|
p = p - F.col(2) * s.len[base + j];
|
|
st.pts[j] = p;
|
|
}
|
|
}
|
|
|
|
// Point weights: W = a I + b d d^T, d the line of sight (depth is the noisy direction).
|
|
struct Weight {
|
|
double a, b;
|
|
V3 d;
|
|
V3 apply(V3 r) const { return r * a + d * (b * dot(d, r)); }
|
|
};
|
|
|
|
Weight weight(V3 y, V3 eye, double sl, double sd) {
|
|
return {1 / (sl * sl), 1 / (sd * sd) - 1 / (sl * sl), unit(y - eye)};
|
|
}
|
|
|
|
// Solves H x = -g for small symmetric positive definite H (Cholesky). False if it isn't.
|
|
constexpr int kMaxP = 9; // a chain's parameters: up to 6 angles and the base's offset
|
|
bool solve_spd(double H[kMaxP][kMaxP], const double *g, double *x, int n) {
|
|
double L[kMaxP][kMaxP] = {};
|
|
for (int i = 0; i < n; ++i)
|
|
for (int j = 0; j <= i; ++j) {
|
|
double s = H[i][j];
|
|
for (int k = 0; k < j; ++k) s -= L[i][k] * L[j][k];
|
|
if (i == j) {
|
|
if (s <= 1e-18) return false;
|
|
L[i][i] = std::sqrt(s);
|
|
} else {
|
|
L[i][j] = s / L[j][j];
|
|
}
|
|
}
|
|
double y[kMaxP];
|
|
for (int i = 0; i < n; ++i) {
|
|
double s = -g[i];
|
|
for (int k = 0; k < i; ++k) s -= L[i][k] * y[k];
|
|
y[i] = s / L[i][i];
|
|
}
|
|
for (int i = n - 1; i >= 0; --i) {
|
|
double s = y[i];
|
|
for (int k = i + 1; k < n; ++k) s -= L[k][i] * x[k];
|
|
x[i] = s / L[i][i];
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// The palm frame straight from measured landmarks (wrist, index, middle and little knuckles).
|
|
bool measured_frame(const V3 p[21], int side, Rot &R) {
|
|
const V3 f0 = p[9] - p[0];
|
|
if (norm(f0) < 0.02) return false;
|
|
const V3 f = unit(f0);
|
|
V3 a = p[5] - p[17];
|
|
a = a - f * dot(a, f);
|
|
if (norm(a) < 0.008) return false;
|
|
a = unit(a);
|
|
const V3 y = cross(f, a) * double(side), z = f * -1;
|
|
R = from_cols(cross(y, z), y, z);
|
|
return true;
|
|
}
|
|
|
|
double angle(V3 a, V3 b) { return std::acos(std::clamp(dot(unit(a), unit(b)), -1.0, 1.0)); }
|
|
|
|
double median(const std::vector<float> &v) {
|
|
if (v.empty()) return 0;
|
|
float tmp[Calibration::kSettle];
|
|
const size_t n = std::min(v.size(), size_t(Calibration::kSettle));
|
|
std::copy(v.begin(), v.begin() + n, tmp);
|
|
std::nth_element(tmp, tmp + n / 2, tmp + n);
|
|
return tmp[n / 2];
|
|
}
|
|
|
|
void to_quantities(const Shape &s, double *q) {
|
|
for (int k = 0; k < 5; ++k)
|
|
for (int j = 0; j < 3; ++j) q[3 * k + j] = s.palm[kPalmIds[k + 1]][j];
|
|
for (int k = 0; k < 15; ++k) q[15 + k] = s.len[phalanx_bone(k)];
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void Shape::from_quantities(const double *q) {
|
|
for (int k = 0; k < 5; ++k)
|
|
for (int j = 0; j < 3; ++j) palm[kPalmIds[k + 1]][j] = q[3 * k + j];
|
|
for (int k = 0; k < 15; ++k) len[phalanx_bone(k)] = q[15 + k];
|
|
for (int c = 0; c < 5; ++c) len[kChainBase[c] - 1] = norm(palm[kChainBase[c]]); // bones 0, 4, 8, 12, 16
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------- calibration
|
|
|
|
void Calibration::add(const V3 local[21], const bool ok[21], const bool finger_ok[5]) {
|
|
double x[kQuantities];
|
|
Shape s;
|
|
for (int k : {1, 5, 9, 13, 17}) s.palm[k] = local[k];
|
|
for (int b = 0; b < 20; ++b) s.len[b] = norm(local[b + 1] - local[b % 4 == 0 ? 0 : b]);
|
|
to_quantities(s, x);
|
|
// a phalanx far off the model's average hand's proportions is a misplaced joint (the dim
|
|
// typing recording measured a middle phalanx 1.9 times as long as that), not this hand
|
|
const double palm_len = norm(local[9] - local[0]);
|
|
for (int k = 0; k < kQuantities; ++k) {
|
|
if (k >= 15) {
|
|
const double r = x[k] / (palm_len * kCanonPhalanx[k - 15] / -kCanonPalm[3][2]);
|
|
if (!(r > 0.7 && r < 1.5)) continue;
|
|
}
|
|
if (k < 15 ? !ok[kPalmIds[k / 3 + 1]] || !ok[0] || !ok[5] || !ok[9] || !ok[17] : !finger_ok[(k - 15) / 3]) continue;
|
|
if (k >= 15) {
|
|
const int b = phalanx_bone(k - 15);
|
|
if (!ok[b] || !ok[b + 1]) continue;
|
|
}
|
|
if (n_[k] < kSettle) {
|
|
buf_[k].push_back(float(x[k]));
|
|
est_[k] = median(buf_[k]); // so far
|
|
if (++n_[k] == kSettle) {
|
|
buf_[k].clear();
|
|
buf_[k].shrink_to_fit();
|
|
}
|
|
} else if (std::fabs(x[k] - est_[k]) < 0.3 * std::fabs(est_[k]) + 0.003) {
|
|
est_[k] += std::clamp(x[k] - est_[k], -2e-5, 2e-5); // a running median, at most 0.02 mm a sample
|
|
++n_[k];
|
|
}
|
|
}
|
|
}
|
|
|
|
Shape Calibration::shape(const Shape &prior) const {
|
|
double p[kQuantities], q[kQuantities];
|
|
to_quantities(prior, p);
|
|
constexpr double kPriorWeight = 8;
|
|
for (int k = 0; k < kQuantities; ++k) {
|
|
if (n_[k] >= kSettle) q[k] = est_[k];
|
|
else if (n_[k] == 0) q[k] = p[k];
|
|
else q[k] = (n_[k] * est_[k] + kPriorWeight * p[k]) / (n_[k] + kPriorWeight);
|
|
}
|
|
Shape s;
|
|
s.from_quantities(q);
|
|
return s;
|
|
}
|
|
|
|
double Calibration::settled() const {
|
|
int least = kSettle;
|
|
for (int k = 0; k < kQuantities; ++k) least = std::min(least, n_[k]);
|
|
return double(least) / kSettle;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------- skeleton
|
|
|
|
Shape Skeleton::canonical(double scale) {
|
|
Shape s;
|
|
double q[kQuantities];
|
|
for (int k = 0; k < 5; ++k)
|
|
for (int j = 0; j < 3; ++j) q[3 * k + j] = kCanonPalm[k + 1][j] * scale;
|
|
for (int k = 0; k < 15; ++k) q[15 + k] = kCanonPhalanx[k] * scale;
|
|
s.from_quantities(q);
|
|
return s;
|
|
}
|
|
|
|
Shape Skeleton::shape(int side, double scale) const {
|
|
const Calibration &me = cal_[side > 0], &other = cal_[side <= 0];
|
|
// the other hand, when it knows more: left and right hands are mirror images
|
|
const Shape prior = other.settled() > me.settled() ? other.shape(canonical(scale)) : canonical(scale);
|
|
return me.shape(prior);
|
|
}
|
|
|
|
void Skeleton::pose(const Shape &s, int side, const Rot &R, V3 t, const double *q, V3 out[21], Rot frames[21]) {
|
|
out[0] = t;
|
|
frames[0] = R;
|
|
for (int c = 0; c < 5; ++c) {
|
|
ChainState st;
|
|
const double *o = q + kAngles + 3 * c;
|
|
chain_fk(s, c, side, rest_frame(s, c, side), R, t, q + kChainQ[c], {o[0], o[1], o[2]}, st);
|
|
const int base = kChainBase[c];
|
|
out[base] = st.base;
|
|
for (int j = 0; j < 3; ++j) out[base + 1 + j] = st.pts[j], frames[base + j] = st.frames[j];
|
|
frames[base + 3] = st.frames[2]; // the tip: its bone's frame
|
|
}
|
|
}
|
|
|
|
double Skeleton::solve(const Shape &s, int side, const V3 pts[21], const double sigma_l[21], const double sigma_d[21],
|
|
V3 eye, const double *prev, int iterations, Rot &R, V3 &t, double *q, double *rms, double limits,
|
|
double chain_rms[5], bool warm) {
|
|
Weight w[21];
|
|
for (int i = 0; i < 21; ++i) w[i] = weight(pts[i], eye, sigma_l[i], sigma_d[i]);
|
|
|
|
// 1. the palm, rigid: from the measured frame, then Gauss-Newton on rotation and position
|
|
if (!measured_frame(pts, side, R)) return -1;
|
|
V3 T[6];
|
|
for (int k = 0; k < 6; ++k) T[k] = mirror(s.palm[kPalmIds[k]], side);
|
|
{
|
|
V3 c{};
|
|
double ws = 0;
|
|
for (int k = 0; k < 6; ++k) {
|
|
const double wk = w[kPalmIds[k]].a;
|
|
c = c + (pts[kPalmIds[k]] - R * T[k]) * wk, ws += wk;
|
|
}
|
|
t = c * (1 / ws);
|
|
}
|
|
for (int it = 0; it < 3; ++it) {
|
|
double H[kMaxP][kMaxP] = {}, g[6] = {}, x[6];
|
|
for (int k = 0; k < 6; ++k) {
|
|
const int i = kPalmIds[k];
|
|
const V3 v = R * T[k], r = v + t - pts[i];
|
|
V3 J[6];
|
|
for (int a = 0; a < 3; ++a) {
|
|
V3 e{};
|
|
e[a] = 1;
|
|
J[a] = cross(e, v);
|
|
J[3 + a] = e;
|
|
}
|
|
V3 WJ[6];
|
|
for (int a = 0; a < 6; ++a) WJ[a] = w[i].apply(J[a]);
|
|
for (int a = 0; a < 6; ++a) {
|
|
g[a] += dot(WJ[a], r);
|
|
for (int b = 0; b <= a; ++b) H[a][b] += dot(J[b], WJ[a]);
|
|
}
|
|
}
|
|
for (int a = 0; a < 6; ++a) {
|
|
for (int b = 0; b < a; ++b) H[b][a] = H[a][b];
|
|
H[a][a] *= 1 + 1e-6;
|
|
}
|
|
if (!solve_spd(H, g, x, 6)) break;
|
|
R = exp_rot({x[0], x[1], x[2]}) * R;
|
|
t = t + V3{x[3], x[4], x[5]};
|
|
if (norm(V3{x[0], x[1], x[2]}) < 1e-4 && norm(V3{x[3], x[4], x[5]}) < 1e-5) break;
|
|
}
|
|
|
|
// 2. each chain from its base on the fitted palm. The base may move off the rigid palm a
|
|
// little (kBaseSigma): the measured knuckles wander far more than a palm bends (the little
|
|
// finger's by 3 cm), so a rigid palm alone puts whole fingers beside their measurements.
|
|
// Angles start straight from the measured bone directions, then Gauss-Newton with the
|
|
// weights, the joint limits and those priors.
|
|
double cost = 0;
|
|
{
|
|
const V3 r = t - pts[0];
|
|
cost += dot(r, w[0].apply(r));
|
|
}
|
|
for (int c = 0; c < 5; ++c) {
|
|
int n;
|
|
const Dof *dofs = chain_dofs(c, &n);
|
|
const int base = kChainBase[c], np = n + 3; // angles, then the base's offset
|
|
const Rot rest = rest_frame(s, c, side);
|
|
double *qc = q + kChainQ[c], *oc = q + kAngles + 3 * c;
|
|
const double *pc = prev ? prev + kChainQ[c] : nullptr;
|
|
// initial offset: most of the way to the measured knuckle
|
|
{
|
|
const V3 d = pts[base] - (R * mirror(s.palm[base], side) + t);
|
|
const double k = kBaseSigma * kBaseSigma / (kBaseSigma * kBaseSigma + sigma_l[base] * sigma_l[base]);
|
|
V3 o = d * k;
|
|
if (norm(o) > kBaseMax) o = o * (kBaseMax / norm(o));
|
|
for (int j = 0; j < 3; ++j) oc[j] = o[j];
|
|
}
|
|
// initial angles
|
|
{
|
|
Rot F = R * rest;
|
|
V3 p = R * mirror(s.palm[base], side) + t + V3{oc[0], oc[1], oc[2]};
|
|
int d = 0;
|
|
for (int j = 0; j < 3; ++j) {
|
|
const V3 l = F.t() * unit(pts[base + 1 + j] - p);
|
|
for (; d < n && dofs[d].joint == j; ++d) {
|
|
double a = 0;
|
|
if (dofs[d].axis == 1) a = std::atan2(-l[0], -l[2]) * side; // abduction (before flexion)
|
|
else if (dofs[d].axis == 0) a = dofs[d].joint == 0 || (c == 0 && j == 1) ? std::asin(std::clamp(-l[1], -1.0, 1.0))
|
|
: std::atan2(-l[1], -l[2]);
|
|
else a = pc ? pc[d] : 0; // twist: unseen in one bone's direction
|
|
qc[d] = std::clamp(a, dofs[d].lo * limits, dofs[d].hi * limits);
|
|
F = F * axis_rot(dofs[d].axis, dof_angle(dofs[d], qc[d], side));
|
|
}
|
|
p = p - F.col(2) * s.len[base + j];
|
|
}
|
|
}
|
|
// priors: the base's offset toward none; a weak pull toward last frame's angles (and the
|
|
// twist toward none), for what one frame doesn't show: the twist of a straight thumb, a
|
|
// finger seen end-on
|
|
double lambda[kMaxP], target[kMaxP], x[kMaxP];
|
|
for (int d = 0; d < n; ++d) {
|
|
const double sigma = dofs[d].axis == 2 ? 0.35 : 0.6;
|
|
lambda[d] = 1e-6 / (0.002 * 0.002) / (sigma * sigma); // 1 rad off weighs like 1 mm off at 2 mm sigma, over sigma^2
|
|
target[d] = dofs[d].axis == 2 ? 0 : pc ? pc[d] : qc[d];
|
|
}
|
|
for (int j = 0; j < 3; ++j) lambda[n + j] = 1 / (kBaseSigma * kBaseSigma), target[n + j] = 0;
|
|
// Gauss-Newton from (qa, oa), in place; returns the chain's cost, and its plain rms in *crms
|
|
auto run = [&](double *qa, double *oa, double *crms) {
|
|
auto params = [&](int k) -> double & { return k < n ? qa[k] : oa[k - n]; };
|
|
ChainState st;
|
|
for (int it = 0; it < iterations; ++it) {
|
|
chain_fk(s, c, side, rest, R, t, qa, {oa[0], oa[1], oa[2]}, st);
|
|
double H[kMaxP][kMaxP] = {}, g[kMaxP] = {};
|
|
for (int m = 0; m < 4; ++m) { // the base, then the chain's three landmarks
|
|
const int lm = base + m;
|
|
const Weight &wm = w[lm];
|
|
const V3 at = m ? st.pts[m - 1] : st.base, r = at - pts[lm];
|
|
V3 J[kMaxP], WJ[kMaxP];
|
|
for (int d = 0; d < n; ++d)
|
|
J[d] = m && dofs[d].joint <= m - 1 ? cross(st.axis[d], at - st.pivot[d]) * dof_sign(dofs[d], side) : V3{0, 0, 0};
|
|
for (int j = 0; j < 3; ++j) J[n + j] = {0, 0, 0}, J[n + j][j] = 1;
|
|
for (int d = 0; d < np; ++d) WJ[d] = wm.apply(J[d]);
|
|
for (int a2 = 0; a2 < np; ++a2) {
|
|
g[a2] += dot(WJ[a2], r);
|
|
for (int b2 = 0; b2 <= a2; ++b2) H[a2][b2] += dot(J[b2], WJ[a2]);
|
|
}
|
|
}
|
|
for (int a2 = 0; a2 < np; ++a2) {
|
|
for (int b2 = 0; b2 < a2; ++b2) H[b2][a2] = H[a2][b2];
|
|
H[a2][a2] = H[a2][a2] * (1 + 1e-3) + lambda[a2];
|
|
g[a2] += lambda[a2] * (params(a2) - target[a2]);
|
|
}
|
|
if (!solve_spd(H, g, x, np)) break;
|
|
double step = 0;
|
|
for (int d = 0; d < n; ++d) {
|
|
const double nq = std::clamp(qa[d] + x[d], dofs[d].lo * limits, dofs[d].hi * limits);
|
|
step = std::max(step, std::fabs(nq - qa[d]));
|
|
qa[d] = nq;
|
|
}
|
|
V3 o{oa[0] + x[n], oa[1] + x[n + 1], oa[2] + x[n + 2]};
|
|
if (norm(o) > kBaseMax) o = o * (kBaseMax / norm(o));
|
|
for (int j = 0; j < 3; ++j) step = std::max(step, std::fabs(o[j] - oa[j]) * 10), oa[j] = o[j]; // 0.1 mm ~ 1e-3 rad
|
|
if (step < 1e-3) break;
|
|
}
|
|
chain_fk(s, c, side, rest, R, t, qa, {oa[0], oa[1], oa[2]}, st);
|
|
double cc = 0, sq = 0;
|
|
for (int m = 0; m < 4; ++m) {
|
|
const V3 r = (m ? st.pts[m - 1] : st.base) - pts[base + m];
|
|
cc += dot(r, w[base + m].apply(r));
|
|
sq += dot(r, r);
|
|
}
|
|
for (int d = 0; d < np; ++d) cc += lambda[d] * (params(d) - target[d]) * (params(d) - target[d]);
|
|
*crms = std::sqrt(sq / 4);
|
|
return cc;
|
|
};
|
|
double crms = 0, cc = run(qc, oc, &crms);
|
|
// and from last frame's pose: the one that fits better wins, so the fit doesn't hop
|
|
// between two poses that fit about as well from one frame's analytic start to the next
|
|
if (pc && warm) {
|
|
double q2[kMaxP], o2[3], crms2 = 0;
|
|
const double *po = prev + kAngles + 3 * c;
|
|
for (int d = 0; d < n; ++d) q2[d] = std::clamp(pc[d], dofs[d].lo * limits, dofs[d].hi * limits);
|
|
for (int j = 0; j < 3; ++j) o2[j] = po[j];
|
|
const double cc2 = run(q2, o2, &crms2);
|
|
if (cc2 < cc) {
|
|
cc = cc2, crms = crms2;
|
|
std::copy(q2, q2 + n, qc);
|
|
std::copy(o2, o2 + 3, oc);
|
|
}
|
|
}
|
|
cost += cc;
|
|
if (chain_rms) chain_rms[c] = crms;
|
|
}
|
|
if (rms) {
|
|
V3 out[21];
|
|
Rot frames[21];
|
|
pose(s, side, R, t, q, out, frames);
|
|
double sum = 0;
|
|
for (int i = 0; i < 21; ++i) sum += dot(out[i] - pts[i], out[i] - pts[i]);
|
|
*rms = std::sqrt(sum / 21);
|
|
}
|
|
return cost;
|
|
}
|
|
|
|
V3 Skeleton::elbow(V3 wrist, V3 mid_knuckle, int side, double forearm, V3 up) {
|
|
// frame-hands' train/forearm/armmodel.py: a neck pivot fixed in the head frame, the
|
|
// shoulders off it in a torso that faces where the head does (level), upper arm 1.5 x the
|
|
// forearm, the elbow on the circle the two bones allow, turned -3 degrees from straight down
|
|
const V3 neck{0, -0.12, 0.08};
|
|
constexpr double kShoulderW = 0.18, kShoulderD = 0.06, kUpper = 1.5, kSwivel = -3 * D, kMaxBend = 75 * D;
|
|
V3 fwd{0, 0, -1};
|
|
fwd = fwd - up * dot(fwd, up);
|
|
const V3 torso = norm(fwd) > 1e-3 ? unit(fwd) : V3{0, 0, -1};
|
|
const V3 right = unit(cross(torso, up));
|
|
const V3 S = neck + right * (side * kShoulderW) - up * kShoulderD;
|
|
const double F = forearm, U = kUpper * forearm;
|
|
const V3 v = wrist - S;
|
|
const double d = norm(v);
|
|
const V3 stub = unit(wrist - mid_knuckle);
|
|
V3 dir = stub;
|
|
if (d > 1e-3) {
|
|
const V3 n = v * (1 / d);
|
|
const double dd = std::clamp(d, std::fabs(U - F) + 1e-4, U + F - 1e-4);
|
|
const double a = (U * U - F * F + dd * dd) / (2 * dd), rho = std::sqrt(std::max(U * U - a * a, 0.0));
|
|
const V3 C = S + n * a;
|
|
V3 down = up * -1;
|
|
down = down - n * dot(n, down);
|
|
if (norm(down) > 1e-6) {
|
|
const V3 v0 = unit(down);
|
|
V3 v1 = cross(n, v0) * double(side);
|
|
if (dot(v1, right) * side < 0) v1 = v1 * -1;
|
|
const V3 E = C + (v0 * std::cos(kSwivel) + v1 * std::sin(kSwivel)) * rho;
|
|
if (norm(E - wrist) > 1e-6) dir = unit(E - wrist);
|
|
}
|
|
}
|
|
// a wrist bends at most about this far from the forearm's line
|
|
const double bend = angle(dir, stub);
|
|
if (bend > kMaxBend) {
|
|
const V3 perp = unit(dir - stub * dot(dir, stub));
|
|
dir = stub * std::cos(kMaxBend) + perp * std::sin(kMaxBend);
|
|
}
|
|
return wrist + dir * F;
|
|
}
|
|
|
|
void Skeleton::update(Fit &f, const V3 pts[21], bool right, double right_score, int nviews, double residual,
|
|
const double *res_pt, V3 eye, double scale, int64_t t_ns) {
|
|
const auto t0 = std::chrono::steady_clock::now();
|
|
const bool continuing = f.valid && t_ns > f.ns && t_ns - f.ns < 300'000'000;
|
|
f.valid = false;
|
|
if (nviews <= 0) return;
|
|
const int model_side = right ? 1 : -1;
|
|
double sl[21], sd[21];
|
|
for (int i = 0; i < 21; ++i) {
|
|
// stereo: a landmark whose rays miss each other is less sure
|
|
const double r = res_pt && res_pt[i] >= 0 ? res_pt[i] : residual;
|
|
sl[i] = nviews >= 2 ? std::hypot(0.002, std::max(0.0, r)) : 0.003;
|
|
sd[i] = nviews >= 2 ? p_.depth_ratio * sl[i] : 0.015;
|
|
}
|
|
// the side: the model's, unless the mirror image fits far better (a finger can't bend
|
|
// backward, so a hand fitted as the wrong side fits badly once its fingers bend) for
|
|
// kFlipFrames in a row. A side chosen that way sticks while the hand is tracked, as the
|
|
// model's call does; switching resets the filter, so it mustn't flicker.
|
|
constexpr int kFlipFrames = 3;
|
|
// The model's call is a running average (Hand::right_score); while it hovers near 0.5 a
|
|
// tracked hand keeps the side it has, so the skeleton doesn't turn inside out.
|
|
int side = model_side;
|
|
if (continuing && f.side != model_side && (f.flipped || std::fabs(right_score - 0.5) < 0.25)) side = f.side;
|
|
if (!continuing) f.votes = 0;
|
|
Shape s = shape(side, scale);
|
|
Rot R;
|
|
V3 t;
|
|
double q[kParams] = {}, rms = 0;
|
|
const double *prev = continuing && f.side == side ? f.q : nullptr;
|
|
double crms[5] = {};
|
|
double cost = solve(s, side, pts, sl, sd, eye, prev, p_.iterations, R, t, q, &rms, p_.limits, crms, p_.warm > 0);
|
|
if (cost < 0) return;
|
|
bool better = false;
|
|
if (rms > 0.015) {
|
|
Rot R2;
|
|
V3 t2;
|
|
double q2[kParams] = {}, rms2 = 0;
|
|
const Shape s2 = shape(-side, scale);
|
|
double crms2[5] = {};
|
|
const double cost2 = solve(s2, -side, pts, sl, sd, eye, continuing && f.side == -side ? f.q : nullptr,
|
|
p_.iterations, R2, t2, q2, &rms2, p_.limits, crms2, p_.warm > 0);
|
|
better = cost2 >= 0 && cost2 < 0.5 * cost;
|
|
if (better && ++f.votes >= kFlipFrames) {
|
|
side = -side, s = s2, R = R2, t = t2, rms = rms2, cost = cost2;
|
|
std::copy(q2, q2 + kParams, q);
|
|
std::copy(crms2, crms2 + 5, crms);
|
|
f.votes = 0;
|
|
++flips;
|
|
}
|
|
}
|
|
if (!better) f.votes = 0;
|
|
f.flipped = side != model_side;
|
|
|
|
// learn the bone lengths from clean stereo frames, in the hand's own palm frame
|
|
if (nviews >= 2 && residual >= 0 && residual <= 0.004 && side == model_side && std::fabs(right_score - 0.5) > 0.3) {
|
|
bool pt_ok[21];
|
|
for (int i = 0; i < 21; ++i) pt_ok[i] = !res_pt || (res_pt[i] >= 0 && res_pt[i] <= 0.004);
|
|
Rot M;
|
|
if (measured_frame(pts, side, M)) {
|
|
V3 local[21];
|
|
const Rot Mt = M.t();
|
|
for (int i = 0; i < 21; ++i) local[i] = mirror(Mt * (pts[i] - pts[0]), side);
|
|
bool ok[5];
|
|
for (int c = 0; c < 5; ++c) {
|
|
const int b = kChainBase[c];
|
|
// bent fingers hide their own joints from the cameras: only fairly straight ones
|
|
const double bend = angle(local[b + 1] - local[b], local[b + 2] - local[b + 1]) +
|
|
angle(local[b + 2] - local[b + 1], local[b + 3] - local[b + 2]);
|
|
ok[c] = bend < 90 * D;
|
|
}
|
|
cal_[side > 0].add(local, pt_ok, ok);
|
|
}
|
|
}
|
|
|
|
// filter the pose: the palm centre, the palm's rotation, each chain's angles
|
|
V3 Tc{};
|
|
for (int k : {0, 5, 9, 13, 17}) Tc = Tc + mirror(s.palm[k], side) * 0.2;
|
|
const V3 c = R * Tc + t;
|
|
const double dt = continuing ? (t_ns - f.ns) / 1e9 : 0;
|
|
if (!continuing || f.side != side || dt <= 0) {
|
|
f.Rf = R, f.cf = c, f.vel = f.angvel = {0, 0, 0};
|
|
std::copy(q, q + kParams, f.qf);
|
|
std::fill(f.dq, f.dq + kParams, 0.0);
|
|
} else {
|
|
auto alpha = [dt](double cutoff) { return 1 / (1 + 1 / (2 * M_PI * cutoff * dt)); };
|
|
const double ad = alpha(p_.d_cutoff);
|
|
f.vel = f.vel + ((c - f.cf) * (1 / dt) - f.vel) * ad;
|
|
f.cf = f.cf + (c - f.cf) * alpha(p_.pos_min + p_.pos_beta * norm(f.vel));
|
|
const V3 delta = log_rot(f.Rf.t() * R); // in the filtered palm's frame
|
|
f.angvel = f.angvel + (f.Rf * delta * (1 / dt) - f.angvel) * ad;
|
|
f.Rf = f.Rf * exp_rot(delta * alpha(p_.rot_min + p_.rot_beta * norm(f.angvel)));
|
|
for (int ch = 0; ch < 5; ++ch) {
|
|
const int a = kChainQ[ch], b = ch + 1 < 5 ? kChainQ[ch + 1] : kAngles;
|
|
// a chain that fits its landmarks badly this frame is mostly a misplaced landmark (a
|
|
// finger hidden behind another): it moves the filter, and its speed, less
|
|
const double trust = std::min(1.0, std::pow(p_.glitch / std::max(crms[ch], 1e-6), 2));
|
|
double speed = 0;
|
|
for (int d = a; d < b; ++d) {
|
|
f.dq[d] += trust * ad * ((q[d] - f.qf[d]) / dt - f.dq[d]);
|
|
speed = std::max(speed, std::fabs(f.dq[d]));
|
|
}
|
|
const double ac = trust * alpha(p_.ang_min + p_.ang_beta * speed);
|
|
for (int d = a; d < b; ++d) f.qf[d] += ac * (q[d] - f.qf[d]);
|
|
for (int d = kAngles + 3 * ch; d < kAngles + 3 * ch + 3; ++d) f.qf[d] += ac * (q[d] - f.qf[d]); // its base
|
|
}
|
|
}
|
|
std::copy(crms, crms + 5, f.chain_rms);
|
|
f.R = R, f.t = t, f.rms = rms, f.side = side, f.ns = t_ns, f.valid = true;
|
|
std::copy(q, q + kParams, f.q);
|
|
{
|
|
Rot fr[21];
|
|
pose(s, side, R, t, q, f.raw, fr);
|
|
}
|
|
const V3 tf = f.cf - f.Rf * Tc;
|
|
pose(s, side, f.Rf, tf, f.qf, f.pts, f.frames);
|
|
std::copy(s.len, s.len + 20, f.len);
|
|
// rough radii, from the capsules' (io.cpp) at this hand's size
|
|
const double k = norm(s.palm[9]) / 0.0815;
|
|
for (int i = 0; i < 21; ++i) f.radius[i] = k * (i == 0 ? 0.020 : i == 1 || i % 4 == 1 ? 0.012 : i <= 4 ? 0.0095 : 0.0085);
|
|
f.elbow = elbow(tf, f.pts[9], side, p_.forearm_ratio * norm(s.palm[9]), up_);
|
|
f.settled = cal_[side > 0].settled();
|
|
f.us = std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - t0).count();
|
|
++fits, us_total += f.us;
|
|
}
|
|
|
|
} // namespace skel
|