def smooth_mask(base_mask_path, smooth_radius, target_smooth_mask_path):
    """Fill in gaps in base mask if there are neighbors.

    Args:
        base_mask_path (str): path to base raster, should be 0, 1 and nodata.
        smooth_radius (int): how far to smooth out at a max radius?
        target_smooth_mask_path (str): target smoothed file.

    Returns:
        None.

    """
    kernel_size = smooth_radius*2+1
    working_dir = tempfile.mkdtemp(
        dir=os.path.dirname(target_smooth_mask_path))
    kernel_path = os.path.join(working_dir, f'kernel_{kernel_size}.tif')
    make_neighborhood_hat_kernel(kernel_size, kernel_path)

    convolved_raster_path = os.path.join(working_dir, 'convolved_mask.tif')
    byte_nodata = 255
    pygeoprocessing.convolve_2d(
        (base_mask_path, 1), (kernel_path, 1), convolved_raster_path,
        ignore_nodata=False, working_dir=working_dir, mask_nodata=False,
        target_nodata=TARGET_NODATA)

    # set required proportion of coverage to turn on a pixel, lets make it a
    # quarter wedge.
    proportion_covered = 0.01
    threshold_val = proportion_covered * 3.14159 * (smooth_radius+1)**2
    pygeoprocessing.raster_calculator(
        [(convolved_raster_path, 1), (threshold_val, 'raw'),
         (TARGET_NODATA, 'raw'), (byte_nodata, 'raw'), ], threshold_op,
        target_smooth_mask_path, gdal.GDT_Byte, byte_nodata)
def erode_one_pixel(base_raster_path, target_raster_path):
    """Take base and erode out one pixel."""
    kernel_raster_path = os.path.join(os.path.dirname(target_raster_path),
                                      'kernel.tif')
    gtiff_driver = gdal.GetDriverByName('GTiff')
    kernel_raster = gtiff_driver.Create(kernel_raster_path, 3, 3, 1,
                                        gdal.GDT_Float32)

    # Make some kind of geotransform, it doesn't matter what but
    # will make GIS libraries behave better if it's all defined
    kernel_raster.SetGeoTransform([0, 1, 0, 0, 0, -1])
    srs = osr.SpatialReference()
    srs.SetWellKnownGeogCS('WGS84')
    kernel_raster.SetProjection(srs.ExportToWkt())

    kernel_band = kernel_raster.GetRasterBand(1)
    kernel_band.SetNoDataValue(-1)
    kernel_band.Fill(1)
    kernel_band = None
    kernel_raster = None

    pygeoprocessing.convolve_2d((base_raster_path, 1), (kernel_raster_path, 1),
                                target_raster_path,
                                target_datatype=gdal.GDT_Byte,
                                target_nodata=255)
Esempio n. 3
0
def raster_average(raster_path, radius, kernel_path, out_path):
    """Average pixel values within a radius.

    Make a search kernel where a pixel has '1' if its centerpoint is within
    the radius of the center pixel's centerpoint.
    For each pixel in a raster, center the search kernel on top of it. Then
    its "neighborhood" includes all the pixels that are below a '1' in the
    search kernel. Add up the neighborhood pixel values and divide by how
    many there are.

    This accounts for edge pixels and nodata pixels. For instance, if the
    kernel covers a 3x3 pixel area centered on each pixel, most pixels will
    have 9 valid pixels in their neighborhood, most edge pixels will have 6,
    and most corner pixels will have 4. Edge and nodata pixels in the
    neighborhood don't count towards the total (denominator in the average).

    Args:
        raster_path (str): path to the raster file to average
        radius (float): distance to average around each pixel's centerpoint in
            raster coordinate system units
        kernel_path (str): path to write out the search kernel raster, an
            intermediate output required by pygeoprocessing.convolve_2d
        out_path (str): path to write out the averaged raster output

    Returns:
        None
    """
    search_kernel = make_search_kernel(raster_path, radius)

    srs = osr.SpatialReference()
    srs.ImportFromEPSG(3857)
    projection_wkt = srs.ExportToWkt()
    pygeoprocessing.numpy_array_to_raster(
        # float32 here to avoid pygeoprocessing bug issue #180
        search_kernel.astype(numpy.float32),
        FLOAT_NODATA,
        (20, -20),
        (0, 0),
        projection_wkt,
        kernel_path)

    # convolve the signal (input raster) with the kernel and normalize
    # this is equivalent to taking an average of each pixel's neighborhood
    pygeoprocessing.convolve_2d(
        (raster_path, 1),
        (kernel_path, 1),
        out_path,
        # pixels with nodata or off the edge of the raster won't count towards
        # the sum or the number of values to normalize by
        ignore_nodata_and_edges=True,
        # divide by number of valid pixels in the kernel (averaging)
        normalize_kernel=True,
        # output will have nodata where ratio_path has nodata
        mask_nodata=True,
        target_datatype=gdal.GDT_Float32,
        target_nodata=FLOAT_NODATA)
Esempio n. 4
0
def clean_convolve_2d(signal_raster_band_path,
                      kernel_raster_band_path,
                      target_raster_path,
                      working_dir=None):
    """Do 2D convolution but mask out any close to 0 values to 0."""
    temp_workspace_dir = tempfile.mkdtemp(dir=os.path.dirname(
        signal_raster_band_path[0]),
                                          prefix='clean_convolve_2d')
    temp_target_raster = os.path.join(temp_workspace_dir, 'result.tif')
    pygeoprocessing.convolve_2d(signal_raster_band_path,
                                kernel_raster_band_path,
                                temp_target_raster,
                                working_dir=working_dir)
    nodata = pygeoprocessing.get_raster_info(temp_target_raster)['nodata'][0]
    pygeoprocessing.raster_calculator([(temp_target_raster, 1),
                                       (1e-6, 'raw')], set_almost_zero_to_zero,
                                      target_raster_path, gdal.GDT_Float32,
                                      nodata)
    try:
        shutil.rmtree(temp_workspace_dir)
    except OSError:
        LOGGER.exception('unable to remove %s' % temp_workspace_dir)
