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

203 lines
8.6 KiB
JavaScript
Raw Normal View History

import { Skia, AlphaType, ColorType, } from '@shopify/react-native-skia';
/**
* Separable box blur on premultiplied RGBA (4-channel 0-255).
* Feather radius in pixels (at the working pick buffer resolution).
* Works on premultiplied values so that color naturally diffuses alongside alpha
* into the feather region preventing "scatter dots" at resolution-mismatched boundaries.
*/
function boxBlurRgbaPremul(rgba, cols, rows, radius) {
const r = Math.max(1, Math.min(16, Math.round(radius)));
const tmp = new Uint8Array(cols * rows * 4);
const dst = new Uint8Array(cols * rows * 4);
// Horizontal pass
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
let sumR = 0, sumG = 0, sumB = 0, sumA = 0;
let cnt = 0;
const base = y * cols * 4;
for (let dx = -r; dx <= r; dx++) {
const xx = x + dx;
if (xx >= 0 && xx < cols) {
const idx = base + xx * 4;
sumR += rgba[idx];
sumG += rgba[idx + 1];
sumB += rgba[idx + 2];
sumA += rgba[idx + 3];
cnt++;
}
}
const o = base + x * 4;
tmp[o] = Math.round(sumR / cnt);
tmp[o + 1] = Math.round(sumG / cnt);
tmp[o + 2] = Math.round(sumB / cnt);
tmp[o + 3] = Math.round(sumA / cnt);
}
}
// Vertical pass
for (let x = 0; x < cols; x++) {
for (let y = 0; y < rows; y++) {
let sumR = 0, sumG = 0, sumB = 0, sumA = 0;
let cnt = 0;
for (let dy = -r; dy <= r; dy++) {
const yy = y + dy;
if (yy >= 0 && yy < rows) {
const idx = yy * cols * 4 + x * 4;
sumR += tmp[idx];
sumG += tmp[idx + 1];
sumB += tmp[idx + 2];
sumA += tmp[idx + 3];
cnt++;
}
}
const o = y * cols * 4 + x * 4;
dst[o] = Math.round(sumR / cnt);
dst[o + 1] = Math.round(sumG / cnt);
dst[o + 2] = Math.round(sumB / cnt);
dst[o + 3] = Math.round(sumA / cnt);
}
}
return dst;
}
/**
* Separable dilation (max filter) on premultiplied RGBA.
* Fills small holes (alpha=0) inside painted regions by taking the
* per-channel maximum in the neighborhood. Applied after boxBlur to
* eliminate scatter dots caused by segmentation gaps in the pick buffer.
*/
function dilateRgbaPremul(rgba, cols, rows, radius) {
const r = Math.max(1, Math.min(8, Math.round(radius)));
const tmp = new Uint8Array(cols * rows * 4);
const dst = new Uint8Array(cols * rows * 4);
// Horizontal pass
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
let maxR = 0, maxG = 0, maxB = 0, maxA = 0;
const base = y * cols * 4;
for (let dx = -r; dx <= r; dx++) {
const xx = x + dx;
if (xx >= 0 && xx < cols) {
const idx = base + xx * 4;
if (rgba[idx + 3] > maxA) {
maxR = rgba[idx];
maxG = rgba[idx + 1];
maxB = rgba[idx + 2];
maxA = rgba[idx + 3];
}
else if (rgba[idx + 3] === maxA && maxA > 0) {
maxR = Math.max(maxR, rgba[idx]);
maxG = Math.max(maxG, rgba[idx + 1]);
maxB = Math.max(maxB, rgba[idx + 2]);
}
}
}
const o = base + x * 4;
tmp[o] = maxR;
tmp[o + 1] = maxG;
tmp[o + 2] = maxB;
tmp[o + 3] = maxA;
}
}
// Vertical pass
for (let x = 0; x < cols; x++) {
for (let y = 0; y < rows; y++) {
let maxR = 0, maxG = 0, maxB = 0, maxA = 0;
for (let dy = -r; dy <= r; dy++) {
const yy = y + dy;
if (yy >= 0 && yy < rows) {
const idx = yy * cols * 4 + x * 4;
if (tmp[idx + 3] > maxA) {
maxR = tmp[idx];
maxG = tmp[idx + 1];
maxB = tmp[idx + 2];
maxA = tmp[idx + 3];
}
else if (tmp[idx + 3] === maxA && maxA > 0) {
maxR = Math.max(maxR, tmp[idx]);
maxG = Math.max(maxG, tmp[idx + 1]);
maxB = Math.max(maxB, tmp[idx + 2]);
}
}
}
const o = y * cols * 4 + x * 4;
dst[o] = maxR;
dst[o + 1] = maxG;
dst[o + 2] = maxB;
dst[o + 3] = maxA;
}
}
return dst;
}
/** 按 pickMap 展开的上色颜色图(与 pick 同尺寸,未上色像素 a=0。支持 maskFeather 产生软边缘 alpha。 */
export function buildPaintColorMapImage(pickBuffer, cols, rows, paintedRegions, featherRadius = 0) {
const pixelCount = cols * rows;
const colorByPickCode = new Map();
for (const [regionId, color] of paintedRegions) {
colorByPickCode.set(regionId + 1, color);
}
// ── Perf fast-path: no feather → 1-pass direct unpremul RGBA (O(N)) ──
// Skips premul buffer, boxBlur H+V, dilate H+V, and unpremul conversion.
// The GPU shader handles boundary noise via unpremultiply (rgb /= a) +
// smoothstep gate, so no CPU-side dilate/filter is needed.
if (featherRadius <= 0.1 && featherRadius >= -0.1) {
const rgba = new Uint8Array(pixelCount * 4);
for (let i = 0; i < pixelCount; i++) {
const code = pickBuffer[i];
const color = code > 0 ? colorByPickCode.get(code) : undefined;
const o = i * 4;
if (color) {
rgba[o] = color.r;
rgba[o + 1] = color.g;
rgba[o + 2] = color.b;
rgba[o + 3] = 255;
}
}
const data = Skia.Data.fromBytes(rgba);
return Skia.Image.MakeImage({ width: cols, height: rows, alphaType: AlphaType.Unpremul, colorType: ColorType.RGBA_8888 }, data, cols * 4);
}
// ── Feathered path: premul + boxBlur + dilate + unpremul ──
// Build hard premultiplied RGBA: where a pick code maps to a painted color,
// store (R, G, B, 255) in premul space. Unpainted pixels stay (0,0,0,0).
const hardPremul = new Uint8Array(pixelCount * 4);
for (let i = 0; i < pixelCount; i++) {
const code = pickBuffer[i];
const color = code > 0 ? colorByPickCode.get(code) : undefined;
const o = i * 4;
if (color) {
hardPremul[o] = color.r;
hardPremul[o + 1] = color.g;
hardPremul[o + 2] = color.b;
hardPremul[o + 3] = 255;
}
}
// Box-blur premultiplied RGBA so color naturally diffuses alongside alpha
// into the feather region. This prevents scatter dots from bilinear
// interpolation at resolution-mismatched boundaries (where unpainted=black
// would otherwise leak into boundary samples).
const blurredPremul = boxBlurRgbaPremul(hardPremul, cols, rows, featherRadius);
// Dilation pass: fill small holes (alpha=0) inside painted regions that
// survive the box blur due to segmentation gaps in the pick buffer.
const filledPremul = dilateRgbaPremul(blurredPremul, cols, rows, featherRadius);
// Unpremultiply and store as RGBA for the final SkImage.
const rgba = new Uint8Array(pixelCount * 4);
for (let i = 0; i < pixelCount; i++) {
const o = i * 4;
const a = filledPremul[o + 3];
if (a > 0) {
rgba[o] = Math.min(255, Math.round((filledPremul[o] * 255) / a));
rgba[o + 1] = Math.min(255, Math.round((filledPremul[o + 1] * 255) / a));
rgba[o + 2] = Math.min(255, Math.round((filledPremul[o + 2] * 255) / a));
rgba[o + 3] = a;
}
}
const data = Skia.Data.fromBytes(rgba);
return Skia.Image.MakeImage({
width: cols,
height: rows,
// Unpremul: rgb stores the target paint color (unmodulated), alpha stores coverage (soft edge feather).
// This ensures ImageShader sampling returns the intended paint color + separate coverage alpha,
// without premultiplication assumptions that could cause dark fringes on soft edges.
alphaType: AlphaType.Unpremul,
colorType: ColorType.RGBA_8888,
}, data, cols * 4);
}
//# sourceMappingURL=paintColorMapTexture.js.map