Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  1. 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?
  2. 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?
  3. Is there a better way to do what I am trying to do? any projection that are specific to these kind of applications?
  4. 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.

like image 750
7andahalf Avatar asked Oct 29 '22 10:10

7andahalf


1 Answers

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:

Registered image

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!

like image 179
pelson Avatar answered Nov 08 '22 13:11

pelson