Add a description of what you changed
@@ -0,0 +1,3 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
After Width: | Height: | Size: 68 KiB |
@@ -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 @@
|
||||
torch
|
||||
pyqt5
|
||||
|
||||
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -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)
|
||||