I need to convert map coordinates into pixels (in order to make a clickable map in html).
Here is a sample map (made using the Basemap package from matplotlib). I have put some labels on it and attempted to calculate the midpoints of the labels in pixels:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
## Step 0: some points to plot
names = [u"Reykjavík", u"Höfn", u"Húsavík"]
lats = [64.133333, 64.25, 66.05]
lons = [-21.933333, -15.216667, -17.316667]
## Step 1: draw a map using matplotlib/Basemap
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
M = Basemap(projection='merc',resolution='c',
llcrnrlat=63,urcrnrlat=67,
llcrnrlon=-24,urcrnrlon=-13)
x, y = M(lons, lats) # transform coordinates according to projection
boxes = []
for xa, ya, name in zip(x, y, names):
box = plt.text(xa, ya, name,
bbox=dict(facecolor='white', alpha=0.5))
boxes.append(box)
M.bluemarble() # a bit fuzzy at this resolution...
plt.savefig('test.png', bbox_inches="tight", pad_inches=0.01)
# Step 2: get the coordinates of the textboxes in pixels and calculate the
# midpoints
F = plt.gcf() # get current figure
R = F.canvas.get_renderer()
midpoints = []
for box in boxes:
bb = box.get_window_extent(renderer=R)
midpoints.append((int((bb.p0[0] + bb.p1[0]) / 2),
int((bb.p0[1] + bb.p1[1]) / 2)))
These calculated points are in the approximately correct relative relation to each other, but do not coincide with the true points. The following code snippet should put a red dot on the midpoint of each label:
# Step 3: use PIL to draw dots on top of the labels
from PIL import Image, ImageDraw
im = Image.open("test.png")
draw = ImageDraw.Draw(im)
for x, y in midpoints:
y = im.size[1] - y # PIL counts rows from top not bottom
draw.ellipse((x-5, y-5, x+5, y+5), fill="#ff0000")
im.save("test.png", "PNG")
I guess that the error comes in where I extract the coordinates of the text boxes (in Step #2). Any help much appreciated.
Notes
fig = plt. figure(figsize=(8, 8)) m = Basemap(projection='lcc', resolution=None, width=8E6, height=8E6, lat_0=45, lon_0=-100,) m. etopo(scale=0.5, alpha=0.5) # Map (long, lat) to (x, y) for plotting x, y = m(-122.3, 47.6) plt.
Two things are happening to cause your pixel positions to be off.
The dpi used to calculated the text position is different from that used to save the figure.
When you use the bbox_inches
option in the savefig
call, it eliminates a lot of white space. You don't take this into account when you are drawing your circles with PIL
(or checking where someone clicked. Also you add a padding in this savefig
call that you may need to account for if it's very large (as I show in my example below). Probably it will not matter if you still use 0.01.
To fix this first issue, just force the figure and the savefig
call to use the same DPI.
To fix the second issue, document the (0,0) position (Axes units) of the axes in pixels, and shift your text positions accordingly.
Here's a slightly modified version of your code:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
## Step 0: some points to plot
names = [u"Reykjavík", u"Höfn", u"Húsavík"]
lats = [64.133333, 64.25, 66.05]
lons = [-21.933333, -15.216667, -17.316667]
## Step 1: draw a map using matplotlib/Basemap
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
# predefined dpi
FIGDPI=80
# set dpi of figure, so that all calculations use this value
plt.gcf().set_dpi(FIGDPI)
M = Basemap(projection='merc',resolution='c',
llcrnrlat=63,urcrnrlat=67,
llcrnrlon=-24,urcrnrlon=-13)
x, y = M(lons, lats) # transform coordinates according to projection
boxes = []
for xa, ya, name in zip(x, y, names):
box = plt.text(xa, ya, name,
bbox=dict(facecolor='white', alpha=0.5))
boxes.append(box)
M.bluemarble() # a bit fuzzy at this resolution...
# predefine padding in inches
PADDING = 2
# force dpi to same value you used in your calculations
plt.savefig('test.png', bbox_inches="tight", pad_inches=PADDING,dpi=FIGDPI)
# document shift due to loss of white space and added padding
origin = plt.gca().transAxes.transform((0,0))
padding = [FIGDPI*PADDING,FIGDPI*PADDING]
Step #2 is unchanged
Step #3 takes account of the origin
# Step 3: use PIL to draw dots on top of the labels
from PIL import Image, ImageDraw
im = Image.open("test.png")
draw = ImageDraw.Draw(im)
for x, y in midpoints:
# deal with shift
x = x-origin[0]+padding[0]
y = y-origin[1]+padding[1]
y = im.size[1] - y # PIL counts rows from top not bottom
draw.ellipse((x-5, y-5, x+5, y+5), fill="#ff0000")
im.save("test.png", "PNG")
This results in:
Notice that I used an exaggerated PADDING
value to test that everything still works, and a value of 0.01 would produce your original figure.
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