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:
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 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!
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With