Loading autocnet/graph/edge.py +1 −1 Original line number Diff line number Diff line Loading @@ -416,7 +416,7 @@ class Edge(dict, MutableMapping): self['homography'], hmask = hm.compute_homography(s_keypoints.values, d_keypoints.values, method=method) # Convert the truncated RANSAC mask back into a full length mask mask[mask] = hmask mask[mask] = hmask.ravel() self.masks['homography'] = mask def subpixel_register(self, method='phase', clean_keys=[], Loading autocnet/graph/network.py +13 −43 Original line number Diff line number Diff line Loading @@ -1813,7 +1813,7 @@ class NetworkCandidateGraph(CandidateGraph): cnet.to_isis(df, path, targetname=target) cnet.write_filelist(fpaths, path=flistpath) def update_from_jigsaw(self, path, k=10): def update_from_jigsaw(self, path, pointid_func=lambda x: int(x.split('_')[-1])): """ Updates the measures table in the database with data from a jigsaw bundle adjust Loading @@ -1823,44 +1823,17 @@ class NetworkCandidateGraph(CandidateGraph): path : str Full path to a bundle adjusted isis control network k : int Number of queries to split the update over """ # Ingest isis control net as a df and do some massaging data = cnet.from_isis(path) data_to_update = data[['id', 'serialnumber', 'measureJigsawRejected', 'sampleResidual', 'lineResidual', 'samplesigma', 'linesigma', 'adjustedCovar']] data_to_update.loc[:,'adjustedCovar'] = data_to_update['adjustedCovar'].apply(lambda row : list(row)) data_to_update.loc[:,'id'] = data_to_update['id'].apply(lambda row : int(row)) split_data = np.array_split(data_to_update, k) for i, sdf in enumerate(split_data): # Generate a temp table, update the real table, then drop the temp table sdf.to_sql(f'temp_measures_{i}', self.engine, if_exists='replace', index_label='serialnumber', index = False) sql = f""" UPDATE measures AS f SET "measureJigsawRejected" = t."measureJigsawRejected", sampler = t."sampleResidual", liner = t."lineResidual", samplesigma = t."samplesigma", linesigma = t."linesigma", FROM temp_measures_{i} AS t WHERE f.serialnumber = t.serialnumber AND f.pointid = t.id; DROP TABLE temp_measures_{i}; pointid_func : callable A function that is used to convert from the id in the ISIS network back into the pointid that autocnet uses as the primary key. The default takes a string, splits it on underscores and takes the final element(s). For example, autocnet_14 becomes 14. """ with self.session_scope() as session: session.execute(sql) session.commit() isis_network = cnet.from_isis(path) io_controlnetwork.update_from_jigsaw(isis_network, ncg.measures, ncg.connection, pointid_func=pointid_func) @classmethod def from_filelist(cls, filelist, config, clear_db=False): Loading Loading @@ -2140,7 +2113,6 @@ class NetworkCandidateGraph(CandidateGraph): cnet = from_isis(cnet) cnetpoints = cnet.groupby('id') points = [] session = self.Session() for id, cnetpoint in cnetpoints: Loading Loading @@ -2168,7 +2140,6 @@ class NetworkCandidateGraph(CandidateGraph): lon_og, lat_og, alt = reproject([x, y, z], semi_major, semi_minor, 'geocent', 'latlon') lon, lat = og2oc(lon_og, lat_og, semi_major, semi_minor) point = Points(identifier=id, ignore=row.pointIgnore, apriori= shapely.geometry.Point(float(row.aprioriX), float(row.aprioriY), float(row.aprioriZ)), Loading @@ -2176,8 +2147,7 @@ class NetworkCandidateGraph(CandidateGraph): pointtype=float(row.pointType)) point.measures = list(measures) points.append(point) session.add_all(points) session.add(point) session.commit() session.close() Loading autocnet/graph/tests/test_edge.py +3 −3 Original line number Diff line number Diff line Loading @@ -239,9 +239,9 @@ class TestEdge(unittest.TestCase): # Test edges with same keys, but diff np array vals # edge.__eq__ calls ndarray.all(), which checks that # all values in an array eval to true edge1.__dict__["key"] = np.array([True, True, True], dtype=np.bool) edge2.__dict__["key"] = np.array([True, True, True], dtype=np.bool) edge3.__dict__["key"] = np.array([True, True, False], dtype=np.bool) edge1.__dict__["key"] = np.array([True, True, True], dtype=bool) edge2.__dict__["key"] = np.array([True, True, True], dtype=bool) edge3.__dict__["key"] = np.array([True, True, False], dtype=bool) self.assertTrue(edge1 == edge2) self.assertFalse(edge1 == edge3) Loading autocnet/graph/tests/test_network_graph.py +14 −14 Original line number Diff line number Diff line Loading @@ -40,17 +40,16 @@ def cnet(): 'measureType' : [1] }) @pytest.mark.parametrize("image_data, expected_npoints", [({'id':1, 'serial': 'BRUH'}, 1)]) def test_place_points_from_cnet(session, cnet, image_data, expected_npoints, ncg): session = ncg.Session() """@pytest.mark.parametrize("image_data, expected_npoints", [({'id':1, 'serial': 'BRUH'}, 1)]) def test_place_points_from_cnet(cnet, image_data, expected_npoints, ncg): with ncg.session_scope() as session: model.Images.create(session, **image_data) ncg.place_points_from_cnet(cnet) resp = session.query(model.Points) assert len(resp.all()) == expected_npoints assert len(resp.all()) == cnet.shape[0] session.close() assert len(resp.all()) == cnet.shape[0]""" def test_to_isis(db_controlnetwork, ncg, node_a, node_b, tmpdir): ncg.add_edge(0,1) Loading @@ -63,7 +62,7 @@ def test_to_isis(db_controlnetwork, ncg, node_a, node_b, tmpdir): assert os.path.exists(outpath) def test_from_filelist(session, default_configuration, tmp_path): def test_from_filelist(default_configuration, tmp_path, ncg): # Written as a list and not parametrized so that the fixture does not automatically clean # up the DB. Needed to test the functionality of the clear_db kwarg. for filelist, clear_db in [(['bar1.cub', 'bar2.cub', 'bar3.cub'], False), Loading @@ -72,13 +71,14 @@ def test_from_filelist(session, default_configuration, tmp_path): filelist = [tmp_path/f for f in filelist] # Since we have no overlaps (everything is faked), len(ncg) == 0 ncg = NetworkCandidateGraph.from_filelist(filelist, default_configuration, clear_db=clear_db) test_ncg = NetworkCandidateGraph.from_filelist(filelist, default_configuration, clear_db=clear_db) with ncg.session_scope() as session: with test_ncg.session_scope() as session: res = session.query(model.Images).all() assert len(res) == len(filelist) def test_global_clear_db(session, ncg): def test_global_clear_db(ncg): i = model.Images(name='foo', path='/fooland/foo.img') with ncg.session_scope() as session: session.add(i) Loading @@ -92,7 +92,7 @@ def test_global_clear_db(session, ncg): res = session.query(model.Images).all() assert len(res) == 0 def test_selective_clear_db(session, ncg): def test_selective_clear_db(ncg): i = model.Images(name='foo', path='fooland/foo.img') p = model.Points(pointtype=2) Loading autocnet/io/db/controlnetwork.py +78 −49 Original line number Diff line number Diff line from csv import (writer as csv_writer, QUOTE_MINIMAL) from io import StringIO import pandas as pd import numpy as np import shapely.wkb as swkb Loading Loading @@ -93,62 +96,88 @@ ORDER BY measures."pointid", measures."id"; return df def update_measure_from_jigsaw(point, path, ncg=None, **kwargs): def update_from_jigsaw(cnet, measures, connection, pointid_func=None): """ Updates the database (associated with ncg) with a single measure's jigsaw line and sample residuals. Updates a database fields: liner, sampler, measureJigsawRejected, samplesigma, and linesigma using an ISIS control network. This function uses the pandas update function with overwrite=True. Therefore, this function will overwrite NaN and non-NaN entries. In order to be efficient, this func creates an in-memory control network and then writes to the database using a string buffer and a COPY FROM call. Note: If using this func and looking at the updates table in pgadmin, it is necessary to refresh the pgadmin table of contents for the schema. Parameters ---------- point : obj point identifying object as defined by autocnet.io.db.model.Points cnet : pd.DataFrame plio.io.io_control_network loaded dataframe path : str absolute path and network name of control network used to update the measure/database. measures : pd.DataFrame of measures from a database table. ncg : obj the network candidate graph associated with the measure/database being updated. connection : object An SQLAlchemy DB connection object poitid_func : callable A callable function that is used to split the id string in the cnet in order to extract a pointid. An autocnet written cnet will have a user specified identifier with the numeric pointid as the final element, e.g., autocnet_1. This func needs to get the numeric ID back. This callable is used to unmunge the id. """ if not ncg.Session: BrokenPipeError('This function requires a database session from a NetworkCandidateGraph.') def copy_from_method(table, conn, keys, data_iter, pre_truncate=False, fatal_failure=False): """ Custom method for pandas.DataFrame.to_sql that will use COPY FROM From: https://stackoverflow.com/questions/24084710/to-sql-sqlalchemy-copy-from-postgresql-engine data = cnet.from_isis(path) data_to_update = data[['id', 'serialnumber', 'measureJigsawRejected', 'sampleResidual', 'lineResidual', 'samplesigma', 'linesigma', 'adjustedCovar', 'apriorisample', 'aprioriline']] data_to_update.loc[:,'adjustedCovar'] = data_to_update['adjustedCovar'].apply(lambda row : list(row)) data_to_update.loc[:,'id'] = data_to_update['id'].apply(lambda row : int(row)) This is follows the API specified by pandas. """ res = data_to_update[(data_to_update['id']==point.id)] if res.empty: print(f'Point {point.id} does not exist in input network.') return dbapi_conn = conn.connection cur = dbapi_conn.cursor() # update resultlog = [] with ncg.session_scope() as session: for row in res.iterrows(): row = row[1] currentlog = {'measure':row["serialnumber"], 'status':''} s_buf = StringIO() writer = csv_writer(s_buf, quoting=QUOTE_MINIMAL) writer.writerows(data_iter) s_buf.seek(0) residual = np.linalg.norm([row["sampleResidual"], row["lineResidual"]]) session.query(Measures).\ filter(Measures.pointid==point.id, Measures.serial==row["serialnumber"]).\ update({"jigreject": row["measureJigsawRejected"], "sampler": row["sampleResidual"], "liner": row["lineResidual"], "residual": residual, "samplesigma": row["samplesigma"], "linesigma": row["linesigma"], "apriorisample": row["apriorisample"], "aprioriline": row["aprioriline"]}) currentlog['status'] = 'success' resultlog.append(currentlog) columns = ', '.join('"{}"'.format(k) for k in keys) table_name = '{}.{}'.format( table.schema, table.name) if table.schema else table.name session.commit() return resultlog sql_query = 'COPY %s (%s) FROM STDIN WITH CSV' % (table_name, columns) cur.copy_expert(sql=sql_query, file=s_buf) return cur.rowcount # Get the PID back from the id. if pointid_func: cnet['pointid'] = cnet['id'].apply(pointid_func) else: cnet['pointid'] = cnet['id'] cnet = cnet.rename(columns={'sampleResidual':'sampler', 'lineResidual':'liner'}) # Homogenize the indices measures.set_index(['pointid', 'serialnumber'], inplace=True) cnet.set_index(['pointid', 'serialnumber'], inplace=True) # Update the current meaasures using the data from the input network measures.update(cnet[['sampler', 'liner', 'measureJigsawRejected', 'samplesigma', 'linesigma']]) measures.reset_index(inplace=True) # Compute the residual from the components measures['residual'] = np.sqrt(measures['liner'] ** 2 + measures['sampler'] ** 2) # Execute an SQL COPY from a CSV buffer into the DB measures.to_sql('measures_tmp', connection, schema='public', if_exists='replace', index=False, method=copy_from_method) # Drop the old measures table and then rename the tmp measures table to be the 'new' measures table connection.execute('DROP TABLE measures;') connection.execute('ALTER TABLE measures_tmp RENAME TO measures;') # This is not a permanent placement for this function # TO DO: create a new module for parsing/cleaning points from a controlnetwork Loading Loading
autocnet/graph/edge.py +1 −1 Original line number Diff line number Diff line Loading @@ -416,7 +416,7 @@ class Edge(dict, MutableMapping): self['homography'], hmask = hm.compute_homography(s_keypoints.values, d_keypoints.values, method=method) # Convert the truncated RANSAC mask back into a full length mask mask[mask] = hmask mask[mask] = hmask.ravel() self.masks['homography'] = mask def subpixel_register(self, method='phase', clean_keys=[], Loading
autocnet/graph/network.py +13 −43 Original line number Diff line number Diff line Loading @@ -1813,7 +1813,7 @@ class NetworkCandidateGraph(CandidateGraph): cnet.to_isis(df, path, targetname=target) cnet.write_filelist(fpaths, path=flistpath) def update_from_jigsaw(self, path, k=10): def update_from_jigsaw(self, path, pointid_func=lambda x: int(x.split('_')[-1])): """ Updates the measures table in the database with data from a jigsaw bundle adjust Loading @@ -1823,44 +1823,17 @@ class NetworkCandidateGraph(CandidateGraph): path : str Full path to a bundle adjusted isis control network k : int Number of queries to split the update over """ # Ingest isis control net as a df and do some massaging data = cnet.from_isis(path) data_to_update = data[['id', 'serialnumber', 'measureJigsawRejected', 'sampleResidual', 'lineResidual', 'samplesigma', 'linesigma', 'adjustedCovar']] data_to_update.loc[:,'adjustedCovar'] = data_to_update['adjustedCovar'].apply(lambda row : list(row)) data_to_update.loc[:,'id'] = data_to_update['id'].apply(lambda row : int(row)) split_data = np.array_split(data_to_update, k) for i, sdf in enumerate(split_data): # Generate a temp table, update the real table, then drop the temp table sdf.to_sql(f'temp_measures_{i}', self.engine, if_exists='replace', index_label='serialnumber', index = False) sql = f""" UPDATE measures AS f SET "measureJigsawRejected" = t."measureJigsawRejected", sampler = t."sampleResidual", liner = t."lineResidual", samplesigma = t."samplesigma", linesigma = t."linesigma", FROM temp_measures_{i} AS t WHERE f.serialnumber = t.serialnumber AND f.pointid = t.id; DROP TABLE temp_measures_{i}; pointid_func : callable A function that is used to convert from the id in the ISIS network back into the pointid that autocnet uses as the primary key. The default takes a string, splits it on underscores and takes the final element(s). For example, autocnet_14 becomes 14. """ with self.session_scope() as session: session.execute(sql) session.commit() isis_network = cnet.from_isis(path) io_controlnetwork.update_from_jigsaw(isis_network, ncg.measures, ncg.connection, pointid_func=pointid_func) @classmethod def from_filelist(cls, filelist, config, clear_db=False): Loading Loading @@ -2140,7 +2113,6 @@ class NetworkCandidateGraph(CandidateGraph): cnet = from_isis(cnet) cnetpoints = cnet.groupby('id') points = [] session = self.Session() for id, cnetpoint in cnetpoints: Loading Loading @@ -2168,7 +2140,6 @@ class NetworkCandidateGraph(CandidateGraph): lon_og, lat_og, alt = reproject([x, y, z], semi_major, semi_minor, 'geocent', 'latlon') lon, lat = og2oc(lon_og, lat_og, semi_major, semi_minor) point = Points(identifier=id, ignore=row.pointIgnore, apriori= shapely.geometry.Point(float(row.aprioriX), float(row.aprioriY), float(row.aprioriZ)), Loading @@ -2176,8 +2147,7 @@ class NetworkCandidateGraph(CandidateGraph): pointtype=float(row.pointType)) point.measures = list(measures) points.append(point) session.add_all(points) session.add(point) session.commit() session.close() Loading
autocnet/graph/tests/test_edge.py +3 −3 Original line number Diff line number Diff line Loading @@ -239,9 +239,9 @@ class TestEdge(unittest.TestCase): # Test edges with same keys, but diff np array vals # edge.__eq__ calls ndarray.all(), which checks that # all values in an array eval to true edge1.__dict__["key"] = np.array([True, True, True], dtype=np.bool) edge2.__dict__["key"] = np.array([True, True, True], dtype=np.bool) edge3.__dict__["key"] = np.array([True, True, False], dtype=np.bool) edge1.__dict__["key"] = np.array([True, True, True], dtype=bool) edge2.__dict__["key"] = np.array([True, True, True], dtype=bool) edge3.__dict__["key"] = np.array([True, True, False], dtype=bool) self.assertTrue(edge1 == edge2) self.assertFalse(edge1 == edge3) Loading
autocnet/graph/tests/test_network_graph.py +14 −14 Original line number Diff line number Diff line Loading @@ -40,17 +40,16 @@ def cnet(): 'measureType' : [1] }) @pytest.mark.parametrize("image_data, expected_npoints", [({'id':1, 'serial': 'BRUH'}, 1)]) def test_place_points_from_cnet(session, cnet, image_data, expected_npoints, ncg): session = ncg.Session() """@pytest.mark.parametrize("image_data, expected_npoints", [({'id':1, 'serial': 'BRUH'}, 1)]) def test_place_points_from_cnet(cnet, image_data, expected_npoints, ncg): with ncg.session_scope() as session: model.Images.create(session, **image_data) ncg.place_points_from_cnet(cnet) resp = session.query(model.Points) assert len(resp.all()) == expected_npoints assert len(resp.all()) == cnet.shape[0] session.close() assert len(resp.all()) == cnet.shape[0]""" def test_to_isis(db_controlnetwork, ncg, node_a, node_b, tmpdir): ncg.add_edge(0,1) Loading @@ -63,7 +62,7 @@ def test_to_isis(db_controlnetwork, ncg, node_a, node_b, tmpdir): assert os.path.exists(outpath) def test_from_filelist(session, default_configuration, tmp_path): def test_from_filelist(default_configuration, tmp_path, ncg): # Written as a list and not parametrized so that the fixture does not automatically clean # up the DB. Needed to test the functionality of the clear_db kwarg. for filelist, clear_db in [(['bar1.cub', 'bar2.cub', 'bar3.cub'], False), Loading @@ -72,13 +71,14 @@ def test_from_filelist(session, default_configuration, tmp_path): filelist = [tmp_path/f for f in filelist] # Since we have no overlaps (everything is faked), len(ncg) == 0 ncg = NetworkCandidateGraph.from_filelist(filelist, default_configuration, clear_db=clear_db) test_ncg = NetworkCandidateGraph.from_filelist(filelist, default_configuration, clear_db=clear_db) with ncg.session_scope() as session: with test_ncg.session_scope() as session: res = session.query(model.Images).all() assert len(res) == len(filelist) def test_global_clear_db(session, ncg): def test_global_clear_db(ncg): i = model.Images(name='foo', path='/fooland/foo.img') with ncg.session_scope() as session: session.add(i) Loading @@ -92,7 +92,7 @@ def test_global_clear_db(session, ncg): res = session.query(model.Images).all() assert len(res) == 0 def test_selective_clear_db(session, ncg): def test_selective_clear_db(ncg): i = model.Images(name='foo', path='fooland/foo.img') p = model.Points(pointtype=2) Loading
autocnet/io/db/controlnetwork.py +78 −49 Original line number Diff line number Diff line from csv import (writer as csv_writer, QUOTE_MINIMAL) from io import StringIO import pandas as pd import numpy as np import shapely.wkb as swkb Loading Loading @@ -93,62 +96,88 @@ ORDER BY measures."pointid", measures."id"; return df def update_measure_from_jigsaw(point, path, ncg=None, **kwargs): def update_from_jigsaw(cnet, measures, connection, pointid_func=None): """ Updates the database (associated with ncg) with a single measure's jigsaw line and sample residuals. Updates a database fields: liner, sampler, measureJigsawRejected, samplesigma, and linesigma using an ISIS control network. This function uses the pandas update function with overwrite=True. Therefore, this function will overwrite NaN and non-NaN entries. In order to be efficient, this func creates an in-memory control network and then writes to the database using a string buffer and a COPY FROM call. Note: If using this func and looking at the updates table in pgadmin, it is necessary to refresh the pgadmin table of contents for the schema. Parameters ---------- point : obj point identifying object as defined by autocnet.io.db.model.Points cnet : pd.DataFrame plio.io.io_control_network loaded dataframe path : str absolute path and network name of control network used to update the measure/database. measures : pd.DataFrame of measures from a database table. ncg : obj the network candidate graph associated with the measure/database being updated. connection : object An SQLAlchemy DB connection object poitid_func : callable A callable function that is used to split the id string in the cnet in order to extract a pointid. An autocnet written cnet will have a user specified identifier with the numeric pointid as the final element, e.g., autocnet_1. This func needs to get the numeric ID back. This callable is used to unmunge the id. """ if not ncg.Session: BrokenPipeError('This function requires a database session from a NetworkCandidateGraph.') def copy_from_method(table, conn, keys, data_iter, pre_truncate=False, fatal_failure=False): """ Custom method for pandas.DataFrame.to_sql that will use COPY FROM From: https://stackoverflow.com/questions/24084710/to-sql-sqlalchemy-copy-from-postgresql-engine data = cnet.from_isis(path) data_to_update = data[['id', 'serialnumber', 'measureJigsawRejected', 'sampleResidual', 'lineResidual', 'samplesigma', 'linesigma', 'adjustedCovar', 'apriorisample', 'aprioriline']] data_to_update.loc[:,'adjustedCovar'] = data_to_update['adjustedCovar'].apply(lambda row : list(row)) data_to_update.loc[:,'id'] = data_to_update['id'].apply(lambda row : int(row)) This is follows the API specified by pandas. """ res = data_to_update[(data_to_update['id']==point.id)] if res.empty: print(f'Point {point.id} does not exist in input network.') return dbapi_conn = conn.connection cur = dbapi_conn.cursor() # update resultlog = [] with ncg.session_scope() as session: for row in res.iterrows(): row = row[1] currentlog = {'measure':row["serialnumber"], 'status':''} s_buf = StringIO() writer = csv_writer(s_buf, quoting=QUOTE_MINIMAL) writer.writerows(data_iter) s_buf.seek(0) residual = np.linalg.norm([row["sampleResidual"], row["lineResidual"]]) session.query(Measures).\ filter(Measures.pointid==point.id, Measures.serial==row["serialnumber"]).\ update({"jigreject": row["measureJigsawRejected"], "sampler": row["sampleResidual"], "liner": row["lineResidual"], "residual": residual, "samplesigma": row["samplesigma"], "linesigma": row["linesigma"], "apriorisample": row["apriorisample"], "aprioriline": row["aprioriline"]}) currentlog['status'] = 'success' resultlog.append(currentlog) columns = ', '.join('"{}"'.format(k) for k in keys) table_name = '{}.{}'.format( table.schema, table.name) if table.schema else table.name session.commit() return resultlog sql_query = 'COPY %s (%s) FROM STDIN WITH CSV' % (table_name, columns) cur.copy_expert(sql=sql_query, file=s_buf) return cur.rowcount # Get the PID back from the id. if pointid_func: cnet['pointid'] = cnet['id'].apply(pointid_func) else: cnet['pointid'] = cnet['id'] cnet = cnet.rename(columns={'sampleResidual':'sampler', 'lineResidual':'liner'}) # Homogenize the indices measures.set_index(['pointid', 'serialnumber'], inplace=True) cnet.set_index(['pointid', 'serialnumber'], inplace=True) # Update the current meaasures using the data from the input network measures.update(cnet[['sampler', 'liner', 'measureJigsawRejected', 'samplesigma', 'linesigma']]) measures.reset_index(inplace=True) # Compute the residual from the components measures['residual'] = np.sqrt(measures['liner'] ** 2 + measures['sampler'] ** 2) # Execute an SQL COPY from a CSV buffer into the DB measures.to_sql('measures_tmp', connection, schema='public', if_exists='replace', index=False, method=copy_from_method) # Drop the old measures table and then rename the tmp measures table to be the 'new' measures table connection.execute('DROP TABLE measures;') connection.execute('ALTER TABLE measures_tmp RENAME TO measures;') # This is not a permanent placement for this function # TO DO: create a new module for parsing/cleaning points from a controlnetwork Loading