├── .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 | "
"
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 |
--------------------------------------------------------------------------------