Added Outpainting Logic

This commit is contained in:
Dariusz L
2025-06-20 19:04:49 +02:00
parent 00db5075bd
commit 2ccc784745
7 changed files with 1421 additions and 1100 deletions

View File

@@ -1,12 +1,11 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { $el } from "../../scripts/ui.js";
import { Canvas } from "./Canvas.js";
import {app} from "../../scripts/app.js";
import {api} from "../../scripts/api.js";
import {$el} from "../../scripts/ui.js";
import {Canvas} from "./Canvas.js";
async function createCanvasWidget(node, widget, app) {
const canvas = new Canvas(node, widget);
// 添加全局样式
const style = document.createElement('style');
style.textContent = `
.painter-button {
@@ -59,6 +58,12 @@ async function createCanvasWidget(node, widget, app) {
border: 1px solid #4a5a6a;
border-radius: 6px;
box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
transition: border-color 0.3s ease; /* Dodano dla płynnej zmiany ramki */
}
.painter-container.drag-over {
border-color: #00ff00; /* Zielona ramka podczas przeciągania */
border-style: dashed;
}
.painter-dialog {
@@ -98,24 +103,23 @@ async function createCanvasWidget(node, widget, app) {
margin: 5px 0;
display: none;
}
.blend-mode-active .blend-opacity-slider {
display: block;
}
.blend-mode-item {
padding: 5px;
cursor: pointer;
position: relative;
}
.blend-mode-item.active {
background-color: rgba(0,0,0,0.1);
}
`;
document.head.appendChild(style);
// 修改控制面板,使其高度自适应
const controlPanel = $el("div.painterControlPanel", {}, [
$el("div.controls.painter-controls", {
style: {
@@ -123,7 +127,7 @@ async function createCanvasWidget(node, widget, app) {
top: "0",
left: "0",
right: "0",
minHeight: "50px", // 改为最小高度
minHeight: "50px",
zIndex: "10",
background: "linear-gradient(to bottom, #404040, #383838)",
borderBottom: "1px solid #2a2a2a",
@@ -134,7 +138,7 @@ async function createCanvasWidget(node, widget, app) {
flexWrap: "wrap",
alignItems: "center"
},
// 添加监听器来动态整画布容器的位置
onresize: (entries) => {
const controlsHeight = entries[0].target.offsetHeight;
canvasContainer.style.top = (controlsHeight + 10) + "px";
@@ -149,16 +153,15 @@ async function createCanvasWidget(node, widget, app) {
input.multiple = true;
input.onchange = async (e) => {
for (const file of e.target.files) {
// 创建图片对象
const img = new Image();
img.onload = async () => {
// 计算适当的缩放比例
const scale = Math.min(
canvas.width / img.width * 0.8,
canvas.height / img.height * 0.8
);
// 创建新图层
const layer = {
image: img,
x: (canvas.width - img.width * scale) / 2,
@@ -168,18 +171,14 @@ async function createCanvasWidget(node, widget, app) {
rotation: 0,
zIndex: canvas.layers.length
};
// 添加图层并选中
canvas.layers.push(layer);
canvas.selectedLayer = layer;
// 渲染画布
canvas.render();
// 立即保存并触发输出更新
await canvas.saveToServer(widget.value);
// 触发节点更新
app.graph.runStep();
};
img.src = URL.createObjectURL(file);
@@ -193,32 +192,13 @@ async function createCanvasWidget(node, widget, app) {
onclick: async () => {
try {
console.log("Import Input clicked");
console.log("Node ID:", node.id);
const response = await fetch(`/ycnode/get_canvas_data/${node.id}`);
console.log("Response status:", response.status);
const result = await response.json();
console.log("Full response data:", result);
if (result.success && result.data) {
if (result.data.image) {
console.log("Found image data, importing...");
await canvas.importImage({
image: result.data.image,
mask: result.data.mask
});
await canvas.saveToServer(widget.value);
app.graph.runStep();
} else {
throw new Error("No image data found in cache");
}
} else {
throw new Error("Invalid response format");
const success = await canvas.importLatestImage();
if (success) {
await canvas.saveToServer(widget.value);
app.graph.runStep();
}
} catch (error) {
console.error("Error importing input:", error);
console.error("Error during import input process:", error);
alert(`Failed to import input: ${error.message}`);
}
}
@@ -341,21 +321,21 @@ async function createCanvasWidget(node, widget, app) {
app.graph.runStep();
}
}),
// 添加水平镜像按钮
$el("button.painter-button", {
textContent: "Mirror H",
onclick: () => {
canvas.mirrorHorizontal();
}
}),
// 添加垂直镜像按钮
$el("button.painter-button", {
textContent: "Mirror V",
onclick: () => {
canvas.mirrorVertical();
}
}),
// 在控制面板中添加抠图按钮
$el("button.painter-button", {
textContent: "Matting",
onclick: async () => {
@@ -363,24 +343,21 @@ async function createCanvasWidget(node, widget, app) {
if (!canvas.selectedLayer) {
throw new Error("Please select an image first");
}
// 获取或创建状态指示器
const statusIndicator = MattingStatusIndicator.getInstance(controlPanel.querySelector('.controls'));
// 添加状态监听
const updateStatus = (event) => {
const {status} = event.detail;
statusIndicator.setStatus(status);
};
api.addEventListener("matting_status", updateStatus);
try {
// 获取图像据
const imageData = await canvas.getLayerImageData(canvas.selectedLayer);
console.log("Sending image to server...");
// 发送请求
const response = await fetch("/matting", {
method: "POST",
headers: {
@@ -392,31 +369,28 @@ async function createCanvasWidget(node, widget, app) {
refinement: 1
})
});
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const result = await response.json();
console.log("Creating new layer with matting result...");
// 创建新图层
const mattedImage = new Image();
mattedImage.onload = async () => {
// 创建临时画布来处理透明度
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = canvas.selectedLayer.width;
tempCanvas.height = canvas.selectedLayer.height;
// 绘制原始图像
tempCtx.drawImage(
mattedImage,
0, 0,
tempCanvas.width, tempCanvas.height
);
// 创建新图层
const newImage = new Image();
newImage.onload = async () => {
const newLayer = {
@@ -428,27 +402,25 @@ async function createCanvasWidget(node, widget, app) {
rotation: canvas.selectedLayer.rotation,
zIndex: canvas.layers.length + 1
};
canvas.layers.push(newLayer);
canvas.selectedLayer = newLayer;
canvas.render();
// 保存并更新
await canvas.saveToServer(widget.value);
app.graph.runStep();
};
// 转换为PNG并保持透明度
newImage.src = tempCanvas.toDataURL('image/png');
};
mattedImage.src = result.matted_image;
console.log("Matting result applied successfully");
} finally {
api.removeEventListener("matting_status", updateStatus);
}
} catch (error) {
console.error("Matting error:", error);
alert(`Error during matting process: ${error.message}`);
@@ -458,29 +430,24 @@ async function createCanvasWidget(node, widget, app) {
])
]);
// 创建ResizeObserver来监控控制面板的高度变化
const resizeObserver = new ResizeObserver((entries) => {
const controlsHeight = entries[0].target.offsetHeight;
canvasContainer.style.top = (controlsHeight + 10) + "px";
});
// 监控控制面板的大小变化
resizeObserver.observe(controlPanel.querySelector('.controls'));
// 获取触发器widget
const triggerWidget = node.widgets.find(w => w.name === "trigger");
// 创建更新函数
const updateOutput = async () => {
// 保存画布
await canvas.saveToServer(widget.value);
// 更新触发器值
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
// 触发节点更新
app.graph.runStep();
};
// 修改所有可能触发更新的操作
const addUpdateToButton = (button) => {
const origClick = button.onclick;
button.onclick = async (...args) => {
@@ -489,63 +456,27 @@ async function createCanvasWidget(node, widget, app) {
};
};
// 为所有按钮添加更新逻辑
controlPanel.querySelectorAll('button').forEach(addUpdateToButton);
// 修改画布容器样式使用动态top值
const canvasContainer = $el("div.painterCanvasContainer.painter-container", {
style: {
position: "absolute",
top: "60px", // 初始值
top: "60px",
left: "10px",
right: "10px",
bottom: "10px",
display: "flex",
justifyContent: "center",
alignItems: "center",
overflow: "hidden"
}
}, [canvas.canvas]);
// 修改节点大小调整逻辑
node.onResize = function() {
const minSize = 300;
const controlsElement = controlPanel.querySelector('.controls');
const controlPanelHeight = controlsElement.offsetHeight; // 取实际高
const padding = 20;
// 保持节点宽度,高度根据画布比例调整
const width = Math.max(this.size[0], minSize);
const height = Math.max(
width * (canvas.height / canvas.width) + controlPanelHeight + padding * 2,
minSize + controlPanelHeight
);
this.size[0] = width;
this.size[1] = height;
// 计算画布的实际可用空间
const availableWidth = width - padding * 2;
const availableHeight = height - controlPanelHeight - padding * 2;
// 更新画布尺寸,保持比例
const scale = Math.min(
availableWidth / canvas.width,
availableHeight / canvas.height
);
canvas.canvas.style.width = (canvas.width * scale) + "px";
canvas.canvas.style.height = (canvas.height * scale) + "px";
// 强制重新渲染
node.onResize = function () {
canvas.render();
};
// 添加拖拽事件监听
canvas.canvas.addEventListener('mouseup', updateOutput);
canvas.canvas.addEventListener('mouseleave', updateOutput);
// 创建一个包含控制面板和画布的容器
const mainContainer = $el("div.painterMainContainer", {
style: {
position: "relative",
@@ -553,19 +484,80 @@ async function createCanvasWidget(node, widget, app) {
height: "100%"
}
}, [controlPanel, canvasContainer]);
const handleFileLoad = async (file) => {
// Sprawdzamy, czy plik jest obrazem
if (!file.type.startsWith('image/')) {
return;
}
const img = new Image();
img.onload = async () => {
// Logika dodawania obrazu jest taka sama jak w przycisku "Add Image"
const scale = Math.min(
canvas.width / img.width * 0.8,
canvas.height / img.height * 0.8
);
const layer = {
image: img,
x: (canvas.width - img.width * scale) / 2,
y: (canvas.height - img.height * scale) / 2,
width: img.width * scale,
height: img.height * scale,
rotation: 0,
zIndex: canvas.layers.length,
blendMode: 'normal',
opacity: 1
};
canvas.layers.push(layer);
canvas.selectedLayer = layer;
canvas.render();
// Używamy funkcji updateOutput, aby zapisać stan i uruchomić graf
await updateOutput();
// Zwolnienie zasobu URL
URL.revokeObjectURL(img.src);
};
img.src = URL.createObjectURL(file);
};
mainContainer.addEventListener('dragover', (e) => {
e.preventDefault(); // Niezbędne, aby zdarzenie 'drop' zadziałało
e.stopPropagation();
// Dodajemy klasę, aby pokazać wizualną informację zwrotną
canvasContainer.classList.add('drag-over');
});
mainContainer.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
// Usuwamy klasę po opuszczeniu obszaru
canvasContainer.classList.remove('drag-over');
});
mainContainer.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
// Usuwamy klasę po upuszczeniu pliku
canvasContainer.classList.remove('drag-over');
if (e.dataTransfer.files) {
// Przetwarzamy wszystkie upuszczone pliki
for (const file of e.dataTransfer.files) {
await handleFileLoad(file);
}
}
});
// 将主容器添加到节点
const mainWidget = node.addDOMWidget("mainContainer", "widget", mainContainer);
// 设置节点的默认大小
node.size = [500, 500]; // 设置初始大小为正方形
// 在执行开始时保存数据
node.size = [500, 500];
api.addEventListener("execution_start", async () => {
// 保存画布
await canvas.saveToServer(widget.value);
// 保存当前节点的输入数据
if (node.inputs[0].link) {
const linkId = node.inputs[0].link;
const inputData = app.nodeOutputs[linkId];
@@ -575,32 +567,31 @@ async function createCanvasWidget(node, widget, app) {
}
});
// 移除原来在 saveToServer 中的缓存清理
const originalSaveToServer = canvas.saveToServer;
canvas.saveToServer = async function(fileName) {
canvas.saveToServer = async function (fileName) {
const result = await originalSaveToServer.call(this, fileName);
// 移除这里的缓存清理
// ImageCache.clear();
return result;
};
node.canvasWidget = canvas;
return {
canvas: canvas,
panel: controlPanel
};
}
// 修改状态指示器类,确保单例模式
class MattingStatusIndicator {
static instance = null;
static getInstance(container) {
if (!MattingStatusIndicator.instance) {
MattingStatusIndicator.instance = new MattingStatusIndicator(container);
}
return MattingStatusIndicator.instance;
}
constructor(container) {
this.indicator = document.createElement('div');
this.indicator.style.cssText = `
@@ -612,7 +603,7 @@ class MattingStatusIndicator {
display: inline-block;
transition: background-color 0.3s;
`;
const style = document.createElement('style');
style.textContent = `
.processing {
@@ -632,12 +623,12 @@ class MattingStatusIndicator {
}
`;
document.head.appendChild(style);
container.appendChild(this.indicator);
}
setStatus(status) {
this.indicator.className = ''; // 清除所有状态
this.indicator.className = '';
if (status) {
this.indicator.classList.add(status);
}
@@ -649,9 +640,8 @@ class MattingStatusIndicator {
}
}
// 验证 ComfyUI 的图像数据格式
function validateImageData(data) {
// 打印完整的输入数据结构
console.log("Validating data structure:", {
hasData: !!data,
type: typeof data,
@@ -659,36 +649,31 @@ function validateImageData(data) {
keys: data ? Object.keys(data) : null,
shape: data?.shape,
dataType: data?.data ? data.data.constructor.name : null,
fullData: data // 打印完整数据
fullData: data
});
// 检查是否为空
if (!data) {
console.log("Data is null or undefined");
return false;
}
// 如果是数组,获取第一个元素
if (Array.isArray(data)) {
console.log("Data is array, getting first element");
data = data[0];
}
// 检查数据结构
if (!data || typeof data !== 'object') {
console.log("Invalid data type");
return false;
}
// 检查是否有数据属性
if (!data.data) {
console.log("Missing data property");
return false;
}
// 检查数据类型
if (!(data.data instanceof Float32Array)) {
// 如果不是 Float32Array尝试转换
try {
data.data = new Float32Array(data.data);
} catch (e) {
@@ -700,79 +685,37 @@ function validateImageData(data) {
return true;
}
// 转换 ComfyUI 图像数据为画布可用格式
function convertImageData(data) {
console.log("Converting image data:", data);
// 如果是数组,获取第一个元素
if (Array.isArray(data)) {
data = data[0];
}
// 获取维度信息 [batch, height, width, channels]
const shape = data.shape;
const height = shape[1]; // 1393
const width = shape[2]; // 1393
const channels = shape[3]; // 3
const height = shape[1];
const width = shape[2];
const channels = shape[3];
const floatData = new Float32Array(data.data);
console.log("Processing dimensions:", { height, width, channels });
// 创建画布格式的数据 (RGBA)
console.log("Processing dimensions:", {height, width, channels});
const rgbaData = new Uint8ClampedArray(width * height * 4);
// 转换数据格式 [batch, height, width, channels] -> RGBA
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;
// 复制 RGB 通道并转换值范围 (0-1 -> 0-255)
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)));
}
// 设置 alpha 通道为完全不透明
rgbaData[pixelIndex + 3] = 255;
}
}
// 返回画布可用的格式
return {
data: rgbaData, // Uint8ClampedArray 格式的 RGBA 数据
width: width, // 图像宽度
height: height // 图像高度
};
}
// 处理遮罩数据
function applyMaskToImageData(imageData, maskData) {
console.log("Applying mask to image data");
const rgbaData = new Uint8ClampedArray(imageData.data);
const width = imageData.width;
const height = imageData.height;
// 获取遮罩数据 [batch, height, width]
const maskShape = maskData.shape;
const maskFloatData = new Float32Array(maskData.data);
console.log(`Applying mask of shape: ${maskShape}`);
// 将遮罩数据应用到 alpha 通道
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;
// 使遮罩值作为 alpha 值,转换值范围从 0-1 到 0-255
const alpha = maskFloatData[maskIndex];
rgbaData[pixelIndex + 3] = Math.max(0, Math.min(255, Math.round(alpha * 255)));
}
}
console.log("Mask application completed");
return {
data: rgbaData,
width: width,
@@ -780,41 +723,66 @@ function applyMaskToImageData(imageData, maskData) {
};
}
// 修改缓存管理
function applyMaskToImageData(imageData, maskData) {
console.log("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);
console.log(`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)));
}
}
console.log("Mask application completed");
return {
data: rgbaData,
width: width,
height: height
};
}
const ImageCache = {
cache: new Map(),
// 存储图像数据
set(key, imageData) {
console.log("Caching image data for key:", key);
this.cache.set(key, imageData);
},
// 获取图像数据
get(key) {
const data = this.cache.get(key);
console.log("Retrieved cached data for key:", key, !!data);
return data;
},
// 检查是否存在
has(key) {
return this.cache.has(key);
},
// 清除缓存
clear() {
console.log("Clearing image cache");
this.cache.clear();
}
};
// 改进数据准备函数
function prepareImageForCanvas(inputImage) {
console.log("Preparing image for canvas:", inputImage);
try {
// 如果是数组,获取第一个元素
if (Array.isArray(inputImage)) {
inputImage = inputImage[0];
}
@@ -823,36 +791,30 @@ function prepareImageForCanvas(inputImage) {
throw new Error("Invalid input image format");
}
// 获取维度信息 [batch, height, width, channels]
const shape = inputImage.shape;
const height = shape[1];
const width = shape[2];
const channels = shape[3];
const floatData = new Float32Array(inputImage.data);
console.log("Image dimensions:", { height, width, channels });
// 创建 RGBA 格式数据
console.log("Image dimensions:", {height, width, channels});
const rgbaData = new Uint8ClampedArray(width * height * 4);
// 转换数据格式 [batch, height, width, channels] -> RGBA
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;
// 转换 RGB 通道 (0-1 -> 0-255)
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)));
}
// 设置 alpha 通道
rgbaData[pixelIndex + 3] = 255;
}
}
// 返回画布需要的格式
return {
data: rgbaData,
width: width,
@@ -869,21 +831,78 @@ app.registerExtension({
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "CanvasNode") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function() {
nodeType.prototype.onNodeCreated = async function () {
const r = onNodeCreated?.apply(this, arguments);
const widget = this.widgets.find(w => w.name === "canvas_image");
await createCanvasWidget(this, widget, app);
return r;
};
const originalGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (_, options) {
originalGetExtraMenuOptions?.apply(this, arguments);
const self = this;
const newOptions = [
{
content: "Open Image",
callback: async () => {
try {
const blob = await self.canvasWidget.getFlattenedCanvasAsBlob();
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (e) {
console.error("Error opening image:", e);
}
},
},
{
content: "Copy Image",
callback: async () => {
try {
const blob = await self.canvasWidget.getFlattenedCanvasAsBlob();
const item = new ClipboardItem({'image/png': blob});
await navigator.clipboard.write([item]);
console.log("Image copied to clipboard.");
} catch (e) {
console.error("Error copying image:", e);
alert("Failed to copy image to clipboard.");
}
},
},
{
content: "Save Image",
callback: async () => {
try {
const blob = await self.canvasWidget.getFlattenedCanvasAsBlob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'canvas_output.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (e) {
console.error("Error saving image:", e);
}
},
},
];
if (options.length > 0) {
options.unshift({content: "___", disabled: true});
}
options.unshift(...newOptions);
};
}
}
});
});
async function handleImportInput(data) {
if (data && data.image) {
const imageData = data.image;
await importImage(imageData);
}
}
}