Get projected coordinates from geometric coordinates - matplotlib

I have a map figure rendered with Cartopy and Matplotlib. I have a specific geometric coordinate (in lat/lon) and I would like to know the pixel coordinate closest to this geometric coordinate's projection (if it is visible), for instance to draw a graphic over the coordinate on the map.
(Note I don't want to draw with Matplotlib; I'm exporting the figure as a bitmap image and drawing in a different part of the pipeline.)
This documentation suggests it might be something like this:
import cartopy, matplotlib.pyplot
fig = matplotlib.pyplot.figure()
ax = fig.add_axes([0, 0, 1, 1], projection=cartopy.crs.Orthographic())
ax.add_feature(cartopy.feature.LAND, facecolor='black')
# Print the location of New York City in display coordinates
lon, lat = -74.0060, 40.7128
trans = cartopy.crs.Geodetic()._as_mpl_transform(ax)
x, y = trans.transform((lon, lat))
print(x, y)
# Or this way
projx, projy = ax.projection.transform_point(lon, lat, cartopy.crs.Geodetic())
x, y = ax.transData.transform((projx, projy))
print(x, y)
Though interestingly, if I plot this point, the figure centers on and zooms into Manhattan, and then the output display coordinates are indeed in the center of the figure at (640, 480).
matplotlib.pyplot.plot(lon, lat, marker='o', color='red', markersize=12,
alpha=0.7, transform=cartopy.crs.Geodetic())

I just found that the transforms are not properly set until the figure is in its final state. So the key is to first draw the figure,
fig.canvas.draw()
or at least apply the aspect properly.
ax.apply_aspect()
Then you will get the correct pixel coordinates out,
import matplotlib.pyplot as plt
import cartopy
import cartopy.crs as ccrs
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1], projection=ccrs.PlateCarree())
ax.add_feature(cartopy.feature.LAND, facecolor='black')
ax.set_global()
# before being able to call any of the transforms, the figure needs to be drawn
fig.canvas.draw()
# or
# ax.apply_aspect()
# Print the location of New York City in display coordinates
lon, lat = -74.0060, 40.7128
trans = ccrs.PlateCarree()._as_mpl_transform(ax)
x, y = trans.transform_point((lon, lat))
print(x,y)
plt.show()
This prints:
188.43377777777778 312.3783111111111
Note that those coordinates refer to the pixels from the lower left corner.

In my sample code I failed to specify the extent of the map. If I add
ax.set_global()
then the transformed coordinates are sensible.
I presented two ways to compute the transformed coordinates, but the way with _as_mpl_transform() seems to return the center point when New York City is not visible. The way with ax.projection.transform_point() returns NaN when off-screen.

Related

Lat/long transformed to OSGB has an offset when plotted

