react-native-mask-segment-c.../dist/utils/paintColorMapTexture.js
a1518 56738b1f06 feat: add MaskSegmentApp source code and config
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 00:23:52 -07:00

203 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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