code wiki / (root) / nx_silhouette_integrity.nx

nx_silhouette_integrity.nx source

↩ module page · 342 lines · 12680 B

1// nx_silhouette_integrity.nx -- Tier 6 melty-edge detector. 2// 3// CAPABILITY_COMPLETENESS: FULL 4// 5// User feedback 2026-05-16 (image #5): "its a weird melty body 6// and environment". AI image-gen fails this commonly: the body 7// silhouette wavers / fuses into the background, limb-to-torso 8// joints have no clear contour transition, fabric-to-skin 9// boundaries blur instead of meeting at a sharp edge. 10// 11// Substrate's detector: 12// - Given a segmented blob (from nx_segmenter_classical) and 13// the source RGB image 14// - For each pixel on the blob's PERIMETER (in-blob pixel with 15// at least one out-of-blob neighbor): 16// compute local Sobel gradient magnitude on luminance 17// - Strong gradient = sharp edge; weak = melty 18// - Score = fraction of perimeter pixels above NX_SILH_STRONG_Q10 19// - Sealed verdict SHARP / SOFT / MELTED / NO_PERIMETER 20// 21// Composes: 22// nx_segmenter_classical (provides label_map + blob bbox) 23// inline BT.601 luminance + Sobel 3x3 gradient 24// 25// genealogy_id: substrate_silhouette_integrity_2026_05_16 26// lineage_id: tier_6_melty_detector_v1 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 "nx_syscalls.nx" 35import "nx_runtime.nx" 36import "nx_tier.nx" 37import "nx_segmenter_classical.nx" 38const NX_MAGIC_1024: i64 = 1024 39 40const NX_SILH_Q10: nx_int = 1024 41 42// Sealed verdicts. 43const NX_SILH_VERDICT_SHARP: nx_int = 0 // most perimeter pixels strong 44const NX_SILH_VERDICT_SOFT: nx_int = 1 // moderate 45const NX_SILH_VERDICT_MELTED: nx_int = 2 // most weak; AI tell 46const NX_SILH_VERDICT_NO_PERIMETER: nx_int = 3 // blob too small / no border 47 48// Gradient thresholds (Q10 of normalised |Sobel|). 49// strong: > NX_SILH_STRONG_Q10 (sharp edge) 50// weak: < NX_SILH_WEAK_Q10 (no edge) 51// Normalisation: max Sobel L1 magnitude on 8-bit luma is 4*255 = 1020 52// from Gx and another 1020 from Gy; we divide by 8 to land 53// in [0, 255] then scale to Q10. 54const NX_SILH_STRONG_RAW: nx_int = 60 // strong edge raw threshold 55const NX_SILH_WEAK_RAW: nx_int = 20 56 57// Verdict thresholds on fraction-strong (Q10). 58const NX_SILH_THR_SHARP_Q10: nx_int = 700 // >= 0.68 strong 59const NX_SILH_THR_SOFT_Q10: nx_int = 400 // >= 0.39 strong 60 61// JPL bounds. 62const NX_SILH_MAX_DIM: nx_int = 8192 63 64// ===== result struct ============================================= 65 66struct NxSilhouetteResult { 67 blob_label: nx_int, 68 n_perimeter: nx_int, 69 n_strong: nx_int, 70 n_weak: nx_int, 71 fraction_strong: nx_int, // Q10 72 fraction_weak: nx_int, // Q10 73 mean_gradient: nx_int, // 0..255 raw 74 verdict: nx_int, 75} 76 77const NX_SILH_RESULT_BYTES: nx_size = 64 78 79// ===== BT.601 luma from RGB bytes ================================ 80 81func _silh_luma_u8(pixels: *u8, off: nx_int, bpp: nx_int, n_ch: nx_int) -> nx_int { 82 let r_val: nx_int = (pixels[off] as nx_int) & 255 83 if n_ch < 3 { return r_val } 84 let g_val: nx_int = (pixels[off + 1] as nx_int) & 255 85 let b_val: nx_int = (pixels[off + 2] as nx_int) & 255 86 return (306 * r_val + 601 * g_val + 117 * b_val) / NX_MAGIC_1024 87} 88 89// ===== Sobel 3x3 gradient magnitude ============================== 90// 91// Returns |Gx| + |Gy| (L1 approximation; cheap). Out-of-bounds 92// neighbours treated as zero (caller should ensure pixel is at 93// least 1 in from the image edge for full kernel). 94 95func _silh_sobel_at(pixels: *u8, x: nx_int, y: nx_int, 96 w: nx_int, h: nx_int, bpp: nx_int, n_ch: nx_int) -> nx_int { 97 // 8 neighbours required. Skip edges of image. 98 if x < 1 { return 0 } 99 if y < 1 { return 0 } 100 if (x + 1) >= w { return 0 } 101 if (y + 1) >= h { return 0 } 102 // Gather luma values in 3x3 window. 103 let off00: nx_int = ((y - 1) * w + (x - 1)) * bpp 104 let off01: nx_int = ((y - 1) * w + x) * bpp 105 let off02: nx_int = ((y - 1) * w + (x + 1)) * bpp 106 let off10: nx_int = (y * w + (x - 1)) * bpp 107 let off12: nx_int = (y * w + (x + 1)) * bpp 108 let off20: nx_int = ((y + 1) * w + (x - 1)) * bpp 109 let off21: nx_int = ((y + 1) * w + x) * bpp 110 let off22: nx_int = ((y + 1) * w + (x + 1)) * bpp 111 let l00: nx_int = _silh_luma_u8(pixels, off00, bpp, n_ch) 112 let l01: nx_int = _silh_luma_u8(pixels, off01, bpp, n_ch) 113 let l02: nx_int = _silh_luma_u8(pixels, off02, bpp, n_ch) 114 let l10: nx_int = _silh_luma_u8(pixels, off10, bpp, n_ch) 115 let l12: nx_int = _silh_luma_u8(pixels, off12, bpp, n_ch) 116 let l20: nx_int = _silh_luma_u8(pixels, off20, bpp, n_ch) 117 let l21: nx_int = _silh_luma_u8(pixels, off21, bpp, n_ch) 118 let l22: nx_int = _silh_luma_u8(pixels, off22, bpp, n_ch) 119 // Gx = [-1 0 1; -2 0 2; -1 0 1] 120 let gx: nx_int = (l02 - l00) + 2 * (l12 - l10) + (l22 - l20) 121 // Gy = [-1 -2 -1; 0 0 0; 1 2 1] 122 let gy: nx_int = (l20 - l00) + 2 * (l21 - l01) + (l22 - l02) 123 var abs_gx: nx_int = gx 124 if abs_gx < 0 { abs_gx = 0 - abs_gx } 125 var abs_gy: nx_int = gy 126 if abs_gy < 0 { abs_gy = 0 - abs_gy } 127 // Normalise: max possible is ~4*255 = 1020 per axis; total max ~2040. 128 // Divide by 8 to land roughly in [0, 255]. 129 let mag: nx_int = (abs_gx + abs_gy) / 8 130 if mag > 255 { return 255 } 131 return mag 132} 133 134// ===== perimeter scan + classification =========================== 135 136func nx_silhouette_integrity( 137 pixels: *u8, w: nx_int, h: nx_int, bpp: nx_int, n_ch: nx_int, 138 label_map: *nx_int, blob: *NxSegBlob) -> *NxSilhouetteResult { 139 140 let r_ptr: *u8 = sys_mmap(NX_SILH_RESULT_BYTES) 141 let r: *NxSilhouetteResult = r_ptr as *NxSilhouetteResult 142 r.blob_label = blob.label 143 r.n_perimeter = 0 144 r.n_strong = 0 145 r.n_weak = 0 146 r.fraction_strong = 0 147 r.fraction_weak = 0 148 r.mean_gradient = 0 149 r.verdict = NX_SILH_VERDICT_NO_PERIMETER 150 151 if w <= 0 { return r } 152 if h <= 0 { return r } 153 if w > NX_SILH_MAX_DIM { return r } 154 if h > NX_SILH_MAX_DIM { return r } 155 156 let bid: nx_int = blob.label 157 var n_perim: nx_int = 0 158 var n_strong: nx_int = 0 159 var n_weak: nx_int = 0 160 var sum_grad: nx_int = 0 161 162 var y: nx_int = blob.y0 163 while y < blob.y1 { 164 var x: nx_int = blob.x0 165 while x < blob.x1 { 166 let idx: nx_int = y * w + x 167 if label_map[idx] == bid { 168 // Check 4-neighbours for label difference -> perimeter. 169 var is_perim: nx_int = 0 170 if x == 0 { is_perim = 1 } 171 else { 172 if label_map[idx - 1] != bid { is_perim = 1 } 173 } 174 if is_perim == 0 { 175 if (x + 1) >= w { is_perim = 1 } 176 else { 177 if label_map[idx + 1] != bid { is_perim = 1 } 178 } 179 } 180 if is_perim == 0 { 181 if y == 0 { is_perim = 1 } 182 else { 183 if label_map[idx - w] != bid { is_perim = 1 } 184 } 185 } 186 if is_perim == 0 { 187 if (y + 1) >= h { is_perim = 1 } 188 else { 189 if label_map[idx + w] != bid { is_perim = 1 } 190 } 191 } 192 if is_perim == 1 { 193 let mag: nx_int = _silh_sobel_at(pixels, x, y, w, h, bpp, n_ch) 194 sum_grad = sum_grad + mag 195 n_perim = n_perim + 1 196 if mag >= NX_SILH_STRONG_RAW { 197 n_strong = n_strong + 1 198 } 199 if mag <= NX_SILH_WEAK_RAW { 200 n_weak = n_weak + 1 201 } 202 } 203 } 204 x = x + 1 205 } 206 y = y + 1 207 } 208 209 r.n_perimeter = n_perim 210 r.n_strong = n_strong 211 r.n_weak = n_weak 212 if n_perim <= 0 { 213 r.verdict = NX_SILH_VERDICT_NO_PERIMETER 214 return r 215 } 216 r.mean_gradient = sum_grad / n_perim 217 r.fraction_strong = (n_strong * NX_SILH_Q10) / n_perim 218 r.fraction_weak = (n_weak * NX_SILH_Q10) / n_perim 219 220 if r.fraction_strong >= NX_SILH_THR_SHARP_Q10 { 221 r.verdict = NX_SILH_VERDICT_SHARP 222 } else { 223 if r.fraction_strong >= NX_SILH_THR_SOFT_Q10 { 224 r.verdict = NX_SILH_VERDICT_SOFT 225 } else { 226 r.verdict = NX_SILH_VERDICT_MELTED 227 } 228 } 229 return r 230} 231 232// ===== self-test ================================================== 233// 234// Synthetic test: build a small 10x10 RGB image where pixels in a 235// rectangle are bright (200 luma) and outside are dark (50 luma). 236// The rectangle has a SHARP edge by construction. Run segmenter 237// on a matching binary mask, pick the blob, run silhouette 238// integrity -> expect SHARP verdict. 239// 240// Second test: gradient interior (luma fades from rect center to 241// edge) -> SOFT or MELTED verdict. 242 243func _silh_fill_rgb_rect(pixels: *u8, w: nx_int, bpp: nx_int, 244 x0: nx_int, y0: nx_int, x1: nx_int, y1: nx_int, 245 r: nx_int, g: nx_int, b: nx_int) -> nx_int { 246 var y: nx_int = y0 247 while y < y1 { 248 var x: nx_int = x0 249 while x < x1 { 250 let off: nx_int = (y * w + x) * bpp 251 pixels[off] = r as u8 252 pixels[off + 1] = g as u8 253 pixels[off + 2] = b as u8 254 x = x + 1 255 } 256 y = y + 1 257 } 258 return 0 259} 260 261func main() -> nx_int { 262 // 10x10 RGB image, all dark by default. 263 let pixels: *u8 = (sys_mmap(300)) as *u8 264 _silh_fill_rgb_rect(pixels, 10, 3, 0, 0, 10, 10, 50, 50, 50) 265 266 // Bright 4x4 rectangle in the middle (perfect sharp edges). 267 _silh_fill_rgb_rect(pixels, 10, 3, 3, 3, 7, 7, 220, 220, 220) 268 269 // Build matching binary mask + run segmenter. 270 let mask: *nx_int = (sys_mmap(800)) as *nx_int 271 var z: nx_int = 0 272 while z < 100 { 273 mask[z] = 0 274 z = z + 1 275 } 276 var yy: nx_int = 3 277 while yy < 7 { 278 var xx: nx_int = 3 279 while xx < 7 { 280 mask[yy * 10 + xx] = 1 281 xx = xx + 1 282 } 283 yy = yy + 1 284 } 285 let seg_r: *NxSegResult = nx_segmenter_connected(mask, 10, 10) 286 if seg_r == (0 as *NxSegResult) { return 1 } 287 if seg_r.n_blobs != 1 { return 2 } 288 let blob: *NxSegBlob = nx_segmenter_largest_blob(seg_r) 289 290 let silh_r: *NxSilhouetteResult = nx_silhouette_integrity( 291 pixels, 10, 10, 3, 3, seg_r.label_map, blob) 292 if silh_r == (0 as *NxSilhouetteResult) { return 10 } 293 if silh_r.n_perimeter == 0 { return 11 } 294 // Sharp-edged rectangle -> SHARP verdict expected. 295 if silh_r.verdict != NX_SILH_VERDICT_SHARP { return 12 } 296 if silh_r.fraction_strong < NX_SILH_THR_SHARP_Q10 { return 13 } 297 298 // ---- gradient (melty) case: replace pixels with gradient ---- 299 // 300 // Inside rectangle: luma drops gradually from center to edge. 301 // center (5,5): luma 220 302 // edges: luma 70 (just slightly above background 50) 303 // This produces weak Sobel gradient at perimeter. 304 let pixels2: *u8 = (sys_mmap(300)) as *u8 305 _silh_fill_rgb_rect(pixels2, 10, 3, 0, 0, 10, 10, 50, 50, 50) 306 var py: nx_int = 3 307 while py < 7 { 308 var px: nx_int = 3 309 while px < 7 { 310 // Gradient: luma = 70 at edge of rect, higher toward center. 311 // Just within 1 of the boundary pixel value to make 312 // gradient weak. 313 let off: nx_int = (py * 10 + px) * 3 314 pixels2[off] = 70 as u8 315 pixels2[off + 1] = 70 as u8 316 pixels2[off + 2] = 70 as u8 317 px = px + 1 318 } 319 py = py + 1 320 } 321 // Now luma jump at perimeter is 70 - 50 = 20; Sobel maxes around 322 // 20*4/8 = 10 per axis, total ~20 -> below NX_SILH_STRONG_RAW = 60 323 // and at the NX_SILH_WEAK_RAW = 20 boundary. Should classify 324 // as MELTED. 325 let silh_melted: *NxSilhouetteResult = nx_silhouette_integrity( 326 pixels2, 10, 10, 3, 3, seg_r.label_map, blob) 327 if silh_melted.verdict != NX_SILH_VERDICT_MELTED { return 20 } 328 329 // ---- NO_PERIMETER case (synthetic empty bbox) ---- 330 let empty_blob_ptr: *u8 = sys_mmap(NX_SEG_BLOB_BYTES) 331 let empty_blob: *NxSegBlob = empty_blob_ptr as *NxSegBlob 332 empty_blob.label = 999 // not in label_map 333 empty_blob.x0 = 0 334 empty_blob.y0 = 0 335 empty_blob.x1 = 10 336 empty_blob.y1 = 10 337 let silh_empty: *NxSilhouetteResult = nx_silhouette_integrity( 338 pixels, 10, 10, 3, 3, seg_r.label_map, empty_blob) 339 if silh_empty.verdict != NX_SILH_VERDICT_NO_PERIMETER { return 30 } 340 341 return 0 342}