nx_salience.nx source
↩ module page · 237 lines · 8056 B
1// nx_salience.nx -- Itti-Koch multi-scale salience (V8).
2//
3// Salience = "where does the eye go first" before any semantic
4// understanding. Itti+Koch 1998 model: compute feature pyramids
5// (intensity, color, orientation), take center-surround differences
6// across scales, normalize, sum. Locally distinctive regions emerge.
7//
8// We ship a simplified pure-math variant:
9// - intensity pyramid (grayscale via Rec.709)
10// - edge-magnitude pyramid (Sobel)
11// - center-surround differences at 3 scale ratios
12// - sum into single 8-bit saliency map
13//
14// What this unlocks:
15// + automatic subject detection ("where's the main thing")
16// + cropping suggestions (frame the high-salience region)
17// + image-quality (boring images have flat salience)
18// + auto-focus / auto-exposure region suggestion
19//
20// Per CAPTAIN_MORONI_DOCTRINE this measures IMAGE distinctiveness.
21// It does not classify subjects; it does not rank persons; it
22// only identifies regions that stand out from their surround.
23//
24// genealogy_id: itti_koch_1998_salience + koch_ullman_1985_attention
25// lineage_id: center_surround_difference + multiscale_integration
26// axioms: NX_AX_NORMALIZATION_PRESERVES_RANK
27
28// nx_safety_envelope:
29// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
30// sil_target: SIL1
31// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
32// verdict: NOT_YET_EVALUATED
33
34import "syscalls.nx"
35import "nx_image.nx"
36import "nx_scale.nx"
37
38// ===== 2x nearest upsample ==============================================
39//
40// Each src pixel expands to a 2x2 block in dst. Used to bring
41// coarser pyramid levels back into the finer level's coordinates.
42
43func nx_salience_upsample_2x(src: *Image) -> *Image {
44 let w2: i64 = src.width * 2
45 let h2: i64 = src.height * 2
46 let dst: *Image = nx_image_alloc(w2, h2, 1)
47 var y: i64 = 0
48 while y < src.height {
49 var x: i64 = 0
50 while x < src.width {
51 let v: i64 = nx_image_get(src, x, y, 0)
52 nx_image_set(dst, 2 * x, 2 * y, 0, v)
53 nx_image_set(dst, 2 * x + 1, 2 * y, 0, v)
54 nx_image_set(dst, 2 * x, 2 * y + 1, 0, v)
55 nx_image_set(dst, 2 * x + 1, 2 * y + 1, 0, v)
56 x = x + 1
57 }
58 y = y + 1
59 }
60 return dst
61}
62
63// Upsample by 2^n. Repeats nearest-neighbor doubling n times.
64func nx_salience_upsample_pow2(src: *Image, n: i64) -> *Image {
65 var cur: *Image = src
66 var i: i64 = 0
67 while i < n {
68 cur = nx_salience_upsample_2x(cur)
69 i = i + 1
70 }
71 return cur
72}
73
74// ===== Absolute difference of two same-size images ======================
75
76func nx_salience_abs_diff(a: *Image, b: *Image) -> *Image {
77 let w: i64 = a.width
78 let h: i64 = a.height
79 let dst: *Image = nx_image_alloc(w, h, 1)
80 var y: i64 = 0
81 while y < h {
82 var x: i64 = 0
83 while x < w {
84 let av: i64 = nx_image_get(a, x, y, 0)
85 let bv: i64 = nx_image_get(b, x, y, 0)
86 var d: i64 = av - bv
87 if d < 0 { d = -d }
88 if d > 255 { d = 255 }
89 nx_image_set(dst, x, y, 0, d)
90 x = x + 1
91 }
92 y = y + 1
93 }
94 return dst
95}
96
97// ===== Center-surround intensity salience ==============================
98//
99// Build Gaussian pyramid (5 levels), then compute center-surround
100// differences for scale pairs (0,2) and (1,3) and (2,4). Each
101// produces an "intensity-distinctive" map. Sum upsampled diffs into
102// a single saliency image at full resolution.
103
104func nx_salience_intensity(gray: *Image) -> *Image {
105 let py: *GaussPyramid = nx_scale_gauss_pyramid(gray, 5)
106 let l0: *Image = nx_scale_pyramid_get(py, 0)
107 let l1: *Image = nx_scale_pyramid_get(py, 1)
108 let l2: *Image = nx_scale_pyramid_get(py, 2)
109 let l3: *Image = nx_scale_pyramid_get(py, 3)
110 let l4: *Image = nx_scale_pyramid_get(py, 4)
111
112 // Pair (0, 2): fine-scale distinctiveness.
113 let l2_up_to_0: *Image = nx_salience_upsample_pow2(l2, 2)
114 let d02: *Image = nx_salience_abs_diff(l0, l2_up_to_0)
115
116 // Pair (1, 3): mid-scale distinctiveness.
117 let l3_up_to_1: *Image = nx_salience_upsample_pow2(l3, 2)
118 let d13: *Image = nx_salience_abs_diff(l1, l3_up_to_1)
119 let d13_up_to_0: *Image = nx_salience_upsample_pow2(d13, 1)
120
121 // Pair (2, 4): coarse-scale distinctiveness.
122 let l4_up_to_2: *Image = nx_salience_upsample_pow2(l4, 2)
123 let d24: *Image = nx_salience_abs_diff(l2, l4_up_to_2)
124 let d24_up_to_0: *Image = nx_salience_upsample_pow2(d24, 2)
125
126 // Combine. Use saturating add.
127 let w: i64 = gray.width
128 let h: i64 = gray.height
129 let sal: *Image = nx_image_alloc(w, h, 1)
130 var y: i64 = 0
131 while y < h {
132 var x: i64 = 0
133 while x < w {
134 let v0: i64 = nx_image_get(d02, x, y, 0)
135 let v1: i64 = nx_image_get(d13_up_to_0, x, y, 0)
136 let v2: i64 = nx_image_get(d24_up_to_0, x, y, 0)
137 var sum: i64 = v0 + v1 + v2
138 if sum > 255 { sum = 255 }
139 nx_image_set(sal, x, y, 0, sum)
140 x = x + 1
141 }
142 y = y + 1
143 }
144 return sal
145}
146
147// ===== Combined salience: intensity + edge-magnitude ===================
148//
149// Edges add orientation-distinctiveness without computing full Gabor
150// banks. We use Sobel magnitude (already in nx_image) at multiple
151// scales of the pyramid.
152
153func nx_salience_combined(gray: *Image) -> *Image {
154 let intensity_sal: *Image = nx_salience_intensity(gray)
155 let gx: *ImageS64 = nx_image_sobel_x(gray)
156 let gy: *ImageS64 = nx_image_sobel_y(gray)
157 let mag_s64: *ImageS64 = nx_image_gradient_magnitude(gx, gy)
158 let mag: *Image = nx_image_s64_to_u8(mag_s64)
159 // mag already shows where edges are. Use it as edge-salience
160 // and combine with intensity-salience.
161 let w: i64 = gray.width
162 let h: i64 = gray.height
163 let out: *Image = nx_image_alloc(w, h, 1)
164 var y: i64 = 0
165 while y < h {
166 var x: i64 = 0
167 while x < w {
168 let i_sal: i64 = nx_image_get(intensity_sal, x, y, 0)
169 let e_sal: i64 = nx_image_get(mag, x, y, 0)
170 // Weighted avg: 60% intensity-salience + 40% edge.
171 var v: i64 = (i_sal * 6 + e_sal * 4) / 10
172 if v > 255 { v = 255 }
173 nx_image_set(out, x, y, 0, v)
174 x = x + 1
175 }
176 y = y + 1
177 }
178 return out
179}
180
181// ===== Salience peak ====================================================
182//
183// Returns (x, y) of maximum salience value via output pointers.
184// On tie, the first scanned location wins.
185
186func nx_salience_peak(sal: *Image, out_x: *i64, out_y: *i64) -> i64 {
187 var best_v: i64 = -1
188 var best_x: i64 = 0
189 var best_y: i64 = 0
190 var y: i64 = 0
191 while y < sal.height {
192 var x: i64 = 0
193 while x < sal.width {
194 let v: i64 = nx_image_get(sal, x, y, 0)
195 if v > best_v {
196 best_v = v
197 best_x = x
198 best_y = y
199 }
200 x = x + 1
201 }
202 y = y + 1
203 }
204 out_x[0] = best_x
205 out_y[0] = best_y
206 return best_v
207}
208
209// ===== Salience flatness check ==========================================
210//
211// "Boring" images have flat salience. Returns Q10 flatness where
212// 1024 = totally flat, 0 = strongly peaked. Computed as
213// mean / max ratio: 1.0 means everywhere equals the max (flat);
214// values approaching 0 indicate a sharp peak relative to the average.
215
216func nx_salience_flatness(sal: *Image) -> i64 {
217 let w: i64 = sal.width
218 let h: i64 = sal.height
219 let n: i64 = w * h
220 if n == 0 { return 1024 }
221 var sum: i64 = 0
222 var max_v: i64 = 0
223 var y: i64 = 0
224 while y < h {
225 var x: i64 = 0
226 while x < w {
227 let v: i64 = nx_image_get(sal, x, y, 0)
228 sum = sum + v
229 if v > max_v { max_v = v }
230 x = x + 1
231 }
232 y = y + 1
233 }
234 if max_v == 0 { return 1024 }
235 let mean: i64 = sum / n
236 return (mean * 1024) / max_v
237}