Repository navigation
GMT_IMAGE: Implement the GMT_IMAGE.to_dataarray method for 3-band images #3128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 40 commits
bcf43f0
a052a1a
56a6d65
f71e79c
4cce4a2
e02b650
f3d4b1f
4390136
5f25669
a3c6c14
5888e10
798e658
3dbf2f2
6b860bf
0bf9368
7d437be
268e34e
86765e1
86f3ffa
cc28247
1e2c973
734dc28
919dc00
b1eacf1
aa4fdc9
c87a3ec
a20d8a2
926427b
c73328e
fb97daa
15b8d53
8433e78
3e3a6f3
12ef40a
f64fbb8
4f2ae48
d49afed
f70bec0
2fd13fb
9972ba1
62f0ce0
f715aee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,7 +5,9 @@ | |||||||||||||
| import ctypes as ctp | ||||||||||||||
| from typing import ClassVar | ||||||||||||||
|
|
||||||||||||||
| from pygmt.datatypes.grid import _GMT_GRID_HEADER | ||||||||||||||
| import numpy as np | ||||||||||||||
| import xarray as xr | ||||||||||||||
| from pygmt.datatypes.header import _GMT_GRID_HEADER | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class _GMT_IMAGE(ctp.Structure): # noqa: N801 | ||||||||||||||
|
|
@@ -63,6 +65,8 @@ class _GMT_IMAGE(ctp.Structure): # noqa: N801 | |||||||||||||
| array([-179.5, -178.5, ..., 178.5, 179.5]) | ||||||||||||||
| >>> y # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS | ||||||||||||||
| array([ 89.5, 88.5, ..., -88.5, -89.5]) | ||||||||||||||
| >>> data.dtype | ||||||||||||||
| dtype('uint8') | ||||||||||||||
| >>> data.shape | ||||||||||||||
| (180, 360, 3) | ||||||||||||||
| >>> data.min(), data.max() | ||||||||||||||
|
|
@@ -91,3 +95,122 @@ class _GMT_IMAGE(ctp.Structure): # noqa: N801 | |||||||||||||
| # Book-keeping variables "hidden" from the API | ||||||||||||||
| ("hidden", ctp.c_void_p), | ||||||||||||||
| ] | ||||||||||||||
|
|
||||||||||||||
| def to_dataarray(self) -> xr.DataArray: | ||||||||||||||
| """ | ||||||||||||||
| Convert a _GMT_IMAGE object to an :class:`xarray.DataArray` object. | ||||||||||||||
|
|
||||||||||||||
| Returns | ||||||||||||||
| ------- | ||||||||||||||
| dataarray | ||||||||||||||
| A :class:`xarray.DataArray` object. | ||||||||||||||
|
|
||||||||||||||
| Examples | ||||||||||||||
| -------- | ||||||||||||||
| >>> from pygmt.clib import Session | ||||||||||||||
| >>> with Session() as lib: | ||||||||||||||
| ... with lib.virtualfile_out(kind="image") as voutimg: | ||||||||||||||
| ... lib.call_module("read", ["@earth_day_01d_p", voutimg, "-Ti"]) | ||||||||||||||
| ... # Read the image from the virtual file | ||||||||||||||
| ... image = lib.read_virtualfile(voutimg, kind="image") | ||||||||||||||
| ... # Convert to xarray.DataArray and use it later | ||||||||||||||
| ... da = image.contents.to_dataarray() | ||||||||||||||
| >>> da # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS | ||||||||||||||
| <xarray.DataArray 'z' (band: 3, y: 180, x: 360)>... | ||||||||||||||
| array([[[ 10, 10, 10, ..., 10, 10, 10], | ||||||||||||||
| [ 10, 10, 10, ..., 10, 10, 10], | ||||||||||||||
| [ 10, 10, 10, ..., 10, 10, 10], | ||||||||||||||
| ..., | ||||||||||||||
| [192, 193, 193, ..., 193, 192, 191], | ||||||||||||||
| [204, 206, 206, ..., 205, 206, 204], | ||||||||||||||
| [208, 210, 210, ..., 210, 210, 208]], | ||||||||||||||
| <BLANKLINE> | ||||||||||||||
| [[ 10, 10, 10, ..., 10, 10, 10], | ||||||||||||||
| [ 10, 10, 10, ..., 10, 10, 10], | ||||||||||||||
| [ 10, 10, 10, ..., 10, 10, 10], | ||||||||||||||
| ..., | ||||||||||||||
| [186, 187, 188, ..., 187, 186, 185], | ||||||||||||||
| [196, 198, 198, ..., 197, 197, 196], | ||||||||||||||
| [199, 201, 201, ..., 201, 202, 199]], | ||||||||||||||
| <BLANKLINE> | ||||||||||||||
| [[ 51, 51, 51, ..., 51, 51, 51], | ||||||||||||||
| [ 51, 51, 51, ..., 51, 51, 51], | ||||||||||||||
| [ 51, 51, 51, ..., 51, 51, 51], | ||||||||||||||
| ..., | ||||||||||||||
| [177, 179, 179, ..., 178, 177, 177], | ||||||||||||||
| [185, 187, 187, ..., 187, 186, 185], | ||||||||||||||
| [189, 191, 191, ..., 191, 191, 189]]], dtype=uint8) | ||||||||||||||
| Coordinates: | ||||||||||||||
| * y (y) float64... 89.5 88.5 87.5 86.5 ... -86.5 -87.5 -88.5 -89.5 | ||||||||||||||
| * x (x) float64... -179.5 -178.5 -177.5 -176.5 ... 177.5 178.5 179.5 | ||||||||||||||
| * band (band) uint8... 1 2 3 | ||||||||||||||
| Attributes: | ||||||||||||||
| title: | ||||||||||||||
| history: | ||||||||||||||
| description: | ||||||||||||||
| long_name: z | ||||||||||||||
| actual_range: [ 1.79769313e+308 -1.79769313e+308] | ||||||||||||||
|
|
||||||||||||||
| >>> da.coords["x"] # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS | ||||||||||||||
| <xarray.DataArray 'x' (x: 360)>... | ||||||||||||||
| array([-179.5, -178.5, -177.5, ..., 177.5, 178.5, 179.5]) | ||||||||||||||
| Coordinates: | ||||||||||||||
| * x (x) float64... -179.5 -178.5 -177.5 -176.5 ... 177.5 178.5 179.5 | ||||||||||||||
| Attributes: | ||||||||||||||
| long_name: x | ||||||||||||||
| axis: X | ||||||||||||||
| actual_range: [-180. 180.] | ||||||||||||||
| >>> da.coords["y"] # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS | ||||||||||||||
| <xarray.DataArray 'y' (y: 180)>... | ||||||||||||||
| array([ 89.5, 88.5, 87.5, 86.5, ..., -87.5, -88.5, -89.5]) | ||||||||||||||
| Coordinates: | ||||||||||||||
| * y (y) float64... 89.5 88.5 87.5 86.5 ... -86.5 -87.5 -88.5 -89.5 | ||||||||||||||
| Attributes: | ||||||||||||||
| long_name: y | ||||||||||||||
| axis: Y | ||||||||||||||
| actual_range: [-90. 90.] | ||||||||||||||
| >>> da.gmt.registration, da.gmt.gtype | ||||||||||||||
| (1, 0) | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The gtype for
Suggested change
according to: $ gmt grdinfo @earth_day_01d
~/.gmt/server/earth/earth_day/earth_day_01d_p.tif: Title: Grid imported via GDAL
~/.gmt/server/earth/earth_day/earth_day_01d_p.tif: Command:
~/.gmt/server/earth/earth_day/earth_day_01d_p.tif: Remark:
~/.gmt/server/earth/earth_day/earth_day_01d_p.tif: Pixel node registration used [Geographic grid]
~/.gmt/server/earth/earth_day/earth_day_01d_p.tif: Grid file format: gd = Import/export through GDAL
~/.gmt/server/earth/earth_day/earth_day_01d_p.tif: x_min: -180 x_max: 180 x_inc: 1 name: x n_columns: 360
~/.gmt/server/earth/earth_day/earth_day_01d_p.tif: y_min: -90 y_max: 90 y_inc: 1 name: y n_rows: 180
~/.gmt/server/earth/earth_day/earth_day_01d_p.tif: v_min: 1.79769313486e+308 v_max: -1.79769313486e+308 name: z
~/.gmt/server/earth/earth_day/earth_day_01d_p.tif: scale_factor: 1 add_offset: 0
~/.gmt/server/earth/earth_day/earth_day_01d_p.tif: Default CPT: We either need to modify the logic here: pygmt/pygmt/datatypes/header.py Line 254 in cf48764
Or I can manually override the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the function (again, not a public API function) that GMT uses to determine the grid type. The logic here is not that complicated to duplicate in Python.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How easy would it be to make that C function public actually?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It doesn't seem too difficult. But we still need to implement our own functions if we want to be compatible with GMT 6.4-6.5.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it'll be some time before GMT 6.6 comes out. I just want the gtype determination to be consistent between GMT and PyGMT, and the best case is to bind to the GMT C function. But we can also reimplement it in PyGMT for now (in a separate PR), maybe in header.py?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another possibility is to wrap the "hidden", private
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In f715aee, I've updated the logic for determining the grid/image gtype based on The logic of codes come from: https://github.com/GenericMappingTools/gmt/blob/7809736ba32d87a4a96b15444419eb176c6a35f3/src/gmt_grdio.c#L3778 |
||||||||||||||
| """ | ||||||||||||||
| # The image header | ||||||||||||||
| header = self.header.contents | ||||||||||||||
|
|
||||||||||||||
| if header.n_bands != 3: | ||||||||||||||
| msg = ( | ||||||||||||||
| f"The raster image has {header.n_bands} band(s). " | ||||||||||||||
| "Currently only 3-band images are supported. " | ||||||||||||||
| "Please consider submitting a feature request to us. " | ||||||||||||||
| ) | ||||||||||||||
| raise NotImplementedError(msg) | ||||||||||||||
|
|
||||||||||||||
| # Get dimensions and their attributes from the header. | ||||||||||||||
| dims, dim_attrs = header.dims, header.dim_attrs | ||||||||||||||
| # The coordinates, given as a tuple of the form (dims, data, attrs) | ||||||||||||||
| x = np.ctypeslib.as_array(self.x, shape=(header.n_columns,)).copy() | ||||||||||||||
| y = np.ctypeslib.as_array(self.y, shape=(header.n_rows,)).copy() | ||||||||||||||
| coords = [ | ||||||||||||||
| (dims[0], y, dim_attrs[0]), | ||||||||||||||
| (dims[1], x, dim_attrs[1]), | ||||||||||||||
| ("band", np.array([1, 2, 3], dtype=np.uint8), None), | ||||||||||||||
| ] | ||||||||||||||
|
|
||||||||||||||
| # Get DataArray without padding | ||||||||||||||
| data = np.ctypeslib.as_array( | ||||||||||||||
| self.data, shape=(header.my, header.mx, header.n_bands) | ||||||||||||||
| ).copy() | ||||||||||||||
| pad = header.pad[:] | ||||||||||||||
| data = data[pad[2] : header.my - pad[3], pad[0] : header.mx - pad[1], :] | ||||||||||||||
|
|
||||||||||||||
| # Create the xarray.DataArray object | ||||||||||||||
| image = xr.DataArray( | ||||||||||||||
| data=data, | ||||||||||||||
| coords=coords, | ||||||||||||||
| name=header.name, | ||||||||||||||
| attrs=header.data_attrs, | ||||||||||||||
|
Comment on lines
+204
to
+205
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The image pygmt/pygmt/datatypes/header.py Lines 193 to 198 in ac44706
The
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree they make no sense, but they're consistent with the behavior in GMT. The GMT's image support was likely added by Joaquim so that you may ping him for more information. |
||||||||||||||
| ).transpose("band", dims[0], dims[1]) | ||||||||||||||
|
|
||||||||||||||
| # Set GMT accessors. | ||||||||||||||
| # Must put at the end, otherwise info gets lost after certain image operations. | ||||||||||||||
| image.gmt.registration = header.registration | ||||||||||||||
| image.gmt.gtype = header.gtype | ||||||||||||||
| return image | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe don't add these attributes if they're empty?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These come from the header no? For GeoTIFF files, they don't make sense since GMT is not parsing the TIFF tags, but the title/history/description might show up for 3-band NetCDF files (if those can be passed into this
GMT_IMAGEstruct).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right,
title/history/descriptionare recommended attributes of CF-1.7. So, for netCDF files, I guess we should always show them even if they're empty.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If those are only needed for CF-1.7, then maybe we can indent the 3
attrs["..."]lines here to be inside theif self.type in {GridFormat.NX}block?pygmt/pygmt/datatypes/header.py
Lines 207 to 217 in cf48764
Same here, we can put
attrs["actual_range"]under the if-block above:pygmt/pygmt/datatypes/header.py
Line 223 in cf48764
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done at 62f0ce0.