使用 Python OpenCV 的收缩/凸起失真

问题描述 投票:0回答:3

我想使用 Python OpenCV 在图像上应用收缩/凸出滤镜。结果应该是这个例子的某种形式:

https://pixijs.io/pixi-filters/tools/screenshots/dist/bulge-pinch.gif

我已阅读以下 stackoverflow 帖子,它应该是滤波器的正确公式:桶形/枕形失真的公式

但我正在努力在 Python OpenCV 中实现这一点。

我读过有关在图像上应用滤镜的地图:使用 OpenCv-python 的扭曲效果

根据我的理解,代码可能看起来像这样:

import numpy as np
import cv2 as cv

f_img = 'example.jpg'
im_cv = cv.imread(f_img)

# grab the dimensions of the image
(h, w, _) = im_cv.shape

# set up the x and y maps as float32
flex_x = np.zeros((h, w), np.float32)
flex_y = np.zeros((h, w), np.float32)

# create map with the barrel pincushion distortion formula
for y in range(h):
    for x in range(w):
        flex_x[y, x] = APPLY FORMULA TO X
        flex_y[y, x] = APPLY FORMULA TO Y

# do the remap  this is where the magic happens
dst = cv.remap(im_cv, flex_x, flex_y, cv.INTER_LINEAR)

cv.imshow('src', im_cv)
cv.imshow('dst', dst)

cv.waitKey(0)
cv.destroyAllWindows()

这是实现示例图像中呈现的失真的正确方法吗?非常感谢有关有用资源或最好的示例的任何帮助。

python opencv image-processing filter distortion
3个回答
8
投票

熟悉 ImageMagick 源代码后,我找到了一种应用失真公式的方法。借助 OpenCV remap 函数,这是一种扭曲图像的方法:

import numpy as np
import cv2 as cv

f_img = 'example.jpg'
im_cv = cv.imread(f_img)

# grab the dimensions of the image
(h, w, _) = im_cv.shape

# set up the x and y maps as float32
flex_x = np.zeros((h, w), np.float32)
flex_y = np.zeros((h, w), np.float32)

# create map with the barrel pincushion distortion formula
for y in range(h):
    delta_y = scale_y * (y - center_y)
    for x in range(w):
        # determine if pixel is within an ellipse
        delta_x = scale_x * (x - center_x)
        distance = delta_x * delta_x + delta_y * delta_y
        if distance >= (radius * radius):
            flex_x[y, x] = x
            flex_y[y, x] = y
        else:
            factor = 1.0
            if distance > 0.0:
                factor = math.pow(math.sin(math.pi * math.sqrt(distance) / radius / 2), -amount)
            flex_x[y, x] = factor * delta_x / scale_x + center_x
            flex_y[y, x] = factor * delta_y / scale_y + center_y

# do the remap  this is where the magic happens
dst = cv.remap(im_cv, flex_x, flex_y, cv.INTER_LINEAR)

cv.imshow('src', im_cv)
cv.imshow('dst', dst)

cv.waitKey(0)
cv.destroyAllWindows()

这与使用 ImageMagick 中的 convert -implode 函数具有相同的效果。


7
投票

您可以使用 Python Wand 中的内爆和爆炸选项来做到这一点,Python Wand 使用 ImageMagick。

输入:

from wand.image import Image
import numpy as np
import cv2

with Image(filename='zelda1.jpg') as img:
    img.virtual_pixel = 'black'
    img.implode(0.5)
    img.save(filename='zelda1_implode.jpg')
    # convert to opencv/numpy array format
    img_implode_opencv = np.array(img)
    img_implode_opencv = cv2.cvtColor(img_implode_opencv, cv2.COLOR_RGB2BGR)

with Image(filename='zelda1.jpg') as img:
    img.virtual_pixel = 'black'
    img.implode(-0.5 )
    img.save(filename='zelda1_explode.jpg')
    # convert to opencv/numpy array format
    img_explode_opencv = np.array(img)
    img_explode_opencv = cv2.cvtColor(img_explode_opencv, cv2.COLOR_RGB2BGR)

# display result with opencv
cv2.imshow("IMPLODE", img_implode_opencv)
cv2.imshow("EXPLODE", img_explode_opencv)
cv2.waitKey(0)

内爆:

爆炸:


0
投票

Bulge 的纯 Python 实现

对于那些寻找纯Python和带有矢量化的凸出效果优化版本的人。在这里。

我使用

numpy
重新实现了通过向量/矩阵运算生成映射矩阵的部分,并将它们包装在一个方便的
BulgeEffect
类中。我还提供了一个演示程序以供快速测试。

该效果的关键部分是用于计算效果圆内像素位移的数学函数。不知何故,我无法让 @Davi Jones 在他的答案中使用的公式起作用。因此,我使用自己的略有不同的函数:

scale * sin(pi * x) ^ amount
。您更改此表达式即可获得不同的失真效果。

