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}