How to split an image into triangular tiles? - python

I would like to split an image into triangle shaped tiles (equilateral) . I have tried to generate the coordinates of a triangle using the function from https://alexwlchan.net/2016/10/tiling-the-plane-with-pillow/.
My code:
#import opencv
import math
image_path="/content/newspaper-icon-in-transparent-style-news-on-vector-25591681.jpg"
#Create Triangles
# https://alexwlchan.net/2016/10/tiling-the-plane-with-pillow/
#A horrizontal offset is added to ensure that images line up
#https://stackoverflow.com/questions/22588074/polygon-crop-clip-using-python-pil
def generate_coordinates_for_unit_triangles(image_width,image_height):
image_width=50;
image_height=50;
h=math.sin(math.pi/3)
for x in range(image_width):
for y in range(int(image_height / h)):
first_c,second_c,third_c=(x, y * h), (x+1, y * h), ((x+0.5, (y+1) * h))
first_sc, second_sc,third_sc=(x+1, y * h), (x+1.5, (y+1) * h), (x+0.5, (y+1) * h)
return first_c, second_c,third_c, first_sc, second_sc,third_sc
#return [(x, y * h), (x+1, y * h), (x+0.5, (y+1) * h)] ,[(x+1, y * h), (x+1.5, (y+1) * h), (x+0.5, (y+1) * h)]
##Generates the two triangles coordinates
first_c, second_c,third_c, first_sc, second_sc,third_sc=generate_coordinates_for_unit_triangles(50,50)
#convert image into numpy array
image_read=Image.open(image_path)
image_to_numpy=np.asarray(image_read)
shape_of_array=image_to_numpy.shape
print(shape_of_array)
mask_image=[first_c, second_c,third_c, first_sc, second_sc,third_sc]
I realized that this may not given my desired output.
The expected input and output is included below:
[Expected input and output][1]
Any guidance on how to approach the problem would be appreciated.
[1]: https://i.stack.imgur.com/vr7rV.jpg

I'm posting this as an answer because it's long, but it's not literally an answer. I'm hoping this will lead you to the next step in your design process.
Here are the design decisions you face, It's clear from your code that you can generate a list of triangle coordinates. Good, what next? You probably know the bounding box of your triangles (largest w and h) advance, so you can certainly create a set of images that contain your triangles, masked off with a black background or alpha=0 background. You could just copy the bounding rectangle to an image, then create a mask using the triangle as a path, and set the alpha to 0 outside of the mask. opencv should be able to do that.
But after you have those, what then? You talked about matching the edges. That's complicated. I suppose you could extract a vector of pixels from the three edges of each triangle, and then do some kind of approximate comparison.
If you do find matches that allow you to stitch together a composite, it is possible (assuming you have alpha=0 in the backgrounds) to blit all of these triangles back into some larger image, kind of like quilting. openvc can do block copy with alpha blending.
So, in the end, I think your problem is achievable, but it's going to be a lot of work, and probably more than we can offer here.

Related

Get exact values of width and height of cv2.minAreaRect()

I am using a script to measure the size of objects using OpenCV. For my pixel_to_mm_ratio I use an Aruco Marker. This is done with exact values.
To draw boxes around the other objects I first find contours and then draw rectangles around them with cv.minAreaRect(). My problem is that width (w) and height (h) are not given as exact numbers (float) but are already rounded (integer):
rect = cv.minAreaRect(cnt)
(x, y), (w, h), angle = rect
Through the rounding of these numbers (w and h) from rectangles around contours i later get an inaccuracy when calculating the width and height in mm.
object_width = w / pixel_mm_ratio
object_height = h / pixel_mm_ratio
Is there a way to get the exact values from cv2.minAreaRect()? Or an other way to grab these values?
Thanks in advance!

Rotating a Color Image in Python Without Using Packages and Using Transformation Matrix

