refactor: share mask distance transform
This commit is contained in:
+2
-66
@@ -1,5 +1,6 @@
|
||||
import { createModuleLogger } from "./log_system/log_funcs.js";
|
||||
import { createCanvas } from "./utils/CommonUtils.js";
|
||||
import { calculateDistanceTransform } from "./utils/MaskPixelUtils.js";
|
||||
const log = createModuleLogger('Mask_tool');
|
||||
export class MaskTool {
|
||||
constructor(canvasInstance, callbacks = {}) {
|
||||
@@ -548,7 +549,7 @@ export class MaskTool {
|
||||
*/
|
||||
createFeatheredMaskFromBinaryData(binaryData, featherRadius, width, height) {
|
||||
// Calculate the fast distance transform
|
||||
const distanceMap = this._fastDistanceTransform(binaryData, width, height);
|
||||
const distanceMap = calculateDistanceTransform(binaryData, width, height);
|
||||
// Find the maximum distance to normalize
|
||||
let maxDistance = 0;
|
||||
for (let i = 0; i < distanceMap.length; i++) {
|
||||
@@ -1828,71 +1829,6 @@ export class MaskTool {
|
||||
// 2. Use unified feathering logic
|
||||
return this.createFeatheredMaskFromBinaryData(binaryData, featherRadius, width, height);
|
||||
}
|
||||
/**
|
||||
* Fast distance transform using the simple two-pass algorithm from ImageAnalysis.ts
|
||||
* Much faster than the complex Felzenszwalb algorithm
|
||||
*/
|
||||
_fastDistanceTransform(binaryMask, width, height) {
|
||||
const distances = new Float32Array(width * height);
|
||||
const infinity = width + height; // A value larger than any possible distance
|
||||
// Initialize distances
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
distances[i] = binaryMask[i] === 1 ? infinity : 0;
|
||||
}
|
||||
// Forward pass (top-left to bottom-right)
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = y * width + x;
|
||||
if (distances[idx] > 0) {
|
||||
let minDist = distances[idx];
|
||||
// Check top neighbor
|
||||
if (y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + x] + 1);
|
||||
}
|
||||
// Check left neighbor
|
||||
if (x > 0) {
|
||||
minDist = Math.min(minDist, distances[y * width + (x - 1)] + 1);
|
||||
}
|
||||
// Check top-left diagonal
|
||||
if (x > 0 && y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + (x - 1)] + Math.sqrt(2));
|
||||
}
|
||||
// Check top-right diagonal
|
||||
if (x < width - 1 && y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + (x + 1)] + Math.sqrt(2));
|
||||
}
|
||||
distances[idx] = minDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Backward pass (bottom-right to top-left)
|
||||
for (let y = height - 1; y >= 0; y--) {
|
||||
for (let x = width - 1; x >= 0; x--) {
|
||||
const idx = y * width + x;
|
||||
if (distances[idx] > 0) {
|
||||
let minDist = distances[idx];
|
||||
// Check bottom neighbor
|
||||
if (y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + x] + 1);
|
||||
}
|
||||
// Check right neighbor
|
||||
if (x < width - 1) {
|
||||
minDist = Math.min(minDist, distances[y * width + (x + 1)] + 1);
|
||||
}
|
||||
// Check bottom-right diagonal
|
||||
if (x < width - 1 && y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + (x + 1)] + Math.sqrt(2));
|
||||
}
|
||||
// Check bottom-left diagonal
|
||||
if (x > 0 && y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + (x - 1)] + Math.sqrt(2));
|
||||
}
|
||||
distances[idx] = minDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
return distances;
|
||||
}
|
||||
/**
|
||||
* Creates an expanded/contracted mask canvas using simple morphological operations
|
||||
* This gives SHARP edges without smoothing, unlike distance transform
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createModuleLogger } from "../log_system/log_funcs.js";
|
||||
import { createCanvas } from "./CommonUtils.js";
|
||||
import { calculateDistanceTransform } from "./MaskPixelUtils.js";
|
||||
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||
const log = createModuleLogger('ImageAnalysis');
|
||||
/**
|
||||
@@ -102,75 +103,6 @@ export function createDistanceFieldMaskSync(image, blendArea) {
|
||||
export const createDistanceFieldMask = withErrorHandling(function (image, blendArea) {
|
||||
return createDistanceFieldMaskSync(image, blendArea);
|
||||
}, 'createDistanceFieldMask');
|
||||
/**
|
||||
* Calculates the Euclidean distance transform of a binary mask.
|
||||
* Uses a two-pass algorithm for efficiency.
|
||||
* @param binaryMask - Binary mask where 1 = inside, 0 = outside
|
||||
* @param width - Width of the mask
|
||||
* @param height - Height of the mask
|
||||
* @returns Float32Array containing distance values
|
||||
*/
|
||||
function calculateDistanceTransform(binaryMask, width, height) {
|
||||
const distances = new Float32Array(width * height);
|
||||
const infinity = width + height; // A value larger than any possible distance
|
||||
// Initialize distances
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
distances[i] = binaryMask[i] === 1 ? infinity : 0;
|
||||
}
|
||||
// Forward pass (top-left to bottom-right)
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = y * width + x;
|
||||
if (distances[idx] > 0) {
|
||||
let minDist = distances[idx];
|
||||
// Check top neighbor
|
||||
if (y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + x] + 1);
|
||||
}
|
||||
// Check left neighbor
|
||||
if (x > 0) {
|
||||
minDist = Math.min(minDist, distances[y * width + (x - 1)] + 1);
|
||||
}
|
||||
// Check top-left diagonal
|
||||
if (x > 0 && y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + (x - 1)] + Math.sqrt(2));
|
||||
}
|
||||
// Check top-right diagonal
|
||||
if (x < width - 1 && y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + (x + 1)] + Math.sqrt(2));
|
||||
}
|
||||
distances[idx] = minDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Backward pass (bottom-right to top-left)
|
||||
for (let y = height - 1; y >= 0; y--) {
|
||||
for (let x = width - 1; x >= 0; x--) {
|
||||
const idx = y * width + x;
|
||||
if (distances[idx] > 0) {
|
||||
let minDist = distances[idx];
|
||||
// Check bottom neighbor
|
||||
if (y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + x] + 1);
|
||||
}
|
||||
// Check right neighbor
|
||||
if (x < width - 1) {
|
||||
minDist = Math.min(minDist, distances[y * width + (x + 1)] + 1);
|
||||
}
|
||||
// Check bottom-right diagonal
|
||||
if (x < width - 1 && y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + (x + 1)] + Math.sqrt(2));
|
||||
}
|
||||
// Check bottom-left diagonal
|
||||
if (x > 0 && y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + (x - 1)] + Math.sqrt(2));
|
||||
}
|
||||
distances[idx] = minDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
return distances;
|
||||
}
|
||||
/**
|
||||
* Calculates distance from edges of a rectangle for opaque images.
|
||||
* @param width - Width of the rectangle
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Calculates the Euclidean distance transform of a binary mask.
|
||||
* Uses a two-pass algorithm for efficiency.
|
||||
* @param binaryMask - Binary mask where 1 = inside, 0 = outside
|
||||
* @param width - Width of the mask
|
||||
* @param height - Height of the mask
|
||||
* @returns Float32Array containing distance values
|
||||
*/
|
||||
export function calculateDistanceTransform(binaryMask, width, height) {
|
||||
const distances = new Float32Array(width * height);
|
||||
const infinity = width + height; // A value larger than any possible distance
|
||||
// Initialize distances
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
distances[i] = binaryMask[i] === 1 ? infinity : 0;
|
||||
}
|
||||
// Forward pass (top-left to bottom-right)
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = y * width + x;
|
||||
if (distances[idx] > 0) {
|
||||
let minDist = distances[idx];
|
||||
// Check top neighbor
|
||||
if (y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + x] + 1);
|
||||
}
|
||||
// Check left neighbor
|
||||
if (x > 0) {
|
||||
minDist = Math.min(minDist, distances[y * width + (x - 1)] + 1);
|
||||
}
|
||||
// Check top-left diagonal
|
||||
if (x > 0 && y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + (x - 1)] + Math.sqrt(2));
|
||||
}
|
||||
// Check top-right diagonal
|
||||
if (x < width - 1 && y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + (x + 1)] + Math.sqrt(2));
|
||||
}
|
||||
distances[idx] = minDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Backward pass (bottom-right to top-left)
|
||||
for (let y = height - 1; y >= 0; y--) {
|
||||
for (let x = width - 1; x >= 0; x--) {
|
||||
const idx = y * width + x;
|
||||
if (distances[idx] > 0) {
|
||||
let minDist = distances[idx];
|
||||
// Check bottom neighbor
|
||||
if (y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + x] + 1);
|
||||
}
|
||||
// Check right neighbor
|
||||
if (x < width - 1) {
|
||||
minDist = Math.min(minDist, distances[y * width + (x + 1)] + 1);
|
||||
}
|
||||
// Check bottom-right diagonal
|
||||
if (x < width - 1 && y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + (x + 1)] + Math.sqrt(2));
|
||||
}
|
||||
// Check bottom-left diagonal
|
||||
if (x > 0 && y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + (x - 1)] + Math.sqrt(2));
|
||||
}
|
||||
distances[idx] = minDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
return distances;
|
||||
}
|
||||
+2
-81
@@ -1,5 +1,6 @@
|
||||
import {createModuleLogger} from "./log_system/log_funcs.js";
|
||||
import { createCanvas } from "./utils/CommonUtils.js";
|
||||
import { calculateDistanceTransform } from "./utils/MaskPixelUtils.js";
|
||||
import type { Canvas } from './Canvas';
|
||||
import type { Point, CanvasState, Layer } from './types';
|
||||
|
||||
@@ -711,7 +712,7 @@ export class MaskTool {
|
||||
*/
|
||||
private createFeatheredMaskFromBinaryData(binaryData: Uint8Array, featherRadius: number, width: number, height: number): HTMLCanvasElement {
|
||||
// Calculate the fast distance transform
|
||||
const distanceMap = this._fastDistanceTransform(binaryData, width, height);
|
||||
const distanceMap = calculateDistanceTransform(binaryData, width, height);
|
||||
|
||||
// Find the maximum distance to normalize
|
||||
let maxDistance = 0;
|
||||
@@ -2271,86 +2272,6 @@ export class MaskTool {
|
||||
return this.createFeatheredMaskFromBinaryData(binaryData, featherRadius, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast distance transform using the simple two-pass algorithm from ImageAnalysis.ts
|
||||
* Much faster than the complex Felzenszwalb algorithm
|
||||
*/
|
||||
private _fastDistanceTransform(binaryMask: Uint8Array, width: number, height: number): Float32Array {
|
||||
const distances = new Float32Array(width * height);
|
||||
const infinity = width + height; // A value larger than any possible distance
|
||||
|
||||
// Initialize distances
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
distances[i] = binaryMask[i] === 1 ? infinity : 0;
|
||||
}
|
||||
|
||||
// Forward pass (top-left to bottom-right)
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = y * width + x;
|
||||
if (distances[idx] > 0) {
|
||||
let minDist = distances[idx];
|
||||
|
||||
// Check top neighbor
|
||||
if (y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + x] + 1);
|
||||
}
|
||||
|
||||
// Check left neighbor
|
||||
if (x > 0) {
|
||||
minDist = Math.min(minDist, distances[y * width + (x - 1)] + 1);
|
||||
}
|
||||
|
||||
// Check top-left diagonal
|
||||
if (x > 0 && y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + (x - 1)] + Math.sqrt(2));
|
||||
}
|
||||
|
||||
// Check top-right diagonal
|
||||
if (x < width - 1 && y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + (x + 1)] + Math.sqrt(2));
|
||||
}
|
||||
|
||||
distances[idx] = minDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backward pass (bottom-right to top-left)
|
||||
for (let y = height - 1; y >= 0; y--) {
|
||||
for (let x = width - 1; x >= 0; x--) {
|
||||
const idx = y * width + x;
|
||||
if (distances[idx] > 0) {
|
||||
let minDist = distances[idx];
|
||||
|
||||
// Check bottom neighbor
|
||||
if (y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + x] + 1);
|
||||
}
|
||||
|
||||
// Check right neighbor
|
||||
if (x < width - 1) {
|
||||
minDist = Math.min(minDist, distances[y * width + (x + 1)] + 1);
|
||||
}
|
||||
|
||||
// Check bottom-right diagonal
|
||||
if (x < width - 1 && y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + (x + 1)] + Math.sqrt(2));
|
||||
}
|
||||
|
||||
// Check bottom-left diagonal
|
||||
if (x > 0 && y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + (x - 1)] + Math.sqrt(2));
|
||||
}
|
||||
|
||||
distances[idx] = minDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return distances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an expanded/contracted mask canvas using simple morphological operations
|
||||
* This gives SHARP edges without smoothing, unlike distance transform
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createModuleLogger } from "../log_system/log_funcs.js";
|
||||
import { createCanvas } from "./CommonUtils.js";
|
||||
import { calculateDistanceTransform } from "./MaskPixelUtils.js";
|
||||
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||
|
||||
const log = createModuleLogger('ImageAnalysis');
|
||||
@@ -115,90 +116,6 @@ export const createDistanceFieldMask = withErrorHandling(function(image: HTMLIma
|
||||
return createDistanceFieldMaskSync(image, blendArea);
|
||||
}, 'createDistanceFieldMask');
|
||||
|
||||
/**
|
||||
* Calculates the Euclidean distance transform of a binary mask.
|
||||
* Uses a two-pass algorithm for efficiency.
|
||||
* @param binaryMask - Binary mask where 1 = inside, 0 = outside
|
||||
* @param width - Width of the mask
|
||||
* @param height - Height of the mask
|
||||
* @returns Float32Array containing distance values
|
||||
*/
|
||||
function calculateDistanceTransform(binaryMask: Uint8Array, width: number, height: number): Float32Array {
|
||||
const distances = new Float32Array(width * height);
|
||||
const infinity = width + height; // A value larger than any possible distance
|
||||
|
||||
// Initialize distances
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
distances[i] = binaryMask[i] === 1 ? infinity : 0;
|
||||
}
|
||||
|
||||
// Forward pass (top-left to bottom-right)
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = y * width + x;
|
||||
if (distances[idx] > 0) {
|
||||
let minDist = distances[idx];
|
||||
|
||||
// Check top neighbor
|
||||
if (y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + x] + 1);
|
||||
}
|
||||
|
||||
// Check left neighbor
|
||||
if (x > 0) {
|
||||
minDist = Math.min(minDist, distances[y * width + (x - 1)] + 1);
|
||||
}
|
||||
|
||||
// Check top-left diagonal
|
||||
if (x > 0 && y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + (x - 1)] + Math.sqrt(2));
|
||||
}
|
||||
|
||||
// Check top-right diagonal
|
||||
if (x < width - 1 && y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + (x + 1)] + Math.sqrt(2));
|
||||
}
|
||||
|
||||
distances[idx] = minDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backward pass (bottom-right to top-left)
|
||||
for (let y = height - 1; y >= 0; y--) {
|
||||
for (let x = width - 1; x >= 0; x--) {
|
||||
const idx = y * width + x;
|
||||
if (distances[idx] > 0) {
|
||||
let minDist = distances[idx];
|
||||
|
||||
// Check bottom neighbor
|
||||
if (y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + x] + 1);
|
||||
}
|
||||
|
||||
// Check right neighbor
|
||||
if (x < width - 1) {
|
||||
minDist = Math.min(minDist, distances[y * width + (x + 1)] + 1);
|
||||
}
|
||||
|
||||
// Check bottom-right diagonal
|
||||
if (x < width - 1 && y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + (x + 1)] + Math.sqrt(2));
|
||||
}
|
||||
|
||||
// Check bottom-left diagonal
|
||||
if (x > 0 && y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + (x - 1)] + Math.sqrt(2));
|
||||
}
|
||||
|
||||
distances[idx] = minDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return distances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates distance from edges of a rectangle for opaque images.
|
||||
* @param width - Width of the rectangle
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Calculates the Euclidean distance transform of a binary mask.
|
||||
* Uses a two-pass algorithm for efficiency.
|
||||
* @param binaryMask - Binary mask where 1 = inside, 0 = outside
|
||||
* @param width - Width of the mask
|
||||
* @param height - Height of the mask
|
||||
* @returns Float32Array containing distance values
|
||||
*/
|
||||
export function calculateDistanceTransform(binaryMask: Uint8Array, width: number, height: number): Float32Array {
|
||||
const distances = new Float32Array(width * height);
|
||||
const infinity = width + height; // A value larger than any possible distance
|
||||
|
||||
// Initialize distances
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
distances[i] = binaryMask[i] === 1 ? infinity : 0;
|
||||
}
|
||||
|
||||
// Forward pass (top-left to bottom-right)
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = y * width + x;
|
||||
if (distances[idx] > 0) {
|
||||
let minDist = distances[idx];
|
||||
|
||||
// Check top neighbor
|
||||
if (y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + x] + 1);
|
||||
}
|
||||
|
||||
// Check left neighbor
|
||||
if (x > 0) {
|
||||
minDist = Math.min(minDist, distances[y * width + (x - 1)] + 1);
|
||||
}
|
||||
|
||||
// Check top-left diagonal
|
||||
if (x > 0 && y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + (x - 1)] + Math.sqrt(2));
|
||||
}
|
||||
|
||||
// Check top-right diagonal
|
||||
if (x < width - 1 && y > 0) {
|
||||
minDist = Math.min(minDist, distances[(y - 1) * width + (x + 1)] + Math.sqrt(2));
|
||||
}
|
||||
|
||||
distances[idx] = minDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backward pass (bottom-right to top-left)
|
||||
for (let y = height - 1; y >= 0; y--) {
|
||||
for (let x = width - 1; x >= 0; x--) {
|
||||
const idx = y * width + x;
|
||||
if (distances[idx] > 0) {
|
||||
let minDist = distances[idx];
|
||||
|
||||
// Check bottom neighbor
|
||||
if (y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + x] + 1);
|
||||
}
|
||||
|
||||
// Check right neighbor
|
||||
if (x < width - 1) {
|
||||
minDist = Math.min(minDist, distances[y * width + (x + 1)] + 1);
|
||||
}
|
||||
|
||||
// Check bottom-right diagonal
|
||||
if (x < width - 1 && y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + (x + 1)] + Math.sqrt(2));
|
||||
}
|
||||
|
||||
// Check bottom-left diagonal
|
||||
if (x > 0 && y < height - 1) {
|
||||
minDist = Math.min(minDist, distances[(y + 1) * width + (x - 1)] + Math.sqrt(2));
|
||||
}
|
||||
|
||||
distances[idx] = minDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return distances;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
function installCanvasStub(sourceAlpha) {
|
||||
const originalDocument = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
const output = { data: null };
|
||||
const context = {
|
||||
clearRect() {},
|
||||
createImageData(width, height) {
|
||||
return { width, height, data: new Uint8ClampedArray(width * height * 4) };
|
||||
},
|
||||
drawImage() {},
|
||||
getImageData(_x, _y, width, height) {
|
||||
const data = new Uint8ClampedArray(width * height * 4);
|
||||
for (let i = 0; i < sourceAlpha.length; i++) {
|
||||
data[i * 4 + 3] = sourceAlpha[i];
|
||||
}
|
||||
return { width, height, data };
|
||||
},
|
||||
putImageData(imageData) {
|
||||
output.data = imageData.data;
|
||||
},
|
||||
};
|
||||
const canvas = {
|
||||
height: 0,
|
||||
width: 0,
|
||||
getContext() {
|
||||
return context;
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
createElement(tagName) {
|
||||
assert.equal(tagName, 'canvas');
|
||||
return canvas;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
output,
|
||||
restore() {
|
||||
if (originalDocument) {
|
||||
Object.defineProperty(globalThis, 'document', originalDocument);
|
||||
} else {
|
||||
delete globalThis.document;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('distance-field mask preserves alpha-distance behavior for transparent pixels', async () => {
|
||||
const stubs = installCanvasStub([255, 128, 0]);
|
||||
|
||||
try {
|
||||
const { createDistanceFieldMaskSync } = await import('../js/utils/ImageAnalysis.js?characterization');
|
||||
createDistanceFieldMaskSync({ width: 3, height: 1 }, 100);
|
||||
|
||||
assert.deepEqual([...stubs.output.data], [
|
||||
255, 255, 255, 255,
|
||||
255, 255, 255, 127,
|
||||
255, 255, 255, 0,
|
||||
]);
|
||||
} finally {
|
||||
stubs.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('mask consumers use one shared distance transform implementation', async () => {
|
||||
const { calculateDistanceTransform } = await import('../js/utils/MaskPixelUtils.js?characterization');
|
||||
const imageAnalysisSource = await readFile(new URL('../src/utils/ImageAnalysis.ts', import.meta.url), 'utf8');
|
||||
const maskToolSource = await readFile(new URL('../src/MaskTool.ts', import.meta.url), 'utf8');
|
||||
|
||||
assert.deepEqual(
|
||||
[...calculateDistanceTransform(new Uint8Array([1, 1, 0]), 3, 1)],
|
||||
[2, 1, 0],
|
||||
);
|
||||
assert.match(imageAnalysisSource, /from "\.\/MaskPixelUtils\.js"/);
|
||||
assert.match(maskToolSource, /from "\.\/utils\/MaskPixelUtils\.js"/);
|
||||
assert.match(maskToolSource, /calculateDistanceTransform\(binaryData, width, height\)/);
|
||||
assert.doesNotMatch(imageAnalysisSource, /function calculateDistanceTransform\(/);
|
||||
assert.doesNotMatch(maskToolSource, /private _fastDistanceTransform\(/);
|
||||
});
|
||||
Reference in New Issue
Block a user