react-native-mask-segment-c.../dist/utils/maskSegmentation.js

1212 lines
42 KiB
JavaScript
Raw Normal View History

import cv from './opencvAdapter';
import { Skia } from '@shopify/react-native-skia';
import { BASEBOARD_SEMANTIC_NAME, classifyBgrPixelToSemantic, getCabinetQuantKeys, getSemanticColorByName, getWallQuantKeys, getBaseboardStripQuantKeys, isStrictBaseboardPixel, } from './maskSemanticPalette';
import { getMaskRuntimeRevision, getMaskSegmentRuntimeConfig } from './maskSegmentRuntime';
function maskCfg() {
return getMaskSegmentRuntimeConfig().mask;
}
const MORPH_KERNEL_SIZE = 5;
const MAX_DASH_OUTLINE_POLYGONS = 10;
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 },
];
}
export function buildRegionOutlinePolygons(reg) {
if (reg.outlinePolygons && reg.outlinePolygons.length > 0) {
return reg.outlinePolygons;
}
if (reg.thinStrip || reg.polygons.length <= MAX_DASH_OUTLINE_POLYGONS) {
return reg.polygons;
}
return [bboxToPolygon(reg.bbox)];
}
import { buildAllRegionOutlinePaths, buildRegionOutlinePathForRegion } from './maskOutlinePaths';
// Re-export for backward compatibility
export { buildAllRegionOutlinePaths, buildRegionOutlinePathForRegion };
function isBaseboardEntry(entry) {
return entry.name === BASEBOARD_SEMANTIC_NAME;
}
/** 踢脚线:仅同行横向补缝,不纵向膨胀 */
function bridgeBaseboardHorizontally(binary, cols, rows) {
const out = new Uint8Array(binary);
const halfW = maskCfg().kickBridgeHalfWPx;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
if (!binary[y * cols + x]) {
continue;
}
for (let dx = -halfW; dx <= halfW; dx++) {
const nx = x + dx;
if (nx < 0 || nx >= cols) {
continue;
}
out[y * cols + nx] = 255;
}
}
}
return out;
}
function rowRunsToPolygons(runs, cols, rows) {
return runs.map(run => [
{ x: run.minX / cols, y: run.y / rows },
{ x: (run.maxX + 1) / cols, y: run.y / rows },
{ x: (run.maxX + 1) / cols, y: (run.y + 1) / rows },
{ x: run.minX / cols, y: (run.y + 1) / rows },
]);
}
function quantChannelSlot(value) {
const q = Math.min(255, Math.round(value / maskCfg().quantStep) * maskCfg().quantStep);
if (q >= 192) {
return q === 255 ? 4 : 3;
}
if (q >= 128) {
return 2;
}
if (q >= 64) {
return 1;
}
return 0;
}
function quantKeyIndex(b, g, r) {
return (quantChannelSlot(b) * 25 + quantChannelSlot(g) * 5 + quantChannelSlot(r));
}
function getStripQuantIndices() {
const revision = getMaskRuntimeRevision();
if (stripIndicesRevision === revision && cachedStripQuantIndices) {
return cachedStripQuantIndices;
}
cachedStripQuantIndices = new Set([...getBaseboardStripQuantKeys()].map(key => {
const [b, g, r] = key.split(',').map(part => Number(part));
return quantKeyIndex(b, g, r);
}));
stripIndicesRevision = revision;
return cachedStripQuantIndices;
}
let stripIndicesRevision = -1;
let cachedStripQuantIndices = null;
let channelSlotLutRevision = -1;
let cachedChannelSlotLut = null;
function buildQuantChannelSlotLut(quantStep) {
const lut = new Uint8Array(256);
for (let value = 0; value < 256; value++) {
const q = Math.min(255, Math.round(value / quantStep) * quantStep);
if (q >= 192) {
lut[value] = q === 255 ? 4 : 3;
}
else if (q >= 128) {
lut[value] = 2;
}
else if (q >= 64) {
lut[value] = 1;
}
else {
lut[value] = 0;
}
}
return lut;
}
function getQuantChannelSlotLut() {
const revision = getMaskRuntimeRevision();
if (channelSlotLutRevision === revision && cachedChannelSlotLut) {
return cachedChannelSlotLut;
}
cachedChannelSlotLut = buildQuantChannelSlotLut(maskCfg().quantStep);
channelSlotLutRevision = revision;
return cachedChannelSlotLut;
}
function quantSlotToChannel(slot) {
if (slot >= 4) {
return 255;
}
if (slot >= 3) {
return 192;
}
if (slot >= 2) {
return 128;
}
if (slot >= 1) {
return 64;
}
return 0;
}
function quantIndexToBgr(idx) {
const bSlot = (idx / 25) | 0;
const gSlot = ((idx % 25) / 5) | 0;
const rSlot = idx % 5;
return [
quantSlotToChannel(bSlot),
quantSlotToChannel(gSlot),
quantSlotToChannel(rSlot),
];
}
let semanticLutRevision = -1;
let cachedSemanticLut = null;
let cachedStripQuantLut = null;
let stripQuantLutRevision = -1;
function getStripQuantLut() {
const revision = getMaskRuntimeRevision();
if (stripQuantLutRevision === revision && cachedStripQuantLut) {
return cachedStripQuantLut;
}
const lut = new Uint8Array(125);
for (const idx of getStripQuantIndices()) {
lut[idx] = 1;
}
cachedStripQuantLut = lut;
stripQuantLutRevision = revision;
return lut;
}
function bboxFromBinary(binary, cols, rows) {
let minX = cols;
let minY = rows;
let maxX = -1;
let maxY = -1;
for (let y = 0; y < rows; y++) {
const row = y * cols;
for (let x = 0; x < cols; x++) {
if (!binary[row + x]) {
continue;
}
if (x < minX) {
minX = x;
}
if (x > maxX) {
maxX = x;
}
if (y < minY) {
minY = y;
}
if (y > maxY) {
maxY = y;
}
}
}
if (maxX < 0) {
return null;
}
return {
x: minX / cols,
y: minY / rows,
w: (maxX - minX + 1) / cols,
h: (maxY - minY + 1) / rows,
};
}
/** 从二值图逐行条带构建蒙版(供 Skia PathBuilder 使用) */
export function appendMaskBinaryToPathBuilder(binary, cols, rows, rect, builder, minRunPx = maskCfg().baseboardMinRunPx) {
for (let y = 0; y < rows; y++) {
let runStart = -1;
const normY0 = y / rows;
const normY1 = (y + 1) / rows;
const screenY0 = rect.y + normY0 * rect.h;
const screenY1 = rect.y + normY1 * rect.h;
for (let x = 0; x <= cols; x++) {
const on = x < cols && binary[y * cols + x] > 0;
if (on && runStart < 0) {
runStart = x;
}
if (!on && runStart >= 0) {
if (x - runStart >= minRunPx) {
const normX0 = runStart / cols;
const normX1 = x / cols;
builder.moveTo(rect.x + normX0 * rect.w, screenY0);
builder.lineTo(rect.x + normX1 * rect.w, screenY0);
builder.lineTo(rect.x + normX1 * rect.w, screenY1);
builder.lineTo(rect.x + normX0 * rect.w, screenY1);
builder.close();
}
runStart = -1;
}
}
}
}
/** 从语义标签逐行条带构建蒙版(避免维护多张二值图) */
export function appendLabelMaskToPathBuilder(labels, semanticIndex, cols, rows, rect, builder, minRunPx = maskCfg().baseboardMinRunPx) {
for (let y = 0; y < rows; y++) {
let runStart = -1;
const row = y * cols;
const normY0 = y / rows;
const normY1 = (y + 1) / rows;
const screenY0 = rect.y + normY0 * rect.h;
const screenY1 = rect.y + normY1 * rect.h;
for (let x = 0; x <= cols; x++) {
const on = x < cols &&
labels[row + x] === semanticIndex;
if (on && runStart < 0) {
runStart = x;
}
if (!on && runStart >= 0) {
if (x - runStart >= minRunPx) {
const normX0 = runStart / cols;
const normX1 = x / cols;
builder.moveTo(rect.x + normX0 * rect.w, screenY0);
builder.lineTo(rect.x + normX1 * rect.w, screenY0);
builder.lineTo(rect.x + normX1 * rect.w, screenY1);
builder.lineTo(rect.x + normX0 * rect.w, screenY1);
builder.close();
}
runStart = -1;
}
}
}
}
function appendRunRectToBuilder(runStart, runEnd, y, cols, rows, rect, builder, minRunPx) {
if (runEnd - runStart < minRunPx) {
return;
}
const normY0 = y / rows;
const normY1 = (y + 1) / rows;
const screenY0 = rect.y + normY0 * rect.h;
const screenY1 = rect.y + normY1 * rect.h;
const normX0 = runStart / cols;
const normX1 = runEnd / cols;
builder.moveTo(rect.x + normX0 * rect.w, screenY0);
builder.lineTo(rect.x + normX1 * rect.w, screenY0);
builder.lineTo(rect.x + normX1 * rect.w, screenY1);
builder.lineTo(rect.x + normX0 * rect.w, screenY1);
builder.close();
}
/** 蒙版路径构建降采样(屏幕显示不需要分割分辨率,点击仍用全分辨率 pickMap */
export function downsampleMaskDataForPaths(maskData, maxLongSide) {
const { labels, baseboardBinary, cols, rows, wallSubLabels } = maskData;
const longSide = Math.max(cols, rows);
if (longSide <= maxLongSide) {
return maskData;
}
const scale = maxLongSide / longSide;
const dstCols = Math.max(1, Math.floor(cols * scale));
const dstRows = Math.max(1, Math.floor(rows * scale));
const outLabels = 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++) {
const sy = Math.min(rows - 1, Math.floor((y * rows) / dstRows));
const srcRow = sy * cols;
const dstRow = y * dstCols;
for (let x = 0; x < dstCols; x++) {
const sx = Math.min(cols - 1, Math.floor((x * cols) / dstCols));
const si = srcRow + sx;
const di = dstRow + x;
outLabels[di] = labels[si];
outBaseboard[di] = baseboardBinary[si];
if (outWallSub && wallSubLabels) {
outWallSub[di] = wallSubLabels[si];
}
}
}
return {
labels: outLabels,
baseboardBinary: outBaseboard,
cols: dstCols,
rows: dstRows,
wallSubLabels: outWallSub,
};
}
/** 单次扫描构建所有分区 Skia 蒙版路径(单 label pass避免每像素 × 语义数循环) */
export function buildAllRegionMaskPaths(regions, maskData, rect) {
const { labels, baseboardBinary, cols, rows } = maskData;
const builders = new Map();
const semanticColors = getMaskSegmentRuntimeConfig().mask.semanticColors;
const regionIdBySemantic = new Int32Array(semanticColors.length);
regionIdBySemantic.fill(-1);
let baseboardRegionId = null;
for (const reg of regions) {
builders.set(reg.id, Skia.Path.Make());
if (reg.thinStrip) {
baseboardRegionId = reg.id;
continue;
}
const semanticIndex = semanticColors.findIndex(entry => entry.name === reg.name);
if (semanticIndex >= 0) {
regionIdBySemantic[semanticIndex] = reg.id;
}
}
const semanticCount = semanticColors.length;
const minRunPx = maskCfg().baseboardMinRunPx;
for (let y = 0; y < rows; y++) {
let baseboardRunStart = -1;
let labelRunStart = -1;
let labelRunSemantic = -1;
const row = y * cols;
for (let x = 0; x <= cols; x++) {
if (baseboardRegionId != null) {
const bbOn = x < cols && baseboardBinary[row + x] > 0;
if (bbOn && baseboardRunStart < 0) {
baseboardRunStart = x;
}
if (!bbOn && baseboardRunStart >= 0) {
appendRunRectToBuilder(baseboardRunStart, x, y, cols, rows, rect, builders.get(baseboardRegionId), minRunPx);
baseboardRunStart = -1;
}
}
let activeSemantic = -1;
if (x < cols) {
const si = labels[row + x];
if (si < semanticCount && regionIdBySemantic[si] >= 0) {
activeSemantic = si;
}
}
if (activeSemantic !== labelRunSemantic) {
if (labelRunSemantic >= 0 && labelRunStart >= 0) {
const regionId = regionIdBySemantic[labelRunSemantic];
appendRunRectToBuilder(labelRunStart, x, y, cols, rows, rect, builders.get(regionId), minRunPx);
}
labelRunSemantic = activeSemantic;
labelRunStart = activeSemantic >= 0 ? x : -1;
}
}
}
const paths = new Map();
for (const [regionId, builder] of builders) {
paths.set(regionId, builder);
}
return paths;
}
function collectRowRuns(binary, cols, rows, minRunPx) {
const runs = [];
for (let y = 0; y < rows; y++) {
let runStart = -1;
for (let x = 0; x <= cols; x++) {
const on = x < cols && binary[y * cols + x] > 0;
if (on && runStart < 0) {
runStart = x;
}
if (!on && runStart >= 0) {
if (x - runStart >= minRunPx) {
runs.push({ minX: runStart, maxX: x - 1, y });
}
runStart = -1;
}
}
}
return runs;
}
function bboxFromPolygons(polygons) {
if (polygons.length === 0) {
return null;
}
let minX = 1;
let minY = 1;
let maxX = 0;
let maxY = 0;
for (const polygon of polygons) {
for (const point of polygon) {
minX = Math.min(minX, point.x);
minY = Math.min(minY, point.y);
maxX = Math.max(maxX, point.x);
maxY = Math.max(maxY, point.y);
}
}
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
}
/** baseboard逐行 1px 条带贴合掩码;点击用横向补缝后的条带 */
function extractBaseboardRowPolygons(binary, cols, rows) {
let totalArea = 0;
for (let i = 0; i < binary.length; i++) {
if (binary[i]) {
totalArea += 1;
}
}
const runs = collectRowRuns(binary, cols, rows, maskCfg().baseboardMinRunPx);
const polygons = rowRunsToPolygons(runs, cols, rows);
const bridged = bridgeBaseboardHorizontally(binary, cols, rows);
const bridgedRuns = collectRowRuns(bridged, cols, rows, maskCfg().baseboardMinRunPx);
const hitPolygons = rowRunsToPolygons(bridgedRuns, cols, rows);
const bbox = bboxFromPolygons(polygons);
return {
polygons,
hitPolygons,
totalArea,
bbox,
};
}
function cloneBinary(binary) {
return new Uint8Array(binary);
}
function subtractBinary(target, mask) {
for (let i = 0; i < target.length; i++) {
if (mask[i]) {
target[i] = 0;
}
}
}
function minPalettePixels(cols, rows) {
return Math.max(300, Math.floor((cols * rows) / 2000));
}
function minPixelsForSemantic(name, cols, rows) {
const base = minPalettePixels(cols, rows);
if (!maskCfg().secondarySemanticNames.has(name)) {
return base;
}
return Math.max(base, Math.floor(cols * rows * maskCfg().secondaryMinPixelRatio));
}
function quantizeChannel(value) {
return Math.min(255, Math.round(value / maskCfg().quantStep) * maskCfg().quantStep);
}
function maskQuantKey(b, g, r) {
return `${quantizeChannel(b)},${quantizeChannel(g)},${quantizeChannel(r)}`;
}
function dilateBinaryBox(source, cols, rows, radiusX, radiusY) {
const temp = new Uint8Array(source.length);
const out = new Uint8Array(source.length);
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
if (!source[y * cols + x]) {
continue;
}
const minX = Math.max(0, x - radiusX);
const maxX = Math.min(cols - 1, x + radiusX);
for (let nx = minX; nx <= maxX; nx++) {
temp[y * cols + nx] = 255;
}
}
}
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
if (!temp[y * cols + x]) {
continue;
}
const minY = Math.max(0, y - radiusY);
const maxY = Math.min(rows - 1, y + radiusY);
for (let ny = minY; ny <= maxY; ny++) {
out[ny * cols + x] = 255;
}
}
}
return out;
}
function buildWallCabinetJunctionMask(buffer, cols, rows) {
const wall = new Uint8Array(cols * rows);
const cabinet = new Uint8Array(cols * rows);
for (let i = 0; i < cols * rows; i++) {
const o = i * 3;
const b = buffer[o];
const g = buffer[o + 1];
const r = buffer[o + 2];
if (isIgnoredColor(b, g, r)) {
continue;
}
const key = maskQuantKey(b, g, r);
if (getWallQuantKeys().has(key)) {
wall[i] = 255;
}
if (getCabinetQuantKeys().has(key)) {
cabinet[i] = 255;
}
}
const wallNear = dilateBinaryBox(wall, cols, rows, maskCfg().junctionHRadiusPx, maskCfg().junctionVRadiusPx);
const cabinetNear = dilateBinaryBox(cabinet, cols, rows, maskCfg().junctionHRadiusPx, maskCfg().junctionVRadiusPx);
const junction = new Uint8Array(cols * rows);
for (let i = 0; i < junction.length; i++) {
if (wallNear[i] && cabinetNear[i]) {
junction[i] = 255;
}
}
return junction;
}
function computeStrictBaseboardBand(strictBaseboard, cols, rows) {
let minY = rows;
let maxY = -1;
for (let y = 0; y < rows; y++) {
const row = y * cols;
for (let x = 0; x < cols; x++) {
if (!strictBaseboard[row + x]) {
continue;
}
if (y < minY) {
minY = y;
}
if (y > maxY) {
maxY = y;
}
}
}
if (maxY < 0) {
return null;
}
return { minY, maxY };
}
/** 仅保留贴近真实踢脚线带的 junction 细条,避免上方墙柜交界零碎区域误入 */
function isJunctionNearStrictBaseboard(idx, strictBaseboard, cols, rows, band) {
if (!band) {
return false;
}
const x = idx % cols;
const y = (idx - x) / cols;
if (y < band.minY - maskCfg().baseboardJunctionRowMarginPx ||
y > band.maxY + maskCfg().baseboardJunctionRowMarginPx) {
return false;
}
const halfW = maskCfg().kickBridgeHalfWPx;
for (let dy = -maskCfg().baseboardJunctionVReachPx; dy <= maskCfg().baseboardJunctionVReachPx; dy++) {
const ny = y + dy;
if (ny < 0 || ny >= rows) {
continue;
}
const row = ny * cols;
for (let dx = -halfW; dx <= halfW; dx++) {
const nx = x + dx;
if (nx < 0 || nx >= cols) {
continue;
}
if (strictBaseboard[row + nx]) {
return true;
}
}
}
return false;
}
function buildBaseboardBinary(buffer, cols, rows, junctionMask) {
const binary = new Uint8Array(cols * rows);
const junction = junctionMask ?? buildWallCabinetJunctionMask(buffer, cols, rows);
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const idx = y * cols + x;
const o = idx * 3;
const b = buffer[o];
const g = buffer[o + 1];
const r = buffer[o + 2];
if (isIgnoredColor(b, g, r)) {
continue;
}
if (isStrictBaseboardPixel(b, g, r)) {
binary[idx] = 255;
}
}
}
const band = computeStrictBaseboardBand(binary, cols, rows);
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const idx = y * cols + x;
if (binary[idx]) {
continue;
}
const o = idx * 3;
const b = buffer[o];
const g = buffer[o + 1];
const r = buffer[o + 2];
if (isIgnoredColor(b, g, r)) {
continue;
}
const key = maskQuantKey(b, g, r);
if (getBaseboardStripQuantKeys().has(key) &&
junction[idx] &&
isJunctionNearStrictBaseboard(idx, binary, cols, rows, band)) {
binary[idx] = 255;
}
}
}
return binary;
}
export function buildBaseboardBinaryFromMask(buffer, cols, rows) {
return buildBaseboardBinary(buffer, cols, rows);
}
/** 分割分辨率踢脚线二值图最近邻放大到点击查表分辨率(避免全图 junction 重算) */
export function upscaleBinaryMask(src, srcCols, srcRows, dstCols, dstRows) {
const dst = new Uint8Array(dstCols * dstRows);
for (let y = 0; y < dstRows; y++) {
const sy = Math.min(srcRows - 1, Math.floor((y * srcRows) / dstRows));
const srcRow = sy * srcCols;
const dstRow = y * dstCols;
for (let x = 0; x < dstCols; x++) {
const sx = Math.min(srcCols - 1, Math.floor((x * srcCols) / dstCols));
dst[dstRow + x] = src[srcRow + sx];
}
}
return dst;
}
function buildMaskPolygonsFromBinary(binary, cols, rows) {
return rowRunsToPolygons(collectRowRuns(binary, cols, rows, maskCfg().baseboardMinRunPx), cols, rows);
}
export function isBaseboardMaskPixel(buffer, cols, rows, x, y, baseboardBinary) {
if (x < 0 || y < 0 || x >= cols || y >= rows) {
return false;
}
if (baseboardBinary) {
return baseboardBinary[y * cols + x] > 0;
}
const o = (y * cols + x) * 3;
const b = buffer[o];
const g = buffer[o + 1];
const r = buffer[o + 2];
if (isIgnoredColor(b, g, r)) {
return false;
}
if (isStrictBaseboardPixel(b, g, r)) {
return true;
}
const key = maskQuantKey(b, g, r);
if (!getBaseboardStripQuantKeys().has(key)) {
return false;
}
const junction = buildWallCabinetJunctionMask(buffer, cols, rows);
return junction[y * cols + x] > 0;
}
export { isStrictBaseboardPixel as isBaseboardPixel } from './maskSemanticPalette';
export function getMaskQuantKey(b, g, r) {
return maskQuantKey(b, g, r);
}
/** @deprecated 请使用 isBaseboardMaskPixel */
export function isKickPlatePixel(b, g, r) {
return isStrictBaseboardPixel(b, g, r);
}
function mergeBBox(bbox, next) {
const x1 = Math.min(bbox.x, next.x);
const y1 = Math.min(bbox.y, next.y);
const x2 = Math.max(bbox.x + bbox.w, next.x + next.w);
const y2 = Math.max(bbox.y + bbox.h, next.y + next.h);
return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
}
function isIgnoredColor(b, g, r) {
const threshold = maskCfg().blackThreshold;
return b < threshold && g < threshold && r < threshold;
}
function countBinaryPixels(binary) {
let count = 0;
for (let i = 0; i < binary.length; i++) {
if (binary[i]) {
count += 1;
}
}
return count;
}
const IGNORE_SEMANTIC_INDEX = 255;
let nameToIndexRevision = -1;
let cachedNameToIndex = null;
function getSemanticNameToIndex() {
const revision = getMaskRuntimeRevision();
if (nameToIndexRevision === revision && cachedNameToIndex) {
return cachedNameToIndex;
}
const colors = getMaskSegmentRuntimeConfig().mask.semanticColors;
cachedNameToIndex = new Map(colors.map((entry, index) => [entry.name, index]));
nameToIndexRevision = revision;
return cachedNameToIndex;
}
function createSemanticLut() {
const revision = getMaskRuntimeRevision();
if (semanticLutRevision === revision && cachedSemanticLut) {
return cachedSemanticLut;
}
const lut = new Uint8Array(125);
lut.fill(IGNORE_SEMANTIC_INDEX);
const colors = getMaskSegmentRuntimeConfig().mask.semanticColors;
const nameToIndex = getSemanticNameToIndex();
for (const entry of colors) {
const semanticIndex = nameToIndex.get(entry.name);
if (semanticIndex === undefined) {
continue;
}
const { b, g, r } = entry.bgr;
lut[quantKeyIndex(b, g, r)] = semanticIndex;
}
for (let idx = 0; idx < 125; idx++) {
if (lut[idx] !== IGNORE_SEMANTIC_INDEX) {
continue;
}
const [b, g, r] = quantIndexToBgr(idx);
const name = classifyBgrPixelToSemantic(b, g, r);
lut[idx] = nameToIndex.get(name) ?? IGNORE_SEMANTIC_INDEX;
}
cachedSemanticLut = lut;
semanticLutRevision = revision;
return lut;
}
/** 单次扫描:像素语义标签 + 像素计数 + bbox不写多张二值图 */
function buildSemanticLayout(buffer, cols, rows) {
const pixelCount = cols * rows;
const labels = new Uint8Array(pixelCount);
labels.fill(IGNORE_SEMANTIC_INDEX);
const counts = new Map();
const bboxes = new Map();
const semanticLut = createSemanticLut();
const nameToIndex = getSemanticNameToIndex();
const indexToName = getMaskSegmentRuntimeConfig().mask.semanticColors.map(entry => entry.name);
const semanticCount = indexToName.length;
const blackThreshold = maskCfg().blackThreshold;
const channelSlotLut = getQuantChannelSlotLut();
const stripQuantLut = getStripQuantLut();
const minX = new Int32Array(semanticCount);
const minY = new Int32Array(semanticCount);
const maxX = new Int32Array(semanticCount);
const maxY = new Int32Array(semanticCount);
const hitMask = new Uint8Array(semanticCount);
const countArr = new Int32Array(semanticCount);
const stripIndices = [];
const strictBaseboard = new Uint8Array(pixelCount);
let strictBaseboardCount = 0;
const baseboardIdx = nameToIndex.get(BASEBOARD_SEMANTIC_NAME);
minX.fill(cols);
minY.fill(rows);
maxX.fill(-1);
maxY.fill(-1);
const buf = buffer;
for (let y = 0; y < rows; y++) {
const row = y * cols;
for (let x = 0; x < cols; x++) {
const i = row + x;
const o = i * 3;
const b = buf[o];
const g = buf[o + 1];
const r = buf[o + 2];
if (b < blackThreshold && g < blackThreshold && r < blackThreshold) {
continue;
}
const lutIdx = channelSlotLut[b] * 25 + channelSlotLut[g] * 5 + channelSlotLut[r];
if (stripQuantLut[lutIdx]) {
stripIndices.push(i);
}
const semanticIndex = semanticLut[lutIdx];
if (semanticIndex === IGNORE_SEMANTIC_INDEX) {
continue;
}
labels[i] = semanticIndex;
hitMask[semanticIndex] = 1;
countArr[semanticIndex] += 1;
if (semanticIndex === baseboardIdx) {
strictBaseboard[i] = 255;
strictBaseboardCount += 1;
}
if (x < minX[semanticIndex]) {
minX[semanticIndex] = x;
}
if (x > maxX[semanticIndex]) {
maxX[semanticIndex] = x;
}
if (y < minY[semanticIndex]) {
minY[semanticIndex] = y;
}
if (y > maxY[semanticIndex]) {
maxY[semanticIndex] = y;
}
}
}
const invCols = 1 / cols;
const invRows = 1 / rows;
for (let semanticIndex = 0; semanticIndex < semanticCount; semanticIndex++) {
if (!hitMask[semanticIndex]) {
continue;
}
const name = indexToName[semanticIndex];
counts.set(name, countArr[semanticIndex]);
bboxes.set(name, {
x: minX[semanticIndex] * invCols,
y: minY[semanticIndex] * invRows,
w: (maxX[semanticIndex] - minX[semanticIndex] + 1) * invCols,
h: (maxY[semanticIndex] - minY[semanticIndex] + 1) * invRows,
});
}
return {
labels,
counts,
bboxes,
stripIndices,
strictBaseboard,
strictBaseboardCount,
};
}
function buildJunctionAtStripPixels(labels, stripIndices, cols, rows) {
const junction = new Uint8Array(cols * rows);
const junctionIndices = [];
const nameToIndex = getSemanticNameToIndex();
const wallIdx = nameToIndex.get('wall');
const cabinetIdx = nameToIndex.get('cabinet');
const junctionH = maskCfg().junctionHRadiusPx;
const junctionV = maskCfg().junctionVRadiusPx;
if (wallIdx === undefined ||
cabinetIdx === undefined ||
stripIndices.length === 0) {
return { junction, junctionIndices };
}
for (const idx of stripIndices) {
const x = idx % cols;
const y = (idx - x) / cols;
let hasWall = false;
let hasCabinet = false;
const minY = Math.max(0, y - junctionV);
const maxY = Math.min(rows - 1, y + junctionV);
const minX = Math.max(0, x - junctionH);
const maxX = Math.min(cols - 1, x + junctionH);
for (let ny = minY; ny <= maxY && !(hasWall && hasCabinet); ny++) {
const rowBase = ny * cols;
for (let nx = minX; nx <= maxX && !(hasWall && hasCabinet); nx++) {
const label = labels[rowBase + nx];
if (label === wallIdx) {
hasWall = true;
}
else if (label === cabinetIdx) {
hasCabinet = true;
}
}
}
if (hasWall && hasCabinet) {
junction[idx] = 255;
junctionIndices.push(idx);
}
}
return { junction, junctionIndices };
}
function finalizeBaseboardBinary(strictBaseboard, strictCount, junctionIndices, cols, rows) {
const binary = new Uint8Array(strictBaseboard);
const band = computeStrictBaseboardBand(binary, cols, rows);
let pixelCount = strictCount;
for (const idx of junctionIndices) {
if (!isJunctionNearStrictBaseboard(idx, binary, cols, rows, band)) {
continue;
}
if (!binary[idx]) {
pixelCount += 1;
}
binary[idx] = 255;
}
return { binary, pixelCount };
}
function buildPickMapAndWorkAreas(labels, indexToName, nameToId, baseboardBinary, cols, rows) {
const pixelCount = cols * rows;
const pick = new Uint8Array(pixelCount);
const workAreas = new Map();
const baseboardName = BASEBOARD_SEMANTIC_NAME;
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;
}
workAreas.set(baseboardName, (workAreas.get(baseboardName) ?? 0) + 1);
continue;
}
const semanticIndex = labels[i];
if (semanticIndex === IGNORE_SEMANTIC_INDEX) {
continue;
}
const name = indexToName[semanticIndex];
if (!name) {
continue;
}
const regionId = nameToId.get(name);
if (regionId !== undefined) {
pick[i] = regionId + 1;
}
workAreas.set(name, (workAreas.get(name) ?? 0) + 1);
}
return { pick, workAreas };
}
/**
* 1-pass 8-neighbour majority-vote dilate on the pick buffer.
* For each zero pixel, count the 8-connected neighbours by their non-zero
* pick code. If any single code appears in 4 neighbours, fill the pixel
* with that code (majority rule).
*
* Compared to the old 4-neighbour "all-must-agree" rule this handles:
* - diagonal holes inside a region (8-connectivity)
* - narrow door / furniture strips where a hole borders both the strip
* AND a neighbouring wall region majority vote picks the region that
* occupies more of the 8-pixel perimeter
*
* Still reads from the ORIGINAL pick buffer to prevent cascade overflow.
* Cost: O(N) with ~20 ops/pixel negligible relative to segmentation.
*/
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;
// Read 8 neighbours from the ORIGINAL pick buffer to avoid cascade.
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)], // bottom-right
];
// Count occurrences of each non-zero code.
const counts = {};
for (let k = 0; k < 8; k++) {
const code = n[k];
if (code !== 0) {
counts[code] = (counts[code] ?? 0) + 1;
}
}
// Majority rule: ≥ 4 of 8 neighbours share the same code.
for (const codeStr of Object.keys(counts)) {
const code = Number(codeStr);
if (counts[code] >= 4) {
dst[i] = code;
break;
}
}
}
}
return dst;
}
function paletteFromCounts(counts, cols, rows) {
const orderedSemantics = getMaskSegmentRuntimeConfig().mask.semanticColors.map(entry => entry.name);
return orderedSemantics
.map(name => {
const pixelCount = counts.get(name) ?? 0;
if (pixelCount < minPixelsForSemantic(name, cols, rows)) {
return null;
}
const ref = getSemanticColorByName(name);
return {
label: orderedSemantics.indexOf(name),
name,
hex: ref.hex,
color: { ...ref.bgr },
};
})
.filter((entry) => entry != null)
.sort((a, b) => (counts.get(b.name) ?? 0) - (counts.get(a.name) ?? 0))
.slice(0, maskCfg().maxRegionColors);
}
async function contourToPolygon(contour, cols, rows, minArea, approxEpsilon) {
const area = await cv.contourArea(contour);
if (area < minArea) {
return null;
}
const rect = await cv.boundingRect(contour);
const perimeter = await cv.arcLength(contour, true);
const maxEpsilonPx = Math.max(cols, rows) * 0.01;
const thinSide = Math.min(rect.width, rect.height);
const epsilonPx = Math.max(1.5, Math.min(perimeter * approxEpsilon, maxEpsilonPx, Math.max(2, thinSide * 0.12)));
const points = await cv.approxPolyDP(contour, epsilonPx, true);
if (points.length < 3) {
return null;
}
let minX = cols;
let minY = rows;
let maxX = 0;
let maxY = 0;
const polygon = points.map(point => {
minX = Math.min(minX, point.x);
maxX = Math.max(maxX, point.x);
minY = Math.min(minY, point.y);
maxY = Math.max(maxY, point.y);
return { x: point.x / cols, y: point.y / rows };
});
return {
polygon,
area,
bbox: {
x: minX / cols,
y: minY / rows,
w: (maxX - minX + 1) / cols,
h: (maxY - minY + 1) / rows,
},
};
}
function extractPolygonsFromBinaryJs(binary, cols, rows, minArea) {
const visited = new Uint8Array(cols * rows);
const polygons = [];
let totalArea = 0;
let bbox = null;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const idx = y * cols + x;
if (!binary[idx] || visited[idx]) {
continue;
}
let minX = x;
let maxX = x;
let minY = y;
let maxY = y;
let area = 0;
const stack = [[x, y]];
visited[idx] = 1;
while (stack.length > 0) {
const [cx, cy] = stack.pop();
area += 1;
minX = Math.min(minX, cx);
maxX = Math.max(maxX, cx);
minY = Math.min(minY, cy);
maxY = Math.max(maxY, cy);
if (cx > 0) {
const left = cy * cols + (cx - 1);
if (binary[left] && !visited[left]) {
visited[left] = 1;
stack.push([cx - 1, cy]);
}
}
if (cx + 1 < cols) {
const right = cy * cols + (cx + 1);
if (binary[right] && !visited[right]) {
visited[right] = 1;
stack.push([cx + 1, cy]);
}
}
if (cy > 0) {
const up = (cy - 1) * cols + cx;
if (binary[up] && !visited[up]) {
visited[up] = 1;
stack.push([cx, cy - 1]);
}
}
if (cy + 1 < rows) {
const down = (cy + 1) * cols + cx;
if (binary[down] && !visited[down]) {
visited[down] = 1;
stack.push([cx, cy + 1]);
}
}
}
if (area < minArea) {
continue;
}
const polygon = [
{ x: minX / cols, y: minY / rows },
{ x: (maxX + 1) / cols, y: minY / rows },
{ x: (maxX + 1) / cols, y: (maxY + 1) / rows },
{ x: minX / cols, y: (maxY + 1) / rows },
];
const partBbox = {
x: minX / cols,
y: minY / rows,
w: (maxX - minX + 1) / cols,
h: (maxY - minY + 1) / rows,
};
polygons.push(polygon);
totalArea += area;
bbox = bbox ? mergeBBox(bbox, partBbox) : partBbox;
}
}
return { polygons, totalArea, bbox };
}
async function extractPolygonsFromBinary(binary, cols, rows, minArea, approxEpsilon) {
const binaryMat = cv.binaryBufferToMat(binary, cols, rows);
const closed = cv.createMat(cols, rows, 1);
try {
const kernel = await cv.getStructuringElement(cv.MORPH_ELLIPSE, {
width: MORPH_KERNEL_SIZE,
height: MORPH_KERNEL_SIZE,
});
try {
await cv.morphologyEx(binaryMat, closed, cv.MORPH_OPEN, kernel);
}
finally {
kernel.release();
}
const contours = await cv.findContours(closed, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
const polygons = [];
let totalArea = 0;
let bbox = null;
for (const contour of contours) {
try {
const part = await contourToPolygon(contour, cols, rows, minArea, approxEpsilon);
if (!part) {
continue;
}
polygons.push(part.polygon);
totalArea += part.area;
bbox = bbox ? mergeBBox(bbox, part.bbox) : part.bbox;
}
finally {
contour.release();
}
}
return { polygons, totalArea, bbox };
}
finally {
binaryMat.release();
closed.release();
}
}
export async function extractRegionsFromMaskBuffer(buffer, cols, rows, _options) {
return extractRegionsFromMaskBufferSync(buffer, cols, rows, _options);
}
export function extractRegionsFromMaskBufferSync(buffer, cols, rows, _options) {
const layout = buildSemanticLayout(buffer, cols, rows);
const baseboardStart = __DEV__ ? performance.now() : 0;
const { junctionIndices } = buildJunctionAtStripPixels(layout.labels, layout.stripIndices, cols, rows);
const { binary: baseboardBinary, pixelCount: baseboardPixels } = finalizeBaseboardBinary(layout.strictBaseboard, layout.strictBaseboardCount, junctionIndices, cols, rows);
layout.counts.set(BASEBOARD_SEMANTIC_NAME, baseboardPixels);
const paletteEntries = paletteFromCounts(layout.counts, cols, rows);
if (paletteEntries.length === 0) {
return {
regions: [],
pickMap: { buffer: new Uint8Array(cols * rows), cols, rows },
labels: layout.labels,
baseboardBinary,
segCols: cols,
segRows: rows,
};
}
const indexToName = getMaskSegmentRuntimeConfig().mask.semanticColors.map(entry => entry.name);
const regionResults = paletteEntries.map((entry) => {
const { label, name, hex, color } = entry;
const isBaseboard = isBaseboardEntry(entry);
try {
const finalBbox = isBaseboard
? bboxFromBinary(baseboardBinary, cols, rows)
: layout.bboxes.get(name);
if (!finalBbox) {
if (__DEV__) {
console.warn(`[MaskSegment] ${name} 无有效轮廓,已跳过`);
}
return null;
}
const finalPolygons = [bboxToPolygon(finalBbox)];
return {
id: label,
name,
hex,
color,
area: layout.counts.get(name) ?? 0,
bbox: finalBbox,
polygons: finalPolygons,
outlinePolygons: finalPolygons,
thinStrip: isBaseboard,
};
}
catch (error) {
if (__DEV__) {
console.warn(`[MaskSegment] 色 #${label} 提取失败:`, error instanceof Error ? error.message : String(error));
}
return null;
}
});
const regions = regionResults.filter((region) => region != null);
regions.sort((a, b) => b.area - a.area);
regions.forEach((reg, index) => {
reg.id = index;
});
const finalRegions = regions.slice(0, maskCfg().maxRegionColors);
const nameToId = new Map(finalRegions.map(reg => [reg.name, reg.id]));
const pickBuildStart = __DEV__ ? performance.now() : 0;
const { pick: pickBufferRaw, workAreas } = buildPickMapAndWorkAreas(layout.labels, indexToName, nameToId, baseboardBinary, cols, rows);
const pickBuffer = dilatePickBuffer1px(pickBufferRaw, cols, rows);
for (const reg of finalRegions) {
const workArea = workAreas.get(reg.name);
if (workArea != null) {
reg.area = workArea;
}
}
return {
regions: finalRegions,
pickMap: { buffer: pickBuffer, cols, rows },
labels: layout.labels,
baseboardBinary,
segCols: cols,
segRows: rows,
};
}
/** @deprecated 请使用 extractRegionsFromMaskBuffer */
export async function extractRegionsFromMask(maskMat, options) {
const { buffer, cols, rows } = cv.matToBuffer(maskMat);
const result = extractRegionsFromMaskBufferSync(buffer, cols, rows, options);
return result.regions;
}
//# sourceMappingURL=maskSegmentation.js.map