Sunday, April 29, 2012

Fisheye lens equation - Simple fisheye effect with one function


I am interested in many kind of computer algorithms, including algorithms for computer graphics, digital image processing and 3D-graphics.

In one of my projects I needed to have "fish eye lens" like effect, and tried to find easy equation or ready function from the internet. To my surprise, there were not complete functions available, just theories and images how it can be achieved. 

Here is a brief tutorial and loop how to create fish eye -effect to any image. The function is written in Java, but as you can see, it is easily rewritten with any programming language. 

If you find this article and function useful and you will use it in your own projects, please refer where the source came. In any case, leave a comment. Questions and requests are welcome!

What is fish eye effect?

Fish eye effect is a mirror effect which happens when the image is drawn on to a surface of a sphere or a hemisphere. The surface does not need to be a perfect sphere, it can also be paraboloid or have other kind of curve as a surface.

Common effect is, that the image is "zoomed" in the center and "shrink" at the edges. The image is usually round (because we are dealing with spheres) but the image can also be clipped to be square.

Mathematics

What we need is a function which maps any pixel from the image to a surface of a sphere. 

In this fish eye effect, we are not using equations for a sphere, but an equation for unit circle:

x^2 + y^2 = 1 
Solving y
y^2 = 1 - x^2
and then finally
y = sqrt(1 - x^2) 

Because we are limiting all our values between 0.0 and 1.0, we can create values for nice arc when x goes from 0.0 to 1.0.


For example, in the image we have marked x-position 0.75, and we can calculate y-position substituting x in y = sqrt(1 - x^2), which gives us y = 0.66.

For our purpose, we will flip the curve with subtracting the result from 1.0:

d = 1 - sqrt(1 - x^2)

This curve will give us values for the difference of the distance of the source pixel and destination pixel, from the center of the image, when mapping from plane to sphere. 

The new distance is then the original distance added with the difference:

r' = r + d  (r' > r)

Polar coordinates

The normal screen coordinates (two dimensional Cartesian coordinates, simply x- and y-axis) are difficult when dealing with trigonometric functions, because the input values are typically between -1 to 1 and the angles are in radians from 0 to 2*PI.

Everything becomes more easier when you change the screen coordinates (x,y) to polar coordinates (r, theta). This is done with simple functions:

From Cartesian to polar:

    r = sqrt(y^2 + x^2)
    theta = atan2(y, x)
    
From polar to Cartesian:

    x = r cos(theta)
    y = r sin(theta)

In polar coordinates, any point in the image can be accessed with the angle (theta) and distance from center (r).

Now we are able to map any (x,y) pixel to polar coordinate (r,theta), then CHANGE the r, and map it back to (x',y'), and the pixel remains at the same line going through center of the image!

Algorithm 

What we need:
  1. the distance from center of the source image to any pixel (x,y) in the same image
  2. the position (x', y') where the pixel belongs in the fish eye image
In the following pseudo code, we are using polar coordinates. The key part in the whole process is to use polar coordinates and this fact: The actual pixel translation from 2D-image to sphere surface is done with manipulating the distance from center (r).

Pseudo code:

for each pixel (x,y)
    normalize (x,y) to (nx, ny) to be in range [-1,1]
    calculate distance from (nx, ny) to center (0,0)
    convert (nx,ny) to polar coordinates
    calculate new distance from center on the sphere surface
        The new distance is r' = r + (1 - sqrt(1 -r^2)) / 2
    translate (nx, ny) back to screen coordinates (x',y')


The image above shows how Points P1 and P2 will be displaced to P1' and P2', so that the angle remains the same. In other words, the point is displaced along the original line towards the edge of the unit circle. The distance from the center tells us how big is the displacement. In the center and near the center the displacement value is zero or almost zero. Near the edges displacement value grows towards value of 1.0.


The image above gives the idea behind the displacement value. For every pixel in the result image plane, there is a pixel from source image plane. In here, the source image plane is stretched and curved. It may be easier to understand, if you think a one single line instead of the plane. Along that line, the pixels are almost normal at the center, but towards the ends, the pixels are fetched further and further away from the center.

Most important thing


The most important thing is to understand, that for each result pixel, there is a source pixel, on the same line going through the center, but with a greater distance.

The source 

