react-native-mask-segment-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +904 -0
  2. package/dist/components/MaskSegmentCanvas.d.ts +6 -0
  3. package/dist/components/MaskSegmentCanvas.d.ts.map +1 -0
  4. package/dist/components/MaskSegmentCanvas.js +2012 -0
  5. package/dist/components/MaskSegmentCanvas.js.map +1 -0
  6. package/dist/components/MaskSegmentCanvas.types.d.ts +189 -0
  7. package/dist/components/MaskSegmentCanvas.types.d.ts.map +1 -0
  8. package/dist/components/MaskSegmentCanvas.types.js +2 -0
  9. package/dist/components/MaskSegmentCanvas.types.js.map +1 -0
  10. package/dist/index.d.ts +6 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +5 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/shaders/regionPaint.sksl.d.ts +3 -0
  15. package/dist/shaders/regionPaint.sksl.d.ts.map +1 -0
  16. package/dist/shaders/regionPaint.sksl.js +72 -0
  17. package/dist/shaders/regionPaint.sksl.js.map +1 -0
  18. package/dist/utils/compositePaintedImage.d.ts +44 -0
  19. package/dist/utils/compositePaintedImage.d.ts.map +1 -0
  20. package/dist/utils/compositePaintedImage.js +146 -0
  21. package/dist/utils/compositePaintedImage.js.map +1 -0
  22. package/dist/utils/exportUtils.d.ts +20 -0
  23. package/dist/utils/exportUtils.d.ts.map +1 -0
  24. package/dist/utils/exportUtils.js +32 -0
  25. package/dist/utils/exportUtils.js.map +1 -0
  26. package/dist/utils/freqLayerPrep.d.ts +23 -0
  27. package/dist/utils/freqLayerPrep.d.ts.map +1 -0
  28. package/dist/utils/freqLayerPrep.js +168 -0
  29. package/dist/utils/freqLayerPrep.js.map +1 -0
  30. package/dist/utils/maskSegmentRuntime.d.ts +43 -0
  31. package/dist/utils/maskSegmentRuntime.d.ts.map +1 -0
  32. package/dist/utils/maskSegmentRuntime.js +181 -0
  33. package/dist/utils/maskSegmentRuntime.js.map +1 -0
  34. package/dist/utils/maskSegmentation.d.ts +133 -0
  35. package/dist/utils/maskSegmentation.d.ts.map +1 -0
  36. package/dist/utils/maskSegmentation.js +1600 -0
  37. package/dist/utils/maskSegmentation.js.map +1 -0
  38. package/dist/utils/maskSemanticPalette.d.ts +31 -0
  39. package/dist/utils/maskSemanticPalette.d.ts.map +1 -0
  40. package/dist/utils/maskSemanticPalette.js +125 -0
  41. package/dist/utils/maskSemanticPalette.js.map +1 -0
  42. package/dist/utils/opencvAdapter.d.ts +116 -0
  43. package/dist/utils/opencvAdapter.d.ts.map +1 -0
  44. package/dist/utils/opencvAdapter.js +353 -0
  45. package/dist/utils/opencvAdapter.js.map +1 -0
  46. package/dist/utils/paintColorMapTexture.d.ts +5 -0
  47. package/dist/utils/paintColorMapTexture.d.ts.map +1 -0
  48. package/dist/utils/paintColorMapTexture.js +203 -0
  49. package/dist/utils/paintColorMapTexture.js.map +1 -0
  50. package/dist/utils/paintShaderRuntime.d.ts +40 -0
  51. package/dist/utils/paintShaderRuntime.d.ts.map +1 -0
  52. package/dist/utils/paintShaderRuntime.js +76 -0
  53. package/dist/utils/paintShaderRuntime.js.map +1 -0
  54. package/dist/utils/pickMapTexture.d.ts +4 -0
  55. package/dist/utils/pickMapTexture.d.ts.map +1 -0
  56. package/dist/utils/pickMapTexture.js +24 -0
  57. package/dist/utils/pickMapTexture.js.map +1 -0
  58. package/dist/utils/pngImage.d.ts +49 -0
  59. package/dist/utils/pngImage.d.ts.map +1 -0
  60. package/dist/utils/pngImage.js +438 -0
  61. package/dist/utils/pngImage.js.map +1 -0
  62. package/dist/utils/resolveAssetPath.d.ts +3 -0
  63. package/dist/utils/resolveAssetPath.d.ts.map +1 -0
  64. package/dist/utils/resolveAssetPath.js +56 -0
  65. package/dist/utils/resolveAssetPath.js.map +1 -0
  66. package/dist/utils/resolveImageUrl.d.ts +3 -0
  67. package/dist/utils/resolveImageUrl.d.ts.map +1 -0
  68. package/dist/utils/resolveImageUrl.js +51 -0
  69. package/dist/utils/resolveImageUrl.js.map +1 -0
  70. package/dist/utils/skiaImage.d.ts +4 -0
  71. package/dist/utils/skiaImage.d.ts.map +1 -0
  72. package/dist/utils/skiaImage.js +12 -0
  73. package/dist/utils/skiaImage.js.map +1 -0
  74. package/package.json +100 -0
  75. package/patches/react-native-fast-opencv+0.4.8.patch +122 -0
  76. package/src/components/MaskSegmentCanvas.tsx +2832 -0
  77. package/src/components/MaskSegmentCanvas.types.ts +216 -0
  78. package/src/globals.d.ts +19 -0
  79. package/src/index.ts +45 -0
  80. package/src/shaders/regionPaint.sksl.ts +71 -0
  81. package/src/upng-js.d.ts +33 -0
  82. package/src/utils/compositePaintedImage.ts +201 -0
  83. package/src/utils/exportUtils.ts +40 -0
  84. package/src/utils/freqLayerPrep.ts +267 -0
  85. package/src/utils/maskSegmentRuntime.ts +257 -0
  86. package/src/utils/maskSegmentation.ts +2294 -0
  87. package/src/utils/maskSemanticPalette.ts +187 -0
  88. package/src/utils/opencvAdapter.ts +539 -0
  89. package/src/utils/paintColorMapTexture.ts +239 -0
  90. package/src/utils/paintShaderRuntime.tsx +150 -0
  91. package/src/utils/pickMapTexture.ts +37 -0
  92. package/src/utils/pngImage.ts +591 -0
  93. package/src/utils/resolveAssetPath.ts +64 -0
  94. package/src/utils/resolveImageUrl.ts +63 -0
  95. package/src/utils/skiaImage.ts +25 -0
