Commit 76526f6f authored by Bauck, Kirsten (Contractor) Hailey's avatar Bauck, Kirsten (Contractor) Hailey
Browse files

Merge branch 'refactor_place_polar_points' into 'main'

Initial refactor of place_point_in_centroids

See merge request astrogeology/autocnet!685
parents 93dc929d d57464db
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -36,6 +36,12 @@ release.
### Added
- Ability to place points in centroids instead of overlaps.
- Ability to find points in centroids focused around a plantary body
- Functionality to insert message into database
- Reformated `test_model.py` and added more test for overlays and points
- Functionality to create a point with reference measure
- 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.

### Fixed
- string injection via format with sqlalchemy text() object.
+21 −1
Original line number Diff line number Diff line
@@ -1757,7 +1757,6 @@ class NetworkCandidateGraph(CandidateGraph):
        if self.async_watchers:
            self._setup_asynchronous_workers()


    def _execute_sql(self, sql):
        """
        Execute a raw SQL string in the database currently specified
@@ -1885,6 +1884,27 @@ class NetworkCandidateGraph(CandidateGraph):
        pipeline.execute()
        return job_counter + 1

    def push_insertion_message(self, queue, queue_counter, msgs):
        """
        Use the redis write queue to stage inserts into the database.

        Parameters
        ----------
        queue : str
                The name of the queue to insert messages into
        
        queue_counter : str
                        The name of queue that tracks the length of queue
        
        msgs : list
               of messages formated for whatever asynchronous func is watching the ingest queue
        """
        pipeline = self.redis_queue.pipeline()
        pipeline.rpush(queue, *msgs)
        pipeline.execute()
        self.redis_queue.incr(queue_counter, amount=len(msgs))
        log.info(f'Pushed {len(msgs)} messages to the {queue} queue for insertion')

    def apply(self,
            function,
            on='edge',
+108 −5
Original line number Diff line number Diff line
import enum
import json
import logging
import os

import sqlalchemy
from sqlalchemy.ext.declarative import declarative_base
@@ -13,6 +15,8 @@ from sqlalchemy_utils import database_exists, create_database
from sqlalchemy.types import TypeDecorator
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm.attributes import QueryableAttribute
from sqlalchemy import text
from sqlalchemy.sql import func

from geoalchemy2 import Geometry
from geoalchemy2.shape import from_shape, to_shape
@@ -20,8 +24,12 @@ from geoalchemy2.shape import from_shape, to_shape
import osgeo
import shapely
from shapely.geometry import Point

from autocnet.transformation.spatial import reproject, og2oc
from autocnet.utils.serializers import JsonEncoder
from autocnet.spatial import isis

log = logging.getLogger(__name__)

Base = declarative_base()

@@ -353,6 +361,13 @@ class Images(BaseMixin, Base):
        res = session.query(cls.geom.ST_Union()).one()[0]
        return to_shape(res)

    @classmethod
    def get_images_intersecting_point(cls, point, session):
        point = f"SRID={cls.latitudinal_srid};{point.wkt}"
        res = session.query(cls.id).filter(func.ST_Intersects(cls.geom, point)).all()
        image_ids = [i[0] for i in res]
        return image_ids

class Overlay(BaseMixin, Base):
    __tablename__ = 'overlay'
    latitudinal_srid = -1
@@ -379,7 +394,7 @@ class Overlay(BaseMixin, Base):
        self._geom = from_shape(geom, srid=self.latitudinal_srid)

    @classmethod
    def overlapping_larger_than(cls, size_threshold, Session):
    def overlapping_larger_than(cls, size_threshold, session):
        """
        Query the Overlay table for an iterable of responses where the objects
        in the iterable have an area greater than a given size.
@@ -389,14 +404,11 @@ class Overlay(BaseMixin, Base):
        size_threshold : Number
                        area >= this arg are returned
        """
        session = Session()
        res = session.query(cls).\
                filter(sqlalchemy.func.ST_Area(cls.geom) >= size_threshold).\
                filter(sqlalchemy.func.array_length(cls.intersections, 1) > 1)
        session.close()
                filter(sqlalchemy.func.array_length(cls.intersections, 1) > 1).all()
        return res


class PointType(enum.IntEnum):
    """
    Enum to enforce point type for ISIS control networks
