Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to map coordinates in AxesImage to coordinates in saved image file?

I use matplotlib to display a matrix of numbers as an image, attach labels along the axes, and save the plot to a PNG file. For the purpose of creating an HTML image map, I need to know the pixel coordinates in the PNG file for a region in the image being displayed by imshow.

I have found an example of how to do this with a regular plot, but when I try to do the same with imshow, the mapping is not correct. Here is my code, which saves an image and attempts to print the pixel coordinates of the center of each square on the diagonal:

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8])
axim = ax.imshow(np.random.random((27,27)), interpolation='nearest')
for x, y in  axim.get_transform().transform(zip(range(28), range(28))):
    print int(x), int(fig.get_figheight() * fig.get_dpi() - y)
plt.savefig('foo.png', dpi=fig.get_dpi())

Here is the resulting foo.png, shown as a screenshot in order to include the rulers:

Screenshot of foo.png with rulers

The output of the script starts and ends as follows:

73 55
92 69
111 83
130 97
149 112
…
509 382
528 396
547 410
566 424
585 439

As you see, the y-coordinates are correct, but the x-coordinates are stretched: they range from 73 to 585 instead of the expected 135 to 506, and they are spaced 19 pixels o.c. instead of the expected 14. What am I doing wrong?

like image 284
Vebjorn Ljosa Avatar asked Jan 12 '11 11:01

Vebjorn Ljosa


2 Answers

This is one of the more confusing parts of trying to get exact pixel values from matplotlib. Matplotlib separates the renderer that handles exact pixel values from the canvas that the figure and axes are drawn on.

Basically, the renderer that exists when the figure is initially created (but not yet displayed) is not necessarily the same as the renderer that is used when displaying the figure or saving it to a file.

What you're doing is correct, but it's using the initial renderer, not the one that's used when the figure is saved.

To illustrate this, here's a slightly simplified version of your code:

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111)
im = ax.imshow(np.random.random((27,27)), interpolation='nearest')

for i in range(28):
    x, y =  ax.transData.transform_point([i,i])
    print '%i, %i' % (x, fig.bbox.height - y)

fig.savefig('foo.png', dpi=fig.dpi)

This yields similar results to what you have above: (the differences are due to different rendering backends between your machine and mine)

89, 55
107, 69
125, 83
...
548, 410
566, 424
585, 439

However, if we do the exact same thing, but instead draw the figure before displaying the coordinates, we get the correct answer!

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111)
im = ax.imshow(np.random.random((27,27)), interpolation='nearest')

fig.canvas.draw()

for i in range(28):
    x, y =  ax.transData.transform_point([i,i])
    print '%i, %i' % (x, fig.bbox.height - y)

fig.savefig('foo.png', dpi=fig.dpi)

This yields: (Keep in mind that the edge of the figure is at <-0.5, -0.5> in data coordinates, not <0, 0>. (i.e. the coordinates for the plotted image are pixel-centered) This is why <0, 0> yields 143, 55, and not 135, 48)

143, 55
157, 69
171, 83
...
498, 410
512, 424
527, 439

Of course, drawing the figure just to draw it again when it's saved is redundant and computationally expensive.

To avoid drawing things twice, you can connect a callback function to the draw event, and output your HTML image map inside this function. As a quick example:

import numpy as np
import matplotlib.pyplot as plt

def print_pixel_coords(event):
    fig = event.canvas.figure
    ax = fig.axes[0] # I'm assuming there's only one subplot here...
    for i in range(28):
        x, y = ax.transData.transform_point([i,i])
        print '%i, %i' % (x, fig.bbox.height - y)

fig = plt.figure()
ax = fig.add_subplot(111)
im = ax.imshow(np.random.random((27,27)), interpolation='nearest')

fig.canvas.mpl_connect('draw_event', print_pixel_coords)

fig.savefig('foo.png', dpi=fig.dpi)

Which yields the correct output, while only drawing the figure once, when it is saved:

143, 55
157, 69
171, 83
...
498, 410
512, 424
527, 439

Another advantage is that you can use any dpi in the call to fig.savefig without having to manually set the fig object's dpi beforehand. Therefore, when using the callback function, you can just do fig.savefig('foo.png'), (or fig.savefig('foo.png', dpi=whatever)) and you'll get output that matches the saved .png file. (The default dpi when saving a figure is 100, while the default dpi for a figure object is 80, which is why you had to specify the dpi to be the same as fig.dpi in the first place)

Hopefully that's at least somewhat clear!

like image 60
Joe Kington Avatar answered Nov 14 '22 22:11

Joe Kington


I don't know if I understand correctly what you are asking for, but if I am , you need the pixel coordinates for the lower left corner of the graph (attempt that in the graph's units these are (-0.5, -0.5) ), and from there, you can compute the pixel coordinates for each box, just with the box side in pixels.

To get these, do:

x = ax.get_xaxis().get_clip_box().x0
y = ax.get_yaxis().get_clip_box().y1

(I got the values 128, 432 for the given image)

like image 23
jsbueno Avatar answered Nov 14 '22 23:11

jsbueno