@@ -0,0 +1,1600 @@
1
+ import cv from './opencvAdapter';
2
+ import { Skia } from '@shopify/react-native-skia';
3
+ import { BASEBOARD_SEMANTIC_NAME, classifyBgrPixelToSemantic, getCabinetQuantKeys, getSemanticColorByName, getWallQuantKeys, getBaseboardStripQuantKeys, isStrictBaseboardPixel, } from './maskSemanticPalette';
4
+ import { getMaskRuntimeRevision, getMaskSegmentRuntimeConfig } from './maskSegmentRuntime';
5
+ function maskCfg() {
6
+ return getMaskSegmentRuntimeConfig().mask;
7
+ }
8
+ const MORPH_KERNEL_SIZE = 5;
9
+ const MAX_DASH_OUTLINE_POLYGONS = 10;
10
+ function bboxToPolygon(bbox) {
11
+ return [
12
+ { x: bbox.x, y: bbox.y },
13
+ { x: bbox.x + bbox.w, y: bbox.y },
14
+ { x: bbox.x + bbox.w, y: bbox.y + bbox.h },
15
+ { x: bbox.x, y: bbox.y + bbox.h },
16
+ ];
17
+ }
18
+ export function buildRegionOutlinePolygons(reg) {
19
+ if (reg.outlinePolygons && reg.outlinePolygons.length > 0) {
20
+ return reg.outlinePolygons;
21
+ }
22
+ if (reg.thinStrip || reg.polygons.length <= MAX_DASH_OUTLINE_POLYGONS) {
23
+ return reg.polygons;
24
+ }
25
+ return [bboxToPolygon(reg.bbox)];
26
+ }
27
+ function isMaskPixelOn(binary, cols, rows, x, y) {
28
+ return (x >= 0 && x < cols && y >= 0 && y < rows && binary[y * cols + x] > 0);
29
+ }
30
+ function collectBoundaryEdges(binary, cols, rows) {
31
+ const edges = [];
32
+ for (let y = 0; y < rows; y++) {
33
+ const row = y * cols;
34
+ for (let x = 0; x < cols; x++) {
35
+ if (!binary[row + x]) {
36
+ continue;
37
+ }
38
+ if (!isMaskPixelOn(binary, cols, rows, x, y - 1)) {
39
+ edges.push({ x0: x, y0: y, x1: x + 1, y1: y });
40
+ }
41
+ if (!isMaskPixelOn(binary, cols, rows, x, y + 1)) {
42
+ edges.push({ x0: x + 1, y0: y + 1, x1: x, y1: y + 1 });
43
+ }
44
+ if (!isMaskPixelOn(binary, cols, rows, x - 1, y)) {
45
+ edges.push({ x0: x, y0: y + 1, x1: x, y1: y });
46
+ }
47
+ if (!isMaskPixelOn(binary, cols, rows, x + 1, y)) {
48
+ edges.push({ x0: x + 1, y0: y, x1: x + 1, y1: y + 1 });
49
+ }
50
+ }
51
+ }
52
+ return edges;
53
+ }
54
+ function chainBoundaryLoops(edges) {
55
+ const outgoing = new Map();
56
+ const edgeKey = (edge) => `${edge.x0},${edge.y0}->${edge.x1},${edge.y1}`;
57
+ for (const edge of edges) {
58
+ const key = `${edge.x0},${edge.y0}`;
59
+ const list = outgoing.get(key);
60
+ if (list) {
61
+ list.push(edge);
62
+ }
63
+ else {
64
+ outgoing.set(key, [edge]);
65
+ }
66
+ }
67
+ const used = new Set();
68
+ const loops = [];
69
+ for (const edge of edges) {
70
+ const startEdgeKey = edgeKey(edge);
71
+ if (used.has(startEdgeKey)) {
72
+ continue;
73
+ }
74
+ const loop = [{ x: edge.x0, y: edge.y0 }];
75
+ let current = edge;
76
+ used.add(startEdgeKey);
77
+ loop.push({ x: current.x1, y: current.y1 });
78
+ while (true) {
79
+ const endKey = `${current.x1},${current.y1}`;
80
+ const startKey = `${loop[0].x},${loop[0].y}`;
81
+ if (endKey === startKey && loop.length > 2) {
82
+ break;
83
+ }
84
+ const candidates = outgoing.get(endKey);
85
+ const next = candidates?.find(candidate => !used.has(edgeKey(candidate)));
86
+ if (!next) {
87
+ break;
88
+ }
89
+ current = next;
90
+ used.add(edgeKey(current));
91
+ loop.push({ x: current.x1, y: current.y1 });
92
+ if (loop.length > edges.length + 1) {
93
+ break;
94
+ }
95
+ }
96
+ if (loop.length >= 4) {
97
+ loops.push(loop);
98
+ }
99
+ }
100
+ return loops;
101
+ }
102
+ function simplifyOrthogonalLoop(points) {
103
+ if (points.length <= 3) {
104
+ return points;
105
+ }
106
+ const out = [points[0]];
107
+ for (let i = 1; i < points.length - 1; i++) {
108
+ const prev = out[out.length - 1];
109
+ const curr = points[i];
110
+ const next = points[i + 1];
111
+ const collinearX = prev.x === curr.x && curr.x === next.x;
112
+ const collinearY = prev.y === curr.y && curr.y === next.y;
113
+ if (!collinearX && !collinearY) {
114
+ out.push(curr);
115
+ }
116
+ }
117
+ out.push(points[points.length - 1]);
118
+ return out;
119
+ }
120
+ function perpendicularDistance2(point, lineStart, lineEnd) {
121
+ const dx = lineEnd.x - lineStart.x;
122
+ const dy = lineEnd.y - lineStart.y;
123
+ if (dx === 0 && dy === 0) {
124
+ return Math.hypot(point.x - lineStart.x, point.y - lineStart.y);
125
+ }
126
+ const t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) /
127
+ (dx * dx + dy * dy);
128
+ const projX = lineStart.x + t * dx;
129
+ const projY = lineStart.y + t * dy;
130
+ return Math.hypot(point.x - projX, point.y - projY);
131
+ }
132
+ /**
133
+ * Ramer-Douglas-Peucker simplification for outline loops.
134
+ * Reduces stair-step jagginess from grid boundary tracing so that dashed
135
+ * outlines render smoothly instead of zigzagging across every pixel edge.
136
+ * Epsilon is in grid-pixel units (1.0 = one mask pixel).
137
+ */
138
+ function simplifyLoopRdp(points, epsilon) {
139
+ if (points.length <= 2) {
140
+ return points;
141
+ }
142
+ let maxDist = 0;
143
+ let index = 0;
144
+ const end = points.length - 1;
145
+ const lineStart = points[0];
146
+ const lineEnd = points[end];
147
+ for (let i = 1; i < end; i++) {
148
+ const p = points[i];
149
+ const dist = perpendicularDistance2(p, lineStart, lineEnd);
150
+ if (dist > maxDist) {
151
+ maxDist = dist;
152
+ index = i;
153
+ }
154
+ }
155
+ if (maxDist > epsilon) {
156
+ const left = simplifyLoopRdp(points.slice(0, index + 1), epsilon);
157
+ const right = simplifyLoopRdp(points.slice(index), epsilon);
158
+ return [...left.slice(0, -1), ...right];
159
+ }
160
+ return [lineStart, lineEnd];
161
+ }
162
+ function loopsToSkPath(loops, cols, rows, rect) {
163
+ const path = Skia.Path.Make();
164
+ for (const rawLoop of loops) {
165
+ // Two-pass simplification: first remove collinear points, then RDP to
166
+ // smooth stair-step artifacts from grid boundary tracing.
167
+ const orthogonal = simplifyOrthogonalLoop(rawLoop);
168
+ const loop = simplifyLoopRdp(orthogonal, 1.0);
169
+ if (loop.length < 2) {
170
+ continue;
171
+ }
172
+ const [first, ...rest] = loop;
173
+ path.moveTo(rect.x + (first.x / cols) * rect.w, rect.y + (first.y / rows) * rect.h);
174
+ for (const point of rest) {
175
+ path.lineTo(rect.x + (point.x / cols) * rect.w, rect.y + (point.y / rows) * rect.h);
176
+ }
177
+ path.close();
178
+ }
179
+ return path;
180
+ }
181
+ function pointInIntegerLoop(px, py, loop) {
182
+ let inside = false;
183
+ for (let i = 0, j = loop.length - 1; i < loop.length; j = i++) {
184
+ const xi = loop[i].x;
185
+ const yi = loop[i].y;
186
+ const xj = loop[j].x;
187
+ const yj = loop[j].y;
188
+ const intersect = yi > py !== yj > py &&
189
+ px < ((xj - xi) * (py - yi)) / (yj - yi + Number.EPSILON) + xi;
190
+ if (intersect) {
191
+ inside = !inside;
192
+ }
193
+ }
194
+ return inside;
195
+ }
196
+ function loopBoundingArea(loop) {
197
+ if (loop.length === 0) {
198
+ return 0;
199
+ }
200
+ let minX = loop[0].x;
201
+ let maxX = loop[0].x;
202
+ let minY = loop[0].y;
203
+ let maxY = loop[0].y;
204
+ for (const point of loop) {
205
+ minX = Math.min(minX, point.x);
206
+ maxX = Math.max(maxX, point.x);
207
+ minY = Math.min(minY, point.y);
208
+ maxY = Math.max(maxY, point.y);
209
+ }
210
+ return (maxX - minX) * (maxY - minY);
211
+ }
212
+ function filterOutlineLoops(loops, cols, rows, seedPx) {
213
+ if (loops.length === 0) {
214
+ return loops;
215
+ }
216
+ const minLoopArea = Math.max(16, Math.floor(cols * rows * 0.00005));
217
+ const significant = loops.filter(loop => loopBoundingArea(loop) >= minLoopArea);
218
+ const candidates = significant.length > 0 ? significant : loops;
219
+ if (seedPx) {
220
+ const sampleX = seedPx.x + 0.5;
221
+ const sampleY = seedPx.y + 0.5;
222
+ const containing = candidates.filter(loop => pointInIntegerLoop(sampleX, sampleY, loop));
223
+ if (containing.length > 0) {
224
+ containing.sort((a, b) => loopBoundingArea(b) - loopBoundingArea(a));
225
+ return containing;
226
+ }
227
+ }
228
+ candidates.sort((a, b) => loopBoundingArea(b) - loopBoundingArea(a));
229
+ // Keep loops ≥ 5% of the largest area to filter isolated noise speckles
230
+ // while retaining genuine disconnected fragments of the same region.
231
+ const minKeepArea = loopBoundingArea(candidates[0]) * 0.05;
232
+ return candidates.filter(loop => loopBoundingArea(loop) >= minKeepArea);
233
+ }
234
+ function floodFillComponent(binary, cols, rows, seedX, seedY) {
235
+ if (seedX < 0 ||
236
+ seedY < 0 ||
237
+ seedX >= cols ||
238
+ seedY >= rows ||
239
+ !binary[seedY * cols + seedX]) {
240
+ return null;
241
+ }
242
+ const out = new Uint8Array(cols * rows);
243
+ const stack = [seedY * cols + seedX];
244
+ out[stack[0]] = 255;
245
+ while (stack.length > 0) {
246
+ const index = stack.pop();
247
+ const x = index % cols;
248
+ const y = (index - x) / cols;
249
+ if (x > 0) {
250
+ const left = index - 1;
251
+ if (binary[left] && !out[left]) {
252
+ out[left] = 255;
253
+ stack.push(left);
254
+ }
255
+ }
256
+ if (x + 1 < cols) {
257
+ const right = index + 1;
258
+ if (binary[right] && !out[right]) {
259
+ out[right] = 255;
260
+ stack.push(right);
261
+ }
262
+ }
263
+ if (y > 0) {
264
+ const up = index - cols;
265
+ if (binary[up] && !out[up]) {
266
+ out[up] = 255;
267
+ stack.push(up);
268
+ }
269
+ }
270
+ if (y + 1 < rows) {
271
+ const down = index + cols;
272
+ if (binary[down] && !out[down]) {
273
+ out[down] = 255;
274
+ stack.push(down);
275
+ }
276
+ }
277
+ }
278
+ return out;
279
+ }
280
+ function findLargestComponentSeed(binary, cols, rows) {
281
+ const visited = new Uint8Array(cols * rows);
282
+ let bestArea = 0;
283
+ let bestSeed = null;
284
+ for (let y = 0; y < rows; y++) {
285
+ const row = y * cols;
286
+ for (let x = 0; x < cols; x++) {
287
+ const start = row + x;
288
+ if (!binary[start] || visited[start]) {
289
+ continue;
290
+ }
291
+ let area = 0;
292
+ let sumX = 0;
293
+ let sumY = 0;
294
+ const stack = [start];
295
+ visited[start] = 1;
296
+ while (stack.length > 0) {
297
+ const index = stack.pop();
298
+ area += 1;
299
+ const px = index % cols;
300
+ const py = (index - px) / cols;
301
+ sumX += px;
302
+ sumY += py;
303
+ if (px > 0) {
304
+ const left = index - 1;
305
+ if (binary[left] && !visited[left]) {
306
+ visited[left] = 1;
307
+ stack.push(left);
308
+ }
309
+ }
310
+ if (px + 1 < cols) {
311
+ const right = index + 1;
312
+ if (binary[right] && !visited[right]) {
313
+ visited[right] = 1;
314
+ stack.push(right);
315
+ }
316
+ }
317
+ if (py > 0) {
318
+ const up = index - cols;
319
+ if (binary[up] && !visited[up]) {
320
+ visited[up] = 1;
321
+ stack.push(up);
322
+ }
323
+ }
324
+ if (py + 1 < rows) {
325
+ const down = index + cols;
326
+ if (binary[down] && !visited[down]) {
327
+ visited[down] = 1;
328
+ stack.push(down);
329
+ }
330
+ }
331
+ }
332
+ if (area > bestArea) {
333
+ bestArea = area;
334
+ bestSeed = {
335
+ x: Math.floor(sumX / area),
336
+ y: Math.floor(sumY / area),
337
+ };
338
+ }
339
+ }
340
+ }
341
+ return bestSeed;
342
+ }
343
+ function buildRegionOutlinePathFromBinary(binary, cols, rows, rect, seedPx) {
344
+ let working = binary;
345
+ if (seedPx) {
346
+ const component = floodFillComponent(binary, cols, rows, seedPx.x, seedPx.y);
347
+ if (!component) {
348
+ return Skia.Path.Make();
349
+ }
350
+ working = component;
351
+ }
352
+ const edges = collectBoundaryEdges(working, cols, rows);
353
+ const loops = chainBoundaryLoops(edges);
354
+ const filtered = filterOutlineLoops(loops, cols, rows, seedPx);
355
+ return loopsToSkPath(filtered, cols, rows, rect);
356
+ }
357
+ function resolveRegionOutlineSeedPx(binary, cols, rows, normSeed) {
358
+ if (normSeed) {
359
+ return {
360
+ x: Math.min(cols - 1, Math.max(0, Math.floor(normSeed.x * cols))),
361
+ y: Math.min(rows - 1, Math.max(0, Math.floor(normSeed.y * rows))),
362
+ };
363
+ }
364
+ return findLargestComponentSeed(binary, cols, rows) ?? undefined;
365
+ }
366
+ export function buildRegionOutlinePathForRegion(regionId, regions, maskData, rect, normSeed) {
367
+ const binaries = extractRegionBinaries(regions, maskData);
368
+ const binary = binaries.get(regionId);
369
+ if (!binary) {
370
+ return Skia.Path.Make();
371
+ }
372
+ const { cols, rows } = maskData;
373
+ const seedPx = resolveRegionOutlineSeedPx(binary, cols, rows, normSeed);
374
+ return buildRegionOutlinePathFromBinary(binary, cols, rows, rect, seedPx);
375
+ }
376
+ function extractRegionBinaries(regions, maskData) {
377
+ const { labels, baseboardBinary, cols, rows } = maskData;
378
+ const size = cols * rows;
379
+ const binaries = new Map();
380
+ const semanticColors = getMaskSegmentRuntimeConfig().mask.semanticColors;
381
+ const regionIdBySemantic = new Int32Array(semanticColors.length);
382
+ regionIdBySemantic.fill(-1);
383
+ let baseboardRegionId = null;
384
+ for (const reg of regions) {
385
+ binaries.set(reg.id, new Uint8Array(size));
386
+ if (reg.thinStrip) {
387
+ baseboardRegionId = reg.id;
388
+ continue;
389
+ }
390
+ const semanticIndex = semanticColors.findIndex(entry => entry.name === reg.name);
391
+ if (semanticIndex >= 0) {
392
+ regionIdBySemantic[semanticIndex] = reg.id;
393
+ }
394
+ }
395
+ const semanticCount = semanticColors.length;
396
+ for (let i = 0; i < size; i++) {
397
+ if (baseboardRegionId != null && baseboardBinary[i] > 0) {
398
+ binaries.get(baseboardRegionId)[i] = 255;
399
+ continue;
400
+ }
401
+ const semanticIndex = labels[i];
402
+ if (semanticIndex < semanticCount && regionIdBySemantic[semanticIndex] >= 0) {
403
+ binaries.get(regionIdBySemantic[semanticIndex])[i] = 255;
404
+ }
405
+ }
406
+ return binaries;
407
+ }
408
+ export function buildAllRegionOutlinePaths(regions, maskData, rect) {
409
+ const { cols, rows } = maskData;
410
+ const binaries = extractRegionBinaries(regions, maskData);
411
+ const map = new Map();
412
+ for (const reg of regions) {
413
+ const binary = binaries.get(reg.id);
414
+ if (!binary) {
415
+ map.set(reg.id, Skia.Path.Make());
416
+ continue;
417
+ }
418
+ // No seed — build outlines from the full binary so all disconnected
419
+ // fragments (common after mask downsampling) get dashed outlines during
420
+ // the init flash loop. Per-region hold highlights still use a touch-seed
421
+ // via buildRegionOutlinePathForRegion for precise fragment isolation.
422
+ map.set(reg.id, buildRegionOutlinePathFromBinary(binary, cols, rows, rect));
423
+ }
424
+ return map;
425
+ }
426
+ function isBaseboardEntry(entry) {
427
+ return entry.name === BASEBOARD_SEMANTIC_NAME;
428
+ }
429
+ /** 踢脚线:仅同行横向补缝,不纵向膨胀 */
430
+ function bridgeBaseboardHorizontally(binary, cols, rows) {
431
+ const out = new Uint8Array(binary);
432
+ const halfW = maskCfg().kickBridgeHalfWPx;
433
+ for (let y = 0; y < rows; y++) {
434
+ for (let x = 0; x < cols; x++) {
435
+ if (!binary[y * cols + x]) {
436
+ continue;
437
+ }
438
+ for (let dx = -halfW; dx <= halfW; dx++) {
439
+ const nx = x + dx;
440
+ if (nx < 0 || nx >= cols) {
441
+ continue;
442
+ }
443
+ out[y * cols + nx] = 255;
444
+ }
445
+ }
446
+ }
447
+ return out;
448
+ }
449
+ function rowRunsToPolygons(runs, cols, rows) {
450
+ return runs.map(run => [
451
+ { x: run.minX / cols, y: run.y / rows },
452
+ { x: (run.maxX + 1) / cols, y: run.y / rows },
453
+ { x: (run.maxX + 1) / cols, y: (run.y + 1) / rows },
454
+ { x: run.minX / cols, y: (run.y + 1) / rows },
455
+ ]);
456
+ }
457
+ function quantChannelSlot(value) {
458
+ const q = Math.min(255, Math.round(value / maskCfg().quantStep) * maskCfg().quantStep);
459
+ if (q >= 192) {
460
+ return q === 255 ? 4 : 3;
461
+ }
462
+ if (q >= 128) {
463
+ return 2;
464
+ }
465
+ if (q >= 64) {
466
+ return 1;
467
+ }
468
+ return 0;
469
+ }
470
+ function quantKeyIndex(b, g, r) {
471
+ return (quantChannelSlot(b) * 25 + quantChannelSlot(g) * 5 + quantChannelSlot(r));
472
+ }
473
+ function getStripQuantIndices() {
474
+ const revision = getMaskRuntimeRevision();
475
+ if (stripIndicesRevision === revision && cachedStripQuantIndices) {
476
+ return cachedStripQuantIndices;
477
+ }
478
+ cachedStripQuantIndices = new Set([...getBaseboardStripQuantKeys()].map(key => {
479
+ const [b, g, r] = key.split(',').map(part => Number(part));
480
+ return quantKeyIndex(b, g, r);
481
+ }));
482
+ stripIndicesRevision = revision;
483
+ return cachedStripQuantIndices;
484
+ }
485
+ let stripIndicesRevision = -1;
486
+ let cachedStripQuantIndices = null;
487
+ let channelSlotLutRevision = -1;
488
+ let cachedChannelSlotLut = null;
489
+ function buildQuantChannelSlotLut(quantStep) {
490
+ const lut = new Uint8Array(256);
491
+ for (let value = 0; value < 256; value++) {
492
+ const q = Math.min(255, Math.round(value / quantStep) * quantStep);
493
+ if (q >= 192) {
494
+ lut[value] = q === 255 ? 4 : 3;
495
+ }
496
+ else if (q >= 128) {
497
+ lut[value] = 2;
498
+ }
499
+ else if (q >= 64) {
500
+ lut[value] = 1;
501
+ }
502
+ else {
503
+ lut[value] = 0;
504
+ }
505
+ }
506
+ return lut;
507
+ }
508
+ function getQuantChannelSlotLut() {
509
+ const revision = getMaskRuntimeRevision();
510
+ if (channelSlotLutRevision === revision && cachedChannelSlotLut) {
511
+ return cachedChannelSlotLut;
512
+ }
513
+ cachedChannelSlotLut = buildQuantChannelSlotLut(maskCfg().quantStep);
514
+ channelSlotLutRevision = revision;
515
+ return cachedChannelSlotLut;
516
+ }
517
+ function quantSlotToChannel(slot) {
518
+ if (slot >= 4) {
519
+ return 255;
520
+ }
521
+ if (slot >= 3) {
522
+ return 192;
523
+ }
524
+ if (slot >= 2) {
525
+ return 128;
526
+ }
527
+ if (slot >= 1) {
528
+ return 64;
529
+ }
530
+ return 0;
531
+ }
532
+ function quantIndexToBgr(idx) {
533
+ const bSlot = (idx / 25) | 0;
534
+ const gSlot = ((idx % 25) / 5) | 0;
535
+ const rSlot = idx % 5;
536
+ return [
537
+ quantSlotToChannel(bSlot),
538
+ quantSlotToChannel(gSlot),
539
+ quantSlotToChannel(rSlot),
540
+ ];
541
+ }
542
+ let semanticLutRevision = -1;
543
+ let cachedSemanticLut = null;
544
+ let cachedStripQuantLut = null;
545
+ let stripQuantLutRevision = -1;
546
+ function getStripQuantLut() {
547
+ const revision = getMaskRuntimeRevision();
548
+ if (stripQuantLutRevision === revision && cachedStripQuantLut) {
549
+ return cachedStripQuantLut;
550
+ }
551
+ const lut = new Uint8Array(125);
552
+ for (const idx of getStripQuantIndices()) {
553
+ lut[idx] = 1;
554
+ }
555
+ cachedStripQuantLut = lut;
556
+ stripQuantLutRevision = revision;
557
+ return lut;
558
+ }
559
+ function bboxFromBinary(binary, cols, rows) {
560
+ let minX = cols;
561
+ let minY = rows;
562
+ let maxX = -1;
563
+ let maxY = -1;
564
+ for (let y = 0; y < rows; y++) {
565
+ const row = y * cols;
566
+ for (let x = 0; x < cols; x++) {
567
+ if (!binary[row + x]) {
568
+ continue;
569
+ }
570
+ if (x < minX) {
571
+ minX = x;
572
+ }
573
+ if (x > maxX) {
574
+ maxX = x;
575
+ }
576
+ if (y < minY) {
577
+ minY = y;
578
+ }
579
+ if (y > maxY) {
580
+ maxY = y;
581
+ }
582
+ }
583
+ }
584
+ if (maxX < 0) {
585
+ return null;
586
+ }
587
+ return {
588
+ x: minX / cols,
589
+ y: minY / rows,
590
+ w: (maxX - minX + 1) / cols,
591
+ h: (maxY - minY + 1) / rows,
592
+ };
593
+ }
594
+ /** 从二值图逐行条带构建蒙版(供 Skia PathBuilder 使用) */
595
+ export function appendMaskBinaryToPathBuilder(binary, cols, rows, rect, builder, minRunPx = maskCfg().baseboardMinRunPx) {
596
+ for (let y = 0; y < rows; y++) {
597
+ let runStart = -1;
598
+ const normY0 = y / rows;
599
+ const normY1 = (y + 1) / rows;
600
+ const screenY0 = rect.y + normY0 * rect.h;
601
+ const screenY1 = rect.y + normY1 * rect.h;
602
+ for (let x = 0; x <= cols; x++) {
603
+ const on = x < cols && binary[y * cols + x] > 0;
604
+ if (on && runStart < 0) {
605
+ runStart = x;
606
+ }
607
+ if (!on && runStart >= 0) {
608
+ if (x - runStart >= minRunPx) {
609
+ const normX0 = runStart / cols;
610
+ const normX1 = x / cols;
611
+ builder.moveTo(rect.x + normX0 * rect.w, screenY0);
612
+ builder.lineTo(rect.x + normX1 * rect.w, screenY0);
613
+ builder.lineTo(rect.x + normX1 * rect.w, screenY1);
614
+ builder.lineTo(rect.x + normX0 * rect.w, screenY1);
615
+ builder.close();
616
+ }
617
+ runStart = -1;
618
+ }
619
+ }
620
+ }
621
+ }
622
+ /** 从语义标签逐行条带构建蒙版(避免维护多张二值图) */
623
+ export function appendLabelMaskToPathBuilder(labels, semanticIndex, cols, rows, rect, builder, minRunPx = maskCfg().baseboardMinRunPx) {
624
+ for (let y = 0; y < rows; y++) {
625
+ let runStart = -1;
626
+ const row = y * cols;
627
+ const normY0 = y / rows;
628
+ const normY1 = (y + 1) / rows;
629
+ const screenY0 = rect.y + normY0 * rect.h;
630
+ const screenY1 = rect.y + normY1 * rect.h;
631
+ for (let x = 0; x <= cols; x++) {
632
+ const on = x < cols &&
633
+ labels[row + x] === semanticIndex;
634
+ if (on && runStart < 0) {
635
+ runStart = x;
636
+ }
637
+ if (!on && runStart >= 0) {
638
+ if (x - runStart >= minRunPx) {
639
+ const normX0 = runStart / cols;
640
+ const normX1 = x / cols;
641
+ builder.moveTo(rect.x + normX0 * rect.w, screenY0);
642
+ builder.lineTo(rect.x + normX1 * rect.w, screenY0);
643
+ builder.lineTo(rect.x + normX1 * rect.w, screenY1);
644
+ builder.lineTo(rect.x + normX0 * rect.w, screenY1);
645
+ builder.close();
646
+ }
647
+ runStart = -1;
648
+ }
649
+ }
650
+ }
651
+ }
652
+ function appendRunRectToBuilder(runStart, runEnd, y, cols, rows, rect, builder, minRunPx) {
653
+ if (runEnd - runStart < minRunPx) {
654
+ return;
655
+ }
656
+ const normY0 = y / rows;
657
+ const normY1 = (y + 1) / rows;
658
+ const screenY0 = rect.y + normY0 * rect.h;
659
+ const screenY1 = rect.y + normY1 * rect.h;
660
+ const normX0 = runStart / cols;
661
+ const normX1 = runEnd / cols;
662
+ builder.moveTo(rect.x + normX0 * rect.w, screenY0);
663
+ builder.lineTo(rect.x + normX1 * rect.w, screenY0);
664
+ builder.lineTo(rect.x + normX1 * rect.w, screenY1);
665
+ builder.lineTo(rect.x + normX0 * rect.w, screenY1);
666
+ builder.close();
667
+ }
668
+ /** 蒙版路径构建降采样(屏幕显示不需要分割分辨率,点击仍用全分辨率 pickMap) */
669
+ export function downsampleMaskDataForPaths(maskData, maxLongSide) {
670
+ const { labels, baseboardBinary, cols, rows } = maskData;
671
+ const longSide = Math.max(cols, rows);
672
+ if (longSide <= maxLongSide) {
673
+ return maskData;
674
+ }
675
+ const scale = maxLongSide / longSide;
676
+ const dstCols = Math.max(1, Math.floor(cols * scale));
677
+ const dstRows = Math.max(1, Math.floor(rows * scale));
678
+ const outLabels = new Uint8Array(dstCols * dstRows);
679
+ const outBaseboard = new Uint8Array(dstCols * dstRows);
680
+ for (let y = 0; y < dstRows; y++) {
681
+ const sy = Math.min(rows - 1, Math.floor((y * rows) / dstRows));
682
+ const srcRow = sy * cols;
683
+ const dstRow = y * dstCols;
684
+ for (let x = 0; x < dstCols; x++) {
685
+ const sx = Math.min(cols - 1, Math.floor((x * cols) / dstCols));
686
+ const si = srcRow + sx;
687
+ const di = dstRow + x;
688
+ outLabels[di] = labels[si];
689
+ outBaseboard[di] = baseboardBinary[si];
690
+ }
691
+ }
692
+ return {
693
+ labels: outLabels,
694
+ baseboardBinary: outBaseboard,
695
+ cols: dstCols,
696
+ rows: dstRows,
697
+ };
698
+ }
699
+ /** 单次扫描构建所有分区 Skia 蒙版路径(单 label pass,避免每像素 × 语义数循环) */
700
+ export function buildAllRegionMaskPaths(regions, maskData, rect) {
701
+ const { labels, baseboardBinary, cols, rows } = maskData;
702
+ const builders = new Map();
703
+ const semanticColors = getMaskSegmentRuntimeConfig().mask.semanticColors;
704
+ const regionIdBySemantic = new Int32Array(semanticColors.length);
705
+ regionIdBySemantic.fill(-1);
706
+ let baseboardRegionId = null;
707
+ for (const reg of regions) {
708
+ builders.set(reg.id, Skia.Path.Make());
709
+ if (reg.thinStrip) {
710
+ baseboardRegionId = reg.id;
711
+ continue;
712
+ }
713
+ const semanticIndex = semanticColors.findIndex(entry => entry.name === reg.name);
714
+ if (semanticIndex >= 0) {
715
+ regionIdBySemantic[semanticIndex] = reg.id;
716
+ }
717
+ }
718
+ const semanticCount = semanticColors.length;
719
+ const minRunPx = maskCfg().baseboardMinRunPx;
720
+ for (let y = 0; y < rows; y++) {
721
+ let baseboardRunStart = -1;
722
+ let labelRunStart = -1;
723
+ let labelRunSemantic = -1;
724
+ const row = y * cols;
725
+ for (let x = 0; x <= cols; x++) {
726
+ if (baseboardRegionId != null) {
727
+ const bbOn = x < cols && baseboardBinary[row + x] > 0;
728
+ if (bbOn && baseboardRunStart < 0) {
729
+ baseboardRunStart = x;
730
+ }
731
+ if (!bbOn && baseboardRunStart >= 0) {
732
+ appendRunRectToBuilder(baseboardRunStart, x, y, cols, rows, rect, builders.get(baseboardRegionId), minRunPx);
733
+ baseboardRunStart = -1;
734
+ }
735
+ }
736
+ let activeSemantic = -1;
737
+ if (x < cols) {
738
+ const si = labels[row + x];
739
+ if (si < semanticCount && regionIdBySemantic[si] >= 0) {
740
+ activeSemantic = si;
741
+ }
742
+ }
743
+ if (activeSemantic !== labelRunSemantic) {
744
+ if (labelRunSemantic >= 0 && labelRunStart >= 0) {
745
+ const regionId = regionIdBySemantic[labelRunSemantic];
746
+ appendRunRectToBuilder(labelRunStart, x, y, cols, rows, rect, builders.get(regionId), minRunPx);
747
+ }
748
+ labelRunSemantic = activeSemantic;
749
+ labelRunStart = activeSemantic >= 0 ? x : -1;
750
+ }
751
+ }
752
+ }
753
+ const paths = new Map();
754
+ for (const [regionId, builder] of builders) {
755
+ paths.set(regionId, builder);
756
+ }
757
+ return paths;
758
+ }
759
+ function collectRowRuns(binary, cols, rows, minRunPx) {
760
+ const runs = [];
761
+ for (let y = 0; y < rows; y++) {
762
+ let runStart = -1;
763
+ for (let x = 0; x <= cols; x++) {
764
+ const on = x < cols && binary[y * cols + x] > 0;
765
+ if (on && runStart < 0) {
766
+ runStart = x;
767
+ }
768
+ if (!on && runStart >= 0) {
769
+ if (x - runStart >= minRunPx) {
770
+ runs.push({ minX: runStart, maxX: x - 1, y });
771
+ }
772
+ runStart = -1;
773
+ }
774
+ }
775
+ }
776
+ return runs;
777
+ }
778
+ function bboxFromPolygons(polygons) {
779
+ if (polygons.length === 0) {
780
+ return null;
781
+ }
782
+ let minX = 1;
783
+ let minY = 1;
784
+ let maxX = 0;
785
+ let maxY = 0;
786
+ for (const polygon of polygons) {
787
+ for (const point of polygon) {
788
+ minX = Math.min(minX, point.x);
789
+ minY = Math.min(minY, point.y);
790
+ maxX = Math.max(maxX, point.x);
791
+ maxY = Math.max(maxY, point.y);
792
+ }
793
+ }
794
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
795
+ }
796
+ /** baseboard:逐行 1px 条带贴合掩码;点击用横向补缝后的条带 */
797
+ function extractBaseboardRowPolygons(binary, cols, rows) {
798
+ let totalArea = 0;
799
+ for (let i = 0; i < binary.length; i++) {
800
+ if (binary[i]) {
801
+ totalArea += 1;
802
+ }
803
+ }
804
+ const runs = collectRowRuns(binary, cols, rows, maskCfg().baseboardMinRunPx);
805
+ const polygons = rowRunsToPolygons(runs, cols, rows);
806
+ const bridged = bridgeBaseboardHorizontally(binary, cols, rows);
807
+ const bridgedRuns = collectRowRuns(bridged, cols, rows, maskCfg().baseboardMinRunPx);
808
+ const hitPolygons = rowRunsToPolygons(bridgedRuns, cols, rows);
809
+ const bbox = bboxFromPolygons(polygons);
810
+ return {
811
+ polygons,
812
+ hitPolygons,
813
+ totalArea,
814
+ bbox,
815
+ };
816
+ }
817
+ function cloneBinary(binary) {
818
+ return new Uint8Array(binary);
819
+ }
820
+ function subtractBinary(target, mask) {
821
+ for (let i = 0; i < target.length; i++) {
822
+ if (mask[i]) {
823
+ target[i] = 0;
824
+ }
825
+ }
826
+ }
827
+ function minPalettePixels(cols, rows) {
828
+ return Math.max(300, Math.floor((cols * rows) / 2000));
829
+ }
830
+ function minPixelsForSemantic(name, cols, rows) {
831
+ const base = minPalettePixels(cols, rows);
832
+ if (!maskCfg().secondarySemanticNames.has(name)) {
833
+ return base;
834
+ }
835
+ return Math.max(base, Math.floor(cols * rows * maskCfg().secondaryMinPixelRatio));
836
+ }
837
+ function quantizeChannel(value) {
838
+ return Math.min(255, Math.round(value / maskCfg().quantStep) * maskCfg().quantStep);
839
+ }
840
+ function maskQuantKey(b, g, r) {
841
+ return `${quantizeChannel(b)},${quantizeChannel(g)},${quantizeChannel(r)}`;
842
+ }
843
+ function dilateBinaryBox(source, cols, rows, radiusX, radiusY) {
844
+ const temp = new Uint8Array(source.length);
845
+ const out = new Uint8Array(source.length);
846
+ for (let y = 0; y < rows; y++) {
847
+ for (let x = 0; x < cols; x++) {
848
+ if (!source[y * cols + x]) {
849
+ continue;
850
+ }
851
+ const minX = Math.max(0, x - radiusX);
852
+ const maxX = Math.min(cols - 1, x + radiusX);
853
+ for (let nx = minX; nx <= maxX; nx++) {
854
+ temp[y * cols + nx] = 255;
855
+ }
856
+ }
857
+ }
858
+ for (let y = 0; y < rows; y++) {
859
+ for (let x = 0; x < cols; x++) {
860
+ if (!temp[y * cols + x]) {
861
+ continue;
862
+ }
863
+ const minY = Math.max(0, y - radiusY);
864
+ const maxY = Math.min(rows - 1, y + radiusY);
865
+ for (let ny = minY; ny <= maxY; ny++) {
866
+ out[ny * cols + x] = 255;
867
+ }
868
+ }
869
+ }
870
+ return out;
871
+ }
872
+ function buildWallCabinetJunctionMask(buffer, cols, rows) {
873
+ const wall = new Uint8Array(cols * rows);
874
+ const cabinet = new Uint8Array(cols * rows);
875
+ for (let i = 0; i < cols * rows; i++) {
876
+ const o = i * 3;
877
+ const b = buffer[o];
878
+ const g = buffer[o + 1];
879
+ const r = buffer[o + 2];
880
+ if (isIgnoredColor(b, g, r)) {
881
+ continue;
882
+ }
883
+ const key = maskQuantKey(b, g, r);
884
+ if (getWallQuantKeys().has(key)) {
885
+ wall[i] = 255;
886
+ }
887
+ if (getCabinetQuantKeys().has(key)) {
888
+ cabinet[i] = 255;
889
+ }
890
+ }
891
+ const wallNear = dilateBinaryBox(wall, cols, rows, maskCfg().junctionHRadiusPx, maskCfg().junctionVRadiusPx);
892
+ const cabinetNear = dilateBinaryBox(cabinet, cols, rows, maskCfg().junctionHRadiusPx, maskCfg().junctionVRadiusPx);
893
+ const junction = new Uint8Array(cols * rows);
894
+ for (let i = 0; i < junction.length; i++) {
895
+ if (wallNear[i] && cabinetNear[i]) {
896
+ junction[i] = 255;
897
+ }
898
+ }
899
+ return junction;
900
+ }
901
+ function computeStrictBaseboardBand(strictBaseboard, cols, rows) {
902
+ let minY = rows;
903
+ let maxY = -1;
904
+ for (let y = 0; y < rows; y++) {
905
+ const row = y * cols;
906
+ for (let x = 0; x < cols; x++) {
907
+ if (!strictBaseboard[row + x]) {
908
+ continue;
909
+ }
910
+ if (y < minY) {
911
+ minY = y;
912
+ }
913
+ if (y > maxY) {
914
+ maxY = y;
915
+ }
916
+ }
917
+ }
918
+ if (maxY < 0) {
919
+ return null;
920
+ }
921
+ return { minY, maxY };
922
+ }
923
+ /** 仅保留贴近真实踢脚线带的 junction 细条,避免上方墙柜交界零碎区域误入 */
924
+ function isJunctionNearStrictBaseboard(idx, strictBaseboard, cols, rows, band) {
925
+ if (!band) {
926
+ return false;
927
+ }
928
+ const x = idx % cols;
929
+ const y = (idx - x) / cols;
930
+ if (y < band.minY - maskCfg().baseboardJunctionRowMarginPx ||
931
+ y > band.maxY + maskCfg().baseboardJunctionRowMarginPx) {
932
+ return false;
933
+ }
934
+ const halfW = maskCfg().kickBridgeHalfWPx;
935
+ for (let dy = -maskCfg().baseboardJunctionVReachPx; dy <= maskCfg().baseboardJunctionVReachPx; dy++) {
936
+ const ny = y + dy;
937
+ if (ny < 0 || ny >= rows) {
938
+ continue;
939
+ }
940
+ const row = ny * cols;
941
+ for (let dx = -halfW; dx <= halfW; dx++) {
942
+ const nx = x + dx;
943
+ if (nx < 0 || nx >= cols) {
944
+ continue;
945
+ }
946
+ if (strictBaseboard[row + nx]) {
947
+ return true;
948
+ }
949
+ }
950
+ }
951
+ return false;
952
+ }
953
+ function buildBaseboardBinary(buffer, cols, rows, junctionMask) {
954
+ const binary = new Uint8Array(cols * rows);
955
+ const junction = junctionMask ?? buildWallCabinetJunctionMask(buffer, cols, rows);
956
+ for (let y = 0; y < rows; y++) {
957
+ for (let x = 0; x < cols; x++) {
958
+ const idx = y * cols + x;
959
+ const o = idx * 3;
960
+ const b = buffer[o];
961
+ const g = buffer[o + 1];
962
+ const r = buffer[o + 2];
963
+ if (isIgnoredColor(b, g, r)) {
964
+ continue;
965
+ }
966
+ if (isStrictBaseboardPixel(b, g, r)) {
967
+ binary[idx] = 255;
968
+ }
969
+ }
970
+ }
971
+ const band = computeStrictBaseboardBand(binary, cols, rows);
972
+ for (let y = 0; y < rows; y++) {
973
+ for (let x = 0; x < cols; x++) {
974
+ const idx = y * cols + x;
975
+ if (binary[idx]) {
976
+ continue;
977
+ }
978
+ const o = idx * 3;
979
+ const b = buffer[o];
980
+ const g = buffer[o + 1];
981
+ const r = buffer[o + 2];
982
+ if (isIgnoredColor(b, g, r)) {
983
+ continue;
984
+ }
985
+ const key = maskQuantKey(b, g, r);
986
+ if (getBaseboardStripQuantKeys().has(key) &&
987
+ junction[idx] &&
988
+ isJunctionNearStrictBaseboard(idx, binary, cols, rows, band)) {
989
+ binary[idx] = 255;
990
+ }
991
+ }
992
+ }
993
+ return binary;
994
+ }
995
+ export function buildBaseboardBinaryFromMask(buffer, cols, rows) {
996
+ return buildBaseboardBinary(buffer, cols, rows);
997
+ }
998
+ /** 分割分辨率踢脚线二值图最近邻放大到点击查表分辨率(避免全图 junction 重算) */
999
+ export function upscaleBinaryMask(src, srcCols, srcRows, dstCols, dstRows) {
1000
+ const dst = new Uint8Array(dstCols * dstRows);
1001
+ for (let y = 0; y < dstRows; y++) {
1002
+ const sy = Math.min(srcRows - 1, Math.floor((y * srcRows) / dstRows));
1003
+ const srcRow = sy * srcCols;
1004
+ const dstRow = y * dstCols;
1005
+ for (let x = 0; x < dstCols; x++) {
1006
+ const sx = Math.min(srcCols - 1, Math.floor((x * srcCols) / dstCols));
1007
+ dst[dstRow + x] = src[srcRow + sx];
1008
+ }
1009
+ }
1010
+ return dst;
1011
+ }
1012
+ function buildMaskPolygonsFromBinary(binary, cols, rows) {
1013
+ return rowRunsToPolygons(collectRowRuns(binary, cols, rows, maskCfg().baseboardMinRunPx), cols, rows);
1014
+ }
1015
+ export function isBaseboardMaskPixel(buffer, cols, rows, x, y, baseboardBinary) {
1016
+ if (x < 0 || y < 0 || x >= cols || y >= rows) {
1017
+ return false;
1018
+ }
1019
+ if (baseboardBinary) {
1020
+ return baseboardBinary[y * cols + x] > 0;
1021
+ }
1022
+ const o = (y * cols + x) * 3;
1023
+ const b = buffer[o];
1024
+ const g = buffer[o + 1];
1025
+ const r = buffer[o + 2];
1026
+ if (isIgnoredColor(b, g, r)) {
1027
+ return false;
1028
+ }
1029
+ if (isStrictBaseboardPixel(b, g, r)) {
1030
+ return true;
1031
+ }
1032
+ const key = maskQuantKey(b, g, r);
1033
+ if (!getBaseboardStripQuantKeys().has(key)) {
1034
+ return false;
1035
+ }
1036
+ const junction = buildWallCabinetJunctionMask(buffer, cols, rows);
1037
+ return junction[y * cols + x] > 0;
1038
+ }
1039
+ export { isStrictBaseboardPixel as isBaseboardPixel } from './maskSemanticPalette';
1040
+ export function getMaskQuantKey(b, g, r) {
1041
+ return maskQuantKey(b, g, r);
1042
+ }
1043
+ /** @deprecated 请使用 isBaseboardMaskPixel */
1044
+ export function isKickPlatePixel(b, g, r) {
1045
+ return isStrictBaseboardPixel(b, g, r);
1046
+ }
1047
+ function mergeBBox(bbox, next) {
1048
+ const x1 = Math.min(bbox.x, next.x);
1049
+ const y1 = Math.min(bbox.y, next.y);
1050
+ const x2 = Math.max(bbox.x + bbox.w, next.x + next.w);
1051
+ const y2 = Math.max(bbox.y + bbox.h, next.y + next.h);
1052
+ return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
1053
+ }
1054
+ function isIgnoredColor(b, g, r) {
1055
+ const threshold = maskCfg().blackThreshold;
1056
+ return b < threshold && g < threshold && r < threshold;
1057
+ }
1058
+ function countBinaryPixels(binary) {
1059
+ let count = 0;
1060
+ for (let i = 0; i < binary.length; i++) {
1061
+ if (binary[i]) {
1062
+ count += 1;
1063
+ }
1064
+ }
1065
+ return count;
1066
+ }
1067
+ const IGNORE_SEMANTIC_INDEX = 255;
1068
+ let nameToIndexRevision = -1;
1069
+ let cachedNameToIndex = null;
1070
+ function getSemanticNameToIndex() {
1071
+ const revision = getMaskRuntimeRevision();
1072
+ if (nameToIndexRevision === revision && cachedNameToIndex) {
1073
+ return cachedNameToIndex;
1074
+ }
1075
+ const colors = getMaskSegmentRuntimeConfig().mask.semanticColors;
1076
+ cachedNameToIndex = new Map(colors.map((entry, index) => [entry.name, index]));
1077
+ nameToIndexRevision = revision;
1078
+ return cachedNameToIndex;
1079
+ }
1080
+ function createSemanticLut() {
1081
+ const revision = getMaskRuntimeRevision();
1082
+ if (semanticLutRevision === revision && cachedSemanticLut) {
1083
+ return cachedSemanticLut;
1084
+ }
1085
+ const lut = new Uint8Array(125);
1086
+ lut.fill(IGNORE_SEMANTIC_INDEX);
1087
+ const colors = getMaskSegmentRuntimeConfig().mask.semanticColors;
1088
+ const nameToIndex = getSemanticNameToIndex();
1089
+ for (const entry of colors) {
1090
+ const semanticIndex = nameToIndex.get(entry.name);
1091
+ if (semanticIndex === undefined) {
1092
+ continue;
1093
+ }
1094
+ const { b, g, r } = entry.bgr;
1095
+ lut[quantKeyIndex(b, g, r)] = semanticIndex;
1096
+ }
1097
+ for (let idx = 0; idx < 125; idx++) {
1098
+ if (lut[idx] !== IGNORE_SEMANTIC_INDEX) {
1099
+ continue;
1100
+ }
1101
+ const [b, g, r] = quantIndexToBgr(idx);
1102
+ const name = classifyBgrPixelToSemantic(b, g, r);
1103
+ lut[idx] = nameToIndex.get(name) ?? IGNORE_SEMANTIC_INDEX;
1104
+ }
1105
+ cachedSemanticLut = lut;
1106
+ semanticLutRevision = revision;
1107
+ return lut;
1108
+ }
1109
+ /** 单次扫描:像素语义标签 + 像素计数 + bbox(不写多张二值图) */
1110
+ function buildSemanticLayout(buffer, cols, rows) {
1111
+ const pixelCount = cols * rows;
1112
+ const labels = new Uint8Array(pixelCount);
1113
+ labels.fill(IGNORE_SEMANTIC_INDEX);
1114
+ const counts = new Map();
1115
+ const bboxes = new Map();
1116
+ const semanticLut = createSemanticLut();
1117
+ const nameToIndex = getSemanticNameToIndex();
1118
+ const indexToName = getMaskSegmentRuntimeConfig().mask.semanticColors.map(entry => entry.name);
1119
+ const semanticCount = indexToName.length;
1120
+ const blackThreshold = maskCfg().blackThreshold;
1121
+ const channelSlotLut = getQuantChannelSlotLut();
1122
+ const stripQuantLut = getStripQuantLut();
1123
+ const minX = new Int32Array(semanticCount);
1124
+ const minY = new Int32Array(semanticCount);
1125
+ const maxX = new Int32Array(semanticCount);
1126
+ const maxY = new Int32Array(semanticCount);
1127
+ const hitMask = new Uint8Array(semanticCount);
1128
+ const countArr = new Int32Array(semanticCount);
1129
+ const stripIndices = [];
1130
+ const strictBaseboard = new Uint8Array(pixelCount);
1131
+ let strictBaseboardCount = 0;
1132
+ const baseboardIdx = nameToIndex.get(BASEBOARD_SEMANTIC_NAME);
1133
+ minX.fill(cols);
1134
+ minY.fill(rows);
1135
+ maxX.fill(-1);
1136
+ maxY.fill(-1);
1137
+ const buf = buffer;
1138
+ for (let y = 0; y < rows; y++) {
1139
+ const row = y * cols;
1140
+ for (let x = 0; x < cols; x++) {
1141
+ const i = row + x;
1142
+ const o = i * 3;
1143
+ const b = buf[o];
1144
+ const g = buf[o + 1];
1145
+ const r = buf[o + 2];
1146
+ if (b < blackThreshold && g < blackThreshold && r < blackThreshold) {
1147
+ continue;
1148
+ }
1149
+ const lutIdx = channelSlotLut[b] * 25 + channelSlotLut[g] * 5 + channelSlotLut[r];
1150
+ if (stripQuantLut[lutIdx]) {
1151
+ stripIndices.push(i);
1152
+ }
1153
+ const semanticIndex = semanticLut[lutIdx];
1154
+ if (semanticIndex === IGNORE_SEMANTIC_INDEX) {
1155
+ continue;
1156
+ }
1157
+ labels[i] = semanticIndex;
1158
+ hitMask[semanticIndex] = 1;
1159
+ countArr[semanticIndex] += 1;
1160
+ if (semanticIndex === baseboardIdx) {
1161
+ strictBaseboard[i] = 255;
1162
+ strictBaseboardCount += 1;
1163
+ }
1164
+ if (x < minX[semanticIndex]) {
1165
+ minX[semanticIndex] = x;
1166
+ }
1167
+ if (x > maxX[semanticIndex]) {
1168
+ maxX[semanticIndex] = x;
1169
+ }
1170
+ if (y < minY[semanticIndex]) {
1171
+ minY[semanticIndex] = y;
1172
+ }
1173
+ if (y > maxY[semanticIndex]) {
1174
+ maxY[semanticIndex] = y;
1175
+ }
1176
+ }
1177
+ }
1178
+ const invCols = 1 / cols;
1179
+ const invRows = 1 / rows;
1180
+ for (let semanticIndex = 0; semanticIndex < semanticCount; semanticIndex++) {
1181
+ if (!hitMask[semanticIndex]) {
1182
+ continue;
1183
+ }
1184
+ const name = indexToName[semanticIndex];
1185
+ counts.set(name, countArr[semanticIndex]);
1186
+ bboxes.set(name, {
1187
+ x: minX[semanticIndex] * invCols,
1188
+ y: minY[semanticIndex] * invRows,
1189
+ w: (maxX[semanticIndex] - minX[semanticIndex] + 1) * invCols,
1190
+ h: (maxY[semanticIndex] - minY[semanticIndex] + 1) * invRows,
1191
+ });
1192
+ }
1193
+ return {
1194
+ labels,
1195
+ counts,
1196
+ bboxes,
1197
+ stripIndices,
1198
+ strictBaseboard,
1199
+ strictBaseboardCount,
1200
+ };
1201
+ }
1202
+ function buildJunctionAtStripPixels(labels, stripIndices, cols, rows) {
1203
+ const junction = new Uint8Array(cols * rows);
1204
+ const junctionIndices = [];
1205
+ const nameToIndex = getSemanticNameToIndex();
1206
+ const wallIdx = nameToIndex.get('wall');
1207
+ const cabinetIdx = nameToIndex.get('cabinet');
1208
+ const junctionH = maskCfg().junctionHRadiusPx;
1209
+ const junctionV = maskCfg().junctionVRadiusPx;
1210
+ if (wallIdx === undefined ||
1211
+ cabinetIdx === undefined ||
1212
+ stripIndices.length === 0) {
1213
+ return { junction, junctionIndices };
1214
+ }
1215
+ for (const idx of stripIndices) {
1216
+ const x = idx % cols;
1217
+ const y = (idx - x) / cols;
1218
+ let hasWall = false;
1219
+ let hasCabinet = false;
1220
+ const minY = Math.max(0, y - junctionV);
1221
+ const maxY = Math.min(rows - 1, y + junctionV);
1222
+ const minX = Math.max(0, x - junctionH);
1223
+ const maxX = Math.min(cols - 1, x + junctionH);
1224
+ for (let ny = minY; ny <= maxY && !(hasWall && hasCabinet); ny++) {
1225
+ const rowBase = ny * cols;
1226
+ for (let nx = minX; nx <= maxX && !(hasWall && hasCabinet); nx++) {
1227
+ const label = labels[rowBase + nx];
1228
+ if (label === wallIdx) {
1229
+ hasWall = true;
1230
+ }
1231
+ else if (label === cabinetIdx) {
1232
+ hasCabinet = true;
1233
+ }
1234
+ }
1235
+ }
1236
+ if (hasWall && hasCabinet) {
1237
+ junction[idx] = 255;
1238
+ junctionIndices.push(idx);
1239
+ }
1240
+ }
1241
+ return { junction, junctionIndices };
1242
+ }
1243
+ function finalizeBaseboardBinary(strictBaseboard, strictCount, junctionIndices, cols, rows) {
1244
+ const binary = new Uint8Array(strictBaseboard);
1245
+ const band = computeStrictBaseboardBand(binary, cols, rows);
1246
+ let pixelCount = strictCount;
1247
+ for (const idx of junctionIndices) {
1248
+ if (!isJunctionNearStrictBaseboard(idx, binary, cols, rows, band)) {
1249
+ continue;
1250
+ }
1251
+ if (!binary[idx]) {
1252
+ pixelCount += 1;
1253
+ }
1254
+ binary[idx] = 255;
1255
+ }
1256
+ return { binary, pixelCount };
1257
+ }
1258
+ function buildPickMapAndWorkAreas(labels, indexToName, nameToId, baseboardBinary, cols, rows) {
1259
+ const pixelCount = cols * rows;
1260
+ const pick = new Uint8Array(pixelCount);
1261
+ const workAreas = new Map();
1262
+ const baseboardName = BASEBOARD_SEMANTIC_NAME;
1263
+ const baseboardId = nameToId.get(baseboardName);
1264
+ const baseboardCode = baseboardId === undefined ? 0 : baseboardId + 1;
1265
+ for (let i = 0; i < pixelCount; i++) {
1266
+ if (baseboardBinary[i]) {
1267
+ if (baseboardCode > 0) {
1268
+ pick[i] = baseboardCode;
1269
+ }
1270
+ workAreas.set(baseboardName, (workAreas.get(baseboardName) ?? 0) + 1);
1271
+ continue;
1272
+ }
1273
+ const semanticIndex = labels[i];
1274
+ if (semanticIndex === IGNORE_SEMANTIC_INDEX) {
1275
+ continue;
1276
+ }
1277
+ const name = indexToName[semanticIndex];
1278
+ if (!name) {
1279
+ continue;
1280
+ }
1281
+ const regionId = nameToId.get(name);
1282
+ if (regionId !== undefined) {
1283
+ pick[i] = regionId + 1;
1284
+ }
1285
+ workAreas.set(name, (workAreas.get(name) ?? 0) + 1);
1286
+ }
1287
+ return { pick, workAreas };
1288
+ }
1289
+ /**
1290
+ * 1-pass 8-neighbour majority-vote dilate on the pick buffer.
1291
+ * For each zero pixel, count the 8-connected neighbours by their non-zero
1292
+ * pick code. If any single code appears in ≥ 4 neighbours, fill the pixel
1293
+ * with that code (majority rule).
1294
+ *
1295
+ * Compared to the old 4-neighbour "all-must-agree" rule this handles:
1296
+ * - diagonal holes inside a region (8-connectivity)
1297
+ * - narrow door / furniture strips where a hole borders both the strip
1298
+ * AND a neighbouring wall region — majority vote picks the region that
1299
+ * occupies more of the 8-pixel perimeter
1300
+ *
1301
+ * Still reads from the ORIGINAL pick buffer to prevent cascade overflow.
1302
+ * Cost: O(N) with ~20 ops/pixel — negligible relative to segmentation.
1303
+ */
1304
+ function dilatePickBuffer1px(pick, cols, rows) {
1305
+ const pixelCount = cols * rows;
1306
+ const dst = new Uint8Array(pixelCount);
1307
+ dst.set(pick);
1308
+ for (let y = 1; y < rows - 1; y++) {
1309
+ for (let x = 1; x < cols - 1; x++) {
1310
+ const i = y * cols + x;
1311
+ if (pick[i] !== 0)
1312
+ continue;
1313
+ // Read 8 neighbours from the ORIGINAL pick buffer to avoid cascade.
1314
+ const n = [
1315
+ pick[(y - 1) * cols + (x - 1)],
1316
+ pick[(y - 1) * cols + x],
1317
+ pick[(y - 1) * cols + (x + 1)],
1318
+ pick[y * cols + (x - 1)],
1319
+ pick[y * cols + (x + 1)],
1320
+ pick[(y + 1) * cols + (x - 1)],
1321
+ pick[(y + 1) * cols + x],
1322
+ pick[(y + 1) * cols + (x + 1)], // bottom-right
1323
+ ];
1324
+ // Count occurrences of each non-zero code.
1325
+ const counts = {};
1326
+ for (let k = 0; k < 8; k++) {
1327
+ const code = n[k];
1328
+ if (code !== 0) {
1329
+ counts[code] = (counts[code] ?? 0) + 1;
1330
+ }
1331
+ }
1332
+ // Majority rule: ≥ 4 of 8 neighbours share the same code.
1333
+ for (const codeStr of Object.keys(counts)) {
1334
+ const code = Number(codeStr);
1335
+ if (counts[code] >= 4) {
1336
+ dst[i] = code;
1337
+ break;
1338
+ }
1339
+ }
1340
+ }
1341
+ }
1342
+ return dst;
1343
+ }
1344
+ function paletteFromCounts(counts, cols, rows) {
1345
+ const orderedSemantics = getMaskSegmentRuntimeConfig().mask.semanticColors.map(entry => entry.name);
1346
+ return orderedSemantics
1347
+ .map(name => {
1348
+ const pixelCount = counts.get(name) ?? 0;
1349
+ if (pixelCount < minPixelsForSemantic(name, cols, rows)) {
1350
+ return null;
1351
+ }
1352
+ const ref = getSemanticColorByName(name);
1353
+ return {
1354
+ label: orderedSemantics.indexOf(name),
1355
+ name,
1356
+ hex: ref.hex,
1357
+ color: { ...ref.bgr },
1358
+ };
1359
+ })
1360
+ .filter((entry) => entry != null)
1361
+ .sort((a, b) => (counts.get(b.name) ?? 0) - (counts.get(a.name) ?? 0))
1362
+ .slice(0, maskCfg().maxRegionColors);
1363
+ }
1364
+ async function contourToPolygon(contour, cols, rows, minArea, approxEpsilon) {
1365
+ const area = await cv.contourArea(contour);
1366
+ if (area < minArea) {
1367
+ return null;
1368
+ }
1369
+ const rect = await cv.boundingRect(contour);
1370
+ const perimeter = await cv.arcLength(contour, true);
1371
+ const maxEpsilonPx = Math.max(cols, rows) * 0.01;
1372
+ const thinSide = Math.min(rect.width, rect.height);
1373
+ const epsilonPx = Math.max(1.5, Math.min(perimeter * approxEpsilon, maxEpsilonPx, Math.max(2, thinSide * 0.12)));
1374
+ const points = await cv.approxPolyDP(contour, epsilonPx, true);
1375
+ if (points.length < 3) {
1376
+ return null;
1377
+ }
1378
+ let minX = cols;
1379
+ let minY = rows;
1380
+ let maxX = 0;
1381
+ let maxY = 0;
1382
+ const polygon = points.map(point => {
1383
+ minX = Math.min(minX, point.x);
1384
+ maxX = Math.max(maxX, point.x);
1385
+ minY = Math.min(minY, point.y);
1386
+ maxY = Math.max(maxY, point.y);
1387
+ return { x: point.x / cols, y: point.y / rows };
1388
+ });
1389
+ return {
1390
+ polygon,
1391
+ area,
1392
+ bbox: {
1393
+ x: minX / cols,
1394
+ y: minY / rows,
1395
+ w: (maxX - minX + 1) / cols,
1396
+ h: (maxY - minY + 1) / rows,
1397
+ },
1398
+ };
1399
+ }
1400
+ function extractPolygonsFromBinaryJs(binary, cols, rows, minArea) {
1401
+ const visited = new Uint8Array(cols * rows);
1402
+ const polygons = [];
1403
+ let totalArea = 0;
1404
+ let bbox = null;
1405
+ for (let y = 0; y < rows; y++) {
1406
+ for (let x = 0; x < cols; x++) {
1407
+ const idx = y * cols + x;
1408
+ if (!binary[idx] || visited[idx]) {
1409
+ continue;
1410
+ }
1411
+ let minX = x;
1412
+ let maxX = x;
1413
+ let minY = y;
1414
+ let maxY = y;
1415
+ let area = 0;
1416
+ const stack = [[x, y]];
1417
+ visited[idx] = 1;
1418
+ while (stack.length > 0) {
1419
+ const [cx, cy] = stack.pop();
1420
+ area += 1;
1421
+ minX = Math.min(minX, cx);
1422
+ maxX = Math.max(maxX, cx);
1423
+ minY = Math.min(minY, cy);
1424
+ maxY = Math.max(maxY, cy);
1425
+ if (cx > 0) {
1426
+ const left = cy * cols + (cx - 1);
1427
+ if (binary[left] && !visited[left]) {
1428
+ visited[left] = 1;
1429
+ stack.push([cx - 1, cy]);
1430
+ }
1431
+ }
1432
+ if (cx + 1 < cols) {
1433
+ const right = cy * cols + (cx + 1);
1434
+ if (binary[right] && !visited[right]) {
1435
+ visited[right] = 1;
1436
+ stack.push([cx + 1, cy]);
1437
+ }
1438
+ }
1439
+ if (cy > 0) {
1440
+ const up = (cy - 1) * cols + cx;
1441
+ if (binary[up] && !visited[up]) {
1442
+ visited[up] = 1;
1443
+ stack.push([cx, cy - 1]);
1444
+ }
1445
+ }
1446
+ if (cy + 1 < rows) {
1447
+ const down = (cy + 1) * cols + cx;
1448
+ if (binary[down] && !visited[down]) {
1449
+ visited[down] = 1;
1450
+ stack.push([cx, cy + 1]);
1451
+ }
1452
+ }
1453
+ }
1454
+ if (area < minArea) {
1455
+ continue;
1456
+ }
1457
+ const polygon = [
1458
+ { x: minX / cols, y: minY / rows },
1459
+ { x: (maxX + 1) / cols, y: minY / rows },
1460
+ { x: (maxX + 1) / cols, y: (maxY + 1) / rows },
1461
+ { x: minX / cols, y: (maxY + 1) / rows },
1462
+ ];
1463
+ const partBbox = {
1464
+ x: minX / cols,
1465
+ y: minY / rows,
1466
+ w: (maxX - minX + 1) / cols,
1467
+ h: (maxY - minY + 1) / rows,
1468
+ };
1469
+ polygons.push(polygon);
1470
+ totalArea += area;
1471
+ bbox = bbox ? mergeBBox(bbox, partBbox) : partBbox;
1472
+ }
1473
+ }
1474
+ return { polygons, totalArea, bbox };
1475
+ }
1476
+ async function extractPolygonsFromBinary(binary, cols, rows, minArea, approxEpsilon) {
1477
+ const binaryMat = cv.binaryBufferToMat(binary, cols, rows);
1478
+ const closed = cv.createMat(cols, rows, 1);
1479
+ try {
1480
+ const kernel = await cv.getStructuringElement(cv.MORPH_ELLIPSE, {
1481
+ width: MORPH_KERNEL_SIZE,
1482
+ height: MORPH_KERNEL_SIZE,
1483
+ });
1484
+ try {
1485
+ await cv.morphologyEx(binaryMat, closed, cv.MORPH_OPEN, kernel);
1486
+ }
1487
+ finally {
1488
+ kernel.release();
1489
+ }
1490
+ const contours = await cv.findContours(closed, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
1491
+ const polygons = [];
1492
+ let totalArea = 0;
1493
+ let bbox = null;
1494
+ for (const contour of contours) {
1495
+ try {
1496
+ const part = await contourToPolygon(contour, cols, rows, minArea, approxEpsilon);
1497
+ if (!part) {
1498
+ continue;
1499
+ }
1500
+ polygons.push(part.polygon);
1501
+ totalArea += part.area;
1502
+ bbox = bbox ? mergeBBox(bbox, part.bbox) : part.bbox;
1503
+ }
1504
+ finally {
1505
+ contour.release();
1506
+ }
1507
+ }
1508
+ return { polygons, totalArea, bbox };
1509
+ }
1510
+ finally {
1511
+ binaryMat.release();
1512
+ closed.release();
1513
+ }
1514
+ }
1515
+ export async function extractRegionsFromMaskBuffer(buffer, cols, rows, _options) {
1516
+ return extractRegionsFromMaskBufferSync(buffer, cols, rows, _options);
1517
+ }
1518
+ export function extractRegionsFromMaskBufferSync(buffer, cols, rows, _options) {
1519
+ const layout = buildSemanticLayout(buffer, cols, rows);
1520
+ const baseboardStart = __DEV__ ? performance.now() : 0;
1521
+ const { junctionIndices } = buildJunctionAtStripPixels(layout.labels, layout.stripIndices, cols, rows);
1522
+ const { binary: baseboardBinary, pixelCount: baseboardPixels } = finalizeBaseboardBinary(layout.strictBaseboard, layout.strictBaseboardCount, junctionIndices, cols, rows);
1523
+ layout.counts.set(BASEBOARD_SEMANTIC_NAME, baseboardPixels);
1524
+ const paletteEntries = paletteFromCounts(layout.counts, cols, rows);
1525
+ if (paletteEntries.length === 0) {
1526
+ return {
1527
+ regions: [],
1528
+ pickMap: { buffer: new Uint8Array(cols * rows), cols, rows },
1529
+ labels: layout.labels,
1530
+ baseboardBinary,
1531
+ segCols: cols,
1532
+ segRows: rows,
1533
+ };
1534
+ }
1535
+ const indexToName = getMaskSegmentRuntimeConfig().mask.semanticColors.map(entry => entry.name);
1536
+ const regionResults = paletteEntries.map((entry) => {
1537
+ const { label, name, hex, color } = entry;
1538
+ const isBaseboard = isBaseboardEntry(entry);
1539
+ try {
1540
+ const finalBbox = isBaseboard
1541
+ ? bboxFromBinary(baseboardBinary, cols, rows)
1542
+ : layout.bboxes.get(name);
1543
+ if (!finalBbox) {
1544
+ if (__DEV__) {
1545
+ console.warn(`[MaskSegment] ${name} 无有效轮廓,已跳过`);
1546
+ }
1547
+ return null;
1548
+ }
1549
+ const finalPolygons = [bboxToPolygon(finalBbox)];
1550
+ return {
1551
+ id: label,
1552
+ name,
1553
+ hex,
1554
+ color,
1555
+ area: layout.counts.get(name) ?? 0,
1556
+ bbox: finalBbox,
1557
+ polygons: finalPolygons,
1558
+ outlinePolygons: finalPolygons,
1559
+ thinStrip: isBaseboard,
1560
+ };
1561
+ }
1562
+ catch (error) {
1563
+ if (__DEV__) {
1564
+ console.warn(`[MaskSegment] 色 #${label} 提取失败:`, error instanceof Error ? error.message : String(error));
1565
+ }
1566
+ return null;
1567
+ }
1568
+ });
1569
+ const regions = regionResults.filter((region) => region != null);
1570
+ regions.sort((a, b) => b.area - a.area);
1571
+ regions.forEach((reg, index) => {
1572
+ reg.id = index;
1573
+ });
1574
+ const finalRegions = regions.slice(0, maskCfg().maxRegionColors);
1575
+ const nameToId = new Map(finalRegions.map(reg => [reg.name, reg.id]));
1576
+ const pickBuildStart = __DEV__ ? performance.now() : 0;
1577
+ const { pick: pickBufferRaw, workAreas } = buildPickMapAndWorkAreas(layout.labels, indexToName, nameToId, baseboardBinary, cols, rows);
1578
+ const pickBuffer = dilatePickBuffer1px(pickBufferRaw, cols, rows);
1579
+ for (const reg of finalRegions) {
1580
+ const workArea = workAreas.get(reg.name);
1581
+ if (workArea != null) {
1582
+ reg.area = workArea;
1583
+ }
1584
+ }
1585
+ return {
1586
+ regions: finalRegions,
1587
+ pickMap: { buffer: pickBuffer, cols, rows },
1588
+ labels: layout.labels,
1589
+ baseboardBinary,
1590
+ segCols: cols,
1591
+ segRows: rows,
1592
+ };
1593
+ }
1594
+ /** @deprecated 请使用 extractRegionsFromMaskBuffer */
1595
+ export async function extractRegionsFromMask(maskMat, options) {
1596
+ const { buffer, cols, rows } = cv.matToBuffer(maskMat);
1597
+ const result = extractRegionsFromMaskBufferSync(buffer, cols, rows, options);
1598
+ return result.regions;
1599
+ }
1600
+ //# sourceMappingURL=maskSegmentation.js.map