@@ -18,7 +18,13 @@ export const PosePoint = {
1818 rightHip : 24 ,
1919} as const ;
2020
21- /** Drawn as the torso: the shoulder line and the box down to the hips. */
21+ /**
22+ * Drawn as the torso: the shoulder line, and the box down to the hips when the hips are
23+ * genuinely in shot.
24+ *
25+ * A signer is usually framed from the chest up, so the hip edges are dropped far more often
26+ * than they are drawn. `isVisible` decides, per edge, at render time.
27+ */
2228export const TORSO_CONNECTIONS : readonly ( readonly [ number , number ] ) [ ] = [
2329 [ PosePoint . leftShoulder , PosePoint . rightShoulder ] ,
2430 [ PosePoint . leftShoulder , PosePoint . leftHip ] ,
@@ -34,11 +40,49 @@ export const ARM_CONNECTIONS: readonly (readonly [number, number])[] = [
3440 [ PosePoint . rightElbow , PosePoint . rightWrist ] ,
3541] ;
3642
37- /** Neck: the head sitting on the shoulder line, drawn as its own segment. */
38- export const NECK_CONNECTIONS : readonly ( readonly [ number , number ] ) [ ] = [
39- [ PosePoint . nose , PosePoint . leftShoulder ] ,
40- [ PosePoint . nose , PosePoint . rightShoulder ] ,
41- ] ;
43+ /**
44+ * Below this, a pose landmark is the model's guess rather than something it saw.
45+ *
46+ * Filming a signer from the chest up leaves the hips invisible, and MediaPipe answers with
47+ * extrapolated coordinates instead of nothing. Drawing those produces a torso box running
48+ * off the bottom of the picture and arms pointing at the frame edges.
49+ */
50+ export const MIN_POSE_VISIBILITY = 0.6 ;
51+
52+ export function isVisible ( point : Landmark | undefined ) : boolean {
53+ // Absent visibility means the estimator does not report it (hands, face) — trust those.
54+ return point !== undefined && ( point . visibility ?? 1 ) >= MIN_POSE_VISIBILITY ;
55+ }
56+
57+ /**
58+ * The neck: one segment from the middle of the shoulders up to the head.
59+ *
60+ * MediaPipe has no neck landmark, so it has to be derived. Drawing nose-to-each-shoulder
61+ * instead — the obvious shortcut — paints a wide triangle across the chest that looks
62+ * nothing like a neck and hides the signing space behind it.
63+ */
64+ export function neckSegment ( pose : readonly Landmark [ ] ) : readonly [ Landmark , Landmark ] | null {
65+ const left = pose [ PosePoint . leftShoulder ] ;
66+ const right = pose [ PosePoint . rightShoulder ] ;
67+ const nose = pose [ PosePoint . nose ] ;
68+ if ( ! isVisible ( left ) || ! isVisible ( right ) || ! isVisible ( nose ) || ! left || ! right || ! nose ) {
69+ return null ;
70+ }
71+
72+ const centre : Landmark = {
73+ x : ( left . x + right . x ) / 2 ,
74+ y : ( left . y + right . y ) / 2 ,
75+ z : ( left . z + right . z ) / 2 ,
76+ } ;
77+ // Stop short of the nose: the neck ends at the chin, and running the line into the face
78+ // mesh just clutters it.
79+ const chin : Landmark = {
80+ x : centre . x + ( nose . x - centre . x ) * 0.6 ,
81+ y : centre . y + ( nose . y - centre . y ) * 0.6 ,
82+ z : centre . z + ( nose . z - centre . z ) * 0.6 ,
83+ } ;
84+ return [ centre , chin ] ;
85+ }
4286
4387export interface PoseLandmarks {
4488 readonly points : readonly Landmark [ ] ;
0 commit comments