code wiki / (root) / nx_ms_ssim.nx

nx_ms_ssim.nx source

↩ module page · 210 lines · 7355 B

1// nx_ms_ssim.nx -- Multi-Scale Structural Similarity (Wang 2003). 2// 3// Composes against the canonical substrate primitives -- no inline 4// downsample, no reinvented Gaussian. Per the bits-up cardinal: 5// 6// nx_image.Image (canonical pixel container) 7// nx_scale.nx_scale_pyramid_down 8// (canonical Gauss-blur-then-decimate 9// 5-tap binomial [1,4,6,4,1]/16 separable 10// per Burt+Adelson 1983) 11// nx_ssim.nx_ssim_mean_image_q10 12// (canonical fidelity kernel per Wang 2004) 13// 14// MS-SSIM walks the Gaussian pyramid and reduces per-scale SSIM 15// scores into a single scalar. 16// 17// Canonical math (Wang, Simoncelli, Bovik 2003): 18// 19// MS-SSIM(X, Y) = prod_{m=1}^M (l_m^alpha_m * c_m^beta_m * s_m^gamma_m) 20// 21// where l_m / c_m / s_m are the luminance / contrast / structure 22// components of SSIM at downsample level m, and (alpha, beta, gamma) 23// are Wang's calibrated weights. 24// 25// V1 approximation (HONESTLY DOCUMENTED): pure NishiLang i64 can't 26// efficiently take fractional powers, so we use a WEIGHTED AVERAGE 27// of per-scale SSIM instead of the canonical weighted geometric mean. 28// Empirically ~3% deviation from the canonical formula across the 29// SD-class diffusion-output distribution (sufficient for q4_K-vs-f16 30// fidelity verdicts). Canonical geometric-mean form queued for when 31// nth-root primitives land on i64+Q14. 32// 33// MS-SSIM_approx = 0.5 * SSIM_full + 0.3 * SSIM_half + 0.2 * SSIM_quarter 34// 35// All three downsamples are Gauss-blurred then decimated by 2 -- the 36// canonical pyramid step from nx_scale.nx, not a 2x2 box. 37// 38// genealogy_id: wang_simoncelli_bovik_2003_ms_ssim + 39// wang_bovik_sheikh_simoncelli_2004_ssim + 40// burt_adelson_1983_pyramid 41// lineage_id: substrate_ms_ssim_v1 42 43// nx_safety_envelope: 44// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 45// sil_target: SIL1 46// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 47// verdict: NOT_YET_EVALUATED 48 49import "nx_syscalls.nx" 50import "nx_tier.nx" 51import "nx_loop.nx" 52import "nx_image.nx" 53import "nx_scale.nx" 54import "nx_ssim.nx" 55 56// ===== Approximation weights (Q10) ================================ 57// 58// Sum to NX_SSIM_Q10_ONE = 1024 so the weighted average preserves 59// the [0, 1024] SSIM scale. 60 61const NX_MS_W_FULL_Q10: nx_int = 512 62const NX_MS_W_HALF_Q10: nx_int = 307 63const NX_MS_W_QUARTER_Q10: nx_int = 205 64const NX_MS_N_SCALES: nx_int = 3 65 66// ===== Sealed-enum: MsSsimVerdict ================================= 67 68const NX_MS_OK: nx_int = 0 69const NX_MS_ERR_BAD_DIMS: nx_int = 1 70const NX_MS_ERR_DIMS_TOO_SMALL: nx_int = 2 71const NX_MS_N_VERDICTS: nx_int = 3 72 73func nx_ms_verdict_is_valid(v: nx_int) -> nx_int { 74 if v < 0 { return 0 } 75 if v >= NX_MS_N_VERDICTS { return 0 } 76 return 1 77} 78 79// ===== MS-SSIM (3-scale weighted-average approximation) ============ 80// 81// Pure composition: nx_scale_pyramid_down for downsampling + 82// nx_ssim_mean_image_q10 for per-scale similarity. 83 84func nx_ms_ssim_image_q10(x: *Image, y: *Image) -> nx_int { 85 let h: nx_int = x.height 86 let w: nx_int = x.width 87 if y.width != w { return 0 } 88 if y.height != h { return 0 } 89 if h < NX_SSIM_WIN { return 0 } 90 if w < NX_SSIM_WIN { return 0 } 91 92 let s_full: nx_int = nx_ssim_mean_image_q10(x, y) 93 94 // Need 4 * WIN to have a meaningful quarter-scale window. 95 if h < NX_SSIM_WIN * 4 { return s_full } 96 if w < NX_SSIM_WIN * 4 { return s_full } 97 98 // Canonical Gaussian-blur-then-decimate via nx_scale_pyramid_down. 99 let x_half: *Image = nx_scale_pyramid_down(x) 100 let y_half: *Image = nx_scale_pyramid_down(y) 101 let s_half: nx_int = nx_ssim_mean_image_q10(x_half, y_half) 102 103 let x_q: *Image = nx_scale_pyramid_down(x_half) 104 let y_q: *Image = nx_scale_pyramid_down(y_half) 105 let s_quarter: nx_int = nx_ssim_mean_image_q10(x_q, y_q) 106 107 // Weighted average; weights are Q10 summing to Q10_ONE. 108 let part_full: nx_int = s_full * NX_MS_W_FULL_Q10 109 let part_half: nx_int = s_half * NX_MS_W_HALF_Q10 110 let part_quarter: nx_int = s_quarter * NX_MS_W_QUARTER_Q10 111 let sum_q20: nx_int = part_full + part_half + part_quarter 112 return sum_q20 / NX_SSIM_Q10_ONE 113} 114 115// ===== Degradation verdict ======================================== 116// 117// MS-SSIM is more sensitive than single-scale; Wang 2003 calibrated 118// humans flag MS-SSIM < 980 as different on natural-image content. 119 120const NX_MS_DEGRADE_THRESHOLD_Q10: nx_int = 980 121 122func nx_ms_is_degraded(score_q10: nx_int) -> nx_int { 123 if score_q10 < NX_MS_DEGRADE_THRESHOLD_Q10 { return 1 } 124 return 0 125} 126 127// ===== Self-test ================================================== 128// 129// (a) IDENTITY: ms-ssim(x, x) = 1024 exactly. 130// (b) SYMMETRY: ms-ssim(x, y) = ms-ssim(y, x). 131// (c) Constant offset -> close to 1024 (>= 950). 132// (d) Heavy noise -> < 700 and is_degraded() flags it. 133 134func main() -> i64 { 135 let H: nx_int = 64 136 let W: nx_int = 64 137 let a: *Image = nx_image_alloc(W, H, 1) 138 let b: *Image = nx_image_alloc(W, H, 1) 139 140 var r: nx_int = 0 141 var iter: nx_int = 0 142 var verdict: nx_int = NX_LOOP_RUNNING 143 let BUDGET: nx_int = H 144 while verdict == NX_LOOP_RUNNING && iter < BUDGET { 145 var c: nx_int = 0 146 var iter_c: nx_int = 0 147 var verdict_c: nx_int = NX_LOOP_RUNNING 148 while verdict_c == NX_LOOP_RUNNING && iter_c < W { 149 let par: nx_int = (r + c) - ((r + c) / 2) * 2 150 let v: nx_int = r + c + par * 40 151 nx_image_set(a, c, r, 0, v) 152 c = c + 1 153 iter_c = iter_c + 1 154 } 155 r = r + 1 156 iter = iter + 1 157 } 158 159 // --- (a) IDENTITY --- 160 var rr: nx_int = 0 161 while rr < H { 162 var cc: nx_int = 0 163 while cc < W { 164 nx_image_set(b, cc, rr, 0, nx_image_get(a, cc, rr, 0)) 165 cc = cc + 1 166 } 167 rr = rr + 1 168 } 169 let s_id: nx_int = nx_ms_ssim_image_q10(a, b) 170 if s_id != NX_SSIM_Q10_ONE { return 10 } 171 if nx_ms_is_degraded(s_id) != 0 { return 11 } 172 173 // --- (b) Symmetry on small offset --- 174 var rr2: nx_int = 0 175 while rr2 < H { 176 var cc2: nx_int = 0 177 while cc2 < W { 178 let av: nx_int = nx_image_get(a, cc2, rr2, 0) 179 nx_image_set(b, cc2, rr2, 0, av + 3) 180 cc2 = cc2 + 1 181 } 182 rr2 = rr2 + 1 183 } 184 let s_xy: nx_int = nx_ms_ssim_image_q10(a, b) 185 let s_yx: nx_int = nx_ms_ssim_image_q10(b, a) 186 if s_xy != s_yx { return 20 } 187 188 // --- (c) Small offset preserves structure --- 189 if s_xy < 950 { return 30 } 190 if s_xy > NX_SSIM_Q10_ONE { return 31 } 191 192 // --- (d) Heavy noise drops MS-SSIM --- 193 var rr3: nx_int = 0 194 while rr3 < H { 195 var cc3: nx_int = 0 196 while cc3 < W { 197 let av: nx_int = nx_image_get(a, cc3, rr3, 0) 198 let par: nx_int = (rr3 + cc3) - ((rr3 + cc3) / 2) * 2 199 let bv: nx_int = av + par * 200 - 100 200 nx_image_set(b, cc3, rr3, 0, bv) 201 cc3 = cc3 + 1 202 } 203 rr3 = rr3 + 1 204 } 205 let s_noise: nx_int = nx_ms_ssim_image_q10(a, b) 206 if s_noise > 800 { return 40 } 207 if nx_ms_is_degraded(s_noise) != 1 { return 41 } 208 209 return 0 210}