@@ -521,6 +533,97 @@ class Points(Base, BaseMixin):
    def maxresidual(self, max_res):
        self._maxresidual = max_res

    @classmethod
    def create_point_with_reference_measure(cls, 
                                            point_geom, 
                                            reference_node, 
                                            sampline, 
                                            choosername='autocnet',
                                            point_type=2):
        """
        Create a new point object with a single measure, the reference measure.

        Parameters
        ----------
        point_geom : object
                     a shapely PointZ object with X,Y,Z coordinates in the latitudinal SRID (BCBF)
        
        reference_node : object
                         An autocnet Node object
        
        sampline : object
                   a shapely Point object with x,y coordinates as the sample, line in the reference image

        choosername : str
                      The string identifier or chooser name added to the point id.

        point_type : int
                     The ISIS control network format point type. 

        Returns
        -------
        point : object
                A new Point object with a single measure, set as the reference measure.
        """
        # Create the point
        point = cls(identifier=choosername,
                apriori=point_geom,
                adjusted=point_geom,
                pointtype=point_type, # Would be 3 or 4 for ground
                cam_type='isis',
                reference_index=0)
        
        # Create the measure for the reference image and add it to the point
        point.measures.append(Measures(sample=sampline.x,
                                        line=sampline.y,
                                        apriorisample=sampline.x,
                                        aprioriline=sampline.y,
                                        imageid=reference_node['node_id'],
                                        serial=reference_node.isis_serial,
                                        measuretype=3,
                                        choosername=choosername))
        return point 

    def add_measures_to_point(self, candidates, choosername='autocnet'):
        """
        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
        to reproject the point's lon/lat into the node's image space. If successful, 
        The measure is added to the point.

        Parameters
        ----------
        candidates : list
                     of autocnet Node objects

        choosername : the identidier or name of the algorithm that selected these measures.
        """
        for node in candidates:
            # Skip images that are not on disk.
            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)
            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:
                log.info(f'interesting point ({self.geom.x},{self.geom.y}) does not project to image {node["image_path"]}')
                continue
            
            self.measures.append(Measures(sample=sample,
                                        line=line,
                                        apriorisample=sample,
                                        aprioriline=line,
                                        imageid=node['node_id'],
                                        serial=node.isis_serial,
                                        measuretype=3,
                                        choosername=choosername))
        log.info(f'Added {len(self.measures)-1} / {len(candidates)} measures to the point.')

    #def subpixel_register(self, Session, pointid, **kwargs):
    #    subpixel.subpixel_register_point(args=(Session, pointid), **kwargs)

+54 −0
Original line number Diff line number Diff line
import pytest
from shapely import MultiPolygon, Polygon, Point

from autocnet.io.db.model import Images

def test_images_exists(tables):
    assert Images.__tablename__ in tables

@pytest.mark.parametrize('data', [
    {'id':1},
    {'name':'foo',
     'path':'/neither/here/nor/there'},
    ])
def test_create_images(session, data):
    i = Images.create(session, **data)
    resp = session.query(Images).filter(Images.id==i.id).first()
    assert i == resp

def test_null_footprint(session):
    i = Images.create(session, geom=None,
                                      serial = 'serial')
    assert i.geom is None

def test_broken_bad_geom(session):
    # An irreperablly damaged poly
    truthgeom = MultiPolygon([Polygon([(0,0), (1,1), (1,2), (1,1), (0,0)])])
    i = Images.create(session, geom=truthgeom,
                                      serial = 'serial')
    resp = session.query(Images).filter(Images.id==i.id).one()
    assert resp.ignore == True

def test_fix_bad_geom(session):
    truthgeom = MultiPolygon([Polygon([(0,0), (0,1), (1,1), (0,1), (1,1), (1,0), (0,0) ])])
    i = Images.create(session, geom=truthgeom,
                                     serial = 'serial')
    resp = session.query(Images).filter(Images.id==i.id).one()
    assert resp.ignore == False
    assert resp.geom.is_valid == True

def test_get_images_intersecting_point(session):

    # Create test objects and put them into database
    i1 = {'id':1, 
        'geom':MultiPolygon([Polygon([(0,0), (-1,0), (-1,-1), (0,-1), (0,0)])])}
    i2={'id':2,
        'geom':MultiPolygon([Polygon([(0,0), (2,0), (2,2), (0,2), (0,0)])])}
    a = Images.create(session, **i1)
    b = Images.create(session, **i2)
    session.commit()

    point = Point(1,0)
    overlap_ids = Images.get_images_intersecting_point(point, session)
    session.close()
    assert overlap_ids == [2]
 No newline at end of file
+0 −79
Original line number Diff line number Diff line
@@ -6,7 +6,6 @@ import numpy as np
import pandas as pd
import pytest
import sqlalchemy
from unittest.mock import MagicMock, patch

from autocnet.io.db import model
from autocnet.graph.network import NetworkCandidateGraph
@@ -61,18 +60,7 @@ def test_create_camera_unique_constraint(session):
    with pytest.raises(sqlalchemy.exc.IntegrityError):
        model.Cameras.create(session, **data)

def test_images_exists(tables):
    assert model.Images.__tablename__ in tables

@pytest.mark.parametrize('data', [
    {'id':1},
    {'name':'foo',
     'path':'/neither/here/nor/there'},
    ])
def test_create_images(session, data):
    i = model.Images.create(session, **data)
    resp = session.query(model.Images).filter(model.Images.id==i.id).first()
    assert i == resp

'''@pytest.mark.parametrize('data', [
    {'id':1},
@@ -86,57 +74,10 @@ def test_create_images_constrined(session, data):
    with pytest.raises(sqlalchemy.exc.IntegrityError):
        model.Images.create(session, **data)'''

def test_overlay_exists(tables):
    assert model.Overlay.__tablename__ in tables

@pytest.mark.parametrize('data', [
    {'id':1},
    {'id':1, 'intersections':[1,2,3]},
    {'id':1, 'intersections':[1,2,3],
     'geom':Polygon([(0,0), (1,0), (1,1), (0,1), (0,0)])}

])
def test_create_overlay(session, data):
    d = model.Overlay.create(session, **data)
    resp = session.query(model.Overlay).filter(model.Overlay.id == d.id).first()
    assert d == resp

def test_points_exists(tables):
    assert model.Points.__tablename__ in tables

@pytest.mark.parametrize("data", [
    {'id':1, 'pointtype':2},
    {'pointtype':2, 'identifier':'123abc'},
    {'pointtype':3, 'apriori':Point(0,0,0)},
    {'pointtype':3, 'adjusted':Point(0,0,0)},
    {'pointtype':2, 'adjusted':Point(1,1,1), 'ignore':False}
])
def test_create_point(session, data):
    p = model.Points.create(session, **data)
    resp = session.query(model.Points).filter(model.Points.id == p.id).first()
    assert p == resp

@pytest.mark.parametrize("data, expected", [
    ({'pointtype':3, 'adjusted':Point(0,-1000000,0)}, Point(270, 0)),
    ({'pointtype':3}, None)
])
def test_create_point_geom(session, data, expected):
    p = model.Points.create(session, **data)
    resp = session.query(model.Points).filter(model.Points.id == p.id).first()

    assert resp.geom == expected

@pytest.mark.parametrize("data, new_adjusted, expected", [
    ({'pointtype':3, 'adjusted':Point(0,-100000,0)}, None, None),
    ({'pointtype':3, 'adjusted':Point(0,-100000,0)}, Point(0,100000,0), Point(90, 0)),
    ({'pointtype':3}, Point(0,-100000,0), Point(270, 0))
])
def test_update_point_geom(session, data, new_adjusted, expected):
    p = model.Points.create(session, **data)
    p.adjusted = new_adjusted
    session.commit()
    resp = session.query(model.Points).filter(model.Points.id == p.id).first()
    assert resp.geom == expected

# def test_point_trigger(session):
#     original = 3
@@ -213,26 +154,6 @@ def test_measure_trigger(session):
    assert resp[2].before['sample'] == new_type
    assert resp[2].after == None

def test_null_footprint(session):
    i = model.Images.create(session, geom=None,
                                      serial = 'serial')
    assert i.geom is None

def test_broken_bad_geom(session):
    # An irreperablly damaged poly
    truthgeom = MultiPolygon([Polygon([(0,0), (1,1), (1,2), (1,1), (0,0)])])
    i = model.Images.create(session, geom=truthgeom,
                                      serial = 'serial')
    resp = session.query(model.Images).filter(model.Images.id==i.id).one()
    assert resp.ignore == True

def test_fix_bad_geom(session):
    truthgeom = MultiPolygon([Polygon([(0,0), (0,1), (1,1), (0,1), (1,1), (1,0), (0,0) ])])
    i = model.Images.create(session, geom=truthgeom,
                                     serial = 'serial')
    resp = session.query(model.Images).filter(model.Images.id==i.id).one()
    assert resp.ignore == False
    assert resp.geom == MultiPolygon([Polygon([(0,1), (1,1), (1,0), (0,0), (0,1)])])

@pytest.mark.parametrize("measure_data, point_data, image_data", [(
    [{'id': 1, 'pointid': 1, 'imageid': 1, 'serial': 'ISISSERIAL1', 'measuretype': 3, 'sample': 0, 'line': 0},
Loading