Basemap plus 3d graph - matplotlib-basemap

Hello Stackoverflow forks,
I'm a enthusiastic python learner.
I have studied python to visualiza my personal project about population density.
I have gone through tutorials about matplotlib and basemap in python.
I came across with the idea about
mapping my 3dimensional graph on top of the basemap which allows me to use geographycal coordinate information.
Can anyone let me know how I could use basemap as a base plane for the 3dimensional graph?
Please let me know which tutorial or references I could go with for developing this.
Best,
Thank you always Stackoverflow forks.

The basemap documentation has a small section on 3D plotting. Here's a simple script to get you started:
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
plt.close('all')
fig = plt.figure()
ax = fig.gca(projection='3d')
extent = [-127, -65, 25, 51]
# make the map and axis.
m = Basemap(llcrnrlon=extent[0], llcrnrlat=extent[2],
urcrnrlon=extent[1], urcrnrlat=extent[3],
projection='cyl', resolution='l', fix_aspect=False, ax=ax)
ax.add_collection3d(m.drawcoastlines(linewidth=0.25))
ax.add_collection3d(m.drawcountries(linewidth=0.25))
ax.add_collection3d(m.drawstates(linewidth=0.25))
ax.view_init(azim = 230, elev = 15)
ax.set_xlabel(u'Longitude (°E)', labelpad=10)
ax.set_ylabel(u'Latitude (°N)', labelpad=10)
ax.set_zlabel(u'Altitude (ft)', labelpad=20)
# values to plot - change as needed. Plots 2 dots, one at elevation 0 and another 100.
# also draws a line between the two.
x, y = m(-85.4808, 32.6099)
ax.plot3D([x, x], [y, y], [0, 100], color = 'green', lw = 0.5)
ax.scatter3D(x, y, 100, s = 5, c = 'k', zorder = 4)
ax.scatter3D(x, y, 0, s = 2, c = 'k', zorder = 4)
ax.set_zlim(0., 400.)
plt.show()

Related

Migrating from basemap to cartopy - coordinates offset?

I was happy with basemap showing some points of interest I have visited, but since basemap is dead, I am moving over to cartopy.
So far so good, I have transferred all the map features (country borders, etc.) without major problems, but I have noticed my points of interest are shifted a bit to North-East in cartopy.
Minimal example from the old basemap (showing two points of interest on the Germany-Czech border):
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
map = Basemap(projection='merc', epsg='3395',
llcrnrlon = 11.5, urcrnrlon = 12.5,
llcrnrlat = 50, urcrnrlat = 50.7,
resolution = 'f')
map.fillcontinents(color='Bisque',lake_color='LightSkyBlue')
map.drawcountries(linewidth=0.2)
lats = [50.3182289, 50.2523744]
lons = [12.1010486, 12.0906336]
x, y = map(lons, lats)
map.plot(x, y, marker = '.', markersize=1, mfc = 'red', mew = 1, mec = 'red', ls = 'None')
plt.savefig('example-basemap.png', dpi=200, bbox_inches='tight', pad_inches=0)
And the same example from cartopy - both points are slightly shifted to North-East as can be verified also e.g. via google maps:
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.pyplot as plt
ax = plt.axes(projection=ccrs.Mercator())
ax.set_extent([11.5, 12.5, 50, 50.7])
ax.add_feature(cfeature.LAND.with_scale('10m'), color='Bisque')
ax.add_feature(cfeature.LAKES.with_scale('10m'), alpha=0.5)
ax.add_feature(cfeature.BORDERS.with_scale('10m'), linewidth=0.2)
lats = [50.3182289, 50.2523744]
lons = [12.1010486, 12.0906336]
ax.plot(lons, lats, transform=ccrs.Geodetic(),
marker = '.', markersize=1, mfc = 'red', mew = 1, mec = 'red', ls = 'None')
plt.savefig('cartopy.png', dpi=200, bbox_inches='tight', pad_inches=0)
Any idea how to get cartopy plot the points on expected coordinates? Tried cartopy 0.18 and 0.20 with the same result.

