Skip to content

Commit

Permalink
Fast Gaussian blur (#1981)
Browse files Browse the repository at this point in the history
* Fast Gaussian blur

* fixed comment

* Ignored incorrect type error
  • Loading branch information
RunDevelopment authored Jul 24, 2023
1 parent 801f79e commit d6052dd
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 12 deletions.
95 changes: 94 additions & 1 deletion backend/src/nodes/impl/image_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from __future__ import annotations

import itertools
import math
import os
import random
import string
from enum import Enum
from typing import List
from typing import List, Tuple

import cv2
import numpy as np
Expand Down Expand Up @@ -332,3 +333,95 @@ def cartesian_product(arrays: List[np.ndarray]) -> np.ndarray:
for i, a in enumerate(arrays):
arr[i, ...] = a[idx[: la - i]]
return arr.reshape(la, -1).T


def fast_gaussian_blur(
img: np.ndarray,
sigma_x: float,
sigma_y: float | None = None,
) -> np.ndarray:
"""
Computes a channel-wise gaussian blur of the given image using a fast approximation.
The maximum error of the approximation is guaranteed to be less than 0.1%.
In addition to that, the error is guaranteed to be smoothly distributed across the image.
There are no sudden spikes in error anywhere.
Specifically, the method is implemented by downsampling the image, blurring the downsampled
image, and then upsampling the blurred image. This is much faster than blurring the full image.
Unfortunately, OpenCV's `resize` method has unfortunate artifacts when upscaling, so we
apply a small gaussian blur to the image after upscaling to smooth out the artifacts. This
single step almost doubles the runtime of the method, but it is still much faster than
blurring the full image.
"""
if sigma_y is None:
sigma_y = sigma_x
if sigma_x == 0 or sigma_y == 0:
return img.copy()

h, w, _ = get_h_w_c(img)

def get_scale_factor(sigma: float) -> float:
if sigma < 11:
return 1
if sigma < 15:
return 1.25
if sigma < 20:
return 1.5
if sigma < 25:
return 2
if sigma < 30:
return 2.5
if sigma < 50:
return 3
if sigma < 100:
return 4
if sigma < 200:
return 6
return 8

def get_sizing(size: int, sigma: float, f: float) -> Tuple[int, float, float]:
"""
Return the size of the downsampled image, the sigma of the downsampled gaussian blur,
and the sigma of the upscaled gaussian blur.
"""
if f <= 1:
# just use simple gaussian, the error is too large otherwise
return size, 0, sigma

size_down = math.ceil(size / f)
f = size / size_down
sigma_up = f
sigma_down = math.sqrt(sigma**2 - sigma_up**2) / f
return size_down, sigma_down, sigma_up

# Handling different sigma values for x and y is difficult, so we take the easy way out
# and just use the smaller one. There are potentially better ways of combining them, but
# this is good enough for now.
scale_factor = min(get_scale_factor(sigma_x), get_scale_factor(sigma_y))
h_down, y_down_sigma, y_up_sigma = get_sizing(h, sigma_y, scale_factor)
w_down, x_down_sigma, x_up_sigma = get_sizing(w, sigma_x, scale_factor)

if h != h_down or w != w_down:
# downsampled gaussian blur
img = cv2.resize(img, (w_down, h_down), interpolation=cv2.INTER_AREA)
img = cv2.GaussianBlur(
img,
(0, 0),
sigmaX=x_down_sigma,
sigmaY=y_down_sigma,
borderType=cv2.BORDER_REFLECT,
)
img = cv2.resize(img, (w, h), interpolation=cv2.INTER_LINEAR)

if x_up_sigma != 0 or y_up_sigma != 0:
# post blur to smooth out artifacts
img = cv2.GaussianBlur(
img,
(0, 0),
sigmaX=x_up_sigma,
sigmaY=y_up_sigma,
borderType=cv2.BORDER_REFLECT,
)

return img
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations

import cv2
import numpy as np

from nodes.impl.image_utils import fast_gaussian_blur
from nodes.properties.inputs import ImageInput, SliderInput
from nodes.properties.outputs import ImageOutput

Expand Down Expand Up @@ -46,5 +46,5 @@ def gaussian_blur_node(
) -> np.ndarray:
if sigma_x == 0 and sigma_y == 0:
return img
else:
return cv2.GaussianBlur(img, (0, 0), sigmaX=sigma_x, sigmaY=sigma_y)

return fast_gaussian_blur(img, sigma_x, sigma_y)
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations

import cv2
import numpy as np

from nodes.impl.image_utils import fast_gaussian_blur
from nodes.properties.inputs import ImageInput, SliderInput
from nodes.properties.outputs import ImageOutput

Expand Down Expand Up @@ -55,7 +55,7 @@ def high_pass_node(
if radius == 0 or contrast == 0:
img = img * 0 + 0.5
else:
img = contrast * (img - cv2.GaussianBlur(img, (0, 0), radius)) + 0.5
img = contrast * (img - fast_gaussian_blur(img, radius)) + 0.5 # type: ignore

if alpha is not None:
img = np.dstack((img, alpha))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import cv2
import numpy as np

from nodes.impl.image_utils import fast_gaussian_blur
from nodes.properties.inputs import ImageInput, SliderInput
from nodes.properties.outputs import ImageOutput

Expand Down Expand Up @@ -57,13 +58,13 @@ def unsharp_mask_node(
if radius == 0 or amount == 0:
return img

blurred = cv2.GaussianBlur(img, (0, 0), radius)
blurred = fast_gaussian_blur(img, radius)

threshold /= 100
if threshold == 0:
img = cv2.addWeighted(img, amount + 1, blurred, -amount, 0)
else:
diff = img - blurred
diff = img - blurred # type: ignore
diff = np.sign(diff) * np.maximum(0, np.abs(diff) - threshold)
img = img + diff * amount

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import numpy as np

import navi
from nodes.impl.image_utils import fast_gaussian_blur
from nodes.impl.normals.edge_filter import EdgeFilter, get_filter_kernels
from nodes.impl.normals.height import HeightSource, get_height_map
from nodes.properties.inputs import (
Expand Down Expand Up @@ -128,12 +129,10 @@ def normal_map_generator_node(

if blur_sharp < 0:
# blur
height = cv2.GaussianBlur(
height, (0, 0), sigmaX=-blur_sharp, sigmaY=-blur_sharp
)
height = fast_gaussian_blur(height, -blur_sharp)
elif blur_sharp > 0:
# sharpen
blurred = cv2.GaussianBlur(height, (0, 0), sigmaX=blur_sharp, sigmaY=blur_sharp)
blurred = fast_gaussian_blur(height, blur_sharp)
height = cv2.addWeighted(height, 2.0, blurred, -1.0, 0)

if min_z > 0:
Expand Down

0 comments on commit d6052dd

Please sign in to comment.