1.5
This commit is contained in:
@@ -15,9 +15,15 @@ There's an example workflow that illustrates all of them at the end.
|
||||
|
||||
There are also some helper nodes.
|
||||
|
||||
If you prefer trying a workflow to reading docs, use of the nodes is illustrated in this seahorse (drag the image into Comfy):
|
||||
If you prefer trying a workflow to reading docs, use of the nodes is illustrated in this blob (drag the image into Comfy):
|
||||
|
||||
<img src="https://github.com/chrisgoringe/cg-image-filter/raw/main/images/seahorse.png" alt="Seahorse" width="200" height="200">
|
||||
<img src="https://github.com/chrisgoringe/cg-image-filter/raw/main/images/blob.png" alt="Seahorse" width="200" height="200">
|
||||
|
||||
## New in 1.5 ##
|
||||
|
||||
- Floating window for extras and tips
|
||||
- Mask editor node now has option for text extras
|
||||
- Keyboard and mouse navigation in zoomed view
|
||||
|
||||
## New in 1.4 ##
|
||||
|
||||
@@ -34,17 +40,6 @@ Video previews!
|
||||
- pick_list to automatically select images in `Image Filter`
|
||||
- optional initial mask input to `Mask Image Filter`
|
||||
|
||||
## New in 1.2 ##
|
||||
|
||||
- Options have all moved to the main settings
|
||||
- New option to show a miniature window instead of taking over the screen - click on it to go into full screen mode
|
||||
- List of ints output from `Split String by Commas`
|
||||
|
||||
## New in 1.1.6 ##
|
||||
|
||||
- `Pick from List` to allow you to select matching items from any list
|
||||
- `Batch from Image List` and `Image List from Batch` to work with image batches
|
||||
- hover mouse over image and press space to zoom, space again to close the zoom
|
||||
|
||||
## Examples of what you might do with them
|
||||
|
||||
@@ -60,11 +55,13 @@ Video previews!
|
||||
|
||||

|
||||
|
||||
- `AutosendIdentical` In the ImageFilter node, if all images are identical (including if there is just one image) then send
|
||||
- `If all images are identical, autosend one` - in the ImageFilter node, if all images are identical (including if there is just one image) then send
|
||||
an image without user interaction.
|
||||
- `ClickSends` In the ImageFilter node, clicking an image sends it instead of selecting it. Useful if you know you only ever want to send one image at most.
|
||||
- `PlaySound` Play a 'ding' sound when any of the filter nodes becomes active.
|
||||
- `SmallWindow` Instead of taking over the whole screen immediately, display a tiny version of the image in the top left. Click that image to go into the full screen mode.
|
||||
- `Clicking an image sends it` - In the ImageFilter node, clicking an image sends it instead of selecting it. Useful if you know you only ever want to send one image at most.
|
||||
- `Show a small popup instead of covering the screen` - instead of taking over the whole screen immediately, display a tiny version of the image in the top left. Click that image to go into the full screen mode. You can move the tiny image window around to where you want it by dragging the title bar.
|
||||
- `Enter the Image Filter node with an image zoomed` - instead of showing the grid of images, zoom in on one. Options are `first` or `last`
|
||||
- `Play sound when activating` - play a 'ding' sound when any of the filter nodes becomes active. You can change the sound by replacing the file `ding.mp3` in the `js` subfolder.
|
||||
- `Video Frames per Second` - when previewing vidoe(s), try to play them at this speed
|
||||
|
||||
---
|
||||
|
||||
@@ -72,10 +69,7 @@ an image without user interaction.
|
||||
|
||||
In `Image Filter` and `Text Image Filter` (not `Mask Image Filter`)
|
||||
|
||||
- `Space` when hovering over an image enlarges it. While enlarged:
|
||||
- `Space` closes the zoom
|
||||
- `ArrowLeft` and `ArrowRight` select the previous/next image
|
||||
- `ArrowUp` clicks this image (select/deselect or send, depending on the value of `ClickSends`)
|
||||
- `Space` when hovering over an image enlarges it. See `Zoom` in the `Image Filter` section below.
|
||||
- `Escape` to click the cancel button
|
||||
- `Enter`to click the send button
|
||||
- digits `0`, `1`, etc. to click an image (zero-indexed) (select/deselect or send, depending on the value of `ClickSends`)
|
||||
@@ -95,8 +89,8 @@ you want to progress:
|
||||
|
||||

