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:
a1518 2026-06-30 22:41:17 -07:00
parent eb246efa8a
commit 70499176d6
29 changed files with 1565 additions and 24 deletions

View File

@ -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

View 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');
});

View File

@ -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"}

View File

@ -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 =

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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 LabL/a/b 均映射到 0255 */
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↔原生往返 */

View File

@ -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"}

View File

@ -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 LabL/a/b 均映射到 0255 */
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;

File diff suppressed because one or more lines are too long

View File

@ -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"}

View File

@ -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) {

File diff suppressed because one or more lines are too long

View File

@ -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;

View File

@ -1 +1 @@
{"version":3,"file":"maskSegmentation.d.ts","sourceRoot":"","sources":["../../src/utils/maskSegmentation.ts"],"names":[],"mappings":"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"} {"version":3,"file":"maskSegmentation.d.ts","sourceRoot":"","sources":["../../src/utils/maskSegmentation.ts"],"names":[],"mappings":"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"}

View File

@ -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避免每像素 × 语义数循环) */

File diff suppressed because one or more lines are too long

9
dist/utils/wallTextureSplit.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import type { SegmentMaskResult } from './maskSegmentation';
/** 非墙像素在 wallSubLabels 中的占位值 */
export declare const WALL_SUB_LABEL_NONE = 255;
/**
* wall wall-1wall-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
View 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
View 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-1wall-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

File diff suppressed because one or more lines are too long

View File

@ -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,

View File

@ -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 =

View File

@ -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 = {

View File

@ -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 LabL/a/b 均映射到 0255 */
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(

View File

@ -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,
}; };
} }

View File

@ -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,
}; };
} }

View 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-1wall-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);
}