Python smooth curve - python

I have set of very closely spaced coordinates. I am connecting those coordinates by drawing line between them by using python's image.draw.line(). But the final curve obtained is not smooth as the lines at the coordinates are not properly intersecting.I also tried drawing arc instead of lines but image.draw.arc() would not take any float input for coordinates. Can anyone suggest me other method to connect those points such that final curve will be smooth.

Splines are the standard way to produce smooth curves connecting a set of points. See the Wikipedia.
In Python, you could use scipy.interpolate to compute a smoth curve:

Pillow doesn't support many way to draw a line. If you try to draw an arch, there is no options for you to choose thickness!
scipy use matplotlib to draw graph. So if you draw lines directly with matplotlib, you can turn off the axes by axis('off') command. For more detail, you may have a look at:
Matplotlib plots: removing axis, legends and white spaces
If you don't have anything to do with axes, I would recommend you to use OpenCV instead of Pillow to handle images.
def draw_line(point_lists):
width, height = 640, 480 # picture's size
img = np.zeros((height, width, 3), np.uint8) + 255 # make the background white
line_width = 1
for line in point_lists:
color = (123,123,123) # change color or make a color generator for your self
pts = np.array(line, dtype=np.int32)
cv2.polylines(img, [pts], False, color, thickness=line_width, lineType=cv2.LINE_AA)
cv2.imshow("Art", img)
cv2.waitKey(0) # miliseconds, 0 means wait forever
lineType=cv2.LINE_AA will draw an antialiased line which is beautiful.

Related

Draw A Curved Line On An Image

I have an image with some points, and I need to draw the line of best fit on the image. The points would make a polynomial line.
This is what I've got so far:
#The coordinates are filled in earlier (self.lx, self.ly)
z = np.polyfit(self.lx, self.ly, 2)
lspace = np.linspace(0, 100, 100)
draw_x = lspace
draw_y = np.polyval(z, draw_x) #I am unsure of how to draw it on to the image
To draw a polyline on an image you can use polylines of opencv:
Drawing Polygon
To draw a polygon, first you need coordinates of vertices. Make those points into an array of shape ROWSx1x2 where ROWS are number of vertices and it should be of type int32. Here we draw a small polygon of with four vertices in yellow color.
pts = np.array([[10,5],[20,30],[70,20],[50,10]], np.int32)
pts = pts.reshape((-1,1,2))
cv.polylines(img,[pts],True,(0,255,255))
Note
If third argument is False, you will get a polylines joining all the points, not a closed shape.
cv.polylines() can be used to draw multiple lines. Just create a list of all the lines you want to draw and pass it to the function. All lines will be drawn individually. It is a much better and faster way to draw a group of lines than calling cv.line() for each line.

How to skew an image by moving its vertex?

