Skip to content

Commit b1443ad

Browse files
author
ryanontheinstide
committed
Squashed commit of the following:
commit 40be6443bca076f01e30930cd1c27d88d032df89 Author: ryanontheinstide <[email protected]> Date: Wed Jan 29 09:51:43 2025 -0500 similarity filter, lazy condition, remove redundant VAE
1 parent a3ba930 commit b1443ad

File tree

9 files changed

+491
-96
lines changed

9 files changed

+491
-96
lines changed

README.MD

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,8 @@ The intention for this repository is to build a suite of nodes that can be used
2626
- **QuickShapeMask**: Rapidly generate shape masks (circle, square) with customizable dimensions.
2727
- **DTypeConverter**: Convert masks between different data types (float16, uint8, float32, float64).
2828
- **FastWebcamCapture**: High-performance webcam capture node with resizing capabilities.
29-
30-
### VAE Tools 🖼️
31-
- **TAESDVaeEncode**: TAESD VAE encoding node.
32-
- **TAESDVaeDecode**: TAESD VAE decoding node.
29+
- **SimilarityFilter**: Filter out similar consecutive images and control downstream execution. Perfect for optimizing real-time workflows by skipping redundant processing of similar frames.
30+
- **LazyCondition**: Powerful conditional execution node that supports any input type. Uses lazy evaluation to truly skip execution of unused paths and maintains state to avoid feedback loops.
3331

3432
## Movement Patterns 🔄
3533

@@ -61,6 +59,12 @@ Sequence controls allow you to specify exact values to cycle through. You can co
6159
### FPS Monitor
6260
Outputs an image and mask showing current and average FPS. Useful for performance monitoring in real-time workflows.
6361

62+
### Utility Controls
63+
Use utility nodes to optimize and control your workflow:
64+
- **FPS Monitor**: Monitor performance with a visual overlay
65+
- **SimilarityFilter**: Skip processing of similar frames by comparing consecutive images. Great for optimizing real-time workflows by only processing frames that have meaningful changes.
66+
- **LazyCondition**: Create conditional execution paths that truly skip processing of unused branches. Works with any input type (images, latents, text, numbers) and maintains state of the last successful output to avoid feedback loops.
67+
6468
## Examples 🎬
6569

6670
### Value Control Demo

__init__.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
from .controls.value_controls import FloatControl, IntControl, StringControl
22
from .controls.sequence_controls import FloatSequence, IntSequence, StringSequence
3-
from .controls.utility_controls import FPSMonitor
3+
from .controls.utility_controls import FPSMonitor, SimilarityFilter, LazyCondition
44
from .controls.motion_controls import MotionController, ROINode, IntegerMotionController
55
from .quick_shape_mask import QuickShapeMask
6-
from .tiny_vae import TAESDVaeEncode, TAESDVaeDecode
76
from .misc_nodes import DTypeConverter, FastWebcamCapture
87

98
NODE_CLASS_MAPPINGS = {
@@ -14,14 +13,18 @@
1413
"IntSequence": IntSequence,
1514
"StringSequence": StringSequence,
1615
"FPSMonitor": FPSMonitor,
16+
"SimilarityFilter": SimilarityFilter,
17+
"LazyCondition": LazyCondition,
1718
"MotionController": MotionController,
1819
"IntegerMotionController": IntegerMotionController,
20+
21+
22+
23+
1924
"ROINode": ROINode,
2025
#"IntervalControl": IntervalCo ntrol,
2126
#"DeltaControl": DeltaControl,
2227
"QuickShapeMask": QuickShapeMask,
23-
"TAESDVaeEncode": TAESDVaeEncode,
24-
"TAESDVaeDecode": TAESDVaeDecode,
2528
"DTypeConverter": DTypeConverter,
2629
"FastWebcamCapture": FastWebcamCapture,
2730
}
@@ -44,8 +47,13 @@
4447
"TAESDVaeDecode": "TAESD VAE Decode (RyanOnTheInside)",
4548
"DTypeConverter": "DType Converter (RyanOnTheInside)",
4649
"FastWebcamCapture": "Fast Webcam Capture (RyanOnTheInside)",
50+
"SimilarityFilter": "Similarity Filter (RyanOnTheInside)",
51+
"LazyCondition": "Lazy Condition (RyanOnTheInside)",
52+
53+
4754
}
4855

