Skip to content
U.S. flag

An official website of the United States government

Official websites use .gov
A .gov website belongs to an official government organization in the United States.

Secure .gov websites use HTTPS
A lock ( ) or https:// means you’ve safely connected to the .gov website. Share sensitive information only on official, secure websites.

CSM Sandbox

Download This Notebook

Install Prerequisites

conda create -n csm-sandbox -c conda-forge knoten=0.4 notebook matplotlib ipywidgets 'ipympl>=0.9.6'
conda activate csm-sandbox
ARM Compatibility Issues

ARM machines may have dependency issues, for now we recommend running in an x86 environment.

On ARM Macs, you can tell conda to create an x86 environment by prefixing your command with CONDA_SUBDIR=osx-64:

CONDA_SUBDIR=osx-64 conda create -n csm-sandbox -c conda-forge knoten=0.4 notebook matplotlib ipywidgets 'ipympl>=0.9.6'

Running the notebook on your computer

This notebook has an interactive widget.

To try it, download it on your computer and unzip it. In a terminal, cd into its folder.

After installing the prerequisites as above, run jupyter notebook in that folder to open the notebook in your browser.

Imports

%matplotlib widget
import os                           # File Path Joining
import json                         # Read ISD as python dictionary
import copy                         # Important to be able to modify the ISD

from knoten import csm              # Knoten CSM

import matplotlib.pyplot as plt     # Math and Plotting Tools
import ipywidgets as widgets
from ipywidgets import Layout
%%html
<style>
div.jupyter-widgets.widget-label {display: none;}
</style>
<!-- Styling for the plot/table below. -->

Methods for reading/printing stats

def print_stats(dict, search_keys):
    for search_key in search_keys:
        print(f"{search_key+": ":&lt;25}" + str(dict[search_key]))

def plot_footprint(lons, lats, aspect):
    plt.close()
    plt.rcParams['figure.figsize'] = aspect
    plt.axes().set_aspect('equal','datalim')
    plt.plot(lons, lats)
    plt.xlabel('Longitude (deg)')
    plt.ylabel('Latitude (deg)')
    plt.title('CSM footprint')
    plt.show()

def plot_footprint_comparison(fp1, fp2, aspect, dic_o, dic_n):
    plt.clf()                                   # clear previous figure 

    fp1_plot, = plt.plot(fp1[0], fp1[1], 'b')   # Plot footprint 1 in blue
    fp2_plot, = plt.plot(fp2[0], fp2[1], 'r')       # Plot footprint 2 in red

    plt.title('Original vs. Modified Footprint')    # Title and axis labels
    plt.xlabel('Longitude (deg)')
    plt.ylabel('Latitude (deg)')
    fp1_plot.set_label(fp1[2])  # Labels/Legend
    fp2_plot.set_label(fp2[2])
    plt.legend()

    plt.axis('equal') # Set equal scale on both axes so the original shape won't be distorted
    plt.grid(color='#DDDDDD', linewidth=0.5) # Grid makes scale/transformation more visible

    # Display Values

    o_foc_len = dic_o['focal_length_model']['focal_length']
    o_det_cen = (dic_o['detector_center']['line'], dic_o['detector_center']['sample'])
    o_disto = dic_o['optical_distortion']['radial']['coefficients']
    o_eph_time = dic_o['center_ephemeris_time']

    n_foc_len = dic_n['focal_length_model']['focal_length']
    n_det_cen = (dic_n['detector_center']['line'], dic_n['detector_center']['sample'])
    n_disto = dic_n['optical_distortion']['radial']['coefficients']
    n_eph_time = dic_n['center_ephemeris_time']

    text_labels = ('Focal Length: \n' +
        'Detector Center (L, S): \n' +
        'Radial Distortion: \n' + 
        'Ephemeris Center Time: ')
    old_values = (f'{o_foc_len:.0f} \n' +
        f'({o_det_cen[0]:.0f}, {o_det_cen[1]:.0f}) \n' +
        f'({o_disto[0]:.2f}, {o_disto[1]:.4f}, {o_disto[2]:.7f}) \n' + 
        f'{o_eph_time:.1f}')
    new_values = (f'→ {n_foc_len:.0f} \n' +
        f'→ ({n_det_cen[0]:.0f}, {n_det_cen[1]:.0f}) \n' +
        f'→ ({n_disto[0]:.2f}, {n_disto[1]:.4f}, {n_disto[2]:.7f}) \n' + 
        f'→ {n_eph_time:.1f}')

    plt.subplots_adjust(top=0.75)                         # Set Plot to bottom 3/4
    plt.gcf().text(0.02, 0.85, text_labels, fontsize=10, color='black') # Put text on top
    plt.gcf().text(0.3, 0.85, old_values, fontsize=10, color='blue') # Put text on top
    plt.gcf().text(0.6, 0.85, new_values, fontsize=10, color='red') # Put text on top

    plt.show()                  # Show plot

Stats/Footprint of original ISD

# Load Dict from JSON-style ISD File
isd_file = 'csm-sandbox-isd-file.json'
isd_file_mod = 'csm-sandbox-isd-file-mod.json'

# The included ISD file in this example came from
# the P01_001404_1722_XI_07S090W.IMG MRO CTX image.

with open(isd_file) as json_file:
    isd_dict = json.load(json_file)