Esempio n. 5
0
def execute(args):
    """Habitat Quality.

    Open files necessary for the portion of the habitat_quality
    model.

    Args:
        workspace_dir (string): a path to the directory that will write output
            and other temporary files (required)
        lulc_cur_path (string): a path to an input land use/land cover raster
            (required)
        lulc_fut_path (string): a path to an input land use/land cover raster
            (optional)
        lulc_bas_path (string): a path to an input land use/land cover raster
            (optional, but required for rarity calculations)
        threat_folder (string): a path to the directory that will contain all
            threat rasters (required)
        threats_table_path (string): a path to an input CSV containing data
            of all the considered threats. Each row is a degradation source
            and each column a different attribute of the source with the
            following names: 'THREAT','MAX_DIST','WEIGHT' (required).
        access_vector_path (string): a path to an input polygon shapefile
            containing data on the relative protection against threats (optional)
        sensitivity_table_path (string): a path to an input CSV file of LULC
            types, whether they are considered habitat, and their sensitivity
            to each threat (required)
        half_saturation_constant (float): a python float that determines
            the spread and central tendency of habitat quality scores
            (required)
        suffix (string): a python string that will be inserted into all
            raster path paths just before the file extension.

    Example Args Dictionary::

        {
            'workspace_dir': 'path/to/workspace_dir',
            'lulc_cur_path': 'path/to/lulc_cur_raster',
            'lulc_fut_path': 'path/to/lulc_fut_raster',
            'lulc_bas_path': 'path/to/lulc_bas_raster',
            'threat_raster_folder': 'path/to/threat_rasters/',
            'threats_table_path': 'path/to/threats_csv',
            'access_vector_path': 'path/to/access_shapefile',
            'sensitivity_table_path': 'path/to/sensitivity_csv',
            'half_saturation_constant': 0.5,
            'suffix': '_results',
        }

    Returns:
        None
    """
    workspace = args['workspace_dir']

    # Append a _ to the suffix if it's not empty and doesn't already have one
    suffix = utils.make_suffix_string(args, 'suffix')

    # Check to see if each of the workspace folders exists.  If not, create the
    # folder in the filesystem.
    inter_dir = os.path.join(workspace, 'intermediate')
    out_dir = os.path.join(workspace, 'output')
    kernel_dir = os.path.join(inter_dir, 'kernels')
    utils.make_directories([inter_dir, out_dir, kernel_dir])

    # get a handle on the folder with the threat rasters
    threat_raster_dir = args['threat_raster_folder']

    threat_dict = utils.build_lookup_from_csv(args['threats_table_path'],
                                              'THREAT',
                                              to_lower=False)
    sensitivity_dict = utils.build_lookup_from_csv(
        args['sensitivity_table_path'], 'LULC', to_lower=False)

    # check that the required headers exist in the sensitivity table.
    # Raise exception if they don't.
    sens_header_list = sensitivity_dict.items()[0][1].keys()
    required_sens_header_list = ['LULC', 'NAME', 'HABITAT']
    missing_sens_header_list = [
        h for h in required_sens_header_list if h not in sens_header_list
    ]
    if missing_sens_header_list:
        raise ValueError('Column(s) %s are missing in the sensitivity table' %
                         (', '.join(missing_sens_header_list)))

    # check that the threat names in the threats table match with the threats
    # columns in the sensitivity table. Raise exception if they don't.
    for threat in threat_dict:
        if 'L_' + threat not in sens_header_list:
            missing_threat_header_list = (set(sens_header_list) -
                                          set(required_sens_header_list))
            raise ValueError(
                'Threat "%s" does not match any column in the sensitivity '
                'table. Possible columns: %s' %
                (threat, missing_threat_header_list))

    # get the half saturation constant
    try:
        half_saturation = float(args['half_saturation_constant'])
    except ValueError:
        raise ValueError('Half-saturation constant is not a numeric number.'
                         'It is: %s' % args['half_saturation_constant'])

    # declare dictionaries to store the land cover and the threat rasters
    # pertaining to the different threats
    lulc_path_dict = {}
    threat_path_dict = {}
    # also store land cover and threat rasters in a list
    lulc_and_threat_raster_list = []
    aligned_raster_list = []
    # declare a set to store unique codes from lulc rasters
    raster_unique_lucodes = set()

    # compile all the threat rasters associated with the land cover
    for lulc_key, lulc_args in (('_c', 'lulc_cur_path'),
                                ('_f', 'lulc_fut_path'), ('_b',
                                                          'lulc_bas_path')):
        if lulc_args in args:
            lulc_path = args[lulc_args]
            lulc_path_dict[lulc_key] = lulc_path
            # save land cover paths in a list for alignment and resize
            lulc_and_threat_raster_list.append(lulc_path)
            aligned_raster_list.append(
                os.path.join(
                    inter_dir,
                    os.path.basename(lulc_path).replace(
                        '.tif', '_aligned.tif')))

            # save unique codes to check if it's missing in sensitivity table
            for _, lulc_block in pygeoprocessing.iterblocks((lulc_path, 1)):
                raster_unique_lucodes.update(numpy.unique(lulc_block))

            # Remove the nodata value from the set of landuser codes.
            nodata = pygeoprocessing.get_raster_info(lulc_path)['nodata'][0]
            try:
                raster_unique_lucodes.remove(nodata)
            except KeyError:
                # KeyError when the nodata value was not encountered in the
                # raster's pixel values.  Same result when nodata value is
                # None.
                pass

            # add a key to the threat dictionary that associates all threat
            # rasters with this land cover
            threat_path_dict['threat' + lulc_key] = {}

            # for each threat given in the CSV file try opening the associated
            # raster which should be found in threat_raster_folder
            for threat in threat_dict:
                # it's okay to have no threat raster for baseline scenario
                threat_path_dict['threat' + lulc_key][threat] = (
                    resolve_ambiguous_raster_path(
                        os.path.join(threat_raster_dir, threat + lulc_key),
                        raise_error=(lulc_key != '_b')))

                # save threat paths in a list for alignment and resize
                threat_path = threat_path_dict['threat' + lulc_key][threat]
                if threat_path:
                    lulc_and_threat_raster_list.append(threat_path)
                    aligned_raster_list.append(
                        os.path.join(
                            inter_dir,
                            os.path.basename(lulc_path).replace(
                                '.tif', '_aligned.tif')))
    # check if there's any lucode from the LULC rasters missing in the
    # sensitivity table
    table_unique_lucodes = set(sensitivity_dict.keys())
    missing_lucodes = raster_unique_lucodes.difference(table_unique_lucodes)
    if missing_lucodes:
        raise ValueError(
            'The following land cover codes were found in your landcover rasters '
            'but not in your sensitivity table. Check your sensitivity table '
            'to see if they are missing: %s. \n\n' %
            ', '.join([str(x) for x in sorted(missing_lucodes)]))

    # Align and resize all the land cover and threat rasters,
    # and tore them in the intermediate folder
    LOGGER.info('Starting aligning and resizing land cover and threat rasters')

    lulc_pixel_size = (pygeoprocessing.get_raster_info(
        args['lulc_cur_path']))['pixel_size']

    aligned_raster_list = [
        os.path.join(inter_dir,
                     os.path.basename(path).replace('.tif', '_aligned.tif'))
        for path in lulc_and_threat_raster_list
    ]

    pygeoprocessing.align_and_resize_raster_stack(
        lulc_and_threat_raster_list, aligned_raster_list,
        ['near'] * len(lulc_and_threat_raster_list), lulc_pixel_size,
        'intersection')

    LOGGER.info('Finished aligning and resizing land cover and threat rasters')

    # Modify paths in lulc_path_dict and threat_path_dict to be aligned rasters
    for lulc_key, lulc_path in lulc_path_dict.iteritems():
        lulc_path_dict[lulc_key] = os.path.join(
            inter_dir,
            os.path.basename(lulc_path).replace('.tif', '_aligned.tif'))
        for threat in threat_dict:
            threat_path = threat_path_dict['threat' + lulc_key][threat]
            if threat_path in lulc_and_threat_raster_list:
                threat_path_dict['threat' + lulc_key][threat] = os.path.join(
                    inter_dir,
                    os.path.basename(threat_path).replace(
                        '.tif', '_aligned.tif'))

    LOGGER.info('Starting habitat_quality biophysical calculations')

    # Rasterize access vector, if value is null set to 1 (fully accessible),
    # else set to the value according to the ACCESS attribute
    cur_lulc_path = lulc_path_dict['_c']
    fill_value = 1.0
    try:
        LOGGER.info('Handling Access Shape')
        access_raster_path = os.path.join(inter_dir,
                                          'access_layer%s.tif' % suffix)
        # create a new raster based on the raster info of current land cover
        pygeoprocessing.new_raster_from_base(cur_lulc_path,
                                             access_raster_path,
                                             gdal.GDT_Float32, [_OUT_NODATA],
                                             fill_value_list=[fill_value])
        pygeoprocessing.rasterize(args['access_vector_path'],
                                  access_raster_path,
                                  burn_values=None,
                                  option_list=['ATTRIBUTE=ACCESS'])

    except KeyError:
        LOGGER.info('No Access Shape Provided, access raster filled with 1s.')

    # calculate the weight sum which is the sum of all the threats' weights
    weight_sum = 0.0
    for threat_data in threat_dict.itervalues():
        # Sum weight of threats
        weight_sum = weight_sum + threat_data['WEIGHT']

    LOGGER.debug('lulc_path_dict : %s', lulc_path_dict)

    # for each land cover raster provided compute habitat quality
    for lulc_key, lulc_path in lulc_path_dict.iteritems():
        LOGGER.info('Calculating habitat quality for landuse: %s', lulc_path)

        # Create raster of habitat based on habitat field
        habitat_raster_path = os.path.join(
            inter_dir, 'habitat%s%s.tif' % (lulc_key, suffix))
        map_raster_to_dict_values(lulc_path,
                                  habitat_raster_path,
                                  sensitivity_dict,
                                  'HABITAT',
                                  _OUT_NODATA,
                                  values_required=False)

        # initialize a list that will store all the threat/threat rasters
        # after they have been adjusted for distance, weight, and access
        deg_raster_list = []

        # a list to keep track of the normalized weight for each threat
        weight_list = numpy.array([])

        # variable to indicate whether we should break out of calculations
        # for a land cover because a threat raster was not found
        exit_landcover = False

        # adjust each threat/threat raster for distance, weight, and access
        for threat, threat_data in threat_dict.iteritems():
            LOGGER.info('Calculating threat: %s.\nThreat data: %s' %
                        (threat, threat_data))

            # get the threat raster for the specific threat
            threat_raster_path = threat_path_dict['threat' + lulc_key][threat]
            LOGGER.info('threat_raster_path %s', threat_raster_path)
            if threat_raster_path is None:
                LOGGER.info(
                    'The threat raster for %s could not be found for the land '
                    'cover %s. Skipping Habitat Quality calculation for this '
                    'land cover.' % (threat, lulc_key))
                exit_landcover = True
                break

            # need the pixel size for the threat raster so we can create
            # an appropriate kernel for convolution
            threat_pixel_size = pygeoprocessing.get_raster_info(
                threat_raster_path)['pixel_size']
            # pixel size tuple could have negative value
            mean_threat_pixel_size = (abs(threat_pixel_size[0]) +
                                      abs(threat_pixel_size[1])) / 2.0

            # convert max distance (given in KM) to meters
            max_dist_m = threat_data['MAX_DIST'] * 1000.0

            # convert max distance from meters to the number of pixels that
            # represents on the raster
            max_dist_pixel = max_dist_m / mean_threat_pixel_size
            LOGGER.debug('Max distance in pixels: %f', max_dist_pixel)

            # blur the threat raster based on the effect of the threat over
            # distance
            decay_type = threat_data['DECAY']
            kernel_path = os.path.join(
                kernel_dir, 'kernel_%s%s%s.tif' % (threat, lulc_key, suffix))
            if decay_type == 'linear':
                make_linear_decay_kernel_path(max_dist_pixel, kernel_path)
            elif decay_type == 'exponential':
                utils.exponential_decay_kernel_raster(max_dist_pixel,
                                                      kernel_path)
            else:
                raise ValueError(
                    "Unknown type of decay in biophysical table, should be "
                    "either 'linear' or 'exponential'. Input was %s for threat"
                    " %s." % (decay_type, threat))

            filtered_threat_raster_path = os.path.join(
                inter_dir, 'filtered_%s%s%s.tif' % (threat, lulc_key, suffix))
            pygeoprocessing.convolve_2d((threat_raster_path, 1),
                                        (kernel_path, 1),
                                        filtered_threat_raster_path)

            # create sensitivity raster based on threat
            sens_raster_path = os.path.join(
                inter_dir, 'sens_%s%s%s.tif' % (threat, lulc_key, suffix))
            map_raster_to_dict_values(lulc_path,
                                      sens_raster_path,
                                      sensitivity_dict,
                                      'L_' + threat,
                                      _OUT_NODATA,
                                      values_required=True)

            # get the normalized weight for each threat
            weight_avg = threat_data['WEIGHT'] / weight_sum

            # add the threat raster adjusted by distance and the raster
            # representing sensitivity to the list to be past to
            # vectorized_rasters below
            deg_raster_list.append(filtered_threat_raster_path)
            deg_raster_list.append(sens_raster_path)

            # store the normalized weight for each threat in a list that
            # will be used below in total_degradation
            weight_list = numpy.append(weight_list, weight_avg)

        # check to see if we got here because a threat raster was missing
        # and if so then we want to skip to the next landcover
        if exit_landcover:
            continue

        def total_degradation(*raster):
            """A vectorized function that computes the degradation value for
                each pixel based on each threat and then sums them together

                *rasters - a list of floats depicting the adjusted threat
                    value per pixel based on distance and sensitivity.
                    The values are in pairs so that the values for each threat
                    can be tracked:
                    [filtered_val_threat1, sens_val_threat1,
                     filtered_val_threat2, sens_val_threat2, ...]
                    There is an optional last value in the list which is the
                    access_raster value, but it is only present if
                    access_raster is not None.

                returns - the total degradation score for the pixel"""

            # we can not be certain how many threats the user will enter,
            # so we handle each filtered threat and sensitivity raster
            # in pairs
            sum_degradation = numpy.zeros(raster[0].shape)
            for index in range(len(raster) / 2):
                step = index * 2
                sum_degradation += (raster[step] * raster[step + 1] *
                                    weight_list[index])

            nodata_mask = numpy.empty(raster[0].shape, dtype=numpy.int8)
            nodata_mask[:] = 0
            for array in raster:
                nodata_mask = nodata_mask | (array == _OUT_NODATA)

            # the last element in raster is access
            return numpy.where(nodata_mask, _OUT_NODATA,
                               sum_degradation * raster[-1])

        # add the access_raster onto the end of the collected raster list. The
        # access_raster will be values from the shapefile if provided or a
        # raster filled with all 1's if not
        deg_raster_list.append(access_raster_path)

        deg_sum_raster_path = os.path.join(
            out_dir, 'deg_sum' + lulc_key + suffix + '.tif')

        LOGGER.info('Starting raster calculation on total_degradation')

        deg_raster_band_list = [(path, 1) for path in deg_raster_list]
        pygeoprocessing.raster_calculator(deg_raster_band_list,
                                          total_degradation,
                                          deg_sum_raster_path,
                                          gdal.GDT_Float32, _OUT_NODATA)

        LOGGER.info('Finished raster calculation on total_degradation')

        # Compute habitat quality
        # ksq: a term used below to compute habitat quality
        ksq = half_saturation**_SCALING_PARAM

        def quality_op(degradation, habitat):
            """Vectorized function that computes habitat quality given
                a degradation and habitat value.

                degradation - a float from the created degradation
                    raster above.
                habitat - a float indicating habitat suitability from
                    from the habitat raster created above.

                returns - a float representing the habitat quality
                    score for a pixel
            """
            degredataion_clamped = numpy.where(degradation < 0, 0, degradation)

            return numpy.where(
                (degradation == _OUT_NODATA) | (habitat == _OUT_NODATA),
                _OUT_NODATA,
                (habitat * (1.0 -
                            ((degredataion_clamped**_SCALING_PARAM) /
                             (degredataion_clamped**_SCALING_PARAM + ksq)))))

        quality_path = os.path.join(out_dir,
                                    'quality' + lulc_key + suffix + '.tif')

        LOGGER.info('Starting raster calculation on quality_op')

        deg_hab_raster_list = [deg_sum_raster_path, habitat_raster_path]

        deg_hab_raster_band_list = [(path, 1) for path in deg_hab_raster_list]
        pygeoprocessing.raster_calculator(deg_hab_raster_band_list, quality_op,
                                          quality_path, gdal.GDT_Float32,
                                          _OUT_NODATA)

        LOGGER.info('Finished raster calculation on quality_op')

    # Compute Rarity if user supplied baseline raster
    if '_b' not in lulc_path_dict:
        LOGGER.info('Baseline not provided to compute Rarity')
    else:
        lulc_base_path = lulc_path_dict['_b']

        # get the area of a base pixel to use for computing rarity where the
        # pixel sizes are different between base and cur/fut rasters
        base_pixel_size = pygeoprocessing.get_raster_info(
            lulc_base_path)['pixel_size']
        base_area = float(abs(base_pixel_size[0]) * abs(base_pixel_size[1]))
        base_nodata = pygeoprocessing.get_raster_info(
            lulc_base_path)['nodata'][0]

        lulc_code_count_b = raster_pixel_count(lulc_base_path)

        # compute rarity for current landscape and future (if provided)
        for lulc_key in ['_c', '_f']:
            if lulc_key not in lulc_path_dict:
                continue
            lulc_path = lulc_path_dict[lulc_key]
            lulc_time = 'current' if lulc_key == '_c' else 'future'

            # get the area of a cur/fut pixel
            lulc_pixel_size = pygeoprocessing.get_raster_info(
                lulc_path)['pixel_size']
            lulc_area = float(
                abs(lulc_pixel_size[0]) * abs(lulc_pixel_size[1]))
            lulc_nodata = pygeoprocessing.get_raster_info(
                lulc_path)['nodata'][0]

            def trim_op(base, cover_x):
                """Trim cover_x to the mask of base.

                Parameters:
                    base (numpy.ndarray): base raster from 'lulc_base'
                    cover_x (numpy.ndarray): either future or current land
                        cover raster from 'lulc_path' above

                Returns:
                    _OUT_NODATA where either array has nodata, otherwise
                    cover_x.
                """
                return numpy.where(
                    (base == base_nodata) | (cover_x == lulc_nodata),
                    base_nodata, cover_x)

            LOGGER.info('Create new cover for %s', lulc_path)

            new_cover_path = os.path.join(
                inter_dir, 'new_cover' + lulc_key + suffix + '.tif')

            LOGGER.info('Starting masking %s land cover to base land cover.' %
                        lulc_time)

            pygeoprocessing.raster_calculator([(lulc_base_path, 1),
                                               (lulc_path, 1)], trim_op,
                                              new_cover_path, gdal.GDT_Float32,
                                              _OUT_NODATA)

            LOGGER.info('Finished masking %s land cover to base land cover.' %
                        lulc_time)

            LOGGER.info('Starting rarity computation on %s land cover.' %
                        lulc_time)

            lulc_code_count_x = raster_pixel_count(new_cover_path)

            # a dictionary to map LULC types to a number that depicts how
            # rare they are considered
            code_index = {}

            # compute rarity index for each lulc code
            # define 0.0 if an lulc code is found in the cur/fut landcover
            # but not the baseline
            for code in lulc_code_count_x.iterkeys():
                if code in lulc_code_count_b:
                    numerator = lulc_code_count_x[code] * lulc_area
                    denominator = lulc_code_count_b[code] * base_area
                    ratio = 1.0 - (numerator / denominator)
                    code_index[code] = ratio
                else:
                    code_index[code] = 0.0

            rarity_path = os.path.join(out_dir,
                                       'rarity' + lulc_key + suffix + '.tif')

            pygeoprocessing.reclassify_raster((new_cover_path, 1), code_index,
                                              rarity_path, gdal.GDT_Float32,
                                              _RARITY_NODATA)

            LOGGER.info('Finished rarity computation on %s land cover.' %
                        lulc_time)
    LOGGER.info('Finished habitat_quality biophysical calculations')
