nx_lash_transition.nx source
↩ module page · 108 lines · 3966 B
1// nx_lash_transition.nx -- eyelid-to-lash edge gradient softness.
2//
3// Mannequin face marker: lashes meet eyelid as a hard sharp line
4// (rendered as a discrete edge). Real lashes fade softly into the
5// eyelid skin tone over 2-3 pixels of micro-blur.
6//
7// Math:
8// Within the eyelid band (3-5 px ABOVE the iris top), measure
9// per-column vertical luminance gradient |Y(y) - Y(y-1)| and
10// accumulate. High mean gradient = sharp line = MANNEQUIN.
11// Low mean gradient = soft fade = NATURAL.
12//
13// Output flat-array:
14// result[0] = pixel_count
15// result[1] = mean_gradient
16// result[2] = max_gradient
17// result[3] = hard_transitions (count of gradients > HARD_THRESH)
18// result[4] = verdict (0=MANNEQUIN, 1=AMBIGUOUS, 2=NATURAL)
19
20// nx_safety_envelope:
21// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
22// sil_target: SIL1
23// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
24// verdict: NOT_YET_EVALUATED
25
26import "nx_syscalls.nx"
27import "nx_image.nx"
28
29const NX_LASH_RES_PIXELS: i64 = 0
30const NX_LASH_RES_MEAN_GRAD: i64 = 1
31const NX_LASH_RES_MAX_GRAD: i64 = 2
32const NX_LASH_RES_HARD_TRANS: i64 = 3
33const NX_LASH_RES_VERDICT: i64 = 4
34const NX_LASH_RES_FIELDS: i64 = 5
35
36const NX_LASH_VERDICT_MANNEQUIN: i64 = 0
37const NX_LASH_VERDICT_AMBIGUOUS: i64 = 1
38const NX_LASH_VERDICT_NATURAL: i64 = 2
39
40// Hard-edge threshold: gradient > 60 = mannequin-like sharp line.
41const NX_LASH_HARD_THRESH: i64 = 60
42const NX_LASH_MEAN_NATURAL_MAX: i64 = 25 // mean gradient ≤ 25 = soft NATURAL
43const NX_LASH_MEAN_MANNEQUIN_MIN: i64 = 45 // mean gradient ≥ 45 = sharp MANNEQUIN
44
45// Compute luminance at (x, y).
46func nx_lash_lum(rgb: *Image, x: i64, y: i64) -> i64 {
47 if x < 0 { return 0 }
48 if y < 0 { return 0 }
49 if x >= rgb.width { return 0 }
50 if y >= rgb.height { return 0 }
51 let r: i64 = nx_image_get(rgb, x, y, 0)
52 let g: i64 = nx_image_get(rgb, x, y, 1)
53 let b: i64 = nx_image_get(rgb, x, y, 2)
54 return (77 * r + 150 * g + 29 * b) / 256
55}
56
57// Score lash-eyelid transition in the band above iris top.
58// bbox layout (4 entries): iris_x0, iris_y0, iris_x1, iris_y1
59// band_height: 4 = scan 4 pixels above iris top
60func nx_lash_transition(rgb: *Image, bbox: *i64, band_height: i64,
61 result: *i64) -> i64 {
62 var i: i64 = 0
63 while i < NX_LASH_RES_FIELDS { result[i] = 0; i = i + 1 }
64 let ix0: i64 = bbox[0]
65 let iy0: i64 = bbox[1]
66 let ix1: i64 = bbox[2]
67 if ix1 <= ix0 { return 1 }
68 if band_height <= 0 { return 2 }
69
70 let band_y0: i64 = iy0 - band_height
71 let band_y1: i64 = iy0
72 if band_y0 < 1 { return 3 } // need y-1 to exist
73
74 var sum_grad: i64 = 0
75 var max_grad: i64 = 0
76 var pixels: i64 = 0
77 var hard: i64 = 0
78
79 var y: i64 = band_y0
80 while y < band_y1 {
81 var x: i64 = ix0
82 while x <= ix1 {
83 let y_curr: i64 = nx_lash_lum(rgb, x, y)
84 let y_above: i64 = nx_lash_lum(rgb, x, y - 1)
85 var grad: i64 = y_curr - y_above
86 if grad < 0 { grad = -grad }
87 sum_grad = sum_grad + grad
88 if grad > max_grad { max_grad = grad }
89 if grad > NX_LASH_HARD_THRESH { hard = hard + 1 }
90 pixels = pixels + 1
91 x = x + 1
92 }
93 y = y + 1
94 }
95
96 result[NX_LASH_RES_PIXELS] = pixels
97 if pixels == 0 { return 0 }
98 let mean_grad: i64 = sum_grad / pixels
99 result[NX_LASH_RES_MEAN_GRAD] = mean_grad
100 result[NX_LASH_RES_MAX_GRAD] = max_grad
101 result[NX_LASH_RES_HARD_TRANS] = hard
102
103 var verdict: i64 = NX_LASH_VERDICT_AMBIGUOUS
104 if mean_grad <= NX_LASH_MEAN_NATURAL_MAX { verdict = NX_LASH_VERDICT_NATURAL }
105 else { if mean_grad >= NX_LASH_MEAN_NATURAL_MAX { if mean_grad >= NX_LASH_MEAN_MANNEQUIN_MIN { verdict = NX_LASH_VERDICT_MANNEQUIN } } }
106 result[NX_LASH_RES_VERDICT] = verdict
107 return 0
108}