def is_near(input_path, radius, distance_path, out_path): """Make binary raster of which pixels are within a radius of a '1' pixel. Args: input_path (str): path to a binary raster where '1' pixels are what we're measuring distance to, in this case roads/connected areas radius (float): distance in pixels which is considered "near". pixels this distance or less from a '1' pixel are marked '1' in the output. Distances are measured centerpoint to centerpoint. distance_path (str): path to write out the raster of distances out_path (str): path to write out the final thresholded raster. Pixels marked '1' are near to a '1' pixel in the input, pixels marked '0' are not. Returns: None """ # Calculate the distance from each pixel to the nearest '1' pixel pygeoprocessing.distance_transform_edt( (input_path, 1), distance_path) def lte_threshold_op(array, threshold): """Binary array of elements less than or equal to the threshold.""" # no need to mask nodata because distance_transform_edt doesn't # output any nodata pixels return array <= threshold # Threshold that to a binary array so '1' means it's within the radius pygeoprocessing.raster_calculator( [(distance_path, 1), (radius, 'raw')], lte_threshold_op, out_path, gdal.GDT_Byte, UINT8_NODATA)
def _map_distance_from_forest_edge(lulc_uri, biophysical_table_uri, edge_distance_uri): """Generates a raster of forest edge distances where each pixel is the distance to the edge of the forest in meters. Parameters: lulc_uri (string): path to the landcover raster that contains integer landcover codes biophysical_table_uri (string): a path to a csv table that indexes landcover codes to forest type, contains at least the fields 'lucode' (landcover integer code) and 'is_forest' (0 or 1 depending on landcover code type) edge_distance_uri (string): path to output raster where each pixel contains the euclidian pixel distance to nearest forest edges on all non-nodata values of lulc_uri Returns: None""" # Build a list of forest lucodes biophysical_table = pygeoprocessing.get_lookup_from_table( biophysical_table_uri, 'lucode') forest_codes = [ lucode for (lucode, ludata) in biophysical_table.iteritems() if int(ludata['is_forest']) == 1 ] # Make a raster where 1 is non-forest landcover types and 0 is forest forest_mask_nodata = 255 lulc_nodata = pygeoprocessing.get_nodata_from_uri(lulc_uri) def mask_non_forest_op(lulc_array): """converts forest lulc codes to 1""" non_forest_mask = ~numpy.in1d(lulc_array.flatten(), forest_codes).reshape(lulc_array.shape) nodata_mask = lulc_array == lulc_nodata return numpy.where(nodata_mask, forest_mask_nodata, non_forest_mask) non_forest_mask_uri = pygeoprocessing.temporary_filename() out_pixel_size = pygeoprocessing.get_cell_size_from_uri(lulc_uri) pygeoprocessing.vectorize_datasets([lulc_uri], mask_non_forest_op, non_forest_mask_uri, gdal.GDT_Byte, forest_mask_nodata, out_pixel_size, "intersection", vectorize_op=False) # Do the distance transform on non-forest pixels pygeoprocessing.distance_transform_edt(non_forest_mask_uri, edge_distance_uri) # good practice to delete temporary files when we're done with them os.remove(non_forest_mask_uri)
def _map_distance_from_tropical_forest_edge(base_lulc_raster_path, biophysical_table_path, edge_distance_path, target_non_forest_mask_path): """Generates a raster of forest edge distances where each pixel is the distance to the edge of the forest in meters. Parameters: base_lulc_raster_path (string): path to the landcover raster that contains integer landcover codes biophysical_table_path (string): path to a csv table that indexes landcover codes to forest type, contains at least the fields 'lucode' (landcover integer code) and 'is_tropical_forest' (0 or 1 depending on landcover code type) edge_distance_path (string): path to output raster where each pixel contains the euclidean pixel distance to nearest forest edges on all non-nodata values of base_lulc_raster_path target_non_forest_mask_path (string): path to the output non forest mask raster Returns: None """ # Build a list of forest lucodes biophysical_table = utils.build_lookup_from_csv(biophysical_table_path, 'lucode', to_lower=False) forest_codes = [ lucode for (lucode, ludata) in biophysical_table.items() if int(ludata['is_tropical_forest']) == 1 ] # Make a raster where 1 is non-forest landcover types and 0 is forest forest_mask_nodata = 255 lulc_nodata = pygeoprocessing.get_raster_info( base_lulc_raster_path)['nodata'] def mask_non_forest_op(lulc_array): """converts forest lulc codes to 1""" non_forest_mask = ~numpy.in1d(lulc_array.flatten(), forest_codes).reshape(lulc_array.shape) nodata_mask = lulc_array == lulc_nodata return numpy.where(nodata_mask, forest_mask_nodata, non_forest_mask) pygeoprocessing.raster_calculator([(base_lulc_raster_path, 1)], mask_non_forest_op, target_non_forest_mask_path, gdal.GDT_Byte, forest_mask_nodata) # Do the distance transform on non-forest pixels pygeoprocessing.distance_transform_edt((target_non_forest_mask_path, 1), edge_distance_path)
def _convert_landscape( base_lulc_uri, replacement_lucode, area_to_convert, focal_landcover_codes, convertible_type_list, score_weight, n_steps, smooth_distance_from_edge_uri, output_landscape_raster_uri, stats_uri): """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_uri (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_uri` 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_uri`. On the first step the distance is calculated from `base_lulc_uri`. smooth_distance_from_edge_uri (string): an intermediate output showing the pixel distance from the edge of the base landcover types output_landscape_raster_uri (string): an output raster that will contain the final fragmented forest layer. stats_uri (string): a path to an output csv that records the number type, and area of pixels converted in `output_landscape_raster_uri` Returns: None. """ tmp_file_registry = { 'non_base_mask': pygeoprocessing.temporary_filename(), 'base_mask': pygeoprocessing.temporary_filename(), 'gaussian_kernel': pygeoprocessing.temporary_filename(), 'distance_from_base_mask_edge': pygeoprocessing.temporary_filename(), 'distance_from_non_base_mask_edge': pygeoprocessing.temporary_filename(), 'convertible_distances': pygeoprocessing.temporary_filename(), 'smooth_distance_from_edge': pygeoprocessing.temporary_filename(), 'distance_from_edge': pygeoprocessing.temporary_filename(), } # 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_uri(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_nodata = pygeoprocessing.get_nodata_from_uri(base_lulc_uri) pixel_size_out = pygeoprocessing.get_cell_size_from_uri(base_lulc_uri) mask_nodata = 2 pygeoprocessing.vectorize_datasets( [base_lulc_uri], lambda x: x, output_landscape_raster_uri, gdal.GDT_Int32, lulc_nodata, pixel_size_out, "intersection", vectorize_op=False, datasets_are_pre_aligned=True) # convert everything furthest from edge for each of n_steps pixel_area_ha = ( pygeoprocessing.get_cell_size_from_uri(base_lulc_uri)**2 / 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 xrange(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.vectorize_datasets( [output_landscape_raster_uri], _mask_base_op, tmp_file_registry[mask_id], gdal.GDT_Byte, mask_nodata, pixel_size_out, "intersection", vectorize_op=False, datasets_are_pre_aligned=True) # create distance transform for the current mask pygeoprocessing.distance_transform_edt( tmp_file_registry[mask_id], tmp_file_registry[distance_id]) # combine inner and outer distance transforms into one distance_nodata = pygeoprocessing.get_nodata_from_uri( tmp_file_registry['distance_from_base_mask_edge']) 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.vectorize_datasets( [tmp_file_registry['distance_from_base_mask_edge'], tmp_file_registry['distance_from_non_base_mask_edge']], _combine_masks, tmp_file_registry['distance_from_edge'], gdal.GDT_Float32, distance_nodata, pixel_size_out, "intersection", vectorize_op=False, datasets_are_pre_aligned=True) # smooth the distance transform to avoid scanline artifacts pygeoprocessing.convolve_2d_uri( tmp_file_registry['distance_from_edge'], tmp_file_registry['gaussian_kernel'], smooth_distance_from_edge_uri) # 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.vectorize_datasets( [smooth_distance_from_edge_uri, output_landscape_raster_uri], _mask_to_convertible_codes, tmp_file_registry['convertible_distances'], gdal.GDT_Float32, convertible_type_nodata, pixel_size_out, "intersection", vectorize_op=False, datasets_are_pre_aligned=True) 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_uri, replacement_lucode, stats_cache, score_weight) _log_stats(stats_cache, pixel_area_ha, stats_uri) for filename in tmp_file_registry.values(): os.remove(filename)
def _map_distance_from_tropical_forest_edge(base_lulc_raster_path, biophysical_table_path, edge_distance_path, target_non_forest_mask_path): """Generates a raster of forest edge distances. Generates a raster of forest edge distances where each pixel is the distance to the edge of the forest in meters. Args: base_lulc_raster_path (string): path to the landcover raster that contains integer landcover codes biophysical_table_path (string): path to a csv table that indexes landcover codes to forest type, contains at least the fields 'lucode' (landcover integer code) and 'is_tropical_forest' (0 or 1 depending on landcover code type) edge_distance_path (string): path to output raster where each pixel contains the euclidean pixel distance to nearest forest edges on all non-nodata values of base_lulc_raster_path target_non_forest_mask_path (string): path to the output non forest mask raster Returns: None """ # Build a list of forest lucodes biophysical_table = utils.build_lookup_from_csv(biophysical_table_path, 'lucode', to_lower=False) forest_codes = [ lucode for (lucode, ludata) in biophysical_table.items() if int(ludata['is_tropical_forest']) == 1 ] # Make a raster where 1 is non-forest landcover types and 0 is forest lulc_nodata = pygeoprocessing.get_raster_info( base_lulc_raster_path)['nodata'] forest_mask_nodata = 255 def mask_non_forest_op(lulc_array): """Convert forest lulc codes to 0. Args: lulc_array (numpy.ndarray): array representing a LULC raster where each forest LULC code is in `forest_codes`. Returns: numpy.ndarray with the same shape as lulc_array. All pixels are 0 (forest), 1 (non-forest), or 255 (nodata). """ non_forest_mask = ~numpy.isin(lulc_array, forest_codes) nodata_mask = lulc_array == lulc_nodata # where LULC has nodata, set value to nodata value (255) # where LULC has data, set to 0 if LULC is a forest type, 1 if it's not return numpy.where(nodata_mask, forest_mask_nodata, non_forest_mask) pygeoprocessing.raster_calculator([(base_lulc_raster_path, 1)], mask_non_forest_op, target_non_forest_mask_path, gdal.GDT_Byte, forest_mask_nodata) # Do the distance transform on non-forest pixels # This is the distance from each pixel to the nearest pixel with value 1. # - for forest pixels, this is the distance to the forest edge # - for non-forest pixels, this is 0 # - for nodata pixels, distance is calculated but is meaningless pygeoprocessing.distance_transform_edt((target_non_forest_mask_path, 1), edge_distance_path) # mask out the meaningless distance pixels so they don't affect the output lulc_raster = gdal.OpenEx(base_lulc_raster_path) lulc_band = lulc_raster.GetRasterBand(1) edge_distance_raster = gdal.OpenEx(edge_distance_path, gdal.GA_Update) edge_distance_band = edge_distance_raster.GetRasterBand(1) for offset_dict in pygeoprocessing.iterblocks((base_lulc_raster_path, 1), offset_only=True): # where LULC has nodata, overwrite edge distance with nodata value lulc_block = lulc_band.ReadAsArray(**offset_dict) distance_block = edge_distance_band.ReadAsArray(**offset_dict) masked_distance_block = numpy.where(lulc_block == lulc_nodata, NODATA_VALUE, distance_block) edge_distance_band.WriteArray(masked_distance_block, xoff=offset_dict['xoff'], yoff=offset_dict['yoff'])
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)