mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-17 16:00:36 +02:00
Hand cutouts: a round hole per hand by default (HANDS_MATTE=circle)
Until the tracking is good enough for a hand-shaped hole, each hand cuts a disk facing the eye, wide enough for the whole hand plus the margin, drawn as the stadium around the ellipse each eye sees on the screen with a soft edge. mesh and capsules stay selectable (HANDS_MATTE, cutouts matte). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
45c716da7c
commit
3c4964bc56
10 files changed
+203
-38
No files matched your search
+1
-1
@@ -83,7 +83,7 @@ get N screens head state key code value scale N s vrkeyboard s
|
||||
visibility always|dashboard|gesture|toggle wrist degrees gesture left|right degrees
|
||||
hide | show | toggle controllers always|outside_games|dashboard ingames hide|visible pause on|off|state
|
||||
conceal N|all reveal N|all concealed cutouts on|off|state cutouts predict on|off cutouts lead ms
|
||||
cutouts matte mesh|capsules cutouts margin deg [ms [deg]] cutouts shift off|spec cutouts roomview auto|on|off
|
||||
cutouts matte circle|mesh|capsules cutouts margin deg [ms [deg]] cutouts shift off|spec cutouts roomview auto|on|off
|
||||
hands L R allowed handinput on|off|force|state palmmenu on|off|state
|
||||
float N mpp x y w h title unfloat N pose N matrix sub N k x y w h | sub N k off minimized N 0|1 carry N
|
||||
rates focused in_view hidden rates? watch seconds phase ms
|
||||
|
||||
+4
-2
@@ -275,13 +275,15 @@ Python, with NumPy and OpenCV. `setup/dev-container.sh` doesn't install them, be
|
||||
- `tools/cut_sets.py REC OUT [--sets N | --at I,J,...]`: copies a few frame sets (by default 8, spread evenly) out of a recording into a small one, to look at or check elsewhere without moving gigabytes. Plain Python, so it also runs on the Frame's host.
|
||||
- `tools/convert_models.py`: how `models/ncnn` was made from the OpenCV Zoo ONNX ports of MediaPipe's models (see `models/NOTICE`).
|
||||
|
||||
To try the hand cutouts without restarting the desktop, `screens/build/ft-handtest [--distance m] [--width m] [--seconds s] [--matte mesh|capsules] [--margin deg]` (built by `screens/build.sh`, run in the dev container, with hand tracking on) shows a test panel of its own, a light grid 1 m wide and 0.8 m ahead by default, and cuts your hands out of it the way ft-screens cuts them out of the screens. Every 2 s it prints the composite's cost.
|
||||
To try the hand cutouts without restarting the desktop, `screens/build/ft-handtest [--distance m] [--width m] [--seconds s] [--matte circle|mesh|capsules] [--margin deg]` (built by `screens/build.sh`, run in the dev container, with hand tracking on) shows a test panel of its own, a light grid 1 m wide and 0.8 m ahead by default, and cuts your hands out of it the way ft-screens cuts them out of the screens. Every 2 s it prints the composite's cost.
|
||||
|
||||
## Hand cutouts and near input
|
||||
|
||||
ft-screens reads the hands file (version 2's skeleton when it's there, else version 1's points) and the gestures file once per tick (`screens/handcut.cpp`). It cuts only with Room View on: with it off, a hole would show the void or the game, not your hand (see "Rendered hands"). `cutouts state` ends with `room view on|off (why)`; `cutouts roomview auto|on|off` forces it.
|
||||
|
||||
**The matte** (`HANDS_MATTE=mesh`, the default) is hand-shaped: a tapered capsule per finger bone, with real fingers' half-widths (10.5 mm at the index's knuckle down to 7.5 mm at its tip, scaled by the hand's palm length); the palm as a 2.4 cm slab through the wrist and the knuckles, widened to the hand's edges; the thumb's web; and an 18 cm stub of the forearm toward the skeleton's elbow (along the hand's axis without a skeleton). The palm, web and forearm are convex pieces, each drawn as its outline grown by the margin with a soft 2 px edge. `HANDS_MATTE=capsules` draws ft-hands' capsules instead, as before, for comparison (`cutouts matte mesh|capsules` switches it live).
|
||||
**The matte** (`HANDS_MATTE=circle`, the default since 2026-10-09) is a round hole per hand: a disk facing the eye, centred on the middle of what the head sees of the hand (the box around its joints, each with its half-width, the wrist's 2.5 cm), wide enough to reach the hand's furthest edge from there, plus the margin. Each eye sees the disk on the screen as an ellipse (a circle on a screen square to it), drawn as the stadium around it with the capsules' soft edge (15% of its radius). It hides more of the screen than the hand needs, and with it the hand's tracking error and the few degrees Room View's picture of the hand sits off it, until the tracking is good enough for a hand-shaped hole. The forearm isn't in it. Building and projecting it costs 4 us median on the desk-work replay (the mesh 26 us), the composite the same as the mesh's or less (`scripts/bench-matte.sh` on probe-moves: 2 pieces per eye against 35).
|
||||
|
||||
`HANDS_MATTE=mesh` is hand-shaped: a tapered capsule per finger bone, with real fingers' half-widths (10.5 mm at the index's knuckle down to 7.5 mm at its tip, scaled by the hand's palm length); the palm as a 2.4 cm slab through the wrist and the knuckles, widened to the hand's edges; the thumb's web; and an 18 cm stub of the forearm toward the skeleton's elbow (along the hand's axis without a skeleton). The palm, web and forearm are convex pieces, each drawn as its outline grown by the margin with a soft 2 px edge. `HANDS_MATTE=capsules` draws ft-hands' capsules instead, as before, for comparison (`cutouts matte circle|mesh|capsules` switches it live).
|
||||
|
||||
Its margin is an angle seen from the eyes, so it covers the same share of the tracking error at any distance: `HANDS_MATTE_MARGIN` (1.5 degrees) with the hand still, plus `HANDS_MATTE_MOTION` (25 ms) of the hand's motion, at most `HANDS_MATTE_MOTION_MAX` (1.5 degrees) more. The cutout placement probes of 2026-10-08 (frame-hands `captures/probe-2026100*/video_analysis`) measured the cutout points against the hand Room View shows: 1.8-2.4 degrees median still (p90 4.1-4.7), 2.3-3.1 moving at 0.1-0.8 m/s, 4-6 above 0.8 m/s. Part of that is the video landmark model's own error, and an edge needs only the error across it (about 70% of a point's). A margin covering the p90 (4.5 degrees, 3 cm at 40 cm) would turn each finger into a 7 cm blob; 1.5 covers about the median. `cutouts margin DEG [MS [MAX]]` changes it live, and so does the Hand cutout margin slider on the Hands page of Frametop Input Settings (0-3 degrees in quarter steps; it sends the still margin only and writes `HANDS_MATTE_MARGIN`). The prediction lead stays 36 ms (`HANDS_MATTE_LEAD`, `cutouts lead`): the probes' best lead for fast hands was 6-14 ms, but 36 costs only 0.1-0.2 degrees.
|
||||
|
||||
|
||||
+98
-2
@@ -126,6 +126,16 @@ bool ParseShift(const std::string &text, std::vector<Shift> &out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *ShapeName(MatteShape s) {
|
||||
return s == MatteShape::Circle ? "circle" : s == MatteShape::Mesh ? "mesh" : "capsules";
|
||||
}
|
||||
|
||||
bool ParseShape(const std::string &name, MatteShape *out) {
|
||||
for (MatteShape s : {MatteShape::Circle, MatteShape::Mesh, MatteShape::Capsules})
|
||||
if (name == ShapeName(s)) return *out = s, true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string ReadMatteSettings(MatteParams &p) {
|
||||
std::string log;
|
||||
auto num = [&](const char *name, double &v, double lo, double hi) {
|
||||
@@ -137,7 +147,7 @@ std::string ReadMatteSettings(MatteParams &p) {
|
||||
else log += std::string(name) + "=" + s + " (ignored) ";
|
||||
};
|
||||
const std::string m = Setting("HANDS_MATTE", "");
|
||||
if (m == "capsules" || m == "mesh") p.mesh = m == "mesh", log += "HANDS_MATTE=" + m + " ";
|
||||
if (!m.empty()) log += "HANDS_MATTE=" + m + (ParseShape(m, &p.shape) ? " " : " (ignored) ");
|
||||
num("HANDS_MATTE_MARGIN", p.marginDeg, 0, 10);
|
||||
num("HANDS_MATTE_MOTION", p.motionMs, 0, 200);
|
||||
num("HANDS_MATTE_MOTION_MAX", p.motionMaxDeg, 0, 10);
|
||||
@@ -510,7 +520,7 @@ void BuildMatte(const std::vector<Hand> &hands, const std::vector<Capsule> &file
|
||||
const MatteParams &p, Matte &out) {
|
||||
out.clear();
|
||||
const V head(headPos[0], headPos[1], headPos[2]);
|
||||
if (!p.mesh) {
|
||||
if (p.shape == MatteShape::Capsules) {
|
||||
// ft-hands' capsules have a margin of 4 mm in them already; the angular margin is
|
||||
// the mesh's (the capsule matte stays as it was, for comparison)
|
||||
out.caps = fileCapsules;
|
||||
@@ -524,6 +534,38 @@ void BuildMatte(const std::vector<Hand> &hands, const std::vector<Capsule> &file
|
||||
const double dist = (V(palm[0], palm[1], palm[2]) - head).norm();
|
||||
const double m = p.Margin(dist, h.speed());
|
||||
const double k = std::clamp((P[9] - P[0]).norm() / kPalmLen, 0.7, 1.4); // this hand's size
|
||||
if (p.shape == MatteShape::Circle) {
|
||||
// the middle of what the head sees of the hand: the box around its joints (each
|
||||
// with its half-width; the wrist's is the hand's) in a plane facing the head...
|
||||
V w;
|
||||
V u[21];
|
||||
double rr[21], d[21];
|
||||
for (int j = 0; j < 21; ++j) {
|
||||
d[j] = std::max(1e-3, (P[j] - head).norm());
|
||||
u[j] = (P[j] - head) * (1 / d[j]), w = w + u[j];
|
||||
rr[j] = (j ? JointRadius(j) : 0.025) * k;
|
||||
}
|
||||
w = w.unit();
|
||||
const V a = (std::fabs(w.y) < 0.9 ? V(0, 1, 0) : V(1, 0, 0)).cross(w).unit(), b = w.cross(a);
|
||||
double lo[2] = {1e9, 1e9}, hi[2] = {-1e9, -1e9};
|
||||
for (int j = 0; j < 21; ++j) {
|
||||
const double f = u[j].dot(w), t = rr[j] / d[j];
|
||||
if (f < 0.1) continue; // beside the head: not seen
|
||||
const double x = u[j].dot(a) / f, y = u[j].dot(b) / f;
|
||||
lo[0] = std::min(lo[0], x - t), hi[0] = std::max(hi[0], x + t);
|
||||
lo[1] = std::min(lo[1], y - t), hi[1] = std::max(hi[1], y + t);
|
||||
}
|
||||
if (lo[0] > hi[0]) continue;
|
||||
const V c = (w + a * ((lo[0] + hi[0]) / 2) + b * ((lo[1] + hi[1]) / 2)).unit();
|
||||
// ...and the widest angle from there to the hand's edge, at the palm's distance
|
||||
double reach = 0;
|
||||
for (int j = 0; j < 21; ++j)
|
||||
reach = std::max(reach, std::acos(std::clamp(u[j].dot(c), -1.0, 1.0)) + std::atan(rr[j] / d[j]));
|
||||
const double r = dist * std::tan(std::min(reach, 1.2)) + m;
|
||||
const V at = head + c * dist;
|
||||
out.circles.push_back({{float(at.x), float(at.y), float(at.z)}, float(r)});
|
||||
continue;
|
||||
}
|
||||
// fingers: a tapered capsule per bone; the tip's end pulled back (MediaPipe's tip
|
||||
// point is the very end of the finger, a capsule's round end reaches past it)
|
||||
static const int kBones[][2] = {{1, 2}, {2, 3}, {3, 4}, {5, 6}, {6, 7}, {7, 8}, {9, 10}, {10, 11},
|
||||
@@ -651,6 +693,56 @@ bool ProjectCapsule(const Panel &p, const Capsule &c, const double eye[3], Capsu
|
||||
return true;
|
||||
}
|
||||
|
||||
// A disk facing the eye, as the eye sees it on the panel: an ellipse there (a circle on a
|
||||
// panel square to the eye), drawn as the stadium around it (a capsule as long as the
|
||||
// ellipse, as wide as it is narrow), with the capsules' soft edge.
|
||||
bool ProjectCircle(const Panel &p, const Circle &k, const double eye[3], Capsule2D *out) {
|
||||
double c[3];
|
||||
const double w[3] = {k.c[0], k.c[1], k.c[2]};
|
||||
ToLocal(p.pose, w, c);
|
||||
if (c[2] < kEps || c[2] > eye[2] - kNear) return false; // behind the panel, or at the eye
|
||||
double cx, cy, g;
|
||||
if (!OnPanel(p, eye, c, &cx, &cy, &g)) return false;
|
||||
const V e(eye[0], eye[1], eye[2]), at(c[0], c[1], c[2]), d = (at - e).unit();
|
||||
const V a = (std::fabs(d.y) < 0.9 ? V(0, 1, 0) : V(1, 0, 0)).cross(d).unit(), b = d.cross(a);
|
||||
// the rim, along the rays from the eye through it (a point near the eye on each: the
|
||||
// rim may reach past the panel's plane, the ray still meets the panel)
|
||||
constexpr int kRim = 16;
|
||||
double rim[kRim][2];
|
||||
int n = 0;
|
||||
for (int i = 0; i < kRim; ++i) {
|
||||
const double t = i * 2 * M_PI / kRim;
|
||||
const V q = at + (a * std::cos(t) + b * std::sin(t)) * k.r;
|
||||
if ((q - e).dot(d) <= 0) continue;
|
||||
const V near = e + (q - e) * 0.1;
|
||||
const double nq[3] = {near.x, near.y, near.z};
|
||||
double x, y, gr;
|
||||
if (OnPanel(p, eye, nq, &x, &y, &gr)) rim[n][0] = x - cx, rim[n][1] = y - cy, ++n;
|
||||
}
|
||||
if (n < kRim / 2) return false;
|
||||
// the long axis: toward the farthest rim point
|
||||
int far = 0;
|
||||
for (int i = 1; i < n; ++i)
|
||||
if (std::hypot(rim[i][0], rim[i][1]) > std::hypot(rim[far][0], rim[far][1])) far = i;
|
||||
const double len = std::hypot(rim[far][0], rim[far][1]);
|
||||
if (!(len > 0.5)) return false;
|
||||
const double ux = rim[far][0] / len, uy = rim[far][1] / len;
|
||||
double lo[2] = {1e9, 1e9}, hi[2] = {-1e9, -1e9};
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const double s = rim[i][0] * ux + rim[i][1] * uy, t = -rim[i][0] * uy + rim[i][1] * ux;
|
||||
lo[0] = std::min(lo[0], s), hi[0] = std::max(hi[0], s), lo[1] = std::min(lo[1], t), hi[1] = std::max(hi[1], t);
|
||||
}
|
||||
// the rim's points are 360/16 degrees apart: the ellipse reaches up to 2% past them
|
||||
const double grow = 1 / std::cos(M_PI / kRim);
|
||||
const double mx = cx + ux * (lo[0] + hi[0]) / 2 - uy * (lo[1] + hi[1]) / 2,
|
||||
my = cy + uy * (lo[0] + hi[0]) / 2 + ux * (lo[1] + hi[1]) / 2;
|
||||
const double A = (hi[0] - lo[0]) / 2 * grow, B = std::min(A, (hi[1] - lo[1]) / 2 * grow);
|
||||
const double half = A - B;
|
||||
if (mx + A < 0 || mx - A > p.pxWidth || my + A < 0 || my - A > p.pxHeight) return false;
|
||||
*out = {float(mx - ux * half), float(my - uy * half), float(mx + ux * half), float(my + uy * half), float(B), float(B)};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keeps the part of a convex point set with lo <= z <= hi: the points inside, and where the
|
||||
// segments between an inside and an outside point cross the plane (the clipped hull's corners
|
||||
// are among them).
|
||||
@@ -798,6 +890,10 @@ bool Project(const Panel &p, const Matte &m, const double eyes[2][3], Spots out[
|
||||
double eye[3];
|
||||
ToLocal(p.pose, eyes[e], eye);
|
||||
if (eye[2] <= 0.01) continue; // behind the panel
|
||||
for (const Circle &c : m.circles) {
|
||||
Capsule2D d;
|
||||
if (ProjectCircle(p, c, eye, &d)) out[e].caps.push_back(d), any = true;
|
||||
}
|
||||
for (const Capsule &c : m.caps) {
|
||||
Capsule2D d;
|
||||
if (ProjectCapsule(p, c, eye, &d)) out[e].caps.push_back(d), any = true;
|
||||
|
||||
+24
-8
@@ -8,11 +8,11 @@
|
||||
// with the head pose at that time, and reads the gestures file next to it (pinches, grips) for
|
||||
// hand input (handinput.h).
|
||||
//
|
||||
// The matte (what's cut out) is built from the hands each tick (BuildMatte): either the
|
||||
// capsules ft-hands publishes, or a hand-shaped one of convex pieces: a tapered capsule per
|
||||
// finger bone, the palm as a slab through the wrist and knuckles, the thumb's web, and the
|
||||
// forearm toward the elbow as a tapered tube. Project() finds where each eye sees them on a
|
||||
// panel, and Renderer draws the panel's client buffer into a side-by-side buffer (left eye |
|
||||
// The matte (what's cut out) is built from the hands each tick (BuildMatte): a round hole per
|
||||
// hand (the default: a disk facing the eye, wide enough for the whole hand), the capsules
|
||||
// ft-hands publishes, or a hand-shaped one of convex pieces: a tapered capsule per finger
|
||||
// bone, the palm as a slab through the wrist and knuckles, the thumb's web, and the forearm
|
||||
// toward the elbow as a tapered tube. Project() finds where each eye sees them on a panel, and Renderer draws the panel's client buffer into a side-by-side buffer (left eye |
|
||||
// right eye) with those spots transparent and the glows of hand input (fingertip, pinch point)
|
||||
// on top. A panel shows that buffer, with the overlay's SideBySide_Parallel flag, only while a
|
||||
// hand is in front of it.
|
||||
@@ -63,13 +63,21 @@ struct Hull {
|
||||
std::vector<std::array<float, 3>> v;
|
||||
};
|
||||
|
||||
// A round hole: a disk around c, radius r (metres, the margin in it), facing whichever eye
|
||||
// looks at it.
|
||||
struct Circle {
|
||||
float c[3];
|
||||
float r;
|
||||
};
|
||||
|
||||
struct Matte {
|
||||
std::vector<Circle> circles;
|
||||
std::vector<Capsule> caps;
|
||||
std::vector<Hull> hulls;
|
||||
// How much wider than the hand each piece is drawn (metres at the hand), per piece: the
|
||||
// capsules have it in their radii already; a hull grows by it on the panel.
|
||||
std::vector<float> hullMargin;
|
||||
void clear() { caps.clear(), hulls.clear(), hullMargin.clear(); }
|
||||
void clear() { circles.clear(), caps.clear(), hulls.clear(), hullMargin.clear(); }
|
||||
};
|
||||
|
||||
struct Capsule2D { // on a panel's texture, pixels from the top left
|
||||
@@ -153,8 +161,16 @@ struct Shift {
|
||||
double yawDeg, pitchDeg; // positive: to the right, up
|
||||
};
|
||||
|
||||
enum class MatteShape {
|
||||
Circle, // a round hole per hand
|
||||
Mesh, // the hand-shaped matte
|
||||
Capsules, // ft-hands' capsules
|
||||
};
|
||||
const char *ShapeName(MatteShape s);
|
||||
bool ParseShape(const std::string &name, MatteShape *out); // circle|mesh|capsules
|
||||
|
||||
struct MatteParams {
|
||||
bool mesh = true; // the hand-shaped matte (else ft-hands' capsules)
|
||||
MatteShape shape = MatteShape::Circle;
|
||||
// Margin around the hand, as an angle seen from the eyes: the probe measured the cutout
|
||||
// points 1.8-2.4 degrees (median) off the hand Room View shows with the hand still (p90
|
||||
// 4.1-4.5), 2.8-3.1 moving (p90 5.7-6.7); part of that is the video landmark model's own
|
||||
@@ -169,7 +185,7 @@ struct MatteParams {
|
||||
};
|
||||
|
||||
// Parses settings from ~/.config/frametop.conf (FT_<NAME> in the environment overrides):
|
||||
// HANDS_MATTE (mesh|capsules), HANDS_MATTE_MARGIN (deg), HANDS_MATTE_MOTION (ms),
|
||||
// HANDS_MATTE (circle|mesh|capsules), HANDS_MATTE_MARGIN (deg), HANDS_MATTE_MOTION (ms),
|
||||
// HANDS_MATTE_MOTION_MAX (deg), HANDS_MATTE_SHIFT ("dist:yaw,pitch dist:yaw,pitch ...", deg).
|
||||
// Returns what it read as "NAME=value" lines, for the log.
|
||||
std::string ReadMatteSettings(MatteParams &p);
|
||||
|
||||
@@ -110,6 +110,7 @@ void AddHull(const std::vector<std::array<float, 3>> &pts, Mesh &out) {
|
||||
void Build(const handcut::Hand &h, const double headPos[3], Mesh &out) {
|
||||
out.clear();
|
||||
handcut::MatteParams p;
|
||||
p.shape = handcut::MatteShape::Mesh;
|
||||
p.marginDeg = p.motionMaxDeg = p.minMarginM = 0;
|
||||
handcut::Matte matte;
|
||||
handcut::BuildMatte({h}, {}, headPos, p, matte);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// (a light grid) in front of you as its own overlay; where ft-hands tracks your hands
|
||||
// in front of it, each eye sees through it, like ft-screens' screens with cutouts.
|
||||
//
|
||||
// ft-handtest [--distance m] [--width m] [--seconds s] [--matte mesh|capsules] [--margin deg]
|
||||
// ft-handtest [--distance m] [--width m] [--seconds s] [--matte circle|mesh|capsules] [--margin deg]
|
||||
//
|
||||
// The matte follows HANDS_MATTE* in ~/.config/frametop.conf (handcut.h); --matte and
|
||||
// --margin override them. Every 2 s it prints the composite's cost (CPU ms per call, and how
|
||||
@@ -218,10 +218,10 @@ int main(int argc, char **argv) {
|
||||
else if (!std::strcmp(argv[i], "--lead") && more) leadMs = std::atof(argv[++i]);
|
||||
else if (!std::strcmp(argv[i], "--dot-mm") && more) dotMm = std::atof(argv[++i]);
|
||||
else if (!std::strcmp(argv[i], "--log") && more) logPath = argv[++i];
|
||||
else if (!std::strcmp(argv[i], "--matte") && more) matte.mesh = std::strcmp(argv[++i], "capsules") != 0;
|
||||
else if (!std::strcmp(argv[i], "--matte") && more && handcut::ParseShape(argv[i + 1], &matte.shape)) ++i;
|
||||
else if (!std::strcmp(argv[i], "--margin") && more) matte.marginDeg = std::atof(argv[++i]);
|
||||
else return std::fprintf(stderr, "usage: ft-handtest [--distance m] [--width m] [--seconds s] "
|
||||
"[--matte mesh|capsules] [--margin deg] [--probe [--lead ms] [--dot-mm mm] [--log FILE]]\n"), 2;
|
||||
"[--matte circle|mesh|capsules] [--margin deg] [--probe [--lead ms] [--dot-mm mm] [--log FILE]]\n"), 2;
|
||||
}
|
||||
signal(SIGINT, Stop);
|
||||
signal(SIGTERM, Stop);
|
||||
@@ -295,7 +295,7 @@ int main(int argc, char **argv) {
|
||||
hands.SetShift(matte.shift);
|
||||
handcut::Matte built;
|
||||
std::printf("matte: %s, margin %.2f deg + %.0f ms of motion (at most %.2f deg more)%s%s\n",
|
||||
matte.mesh ? "mesh" : "capsules", matte.marginDeg, matte.motionMs, matte.motionMaxDeg,
|
||||
handcut::ShapeName(matte.shape), matte.marginDeg, matte.motionMs, matte.motionMaxDeg,
|
||||
settings.empty() ? "" : "; settings: ", settings.c_str());
|
||||
bool cutting = false;
|
||||
const int64_t start = MonoNs();
|
||||
|
||||
@@ -695,10 +695,11 @@ void TestMatte() {
|
||||
Rig r;
|
||||
r.Frame({TipAt(0, 0, 0.1)});
|
||||
handcut::MatteParams mp;
|
||||
mp.shape = handcut::MatteShape::Mesh;
|
||||
handcut::Matte m;
|
||||
const double head[3] = {0, 0, 0};
|
||||
handcut::BuildMatte(r.hands.shown(), r.hands.capsules(), head, mp, m);
|
||||
CHECK(m.caps.size() == 15 && m.hulls.size() == 3);
|
||||
CHECK(m.caps.size() == 15 && m.hulls.size() == 3 && m.circles.empty());
|
||||
handcut::Panel p{At(0, 0, -0.5), 1.0, 0.5625, 0, 1920, 1080};
|
||||
const double eyes[2][3] = {{-0.032, 0, 0}, {0.032, 0, 0}};
|
||||
handcut::Spots s[2];
|
||||
@@ -723,6 +724,52 @@ void TestMatte() {
|
||||
handcut::BuildMatte(t.hands.shown(), t.hands.capsules(), head, mp, m);
|
||||
handcut::Project(p, m, eyes, s);
|
||||
for (const handcut::Capsule2D &k : s[0].caps) CHECK(k.ra < 400 && k.rb < 400);
|
||||
// the circle (the default): one round hole per hand, around every joint as each eye sees
|
||||
// it, and a margin past the outermost
|
||||
{
|
||||
Rig c;
|
||||
c.Frame({TipAt(0, 0, 0.1)});
|
||||
handcut::MatteParams cp;
|
||||
CHECK(cp.shape == handcut::MatteShape::Circle);
|
||||
handcut::BuildMatte(c.hands.shown(), c.hands.capsules(), head, cp, m);
|
||||
CHECK(m.circles.size() == 1 && m.caps.empty() && m.hulls.empty());
|
||||
CHECK(handcut::Project(p, m, eyes, s));
|
||||
const handcut::Hand &h = c.hands.shown()[0];
|
||||
for (int e = 0; e < 2; ++e) {
|
||||
CHECK(s[e].caps.size() == 1 && s[e].hulls.empty());
|
||||
if (s[e].caps.size() != 1) continue;
|
||||
const handcut::Capsule2D &k = s[e].caps[0];
|
||||
// a panel square to the head: round, but for the eye seeing the disk a little off its axis
|
||||
CHECK(std::hypot(k.bx - k.ax, k.by - k.ay) < 0.05 * k.ra);
|
||||
double worst = 0;
|
||||
for (int j = 0; j < 21; ++j) {
|
||||
// where the eye sees joint j on the panel (z -0.5)
|
||||
const double f = 0.5 / -double(h.p[j][2]);
|
||||
const double x = (eyes[e][0] + (h.p[j][0] - eyes[e][0]) * f + 0.5) * 1920, y = (0.5 - h.p[j][1] * f / 0.5625) * 1080;
|
||||
worst = std::max(worst, std::hypot(x - k.ax, y - k.ay));
|
||||
}
|
||||
const double margin = mp.Margin(0.45, 0) * 1.1 * 1920; // about the palm's distance
|
||||
CHECK(worst + margin < k.ra);
|
||||
CHECK(k.ra < worst + 0.06 * 1920); // ...not much more: a finger's width or so
|
||||
}
|
||||
// a panel turned 50 degrees: the hole is the disk seen at a slant, longer across
|
||||
handcut::Panel q{RotY(50 * M_PI / 180, 0, 0, -0.5), 1.0, 0.5625, 0, 1920, 1080};
|
||||
handcut::Spots t[2];
|
||||
CHECK(handcut::Project(q, m, eyes, t) && t[0].caps.size() == 1);
|
||||
if (t[0].caps.size() == 1) {
|
||||
const handcut::Capsule2D &k = t[0].caps[0];
|
||||
CHECK(std::fabs(k.bx - k.ax) > 10 && std::fabs(k.by - k.ay) < std::fabs(k.bx - k.ax) * 0.2);
|
||||
}
|
||||
// the hand pushed through the panel: no hole
|
||||
Rig d;
|
||||
d.Frame({TipAt(0, 0, -0.30)});
|
||||
handcut::BuildMatte(d.hands.shown(), d.hands.capsules(), head, cp, m);
|
||||
CHECK(m.circles.size() == 1 && !handcut::Project(p, m, eyes, s));
|
||||
// the setting's names
|
||||
handcut::MatteShape shape;
|
||||
CHECK(handcut::ParseShape("mesh", &shape) && shape == handcut::MatteShape::Mesh);
|
||||
CHECK(handcut::ParseShape("circle", &shape) && shape == handcut::MatteShape::Circle && !handcut::ParseShape("round", &shape));
|
||||
}
|
||||
// shift settings parse
|
||||
std::vector<handcut::Shift> sh;
|
||||
CHECK(handcut::ParseShift("0.5:-1,0.5 0.3:1.2,-0.4", sh) && sh.size() == 2 && sh[0].dist == 0.3);
|
||||
@@ -1140,14 +1187,14 @@ int Replay(const char *path) {
|
||||
}
|
||||
}
|
||||
// 3. The matte: every frame's hands against a panel 0.6 m ahead, CPU cost of building and
|
||||
// projecting it, mesh against capsules.
|
||||
// projecting it, circle, mesh and capsules.
|
||||
{
|
||||
handcut::Hands hands;
|
||||
handcut::Panel P{At(0, -0.15, -0.6), 1.2, 0.675, 0, 3440, 1935};
|
||||
const double eyes[2][3] = {{-0.032, 0, 0}, {0.032, 0, 0}}, head[3] = {0, 0, 0};
|
||||
for (bool mesh : {true, false}) {
|
||||
for (handcut::MatteShape shape : {handcut::MatteShape::Circle, handcut::MatteShape::Mesh, handcut::MatteShape::Capsules}) {
|
||||
handcut::MatteParams mp;
|
||||
mp.mesh = mesh;
|
||||
mp.shape = shape;
|
||||
handcut::Matte m;
|
||||
handcut::Spots s[2];
|
||||
std::vector<double> us;
|
||||
@@ -1168,7 +1215,7 @@ int Replay(const char *path) {
|
||||
std::sort(us.begin(), us.end());
|
||||
std::printf("matte %-8s: %zu frames with hands on the panel, %.1f pieces per eye, %zu bad; build+project "
|
||||
"median %.0f us, 90%% %.0f us, max %.0f us\n",
|
||||
mesh ? "mesh" : "capsules", withHands, withHands ? pieces / 2.0 / withHands : 0.0, bad,
|
||||
handcut::ShapeName(shape), withHands, withHands ? pieces / 2.0 / withHands : 0.0, bad,
|
||||
us[us.size() / 2], us[us.size() * 9 / 10], us.back());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// The cutout composite's cost, offline: recorded hands (ft-handreplay --publish-dump) cut out of
|
||||
// a 3440x1935 test buffer on a panel 0.6 m ahead, through handcut's Renderer on the GPU (no
|
||||
// SteamVR, no overlay), the hand-shaped matte against ft-hands' capsules.
|
||||
// SteamVR, no overlay), the round, hand-shaped and ft-hands' capsule mattes.
|
||||
//
|
||||
// matte_bench DUMP [--frames N] [--panel-px W H]
|
||||
//
|
||||
@@ -99,7 +99,8 @@ int main(int argc, char **argv) {
|
||||
// GPU's clocks and whatever else runs weigh on them alike.
|
||||
struct Mode {
|
||||
const char *name;
|
||||
bool mesh, glows, still;
|
||||
handcut::MatteShape shape;
|
||||
bool glows, still;
|
||||
handcut::Hands hands;
|
||||
handcut::Matte m;
|
||||
std::vector<double> cpu, gpu;
|
||||
@@ -108,9 +109,10 @@ int main(int argc, char **argv) {
|
||||
};
|
||||
std::vector<Mode> modes;
|
||||
for (bool still : {false, true}) {
|
||||
modes.push_back({"mesh", true, false, still, {}, {}, {}, {}});
|
||||
modes.push_back({"capsules", false, false, still, {}, {}, {}, {}});
|
||||
modes.push_back({"mesh + 2 glows", true, true, still, {}, {}, {}, {}});
|
||||
modes.push_back({"circle", handcut::MatteShape::Circle, false, still, {}, {}, {}, {}});
|
||||
modes.push_back({"mesh", handcut::MatteShape::Mesh, false, still, {}, {}, {}, {}});
|
||||
modes.push_back({"capsules", handcut::MatteShape::Capsules, false, still, {}, {}, {}, {}});
|
||||
modes.push_back({"circle + 2 glows", handcut::MatteShape::Circle, true, still, {}, {}, {}, {}});
|
||||
}
|
||||
uint64_t serial = 1;
|
||||
for (size_t k = 0; k < hands.size(); ++k) {
|
||||
@@ -119,7 +121,7 @@ int main(int argc, char **argv) {
|
||||
for (size_t i = 0; i < modes.size(); ++i) {
|
||||
Mode &md = modes[i];
|
||||
handcut::MatteParams mp;
|
||||
mp.mesh = md.mesh;
|
||||
mp.shape = md.shape;
|
||||
md.hands.Feed(hands[k].data(), hands[k].size(), &gestures[k], Identity(), int64_t(v1->publish_ns));
|
||||
handcut::BuildMatte(md.hands.shown(), md.hands.capsules(), head, mp, md.m);
|
||||
handcut::Spots s[2];
|
||||
@@ -137,7 +139,7 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
}
|
||||
for (const Mode &md : modes)
|
||||
std::printf("%-15s %-12s %d composites (%d draws, %d partial): CPU median %.3f ms, 90%% %.3f; GPU finish median "
|
||||
std::printf("%-16s %-12s %d composites (%d draws, %d partial): CPU median %.3f ms, 90%% %.3f; GPU finish median "
|
||||
"%.2f ms, 90%% %.2f, max %.2f; %.1f pieces per eye\n",
|
||||
md.name, md.still ? "still screen" : "video", md.cut, md.draws, md.partial, Pct(md.cpu, 0.5),
|
||||
Pct(md.cpu, 0.9), Pct(md.gpu, 0.5), Pct(md.gpu, 0.9), Pct(md.gpu, 1), md.cut ? md.pieces / md.cut : 0.0);
|
||||
|
||||
+9
-8
@@ -56,7 +56,8 @@
|
||||
// - hand cutouts (handcut.cpp): where ft-hands (hands/) tracks a hand between an eye and a
|
||||
// screen, that eye sees through the screen (to Room View). Only then is the screen
|
||||
// drawn by us, into a side-by-side buffer (one half per eye); otherwise its client
|
||||
// buffer is shown as is. The matte is hand-shaped (or ft-hands' capsules, HANDS_MATTE).
|
||||
// buffer is shown as is. The matte is a round hole per hand (or hand-shaped, or ft-hands'
|
||||
// capsules: HANDS_MATTE).
|
||||
// Only with Room View on (roomview.h): with it off a cutout would show the void or the game,
|
||||
// and ft-handview (handview.cpp) draws the hands instead.
|
||||
// - near hand input (handinput.cpp): poke a screen or floating window with the index finger
|
||||
@@ -2370,7 +2371,7 @@ bool ft_vr_init(void) {
|
||||
std::printf("hands: pokes within %.0f deg of a panel's normal, not from a hand %.2f m under the eyes and within %.2f m\n",
|
||||
k.fingerAngle, k.restBelow, k.restNear);
|
||||
std::printf("hands: matte %s, margin %.2f deg + %.0f ms of motion (at most %.2f more), lead %.0f ms, shift %s; near input %s%s%s\n",
|
||||
g_matte.mesh ? "mesh" : "capsules", g_matte.marginDeg, g_matte.motionMs, g_matte.motionMaxDeg,
|
||||
handcut::ShapeName(g_matte.shape), g_matte.marginDeg, g_matte.motionMs, g_matte.motionMaxDeg,
|
||||
g_hands.leadMs(), g_matte.shift.empty() ? "off" : "on",
|
||||
g_nearMode == NearMode::Off ? "off" : g_nearMode == NearMode::Force ? "forced on" : "on (the pointer helper's word)",
|
||||
matte.empty() ? "" : "; settings: ", matte.c_str());
|
||||
@@ -2966,7 +2967,7 @@ void ft_vr_keyboard_hide(void) {
|
||||
// cutouts roomview auto|on|off cut only with Room View on: detected (roomview.h), or forced
|
||||
// cutouts predict on|off move the hands ahead along their velocity (on by default)
|
||||
// cutouts lead <ms> ...to this long after now: about when the frame is on the displays
|
||||
// cutouts matte mesh|capsules the hand-shaped matte, or ft-hands' capsules
|
||||
// cutouts matte circle|mesh|capsules a round hole per hand, the hand-shaped matte, or ft-hands' capsules
|
||||
// cutouts margin <deg> [<motion ms> [<motion max deg>]] the matte's margin around the hand
|
||||
// cutouts shift off|"<dist>:<yaw>,<pitch> ..." the per-distance correction (degrees; off by default)
|
||||
// hands <L> <R> <allowed> the pointer helper: each side hand|controller|off, hand input allowed 0|1
|
||||
@@ -3180,9 +3181,9 @@ void ft_vr_command(const char *cmd, char *reply, int size) {
|
||||
g_hands.SetPrediction(!std::strcmp(arg, "on"), g_hands.leadMs());
|
||||
else if (!std::strcmp(word, "lead") && std::sscanf(cmd, "cutouts lead %lf", &ms) == 1)
|
||||
g_hands.SetPrediction(g_hands.predicting(), ms);
|
||||
else if (!std::strcmp(word, "matte") && std::sscanf(cmd, "cutouts matte %15s", arg) == 1 &&
|
||||
(!std::strcmp(arg, "mesh") || !std::strcmp(arg, "capsules")))
|
||||
g_matte.mesh = !std::strcmp(arg, "mesh");
|
||||
else if (handcut::MatteShape shape; !std::strcmp(word, "matte") && std::sscanf(cmd, "cutouts matte %15s", arg) == 1 &&
|
||||
handcut::ParseShape(arg, &shape))
|
||||
g_matte.shape = shape;
|
||||
else if (const int got = std::sscanf(cmd, "cutouts margin %lf %lf %lf", &ms, &a, &b);
|
||||
!std::strcmp(word, "margin") && got >= 1 && ms >= 0 && ms <= 10) {
|
||||
g_matte.marginDeg = ms;
|
||||
@@ -3200,7 +3201,7 @@ void ft_vr_command(const char *cmd, char *reply, int size) {
|
||||
g_roomView.Update(std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now().time_since_epoch()).count(),
|
||||
g_gameRunning, g_constructRoomView);
|
||||
} else if (std::strcmp(word, "state") != 0)
|
||||
return (void)std::snprintf(reply, size, "error cutouts on|off|state|predict on|off|lead <ms>|matte mesh|capsules|"
|
||||
return (void)std::snprintf(reply, size, "error cutouts on|off|state|predict on|off|lead <ms>|matte circle|mesh|capsules|"
|
||||
"margin <deg> [ms [deg]]|shift off|<spec>|roomview auto|on|off");
|
||||
// Composite's counts since the last state, per second.
|
||||
static auto since = Clock::now();
|
||||
@@ -3215,7 +3216,7 @@ void ft_vr_command(const char *cmd, char *reply, int size) {
|
||||
g_cutouts ? "on" : "off", g_cutterState > 0 ? "ready" : g_cutterState < 0 ? "unavailable" : "idle",
|
||||
g_cutter.lastMs(), g_hands.predicting() ? "on" : "off", g_hands.leadMs(), c.draws / dt,
|
||||
c.partial / dt, c.same / dt, c.busy / dt, c.waits / dt, calls ? c.cpuMs / calls : 0.0, c.worstMs,
|
||||
g_matte.mesh ? "mesh" : "capsules", g_matte.marginDeg, g_matte.motionMs, g_matte.motionMaxDeg,
|
||||
handcut::ShapeName(g_matte.shape), g_matte.marginDeg, g_matte.motionMs, g_matte.motionMaxDeg,
|
||||
g_matte.shift.empty() ? "off" : "on", g_roomView.on() ? "on" : "off", g_roomView.why(),
|
||||
g_roomView.on() ? "" : ": no cutouts");
|
||||
} else if (int x0, y0, w0, h0, t0; std::sscanf(cmd, "float %d %lf %d %d %d %d %d", &n, &w, &x0, &y0, &w0, &h0, &t0) == 7) {
|
||||
|
||||
@@ -50,7 +50,7 @@ HANDS_BRIGHT_OFF=25 # ... and back to the mono cameras under this
|
||||
HANDS_COLOR_LEFT=color_video0 # hand tracking: which colour camera is passthrough_left (hands/tools/check_color.py tells)
|
||||
HANDS_COLOR_CROP=subtract # hand tracking: how the colour calibration's crop applies, subtract | none (check_color.py tells)
|
||||
#HANDS_MODELS= # hand tracking: a folder of palm.ncnn.* and hand.ncnn.* to use instead of the stock models (hands/models/ncnn)
|
||||
HANDS_MATTE=mesh # hand cutouts: mesh = hand-shaped, from the skeleton | capsules = ft-hands' capsules (the old look)
|
||||
HANDS_MATTE=circle # hand cutouts: circle = a round hole per hand | mesh = hand-shaped, from the skeleton | capsules = ft-hands' capsules (the old look)
|
||||
HANDS_MATTE_MARGIN=1.5 # hand cutouts: degrees (seen from the eyes) the hole reaches past the hand, held still (Hand cutout margin on the Hands page, live)
|
||||
HANDS_MATTE_MOTION=25 # ... plus how far the hand moves in this many ms
|
||||
HANDS_MATTE_MOTION_MAX=1.5 # ... up to this many degrees more
|
||||
|
||||
Reference in new issue
Block a user