code wiki / _hdl_build / nx_facs.nx

nx_facs.nx source

↩ module page · 257 lines · 13683 B

1// nx_facs.nx -- ★FACIAL ACTION CODING AS DATA: the expression basis, separated from identity. 2// 3// ★THE DEFECT THIS FIXES, and this codebase has cured it once already. nx_anatstack drives expressions 4// from `AS_EXPR`, a SINGLE ENUM (0 neutral, 1 smile, 2 surprise, 3 frown) fed to an if-chain of 5// hardcoded part indices and magnitudes. Two consequences, and the second is structural: 6// (a) expressions are CODE, not data -- adding one means editing a function; 7// (b) ★★ONLY ONE EXPRESSION CAN EXIST AT A TIME. You cannot smile AND raise a brow. A real face is 8// always a COMBINATION, so the enum cannot represent even ordinary human faces. 9// ★nx_skelgen already wrote the cure for exactly this shape: "THE BODY PLAN AS DATA -- until now the 10// plan WAS the code: a chain of if b == n ... that works for exactly one species and one subject." 11// Same disease, same fix: a TABLE. 12// 13// ★★★AND THE RIGHT TABLE IS FACS, BECAUSE OUR EXPRESSIONS ARE MUSCLE-DRIVEN. An Action Unit IS a named 14// muscle action (AU12 = zygomaticus major, AU4 = corrugator), so for an anatomically-driven stack the 15// standard basis and the physical basis are THE SAME THING. Google's GNM spends 150 statistical 16// parameters on the lower face; a causal system needs ~30 named muscle actions to span the same space, 17// because each one means something. **That is our exceed axis: expression by CAUSE, not by fit.** 18// 19// SEPARATION OF CONCERNS, which is GNM's first architectural idea: IDENTITY is the anthropometric 20// parameter set (who the face is); EXPRESSION is an AU WEIGHT VECTOR (what it is doing). They no 21// longer share a representation and can be solved independently. 22// 23// nx_facs selftest 24// license_tier: ORIGINAL expect_exit: 0 No hw writes (Rule 26). 25import "nx_anatstack.nx" 26import "nx_gate_verdict.nx" 27 28// effect channels -- these name the four deformation verbs nx_anatstack already exposes 29const FA_SHIFTY: i64 = 0 30const FA_SHIFTZ: i64 = 1 31const FA_GROWX: i64 = 2 32const FA_GROWY: i64 = 3 33// action units actually implemented (FACS numbering, kept so the ids mean what the literature means) 34const AU1: i64 = 1 // inner brow raiser -- frontalis, medial 35const AU2: i64 = 2 // outer brow raiser -- frontalis, lateral 36const AU4: i64 = 4 // brow lowerer -- corrugator supercilii 37const AU5: i64 = 5 // upper lid raiser -- levator palpebrae 38const AU6: i64 = 6 // cheek raiser -- orbicularis oculi, pars orbitalis 39const AU12: i64 = 12 // lip corner puller -- zygomaticus major 40const AU15: i64 = 15 // lip corner depressor-- depressor anguli oris 41const AU26: i64 = 26 // jaw drop -- masseter relaxation / lateral pterygoid 42const FA_MAXAU: i64 = 64 // weight slots, indexed by FACS number 43const FA_NEFF: i64 = 21 // rows in the effect table 44const FA_ES: i64 = 4 // effect stride: au, part, channel, amount-at-full-intensity 45const FA_FULL: i64 = 1024 46 47// ★THE BASIS AS DATA. Every row is (action unit, anatstack part, channel, displacement at full 48// intensity). The magnitudes are the ones nx_anatstack's if-chain already used, DECOMPOSED onto the 49// muscle that causes them -- so this is a re-derivation of proven numbers, not a new guess. 50func fa_table(E: *i64) -> i64 { 51 var i: i64 = 0 52 // AU6 cheek raiser: malar fat up and back, both sides 53 E[i]=AU6; E[i+1]=25; E[i+2]=FA_SHIFTY; E[i+3]=55; i=i+FA_ES 54 E[i]=AU6; E[i+1]=25; E[i+2]=FA_SHIFTZ; E[i+3]=12; i=i+FA_ES 55 E[i]=AU6; E[i+1]=26; E[i+2]=FA_SHIFTY; E[i+3]=55; i=i+FA_ES 56 E[i]=AU6; E[i+1]=26; E[i+2]=FA_SHIFTZ; E[i+3]=12; i=i+FA_ES 57 // AU12 lip corner puller: mouth widens and rises 58 E[i]=AU12; E[i+1]=31; E[i+2]=FA_GROWX; E[i+3]=40; i=i+FA_ES 59 E[i]=AU12; E[i+1]=31; E[i+2]=FA_SHIFTY; E[i+3]=16; i=i+FA_ES 60 E[i]=AU12; E[i+1]=32; E[i+2]=FA_GROWX; E[i+3]=36; i=i+FA_ES 61 E[i]=AU12; E[i+1]=32; E[i+2]=FA_SHIFTY; E[i+3]=10; i=i+FA_ES 62 // AU1 / AU2 brow raise (frontalis) -- split medial/lateral, which the enum could not distinguish 63 E[i]=AU1; E[i+1]=8; E[i+2]=FA_SHIFTY; E[i+3]=45; i=i+FA_ES 64 E[i]=AU2; E[i+1]=22; E[i+2]=FA_SHIFTY; E[i+3]=40; i=i+FA_ES 65 // AU5 upper lid raiser: the eye aperture opens 66 E[i]=AU5; E[i+1]=29; E[i+2]=FA_GROWY; E[i+3]=15; i=i+FA_ES 67 E[i]=AU5; E[i+1]=30; E[i+2]=FA_GROWY; E[i+3]=15; i=i+FA_ES 68 // AU26 jaw drop: mandible and the lower mouth follow it down 69 E[i]=AU26; E[i+1]=9; E[i+2]=FA_SHIFTY; E[i+3]=0-90; i=i+FA_ES 70 E[i]=AU26; E[i+1]=10; E[i+2]=FA_SHIFTY; E[i+3]=0-90; i=i+FA_ES 71 E[i]=AU26; E[i+1]=11; E[i+2]=FA_SHIFTY; E[i+3]=0-90; i=i+FA_ES 72 E[i]=AU26; E[i+1]=12; E[i+2]=FA_SHIFTY; E[i+3]=0-90; i=i+FA_ES 73 E[i]=AU26; E[i+1]=32; E[i+2]=FA_SHIFTY; E[i+3]=0-120;i=i+FA_ES 74 // AU4 brow lowerer (corrugator) 75 E[i]=AU4; E[i+1]=8; E[i+2]=FA_SHIFTY; E[i+3]=0-32; i=i+FA_ES 76 E[i]=AU4; E[i+1]=22; E[i+2]=FA_SHIFTY; E[i+3]=0-20; i=i+FA_ES 77 // AU15 lip corner depressor 78 E[i]=AU15; E[i+1]=31; E[i+2]=FA_SHIFTY; E[i+3]=0-22; i=i+FA_ES 79 E[i]=AU15; E[i+1]=32; E[i+2]=FA_SHIFTY; E[i+3]=0-28; i=i+FA_ES 80 return 0 81} 82func fa_clear(W: *i64) -> i64 { var i: i64 = 0; while i < FA_MAXAU { W[i] = 0; i = i + 1 } return 0 } 83func fa_set(W: *i64, au: i64, w: i64) -> i64 { 84 if au < 0 { return 0-1 } 85 if au >= FA_MAXAU { return 0-1 } 86 W[au] = w 87 return 0 88} 89// ★APPLY EVERY ACTIVE AU, ADDITIVELY. This one loop is the whole capability the enum lacked: any 90// number of action units may be non-zero at once and their effects SUM on the shared parts. 91func fa_apply(P: *i64, W: *i64, E: *i64) -> i64 { 92 var r: i64 = 0 93 while r < FA_NEFF { 94 let au: i64 = E[r*FA_ES] 95 let w: i64 = W[au] 96 if w != 0 { 97 let part: i64 = E[r*FA_ES+1] 98 let ch: i64 = E[r*FA_ES+2] 99 let amt: i64 = E[r*FA_ES+3] * w / FA_FULL 100 if ch == FA_SHIFTY { as_shifty(P, part, amt) } 101 if ch == FA_SHIFTZ { as_shiftz(P, part, amt) } 102 if ch == FA_GROWX { as_growx(P, part, amt) } 103 if ch == FA_GROWY { as_growy(P, part, amt) } 104 } 105 r = r + 1 106 } 107 return 0 108} 109// the three legacy expressions, now as AU COMBINATIONS rather than as branches 110func fa_smile(W: *i64, t: i64) -> i64 { fa_set(W,AU6,t); fa_set(W,AU12,t); return 0 } 111func fa_surprise(W: *i64, t: i64) -> i64 { fa_set(W,AU1,t); fa_set(W,AU2,t); fa_set(W,AU5,t); fa_set(W,AU26,t); return 0 } 112func fa_frown(W: *i64, t: i64) -> i64 { fa_set(W,AU4,t); fa_set(W,AU15,t); return 0 } 113 114const FA_NP: i64 = 64 115// snapshot every part's centre so two builds can be compared numerically 116func fa_snap(P: *i64, np: i64, out: *i64) -> i64 { 117 var k: i64 = 0 118 while k < np*3 { out[k] = 0; k = k + 1 } 119 k = 0 120 while k < np { 121 // ⚠the part record is `layer, type, p1(3), r(3), p2(3), _` -- the CENTRE p1 is at +2,+3,+4. 122 // The first version read +1,+2,+3, i.e. (type, x, y), so the "y" slot actually held x and the 123 // lip-rise measurement read a coordinate AU12 never touches. 124 // ★★AND SEVEN OF EIGHT TEETH STILL PASSED, because they compared two snapshots taken the SAME 125 // wrong way -- self-consistent comparisons cannot catch a wrong field mapping. Only T5, which 126 // referenced an ABSOLUTE named quantity (the lip's rise in millimetres), could. 127 out[k*3] = P[k*AS_STRIDE+2] 128 out[k*3+1] = P[k*AS_STRIDE+3] 129 out[k*3+2] = P[k*AS_STRIDE+4] 130 k = k + 1 131 } 132 return 0 133} 134func fa_diff(a: *i64, b: *i64, n: i64) -> i64 { 135 var d: i64 = 0 136 var i: i64 = 0 137 while i < n*3 { var t: i64 = a[i]-b[i]; if t < 0 { t = 0-t } d = d + t; i = i + 1 } 138 return d 139} 140 141func main(argc: i64, argv: *i64) -> i64 { 142 let ctr: *i64 = gv_ctr() 143 gv_head("nx_facs selftest -- FACS action units as DATA; expression separated from identity" as *u8) 144 let E: *i64 = sys_mmap(FA_NEFF*FA_ES*8) as *i64 145 fa_table(E) 146 let W: *i64 = sys_mmap(FA_MAXAU*8) as *i64 147 let P: *i64 = sys_mmap(FA_NP*AS_STRIDE*8) as *i64 148 let sLegacy: *i64 = sys_mmap(FA_NP*3*8) as *i64 149 let sFacs: *i64 = sys_mmap(FA_NP*3*8) as *i64 150 let sNeutral: *i64 = sys_mmap(FA_NP*3*8) as *i64 151 152 // neutral reference 153 anat_set_expr(0, 0) 154 let np: i64 = anatstack_build(P) 155 fa_snap(P, np, sNeutral) 156 var t0: i64 = 0 157 if np >= 34 { t0 = 1 } 158 gv_check("T0 the layered head builds (anatstack parts >= 34)" as *u8, t0, ctr) 159 160 // ★★T1 FAITHFUL RE-DERIVATION: the AU decomposition of SMILE must reproduce the legacy branch 161 // EXACTLY. If it does not, the table is a new expression wearing the old name. 162 anat_set_expr(1, FA_FULL) 163 anatstack_build(P) 164 fa_snap(P, np, sLegacy) 165 anat_set_expr(0, 0) 166 anatstack_build(P) 167 fa_clear(W); fa_smile(W, FA_FULL); fa_apply(P, W, E) 168 fa_snap(P, np, sFacs) 169 let dsm: i64 = fa_diff(sLegacy, sFacs, np) 170 gv_puts(" smile: legacy-vs-AU total part delta=" as *u8); gv_num(dsm); gv_puts("\n" as *u8) 171 var t1: i64 = 0 172 if dsm == 0 { t1 = 1 } 173 gv_check("T1 AU6+AU12 reproduces the legacy SMILE exactly" as *u8, t1, ctr) 174 175 // T2 same for surprise (the 4-AU one) and frown 176 anat_set_expr(2, FA_FULL); anatstack_build(P); fa_snap(P, np, sLegacy) 177 anat_set_expr(0, 0); anatstack_build(P) 178 fa_clear(W); fa_surprise(W, FA_FULL); fa_apply(P, W, E); fa_snap(P, np, sFacs) 179 let dsu: i64 = fa_diff(sLegacy, sFacs, np) 180 anat_set_expr(3, FA_FULL); anatstack_build(P); fa_snap(P, np, sLegacy) 181 anat_set_expr(0, 0); anatstack_build(P) 182 fa_clear(W); fa_frown(W, FA_FULL); fa_apply(P, W, E); fa_snap(P, np, sFacs) 183 let dfr: i64 = fa_diff(sLegacy, sFacs, np) 184 gv_puts(" surprise delta=" as *u8); gv_num(dsu) 185 gv_puts(" frown delta=" as *u8); gv_num(dfr); gv_puts("\n" as *u8) 186 var t2: i64 = 0 187 if dsu == 0 { if dfr == 0 { t2 = 1 } } 188 gv_check("T2 SURPRISE (4 AUs) and FROWN (2 AUs) reproduce their legacy branches exactly" as *u8, t2, ctr) 189 190 // ★★★T3 THE CAPABILITY THE ENUM STRUCTURALLY COULD NOT HAVE: two expressions AT ONCE. 191 // Smile while raising the inner brow -- AS_EXPR can hold only one value, so this face was 192 // previously unrepresentable at any intensity. 193 anat_set_expr(0, 0); anatstack_build(P) 194 fa_clear(W); fa_set(W, AU12, FA_FULL); fa_apply(P, W, E) 195 let sOnly12: *i64 = sys_mmap(FA_NP*3*8) as *i64 196 fa_snap(P, np, sOnly12) 197 anat_set_expr(0, 0); anatstack_build(P) 198 fa_clear(W); fa_set(W, AU1, FA_FULL); fa_apply(P, W, E) 199 let sOnly1: *i64 = sys_mmap(FA_NP*3*8) as *i64 200 fa_snap(P, np, sOnly1) 201 anat_set_expr(0, 0); anatstack_build(P) 202 fa_clear(W); fa_set(W, AU12, FA_FULL); fa_set(W, AU1, FA_FULL); fa_apply(P, W, E) 203 let sBoth: *i64 = sys_mmap(FA_NP*3*8) as *i64 204 fa_snap(P, np, sBoth) 205 // the combination must differ from EACH single AU, and carry both of their displacements 206 let d1: i64 = fa_diff(sBoth, sOnly12, np) 207 let d2: i64 = fa_diff(sBoth, sOnly1, np) 208 gv_puts(" combination vs AU12-only=" as *u8); gv_num(d1) 209 gv_puts(" vs AU1-only=" as *u8); gv_num(d2); gv_puts("\n" as *u8) 210 var t3: i64 = 0 211 if d1 > 0 { if d2 > 0 { t3 = 1 } } 212 gv_check("T3 COMPOSITION: two AUs act simultaneously -- unrepresentable under the enum" as *u8, t3, ctr) 213 214 // ★T4 and the composition is genuinely ADDITIVE on disjoint parts: brow moved by exactly AU1's 215 // amount and mouth by exactly AU12's, so combining did not dilute either. 216 var t4: i64 = 1 217 if sBoth[8*3+1] != sOnly1[8*3+1] { t4 = 0 } 218 if sBoth[31*3+1] != sOnly12[31*3+1] { t4 = 0 } 219 gv_check("T4 ADDITIVE: each AU keeps its full displacement in the combination" as *u8, t4, ctr) 220 221 // T5 intensity is linear: half weight, half displacement 222 anat_set_expr(0, 0); anatstack_build(P) 223 fa_clear(W); fa_set(W, AU12, FA_FULL/2); fa_apply(P, W, E) 224 let sHalf: *i64 = sys_mmap(FA_NP*3*8) as *i64 225 fa_snap(P, np, sHalf) 226 let full: i64 = sOnly12[31*3+1] - sNeutral[31*3+1] 227 let half: i64 = sHalf[31*3+1] - sNeutral[31*3+1] 228 gv_puts(" AU12 lip rise: full=" as *u8); gv_num(full) 229 gv_puts(" half=" as *u8); gv_num(half); gv_puts("\n" as *u8) 230 gv_puts(" DIAG np=" as *u8); gv_num(np) 231 gv_puts(" neutral[31].y=" as *u8); gv_num(sNeutral[31*3+1]) 232 gv_puts(" only12[31].y=" as *u8); gv_num(sOnly12[31*3+1]) 233 gv_puts(" half[31].y=" as *u8); gv_num(sHalf[31*3+1]) 234 gv_puts(" only12[32].y=" as *u8); gv_num(sOnly12[32*3+1]) 235 gv_puts(" neutral[32].y=" as *u8); gv_num(sNeutral[32*3+1]); gv_puts("\n" as *u8) 236 var t5: i64 = 0 237 if full != 0 { if half*2 == full { t5 = 1 } } 238 gv_check("T5 intensity is LINEAR: half weight gives exactly half the displacement" as *u8, t5, ctr) 239 240 // ★T6 ANTI-VACUITY: all weights zero must be a genuine no-op, so a face with no expression is the 241 // identity face and not a slightly-moved one. 242 anat_set_expr(0, 0); anatstack_build(P) 243 fa_clear(W); fa_apply(P, W, E) 244 let sZero: *i64 = sys_mmap(FA_NP*3*8) as *i64 245 fa_snap(P, np, sZero) 246 var t6: i64 = 0 247 if fa_diff(sZero, sNeutral, np) == 0 { t6 = 1 } 248 gv_check("T6 ANTI-VACUITY: a zero AU vector is exactly the neutral face" as *u8, t6, ctr) 249 250 // T7 an out-of-range AU is refused rather than writing past the weight table 251 var t7: i64 = 0 252 if fa_set(W, 0-1, 100) < 0 { if fa_set(W, FA_MAXAU, 100) < 0 { t7 = 1 } } 253 gv_check("T7 an unknown action unit is REFUSED, never written out of range" as *u8, t7, ctr) 254 255 return gv_verdict("FACS-GATE" as *u8, ctr, 256 "FACS action units as data: legacy expressions re-derived exactly, and combinations the enum could not represent" as *u8) 257}