nx_garment_extract_measurements.nx source
↩ module page · 290 lines · 9990 B
1// nx_garment_extract_measurements.nx -- product photo -> GarmentDimensions.
2//
3// Closes the RETAILER side of the lossless-sizing pipeline. Retailer
4// photographs each SKU with a reference object in frame (or beside it);
5// substrate extracts real bust/waist/hip/length/shoulder/sleeve/inseam
6// in millimeters. Eliminates the manual "type 36 measurements per
7// SKU" tax that's the chief reason apparel sizing data is so dirty
8// in the first place.
9//
10// Pairs directly with nx_body_measurement_extract on the customer
11// side + nx_garment_fit_match for the verdict. All three primitives
12// use the same silhouette + reference-scale + caller-supplied
13// landmark-Y pattern; substrate doesn't pretend to detect landmarks
14// (that's a separate future primitive nx_garment_landmark_detect.nx).
15//
16// CONTRACT (V1):
17// silhouette binary mask of the garment (background-removed)
18// reference_pixels pixel length of reference object's long axis
19// reference_mm real-world long axis in mm
20// landmark_y[] row indices: top / bust / waist / hip / hem /
21// sleeve_end / inseam_top
22// cut garment cut sealed-enum (NX_FIT_CUT_*) -- caller
23// knows the SKU's cut; substrate routes flattery
24// math accordingly
25// out *GarmentExtractReport carrying GarmentDimensions
26//
27// genealogy_id: iso_8559_1989_body_measure + asbm_2017_apparel_grading +
28// ashdown_2007_sizing_systems
29// lineage_id: garment_measurement_extract_q10_mm
30
31// nx_safety_envelope:
32// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
33// sil_target: SIL1
34// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
35// verdict: NOT_YET_EVALUATED
36
37import "nx_syscalls.nx"
38import "nx_tier.nx"
39import "nx_image.nx"
40import "nx_garment_fit_match.nx"
41
42const NX_GME_Q: nx_int = 1024
43
44// Landmark Y-index sealed constants (input array indices)
45const NX_GME_LM_TOP: nx_int = 0 // top of shoulder seam OR waistband
46const NX_GME_LM_BUST: nx_int = 1
47const NX_GME_LM_WAIST: nx_int = 2
48const NX_GME_LM_HIP: nx_int = 3
49const NX_GME_LM_HEM: nx_int = 4 // bottom of garment
50const NX_GME_LM_SLEEVE_END: nx_int = 5 // 0 if sleeveless
51const NX_GME_LM_INSEAM_TOP: nx_int = 6 // 0 if not pants
52const NX_GME_LM_COUNT: nx_int = 7
53
54// Fidelity sealed-enum bands
55const NX_GME_FIDELITY_HIGH: nx_int = 0
56const NX_GME_FIDELITY_MODERATE: nx_int = 1
57const NX_GME_FIDELITY_LOW: nx_int = 2
58const NX_GME_FIDELITY_UNUSABLE: nx_int = 3
59const NX_GME_N_FIDELITY_BANDS: nx_int = 4
60
61struct GarmentExtractReport {
62 garment: GarmentDimensions,
63 scale_px_per_mm_q10: nx_int,
64 silhouette_area: nx_int,
65 fidelity_band: nx_int,
66 fidelity_q10: nx_int,
67}
68
69// ===== Helpers (mirrors nx_body_measurement_extract) =================
70
71func _gme_width_at_row(sil: *Image, y: nx_int) -> nx_int {
72 let w: nx_int = sil.width
73 let h: nx_int = sil.height
74 if y < 0 { return 0 }
75 if y >= h { return 0 }
76 var left: nx_int = -1
77 var right: nx_int = -1
78 var x: nx_int = 0
79 while x < w {
80 let v: nx_int = nx_image_get(sil, x, y, 0)
81 if v > 0 {
82 if left < 0 { left = x }
83 right = x
84 }
85 x = x + 1
86 }
87 if left < 0 { return 0 }
88 return right - left + 1
89}
90
91func _gme_silhouette_area(sil: *Image) -> nx_int {
92 let w: nx_int = sil.width
93 let h: nx_int = sil.height
94 var sum: nx_int = 0
95 var y: nx_int = 0
96 while y < h {
97 var x: nx_int = 0
98 while x < w {
99 let v: nx_int = nx_image_get(sil, x, y, 0)
100 if v > 0 { sum = sum + 1 }
101 x = x + 1
102 }
103 y = y + 1
104 }
105 return sum
106}
107
108func _gme_px_to_mm(pixels: nx_int, scale_q10: nx_int) -> nx_int {
109 if scale_q10 <= 0 { return 0 }
110 return (pixels * NX_GME_Q) / scale_q10
111}
112
113// ===== Auto-locate helpers for callers without exact landmark Y ====
114//
115// Narrowest-in-range is the canonical waist locator (mid-torso local
116// minimum); widest-in-range is the canonical hip locator (below-waist
117// local maximum).
118
119func nx_gme_narrowest_width_in_range(sil: *Image,
120 y_lo: nx_int, y_hi: nx_int) -> nx_int {
121 if y_lo >= y_hi { return 0 }
122 var min_w: nx_int = -1
123 var y: nx_int = y_lo
124 while y <= y_hi {
125 let w: nx_int = _gme_width_at_row(sil, y)
126 if w > 0 {
127 if min_w < 0 { min_w = w }
128 if w < min_w { min_w = w }
129 }
130 y = y + 1
131 }
132 if min_w < 0 { return 0 }
133 return min_w
134}
135
136func nx_gme_widest_width_in_range(sil: *Image,
137 y_lo: nx_int, y_hi: nx_int) -> nx_int {
138 if y_lo >= y_hi { return 0 }
139 var max_w: nx_int = 0
140 var y: nx_int = y_lo
141 while y <= y_hi {
142 let w: nx_int = _gme_width_at_row(sil, y)
143 if w > max_w { max_w = w }
144 y = y + 1
145 }
146 return max_w
147}
148
149// Find the leftmost-foreground pixel's column at row y.
150// Used for sleeve_end measurement (horizontal extent of arm from
151// shoulder line out to wrist). Returns -1 if no foreground in row.
152func _gme_left_edge_at_row(sil: *Image, y: nx_int) -> nx_int {
153 let w: nx_int = sil.width
154 let h: nx_int = sil.height
155 if y < 0 { return -1 }
156 if y >= h { return -1 }
157 var x: nx_int = 0
158 while x < w {
159 let v: nx_int = nx_image_get(sil, x, y, 0)
160 if v > 0 { return x }
161 x = x + 1
162 }
163 return -1
164}
165
166func _gme_right_edge_at_row(sil: *Image, y: nx_int) -> nx_int {
167 let w: nx_int = sil.width
168 let h: nx_int = sil.height
169 if y < 0 { return -1 }
170 if y >= h { return -1 }
171 var x: nx_int = w - 1
172 while x >= 0 {
173 let v: nx_int = nx_image_get(sil, x, y, 0)
174 if v > 0 { return x }
175 x = x - 1
176 }
177 return -1
178}
179
180// ===== Public extractor ===============================================
181
182func nx_garment_extract_measurements(
183 sil: *Image,
184 reference_pixels: nx_int,
185 reference_mm: nx_int,
186 landmark_y: *nx_int,
187 cut: nx_int,
188 out: *GarmentExtractReport
189) -> nx_int {
190 if reference_pixels <= 0 { return 1 }
191 if reference_mm <= 0 { return 1 }
192
193 let scale_q10: nx_int = (reference_pixels * NX_GME_Q) / reference_mm
194 out.scale_px_per_mm_q10 = scale_q10
195 if scale_q10 <= 0 { return 1 }
196
197 let top_y: nx_int = landmark_y[NX_GME_LM_TOP]
198 let bust_y: nx_int = landmark_y[NX_GME_LM_BUST]
199 let waist_y: nx_int = landmark_y[NX_GME_LM_WAIST]
200 let hip_y: nx_int = landmark_y[NX_GME_LM_HIP]
201 let hem_y: nx_int = landmark_y[NX_GME_LM_HEM]
202 let sleeve_end_y: nx_int = landmark_y[NX_GME_LM_SLEEVE_END]
203 let inseam_top_y: nx_int = landmark_y[NX_GME_LM_INSEAM_TOP]
204
205 // ---- Cross-section widths ----
206 let top_w: nx_int = _gme_width_at_row(sil, top_y)
207 let bust_w: nx_int = _gme_width_at_row(sil, bust_y)
208 let waist_w: nx_int = _gme_width_at_row(sil, waist_y)
209 let hip_w: nx_int = _gme_width_at_row(sil, hip_y)
210
211 // ---- Pixel widths -> mm ----
212 let shoulder_mm: nx_int = _gme_px_to_mm(top_w, scale_q10)
213 let bust_mm: nx_int = _gme_px_to_mm(bust_w, scale_q10)
214 let waist_mm: nx_int = _gme_px_to_mm(waist_w, scale_q10)
215 let hip_mm: nx_int = _gme_px_to_mm(hip_w, scale_q10)
216
217 // Length: top to hem
218 let length_px: nx_int = hem_y - top_y
219 let length_mm: nx_int = _gme_px_to_mm(length_px, scale_q10)
220
221 // Sleeve length: horizontal extent from shoulder line outward.
222 // If sleeve_end_y is 0 (sleeveless), sleeve_length stays 0.
223 var sleeve_mm: nx_int = 0
224 if sleeve_end_y > 0 {
225 // Sleeve length = vertical drop from top_y to sleeve_end_y
226 // (sleeves typically drop diagonally; this is a chord
227 // approximation -- accurate to ~5% for typical sleeve angles;
228 // future primitive nx_arc_length_along_silhouette gives true
229 // arc length).
230 let sleeve_px: nx_int = sleeve_end_y - top_y
231 if sleeve_px > 0 {
232 sleeve_mm = _gme_px_to_mm(sleeve_px, scale_q10)
233 }
234 }
235
236 // Inseam: inseam_top to hem (pants only).
237 var inseam_mm: nx_int = 0
238 if inseam_top_y > 0 {
239 let inseam_px: nx_int = hem_y - inseam_top_y
240 if inseam_px > 0 {
241 inseam_mm = _gme_px_to_mm(inseam_px, scale_q10)
242 }
243 }
244
245 // ---- Populate GarmentDimensions ----
246 out.garment.bust_mm = bust_mm
247 out.garment.waist_mm = waist_mm
248 out.garment.hip_mm = hip_mm
249 out.garment.length_mm = length_mm
250 out.garment.shoulder_width_mm = shoulder_mm
251 out.garment.sleeve_length_mm = sleeve_mm
252 out.garment.inseam_mm = inseam_mm
253 out.garment.cut = cut
254
255 // ---- Fidelity gate ----
256 out.silhouette_area = _gme_silhouette_area(sil)
257
258 var fid: nx_int = NX_GME_Q
259 if out.silhouette_area < 100 { fid = fid / 2 }
260 if bust_w == 0 { fid = fid / 2 }
261 if waist_w == 0 { fid = fid / 2 }
262 if hip_w == 0 { fid = fid / 2 }
263 if length_px <= 0 { fid = 0 }
264 out.fidelity_q10 = fid
265 out.garment.extracted_fidelity_q10 = fid
266
267 if fid == 0 {
268 out.fidelity_band = NX_GME_FIDELITY_UNUSABLE
269 } else {
270 if fid >= 768 { out.fidelity_band = NX_GME_FIDELITY_HIGH }
271 if fid < 768 { out.fidelity_band = NX_GME_FIDELITY_MODERATE }
272 if fid < 384 { out.fidelity_band = NX_GME_FIDELITY_LOW }
273 }
274
275 return 0
276}
277
278// ===== Sealed-enum validity =========================================
279
280func nx_garment_extract_fidelity_is_valid(b: nx_int) -> nx_int {
281 if b < 0 { return 0 }
282 if b >= NX_GME_N_FIDELITY_BANDS { return 0 }
283 return 1
284}
285
286func nx_garment_extract_landmark_is_valid(l: nx_int) -> nx_int {
287 if l < 0 { return 0 }
288 if l >= NX_GME_LM_COUNT { return 0 }
289 return 1
290}