简体   繁体   English

如何改进 Python OpenCV 中的可变镜头模糊算法?

[英]How to improve a variable lens blur algorithm in Python OpenCV?

I want to emulate the blur of a cheap camera lens (like Holga ).我想模仿廉价相机镜头(如Holga )的模糊效果。
Blur is very weak close to the photo center.靠近照片中心的模糊非常弱。
And it's getting more decisive close to corners.它在靠近弯道时变得更加果断。

I wrote the code and it works in general.我编写了代码,它通常可以工作。

Input image:输入图像:

输入图像

Result image:结果图片:

结果图片 . .

But I feel that it could be done better and faster.但我觉得它可以做得更好更快。
I've found a similar question but it still has no answer.我发现了一个类似的问题,但仍然没有答案。

How to improve an algorithm speed and avoid iteration over pixels?如何提高算法速度并避免像素迭代?

UPDATE:更新:
It's not the same as standard Gaussian or 2D filter blur with constant kernel size.它与具有恒定内核大小的标准高斯或 2D 过滤器模糊不同。

import cv2
import numpy as np
import requests
from tqdm import tqdm
import warnings
warnings.filterwarnings("ignore")

def blur(img=None, blur_radius=None, test=False):
    # test image loading
    if img is None:
        test=True
        print('test mode ON')
        print('loading image...')
        url = r'http://www.lenna.org/lena_std.tif'
        resp = requests.get(url, stream=True).raw
        img = np.asarray(bytearray(resp.read()), dtype="uint8")
        img = cv2.imdecode(img, cv2.IMREAD_COLOR)
        cv2.imwrite('img_input.png', img)
        print('image loaded')

    # channels splitting
    img_lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
    l, a, b = cv2.split(img_lab)
    if test:
        cv2.imwrite('l_channel.png', l)
        print('l channel saved')

    # make blur map 
    height, width = l.shape[:2]
    center = np.array([height/2, width/2])
    diag = ((height / 2) ** 2 + (width / 2) ** 2) ** 0.5
    blur_map = np.linalg.norm(
        np.indices(img.shape[:2]) - center[:,None,None] + 0.5,
        axis = 0
    )

    if blur_radius is None:
        blur_radius = int(max(height, width) * 0.03)

    blur_map = blur_map / diag 
    blur_map = blur_map * blur_radius
    if test:
        blur_map_norm = cv2.normalize(blur_map, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_32F)
        cv2.imwrite('blur_map.png', blur_map_norm)
        print('blur map saved')

    # very inefficient blur algorithm!!!
    l_blur = np.copy(l)
    for x in tqdm(range(width)):
        for y in range(height):
            kernel_size = int(blur_map[y, x])
       
            if kernel_size == 0:
                l_blur[y, x] = l[y, x]
                continue
            
            kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
            cut = l[
                max(0, y - kernel_size):min(height, y + kernel_size),
                max(0, x - kernel_size):min(width, x + kernel_size)
            ]

            if cut.shape == kernel.shape:
                cut = (cut * kernel).mean()
            else:
                cut = cut.mean()

            l_blur[y, x] = cut
    if test: cv2.imwrite('l_blur.png', l_blur); print('l_blur saved')
    if test: print('done')
    return l_blur

blur() 

The only way to implement a filter where the kernel is different for every pixel is to create the kernel for each pixel and apply it in a loop, like OP's code does.实现每个像素的内核不同的过滤器的唯一方法是为每个像素创建内核并在循环中应用它,就像 OP 的代码一样。 The Fourier transform does not apply to this case.傅里叶变换不适用于这种情况。 Python is a very slow language, the same algorithm implemented in a compiled language would be much faster. Python 是一种非常慢的语言,用编译语言实现的相同算法会快得多。 Unless there is some predefined structure in how the kernel is created at each pixel, there is no way to reduce the complexity of the algorithm.除非在每个像素处创建内核的方式中有一些预定义的结构,否则无法降低算法的复杂性。

