code wiki / (root) / nx_aesthetics.nx

nx_aesthetics.nx source

↩ module page · 223 lines · 8287 B

1// nx_aesthetics.nx -- composite image-quality scoring (V10). 2// 3// Synthesizes prior V1-V8 primitives into a single objective image 4// quality report. All measures are Q10 fixed-point. 5// 6// Five components: 7// 1. color harmony (V1) -- hue distribution narrowness 8// 2. lighting quality (V3) -- exposure, range, clipping 9// 3. composition (V2) -- thirds + balance + restrained symmetry 10// 4. subject clarity (V8) -- salience peakedness (low flatness) 11// 5. sharpness (L4) -- mean Sobel magnitude 12// 13// Composite is the equal-weighted mean (configurable in future). 14// 15// Per CAPTAIN_MORONI_DOCTRINE: substrate scores IMAGE properties. 16// Never SUBJECTS. Never PERSONS. Never assigns beauty to humans. 17// Bad photo of a beautiful subject scores poorly; good photo of a 18// mundane subject scores well -- because the score is about the 19// photo, not the people in it. 20// 21// genealogy_id: birkhoff_1933_aesthetic_measure + datta_2006_studying 22// + ke_2006_quality_assessment + machajdik_2010_emotion 23// lineage_id: composite_image_quality_score 24 25// nx_safety_envelope: 26// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 27// sil_target: SIL1 28// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 29// verdict: NOT_YET_EVALUATED 30 31import "syscalls.nx" 32import "nx_image.nx" 33import "nx_color_v2.nx" 34import "nx_compose.nx" 35import "nx_light.nx" 36import "nx_salience.nx" 37const K_MAGIC_1024: i64 = 1024 38 39struct AestheticsReport { 40 color_harmony: i64, // Q10 41 lighting_quality: i64, // Q10 42 composition: i64, // Q10 43 subject_clarity: i64, // Q10 44 sharpness: i64, // Q10 45 composite: i64, // Q10 46} 47 48// ===== Component scorers =============================================== 49 50// Color harmony: count unique hue bins (16 bins of 22.5 deg each) 51// among non-gray pixels. Fewer occupied bins = more harmonious. 52// Score = 1024 * (16 - occupied) / 16 clamped at 1024. 53// Achromatic (all gray) returns 512 (neutral). 54 55func nx_aesthetics_color_harmony(rgb: *Image) -> i64 { 56 let w: i64 = rgb.width 57 let h: i64 = rgb.height 58 let bins: *i64 = (sys_mmap(16 * 8)) as *i64 59 var i: i64 = 0 60 while i < 16 { 61 bins[i] = 0 62 i = i + 1 63 } 64 let hsv: *HSVColor = (sys_mmap(24)) as *HSVColor 65 var n_chromatic: i64 = 0 66 var y: i64 = 0 67 while y < h { 68 var x: i64 = 0 69 while x < w { 70 let r: i64 = nx_image_get(rgb, x, y, 0) 71 let g: i64 = nx_image_get(rgb, x, y, 1) 72 let b: i64 = nx_image_get(rgb, x, y, 2) 73 nx_color_rgb_to_hsv(r, g, b, hsv) 74 if hsv.s >= 30 { // skip near-gray 75 let bin: i64 = hsv.h / 23 // ~16 bins of 22.5 deg 76 if bin >= 0 { 77 if bin < 16 { 78 bins[bin] = bins[bin] + 1 79 n_chromatic = n_chromatic + 1 80 } 81 } 82 } 83 x = x + 1 84 } 85 y = y + 1 86 } 87 if n_chromatic == 0 { return 512 } // achromatic -> neutral 88 var occupied: i64 = 0 89 var j: i64 = 0 90 while j < 16 { 91 if bins[j] > 0 { occupied = occupied + 1 } 92 j = j + 1 93 } 94 // 1 bin = perfect harmony (1024), 16 = chaotic (0) 95 return ((16 - occupied) * K_MAGIC_1024) / 16 96} 97 98// Lighting quality: penalize bad exposure (mean far from 128), 99// small dynamic range, and excessive clipping. 100// 101// score = avg of: 102// exposure_score = 1024 - |mean - 128| * 8 (clamped) 103// range_score = min(range, 200) * 1024 / 200 104// clipping_score = 1024 - max(under, over) (clamped) 105 106func nx_aesthetics_lighting_quality(gray: *Image) -> i64 { 107 let mean: i64 = nx_light_mean_luminance(gray) 108 let range: i64 = nx_light_dynamic_range(gray) 109 let under: i64 = nx_light_underexposed_frac(gray, 10) 110 let over: i64 = nx_light_overexposed_frac(gray, 245) 111 112 var dev: i64 = mean - 128 113 if dev < 0 { dev = -dev } 114 var exposure_score: i64 = K_MAGIC_1024 - dev * 8 115 if exposure_score < 0 { exposure_score = 0 } 116 117 var range_clamped: i64 = range 118 if range_clamped > 200 { range_clamped = 200 } 119 let range_score: i64 = (range_clamped * K_MAGIC_1024) / 200 120 121 var max_clip: i64 = under 122 if over > max_clip { max_clip = over } 123 var clipping_score: i64 = K_MAGIC_1024 - max_clip 124 if clipping_score < 0 { clipping_score = 0 } 125 126 return (exposure_score + range_score + clipping_score) / 3 127} 128 129// Composition score: weighted blend of thirds + bounded symmetry + 130// good balance. Excessive symmetry penalized (it's boring); slight 131// off-center improves over dead-center. 132 133func nx_aesthetics_composition(gray: *Image) -> i64 { 134 let thirds: i64 = nx_compose_thirds_score(gray) 135 let h_sym: i64 = nx_compose_h_symmetry(gray) 136 let balance: i64 = nx_compose_balance_score(gray) 137 138 // Thirds: bonus above baseline 444. 139 var thirds_score: i64 = (thirds - 444) * 4 140 if thirds_score < 0 { thirds_score = 0 } 141 if thirds_score > K_MAGIC_1024 { thirds_score = K_MAGIC_1024 } 142 143 // Symmetry: reward MODERATE symmetry (peak around 700/1024 Q10). 144 // Score = 1024 - |sym - 700| * 2 (clamped to 0..1024). 145 var sym_dev: i64 = h_sym - 700 146 if sym_dev < 0 { sym_dev = -sym_dev } 147 var sym_score: i64 = K_MAGIC_1024 - sym_dev * 2 148 if sym_score < 0 { sym_score = 0 } 149 150 // Balance: reward slight off-center (peak around 100-200 Q10 151 // from center). Score peaks there and decays both ways. 152 var bal_dev: i64 = balance - 150 153 if bal_dev < 0 { bal_dev = -bal_dev } 154 var bal_score: i64 = K_MAGIC_1024 - bal_dev * 4 155 if bal_score < 0 { bal_score = 0 } 156 157 return (thirds_score + sym_score + bal_score) / 3 158} 159 160// Subject clarity from salience flatness. 161// Salience flatness is 1024 = completely flat (boring), 0 = sharp 162// peak. Invert: clarity = 1024 - flatness. 163 164func nx_aesthetics_subject_clarity(gray: *Image) -> i64 { 165 let sal: *Image = nx_salience_intensity(gray) 166 let flatness: i64 = nx_salience_flatness(sal) 167 var clarity: i64 = K_MAGIC_1024 - flatness 168 if clarity < 0 { clarity = 0 } 169 return clarity 170} 171 172// Sharpness: mean Sobel magnitude / 64, clamped to [0, 1024]. 173// Mean Sobel magnitude tends to be 0..200 on typical photos. 174 175func nx_aesthetics_sharpness(gray: *Image) -> i64 { 176 let gx: *ImageS64 = nx_image_sobel_x(gray) 177 let gy: *ImageS64 = nx_image_sobel_y(gray) 178 let mag: *ImageS64 = nx_image_gradient_magnitude(gx, gy) 179 let w: i64 = gray.width 180 let h: i64 = gray.height 181 // Sobel is undefined on the 1-px border: nx_image_get returns 0 for 182 // out-of-bounds neighbours, which fabricates a false edge against the 183 // frame (a uniform image would score non-zero). Sum the INTERIOR only. 184 if w < 3 { return 0 } 185 if h < 3 { return 0 } 186 let n: i64 = (w - 2) * (h - 2) 187 var sum: i64 = 0 188 var y: i64 = 1 189 while y < h - 1 { 190 var x: i64 = 1 191 while x < w - 1 { 192 sum = sum + nx_image_s64_get(mag, x, y) 193 x = x + 1 194 } 195 y = y + 1 196 } 197 let mean_mag: i64 = sum / n 198 var score: i64 = mean_mag * 16 199 if score < 0 { score = 0 } 200 if score > K_MAGIC_1024 { score = K_MAGIC_1024 } 201 return score 202} 203 204// ===== Composite ======================================================== 205// 206// Equal-weight mean of five components. Fills the report struct. 207 208func nx_aesthetics_compute(rgb: *Image, gray: *Image, 209 report: *AestheticsReport) -> i64 { 210 report.color_harmony = nx_aesthetics_color_harmony(rgb) 211 report.lighting_quality = nx_aesthetics_lighting_quality(gray) 212 report.composition = nx_aesthetics_composition(gray) 213 report.subject_clarity = nx_aesthetics_subject_clarity(gray) 214 report.sharpness = nx_aesthetics_sharpness(gray) 215 report.composite = ( 216 report.color_harmony + 217 report.lighting_quality + 218 report.composition + 219 report.subject_clarity + 220 report.sharpness 221 ) / 5 222 return 0 223}