Esempio n. 6
0
def calculate_reef_population_value(shore_sample_point_vector_path,
                                    dem_raster_path, reef_habitat_raster_path,
                                    population_raster_path_id_target_list,
                                    temp_workspace_dir):
    """Calculate population within protective range of reefs.

    Parameters:
        shore_sample_point_vector_path (str): path to a point shapefile that is
            used for referencing the points of interest on the coastline where.
        dem_raster_path (str): path to a dem used to mask population by height
            in wgs84 lat/lng projection.
        reef_habitat_raster_path (str): path to a mask raster where reef
            habitat exists.
        population_raster_path_id_list (list): list of
            (raster_path, field_id, target_path)
            tuples. The values in the raster paths will be masked where it
            overlaps with < 10m dem height and convolved within 2km. That
            result is in turn spread onto the habitat coverage at a distance
            of the protective distance of reefs. These rasters are in
            wgs84 lat/lng projection.

    Returns:
        None

    """
    wgs84_srs = osr.SpatialReference()
    wgs84_srs.ImportFromEPSG(4326)
    aligned_pop_raster_list = align_raster_list(
        [x[0] for x in population_raster_path_id_target_list] +
        [reef_habitat_raster_path, dem_raster_path],
        temp_workspace_dir,
        wgs84_srs.ExportToWkt(),
        align_index=len(population_raster_path_id_target_list))

    raster_info = pygeoprocessing.get_raster_info(aligned_pop_raster_list[0])
    target_pixel_size = raster_info['pixel_size']

    n_pixels_in_prot_dist = max(
        1, int(REEF_PROT_DIST / (M_PER_DEGREE * abs(target_pixel_size[0]))))
    kernel_radius = [n_pixels_in_prot_dist, n_pixels_in_prot_dist]
    kernel_filepath = os.path.join(temp_workspace_dir, 'reef_kernel.tif')
    create_averaging_kernel_raster(kernel_radius,
                                   kernel_filepath,
                                   normalize=False)

    buffered_point_raster_mask_path = os.path.join(temp_workspace_dir,
                                                   'reef_buffer_mask.tif')
    make_buffered_point_raster_mask(shore_sample_point_vector_path,
                                    aligned_pop_raster_list[0],
                                    temp_workspace_dir, 'reefs_all',
                                    REEF_PROT_DIST,
                                    buffered_point_raster_mask_path)

    for pop_index, (
            _, pop_id,
            target_path) in enumerate(population_raster_path_id_target_list):
        # mask to < 10m
        pop_height_masked_path = os.path.join(temp_workspace_dir,
                                              '%s_masked_by_10m.tif' % pop_id)

        pygeoprocessing.raster_calculator(
            [
                (aligned_pop_raster_list[pop_index], 1),
                (aligned_pop_raster_list[-1], 1),
                (10.0, 'raw'),  # mask to 10 meters
                (raster_info['nodata'][0], 'raw')
            ],  # the -1 index is the dem
            mask_by_height_op,
            pop_height_masked_path,
            gdal.GDT_Float32,
            raster_info['nodata'][0])

        # spread the < 10m population out 2km
        n_pixels_in_2km = int(2000.0 /
                              (M_PER_DEGREE * abs(target_pixel_size[0])))
        kernel_radius_2km = [n_pixels_in_2km, n_pixels_in_2km]
        kernel_2km_filepath = os.path.join(temp_workspace_dir,
                                           '2km_kernel_%s.tif' % pop_id)
        create_averaging_kernel_raster(kernel_radius_2km,
                                       kernel_2km_filepath,
                                       normalize=False)
        pop_sum_within_2km_path = os.path.join(
            temp_workspace_dir, 'pop_sum_within_2km_%s.tif' % pop_id)
        pygeoprocessing.convolve_2d((pop_height_masked_path, 1),
                                    (kernel_2km_filepath, 1),
                                    pop_sum_within_2km_path)

        align_reef_habitat_raster_path = aligned_pop_raster_list[-2]
        population_hab_spread_raster_path = os.path.join(
            temp_workspace_dir, 'reef_%s_spread.tif' % (pop_id))
        clean_convolve_2d((pop_sum_within_2km_path, 1), (kernel_filepath, 1),
                          population_hab_spread_raster_path)
        hab_raster_info = pygeoprocessing.get_raster_info(
            align_reef_habitat_raster_path)

        # warp pop result to overlay
        clipped_pop_hab_spread_raster_path = os.path.join(
            temp_workspace_dir, 'reef_%s_spread_clipped.tif' % pop_id)
        pygeoprocessing.warp_raster(population_hab_spread_raster_path,
                                    hab_raster_info['pixel_size'],
                                    clipped_pop_hab_spread_raster_path, 'near')

        hab_spread_nodata = pygeoprocessing.get_raster_info(
            clipped_pop_hab_spread_raster_path)['nodata'][0]
        hab_nodata = hab_raster_info['nodata'][0]

        pygeoprocessing.raster_calculator(
            [(clipped_pop_hab_spread_raster_path, 1),
             (align_reef_habitat_raster_path, 1),
             (buffered_point_raster_mask_path, 1), (hab_spread_nodata, 'raw'),
             (hab_nodata, 'raw')], intersect_and_mask_raster_op, target_path,
            gdal.GDT_Float32, hab_spread_nodata),

        ecoshard.build_overviews(target_path)
