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}