API Reference

pysarflow

Preprocessing Sentinel1 GRD Data.

This Python script enables users to perform essential preprocessing steps on Sentinel-1 GRD data, including thermal noise removal, radiometric calibration, and terrain correction.

It accepts Sentinel-1 products in the .SAFE format.

The script depends on the 'esa_snappy' library, which must be installed in the Python environment where this script is executed.

This file can also be imported as a module and contains the following functions

Major: * read_grd_product Supporting: *

apply_orbit_file(product)

Apply precise satellite orbit file to the SAR product.

This operation orthorectifies the SAR product to improve accuracy by using precise orbit information.

Parameters:
  • product (Product) –

    The input SAR Product object.

Returns:
  • esa_snappy.Product: The product with orbit file applied.

Source code in pysarflow/grd.py
def apply_orbit_file(product):
    """
    Apply precise satellite orbit file to the SAR product.

    This operation orthorectifies the SAR product to improve accuracy by using
    precise orbit information.

    Args:
        product (esa_snappy.Product): The input SAR Product object.

    Returns:
        esa_snappy.Product: The product with orbit file applied.
    """
    print("\tApplying Orbit File...")
    parameters = HashMap()
    parameters.put(
        "orbitType", "Sentinel Precise (Auto Download)"
    )  # 'Sentinel Precise (Auto Download) specifically for Sentinel-1
    parameters.put(
        "continueOnFail", "false"
    )  # Do not continue if orbit file application fails

    output = GPF.createProduct("Apply-Orbit-File", parameters, product)
    print("\tOrbit File applied.")
    return output

band_difference(product_stacked)

Computes the difference between two bands of a stacked SAR product.

A new band named 'Difference_Band' is created, calculated as: Difference_Band = band2 - band1

Parameters:
  • product_stacked (Product) –

    Input SAR product with at least two bands (e.g., pre- and post-event).

Returns:
  • esa_snappy.Product: A new product containing the 'Difference_Band'.

Raises:
  • RuntimeError

    If the BandMaths operation fails.

Source code in pysarflow/grd.py
def band_difference(product_stacked):
    """
    Computes the difference between two bands of a stacked SAR product.

    A new band named 'Difference_Band' is created, calculated as:
        Difference_Band = band2 - band1

    Args:
        product_stacked (esa_snappy.Product): Input SAR product with at least two bands (e.g., pre- and post-event).

    Returns:
        esa_snappy.Product: A new product containing the 'Difference_Band'.

    Raises:
        RuntimeError: If the BandMaths operation fails.
    """

    try:
        band_names = list(product_stacked.getBandNames())
        BandDescriptor = jpy.get_type(
            "org.esa.snap.core.gpf.common.BandMathsOp$BandDescriptor"
        )

        # Create a BandDescriptor object
        band_def = BandDescriptor()
        band_def.name = "Difference_Band"
        band_def.type = "float32"
        band_def.expression = f"{band_names[1]} - {band_names[0]}"  # Ensure these band names exist in product_stacked
        band_def.noDataValue = 0.0
        band_def.description = "Post - Pre difference"

        # Create a Java array of BandDescriptor
        Array = jpy.array("org.esa.snap.core.gpf.common.BandMathsOp$BandDescriptor", 1)
        targetBands = Array
        targetBands[0] = band_def  # Assign band_def to the first index of the array

        # Parameters HashMap
        parameters = jpy.get_type("java.util.HashMap")()
        parameters.put("targetBands", targetBands)

        # Run BandMaths
        diff_product = GPF.createProduct("BandMaths", parameters, product_stacked)
        return diff_product
    except Exception as e:
        raise RuntimeError(f"Terrain correction failed: {e}")

border_noise_removal(product)

Remove border noise from the SAR product.

Border noise appears as a low-backscatter band along the image edges and can affect SAR measurements near scene boundaries. This operation trims the noisy border region to improve data quality.

Parameters:
  • product (Product) –

    The input SAR Product object.

Returns:
  • esa_snappy.Product: The product after border noise removal.

Source code in pysarflow/grd.py
def border_noise_removal(product):
    """Remove border noise from the SAR product.

    Border noise appears as a low-backscatter band along the image edges
    and can affect SAR measurements near scene boundaries. This operation
    trims the noisy border region to improve data quality.

    Args:
        product (esa_snappy.Product): The input SAR Product object.

    Returns:
        esa_snappy.Product: The product after border noise removal.
    """
    print("\tPerforming border noise removal...")
    parameters = HashMap()
    parameters.put("borderLimit", "500")
    parameters.put("trimThreshold", "0.5")
    output = GPF.createProduct("Remove-GRD-Border-Noise", parameters, product)
    print("\tBorder noise removed.")
    return output

conversion_to_db(product)

Converts SAR backscatter values from linear scale to decibels (dB).

This transformation is commonly applied to Sentinel-1 GRD products after radiometric calibration, as dB units are easier to interpret and compare across scenes.

Parameters:
  • product (Product) –

    Input SAR product in linear scale.

Returns:
  • esa_snappy.Product: Product with backscatter values expressed in dB.

Source code in pysarflow/grd.py
def conversion_to_db(product):
    """
    Converts SAR backscatter values from linear scale to decibels (dB).

    This transformation is commonly applied to Sentinel-1 GRD products
    after radiometric calibration, as dB units are easier to interpret
    and compare across scenes.

    Args:
        product (snappy.Product): Input SAR product in linear scale.

    Returns:
        esa_snappy.Product: Product with backscatter values expressed in dB.
    """
    parameters = HashMap()
    output = GPF.createProduct("linearToFromdB", parameters, product)
    print("\tConversion complete.")
    return output

export(product, output_path)

Exports a SNAP product as a GeoTIFF file.

Parameters:
  • product (Product) –

    The processed SNAP product to export.

  • output_path (str) –

    Destination .tif file path.

Source code in pysarflow/grd.py
def export(product, output_path) -> None:
    """
    Exports a SNAP product as a GeoTIFF file.

    Args:
        product (snappy.Product): The processed SNAP product to export.
        output_path (str): Destination .tif file path.
    """
    # Ensure output directory exists
    os.makedirs(os.path.dirname(output_path), exist_ok=True)

    print(f"Exporting product to {output_path} (GeoTIFF)...")
    ProductIO.writeProduct(product, output_path, "GeoTIFF")
    print("\tExport complete.")

generateFloodMask(product_masked, threshold=0.1)

Generates a flood mask from a masked difference band in a SAR product.

The function creates a new band called 'flooded', where pixels are classified as: - -9999.0: NoData - 0: Permanent water - 1: Flooded - 2: Others

Parameters:
  • product_masked (Product) –

    Input SAR product with masked difference band.

  • threshold (float, default: 0.1 ) –

    Threshold for identifying flooded pixels. Default is 0.1.

Returns:
  • esa_snappy.Product: A new product with a 'flooded' band representing the flood mask.

Raises:
  • RuntimeError

    If the BandMaths operation fails.

Source code in pysarflow/grd.py
def generateFloodMask(product_masked, threshold=0.1):
    """
    Generates a flood mask from a masked difference band in a SAR product.

    The function creates a new band called 'flooded', where pixels are classified as:
        - -9999.0: NoData
        - 0: Permanent water
        - 1: Flooded
        - 2: Others

    Args:
        product_masked (esa_snappy.Product): Input SAR product with masked difference band.
        threshold (float, optional): Threshold for identifying flooded pixels. Default is 0.1.

    Returns:
        esa_snappy.Product: A new product with a 'flooded' band representing the flood mask.

    Raises:
        RuntimeError: If the BandMaths operation fails.
    """
    try:
        parameters = HashMap()
        BandDescriptor = jpy.get_type(
            "org.esa.snap.core.gpf.common.BandMathsOp$BandDescriptor"
        )

        targetBand = BandDescriptor()
        targetBand.name = "flooded"
        targetBand.type = "float32"

        # Threshold the masked difference band
        targetBand.expression = f"(Permanent_Water_Masked == -9999.0 ? -9999.0 : (Permanent_Water_Masked == 0 ? 0 : (Permanent_Water_Masked > {threshold} ? 1 : 2)))"

        targetBands = jpy.array(
            "org.esa.snap.core.gpf.common.BandMathsOp$BandDescriptor", 1
        )
        targetBands[0] = targetBand
        parameters.put("targetBands", targetBands)

        binary_flood = GPF.createProduct("BandMaths", parameters, product_masked)
        return binary_flood
    except Exception as e:
        raise RuntimeError(f"Terrain correction failed: {e}")

maskPermanentWater(product)

Masks permanent water areas in a SAR product using land cover data.

The function first adds a land cover band to the input product, then creates a binary water mask based on GlobCover classification (210 = water). Finally, it applies the mask to the SAR product, creating a permanent water masked while preserving NoData values.

Parameters:
  • product (Product) –

    Input SAR product.

Returns:
  • esa_snappy.Product: SAR product with permanent water masked, containing a 'Permanent_Water_Masked' band.

Raises:
  • RuntimeError

    If masking or band math operations fail.

Source code in pysarflow/grd.py
def maskPermanentWater(product):
    """
    Masks permanent water areas in a SAR product using land cover data.

    The function first adds a land cover band to the input product, then creates a
    binary water mask based on GlobCover classification (210 = water). Finally, it
    applies the mask to the SAR product, creating a permanent water masked while
    preserving NoData values.

    Args:
        product (esa_snappy.Product): Input SAR product.

    Returns:
        esa_snappy.Product: SAR product with permanent water masked, containing
                            a 'Permanent_Water_Masked' band.

    Raises:
        RuntimeError: If masking or band math operations fail.
    """
    try:
        print("Masking permanent waster based on landcover data")
        # Add land cover band
        parameters = HashMap()
        parameters.put("landCoverNames", "GlobCover")
        mask_with_land_cover = GPF.createProduct("AddLandCover", parameters, product)
        del parameters

        # Create binary water band
        BandDescriptor = jpy.get_type(
            "org.esa.snap.core.gpf.common.BandMathsOp$BandDescriptor"
        )
        parameters = HashMap()
        targetBand = BandDescriptor()
        targetBand.name = "BinaryWater"
        targetBand.type = "uint8"
        targetBand.expression = "(land_cover_GlobCover == 210) ? 0 : 1"
        targetBands = jpy.array(
            "org.esa.snap.core.gpf.common.BandMathsOp$BandDescriptor", 1
        )
        targetBands[0] = targetBand
        parameters.put("targetBands", targetBands)
        water_mask = GPF.createProduct("BandMaths", parameters, mask_with_land_cover)

        del parameters
        parameters = HashMap()
        BandDescriptor = jpy.get_type(
            "org.esa.snap.core.gpf.common.BandMathsOp$BandDescriptor"
        )
        water_mask.addBand(product.getBand("Sigma0_VV"))
        targetBand = BandDescriptor()
        targetBand.name = "Permanent_Water_Masked"
        targetBand.type = "float32"
        targetBand.expression = (
            "(Sigma0_VV == -9999.0) ? -9999.0 : ((BinaryWater == 1) ? Sigma0_VV : 0)"
        )
        # targetBand.expression = '(BinaryWater == 1) ? Sigma0_VV : 0'
        # targetBand.expression = '(BinaryWater == 1) ? Difference_Band : 0'

        targetBands = jpy.array(
            "org.esa.snap.core.gpf.common.BandMathsOp$BandDescriptor", 1
        )
        targetBands[0] = targetBand
        parameters.put("targetBands", targetBands)
        product_masked = GPF.createProduct("BandMaths", parameters, water_mask)
        print("\tMasking permanent water complete.")
        return product_masked
    except Exception as e:
        raise RuntimeError(f"Terrain correction failed: {e}")