annulus with scipy Delaunay

i try to draw a 3d solid that represents an annulus. I have used the scipy module and Delaunay to do the calculation.
Unfortunately the plot shows a 3d cylinder and not an annulus. Has somebody an idea how to modify the code? Is scipy the right module? Can i use Delaunay with retangular shapes?
thanks in advance!
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from scipy.spatial import Delaunay
points = 50
theta = np.linspace(0,2*np.pi,points)
radius_middle = 7.5
radius_inner = 7
radius_outer = 8
x_m_cartesian = radius_middle * np.cos(theta)
y_m_cartesian = radius_middle * np.sin(theta)
z_m_cartesian = np.zeros(points)
M_m = np.c_[x_m_cartesian,y_m_cartesian,z_m_cartesian]
x_i_cartesian = radius_inner * np.cos(theta)
y_i_cartesian = radius_inner * np.sin(theta)
z_i_cartesian = np.zeros(points)
M_i = np.c_[x_i_cartesian,y_i_cartesian,z_i_cartesian]
x1_m_cartesian = radius_middle * np.cos(theta)
y1_m_cartesian = radius_middle * np.sin(theta)
z1_m_cartesian = np.ones(points)
M1_m = np.c_[x1_m_cartesian,y1_m_cartesian,z1_m_cartesian]
x2_i_cartesian = radius_inner * np.cos(theta)
y2_i_cartesian = radius_inner * np.sin(theta)
z2_i_cartesian = np.ones(points)
M2_i = np.c_[x2_i_cartesian,y2_i_cartesian,z2_i_cartesian]
M = np.vstack((M_m,M_i,M1_m,M2_i))
# Delaunay
CH = Delaunay(M).convex_hull
x,y,z = M[:,0],M[:,1],M[:,2]
fig = plt.figure(figsize=(12,8))
ax = fig.add_subplot(111,projection='3d')
#ax.scatter(x[:,0],y[:,1],z[:,2])
ax.plot_trisurf(x,y,z,triangles=CH, shade=False, color='lightblue',lw=1, edgecolor='k')
plt.show()
As noted in the comments the convex hull is a convex shape and therefore cannot represent an annulus. However, the concept of the concave hull (also known as the alpha-shape) is probably appropriate for your needs. Basically, the alpha-shape removes from the Delaunay triangulation the triangles (tetrahedra in your 3D case) that have a circumradius greater than some value (defined by the alpha parameter).
This answer provides an implementation of the alpha-shape surface (i.e., the outer boundary) for 3D points. Using the alpha_shape_3D function from that answer, with an alpha value of 3, resulted in the figure below.
The following two lines in the code (replacing the assignment to CH and the plot function) do the job.
vertices, edges, facets = alpha_shape_3D(pos=M, alpha=3.)
ax.plot_trisurf(x,y,z,triangles=facets, shade=False, color='lightblue',lw=1, edgecolor='k')

fill oceans for basemap in 3D

I would like to fill oceans for my basemap in 3D but
ax.add_collection3d(m.drawmapboundary(fill_color='aqua'))
doesn't seem to work because the basemap drawmapboundary method doesn’t return an object supported by add_collection3d but a matplotlib.collections.PatchCollection object. Is there any workaround similar to the one done for land polygons here? Thank you!
Drawing a rectangle (polygon) below the map is one solution. Here is the working code that you may try.
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.basemap import Basemap
from matplotlib.collections import PolyCollection
map = Basemap()
fig = plt.figure()
ax = Axes3D(fig)
ax.azim = 270
ax.elev = 50
ax.dist = 8
ax.add_collection3d(map.drawcoastlines(linewidth=0.20))
ax.add_collection3d(map.drawcountries(linewidth=0.15))
polys = []
for polygon in map.landpolygons:
polys.append(polygon.get_coords())
# This fills polygons with colors
lc = PolyCollection(polys, edgecolor='black', linewidth=0.3, \
facecolor='#BBAAAA', alpha=1.0, closed=False)
lcs = ax.add_collection3d(lc, zs=0) # set zero zs
# Create underlying blue color rectangle
# It's `zs` value is -0.003, so it is plotted below land polygons
bpgon = np.array([[-180., -90],
[-180, 90],
[180, 90],
[180, -90]])
polys2 = []
polys2.append(bpgon)
lc2 = PolyCollection(polys2, edgecolor='none', linewidth=0.1, \
facecolor='#445599', alpha=1.0, closed=False)
lcs2 = ax.add_collection3d(lc2, zs=-0.003) # set negative zs value
plt.show()
The resulting plot:

cartopy: map overlay on NOAA APT image

I am working on a project trying to decode NOAA APT images, so far I reached the stage where I can get the images from raw IQ recordings from RTLSDRs. Here is one of the decoded images,
Decoded NOAA APT image this image will be used as input for the code (seen as m3.png here on)
Now I am working on overlaying map boundaries on the image (Note: Only on the left half part of the above image)
We know, the time at which the image was captured and the satellite info: position, direction etc. So, I used the position of the satellite to get the center of map projection and and direction of satellite to rotate the image appropriately.
First I tried in Basemap, here is the code
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
import numpy as np
from scipy import ndimage
im = plt.imread('m3.png')
im = im[:,85:995] # crop only the first part of whole image
rot = 198.3913296679117 # degrees, direction of sat movement
center = (50.83550180700588, 16.430852851867176) # lat long
rotated_img = ndimage.rotate(im, rot) # rotate image
w = rotated_img.shape[1]*4000*0.81 # in meters, spec says 4km per pixel, but I had to make it 81% less to get better image
h = rotated_img.shape[0]*4000*0.81 # in meters, spec says 4km per pixel, but I had to make it 81% less to get better image
m = Basemap(projection='cass',lon_0 = center[1],lat_0 = center[0],width = w,height = h, resolution = "i")
m.drawcoastlines(color='yellow')
m.drawcountries(color='yellow')
im = plt.imshow(rotated_img, cmap='gray', extent=(*plt.xlim(), *plt.ylim()))
plt.show()
I got this image as a result, which seems pretty good
I wanted to move the code to Cartopy as it is easier to install and is actively being developed. I was unable to find a similar way to set boundaries i.e. width and height in meters. So, I modified most similar example. I found a function which would add meters to longs and lats and used that to set the boundaries.
Here is the code in Cartopy,
import matplotlib.pyplot as plt
import numpy as np
import cartopy.crs as ccrs
from scipy import ndimage
import cartopy.feature
im = plt.imread('m3.png')
im = im[:,85:995] # crop only the first part of whole image
rot = 198.3913296679117 # degrees, direction of sat movement
center = (50.83550180700588, 16.430852851867176) # lat long
def add_m(center, dx, dy):
# source: https://stackoverflow.com/questions/7477003/calculating-new-longitude-latitude-from-old-n-meters
new_latitude = center[0] + (dy / 6371000.0) * (180 / np.pi)
new_longitude = center[1] + (dx / 6371000.0) * (180 / np.pi) / np.cos(center[0] * np.pi/180)
return [new_latitude, new_longitude]
fig = plt.figure()
img = ndimage.rotate(im, rot)
dx = img.shape[0]*4000/2*0.81 # in meters
dy = img.shape[1]*4000/2*0.81 # in meters
leftbot = add_m(center, -1*dx, -1*dy)
righttop = add_m(center, dx, dy)
img_extent = (leftbot[1], righttop[1], leftbot[0], righttop[0])
ax = plt.axes(projection=ccrs.PlateCarree())
ax.imshow(img, origin='upper', cmap='gray', extent=img_extent, transform=ccrs.PlateCarree())
ax.coastlines(resolution='50m', color='yellow', linewidth=1)
ax.add_feature(cartopy.feature.BORDERS, linestyle='-', edgecolor='yellow')
plt.show()
Here is the result from Cartopy, it is not as good as the result from Basemap.
I have following questions:
I found it impossible to rotate the map instead of the image, in
both basemap and cartopy. Hence I resorted to rotating the image, is
there a way to rotate the map?
How do I improve the output of cartopy? I think it is the way in
which I am calculating the extent a problem. Is there a way I can
provide meters to set the boundaries of the image?
Is there a better way to do what I am trying to do? any projection that are specific to these kind of applications?
I am adjusting the scale (the part where I decide the number of kms per pixel) manually, is there a way to do this based
on
satellite's altitude?
Any sort of input would be highly appreciated. Thank you so much for your time!
If you are interested you can find the project here.
As far as I can see, there is no ability for the underlying Proj.4 to define satellite projections with rotated perspectives (happy to be shown otherwise - I'm no expert!) (note: perhaps via ob_tran?). This is the main reason you can't do this in "native" coordinates/orientation with Basemap or Cartopy.
This question really comes down to a georeferencing problem, to which I couldn't find enough information in places like https://www.cder.dz/download/Art7-1_1.pdf.
My solution is entirely a fudge, but does get you quite close to referencing this image. I double the fudge factors are actually universal, which is a bit of an issue if you want to write general-purpose code.
Some of the fudges I had to make (trial-and-error):
adjust the satellite bearing by 3.2 degrees
adjust where the image centre is by moving it along the satellite trajectory by 10km
adjust where the image centre is by moving it perpendicularly along the satellite trajectory by 10km
scale the x and y pixel sizes by 0.62 and 0.65 respectively
use the "near-sided perspective" projection at an unrealistic satellite_height
The result is what appears to be a relatively well registered image, but as I say, seems unlikely to be generally applicable to all images received:
The code to produce this image (fairly involved, but complete):
import urllib.request
urllib.request.urlretrieve('https://i.stack.imgur.com/UBIuA.jpg', 'm3.jpg')
import matplotlib.pyplot as plt
import numpy as np
import cartopy.crs as ccrs
from scipy import ndimage
import cartopy.feature
im = plt.imread('m3.jpg')
im = im[:,85:995] # crop only the first part of whole image
rot = 198.3913296679117 # degrees, direction of sat movement
center = (50.83550180700588, 16.430852851867176) # lat long
import numpy as np
from cartopy.geodesic import Geodesic
import matplotlib.transforms as mtransforms
from matplotlib.axes import Axes
tweaked_rot = rot - 3.2
geod = Geodesic()
# Move the center along the trajectory of the satellite by 10KM
f = np.array(
geod.direct([center[1], center[0]],
180 - tweaked_rot,
10000))
tweaked_center = f[0, 0], f[0, 1]
# Move the satellite perpendicular from its proposed trajectory by 15KM
f = np.array(
geod.direct([tweaked_center[0], tweaked_center[1]],
180 - tweaked_rot + 90,
10000))
tweaked_center = f[0, 0], f[0, 1]
data_crs = ccrs.NearsidePerspective(
central_latitude=tweaked_center[1],
central_longitude=tweaked_center[0],
)
# Compute the center in data_crs coordinates.
center_lon_lat_ortho = data_crs.transform_point(
tweaked_center[0], tweaked_center[1], ccrs.Geodetic())
# Define the affine rotation in terms of matplotlib transforms.
rotation = mtransforms.Affine2D().rotate_deg_around(
center_lon_lat_ortho[0], center_lon_lat_ortho[1], tweaked_rot)
# Some fudge factors. Sorry - there are entirely application specific,
# perhaps some reading of https://www.cder.dz/download/Art7-1_1.pdf
# would enlighten these... :(
ff_x, ff_y = 0.62, 0.65
ff_x = ff_y = 0.81
x_extent = im.shape[1]*4000/2 * ff_x
y_extent = im.shape[0]*4000/2 * ff_y
img_extent = [-x_extent, x_extent, -y_extent, y_extent]
fig = plt.figure(figsize=(10, 10))
ax = plt.axes(projection=data_crs)
ax.margins(0.02)
with ax.hold_limits():
ax.stock_img()
# Uing matplotlib's image transforms if the projection is the
# same as the map, otherwise we need to fall back to cartopy's
# (slower) image resampling algorithm
if ax.projection == data_crs:
transform = rotation + ax.transData
else:
transform = rotation + data_crs._as_mpl_transform(ax)
# Use the original Axes method rather than cartopy's GeoAxes.imshow.
mimg = Axes.imshow(ax, im, origin='upper', cmap='gray',
extent=img_extent, transform=transform)
lower_left = rotation.frozen().transform_point([-x_extent, -y_extent])
lower_right = rotation.frozen().transform_point([x_extent, -y_extent])
upper_left = rotation.frozen().transform_point([-x_extent, y_extent])
upper_right = rotation.frozen().transform_point([x_extent, y_extent])
plt.plot(lower_left[0], lower_left[1],
upper_left[0], upper_left[1],
upper_right[0], upper_right[1],
lower_right[0], lower_right[1],
marker='x', color='black',
transform=data_crs)
ax.coastlines(resolution='10m', color='yellow', linewidth=1)
ax.add_feature(cartopy.feature.BORDERS, linestyle='-', edgecolor='yellow')
sat_pos = np.array(geod.direct(tweaked_center, 180 - tweaked_rot,
np.linspace(-x_extent*2, x_extent*2, 50)))
with ax.hold_limits():
plt.plot(sat_pos[:, 0], sat_pos[:, 1], transform=ccrs.Geodetic(),
label='Satellite path')
plt.plot(tweaked_center, 'ob')
plt.legend()
As you can probably tell, I got a bit carried away with this question. It is a super interesting problem, but not really a cartopy/Basemap one per-say.
Hope that helps!

Wrong annotation text location on basemap

I try to annotate sampling position name on each scatter sample location based on mercator projection. I used sampling position as annotation location with map projection coordinate. But placed annotation location is wrong position on map.
Ex. When I annotate some position in pacific ocean, annotate text placed on Japan inland.
My map is below.
Here is my code.
from mpl_toolkits.basemap import Basemap
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
from matplotlib.patches import Polygon
# create new figure, axes instances.
fig=plt.figure()
ax=fig.add_axes([0.1,0.1,0.8,0.8])
# setup mercator map projection.
m = Basemap(llcrnrlon=120.,llcrnrlat=10.,urcrnrlon=150.,urcrnrlat=44.,\
rsphere=(6378137.00,6356752.3142),\
resolution='l',projection='merc',\
lat_0=20.,lon_0=135.,lat_ts=25)
# nylat, nylon are lat/lon of Guam
nylat = 14; nylon = 140
# lonlat, lonlon are lat/lon of Busan.
lonlat = 35; lonlon = 128
# Draw Background Map
m.bluemarble()
m.drawgreatcircle(nylon,nylat,lonlon,lonlat,linewidth=2,color='b')
m.drawcoastlines()
m.fillcontinents()
# draw parallels
m.drawparallels(np.arange(10,50,10),labels=[1,1,0,1])
# draw meridians
m.drawmeridians(np.arange(120,155,10),labels=[1,1,0,1])
#m.drawmapscale(129, 15, 131, 16, 500, barstyle='fancy') drawing map scale
m.ax = ax
# Draw your dataset
lons = [130, 131, 132, 133]
lats = [21, 22, 23, 24]
Annotes = ['Sample #1', '2', '3', '4']
x, y = m(lons, lats)
#m.scatter(x1,y1,s=sizes,c=cols,marker="o",cmap=cm.cool,alpha=0.7)
m.scatter(x, y, s= 20, marker='*',color='y',alpha=0.7)
plt.annotate(Annotes[0], xy=(1110450, 1147253),xycoords='data',
xytext=(1110450, 1147253),textcoords='data',
arrowprops=dict(arrowstyle="->")
)
ax.set_title('Cruise line from Guam to Busan')
plt.show()
Thanks in advance. Any comments welcome!