Plotting lines over an image using the same projection - python

I want to make a plot using .fits files (astronomical images) and I am experiencing two issues which I think they are related:
Using this example from astropy:
from matplotlib import pyplot as plt
from astropy.io import fits
from astropy.wcs import WCS
from astropy.utils.data import download_file
fits_file = 'http://data.astropy.org/tutorials/FITS-images/HorseHead.fits'
image_file = download_file(fits_file, cache=True)
hdu = fits.open(image_file)[0]
wcs = WCS(hdu.header)
fig = plt.figure()
fig.add_subplot(111, projection=wcs)
plt.imshow(hdu.data, origin='lower', cmap='cubehelix')
plt.xlabel('RA')
plt.ylabel('Dec')
plt.show()
I can generate this image:
Now I would like to plot some points using the same coordinates as the image:
plt.scatter(85, -2, color='red')
However, when I do this:
I am ploting at the pixel coordinantes. Furthermore, the image no longer matches the frame size (although the coordinates seem fine)
Any advice on how to deal with these issues?

It is very easy to plot given coordinates. All you have to do is apply a transform.
I copied your example and added comments where I changed something and why.
from matplotlib import pyplot as plt
from astropy.io import fits
from astropy.wcs import WCS
from astropy.utils.data import download_file
fits_file = 'http://data.astropy.org/tutorials/FITS-images/HorseHead.fits'
image_file = download_file(fits_file, cache=True)
# Note that it's better to open the file with a context manager so no
# file handle is accidentally left open.
with fits.open(image_file) as hdus:
img = hdus[0].data
wcs = WCS(hdus[0].header)
fig = plt.figure()
# You need to "catch" the axes here so you have access to the transform-function.
ax = fig.add_subplot(111, projection=wcs)
plt.imshow(img, origin='lower', cmap='cubehelix')
plt.xlabel('RA')
plt.ylabel('Dec')
# Apply a transform-function:
plt.scatter(85, -2, color='red', transform=ax.get_transform('world'))
And the result is:
Note that if you want the Canvas to only show the region of the image just apply the limits again afterwards:
# Add a scatter point which is in the extend of the image:
plt.scatter(85.3, -2.5, color='red', transform=ax.get_transform('world'))
plt.ylim(0, img.shape[0])
plt.xlim(0, img.shape[1])
which gives:
A side note as well here. AstroPy has a great units support so instead of converting arcmins and arcsecs to degrees you can just define the "unit". You still need the transform though:
from astropy import units as u
x0 = 85 * u.degree + 20 * u.arcmin
y0 = -(2 * u.degree + 25 * u.arcmin)
plt.scatter(x0, y0, color='red', transform=ax.get_transform('world'))

Related

How to fix BytesIO numpy image array returning blank?

I'm trying to save a Matplotlib plot to an array using BytesIO as suggested here: Matplotlib save plot to NumPy array. Here is my code
import lightkurve
import matplotlib.pyplot as plt
import numpy as np
import io
def download(search):
lc = search.download() # downloads lightcurve as lightcurve object
if lc is not None:
fig,ax = plt.subplots()
ax.scatter(lc.time.value.tolist(), lc.flux.value.tolist(), color='k')
ax.autoscale()
ax.set_xlabel('Time (BTJD)')
ax.set_ylabel('Flux')
fig.show()
io_buf = io.BytesIO()
fig.savefig(io_buf,format="raw")
io_buf.seek(0)
img_arr = np.frombuffer(io_buf.getvalue(),dtype=np.uint8)
io_buf.close()
return img_arr
For some reason, the returned image array only contains the repeated value 255 like so: [255 255 255 ... 255 255 255] suggesting a blank image. I've tried using plt instead of fig, autoscaling the axes in case they weren't showing, and plotting instead with the Lightkurve built-in plotting function lc.plot(ax=ax) but nothing has changed. Does anyone know how to fix this?
I couldn't reproduce your bug. In fact, I ran your code (with some modifications) and the resulting image was exactly like the original image. Did you thoroughly check if your img_arr had only 255s? (e.g., np.unique(img_arr), in my case, len(np.unique(imgarr)) == 231)
import lightkurve
import matplotlib.pyplot as plt
import numpy as np
import io
def download(search):
lc = search.download() # downloads lightcurve as lightcurve object
if lc is not None:
fig,ax = plt.subplots()
ax.scatter(lc.time.value.tolist(), lc.flux.value.tolist(), color='k')
ax.autoscale()
ax.set_xlabel('Time (BTJD)')
ax.set_ylabel('Flux')
fig.show()
io_buf = io.BytesIO()
fig.savefig(io_buf,format="raw")
fig.savefig('test.png') # So I could see the dimensions of the array
io_buf.seek(0)
img_arr = np.frombuffer(io_buf.getvalue(),dtype=np.uint8)
io_buf.close()
return img_arr
# I put something random -- Next time, provide this step so others can more easily debug your code. Never touched lightkurve before
search = lightkurve.search_lightcurve('KIC 757076', author="Kepler", quarter=3)
imgarr = download(search)
fig, ax = plt.subplots()
ax.imshow(imgarr.reshape(288, -1), aspect=4, cmap='gray') # Visualizing the image from the array. Got '288' from the dimensions of the png.
Original plot:
Reconstructed plot:

