code wiki / (root) / nx_charjudge_lib.nx

nx_charjudge_lib.nx source

↩ module page · 653 lines · 29466 B

1// nx_charjudge_lib.nx -- CHARACTER-IMAGE JUDGE v2 core (operator 2026-08-04: "eat the debt and build to 2// actually get to sota"). v1 (composition-bimodality x palette) reproduced the human ordering and killed the 3// patch-scramble that fooled ns_assess -- but had a NAMED gameable vector: a designed smooth+noise+palette 4// collage. v2 adds the axes that close it: 5// CONTOUR COHERENCE -- permil of edge pixels on runs >= run_min (row+col propagation). A scramble cannot 6// carry a contour past its patch size; per-pixel noise has edges but no runs. 7// FACE PRESENCE -- multi-scale center-surround dark-blob (iris/pupil class, anime AND photo) + PAIR geometry 8// (two compact blobs, level, spaced). No face = capability floor 0 (a blob with no face is not 40pc of a 9// character). Ring compactness (all 8 ring points lighter) rejects line-art edges. 10// HEADLINE = MIN(composition, palette, contour, face). Thresholds = CONF ROWS (knowledge/charjudge.conf, 11// key=value, # comments; code defaults = the 2026-08-04 battery calibration) per rules 11+17. 12// All integer, zero third-party. Plain-if only in this lib (imported-else parser landmine, debt 1784673261). 13// Buffers are mmap-per-call, bounded by call count (CLI=1, gate<16) -- not a daemon lib. 14import "nx_syscalls.nx" 15 16const CHJ_CELL: i64 = 16 17const CHJ_BUCKETS: i64 = 4096 18const CHJ_CAND_MAX: i64 = 128 19const CHJ_MAGIC_65536: i64 = 65536 20const CHJ_C_SMOOTH_T: i64 = 0 21const CHJ_C_DETAIL_T: i64 = 1 22const CHJ_C_SMOOTH_DIV: i64 = 2 23const CHJ_C_DETAIL_DIV: i64 = 3 24const CHJ_C_PAL_DIV: i64 = 4 25const CHJ_C_EDGE_T: i64 = 5 26const CHJ_C_RUN_MIN: i64 = 6 27const CHJ_C_COH_DIV: i64 = 7 28const CHJ_C_EYE_T: i64 = 8 29const CHJ_C_FACE_REQ: i64 = 9 30// ★EYE-PAIR GEOMETRY. Every one of these was an absolute PIXEL literal, which made the face axis a function 31// of RENDER SIZE rather than of the subject: on ONE mesh, 256x384 scores face_axis 1000 and 512x768 scores 32// 0. A feature window measured in raw pixels judges the resolution, and it returns 0, which reads as "the 33// subject has no face". They are config now, and the defaults below are EXACTLY the previous literals, so 34// behaviour at any resolution that already worked is unchanged by construction. 35const CHJ_C_EYE_DY: i64 = 10 // max vertical misalignment of a candidate pair 36const CHJ_C_EYE_DXMIN: i64 = 11 // min horizontal separation of a pair 37const CHJ_C_EYE_DXMAX: i64 = 12 // max horizontal separation of a pair 38const CHJ_C_PROBE_FAR: i64 = 13 // axial centre-surround probe radius 39const CHJ_C_PROBE_DIAG: i64 = 14 // diagonal centre-surround probe radius 40const CHJ_C_DEDUP_R: i64 = 15 // candidate dedup radius 41const CHJ_C_BORDER: i64 = 16 // scan border margin 42const CHJ_C_STRIDE: i64 = 17 // scan stride 43const CHJ_C_PYR_MIN: i64 = 18 // smallest pyramid level edge the scan is still meaningful on 44// ★THE WIDTH THESE PIXEL GEOMETRIES WERE CALIBRATED AT. A centre-surround probe of FIXED radius is 45// scale-variant BY CONSTRUCTION: the feature it hunts (an eye) grows with the render, so at twice the 46// width the probe ring falls INSIDE the dark region and the candidate never fires. Measured on one mesh: 47// 256x384 yields 5 candidates and face_axis 1000; 512x768 yields at most 2 across five pyramid levels and 48// face_axis 0. Lengths are therefore scaled by w/ref_w at each level; ref_w=256 makes that scaling exactly 49// 1.0 at the width the numbers came from, so every previously-working resolution is unchanged. 50// ⚠eye_t is NOT scaled -- it is a CONTRAST threshold in luma, not a length. 51const CHJ_C_REF_W: i64 = 19 52// ★CONTOUR SMOOTHING SCALE, EXPRESSED AS A DIVISOR OF run_min RATHER THAN AS ITS OWN LENGTH. The contour 53// axis defines a contour as an orientation-stable run of at least run_min pixels, so the scale at which the 54// gradient is estimated must be a fixed fraction of that SAME length. Authoring it as an independent number 55// is how two calibrated constants silently drift apart the moment either one is retuned. 56const CHJ_C_CONTOUR_BLUR_DIV: i64 = 20 57const CHJ_NCONF: i64 = 21 58// out[] slot count. The CLI allocated a raw 96 bytes, which is exactly 12 slots -- one short of the 59// diagnostics below. A byte count where a slot count belongs is how an array grows past its buffer 60// without anyone noticing, because mmap rounds to a page and the overflow reads back plausible. 61const CHJ_NOUT: i64 = 16 62 63func chj_conf_defaults(cf: *i64) -> i64 { 64 cf[CHJ_C_SMOOTH_T] = 2 65 cf[CHJ_C_DETAIL_T] = 10 66 cf[CHJ_C_SMOOTH_DIV] = 150 67 cf[CHJ_C_DETAIL_DIV] = 300 68 cf[CHJ_C_PAL_DIV] = 512 69 cf[CHJ_C_EDGE_T] = 12 70 cf[CHJ_C_RUN_MIN] = 24 71 cf[CHJ_C_COH_DIV] = 60 72 cf[CHJ_C_EYE_T] = 45 73 cf[CHJ_C_FACE_REQ] = 1 74 cf[CHJ_C_EYE_DY] = 5 75 cf[CHJ_C_EYE_DXMIN] = 8 76 cf[CHJ_C_EYE_DXMAX] = 44 77 cf[CHJ_C_PROBE_FAR] = 4 78 cf[CHJ_C_PROBE_DIAG] = 3 79 cf[CHJ_C_DEDUP_R] = 6 80 cf[CHJ_C_BORDER] = 5 81 cf[CHJ_C_STRIDE] = 2 82 cf[CHJ_C_PYR_MIN] = 24 83 cf[CHJ_C_REF_W] = 256 84 // radius = run_min/8 -> a 7px kernel against a 24px run: wide enough to average out surface texture 85 // whose period is a few pixels, narrow enough that a contour of the length this axis calls coherent 86 // survives it. Validated below by re-running the two textured/untextured A/B pairs and the adversarial 87 // scramble and collage fixtures -- not chosen by taste. 88 cf[CHJ_C_CONTOUR_BLUR_DIV] = 8 89 return 0 90} 91func chj_keyeq(buf: *u8, at: i64, end: i64, key: *u8) -> i64 { 92 var k: i64 = 0 93 var i: i64 = at 94 while key[k] != (0 as u8) { 95 if i >= end { return 0 } 96 if buf[i] != key[k] { return 0 } 97 i = i + 1 98 k = k + 1 99 } 100 if i >= end { return 0 } 101 if buf[i] != (61 as u8) { return 0 } 102 return 1 103} 104func chj_slot_for(buf: *u8, at: i64, end: i64) -> i64 { 105 if chj_keyeq(buf, at, end, "eye_dy" as *u8) == 1 { return CHJ_C_EYE_DY } 106 if chj_keyeq(buf, at, end, "eye_dxmin" as *u8) == 1 { return CHJ_C_EYE_DXMIN } 107 if chj_keyeq(buf, at, end, "eye_dxmax" as *u8) == 1 { return CHJ_C_EYE_DXMAX } 108 if chj_keyeq(buf, at, end, "probe_far" as *u8) == 1 { return CHJ_C_PROBE_FAR } 109 if chj_keyeq(buf, at, end, "probe_diag" as *u8) == 1 { return CHJ_C_PROBE_DIAG } 110 if chj_keyeq(buf, at, end, "dedup_r" as *u8) == 1 { return CHJ_C_DEDUP_R } 111 if chj_keyeq(buf, at, end, "border" as *u8) == 1 { return CHJ_C_BORDER } 112 if chj_keyeq(buf, at, end, "stride" as *u8) == 1 { return CHJ_C_STRIDE } 113 if chj_keyeq(buf, at, end, "pyr_min" as *u8) == 1 { return CHJ_C_PYR_MIN } 114 if chj_keyeq(buf, at, end, "ref_w" as *u8) == 1 { return CHJ_C_REF_W } 115 if chj_keyeq(buf, at, end, "contour_blur_div" as *u8) == 1 { return CHJ_C_CONTOUR_BLUR_DIV } 116 if chj_keyeq(buf, at, end, "smooth_t" as *u8) == 1 { return CHJ_C_SMOOTH_T } 117 if chj_keyeq(buf, at, end, "detail_t" as *u8) == 1 { return CHJ_C_DETAIL_T } 118 if chj_keyeq(buf, at, end, "smooth_div" as *u8) == 1 { return CHJ_C_SMOOTH_DIV } 119 if chj_keyeq(buf, at, end, "detail_div" as *u8) == 1 { return CHJ_C_DETAIL_DIV } 120 if chj_keyeq(buf, at, end, "pal_div" as *u8) == 1 { return CHJ_C_PAL_DIV } 121 if chj_keyeq(buf, at, end, "edge_t" as *u8) == 1 { return CHJ_C_EDGE_T } 122 if chj_keyeq(buf, at, end, "run_min" as *u8) == 1 { return CHJ_C_RUN_MIN } 123 if chj_keyeq(buf, at, end, "coh_div" as *u8) == 1 { return CHJ_C_COH_DIV } 124 if chj_keyeq(buf, at, end, "eye_t" as *u8) == 1 { return CHJ_C_EYE_T } 125 if chj_keyeq(buf, at, end, "face_required" as *u8) == 1 { return CHJ_C_FACE_REQ } 126 return 0-1 127} 128func chj_conf_load(path: *u8, cf: *i64) -> i64 { 129 chj_conf_defaults(cf) 130 if (path as i64) == 0 { return 0 } 131 let lenp: *i64 = sys_mmap(16) as *i64 132 let buf: *u8 = sys_read_file(path, lenp) 133 if (buf as i64) == 0 { return 0 } 134 let n: i64 = lenp[0] 135 var i: i64 = 0 136 while i < n { 137 var e: i64 = i 138 var go: i64 = 1 139 while go == 1 { 140 if e >= n { go = 0 } 141 if go == 1 { if buf[e] == (10 as u8) { go = 0 } } 142 if go == 1 { e = e + 1 } 143 } 144 if e > i { if buf[i] != (35 as u8) { 145 let slot: i64 = chj_slot_for(buf, i, e) 146 if slot >= 0 { 147 var p: i64 = i 148 var col: i64 = 0-1 149 var g2: i64 = 1 150 while g2 == 1 { 151 if p >= e { g2 = 0 } 152 if g2 == 1 { if buf[p] == (61 as u8) { col = p; g2 = 0 } } 153 if g2 == 1 { p = p + 1 } 154 } 155 if col >= 0 { 156 var v: i64 = 0 157 var neg: i64 = 0 158 var q: i64 = col + 1 159 if q < e { if buf[q] == (45 as u8) { neg = 1; q = q + 1 } } 160 while q < e { 161 let c: i64 = buf[q] as i64 162 if c >= 48 { if c <= 57 { v = v*10 + (c-48) } } 163 q = q + 1 164 } 165 if neg == 1 { v = 0-v } 166 cf[slot] = v 167 } 168 } 169 } } 170 i = e + 1 171 } 172 return 0 173} 174 175func chj_luma(fb: *i64, w: i64, h: i64, luma: *i64) -> i64 { 176 var q: i64 = 0 177 while q < w*h { 178 let px: i64 = fb[q] 179 let r: i64 = px&255 180 let g: i64 = (px>>8)&255 181 let b: i64 = (px>>16)&255 182 luma[q] = (r*299 + g*587 + b*114)/1000 183 q = q + 1 184 } 185 return 0 186} 187 188// composition bimodality: 16px cells classed smooth/detail by mean |grad| per pixel. out[0]=smooth_permil out[1]=detail_permil 189func chj_cells(luma: *i64, w: i64, h: i64, cf: *i64, outp: *i64) -> i64 { 190 let cw: i64 = w/CHJ_CELL 191 let chh: i64 = h/CHJ_CELL 192 var smooth: i64 = 0 193 var detail: i64 = 0 194 var cy: i64 = 0 195 while cy < chh { 196 var cx: i64 = 0 197 while cx < cw { 198 var e: i64 = 0 199 var yy: i64 = 0 200 while yy < CHJ_CELL { 201 var xx: i64 = 0 202 while xx < CHJ_CELL { 203 let ix: i64 = cx*CHJ_CELL + xx 204 let iy: i64 = cy*CHJ_CELL + yy 205 if ix < w-1 { if iy < h-1 { 206 let l0: i64 = luma[iy*w + ix] 207 var gx: i64 = luma[iy*w + ix + 1] - l0 208 var gy: i64 = luma[(iy+1)*w + ix] - l0 209 if gx < 0 { gx = 0-gx } 210 if gy < 0 { gy = 0-gy } 211 e = e + gx + gy 212 } } 213 xx = xx + 1 214 } 215 yy = yy + 1 216 } 217 let me: i64 = e/(CHJ_CELL*CHJ_CELL) 218 if me < cf[CHJ_C_SMOOTH_T] { smooth = smooth + 1 } 219 if me > cf[CHJ_C_DETAIL_T] { detail = detail + 1 } 220 cx = cx + 1 221 } 222 cy = cy + 1 223 } 224 let ncell: i64 = cw*chh 225 if ncell < 1 { outp[0] = 0; outp[1] = 0; return 0 } 226 outp[0] = smooth*1000/ncell 227 outp[1] = detail*1000/ncell 228 return 0 229} 230 231func chj_palette(fb: *i64, w: i64, h: i64) -> i64 { 232 let seen: *u8 = sys_mmap(CHJ_BUCKETS) 233 var q: i64 = 0 234 while q < w*h { 235 let px: i64 = fb[q] 236 let r: i64 = px&255 237 let g: i64 = (px>>8)&255 238 let b: i64 = (px>>16)&255 239 seen[(r/16)*256 + (g/16)*16 + b/16] = 1 as u8 240 q = q + 1 241 } 242 var pal: i64 = 0 243 q = 0 244 while q < CHJ_BUCKETS { if seen[q] == (1 as u8) { pal = pal + 1 } q = q + 1 } 245 return pal 246} 247 248// contour coherence: permil of edge pixels on ORIENTATION-STABLE runs >= run_min. Plain run-length was 249// designed-out at gate time: dense noise is fully CONNECTED (runs grow trivially) and a patch-scramble's 250// aligned seams are long lines -- so a run only extends between neighbors in the SAME quantized gradient 251// orientation bin (4 bins). Noise dies at ~1/4 per step; real contours hold a bin over long arcs. 252// outp[0]=coherent_permil outp[1]=edge_total. returns the banded axis. 253func chj_contour(luma: *i64, w: i64, h: i64, cf: *i64, outp: *i64) -> i64 { 254 let et: i64 = cf[CHJ_C_EDGE_T] 255 let rm: i64 = cf[CHJ_C_RUN_MIN] 256 // ★CONTOURS ARE LOW-FREQUENCY STRUCTURE AND WERE BEING MEASURED ON THE HIGH-FREQUENCY CHANNEL. 257 // The orientation bin came from 1-pixel finite differences on RAW luma, so surface texture perturbs the 258 // bin ALONG a silhouette and the run breaks before it reaches run_min. Measured on two independent A/B 259 // pairs (the same body, untextured vs PBR-textured): coherent runs collapsed 304->13 and 644->52, so the 260 // axis reported an 8.9x and 6.8x REGRESSION for landing the skin bake while composition, palette and 261 // detail all improved. An axis that punishes the exact improvement it exists to reward is worse than no 262 // axis at all, because its verdict is authoritative and inverted. 263 // Smoothing first is also what makes the adversarial kills STRONGER, not weaker: per-pixel noise falls 264 // below edge_t entirely instead of being counted and then out-competed, while a patch seam is a real 265 // step edge that survives any small kernel. 266 var br: i64 = rm/cf[CHJ_C_CONTOUR_BLUR_DIV] 267 if br < 1 { br = 1 } 268 let bh: *i64 = sys_mmap(w*h*8) as *i64 269 let lb: *i64 = sys_mmap(w*h*8) as *i64 270 var by: i64 = 0 271 while by < h { 272 var bx: i64 = 0 273 while bx < w { 274 var acc: i64 = 0 275 var cnt: i64 = 0 276 var k: i64 = 0-br 277 while k <= br { 278 var sx: i64 = bx+k 279 if sx < 0 { sx = 0 } 280 if sx > w-1 { sx = w-1 } 281 acc = acc + luma[by*w+sx] 282 cnt = cnt + 1 283 k = k + 1 284 } 285 bh[by*w+bx] = acc/cnt 286 bx = bx + 1 287 } 288 by = by + 1 289 } 290 by = 0 291 while by < h { 292 var bx2: i64 = 0 293 while bx2 < w { 294 var acc2: i64 = 0 295 var cnt2: i64 = 0 296 var k2: i64 = 0-br 297 while k2 <= br { 298 var sy: i64 = by+k2 299 if sy < 0 { sy = 0 } 300 if sy > h-1 { sy = h-1 } 301 acc2 = acc2 + bh[sy*w+bx2] 302 cnt2 = cnt2 + 1 303 k2 = k2 + 1 304 } 305 lb[by*w+bx2] = acc2/cnt2 306 bx2 = bx2 + 1 307 } 308 by = by + 1 309 } 310 // the horizontal pass is scratch -- free it before the edge maps allocate. Resource excellence is a 311 // shipping criterion: this organ is forked once per image across a 169-render corpus, and an unfreed 312 // w*h*8 buffer is 4.4 MB per 1200x460 frame carried for the rest of the run for no reason. 313 sys_munmap(bh as *u8, w*h*8) 314 let emap: *u8 = sys_mmap(w*h) 315 let bmap: *u8 = sys_mmap(w*h) 316 var etotal: i64 = 0 317 var y: i64 = 0 318 while y < h-1 { 319 var x: i64 = 0 320 while x < w-1 { 321 let l0: i64 = lb[y*w+x] 322 let sgx: i64 = lb[y*w+x+1] - l0 323 let sgy: i64 = lb[(y+1)*w+x] - l0 324 var agx: i64 = sgx 325 if agx < 0 { agx = 0-agx } 326 var agy: i64 = sgy 327 if agy < 0 { agy = 0-agy } 328 if agx+agy > et { 329 emap[y*w+x] = 1 as u8 330 etotal = etotal + 1 331 // 8 orientation bins (sign gx, sign gy, |gx|>|gy|): 4 bins percolate through dense 332 // noise (58 pct continuation over 3 neighbors, supercritical -- gate T4 caught it); 333 // 8 bins = ~30 pct, subcritical, noise runs die exponentially, straight contours hold. 334 var bin: i64 = 0 335 if sgx < 0 { bin = bin + 4 } 336 if sgy < 0 { bin = bin + 2 } 337 if agx > agy { bin = bin + 1 } 338 bmap[y*w+x] = bin as u8 339 } 340 x = x + 1 341 } 342 y = y + 1 343 } 344 var coh: i64 = 0 345 let prev: *i64 = sys_mmap(w*8) as *i64 346 let curr: *i64 = sys_mmap(w*8) as *i64 347 let prevb: *i64 = sys_mmap(w*8) as *i64 348 let currb: *i64 = sys_mmap(w*8) as *i64 349 var i: i64 = 0 350 while i < w { prev[i] = 0; prevb[i] = 0-1; i = i + 1 } 351 y = 0 352 while y < h { 353 var x2: i64 = 0 354 while x2 < w { 355 var r: i64 = 0 356 var b: i64 = 0-1 357 if emap[y*w+x2] == (1 as u8) { 358 b = bmap[y*w+x2] as i64 359 var m: i64 = 0 360 if prevb[x2] == b { m = prev[x2] } 361 if x2 > 0 { if prevb[x2-1] == b { if prev[x2-1] > m { m = prev[x2-1] } } } 362 if x2 < w-1 { if prevb[x2+1] == b { if prev[x2+1] > m { m = prev[x2+1] } } } 363 r = m + 1 364 if r >= rm { coh = coh + 1 } 365 } 366 curr[x2] = r 367 currb[x2] = b 368 x2 = x2 + 1 369 } 370 var x3: i64 = 0 371 while x3 < w { prev[x3] = curr[x3]; prevb[x3] = currb[x3]; x3 = x3 + 1 } 372 y = y + 1 373 } 374 let prevh: *i64 = sys_mmap(h*8) as *i64 375 let currh: *i64 = sys_mmap(h*8) as *i64 376 let prevhb: *i64 = sys_mmap(h*8) as *i64 377 let currhb: *i64 = sys_mmap(h*8) as *i64 378 i = 0 379 while i < h { prevh[i] = 0; prevhb[i] = 0-1; i = i + 1 } 380 var x4: i64 = 0 381 while x4 < w { 382 var y2: i64 = 0 383 while y2 < h { 384 var r2: i64 = 0 385 var b2: i64 = 0-1 386 if emap[y2*w+x4] == (1 as u8) { 387 b2 = bmap[y2*w+x4] as i64 388 var m2: i64 = 0 389 if prevhb[y2] == b2 { m2 = prevh[y2] } 390 if y2 > 0 { if prevhb[y2-1] == b2 { if prevh[y2-1] > m2 { m2 = prevh[y2-1] } } } 391 if y2 < h-1 { if prevhb[y2+1] == b2 { if prevh[y2+1] > m2 { m2 = prevh[y2+1] } } } 392 r2 = m2 + 1 393 if r2 >= rm { coh = coh + 1 } 394 } 395 currh[y2] = r2 396 currhb[y2] = b2 397 y2 = y2 + 1 398 } 399 var y3: i64 = 0 400 while y3 < h { prevh[y3] = currh[y3]; prevhb[y3] = currhb[y3]; y3 = y3 + 1 } 401 x4 = x4 + 1 402 } 403 var perm: i64 = 0 404 if etotal > 0 { perm = coh*1000/etotal } 405 if etotal < w*h/400 { perm = 0 } 406 outp[0] = perm 407 outp[1] = etotal 408 // FREE THE SCRATCH. A census over a 169-render corpus multiplies every unfreed buffer by the corpus 409 // size; at ~1MP per frame this function alone held ~4.7 GB of address space for no reason, on a box 410 // already sitting at 720 permil swap. Resource excellence is a shipping criterion, not tidiness. 411 sys_munmap(lb as *u8, w*h*8) 412 sys_munmap(emap, w*h) 413 sys_munmap(bmap, w*h) 414 sys_munmap(prev as *u8, w*8) 415 sys_munmap(curr as *u8, w*8) 416 sys_munmap(prevb as *u8, w*8) 417 sys_munmap(currb as *u8, w*8) 418 sys_munmap(prevh as *u8, h*8) 419 sys_munmap(currh as *u8, h*8) 420 sys_munmap(prevhb as *u8, h*8) 421 sys_munmap(currhb as *u8, h*8) 422 var axis: i64 = perm*1000/cf[CHJ_C_COH_DIV] 423 if axis > 1000 { axis = 1000 } 424 return axis 425} 426 427// one face scan at the current scale: center-surround compact dark blobs -> level spaced pair 428func chj_face_scan(l: *i64, w: i64, h: i64, cf: *i64, ncout: *i64) -> i64 { 429 let eyet: i64 = cf[CHJ_C_EYE_T] 430 // ★SCALE-COVARIANT LENGTHS. Every geometric constant below is expressed at ref_w and scaled to the 431 // width actually being scanned, so the detector hunts the same FEATURE rather than the same pixel 432 // count. At w == ref_w the factor is exactly 1000/1000, which is why this changes nothing at the 433 // resolutions that already worked. Each result is floored at 1: a probe radius of zero reads the 434 // centre pixel against itself and would make every pixel a candidate. 435 // THE PYRAMID IS THE SCALE SEARCH; THE PROBE IS FIXED IN PIXELS. Scaling by the CURRENT level width 436 // made probe and pyramid cancel exactly, so every level re-ran one identical scan and the search was 437 // INERT. Scaling by the ORIGINAL width probes one large size at every level: measured 2026-08-15 that 438 // recovered a 640px face close-up (0 -> 1000) and LOST an 800px body render (1000 -> 0). Trading one 439 // false negative for another is not a fix. Fixed pixels + pyramid spans the whole size range. 440 let sc: i64 = 1000 441 var pf: i64 = cf[CHJ_C_PROBE_FAR]*sc/1000 442 if pf < 1 { pf = 1 } 443 var pd: i64 = cf[CHJ_C_PROBE_DIAG]*sc/1000 444 if pd < 1 { pd = 1 } 445 // the border must clear the widest probe or the surround reads outside the buffer. Derived from the 446 // probe radii rather than trusted from config: a border narrower than the probe is not a tuning 447 // choice, it is an out-of-bounds read. 448 var bd: i64 = cf[CHJ_C_BORDER]*sc/1000 449 if bd < pf { bd = pf } 450 if bd < pd { bd = pd } 451 var st: i64 = cf[CHJ_C_STRIDE]*sc/1000 452 if st < 1 { st = 1 } 453 var dr: i64 = cf[CHJ_C_DEDUP_R]*sc/1000 454 if dr < 1 { dr = 1 } 455 let cx: *i64 = sys_mmap(CHJ_CAND_MAX*8) as *i64 456 let cy: *i64 = sys_mmap(CHJ_CAND_MAX*8) as *i64 457 var nc: i64 = 0 458 var y: i64 = bd 459 while y < h-bd { 460 var x: i64 = bd 461 while x < w-bd { 462 let c: i64 = l[y*w+x] 463 var okc: i64 = 1 464 let p1: i64 = l[y*w + x+pf] 465 let p2: i64 = l[y*w + x-pf] 466 let p3: i64 = l[(y+pf)*w + x] 467 let p4: i64 = l[(y-pf)*w + x] 468 let p5: i64 = l[(y+pd)*w + x+pd] 469 let p6: i64 = l[(y+pd)*w + x-pd] 470 let p7: i64 = l[(y-pd)*w + x+pd] 471 let p8: i64 = l[(y-pd)*w + x-pd] 472 if p1 <= c + eyet/2 { okc = 0 } 473 if p2 <= c + eyet/2 { okc = 0 } 474 if p3 <= c + eyet/2 { okc = 0 } 475 if p4 <= c + eyet/2 { okc = 0 } 476 if p5 <= c + eyet/2 { okc = 0 } 477 if p6 <= c + eyet/2 { okc = 0 } 478 if p7 <= c + eyet/2 { okc = 0 } 479 if p8 <= c + eyet/2 { okc = 0 } 480 if okc == 1 { if (p1+p2+p3+p4+p5+p6+p7+p8)/8 - c > eyet { 481 var near: i64 = 0 482 var k: i64 = 0 483 while k < nc { 484 var ddx: i64 = cx[k]-x 485 if ddx < 0 { ddx = 0-ddx } 486 var ddy: i64 = cy[k]-y 487 if ddy < 0 { ddy = 0-ddy } 488 if ddx < dr { if ddy < dr { near = 1 } } 489 k = k + 1 490 } 491 if near == 0 { if nc < CHJ_CAND_MAX { cx[nc] = x; cy[nc] = y; nc = nc + 1 } } 492 } } 493 x = x + st 494 } 495 y = y + st 496 } 497 // report how many blob candidates this level produced, so a zero face verdict can be attributed: 498 // no candidates means the centre-surround probe never fired, a nonzero count with no pair means the 499 // separation window rejected them. Those are different defects with different fixes. 500 ncout[0] = nc 501 var i: i64 = 0 502 while i < nc { 503 var j: i64 = i+1 504 while j < nc { 505 var dyv: i64 = cy[i]-cy[j] 506 if dyv < 0 { dyv = 0-dyv } 507 var dxv: i64 = cx[i]-cx[j] 508 if dxv < 0 { dxv = 0-dxv } 509 // the separation window scales with the render for the same reason the probe does: an 510 // interpupillary distance is a length on the subject, not a pixel count. 511 var wdy: i64 = cf[CHJ_C_EYE_DY]*sc/1000 512 if wdy < 1 { wdy = 1 } 513 var wlo: i64 = cf[CHJ_C_EYE_DXMIN]*sc/1000 514 if wlo < 1 { wlo = 1 } 515 var whi: i64 = cf[CHJ_C_EYE_DXMAX]*sc/1000 516 if whi < wlo { whi = wlo } 517 if dyv <= wdy { if dxv >= wlo { if dxv <= whi { 518 // ★TWO DARK BLOBS ARE NOT A FACE. "compact, dark, level, correctly spaced" is satisfied 519 // trivially by a voxel landscape: measured, snap_craft.png -- a craft-world screenshot with 520 // no character face in it at all -- passed this gate and scored face 1000, while an actual 521 // face close-up scored 0. Between a real pair of eyes there is always lit skin, so the 522 // midpoint must be LIGHTER than both blobs by the same contrast the blob test already uses. 523 // eye_t is reused deliberately: one contrast threshold for one physical claim, never a 524 // second number to drift away from it. 525 // ONE BRIGHT PIXEL IS NOT A FACE EITHER. The single-midpoint form STILL passed a voxel 526 // landscape -- snap_craft.png measured face 1000 with no character in frame -- because 527 // between two dark blocks there is very often one bright pixel of sky or ground. A real 528 // pair of eyes is separated by a CONTINUOUS lit bridge of skin, so sample across it at the 529 // quarter, half and three-quarter points and require EVERY one lighter than BOTH blobs. 530 // Three samples cost nothing and are far harder for scenery to satisfy by accident. 531 let li: i64 = l[cy[i]*w+cx[i]] 532 let lj: i64 = l[cy[j]*w+cx[j]] 533 var bridge: i64 = 1 534 var bq: i64 = 1 535 while bq <= 3 { 536 let bxp: i64 = cx[i] + (cx[j]-cx[i])*bq/4 537 let byp: i64 = cy[i] + (cy[j]-cy[i])*bq/4 538 let bv: i64 = l[byp*w + bxp] 539 if bv - li <= eyet { bridge = 0 } 540 if bv - lj <= eyet { bridge = 0 } 541 bq = bq + 1 542 } 543 if bridge == 1 { return 1 } 544 } } } 545 j = j + 1 546 } 547 i = i + 1 548 } 549 return 0 550} 551 552func chj_face(luma: *i64, w: i64, h: i64, cf: *i64, diag: *i64) -> i64 { 553 var cw: i64 = w 554 var chh: i64 = h 555 var cur: *i64 = sys_mmap(w*h*8) as *i64 556 var i: i64 = 0 557 while i < w*h { cur[i] = luma[i]; i = i + 1 } 558 // ★THE PYRAMID RUNS TO ITS STRUCTURAL FLOOR, NOT TO AN ARBITRARY COUNT. This loop was capped at three 559 // levels, so the detector only ever covered eye separations of dxmin..4*dxmax at the original scale. 560 // That cap, not the acceptance window, is why the SAME mesh scored face_axis 1000 at 256x384 and 0 at 561 // 512x768: the pair was resolvable, just never at a level the loop reached. The real stop is pyr_min -- 562 // the size below which a scan means nothing -- and it is now the only stop. 563 let ncp: *i64 = sys_mmap(16) as *i64 564 var done: i64 = 0 565 var levels: i64 = 0 566 var maxnc: i64 = 0 567 while done == 0 { 568 levels = levels + 1 569 ncp[0] = 0 570 let hit: i64 = chj_face_scan(cur, cw, chh, cf, ncp) 571 if ncp[0] > maxnc { maxnc = ncp[0] } 572 diag[0] = levels 573 diag[1] = maxnc 574 diag[2] = cw 575 if hit == 1 { return 1000 } 576 let nw: i64 = cw/2 577 let nh: i64 = chh/2 578 if nw < cf[CHJ_C_PYR_MIN] { done = 1 } 579 if nh < cf[CHJ_C_PYR_MIN] { done = 1 } 580 if done == 0 { 581 let nxt: *i64 = sys_mmap(nw*nh*8) as *i64 582 var y: i64 = 0 583 while y < nh { 584 var x: i64 = 0 585 while x < nw { 586 nxt[y*nw+x] = (cur[(2*y)*cw + 2*x] + cur[(2*y)*cw + 2*x+1] + cur[(2*y+1)*cw + 2*x] + cur[(2*y+1)*cw + 2*x+1])/4 587 x = x + 1 588 } 589 y = y + 1 590 } 591 // the level just scanned is dead once the next is built -- free it before advancing, or a full 592 // pyramid holds every level it ever visited. 593 sys_munmap(cur as *u8, cw*chh*8) 594 cur = nxt 595 cw = nw 596 chh = nh 597 } 598 } 599 return 0 600} 601 602// the judge. out: 0 composition 1 palette_axis 2 contour_axis 3 face_axis 4 HEADLINE 603// 5 smooth_permil 6 detail_permil 7 pal_buckets 8 coherent_permil 9 edge_total 604func chj_judge(fb: *i64, w: i64, h: i64, confpath: *u8, out: *i64) -> i64 { 605 let cf: *i64 = sys_mmap(CHJ_NCONF*8) as *i64 606 chj_conf_load(confpath, cf) 607 let luma: *i64 = sys_mmap(w*h*8) as *i64 608 chj_luma(fb, w, h, luma) 609 let cellout: *i64 = sys_mmap(16) as *i64 610 chj_cells(luma, w, h, cf, cellout) 611 let sp: i64 = cellout[0] 612 let dp: i64 = cellout[1] 613 var sb: i64 = sp*1000/cf[CHJ_C_SMOOTH_DIV] 614 if sb > 1000 { sb = 1000 } 615 var db: i64 = dp*1000/cf[CHJ_C_DETAIL_DIV] 616 if db > 1000 { db = 1000 } 617 var comp: i64 = sb 618 if db < comp { comp = db } 619 let pal: i64 = chj_palette(fb, w, h) 620 var palax: i64 = pal*1000/cf[CHJ_C_PAL_DIV] 621 if palax > 1000 { palax = 1000 } 622 let conout: *i64 = sys_mmap(16) as *i64 623 let conax: i64 = chj_contour(luma, w, h, cf, conout) 624 // ALWAYS COMPUTE; SEPARATELY DECIDE WHETHER IT VOTES. Skipping the computation when the axis was 625 // disabled left out[3] at its initialiser of 1000 -- A CONSTANT WEARING THE SHAPE OF A MEASUREMENT, 626 // which reads as "face detected, perfectly" on every image including a landscape with nothing in it. 627 // Demoting an axis out of the verdict must not also stop measuring it, or the diagnostic that justified 628 // the demotion disappears at exactly the moment it is needed. The axis is now always measured and always 629 // reported with its attribution; face_required decides ONLY whether it may lower the headline. 630 let fdiag: *i64 = sys_mmap(32) as *i64 631 fdiag[0] = 0 632 fdiag[1] = 0 633 fdiag[2] = 0 634 let faceax: i64 = chj_face(luma, w, h, cf, fdiag) 635 out[10] = fdiag[0] 636 out[11] = fdiag[1] 637 out[12] = fdiag[2] 638 var head: i64 = comp 639 if palax < head { head = palax } 640 if conax < head { head = conax } 641 if cf[CHJ_C_FACE_REQ] == 1 { if faceax < head { head = faceax } } 642 out[0] = comp 643 out[1] = palax 644 out[2] = conax 645 out[3] = faceax 646 out[4] = head 647 out[5] = sp 648 out[6] = dp 649 out[7] = pal 650 out[8] = conout[0] 651 out[9] = conout[1] 652 return head 653}