verhovsky / squircle

Stretch circular images into squares and squish squares (or any square matrix) into circles.
MIT License
9 stars 0 forks source link

improve performance #4

Open verhovsky opened 3 years ago

verhovsky commented 3 years ago

It currently takes a perceptible amount of time convert even a 400 by 400 image.

Some options are

adbensi commented 1 year 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()
adbensi commented 1 year ago

Andrej found one way, https://stackoverflow.com/questions/76838644/it-is-possible-convert-part-of-this-code-to-improve-speed 80x faster, very nice!

manfred-lindmark commented 1 year ago

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. original to_square to_circle