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}