3

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:

Matplotlib moiree

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:

Matplotlib nearest neighbor

And here is what the correct output would look like (I cropped it to 500x500, the full version would be 1001x1001):

Correct output

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?

amaurea
  • 4,950
  • 26
  • 35
  • Can you add a minimal example of code so we can see what format your data is in? For an example, [here's an old question of mine](http://stackoverflow.com/q/15160123/553404). I would think matplotlib can do this so it would be great if you can add the code you've tried so that we can take a look at (and hopefully fix) the "1-to-1 mapping" problem you describe. You could add an example image with: from `from skimage.data import coffee` ... `image = coffee()`. – YXD Feb 21 '15 at 16:00
  • 1
    I think you will have to focus your question more on your specific problem. A reproducible example and an image how the expected output should look like, would be great. As it is now the question can be interpreted as a request to find a library and that's offtopic on SO. – cel Feb 21 '15 at 16:05
  • 1
    I've added a short example of matplotlib repixelization now. – amaurea Feb 21 '15 at 16:17
  • 1
    @cel: It *is* a request to find a library. A large fraction of problems here are solved by finding the right library for the job. Where would be on-topic for this question if SO isn't? – amaurea Feb 21 '15 at 16:20
  • @Mr E: And thanks for looking into this. If matplotlib could be made to work, that would be great, and would save me a lot of time. I've uploaded an example showing what the correct output would look like. Do you think it's possible to achieve that in matplotlib? – amaurea Feb 21 '15 at 16:55
  • It's not clear what you want to do with your chequerboard grid or what the output should be. Is this a question about generating and saving an image or is it about plotting and visualising in a window? Is it meant to be displayed on top of another image? Or you just want to end up with a png of a certain size with this pattern? – YXD Feb 21 '15 at 18:22
  • @Mr E: I have a 2d array of data. I want to draw a coordinate grid over it. I don't want this to mess up the data. The checkerboard image was an example of what data might look like. The grid would be on top of that. Matplotlib can draw the grid, but if I use matplotlib, the data gets messed up. I chose the checkerboard to illustrate that. Hence why I'm looking for other ways to draw the grid. In the end, the result would be written to an image file, such as a png file, but that's the easy part. – amaurea Feb 21 '15 at 18:33
  • I think there are two things here: the data and the figure. How you save the data is important. A Matplotlib figure will normally contain a zoomed version of your image and when you call `plt.savefig` it effectively takes a screenshot at whatever size the plot is displayed (even if it is not on screen) and saves that. It would help if you could add an example of your data and show or explain more clearly what you want the output to be. Maybe instead of saving the figure, you just want to be using `imsave` from `scipy.misc` or `skimage.io` and interpolating the two images with your own code. – YXD Feb 21 '15 at 18:56
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/71398/discussion-between-amaurea-and-mr-e). – amaurea Feb 21 '15 at 19:11

1 Answers1

2

With matplotlib, playing around with dpi seems to give OK results, once you remove frames and tick marks.

There's one snatch however that the bounding box calculation runs into rounding errors, so we need to fix the bbox size manually:

import numpy as np
import matplotlib
import matplotlib.pyplot as plt

# Some fake data
n = 1001
data = np.arange(n*n).reshape(n,n)%2
data = data[:n//2,:]

def get_bitmap_frame(data, dpi=None):
    """
    Construct a figure for overlaying elements on top of bitmap data.

    Returns
    -------
    fig, ax
        Matplotlib figure and axis objects
    """
    if dpi is None:
        dpi = matplotlib.rcParams['savefig.dpi']
    dpi = float(dpi)

    fig = plt.figure(figsize=(data.shape[1]/dpi, data.shape[0]/dpi), dpi=dpi)
    ax = fig.add_axes([0, 0, 1, 1], frame_on=False)
    ax.xaxis.set_visible(False)
    ax.yaxis.set_visible(False)
    ax.imshow(data, interpolation='none')

    # Note: it can happen that floating point rounding error in
    # bbox calculation results to the bbox size being off by one
    # pixel. Because of this, we set it manually
    fig.bbox = matplotlib.transforms.Bbox.from_bounds(0, 0, data.shape[1], data.shape[0])
    return fig, ax

fig, ax = get_bitmap_frame(data)

# Annotate
ax.text(100, 100, 'Hello!', color='w')

fig.savefig("test.png")
pv.
  • 33,875
  • 8
  • 55
  • 49
  • That's very promising! This would be just what I need if it's possible to do this while still keeping the frames and grid. I guess that would make it difficult to compute the correct bounds, though. – amaurea Feb 21 '15 at 19:39
  • You can just remove the `xaxis.set_visible(False)` lines and add `ax.grid(True)` to do that, or you can draw them manually. They are drawn on top of the image, and don't affect scaling. I removed them just because you didn't have them in what your "correct result" image above. – pv. Feb 21 '15 at 22:09
  • Ok, you can indeed draw a grid with matplotlib without messing up the pixels, so this answers my question. Thanks!. But it seems like this solution won't scale easily to adding numbered ticks along the axis, colorbars etc, right? – amaurea Feb 21 '15 at 22:18
  • You probably only need to make sure that the size of the axes, as given to fig.add_axes, corresponds to size in pixels. Above, the axes fills the whole figure. Whether you can end up with off-by-one-pixel rounding issues --- no idea, but probably needs huge figures before floating point precision runs out. – pv. Feb 21 '15 at 22:19