plotBand(product1, band_name1, vmin=None, vmax=None, cmap=plt.cm.binary, figsize=(10, 10))

Plots one or two SNAP Product bands side by side. Axes ticks are shown, and each subplot has its own title.

Parameters:
  • product1

    First SNAP Product.

  • band_name1

    Band name for first product.

  • product2

    Optional second SNAP Product.

  • band_name2

    Band name for second product.

  • vmin, (vmax) –

    Colormap limits.

  • cmap

    Colormap.

  • figsize

    Figure size.

Returns:
  • list of matplotlib.image.AxesImage: The image plot objects.

Source code in pysarflow/grd.py
def plotBand(
    product1, band_name1, vmin=None, vmax=None, cmap=plt.cm.binary, figsize=(10, 10)
):
    """
    Plots one or two SNAP Product bands side by side.
    Axes ticks are shown, and each subplot has its own title.

    Args:
        product1: First SNAP Product.
        band_name1: Band name for first product.
        product2: Optional second SNAP Product.
        band_name2: Band name for second product.
        vmin, vmax: Colormap limits.
        cmap: Colormap.
        figsize: Figure size.

    Returns:
        list of matplotlib.image.AxesImage: The image plot objects.
    """

    def get_band_data(product, band_name):
        band = product.getBand(band_name)
        if band is None:
            raise ValueError(f"Band '{band_name}' not found in product.")
        w, h = band.getRasterWidth(), band.getRasterHeight()
        data = np.zeros(w * h, np.float32)
        band.readPixels(0, 0, w, h, data)
        data.shape = (h, w)
        return data

    data1 = get_band_data(product1, band_name1)

    plt.figure(figsize=figsize)
    img1 = plt.imshow(data1, cmap=cmap, vmin=vmin, vmax=vmax)
    plt.xticks()
    plt.yticks()
    plt.title(f"Band: {band_name1}")
    plt.show()
    return [img1]

preprocess_grd_product(file_path, config)

Preprocess a Sentinel-1 GRD product using a given configuration.

The function performs a series of preprocessing steps including AOI subsetting, orbit file application, thermal and border noise removal, radiometric calibration, speckle filtering, and terrain correction.

Parameters:
  • file_path (str) –

    Path to the Sentinel-1 GRD product file.

  • config (dict) –

    Configuration dictionary containing preprocessing parameters, such as: - aoi_bbox (list): Bounding box [minLon, minLat, maxLon, maxLat]. - polarization (str, optional): Polarization to calibrate, e.g., "VV" or "VH". Defaults to "VV". - pols_selected (list, optional): List of polarizations to include. Defaults to None. - speckle_filter (str, optional): Speckle filter type. Defaults to "Lee". - filterSizeX (int or str, optional): Filter kernel size in X direction. Defaults to 5. - filterSizeY (int or str, optional): Filter kernel size in Y direction. Defaults to 5. - demName (str, optional): DEM name for terrain correction. Defaults to "SRTM 3Sec". - pixelSpacingInMeter (float or str, optional): Pixel spacing for terrain correction. Defaults to 10.0. - demResamplingMethod (str, optional): Resampling method for DEM. Defaults to "BILINEAR_INTERPOLATION". - imgResamplingMethod (str, optional): Resampling method for image. Defaults to "BILINEAR_INTERPOLATION".

Returns:
  • esa_snappy.Product: Preprocessed Sentinel-1 product.

Raises:
  • RuntimeError

    If any step of the preprocessing fails.

Source code in pysarflow/grd.py
def preprocess_grd_product(file_path, config):
    """
    Preprocess a Sentinel-1 GRD product using a given configuration.

    The function performs a series of preprocessing steps including AOI subsetting,
    orbit file application, thermal and border noise removal, radiometric calibration,
    speckle filtering, and terrain correction.

    Args:
        file_path (str): Path to the Sentinel-1 GRD product file.
        config (dict): Configuration dictionary containing preprocessing parameters, such as:
            - aoi_bbox (list): Bounding box [minLon, minLat, maxLon, maxLat].
            - polarization (str, optional): Polarization to calibrate,
              e.g., "VV" or "VH". Defaults to "VV".
            - pols_selected (list, optional): List of polarizations to include. Defaults to None.
            - speckle_filter (str, optional): Speckle filter type. Defaults to "Lee".
            - filterSizeX (int or str, optional): Filter kernel size in X direction. Defaults to 5.
            - filterSizeY (int or str, optional): Filter kernel size in Y direction. Defaults to 5.
            - demName (str, optional): DEM name for terrain correction. Defaults to "SRTM 3Sec".
            - pixelSpacingInMeter (float or str, optional): Pixel spacing for terrain correction.
              Defaults to 10.0.
            - demResamplingMethod (str, optional): Resampling method for DEM.
              Defaults to "BILINEAR_INTERPOLATION".
            - imgResamplingMethod (str, optional): Resampling method for image.
              Defaults to "BILINEAR_INTERPOLATION".

    Returns:
        esa_snappy.Product: Preprocessed Sentinel-1 product.

    Raises:
        RuntimeError: If any step of the preprocessing fails.
    """
    try:
        aoi_bbox = config["aoi_bbox"]
        polarization = config.get("polarization", "VV")
        pols_selected = config.get("pols_selected", None)
        filter = config.get("speckle_filter", "Lee")
        filterSizeX = config.get("filterSizeX", "5")
        filterSizeY = config.get("filterSizeY", "5")
        demName = config.get("demName", "SRTM 3Sec")
        pixelSpacingInMeter = config.get("pixelSpacingInMeter", "10.0")
        demResamplingMethod = config.get(
            "demResamplingMethod", "BILINEAR_INTERPOLATION"
        )
        imgResamplingMethod = config.get(
            "imgResamplingMethod", "BILINEAR_INTERPOLATION"
        )

        product = read_grd_product(file_path)

        # 1. Subset AOI (optional)
        if aoi_bbox:
            product = subset_AOI(product=product, bbox=config["aoi_bbox"])

        # 2. Apply corrections
        product = apply_orbit_file(product)
        product = thermal_noise_removal(product)
        product = border_noise_removal(product)

        # 3. Radiometric Calibration
        product = radiometric_calibration(
            product, polarization=polarization, pols_selected=pols_selected
        )

        # 4. Speckle Filter
        product = speckle_filter(
            product,
            filter=filter,
            filterSizeX=filterSizeX,
            filterSizeY=filterSizeY,
        )

        # 5. Terrain Correction
        product = terrain_correction(
            product,
            demName=demName,
            pixelSpacingInMeter=pixelSpacingInMeter,
            demResamplingMethod=demResamplingMethod,
            imgResamplingMethod=imgResamplingMethod,
        )

        return product
    except Exception as e:
        raise RuntimeError(f"Terrain correction failed: {e}")

radiometric_calibration(product, polarization, pols_selected)

Perform radiometric calibration on the SAR product.

Calibration converts the raw SAR data into radar brightness values.

Parameters:
  • product (Product) –

    The input SAR Product object.

  • polarization (str) –

    the desired output polarization type.

  • pols_selected (str) –

    the polarizations to be calibrated

Returns:
  • esa_snappy.Product: The radiometrically calibrated SAR Product object.

Raises:
  • ValueError

    If an unsupported 'polarization' type is provided.

Source code in pysarflow/grd.py
def radiometric_calibration(product, polarization, pols_selected):
    """
    Perform radiometric calibration on the SAR product.

    Calibration converts the raw SAR data into radar brightness values.

    Args:
        product (esa_snappy.Product): The input SAR Product object.
        polarization (str): the desired output polarization type.
        pols_selected (str): the polarizations to be calibrated

    Returns:
        esa_snappy.Product: The radiometrically calibrated SAR Product object.

    Raises:
        ValueError: If an unsupported 'polarization' type is provided.
    """
    print(f"\tRadiometric calibration for polarization(s): {pols_selected}...")
    parameters = HashMap()
    parameters.put("outputSigmaBand", True)
    parameters.put("outputImageScaleInDb", False)  # Output linear scale, not dB

    # Determine source bands based on the input polarization type
    if polarization == "DH":  # Dual-horizontal: HH, HV
        parameters.put("sourceBands", "Intensity_HH,Intensity_HV")
    elif polarization == "DV":  # Dual-vertical: VH, VV
        parameters.put("sourceBands", "Intensity_VH,Intensity_VV")
    elif polarization == "SH" or polarization == "HH":  # Single-horizontal: HH
        parameters.put("sourceBands", "Intensity_HH")
    elif polarization == "SV" or polarization == "VV":  # Single-vertical: VV
        parameters.put("sourceBands", "Intensity_VV")
    else:
        raise ValueError(
            f"Unsupported polarization type: {polarization}. "
            "Please use 'DH', 'DV', 'SH'/'HH', or 'SV'/'VV'."
        )

    # This parameter directly controls which output bands are generated
    parameters.put("selectedPolarisations", pols_selected)

    output = GPF.createProduct("Calibration", parameters, product)
    print("\tRadiometric calibration completed.")
    return output

read_grd_product(file_path)

Read a Sentinel-1 SAR GRD product from a .SAFE directory or a .zip archive.

This function utilizes ESA SNAP's ProductIO.readProduct to load the SAR data into a SNAP Product object, which is the base object for further processing.

Parameters:
  • file_path (str) –

    The file path to the Sentinel-1 .SAFE directory (unzipped) or the .zip archive containing the GRD product.

Returns:
  • esa_snappy.Product: A SNAP Product object representing the loaded SAR data.

Raises:
  • FileNotFoundError

    If the specified zip_path does not exist.

Source code in pysarflow/grd.py
def read_grd_product(file_path):
    """
    Read a Sentinel-1 SAR GRD product from a .SAFE directory or a .zip archive.

    This function utilizes ESA SNAP's `ProductIO.readProduct` to load the SAR data
    into a SNAP Product object, which is the base object for further processing.

    Args:
        file_path (str): The file path to the Sentinel-1 .SAFE directory (unzipped)
                        or the .zip archive containing the GRD product.

    Returns:
        esa_snappy.Product: A SNAP Product object representing the loaded SAR data.

    Raises:
        FileNotFoundError: If the specified zip_path does not exist.
    """
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"Product path not found: {file_path}")

    print(f"Reading SAR product from: {file_path}...")
    product = ProductIO.readProduct(file_path)
    print("\tProduct read successfully.")
    return product

