Skip to content

Commit 57f80af

Browse files
committed
add painter node
1 parent 3ebe1ac commit 57f80af

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

comfy_extras/nodes_painter.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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, UI
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="Painter",
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",
41+
default="",
42+
socketless=True,
43+
extra_dict={"widgetType": "PAINTER", "image_upload": True},
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, 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 and mask.strip():
90+
mask_path = folder_paths.get_annotated_filepath(mask)
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, ui=UI.PreviewImage(out_image))
111+
112+
@classmethod
113+
def fingerprint_inputs(cls, mask, width, height, bg_color="#000000", image=None):
114+
if mask and mask.strip():
115+
mask_path = folder_paths.get_annotated_filepath(mask)
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+
124+
125+
class PainterExtension(ComfyExtension):
126+
@override
127+
async def get_node_list(self):
128+
return [PainterNode]
129+
130+
131+
async def comfy_entrypoint():
132+
return PainterExtension()

nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2448,6 +2448,7 @@ async def init_builtin_extra_nodes():
24482448
"nodes_toolkit.py",
24492449
"nodes_replacements.py",
24502450
"nodes_nag.py",
2451+
"nodes_painter.py",
24512452
]
24522453

24532454
import_failed = []

0 commit comments

Comments
 (0)