If I take a set of lat/long points, convert them to OSGB crs using cartopy's "transform_points" method and then plot them, they are offset compared to plotting the lat/long directly. If I transform them back to lat/long, they plot ok. If I convert them to UTM coords they plot ok. Am I missing something?
Example code:
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
from cartopy.io.img_tiles import OSM
# set up figure and background map tile
fig = plt.figure()
ax = plt.axes(projection=OSM().crs)
ax.set_extent((-2.473911, -2.410147, 50.567006, 50.605896))
ax.add_image(imagery, 14)
# Plot some test points using lat/long (PlateCarree) crs
test_lonlat = np.array([[-2.464482, -2.432523, -2.437892], [50.593243, 50.596390, 50.573177]])
plt.plot(test_lonlat[0], test_lonlat[1], 'r:+', transform=ccrs.PlateCarree())
# Transform to OSGB coords and plot
test_OS = ccrs.OSGB().transform_points(ccrs.PlateCarree(), test_lonlat[0], test_lonlat[1])
plt.plot(test_OS[:, 0], test_OS[:, 1], 'kx-', transform=ccrs.OSGB())
plt.show()
Thanks for any advice.
Simpler example using single point
fig = plt.figure(figsize=[7, 7])
ax = plt.axes(projection=OSM().crs)
ax.set_extent((-2.435, -2.405, 50.575, 50.595))
ax.add_image(OSM(), 14)
# Simpler test of OSGB crs
# Take point at end of outer breakwater: OS (370783, 76226) or lat/long (-2.414343, 50.584978)
point_os = [370774.0, 76221.0]
point_lonlat = [-2.414278, 50.584971]
# Plot each on OSM tile - both look ok but small error
plt.plot(point_os[0], point_os[1], 'r+', transform=ccrs.OSGB(), markersize=15)
plt.plot(point_lonlat[0], point_lonlat[1], 'kx', transform=ccrs.PlateCarree(), markersize=10)
# Convert lat/long to OSGB and plot - now offset by ~50 m to NW
point_os_new = ccrs.OSGB().transform_point(point_lonlat[0], point_lonlat[1], ccrs.PlateCarree())
plt.plot(point_os_new[0], point_os_new[1], 'm^', transform=ccrs.OSGB(), markersize=10)
# Print both sets of OS coords
print(f'Original point: {point_os}')
print(f'Transformed point: {point_os_new}')
When you create an axes to plot with this line of code
ax = plt.axes(projection=OSM().crs)
you are expected to use cartopy.crs.Mercator projection coordinates to plot on it as OSM().crs is Mercator.
This part of your code:
# Transform to OSGB coords and plot
test_OS = ccrs.OSGB().transform_points(ccrs.PlateCarree(), \
test_lonlat[0], test_lonlat[1])
plt.plot(test_OS[:, 0], test_OS[:, 1], 'kx-', transform=ccrs.OSGB())
uses OSGB coordinates to plot on Mercator projection. That's the wrong part.
The correct lines of code should be
test2_OS = OSM().crs.transform_points(ccrs.PlateCarree(), \
test_lonlat[0], test_lonlat[1])
ax.plot(test2_OS[:, 0], test2_OS[:, 1], 'kx-', transform=OSM().crs)
Edit1
The tile images in my plot above is OSM(), via ax.add_image(OSM(), 14).
The problems the OP faces is not using matching coordinate systems between the data and the axes, or wrong values.

is there a way to use matplotlib.patches.Wedge's radius in km instead of degrees?

I currently want to draw a sector-like wedge in Cartopy, so I look up the matplotlib.patches.Wedge method. It's almost the function I need, but the unit of the parameter radius it needs is in degrees, rather than kilometers.
Is there a way to use matplotlib.patches.Wedge method in kilometers rather than degrees?
Thanks.
my idea image here
I assume you saying "rather than degrees" means you're working with a position in lon/lat. The easiest way is going to be to convert your lon/lat center point to a location on the map. We'll also actually be using the radius in meters when plotting:
import matplotlib.pyplot as plt
from matplotlib.patches import Wedge
import cartopy.crs as ccrs
proj = ccrs.LambertConformal()
latlon_proj = ccrs.PlateCarree()
lat = 18
lon = 140
radius = 500 # In km
x, y = proj.transform_point(lon, lat, src_crs=latlon_proj)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=proj)
ax.plot(x, y, 'ro')
# Here we need to convert radius to meters
ax.add_patch(Wedge((x, y), r=radius * 1000, theta1=0, theta2=90))
ax.coastlines()

Is there a way to extract the pixel co-ordinates of a plotted line in matplotlib

