"""
Contains code that performs "localization," or the mapping between observation
pixel coordinates and real-world latitude/longitude coordinates.
PDSC uses the convention that pixel space coordinates have their origin at the
top left of the image, with the positive :math:`x`-direction corresponding to
increasing columns and the positive :math:`y`-direction corresponding to
increasing rows. Row and column indices start at 0. The mapping from line/sample
to row/column is instrument-specific, but typically lines are rows and samples
are columns.
.. Warning::
The localization provided by PDSC is not intended to be the most accurate
localization possible, but rather the most accurate localization achievable
*using only information available within PDS cumulative index files*.
Therefore, PDSC localization often relies on assumptions that introduce
errors whose magnitudes vary across instruments.
"""
from __future__ import division
from future.utils import with_metaclass
import abc
import numpy as np
from scipy.ndimage import zoom
from scipy.optimize import fmin
from sklearn.neighbors import DistanceMetric
# Requires geographiclib-1.49
from geographiclib.geodesic import Geodesic
from .util import registerer, standard_progress_bar
# https://tharsis.gsfc.nasa.gov/geodesy.html
MARS_RADIUS_M = 3396200.
MARS_FLATTENING = 1.0 / 169.8
LOCALIZERS = {}
register_localizer = registerer(LOCALIZERS)
"""
A decorator that can be used to register a class or function that constructs a
localizer to a particular instrument.
:param instrument: PDSC instrument name
:return: decorator that registers target to given instrument
See :ref:`Extending PDSC` for more details.
"""
[docs]def geodesic_distance(latlon1, latlon2, radius=MARS_RADIUS_M):
"""
Computes the geodesic distance on a spherical body between two points
:param latlon1: a pair containing the latitude and east longitude (in
radians) of the first point
:param latlon2: a pair containing the latitude and east longitude (in
radians) of the second point
:param radius: the radius (in meters) of the spherical body for which
distance is computed (defaults to `mean Mars equatorial radius
<https://tharsis.gsfc.nasa.gov/geodesy.html>`_)
:return: geodesic distance (in meters) between the two given points
>>> import numpy as np
>>> geodesic_distance((0, 0), (0, np.pi))
10669476.970121656
"""
haversine = DistanceMetric.get_metric('haversine')
return float(radius*haversine.pairwise([latlon1], [latlon2]))
[docs]def latlon2unit(latlon):
"""
Converts a latitude, longitude pair into a vector representing that point on
a unit circle
:param latlon: a pair containing the latitude and east longitude (in
degrees) of a point on a unit sphere
:return: the Cartesian coordinates of the point on a unit sphere
"""
llrad = np.deg2rad(latlon)
sinll = np.sin(llrad)
cosll = np.cos(llrad)
return np.array([
cosll[0]*cosll[1],
cosll[0]*sinll[1],
sinll[0]
])
[docs]def xyz2latlon(xyz):
"""
Converts a point in Cartesian coordinates to the latitude and longitude of
that point projected onto a unit sphere
:param xyz: Cartesian coordinates of a nonzero point
:return: latitude and east longitude (in degrees) of the point projected
onto a unit sphere
>>> xyz2latlon((0, 0, 1))
array([90., 0.])
>>> xyz2latlon((1, 0, 0))
array([0., 0.])
>>> xyz2latlon((0, 0, 0))
Traceback (most recent call last):
...
ValueError: Point must be nonzero
"""
norm = np.linalg.norm(xyz)
if norm == 0:
raise ValueError('Point must be nonzero')
x, y, z = (xyz / norm)
return np.rad2deg([
np.arcsin(z),
np.arctan2(y, x)
])
[docs]class Localizer(with_metaclass(abc.ABCMeta, object)):
"""
Base class for all localizers
Subclasses need only implement
:py:meth:`~pdsc.localization.Localizer.pixel_to_latlon` and the reverse
translation :py:meth:`~pdsc.localization.Localizer.latlon_to_pixel` is
derived from it.
"""
BODY_RADIUS = MARS_RADIUS_M
"""
Radius of the observed body (defaults to `mean Mars equatorial radius
<https://tharsis.gsfc.nasa.gov/geodesy.html>`_)
"""
DEFAULT_RESOLUTION_M = 0.1
"""
Default resolution (in meters) when optimizing the reverse mapping from
latitude and longitude back to pixel coordinates via
:py:meth:`~pdsc.localization.Localizer.latlon_to_pixel`
"""
NORMALIZED_PIXEL_SPACE = False
"""
If ``True``, pixel coordinates are defined such that (0, 0) is the top left
corner of the image and (1, 1) is the bottom right. Otherwise, (0, 0) is the
top left and (rows, cols) is the bottom right
"""
@abc.abstractproperty
def observation_width_m(self):
"""
Total observation width (cross-track) in meters
"""
pass # pragma: no cover
@abc.abstractproperty
def observation_length_m(self):
"""
Total observation length (along-track) in meters
"""
pass # pragma: no cover
[docs] @abc.abstractmethod
def pixel_to_latlon(self, row, col):
"""
Converts pixel coordinates to latitude and longitude coordinates within
an observation
:param row: image row, starting at 0 at the top of the image
:param col: image column, starting at 0 at the left of the image
.. Note::
If :py:attr:`~Localizer.NORMALIZED_PIXEL_SPACE` for this localizer
is ``False``, then the pixel coordinates range from zero to one less
than the number of total rows/columns in the image. Otherwise, the
pixel coordinates range from zero to one along each dimension.
"""
pass # pragma: no cover
[docs] def latlon_to_pixel(self, lat, lon, resolution_m=None, resolution_pix=0.1):
"""
Converts a latitude and longitude location to pixel coordinates within
an observation
:param lat:
latitude (in degrees)
:param lon:
east longitude (in degrees)
:param resolution_m:
the resolution (in meters) when optimizing the mapping from latitude
and longitude to pixel coordinates; if ``None``, the value defaults
to :py:attr:`~Localizer.DEFAULT_RESOLUTION_M`
:param resolution_pix:
the resolution (in pixels) when optimizing the mapping from latitude
and longitude to pixel coordinates
:return: the tuple containing the row and column of the specified
location to within the stricter of the two resolution requirements
"""
if resolution_m is None: resolution_m = self.DEFAULT_RESOLUTION_M
loc = np.deg2rad([lat, lon])
def f(u):
loc_u = np.deg2rad(self.pixel_to_latlon(*u))
return geodesic_distance(loc, loc_u, self.BODY_RADIUS)
u0 = (0, 0)
ustar = fmin(f, u0, xtol=resolution_pix, ftol=resolution_m, disp=False)
return tuple(ustar)
[docs]class GeodesicLocalizer(Localizer):
"""
The :py:class:`GeodesicLocalizer` is a type of localizer that is used when
observation locations are described in terms of a center latitude/longitude
and a line-of-flight direction. This localizer assumes that along-track
pixels in the center column of the observaton roughly follow a geodesic path
in the direction of flight and cross-track pixels in each row are
perpendicular to this path.
"""
BODY = Geodesic(MARS_RADIUS_M, MARS_FLATTENING)
"""
A :py:class:`~geographiclib.geodesic.Geodesic` object describing the target
body
"""
def __init__(self, center_row, center_col, center_lat, center_lon,
n_rows, n_cols, pixel_height_m, pixel_width_m,
north_azimuth_deg, flight_direction=1):
"""
:param center_row:
the center row of the observation
:param center_col:
the center column of the observation
:param center_lat:
the latitude (in degrees) of the pixel at the center of the
observation
:param center_lat:
the longitude (east, in degrees) of the pixel at the center of the
observation
:param n_rows:
the total number of rows in the observation
:param n_cols:
the total number of columns in the observation
:param pixel_height_m:
the pixel height (in meters)
:param pixel_width_m:
the pixel width (in meters)
:param north_azimuth_deg:
the clockwise angle (in degrees) from a vector that points 90
degrees counter-clockwise from the line-of-flight direction to north
:param flight_direction:
a multiplicative factor indicating the direction of flight relative
to the :math:`y`-direction in pixel space; if
``flight_direction=1``, then the flight direction is from the the
top down, whereas ``flight_direction=-1`` indicates a bottom-up
direction of flight
.. Note::
If :py:attr:`~Localizer.NORMALIZED_PIXEL_SPACE` for this localizer
is ``True``, then the pixel coordinates range from zero to one.
Consequently, the attributes :py:attr:`~GeodesicLocalizer.n_cols`
and :py:attr:`~GeodesicLocalizer.n_rows` are both equal to one, and
the attributes :py:attr:`~GeodesicLocalizer.pixel_width_m` and
:py:attr:`~GeodesicLocalizer.pixel_height_m` are both the width and
height of the *entire* observation.
"""
if n_rows <= 0: raise ValueError('No image rows')
if n_cols <= 0: raise ValueError('No image columns')
if pixel_height_m <= 0: raise ValueError('Negative pixel height')
if pixel_width_m <= 0: raise ValueError('Negative pixel width')
self.center_row = center_row
self.center_col = center_col
self.center_lat = center_lat
self.center_lon = center_lon
self.n_rows = n_rows
self.n_cols = n_cols
self.pixel_height_m = pixel_height_m
self.pixel_width_m = pixel_width_m
self.north_azimuth_deg = north_azimuth_deg
self.flight_direction = flight_direction
self._height = None
self._width = None
@property
def observation_width_m(self):
if self._width is None:
self._width = self.pixel_width_m*self.n_cols
return self._width
@property
def observation_length_m(self):
if self._height is None:
self._height = self.pixel_height_m*self.n_rows
return self._height
[docs] def pixel_to_latlon(self, row, col):
x_m = (col - self.center_col) * self.pixel_width_m
y_m = (row - self.center_row) * self.pixel_height_m
y_m *= self.flight_direction
flight_line_point = self.BODY.Direct(
self.center_lat, self.center_lon,
90 - self.north_azimuth_deg,
y_m
)
cross_line_point = self.BODY.Direct(
flight_line_point['lat2'],
flight_line_point['lon2'],
flight_line_point['azi2'] - 90,
x_m
)
return cross_line_point['lat2'], cross_line_point['lon2']
[docs] def location_mask(self, subsample_rows=10, subsample_cols=25,
reinterpolate=True, verbose=False):
"""
Compute a latitude and longitude for every pixel in an observation (with
subsampling/reinterpolation to increase efficiency)
:param subsample_rows: only compute location once every this many rows
:param subsample_cols: only compute location once every this many
columns
:param reinterpolate: if subsampling is used, reinterpolate values for
skipped pixels
:param verbose: if ``True``, display a progress bar
:return: a :py:class:`numpy.array` containing the latitude and east
longitude (in degrees) along the last dimension for every pixel in
the image, modulo subsampling
.. Warning::
This function is experimental and the reinterpolation step does not
correctly handle discontinuities that arise near the "date line" or
the poles.
"""
nrows = int(np.ceil(self.n_rows // subsample_rows))
ncols = int(np.ceil(self.n_cols // subsample_cols))
progress = standard_progress_bar('Computing Location Mask', verbose)
L = np.array([
[self.pixel_to_latlon(r, c)
for c in np.linspace(0, self.n_cols - 1, ncols)]
for r in progress(np.linspace(0, self.n_rows - 1, nrows))
])
if reinterpolate:
zoom_factor = (
float(self.n_rows) / L.shape[0],
float(self.n_cols) / L.shape[1]
)
L = np.dstack([
zoom(L[..., 0], zoom_factor, order=1, mode='nearest'),
zoom(L[..., 1], zoom_factor, order=1, mode='nearest')
])
return L
[docs]class FourCornerLocalizer(GeodesicLocalizer):
"""
The :py:class:`FourCornerLocalizer` is a type of localizer that is used when
observation locations are described using the latitude and longitude of the
four corners of the observation.
"""
def __init__(self, corners, n_rows, n_cols, flight_direction):
"""
:param corners:
an ordered collection of the four image corners, each a latitude and
east longitude pair (in degrees); the order of the corners is:
- top left
- bottom left
- bottom right
- top right
:param n_rows: the number of rows in the image
:param n_cols: the number of columns in the image
:param flight_direction:
a multiplicative factor indicating the direction of flight relative
to the :math:`y`-direction in pixel space; if
``flight_direction=1``, then the flight direction is from the the
top down, whereas ``flight_direction=-1`` indicates a bottom-up
direction of flight
"""
if n_rows <= 0: raise ValueError('No image rows')
if n_cols <= 0: raise ValueError('No image columns')
self.n_rows = n_rows
self.n_cols = n_cols
self.flight_direction = flight_direction
self.corners = np.asarray(corners)
self.corner_matrix = np.array([
[latlon2unit(corners[0]), latlon2unit(corners[3])],
[latlon2unit(corners[1]), latlon2unit(corners[2])],
])
corners = np.deg2rad(corners)
self.pixel_height_m = (
(
geodesic_distance(corners[0], corners[3]) +
geodesic_distance(corners[1], corners[2])
) / (2*n_rows)
)
self.pixel_width_m = (
(
geodesic_distance(corners[0], corners[1]) +
geodesic_distance(corners[2], corners[3])
) / (2*n_cols)
)
self._height = None
self._width = None
[docs] def pixel_to_latlon(self, row, col):
# Use bi-linear interpolation
C = self.corner_matrix
dx = np.array([self.n_cols - col, col])
dy = np.array([self.n_rows - row, row])
interpolated = np.array([
np.dot(dx, np.dot(C[..., dim], dy.T))
for dim in range(3)
]) / float(self.n_rows*self.n_cols)
return tuple(xyz2latlon(interpolated))
[docs]class MapLocalizer(Localizer):
"""
The :py:class:`MapLocalizer` supports map-projected observations.
Specifically, it supports the EQUIRECTANGULAR and POLAR STEREOGRAPHIC
projection types used for HiRISE observations.
"""
MARS_RADIUS_POLAR = 3376200
"""
The Mars polar radius used for `HiRISE map projections
<https://hirise-pds.lpl.arizona.edu/PDS/CATALOG/DSMAP.CAT>`_
"""
MARS_RADIUS_EQUATORIAL = 3396190
"""
The Mars equatorial radius used for `HiRISE map projections
<https://hirise-pds.lpl.arizona.edu/PDS/CATALOG/DSMAP.CAT>`_
"""
def __init__(self, proj_type, proj_latitude, proj_longitude,
map_scale, row_offset, col_offset, lines, samples):
"""
:param proj_type: map projection type
:param proj_latitude: projection center latitude
:param proj_longitude: projection center longitude
:param map_scale: projection map scale (meters)
:param row_offset: projection row offset
:param col_offset: projection col offset
:param lines: total observation lines (rows)
:param samples: total observation samples (columns)
See https://hirise-pds.lpl.arizona.edu/PDS/CATALOG/DSMAP.CAT for a
further description of these parameters.
"""
self.proj_type = proj_type
self.proj_latitude = np.deg2rad(proj_latitude)
self.proj_longitude = np.deg2rad(proj_longitude)
self.map_scale = map_scale
self.row_offset = row_offset
self.col_offset = col_offset
self.lines = lines
self.samples = samples
self._width = None
self._height = None
a = self.MARS_RADIUS_POLAR*np.cos(self.proj_latitude)
b = self.MARS_RADIUS_EQUATORIAL*np.sin(self.proj_latitude)
self.R = (
(self.MARS_RADIUS_POLAR*self.MARS_RADIUS_EQUATORIAL) /
np.sqrt(a**2 + b**2)
)
self.cos_proj_lat = np.cos(self.proj_latitude)
def _equirect_pixel_to_latlon(self, row, col):
x = (col - self.col_offset)*self.map_scale
y = -(row - self.row_offset)*self.map_scale
return (
np.rad2deg(y / self.R),
np.rad2deg(
self.proj_longitude + x / (self.R*self.cos_proj_lat)
)
)
def _equirect_latlon_to_pixel(self, lat, lon):
lat_rad = np.deg2rad(lat)
lon_rad = np.deg2rad(lon % 360.)
x = self.R*(lon_rad - self.proj_longitude)*self.cos_proj_lat
y = self.R*lat_rad
row = (-y / self.map_scale) + self.row_offset
col = (x / self.map_scale) + self.col_offset
return row, col
def _polar_pixel_to_latlon(self, row, col):
x = (col - self.col_offset)*self.map_scale
y = -(row - self.row_offset)*self.map_scale
P = np.sqrt(x**2 + y**2)
C = 2*np.arctan(P / (2*self.MARS_RADIUS_POLAR))
lon = np.rad2deg(
self.proj_longitude +
np.arctan2(x, -np.sign(self.proj_latitude)*y)
)
lat = np.rad2deg(np.arcsin(
np.cos(C)*np.sin(self.proj_latitude) +
y*np.sin(C)*np.cos(self.proj_latitude)/P
))
return lat, lon
def _polar_latlon_to_pixel(self, lat, lon):
lat_rad = np.deg2rad(lat)
lon_rad = np.deg2rad(lon % 360.)
T = np.tan((np.pi / 4.0) - np.abs(lat_rad / 2.0))
A = 2*self.MARS_RADIUS_POLAR*T
x = A*np.sin(lon_rad - self.proj_longitude)
y = -A*np.cos(lon_rad - self.proj_longitude)*np.sign(self.proj_latitude)
row = (-y / self.map_scale) + self.row_offset
col = (x / self.map_scale) + self.col_offset
return row, col
@property
def observation_width_m(self):
if self._width is None:
self._width = self.samples*self.map_scale
return self._width
@property
def observation_length_m(self):
if self._height is None:
self._height = self.lines*self.map_scale
return self._height
[docs] def pixel_to_latlon(self, row, col):
if self.proj_type == 'EQUIRECTANGULAR':
return self._equirect_pixel_to_latlon(row, col)
elif self.proj_type == 'POLAR STEREOGRAPHIC':
return self._polar_pixel_to_latlon(row, col)
else:
raise ValueError('Unknown projection type "%s"' % self.proj_type)
[docs] def latlon_to_pixel(self, lat, lon):
if self.proj_type == 'EQUIRECTANGULAR':
return self._equirect_latlon_to_pixel(lat, lon)
elif self.proj_type == 'POLAR STEREOGRAPHIC':
return self._polar_latlon_to_pixel(lat, lon)
else:
raise ValueError('Unknown projection type "%s"' % self.proj_type)
[docs]@register_localizer('ctx')
class CtxLocalizer(GeodesicLocalizer):
"""
A localizer for the CTX instrument (subclass of
:py:class:`GeodesicLocalizer`)
"""
DEFAULT_RESOLUTION_M = 1e-3
"""
Sets the default resolution for CTX localization
"""
BODY = Geodesic(MARS_RADIUS_M, 0.0) # Works better assuming sphere
"""
Uses a Geodesic model for CTX that assumes Mars is spherical, which seems to
work better in practice.
"""
def __init__(self, metadata):
"""
:param metadata:
"ctx" :py:class:`~pdsc.metadata.PdsMetadata` object
"""
flipped_na = (180 - metadata.north_azimuth
if metadata.usage_note == 'F' else metadata.north_azimuth)
super(CtxLocalizer, self).__init__(
metadata.lines / 2.0, metadata.samples / 2.0,
metadata.center_latitude, metadata.center_longitude,
metadata.lines, metadata.samples,
metadata.image_height / metadata.lines,
metadata.image_width / metadata.samples,
flipped_na, -1
)
[docs]@register_localizer('themis_vis')
@register_localizer('themis_ir')
class ThemisLocalizer(GeodesicLocalizer):
"""
A localizer for the THEMIS VIS and IR instruments (subclass of
:py:class:`GeodesicLocalizer`)
"""
DEFAULT_RESOLUTION_M = 1e-3
"""
Sets the default resolution for THEMIS localization
"""
def __init__(self, metadata):
"""
:param metadata:
"themis_ir" or "themis_vis" :py:class:`~pdsc.metadata.PdsMetadata`
object
"""
super(ThemisLocalizer, self).__init__(
metadata.lines / 2.0, metadata.samples / 2.0,
metadata.center_latitude, metadata.center_longitude,
metadata.lines, metadata.samples,
metadata.pixel_aspect_ratio*metadata.pixel_width,
metadata.pixel_width,
metadata.north_azimuth, 1
)
[docs]@register_localizer('hirise_edr')
class HiRiseLocalizer(GeodesicLocalizer):
"""
A localizer for the HiRISE EDR observations (subclass of
:py:class:`GeodesicLocalizer`)
"""
DEFAULT_RESOLUTION_M = 1e-6
"""
Sets the default resolution for HiRISE EDR localization
"""
# Each CCD is 2048 pixels across, but they overlap
# by 48 pixels.
CCD_TABLE = {
'RED0': -9000,
'RED1': -7000,
'RED2': -5000,
'RED3': -3000,
'RED4': -1000,
'RED5': 1000,
'RED6': 3000,
'RED7': 5000,
'RED8': 7000,
'RED9': 9000,
'IR10': -1000,
'IR11': 1000,
'BG12': -1000,
'BG13': 1000,
}
"""
A mapping from HiRISE CCDs to pixel offsets from the center of the
observation. Each CCD is 2048 pixels across, but they overlap by 48 pixels.
See Figure 2.1.b in the `HiRISE EDR SIS
<https://hirise.lpl.arizona.edu/pdf/HiRISE_EDR_SIS.pdf>`_.
"""
CHANNEL_OFFSET = {
0: 512,
1: -512,
}
"""
Each HiRISE CCD is split into two channels, each 1024 pixels of the full
2048-pixel CCD. Within a CCD, this dictionary defines a mapping from channel
to the offset of the center pixel within that channel.
See Figure 2.1.b in the `HiRISE EDR SIS
<https://hirise.lpl.arizona.edu/pdf/HiRISE_EDR_SIS.pdf>`_.
"""
def __init__(self, metadata):
"""
:param metadata:
"hirise_edr" :py:class:`~pdsc.metadata.PdsMetadata` object
"""
helper_localizer = GeodesicLocalizer(
metadata.lines / 2.0, metadata.samples / 2.0,
metadata.center_latitude, metadata.center_longitude,
metadata.lines, metadata.samples,
metadata.pixel_width,
metadata.pixel_width,
metadata.north_azimuth, 1
)
edr_center_col = float(
self.CCD_TABLE[metadata.ccd_name] +
self.CHANNEL_OFFSET[metadata.channel_number]
) / metadata.binning
edr_center_lat, edr_center_lon = helper_localizer.pixel_to_latlon(
metadata.lines / 2.0, edr_center_col)
super(HiRiseLocalizer, self).__init__(
metadata.lines / 2.0, metadata.samples / 2.0,
edr_center_lat, edr_center_lon,
metadata.lines, metadata.samples,
metadata.pixel_width,
metadata.pixel_width,
metadata.north_azimuth, 1
)
[docs]class HiRiseRdrNoMapLocalizer(FourCornerLocalizer):
"""
A localizer for the HiRISE RDR NOMAP observations (subclass of
:py:class:`FourCornerLocalizer`)
"""
DEFAULT_RESOLUTION_M = 1e-6
"""
Sets the default resolution for HiRISE NOMAP localization
"""
NORMALIZED_PIXEL_SPACE = True
"""
The HiRISE RDR cumulative index metadata does not contain information about
the size of the NOMAP data product. Therefore, we must use a normalized
pixel space for these observations.
"""
def __init__(self, metadata):
"""
:param metadata:
"hirise_rdr" :py:class:`~pdsc.metadata.PdsMetadata` object
"""
corners = np.array([
[metadata.corner1_latitude, metadata.corner1_longitude],
[metadata.corner2_latitude, metadata.corner2_longitude],
[metadata.corner3_latitude, metadata.corner3_longitude],
[metadata.corner4_latitude, metadata.corner4_longitude],
])
super(HiRiseRdrNoMapLocalizer, self).__init__(
corners, 1.0, 1.0, 1
)
[docs]class HiRiseRdrLocalizer(MapLocalizer):
"""
A localizer for the HiRISE RDR (map-projected) observations (subclass of
:py:class:`MapLocalizer`)
"""
DEFAULT_RESOLUTION_M = 1e-6
"""
Sets the default resolution for HiRISE RDR localization, although this
attribute is not used for the :py:class:`MapLocalizer`
"""
def __init__(self, metadata):
"""
:param metadata:
"hirise_rdr" :py:class:`~pdsc.metadata.PdsMetadata` object
"""
super(HiRiseRdrLocalizer, self).__init__(
metadata.map_projection_type,
metadata.projection_center_latitude,
metadata.projection_center_longitude,
metadata.map_scale,
metadata.line_projection_offset,
metadata.sample_projection_offset,
metadata.lines,
metadata.samples
)
[docs]class HiRiseRdrBrowseLocalizer(HiRiseRdrLocalizer):
"""
A localizer for the HiRISE RDR (map-projected) "browse" images (subclass of
:py:class:`HiRiseRdrLocalizer`)
This classifier is included for convenience; it simply scales the pixel
coordinates of the browse image to/from those of the full image before/after
calling the super-class implementation.
"""
HIRISE_BROWSE_WIDTH = 2048
"""
The default width of HiRISE browse images
"""
def __init__(self, metadata, browse_width):
"""
:param metadata:
"hirise_rdr" :py:class:`~pdsc.metadata.PdsMetadata` object
:param browse_width:
the width of the HiRISE browse image (if it varies from the default
value)
"""
super(HiRiseRdrBrowseLocalizer, self).__init__(metadata)
self.scale_factor = float(browse_width) / metadata.samples
if self.scale_factor <= 0:
raise ValueError('Invalid scale factor: %f' % self.scale_factor)
[docs] def pixel_to_latlon(self, row, col):
return super(HiRiseRdrBrowseLocalizer, self).pixel_to_latlon(
row / self.scale_factor, col / self.scale_factor
)
[docs] def latlon_to_pixel(self, lat, lon):
pix = super(HiRiseRdrBrowseLocalizer, self).latlon_to_pixel(lat, lon)
return pix[0]*self.scale_factor, pix[1]*self.scale_factor
[docs]@register_localizer('hirise_rdr')
def hirise_rdr_localizer(metadata, nomap=False, browse=False,
browse_width=HiRiseRdrBrowseLocalizer.HIRISE_BROWSE_WIDTH):
"""
Constructs the appropriate HiRISE RDR localizer for the desired data
product type
:param metadata:
"hirise_rdr" :py:class:`~pdsc.metadata.PdsMetadata` object
:param nomap:
construct localizer for the NOMAP (non-map-projected) data product
:param browse:
construct localizer for the BROWSE data product
:param browse_width:
if ``browse=True``, use this value as the width of the browse image
:return: a :py:class:`Localizer` for the appropriate data product
"""
if nomap:
return HiRiseRdrNoMapLocalizer(metadata)
else:
if browse:
return HiRiseRdrBrowseLocalizer(metadata, browse_width)
else:
return HiRiseRdrLocalizer(metadata)
[docs]@register_localizer('moc')
class MocLocalizer(GeodesicLocalizer):
"""
A localizer for the MOC observations (subclass of
:py:class:`GeodesicLocalizer`)
"""
DEFAULT_RESOLUTION_M = 1e-3
"""
Sets the default resolution for MOC localization
"""
BODY = Geodesic(MARS_RADIUS_M, 0.0)
"""
Uses a Geodesic model for MOC that assumes Mars is spherical, which seems to
work better in practice.
"""
def __init__(self, metadata):
"""
:param metadata:
"moc" :py:class:`~pdsc.metadata.PdsMetadata` object
"""
super(MocLocalizer, self).__init__(
metadata.lines / 2.0, metadata.samples / 2.0,
metadata.center_latitude, metadata.center_longitude,
metadata.lines, metadata.samples,
metadata.image_height / metadata.lines,
metadata.image_width / metadata.samples,
metadata.north_azimuth, 1
)
[docs]def get_localizer(metadata, *args, **kwargs):
"""
Get a localizer for an observation corresponding to the provided metadata
:param metadata:
a :py:class:`~pdsc.metadata.PdsMetadata` object for an observation
:param \*args:
additional args provided to the localizer constructor
:param \**kwargs:
additional kwargs provided to the localizer constructor
:return: a :py:class:`Localizer` for the observation
.. Note::
The :py:meth:`get_localizer` method determines the appropriate localizer
to use for the observation by looking for the class or function that was
registered to the instrument using the :py:meth:`register_localizer`
decorator. See :ref:`Extending PDSC` for more details.
"""
if metadata.instrument not in LOCALIZERS:
raise IndexError(
'No localizer implemented for %s' % metadata.instrument)
return LOCALIZERS[metadata.instrument](metadata, *args, **kwargs)