Files
Comfyui-LayerForge/src/utils/ImageUtils.ts
Dariusz L 207bacc1f8 Refactor image and mask utility functions
Moved convertToImage, createMaskFromImageSrc, and canvasToMaskImage from MaskProcessingUtils and mask_utils to ImageUtils for better modularity and reuse. Updated imports in dependent modules to use the new locations. Removed duplicate implementations from mask_utils and MaskProcessingUtils.
2025-07-27 18:47:41 +02:00

389 lines
12 KiB
TypeScript

import {createModuleLogger} from "./LoggerUtils.js";
import {withErrorHandling, createValidationError} from "../ErrorHandler.js";
import { createCanvas } from "./CommonUtils.js";
import type { Tensor, ImageDataPixel } from '../types';
const log = createModuleLogger('ImageUtils');
export function validateImageData(data: any): boolean {
log.debug("Validating data structure:", {
hasData: !!data,
type: typeof data,
isArray: Array.isArray(data),
keys: data ? Object.keys(data) : null,
shape: data?.shape,
dataType: data?.data ? data.data.constructor.name : null,
fullData: data
});
if (!data) {
log.info("Data is null or undefined");
return false;
}
if (Array.isArray(data)) {
log.debug("Data is array, getting first element");
data = data[0];
}
if (!data || typeof data !== 'object') {
log.info("Invalid data type");
return false;
}
if (!data.data) {
log.info("Missing data property");
return false;
}
if (!(data.data instanceof Float32Array)) {
try {
data.data = new Float32Array(data.data);
} catch (e) {
log.error("Failed to convert data to Float32Array:", e);
return false;
}
}
return true;
}
export function convertImageData(data: any): ImageDataPixel {
log.info("Converting image data:", data);
if (Array.isArray(data)) {
data = data[0];
}
const shape = data.shape;
const height = shape[1];
const width = shape[2];
const channels = shape[3];
const floatData = new Float32Array(data.data);
log.debug("Processing dimensions:", {height, width, channels});
const rgbaData = new Uint8ClampedArray(width * height * 4);
for (let h = 0; h < height; h++) {
for (let w = 0; w < width; w++) {
const pixelIndex = (h * width + w) * 4;
const tensorIndex = (h * width + w) * channels;
for (let c = 0; c < channels; c++) {
const value = floatData[tensorIndex + c];
rgbaData[pixelIndex + c] = Math.max(0, Math.min(255, Math.round(value * 255)));
}
rgbaData[pixelIndex + 3] = 255;
}
}
return {
data: rgbaData,
width: width,
height: height
};
}
export function applyMaskToImageData(imageData: ImageDataPixel, maskData: Tensor): ImageDataPixel {
log.info("Applying mask to image data");
const rgbaData = new Uint8ClampedArray(imageData.data);
const width = imageData.width;
const height = imageData.height;
const maskShape = maskData.shape;
const maskFloatData = new Float32Array(maskData.data);
log.debug(`Applying mask of shape: ${maskShape}`);
for (let h = 0; h < height; h++) {
for (let w = 0; w < width; w++) {
const pixelIndex = (h * width + w) * 4;
const maskIndex = h * width + w;
const alpha = maskFloatData[maskIndex];
rgbaData[pixelIndex + 3] = Math.max(0, Math.min(255, Math.round(alpha * 255)));
}
}
log.info("Mask application completed");
return {
data: rgbaData,
width: width,
height: height
};
}
export const prepareImageForCanvas = withErrorHandling(function (inputImage: any): ImageDataPixel {
log.info("Preparing image for canvas:", inputImage);
if (Array.isArray(inputImage)) {
inputImage = inputImage[0];
}
if (!inputImage || !inputImage.shape || !inputImage.data) {
throw createValidationError("Invalid input image format", {inputImage});
}
const shape = inputImage.shape;
const height = shape[1];
const width = shape[2];
const channels = shape[3];
const floatData = new Float32Array(inputImage.data);
log.debug("Image dimensions:", {height, width, channels});
const rgbaData = new Uint8ClampedArray(width * height * 4);
for (let h = 0; h < height; h++) {
for (let w = 0; w < width; w++) {
const pixelIndex = (h * width + w) * 4;
const tensorIndex = (h * width + w) * channels;
for (let c = 0; c < channels; c++) {
const value = floatData[tensorIndex + c];
rgbaData[pixelIndex + c] = Math.max(0, Math.min(255, Math.round(value * 255)));
}
rgbaData[pixelIndex + 3] = 255;
}
}
return {
data: rgbaData,
width: width,
height: height
};
}, 'prepareImageForCanvas');
export const imageToTensor = withErrorHandling(async function (image: HTMLImageElement | HTMLCanvasElement): Promise<Tensor> {
if (!image) {
throw createValidationError("Image is required");
}
const { canvas, ctx } = createCanvas(image.width, image.height, '2d', { willReadFrequently: true });
if (ctx) {
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = new Float32Array(canvas.width * canvas.height * 3);
for (let i = 0; i < imageData.data.length; i += 4) {
const pixelIndex = i / 4;
data[pixelIndex * 3] = imageData.data[i] / 255;
data[pixelIndex * 3 + 1] = imageData.data[i + 1] / 255;
data[pixelIndex * 3 + 2] = imageData.data[i + 2] / 255;
}
return {
data: data,
shape: [1, canvas.height, canvas.width, 3],
width: canvas.width,
height: canvas.height
};
}
throw new Error("Canvas context not available");
}, 'imageToTensor');
export const tensorToImage = withErrorHandling(async function (tensor: Tensor): Promise<HTMLImageElement> {
if (!tensor || !tensor.data || !tensor.shape) {
throw createValidationError("Invalid tensor format", {tensor});
}
const [, height, width, channels] = tensor.shape;
const { canvas, ctx } = createCanvas(width, height, '2d', { willReadFrequently: true });
if (ctx) {
const imageData = ctx.createImageData(width, height);
const data = tensor.data;
for (let i = 0; i < width * height; i++) {
const pixelIndex = i * 4;
const tensorIndex = i * channels;
imageData.data[pixelIndex] = Math.round(data[tensorIndex] * 255);
imageData.data[pixelIndex + 1] = Math.round(data[tensorIndex + 1] * 255);
imageData.data[pixelIndex + 2] = Math.round(data[tensorIndex + 2] * 255);
imageData.data[pixelIndex + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = canvas.toDataURL();
});
}
throw new Error("Canvas context not available");
}, 'tensorToImage');
export const resizeImage = withErrorHandling(async function (image: HTMLImageElement, maxWidth: number, maxHeight: number): Promise<HTMLImageElement> {
if (!image) {
throw createValidationError("Image is required");
}
const originalWidth = image.width;
const originalHeight = image.height;
const scale = Math.min(maxWidth / originalWidth, maxHeight / originalHeight);
const newWidth = Math.round(originalWidth * scale);
const newHeight = Math.round(originalHeight * scale);
const { canvas, ctx } = createCanvas(newWidth, newHeight, '2d', { willReadFrequently: true });
if (ctx) {
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(image, 0, 0, newWidth, newHeight);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = canvas.toDataURL();
});
}
throw new Error("Canvas context not available");
}, 'resizeImage');
export const createThumbnail = withErrorHandling(async function (image: HTMLImageElement, size = 128): Promise<HTMLImageElement> {
return resizeImage(image, size, size);
}, 'createThumbnail');
export const imageToBase64 = withErrorHandling(function (image: HTMLImageElement | HTMLCanvasElement, format = 'png', quality = 0.9): string {
if (!image) {
throw createValidationError("Image is required");
}
const width = image instanceof HTMLImageElement ? image.naturalWidth : image.width;
const height = image instanceof HTMLImageElement ? image.naturalHeight : image.height;
const { canvas, ctx } = createCanvas(width, height, '2d', { willReadFrequently: true });
if (ctx) {
ctx.drawImage(image, 0, 0);
const mimeType = `image/${format}`;
return canvas.toDataURL(mimeType, quality);
}
throw new Error("Canvas context not available");
}, 'imageToBase64');
export const base64ToImage = withErrorHandling(function (base64: string): Promise<HTMLImageElement> {
if (!base64) {
throw createValidationError("Base64 string is required");
}
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error("Failed to load image from base64"));
img.src = base64;
});
}, 'base64ToImage');
export function isValidImage(image: any): image is HTMLImageElement | HTMLCanvasElement {
return image &&
(image instanceof HTMLImageElement || image instanceof HTMLCanvasElement) &&
image.width > 0 &&
image.height > 0;
}
export function getImageInfo(image: HTMLImageElement | HTMLCanvasElement): {width: number, height: number, aspectRatio: number, area: number} | null {
if (!isValidImage(image)) {
return null;
}
const width = image instanceof HTMLImageElement ? image.naturalWidth : image.width;
const height = image instanceof HTMLImageElement ? image.naturalHeight : image.height;
return {
width,
height,
aspectRatio: width / height,
area: width * height
};
}
export function createImageFromSource(source: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = source;
});
}
export const createEmptyImage = withErrorHandling(function (width: number, height: number, color = 'transparent'): Promise<HTMLImageElement> {
const { canvas, ctx } = createCanvas(width, height, '2d', { willReadFrequently: true });
if (ctx) {
if (color !== 'transparent') {
ctx.fillStyle = color;
ctx.fillRect(0, 0, width, height);
}
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = canvas.toDataURL();
});
}
throw new Error("Canvas context not available");
}, 'createEmptyImage');
/**
* Converts a canvas or image to an Image element
* Consolidated from MaskProcessingUtils.convertToImage()
* @param source - Source canvas or image
* @returns Promise with Image element
*/
export async function convertToImage(source: HTMLCanvasElement | HTMLImageElement): Promise<HTMLImageElement> {
if (source instanceof HTMLImageElement) {
return source; // Already an image
}
const image = new Image();
image.src = source.toDataURL();
await new Promise<void>((resolve, reject) => {
image.onload = () => resolve();
image.onerror = reject;
});
return image;
}
/**
* Creates a mask from image source for use in mask editor
* Consolidated from mask_utils.create_mask_from_image_src()
* @param imageSrc - Image source (URL or data URL)
* @returns Promise returning Image object
*/
export function createMaskFromImageSrc(imageSrc: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = imageSrc;
});
}
/**
* Converts canvas to Image for use as mask
* Consolidated from mask_utils.canvas_to_mask_image()
* @param canvas - Canvas to convert
* @returns Promise returning Image object
*/
export function canvasToMaskImage(canvas: HTMLCanvasElement): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = canvas.toDataURL();
});
}