implemented new feats, fixed bugs.

This commit is contained in:
vincentfs
2025-12-08 15:30:14 +08:00
parent f82580e19e
commit 9a34deae25
8 changed files with 401 additions and 1046 deletions
+1
View File
@@ -5,3 +5,4 @@ __pycache__
*.pth
*.ini
py/nodes_xfeat.py
examples/xfeat
-902
View File
@@ -1,902 +0,0 @@
{
"last_node_id": 178,
"last_link_id": 279,
"nodes": [
{
"id": 68,
"type": "AG Blur Median",
"pos": [
-724.2387084960938,
310.97021484375
],
"size": [
315,
58
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "nparrays",
"type": "NPARRAY",
"link": 80
}
],
"outputs": [
{
"name": "dst",
"type": "NPARRAY",
"links": [
232
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG Blur Median"
},
"widgets_values": [
5
]
},
{
"id": 45,
"type": "LoadImage",
"pos": [
-1346.0712890625,
311.7904357910156
],
"size": [
315,
314
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
139
],
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "v0.3.10",
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"01_Raw.jpg",
"image"
]
},
{
"id": 167,
"type": "AG Threshold",
"pos": [
-132.14578247070312,
311.88262939453125
],
"size": [
315,
106
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "nparrays",
"type": "NPARRAY",
"link": 231
}
],
"outputs": [
{
"name": "dst",
"type": "NPARRAY",
"links": [
233
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG Threshold"
},
"widgets_values": [
30,
255,
"BINARY"
]
},
{
"id": 81,
"type": "AG Bitwise Not",
"pos": [
-368.5704345703125,
311.11083984375
],
"size": [
214.20001220703125,
26
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "nparrays",
"type": "NPARRAY",
"link": 232
}
],
"outputs": [
{
"name": "dst",
"type": "NPARRAY",
"links": [
231
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG Bitwise Not"
},
"widgets_values": []
},
{
"id": 56,
"type": "AG To Nparray",
"pos": [
-968.87451171875,
312.17279052734375
],
"size": [
210,
26
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 139
}
],
"outputs": [
{
"name": "nparrays",
"type": "NPARRAY",
"links": [
80,
276
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG To Nparray"
},
"widgets_values": []
},
{
"id": 85,
"type": "AG Hough Lines Accumulator",
"pos": [
223.89540100097656,
310.5812683105469
],
"size": [
365.4000244140625,
198
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "np_bins",
"type": "NPARRAY",
"link": 233
}
],
"outputs": [
{
"name": "dst",
"type": "NPARRAY",
"links": [
137,
260,
277
],
"slot_index": 0
},
{
"name": "ticks",
"type": "*",
"links": [
261
],
"slot_index": 1
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG Hough Lines Accumulator"
},
"widgets_values": [
0.19999999999999998,
0,
90,
1,
-0.505324,
1
]
},
{
"id": 178,
"type": "AG K Largest Points",
"pos": [
803.9663696289062,
734.7616577148438
],
"size": [
210,
78
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "nparrays",
"type": "NPARRAY",
"link": 277
},
{
"name": "ticks",
"type": "*",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "points",
"type": "*",
"links": [
278
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG K Largest Points"
},
"widgets_values": [
5
]
},
{
"id": 175,
"type": "AG K Largest Points",
"pos": [
808.4725952148438,
1024.1015625
],
"size": [
210,
78
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "nparrays",
"type": "NPARRAY",
"link": 260
},
{
"name": "ticks",
"type": "*",
"link": 261,
"shape": 7
}
],
"outputs": [
{
"name": "points",
"type": "*",
"links": [
266
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG K Largest Points"
},
"widgets_values": [
50
]
},
{
"id": 149,
"type": "AG Draw Circles",
"pos": [
1054.504638671875,
738.0717163085938
],
"size": [
210,
126
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "nparrays",
"type": "NPARRAY",
"link": 250
},
{
"name": "points",
"type": "*",
"link": 278
}
],
"outputs": [
{
"name": "dst",
"type": "NPARRAY",
"links": [
244
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG Draw Circles"
},
"widgets_values": [
10,
"RED",
-1
]
},
{
"id": 169,
"type": "AG Point to Line Params",
"pos": [
1060.9705810546875,
1067.9710693359375
],
"size": [
218.39999389648438,
26
],
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "points",
"type": "*",
"link": 266
}
],
"outputs": [
{
"name": "lines",
"type": "*",
"links": [
240
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG Point to Line Params"
},
"widgets_values": []
},
{
"id": 170,
"type": "AG Hough Lines Plotter",
"pos": [
1331.994384765625,
986.458740234375
],
"size": [
210,
102
],
"flags": {},
"order": 14,
"mode": 0,
"inputs": [
{
"name": "nparrays",
"type": "NPARRAY",
"link": 276
},
{
"name": "lines",
"type": "*",
"link": 240
}
],
"outputs": [
{
"name": "dst",
"type": "NPARRAY",
"links": [
242
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG Hough Lines Plotter"
},
"widgets_values": [
"GREEN",
2
]
},
{
"id": 137,
"type": "AG To Image",
"pos": [
1604.931884765625,
985.5106811523438
],
"size": [
140,
26
],
"flags": {},
"order": 16,
"mode": 0,
"inputs": [
{
"name": "nparrays",
"type": "NPARRAY",
"link": 242
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": [
157
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG To Image"
},
"widgets_values": []
},
{
"id": 171,
"type": "AG To Image",
"pos": [
1603.8665771484375,
739.9141845703125
],
"size": [
140,
26
],
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "nparrays",
"type": "NPARRAY",
"link": 244
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": [
245
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG To Image"
},
"widgets_values": []
},
{
"id": 114,
"type": "AG Colormap Hot Gray",
"pos": [
737.6822509765625,
311.9242858886719
],
"size": [
289.79998779296875,
26
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "nparrays",
"type": "NPARRAY",
"link": 137
}
],
"outputs": [
{
"name": "dst",
"type": "NPARRAY",
"links": [
138,
250
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG Colormap Hot Gray"
},
"widgets_values": []
},
{
"id": 82,
"type": "AG To Image",
"pos": [
1592.6949462890625,
317.0210266113281
],
"size": [
140,
26
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "nparrays",
"type": "NPARRAY",
"link": 138
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": [
120
],
"slot_index": 0
}
],
"properties": {
"cnr_id": "archigraph",
"ver": "02af834a760fe748de630be7273371718830ac54",
"Node name for S&R": "AG To Image"
},
"widgets_values": []
},
{
"id": 172,
"type": "PreviewImage",
"pos": [
1840,
650
],
"size": [
317.501953125,
255.177001953125
],
"flags": {},
"order": 15,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 245
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "v0.3.10",
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 138,
"type": "PreviewImage",
"pos": [
1846.479248046875,
996.264892578125
],
"size": [
638.4635620117188,
247.25985717773438
],
"flags": {},
"order": 17,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 157
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "v0.3.10",
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 75,
"type": "PreviewImage",
"pos": [
1841.726318359375,
313.2637634277344
],
"size": [
305.1395263671875,
253.12794494628906
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 120
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "v0.3.10",
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
}
],
"links": [
[
80,
56,
0,
68,
0,
"NPARRAY"
],
[
120,
82,
0,
75,
0,
"IMAGE"
],
[
137,
85,
0,
114,
0,
"NPARRAY"
],
[
138,
114,
0,
82,
0,
"NPARRAY"
],
[
139,
45,
0,
56,
0,
"IMAGE"
],
[
157,
137,
0,
138,
0,
"IMAGE"
],
[
231,
81,
0,
167,
0,
"NPARRAY"
],
[
232,
68,
0,
81,
0,
"NPARRAY"
],
[
233,
167,
0,
85,
0,
"NPARRAY"
],
[
240,
169,
0,
170,
1,
"*"
],
[
242,
170,
0,
137,
0,
"NPARRAY"
],
[
244,
149,
0,
171,
0,
"NPARRAY"
],
[
245,
171,
0,
172,
0,
"IMAGE"
],
[
250,
114,
0,
149,
0,
"NPARRAY"
],
[
260,
85,
0,
175,
0,
"NPARRAY"
],
[
261,
85,
1,
175,
1,
"*"
],
[
266,
175,
0,
169,
0,
"*"
],
[
276,
56,
0,
170,
0,
"NPARRAY"
],
[
277,
85,
0,
178,
0,
"NPARRAY"
],
[
278,
178,
0,
149,
1,
"*"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.26734863064137815,
"offset": [
2324.5850053987074,
671.1494564543829
]
},
"node_versions": {
"ComfyUI-ArchiGraph": "02af834a760fe748de630be7273371718830ac54",
"comfy-core": "v0.3.10",
"comfyui-custom-scripts": "1.2.5"
}
},
"version": 0.4
}
+78
View File
@@ -0,0 +1,78 @@
// below implementation is from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
// Displays input text on a node
app.registerExtension({
name: "AG Printf",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "AG Printf") {
function populate(text) {
if (this.widgets) {
// On older frontend versions there is a hidden converted-widget
const isConvertedWidget = +!!this.inputs?.[0].widget;
for (let i = isConvertedWidget; i < this.widgets.length; i++) {
this.widgets[i].onRemove?.();
}
this.widgets.length = isConvertedWidget;
}
const v = [...text];
if (!v[0]) {
v.shift();
}
for (let list of v) {
// Force list to be an array, not sure why sometimes it is/isn't
if (!(list instanceof Array)) list = [list];
for (const l of list) {
const w = ComfyWidgets["STRING"](this, "text_" + this.widgets?.length ?? 0, ["STRING", { multiline: true }], app).widget;
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
w.value = l;
}
}
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) {
sz[0] = this.size[0];
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1];
}
this.onResize?.(sz);
app.graph.setDirtyCanvas(true, false);
});
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
populate.call(this, message.text);
};
const VALUES = Symbol();
const configure = nodeType.prototype.configure;
nodeType.prototype.configure = function () {
// Store unmodified widget values as they get removed on configure by new frontend
this[VALUES] = arguments[0]?.widgets_values;
return configure?.apply(this, arguments);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments);
const widgets_values = this[VALUES];
if (widgets_values?.length) {
// In newer frontend there seems to be a delay in creating the initial widget
requestAnimationFrame(() => {
populate.call(this, widgets_values.slice(+(widgets_values.length > 1 && this.inputs?.[0].widget)));
});
}
};
}
},
});
+17 -1
View File
@@ -25,6 +25,8 @@ try:
"JsonLoad",
"NparraySave",
"NparrayLoad",
"SaveNparrayImage",
"PreviewNparrayImage",
"Printf",
"SliceList",
@@ -55,6 +57,7 @@ try:
"MedianBlur",
"Blur",
"Sobel",
"Canny",
"HoughLines",
"HoughLinesP",
@@ -86,9 +89,22 @@ try:
"KLargestPoints",
"PointToLineParams",
"IntersectImageWithMask",
"MorphologyByRegions",
"MorphologyByRegionsWithStats",
"DrawQuiver",
"LabelsToContours",
"ContoursToLabels",
"LabelsPreview",
"CreateGVF",
"GVFSnake",
):
if hasattr(nodes_xfeat, name):
AddMapping(getattr(nodes_xfeat, name))
# from .py.nodes_xfeat import MorphCloseByRegions
# AddMapping(MorphCloseByRegions)
except ImportError as e:
log("Premium nodes ignored", msg_color="BRIGHT_GREEN")
+83
View File
@@ -0,0 +1,83 @@
import cv2
ColorEnum = {
"WHITE": (255, 255, 255),
"BLACK": (0, 0, 0),
"RED": (0, 0, 255),
"GREEN": (0, 255, 0),
"BLUE": (255, 0, 0),
}
ThreshEnum = {
"BINARY": cv2.THRESH_BINARY,
"BINARY_INV": cv2.THRESH_BINARY_INV,
"TRUNC": cv2.THRESH_TRUNC,
"TOZERO": cv2.THRESH_TOZERO,
"TOZERO_INV": cv2.THRESH_TOZERO_INV,
}
BorderEnum = {
"DEFAULT": cv2.BORDER_DEFAULT,
"CONSTANT": cv2.BORDER_CONSTANT,
"REPLICATE": cv2.BORDER_REPLICATE,
"WRAP": cv2.BORDER_WRAP,
}
ContourModeEnum = {
"RETR_EXTERNAL": cv2.RETR_EXTERNAL,
"RETR_LIST": cv2.RETR_LIST,
"RETR_CCOMP": cv2.RETR_CCOMP,
"RETR_TREE": cv2.RETR_TREE,
}
ContourMethodEnum = {
"CHAIN_APPROX_NONE": cv2.CHAIN_APPROX_NONE,
"CHAIN_APPROX_SIMPLE": cv2.CHAIN_APPROX_SIMPLE,
"CHAIN_APPROX_TC89_L1": cv2.CHAIN_APPROX_TC89_L1,
"CHAIN_APPROX_TC89_KCOS": cv2.CHAIN_APPROX_TC89_KCOS,
}
MorphEnum = {
"ERODE": cv2.MORPH_ERODE,
"DILATE": cv2.MORPH_DILATE,
"OPEN": cv2.MORPH_OPEN,
"CLOSE": cv2.MORPH_CLOSE,
"GRADIENT": cv2.MORPH_GRADIENT,
"TOPHAT": cv2.MORPH_TOPHAT,
"BLACKHAT": cv2.MORPH_BLACKHAT,
}
DepthEnum = {
"CV_8U": cv2.CV_8U,
"CV_8S": cv2.CV_8S,
"CV_16U": cv2.CV_16U,
"CV_16S": cv2.CV_16S,
"CV_32S": cv2.CV_32S,
"CV_32F": cv2.CV_32F,
"CV_64F": cv2.CV_64F,
}
ColormapEnum = {
'AUTUMN': cv2.COLORMAP_AUTUMN,
'BONE': cv2.COLORMAP_BONE,
'JET': cv2.COLORMAP_JET,
'WINTER': cv2.COLORMAP_WINTER,
'RAINBOW': cv2.COLORMAP_RAINBOW, # 真正的彩虹色阶
'OCEAN': cv2.COLORMAP_OCEAN,
'SUMMER': cv2.COLORMAP_SUMMER,
'SPRING': cv2.COLORMAP_SPRING,
'COOL': cv2.COLORMAP_COOL,
'HSV': cv2.COLORMAP_HSV,
'PINK': cv2.COLORMAP_PINK,
'HOT': cv2.COLORMAP_HOT,
'PARULA': cv2.COLORMAP_PARULA,
'MAGMA': cv2.COLORMAP_MAGMA,
'INFERNO': cv2.COLORMAP_INFERNO,
'PLASMA': cv2.COLORMAP_PLASMA,
'VIRIDIS': cv2.COLORMAP_VIRIDIS,
'CIVIDIS': cv2.COLORMAP_CIVIDIS,
'TWILIGHT': cv2.COLORMAP_TWILIGHT,
'TWILIGHT_SHIFTED': cv2.COLORMAP_TWILIGHT_SHIFTED,
'TURBO': cv2.COLORMAP_TURBO,
'DEEPGREEN': cv2.COLORMAP_DEEPGREEN,
}
+116 -129
View File
@@ -1,10 +1,12 @@
CURRENT_CATEGORY = "🐦‍🔥 ArchiGraph/⭕ OpenCV" # organized by py file
CURRENT_CATEGORY = "🐦‍🔥 ArchiGraph/📀 OpenCV" # organized by py file
import torch
import numpy as np
import cv2
import comfy.model_management as model_management
from .anyType import ANY
from .enums import ColorEnum, ThreshEnum, BorderEnum, ContourModeEnum, \
ContourMethodEnum, DepthEnum, ColormapEnum
#---------------------------------------------------------------------------------------------------------------------#
# Helpers
@@ -133,19 +135,11 @@ class Threshold:
DESCRIPTION = "OpenCV Threshold. Applies a fixed-level threshold to each array element."
def execute(self, nparrays, thresh, maxval, type):
TypeEnum = {
"BINARY": cv2.THRESH_BINARY,
"BINARY_INV": cv2.THRESH_BINARY_INV,
"TRUNC": cv2.THRESH_TRUNC,
"TOZERO": cv2.THRESH_TOZERO,
"TOZERO_INV": cv2.THRESH_TOZERO_INV,
}
batch_size = nparrays.shape[0]
result = np.zeros_like(nparrays)
for b in range(batch_size):
_, result[b] = cv2.threshold(nparrays[b], thresh, maxval, TypeEnum[type])
_, result[b] = cv2.threshold(nparrays[b], thresh, maxval, ThreshEnum[type])
return (result,)
@@ -195,12 +189,6 @@ class GaussianBlur:
DESCRIPTION = "OpenCV GaussianBlur. Default anchor is center of the kernel."
def execute(self, nparrays, ksize_x, ksize_y, sigma_x, sigma_y, borderType):
BorderEnum = {
"DEFAULT": cv2.BORDER_DEFAULT,
"CONSTANT": cv2.BORDER_CONSTANT,
"REPLICATE": cv2.BORDER_REPLICATE,
"WRAP": cv2.BORDER_WRAP,
}
if ksize_y == 1: ksize_y = ksize_x
batch_size = nparrays.shape[0]
@@ -282,12 +270,6 @@ class Blur:
DESCRIPTION = "OpenCV Blur. Default anchor is center of the kernel."
def execute(self, nparrays, ksize_x, ksize_y, borderType):
BorderEnum = {
"DEFAULT": cv2.BORDER_DEFAULT,
"CONSTANT": cv2.BORDER_CONSTANT,
"REPLICATE": cv2.BORDER_REPLICATE,
"WRAP": cv2.BORDER_WRAP,
}
if ksize_y == 1: ksize_y = ksize_x
batch_size = nparrays.shape[0]
@@ -297,7 +279,71 @@ class Blur:
result[b] = cv2.blur(nparrays[b], (ksize_x, ksize_y), borderType=BorderEnum[borderType])
return (result,)
#---------------------------------------------------------------------------------------------------------------------#
class Sobel:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"nparrays": ("NPARRAY",),
"dx": ("INT", {
"default": 1,
"min": 0,
"max": 2,
"tooltip": "Order of the derivative x."
}),
"dy": ("INT", {
"default": 0,
"min": 0,
"max": 2,
"tooltip": "Order of the derivative y."
}),
"ksize": ("INT", {
"default": 3,
"min": 1,
"max": 31,
"step": 2, # Must be odd number
"tooltip": "Aperture size for the Sobel operator. Must be odd and greater than 1."
}),
"ddepth": (["CV_8U", "CV_8S", "CV_16U", "CV_16S", "CV_32S", "CV_32F", "CV_64F"], {
"default": "CV_32F",
"tooltip": "Desired depth of the destination image. Use CV_32F/64F for float output."
}),
"converted": ("BOOLEAN", {
"default": True,
"tooltip": "If true, the output will be converted with cv2.convertScaleAbs() to 8-bit unsigned integer. If false, the output will remain unprocessed."
}),
},
}
RETURN_TYPES = ("NPARRAY",)
RETURN_NAMES = ("dst",)
FUNCTION = "execute"
NAME = "Sobel"
CATEGORY = CURRENT_CATEGORY
DESCRIPTION = "OpenCV Sobel. Computes the first, second, third, or mixed image derivatives using an extended Sobel operator."
def execute(self, nparrays, dx, dy, ksize, ddepth, converted):
batch_size = nparrays.shape[0]
batches = []
for b in range(batch_size):
# force input to be gray
if (len(nparrays[b].shape) == 3 and nparrays[b].shape[2] == 3):
gray = cv2.cvtColor(nparrays[b], cv2.COLOR_BGR2GRAY)
else:
gray = nparrays[b]
if converted:
batch = cv2.convertScaleAbs(cv2.Sobel(gray, DepthEnum[ddepth], dx, dy, ksize=ksize))
else:
batch = cv2.Sobel(gray, DepthEnum[ddepth], dx, dy, ksize=ksize)
batches.append(batch)
result = np.stack(batches, axis=0)
return (result,)
#---------------------------------------------------------------------------------------------------------------------#
class Canny:
@classmethod
@@ -396,7 +442,7 @@ class HoughLines:
},
}
RETURN_TYPES = (ANY,)
RETURN_TYPES = ("LIST_LINE",)
RETURN_NAMES = ("lines",)
FUNCTION = "execute"
NAME = "Hough Lines"
@@ -459,7 +505,7 @@ class HoughLinesP:
},
}
RETURN_TYPES = (ANY,)
RETURN_TYPES = ("LIST_LINE",)
RETURN_NAMES = ("lines",)
FUNCTION = "execute"
NAME = "Hough Lines Probabilistic"
@@ -498,7 +544,7 @@ class FindContours:
"tooltip": "Binary image. Non-zero pixels are treated as 1's. \nYou can use compare(), threshold(), inRange(), Canny() etc. to create a binary image."
}),
"mode": (["RETR_EXTERNAL", "RETR_LIST", "RETR_CCOMP", "RETR_TREE"], {
"default": "RETR_EXTERNAL",
"default": "RETR_CCOMP",
"tooltip": "Contour retrieval mode"
}),
"method": (["CHAIN_APPROX_NONE", "CHAIN_APPROX_SIMPLE", "CHAIN_APPROX_TC89_L1", "CHAIN_APPROX_TC89_KCOS"], {
@@ -508,8 +554,9 @@ class FindContours:
},
}
RETURN_TYPES = (ANY, ANY, )
RETURN_NAMES = ("contours", "hierachy", )
SINGULAR_ONLY = True
RETURN_TYPES = ("CV_CONTOURS", "CV_HIERARCHY", )
RETURN_NAMES = ("contours", "hierarchy", )
FUNCTION = "execute"
NAME = "Find Contours"
CATEGORY = CURRENT_CATEGORY
@@ -518,39 +565,18 @@ class FindContours:
"Return: - hierarchy, containing info about topology. Same length as contours. Each has 4 elements, [next, prev, first child, parent]."
def execute(self, Np_bins, mode, method):
ModeEnum = {
"RETR_EXTERNAL": cv2.RETR_EXTERNAL,
"RETR_LIST": cv2.RETR_LIST,
"RETR_CCOMP": cv2.RETR_CCOMP,
"RETR_TREE": cv2.RETR_TREE,
}
MethodEnum = {
"CHAIN_APPROX_NONE": cv2.CHAIN_APPROX_NONE,
"CHAIN_APPROX_SIMPLE": cv2.CHAIN_APPROX_SIMPLE,
"CHAIN_APPROX_TC89_L1": cv2.CHAIN_APPROX_TC89_L1,
"CHAIN_APPROX_TC89_KCOS": cv2.CHAIN_APPROX_TC89_KCOS,
}
if Np_bins.shape[0] != 1:
raise ValueError("FindContours node only supports batch size of 1.")
batch_size = Np_bins.shape[0]
# Convert to grayscale if needed
if len(Np_bins[0].shape) == 3 and Np_bins[0].shape[2] == 3:
gray = cv2.cvtColor(Np_bins[0], cv2.COLOR_BGR2GRAY)
else:
gray = Np_bins[0]
list_contours = []
list_hierarchy = []
contours, hierarchy = cv2.findContours(gray, ContourModeEnum[mode], ContourMethodEnum[method])
for b in range(batch_size):
# Convert to grayscale if needed
if len(Np_bins[b].shape) == 3 and Np_bins[b].shape[2] == 3:
gray = cv2.cvtColor(Np_bins[b], cv2.COLOR_BGR2GRAY)
else:
gray = Np_bins[b]
contours, hierarchy = cv2.findContours(gray, ModeEnum[mode], MethodEnum[method])
list_contours.append(contours)
list_hierarchy.append(hierarchy)
# contour_image = np.zeros_like(Np_bins[b])
# cv2.drawContours(contour_image, contours, -1, (255, 255, 255), 1)
return (list_contours, list_hierarchy, )
return (contours, hierarchy, )
#---------------------------------------------------------------------------------------------------------------------#
class DrawContours:
@@ -561,7 +587,7 @@ class DrawContours:
"nparrays": ("NPARRAY", {
"tooltip": "Input image to draw contours on. Should be color image."
}),
"contours": (ANY, {
"contours": ("CV_CONTOURS", {
"tooltip": "Contours to draw. Use FindContours node to get contours."
}),
"color": (["WHITE", "BLACK", "RED", "GREEN", "BLUE"], {
@@ -570,10 +596,10 @@ class DrawContours:
}),
"thickness": ("INT", {
"default": 2,
"min": 1,
"min": -1,
"max": 20,
"step": 1,
"tooltip": "Thickness of the contour lines."
"tooltip": "Thickness of the contour lines. -1 means filled."
}),
"hideBackground": ("BOOLEAN", {
"default": False,
@@ -582,6 +608,7 @@ class DrawContours:
},
}
SINGULAR_ONLY = True
RETURN_TYPES = ("NPARRAY",)
RETURN_NAMES = ("dst",)
FUNCTION = "execute"
@@ -590,28 +617,22 @@ class DrawContours:
DESCRIPTION = "OpenCV Draw Contours. Always draws all contours in the list."
def execute(self, nparrays, contours, color, thickness, hideBackground):
batch_size = min(nparrays.shape[0], len(contours)) # in case different batch size, use the smaller ones
colorEnum = {
"WHITE": (255, 255, 255),
"BLACK": (0, 0, 0),
"RED": (0, 0, 255),
"GREEN": (0, 255, 0),
"BLUE": (255, 0, 0),
}
result = np.zeros_like(nparrays)
if nparrays.shape[0] != 1:
raise ValueError("DrawContours node only supports batch size of 1.")
if hideBackground:
height = nparrays.shape[1]
width = nparrays.shape[2]
input = np.zeros((height, width, 3), dtype=np.uint8)
for b in range(batch_size):
result[b] = cv2.drawContours(input, contours[b], -1, colorEnum[color], thickness)
else:
for b in range(batch_size):
result[b] = cv2.drawContours(nparrays[b], contours[b], -1, colorEnum[color], thickness)
return (result,)
# convert to color if needed
if len(nparrays[0].shape) == 2 or nparrays[0].shape[2] == 1:
input = cv2.cvtColor(nparrays[0], cv2.COLOR_GRAY2BGR)
else:
input = nparrays[0]
result = cv2.drawContours(input, contours, -1, ColorEnum[color], thickness)
return (result[np.newaxis, :],)
#---------------------------------------------------------------------------------------------------------------------#
class Circles:
@@ -655,13 +676,6 @@ class Circles:
def execute(self, nparrays, points, radius, color, thickness):
batch_size = nparrays.shape[0]
colorEnum = {
"WHITE": (255, 255, 255),
"BLACK": (0, 0, 0),
"RED": (0, 0, 255),
"GREEN": (0, 255, 0),
"BLUE": (255, 0, 0),
}
result = np.zeros((batch_size, nparrays.shape[1], nparrays.shape[2], 3), dtype=np.uint8)
@@ -673,7 +687,7 @@ class Circles:
result[b] = nparrays[b]
for point in points[b]:
result[b] = cv2.circle(result[b], point, radius, colorEnum[color], thickness)
result[b] = cv2.circle(result[b], point, radius, ColorEnum[color], thickness)
return (result,)
@@ -690,53 +704,28 @@ class ConnectedComponentsWithStats:
"default": "8",
"tooltip": "Pixel connectivity to use."
}),
"colormap": (list(cls._COLORMAPS.keys()), {
"default": "HSV",
"tooltip": "Colormap to use for preview."
"removeSurrounding": ("BOOLEAN", {
"default": True,
"tooltip": "If true, the surrounding part (different from background) will be removed from output."
}),
},
}
SINGULAR_ONLY = True
RETURN_TYPES = ("INT", "NPARRAY", ANY, ANY, "NPARRAY",)
RETURN_NAMES = ("num_labels", "labels", "stats", "centroids", "preview",)
RETURN_TYPES = ("INT", "NPARRAY", ANY, ANY,)
RETURN_NAMES = ("num_labels", "labels", "stats", "centroids",)
FUNCTION = "execute"
NAME = "Connected Components With Stats"
CATEGORY = CURRENT_CATEGORY
DESCRIPTION = "OpenCV Connected Components With Stats. Labels connected components in a binary image and computes statistics for each component."
_COLORMAPS = {
'AUTUMN': cv2.COLORMAP_AUTUMN,
'BONE': cv2.COLORMAP_BONE,
'JET': cv2.COLORMAP_JET,
'WINTER': cv2.COLORMAP_WINTER,
'RAINBOW': cv2.COLORMAP_RAINBOW, # 真正的彩虹色阶
'OCEAN': cv2.COLORMAP_OCEAN,
'SUMMER': cv2.COLORMAP_SUMMER,
'SPRING': cv2.COLORMAP_SPRING,
'COOL': cv2.COLORMAP_COOL,
'HSV': cv2.COLORMAP_HSV,
'PINK': cv2.COLORMAP_PINK,
'HOT': cv2.COLORMAP_HOT,
'PARULA': cv2.COLORMAP_PARULA,
'MAGMA': cv2.COLORMAP_MAGMA,
'INFERNO': cv2.COLORMAP_INFERNO,
'PLASMA': cv2.COLORMAP_PLASMA,
'VIRIDIS': cv2.COLORMAP_VIRIDIS,
'CIVIDIS': cv2.COLORMAP_CIVIDIS,
'TWILIGHT': cv2.COLORMAP_TWILIGHT,
'TWILIGHT_SHIFTED': cv2.COLORMAP_TWILIGHT_SHIFTED,
'TURBO': cv2.COLORMAP_TURBO,
'DEEPGREEN': cv2.COLORMAP_DEEPGREEN,
}
def execute(self, Np_bin, connectivity, colormap):
def execute(self, Np_bin, connectivity, removeSurrounding):
if Np_bin.shape[0] != 1:
raise ValueError("ConnectedComponentsWithStats node only supports batch size of 1.")
ConnectivityInput = 4 if connectivity == "4" else 8
# Convert to grayscale if needed
if len(Np_bin.shape) == 3 and Np_bin.shape[2] == 3:
if len(Np_bin[0].shape) == 3 and Np_bin[0].shape[2] == 3:
gray = cv2.cvtColor(Np_bin[0], cv2.COLOR_BGR2GRAY)
else:
gray = Np_bin[0]
@@ -745,17 +734,15 @@ class ConnectedComponentsWithStats:
if num_labels <= 1:
raise ValueError("No objects found in the binary image.")
elif num_labels == 2:
# use black and white directly
preview = (labels * 255).astype(np.uint8)[np.newaxis, :]
else:
# normalize the labels to 0-255 range for visualization
num_objects = num_labels - 1 # Exclude background label 0
normalized_labels = labels * (255 // num_objects) # Normalize to 0-255, will never reach 1
colored = cv2.applyColorMap(normalized_labels.astype(np.uint8), self._COLORMAPS[colormap])
# force label=0 to be black, because colormap[0] is usually not pure black (like deep blue/purple)
colored[labels == 0] = [0, 0, 0]
preview = colored[np.newaxis, :]
if removeSurrounding:
corner_labels = (labels[0,0], labels[0,-1], labels[-1,0], labels[-1,-1])
# if more than 3 corners share the same label, consider it as surrounding
surrounding_label = max(set(corner_labels), key=corner_labels.count)
if surrounding_label != 0 and corner_labels.count(surrounding_label) >= 3:
labels[labels == surrounding_label] = 0 # set surrounding to background
# re-label the largest label to use surrounding_label
labels[labels == num_labels - 1] = surrounding_label
num_labels -= 1
return (num_labels, labels, stats, centroids, preview,)
return (num_labels, labels, stats, centroids, )
+105 -13
View File
@@ -1,11 +1,16 @@
CURRENT_CATEGORY = "🐦‍🔥 ArchiGraph/🛠️ Utils" # organized by py file
import os
import random
import torch
from comfy.utils import common_upscale
import numpy as np
from .anyType import ANY
from .nodes_opencv import Nparray2Image
# import comfyui core node
from nodes import SaveImage
import folder_paths
NPARRAY_DIR = os.path.join(folder_paths.get_output_directory(), "nparrays")
JSON_DIR = os.path.join(folder_paths.get_output_directory(), "jsons")
@@ -62,10 +67,13 @@ class JsonLoad:
files = []
else:
files = sorted([f for f in os.listdir(JSON_DIR) if f.endswith('.json')])
if len(files) == 0:
files = ["None"] # Placeholder to avoid empty list issues
return {
"required": {
"jsonfile": (sorted(files), ),
"jsonfile": (files, ),
"filename_noExt": ("STRING", {
"default": "",
"tooltip": "Filename without extension (extension .json will be added automatically). If specified, npyfile will be ignored.",
@@ -85,6 +93,10 @@ class JsonLoad:
if filename_noExt != "":
jsonfile = filename_noExt + ".json"
else:
if jsonfile == "None":
raise FileNotFoundError("JSON files not found in the jsons directory.")
file_path = os.path.join(JSON_DIR, jsonfile)
if not os.path.exists(file_path):
@@ -136,6 +148,9 @@ class NparrayLoad:
files = []
else:
files = sorted([f for f in os.listdir(NPARRAY_DIR) if f.endswith('.npy')])
if len(files) == 0:
files = ["None"] # Placeholder to avoid empty list issues
return {
"required": {
@@ -157,6 +172,10 @@ class NparrayLoad:
def execute(self, npyfile, filename_noExt=""):
if filename_noExt != "":
npyfile = filename_noExt + ".npy"
else:
if npyfile == "None":
raise FileNotFoundError("Npy files not found in the nparrays directory.")
file_path = os.path.join(NPARRAY_DIR, npyfile)
if not os.path.exists(file_path):
@@ -166,18 +185,72 @@ class NparrayLoad:
return (nparray,)
#---------------------------------------------------------------------------------------------------------------------#
class SaveNparrayImage(SaveImage):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"nparrays": ("NPARRAY", {"tooltip": "The nparray images to save."}),
"filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."})
},
"hidden": {
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
},
}
OUTPUT_NODE = True
RETURN_TYPES = ()
FUNCTION = "execute"
NAME = "Save Nparray Image"
CATEGORY = CURRENT_CATEGORY
DESCRIPTION = "Saves the input nparray images to your ComfyUI output directory."
def execute(self, nparrays, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
images = Nparray2Image().execute(nparrays)[0]
return super().save_images(images, filename_prefix, prompt, extra_pnginfo)
#---------------------------------------------------------------------------------------------------------------------#
class PreviewNparrayImage(SaveNparrayImage):
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
self.compress_level = 1
@classmethod
def INPUT_TYPES(cls):
return {"required":
{"nparrays": ("NPARRAY", ), },
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
NAME = "Preview Nparray Image"
DESCRIPTION = "Previews the input nparray images by saving them to a temporary directory."
#---------------------------------------------------------------------------------------------------------------------#
class Printf:
'''
Later can be used to unravel list and show specified elements only.
'''
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"any": (ANY, {
"forceInput": True,
"tooltip": "Select objects to print."
}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
OUTPUT_NODE = True
# INPUT_IS_LIST = True # if the input is a list, workflow will pass each element separately
OUTPUT_IS_LIST = (True,)
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("string",)
FUNCTION = "execute"
@@ -185,28 +258,47 @@ class Printf:
CATEGORY = CURRENT_CATEGORY
DESCRIPTION = "Print object info to console for debugging. Supports any input type."
def execute(self, any):
message = ""
def execute(self, any, unique_id=None, extra_pnginfo=None):
# prepare text, caution: text is parse at frontend as list of strings
text = []
if isinstance(any, np.ndarray):
message = f"Type: Nparray, Shape: {any.shape}, Dtype: {any.dtype}"
text.append(f"Type: Nparray, Shape: {any.shape}, Dtype: {any.dtype}")
elif isinstance(any, torch.Tensor):
message = f"Type: Tensor, Shape: {any.shape}, Dtype: {any.dtype}, Device: {any.device}"
text.append(f"Type: Tensor, Shape: {any.shape}, Dtype: {any.dtype}, Device: {any.device}")
elif isinstance(any, list):
message = f"Type: List, Length: {len(any)}\n"
text.append(f"Type: List, Length: {len(any)}")
for i, item in enumerate(any):
message += f"--[{i}]: Type: {type(item)}, Value:\n{item}\n"
text.append(f"[{i}]: Type: {type(item)}, Value:\n{item}")
elif isinstance(any, dict):
message = f"Type: Dict, Length: {len(any)}\n"
text.append(f"Type: Dict, Length: {len(any)}")
for key, value in any.items():
message += f"--Key: {key}, Type: {type(value)}, Value:\n{value}\n"
text.append(f"Key: {key}, Type: {type(value)}, Value:\n{value}")
elif isinstance(any, tuple):
message = f"Type: Tuple, Length: {len(any)}\n"
text.append(f"Type: Tuple, Length: {len(any)}")
for i, item in enumerate(any):
message += f"--[{i}]: Type: {type(item)}, Value:\n{item}\n"
text.append(f"[{i}]: Type: {type(item)}, Value:\n{item}")
else:
message = f"Type: {type(any)}, Value:\n{any}\n"
text.append(f"Type: {type(any)}, Value:\n{any}")
return (message,)
# save to workflow pnginfo
if unique_id is not None and extra_pnginfo is not None:
if not isinstance(extra_pnginfo, list):
print("Error: extra_pnginfo is not a list")
elif (
not isinstance(extra_pnginfo[0], dict)
or "workflow" not in extra_pnginfo[0]
):
print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
else:
workflow = extra_pnginfo[0]["workflow"]
node = next(
(x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])),
None,
)
if node:
node["widgets_values"] = [text]
return {"ui": {"text": text}, "result": (text,)}
#---------------------------------------------------------------------------------------------------------------------#
class SliceList:
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "archigraph"
description = "Implementation of architectural related graph algorithm in ComfyUI."
version = "0.0.5"
version = "0.0.6"
license = {file = "LICENSE"}
[project.urls]