Add a description of what you changed

This commit is contained in:
SS-snap
2024-09-30 16:06:42 +08:00
parent 77f6a6aad1
commit b582e28d80
38 changed files with 832 additions and 1 deletions
+3
View File
@@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+26
View File
@@ -0,0 +1,26 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="13">
<item index="0" class="java.lang.String" itemvalue="torchvision" />
<item index="1" class="java.lang.String" itemvalue="flash_attn" />
<item index="2" class="java.lang.String" itemvalue="numpy" />
<item index="3" class="java.lang.String" itemvalue="bitsandbytes" />
<item index="4" class="java.lang.String" itemvalue="bitsandbytes-0.43.3" />
<item index="5" class="java.lang.String" itemvalue="accelerate-0.33.0" />
<item index="6" class="java.lang.String" itemvalue="psutil-6.0.0" />
<item index="7" class="java.lang.String" itemvalue="transformers" />
<item index="8" class="java.lang.String" itemvalue="Pillow" />
<item index="9" class="java.lang.String" itemvalue="PyQt5" />
<item index="10" class="java.lang.String" itemvalue="accelerate" />
<item index="11" class="java.lang.String" itemvalue="torch" />
<item index="12" class="java.lang.String" itemvalue="psutil" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Comfyui_Snap-Processing.iml" filepath="$PROJECT_DIR$/.idea/Comfyui_Snap-Processing.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+159
View File
@@ -0,0 +1,159 @@
from PyQt5.QtCore import QEventLoop
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QImage
import time
import os
import random
import torch
from PIL import Image
import numpy as np
from io import BytesIO
from .ui.canvas_window import CanvasWindow
class PyQtCanvasNode:
@staticmethod
def INPUT_TYPES():
return {
"required": {
"image": ("IMAGE",), # Required image input
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("IMAGE", "INT", "STRING") # Added "STRING" to the return types
FUNCTION = "activate_pyqt"
CATEGORY = "Snap Processing"
def activate_pyqt(self, image, seed):
try:
# Use the user-provided seed value
print(f"Received seed: {seed}")
# Convert tensor to QImage
qimage = self.tensor_to_qimage(image)
print("Converted tensor to QImage")
# Run PyQt GUI and block the main thread until the user finishes
modified_image = self.run_pyqt_gui(qimage)
print("PyQt GUI closed")
# Convert the modified QImage to a NumPy array
numpy_array = self.qimage_to_numpy(modified_image)
print("Converted QImage to NumPy array")
# Define the save directory
save_directory = os.path.join(os.getcwd(), "ComfyUI", "custom_nodes", "Comfyui_Snap-Processing", "save")
if not os.path.exists(save_directory):
os.makedirs(save_directory)
# Create a unique file name using the seed and current timestamp
timestamp = int(time.time())
png_image_filename = f"output_image_seed{seed}_{timestamp}.png"
png_image_path = os.path.join(save_directory, png_image_filename)
self.numpy_to_png(numpy_array, png_image_path)
print(f"Saved image to {png_image_path}")
# Get the absolute path for consistency
absolute_png_image_path = os.path.abspath(png_image_path)
# Return the modified image, seed, and the file path
return (self.qimage_to_tensor(modified_image), seed, absolute_png_image_path)
except Exception as e:
print(f"Error in activate_pyqt: {e}")
raise e
def run_pyqt_gui(self, input_image):
try:
app = QApplication.instance()
if app is None:
app = QApplication([]) # Create the global application
# Always create a new CanvasWindow instance
dialog = CanvasWindow(input_image)
dialog.save_signal.connect(self.on_save)
dialog.close_signal.connect(self.on_close)
print("PyQt CanvasWindow shown")
dialog.exec_() # Block until the dialog is closed
print("Dialog closed")
modified_image = dialog.get_modified_image()
dialog.deleteLater() # Ensure the window is properly destroyed
print("CanvasWindow deleted")
return modified_image
except Exception as e:
print(f"Error in run_pyqt_gui: {e}")
raise e
def on_save(self):
print("Image saved successfully")
def on_close(self):
print("Window closed")
def tensor_to_qimage(self, tensor):
if tensor.ndim == 4:
tensor = tensor.squeeze(0)
if tensor.ndim != 3:
raise ValueError(f"Expected tensor with 3 dimensions, but got {tensor.ndim} dimensions.")
if tensor.shape[0] in [1, 3, 4]:
pass
elif tensor.shape[2] in [1, 3, 4]:
tensor = tensor.permute(2, 0, 1)
else:
raise ValueError("Tensor has unsupported shape.")
num_channels = tensor.shape[0]
if num_channels == 1:
tensor = tensor.repeat(3, 1, 1)
elif num_channels == 2:
tensor = torch.cat([tensor, tensor[0:1, :, :]], dim=0)
elif num_channels == 4:
tensor = tensor[:3, :, :]
elif num_channels != 3:
raise ValueError(f"Unsupported number of channels: {num_channels}")
image_data = tensor.mul(255).byte().cpu().numpy()
image_data = image_data.transpose(1, 2, 0)
pil_image = Image.fromarray(image_data, 'RGB')
qimage = self.pil_image_to_qimage(pil_image)
return qimage
def qimage_to_tensor(self, qimage):
qimage = qimage.convertToFormat(QImage.Format_RGB888)
width = qimage.width()
height = qimage.height()
ptr = qimage.bits()
ptr.setsize(qimage.byteCount())
image_np = np.array(ptr).reshape((height, width, 3))
tensor = torch.from_numpy(image_np).permute(2, 0, 1).float() / 255.0
return tensor
def qimage_to_numpy(self, qimage):
qimage = qimage.convertToFormat(QImage.Format_RGB888)
width = qimage.width()
height = qimage.height()
ptr = qimage.bits()
ptr.setsize(qimage.byteCount())
image_np = np.array(ptr).reshape((height, width, 3))
return image_np
def numpy_to_png(self, numpy_array, save_path):
if numpy_array.dtype != np.uint8:
numpy_array = (numpy_array * 255).astype(np.uint8)
image = Image.fromarray(numpy_array)
image.save(save_path, format='PNG')
def pil_image_to_qimage(self, pil_image):
buffer = BytesIO()
pil_image.save(buffer, format="PNG")
image_bytes = buffer.getvalue()
qimage = QImage.fromData(image_bytes, "PNG")
return qimage
+80
View File
@@ -0,0 +1,80 @@
import torch
import hashlib
from pathlib import Path
from typing import Iterable
from PIL import Image, ImageOps
import numpy as np
import folder_paths
import os # 引入 os 库用于创建目录
class Snapload:
@classmethod
def INPUT_TYPES(s):
return {"required":
{"image": ("STRING", {"default": r"123"})},
}
CATEGORY = "Snap Processing"
RETURN_TYPES = ("IMAGE", "MASK")
FUNCTION = "load_image"
def load_image(self, image):
# 调用 _resolve_path 来获取图像路径
image_path = Snapload._resolve_path(image)
# 打开并处理图像
i = Image.open(image_path)
i = ImageOps.exif_transpose(i)
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
# 如果有 Alpha 通道,则生成掩码
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
# 否则生成一个全为零的掩码
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
return (image, mask)
# 将图像路径解析为相对路径,默认为指定的路径
def _resolve_path(image) -> Path:
# 如果未提供图像路径,则使用默认路径
if not image:
default_path = Path("ComfyUI/custom_nodes/Comfyui_Snap-Processing/save/output.png").resolve()
else:
default_path = Path(folder_paths.get_annotated_filepath(image)).resolve()
# 创建默认保存目录,如果不存在
save_directory = default_path.parent
if not save_directory.exists():
os.makedirs(save_directory)
# 返回默认路径或解析后的路径
return default_path
@classmethod
def IS_CHANGED(s, image):
# 获取图像路径并计算其哈希值,判断是否改变
image_path = Snapload._resolve_path(image)
m = hashlib.sha256()
with open(image_path, 'rb') as f:
m.update(f.read())
return m.digest().hex()
@classmethod
def VALIDATE_INPUTS(s, image):
# 如果图像为空,则返回 True
if image is None:
return True
# 验证图像路径是否存在
image_path = Snapload._resolve_path(image)
if not image_path.exists():
return "Invalid image path: {}".format(image_path)
return True
+6
View File
@@ -1,13 +1,19 @@
from .area_calculator import AreaCalculator
from .Snap_canvas import PyQtCanvasNode
from .Snapload import Snapload
VERSION = "1.0"
NODE_CLASS_MAPPINGS = {
"AreaCalculator": AreaCalculator,
"PyQtCanvasNode": PyQtCanvasNode,
"Snapload": Snapload,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AreaCalculator": "Snap Area",
"PyQtCanvasNode": "Snap Canvas",
"Snapload": "Snap load",
}
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

+303
View File
@@ -0,0 +1,303 @@
{
"last_node_id": 17,
"last_link_id": 32,
"nodes": [
{
"id": 3,
"type": "PreviewImage",
"pos": {
"0": 913.2920532226562,
"1": 353.52850341796875
},
"size": {
"0": 210,
"1": 246
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 2,
"label": "图像"
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 2,
"type": "LoadImage",
"pos": {
"0": 307.2921142578125,
"1": 162.52877807617188
},
"size": {
"0": 315,
"1": 314.00006103515625
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
1
],
"shape": 3,
"label": "图像"
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3,
"label": "遮罩"
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"1.png",
"image"
]
},
{
"id": 4,
"type": "Snapload",
"pos": {
"0": 663.2920532226562,
"1": 353.52850341796875
},
"size": {
"0": 210,
"1": 54
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "STRING",
"link": 3,
"widget": {
"name": "image"
},
"label": "image"
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
2
],
"slot_index": 0,
"shape": 3,
"label": "IMAGE"
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3,
"label": "MASK"
}
],
"properties": {
"Node name for S&R": "Snapload"
},
"widgets_values": [
"123"
]
},
{
"id": 1,
"type": "PyQtCanvasNode",
"pos": {
"0": 653.2920532226562,
"1": 163.52877807617188
},
"size": {
"0": 315,
"1": 122
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 1,
"slot_index": 0,
"label": "image"
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [],
"slot_index": 0,
"shape": 3,
"label": "IMAGE"
},
{
"name": "INT",
"type": "INT",
"links": null,
"slot_index": 1,
"shape": 3,
"label": "INT"
},
{
"name": "STRING",
"type": "STRING",
"links": [
3
],
"slot_index": 2,
"shape": 3,
"label": "STRING"
}
],
"properties": {
"Node name for S&R": "PyQtCanvasNode"
},
"widgets_values": [
867571087443497,
"randomize"
]
}
],
"links": [
[
1,
2,
0,
1,
0,
"IMAGE"
],
[
2,
4,
0,
3,
0,
"IMAGE"
],
[
3,
1,
2,
4,
0,
"STRING"
],
[
23,
10,
2,
13,
1,
"VAE"
],
[
24,
10,
1,
15,
0,
"CLIP"
],
[
25,
10,
1,
16,
0,
"CLIP"
],
[
26,
11,
0,
5,
0,
"MODEL"
],
[
27,
10,
2,
5,
1,
"VAE"
],
[
28,
10,
2,
13,
1,
"VAE"
],
[
29,
10,
1,
15,
0,
"CLIP"
],
[
30,
10,
1,
16,
0,
"CLIP"
],
[
31,
11,
0,
5,
0,
"MODEL"
],
[
32,
10,
2,
5,
1,
"VAE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.8769226950000005,
"offset": [
-155.482269502211,
130.50066223270278
]
}
},
"version": 0.4
}
+1 -1
View File
@@ -1 +1 @@
torch
pyqt5
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
+226
View File
@@ -0,0 +1,226 @@
import os
import time
from PyQt5.QtWidgets import (
QDialog, QPushButton, QVBoxLayout, QLineEdit, QHBoxLayout,
QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGraphicsItem
)
from PyQt5.QtGui import QPixmap, QImage, QColor, QPainter
from PyQt5.QtCore import pyqtSignal, Qt, QRectF
class CanvasWindow(QDialog):
save_signal = pyqtSignal()
close_signal = pyqtSignal()
def __init__(self, input_image):
super().__init__()
self.setWindowTitle("PyQt5 Image Canvas")
self.input_image = input_image
self.canvas_width = 512
self.canvas_height = 512
self.modified_image = None
self.scene = QGraphicsScene()
self.view = QGraphicsView(self.scene)
self.view.setStyleSheet("border: 1px solid black;")
self.view.setFixedSize(self.canvas_width + 2, self.canvas_height + 2)
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.canvas = QImage(self.canvas_width, self.canvas_height, QImage.Format_RGB888)
self.canvas.fill(QColor('white'))
self.canvas_pixmap_item = QGraphicsPixmapItem(QPixmap.fromImage(self.canvas))
self.scene.addItem(self.canvas_pixmap_item)
scaled_pixmap = QPixmap.fromImage(self.input_image).scaled(
self.canvas_width, self.canvas_height, Qt.KeepAspectRatio, Qt.SmoothTransformation
)
self.input_pixmap_item = ResizablePixmapItem(scaled_pixmap, self.canvas_width, self.canvas_height)
self.scene.addItem(self.input_pixmap_item)
save_button = QPushButton("Save and Close", self)
save_button.clicked.connect(self.save_and_close)
self.canvas_width_input = QLineEdit(self)
self.canvas_width_input.setPlaceholderText("Canvas Width")
self.canvas_width_input.setText(str(self.canvas_width))
self.canvas_height_input = QLineEdit(self)
self.canvas_height_input.setPlaceholderText("Canvas Height")
self.canvas_height_input.setText(str(self.canvas_height))
size_button = QPushButton("Set Canvas Size", self)
size_button.clicked.connect(self.set_canvas_size)
hbox = QHBoxLayout()
hbox.addWidget(self.canvas_width_input)
hbox.addWidget(self.canvas_height_input)
hbox.addWidget(size_button)
vbox = QVBoxLayout()
vbox.addWidget(self.view)
vbox.addLayout(hbox)
vbox.addWidget(save_button)
self.setLayout(vbox)
self.scene.setSceneRect(0, 0, self.canvas_width, self.canvas_height)
self.view.setSceneRect(0, 0, self.canvas_width, self.canvas_height)
def save_and_close(self):
self.save_image()
self.accept()
def save_image(self):
image = QImage(self.canvas_width, self.canvas_height, QImage.Format_RGB888)
image.fill(QColor('white'))
painter = QPainter(image)
target_rect = QRectF(0, 0, self.canvas_width, self.canvas_height)
source_rect = self.scene.sceneRect()
self.scene.render(painter, target_rect, source_rect)
painter.end()
self.modified_image = image
save_directory = os.path.join(os.getcwd(), "ComfyUI", "custom_nodes", "Comfyui_Snap-Processing", "save")
if not os.path.exists(save_directory):
os.makedirs(save_directory)
timestamp = int(time.time() * 1000)
save_path = os.path.join(save_directory, "output.png")
image.save(save_path)
self.save_signal.emit()
def closeEvent(self, event):
self.close_signal.emit()
event.accept()
def set_canvas_size(self):
try:
width = int(self.canvas_width_input.text())
height = int(self.canvas_height_input.text())
except ValueError:
return
self.canvas_width = width
self.canvas_height = height
self.canvas = QImage(self.canvas_width, self.canvas_height, QImage.Format_RGB888)
self.canvas.fill(QColor('white'))
self.canvas_pixmap_item.setPixmap(QPixmap.fromImage(self.canvas))
self.scene.setSceneRect(0, 0, self.canvas_width, self.canvas_height)
self.view.setSceneRect(0, 0, self.canvas_width, self.canvas_height)
self.view.setFixedSize(self.canvas_width + 2, self.canvas_height + 2)
def get_modified_image(self):
return self.modified_image
class ResizablePixmapItem(QGraphicsPixmapItem):
def __init__(self, pixmap, canvas_width, canvas_height):
super().__init__(pixmap)
self.canvas_width = canvas_width
self.canvas_height = canvas_height
self.original_pixmap = pixmap
self.setFlags(
QGraphicsItem.ItemIsSelectable |
QGraphicsItem.ItemIsMovable |
QGraphicsItem.ItemSendsGeometryChanges
)
self.setAcceptHoverEvents(True)
self.is_resizing = False
self.resize_handle_size = 10
self.aspect_ratio = pixmap.width() / pixmap.height()
self.setCursor(Qt.ArrowCursor)
self.min_scale = 0.1
self.max_scale = 10
def hoverMoveEvent(self, event):
if self.is_in_resize_area(event.pos()):
self.setCursor(Qt.SizeFDiagCursor)
else:
self.setCursor(Qt.ArrowCursor)
super().hoverMoveEvent(event)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton and self.is_in_resize_area(event.pos()):
self.is_resizing = True
self.resize_start_pos = event.pos()
event.accept()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.is_resizing:
offset = event.pos() - self.resize_start_pos
self.resize_image(offset)
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.is_resizing:
self.is_resizing = False
event.accept()
else:
super().mouseReleaseEvent(event)
def boundingRect(self):
return QRectF(0, 0, self.pixmap().width(), self.pixmap().height())
def resize_image(self, offset):
scale_factor = 0.1
new_width = self.boundingRect().width() + offset.x() * scale_factor
new_height = new_width / self.aspect_ratio
self.prepareGeometryChange()
if 10 < new_width <= self.canvas_width * 2 and 10 < new_height <= self.canvas_height * 2:
scaled_pixmap = self.original_pixmap.scaled(
int(new_width), int(new_height),
Qt.KeepAspectRatio, Qt.SmoothTransformation
)
self.setPixmap(scaled_pixmap)
self.update()
self.prepareGeometryChange()
def is_in_resize_area(self, pos):
rect = self.boundingRect()
resize_rect = QRectF(
rect.right() - self.resize_handle_size,
rect.bottom() - self.resize_handle_size,
self.resize_handle_size,
self.resize_handle_size
)
return resize_rect.contains(pos)
def wheelEvent(self, event):
try:
if hasattr(event, 'angleDelta'):
delta = event.angleDelta().y()
elif hasattr(event, 'delta'):
delta = event.delta()
else:
delta = 0
factor = 1.1 if delta > 0 else 0.9
self.resize_with_factor(factor)
except AttributeError:
pass
def resize_with_factor(self, factor):
new_width = self.boundingRect().width() * factor
new_height = new_width / self.aspect_ratio
if self.min_scale * self.canvas_width <= new_width <= self.max_scale * self.canvas_width and \
self.min_scale * self.canvas_height <= new_height <= self.max_scale * self.canvas_height:
scaled_pixmap = self.original_pixmap.scaled(
int(new_width), int(new_height), Qt.KeepAspectRatio, Qt.SmoothTransformation
)
self.setPixmap(scaled_pixmap)