Esempio n. 7
0
def _convert_landscape(
        base_lulc_path, replacement_lucode, area_to_convert,
        focal_landcover_codes, convertible_type_list, score_weight, n_steps,
        smooth_distance_from_edge_path, output_landscape_raster_path,
        stats_path, workspace_dir):
    """Expand replacement lucodes in relation to the focal lucodes.

    If the sign on `score_weight` is positive, expansion occurs marches
    away from the focal types, while if `score_weight` is negative conversion
    marches toward the focal types.

    Parameters:
        base_lulc_path (string): path to landcover raster that will be used as
            the base landcover map to agriculture pixels
        replacement_lucode (int): agriculture landcover code type found in the
            raster at `base_lulc_path`
        area_to_convert (float): area (Ha) to convert to agriculture
        focal_landcover_codes (list of int): landcover codes that are used to
            calculate proximity
        convertible_type_list (list of int): landcover codes that are allowable
            to be converted to agriculture
        score_weight (float): this value is used to multiply the distance from
            the focal landcover types when prioritizing which pixels in
            `convertable_type_list` are to be converted.  If negative,
            conversion occurs toward the focal types, if positive occurs away
            from the focal types.
        n_steps (int): number of steps to convert the landscape.  On each step
            the distance transform will be applied on the
            current value of the `focal_landcover_codes` pixels in
            `output_landscape_raster_path`.  On the first step the distance
            is calculated from `base_lulc_path`.
        smooth_distance_from_edge_path (string): an intermediate output showing
            the pixel distance from the edge of the base landcover types
        output_landscape_raster_path (string): an output raster that will
            contain the final fragmented forest layer.
        stats_path (string): a path to an output csv that records the number
            type, and area of pixels converted in `output_landscape_raster_path`
        workspace_dir (string): workspace directory that will be used to
            hold temporary files. On a successful run of this function,
            the temporary directory will be removed.

    Returns:
        None.

    """
    temp_dir = tempfile.mkdtemp(prefix='temp_dir', dir=workspace_dir)
    tmp_file_registry = {
        'non_base_mask': os.path.join(temp_dir, 'non_base_mask.tif'),
        'base_mask': os.path.join(temp_dir, 'base_mask.tif'),
        'gaussian_kernel': os.path.join(temp_dir, 'gaussian_kernel.tif'),
        'distance_from_base_mask_edge': os.path.join(
            temp_dir, 'distance_from_base_mask_edge.tif'),
        'distance_from_non_base_mask_edge': os.path.join(
            temp_dir, 'distance_from_non_base_mask_edge.tif'),
        'convertible_distances': os.path.join(
            temp_dir, 'convertible_distances.tif'),
        'distance_from_edge': os.path.join(
            temp_dir, 'distance_from_edge.tif'),
    }
    # a sigma of 1.0 gives nice visual results to smooth pixel level artifacts
    # since a pixel is the 1.0 unit
    _make_gaussian_kernel_path(1.0, tmp_file_registry['gaussian_kernel'])

    # create the output raster first as a copy of the base landcover so it can
    # be looped on for each step
    lulc_raster_info = pygeoprocessing.get_raster_info(base_lulc_path)
    lulc_nodata = lulc_raster_info['nodata'][0]
    mask_nodata = 2
    pygeoprocessing.raster_calculator(
        [(base_lulc_path, 1)], lambda x: x, output_landscape_raster_path,
        gdal.GDT_Int32, lulc_nodata)

    # convert everything furthest from edge for each of n_steps
    pixel_area_ha = (
        abs(lulc_raster_info['pixel_size'][0]) *
        abs(lulc_raster_info['pixel_size'][1])) / 10000.0
    max_pixels_to_convert = int(math.ceil(area_to_convert / pixel_area_ha))
    convertible_type_nodata = -1
    pixels_left_to_convert = max_pixels_to_convert
    pixels_to_convert = max_pixels_to_convert / n_steps
    stats_cache = collections.defaultdict(int)

    # pylint complains when these are defined inside the loop
    invert_mask = None
    distance_nodata = None

    for step_index in range(n_steps):
        LOGGER.info('step %d of %d', step_index+1, n_steps)
        pixels_left_to_convert -= pixels_to_convert

        # Often the last segement of the steps will overstep the  number of
        # pixels to convert, this check converts the exact amount
        if pixels_left_to_convert < 0:
            pixels_to_convert += pixels_left_to_convert

        # create distance transforms for inside and outside the base lulc codes
        LOGGER.info('create distance transform for current landcover')
        for invert_mask, mask_id, distance_id in [
                (False, 'non_base_mask', 'distance_from_non_base_mask_edge'),
                (True, 'base_mask', 'distance_from_base_mask_edge')]:

            def _mask_base_op(lulc_array):
                """Create a mask of valid non-base pixels only."""
                base_mask = numpy.in1d(
                    lulc_array.flatten(), focal_landcover_codes).reshape(
                        lulc_array.shape)
                if invert_mask:
                    base_mask = ~base_mask
                return numpy.where(
                    lulc_array == lulc_nodata,
                    mask_nodata, base_mask)
            pygeoprocessing.raster_calculator(
                [(output_landscape_raster_path, 1)], _mask_base_op,
                tmp_file_registry[mask_id], gdal.GDT_Byte,
                mask_nodata)

            # create distance transform for the current mask
            pygeoprocessing.distance_transform_edt(
                (tmp_file_registry[mask_id], 1),
                tmp_file_registry[distance_id],
                working_dir=temp_dir)

        # combine inner and outer distance transforms into one
        distance_nodata = pygeoprocessing.get_raster_info(
            tmp_file_registry['distance_from_base_mask_edge'])['nodata'][0]

        def _combine_masks(base_distance_array, non_base_distance_array):
            """Create a mask of valid non-base pixels only."""
            result = non_base_distance_array
            valid_base_mask = base_distance_array > 0.0
            result[valid_base_mask] = base_distance_array[valid_base_mask]
            return result
        pygeoprocessing.raster_calculator(
            [(tmp_file_registry['distance_from_base_mask_edge'], 1),
             (tmp_file_registry['distance_from_non_base_mask_edge'], 1)],
            _combine_masks, tmp_file_registry['distance_from_edge'],
            gdal.GDT_Float32, distance_nodata)

        # smooth the distance transform to avoid scanline artifacts
        pygeoprocessing.convolve_2d(
            (tmp_file_registry['distance_from_edge'], 1),
            (tmp_file_registry['gaussian_kernel'], 1),
            smooth_distance_from_edge_path)

        # turn inside and outside masks into a single mask
        def _mask_to_convertible_codes(distance_from_base_edge, lulc):
            """Mask out the distance transform to a set of lucodes."""
            convertible_mask = numpy.in1d(
                lulc.flatten(), convertible_type_list).reshape(lulc.shape)
            return numpy.where(
                convertible_mask, distance_from_base_edge,
                convertible_type_nodata)
        pygeoprocessing.raster_calculator(
            [(smooth_distance_from_edge_path, 1),
             (output_landscape_raster_path, 1)],
            _mask_to_convertible_codes,
            tmp_file_registry['convertible_distances'], gdal.GDT_Float32,
            convertible_type_nodata)

        LOGGER.info(
            'convert %d pixels to lucode %d', pixels_to_convert,
            replacement_lucode)
        _convert_by_score(
            tmp_file_registry['convertible_distances'], pixels_to_convert,
            output_landscape_raster_path, replacement_lucode, stats_cache,
            score_weight)

    _log_stats(stats_cache, pixel_area_ha, stats_path)
    try:
        shutil.rmtree(temp_dir)
    except OSError:
        LOGGER.warn(
            "Could not delete temporary working directory '%s'", temp_dir)
    # Make convolution kernel
    kernel_path = os.path.join(churn_dir, 'kernel.tif')
    # assume square pixels
    kernel_radius = int(args.protective_distance // target_pixel_size[0])
    LOGGER.info(f"kernel radius: {kernel_radius}")
    kernel_x, kernel_y = numpy.meshgrid(range((kernel_radius - 1) * 2 + 1),
                                        range((kernel_radius - 1) * 2 + 1))
    kernel_distance = numpy.sqrt((kernel_x - (kernel_radius - 1))**2 +
                                 (kernel_y - (kernel_radius - 1))**2)
    kernel_array = (kernel_distance <= kernel_radius).astype(numpy.int8)

    pygeoprocessing.numpy_array_to_raster(kernel_array, 0, (1, -1), (0, 0),
                                          None, kernel_path)

    # Convolve CV points for coverage
    convolve_target_raster_path = os.path.join(churn_dir, 'convolve_2d.tif')
    pygeoprocessing.convolve_2d((shore_point_raster_path, 1), (kernel_path, 1),
                                convolve_target_raster_path,
                                ignore_nodata_and_edges=False,
                                mask_nodata=False,
                                normalize_kernel=False,
                                target_datatype=gdal.GDT_Float64)

    target_habitat_value_raster_path = os.path.join(
        args.workspace_dir, args.target_habitat_value_raster_filename)

    # TODO: mask result to habitat
    mask_by_nodata(convolve_target_raster_path, habitat_mask_raster_path,
                   target_habitat_value_raster_path)
def _create_marginal_value_layer(
        future_raster_path, base_raster_path,
        gaussian_blur_pixel_radius, mask_raster_path, target_raster_path):
    """Calculate marginal value layer.

    Calculated by taking the difference of future from base, Gaussian blurring
    that result by the given radius, and masking by the given raster mask.

    Args:
        future_raster_path (str): raster A, same nodata and size as B
        base_raster_path (str): raster B
        gaussian_blur_pixel_radius (int): number of pixels to blur out when
            determining marginal value of that pixel.
        mask_raster_path (str): path to raster where anything not 1 is masked
            to 0/nodata.
        target_diff_raster_path (str): result of A-B accounting for nodata.

    Returns:
        None
    """
    raster_info = pygeoprocessing.get_raster_info(future_raster_path)
    nodata = raster_info['nodata'][0]

    def _diff_op(a_array, b_array):
        """Return a-b and consider nodata."""
        result = numpy.copy(a_array)
        valid_mask = ~numpy.isclose(a_array, nodata)
        result[valid_mask] -= b_array[valid_mask]
        return result

    churn_dir = tempfile.mkdtemp(dir=os.path.dirname(target_raster_path))
    diff_raster_path = os.path.join(churn_dir, 'diff.tif')
    pygeoprocessing.raster_calculator(
        [(future_raster_path, 1), (base_raster_path, 1)], _diff_op,
        diff_raster_path, raster_info['datatype'], nodata)

    # Gaussian filter
    if gaussian_blur_pixel_radius is not None:
        kernel_raster_path = os.path.join(churn_dir, 'kernel.tif')
        mask_gf_path = os.path.join(churn_dir, 'gf.tif')
        if os.path.exists(mask_gf_path):
            os.remove(mask_gf_path)
        make_kernel_raster(
            gaussian_blur_pixel_radius, kernel_raster_path)
        pygeoprocessing.convolve_2d(
            (diff_raster_path, 1), (kernel_raster_path, 1), mask_gf_path,
            ignore_nodata_and_edges=False, mask_nodata=True,
            target_nodata=0.0)
    else:
        mask_gf_path = diff_raster_path

    def _mask_op(base_array, mask_array):
        """Return base where mask is 1, otherwise 0 or nodata."""
        result = numpy.copy(base_array)
        zero_mask = (~numpy.isclose(base_array, nodata)) & (mask_array != 1)
        result[zero_mask] = 0
        return result

    pygeoprocessing.raster_calculator(
        [(mask_gf_path, 1), (mask_raster_path, 1)], _mask_op,
        target_raster_path, raster_info['datatype'], nodata)

    shutil.rmtree(churn_dir)
Esempio n. 10
0
def fill_by_convolution(base_raster_path, convolve_radius,
                        target_filled_raster_path):
    """Clip and fill.

    Clip the base raster data to the bounding box then fill any noodata
    holes with a weighted distance convolution.

    Args:
        base_raster_path (str): path to base raster
        convolve_radius (float): maximum convolution distance kernel in
            projected units of base.
        target_filled_raster_path (str): raster created by convolution fill,
            if holes are too far from valid pixels resulting fill will be
            nonsensical, perhaps NaN.

    Return:
        None
    """
    try:
        LOGGER.info(f'filling {base_raster_path}')
        # create working directory in the same directory as the target with
        # the same name as the target file so it can't be duplicated
        # easier to spot for debugging too
        working_dir = os.path.join(
            os.path.dirname(target_filled_raster_path),
            os.path.basename(os.path.splitext(target_filled_raster_path)[0]))
        try:
            os.makedirs(working_dir)
        except OSError:
            pass

        basename = os.path.basename(target_filled_raster_path)
        base_raster_info = pygeoprocessing.get_raster_info(base_raster_path)

        # this ensures a minimum of 3 pixels in case the pixel size is too
        # chunky
        n = max(3, int(convolve_radius / base_raster_info['pixel_size'][0]))
        base = numpy.zeros((n, n))
        base[n // 2, n // 2] = 1
        kernel_array = scipy.ndimage.filters.gaussian_filter(base, n / 3)
        kernel_raster_path = os.path.join(working_dir, f'kernel_{basename}')
        geotransform = base_raster_info['geotransform']
        pygeoprocessing.numpy_array_to_raster(
            kernel_array, None, base_raster_info['pixel_size'],
            (geotransform[0], geotransform[3]),
            base_raster_info['projection_wkt'], kernel_raster_path)

        # scrub input raster
        sanitized_base_raster_path = os.path.join(working_dir,
                                                  f'sanitized_{basename}')
        sanitize_raster(base_raster_path, sanitized_base_raster_path)

        # mask valid
        valid_raster_path = os.path.join(working_dir, f'sanitized_{basename}')
        pygeoprocessing.raster_calculator(
            [(base_raster_path, 1), (base_raster_info['nodata'][0], 'raw')],
            _mask_valid_op, valid_raster_path, gdal.GDT_Byte, None)
        mask_kernel_raster_path = os.path.join(working_dir,
                                               f'mask_kernel_{basename}')
        geotransform = base_raster_info['geotransform']
        mask_kernel_array = numpy.copy(kernel_array)
        mask_kernel_array[:] = 1
        pygeoprocessing.numpy_array_to_raster(
            mask_kernel_array, None, base_raster_info['pixel_size'],
            (geotransform[0], geotransform[3]),
            base_raster_info['projection_wkt'], mask_kernel_raster_path)
        coverage_raster_path = os.path.join(working_dir,
                                            f'coverage_{basename}')
        pygeoprocessing.convolve_2d((valid_raster_path, 1),
                                    (mask_kernel_raster_path, 1),
                                    coverage_raster_path,
                                    mask_nodata=False,
                                    target_nodata=-1,
                                    target_datatype=gdal.GDT_Byte,
                                    working_dir=working_dir)

        # this raster will be filled with the entire convolution
        backfill_raster_path = os.path.join(working_dir,
                                            f'backfill_{basename}')
        base_nodata = base_raster_info['nodata'][0]
        if base_nodata is None:
            target_datatype = gdal.GDT_Float64
        else:
            target_datatype = base_raster_info['datatype']
        LOGGER.info(f'create backfill from {sanitized_base_raster_path} to '
                    f'{backfill_raster_path}')
        pygeoprocessing.convolve_2d((sanitized_base_raster_path, 1),
                                    (kernel_raster_path, 1),
                                    backfill_raster_path,
                                    ignore_nodata_and_edges=True,
                                    mask_nodata=False,
                                    normalize_kernel=True,
                                    target_nodata=base_nodata,
                                    target_datatype=target_datatype,
                                    working_dir=working_dir)

        LOGGER.info(
            f'fill nodata of {base_raster_path} to {backfill_raster_path}')
        pygeoprocessing.raster_calculator([(base_raster_path, 1),
                                           (backfill_raster_path, 1),
                                           (coverage_raster_path, 1),
                                           (base_nodata, 'raw')],
                                          _fill_nodata_op,
                                          target_filled_raster_path,
                                          base_raster_info['datatype'],
                                          base_nodata)
        shutil.rmtree(working_dir)
    except Exception:
        LOGGER.exception(
            f'error on fill by convolution {target_filled_raster_path}')
        raise