Similar to in this StackOverflow post, I understand that it is possible to extract the pixel co-ordinates from points plotted in a pyplot figure.
How to get pixel coordinates for Matplotlib-generated scatterplot?
However, what if we plotted a line between each of those points and wanted to get the location of all the pixels of not just those plotted dots, but all pixels that make up the line.
Is this something that is possible with matplotlib?
A line isn't made up of pixels. The pixels in its trajectory are modified taking line width and antialiasing into account. Drawing a line with default settings and zooming in on the image looks like the image below. Very few pixels get the full 100% of the given color. Lots of pixels are changed.
Depending on your final goal, you could calculate pixel coordinates using the method described in the post you linked (note that the pixels on a saved image can deviate a bit from the pixels on-screen). And then use e.g. Bresenham's line algorithm to find the coordinates of points in-between. Note that a naive Bresenham's algorithm would draw a 45 degree line much thinner looking than a horizontal line. On a modern screen a one-pixel wide line would be almost invisible.
Here is a possible Bresenham-like interpretation of the linked code:
import numpy as np
import matplotlib.pyplot as plt
def points_in_line(x0, y0, x1, y1):
dx = np.round(np.abs(x1 - x0))
dy = np.round(np.abs(y1 - y0))
steps = int(np.round(max(dx, dy))) + 1
return np.vstack([np.linspace(x0, x1, steps), np.linspace(y0, y1, steps)]).T
fig, ax = plt.subplots()
points, = ax.plot([0, 1, 2, 4, 5, 6, 9], [0, 5, 3, 2, 2, 9, 8], 'b-')
ax.axis([-1, 10, -1, 10])
# Get the x and y data and transform them into pixel coordinates
x, y = points.get_data()
xy_pixels = ax.transData.transform(np.vstack([x, y]).T)
x_pix, y_pix = xy_pixels.T
# find all points in each line
all_pix = [points_in_line(x0, y0, x1, y1) for x0, y0, x1, y1 in zip(x_pix[:-1], y_pix[:-1], x_pix[1:], y_pix[1:])]
all_x_pix, all_y_pix = np.concatenate(all_pix).T
# In matplotlib, 0,0 is the lower left corner, whereas it's usually the upper
# left for most image software, so we'll flip the y-coords...
width, height = fig.canvas.get_width_height()
all_y_pix = height - all_y_pix
print('Coordinates of the lines in pixel coordinates...')
for xp, yp in zip(all_x_pix, all_y_pix):
print(f'{x:0.2f}\t{y:0.2f}')
# save the figure with its current DPI
fig.savefig('test.png', dpi=fig.dpi)

how to add variable error bars to scatter plot points with shared axes in python matplotlib

I have generated a plot that shows a topographic profile with points along the profile that represent dated points. However, these dated points also have symmetric uncertainty values/error bars (that typically vary for each point).
In this example, I treat non-dated locations as 'np.nan'. I would like to add uncertainty values to the y2 axis (Mean Age) with defined uncertainty values as y2err.
Everytime I use the ax2.errorbar( ... ) line, my graph is squeezed and distorted.
import numpy as np
import matplotlib.pyplot as plt
fig, ax1 = plt.subplots()
#Longitude = x; Elevation = y
x = (-110.75696,-110.75668,-110.75640,-110.75612,-110.75584,-110.75556,-110.75528)
y = (877,879,878,873,871,872,872)
ax1.plot(x, y)
ax1.set_xlabel('Longitude')
# Make the y-axis label, ticks and tick labels match the line color.
ax1.set_ylabel('Elevation', color='b')
ax1.tick_params('y', colors='b')
ax2 = ax1.twinx()
# Mean Age, np.nan = 0.0
y2 = (np.nan,20,np.nan,np.nan,np.nan,np.nan,np.nan)
y2err = (np.nan,5,np.nan,np.nan,np.nan,np.nan,np.nan)
ax2.scatter(x, y2, color='r')
#add error bars to scatter plot points
# (??????) ax2.errorbar(x, y, y2, y2err, capsize = 0, color='black')
ax2.set_ylim(10,30)
ax2.set_ylabel('Mean Age', color='r')
ax2.tick_params('y', colors='r')
fig.tight_layout()
plt.show()
If I do not apply the ax2.errorbar... line my plot looks like the first image, which is what I want but with the points showing uncertainty values (+/- equal on both side of point in the y-axis direction).
Plot of Elevation vs Age without error bars
When I use the ax2.errorbar line it looks like the second image:
Plot when using ax2.errorbar line
Thanks!

