├── .gitignore ├── From Fatianda (use_this).ipynb ├── LICENSE ├── README.md ├── grd2geotiff.ipynb ├── grd2geotiff.py └── test_data ├── test_compressed.grd ├── test_compressed.grd.gi ├── test_compressed.grd.xml ├── test_uncompressed.grd ├── test_uncompressed.grd.gi └── test_uncompressed.grd.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | test*.tif 133 | old.ipynb 134 | -------------------------------------------------------------------------------- /From Fatianda (use_this).ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "37bb9833", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# Copyright (c) 2018 The Harmonica Developers.\n", 11 | "# Distributed under the terms of the BSD 3-Clause License.\n", 12 | "# SPDX-License-Identifier: BSD-3-Clause\n", 13 | "#\n", 14 | "# This code is part of the Fatiando a Terra project (https://www.fatiando.org)\n", 15 | "#\n", 16 | "# The support for compressed grids was inspired by the code in\n", 17 | "# https://github.com/Loop3D/geosoft_grid copyrighted by Loop3D and released\n", 18 | "# under the MIT License.\n", 19 | "\"\"\"\n", 20 | "Function to read Oasis Montaj© .grd file\n", 21 | "\"\"\"\n", 22 | "\n", 23 | "import array\n", 24 | "import zlib\n", 25 | "\n", 26 | "import numpy as np\n", 27 | "import xarray as xr\n", 28 | "\n", 29 | "# Define the valid element sizes (ES variable) for GRD files\n", 30 | "# (values > 1024 correspond to compressed versions of the grid)\n", 31 | "VALID_ELEMENT_SIZES = (1, 2, 4, 8, 1024 + 1, 1024 + 2, 1024 + 4, 1024 + 8)\n", 32 | "# Define dummy values for each data type\n", 33 | "DUMMIES = {\n", 34 | " \"b\": -127,\n", 35 | " \"B\": 255,\n", 36 | " \"h\": -32767,\n", 37 | " \"H\": 65535,\n", 38 | " \"i\": -2147483647,\n", 39 | " \"I\": 4294967295,\n", 40 | " \"f\": -1e32,\n", 41 | " \"d\": -1e32,\n", 42 | "}\n", 43 | "\n", 44 | "\n", 45 | "def load_oasis_montaj_grid(fname):\n", 46 | " \"\"\"\n", 47 | " Reads gridded data from an Oasis Montaj© .grd file.\n", 48 | " The version 2 of the Geosoft© Grid File Format (GRD) stores gridded\n", 49 | " products in binary data. This function can read those files and parse the\n", 50 | " information in the header. It returns the data in\n", 51 | " a :class:`xarray.DataArray` for convenience.\n", 52 | " .. warning::\n", 53 | " This function has not been tested against a wide range of GRD files.\n", 54 | " This could lead to incorrect readings of the stored data. Please report\n", 55 | " any unwanted behaviour by opening an issue in Harmonica:\n", 56 | " https://github.com/fatiando/harmonica/issues\n", 57 | " .. important::\n", 58 | " This function only supports reading GRD files using the **version 2**\n", 59 | " of the Geosoft© Grid File Format.\n", 60 | " .. important::\n", 61 | " This function is not supporting orderings different than ±1,\n", 62 | " or colour grids.\n", 63 | " Parameters\n", 64 | " ----------\n", 65 | " fname : string or file-like object\n", 66 | " Path to the .grd file.\n", 67 | " Returns\n", 68 | " -------\n", 69 | " grid : :class:`xarray.DataArray`\n", 70 | " :class:`xarray.DataArray` containing the grid, its coordinates and\n", 71 | " header information.\n", 72 | "\n", 73 | " References\n", 74 | " ----------\n", 75 | " https://help.seequent.com/Oasis-montaj/9.9/en/Content/ss/glossary/grid_file_format__grd.htm\n", 76 | " https://github.com/Loop3D/geosoft_grid\n", 77 | " \"\"\"\n", 78 | " # Read the header and the grid array\n", 79 | " with open(fname, \"rb\") as grd_file:\n", 80 | " # Read the header (first 512 bytes)\n", 81 | " header = _read_header(grd_file.read(512))\n", 82 | " # Check for valid flags\n", 83 | " _check_ordering(header[\"ordering\"])\n", 84 | " _check_sign_flag(header[\"sign_flag\"])\n", 85 | " # Get data type for the grid elements\n", 86 | " data_type = _get_data_type(header[\"n_bytes_per_element\"], header[\"sign_flag\"])\n", 87 | " # Read grid\n", 88 | " grid = grd_file.read()\n", 89 | " # Decompress grid if needed\n", 90 | " if header[\"n_bytes_per_element\"] > 1024:\n", 91 | " grid = _decompress_grid(grid)\n", 92 | " # Load the grid values as an array with the proper data_type\n", 93 | " grid = array.array(data_type, grid)\n", 94 | " # Convert to numpy array as float64\n", 95 | " grid = np.array(grid, dtype=np.float64)\n", 96 | " # Remove dummy values\n", 97 | " grid = _remove_dummies(grid, data_type)\n", 98 | " # Scale the grid\n", 99 | " grid = np.array(grid / header[\"data_factor\"] + header[\"base_value\"])\n", 100 | " # Reshape the grid based on the ordering\n", 101 | " if header[\"ordering\"] == 1:\n", 102 | " order = \"C\"\n", 103 | " shape = (header[\"shape_v\"], header[\"shape_e\"])\n", 104 | " spacing = (header[\"spacing_v\"], header[\"spacing_e\"])\n", 105 | " elif header[\"ordering\"] == -1:\n", 106 | " order = \"F\"\n", 107 | " shape = (header[\"shape_e\"], header[\"shape_v\"])\n", 108 | " spacing = (header[\"spacing_e\"], header[\"spacing_v\"])\n", 109 | " grid = grid.reshape(shape, order=order)\n", 110 | " # Build coords\n", 111 | " if header[\"rotation\"] == 0:\n", 112 | " easting, northing = _build_coordinates(\n", 113 | " header[\"x_origin\"], header[\"y_origin\"], shape, spacing\n", 114 | " )\n", 115 | " dims = (\"northing\", \"easting\")\n", 116 | " coords = {\"easting\": easting, \"northing\": northing}\n", 117 | " else:\n", 118 | " easting, northing = _build_rotated_coordinates(\n", 119 | " header[\"x_origin\"], header[\"y_origin\"], shape, spacing, header[\"rotation\"]\n", 120 | " )\n", 121 | " dims = (\"y\", \"x\")\n", 122 | " coords = {\"easting\": (dims, easting), \"northing\": (dims, northing)}\n", 123 | " # Build an xarray.DataArray for the grid\n", 124 | " grid = xr.DataArray(\n", 125 | " grid,\n", 126 | " coords=coords,\n", 127 | " dims=dims,\n", 128 | " attrs=header,\n", 129 | " )\n", 130 | " return grid\n", 131 | "\n", 132 | "\n", 133 | "def _read_header(header_bytes):\n", 134 | " \"\"\"\n", 135 | " Read GRD file header\n", 136 | " Parameters\n", 137 | " ----------\n", 138 | " header_bytes : byte\n", 139 | " A sequence of 512 bytes containing the header of a\n", 140 | " GRD file.\n", 141 | " Returns\n", 142 | " -------\n", 143 | " header : dict\n", 144 | " Dictionary containing the information present in the\n", 145 | " header.\n", 146 | " Notes\n", 147 | " -----\n", 148 | " The GRD header consists in 512 contiguous bytes.\n", 149 | " It's divided in four sections:\n", 150 | " * Data Storage\n", 151 | " * Geographic Information\n", 152 | " * Data (Z) Scaling\n", 153 | " * Undefined Application Parameters\n", 154 | " \"\"\"\n", 155 | " header = {}\n", 156 | " # Read data storage\n", 157 | " ES, SF, NE, NV, KX = array.array(\"i\", header_bytes[0 : 5 * 4]) # noqa: N806\n", 158 | " header.update(\n", 159 | " {\n", 160 | " \"n_bytes_per_element\": ES,\n", 161 | " \"sign_flag\": SF,\n", 162 | " \"shape_e\": NE,\n", 163 | " \"shape_v\": NV,\n", 164 | " \"ordering\": KX,\n", 165 | " }\n", 166 | " )\n", 167 | " # Read geographic info\n", 168 | " DE, DV, X0, Y0, ROT = array.array(\"d\", header_bytes[20 : 20 + 5 * 8]) # noqa: N806\n", 169 | " header.update(\n", 170 | " {\n", 171 | " \"spacing_e\": DE,\n", 172 | " \"spacing_v\": DV,\n", 173 | " \"x_origin\": X0,\n", 174 | " \"y_origin\": Y0,\n", 175 | " \"rotation\": ROT,\n", 176 | " }\n", 177 | " )\n", 178 | " # Read data scaling\n", 179 | " ZBASE, ZMULT = array.array(\"d\", header_bytes[60 : 60 + 2 * 8]) # noqa: N806\n", 180 | " header.update(\n", 181 | " {\n", 182 | " \"base_value\": ZBASE,\n", 183 | " \"data_factor\": ZMULT,\n", 184 | " }\n", 185 | " )\n", 186 | " # Read optional parameters\n", 187 | " # (ignore map LABEL and MAPNO)\n", 188 | " PROJ, UNITX, UNITY, UNITZ, NVPTS = array.array( # noqa: N806\n", 189 | " \"i\", header_bytes[140 : 140 + 5 * 4]\n", 190 | " )\n", 191 | " IZMIN, IZMAX, IZMED, IZMEA = array.array( # noqa: N806\n", 192 | " \"f\", header_bytes[160 : 160 + 4 * 4]\n", 193 | " )\n", 194 | " (ZVAR,) = array.array(\"d\", header_bytes[176 : 176 + 8]) # noqa: N806\n", 195 | " (PRCS,) = array.array(\"i\", header_bytes[184 : 184 + 4]) # noqa: N806\n", 196 | " header.update(\n", 197 | " {\n", 198 | " \"map_projection\": PROJ,\n", 199 | " \"units_x\": UNITX,\n", 200 | " \"units_y\": UNITY,\n", 201 | " \"units_z\": UNITZ,\n", 202 | " \"n_valid_points\": NVPTS,\n", 203 | " \"grid_min\": IZMIN,\n", 204 | " \"grid_max\": IZMAX,\n", 205 | " \"grid_median\": IZMED,\n", 206 | " \"grid_mean\": IZMEA,\n", 207 | " \"grid_variance\": ZVAR,\n", 208 | " \"process_flag\": PRCS,\n", 209 | " }\n", 210 | " )\n", 211 | " return header\n", 212 | "\n", 213 | "\n", 214 | "def _check_ordering(ordering):\n", 215 | " \"\"\"\n", 216 | " Check if the ordering value is within the ones we are supporting\n", 217 | " \"\"\"\n", 218 | " if ordering not in (-1, 1):\n", 219 | " raise NotImplementedError(\n", 220 | " f\"Found an ordering (a.k.a as KX) equal to '{ordering}'. \"\n", 221 | " + \"Only orderings equal to 1 and -1 are supported.\"\n", 222 | " )\n", 223 | "\n", 224 | "\n", 225 | "def _check_sign_flag(sign_flag):\n", 226 | " \"\"\"\n", 227 | " Check if sign_flag value is within the ones we are supporting\n", 228 | " \"\"\"\n", 229 | " if sign_flag == 3:\n", 230 | " raise NotImplementedError(\n", 231 | " \"Reading .grd files with colour grids is not currenty supported.\"\n", 232 | " )\n", 233 | "\n", 234 | "\n", 235 | "def _get_data_type(n_bytes_per_element, sign_flag):\n", 236 | " \"\"\"\n", 237 | " Return the data type for the grid values\n", 238 | " References\n", 239 | " ----------\n", 240 | " https://docs.python.org/3/library/array.html\n", 241 | " \"\"\"\n", 242 | " # Check if number of bytes per element is valid\n", 243 | " if n_bytes_per_element not in VALID_ELEMENT_SIZES:\n", 244 | " raise NotImplementedError(\n", 245 | " \"Found a 'Grid data element size' (a.k.a. 'ES') value \"\n", 246 | " + f\"of '{n_bytes_per_element}'. \"\n", 247 | " \"Only values equal to 1, 2, 4 and 8 are valid, \"\n", 248 | " + \"along with their compressed counterparts (1025, 1026, 1028, 1032).\"\n", 249 | " )\n", 250 | " # Shift the n_bytes_per_element in case of compressed grids\n", 251 | " if n_bytes_per_element > 1024:\n", 252 | " n_bytes_per_element -= 1024\n", 253 | " # Determine the data type of the grid elements\n", 254 | " if n_bytes_per_element == 1:\n", 255 | " if sign_flag == 0:\n", 256 | " data_type = \"B\" # unsigned char\n", 257 | " elif sign_flag == 1:\n", 258 | " data_type = \"b\" # signed char\n", 259 | " elif n_bytes_per_element == 2:\n", 260 | " if sign_flag == 0:\n", 261 | " data_type = \"H\" # unsigned short\n", 262 | " elif sign_flag == 1:\n", 263 | " data_type = \"h\" # signed short\n", 264 | " elif n_bytes_per_element == 4:\n", 265 | " if sign_flag == 0:\n", 266 | " data_type = \"I\" # unsigned int\n", 267 | " elif sign_flag == 1:\n", 268 | " data_type = \"i\" # signed int\n", 269 | " elif sign_flag == 2:\n", 270 | " data_type = \"f\" # float\n", 271 | " elif n_bytes_per_element == 8:\n", 272 | " data_type = \"d\"\n", 273 | " return data_type\n", 274 | "\n", 275 | "\n", 276 | "def _remove_dummies(grid, data_type):\n", 277 | " \"\"\"\n", 278 | " Replace dummy values for NaNs\n", 279 | " \"\"\"\n", 280 | " # Create dictionary with dummy value for each data type\n", 281 | " if data_type in (\"f\", \"d\"):\n", 282 | " grid[grid <= DUMMIES[data_type]] = np.nan\n", 283 | " return grid\n", 284 | " grid[grid == DUMMIES[data_type]] = np.nan\n", 285 | " return grid\n", 286 | "\n", 287 | "\n", 288 | "def _decompress_grid(grid_compressed):\n", 289 | " \"\"\"\n", 290 | " Decompress the grid using gzip\n", 291 | " Even if the header specifies that the grid is compressed using a LZRW1\n", 292 | " algorithm, it's using gzip instead. The first two 4 bytes sequences\n", 293 | " correspond to the compression signature and\n", 294 | " to the compression type. We are going to ignore those and start reading\n", 295 | " from the number of blocks (offset 8).\n", 296 | " Parameters\n", 297 | " ----------\n", 298 | " grid_compressed : bytes\n", 299 | " Sequence of bytes corresponding to the compressed grid. They should be\n", 300 | " every byte starting from offset 512 of the GRD file until its end.\n", 301 | " Returns\n", 302 | " -------\n", 303 | " grid : bytes\n", 304 | " Uncompressed version of the ``grid_compressed`` parameter.\n", 305 | " \"\"\"\n", 306 | " # Number of blocks\n", 307 | " (n_blocks,) = array.array(\"i\", grid_compressed[8 : 8 + 4])\n", 308 | " # Number of vectors per block\n", 309 | " (vectors_per_block,) = array.array(\"i\", grid_compressed[12 : 12 + 4])\n", 310 | " # File offset from start of every block\n", 311 | " block_offsets = array.array(\"q\", grid_compressed[16 : 16 + n_blocks * 8])\n", 312 | " # Compressed size of every block\n", 313 | " compressed_block_sizes = array.array(\n", 314 | " \"i\",\n", 315 | " grid_compressed[16 + n_blocks * 8 : 16 + n_blocks * 8 + n_blocks * 4],\n", 316 | " )\n", 317 | " # Combine grid\n", 318 | " grid = b\"\"\n", 319 | " # Read each block\n", 320 | " for i in range(n_blocks):\n", 321 | " # Define the start and end offsets for each compressed blocks\n", 322 | " # We need to remove the 512 to account for the missing header.\n", 323 | " # There is an unexplained 16 byte header that we also need to remove.\n", 324 | " start_offset = block_offsets[i] - 512 + 16\n", 325 | " end_offset = compressed_block_sizes[i] + block_offsets[i] - 512\n", 326 | " # Decompress the block\n", 327 | " grid_sub = zlib.decompress(\n", 328 | " grid_compressed[start_offset:end_offset],\n", 329 | " bufsize=zlib.DEF_BUF_SIZE,\n", 330 | " )\n", 331 | " # Add it to the running grid\n", 332 | " grid += grid_sub\n", 333 | " return grid\n", 334 | "\n", 335 | "\n", 336 | "def _build_coordinates(west, south, shape, spacing):\n", 337 | " \"\"\"\n", 338 | " Create the coordinates for the grid\n", 339 | " Generates 1d arrays for the easting and northing coordinates of the grid.\n", 340 | " Assumes unrotated grids.\n", 341 | " Parameters\n", 342 | " ----------\n", 343 | " west : float\n", 344 | " Westernmost coordinate of the grid.\n", 345 | " south : float\n", 346 | " Southernmost coordinate of the grid.\n", 347 | " shape : tuple\n", 348 | " Tuple of ints containing the number of elements along each direction in\n", 349 | " the following order: ``n_northing``, ``n_easting``\n", 350 | " spacing : tuple\n", 351 | " Tuple of floats containing the distance between adjacent grid elements\n", 352 | " along each direction in the following order: ``s_northing``,\n", 353 | " ``s_easting``.\n", 354 | " Returns\n", 355 | " -------\n", 356 | " easting : 1d-array\n", 357 | " Array containing the values of the easting coordinates of the grid.\n", 358 | " northing : 1d-array\n", 359 | " Array containing the values of the northing coordinates of the grid.\n", 360 | " \"\"\"\n", 361 | " easting = np.linspace(west, west + spacing[1] * (shape[1] - 1), shape[1])\n", 362 | " northing = np.linspace(south, south + spacing[0] * (shape[0] - 1), shape[0])\n", 363 | " return easting, northing\n", 364 | "\n", 365 | "\n", 366 | "def _build_rotated_coordinates(west, south, shape, spacing, rotation_deg):\n", 367 | " \"\"\"\n", 368 | " Create the coordinates for a rotated grid\n", 369 | " Generates 2d arrays for the easting and northing coordinates of the grid.\n", 370 | " Assumes rotated grids.\n", 371 | " Parameters\n", 372 | " ----------\n", 373 | " west : float\n", 374 | " Westernmost coordinate of the grid.\n", 375 | " south : float\n", 376 | " Southernmost coordinate of the grid.\n", 377 | " shape : tuple\n", 378 | " Tuple of ints containing the number of elements along each unrotated\n", 379 | " direction in the following order: ``n_y``, ``n_x``\n", 380 | " spacing : tuple\n", 381 | " Tuple of floats containing the distance between adjacent grid elements\n", 382 | " along each unrotated direction in the following order: ``spacing_y``,\n", 383 | " ``spacing_x``.\n", 384 | " Returns\n", 385 | " -------\n", 386 | " easting : 2d-array\n", 387 | " Array containing the values of the easting coordinates of the grid.\n", 388 | " northing : 2d-array\n", 389 | " Array containing the values of the northing coordinates of the grid.\n", 390 | " Notes\n", 391 | " -----\n", 392 | " The ``x`` and ``y`` coordinates are the abscissa and the ordinate of the\n", 393 | " unrotated grid before the translation, respectively.\n", 394 | " \"\"\"\n", 395 | " # Define the grid coordinates before the rotation\n", 396 | " x = np.linspace(0, spacing[1] * (shape[1] - 1), shape[1])\n", 397 | " y = np.linspace(0, spacing[0] * (shape[0] - 1), shape[0])\n", 398 | " # Compute a meshgrid\n", 399 | " x, y = np.meshgrid(x, y)\n", 400 | " # Rotate and shift to get easting and northing\n", 401 | " rotation_rad = np.radians(rotation_deg)\n", 402 | " cos, sin = np.cos(rotation_rad), np.sin(rotation_rad)\n", 403 | " easting = west + x * cos - y * sin\n", 404 | " northing = south + x * sin + y * cos\n", 405 | " return easting, northing" 406 | ] 407 | }, 408 | { 409 | "cell_type": "markdown", 410 | "id": "4124ca77", 411 | "metadata": {}, 412 | "source": [ 413 | "### Code to convert extracted xarray to geotiff format file" 414 | ] 415 | }, 416 | { 417 | "cell_type": "code", 418 | "execution_count": null, 419 | "id": "d786c4c5", 420 | "metadata": {}, 421 | "outputs": [], 422 | "source": [ 423 | "import rasterio\n", 424 | "from rasterio.transform import from_origin\n", 425 | "\n", 426 | "def write_geotiff(out_path,grid_uc,epsg):\n", 427 | " \"\"\"\n", 428 | " write out grid in geotiff format\n", 429 | " Parameters\n", 430 | " ----------\n", 431 | " out_path : string\n", 432 | " file path and name to write to\n", 433 | " grid_uc : xarray\n", 434 | " grid and header info \n", 435 | " epsg: string\n", 436 | " user defined epsg\n", 437 | " \"\"\" \n", 438 | " transform = from_origin(grid_uc.attrs['x_origin']-(grid_uc.attrs['spacing_e']/2), grid_uc.attrs['y_origin']-(grid_uc.attrs['spacing_v']/2), grid_uc.attrs['spacing_e'], -grid_uc.attrs['spacing_v'])\n", 439 | "\n", 440 | " new_dataset = rasterio.open(\n", 441 | " out_path,\n", 442 | " \"w\",\n", 443 | " driver=\"GTiff\",\n", 444 | " height=grid_uc.attrs['shape_v'],\n", 445 | " width=grid_uc.attrs['shape_e'],\n", 446 | " count=1,\n", 447 | " dtype=rasterio.float32,\n", 448 | " crs='epsg:'+epsg,#\"+proj=longlat\",\n", 449 | " transform=transform,\n", 450 | " nodata=-1e8,\n", 451 | " )\n", 452 | "\n", 453 | " new_dataset.write(grid_uc.values, 1)\n", 454 | " new_dataset.close()\n", 455 | " print(\"dtm geotif saved as\", out_path)\n" 456 | ] 457 | }, 458 | { 459 | "cell_type": "markdown", 460 | "id": "360df4ac", 461 | "metadata": {}, 462 | "source": [ 463 | "### Example code to convert uncompressed grid" 464 | ] 465 | }, 466 | { 467 | "cell_type": "code", 468 | "execution_count": null, 469 | "id": "f2934ef2", 470 | "metadata": {}, 471 | "outputs": [], 472 | "source": [ 473 | "filename='./test_data/test_uncompressed.grd' # uncompressed example\n", 474 | "grid_uc=load_oasis_montaj_grid(filename)\n", 475 | "write_geotiff('./test1.tif',grid_uc,'4326')" 476 | ] 477 | }, 478 | { 479 | "cell_type": "markdown", 480 | "id": "24f4a5a9", 481 | "metadata": {}, 482 | "source": [ 483 | "### Example code to convert compressed grid" 484 | ] 485 | }, 486 | { 487 | "cell_type": "code", 488 | "execution_count": null, 489 | "id": "e53f5bfa", 490 | "metadata": {}, 491 | "outputs": [], 492 | "source": [ 493 | "filename='./test_data/test_compressed.grd' # compressed example\n", 494 | "grid_c=load_oasis_montaj_grid(filename)\n", 495 | "write_geotiff('./test2.tif',grid_c,'4326')" 496 | ] 497 | }, 498 | { 499 | "cell_type": "markdown", 500 | "id": "f8b5f1c3", 501 | "metadata": {}, 502 | "source": [ 503 | "### Example code to parse XML to get epsg" 504 | ] 505 | }, 506 | { 507 | "cell_type": "code", 508 | "execution_count": null, 509 | "id": "70b37221", 510 | "metadata": {}, 511 | "outputs": [], 512 | "source": [ 513 | "# Suggested code to parse xml to get CRS from\n", 514 | "# Santiago Soler \n", 515 | "# https://github.com/fatiando/harmonica/pull/348#issuecomment-1327811755\n", 516 | "\n", 517 | "\n", 518 | "def extract_proj_str(fname):\n", 519 | " with open(fname, \"r\") as f:\n", 520 | " for line in f:\n", 521 | " if \"projection=\" in line:\n", 522 | " # print lines with projection info\n", 523 | " print(line)\n", 524 | "\n", 525 | " # extract projection string\n", 526 | " proj = line.split('projection=\"')[1].split('\" ')[0]\n", 527 | " print(proj)\n", 528 | "\n", 529 | " # remove non-alphanumeric characters if present\n", 530 | " if proj.isalnum() is False:\n", 531 | " proj = ''.join(filter(str.isalnum, proj))\n", 532 | "\n", 533 | " assert proj.isalnum\n", 534 | " try:\n", 535 | " proj\n", 536 | " except:\n", 537 | " raise NameError(\"string 'projection=' not found in file.\")\n", 538 | " proj = None\n", 539 | "\n", 540 | " return proj\n", 541 | "\n", 542 | "proj = extract_proj_str(filename+\".xml\")" 543 | ] 544 | } 545 | ], 546 | "metadata": { 547 | "kernelspec": { 548 | "display_name": "Python 3 (ipykernel)", 549 | "language": "python", 550 | "name": "python3" 551 | }, 552 | "language_info": { 553 | "codemirror_mode": { 554 | "name": "ipython", 555 | "version": 3 556 | }, 557 | "file_extension": ".py", 558 | "mimetype": "text/x-python", 559 | "name": "python", 560 | "nbconvert_exporter": "python", 561 | "pygments_lexer": "ipython3", 562 | "version": "3.8.15" 563 | } 564 | }, 565 | "nbformat": 4, 566 | "nbformat_minor": 5 567 | } 568 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Loop3D 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Code to convert geosoft grids to geotiffs based on info at: 2 | 3 | https://help.seequent.com/Oasis-montaj/9.9/en/Content/ss/process_data/grid_data/c/geosoft_grid_file_format.htm plus info that compression is in fact zlib not LZRW1 regardless of what COMP_TYPE says! (Thanks Evren Pakyuz-Charrier). 4 | 5 | ### Current development of this code has been taken up by Fatiando: 6 | https://github.com/fatiando/harmonica/blob/main/harmonica/_io/oasis_montaj_grd.py 7 | 8 | ### Also available as a QGIS Plugin here: 9 | https://github.com/Loop3D/grd_loader 10 | 11 | ### Also available here as command line version: 12 | Usage: python grd2geotiff.py input_grid_path [string] epsg [int] 13 | 14 | ### Test data: 15 | From CPRM Open Access geophysical data server https://geosgb.cprm.gov.br/geosgb/downloads.html 16 | 17 | ### Requirements: 18 | numpy 19 | xarray 20 | rasterio 21 | -------------------------------------------------------------------------------- /grd2geotiff.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "id": "view-in-github", 7 | "colab_type": "text" 8 | }, 9 | "source": [ 10 | "\"Open" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "id": "37bb9833", 17 | "metadata": { 18 | "id": "37bb9833" 19 | }, 20 | "outputs": [], 21 | "source": [ 22 | "# Copyright (c) 2018 The Harmonica Developers.\n", 23 | "# Distributed under the terms of the BSD 3-Clause License.\n", 24 | "# SPDX-License-Identifier: BSD-3-Clause\n", 25 | "#\n", 26 | "# This code is part of the Fatiando a Terra project (https://www.fatiando.org)\n", 27 | "#\n", 28 | "# The support for compressed grids was inspired by the code in\n", 29 | "# https://github.com/Loop3D/geosoft_grid copyrighted by Loop3D and released\n", 30 | "# under the MIT License.\n", 31 | "\"\"\"\n", 32 | "Function to read Oasis Montaj© .grd file\n", 33 | "\"\"\"\n", 34 | "\n", 35 | "import array\n", 36 | "import zlib\n", 37 | "\n", 38 | "import numpy as np\n", 39 | "import xarray as xr\n", 40 | "\n", 41 | "# Define the valid element sizes (ES variable) for GRD files\n", 42 | "# (values > 1024 correspond to compressed versions of the grid)\n", 43 | "VALID_ELEMENT_SIZES = (1, 2, 4, 8, 1024 + 1, 1024 + 2, 1024 + 4, 1024 + 8)\n", 44 | "# Define dummy values for each data type\n", 45 | "DUMMIES = {\n", 46 | " \"b\": -127,\n", 47 | " \"B\": 255,\n", 48 | " \"h\": -32767,\n", 49 | " \"H\": 65535,\n", 50 | " \"i\": -2147483647,\n", 51 | " \"I\": 4294967295,\n", 52 | " \"f\": -1e32,\n", 53 | " \"d\": -1e32,\n", 54 | "}\n", 55 | "\n", 56 | "\n", 57 | "def load_oasis_montaj_grid(fname):\n", 58 | " \"\"\"\n", 59 | " Reads gridded data from an Oasis Montaj© .grd file.\n", 60 | " The version 2 of the Geosoft© Grid File Format (GRD) stores gridded\n", 61 | " products in binary data. This function can read those files and parse the\n", 62 | " information in the header. It returns the data in\n", 63 | " a :class:`xarray.DataArray` for convenience.\n", 64 | " .. warning::\n", 65 | " This function has not been tested against a wide range of GRD files.\n", 66 | " This could lead to incorrect readings of the stored data. Please report\n", 67 | " any unwanted behaviour by opening an issue in Harmonica:\n", 68 | " https://github.com/fatiando/harmonica/issues\n", 69 | " .. important::\n", 70 | " This function only supports reading GRD files using the **version 2**\n", 71 | " of the Geosoft© Grid File Format.\n", 72 | " .. important::\n", 73 | " This function is not supporting orderings different than ±1,\n", 74 | " or colour grids.\n", 75 | " Parameters\n", 76 | " ----------\n", 77 | " fname : string or file-like object\n", 78 | " Path to the .grd file.\n", 79 | " Returns\n", 80 | " -------\n", 81 | " grid : :class:`xarray.DataArray`\n", 82 | " :class:`xarray.DataArray` containing the grid, its coordinates and\n", 83 | " header information.\n", 84 | "\n", 85 | " References\n", 86 | " ----------\n", 87 | " https://help.seequent.com/Oasis-montaj/9.9/en/Content/ss/glossary/grid_file_format__grd.htm\n", 88 | " https://github.com/Loop3D/geosoft_grid\n", 89 | " \"\"\"\n", 90 | " # Read the header and the grid array\n", 91 | " with open(fname, \"rb\") as grd_file:\n", 92 | " # Read the header (first 512 bytes)\n", 93 | " header = _read_header(grd_file.read(512))\n", 94 | " # Check for valid flags\n", 95 | " _check_ordering(header[\"ordering\"])\n", 96 | " _check_sign_flag(header[\"sign_flag\"])\n", 97 | " # Get data type for the grid elements\n", 98 | " data_type = _get_data_type(header[\"n_bytes_per_element\"], header[\"sign_flag\"])\n", 99 | " # Read grid\n", 100 | " grid = grd_file.read()\n", 101 | " # Decompress grid if needed\n", 102 | " if header[\"n_bytes_per_element\"] > 1024:\n", 103 | " grid = _decompress_grid(grid)\n", 104 | " # Load the grid values as an array with the proper data_type\n", 105 | " grid = array.array(data_type, grid)\n", 106 | " # Convert to numpy array as float64\n", 107 | " grid = np.array(grid, dtype=np.float64)\n", 108 | " # Remove dummy values\n", 109 | " grid = _remove_dummies(grid, data_type)\n", 110 | " # Scale the grid\n", 111 | " grid = np.array(grid / header[\"data_factor\"] + header[\"base_value\"])\n", 112 | " # Reshape the grid based on the ordering\n", 113 | " if header[\"ordering\"] == 1:\n", 114 | " order = \"C\"\n", 115 | " shape = (header[\"shape_v\"], header[\"shape_e\"])\n", 116 | " spacing = (header[\"spacing_v\"], header[\"spacing_e\"])\n", 117 | " elif header[\"ordering\"] == -1:\n", 118 | " order = \"F\"\n", 119 | " shape = (header[\"shape_e\"], header[\"shape_v\"])\n", 120 | " spacing = (header[\"spacing_e\"], header[\"spacing_v\"])\n", 121 | " grid = grid.reshape(shape, order=order)\n", 122 | " # Build coords\n", 123 | " if header[\"rotation\"] == 0:\n", 124 | " easting, northing = _build_coordinates(\n", 125 | " header[\"x_origin\"], header[\"y_origin\"], shape, spacing\n", 126 | " )\n", 127 | " dims = (\"northing\", \"easting\")\n", 128 | " coords = {\"easting\": easting, \"northing\": northing}\n", 129 | " else:\n", 130 | " easting, northing = _build_rotated_coordinates(\n", 131 | " header[\"x_origin\"], header[\"y_origin\"], shape, spacing, header[\"rotation\"]\n", 132 | " )\n", 133 | " dims = (\"y\", \"x\")\n", 134 | " coords = {\"easting\": (dims, easting), \"northing\": (dims, northing)}\n", 135 | " # Build an xarray.DataArray for the grid\n", 136 | " grid = xr.DataArray(\n", 137 | " grid,\n", 138 | " coords=coords,\n", 139 | " dims=dims,\n", 140 | " attrs=header,\n", 141 | " )\n", 142 | " return grid\n", 143 | "\n", 144 | "\n", 145 | "def _read_header(header_bytes):\n", 146 | " \"\"\"\n", 147 | " Read GRD file header\n", 148 | " Parameters\n", 149 | " ----------\n", 150 | " header_bytes : byte\n", 151 | " A sequence of 512 bytes containing the header of a\n", 152 | " GRD file.\n", 153 | " Returns\n", 154 | " -------\n", 155 | " header : dict\n", 156 | " Dictionary containing the information present in the\n", 157 | " header.\n", 158 | " Notes\n", 159 | " -----\n", 160 | " The GRD header consists in 512 contiguous bytes.\n", 161 | " It's divided in four sections:\n", 162 | " * Data Storage\n", 163 | " * Geographic Information\n", 164 | " * Data (Z) Scaling\n", 165 | " * Undefined Application Parameters\n", 166 | " \"\"\"\n", 167 | " header = {}\n", 168 | " # Read data storage\n", 169 | " ES, SF, NE, NV, KX = array.array(\"i\", header_bytes[0 : 5 * 4]) # noqa: N806\n", 170 | " header.update(\n", 171 | " {\n", 172 | " \"n_bytes_per_element\": ES,\n", 173 | " \"sign_flag\": SF,\n", 174 | " \"shape_e\": NE,\n", 175 | " \"shape_v\": NV,\n", 176 | " \"ordering\": KX,\n", 177 | " }\n", 178 | " )\n", 179 | " # Read geographic info\n", 180 | " DE, DV, X0, Y0, ROT = array.array(\"d\", header_bytes[20 : 20 + 5 * 8]) # noqa: N806\n", 181 | " header.update(\n", 182 | " {\n", 183 | " \"spacing_e\": DE,\n", 184 | " \"spacing_v\": DV,\n", 185 | " \"x_origin\": X0,\n", 186 | " \"y_origin\": Y0,\n", 187 | " \"rotation\": ROT,\n", 188 | " }\n", 189 | " )\n", 190 | " # Read data scaling\n", 191 | " ZBASE, ZMULT = array.array(\"d\", header_bytes[60 : 60 + 2 * 8]) # noqa: N806\n", 192 | " header.update(\n", 193 | " {\n", 194 | " \"base_value\": ZBASE,\n", 195 | " \"data_factor\": ZMULT,\n", 196 | " }\n", 197 | " )\n", 198 | " # Read optional parameters\n", 199 | " # (ignore map LABEL and MAPNO)\n", 200 | " PROJ, UNITX, UNITY, UNITZ, NVPTS = array.array( # noqa: N806\n", 201 | " \"i\", header_bytes[140 : 140 + 5 * 4]\n", 202 | " )\n", 203 | " IZMIN, IZMAX, IZMED, IZMEA = array.array( # noqa: N806\n", 204 | " \"f\", header_bytes[160 : 160 + 4 * 4]\n", 205 | " )\n", 206 | " (ZVAR,) = array.array(\"d\", header_bytes[176 : 176 + 8]) # noqa: N806\n", 207 | " (PRCS,) = array.array(\"i\", header_bytes[184 : 184 + 4]) # noqa: N806\n", 208 | " header.update(\n", 209 | " {\n", 210 | " \"map_projection\": PROJ,\n", 211 | " \"units_x\": UNITX,\n", 212 | " \"units_y\": UNITY,\n", 213 | " \"units_z\": UNITZ,\n", 214 | " \"n_valid_points\": NVPTS,\n", 215 | " \"grid_min\": IZMIN,\n", 216 | " \"grid_max\": IZMAX,\n", 217 | " \"grid_median\": IZMED,\n", 218 | " \"grid_mean\": IZMEA,\n", 219 | " \"grid_variance\": ZVAR,\n", 220 | " \"process_flag\": PRCS,\n", 221 | " }\n", 222 | " )\n", 223 | " return header\n", 224 | "\n", 225 | "\n", 226 | "def _check_ordering(ordering):\n", 227 | " \"\"\"\n", 228 | " Check if the ordering value is within the ones we are supporting\n", 229 | " \"\"\"\n", 230 | " if ordering not in (-1, 1):\n", 231 | " raise NotImplementedError(\n", 232 | " f\"Found an ordering (a.k.a as KX) equal to '{ordering}'. \"\n", 233 | " + \"Only orderings equal to 1 and -1 are supported.\"\n", 234 | " )\n", 235 | "\n", 236 | "\n", 237 | "def _check_sign_flag(sign_flag):\n", 238 | " \"\"\"\n", 239 | " Check if sign_flag value is within the ones we are supporting\n", 240 | " \"\"\"\n", 241 | " if sign_flag == 3:\n", 242 | " raise NotImplementedError(\n", 243 | " \"Reading .grd files with colour grids is not currenty supported.\"\n", 244 | " )\n", 245 | "\n", 246 | "\n", 247 | "def _get_data_type(n_bytes_per_element, sign_flag):\n", 248 | " \"\"\"\n", 249 | " Return the data type for the grid values\n", 250 | " References\n", 251 | " ----------\n", 252 | " https://docs.python.org/3/library/array.html\n", 253 | " \"\"\"\n", 254 | " # Check if number of bytes per element is valid\n", 255 | " if n_bytes_per_element not in VALID_ELEMENT_SIZES:\n", 256 | " raise NotImplementedError(\n", 257 | " \"Found a 'Grid data element size' (a.k.a. 'ES') value \"\n", 258 | " + f\"of '{n_bytes_per_element}'. \"\n", 259 | " \"Only values equal to 1, 2, 4 and 8 are valid, \"\n", 260 | " + \"along with their compressed counterparts (1025, 1026, 1028, 1032).\"\n", 261 | " )\n", 262 | " # Shift the n_bytes_per_element in case of compressed grids\n", 263 | " if n_bytes_per_element > 1024:\n", 264 | " n_bytes_per_element -= 1024\n", 265 | " # Determine the data type of the grid elements\n", 266 | " if n_bytes_per_element == 1:\n", 267 | " if sign_flag == 0:\n", 268 | " data_type = \"B\" # unsigned char\n", 269 | " elif sign_flag == 1:\n", 270 | " data_type = \"b\" # signed char\n", 271 | " elif n_bytes_per_element == 2:\n", 272 | " if sign_flag == 0:\n", 273 | " data_type = \"H\" # unsigned short\n", 274 | " elif sign_flag == 1:\n", 275 | " data_type = \"h\" # signed short\n", 276 | " elif n_bytes_per_element == 4:\n", 277 | " if sign_flag == 0:\n", 278 | " data_type = \"I\" # unsigned int\n", 279 | " elif sign_flag == 1:\n", 280 | " data_type = \"i\" # signed int\n", 281 | " elif sign_flag == 2:\n", 282 | " data_type = \"f\" # float\n", 283 | " elif n_bytes_per_element == 8:\n", 284 | " data_type = \"d\"\n", 285 | " return data_type\n", 286 | "\n", 287 | "\n", 288 | "def _remove_dummies(grid, data_type):\n", 289 | " \"\"\"\n", 290 | " Replace dummy values for NaNs\n", 291 | " \"\"\"\n", 292 | " # Create dictionary with dummy value for each data type\n", 293 | " if data_type in (\"f\", \"d\"):\n", 294 | " grid[grid <= DUMMIES[data_type]] = np.nan\n", 295 | " return grid\n", 296 | " grid[grid == DUMMIES[data_type]] = np.nan\n", 297 | " return grid\n", 298 | "\n", 299 | "\n", 300 | "def _decompress_grid(grid_compressed):\n", 301 | " \"\"\"\n", 302 | " Decompress the grid using gzip\n", 303 | " Even if the header specifies that the grid is compressed using a LZRW1\n", 304 | " algorithm, it's using gzip instead. The first two 4 bytes sequences\n", 305 | " correspond to the compression signature and\n", 306 | " to the compression type. We are going to ignore those and start reading\n", 307 | " from the number of blocks (offset 8).\n", 308 | " Parameters\n", 309 | " ----------\n", 310 | " grid_compressed : bytes\n", 311 | " Sequence of bytes corresponding to the compressed grid. They should be\n", 312 | " every byte starting from offset 512 of the GRD file until its end.\n", 313 | " Returns\n", 314 | " -------\n", 315 | " grid : bytes\n", 316 | " Uncompressed version of the ``grid_compressed`` parameter.\n", 317 | " \"\"\"\n", 318 | " # Number of blocks\n", 319 | " (n_blocks,) = array.array(\"i\", grid_compressed[8 : 8 + 4])\n", 320 | " # Number of vectors per block\n", 321 | " (vectors_per_block,) = array.array(\"i\", grid_compressed[12 : 12 + 4])\n", 322 | " # File offset from start of every block\n", 323 | " block_offsets = array.array(\"q\", grid_compressed[16 : 16 + n_blocks * 8])\n", 324 | " # Compressed size of every block\n", 325 | " compressed_block_sizes = array.array(\n", 326 | " \"i\",\n", 327 | " grid_compressed[16 + n_blocks * 8 : 16 + n_blocks * 8 + n_blocks * 4],\n", 328 | " )\n", 329 | " # Combine grid\n", 330 | " grid = b\"\"\n", 331 | " # Read each block\n", 332 | " for i in range(n_blocks):\n", 333 | " # Define the start and end offsets for each compressed blocks\n", 334 | " # We need to remove the 512 to account for the missing header.\n", 335 | " # There is an unexplained 16 byte header that we also need to remove.\n", 336 | " start_offset = block_offsets[i] - 512 + 16\n", 337 | " end_offset = compressed_block_sizes[i] + block_offsets[i] - 512\n", 338 | " # Decompress the block\n", 339 | " grid_sub = zlib.decompress(\n", 340 | " grid_compressed[start_offset:end_offset],\n", 341 | " bufsize=zlib.DEF_BUF_SIZE,\n", 342 | " )\n", 343 | " # Add it to the running grid\n", 344 | " grid += grid_sub\n", 345 | " return grid\n", 346 | "\n", 347 | "\n", 348 | "def _build_coordinates(west, south, shape, spacing):\n", 349 | " \"\"\"\n", 350 | " Create the coordinates for the grid\n", 351 | " Generates 1d arrays for the easting and northing coordinates of the grid.\n", 352 | " Assumes unrotated grids.\n", 353 | " Parameters\n", 354 | " ----------\n", 355 | " west : float\n", 356 | " Westernmost coordinate of the grid.\n", 357 | " south : float\n", 358 | " Southernmost coordinate of the grid.\n", 359 | " shape : tuple\n", 360 | " Tuple of ints containing the number of elements along each direction in\n", 361 | " the following order: ``n_northing``, ``n_easting``\n", 362 | " spacing : tuple\n", 363 | " Tuple of floats containing the distance between adjacent grid elements\n", 364 | " along each direction in the following order: ``s_northing``,\n", 365 | " ``s_easting``.\n", 366 | " Returns\n", 367 | " -------\n", 368 | " easting : 1d-array\n", 369 | " Array containing the values of the easting coordinates of the grid.\n", 370 | " northing : 1d-array\n", 371 | " Array containing the values of the northing coordinates of the grid.\n", 372 | " \"\"\"\n", 373 | " easting = np.linspace(west, west + spacing[1] * (shape[1] - 1), shape[1])\n", 374 | " northing = np.linspace(south, south + spacing[0] * (shape[0] - 1), shape[0])\n", 375 | " return easting, northing\n", 376 | "\n", 377 | "\n", 378 | "def _build_rotated_coordinates(west, south, shape, spacing, rotation_deg):\n", 379 | " \"\"\"\n", 380 | " Create the coordinates for a rotated grid\n", 381 | " Generates 2d arrays for the easting and northing coordinates of the grid.\n", 382 | " Assumes rotated grids.\n", 383 | " Parameters\n", 384 | " ----------\n", 385 | " west : float\n", 386 | " Westernmost coordinate of the grid.\n", 387 | " south : float\n", 388 | " Southernmost coordinate of the grid.\n", 389 | " shape : tuple\n", 390 | " Tuple of ints containing the number of elements along each unrotated\n", 391 | " direction in the following order: ``n_y``, ``n_x``\n", 392 | " spacing : tuple\n", 393 | " Tuple of floats containing the distance between adjacent grid elements\n", 394 | " along each unrotated direction in the following order: ``spacing_y``,\n", 395 | " ``spacing_x``.\n", 396 | " Returns\n", 397 | " -------\n", 398 | " easting : 2d-array\n", 399 | " Array containing the values of the easting coordinates of the grid.\n", 400 | " northing : 2d-array\n", 401 | " Array containing the values of the northing coordinates of the grid.\n", 402 | " Notes\n", 403 | " -----\n", 404 | " The ``x`` and ``y`` coordinates are the abscissa and the ordinate of the\n", 405 | " unrotated grid before the translation, respectively.\n", 406 | " \"\"\"\n", 407 | " # Define the grid coordinates before the rotation\n", 408 | " x = np.linspace(0, spacing[1] * (shape[1] - 1), shape[1])\n", 409 | " y = np.linspace(0, spacing[0] * (shape[0] - 1), shape[0])\n", 410 | " # Compute a meshgrid\n", 411 | " x, y = np.meshgrid(x, y)\n", 412 | " # Rotate and shift to get easting and northing\n", 413 | " rotation_rad = np.radians(rotation_deg)\n", 414 | " cos, sin = np.cos(rotation_rad), np.sin(rotation_rad)\n", 415 | " easting = west + x * cos - y * sin\n", 416 | " northing = south + x * sin + y * cos\n", 417 | " return easting, northing" 418 | ] 419 | }, 420 | { 421 | "cell_type": "markdown", 422 | "id": "4124ca77", 423 | "metadata": { 424 | "id": "4124ca77" 425 | }, 426 | "source": [ 427 | "### Code to convert extracted xarray to geotiff format file" 428 | ] 429 | }, 430 | { 431 | "cell_type": "code", 432 | "execution_count": null, 433 | "id": "d786c4c5", 434 | "metadata": { 435 | "id": "d786c4c5" 436 | }, 437 | "outputs": [], 438 | "source": [ 439 | "import rasterio\n", 440 | "from rasterio.transform import from_origin\n", 441 | "\n", 442 | "def write_geotiff(out_path,grid_uc,epsg):\n", 443 | " \"\"\"\n", 444 | " write out grid in geotiff format\n", 445 | " Parameters\n", 446 | " ----------\n", 447 | " out_path : string\n", 448 | " file path and name to write to\n", 449 | " grid_uc : xarray\n", 450 | " grid and header info \n", 451 | " epsg: string\n", 452 | " user defined epsg\n", 453 | " \"\"\" \n", 454 | " transform = from_origin(grid_uc.attrs['x_origin']-(grid_uc.attrs['spacing_e']/2), grid_uc.attrs['y_origin']-(grid_uc.attrs['spacing_v']/2), grid_uc.attrs['spacing_e'], -grid_uc.attrs['spacing_v'])\n", 455 | "\n", 456 | " new_dataset = rasterio.open(\n", 457 | " out_path,\n", 458 | " \"w\",\n", 459 | " driver=\"GTiff\",\n", 460 | " height=grid_uc.attrs['shape_v'],\n", 461 | " width=grid_uc.attrs['shape_e'],\n", 462 | " count=1,\n", 463 | " dtype=rasterio.float32,\n", 464 | " crs='epsg:'+epsg,#\"+proj=longlat\",\n", 465 | " transform=transform,\n", 466 | " nodata=-1e8,\n", 467 | " )\n", 468 | "\n", 469 | " new_dataset.write(grid_uc.values, 1)\n", 470 | " new_dataset.close()\n", 471 | " print(\"dtm geotif saved as\", out_path)\n" 472 | ] 473 | }, 474 | { 475 | "cell_type": "markdown", 476 | "id": "360df4ac", 477 | "metadata": { 478 | "id": "360df4ac" 479 | }, 480 | "source": [ 481 | "### Example code to convert uncompressed grid" 482 | ] 483 | }, 484 | { 485 | "cell_type": "code", 486 | "execution_count": null, 487 | "id": "f2934ef2", 488 | "metadata": { 489 | "id": "f2934ef2" 490 | }, 491 | "outputs": [], 492 | "source": [ 493 | "filename='./test_data/test_uncompressed.grd' # uncompressed example\n", 494 | "grid_uc=load_oasis_montaj_grid(filename)\n", 495 | "write_geotiff('./test1.tif',grid_uc,'4326')" 496 | ] 497 | }, 498 | { 499 | "cell_type": "markdown", 500 | "id": "24f4a5a9", 501 | "metadata": { 502 | "id": "24f4a5a9" 503 | }, 504 | "source": [ 505 | "### Example code to convert compressed grid" 506 | ] 507 | }, 508 | { 509 | "cell_type": "code", 510 | "execution_count": null, 511 | "id": "e53f5bfa", 512 | "metadata": { 513 | "id": "e53f5bfa" 514 | }, 515 | "outputs": [], 516 | "source": [ 517 | "filename='./test_data/test_compressed.grd' # compressed example\n", 518 | "grid_c=load_oasis_montaj_grid(filename)\n", 519 | "write_geotiff('./test2.tif',grid_c,'4326')" 520 | ] 521 | }, 522 | { 523 | "cell_type": "markdown", 524 | "id": "f8b5f1c3", 525 | "metadata": { 526 | "id": "f8b5f1c3" 527 | }, 528 | "source": [ 529 | "### Example code to parse XML to get epsg" 530 | ] 531 | }, 532 | { 533 | "cell_type": "code", 534 | "execution_count": null, 535 | "id": "70b37221", 536 | "metadata": { 537 | "id": "70b37221" 538 | }, 539 | "outputs": [], 540 | "source": [ 541 | "# Suggested code to parse xml to get CRS from\n", 542 | "# Santiago Soler \n", 543 | "# https://github.com/fatiando/harmonica/pull/348#issuecomment-1327811755\n", 544 | "\n", 545 | "\n", 546 | "def extract_proj_str(fname):\n", 547 | " with open(fname, \"r\") as f:\n", 548 | " for line in f:\n", 549 | " if \"projection=\" in line:\n", 550 | " # print lines with projection info\n", 551 | " print(line)\n", 552 | "\n", 553 | " # extract projection string\n", 554 | " proj = line.split('projection=\"')[1].split('\" ')[0]\n", 555 | " print(proj)\n", 556 | "\n", 557 | " # remove non-alphanumeric characters if present\n", 558 | " if proj.isalnum() is False:\n", 559 | " proj = ''.join(filter(str.isalnum, proj))\n", 560 | "\n", 561 | " assert proj.isalnum\n", 562 | " try:\n", 563 | " proj\n", 564 | " except:\n", 565 | " raise NameError(\"string 'projection=' not found in file.\")\n", 566 | " proj = None\n", 567 | "\n", 568 | " return proj\n", 569 | "\n", 570 | "proj = extract_proj_str(filename+\".xml\")" 571 | ] 572 | } 573 | ], 574 | "metadata": { 575 | "kernelspec": { 576 | "display_name": "Python 3 (ipykernel)", 577 | "language": "python", 578 | "name": "python3" 579 | }, 580 | "language_info": { 581 | "codemirror_mode": { 582 | "name": "ipython", 583 | "version": 3 584 | }, 585 | "file_extension": ".py", 586 | "mimetype": "text/x-python", 587 | "name": "python", 588 | "nbconvert_exporter": "python", 589 | "pygments_lexer": "ipython3", 590 | "version": "3.9.15" 591 | }, 592 | "colab": { 593 | "provenance": [], 594 | "include_colab_link": true 595 | } 596 | }, 597 | "nbformat": 4, 598 | "nbformat_minor": 5 599 | } -------------------------------------------------------------------------------- /grd2geotiff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # command line version of convertor hacked togetehr by M. Jessell 5 | # Usage python grd2geotiff.py input_grid_path epsg 6 | 7 | # Copyright (c) 2018 The Harmonica Developers. 8 | # Distributed under the terms of the BSD 3-Clause License. 9 | # SPDX-License-Identifier: BSD-3-Clause 10 | # 11 | # This code is modefiied from the Fatiando a Terra project (https://www.fatiando.org) 12 | # 13 | # The support for compressed grids was inspired by the code in 14 | # https://github.com/Loop3D/geosoft_grid copyrighted by Loop3D and released 15 | # under the MIT License. 16 | """ 17 | Function to read Oasis Montaj© .grd file 18 | """ 19 | 20 | import array 21 | import zlib 22 | 23 | import numpy as np 24 | import xarray as xr 25 | 26 | # Define the valid element sizes (ES variable) for GRD files 27 | # (values > 1024 correspond to compressed versions of the grid) 28 | VALID_ELEMENT_SIZES = (1, 2, 4, 8, 1024 + 1, 1024 + 2, 1024 + 4, 1024 + 8) 29 | # Define dummy values for each data type 30 | DUMMIES = { 31 | "b": -127, 32 | "B": 255, 33 | "h": -32767, 34 | "H": 65535, 35 | "i": -2147483647, 36 | "I": 4294967295, 37 | "f": -1e32, 38 | "d": -1e32, 39 | } 40 | 41 | 42 | def load_oasis_montaj_grid(fname): 43 | """ 44 | Reads gridded data from an Oasis Montaj© .grd file. 45 | The version 2 of the Geosoft© Grid File Format (GRD) stores gridded 46 | products in binary data. This function can read those files and parse the 47 | information in the header. It returns the data in 48 | a :class:`xarray.DataArray` for convenience. 49 | .. warning:: 50 | This function has not been tested against a wide range of GRD files. 51 | This could lead to incorrect readings of the stored data. Please report 52 | any unwanted behaviour by opening an issue in Harmonica: 53 | https://github.com/fatiando/harmonica/issues 54 | .. important:: 55 | This function only supports reading GRD files using the **version 2** 56 | of the Geosoft© Grid File Format. 57 | .. important:: 58 | This function is not supporting orderings different than ±1, 59 | or colour grids. 60 | Parameters 61 | ---------- 62 | fname : string or file-like object 63 | Path to the .grd file. 64 | Returns 65 | ------- 66 | grid : :class:`xarray.DataArray` 67 | :class:`xarray.DataArray` containing the grid, its coordinates and 68 | header information. 69 | 70 | References 71 | ---------- 72 | https://help.seequent.com/Oasis-montaj/9.9/en/Content/ss/glossary/grid_file_format__grd.htm 73 | https://github.com/Loop3D/geosoft_grid 74 | """ 75 | # Read the header and the grid array 76 | with open(fname, "rb") as grd_file: 77 | # Read the header (first 512 bytes) 78 | header = _read_header(grd_file.read(512)) 79 | # Check for valid flags 80 | _check_ordering(header["ordering"]) 81 | _check_sign_flag(header["sign_flag"]) 82 | # Get data type for the grid elements 83 | data_type = _get_data_type(header["n_bytes_per_element"], header["sign_flag"]) 84 | # Read grid 85 | grid = grd_file.read() 86 | # Decompress grid if needed 87 | if header["n_bytes_per_element"] > 1024: 88 | grid = _decompress_grid(grid) 89 | # Load the grid values as an array with the proper data_type 90 | grid = array.array(data_type, grid) 91 | # Convert to numpy array as float64 92 | grid = np.array(grid, dtype=np.float64) 93 | # Remove dummy values 94 | grid = _remove_dummies(grid, data_type) 95 | # Scale the grid 96 | grid = np.array(grid / header["data_factor"] + header["base_value"]) 97 | # Reshape the grid based on the ordering 98 | if header["ordering"] == 1: 99 | order = "C" 100 | shape = (header["shape_v"], header["shape_e"]) 101 | spacing = (header["spacing_v"], header["spacing_e"]) 102 | elif header["ordering"] == -1: 103 | order = "F" 104 | shape = (header["shape_e"], header["shape_v"]) 105 | spacing = (header["spacing_e"], header["spacing_v"]) 106 | grid = grid.reshape(shape, order=order) 107 | # Build coords 108 | if header["rotation"] == 0: 109 | easting, northing = _build_coordinates( 110 | header["x_origin"], header["y_origin"], shape, spacing 111 | ) 112 | dims = ("northing", "easting") 113 | coords = {"easting": easting, "northing": northing} 114 | else: 115 | easting, northing = _build_rotated_coordinates( 116 | header["x_origin"], header["y_origin"], shape, spacing, header["rotation"] 117 | ) 118 | dims = ("y", "x") 119 | coords = {"easting": (dims, easting), "northing": (dims, northing)} 120 | # Build an xarray.DataArray for the grid 121 | grid = xr.DataArray( 122 | grid, 123 | coords=coords, 124 | dims=dims, 125 | attrs=header, 126 | ) 127 | return grid 128 | 129 | 130 | def _read_header(header_bytes): 131 | """ 132 | Read GRD file header 133 | Parameters 134 | ---------- 135 | header_bytes : byte 136 | A sequence of 512 bytes containing the header of a 137 | GRD file. 138 | Returns 139 | ------- 140 | header : dict 141 | Dictionary containing the information present in the 142 | header. 143 | Notes 144 | ----- 145 | The GRD header consists in 512 contiguous bytes. 146 | It's divided in four sections: 147 | * Data Storage 148 | * Geographic Information 149 | * Data (Z) Scaling 150 | * Undefined Application Parameters 151 | """ 152 | header = {} 153 | # Read data storage 154 | ES, SF, NE, NV, KX = array.array("i", header_bytes[0 : 5 * 4]) # noqa: N806 155 | header.update( 156 | { 157 | "n_bytes_per_element": ES, 158 | "sign_flag": SF, 159 | "shape_e": NE, 160 | "shape_v": NV, 161 | "ordering": KX, 162 | } 163 | ) 164 | # Read geographic info 165 | DE, DV, X0, Y0, ROT = array.array("d", header_bytes[20 : 20 + 5 * 8]) # noqa: N806 166 | header.update( 167 | { 168 | "spacing_e": DE, 169 | "spacing_v": DV, 170 | "x_origin": X0, 171 | "y_origin": Y0, 172 | "rotation": ROT, 173 | } 174 | ) 175 | # Read data scaling 176 | ZBASE, ZMULT = array.array("d", header_bytes[60 : 60 + 2 * 8]) # noqa: N806 177 | header.update( 178 | { 179 | "base_value": ZBASE, 180 | "data_factor": ZMULT, 181 | } 182 | ) 183 | # Read optional parameters 184 | # (ignore map LABEL and MAPNO) 185 | PROJ, UNITX, UNITY, UNITZ, NVPTS = array.array( # noqa: N806 186 | "i", header_bytes[140 : 140 + 5 * 4] 187 | ) 188 | IZMIN, IZMAX, IZMED, IZMEA = array.array( # noqa: N806 189 | "f", header_bytes[160 : 160 + 4 * 4] 190 | ) 191 | (ZVAR,) = array.array("d", header_bytes[176 : 176 + 8]) # noqa: N806 192 | (PRCS,) = array.array("i", header_bytes[184 : 184 + 4]) # noqa: N806 193 | header.update( 194 | { 195 | "map_projection": PROJ, 196 | "units_x": UNITX, 197 | "units_y": UNITY, 198 | "units_z": UNITZ, 199 | "n_valid_points": NVPTS, 200 | "grid_min": IZMIN, 201 | "grid_max": IZMAX, 202 | "grid_median": IZMED, 203 | "grid_mean": IZMEA, 204 | "grid_variance": ZVAR, 205 | "process_flag": PRCS, 206 | } 207 | ) 208 | return header 209 | 210 | 211 | def _check_ordering(ordering): 212 | """ 213 | Check if the ordering value is within the ones we are supporting 214 | """ 215 | if ordering not in (-1, 1): 216 | raise NotImplementedError( 217 | f"Found an ordering (a.k.a as KX) equal to '{ordering}'. " 218 | + "Only orderings equal to 1 and -1 are supported." 219 | ) 220 | 221 | 222 | def _check_sign_flag(sign_flag): 223 | """ 224 | Check if sign_flag value is within the ones we are supporting 225 | """ 226 | if sign_flag == 3: 227 | raise NotImplementedError( 228 | "Reading .grd files with colour grids is not currenty supported." 229 | ) 230 | 231 | 232 | def _get_data_type(n_bytes_per_element, sign_flag): 233 | """ 234 | Return the data type for the grid values 235 | References 236 | ---------- 237 | https://docs.python.org/3/library/array.html 238 | """ 239 | # Check if number of bytes per element is valid 240 | if n_bytes_per_element not in VALID_ELEMENT_SIZES: 241 | raise NotImplementedError( 242 | "Found a 'Grid data element size' (a.k.a. 'ES') value " 243 | + f"of '{n_bytes_per_element}'. " 244 | "Only values equal to 1, 2, 4 and 8 are valid, " 245 | + "along with their compressed counterparts (1025, 1026, 1028, 1032)." 246 | ) 247 | # Shift the n_bytes_per_element in case of compressed grids 248 | if n_bytes_per_element > 1024: 249 | n_bytes_per_element -= 1024 250 | # Determine the data type of the grid elements 251 | if n_bytes_per_element == 1: 252 | if sign_flag == 0: 253 | data_type = "B" # unsigned char 254 | elif sign_flag == 1: 255 | data_type = "b" # signed char 256 | elif n_bytes_per_element == 2: 257 | if sign_flag == 0: 258 | data_type = "H" # unsigned short 259 | elif sign_flag == 1: 260 | data_type = "h" # signed short 261 | elif n_bytes_per_element == 4: 262 | if sign_flag == 0: 263 | data_type = "I" # unsigned int 264 | elif sign_flag == 1: 265 | data_type = "i" # signed int 266 | elif sign_flag == 2: 267 | data_type = "f" # float 268 | elif n_bytes_per_element == 8: 269 | data_type = "d" 270 | return data_type 271 | 272 | 273 | def _remove_dummies(grid, data_type): 274 | """ 275 | Replace dummy values for NaNs 276 | """ 277 | # Create dictionary with dummy value for each data type 278 | if data_type in ("f", "d"): 279 | grid[grid <= DUMMIES[data_type]] = np.nan 280 | return grid 281 | grid[grid == DUMMIES[data_type]] = np.nan 282 | return grid 283 | 284 | 285 | def _decompress_grid(grid_compressed): 286 | """ 287 | Decompress the grid using gzip 288 | Even if the header specifies that the grid is compressed using a LZRW1 289 | algorithm, it's using gzip instead. The first two 4 bytes sequences 290 | correspond to the compression signature and 291 | to the compression type. We are going to ignore those and start reading 292 | from the number of blocks (offset 8). 293 | Parameters 294 | ---------- 295 | grid_compressed : bytes 296 | Sequence of bytes corresponding to the compressed grid. They should be 297 | every byte starting from offset 512 of the GRD file until its end. 298 | Returns 299 | ------- 300 | grid : bytes 301 | Uncompressed version of the ``grid_compressed`` parameter. 302 | """ 303 | # Number of blocks 304 | (n_blocks,) = array.array("i", grid_compressed[8 : 8 + 4]) 305 | # Number of vectors per block 306 | (vectors_per_block,) = array.array("i", grid_compressed[12 : 12 + 4]) 307 | # File offset from start of every block 308 | block_offsets = array.array("q", grid_compressed[16 : 16 + n_blocks * 8]) 309 | # Compressed size of every block 310 | compressed_block_sizes = array.array( 311 | "i", 312 | grid_compressed[16 + n_blocks * 8 : 16 + n_blocks * 8 + n_blocks * 4], 313 | ) 314 | # Combine grid 315 | grid = b"" 316 | # Read each block 317 | for i in range(n_blocks): 318 | # Define the start and end offsets for each compressed blocks 319 | # We need to remove the 512 to account for the missing header. 320 | # There is an unexplained 16 byte header that we also need to remove. 321 | start_offset = block_offsets[i] - 512 + 16 322 | end_offset = compressed_block_sizes[i] + block_offsets[i] - 512 323 | # Decompress the block 324 | grid_sub = zlib.decompress( 325 | grid_compressed[start_offset:end_offset], 326 | bufsize=zlib.DEF_BUF_SIZE, 327 | ) 328 | # Add it to the running grid 329 | grid += grid_sub 330 | return grid 331 | 332 | 333 | def _build_coordinates(west, south, shape, spacing): 334 | """ 335 | Create the coordinates for the grid 336 | Generates 1d arrays for the easting and northing coordinates of the grid. 337 | Assumes unrotated grids. 338 | Parameters 339 | ---------- 340 | west : float 341 | Westernmost coordinate of the grid. 342 | south : float 343 | Southernmost coordinate of the grid. 344 | shape : tuple 345 | Tuple of ints containing the number of elements along each direction in 346 | the following order: ``n_northing``, ``n_easting`` 347 | spacing : tuple 348 | Tuple of floats containing the distance between adjacent grid elements 349 | along each direction in the following order: ``s_northing``, 350 | ``s_easting``. 351 | Returns 352 | ------- 353 | easting : 1d-array 354 | Array containing the values of the easting coordinates of the grid. 355 | northing : 1d-array 356 | Array containing the values of the northing coordinates of the grid. 357 | """ 358 | easting = np.linspace(west, west + spacing[1] * (shape[1] - 1), shape[1]) 359 | northing = np.linspace(south, south + spacing[0] * (shape[0] - 1), shape[0]) 360 | return easting, northing 361 | 362 | 363 | def _build_rotated_coordinates(west, south, shape, spacing, rotation_deg): 364 | """ 365 | Create the coordinates for a rotated grid 366 | Generates 2d arrays for the easting and northing coordinates of the grid. 367 | Assumes rotated grids. 368 | Parameters 369 | ---------- 370 | west : float 371 | Westernmost coordinate of the grid. 372 | south : float 373 | Southernmost coordinate of the grid. 374 | shape : tuple 375 | Tuple of ints containing the number of elements along each unrotated 376 | direction in the following order: ``n_y``, ``n_x`` 377 | spacing : tuple 378 | Tuple of floats containing the distance between adjacent grid elements 379 | along each unrotated direction in the following order: ``spacing_y``, 380 | ``spacing_x``. 381 | Returns 382 | ------- 383 | easting : 2d-array 384 | Array containing the values of the easting coordinates of the grid. 385 | northing : 2d-array 386 | Array containing the values of the northing coordinates of the grid. 387 | Notes 388 | ----- 389 | The ``x`` and ``y`` coordinates are the abscissa and the ordinate of the 390 | unrotated grid before the translation, respectively. 391 | """ 392 | # Define the grid coordinates before the rotation 393 | x = np.linspace(0, spacing[1] * (shape[1] - 1), shape[1]) 394 | y = np.linspace(0, spacing[0] * (shape[0] - 1), shape[0]) 395 | # Compute a meshgrid 396 | x, y = np.meshgrid(x, y) 397 | # Rotate and shift to get easting and northing 398 | rotation_rad = np.radians(rotation_deg) 399 | cos, sin = np.cos(rotation_rad), np.sin(rotation_rad) 400 | easting = west + x * cos - y * sin 401 | northing = south + x * sin + y * cos 402 | return easting, northing 403 | 404 | 405 | # ### Code to convert extracted xarray to geotiff format file 406 | 407 | # In[ ]: 408 | 409 | 410 | import rasterio 411 | from rasterio.transform import from_origin 412 | 413 | def write_geotiff(out_path,grid_uc,epsg): 414 | """ 415 | write out grid in geotiff format 416 | Parameters 417 | ---------- 418 | out_path : string 419 | file path and name to write to 420 | grid_uc : xarray 421 | grid and header info 422 | epsg: string 423 | user defined epsg 424 | """ 425 | transform = from_origin(grid_uc.attrs['x_origin']-(grid_uc.attrs['spacing_e']/2), grid_uc.attrs['y_origin']-(grid_uc.attrs['spacing_v']/2), grid_uc.attrs['spacing_e'], -grid_uc.attrs['spacing_v']) 426 | 427 | new_dataset = rasterio.open( 428 | out_path, 429 | "w", 430 | driver="GTiff", 431 | height=grid_uc.attrs['shape_v'], 432 | width=grid_uc.attrs['shape_e'], 433 | count=1, 434 | dtype=rasterio.float32, 435 | crs='epsg:'+epsg,#"+proj=longlat", 436 | transform=transform, 437 | nodata=-1e8, 438 | ) 439 | 440 | new_dataset.write(grid_uc.values, 1) 441 | new_dataset.close() 442 | print("dtm geotif saved as", out_path) 443 | 444 | 445 | # ### Example code to convert uncompressed grid 446 | # Usage python grd2geotiff.py input_grid_path epsg 447 | 448 | 449 | import sys 450 | 451 | if(len(sys.argv)<3): 452 | print('usage: python grd2geotiff.py input_grid_path [string] epsg [int]') 453 | print(' e.g. python grd2geotiff.py test_data/test_compressed.grd 4326') 454 | print() 455 | print('output: input filename root with tif suffix') 456 | exit() 457 | filename=str(sys.argv[1]) 458 | filename_out=filename.replace('.grd','.tif') 459 | epsg=str(sys.argv[2]) 460 | 461 | grid_uc=load_oasis_montaj_grid(filename) 462 | write_geotiff(filename_out,grid_uc,epsg) 463 | 464 | 465 | 466 | 467 | 468 | # Suggested code to parse xml to get CRS from 469 | # Santiago Soler 470 | # https://github.com/fatiando/harmonica/pull/348#issuecomment-1327811755 471 | 472 | 473 | def extract_proj_str(fname): 474 | with open(fname, "r") as f: 475 | for line in f: 476 | if "projection=" in line: 477 | # print lines with projection info 478 | print(line) 479 | 480 | # extract projection string 481 | proj = line.split('projection="')[1].split('" ')[0] 482 | print(proj) 483 | 484 | # remove non-alphanumeric characters if present 485 | if proj.isalnum() is False: 486 | proj = ''.join(filter(str.isalnum, proj)) 487 | 488 | assert proj.isalnum 489 | try: 490 | proj 491 | except: 492 | raise NameError("string 'projection=' not found in file.") 493 | proj = None 494 | 495 | return proj 496 | 497 | #proj = extract_proj_str(filename+".xml") 498 | 499 | -------------------------------------------------------------------------------- /test_data/test_compressed.grd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/geosoft_grid/10cf94dc3512f40ad13ed6d009b9247162631f47/test_data/test_compressed.grd -------------------------------------------------------------------------------- /test_data/test_compressed.grd.gi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/geosoft_grid/10cf94dc3512f40ad13ed6d009b9247162631f47/test_data/test_compressed.grd.gi -------------------------------------------------------------------------------- /test_data/test_compressed.grd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test_uncompressed 6 | Geosoft Grid 7 | d:\dropbox\loop_minex\geosoft_grid\test.gpf 8 | DEP35607\mark 9 | 2022-11-10 10 | DEP35607\mark 11 | 2022-11-10 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 2022-11-10T09:29:56 60 | 61 | 62 | 63 | 64 | DEP35607\mark 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | test_uncompressed 119 | 120 | 121 | d:\dropbox\loop_minex\geosoft_grid\test.gpf 122 | 123 | 124 | 125 | 126 | 2022-11-10 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | Geosoft Grid 136 | 137 | 138 | 139 | 140 | 141 | 142 | DEP35607\mark 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -40.46822 152 | 153 | 154 | -37.66952 155 | 156 | 157 | -8.45502 158 | 159 | 160 | -6.00002 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | DEP35607\mark 172 | 173 | 174 | 175 | 176 | 2022-11-10 177 | 178 | 179 | 180 | 181 | 182 | 183 | ESRI 184 | 185 | 186 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433],AUTHORITY["EPSG",4326]] 187 | 188 | 189 | 1 190 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /test_data/test_uncompressed.grd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/geosoft_grid/10cf94dc3512f40ad13ed6d009b9247162631f47/test_data/test_uncompressed.grd -------------------------------------------------------------------------------- /test_data/test_uncompressed.grd.gi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/geosoft_grid/10cf94dc3512f40ad13ed6d009b9247162631f47/test_data/test_uncompressed.grd.gi -------------------------------------------------------------------------------- /test_data/test_uncompressed.grd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test_uncompressed 6 | Geosoft Grid 7 | d:\dropbox\loop_minex\geosoft_grid\test.gpf 8 | DEP35607\mark 9 | 2022-11-10 10 | DEP35607\mark 11 | 2022-11-10 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 2022-11-10T09:29:56 60 | 61 | 62 | 63 | 64 | DEP35607\mark 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | test_uncompressed 119 | 120 | 121 | d:\dropbox\loop_minex\geosoft_grid\test.gpf 122 | 123 | 124 | 125 | 126 | 2022-11-10 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | Geosoft Grid 136 | 137 | 138 | 139 | 140 | 141 | 142 | DEP35607\mark 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -40.46822 152 | 153 | 154 | -37.66952 155 | 156 | 157 | -8.45502 158 | 159 | 160 | -6.00002 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | DEP35607\mark 172 | 173 | 174 | 175 | 176 | 2022-11-10 177 | 178 | 179 | 180 | 181 | 182 | 183 | ESRI 184 | 185 | 186 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433],AUTHORITY["EPSG",4326]] 187 | 188 | 189 | 1 190 | 191 | 192 | 193 | 194 | 195 | --------------------------------------------------------------------------------