For example, the uniform filter with a square kernel (commonly called the "box" filter) can be computed based on the integral image, using only 4 additions per pixel.例如,可以基于积分图像计算具有方形内核的均匀滤波器(通常称为“盒”滤波器),每个像素仅使用 4 次加法。 This implementation should be able to choose a different kernel size at each pixel without any additional cost.此实现应该能够在每个像素处选择不同的内核大小,而无需任何额外成本。

DIPlib has an implementation of an adaptive Gaussian filter [disclaimer: I'm an author of DIPlib, but I did not implement this functionality]. DIPlib 有一个自适应高斯滤波器的实现[免责声明:我是 DIPlib 的作者,但我没有实现这个功能]。 Here is the documentation . 这是文档 This filter applies a Gaussian filter, but the Gaussian kernel is scaled and rotated differently at every pixel.此过滤器应用高斯过滤器,但高斯核在每个像素处的缩放和旋转方式不同。

Lens blur is not a Gaussian, but it's not easy to see the difference by eye in most cases;镜头模糊不是高斯模糊,但在大多数情况下很难用肉眼看出差异; the difference matters only if there is a very small dot with high contrast.只有当有一个非常小的点具有高对比度时,差异才重要。

OP's case would be implemented as follows: OP的案例将执行如下:

import diplib as dip

img = dip.ImageRead('examples/trui.ics')

blur_map = dip.CreateRadiusSquareCoordinate(img.Sizes())
blur_map /= dip.Maximum(blur_map)

img_blur = dip.AdaptiveGauss(img, [0, blur_map], sigmas=[5])

(the blur_map here is defined differently, I chose a quadratic function of the distance to the center, because I think it looks really nice; use dip.CreateRadiusCoordinate() to reproduce OP's map). (这里的blur_map定义不同,我选择了到中心距离的二次函数,因为我觉得它看起来真的很好;使用dip.CreateRadiusCoordinate()来重现 OP 的地图)。

输入图像 输出图像

I've chosen a maximum blur of 5 (this is the sigma, in pixels, of the Gaussian, not the footprint of the kernel), and blur_map here scales this sigma with a factor between 0 in the middle and 1 at the corners of the image.我选择了 5 的最大模糊(这是高斯的 sigma,以像素为单位,而不是内核的足迹),并且blur_map在这里用中间的 0 和角落的 1 之间的因子缩放这个 sigma图片。

Another interesting effect would be as follows, with increasing blur tangential to each circle centered in the middle of the image, with very little blur radially:另一个有趣的效果如下,随着图像中间每个圆的切向模糊增加,径向模糊很少:

angle_map = dip.CreatePhiCoordinate(img.Sizes())
img_blur = dip.AdaptiveGauss(img, [angle_map, blur_map], sigmas=[8,1])

输出图像

Here is one way to apply (uniform, non-varying) lens defocus blur in Python/OpenCV by transforming both the image and filter to the Fourier (frequency) domain.这是在 Python/OpenCV 中应用(均匀、不变)镜头散焦模糊的一种方法,方法是将图像和滤波器都转换到傅立叶(频率)域。

  • Read the input读取输入
  • Take dft of input to transform to Fourier domain将输入的 dft 转换为傅立叶域
  • Draw a white filled circle on a black background the size of the input as a mask (filter kernel).在黑色背景上绘制一个白色实心圆圈,输入大小作为掩码(过滤器内核)。 This is the defocus kernel in the spatial domain, ie a circular rect function.这是空间域中的散焦核,即圆形矩形函数。
  • Blur the circle slightly to anti-alias the edge稍微模糊圆圈以消除边缘的锯齿
  • Roll the mask so that the center is at the origin (top left corner) and normalize so that the sum of values = 1滚动蒙版,使中心位于原点(左上角)并标准化,使值的总和 = 1
  • Take dft of mask to transform to Fourier domain where its amplitude profile is a jinx function.将掩码的 dft 转换为傅立叶域,其中其幅度分布是一个 jinx 函数。
  • Multiply the two dft images to apply the blur将两个 dft 图像相乘以应用模糊
  • Take the idft of the product to transform back to spatial domain取乘积的 idft 变换回空间域
  • Get the magnitude of the real and imaginary components of the product, clip and convert to uint8 as the result获取乘积的实部和虚部的大小,裁剪并转换为uint8作为结果
  • Save the result保存结果

Input:输入:

在此处输入图像描述

import numpy as np
import cv2

# read input and convert to grayscale
img = cv2.imread('lena_512_gray.png', cv2.IMREAD_GRAYSCALE)

# do dft saving as complex output
dft_img = np.fft.fft2(img, axes=(0,1))

# create circle mask
radius = 32
mask = np.zeros_like(img)
cy = mask.shape[0] // 2
cx = mask.shape[1] // 2
cv2.circle(mask, (cx,cy), radius, 255, -1)[0]

# blur the mask slightly to antialias
mask = cv2.GaussianBlur(mask, (3,3), 0)

# roll the mask so that center is at origin and normalize to sum=1
mask_roll = np.roll(mask, (256,256), axis=(0,1))
mask_norm = mask_roll / mask_roll.sum() 

# take dft of mask
dft_mask_norm = np.fft.fft2(mask_norm, axes=(0,1))

# apply dft_mask to dft_img
dft_shift_product = np.multiply(dft_img, dft_mask_norm)

# do idft saving as complex output
img_filtered = np.fft.ifft2(dft_shift_product, axes=(0,1))

# combine complex real and imaginary components to form (the magnitude for) the original image again
img_filtered = np.abs(img_filtered).clip(0,255).astype(np.uint8)

cv2.imshow("ORIGINAL", img)
cv2.imshow("MASK", mask)
cv2.imshow("FILTERED DFT/IFT ROUND TRIP", img_filtered)
cv2.waitKey(0)
cv2.destroyAllWindows()

# write result to disk
cv2.imwrite("lena_512_gray_mask.png", mask)
cv2.imwrite("lena_dft_numpy_lowpass_filtered_rad32.jpg", img_filtered)

Mask - Filter Kernel In Spatial Domain:掩码 - 空间域中的过滤器内核:

在此处输入图像描述

Result for Circle Radius=4:圆半径 = 4 的结果:

在此处输入图像描述

Result for Circle Radius=8:圆半径 = 8 的结果:

在此处输入图像描述

Result for Circle Radius=16:圆半径 = 16 的结果:

在此处输入图像描述

Result for Circle Radius=32圆半径=32 的结果

在此处输入图像描述 :

ADDITION添加

Using OpenCV for the dft, etc rather than Numpy, the above becomes:将 OpenCV 用于 dft 等而不是 Numpy,以上变为:

import numpy as np
import cv2

# read input and convert to grayscale
img = cv2.imread('lena_512_gray.png', cv2.IMREAD_GRAYSCALE)

# do dft saving as complex output
dft_img = cv2.dft(np.float32(img), flags = cv2.DFT_COMPLEX_OUTPUT)

# create circle mask
radius = 32
mask = np.zeros_like(img)
cy = mask.shape[0] // 2
cx = mask.shape[1] // 2
cv2.circle(mask, (cx,cy), radius, 255, -1)[0]

# blur the mask slightly to antialias
mask = cv2.GaussianBlur(mask, (3,3), 0)

# roll the mask so that center is at origin and normalize to sum=1
mask_roll = np.roll(mask, (256,256), axis=(0,1))
mask_norm = mask_roll / mask_roll.sum() 

# take dft of mask
dft_mask_norm = cv2.dft(np.float32(mask_norm), flags = cv2.DFT_COMPLEX_OUTPUT)

# apply dft_mask to dft_img
dft_product = cv2.mulSpectrums(dft_img, dft_mask_norm, 0)

# do idft saving as complex output, then clip and convert to uint8
img_filtered = cv2.idft(dft_product, flags=cv2.DFT_SCALE+cv2.DFT_REAL_OUTPUT)
img_filtered = img_filtered.clip(0,255).astype(np.uint8)

cv2.imshow("ORIGINAL", img)
cv2.imshow("MASK", mask)
cv2.imshow("FILTERED DFT/IFT ROUND TRIP", img_filtered)
cv2.waitKey(0)
cv2.destroyAllWindows()

# write result to disk
cv2.imwrite("lena_512_gray_mask.png", mask)
cv2.imwrite("lena_dft_opencv_defocus_rad32.jpg", img_filtered)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM