diff --git a/.gitignore b/.gitignore index f8d8dfa..6147f18 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__ *.pth *.ini py/nodes_xfeat.py +examples/xfeat \ No newline at end of file diff --git a/examples/HoughLinesAccumulator.json b/examples/HoughLinesAccumulator.json deleted file mode 100644 index ab29599..0000000 --- a/examples/HoughLinesAccumulator.json +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/js/v1/Printf.js b/js/v1/Printf.js new file mode 100644 index 0000000..704f18d --- /dev/null +++ b/js/v1/Printf.js @@ -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))); + }); + } + }; + } + }, +}); diff --git a/node_mappings.py b/node_mappings.py index 6bc893a..e6910db 100644 --- a/node_mappings.py +++ b/node_mappings.py @@ -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") \ No newline at end of file diff --git a/py/enums.py b/py/enums.py new file mode 100644 index 0000000..bc7a3c5 --- /dev/null +++ b/py/enums.py @@ -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, +} \ No newline at end of file diff --git a/py/nodes_opencv.py b/py/nodes_opencv.py index 6ec674f..b13fc8e 100644 --- a/py/nodes_opencv.py +++ b/py/nodes_opencv.py @@ -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,) \ No newline at end of file + return (num_labels, labels, stats, centroids, ) \ No newline at end of file diff --git a/py/nodes_utils.py b/py/nodes_utils.py index ea38f45..c293fff 100644 --- a/py/nodes_utils.py +++ b/py/nodes_utils.py @@ -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: diff --git a/pyproject.toml b/pyproject.toml index 4e47ec7..566aec4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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]