Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calculate pixel values from latitude/longitude coordinates (using matplotlib Basemap)

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")

sample output

  • Red dots should be in the middle of the labels.

I guess that the error comes in where I extract the coordinates of the text boxes (in Step #2). Any help much appreciated.

Notes

  • Perhaps the solution is something along the lines of this answer?
like image 814
Michael Dunn Avatar asked Jan 24 '12 16:01

Michael Dunn


People also ask

How do you plot a basemap in Python?

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.


1 Answers

Two things are happening to cause your pixel positions to be off.

  1. The dpi used to calculated the text position is different from that used to save the figure.

  2. 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:

enter image description here

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.

like image 174
Yann Avatar answered Oct 07 '22 16:10

Yann