diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..e5df061b3dbe6e30b22cd1f93def6f713cb10f3d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +csmapi +jinja +pyproj +numpy +scipy +gdal +pvl diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..02074f820b6a8a8f27b49e3a122067bf3c7cbc27 --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup, find_packages +setup( + name='spaetzle', + version='0.1.0', + long_description='', + packages=find_packages(), + include_package_data=True, + zip_safe=True, + scripts=[] +) diff --git a/spaetzle/__init__.py b/spaetzle/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6d19110a2dfecebd3cb8d87a751187d2f11540b2 --- /dev/null +++ b/spaetzle/__init__.py @@ -0,0 +1,12 @@ +import ctypes +from distutils import sysconfig +import os +import warnings + +from csmapi import csmapi + +# Register the usgscam plugin with the csmapi + +lib = ctypes.CDLL(os.path.abspath(os.path.join(sysconfig.get_python_lib(), '../../libusgscsm.so'))) +if not lib: + warnings.warn('Unable to load usgscsm shared library') diff --git a/spaetzle/csm.py b/spaetzle/csm.py new file mode 100644 index 0000000000000000000000000000000000000000..706944b0127c1e87465738217fd1bf7e3e1525c5 --- /dev/null +++ b/spaetzle/csm.py @@ -0,0 +1,308 @@ +import datetime +import json +import os + +from csmapi import csmapi +import jinja2 + +import numpy as np +import scipy.stats +import pyproj +import gdal +from gdal import ogr +import pvl + + +def generate_boundary(isize, nnodes=5, n_points=10): + ''' + Generates a bounding box given a camera model + + Parameters + ---------- + camera : object + csmapi generated camera model + + nnodes : int + Not sure + + n_points : int + Number of points to generate between the corners of the bounding + box per side. + + Returns + ------- + boundary : list + List of full bounding box + ''' + x = np.linspace(0, isize[0], n_points) + y = np.linspace(0, isize[1], n_points) + boundary = [(i,0.) for i in x] + [(isize[0], i) for i in y[1:]] +\ + [(i, isize[1]) for i in x[::-1][1:]] + [(0.,i) for i in y[::-1][1:]] + + return boundary + +def generate_latlon_boundary(camera, nnodes=5, semi_major=3396190, semi_minor=3376200, n_points=10): + ''' + Generates a latlon bounding box given a camera model + + Parameters + ---------- + camera : object + csmapi generated camera model + + nnodes : int + Not sure + + semi_major : int + Semimajor axis of the target body + + semi_minor : int + Semiminor axis of the target body + + n_points : int + Number of points to generate between the corners of the bounding + box per side. + + Returns + ------- + lons : list + List of longitude values + + lats : list + List of latitude values + + alts : list + List of altitude values + ''' + isize = camera.getImageSize() + isize = (isize.line, isize.samp) + + boundary = generate_boundary(isize, nnodes=nnodes, n_points = n_points) + + ecef = pyproj.Proj(proj='geocent', a=semi_major, b=semi_minor) + lla = pyproj.Proj(proj='latlon', a=semi_major, b=semi_minor) + + gnds = np.empty((len(boundary), 3)) + + for i, b in enumerate(boundary): + # Could be potential errors or warnings from imageToGround + try: + gnd = camera.imageToGround(csmapi.ImageCoord(*b), 0) + except: + pass + + gnds[i] = [gnd.x, gnd.y, gnd.z] + + lons, lats, alts = pyproj.transform(ecef, lla, gnds[:,0], gnds[:,1], gnds[:,2]) + return lons, lats, alts + +def generate_gcps(camera, nnodes=5, semi_major=3396190, semi_minor=3376200, n_points=10): + ''' + Generates an area of ground control points formated as: + <GCP Id="" Info="" Pixel="" Line="" X="" Y="" Z="" /> per record + + Parameters + ---------- + camera : object + csmapi generated camera model + + nnodes : int + Not sure + + semi_major : int + Semimajor axis of the target body + + semi_minor : int + Semiminor axis of the target body + + n_points : int + Number of points to generate between the corners of the bounding + box per side. + + Returns + ------- + gcps : list + List of all gcp records generated + ''' + lons, lats, alts = generate_latlon_boundary(camera, nnodes=nnodes, + semi_major=semi_major, + semi_minor=semi_minor, + n_points=n_points) + + lla = np.vstack((lons, lats, alts)).T + + tr = zip(boundary, lla) + + gcps = [] + for i, t in enumerate(tr): + l = '<GCP Id="{}" Info="{}" Pixel="{}" Line="{}" X="{}" Y="{}" Z="{}" />'.format(i, i, t[0][1], t[0][0], t[1][0], t[1][1], t[1][2]) + gcps.append(l) + + return gcps + +def generate_latlon_footprint(camera, nnodes=5, semi_major=3396190, semi_minor=3376200, n_points=10): + ''' + Generates a latlon footprint from a csmapi generated camera model + + Parameters + ---------- + camera : object + csmapi generated camera model + + nnodes : int + Not sure + + semi_major : int + Semimajor axis of the target body + + semi_minor : int + Semiminor axis of the target body + + n_points : int + Number of points to generate between the corners of the bounding + box per side. + + Returns + ------- + : object + ogr multipolygon containing between one and two polygons + ''' + lons, lats, _ = generate_latlon_boundary(camera, nnodes=nnodes, + semi_major=semi_major, + semi_minor=semi_minor, + n_points=n_points) + + # Transform coords from -180, 180 to 0, 360 + # Makes crossing the maridian easier to identify + ll_coords = [*zip(((lons + 180) % 360), lats)] + + ring = ogr.Geometry(ogr.wkbLinearRing) + wrap_ring = ogr.Geometry(ogr.wkbLinearRing) + + poly = ogr.Geometry(ogr.wkbPolygon) + wrap_poly = ogr.Geometry(ogr.wkbPolygon) + + multipoly = ogr.Geometry(ogr.wkbMultiPolygon) + + current_ring = ring + switch_point = None + previous_point = ll_coords[0] + + for coord in ll_coords: + + coord_diff = previous_point[0] - coord[0] + + if coord_diff > 0 and np.isclose(previous_point[0], 360, rtol = 1e-03) and \ + np.isclose(coord[0], 0, atol=1e0, rtol=1e-01): + regression = scipy.stats.linregress([previous_point[0], coord[0]], [previous_point[1], coord[1]]) + slope, b = regression.slope, regression.intercept + current_ring.AddPoint(360 - 180, (slope*360 + b)) + current_ring = wrap_ring + switch_point = 0 - 180, (slope*0 + b) + current_ring.AddPoint(*switch_point) + + elif coord_diff < 0 and np.isclose(previous_point[0], 0, atol=1e0, rtol=1e-01) and \ + np.isclose(coord[0], 360, rtol = 1e-03): + regression = scipy.stats.linregress([previous_point[0], coord[0]], [previous_point[1], coord[1]]) + slope, b = regression.slope, regression.intercept + current_ring.AddPoint(0 - 180, (slope*0 + b)) + current_ring.AddPoint(*switch_point) + current_ring = ring + current_ring.AddPoint(360 - 180, (slope*360 + b)) + + lat, lon = coord + current_ring.AddPoint(lat - 180, lon) + previous_point = coord + + poly.AddGeometry(ring) + wrap_poly.AddGeometry(wrap_ring) + + if not wrap_poly.IsEmpty(): + multipoly.AddGeometry(wrap_poly) + + if not poly.IsEmpty(): + multipoly.AddGeometry(poly) + + return multipoly + +def generate_bodyfixed_footprint(camera, nnodes=5, semi_major=3396190, semi_minor=3376200, n_points=10): + ''' + Generates a bodyfixed footprint from a csmapi generated camera model + + Parameters + ---------- + camera : object + csmapi generated camera model + + nnodes : int + Not sure + + n_points : int + Number of points to generate between the corners of the bounding + box per side. + + Returns + ------- + : object + ogr polygon + ''' + latlon_fp = generate_latlon_footprint(camera, nnodes=5, semi_major = semi_major, semi_minor = semi_minor, n_points = n_points) + + ecef = pyproj.Proj(proj='geocent', a=semi_major, b=semi_minor) + lla = pyproj.Proj(proj='latlon', a=semi_major, b=semi_minor) + + # Step over all geometry objects in the latlon footprint + for i in range(latlon_fp.GetGeometryCount()): + latlon_coords = np.array(latlon_fp.GetGeometryRef(i).GetGeometryRef(0).GetPoints()) + + # Check if the geometry object is populated with points + if len(latlon_coords) > 0: + x, y, z = pyproj.transform(lla, ecef, latlon_coords[:,0], latlon_coords[:,1], latlon_coords[:,2]) + + # Step over all coordinate points in a geometry object and update said point + for j, _ in enumerate(latlon_coords): + latlon_fp.GetGeometryRef(i).GetGeometryRef(0).SetPoint(j, x[j], y[j], 0) + + return latlon_fp + +def generate_vrt(camera, raster_size, fpath, outpath=None, no_data_value=0): + gcps = generate_gcps(camera) + xsize, ysize = raster_size + + if outpath is None: + outpath = os.path.dirname(fpath) + outname = os.path.splitext(os.path.basename(fpath))[0] + '.vrt' + outname = os.path.join(outpath, outname) + + xsize, ysize = raster_size + vrt = r'''<VRTDataset rasterXSize="{{ xsize }}" rasterYSize="{{ ysize }}"> + <Metadata/> + <GCPList Projection="{{ proj }}"> + {% for gcp in gcps -%} + {{gcp}} + {% endfor -%} + </GCPList> + <VRTRasterBand dataType="Float32" band="1"> + <NoDataValue>{{ no_data_value }}</NoDataValue> + <Metadata/> + <ColorInterp>Gray</ColorInterp> + <SimpleSource> + <SourceFilename relativeToVRT="0">{{ fpath }}</SourceFilename> + <SourceBand>1</SourceBand> + <SourceProperties rasterXSize="{{ xsize }}" rasterYSize="{{ ysize }}" + DataType="Float32" BlockXSize="512" BlockYSize="512"/> + <SrcRect xOff="0" yOff="0" xSize="{{ xsize }}" ySize="{{ ysize }}"/> + <DstRect xOff="0" yOff="0" xSize="{{ xsize }}" ySize="{{ ysize }}"/> + </SimpleSource> + </VRTRasterBand> + </VRTDataset>''' + + context = {'xsize':xsize, 'ysize':ysize, + 'gcps':gcps, + 'proj':'+proj=longlat +a=3396190 +b=3376200 +no_defs', + 'fpath':fpath, + 'no_data_value':no_data_value} + template = jinja2.Template(vrt) + tmp = template.render(context) + warp_options = gdal.WarpOptions(format='VRT', dstNodata=0) + gdal.Warp(outname, tmp, options=warp_options)