feat: implement inpainting functionality with mask handling
- Added `createMaskedPixelReplacementSource` function to handle pixel replacement using inpainting. - Introduced `buildInpaintBundle` to prepare inpainting data including mask generation and validation. - Created utility functions for mask operations such as `applyMaskedContentModeToRgba`, `expandRectWithinBounds`, and others for mask manipulation. - Developed tests for inpainting preparation and mask raster utilities to ensure functionality and correctness. - Implemented mask raster operations including inversion, feathering, blurring, and more.
This commit is contained in:
@@ -7,6 +7,7 @@ import { resolveTransformTargetBounds } from "@editor/transform-targets";
|
||||
import type { AppStore } from "@editor/store";
|
||||
import type { ChromaKeySettings } from "@editor/tools";
|
||||
import type { SelectionState } from "@editor/state";
|
||||
import { blurMaskValues, despeckleMaskValues, dilateMaskValues, erodeMaskValues } from "../mask/maskRaster";
|
||||
import { BottomControlColorPicker } from "./ColorPicker";
|
||||
import { BottomControlDivider } from "./Divider";
|
||||
import { BottomControlSlider } from "./Slider";
|
||||
@@ -179,8 +180,8 @@ async function applyChromaKeyMask(target: NonNullable<ReturnType<typeof resolveC
|
||||
const source = await chromaKeyMaskSource(target.asset.source, target.asset.intrinsicSize.w, target.asset.intrinsicSize.h, settings);
|
||||
dispatch(commandIds.toolSetBrushStrokePreview, undefined);
|
||||
|
||||
if (target.maskAsset) {
|
||||
dispatch(commandIds.documentUpdateAssetSource, { assetId: target.maskAsset.id, source });
|
||||
if (target.maskAsset && target.maskLayer && target.maskLayer.type !== "group") {
|
||||
dispatch(commandIds.documentApplyLayerMaskOperation, { maskLayerId: target.maskLayer.id, source, mimeType: "image/png", operation: { type: "chromaKey" } });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -293,85 +294,13 @@ function postProcessAlpha(alpha: Uint8ClampedArray, width: number, height: numbe
|
||||
const choke = Math.round(Math.max(-20, Math.min(20, settings.choke)));
|
||||
const feather = Math.round(Math.max(0, Math.min(20, settings.feather)));
|
||||
|
||||
if (despeckle > 0) next = despeckleAlpha(next, width, height, despeckle);
|
||||
if (choke > 0) next = erodeAlpha(next, width, height, choke);
|
||||
if (choke < 0) next = dilateAlpha(next, width, height, -choke);
|
||||
if (feather > 0) next = blurAlpha(next, width, height, feather);
|
||||
if (despeckle > 0) next = despeckleMaskValues(next, width, height, despeckle);
|
||||
if (choke > 0) next = erodeMaskValues(next, width, height, choke);
|
||||
if (choke < 0) next = dilateMaskValues(next, width, height, -choke);
|
||||
if (feather > 0) next = blurMaskValues(next, width, height, feather);
|
||||
return next;
|
||||
}
|
||||
|
||||
function despeckleAlpha(alpha: Uint8ClampedArray, width: number, height: number, strength: number) {
|
||||
const radius = Math.max(1, Math.ceil(strength / 6));
|
||||
const threshold = Math.max(1, Math.round(strength / 2));
|
||||
const next = new Uint8ClampedArray(alpha);
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const index = y * width + x;
|
||||
const visible = (alpha[index] ?? 0) > 127;
|
||||
let same = 0;
|
||||
for (let oy = -radius; oy <= radius; oy++) {
|
||||
for (let ox = -radius; ox <= radius; ox++) {
|
||||
if (ox === 0 && oy === 0) continue;
|
||||
const sample = alpha[clamp(y + oy, 0, height - 1) * width + clamp(x + ox, 0, width - 1)] ?? 0;
|
||||
if ((sample > 127) === visible) same += 1;
|
||||
}
|
||||
}
|
||||
if (same <= threshold) next[index] = visible ? 0 : 255;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function erodeAlpha(alpha: Uint8ClampedArray, width: number, height: number, radius: number) {
|
||||
const next = new Uint8ClampedArray(alpha.length);
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
let value = 255;
|
||||
for (let oy = -radius; oy <= radius; oy++) {
|
||||
for (let ox = -radius; ox <= radius; ox++) value = Math.min(value, alpha[clamp(y + oy, 0, height - 1) * width + clamp(x + ox, 0, width - 1)] ?? 0);
|
||||
}
|
||||
next[y * width + x] = value;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function dilateAlpha(alpha: Uint8ClampedArray, width: number, height: number, radius: number) {
|
||||
const next = new Uint8ClampedArray(alpha.length);
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
let value = 0;
|
||||
for (let oy = -radius; oy <= radius; oy++) {
|
||||
for (let ox = -radius; ox <= radius; ox++) value = Math.max(value, alpha[clamp(y + oy, 0, height - 1) * width + clamp(x + ox, 0, width - 1)] ?? 0);
|
||||
}
|
||||
next[y * width + x] = value;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function blurAlpha(alpha: Uint8ClampedArray, width: number, height: number, radius: number) {
|
||||
const next = new Uint8ClampedArray(alpha.length);
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
let total = 0;
|
||||
let count = 0;
|
||||
for (let oy = -radius; oy <= radius; oy++) {
|
||||
for (let ox = -radius; ox <= radius; ox++) {
|
||||
total += alpha[clamp(y + oy, 0, height - 1) * width + clamp(x + ox, 0, width - 1)] ?? 0;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
next[y * width + x] = Math.round(total / count);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function loadImage(source: string) {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image();
|
||||
|
||||
Reference in New Issue
Block a user