Source code for cartopy.img_transform

# Copyright Crown and Cartopy Contributors
#
# This file is part of Cartopy and is released under the BSD 3-clause license.
# See LICENSE in the root of the repository for full licensing details.
"""
Generic functionality to support Cartopy image transformations.

"""

import numpy as np


try:
    from pykdtree.kdtree import KDTree as _kdtreeClass
    _is_pykdtree = True
except ImportError:
    try:
        from scipy.spatial import cKDTree as _kdtreeClass
    except ImportError as e:
        raise ImportError("Using image transforms requires either "
                          "pykdtree or scipy.") from e
    _is_pykdtree = False

import cartopy.crs as ccrs


[docs] def mesh_projection(projection, nx, ny, x_extents=(None, None), y_extents=(None, None)): """ Return sample points in the given projection which span the entire projection range evenly. The range of the x-direction and y-direction sample points will be within the bounds of the projection or specified extents. Parameters ---------- projection A :class:`~cartopy.crs.Projection` instance. nx: int The number of sample points in the projection x-direction. ny: int The number of sample points in the projection y-direction. x_extents: optional The (lower, upper) x-direction extent of the projection. Defaults to the :attr:`~cartopy.crs.Projection.x_limits`. y_extents: optional The (lower, upper) y-direction extent of the projection. Defaults to the :attr:`~cartopy.crs.Projection.y_limits`. Returns ------- A tuple of three items. The x-direction sample points :class:`numpy.ndarray` of shape (nx, ny), y-direction sample points :class:`numpy.ndarray` of shape (nx, ny), and the extent of the projection range as ``(x-lower, x-upper, y-lower, y-upper)``. """ def extent(specified, default, index): if specified[index] is not None: return specified[index] else: return default[index] # Establish the x-direction and y-direction extents. x_lower = extent(x_extents, projection.x_limits, 0) x_upper = extent(x_extents, projection.x_limits, 1) y_lower = extent(y_extents, projection.y_limits, 0) y_upper = extent(y_extents, projection.y_limits, 1) # Calculate evenly spaced sample points spanning the # extent - excluding endpoint. x, xstep = np.linspace(x_lower, x_upper, nx, retstep=True, endpoint=False) y, ystep = np.linspace(y_lower, y_upper, ny, retstep=True, endpoint=False) # Deal with single point corner case and the difference # between np.linspace v1.9 and v1.10+ retstep nan result. if nx == 1 and np.isnan(xstep): xstep = x_upper - x_lower if ny == 1 and np.isnan(ystep): ystep = y_upper - y_lower # Offset the sample points to be within the extent range. x += 0.5 * xstep y += 0.5 * ystep # Generate the x-direction and y-direction meshgrids. x, y = np.meshgrid(x, y) return x, y, [x_lower, x_upper, y_lower, y_upper]
[docs] def warp_img(fname, target_proj, source_proj=None, target_res=(400, 200)): """ Regrid the image file from the source projection to the target projection. Parameters ---------- fname Image filename to be loaded and warped. target_proj The target :class:`~cartopy.crs.Projection` instance for the image. source_proj: optional The source :class:`~cartopy.crs.Projection` instance of the image. Defaults to a :class:`~cartopy.crs.PlateCarree` projection. target_res: optional The (nx, ny) resolution of the target projection. Where nx defaults to 400 sample points, and ny defaults to 200 sample points. """ if source_proj is None: source_proj = ccrs.PlateCarree() raise NotImplementedError('Not yet implemented.')
[docs] def warp_array(array, target_proj, source_proj=None, target_res=(400, 200), source_extent=None, target_extent=None, mask_extrapolated=False): """ Regrid the data array from the source projection to the target projection. Also see, :func:`~cartopy.img_transform.regrid`. Parameters ---------- array The :class:`numpy.ndarray` of data to be regridded to the target projection. target_proj The target :class:`~cartopy.crs.Projection` instance for the data. source_proj: optional The source :class:`~cartopy.crs.Projection' instance of the data. Defaults to a :class:`~cartopy.crs.PlateCarree` projection. target_res: optional The (nx, ny) resolution of the target projection. Where nx defaults to 400 sample points, and ny defaults to 200 sample points. source_extent: optional The (x-lower, x-upper, y-lower, y-upper) extent in native source projection coordinates. target_extent: optional The (x-lower, x-upper, y-lower, y-upper) extent in native target projection coordinates. mask_extrapolated: optional Assume that the source coordinate is rectilinear and so mask the resulting target grid values which lie outside the source grid domain. Returns ------- array, extent A tuple of the regridded :class:`numpy.ndarray` in the target projection and the (x-lower, x-upper, y-lower, y-upper) target projection extent. """ # source_extent is in source coordinates. if source_extent is None: source_extent = [None] * 4 # target_extent is in target coordinates. if target_extent is None: target_extent = [None] * 4 source_x_extents = source_extent[:2] source_y_extents = source_extent[2:] target_x_extents = target_extent[:2] target_y_extents = target_extent[2:] if source_proj is None: source_proj = ccrs.PlateCarree() ny, nx = array.shape[:2] source_native_xy = mesh_projection(source_proj, nx, ny, x_extents=source_x_extents, y_extents=source_y_extents) # XXX Take into account the extents of the original to determine # target_extents? target_native_x, target_native_y, extent = mesh_projection( target_proj, target_res[0], target_res[1], x_extents=target_x_extents, y_extents=target_y_extents) array = regrid(array, source_native_xy[0], source_native_xy[1], source_proj, target_proj, target_native_x, target_native_y, mask_extrapolated) return array, extent
def _determine_bounds(x_coords, y_coords, source_cs): # Returns bounds corresponding to one or two rectangles depending on # transformation between ranges. bounds = dict(x=[]) half_px = abs(np.diff(x_coords[:2])).max() / 2. if (((hasattr(source_cs, 'is_geodetic') and source_cs.is_geodetic()) or isinstance(source_cs, ccrs.PlateCarree)) and x_coords.max() > 180): if x_coords.min() < 180: bounds['x'].append([x_coords.min() - half_px, 180]) bounds['x'].append([-180, x_coords.max() - 360 + half_px]) else: bounds['x'].append([x_coords.min() - 180 - half_px, x_coords.max() - 180 + half_px]) else: bounds['x'].append([x_coords.min() - half_px, x_coords.max() + half_px]) bounds['y'] = [y_coords.min(), y_coords.max()] return bounds
[docs] def regrid(array, source_x_coords, source_y_coords, source_proj, target_proj, target_x_points, target_y_points, mask_extrapolated=False): """ Regrid the data array from the source projection to the target projection. Parameters ---------- array The :class:`numpy.ndarray` of data to be regridded to the target projection. source_x_coords A 2-dimensional source projection :class:`numpy.ndarray` of x-direction sample points. source_y_coords A 2-dimensional source projection :class:`numpy.ndarray` of y-direction sample points. source_proj The source :class:`~cartopy.crs.Projection` instance. target_proj The target :class:`~cartopy.crs.Projection` instance. target_x_points A 2-dimensional target projection :class:`numpy.ndarray` of x-direction sample points. target_y_points A 2-dimensional target projection :class:`numpy.ndarray` of y-direction sample points. mask_extrapolated: optional Assume that the source coordinate is rectilinear and so mask the resulting target grid values which lie outside the source grid domain. Defaults to False. Returns ------- new_array The data array regridded in the target projection. """ # Stack our original xyz array, this will also wrap coords when necessary xyz = source_proj.transform_points(source_proj, source_x_coords.flatten(), source_y_coords.flatten()) # Transform the target points into the source projection target_xyz = source_proj.transform_points(target_proj, target_x_points.flatten(), target_y_points.flatten()) # Find mask of valid points before querying kdtree: scipy >= 1.11 errors # when querying nan points, might as well use for pykdtree too. indices = np.zeros(target_xyz.shape[0], dtype=int) finite_xyz = np.all(np.isfinite(target_xyz), axis=-1) if _is_pykdtree: kdtree = _kdtreeClass(xyz) # Use sqr_dists=True because we don't care about distances, # and it saves a sqrt. _, indices[finite_xyz] = kdtree.query(target_xyz[finite_xyz, :], k=1, sqr_dists=True) else: # Versions of scipy >= v0.16 added the balanced_tree argument, # which caused the KDTree to hang with this input. kdtree = _kdtreeClass(xyz, balanced_tree=False) _, indices[finite_xyz] = kdtree.query(target_xyz[finite_xyz, :], k=1) mask = ~finite_xyz | (indices >= len(xyz)) indices[mask] = 0 desired_ny, desired_nx = target_x_points.shape # Squash the first two dims of the source array into one temp_array = array.reshape((-1,) + array.shape[2:]) if np.any(mask): new_array = np.ma.array(temp_array[indices]) new_array[mask] = np.ma.masked else: new_array = temp_array[indices] new_array.shape = (desired_ny, desired_nx) + (array.shape[2:]) # Do double transform to clip points that do not map back and forth # to the same point to within a fixed fractional offset. # NOTE: This only needs to be done for (pseudo-)cylindrical projections, # or any others which have the concept of wrapping back_to_target_xyz = target_proj.transform_points(source_proj, target_xyz[:, 0], target_xyz[:, 1]) back_to_target_x = back_to_target_xyz[:, 0].reshape(desired_ny, desired_nx) back_to_target_y = back_to_target_xyz[:, 1].reshape(desired_ny, desired_nx) FRACTIONAL_OFFSET_THRESHOLD = 0.1 # data has moved by 10% of the map x_extent = np.abs(target_proj.x_limits[1] - target_proj.x_limits[0]) y_extent = np.abs(target_proj.y_limits[1] - target_proj.y_limits[0]) non_self_inverse_points = (((np.abs(target_x_points - back_to_target_x) / x_extent) > FRACTIONAL_OFFSET_THRESHOLD) | ((np.abs(target_y_points - back_to_target_y) / y_extent) > FRACTIONAL_OFFSET_THRESHOLD)) if np.any(non_self_inverse_points): if not np.ma.isMaskedArray(new_array): new_array = np.ma.array(new_array, mask=False) new_array[non_self_inverse_points] = np.ma.masked # Transform the target points to the source projection and mask any points # that fall outside the original source domain. if mask_extrapolated: target_in_source_x = target_xyz[:, 0].reshape(desired_ny, desired_nx) target_in_source_y = target_xyz[:, 1].reshape(desired_ny, desired_nx) bounds = _determine_bounds(source_x_coords, source_y_coords, source_proj) outside_source_domain = ((target_in_source_y >= bounds['y'][1]) | (target_in_source_y <= bounds['y'][0])) tmp_inside = np.zeros_like(outside_source_domain) for bound_x in bounds['x']: tmp_inside = tmp_inside | ((target_in_source_x <= bound_x[1]) & (target_in_source_x >= bound_x[0])) outside_source_domain = outside_source_domain | ~tmp_inside if np.any(outside_source_domain): if not np.ma.isMaskedArray(new_array): new_array = np.ma.array(new_array, mask=False) new_array[outside_source_domain] = np.ma.masked return new_array