speckle_filter(product, filterSizeY='5', filterSizeX='5', filter='Lee')

Apply speckle filtering to a SAR product using the specified filter parameters.

This function uses the Sentinel-1 toolbox's Speckle-Filter operator via GPF (Graph Processing Framework) to reduce speckle noise in SAR imagery.

Parameters:

product : Product The input SAR product to be filtered.

str, optional

Filter size in the Y direction (default is '5').

str, optional

Filter size in the X direction (default is '5').

str, optional

Type of speckle filter to apply. Common options include "Lee", "Refined Lee", etc. Default is "Lee".

Returns:

Product The filtered SAR product after applying the speckle filter.

Source code in pysarflow/grd.py
def speckle_filter(product, filterSizeY="5", filterSizeX="5", filter="Lee"):
    """Apply speckle filtering to a SAR product using the specified filter parameters.

    This function uses the Sentinel-1 toolbox's Speckle-Filter operator via GPF
    (Graph Processing Framework) to reduce speckle noise in SAR imagery.

    Parameters:
    -----------
    product : Product
        The input SAR product to be filtered.

    filterSizeY : str, optional
        Filter size in the Y direction (default is '5').

    filterSizeX : str, optional
        Filter size in the X direction (default is '5').

    filter : str, optional
        Type of speckle filter to apply. Common options include "Lee", "Refined Lee", etc.
        Default is "Lee".

    Returns:
    --------
    Product
        The filtered SAR product after applying the speckle filter.
    """

    band_name = list(product.getBandNames())
    parameters = HashMap()
    parameters.put("sourceBands", band_name[0])
    parameters.put("filter", filter)
    parameters.put("filterSizeX", filterSizeX)
    parameters.put("filterSizeY", filterSizeY)
    parameters.put("dampingFactor", "2")
    parameters.put("estimateENL", "true")
    parameters.put("enl", "1.0")
    parameters.put("numLooksStr", "1")
    parameters.put("targetWindowSizeStr", "3x3")
    parameters.put("sigmaStr", "0.9")
    parameters.put("anSize", "50")
    speckle_filter_output = GPF.createProduct("Speckle-Filter", parameters, product)
    width = speckle_filter_output.getSceneRasterWidth()
    height = speckle_filter_output.getSceneRasterHeight()

    print("Columns (width):", width)
    print("Rows (height):", height)

    print("\tSpeckle filter completed.")
    return speckle_filter_output

stack(master_product, slave_product)

Collocates two SAR products (master and slave) into a single stacked product.

This creates a new product where the bands from the master and slave products are aligned spatially. Optionally, the master and slave components can be renamed to avoid conflicts.

Parameters:
  • master_product (Product) –

    The reference SAR product.

  • slave_product (Product) –

    The secondary SAR product to

Returns:
  • esa_snappy.Product: A new product containing collocated bands from both master and slave products.

Raises:
  • RuntimeError

    If the Collocate operation fails.

Source code in pysarflow/grd.py
def stack(master_product, slave_product):
    """
    Collocates two SAR products (master and slave) into a single stacked product.

    This creates a new product where the bands from the master and slave products
    are aligned spatially. Optionally, the master and slave components can be renamed
    to avoid conflicts.

    Args:
        master_product (esa_snappy.Product): The reference SAR product.
        slave_product (esa_snappy.Product): The secondary SAR product to
        be collocated with the master.

    Returns:
        esa_snappy.Product: A new product containing collocated bands from both master and slave products.

    Raises:
        RuntimeError: If the Collocate operation fails.
    """
    parameters = HashMap()
    parameters.put("targetProductType", "Collocated")
    parameters.put("resamplingType", "NEAREST_NEIGHBOUR")  # or 'Bilinear', 'Bicubic'
    parameters.put("renameMasterComponents", True)
    parameters.put("renameSlaveComponents", True)

    stacked = GPF.createProduct(
        "Collocate", parameters, [master_product, slave_product]
    )
    return stacked

subset_AOI(product, bbox=[], file_path=None)

Subset the product to a specific AOI to reduce resource usage for large images.

Parameters:
  • product (Product) –

    Input SAR product.

  • bbox (list, default: [] ) –

    Bounding box as [minLon, minLat, maxLon, maxLat].

Returns:
  • esa_snappy.Product: Subsetted product.

Raises:
  • ValueError

    If bbox is None or invalid.

  • Exception

    If both bbox or file_path is not passed

Source code in pysarflow/grd.py
def subset_AOI(product, bbox=[], file_path=None):
    """
    Subset the product to a specific AOI to reduce resource usage for large images.

    Args:
        product (esa_snappy.Product): Input SAR product.
        bbox (list): Bounding box as [minLon, minLat, maxLon, maxLat].

    Returns:
        esa_snappy.Product: Subsetted product.

    Raises:
        ValueError: If bbox is None or invalid.
        Exception: If both bbox or file_path is not passed
    """
    if bbox:
        if len(bbox) != 4:
            raise ValueError("bbox must be a list of [minLon, minLat, maxLon, maxLat]")
        geometry_wkt = (
            f"POLYGON(({bbox[0]} {bbox[1]}, {bbox[2]} {bbox[1]}, "
            f"{bbox[2]} {bbox[3]}, {bbox[0]} {bbox[3]}, {bbox[0]} {bbox[1]}))"
        )
    elif file_path:
        geometry_wkt = extract_bbox(file_path=file_path)
    else:
        raise Exception("Either bbox or file_path should be provided")

    print("\tSubsetting using bounding box:", bbox)

    geometry = WKTReader().read(geometry_wkt)
    HashMap = jpy.get_type("java.util.HashMap")
    GPF.getDefaultInstance().getOperatorSpiRegistry().loadOperatorSpis()
    parameters = HashMap()
    parameters.put("copyMetadata", True)
    parameters.put("geoRegion", geometry)
    output = GPF.createProduct("Subset", parameters, product)

    width = output.getSceneRasterWidth()
    height = output.getSceneRasterHeight()

    print("Columns (width):", width)
    print("Rows (height):", height)

    print("\tProduct subsetted.")
    return output

terrain_correction(product, demName='SRTM 3Sec', pixelSpacingInMeter=10.0, demResamplingMethod='BILINEAR_INTERPOLATION', imgResamplingMethod='BILINEAR_INTERPOLATION')

Performs terrain correction on a SAR product using a specified DEM. This step corrects geometric distortions caused by the side-looking geometry of SAR sensors and normalizes pixel spacing.

Parameters:
  • product

    Product Input SAR product.

  • demName

    str, optional DEM source to use (e.g., 'SRTM 3Sec', 'Copernicus 30m').

  • pixelSpacingInMeter

    float, optional Output pixel spacing in meters. Default is 10.0.

  • demResamplingMethod

    str, optional Resampling method for DEM (e.g., 'NEAREST_NEIGHBOUR', 'BILINEAR_INTERPOLATION', 'CUBIC_CONVOLUTION').

  • imgResamplingMethod

    str, optional Resampling method for image bands (e.g., 'NEAREST_NEIGHBOUR', 'BILINEAR_INTERPOLATION', 'CUBIC_CONVOLUTION').

Returns:
  • esa_snappy.Product: Terrain-corrected product.

Raises:
  • RuntimeError

    If terrain correction fails during processing.

Source code in pysarflow/grd.py
def terrain_correction(
    product,
    demName="SRTM 3Sec",
    pixelSpacingInMeter=10.0,
    demResamplingMethod="BILINEAR_INTERPOLATION",
    imgResamplingMethod="BILINEAR_INTERPOLATION",
):
    """
    Performs terrain correction on a SAR product using a specified DEM.
    This step corrects geometric distortions caused by the side-looking
    geometry of SAR sensors and normalizes pixel spacing.

    Args:
        product : Product
            Input SAR product.
        demName : str, optional
            DEM source to use (e.g., 'SRTM 3Sec', 'Copernicus 30m').
        pixelSpacingInMeter : float, optional
            Output pixel spacing in meters. Default is 10.0.
        demResamplingMethod : str, optional
            Resampling method for DEM
            (e.g., 'NEAREST_NEIGHBOUR', 'BILINEAR_INTERPOLATION', 'CUBIC_CONVOLUTION').
        imgResamplingMethod : str, optional
            Resampling method for image bands
            (e.g., 'NEAREST_NEIGHBOUR', 'BILINEAR_INTERPOLATION', 'CUBIC_CONVOLUTION').

    Returns:
        esa_snappy.Product: Terrain-corrected product.

    Raises:
        RuntimeError: If terrain correction fails during processing.
    """
    try:
        band_names = list(product.getBandNames())
        parameters = HashMap()
        parameters.put("demName", demName)
        parameters.put("nodataValueAtSea", False)
        parameters.put("sourceBands", band_names[0])
        parameters.put("saveDem", False)  # Avoid saving DEM band
        parameters.put("saveLatLon", False)
        parameters.put(
            "imgResamplingMethod", imgResamplingMethod
        )  # Ensure smooth interpolation
        parameters.put(
            "pixelSpacingInMeter", pixelSpacingInMeter
        )  # Ensure smooth interpolation
        parameters.put(
            "demResamplingMethod", demResamplingMethod
        )  # Ensure smooth interpolation
        parameters.put("noDataValue", -9999.0)
        tc_output = GPF.createProduct("Terrain-Correction", parameters, product)
        tc_output = convert_0_to_nan(tc_output)
        # width  = tc_output.getSceneRasterWidth()
        # height = tc_output.getSceneRasterHeight()
        # print("Columns (width):", width)
        # print("Rows (height):", height)
        print("\tTerrain correction completed.")
        return tc_output
    except Exception as e:
        raise RuntimeError(f"Terrain correction failed: {e}")

thermal_noise_removal(product)

Remove thermal noise from the SAR product.

Thermal noise is a constant noise floor that affects SAR images, especially in low-backscatter areas. This step removes this noise, improving the signal-to-noise ratio.

Parameters:
  • product (Product) –

    The input SAR Product object.

Returns:
  • esa_snappy.Product: The product after thermal noise removal.

Source code in pysarflow/grd.py
def thermal_noise_removal(product):
    """
    Remove thermal noise from the SAR product.

    Thermal noise is a constant noise floor that affects SAR images, especially
    in low-backscatter areas. This step removes this noise, improving the signal-to-noise ratio.

    Args:
        product (esa_snappy.Product): The input SAR Product object.

    Returns:
        esa_snappy.Product: The product after thermal noise removal.
    """
    print("\tPerforming thermal noise removal...")
    parameters = HashMap()
    parameters.put("removeThermalNoise", True)
    output = GPF.createProduct("ThermalNoiseRemoval", parameters, product)
    print("\tThermal noise removed.")
    return output