|
||||
|
||||
Click the images that you want to keep (their border will turn green) and then click 'Send' (or press 's') to continue the workflow.
|
||||
If you don't want to keep any of the images, click 'Cancel' (or press 'x') to terminate the workflow.
|
||||
Click the images that you want to keep (their border will turn green) and then click 'Send' to continue the workflow.
|
||||
If you don't want to keep any of the images, click 'Cancel' (or press 'escape') to terminate the workflow.
|
||||
|
||||
The node also has a timeout specified, and a countdown is shown on the left hand side. If you don't Send or Cancel before the timeout,
|
||||
the node will either cancel or send all the images, depending on the option you have selected.
|
||||
@@ -105,6 +99,19 @@ Here's a simple use: generate a batch of images and pick which ones you want to
|
||||
|
||||

|
||||
|
||||
### Zoom
|
||||
|
||||
If you hover over an image (it gets a red border) and press the space bar, it will zoom to fill the screen.
|
||||
You can also use the `Enter the Image Filter node with an image zoomed` setting to start in this mode.
|
||||
|
||||
While zoomed, you can navigate with keyboard or mouse:
|
||||
|
||||
| |Mouse|Keyboard|Notes|
|
||||
|-|-|-|-|
|
||||
|Select/unselect/send|Click the zoomed image|`Arrow Up`|Will select/unselect the image, or send it if you have `Clicking an image sends it` turned on|
|
||||
|Previous/Next|Click the arrows to the left or right|`Arrow Left` `Arrow Right`|The image number is shown near the top right|
|
||||
|Unzoom| |`Space`|Back to the grid view|
|
||||
|
||||
### Optional inputs
|
||||
|
||||
The Latent and Mask inputs are optional. If used, they should have the same number of latents (or masks) as the image batch,
|
||||
@@ -228,9 +235,9 @@ This workflow:
|
||||
- inpaints
|
||||
- uses 'Image Filter' to choose which, if either, of the two images (before and after inpaint) to save
|
||||
|
||||
The workflow is embedded in the seahorse in a bottle:
|
||||
The workflow is embedded in the blob in a bottle:
|
||||
|
||||
<img src="https://github.com/chrisgoringe/cg-image-filter/raw/main/images/seahorse.png" alt="Seahorse" width="200" height="200">
|
||||
<img src="https://github.com/chrisgoringe/cg-image-filter/raw/main/images/blob.png" alt="Seahorse" width="200" height="200">
|
||||
|
||||
# Bugs, Ideas, and the future
|
||||
|
||||
|
||||
+4
-4
@@ -5,7 +5,7 @@ from comfy.model_management import InterruptProcessingException
|
||||
import os
|
||||
import torch
|
||||
|
||||
from .image_filter_messaging import send_and_wait, Response
|
||||
from .image_filter_messaging import send_and_wait, Response, TimeoutResponse
|
||||
|
||||
HIDDEN = {
|
||||
"prompt": "PROMPT",
|
||||
@@ -66,7 +66,7 @@ class ImageFilter(PreviewImage):
|
||||
|
||||
response:Response = send_and_wait(payload, timeout, uid, node_identifier)
|
||||
|
||||
if response.timeout:
|
||||
if isinstance(response, TimeoutResponse):
|
||||
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,]
|
||||
@@ -75,7 +75,7 @@ class ImageFilter(PreviewImage):
|
||||
e1, e2, e3 = response.get_extras([extra1, extra2, extra3])
|
||||
images_to_return = response.selection
|
||||
|
||||
if len(images_to_return) == 0: raise InterruptProcessingException()
|
||||
if images_to_return is None or 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) ]
|
||||
@@ -123,7 +123,7 @@ class TextImageFilterWithExtras(PreviewImage):
|
||||
if mask is not None: payload['mask_urls'] = self.save_images(images=mask_to_image(mask), **kwargs)['ui']['images']
|
||||
|
||||
response = send_and_wait(payload, timeout, uid, node_identifier)
|
||||
if response.timeout:
|
||||
if isinstance(response, TimeoutResponse):
|
||||
return (image, text, extra1, extra2, extra3)
|
||||
|
||||
return (image, response.text, *response.get_extras([extra1, extra2, extra3]))
|
||||
|
||||
+31
-26
@@ -5,11 +5,10 @@ import time, json
|
||||
from typing import Optional
|
||||
|
||||
REQUEST_RESHOW = "-1"
|
||||
REQUEST_TIMER_RESET = "-2"
|
||||
CANCEL = "-3"
|
||||
WAITING_FOR_RESPONSE = "-9"
|
||||
|
||||
SPECIALS = [REQUEST_RESHOW, REQUEST_TIMER_RESET, CANCEL, WAITING_FOR_RESPONSE]
|
||||
SPECIALS = [REQUEST_RESHOW, CANCEL, WAITING_FOR_RESPONSE]
|
||||
|
||||
class Response:
|
||||
def __init__(self, selection:Optional[list[int]] = None, text:Optional[str] = None,
|
||||
@@ -18,18 +17,17 @@ class Response:
|
||||
self.text = text
|
||||
self.masked_image = masked_image
|
||||
self.extras = extras
|
||||
self.timeout = False
|
||||
|
||||
def get_extras(self,defaults:list[str]) -> list[str]:
|
||||
return self.extras or defaults
|
||||
|
||||
class TimeoutResponse(Response):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.timeout = True
|
||||
class TimeoutResponse(Response): pass
|
||||
class CancelledResponse(Response): pass
|
||||
class RequestResponse(Response): pass
|
||||
|
||||
class MessageState:
|
||||
latest = None
|
||||
unique_expected = None
|
||||
def __init__(self, data:dict|str={}):
|
||||
if not isinstance(data,dict): data = json.loads(data)
|
||||
self.unique:str = data.pop('unique', None)
|
||||
@@ -40,11 +38,18 @@ class MessageState:
|
||||
def waiting_state(cls): return MessageState(data={'special':WAITING_FOR_RESPONSE})
|
||||
|
||||
@classmethod
|
||||
def start_waiting(cls): cls.latest = cls.waiting_state()
|
||||
def request_state(cls): return MessageState(data={'special':REQUEST_RESHOW})
|
||||
|
||||
@classmethod
|
||||
def start_waiting(cls, unique):
|
||||
cls.latest = cls.waiting_state()
|
||||
cls.unique_expected = unique
|
||||
|
||||
@classmethod
|
||||
def get_response(cls) -> Response:
|
||||
if cls.waiting(): return TimeoutResponse()
|
||||
if cls.latest.cancelled: return CancelledResponse()
|
||||
if cls.latest.request: return RequestResponse()
|
||||
return cls.latest.response
|
||||
|
||||
@classmethod
|
||||
@@ -53,8 +58,11 @@ class MessageState:
|
||||
@classmethod
|
||||
def waiting(cls): return cls.latest.special == WAITING_FOR_RESPONSE
|
||||
|
||||
@classmethod
|
||||
def cancelled(cls): return cls.latest.special == CANCEL
|
||||
@property
|
||||
def cancelled(self): return self.special == CANCEL
|
||||
|
||||
@property
|
||||
def request(self): return self.special == REQUEST_RESHOW
|
||||
|
||||
@property
|
||||
def real(self): return self.special is None
|
||||
@@ -66,17 +74,18 @@ async def cg_image_filter_message(request):
|
||||
response = post.get("response")
|
||||
message = MessageState(response)
|
||||
|
||||
if (MessageState.waiting()):
|
||||
MessageState.latest = message
|
||||
elif (message.real and not MessageState.cancelled()):
|
||||
MessageState.latest = message
|
||||
if str(MessageState.unique_expected)==str(message.unique):
|
||||
if (MessageState.waiting()):
|
||||
MessageState.latest = message
|
||||
else:
|
||||
print(f"Ignoring response {response} because not waiting for one")
|
||||
else:
|
||||
print(f"Ignoring response {response}")
|
||||
print(f"Ignoring mismatched response {response}")
|
||||
|
||||
return web.json_response({})
|
||||
|
||||
def wait_for_response(secs, uid, unique) -> Response:
|
||||
MessageState.start_waiting()
|
||||
MessageState.start_waiting(unique)
|
||||
try:
|
||||
end_time = time.monotonic() + secs
|
||||
while(time.monotonic() < end_time and MessageState.waiting()):
|
||||
@@ -85,20 +94,16 @@ def wait_for_response(secs, uid, unique) -> Response:
|
||||
time.sleep(0.2)
|
||||
if MessageState.waiting():
|
||||
PromptServer.instance.send_sync("cg-image-filter-images", {"timeout": True, "uid": uid, "unique":unique})
|
||||
elif MessageState.cancelled():
|
||||
raise InterruptProcessingException()
|
||||
elif str(MessageState.latest.unique)!=str(unique):
|
||||
print("Mismatched uniques...")
|
||||
return MessageState.get_response()
|
||||
finally: MessageState.stop_waiting()
|
||||
|
||||
def send_and_wait(payload, timeout, uid, unique) -> Response:
|
||||
payload['uid'] = uid
|
||||
payload['unique'] = unique
|
||||
PromptServer.instance.send_sync("cg-image-filter-images", payload)
|
||||
return wait_for_response(timeout, uid, unique)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
while True:
|
||||
PromptServer.instance.send_sync("cg-image-filter-images", payload)
|
||||
r = wait_for_response(timeout, uid, unique)
|
||||
if isinstance(r,CancelledResponse): raise InterruptProcessingException()
|
||||
if (not isinstance(r, RequestResponse)): return r
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 878 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
+3
-1
@@ -34,6 +34,7 @@ app.registerExtension({
|
||||
{
|
||||
id: "Image Filter.Actions.Click Sends",
|
||||
name: "Clicking an image sends it",
|
||||
tooltip: "Use if you always want to send exactly one image.",
|
||||
type: "boolean",
|
||||
defaultValue: false
|
||||
},
|
||||
@@ -54,12 +55,13 @@ app.registerExtension({
|
||||
id: "Image Filter.UI.Small Window",
|
||||
name: "Show a small popup instead of covering the screen",
|
||||
type: "boolean",
|
||||
tooltip: "click the small popup to activate it",
|
||||
tooltip: "Click the small popup to activate it",
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: "Image Filter.Z.Detailed Logging",
|
||||
name: "Turn on detailed logging",
|
||||
tooltip: "If you are asked to for debugging!",
|
||||
type: "boolean",
|
||||
defaultValue: false
|
||||
},
|
||||
|
||||
+25
-17
@@ -82,6 +82,7 @@ class Popup extends HTMLSpanElement {
|
||||
this.picked = new Set()
|
||||
|
||||
document.addEventListener("keydown", this.on_key_down.bind(this))
|
||||
document.addEventListener("keypress", this.on_key_press.bind(this))
|
||||
|
||||
document.body.appendChild(this)
|
||||
this.last_response_sent = 0
|
||||
@@ -407,25 +408,32 @@ class Popup extends HTMLSpanElement {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
on_key_down(e) {
|
||||
on_key_press(e) {
|
||||
if (document.activeElement?.type=='text' || document.activeElement?.type=='textarea') {
|
||||
if (this.floating_window.contains(document.activeElement) || this.contains(document.activeElement)) return
|
||||
}
|
||||
if (this.state!=State.IDLE && this.state!=State.TINY) {
|
||||
this.eat_event(e)
|
||||
}
|
||||
}
|
||||
|
||||
on_key_down(e) {
|
||||
if (document.activeElement?.type=='text' || document.activeElement?.type=='textarea') {
|
||||
if (this.floating_window.contains(document.activeElement) || this.contains(document.activeElement)) return
|
||||
}
|
||||
if (this.state==State.FILTER || this.state==State.TEXT) {
|
||||
if (document.activeElement?.type=='text' || document.activeElement?.type=='textarea') {
|
||||
return
|
||||
} else {
|
||||
if (e.key=='Enter') {
|
||||
this.send_current_state()
|
||||
return this.eat_event(e)
|
||||
}
|
||||
if (e.key=='Escape') {
|
||||
this.send_cancel()
|
||||
return this.eat_event(e)
|
||||
}
|
||||
if (`${parseInt(e.key)}`==e.key) {
|
||||
this.select_unselect(parseInt(e.key))
|
||||
this.render()
|
||||
return this.eat_event(e)
|
||||
}
|
||||
if (e.key=='Enter') {
|
||||
this.send_current_state()
|
||||
return this.eat_event(e)
|
||||
}
|
||||
if (e.key=='Escape') {
|
||||
this.send_cancel()
|
||||
return this.eat_event(e)
|
||||
}
|
||||
if (`${parseInt(e.key)}`==e.key) {
|
||||
this.select_unselect(parseInt(e.key))
|
||||
this.render()
|
||||
return this.eat_event(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user