Apply Polar Coordinates to UIImage

635 views Asked by At

I want to convert regular panoramic photos to polar coordinates to create a "tiny earth" effect but i cannot figure out how to solve this. I would assume there is some helpful Core Graphics filter or third party library but i cannot find any.

Example:

Tiny Earth Panorama

1

There are 1 answers

5
BConic On BEST ANSWER

This is actually quite simple, you just have to apply polar coordinates. Here is a fully commented example (implemented in C++ and using OpenCV only for data structures and image loading and display):

#include <opencv2/highgui.hpp>

// Function returning the bilinear interpolation of the input image at input coordinates
cv::Vec3b interpolate(const cv::Mat &image, float x, float y)
{
    // Compute bilinear interpolation weights
    float floorx=std::floor(x), floory=std::floor(y);
    float fracx=x-floorx, fracy=y-floory;
    float w00=(1-fracy)*(1-fracx), w01=(1-fracy)*fracx, w10=fracy*(1-fracx), w11=fracy*fracx;

    // Read the input image values at the 4 pixels surrounding the floating point (x,y) coordinates
    cv::Vec3b val00 = image.at<cv::Vec3b>(floory, floorx);
    cv::Vec3b val01 = (floorx<image.cols-1 ? image.at<cv::Vec3b>(floory, floorx+1) : image.at<cv::Vec3b>(floory, 0));       // Enable interpolation between the last right-most and left-most columns
    cv::Vec3b val10 = image.at<cv::Vec3b>(floory+1, floorx);
    cv::Vec3b val11 = (floorx<image.cols-1 ? image.at<cv::Vec3b>(floory+1, floorx+1) : image.at<cv::Vec3b>(floory+1, 0));   // Enable interpolation between the last right-most and left-most columns

    // Compute the interpolated color
    cv::Vec3b val_interp;
    val_interp.val[0] = cv::saturate_cast<uchar>(val00.val[0]*w00+val01.val[0]*w01+val10.val[0]*w10+val11.val[0]*w11);
    val_interp.val[1] = cv::saturate_cast<uchar>(val00.val[1]*w00+val01.val[1]*w01+val10.val[1]*w10+val11.val[1]*w11);
    val_interp.val[2] = cv::saturate_cast<uchar>(val00.val[2]*w00+val01.val[2]*w01+val10.val[2]*w10+val11.val[2]*w11);
    return val_interp;
}

// Main function
void main()
{
    const float pi = 3.1415926535897932384626433832795;

    // Load and display color panorama image
    cv::Mat panorama = cv::imread("../panorama_sd.jpg", cv::IMREAD_COLOR);
    cv::namedWindow("Panorama");
    cv::imshow("Panorama", panorama);

    // Infer the size of the final image from the dimensions of the panorama
    cv::Size result_size(panorama.rows*2, panorama.rows*2);
    float ctrx=result_size.width/2, ctry=result_size.height/2;

    // Initialize an image with black background, with inferred dimensions and same color format as input panorama
    cv::Mat tiny_earth_img = cv::Mat::zeros(result_size, panorama.type());
    cv::Vec3b *pbuffer_img = tiny_earth_img.ptr<cv::Vec3b>();   // Get a pointer to the buffer of the image (sequence of 8-bit interleaved BGR values)

    // Generate the TinyEarth image by looping over all its pixels
    for(int y=0; y<result_size.height; ++y) {
        for(int x=0; x<result_size.width; ++x, ++pbuffer_img) {

            // Compute the polar coordinates associated with the current (x,y) point in the final image
            float dx=x-ctrx, dy=y-ctry;
            float radius = std::sqrt(dx*dx+dy*dy);
            float angle = std::atan2(dy,dx)/(2*pi); // Result in [-0.5, 0.5]
            angle = (angle<0 ? angle+1 : angle);    // Result in [0,1[

            // Map the polar coordinates to cartesian coordinates in the panorama image
            float panx = panorama.cols*angle;
            float pany = panorama.rows-1-radius;    // We want the bottom of the panorama to be at the center

            // Ignore pixels which cannot be linearly interpolated in the panorama image
            if(std::floor(panx)<0 || std::floor(panx)+1>panorama.cols || std::floor(pany)<0 || std::floor(pany)+1>panorama.rows-1)
                continue;

            // Interpolate the panorama image at coordinates (panx, pany), and store this value in the final image
            pbuffer_img[0] = interpolate(panorama, panx, pany);
        }
    }

    // Display the final image
    cv::imwrite("../tinyearth.jpg", tiny_earth_img);
    cv::namedWindow("TinyEarth");
    cv::imshow("TinyEarth", tiny_earth_img);
    cv::waitKey();
}

Sample input panorama (source):

enter image description here

Resulting image:

enter image description here

EDIT:

To answer your remark about the black borders, you can adjust the mapping function (which maps pixel coordinates in the final image to pixel coordinates in the panorama image) to achieve what you want to do. Here are some examples:

Source panorama:

enter image description here

1) Original mapping: pixels with radius>panorama.rows/2 are left un-touched (hence you can have whatever background image show up there)

float panx = panorama.cols*angle;
float pany = panorama.rows-1-radius;

Result:

enter image description here

2) Closest-point mapping: pixels with radius>panorama.rows/2 are mapped to the closest valid pixel in the panorama.

float panx = panorama.cols*angle;
float pany = std::max(0.f,panorama.rows-1-radius);

Result:

enter image description here

3) Zoomed-in mapping: the tiny-earth image is zoomed in so that pixels with radius>panorama.rows/2 are mapped to valid panorama pixels, however some parts of the panorama are now mapped outside the tiny-earth image (at the top/bottom/left/right)

float panx = panorama.cols*angle;
float pany = panorama.rows-1-0.70710678118654752440084436210485*radius;

Result:

enter image description here

4) Logarithmic mapping: a non-linear mapping involving the log function is used to minimize the areas of the panorama which are mapped outside the tiny-earth image (you may adjust the 100 constant to scale more or less).

const float scale_cst = 100;
float panx = panorama.cols*angle;
float pany = (panorama.rows-1)*(1-std::log(1+scale_cst*0.70710678118654752440084436210485*radius/panorama.rows)/std::log(1+scale_cst));

Result:

enter image description here