Loading CHANGELOG.md +6 −0 Original line number Diff line number Diff line Loading @@ -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. Loading autocnet/graph/network.py +21 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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', Loading autocnet/io/db/model.py +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 Loading @@ -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 Loading @@ -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() Loading Loading @@ -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 Loading @@ -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. Loading @@ -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 Loading Loading @@ -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) Loading autocnet/io/db/tests/test_images.py 0 → 100644 +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 autocnet/io/db/tests/test_model.py +0 −79 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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}, Loading @@ -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 Loading Loading @@ -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 Loading
CHANGELOG.md +6 −0 Original line number Diff line number Diff line Loading @@ -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. Loading
autocnet/graph/network.py +21 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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', Loading
autocnet/io/db/model.py +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 Loading @@ -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 Loading @@ -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() Loading Loading @@ -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 Loading @@ -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. Loading @@ -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 Loading Loading @@ -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) Loading
autocnet/io/db/tests/test_images.py 0 → 100644 +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
autocnet/io/db/tests/test_model.py +0 −79 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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}, Loading @@ -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 Loading Loading @@ -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