librosa mel spectrogram Hz scaling issue

I am having some odd vertical scaling issues with librosa.feature.melspectrogram(). It seems that when I use librosa.load() with sr=None, the Hz scale doesn't coincide with the intended spectrographic features. To investigate this further, I looked at a pure 1,000Hz tone which I got from https://www.mediacollege.com/audio/tone/download/
import librosa
import librosa.display
import matplotlib.pyplot as plt
import numpy as np
filename = '1kHz_44100Hz_16bit_05sec.wav'
y1, sr1 = librosa.load(filename,sr=None)
y2, sr2 = librosa.load(filename)
fig, ax = plt.subplots(1,2)
S = librosa.feature.melspectrogram(y1, sr=sr1, n_mels=128)
S_DB = librosa.power_to_db(S, ref=np.max)
librosa.display.specshow(S_DB, sr=sr1, y_axis='mel', ax=ax[0]);
ax[0].title.set_text(f"sr1={sr1}\nload(filename,sr=None)")
S = librosa.feature.melspectrogram(y2, sr=sr2, n_mels=128)
S_DB = librosa.power_to_db(S, ref=np.max)
librosa.display.specshow(S_DB, sr=sr2, y_axis='mel', ax=ax[1]);
ax[1].title.set_text(f"sr2={sr2}\nload(filename)")
plt.tight_layout()
I'm not sure why the 1kHz tone is not lining up in both spectrograms. I would suspect the one with sr=None to be the more accurate as it is using the actual samplerate from the file. Would anyone know why there is a difference? The feature in the left plot is obviously not at 1kHz, but more like 800Hz or so. Thanks.

Fill oceans in basemap [duplicate]

