code wiki / _hdl_build / nx_editjudge_lib.nx

nx_editjudge_lib.nx source

↩ module page · 197 lines · 9832 B

1// nx_editjudge_lib.nx -- THE EDIT-COMPLIANCE JUDGE: did the requested change actually happen? 2// 3// WHY IT EXISTS (a MEASURED result, not a guess). The convergence loop optimised RETENTION and 4// converged in 4 renders to 845 -- and the requested emerald-green dress was still YELLOW. Retention 5// measures PRESERVATION, so maximising it alone walks toward changing nothing. That is the metric 6// quietly becoming the target. The cure is a SECOND, ORTHOGONAL judge asking the opposite question, 7// and reporting the two jointly (MIN) so a pipeline cannot win by only preserving OR only changing. 8// 9// WHAT IT MEASURES, for a colour edit from_hue -> to_hue: 10// 1. In the REFERENCE, find the garment: pixels whose hue is near from_hue AND that are saturated 11// enough to be a real colour (not near-gray/white/black). That mask IS the region to be edited -- 12// derived from the image, not hand-drawn, so it cannot be gamed by choosing a flattering box. 13// 2. At those SAME pixel locations in the generated image, count how many have shifted to near 14// to_hue. compliance_permil = shifted / masked. 15// 3. Also report how many DRIFTED (moved off from_hue but not onto to_hue) and how many STAYED, so 16// "nothing changed" (stayed high) and "changed to the wrong thing" (drift high) are distinguishable 17// failures, not one blended number. 18// 19// The signal is orthogonal to retention BY CONSTRUCTION: retention rises when pixels stay put; 20// compliance rises when the masked pixels move to a specific new hue. You cannot maximise both by 21// doing nothing, and you cannot maximise both by re-rolling the whole frame. 22// 23// Composes nx_png_decoder + nx_image; the hue/sat math is integer HSV (no float, never-brick #26). 24// license_tier: ORIGINAL 25import "nx_png_decoder.nx" 26import "nx_image.nx" 27 28const EJ_SAT_MIN: i64 = 60 // permil-of-255 saturation floor: below this a pixel is too gray to 29 // carry a trustworthy hue, so it is excluded from BOTH mask and match 30const EJ_HUE_TOL: i64 = 30 // degrees: a hue within this of the anchor counts as "that colour" 31 32func ej_min3(a: i64, b: i64, c: i64) -> i64 { var m: i64 = a; if b < m { m = b } if c < m { m = c } return m } 33func ej_max3(a: i64, b: i64, c: i64) -> i64 { var m: i64 = a; if b > m { m = b } if c > m { m = c } return m } 34 35// Integer HSV hue in degrees [0,359], or -1 for an achromatic pixel. Standard piecewise formula done 36// in integers; the /delta divisions lose <1 degree, which is far inside EJ_HUE_TOL. 37func ej_hue(r: i64, g: i64, b: i64) -> i64 { 38 let mx: i64 = ej_max3(r, g, b) 39 let mn: i64 = ej_min3(r, g, b) 40 let d: i64 = mx - mn 41 if d == 0 { return 0 - 1 } 42 var h: i64 = 0 43 if mx == r { h = (60 * (g - b)) / d } 44 else { if mx == g { h = (60 * (b - r)) / d + 120 } else { h = (60 * (r - g)) / d + 240 } } 45 if h < 0 { h = h + 360 } 46 if h >= 360 { h = h - 360 } 47 return h 48} 49 50// Saturation in permil-of-255: delta/max scaled. 0 for black. 51func ej_sat(r: i64, g: i64, b: i64) -> i64 { 52 let mx: i64 = ej_max3(r, g, b) 53 let mn: i64 = ej_min3(r, g, b) 54 if mx == 0 { return 0 } 55 return ((mx - mn) * 255) / mx 56} 57 58// circular distance between two hues in degrees (0..180) 59func ej_hue_dist(a: i64, b: i64) -> i64 { 60 if a < 0 { return 999 } 61 if b < 0 { return 999 } 62 var d: i64 = a - b 63 if d < 0 { d = 0 - d } 64 if d > 180 { d = 360 - d } 65 return d 66} 67 68// Decode a PNG to interleaved RGB (3 bytes/pixel); returns null + writes 0 to out_w on any failure. 69// FAIL-CLOSED: a failed decode must never become a zeroed buffer that reads as a valid black image. 70func ej_load_rgb(path: *u8, out_w: *i64, out_h: *i64) -> *u8 { 71 out_w[0] = 0 72 out_h[0] = 0 73 let lb: *i64 = sys_mmap(8) 74 let buf: *u8 = sys_read_file(path, lb) 75 if buf == (0 as *u8) { return 0 as *u8 } 76 let n: i64 = lb[0] 77 if n <= 0 { return 0 as *u8 } 78 let res: *NxPngResult = nx_png_decode(buf, n) 79 if res == (0 as *NxPngResult) { return 0 as *u8 } 80 if (res.error_code as i64) != 0 { return 0 as *u8 } 81 let hdr: *NxPngHeader = res.header 82 let w: i64 = hdr.width as i64 83 let h: i64 = hdr.height as i64 84 let nch: i64 = res.n_channels as i64 85 let px: *u8 = res.pixels 86 if w <= 0 { return 0 as *u8 } 87 if h <= 0 { return 0 as *u8 } 88 let out: *u8 = sys_mmap(w * h * 3) 89 var i: i64 = 0 90 while i < w * h { 91 let o: i64 = i * nch 92 if nch >= 3 { out[i*3] = px[o]; out[i*3+1] = px[o+1]; out[i*3+2] = px[o+2] } 93 else { out[i*3] = px[o]; out[i*3+1] = px[o]; out[i*3+2] = px[o] } 94 i = i + 1 95 } 96 out_w[0] = w 97 out_h[0] = h 98 return out 99} 100 101// Is pixel index i (in a w-by-h image) inside the normalised box [x0,y0,x1,y1] (permil of w/h)? 102// A full-frame box is x0=0 y0=0 x1=1000 y1=1000. The box is why "recolour the dress" does not also 103// recolour the face: skin shares the garment's WARM HUE, so a hue mask alone is not enough -- it needs 104// a spatial constraint. MEASURED: a hue-only recolour of a yellow dress turned the model's face green. 105func ej_in_box(i: i64, w: i64, h: i64, x0: i64, y0: i64, x1: i64, y1: i64) -> i64 { 106 let px: i64 = i % w 107 let py: i64 = i / w 108 let bx0: i64 = (x0 * w) / 1000 109 let bx1: i64 = (x1 * w) / 1000 110 let by0: i64 = (y0 * h) / 1000 111 let by1: i64 = (y1 * h) / 1000 112 if px < bx0 { return 0 } 113 if px >= bx1 { return 0 } 114 if py < by0 { return 0 } 115 if py >= by1 { return 0 } 116 return 1 117} 118 119// Boxed measurement. Only pixels inside the normalised box are eligible for the mask, so the judge and 120// the editor can be constrained to the SAME garment region -- a judge that shares the editor's greedy 121// hue mask would bless a face-recolour, which is the exact trap the box closes. 122// ⚠sat_min is part of the operator's SPEC ("the garment is saturated fabric"), NOT the editor's mask -- 123// DEC026 forbids sharing the editor's mask, not agreeing on what the target object IS. MEASURED why this 124// matters: judging a correct edit with sat_min=60 counted the deliberately-spared SKIN as "should have 125// changed but stayed" and reported compliance 413 for an edit that footprint(260), retention(921) and the 126// eye all called excellent. A judge whose object definition is wrong reports a wrong number confidently. 127func ej_compliance_spec(ref: *u8, gen: *u8, w: i64, h: i64, from_hue: i64, to_hue: i64, 128 x0: i64, y0: i64, x1: i64, y1: i64, sat_min: i64, 129 out_masked: *i64, out_shift: *i64, out_drift: *i64, out_stay: *i64) -> i64 { 130 var masked: i64 = 0 131 var shifted: i64 = 0 132 var drifted: i64 = 0 133 var stayed: i64 = 0 134 let npix: i64 = w * h 135 var i: i64 = 0 136 while i < npix { 137 if ej_in_box(i, w, h, x0, y0, x1, y1) == 1 { 138 let rr: i64 = ref[i*3] as i64 139 let rg: i64 = ref[i*3+1] as i64 140 let rb: i64 = ref[i*3+2] as i64 141 if ej_sat(rr, rg, rb) >= sat_min { 142 let rhue: i64 = ej_hue(rr, rg, rb) 143 if ej_hue_dist(rhue, from_hue) <= EJ_HUE_TOL { 144 masked = masked + 1 145 let gr: i64 = gen[i*3] as i64 146 let gg: i64 = gen[i*3+1] as i64 147 let gb: i64 = gen[i*3+2] as i64 148 let ghue: i64 = ej_hue(gr, gg, gb) 149 if ej_hue_dist(ghue, to_hue) <= EJ_HUE_TOL { shifted = shifted + 1 } 150 else { if ej_hue_dist(ghue, from_hue) <= EJ_HUE_TOL { stayed = stayed + 1 } else { drifted = drifted + 1 } } 151 } 152 } 153 } 154 i = i + 1 155 } 156 out_masked[0] = masked 157 out_shift[0] = shifted 158 out_drift[0] = drifted 159 out_stay[0] = stayed 160 if masked == 0 { return 0 } 161 return (shifted * 1000) / masked 162} 163 164// ---- INDEPENDENT COLLATERAL SIGNAL ------------------------------------------------------------ 165// DEC026 forbids the judge from sharing the editor's mask -- a judge that reuses the editor's notion 166// of "the garment" will bless the editor's own mistake (MEASURED: a hue mask took the model's face and 167// the judge scored it 1000). So the collateral check uses NO mask at all: it simply asks what FRACTION 168// OF THE WHOLE FRAME changed hue. A garment recolour has a small footprint by nature; repainting most 169// of the picture is implausible for "change the dress" whatever the compliance number says. 170// This is deliberately crude and mask-free -- that is exactly why it cannot be fooled the same way. 171func ej_footprint(ref: *u8, gen: *u8, npix: i64) -> i64 { 172 var moved: i64 = 0 173 var i: i64 = 0 174 while i < npix { 175 let rh: i64 = ej_hue(ref[i*3] as i64, ref[i*3+1] as i64, ref[i*3+2] as i64) 176 let gh: i64 = ej_hue(gen[i*3] as i64, gen[i*3+1] as i64, gen[i*3+2] as i64) 177 if ej_hue_dist(rh, gh) > EJ_HUE_TOL { moved = moved + 1 } 178 i = i + 1 179 } 180 if npix <= 0 { return 0 } 181 return (moved * 1000) / npix 182} 183 184// Boxed wrapper at the default saturation floor (back-compat). 185func ej_compliance_box(ref: *u8, gen: *u8, w: i64, h: i64, from_hue: i64, to_hue: i64, 186 x0: i64, y0: i64, x1: i64, y1: i64, 187 out_masked: *i64, out_shift: *i64, out_drift: *i64, out_stay: *i64) -> i64 { 188 return ej_compliance_spec(ref, gen, w, h, from_hue, to_hue, x0, y0, x1, y1, EJ_SAT_MIN, 189 out_masked, out_shift, out_drift, out_stay) 190} 191 192// Full-frame convenience wrapper (back-compat: same signature, box = whole image). 193func ej_compliance(ref: *u8, gen: *u8, npix: i64, from_hue: i64, to_hue: i64, 194 out_masked: *i64, out_shift: *i64, out_drift: *i64, out_stay: *i64) -> i64 { 195 return ej_compliance_spec(ref, gen, npix, 1, from_hue, to_hue, 0, 0, 1000, 1000, EJ_SAT_MIN, 196 out_masked, out_shift, out_drift, out_stay) 197}