Add splitWalls to subdivide wall regions by texture boundaries.
Enables optional wall-1/wall-2… sub-regions with independent paint and undo, plus example toggle and unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
eb246efa8a
commit
70499176d6
@ -499,6 +499,14 @@ const hasError = watchState === 'error';
|
|||||||
| `baseboardJunctionRowMarginPx` | `number` | `1` | 踢脚线交界行边距 |
|
| `baseboardJunctionRowMarginPx` | `number` | `1` | 踢脚线交界行边距 |
|
||||||
| `baseboardJunctionVReachPx` | `number` | `2` | 踢脚线交界纵向延伸 |
|
| `baseboardJunctionVReachPx` | `number` | `2` | 踢脚线交界纵向延伸 |
|
||||||
| `baseboardMinRunPx` | `number` | `2` | 蒙版条带最小 run 长度 |
|
| `baseboardMinRunPx` | `number` | `2` | 蒙版条带最小 run 长度 |
|
||||||
|
| `splitWalls` | `boolean` | `false` | 在 wall 掩码内按纹理边界细分为 `wall-1`、`wall-2`… |
|
||||||
|
| `splitWallsMaxCount` | `number` | `8` | 墙壁子区最大数量 |
|
||||||
|
| `splitWallsMinAreaRatio` | `number` | `0.002` | 碎块最小面积比(相对 seg 总像素) |
|
||||||
|
| `splitWallsColorDistSq` | `number` | `1400` | 连通域色度均值距离平方阈值(墙内光影容忍,材质间更严) |
|
||||||
|
| `splitWallsChromaBlurRadius` | `number` | `5` | 预留:色度平滑半径 |
|
||||||
|
| `splitWallsNeutralChromaMax` | `number` | `14` | 白/灰墙低饱和判定半径;与有色墙强制分界 |
|
||||||
|
|
||||||
|
开启 `splitWalls` 后,原有单一 `wall` 区域会被替换为多个 `wall-N` 子区,各自独立上色与撤销。旧 Session 中 `regionName: 'wall'` 无法映射到新子区名,需重新上色。
|
||||||
|
|
||||||
### pipelineConfig
|
### pipelineConfig
|
||||||
|
|
||||||
|
|||||||
198
__tests__/wallTextureSplit.test.ts
Normal file
198
__tests__/wallTextureSplit.test.ts
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
jest.mock('../src/utils/opencvAdapter', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
resetMaskSegmentRuntimeConfig,
|
||||||
|
setMaskSegmentRuntimeConfig,
|
||||||
|
} from '../src/utils/maskSegmentRuntime';
|
||||||
|
import type { SegmentMaskResult } from '../src/utils/maskSegmentation';
|
||||||
|
import { splitWallRegionsByTexture } from '../src/utils/wallTextureSplit';
|
||||||
|
|
||||||
|
const WALL_IDX = 3;
|
||||||
|
const IGNORE = 255;
|
||||||
|
|
||||||
|
function buildSyntheticWallResult(
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
): SegmentMaskResult {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const labels = new Uint8Array(pixelCount);
|
||||||
|
labels.fill(WALL_IDX);
|
||||||
|
const baseboardBinary = new Uint8Array(pixelCount);
|
||||||
|
const pick = new Uint8Array(pixelCount);
|
||||||
|
pick.fill(1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
regions: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'wall',
|
||||||
|
hex: '#4363D8',
|
||||||
|
color: { b: 216, g: 99, r: 67 },
|
||||||
|
polygons: [
|
||||||
|
[
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: cols, y: 0 },
|
||||||
|
{ x: cols, y: rows },
|
||||||
|
{ x: 0, y: rows },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
bbox: { x: 0, y: 0, w: cols, h: rows },
|
||||||
|
area: pixelCount,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pickMap: { buffer: pick, cols, rows },
|
||||||
|
labels,
|
||||||
|
baseboardBinary,
|
||||||
|
segCols: cols,
|
||||||
|
segRows: rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSplitOrigin(
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
leftBgr: [number, number, number],
|
||||||
|
rightBgr: [number, number, number],
|
||||||
|
): Uint8Array {
|
||||||
|
const buffer = new Uint8Array(cols * rows * 3);
|
||||||
|
const mid = Math.floor(cols / 2);
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
const [b, g, r] = x < mid ? leftBgr : rightBgr;
|
||||||
|
const i = (y * cols + x) * 3;
|
||||||
|
buffer[i] = b;
|
||||||
|
buffer[i + 1] = g;
|
||||||
|
buffer[i + 2] = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickRegionIdAt(
|
||||||
|
pick: Uint8Array,
|
||||||
|
cols: number,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
): number | null {
|
||||||
|
const code = pick[y * cols + x];
|
||||||
|
return code > 0 ? code - 1 : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetMaskSegmentRuntimeConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
resetMaskSegmentRuntimeConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('splitWalls:false leaves regions unchanged', () => {
|
||||||
|
const cols = 24;
|
||||||
|
const rows = 12;
|
||||||
|
const base = buildSyntheticWallResult(cols, rows);
|
||||||
|
const origin = buildSplitOrigin(cols, rows, [8, 8, 8], [240, 240, 240]);
|
||||||
|
|
||||||
|
setMaskSegmentRuntimeConfig({ maskConfig: { splitWalls: false } });
|
||||||
|
const result = splitWallRegionsByTexture(base, origin, cols, rows, 10);
|
||||||
|
|
||||||
|
expect(result.regions.find(r => r.name === 'wall')).toBeTruthy();
|
||||||
|
expect(result.regions.some(r => /^wall-\d+$/.test(r.name))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('white wall and blue wall split into separate regions', () => {
|
||||||
|
const cols = 32;
|
||||||
|
const rows = 16;
|
||||||
|
const base = buildSyntheticWallResult(cols, rows);
|
||||||
|
const origin = buildSplitOrigin(cols, rows, [245, 245, 245], [180, 120, 60]);
|
||||||
|
|
||||||
|
setMaskSegmentRuntimeConfig({
|
||||||
|
maskConfig: { splitWalls: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = splitWallRegionsByTexture(base, origin, cols, rows, 10);
|
||||||
|
const wallSubs = result.regions.filter(r => /^wall-\d+$/.test(r.name));
|
||||||
|
expect(wallSubs.length).toBeGreaterThanOrEqual(2);
|
||||||
|
|
||||||
|
const pick = result.pickMap.buffer;
|
||||||
|
const leftId = pickRegionIdAt(pick, cols, 4, rows >> 1);
|
||||||
|
const rightId = pickRegionIdAt(pick, cols, cols - 4, rows >> 1);
|
||||||
|
expect(leftId).not.toBeNull();
|
||||||
|
expect(rightId).not.toBeNull();
|
||||||
|
expect(leftId).not.toBe(rightId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('splitWalls:true splits wall into wall-1 and wall-2 by chroma', () => {
|
||||||
|
const cols = 24;
|
||||||
|
const rows = 12;
|
||||||
|
const base = buildSyntheticWallResult(cols, rows);
|
||||||
|
// 左蓝右暖色(色度差异明显)
|
||||||
|
const origin = buildSplitOrigin(cols, rows, [220, 50, 30], [50, 200, 240]);
|
||||||
|
|
||||||
|
setMaskSegmentRuntimeConfig({
|
||||||
|
maskConfig: {
|
||||||
|
splitWalls: true,
|
||||||
|
splitWallsMinAreaRatio: 0.001,
|
||||||
|
splitWallsColorDistSq: 400,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = splitWallRegionsByTexture(base, origin, cols, rows, 10);
|
||||||
|
|
||||||
|
const wallSubs = result.regions.filter(r => /^wall-\d+$/.test(r.name));
|
||||||
|
expect(wallSubs.length).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(result.regions.some(r => r.name === 'wall')).toBe(false);
|
||||||
|
expect(result.wallSubLabels).toBeDefined();
|
||||||
|
|
||||||
|
const pick = result.pickMap.buffer;
|
||||||
|
const leftId = pickRegionIdAt(pick, cols, 4, rows >> 1);
|
||||||
|
const rightId = pickRegionIdAt(pick, cols, cols - 4, rows >> 1);
|
||||||
|
expect(leftId).not.toBeNull();
|
||||||
|
expect(rightId).not.toBeNull();
|
||||||
|
expect(leftId).not.toBe(rightId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('same-color wall with lighting gradient stays one region', () => {
|
||||||
|
const cols = 32;
|
||||||
|
const rows = 16;
|
||||||
|
const base = buildSyntheticWallResult(cols, rows);
|
||||||
|
const origin = new Uint8Array(cols * rows * 3);
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
const t = x / Math.max(1, cols - 1);
|
||||||
|
const i = (y * cols + x) * 3;
|
||||||
|
// 同一蓝色墙面,从左暗阴影到右亮部
|
||||||
|
origin[i] = Math.round(160 + t * 70);
|
||||||
|
origin[i + 1] = Math.round(70 + t * 50);
|
||||||
|
origin[i + 2] = Math.round(40 + t * 30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMaskSegmentRuntimeConfig({
|
||||||
|
maskConfig: { splitWalls: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = splitWallRegionsByTexture(base, origin, cols, rows, 10);
|
||||||
|
const wallSubs = result.regions.filter(r => /^wall-\d+$/.test(r.name));
|
||||||
|
expect(wallSubs).toHaveLength(1);
|
||||||
|
expect(wallSubs[0].name).toBe('wall-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('single-texture wall becomes wall-1 when splitWalls enabled', () => {
|
||||||
|
const cols = 16;
|
||||||
|
const rows = 8;
|
||||||
|
const base = buildSyntheticWallResult(cols, rows);
|
||||||
|
const origin = buildSplitOrigin(cols, rows, [120, 120, 120], [120, 120, 120]);
|
||||||
|
|
||||||
|
setMaskSegmentRuntimeConfig({
|
||||||
|
maskConfig: { splitWalls: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = splitWallRegionsByTexture(base, origin, cols, rows, 10);
|
||||||
|
|
||||||
|
const wallSubs = result.regions.filter(r => /^wall-\d+$/.test(r.name));
|
||||||
|
expect(wallSubs).toHaveLength(1);
|
||||||
|
expect(wallSubs[0].name).toBe('wall-1');
|
||||||
|
});
|
||||||
2
dist/components/MaskSegmentCanvas.d.ts.map
vendored
2
dist/components/MaskSegmentCanvas.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"MaskSegmentCanvas.d.ts","sourceRoot":"","sources":["../../src/components/MaskSegmentCanvas.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAmDf,OAAO,KAAK,EAEV,sBAAsB,EACtB,oBAAoB,EAMrB,MAAM,2BAA2B,CAAC;AAcnC,YAAY,EACV,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,QAAQ,EACR,iBAAiB,GAClB,MAAM,2BAA2B,CAAC;AA4ZnC,QAAA,MAAM,iBAAiB,qGAoqErB,CAAC;AA8HH,eAAe,iBAAiB,CAAC"}
|
{"version":3,"file":"MaskSegmentCanvas.d.ts","sourceRoot":"","sources":["../../src/components/MaskSegmentCanvas.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAqDf,OAAO,KAAK,EAEV,sBAAsB,EACtB,oBAAoB,EAMrB,MAAM,2BAA2B,CAAC;AAcnC,YAAY,EACV,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,QAAQ,EACR,iBAAiB,GAClB,MAAM,2BAA2B,CAAC;AA4ZnC,QAAA,MAAM,iBAAiB,qGA2qErB,CAAC;AA8HH,eAAe,iBAAiB,CAAC"}
|
||||||
8
dist/components/MaskSegmentCanvas.js
vendored
8
dist/components/MaskSegmentCanvas.js
vendored
@ -6,6 +6,7 @@ import { runOnJS } from 'react-native-reanimated';
|
|||||||
import { launchImageLibrary } from 'react-native-image-picker';
|
import { launchImageLibrary } from 'react-native-image-picker';
|
||||||
import cv from '../utils/opencvAdapter';
|
import cv from '../utils/opencvAdapter';
|
||||||
import { buildAllRegionOutlinePaths, buildRegionOutlinePathForRegion, downsampleMaskDataForPaths, extractRegionsFromMaskBufferSync, isBaseboardMaskPixel, upscaleBinaryMask, } from '../utils/maskSegmentation';
|
import { buildAllRegionOutlinePaths, buildRegionOutlinePathForRegion, downsampleMaskDataForPaths, extractRegionsFromMaskBufferSync, isBaseboardMaskPixel, upscaleBinaryMask, } from '../utils/maskSegmentation';
|
||||||
|
import { splitWallRegionsByTexture } from '../utils/wallTextureSplit';
|
||||||
import { clearDerivedImageCache, readPngBgrBuffer, prewarmPngBgrCache, resizeBgrBuffer, } from '../utils/pngImage';
|
import { clearDerivedImageCache, readPngBgrBuffer, prewarmPngBgrCache, resizeBgrBuffer, } from '../utils/pngImage';
|
||||||
import { hashUrl, resolveImageUrl } from '../utils/resolveImageUrl';
|
import { hashUrl, resolveImageUrl } from '../utils/resolveImageUrl';
|
||||||
import { compositePaintedImage } from '../utils/compositePaintedImage';
|
import { compositePaintedImage } from '../utils/compositePaintedImage';
|
||||||
@ -827,13 +828,17 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
rows: imgH,
|
rows: imgH,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const [segmentResult] = await Promise.all([
|
const [segmentResultRaw, workScaled] = await Promise.all([
|
||||||
segmentTask,
|
segmentTask,
|
||||||
workScaledTask,
|
workScaledTask,
|
||||||
]);
|
]);
|
||||||
if (isCancelled()) {
|
if (isCancelled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let segmentResult = segmentResultRaw;
|
||||||
|
if (getMaskSegmentRuntimeConfig().mask.splitWalls) {
|
||||||
|
segmentResult = splitWallRegionsByTexture(segmentResult, workScaled.buffer, segW, segH, minArea);
|
||||||
|
}
|
||||||
const paintPromise = paintLayersPromiseRef.current ?? Promise.resolve();
|
const paintPromise = paintLayersPromiseRef.current ?? Promise.resolve();
|
||||||
emitWatch('mask_sampled', { regionCount: segmentResult.regions.length });
|
emitWatch('mask_sampled', { regionCount: segmentResult.regions.length });
|
||||||
timeLog(`▶ segmentation completed, valid regions: ${segmentResult.regions.length}`);
|
timeLog(`▶ segmentation completed, valid regions: ${segmentResult.regions.length}`);
|
||||||
@ -852,6 +857,7 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
baseboardBinary: segmentResult.baseboardBinary,
|
baseboardBinary: segmentResult.baseboardBinary,
|
||||||
cols: segmentResult.segCols,
|
cols: segmentResult.segCols,
|
||||||
rows: segmentResult.segRows,
|
rows: segmentResult.segRows,
|
||||||
|
wallSubLabels: segmentResult.wallSubLabels,
|
||||||
};
|
};
|
||||||
baseboardPickMaskRef.current = null;
|
baseboardPickMaskRef.current = null;
|
||||||
kickRegionIdRef.current =
|
kickRegionIdRef.current =
|
||||||
|
|||||||
2
dist/components/MaskSegmentCanvas.js.map
vendored
2
dist/components/MaskSegmentCanvas.js.map
vendored
File diff suppressed because one or more lines are too long
12
dist/components/MaskSegmentCanvas.types.d.ts
vendored
12
dist/components/MaskSegmentCanvas.types.d.ts
vendored
@ -42,6 +42,18 @@ export type MaskSegmentConfig = {
|
|||||||
baseboardJunctionRowMarginPx?: number;
|
baseboardJunctionRowMarginPx?: number;
|
||||||
baseboardJunctionVReachPx?: number;
|
baseboardJunctionVReachPx?: number;
|
||||||
baseboardMinRunPx?: number;
|
baseboardMinRunPx?: number;
|
||||||
|
/** 在 wall 掩码内按纹理边界细分为 wall-1、wall-2… */
|
||||||
|
splitWalls?: boolean;
|
||||||
|
/** 墙壁子区最大数量 */
|
||||||
|
splitWallsMaxCount?: number;
|
||||||
|
/** 碎块最小面积比(相对 seg 总像素) */
|
||||||
|
splitWallsMinAreaRatio?: number;
|
||||||
|
/** Lab 色度 + 高频纹理特征距离平方阈值(不含亮度,削弱光影影响) */
|
||||||
|
splitWallsColorDistSq?: number;
|
||||||
|
/** 色度平滑半径(像素),仅用于纹理能量计算 */
|
||||||
|
splitWallsChromaBlurRadius?: number;
|
||||||
|
/** 低饱和(白/灰墙)判定半径,用于与有色墙强制分界 */
|
||||||
|
splitWallsNeutralChromaMax?: number;
|
||||||
};
|
};
|
||||||
export type PaintConfig = {
|
export type PaintConfig = {
|
||||||
palette?: BgrColor[];
|
palette?: BgrColor[];
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
6
dist/utils/freqLayerPrep.d.ts
vendored
6
dist/utils/freqLayerPrep.d.ts
vendored
@ -10,6 +10,12 @@ export type PaintResourceBatch = {
|
|||||||
};
|
};
|
||||||
/** OpenCV 8-bit Lab L 通道(BGR 输入,供单测与近似对照) */
|
/** OpenCV 8-bit Lab L 通道(BGR 输入,供单测与近似对照) */
|
||||||
export declare function bgrToLabL(b: number, g: number, r: number): number;
|
export declare function bgrToLabL(b: number, g: number, r: number): number;
|
||||||
|
/** BGR → 8-bit Lab(L/a/b 均映射到 0–255) */
|
||||||
|
export declare function bgrToLab(b: number, g: number, r: number): {
|
||||||
|
l: number;
|
||||||
|
a: number;
|
||||||
|
b: number;
|
||||||
|
};
|
||||||
export declare function bgrBufferToRgbaBuffer(bgr: Uint8Array, cols: number, rows: number): Uint8Array;
|
export declare function bgrBufferToRgbaBuffer(bgr: Uint8Array, cols: number, rows: number): Uint8Array;
|
||||||
export declare function releaseFreqLayerImages(layers: FreqLayerImages | null): void;
|
export declare function releaseFreqLayerImages(layers: FreqLayerImages | null): void;
|
||||||
/** 复用已上传的 BGR Mat,避免重复 bufferToMat + JS↔原生往返 */
|
/** 复用已上传的 BGR Mat,避免重复 bufferToMat + JS↔原生往返 */
|
||||||
|
|||||||
2
dist/utils/freqLayerPrep.d.ts.map
vendored
2
dist/utils/freqLayerPrep.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"freqLayerPrep.d.ts","sourceRoot":"","sources":["../../src/utils/freqLayerPrep.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAG1D,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,eAAe,CAAC;CACzB,CAAC;AAEF,6CAA6C;AAC7C,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA2BjE;AAED,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,UAAU,CAYZ;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,QAGpE;AAyDD,gDAAgD;AAChD,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAyEjC;AAED,+CAA+C;AAC/C,wBAAsB,mCAAmC,CACvD,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,GACpD,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA8BpC;AAED,kEAAkE;AAClE,wBAAsB,8BAA8B,CAClD,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAWjC;AAED,oDAAoD;AACpD,wBAAsB,0BAA0B,CAC9C,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAEzB"}
|
{"version":3,"file":"freqLayerPrep.d.ts","sourceRoot":"","sources":["../../src/utils/freqLayerPrep.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAG1D,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,eAAe,CAAC;CACzB,CAAC;AAEF,6CAA6C;AAC7C,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wCAAwC;AACxC,wBAAgB,QAAQ,CACtB,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAkCrC;AAED,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,UAAU,CAYZ;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,QAGpE;AAyDD,gDAAgD;AAChD,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAyEjC;AAED,+CAA+C;AAC/C,wBAAsB,mCAAmC,CACvD,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,GACpD,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA8BpC;AAED,kEAAkE;AAClE,wBAAsB,8BAA8B,CAClD,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAWjC;AAED,oDAAoD;AACpD,wBAAsB,0BAA0B,CAC9C,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAEzB"}
|
||||||
12
dist/utils/freqLayerPrep.js
vendored
12
dist/utils/freqLayerPrep.js
vendored
@ -2,6 +2,10 @@ import cv from './opencvAdapter';
|
|||||||
import { getMaskSegmentRuntimeConfig } from './maskSegmentRuntime';
|
import { getMaskSegmentRuntimeConfig } from './maskSegmentRuntime';
|
||||||
/** OpenCV 8-bit Lab L 通道(BGR 输入,供单测与近似对照) */
|
/** OpenCV 8-bit Lab L 通道(BGR 输入,供单测与近似对照) */
|
||||||
export function bgrToLabL(b, g, r) {
|
export function bgrToLabL(b, g, r) {
|
||||||
|
return bgrToLab(b, g, r).l;
|
||||||
|
}
|
||||||
|
/** BGR → 8-bit Lab(L/a/b 均映射到 0–255) */
|
||||||
|
export function bgrToLab(b, g, r) {
|
||||||
let rf = r / 255;
|
let rf = r / 255;
|
||||||
let gf = g / 255;
|
let gf = g / 255;
|
||||||
let bf = b / 255;
|
let bf = b / 255;
|
||||||
@ -23,7 +27,13 @@ export function bgrToLabL(b, g, r) {
|
|||||||
fy = fy > delta3 ? Math.cbrt(fy) : fy / (3 * delta * delta) + 4 / 29;
|
fy = fy > delta3 ? Math.cbrt(fy) : fy / (3 * delta * delta) + 4 / 29;
|
||||||
fz = fz > delta3 ? Math.cbrt(fz) : fz / (3 * delta * delta) + 4 / 29;
|
fz = fz > delta3 ? Math.cbrt(fz) : fz / (3 * delta * delta) + 4 / 29;
|
||||||
const L = fy * 116 - 16;
|
const L = fy * 116 - 16;
|
||||||
return Math.max(0, Math.min(255, Math.round((L * 255) / 100)));
|
const a = 500 * (fx - fy);
|
||||||
|
const bLab = 200 * (fy - fz);
|
||||||
|
return {
|
||||||
|
l: Math.max(0, Math.min(255, Math.round((L * 255) / 100))),
|
||||||
|
a: Math.max(0, Math.min(255, Math.round(a + 128))),
|
||||||
|
b: Math.max(0, Math.min(255, Math.round(bLab + 128))),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export function bgrBufferToRgbaBuffer(bgr, cols, rows) {
|
export function bgrBufferToRgbaBuffer(bgr, cols, rows) {
|
||||||
const pixelCount = cols * rows;
|
const pixelCount = cols * rows;
|
||||||
|
|||||||
2
dist/utils/freqLayerPrep.js.map
vendored
2
dist/utils/freqLayerPrep.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/utils/maskSegmentRuntime.d.ts.map
vendored
2
dist/utils/maskSegmentRuntime.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"maskSegmentRuntime.d.ts","sourceRoot":"","sources":["../../src/utils/maskSegmentRuntime.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,cAAc,EAEf,MAAM,uCAAuC,CAAC;AAC/C,WAAW;AACX,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,cAAc,CAQlD,CAAC;AAEF,aAAa;AACb,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,cAAc,CAQpD,CAAC;AAEF,UAAU;AACV,eAAO,MAAM,YAAY,EAAE,QAAQ,CAAC,cAAc,CAQjD,CAAC;AACF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,cAAc,EAAE,QAAQ,CAAC,cAAc,CAAC,CAI7E,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,cAAc,CAAiB,CAAC;AAE/E,wBAAgB,qBAAqB,CACnC,MAAM,CAAC,EAAE,cAAc,EACvB,SAAS,CAAC,EAAE,cAAc,GACzB,QAAQ,CAAC,cAAc,CAAC,CAI1B;AAWD,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,WAAW,CAkBtD,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,iBAAiB,CAOlE,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CACxC,IAAI,CACF,iBAAiB,EACf,yBAAyB,GACzB,eAAe,GACf,kBAAkB,GAClB,wBAAwB,CAC3B,CACF,GAAG;IACF,uBAAuB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,sBAAsB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACpC,cAAc,EAAE,iBAAiB,EAAE,CAAC;CAkBrC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IACnC,IAAI,EAAE,OAAO,mBAAmB,CAAC;IACjC,KAAK,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC7B,WAAW,EAAE,QAAQ,CAAC,iBAAiB,CAAC,CAAC;CAC1C,CAAC;AAMF,wBAAgB,eAAe,CAC7B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,mBAAmB,CA2C5B;AAED,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,EAAE;IAC1C,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC,GAAG,0BAA0B,CAiB7B;AAKD,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE;IAClD,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC,GAAG,0BAA0B,CAiB7B;AAED,wBAAgB,2BAA2B,IAAI,0BAA0B,CAExE;AAED,wBAAgB,6BAA6B,IAAI,0BAA0B,CAI1E"}
|
{"version":3,"file":"maskSegmentRuntime.d.ts","sourceRoot":"","sources":["../../src/utils/maskSegmentRuntime.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,cAAc,EAEf,MAAM,uCAAuC,CAAC;AAC/C,WAAW;AACX,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,cAAc,CAQlD,CAAC;AAEF,aAAa;AACb,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,cAAc,CAQpD,CAAC;AAEF,UAAU;AACV,eAAO,MAAM,YAAY,EAAE,QAAQ,CAAC,cAAc,CAQjD,CAAC;AACF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,cAAc,EAAE,QAAQ,CAAC,cAAc,CAAC,CAI7E,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,cAAc,CAAiB,CAAC;AAE/E,wBAAgB,qBAAqB,CACnC,MAAM,CAAC,EAAE,cAAc,EACvB,SAAS,CAAC,EAAE,cAAc,GACzB,QAAQ,CAAC,cAAc,CAAC,CAI1B;AAWD,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,WAAW,CAkBtD,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,iBAAiB,CAOlE,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CACxC,IAAI,CACF,iBAAiB,EACf,yBAAyB,GACzB,eAAe,GACf,kBAAkB,GAClB,wBAAwB,CAC3B,CACF,GAAG;IACF,uBAAuB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,sBAAsB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACpC,cAAc,EAAE,iBAAiB,EAAE,CAAC;CAwBrC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IACnC,IAAI,EAAE,OAAO,mBAAmB,CAAC;IACjC,KAAK,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC7B,WAAW,EAAE,QAAQ,CAAC,iBAAiB,CAAC,CAAC;CAC1C,CAAC;AAMF,wBAAgB,eAAe,CAC7B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,mBAAmB,CA0D5B;AAED,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,EAAE;IAC1C,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC,GAAG,0BAA0B,CAiB7B;AAKD,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE;IAClD,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC,GAAG,0BAA0B,CAiB7B;AAED,wBAAgB,2BAA2B,IAAI,0BAA0B,CAExE;AAED,wBAAgB,6BAA6B,IAAI,0BAA0B,CAI1E"}
|
||||||
16
dist/utils/maskSegmentRuntime.js
vendored
16
dist/utils/maskSegmentRuntime.js
vendored
@ -91,6 +91,12 @@ export const DEFAULT_MASK_CONFIG = {
|
|||||||
baseboardJunctionRowMarginPx: 1,
|
baseboardJunctionRowMarginPx: 1,
|
||||||
baseboardJunctionVReachPx: 2,
|
baseboardJunctionVReachPx: 2,
|
||||||
baseboardMinRunPx: 2,
|
baseboardMinRunPx: 2,
|
||||||
|
splitWalls: false,
|
||||||
|
splitWallsMaxCount: 8,
|
||||||
|
splitWallsMinAreaRatio: 0.002,
|
||||||
|
splitWallsColorDistSq: 1400,
|
||||||
|
splitWallsChromaBlurRadius: 5,
|
||||||
|
splitWallsNeutralChromaMax: 14,
|
||||||
};
|
};
|
||||||
function toStringSet(values) {
|
function toStringSet(values) {
|
||||||
return new Set(values ?? []);
|
return new Set(values ?? []);
|
||||||
@ -127,6 +133,16 @@ export function mergeMaskConfig(partial) {
|
|||||||
baseboardJunctionVReachPx: partial.baseboardJunctionVReachPx ??
|
baseboardJunctionVReachPx: partial.baseboardJunctionVReachPx ??
|
||||||
DEFAULT_MASK_CONFIG.baseboardJunctionVReachPx,
|
DEFAULT_MASK_CONFIG.baseboardJunctionVReachPx,
|
||||||
baseboardMinRunPx: partial.baseboardMinRunPx ?? DEFAULT_MASK_CONFIG.baseboardMinRunPx,
|
baseboardMinRunPx: partial.baseboardMinRunPx ?? DEFAULT_MASK_CONFIG.baseboardMinRunPx,
|
||||||
|
splitWalls: partial.splitWalls ?? DEFAULT_MASK_CONFIG.splitWalls,
|
||||||
|
splitWallsMaxCount: partial.splitWallsMaxCount ?? DEFAULT_MASK_CONFIG.splitWallsMaxCount,
|
||||||
|
splitWallsMinAreaRatio: partial.splitWallsMinAreaRatio ??
|
||||||
|
DEFAULT_MASK_CONFIG.splitWallsMinAreaRatio,
|
||||||
|
splitWallsColorDistSq: partial.splitWallsColorDistSq ??
|
||||||
|
DEFAULT_MASK_CONFIG.splitWallsColorDistSq,
|
||||||
|
splitWallsChromaBlurRadius: partial.splitWallsChromaBlurRadius ??
|
||||||
|
DEFAULT_MASK_CONFIG.splitWallsChromaBlurRadius,
|
||||||
|
splitWallsNeutralChromaMax: partial.splitWallsNeutralChromaMax ??
|
||||||
|
DEFAULT_MASK_CONFIG.splitWallsNeutralChromaMax,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export function createRuntimeConfig(input) {
|
export function createRuntimeConfig(input) {
|
||||||
|
|||||||
2
dist/utils/maskSegmentRuntime.js.map
vendored
2
dist/utils/maskSegmentRuntime.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/utils/maskSegmentation.d.ts
vendored
3
dist/utils/maskSegmentation.d.ts
vendored
@ -12,6 +12,8 @@ export type SegmentMaskResult = {
|
|||||||
baseboardBinary: Uint8Array;
|
baseboardBinary: Uint8Array;
|
||||||
segCols: number;
|
segCols: number;
|
||||||
segRows: number;
|
segRows: number;
|
||||||
|
/** splitWalls 时:墙像素 → 子区索引 0..N-1,非墙为 WALL_SUB_LABEL_NONE */
|
||||||
|
wallSubLabels?: Uint8Array;
|
||||||
};
|
};
|
||||||
export type SegmentRegion = {
|
export type SegmentRegion = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -95,6 +97,7 @@ export type RegionMaskData = {
|
|||||||
baseboardBinary: Uint8Array;
|
baseboardBinary: Uint8Array;
|
||||||
cols: number;
|
cols: number;
|
||||||
rows: number;
|
rows: number;
|
||||||
|
wallSubLabels?: Uint8Array;
|
||||||
};
|
};
|
||||||
/** 蒙版路径构建降采样(屏幕显示不需要分割分辨率,点击仍用全分辨率 pickMap) */
|
/** 蒙版路径构建降采样(屏幕显示不需要分割分辨率,点击仍用全分辨率 pickMap) */
|
||||||
export declare function downsampleMaskDataForPaths(maskData: RegionMaskData, maxLongSide: number): RegionMaskData;
|
export declare function downsampleMaskDataForPaths(maskData: RegionMaskData, maxLongSide: number): RegionMaskData;
|
||||||
|
|||||||
2
dist/utils/maskSegmentation.d.ts.map
vendored
2
dist/utils/maskSegmentation.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"maskSegmentation.d.ts","sourceRoot":"","sources":["../../src/utils/maskSegmentation.ts"],"names":[],"mappings":"AAAA,OAAW,EAAuB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAQ,KAAK,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAkB/D,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,eAAe,EAAE,UAAU,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,cAAc;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe;IACf,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,EAAE,CAAC;IACvC,6BAA6B;IAC7B,YAAY,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,EAAE,CAAC;IAC5C,WAAW,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,EAAE,CAAC;IAC3C,eAAe,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,EAAE,CAAC;IAC/C,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAgBF,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,EAAE,EAAE,CAQ5E;AAqcD,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,aAAa,EAAE,EACxB,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACpD,QAAQ,CAAC,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAClC,MAAM,CAUR;AA2CD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,aAAa,EAAE,EACxB,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBrB;AAkND,0CAA0C;AAC1C,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACpD,OAAO,EAAE;IACP,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IAC1C,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IAC1C,KAAK,EAAE,MAAM,OAAO,CAAC;CACtB,EACD,QAAQ,SAA8B,GACrC,IAAI,CA2BN;AAED,+BAA+B;AAC/B,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACpD,OAAO,EAAE;IACP,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IAC1C,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IAC1C,KAAK,EAAE,MAAM,OAAO,CAAC;CACtB,EACD,QAAQ,SAA8B,GACrC,IAAI,CA8BN;AAkCD,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,UAAU,CAAC;IACnB,eAAe,EAAE,UAAU,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,+CAA+C;AAC/C,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,MAAM,GAClB,cAAc,CAgChB;AAED,uDAAuD;AACvD,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,aAAa,EAAE,EACxB,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAqFrB;AAwGD,KAAK,SAAS,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAgQ1C,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,UAAU,CAEZ;AAED,iDAAiD;AACjD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,UAAU,CAYZ;AAcD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,eAAe,CAAC,EAAE,UAAU,GAAG,IAAI,GAClC,OAAO,CAuBT;AAED,OAAO,EAAE,sBAAsB,IAAI,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEnF,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,2CAA2C;AAC3C,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAEzE;AAunBD,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE;IACR,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACvB,GACA,OAAO,CAAC,iBAAiB,CAAC,CAE5B;AAED,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE;IACR,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACvB,GACA,iBAAiB,CAiHnB;AAED,mDAAmD;AACnD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,UAAU,EACnB,OAAO,EAAE;IACP,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACvB,GACA,OAAO,CAAC,aAAa,EAAE,CAAC,CAI1B"}
|
{"version":3,"file":"maskSegmentation.d.ts","sourceRoot":"","sources":["../../src/utils/maskSegmentation.ts"],"names":[],"mappings":"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"}
|
||||||
29
dist/utils/maskSegmentation.js
vendored
29
dist/utils/maskSegmentation.js
vendored
@ -374,12 +374,13 @@ export function buildRegionOutlinePathForRegion(regionId, regions, maskData, rec
|
|||||||
return buildRegionOutlinePathFromBinary(binary, cols, rows, rect, seedPx);
|
return buildRegionOutlinePathFromBinary(binary, cols, rows, rect, seedPx);
|
||||||
}
|
}
|
||||||
function extractRegionBinaries(regions, maskData) {
|
function extractRegionBinaries(regions, maskData) {
|
||||||
const { labels, baseboardBinary, cols, rows } = maskData;
|
const { labels, baseboardBinary, cols, rows, wallSubLabels } = maskData;
|
||||||
const size = cols * rows;
|
const size = cols * rows;
|
||||||
const binaries = new Map();
|
const binaries = new Map();
|
||||||
const semanticColors = getMaskSegmentRuntimeConfig().mask.semanticColors;
|
const semanticColors = getMaskSegmentRuntimeConfig().mask.semanticColors;
|
||||||
const regionIdBySemantic = new Int32Array(semanticColors.length);
|
const regionIdBySemantic = new Int32Array(semanticColors.length);
|
||||||
regionIdBySemantic.fill(-1);
|
regionIdBySemantic.fill(-1);
|
||||||
|
const wallSubRegionIds = new Map();
|
||||||
let baseboardRegionId = null;
|
let baseboardRegionId = null;
|
||||||
for (const reg of regions) {
|
for (const reg of regions) {
|
||||||
binaries.set(reg.id, new Uint8Array(size));
|
binaries.set(reg.id, new Uint8Array(size));
|
||||||
@ -387,17 +388,33 @@ function extractRegionBinaries(regions, maskData) {
|
|||||||
baseboardRegionId = reg.id;
|
baseboardRegionId = reg.id;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const wallMatch = /^wall-(\d+)$/.exec(reg.name);
|
||||||
|
if (wallMatch && wallSubLabels) {
|
||||||
|
wallSubRegionIds.set(Number(wallMatch[1]) - 1, reg.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const semanticIndex = semanticColors.findIndex(entry => entry.name === reg.name);
|
const semanticIndex = semanticColors.findIndex(entry => entry.name === reg.name);
|
||||||
if (semanticIndex >= 0) {
|
if (semanticIndex >= 0) {
|
||||||
regionIdBySemantic[semanticIndex] = reg.id;
|
regionIdBySemantic[semanticIndex] = reg.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const semanticCount = semanticColors.length;
|
const semanticCount = semanticColors.length;
|
||||||
|
const wallIdx = semanticColors.findIndex(entry => entry.name === 'wall');
|
||||||
for (let i = 0; i < size; i++) {
|
for (let i = 0; i < size; i++) {
|
||||||
if (baseboardRegionId != null && baseboardBinary[i] > 0) {
|
if (baseboardRegionId != null && baseboardBinary[i] > 0) {
|
||||||
binaries.get(baseboardRegionId)[i] = 255;
|
binaries.get(baseboardRegionId)[i] = 255;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (wallSubLabels && wallIdx >= 0 && labels[i] === wallIdx) {
|
||||||
|
const subIdx = wallSubLabels[i];
|
||||||
|
if (subIdx !== 255) {
|
||||||
|
const regionId = wallSubRegionIds.get(subIdx);
|
||||||
|
if (regionId !== undefined) {
|
||||||
|
binaries.get(regionId)[i] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const semanticIndex = labels[i];
|
const semanticIndex = labels[i];
|
||||||
if (semanticIndex < semanticCount && regionIdBySemantic[semanticIndex] >= 0) {
|
if (semanticIndex < semanticCount && regionIdBySemantic[semanticIndex] >= 0) {
|
||||||
binaries.get(regionIdBySemantic[semanticIndex])[i] = 255;
|
binaries.get(regionIdBySemantic[semanticIndex])[i] = 255;
|
||||||
@ -667,7 +684,7 @@ function appendRunRectToBuilder(runStart, runEnd, y, cols, rows, rect, builder,
|
|||||||
}
|
}
|
||||||
/** 蒙版路径构建降采样(屏幕显示不需要分割分辨率,点击仍用全分辨率 pickMap) */
|
/** 蒙版路径构建降采样(屏幕显示不需要分割分辨率,点击仍用全分辨率 pickMap) */
|
||||||
export function downsampleMaskDataForPaths(maskData, maxLongSide) {
|
export function downsampleMaskDataForPaths(maskData, maxLongSide) {
|
||||||
const { labels, baseboardBinary, cols, rows } = maskData;
|
const { labels, baseboardBinary, cols, rows, wallSubLabels } = maskData;
|
||||||
const longSide = Math.max(cols, rows);
|
const longSide = Math.max(cols, rows);
|
||||||
if (longSide <= maxLongSide) {
|
if (longSide <= maxLongSide) {
|
||||||
return maskData;
|
return maskData;
|
||||||
@ -677,6 +694,10 @@ export function downsampleMaskDataForPaths(maskData, maxLongSide) {
|
|||||||
const dstRows = Math.max(1, Math.floor(rows * scale));
|
const dstRows = Math.max(1, Math.floor(rows * scale));
|
||||||
const outLabels = new Uint8Array(dstCols * dstRows);
|
const outLabels = new Uint8Array(dstCols * dstRows);
|
||||||
const outBaseboard = new Uint8Array(dstCols * dstRows);
|
const outBaseboard = new Uint8Array(dstCols * dstRows);
|
||||||
|
const outWallSub = wallSubLabels != null ? new Uint8Array(dstCols * dstRows) : undefined;
|
||||||
|
if (outWallSub) {
|
||||||
|
outWallSub.fill(255);
|
||||||
|
}
|
||||||
for (let y = 0; y < dstRows; y++) {
|
for (let y = 0; y < dstRows; y++) {
|
||||||
const sy = Math.min(rows - 1, Math.floor((y * rows) / dstRows));
|
const sy = Math.min(rows - 1, Math.floor((y * rows) / dstRows));
|
||||||
const srcRow = sy * cols;
|
const srcRow = sy * cols;
|
||||||
@ -687,6 +708,9 @@ export function downsampleMaskDataForPaths(maskData, maxLongSide) {
|
|||||||
const di = dstRow + x;
|
const di = dstRow + x;
|
||||||
outLabels[di] = labels[si];
|
outLabels[di] = labels[si];
|
||||||
outBaseboard[di] = baseboardBinary[si];
|
outBaseboard[di] = baseboardBinary[si];
|
||||||
|
if (outWallSub && wallSubLabels) {
|
||||||
|
outWallSub[di] = wallSubLabels[si];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -694,6 +718,7 @@ export function downsampleMaskDataForPaths(maskData, maxLongSide) {
|
|||||||
baseboardBinary: outBaseboard,
|
baseboardBinary: outBaseboard,
|
||||||
cols: dstCols,
|
cols: dstCols,
|
||||||
rows: dstRows,
|
rows: dstRows,
|
||||||
|
wallSubLabels: outWallSub,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/** 单次扫描构建所有分区 Skia 蒙版路径(单 label pass,避免每像素 × 语义数循环) */
|
/** 单次扫描构建所有分区 Skia 蒙版路径(单 label pass,避免每像素 × 语义数循环) */
|
||||||
|
|||||||
2
dist/utils/maskSegmentation.js.map
vendored
2
dist/utils/maskSegmentation.js.map
vendored
File diff suppressed because one or more lines are too long
9
dist/utils/wallTextureSplit.d.ts
vendored
Normal file
9
dist/utils/wallTextureSplit.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { SegmentMaskResult } from './maskSegmentation';
|
||||||
|
/** 非墙像素在 wallSubLabels 中的占位值 */
|
||||||
|
export declare const WALL_SUB_LABEL_NONE = 255;
|
||||||
|
/**
|
||||||
|
* 在语义分割完成后,将 wall 区域按原图纹理特征细分为 wall-1、wall-2…
|
||||||
|
*/
|
||||||
|
export declare function splitWallRegionsByTexture(result: SegmentMaskResult, originBgr: Uint8Array, cols: number, rows: number, minArea: number): SegmentMaskResult;
|
||||||
|
export declare function isWallSubRegionName(name: string): boolean;
|
||||||
|
//# sourceMappingURL=wallTextureSplit.d.ts.map
|
||||||
1
dist/utils/wallTextureSplit.d.ts.map
vendored
Normal file
1
dist/utils/wallTextureSplit.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"wallTextureSplit.d.ts","sourceRoot":"","sources":["../../src/utils/wallTextureSplit.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAiB,MAAM,oBAAoB,CAAC;AAG3E,gCAAgC;AAChC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAofvC;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,iBAAiB,EACzB,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,iBAAiB,CAqJnB;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzD"}
|
||||||
479
dist/utils/wallTextureSplit.js
vendored
Normal file
479
dist/utils/wallTextureSplit.js
vendored
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
import { bgrToLab } from './freqLayerPrep';
|
||||||
|
import { getMaskSegmentRuntimeConfig } from './maskSegmentRuntime';
|
||||||
|
import { getSemanticColorByName } from './maskSemanticPalette';
|
||||||
|
/** 非墙像素在 wallSubLabels 中的占位值 */
|
||||||
|
export const WALL_SUB_LABEL_NONE = 255;
|
||||||
|
function maskCfg() {
|
||||||
|
return getMaskSegmentRuntimeConfig().mask;
|
||||||
|
}
|
||||||
|
function bboxToPolygon(bbox) {
|
||||||
|
return [
|
||||||
|
{ x: bbox.x, y: bbox.y },
|
||||||
|
{ x: bbox.x + bbox.w, y: bbox.y },
|
||||||
|
{ x: bbox.x + bbox.w, y: bbox.y + bbox.h },
|
||||||
|
{ x: bbox.x, y: bbox.y + bbox.h },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function computeLabChromaMaps(originBgr, cols, rows) {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const aMap = new Uint8Array(pixelCount);
|
||||||
|
const bMap = new Uint8Array(pixelCount);
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const s = i * 3;
|
||||||
|
const lab = bgrToLab(originBgr[s], originBgr[s + 1], originBgr[s + 2]);
|
||||||
|
aMap[i] = lab.a;
|
||||||
|
bMap[i] = lab.b;
|
||||||
|
}
|
||||||
|
return { aMap, bMap };
|
||||||
|
}
|
||||||
|
function chromaMag(a, b) {
|
||||||
|
const da = a - 128;
|
||||||
|
const db = b - 128;
|
||||||
|
return Math.sqrt(da * da + db * db);
|
||||||
|
}
|
||||||
|
function chromaDistSq(a0, b0, a1, b1) {
|
||||||
|
const da = a0 - a1;
|
||||||
|
const db = b0 - b1;
|
||||||
|
return da * da + db * db;
|
||||||
|
}
|
||||||
|
/** 白/灰墙与蓝/有色墙、或两种不同色相墙之间强制视为材质边界 */
|
||||||
|
function isCrossMaterialBoundary(a0, b0, a1, b1, neutralChromaMax) {
|
||||||
|
const m0 = chromaMag(a0, b0);
|
||||||
|
const m1 = chromaMag(a1, b1);
|
||||||
|
const neutralGate = neutralChromaMax * 2.2;
|
||||||
|
if (m0 <= neutralChromaMax && m1 > neutralGate)
|
||||||
|
return true;
|
||||||
|
if (m1 <= neutralChromaMax && m0 > neutralGate)
|
||||||
|
return true;
|
||||||
|
if (m0 > neutralChromaMax && m1 > neutralChromaMax) {
|
||||||
|
const hue0 = Math.atan2(b0 - 128, a0 - 128);
|
||||||
|
const hue1 = Math.atan2(b1 - 128, a1 - 128);
|
||||||
|
let dh = Math.abs(hue0 - hue1);
|
||||||
|
if (dh > Math.PI)
|
||||||
|
dh = 2 * Math.PI - dh;
|
||||||
|
if (dh > Math.PI / 4)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function canMergeWallPixels(refA, refB, na, nb, distSqThreshold, neutralChromaMax) {
|
||||||
|
if (isCrossMaterialBoundary(refA, refB, na, nb, neutralChromaMax)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return chromaDistSq(refA, refB, na, nb) <= distSqThreshold;
|
||||||
|
}
|
||||||
|
function findWallSemanticIndex() {
|
||||||
|
const colors = maskCfg().semanticColors;
|
||||||
|
return colors.findIndex(entry => entry.name === 'wall');
|
||||||
|
}
|
||||||
|
function isWallPixel(labels, baseboardBinary, wallIdx, i) {
|
||||||
|
if (baseboardBinary[i])
|
||||||
|
return false;
|
||||||
|
return labels[i] === wallIdx;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 4-连通区域生长:与连通域色度均值比较,避免链式桥接;中性/有色墙强制分界。
|
||||||
|
*/
|
||||||
|
function labelWallComponents(labels, baseboardBinary, wallIdx, aMap, bMap, cols, rows, distSqThreshold, neutralChromaMax) {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const compLabels = new Int32Array(pixelCount);
|
||||||
|
compLabels.fill(-1);
|
||||||
|
let compCount = 0;
|
||||||
|
const queue = new Int32Array(pixelCount);
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
const i = y * cols + x;
|
||||||
|
if (!isWallPixel(labels, baseboardBinary, wallIdx, i))
|
||||||
|
continue;
|
||||||
|
if (compLabels[i] >= 0)
|
||||||
|
continue;
|
||||||
|
const compId = compCount;
|
||||||
|
compCount += 1;
|
||||||
|
const seedA = aMap[i];
|
||||||
|
const seedB = bMap[i];
|
||||||
|
let sumA = seedA;
|
||||||
|
let sumB = seedB;
|
||||||
|
let count = 1;
|
||||||
|
let head = 0;
|
||||||
|
let tail = 0;
|
||||||
|
queue[tail++] = i;
|
||||||
|
compLabels[i] = compId;
|
||||||
|
while (head < tail) {
|
||||||
|
const ci = queue[head++];
|
||||||
|
const cx = ci % cols;
|
||||||
|
const cy = (ci / cols) | 0;
|
||||||
|
const meanA = sumA / count;
|
||||||
|
const meanB = sumB / count;
|
||||||
|
const neighbors = [ci - 1, ci + 1, ci - cols, ci + cols];
|
||||||
|
for (const ni of neighbors) {
|
||||||
|
if (ni < 0 || ni >= pixelCount)
|
||||||
|
continue;
|
||||||
|
const nx = ni % cols;
|
||||||
|
if (Math.abs(nx - cx) > 1)
|
||||||
|
continue;
|
||||||
|
if (!isWallPixel(labels, baseboardBinary, wallIdx, ni))
|
||||||
|
continue;
|
||||||
|
if (compLabels[ni] >= 0)
|
||||||
|
continue;
|
||||||
|
const na = aMap[ni];
|
||||||
|
const nb = bMap[ni];
|
||||||
|
const stepOk = canMergeWallPixels(aMap[ci], bMap[ci], na, nb, distSqThreshold * 1.8, neutralChromaMax);
|
||||||
|
const meanOk = canMergeWallPixels(meanA, meanB, na, nb, distSqThreshold, neutralChromaMax);
|
||||||
|
const seedOk = canMergeWallPixels(seedA, seedB, na, nb, distSqThreshold * 3, neutralChromaMax);
|
||||||
|
if (!stepOk || !meanOk || !seedOk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
compLabels[ni] = compId;
|
||||||
|
sumA += na;
|
||||||
|
sumB += nb;
|
||||||
|
count += 1;
|
||||||
|
queue[tail++] = ni;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { compLabels, compCount };
|
||||||
|
}
|
||||||
|
function computeComponentStats(compLabels, compCount, cols, rows) {
|
||||||
|
const stats = Array.from({ length: compCount }, (_, label) => ({
|
||||||
|
label,
|
||||||
|
area: 0,
|
||||||
|
bbox: { x: cols, y: rows, w: 0, h: 0 },
|
||||||
|
}));
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
const i = y * cols + x;
|
||||||
|
const comp = compLabels[i];
|
||||||
|
if (comp < 0)
|
||||||
|
continue;
|
||||||
|
const s = stats[comp];
|
||||||
|
s.area += 1;
|
||||||
|
if (x < s.bbox.x)
|
||||||
|
s.bbox.x = x;
|
||||||
|
if (y < s.bbox.y)
|
||||||
|
s.bbox.y = y;
|
||||||
|
const right = x + 1;
|
||||||
|
const bottom = y + 1;
|
||||||
|
if (right > s.bbox.x + s.bbox.w)
|
||||||
|
s.bbox.w = right - s.bbox.x;
|
||||||
|
if (bottom > s.bbox.y + s.bbox.h)
|
||||||
|
s.bbox.h = bottom - s.bbox.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
function computeComponentChromaMeans(compLabels, aMap, bMap, compCount, cols, rows) {
|
||||||
|
const sumA = new Float64Array(compCount);
|
||||||
|
const sumB = new Float64Array(compCount);
|
||||||
|
const counts = new Float64Array(compCount);
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const comp = compLabels[i];
|
||||||
|
if (comp < 0)
|
||||||
|
continue;
|
||||||
|
sumA[comp] += aMap[i];
|
||||||
|
sumB[comp] += bMap[i];
|
||||||
|
counts[comp] += 1;
|
||||||
|
}
|
||||||
|
const meanA = new Float64Array(compCount);
|
||||||
|
const meanB = new Float64Array(compCount);
|
||||||
|
for (let c = 0; c < compCount; c++) {
|
||||||
|
if (counts[c] > 0) {
|
||||||
|
meanA[c] = sumA[c] / counts[c];
|
||||||
|
meanB[c] = sumB[c] / counts[c];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
meanA[c] = 128;
|
||||||
|
meanB[c] = 128;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { meanA, meanB };
|
||||||
|
}
|
||||||
|
function mergeSmallComponents(compLabels, stats, aMap, bMap, cols, rows, minArea, distSqThreshold, neutralChromaMax) {
|
||||||
|
const compCount = stats.length;
|
||||||
|
const { meanA, meanB } = computeComponentChromaMeans(compLabels, aMap, bMap, compCount, cols, rows);
|
||||||
|
const adjacency = new Map();
|
||||||
|
const addEdge = (a, b) => {
|
||||||
|
if (a === b || a < 0 || b < 0)
|
||||||
|
return;
|
||||||
|
let map = adjacency.get(a);
|
||||||
|
if (!map) {
|
||||||
|
map = new Map();
|
||||||
|
adjacency.set(a, map);
|
||||||
|
}
|
||||||
|
map.set(b, (map.get(b) ?? 0) + 1);
|
||||||
|
};
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
const i = y * cols + x;
|
||||||
|
const a = compLabels[i];
|
||||||
|
if (a < 0)
|
||||||
|
continue;
|
||||||
|
if (x + 1 < cols) {
|
||||||
|
const b = compLabels[i + 1];
|
||||||
|
if (b >= 0)
|
||||||
|
addEdge(a, b);
|
||||||
|
}
|
||||||
|
if (y + 1 < rows) {
|
||||||
|
const b = compLabels[i + cols];
|
||||||
|
if (b >= 0)
|
||||||
|
addEdge(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const remap = new Int32Array(compCount);
|
||||||
|
for (let i = 0; i < compCount; i++)
|
||||||
|
remap[i] = i;
|
||||||
|
const find = (x) => {
|
||||||
|
while (remap[x] !== x) {
|
||||||
|
remap[x] = remap[remap[x]];
|
||||||
|
x = remap[x];
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
const union = (a, b) => {
|
||||||
|
const ra = find(a);
|
||||||
|
const rb = find(b);
|
||||||
|
if (ra === rb)
|
||||||
|
return;
|
||||||
|
const areaA = stats[ra].area;
|
||||||
|
const areaB = stats[rb].area;
|
||||||
|
if (areaA >= areaB) {
|
||||||
|
remap[rb] = ra;
|
||||||
|
stats[ra].area += stats[rb].area;
|
||||||
|
stats[rb].area = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
remap[ra] = rb;
|
||||||
|
stats[rb].area += stats[ra].area;
|
||||||
|
stats[ra].area = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (let c = 0; c < compCount; c++) {
|
||||||
|
if (stats[c].area >= minArea)
|
||||||
|
continue;
|
||||||
|
const neighbors = adjacency.get(c);
|
||||||
|
if (!neighbors || neighbors.size === 0)
|
||||||
|
continue;
|
||||||
|
let bestNeighbor = -1;
|
||||||
|
let bestBorder = 0;
|
||||||
|
for (const [nb, border] of neighbors) {
|
||||||
|
if (border > bestBorder) {
|
||||||
|
bestBorder = border;
|
||||||
|
bestNeighbor = nb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestNeighbor >= 0) {
|
||||||
|
if (!canMergeWallPixels(meanA[c], meanB[c], meanA[bestNeighbor], meanB[bestNeighbor], distSqThreshold, neutralChromaMax)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
union(c, bestNeighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const c = compLabels[i];
|
||||||
|
if (c < 0)
|
||||||
|
continue;
|
||||||
|
compLabels[i] = find(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function relabelComponentsContiguous(compLabels, cols, rows) {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const remap = new Map();
|
||||||
|
const out = new Int32Array(pixelCount);
|
||||||
|
out.fill(-1);
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const c = compLabels[i];
|
||||||
|
if (c < 0)
|
||||||
|
continue;
|
||||||
|
let next = remap.get(c);
|
||||||
|
if (next === undefined) {
|
||||||
|
next = remap.size;
|
||||||
|
remap.set(c, next);
|
||||||
|
}
|
||||||
|
out[i] = next;
|
||||||
|
}
|
||||||
|
const compCount = remap.size;
|
||||||
|
const stats = computeComponentStats(out, compCount, cols, rows);
|
||||||
|
return { labels: out, compCount, stats };
|
||||||
|
}
|
||||||
|
function buildPickMapAfterWallSplit(labels, baseboardBinary, wallIdx, wallSubLabels, indexToName, nameToId, cols, rows) {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const pick = new Uint8Array(pixelCount);
|
||||||
|
const baseboardName = 'baseboard';
|
||||||
|
const baseboardId = nameToId.get(baseboardName);
|
||||||
|
const baseboardCode = baseboardId === undefined ? 0 : baseboardId + 1;
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
if (baseboardBinary[i]) {
|
||||||
|
if (baseboardCode > 0) {
|
||||||
|
pick[i] = baseboardCode;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (labels[i] === wallIdx && wallSubLabels[i] !== WALL_SUB_LABEL_NONE) {
|
||||||
|
const wallName = `wall-${wallSubLabels[i] + 1}`;
|
||||||
|
const regionId = nameToId.get(wallName);
|
||||||
|
if (regionId !== undefined) {
|
||||||
|
pick[i] = regionId + 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const semanticIndex = labels[i];
|
||||||
|
if (semanticIndex === 255)
|
||||||
|
continue;
|
||||||
|
const name = indexToName[semanticIndex];
|
||||||
|
if (!name)
|
||||||
|
continue;
|
||||||
|
const regionId = nameToId.get(name);
|
||||||
|
if (regionId !== undefined) {
|
||||||
|
pick[i] = regionId + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pick;
|
||||||
|
}
|
||||||
|
function dilatePickBuffer1px(pick, cols, rows) {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const dst = new Uint8Array(pixelCount);
|
||||||
|
dst.set(pick);
|
||||||
|
for (let y = 1; y < rows - 1; y++) {
|
||||||
|
for (let x = 1; x < cols - 1; x++) {
|
||||||
|
const i = y * cols + x;
|
||||||
|
if (pick[i] !== 0)
|
||||||
|
continue;
|
||||||
|
const n = [
|
||||||
|
pick[(y - 1) * cols + (x - 1)],
|
||||||
|
pick[(y - 1) * cols + x],
|
||||||
|
pick[(y - 1) * cols + (x + 1)],
|
||||||
|
pick[y * cols + (x - 1)],
|
||||||
|
pick[y * cols + (x + 1)],
|
||||||
|
pick[(y + 1) * cols + (x - 1)],
|
||||||
|
pick[(y + 1) * cols + x],
|
||||||
|
pick[(y + 1) * cols + (x + 1)],
|
||||||
|
];
|
||||||
|
const counts = {};
|
||||||
|
for (let k = 0; k < 8; k++) {
|
||||||
|
const code = n[k];
|
||||||
|
if (code !== 0) {
|
||||||
|
counts[code] = (counts[code] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const codeStr of Object.keys(counts)) {
|
||||||
|
const code = Number(codeStr);
|
||||||
|
if (counts[code] >= 4) {
|
||||||
|
dst[i] = code;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 在语义分割完成后,将 wall 区域按原图纹理特征细分为 wall-1、wall-2…
|
||||||
|
*/
|
||||||
|
export function splitWallRegionsByTexture(result, originBgr, cols, rows, minArea) {
|
||||||
|
const cfg = maskCfg();
|
||||||
|
if (!cfg.splitWalls) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const wallRegion = result.regions.find(reg => reg.name === 'wall');
|
||||||
|
if (!wallRegion) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const wallIdx = findWallSemanticIndex();
|
||||||
|
if (wallIdx < 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const { labels, baseboardBinary, regions } = result;
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
if (originBgr.length < pixelCount * 3) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const { aMap: rawA, bMap: rawB } = computeLabChromaMaps(originBgr, cols, rows);
|
||||||
|
const distSqThreshold = cfg.splitWallsColorDistSq;
|
||||||
|
const neutralChromaMax = cfg.splitWallsNeutralChromaMax;
|
||||||
|
const minAreaFloor = Math.max(minArea, Math.floor(cols * rows * cfg.splitWallsMinAreaRatio));
|
||||||
|
const { compLabels: rawCompLabels, compCount: rawCount } = labelWallComponents(labels, baseboardBinary, wallIdx, rawA, rawB, cols, rows, distSqThreshold, neutralChromaMax);
|
||||||
|
if (rawCount === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
let stats = computeComponentStats(rawCompLabels, rawCount, cols, rows);
|
||||||
|
mergeSmallComponents(rawCompLabels, stats, rawA, rawB, cols, rows, minAreaFloor, distSqThreshold, neutralChromaMax);
|
||||||
|
const { labels: finalCompLabels, compCount, stats: finalStats } = relabelComponentsContiguous(rawCompLabels, cols, rows);
|
||||||
|
if (compCount === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// 按面积降序,截断至 maxCount
|
||||||
|
const ranked = finalStats
|
||||||
|
.map((s, idx) => ({ ...s, origIdx: idx }))
|
||||||
|
.filter(s => s.area > 0)
|
||||||
|
.sort((a, b) => b.area - a.area)
|
||||||
|
.slice(0, cfg.splitWallsMaxCount);
|
||||||
|
const rankMap = new Map();
|
||||||
|
ranked.forEach((s, rank) => {
|
||||||
|
rankMap.set(s.origIdx, rank);
|
||||||
|
});
|
||||||
|
const wallSubLabels = new Uint8Array(pixelCount);
|
||||||
|
wallSubLabels.fill(WALL_SUB_LABEL_NONE);
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const c = finalCompLabels[i];
|
||||||
|
if (c < 0)
|
||||||
|
continue;
|
||||||
|
const rank = rankMap.get(c);
|
||||||
|
if (rank === undefined)
|
||||||
|
continue;
|
||||||
|
wallSubLabels[i] = rank;
|
||||||
|
}
|
||||||
|
const wallRef = getSemanticColorByName('wall');
|
||||||
|
const wallHex = wallRef?.hex ?? wallRegion.hex;
|
||||||
|
const wallColor = wallRef?.bgr ?? wallRegion.color;
|
||||||
|
const nonWallRegions = regions.filter(reg => reg.name !== 'wall');
|
||||||
|
const wallSubRegions = ranked.map((s, rank) => {
|
||||||
|
const bbox = s.bbox;
|
||||||
|
const poly = bboxToPolygon(bbox);
|
||||||
|
return {
|
||||||
|
id: 0,
|
||||||
|
name: `wall-${rank + 1}`,
|
||||||
|
hex: wallHex,
|
||||||
|
color: { ...wallColor },
|
||||||
|
polygons: [poly],
|
||||||
|
outlinePolygons: [poly],
|
||||||
|
bbox,
|
||||||
|
area: s.area,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const mergedRegions = [...nonWallRegions, ...wallSubRegions];
|
||||||
|
mergedRegions.sort((a, b) => b.area - a.area);
|
||||||
|
mergedRegions.forEach((reg, index) => {
|
||||||
|
reg.id = index;
|
||||||
|
});
|
||||||
|
const nameToId = new Map(mergedRegions.map(reg => [reg.name, reg.id]));
|
||||||
|
const indexToName = cfg.semanticColors.map(entry => entry.name);
|
||||||
|
const pickRaw = buildPickMapAfterWallSplit(labels, baseboardBinary, wallIdx, wallSubLabels, indexToName, nameToId, cols, rows);
|
||||||
|
const pickBuffer = dilatePickBuffer1px(pickRaw, cols, rows);
|
||||||
|
for (const reg of mergedRegions) {
|
||||||
|
if (!/^wall-\d+$/.test(reg.name))
|
||||||
|
continue;
|
||||||
|
const subIdx = Number(reg.name.slice(5)) - 1;
|
||||||
|
let area = 0;
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
if (wallSubLabels[i] === subIdx)
|
||||||
|
area += 1;
|
||||||
|
}
|
||||||
|
reg.area = area;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
regions: mergedRegions,
|
||||||
|
pickMap: { buffer: pickBuffer, cols, rows },
|
||||||
|
labels,
|
||||||
|
baseboardBinary,
|
||||||
|
segCols: cols,
|
||||||
|
segRows: rows,
|
||||||
|
wallSubLabels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function isWallSubRegionName(name) {
|
||||||
|
return /^wall-\d+$/.test(name);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=wallTextureSplit.js.map
|
||||||
1
dist/utils/wallTextureSplit.js.map
vendored
Normal file
1
dist/utils/wallTextureSplit.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -121,6 +121,7 @@ function App(): React.JSX.Element {
|
|||||||
|
|
||||||
// Demo 模式
|
// Demo 模式
|
||||||
const [useCustomColors, setUseCustomColors] = useState(false);
|
const [useCustomColors, setUseCustomColors] = useState(false);
|
||||||
|
const [splitWalls, setSplitWalls] = useState(false);
|
||||||
const [pipelinePreset, setPipelinePreset] = useState<PipelinePreset>('medium');
|
const [pipelinePreset, setPipelinePreset] = useState<PipelinePreset>('medium');
|
||||||
const [groupIndex, setGroupIndex] = useState(0);
|
const [groupIndex, setGroupIndex] = useState(0);
|
||||||
|
|
||||||
@ -371,13 +372,22 @@ function App(): React.JSX.Element {
|
|||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
|
<Text style={styles.modeDivider}>|</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.modeChip, splitWalls && styles.modeChipActive]}
|
||||||
|
onPress={() => setSplitWalls(v => !v)}
|
||||||
|
>
|
||||||
|
<Text style={[styles.modeChipText, splitWalls && styles.modeChipTextActive]}>
|
||||||
|
墙壁细分
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 画布 */}
|
{/* 画布 */}
|
||||||
<View style={styles.canvasHost}>
|
<View style={styles.canvasHost}>
|
||||||
<MaskSegmentCanvas
|
<MaskSegmentCanvas
|
||||||
key={`image-group-${groupIndex}`}
|
key={`image-group-${groupIndex}-split-${splitWalls ? 1 : 0}`}
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
style={styles.canvas}
|
style={styles.canvas}
|
||||||
originUrl={imagePaths.origin}
|
originUrl={imagePaths.origin}
|
||||||
@ -388,6 +398,7 @@ function App(): React.JSX.Element {
|
|||||||
maskConfig={{
|
maskConfig={{
|
||||||
...DEFAULT_MASK_CONFIG,
|
...DEFAULT_MASK_CONFIG,
|
||||||
maxRegionColors: 6,
|
maxRegionColors: 6,
|
||||||
|
splitWalls,
|
||||||
}}
|
}}
|
||||||
paintConfig={{
|
paintConfig={{
|
||||||
...DEFAULT_PAINT_CONFIG,
|
...DEFAULT_PAINT_CONFIG,
|
||||||
|
|||||||
@ -28,8 +28,10 @@ import {
|
|||||||
extractRegionsFromMaskBufferSync,
|
extractRegionsFromMaskBufferSync,
|
||||||
isBaseboardMaskPixel,
|
isBaseboardMaskPixel,
|
||||||
upscaleBinaryMask,
|
upscaleBinaryMask,
|
||||||
|
type RegionMaskData,
|
||||||
type SegmentRegion,
|
type SegmentRegion,
|
||||||
} from '../utils/maskSegmentation';
|
} from '../utils/maskSegmentation';
|
||||||
|
import { splitWallRegionsByTexture } from '../utils/wallTextureSplit';
|
||||||
import {
|
import {
|
||||||
clearDerivedImageCache,
|
clearDerivedImageCache,
|
||||||
readPngBgrBuffer,
|
readPngBgrBuffer,
|
||||||
@ -809,12 +811,7 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
cols: number;
|
cols: number;
|
||||||
rows: number;
|
rows: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const regionMaskDataRef = useRef<{
|
const regionMaskDataRef = useRef<RegionMaskData | null>(null);
|
||||||
labels: Uint8Array;
|
|
||||||
baseboardBinary: Uint8Array;
|
|
||||||
cols: number;
|
|
||||||
rows: number;
|
|
||||||
} | null>(null);
|
|
||||||
const workBufferRef = useRef<WorkScaledBgr | null>(null);
|
const workBufferRef = useRef<WorkScaledBgr | null>(null);
|
||||||
const paintLayersPromiseRef = useRef<Promise<void> | null>(null);
|
const paintLayersPromiseRef = useRef<Promise<void> | null>(null);
|
||||||
const loadPaintLayersRef = useRef<() => Promise<void>>(() => Promise.resolve());
|
const loadPaintLayersRef = useRef<() => Promise<void>>(() => Promise.resolve());
|
||||||
@ -1216,13 +1213,24 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const [segmentResult] = await Promise.all([
|
const [segmentResultRaw, workScaled] = await Promise.all([
|
||||||
segmentTask,
|
segmentTask,
|
||||||
workScaledTask,
|
workScaledTask,
|
||||||
]);
|
]);
|
||||||
if (isCancelled()) {
|
if (isCancelled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let segmentResult = segmentResultRaw;
|
||||||
|
if (getMaskSegmentRuntimeConfig().mask.splitWalls) {
|
||||||
|
segmentResult = splitWallRegionsByTexture(
|
||||||
|
segmentResult,
|
||||||
|
workScaled.buffer,
|
||||||
|
segW,
|
||||||
|
segH,
|
||||||
|
minArea,
|
||||||
|
);
|
||||||
|
}
|
||||||
const paintPromise =
|
const paintPromise =
|
||||||
paintLayersPromiseRef.current ?? Promise.resolve();
|
paintLayersPromiseRef.current ?? Promise.resolve();
|
||||||
emitWatch('mask_sampled', { regionCount: segmentResult.regions.length });
|
emitWatch('mask_sampled', { regionCount: segmentResult.regions.length });
|
||||||
@ -1246,6 +1254,7 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
baseboardBinary: segmentResult.baseboardBinary,
|
baseboardBinary: segmentResult.baseboardBinary,
|
||||||
cols: segmentResult.segCols,
|
cols: segmentResult.segCols,
|
||||||
rows: segmentResult.segRows,
|
rows: segmentResult.segRows,
|
||||||
|
wallSubLabels: segmentResult.wallSubLabels,
|
||||||
};
|
};
|
||||||
baseboardPickMaskRef.current = null;
|
baseboardPickMaskRef.current = null;
|
||||||
kickRegionIdRef.current =
|
kickRegionIdRef.current =
|
||||||
|
|||||||
@ -53,6 +53,18 @@ export type MaskSegmentConfig = {
|
|||||||
baseboardJunctionRowMarginPx?: number;
|
baseboardJunctionRowMarginPx?: number;
|
||||||
baseboardJunctionVReachPx?: number;
|
baseboardJunctionVReachPx?: number;
|
||||||
baseboardMinRunPx?: number;
|
baseboardMinRunPx?: number;
|
||||||
|
/** 在 wall 掩码内按纹理边界细分为 wall-1、wall-2… */
|
||||||
|
splitWalls?: boolean;
|
||||||
|
/** 墙壁子区最大数量 */
|
||||||
|
splitWallsMaxCount?: number;
|
||||||
|
/** 碎块最小面积比(相对 seg 总像素) */
|
||||||
|
splitWallsMinAreaRatio?: number;
|
||||||
|
/** Lab 色度 + 高频纹理特征距离平方阈值(不含亮度,削弱光影影响) */
|
||||||
|
splitWallsColorDistSq?: number;
|
||||||
|
/** 色度平滑半径(像素),仅用于纹理能量计算 */
|
||||||
|
splitWallsChromaBlurRadius?: number;
|
||||||
|
/** 低饱和(白/灰墙)判定半径,用于与有色墙强制分界 */
|
||||||
|
splitWallsNeutralChromaMax?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PaintConfig = {
|
export type PaintConfig = {
|
||||||
|
|||||||
@ -15,6 +15,15 @@ export type PaintResourceBatch = {
|
|||||||
|
|
||||||
/** OpenCV 8-bit Lab L 通道(BGR 输入,供单测与近似对照) */
|
/** OpenCV 8-bit Lab L 通道(BGR 输入,供单测与近似对照) */
|
||||||
export function bgrToLabL(b: number, g: number, r: number): number {
|
export function bgrToLabL(b: number, g: number, r: number): number {
|
||||||
|
return bgrToLab(b, g, r).l;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** BGR → 8-bit Lab(L/a/b 均映射到 0–255) */
|
||||||
|
export function bgrToLab(
|
||||||
|
b: number,
|
||||||
|
g: number,
|
||||||
|
r: number,
|
||||||
|
): { l: number; a: number; b: number } {
|
||||||
let rf = r / 255;
|
let rf = r / 255;
|
||||||
let gf = g / 255;
|
let gf = g / 255;
|
||||||
let bf = b / 255;
|
let bf = b / 255;
|
||||||
@ -40,7 +49,14 @@ export function bgrToLabL(b: number, g: number, r: number): number {
|
|||||||
fz = fz > delta3 ? Math.cbrt(fz) : fz / (3 * delta * delta) + 4 / 29;
|
fz = fz > delta3 ? Math.cbrt(fz) : fz / (3 * delta * delta) + 4 / 29;
|
||||||
|
|
||||||
const L = fy * 116 - 16;
|
const L = fy * 116 - 16;
|
||||||
return Math.max(0, Math.min(255, Math.round((L * 255) / 100)));
|
const a = 500 * (fx - fy);
|
||||||
|
const bLab = 200 * (fy - fz);
|
||||||
|
|
||||||
|
return {
|
||||||
|
l: Math.max(0, Math.min(255, Math.round((L * 255) / 100))),
|
||||||
|
a: Math.max(0, Math.min(255, Math.round(a + 128))),
|
||||||
|
b: Math.max(0, Math.min(255, Math.round(bLab + 128))),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bgrBufferToRgbaBuffer(
|
export function bgrBufferToRgbaBuffer(
|
||||||
|
|||||||
@ -131,6 +131,12 @@ export const DEFAULT_MASK_CONFIG: Required<
|
|||||||
baseboardJunctionRowMarginPx: 1,
|
baseboardJunctionRowMarginPx: 1,
|
||||||
baseboardJunctionVReachPx: 2,
|
baseboardJunctionVReachPx: 2,
|
||||||
baseboardMinRunPx: 2,
|
baseboardMinRunPx: 2,
|
||||||
|
splitWalls: false,
|
||||||
|
splitWallsMaxCount: 8,
|
||||||
|
splitWallsMinAreaRatio: 0.002,
|
||||||
|
splitWallsColorDistSq: 1400,
|
||||||
|
splitWallsChromaBlurRadius: 5,
|
||||||
|
splitWallsNeutralChromaMax: 14,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ResolvedMaskSegmentRuntime = {
|
export type ResolvedMaskSegmentRuntime = {
|
||||||
@ -188,6 +194,21 @@ export function mergeMaskConfig(
|
|||||||
DEFAULT_MASK_CONFIG.baseboardJunctionVReachPx,
|
DEFAULT_MASK_CONFIG.baseboardJunctionVReachPx,
|
||||||
baseboardMinRunPx:
|
baseboardMinRunPx:
|
||||||
partial.baseboardMinRunPx ?? DEFAULT_MASK_CONFIG.baseboardMinRunPx,
|
partial.baseboardMinRunPx ?? DEFAULT_MASK_CONFIG.baseboardMinRunPx,
|
||||||
|
splitWalls: partial.splitWalls ?? DEFAULT_MASK_CONFIG.splitWalls,
|
||||||
|
splitWallsMaxCount:
|
||||||
|
partial.splitWallsMaxCount ?? DEFAULT_MASK_CONFIG.splitWallsMaxCount,
|
||||||
|
splitWallsMinAreaRatio:
|
||||||
|
partial.splitWallsMinAreaRatio ??
|
||||||
|
DEFAULT_MASK_CONFIG.splitWallsMinAreaRatio,
|
||||||
|
splitWallsColorDistSq:
|
||||||
|
partial.splitWallsColorDistSq ??
|
||||||
|
DEFAULT_MASK_CONFIG.splitWallsColorDistSq,
|
||||||
|
splitWallsChromaBlurRadius:
|
||||||
|
partial.splitWallsChromaBlurRadius ??
|
||||||
|
DEFAULT_MASK_CONFIG.splitWallsChromaBlurRadius,
|
||||||
|
splitWallsNeutralChromaMax:
|
||||||
|
partial.splitWallsNeutralChromaMax ??
|
||||||
|
DEFAULT_MASK_CONFIG.splitWallsNeutralChromaMax,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,8 @@ export type SegmentMaskResult = {
|
|||||||
baseboardBinary: Uint8Array;
|
baseboardBinary: Uint8Array;
|
||||||
segCols: number;
|
segCols: number;
|
||||||
segRows: number;
|
segRows: number;
|
||||||
|
/** splitWalls 时:墙像素 → 子区索引 0..N-1,非墙为 WALL_SUB_LABEL_NONE */
|
||||||
|
wallSubLabels?: Uint8Array;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SegmentRegion = {
|
export type SegmentRegion = {
|
||||||
@ -548,12 +550,13 @@ function extractRegionBinaries(
|
|||||||
regions: SegmentRegion[],
|
regions: SegmentRegion[],
|
||||||
maskData: RegionMaskData,
|
maskData: RegionMaskData,
|
||||||
): Map<number, Uint8Array> {
|
): Map<number, Uint8Array> {
|
||||||
const { labels, baseboardBinary, cols, rows } = maskData;
|
const { labels, baseboardBinary, cols, rows, wallSubLabels } = maskData;
|
||||||
const size = cols * rows;
|
const size = cols * rows;
|
||||||
const binaries = new Map<number, Uint8Array>();
|
const binaries = new Map<number, Uint8Array>();
|
||||||
const semanticColors = getMaskSegmentRuntimeConfig().mask.semanticColors;
|
const semanticColors = getMaskSegmentRuntimeConfig().mask.semanticColors;
|
||||||
const regionIdBySemantic = new Int32Array(semanticColors.length);
|
const regionIdBySemantic = new Int32Array(semanticColors.length);
|
||||||
regionIdBySemantic.fill(-1);
|
regionIdBySemantic.fill(-1);
|
||||||
|
const wallSubRegionIds = new Map<number, number>();
|
||||||
let baseboardRegionId: number | null = null;
|
let baseboardRegionId: number | null = null;
|
||||||
|
|
||||||
for (const reg of regions) {
|
for (const reg of regions) {
|
||||||
@ -562,6 +565,11 @@ function extractRegionBinaries(
|
|||||||
baseboardRegionId = reg.id;
|
baseboardRegionId = reg.id;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const wallMatch = /^wall-(\d+)$/.exec(reg.name);
|
||||||
|
if (wallMatch && wallSubLabels) {
|
||||||
|
wallSubRegionIds.set(Number(wallMatch[1]) - 1, reg.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const semanticIndex = semanticColors.findIndex(
|
const semanticIndex = semanticColors.findIndex(
|
||||||
entry => entry.name === reg.name,
|
entry => entry.name === reg.name,
|
||||||
);
|
);
|
||||||
@ -571,11 +579,22 @@ function extractRegionBinaries(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const semanticCount = semanticColors.length;
|
const semanticCount = semanticColors.length;
|
||||||
|
const wallIdx = semanticColors.findIndex(entry => entry.name === 'wall');
|
||||||
for (let i = 0; i < size; i++) {
|
for (let i = 0; i < size; i++) {
|
||||||
if (baseboardRegionId != null && baseboardBinary[i] > 0) {
|
if (baseboardRegionId != null && baseboardBinary[i] > 0) {
|
||||||
binaries.get(baseboardRegionId)![i] = 255;
|
binaries.get(baseboardRegionId)![i] = 255;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (wallSubLabels && wallIdx >= 0 && labels[i] === wallIdx) {
|
||||||
|
const subIdx = wallSubLabels[i];
|
||||||
|
if (subIdx !== 255) {
|
||||||
|
const regionId = wallSubRegionIds.get(subIdx);
|
||||||
|
if (regionId !== undefined) {
|
||||||
|
binaries.get(regionId)![i] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const semanticIndex = labels[i];
|
const semanticIndex = labels[i];
|
||||||
if (semanticIndex < semanticCount && regionIdBySemantic[semanticIndex] >= 0) {
|
if (semanticIndex < semanticCount && regionIdBySemantic[semanticIndex] >= 0) {
|
||||||
binaries.get(regionIdBySemantic[semanticIndex])![i] = 255;
|
binaries.get(regionIdBySemantic[semanticIndex])![i] = 255;
|
||||||
@ -942,6 +961,7 @@ export type RegionMaskData = {
|
|||||||
baseboardBinary: Uint8Array;
|
baseboardBinary: Uint8Array;
|
||||||
cols: number;
|
cols: number;
|
||||||
rows: number;
|
rows: number;
|
||||||
|
wallSubLabels?: Uint8Array;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 蒙版路径构建降采样(屏幕显示不需要分割分辨率,点击仍用全分辨率 pickMap) */
|
/** 蒙版路径构建降采样(屏幕显示不需要分割分辨率,点击仍用全分辨率 pickMap) */
|
||||||
@ -949,7 +969,7 @@ export function downsampleMaskDataForPaths(
|
|||||||
maskData: RegionMaskData,
|
maskData: RegionMaskData,
|
||||||
maxLongSide: number,
|
maxLongSide: number,
|
||||||
): RegionMaskData {
|
): RegionMaskData {
|
||||||
const { labels, baseboardBinary, cols, rows } = maskData;
|
const { labels, baseboardBinary, cols, rows, wallSubLabels } = maskData;
|
||||||
const longSide = Math.max(cols, rows);
|
const longSide = Math.max(cols, rows);
|
||||||
if (longSide <= maxLongSide) {
|
if (longSide <= maxLongSide) {
|
||||||
return maskData;
|
return maskData;
|
||||||
@ -960,6 +980,11 @@ export function downsampleMaskDataForPaths(
|
|||||||
const dstRows = Math.max(1, Math.floor(rows * scale));
|
const dstRows = Math.max(1, Math.floor(rows * scale));
|
||||||
const outLabels = new Uint8Array(dstCols * dstRows);
|
const outLabels = new Uint8Array(dstCols * dstRows);
|
||||||
const outBaseboard = new Uint8Array(dstCols * dstRows);
|
const outBaseboard = new Uint8Array(dstCols * dstRows);
|
||||||
|
const outWallSub =
|
||||||
|
wallSubLabels != null ? new Uint8Array(dstCols * dstRows) : undefined;
|
||||||
|
if (outWallSub) {
|
||||||
|
outWallSub.fill(255);
|
||||||
|
}
|
||||||
|
|
||||||
for (let y = 0; y < dstRows; y++) {
|
for (let y = 0; y < dstRows; y++) {
|
||||||
const sy = Math.min(rows - 1, Math.floor((y * rows) / dstRows));
|
const sy = Math.min(rows - 1, Math.floor((y * rows) / dstRows));
|
||||||
@ -971,6 +996,9 @@ export function downsampleMaskDataForPaths(
|
|||||||
const di = dstRow + x;
|
const di = dstRow + x;
|
||||||
outLabels[di] = labels[si];
|
outLabels[di] = labels[si];
|
||||||
outBaseboard[di] = baseboardBinary[si];
|
outBaseboard[di] = baseboardBinary[si];
|
||||||
|
if (outWallSub && wallSubLabels) {
|
||||||
|
outWallSub[di] = wallSubLabels[si];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -979,6 +1007,7 @@ export function downsampleMaskDataForPaths(
|
|||||||
baseboardBinary: outBaseboard,
|
baseboardBinary: outBaseboard,
|
||||||
cols: dstCols,
|
cols: dstCols,
|
||||||
rows: dstRows,
|
rows: dstRows,
|
||||||
|
wallSubLabels: outWallSub,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
669
src/utils/wallTextureSplit.ts
Normal file
669
src/utils/wallTextureSplit.ts
Normal file
@ -0,0 +1,669 @@
|
|||||||
|
import { bgrToLab } from './freqLayerPrep';
|
||||||
|
import { getMaskSegmentRuntimeConfig } from './maskSegmentRuntime';
|
||||||
|
import type { SegmentMaskResult, SegmentRegion } from './maskSegmentation';
|
||||||
|
import { getSemanticColorByName } from './maskSemanticPalette';
|
||||||
|
|
||||||
|
/** 非墙像素在 wallSubLabels 中的占位值 */
|
||||||
|
export const WALL_SUB_LABEL_NONE = 255;
|
||||||
|
|
||||||
|
type WallComponent = {
|
||||||
|
label: number;
|
||||||
|
area: number;
|
||||||
|
bbox: SegmentRegion['bbox'];
|
||||||
|
};
|
||||||
|
|
||||||
|
function maskCfg() {
|
||||||
|
return getMaskSegmentRuntimeConfig().mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bboxToPolygon(bbox: SegmentRegion['bbox']): { x: number; y: number }[] {
|
||||||
|
return [
|
||||||
|
{ x: bbox.x, y: bbox.y },
|
||||||
|
{ x: bbox.x + bbox.w, y: bbox.y },
|
||||||
|
{ x: bbox.x + bbox.w, y: bbox.y + bbox.h },
|
||||||
|
{ x: bbox.x, y: bbox.y + bbox.h },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeLabChromaMaps(
|
||||||
|
originBgr: Uint8Array,
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
): { aMap: Uint8Array; bMap: Uint8Array } {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const aMap = new Uint8Array(pixelCount);
|
||||||
|
const bMap = new Uint8Array(pixelCount);
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const s = i * 3;
|
||||||
|
const lab = bgrToLab(originBgr[s], originBgr[s + 1], originBgr[s + 2]);
|
||||||
|
aMap[i] = lab.a;
|
||||||
|
bMap[i] = lab.b;
|
||||||
|
}
|
||||||
|
return { aMap, bMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
function chromaMag(a: number, b: number): number {
|
||||||
|
const da = a - 128;
|
||||||
|
const db = b - 128;
|
||||||
|
return Math.sqrt(da * da + db * db);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chromaDistSq(a0: number, b0: number, a1: number, b1: number): number {
|
||||||
|
const da = a0 - a1;
|
||||||
|
const db = b0 - b1;
|
||||||
|
return da * da + db * db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 白/灰墙与蓝/有色墙、或两种不同色相墙之间强制视为材质边界 */
|
||||||
|
function isCrossMaterialBoundary(
|
||||||
|
a0: number,
|
||||||
|
b0: number,
|
||||||
|
a1: number,
|
||||||
|
b1: number,
|
||||||
|
neutralChromaMax: number,
|
||||||
|
): boolean {
|
||||||
|
const m0 = chromaMag(a0, b0);
|
||||||
|
const m1 = chromaMag(a1, b1);
|
||||||
|
const neutralGate = neutralChromaMax * 2.2;
|
||||||
|
|
||||||
|
if (m0 <= neutralChromaMax && m1 > neutralGate) return true;
|
||||||
|
if (m1 <= neutralChromaMax && m0 > neutralGate) return true;
|
||||||
|
|
||||||
|
if (m0 > neutralChromaMax && m1 > neutralChromaMax) {
|
||||||
|
const hue0 = Math.atan2(b0 - 128, a0 - 128);
|
||||||
|
const hue1 = Math.atan2(b1 - 128, a1 - 128);
|
||||||
|
let dh = Math.abs(hue0 - hue1);
|
||||||
|
if (dh > Math.PI) dh = 2 * Math.PI - dh;
|
||||||
|
if (dh > Math.PI / 4) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canMergeWallPixels(
|
||||||
|
refA: number,
|
||||||
|
refB: number,
|
||||||
|
na: number,
|
||||||
|
nb: number,
|
||||||
|
distSqThreshold: number,
|
||||||
|
neutralChromaMax: number,
|
||||||
|
): boolean {
|
||||||
|
if (isCrossMaterialBoundary(refA, refB, na, nb, neutralChromaMax)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return chromaDistSq(refA, refB, na, nb) <= distSqThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findWallSemanticIndex(): number {
|
||||||
|
const colors = maskCfg().semanticColors;
|
||||||
|
return colors.findIndex(entry => entry.name === 'wall');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWallPixel(
|
||||||
|
labels: Uint8Array,
|
||||||
|
baseboardBinary: Uint8Array,
|
||||||
|
wallIdx: number,
|
||||||
|
i: number,
|
||||||
|
): boolean {
|
||||||
|
if (baseboardBinary[i]) return false;
|
||||||
|
return labels[i] === wallIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4-连通区域生长:与连通域色度均值比较,避免链式桥接;中性/有色墙强制分界。
|
||||||
|
*/
|
||||||
|
function labelWallComponents(
|
||||||
|
labels: Uint8Array,
|
||||||
|
baseboardBinary: Uint8Array,
|
||||||
|
wallIdx: number,
|
||||||
|
aMap: Uint8Array,
|
||||||
|
bMap: Uint8Array,
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
distSqThreshold: number,
|
||||||
|
neutralChromaMax: number,
|
||||||
|
): { compLabels: Int32Array; compCount: number } {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const compLabels = new Int32Array(pixelCount);
|
||||||
|
compLabels.fill(-1);
|
||||||
|
let compCount = 0;
|
||||||
|
|
||||||
|
const queue = new Int32Array(pixelCount);
|
||||||
|
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
const i = y * cols + x;
|
||||||
|
if (!isWallPixel(labels, baseboardBinary, wallIdx, i)) continue;
|
||||||
|
if (compLabels[i] >= 0) continue;
|
||||||
|
|
||||||
|
const compId = compCount;
|
||||||
|
compCount += 1;
|
||||||
|
|
||||||
|
const seedA = aMap[i];
|
||||||
|
const seedB = bMap[i];
|
||||||
|
let sumA = seedA;
|
||||||
|
let sumB = seedB;
|
||||||
|
let count = 1;
|
||||||
|
|
||||||
|
let head = 0;
|
||||||
|
let tail = 0;
|
||||||
|
queue[tail++] = i;
|
||||||
|
compLabels[i] = compId;
|
||||||
|
|
||||||
|
while (head < tail) {
|
||||||
|
const ci = queue[head++];
|
||||||
|
const cx = ci % cols;
|
||||||
|
const cy = (ci / cols) | 0;
|
||||||
|
const meanA = sumA / count;
|
||||||
|
const meanB = sumB / count;
|
||||||
|
|
||||||
|
const neighbors = [ci - 1, ci + 1, ci - cols, ci + cols];
|
||||||
|
for (const ni of neighbors) {
|
||||||
|
if (ni < 0 || ni >= pixelCount) continue;
|
||||||
|
const nx = ni % cols;
|
||||||
|
if (Math.abs(nx - cx) > 1) continue;
|
||||||
|
if (!isWallPixel(labels, baseboardBinary, wallIdx, ni)) continue;
|
||||||
|
if (compLabels[ni] >= 0) continue;
|
||||||
|
|
||||||
|
const na = aMap[ni];
|
||||||
|
const nb = bMap[ni];
|
||||||
|
const stepOk = canMergeWallPixels(
|
||||||
|
aMap[ci],
|
||||||
|
bMap[ci],
|
||||||
|
na,
|
||||||
|
nb,
|
||||||
|
distSqThreshold * 1.8,
|
||||||
|
neutralChromaMax,
|
||||||
|
);
|
||||||
|
const meanOk = canMergeWallPixels(
|
||||||
|
meanA,
|
||||||
|
meanB,
|
||||||
|
na,
|
||||||
|
nb,
|
||||||
|
distSqThreshold,
|
||||||
|
neutralChromaMax,
|
||||||
|
);
|
||||||
|
const seedOk = canMergeWallPixels(
|
||||||
|
seedA,
|
||||||
|
seedB,
|
||||||
|
na,
|
||||||
|
nb,
|
||||||
|
distSqThreshold * 3,
|
||||||
|
neutralChromaMax,
|
||||||
|
);
|
||||||
|
if (!stepOk || !meanOk || !seedOk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
compLabels[ni] = compId;
|
||||||
|
sumA += na;
|
||||||
|
sumB += nb;
|
||||||
|
count += 1;
|
||||||
|
queue[tail++] = ni;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { compLabels, compCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeComponentStats(
|
||||||
|
compLabels: Int32Array,
|
||||||
|
compCount: number,
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
): WallComponent[] {
|
||||||
|
const stats: WallComponent[] = Array.from({ length: compCount }, (_, label) => ({
|
||||||
|
label,
|
||||||
|
area: 0,
|
||||||
|
bbox: { x: cols, y: rows, w: 0, h: 0 },
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
const i = y * cols + x;
|
||||||
|
const comp = compLabels[i];
|
||||||
|
if (comp < 0) continue;
|
||||||
|
const s = stats[comp];
|
||||||
|
s.area += 1;
|
||||||
|
if (x < s.bbox.x) s.bbox.x = x;
|
||||||
|
if (y < s.bbox.y) s.bbox.y = y;
|
||||||
|
const right = x + 1;
|
||||||
|
const bottom = y + 1;
|
||||||
|
if (right > s.bbox.x + s.bbox.w) s.bbox.w = right - s.bbox.x;
|
||||||
|
if (bottom > s.bbox.y + s.bbox.h) s.bbox.h = bottom - s.bbox.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeComponentChromaMeans(
|
||||||
|
compLabels: Int32Array,
|
||||||
|
aMap: Uint8Array,
|
||||||
|
bMap: Uint8Array,
|
||||||
|
compCount: number,
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
): { meanA: Float64Array; meanB: Float64Array } {
|
||||||
|
const sumA = new Float64Array(compCount);
|
||||||
|
const sumB = new Float64Array(compCount);
|
||||||
|
const counts = new Float64Array(compCount);
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const comp = compLabels[i];
|
||||||
|
if (comp < 0) continue;
|
||||||
|
sumA[comp] += aMap[i];
|
||||||
|
sumB[comp] += bMap[i];
|
||||||
|
counts[comp] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meanA = new Float64Array(compCount);
|
||||||
|
const meanB = new Float64Array(compCount);
|
||||||
|
for (let c = 0; c < compCount; c++) {
|
||||||
|
if (counts[c] > 0) {
|
||||||
|
meanA[c] = sumA[c] / counts[c];
|
||||||
|
meanB[c] = sumB[c] / counts[c];
|
||||||
|
} else {
|
||||||
|
meanA[c] = 128;
|
||||||
|
meanB[c] = 128;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { meanA, meanB };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeSmallComponents(
|
||||||
|
compLabels: Int32Array,
|
||||||
|
stats: WallComponent[],
|
||||||
|
aMap: Uint8Array,
|
||||||
|
bMap: Uint8Array,
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
minArea: number,
|
||||||
|
distSqThreshold: number,
|
||||||
|
neutralChromaMax: number,
|
||||||
|
): void {
|
||||||
|
const compCount = stats.length;
|
||||||
|
const { meanA, meanB } = computeComponentChromaMeans(
|
||||||
|
compLabels,
|
||||||
|
aMap,
|
||||||
|
bMap,
|
||||||
|
compCount,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
);
|
||||||
|
const adjacency = new Map<number, Map<number, number>>();
|
||||||
|
|
||||||
|
const addEdge = (a: number, b: number) => {
|
||||||
|
if (a === b || a < 0 || b < 0) return;
|
||||||
|
let map = adjacency.get(a);
|
||||||
|
if (!map) {
|
||||||
|
map = new Map();
|
||||||
|
adjacency.set(a, map);
|
||||||
|
}
|
||||||
|
map.set(b, (map.get(b) ?? 0) + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
const i = y * cols + x;
|
||||||
|
const a = compLabels[i];
|
||||||
|
if (a < 0) continue;
|
||||||
|
if (x + 1 < cols) {
|
||||||
|
const b = compLabels[i + 1];
|
||||||
|
if (b >= 0) addEdge(a, b);
|
||||||
|
}
|
||||||
|
if (y + 1 < rows) {
|
||||||
|
const b = compLabels[i + cols];
|
||||||
|
if (b >= 0) addEdge(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const remap = new Int32Array(compCount);
|
||||||
|
for (let i = 0; i < compCount; i++) remap[i] = i;
|
||||||
|
|
||||||
|
const find = (x: number): number => {
|
||||||
|
while (remap[x] !== x) {
|
||||||
|
remap[x] = remap[remap[x]];
|
||||||
|
x = remap[x];
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
const union = (a: number, b: number) => {
|
||||||
|
const ra = find(a);
|
||||||
|
const rb = find(b);
|
||||||
|
if (ra === rb) return;
|
||||||
|
const areaA = stats[ra].area;
|
||||||
|
const areaB = stats[rb].area;
|
||||||
|
if (areaA >= areaB) {
|
||||||
|
remap[rb] = ra;
|
||||||
|
stats[ra].area += stats[rb].area;
|
||||||
|
stats[rb].area = 0;
|
||||||
|
} else {
|
||||||
|
remap[ra] = rb;
|
||||||
|
stats[rb].area += stats[ra].area;
|
||||||
|
stats[ra].area = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let c = 0; c < compCount; c++) {
|
||||||
|
if (stats[c].area >= minArea) continue;
|
||||||
|
const neighbors = adjacency.get(c);
|
||||||
|
if (!neighbors || neighbors.size === 0) continue;
|
||||||
|
let bestNeighbor = -1;
|
||||||
|
let bestBorder = 0;
|
||||||
|
for (const [nb, border] of neighbors) {
|
||||||
|
if (border > bestBorder) {
|
||||||
|
bestBorder = border;
|
||||||
|
bestNeighbor = nb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestNeighbor >= 0) {
|
||||||
|
if (
|
||||||
|
!canMergeWallPixels(
|
||||||
|
meanA[c],
|
||||||
|
meanB[c],
|
||||||
|
meanA[bestNeighbor],
|
||||||
|
meanB[bestNeighbor],
|
||||||
|
distSqThreshold,
|
||||||
|
neutralChromaMax,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
union(c, bestNeighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const c = compLabels[i];
|
||||||
|
if (c < 0) continue;
|
||||||
|
compLabels[i] = find(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function relabelComponentsContiguous(
|
||||||
|
compLabels: Int32Array,
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
): { labels: Int32Array; compCount: number; stats: WallComponent[] } {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const remap = new Map<number, number>();
|
||||||
|
const out = new Int32Array(pixelCount);
|
||||||
|
out.fill(-1);
|
||||||
|
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const c = compLabels[i];
|
||||||
|
if (c < 0) continue;
|
||||||
|
let next = remap.get(c);
|
||||||
|
if (next === undefined) {
|
||||||
|
next = remap.size;
|
||||||
|
remap.set(c, next);
|
||||||
|
}
|
||||||
|
out[i] = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compCount = remap.size;
|
||||||
|
const stats = computeComponentStats(out, compCount, cols, rows);
|
||||||
|
return { labels: out, compCount, stats };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPickMapAfterWallSplit(
|
||||||
|
labels: Uint8Array,
|
||||||
|
baseboardBinary: Uint8Array,
|
||||||
|
wallIdx: number,
|
||||||
|
wallSubLabels: Uint8Array,
|
||||||
|
indexToName: string[],
|
||||||
|
nameToId: Map<string, number>,
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
): Uint8Array {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const pick = new Uint8Array(pixelCount);
|
||||||
|
const baseboardName = 'baseboard';
|
||||||
|
const baseboardId = nameToId.get(baseboardName);
|
||||||
|
const baseboardCode = baseboardId === undefined ? 0 : baseboardId + 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
if (baseboardBinary[i]) {
|
||||||
|
if (baseboardCode > 0) {
|
||||||
|
pick[i] = baseboardCode;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labels[i] === wallIdx && wallSubLabels[i] !== WALL_SUB_LABEL_NONE) {
|
||||||
|
const wallName = `wall-${wallSubLabels[i] + 1}`;
|
||||||
|
const regionId = nameToId.get(wallName);
|
||||||
|
if (regionId !== undefined) {
|
||||||
|
pick[i] = regionId + 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const semanticIndex = labels[i];
|
||||||
|
if (semanticIndex === 255) continue;
|
||||||
|
const name = indexToName[semanticIndex];
|
||||||
|
if (!name) continue;
|
||||||
|
const regionId = nameToId.get(name);
|
||||||
|
if (regionId !== undefined) {
|
||||||
|
pick[i] = regionId + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pick;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dilatePickBuffer1px(
|
||||||
|
pick: Uint8Array,
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
): Uint8Array {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const dst = new Uint8Array(pixelCount);
|
||||||
|
dst.set(pick);
|
||||||
|
|
||||||
|
for (let y = 1; y < rows - 1; y++) {
|
||||||
|
for (let x = 1; x < cols - 1; x++) {
|
||||||
|
const i = y * cols + x;
|
||||||
|
if (pick[i] !== 0) continue;
|
||||||
|
|
||||||
|
const n = [
|
||||||
|
pick[(y - 1) * cols + (x - 1)],
|
||||||
|
pick[(y - 1) * cols + x],
|
||||||
|
pick[(y - 1) * cols + (x + 1)],
|
||||||
|
pick[y * cols + (x - 1)],
|
||||||
|
pick[y * cols + (x + 1)],
|
||||||
|
pick[(y + 1) * cols + (x - 1)],
|
||||||
|
pick[(y + 1) * cols + x],
|
||||||
|
pick[(y + 1) * cols + (x + 1)],
|
||||||
|
];
|
||||||
|
|
||||||
|
const counts: Record<number, number> = {};
|
||||||
|
for (let k = 0; k < 8; k++) {
|
||||||
|
const code = n[k];
|
||||||
|
if (code !== 0) {
|
||||||
|
counts[code] = (counts[code] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const codeStr of Object.keys(counts)) {
|
||||||
|
const code = Number(codeStr);
|
||||||
|
if (counts[code] >= 4) {
|
||||||
|
dst[i] = code;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在语义分割完成后,将 wall 区域按原图纹理特征细分为 wall-1、wall-2…
|
||||||
|
*/
|
||||||
|
export function splitWallRegionsByTexture(
|
||||||
|
result: SegmentMaskResult,
|
||||||
|
originBgr: Uint8Array,
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
minArea: number,
|
||||||
|
): SegmentMaskResult {
|
||||||
|
const cfg = maskCfg();
|
||||||
|
if (!cfg.splitWalls) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallRegion = result.regions.find(reg => reg.name === 'wall');
|
||||||
|
if (!wallRegion) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallIdx = findWallSemanticIndex();
|
||||||
|
if (wallIdx < 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { labels, baseboardBinary, regions } = result;
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
if (originBgr.length < pixelCount * 3) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { aMap: rawA, bMap: rawB } = computeLabChromaMaps(originBgr, cols, rows);
|
||||||
|
const distSqThreshold = cfg.splitWallsColorDistSq;
|
||||||
|
const neutralChromaMax = cfg.splitWallsNeutralChromaMax;
|
||||||
|
const minAreaFloor = Math.max(
|
||||||
|
minArea,
|
||||||
|
Math.floor(cols * rows * cfg.splitWallsMinAreaRatio),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { compLabels: rawCompLabels, compCount: rawCount } = labelWallComponents(
|
||||||
|
labels,
|
||||||
|
baseboardBinary,
|
||||||
|
wallIdx,
|
||||||
|
rawA,
|
||||||
|
rawB,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
distSqThreshold,
|
||||||
|
neutralChromaMax,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rawCount === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stats = computeComponentStats(rawCompLabels, rawCount, cols, rows);
|
||||||
|
mergeSmallComponents(
|
||||||
|
rawCompLabels,
|
||||||
|
stats,
|
||||||
|
rawA,
|
||||||
|
rawB,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
minAreaFloor,
|
||||||
|
distSqThreshold,
|
||||||
|
neutralChromaMax,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { labels: finalCompLabels, compCount, stats: finalStats } =
|
||||||
|
relabelComponentsContiguous(rawCompLabels, cols, rows);
|
||||||
|
|
||||||
|
if (compCount === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按面积降序,截断至 maxCount
|
||||||
|
const ranked = finalStats
|
||||||
|
.map((s, idx) => ({ ...s, origIdx: idx }))
|
||||||
|
.filter(s => s.area > 0)
|
||||||
|
.sort((a, b) => b.area - a.area)
|
||||||
|
.slice(0, cfg.splitWallsMaxCount);
|
||||||
|
|
||||||
|
const rankMap = new Map<number, number>();
|
||||||
|
ranked.forEach((s, rank) => {
|
||||||
|
rankMap.set(s.origIdx, rank);
|
||||||
|
});
|
||||||
|
|
||||||
|
const wallSubLabels = new Uint8Array(pixelCount);
|
||||||
|
wallSubLabels.fill(WALL_SUB_LABEL_NONE);
|
||||||
|
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const c = finalCompLabels[i];
|
||||||
|
if (c < 0) continue;
|
||||||
|
const rank = rankMap.get(c);
|
||||||
|
if (rank === undefined) continue;
|
||||||
|
wallSubLabels[i] = rank;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallRef = getSemanticColorByName('wall');
|
||||||
|
const wallHex = wallRef?.hex ?? wallRegion.hex;
|
||||||
|
const wallColor = wallRef?.bgr ?? wallRegion.color;
|
||||||
|
|
||||||
|
const nonWallRegions = regions.filter(reg => reg.name !== 'wall');
|
||||||
|
const wallSubRegions: SegmentRegion[] = ranked.map((s, rank) => {
|
||||||
|
const bbox = s.bbox;
|
||||||
|
const poly = bboxToPolygon(bbox);
|
||||||
|
return {
|
||||||
|
id: 0,
|
||||||
|
name: `wall-${rank + 1}`,
|
||||||
|
hex: wallHex,
|
||||||
|
color: { ...wallColor },
|
||||||
|
polygons: [poly],
|
||||||
|
outlinePolygons: [poly],
|
||||||
|
bbox,
|
||||||
|
area: s.area,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedRegions = [...nonWallRegions, ...wallSubRegions];
|
||||||
|
mergedRegions.sort((a, b) => b.area - a.area);
|
||||||
|
mergedRegions.forEach((reg, index) => {
|
||||||
|
reg.id = index;
|
||||||
|
});
|
||||||
|
|
||||||
|
const nameToId = new Map(mergedRegions.map(reg => [reg.name, reg.id]));
|
||||||
|
const indexToName = cfg.semanticColors.map(entry => entry.name);
|
||||||
|
|
||||||
|
const pickRaw = buildPickMapAfterWallSplit(
|
||||||
|
labels,
|
||||||
|
baseboardBinary,
|
||||||
|
wallIdx,
|
||||||
|
wallSubLabels,
|
||||||
|
indexToName,
|
||||||
|
nameToId,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
);
|
||||||
|
const pickBuffer = dilatePickBuffer1px(pickRaw, cols, rows);
|
||||||
|
|
||||||
|
for (const reg of mergedRegions) {
|
||||||
|
if (!/^wall-\d+$/.test(reg.name)) continue;
|
||||||
|
const subIdx = Number(reg.name.slice(5)) - 1;
|
||||||
|
let area = 0;
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
if (wallSubLabels[i] === subIdx) area += 1;
|
||||||
|
}
|
||||||
|
reg.area = area;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
regions: mergedRegions,
|
||||||
|
pickMap: { buffer: pickBuffer, cols, rows },
|
||||||
|
labels,
|
||||||
|
baseboardBinary,
|
||||||
|
segCols: cols,
|
||||||
|
segRows: rows,
|
||||||
|
wallSubLabels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWallSubRegionName(name: string): boolean {
|
||||||
|
return /^wall-\d+$/.test(name);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user