├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── download_all_models.py ├── import_error_install.bat ├── install.bat ├── nodes.py ├── raft.py ├── requirements.txt ├── toy.png └── workflow_images ├── alpha_matte.png ├── enhance_detail.png └── guided_filter_alpha.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # models 7 | models/ 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/#use-with-ide 113 | .pdm.toml 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | #.idea/ 164 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 spacepxl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI-Image-Filters 2 | 3 | Image and matte filtering nodes for ComfyUI 4 | 5 | ``` 6 | latent/filters/* 7 | image/filters/* 8 | mask/filters/* 9 | ``` 10 | 11 | Two install batch files are provided, `install.bat` which only installs requirements, and `import_error_install.bat`, which uninstalls all versions of opencv-python before reinstalling only the correct version, opencv-contrib-python (use this if you get import errors relating to opencv or cv2, which are caused by having multiple versions or the wrong version of opencv installed.) 12 | 13 | ## Nodes 14 | 15 | ### Alpha Clean 16 | 17 | Clean up holes and near-solid areas in a matte. 18 | 19 | ### Alpha Matte 20 | 21 | Takes an image and alpha or trimap, and refines the edges with closed-form matting. Optionally extracts the foreground and background colors as well. Good for cleaning up SAM segments or hand drawn masks. 22 | 23 | ![alphamatte](https://github.com/spacepxl/ComfyUI-Image-Filters/blob/main/workflow_images/alpha_matte.png) 24 | 25 | ### Blur Image (Fast) 26 | 27 | Blurs images using opencv gaussian blur, which is >100x faster than comfy image blur. Supports larger blur radius, and separate x/y controls. 28 | 29 | ### Blur Mask (Fast) 30 | 31 | Same as Blur Image (Fast) but for masks instead of images. 32 | 33 | ### Dilate/Erode Mask 34 | 35 | Dilate or erode masks, with either a box or circle filter. 36 | 37 | ### Enhance Detail 38 | 39 | Increase or decrease details in an image or batch of images using a guided filter (as opposed to the typical gaussian blur used by most sharpening filters.) 40 | 41 | ![enhance](https://github.com/spacepxl/ComfyUI-Image-Filters/blob/main/workflow_images/enhance_detail.png) 42 | 43 | ### Guided Filter Alpha 44 | 45 | Use a guided filter to feather edges of a matte based on similar RGB colors. Works best with a strong color separation between FG and BG. 46 | 47 | ![guidedfilteralpha](https://github.com/spacepxl/ComfyUI-Image-Filters/blob/main/workflow_images/guided_filter_alpha.png) 48 | 49 | ### Remap Range 50 | 51 | Fits the color range of an image to a new blackpoint and whitepoint (clamped). Useful for clamping or thresholding soft mattes. 52 | 53 | ### Clamp Outliers 54 | 55 | Clamps latents that are more than n standard deviations away from 0. Could help with fireflies or stray noise that disrupt the VAE decode. 56 | 57 | ### AdaIN Latent/Image 58 | 59 | Normalizes latents/images to the mean and std dev of a reference input. Useful for getting rid of color shift from high denoise strength, or matching color to a reference in general. 60 | 61 | ### Batch Normalize Latent/Image 62 | 63 | Normalizes each frame in a batch to the overall mean and std dev, good for removing overall brightness flickering. 64 | 65 | ### Difference Checker 66 | 67 | Absolute value of the difference between inputs, with a multiplier to boost dark values for easier viewing. Alternative to the vanilla merge difference node, which is only a subtraction without the abs() 68 | 69 | ### Image Constant (RGB/HSV) 70 | 71 | Create an empty image of any color, either RGB or HSV 72 | 73 | ### Offset Latent Image 74 | 75 | Create an empty latent image with custom values, for offset noise but with per-channel control. Can be combined with Latent Stats to get channel values. 76 | 77 | ### Latent Stats 78 | 79 | Prints some stats about the latents (dimensions, and per-channel mean, std dev, min, and max) 80 | 81 | ### Tonemap / UnTonemap 82 | 83 | Apply or remove a log + contrast curve tonemap 84 | 85 | Apply tonemap: 86 | ``` 87 | power = 1.7 88 | SLog3R = clamp((log10((r + 0.01)/0.19) * 261.5 + 420) / 1023, 0, 1) 89 | SLog3G = clamp((log10((g + 0.01)/0.19) * 261.5 + 420) / 1023, 0, 1) 90 | SLog3B = clamp((log10((b + 0.01)/0.19) * 261.5 + 420) / 1023, 0, 1) 91 | 92 | r = r > 0.06 ? pow(1 / (1 + (1 / pow(SLog3R / (1 - SLog3R), power))), power) : r 93 | g = g > 0.06 ? pow(1 / (1 + (1 / pow(SLog3G / (1 - SLog3G), power))), power) : g 94 | b = b > 0.06 ? pow(1 / (1 + (1 / pow(SLog3B / (1 - SLog3B), power))), power) : b 95 | ``` 96 | 97 | Remove tonemap: 98 | ``` 99 | power = 1.7 100 | SR = 1 / (1 + pow((-1/pow(r, 1/power)) * (pow(r, 1/power) - 1), 1/power)) 101 | SG = 1 / (1 + pow((-1/pow(g, 1/power)) * (pow(g, 1/power) - 1), 1/power)) 102 | SB = 1 / (1 + pow((-1/pow(b, 1/power)) * (pow(b, 1/power) - 1), 1/power)) 103 | 104 | r = r > 0.06 ? pow(10, (SR * 1023 - 420)/261.5) * 0.19 - 0.01 : r 105 | g = g > 0.06 ? pow(10, (SG * 1023 - 420)/261.5) * 0.19 - 0.01 : g 106 | b = b > 0.06 ? pow(10, (SB * 1023 - 420)/261.5) * 0.19 - 0.01 : b 107 | ``` 108 | 109 | ### Exposure Adjust 110 | 111 | Linear exposure adjustment in f-stops, with optional tonemap. 112 | 113 | ### Convert Normals 114 | 115 | Translate between different normal map color spaces, with optional normalization fix and black region fix. 116 | 117 | ### Batch Average Image 118 | 119 | Returns the single average image of a batch. 120 | 121 | ### Normal Map (Simple) 122 | 123 | Simple high-frequency normal map from Scharr operator 124 | 125 | ### Keyer 126 | 127 | Image keyer with luma/sat/channel/greenscreen/etc options 128 | 129 | ### JitterImage, UnJitterImage, BatchAverageUnJittered 130 | 131 | For supersampling/antialiasing workflows. 132 | 133 | ### Shuffle 134 | 135 | Move channels around at will. 136 | 137 | ### ColorMatch 138 | 139 | Match image color to reference image, using mean or blur. Similar to AdaIN. 140 | 141 | ### RestoreDetail 142 | 143 | Transfers details from one image to another using frequency separation techniques. Useful for restoring the lost details from IC-Light or other img2img workflows. Has options for add/subtract method (fewer artifacts, but mostly ignores highlights) or divide/multiply (more natural but can create artifacts in areas that go from dark to bright), and either gaussian blur or guided filter (prevents oversharpened edges). 144 | 145 | ![restore_detail](https://github.com/spacepxl/ComfyUI-Image-Filters/assets/143970342/aa4fedce-e622-4ebe-b8e7-6348d37878a5) 146 | 147 | ### BetterFilmGrain 148 | 149 | Yet another film grain node, but this one looks better (realistic grain structure, no pixel-perfect RGB glitter, natural luminance/intensity response) and is 10x faster than the next best option (ProPostFilmGrain). 150 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS 2 | 3 | __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] -------------------------------------------------------------------------------- /download_all_models.py: -------------------------------------------------------------------------------- 1 | from raft import load_raft 2 | 3 | load_raft() -------------------------------------------------------------------------------- /import_error_install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set "requirements_txt=%~dp0\requirements.txt" 4 | set "python_exec=..\..\..\python_embeded\python.exe" 5 | 6 | echo installing requirements... 7 | 8 | if exist "%python_exec%" ( 9 | echo Installing with ComfyUI Portable 10 | %python_exec% -s -m pip uninstall -y opencv-python opencv-contrib-python opencv-python-headless opencv-contrib-python-headless 11 | for /f "delims=" %%i in (%requirements_txt%) do ( 12 | %python_exec% -s -m pip install "%%i" 13 | ) 14 | ) else ( 15 | echo Installing with system Python 16 | pip uninstall -y opencv-python opencv-contrib-python opencv-python-headless opencv-contrib-python-headless 17 | for /f "delims=" %%i in (%requirements_txt%) do ( 18 | pip install "%%i" 19 | ) 20 | ) 21 | 22 | pause -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set "requirements_txt=%~dp0\requirements.txt" 4 | set "python_exec=..\..\..\python_embeded\python.exe" 5 | 6 | echo installing requirements... 7 | 8 | if exist "%python_exec%" ( 9 | echo Installing with ComfyUI Portable 10 | for /f "delims=" %%i in (%requirements_txt%) do ( 11 | %python_exec% -s -m pip install "%%i" 12 | ) 13 | ) else ( 14 | echo Installing with system Python 15 | for /f "delims=" %%i in (%requirements_txt%) do ( 16 | pip install "%%i" 17 | ) 18 | ) 19 | 20 | pause -------------------------------------------------------------------------------- /nodes.py: -------------------------------------------------------------------------------- 1 | import math 2 | import copy 3 | import torch 4 | import torch.nn.functional as F 5 | import numpy as np 6 | import cv2 7 | from pymatting import estimate_alpha_cf, estimate_foreground_ml, fix_trimap 8 | from tqdm import trange 9 | 10 | try: 11 | from cv2.ximgproc import guidedFilter 12 | except ImportError: 13 | print("\033[33mUnable to import guidedFilter, make sure you have only opencv-contrib-python or run the import_error_install.bat script\033[m") 14 | 15 | import comfy.model_management 16 | import node_helpers 17 | from comfy.utils import ProgressBar 18 | from comfy_extras.nodes_post_processing import gaussian_kernel 19 | from .raft import * 20 | 21 | MAX_RESOLUTION=8192 22 | 23 | # gaussian blur a tensor image batch in format [B x H x W x C] on H/W (spatial, per-image, per-channel) 24 | def cv_blur_tensor(images, dx, dy): 25 | if min(dx, dy) > 100: 26 | np_img = F.interpolate(images.detach().clone().movedim(-1,1), scale_factor=0.1, mode='bilinear').movedim(1,-1).cpu().numpy() 27 | for index, image in enumerate(np_img): 28 | np_img[index] = cv2.GaussianBlur(image, (dx // 20 * 2 + 1, dy // 20 * 2 + 1), 0) 29 | return F.interpolate(torch.from_numpy(np_img).movedim(-1,1), size=(images.shape[1], images.shape[2]), mode='bilinear').movedim(1,-1) 30 | else: 31 | np_img = images.detach().clone().cpu().numpy() 32 | for index, image in enumerate(np_img): 33 | np_img[index] = cv2.GaussianBlur(image, (dx, dy), 0) 34 | return torch.from_numpy(np_img) 35 | 36 | # guided filter a tensor image batch in format [B x H x W x C] on H/W (spatial, per-image, per-channel) 37 | def guided_filter_tensor(ref, images, d, s): 38 | if d > 100: 39 | np_img = F.interpolate(images.detach().clone().movedim(-1,1), scale_factor=0.1, mode='bilinear').movedim(1,-1).cpu().numpy() 40 | np_ref = F.interpolate(ref.detach().clone().movedim(-1,1), scale_factor=0.1, mode='bilinear').movedim(1,-1).cpu().numpy() 41 | for index, image in enumerate(np_img): 42 | np_img[index] = guidedFilter(np_ref[index], image, d // 20 * 2 + 1, s) 43 | return F.interpolate(torch.from_numpy(np_img).movedim(-1,1), size=(images.shape[1], images.shape[2]), mode='bilinear').movedim(1,-1) 44 | else: 45 | np_img = images.detach().clone().cpu().numpy() 46 | np_ref = ref.cpu().numpy() 47 | for index, image in enumerate(np_img): 48 | np_img[index] = guidedFilter(np_ref[index], image, d, s) 49 | return torch.from_numpy(np_img) 50 | 51 | # std_dev and mean of tensor t within local spatial filter size d, per-image, per-channel [B x H x W x C] 52 | def std_mean_filter(t, d): 53 | t_mean = cv_blur_tensor(t, d, d) 54 | t_diff_squared = (t - t_mean) ** 2 55 | t_std = torch.sqrt(cv_blur_tensor(t_diff_squared, d, d)) 56 | return t_std, t_mean 57 | 58 | def RGB2YCbCr(t): 59 | YCbCr = t.detach().clone() 60 | YCbCr[:,:,:,0] = 0.2123 * t[:,:,:,0] + 0.7152 * t[:,:,:,1] + 0.0722 * t[:,:,:,2] 61 | YCbCr[:,:,:,1] = 0 - 0.1146 * t[:,:,:,0] - 0.3854 * t[:,:,:,1] + 0.5 * t[:,:,:,2] 62 | YCbCr[:,:,:,2] = 0.5 * t[:,:,:,0] - 0.4542 * t[:,:,:,1] - 0.0458 * t[:,:,:,2] 63 | return YCbCr 64 | 65 | def YCbCr2RGB(t): 66 | RGB = t.detach().clone() 67 | RGB[:,:,:,0] = t[:,:,:,0] + 1.5748 * t[:,:,:,2] 68 | RGB[:,:,:,1] = t[:,:,:,0] - 0.1873 * t[:,:,:,1] - 0.4681 * t[:,:,:,2] 69 | RGB[:,:,:,2] = t[:,:,:,0] + 1.8556 * t[:,:,:,1] 70 | return RGB 71 | 72 | def hsv_to_rgb(h, s, v): 73 | if s: 74 | if h == 1.0: h = 0.0 75 | i = int(h*6.0) 76 | f = h*6.0 - i 77 | 78 | w = v * (1.0 - s) 79 | q = v * (1.0 - s * f) 80 | t = v * (1.0 - s * (1.0 - f)) 81 | 82 | if i==0: return (v, t, w) 83 | if i==1: return (q, v, w) 84 | if i==2: return (w, v, t) 85 | if i==3: return (w, q, v) 86 | if i==4: return (t, w, v) 87 | if i==5: return (v, w, q) 88 | else: return (v, v, v) 89 | 90 | def sRGBtoLinear(npArray): 91 | less = npArray <= 0.0404482362771082 92 | npArray[less] = npArray[less] / 12.92 93 | npArray[~less] = np.power((npArray[~less] + 0.055) / 1.055, 2.4) 94 | 95 | def linearToSRGB(npArray): 96 | less = npArray <= 0.0031308 97 | npArray[less] = npArray[less] * 12.92 98 | npArray[~less] = np.power(npArray[~less], 1/2.4) * 1.055 - 0.055 99 | 100 | def linearToTonemap(npArray, tonemap_scale): 101 | npArray /= tonemap_scale 102 | more = npArray > 0.06 103 | SLog3 = np.clip((np.log10((npArray + 0.01)/0.19) * 261.5 + 420) / 1023, 0, 1) 104 | npArray[more] = np.power(1 / (1 + (1 / np.power(SLog3[more] / (1 - SLog3[more]), 1.7))), 1.7) 105 | npArray *= tonemap_scale 106 | 107 | def tonemapToLinear(npArray, tonemap_scale): 108 | npArray /= tonemap_scale 109 | more = npArray > 0.06 110 | x = np.power(np.clip(npArray, 0.000001, 1), 1/1.7) 111 | ut = 1 / (1 + np.power((-1 / x) * (x - 1), 1/1.7)) 112 | npArray[more] = np.power(10, (ut[more] * 1023 - 420)/261.5) * 0.19 - 0.01 113 | npArray *= tonemap_scale 114 | 115 | def exposure(npArray, stops): 116 | more = npArray > 0 117 | npArray[more] *= pow(2, stops) 118 | 119 | def randn_like_g(x, generator=None): 120 | device = generator.device if generator is not None else x.device 121 | r = torch.randn(x.size(), generator=generator, dtype=x.dtype, layout=x.layout, device=device) 122 | return r.to(x.device) 123 | 124 | class AlphaClean: 125 | def __init__(self): 126 | pass 127 | 128 | @classmethod 129 | def INPUT_TYPES(s): 130 | return { 131 | "required": { 132 | "images": ("IMAGE",), 133 | "radius": ("INT", { 134 | "default": 8, 135 | "min": 1, 136 | "max": 64, 137 | "step": 1 138 | }), 139 | "fill_holes": ("INT", { 140 | "default": 1, 141 | "min": 0, 142 | "max": 16, 143 | "step": 1 144 | }), 145 | "white_threshold": ("FLOAT", { 146 | "default": 0.9, 147 | "min": 0.01, 148 | "max": 1.0, 149 | "step": 0.01 150 | }), 151 | "extra_clip": ("FLOAT", { 152 | "default": 0.98, 153 | "min": 0.01, 154 | "max": 1.0, 155 | "step": 0.01 156 | }), 157 | }, 158 | } 159 | 160 | RETURN_TYPES = ("IMAGE",) 161 | FUNCTION = "alpha_clean" 162 | 163 | CATEGORY = "image/filters" 164 | 165 | def alpha_clean(self, images: torch.Tensor, radius: int, fill_holes: int, white_threshold: float, extra_clip: float): 166 | 167 | d = radius * 2 + 1 168 | i_dup = copy.deepcopy(images.cpu().numpy()) 169 | 170 | for index, image in enumerate(i_dup): 171 | 172 | cleaned = cv2.bilateralFilter(image, 9, 0.05, 8) 173 | 174 | alpha = np.clip((image - white_threshold) / (1 - white_threshold), 0, 1) 175 | rgb = image * alpha 176 | 177 | alpha = cv2.GaussianBlur(alpha, (d,d), 0) * 0.99 + np.average(alpha) * 0.01 178 | rgb = cv2.GaussianBlur(rgb, (d,d), 0) * 0.99 + np.average(rgb) * 0.01 179 | 180 | rgb = rgb / np.clip(alpha, 0.00001, 1) 181 | rgb = rgb * extra_clip 182 | 183 | cleaned = np.clip(cleaned / rgb, 0, 1) 184 | 185 | if fill_holes > 0: 186 | fD = fill_holes * 2 + 1 187 | gamma = cleaned * cleaned 188 | kD = np.ones((fD, fD), np.uint8) 189 | kE = np.ones((fD + 2, fD + 2), np.uint8) 190 | gamma = cv2.dilate(gamma, kD, iterations=1) 191 | gamma = cv2.erode(gamma, kE, iterations=1) 192 | gamma = cv2.GaussianBlur(gamma, (fD, fD), 0) 193 | cleaned = np.maximum(cleaned, gamma) 194 | 195 | i_dup[index] = cleaned 196 | 197 | return (torch.from_numpy(i_dup),) 198 | 199 | class AlphaMatte: 200 | def __init__(self): 201 | pass 202 | 203 | @classmethod 204 | def INPUT_TYPES(s): 205 | return { 206 | "required": { 207 | "images": ("IMAGE",), 208 | "alpha_trimap": ("IMAGE",), 209 | "preblur": ("INT", { 210 | "default": 8, 211 | "min": 0, 212 | "max": 256, 213 | "step": 1 214 | }), 215 | "blackpoint": ("FLOAT", { 216 | "default": 0.01, 217 | "min": 0.0, 218 | "max": 0.99, 219 | "step": 0.01 220 | }), 221 | "whitepoint": ("FLOAT", { 222 | "default": 0.99, 223 | "min": 0.01, 224 | "max": 1.0, 225 | "step": 0.01 226 | }), 227 | "max_iterations": ("INT", { 228 | "default": 1000, 229 | "min": 100, 230 | "max": 10000, 231 | "step": 100 232 | }), 233 | "estimate_fg": (["true", "false"],), 234 | }, 235 | } 236 | 237 | RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE",) 238 | RETURN_NAMES = ("alpha", "fg", "bg",) 239 | FUNCTION = "alpha_matte" 240 | 241 | CATEGORY = "image/filters" 242 | 243 | def alpha_matte(self, images, alpha_trimap, preblur, blackpoint, whitepoint, max_iterations, estimate_fg): 244 | 245 | d = preblur * 2 + 1 246 | 247 | i_dup = copy.deepcopy(images.cpu().numpy().astype(np.float64)) 248 | a_dup = copy.deepcopy(alpha_trimap.cpu().numpy().astype(np.float64)) 249 | fg = copy.deepcopy(images.cpu().numpy().astype(np.float64)) 250 | bg = copy.deepcopy(images.cpu().numpy().astype(np.float64)) 251 | 252 | 253 | for index, image in enumerate(i_dup): 254 | trimap = a_dup[index][:,:,0] # convert to single channel 255 | if preblur > 0: 256 | trimap = cv2.GaussianBlur(trimap, (d, d), 0) 257 | trimap = fix_trimap(trimap, blackpoint, whitepoint) 258 | 259 | alpha = estimate_alpha_cf(image, trimap, laplacian_kwargs={"epsilon": 1e-6}, cg_kwargs={"maxiter":max_iterations}) 260 | 261 | if estimate_fg == "true": 262 | fg[index], bg[index] = estimate_foreground_ml(image, alpha, return_background=True) 263 | 264 | a_dup[index] = np.stack([alpha, alpha, alpha], axis = -1) # convert back to rgb 265 | 266 | return ( 267 | torch.from_numpy(a_dup.astype(np.float32)), # alpha 268 | torch.from_numpy(fg.astype(np.float32)), # fg 269 | torch.from_numpy(bg.astype(np.float32)), # bg 270 | ) 271 | 272 | class BetterFilmGrain: 273 | @classmethod 274 | def INPUT_TYPES(s): 275 | return { 276 | "required": { 277 | "image": ("IMAGE",), 278 | "scale": ("FLOAT", {"default": 0.5, "min": 0.25, "max": 2.0, "step": 0.05}), 279 | "strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.01}), 280 | "saturation": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.01}), 281 | "toe": ("FLOAT", {"default": 0.0, "min": -0.2, "max": 0.5, "step": 0.001}), 282 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 283 | }, 284 | } 285 | 286 | RETURN_TYPES = ("IMAGE",) 287 | FUNCTION = "grain" 288 | 289 | CATEGORY = "image/filters" 290 | 291 | def grain(self, image, scale, strength, saturation, toe, seed): 292 | t = image.detach().clone() 293 | torch.manual_seed(seed) 294 | grain = torch.rand(t.shape[0], int(t.shape[1] // scale), int(t.shape[2] // scale), 3) 295 | 296 | YCbCr = RGB2YCbCr(grain) 297 | YCbCr[:,:,:,0] = cv_blur_tensor(YCbCr[:,:,:,0], 3, 3) 298 | YCbCr[:,:,:,1] = cv_blur_tensor(YCbCr[:,:,:,1], 15, 15) 299 | YCbCr[:,:,:,2] = cv_blur_tensor(YCbCr[:,:,:,2], 11, 11) 300 | 301 | grain = (YCbCr2RGB(YCbCr) - 0.5) * strength 302 | grain[:,:,:,0] *= 2 303 | grain[:,:,:,2] *= 3 304 | grain += 1 305 | grain = grain * saturation + grain[:,:,:,1].unsqueeze(3).repeat(1,1,1,3) * (1 - saturation) 306 | 307 | grain = F.interpolate(grain.movedim(-1,1), size=(t.shape[1], t.shape[2]), mode='bilinear').movedim(1,-1) 308 | t[:,:,:,:3] = torch.clip((1 - (1 - t[:,:,:,:3]) * grain) * (1 - toe) + toe, 0, 1) 309 | return(t,) 310 | 311 | class BlurImageFast: 312 | def __init__(self): 313 | pass 314 | 315 | @classmethod 316 | def INPUT_TYPES(s): 317 | return { 318 | "required": { 319 | "images": ("IMAGE",), 320 | "radius_x": ("INT", { 321 | "default": 1, 322 | "min": 0, 323 | "max": 1023, 324 | "step": 1 325 | }), 326 | "radius_y": ("INT", { 327 | "default": 1, 328 | "min": 0, 329 | "max": 1023, 330 | "step": 1 331 | }), 332 | }, 333 | } 334 | 335 | RETURN_TYPES = ("IMAGE",) 336 | FUNCTION = "blur_image" 337 | 338 | CATEGORY = "image/filters" 339 | 340 | def blur_image(self, images, radius_x, radius_y): 341 | 342 | if radius_x + radius_y == 0: 343 | return (images,) 344 | 345 | dx = radius_x * 2 + 1 346 | dy = radius_y * 2 + 1 347 | 348 | dup = copy.deepcopy(images.cpu().numpy()) 349 | 350 | for index, image in enumerate(dup): 351 | dup[index] = cv2.GaussianBlur(image, (dx, dy), 0) 352 | 353 | return (torch.from_numpy(dup),) 354 | 355 | class BlurMaskFast: 356 | def __init__(self): 357 | pass 358 | 359 | @classmethod 360 | def INPUT_TYPES(s): 361 | return { 362 | "required": { 363 | "masks": ("MASK",), 364 | "radius_x": ("INT", { 365 | "default": 1, 366 | "min": 0, 367 | "max": 1023, 368 | "step": 1 369 | }), 370 | "radius_y": ("INT", { 371 | "default": 1, 372 | "min": 0, 373 | "max": 1023, 374 | "step": 1 375 | }), 376 | }, 377 | } 378 | 379 | RETURN_TYPES = ("MASK",) 380 | FUNCTION = "blur_mask" 381 | 382 | CATEGORY = "mask/filters" 383 | 384 | def blur_mask(self, masks, radius_x, radius_y): 385 | 386 | if radius_x + radius_y == 0: 387 | return (masks,) 388 | 389 | dx = radius_x * 2 + 1 390 | dy = radius_y * 2 + 1 391 | 392 | dup = copy.deepcopy(masks.cpu().numpy()) 393 | 394 | for index, mask in enumerate(dup): 395 | dup[index] = cv2.GaussianBlur(mask, (dx, dy), 0) 396 | 397 | return (torch.from_numpy(dup),) 398 | 399 | class ColorMatchImage: 400 | @classmethod 401 | def INPUT_TYPES(s): 402 | return { 403 | "required": { 404 | "images": ("IMAGE", ), 405 | "reference": ("IMAGE", ), 406 | "blur_type": (["blur", "guidedFilter"],), 407 | "blur_size": ("INT", {"default": 0, "min": 0, "max": 1023}), 408 | "factor": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "round": 0.01}), 409 | }, 410 | } 411 | 412 | RETURN_TYPES = ("IMAGE",) 413 | FUNCTION = "batch_normalize" 414 | 415 | CATEGORY = "image/filters" 416 | 417 | def batch_normalize(self, images, reference, blur_type, blur_size, factor): 418 | t = images.detach().clone() + 0.1 419 | ref = reference.detach().clone() + 0.1 420 | 421 | if ref.shape[0] < t.shape[0]: 422 | ref = ref[0].unsqueeze(0).repeat(t.shape[0], 1, 1, 1) 423 | 424 | if blur_size == 0: 425 | mean = torch.mean(t, (1,2), keepdim=True) 426 | mean_ref = torch.mean(ref, (1,2), keepdim=True) 427 | 428 | for i in range(t.shape[0]): 429 | for c in range(3): 430 | t[i,:,:,c] /= mean[i,0,0,c] 431 | t[i,:,:,c] *= mean_ref[i,0,0,c] 432 | else: 433 | d = blur_size * 2 + 1 434 | 435 | if blur_type == "blur": 436 | blurred = cv_blur_tensor(t, d, d) 437 | blurred_ref = cv_blur_tensor(ref, d, d) 438 | elif blur_type == "guidedFilter": 439 | blurred = guided_filter_tensor(t, t, d, 0.01) 440 | blurred_ref = guided_filter_tensor(ref, ref, d, 0.01) 441 | 442 | for i in range(t.shape[0]): 443 | for c in range(3): 444 | t[i,:,:,c] /= blurred[i,:,:,c] 445 | t[i,:,:,c] *= blurred_ref[i,:,:,c] 446 | 447 | t = t - 0.1 448 | torch.clamp(torch.lerp(images, t, factor), 0, 1) 449 | return (t,) 450 | 451 | class RestoreDetail: 452 | @classmethod 453 | def INPUT_TYPES(s): 454 | return { 455 | "required": { 456 | "images": ("IMAGE", ), 457 | "detail": ("IMAGE", ), 458 | "mode": (["add", "multiply"],), 459 | "blur_type": (["blur", "guidedFilter"],), 460 | "blur_size": ("INT", {"default": 1, "min": 1, "max": 1023}), 461 | "factor": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "round": 0.01}), 462 | }, 463 | } 464 | 465 | RETURN_TYPES = ("IMAGE",) 466 | FUNCTION = "batch_normalize" 467 | 468 | CATEGORY = "image/filters" 469 | 470 | def batch_normalize(self, images, detail, mode, blur_type, blur_size, factor): 471 | t = images.detach().clone() + 0.1 472 | ref = detail.detach().clone() + 0.1 473 | 474 | if ref.shape[0] < t.shape[0]: 475 | ref = ref[0].unsqueeze(0).repeat(t.shape[0], 1, 1, 1) 476 | 477 | d = blur_size * 2 + 1 478 | 479 | if blur_type == "blur": 480 | blurred = cv_blur_tensor(t, d, d) 481 | blurred_ref = cv_blur_tensor(ref, d, d) 482 | elif blur_type == "guidedFilter": 483 | blurred = guided_filter_tensor(t, t, d, 0.01) 484 | blurred_ref = guided_filter_tensor(ref, ref, d, 0.01) 485 | 486 | if mode == "multiply": 487 | t = (ref / blurred_ref) * blurred 488 | else: 489 | t = (ref - blurred_ref) + blurred 490 | 491 | t = t - 0.1 492 | t = torch.clamp(torch.lerp(images, t, factor), 0, 1) 493 | return (t,) 494 | 495 | class DilateErodeMask: 496 | def __init__(self): 497 | pass 498 | 499 | @classmethod 500 | def INPUT_TYPES(s): 501 | return { 502 | "required": { 503 | "masks": ("MASK",), 504 | "radius": ("INT", { 505 | "default": 0, 506 | "min": -1023, 507 | "max": 1023, 508 | "step": 1 509 | }), 510 | "shape": (["box", "circle"],), 511 | }, 512 | } 513 | 514 | RETURN_TYPES = ("MASK",) 515 | FUNCTION = "dilate_mask" 516 | 517 | CATEGORY = "mask/filters" 518 | 519 | def dilate_mask(self, masks, radius, shape): 520 | 521 | if radius == 0: 522 | return (masks,) 523 | 524 | s = abs(radius) 525 | d = s * 2 + 1 526 | k = np.zeros((d, d), np.uint8) 527 | if shape == "circle": 528 | k = cv2.circle(k, (s,s), s, 1, -1) 529 | else: 530 | k += 1 531 | 532 | dup = copy.deepcopy(masks.cpu().numpy()) 533 | 534 | for index, mask in enumerate(dup): 535 | if radius > 0: 536 | dup[index] = cv2.dilate(mask, k, iterations=1) 537 | else: 538 | dup[index] = cv2.erode(mask, k, iterations=1) 539 | 540 | return (torch.from_numpy(dup),) 541 | 542 | class EnhanceDetail: 543 | def __init__(self): 544 | pass 545 | 546 | @classmethod 547 | def INPUT_TYPES(s): 548 | return { 549 | "required": { 550 | "images": ("IMAGE",), 551 | "filter_radius": ("INT", { 552 | "default": 2, 553 | "min": 1, 554 | "max": 64, 555 | "step": 1 556 | }), 557 | "sigma": ("FLOAT", { 558 | "default": 0.1, 559 | "min": 0.01, 560 | "max": 100.0, 561 | "step": 0.01 562 | }), 563 | "denoise": ("FLOAT", { 564 | "default": 0.1, 565 | "min": 0.0, 566 | "max": 10.0, 567 | "step": 0.01 568 | }), 569 | "detail_mult": ("FLOAT", { 570 | "default": 2.0, 571 | "min": 0.0, 572 | "max": 100.0, 573 | "step": 0.1 574 | }), 575 | }, 576 | } 577 | 578 | RETURN_TYPES = ("IMAGE",) 579 | FUNCTION = "enhance" 580 | 581 | CATEGORY = "image/filters" 582 | 583 | def enhance(self, images: torch.Tensor, filter_radius: int, sigma: float, denoise: float, detail_mult: float): 584 | 585 | if filter_radius == 0: 586 | return (images,) 587 | 588 | d = filter_radius * 2 + 1 589 | s = sigma / 10 590 | n = denoise / 10 591 | 592 | dup = copy.deepcopy(images.cpu().numpy()) 593 | 594 | for index, image in enumerate(dup): 595 | imgB = image 596 | if denoise>0.0: 597 | imgB = cv2.bilateralFilter(image, d, n, d) 598 | 599 | imgG = np.clip(guidedFilter(image, image, d, s), 0.001, 1) 600 | 601 | details = (imgB/imgG - 1) * detail_mult + 1 602 | dup[index] = np.clip(details*imgG - imgB + image, 0, 1) 603 | 604 | return (torch.from_numpy(dup),) 605 | 606 | # DEPRECATED: use GuidedFilterImage instead 607 | class GuidedFilterAlpha: 608 | def __init__(self): 609 | pass 610 | 611 | @classmethod 612 | def INPUT_TYPES(s): 613 | return { 614 | "required": { 615 | "images": ("IMAGE",), 616 | "alpha": ("IMAGE",), 617 | "filter_radius": ("INT", { 618 | "default": 8, 619 | "min": 1, 620 | "max": 64, 621 | "step": 1 622 | }), 623 | "sigma": ("FLOAT", { 624 | "default": 0.1, 625 | "min": 0.01, 626 | "max": 1.0, 627 | "step": 0.01 628 | }), 629 | }, 630 | } 631 | 632 | RETURN_TYPES = ("IMAGE",) 633 | FUNCTION = "guided_filter_alpha" 634 | 635 | CATEGORY = "image/filters" 636 | 637 | def guided_filter_alpha(self, images: torch.Tensor, alpha: torch.Tensor, filter_radius: int, sigma: float): 638 | 639 | d = filter_radius * 2 + 1 640 | s = sigma / 10 641 | 642 | i_dup = copy.deepcopy(images.cpu().numpy()) 643 | a_dup = copy.deepcopy(alpha.cpu().numpy()) 644 | 645 | for index, image in enumerate(i_dup): 646 | alpha_work = a_dup[index] 647 | i_dup[index] = guidedFilter(image, alpha_work, d, s) 648 | 649 | return (torch.from_numpy(i_dup),) 650 | 651 | class GuidedFilterImage: 652 | @classmethod 653 | def INPUT_TYPES(s): 654 | return { 655 | "required": { 656 | "images": ("IMAGE", ), 657 | "guide": ("IMAGE", ), 658 | "size": ("INT", {"default": 4, "min": 0, "max": 1023}), 659 | "sigma": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 100.0, "step": 0.01}), 660 | }, 661 | } 662 | 663 | RETURN_TYPES = ("IMAGE",) 664 | FUNCTION = "filter_image" 665 | 666 | CATEGORY = "image/filters" 667 | 668 | def filter_image(self, images, guide, size, sigma): 669 | d = size * 2 + 1 670 | s = sigma / 10 671 | filtered = guided_filter_tensor(guide, images, d, s) 672 | return (filtered,) 673 | 674 | class MedianFilterImage: 675 | @classmethod 676 | def INPUT_TYPES(s): 677 | return { 678 | "required": { 679 | "images": ("IMAGE", ), 680 | "size": ("INT", {"default": 1, "min": 1, "max": 1023}), 681 | }, 682 | } 683 | 684 | RETURN_TYPES = ("IMAGE",) 685 | FUNCTION = "filter_image" 686 | 687 | CATEGORY = "image/filters" 688 | 689 | def filter_image(self, images, size): 690 | np_images = images.detach().clone().cpu().numpy() 691 | d = size * 2 + 1 692 | for index, image in enumerate(np_images): 693 | if d > 5: 694 | work_image = image * 255 695 | work_image = cv2.medianBlur(work_image.astype(np.uint8), d) 696 | np_images[index] = work_image.astype(np.float32) / 255 697 | else: 698 | np_images[index] = cv2.medianBlur(image, d) 699 | return (torch.from_numpy(np_images),) 700 | 701 | class BilateralFilterImage: 702 | @classmethod 703 | def INPUT_TYPES(s): 704 | return { 705 | "required": { 706 | "images": ("IMAGE", ), 707 | "size": ("INT", {"default": 8, "min": 1, "max": 64}), 708 | "sigma_color": ("FLOAT", {"default": 0.5, "min": 0.01, "max": 1000.0, "step": 0.01}), 709 | "sigma_space": ("FLOAT", {"default": 100.0, "min": 0.01, "max": 1000.0, "step": 0.01}), 710 | }, 711 | } 712 | 713 | RETURN_TYPES = ("IMAGE",) 714 | FUNCTION = "filter_image" 715 | 716 | CATEGORY = "image/filters" 717 | 718 | def filter_image(self, images, size, sigma_color, sigma_space): 719 | np_images = images.detach().clone().cpu().numpy() 720 | d = size * 2 + 1 721 | for index, image in enumerate(np_images): 722 | np_images[index] = cv2.bilateralFilter(image, d, sigma_color, sigma_space) 723 | return (torch.from_numpy(np_images),) 724 | 725 | class FrequencyCombine: 726 | @classmethod 727 | def INPUT_TYPES(s): 728 | return { 729 | "required": { 730 | "high_frequency": ("IMAGE", ), 731 | "low_frequency": ("IMAGE", ), 732 | "mode": (["subtract", "divide"],), 733 | "eps": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 0.99, "step": 0.01}), 734 | }, 735 | } 736 | 737 | RETURN_TYPES = ("IMAGE",) 738 | FUNCTION = "filter_image" 739 | 740 | CATEGORY = "image/filters" 741 | 742 | def filter_image(self, high_frequency, low_frequency, mode, eps): 743 | t = low_frequency.detach().clone() 744 | if mode == "subtract": 745 | t = t + high_frequency - 0.5 746 | else: 747 | t = (high_frequency * 2) * (t + eps) - eps 748 | return (torch.clamp(t, 0, 1),) 749 | 750 | class FrequencySeparate: 751 | @classmethod 752 | def INPUT_TYPES(s): 753 | return { 754 | "required": { 755 | "original": ("IMAGE", ), 756 | "low_frequency": ("IMAGE", ), 757 | "mode": (["subtract", "divide"],), 758 | "eps": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 0.99, "step": 0.01}), 759 | }, 760 | } 761 | 762 | RETURN_TYPES = ("IMAGE",) 763 | RETURN_NAMES = ("high_frequency",) 764 | FUNCTION = "filter_image" 765 | 766 | CATEGORY = "image/filters" 767 | 768 | def filter_image(self, original, low_frequency, mode, eps): 769 | t = original.detach().clone() 770 | if mode == "subtract": 771 | t = t - low_frequency + 0.5 772 | else: 773 | t = ((t + eps) / (low_frequency + eps)) * 0.5 774 | return (t,) 775 | 776 | class RemapRange: 777 | @classmethod 778 | def INPUT_TYPES(s): 779 | return { 780 | "required": { 781 | "image": ("IMAGE",), 782 | "blackpoint": ("FLOAT", { 783 | "default": 0.0, 784 | "min": 0.0, 785 | "max": 1.0, 786 | "step": 0.01 787 | }), 788 | "whitepoint": ("FLOAT", { 789 | "default": 1.0, 790 | "min": 0.01, 791 | "max": 1.0, 792 | "step": 0.01 793 | }), 794 | }, 795 | } 796 | 797 | RETURN_TYPES = ("IMAGE",) 798 | FUNCTION = "remap" 799 | 800 | CATEGORY = "image/filters" 801 | 802 | def remap(self, image: torch.Tensor, blackpoint: float, whitepoint: float): 803 | 804 | bp = min(blackpoint, whitepoint - 0.001) 805 | scale = 1 / (whitepoint - bp) 806 | 807 | i_dup = copy.deepcopy(image.cpu().numpy()) 808 | i_dup = np.clip((i_dup - bp) * scale, 0.0, 1.0) 809 | 810 | return (torch.from_numpy(i_dup),) 811 | 812 | class ClampImage: 813 | @classmethod 814 | def INPUT_TYPES(s): 815 | return { 816 | "required": { 817 | "image": ("IMAGE",), 818 | "blackpoint": ("FLOAT", { 819 | "default": 0.0, 820 | "min": 0.0, 821 | "max": 1.0, 822 | "step": 0.001 823 | }), 824 | "whitepoint": ("FLOAT", { 825 | "default": 1.0, 826 | "min": 0.0, 827 | "max": 1.0, 828 | "step": 0.001 829 | }), 830 | }, 831 | } 832 | 833 | RETURN_TYPES = ("IMAGE",) 834 | FUNCTION = "clamp_image" 835 | 836 | CATEGORY = "image/filters" 837 | 838 | def clamp_image(self, image: torch.Tensor, blackpoint: float, whitepoint: float): 839 | clamped_image = torch.clamp(torch.nan_to_num(image.detach().clone()), min=blackpoint, max=whitepoint) 840 | return (clamped_image,) 841 | 842 | Channel_List = ["red", "green", "blue", "alpha", "white", "black"] 843 | Alpha_List = ["red", "green", "blue", "alpha", "white", "black", "none"] 844 | class ShuffleChannels: 845 | @classmethod 846 | def INPUT_TYPES(s): 847 | return { 848 | "required": { 849 | "image": ("IMAGE",), 850 | "red": (Channel_List, {"default": "red"}), 851 | "green": (Channel_List, {"default": "green"}), 852 | "blue": (Channel_List, {"default": "blue"}), 853 | "alpha": (Alpha_List, {"default": "none"}), 854 | }, 855 | } 856 | 857 | RETURN_TYPES = ("IMAGE",) 858 | FUNCTION = "shuffle" 859 | 860 | CATEGORY = "image/filters" 861 | 862 | def shuffle(self, image, red, green, blue, alpha): 863 | ch = 3 if alpha == "none" else 4 864 | t = torch.zeros((image.shape[0], image.shape[1], image.shape[2], ch), dtype=image.dtype, device=image.device) 865 | image_copy = image.detach().clone() 866 | 867 | ch_key = [red, green, blue, alpha] 868 | for i in range(ch): 869 | if ch_key[i] == "white": 870 | t[:,:,:,i] = 1 871 | elif ch_key[i] == "red": 872 | t[:,:,:,i] = image_copy[:,:,:,0] 873 | elif ch_key[i] == "green": 874 | t[:,:,:,i] = image_copy[:,:,:,1] 875 | elif ch_key[i] == "blue": 876 | t[:,:,:,i] = image_copy[:,:,:,2] 877 | elif ch_key[i] == "alpha": 878 | if image.shape[3] > 3: 879 | t[:,:,:,i] = image_copy[:,:,:,3] 880 | else: 881 | t[:,:,:,i] = 1 882 | 883 | return(t,) 884 | 885 | class ClampOutliers: 886 | def __init__(self): 887 | pass 888 | 889 | @classmethod 890 | def INPUT_TYPES(s): 891 | return { 892 | "required": { 893 | "latents": ("LATENT", ), 894 | "std_dev": ("FLOAT", {"default": 3.0, "min": 0.1, "max": 100.0, "step": 0.1, "round": 0.1}), 895 | }, 896 | } 897 | 898 | RETURN_TYPES = ("LATENT",) 899 | FUNCTION = "clamp_outliers" 900 | 901 | CATEGORY = "latent/filters" 902 | 903 | def clamp_outliers(self, latents, std_dev): 904 | latents_copy = copy.deepcopy(latents) 905 | t = latents_copy["samples"] 906 | 907 | for i, latent in enumerate(t): 908 | for j, channel in enumerate(latent): 909 | sd, mean = torch.std_mean(channel, dim=None) 910 | t[i,j] = torch.clamp(channel, min = -sd * std_dev + mean, max = sd * std_dev + mean) 911 | 912 | latents_copy["samples"] = t 913 | return (latents_copy,) 914 | 915 | class AdainLatent: 916 | def __init__(self): 917 | pass 918 | 919 | @classmethod 920 | def INPUT_TYPES(s): 921 | return { 922 | "required": { 923 | "latents": ("LATENT", ), 924 | "reference": ("LATENT", ), 925 | "factor": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "round": 0.01}), 926 | }, 927 | } 928 | 929 | RETURN_TYPES = ("LATENT",) 930 | FUNCTION = "batch_normalize" 931 | 932 | CATEGORY = "latent/filters" 933 | 934 | def batch_normalize(self, latents, reference, factor): 935 | latents_copy = copy.deepcopy(latents) 936 | t = latents_copy["samples"] # [B x C x H x W] 937 | 938 | t = t.movedim(0,1) # [C x B x H x W] 939 | for c in range(t.size(0)): 940 | for i in range(t.size(1)): 941 | r_sd, r_mean = torch.std_mean(reference["samples"][i, c], dim=None) # index by original dim order 942 | i_sd, i_mean = torch.std_mean(t[c, i], dim=None) 943 | 944 | t[c, i] = ((t[c, i] - i_mean) / i_sd) * r_sd + r_mean 945 | 946 | latents_copy["samples"] = torch.lerp(latents["samples"], t.movedim(1,0), factor) # [B x C x H x W] 947 | return (latents_copy,) 948 | 949 | class AdainFilterLatent: 950 | def __init__(self): 951 | pass 952 | 953 | @classmethod 954 | def INPUT_TYPES(s): 955 | return { 956 | "required": { 957 | "latents": ("LATENT", ), 958 | "reference": ("LATENT", ), 959 | "filter_size": ("INT", {"default": 1, "min": 1, "max": 128}), 960 | "factor": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "round": 0.01}), 961 | }, 962 | } 963 | 964 | RETURN_TYPES = ("LATENT",) 965 | FUNCTION = "batch_normalize" 966 | 967 | CATEGORY = "latent/filters" 968 | 969 | def batch_normalize(self, latents, reference, filter_size, factor): 970 | latents_copy = copy.deepcopy(latents) 971 | 972 | t = latents_copy["samples"].movedim(1, -1) # [B x C x H x W] -> [B x H x W x C] 973 | ref = reference["samples"].movedim(1, -1) 974 | 975 | d = filter_size * 2 + 1 976 | 977 | t_std, t_mean = std_mean_filter(t, d) 978 | ref_std, ref_mean = std_mean_filter(ref, d) 979 | 980 | t = (t - t_mean) / t_std 981 | t = t * ref_std + ref_mean 982 | t = t.movedim(-1, 1) # [B x H x W x C] -> [B x C x H x W] 983 | 984 | latents_copy["samples"] = torch.lerp(latents["samples"], t, factor) 985 | return (latents_copy,) 986 | 987 | class SharpenFilterLatent: 988 | def __init__(self): 989 | pass 990 | 991 | @classmethod 992 | def INPUT_TYPES(s): 993 | return { 994 | "required": { 995 | "latents": ("LATENT", ), 996 | "filter_size": ("INT", {"default": 1, "min": 1, "max": 128}), 997 | "factor": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01, "round": 0.01}), 998 | }, 999 | } 1000 | 1001 | RETURN_TYPES = ("LATENT",) 1002 | FUNCTION = "filter_latent" 1003 | 1004 | CATEGORY = "latent/filters" 1005 | 1006 | def filter_latent(self, latents, filter_size, factor): 1007 | latents_copy = copy.deepcopy(latents) 1008 | t = latents_copy["samples"].movedim(1, -1) # [B x C x H x W] -> [B x H x W x C] 1009 | 1010 | d = filter_size * 2 + 1 1011 | t_blurred = cv_blur_tensor(t, d, d) 1012 | 1013 | t = t - t_blurred 1014 | t = t * factor 1015 | t = t + t_blurred 1016 | 1017 | t = t.movedim(-1, 1) # [B x H x W x C] -> [B x C x H x W] 1018 | latents_copy["samples"] = t 1019 | return (latents_copy,) 1020 | 1021 | class AdainImage: 1022 | def __init__(self): 1023 | pass 1024 | 1025 | @classmethod 1026 | def INPUT_TYPES(s): 1027 | return { 1028 | "required": { 1029 | "images": ("IMAGE", ), 1030 | "reference": ("IMAGE", ), 1031 | "factor": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "round": 0.01}), 1032 | }, 1033 | } 1034 | 1035 | RETURN_TYPES = ("IMAGE",) 1036 | FUNCTION = "batch_normalize" 1037 | 1038 | CATEGORY = "image/filters" 1039 | 1040 | def batch_normalize(self, images, reference, factor): 1041 | t = copy.deepcopy(images) # [B x H x W x C] 1042 | 1043 | t = t.movedim(-1,0) # [C x B x H x W] 1044 | for c in range(t.size(0)): 1045 | for i in range(t.size(1)): 1046 | r_sd, r_mean = torch.std_mean(reference[i, :, :, c], dim=None) # index by original dim order 1047 | i_sd, i_mean = torch.std_mean(t[c, i], dim=None) 1048 | 1049 | t[c, i] = ((t[c, i] - i_mean) / i_sd) * r_sd + r_mean 1050 | 1051 | t = torch.lerp(images, t.movedim(0,-1), factor) # [B x H x W x C] 1052 | return (t,) 1053 | 1054 | class BatchNormalizeLatent: 1055 | def __init__(self): 1056 | pass 1057 | 1058 | @classmethod 1059 | def INPUT_TYPES(s): 1060 | return { 1061 | "required": { 1062 | "latents": ("LATENT", ), 1063 | "factor": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "round": 0.01}), 1064 | }, 1065 | } 1066 | 1067 | RETURN_TYPES = ("LATENT",) 1068 | FUNCTION = "batch_normalize" 1069 | 1070 | CATEGORY = "latent/filters" 1071 | 1072 | def batch_normalize(self, latents, factor): 1073 | latents_copy = copy.deepcopy(latents) 1074 | t = latents_copy["samples"] # [B x C x H x W] 1075 | 1076 | t = t.movedim(0,1) # [C x B x H x W] 1077 | for c in range(t.size(0)): 1078 | c_sd, c_mean = torch.std_mean(t[c], dim=None) 1079 | 1080 | for i in range(t.size(1)): 1081 | i_sd, i_mean = torch.std_mean(t[c, i], dim=None) 1082 | 1083 | t[c, i] = (t[c, i] - i_mean) / i_sd 1084 | 1085 | t[c] = t[c] * c_sd + c_mean 1086 | 1087 | latents_copy["samples"] = torch.lerp(latents["samples"], t.movedim(1,0), factor) # [B x C x H x W] 1088 | return (latents_copy,) 1089 | 1090 | class BatchNormalizeImage: 1091 | def __init__(self): 1092 | pass 1093 | 1094 | @classmethod 1095 | def INPUT_TYPES(s): 1096 | return { 1097 | "required": { 1098 | "images": ("IMAGE", ), 1099 | "factor": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "round": 0.01}), 1100 | }, 1101 | } 1102 | 1103 | RETURN_TYPES = ("IMAGE",) 1104 | FUNCTION = "batch_normalize" 1105 | 1106 | CATEGORY = "image/filters" 1107 | 1108 | def batch_normalize(self, images, factor): 1109 | t = copy.deepcopy(images) # [B x H x W x C] 1110 | 1111 | t = t.movedim(-1,0) # [C x B x H x W] 1112 | for c in range(t.size(0)): 1113 | c_sd, c_mean = torch.std_mean(t[c], dim=None) 1114 | 1115 | for i in range(t.size(1)): 1116 | i_sd, i_mean = torch.std_mean(t[c, i], dim=None) 1117 | 1118 | t[c, i] = (t[c, i] - i_mean) / i_sd 1119 | 1120 | t[c] = t[c] * c_sd + c_mean 1121 | 1122 | t = torch.lerp(images, t.movedim(0,-1), factor) # [B x H x W x C] 1123 | return (t,) 1124 | 1125 | class DifferenceChecker: 1126 | def __init__(self): 1127 | pass 1128 | 1129 | @classmethod 1130 | def INPUT_TYPES(s): 1131 | return { 1132 | "required": { 1133 | "images1": ("IMAGE", ), 1134 | "images2": ("IMAGE", ), 1135 | "multiplier": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1000.0, "step": 0.01, "round": 0.01}), 1136 | "print_MAE": ("BOOLEAN", {"default": False}), 1137 | }, 1138 | } 1139 | 1140 | RETURN_TYPES = ("IMAGE",) 1141 | FUNCTION = "difference_checker" 1142 | OUTPUT_NODE = True 1143 | CATEGORY = "image/filters" 1144 | 1145 | def difference_checker(self, images1, images2, multiplier, print_MAE): 1146 | t = copy.deepcopy(images1) 1147 | t = torch.abs(images1 - images2) 1148 | if print_MAE: 1149 | print(f"MAE = {torch.mean(t)}") 1150 | return (torch.clamp(t * multiplier, min=0, max=1),) 1151 | 1152 | class ImageConstant: 1153 | def __init__(self, device="cpu"): 1154 | self.device = device 1155 | 1156 | @classmethod 1157 | def INPUT_TYPES(s): 1158 | return {"required": { "width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}), 1159 | "height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}), 1160 | "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), 1161 | "red": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), 1162 | "green": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), 1163 | "blue": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), 1164 | }} 1165 | RETURN_TYPES = ("IMAGE",) 1166 | FUNCTION = "generate" 1167 | 1168 | CATEGORY = "image/filters" 1169 | 1170 | def generate(self, width, height, batch_size, red, green, blue): 1171 | r = torch.full([batch_size, height, width, 1], red) 1172 | g = torch.full([batch_size, height, width, 1], green) 1173 | b = torch.full([batch_size, height, width, 1], blue) 1174 | return (torch.cat((r, g, b), dim=-1), ) 1175 | 1176 | class ImageConstantHSV: 1177 | def __init__(self, device="cpu"): 1178 | self.device = device 1179 | 1180 | @classmethod 1181 | def INPUT_TYPES(s): 1182 | return {"required": { "width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}), 1183 | "height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}), 1184 | "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), 1185 | "hue": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), 1186 | "saturation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), 1187 | "value": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), 1188 | }} 1189 | RETURN_TYPES = ("IMAGE",) 1190 | FUNCTION = "generate" 1191 | 1192 | CATEGORY = "image/filters" 1193 | 1194 | def generate(self, width, height, batch_size, hue, saturation, value): 1195 | red, green, blue = hsv_to_rgb(hue, saturation, value) 1196 | 1197 | r = torch.full([batch_size, height, width, 1], red) 1198 | g = torch.full([batch_size, height, width, 1], green) 1199 | b = torch.full([batch_size, height, width, 1], blue) 1200 | return (torch.cat((r, g, b), dim=-1), ) 1201 | 1202 | class OffsetLatentImage: 1203 | def __init__(self): 1204 | self.device = comfy.model_management.intermediate_device() 1205 | 1206 | @classmethod 1207 | def INPUT_TYPES(s): 1208 | return {"required": { "width": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8}), 1209 | "height": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8}), 1210 | "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), 1211 | "offset_0": ("FLOAT", {"default": 0.0, "min": -10.0, "max": 10.0, "step": 0.1, "round": 0.1}), 1212 | "offset_1": ("FLOAT", {"default": 0.0, "min": -10.0, "max": 10.0, "step": 0.1, "round": 0.1}), 1213 | "offset_2": ("FLOAT", {"default": 0.0, "min": -10.0, "max": 10.0, "step": 0.1, "round": 0.1}), 1214 | "offset_3": ("FLOAT", {"default": 0.0, "min": -10.0, "max": 10.0, "step": 0.1, "round": 0.1}), 1215 | }} 1216 | RETURN_TYPES = ("LATENT",) 1217 | FUNCTION = "generate" 1218 | 1219 | CATEGORY = "latent" 1220 | 1221 | def generate(self, width, height, batch_size, offset_0, offset_1, offset_2, offset_3): 1222 | latent = torch.zeros([batch_size, 4, height // 8, width // 8], device=self.device) 1223 | latent[:,0,:,:] = offset_0 1224 | latent[:,1,:,:] = offset_1 1225 | latent[:,2,:,:] = offset_2 1226 | latent[:,3,:,:] = offset_3 1227 | return ({"samples":latent}, ) 1228 | 1229 | class RelightSimple: 1230 | @classmethod 1231 | def INPUT_TYPES(s): 1232 | return { 1233 | "required": { 1234 | "image": ("IMAGE",), 1235 | "normals": ("IMAGE",), 1236 | "x": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.001}), 1237 | "y": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.001}), 1238 | "z": ("FLOAT", {"default": 1.0, "min": -1.0, "max": 1.0, "step": 0.001}), 1239 | "brightness": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100, "step": 0.01}), 1240 | }, 1241 | } 1242 | 1243 | RETURN_TYPES = ("IMAGE",) 1244 | FUNCTION = "relight" 1245 | 1246 | CATEGORY = "image/filters" 1247 | 1248 | def relight(self, image, normals, x, y, z, brightness): 1249 | if image.shape[0] != normals.shape[0]: 1250 | raise Exception("Batch size for image and normals must match") 1251 | norm = normals.detach().clone() * 2 - 1 1252 | norm = F.interpolate(norm.movedim(-1,1), size=(image.shape[1], image.shape[2]), mode='bilinear').movedim(1,-1) 1253 | light = torch.tensor([x, y, z]) 1254 | light = F.normalize(light, dim=0) 1255 | 1256 | diffuse = norm[:,:,:,0] * light[0] + norm[:,:,:,1] * light[1] + norm[:,:,:,2] * light[2] 1257 | diffuse = torch.clip(diffuse.unsqueeze(3).repeat(1,1,1,3), 0, 1) 1258 | 1259 | relit = image.detach().clone() 1260 | relit[:,:,:,:3] = torch.clip(relit[:,:,:,:3] * diffuse * brightness, 0, 1) 1261 | return (relit,) 1262 | 1263 | class LatentStats: 1264 | @classmethod 1265 | def INPUT_TYPES(s): 1266 | return {"required": {"latent": ("LATENT", ),}} 1267 | 1268 | RETURN_TYPES = ("STRING", "FLOAT", "FLOAT", "FLOAT", "FLOAT") 1269 | RETURN_NAMES = ("stats", "c0_mean", "c1_mean", "c2_mean", "c3_mean") 1270 | FUNCTION = "notify" 1271 | OUTPUT_NODE = True 1272 | 1273 | CATEGORY = "utils" 1274 | 1275 | def notify(self, latent): 1276 | latents = latent["samples"] 1277 | channels = latents.size(1) 1278 | width, height = latents.size(3), latents.size(2) 1279 | 1280 | text = ["",] 1281 | text[0] = f"batch size: {latents.size(0)}" 1282 | text.append(f"channels: {channels}") 1283 | text.append(f"width: {width} ({width * 8})") 1284 | text.append(f"height: {height} ({height * 8})") 1285 | 1286 | cmean = [0,0,0,0] 1287 | for i in range(channels): 1288 | minimum = torch.min(latents[:,i,:,:]).item() 1289 | maximum = torch.max(latents[:,i,:,:]).item() 1290 | std_dev, mean = torch.std_mean(latents[:,i,:,:], dim=None) 1291 | if i < 4: 1292 | cmean[i] = mean 1293 | 1294 | text.append(f"c{i} mean: {mean:.1f} std_dev: {std_dev:.1f} min: {minimum:.1f} max: {maximum:.1f}") 1295 | 1296 | 1297 | printtext = "\033[36mLatent Stats:\033[m" 1298 | for t in text: 1299 | printtext += "\n " + t 1300 | 1301 | returntext = "" 1302 | for i in range(len(text)): 1303 | if i > 0: 1304 | returntext += "\n" 1305 | returntext += text[i] 1306 | 1307 | print(printtext) 1308 | return (returntext, cmean[0], cmean[1], cmean[2], cmean[3]) 1309 | 1310 | class Tonemap: 1311 | @classmethod 1312 | def INPUT_TYPES(s): 1313 | return { 1314 | "required": { 1315 | "images": ("IMAGE",), 1316 | "input_mode": (["linear", "sRGB"],), 1317 | "output_mode": (["sRGB", "linear"],), 1318 | "tonemap_scale": ("FLOAT", {"default": 1, "min": 0.1, "max": 10, "step": 0.01}), 1319 | }, 1320 | } 1321 | 1322 | RETURN_TYPES = ("IMAGE",) 1323 | FUNCTION = "apply" 1324 | 1325 | CATEGORY = "image/filters" 1326 | 1327 | def apply(self, images, input_mode, output_mode, tonemap_scale): 1328 | t = images.detach().clone().cpu().numpy().astype(np.float32) 1329 | 1330 | if input_mode == "sRGB": 1331 | sRGBtoLinear(t[:,:,:,:3]) 1332 | 1333 | linearToTonemap(t[:,:,:,:3], tonemap_scale) 1334 | 1335 | if output_mode == "sRGB": 1336 | linearToSRGB(t[:,:,:,:3]) 1337 | t = np.clip(t, 0, 1) 1338 | 1339 | t = torch.from_numpy(t) 1340 | return (t,) 1341 | 1342 | class UnTonemap: 1343 | @classmethod 1344 | def INPUT_TYPES(s): 1345 | return { 1346 | "required": { 1347 | "images": ("IMAGE",), 1348 | "input_mode": (["sRGB", "linear"],), 1349 | "output_mode": (["linear", "sRGB"],), 1350 | "tonemap_scale": ("FLOAT", {"default": 1, "min": 0.1, "max": 10, "step": 0.01}), 1351 | }, 1352 | } 1353 | 1354 | RETURN_TYPES = ("IMAGE",) 1355 | FUNCTION = "apply" 1356 | 1357 | CATEGORY = "image/filters" 1358 | 1359 | def apply(self, images, input_mode, output_mode, tonemap_scale): 1360 | t = images.detach().clone().cpu().numpy().astype(np.float32) 1361 | 1362 | if input_mode == "sRGB": 1363 | sRGBtoLinear(t[:,:,:,:3]) 1364 | 1365 | tonemapToLinear(t[:,:,:,:3], tonemap_scale) 1366 | 1367 | if output_mode == "sRGB": 1368 | linearToSRGB(t[:,:,:,:3]) 1369 | t = np.clip(t, 0, 1) 1370 | 1371 | t = torch.from_numpy(t) 1372 | return (t,) 1373 | 1374 | class ExposureAdjust: 1375 | @classmethod 1376 | def INPUT_TYPES(s): 1377 | return { 1378 | "required": { 1379 | "images": ("IMAGE",), 1380 | "stops": ("FLOAT", {"default": 0.0, "min": -100, "max": 100, "step": 0.01}), 1381 | "input_mode": (["sRGB", "linear"],), 1382 | "output_mode": (["sRGB", "linear"],), 1383 | "tonemap": (["linear", "Reinhard", "linlog"], {"default": "Reinhard"}), 1384 | "tonemap_scale": ("FLOAT", {"default": 1, "min": 0.1, "max": 10, "step": 0.01}), 1385 | }, 1386 | } 1387 | 1388 | RETURN_TYPES = ("IMAGE",) 1389 | FUNCTION = "adjust_exposure" 1390 | 1391 | CATEGORY = "image/filters" 1392 | 1393 | def adjust_exposure(self, images, stops, input_mode, output_mode, tonemap, tonemap_scale): 1394 | t = images.detach().clone().cpu().numpy().astype(np.float32) 1395 | 1396 | if input_mode == "sRGB": 1397 | sRGBtoLinear(t[...,:3]) 1398 | 1399 | if tonemap == "linlog": 1400 | tonemapToLinear(t[...,:3], tonemap_scale) 1401 | elif tonemap == "Reinhard": 1402 | t = np.clip(t, 0, 0.999) 1403 | t[...,:3] = -t[...,:3] / (t[...,:3] - 1) 1404 | 1405 | exposure(t[...,:3], stops) 1406 | 1407 | if tonemap == "linlog": 1408 | linearToTonemap(t[...,:3], tonemap_scale) 1409 | elif tonemap == "Reinhard": 1410 | t[...,:3] = t[...,:3] / (t[...,:3] + 1) 1411 | 1412 | if output_mode == "sRGB": 1413 | linearToSRGB(t[...,:3]) 1414 | t = np.clip(t, 0, 1) 1415 | 1416 | t = torch.from_numpy(t) 1417 | return (t,) 1418 | 1419 | # Normal map standard coordinates: +r:+x:right, +g:+y:up, +b:+z:in 1420 | class ConvertNormals: 1421 | @classmethod 1422 | def INPUT_TYPES(s): 1423 | return { 1424 | "required": { 1425 | "normals": ("IMAGE",), 1426 | "input_mode": (["BAE", "MiDaS", "Standard"],), 1427 | "output_mode": (["BAE", "MiDaS", "Standard"],), 1428 | "scale_XY": ("FLOAT",{"default": 1, "min": 0, "max": 100, "step": 0.001}), 1429 | "normalize": ("BOOLEAN", {"default": True}), 1430 | "fix_black": ("BOOLEAN", {"default": True}), 1431 | }, 1432 | "optional": { 1433 | "optional_fill": ("IMAGE",), 1434 | }, 1435 | } 1436 | 1437 | RETURN_TYPES = ("IMAGE",) 1438 | FUNCTION = "convert_normals" 1439 | 1440 | CATEGORY = "image/filters" 1441 | 1442 | def convert_normals(self, normals, input_mode, output_mode, scale_XY, normalize, fix_black, optional_fill=None): 1443 | t = normals.detach().clone() 1444 | 1445 | if input_mode == "BAE": 1446 | t[:,:,:,0] = 1 - t[:,:,:,0] # invert R 1447 | elif input_mode == "MiDaS": 1448 | t[:,:,:,:3] = torch.stack([1 - t[:,:,:,2], t[:,:,:,1], t[:,:,:,0]], dim=3) # BGR -> RGB and invert R 1449 | 1450 | if fix_black: 1451 | key = torch.clamp(1 - t[:,:,:,2] * 2, min=0, max=1) 1452 | if optional_fill == None: 1453 | t[:,:,:,0] += key * 0.5 1454 | t[:,:,:,1] += key * 0.5 1455 | t[:,:,:,2] += key 1456 | else: 1457 | fill = optional_fill.detach().clone() 1458 | if fill.shape[1:3] != t.shape[1:3]: 1459 | fill = F.interpolate(fill.movedim(-1,1), size=(t.shape[1], t.shape[2]), mode='bilinear').movedim(1,-1) 1460 | if fill.shape[0] != t.shape[0]: 1461 | fill = fill[0].unsqueeze(0).expand(t.shape[0], -1, -1, -1) 1462 | t[:,:,:,:3] += fill[:,:,:,:3] * key.unsqueeze(3).expand(-1, -1, -1, 3) 1463 | 1464 | t[:,:,:,:2] = (t[:,:,:,:2] - 0.5) * scale_XY + 0.5 1465 | 1466 | if normalize: 1467 | t[:,:,:,:3] = F.normalize(t[:,:,:,:3] * 2 - 1, dim=3) / 2 + 0.5 1468 | 1469 | if output_mode == "BAE": 1470 | t[:,:,:,0] = 1 - t[:,:,:,0] # invert R 1471 | elif output_mode == "MiDaS": 1472 | t[:,:,:,:3] = torch.stack([t[:,:,:,2], t[:,:,:,1], 1 - t[:,:,:,0]], dim=3) # invert R and BGR -> RGB 1473 | 1474 | return (t,) 1475 | 1476 | class BatchAverageImage: 1477 | @classmethod 1478 | def INPUT_TYPES(s): 1479 | return { 1480 | "required": { 1481 | "images": ("IMAGE",), 1482 | "operation": (["mean", "median"],), 1483 | }, 1484 | } 1485 | 1486 | RETURN_TYPES = ("IMAGE",) 1487 | FUNCTION = "apply" 1488 | 1489 | CATEGORY = "image/filters" 1490 | 1491 | def apply(self, images, operation): 1492 | t = images.detach().clone() 1493 | if operation == "mean": 1494 | return (torch.mean(t, dim=0, keepdim=True),) 1495 | elif operation == "median": 1496 | return (torch.median(t, dim=0, keepdim=True)[0],) 1497 | return(t,) 1498 | 1499 | class NormalMapSimple: 1500 | @classmethod 1501 | def INPUT_TYPES(s): 1502 | return { 1503 | "required": { 1504 | "images": ("IMAGE",), 1505 | "scale_XY": ("FLOAT",{"default": 1, "min": 0, "max": 100, "step": 0.001}), 1506 | }, 1507 | } 1508 | 1509 | RETURN_TYPES = ("IMAGE",) 1510 | FUNCTION = "normal_map" 1511 | 1512 | CATEGORY = "image/filters" 1513 | 1514 | def normal_map(self, images, scale_XY): 1515 | t = images.detach().clone().cpu().numpy().astype(np.float32) 1516 | L = np.mean(t[:,:,:,:3], axis=3) 1517 | for i in range(t.shape[0]): 1518 | t[i,:,:,0] = cv2.Scharr(L[i], -1, 1, 0, cv2.BORDER_REFLECT) * -1 1519 | t[i,:,:,1] = cv2.Scharr(L[i], -1, 0, 1, cv2.BORDER_REFLECT) 1520 | t[:,:,:,2] = 1 1521 | t = torch.from_numpy(t) 1522 | t[:,:,:,:2] *= scale_XY 1523 | t[:,:,:,:3] = F.normalize(t[:,:,:,:3], dim=3) / 2 + 0.5 1524 | return (t,) 1525 | 1526 | class DepthToNormals: 1527 | @classmethod 1528 | def INPUT_TYPES(s): 1529 | return { 1530 | "required": { 1531 | "depth": ("IMAGE",), 1532 | "scale": ("FLOAT",{"default": 1, "min": 0.001, "max": 1000, "step": 0.001}), 1533 | "output_mode": (["Standard", "BAE", "MiDaS"],), 1534 | }, 1535 | } 1536 | 1537 | RETURN_TYPES = ("IMAGE",) 1538 | RETURN_NAMES = ("normals",) 1539 | FUNCTION = "normal_map" 1540 | 1541 | CATEGORY = "image/filters" 1542 | 1543 | def normal_map(self, depth, scale, output_mode): 1544 | kernel_x = torch.Tensor([[0,0,0],[1,0,-1],[0,0,0]]).unsqueeze(0).unsqueeze(0).repeat(3, 1, 1, 1) 1545 | kernel_y = torch.Tensor([[0,1,0],[0,0,0],[0,-1,0]]).unsqueeze(0).unsqueeze(0).repeat(3, 1, 1, 1) 1546 | conv2d = F.conv2d 1547 | pad = F.pad 1548 | 1549 | size_x = depth.size(2) 1550 | size_y = depth.size(1) 1551 | max_dim = max(size_x, size_y) 1552 | position_map = depth.detach().clone() * scale 1553 | xs = torch.linspace(-1 * size_x / max_dim, 1 * size_x / max_dim, steps=size_x) 1554 | ys = torch.linspace(-1 * size_y / max_dim, 1 * size_y / max_dim, steps=size_y) 1555 | grid_x, grid_y = torch.meshgrid(xs, ys, indexing='xy') 1556 | position_map[..., 0] = grid_x.unsqueeze(0) 1557 | position_map[..., 1] = grid_y.unsqueeze(0) 1558 | 1559 | position_map = position_map.movedim(-1, 1) # BCHW 1560 | grad_x = conv2d(pad(position_map, (1,1,1,1), mode='replicate'), kernel_x, padding='valid', groups=3) 1561 | grad_y = conv2d(pad(position_map, (1,1,1,1), mode='replicate'), kernel_y, padding='valid', groups=3) 1562 | 1563 | cross_product = torch.cross(grad_x, grad_y, dim=1) 1564 | normals = F.normalize(cross_product) 1565 | normals[:, 1] *= -1 1566 | 1567 | if output_mode != "Standard": 1568 | normals[:, 0] *= -1 1569 | 1570 | if output_mode == "MiDaS": 1571 | normals = torch.flip(normals, dims=[1,]) 1572 | 1573 | normals = normals.movedim(1, -1) * 0.5 + 0.5 # BHWC 1574 | return (normals,) 1575 | 1576 | class Keyer: 1577 | @classmethod 1578 | def INPUT_TYPES(s): 1579 | return { 1580 | "required": { 1581 | "images": ("IMAGE",), 1582 | "operation": (["luminance", "saturation", "max", "min", "red", "green", "blue", "redscreen", "greenscreen", "bluescreen"],), 1583 | "low": ("FLOAT",{"default": 0, "step": 0.001}), 1584 | "high": ("FLOAT",{"default": 1, "step": 0.001}), 1585 | "gamma": ("FLOAT",{"default": 1.0, "min": 0.001, "step": 0.001}), 1586 | "premult": ("BOOLEAN", {"default": True}), 1587 | }, 1588 | } 1589 | 1590 | RETURN_TYPES = ("IMAGE", "IMAGE", "MASK") 1591 | RETURN_NAMES = ("image", "alpha", "mask") 1592 | FUNCTION = "keyer" 1593 | 1594 | CATEGORY = "image/filters" 1595 | 1596 | def keyer(self, images, operation, low, high, gamma, premult): 1597 | t = images[:,:,:,:3].detach().clone() 1598 | 1599 | if operation == "luminance": 1600 | alpha = 0.2126 * t[:,:,:,0] + 0.7152 * t[:,:,:,1] + 0.0722 * t[:,:,:,2] 1601 | elif operation == "saturation": 1602 | minV = torch.min(t, 3)[0] 1603 | maxV = torch.max(t, 3)[0] 1604 | mask = maxV != 0 1605 | alpha = maxV 1606 | alpha[mask] = (maxV[mask] - minV[mask]) / maxV[mask] 1607 | elif operation == "max": 1608 | alpha = torch.max(t, 3)[0] 1609 | elif operation == "min": 1610 | alpha = torch.min(t, 3)[0] 1611 | elif operation == "red": 1612 | alpha = t[:,:,:,0] 1613 | elif operation == "green": 1614 | alpha = t[:,:,:,1] 1615 | elif operation == "blue": 1616 | alpha = t[:,:,:,2] 1617 | elif operation == "redscreen": 1618 | alpha = 0.7 * (t[:,:,:,1] + t[:,:,:,2]) - t[:,:,:,0] + 1 1619 | elif operation == "greenscreen": 1620 | alpha = 0.7 * (t[:,:,:,0] + t[:,:,:,2]) - t[:,:,:,1] + 1 1621 | elif operation == "bluescreen": 1622 | alpha = 0.7 * (t[:,:,:,0] + t[:,:,:,1]) - t[:,:,:,2] + 1 1623 | else: # should never be reached 1624 | alpha = t[:,:,:,0] * 0 1625 | 1626 | if low == high: 1627 | alpha = (alpha > high).to(t.dtype) 1628 | else: 1629 | alpha = (alpha - low) / (high - low) 1630 | 1631 | if gamma != 1.0: 1632 | alpha = torch.pow(alpha, 1/gamma) 1633 | alpha = torch.clamp(alpha, min=0, max=1).unsqueeze(3).repeat(1,1,1,3) 1634 | if premult: 1635 | t *= alpha 1636 | return (t, alpha, alpha[:,:,:,0]) 1637 | 1638 | jitter_matrix = torch.Tensor([[[1, 0, 0], [0, 1, 0]], [[1, 0, 1], [0, 1, 0]], [[1, 0, 1], [0, 1, 1]], 1639 | [[1, 0, 0], [0, 1, 1]], [[1, 0,-1], [0, 1, 1]], [[1, 0,-1], [0, 1, 0]], 1640 | [[1, 0,-1], [0, 1,-1]], [[1, 0, 0], [0, 1,-1]], [[1, 0, 1], [0, 1,-1]]]) 1641 | 1642 | class JitterImage: 1643 | @classmethod 1644 | def INPUT_TYPES(s): 1645 | return { 1646 | "required": { 1647 | "images": ("IMAGE",), 1648 | "jitter_scale": ("FLOAT", {"default": 1.0, "min": 0.1, "step": 0.1}), 1649 | }, 1650 | } 1651 | 1652 | RETURN_TYPES = ("IMAGE",) 1653 | FUNCTION = "jitter" 1654 | 1655 | CATEGORY = "image/filters/jitter" 1656 | 1657 | def jitter(self, images, jitter_scale): 1658 | t = images.detach().clone().movedim(-1,1) # [B x C x H x W] 1659 | 1660 | theta = jitter_matrix.detach().clone().to(t.device) 1661 | theta[:,0,2] *= jitter_scale * 2 / t.shape[3] 1662 | theta[:,1,2] *= jitter_scale * 2 / t.shape[2] 1663 | affine = F.affine_grid(theta, torch.Size([9, t.shape[1], t.shape[2], t.shape[3]])) 1664 | 1665 | batch = [] 1666 | for i in range(t.shape[0]): 1667 | jb = t[i].repeat(9,1,1,1) 1668 | jb = F.grid_sample(jb, affine, mode='bilinear', padding_mode='border', align_corners=None) 1669 | batch.append(jb) 1670 | 1671 | t = torch.cat(batch, dim=0).movedim(1,-1) # [B x H x W x C] 1672 | return (t,) 1673 | 1674 | class UnJitterImage: 1675 | @classmethod 1676 | def INPUT_TYPES(s): 1677 | return { 1678 | "required": { 1679 | "images": ("IMAGE",), 1680 | "jitter_scale": ("FLOAT", {"default": 1.0, "min": 0.1, "step": 0.1}), 1681 | "oflow_align": ("BOOLEAN", {"default": False}), 1682 | }, 1683 | } 1684 | 1685 | RETURN_TYPES = ("IMAGE",) 1686 | FUNCTION = "jitter" 1687 | 1688 | CATEGORY = "image/filters/jitter" 1689 | 1690 | def jitter(self, images, jitter_scale, oflow_align): 1691 | t = images.detach().clone().movedim(-1,1) # [B x C x H x W] 1692 | 1693 | if oflow_align: 1694 | pbar = ProgressBar(t.shape[0] // 9) 1695 | raft_model, raft_device = load_raft() 1696 | batch = [] 1697 | for i in trange(t.shape[0] // 9): 1698 | batch1 = t[i*9].unsqueeze(0).repeat(9,1,1,1) 1699 | batch2 = t[i*9:i*9+9] 1700 | flows = raft_flow(raft_model, raft_device, batch1, batch2) 1701 | batch.append(flows) 1702 | pbar.update(1) 1703 | flows = torch.cat(batch, dim=0) 1704 | t = flow_warp(t, flows) 1705 | else: 1706 | theta = jitter_matrix.detach().clone().to(t.device) 1707 | theta[:,0,2] *= jitter_scale * -2 / t.shape[3] 1708 | theta[:,1,2] *= jitter_scale * -2 / t.shape[2] 1709 | affine = F.affine_grid(theta, torch.Size([9, t.shape[1], t.shape[2], t.shape[3]])) 1710 | batch = [] 1711 | for i in range(t.shape[0] // 9): 1712 | jb = t[i*9:i*9+9] 1713 | jb = F.grid_sample(jb, affine, mode='bicubic', padding_mode='border', align_corners=None) 1714 | batch.append(jb) 1715 | t = torch.cat(batch, dim=0) 1716 | 1717 | t = t.movedim(1,-1) # [B x H x W x C] 1718 | return (t,) 1719 | 1720 | class BatchAverageUnJittered: 1721 | @classmethod 1722 | def INPUT_TYPES(s): 1723 | return { 1724 | "required": { 1725 | "images": ("IMAGE",), 1726 | "operation": (["mean", "median"],), 1727 | }, 1728 | } 1729 | 1730 | RETURN_TYPES = ("IMAGE",) 1731 | FUNCTION = "apply" 1732 | 1733 | CATEGORY = "image/filters/jitter" 1734 | 1735 | def apply(self, images, operation): 1736 | t = images.detach().clone() 1737 | 1738 | batch = [] 1739 | for i in range(t.shape[0] // 9): 1740 | if operation == "mean": 1741 | batch.append(torch.mean(t[i*9:i*9+9], dim=0, keepdim=True)) 1742 | elif operation == "median": 1743 | batch.append(torch.median(t[i*9:i*9+9], dim=0, keepdim=True)[0]) 1744 | 1745 | return (torch.cat(batch, dim=0),) 1746 | 1747 | class BatchAlign: 1748 | @classmethod 1749 | def INPUT_TYPES(s): 1750 | return { 1751 | "required": { 1752 | "images": ("IMAGE",), 1753 | "ref_frame": ("INT", {"default": 0, "min": 0}), 1754 | "direction": (["forward", "backward"],), 1755 | "blur": ("INT", {"default": 0, "min": 0}), 1756 | }, 1757 | } 1758 | 1759 | RETURN_TYPES = ("IMAGE", "IMAGE") 1760 | RETURN_NAMES = ("aligned", "flow") 1761 | FUNCTION = "apply" 1762 | 1763 | CATEGORY = "image/filters" 1764 | 1765 | def apply(self, images, ref_frame, direction, blur): 1766 | t = images.detach().clone().movedim(-1,1) # [B x C x H x W] 1767 | rf = min(ref_frame, t.shape[0] - 1) 1768 | 1769 | raft_model, raft_device = load_raft() 1770 | ref = t[rf].unsqueeze(0).repeat(t.shape[0],1,1,1) 1771 | if direction == "forward": 1772 | flows = raft_flow(raft_model, raft_device, ref, t) 1773 | else: 1774 | flows = raft_flow(raft_model, raft_device, t, ref) * -1 1775 | 1776 | if blur > 0: 1777 | d = blur * 2 + 1 1778 | dup = flows.movedim(1,-1).detach().clone().cpu().numpy() 1779 | blurred = [] 1780 | for img in dup: 1781 | blurred.append(torch.from_numpy(cv2.GaussianBlur(img, (d,d), 0)).unsqueeze(0).movedim(-1,1)) 1782 | flows = torch.cat(blurred).to(flows.device) 1783 | 1784 | t = flow_warp(t, flows) 1785 | 1786 | t = t.movedim(1,-1) # [B x H x W x C] 1787 | f = images.detach().clone() * 0 1788 | f[:,:,:,:2] = flows.movedim(1,-1) 1789 | return (t,f) 1790 | 1791 | class InstructPixToPixConditioningAdvanced: 1792 | @classmethod 1793 | def INPUT_TYPES(s): 1794 | return {"required": {"positive": ("CONDITIONING", ), 1795 | "negative": ("CONDITIONING", ), 1796 | "new": ("LATENT", ), 1797 | "new_scale": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0, "step": 0.01}), 1798 | "original": ("LATENT", ), 1799 | "original_scale": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0, "step": 0.01}), 1800 | }} 1801 | 1802 | RETURN_TYPES = ("CONDITIONING","CONDITIONING","CONDITIONING","LATENT") 1803 | RETURN_NAMES = ("cond1", "cond2", "negative", "latent") 1804 | FUNCTION = "encode" 1805 | 1806 | CATEGORY = "conditioning/instructpix2pix" 1807 | 1808 | def encode(self, positive, negative, new, new_scale, original, original_scale): 1809 | new_shape, orig_shape = new["samples"].shape, original["samples"].shape 1810 | if new_shape != orig_shape: 1811 | raise Exception(f"Latent shape mismatch: {new_shape} and {orig_shape}") 1812 | 1813 | out_latent = {} 1814 | out_latent["samples"] = new["samples"] * new_scale 1815 | out = [] 1816 | for conditioning in [positive, negative]: 1817 | c = [] 1818 | for t in conditioning: 1819 | d = t[1].copy() 1820 | d["concat_latent_image"] = original["samples"] * original_scale 1821 | n = [t[0], d] 1822 | c.append(n) 1823 | out.append(c) 1824 | return (out[0], out[1], negative, out_latent) 1825 | 1826 | class InpaintConditionEncode: 1827 | @classmethod 1828 | def INPUT_TYPES(s): 1829 | return { 1830 | "required": { 1831 | "vae": ("VAE", ), 1832 | "pixels": ("IMAGE", ), 1833 | "mask": ("MASK", ), 1834 | },} 1835 | 1836 | RETURN_TYPES = ("INPAINT_CONDITION",) 1837 | RETURN_NAMES = ("inpaint_condition",) 1838 | FUNCTION = "encode" 1839 | CATEGORY = "conditioning/inpaint" 1840 | 1841 | def encode(self, vae, pixels, mask): 1842 | x = (pixels.shape[1] // 8) * 8 1843 | y = (pixels.shape[2] // 8) * 8 1844 | mask = F.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(pixels.shape[1], pixels.shape[2]), mode="bilinear") 1845 | 1846 | pixels = pixels.clone() 1847 | if pixels.shape[1] != x or pixels.shape[2] != y: 1848 | x_offset = (pixels.shape[1] % 8) // 2 1849 | y_offset = (pixels.shape[2] % 8) // 2 1850 | pixels = pixels[:,x_offset:x + x_offset, y_offset:y + y_offset,:] 1851 | mask = mask[:,:,x_offset:x + x_offset, y_offset:y + y_offset] 1852 | 1853 | m = (1.0 - mask.round()).squeeze(1) 1854 | for i in range(3): 1855 | pixels[:,:,:,i] -= 0.5 1856 | pixels[:,:,:,i] *= m 1857 | pixels[:,:,:,i] += 0.5 1858 | concat_latent = vae.encode(pixels) 1859 | 1860 | return ({"concat_latent_image": concat_latent, "concat_mask": mask},) 1861 | 1862 | class InpaintConditionApply: 1863 | @classmethod 1864 | def INPUT_TYPES(s): 1865 | return { 1866 | "required": { 1867 | "positive": ("CONDITIONING", ), 1868 | "negative": ("CONDITIONING", ), 1869 | "inpaint_condition": ("INPAINT_CONDITION", ), 1870 | "noise_mask": ("BOOLEAN", {"default": False, "tooltip": "Add a noise mask to the latent so sampling will only happen within the mask. Might improve results or completely break things depending on the model."}), 1871 | }, 1872 | "optional": { 1873 | "latents_optional": ("LATENT",), 1874 | },} 1875 | 1876 | RETURN_TYPES = ("CONDITIONING","CONDITIONING","LATENT") 1877 | RETURN_NAMES = ("positive", "negative", "latent") 1878 | FUNCTION = "encode" 1879 | 1880 | CATEGORY = "conditioning/inpaint" 1881 | 1882 | def encode(self, positive, negative, inpaint_condition, noise_mask=True, latents_optional=None): 1883 | concat_latent = inpaint_condition["concat_latent_image"] 1884 | concat_mask = inpaint_condition["concat_mask"] 1885 | 1886 | if latents_optional is not None: 1887 | out_latent = latents_optional.copy() 1888 | else: 1889 | out_latent = {} 1890 | out_latent["samples"] = torch.zeros_like(concat_latent) 1891 | 1892 | if noise_mask: 1893 | out_latent["noise_mask"] = concat_mask 1894 | 1895 | out = [] 1896 | for conditioning in [positive, negative]: 1897 | c = node_helpers.conditioning_set_values(conditioning, {"concat_latent_image": concat_latent, 1898 | "concat_mask": concat_mask}) 1899 | out.append(c) 1900 | return (out[0], out[1], out_latent) 1901 | 1902 | class LatentNormalizeShuffle: 1903 | def __init__(self): 1904 | pass 1905 | 1906 | @classmethod 1907 | def INPUT_TYPES(s): 1908 | return { 1909 | "required": { 1910 | "latents": ("LATENT", ), 1911 | "flatten": ("INT", {"default": 0, "min": 0, "max": 16}), 1912 | "normalize": ("BOOLEAN", {"default": True}), 1913 | "shuffle": ("BOOLEAN", {"default": True}), 1914 | }, 1915 | } 1916 | 1917 | RETURN_TYPES = ("LATENT",) 1918 | FUNCTION = "batch_normalize" 1919 | 1920 | CATEGORY = "latent/filters" 1921 | 1922 | def batch_normalize(self, latents, flatten, normalize, shuffle): 1923 | latents_copy = copy.deepcopy(latents) 1924 | t = latents_copy["samples"] # [B x C x H x W] 1925 | 1926 | if flatten > 0: 1927 | d = flatten * 2 + 1 1928 | channels = t.shape[1] 1929 | kernel = gaussian_kernel(d, 1, device=t.device).repeat(channels, 1, 1).unsqueeze(1) 1930 | t_blurred = F.conv2d(t, kernel, padding='same', groups=channels) 1931 | t = t - t_blurred 1932 | 1933 | if normalize: 1934 | for b in range(t.shape[0]): 1935 | for c in range(4): 1936 | t_sd, t_mean = torch.std_mean(t[b,c]) 1937 | t[b,c] = (t[b,c] - t_mean) / t_sd 1938 | 1939 | if shuffle: 1940 | t_shuffle = [] 1941 | for i in (1,2,3,0): 1942 | t_shuffle.append(t[:,i]) 1943 | t = torch.stack(t_shuffle, dim=1) 1944 | 1945 | latents_copy["samples"] = t 1946 | return (latents_copy,) 1947 | 1948 | class RandnLikeLatent: 1949 | def __init__(self): 1950 | pass 1951 | 1952 | @classmethod 1953 | def INPUT_TYPES(s): 1954 | return { 1955 | "required": { 1956 | "latents": ("LATENT", ), 1957 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True, "tooltip": "The random seed used for creating the noise."}), 1958 | }, 1959 | } 1960 | 1961 | RETURN_TYPES = ("LATENT",) 1962 | FUNCTION = "generate" 1963 | 1964 | CATEGORY = "latent/filters" 1965 | 1966 | def generate(self, latents, seed): 1967 | latents_copy = copy.deepcopy(latents) 1968 | gen_cpu = torch.Generator(device="cpu").manual_seed(seed) 1969 | latents_copy["samples"] = randn_like_g(latents_copy["samples"], generator=gen_cpu) 1970 | return (latents_copy,) 1971 | 1972 | class PrintSigmas: 1973 | @classmethod 1974 | def INPUT_TYPES(s): 1975 | return {"required": { 1976 | "sigmas": ("SIGMAS",) 1977 | }} 1978 | RETURN_TYPES = ("SIGMAS",) 1979 | FUNCTION = "notify" 1980 | OUTPUT_NODE = True 1981 | CATEGORY = "utils" 1982 | 1983 | def notify(self, sigmas): 1984 | print(sigmas) 1985 | return (sigmas,) 1986 | 1987 | class VisualizeLatents: 1988 | @classmethod 1989 | def INPUT_TYPES(s): 1990 | return {"required": {"latent": ("LATENT", ),}} 1991 | 1992 | RETURN_TYPES = ("IMAGE",) 1993 | FUNCTION = "visualize" 1994 | 1995 | CATEGORY = "utils" 1996 | 1997 | def visualize(self, latent): 1998 | latents = latent["samples"] 1999 | batch, channels, height, width = latents.size() 2000 | 2001 | latents = latents - latents.mean() 2002 | latents = latents / latents.std() 2003 | latents = latents / 10 + 0.5 2004 | 2005 | scale = int(channels ** 0.5) 2006 | vis = torch.zeros(batch, height * scale, width * scale) 2007 | 2008 | for i in range(channels): 2009 | start_h = (i % scale) * height 2010 | end_h = start_h + height 2011 | start_w = (i // scale) * width 2012 | end_w = start_w + width 2013 | 2014 | vis[:, start_h:end_h, start_w:end_w] = latents[:, i] 2015 | 2016 | return (vis.unsqueeze(-1).repeat(1, 1, 1, 3),) 2017 | 2018 | class GameOfLife: 2019 | @classmethod 2020 | def INPUT_TYPES(s): 2021 | return { 2022 | "required": { 2023 | "width": ("INT", { "default": 32, "min": 8, "max": 1024, "step": 1}), 2024 | "height": ("INT", { "default": 32, "min": 8, "max": 1024, "step": 1}), 2025 | "cell_size": ("INT", { "default": 16, "min": 8, "max": 1024, "step": 8}), 2026 | "seed": ("INT", { "default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}), 2027 | "threshold": ("FLOAT", { "default": 0.8, "min": 0.0, "max": 1.0, "step": 0.01}), 2028 | "steps": ("INT", { "default": 64, "min": 1, "max": 999999, "step": 1}), 2029 | }, 2030 | "optional": { 2031 | "optional_start": ("MASK", ), 2032 | }, 2033 | } 2034 | 2035 | RETURN_TYPES = ("IMAGE", "MASK", "MASK", "MASK") 2036 | RETURN_NAMES = ("image", "mask", "off", "on") 2037 | FUNCTION = "game" 2038 | 2039 | CATEGORY = "image/filters" 2040 | 2041 | def game(self, width, height, cell_size, seed, threshold, steps, optional_start=None): 2042 | F = torch.nn.functional 2043 | 2044 | if optional_start is None: 2045 | # base random initialization 2046 | torch.manual_seed(seed) 2047 | grid = torch.rand(1, 1, height, width) 2048 | else: 2049 | grid = optional_start[0].unsqueeze(0).unsqueeze(0) 2050 | grid = F.interpolate(grid, size=(height, width)) 2051 | 2052 | grid = (grid > threshold).type(torch.uint8) 2053 | empty = torch.zeros(1, 1, height, width, dtype=torch.uint8) 2054 | 2055 | # neighbor convolution kernel 2056 | kernel = torch.ones(1, 1, 3, 3, dtype=torch.uint8) 2057 | kernel[0, 0, 1, 1] = 0 2058 | 2059 | game_states = [[], [], []] # grid, turn_off, turn_on 2060 | game_states[0].append(grid.detach().clone()) 2061 | game_states[1].append(empty.detach().clone()) 2062 | game_states[2].append(empty.detach().clone()) 2063 | for step in range(steps - 1): 2064 | new_state = grid.detach().clone() 2065 | neighbors = F.conv2d(F.pad(new_state, pad=(1, 1, 1, 1), mode="circular"), kernel) #, padding="same") 2066 | 2067 | # If a cell is ON and has fewer than two neighbors that are ON, it turns OFF 2068 | new_state[(new_state == 1) == (neighbors < 2)] = 0 2069 | 2070 | # If a cell is ON and has either two or three neighbors that are ON, it remains ON. 2071 | 2072 | # If a cell is ON and has more than three neighbors that are ON, it turns OFF. 2073 | new_state[(new_state == 1) == (neighbors > 3)] = 0 2074 | 2075 | # If a cell is OFF and has exactly three neighbors that are ON, it turns ON. 2076 | new_state[(new_state == 0) == (neighbors == 3)] = 1 2077 | 2078 | turn_off = ((grid - new_state) == 1).type(torch.uint8) 2079 | turn_on = ((new_state - grid) == 1).type(torch.uint8) 2080 | 2081 | game_states[0].append(new_state.detach().clone()) 2082 | game_states[1].append(turn_off.detach().clone()) 2083 | game_states[2].append(turn_on.detach().clone()) 2084 | 2085 | grid = new_state 2086 | 2087 | def postprocess(tensorlist, to_image=False): 2088 | game_anim = torch.cat(tensorlist, dim=0).type(torch.float32) 2089 | game_anim = F.interpolate(game_anim, size=(height * cell_size, width * cell_size)) 2090 | game_anim = torch.squeeze(game_anim, dim=1) # BCHW -> BHW 2091 | if to_image: 2092 | game_anim = game_anim.unsqueeze(-1).repeat(1,1,1,3) # BHWC 2093 | return game_anim 2094 | 2095 | image = postprocess(game_states[0], to_image=True) 2096 | mask = postprocess(game_states[0]) 2097 | off = postprocess(game_states[1]) 2098 | on = postprocess(game_states[2]) 2099 | 2100 | return (image, mask, off, on) 2101 | 2102 | modeltest_code_default = """d = model.model.model_config.unet_config 2103 | for k in d.keys(): 2104 | print(k, d[k])""" 2105 | 2106 | class ModelTest: 2107 | @classmethod 2108 | def INPUT_TYPES(s): 2109 | return {"required": { 2110 | "model": ("MODEL",), 2111 | "code": ("STRING", {"multiline": True, "default": modeltest_code_default}), 2112 | }} 2113 | RETURN_TYPES = () 2114 | FUNCTION = "test" 2115 | OUTPUT_NODE = True 2116 | CATEGORY = "utils" 2117 | 2118 | def test(self, model, code): 2119 | exec(code) 2120 | return () 2121 | 2122 | class ConditioningSubtract: 2123 | @classmethod 2124 | def INPUT_TYPES(s): 2125 | return {"required": { 2126 | "cond_orig": ("CONDITIONING", ), 2127 | "cond_subtract": ("CONDITIONING", ), 2128 | "subtract_strength": ("FLOAT", {"default": 1.0, "step": 0.01}), 2129 | }} 2130 | RETURN_TYPES = ("CONDITIONING",) 2131 | FUNCTION = "addWeighted" 2132 | 2133 | CATEGORY = "conditioning" 2134 | 2135 | def addWeighted(self, cond_orig, cond_subtract, subtract_strength): 2136 | out = [] 2137 | 2138 | if len(cond_subtract) > 1: 2139 | logging.warning("Warning: ConditioningSubtract cond_subtract contains more than 1 cond, only the first one will actually be applied to cond_orig.") 2140 | 2141 | cond_from = cond_subtract[0][0] 2142 | pooled_output_from = cond_subtract[0][1].get("pooled_output", None) 2143 | 2144 | for i in range(len(cond_orig)): 2145 | t1 = cond_orig[i][0] 2146 | pooled_output_to = cond_orig[i][1].get("pooled_output", pooled_output_from) 2147 | t0 = cond_from[:,:t1.shape[1]] 2148 | if t0.shape[1] < t1.shape[1]: 2149 | t0 = torch.cat([t0] + [torch.zeros((1, (t1.shape[1] - t0.shape[1]), t1.shape[2]))], dim=1) 2150 | 2151 | tw = t1 - torch.mul(t0, subtract_strength) 2152 | t_to = cond_orig[i][1].copy() 2153 | if pooled_output_from is not None and pooled_output_to is not None: 2154 | t_to["pooled_output"] = pooled_output_to - torch.mul(pooled_output_from, subtract_strength) 2155 | elif pooled_output_from is not None: 2156 | t_to["pooled_output"] = pooled_output_from 2157 | 2158 | n = [tw, t_to] 2159 | out.append(n) 2160 | return (out, ) 2161 | 2162 | class Noise_CustomNoise: 2163 | def __init__(self, noise_latent): 2164 | self.seed = 0 2165 | self.noise_latent = noise_latent 2166 | 2167 | def generate_noise(self, input_latent): 2168 | return self.noise_latent.detach().clone().cpu() 2169 | 2170 | class CustomNoise: 2171 | @classmethod 2172 | def INPUT_TYPES(s): 2173 | return {"required":{ 2174 | "noise": ("LATENT",), 2175 | }} 2176 | 2177 | RETURN_TYPES = ("NOISE",) 2178 | FUNCTION = "get_noise" 2179 | CATEGORY = "sampling/custom_sampling/noise" 2180 | 2181 | def get_noise(self, noise): 2182 | noise_latent = noise["samples"].detach().clone() 2183 | std, mean = torch.std_mean(noise_latent, dim=(-2, -1), keepdim=True) 2184 | noise_latent = (noise_latent - mean) / std 2185 | return (Noise_CustomNoise(noise_latent),) 2186 | 2187 | class ExtractNFrames: 2188 | @classmethod 2189 | def INPUT_TYPES(s): 2190 | return { 2191 | "required": { 2192 | "frames": ("INT", {"default": 16, "min": 2}), 2193 | }, 2194 | "optional": { 2195 | "images": ("IMAGE",), 2196 | "masks": ("MASK",), 2197 | }, 2198 | } 2199 | 2200 | RETURN_TYPES = ("LIST", "IMAGE", "MASK") 2201 | RETURN_NAMES = ("index_list", "images", "masks") 2202 | FUNCTION = "extract" 2203 | 2204 | CATEGORY = "image/filters/frames" 2205 | 2206 | def extract(self, frames, images=None, masks=None): 2207 | original_length = 2 2208 | if images is not None: original_length = max(original_length, len(images)) 2209 | if masks is not None: original_length = max(original_length, len(masks)) 2210 | 2211 | n = min(original_length, frames) 2212 | step = step = (original_length - 1) / (n - 1) 2213 | ids = [round(i * step) for i in range(n)] 2214 | while len(ids) < frames: 2215 | ids.append(ids[-1]) 2216 | 2217 | new_images = [] 2218 | new_masks = [] 2219 | for i in ids: 2220 | if images is not None: 2221 | new_images.append(images[min(i, len(images) - 1)].detach().clone()) 2222 | else: 2223 | new_images.append(torch.zeros(512, 512, 3)) 2224 | 2225 | if masks is not None: 2226 | new_masks.append(masks[min(i, len(masks) - 1)].detach().clone()) 2227 | else: 2228 | new_masks.append(torch.zeros(512, 512)) 2229 | 2230 | return (ids, torch.stack(new_images, dim=0), torch.stack(new_masks, dim=0)) 2231 | 2232 | class MergeFramesByIndex: 2233 | @classmethod 2234 | def INPUT_TYPES(s): 2235 | return { 2236 | "required": { 2237 | "index_list": ("LIST",), 2238 | "orig_images": ("IMAGE",), 2239 | "images": ("IMAGE",), 2240 | }, 2241 | "optional": { 2242 | "orig_masks": ("MASK",), 2243 | "masks": ("MASK",), 2244 | }, 2245 | } 2246 | 2247 | RETURN_TYPES = ("IMAGE", "MASK") 2248 | RETURN_NAMES = ("images", "masks") 2249 | FUNCTION = "merge" 2250 | 2251 | CATEGORY = "image/filters/frames" 2252 | 2253 | def merge(self, index_list, orig_images, images, orig_masks=None, masks=None): 2254 | new_images = orig_images.detach().clone() 2255 | new_masks = torch.ones_like(new_images[..., 0]) # BHW 2256 | if orig_masks is not None: 2257 | for i in range(len(new_masks)): 2258 | new_masks[i] = orig_masks[min(i, len(orig_masks) - 1)].detach().clone() 2259 | 2260 | for i, frame in enumerate(index_list): 2261 | frame_mask = masks[i] if masks is not None else torch.ones_like(new_masks[i]) 2262 | new_images[frame] *= (1 - frame_mask[..., None]) 2263 | new_images[frame] += images[i].detach().clone() * frame_mask[..., None] 2264 | new_masks[frame] *= 0 2265 | 2266 | return (new_images, new_masks) 2267 | 2268 | class Hunyuan3Dv2LatentUpscaleBy: 2269 | @classmethod 2270 | def INPUT_TYPES(s): 2271 | return { 2272 | "required": { 2273 | "samples": ("LATENT",), 2274 | "scale_by": ("FLOAT", {"default": 2.0, "min": 0.01, "max": 8.0, "step": 0.01}), 2275 | }, 2276 | } 2277 | 2278 | RETURN_TYPES = ("LATENT",) 2279 | FUNCTION = "upscale" 2280 | CATEGORY = "latent/filters" 2281 | 2282 | def upscale(self, samples, scale_by): 2283 | s = samples.copy() 2284 | size = round(samples["samples"].shape[-1] * scale_by) 2285 | s["samples"] = F.interpolate(samples["samples"], size=(size,), mode="nearest-exact") 2286 | return (s,) 2287 | 2288 | NODE_CLASS_MAPPINGS = { 2289 | "AdainFilterLatent": AdainFilterLatent, 2290 | "AdainImage": AdainImage, 2291 | "AdainLatent": AdainLatent, 2292 | "AlphaClean": AlphaClean, 2293 | "AlphaMatte": AlphaMatte, 2294 | "BatchAlign": BatchAlign, 2295 | "BatchAverageImage": BatchAverageImage, 2296 | "BatchAverageUnJittered": BatchAverageUnJittered, 2297 | "BatchNormalizeImage": BatchNormalizeImage, 2298 | "BatchNormalizeLatent": BatchNormalizeLatent, 2299 | "BetterFilmGrain": BetterFilmGrain, 2300 | "BilateralFilterImage": BilateralFilterImage, 2301 | "BlurImageFast": BlurImageFast, 2302 | "BlurMaskFast": BlurMaskFast, 2303 | "ClampImage": ClampImage, 2304 | "ClampOutliers": ClampOutliers, 2305 | "ColorMatchImage": ColorMatchImage, 2306 | "ConditioningSubtract": ConditioningSubtract, 2307 | "ConvertNormals": ConvertNormals, 2308 | "CustomNoise": CustomNoise, 2309 | "DepthToNormals": DepthToNormals, 2310 | "DifferenceChecker": DifferenceChecker, 2311 | "DilateErodeMask": DilateErodeMask, 2312 | "EnhanceDetail": EnhanceDetail, 2313 | "ExposureAdjust": ExposureAdjust, 2314 | "ExtractNFrames": ExtractNFrames, 2315 | "FrequencyCombine": FrequencyCombine, 2316 | "FrequencySeparate": FrequencySeparate, 2317 | "GameOfLife": GameOfLife, 2318 | "GuidedFilterAlpha": GuidedFilterAlpha, 2319 | "GuidedFilterImage": GuidedFilterImage, 2320 | "Hunyuan3Dv2LatentUpscaleBy": Hunyuan3Dv2LatentUpscaleBy, 2321 | "ImageConstant": ImageConstant, 2322 | "ImageConstantHSV": ImageConstantHSV, 2323 | "InpaintConditionApply": InpaintConditionApply, 2324 | "InpaintConditionEncode": InpaintConditionEncode, 2325 | "InstructPixToPixConditioningAdvanced": InstructPixToPixConditioningAdvanced, 2326 | "JitterImage": JitterImage, 2327 | "Keyer": Keyer, 2328 | "LatentNormalizeShuffle": LatentNormalizeShuffle, 2329 | "RandnLikeLatent": RandnLikeLatent, 2330 | "LatentStats": LatentStats, 2331 | "MedianFilterImage": MedianFilterImage, 2332 | "MergeFramesByIndex": MergeFramesByIndex, 2333 | "ModelTest": ModelTest, 2334 | "NormalMapSimple": NormalMapSimple, 2335 | "OffsetLatentImage": OffsetLatentImage, 2336 | "PrintSigmas": PrintSigmas, 2337 | "RelightSimple": RelightSimple, 2338 | "RemapRange": RemapRange, 2339 | "RestoreDetail": RestoreDetail, 2340 | "SharpenFilterLatent": SharpenFilterLatent, 2341 | "ShuffleChannels": ShuffleChannels, 2342 | "Tonemap": Tonemap, 2343 | "UnJitterImage": UnJitterImage, 2344 | "UnTonemap": UnTonemap, 2345 | "VisualizeLatents": VisualizeLatents, 2346 | } 2347 | 2348 | NODE_DISPLAY_NAME_MAPPINGS = { 2349 | "AdainFilterLatent": "AdaIN Filter (Latent)", 2350 | "AdainImage": "AdaIN (Image)", 2351 | "AdainLatent": "AdaIN (Latent)", 2352 | "AlphaClean": "Alpha Clean", 2353 | "AlphaMatte": "Alpha Matte", 2354 | "BatchAlign": "Batch Align (RAFT)", 2355 | "BatchAverageImage": "Batch Average Image", 2356 | "BatchAverageUnJittered": "Batch Average Un-Jittered", 2357 | "BatchNormalizeImage": "Batch Normalize (Image)", 2358 | "BatchNormalizeLatent": "Batch Normalize (Latent)", 2359 | "BetterFilmGrain": "Better Film Grain", 2360 | "BilateralFilterImage": "Bilateral Filter Image", 2361 | "BlurImageFast": "Blur Image (Fast)", 2362 | "BlurMaskFast": "Blur Mask (Fast)", 2363 | "ClampImage": "Clamp Image", 2364 | "ClampOutliers": "Clamp Outliers", 2365 | "ColorMatchImage": "Color Match Image", 2366 | "ConditioningSubtract": "ConditioningSubtract", 2367 | "ConvertNormals": "Convert Normals", 2368 | "CustomNoise": "CustomNoise", 2369 | "DepthToNormals": "Depth To Normals", 2370 | "DifferenceChecker": "Difference Checker", 2371 | "DilateErodeMask": "Dilate/Erode Mask", 2372 | "EnhanceDetail": "Enhance Detail", 2373 | "ExposureAdjust": "Exposure Adjust", 2374 | "ExtractNFrames": "Extract N Frames", 2375 | "FrequencyCombine": "Frequency Combine", 2376 | "FrequencySeparate": "Frequency Separate", 2377 | "GameOfLife": "Game Of Life", 2378 | "GuidedFilterAlpha": "(DEPRECATED) Guided Filter Alpha", 2379 | "GuidedFilterImage": "Guided Filter Image", 2380 | "Hunyuan3Dv2LatentUpscaleBy": "Upscale Hunyuan3Dv2 Latent By", 2381 | "ImageConstant": "Image Constant Color (RGB)", 2382 | "ImageConstantHSV": "Image Constant Color (HSV)", 2383 | "InpaintConditionApply": "Inpaint Condition Apply", 2384 | "InpaintConditionEncode": "Inpaint Condition Encode", 2385 | "InstructPixToPixConditioningAdvanced": "InstructPixToPixConditioningAdvanced", 2386 | "JitterImage": "Jitter Image", 2387 | "Keyer": "Keyer", 2388 | "LatentNormalizeShuffle": "LatentNormalizeShuffle", 2389 | "RandnLikeLatent": "RandnLikeLatent", 2390 | "LatentStats": "Latent Stats", 2391 | "MedianFilterImage": "Median Filter Image", 2392 | "MergeFramesByIndex": "Merge Frames By Index", 2393 | "ModelTest": "Model Test", 2394 | "NormalMapSimple": "Normal Map (Simple)", 2395 | "OffsetLatentImage": "Offset Latent Image", 2396 | "PrintSigmas": "PrintSigmas", 2397 | "RelightSimple": "Relight (Simple)", 2398 | "RemapRange": "Remap Range", 2399 | "RestoreDetail": "Restore Detail", 2400 | "SharpenFilterLatent": "Sharpen Filter (Latent)", 2401 | "ShuffleChannels": "Shuffle Channels", 2402 | "Tonemap": "Tonemap", 2403 | "UnJitterImage": "Un-Jitter Image", 2404 | "UnTonemap": "UnTonemap", 2405 | "VisualizeLatents": "Visualize Latents", 2406 | } -------------------------------------------------------------------------------- /raft.py: -------------------------------------------------------------------------------- 1 | import os 2 | import torch 3 | import torch.nn.functional as F 4 | from torchvision.models.optical_flow import Raft_Large_Weights, raft_large 5 | 6 | 7 | def load_raft(): 8 | model_dir = os.path.join(os.path.split(__file__)[0], "models") 9 | if not os.path.exists(model_dir): 10 | os.mkdir(model_dir) 11 | 12 | raft_weights = Raft_Large_Weights.DEFAULT 13 | raft_path = os.path.join(model_dir, str(raft_weights) + ".pth") 14 | 15 | if os.path.exists(raft_path): 16 | model = raft_large() 17 | model.load_state_dict(torch.load(raft_path)) 18 | else: 19 | model = raft_large(weights=raft_weights, progress=True) 20 | torch.save(model.state_dict(), raft_path) 21 | 22 | device = "cuda" if torch.cuda.is_available() else "cpu" 23 | model = model.to(device).eval() 24 | return (model, device) 25 | 26 | def raft_flow(model, device, batch1, batch2): 27 | orig_H = batch1.shape[2] 28 | orig_W = batch1.shape[3] 29 | scale_factor = max(orig_H, orig_W) / 512 30 | new_H = int(((orig_H / scale_factor) // 8) * 8) 31 | new_W = int(((orig_W / scale_factor) // 8) * 8) 32 | 33 | if scale_factor > 1 or max(orig_H % 8, orig_W % 8) > 0: 34 | batch1_scaled = F.interpolate(batch1, size=(new_H, new_W), mode='bilinear') 35 | batch2_scaled = F.interpolate(batch2, size=(new_H, new_W), mode='bilinear') 36 | 37 | with torch.no_grad(): 38 | flow = model(batch1_scaled.to(device), batch2_scaled.to(device))[-1] 39 | flow = F.interpolate(flow, size=(orig_H, orig_W), mode='bilinear') 40 | flow[:,0,:,:] *= orig_W / new_W 41 | flow[:,1,:,:] *= orig_H / new_H 42 | else: 43 | with torch.no_grad(): 44 | flow = model(batch1.to(device), batch2.to(device))[-1] 45 | 46 | return flow.to(batch1.device) 47 | 48 | def flow_warp(image, flow): 49 | B, C, H, W = image.size() 50 | # mesh grid 51 | xx = torch.arange(0, W).view(1, -1).repeat(H, 1) 52 | yy = torch.arange(0, H).view(-1, 1).repeat(1, W) 53 | xx = xx.view(1, 1, H, W).repeat(B, 1, 1, 1) 54 | yy = yy.view(1, 1, H, W).repeat(B, 1, 1, 1) 55 | grid = torch.cat((xx, yy), 1).float() 56 | 57 | grid = grid.to(image.device) 58 | vgrid = grid + flow 59 | 60 | # scale grid to [-1,1] for grid_sample 61 | vgrid[:, 0, :, :] = 2.0 * vgrid[:, 0, :, :].clone() / max(W - 1, 1) - 1.0 62 | vgrid[:, 1, :, :] = 2.0 * vgrid[:, 1, :, :].clone() / max(H - 1, 1) - 1.0 63 | vgrid = vgrid.permute(0, 2, 3, 1) 64 | output = F.grid_sample(image, vgrid, mode='bicubic', padding_mode='border', align_corners=True) 65 | return output -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opencv-contrib-python>=4.7.0.72 2 | opencv-contrib-python-headless>=4.7.0.72 3 | opencv-python>=4.7.0.72 4 | opencv-python-headless>=4.7.0.72 5 | pymatting -------------------------------------------------------------------------------- /toy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacepxl/ComfyUI-Image-Filters/5abb3c2395739e0b94c2061bbc49bd1349de97c4/toy.png -------------------------------------------------------------------------------- /workflow_images/alpha_matte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacepxl/ComfyUI-Image-Filters/5abb3c2395739e0b94c2061bbc49bd1349de97c4/workflow_images/alpha_matte.png -------------------------------------------------------------------------------- /workflow_images/enhance_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacepxl/ComfyUI-Image-Filters/5abb3c2395739e0b94c2061bbc49bd1349de97c4/workflow_images/enhance_detail.png -------------------------------------------------------------------------------- /workflow_images/guided_filter_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacepxl/ComfyUI-Image-Filters/5abb3c2395739e0b94c2061bbc49bd1349de97c4/workflow_images/guided_filter_alpha.png --------------------------------------------------------------------------------