import torch import torchvision.transforms as transforms import torchvision.models as models import torch.nn.functional as F import numpy as np from PIL import Image, ImageFilter import os import hashlib CLIP_MODEL_NAME = "ViT-B/32" CLIP_DOWNLOAD_PATH = os.path.join(os.path.expanduser("~"), ".cache", "clip") # Helper to download/load CLIP model from OpenAI def load_clip_model(): import clip # requires `clip` package from OpenAI model, preprocess = clip.load(CLIP_MODEL_NAME, device="cpu", download_root=CLIP_DOWNLOAD_PATH) return model.eval(), preprocess # Image Complexity via Edge Density def compute_edge_density(image: Image.Image) -> float: grayscale = image.convert("L") edges = grayscale.filter(ImageFilter.FIND_EDGES) edge_array = np.asarray(edges, dtype=np.uint8) edge_density = np.mean(edge_array > 20) # percentage of edge pixels return round(edge_density * 10, 3) # scale 0-10 # Image Novelty via distance from reference CLIP embeddings class ClipImageEmbedder: def __init__(self): self.model, self.preprocess = load_clip_model() def get_embedding(self, image: Image.Image) -> torch.Tensor: image_input = self.preprocess(image).unsqueeze(0) with torch.no_grad(): embedding = self.model.encode_image(image_input).float() return F.normalize(embedding, dim=-1) # You could preload this from reference images REFERENCE_EMBEDDINGS = [] class EndlessNode_ImageNoveltyScorer: def __init__(self): self.embedder = ClipImageEmbedder() @classmethod def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE",), "reference_images": ("IMAGE", {"default": None, "optional": True}), } } RETURN_TYPES = ("FLOAT",) RETURN_NAMES = ("novelty_score",) FUNCTION = "score_novelty" CATEGORY = "Endless 🌊✨/Image Scoring" def score_novelty(self, image, reference_images=None): img = self._to_pil(image) img_emb = self.embedder.get_embedding(img) references = REFERENCE_EMBEDDINGS if reference_images is not None: references = [self.embedder.get_embedding(self._to_pil(ref)) for ref in reference_images] if not references: return (0.0,) sims = [F.cosine_similarity(img_emb, ref_emb).item() for ref_emb in references] avg_sim = sum(sims) / len(sims) novelty = round((1.0 - avg_sim) * 10, 3) # higher = more novel return (novelty,) def _to_pil(self, img): if isinstance(img, torch.Tensor): img = img.squeeze().detach().cpu().numpy() if isinstance(img, np.ndarray): if img.max() <= 1.0: img = (img * 255).astype(np.uint8) else: img = img.astype(np.uint8) if img.ndim == 3: return Image.fromarray(img) elif img.ndim == 2: return Image.fromarray(img, mode='L') elif isinstance(img, Image.Image): return img else: raise ValueError(f"Unsupported image type: {type(img)}") class EndlessNode_ImageComplexityScorer: def __init__(self): pass @classmethod def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE",) } } RETURN_TYPES = ("FLOAT",) RETURN_NAMES = ("complexity_score",) FUNCTION = "score_complexity" CATEGORY = "Endless 🌊✨/Image Scoring" def score_complexity(self, image): img = self._to_pil(image) complexity = compute_edge_density(img) return (complexity,) def _to_pil(self, img): if isinstance(img, torch.Tensor): img = img.squeeze().detach().cpu().numpy() if isinstance(img, np.ndarray): if img.max() <= 1.0: img = (img * 255).astype(np.uint8) else: img = img.astype(np.uint8) if img.ndim == 3: return Image.fromarray(img) elif img.ndim == 2: return Image.fromarray(img, mode='L') elif isinstance(img, Image.Image): return img else: raise ValueError(f"Unsupported image type: {type(img)}")