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}