Skip to content

Commit 815b2f9

Browse files
committed
dither
1 parent 8f0ab85 commit 815b2f9

File tree

3 files changed

+100
-14
lines changed

3 files changed

+100
-14
lines changed

__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
from .nodes.image_utility import ImagesIndexNode
3434
from .nodes.image_utility import ImagesCatNode
3535
from .nodes.image_utility import ImagesInfoNode
36+
from .nodes.image_utility import DitherNode
37+
3638

3739
from .nodes.utility import PrintAnyNode
3840
from .nodes.utility import PrintImageNode
@@ -102,6 +104,7 @@
102104
"ImagesInfoNode": ImagesInfoNode,
103105
"Base64ToImageNode": Base64ToImageNode,
104106
"ASCIICharNode": ASCIICharNode,
107+
"DitherNode": DitherNode,
105108
}
106109

107110
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -153,6 +156,7 @@
153156
"ImagesCatNode": "Images Cat",
154157
"ImagesInfoNode": "Images Info",
155158
"ASCIICharNode": "ASCII Art Text",
159+
"DitherNode": "Dither",
156160
}
157161

158162

nodes/image_utility.py

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,30 +62,30 @@ def pil2tensor_mask(pil):
6262
toTensor = transforms.ToTensor()
6363

6464

65-
def pil2tensor(pil):
65+
def pil_to_tensor(pil):
6666
# [C, H, W] to [H, W, C]
6767
return toTensor(pil).permute(1, 2, 0)
6868

6969

70-
def tensor2pil(tensor):
70+
def tensor_to_pil(tensor):
7171
# [H, W, C] to [C, H, W]
7272
return toPIL(tensor.permute(2, 0, 1))
7373

7474

75-
def tensor2batch(tensor, h, w, c):
75+
def tensor_to_batch(tensor, h, w, c):
7676
tensor = torch.cat(tensor)
7777
tensor = tensor.reshape(-1, h, w, c)
7878
return tensor
7979

8080

81-
def batch2list(tensor_batch):
81+
def batch_to_list(tensor_batch):
8282
tensors = []
8383
for i in range(tensor_batch.shape[0]):
8484
tensors.append(tensor_batch[i])
8585
return tensors
8686

8787

88-
def list2batch(tensor_list):
88+
def list_to_batch(tensor_list):
8989
return torch.cat(tensor_list)
9090

9191

@@ -102,6 +102,14 @@ def load_pil_from_url(url):
102102
return pil, name
103103

104104

105+
def pil_to_np(pilimage):
106+
return np.array(pilimage) / 255
107+
108+
109+
def np_to_pil(image):
110+
return Image.fromarray((image * 255).astype("uint8"))
111+
112+
105113
class DownloadImageNode:
106114
def __init__(self):
107115
self.output_dir = folder_paths.get_temp_directory()
@@ -180,7 +188,7 @@ def INPUT_TYPES(cls):
180188

181189
def function(self, image, directory, name, type, compress_level):
182190
for i in image:
183-
pil = tensor2pil(i)
191+
pil = tensor_to_pil(i)
184192
name = f"{name}.{type}"
185193
pil.save(os.path.join(directory, name), compress_level=compress_level)
186194

@@ -371,7 +379,7 @@ def INPUT_TYPES(cls):
371379
def node_function(self, url, channels):
372380
pil, name = load_pil_from_url(url)
373381
pil.convert(channels)
374-
img_out = pil2tensor(pil)
382+
img_out = pil_to_tensor(pil)
375383
return (img_out, img_out, name)
376384

377385

@@ -403,7 +411,7 @@ def load_image_to_tensor(directory, recursive, channels):
403411
elif pil.mode == "RGB" and channels == "RGBA":
404412
pil = pil.convert("RGBA")
405413

406-
image_tensor = pil2tensor(pil)
414+
image_tensor = pil_to_tensor(pil)
407415
image_tensor = image_tensor.unsqueeze(0) # Add batch dimension
408416
image_tensors.append(image_tensor)
409417

@@ -472,7 +480,7 @@ def node_function(self, directory, recursive, channels):
472480
raise Exception("folder_path is not valid: " + directory)
473481

474482
(out_image, out_dir, out_name) = load_image_to_tensor(directory, recursive, channels)
475-
out_image = list2batch(out_image)
483+
out_image = list_to_batch(out_image)
476484

477485
return (out_image, out_dir, out_name)
478486

@@ -500,7 +508,7 @@ def INPUT_TYPES(cls):
500508