I am trying to create a method that rotates a color image in Python by 0, 90, 180, or 270 degrees. This is my code so far. As it stands, the output is a blank image. I also am trying to get the code to be as efficient as possible. Not sure what I am doing wrong. Image is a 3D array while rotate_angle is a int. The idea I was trying was to take all the (x,y) coordinates of pixels from the original image and multiply them with the transformation matrix (x_transformed, y_transformed). Then, the colors at (x_transformed, y_transformed) on the new image would just be the colors at (x,y) on the old image.
def rotate_image(image, rotate_angle):
output_image = image
# Convert degrees to radian
angle = math.radians(rotate_angle)
# For all values of height
for i in range(image.shape[0]):
# And all values of width
for j in range(image.shape[1]):
# Take the x and y coordinates of the existing points
y = image.shape[0]-1
x = image.shape[1]-1
# Keep in mind rotation matrix is [(cos, sin), (-sin, cos)] [(x,y)]
y_n = int(-x * math.sin(angle) + y * math.cos(angle))
x_n = int(x * math.cos(angle) + y * math.sin(angle))
# Rotating to where we are and then copying data from where we were
output_image[y_n, x_n, :] = image[i, j, :]
return output_image
Of course, if there is a more efficient way, I am also interested in knowing what it is. I know there are packages out there that can do what I am doing, but I wish to make the method myself and with the tools I have.

PIL zoom into image at a particular point

I'm creating some images with python imaging library (PIL). Now, like we zoom into a map at a particular location, I want to similarly zoom into my image at a specified point. Note that this is different from resizing the image. I want the size to remain the same. I couldn't find any inbuilt method in the documentation that does this. Is anyone aware of a method that might achieve this. I'd ideally like to do this without other dependencies like openCV.
I think you mean this:
def zoom_at(img, x, y, zoom):
w, h = img.size
zoom2 = zoom * 2
img = img.crop((x - w / zoom2, y - h / zoom2,
x + w / zoom2, y + h / zoom2))
return img.resize((w, h), Image.LANCZOS)
This will crop the image around the point where you zoom into and then upscale the resulting image to the original size.

How can I create a circular mask for a numpy array?

