diff --git a/.travis.yml b/.travis.yml index 5d4c074ddb90a7e1fbe45f24f16a30c3732054cf..027346f8311408e148fbbcf3870bb0ed67090061 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,7 +44,7 @@ install: - conda config --add channels conda-forge - conda config --add channels jlaura - conda install -c conda-forge gdal h5py - - conda install pandas sqlalchemy pyyaml + - conda install pandas sqlalchemy pyyaml networkx - conda install -c jlaura pvl protobuf # Development installation diff --git a/README.rst b/README.rst index 4ca3f3ff4d0fabcbf1138c78607ff6e87362864d..c15a55c5faa782be65cce5c594ccf38aff2ddfdc 100644 --- a/README.rst +++ b/README.rst @@ -10,13 +10,14 @@ Planetary Input / Output :target: https://pypi.python.org/pypi/plio .. image:: https://travis-ci.org/USGS-Astrogeology/plio.svg?branch=master - :target: https://travis-ci.org/USGS-Astrogeology/plio + :target: https://travis-ci.org/USGS-Astrogeology/plio -.. image:: https://coveralls.io/repos/github/USGS-Astrogeology/plio/badge.svg?branch=master :target: https://coveralls.io/github/USGS-Astrogeology/plio?branch=master +.. image:: https://coveralls.io/repos/github/USGS-Astrogeology/plio/badge.svg?branch=master + :target: https://coveralls.io/github/USGS-Astrogeology/plio?branch=master .. image:: https://readthedocs.org/projects/plio/badge/?version=latest - :target: http://plio.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status + :target: http://plio.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status A planetary file I/O API diff --git a/appveyor.yml b/appveyor.yml index ad091115a9002a4b2559a563cdb01681c284b9c2..47dedfaf6f75f9393f5a874551481630c1d5e354 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -53,7 +53,7 @@ install: - cmd: conda config --add channels conda-forge - cmd: conda config --add channels jlaura - cmd: conda install --yes -c conda-forge gdal h5py - - cmd: conda install --yes pandas sqlalchemy pyyaml + - cmd: conda install --yes pandas sqlalchemy pyyaml networkx - cmd: conda install --yes -c jlaura protobuf pvl # Development installation diff --git a/conda/build.sh b/conda/build.sh deleted file mode 100755 index 4dad93a1c02abe34b4326e6712b9d046abd2f4a8..0000000000000000000000000000000000000000 --- a/conda/build.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -$PYTHON setup.py install diff --git a/conda/meta.yaml b/conda/meta.yaml index 08523fa178075ebb9be9e4e9e21319cab176a844..18fbf121a501256f19dd85b24621c628de4ef3d7 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -2,16 +2,22 @@ package: name: plio version: 0.1.0 -source: - git_url: https://github.com/USGS-Astrogeology/plio.git +source: + path: ../ + +build: + number: 0 + skip: true #[win] + script: python setup.py install --single-version-externally-managed --record=record.txt requirements: - build: - - python + build: + - python + - setuptools - numpy - pvl - protobuf 3.0.0b2 - - gdal >=2 + - gdal - icu - h5py - pandas @@ -19,14 +25,21 @@ requirements: - pyyaml run: - python + - setuptools - numpy - pvl - protobuf 3.0.0b2 - - gdal >=2 + - gdal - icu - h5py - pandas - sqlalchemy - pyyaml +test: + imports: + - plio +about: + home: http://github.com/USGS-Astrogeology/plio + license: Public Domain diff --git a/environment.yml b/environment.yml index c9752688e6a26f7550601269b160d255cc27df5c..f51c74af88f78d0d7468b559581c862cc1172c07 100644 --- a/environment.yml +++ b/environment.yml @@ -10,3 +10,4 @@ dependencies: - pandas - sqlalchemy - pyyaml + - networkx diff --git a/plio/io/io_autocnetgraph.py b/plio/io/io_autocnetgraph.py new file mode 100644 index 0000000000000000000000000000000000000000..c1360aba6acd26275cfe29f6c1d0de71653a72e1 --- /dev/null +++ b/plio/io/io_autocnetgraph.py @@ -0,0 +1,132 @@ +from io import BytesIO +import json +import os +import warnings +from zipfile import ZipFile + +from networkx.readwrite import json_graph +import numpy as np +import pandas as pd + + +try: + import autocnet + autocnet_avail = True +except: + autocnet_avail = False + +class NumpyEncoder(json.JSONEncoder): + def default(self, obj): + """If input object is an ndarray it will be converted into a dict + holding dtype, shape and the data, base64 encoded. + """ + if isinstance(obj, np.ndarray): + return dict(__ndarray__= obj.tolist(), + dtype=str(obj.dtype), + shape=obj.shape) + # Let the base class default method raise the TypeError + return json.JSONEncoder.default(self, obj) + +def save(network, projectname): + """ + Save an AutoCNet candiate graph to disk in a compressed file. The + graph adjacency structure is stored as human readable JSON and all + potentially large numpy arrays are stored as compressed binary. The + project archive is a standard .zip file that can have any ending, + e.g., <projectname>.project, <projectname>.zip, <projectname>.myname. + + TODO: This func. writes a intermediary .npz to disk when saving. Can + we write the .npz to memory? + + Parameters + ---------- + network : object + The AutoCNet Candidate Graph object + + projectname : str + The PATH to the output file. + """ + # Convert the graph into json format + js = json_graph.node_link_data(network) + + with ZipFile(projectname, 'w') as pzip: + js_str = json.dumps(js, cls=NumpyEncoder, sort_keys=True, indent=4) + pzip.writestr('graph.json', js_str) + + # Write the array node_attributes to hdf + for n, data in network.nodes_iter(data=True): + grp = data['node_id'] + np.savez('{}.npz'.format(data['node_id']), + descriptors=data.descriptors, + _keypoints=data._keypoints, + _keypoints_idx=data._keypoints.index, + _keypoints_columns=data._keypoints.columns) + pzip.write('{}.npz'.format(data['node_id'])) + os.remove('{}.npz'.format(data['node_id'])) + + # Write the array edge attributes to hdf + for s, d, data in network.edges_iter(data=True): + if s > d: + s, d = d, s + grp = str((s,d)) + np.savez('{}_{}.npz'.format(s, d), + matches=data.matches, + matches_idx=data.matches.index, + matches_columns=data.matches.columns, + _masks=data._masks, + _masks_idx=data._masks.index, + _masks_columns=data._masks.columns) + pzip.write('{}_{}.npz'.format(s, d)) + os.remove('{}_{}.npz'.format(s, d)) + +def json_numpy_obj_hook(dct): + """Decodes a previously encoded numpy ndarray with proper shape and dtype. + + :param dct: (dict) json encoded ndarray + :return: (ndarray) if input was an encoded ndarray + """ + if isinstance(dct, dict) and '__ndarray__' in dct: + data = np.asarray(dct['__ndarray__']) + return np.frombuffer(data, dct['dtype']).reshape(dct['shape']) + return dct + +def load(projectname): + if autocnet_avail is False: + warning.warn('AutoCNet Library is not available. Unable to load an AutoCNet CandidateGraph') + return + + with ZipFile(projectname, 'r') as pzip: + # Read the graph object + with pzip.open('graph.json', 'r') as g: + data = json.loads(g.read().decode(),object_hook=json_numpy_obj_hook) + + cg = autocnet.graph.network.CandidateGraph() + Edge = autocnet.graph.edge.Edge + Node = autocnet.graph.node.Node + # Reload the graph attributes + cg.graph = data['graph'] + # Handle nodes + for d in data['nodes']: + n = Node(image_name=d['image_name'], image_path=d['image_path'], node_id=d['id']) + n['hash'] = d['hash'] + # Load the byte stream for the nested npz file into memory and then unpack + nzf = np.load(BytesIO(pzip.read('{}.npz'.format(d['id'])))) + n._keypoints = pd.DataFrame(nzf['_keypoints'], index=nzf['_keypoints_idx'], columns=nzf['_keypoints_columns']) + n.descriptors = nzf['descriptors'] + cg.add_node(d['node_id']) + cg.node[d['node_id']] = n + for e in data['links']: + cg.add_edge(e['source'], e['target']) + edge = Edge() + edge.source = cg.node[e['source']] + edge.destination = cg.node[e['target']] + edge['fundamental_matrix'] = e['fundamental_matrix'] + edge['weight'] = e['weight'] + nzf = np.load(BytesIO(pzip.read('{}_{}.npz'.format(e['source'], e['target'])))) + + edge._masks = pd.DataFrame(nzf['_masks'], index=nzf['_masks_idx'], columns=nzf['_masks_columns']) + edge.matches = pd.DataFrame(nzf['matches'], index=nzf['matches_idx'], columns=nzf['matches_columns']) + # Add a mock edge + cg.edge[e['source']][e['target']] = edge + + return cg diff --git a/plio/io/io_controlnetwork.py b/plio/io/io_controlnetwork.py index afcf5a48b6f197788a7458c7af11af3356371974..b120bd6a89923e3a09e710c430cf6f280605689c 100644 --- a/plio/io/io_controlnetwork.py +++ b/plio/io/io_controlnetwork.py @@ -222,10 +222,12 @@ class IsisStore(object): measure_spec = point_spec.Measure() # For all of the attributes, set if they are an dict accessible attr of the obj. for attr, attrtype in self.measure_attrs: + if attr in g.columns: setattr(measure_spec, attr, attrtype(m[attr])) measure_spec.type = int(m.measure_type) - + measure_spec.line = m.y + measure_spec.sample = m.x measure_iterable.append(measure_spec) self.nmeasures += 1 point_spec.measures.extend(measure_iterable) @@ -364,5 +366,3 @@ class IsisStore(object): if self._handle is not None: self._handle.close() self._handle = None - - diff --git a/plio/io/io_gdal.py b/plio/io/io_gdal.py index 555f73ebe17d61838b74e4a5286c4f038b9caeba..372b118f3da56a8324d2f23f13077a4df7955360 100644 --- a/plio/io/io_gdal.py +++ b/plio/io/io_gdal.py @@ -52,7 +52,7 @@ class GeoDataset(object): The bounding box of the image in lat/lon space geotransform : object - Geotransform reference OGR object as an array of size 6 containing the affine + Geotransform reference OGR object as an array of size 6 containing the affine transformation coefficients for transforming from raw sample/line to projected x/y. xproj = geotransform[0] + sample * geotransform[1] + line * geotransform[2] yproj = geotransform[3] + sample * geotransform[4] + line * geotransform[5] @@ -61,7 +61,7 @@ class GeoDataset(object): Geospatial coordinate system OSR object. latlon_extent : list - of two tuples containing the latitide/longitude boundaries. + of two tuples containing the latitide/longitude boundaries. This list is in the form [(lowerlat, lowerlon), (upperlat, upperlon)]. pixel_width : float @@ -87,9 +87,9 @@ class GeoDataset(object): Note: This is the third value geotransform array. xy_extent : list - of two tuples containing the sample/line boundaries. - The first value is the upper left corner of the upper left pixel and - the second value is the lower right corner of the lower right pixel. + of two tuples containing the sample/line boundaries. + The first value is the upper left corner of the upper left pixel and + the second value is the lower right corner of the lower right pixel. This list is in the form [(minx, miny), (maxx, maxy)]. xy_corners : list @@ -101,26 +101,26 @@ class GeoDataset(object): Note: This is the fifth value geotransform array. coordinate_transformation : object - The coordinate transformation from the spatial reference system to + The coordinate transformation from the spatial reference system to the geospatial coordinate system. - + inverse_coordinate_transformation : object - The coordinate transformation from the geospatial + The coordinate transformation from the geospatial coordinate system to the spatial reference system. - + scale : tuple - The name and value of the linear projection units of the spatial reference system. + The name and value of the linear projection units of the spatial reference system. This tuple is of type string/float of the form (unit name, value). To transform a linear distance to meters, multiply by this value. If no units are available ("Meters", 1) will be returned. - + spheroid : tuple - The spheroid found in the metadata using the spatial reference system. + The spheroid found in the metadata using the spatial reference system. This is of the form (semi-major, semi-minor, inverse flattening). raster_size : tuple The dimensions of the raster, i.e. (number of samples, number of lines). - + central_meridian : float The central meridian of the map projection from the metadata. @@ -382,7 +382,7 @@ class GeoDataset(object): ------- lat, lon : tuple (Latitude, Longitude) corresponding to the given (x,y). - + """ try: geotransform = self.geotransform @@ -410,7 +410,7 @@ class GeoDataset(object): ------- x, y : tuple (Sample, line) position corresponding to the given (latitude, longitude). - + """ geotransform = self.geotransform upperlat, upperlon, _ = self.inverse_coordinate_transformation.TransformPoint(lon, lat) @@ -567,7 +567,8 @@ def match_rasters(match_to, match_from, destination, match_from__srs = match_from.dataset.GetProjection() match_from__gt = match_from.geotransform - dst = gdal.GetDriverByName('GTiff').Create(destination, width, height, 1, gdalconst.GDT_Float32) + dst = gdal.GetDriverByName('GTiff').Create(destination, width, height, match_from.RasterCount, + gdalconst.GDT_Float32) dst.SetGeoTransform(match_to_gt) dst.SetProjection(match_to_srs) diff --git a/plio/io/io_spectral_profiler.py b/plio/io/io_spectral_profiler.py index 0619eb649ac18b59758e01ad399d3d22ba1dcdf6..98cb989130fb5d871f4547528f7266b6f78dbdd3 100755 --- a/plio/io/io_spectral_profiler.py +++ b/plio/io/io_spectral_profiler.py @@ -1,9 +1,10 @@ +import os import pandas as pd import pvl import numpy as np from plio.utils.utils import find_in_dict - +from plio.io.io_gdal import GeoDataset class Spectral_Profiler(object): @@ -52,6 +53,7 @@ class Spectral_Profiler(object): label = pvl.load(input_data) self.label = label + self.input_data = input_data with open(input_data, 'rb') as indata: # Extract and handle the ancillary data ancillary_data = find_in_dict(label, "ANCILLARY_AND_SUPPLEMENT_DATA") @@ -128,3 +130,20 @@ class Spectral_Profiler(object): self.spectra[i] = self.spectra[i][self.spectra[i]['QA'] < qa_threshold] self.spectra = pd.Panel(self.spectra) + + def open_browse(self, extension='.jpg'): + """ + Attempt to open the browse image corresponding to the spc file + + Parameters + ---------- + extension : str + The file type extension to be added to the base name + of the spc file. + + Returns + ------- + + """ + path, ext = os.path.splitext(self.input_data) + self.browse = GeoDataset(path + extension) diff --git a/plio/io/tests/test_io_spectral_profiler.py b/plio/io/tests/test_io_spectral_profiler.py index 61127a6053936d90212d0841b4948b7cf489897a..3d05e05c4241e3fbc900aa9579072238a3bb06d6 100644 --- a/plio/io/tests/test_io_spectral_profiler.py +++ b/plio/io/tests/test_io_spectral_profiler.py @@ -8,7 +8,7 @@ sys.path.insert(0, os.path.abspath('..')) from plio.examples import get_path from plio.io import io_spectral_profiler - +from plio.io.io_gdal import GeoDataset class Test_Spectral_Profiler_IO(unittest.TestCase): @@ -21,6 +21,11 @@ class Test_Spectral_Profiler_IO(unittest.TestCase): self.assertIsInstance(ds.spectra, pd.Panel) self.assertEqual(ds.spectra[0].columns.tolist(), ['RAW', 'REF1', 'REF2', 'QA']) + def test_read_browse(self): + ds = io_spectral_profiler.Spectral_Profiler(self.examplefile) + ds.open_browse() + self.assertIsInstance(ds.browse, GeoDataset) + self.assertEqual(ds.browse.read_array().shape, (512, 456)) if __name__ == '__main__': unittest.main() diff --git a/setup.py b/setup.py index 61e417d15fadcea8592a3691c343ae4f66a3eb05..ca82517062677bfeed26e5018d5a51770cf169a7 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ def setup_package(): else: glob_name = 'examples/' + i + '/*' examples.add(glob_name) - + setup( name = "plio", version = VERSION, @@ -37,13 +37,16 @@ def setup_package(): ['sqlalchemy_json/*.py', 'sqlalchemy_json/LICENSE']}, zip_safe=False, install_requires=[ - 'gdal>=2', + 'gdal', + 'numpy', 'pvl', 'protobuf==3.0.0b2', 'h5py', + 'icu', 'pandas', 'sqlalchemy', - 'pyyaml'], + 'pyyaml', + 'certifi'], classifiers=[ "Development Status :: 3 - Alpha", "Topic :: Utilities",