4

I want to apply a pinch/bulge filter on an image using Python OpenCV. The result should be some kind of this example:

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

I've read the following stackoverflow post that should be the correct formula for the filter: Formulas for Barrel/Pincushion distortion

But I'm struggling to implement this in Python OpenCV.

I've read about maps to apply filter on an image: Distortion effect using OpenCv-python

As for my understanding, the code could look something like this:

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()

Is this the correct way to achieve the distortion presented in the example image? Any help regarding useful ressources or preferably examples are much appreciated.

Jeru Luke
  • 20,118
  • 13
  • 80
  • 87
Davi Jones
  • 123
  • 1
  • 6

2 Answers2

7

After familiarizing myself with the ImageMagick source code, I've found a way to apply the formula for distortion. With the help of the OpenCV remap function, this is a way to distort an image:

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()

This has the same effect as using the convert -implode function from ImageMagick.

Davi Jones
  • 123
  • 1
  • 6
  • I believe your formula is for implode (from ImageMagick). The barrel/pincushion equation is a polynomial in radius and is different from what you have coded. Nevertheless, great job and likely more what you wanted anyway. – fmw42 Sep 27 '20 at 17:51
  • @fm42 could you point me to the barrel/pincushion formula? – Davi Jones Sep 30 '20 at 05:02
  • See https://imagemagick.org/Usage/distorts/#barrel and in ImageMagick in distort.c at line 1309 – fmw42 Sep 30 '20 at 18:27
6

You can do that using implode and explode options in Python Wand, which uses ImageMagick.

Input:

enter image description here

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)

Implode:

enter image description here

Explode:

enter image description here

fmw42
  • 46,825
  • 10
  • 62
  • 80
  • I've also read about the ImageMagick filter,. but I wanted to avoid the extra dependencies. Still, legit solution for my problem. Thank you. – Davi Jones Sep 26 '20 at 07:40
  • Is there a way to specify a center point for the implode effect and not apply it to the center of the image? – Tina J Aug 29 '23 at 03:43
  • I am not sure I understand. You can make a mask of the region you want it to apply (white where you want it and black where you don't want it). Then process the whole image and use the mask to merge the processed and original image. Where the mask is white, use the processed image and where the mask is black use the original image. See np.where(mask=0, original, processed). Alternately, modify the equations in the pinch/bulge to use the mask to do the same. – fmw42 Aug 29 '23 at 16:21