mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-14 16:20:50 +08:00
Image multi upload widget
This commit is contained in:
parent
29c50954ea
commit
d934119333
69
nodes.py
69
nodes.py
@ -1085,7 +1085,7 @@ class LoadImage:
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
|
||||
return {"required":
|
||||
{"image": (sorted(files), )},
|
||||
{"image": (sorted(files), { "forceInput": True })},
|
||||
}
|
||||
|
||||
CATEGORY = "image"
|
||||
@ -1121,6 +1121,72 @@ class LoadImage:
|
||||
|
||||
return True
|
||||
|
||||
class LoadImageBatch:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
|
||||
return {"required":
|
||||
{"images": (sorted(files), )},
|
||||
}
|
||||
|
||||
CATEGORY = "image"
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
FUNCTION = "load_images"
|
||||
|
||||
INPUT_IS_LIST = True
|
||||
OUTPUT_IS_LIST = (True, True, )
|
||||
|
||||
def load_images(self, images):
|
||||
output_images = []
|
||||
output_masks = []
|
||||
|
||||
for i in range(len(images)):
|
||||
image_path = folder_paths.get_annotated_filepath(images[i])
|
||||
|
||||
i = Image.open(image_path)
|
||||
i = ImageOps.exif_transpose(i)
|
||||
image = i.convert("RGB")
|
||||
image = np.array(image).astype(np.float32) / 255.0
|
||||
image = torch.from_numpy(image)[None,]
|
||||
if 'A' in i.getbands():
|
||||
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
|
||||
mask = 1. - torch.from_numpy(mask)
|
||||
else:
|
||||
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
|
||||
|
||||
output_images.append(image)
|
||||
output_masks.append(mask)
|
||||
|
||||
return (output_images, output_masks, )
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, images):
|
||||
hashes = []
|
||||
|
||||
for image in images:
|
||||
image_path = folder_paths.get_annotated_filepath(image)
|
||||
m = hashlib.sha256()
|
||||
with open(image_path, 'rb') as f:
|
||||
m.update(f.read())
|
||||
hashes.append(m.digest().hex())
|
||||
|
||||
return hashes
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(s, images):
|
||||
invalid = []
|
||||
|
||||
for image in images:
|
||||
if not folder_paths.exists_annotated_filepath(image):
|
||||
invalid.append(image)
|
||||
|
||||
if len(invalid) > 0:
|
||||
return "Invalid image file(s): {}".format(", ".join(invalid))
|
||||
|
||||
return True
|
||||
|
||||
class LoadImageMask:
|
||||
_color_channels = ["alpha", "red", "green", "blue"]
|
||||
@classmethod
|
||||
@ -1289,6 +1355,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"PreviewImage": PreviewImage,
|
||||
"LoadImage": LoadImage,
|
||||
"LoadImageMask": LoadImageMask,
|
||||
"LoadImageBatch": LoadImageBatch,
|
||||
"ImageScale": ImageScale,
|
||||
"ImageInvert": ImageInvert,
|
||||
"ImagePadForOutpaint": ImagePadForOutpaint,
|
||||
|
||||
@ -5,8 +5,14 @@ import { app } from "/scripts/app.js";
|
||||
app.registerExtension({
|
||||
name: "Comfy.UploadImage",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "LoadImage" || nodeData.name === "LoadImageMask") {
|
||||
switch (nodeData.name) {
|
||||
case "LoadImage":
|
||||
case "LoadImageMask":
|
||||
nodeData.input.required.upload = ["IMAGEUPLOAD"];
|
||||
break;
|
||||
case "LoadImageBatch":
|
||||
nodeData.input.required.upload = ["MULTIIMAGEUPLOAD"];
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -1110,22 +1110,23 @@ export class ComfyApp {
|
||||
for (const inputName in inputs) {
|
||||
const inputData = inputs[inputName];
|
||||
const type = inputData[0];
|
||||
const inputShape = nodeData["input_is_list"] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE;
|
||||
|
||||
if(inputData[1]?.forceInput) {
|
||||
this.addInput(inputName, type);
|
||||
this.addInput(inputName, type, { shape: inputShape });
|
||||
} else {
|
||||
if (Array.isArray(type)) {
|
||||
// Enums
|
||||
Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {});
|
||||
Object.assign(config, widgets.COMBO(this, inputName, inputData, nodeData, app) || {});
|
||||
} else if (`${type}:${inputName}` in widgets) {
|
||||
// Support custom widgets by Type:Name
|
||||
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
|
||||
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, nodeData, app) || {});
|
||||
} else if (type in widgets) {
|
||||
// Standard type widgets
|
||||
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
|
||||
Object.assign(config, widgets[type](this, inputName, inputData, nodeData, app) || {});
|
||||
} else {
|
||||
// Node connection inputs
|
||||
this.addInput(inputName, type);
|
||||
this.addInput(inputName, type, { shape: inputShape });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1133,7 +1134,7 @@ export class ComfyApp {
|
||||
for (const o in nodeData["output"]) {
|
||||
const output = nodeData["output"][o];
|
||||
const outputName = nodeData["output_name"][o] || output;
|
||||
const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE ;
|
||||
const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE;
|
||||
this.addOutput(outputName, output, { shape: outputShape });
|
||||
}
|
||||
|
||||
|
||||
@ -246,55 +246,217 @@ function addMultilineWidget(node, name, opts, app) {
|
||||
return { minWidth: 400, minHeight: 200, widget };
|
||||
}
|
||||
|
||||
export const ComfyWidgets = {
|
||||
"INT:seed": seedWidget,
|
||||
"INT:noise_seed": seedWidget,
|
||||
FLOAT(node, inputName, inputData) {
|
||||
const { val, config } = getNumberDefaults(inputData, 0.5);
|
||||
return { widget: node.addWidget("number", inputName, val, () => {}, config) };
|
||||
},
|
||||
INT(node, inputName, inputData) {
|
||||
const { val, config } = getNumberDefaults(inputData, 1);
|
||||
Object.assign(config, { precision: 0 });
|
||||
return {
|
||||
widget: node.addWidget(
|
||||
"number",
|
||||
inputName,
|
||||
val,
|
||||
function (v) {
|
||||
const s = this.options.step / 10;
|
||||
this.value = Math.round(v / s) * s;
|
||||
},
|
||||
config
|
||||
),
|
||||
};
|
||||
},
|
||||
STRING(node, inputName, inputData, app) {
|
||||
const defaultVal = inputData[1].default || "";
|
||||
const multiline = !!inputData[1].multiline;
|
||||
const FLOAT = (node, inputName, inputData) => {
|
||||
const { val, config } = getNumberDefaults(inputData, 0.5);
|
||||
return { widget: node.addWidget("number", inputName, val, () => {}, config) };
|
||||
}
|
||||
|
||||
if (multiline) {
|
||||
return addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
|
||||
} else {
|
||||
return { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) };
|
||||
}
|
||||
},
|
||||
COMBO(node, inputName, inputData) {
|
||||
const type = inputData[0];
|
||||
let defaultValue = type[0];
|
||||
if (inputData[1] && inputData[1].default) {
|
||||
defaultValue = inputData[1].default;
|
||||
}
|
||||
const INT = (node, inputName, inputData) => {
|
||||
const { val, config } = getNumberDefaults(inputData, 1);
|
||||
Object.assign(config, { precision: 0 });
|
||||
return {
|
||||
widget: node.addWidget(
|
||||
"number",
|
||||
inputName,
|
||||
val,
|
||||
function (v) {
|
||||
const s = this.options.step / 10;
|
||||
this.value = Math.round(v / s) * s;
|
||||
},
|
||||
config
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const STRING = (node, inputName, inputData, nodeData, app) => {
|
||||
const defaultVal = inputData[1].default || "";
|
||||
const multiline = !!inputData[1].multiline;
|
||||
|
||||
if (multiline) {
|
||||
return addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
|
||||
} else {
|
||||
return { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) };
|
||||
}
|
||||
}
|
||||
|
||||
const COMBO = (node, inputName, inputData, nodeData) => {
|
||||
const type = inputData[0];
|
||||
let defaultValue = type[0];
|
||||
if (inputData[1] && inputData[1].default) {
|
||||
defaultValue = inputData[1].default;
|
||||
}
|
||||
|
||||
if (nodeData["input_is_list"]) {
|
||||
defaultValue = [defaultValue]
|
||||
const widget = node.addWidget("text", inputName, defaultValue, () => {}, { values: type })
|
||||
widget.disabled = true;
|
||||
return { widget };
|
||||
}
|
||||
else {
|
||||
return { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) };
|
||||
},
|
||||
IMAGEUPLOAD(node, inputName, inputData, app) {
|
||||
const imageWidget = node.widgets.find((w) => w.name === "image");
|
||||
let uploadWidget;
|
||||
}
|
||||
}
|
||||
|
||||
function showImage(name) {
|
||||
const IMAGEUPLOAD = (node, inputName, inputData, nodeData, app) => {
|
||||
const imageWidget = node.widgets.find((w) => w.name === "image");
|
||||
let uploadWidget;
|
||||
|
||||
function showImage(name) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
node.imgs = [img];
|
||||
app.graph.setDirtyCanvas(true);
|
||||
};
|
||||
let folder_separator = name.lastIndexOf("/");
|
||||
let subfolder = "";
|
||||
if (folder_separator > -1) {
|
||||
subfolder = name.substring(0, folder_separator);
|
||||
name = name.substring(folder_separator + 1);
|
||||
}
|
||||
img.src = `/view?filename=${name}&type=input&subfolder=${subfolder}`;
|
||||
node.setSizeForImage?.();
|
||||
}
|
||||
|
||||
var default_value = imageWidget.value;
|
||||
Object.defineProperty(imageWidget, "value", {
|
||||
set : function(value) {
|
||||
this._real_value = value;
|
||||
},
|
||||
|
||||
get : function() {
|
||||
let value = "";
|
||||
if (this._real_value) {
|
||||
value = this._real_value;
|
||||
} else {
|
||||
return default_value;
|
||||
}
|
||||
|
||||
if (value.filename) {
|
||||
let real_value = value;
|
||||
value = "";
|
||||
if (real_value.subfolder) {
|
||||
value = real_value.subfolder + "/";
|
||||
}
|
||||
|
||||
value += real_value.filename;
|
||||
|
||||
if(real_value.type && real_value.type !== "input")
|
||||
value += ` [${real_value.type}]`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
// Add our own callback to the combo widget to render an image when it changes
|
||||
const cb = node.callback;
|
||||
imageWidget.callback = function () {
|
||||
showImage(imageWidget.value);
|
||||
if (cb) {
|
||||
return cb.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// On load if we have a value then render the image
|
||||
// The value isnt set immediately so we need to wait a moment
|
||||
// No change callbacks seem to be fired on initial setting of the value
|
||||
requestAnimationFrame(() => {
|
||||
if (imageWidget.value) {
|
||||
showImage(imageWidget.value);
|
||||
}
|
||||
});
|
||||
|
||||
async function uploadFile(file, updateNode) {
|
||||
try {
|
||||
// Wrap file in formdata so it includes filename
|
||||
const body = new FormData();
|
||||
body.append("image", file);
|
||||
const resp = await fetch("/upload/image", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json();
|
||||
// Add the file as an option and update the widget value
|
||||
if (!imageWidget.options.values.includes(data.name)) {
|
||||
imageWidget.options.values.push(data.name);
|
||||
}
|
||||
|
||||
if (updateNode) {
|
||||
showImage(data.name);
|
||||
|
||||
imageWidget.value = data.name;
|
||||
}
|
||||
} else {
|
||||
alert(resp.status + " - " + resp.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
|
||||
const fileInput = document.createElement("input");
|
||||
Object.assign(fileInput, {
|
||||
type: "file",
|
||||
accept: "image/jpeg,image/png,image/webp",
|
||||
style: "display: none",
|
||||
onchange: async () => {
|
||||
if (fileInput.files.length) {
|
||||
await uploadFile(fileInput.files[0], true);
|
||||
}
|
||||
},
|
||||
});
|
||||
document.body.append(fileInput);
|
||||
|
||||
// Create the button widget for selecting the files
|
||||
uploadWidget = node.addWidget("button", "choose file to upload", "image", () => {
|
||||
fileInput.value = null;
|
||||
fileInput.click();
|
||||
});
|
||||
uploadWidget.serialize = false;
|
||||
|
||||
// Add handler to check if an image is being dragged over our node
|
||||
node.onDragOver = function (e) {
|
||||
if (e.dataTransfer && e.dataTransfer.items) {
|
||||
const image = [...e.dataTransfer.items].find((f) => f.kind === "file" && f.type.startsWith("image/"));
|
||||
return !!image;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// On drop upload files
|
||||
node.onDragDrop = function (e) {
|
||||
console.log("onDragDrop called");
|
||||
let handled = false;
|
||||
for (const file of e.dataTransfer.files) {
|
||||
if (file.type.startsWith("image/")) {
|
||||
uploadFile(file, !handled); // Dont await these, any order is fine, only update on first one
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
return handled;
|
||||
};
|
||||
|
||||
return { widget: uploadWidget };
|
||||
}
|
||||
|
||||
const MULTIIMAGEUPLOAD = (node, inputName, inputData, nodeData, app) => {
|
||||
const imagesWidget = node.widgets.find((w) => w.name === "images");
|
||||
let uploadWidget;
|
||||
let clearWidget;
|
||||
|
||||
function showImages(names) {
|
||||
node.imgs = []
|
||||
|
||||
for (const name of names) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
node.imgs = [img];
|
||||
// TODO await this?
|
||||
node.imgs.push(img)
|
||||
node.imageIndex = null;
|
||||
node.setSizeForImage?.();
|
||||
app.graph.setDirtyCanvas(true);
|
||||
};
|
||||
let folder_separator = name.lastIndexOf("/");
|
||||
@ -306,21 +468,20 @@ export const ComfyWidgets = {
|
||||
img.src = `/view?filename=${name}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}`;
|
||||
node.setSizeForImage?.();
|
||||
}
|
||||
}
|
||||
|
||||
var default_value = imageWidget.value;
|
||||
Object.defineProperty(imageWidget, "value", {
|
||||
set : function(value) {
|
||||
this._real_value = value;
|
||||
},
|
||||
var default_value = imagesWidget.value;
|
||||
Object.defineProperty(imagesWidget, "value", {
|
||||
set : function(value) {
|
||||
this._real_value = value;
|
||||
},
|
||||
|
||||
get : function() {
|
||||
let value = "";
|
||||
if (this._real_value) {
|
||||
value = this._real_value;
|
||||
} else {
|
||||
return default_value;
|
||||
}
|
||||
get : function() {
|
||||
this._real_value ||= []
|
||||
|
||||
const result = []
|
||||
|
||||
for (const value of this._real_value) {
|
||||
if (value.filename) {
|
||||
let real_value = value;
|
||||
value = "";
|
||||
@ -333,29 +494,35 @@ export const ComfyWidgets = {
|
||||
if(real_value.type && real_value.type !== "input")
|
||||
value += ` [${real_value.type}]`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
// Add our own callback to the combo widget to render an image when it changes
|
||||
const cb = node.callback;
|
||||
imageWidget.callback = function () {
|
||||
showImage(imageWidget.value);
|
||||
if (cb) {
|
||||
return cb.apply(this, arguments);
|
||||
result.push(value)
|
||||
}
|
||||
};
|
||||
|
||||
// On load if we have a value then render the image
|
||||
// The value isnt set immediately so we need to wait a moment
|
||||
// No change callbacks seem to be fired on initial setting of the value
|
||||
requestAnimationFrame(() => {
|
||||
if (imageWidget.value) {
|
||||
showImage(imageWidget.value);
|
||||
}
|
||||
});
|
||||
this._real_value = result
|
||||
return this._real_value;
|
||||
}
|
||||
});
|
||||
|
||||
async function uploadFile(file, updateNode) {
|
||||
// Add our own callback to the combo widget to render an image when it changes
|
||||
const cb = node.callback;
|
||||
imagesWidget.callback = function () {
|
||||
showImages(imagesWidget.value);
|
||||
if (cb) {
|
||||
return cb.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// On load if we have a value then render the image
|
||||
// The value isnt set immediately so we need to wait a moment
|
||||
// No change callbacks seem to be fired on initial setting of the value
|
||||
requestAnimationFrame(() => {
|
||||
if (Array.isArray(imagesWidget.value) && imagesWidget.value.length > 0) {
|
||||
showImages(imagesWidget.value);
|
||||
}
|
||||
});
|
||||
|
||||
async function uploadFiles(files, updateNode) {
|
||||
for (const file of files) {
|
||||
try {
|
||||
// Wrap file in formdata so it includes filename
|
||||
const body = new FormData();
|
||||
@ -368,14 +535,12 @@ export const ComfyWidgets = {
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json();
|
||||
// Add the file as an option and update the widget value
|
||||
if (!imageWidget.options.values.includes(data.name)) {
|
||||
imageWidget.options.values.push(data.name);
|
||||
if (!imagesWidget.options.values.includes(data.name)) {
|
||||
imagesWidget.options.values.push(data.name);
|
||||
}
|
||||
|
||||
if (updateNode) {
|
||||
showImage(data.name);
|
||||
|
||||
imageWidget.value = data.name;
|
||||
imagesWidget.value.push(data.name)
|
||||
}
|
||||
} else {
|
||||
alert(resp.status + " - " + resp.statusText);
|
||||
@ -385,49 +550,72 @@ export const ComfyWidgets = {
|
||||
}
|
||||
}
|
||||
|
||||
const fileInput = document.createElement("input");
|
||||
Object.assign(fileInput, {
|
||||
type: "file",
|
||||
accept: "image/jpeg,image/png,image/webp",
|
||||
style: "display: none",
|
||||
onchange: async () => {
|
||||
if (fileInput.files.length) {
|
||||
await uploadFile(fileInput.files[0], true);
|
||||
}
|
||||
},
|
||||
});
|
||||
document.body.append(fileInput);
|
||||
if (updateNode) {
|
||||
showImages(imagesWidget.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the button widget for selecting the files
|
||||
uploadWidget = node.addWidget("button", "choose file to upload", "image", () => {
|
||||
fileInput.click();
|
||||
});
|
||||
uploadWidget.serialize = false;
|
||||
|
||||
// Add handler to check if an image is being dragged over our node
|
||||
node.onDragOver = function (e) {
|
||||
if (e.dataTransfer && e.dataTransfer.items) {
|
||||
const image = [...e.dataTransfer.items].find((f) => f.kind === "file" && f.type.startsWith("image/"));
|
||||
return !!image;
|
||||
const fileInput = document.createElement("input");
|
||||
Object.assign(fileInput, {
|
||||
type: "file",
|
||||
multiple: "multiple",
|
||||
accept: "image/jpeg,image/png,image/webp",
|
||||
style: "display: none",
|
||||
onchange: async () => {
|
||||
if (fileInput.files.length) {
|
||||
await uploadFiles(fileInput.files, true);
|
||||
}
|
||||
},
|
||||
});
|
||||
document.body.append(fileInput);
|
||||
|
||||
return false;
|
||||
};
|
||||
// Create the button widget for selecting the files
|
||||
uploadWidget = node.addWidget("button", "choose files to upload", "images", () => {
|
||||
fileInput.value = null;
|
||||
fileInput.click();
|
||||
});
|
||||
uploadWidget.serialize = false;
|
||||
|
||||
// On drop upload files
|
||||
node.onDragDrop = function (e) {
|
||||
console.log("onDragDrop called");
|
||||
let handled = false;
|
||||
for (const file of e.dataTransfer.files) {
|
||||
if (file.type.startsWith("image/")) {
|
||||
uploadFile(file, !handled); // Dont await these, any order is fine, only update on first one
|
||||
handled = true;
|
||||
}
|
||||
clearWidget = node.addWidget("button", "clear all uploads", "images", () => {
|
||||
imagesWidget.value = []
|
||||
showImages(imagesWidget.value);
|
||||
});
|
||||
clearWidget.serialize = false;
|
||||
|
||||
// Add handler to check if an image is being dragged over our node
|
||||
node.onDragOver = function (e) {
|
||||
if (e.dataTransfer && e.dataTransfer.items) {
|
||||
const image = [...e.dataTransfer.items].find((f) => f.kind === "file" && f.type.startsWith("image/"));
|
||||
return !!image;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// On drop upload files
|
||||
node.onDragDrop = function (e) {
|
||||
console.log("onDragDrop called");
|
||||
let handled = false;
|
||||
for (const file of e.dataTransfer.files) {
|
||||
if (file.type.startsWith("image/")) {
|
||||
uploadFile(file, !handled); // Dont await these, any order is fine, only update on first one
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
return handled;
|
||||
};
|
||||
return handled;
|
||||
};
|
||||
|
||||
return { widget: uploadWidget };
|
||||
},
|
||||
return { widget: uploadWidget };
|
||||
}
|
||||
|
||||
export const ComfyWidgets = {
|
||||
"INT:seed": seedWidget,
|
||||
"INT:noise_seed": seedWidget,
|
||||
FLOAT,
|
||||
INT,
|
||||
STRING,
|
||||
COMBO,
|
||||
IMAGEUPLOAD,
|
||||
MULTIIMAGEUPLOAD,
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user