summaryrefslogtreecommitdiff
path: root/grid_tools/vertoffset_grid_to_gtiff.py
diff options
context:
space:
mode:
Diffstat (limited to 'grid_tools/vertoffset_grid_to_gtiff.py')
-rwxr-xr-xgrid_tools/vertoffset_grid_to_gtiff.py389
1 files changed, 389 insertions, 0 deletions
diff --git a/grid_tools/vertoffset_grid_to_gtiff.py b/grid_tools/vertoffset_grid_to_gtiff.py
new file mode 100755
index 0000000..db06746
--- /dev/null
+++ b/grid_tools/vertoffset_grid_to_gtiff.py
@@ -0,0 +1,389 @@
+#!/usr/bin/env python
+###############################################################################
+# $Id$
+#
+# Project: PROJ
+# Purpose: Convert a GTX or similar file into an optimized GTiff
+# Author: Even Rouault <even.rouault at spatialys.com>
+#
+###############################################################################
+# Copyright (c) 2019, Even Rouault <even.rouault at spatialys.com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+###############################################################################
+
+# python vertoffset_grid_to_gtiff.py $HOME/proj/proj-datumgrid/egm96_15.gtx egm96_15.tif --type GEOGRAPHIC_TO_VERTICAL --source-crs EPSG:4326 --target-crs EPSG:5773 --copyright "Public Domain. Derived from work at http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm96/egm96.html"
+# python vertoffset_grid_to_gtiff.py $HOME/proj/proj-datumgrid/world/egm08_25.gtx egm08_25.tif --type GEOGRAPHIC_TO_VERTICAL --source-crs EPSG:4326 --target-crs EPSG:3855 --copyright "Public Domain. Derived from work at http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm2008/egm08_wgs84.html"
+# python vertoffset_grid_to_gtiff.py $HOME/proj/proj-datumgrid/north-america/vertcone.gtx vertcone.tif --type VERTICAL_TO_VERTICAL --source-crs EPSG:7968 --target-crs EPSG:5703 --interpolation-crs EPSG:4267 --copyright "Public Domain. Derived from work at https://www.ngs.noaa.gov/PC_PROD/VERTCON/"
+
+from osgeo import gdal
+from osgeo import osr
+from cloud_optimize_gtiff import generate_optimized_file
+import argparse
+import datetime
+import os
+import struct
+
+
+def get_args():
+ parser = argparse.ArgumentParser(
+ description='Convert a vertical offset grid into PROJ GeoTIFF.')
+ parser.add_argument('source',
+ help='Source file')
+ parser.add_argument('dest',
+ help='Destination GeoTIFF file')
+
+ parser.add_argument('--type', dest='type', required=True,
+ choices=['GEOGRAPHIC_TO_VERTICAL',
+ 'VERTICAL_TO_VERTICAL'],
+ help='Type of grid')
+
+ parser.add_argument('--source-crs', dest='source_crs', required=True,
+ help='Source CRS as EPSG:XXXX or WKT')
+
+ parser.add_argument('--interpolation-crs', dest='interpolation_crs',
+ help='Interpolation CRS as EPSG:XXXX or WKT. Required for type=VERTICAL_TO_VERTICAL')
+
+ parser.add_argument('--target-crs', dest='target_crs', required=True,
+ help='Target CRS as EPSG:XXXX or WKT')
+
+ parser.add_argument('--copyright', dest='copyright', required=True,
+ help='Copyright info')
+
+ parser.add_argument('--description', dest='description',
+ help='Description')
+
+ parser.add_argument('--encoding',
+ dest='encoding',
+ choices=['float32', 'uint16', 'int32-scale-1-1000'],
+ default='float32',
+ help='Binary encoding. int32-scale-1-1000 is for for Canadian .byn')
+
+ parser.add_argument('--ignore-nodata', dest='ignore_nodata',
+ action='store_true',
+ help='Whether to ignore nodata')
+
+ parser.add_argument('--datetime', dest='datetime',
+ help='Value for TIFF DateTime tag as YYYY:MM:DD HH:MM:SS, or "NONE" to not write it. If not specified, current date time is used')
+
+ parser.add_argument('--area-of-use', dest='area_of_use',
+ help='Area of use')
+
+ return parser.parse_args()
+
+
+def create_unoptimized_file(sourcefilename, tmpfilename, args):
+ src_ds = gdal.Open(sourcefilename)
+ subdatsets = src_ds.GetSubDatasets()
+ if not subdatsets:
+ subdatsets = [(sourcefilename, None)]
+
+ src_basename = os.path.basename(args.source)
+ nbands = 1
+
+ compact_md = True if len(subdatsets) > 50 else False
+
+ is_vertcon = src_basename in (
+ 'vertcone.gtx', 'vertconw.gtx', 'vertconc.gtx')
+
+ for idx_ifd, subds in enumerate(subdatsets):
+ src_ds = gdal.Open(subds[0])
+
+ if args.encoding == 'int32-scale-1-1000':
+ target_dt = gdal.GDT_Int32
+ elif args.encoding == 'uint16':
+ target_dt = gdal.GDT_UInt16
+ else:
+ target_dt = gdal.GDT_Float32
+ tmp_ds = gdal.GetDriverByName('GTiff').Create('/vsimem/tmp',
+ src_ds.RasterXSize,
+ src_ds.RasterYSize,
+ nbands,
+ target_dt)
+ interpolation_crs = osr.SpatialReference()
+ if args.type == 'GEOGRAPHIC_TO_VERTICAL':
+ interpolation_crs.SetFromUserInput(args.source_crs)
+ else:
+ interpolation_crs.SetFromUserInput(args.interpolation_crs)
+ assert interpolation_crs.IsGeographic()
+ tmp_ds.SetSpatialRef(interpolation_crs)
+ tmp_ds.SetGeoTransform(src_ds.GetGeoTransform())
+ tmp_ds.SetMetadataItem('AREA_OR_POINT', 'Point')
+
+ if args.type == 'GEOGRAPHIC_TO_VERTICAL':
+ tmp_ds.SetMetadataItem(
+ 'TYPE', 'VERTICAL_OFFSET_GEOGRAPHIC_TO_VERTICAL')
+ else:
+ tmp_ds.SetMetadataItem(
+ 'TYPE', 'VERTICAL_OFFSET_VERTICAL_TO_VERTICAL')
+
+ src_crs = osr.SpatialReference()
+ src_crs.SetFromUserInput(args.source_crs)
+ if args.type == 'VERTICAL_TO_VERTICAL':
+ assert src_crs.IsVertical()
+ src_auth_name = src_crs.GetAuthorityName(None)
+ src_auth_code = src_crs.GetAuthorityCode(None)
+ if idx_ifd == 0 or not compact_md:
+ if src_auth_name == 'EPSG' and src_auth_code:
+ tmp_ds.SetMetadataItem(
+ 'source_crs_epsg_code', src_auth_code)
+ else:
+ tmp_ds.SetMetadataItem(
+ 'source_crs_wkt', src_crs.ExportToWkt(['FORMAT=WKT2_2018']))
+
+ dst_crs = osr.SpatialReference()
+ dst_crs.SetFromUserInput(args.target_crs)
+ assert dst_crs.IsVertical()
+ dst_auth_name = dst_crs.GetAuthorityName(None)
+ dst_auth_code = dst_crs.GetAuthorityCode(None)
+ if idx_ifd == 0 or not compact_md:
+ if dst_auth_name == 'EPSG' and dst_auth_code:
+ tmp_ds.SetMetadataItem('target_crs_epsg_code', dst_auth_code)
+ else:
+ tmp_ds.SetMetadataItem(
+ 'target_crs_wkt', dst_crs.ExportToWkt(['FORMAT=WKT2_2018']))
+
+ def is_false_positive_nodata(y, x):
+ return src_basename == 'egm08_25.gtx' and y == 2277 and x == 6283
+
+ if args.encoding == 'int32-scale-1-1000':
+ assert src_ds.RasterCount == 1
+ scale = 0.001
+ nodata = None if args.ignore_nodata else src_ds.GetRasterBand(
+ 1).GetNoDataValue()
+ data = src_ds.GetRasterBand(1).ReadAsArray()
+ if nodata is None:
+ data = data / scale
+ else:
+ dst_nodata = 9999000
+ nodata_as_f = struct.pack('f', nodata)
+ has_warned_nodata = False
+ for y in range(src_ds.RasterYSize):
+ for x in range(src_ds.RasterXSize):
+ is_nodata = False
+ if struct.pack('f', data[y][x]) == nodata_as_f:
+ if is_false_positive_nodata(y, x):
+ pass
+ elif not has_warned_nodata:
+ print(
+ 'At least one value matches nodata (at %d,%d). Setting it' % (y, x))
+ tmp_ds.GetRasterBand(
+ 1).SetNoDataValue(dst_nodata)
+ has_warned_nodata = True
+ is_nodata = True
+ else:
+ is_nodata = True
+
+ if is_nodata:
+ data[y][x] = dst_nodata
+ else:
+ data[y][x] = data[y][x] / scale
+
+ tmp_ds.GetRasterBand(1).WriteArray(data)
+ tmp_ds.GetRasterBand(1).SetOffset(0)
+ tmp_ds.GetRasterBand(1).SetScale(scale)
+ if idx_ifd == 0 or not compact_md:
+ tmp_ds.GetRasterBand(1).SetDescription(
+ 'geoid_undulation' if args.type == 'GEOGRAPHIC_TO_VERTICAL' else 'vertical_offset')
+ tmp_ds.GetRasterBand(1).SetUnitType('metre')
+
+ elif args.encoding == 'uint16':
+ for i in (1, ):
+ min, max = src_ds.GetRasterBand(i).ComputeRasterMinMax()
+ data = src_ds.GetRasterBand(i).ReadAsArray()
+ if is_vertcon: # in millimetres originally !
+ assert min < -100 or max > 100
+ min = min * 0.001
+ max = max * 0.001
+
+ dst_nodata = 65535
+ scale = (max - min) / 65534 # we use 65535 for nodata
+
+ nodata = None if args.ignore_nodata else src_ds.GetRasterBand(
+ i).GetNoDataValue()
+ if nodata is None:
+ if is_vertcon: # in millimetres originally !
+ data = data * 0.001
+ data = (data - min) / scale
+ else:
+ nodata_as_f = struct.pack('f', nodata)
+ has_warned_nodata = False
+ for y in range(src_ds.RasterYSize):
+ for x in range(src_ds.RasterXSize):
+ is_nodata = False
+ if struct.pack('f', data[y][x]) == nodata_as_f:
+ if is_false_positive_nodata(y, x):
+ pass
+ elif not has_warned_nodata:
+ print(
+ 'At least one value matches nodata (at %d,%d). Setting it' % (y, x))
+ tmp_ds.GetRasterBand(
+ i).SetNoDataValue(dst_nodata)
+ has_warned_nodata = True
+ is_nodata = True
+ else:
+ is_nodata = True
+
+ if is_nodata:
+ data[y][x] = dst_nodata
+ else:
+ if is_vertcon: # in millimetres originally !
+ data[y][x] = (
+ data[y][x] * 0.001 - min) / scale
+ else:
+ data[y][x] = (data[y][x] - min) / scale
+
+ tmp_ds.GetRasterBand(i).WriteArray(data)
+ tmp_ds.GetRasterBand(i).SetOffset(min)
+ tmp_ds.GetRasterBand(i).SetScale(scale)
+ if idx_ifd == 0 or not compact_md:
+ tmp_ds.GetRasterBand(i).SetDescription(
+ 'geoid_undulation' if args.type == 'GEOGRAPHIC_TO_VERTICAL' else 'vertical_offset')
+ tmp_ds.GetRasterBand(i).SetUnitType('metre')
+
+ else:
+ for i in (1,):
+ data = src_ds.GetRasterBand(i).ReadRaster()
+
+ nodata = None if args.ignore_nodata else src_ds.GetRasterBand(
+ i).GetNoDataValue()
+ nvalues = src_ds.RasterXSize * src_ds.RasterYSize
+ dst_nodata = -32768
+ if nodata:
+ packed_src_nodata = struct.pack('f', nodata)
+ packed_dst_nodata = struct.pack('f', dst_nodata)
+ assert src_ds.GetRasterBand(1).DataType == gdal.GDT_Float32
+ has_warned_nodata = False
+ out_data = bytearray(data)
+ for idx in range(len(data)//4):
+ is_nodata = False
+ if data[idx*4:idx*4+4] == packed_src_nodata:
+ y = idx // src_ds.RasterXSize
+ x = idx % src_ds.RasterXSize
+ if is_false_positive_nodata(y, x):
+ pass
+ elif not has_warned_nodata:
+ print(
+ 'At least one value matches nodata (at idx %d,%d). Setting it' % (y, x))
+ tmp_ds.GetRasterBand(
+ i).SetNoDataValue(dst_nodata)
+ has_warned_nodata = True
+ is_nodata = True
+ else:
+ is_nodata = True
+
+ if is_nodata:
+ out_data[idx*4:idx*4+4] = packed_dst_nodata
+
+ data = bytes(out_data)
+
+ if is_vertcon: # in millimetres originally !
+ min, max = src_ds.GetRasterBand(i).ComputeRasterMinMax()
+ assert min < -100 or max > 100
+ assert src_ds.GetRasterBand(1).DataType == gdal.GDT_Float32
+ out_data = b''
+ for v in struct.unpack('f' * nvalues, data):
+ out_data += struct.pack('f', v * 0.001 if v !=
+ dst_nodata else dst_nodata)
+ data = out_data
+
+ tmp_ds.GetRasterBand(i).WriteRaster(0, 0, src_ds.RasterXSize, src_ds.RasterYSize,
+ data)
+ if idx_ifd == 0 or not compact_md:
+ tmp_ds.GetRasterBand(i).SetDescription(
+ 'geoid_undulation' if args.type == 'GEOGRAPHIC_TO_VERTICAL' else 'vertical_offset')
+ tmp_ds.GetRasterBand(i).SetUnitType('metre')
+
+ if idx_ifd == 0:
+ desc = args.description
+ if not desc:
+ src_auth_name = src_crs.GetAuthorityName(None)
+ src_auth_code = src_crs.GetAuthorityCode(None)
+
+ desc = src_crs.GetName()
+ if src_auth_name and src_auth_code:
+ desc += ' (' + src_auth_name + ':' + src_auth_code + ')'
+ desc += ' to '
+ desc += dst_crs.GetName()
+ if dst_auth_name and dst_auth_code:
+ desc += ' (' + dst_auth_name + ':' + dst_auth_code + ')'
+ desc += '. Converted from '
+ desc += src_basename
+ desc += ' (last modified at '
+ desc += datetime.datetime.utcfromtimestamp(
+ os.stat(sourcefilename).st_mtime).strftime("%Y/%m/%d")
+ desc += ')'
+
+ tmp_ds.SetMetadataItem('TIFFTAG_IMAGEDESCRIPTION', desc)
+ if args.copyright:
+ tmp_ds.SetMetadataItem('TIFFTAG_COPYRIGHT', args.copyright)
+ if args.datetime and args.datetime != 'NONE':
+ tmp_ds.SetMetadataItem('TIFFTAG_DATETIME', args.datetime)
+ if args.area_of_use:
+ tmp_ds.SetMetadataItem('area_of_use', args.area_of_use)
+
+ options = ['PHOTOMETRIC=MINISBLACK',
+ 'COMPRESS=DEFLATE',
+ 'PREDICTOR=3' if target_dt == gdal.GDT_Float32 else 'PREDICTOR=2',
+ 'INTERLEAVE=BAND',
+ 'GEOTIFF_VERSION=1.1']
+ if tmp_ds.RasterXSize > 256 and tmp_ds.RasterYSize > 256:
+ options.append('TILED=YES')
+ else:
+ options.append('BLOCKYSIZE=' + str(tmp_ds.RasterYSize))
+ if gdal.VSIStatL(tmpfilename) is not None:
+ options.append('APPEND_SUBDATASET=YES')
+
+ assert gdal.GetDriverByName('GTiff').CreateCopy(tmpfilename, tmp_ds,
+ options=options)
+
+
+def check(sourcefilename, destfilename, args):
+ src_ds = gdal.Open(sourcefilename)
+ src_subdatsets = src_ds.GetSubDatasets()
+ if not src_subdatsets:
+ src_subdatsets = [(sourcefilename, None)]
+
+ dst_ds = gdal.Open(destfilename)
+ dst_subdatsets = dst_ds.GetSubDatasets()
+ if not dst_subdatsets:
+ dst_subdatsets = [(destfilename, None)]
+
+ assert len(src_subdatsets) == len(dst_subdatsets)
+ for dst_subds in dst_subdatsets:
+ dst_ds = gdal.Open(dst_subds[0])
+ dst_ds.GetRasterBand(1).Checksum()
+ assert gdal.GetLastErrorMsg() == ''
+
+
+if __name__ == '__main__':
+
+ args = get_args()
+
+ tmpfilename = args.dest + '.tmp'
+ gdal.Unlink(tmpfilename)
+
+ if not args.datetime and args.datetime != 'NONE':
+ args.datetime = datetime.date.today().strftime("%Y:%m:%d %H:%M:%S")
+
+ create_unoptimized_file(args.source, tmpfilename, args)
+ generate_optimized_file(tmpfilename, args.dest)
+ check(args.source, args.dest, args)
+
+ gdal.Unlink(tmpfilename)