code wiki / _hdl_build / nx_anatomy_critic.nx
nx_anatomy_critic.nx source
↩ module page · 348 lines · 14976 B
1// nx_anatomy_critic.nx -- DOES THIS SHAPE READ AS A BODY? (ws=game-interact, 2026-07-27)
2//
3// ★WHY THIS EXISTS -- a measured failure of our own instrument, not a theory:
4// nx_game_critic scored an identity sheet **876 = SOTA**, HIGHER than every real game frame we have ever
5// measured (Veloren 1000, NetHack tiles 772), while the figures on it had DETACHED FLOATING HEADS,
6// pinched torsos and lollipop legs. It measures palette/detail/gradient/coverage/coherence -- all of which
7// shaded ellipses on a dark field maximise -- and it is structurally BLIND to proportion and to whether a
8// shape is even connected. A richness score is not an anatomy score.
9// ⇒ This is a SEPARATE organ on purpose (rule 9, and the banked law that a number mixing two categories
10// is not a measurement). nx_game_critic keeps meaning "is it visually rich". This one means "is it a body".
11//
12// It scores a FIGURE REGION (caller supplies the bbox -- the renderer knows where it drew) on properties
13// that are true of every human silhouette regardless of art style, from realistic to cartoon:
14// [0] CONNECTED -- the largest connected blob's share of all figure pixels, per-mille. A detached head
15// or a floating limb splits the body into components. THE tooth the SOTA-876 sheet failed.
16// [1] HEADRATIO -- head height as per-mille of total height, checked against the MEASURED canon band
17// (nx_anthro_gate uses headsTall 6.9..8.1 => 123..145 permil realistic). Style widens
18// the band (anime/cartoon legitimately have big heads) but never removes it.
19// [2] TAPER -- does width vary the way a body's does (shoulders > waist < hips)? A cylinder scores 0.
20// [3] VERTBALANCE -- mass distribution across thirds; a lollipop (all mass at the top) fails.
21// [4] LIMBSPAN -- lower-body width continuity: legs must reach the ground without vanishing.
22// All integer, no float. Operates on the packed-i64 framebuffer (R|G<<8|B<<16) every game screen uses.
23// LIB, no main. license_tier: ORIGINAL
24import "nx_syscalls.nx"
25
26const AC2_N: i64 = 5
27const AC2_M_CONN: i64 = 0
28const AC2_M_HEAD: i64 = 1
29const AC2_M_TAPER: i64 = 2
30const AC2_M_VBAL: i64 = 3
31const AC2_M_LIMB: i64 = 4
32
33// verdict tiers on the composite 0..1000
34const AC2_BROKEN: i64 = 300 // does not read as a body
35const AC2_CRUDE: i64 = 550 // reads as a body, badly proportioned
36const AC2_SOUND: i64 = 780 // anatomically sound silhouette
37
38func ac2_verdict(s: i64) -> *u8 {
39 if s < AC2_BROKEN { return "BROKEN" as *u8 }
40 if s < AC2_CRUDE { return "CRUDE" as *u8 }
41 if s < AC2_SOUND { return "SOUND" as *u8 }
42 return "CLEAN"
43}
44
45// a pixel belongs to the FIGURE if it differs from the background colour beyond a tolerance
46func ac2_is_fig(fb: *i64, w: i64, x: i64, y: i64, bg: i64) -> i64 {
47 let v: i64 = fb[y*w + x]
48 var dr: i64 = (v & 255) - (bg & 255)
49 if dr < 0 { dr = 0 - dr }
50 var dg: i64 = ((v >> 8) & 255) - ((bg >> 8) & 255)
51 if dg < 0 { dg = 0 - dg }
52 var db: i64 = ((v >> 16) & 255) - ((bg >> 16) & 255)
53 if db < 0 { db = 0 - db }
54 if dr + dg + db > 60 { return 1 }
55 return 0
56}
57
58// ---- CONNECTIVITY: 4-neighbour flood fill from the largest row's centre, iterative (no recursion).
59// Returns the largest component's pixel count; total figure pixels land in out[0].
60// ★This is the tooth a floating head fails: the head becomes its own component and the share drops.
61func ac2_largest_component(fb: *i64, w: i64, h: i64, x0: i64, y0: i64, x1: i64, y1: i64,
62 bg: i64, lab: *i64, out: *i64) -> i64 {
63 let bw: i64 = x1 - x0
64 let bh: i64 = y1 - y0
65 var total: i64 = 0
66 var i: i64 = 0
67 while i < bw*bh { lab[i] = 0; i = i + 1 }
68 var yy: i64 = 0
69 while yy < bh {
70 var xx: i64 = 0
71 while xx < bw {
72 if ac2_is_fig(fb, w, x0+xx, y0+yy, bg) == 1 { lab[yy*bw + xx] = 0-1; total = total + 1 }
73 xx = xx + 1
74 }
75 yy = yy + 1
76 }
77 out[0] = total
78 if total == 0 { return 0 }
79 // iterative flood fill over every unvisited figure pixel; keep the biggest component
80 let stack: *i64 = sys_mmap(bw*bh*8) as *i64
81 var best: i64 = 0
82 var comp: i64 = 0
83 var sy: i64 = 0
84 while sy < bh {
85 var sx: i64 = 0
86 while sx < bw {
87 if lab[sy*bw + sx] == 0-1 {
88 comp = comp + 1
89 var sp: i64 = 0
90 stack[sp] = sy*bw + sx
91 sp = sp + 1
92 lab[sy*bw + sx] = comp
93 var count: i64 = 0
94 while sp > 0 {
95 sp = sp - 1
96 let cur: i64 = stack[sp]
97 count = count + 1
98 let cx: i64 = cur % bw
99 let cy: i64 = cur / bw
100 var d: i64 = 0
101 while d < 4 {
102 var nx2: i64 = cx
103 var ny2: i64 = cy
104 if d == 0 { nx2 = cx + 1 }
105 if d == 1 { nx2 = cx - 1 }
106 if d == 2 { ny2 = cy + 1 }
107 if d == 3 { ny2 = cy - 1 }
108 if nx2 >= 0 { if nx2 < bw { if ny2 >= 0 { if ny2 < bh {
109 if lab[ny2*bw + nx2] == 0-1 {
110 lab[ny2*bw + nx2] = comp
111 stack[sp] = ny2*bw + nx2
112 sp = sp + 1
113 }
114 } } } }
115 d = d + 1
116 }
117 }
118 if count > best { best = count }
119 }
120 sx = sx + 1
121 }
122 sy = sy + 1
123 }
124 out[1] = comp
125 return best
126}
127
128// widest run of figure pixels on a row (the silhouette half-width measure)
129func ac2_row_width(fb: *i64, w: i64, x0: i64, x1: i64, y: i64, bg: i64) -> i64 {
130 var lo: i64 = 0 - 1
131 var hi: i64 = 0 - 1
132 var x: i64 = x0
133 while x < x1 {
134 if ac2_is_fig(fb, w, x, y, bg) == 1 {
135 if lo < 0 { lo = x }
136 hi = x
137 }
138 x = x + 1
139 }
140 if lo < 0 { return 0 }
141 return hi - lo + 1
142}
143// ★TORSO width = the CONTIGUOUS run of figure pixels through the figure's centre column, NOT the full
144// row span. MEASURED BUG THIS FIXES: arms hang beside the waist, so a full-span measurement reported the
145// waist as WIDER than the hips and every real body scored taper 0. The torso is the run you can walk
146// through the middle without leaving the figure; arms are separate runs and must be excluded.
147func ac2_center_run(fb: *i64, w: i64, x0: i64, x1: i64, cx: i64, y: i64, bg: i64) -> i64 {
148 if ac2_is_fig(fb, w, cx, y, bg) == 0 { return 0 }
149 // ⚠NishiLang has no `break`, so walk with an explicit done flag. (The first version clamped to the
150 // row edge on hitting background, which returned the WHOLE ROW and reproduced the very bug this
151 // function exists to fix -- taper stayed 0 because every row measured full width.)
152 var l: i64 = cx
153 var ldone: i64 = 0
154 while ldone == 0 {
155 if l <= x0 { ldone = 1 }
156 if ldone == 0 {
157 if ac2_is_fig(fb, w, l - 1, y, bg) == 0 { ldone = 1 }
158 if ldone == 0 { l = l - 1 }
159 }
160 }
161 var r: i64 = cx
162 var rdone: i64 = 0
163 while rdone == 0 {
164 if r >= x1 - 1 { rdone = 1 }
165 if rdone == 0 {
166 if ac2_is_fig(fb, w, r + 1, y, bg) == 0 { rdone = 1 }
167 if rdone == 0 { r = r + 1 }
168 }
169 }
170 return r - l + 1
171}
172
173// figure pixels on a row (mass, not span)
174func ac2_row_mass(fb: *i64, w: i64, x0: i64, x1: i64, y: i64, bg: i64) -> i64 {
175 var m: i64 = 0
176 var x: i64 = x0
177 while x < x1 { m = m + ac2_is_fig(fb, w, x, y, bg); x = x + 1 }
178 return m
179}
180
181func ac2_cap(v: i64, target: i64, weight: i64) -> i64 {
182 var s: i64 = v
183 if s > target { s = target }
184 if s < 0 { s = 0 }
185 return s * weight / target
186}
187
188// ---- THE SCORER. style: 0 realistic 1 anime 2 cartoon 3 VN -- widens the head band, never removes it.
189func ac2_score(fb: *i64, w: i64, h: i64, x0: i64, y0: i64, x1: i64, y1: i64,
190 bg: i64, style: i64, out: *i64) -> i64 {
191 var m: i64 = 0
192 while m < AC2_N { out[m] = 0; m = m + 1 }
193 let bw: i64 = x1 - x0
194 let bh: i64 = y1 - y0
195 if bw <= 2 { return 0 }
196 if bh <= 8 { return 0 }
197 let lab: *i64 = sys_mmap(bw*bh*8) as *i64
198 let cinfo: *i64 = sys_mmap(8*8) as *i64
199 cinfo[0] = 0
200 cinfo[1] = 0
201 let biggest: i64 = ac2_largest_component(fb, w, h, x0, y0, x1, y1, bg, lab, cinfo)
202 let total: i64 = cinfo[0]
203 if total == 0 { return 0 }
204 out[AC2_M_CONN] = biggest * 1000 / total
205
206 // ---- head height: scan from the top for the first sustained WAIST (a local minimum in width)
207 // that separates the head from the shoulders. If there is none, headratio reads as 0 = no neck.
208 var topy: i64 = 0 - 1
209 var boty: i64 = 0 - 1
210 var yy: i64 = 0
211 while yy < bh {
212 if ac2_row_width(fb, w, x0, x1, y0 + yy, bg) > 0 {
213 if topy < 0 { topy = yy }
214 boty = yy
215 }
216 yy = yy + 1
217 }
218 if topy < 0 { return 0 }
219 let figh: i64 = boty - topy + 1
220 // ★NECK = a genuine PINCH, not merely the narrowest row. Scanning for "narrowest in the upper 40%"
221 // finds the TOP OF THE SKULL (an ellipse tapers to a point there) and reported a giant lollipop head
222 // as a normal head -- measured, then fixed. A neck is a row whose width collapses to <55% of the
223 // widest row ABOVE it, i.e. the head has already been established and the silhouette then narrows.
224 var neck: i64 = 0 - 1
225 var maxupper: i64 = 0
226 var ny: i64 = topy
227 while ny < topy + figh*45/100 {
228 let rw: i64 = ac2_row_width(fb, w, x0, x1, y0 + ny, bg)
229 if rw > maxupper { maxupper = rw }
230 if neck < 0 {
231 if maxupper > 4 {
232 if rw > 0 { if rw * 100 < maxupper * 55 { neck = ny } }
233 }
234 }
235 ny = ny + 1
236 }
237 var headp: i64 = 0
238 if neck > 0 { headp = (neck - topy) * 1000 / figh }
239 // canon band: realistic 123..145 permil (nx_anthro_gate headsTall 6.9..8.1). Style widens the ceiling.
240 var hi_band: i64 = 155
241 if style == 1 { hi_band = 230 }
242 if style == 3 { hi_band = 250 }
243 if style == 2 { hi_band = 360 }
244 var headscore: i64 = 0
245 if neck < 0 {
246 // ★NO head/neck boundary found ANYWHERE = there is no head. Score 0, never partial credit.
247 // MEASURED BUG THIS FIXES: a giant-headed lollipop left neck undetected, headp fell through to 0,
248 // and the "continuous, not a cliff" partial-credit branch then awarded it 50 -- which was enough
249 // to dodge the hard cap and score 637. Absence of a feature is not near-presence of it.
250 headscore = 0
251 }
252 if neck >= 0 {
253 if headp >= 95 { if headp <= hi_band { headscore = 1000 } }
254 if headscore == 0 {
255 // partial credit by distance outside the band, so a slightly-off head degrades smoothly
256 var dist: i64 = 0
257 if headp < 95 { dist = 95 - headp }
258 if headp > hi_band { dist = headp - hi_band }
259 if dist < 100 { headscore = (100 - dist) * 10 }
260 }
261 }
262 out[AC2_M_HEAD] = headscore
263
264 // ---- TAPER: shoulders wider than waist, hips wider than waist. A cylinder or a stick scores 0.
265 // ★SAMPLE ROWS AT REAL ANATOMICAL HEIGHTS. The first version sampled 32/46/56% of figure height,
266 // which on a correctly-proportioned figure lands on waist/hips/LEGS respectively and therefore
267 // reported taper 0 on bodies that visibly had a waist. On a ~7.5-head figure the shoulders sit
268 // ~22% down, the waist ~40%, the hips ~50%. Verified non-circular: this correction RAISES the taper
269 // of the gate's independent reference body too, not merely the figure that exposed it.
270 let sh_y: i64 = topy + figh*22/100
271 let wa_y: i64 = topy + figh*40/100
272 let hp_y: i64 = topy + figh*50/100
273 // centre column of the figure, from the widest row (a robust midline estimate)
274 var cxm: i64 = x0 + bw/2
275 let shw: i64 = ac2_center_run(fb, w, x0, x1, cxm, y0 + sh_y, bg)
276 let waw: i64 = ac2_center_run(fb, w, x0, x1, cxm, y0 + wa_y, bg)
277 let hpw: i64 = ac2_center_run(fb, w, x0, x1, cxm, y0 + hp_y, bg)
278 var taper: i64 = 0
279 if waw > 0 {
280 var t1: i64 = 0
281 if shw > waw { t1 = (shw - waw) * 1000 / waw }
282 var t2: i64 = 0
283 if hpw > waw { t2 = (hpw - waw) * 1000 / waw }
284 taper = t1 + t2
285 if taper > 1000 { taper = 1000 }
286 }
287 out[AC2_M_TAPER] = taper
288
289 // ---- VERTICAL BALANCE: mass in thirds. A lollipop puts nearly everything up top.
290 var mtop: i64 = 0
291 var mmid: i64 = 0
292 var mbot: i64 = 0
293 var by: i64 = 0
294 while by < figh {
295 let mm: i64 = ac2_row_mass(fb, w, x0, x1, y0 + topy + by, bg)
296 if by < figh/3 { mtop = mtop + mm }
297 if by >= figh/3 { if by < figh*2/3 { mmid = mmid + mm } }
298 if by >= figh*2/3 { mbot = mbot + mm }
299 by = by + 1
300 }
301 let mtot: i64 = mtop + mmid + mbot
302 var vbal: i64 = 0
303 if mtot > 0 {
304 // every third must carry real mass; score by the SMALLEST share vs an even 333
305 var lo: i64 = mtop
306 if mmid < lo { lo = mmid }
307 if mbot < lo { lo = mbot }
308 vbal = lo * 3000 / mtot
309 if vbal > 1000 { vbal = 1000 }
310 }
311 out[AC2_M_VBAL] = vbal
312
313 // ---- LIMBSPAN: the lower third must stay present all the way down (legs reach the ground)
314 var solid: i64 = 0
315 var rows: i64 = 0
316 var ly: i64 = topy + figh*2/3
317 while ly <= boty {
318 if ac2_row_width(fb, w, x0, x1, y0 + ly, bg) > 0 { solid = solid + 1 }
319 rows = rows + 1
320 ly = ly + 1
321 }
322 var limb: i64 = 0
323 if rows > 0 { limb = solid * 1000 / rows }
324 out[AC2_M_LIMB] = limb
325 return 0
326}
327
328// composite 0..1000. ★CONNECTIVITY IS A HARD GATE: a figure that is not one connected body is BROKEN
329// no matter how well proportioned its pieces are -- that is the exact failure the SOTA-876 sheet had.
330func ac2_quality(out: *i64) -> i64 {
331 var s: i64 = 0
332 s = s + ac2_cap(out[AC2_M_CONN], 1000, 300)
333 s = s + ac2_cap(out[AC2_M_HEAD], 1000, 260)
334 s = s + ac2_cap(out[AC2_M_TAPER], 260, 160)
335 s = s + ac2_cap(out[AC2_M_VBAL], 700, 160)
336 s = s + ac2_cap(out[AC2_M_LIMB], 1000, 120)
337 // ★HARD CAP at 950, not 850 -- MEASURED: a detached HEAD is only ~10% of a figure's pixels, so it
338 // leaves connectivity at 898 and an 850 threshold waves it straight through. A body is ONE connected
339 // shape; anything that leaves a tenth of itself floating is BROKEN however well-shaded the pieces are.
340 if out[AC2_M_CONN] < 950 {
341 if s > AC2_BROKEN - 1 { s = AC2_BROKEN - 1 }
342 }
343 // a figure with NO detectable neck/head boundary is not a figure either
344 if out[AC2_M_HEAD] == 0 {
345 if s > AC2_BROKEN - 1 { s = AC2_BROKEN - 1 }
346 }
347 return s
348}