This question already has answers here:
Plot only on continent in matplotlib
(5 answers)
Closed 5 years ago.
I am trying to plot 1x1 degree data on a matplotlib.Basemap, and I want to fill the ocean with white. However, in order for the boundaries of the ocean to follow the coastlines drawn by matplotlib, the resolution of the white ocean mask should be much higher than the resolution of my data.
After searching around for a long time I tried the two possible solutions:
(1) maskoceans() and is_land() functions, but since my data is lower resolution than the map drawn by basemap it does not look good on the edges. I do not want to interpolate my data to higher resolution either.
(2) m.drawlsmask(), but since zorder cannot be assigned the pcolormesh plot always overlays the mask.
This code
import numpy as np
import matplotlib.pyplot as plt
import mpl_toolkits.basemap as bm
#Make data
lon = np.arange(0,360,1)
lat = np.arange(-90,91,1)
data = np.random.rand(len(lat),len(lon))
#Draw map
plt.figure()
m = bm.Basemap(resolution='i',projection='laea', width=1500000, height=2900000, lat_ts=60, lat_0=72, lon_0=319)
m.drawcoastlines(linewidth=1, color='white')
data, lon = bm.addcyclic(data,lon)
x,y = m(*np.meshgrid(lon,lat))
plt.pcolormesh(x,y,data)
plt.savefig('1.png',dpi=300)
Produces this image:
Adding m.fillcontinents(color='white') produces the following image, which is what I need but to fill the ocean and not the land.
Edit:
m.drawmapboundary(fill_color='lightblue') also fills over land and can therefore not be used.
The desired outcome is that the oceans are white, while what I plotted with plt.pcolormesh(x,y,data) shows up over the lands.
I found a much nicer solution to the problem which uses the polygons defined by the coastlines in the map to produce a matplotlib.PathPatch that overlays the ocean areas. This solution has a much better resolution and is much faster:
from matplotlib import pyplot as plt
from mpl_toolkits import basemap as bm
from matplotlib import colors
import numpy as np
import numpy.ma as ma
from matplotlib.patches import Path, PathPatch
fig, ax = plt.subplots()
lon_0 = 319
lat_0 = 72
##some fake data
lons = np.linspace(lon_0-60,lon_0+60,10)
lats = np.linspace(lat_0-15,lat_0+15,5)
lon, lat = np.meshgrid(lons,lats)
TOPO = np.sin(np.pi*lon/180)*np.exp(lat/90)
m = bm.Basemap(resolution='i',projection='laea', width=1500000, height=2900000, lat_ts=60, lat_0=lat_0, lon_0=lon_0, ax = ax)
m.drawcoastlines(linewidth=0.5)
x,y = m(lon,lat)
pcol = ax.pcolormesh(x,y,TOPO)
##getting the limits of the map:
x0,x1 = ax.get_xlim()
y0,y1 = ax.get_ylim()
map_edges = np.array([[x0,y0],[x1,y0],[x1,y1],[x0,y1]])
##getting all polygons used to draw the coastlines of the map
polys = [p.boundary for p in m.landpolygons]
##combining with map edges
polys = [map_edges]+polys[:]
##creating a PathPatch
codes = [
[Path.MOVETO] + [Path.LINETO for p in p[1:]]
for p in polys
]
polys_lin = [v for p in polys for v in p]
codes_lin = [c for cs in codes for c in cs]
path = Path(polys_lin, codes_lin)
patch = PathPatch(path,facecolor='white', lw=0)
##masking the data:
ax.add_patch(patch)
plt.show()
The output looks like this:
Original solution:
You can use an array with greater resolution in basemap.maskoceans, such that the resolution fits the continent outlines. Afterwards, you can just invert the mask and plot the masked array on top of your data.
Somehow I only got basemap.maskoceans to work when I used the full range of the map (e.g. longitudes from -180 to 180 and latitudes from -90 to 90). Given that one needs quite a high resolution to make it look nice, the computation takes a while:
from matplotlib import pyplot as plt
from mpl_toolkits import basemap as bm
from matplotlib import colors
import numpy as np
import numpy.ma as ma
fig, ax = plt.subplots()
lon_0 = 319
lat_0 = 72
##some fake data
lons = np.linspace(lon_0-60,lon_0+60,10)
lats = np.linspace(lat_0-15,lat_0+15,5)
lon, lat = np.meshgrid(lons,lats)
TOPO = np.sin(np.pi*lon/180)*np.exp(lat/90)
m = bm.Basemap(resolution='i',projection='laea', width=1500000, height=2900000, lat_ts=60, lat_0=lat_0, lon_0=lon_0, ax = ax)
m.drawcoastlines(linewidth=0.5)
x,y = m(lon,lat)
pcol = ax.pcolormesh(x,y,TOPO)
##producing a mask -- seems to only work with full coordinate limits
lons2 = np.linspace(-180,180,10000)
lats2 = np.linspace(-90,90,5000)
lon2, lat2 = np.meshgrid(lons2,lats2)
x2,y2 = m(lon2,lat2)
pseudo_data = np.ones_like(lon2)
masked = bm.maskoceans(lon2,lat2,pseudo_data)
masked.mask = ~masked.mask
##plotting the mask
cmap = colors.ListedColormap(['w'])
pcol = ax.pcolormesh(x2,y2,masked, cmap=cmap)
plt.show()
The result looks like this:

Custom markers using Python (matplotlib)