public static int[] fisheye(int[] srcpixels, double w, double h) {

    /*
     *    Fish eye effect
     *    tejopa, 2012-04-29
     *    http://popscan.blogspot.com
     *    http://www.eemeli.de
     */

    // create the result data
    int[] dstpixels = new int[(int)(w*h)];            
    // for each row
    for (int y=0;y<h;y++) {                                
        // normalize y coordinate to -1 ... 1
        double ny = ((2*y)/h)-1;                        
        // pre calculate ny*ny
        double ny2 = ny*ny;                                
        // for each column
        for (int x=0;x<w;x++) {                            
            // normalize x coordinate to -1 ... 1
            double nx = ((2*x)/w)-1;                    
            // pre calculate nx*nx
            double nx2 = nx*nx;
            // calculate distance from center (0,0)
            // this will include circle or ellipse shape portion
            // of the image, depending on image dimensions
            // you can experiment with images with different dimensions
            double r = Math.sqrt(nx2+ny2);                
            // discard pixels outside from circle!
            if (0.0<=r&&r<=1.0) {                            
                double nr = Math.sqrt(1.0-r*r);            
                // new distance is between 0 ... 1
                nr = (r + (1.0-nr)) / 2.0;
                // discard radius greater than 1.0
                if (nr<=1.0) {
                    // calculate the angle for polar coordinates
                    double theta = Math.atan2(ny,nx);         
                    // calculate new x position with new distance in same angle
                    double nxn = nr*Math.cos(theta);        
                    // calculate new y position with new distance in same angle
                    double nyn = nr*Math.sin(theta);        
                    // map from -1 ... 1 to image coordinates
                    int x2 = (int)(((nxn+1)*w)/2.0);        
                    // map from -1 ... 1 to image coordinates
                    int y2 = (int)(((nyn+1)*h)/2.0);        
                    // find (x2,y2) position from source pixels
                    int srcpos = (int)(y2*w+x2);            
                    // make sure that position stays within arrays
                    if (srcpos>=0 & srcpos < w*h) {
                        // get new pixel (x2,y2) and put it to target array at (x,y)
                        dstpixels[(int)(y*w+x)] = srcpixels[srcpos];    
                    }
                }
            }
        }
    }
    //return result pixels
    return dstpixels;

Example pictures 

Original

After applying fish eye function

 

10 comments:

Anonymous said...

Hi there,

I have to built a project with a fisheye lense (a specific amount of pixels around the coordinates where I clicked)

Everything works but the fisheye.

If I use your code it only zooms but there is no fisheye

Jussi said...

Hi,

Have you copied the code with copy and paste? It should produce the effect as in the sample pictures.

If you have not, make sure you are using double (or float) values in cos, sin and atan functions.

You could also check that the line:

nr = (r + (1.0-nr)) / 2.0;

really produces values from 0 to 1.

Anonymous said...

Nice stuff.
Just translated it to Just Basic.
http://justbasic.conforums.com/index.cgi?action=display&board=games&num=1342726626&start=0#1342726626
Though I translated pseudocode - so I bumped in line
"The new distance is r' = r + (1 - sqrt(1 -r^2))", and this line apparently missed "/2" factor.

Jussi said...

Yes! Thank you! :)

Anonymous said...

This is a great article. I've been scouring the internet for information about fisheye transformations and this is the first I found where I totally understood everything because you explained it in such a clear way. I'm just sad that you haven't got further articles on the subject of 3D graphics. What I was really looking for was an explanation of transforming to/from a paraboloid rather than a sphere but this at least helps to point me in the right direction.

T.Natraj said...

how can i get the fisheye effect at a
specific pixel?


Now in this code we are getting fisheye effect at center only...

can u help me...?

Anonymous said...

Great tutorial, I could not find anything similar on the internet. In your source code you have: r' = (r + (1 - sqrt(1 - r*r)) / 2. And in the pseudo code explantation you have: r = r + (1 - sqrt(1 - r*r)) / 2. Which one is correct? Also, could you explain where the divide by two comes from? Thanks again!

Alex said...

Hi, I'm trying to get this code to work in my project, but I can't figure out why it doesn't work. I put it in the act method, but nothing happens. I'm new to coding, sorry if the answer is obvious. Thanks!

Unknown said...

Hi !

I'd like to create this effect but on a "square lens" effect. Could you help me convert the sphere conversion into a squary shape ? thanks !

Tin Man said...

This is for setting destination = source pixel.
What if I want to do the reverse...like knowing the source how do i calculate destination.
I ask this because let's say i have a grid with NxN or NxM sections, and i want to select each small square/rectangle of the source and perspective it into destination (sphere) by specifying top-left, top-right, bottom-left, bottom-right of the new destination shape.
Please let me know how i can do this.