55import time
66from io import StringIO
77from pathlib import Path
8- from typing import List , Tuple
8+ from typing import List , Tuple , Union
99from uuid import uuid4
1010
1111from django .conf import settings
1212from osgeo import gdal , ogr , osr
1313
1414from ohmg .core .utils .srs import retrieve_srs_wkt
1515
16- from .geometry import angle_from_coords
16+ from .geometry import azimuth_from_coords
1717
1818logger = logging .getLogger (__name__ )
1919
@@ -244,73 +244,57 @@ def _load_gcps_from_geojson(self, geo_json):
244244 )
245245 self .gcps .append (gcp )
246246
247- def _geo_coords_from_gcp (self , gcp : gdal .GCP ) -> Tuple [float , float ]:
248- """Return the geographic x, y coords from the input GCP"""
249- return (gcp .GCPX , gcp .GCPY )
250-
251- def _pixel_coords_from_gcp (
252- self , gcp : gdal .GCP , cartesian_y : bool = False
253- ) -> Tuple [float , float ]:
254- """Return the image pixel coords from the input GCP.
255-
256- Y coordinate is measured down from the top of the image.
257-
258- If cartesian_y=True, then invert the Y coordinate against the height
259- of the dataset, to match a cartesian plane with 0,0 at the bottom left of the
260- image."""
261- x , y = gcp .GCPPixel , gcp .GCPLine
262- if cartesian_y :
263- ds = gdal .Open (self .gcps_vrt .get_vsi_url ())
264- y = ds .RasterYSize - y
265- return (x , y )
266-
267247 def _calculate_scale (self ) -> float :
268248 """Compares two GCPs and returns a scale factor."""
269249
270- if len (self .gcps ) != 2 :
271- raise Exception ("Two GCPs are needed to calculate a scale factor" )
250+ if len (self .gcps ) < 2 :
251+ raise Exception ("At least GCPs are needed to calculate a scale factor" )
272252
273- gcp1 , gcp2 = self .gcps
253+ gcp1 , gcp2 = self .gcps [ 0 ], self . gcps [ 1 ]
274254
275255 # distance between the geographic coords in each GCP
276256 # this distance is absolute so the order of the coords doesn't matter
277- pt_dist = math .dist (
278- self . _geo_coords_from_gcp (gcp1 ),
279- self . _geo_coords_from_gcp (gcp2 ),
257+ geo_dist = math .dist (
258+ (gcp1 . GCPX , gcp1 . GCPY ),
259+ (gcp2 . GCPX , gcp2 . GCPY ),
280260 )
281261
282262 # distance between the pixel coords in each GCP
283263 # this distance is absolute so the order of the coords doesn't matter
284- px_dist = math .dist (
285- self . _pixel_coords_from_gcp (gcp1 ),
286- self . _pixel_coords_from_gcp (gcp2 ),
264+ img_dist = math .dist (
265+ (gcp1 . GCPPixel , gcp1 . GCPLine ),
266+ (gcp2 . GCPPixel , gcp2 . GCPLine ),
287267 )
288268
289- return pt_dist / px_dist
269+ return geo_dist / img_dist
290270
291- def _calculate_rotation_from_north (self ) -> float :
271+ def _calculate_rotation (self , img_height : Union [ float , None ] = None ) -> float :
292272 """Compares two GCPs and calculates the difference in the angles
293273 between the geometric points and the pixel points.
294274
295275 Returns the angle in degrees from 'north', i.e. the positive Y axis"""
296- if len (self .gcps ) != 2 :
297- raise Exception ("Two GCPs are needed to calculate a scale factor" )
298276
299- ## the GCPLine value is the number of pixels DOWN from the top of the
300- ## image, so we'll call it "upper" because it appears the higher of the
301- ## two on the page
302- upper_gcp = min (self .gcps , key = lambda x : x .GCPLine )
303- lower_gcp = max (self .gcps , key = lambda x : x .GCPLine )
277+ if len (self .gcps ) < 2 :
278+ raise Exception ("At least two GCPs are needed to calculate a scale factor" )
304279
305- pt_degrees = angle_from_coords (
306- self ._geo_coords_from_gcp (upper_gcp ), self ._geo_coords_from_gcp (lower_gcp )
307- )
308- px_degrees = angle_from_coords (
309- self ._pixel_coords_from_gcp (upper_gcp , cartesian_y = True ),
310- self ._pixel_coords_from_gcp (lower_gcp , cartesian_y = True ),
311- )
280+ # sort the GCPs so they are ordered from lowest to highest,
281+ # then right to left, as they appear on the source image.
282+ # this allows us to figure out the orientation
283+ self .gcps .sort (key = lambda x : x .GCPPixel )
284+ self .gcps .sort (key = lambda x : x .GCPLine , reverse = True )
285+
286+ # make sure img height is set because it is needed to properly
287+ # handle the inverted Y coords.
288+ if not img_height :
289+ ds = gdal .Open (self .gcps_vrt .get_vsi_url ())
290+ img_height = ds .RasterYSize
291+ img_coords = [(i .GCPPixel , img_height - i .GCPLine ) for i in self .gcps ]
292+ img_azimuth = azimuth_from_coords (img_coords )
293+
294+ geo_coords = [(i .GCPX , i .GCPY ) for i in self .gcps ]
295+ geo_azimuth = azimuth_from_coords (geo_coords )
312296
313- difference = pt_degrees - px_degrees
297+ difference = geo_azimuth - img_azimuth
314298 return difference
315299
316300 def _calculate_helmert_offsets (self , scale : float , rotation : float ) -> Tuple [float , float ]:
@@ -325,8 +309,8 @@ def _calculate_helmert_offsets(self, scale: float, rotation: float) -> Tuple[flo
325309 dist_to_page_edge = use_gcp .GCPLine * scale
326310 dist_to_page_top = use_gcp .GCPPixel * scale
327311
328- ## rotation is degrees from positive y-axis, adjust to be degrees from
329- ## positive x-axis
312+ ## rotation is azimuth, i.e. degrees from positive y-axis,
313+ ## must adjust to be degrees from positive x-axis
330314 theta1 = 90 - rotation
331315 ## further adjustments to normalize
332316 if theta1 < 0 :
@@ -421,7 +405,7 @@ def make_warped_vrt(
421405 scale = self ._calculate_scale ()
422406
423407 ## get rotation
424- rotation = self ._calculate_rotation_from_north ()
408+ rotation = self ._calculate_rotation ()
425409 ## adjust and convert to arcseconds
426410 arcseconds = (rotation + 90 ) * 3600
427411
0 commit comments