code wiki / (root) / nx_salience.nx

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}