nx_areola_localize.nx source
↩ module page · 172 lines · 6010 B
1// nx_areola_localize.nx -- find areola disk within breast bbox.
2//
3// Within the breast bbox produced by nx_breast_localize, the areola
4// is the small darker-pigmented region. Substrate threshold:
5// areola pixel = luminance < (breast_mean_luminance - DARKNESS_DELTA)
6//
7// Returns bbox + area + centroid of the largest contiguous-by-row dark
8// cluster. Feeds nx_areola_pigment for the pigment-vs-reproductive-
9// state verdict.
10//
11// Output flat-array (per single breast):
12// result[0..3] areola bbox: x0, y0, x1, y1
13// result[4] area_px
14// result[5] centroid_x
15// result[6] centroid_y
16// result[7] breast_mean_luminance (debug)
17
18// nx_safety_envelope:
19// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
20// sil_target: SIL1
21// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
22// verdict: NOT_YET_EVALUATED
23
24import "nx_syscalls.nx"
25import "nx_image.nx"
26
27const NX_AREOLAL_RES_X0: i64 = 0
28const NX_AREOLAL_RES_Y0: i64 = 1
29const NX_AREOLAL_RES_X1: i64 = 2
30const NX_AREOLAL_RES_Y1: i64 = 3
31const NX_AREOLAL_RES_AREA: i64 = 4
32const NX_AREOLAL_RES_CENTROID_X: i64 = 5
33const NX_AREOLAL_RES_CENTROID_Y: i64 = 6
34const NX_AREOLAL_RES_BREAST_MEAN: i64 = 7
35const NX_AREOLAL_RES_FIELDS: i64 = 8
36
37// Pixels darker than baseline by this Y delta = areola candidate.
38const NX_AREOLAL_DARKNESS_DELTA: i64 = 18
39
40// Compute mean luminance over the breast bbox.
41// breast_bbox: i64 array [x0, y0, x1, y1]
42func nx_areolal_breast_mean(rgb: *Image, breast_bbox: *i64) -> i64 {
43 let x0: i64 = breast_bbox[0]
44 let y0: i64 = breast_bbox[1]
45 let x1: i64 = breast_bbox[2]
46 let y1: i64 = breast_bbox[3]
47 var sum: i64 = 0
48 var n: i64 = 0
49 var y: i64 = y0
50 while y <= y1 {
51 if y >= 0 { if y < rgb.height {
52 var x: i64 = x0
53 while x <= x1 {
54 if x >= 0 { if x < rgb.width {
55 let r: i64 = nx_image_get(rgb, x, y, 0)
56 let g: i64 = nx_image_get(rgb, x, y, 1)
57 let b: i64 = nx_image_get(rgb, x, y, 2)
58 let yv: i64 = (77 * r + 150 * g + 29 * b) / 256
59 sum = sum + yv
60 n = n + 1
61 } }
62 x = x + 1
63 }
64 } }
65 y = y + 1
66 }
67 if n == 0 { return 0 }
68 return sum / n
69}
70
71// Locate areola via luminance threshold within breast bbox.
72func nx_areola_localize(rgb: *Image, breast_bbox: *i64, result: *i64) -> i64 {
73 var i: i64 = 0
74 while i < NX_AREOLAL_RES_FIELDS { result[i] = 0; i = i + 1 }
75
76 let x0: i64 = breast_bbox[0]
77 let y0: i64 = breast_bbox[1]
78 let x1: i64 = breast_bbox[2]
79 let y1: i64 = breast_bbox[3]
80 if x1 <= x0 { return 1 }
81 if y1 <= y0 { return 2 }
82
83 let baseline: i64 = nx_areolal_breast_mean(rgb, breast_bbox)
84 result[NX_AREOLAL_RES_BREAST_MEAN] = baseline
85 let threshold: i64 = baseline - NX_AREOLAL_DARKNESS_DELTA
86 if threshold < 0 { return 3 }
87
88 // Accumulate dark-pixel bbox + sum-coords for centroid.
89 var areola_x0: i64 = x1
90 var areola_y0: i64 = y1
91 var areola_x1: i64 = x0
92 var areola_y1: i64 = y0
93 var area: i64 = 0
94 var sum_x: i64 = 0
95 var sum_y: i64 = 0
96 var y: i64 = y0
97 while y <= y1 {
98 if y >= 0 { if y < rgb.height {
99 var x: i64 = x0
100 while x <= x1 {
101 if x >= 0 { if x < rgb.width {
102 let r: i64 = nx_image_get(rgb, x, y, 0)
103 let g: i64 = nx_image_get(rgb, x, y, 1)
104 let b: i64 = nx_image_get(rgb, x, y, 2)
105 let yv: i64 = (77 * r + 150 * g + 29 * b) / 256
106 if yv < threshold {
107 if x < areola_x0 { areola_x0 = x }
108 if x > areola_x1 { areola_x1 = x }
109 if y < areola_y0 { areola_y0 = y }
110 if y > areola_y1 { areola_y1 = y }
111 area = area + 1
112 sum_x = sum_x + x
113 sum_y = sum_y + y
114 }
115 } }
116 x = x + 1
117 }
118 } }
119 y = y + 1
120 }
121
122 if area > 0 {
123 result[NX_AREOLAL_RES_X0] = areola_x0
124 result[NX_AREOLAL_RES_Y0] = areola_y0
125 result[NX_AREOLAL_RES_X1] = areola_x1
126 result[NX_AREOLAL_RES_Y1] = areola_y1
127 result[NX_AREOLAL_RES_AREA] = area
128 result[NX_AREOLAL_RES_CENTROID_X] = sum_x / area
129 result[NX_AREOLAL_RES_CENTROID_Y] = sum_y / area
130 }
131 return 0
132}
133
134// Build an areola binary mask (255 in dark pixels within breast bbox,
135// 0 elsewhere). Used downstream by nx_areola_pigment which expects a
136// mask input.
137func nx_areola_build_mask(rgb: *Image, breast_bbox: *i64, mask_out: *Image) -> i64 {
138 let w: i64 = mask_out.width
139 let h: i64 = mask_out.height
140 var yy: i64 = 0
141 while yy < h {
142 var xx: i64 = 0
143 while xx < w { nx_image_set(mask_out, xx, yy, 0, 0); xx = xx + 1 }
144 yy = yy + 1
145 }
146 let x0: i64 = breast_bbox[0]
147 let y0: i64 = breast_bbox[1]
148 let x1: i64 = breast_bbox[2]
149 let y1: i64 = breast_bbox[3]
150 let baseline: i64 = nx_areolal_breast_mean(rgb, breast_bbox)
151 let threshold: i64 = baseline - NX_AREOLAL_DARKNESS_DELTA
152 if threshold < 0 { return 1 }
153
154 var y: i64 = y0
155 while y <= y1 {
156 if y >= 0 { if y < h {
157 var x: i64 = x0
158 while x <= x1 {
159 if x >= 0 { if x < w {
160 let r: i64 = nx_image_get(rgb, x, y, 0)
161 let g: i64 = nx_image_get(rgb, x, y, 1)
162 let b: i64 = nx_image_get(rgb, x, y, 2)
163 let yv: i64 = (77 * r + 150 * g + 29 * b) / 256
164 if yv < threshold { nx_image_set(mask_out, x, y, 0, 255) }
165 } }
166 x = x + 1
167 }
168 } }
169 y = y + 1
170 }
171 return 0
172}