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}