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 pltfig = 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](https://i.stack.imgur.com/RCzSQ.png)
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?
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 pltfig = 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 pltfig = 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 pltdef print_pixel_coords(event):fig = event.canvas.figureax = 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!