"""
This module provides some basic visualization functions for various relnav results.
Contents
--------
"""
import matplotlib.pyplot as plt
from matplotlib.axes import Axes
from matplotlib.figure import Figure
try:
from matplotlib.animation import PillowWriter
except UnicodeDecodeError:
print('unable to import PillowWriter')
PillowWriter = None
import numpy as np
from giant.relative_opnav.relnav_class import RelativeOpNav
from giant.ray_tracer.shapes import Shape
ANGLES = np.linspace(0, 2 * np.pi, 500)
SCAN_VECTORS = np.vstack([np.cos(ANGLES), np.sin(ANGLES), np.zeros(ANGLES.size)])
[docs]
def show_templates(relnav: RelativeOpNav, index: int, target_ind: int,
ax1: Axes | None = None, ax2: Axes | None = None, fig: Figure | None = None) \
-> tuple[Axes, Axes, Figure]:
"""
Show the rendered template alongside of a specific image.
:param relnav: the :class:`.RelativeOpNav` instance to plot the residuals from
:param index: the image index (in th camera) to show
:param ax1: the axes to show the image on (if None one will be created)
:param ax2: the axes to show the template on (if None, one will be created)
:param fig: the fig containing ax1 and ax2 (if None, one will be created)
:returns: a tuple containing the image axes, the template axes, and the figure
"""
# retrieve the image we are processing
image = relnav.camera.images[index]
# update the scene to reflect the current time
relnav.scene.update(image)
if (ax1 is None) or (ax2 is None) or (fig is None):
fig = plt.figure()
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
# set the title so we know what we're looking at
fig.suptitle('{} {}'.format(image.observation_date.isoformat(), relnav.scene.target_objs[target_ind].name))
# show the image
ax1.imshow(image, cmap='gray')
# determine the location of the template in the image (roughly)
assert (templ := relnav.saved_templates[index][target_ind]) is not None
template_shape = np.array(templ.shape[::-1])
template_size = template_shape // 2
center = np.round(relnav.center_finding_results[index, target_ind]["measured"][:2])
if not np.isfinite(center).all():
print('invalid solved-for center. using predicted.')
center = np.round(relnav.center_finding_results[index, target_ind]["predicted"][:2])
min_bounds = center - template_size
max_bounds = center + template_size + (template_shape % 2)
# crop the image, accounting for when the shape is odd using the modulo
ax1.set_xlim(min_bounds[0], max_bounds[0])
ax1.set_ylim(max_bounds[1], min_bounds[1])
# label this subplot as the image
ax1.set_title('Image')
# show the template
ax2.imshow(templ, cmap='gray')
# label this subplot as the template
ax2.set_title('Template')
return ax1, ax2, fig
[docs]
def show_limbs(relnav: RelativeOpNav, index: int, ax: Axes | None = None) -> tuple[Axes, np.ndarray, np.ndarray]:
"""
Show the observed and computed limbs on a specific image.
:param relnav: the :class:`.RelativeOpNav` instance to plot the residuals from
:param index: the image index (in th camera) to show
:param ax: the axes to plot to (or None, in which case new axes will be created)
:returns: a tuple of the axes that was plotted to, the max bounds of the limbs, and the min bounds of the limbs
"""
# retrieve the image we are processing
image = relnav.camera.images[index]
# retrieve the observation observation_date of the image we are processing
date = image.observation_date
# update the scene to reflect the current time
relnav.scene.update(image)
if ax is None:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.imshow(image, cmap='gray')
# initialize variables to store the bounds of the limbs
min_limb_bounds = np.array([np.inf, np.inf])
max_limb_bounds = np.array([-np.inf, -np.inf])
for target_ind, target in enumerate(relnav.scene.target_objs):
assert isinstance(target.shape, Shape), "the target must contain a traceable shape"
# determine the a priori distance to the target
apriori_distance = np.linalg.norm(target.position)
# get the a priori limb points
# define the line of sight to the body in the camera frame
apriori_los = target.position.ravel() / apriori_distance
# find the limb points in the camera frame
apriori_limbs_cam = target.shape.find_limbs(apriori_los, SCAN_VECTORS)
# project the limb points into the image
apriori_limbs_image = relnav.camera.model.project_onto_image(apriori_limbs_cam,
image=index,
temperature=image.temperature)
# plot the a priori limb points
ax.plot(*apriori_limbs_image, linewidth=1, label='{} a priori limbs'.format(target.name))
# adjust the target object to its observed location
rtype = relnav.center_finding_results[index, target_ind]['type']
if not rtype:
rtype = relnav.relative_position_results[index, target_ind]['type']
if rtype in [b'cof', 'cof']:
los = relnav.camera.model.pixels_to_unit(relnav.center_finding_results[index, target_ind]['measured'][:2],
temperature=image.temperature, image=index)
if np.isfinite(los).all():
target.change_position(los * apriori_distance)
elif rtype in [b'pos', 'pos']:
los = relnav.relative_position_results[index, target_ind]['measured'].copy()
los /= np.linalg.norm(los)
if np.isfinite(los).all():
target.change_position(relnav.relative_position_results[index, target_ind]['measured'])
else:
raise ValueError("Can't display limbs for {} type relnav".format(rtype))
limbs_cam = target.shape.find_limbs(los, SCAN_VECTORS)
# project the limb points into the image
limbs_image = relnav.camera.model.project_onto_image(limbs_cam,
image=index,
temperature=image.temperature)
# update the limb bounds
min_limb_bounds = np.minimum(min_limb_bounds, limbs_image.min(axis=-1))
max_limb_bounds = np.maximum(max_limb_bounds, limbs_image.max(axis=-1))
# plot the updated limb points
ax.plot(*limbs_image, linewidth=1, label='{} solved for limbs'.format(target.name))
if rtype in [b'cof', 'cof']:
# show the predicted center pixel
ax.scatter(*relnav.center_finding_results[index, target_ind]['predicted'][:2],
label='{} predicted center'.format(target.name))
# show the solved for center
ax.scatter(*relnav.center_finding_results[index, target_ind]['measured'][:2],
label='{} solved-for center'.format(target.name))
else:
# get the apriori image location
apriori_image_pos = relnav.camera.model.project_onto_image(
relnav.relative_position_results[index, target_ind]['predicted'],
image=index, temperature=image.temperature)
# get the solved for image location
image_pos = relnav.camera.model.project_onto_image(
relnav.relative_position_results[index, target_ind]['measured'],
image=index, temperature=image.temperature)
# show the centers
ax.scatter(*apriori_image_pos, label='{} predicted center'.format(target.name))
ax.scatter(*image_pos, label='{} solved-for center'.format(target.name))
# set the title so we know what image we're looking at
ax.set_title(date.isoformat())
return ax, max_limb_bounds, min_limb_bounds
[docs]
def limb_summary_gif(relnav: RelativeOpNav, fps: int = 2, outfile: str = './opnavsummary.gif', dpi: int = 100):
"""
Generate a GIF of the observed and predicted limbs
:param relnav: the :class:`.RelativeOpNav` instance to plot the residuals from
:param fps: the frames per second for the gif
:param outfile: the file to save the gif to
:param dpi: digital pixels per inch of the resulting gif
"""
# initialize the figure and axes
fig = plt.figure()
fig.set_layout_engine('tight')
ax = fig.add_subplot(111)
# initialize the writer
assert PillowWriter is not None
writer = PillowWriter(fps=fps)
writer.setup(fig=fig, outfile=outfile, dpi=dpi)
# loop through each image and save the frame
for ind, image in relnav.camera:
ax.clear()
_, max_limbs, min_limbs = show_limbs(relnav, ind, ax=ax)
# set the limits to highlight only the portion of interest
if np.isfinite(min_limbs).all() and np.isfinite(max_limbs).all():
ax.set_xlim(min_limbs[0] - 10, max_limbs[0] + 10)
ax.set_ylim(min_limbs[1] - 10, max_limbs[1] + 10)
fig.legend().set_draggable(True)
writer.grab_frame()
writer.finish()
plt.close(fig)
[docs]
def template_summary_gif(relnav: RelativeOpNav, fps: int = 2, outfile: str = './templatesummary.gif', dpi: int = 100):
"""
Generate a GIF of the rendered templates from relnav
:param relnav: the :class:`.RelativeOpNav` instance to plot the residuals from
:param fps: the frames per second for the gif
:param outfile: the file to save the gif to
:param dpi: digital pixels per inch of the resulting gif
"""
# initialize the figure and axes
fig = plt.figure()
fig.set_layout_engine('tight')
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
# initialize the writer
assert PillowWriter is not None
writer = PillowWriter(fps=fps)
writer.setup(fig=fig, outfile=outfile, dpi=dpi)
# loop through each image and save the frame
for ind, image in relnav.camera:
# loop through each object
for obj_ind in range(len(relnav.scene.target_objs)):
if relnav.saved_templates[ind] is not None:
if relnav.saved_templates[ind][obj_ind] is not None:
ax1.clear()
ax2.clear()
show_templates(relnav, ind, obj_ind, ax1=ax1, ax2=ax2, fig=fig)
writer.grab_frame()
writer.finish()
plt.close(fig)
[docs]
def show_center_finding_residuals(relnav: RelativeOpNav):
"""
Plot the center finding residuals as a function of time.
:param relnav: the :class:`.RelativeOpNav` instance to plot the residuals from
"""
fig = plt.figure()
ax = fig.add_subplot(111)
# initialize lists to store the data
resids = []
dates = []
# loop through each image
for ind, image in relnav.camera:
# store a list in the resids list and the datetime object in the dates list
resids.append([])
dates.append(image.observation_date)
# loop through each target
for target_ind in range(len(relnav.scene.target_objs)):
# determine the type of results we are considering
if relnav.center_finding_results[ind, target_ind]["type"] in [b'cof', b'lmk', 'cof', 'lmk']:
# compute the observed minus computed residuals
resids[-1].append((relnav.center_finding_results[ind, target_ind]['measured'] -
relnav.center_finding_results[ind, target_ind]['predicted'])[:2])
else: # if we are considering a technique that estimates the full 3DOF position
# project the predicted and measured positions onto the image and compute the o-c resids
resids[-1].append(
relnav.camera.model.project_onto_image(relnav.center_finding_results[ind, target_ind]['measured'],
image=ind, temperature=image.temperature) -
relnav.camera.model.project_onto_image(relnav.center_finding_results[ind, target_ind]['predicted'],
image=ind, temperature=image.temperature))
# stack all of the resids together
resids = np.asarray(resids)
dates = np.asarray(dates, dtype='datetime64[us]')
# loop through each target again and plot the residuals vs time
for target_ind, target in enumerate(relnav.scene.target_objs):
ax.scatter(dates, resids[:, target_ind, 0], label='{} Columns'.format(target.name))
ax.scatter(dates, resids[:, target_ind, 1], label='{} Rows'.format(target.name))
# set the labels and update the x axis to display the dates better
ax.set_xlabel('Observation Date')
ax.set_ylabel('O-C Residuals, pix')
fig.autofmt_xdate()
# create a legend
fig.legend().set_draggable(True)
[docs]
def scatter_residuals_sun_dependent(relnav: RelativeOpNav):
"""
Show observed minus computed residuals with units of pixels plotted in a frame rotated so that +x points towards the
sun in the image.
:param relnav: the :class:`.RelativeOpNav` instance to plot the residuals from
"""
resids = []
# loop through each image
assert relnav.scene.light_obj is not None
for ind, image in relnav.camera:
# loop through each target
for target_ind in range(len(relnav.scene.target_objs)):
# update the scene so we can get the sun direction
relnav.scene.update(image)
# figure out the direction to the sun in the image
line_of_sight_sun = relnav.camera.model.project_directions(relnav.scene.light_obj.position.ravel())
# get the rotation to make the x axis line up with this direction
# since line_of_sight_sun is a unit vector the x component = cos(theta) and y component = sin of theta
# so the following gives
# [[ cos(theta), sin(theta)],
# [-sin(theta), cos(theta)]]
# which rotates from the image frame to the frame with +x pointing towards the sun
rmat = np.array([line_of_sight_sun, line_of_sight_sun[::-1] * [-1, 1]])
# determine the type of results we are considering
if relnav.center_finding_results[ind, target_ind]["type"] in [b'cof', b'lmk', 'cof', 'lmk']:
# compute the observed minus computed residuals
resids.append(rmat @ (relnav.center_finding_results[ind, target_ind]['measured'] -
relnav.center_finding_results[ind, target_ind]['predicted'])[:2])
else: # if we are considering a technique that estimates the full 3DOF position
# project the predicted and measured positions onto the image and compute the o-c resids
resids.append(rmat @
(relnav.camera.model.project_onto_image(
relnav.center_finding_results[ind, target_ind]['measured'],
image=ind, temperature=image.temperature
) -
relnav.camera.model.project_onto_image(
relnav.center_finding_results[ind, target_ind]['predicted'],
image=ind, temperature=image.temperature)
))
# stack all of the resids together
resids = np.asarray(resids)
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(*resids)
ax.set_xlabel("Sun direction O-C error, pix")
ax.set_xlabel("Anti-sun direction O-C error, pix")
[docs]
def plot_residuals_sun_dependent_time(relnav: RelativeOpNav):
"""
Show observed minus computed residuals with units of pixels plotted in a frame rotated so that +x points towards the
sun in the image.
This is done with a time series (so the x axis of the plot is time and the y axis is residual in pixels) with 2
different series
:param relnav: the :class:`.RelativeOpNav` instance to plot the residuals from
"""
dates = []
resids = []
# loop through each image
assert relnav.scene.light_obj is not None
for ind, image in relnav.camera:
# store a list in the resids list and the datetime object in the dates list
resids.append([])
dates.append(image.observation_date)
# loop through each target
for target_ind in range(len(relnav.scene.target_objs)):
# update the scene so we can get the sun direction
relnav.scene.update(image)
# figure out the direction to the sun in the image
line_of_sight_sun = relnav.camera.model.project_directions(relnav.scene.light_obj.position.ravel())
# get the rotation to make the x axis line up with this direction
# since line_of_sight_sun is a unit vector the x component = cos(theta) and y component = sin of theta
# so the following gives
# [[ cos(theta), sin(theta)],
# [-sin(theta), cos(theta)]]
# which rotates from the image frame to the frame with +x pointing towards the sun
rmat = np.array([line_of_sight_sun, line_of_sight_sun[::-1] * [-1, 1]])
# determine the type of results we are considering
if relnav.center_finding_results[ind, target_ind]["type"] in [b'cof', b'lmk', 'cof', 'lmk']:
# compute the observed minus computed residuals
resids[-1].append(rmat @ (relnav.center_finding_results[ind, target_ind]['measured'] -
relnav.center_finding_results[ind, target_ind]['predicted'])[:2])
else: # if we are considering a technique that estimates the full 3DOF position
# project the predicted and measured positions onto the image and compute the o-c resids
resids[-1].append(rmat @
(relnav.camera.model.project_onto_image(
relnav.center_finding_results[ind, target_ind]['measured'],
image=ind, temperature=image.temperature
) -
relnav.camera.model.project_onto_image(
relnav.center_finding_results[ind, target_ind]['predicted'],
image=ind, temperature=image.temperature)
))
# stack all of the resids together
resids = np.asarray(resids)
fig = plt.figure()
ax = fig.add_subplot(111)
# loop through each target again and plot the residuals vs time
for target_ind, target in enumerate(relnav.scene.target_objs):
ax.scatter(dates, resids[:, target_ind, 0], label='{} Sun direction'.format(target.name))
ax.scatter(dates, resids[:, target_ind, 1], label='{} Anti sun direction'.format(target.name))
# set the labels and update the x axis to display the dates better
ax.set_xlabel('Observation Date')
ax.set_ylabel('O-C Residuals, pix')
fig.autofmt_xdate()
# create a legend
fig.legend().set_draggable(True)