nx_body_measurement_extract.nx source
↩ module page · 270 lines · 10639 B
1// nx_body_measurement_extract.nx -- silhouette + scale -> BodyDimensions.
2//
3// Closes the customer-facing side of the retail-fit pipeline. Customer
4// takes a body photo (or stands in front of any camera); substrate
5// extracts measurable dimensions in millimeters. Pairs directly with
6// nx_garment_fit_match for the substrate-grounded "Small" replacement.
7//
8// PRIVACY NOTE: the substrate runs CLIENT-SIDE. Pixel data + measurements
9// stay on the customer's device. Only the verdict (PERFECT_TAILORED /
10// GOOD_FIT / etc. + recommended SKU rank) ships to the retailer's
11// server. No body-photo upload; no body-measurement leak. This is
12// the privacy-preserving inversion of the typical "send us your
13// measurements" e-commerce flow.
14//
15// CONTRACT (V1):
16// silhouette binary mask (foreground=1, background=0); single
17// channel; same w x h as the original photo
18// reference_px pixels measured for the reference object's
19// long axis (e.g., card_long_axis_pixels)
20// reference_mm the reference's real-world long axis in mm
21// (credit card 85.6mm; A4 paper 297mm; etc.)
22// landmark_y[] caller-supplied landmark row indices:
23// [0] top_of_head (auto-detectable as
24// topmost foreground row)
25// [1] shoulder_y (~ head + 0.13 * height)
26// [2] bust_y (~ head + 0.25 * height)
27// [3] waist_y (~ head + 0.37 * height,
28// OR narrowest-cross-
29// section search range)
30// [4] hip_y (~ head + 0.50 * height)
31// [5] inseam_top_y (groin / crotch)
32// [6] ankle_y (auto-detectable as
33// bottommost foreground row)
34// out *BodyDimensions struct from nx_garment_fit_match
35//
36// Future brick (queued): nx_body_landmark_detect.nx -- given silhouette,
37// emit landmark_y array automatically via anthropometric proportion
38// fitting + edge curvature. Until then, the caller provides them.
39//
40// genealogy_id: iso_8559_1989_body_measure + heine_2012_anthropometry +
41// winks_1997_static_dimensions + pheasant_2003_bodyspace
42// lineage_id: body_measurement_extract_q10_mm
43
44// nx_safety_envelope:
45// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
46// sil_target: SIL1
47// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
48// verdict: NOT_YET_EVALUATED
49
50import "nx_syscalls.nx"
51import "nx_tier.nx"
52import "nx_image.nx"
53import "nx_garment_fit_match.nx"
54
55const NX_BME_Q: nx_int = 1024
56
57// Landmark Y-index sealed constants (input array indices)
58const NX_BME_LM_TOP_OF_HEAD: nx_int = 0
59const NX_BME_LM_SHOULDER: nx_int = 1
60const NX_BME_LM_BUST: nx_int = 2
61const NX_BME_LM_WAIST: nx_int = 3
62const NX_BME_LM_HIP: nx_int = 4
63const NX_BME_LM_INSEAM_TOP: nx_int = 5
64const NX_BME_LM_ANKLE: nx_int = 6
65const NX_BME_LM_COUNT: nx_int = 7
66
67// Extraction fidelity sealed-enum bands (output)
68const NX_BME_FIDELITY_HIGH: nx_int = 0 // tight reference, clean silhouette
69const NX_BME_FIDELITY_MODERATE: nx_int = 1
70const NX_BME_FIDELITY_LOW: nx_int = 2 // silhouette holes / no reference
71const NX_BME_FIDELITY_UNUSABLE: nx_int = 3 // refuse to claim measurements
72const NX_BME_N_FIDELITY_BANDS: nx_int = 4
73
74struct BodyExtractReport {
75 body: BodyDimensions,
76 scale_px_per_mm_q10: nx_int, // pixels per mm in Q10
77 silhouette_area: nx_int, // pixel count of foreground
78 fidelity_band: nx_int, // NX_BME_FIDELITY_*
79 fidelity_q10: nx_int,
80}
81
82// ===== Helpers ========================================================
83
84// Measure horizontal width of the silhouette at row y.
85// Walks left-to-right finding first/last foreground pixel.
86// Returns pixel count (0 if no foreground at that row).
87func _bme_width_at_row(sil: *Image, y: nx_int) -> nx_int {
88 let w: nx_int = sil.width
89 let h: nx_int = sil.height
90 if y < 0 { return 0 }
91 if y >= h { return 0 }
92 var left: nx_int = -1
93 var right: nx_int = -1
94 var x: nx_int = 0
95 while x < w {
96 let v: nx_int = nx_image_get(sil, x, y, 0)
97 if v > 0 {
98 if left < 0 { left = x }
99 right = x
100 }
101 x = x + 1
102 }
103 if left < 0 { return 0 }
104 return right - left + 1
105}
106
107// Sum foreground pixels (silhouette area for fidelity gate).
108func _bme_silhouette_area(sil: *Image) -> nx_int {
109 let w: nx_int = sil.width
110 let h: nx_int = sil.height
111 var sum: nx_int = 0
112 var y: nx_int = 0
113 while y < h {
114 var x: nx_int = 0
115 while x < w {
116 let v: nx_int = nx_image_get(sil, x, y, 0)
117 if v > 0 { sum = sum + 1 }
118 x = x + 1
119 }
120 y = y + 1
121 }
122 return sum
123}
124
125// Find the narrowest horizontal width in a Y-range (waist auto-locate).
126// Returns the narrowest width. Useful when caller doesn't have an
127// exact waist_y -- pass a y-range bracketing the mid-torso and let
128// the substrate find the local minimum.
129func nx_bme_narrowest_width_in_range(sil: *Image,
130 y_lo: nx_int, y_hi: nx_int) -> nx_int {
131 if y_lo >= y_hi { return 0 }
132 var min_w: nx_int = -1
133 var y: nx_int = y_lo
134 while y <= y_hi {
135 let w: nx_int = _bme_width_at_row(sil, y)
136 if w > 0 {
137 if min_w < 0 { min_w = w }
138 if w < min_w { min_w = w }
139 }
140 y = y + 1
141 }
142 if min_w < 0 { return 0 }
143 return min_w
144}
145
146// Find the widest horizontal width in a Y-range (hip auto-locate).
147func nx_bme_widest_width_in_range(sil: *Image,
148 y_lo: nx_int, y_hi: nx_int) -> nx_int {
149 if y_lo >= y_hi { return 0 }
150 var max_w: nx_int = 0
151 var y: nx_int = y_lo
152 while y <= y_hi {
153 let w: nx_int = _bme_width_at_row(sil, y)
154 if w > max_w { max_w = w }
155 y = y + 1
156 }
157 return max_w
158}
159
160// Convert pixels to millimeters using the reference-object scale.
161// scale_q10 = px_per_mm * Q10. mm = (pixels * Q10) / scale_q10.
162func _bme_px_to_mm(pixels: nx_int, scale_q10: nx_int) -> nx_int {
163 if scale_q10 <= 0 { return 0 }
164 return (pixels * NX_BME_Q) / scale_q10
165}
166
167// ===== Public extractor ===============================================
168
169func nx_body_measurement_extract(
170 sil: *Image,
171 reference_pixels: nx_int,
172 reference_mm: nx_int,
173 landmark_y: *nx_int,
174 archetype: nx_int,
175 out: *BodyExtractReport
176) -> nx_int {
177 // ---- Compute scale ----
178 if reference_pixels <= 0 { return 1 }
179 if reference_mm <= 0 { return 1 }
180 // scale_q10 = (reference_pixels * Q10) / reference_mm -- px-per-mm in Q10
181 let scale_q10: nx_int = (reference_pixels * NX_BME_Q) / reference_mm
182 out.scale_px_per_mm_q10 = scale_q10
183 if scale_q10 <= 0 { return 1 }
184
185 // ---- Per-landmark widths ----
186 let shoulder_y: nx_int = landmark_y[NX_BME_LM_SHOULDER]
187 let bust_y: nx_int = landmark_y[NX_BME_LM_BUST]
188 let waist_y: nx_int = landmark_y[NX_BME_LM_WAIST]
189 let hip_y: nx_int = landmark_y[NX_BME_LM_HIP]
190 let top_y: nx_int = landmark_y[NX_BME_LM_TOP_OF_HEAD]
191 let ankle_y: nx_int = landmark_y[NX_BME_LM_ANKLE]
192 let inseam_y: nx_int = landmark_y[NX_BME_LM_INSEAM_TOP]
193
194 let shoulder_w: nx_int = _bme_width_at_row(sil, shoulder_y)
195 let bust_w: nx_int = _bme_width_at_row(sil, bust_y)
196 let waist_w: nx_int = _bme_width_at_row(sil, waist_y)
197 let hip_w: nx_int = _bme_width_at_row(sil, hip_y)
198
199 // ---- Pixel widths -> mm ----
200 // Note: silhouette WIDTH is the chord; the human body's
201 // circumference at that level is approximated as pi * width / 2
202 // (assuming roughly elliptical cross-section). V1 uses width
203 // directly; V2 will apply the elliptical-correction factor once
204 // depth-from-2-photos primitive ships.
205 let shoulder_mm: nx_int = _bme_px_to_mm(shoulder_w, scale_q10)
206 let bust_mm: nx_int = _bme_px_to_mm(bust_w, scale_q10)
207 let waist_mm: nx_int = _bme_px_to_mm(waist_w, scale_q10)
208 let hip_mm: nx_int = _bme_px_to_mm(hip_w, scale_q10)
209
210 // Height: top_of_head to ankle (vertical distance in pixels -> mm)
211 let height_px: nx_int = ankle_y - top_y
212 let height_mm: nx_int = _bme_px_to_mm(height_px, scale_q10)
213
214 // Inseam: inseam_top to ankle
215 let inseam_px: nx_int = ankle_y - inseam_y
216 let inseam_mm: nx_int = _bme_px_to_mm(inseam_px, scale_q10)
217
218 // Arm length: shoulder to ~ wrist (estimated as 0.45 * height
219 // anthropometric average until landmark detector ships)
220 let arm_mm: nx_int = (height_mm * 45) / 100
221
222 // ---- Populate BodyDimensions ----
223 out.body.bust_mm = bust_mm
224 out.body.waist_mm = waist_mm
225 out.body.hip_mm = hip_mm
226 out.body.height_mm = height_mm
227 out.body.shoulder_width_mm = shoulder_mm
228 out.body.arm_length_mm = arm_mm
229 out.body.inseam_mm = inseam_mm
230 out.body.archetype = archetype
231
232 // ---- Fidelity gate ----
233 // Silhouette area must be non-trivial; landmark widths must all
234 // be > 0; height-to-width ratio must be reasonable (anthropometric
235 // average ~ 3:1 to 4:1 for full-body shots).
236 out.silhouette_area = _bme_silhouette_area(sil)
237
238 var fid: nx_int = NX_BME_Q
239 if out.silhouette_area < 100 { fid = fid / 2 }
240 if shoulder_w == 0 { fid = fid / 2 }
241 if bust_w == 0 { fid = fid / 2 }
242 if waist_w == 0 { fid = fid / 2 }
243 if hip_w == 0 { fid = fid / 2 }
244 if height_px <= 0 { fid = 0 }
245 out.fidelity_q10 = fid
246
247 if fid == 0 {
248 out.fidelity_band = NX_BME_FIDELITY_UNUSABLE
249 } else {
250 if fid >= 768 { out.fidelity_band = NX_BME_FIDELITY_HIGH }
251 if fid < 768 { out.fidelity_band = NX_BME_FIDELITY_MODERATE }
252 if fid < 384 { out.fidelity_band = NX_BME_FIDELITY_LOW }
253 }
254
255 return 0
256}
257
258// ===== Sealed-enum validity =========================================
259
260func nx_body_measurement_fidelity_is_valid(b: nx_int) -> nx_int {
261 if b < 0 { return 0 }
262 if b >= NX_BME_N_FIDELITY_BANDS { return 0 }
263 return 1
264}
265
266func nx_body_measurement_landmark_is_valid(l: nx_int) -> nx_int {
267 if l < 0 { return 0 }
268 if l >= NX_BME_LM_COUNT { return 0 }
269 return 1
270}