Adds Kuwahara Blur and Parabolize
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
wip/
|
||||
.vscode/*
|
||||
@@ -18,19 +18,23 @@ Both images have the workflow attached, and it is included so feel free to use i
|
||||
- Blur: Applies a Gaussian blur to the input image, softening the details
|
||||
- CannyEdgeDetection: Applies Canny edge detection to the input image
|
||||
- Chromatic Aberration: Shifts the color channels in an image, creating a glitch aesthetic
|
||||
- ColorCorrect: Adjusts the color balance, temperature, hue, brightness, contrast, saturation, and gamma of an image
|
||||
- **ColorCorrect: Adjusts the color balance, temperature, hue, brightness, contrast, saturation, and gamma of an image**
|
||||
- Dissolve: Creates a grainy blend of two images using random pixels based on a dissolve factor.
|
||||
- DodgeAndBurn: Adjusts image brightness using dodge and burn effects based on a mask and intensity.
|
||||
- FilmGrain: Adds a film grain effect to the image, along with options to control the temperature, and vignetting
|
||||
- Glow: Applies a blur with a specified radius and then blends it with the original image. Creates a nice glowing effect.
|
||||
- **KuwaharaBlur: Applies an edge preserving blur, creating a stunning and unique effect.**
|
||||
- Parabolize: Applies a color transformation effect using a parabolic formula
|
||||
- PencilSketch: Converts an image into a hand-drawn pencil sketch style.
|
||||
- PixelSort: Rearranges the pixels in the input image based on their values, and input mask. Creates a cool glitch like effect.
|
||||
- **PixelSort: Rearranges the pixels in the input image based on their values, and input mask. Creates a cool glitch like effect.**
|
||||
- Pixelize: Applies a pixelization effect, simulating the reducing of resolution
|
||||
- Quantize: Set and dither the amount of colors in an image from 0-256, reducing color information
|
||||
- **Quantize: Set and dither the amount of colors in an image from 0-256, reducing color information**
|
||||
- Sharpen: Enhances the details in an image by applying a sharpening filter
|
||||
- Solarize: Inverts image colors based on a threshold for a striking, high-contrast effect
|
||||
- **Solarize: Inverts image colors based on a threshold for a striking, high-contrast effect**
|
||||
- Vignette: Applies a vignette effect, putting the corners of the image in shadow
|
||||
|
||||
**Bolded Nodes are my Personal Favorites, and highly recommended to expirement with**
|
||||
|
||||
## Combine Nodes
|
||||
|
||||
By default `post_processing_nodes.py` should have all of the combined nodes. If you want a subset of nodes, you can run
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
import multiprocessing as mp
|
||||
import torch
|
||||
|
||||
class KuwaharaBlur:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"blur_radius": ("INT", {
|
||||
"default": 3,
|
||||
"min": 0,
|
||||
"max": 31,
|
||||
"step": 1
|
||||
}),
|
||||
"method": (["mean", "gaussian"],),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "apply_kuwahara_filter"
|
||||
|
||||
CATEGORY = "postprocessing"
|
||||
|
||||
def apply_kuwahara_filter(self, image: np.ndarray, blur_radius: int, method: str):
|
||||
if blur_radius == 0:
|
||||
return (image,)
|
||||
|
||||
out = torch.zeros_like(image)
|
||||
batch_size, height, width, channels = image.shape
|
||||
|
||||
for b in range(batch_size):
|
||||
image = image[b].cpu().numpy() * 255.0
|
||||
image = image.astype(np.uint8)
|
||||
|
||||
out[b] = torch.from_numpy(kuwahara(image, method=method, radius=blur_radius)) / 255.0
|
||||
|
||||
return (out,)
|
||||
|
||||
def kuwahara(orig_img, method="mean", radius=3, sigma=None):
|
||||
if method == "gaussian" and sigma is None:
|
||||
sigma = -1
|
||||
|
||||
image = orig_img.astype(np.float32, copy=False)
|
||||
avgs = np.empty((4, *image.shape), dtype=image.dtype)
|
||||
stddevs = np.empty((4, *image.shape[:2]), dtype=image.dtype)
|
||||
image_2d = cv2.cvtColor(orig_img, cv2.COLOR_BGR2GRAY).astype(image.dtype, copy=False)
|
||||
avgs_2d = np.empty((4, *image.shape[:2]), dtype=image.dtype)
|
||||
|
||||
squared_img = image_2d ** 2
|
||||
|
||||
if method == "mean":
|
||||
kxy = np.ones(radius + 1, dtype=image.dtype) / (radius + 1)
|
||||
elif method == "gaussian":
|
||||
kxy = cv2.getGaussianKernel(2 * radius + 1, sigma, ktype=cv2.CV_32F)
|
||||
kxy /= kxy[radius:].sum()
|
||||
klr = np.array([kxy[:radius+1], kxy[radius:]])
|
||||
kindexes = [[1, 1], [1, 0], [0, 1], [0, 0]]
|
||||
|
||||
shift = [(0, 0), (0, radius), (radius, 0), (radius, radius)]
|
||||
|
||||
for k in range(4):
|
||||
if method == "mean":
|
||||
kx, ky = kxy, kxy
|
||||
else:
|
||||
kx, ky = klr[kindexes[k]]
|
||||
cv2.sepFilter2D(image, -1, kx, ky, avgs[k], shift[k])
|
||||
cv2.sepFilter2D(image_2d, -1, kx, ky, avgs_2d[k], shift[k])
|
||||
cv2.sepFilter2D(squared_img, -1, kx, ky, stddevs[k], shift[k])
|
||||
stddevs[k] = stddevs[k] - avgs_2d[k] ** 2
|
||||
|
||||
indices = np.argmin(stddevs, axis=0)
|
||||
filtered = np.take_along_axis(avgs, indices[None,...,None], 0).reshape(image.shape)
|
||||
|
||||
return filtered.astype(orig_img.dtype)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KuwaharaBlur": KuwaharaBlur
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import torch
|
||||
|
||||
class Parabolize:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"coeff": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": -10.0,
|
||||
"max": 10.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
"vertex_x": ("FLOAT", {
|
||||
"default": 0.5,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
"vertex_y": ("FLOAT", {
|
||||
"default": 0.5,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "parabolize_image"
|
||||
|
||||
CATEGORY = "postprocessing"
|
||||
|
||||
def parabolize_image(self, image: torch.Tensor, coeff: float, vertex_x: float, vertex_y: float):
|
||||
parabolized_image = coeff * torch.pow(image - vertex_x, 2) + vertex_y
|
||||
parabolized_image = torch.clamp(parabolized_image, 0, 1)
|
||||
return (parabolized_image,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Parabolize": Parabolize,
|
||||
}
|
||||
+375
-102
@@ -3,12 +3,9 @@ import torch.nn.functional as F
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image, ImageEnhance
|
||||
import multiprocessing as mp
|
||||
from PIL import Image
|
||||
import imageio.v2 as imageio
|
||||
from math import sqrt
|
||||
import sys
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
|
||||
|
||||
@@ -661,6 +658,121 @@ class Glow:
|
||||
def add_glow(self, img, blurred_img, intensity):
|
||||
return img + blurred_img * intensity
|
||||
|
||||
class KuwaharaBlur:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"blur_radius": ("INT", {
|
||||
"default": 3,
|
||||
"min": 0,
|
||||
"max": 31,
|
||||
"step": 1
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "apply_kuwahara_filter"
|
||||
|
||||
CATEGORY = "postprocessing"
|
||||
|
||||
def apply_kuwahara_filter(self, image: np.ndarray, blur_radius: int):
|
||||
if blur_radius == 0:
|
||||
return (image,)
|
||||
|
||||
out = torch.zeros_like(image)
|
||||
batch_size, height, width, channels = image.shape
|
||||
|
||||
for b in range(batch_size):
|
||||
image = image[b].cpu().numpy() * 255.0
|
||||
image = image.astype(np.uint8)
|
||||
|
||||
out[b] = torch.from_numpy(kuwahara(image, method="gaussian", radius=blur_radius)) / 255.0
|
||||
|
||||
return (out,)
|
||||
|
||||
def kuwahara(orig_img, method="mean", radius=3, sigma=None, grayconv=cv2.COLOR_BGR2GRAY, image_2d=None):
|
||||
if method == "gaussian" and sigma is None:
|
||||
sigma = -1
|
||||
|
||||
image = orig_img.astype(np.float32, copy=False)
|
||||
image_2d = image_2d.astype(image.dtype, copy=False) if image_2d is not None else None
|
||||
avgs = np.empty((4, *image.shape), dtype=image.dtype)
|
||||
stddevs = np.empty((4, *image.shape[:2]), dtype=image.dtype)
|
||||
|
||||
if image_2d is None:
|
||||
image_2d = cv2.cvtColor(orig_img, grayconv).astype(image.dtype, copy=False)
|
||||
avgs_2d = np.empty((4, *image.shape[:2]), dtype=image.dtype)
|
||||
|
||||
squared_img = image_2d ** 2
|
||||
|
||||
if method == "mean":
|
||||
kxy = np.ones(radius + 1, dtype=image.dtype) / (radius + 1)
|
||||
elif method == "gaussian":
|
||||
kxy = cv2.getGaussianKernel(2 * radius + 1, sigma, ktype=cv2.CV_32F)
|
||||
kxy /= kxy[radius:].sum()
|
||||
klr = np.array([kxy[:radius+1], kxy[radius:]])
|
||||
kindexes = [[1, 1], [1, 0], [0, 1], [0, 0]]
|
||||
|
||||
shift = [(0, 0), (0, radius), (radius, 0), (radius, radius)]
|
||||
|
||||
for k in range(4):
|
||||
kx, ky = kxy, kxy if method == "mean" else klr[kindexes[k]]
|
||||
cv2.sepFilter2D(image, -1, kx, ky, avgs[k], shift[k])
|
||||
cv2.sepFilter2D(image_2d, -1, kx, ky, avgs_2d[k], shift[k])
|
||||
cv2.sepFilter2D(squared_img, -1, kx, ky, stddevs[k], shift[k])
|
||||
stddevs[k] = stddevs[k] - avgs_2d[k] ** 2
|
||||
|
||||
indices = np.argmin(stddevs, axis=0)
|
||||
filtered = np.take_along_axis(avgs, indices[None,...,None], 0).reshape(image.shape)
|
||||
|
||||
return filtered.astype(orig_img.dtype)
|
||||
|
||||
class Parabolize:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"coeff": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": -10.0,
|
||||
"max": 10.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
"vertex_x": ("FLOAT", {
|
||||
"default": 0.5,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
"vertex_y": ("FLOAT", {
|
||||
"default": 0.5,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "parabolize_image"
|
||||
|
||||
CATEGORY = "postprocessing"
|
||||
|
||||
def parabolize_image(self, image: torch.Tensor, coeff: float, vertex_x: float, vertex_y: float):
|
||||
parabolized_image = coeff * torch.pow(image - vertex_x, 2) + vertex_y
|
||||
parabolized_image = torch.clamp(parabolized_image, 0, 1)
|
||||
return (parabolized_image,)
|
||||
|
||||
class PencilSketch:
|
||||
def __init__(self):
|
||||
pass
|
||||
@@ -1068,6 +1180,189 @@ class ElectroShock:
|
||||
|
||||
return (result,)
|
||||
|
||||
class KuwaharaFilter:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"blur_radius": ("INT", {
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 15,
|
||||
"step": 1
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "apply_kuwahara_filter"
|
||||
|
||||
CATEGORY = "postprocessing"
|
||||
|
||||
def apply_kuwahara_filter(self, image: np.ndarray, blur_radius: int):
|
||||
if blur_radius == 0:
|
||||
return (image,)
|
||||
|
||||
kernel_size = blur_radius * 2 + 1
|
||||
out = torch.zeros_like(image)
|
||||
|
||||
batch_size, height, width, channels = image.shape
|
||||
|
||||
for b in range(batch_size):
|
||||
image = image[b].cpu().numpy() * 255.0
|
||||
image = image.astype(np.uint8)
|
||||
|
||||
out[b] = torch.from_numpy(kuwahara_filter_rgb(image, kernel_size)) / 255.0
|
||||
|
||||
return (out,)
|
||||
|
||||
def kuwahara_filter_rgb(img, kernel_size):
|
||||
b, g, r = cv2.split(img)
|
||||
|
||||
b_filtered, g_filtered, r_filtered = apply_filter((b, kernel_size)), apply_filter((g, kernel_size)), apply_filter((r, kernel_size))
|
||||
|
||||
out = cv2.merge((b_filtered, g_filtered, r_filtered))
|
||||
|
||||
return out
|
||||
|
||||
def apply_filter(args):
|
||||
channel, kernel_size = args
|
||||
return kuwahara_filter(channel, kernel_size)
|
||||
|
||||
def kuwahara_filter(img, kernel_size):
|
||||
# Pad the image to handle borders
|
||||
pad_size = kernel_size // 2
|
||||
img_padded = cv2.copyMakeBorder(img, pad_size, pad_size, pad_size, pad_size, cv2.BORDER_REFLECT)
|
||||
|
||||
# Initialize output image
|
||||
h, w = img.shape[:2]
|
||||
out = np.zeros_like(img)
|
||||
|
||||
# Apply Kuwahara filter to each pixel
|
||||
for i in range(pad_size, h + pad_size):
|
||||
for j in range(pad_size, w + pad_size):
|
||||
# Divide the image into 4 overlapping square regions
|
||||
regions = [
|
||||
img_padded[i-pad_size:i+pad_size+1, j-pad_size:j+pad_size+1],
|
||||
img_padded[i-pad_size:i+pad_size+1, j:j+kernel_size+1],
|
||||
img_padded[i:i+kernel_size+1, j-pad_size:j+pad_size+1],
|
||||
img_padded[i:i+kernel_size+1, j:j+kernel_size+1]
|
||||
]
|
||||
|
||||
# Compute mean and variance of each region
|
||||
means = [np.mean(region) for region in regions]
|
||||
variances = [np.var(region) for region in regions]
|
||||
|
||||
# Choose the region with the smallest variance as the output value
|
||||
min_var_index = np.argmin(variances)
|
||||
out[i-pad_size, j-pad_size] = means[min_var_index]
|
||||
|
||||
return out
|
||||
|
||||
class Liquidify:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"viscosity": ("INT", {
|
||||
"default": 10,
|
||||
"min": 0,
|
||||
"max": 20,
|
||||
"step": 1
|
||||
}),
|
||||
"turbulence": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "liquidify"
|
||||
|
||||
CATEGORY = "postprocessing"
|
||||
|
||||
def liquidify(image: torch.Tensor, viscosity: int, turbulence: float):
|
||||
image = image.permute(0, 3, 1, 2) # Torch wants (B, C, H, W) we use (B, H, W, C)
|
||||
|
||||
n, c, h, w = image.size()
|
||||
grid_x, grid_y = torch.meshgrid(torch.arange(h), torch.arange(w))
|
||||
grid_x = grid_x.to(image.device)
|
||||
grid_y = grid_y.to(image.device)
|
||||
|
||||
displacement = torch.randn(n, 2, h, w).to(image.device)
|
||||
displacement = F.gaussian_blur(displacement, kernel_size=viscosity, sigma=turbulence)
|
||||
|
||||
flow_x = torch.clamp(grid_x + displacement[:, 0], 0, w - 1).unsqueeze(1) - grid_x.unsqueeze(0)
|
||||
flow_y = torch.clamp(grid_y + displacement[:, 1], 0, h - 1).unsqueeze(1) - grid_y.unsqueeze(0)
|
||||
|
||||
warped = F.grid_sample(image, torch.stack((flow_x, flow_y), dim=1), padding_mode='border')
|
||||
|
||||
warped = warped.permute(0, 2, 3, 1) # Back to (B, H, W, C)
|
||||
return (warped,)
|
||||
|
||||
class StippleEffect:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"dot_size": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.1,
|
||||
"max": 5.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
"density": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.1,
|
||||
"max": 5.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
"intensity": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.1,
|
||||
"max": 5.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "stipple_effect"
|
||||
|
||||
CATEGORY = "postprocessing"
|
||||
|
||||
def stipple_effect(self, image: torch.Tensor, dot_size: float, density: float, intensity: float):
|
||||
def create_dot_pattern(dot_size, intensity):
|
||||
dot_pattern = torch.ones((1, 1, int(dot_size), int(dot_size))) * intensity
|
||||
return dot_pattern
|
||||
|
||||
x = image.permute(0, 3, 1, 2)
|
||||
gray_image = x.mean(dim=1, keepdim=True)
|
||||
dot_pattern = create_dot_pattern(dot_size, intensity)
|
||||
|
||||
stippled_image = torch.nn.functional.conv2d(gray_image, dot_pattern, stride=int(dot_size), groups=1)
|
||||
stippled_image = torch.clamp(stippled_image, 0, 1)
|
||||
|
||||
output = stippled_image.expand(-1, 3, -1, -1)
|
||||
output = output.permute(0, 2, 3, 1)
|
||||
|
||||
return (output,)
|
||||
|
||||
def gaussian_kernel(kernel_size: int, sigma: float):
|
||||
x, y = torch.meshgrid(torch.linspace(-1, 1, kernel_size), torch.linspace(-1, 1, kernel_size), indexing="ij")
|
||||
d = torch.sqrt(x * x + y * y)
|
||||
@@ -1165,115 +1460,88 @@ def pixel_sort(img, mask, horizontal_sort=False, span_limit=None, sort_by='H', r
|
||||
|
||||
return sorted_image
|
||||
|
||||
def get_fish_xn_yn(source_x, source_y, radius, distortion):
|
||||
"""
|
||||
Get normalized x, y pixel coordinates from the original image and return normalized
|
||||
x, y pixel coordinates in the destination fished image.
|
||||
:param distortion: Amount in which to move pixels from/to center.
|
||||
As distortion grows, pixels will be moved further from the center, and vice versa.
|
||||
"""
|
||||
def kuwahara_filter(img, kernel_size):
|
||||
# Pad the image to handle borders
|
||||
pad_size = kernel_size // 2
|
||||
img_padded = cv2.copyMakeBorder(img, pad_size, pad_size, pad_size, pad_size, cv2.BORDER_REFLECT)
|
||||
|
||||
if 1 - distortion*(radius**2) == 0:
|
||||
return source_x, source_y
|
||||
# Initialize output image
|
||||
h, w = img.shape[:2]
|
||||
out = np.zeros_like(img)
|
||||
|
||||
return source_x / (1 - (distortion*(radius**2))), source_y / (1 - (distortion*(radius**2)))
|
||||
# Apply Kuwahara filter to each pixel
|
||||
for i in range(pad_size, h + pad_size):
|
||||
for j in range(pad_size, w + pad_size):
|
||||
# Divide the image into 4 overlapping square regions
|
||||
regions = [
|
||||
img_padded[i-pad_size:i+pad_size+1, j-pad_size:j+pad_size+1],
|
||||
img_padded[i-pad_size:i+pad_size+1, j:j+kernel_size+1],
|
||||
img_padded[i:i+kernel_size+1, j-pad_size:j+pad_size+1],
|
||||
img_padded[i:i+kernel_size+1, j:j+kernel_size+1]
|
||||
]
|
||||
|
||||
# Compute mean and variance of each region
|
||||
means = [np.mean(region) for region in regions]
|
||||
variances = [np.var(region) for region in regions]
|
||||
|
||||
# Choose the region with the smallest variance as the output value
|
||||
min_var_index = np.argmin(variances)
|
||||
out[i-pad_size, j-pad_size] = means[min_var_index]
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def kuwahara_filter_rgb(img, kernel_size):
|
||||
# Split the image into color channels
|
||||
b, g, r = cv2.split(img)
|
||||
|
||||
def fish(img, distortion_coefficient):
|
||||
"""
|
||||
:type img: numpy.ndarray
|
||||
:param distortion_coefficient: The amount of distortion to apply.
|
||||
:return: numpy.ndarray - the image with applied effect.
|
||||
"""
|
||||
# Apply the filter to each channel
|
||||
b_filtered = kuwahara_filter(b, kernel_size)
|
||||
g_filtered = kuwahara_filter(g, kernel_size)
|
||||
r_filtered = kuwahara_filter(r, kernel_size)
|
||||
|
||||
# If input image is only BW or RGB convert it to RGBA
|
||||
# So that output 'frame' can be transparent.
|
||||
w, h = img.shape[0], img.shape[1]
|
||||
if len(img.shape) == 2:
|
||||
# Duplicate the one BW channel twice to create Black and White
|
||||
# RGB image (For each pixel, the 3 channels have the same value)
|
||||
bw_channel = np.copy(img)
|
||||
img = np.dstack((img, bw_channel))
|
||||
img = np.dstack((img, bw_channel))
|
||||
if len(img.shape) == 3 and img.shape[2] == 3:
|
||||
print("RGB to RGBA")
|
||||
img = np.dstack((img, np.full((w, h), 255)))
|
||||
# Merge the filtered channels back into an RGB image
|
||||
out = cv2.merge((b_filtered, g_filtered, r_filtered))
|
||||
|
||||
# prepare array for dst image
|
||||
dstimg = np.zeros_like(img)
|
||||
|
||||
# floats for calcultions
|
||||
w, h = float(w), float(h)
|
||||
|
||||
# easier calcultion if we traverse x, y in dst image
|
||||
for x in range(len(dstimg)):
|
||||
for y in range(len(dstimg[x])):
|
||||
|
||||
# normalize x and y to be in interval of [-1, 1]
|
||||
xnd, ynd = float((2*x - w)/w), float((2*y - h)/h)
|
||||
|
||||
# get xn and yn distance from normalized center
|
||||
rd = sqrt(xnd**2 + ynd**2)
|
||||
|
||||
# new normalized pixel coordinates
|
||||
xdu, ydu = get_fish_xn_yn(xnd, ynd, rd, distortion_coefficient)
|
||||
|
||||
# convert the normalized distorted xdn and ydn back to image pixels
|
||||
xu, yu = int(((xdu + 1)*w)/2), int(((ydu + 1)*h)/2)
|
||||
|
||||
# if new pixel is in bounds copy from source pixel to destination pixel
|
||||
if 0 <= xu and xu < img.shape[0] and 0 <= yu and yu < img.shape[1]:
|
||||
dstimg[x][y] = img[xu][yu]
|
||||
|
||||
return dstimg.astype(np.uint8)
|
||||
return out
|
||||
|
||||
|
||||
|
||||
def parse_args(args=sys.argv[1:]):
|
||||
"""Parse arguments."""
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Apply fish-eye effect to images.",
|
||||
prog='python3 fish.py')
|
||||
|
||||
parser.add_argument("-i", "--image", help="path to image file."
|
||||
" If no input is given, the supplied example 'grid.jpg' will be used.",
|
||||
type=str, default="test.png")
|
||||
|
||||
parser.add_argument("-o", "--outpath", help="file path to write output to."
|
||||
" format: <path>.<format(jpg,png,etc..)>",
|
||||
type=str, default="fish.png")
|
||||
|
||||
parser.add_argument("-d", "--distortion",
|
||||
help="The distoration coefficient. How much the move pixels from/to the center."
|
||||
" Recommended values are between -1 and 1."
|
||||
" The bigger the distortion, the further pixels will be moved outwars from the center (fisheye)."
|
||||
" The Smaller the distortion, the closer pixels will be move inwards toward the center (rectilinear)."
|
||||
" For example, to reverse the fisheye effect with --distoration 0.5,"
|
||||
" You can run with --distortion -0.3."
|
||||
" Note that due to double processing the result will be somewhat distorted.",
|
||||
type=float, default=0.5)
|
||||
|
||||
return parser.parse_args(args)
|
||||
def apply_filter(args):
|
||||
channel, kernel_size = args
|
||||
return kuwahara_filter(channel, kernel_size)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args()
|
||||
try:
|
||||
imgobj = imageio.imread(args.image)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
if os.path.exists(args.outpath):
|
||||
ans = input(
|
||||
args.outpath + " exists. File will be overridden. Continue? y/n: ")
|
||||
if ans.lower() != 'y':
|
||||
print("exiting")
|
||||
sys.exit(0)
|
||||
def kuwahara_filter_rgb_multiprocessing(img, kernel_size):
|
||||
# Split the image into color channels
|
||||
b, g, r = cv2.split(img)
|
||||
|
||||
# Function to apply the filter to a channel
|
||||
|
||||
# Create a multiprocessing Pool with 3 processes
|
||||
with mp.Pool(3) as pool:
|
||||
# Map the apply_filter function to the channels
|
||||
b_filtered, g_filtered, r_filtered = pool.map(apply_filter, ((b, kernel_size), (g, kernel_size), (r, kernel_size)))
|
||||
|
||||
# Merge the filtered channels back into an RGB image
|
||||
out = cv2.merge((b_filtered, g_filtered, r_filtered))
|
||||
|
||||
return out
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
img = cv2.imread('test.png')
|
||||
|
||||
start_time = time.time()
|
||||
# Apply Kuwahara filter with kernel size of 5
|
||||
out = kuwahara_filter_rgb_multiprocessing(img, kernel_size=5)
|
||||
end_time = time.time()
|
||||
print(f"Time elapsed: {end_time - start_time:.5f} seconds")
|
||||
|
||||
# Display output image
|
||||
cv2.imshow('output_image', out)
|
||||
cv2.waitKey(0)
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
output_img = fish(imgobj, args.distortion)
|
||||
imageio.imwrite(args.outpath, output_img, format='png')
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ArithmeticBlend": ArithmeticBlend,
|
||||
@@ -1286,6 +1554,8 @@ NODE_CLASS_MAPPINGS = {
|
||||
"DodgeAndBurn": DodgeAndBurn,
|
||||
"FilmGrain": FilmGrain,
|
||||
"Glow": Glow,
|
||||
"KuwaharaBlur": KuwaharaBlur,
|
||||
"Parabolize": Parabolize,
|
||||
"PencilSketch": PencilSketch,
|
||||
"PixelSort": PixelSort,
|
||||
"Pixelize": Pixelize,
|
||||
@@ -1294,4 +1564,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"Solarize": Solarize,
|
||||
"Vignette": Vignette,
|
||||
"ElectroShock": ElectroShock,
|
||||
"KuwaharaFilter": KuwaharaFilter,
|
||||
"Liquidify": Liquidify,
|
||||
"StippleEffect": StippleEffect,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user