# Print selected values from ISD
print_stats(isd_dict, ('focal_length_model', 'detector_center', 'optical_distortion', 'center_ephemeris_time'))

# Create Camera Model
camera = csm.create_csm(isd_file)

# Get the footprint using the model
boundary = csm.generate_boundary((isd_dict['image_lines'], isd_dict['image_samples']))
lons, lats, alts = csm.generate_latlon_boundary(camera, boundary)

# # This line can plot the footprint of the original ISD
# plot_footprint(lons, lats, [5,1])
focal_length_model:      {'focal_length': 352.9271664}
detector_center:         {'line': 0.430442527, 'sample': 2542.96099}
optical_distortion:      {'radial': {'coefficients': [-0.007343392592005451, 2.83758786362417e-05, 1.28419891240271e-08]}}
center_ephemeris_time:   216723488.96431854

Modify ISD/write to file

# clear the plot from any previous footprints/plots
plt.close()

# Copy the ISD Dictionary, we will modify it and compare to the original.
isd_dict_mod = copy.deepcopy(isd_dict)

print('Adjust Sliders to add or subtract from the values at the following ISD Keys.')
print('Note: \033[34mThe original blue footprint is staying in the same place.\033[0m')
print('\033[31mThe modifications to the ISD change the geometry of the new red footprint.\033[0m')
print()

# Slider Widgets
wide_lay  = Layout(width='600px')
wide_desc = {'description_width': '150px'}
@widgets.interact(
        fl_add=widgets.FloatSlider(min=-250, max=250, step=1, description='Focal Length', layout=wide_lay, style=wide_desc, readout_format='.0f'), 
        dcl_add=widgets.FloatSlider(min=-4000, max=4000, step=50, description='Detector Center Line', layout=wide_lay, style=wide_desc, readout_format='.0f'),
        dcs_add=widgets.FloatSlider(min=-4000, max=4000, step=50, description='Detector Center Sample', layout=wide_lay, style=wide_desc, readout_format='.0f'),
        opt_x=widgets.FloatSlider(min=-1, max=1, step=0.01, description='Optical Distortion X', layout=wide_lay, style=wide_desc, readout_format='.2f'),
        opt_y=widgets.FloatSlider(min=-0.003, max=0.003, step=0.0001, description='Optical Distortion Y', layout=wide_lay, style=wide_desc, readout_format='.4f'),
        opt_z=widgets.FloatSlider(min=-1e-5, max=1e-5, step=1e-7, description='Optical Distortion Z', layout=wide_lay, style=wide_desc, readout_format='.7f'),
        ect_add=widgets.FloatSlider(min=-20, max=20, step=0.1, description='Exposure (Center) Time', layout=wide_lay, style=wide_desc, readout_format='.1f')
    )
# This function executed whenever one of the slider widgets is adjusted
def exec_widget_function(fl_add, dcl_add, dcs_add, opt_x, opt_y, opt_z, ect_add):

    # Note: print() statements within this function may cause flickering.
    #       Try writing something on the plot instead if you need output.

    # If you're curious where the ISD values came from, 
    # Detector Center was from NAIF Boresight Line/Sample
    # Optical Distortion was from NAIF OD_K
    # ISIS uses the NAIF Keywords, but Knoten CSM uses other derived ISD values.

    old_fl = isd_dict['focal_length_model']['focal_length']
    old_dcl = isd_dict['detector_center']['line']
    old_dcs = isd_dict['detector_center']['sample']
    old_odx = isd_dict['optical_distortion']['radial']['coefficients'][0]
    old_ody = isd_dict['optical_distortion']['radial']['coefficients'][1]
    old_odz = isd_dict['optical_distortion']['radial']['coefficients'][2]
    old_ect = isd_dict['center_ephemeris_time']

    new_values = {
        'focal_length_model': {'focal_length': old_fl + fl_add},
        'detector_center':    {'line': old_dcl + dcl_add, 'sample': old_dcs + dcs_add},
        'optical_distortion': {'radial': {'coefficients': [old_odx + opt_x, old_ody + opt_y, old_odz + opt_z]}},
        'center_ephemeris_time': old_ect + ect_add
    }

    # Modify Values in Dictionary
    for key,value in new_values.items(): 
        isd_dict_mod[key] = new_values[key]

    # Write ISD to file
    with open(isd_file_mod, 'w') as json_file:
        json.dump(isd_dict_mod, json_file, indent=4)

    # Create Camera Model
    camera = csm.create_csm(isd_file_mod)

    # Get the footprint using the model
    boundary_mod = csm.generate_boundary((isd_dict_mod["image_lines"], isd_dict_mod["image_samples"]))
    lons_mod, lats_mod, alts_mod = csm.generate_latlon_boundary(camera, boundary_mod)

    # Plot it
    plot_footprint_comparison((lons, lats, "Original"),(lons_mod, lats_mod, "Modified"), [7,3], isd_dict, isd_dict_mod)
Adjust Sliders to add or subtract from the values at the following ISD Keys.
Note: The original blue footprint is staying in the same place.
The modifications to the ISD change the geometry of the new red footprint.


interactive(children=(FloatSlider(value=0.0, description='Focal Length', layout=Layout(width='600px'), max=250…

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

(This empty space below helps the above widget display with less jumping/flickering.)