501509
def fill_alpha(self, tensor, alpha_threshold, fill_color):
502510

503-
pil = tensor2pil(tensor)
511+
pil = tensor_to_pil(tensor)
504512
if pil.mode != "RGBA":
505513
raise Exception("Image mode is not RGBA")
506514

@@ -516,7 +524,7 @@ def fill_alpha(self, tensor, alpha_threshold, fill_color):
516524

517525
new_pil = Image.new("RGB", pil.size)
518526
new_pil.putdata(new_pixels)
519-
image_tensor = pil2tensor(new_pil)
527+
image_tensor = pil_to_tensor(new_pil)
520528
return image_tensor
521529

522530
def node_function(self, image, alpha_threshold, r, g, b):
@@ -558,7 +566,7 @@ def INPUT_TYPES(cls):
558566
CATEGORY = "Fair/image"
559567

560568
def function(self, image):
561-
pil = tensor2pil(image)
569+
pil = tensor_to_pil(image)
562570
encoded_base64 = pil_to_base64(pil)
563571
return (encoded_base64,)
564572

@@ -602,7 +610,7 @@ def INPUT_TYPES(cls):
602610

603611
def function(self, string):
604612
pil = base64_to_pil(string)
605-
image = pil2tensor(pil)
613+
image = pil_to_tensor(pil)
606614
return (image,)
607615

608616

@@ -800,3 +808,77 @@ def INPUT_TYPES(cls):
800808
def node_function(self, images):
801809
width, height, length = (images.shape[2], images.shape[1], images.shape[0])
802810
return (width, height, length)
811+
812+
813+
class DitherNode:
814+
def __init__(self):
815+
pass
816+
817+
@classmethod
818+
def INPUT_TYPES(cls):
819+
return {
820+
"required": {
821+
"images": (IO.IMAGE, {"defaultInput": True}),
822+
"dither": (["Modulation", "Floyd–Steinberg", "Halftone"], {"default": "Modulation"}),
823+
}
824+
}
825+
826+
FUNCTION = "node_function"
827+
CATEGORY = "Fair/image"
828+
RETURN_TYPES = (IO.IMAGE,)
829+
RETURN_NAMES = ("images",)
830+
831+
def floyd_steinberg(self, image):
832+
# image: np.array of shape (height, width), dtype=float, 0.0-1.0
833+
# works in-place!
834+
h, w = image.shape
835+
for y in range(h):
836+
for x in range(w):
837+
old = image[y, x]
838+
new = np.round(old)
839+
image[y, x] = new
840+
error = old - new
841+
# precomputing the constants helps
842+
if x + 1 < w:
843+
image[y, x + 1] += error * 0.4375 # right, 7 / 16
844+
if (y + 1 < h) and (x + 1 < w):
845+
image[y + 1, x + 1] += error * 0.0625 # right, down, 1 / 16
846+
if y + 1 < h:
847+
image[y + 1, x] += error * 0.3125 # down, 5 / 16
848+
if (x - 1 >= 0) and (y + 1 < h):
849+
image[y + 1, x - 1] += error * 0.1875 # left, down, 3 / 16
850+
return image
851+
852+
def modulation(self, image):
853+
# image: np.array of shape (height, width), dtype=float, 0.0-1.0
854+
# works in-place!
855+
h, w = image.shape
856+
for y in range(h):
857+
for x in range(w):
858+
old = image[y, x]
859+
new = np.round(old)
860+
image[y, x] = new
861+
error = old - new
862+
# precomputing the constants helps
863+
if x + 1 < w:
864+
image[y, x + 1] += error * 1 # right
865+
return image
866+
867+
def ErrorDiffusionDithering(self, pil):
868+
# 转换为黑白255
869+
paletted = pil.convert("1", matrix=Image.FLOYDSTEINBERG)
870+
paletted = paletted.convert("RGB")
871+
return paletted
872+
873+
def node_function(self, images, dither):
874+
out_images = []
875+
876+
for image in images:
877+
pil = tensor_to_pil(image).convert("L")
878+
img_np = pil_to_np(pil)
879+
img_np = self.modulation(img_np)
880+
image = torch.from_numpy(img_np)
881+
out_images.append(image)
882+
883+
out_images = torch.stack(out_images)
884+
return (out_images,)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Python Script
6363
6464
Load LoRA Dual
6565
"""
66-
version = "1.0.69"
66+
version = "1.0.70"
6767
license = { file = "LICENSE" }
6868
dependencies = [
6969
"googletrans",

0 commit comments

Comments
 (0)