I need to overplot general coordinate grids on images in python. I can compute the pixel coordinates of the grid lines, so I just need a module capable of drawing them as dashed lines on top of an image. The image comes in the form of a numpy
array, so I need to be able to convert between those and the image format used by the drawing library (one direction is sufficient - I can either draw the grid and export it to numpy
, or import the numpy
array and draw on it). It also needs to be reasonably fast.
Here's what I've tried:
wand
Recently got support for drawing dashed lines, and is numpy-compatible:
with Drawing() as draw:
draw.stroke_antialias = False
draw.stroke_dash_array = [1,3]
draw.stroke_color = Color("gray")
draw.fill_opacity = 0
points = calc_grid_points()
draw.polyline(points)
with Image(width=width, height=height) as img:
draw(img)
return np.fromstring(img.make_blob("RGBA"),np.uint8).reshape(img.height, img.width, 4)
However, drawing a few hundred dashed lines over a 2000x1000 image with this library takes 30s! And pretty much all that time is spent in draw(img)
. So unless I'm doing something terribly wrong here, wand is just too slow.
PIL
Python Image Library in general works fine, but it does not seem to support dashed lines. I've not seen anybody state it directly, but a google search just yields 2-3 people asking about it and not getting any answers. Non-dashed coordinate grids don't look as nice, and cover up to much of the image being plotted. PIL is much faster than wand. This dashless but otherwise equivalent version to the wand version above takes only 0.06 s to draw. That's 450 times faster than wand!
img = PIL.Image.new("RGBA", (width, height))
draw = PIL.ImageDraw.Draw(img)
segs = calc_grid_points()
for seg in segs:
draw.line([tuple(i) for i in seg], fill=(0,0,0,32))
return np.array(img)
GD
gd
supports dashed lines, but I didn't find an efficient way of converting its images to and from numpy
arrays.
Matplotlib
Matplotlib has proper support for drawing coordinate grids and axes, but sadly it seems to be impossible to avoid repixelization. It insists on building its new set of pixels that never have a 1-to-1 mapping with the original pixels. That's fine for smooth images, but not for images with large changes from pixel to pixel.
Here is an example of matplotlib repixelization:
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
n = 1001
data = np.arange(n*n).reshape(n,n)%2
plt.imshow(data)
plt.savefig("test.png")
This image contains a pattern that switches between 0 and 1 for every pixel, in a chessboard pattern. But as far as I know there is no way to ensure that one pixel of data will correspond to exactly one pixel in the output. And if it doesn't, you get moiree patterns like what this code produces:
Manually tweaking the dpi
setting can reduce this problem, but not eliminate it. The correct output would have the data part of the plot take up exactly 1001 by 1001 pixels (so the total image would be larger than that), and show 500 repetitions of the pattern in each direction.
Edit: Adding interpolation='none'
does not help. That just causes matplotlib to use nearest neighbor. It still doesn't show all the data. Here is what the output looks like then:
And here is what the correct output would look like (I cropped it to 500x500, the full version would be 1001x1001):
But matplotlib isn't really my issue - it's simply drawing dashed lines on an image in python. Matplotlib is simply one possible way of doing that.
Alternatives?
So I wonder, are there any other image libraries out there that would achieve this? Or have I overlooked features of the ones above that make them usable?