Preprocessing Sentinel1 SLC Data

This Python script enables users to perform essential preprocessing steps on Sentinel-1 SLC data, including thermal noise removal, radiometric calibration, and terrain correction.

It accepts Sentinel-1 products in the .SAFE format.

The script depends on the 'esa_snappy' library, which must be installed in the Python environment where this script is executed.

This file can also be imported as a module and contains the following functions

Major: * read_slc_product Supporting: *

apply_orbit(product, orbit_type='Sentinel Precise (Auto Download)')

Run SNAP 'Apply-Orbit-File' on a Product.

Parameters:
  • product

    Input product to process. Ouput of TOPSAR-split

  • orbit_type

    The orbit source/type to use. Common values include: - "Sentinel Precise (Auto Download)" – preferred. - "Sentinel Restituted (Auto Download)" – fallback if precise is not yet available. - "DORIS Precise VOR (ENVISAT)" – for other missions.

Returns:
  • Product

    A new SNAP Product with orbits applied.

Source code in pysarflow/slc.py
def apply_orbit(product,
                orbit_type="Sentinel Precise (Auto Download)"):
    """
    Run SNAP 'Apply-Orbit-File' on a Product.

    Args:
        product:
            Input product to process. Ouput of TOPSAR-split
        orbit_type:
            The orbit source/type to use. Common values include:
              - `"Sentinel Precise (Auto Download)"` – preferred.
              - `"Sentinel Restituted (Auto Download)"` – fallback if precise is not yet available.
              - `"DORIS Precise VOR (ENVISAT)"` – for other missions.

    Returns:
        Product: A new SNAP `Product` with orbits applied.
    """
    Boolean = jpy.get_type('java.lang.Boolean')
    Integer = jpy.get_type('java.lang.Integer')

    parameters = HashMap()
    parameters.put("orbitType", orbit_type)
    parameters.put("polyDegree", Integer(3))
    parameters.put("continueOnFail", Boolean(True))

    out = GPF.createProduct("Apply-Orbit-File", parameters, product)
    print(f"Apply-Orbit-File: {orbit_type}")
    return out

back_geocoding(products, dem_name='SRTM 1Sec HGT', ext_dem=None)

Run SNAP Back-Geocoding on master + slave(s).

Parameters:
  • master ( ) –

    SNAP Product (Apply-Orbit-File already done). Output of the apply orbit

  • slaves ( ) –

    list of SNAP Products (Apply-Orbit-File already done). Output of the apply orbit

  • dem_name

    name of DEM in SNAP auxdata (default SRTM 1Sec HGT)

  • ext_dem ( , default: None ) –

    optional external DEM product

Returns:
  • SNAP Product with master + co-registered slave or slaves

Source code in pysarflow/slc.py
def back_geocoding(products, dem_name="SRTM 1Sec HGT", ext_dem=None):
    """
    Run SNAP Back-Geocoding on master + slave(s).

    Args:
        master   : SNAP Product (Apply-Orbit-File already done). Output of the apply orbit
        slaves   : list of SNAP Products (Apply-Orbit-File already done). Output of the apply orbit 
        dem_name : name of DEM in SNAP auxdata (default SRTM 1Sec HGT)
        ext_dem  : optional external DEM product

    Returns:
        SNAP Product with master + co-registered slave or slaves
    """
    print("Running Back-Geocoding...")

    parameters = HashMap()
    parameters.put("demName", dem_name)
    parameters.put("demResamplingMethod", "BILINEAR_INTERPOLATION")
    parameters.put("resamplingType", "BILINEAR_INTERPOLATION")
    parameters.put("maskOutAreaWithoutElevation", True)
    parameters.put("outputDerampDemodPhase", True)
    parameters.put("disableReramp", False)


    if ext_dem is not None:
        parameters.put("externalDEMFile", ext_dem)

    output = GPF.createProduct("Back-Geocoding", parameters, products) 
    print("Back geocoding applied!")
    return output

burst_for_geometry(product, safe_dir, geom, subswath=None)

Determine TOPS burst index (or range) for a geometry in a Sentinel-1 IW SLC.

Parameters:
  • product ( ) –

    Input SAR product.

  • safe_dir ( ) –

    path to the *.SAFE folder (str or Path)

  • geom ( ) –

    Shapely Point/Polygon, or WKT string, or bbox tuple (minlon,minlat,maxlon,maxlat)

  • subswath ( , default: None ) –

    optional 'IW1'|'IW2'|'IW3' to force a swath

