diff --git a/CHANGELOG.md b/CHANGELOG.md index 67ff5de8abb4c5b712f1d3fddd7877a3b27838d2..e25120629952e5c31e0a79aea57b7fd103afea9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,9 @@ release. - Functionality to add measure to a point - Functionality to convert from sample, line (x,y) pixel coordinates to Body-Centered, Body-Fixed (BCBF) coordinates in meters. - Functionality to test for valid input images. +- Refactored place_points_in_overlap to make it easier to understand and work with +- Created `sensor.py` that creates a class for either a 'csm' camera sensor or an 'isis' camera sensor. This removes confusing and wordy code. Now just need to create a sensor object based on the input 'csm' or 'isis'. Code inside classes will figure out the rest. +- Fuctionality to convert between oc2xyz and xyz2oc in `spatial.py` ### Fixed - string injection via format with sqlalchemy text() object. diff --git a/autocnet/io/db/model.py b/autocnet/io/db/model.py index 769835c7afdc62fbac06c4bdd1e64b707816ebe3..cb1e7920d55e597b5bb8455f611a09a61eb00a6c 100644 --- a/autocnet/io/db/model.py +++ b/autocnet/io/db/model.py @@ -584,7 +584,7 @@ class Points(Base, BaseMixin): choosername=choosername)) return point - def add_measures_to_point(self, candidates, choosername='autocnet'): + def add_measures_to_point(self, candidates, sensor, choosername='autocnet', **kwargs): """ Attempt to add 1+ measures to a point from a list of candidate nodes. The function steps over each node and attempts to use the node's sensor model @@ -603,10 +603,9 @@ class Points(Base, BaseMixin): if not os.path.exists(node['image_path']): log.info(f'Unable to find input image {node["image_path"]}') continue - try: - # ToDo: We want ot abstract away this to be a generic sensor model. No more 'isis' vs. 'csm' in the code - sample, line = isis.ground_to_image(node["image_path"], self.geom.x, self.geom.y) + # TODO: Take this projection out of the CSM model and work it into the point + sample, line = sensor.calculate_sample_line(node, self.geom.x, self.geom.y, **kwargs) except: log.info(f"{node['image_path']} failed ground_to_image. Likely due to being processed incorrectly or is just a bad image that failed campt.") diff --git a/autocnet/io/db/tests/test_points.py b/autocnet/io/db/tests/test_points.py index 080aecc268d09d1b116e4bd5e8b0607c0d61e963..e32503e538d0f23a33515b6b18c7d55a216ad5fa 100644 --- a/autocnet/io/db/tests/test_points.py +++ b/autocnet/io/db/tests/test_points.py @@ -3,6 +3,7 @@ import pytest import shapely from autocnet.io.db.model import Points +from autocnet.spatial import sensor def test_points_exists(tables): assert Points.__tablename__ in tables @@ -72,6 +73,7 @@ def test_create_point_with_reference_measure(session): def test_add_measures_to_point(session): point = Points() point.adjusted = shapely.Point(0,0,0) + test_sensor = sensor.create_sensor('isis') node = MagicMock() node.isis_serial = 'serial' @@ -81,7 +83,7 @@ def test_add_measures_to_point(session): with patch('autocnet.spatial.isis.ground_to_image') as mocked_call: mocked_call.return_value = (0.5, 0.5) - point.add_measures_to_point(reference_nodes) + point.add_measures_to_point(reference_nodes, test_sensor) assert len(point.measures) == 4 assert point.measures[0].line == 0.5 diff --git a/autocnet/matcher/validation.py b/autocnet/matcher/validation.py new file mode 100644 index 0000000000000000000000000000000000000000..4a9b007974b8b0752ce943e8b0bbad0509bdd5b7 --- /dev/null +++ b/autocnet/matcher/validation.py @@ -0,0 +1,72 @@ +import numpy as np + +def is_valid_lroc_polar_image(roi_array, + include_var=True, + include_mean=False, + include_std=False): + """ + Checks if a numpy array representing an ROI from an lorc polar image is valid. + Can check using variance, mean, and standard deviation, baed on user input. + It is highly encouraged that at the very least the variance check is used. + + Parameters + __________ + roi_array : np.array + A numpy array representing a ROI from an image, meaning the values are pixels + include_var : bool + Choose whether to filter images based on variance. Default True. + include_mean : bool + Choose whether to filter images based on mean. Default True. + Goal is to get rid of overally dark images. + include_std : bool + Choose whether to filter images based on standard deviation. Default True. + Goal is to get rid of overally saturated images. + + Returns + _______ + is_valid : bool + Returns True is passes the checks, returns false otherwise. + """ + functions = [] + + if include_var: + # Get rid of super bad images + var_func = lambda x : False if np.var(roi_array) == 0 else True + functions.append(var_func) + if include_mean: + # Get rid of overally dark images + mean_func = lambda x : False if np.mean(roi_array) < 0.0005 else True + functions.append(mean_func) + if include_std: + # Get rid over overally saturated images + std_func = lambda x : False if np.std(roi_array) > 0.001 else True + functions.append(std_func) + + return all(func(roi_array) for func in functions) + +def is_valid_lroc_image(roi_array, include_var=True): + """ + Checks if a numpy array representing an ROI from an lroc image is valid. + Can check using variance, based on user input. + It is highly encouraged that the variance check is used. + + Parameters + __________ + roi_array : np.array + A numpy array representing a ROI from an image, meaning the values are pixels + include_var : bool + Choose whether to filter images based on variance. Default True. + + Returns + _______ + is_valid : bool + Returns True is passes the checks, returns false otherwise. + """ + functions = [] + + if include_var: + # Get rid of super bad images + var_func = lambda x : False if np.var(roi_array) == 0 else True + functions.append(var_func) + + return all(func(roi_array) for func in functions) \ No newline at end of file diff --git a/autocnet/spatial/centroids.py b/autocnet/spatial/centroids.py index 61d65de35132641fe94067cd549a72ed773d3d53..c413a06e0de5c104ee13047a8415b83251c77f09 100644 --- a/autocnet/spatial/centroids.py +++ b/autocnet/spatial/centroids.py @@ -1,18 +1,17 @@ import json import logging import math -import os import numpy as np import shapely -from sqlalchemy import text from autocnet.cg.cg import create_points_along_line -from autocnet.io.db.model import Images, Measures, Overlay, Points, JsonEncoder +from autocnet.io.db.model import Images, Points, JsonEncoder from autocnet.graph.node import NetworkNode from autocnet.spatial import isis from autocnet.transformation import roi from autocnet.matcher.cpu_extractor import extract_most_interesting +from autocnet.matcher.validation import is_valid_lroc_polar_image import time # Set up logging file @@ -143,50 +142,6 @@ def find_points_in_centroids(radius, points.extend(line_points) return points -def is_valid_lroc_polar_image(roi_array, - include_var=True, - include_mean=False, - include_std=False): - """ - Checks if a numpy array representing an ROI from an image is valid. - Can check using variance, mean, and standard deviation, baed on unser input. - It is highly encouraged that at the very least the variance check is used. - - Parameters - __________ - roi_array : np.array - A numpy array representing a ROI from an image, meaning the values are pixels - include_var : bool - Choose whether to filter images based on variance. Default True. - include_mean : bool - Choose whether to filter images based on mean. Default True. - Goal is to get rid of overally dark images. - include_std : bool - Choose whether to filter images based on standard deviation. Default True. - Goal is to get rid of overally saturated images. - - Returns - _______ - is_valid : bool - Returns True is passes the checks, returns false otherwise. - """ - functions = [] - - if include_var: - # Get rid of super bad images - var_func = lambda x : False if np.var(roi_array) == 0 else True - functions.append(var_func) - if include_mean: - # Get rid of overally dark images - mean_func = lambda x : False if np.mean(roi_array) < 0.0005 else True - functions.append(mean_func) - if include_std: - # Get rid over overally saturated images - std_func = lambda x : False if np.std(roi_array) > 0.001 else True - functions.append(std_func) - - return all(func(roi_array) for func in functions) - def find_intresting_point(nodes, lon, lat, size=71): """ Find an intresting point close the given lon, lat given a list data structure that contains diff --git a/autocnet/spatial/overlap.py b/autocnet/spatial/overlap.py index 45829f2259cc9e6080b62d119254f6e6932bff6c..5d85ad133165b8935306aac144b2f9aaf3340881 100644 --- a/autocnet/spatial/overlap.py +++ b/autocnet/spatial/overlap.py @@ -1,28 +1,21 @@ -import os import time import logging -import json -from subprocess import CalledProcessError import warnings -from redis import StrictRedis -import numpy as np -import pyproj import shapely -import sqlalchemy -from plio.io.io_gdal import GeoDataset - +import json +from subprocess import CalledProcessError +import csmapi from autocnet.cg import cg as compgeom from autocnet.graph.node import NetworkNode from autocnet.io.db.model import Images, Measures, Overlay, Points, JsonEncoder +from autocnet.spatial import sensor +from autocnet.transformation import roi from autocnet.spatial import isis from autocnet.matcher.cpu_extractor import extract_most_interesting -from autocnet.transformation.spatial import reproject, og2oc, oc2og -from autocnet.transformation import roi - -from plurmy import Slurm -import csmapi +from autocnet.matcher.validation import is_valid_lroc_image +from autocnet.transformation.spatial import reproject, og2oc, oc2og, oc2xyz, xyz2oc # set up the logger file log = logging.getLogger(__name__) @@ -68,10 +61,93 @@ def place_points_in_overlaps(size_threshold=0.0007, point_type=point_type, ncg=ncg) +def find_interesting_point(nodes, lon, lat, sensor, size=71, **kwargs): + """ + Find an interesting point close the given lon, lat given a list data structure that contains + the image_path and the geodata for the image. + + Parameters + ___________ + nodes : list + A list of autocnet.graph.node or data structure objects containing image_path and geodata + This contains the image data for all the images the intersect that lat/lon + + lon : int + The longitude of the point one is interested in + + lat : int + The latitude of the point one is interested in + + size : int + The amount of pixel around a points initial location to search for an + interesting feature to which to shift the point. + kwargs : dict + Contain information to be used if the csm sensor model is passed + csm_kwargs = { + 'semi_major': int, + 'semi_minor': int, + 'height': int, + 'ncg': obj, + 'needs_projection': bool + } + Returns + _______ + reference_index : int + An index that refers to image that was choosen to be used as the reference image. + This is the image in which an interesting point was found. + + point : shapely.geometry.point + The intresting point close to the given lat/lon + + """ + # Itterate through the images to find an interesting point + for reference_index, node in enumerate(nodes): + log.debug(f'Trying image: {node["image_path"].split("/")[-1]}') + # reference_index is the index into the list of measures for the image that is not shifted and is set at the + # reference against which all other images are registered. + sample, line = sensor.calculate_sample_line(node, lon, lat, **kwargs) + + # If sample/line are None, point is not in image + if sample == None or line == None: + log.info(f'point ({lon}, {lat}) does not project to reference image {node["image_path"]}') + continue + + # Extract ORB features in a sub-image around the desired point + image_roi = roi.Roi(node.geodata, sample, line, size_x=size, size_y=size) + try: + roi_array = image_roi.clipped_array # Units are pixels for the array + except: + log.info(f'Failed to find interesting features in image.') + continue + + # Check if the image is valid and could be used as the reference + if not is_valid_lroc_image(roi_array, include_var=True, include_mean=False, include_std=False): + log.info('Failed to find interesting features in image due to poor quality image.') + continue + + # Extract the most interesting feature in the search window + interesting = extract_most_interesting(image_roi.clipped_array) + + if interesting is not None: + # We have found an interesting feature and have identified the reference point. + # kps are in the image space with upper left origin and the roi could be the requested size + # or smaller if near an image boundary. So use the roi upper left_x and top_y for the actual origin. + left_x, _, top_y, _ = image_roi.image_extent + newsample = left_x + interesting.x + newline = top_y + interesting.y + log.debug(f'Current reference index: {reference_index}.') + return reference_index, shapely.geometry.Point(newsample, newline) + + # Tried all the images, so return a shapely point un-modified, the last sample/line. + log.info('Unable to find an interesting point, falling back to the a priori pointing') + log.debug(f'Current reference index: {reference_index}.') + return reference_index, shapely.geometry.Point(sample, line) + def place_points_in_overlap(overlap, - identifier="autocnet", - cam_type="csm", - size=71, + identifier="place_points_in_overlaps", + cam_type="isis", + interesting_func=find_interesting_point, + interesting_func_kwargs={"size":71}, distribute_points_kwargs={}, point_type=2, ncg=None, @@ -94,30 +170,30 @@ def place_points_in_overlap(overlap, options: {"csm", "isis"} Pick what kind of camera model implementation to use. - size : int - The amount of pixel around a points initial location to search for an - interesting feature to which to shift the point. + interesting_func : callable + A function that takes a list of nodes, a longitude, a latitude, and arbitrary + kwargs and returns a tuple with a reference index (integer) and a shapely Point object - distribute_points_kwargs: dict - kwargs to pass to autocnet.cg.cg.distribute_points_in_geom + interesting_func_kwargs : dict + With keyword arguments required by the passed interesting_func point_type: int - The type of point being placed. Default is pointtype=2, corresponding to - free points. + The type of point being placed. Default is pointtype=2, corresponding to + free points. ncg: obj - An autocnet.graph.network NetworkCandidateGraph instance representing the network - to apply this function to + An autocnet.graph.network NetworkCandidateGraph instance representing the network + to apply this function to use_cache : bool - If False (default) this func opens a database session and writes points - and measures directly to the respective tables. If True, this method writes - messages to the point_insert (defined in ncg.config) redis queue for - asynchronous (higher performance) inserts. - + If False (default) this func opens a database session and writes points + and measures directly to the respective tables. If True, this method writes + messages to the point_insert (defined in ncg.config) redis queue for + asynchronous (higher performance) inserts. + ratio_size : float - Used in calling the function distribute_points_in_geom to determine the - minimum size the ratio can be to be considered a sliver and ignored. + Used in calling the function distribute_points_in_geom to determine the + minimum size the ratio can be to be considered a sliver and ignored. Returns ------- @@ -133,33 +209,26 @@ def place_points_in_overlap(overlap, autocnet.model.io.db.PointType: for the point type options. autocnet.graph.network.NetworkCandidateGraph: for associated properties and functionalities of the NetworkCandidateGraph class + """ t1 = time.time() if not ncg.Session: raise BrokenPipeError('This func requires a database session from a NetworkCandidateGraph.') - + # Determine what sensor type to use - avail_cams = {"isis", "csm"} - cam_type = cam_type.lower() - if cam_type not in cam_type: - raise Exception(f'{cam_type} is not one of valid camera: {avail_cams}') - - points = [] - semi_major = ncg.config['spatial']['semimajor_rad'] - semi_minor = ncg.config['spatial']['semiminor_rad'] + sensor = sensor.create_sensor(cam_type) - ta = time.time() # Determine the point distribution in the overlap geom geom = overlap.geom - valid = compgeom.distribute_points_in_geom(geom, ratio_size=ratio_size, **distribute_points_kwargs, **kwargs) + candidate_points = compgeom.distribute_points_in_geom(geom, ratio_size=ratio_size, **distribute_points_kwargs, **kwargs) if not valid.any(): warnings.warn(f'Failed to distribute points in overlap {overlap.id}') return [] - - log.info(f'Have {len(valid)} potential points to place in overlap {overlap.id}.') - tb = time.time() - log.info(f'Point distribution took {tb-ta} seconds.') - # Setup the node objects that are covered by the geom + + log.info(f'Have {len(candidate_points)} potential points to place in overlap {overlap.id}.') + + # Instantiate the nodes in the NCG. This is done because we assume that the ncg passed is empty + # and part of a cluster submission. nodes = [] with ncg.session_scope() as session: for id in overlap.intersections: @@ -167,180 +236,139 @@ def place_points_in_overlap(overlap, nn = NetworkNode(node_id=id, image_path=res.path) nn.parent = ncg nodes.append(nn) - tc = time.time() - log.info(f'Took {tc-tb} seconds to instantiate {len(nodes)} images.') - - - log.info(f'Attempting to place measures in {len(nodes)} images.') - for v in valid: - log.debug(f'Valid point: {v}') - lon = v[0] - lat = v[1] - - # Calculate the height, the distance (in meters) above or - # below the aeroid (meters above or below the BCBF spheroid). - height = ncg.dem.get_height(lat, lon) - - # Need to get the first node and then convert from lat/lon to image space - for reference_index, node in enumerate(nodes): - log.debug(f'Starting with reference_index: {reference_index}') - # reference_index is the index into the list of measures for the image that is not shifted and is set at the - # reference against which all other images are registered. - if cam_type == "isis": - sample, line = isis.ground_to_image(node["image_path"], lon, lat) - if sample == None or line == None: - log.warning(f'point ({lon}, {lat}) does not project to reference image {node["image_path"]}') - continue - if cam_type == "csm": - lon_og, lat_og = oc2og(lon, lat, semi_major, semi_minor) - x, y, z = reproject([lon_og, lat_og, height], - semi_major, semi_minor, - 'latlon', 'geocent') - # The CSM conversion makes the LLA/ECEF conversion explicit - gnd = csmapi.EcefCoord(x, y, z) - image_coord = node.camera.groundToImage(gnd) - sample, line = image_coord.samp, image_coord.line - - # Extract ORB features in a sub-image around the desired point - image_roi = roi.Roi(node.geodata, sample, line, size_x=size, size_y=size) - try: - if image_roi.variance == 0: - log.warning(f'Failed to find interesting features in image.') - continue - except: - log.warning(f'Failed to find interesting features in image.') - continue - # Extract the most interesting feature in the search window - interesting = extract_most_interesting(image_roi.clipped_array) - if interesting is not None: - # We have found an interesting feature and have identified the reference point. - break - log.debug(f'Current reference index: {reference_index}.') - if interesting is None: - log.warning('Unable to find an interesting point, falling back to the a priori pointing') - newsample = sample - newline = line - else: - # kps are in the image space with upper left origin and the roi - # could be the requested size or smaller if near an image boundary. - # So use the roi upper left_x and top_y for the actual origin. - left_x, _, top_y, _ = image_roi.image_extent - newsample = left_x + interesting.x - newline = top_y + interesting.y - - # Get the updated lat/lon from the feature in the node - if cam_type == "isis": - try: - p = isis.point_info(node["image_path"], newsample, newline, point_type="image") - except CalledProcessError as e: - if 'Requested position does not project in camera model' in e.stderr: - log.exception(node["image_path"]) - log.exception(f'interesting point ({newsample}, {newline}) does not project back to ground') - continue - try: - x, y, z = p["BodyFixedCoordinate"].value - except: - x, y, z = ["BodyFixedCoordinate"] - - if getattr(p["BodyFixedCoordinate"], "units", "None").lower() == "km": - x = x * 1000 - y = y * 1000 - z = z * 1000 - elif cam_type == "csm": - image_coord = csmapi.ImageCoord(newline, newsample) - pcoord = node.camera.imageToGround(image_coord) - # Get the BCEF coordinate from the lon, lat - updated_lon_og, updated_lat_og, _ = reproject([pcoord.x, pcoord.y, pcoord.z], - semi_major, semi_minor, 'geocent', 'latlon') - updated_lon, updated_lat = og2oc(updated_lon_og, updated_lat_og, semi_major, semi_minor) - - updated_height = ncg.dem.get_height(updated_lat, updated_lon) - + + for valid in candidate_points: + add_point_to_overlap_network(valid, + nodes, + sensor, + geom, + identifier=identifier, + interesting_func=interesting_func, + interesting_func_kwargs=interesting_func_kwargs, + point_type=point_type, + ncg=ncg, + use_cache=use_cache, + **kwargs) + t2 = time.time() + log.info(f'Placed {len(candidate_points)} in {t2-t1} seconds.') + +def add_point_to_overlap_network(valid, + nodes, + sensor, + geom, + identifier="place_points_in_overlap", + interesting_func=find_interesting_point, + interesting_func_kwargs={"size":71}, + point_type=2, + ncg=None, + use_cache=False, + **kwargs): + """ + Place points into an centroid geometry by back-projecting using sensor models. + The DEM specified in the config file will be used to calculate point elevations. - # Get the BCEF coordinate from the lon, lat - x, y, z = reproject([updated_lon_og, updated_lat_og, updated_height], - semi_major, semi_minor, 'latlon', 'geocent') + Parameters + ---------- + valid : list + point coordinates in the form (x1,y1) + + nodes: list + list of node objects (the images) + + sensor: string + sensor that is being used, either isis or csm - # If the updated point is outside of the overlap, then revert back to the - # original point and hope the matcher can handle it when sub-pixel registering - updated_lon_og, updated_lat_og, updated_height = reproject([x, y, z], semi_major, semi_minor, - 'geocent', 'latlon') - updated_lon, updated_lat = og2oc(updated_lon_og, updated_lat_og, semi_major, semi_minor) + geom: obj + overlap geom to be used - if not geom.contains(shapely.geometry.Point(updated_lon, updated_lat)): - lon_og, lat_og = oc2og(lon, lat, semi_major, semi_minor) - x, y, z = reproject([lon_og, lat_og, height], - semi_major, semi_minor, 'latlon', 'geocent') - updated_lon_og, updated_lat_og, updated_height = reproject([x, y, z], semi_major, semi_minor, - 'geocent', 'latlon') - updated_lon, updated_lat = og2oc(updated_lon_og, updated_lat_og, semi_major, semi_minor) + interesting_func : callable + A function that takes a list of nodes, a longitude, a latitude, and arbitrary + kwargs and returns a tuple with a reference index (integer) and a shapely Point object - point_geom = shapely.geometry.Point(x, y, z) - log.debug(f'Creating point with reference_index: {reference_index}') - point = Points(identifier=identifier, - overlapid=overlap.id, - apriori=point_geom, - adjusted=point_geom, - pointtype=point_type, # Would be 3 or 4 for ground - cam_type=cam_type, - reference_index=reference_index) + interesting_func_kwargs : dict + With keyword arguments required by the passed interesting_func - # Compute ground point to back project into measurtes - gnd = csmapi.EcefCoord(x, y, z) + point_type: int + The type of point being placed. Default is pointtype=2, corresponding to + free points. - for current_index, node in enumerate(nodes): - if cam_type == "csm": - image_coord = node.camera.groundToImage(gnd) - sample, line = image_coord.samp, image_coord.line - if cam_type == "isis": - # If this try/except fails, then the reference_index could be wrong because the length - # of the measures list is different than the length of the nodes list that was used - # to find the most interesting feature. - if not os.path.exists(node["image_path"]): - log.warning(f'Unable to find input image {node["image_path"]}') - continue - sample, line = isis.ground_to_image(node["image_path"], updated_lon, updated_lat) - if sample == None or line == None: - #except CalledProcessError as e: - #except: # CalledProcessError is not catching the ValueError that this try/except is attempting to handle. - log.warning(f'interesting point ({updated_lon},{updated_lat}) does not project to image {node["image_path"]}') - # If the current_index is greater than the reference_index, the change in list size does - # not impact the positional index of the reference. If current_index is less than the - # reference_index, then the reference_index needs to de-increment by one for each time - # a measure fails to be placed. - if current_index < reference_index: - reference_index -= 1 - log.debug('Reference de-incremented.') - continue - point.measures.append(Measures(sample=sample, - line=line, - apriorisample=sample, - aprioriline=line, - imageid=node['node_id'], - serial=node.isis_serial, - measuretype=3, - choosername='place_points_in_overlap')) - log.debug(f'Current reference index in code: {reference_index}.') - log.debug(f'Current reference index on point: {point.reference_index}') - if len(point.measures) >= 2: - points.append(point) - log.info(f'Able to place {len(points)} points.') + ncg: obj + An autocnet.graph.network NetworkCandidateGraph instance representing the network + to apply this function to - if not points: return + use_cache : bool + If False (default) this func opens a database session and writes points + and measures directly to the respective tables. If True, this method writes + messages to the point_insert (defined in ncg.config) redis queue for + asynchronous (higher performance) inserts. + """ + + t1 = time.time() - # Insert the points into the database asynchronously (via redis) or synchronously via the ncg + log.debug(f'Valid point: {valid}') + lat = valid[1] + lon = valid[0] + + semi_major=ncg.config['spatial']['semimajor_rad'] + semi_minor=ncg.config['spatial']['semiminor_rad'] + height=ncg.dem.get_height(lat, lon) + + # Add additional_kargs for CSM model + csm_kwargs = { + 'semi_major': semi_major, + 'semi_minor': semi_minor, + 'height': height, + 'ncg': ncg, + 'needs_projection': True, + } + + # Find the intresting sampleline and what image it is in + reference_index, interesting_sampline = interesting_func(nodes, lon, lat, sensor, size=interesting_func_kwargs['size'], **csm_kwargs) + log.info(f'Found an interesting feature in {nodes[reference_index]["image_path"]} at {interesting_sampline.x}, {interesting_sampline.y}.') + + # Get the updated X,Y,Z location of the point and reproject to get the updates lon, lat. + # The io.db.Point class handles all xyz to lat/lon and ographic/ocentric conversions in it's + # adjusted property setter. + reference_node = nodes[reference_index] + x,y,z = sensor.linesamp2xyz(reference_node, interesting_sampline.x, interesting_sampline.y, **csm_kwargs) + + # If the updated point is outside of the overlap, then revert back to the + # original point and hope the matcher can handle it when sub-pixel registering + updated_lon, updated_lat = xyz2oc(x, y, z, semi_major, semi_minor) + if not geom.contains(shapely.geometry.Point(updated_lon, updated_lat)): + x,y,z = oc2xyz(lon, lat, semi_major, semi_minor, height) + updated_lon, updated_lat = xyz2oc(x, y, z, semi_major, semi_minor) + + # Create the point object for insertion into the database + point_geom = shapely.geometry.Point(x, y, z) + + # Create the new point + point = Points.create_point_with_reference_measure(point_geom, + reference_node, + interesting_sampline, + choosername=identifier, + point_type=point_type) + log.debug(f'Created point: {point}.') + + # Remove the reference_indexed measure from the list of candidates. + # It has been added by the create_point_with_reference_measure function. + del nodes[reference_index] + + # Dont need projections + # TODO: Take this projection out of the CSM model and work it into the point + kwargs['needs_projection']=False + # Iterate through all other, non-reference images in the overlap and attempt to add a measure. + point.add_measures_to_point(nodes, sensor, choosername=identifier, **csm_kwargs) + + # Insert the point into the database asynchronously (via redis) or synchronously via the ncg if use_cache: - pipeline = ncg.redis_queue.pipeline() - msgs = [json.dumps(point.to_dict(_hide=[]), cls=JsonEncoder) for point in points] - pipeline.rpush(ncg.point_insert_queue, *msgs) - pipeline.execute() - # Push - log.info('Using the cache') - # ncg.redis_queue.rpush(ncg.point_insert_queue, *[json.dumps(point.to_dict(_hide=[]), cls=JsonEncoder) for point in points]) - ncg.redis_queue.incr(ncg.point_insert_counter, amount=len(points)) + msgs = json.dumps(point.to_dict(_hide=[]), cls=JsonEncoder) + ncg.push_insertion_message(ncg.point_insert_queue, + ncg.point_insert_counter, + msgs) else: with ncg.session_scope() as session: - for point in points: + if len(point.measures) >= 2: session.add(point) t2 = time.time() log.info(f'Total processing time was {t2-t1} seconds.') diff --git a/autocnet/spatial/sensor.py b/autocnet/spatial/sensor.py new file mode 100644 index 0000000000000000000000000000000000000000..a4596f687ea3463854b0c1dd73450cb3a833b6fd --- /dev/null +++ b/autocnet/spatial/sensor.py @@ -0,0 +1,195 @@ +from autocnet.transformation.spatial import reproject, og2oc, oc2xyz +from autocnet.spatial import isis +from autocnet.spatial import overlap +import csmapi +import logging + +# set up the logger file +log = logging.getLogger(__name__) + +class ISISSensor: + def __init__(self): + self.sensor_type = "ISISSensor" + + def calculate_sample_line(self, node, lon, lat, **kwargs): + """ + Calculate the sample and line for an isis camera sensor + + Parameters + ---------- + node: obj + autocnet object containing image information + + lon: int + longitude of point + + lat: int + latitude of point + + Returns + ------- + sample: int + sample of point + line: int + lint of point + """ + sample, line = isis.ground_to_image(node["image_path"], lon, lat) + return sample, line + + def linesamp2xyz(node, sample, line, **kwargs): + """ + Convert a line and sample into and x,y,z point for isis camera model + + Parameters + ---------- + node: obj + autocnet object containing image information + + sample: int + sample of point + + line: int + lint of point + + Returns + ------- + x,y,z : int(s) + x,y,z coordinates of the point + """ + cube_path = node['image_path'] + + try: + p = isis.point_info(cube_path, sample, line, point_type='image') + except: + log.debug(f"Image coordinates {sample}, {line} do not project fot image {cube_path}.") + + try: + x, y, z = p["BodyFixedCoordinate"].value + except: + x, y, z = p["BodyFixedCoordinate"] + + if getattr(p["BodyFixedCoordinate"], "units", "None").lower() == "km": + x = x * 1000 + y = y * 1000 + z = z * 1000 + + return x,y,z + +class CSMSensor: + def __init__(self): + self.sensor_type = "CSMSensor" + + def calculate_sample_line(self, node, lon, lat, **kwargs): + """ + Calculate the sample and line for an csm camera sensor + + Parameters + ---------- + node: obj + autocnet object containing image information + + lon: int + longitude of point + + lat: int + latitude of point + + kwargs: dict + Contain information to be used if the csm sensor model is passed + csm_kwargs = { + 'semi_major': int, + 'semi_minor': int, + 'height': int, + 'ncg': obj, + 'needs_projection': bool + } + Returns + ------- + sample: int + sample of point + line: int + lint of point + """ + semi_major = kwargs.get('semi_major', None) + semi_minor = kwargs.get('semi_minor', None) + needs_projection = kwargs.get('needs_projection', None) + + # TODO: Take this projection out of the CSM model and work it into the point + if needs_projection: + height = kwargs.get('height', None) + x,y,z = oc2xyz(lon, lat, semi_major, semi_minor, height) + # The CSM conversion makes the LLA/ECEF conversion explicit + gnd = csmapi.EcefCoord(x, y, z) + else: + ncg = kwargs.get('ncg', None) + height = ncg.dem.get_height(lat, lon) + # Get the BCEF coordinate from the lon, lat + x, y, z = reproject([lon, lat, height], + semi_major, semi_minor, 'latlon', 'geocent') + gnd = csmapi.EcefCoord(x, y, z) + + image_coord = node.camera.groundToImage(gnd) + sample, line = image_coord.samp, image_coord.line + return sample,line + + def linesamp2xyz(node, sample, line, **kwargs): + """ + Convert a line and sample into and x,y,z point for csm camera model + + Parameters + ---------- + node: obj + autocnet object containing image information + + sample: int + sample of point + + line: int + lint of point + + kwargs: dict + Contain information to be used if the csm sensor model is passed + csm_kwargs = { + 'semi_major': int, + 'semi_minor': int, + 'ncg': obj, + } + + Returns + ------- + x,y,z : int(s) + x,y,z coordinates of the point + """ + semi_major = kwargs.get('semi_major', None) + semi_minor = kwargs.get('semi_minor', None) + ncg = kwargs.get('ncg', None) + + image_coord = csmapi.ImageCoord(sample, line) + pcoord = node.camera.imageToGround(image_coord) + # Get the BCEF coordinate from the lon, lat + # TODO: Take this projection out of the CSM model and work it into the point + updated_lon_og, updated_lat_og, _ = reproject([pcoord.x, pcoord.y, pcoord.z], + semi_major, semi_minor, 'geocent', 'latlon') + updated_lon, updated_lat = og2oc(updated_lon_og, updated_lat_og, semi_major, semi_minor) + + updated_height = ncg.dem.get_height(updated_lat, updated_lon) + + + # Get the BCEF coordinate from the lon, lat + x, y, z = reproject([updated_lon_og, updated_lat_og, updated_height], + semi_major, semi_minor, 'latlon', 'geocent') + + return x,y,z + +def create_sensor(sensor_type): + sensor_type = sensor_type.lower() + sensor_classes = { + "isis": ISISSensor, + "csm": CSMSensor + } + + if sensor_type in sensor_classes: + return sensor_classes[sensor_type]() + else: + raise Exception(f"Unsupported sensor type: {sensor_type}, accept 'isis' or 'csm'") + \ No newline at end of file diff --git a/autocnet/transformation/spatial.py b/autocnet/transformation/spatial.py index a05df56c13f43a92b00f7571197240d07baca0dc..389d4a81359c8b0023b744c08acbb65781fd49a1 100644 --- a/autocnet/transformation/spatial.py +++ b/autocnet/transformation/spatial.py @@ -113,3 +113,68 @@ def reproject(record, semi_major, semi_minor, source_proj, dest_proj, **kwargs): y, x, z = pyproj.transform(source_pyproj, dest_pyproj, record[0], record[1], record[2], **kwargs) return y, x, z + +# TODO: Take this projection out of the CSM model and work it into the point +def oc2xyz(lon, lat, semi_major, semi_minor, height): + """ + Project from ocentric to graphic + + Parameters + ---------- + lon: int + longitude of point + + lat: int + latitude of point + + semi_minor: int + Get from the plantery body in use + + semi_major: int + Get from the plantary body in use + + Returns + ------- + x,y,z: int(s) + The x, y, z coordinated to the converted point + """ + lon_og, lat_og = oc2og(lon, lat, semi_major, semi_minor) + x, y, z = reproject([lon_og, lat_og, height], + semi_major, semi_minor, + 'latlon', 'geocent') + return x,y,z + +# TODO: Take this projection out of the CSM model and work it into the point +def xyz2oc(x, y, z, semi_major, semi_minor): + """ + Project from graphic to ocentric + + Parameters + ---------- + x: int + x coordinate of the point + + y: int + y coordinate of the point + + z: int + z coordinate (height) of the point + + semi_minor: int + Get from the plantery body in use + + semi_major: int + Get from the plantary body in use + + Returns + ------- + updated_lon: int + longitude of the point after conversion + + updated_lat: int + latitude of the point after conversion + """ + lon_og, lat_og, _ = reproject([x, y, z], semi_major, semi_minor, + 'geocent', 'latlon') + updated_lon, updated_lat = og2oc(lon_og, lat_og, semi_major, semi_minor) + return updated_lon, updated_lat \ No newline at end of file