Commit a84b11d8 authored by Laura, Jason R's avatar Laura, Jason R
Browse files

Merge branch 'place_polar_points' into 'main'

Functions to place polar points

See merge request astrogeology/autocnet!683
parents 5370ca10 565b6e09
Loading
Loading
Loading
Loading
+3 −0
Changes for CHANGELOG.md: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -33,6 +33,9 @@ heading to indicate that only the bug fixes and security fixes are in the bug fi
release.
-->
## [Unreleased]
### Added
- Ability to place points in centroids instead of overlaps.
- Ability to find points in centroids focused around a plantary body

## [1.0.2]
### Fixed
+374 −0
Changes for autocnet/spatial/centroids.py: 374 added lines, 0 removed lines.
Original line number Diff line number Diff line
import numpy as np
import shapely
import math
import csmapi
import os
import logging
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.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.transformation.spatial import reproject, og2oc, oc2og
import time

# Set up logging file
log = logging.getLogger(__name__)

def get_number_of_points(target_distance_km, latitude, radius):
    """
    Calculate the number of points needed to maintain a certain target distance (in km)
    around a specific latitude for a target planetary body. This assume that the body
    is spherical and not ellipsoidal.

    Parameters
    ___________
    target_distance_km : int
        The target distance to maintin between points

    latitude : int
        the latitude where points are needed
    
    radius : int
        radius of the planetary body
    
    Returns
    _______
    num_points : int
        Number of points needed to maintin target distance
    """

    # Convert to radians
    target_distance_rad = target_distance_km / radius

    # Calculate longitudal distance between two points
    longitudinal_distance = 2 * math.asin(math.sqrt(math.sin(target_distance_rad/2)**2 / (math.cos(math.radians(latitude))**2)))

    # Calculate the circumference of the planetary body at the given latitude
    body_circumference = 2 * math.pi * radius * math.cos(math.radians(latitude))

    # Calculate points needed
    num_points = int(body_circumference / longitudinal_distance) + 1

    return num_points

def winnow_points(polygon, list_of_points):
    """
    This removes any points in a list that arent within a given polygon

    Parameters
    ___________

    polygon : shapely.geom
        A shapely geometry object
    
    list_of_points : list
        list of point coordinates in the form
        [(x1,y1), (x2,y2), ..., (xn, yn)]
    
    Returns
    _______
    points : list
        list of point coordinates in the form
        [(x1,y1), (x2,y2), ..., (xn, yn)]
    """
    return [(point[0], point[1]) for point in list_of_points if polygon.contains(shapely.geometry.Point(point[0], point[1]))]

def find_points_in_centroids(radius, 
                             target_distance_in_km,
                             polygon=None,
                             min_lat=None,
                             max_lat=None,
                             longitude_min=-180,
                             longitude_max=180):
    """
    This finds points within a specified geometry.
    It finds those points be placing points in a centroid pattern
    around latitude lines (default longitude: -180, 180) where the center
    is the center of the min_lat/max_lat/min_lon/max_lon

    Parameters
    ___________
    radius : int
        radius of the planetary body
    
    target_distance_in_km : int
        The target distance to maintain between points

    polygon : shapely.geom
        A shapely geometry object
    
    min_lat : int
        The minimum latitude of the wanted area
        If polygon is passed, not necessary
    
    min_long : int
        The minimum longitude of the wanted area
        If polygon is passed, not necessary
    
    longitude_min : int
        The minimum longitde (ex. -180 or 0)

    longitude_max : int
        The maximum longitde (ex. 180 or 360)
    
    Returns
    _______
    points : list
        list of point coordinates in the form
        [(x1,y1), (x2,y2), ..., (xn, yn)]
    """
    # Set up processes
    if polygon:
        _, min_lat, _, max_lat = polygon.bounds
    latitude_intervals = np.arange(max_lat, min_lat, -0.005)
    points=[]

    # Itterate through each latitude, decrimenting at -0.005
    # TODO: decide if this decrement should be a user input
    for lat in latitude_intervals:
        # Calculate the number of points needed to space points along a latitude line every __km
        num_points = get_number_of_points(target_distance_in_km, lat, radius)
        p1 = (longitude_min, lat)
        p2 = (longitude_max, lat)
        # Create a list of points for latitude lines
        line_points = [(point[0], point[1]) for point in create_points_along_line(p1, p2, num_points)]
        # If a polygon was given limit points so that they are only within the polygon
        if polygon:
            line_points = winnow_points(polygon, line_points)
        if len(line_points)!=0:
            points.extend(line_points)
    return points

def place_points_in_centroids(valid,
                            identifier="autocnet",
                            cam_type="csm", 
                            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.

    Parameters
    ----------
    valid : list
        list of point coordinates in the form
        [(x1,y1), (x2,y2), ..., (xn, yn)]

    identifier: str
        The tag used to distinguish points laid down by this function.

    cam_type : str
        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.

    point_type: int
        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

    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()
    points = []
    
    semi_major = ncg.config['spatial']['semimajor_rad']
    semi_minor = ncg.config['spatial']['semiminor_rad']

    v=shapely.geometry.Point(valid[0], valid[1])
    # Get all the images that the point intersects
    sql_query=text(f"SELECT id FROM images WHERE ST_Intersects('SRID=30100;{v}', geom)")
    with ncg.session_scope() as session:
        res = session.execute(sql_query).all()
    overlap_ids = [id[0] for id in res]

    # Make the nodes
    nodes = []
    with ncg.session_scope() as session:
        for id in overlap_ids:
            res = session.query(Images).filter(Images.id == id).one()
            nn = NetworkNode(node_id=id, image_path=res.path)
            nn.parent = ncg
            nodes.append(nn)

    lon = v.x
    lat = v.y
    log.info(f'Point: {lon, lat}')

    # 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.info(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.info(f'Failed to find interesting features in image.')
                continue
        except:
            log.info(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.info('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.debug(node["image_path"])
                log.info(f'interesting point ({newsample}, {newline}) does not project back to ground')
        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)


        # 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')


    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)

    # Make the point
    point_geom = shapely.geometry.Point(x, y, z)
    log.debug(f'Creating point with reference_index: {reference_index}')
    point = Points(identifier=identifier,
                apriori=point_geom,
                adjusted=point_geom,
                pointtype=point_type, # Would be 3 or 4 for ground
                cam_type=cam_type,
                reference_index=reference_index)

    # Compute ground point to back project into measurtes
    gnd = csmapi.EcefCoord(x, y, z)

    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.info(f'Unable to find input image {node["image_path"]}')
                continue
            try:
                sample, line = isis.ground_to_image(node["image_path"], updated_lon, updated_lat)
            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.")
            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.info(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
        if node.isis_serial:
            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_centroid'))
        else:
            log.info(f"{node['node_id']} serial number is NULL")
    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.')

    if not points: return

    # Insert the points 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.incr(ncg.point_insert_counter, amount=len(points))
    else:
        with ncg.session_scope() as session:
            for point in points:
                session.add(point)
    t2 = time.time()
    log.info(f'Total processing time was {t2-t1} seconds.')

    return
 No newline at end of file