I would like to know how I can generate the marker for the black colored line shown in this picture. (Source: NCEP & NOAA)
It's the marker for a storm or hurricane in standard weather maps.
I can probably generate an image file of the marker symbol. But, I am not aware of how I can tell matplotlib to use the image as a marker.
The marker looks like a 6. If this is the case, you can use a 6 as a marker as follows:
import matplotlib.pyplot as plt
x = [1,2,3,4]
y = [2,3,1,4]
plt.scatter(x,y, s= 100,marker="$6$")
plt.show()
If this is not an option, you may define your custom marker using a path. To this end, the coordinates of the path need to be known. I have invented some values below, maybe they already suit the needs here.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.path as mpath
def get_hurricane():
u = np.array([ [2.444,7.553],
[0.513,7.046],
[-1.243,5.433],
[-2.353,2.975],
[-2.578,0.092],
[-2.075,-1.795],
[-0.336,-2.870],
[2.609,-2.016] ])
u[:,0] -= 0.098
codes = [1] + [2]*(len(u)-2) + [2]
u = np.append(u, -u[::-1], axis=0)
codes += codes
return mpath.Path(3*u, codes, closed=False)
hurricane = get_hurricane()
plt.scatter([1,1,2],[1.4,2.3,2.8], s=350, marker=hurricane,
edgecolors="crimson", facecolors='none', linewidth=2)
plt.scatter([0,1,2],[1,3,1], s=150, marker=hurricane,
edgecolors="k", facecolors='none')
plt.scatter([0,1.8,3],[0,2,4], s=150, marker="o",
edgecolors="k", facecolors='none')
plt.show()

Move 3D plot to avoid clipping by margins

I'm trying to figure out how I can get the 3D matplotlib images below to plot higher on the canvas so it doesn't get clipped. Here is the code I'm using to create the plot. I couldn't find a way to attach the text file containing the Z elevations (referenced in the code below), but it is simply a 2D array containing a surface made up of values ranging between 0 and 1.
import os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from mpl_toolkits.mplot3d import Axes3D
nrow=30
ncol=100
f = open(r'C:\temp\fracEvapCume_200.txt','r')
fracEvapTS = np.loadtxt(f)
f.close()
X, Y = np.meshgrid(ncol, nrow)
Y3d, X3d = np.mgrid[0:Y, 0:X]
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.auto_scale_xyz([0, 100], [0, 30], [0, 0.2])
Y3d, X3d = np.mgrid[0:Y, 0:X]
Z = fracEvapTS
surf = ax.plot_surface(X3d, Y3d, Z, cmap='autumn', cstride=2, rstride=2)
ax.set_xlabel("X-Label")
ax.set_ylabel("Y-Label")
ax.set_zlabel("Z-Label")
ax.pbaspect = [1., .33, 0.25]
ax.dist = 7
plt.tight_layout()
plt.savefig('clipped.png')
In order to get the ax.pbaspect=[1., .33, 0.25] line to work, changes to the get_proj function inside site-packages\mpl_toolkits\mplot3d\axes3d.py were made as suggested in this post. In order to get the figure to draw larger, I added ax.dist = 7 based on this post. Lastly, based on this post I was hoping that plt.tight_layout() would roll back the margins and prevent the red/yellow surface shown below from being clipped, but that didn't work either. I'm failing to find the command that will move the image up on the canvas, thereby avoiding all of the unnecessary white space at the top of the figure and preventing the red/yellow surface from getting clipped. Is there one line of Python that will accomplish this?
after adding the line plt.tight_layout(), it made matters worse:
The problem is that your modification to site-packages\mpl_toolkits\mplot3d\axes3d.py changes the projection matrix, without changing the center of the view, messing up the position of the scene once transfomed in camera coordinates.
So when the view is zoomed (with ax.dist) then moved, the plot sometimes gets out of the canvas.
You need to replace the following line to the get_proj function in axes3d.py :
# look into the middle of the new coordinates
R = np.array([0.5, 0.5, 0.5])
By :
# look into the middle of the new coordinates
try:
R = np.array(self.pbaspect)/2
except AttributeError:
R = np.array([0.5, 0.5, 0.5])
And this should work :
PS : Code used to make the figures :
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from mpl_toolkits.mplot3d import Axes3D
nrow=30
ncol=100
X, Y = np.meshgrid(ncol, nrow)
Y3d, X3d = np.mgrid[0:Y, 0:X]
Z = np.sin(Y3d/Y)*np.sin(X3d/X)
fig = plt.figure()
for i in range(4):
ax = fig.add_subplot(2,2,i,projection='3d')
ax.auto_scale_xyz([0, 100], [0, 30], [0, 0.2])
surf = ax.plot_surface(X3d, Y3d, Z, cmap='autumn', cstride=2, rstride=2)
ax.set_xlabel("X-Label")
ax.set_ylabel("Y-Label")
ax.set_zlabel("Z-Label")
ax.pbaspect = [1., .33, 0.25]
ax.dist = 7

Categories