Compare commits

...
20 Commits
Author SHA1 Message Date
Dr.Lt.Data e70b4df9b5 fixed: reproduce mode is reflected to the saved metadata but it wans't 2025-01-27 11:05:12 +09:00
Dr.Lt.Data af1ef7e441 feat: wildcards nodes - reproduce mode is added.
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/882

fixed: js crash
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/886#issuecomment-2613922083

removed: combo bool migration script
2025-01-27 10:55:47 +09:00
Dr.Lt.Data d8738eee2f hotfix: front - robustness fix
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/886#issuecomment-2600716281
2025-01-19 18:08:59 +09:00
Dr.Lt.Data c397c68ca3 version marker 2025-01-19 03:17:25 +09:00
Alex Butler 0e0722ec08 Add tiled vae encoding/decoding toggle to detailer nodes (#883)
* detailers: support optional tiled vae encoding & decoding

* Remove start encoding/decoding logging
2025-01-19 03:16:56 +09:00
Dr.Lt.Data 70d0540895 improved: tooltips 2025-01-14 00:47:27 +09:00
Dr.Lt.Data 7330577a0f version marker 2025-01-10 00:49:38 +09:00
Symbiomatrix b1d760291f Dummy shortcircuit. (#880) 2025-01-10 00:49:06 +09:00
Dr.Lt.Data 12e838a320 improved: refresh wildcard
Now refresh feature is moved to menu item.
`Edit > Impact: Refresh Wildcard`
2025-01-06 22:59:33 +09:00
Dr.Lt.Data 8e8621df49 update .gitignore 2024-12-30 02:32:46 +09:00
Dr.Lt.Data cdb7b4d3b0 fix: compatibility patch for VAEEncodeTiled's overlap
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/863#issuecomment-2560861500
2024-12-25 16:14:51 +09:00
Dr.Lt.Data 21eecb0c03 fix: install script - folder_paths error 2024-12-22 18:40:09 +09:00
Dr.Lt.Data 9402ecf4f9 refactor: use ControlNetApplyAdvanced instead of legacy ControlNetApply 2024-12-21 18:21:58 +09:00
Dr.Lt.Data c21b361e2a fix: VAEEncodeTiled - overlap compatibility patch
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/863
2024-12-21 17:41:24 +09:00
Dr.Lt.Data 8f04714145 fixed: inversed_switch - If the select is pointing to an out-of-range output, remove the input connection instead of the output.
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/851#issuecomment-2543353847
2024-12-19 23:29:34 +09:00
Dr.Lt.Data a311f9278d fixed: sam detector - invalid position
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/853
2024-12-19 23:06:49 +09:00
Dr.Lt.Data 0ec21f1a4b version marker 2024-12-19 22:03:11 +09:00
Andrés Zsögön 40a3df3acd Added Mask Rect Area and Mask Rect Area (Advanced) nodes (#861)
Added nodes for simple rectangular masks of arbitrary size and position with built-in preview canvas. Closes issue #856.
2024-12-19 22:02:40 +09:00
Dr.Lt.Data 9ba92862c4 update README.md 2024-12-17 11:03:02 +09:00
Dr.Lt.Data 2c2e148205 update README.md 2024-12-17 10:58:17 +09:00
19 changed files with 1165 additions and 220 deletions
+2
View File
@@ -7,3 +7,5 @@ subpack
impact_subpack
*.txt
*.yaml
!requirements.txt
!LICENSE.txt
+13 -49
View File
@@ -68,6 +68,8 @@ NOTE: The UltralyticsDetectorProvider node is not part of the ComfyUI-Impact-Pac
* `Dilate Mask` - Dilate Mask.
* Support erosion for negative value.
* `Gaussian Blur Mask` - Apply Gaussian Blur to Mask. You can utilize this for mask feathering.
* `Mask Rect Area` - Create a rectangular mask defined by percentages with preview canvas.
* `Mask Rect Area (Advanced)` - Create a rectangular mask defined by pixels and image size.
### [Detailer nodes](https://github.com/ltdrdata/ComfyUI-extension-tutorials/blob/Main/ComfyUI-Impact-Pack/tutorial/detailers.md)
* `Detailer (SEGS)` - Refines the image based on SEGS.
@@ -273,6 +275,7 @@ NOTE: The UltralyticsDetectorProvider node is not part of the ComfyUI-Impact-Pac
* For supported labels, please refer to the `config.json` of the respective HuggingFace repository.
* `#Female` and `#Male` are symbols that group multiple labels such as `Female, women, woman, ...`, for convenience, rather than being single labels.
### Etc nodes
* `Impact Scheduler Adapter` - With the addition of AYS to the scheduler of the Impact Pack and Inspire Pack, there is an issue of incompatibility when the existing scheduler widget is converted to input. The Impact Scheduler Adapter allows for an indirect connection to be possible.
* `StringListToString` - Convert String List to String
@@ -288,60 +291,25 @@ NOTE: The UltralyticsDetectorProvider node is not part of the ComfyUI-Impact-Pac
* `List Bridge` - When passing the list output through this node, it collects and organizes the data before forwarding it, which ensures that the previous stage's sub-workflow has been completed.
## MMDet nodes (DEPRECATED) - Don't use these nodes
* MMDetDetectorProvider - Loads the MMDet model to provide BBOX_DETECTOR and SEGM_DETECTOR.
* To use the existing MMDetDetectorProvider, you need to enable the MMDet usage configuration.
## Feature
* `Interactive SAM Detector (Clipspace)` - When you right-click on a node that has 'MASK' and 'IMAGE' outputs, a context menu will open. From this menu, you can either open a dialog to create a SAM Mask using 'Open in SAM Detector', or copy the content (likely mask data) using 'Copy (Clipspace)' and generate a mask using 'Impact SAM Detector' from the clipspace menu, and then paste it using 'Paste (Clipspace)'.
* Providing a feature to detect errors that occur when mixing models and clips from checkpoints such as `SDXL Base`, `SDXL Refiner`, `SD1.x`, `SD2.x` during sample execution, and reporting appropriate errors.
## Deprecated
* The following nodes have been kept only for compatibility with existing workflows, and are no longer supported. Please replace them with new nodes.
* ONNX Detector (SEGS) - BBOX Detector (SEGS)
* MMDetLoader -> MMDetDetectorProvider
* SegsMaskCombine -> SEGS to MASK (combined)
* BboxDetectorForEach -> BBOX Detector (SEGS)
* SegmDetectorForEach -> SEGM Detector (SEGS)
* BboxDetectorCombined -> BBOX Detector (combined)
* SegmDetectorCombined -> SEGM Detector (combined)
* MaskPainter -> PreviewBridge
* To use the existing deprecated legacy nodes, you need to enable the MMDet usage configuration.
## How to activate 'MMDet usage' (DEPRECATED)
* Upon the initial execution, an `impact-pack.ini` file will be generated in the custom_nodes/ComfyUI-Impact-Pack directory.
```
[default]
dependency_version = 2
mmdet_skip = True
```
* Change `mmdet_skip = True` to `mmdet_skip = False`
```
[default]
dependency_version = 2
mmdet_skip = False
```
* Restart ComfyUI
## Installation
## How To Install?
### Install via ComfyUI-Manager (Recommended)
* Search `ComfyUI Impact Pack` in ComfyUI-Manager and click `Install` button.
### Manual Install (Not Recommended)
1. `cd custom_nodes`
2. `git clone https://github.com/ltdrdata/ComfyUI-Impact-Pack.git`
2. `git clone https://github.com/ltdrdata/ComfyUI-Impact-Pack`
3. `cd ComfyUI-Impact-Pack`
4. (optional) `git clone https://github.com/ltdrdata/ComfyUI-Impact-Subpack impact_subpack`
* Impact Pack will automatically download subpack during its initial launch.
5. (optional) `python install-manual.py`
* Impact Pack will automatically install its dependencies during its initial launch.
* For the portable version, you should execute the command `..\..\..\python_embeded\python.exe install-manual.py` to run the installation script.
6. Restart ComfyUI
4. `pip install -r requirements.txt`
* **IMPORTANT**:
* You must install it within the Python environment where ComfyUI is running.
* For the portable version, use `<installed path>\python_embeded\python.exe -m pip` instead of `pip`. For a `venv`, activate the `venv` first and then use `pip`.
5. Restart ComfyUI
* NOTE1: If an error occurs during the installation process, please refer to [Troubleshooting Page](troubleshooting/TROUBLESHOOTING.md) for assistance.
* NOTE2: You can use this colab notebook [colab notebook](https://colab.research.google.com/github/ltdrdata/ComfyUI-Impact-Pack/blob/Main/notebook/comfyui_colab_impact_pack.ipynb) to launch it. This notebook automatically downloads the impact pack to the custom_nodes directory, installs the tested dependencies, and runs it.
@@ -363,9 +331,6 @@ mmdet_skip = False
* (deprecated) openmim # for mim
* (deprecated) pycocotools # for mim
* mim install (deprecated)
* mmcv==2.0.0, mmdet==3.0.0, mmengine==0.7.2
* linux packages (ubuntu)
* libgl1-mesa-glx
* libglib2.0-0
@@ -388,17 +353,16 @@ sam_editor_model = sam_vit_b_01ec64.pth
```
## Other Materials (auto-download on initial startup)
## Other Materials (auto-download when installing)
* ComfyUI/models/mmdets/bbox <= https://huggingface.co/dustysys/ddetailer/resolve/main/mmdet/bbox/mmdet_anime-face_yolov3.pth
* ComfyUI/models/mmdets/bbox <= https://raw.githubusercontent.com/Bing-su/dddetailer/master/config/mmdet_anime-face_yolov3.py
* ComfyUI/models/sams <= https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth
## Troubleshooting page
* [Troubleshooting Page](troubleshooting/TROUBLESHOOTING.md)
## How to use (DDetailer feature)
## How To Use (DDetailer feature)
#### 1. Basic auto face detection and refine exapmle.
![simple](https://github.com/ltdrdata/ComfyUI-extension-tutorials/raw/Main/ComfyUI-Impact-Pack/images/simple.png)
+4
View File
@@ -137,6 +137,8 @@ NODE_CLASS_MAPPINGS = {
"BitwiseAndMask": BitwiseAndMask,
"SubtractMask": SubtractMask,
"AddMask": AddMask,
"MaskRectArea": MaskRectArea,
"MaskRectAreaAdvanced": MaskRectAreaAdvanced,
"ImpactSegsAndMask": SegsBitwiseAndMask,
"ImpactSegsAndMaskForEach": SegsBitwiseAndMaskForEach,
"EmptySegs": EmptySEGS,
@@ -318,6 +320,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"BitwiseAndMask": "Pixelwise(MASK & MASK)",
"SubtractMask": "Pixelwise(MASK - MASK)",
"AddMask": "Pixelwise(MASK + MASK)",
"MaskRectArea": "Mask Rect Area",
"MaskRectAreaAdvanced": "Mask Rect Area (Advanced)",
"ImpactFlattenMask": "Flatten Mask Batch",
"DetailerForEach": "Detailer (SEGS)",
"DetailerForEachPipe": "Detailer (SEGS/pipe)",
-1
View File
@@ -69,7 +69,6 @@ def process_wrap(cmd_str, cwd=None, handler=None, env=None):
try:
import platform
import folder_paths
from torchvision.datasets.utils import download_url
import impact.config
-35
View File
@@ -1,35 +0,0 @@
import { ComfyApp, app } from "../../scripts/app.js";
let conflict_check = undefined;
app.registerExtension({
name: "Comfy.impact.comboBoolMigration",
nodeCreated(node, app) {
for(let i in node.widgets) {
let widget = node.widgets[i];
if(conflict_check == undefined) {
conflict_check = !!app.extensions.find((ext) => ext.name === "Comfy.comboBoolMigration");
}
if(conflict_check)
return;
if(widget.type == "toggle") {
let value = widget.value;
var v = Object.getOwnPropertyDescriptor(widget, 'value');
if(!v) {
Object.defineProperty(widget, "value", {
set: (value) => {
delete widget.value;
widget.value = value == true || value == widget.options.on;
},
get: () => { return value; }
});
}
}
}
}
});
+41 -6
View File
@@ -93,7 +93,7 @@ const input_dirty = {};
const output_tracking = {};
function progressExecuteHandler(event) {
if(event.detail.output.aux){
if(event.detail?.output?.aux){
const id = event.detail.node;
if(input_tracking.hasOwnProperty(id)) {
if(input_tracking.hasOwnProperty(id) && input_tracking[id][0] != event.detail.output.aux[0]) {
@@ -222,6 +222,31 @@ api.addEventListener("executed", progressExecuteHandler);
app.registerExtension({
name: "Comfy.Impack",
commands: [
{
id: 'refresh-impact-wildcard',
label: 'Impact: Refresh Wildcard',
function: async () => {
await api.fetchApi('/impact/wildcards/refresh');
await load_wildcards();
app.extensionManager.toast.add({
severity: 'info',
summary: 'Refreshed!',
detail: 'Impact Wildcard List is refreshed!!',
life: 3000
});
}
}
],
menuCommands: [
{
path: ['Edit'],
commands: ['refresh-impact-wildcard']
}
],
loadedGraphNode(node, app) {
if (node.comfyClass == "MaskPainter") {
input_dirty[node.id + ""] = true;
@@ -248,7 +273,7 @@ app.registerExtension({
}
else {
const node = app.graph.getNodeById(link_info.origin_id);
slot_type = node.outputs[link_info.origin_slot].type;
slot_type = node.outputs[link_info.origin_slot]?.type;
}
this.inputs[0].type = slot_type;
@@ -340,7 +365,11 @@ app.registerExtension({
// connect input
if(this.inputs[0].type == '*'){
const node = app.graph.getNodeById(link_info.origin_id);
let origin_type = node.outputs[link_info.origin_slot].type;
let origin_type = node.outputs[link_info.origin_slot]?.type;
if(origin_type==undefined) {
return; // fallback
}
if(origin_type == '*') {
this.disconnectInput(link_info.target_slot);
@@ -730,14 +759,20 @@ app.registerExtension({
// mode combo
Object.defineProperty(mode_widget, "value", {
set: (value) => {
node._mode_value = value == true || value == "Populate";
populated_text_widget.inputEl.disabled = value == true || value == "Populate";
if(value == true)
node._mode_value = "populate";
else if(value == false)
node._mode_value = "fixed";
else
node._mode_value = value; // combo value
populated_text_widget.inputEl.disabled = node._mode_value != 'populate';
},
get: () => {
if(node._mode_value != undefined)
return node._mode_value;
else
return true;
return 'populate';
}
});
}
+9 -8
View File
@@ -262,7 +262,7 @@ class ImpactSamEditorDialog extends ComfyDialog {
const pointsCanvas = document.createElement('canvas');
imgCanvas.id = "imageCanvas";
maskCanvas.id = "maskCanvas";
maskCanvas.id = "samEditorMaskCanvas";
pointsCanvas.id = "pointsCanvas";
this.setlayout(imgCanvas, maskCanvas, pointsCanvas);
@@ -356,13 +356,13 @@ class ImpactSamEditorDialog extends ComfyDialog {
let w = (drawWidth * imgCanvas.clientWidth/imgCanvas.width) + "px";
let h = (drawHeight * imgCanvas.clientHeight/imgCanvas.height) + "px";
pointsCanvas.width = drawWidth;
pointsCanvas.height = drawHeight;
pointsCanvas.width = drawWidth * imgCanvas.clientWidth/imgCanvas.width;
pointsCanvas.height = drawHeight * imgCanvas.clientHeight/imgCanvas.height;
pointsCanvas.style.top = imgCanvas.offsetTop + "px";
pointsCanvas.style.left = imgCanvas.offsetLeft + "px";
maskCanvas.style.width = w;
maskCanvas.style.height = h;
maskCanvas.width = pointsCanvas.width;
maskCanvas.height = pointsCanvas.height;
maskCanvas.style.top = imgCanvas.offsetTop + "px";
maskCanvas.style.left = imgCanvas.offsetLeft + "px";
@@ -476,8 +476,9 @@ class ImpactSamEditorDialog extends ComfyDialog {
for(const i in self.prompt_points) {
const [is_positive, x, y] = self.prompt_points[i];
const point = [x,y];
if(is_positive)
if(is_positive) {
positive_points.push(point);
}
else
negative_points.push(point);
}
@@ -511,8 +512,8 @@ class ImpactSamEditorDialog extends ComfyDialog {
const x = event.offsetX || event.targetTouches[0].clientX - maskRect.left;
const y = event.offsetY || event.targetTouches[0].clientY - maskRect.top;
const originalX = x * self.image.width / self.pointsCanvas.width;
const originalY = y * self.image.height / self.pointsCanvas.height;
const originalX = x * self.image.width / self.pointsCanvas.clientWidth;
const originalY = y * self.image.height / self.pointsCanvas.clientHeight;
var point = null;
if (event.button == 0) {
-20
View File
@@ -1,20 +0,0 @@
import { ComfyApp, app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
let refresh_btn = document.getElementById('comfy-refresh-button');
let refresh_btn2 = document.querySelector('button[title="Refresh widgets in nodes to find new models or files"]');
let orig = refresh_btn.onclick;
if(refresh_btn) {
refresh_btn.onclick = function() {
orig();
api.fetchApi('/impact/wildcards/refresh');
};
}
if(refresh_btn2) {
refresh_btn2?.addEventListener('click', function() {
api.fetchApi('/impact/wildcards/refresh');
});
}
+381
View File
@@ -0,0 +1,381 @@
import { app } from "/scripts/app.js";
function showPreviewCanvas(node, app) {
const widget = {
type: "customCanvas",
name: "mask-rect-area-canvas",
get value() {
return this.canvas.value;
},
set value(x) {
this.canvas.value = x;
},
draw: function (ctx, node, widgetWidth, widgetY) {
// If we are initially offscreen when created we wont have received a resize event
// Calculate it here instead
if (!node.canvasHeight) {
computeCanvasSize(node, node.size);
}
const visible = true;
const t = ctx.getTransform();
const margin = 12;
const border = 2;
const widgetHeight = node.canvasHeight;
const width = Math.round(node.properties["width"]);
const height = Math.round(node.properties["height"]);
const scale = Math.min((widgetWidth - margin * 3) / width, (widgetHeight - margin * 3) / height);
const blurRadius = node.properties["blur_radius"] || 0;
const index = 0;
Object.assign(this.canvas.style, {
left: `${t.e}px`,
top: `${t.f + (widgetY * t.d)}px`,
width: `${widgetWidth * t.a}px`,
height: `${widgetHeight * t.d}px`,
position: "absolute",
zIndex: 1,
fontSize: `${t.d * 10.0}px`,
pointerEvents: "none"
});
this.canvas.hidden = !visible;
let backgroundWidth = width * scale;
let backgroundHeight = height * scale;
let xOffset = margin;
if (backgroundWidth < widgetWidth) {
xOffset += (widgetWidth - backgroundWidth) / 2 - margin;
}
let yOffset = (margin / 2);
if (backgroundHeight < widgetHeight) {
yOffset += (widgetHeight - backgroundHeight) / 2 - margin;
}
let widgetX = xOffset;
widgetY = widgetY + yOffset;
// Draw the background border
ctx.fillStyle = globalThis.LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.fillRect(widgetX - border, widgetY - border, backgroundWidth + border * 2, backgroundHeight + border * 2)
// Draw the main background area
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR;
ctx.fillRect(widgetX, widgetY, backgroundWidth, backgroundHeight);
// Draw the conditioning zone
let [x, y, w, h] = getDrawArea(node, backgroundWidth, backgroundHeight);
ctx.fillStyle = getDrawColor(0, "80");
ctx.fillRect(widgetX + x, widgetY + y, w, h);
ctx.beginPath();
ctx.lineWidth = 1;
// Draw grid lines
for (let x = 0; x <= width / 64; x += 1) {
ctx.moveTo(widgetX + x * 64 * scale, widgetY);
ctx.lineTo(widgetX + x * 64 * scale, widgetY + backgroundHeight);
}
for (let y = 0; y <= height / 64; y += 1) {
ctx.moveTo(widgetX, widgetY + y * 64 * scale);
ctx.lineTo(widgetX + backgroundWidth, widgetY + y * 64 * scale);
}
ctx.strokeStyle = "#66666650";
ctx.stroke();
ctx.closePath();
// Draw current zone
let [sx, sy, sw, sh] = getDrawArea(node, backgroundWidth, backgroundHeight);
ctx.fillStyle = getDrawColor(0, "80");
ctx.fillRect(widgetX + sx, widgetY + sy, sw, sh);
ctx.fillStyle = getDrawColor(0, "40");
ctx.fillRect(widgetX + sx + border, widgetY + sy + border, sw - border * 2, sh - border * 2);
// Draw white border around the current zone
ctx.strokeStyle = globalThis.LiteGraph.NODE_SELECTED_TITLE_COLOR;
ctx.lineWidth = 2;
ctx.strokeRect(widgetX + sx, widgetY + sy, sw, sh);
// Display
ctx.beginPath();
ctx.arc(LiteGraph.NODE_SLOT_HEIGHT * 0.5, LiteGraph.NODE_SLOT_HEIGHT * (index + 0.5) + 4, 4, 0, Math.PI * 2);
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = "white";
ctx.stroke();
ctx.lineWidth = 1;
ctx.closePath();
// Draw progress bar canvas
if (backgroundWidth < widgetWidth) {
xOffset += (widgetWidth - backgroundWidth) / 2 - margin;
}
// Ajustar las coordenadas X e Y
const barHeight = 8;
let widgetYBar = widgetY + backgroundHeight + margin;
// Dibujar el borde negro alrededor de la barra
ctx.fillStyle = globalThis.LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.fillRect(
widgetX - border,
widgetYBar - border,
backgroundWidth + border * 2,
barHeight + border * 2
);
// Dibujar el área principal de la barra (fondo)
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR; // Mismo color de fondo que el canvas
ctx.fillRect(
widgetX,
widgetYBar,
backgroundWidth,
barHeight
);
// Draw progress bar grid
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "#66666650";
// Calcular el número de líneas en función del tamaño de la barra
const numLines = Math.floor(backgroundWidth / 64);
// Dibujar líneas del grid
for (let x = 0; x <= width / 64; x += 1) {
ctx.moveTo(widgetX + x * 64 * scale, widgetYBar);
ctx.lineTo(widgetX + x * 64 * scale, widgetYBar + barHeight);
}
ctx.stroke();
ctx.closePath();
// Dibujar progreso (basado en blur_radius)
const progress = Math.min(blurRadius / 255, 1);
ctx.fillStyle = "rgba(0, 120, 255, 0.5)";
ctx.fillRect(
widgetX,
widgetYBar,
backgroundWidth * progress,
barHeight
);
}
};
widget.canvas = document.createElement("canvas");
widget.canvas.className = "mask-rect-area-canvas";
widget.parent = node;
document.body.appendChild(widget.canvas);
node.addCustomWidget(widget);
app.canvas.onDrawBackground = function () {
// Draw node isnt fired once the node is off the screen
// if it goes off screen quickly, the input may not be removed
// this shifts it off screen so it can be moved back if the node is visible.
for (let n in app.graph._nodes) {
n = app.graph._nodes[n];
for (let w in n.widgets) {
let wid = n.widgets[w];
if (Object.hasOwn(wid, "canvas")) {
wid.canvas.style.left = -8000 + "px";
wid.canvas.style.position = "absolute";
}
}
}
};
node.onResize = function (size) {
computeCanvasSize(node, size);
};
return {minWidth: 200, minHeight: 200, widget};
}
app.registerExtension({
name: 'drltdata.MaskRectAreaAdvanced',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MaskRectAreaAdvanced") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
this.setProperty("width", 512);
this.setProperty("height", 512);
this.setProperty("x", 0);
this.setProperty("y", 0);
this.setProperty("w", 256);
this.setProperty("h", 256);
this.setProperty("blur_radius", 0);
this.selected = false;
this.index = 3;
this.serialize_widgets = true;
CUSTOM_INT(this, "x", 0, function (v, _, node) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
node.properties["x"] = this.value;
});
CUSTOM_INT(this, "y", 0, function (v, _, node) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
node.properties["y"] = this.value;
});
CUSTOM_INT(this, "width", 256, function (v, _, node) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
node.properties["w"] = this.value;
});
CUSTOM_INT(this, "height", 256, function (v, _, node) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
node.properties["h"] = this.value;
});
CUSTOM_INT(this, "image_width", 512, function (v, _, node) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
node.properties["width"] = this.value;
});
CUSTOM_INT(this, "image_height", 512, function (v, _, node) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
node.properties["height"] = this.value;
});
CUSTOM_INT(this, "blur_radius", 0, function (v, _, node) {
this.value = Math.round(v) || 0;
node.properties["blur_radius"] = this.value;
},
{"min": 0, "max": 255, "step": 10}
);
showPreviewCanvas(this, app);
this.onSelected = function () {
this.selected = true;
};
this.onDeselected = function () {
this.selected = false;
};
return r;
};
}
}
});
// Calculate the drawing area using individual properties.
function getDrawArea(node, backgroundWidth, backgroundHeight) {
let x = node.properties["x"] * backgroundWidth / node.properties["width"];
let y = node.properties["y"] * backgroundHeight / node.properties["height"];
let w = node.properties["w"] * backgroundWidth / node.properties["width"];
let h = node.properties["h"] * backgroundHeight / node.properties["height"];
if (x > backgroundWidth) {
x = backgroundWidth;
}
if (y > backgroundHeight) {
y = backgroundHeight;
}
if (x + w > backgroundWidth) {
w = Math.max(0, backgroundWidth - x);
}
if (y + h > backgroundHeight) {
h = Math.max(0, backgroundHeight - y);
}
return [x, y, w, h];
}
function CUSTOM_INT(node, inputName, val, func, config = {}) {
return {
widget: node.addWidget(
"number",
inputName,
val,
func,
Object.assign({}, {min: 0, max: 4096, step: 640, precision: 0}, config)
)
};
}
function getDrawColor(percent, alpha) {
let h = 360 * percent;
let s = 50;
let l = 50;
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
};
return `#${f(0)}${f(8)}${f(4)}${alpha}`;
}
function computeCanvasSize(node, size) {
if (node.widgets[0].last_y == null) {
return;
}
const MIN_HEIGHT = 220;
const MIN_WIDTH = 240;
let y = LiteGraph.NODE_WIDGET_HEIGHT * Math.max(node.inputs.length, node.outputs.length) + 5;
let freeSpace = size[1] - y;
// Compute the height of all non-customCanvas widgets
let widgetHeight = 0;
for (let i = 0; i < node.widgets.length; i++) {
const w = node.widgets[i];
if (w.type !== "customCanvas") {
if (w.computeSize) {
widgetHeight += w.computeSize()[1] + 4;
} else {
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 5;
}
}
}
// Ensure there is enough vertical space
freeSpace -= widgetHeight;
// Adjust the height of the node if needed
if (freeSpace < MIN_HEIGHT) {
freeSpace = MIN_HEIGHT;
node.size[1] = y + widgetHeight + freeSpace;
node.graph.setDirtyCanvas(true);
}
// Ensure the node width meets the minimum width requirement
if (node.size[0] < MIN_WIDTH) {
node.size[0] = MIN_WIDTH;
node.graph.setDirtyCanvas(true);
}
// Position each of the widgets
for (const w of node.widgets) {
w.y = y;
if (w.type === "customCanvas") {
y += freeSpace;
} else if (w.computeSize) {
y += w.computeSize()[1] + 4;
} else {
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
node.canvasHeight = freeSpace;
}
+366
View File
@@ -0,0 +1,366 @@
import { app } from "/scripts/app.js";
function showPreviewCanvas(node, app) {
const widget = {
type: "customCanvas",
name: "mask-rect-area-canvas",
get value() {
return this.canvas.value;
},
set value(x) {
this.canvas.value = x;
},
draw: function (ctx, node, widgetWidth, widgetY) {
// If we are initially offscreen when created we wont have received a resize event
// Calculate it here instead
if (!node.canvasHeight) {
computeCanvasSize(node, node.size);
}
const visible = true;
const t = ctx.getTransform();
const margin = 12;
const border = 2;
const widgetHeight = node.canvasHeight;
const width = 512;
const height = 512;
const scale = Math.min((widgetWidth - margin * 3) / width, (widgetHeight - margin * 3) / height);
const blurRadius = node.properties["blur_radius"] || 0;
const index = 0;
Object.assign(this.canvas.style, {
left: `${t.e}px`,
top: `${t.f + (widgetY * t.d)}px`,
width: `${widgetWidth * t.a}px`,
height: `${widgetHeight * t.d}px`,
position: "absolute",
zIndex: 1,
fontSize: `${t.d * 10.0}px`,
pointerEvents: "none"
});
this.canvas.hidden = !visible;
let backgroundWidth = width * scale;
let backgroundHeight = height * scale;
let xOffset = margin;
if (backgroundWidth < widgetWidth) {
xOffset += (widgetWidth - backgroundWidth) / 2 - margin;
}
let yOffset = (margin / 2);
if (backgroundHeight < widgetHeight) {
yOffset += (widgetHeight - backgroundHeight) / 2 - margin;
}
let widgetX = xOffset;
widgetY = widgetY + yOffset;
// Draw the background border
ctx.fillStyle = globalThis.LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.fillRect(widgetX - border, widgetY - border, backgroundWidth + border * 2, backgroundHeight + border * 2);
// Draw the main background area
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR;
ctx.fillRect(widgetX, widgetY, backgroundWidth, backgroundHeight);
// Draw the conditioning zone
let [x, y, w, h] = getDrawArea(node, backgroundWidth, backgroundHeight);
ctx.fillStyle = getDrawColor(0, "80");
ctx.fillRect(widgetX + x, widgetY + y, w, h);
ctx.beginPath();
ctx.lineWidth = 1;
// Draw grid lines
for (let x = 0; x <= width / 64; x += 1) {
ctx.moveTo(widgetX + x * 64 * scale, widgetY);
ctx.lineTo(widgetX + x * 64 * scale, widgetY + backgroundHeight);
}
for (let y = 0; y <= height / 64; y += 1) {
ctx.moveTo(widgetX, widgetY + y * 64 * scale);
ctx.lineTo(widgetX + backgroundWidth, widgetY + y * 64 * scale);
}
ctx.strokeStyle = "#66666650";
ctx.stroke();
ctx.closePath();
// Draw current zone
let [sx, sy, sw, sh] = getDrawArea(node, backgroundWidth, backgroundHeight);
ctx.fillStyle = getDrawColor(0, "80");
ctx.fillRect(widgetX + sx, widgetY + sy, sw, sh);
ctx.fillStyle = getDrawColor(0, "40");
ctx.fillRect(widgetX + sx + border, widgetY + sy + border, sw - border * 2, sh - border * 2);
// Draw white border around the current zone
ctx.strokeStyle = globalThis.LiteGraph.NODE_SELECTED_TITLE_COLOR;
ctx.lineWidth = 2;
ctx.strokeRect(widgetX + sx, widgetY + sy, sw, sh);
//ctx.strokeRect(finalSX, finalSY, finalSW, finalSH);
// Display
ctx.beginPath();
ctx.arc(LiteGraph.NODE_SLOT_HEIGHT * 0.5, LiteGraph.NODE_SLOT_HEIGHT * (index + 0.5) + 4, 4, 0, Math.PI * 2);
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = "white";
ctx.stroke();
ctx.lineWidth = 1;
ctx.closePath();
// Draw progress bar canvas
if (backgroundWidth < widgetWidth) {
xOffset += (widgetWidth - backgroundWidth) / 2 - margin;
}
const barHeight = 8;
let widgetYBar = widgetY + backgroundHeight + margin;
// Draw progress bar border
ctx.fillStyle = globalThis.LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.fillRect(
widgetX - border,
widgetYBar - border,
backgroundWidth + border * 2,
barHeight + border * 2
);
// Draw progress bar area
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR; // Mismo color de fondo que el canvas
ctx.fillRect(
widgetX,
widgetYBar,
backgroundWidth,
barHeight
);
// Draw progress bar grid
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "#66666650";
// Determine max lines
const numLines = Math.floor(backgroundWidth / 64);
// Draw progress bar grid
for (let x = 0; x <= width / 64; x += 1) {
ctx.moveTo(widgetX + x * 64 * scale, widgetYBar);
ctx.lineTo(widgetX + x * 64 * scale, widgetYBar + barHeight);
}
ctx.stroke();
ctx.closePath();
// Draw progress bar
const progress = Math.min(blurRadius / 255, 1);
ctx.fillStyle = "rgba(0, 120, 255, 0.5)";
ctx.fillRect(
widgetX,
widgetYBar,
backgroundWidth * progress,
barHeight
);
}
};
widget.canvas = document.createElement("canvas");
widget.canvas.className = "mask-rect-area-canvas";
widget.parent = node;
document.body.appendChild(widget.canvas);
node.addCustomWidget(widget);
app.canvas.onDrawBackground = function () {
// Draw node isnt fired once the node is off the screen
// if it goes off screen quickly, the input may not be removed
// this shifts it off screen so it can be moved back if the node is visible.
for (let n in app.graph._nodes) {
n = app.graph._nodes[n];
for (let w in n.widgets) {
let wid = n.widgets[w];
if (Object.hasOwn(wid, "canvas")) {
wid.canvas.style.left = -8000 + "px";
wid.canvas.style.position = "absolute";
}
}
}
};
node.onResize = function (size) {
computeCanvasSize(node, size);
};
return {minWidth: 200, minHeight: 200, widget};
}
app.registerExtension({
name: 'drltdata.MaskRectArea',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MaskRectArea") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
this.setProperty("width", 512);
this.setProperty("height", 512);
this.setProperty("x", 0);
this.setProperty("y", 0);
this.setProperty("w", 50);
this.setProperty("h", 50);
this.setProperty("blur_radius", 0);
this.selected = false;
this.index = 3;
this.serialize_widgets = true;
CUSTOM_INT(this, "x", 0, function (v, _, node) {
this.value = Math.max(0, Math.min(100, Math.round(v))); // Limitar entre 0 y 100
node.properties["x"] = this.value;
});
CUSTOM_INT(this, "y", 0, function (v, _, node) {
this.value = Math.max(0, Math.min(100, Math.round(v)));
node.properties["y"] = this.value;
});
CUSTOM_INT(this, "w", 50, function (v, _, node) {
this.value = Math.max(0, Math.min(100, Math.round(v)));
node.properties["w"] = this.value;
});
CUSTOM_INT(this, "h", 50, function (v, _, node) {
this.value = Math.max(0, Math.min(100, Math.round(v)));
node.properties["h"] = this.value;
});
CUSTOM_INT(this, "blur_radius", 0, function (v, _, node) {
this.value = Math.round(v) || 0;
node.properties["blur_radius"] = this.value;
},
{"min": 0, "max": 255, "step": 10}
);
showPreviewCanvas(this, app);
this.onSelected = function () {
this.selected = true;
};
this.onDeselected = function () {
this.selected = false;
};
return r;
};
}
}
});
// Calculate the drawing area using percentage-based properties.
function getDrawArea(node, backgroundWidth, backgroundHeight) {
// Convert percentages to actual pixel values based on the background dimensions
let x = (node.properties["x"] / 100) * backgroundWidth;
let y = (node.properties["y"] / 100) * backgroundHeight;
let w = (node.properties["w"] / 100) * backgroundWidth;
let h = (node.properties["h"] / 100) * backgroundHeight;
// Ensure the values do not exceed the background boundaries
if (x > backgroundWidth) {
x = backgroundWidth;
}
if (y > backgroundHeight) {
y = backgroundHeight;
}
// Adjust width and height to fit within the background dimensions
if (x + w > backgroundWidth) {
w = Math.max(0, backgroundWidth - x);
}
if (y + h > backgroundHeight) {
h = Math.max(0, backgroundHeight - y);
}
return [x, y, w, h];
}
function CUSTOM_INT(node, inputName, val, func, config = {}) {
return {
widget: node.addWidget(
"number",
inputName,
val,
func,
Object.assign({}, {min: 0, max: 100, step: 10, precision: 0}, config)
)
};
}
function getDrawColor(percent, alpha) {
let h = 360 * percent;
let s = 50;
let l = 50;
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
};
return `#${f(0)}${f(8)}${f(4)}${alpha}`;
}
function computeCanvasSize(node, size) {
if (node.widgets[0].last_y == null) {
return;
}
const MIN_HEIGHT = 200;
const MIN_WIDTH = 200;
let y = LiteGraph.NODE_WIDGET_HEIGHT * Math.max(node.inputs.length, node.outputs.length) + 5;
let freeSpace = size[1] - y;
// Compute the height of all non-customCanvas widgets
let widgetHeight = 0;
for (let i = 0; i < node.widgets.length; i++) {
const w = node.widgets[i];
if (w.type !== "customCanvas") {
if (w.computeSize) {
widgetHeight += w.computeSize()[1] + 4;
} else {
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 5;
}
}
}
// Ensure there is enough vertical space
freeSpace -= widgetHeight;
// Adjust the height of the node if needed
if (freeSpace < MIN_HEIGHT) {
freeSpace = MIN_HEIGHT;
node.size[1] = y + widgetHeight + freeSpace;
node.graph.setDirtyCanvas(true);
}
// Ensure the node width meets the minimum width requirement
if (node.size[0] < MIN_WIDTH) {
node.size[0] = MIN_WIDTH;
node.graph.setDirtyCanvas(true);
}
// Position each of the widgets
for (const w of node.widgets) {
w.y = y;
if (w.type === "customCanvas") {
y += freeSpace;
} else if (w.computeSize) {
y += w.computeSize()[1] + 4;
} else {
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
node.canvasHeight = freeSpace;
}
+15 -10
View File
@@ -27,7 +27,7 @@ class SEGSDetailerForAnimateDiff:
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (core.SCHEDULERS,),
"denoise": ("FLOAT", {"default": 0.5, "min": 0.0001, "max": 1.0, "step": 0.01}),
"basic_pipe": ("BASIC_PIPE",),
"basic_pipe": ("BASIC_PIPE", {"tooltip": "If the `ImpactDummyInput` is connected to the model in the basic_pipe, the inference stage is skipped."}),
"refiner_ratio": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0}),
},
"optional": {
@@ -60,7 +60,7 @@ class SEGSDetailerForAnimateDiff:
new_segs = []
cnet_image_list = []
if noise_mask_feather > 0 and 'denoise_mask_function' not in model.model_options:
if not (isinstance(model, str) and model == "DUMMY") and noise_mask_feather > 0 and 'denoise_mask_function' not in model.model_options:
model = nodes_differential_diffusion.DifferentialDiffusion().apply(model)[0]
for seg in segs[1]:
@@ -94,13 +94,18 @@ class SEGSDetailerForAnimateDiff:
for condition, details in negative
]
enhanced_image_tensor, cnet_images = core.enhance_detail_for_animatediff(cropped_image_frames, model, clip, vae, guide_size, guide_size_for, max_size,
seg.bbox, seed, steps, cfg, sampler_name, scheduler,
cropped_positive, cropped_negative, denoise, seg.cropped_mask,
refiner_ratio=refiner_ratio, refiner_model=refiner_model,
refiner_clip=refiner_clip, refiner_positive=refiner_positive,
refiner_negative=refiner_negative, control_net_wrapper=seg.control_net_wrapper,
noise_mask_feather=noise_mask_feather, scheduler_func=scheduler_func_opt)
if not (isinstance(model, str) and model == "DUMMY"):
enhanced_image_tensor, cnet_images = core.enhance_detail_for_animatediff(cropped_image_frames, model, clip, vae, guide_size, guide_size_for, max_size,
seg.bbox, seed, steps, cfg, sampler_name, scheduler,
cropped_positive, cropped_negative, denoise, seg.cropped_mask,
refiner_ratio=refiner_ratio, refiner_model=refiner_model,
refiner_clip=refiner_clip, refiner_positive=refiner_positive,
refiner_negative=refiner_negative, control_net_wrapper=seg.control_net_wrapper,
noise_mask_feather=noise_mask_feather, scheduler_func=scheduler_func_opt)
else:
enhanced_image_tensor = cropped_image_frames
cnet_images = None
if cnet_images is not None:
cnet_image_list.extend(cnet_images)
@@ -143,7 +148,7 @@ class DetailerForEachPipeForAnimateDiff:
"scheduler": (core.SCHEDULERS,),
"denoise": ("FLOAT", {"default": 0.5, "min": 0.0001, "max": 1.0, "step": 0.01}),
"feather": ("INT", {"default": 5, "min": 0, "max": 100, "step": 1}),
"basic_pipe": ("BASIC_PIPE", ),
"basic_pipe": ("BASIC_PIPE", {"tooltip": "If the `ImpactDummyInput` is connected to the model in the basic_pipe, the inference stage is skipped."}),
"refiner_ratio": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0}),
},
"optional": {
+1 -1
View File
@@ -1,7 +1,7 @@
import configparser
import os
version_code = [8, 0, 1]
version_code = [8, 5, 1]
version = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
dependency_version = 24
+44 -19
View File
@@ -244,7 +244,8 @@ def enhance_detail(image, model, clip, vae, guide_size, guide_size_for_bbox, max
detailer_hook=None,
refiner_ratio=None, refiner_model=None, refiner_clip=None, refiner_positive=None,
refiner_negative=None, control_net_wrapper=None, cycle=1,
inpaint_model=False, noise_mask_feather=0, scheduler_func=None):
inpaint_model=False, noise_mask_feather=0, scheduler_func=None,
vae_tiled_encode=False, vae_tiled_decode=False):
if noise_mask is not None:
noise_mask = utils.tensor_gaussian_blur_mask(noise_mask, noise_mask_feather)
@@ -334,7 +335,7 @@ def enhance_detail(image, model, clip, vae, guide_size, guide_size_for_bbox, max
print(f"[Impact Pack] ComfyUI is an outdated version.")
positive, negative, latent_image = imc_encode(positive, negative, upscaled_image, vae, noise_mask)
else:
latent_image = to_latent_image(upscaled_image, vae)
latent_image = to_latent_image(upscaled_image, vae, vae_tiled_encode=vae_tiled_encode)
if noise_mask is not None:
latent_image['noise_mask'] = noise_mask
@@ -369,12 +370,18 @@ def enhance_detail(image, model, clip, vae, guide_size, guide_size_for_bbox, max
refined_latent = detailer_hook.pre_decode(refined_latent)
# non-latent downscale - latent downscale cause bad quality
try:
# try to decode image normally
refined_image = vae.decode(refined_latent['samples'])
except Exception as e:
#usually an out-of-memory exception from the decode, so try a tiled approach
refined_image = vae.decode_tiled(refined_latent["samples"], tile_x=64, tile_y=64, )
start = time.time()
if vae_tiled_decode:
(refined_image,) = nodes.VAEDecodeTiled().decode(vae, refined_latent, 512) # using default settings
print(f"[Impact Pack] vae decoded (tiled) in {time.time() - start:.1f}s")
else:
try:
refined_image = vae.decode(refined_latent['samples'])
except Exception as e:
# usually an out-of-memory exception from the decode, so try a tiled approach
print(f"[Impact Pack] failed after {time.time() - start:.1f}s, doing vae.decode_tiled 64...")
refined_image = vae.decode_tiled(refined_latent["samples"], tile_x=64, tile_y=64, )
print(f"[Impact Pack] vae decoded in {time.time() - start:.1f}s")
if detailer_hook is not None:
refined_image = detailer_hook.post_decode(refined_image)
@@ -1384,9 +1391,14 @@ def vae_decode(vae, samples, use_tile, hook, tile_size=512, overlap=64):
return pixels
def vae_encode(vae, pixels, use_tile, hook, tile_size=512):
def vae_encode(vae, pixels, use_tile, hook, tile_size=512, overlap=64):
if use_tile:
samples = nodes.VAEEncodeTiled().encode(vae, pixels, tile_size)[0]
encoder = nodes.VAEEncodeTiled()
if 'overlap' in inspect.signature(encoder.encode).parameters:
samples = encoder.encode(vae, pixels, tile_size, overlap=overlap)[0]
else:
print(f"[Impact Pack] Your ComfyUI is outdated.")
samples = encoder.encode(vae, pixels, tile_size)[0]
else:
samples = nodes.VAEEncode().encode(vae, pixels)[0]
@@ -1412,7 +1424,7 @@ def latent_upscale_on_pixel_space_shape2(samples, scale_method, w, h, vae, use_t
if hook is not None:
pixels = hook.post_upscale(pixels)
return (vae_encode(vae, pixels, use_tile, hook, tile_size=tile_size), old_pixels)
return vae_encode(vae, pixels, use_tile, hook, tile_size=tile_size, overlap=overlap), old_pixels
def latent_upscale_on_pixel_space(samples, scale_method, scale_factor, vae, use_tile=False, tile_size=512, save_temp_prefix=None, hook=None, overlap=64):
@@ -1433,7 +1445,7 @@ def latent_upscale_on_pixel_space2(samples, scale_method, scale_factor, vae, use
if hook is not None:
pixels = hook.post_upscale(pixels)
return (vae_encode(vae, pixels, use_tile, hook, tile_size=tile_size), old_pixels)
return vae_encode(vae, pixels, use_tile, hook, tile_size=tile_size, overlap=overlap), old_pixels
def latent_upscale_on_pixel_space_with_model_shape(samples, scale_method, upscale_model, new_w, new_h, vae, use_tile=False, tile_size=512, save_temp_prefix=None, hook=None, overlap=64):
@@ -1464,7 +1476,7 @@ def latent_upscale_on_pixel_space_with_model_shape2(samples, scale_method, upsca
if hook is not None:
pixels = hook.post_upscale(pixels)
return (vae_encode(vae, pixels, use_tile, hook, tile_size=tile_size), old_pixels)
return vae_encode(vae, pixels, use_tile, hook, tile_size=tile_size, overlap=overlap), old_pixels
def latent_upscale_on_pixel_space_with_model(samples, scale_method, upscale_model, scale_factor, vae, use_tile=False,
@@ -1500,7 +1512,7 @@ def latent_upscale_on_pixel_space_with_model2(samples, scale_method, upscale_mod
if hook is not None:
pixels = hook.post_upscale(pixels)
return (vae_encode(vae, pixels, use_tile, hook, tile_size=tile_size), old_pixels)
return vae_encode(vae, pixels, use_tile, hook, tile_size=tile_size, overlap=overlap), old_pixels
class TwoSamplersForMaskUpscaler:
@@ -1670,8 +1682,14 @@ class PixelKSampleUpscaler:
preprocessor = nodes.NODE_CLASS_MAPPINGS['TilePreprocessor']()
# might add capacity to set pyrUp_iters later, not needed for now though
preprocessed = preprocessor.execute(images, pyrUp_iters=3, resolution=min(image_w, image_h))[0]
apply_cnet = getattr(nodes.ControlNetApply(), nodes.ControlNetApply.FUNCTION)
positive = apply_cnet(positive, self.tile_cnet, preprocessed, strength=self.tile_cnet_strength)[0]
positive, negative = nodes.ControlNetApplyAdvanced().apply_controlnet(positive=positive,
negative=negative,
control_net=self.tile_cnet,
image=preprocessed,
strength=self.tile_cnet_strength,
start_percent=0,
end_percent=1.0,
vae=self.vae)
refined_latent = impact_sampling.impact_sample(model, seed, steps, cfg, sampler_name, scheduler,
positive, negative, upscaled_latent, denoise, scheduler_func=self.scheduler_func)
@@ -1942,7 +1960,7 @@ class PixelTiledKSampleUpscaler:
def __init__(self, scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative,
denoise,
tile_width, tile_height, tiling_strategy,
upscale_model_opt=None, hook_opt=None, tile_cnet_opt=None, tile_size=512, tile_cnet_strength=1.0):
upscale_model_opt=None, hook_opt=None, tile_cnet_opt=None, tile_size=512, tile_cnet_strength=1.0, overlap=64):
self.params = scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise
self.vae = vae
self.tile_params = tile_width, tile_height, tiling_strategy
@@ -1952,6 +1970,7 @@ class PixelTiledKSampleUpscaler:
self.tile_size = tile_size
self.is_tiled = True
self.tile_cnet_strength = tile_cnet_strength
self.overlap = overlap
def tiled_ksample(self, latent, images):
if "BNK_TiledKSampler" in nodes.NODE_CLASS_MAPPINGS:
@@ -1974,8 +1993,14 @@ class PixelTiledKSampleUpscaler:
preprocessor = nodes.NODE_CLASS_MAPPINGS['TilePreprocessor']()
# might add capacity to set pyrUp_iters later, not needed for now though
preprocessed = preprocessor.execute(images, pyrUp_iters=3, resolution=min(image_w, image_h))[0]
apply_cnet = getattr(nodes.ControlNetApply(), nodes.ControlNetApply.FUNCTION)
positive = apply_cnet(positive, self.tile_cnet, preprocessed, strength=self.tile_cnet_strength)[0]
positive, negative = nodes.ControlNetApplyAdvanced().apply_controlnet(positive=positive,
negative=negative,
control_net=self.tile_cnet,
image=preprocessed,
strength=self.tile_cnet_strength,
start_percent=0, end_percent=1.0,
vae=self.vae)
return TiledKSampler().sample(model, seed, tile_width, tile_height, tiling_strategy, steps, cfg, sampler_name,
scheduler, positive, negative, latent, denoise)[0]
+214 -45
View File
@@ -28,6 +28,7 @@ import base64
import impact.wildcards as wildcards
from . import hooks
from . import utils
import inspect
try:
@@ -191,7 +192,7 @@ class DetailerForEach:
return {"required": {
"image": ("IMAGE", ),
"segs": ("SEGS", ),
"model": ("MODEL",),
"model": ("MODEL", {"tooltip": "If the `ImpactDummyInput` is connected to the model, the inference stage is skipped."}),
"clip": ("CLIP",),
"vae": ("VAE",),
"guide_size": ("FLOAT", {"default": 512, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
@@ -217,6 +218,8 @@ class DetailerForEach:
"inpaint_model": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
"noise_mask_feather": ("INT", {"default": 20, "min": 0, "max": 100, "step": 1}),
"scheduler_func_opt": ("SCHEDULER_FUNC",),
"tiled_encode": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
"tiled_decode": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
}
}
@@ -233,7 +236,7 @@ class DetailerForEach:
def do_detail(image, segs, model, clip, vae, guide_size, guide_size_for_bbox, max_size, seed, steps, cfg, sampler_name, scheduler,
positive, negative, denoise, feather, noise_mask, force_inpaint, wildcard_opt=None, detailer_hook=None,
refiner_ratio=None, refiner_model=None, refiner_clip=None, refiner_positive=None, refiner_negative=None,
cycle=1, inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None):
cycle=1, inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None, tiled_encode=False, tiled_decode=False):
if len(image) > 1:
raise Exception('[Impact Pack] ERROR: DetailerForEach does not allow image batches.\nPlease refer to https://github.com/ltdrdata/ComfyUI-extension-tutorials/blob/Main/ComfyUI-Impact-Pack/tutorial/batching-detailer.md for more information.')
@@ -269,7 +272,7 @@ class DetailerForEach:
else:
ordered_segs = segs[1]
if noise_mask_feather > 0 and 'denoise_mask_function' not in model.model_options:
if not (isinstance(model, str) and model == "DUMMY") and noise_mask_feather > 0 and 'denoise_mask_function' not in model.model_options:
model = nodes_differential_diffusion.DifferentialDiffusion().apply(model)[0]
for i, seg in enumerate(ordered_segs):
@@ -297,13 +300,16 @@ class DetailerForEach:
seg_seed = seed + i if seg_seed is None else seg_seed
cropped_positive = [
[condition, {
k: core.crop_condition_mask(v, image, seg.crop_region) if k == "mask" else v
for k, v in details.items()
}]
for condition, details in positive
]
if not isinstance(positive, str):
cropped_positive = [
[condition, {
k: core.crop_condition_mask(v, image, seg.crop_region) if k == "mask" else v
for k, v in details.items()
}]
for condition, details in positive
]
else:
cropped_positive = positive
if not isinstance(negative, str):
cropped_negative = [
@@ -324,16 +330,21 @@ class DetailerForEach:
break
orig_cropped_image = cropped_image.clone()
enhanced_image, cnet_pils = core.enhance_detail(cropped_image, model, clip, vae, guide_size, guide_size_for_bbox, max_size,
seg.bbox, seg_seed, steps, cfg, sampler_name, scheduler,
cropped_positive, cropped_negative, denoise, cropped_mask, force_inpaint,
wildcard_opt=wildcard_item, wildcard_opt_concat_mode=wildcard_concat_mode,
detailer_hook=detailer_hook,
refiner_ratio=refiner_ratio, refiner_model=refiner_model,
refiner_clip=refiner_clip, refiner_positive=refiner_positive,
refiner_negative=refiner_negative, control_net_wrapper=seg.control_net_wrapper,
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather,
scheduler_func=scheduler_func_opt)
if not (isinstance(model, str) and model == "DUMMY"):
enhanced_image, cnet_pils = core.enhance_detail(cropped_image, model, clip, vae, guide_size, guide_size_for_bbox, max_size,
seg.bbox, seg_seed, steps, cfg, sampler_name, scheduler,
cropped_positive, cropped_negative, denoise, cropped_mask, force_inpaint,
wildcard_opt=wildcard_item, wildcard_opt_concat_mode=wildcard_concat_mode,
detailer_hook=detailer_hook,
refiner_ratio=refiner_ratio, refiner_model=refiner_model,
refiner_clip=refiner_clip, refiner_positive=refiner_positive,
refiner_negative=refiner_negative, control_net_wrapper=seg.control_net_wrapper,
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather,
scheduler_func=scheduler_func_opt, vae_tiled_encode=tiled_encode,
vae_tiled_decode=tiled_decode)
else:
enhanced_image = cropped_image
cnet_pils = None
if cnet_pils is not None:
cnet_pil_list.extend(cnet_pils)
@@ -376,13 +387,15 @@ class DetailerForEach:
def doit(self, image, segs, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name,
scheduler, positive, negative, denoise, feather, noise_mask, force_inpaint, wildcard, cycle=1,
detailer_hook=None, inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None):
detailer_hook=None, inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None,
tiled_encode=False, tiled_decode=False):
enhanced_img, *_ = \
DetailerForEach.do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps,
cfg, sampler_name, scheduler, positive, negative, denoise, feather, noise_mask,
force_inpaint, wildcard, detailer_hook,
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func_opt=scheduler_func_opt)
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather,
scheduler_func_opt=scheduler_func_opt, tiled_encode=tiled_encode, tiled_decode=tiled_decode)
return (enhanced_img, )
@@ -405,7 +418,7 @@ class DetailerForEachPipe:
"feather": ("INT", {"default": 5, "min": 0, "max": 100, "step": 1}),
"noise_mask": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
"force_inpaint": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
"basic_pipe": ("BASIC_PIPE", ),
"basic_pipe": ("BASIC_PIPE", {"tooltip": "If the `ImpactDummyInput` is connected to the model in the basic_pipe, the inference stage is skipped."}),
"wildcard": ("STRING", {"multiline": True, "dynamicPrompts": False}),
"refiner_ratio": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0}),
@@ -417,6 +430,8 @@ class DetailerForEachPipe:
"inpaint_model": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
"noise_mask_feather": ("INT", {"default": 20, "min": 0, "max": 100, "step": 1}),
"scheduler_func_opt": ("SCHEDULER_FUNC",),
"tiled_encode": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
"tiled_decode": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
}
}
@@ -430,7 +445,8 @@ class DetailerForEachPipe:
def doit(self, image, segs, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
denoise, feather, noise_mask, force_inpaint, basic_pipe, wildcard,
refiner_ratio=None, detailer_hook=None, refiner_basic_pipe_opt=None,
cycle=1, inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None):
cycle=1, inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None,
tiled_encode=False, tiled_decode=False):
if len(image) > 1:
raise Exception('[Impact Pack] ERROR: DetailerForEach does not allow image batches.\nPlease refer to https://github.com/ltdrdata/ComfyUI-extension-tutorials/blob/Main/ComfyUI-Impact-Pack/tutorial/batching-detailer.md for more information.')
@@ -448,7 +464,8 @@ class DetailerForEachPipe:
force_inpaint, wildcard, detailer_hook,
refiner_ratio=refiner_ratio, refiner_model=refiner_model,
refiner_clip=refiner_clip, refiner_positive=refiner_positive, refiner_negative=refiner_negative,
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func_opt=scheduler_func_opt)
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func_opt=scheduler_func_opt,
tiled_encode=tiled_encode, tiled_decode=tiled_decode)
# set fallback image
if len(cnet_pil_list) == 0:
@@ -462,7 +479,7 @@ class FaceDetailer:
def INPUT_TYPES(s):
return {"required": {
"image": ("IMAGE", ),
"model": ("MODEL",),
"model": ("MODEL", {"tooltip": "If the `ImpactDummyInput` is connected to the model, the inference stage is skipped."}),
"clip": ("CLIP",),
"vae": ("VAE",),
"guide_size": ("FLOAT", {"default": 512, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
@@ -505,6 +522,8 @@ class FaceDetailer:
"inpaint_model": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
"noise_mask_feather": ("INT", {"default": 20, "min": 0, "max": 100, "step": 1}),
"scheduler_func_opt": ("SCHEDULER_FUNC",),
"tiled_encode": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
"tiled_decode": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
}}
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "MASK", "DETAILER_PIPE", "IMAGE")
@@ -522,7 +541,7 @@ class FaceDetailer:
sam_mask_hint_use_negative, drop_size,
bbox_detector, segm_detector=None, sam_model_opt=None, wildcard_opt=None, detailer_hook=None,
refiner_ratio=None, refiner_model=None, refiner_clip=None, refiner_positive=None, refiner_negative=None, cycle=1,
inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None):
inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None, tiled_encode=False, tiled_decode=False):
# make default prompt as 'face' if empty prompt for CLIPSeg
bbox_detector.setAux('face')
@@ -554,7 +573,8 @@ class FaceDetailer:
refiner_ratio=refiner_ratio, refiner_model=refiner_model,
refiner_clip=refiner_clip, refiner_positive=refiner_positive,
refiner_negative=refiner_negative,
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func_opt=scheduler_func_opt)
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather,
scheduler_func_opt=scheduler_func_opt, tiled_encode=tiled_encode, tiled_decode=tiled_decode)
else:
enhanced_img = image
cropped_enhanced = []
@@ -580,7 +600,8 @@ class FaceDetailer:
bbox_threshold, bbox_dilation, bbox_crop_factor,
sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold,
sam_mask_hint_use_negative, drop_size, bbox_detector, wildcard, cycle=1,
sam_model_opt=None, segm_detector_opt=None, detailer_hook=None, inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None):
sam_model_opt=None, segm_detector_opt=None, detailer_hook=None, inpaint_model=False, noise_mask_feather=0,
scheduler_func_opt=None, tiled_encode=False, tiled_decode=False):
result_img = None
result_mask = None
@@ -598,7 +619,8 @@ class FaceDetailer:
bbox_threshold, bbox_dilation, bbox_crop_factor,
sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold,
sam_mask_hint_use_negative, drop_size, bbox_detector, segm_detector_opt, sam_model_opt, wildcard, detailer_hook,
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func_opt=scheduler_func_opt)
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func_opt=scheduler_func_opt,
tiled_encode=tiled_encode, tiled_decode=tiled_decode)
result_img = torch.cat((result_img, enhanced_img), dim=0) if result_img is not None else enhanced_img
result_mask = torch.cat((result_mask, mask), dim=0) if result_mask is not None else mask
@@ -986,6 +1008,7 @@ class PixelTiledKSampleUpscalerProvider:
"pk_hook_opt": ("PK_HOOK", ),
"tile_cnet_opt": ("CONTROL_NET", ),
"tile_cnet_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"overlap": ("INT", {"default": 64, "min": 0, "max": 4096, "step": 32}),
}
}
@@ -995,11 +1018,11 @@ class PixelTiledKSampleUpscalerProvider:
CATEGORY = "ImpactPack/Upscale"
def doit(self, scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise, tile_width, tile_height, tiling_strategy, upscale_model_opt=None,
pk_hook_opt=None, tile_cnet_opt=None, tile_cnet_strength=1.0):
pk_hook_opt=None, tile_cnet_opt=None, tile_cnet_strength=1.0, overlap=64):
if "BNK_TiledKSampler" in nodes.NODE_CLASS_MAPPINGS:
upscaler = core.PixelTiledKSampleUpscaler(scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise,
tile_width, tile_height, tiling_strategy, upscale_model_opt, pk_hook_opt, tile_cnet_opt,
tile_size=max(tile_width, tile_height), tile_cnet_strength=tile_cnet_strength)
tile_size=max(tile_width, tile_height), tile_cnet_strength=tile_cnet_strength, overlap=overlap)
return (upscaler, )
else:
utils.try_install_custom_node('https://github.com/BlenderNeko/ComfyUI_TiledKSampler',
@@ -1312,7 +1335,11 @@ class IterativeImageUpscale:
core.update_node_status(unique_id, "VAEEncode (first)", 0)
if upscaler.is_tiled:
latent = nodes.VAEEncodeTiled().encode(vae, pixels, upscaler.tile_size)[0]
encoder = nodes.VAEEncodeTiled()
if 'overlap' in inspect.signature(encoder.encode).parameters:
latent = encoder.encode(vae, pixels, upscaler.tile_size, overlap=upscaler.overlap)[0]
else:
latent = encoder.encode(vae, pixels, upscaler.tile_size)[0]
else:
latent = nodes.VAEEncode().encode(vae, pixels)[0]
@@ -1334,7 +1361,7 @@ class FaceDetailerPipe:
def INPUT_TYPES(s):
return {"required": {
"image": ("IMAGE", ),
"detailer_pipe": ("DETAILER_PIPE",),
"detailer_pipe": ("DETAILER_PIPE", {"tooltip": "If the `ImpactDummyInput` is connected to the model in the detailer_pipe, the inference stage is skipped."}),
"guide_size": ("FLOAT", {"default": 512, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
"guide_size_for": ("BOOLEAN", {"default": True, "label_on": "bbox", "label_off": "crop_region"}),
"max_size": ("FLOAT", {"default": 1024, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
@@ -1368,6 +1395,8 @@ class FaceDetailerPipe:
"inpaint_model": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
"noise_mask_feather": ("INT", {"default": 20, "min": 0, "max": 100, "step": 1}),
"scheduler_func_opt": ("SCHEDULER_FUNC",),
"tiled_encode": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
"tiled_decode": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
}
}
@@ -1382,7 +1411,8 @@ class FaceDetailerPipe:
denoise, feather, noise_mask, force_inpaint, bbox_threshold, bbox_dilation, bbox_crop_factor,
sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion,
sam_mask_hint_threshold, sam_mask_hint_use_negative, drop_size, refiner_ratio=None,
cycle=1, inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None):
cycle=1, inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None,
tiled_encode=False, tiled_decode=False):
result_img = None
result_mask = None
@@ -1405,7 +1435,8 @@ class FaceDetailerPipe:
sam_mask_hint_use_negative, drop_size, bbox_detector, segm_detector, sam_model_opt, wildcard, detailer_hook,
refiner_ratio=refiner_ratio, refiner_model=refiner_model,
refiner_clip=refiner_clip, refiner_positive=refiner_positive, refiner_negative=refiner_negative,
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func_opt=scheduler_func_opt)
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func_opt=scheduler_func_opt,
tiled_encode=tiled_encode, tiled_decode=tiled_decode)
result_img = torch.cat((result_img, enhanced_img), dim=0) if result_img is not None else enhanced_img
result_mask = torch.cat((result_mask, mask), dim=0) if result_mask is not None else mask
@@ -1539,7 +1570,7 @@ class DetailerForEachTest(DetailerForEach):
def doit(self, image, segs, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name,
scheduler, positive, negative, denoise, feather, noise_mask, force_inpaint, wildcard, detailer_hook=None,
cycle=1, inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None):
cycle=1, inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None, tiled_encode=False, tiled_decode=False):
if len(image) > 1:
raise Exception('[Impact Pack] ERROR: DetailerForEach does not allow image batches.\nPlease refer to https://github.com/ltdrdata/ComfyUI-extension-tutorials/blob/Main/ComfyUI-Impact-Pack/tutorial/batching-detailer.md for more information.')
@@ -1548,7 +1579,8 @@ class DetailerForEachTest(DetailerForEach):
DetailerForEach.do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps,
cfg, sampler_name, scheduler, positive, negative, denoise, feather, noise_mask,
force_inpaint, wildcard, detailer_hook,
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func_opt=scheduler_func_opt)
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather,
scheduler_func_opt=scheduler_func_opt, tiled_encode=tiled_encode, tiled_decode=tiled_decode)
# set fallback image
if len(cropped) == 0:
@@ -1577,7 +1609,8 @@ class DetailerForEachTestPipe(DetailerForEachPipe):
def doit(self, image, segs, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
denoise, feather, noise_mask, force_inpaint, basic_pipe, wildcard, cycle=1,
refiner_ratio=None, detailer_hook=None, refiner_basic_pipe_opt=None, inpaint_model=False, noise_mask_feather=0, scheduler_func_opt=None):
refiner_ratio=None, detailer_hook=None, refiner_basic_pipe_opt=None, inpaint_model=False, noise_mask_feather=0,
scheduler_func_opt=None, tiled_encode=False, tiled_decode=False):
if len(image) > 1:
raise Exception('[Impact Pack] ERROR: DetailerForEach does not allow image batches.\nPlease refer to https://github.com/ltdrdata/ComfyUI-extension-tutorials/blob/Main/ComfyUI-Impact-Pack/tutorial/batching-detailer.md for more information.')
@@ -1596,7 +1629,8 @@ class DetailerForEachTestPipe(DetailerForEachPipe):
refiner_ratio=refiner_ratio, refiner_model=refiner_model,
refiner_clip=refiner_clip, refiner_positive=refiner_positive,
refiner_negative=refiner_negative,
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func_opt=scheduler_func_opt)
cycle=cycle, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather,
scheduler_func_opt=scheduler_func_opt, tiled_encode=tiled_encode, tiled_decode=tiled_decode)
# set fallback image
if len(cropped) == 0:
@@ -1834,6 +1868,135 @@ def get_file_item(base_type, path):
}
class MaskRectArea:
# Creates a rectangle mask using percentage.
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
},
"hidden": {"extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"}
}
RETURN_TYPES = ("MASK",)
CATEGORY = "ImpactPack/Operation"
FUNCTION = "create_mask"
def create_mask(self, extra_pnginfo, unique_id, **kwargs):
# search for node
node_found = False
for node in extra_pnginfo["workflow"]["nodes"]:
if node["id"] == int(unique_id):
min_x = node["properties"].get("x", 0) / 100
min_y = node["properties"].get("y", 0) / 100
width = node["properties"].get("w", 0) / 100
height = node["properties"].get("h", 0) / 100
blur_radius = node["properties"].get("blur_radius", 0)
node_found = True
break
if not node_found:
raise ValueError(f"No node found with unique_id {unique_id}.")
# Create a mask with standard resolution (e.g., 512x512)
resolution = 512
mask = torch.zeros((resolution, resolution))
# Calculate pixel coordinates
min_x_px = int(min_x * resolution)
min_y_px = int(min_y * resolution)
max_x_px = int((min_x + width) * resolution)
max_y_px = int((min_y + height) * resolution)
# Draw the rectangle on the mask
mask[min_y_px:max_y_px, min_x_px:max_x_px] = 1
# Apply blur if the radii are greater than 0
if blur_radius > 0:
dx = blur_radius * 2 + 1
dy = blur_radius * 2 + 1
# Convert the mask to a format compatible with OpenCV (numpy array)
mask_np = mask.cpu().numpy().astype("float32")
# Apply Gaussian Blur
blurred_mask = cv2.GaussianBlur(mask_np, (dx, dy), 0)
# Convert back to tensor
mask = torch.from_numpy(blurred_mask)
# Return the mask as a tensor with an additional channel
return (mask.unsqueeze(0),)
class MaskRectAreaAdvanced:
# Creates a rectangle mask using pixels relative to image size.
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
},
"hidden": {"extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"}
}
RETURN_TYPES = ("MASK",)
CATEGORY = "ImpactPack/Operation"
FUNCTION = "create_mask_advanced"
def create_mask_advanced(self, extra_pnginfo, unique_id, **kwargs):
# search for node
node_found = False
for node in extra_pnginfo["workflow"]["nodes"]:
if node["id"] == int(unique_id):
min_x = node["properties"]["x"]
min_y = node["properties"]["y"]
width = node["properties"]["w"]
height = node["properties"]["h"]
image_width = node["properties"]["width"]
image_height = node["properties"]["height"]
blur_radius = node["properties"]["blur_radius"]
node_found = True
break
if not node_found:
raise ValueError(f"No node found with unique_id {unique_id}.")
# Calculate maximum coordinates
max_x = min_x + width
max_y = min_y + height
# Create a mask with the image dimensions
mask = torch.zeros((image_height, image_width))
# Draw the rectangle on the mask
mask[int(min_y):int(max_y), int(min_x):int(max_x)] = 1
# Apply blur if the radii are greater than 0
if blur_radius > 0:
dx = blur_radius * 2 + 1
dy = blur_radius * 2 + 1
# Convert the mask to a format compatible with OpenCV (numpy array)
mask_np = mask.cpu().numpy().astype("float32")
# Apply Gaussian Blur
blurred_mask = cv2.GaussianBlur(mask_np, (dx, dy), 0)
# Convert back to tensor
mask = torch.from_numpy(blurred_mask)
# Return the mask as a tensor with an additional channel
return (mask.unsqueeze(0),)
class ImageReceiver:
@classmethod
def INPUT_TYPES(s):
@@ -2180,7 +2343,11 @@ class ImpactWildcardProcessor:
return {"required": {
"wildcard_text": ("STRING", {"multiline": True, "dynamicPrompts": False, "tooltip": "Enter a prompt using wildcard syntax."}),
"populated_text": ("STRING", {"multiline": True, "dynamicPrompts": False, "tooltip": "The actual value passed during the execution of 'ImpactWildcardProcessor' is what is shown here. The behavior varies slightly depending on the mode. Wildcard syntax can also be used in 'populated_text'."}),
"mode": ("BOOLEAN", {"default": True, "label_on": "Populate", "label_off": "Fixed", "tooltip": "Populate: Before running the workflow, it overwrites the existing value of 'populated_text' with the prompt processed from 'wildcard_text'. In this mode, 'populated_text' cannot be edited.\nFixed: Ignores wildcard_text and keeps 'populated_text' as is. You can edit 'populated_text' in this mode."}),
"mode": (["populate", "fixed", "reproduce"], {"default": "populate", "tooltip":
"populate: Before running the workflow, it overwrites the existing value of 'populated_text' with the prompt processed from 'wildcard_text'. In this mode, 'populated_text' cannot be edited.\n"
"fixed: Ignores wildcard_text and keeps 'populated_text' as is. You can edit 'populated_text' in this mode.\n"
"reproduce: This mode operates as 'fixed' mode only once for reproduction, and then it switches to 'populate' mode."
}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Determines the random seed to be used for wildcard processing."}),
"Select to add Wildcard": (["Select the Wildcard to add to the text"],),
},
@@ -2189,7 +2356,7 @@ class ImpactWildcardProcessor:
CATEGORY = "ImpactPack/Prompt"
DESCRIPTION = ("The 'ImpactWildcardProcessor' processes text prompts written in wildcard syntax and outputs the processed text prompt.\n\n"
"TIP: Before the workflow is executed, the processing result of 'wildcard_text' is displayed in 'populated_text', and the populated text is saved along with the workflow. If you want to use a seed converted as input, write the prompt directly in 'populated_text' instead of 'wildcard_text', and set the mode to 'Fixed'.")
"TIP: Before the workflow is executed, the processing result of 'wildcard_text' is displayed in 'populated_text', and the populated text is saved along with the workflow. If you want to use a seed converted as input, write the prompt directly in 'populated_text' instead of 'wildcard_text', and set the mode to 'fixed'.")
RETURN_TYPES = ("STRING", )
FUNCTION = "doit"
@@ -2211,8 +2378,10 @@ class ImpactWildcardEncode:
"clip": ("CLIP",),
"wildcard_text": ("STRING", {"multiline": True, "dynamicPrompts": False, "tooltip": "Enter a prompt using wildcard syntax."}),
"populated_text": ("STRING", {"multiline": True, "dynamicPrompts": False, "tooltip": "The actual value passed during the execution of 'ImpactWildcardEncode' is what is shown here. The behavior varies slightly depending on the mode. Wildcard syntax can also be used in 'populated_text'."}),
"mode": ("BOOLEAN", {"default": True, "label_on": "Populate", "label_off": "Fixed", "tooltip": "Populate: Before running the workflow, it overwrites the existing value of 'populated_text' with the prompt processed from 'wildcard_text'. In this mode, 'populated_text' cannot be edited.\n"
"Fixed: Ignores wildcard_text and keeps 'populated_text' as is. You can edit 'populated_text' in this mode."}),
"mode": (["populate", "fixed", "reproduce"], {"tooltip":
"populate: Before running the workflow, it overwrites the existing value of 'populated_text' with the prompt processed from 'wildcard_text'. In this mode, 'populated_text' cannot be edited.\n"
"fixed: Ignores wildcard_text and keeps 'populated_text' as is. You can edit 'populated_text' in this mode\n."
"reproduce: This mode operates as 'fixed' mode only once for reproduction, and then it switches to 'populate' mode."}),
"Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"), ),
"Select to add Wildcard": (["Select the Wildcard to add to the text"], ),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Determines the random seed to be used for wildcard processing."}),
@@ -2222,7 +2391,7 @@ class ImpactWildcardEncode:
CATEGORY = "ImpactPack/Prompt"
DESCRIPTION = ("The 'ImpactWildcardEncode' node processes text prompts written in wildcard syntax and outputs them as conditioning. It also supports LoRA syntax, with the applied LoRA reflected in the model's output.\n\n"
"TIP1: Before the workflow is executed, the processing result of 'wildcard_text' is displayed in 'populated_text', and the populated text is saved along with the workflow. If you want to use a seed converted as input, write the prompt directly in 'populated_text' instead of 'wildcard_text', and set the mode to 'Fixed'.\n"
"TIP1: Before the workflow is executed, the processing result of 'wildcard_text' is displayed in 'populated_text', and the populated text is saved along with the workflow. If you want to use a seed converted as input, write the prompt directly in 'populated_text' instead of 'wildcard_text', and set the mode to 'fixed'.\n"
"TIP2: If the 'Inspire Pack' is installed, LBW(LoRA Block Weight) syntax can also be applied.")
RETURN_TYPES = ("MODEL", "CLIP", "CONDITIONING", "STRING")
+37 -10
View File
@@ -22,6 +22,7 @@ import comfy
from io import BytesIO
import random
from server import PromptServer
import logging
sam_predictor = None
@@ -79,7 +80,7 @@ async def sam_prepare(request):
model_name = os.path.join(impact_pack.model_path, "sams", model_name)
print(f"[INFO] ComfyUI-Impact-Pack: Loading SAM model '{impact_pack.model_path}'")
logging.info(f"[Impact Pack] Loading SAM model '{impact_pack.model_path}'")
filename, image_dir = folder_paths.annotated_filepath(data["filename"])
@@ -95,7 +96,7 @@ async def sam_prepare(request):
thread = threading.Thread(target=async_prepare_sam, args=(image_dir, model_name, filename,))
thread.start()
print(f"[INFO] ComfyUI-Impact-Pack: SAM model loaded. ")
logging.info("[Impact Pack] SAM model loaded. ")
return web.Response(status=200)
@@ -107,7 +108,7 @@ async def release_sam(request):
del sam_predictor
sam_predictor = None
print(f"[INFO] ComfyUI-Impact-Pack: unloading SAM model")
logging.info("[Impact Pack]: unloading SAM model")
@PromptServer.instance.routes.post("/sam/detect")
@@ -324,20 +325,24 @@ def onprompt_for_switch(json_data):
cls = v['class_type']
if cls == 'ImpactInversedSwitch':
# if 'sel_mode' is 'select_on_prompt'
if 'sel_mode' in v['inputs'] and v['inputs']['sel_mode'] and 'select' in v['inputs']:
select_input = v['inputs']['select']
# if 'select' is converted input
if isinstance(select_input, list) and len(select_input) == 2:
input_node = json_data['prompt'][select_input[0]]
if input_node['class_type'] == 'ImpactInt' and 'inputs' in input_node and 'value' in input_node['inputs']:
inversed_switch_info[k] = input_node['inputs']['value']
else:
print(f"\n##### ##### #####\n[WARN] {cls}: For the 'select' operation, only 'select_index' of the 'ImpactInversedSwitch', which is not an input, or 'ImpactInt' and 'Primitive' are allowed as inputs if 'select_on_prompt' is selected.\n##### ##### #####\n")
logging.warning(f"\n##### ##### #####\n[Impact Pack] {cls}: For the 'select' operation, only 'select_index' of the 'ImpactInversedSwitch', which is not an input, or 'ImpactInt' and 'Primitive' are allowed as inputs if 'select_on_prompt' is selected.\n##### ##### #####\n")
else:
inversed_switch_info[k] = select_input
elif cls in ['ImpactSwitch', 'LatentSwitch', 'SEGSSwitch', 'ImpactMakeImageList']:
# if 'sel_mode' is 'select_on_prompt'
if 'sel_mode' in v['inputs'] and v['inputs']['sel_mode'] and 'select' in v['inputs']:
select_input = v['inputs']['select']
# if 'select' is converted input
if isinstance(select_input, list) and len(select_input) == 2:
input_node = json_data['prompt'][select_input[0]]
if input_node['class_type'] == 'ImpactInt' and 'inputs' in input_node and 'value' in input_node['inputs']:
@@ -346,7 +351,7 @@ def onprompt_for_switch(json_data):
if isinstance(input_node['inputs']['select'], int):
onprompt_switch_info[k] = input_node['inputs']['select']
else:
print(f"\n##### ##### #####\n[WARN] {cls}: For the 'select' operation, only 'select_index' of the 'ImpactSwitch', which is not an input, or 'ImpactInt' and 'Primitive' are allowed as inputs if 'select_on_prompt' is selected.\n##### ##### #####\n")
logging.warning(f"\n##### ##### #####\n[Impact Pack] {cls}: For the 'select' operation, only 'select_index' of the 'ImpactSwitch', which is not an input, or 'ImpactInt' and 'Primitive' are allowed as inputs if 'select_on_prompt' is selected.\n##### ##### #####\n")
else:
onprompt_switch_info[k] = select_input
@@ -377,6 +382,8 @@ def onprompt_for_switch(json_data):
if vv[0] in inversed_switch_info:
if vv[1] + 1 != inversed_switch_info[vv[0]]:
disable_targets.add(kk)
else:
del inversed_switch_info[k]
if vv[0] in disabled_switch:
disable_targets.add(kk)
@@ -396,6 +403,11 @@ def onprompt_for_switch(json_data):
for kk in disable_targets:
del v['inputs'][kk]
# inversed_switch - select out of range
for target in inversed_switch_info.keys():
del json_data['prompt'][target]['inputs']['input']
def onprompt_for_pickers(json_data):
detected_pickers = set()
@@ -466,7 +478,17 @@ def onprompt_populate_wildcards(json_data):
for k, v in prompt.items():
if 'class_type' in v and (v['class_type'] == 'ImpactWildcardEncode' or v['class_type'] == 'ImpactWildcardProcessor'):
inputs = v['inputs']
if inputs['mode'] and isinstance(inputs['populated_text'], str):
# legacy adapter
if isinstance(inputs['mode'], bool):
if inputs['mode']:
new_mode = 'populate'
else:
new_mode = 'fixed'
inputs['mode'] = new_mode
if inputs['mode'] == 'populate' and isinstance(inputs['populated_text'], str):
if isinstance(inputs['seed'], list):
try:
input_node = prompt[inputs['seed'][0]]
@@ -479,7 +501,7 @@ def onprompt_populate_wildcards(json_data):
if not isinstance(input_seed, int):
continue
else:
print(f"[Impact Pack] Only `ImpactInt`, `Seed (rgthree)` and `Primitive` Node are allowed as the seed for '{v['class_type']}'. It will be ignored. ")
logging.info(f"[Impact Pack] Only `ImpactInt`, `Seed (rgthree)` and `Primitive` Node are allowed as the seed for '{v['class_type']}'. It will be ignored. ")
continue
except:
continue
@@ -487,17 +509,22 @@ def onprompt_populate_wildcards(json_data):
input_seed = int(inputs['seed'])
inputs['populated_text'] = wildcards.process(inputs['wildcard_text'], input_seed)
inputs['mode'] = False
inputs['mode'] = 'reproduce'
PromptServer.instance.send_sync("impact-node-feedback", {"node_id": k, "widget_name": "populated_text", "type": "STRING", "value": inputs['populated_text']})
updated_widget_values[k] = inputs['populated_text']
if inputs['mode'] == 'reproduce':
PromptServer.instance.send_sync("impact-node-feedback", {"node_id": k, "widget_name": "mode", "type": "STRING", "value": 'populate'})
if 'extra_data' in json_data and 'extra_pnginfo' in json_data['extra_data']:
for node in json_data['extra_data']['extra_pnginfo']['workflow']['nodes']:
key = str(node['id'])
if key in updated_widget_values:
node['widgets_values'][1] = updated_widget_values[key]
node['widgets_values'][2] = False
node['widgets_values'][2] = 'reproduce'
def onprompt_for_remote(json_data):
@@ -542,7 +569,7 @@ def onprompt(json_data):
regional_sampler_seed_update(json_data)
core.current_prompt = json_data
except Exception as e:
print(f"[WARN] ComfyUI-Impact-Pack: Error on prompt - several features will not work.\n{e}")
logging.warning(f"[Impact Pack] ComfyUI-Impact-Pack: Error on prompt - several features will not work.\n{e}")
return json_data
+13 -9
View File
@@ -38,7 +38,7 @@ class SEGSDetailer:
"denoise": ("FLOAT", {"default": 0.5, "min": 0.0001, "max": 1.0, "step": 0.01}),
"noise_mask": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
"force_inpaint": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
"basic_pipe": ("BASIC_PIPE",),
"basic_pipe": ("BASIC_PIPE", {"tooltip": "If the `ImpactDummyInput` is connected to the model in the basic_pipe, the inference stage is skipped."}),
"refiner_ratio": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 100}),
@@ -76,7 +76,7 @@ class SEGSDetailer:
new_segs = []
cnet_pil_list = []
if noise_mask_feather > 0 and 'denoise_mask_function' not in model.model_options:
if not (isinstance(model, str) and model == "DUMMY") and noise_mask_feather > 0 and 'denoise_mask_function' not in model.model_options:
model = nodes_differential_diffusion.DifferentialDiffusion().apply(model)[0]
for i in range(batch_size):
@@ -113,13 +113,17 @@ class SEGSDetailer:
for condition, details in negative
]
enhanced_image, cnet_pils = core.enhance_detail(cropped_image, model, clip, vae, guide_size, guide_size_for, max_size,
seg.bbox, seed, steps, cfg, sampler_name, scheduler,
cropped_positive, cropped_negative, denoise, cropped_mask, force_inpaint,
refiner_ratio=refiner_ratio, refiner_model=refiner_model,
refiner_clip=refiner_clip, refiner_positive=refiner_positive, refiner_negative=refiner_negative,
control_net_wrapper=seg.control_net_wrapper, cycle=cycle,
inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func=scheduler_func_opt)
if not (isinstance(model, str) and model == "DUMMY"):
enhanced_image, cnet_pils = core.enhance_detail(cropped_image, model, clip, vae, guide_size, guide_size_for, max_size,
seg.bbox, seed, steps, cfg, sampler_name, scheduler,
cropped_positive, cropped_negative, denoise, cropped_mask, force_inpaint,
refiner_ratio=refiner_ratio, refiner_model=refiner_model,
refiner_clip=refiner_clip, refiner_positive=refiner_positive, refiner_negative=refiner_negative,
control_net_wrapper=seg.control_net_wrapper, cycle=cycle,
inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func=scheduler_func_opt)
else:
enhanced_image = cropped_image
cnet_pils = None
if cnet_pils is not None:
cnet_pil_list.extend(cnet_pils)
+14 -3
View File
@@ -526,6 +526,9 @@ class ReencodeLatent:
"output_vae": ("VAE", ),
"tile_size": ("INT", {"default": 512, "min": 320, "max": 4096, "step": 64}),
},
"optional": {
"overlap": ("INT", {"default": 64, "min": 0, "max": 4096, "step": 32, "tooltip": "This setting applies when 'tile_mode' is enabled."}),
}
}
CATEGORY = "ImpactPack/Util"
@@ -533,14 +536,22 @@ class ReencodeLatent:
RETURN_TYPES = ("LATENT", )
FUNCTION = "doit"
def doit(self, samples, tile_mode, input_vae, output_vae, tile_size=512):
def doit(self, samples, tile_mode, input_vae, output_vae, tile_size=512, overlap=64):
if tile_mode in ["Both", "Decode(input) only"]:
pixels = nodes.VAEDecodeTiled().decode(input_vae, samples, tile_size)[0]
decoder = nodes.VAEDecodeTiled()
if 'overlap' in inspect.signature(decoder.decode).parameters:
pixels = decoder.decode(input_vae, samples, tile_size, overlap=overlap)[0]
else:
pixels = decoder.decode(input_vae, samples, tile_size, overlap=overlap)[0]
else:
pixels = nodes.VAEDecode().decode(input_vae, samples)[0]
if tile_mode in ["Both", "Encode(output) only"]:
return nodes.VAEEncodeTiled().encode(output_vae, pixels, tile_size)
encoder = nodes.VAEEncodeTiled()
if 'overlap' in inspect.signature(encoder.encode).parameters:
return encoder.encode(output_vae, pixels, tile_size, overlap=overlap)
else:
return encoder.encode(output_vae, pixels, tile_size)
else:
return nodes.VAEEncode().encode(output_vae, pixels)
+10 -3
View File
@@ -7,6 +7,7 @@ import nodes
from . import config
from PIL import Image
import comfy
import time
class TensorBatchBuilder:
@@ -501,15 +502,21 @@ def crop_image(image, crop_region):
return crop_tensor4(image, crop_region)
def to_latent_image(pixels, vae):
def to_latent_image(pixels, vae, vae_tiled_encode=False):
x = pixels.shape[1]
y = pixels.shape[2]
if pixels.shape[1] != x or pixels.shape[2] != y:
pixels = pixels[:, :x, :y, :]
vae_encode = nodes.VAEEncode()
start = time.time()
if vae_tiled_encode:
encoded = nodes.VAEEncodeTiled().encode(vae, pixels, 512, overlap=64)[0] # using default settings
print(f"[Impact Pack] vae encoded (tiled) in {time.time() - start:.1f}s")
else:
encoded = nodes.VAEEncode().encode(vae, pixels)[0]
print(f"[Impact Pack] vae encoded in {time.time() - start:.1f}s")
return vae_encode.encode(vae, pixels)[0]
return encoded
def empty_pil_tensor(w=64, h=64):
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-impact-pack"
description = "This node pack offers various detector nodes and detailer nodes that allow you to configure a workflow that automatically enhances facial details. And provide iterative upscaler."
version = "8.0.1"
version = "8.5.1"
license = { file = "LICENSE.txt" }
dependencies = ["segment-anything", "scikit-image", "piexif", "transformers", "opencv-python-headless", "GitPython", "scipy>=1.11.4"]