code wiki / _hdl_build / nx_refbench_lib.nx
nx_refbench_lib.nx source
↩ module page · 125 lines · 5565 B
1// nx_refbench_lib.nx -- THE REFERENCE-RETENTION KERNEL (shared by the tool and its gate).
2//
3// Extracted as a lib on day one so the CLI (nx_refbench.nx) and the gate
4// (nx_refbench_gate.nx) measure with the SAME code. A gate that re-implements the
5// thing it is grading proves nothing about the thing that ships.
6//
7// THE QUESTION: when we generate FROM a reference image, how much of the reference
8// actually survived? Prompt-only round-trips were already MEASURED to be a lossy codec
9// (semantics ~85%, geometry ~50%, identity 0%) -- which is why the L2/L3/L4 structure
10// channels exist. But an unmeasured channel is an UNPROVEN channel: without a ruler,
11// "img2img preserved the composition" is an opinion.
12//
13// TWO INDEPENDENT CHANNELS, HEADLINE = MIN. A generator can hold global layout while
14// destroying local structure, or hold local structure while shifting every tone. Those
15// are DIFFERENT failures with DIFFERENT fixes, so they get different numbers and the
16// headline reports the WEAKEST -- per the report-MIN-across-judges discipline. One
17// blended number is not a measurement.
18// * structure = SSIM (Wang 2004) on a common plane: local luminance/contrast/covariance.
19// * perceptual = dHash Hamming (Krawetz 2011): RELATIVE order of adjacent pixels, so it
20// is invariant to uniform brightness/contrast and robust to rescale.
21// Agreement between two independent descriptors is evidence; a single descriptor is a
22// target waiting to be gamed.
23//
24// COMPOSES ONLY PROVEN ORGANS -- zero new ranking or resize math: nx_png_decoder,
25// nx_image (canonical *Image), nx_ssim, nx_phash (whose downscale kernel is reused as
26// the common-resolution plane, so a 2550x3300 reference and a 768x1024 render are
27// compared at ONE scale by ONE kernel instead of growing a second resampler).
28// license_tier: ORIGINAL
29import "nx_png_decoder.nx"
30import "nx_image.nx"
31import "nx_ssim.nx"
32import "nx_phash.nx"
33
34const RB_PLANE: i64 = 256 // common comparison plane (both sides resampled to this)
35
36// ITU-R BT.601 luma, integer. Grayscale is the shared substrate both descriptors
37// consume, so colour-space handling happens EXACTLY once, here.
38func rb_to_gray(px: *u8, w: i64, h: i64, nch: i64) -> *u8 {
39 let g: *u8 = sys_mmap(w * h)
40 var i: i64 = 0
41 while i < w * h {
42 let o: i64 = i * nch
43 if nch >= 3 {
44 let r: i64 = px[o] as i64
45 let gg: i64 = px[o + 1] as i64
46 let b: i64 = px[o + 2] as i64
47 g[i] = (((r * 299 + gg * 587 + b * 114) / 1000) as u8)
48 } else {
49 g[i] = px[o]
50 }
51 i = i + 1
52 }
53 return g
54}
55
56// Decode a PNG off disk into a grayscale buffer. Returns null on ANY failure and writes
57// 0 into out_w. FAIL-CLOSED ON PURPOSE: a failed decode must never fall through to a
58// zeroed plane, because two zeroed planes would score a perfect 1000 against each other
59// -- the blank-decoy trap that has bitten this ecosystem before.
60func rb_load_gray(path: *u8, out_w: *i64, out_h: *i64) -> *u8 {
61 out_w[0] = 0
62 out_h[0] = 0
63 let len_box: *i64 = sys_mmap(8)
64 let buf: *u8 = sys_read_file(path, len_box)
65 if buf == (0 as *u8) { return 0 as *u8 }
66 let n: i64 = len_box[0]
67 if n <= 0 { return 0 as *u8 }
68 let res: *NxPngResult = nx_png_decode(buf, n)
69 if res == (0 as *NxPngResult) { return 0 as *u8 }
70 let ec: i64 = res.error_code as i64
71 if ec != 0 { return 0 as *u8 }
72 let hdr: *NxPngHeader = res.header
73 let w: i64 = hdr.width as i64
74 let h: i64 = hdr.height as i64
75 let nch: i64 = res.n_channels as i64
76 let px: *u8 = res.pixels
77 if w <= 0 { return 0 as *u8 }
78 if h <= 0 { return 0 as *u8 }
79 out_w[0] = w
80 out_h[0] = h
81 return rb_to_gray(px, w, h, nch)
82}
83
84// Wrap a raw grayscale plane as the canonical *Image so nx_ssim consumes exactly the
85// type every other imaging organ consumes (no private image struct in this organ).
86func rb_wrap(gray: *u8, side: i64) -> *Image {
87 let img: *Image = nx_image_alloc(side, side, 1)
88 img.pixels = gray
89 img.stride = side
90 return img
91}
92
93// The measurement. Both sides land on ONE plane at ONE scale before any comparison, so
94// a size difference can never masquerade as a content difference.
95func rb_compare(ga: *u8, wa: i64, ha: i64, gb: *u8, wb: i64, hb: i64,
96 out_struct: *i64, out_percep: *i64) -> i64 {
97 let pa: *u8 = sys_mmap(RB_PLANE * RB_PLANE)
98 let pb: *u8 = sys_mmap(RB_PLANE * RB_PLANE)
99 nx_phash_downscale(ga, wa, ha, pa, RB_PLANE, RB_PLANE)
100 nx_phash_downscale(gb, wb, hb, pb, RB_PLANE, RB_PLANE)
101
102 // structure: SSIM returns Q10 (0..1024) -> permil
103 let ia: *Image = rb_wrap(pa, RB_PLANE)
104 let ib: *Image = rb_wrap(pb, RB_PLANE)
105 let s_q10: i64 = nx_ssim_mean_image_q10(ia, ib) as i64
106 var s_permil: i64 = s_q10 * 1000 / 1024
107 if s_permil < 0 { s_permil = 0 }
108 if s_permil > 1000 { s_permil = 1000 }
109 out_struct[0] = s_permil
110
111 // perceptual: dHash on the NATIVE planes -- the hash is scale-robust BY DESIGN, so
112 // hashing post-downscale would throw away the very robustness being measured.
113 let fa: i64 = nx_phash_dhash(ga, wa, ha)
114 let fb: i64 = nx_phash_dhash(gb, wb, hb)
115 let ham: i64 = nx_simhash_hamming(fa, fb)
116 out_percep[0] = (64 - ham) * 1000 / 64
117 return 0
118}
119
120// The headline is the WEAKEST channel, never the average. Exposed as a function so the
121// tool and the gate can never disagree about what "retention" means.
122func rb_retention(sp: i64, pp: i64) -> i64 {
123 if pp < sp { return pp }
124 return sp
125}