nx_natstat.nx source
↩ module page · 472 lines · 19321 B
1// nx_natstat.nx -- NATURAL-IMAGE-STATISTICS photoreal metric = the REVERSE AI JUDGE. Real photographs have a
2// signature our old critic missed: DETAIL AT EVERY SCALE (a ~1/f amplitude spectrum -> roughly EQUAL detail
3// energy per octave). Clay/CG fails it two ways: too SMOOTH (fine + mid octaves empty) or GRAIN-ONLY (a spike
4// at the finest octave, smooth beneath). We measure the multi-scale detail profile + its balance (+ gradient
5// heavy-tail + colour spread) and return a graded PHOTOREAL LEVEL 0..1000. Uses:
6// (a) REVERSE JUDGE: maximise this to push Z-Image toward more-real output for games/VR (evaluator-in-reverse);
7// (b) "USE-AS-PHOTOREAL-TO-LEVEL": even a known-AI image is usable as photoreal up to the level it scores.
8// Integer, deterministic, no FFT (Laplacian-pyramid octave energies). license_tier: ORIGINAL
9import "nx_syscalls.nx"
10import "nx_vecmath.nx"
11const K_MAGIC_65536: i64 = 65536
12const K_MAGIC_3200: i64 = 3200
13const K_MAGIC_6500: i64 = 6500
14
15func ns_lum(g: i64) -> i64 { return ((g & 255) + ((g >> 8) & 255) + ((g >> 16) & 255)) / 3 }
16func ns_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
17func ns_isqrt(v: i64) -> i64 { return vm_isqrt(v) }
18
19// ★N1 NSS: MSCN (Mean-Subtracted Contrast-Normalized) coefficients + the GGD RATIO ρ=(E|x|)²/E[x²] (the core
20// BRISQUE/NIQE feature). For a NATURAL image the MSCN coefficients are ~unit-Gaussian -> ρ ≈ 0.637 (=2/π);
21// synthetic/CG deviates. Local mean+std over a 5x5 window; returns ρ in PER-MILLE (natural ~637).
22func ns_mscn_rho(fb: *i64, w: i64, h: i64) -> i64 {
23 let R: i64 = 2
24 var sum_abs: i64 = 0
25 var sum_sq: i64 = 0
26 var n: i64 = 0
27 var y: i64 = R
28 while y < h - R {
29 var x: i64 = R
30 while x < w - R {
31 var m: i64 = 0
32 var m2: i64 = 0
33 var dy: i64 = 0 - R
34 while dy <= R {
35 var dx: i64 = 0 - R
36 while dx <= R {
37 let v: i64 = ns_lum(fb[(y + dy) * w + (x + dx)])
38 m = m + v
39 m2 = m2 + v * v
40 dx = dx + 1
41 }
42 dy = dy + 1
43 }
44 let mean: i64 = m / 25
45 var vr: i64 = m2 / 25 - mean * mean
46 if vr < 0 { vr = 0 }
47 let sd: i64 = ns_isqrt(vr)
48 let cval: i64 = ns_lum(fb[y * w + x])
49 let mscn: i64 = (cval - mean) * 64 / (sd + 3) // fx64, C=3 stability
50 sum_abs = sum_abs + ns_abs(mscn)
51 sum_sq = sum_sq + mscn * mscn / 64 // Σ mscn² in fx64
52 n = n + 1
53 x = x + 1
54 }
55 y = y + 1
56 }
57 if n < 1 { return 0 }
58 if sum_sq < 1 { return 0 }
59 return sum_abs * sum_abs * 1000 / n / sum_sq / 64 // ρ per-mille (natural ~637)
60}
61// ★BRISQUE pairwise features: fill an MSCN buffer once, then read ρ + PAIRWISE-PRODUCT means (how adjacent MSCN
62// coeffs correlate -- the local-structure signal that ρ alone misses; all MSCN-derived => content-invariant).
63func ns_mscn_fill(fb: *i64, w: i64, h: i64, mbuf: *i64) -> i64 {
64 var i: i64 = 0
65 while i < w * h { mbuf[i] = 0; i = i + 1 }
66 let R: i64 = 2
67 var y: i64 = R
68 while y < h - R {
69 var x: i64 = R
70 while x < w - R {
71 var m: i64 = 0
72 var m2: i64 = 0
73 var dy: i64 = 0 - R
74 while dy <= R {
75 var dx: i64 = 0 - R
76 while dx <= R {
77 let v: i64 = ns_lum(fb[(y + dy) * w + (x + dx)])
78 m = m + v
79 m2 = m2 + v * v
80 dx = dx + 1
81 }
82 dy = dy + 1
83 }
84 let mean: i64 = m / 25
85 var vr: i64 = m2 / 25 - mean * mean
86 if vr < 0 { vr = 0 }
87 mbuf[y * w + x] = (ns_lum(fb[y * w + x]) - mean) * 64 / (ns_isqrt(vr) + 3) // MSCN fx64
88 x = x + 1
89 }
90 y = y + 1
91 }
92 return 0
93}
94func ns_mscn_rho_buf(mbuf: *i64, w: i64, h: i64) -> i64 {
95 var sa: i64 = 0
96 var sq: i64 = 0
97 var n: i64 = 0
98 var y: i64 = 2
99 while y < h - 2 {
100 var x: i64 = 2
101 while x < w - 2 {
102 let m: i64 = mbuf[y * w + x]
103 sa = sa + ns_abs(m)
104 sq = sq + m * m / 64
105 n = n + 1
106 x = x + 1
107 }
108 y = y + 1
109 }
110 if n < 1 { return 0 }
111 if sq < 1 { return 0 }
112 return sa * sa * 1000 / n / sq / 64
113}
114// mean pairwise product of MSCN neighbours in direction (dx,dy), fx64. Natural images -> characteristically
115// negative (adjacent normalised coeffs anti-correlate) with a specific magnitude.
116func ns_mscn_pair(mbuf: *i64, w: i64, h: i64, dx: i64, dy: i64) -> i64 {
117 var s: i64 = 0
118 var n: i64 = 0
119 var y: i64 = 2
120 while y < h - 3 {
121 var x: i64 = 2
122 while x < w - 3 {
123 s = s + mbuf[y * w + x] * mbuf[(y + dy) * w + (x + dx)] / 64
124 n = n + 1
125 x = x + 1
126 }
127 y = y + 1
128 }
129 if n < 1 { return 0 }
130 return s / n
131}
132
133// mean |Laplacian| (centre - avg 4-neighbours) over the interior, in luminance, x100 for precision = octave detail energy
134func ns_detail(fb: *i64, w: i64, h: i64) -> i64 {
135 if w < 3 { return 0 }
136 if h < 3 { return 0 }
137 var s: i64 = 0
138 var n: i64 = 0
139 var y: i64 = 1
140 while y < h - 1 {
141 var x: i64 = 1
142 while x < w - 1 {
143 let c: i64 = ns_lum(fb[y * w + x])
144 let nb: i64 = (ns_lum(fb[y * w + x - 1]) + ns_lum(fb[y * w + x + 1]) + ns_lum(fb[(y - 1) * w + x]) + ns_lum(fb[(y + 1) * w + x])) / 4
145 s = s + ns_abs(c - nb)
146 n = n + 1
147 x = x + 1
148 }
149 y = y + 1
150 }
151 if n < 1 { n = 1 }
152 return s * 100 / n
153}
154// 5x5 local std (activity) at (x,y) -- used for NIQE-style patch/subject selection.
155func ns_local_std(fb: *i64, w: i64, x: i64, y: i64) -> i64 {
156 var m: i64 = 0
157 var m2: i64 = 0
158 var dy: i64 = 0 - 2
159 while dy <= 2 {
160 var dx: i64 = 0 - 2
161 while dx <= 2 {
162 let v: i64 = ns_lum(fb[(y + dy) * w + (x + dx)])
163 m = m + v
164 m2 = m2 + v * v
165 dx = dx + 1
166 }
167 dy = dy + 1
168 }
169 let mean: i64 = m / 25
170 var vr: i64 = m2 / 25 - mean * mean
171 if vr < 0 { vr = 0 }
172 return ns_isqrt(vr)
173}
174// ★AXIS 3: gradient KURTOSIS over ACTIVE (subject) pixels only -- NIQE-style patch selection excludes the flat
175// background/sky so the statistic reflects our SURFACE, not scene composition. Natural photos ~ kurtosis 7-32
176// (×100 = 700-3200); an over-smooth blob w/ sparse hard edges spikes FAR higher (flat regions + rare big edges).
177func ns_grad_kurt(fb: *i64, w: i64, h: i64) -> i64 {
178 var peak: i64 = 0
179 var y: i64 = 2
180 while y < h - 2 {
181 var x: i64 = 2
182 while x < w - 2 {
183 let s: i64 = ns_local_std(fb, w, x, y)
184 if s > peak { peak = s }
185 x = x + 3
186 }
187 y = y + 3
188 }
189 let thresh: i64 = peak / 10 // active = >=10% of peak local activity (skips flat bg)
190 var sg2: i64 = 0
191 var sg4: i64 = 0
192 var n: i64 = 0
193 y = 2
194 while y < h - 2 {
195 var x: i64 = 2
196 while x < w - 2 {
197 if ns_local_std(fb, w, x, y) > thresh {
198 let gx: i64 = ns_lum(fb[y * w + x + 1]) - ns_lum(fb[y * w + x - 1])
199 let gy: i64 = ns_lum(fb[(y + 1) * w + x]) - ns_lum(fb[(y - 1) * w + x])
200 let g: i64 = (ns_abs(gx) + ns_abs(gy)) / 4
201 let g2: i64 = g * g
202 sg2 = sg2 + g2
203 sg4 = sg4 + g2 * g2
204 n = n + 1
205 }
206 x = x + 1
207 }
208 y = y + 1
209 }
210 if n < 1 { return 0 }
211 let mg2: i64 = sg2 / n
212 let mg4: i64 = sg4 / n
213 if mg2 < 1 { return 0 }
214 return mg4 * 100 / (mg2 * mg2)
215}
216// ★AXIS 4: COLOURFULNESS (Hasler-Süsstrunk) -- natural photos sit in a characteristic colour-spread range
217// (not grey, not over-saturated). Returns colourfulness in the 0..~110 luminance scale.
218func ns_colorful(fb: *i64, w: i64, h: i64) -> i64 {
219 var srg: i64 = 0
220 var syb: i64 = 0
221 var srg2: i64 = 0
222 var syb2: i64 = 0
223 var n: i64 = 0
224 var i: i64 = 0
225 let tot: i64 = w * h
226 while i < tot {
227 let g: i64 = fb[i]
228 let rr: i64 = g & 255
229 let gg: i64 = (g >> 8) & 255
230 let bb: i64 = (g >> 16) & 255
231 let rg: i64 = rr - gg
232 let yb: i64 = (rr + gg) / 2 - bb
233 srg = srg + rg
234 syb = syb + yb
235 srg2 = srg2 + rg * rg
236 syb2 = syb2 + yb * yb
237 n = n + 1
238 i = i + 1
239 }
240 if n < 1 { return 0 }
241 let mrg: i64 = srg / n
242 let myb: i64 = syb / n
243 var vrg: i64 = srg2 / n - mrg * mrg
244 var vyb: i64 = syb2 / n - myb * myb
245 if vrg < 0 { vrg = 0 }
246 if vyb < 0 { vyb = 0 }
247 let sd: i64 = ns_isqrt(vrg + vyb)
248 let mm: i64 = ns_isqrt(mrg * mrg + myb * myb)
249 return sd + mm * 3 / 10
250}
251// ★AXIS 5: AI-FINGERPRINT -- generator upsampling leaves PERIODIC high-freq artifacts. Autocorrelate the
252// horizontal high-pass residual at lags 2 & 4; a real photo's residual decorrelates (~0), AI/grid spikes it.
253// Returns the mean |autocorrelation| at lag 2&4, per-mille (LOW = clean/real, HIGH = periodic artifact).
254func ns_ai_periodicity(fb: *i64, w: i64, h: i64) -> i64 {
255 var s0: i64 = 0
256 var s2: i64 = 0
257 var s4: i64 = 0
258 var y: i64 = 1
259 while y < h - 1 {
260 var x: i64 = 2
261 while x < w - 6 {
262 let hp0: i64 = ns_lum(fb[y * w + x]) - (ns_lum(fb[y * w + x - 1]) + ns_lum(fb[y * w + x + 1])) / 2
263 let hp2: i64 = ns_lum(fb[y * w + x + 2]) - (ns_lum(fb[y * w + x + 1]) + ns_lum(fb[y * w + x + 3])) / 2
264 let hp4: i64 = ns_lum(fb[y * w + x + 4]) - (ns_lum(fb[y * w + x + 3]) + ns_lum(fb[y * w + x + 5])) / 2
265 s0 = s0 + hp0 * hp0
266 s2 = s2 + hp0 * hp2
267 s4 = s4 + hp0 * hp4
268 x = x + 1
269 }
270 y = y + 1
271 }
272 if s0 < 1 { return 0 }
273 return (ns_abs(s2) * 1000 / s0 + ns_abs(s4) * 1000 / s0) / 2
274}
275// box-downsample by 2 into out (nw=w/2 x nh=h/2)
276func ns_down2(fb: *i64, w: i64, h: i64, out: *i64) -> i64 {
277 let nw: i64 = w / 2
278 let nh: i64 = h / 2
279 var y: i64 = 0
280 while y < nh {
281 var x: i64 = 0
282 while x < nw {
283 let a: i64 = fb[(y * 2) * w + x * 2]
284 let b: i64 = fb[(y * 2) * w + x * 2 + 1]
285 let c: i64 = fb[(y * 2 + 1) * w + x * 2]
286 let d: i64 = fb[(y * 2 + 1) * w + x * 2 + 1]
287 let rr: i64 = ((a & 255) + (b & 255) + (c & 255) + (d & 255)) / 4
288 let gg: i64 = (((a >> 8) & 255) + ((b >> 8) & 255) + ((c >> 8) & 255) + ((d >> 8) & 255)) / 4
289 let bb: i64 = (((a >> 16) & 255) + ((b >> 16) & 255) + ((c >> 16) & 255) + ((d >> 16) & 255)) / 4
290 out[y * nw + x] = rr + gg * 256 + bb * K_MAGIC_65536
291 x = x + 1
292 }
293 y = y + 1
294 }
295 return 0
296}
297// raw scale-invariance feature = the octave-ratio SPREAD (0 = perfect power law; large = CG). For corpus fitting.
298func ns_scale_spread(fb: *i64, w: i64, h: i64, buf1: *i64, buf2: *i64) -> i64 {
299 let e0: i64 = ns_detail(fb, w, h)
300 ns_down2(fb, w, h, buf1); let e1: i64 = ns_detail(buf1, w / 2, h / 2)
301 ns_down2(buf1, w / 2, h / 2, buf2); let e2: i64 = ns_detail(buf2, w / 4, h / 4)
302 ns_down2(buf2, w / 4, h / 4, buf1); let e3: i64 = ns_detail(buf1, w / 8, h / 8)
303 var d1: i64 = e1
304 var d2: i64 = e2
305 var d3: i64 = e3
306 if d1 < 1 { d1 = 1 }
307 if d2 < 1 { d2 = 1 }
308 if d3 < 1 { d3 = 1 }
309 let r1: i64 = e0 * 256 / d1
310 let r2: i64 = e1 * 256 / d2
311 let r3: i64 = e2 * 256 / d3
312 var mn: i64 = r1
313 var mx: i64 = r1
314 if r2 < mn { mn = r2 }
315 if r2 > mx { mx = r2 }
316 if r3 < mn { mn = r3 }
317 if r3 > mx { mx = r3 }
318 return mx - mn
319}
320// 1000 inside the natural band [lo,hi]; linear falloff over `edge` beyond either side.
321func ns_band(v: i64, lo: i64, hi: i64, edge: i64) -> i64 {
322 var d: i64 = 0
323 if v < lo { d = lo - v }
324 if v > hi { d = v - hi }
325 if d == 0 { return 1000 }
326 var s: i64 = 1000 - d * 1000 / edge
327 if s < 0 { s = 0 }
328 return s
329}
330// ★COMPREHENSIVE "is this photoreal?" -- fills out[0..4] with PER-AXIS scores (0..1000) so a graphics engine
331// can SEE which axes it fails (= its gaps), and returns the weighted OVERALL level. AI-fingerprint is axis 4,
332// ONE part of the whole, not the whole. out MUST hold >=5 i64. buf1/buf2 = scratch >= w*h*8 each.
333// axis0 SCALE-INVARIANCE (power-law spectrum) · axis1 MSCN GGD-ratio (local NSS) · axis2 GRADIENT-KURTOSIS
334// (edge heavy-tail; too-high = over-smooth CG) · axis3 COLOUR-NATURALNESS · axis4 AI-FINGERPRINT (freq artifact)
335func ns_assess(fb: *i64, w: i64, h: i64, buf1: *i64, buf2: *i64, out: *i64) -> i64 {
336 // --- axis0: scale-invariance via the octave-ratio power law ---
337 let e0: i64 = ns_detail(fb, w, h)
338 ns_down2(fb, w, h, buf1); let e1: i64 = ns_detail(buf1, w / 2, h / 2)
339 ns_down2(buf1, w / 2, h / 2, buf2); let e2: i64 = ns_detail(buf2, w / 4, h / 4)
340 ns_down2(buf2, w / 4, h / 4, buf1); let e3: i64 = ns_detail(buf1, w / 8, h / 8)
341 var a0: i64 = 0
342 if e3 >= 40 {
343 var d1: i64 = e1
344 var d2: i64 = e2
345 var d3: i64 = e3
346 if d1 < 1 { d1 = 1 }
347 if d2 < 1 { d2 = 1 }
348 if d3 < 1 { d3 = 1 }
349 let r1: i64 = e0 * 256 / d1
350 let r2: i64 = e1 * 256 / d2
351 let r3: i64 = e2 * 256 / d3
352 var mn: i64 = r1
353 var mx: i64 = r1
354 if r2 < mn { mn = r2 }
355 if r2 > mx { mx = r2 }
356 if r3 < mn { mn = r3 }
357 if r3 > mx { mx = r3 }
358 a0 = 1000 - (mx - mn) * 20
359 if a0 < 0 { a0 = 0 }
360 }
361 // --- axis1: MSCN FAMILY (BRISQUE) = ρ + horizontal/vertical pairwise-product means, all CORPUS-FIT +
362 // content-invariant. MIN => every MSCN statistic must be natural. Centers ρ=604 ph=18 pv=10 (12 real photos).
363 // Reuse buf2 (pyramid already done with it) as the MSCN coefficient buffer.
364 ns_mscn_fill(fb, w, h, buf2)
365 let rho: i64 = ns_mscn_rho_buf(buf2, w, h)
366 let ph: i64 = ns_mscn_pair(buf2, w, h, 1, 0)
367 let pv: i64 = ns_mscn_pair(buf2, w, h, 0, 1)
368 let pd1: i64 = ns_mscn_pair(buf2, w, h, 1, 1)
369 let pd2: i64 = ns_mscn_pair(buf2, w, h, 1, 0 - 1)
370 var s_rho: i64 = 1000 - ns_abs(rho - 604) * 14 / 10
371 var s_ph: i64 = 1000 - ns_abs(ph - 18) * 1000 / 40
372 var s_pv: i64 = 1000 - ns_abs(pv - 10) * 1000 / 35
373 var s_d1: i64 = 1000 - ns_abs(pd1 - 3) * 1000 / 30 // diagonals: near-zero + noisy -> GENTLE bands
374 var s_d2: i64 = 1000 - ns_abs(pd2 - 2) * 1000 / 30
375 if s_rho < 0 { s_rho = 0 }
376 if s_ph < 0 { s_ph = 0 }
377 if s_pv < 0 { s_pv = 0 }
378 if s_d1 < 0 { s_d1 = 0 }
379 if s_d2 < 0 { s_d2 = 0 }
380 var a1: i64 = s_rho
381 if s_ph < a1 { a1 = s_ph }
382 if s_pv < a1 { a1 = s_pv }
383 if s_d1 < a1 { a1 = s_d1 }
384 if s_d2 < a1 { a1 = s_d2 }
385 // --- axis2: gradient kurtosis in the natural band (too-high = over-smooth blob CG = OUR typical gap) ---
386 let a2: i64 = ns_band(ns_grad_kurt(fb, w, h), 700, K_MAGIC_3200, K_MAGIC_6500)
387 // --- axis3: colour naturalness -- GREY bites (tight low edge) but VIVID real photos pass (corpus has colour
388 // up to 96); only EXTREME oversaturation (>100, synthetic bars) bites on the high side ---
389 let cf: i64 = ns_colorful(fb, w, h)
390 var a3: i64 = 1000
391 if cf < 12 { a3 = ns_band(cf, 12, 58, 16) }
392 if cf > 100 { a3 = 1000 - (cf - 100) * 1000 / 40 }
393 if a3 < 0 { a3 = 0 }
394 // --- axis4: AI-fingerprint (periodic freq artifact; low=clean) ---
395 let a4: i64 = ns_band(ns_ai_periodicity(fb, w, h), 0, 130, 320)
396 out[0] = a0
397 out[1] = a1
398 out[2] = a2
399 out[3] = a3
400 out[4] = a4
401 // ★realism LEVEL = MIN of the CONTENT-INVARIANT axes {MSCN(1), colour(3), ai-fp(4)}. scale-invariance(0) +
402 // raw-kurtosis(2) are CONTENT-CONFOUNDED (busy scene vs smooth portrait -- corpus proved they crater real
403 // busy photos) so they stay in the gap-map (out[]) but are EXCLUDED from realism. MIN = you are only as
404 // photoreal as your WORST true-realism statistic -> no flattery (our clay's MSCN=408 tanks it, colour/ai
405 // can't rescue it), and real photos (all three axes natural) stay high.
406 var lvl: i64 = out[1]
407 if out[3] < lvl { lvl = out[3] }
408 if out[4] < lvl { lvl = out[4] }
409 return lvl
410}
411// thin wrapper: the single graded PHOTOREAL LEVEL 0..1000.
412func ns_photoreal(fb: *i64, w: i64, h: i64, buf1: *i64, buf2: *i64) -> i64 {
413 let out: *i64 = sys_mmap(64) as *i64
414 return ns_assess(fb, w, h, buf1, buf2, out)
415}
416// ★HARDENING (2026-07-23 red-team): lag-1 spatial autocorrelation of luminance, per-mille. Natural images are
417// locally COHERENT (neighbours correlated: smooth regions + structured edges) ~high; white NOISE ~0. This is the
418// axis MSCN/detail miss -- pure noise maxes MSCN but has ZERO coherence, so without this the grader rates noise
419// SEMI-REAL. Averages horizontal + vertical lag-1 correlation over the interior.
420func ns_coherence(fb: *i64, w: i64, h: i64) -> i64 {
421 let np: i64 = w*h
422 var sum: i64 = 0
423 var i: i64 = 0
424 while i < np { sum = sum + ns_lum(fb[i]); i = i + 1 }
425 let mu: i64 = sum / np
426 var num: i64 = 0
427 var den: i64 = 0
428 var y: i64 = 0
429 while y < h - 1 {
430 var x: i64 = 0
431 while x < w - 1 {
432 let c: i64 = ns_lum(fb[y*w+x]) - mu
433 let rt: i64 = ns_lum(fb[y*w+x+1]) - mu
434 let dn: i64 = ns_lum(fb[(y+1)*w+x]) - mu
435 num = num + c*rt + c*dn
436 den = den + c*c*2
437 x = x + 1
438 }
439 y = y + 1
440 }
441 if den < 1 { return 0 }
442 var r: i64 = num*1000/den
443 if r < 0 { r = 0 }
444 if r > 1000 { r = 1000 }
445 return r
446}
447// ★HARDENED assess (2026-07-23): the coherence-GATED photoreal level. ns_assess alone rates pure noise
448// SEMI-REAL (475) because MSCN maxes on noise; multiply by a coherence knee so an incoherent image CANNOT score
449// high. KNEE=900 = the lower edge of the MEASURED genuine-content coherence cluster (real photos + real renders
450// all 970-998; pure noise 0; a real photo wrecked with heavy noise 606) -> data-derived, not a dialled constant.
451// out[5]=coherence is written for transparency. Genuine content (coh>=900) is UNCHANGED; noise collapses to CLAY.
452func ns_assess2(fb: *i64, w: i64, h: i64, buf1: *i64, buf2: *i64, out: *i64) -> i64 {
453 let base: i64 = ns_assess(fb, w, h, buf1, buf2, out)
454 let coh: i64 = ns_coherence(fb, w, h)
455 out[5] = coh
456 var lvl: i64 = base
457 if coh < 900 { lvl = base * coh / 900 }
458 return lvl
459}
460func ns_verdict(level: i64) -> *u8 {
461 if level >= 650 { return "PHOTOREAL-GRADE" as *u8 }
462 if level >= 380 { return "SEMI-REAL" as *u8 }
463 if level >= 180 { return "STYLIZED-CG" as *u8 }
464 return "CLAY" as *u8
465}
466func ns_axisname(i: i64) -> *u8 {
467 if i == 0 { return "scale-invariance (multi-scale power law) -- add texture at ALL scales" as *u8 }
468 if i == 1 { return "MSCN GGD-ratio (local contrast statistics)" as *u8 }
469 if i == 2 { return "gradient-kurtosis -- too smooth: flat blobs + sparse edges, need pervasive micro-texture" as *u8 }
470 if i == 3 { return "colour-naturalness (grey / over-saturated)" as *u8 }
471 return "ai-fingerprint (periodic generation artifact)" as *u8
472}