How to hide contour lines / data from a specific area on Basemap

I am working some meteorological data to plot contour lines on a basemap. The full working example code I have done earlier is here How to remove/omit smaller contour lines using matplotlib. All works fine and I don’t complain with the contour plot. However there is a special case that I have to hide all contour lines over a specific region (irregular lat & lon) on a Basemap.
The only possible solution I can think of is to draw a ploygon lines over a desired region and fill with the color of same as Basemap. After lot of search I found this link How to draw rectangles on a Basemap (code below)
from mpl_toolkits.basemap import Basemap
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
def draw_screen_poly( lats, lons, m):
x, y = m( lons, lats )
xy = zip(x,y)
poly = Polygon( xy, facecolor='red', alpha=0.4 )
plt.gca().add_patch(poly)
lats = [ -30, 30, 30, -30 ]
lons = [ -50, -50, 50, 50 ]
m = Basemap(projection='sinu',lon_0=0)
m.drawcoastlines()
m.drawmapboundary()
draw_screen_poly( lats, lons, m )
plt.show()
It seems to work partially. However, I want to draw a region which is irregular.
Any solution is appreciated.
Edit: 1
I have understood where the problem is. It seems that any colour (facecolor) filled within the polygon region does not make it hide anything below. Always it is transparent only, irrespective of alpha value used or not. To illustrate the problem, I have cropped the image which has all three regions ie. contour, basemap region and polygon region. Polygon region is filled with red colour but as you can see, the contour lines are always visible. The particular line I have used in the above code is :-
poly = Polygon(xy, facecolor='red', edgecolor='b')
Therefore the problem is not with the code above. It seem the problem with the polygon fill. But still no solution for this issue. The resulting image (cropped image) is below (See my 2nd edit below the attached image):-
Edit 2:
Taking clue from this http://matplotlib.1069221.n5.nabble.com/Clipping-a-plot-inside-a-polygon-td41950.html which has the similar requirement of mine, I am able to remove some the data. However, the removed data is only from outside of polygon region instead of within. Here is the code I have taken clue from:-
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import RegularPolygon
data = np.arange(100).reshape(10, 10)
fig = plt.figure()
ax = fig.add_subplot(111)
ax.contourf(data)
poly = RegularPolygon([ 0.5, 0.5], 6, 0.4, fc='none',
ec='k', transform=ax.transAxes)
for artist in ax.get_children():
artist.set_clip_path(poly)
Now my question is that what command is used for removing the data within the polygon region?
Didn't noticed there was a claim on this so I might just give the solution already proposed here. You can tinker with the zorder to hide stuff behind your polygon:
import matplotlib
import matplotlib.mlab as mlab
import matplotlib.pyplot as plt
matplotlib.rcParams['xtick.direction'] = 'out'
matplotlib.rcParams['ytick.direction'] = 'out'
delta = 0.025
x = np.arange(-3.0, 3.0, delta)
y = np.arange(-2.0, 2.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = mlab.bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0)
Z2 = mlab.bivariate_normal(X, Y, 1.5, 0.5, 1, 1)
# difference of Gaussians
Z = 10.0 * (Z2 - Z1)
# Create a simple contour plot with labels using default colors. The
# inline argument to clabel will control whether the labels are draw
# over the line segments of the contour, removing the lines beneath
# the label
fig = plt.figure()
ax = fig.add_subplot(111)
CS = plt.contour(X, Y, Z,zorder=3)
plt.clabel(CS, inline=1, fontsize=10)
plt.title('Simplest default with labels')
rect1 = matplotlib.patches.Rectangle((0,0), 2, 1, color='white',zorder=5)
ax.add_patch(rect1)
plt.show()
, the result is: