diff --git a/__init__.py b/__init__.py index 4bc5717..5af9b9c 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ @author: BiffMunky @title: 🌌 An Endless Sea of Stars Nodes 🌌 @nickname: 🌌 Endless Nodes 🌌 -@description: A small set of nodes I created for various numerical and text inputs. +@description: A small set of nodes I created for various numerical and text inputs. Features switches for text and numbers, parameter collection nodes, and two aesthetic scoring modwls. """ from .endless_nodes import * @@ -15,14 +15,16 @@ NODE_CLASS_MAPPINGS = { "Endless Nodes Parameterizer & Prompts": EndlessNode_XLParameterizerPrompt, "Endless Nodes Combo Parameterizer": EndlessNode_ComboXLParameterizer, "Endless Nodes Combo Parameterizer & Prompts": EndlessNode_ComboXLParameterizerPrompt, - "Endless Nodes Image Save with Text File": EndlessNode_ImageSaver, + #"Endless Nodes Image Save with Text File": EndlessNode_ImageSaver, # "Endless Nodes Display String": EndlessNode_DisplayString, # "Endless Nodes Display Number": EndlessNode_DisplayNumber, # "Endless Nodes Display Integer": EndlessNode_DisplayInt, # "Endless Nodes Display Float": EndlessNode_DisplayFloat, "Endless Nodes Aesthetic Scoring": EndlessNode_Scoring, + "Endless Nodes Image Reward": EndlessNode_ImageReward, + } __all__ = ['NODE_CLASS_MAPPINGS'] -print("\033[36m 🌌 An Endless Sea of Stars Custom Nodes 🌌 V0.23 \033[34m: \033[92mLoaded\033[0m") \ No newline at end of file +print("\033[36m 🌌 An Endless Sea of Stars Custom Nodes 🌌 V0.24 \033[34m: \033[92mLoaded\033[0m") \ No newline at end of file diff --git a/changlelog.md b/changlelog.md index 3678a56..cb2a03b 100644 --- a/changlelog.md +++ b/changlelog.md @@ -1,3 +1,4 @@ +Sep 24/23: 0.24 - Added In Image Reward scoring model with a single node to load model and output standard deviation and scoring via number or string nodes Sep 24/23: 0.23 - Rework Aesthetic Score model and integrate it into single node to display score, added a requirements file Sep 23/23: 0.22 - Unreleased, convert ImageReward output to base ten score Sep 22/23: 0.21 - Unreleased, recategorized nodes into submenus, added some vanity coding to the node names, changed the ComfyUI manager header text diff --git a/endless_nodes.py b/endless_nodes.py index 906c5bf..67c5a6e 100644 --- a/endless_nodes.py +++ b/endless_nodes.py @@ -2,10 +2,11 @@ @author: BiffMunky @title: 🌌 An Endless Sea of Stars Nodes 🌌 @nickname: 🌌 Endless Nodes 🌌 -@description: A small set of nodes I created for various numerical and text inputs. +@description: A small set of nodes I created for various numerical and text inputs. Features switches for text and numbers, parameter collection nodes, and two aesthetic scoring modwls. """ -# Version 0.23 - Aesthetic Scoring TYpe 1 addeded +# Version 0.24 - Imagr Rearwd nodeaddeded +#0.23 - Aesthetic Scorer addeded #0.22 Unreleased - intro'd asestheic score #0.21 unreleased -- trying for display nodes #0.20 sorted categories of nodes @@ -35,7 +36,7 @@ import sys import statistics import torch import torch.nn as nn - +import ImageReward as RM sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy")) @@ -416,9 +417,9 @@ class EndlessNode_Scoring: def INPUT_TYPES(cls): return { "required": { - "model_name": (folder_paths.get_filename_list("aesthetic"), ), + "model_name": (folder_paths.get_filename_list("aesthetic"), {"multiline": False, "default": "chadscorer.pth"}), "image": ("IMAGE",), - } + } } RETURN_TYPES = ("NUM",) @@ -447,105 +448,150 @@ class EndlessNode_Scoring: del model return (final_prediction,) - - ##test of image saver ## -class EndlessNode_ImageSaver: +class EndlessNode_ImageReward: def __init__(self): - self.output_dir = folder_paths.get_output_directory() - self.type = "output" + self.model = None @classmethod def INPUT_TYPES(cls): return { "required": { + "model": ("STRING", {"multiline": False, "default": "ImageReward-v1.0"}), + "prompt": ("STRING", {"multiline": True, "forceInput": True}), "images": ("IMAGE",), - "filename_prefix": ("STRING", {"default": "ComfyUI"}), - "subfolder": ("STRING", {"default": None}), # Add subfolder input - }, - "hidden": { - "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO" +# "rounded": ("BOOL", {"default": False}) # Add a boolean input }, } - RETURN_TYPES = () - FUNCTION = "save_images" + RETURN_TYPES = ("FLOAT", "STRING", "FLOAT", "STRING") + RETURN_NAMES = ("SCORE_FLOAT", "SCORE_STRING", "VALUE_FLOAT", "VALUE_STRING") + OUTPUT_NODE = False - OUTPUT_NODE = True + CATEGORY = "Endless 🌌/Scoring" - CATEGORY = "Endless 🌌/IO" + FUNCTION = "process_images" - def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None, subfolder=None): + def process_images(self, model, prompt, images,): #rounded): + if self.model is None: + self.model = RM.load(model) - # Replace illegal characters in the filename prefix with dashes - filename_prefix = re.sub(r'[<>:"\/\\|?*]', '-', filename_prefix) - - # Get the current date in Y-m-d format - today = datetime.datetime.now().strftime("%Y-%m-%d") - - # If a custom subfolder is provided, use it; otherwise, use the date - if subfolder is not None: - full_output_folder = os.path.join(self.output_dir, subfolder) - else: - full_output_folder = os.path.join(self.output_dir, today) - - # Create the subfolder if it doesn't exist - os.makedirs(full_output_folder, exist_ok=True) - - counter = self.get_next_number(full_output_folder) - - results = list() + score = 0.0 for image in images: - i = 255. * image.cpu().numpy() + # convert to PIL image + i = 255.0 * image.cpu().numpy() img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + score += self.model.score(prompt, [img]) + score /= len(images) + + # if rounded: + # # Round the score to two decimal places + # score = round(score, 2) + + # assume std dev follows normal distribution curve + valuescale = 0.5 * (1 + math.erf(score / math.sqrt(2))) * 10 # *10 to get a value between -10 + return (score, str(score), valuescale, str(valuescale)) + + + # ##test of image saver ## + + +# class EndlessNode_ImageSaver: + # def __init__(self): + # self.output_dir = folder_paths.get_output_directory() + # self.type = "output" + + # @classmethod + # def INPUT_TYPES(cls): + # return { + # "required": { + # "images": ("IMAGE",), + # "filename_prefix": ("STRING", {"default": "ComfyUI"}), + # "subfolder": ("STRING", {"default": None}), # Add subfolder input + # }, + # "hidden": { + # "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO" + # }, + # } + + # RETURN_TYPES = () + # FUNCTION = "save_images" + + # OUTPUT_NODE = True + + # CATEGORY = "Endless 🌌/IO" + + # def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None, subfolder=None): + + # # Replace illegal characters in the filename prefix with dashes + # filename_prefix = re.sub(r'[<>:"\/\\|?*]', '-', filename_prefix) + + # # Get the current date in Y-m-d format + # today = datetime.datetime.now().strftime("%Y-%m-%d") + + # # If a custom subfolder is provided, use it; otherwise, use the date + # if subfolder is not None: + # full_output_folder = os.path.join(self.output_dir, subfolder) + # else: + # full_output_folder = os.path.join(self.output_dir, today) + + # # Create the subfolder if it doesn't exist + # os.makedirs(full_output_folder, exist_ok=True) + + # counter = self.get_next_number(full_output_folder) + + # results = list() + # for image in images: + # i = 255. * image.cpu().numpy() + # img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) - metadata = PngInfo() - if prompt is not None: - metadata.add_text("prompt", json.dumps(prompt)) - if extra_pnginfo is not None: - for x in extra_pnginfo: - metadata.add_text(x, json.dumps(extra_pnginfo[x])) + # metadata = PngInfo() + # if prompt is not None: + # metadata.add_text("prompt", json.dumps(prompt)) + # if extra_pnginfo is not None: + # for x in extra_pnginfo: + # metadata.add_text(x, json.dumps(extra_pnginfo[x])) - file = f"{counter:05}-c-{filename_prefix}.png" - img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4) - results.append({ - "filename": file, - "subfolder": full_output_folder, - "type": self.type - }) + # file = f"{counter:05}-c-{filename_prefix}.png" + # img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4) + # results.append({ + # "filename": file, + # "subfolder": full_output_folder, + # "type": self.type + # }) - # Check if a user-specified folder for TEXT files is provided - if subfolder is not None: - # Create the full path for the TEXT file using the same name as the PNG - text_file = os.path.join(subfolder, f"{counter:05}-c-{filename_prefix}.txt") - else: - # Use the same folder as the image if no custom subfolder is provided - text_file = os.path.join(full_output_folder, f"{counter:05}-c-{filename_prefix}.txt") + # # Check if a user-specified folder for TEXT files is provided + # if subfolder is not None: + # # Create the full path for the TEXT file using the same name as the PNG + # text_file = os.path.join(subfolder, f"{counter:05}-c-{filename_prefix}.txt") + # else: + # # Use the same folder as the image if no custom subfolder is provided + # text_file = os.path.join(full_output_folder, f"{counter:05}-c-{filename_prefix}.txt") - # Save some example text content to the TEXT file (you can modify this) - with open(text_file, 'w') as text: - text.write("This is an example text file.") + # # Save some example text content to the TEXT file (you can modify this) + # with open(text_file, 'w') as text: + # text.write("This is an example text file.") - counter += 1 + # counter += 1 - return {"ui": {"images": results}} + # return {"ui": {"images": results}} - def get_next_number(self, directory): - files = os.listdir(directory) - highest_number = 0 - for file in files: - parts = file.split('-') - try: - num = int(parts[0]) - if num > highest_number: - highest_number = num - except ValueError: - # If it's not a number, skip this file - continue + # def get_next_number(self, directory): + # files = os.listdir(directory) + # highest_number = 0 + # for file in files: + # parts = file.split('-') + # try: + # num = int(parts[0]) + # if num > highest_number: + # highest_number = num + # except ValueError: + # # If it's not a number, skip this file + # continue - # Return the next number - return highest_number + 1 + # # Return the next number + # return highest_number + 1 #-------------------------------------- @@ -569,5 +615,7 @@ class EndlessNode_ImageSaver: # The scorer uses the MLP class code from Christoph Schuhmann # #https://github.com/christophschuhmann/improved-aesthetic-predictor +#[Zane A's ComfyUI-ImageReward](https://github.com/ZaneA/ComfyUI-ImageReward) for the original coding for the Umagr Reward nodee # -#-------------------------------------- +#Zane's node in turn uses [ImageReward](https://github.com/THUDM/ImageReward) +#-------------------------------------- \ No newline at end of file diff --git a/readme.md b/readme.md index ea403f1..cfd579d 100644 --- a/readme.md +++ b/readme.md @@ -8,11 +8,12 @@ Rightly or wrongly, I am pretending to teach myself a bit of Python to get some **UPDATE: Sep 24, 2023** -+ Took the node from https://github.com/strimmlarn that does aesthetic scoring and re-purposed it ++ Took the node from https://github.com/ZaneA/ComfyUI-ImageReward that uses Image Reward and repurposed it ++ Took the node from https://github.com/strimmlarn that does aesthetic scoring and repurposed it **UPDATE: Sep 20, 2023** -+ Added an eight input number switch because I needed it ++ Added an eight-input number switch because I needed it **UPDATE: Sep 18, 2023** @@ -27,7 +28,7 @@ Rightly or wrongly, I am pretending to teach myself a bit of Python to get some + Added the Endless Nodes Parameterizer -## Install +## Install and Requirements Navigate to your /ComfyUI/custom_nodes/ folder @@ -37,7 +38,7 @@ In Windows, you can then right-click to start a command prompt and type: You can also get the nodes via the [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) -**NOTE: Requires CLIP and Pytorch-Lightning for the Aesthetic Scorer! I've added them in the requirement file but if it doesn't work, yo will need to download manually** +**NOTE: Requires CLIP and Pytorch-Lightning for the Aesthetic Scorer and ImageReward for the my take on the Image Reward node scorer. I've added them in the requirement file but if it doesn't work, you will need to download manually** ## Node List @@ -95,9 +96,9 @@ After making the Parameterizer, I realized having two separate ones for both the ![comboparameterizerprompt](./img/comboparameterizerprompt.png) -## Aesthetic Scoring Output ## +## Aesthetic Scorer ## -This node will output a predicted aestheic score as a number and dispaly it with the appropriate node (e.g., rgthree's "Any" node). I took the node from https://github.com/strimmlarn that does aesthetic scoring and repurposed it so that it is simpler and outputs the score as a number. I combined the model loader and score calculator into one, and removed the Aesthetic Score Sorter. +This node will output a predicted aestheic score as a number and display it with the appropriate node (e.g., rgthree's ["Any"](https://github.com/rgthree/rgthree-comfy#display-any) node). I took the node from https://github.com/strimmlarn that does aesthetic scoring and repurposed it so that it is simpler and outputs the score as a number. I combined the model loader and score calculator into one, and removed the Aesthetic Score Sorter. ![aestheticone](./img/aestheticone.png) @@ -105,33 +106,41 @@ You can load a number of scoring models, I use the "chadscorer" model found here https://github.com/grexzen/SD-Chad/blob/main/chadscorer.pth -As for the original node from strimmlarn, please refer to this GitHub if you would like to examine it: +As for the original node from strimmlarn, please refer to this GitHub if you would like to examine it: https://github.com/strimmlarn/ComfyUI-Strimmlarns-Aesthetic-Score The scorer adds about 7-10 seconds to a workflow on my Nvidia 3060 12 GB card, your mileage may vary +## Image Reward## + +This node will output a predicted aesthetic score as a number and display it with the appropriate node (e.g., rgthree's ["Any"](https://github.com/rgthree/rgthree-comfy#display-any) node). I took the node from https://github.com/ZaneA/ComfyUI-ImageReward that in turn scores images using [ImageReward](https://github.com/THUDM/ImageReward). I combined the model loader and score calculator into one and added output nodes for both the standard deviation calculation (which is what Zane's node does) and the score on a scale of one to ten based on some simple statistic calculations. + +The difference between this node and the Aesthetics Scorer is that the underlying ImageReward is based on Reward Feedback Learning (ReFL) and uses 137K input samples that were scored by humans. It often score much lower than the Aesthetics Scorer, but not always! + +![imagereward](./img/imagereward.png) + +As with the Aesthetics Scorer, the Image Reward node adds about 7-10 seconds to a workflow on my Nvidia 3060 12 GB card, your mileage may vary. + +For added GPU time cycle consumption, put them both in and watch how often they vehemently disagree with the scoring :) + ## Usage License and Restrictions See GPL Licensing V3 for usage. You may modify this code as long as you keep the credits for this repository and for those noted in the credit section below. **YOU ARE EXPRESSLY FORBIDDEN FROM USING THIS NODE TO CREATE ANY IMAGES OR ARTWORK THAT VIOLATES THE STABLE DIFFUSION USAGE NOTES [HERE](https://huggingface.co/stabilityai/stable-diffusion-2#misuse-malicious-use-and-out-of-scope-use) AND [HERE](https://huggingface.co/stabilityai/stable-diffusion-2#misuse-and-malicious-use).** -For example, don't be a mouth-breathing dick who creates fake celebrity nudes or sexual content of **anyone, even** if you have their consent +For example, don't be a mouth-breather who creates fake celebrity nudes or sexual content of **anyone, even if you have their consent**. JUST. DON’T. BE. A. DICK/BITCH. The author expressly disclaims any liability for any images you create using these nodes. ## Disclaimer -These nodes may or may not be maintained. They work on my system, but may not on yours. +These nodes may or may not be maintained. They work on my system but may not on yours. ## Credits -[Comfyroll Custom Nodes](https://github.com/RockOfFire/ComfyUI_Comfyroll_CustomNode) for the overall node code layout, coding snippets, and inspiration for the text input and number switches - - [WLSH Nodes](https://github.com/wallish77/wlsh_nodes) for some coding for the Integer Widget - -[ComfyUI](https://github.com/comfyanonymous/ComfyUI) Interface for the basic ideas of what nodes I wanted - -[ComfyUI-Strimmlarns-Aesthetic-Score](https://github.com/strimmlarn/ComfyUI-Strimmlarns-Aesthetic-Score) for the original coding for Aesthetic Scoring Type One - -The orginal scorer, and therefore my derivative too, use the [MLP class code](https://github.com/christophschuhmann/improved-aesthetic-predictor) from Christoph Schuhmann \ No newline at end of file ++[Comfyroll Custom Nodes](https://github.com/RockOfFire/ComfyUI_Comfyroll_CustomNode) for the overall node code layout, coding snippets, and inspiration for the text input and number switches. ++[WLSH Nodes](https://github.com/wallish77/wlsh_nodes) for some coding for the Integer Widget. ++[ComfyUI](https://github.com/comfyanonymous/ComfyUI) Interface for the basic ideas of what nodes I wanted. ++[ComfyUI-Strimmlarns-Aesthetic-Score](https://github.com/strimmlarn/ComfyUI-Strimmlarns-Aesthetic-Score) for the original coding for the Aesthetic Scorer. The original scorer, and therefore my derivative too, use the [MLP class code](https://github.com/christophschuhmann/improved-aesthetic-predictor) from Christoph Schuhmann ++[Zane A's ComfyUI-ImageReward](https://github.com/ZaneA/ComfyUI-ImageReward) for the original coding for the Image Reward node. Zane's node in turn uses [ImageReward](https://github.com/THUDM/ImageReward) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 71d5ff7..dbe97e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ clip -pytorch-lightning \ No newline at end of file +pytorch-lightning +image-reward==1.4 \ No newline at end of file