Open verhovsky opened 3 years ago
This is possible?
This is the short code to try.
#!/usr/bin/env python3
# coding: utf-8
# original code https://github.com/verhovsky/squircle/blob/master/squircle.py
import cv2
import math
import time
import numpy
_epsilon = 0.0000000001
def _sgn(x):
if x == 0.0:
return 0.0
if x < 0:
return -1.0
return 1.0
def _pixel_coordinates_to_unit(coordinate, max_value):
return coordinate / max_value * 2 - 1
def _one_coordinates_to_pixels(coordinate, max_value):
return (coordinate + 1) / 2 * max_value
def _stretch_square_to_disc(x, y):
if (abs(x) < _epsilon) or (abs(y) < _epsilon):
return x, y
x2 = x * x
y2 = y * y
hypotenuse_squared = x * x + y * y
reciprocal_hypotenuse = 1.0 / math.sqrt(hypotenuse_squared)
multiplier = 1.0
if x2 > y2:
multiplier = _sgn(x) * x * reciprocal_hypotenuse
else:
multiplier = _sgn(y) * y * reciprocal_hypotenuse
return x * multiplier, y * multiplier
def _transform(inp):
result = numpy.zeros_like(inp)
for x, row in enumerate(inp):
unit_x = _pixel_coordinates_to_unit(x, len(inp))
for y, _ in enumerate(row):
unit_y = _pixel_coordinates_to_unit(y, len(row))
try:
uv = _stretch_square_to_disc(unit_x, unit_y)
if uv is None:
continue
u, v = uv
u = _one_coordinates_to_pixels(u, len(inp))
v = _one_coordinates_to_pixels(v, len(row))
result[x][y] = inp[math.floor(u)][math.floor(v)]
except IndexError:
pass
return result
# -- load and test
img = cv2.imread('circle.png')
elapsed = round(time.time() * 1000)
squareImage = _transform(img[0:224, 0:224])
print(str(round(time.time() * 1000) - elapsed)+' ms to squareImage')
cv2.imshow('square', squareImage)
key = cv2.waitKey(0)
cv2.destroyAllWindows()
Andrej found one way, https://stackoverflow.com/questions/76838644/it-is-possible-convert-part-of-this-code-to-improve-speed 80x faster, very nice!
I needed a faster version of the elliptical grid mapping so I implemented it using numpy array math. It's a lot faster than looping through every pixel, and an advantage over the numba solution from stackoverflow is that it doesn't need to compile the function for the first use. The first 224x224 image takes 2-4 ms, and then around 0.6 ms for each additional image of the same size. I used three key methods to speed it up: 1 - replace all loops with matrix operations, 2 - cache and reuse the calculated mapping for additional images with the same size, 3 - only calculate forward mapping, and use it for reverse mapping as well. The basic implementation working on a numpy array is below:
def elliptical_transform(inp, mode):
size = inp.shape[0]
xx, yy = np.mgrid[-1 : 1 : size * 1j, -1 : 1 : size * 1j]
uu = xx * np.sqrt(1 - np.square(yy) / 2)
vv = yy * np.sqrt(1 - np.square(xx) / 2)
uu = _denormalize(uu)
vv = _denormalize(vv)
mapping = (uu, vv)
if mode == "square":
result = inp[mapping]
elif mode == "circle":
result = np.zeros_like(inp)
result[mapping] = inp
return result
def _denormalize(arr):
''' Go from (-1, 1) range to pixel coordinates '''
arr += 1
arr *= (arr.shape[0] - 1) / 2
arr = np.floor(arr).astype(int)
return arr
I will try to attach the complete file squircle_transforms.py with demonstrations. It also supports either PIL Image or numpy array as input, and an optional supersampling to get smoother output.
It currently takes a perceptible amount of time convert even a 400 by 400 image.
Some options are