代码可能设计得不好,而且我不是Pythonic(其中某些部分可能有更多Pythonic版本。)。因此,它有待改进。

demo

如何使用

要应用效果,您必须创建

BulgeEffect
类的实例,将图像设置为 image 属性,设置选项(中心、效果半径、扭曲量等),调用 apply 方法,然后获取从图像属性返回的结果。

我不喜欢这个工作流程。这对我来说很丑,但我最终无法得到一个更优雅的版本。

演示中的示例:

bulge_effect.image = img_copy
bulge_effect.set_options(BulgeOptions(x, y, 100, quality=BulgeQuality.LOW))
bulge_effect.apply()
img_copy = bulge_effect.image

内爆

要使图像内爆,您可以给出正的比例值。默认比例值为

-0.45

不同的质量模式

有 4 种质量模式完全映射到 opencv 的插值模式。

import cv2
import numpy as np
from enum import IntEnum

class BulgeQuality(IntEnum):
    LOW = 0
    NORMAL = 1
    HIGH = 2
    HIGHEST = 3

class BulgeOptions:
    def __init__(self, x, y, radius, scale=-0.45, amount=4, quality=BulgeQuality.NORMAL) -> None:
        self.center = np.array([x, y], np.float32)
        self.radius = radius
        self.scale = scale
        self.amount = amount
        self.quality = quality

class BulgeEffect:
    INTERP_TYPES = [cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_LANCZOS4]
    def __init__(self, img, options=None): 
        self._map_x = None
        self._map_y = None
        self._img = img
        self._create_map()
        if options is None:
            options = BulgeOptions(img.shape[1] // 2, img.shape[0] // 2, 50)
        self._options = options
        self._build_map()
    
    @property
    def amount(self):
        return self._options.amount

    @property
    def center(self):
        return self._options.center
    
    @property
    def image(self):
        return self._img
    
    @property
    def quality(self):
        return self._options.quality
    
    @property
    def radius(self):
        return self._options.radius
    
    @property
    def scale(self):
        return self._options.scale

    @image.setter
    def image(self, image, image_changed=False):
        self._img = image
        if image_changed:
            self._create_map()

    def set_options(self, options):
        self._options = options
        self._create_map()
        self._build_map()

    def apply(self):
        self._img = cv2.remap(self._img, self._map_x, self._map_y, self._get_interp_from_quality(self.quality))
 
    def _create_map(self):
        W = self._img.shape[1]
        H = self._img.shape[0]
        self._map_y, self._map_x = np.mgrid[:H, :W].astype(np.float32)

    def _build_map(self):
        dv_x = self._map_x - self._options.center[0]
        dv_y = self._map_y - self._options.center[1]
        circle_distances = np.sqrt(dv_x ** 2 + dv_y ** 2)
        circle_indices = circle_distances <= self._options.radius 
        t = circle_distances / self._options.radius 
        s = -0.45 * (np.sin(np.pi * t)) ** 4 
        self._map_x[circle_indices] = self._map_x[circle_indices] + np.multiply(dv_x[circle_indices], s[circle_indices])
        self._map_y[circle_indices] = self._map_y[circle_indices] + np.multiply(dv_y[circle_indices], s[circle_indices])

    def _get_interp_from_quality(self, quality):
        return BulgeEffect.INTERP_TYPES[quality]
     
img = None
img_copy = None
bulge_effect = None

def on_mouse_move(x, y, button): 
    global img_copy
    if img is None:
        return
    np.copyto(img_copy, img) 
    bulge_effect.image = img_copy
    bulge_effect.set_options(BulgeOptions(x, y, 100, quality=BulgeQuality.LOW))
    bulge_effect.apply()
    img_copy = bulge_effect.image
    cv2.imshow("buldge", img_copy) 

def get_button(flags):
    if flags & cv2.EVENT_FLAG_LBUTTON:
        return 'left'
    if flags & cv2.EVENT_FLAG_RBUTTON:
        return 'right'
    if flags & cv2.EVENT_FLAG_MBUTTON:
        return 'middle'
    return 'none'

def mouse_event_handler(event, x, y, flags, param):
    button_down = [cv2.EVENT_LBUTTONDOWN, cv2.EVENT_RBUTTONDOWN, cv2.EVENT_MBUTTONDOWN]
    button_up = [cv2.EVENT_LBUTTONUP, cv2.EVENT_RBUTTONUP, cv2.EVENT_MBUTTONUP]
    button = get_button(flags) 
    if event == cv2.EVENT_MOUSEMOVE:
        on_mouse_move(x, y, button)


if __name__ == "__main__": 
    img = cv2.imread('test2.png')
    img_copy = img.copy()
    bulge_effect = BulgeEffect(img_copy)
    cv2.imshow('buldge', img_copy)
    cv2.setMouseCallback('buldge', mouse_event_handler)
    
    cv2.waitKey(0)
© www.soinside.com 2019 - 2024. All rights reserved.