Returns (dict): { 'swath': 'IW1'|'IW2'|'IW3', 'band_name': 'Intensity_IW1_VV', # band used 'linesPerBurst': 1495, 'numberOfBursts': 10, 'burst': 5, # for Point 'firstBurst': 4, # for Polygon 'lastBurst': 6, # for Polygon 'geom_type': 'Point'|'Polygon' }

Raises:

ValueError: 
- If geom is not a Shapely Point/Polygon, a WKT string, or a 4-element bbox ``(minx, miny, maxx, maxy)``
- If the geometry lies outside the products IW1/IW2/IW3 coverage.
- If the AOI polygon does not intersect the chosen sub-swath.

FileNotFoundError:
- If no annotation XML file is found for the selected sub-swath
Source code in pysarflow/slc.py
def burst_for_geometry(product, safe_dir, geom, subswath=None):
    """
    Determine TOPS burst index (or range) for a geometry in a Sentinel-1 IW SLC.

    Args:
      product   : Input SAR product.
      safe_dir  : path to the *.SAFE folder (str or Path)
      geom      : Shapely Point/Polygon, or WKT string, or bbox tuple (minlon,minlat,maxlon,maxlat)
      subswath  : optional 'IW1'|'IW2'|'IW3' to force a swath

    Returns (dict):
      {
        'swath': 'IW1'|'IW2'|'IW3',
        'band_name': 'Intensity_IW1_VV',   # band used
        'linesPerBurst': 1495,
        'numberOfBursts': 10,
        'burst': 5,              # for Point
        'firstBurst': 4,         # for Polygon
        'lastBurst': 6,          # for Polygon
        'geom_type': 'Point'|'Polygon'
      }

    Raises:

        ValueError: 
        - If geom is not a Shapely Point/Polygon, a WKT string, or a 4-element bbox ``(minx, miny, maxx, maxy)``
        - If the geometry lies outside the products IW1/IW2/IW3 coverage.
        - If the AOI polygon does not intersect the chosen sub-swath.

        FileNotFoundError:
        - If no annotation XML file is found for the selected sub-swath

    """

    # --- normalize geometry ---
    if isinstance(geom, (list, tuple)) and len(geom) == 4:
        geom = box(*geom)
    elif isinstance(geom, str):
        geom = _wkt.loads(geom)
    elif not isinstance(geom, (Point, Polygon)):
        raise ValueError("geom must be Point/Polygon, WKT string, or bbox tuple")

    is_point = isinstance(geom, Point)
    aoi = geom if not is_point else geom

    names = list(product.getBandNames())

    # --- pick a band that actually covers the geometry (auto-detect swath if needed) ---
    def band_contains_point(band, lat, lon):
        gc = band.getGeoCoding()
        if gc is None: return False, None
        pp = gc.getPixelPos(GeoPos(float(lat), float(lon)), None)
        ok = (pp is not None and math.isfinite(pp.x) and math.isfinite(pp.y)
              and 0 <= pp.x < band.getRasterWidth()
              and 0 <= pp.y < band.getRasterHeight())
        return ok, pp

    # center we’ll use to probe coverage
    probe = (aoi.y, aoi.x) if is_point else (aoi.centroid.y, aoi.centroid.x)

    sw_order = [subswath] if subswath in ("IW1", "IW2", "IW3") else ["IW1", "IW2", "IW3"]
    chosen_sw, chosen_band_name, chosen_pp = None, None, None

    for sw in sw_order:
        cands = [n for n in names if sw in n]
        for name in cands:
            ok, pp = band_contains_point(product.getBand(name), *probe)
            if ok:
                chosen_sw, chosen_band_name, chosen_pp = sw, name, pp
                break
        if chosen_sw:
            break

    if chosen_sw is None:
        raise ValueError("Geometry is outside IW1/IW2/IW3 coverage for this product.")

    band = product.getBand(chosen_band_name)
    gc = band.getGeoCoding()
    W, H = band.getRasterWidth(), band.getRasterHeight()

    # --- read annotation XML for this swath (namespace-agnostic) ---
    safe_dir = Path(safe_dir)
    ann_dir = safe_dir / "annotation"
    xml_files = sorted(ann_dir.glob(f"*{chosen_sw.lower()}*.xml"))
    if not xml_files:
        raise FileNotFoundError(f"No annotation XML found for {chosen_sw} in {ann_dir}")
    root = ET.parse(xml_files[0]).getroot()

    def find_text_any(root, local):
        for el in root.iter():
            if el.tag.rsplit('}', 1)[-1] == local and el.text:
                return el.text.strip()
        return None

    txt_lpb = find_text_any(root, "linesPerBurst")
    bursts_nodes = [el for el in root.iter() if el.tag.rsplit('}', 1)[-1] == "burst"]
    numberOfBursts = int(find_text_any(root, "numberOfBursts")) if find_text_any(root, "numberOfBursts") else len(bursts_nodes)

    if txt_lpb:
        linesPerBurst = int(txt_lpb)
    else:
        # try infer from firstAzimuthLine step, else fallback to H / numBursts
        first_lines = []
        for b in bursts_nodes[:2]:
            for el in b.iter():
                if el.tag.rsplit('}', 1)[-1] == "firstAzimuthLine" and el.text:
                    first_lines.append(int(el.text)); break
        linesPerBurst = max(1, (first_lines[1] - first_lines[0]) if len(first_lines) == 2 else H // max(1, numberOfBursts))

    out = {
        "sub-swath": chosen_sw,
        "band_name": chosen_band_name,
        "linesPerBurst": linesPerBurst,
        "numberOfBursts": numberOfBursts,
        "geom_type": "Point" if is_point else "Polygon",
    }

    # --- map geometry to pixel rows and compute burst index / range ---
    def clamp_b(b): return max(1, min(numberOfBursts, b))

    if is_point:
        y = int(chosen_pp.y)
        out["burst"] = clamp_b(y // linesPerBurst + 1)
    else:
        # use exterior vertices (and centroid) to get min/max valid rows
        ys = []
        for lon, lat in list(aoi.exterior.coords) + [(aoi.centroid.x, aoi.centroid.y)]:
            pp = gc.getPixelPos(GeoPos(float(lat), float(lon)), None)
            if (pp is not None and math.isfinite(pp.y) and 0 <= pp.y < H):
                ys.append(pp.y)
        if not ys:
            raise ValueError("AOI polygon does not intersect the chosen sub-swath.")
        y_min, y_max = min(ys), max(ys)
        out["firstBurst"] = clamp_b(int(y_min) // linesPerBurst + 1)
        out["lastBurst"]  = clamp_b(int(y_max) // linesPerBurst + 1)

    return out

enhanced_spectral_diversity(product, preset='default', **overrides)

Enhanced Spectral Diversity (ESD) with sensible defaults + optional overrides.

Args: product: A SNAP Product — usually the output of Back-Geocoding containing the master and one or more slave images. preset: Convenience preset passed to esd_parameters(preset) that supplies a default parameter set. Expected values: - "default" - full computation. - "fast" – less lighter computation. - "faster" – lighter computation. **overrides: Any ESD operator parameter you want to force/override from the preset (e.g., cohWinAz=5, cohWinRg=10, maxIterations=25, etc.). Keys must match the operator's parameter names.

Returns:
  • Product

    A new SNAP Product with ESD refinement applied.

Source code in pysarflow/slc.py
def enhanced_spectral_diversity(product, preset="default", **overrides):
    """
    Enhanced Spectral Diversity (ESD) with sensible defaults + optional overrides.

     Args:
        product:
            A SNAP `Product` — usually the output of Back-Geocoding
            containing the master and one or more slave images.
        preset:
            Convenience preset passed to `esd_parameters(preset)` that supplies a
            default parameter set. Expected values:
            - `"default"` - full computation.
            - `"fast"`  – less lighter computation.
            - `"faster"`    – lighter computation.
        **overrides:
            Any ESD operator parameter you want to force/override from the
            preset (e.g., `cohWinAz=5`, `cohWinRg=10`, `maxIterations=25`,
            etc.). Keys must match the operator's parameter names.

    Returns:
        Product: A new SNAP `Product` with ESD refinement applied.
    """
    parameters = HashMap()
    for k, v in esd_parameters(preset).items():
        parameters.put(k, v)
    # user overrides win if provided
    for k, v in overrides.items():
        parameters.put(k, v)
    esd = GPF.createProduct("Enhanced-Spectral-Diversity", parameters, product)
    return esd

goldstein_phase_filtering(product)

Apply Goldstein Phase Filtering to an interferogram.

Goldstein filtering is used in InSAR processing to enhance the signal-to-noise ratio of the interferometric phase. This filter suppresses noise while preserving the phase fringes, improving the quality of unwrapping and subsequent deformation analysis.

Args:

product : org.esa.snap.core.datamodel.Product The input product containing the interferometric phase, typically the output of the "Interferogram" operator.

Returns:

output : org.esa.snap.core.datamodel.Product The product with Goldstein phase filtering applied.

Notes:

  • alpha: Controls the filtering strength. Higher values result in stronger filtering (default is 1.0).
  • FFTSizeString: Defines the FFT window size used for filtering (default is '64').
  • windowSizeString: Defines the size of the filtering window (default is '3').
  • useCoherenceMask: If set to True, filtering is applied only where coherence exceeds the given threshold.
  • coherenceThreshold: Minimum coherence value used if coherence masking is enabled (default is 0.2, but ignored if useCoherenceMask is False).

This function is typically used after interferogram generation and before phase unwrapping in an InSAR processing chain.

Source code in pysarflow/slc.py
def goldstein_phase_filtering(product):
    """
    Apply Goldstein Phase Filtering to an interferogram.

    Goldstein filtering is used in InSAR processing to enhance the signal-to-noise 
    ratio of the interferometric phase. This filter suppresses noise while preserving 
    the phase fringes, improving the quality of unwrapping and subsequent deformation 
    analysis.

    Args:
    ----------
    product : org.esa.snap.core.datamodel.Product
        The input product containing the interferometric phase, typically the 
        output of the "Interferogram" operator.

    Returns:
    -------
    output : org.esa.snap.core.datamodel.Product
        The product with Goldstein phase filtering applied.

    Notes:
    -----
    - `alpha`: Controls the filtering strength. Higher values result in stronger 
      filtering (default is 1.0).
    - `FFTSizeString`: Defines the FFT window size used for filtering (default is '64').
    - `windowSizeString`: Defines the size of the filtering window (default is '3').
    - `useCoherenceMask`: If set to True, filtering is applied only where coherence 
      exceeds the given threshold.
    - `coherenceThreshold`: Minimum coherence value used if coherence masking is enabled 
      (default is 0.2, but ignored if `useCoherenceMask` is False).

    This function is typically used after interferogram generation and before phase 
    unwrapping in an InSAR processing chain.
    """
    parameters = HashMap()
    print('Apply Goldstein Phase Filtering...')
    parameters.put('alpha', 1.0)
    parameters.put('FFTSizeString', '64')
    parameters.put('windowSizeString', '3')
    parameters.put('useCoherenceMask', False)
    parameters.put('coherenceThreshold', 0.2)  
    output = GPF.createProduct("GoldsteinPhaseFiltering", parameters, product)
    print("Goldstein Phase Filtering applied!")
    return output

interferogram(product)

Generate an interferogram from a Sentinel-1 interferometric product.

This function uses the SNAP Graph Processing Framework (GPF) to create an interferogram, which represents the phase difference between two co-registered SAR images. The interferogram is an essential step in interferometric SAR (InSAR) processing for deriving surface deformation, elevation models, or coherence analysis.

Args:

product : org.esa.snap.core.datamodel.Product The co-registered Sentinel-1 product (usually the output of the "Back-Geocoding" operator with two coregistered images).

Returns:

output : org.esa.snap.core.datamodel.Product The product containing the generated interferogram and optional coherence band.

Notes:

  • The function uses predefined parameters for flat-earth phase removal, polynomial fitting, and orbit interpolation.
  • Coherence estimation is enabled by default.
  • Pixel size is set to be square.
  • Uncomment and customize the window size parameters if you want to control the coherence estimation resolution.
  • This function is typically followed by Goldstein filtering and phase unwrapping steps in an InSAR workflow.
Source code in pysarflow/slc.py
def interferogram(product):
    """
    Generate an interferogram from a Sentinel-1 interferometric product.

    This function uses the SNAP Graph Processing Framework (GPF) to create 
    an interferogram, which represents the phase difference between two 
    co-registered SAR images. The interferogram is an essential step in 
    interferometric SAR (InSAR) processing for deriving surface deformation, 
    elevation models, or coherence analysis.

    Args:
    ----------
    product : org.esa.snap.core.datamodel.Product
        The co-registered Sentinel-1 product (usually the output of the 
        "Back-Geocoding" operator with two coregistered images).

    Returns:
    -------
    output : org.esa.snap.core.datamodel.Product
        The product containing the generated interferogram and optional 
        coherence band.

    Notes:
    -----
    - The function uses predefined parameters for flat-earth phase removal, 
      polynomial fitting, and orbit interpolation.
    - Coherence estimation is enabled by default.
    - Pixel size is set to be square.
    - Uncomment and customize the window size parameters if you want to control 
      the coherence estimation resolution.
    - This function is typically followed by Goldstein filtering and phase 
      unwrapping steps in an InSAR workflow.
    """
    parameters = HashMap()
    print('Creating interferogram ...')
    parameters.put("Subtract flat-earth phase", True)
    parameters.put("Degree of \"Flat Earth\" polynomial", 5)
    parameters.put("Number of \"Flat Earth\" estimation points", 501)
    parameters.put("Orbit interpolation degree", 3)
    parameters.put("Include coherence estimation", True)
    parameters.put("Square Pixel", True)
    parameters.put("Independent Window Sizes", False)
    #parameters.put("Coherence Azimuth Window Size", 10)
    #parameters.put("Coherence Range Window Size", 2)
    output = GPF.createProduct("Interferogram", parameters, product) 
    print("Interferogram created!")
    return output

multilooking(product, n_rg=3, n_az=1, source_bands=None, output_intensity=False)

SNAP 'Multilook' operator.

Parameters:
  • product ( ) –

    SNAP Product (typically AFTER TOPSAR-Deburst)

  • n_rg (int) , default: 3 ) –

    number of looks in range (speckle ↓, res ↓)

  • n_az (int) , default: 1 ) –

    number of looks in azimuth

  • source_bands ( , default: None ) –

    optional list/CSV of bands to process (e.g. 'i_,q_,coh_*')

  • output_intensity

    for complex inputs, also write intensity bands

Returns:
  • SNAP Product with multilooked bands

Source code in pysarflow/slc.py
def multilooking(product, n_rg=3, n_az=1, source_bands=None, output_intensity=False):
    """
    SNAP 'Multilook' operator.

    Args:
      product         : SNAP Product (typically AFTER TOPSAR-Deburst)
      n_rg (int)      : number of looks in range (speckle ↓, res ↓)
      n_az (int)      : number of looks in azimuth
      source_bands    : optional list/CSV of bands to process (e.g. 'i_*,q_*,coh_*')
      output_intensity: for complex inputs, also write intensity bands

    Returns:
      SNAP Product with multilooked bands
    """
    Integer = jpy.get_type('java.lang.Integer')
    Boolean = jpy.get_type('java.lang.Boolean')

    parameters = HashMap()
    parameters.put('nRgLooks', Integer(n_rg))
    parameters.put('nAzLooks', Integer(n_az))
    parameters.put('outputIntensity', Boolean(output_intensity))
    if source_bands:
        if isinstance(source_bands, (list, tuple)):
            source_bands = ",".join(source_bands)
        parameters.put('sourceBands', str(source_bands))

    return GPF.createProduct('Multilook', parameters, product)

phase_to_elevation(product, DEM)

Convert unwrapped interferometric phase to elevation using a Digital Elevation Model (DEM).

This function uses the SNAP Graph Processing Framework (GPF) to apply the "Phase to Elevation" operator. It transforms unwrapped phase data into an elevation map by referencing a known DEM. This step is commonly used in Differential InSAR (DInSAR) or when generating DEMs from SAR data.

Args:

product : org.esa.snap.core.datamodel.Product The input product containing unwrapped interferometric phase, typically the result of a phase unwrapping operator.

str

The name of the DEM to use for reference (e.g., 'SRTM 3Sec', 'Copernicus 30m', or path to an external DEM file).

Returns:

output : org.esa.snap.core.datamodel.Product The product containing the elevation map derived from phase data.

Notes:

  • The DEM is used to geocode the elevation output and aid in the transformation.
  • Bilinear interpolation is used to resample the DEM for better accuracy.
  • externalDEMNoDataValue is set to 0.0, which defines how to handle no-data pixels in the DEM.
  • This step assumes the input phase has already been unwrapped and filtered.
Source code in pysarflow/slc.py
def phase_to_elevation(product, DEM):
    """
    Convert unwrapped interferometric phase to elevation using a Digital Elevation Model (DEM).

    This function uses the SNAP Graph Processing Framework (GPF) to apply the 
    "Phase to Elevation" operator. It transforms unwrapped phase data into an 
    elevation map by referencing a known DEM. This step is commonly used in 
    Differential InSAR (DInSAR) or when generating DEMs from SAR data.

    Args:
    ----------
    product : org.esa.snap.core.datamodel.Product
        The input product containing unwrapped interferometric phase, typically 
        the result of a phase unwrapping operator.

    DEM : str
        The name of the DEM to use for reference (e.g., 'SRTM 3Sec', 'Copernicus 30m', 
        or path to an external DEM file).

    Returns:
    -------
    output : org.esa.snap.core.datamodel.Product
        The product containing the elevation map derived from phase data.

    Notes:
    -----
    - The DEM is used to geocode the elevation output and aid in the transformation.
    - Bilinear interpolation is used to resample the DEM for better accuracy.
    - `externalDEMNoDataValue` is set to 0.0, which defines how to handle no-data pixels 
      in the DEM.
    - This step assumes the input phase has already been unwrapped and filtered.
    """

    parameters = HashMap()
    print('Turning Phase to Elevation...')
    parameters.put('demName', DEM)
    parameters.put('demResamplingMethod', 'BILINEAR_INTERPOLATION')
    parameters.put('externalDEMNoDataValue', 0.0)
    output = GPF.createProduct("PhaseToElevation", parameters, product)
    print("Phase to Elevation applied!")
    return output

plot(dim_path, i_band=None, q_band=None, coh_band=None, downsample=1, fade=(0.2, 0.8), title='', save_path=None, return_rgb=False, ax=None)

Visualize an interferogram as SNAP-like rainbow (HSV). This can be used to plot any interferogram outputs

Parameters:
  • dim_path ( ) –

    path to the .dim file (next to the .data/ folder)

  • i_band ( , default: None ) –

    name of the real (i) interferogram band; auto-detected if None

  • q_band ( , default: None ) –

    name of the imag (q) interferogram band; auto-detected if None

  • coh_band ( , default: None ) –

    name of the coherence band; auto-detected if None

  • downsample

    integer stride for quick viewing (e.g., 2, 4)

  • fade ( , default: (0.2, 0.8) ) –

    tuple (v_min, v_max) mapping coherence → value (brightness)

  • title ( , default: '' ) –

    plot title

  • save_path ( , default: None ) –

    if set, write the RGB to this path (e.g., 'phase.png')

  • return_rgb

    if True, return the RGB numpy array

  • ax ( , default: None ) –

    optional matplotlib axes to draw on

Returns:
  • rgb (H,W,3) array if return_rgb=True, else None.

Source code in pysarflow/slc.py
def plot(
    dim_path,
    i_band=None, q_band=None, coh_band=None,
    downsample=1,
    fade=(0.2, 0.8),          # (min,max) brightness from coherence
    title="",
    save_path=None,
    return_rgb=False,
    ax=None,
):
    """
    Visualize an interferogram as SNAP-like rainbow (HSV). This can be used to plot any interferogram outputs

    Args:
      dim_path   : path to the .dim file (next to the .data/ folder)
      i_band     : name of the real (i) interferogram band; auto-detected if None
      q_band     : name of the imag (q) interferogram band; auto-detected if None
      coh_band   : name of the coherence band; auto-detected if None
      downsample : integer stride for quick viewing (e.g., 2, 4)
      fade       : tuple (v_min, v_max) mapping coherence → value (brightness)
      title      : plot title
      save_path  : if set, write the RGB to this path (e.g., 'phase.png')
      return_rgb : if True, return the RGB numpy array
      ax         : optional matplotlib axes to draw on

    Returns:
      rgb (H,W,3) array if return_rgb=True, else None.
    """
    p = ProductIO.readProduct(dim_path)
    try:
        # --- band auto-detect (if names not given) ---
        names = list(p.getBandNames())
        def pick(prefix):
            for n in names:
                nn = n.lower()
                if nn.startswith(prefix):  # strict startswith
                    return n
            for n in names:                 # fallback: contains
                if prefix in n.lower():
                    return n
            return None

        i_band  = i_band  or pick('i_ifg')
        q_band  = q_band  or pick('q_ifg')
        coh_band = coh_band or pick('coh_')

        if not (i_band and q_band and coh_band):
            raise ValueError(
                f"Could not find required bands. "
                f"i_band={i_band}, q_band={q_band}, coh_band={coh_band}. "
                f"Available: {names[:12]}{' ...' if len(names)>12 else ''}"
            )

        w, h = p.getSceneRasterWidth(), p.getSceneRasterHeight()
        buf = np.zeros(w*h, np.float32)

        bi = p.getBand(i_band); bi.readPixels(0,0,w,h,buf); i = buf.reshape(h,w).copy()
        bq = p.getBand(q_band); bq.readPixels(0,0,w,h,buf); q = buf.reshape(h,w).copy()
        bc = p.getBand(coh_band); bc.readPixels(0,0,w,h,buf); coh = buf.reshape(h,w).copy()

        if downsample and downsample > 1:
            s = slice(None, None, int(downsample))
            i, q, coh = i[s, s], q[s, s], coh[s, s]

        # --- phase → HSV ---
        phase = np.arctan2(q, i)                         # [-pi, +pi]
        hue   = (phase + np.pi) / (2*np.pi)              # [0,1]
        sat   = np.ones_like(hue)
        vmin, vmax = fade
        val   = vmin + (vmax - vmin) * np.clip(coh, 0, 1)

        rgb = hsv_to_rgb(np.dstack([hue, sat, val]))

        # --- plot ---
        if ax is None:
            plt.figure(figsize=(10, 6))
            ax = plt.gca()
        ax.imshow(rgb, origin="upper")
        ax.set_xticks([]); ax.set_yticks([])
        ax.set_title(title)

        if save_path:
            # matplotlib expects 0..1 floats; rgb already is
            plt.imsave(save_path, rgb)

        return rgb if return_rgb else None

    finally:
        try:
            p.dispose()
        except Exception:
             pass

read_slc_product(product_path)

Reads a Sentinel-1 SLC product using SNAP's ProductIO.

This function checks whether the provided product path exists on disk, then attempts to load the product using ProductIO.readProduct. If the product cannot be read, it raises a RuntimeError with details about the failure.

Parameters:
  • product_path (str) –

    Path to the Sentinel-1 SAFE format product directory or file.

Returns:
  • product

    The SNAP product object representing the loaded Sentinel-1 data.

Raises:
  • FileNotFoundError

    If the specified product path does not exist.

  • RuntimeError

    If reading the product fails due to unexpected errors.

Source code in pysarflow/slc.py
def read_slc_product(product_path):
    """
    Reads a Sentinel-1 SLC product using SNAP's ProductIO.

    This function checks whether the provided product path exists on disk, then
    attempts to load the product using ProductIO.readProduct. If the product
    cannot be read, it raises a RuntimeError with details about the failure.

    Args:
        product_path (str): Path to the Sentinel-1 SAFE format product directory or file.

    Returns:
        product: The SNAP product object representing the loaded Sentinel-1 data.

    Raises:
        FileNotFoundError: If the specified product path does not exist.
        RuntimeError: If reading the product fails due to unexpected errors.
    """
    try:
        if not os.path.exists(product_path):
            raise FileNotFoundError(f"Product path does not exist: {product_path}")
        product = ProductIO.readProduct(product_path)
        return product
    except Exception as e:
        raise RuntimeError(
            f"An error occurred while reading the slc product: {str(e)}"
        ) from e

save_product(product, filename, output_dir='_results', fmt='BEAM-DIMAP')

Save a SNAP product to disk.

Args

product : snappy.Product The SNAP product to save. filename : str Output filename (without extension). output_dir : str, optional Directory where results will be saved (default: "_results"). fmt : str, optional Output format (default: "BEAM-DIMAP").

Returns

str The full output path where the product was saved.

Source code in pysarflow/slc.py
def save_product(product, filename, output_dir="_results", fmt="BEAM-DIMAP"):
    """
    Save a SNAP product to disk.

    Args
    ----------
    product : snappy.Product
        The SNAP product to save.
    filename : str
        Output filename (without extension).
    output_dir : str, optional
        Directory where results will be saved (default: "_results").
    fmt : str, optional
        Output format (default: "BEAM-DIMAP").

    Returns
    -------
    str
        The full output path where the product was saved.
    """
    out_path = f"{output_dir}/{filename}"
    print(f"Saving product to {out_path} ({fmt})...")
    ProductIO.writeProduct(product, out_path, fmt)
    print("Product saved successfully.")
    return out_path

snaphu_export(xml_path, new_input_file, new_target_folder)

Modify a SNAP XML graph to update input and output paths, then execute it using GPT.

This function updates the SNAP XML workflow file by: - Replacing the file path in the 'Read' node with the given input file. - Replacing the target folder in the 'SnaphuExport' node with the given output folder. After updating, it saves the modified XML and runs the graph using the gpt command-line tool.

Parameters

xml_path : str Path to the SNAP XML graph file to be modified and executed. new_input_file : str Path to the new input file (to replace the one in the 'Read' node). new_target_folder : str Path to the new target folder (to replace the one in the 'SnaphuExport' node).

Returns

None The function prints progress updates and the output of the gpt execution.

Raises

FileNotFoundError If the provided xml_path does not exist. xml.etree.ElementTree.ParseError If the XML file cannot be parsed. subprocess.CalledProcessError If the gpt command execution fails.

Notes

  • Requires SNAP's Graph Processing Tool (gpt) to be installed and available in the system PATH.
  • The XML graph must contain nodes with IDs 'Read' and 'SnaphuExport' for the function to work correctly.
Source code in pysarflow/slc.py
def snaphu_export(xml_path, new_input_file, new_target_folder):
    """
    Modify a SNAP XML graph to update input and output paths, then execute it using GPT.

    This function updates the SNAP XML workflow file by:
    - Replacing the file path in the 'Read' node with the given input file.
    - Replacing the target folder in the 'SnaphuExport' node with the given output folder.
    After updating, it saves the modified XML and runs the graph using the `gpt` command-line tool.

    Parameters
    ----------
    xml_path : str
        Path to the SNAP XML graph file to be modified and executed.
    new_input_file : str
        Path to the new input file (to replace the one in the 'Read' node).
    new_target_folder : str
        Path to the new target folder (to replace the one in the 'SnaphuExport' node).

    Returns
    -------
    None
        The function prints progress updates and the output of the `gpt` execution.

    Raises
    ------
    FileNotFoundError
        If the provided `xml_path` does not exist.
    xml.etree.ElementTree.ParseError
        If the XML file cannot be parsed.
    subprocess.CalledProcessError
        If the `gpt` command execution fails.

    Notes
    -----
    - Requires SNAP's Graph Processing Tool (`gpt`) to be installed and available in the system PATH.
    - The XML graph must contain nodes with IDs 'Read' and 'SnaphuExport' for the function to work correctly.
    """
    print("Snaphu exporting...")
    tree = ET.parse(xml_path)
    root = tree.getroot()

    for node in root.findall(".//node[@id='Read']/parameters/file"):
        node.text = new_input_file

    for node in root.findall(".//node[@id='SnaphuExport']/parameters/targetFolder"):
        node.text = new_target_folder

    tree.write(xml_path, encoding="UTF-8", xml_declaration=True)

    gpt_command = "gpt"  
    result = subprocess.run([gpt_command, xml_path], capture_output=True, text=True, check=True)
    print("Processing complete.\nOutput:\n", result.stdout)

snaphu_import(source_product, unwrapped_product)

Import SNAPHU unwrapped interferogram results back into SNAP.

This function uses SNAP's Graph Processing Framework (GPF) to merge the unwrapped interferogram produced by SNAPHU with the original source product. The resulting product contains the unwrapped phase information, making it available for further processing within SNAP.

Parameters

source_product : snappy.Product The original interferogram product before SNAPHU unwrapping. unwrapped_product : snappy.Product The product containing the SNAPHU unwrapped interferogram.

Returns

snappy.Product A SNAP product containing the imported unwrapped phase data.

Notes

  • The parameter 'doNotKeepWrapped' is set to False to keep the wrapped phase along with the unwrapped phase.
  • Requires SNAP's snappy module and the GPF operator 'SnaphuImport'.
Source code in pysarflow/slc.py
def snaphu_import(source_product, unwrapped_product): 
    """
    Import SNAPHU unwrapped interferogram results back into SNAP.

    This function uses SNAP's Graph Processing Framework (GPF) to merge the 
    unwrapped interferogram produced by SNAPHU with the original source product. 
    The resulting product contains the unwrapped phase information, making it 
    available for further processing within SNAP.

    Parameters
    ----------
    source_product : snappy.Product
        The original interferogram product before SNAPHU unwrapping.
    unwrapped_product : snappy.Product
        The product containing the SNAPHU unwrapped interferogram.

    Returns
    -------
    snappy.Product
        A SNAP product containing the imported unwrapped phase data.

    Notes
    -----
    - The parameter `'doNotKeepWrapped'` is set to False to keep the wrapped phase 
      along with the unwrapped phase.
    - Requires SNAP's snappy module and the GPF operator `'SnaphuImport'`.
    """ 
    parameters = HashMap()
    print("SNAPHU importing...")
    parameters.put('doNotKeepWrapped', False)
    products = [source_product, unwrapped_product]
    output = GPF.createProduct('SnaphuImport', parameters, products)
    print("SNAPHU imported...")
    return output  

snaphu_unwrapping(conf_file_path, snaphu_exe_path, output_directory)

Run the SNAPHU unwrapping process using a configuration file.

This function reads a SNAPHU configuration file, extracts the command line arguments, and executes the SNAPHU binary with those arguments in the specified output directory. It ensures the working directory exists before execution and reports success or failure after completion.

Parameters

conf_file_path : str Path to the SNAPHU configuration file (typically generated by SNAP or manually prepared). snaphu_exe_path : str Path to the SNAPHU executable to be used for unwrapping. output_directory : str Directory where SNAPHU will be executed and output files will be stored.

Returns

bool True if SNAPHU unwrapping completed successfully (return code 0), False otherwise.

Raises

FileNotFoundError If the configuration file does not exist. IndexError If the configuration file has fewer than 7 lines (expected command at line 7). subprocess.SubprocessError If there is an unexpected error while executing the SNAPHU command.

Notes

  • The function assumes that the unwrapping command is located on line 7 of the configuration file.
  • Any leading '#' or 'snaphu' prefixes in the command line will be stripped before execution.
  • Requires SNAPHU to be compiled and available at the specified snaphu_exe_path.
Source code in pysarflow/slc.py
def snaphu_unwrapping(conf_file_path, snaphu_exe_path, output_directory):
    """
    Run the SNAPHU unwrapping process using a configuration file.

    This function reads a SNAPHU configuration file, extracts the command line 
    arguments, and executes the SNAPHU binary with those arguments in the specified 
    output directory. It ensures the working directory exists before execution and 
    reports success or failure after completion.

    Parameters
    ----------
    conf_file_path : str
        Path to the SNAPHU configuration file (typically generated by SNAP or manually prepared).
    snaphu_exe_path : str
        Path to the SNAPHU executable to be used for unwrapping.
    output_directory : str
        Directory where SNAPHU will be executed and output files will be stored.

    Returns
    -------
    bool
        True if SNAPHU unwrapping completed successfully (return code 0), 
        False otherwise.

    Raises
    ------
    FileNotFoundError
        If the configuration file does not exist.
    IndexError
        If the configuration file has fewer than 7 lines (expected command at line 7).
    subprocess.SubprocessError
        If there is an unexpected error while executing the SNAPHU command.

    Notes
    -----
    - The function assumes that the unwrapping command is located on line 7 of the configuration file.
    - Any leading '#' or 'snaphu' prefixes in the command line will be stripped before execution.
    - Requires SNAPHU to be compiled and available at the specified `snaphu_exe_path`.
    """
    with open(conf_file_path, 'r') as file:
        lines = file.readlines()

    if len(lines) <= 6:
        raise IndexError("Configuration file doesn't have enough lines")

    command_line = lines[6].strip()

    if command_line.startswith('#'):
        command_line = command_line[1:].strip()
    if command_line.startswith('snaphu'):
        command_line = command_line[6:].strip()

    full_command = f'"{snaphu_exe_path}" {command_line}'

    print(f"Running SNAPHU command: {full_command}")
    print(f"Working directory: {output_directory}")

    os.makedirs(output_directory, exist_ok=True)

    result = subprocess.run(
        full_command,
        cwd=output_directory,
        shell=True,
        capture_output=True,
        text=True
    )

    if result.returncode == 0:
        print("SNAPHU unwrapping completed successfully!")
        print("Output:", result.stdout)
    else:
        print("SNAPHU unwrapping failed!")
        print("Error:", result.stderr)
        print("Return code:", result.returncode)
    return result.returncode == 0

temporal_baseline(product1_path, product2_path)

Calculate and print the temporal baseline between two Sentinel-1 products.

The temporal baseline is defined as the absolute difference in days between the acquisition times (start times) of the two SAR products. This metric is commonly used in interferometric SAR (InSAR) analysis to assess the temporal separation between image acquisitions.

Args:

product1_path : str File path to the first Sentinel-1 product (e.g., the master image). product2_path : str File path to the second Sentinel-1 product (e.g., the slave image).

Returns: It prints the temporal baseline None The function prints the temporal baseline in days and does not return a value.

Notes:

  • This function uses read_product to open the Sentinel-1 products.
  • It assumes that both products contain valid start time metadata.
  • Resources are released after processing by calling .dispose() on each product.
Source code in pysarflow/slc.py
def temporal_baseline(product1_path, product2_path):
    """
    Calculate and print the temporal baseline between two Sentinel-1 products.

    The temporal baseline is defined as the absolute difference in days between 
    the acquisition times (start times) of the two SAR products. This metric is 
    commonly used in interferometric SAR (InSAR) analysis to assess the temporal 
    separation between image acquisitions.

    Args:
    ----------
    product1_path : str
        File path to the first Sentinel-1 product (e.g., the master image).
    product2_path : str
        File path to the second Sentinel-1 product (e.g., the slave image).

    Returns:
    It prints the temporal baseline
    None
        The function prints the temporal baseline in days and does not return a value.

    Notes:
    -----
    - This function uses `read_product` to open the Sentinel-1 products.
    - It assumes that both products contain valid start time metadata.
    - Resources are released after processing by calling `.dispose()` on each product.
    """
    product1 =  read_slc_product(product1_path)
    product2 =  read_slc_product(product2_path)
    master_time = product1.getStartTime()
    slave_time = product2.getStartTime()
    temporal_baseline = abs(slave_time.getMJD() - master_time.getMJD())

    print(f"Temporal Baseline: {temporal_baseline:.1f} days")

    product1.dispose()
    product2.dispose()
    return

terrain_correction_slc(product, DEM)

Apply terrain correction to a SAR product using a specified DEM.

Terrain correction removes geometric distortions caused by topography and sensor viewing geometry. This step geocodes the image into a map coordinate system and ensures that pixel locations align with their true geographic position.

Args

product : snappy.Product The SAR product to which terrain correction will be applied. DEM : str The name of the Digital Elevation Model (e.g., 'SRTM 3Sec' or a custom DEM) to be used for terrain correction.

Returns

snappy.Product The terrain-corrected product.

Notes

  • The DEM is saved as part of the output product.
  • Areas with missing DEM values are assigned an external no-data value (0.0).
  • This step is typically performed near the end of the preprocessing chain to produce a geocoded product suitable for analysis and visualization.
Source code in pysarflow/slc.py
def terrain_correction_slc(product, DEM):
    """
    Apply terrain correction to a SAR product using a specified DEM.

    Terrain correction removes geometric distortions caused by topography and sensor 
    viewing geometry. This step geocodes the image into a map coordinate system 
    and ensures that pixel locations align with their true geographic position.

    Args
    ----------
    product : snappy.Product
        The SAR product to which terrain correction will be applied.
    DEM : str
        The name of the Digital Elevation Model (e.g., 'SRTM 3Sec' or a custom DEM) 
        to be used for terrain correction.

    Returns
    -------
    snappy.Product
        The terrain-corrected product.

    Notes
    -----
    - The DEM is saved as part of the output product.
    - Areas with missing DEM values are assigned an external no-data value (0.0).
    - This step is typically performed near the end of the preprocessing chain to 
      produce a geocoded product suitable for analysis and visualization.
    """
    parameters = HashMap()
    print('Applying Terrain Correction...')
    parameters.put('demName', DEM)
    parameters.put('saveDEM', True)
    parameters.put('externalDEMNoDataValue', 0.0)
    output = GPF.createProduct("Terrain-Correction", parameters, product)
    print("Terrain Correction applied!")
    return output

topsar_deburst(product, polarization)

Apply TOPSAR deburst operation to a Sentinel-1 product.

This function removes burst discontinuities in TOPSAR acquisitions by merging bursts into a seamless image for the specified polarization. It is a necessary preprocessing step for Sentinel-1 TOPSAR IW and EW data before further interferometric or geocoding analysis.

Args

product : snappy.Product The input Sentinel-1 product to which the deburst operation will be applied. polarization : str The polarization channel to process (e.g., 'VV', 'VH', 'HH', 'HV').

Returns

snappy.Product The deburst-processed Sentinel-1 product.

Source code in pysarflow/slc.py
def topsar_deburst(product, polarization):  
    """
    Apply TOPSAR deburst operation to a Sentinel-1 product.

    This function removes burst discontinuities in TOPSAR acquisitions 
    by merging bursts into a seamless image for the specified polarization. 
    It is a necessary preprocessing step for Sentinel-1 TOPSAR IW and EW 
    data before further interferometric or geocoding analysis.

    Args
    ----------
    product : snappy.Product
        The input Sentinel-1 product to which the deburst operation will be applied.
    polarization : str
        The polarization channel to process (e.g., 'VV', 'VH', 'HH', 'HV').

    Returns
    -------
    snappy.Product
        The deburst-processed Sentinel-1 product.
    """
    parameters = HashMap()
    print('Apply TOPSAR Deburst...')
    parameters.put("Polarisations", polarization)
    output = GPF.createProduct("TOPSAR-Deburst", parameters, product)
    print("TOPSAR Deburst applied!")
    return output  

topsar_split(product, burst_dict, pols=None, output_complex=True)

Run TOPSAR-Split using burst indices from burst_for_geometry(...).

Parameters:
  • product

    Input SAR product. Output of the burst_for_geometry

  • burst_dict

    output from the burst_for_geometry function

Returns Product restricted to the specified sub-swath, burst range, and polarisations.

Source code in pysarflow/slc.py
def topsar_split(product, burst_dict, pols=None, output_complex=True):
    """
    Run TOPSAR-Split using burst indices from burst_for_geometry(...).

    Args:
      product : Input SAR product. Output of the burst_for_geometry
      burst_dict : output from the burst_for_geometry function
      pols = polarization

    Returns
        Product restricted to the specified sub-swath, burst range, and polarisations.
    """
    band = burst_dict['band_name']              # e.g. 'i_IW1_VH'
    swath = next(iw for iw in ['IW1','IW2','IW3','EW1','EW2','EW3','EW4','EW5'] if iw in band)

    if pols is None:
        pols = band.split('_')[-1]              # infer polarisation from band name

    if burst_dict['geom_type'] == 'Point':
        fb = lb = int(burst_dict['burst'])
    else:
        fb, lb = int(burst_dict['firstBurst']), int(burst_dict['lastBurst'])
        if fb > lb: fb, lb = lb, fb

    # Build parameters
    parameters = HashMap()
    parameters.put('subswath', swath)
    parameters.put('selectedPolarisations', pols)
    parameters.put('firstBurstIndex', Integer(fb))
    parameters.put('lastBurstIndex', Integer(lb))
    parameters.put('outputComplex', bool(output_complex))

    # Run TOPSAR-Split
    output = GPF.createProduct("TOPSAR-Split", parameters, product)
    print(f"TOPSAR-Split applied: {swath} bursts {fb}{lb} ({pols})")
    return output

It includes common or secondary functionalities that would be used in other modules.

convert_0_to_nan(product)

Convert all zero values in the bands of a Sentinel-1 product to NaN (represented as -9999.0).

This function iterates over all bands in the input product and replaces pixels with a value of 0 with -9999.0, which is commonly used as the NoData value in SNAP products. The data type of each band is set to float32 to accommodate NaN values.

Parameters:
  • product (Product) –

    Sentinel-1 product whose zero values need to be converted.

Returns:
  • esa_snappy.Product: A new product with zero values replaced by -9999.0 in all bands.

Source code in pysarflow/utils.py
def convert_0_to_nan(product):
    """
    Convert all zero values in the bands of a Sentinel-1 product to NaN (represented as -9999.0).

    This function iterates over all bands in the input product and replaces
    pixels with a value of 0 with -9999.0, which is commonly used as the NoData value
    in SNAP products. The data type of each band is set to float32 to accommodate NaN values.

    Args:
        product (esa_snappy.Product): Sentinel-1 product whose zero values need to be converted.

    Returns:
        esa_snappy.Product: A new product with zero values replaced by -9999.0 in all bands.
    """
    band_names = list(product.getBandNames())
    BandDescriptor = jpy.get_type('org.esa.snap.core.gpf.common.BandMathsOp$BandDescriptor')

    # Create Java array of BandDescriptor
    Array = jpy.array('org.esa.snap.core.gpf.common.BandMathsOp$BandDescriptor', len(band_names))

    for i, name in enumerate(band_names):
        band_def = BandDescriptor()
        band_def.name = name   # avoid overwrite
        band_def.type = 'float32'
        band_def.expression = f"{name} == 0 ? -9999.0 : {name}"
        band_def.noDataValue = -9999.0     # match replacement value
        Array[i] = band_def

    # Parameters HashMap
    parameters = jpy.get_type('java.util.HashMap')()
    parameters.put('targetBands', Array)

    # Run BandMaths
    updated_product = GPF.createProduct('BandMaths', parameters, product)
    return updated_product

extract_bbox(file_path)

Extracts a bounding box from a shapefile and returns it as a WKT polygon.

Parameters:
  • file_path (str) –

    Path to the shapefile (.shp) containing the boundary geometry.

Returns:
  • str

    WKT string representing the bounding polygon for use in SNAP SubsetOp.

Source code in pysarflow/utils.py
def extract_bbox(file_path):
    """
    Extracts a bounding box from a shapefile and returns it as a WKT polygon.

    Args:
        file_path (str): Path to the shapefile (.shp) containing the boundary geometry.

    Returns:
        str: WKT string representing the bounding polygon for use in SNAP SubsetOp.
    """
    r = shapefile.Reader("data/island_boundary2.shp")
    g=[]
    for s in r.shapes():
        g.append(pygeoif.geometry.as_shape(s))
    m = pygeoif.MultiPoint(g)
    wkt = str(m.wkt).replace("MULTIPOINT","POLYGON(") + ")"
    SubsetOp = jpy.get_type('org.esa.snap.core.gpf.common.SubsetOp')
    bounding_wkt = wkt
    return bounding_wkt

extract_info(product_path)

Extract and display basic information about a Sentinel-1 product.

Parameters

product_path : str Path to the Sentinel-1 product file.

Prints

  • Product name
  • Product type
  • Product description
  • Scene width and height
  • Acquisition start and end time
  • Number of bands and their names

Notes

This function uses the SNAP API to read the product and display its metadata. It disposes of the product after extraction to free resources. Useful for quickly inspecting a product before further processing.

Source code in pysarflow/utils.py
def extract_info(product_path):
    """
    Extract and display basic information about a Sentinel-1 product.

    Parameters
    ----------
    product_path : str
        Path to the Sentinel-1 product file.

    Prints
    ------
    - Product name
    - Product type
    - Product description
    - Scene width and height
    - Acquisition start and end time
    - Number of bands and their names

    Notes
    -----
    This function uses the SNAP API to read the product and display its metadata. 
    It disposes of the product after extraction to free resources. 
    Useful for quickly inspecting a product before further processing.
    """
    product = read_product(product_path)
    print("Product name:", product.getName())
    print("Product type:", product.getProductType())
    print("Description:", product.getDescription())
    #print("Beam Mode:", check_beam_mode(product_path))
    print("Scene width:", product.getSceneRasterWidth())
    print("Scene height:", product.getSceneRasterHeight())

    metadata = product.getMetadataRoot()
    print("Start time:", product.getStartTime())
    print("End time:", product.getEndTime())

    print("\n Bands")
    count = 0
    band_names = product.getBandNames()
    number_bands = len(list(band_names))
    print("Number of bands:", number_bands)
    while count < number_bands:
        band = product.getBand(band_names[count])
        print("Band name:", band.getName())
        count += 1

    product.dispose()
    print("\n")
    return

get_subswath(aoi, product)

Identify the Sentinel-1 subswath (e.g., IW1, IW2, or IW3) covering the given Area of Interest (AOI).

This function determines which Sentinel-1 interferometric wide (IW) subswath contains the centroid of a given AOI. This is useful when selecting or cropping data specific to one of the three IW subswaths.

Parameters:

aoi : str | shapely.geometry.Polygon | list[tuple[float, float]] The Area of Interest to test for subswath coverage. It can be: - A WKT polygon string (e.g., 'POLYGON((lon lat, lon lat, ...))') - A Shapely Polygon object - A list or tuple of (lon, lat) coordinate pairs

org.esa.snap.core.datamodel.Product

The Sentinel-1 product containing IW subswath bands.

Returns:

result : str The name of the subswath ('IW1', 'IW2', or 'IW3') that contains the AOI centroid. Returns 'No subswath for the AOI' if none of the available subswaths cover it.

Notes:

  • The function uses the product's band names to infer available subswaths.
  • It checks whether the AOI centroid lies within the bounds of each subswath's geocoded raster.
  • Requires that the product bands include geocoding information.
Source code in pysarflow/utils.py
def get_subswath(aoi, product):
    """
    Identify the Sentinel-1 subswath (e.g., IW1, IW2, or IW3) covering the given Area of Interest (AOI).

    This function determines which Sentinel-1 interferometric wide (IW) subswath contains 
    the centroid of a given AOI. This is useful when selecting or cropping data specific 
    to one of the three IW subswaths.

    Parameters:
    ----------
    aoi : str | shapely.geometry.Polygon | list[tuple[float, float]]
        The Area of Interest to test for subswath coverage. It can be:
        - A WKT polygon string (e.g., 'POLYGON((lon lat, lon lat, ...))')
        - A Shapely `Polygon` object
        - A list or tuple of (lon, lat) coordinate pairs

    product : org.esa.snap.core.datamodel.Product
        The Sentinel-1 product containing IW subswath bands.

    Returns:
    -------
    result : str
        The name of the subswath ('IW1', 'IW2', or 'IW3') that contains the AOI centroid.
        Returns 'No subswath for the AOI' if none of the available subswaths cover it.

    Notes:
    -----
    - The function uses the product's band names to infer available subswaths.
    - It checks whether the AOI centroid lies within the bounds of each subswath's geocoded raster.
    - Requires that the product bands include geocoding information.
    """
    if isinstance(aoi, str) and aoi.startswith('POLYGON'):
        coords_str = aoi.replace('POLYGON((', '').replace('))', '')
        coord_pairs = coords_str.split(',')
        coords = []
        for pair in coord_pairs:
            lon, lat = map(float, pair.strip().split())
            coords.append((lon, lat))
        aoi_polygon = Polygon(coords)
    elif isinstance(aoi, Polygon):
        aoi_polygon = aoi
    elif isinstance(aoi, (list, tuple)):
        aoi_polygon = Polygon(aoi)
    else:
        raise ValueError("AOI must be a WKT string, Shapely Polygon, or list of (lon, lat) tuples")

    centroid = aoi_polygon.centroid

    band_names = list(product.getBandNames())
    subswaths = set()

    for band_name in band_names:
        if '_IW' in band_name:
            sw_part = band_name.split('_IW')[1][:1]
            if sw_part.isdigit():
                subswaths.add(f"IW{sw_part}")

    result = "No subswath for the AOI"
    for subswath in sorted(subswaths):
        subswath_bands = [band for band in band_names if f'_IW{subswath[-1]}_' in band]
        if not subswath_bands:
            continue

        band = product.getBand(subswath_bands[0])
        geo_coding = band.getGeoCoding()

        if geo_coding:
            pixel_pos = geo_coding.getPixelPos(esa_snappy.GeoPos(centroid.y, centroid.x), None)
            width = band.getRasterWidth()
            height = band.getRasterHeight()

            if 0 <= pixel_pos.x < width and 0 <= pixel_pos.y < height:
                result = subswath

    return result