diff --git a/README.md b/README.md index ed1119a..8461d60 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ If you prefer trying a workflow to reading docs, use of the nodes is illustrated Seahorse +## New in 1.4 ## + +Video previews! + ## New in 1.3.2 ## - works with old mask editor as well @@ -119,6 +123,14 @@ You could also use this to make a larger batch of images (repeat a value and the Note that this uses zero indexing (the first image is '0'). +### video_frames + +If you set this to greater than one, the image filter node will batch up each set of n images and treat them as a video snip. + +Set the FPS for playback in the global settings. + +This is a new, experimental feature, so please report any issues... + --- ## Mask Image Filter diff --git a/__init__.py b/__init__.py index 1e32ada..2ab64d5 100644 --- a/__init__.py +++ b/__init__.py @@ -9,7 +9,7 @@ from .image_filter import ImageFilter, MaskImageFilter, TextImageFilterWithExtra from .list_utility_nodes import PickFromList, BatchFromImageList, ImageListFromBatch from .string_utility_nodes import SplitByCommas, StringToFloat, StringToInt -VERSION = "1.3.2" +VERSION = "1.4" WEB_DIRECTORY = "./js" NODE_CLASS_MAPPINGS= { diff --git a/image_filter.py b/image_filter.py index 2ff5e3b..eca727e 100644 --- a/image_filter.py +++ b/image_filter.py @@ -90,6 +90,7 @@ class ImageFilter(PreviewImage): FUNCTION = "func" CATEGORY = "image_filter" OUTPUT_NODE = False + DESCRIPTION = "Allows you to preview images and choose which, if any to proceed with" @classmethod def INPUT_TYPES(s): @@ -107,6 +108,7 @@ class ImageFilter(PreviewImage): "extra2" : ("STRING", {"default":""}), "extra3" : ("STRING", {"default":""}), "pick_list" : ("STRING", {"default":"", "tooltip":"If a comma separated list of integers is provided, the images with these indices will be selected automatically."}), + "video_frames" : ("INT", {"default":1, "min":1, "tooltip": "treat each block of n images as a video"}), }, "hidden": HIDDEN, } @@ -115,7 +117,7 @@ class ImageFilter(PreviewImage): def IS_CHANGED(cls, pick_list, **kwargs): return pick_list or float("NaN") - def func(self, images, timeout, ontimeout, uid, node_identifier, tip="", extra1="", extra2="", extra3="", latents=None, masks=None, pick_list:str="", **kwargs): + def func(self, images, timeout, ontimeout, uid, node_identifier, tip="", extra1="", extra2="", extra3="", latents=None, masks=None, pick_list:str="", video_frames:int=1, **kwargs): e1, e2, e3 = extra1, extra2, extra3 B = images.shape[0] @@ -127,7 +129,7 @@ class ImageFilter(PreviewImage): if len(images_to_return) == 0: all_the_same = ( B and all( (images[i]==images[0]).all() for i in range(1,B) )) urls:list[str] = self.save_images(images=images, **kwargs)['ui']['images'] - payload = {"uid": uid, "urls":urls, "allsame":all_the_same, "extras":[extra1, extra2, extra3], "tip":tip} + payload = {"uid": uid, "urls":urls, "allsame":all_the_same, "extras":[extra1, extra2, extra3], "tip":tip, "video_frames":video_frames} response = send_with_resend(payload, timeout, uid, node_identifier) @@ -138,10 +140,13 @@ class ImageFilter(PreviewImage): if ontimeout=='send none': images_to_return = [] if ontimeout=='send all': images_to_return = [*range(len(images))] if ontimeout=='send first': images_to_return = [0,] - if ontimeout=='send last': images_to_return = [len(images)-1,] + if ontimeout=='send last': images_to_return = [len(images)//video_frames,] if len(images_to_return) == 0: raise InterruptProcessingException() + if video_frames>1: + images_to_return = [ key*video_frames + frm for key in images_to_return for frm in range(video_frames) ] + images = torch.stack(list(images[i] for i in images_to_return)) latents = {"samples": torch.stack(list(latents['samples'][i] for i in images_to_return))} if latents is not None else None masks = torch.stack(list(masks[i] for i in images_to_return)) if masks is not None else None diff --git a/js/image_filter.js b/js/image_filter.js index 40f9a16..3fc5d77 100644 --- a/js/image_filter.js +++ b/js/image_filter.js @@ -40,6 +40,12 @@ app.registerExtension({ type: "boolean", defaultValue: false }, + { + id: "ImageFilter.FPS", + name: "Video Frames per Second", + type: "int", + defaultValue: 1, + } ], setup() { create('link', null, document.getElementsByTagName('HEAD')[0], diff --git a/js/popup.js b/js/popup.js index d76665b..6524c65 100644 --- a/js/popup.js +++ b/js/popup.js @@ -247,6 +247,8 @@ class Popup extends HTMLSpanElement { handle_urls(detail) { this.n_extras = detail.extras ? detail.extras.length : 0 + this.video_frames = detail.video_frames || 1 + this.extras.innerHTML = '' for (let i=0; i{ console.log(url) - const img = create('img', null, this.grid, {src:get_full_url(url)}) + if (i%this.video_frames == 0) { + latestImage = create('img', null, this.grid, {src:get_full_url(url)}) + latestImage.onload = this.layout.bind(this) + latestImage.image_index = i/this.video_frames + latestImage.addEventListener('mouseover', (e)=>this.on_mouse_enter(latestImage)) + latestImage.addEventListener('mouseout', (e)=>this.on_mouse_out(latestImage)) + latestImage.frames = [get_full_url(url),] + } else { + latestImage.frames.push(get_full_url(url)) + } if (detail.mask_urls) { create('img', null, this.overlaygrid, {src:get_full_url(detail.mask_urls[i])})} - img.onload = this.layout.bind(this) - img.image_index = i - img.addEventListener('mouseover', (e)=>this.on_mouse_enter(img)) - img.addEventListener('mouseout', (e)=>this.on_mouse_out(img)) + }) this.layout() + + if (this.video_frames>1) { + this.frame = 0 + setTimeout(this.advance_videos.bind(this), 1000) + } } + advance_videos() { + if (this.state == State.INACTIVE) return + + this.frame = (this.frame+1)%this.video_frames + Array.from(this.grid.children).forEach((img)=>{img.src = img.frames[this.frame]}) + + const fps = app.ui.settings.getSettingValue("ImageFilter.FPS") + const delay = (fps>0) ? 1000/fps : 1000 + setTimeout(this.advance_videos.bind(this), delay) + } + on_mouse_enter(img) { this.mouse_is_over = img this.redraw() diff --git a/pyproject.toml b/pyproject.toml index c330c33..49496a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "cg-image-filter" description = "A set of custom nodes that pause a workflow while you select images, add masks, or edit text." -version = "1.3.2" +version = "1.4" license = { file = "LICENSE" } [project.urls]