I am trying to circular mask an image in Python. I found some example code on the web, but I'm not sure how to change the maths to get my circle in the correct place.
I have an image image_data of type numpy.ndarray with shape (3725, 4797, 3):
total_rows, total_cols, total_layers = image_data.shape
X, Y = np.ogrid[:total_rows, :total_cols]
center_row, center_col = total_rows/2, total_cols/2
dist_from_center = (X - total_rows)**2 + (Y - total_cols)**2
radius = (total_rows/2)**2
circular_mask = (dist_from_center > radius)
I see that this code applies euclidean distance to calculate dist_from_center, but I don't understand the X - total_rows and Y - total_cols part. This produces a mask that is a quarter of a circle, centered on the top-left of the image.
What role are X and Y playing on the circle? And how can I modify this code to produce a mask that is centered somewhere else in the image instead?
The algorithm you got online is partly wrong, at least for your purposes. If we have the following image, we want it masked like so:
The easiest way to create a mask like this is how your algorithm goes about it, but it's not presented in the way that you want, nor does it give you the ability to modify it in an easy way. What we need to do is look at the coordinates for each pixel in the image, and get a true/false value for whether or not that pixel is within the radius. For example, here's a zoomed in picture showing the circle radius and the pixels that were strictly within that radius:
Now, to figure out which pixels lie inside the circle, we'll need the indices of each pixel in the image. The function np.ogrid() gives two vectors, each containing the pixel locations (or indices): there's a column vector for the column indices and a row vector for the row indices:
>>> np.ogrid[:4,:5]
[array([[0],
[1],
[2],
[3]]), array([[0, 1, 2, 3, 4]])]
This format is useful for broadcasting so that if we use them in certain functions, it will actually create a grid of all the indices instead of just those two vectors. We can thus use np.ogrid() to create the indices (or pixel coordinates) of the image, and then check each pixel coordinate to see if it's inside or outside the circle. In order to tell whether it's inside the center, we can simply find the Euclidean distance from the center to every pixel location, and then if that distance is less than the circle radius, we'll mark that as included in the mask, and if it's greater than that, we'll exclude it from the mask.
Now we've got everything we need to make a function that creates this mask. Furthermore we'll add a little bit of nice functionality to it; we can send in the center and the radius, or have it automatically calculate them.
def create_circular_mask(h, w, center=None, radius=None):
if center is None: # use the middle of the image
center = (int(w/2), int(h/2))
if radius is None: # use the smallest distance between the center and image walls
radius = min(center[0], center[1], w-center[0], h-center[1])
Y, X = np.ogrid[:h, :w]
dist_from_center = np.sqrt((X - center[0])**2 + (Y-center[1])**2)
mask = dist_from_center <= radius
return mask
In this case, dist_from_center is a matrix the same height and width that is specified. It broadcasts the column and row index vectors into a matrix, where the value at each location is the distance from the center. If we were to visualize this matrix as an image (scaling it into the proper range), then it would be a gradient radiating from the center we specify:
So when we compare it to radius, it's identical to thresholding this gradient image.
Note that the final mask is a matrix of booleans; True if that location is within the radius from the specified center, False otherwise. So we can then use this mask as an indicator for a region of pixels we care about, or we can take the opposite of that boolean (~ in numpy) to select the pixels outside that region. So using this function to color pixels outside the circle black, like I did up at the top of this post, is as simple as:
h, w = img.shape[:2]
mask = create_circular_mask(h, w)
masked_img = img.copy()
masked_img[~mask] = 0
But if we wanted to create a circular mask at a different point than the center, we could specify it (note that the function is expecting the center coordinates in x, y order, not the indexing row, col = y, x order):
center = (int(w/4), int(h/4))
mask = create_circular_mask(h, w, center=center)
Which, since we're not giving a radius, would give us the largest radius so that the circle would still fit in the image bounds:
Or we could let it calculate the center but use a specified radius:
radius = h/4
mask = create_circular_mask(h, w, radius=radius)
Giving us a centered circle with a radius that doesn't extend exactly to the smallest dimension:
And finally, we could specify any radius and center we wanted, including a radius that extends outside the image bounds (and the center can even be outside the image bounds!):
center = (int(w/4), int(h/4))
radius = h/2
mask = create_circular_mask(h, w, center=center, radius=radius)
What the algorithm you found online does is equivalent to setting the center to (0, 0) and setting the radius to h:
mask = create_circular_mask(h, w, center=(0, 0), radius=h)
I'd like to offer a way to do this that doesn't involve the np.ogrid() function. I'll crop an image called "robot.jpg", which is 491 x 491 pixels. For readability I'm not going to define as many variables as I would in a real program:
Import libraries:
import matplotlib.pyplot as plt
from matplotlib import image
import numpy as np
Import the image, which I'll call "z". This is a color image so I'm also pulling out just a single color channel. Following that, I'll display it:
z = image.imread('robot.jpg')
z = z[:,:,1]
zimg = plt.imshow(z,cmap="gray")
plt.show()
robot.jpg as displayed by matplotlib.pyplot
To wind up with a numpy array (image matrix) with a circle in it to use as a mask, I'm going to start with this:
x = np.linspace(-10, 10, 491)
y = np.linspace(-10, 10, 491)
x, y = np.meshgrid(x, y)
x_0 = -3
y_0 = -6
mask = np.sqrt((x-x_0)**2+(y-y_0)**2)
Note the equation of a circle on that last line, where x_0 and y_0 are defining the center point of the circle in a grid which is 491 elements tall and wide. Because I defined the grid to go from -10 to 10 in both x and y, it is within that system of units that x_0 and x_y set the center point of the circle with respect to the center of the image.
To see what that produces I run:
maskimg = plt.imshow(mask,cmap="gray")
plt.show()
Our "proto" masking circle
To turn that into an actual binary-valued mask, I'm just going to take every pixel below a certain value and set it to 0, and take every pixel above a certain value and set it to 256. The "certain value" will determine the radius of the circle in the same units defined above, so I'll call that 'r'. Here I'll set 'r' to something and then loop through every pixel in the mask to determine if it should be "on" or "off":
r = 7
for x in range(0,490):
for y in range(0,490):
if mask[x,y] < r:
mask[x,y] = 0
elif mask[x,y] >= r:
mask[x,y] = 256
maskimg = plt.imshow(mask,cmap="gray")
plt.show()
The mask
Now I'll just multiply the mask by the image element-wise, then display the result:
z_masked = np.multiply(z,mask)
zimg_masked = plt.imshow(z_masked,cmap="gray")
plt.show()
To invert the mask I can just swap the 0 and the 256 in the thresholding loop above, and if I do that I get:
Masked version of robot.jpg
The other answers work, but they are slow, so I will propose an answer using skimage.draw.disk. Using this is faster and I find it simple to use. Simply specify the center of the circle and radius then use the output to create a mask
from skimage.draw import disk
mask = np.zeros((10, 10), dtype=np.uint8)
row = 4
col = 5
radius = 5
rr, cc = disk(row, col, radius)
mask[rr, cc] = 1

