Wrap image around a circle Wrap image around a circle python-3.x python-3.x

Wrap image around a circle


You are having problems filling up your circle because you are approaching this from the wrong way – quite literally.

When mapping from a source to a target, you need to fill your target, and map each translated pixel from this into the source image. Then, there is no chance at all you miss a pixel, and, equally, you will never draw (nor lookup) a pixel more than once.

The following is a bit rough-and-ready, it only serves as a concept example. I first wrote some code to draw a filled circle, top to bottom. Then I added some more code to remove the center part (and added a variable Ri, for "inner radius"). This leads to a solid ring, where all pixels are only drawn once: top to bottom, left to right.

a solid ring

How you exactly draw the ring is not actually important! I used trig at first because I thought of re-using the angle bit, but it can be done with Pythagorus' as well, and even with Bresenham's circle routine. All you need to keep in mind is that you iterate over the target rows and columns, not the source. This provides actual x,y coordinates that you can feed into the remapping procedure.

With the above done and working, I wrote the trig functions to translate from the coordinates I would put a pixel at into the original image. For this, I created a test image containing some text:

test image with text

and a good thing that was, too, as in the first attempt I got the text twice (once left, once right) and mirrored – that needed a few minor tweaks. Also note the background grid. I added that to check if the 'top' and 'bottom' lines – the outermost and innermost circles – got drawn correctly.

Running my code with this image and Ro,Ri at 100 and 50, I get this result:

round test image

You can see that the trig functions make it start at the rightmost point, move clockwise, and have the top of the image pointing outwards. All can be trivially adjusted, but this way it mimics the orientation that you want your image drawn.

This is the result with your iris-image, using 33 for the inner radius:

iris to eye mapping

and here is a nice animation, showing the stability of the mapping:

aww bright light ahead!

Finally, then, my code is:

import math as mfrom PIL import ImageRo = 100.0Ri = 50.0# img = [[1 for x in range(int(width))] for y in range(int(height))]cir = [[0 for x in range(int(Ro * 2))] for y in range(int(Ro * 2))]# image = Image.open('0vWEI.png')image = Image.open('this-is-a-test.png')# data = image.convert('RGB')pixels = image.load()width, height = image.sizedef shom_im(img):  # for showing data as image    list_image = [item for sublist in img for item in sublist]    new_image = Image.new("RGB", (len(img[0]), len(img)))    new_image.putdata(list_image)    new_image.save("result1.png","PNG")    new_image.show()for i in range(int(Ro)):    # outer_radius = Ro*m.cos(m.asin(i/Ro))    outer_radius = m.sqrt(Ro*Ro - i*i)    for j in range(-int(outer_radius),int(outer_radius)):        if i < Ri:            # inner_radius = Ri*m.cos(m.asin(i/Ri))            inner_radius = m.sqrt(Ri*Ri - i*i)        else:            inner_radius = -1        if j < -inner_radius or j > inner_radius:            # this is the destination            # solid:            # cir[int(Ro-i)][int(Ro+j)] = (255,255,255)            # cir[int(Ro+i)][int(Ro+j)] = (255,255,255)            # textured:            x = Ro+j            y = Ro-i            # calculate source            angle = m.atan2(y-Ro,x-Ro)/2            distance = m.sqrt((y-Ro)*(y-Ro) + (x-Ro)*(x-Ro))            distance = m.floor((distance-Ri+1)*(height-1)/(Ro-Ri))        #   if distance >= height:        #       distance = height-1            cir[int(y)][int(x)] = pixels[int(width*angle/m.pi) % width, height-distance-1]            y = Ro+i            # calculate source            angle = m.atan2(y-Ro,x-Ro)/2            distance = m.sqrt((y-Ro)*(y-Ro) + (x-Ro)*(x-Ro))            distance = m.floor((distance-Ri+1)*(height-1)/(Ro-Ri))        #   if distance >= height:        #       distance = height-1            cir[int(y)][int(x)] = pixels[int(width*angle/m.pi) % width, height-distance-1]shom_im(cir)

The commented-out lines draw a solid white ring. Note the various tweaks here and there to get the best result. For instance, the distance is measured from the center of the ring, and so returns a low value for close to the center and the largest values for the outside of the circle. Mapping that directly back onto the target image would display the text with its top "inwards", pointing to the inner hole. So I inverted this mapping with height - distance - 1, where the -1 is to make it map from 0 to height again.

A similar fix is in the calculation of distance itself; without the tweaks Ri+1 and height-1 either the innermost or the outermost row would not get drawn, indicating that the calculation is just one pixel off (which was exactly the purpose of that grid).


I think what you need is a noise filter. There are many implementations from which I think Gaussian filter would give a good result. You can find a list of filters here. If it gets blurred too much:

  • keep your first calculated image
  • calculate filtered image
  • copy fixed pixels from filtered image to first calculated image

Here is a crude average filter written by hand:

cir_R = int(Ro*2) # outer circle 2*rinner_r = int(Ro - 0.5 - len(img)) # inner circle rfor i in range(1, cir_R-1):    for j in range(1, cir_R-1):        if cir[i][j] == 0: # missing pixel            dx = int(i-Ro)            dy = int(j-Ro)            pix_r2 = dx*dx + dy*dy # distance to center            if pix_r2 <= Ro*Ro and pix_r2 >= inner_r*inner_r:                cir[i][j] = (cir[i-1][j] + cir[i+1][j] + cir[i][j-1] +                    cir[i][j+1])/4shom_im(cir)

and the result:

enter image description here

This basically scans between two ranges checks for missing pixels and replaces them with average of 4 pixels adjacent to it. In this black white case it is all white.

Hope it helps!