martibosch / pylandstats

Computing landscape metrics in the Python ecosystem
https://doi.org/10.1371/journal.pone.0225734
GNU General Public License v3.0
82 stars 16 forks source link

Compute CORE proportion_of_landscape #42

Closed ferreirav closed 7 months ago

ferreirav commented 1 year ago

Calculation of Core Proportion of Landscape

Hi @martibosch,

Thanks for such great work with package and exhaustive dcumentation.

I am trying to calculate proportion_of_landscape, although I would like to remove cells that are labeled with the Moore/Queen rule (neighborhood rule), so I could remove the influence of edge effects and then calculate a CORE value for each class type? Is this implemented somehow in pylandstats? What could be the best approach to it?

Thanks for the help and suggestions, Vitor

martibosch commented 1 year ago

Hello Vitor,

thank you for using pylandstats. CORE metrics are not currently implemented but I can definetly add them for the release v3.0.0. I could have a first implementation ready by the beginning of next week, would that work for you?

Best, Martí

ferreirav commented 1 year ago

Hi Marti,

Yes that would be great!! In the meanwhile I have used some bespoke function that interact with pylandstats.

def remove_edges(binary_arr):
    """    
    Remove edge cells from a binary array.
    https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.convolve.html

    This function uses a convolution with a 4-neighbor kernel to identify and remove 
    isolated cells (i.e., cells that don't have any neighbors).

    Parameters
    ----------
    binary_arr : numpy.ndarray
        A 2D binary array with 1s representing the target cells/habitat type and 0s representing background.

    Returns
    -------
    numpy.ndarray
        A binary array with edges removed removed.

    Notes
    -----
    An isolated cell is identified when the convolution of the cell and its neighbors 
    results in a value of 5 (itself and four 0s as its neighbors).
    """

    # Define the 4-neighbor kernel
    kernel = [[0, 1, 0],
              [1, 1, 1],
              [0, 1, 0]]

    # Convolve the binary array with the kernel
    convolved = ndimage.convolve(binary_arr, kernel, mode='constant', cval=0)

    # Return a new binary array: 1 if the convolution result is 5, 0 otherwise
    return (convolved == 5).astype(int)
def remove_edges_labeled(landscape_arr):
    """
    Remove the edges from the patches from a labeled landscape array.
    https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.label.html

    This function generates a binary mask from the array with habitat type labels, 
    removes the edges using the `remove_edges` function and then returns the patch labels
    to the landscape array.

    Parameters
    ----------
    landscape_arr : numpy.ndarray
        A 2D array where each unique integer (greater than 0) represents a labeled patch 
        in the landscape. 0s represent the background or no-data values.

    Returns
    -------
    numpy.ndarray
        A labeled array with edge cells removed.
    """
    # Generate a binary mask of the landscape array
    binary_mask = (landscape_arr[0] > 0).astype(int)

    # Remove edge cells from the binary mask
    cleaned_mask = remove_edges(binary_mask)

    # Return the patch labels to the new core patch array
    new_core_patch_array = landscape_arr[0] * cleaned_mask

    # Return a labels features array
    core_patches_array, _ = ndimage.label(new_core_patch_array)

    return core_patches_array
def calculate_cpland(landscape_path):
    """
    Calculate the CORE proportion of the landscape (CPLAND) for each class after removing 
    the edges. The Core Area metric is calculated after removing the edges of each patch. 
    A cell is defined as core if it has no neighbor with the different value than itself.

    Parameters
    ----------
    landscape_path : str
        Path to the landscape GeoTiff file to be analyzed.

    Returns
    -------
    pandas.DataFrame
        A DataFrame with class values and their CPLAND proportions.
    """

    # Load the landscape
    dyn_landcover = pls.Landscape(landscape_path)

    # Calculate the area for every class value
    class_values = dyn_landcover.classes
    patch_arrays = [dyn_landcover.class_label(class_val) for class_val in class_values]
    core_patch_array = [remove_edges_labeled(arr) for arr in patch_arrays]

    # Calculate patch areas and sum them up
    areas = [dyn_landcover.compute_patch_areas(arr) for arr in core_patch_array]
    summed_areas = [np.sum(arr) for arr in areas]

    # Calculate the proportion of landscape for each class
    coreland_proportions = [((area / dyn_landcover.landscape_area) * 100) for area in summed_areas]

    # Create a DataFrame with the results
    results_df = pd.DataFrame({
    'class_val': class_values,
    'Core PLAND': coreland_proportions}).set_index('class_val')

    return results_df

I don't know if these will be helpful but at this end, these are working properly! Not tested though! However, I could not integrate these within the pylandstats module as that step was a bit more challenging!

Any chance you could also add the option of a 8-neighbor kernel to the function remove_edges()?

Thanks for prompt reply and help! If need any help from this end feel free to touch by. :)

martibosch commented 11 months ago

Hello @ferreirav,

I drafted a PR with the CORE metrics at: https://github.com/martibosch/pylandstats/pull/45. There are currently some issues with the build tools.

Regarding the code: rather than a convolution, I use a binary erosion to remove the edges (see the compute_core_label_arr method. The rest of the code is organized to be as DRY and object-oriented as possible, since I have taken the opportunity to add further CORE metrics. I have checked and the computed values are the same as FRAGSTATS. By the way, to get the same values, removing the edges must be done with the Von Neumann/4-neighbour kernel.

I hope this works for your needs. If so, I will close it once the PR is merged. I plan to include this in v3.0.0rc2.

Thank you again for using pylandstats and sharing your code ideas. Best, Martí

ferreirav commented 11 months ago

Hi Martin,Much appreciated all your work around this!Yes, it will be extremely helpful.Kind regards,Vitor FerreiraOn 29 Sep 2023, at 08:21, Martí Bosch @.***> wrote: Hello @ferreirav, I drafted a PR with the CORE metrics at: #45. There are currently some issues with the build tools. Regarding the code: rather than a convolution, I use a binary erosion to remove the edges (see the compute_core_label_arr method. The rest of the code is organized to be as DRY and object-oriented as possible, since I have taken the opportunity to add further CORE metrics. I have checked and the computed values are the same as FRAGSTATS. By the way, to get the same values, removing the edges must be done with the Von Neumann/4-neighbour kernel. I hope this works for your needs. If so, I will close it once the PR is merged. I plan to include this in v3.0.0rc2. Thank you again for using pylandstats and sharing your code ideas. Best, Martí

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: @.***>