I'm trying to find a way to transform an image by translating one of its vertexes.
I have already found various methods for transforming an image like rotation and scaling, but none of the methods involved skewing like so:
There is shearing, but it's not the same since it can move two or more of the image's vertex while I only want to move one.
What can I use that can perform such an operation?
I took your "cat-thing" and resized it to a nice size, added some perfectly vertical and horizontal white gridlines and added some extra canvas in red at the bottom to give myself room to transform it. That gave me this which is 400 pixels wide and 450 pixels tall:
I then used ImageMagick to do a "Bilinear Forward Transform" in Terminal. Basically you give it 4 pairs of points, the first pair is where the top-left corner is before the transform and then where it must move to. The next pair is where the top-right corner is originally followed by where it ends up. Then the bottom-right. Then the bottom-left. As you can see, 3 of the 4 pairs are unmoved - only the bottom-right corner moves. I also made the virtual pixel black so you can see where pixels were invented by the transform in black:
convert cat.png -matte -virtual-pixel black -interpolate Spline -distort BilinearForward '0,0 0,0 399,0 399,0 399,349 330,430 0,349 0,349' bilinear.png
I also did a "Perspective Transform" using the same transform coordinates:
convert cat.png -matte -virtual-pixel black -distort Perspective '0,0 0,0 399,0 399,0 399,349 330,430 0,349 0,349' perspective.png
Finally, to illustrate the difference, I made a flickering comparison between the 2 images so you can see the difference:
I am indebted to Anthony Thyssen for his excellent work here which I commend to you.
I understand you were looking for a Python solution and would point out that there is a Python binding to ImageMagick called Wand which you may like to use - here.
Note that I only used red and black to illustrate what is going on (atop the Stack Overflow white background) and where aspects of the result come from, you would obviously use white for both!
The perspective transformation is likely what you want, since it preserves straight lines at any angle. (The inverse bilinear only preserves horizontal and vertical straight lines).
Here is how to do it in ImageMagick, Python Wand (based upon ImageMagick) and Python OpenCV.
Input:
ImageMagick
(Note the +distort makes the output the needed size to hold the full result and is not restricted to the size of the input. Also the -virtual-pixel white sets color of the area outside the image pixels to white. The points are ordered clockwise from the top left in pairs as inx,iny outx,outy)
convert cat.png -virtual-pixel white +distort perspective \
"0,0 0,0 359,0 359,0 379,333 306,376 0,333 0,333" \
cat_perspective_im.png
Python Wand
(Note the best_fit=true makes the output the needed size to hold the full result and is not restricted to the size of the input.)
#!/bin/python3.7
from wand.image import Image
from wand.display import display
with Image(filename='cat.png') as img:
img.virtual_pixel = 'white'
img.distort('perspective', (0,0, 0,0, 359,0, 359,0, 379,333, 306,376, 0,333, 0,333), best_fit=True)
img.save(filename='cat_perspective_wand.png')
display(img)
Python OpenCV
#!/bin/python3.7
import cv2
import numpy as np
# Read source image.
img_src = cv2.imread('cat.png')
# Four corners of source image
# Coordinates are in x,y system with x horizontal to the right and y vertical downward
pts_src = np.float32([[0,0], [359,0], [379,333], [0,333]])
# Four corners of destination image.
pts_dst = np.float32([[0, 0], [359,0], [306,376], [0,333]])
# Get perspecive matrix if only 4 points
m = cv2.getPerspectiveTransform(pts_src,pts_dst)
# Warp source image to destination based on matrix
# size argument is width x height
# compute from max output coordinates
img_out = cv2.warpPerspective(img_src, m, (359+1,376+1), cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=(255, 255, 255))
# Save output
cv2.imwrite('cat_perspective_opencv.png', img_out)
# Display result
cv2.imshow("Warped Source Image", img_out)
cv2.waitKey(0)
cv2.destroyAllWindows()

Dotted or dashed line with Python PILLOW

How to draw a dotted or dashed line or rectangle with Python PILLOW. Can anyone help me? Using openCV I can do that. But I want it using Pillow.
Thanks to #martineau's comment, I figured out how to draw a dotted line. Here is my code.
cur_x = 0
cur_y = 0
image_width = 600
for x in range(cur_x, image_width, 4):
draw.line([(x, cur_y), (x + 2, cur_y)], fill=(170, 170, 170))
This will draw a dotted line of gray color.
I decided to write up the idea I suggested in the comments - namely to draw the shapes with solid lines and then overlay a thresholded noisy image to obliterate parts of the line.
I made all the noise on a smaller image and then scaled it up so that the noise was "more clumped" instead of tiny blobs.
So this is just the generation of the test image:
#!/usr/local/bin/python3
import numpy as np
from PIL import Image, ImageDraw
# Make empty black image
im = Image.new('L', (640,480))
# Draw white rectangle and ellipse
draw = ImageDraw.Draw(im)
draw.rectangle([20,20,620,460],outline=255)
draw.ellipse([100,100,540,380],outline=255)
And this is generating the noise overlay and overlaying it - you can just delete this sentence and join the two lumps of code together:
# Make noisy overlay, 1/4 the size, threshold at 50%, scale up to full-size
noise = np.random.randint(0,256,(120,160),dtype=np.uint8)
noise = (noise>128)*255
noiseim = Image.fromarray(noise.astype(np.uint8))
noiseim = noiseim.resize((640,480), resample=Image.NEAREST)
# Paste the noise in, but only allowing the white shape outlines to be affected
im.paste(noiseim,mask=im)
im.save('result.png')
The result is this:
The solidly-drawn image is like this:
The noise is like this:
The following function draws a dashed line. It might be slow, but it works and I needed it.
"dashlen" is the length of the dashes, in pixels. -
"ratio" is the ratio of the empty space to the dash length (the higher the value the more empty space you get)
import math # math has the fastest sqrt
def linedashed(x0, y0, x1, y1, dashlen=4, ratio=3):
dx=x1-x0 # delta x
dy=y1-y0 # delta y
# check whether we can avoid sqrt
if dy==0: len=dx
elif dx==0: len=dy
else: len=math.sqrt(dx*dx+dy*dy) # length of line
xa=dx/len # x add for 1px line length
ya=dy/len # y add for 1px line length
step=dashlen*ratio # step to the next dash
a0=0
while a0<len:
a1=a0+dashlen
if a1>len: a1=len
draw.line((x0+xa*a0, y0+ya*a0, x0+xa*a1, y0+ya*a1), fill = (0,0,0))
a0+=step
I know this question is a bit old (4 y.o. at the time of my writing this answer), but as it happened I was in need of drawing a patterned line.
So I concocted my own solution here: https://codereview.stackexchange.com/questions/281582/algorithm-to-traverse-a-path-through-several-data-points-and-draw-a-patterned-li
(Sorry the solution was a bit long, better to just look there. The code works, though, that's why it's in CodeReview SE).
Provide the right "pattern dictionary", where blank segments are represented by setting color to None, and you should be good to go.

