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}