56+
4957
WEB_DIRECTORY = "./web/js"
5058

5159
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']

controls/similar_image_filter.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import torch
2+
from typing import Optional
3+
import random
4+
5+
class SimilarImageFilter:
6+
def __init__(self, threshold: float = 0.98, max_skip_frame: float = 10) -> None:
7+
self.threshold = threshold
8+
9+
self.prev_tensor = None
10+
self.cos = torch.nn.CosineSimilarity(dim=0, eps=1e-6)
11+
self.max_skip_frame = max_skip_frame
12+
self.skip_count = 0
13+
14+
def __call__(self, x: torch.Tensor) -> Optional[torch.Tensor]:
15+
if self.prev_tensor is None:
16+
self.prev_tensor = x.detach().clone()
17+
return x
18+
else:
19+
cos_sim = self.cos(self.prev_tensor.reshape(-1), x.reshape(-1)).item()
20+
sample = random.uniform(0, 1)
21+
if self.threshold >= 1:
22+
skip_prob = 0
23+
else:
24+
skip_prob = max(0, 1 - (1 - cos_sim) / (1 - self.threshold))
25+
26+
# not skip frame
27+
if skip_prob < sample:
28+
self.prev_tensor = x.detach().clone()
29+
return x
30+
# skip frame
31+
else:
32+
if self.skip_count > self.max_skip_frame:
33+
self.skip_count = 0
34+
self.prev_tensor = x.detach().clone()
35+
return x
36+
else:
37+
self.skip_count += 1
38+
return None
39+
40+
def set_threshold(self, threshold: float) -> None:
41+
self.threshold = threshold
42+
43+
def set_max_skip_frame(self, max_skip_frame: float) -> None:
44+
self.max_skip_frame = max_skip_frame

controls/utility_controls.py

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
from ..base.control_base import ControlNodeBase
1+
import os
2+
import sys
23
import time
34
import numpy as np
45
import cv2
56
import torch
7+
import random
8+
9+
# Add package root to Python path
10+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11+
from base.control_base import ControlNodeBase
12+
from controls.similar_image_filter import SimilarImageFilter
613