Detect not straight lines with hough transform or other image processing algorithm

Is there a way to detect lines which are not perfectly straight?
I have objects which represents rectangles but are slightly uneven because of the use of a wide angle camera distortion and bad quality preprocessing. Also I have to do a perspective transformation beforehand which is another factor for the poor line quality.
After detecting the edges with the canny filter, I get for example the following image:
I tried to find the edge lines with the hough lines algorithm. But because of the bad quality of the shape with a lot of bumpiness it is not possible to find the sloping edges.
I tried the normal hough line transform (red lines) and also with the Probabilistic Hough Line Transform (green lines), but the result is quite bad.
Are there any other options to detect something like that? Or is there a way to improve my image, so I get straight lines? The distortion of the lines is variable, so it is really hard to fix.
Here another example:
I'm using python 3.4 with opencv 3.2, numpy 1.12.
Any input or hint for a possible new way to solve this would be awesome.
Your edges are pretty clear -- definitely good enough for probabilistic Hough line transforms. I think you just need to play with the free parameters a bit more.
import numpy as np
import matplotlib.pyplot as plt
from skimage.transform import probabilistic_hough_line
from skimage import draw
def restore_lines(distorted):
lines = probabilistic_hough_line(distorted,
threshold=2,
line_length=20,
line_gap=15)
restored = np.zeros_like(distorted, dtype=np.uint8)
for line in lines:
p0, p1 = line
rr, cc = draw.line(p0[1], p0[0], p1[1], p1[0])
restored[rr, cc] += 1
return restored
# distorted = plt.imread('backslash.png')
distorted = plt.imread('tick.png')
# imread returns non-grayscale image in this case
distorted = distorted[:,:,0]
# restore
restored = restore_lines(distorted)
fig, axes = plt.subplots(1,2)
axes[0].imshow(distorted, cmap='gray', interpolation='none')
axes[1].imshow(restored, cmap='gray', interpolation='none')
axes[0].set_title('Original')
axes[1].set_title('Restored')
for ax in axes:
ax.set_xticks([])
ax.set_yticks([])

Draw Ellipse in Python PIL with line thickness

