Skip to content

Commit c4063aa

Browse files
committed
add automatic marking nodes
1 parent 93dd4c8 commit c4063aa

3 files changed

Lines changed: 431 additions & 0 deletions

File tree

__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
segment_anything,
2222
showcase,
2323
supernode,
24+
nodes_automatic_marking,
2425
)
2526

2627

@@ -36,6 +37,7 @@ def update_mappings(module):
3637
update_mappings(nodes_controlnet_union_sdxl)
3738
update_mappings(mzkolors)
3839
update_mappings(segment_anything)
40+
update_mappings(nodes_automatic_marking)
3941

4042
try:
4143
import bizy_server

nodes_automatic_marking.py

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
import os
2+
from concurrent.futures import ThreadPoolExecutor
3+
import numpy as np
4+
import torch
5+
import os
6+
7+
from PIL import Image, ImageOps
8+
9+
import folder_paths
10+
11+
# from .llm import BizyAirJoyCaption2
12+
from .nodes_automatic_marking_utils import joycaption2
13+
14+
class BizyAirMultiJoyCaption2:
15+
@classmethod
16+
def INPUT_TYPES(s):
17+
return {
18+
"required": {
19+
"image": ("IMAGE",),
20+
"do_sample": ([True, False],),
21+
"temperature": (
22+
"FLOAT",
23+
{
24+
"default": 0.5,
25+
"min": 0.0,
26+
"max": 2.0,
27+
"step": 0.01,
28+
"round": 0.001,
29+
"display": "number",
30+
},
31+
),
32+
"max_tokens": (
33+
"INT",
34+
{
35+
"default": 256,
36+
"min": 16,
37+
"max": 512,
38+
"step": 16,
39+
"display": "number",
40+
},
41+
),
42+
"caption_type": (
43+
[
44+
"Descriptive",
45+
"Descriptive (Informal)",
46+
"Training Prompt",
47+
"MidJourney",
48+
"Booru tag list",
49+
"Booru-like tag list",
50+
"Art Critic",
51+
"Product Listing",
52+
"Social Media Post",
53+
],
54+
),
55+
"caption_length": (
56+
["any", "very short", "short", "medium-length", "long", "very long"]
57+
+ [str(i) for i in range(20, 261, 10)],
58+
),
59+
"extra_options": (
60+
"STRING",
61+
{
62+
"default": "If there is a person/character in the image you must refer to them as {name}.",
63+
"tooltip": "Extra options for the model",
64+
"multiline": True,
65+
},
66+
),
67+
"name_input": (
68+
"STRING",
69+
{
70+
"default": "Jack",
71+
"tooltip": "Name input is only used if an Extra Option is selected that requires it.",
72+
},
73+
),
74+
"custom_prompt": (
75+
"STRING",
76+
{
77+
"default": "",
78+
"multiline": True,
79+
},
80+
),
81+
}
82+
}
83+
84+
RETURN_TYPES = ("STRING",)
85+
FUNCTION = "multi_joycaption"
86+
NODE_DISPLAY_NAME = "☁️BizyAir Multi Joy Caption"
87+
88+
def multi_joycaption(self, image, **kwargs):
89+
captions = []
90+
input_images = [img for img in image]
91+
92+
with ThreadPoolExecutor(max_workers=5) as executor:
93+
results = list(executor.map(lambda img: joycaption2(image=img.unsqueeze(0), **kwargs), input_images))
94+
95+
for i, result in enumerate(results):
96+
captions.append(result[0])
97+
# pbar.update_absolute(i + 1)
98+
combined_caption = " | ".join(captions)
99+
100+
return {"ui": {"text": (combined_caption,)}, "result": (combined_caption,)}
101+
102+
103+
class SaveCaptionsAndImages:
104+
@classmethod
105+
def INPUT_TYPES(s):
106+
return {
107+
"required": {
108+
"captions": ("STRING", {"multiline": True}),
109+
"images": ("IMAGE",),
110+
"directory_prefix": (
111+
"STRING",
112+
{"default": "lora_dataset", "multiline": False},
113+
),
114+
},
115+
}
116+
117+
RETURN_TYPES = ()
118+
OUTPUT_NODE = True
119+
FUNCTION = "apply"
120+
121+
def apply(self, captions, images, directory_prefix):
122+
123+
# Split the captions string into a list using " | " as the delimiter
124+
caption_list = captions.split(" | ")
125+
full_output_folder = folder_paths.get_output_directory()
126+
# Find the next available directory number
127+
i = 0
128+
while True:
129+
dir_path = os.path.join(full_output_folder, f"{directory_prefix}_{i:03d}")
130+
if not os.path.exists(dir_path):
131+
break
132+
i += 1
133+
# Validate input
134+
if len(caption_list) != len(images):
135+
raise ValueError(
136+
"The number of captions does not match the number of images."
137+
)
138+
139+
for batch_number, (image, caption) in enumerate(zip(images, caption_list)):
140+
# Generate a unique filename for each image
141+
filename = f"image_{batch_number:04d}"
142+
143+
# Generate file paths
144+
image_filepath = os.path.join(dir_path, f"{filename}.png")
145+
caption_filepath = os.path.join(dir_path, f"{filename}.txt")
146+
147+
# Ensure directory exists
148+
os.makedirs(dir_path, exist_ok=True)
149+
150+
# Save the image
151+
i = 255.0 * image.cpu().numpy()
152+
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
153+
img.save(image_filepath)
154+
155+
# Write caption to file
156+
with open(caption_filepath, "w", encoding="utf-8") as caption_file:
157+
caption_file.write(caption)
158+
159+
print(f"Image saved to: {image_filepath}")
160+
print(f"Caption saved to: {caption_filepath}")
161+
162+
return {}
163+
164+
class BizyAirLoadImagesFromFolder:
165+
@classmethod
166+
def INPUT_TYPES(s):
167+
return {
168+
"required": {
169+
"folder": ("STRING", {"default": ""}),
170+
"width": ("INT", {"default": 1024, "min": 64, "step": 1}),
171+
"height": ("INT", {"default": 1024, "min": 64, "step": 1}),
172+
"keep_aspect_ratio": (["crop", "pad", "stretch",],),
173+
},
174+
"optional": {
175+
"image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
176+
"start_index": ("INT", {"default": 0, "min": 0, "step": 1}),
177+
"include_subfolders": ("BOOLEAN", {"default": False}),
178+
}
179+
}
180+
181+
RETURN_TYPES = ("IMAGE", "MASK", "INT", "STRING",)
182+
RETURN_NAMES = ("image", "mask", "count", "image_path",)
183+
FUNCTION = "load_images"
184+
CATEGORY = "☁️BizyAir/marking"
185+
DESCRIPTION = """Loads images from a folder into a batch, images are resized and loaded into a batch."""
186+
187+
def load_images(self, folder, width, height, image_load_cap, start_index, keep_aspect_ratio, include_subfolders=False):
188+
if not os.path.isdir(folder):
189+
raise FileNotFoundError(f"Folder '{folder} cannot be found.'")
190+
191+
valid_extensions = ['.jpg', '.jpeg', '.png', '.webp']
192+
image_paths = []
193+
if include_subfolders:
194+
for root, _, files in os.walk(folder):
195+
for file in files:
196+
if any(file.lower().endswith(ext) for ext in valid_extensions):
197+
image_paths.append(os.path.join(root, file))
198+
else:
199+
for file in os.listdir(folder):
200+
if any(file.lower().endswith(ext) for ext in valid_extensions):
201+
image_paths.append(os.path.join(folder, file))
202+
203+
dir_files = sorted(image_paths)
204+
205+
if len(dir_files) == 0:
206+
raise FileNotFoundError(f"No files in directory '{folder}'.")
207+
208+
# start at start_index
209+
dir_files = dir_files[start_index:]
210+
211+
images = []
212+
masks = []
213+
image_path_list = []
214+
215+
limit_images = False
216+
if image_load_cap > 0:
217+
limit_images = True
218+
image_count = 0
219+
220+
for image_path in dir_files:
221+
if os.path.isdir(image_path):
222+
continue
223+
if limit_images and image_count >= image_load_cap:
224+
break
225+
i = Image.open(image_path)
226+
i = ImageOps.exif_transpose(i)
227+
228+
# Resize image to maximum dimensions
229+
if i.size != (width, height):
230+
i = self.resize_with_aspect_ratio(i, width, height, keep_aspect_ratio)
231+
232+
233+
image = i.convert("RGB")
234+
image = np.array(image).astype(np.float32) / 255.0
235+
image = torch.from_numpy(image)[None,]
236+
237+
if 'A' in i.getbands():
238+
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
239+
mask = 1. - torch.from_numpy(mask)
240+
if mask.shape != (height, width):
241+
mask = torch.nn.functional.interpolate(mask.unsqueeze(0).unsqueeze(0),
242+
size=(height, width),
243+
mode='bilinear',
244+
align_corners=False).squeeze()
245+
else:
246+
mask = torch.zeros((height, width), dtype=torch.float32, device="cpu")
247+
248+
images.append(image)
249+
masks.append(mask)
250+
image_path_list.append(image_path)
251+
image_count += 1
252+
253+
if len(images) == 1:
254+
return (images[0], masks[0], 1, image_path_list)
255+
256+
elif len(images) > 1:
257+
image1 = images[0]
258+
mask1 = masks[0].unsqueeze(0)
259+
260+
for image2 in images[1:]:
261+
image1 = torch.cat((image1, image2), dim=0)
262+
263+
for mask2 in masks[1:]:
264+
mask1 = torch.cat((mask1, mask2.unsqueeze(0)), dim=0)
265+
266+
return (image1, mask1, len(images), image_path_list)
267+
def resize_with_aspect_ratio(self, img, width, height, mode):
268+
if mode == "stretch":
269+
return img.resize((width, height), Image.Resampling.LANCZOS)
270+
271+
img_width, img_height = img.size
272+
aspect_ratio = img_width / img_height
273+
target_ratio = width / height
274+
275+
if mode == "crop":
276+
# Calculate dimensions for center crop
277+
if aspect_ratio > target_ratio:
278+
# Image is wider - crop width
279+
new_width = int(height * aspect_ratio)
280+
img = img.resize((new_width, height), Image.Resampling.LANCZOS)
281+
left = (new_width - width) // 2
282+
return img.crop((left, 0, left + width, height))
283+
else:
284+
# Image is taller - crop height
285+
new_height = int(width / aspect_ratio)
286+
img = img.resize((width, new_height), Image.Resampling.LANCZOS)
287+
top = (new_height - height) // 2
288+
return img.crop((0, top, width, top + height))
289+
290+
elif mode == "pad":
291+
pad_color = self.get_edge_color(img)
292+
# Calculate dimensions for padding
293+
if aspect_ratio > target_ratio:
294+
# Image is wider - pad height
295+
new_height = int(width / aspect_ratio)
296+
img = img.resize((width, new_height), Image.Resampling.LANCZOS)
297+
padding = (height - new_height) // 2
298+
padded = Image.new('RGBA', (width, height), pad_color)
299+
padded.paste(img, (0, padding))
300+
return padded
301+
else:
302+
# Image is taller - pad width
303+
new_width = int(height * aspect_ratio)
304+
img = img.resize((new_width, height), Image.Resampling.LANCZOS)
305+
padding = (width - new_width) // 2
306+
padded = Image.new('RGBA', (width, height), pad_color)
307+
padded.paste(img, (padding, 0))
308+
return padded
309+
def get_edge_color(self, img):
310+
from PIL import ImageStat
311+
"""Sample edges and return dominant color"""
312+
width, height = img.size
313+
img = img.convert('RGBA')
314+
315+
# Create 1-pixel high/wide images from edges
316+
top = img.crop((0, 0, width, 1))
317+
bottom = img.crop((0, height-1, width, height))
318+
left = img.crop((0, 0, 1, height))
319+
right = img.crop((width-1, 0, width, height))
320+
321+
# Combine edges into single image
322+
edges = Image.new('RGBA', (width*2 + height*2, 1))
323+
edges.paste(top, (0, 0))
324+
edges.paste(bottom, (width, 0))
325+
edges.paste(left.resize((height, 1)), (width*2, 0))
326+
edges.paste(right.resize((height, 1)), (width*2 + height, 0))
327+
328+
# Get median color
329+
stat = ImageStat.Stat(edges)
330+
median = tuple(map(int, stat.median))
331+
return median
332+
333+
334+
NODE_CLASS_MAPPINGS = {
335+
"BizyAirLoadImagesFromFolder": BizyAirLoadImagesFromFolder,
336+
"BizyAirMultiJoyCaption2": BizyAirMultiJoyCaption2,
337+
"SaveCaptionsAndImages": SaveCaptionsAndImages,
338+
}
339+
NODE_DISPLAY_NAME_MAPPINGS = {
340+
"BizyAirLoadImagesFromFolder": "☁️BizyAir LoadImagesFromFolder",
341+
"BizyAirMultiJoyCaption2": "☁️BizyAir Multi Joy Caption2",
342+
"SaveCaptionsAndImages": "☁️BizyAir Save Captions And Images",
343+
}

0 commit comments

Comments
 (0)