714
class FPSMonitor(ControlNodeBase):
815
"""Generates an FPS overlay as an image and mask"""
@@ -121,3 +128,154 @@ def update(self, width: int, height: int, text_color: int, text_size: float, win
121128
self.set_state(state)
122129

123130
return (state["cached_image"], state["cached_mask"])
131+
132+
class SimilarityFilter(ControlNodeBase):
133+
DESCRIPTION = "Filters out similar consecutive images and outputs a signal to control downstream execution."
134+
135+
@classmethod
136+
def INPUT_TYPES(s):
137+
return {
138+
"required": {
139+
"image": ("IMAGE", {
140+
"tooltip": "Input image to compare with previous frame"
141+
}),
142+
"always_execute": ("BOOLEAN", {
143+
"default": False,
144+
}),
145+
"threshold": ("FLOAT", {
146+
"default": 0.98,
147+
"min": 0.0,
148+
"max": 1.0,
149+
"step": 0.01,
150+
"tooltip": "Similarity threshold (0-1). Higher values mean more frames are considered similar"
151+
}),
152+
"max_skip_frames": ("INT", {
153+
"default": 10,
154+
"min": 1,
155+
"max": 100,
156+
"step": 1,
157+
"tooltip": "Maximum number of consecutive frames to skip before forcing execution"
158+
}),
159+
}
160+
}
161+
162+
RETURN_TYPES = ("IMAGE", "BOOLEAN")
163+
RETURN_NAMES = ("image", "should_execute")
164+
FUNCTION = "update"
165+
CATEGORY = "real-time/control/utility"
166+
167+
def __init__(self):
168+
super().__init__()
169+
self._similarity_filter = SimilarImageFilter()
170+
171+
def update(self, image, threshold=0.98, max_skip_frames=10, always_execute=False):
172+
print(f"[DEBUG] Input image object: {hex(id(image))}, shape: {image.shape}, device: {image.device}")
173+
174+
# Get state with defaults
175+
state = self.get_state({
176+
"prev_image": None,
177+
"skip_count": 0
178+
})
179+
180+
# First frame case
181+
if state["prev_image"] is None:
182+
state["prev_image"] = image.detach().clone()
183+
state["skip_count"] = 0
184+
self.set_state(state)
185+
return (image, True) # Always execute first frame
186+
187+
# Update filter parameters
188+
self._similarity_filter.set_threshold(threshold)
189+
self._similarity_filter.set_max_skip_frame(max_skip_frames)
190+
191+
# Use filter to check similarity
192+
result = self._similarity_filter(image)
193+
should_execute = result is not None
194+
195+
# If we should skip (probability check)
196+
if not should_execute:
197+
# Check if we've skipped too many frames
198+
if state["skip_count"] >= max_skip_frames:
199+
state["prev_image"] = image.detach().clone()
200+
state["skip_count"] = 0
201+
self.set_state(state)
202+
return (image, True) # Force execution after max skips
203+
204+
# Skip frame - return previous image and False for execution
205+
state["skip_count"] += 1
206+
self.set_state(state)
207+
return (state["prev_image"], False)
208+
209+
# Frame is different enough - process it
210+
state["prev_image"] = image.detach().clone()
211+
state["skip_count"] = 0
212+
self.set_state(state)
213+
return (image, True)
214+
215+
class AlwaysEqualProxy(str):
216+
#borrowed from https://github.com/theUpsider/ComfyUI-Logic
217+
def __eq__(self, _):
218+
return True
219+
220+
def __ne__(self, _):
221+
return False
222+
223+
class LazyCondition(ControlNodeBase):
224+
DESCRIPTION = "Uses lazy evaluation to truly skip execution of unused paths. Maintains state of the last value to circumvent feedback loops."
225+
226+
@classmethod
227+
def INPUT_TYPES(s):
228+
return {
229+
"required": {
230+
"condition": (AlwaysEqualProxy("*"), {
231+
"tooltip": "When truthy (non-zero, non-empty, non-None), evaluates and returns if_true path. When falsy, returns either fallback or previous state of if_true.",
232+
"forceInput": True,
233+
}),
234+
"if_true": (AlwaysEqualProxy("*"), {
235+
"lazy": True,
236+
"tooltip": "The path that should only be evaluated when condition is truthy"
237+
}),
238+
"fallback": (AlwaysEqualProxy("*"), {
239+
"tooltip": "Alternative value to use when condition is falsy or no previous state of if_true"
240+
}),
241+
"use_fallback": ("BOOLEAN", {
242+
"default": False,
243+
"tooltip": "When False, uses last successful if_true result (if one exists). When True, uses fallback value"
244+
}),
245+
}
246+
}
247+
248+
RETURN_TYPES = (AlwaysEqualProxy("*"),)
249+
FUNCTION = "update"
250+
CATEGORY = "real-time/control/utility"
251+
252+
def check_lazy_status(self, condition, if_true, fallback, use_fallback):
253+
"""Only evaluate the if_true path if condition is truthy."""
254+
needed = ["fallback"] # Always need the fallback value
255+
if condition:
256+
needed.append("if_true")
257+
return needed
258+
259+
def update(self, condition, if_true, fallback, use_fallback):
260+
"""Route to either if_true output or fallback value, maintaining state of last if_true."""
261+
state = self.get_state({
262+
"prev_output": None
263+
})
264+
265+
if condition: # Let Python handle truthiness
266+
# Update last state when we run if_true path
267+
state["prev_output"] = if_true if if_true is not None else fallback
268+
if hasattr(if_true, 'detach'):
269+
state["prev_output"] = if_true.detach().clone()
270+
self.set_state(state)
271+
return (if_true,)
272+
else:
273+
if use_fallback or state["prev_output"] is None:
274+
return (fallback,)
275+
else:
276+
return (state["prev_output"],)
277+
278+
279+
280+
281+

0 commit comments

Comments
 (0)