I am trying to draw a circle on an image, using Python. I tried this using PIL but I would like to specify a linewidth. Currently, PIL draws a circle but the border is too thin.
Here is what I have done.
For a test image: I created a 1632 X 1200 image in MS Paint and filled it green. I called it test_1.jpg. Here is the input file:
from PIL import Image, ImageDraw
im = Image.open('test_1.jpg')
width, height = im.size
eX, eY = 816,816 #Size of Bounding Box for ellipse
bbox = (width/2 - eX/2, height/2 - eY/2, width/2 + eX/2, height/2 + eY/2)
draw = ImageDraw.Draw(im)
bbox_L = []
for j in range(0,5):
bbox_L.append([element+j for element in bbox])
draw.ellipse(tuple(bbox_L[j]), outline ='white')
im.show()
Basically, I tried to draw multiple circles that would be centered at the same spot but with a different radius. My thinking was that this would create the effect of a thicker line.
However, this is producing the output shown in the attached file below:
Problem: As you can see, the bottom-left and top-right are too thin. Also, there are gaps between the various circles (see top left and bottom right).
The circle has a varying thickness. I am looking a circle with a uniform thickness.
Question:
Is there a way to do draw a circle in Python, on an image like test_1.jpg, using PIL, NumPy, etc. and to specify line thickness?
I had the same problem, and decided to write a helper function, similar to yours. This function draws two concentric ellipses in black and white on a mask layer, and the intended outline colour is stamped onto the original image through the mask. To get smoother results (antialias), the ellipses and mask is drawn in higher resolution.
Output with and without antialias
The white ellipse is 20 pixels wide, and the black ellipse is 0.5 pixels wide.
Code
from PIL import Image, ImageDraw
def draw_ellipse(image, bounds, width=1, outline='white', antialias=4):
"""Improved ellipse drawing function, based on PIL.ImageDraw."""
# Use a single channel image (mode='L') as mask.
# The size of the mask can be increased relative to the imput image
# to get smoother looking results.
mask = Image.new(
size=[int(dim * antialias) for dim in image.size],
mode='L', color='black')
draw = ImageDraw.Draw(mask)
# draw outer shape in white (color) and inner shape in black (transparent)
for offset, fill in (width/-2.0, 'white'), (width/2.0, 'black'):
left, top = [(value + offset) * antialias for value in bounds[:2]]
right, bottom = [(value - offset) * antialias for value in bounds[2:]]
draw.ellipse([left, top, right, bottom], fill=fill)
# downsample the mask using PIL.Image.LANCZOS
# (a high-quality downsampling filter).
mask = mask.resize(image.size, Image.LANCZOS)
# paste outline color to input image through the mask
image.paste(outline, mask=mask)
# green background image
image = Image.new(mode='RGB', size=(700, 300), color='green')
ellipse_box = [50, 50, 300, 250]
# draw a thick white ellipse and a thin black ellipse
draw_ellipse(image, ellipse_box, width=20)
# draw a thin black line, using higher antialias to preserve finer detail
draw_ellipse(image, ellipse_box, outline='black', width=.5, antialias=8)
# Lets try without antialiasing
ellipse_box[0] += 350
ellipse_box[2] += 350
draw_ellipse(image, ellipse_box, width=20, antialias=1)
draw_ellipse(image, ellipse_box, outline='black', width=1, antialias=1)
image.show()
I've only tested this code in python 3.4, but I think it should work with 2.7 without major modification.
Simple (but not nice) solution is to draw two circles (the smaller one with color of background):
outline = 10 # line thickness
draw.ellipse((x1-outline, y1-outline, x2+outline, y2+outline), fill=outline_color)
draw.ellipse((x1, y1, x2, y2), fill=background_color)
From version 5.3.0 onwards, released on 18 Oct 2018, Pillow has supported width for ImageDraw.ellipse. I doubt many people are using PIL nowadays.
I don't think there's a way to specify ellipse thickness, but you probably can draw lines at each pixel where ellipse pass, with the argument width=...
NB: I'm foreign, so sorry if my english is wrong.
You can use the Image.core.draw method like this:
zero_array = np.zeros((224,224))
im = Image.fromarray(np.uint8(zero_array))
draw = ImageDraw.Draw(im)
dr_im = Image.core.draw(im.getdata(), 0)
dr_im.draw_rectangle((22,33, 150,100),220,2)
dr_im.draw_rectangle((22,33, 150,100),125,0)
#draw.rectangle((22,33, 150,100), fill=220,outline = 125)
print(np.array(im)[33][23])
im.show()

Categories

Resources