6 Commits
Author SHA1 Message Date
Rayko 45a83c260a Update pyproject.toml 2026-11-09 20:15:42 +03:00
Rayko 567a99e601 Update package.json 2026-11-09 20:15:28 +03:00
Rayko 2e67398dc7 Update __init__.py 2026-11-09 20:15:08 +03:00
Rayko e8c07160fa Update spline_mask.js 2026-11-09 20:14:27 +03:00
Rayko b9d09c4ec6 Update Rayko_Spline_Mask.py 2026-11-09 20:13:29 +03:00
Rayko 0dd7df8f65 Update README.md 2026-11-09 20:12:00 +03:00
6 changed files with 41 additions and 36 deletions
+2 -2
View File
@@ -1192,7 +1192,7 @@ Detailed inspection of image differences at various zoom levels
# 🦊 RS Load Image
**A node for load image and creating a spline mask**
<img width="800" height="715" alt="RS Load Image1" src="https://github.com/user-attachments/assets/c5150b6a-88b6-4e2a-86fa-7cd70346cab8" />
<img width="1041" height="747" alt="Screenshot_4" src="https://github.com/user-attachments/assets/35f3b53d-d569-443b-b391-5a334625165d" />
### 🔥 Features
The node has the functionality of the native Load Image node, but instead of the native mask editor, a built-in spline editor is used. More accurate selection based on the principle of the Lasso tool from Photoshop.
@@ -1201,7 +1201,7 @@ The node has the functionality of the native Load Image node, but instead of the
The node is ready for use immediately after it is added. Images are added using the "🎨 IMAGE" (images from the input folder) and "🖼️ UPLOAD IMAGE" (images from any folder on your PC) buttons. You can scale the node to a convenient size to more accurately place the points of the spline. Incorrectly positioned points can be deleted by left-clicking with the CTRL key held down. To remove all points from the preview area, click the "🔴 CLEAR POINTS" button.
### ↔️ Inputs and Outputs:
The IMAGE output returns the original image unchanged. The MASK output returns a black and white mask where the white area corresponds to the drawn polygon.
The IMAGE output returns the original image unchanged. The MASK output returns a black and white mask where the white area corresponds to the drawn polygon. The WIDTH and HEIGHT output the dimensions of the uploaded image in INT.
</details>
<details>
+23 -19
View File
@@ -39,20 +39,19 @@ class RaykoSplineMask:
}
return inputs
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("image", "mask")
RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT")
RETURN_NAMES = ("image", "mask", "width", "height")
FUNCTION = "create_mask"
CATEGORY = "🦊 RaykoStudio"
OUTPUT_NODE = True
DESCRIPTION = "A node for load image and creating a spline mask"
def create_mask(self, image, coordinates="[]"):
img_path = None
img_tensor = None
if image and isinstance(image, str) and image.strip():
input_dir = folder_paths.get_input_directory()
if "/" in image:
parts = image.split("/")
subfolder = parts[0]
@@ -60,10 +59,10 @@ class RaykoSplineMask:
full_path = os.path.join(input_dir, subfolder, filename)
else:
full_path = os.path.join(input_dir, image)
if os.path.exists(full_path):
img_path = full_path
if img_path and os.path.exists(img_path):
try:
i = Image.open(img_path)
@@ -74,32 +73,37 @@ class RaykoSplineMask:
if img_tensor is None:
h, w = 512, 512
return (torch.zeros((1, h, w, 3)), torch.zeros((1, h, w)))
return (
torch.zeros((1, h, w, 3)),
torch.zeros((1, h, w)),
int(w),
int(h),
)
h, w = img_tensor.shape[1], img_tensor.shape[2]
mask_np = np.zeros((h, w), dtype=np.float32)
try:
coords = json.loads(coordinates.replace("'", '"'))
if isinstance(coords, list) and len(coords) >= 3:
pts = [(max(0, min(int(float(p['x'])), w-1)),
pts = [(max(0, min(int(float(p['x'])), w-1)),
max(0, min(int(float(p['y'])), h-1))) for p in coords]
pil_mask = Image.new('L', (w, h), 0)
draw = ImageDraw.Draw(pil_mask)
draw.polygon(pts, fill=255)
mask_np = np.array(pil_mask).astype(np.float32) / 255.0
print(f"[SPLINE 🦊] Mask created with {len(pts)} points")
else:
print(f"[SPLINE 🦊] Less than 3 points - empty mask")
except Exception as e:
print(f"[SPLINE 🦊] Error parsing coordinates: {e}")
mask_tensor = torch.from_numpy(mask_np).unsqueeze(0)
return (img_tensor, mask_tensor)
return (img_tensor, mask_tensor, int(w), int(h))
if PromptServer is not None and web is not None:
@PromptServer.instance.routes.get("/rayko/spline/images")
@@ -113,11 +117,11 @@ if PromptServer is not None and web is not None:
if file.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp')):
rel_path = os.path.relpath(os.path.join(root, file), input_dir)
images.append(rel_path.replace('\\', '/'))
return web.json_response({"images": images})
except Exception as e:
print(f"[SPLINE 🦊] API Error getting images: {e}")
return web.json_response({"images": []}, status=200)
NODE_CLASS_MAPPINGS = {"RaykoSplineMask": RaykoSplineMask}
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoSplineMask": "🦊 RS Load Image"}
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoSplineMask": "🦊 RS Load Image"}
+1 -1
View File
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
REQUIRED_MODULES: Set[str] = set()
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
__version__ = "0.51.3"
__version__ = "0.51.4"
def load_modules():
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ComfyUI_RaykoStudio",
"version": "0.51.3",
"version": "0.51.4",
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
"author": "Raykosan",
"license": "Apache License 2.0",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI_RaykoStudio"
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
version = "0.51.3"
version = "0.51.4"
license = { file = "LICENSE" }
dependencies = ["torch", "opencv-python", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"]
+13 -12
View File
@@ -17,8 +17,9 @@ app.registerExtension({
spline_coords: "[]"
};
// Чуть увеличиваем высоту, чтобы вместить 4 слота + виджеты + кнопки
node.targetWidth = 320;
node.setSize([320, 410]);
node.setSize([320, 440]);
const imageWidget = node.widgets?.find(w => w.name === "image");
const coordsWidget = node.widgets?.find(w => w.name === "coordinates");
@@ -48,7 +49,6 @@ app.registerExtension({
node.buttons = [];
node.imageList = [];
// Новая функция для создания полной маски
node.fillFullMask = function() {
if (!node.imageLoaded || !node.image) return;
@@ -494,7 +494,7 @@ app.registerExtension({
const padding = 15;
const gap = 10;
const btnW = (this.size[0] - 2 * padding - 3 * gap) / 4; // Пересчет на 4 кнопки
const btnW = (this.size[0] - 2 * padding - 3 * gap) / 4;
const btnH = 28;
const btnYOffset = 45;
@@ -539,18 +539,21 @@ app.registerExtension({
const nodeScreenX = canvasRect.left + ((graphX + ds.offset[0]) * scale);
const nodeScreenY = canvasRect.top + ((graphY + ds.offset[1]) * scale);
// Считаем высоту абсолютно всех виджетов (даже скрытых)
let widgetsHeight = 0;
if (node.widgets) {
for (const w of node.widgets) {
if (!w.hidden) widgetsHeight += 20;
widgetsHeight += 20;
}
}
const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30;
// Оставляем стандартные отступы, чтобы картинка занимала всю ширину
const leftRightPadding = 10;
const topPadding = 18;
const bottomPadding = 10;
const footerHeight = 50;
const footerHeight = 60; // Выделяем место под кнопки в самом низу
const availableHeight = (node.size[1] * scale) - (titleBarHeight * scale) - (widgetsHeight * scale) - (footerHeight * scale) - (topPadding * scale) - (bottomPadding * scale);
const availableWidth = (node.size[0] * scale) - (leftRightPadding * 2 * scale);
@@ -738,7 +741,7 @@ app.registerExtension({
let widgetsHeight = 0;
if (this.widgets) {
for (const w of this.widgets) {
if (!w.hidden) widgetsHeight += 20;
widgetsHeight += 20;
}
}
@@ -746,11 +749,10 @@ app.registerExtension({
const leftRightPadding = 10;
const topPadding = 18;
const bottomPadding = 10;
const footerHeight = 50;
const footerHeight = 60; // Выделяем место под кнопки
const btnH = 28;
const btnY = h - 45;
// Пересчет ширины кнопок на 4 элемента
const btnPadding = 15;
const btnGap = 10;
const btnW = (w - 2 * btnPadding - 3 * btnGap) / 4;
@@ -809,10 +811,9 @@ app.registerExtension({
const radius = 4;
const x = btn.x, y = btn.y, w = btn.w, h = btn.h;
// Ховер для бледной кнопки FULL
let fillColor = btn.color;
if (btn.label === "✔️ FULL" && btn.hover) {
fillColor = "#4CAF50"; // Полная яркость при наведении
fillColor = "#4CAF50";
}
ctx.fillStyle = btn.hover ? "#444" : "#2a2a2a";
@@ -867,7 +868,7 @@ app.registerExtension({
node.onResize = function(size) {
if (size[0] < 320) size[0] = 320;
if (size[1] < 410) size[1] = 410;
if (size[1] < 440) size[1] = 440;
_lastRect = null;
syncPosition();
@@ -924,4 +925,4 @@ app.registerExtension({
},
setup() {}
});
});