Skip to content

Commit 7a39fa9

Browse files
committed
add painter node
1 parent 873de5f commit 7a39fa9

File tree

2 files changed

+140
-1
lines changed

2 files changed

+140
-1
lines changed

comfy_extras/nodes_painter.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from __future__ import annotations
2+
3+
import hashlib
4+
import os
5+
6+
import numpy as np
7+
import torch
8+
from PIL import Image
9+
10+
import folder_paths
11+
import node_helpers
12+
from comfy_api.latest import ComfyExtension, io
13+
from typing_extensions import override
14+
15+
16+
def hex_to_rgb(hex_color: str) -> tuple[float, float, float]:
17+
hex_color = hex_color.lstrip("#")
18+
if len(hex_color) != 6:
19+
return (0.0, 0.0, 0.0)
20+
r = int(hex_color[0:2], 16) / 255.0
21+
g = int(hex_color[2:4], 16) / 255.0
22+
b = int(hex_color[4:6], 16) / 255.0
23+
return (r, g, b)
24+
25+
26+
class PainterNode(io.ComfyNode):
27+
@classmethod
28+
def define_schema(cls):
29+
return io.Schema(
30+
node_id="PainterNode",
31+
display_name="Painter",
32+
category="image",
33+
inputs=[
34+
io.Image.Input(
35+
"image",
36+
optional=True,
37+
tooltip="Optional base image to paint over",
38+
),
39+
io.String.Input(
40+
"mask_filename",
41+
default="",
42+
socketless=True,
43+
extra_dict={"widgetType": "PAINTER"},
44+
),
45+
io.Int.Input(
46+
"width",
47+
default=512,
48+
min=64,
49+
max=4096,
50+
step=64,
51+
socketless=True,
52+
extra_dict={"hidden": True},
53+
),
54+
io.Int.Input(
55+
"height",
56+
default=512,
57+
min=64,
58+
max=4096,
59+
step=64,
60+
socketless=True,
61+
extra_dict={"hidden": True},
62+
),
63+
io.String.Input(
64+
"bg_color",
65+
default="#000000",
66+
socketless=True,
67+
extra_dict={"hidden": True, "widgetType": "COLOR"},
68+
),
69+
],
70+
outputs=[
71+
io.Image.Output("IMAGE"),
72+
io.Mask.Output("MASK"),
73+
],
74+
)
75+
76+
@classmethod
77+
def execute(cls, mask_filename, width, height, bg_color="#000000", image=None) -> io.NodeOutput:
78+
if image is not None:
79+
h, w = image.shape[1], image.shape[2]
80+
base_image = image
81+
else:
82+
h, w = height, width
83+
r, g, b = hex_to_rgb(bg_color)
84+
base_image = torch.zeros((1, h, w, 3), dtype=torch.float32)
85+
base_image[0, :, :, 0] = r
86+
base_image[0, :, :, 1] = g
87+
base_image[0, :, :, 2] = b
88+
89+
if mask_filename and mask_filename.strip():
90+
mask_path = folder_paths.get_annotated_filepath(mask_filename)
91+
painter_img = node_helpers.pillow(Image.open, mask_path)
92+
painter_img = painter_img.convert("RGBA")
93+
94+
if painter_img.size != (w, h):
95+
painter_img = painter_img.resize((w, h), Image.LANCZOS)
96+
97+
painter_np = np.array(painter_img).astype(np.float32) / 255.0
98+
painter_rgb = painter_np[:, :, :3]
99+
painter_alpha = painter_np[:, :, 3:4]
100+
101+
mask_tensor = torch.from_numpy(painter_np[:, :, 3]).unsqueeze(0)
102+
103+
base_np = base_image[0].cpu().numpy()
104+
composited = painter_rgb * painter_alpha + base_np * (1.0 - painter_alpha)
105+
out_image = torch.from_numpy(composited).unsqueeze(0)
106+
else:
107+
mask_tensor = torch.zeros((1, h, w), dtype=torch.float32)
108+
out_image = base_image
109+
110+
return io.NodeOutput(out_image, mask_tensor)
111+
112+
@classmethod
113+
def fingerprint_inputs(cls, mask_filename, width, height, bg_color="#000000", image=None):
114+
if mask_filename and mask_filename.strip():
115+
mask_path = folder_paths.get_annotated_filepath(mask_filename)
116+
if os.path.exists(mask_path):
117+
m = hashlib.sha256()
118+
with open(mask_path, "rb") as f:
119+
m.update(f.read())
120+
return m.digest().hex()
121+
return ""
122+
123+
@classmethod
124+
def validate_inputs(cls, mask_filename, width, height, bg_color="#000000", image=None):
125+
if mask_filename and mask_filename.strip():
126+
if not folder_paths.exists_annotated_filepath(mask_filename):
127+
return "Invalid mask file: {}".format(mask_filename)
128+
return True
129+
130+
131+
class PainterExtension(ComfyExtension):
132+
@override
133+
async def get_node_list(self):
134+
return [PainterNode]
135+
136+
137+
async def comfy_entrypoint():
138+
return PainterExtension()

nodes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2433,7 +2433,8 @@ async def init_builtin_extra_nodes():
24332433
"nodes_image_compare.py",
24342434
"nodes_zimage.py",
24352435
"nodes_lora_debug.py",
2436-
"nodes_color.py"
2436+
"nodes_color.py",
2437+
"nodes_painter.py"
24372438
]
24382439

24392440
import_failed = []

0 commit comments

Comments
 (0)