Sorting a list of RGB triplets into a spectrum

I have a list of RGB triplets, and I'd like to plot them in such a way that they form something like a spectrum.
I've converted them to HSV, which people seem to recommend.
from PIL import Image, ImageDraw
import colorsys
def make_rainbow_rgb(colors, width, height):
"""colors is an array of RGB tuples, with values between 0 and 255"""
img = Image.new("RGBA", (width, height))
canvas = ImageDraw.Draw(img)
def hsl(x):
to_float = lambda x : x / 255.0
(r, g, b) = map(to_float, x)
h, s, l = colorsys.rgb_to_hsv(r,g,b)
h = h if 0 < h else 1 # 0 -> 1
return h, s, l
rainbow = sorted(colors, key=hsl)
dx = width / float(len(colors))
x = 0
y = height / 2.0
for rgb in rainbow:
canvas.line((x, y, x + dx, y), width=height, fill=rgb)
x += dx
img.show()
However, the result doesn't look very much like a nice rainbow-y spectrum. I suspect I need to either convert to a different color space or handle the HSL triplet differently.
Does anyone know what I need to do to make this data look roughly like a rainbow?
Update:
I was playing around with Hilbert curves and revisited this problem. Sorting the RGB values (same colors in both images) by their position along a Hilbert curve yields an interesting (if still not entirely satisfying) result:
You're trying to convert a three-dimensional space into a one-dimensional space. There's no guarantee that you can make a pleasing rainbow out of it, as Oli says.
What you can do is "bucket" the colors into a few different categories based on saturation and value/lightness, and then sort within the categories, to get several independent gradients. For example, high-saturation colors first for the classic rainbow, then mid-saturation high-value colors (pastels), then low-saturation (grays).
Alternately, if all you care about is the rainbow, convert to hsl, then slam saturation to 1.0 and value to 0.5, convert back to rgb and render that instead of the original color.
Presumably you are sorting by hue (i.e. H)? That will give a nice result if S and L (or V) are constant, but if they are varying independently, then you will get a bit of a mess!
An interesting method for reducing dimensionality of color spaces uses the space-filling Hilbert curve. Two relevant articles are:
Color Space Dimension Reduction - overview of several methods for reducing dimensionality of color data
Portrait of the Hilbert Curve - detailed article about Hilbert curves and application to color-space dimensionality reduction
They both consider 3d -> 2d reduction, but the intermediate step of mapping to the 1d curve could be a solution to your problem.
Here are some rainbows I made recently, you can modify the idea to do what you want
from PIL import Image, ImageDraw # pip install pillow
import numpy as np
from matplotlib import pyplot as plt
strip_h, strip_w = 100, 720
strip = 255*np.ones((strip_h,strip_w,3), dtype='uint8')
image_val = Image.fromarray(strip)
image_sat = Image.fromarray(strip)
draw0 = ImageDraw.Draw(image_val)
draw1 = ImageDraw.Draw(image_sat)
for y in range(strip_h):
for x in range(strip_w):
draw0.point([x, y], fill='hsl(%d,%d%%,%d%%)'%(x%360,y,50))
draw1.point([x, y], fill='hsl(%d,%d%%,%d%%)'%(x%360,100,y))
plt.subplot(2,1,1)
plt.imshow(image_val)
plt.subplot(2,1,2)
plt.imshow(image_sat)
plt.show()
This seems incorrect.
canvas.line((x, y, x + dx, y), width=height, fill=rgb)
Try this.
canvas.rectangle([(x, y), (x+dx, y+height)], fill=rgb)

Categories