nx_face_count.nx source
↩ module page · 171 lines · 6193 B
1// nx_face_count.nx -- face-cardinality detector.
2// Dark-blob CC inside FACE bbox + eye-pair clustering.
3
4// nx_safety_envelope:
5// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
6// sil_target: SIL1
7// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
8// verdict: NOT_YET_EVALUATED
9
10import "nx_syscalls.nx"
11import "nx_image.nx"
12import "nx_features.nx"
13import "nx_region.nx"
14const NX_MAGIC_1024: i64 = 1024
15
16const NX_FC_NO_FACE: i64 = 0
17const NX_FC_SINGLE_FACE: i64 = 1
18const NX_FC_DOUBLE_FACE: i64 = 2
19const NX_FC_MULTI_FACE: i64 = 3
20
21const NX_FC_DARK_THRESH: i64 = 50
22const NX_FC_MIN_EYE_AREA_Q10: i64 = 5
23const NX_FC_MAX_EYE_AREA_Q10: i64 = 100
24const NX_FC_Y_TOLERANCE_Q10: i64 = 100
25const NX_FC_INTER_EYE_MIN_Q10: i64 = 200
26const NX_FC_INTER_EYE_MAX_Q10: i64 = 500
27const NX_FC_MIN_EYE_ABS: i64 = 2
28const NX_FC_MIN_YTOL_ABS: i64 = 2
29const NX_FC_MIN_INTER_ABS: i64 = 3
30
31struct FaceCountVerdict {
32 kind: i64,
33 face_region_count: i64,
34 eye_pair_count: i64,
35 eye_blob_count: i64,
36 severity_q10: i64,
37 fidelity_q10: i64,
38}
39
40func nx_fc_dark_mask(gray: *Image) -> *Image {
41 let dark: *Image = nx_image_alloc(gray.width, gray.height, 1)
42 var y: i64 = 0
43 while y < gray.height {
44 var x: i64 = 0
45 while x < gray.width {
46 var v: i64 = 0
47 if nx_image_get(gray, x, y, 0) < NX_FC_DARK_THRESH { v = 255 }
48 nx_image_set(dark, x, y, 0, v)
49 x = x + 1
50 }
51 y = y + 1
52 }
53 return dark
54}
55
56func nx_face_count_compute(rgb: *Image, verdict: *FaceCountVerdict) -> i64 {
57 verdict.kind = NX_FC_NO_FACE
58 verdict.face_region_count = 0
59 verdict.eye_pair_count = 0
60 verdict.eye_blob_count = 0
61 verdict.severity_q10 = 0
62 verdict.fidelity_q10 = 0
63
64 let set: *RegionSet = nx_region_segment(rgb)
65 let n_faces: i64 = nx_region_count_kind(set, NX_REGION_FACE)
66 verdict.face_region_count = n_faces
67 if n_faces == 0 { return 0 }
68
69 let f_idx: i64 = nx_region_find_kind(set, NX_REGION_FACE)
70 let f_min_x: i64 = nx_region_get_field(set, f_idx, NX_REGION_F_BBOX_MIN_X)
71 let f_min_y: i64 = nx_region_get_field(set, f_idx, NX_REGION_F_BBOX_MIN_Y)
72 let f_max_x: i64 = nx_region_get_field(set, f_idx, NX_REGION_F_BBOX_MAX_X)
73 let f_max_y: i64 = nx_region_get_field(set, f_idx, NX_REGION_F_BBOX_MAX_Y)
74 let f_w: i64 = f_max_x - f_min_x + 1
75 let f_h: i64 = f_max_y - f_min_y + 1
76 let f_area: i64 = f_w * f_h
77 if f_area <= 0 { return 0 }
78
79 var min_eye: i64 = (f_area * NX_FC_MIN_EYE_AREA_Q10) / NX_MAGIC_1024
80 if min_eye < NX_FC_MIN_EYE_ABS { min_eye = NX_FC_MIN_EYE_ABS }
81 let max_eye: i64 = (f_area * NX_FC_MAX_EYE_AREA_Q10) / NX_MAGIC_1024
82 var y_tol: i64 = (f_h * NX_FC_Y_TOLERANCE_Q10) / NX_MAGIC_1024
83 if y_tol < NX_FC_MIN_YTOL_ABS { y_tol = NX_FC_MIN_YTOL_ABS }
84 var inter_min: i64 = (f_w * NX_FC_INTER_EYE_MIN_Q10) / NX_MAGIC_1024
85 if inter_min < NX_FC_MIN_INTER_ABS { inter_min = NX_FC_MIN_INTER_ABS }
86 let inter_max: i64 = (f_w * NX_FC_INTER_EYE_MAX_Q10) / NX_MAGIC_1024
87
88 let gray: *Image = nx_image_to_grayscale(rgb)
89 let dark: *Image = nx_fc_dark_mask(gray)
90 let cc: *CCResult = nx_feat_connected_components(dark)
91 let nblobs: i64 = cc.n_components
92 if nblobs == 0 { return 0 }
93
94 let stats: *i64 = (sys_mmap(nblobs * 7 * 8 + 16)) as *i64
95 nx_feat_component_stats(cc, stats)
96
97 let cand: *i64 = (sys_mmap(nblobs * 2 * 8 + 16)) as *i64
98 var n_cand: i64 = 0
99 var i: i64 = 0
100 while i < nblobs {
101 let area: i64 = stats[i * 7]
102 if area >= min_eye {
103 if area <= max_eye {
104 let cx: i64 = stats[i * 7 + 5] / 1000
105 let cy: i64 = stats[i * 7 + 6] / 1000
106 if cx >= f_min_x {
107 if cx <= f_max_x {
108 if cy >= f_min_y {
109 if cy <= f_max_y {
110 cand[n_cand * 2] = cx
111 cand[n_cand * 2 + 1] = cy
112 n_cand = n_cand + 1
113 }
114 }
115 }
116 }
117 }
118 }
119 i = i + 1
120 }
121 verdict.eye_blob_count = n_cand
122
123 let paired: *i64 = (sys_mmap(n_cand * 8 + 16)) as *i64
124 var k: i64 = 0
125 while k < n_cand { paired[k] = 0; k = k + 1 }
126 var pair_count: i64 = 0
127 i = 0
128 while i < n_cand {
129 if paired[i] == 0 {
130 let cx_i: i64 = cand[i * 2]
131 let cy_i: i64 = cand[i * 2 + 1]
132 var j: i64 = i + 1
133 var found: i64 = 0
134 while j < n_cand {
135 if found == 0 {
136 if paired[j] == 0 {
137 let cx_j: i64 = cand[j * 2]
138 let cy_j: i64 = cand[j * 2 + 1]
139 var dy: i64 = cy_j - cy_i
140 if dy < 0 { dy = -dy }
141 var dx: i64 = cx_j - cx_i
142 if dx < 0 { dx = -dx }
143 if dy <= y_tol {
144 if dx >= inter_min {
145 if dx <= inter_max {
146 paired[i] = 1; paired[j] = 1
147 pair_count = pair_count + 1
148 found = 1
149 }
150 }
151 }
152 }
153 }
154 j = j + 1
155 }
156 }
157 i = i + 1
158 }
159 verdict.eye_pair_count = pair_count
160 if pair_count == 0 { verdict.kind = NX_FC_NO_FACE }
161 if pair_count == 1 { verdict.kind = NX_FC_SINGLE_FACE }
162 if pair_count == 2 { verdict.kind = NX_FC_DOUBLE_FACE }
163 if pair_count >= 3 { verdict.kind = NX_FC_MULTI_FACE }
164 var extra: i64 = pair_count - 1
165 if extra < 0 { extra = 0 }
166 var sev: i64 = extra * 512
167 if sev > NX_MAGIC_1024 { sev = NX_MAGIC_1024 }
168 verdict.severity_q10 = sev
169 verdict.fidelity_q10 = nx_region_get_field(set, f_idx, NX_REGION_F_CONF_Q10)
170 return 0
171}