code wiki / _hdl_build / nx_garment_lib.nx
nx_garment_lib.nx source
↩ module page · 587 lines · 24847 B
1// nx_garment_lib.nx -- SPATIALLY-COHERENT GARMENT MASK: replaces the rectangle that left a visible seam.
2//
3// THE MEASURED PROBLEM. A hue-only mask for a yellow dress also caught the model's FACE, because skin
4// and warm fabric are genuinely close in hue -- so no hue tolerance can separate them without either
5// losing the dress or taking the face. A rectangle "fixed" it only by brute force, and left a seam.
6//
7// THE PRINCIPLED SEPARATOR IS SPATIAL COHERENCE, NOT A TIGHTER THRESHOLD. A garment is ONE LARGE
8// CONTIGUOUS REGION. Skin is a different region that merely resembles it in colour. So: build the
9// colour-candidate set, label 8-connected components, and keep the LARGEST -- the dress survives, the
10// face falls away as a separate component, and the boundary follows the actual fabric edge instead of
11// an invented rectangle. No tuned constant decides which is which; the geometry does.
12//
13// Composes nx_img_segment's proven flood-fill labeller (ink=0/paper=255 convention) -- zero new
14// component-labelling maths -- and nx_editjudge_lib's hue/sat primitives, so ONE definition of "this
15// pixel is that colour" governs the candidate set here and the judging elsewhere.
16// license_tier: ORIGINAL
17import "nx_editjudge_lib.nx"
18import "nx_img_segment.nx"
19import "nx_color_v2.nx"
20
21const GM_MAXCOMPS: i64 = 20000 // generous: a noisy candidate set fragments; seg_components bails past this
22
23// ⚠MEASURED, AND IT REFUTED THE FIRST DESIGN: 8-connected labelling alone does NOT separate a garment
24// from skin, because they TOUCH at the neck and chest -- the flood fill walks straight across the
25// boundary and returns one blob (largest component 475336 of 495207 candidates; the face stayed green).
26// Spatial coherence cannot separate regions that are physically adjacent. The discriminator that DOES
27// survive contact is SATURATION: dyed fabric is far more saturated than skin at the same hue. So the
28// saturation floor is a PARAMETER here, swept and chosen by measurement, never a constant tuned by eye.
29func gm_candidate_mask(rgb: *u8, npix: i64, hue: i64, sat_min: i64, out_bin: *u8) -> i64 {
30 var n: i64 = 0
31 var i: i64 = 0
32 while i < npix {
33 let r: i64 = rgb[i*3] as i64
34 let g: i64 = rgb[i*3+1] as i64
35 let b: i64 = rgb[i*3+2] as i64
36 out_bin[i] = (255 as u8)
37 if ej_sat(r, g, b) >= sat_min {
38 if ej_hue_dist(ej_hue(r, g, b), hue) <= EJ_HUE_TOL {
39 out_bin[i] = (0 as u8)
40 n = n + 1
41 }
42 }
43 i = i + 1
44 }
45 return n
46}
47
48// ★THE DISCRIMINATOR THAT ACTUALLY SEPARATES THEM (F1092 rung 3).
49// Two attempts failed for the same reason: they tried to define the GARMENT, and skin and warm fabric
50// are genuinely inseparable in hue. Raising the saturation floor spared the face but also threw away
51// the garment's own shadowed folds (compliance fell to 602); connected components merged them because
52// they physically TOUCH at the neck.
53// ★INVERT THE PROBLEM: do not define the garment -- define SKIN, which is far better characterised than
54// "fabric" and is stable across people. Kovac's published classifier (Y>80, 85<Cb<135, 135<Cr<180) does
55// it in YCbCr CHROMA space -- a DIFFERENT space from the hue that failed -- so it separates precisely
56// where a hue threshold cannot. Garment = the right hue, saturated enough, AND NOT SKIN.
57// This composes nx_color_v2's existing classifier; no skin-detection maths is written here.
58func gm_candidate_mask_noskin(rgb: *u8, npix: i64, hue: i64, sat_min: i64, out_bin: *u8) -> i64 {
59 var n: i64 = 0
60 var i: i64 = 0
61 while i < npix {
62 let r: i64 = rgb[i*3] as i64
63 let g: i64 = rgb[i*3+1] as i64
64 let b: i64 = rgb[i*3+2] as i64
65 out_bin[i] = (255 as u8)
66 if nx_color_kovac_skin_pixel(r, g, b) == 0 {
67 if ej_sat(r, g, b) >= sat_min {
68 if ej_hue_dist(ej_hue(r, g, b), hue) <= EJ_HUE_TOL {
69 out_bin[i] = (0 as u8)
70 n = n + 1
71 }
72 }
73 }
74 i = i + 1
75 }
76 return n
77}
78
79// ★MULTI-MODAL MASK (F1141) -- the last thing standing between this and patterned fabric.
80// A single hue anchor stops describing the object the moment the garment has more than one tone: the
81// second tone is not flattened, it is NEVER SELECTED, which is why changing the paint operation alone
82// measured as a no-op. The fix has to be in the SELECTION.
83//
84// THE METHOD, and why it is two steps rather than one:
85// 1. the hue-anchored skin-aware core answers WHERE the garment is (a spatial region we trust);
86// 2. a hue HISTOGRAM over that region answers WHAT COLOURS it actually contains.
87//
88// ⚠⚠MEASURED AND CURRENTLY A REGRESSION -- EXPERIMENTAL, NOT THE DEFAULT. On the solid reference this
89// grades 371 against the skin-aware champion's 620: compliance 1000 with footprint 629, which is the
90// exact signature of "recoloured the face too". ROOT CAUSE: step 2 histograms the CORE'S BOUNDING BOX,
91// and that rectangle spans background as well as garment, so BACKGROUND hues are promoted to garment
92// "tones" and the mask then admits half the frame.
93// ★THE REAL TENSION, stated so the next attempt does not rediscover it: modes taken from the CORE ONLY
94// are safe but cannot discover a second tone (the second tone is precisely what the hue anchor
95// excluded); modes taken from the BOUNDING BOX discover it but drag in the background. The fix is
96// neither -- it is a DILATION of the core (pixels spatially adjacent to trusted garment), which admits
97// an adjacent second tone without admitting distant background. Not yet built.
98// Kept opt-in behind the `multi` verb so the champion path is untouched; no capability claim is made.
99const GM_HUE_BINS: i64 = 36 // 10 degrees per bin: coarse enough to be robust to dithering noise
100const GM_MODE_MIN_PM: i64 = 60 // a bin must hold >=6% of the region to count as a tone, not noise
101const GM_MAX_MODES: i64 = 6
102
103// ★F1143 -- DILATION, the fix for the bounding-box defect.
104// A bounding box is a SUPERSET containing everything the region deliberately excluded, which is why
105// histogramming it promoted background hues to garment "tones" (grade 371). A DILATION is the opposite:
106// it grows the trusted region by a bounded distance, so it can reach a tone that is ADJACENT to known
107// garment while never reaching background that is far away. One morphological step = one pixel of
108// reach, so the radius is an explicit, sweepable budget rather than an implicit rectangle.
109func gm_dilate(mask: *u8, w: i64, h: i64, iters: i64, out: *u8) -> i64 {
110 let npix: i64 = w * h
111 var i: i64 = 0
112 while i < npix { out[i] = mask[i]; i = i + 1 }
113 let tmp: *u8 = sys_mmap(npix)
114 var it: i64 = 0
115 while it < iters {
116 i = 0
117 while i < npix { tmp[i] = out[i]; i = i + 1 }
118 var y: i64 = 0
119 while y < h {
120 var x: i64 = 0
121 while x < w {
122 if tmp[y*w + x] == (1 as u8) {
123 var dy: i64 = 0 - 1
124 while dy <= 1 {
125 var dx: i64 = 0 - 1
126 while dx <= 1 {
127 let nx2: i64 = x + dx
128 let ny2: i64 = y + dy
129 if nx2 >= 0 { if nx2 < w { if ny2 >= 0 { if ny2 < h {
130 out[ny2*w + nx2] = (1 as u8)
131 } } } }
132 dx = dx + 1
133 }
134 dy = dy + 1
135 }
136 }
137 x = x + 1
138 }
139 y = y + 1
140 }
141 it = it + 1
142 }
143 var n: i64 = 0
144 i = 0
145 while i < npix { if out[i] == (1 as u8) { n = n + 1 } i = i + 1 }
146 return n
147}
148
149// Histogram hues over an explicit MASK (not a box). This is the corrected step 2.
150func gm_hue_modes_mask(rgb: *u8, npix: i64, region: *u8, sat_min: i64, out_modes: *i64) -> i64 {
151 let hist: *i64 = sys_mmap(GM_HUE_BINS * 8) as *i64
152 var b: i64 = 0
153 while b < GM_HUE_BINS { hist[b] = 0; b = b + 1 }
154 var total: i64 = 0
155 var i: i64 = 0
156 while i < npix {
157 if region[i] == (1 as u8) {
158 let r: i64 = rgb[i*3] as i64
159 let g: i64 = rgb[i*3+1] as i64
160 let b2: i64 = rgb[i*3+2] as i64
161 if nx_color_kovac_skin_pixel(r, g, b2) == 0 {
162 if ej_sat(r, g, b2) >= sat_min {
163 let hu: i64 = ej_hue(r, g, b2)
164 if hu >= 0 { hist[(hu * GM_HUE_BINS) / 360] = hist[(hu * GM_HUE_BINS) / 360] + 1; total = total + 1 }
165 }
166 }
167 }
168 i = i + 1
169 }
170 var nm: i64 = 0
171 if total <= 0 { return 0 }
172 b = 0
173 while b < GM_HUE_BINS {
174 if nm < GM_MAX_MODES {
175 if (hist[b] * 1000) / total >= GM_MODE_MIN_PM {
176 out_modes[nm] = (b * 360) / GM_HUE_BINS + (180 / GM_HUE_BINS)
177 nm = nm + 1
178 }
179 }
180 b = b + 1
181 }
182 return nm
183}
184
185// Corrected multi-modal pipeline: core -> DILATE by `reach` -> hue modes over the DILATED MASK ->
186// accept any mode INSIDE the dilated region -> largest connected component.
187func gm_segment_dilate(rgb: *u8, w: i64, h: i64, hue: i64, sat_min: i64, reach: i64, out_mask: *u8,
188 out_nmodes: *i64, out_kept_px: *i64, out_modes: *i64) -> i64 {
189 let npix: i64 = w * h
190 let core: *u8 = sys_mmap(npix)
191 let nc: *i64 = sys_mmap(8)
192 let kp: *i64 = sys_mmap(8)
193 if gm_segment_noskin(rgb, w, h, hue, sat_min, core, nc, kp) == 0 { out_nmodes[0] = 0; out_kept_px[0] = 0; return 0 }
194 let region: *u8 = sys_mmap(npix)
195 gm_dilate(core, w, h, reach, region)
196 let nm: i64 = gm_hue_modes_mask(rgb, npix, region, sat_min, out_modes)
197 out_nmodes[0] = nm
198 if nm == 0 { out_kept_px[0] = 0; return 0 }
199 let bin2: *u8 = sys_mmap(npix)
200 var i: i64 = 0
201 while i < npix {
202 bin2[i] = (255 as u8)
203 if region[i] == (1 as u8) {
204 let r: i64 = rgb[i*3] as i64
205 let g: i64 = rgb[i*3+1] as i64
206 let b3: i64 = rgb[i*3+2] as i64
207 if nx_color_kovac_skin_pixel(r, g, b3) == 0 {
208 if ej_sat(r, g, b3) >= sat_min {
209 if gm_near_any_mode(ej_hue(r, g, b3), out_modes, nm) == 1 { bin2[i] = (0 as u8) }
210 }
211 }
212 }
213 i = i + 1
214 }
215 let labels: *i64 = sys_mmap(npix * 8) as *i64
216 let boxes: *i64 = sys_mmap(GM_MAXCOMPS * 4 * 8) as *i64
217 let ncomp: i64 = seg_components(bin2, w, h, labels, boxes, GM_MAXCOMPS)
218 let keep: i64 = gm_largest_id(labels, npix, ncomp)
219 var kept: i64 = 0
220 i = 0
221 while i < npix {
222 out_mask[i] = (0 as u8)
223 if keep > 0 { if labels[i] == keep { out_mask[i] = (1 as u8); kept = kept + 1 } }
224 i = i + 1
225 }
226 out_kept_px[0] = kept
227 return keep
228}
229
230// Bounding box of the seed mask (the region we trust to be garment). Returns pixel count.
231func gm_mask_bbox(mask: *u8, w: i64, h: i64, out4: *i64) -> i64 {
232 var minx: i64 = w
233 var miny: i64 = h
234 var maxx: i64 = 0 - 1
235 var maxy: i64 = 0 - 1
236 var n: i64 = 0
237 var i: i64 = 0
238 while i < w*h {
239 if mask[i] == (1 as u8) {
240 let x: i64 = i % w
241 let y: i64 = i / w
242 if x < minx { minx = x }
243 if y < miny { miny = y }
244 if x > maxx { maxx = x }
245 if y > maxy { maxy = y }
246 n = n + 1
247 }
248 i = i + 1
249 }
250 out4[0] = minx
251 out4[1] = miny
252 out4[2] = maxx
253 out4[3] = maxy
254 return n
255}
256
257// Histogram the hues of non-skin saturated pixels inside the box, and emit every bin that clears the
258// share threshold as a mode (bin centre, degrees). Returns the mode count.
259func gm_hue_modes(rgb: *u8, w: i64, h: i64, bx: *i64, sat_min: i64, out_modes: *i64) -> i64 {
260 let hist: *i64 = sys_mmap(GM_HUE_BINS * 8) as *i64
261 var b: i64 = 0
262 while b < GM_HUE_BINS { hist[b] = 0; b = b + 1 }
263 var total: i64 = 0
264 var y: i64 = bx[1]
265 while y <= bx[3] {
266 var x: i64 = bx[0]
267 while x <= bx[2] {
268 let i: i64 = y*w + x
269 let r: i64 = rgb[i*3] as i64
270 let g: i64 = rgb[i*3+1] as i64
271 let bb2: i64 = rgb[i*3+2] as i64
272 if nx_color_kovac_skin_pixel(r, g, bb2) == 0 {
273 if ej_sat(r, g, bb2) >= sat_min {
274 let hu: i64 = ej_hue(r, g, bb2)
275 if hu >= 0 {
276 let bin: i64 = (hu * GM_HUE_BINS) / 360
277 hist[bin] = hist[bin] + 1
278 total = total + 1
279 }
280 }
281 }
282 x = x + 1
283 }
284 y = y + 1
285 }
286 var nm: i64 = 0
287 if total <= 0 { return 0 }
288 b = 0
289 while b < GM_HUE_BINS {
290 if nm < GM_MAX_MODES {
291 if (hist[b] * 1000) / total >= GM_MODE_MIN_PM {
292 out_modes[nm] = (b * 360) / GM_HUE_BINS + (180 / GM_HUE_BINS)
293 nm = nm + 1
294 }
295 }
296 b = b + 1
297 }
298 return nm
299}
300
301func gm_near_any_mode(hue: i64, modes: *i64, nm: i64) -> i64 {
302 var i: i64 = 0
303 while i < nm {
304 if ej_hue_dist(hue, modes[i]) <= EJ_HUE_TOL { return 1 }
305 i = i + 1
306 }
307 return 0
308}
309
310// Full multi-modal pipeline: skin-aware core -> its box -> hue modes in that box -> accept ANY mode
311// (still non-skin, still saturated, still inside the region) -> keep the largest connected component.
312func gm_segment_multimodal(rgb: *u8, w: i64, h: i64, hue: i64, sat_min: i64, out_mask: *u8,
313 out_nmodes: *i64, out_kept_px: *i64, out_modes: *i64) -> i64 {
314 let npix: i64 = w * h
315 let core: *u8 = sys_mmap(npix)
316 let nc: *i64 = sys_mmap(8)
317 let kp: *i64 = sys_mmap(8)
318 if gm_segment_noskin(rgb, w, h, hue, sat_min, core, nc, kp) == 0 { out_nmodes[0] = 0; out_kept_px[0] = 0; return 0 }
319 let bx: *i64 = sys_mmap(32) as *i64
320 if gm_mask_bbox(core, w, h, bx) == 0 { out_nmodes[0] = 0; out_kept_px[0] = 0; return 0 }
321 let nm: i64 = gm_hue_modes(rgb, w, h, bx, sat_min, out_modes)
322 out_nmodes[0] = nm
323 if nm == 0 { out_kept_px[0] = 0; return 0 }
324 // rebuild the candidate set accepting ANY discovered tone, confined to the trusted region
325 let bin2: *u8 = sys_mmap(npix)
326 var i: i64 = 0
327 while i < npix { bin2[i] = (255 as u8); i = i + 1 }
328 var y: i64 = bx[1]
329 while y <= bx[3] {
330 var x: i64 = bx[0]
331 while x <= bx[2] {
332 let p: i64 = y*w + x
333 let r: i64 = rgb[p*3] as i64
334 let g: i64 = rgb[p*3+1] as i64
335 let b3: i64 = rgb[p*3+2] as i64
336 if nx_color_kovac_skin_pixel(r, g, b3) == 0 {
337 if ej_sat(r, g, b3) >= sat_min {
338 if gm_near_any_mode(ej_hue(r, g, b3), out_modes, nm) == 1 { bin2[p] = (0 as u8) }
339 }
340 }
341 x = x + 1
342 }
343 y = y + 1
344 }
345 let labels: *i64 = sys_mmap(npix * 8) as *i64
346 let boxes: *i64 = sys_mmap(GM_MAXCOMPS * 4 * 8) as *i64
347 let ncomp: i64 = seg_components(bin2, w, h, labels, boxes, GM_MAXCOMPS)
348 let keep: i64 = gm_largest_id(labels, npix, ncomp)
349 var kept: i64 = 0
350 i = 0
351 while i < npix {
352 out_mask[i] = (0 as u8)
353 if keep > 0 { if labels[i] == keep { out_mask[i] = (1 as u8); kept = kept + 1 } }
354 i = i + 1
355 }
356 out_kept_px[0] = kept
357 return keep
358}
359
360// Skin-aware segmentation: candidate set excludes skin, then keep the largest connected region so the
361// boundary still follows real fabric rather than a rectangle.
362func gm_segment_noskin(rgb: *u8, w: i64, h: i64, hue: i64, sat_min: i64, out_mask: *u8,
363 out_ncomp: *i64, out_kept_px: *i64) -> i64 {
364 let npix: i64 = w * h
365 let bin: *u8 = sys_mmap(npix)
366 gm_candidate_mask_noskin(rgb, npix, hue, sat_min, bin)
367 let labels: *i64 = sys_mmap(npix * 8) as *i64
368 let boxes: *i64 = sys_mmap(GM_MAXCOMPS * 4 * 8) as *i64
369 let ncomp: i64 = seg_components(bin, w, h, labels, boxes, GM_MAXCOMPS)
370 out_ncomp[0] = ncomp
371 let keep: i64 = gm_largest_id(labels, npix, ncomp)
372 var kept: i64 = 0
373 var i: i64 = 0
374 while i < npix {
375 out_mask[i] = (0 as u8)
376 if keep > 0 { if labels[i] == keep { out_mask[i] = (1 as u8); kept = kept + 1 } }
377 i = i + 1
378 }
379 out_kept_px[0] = kept
380 return keep
381}
382
383// Which component id has the most pixels? (The garment is the largest contiguous colour region.)
384// Returns 0 if there are none.
385func gm_largest_id(labels: *i64, npix: i64, ncomp: i64) -> i64 {
386 if ncomp <= 0 { return 0 }
387 let counts: *i64 = sys_mmap((ncomp + 1) * 8) as *i64
388 var i: i64 = 0
389 while i <= ncomp { counts[i] = 0; i = i + 1 }
390 i = 0
391 while i < npix {
392 let l: i64 = labels[i]
393 if l > 0 { if l <= ncomp { counts[l] = counts[l] + 1 } }
394 i = i + 1
395 }
396 var best: i64 = 0
397 var bestn: i64 = 0
398 var c: i64 = 1
399 while c <= ncomp {
400 if counts[c] > bestn { bestn = counts[c]; best = c }
401 c = c + 1
402 }
403 return best
404}
405
406// ---- SEEDED REGION GROWING AGAINST A LEARNED MODEL -------------------------------------------
407// WHY THIS EXISTS. The threshold mask above works only for a SOLID, SATURATED garment: a high floor
408// that excludes skin also excludes the garment's own SHADOWED FOLDS (measured: sat 140 spared the face
409// but recoloured only 602 permil of the dress). A patterned or dark garment defeats it outright, because
410// no single hue+floor describes the object.
411//
412// THE FIX IS TO STOP GUESSING A THRESHOLD AND LEARN ONE FROM THE IMAGE. Take a CONFIDENT CORE (the
413// largest strongly-saturated region -- pixels we are sure are fabric), measure that core's ACTUAL colour
414// spread, then grow outward through 8-connected neighbours that fit the core's own model. Two
415// constraints hold it in: ADJACENCY (it can only reach pixels touching the region) and the LEARNED MODEL
416// (skin sits far outside the fabric's saturation distribution, so growth stops at the neck instead of
417// flooding the face). Tolerances are DERIVED from the core's measured spread, not typed in by hand.
418const GM_SEED_SAT: i64 = 170 // saturation floor for the CONFIDENT CORE only (deliberately strict:
419 // this seeds the model, it does not define the final mask)
420const GM_SPREAD_MIN: i64 = 24 // floor on a derived tolerance so a perfectly-flat core can still grow
421const GM_SPREAD_MUL: i64 = 3 // half-widths are this multiple of the core's mean absolute deviation
422
423// Measure the core's colour model: mean hue / sat / val and the mean-absolute-deviation of each.
424func gm_model(rgb: *u8, npix: i64, mask: *u8, out6: *i64) -> i64 {
425 var n: i64 = 0
426 var sh: i64 = 0
427 var ss: i64 = 0
428 var sv: i64 = 0
429 var i: i64 = 0
430 while i < npix {
431 if mask[i] == (1 as u8) {
432 let r: i64 = rgb[i*3] as i64
433 let g: i64 = rgb[i*3+1] as i64
434 let b: i64 = rgb[i*3+2] as i64
435 sh = sh + ej_hue(r, g, b)
436 ss = ss + ej_sat(r, g, b)
437 sv = sv + ej_max3(r, g, b)
438 n = n + 1
439 }
440 i = i + 1
441 }
442 if n == 0 { return 0 }
443 let mh: i64 = sh / n
444 let ms: i64 = ss / n
445 let mv: i64 = sv / n
446 // mean absolute deviation per channel = the object's own measured spread
447 var dh: i64 = 0
448 var ds: i64 = 0
449 var dv: i64 = 0
450 i = 0
451 while i < npix {
452 if mask[i] == (1 as u8) {
453 let r: i64 = rgb[i*3] as i64
454 let g: i64 = rgb[i*3+1] as i64
455 let b: i64 = rgb[i*3+2] as i64
456 dh = dh + ej_hue_dist(ej_hue(r, g, b), mh)
457 var t: i64 = ej_sat(r, g, b) - ms
458 if t < 0 { t = 0 - t }
459 ds = ds + t
460 t = ej_max3(r, g, b) - mv
461 if t < 0 { t = 0 - t }
462 dv = dv + t
463 n = n
464 }
465 i = i + 1
466 }
467 out6[0] = mh
468 out6[1] = ms
469 out6[2] = mv
470 var th: i64 = (dh / n) * GM_SPREAD_MUL
471 var ts: i64 = (ds / n) * GM_SPREAD_MUL
472 var tv: i64 = (dv / n) * GM_SPREAD_MUL
473 if th < GM_SPREAD_MIN { th = GM_SPREAD_MIN }
474 if ts < GM_SPREAD_MIN { ts = GM_SPREAD_MIN }
475 if tv < GM_SPREAD_MIN { tv = GM_SPREAD_MIN }
476 out6[3] = th
477 out6[4] = ts
478 out6[5] = tv
479 return n
480}
481
482// ⚠MEASURED: a SYMMETRIC tolerance grows NOTHING. The confident core is high-saturation by
483// construction, so it learns a narrow, high-saturation model that cannot reach its own shadowed folds
484// (core 176,360 px, grown 0). The bootstrap is self-limiting.
485// ★THE FIX IS PHYSICAL, NOT A TUNED NUMBER: shading scales illumination, which drives VALUE and
486// SATURATION DOWN while leaving HUE essentially unchanged. So the acceptance region is ASYMMETRIC --
487// tight on hue (identity of the colour), tight upward on sat/val (nothing should be BRIGHTER than the
488// lit core), and generous downward (that direction is shadow, which is still the same fabric).
489const GM_SHADOW_MUL: i64 = 4 // how much further the model may reach in the shadow direction only
490
491func gm_fits(rgb: *u8, i: i64, m: *i64) -> i64 {
492 let r: i64 = rgb[i*3] as i64
493 let g: i64 = rgb[i*3+1] as i64
494 let b: i64 = rgb[i*3+2] as i64
495 // hue: the colour's identity -- symmetric and tight
496 if ej_hue_dist(ej_hue(r, g, b), m[0]) > m[3] { return 0 }
497 // saturation: allow far DOWN (shadow), little UP
498 let ds: i64 = ej_sat(r, g, b) - m[1]
499 if ds > m[4] { return 0 }
500 if (0 - ds) > m[4] * GM_SHADOW_MUL { return 0 }
501 // value: same asymmetry
502 let dv: i64 = ej_max3(r, g, b) - m[2]
503 if dv > m[5] { return 0 }
504 if (0 - dv) > m[5] * GM_SHADOW_MUL { return 0 }
505 return 1
506}
507
508// Grow the seed mask through 8-connected neighbours that fit the model. Explicit stack, no recursion.
509func gm_grow(rgb: *u8, w: i64, h: i64, mask: *u8, m: *i64) -> i64 {
510 let npix: i64 = w * h
511 let stack: *i64 = sys_mmap((npix + 1) * 8) as *i64
512 var sp: i64 = 0
513 var i: i64 = 0
514 while i < npix { if mask[i] == (1 as u8) { stack[sp] = i; sp = sp + 1 } i = i + 1 }
515 var grown: i64 = 0
516 while sp > 0 {
517 sp = sp - 1
518 let cur: i64 = stack[sp]
519 let cx: i64 = cur % w
520 let cy: i64 = cur / w
521 var dy: i64 = 0 - 1
522 while dy <= 1 {
523 var dx: i64 = 0 - 1
524 while dx <= 1 {
525 let nxp: i64 = cx + dx
526 let nyp: i64 = cy + dy
527 if nxp >= 0 { if nxp < w { if nyp >= 0 { if nyp < h {
528 let np: i64 = nyp * w + nxp
529 if mask[np] == (0 as u8) {
530 if gm_fits(rgb, np, m) == 1 {
531 mask[np] = (1 as u8)
532 stack[sp] = np
533 sp = sp + 1
534 grown = grown + 1
535 }
536 }
537 } } } }
538 dx = dx + 1
539 }
540 dy = dy + 1
541 }
542 }
543 return grown
544}
545
546// Full learned pipeline: confident core -> model -> grow. Writes core px and grown px.
547// Returns total mask pixels (0 = no confident core found).
548func gm_segment_grow(rgb: *u8, w: i64, h: i64, hue: i64, out_mask: *u8,
549 out_core: *i64, out_grown: *i64, out_model: *i64) -> i64 {
550 let npix: i64 = w * h
551 let nc: *i64 = sys_mmap(8)
552 let kp: *i64 = sys_mmap(8)
553 // strict core: the pixels we are SURE are fabric
554 let keep: i64 = gm_segment_sat(rgb, w, h, hue, GM_SEED_SAT, out_mask, nc, kp)
555 out_core[0] = kp[0]
556 out_grown[0] = 0
557 if keep == 0 { return 0 }
558 if gm_model(rgb, npix, out_mask, out_model) == 0 { return 0 }
559 out_grown[0] = gm_grow(rgb, w, h, out_mask, out_model)
560 var total: i64 = 0
561 var i: i64 = 0
562 while i < npix { if out_mask[i] == (1 as u8) { total = total + 1 } i = i + 1 }
563 return total
564}
565
566// Full pipeline: rgb -> garment mask (1 = in garment, 0 = not). Writes the component count and the
567// kept component's pixel count. Returns the kept component id (0 = nothing found, mask all zero).
568func gm_segment_sat(rgb: *u8, w: i64, h: i64, hue: i64, sat_min: i64, out_mask: *u8,
569 out_ncomp: *i64, out_kept_px: *i64) -> i64 {
570 let npix: i64 = w * h
571 let bin: *u8 = sys_mmap(npix)
572 gm_candidate_mask(rgb, npix, hue, sat_min, bin)
573 let labels: *i64 = sys_mmap(npix * 8) as *i64
574 let boxes: *i64 = sys_mmap(GM_MAXCOMPS * 4 * 8) as *i64
575 let ncomp: i64 = seg_components(bin, w, h, labels, boxes, GM_MAXCOMPS)
576 out_ncomp[0] = ncomp
577 let keep: i64 = gm_largest_id(labels, npix, ncomp)
578 var kept: i64 = 0
579 var i: i64 = 0
580 while i < npix {
581 out_mask[i] = (0 as u8)
582 if keep > 0 { if labels[i] == keep { out_mask[i] = (1 as u8); kept = kept + 1 } }
583 i = i + 1
584 }
585 out_kept_px[0] = kept
586 return keep
587}