Coverage for py_tools_ds/geo/coord_trafo.py: 78%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
3# py_tools_ds - A collection of geospatial data analysis tools that simplify standard
4# operations when handling geospatial raster and vector data as well as projections.
5#
6# Copyright (C) 2016-2021
7# - Daniel Scheffler (GFZ Potsdam, daniel.scheffler@gfz-potsdam.de)
8# - Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences Potsdam,
9# Germany (https://www.gfz-potsdam.de/)
10#
11# This software was developed within the context of the GeoMultiSens project funded
12# by the German Federal Ministry of Education and Research
13# (project grant code: 01 IS 14 010 A-C).
14#
15# Licensed under the Apache License, Version 2.0 (the "License");
16# you may not use this file except in compliance with the License.
17# You may obtain a copy of the License at
18#
19# http://www.apache.org/licenses/LICENSE-2.0
20#
21# Unless required by applicable law or agreed to in writing, software
22# distributed under the License is distributed on an "AS IS" BASIS,
23# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24# See the License for the specific language governing permissions and
25# limitations under the License.
27import pyproj
28import numpy as np
29from shapely.ops import transform
30from typing import Union, Sequence, List # noqa F401 # flake8 issue
31from osgeo import gdal, osr
33__author__ = "Daniel Scheffler"
36def transform_utm_to_wgs84(easting, northing, zone, south=False):
37 # type: (float, float, int, bool) -> (float, float)
38 """Transform an UTM coordinate to lon, lat, altitude."""
39 UTM = pyproj.Proj(proj='utm',
40 zone=abs(zone),
41 ellps='WGS84',
42 south=(zone < 0 or south))
43 return UTM(easting, northing, inverse=True)
46def get_utm_zone(longitude):
47 # type: (float) -> int
48 """Get the UTM zone corresponding to the given longitude."""
49 return int(1 + (longitude + 180.0) / 6.0)
52def transform_wgs84_to_utm(lon, lat, zone=None):
53 # type: (float, float, int) -> (float, float)
54 """Return easting, northing, altitude for the given Lon/Lat coordinate.
56 :param lon: longitude
57 :param lat: latitude
58 :param zone: UTM zone (if not set it is derived automatically)
59 """
60 if not (-180. <= lon <= 180. and -90. <= lat <= 90.):
61 raise ValueError((lon, lat), 'Input coordinates for transform_wgs84_to_utm() out of range.')
63 UTM = pyproj.Proj(proj='utm',
64 zone=get_utm_zone(lon) if zone is None else zone,
65 ellps='WGS84')
66 return UTM(lon, lat)
69def transform_any_prj(prj_src, prj_tgt, x, y):
70 # type: (Union[str, int], Union[str, int], Union[float, np.ndarray], Union[float, np.ndarray]) -> (Union[float, np.ndarray], Union[float, np.ndarray]) # noqa
71 """Transform X/Y data from any source projection to any target projection.
73 :param prj_src: GDAL projection as WKT string or EPSG code ('epsg:1234' or <EPSG_int>)
74 :param prj_tgt: GDAL projection as WKT string or EPSG code ('epsg:1234' or <EPSG_int>)
75 :param x: X-coordinate(s) to be transformed (either <float> or 1D-<np.ndarray>)
76 :param y: X-coordinate(s) to be transformed (either <float> or 1D-<np.ndarray>)
77 :return:
78 """
79 transformer = pyproj.Transformer.from_crs(pyproj.CRS.from_user_input(prj_src),
80 pyproj.CRS.from_user_input(prj_tgt),
81 always_xy=True)
82 return transformer.transform(x, y)
85def transform_coordArray(prj_src, prj_tgt, Xarr, Yarr, Zarr=None):
86 # type: (str, str, np.ndarray, np.ndarray, np.ndarray) -> Sequence[np.ndarray]
87 """Transform a geolocation array from one projection into another.
89 HINT: This function is faster than transform_any_prj but works only for geolocation arrays.
91 :param prj_src: WKT string
92 :param prj_tgt: WKT string
93 :param Xarr: np.ndarray of shape (rows,cols)
94 :param Yarr: np.ndarray of shape (rows,cols)
95 :param Zarr: np.ndarray of shape (rows,cols)
96 :return:
97 """
98 drv = gdal.GetDriverByName('MEM')
99 geoloc_ds = drv.Create('geoloc', Xarr.shape[1], Xarr.shape[0], 3, gdal.GDT_Float64)
100 geoloc_ds.GetRasterBand(1).WriteArray(Xarr)
101 geoloc_ds.GetRasterBand(2).WriteArray(Yarr)
102 if Zarr is not None:
103 geoloc_ds.GetRasterBand(3).WriteArray(Zarr)
105 def strip_wkt(wkt):
106 return wkt\
107 .replace('\n', '')\
108 .replace('\r', '')\
109 .replace(' ', '')
111 transformer = gdal.Transformer(None, None, ['SRC_SRS=' + strip_wkt(prj_src),
112 'DST_SRS=' + strip_wkt(prj_tgt)])
113 status = transformer.TransformGeolocations(geoloc_ds.GetRasterBand(1),
114 geoloc_ds.GetRasterBand(2),
115 geoloc_ds.GetRasterBand(3))
117 if status:
118 raise Exception('Error transforming coordinate array: ' + gdal.GetLastErrorMsg())
120 Xarr = geoloc_ds.GetRasterBand(1).ReadAsArray()
121 Yarr = geoloc_ds.GetRasterBand(2).ReadAsArray()
123 if Zarr is not None:
124 Zarr = geoloc_ds.GetRasterBand(3).ReadAsArray()
125 return Xarr, Yarr, Zarr
126 else:
127 return Xarr, Yarr
130def mapXY2imXY(mapXY, gt):
131 # type: (Union[tuple, np.ndarray], Sequence) -> Union[tuple, np.ndarray]
132 """Translate the given geo coordinates into pixel locations according to the given image geotransform.
134 :param mapXY: <tuple, np.ndarray> The geo coordinates to be translated in the form (x,y) or as np.ndarray [Nx1].
135 :param gt: <list> GDAL geotransform
136 :returns: <tuple, np.ndarray> image coordinate tuple X/Y (column, row) or np.ndarray [Nx2]
137 """
138 if isinstance(mapXY, np.ndarray):
139 ndim = mapXY.ndim
140 if ndim == 1:
141 mapXY = mapXY.reshape(1, 2)
142 assert mapXY.shape[1] == 2, 'An array in shape [Nx2] is needed. Got shape %s.' % mapXY.shape
143 imXY = np.empty_like(mapXY, dtype=float)
144 imXY[:, 0] = (mapXY[:, 0] - gt[0]) / gt[1]
145 imXY[:, 1] = (mapXY[:, 1] - gt[3]) / gt[5]
146 return imXY if ndim > 1 else imXY.flatten()
147 else:
148 return (mapXY[0] - gt[0]) / gt[1],\
149 (mapXY[1] - gt[3]) / gt[5]
152def imXY2mapXY(imXY, gt):
153 # type: (Union[tuple, np.ndarray], Sequence) -> Union[tuple, np.ndarray]
154 """Translate the given pixel X/Y locations into geo coordinates according to the given image geotransform.
156 :param imXY: <tuple, np.ndarray> The image coordinates to be translated in the form (x,y) or as np.ndarray [Nx1].
157 :param gt: <list> GDAL geotransform
158 :returns: <tuple, np.ndarray> geo coordinate tuple X/Y (mapX, mapY) or np.ndarray [Nx2]
159 """
160 if isinstance(imXY, np.ndarray):
161 ndim = imXY.ndim
162 if imXY.ndim == 1:
163 imXY = imXY.reshape(1, 2)
164 assert imXY.shape[1] == 2, 'An array in shape [Nx2] is needed. Got shape %s.' % imXY.shape
165 mapXY = np.empty_like(imXY, dtype=float)
166 mapXY[:, 0] = gt[0] + imXY[:, 0] * abs(gt[1])
167 mapXY[:, 1] = gt[3] - imXY[:, 1] * abs(gt[5])
168 return mapXY if ndim > 1 else mapXY.flatten()
169 else:
170 return (gt[0] + imXY[0] * abs(gt[1])),\
171 (gt[3] - imXY[1] * abs(gt[5]))
174def mapYX2imYX(mapYX, gt):
175 return (mapYX[0] - gt[3]) / gt[5],\
176 (mapYX[1] - gt[0]) / gt[1]
179def imYX2mapYX(imYX, gt):
180 return gt[3] - (imYX[0] * abs(gt[5])),\
181 gt[0] + (imYX[1] * gt[1])
184def pixelToMapYX(pixelCoords, geotransform, projection):
185 # MapXYPairs = imXY2mapXY(np.array(pixelPairs), geotransform)
187 # Create a spatial reference object
188 srs = osr.SpatialReference()
189 srs.ImportFromWkt(projection)
191 # Set up the coordinate transformation object
192 ct = osr.CoordinateTransformation(srs, srs)
194 pixelCoords = [pixelCoords] if not type(pixelCoords[0]) in [list, tuple] else pixelCoords
196 mapXYPairs = [ct.TransformPoint(pixX * geotransform[1] + geotransform[0],
197 pixY * geotransform[5] + geotransform[3])[:2]
198 for pixX, pixY in pixelCoords]
200 return [[mapY, mapX] for mapX, mapY in mapXYPairs]
203def pixelToLatLon(pixelPairs, geotransform, projection):
204 # type: (List[Sequence[float, float]], Sequence, str) -> List[Sequence[float, float]]
205 """Translate the given pixel X/Y locations into latitude/longitude locations.
207 :param pixelPairs: Image coordinate pairs to be translated in the form [[x1,y1],[x2,y2]]
208 :param geotransform: GDAL GeoTransform
209 :param projection: WKT string
210 :returns: The lat/lon translation of the pixel pairings in the form [[lat1,lon1],[lat2,lon2]]
211 """
212 MapXYPairs = imXY2mapXY(np.array(pixelPairs), geotransform)
213 lons, lats = transform_any_prj(projection, 4326, MapXYPairs[:, 0], MapXYPairs[:, 1])
215 # validate output lat/lon values
216 if not (-180 <= np.min(lons) <= 180) or not (-180 <= np.max(lons) <= 180):
217 raise RuntimeError('Output longitude values out of bounds.')
218 if not (-90 <= np.min(lats) <= 90) or not (-90 <= np.max(lats) <= 90):
219 raise RuntimeError('Output latitudes values out of bounds.')
221 LatLonPairs = np.vstack([lats, lons]).T.tolist()
223 return LatLonPairs
226def latLonToPixel(latLonPairs, geotransform, projection):
227 # type: (List[Sequence[float, float]], Sequence, str) -> List[Sequence[float, float]]
228 """Translate the given latitude/longitude pairs into pixel X/Y locations.
230 :param latLonPairs: The decimal lat/lon pairings to be translated in the form [[lat1,lon1],[lat2,lon2]]
231 :param geotransform: GDAL GeoTransform
232 :param projection: WKT string
233 :return: The pixel translation of the lat/lon pairings in the form [[x1,y1],[x2,y2]]
234 """
235 latLonArr = np.array(latLonPairs)
237 # validate input lat/lon values
238 if not (-180 <= np.min(latLonArr[:, 1]) <= 180) or not (-180 <= np.max(latLonArr[:, 1]) <= 180):
239 raise ValueError('Longitude value out of bounds.')
240 if not (-90 <= np.min(latLonArr[:, 0]) <= 90) or not (-90 <= np.max(latLonArr[:, 0]) <= 90):
241 raise ValueError('Latitude value out of bounds.')
243 mapXs, mapYs = transform_any_prj(4326, projection, latLonArr[:, 1], latLonArr[:, 0])
244 imXYarr = mapXY2imXY(np.vstack([mapXs, mapYs]).T, geotransform)
246 pixelPairs = imXYarr.tolist()
248 return pixelPairs
251def lonlat_to_pixel(lon, lat, inverse_geo_transform):
252 """Translate the given lon, lat to the grid pixel coordinates in data array (zero start)."""
253 # transform to utm
254 utm_x, utm_y = transform_wgs84_to_utm(lon, lat)
256 # apply inverse geo transform
257 pixel_x, pixel_y = gdal.ApplyGeoTransform(inverse_geo_transform, utm_x, utm_y)
258 pixel_x = int(pixel_x) - 1 # adjust to 0 start for array
259 pixel_y = int(pixel_y) - 1 # adjust to 0 start for array
261 return pixel_x, abs(pixel_y) # y pixel is likly a negative value given geo_transform
264def transform_GCPlist(gcpList, prj_src, prj_tgt):
265 """
267 :param gcpList: <list> list of ground control points in the output coordinate system
268 to be used for warping, e.g. [gdal.GCP(mapX,mapY,mapZ,column,row),...]
269 :param prj_src: WKT string
270 :param prj_tgt: WKT string
271 :return:
272 """
273 return [gdal.GCP(*(list(transform_any_prj(prj_src, prj_tgt, GCP.GCPX, GCP.GCPY)) +
274 [GCP.GCPZ, GCP.GCPPixel, GCP.GCPLine])) # Python 2.7 compatible syntax
275 for GCP in gcpList]
278def reproject_shapelyGeometry(shapelyGeometry, prj_src, prj_tgt):
279 """Reproject any shapely geometry from one projection to another.
281 :param shapelyGeometry: any shapely geometry instance
282 :param prj_src: GDAL projection as WKT string or EPSG code ('epsg:1234' or <EPSG_int>)
283 :param prj_tgt: GDAL projection as WKT string or EPSG code ('epsg:1234' or <EPSG_int>)
284 """
285 project = pyproj.Transformer.from_proj(
286 pyproj.CRS.from_user_input(prj_src),
287 pyproj.CRS.from_user_input(prj_tgt),
288 always_xy=True)
290 return transform(project.transform, shapelyGeometry) # apply projection