Skip to content

Entrypoints

Entrypoints are functions that can be directly called to perform a high-level task. Each of the entrypoints listed below can also be called as a command line script; the scripts accept arguments that are passed straight through to these functions.

For flag-by-flag documentation of the command line form, including which arguments are required, see Command Line Usage.

generate_predictions(raster_folder_path, chip_size, tree_detection_model, chip_stride=None, chip_overlap_percentage=None, use_units_meters=False, region_of_interest=None, resolution=None, output_CRS=None, predictions_save_path=None, view_predictions_plot=False, run_nms=True, iou_threshold=0.3, min_confidence=0.3, batch_size=1, detectree2_weights_path=Path(CHECKPOINTS_FOLDER, '230103_randresize_full.pth'), sam2_checkpoint_path=Path(CHECKPOINTS_FOLDER, 'sam2.1_hiera_large.pt'), sam3_checkpoint_path=Path(CHECKPOINTS_FOLDER, 'sam3.pt'), sam3_huggingface_token=None, detector_kwargs={})

Entrypoint script to generate tree detections for a raster dataset input. Supports visualizing and saving predictions.

Parameters:

Name Type Description Default
raster_folder_path PATH_TYPE

Path to the folder or raster files.

required
chip_size float

Dimension of the chip. May be pixels or meters, based on use_units_meters.

required
chip_stride Optional[float]

Stride of the chip. May be pixels or meters, based on use_units_meters. If used, chip_overlap_percentage should not be set. Defaults to None.

None
tree_detection_model str

Selected model for detecting trees. One of "deepforest", "detectree2", "sam2", "sam3", or "tcd".

required
chip_overlap_percentage Optional[float]

Percent overlap of the chip from 0-100. If used, chip_stride should not be set. Defaults to None.

None
use_units_meters bool

Use units of meters rather than pixels when interpreting the chip_size and chip_stride. Defaults to False.

False
region_of_interest Optional[BOUNDARY_TYPE]

Only data from this spatial region will be included in the dataloader. Defaults to None.

None
resolution Optional[float]

Spatial resolution the data in meters/pixel. If un-set, will be the resolution of the first raster data that is read. Defaults to None.

None
output_CRS Optional[CRS]

(Optional[pyproj.CRS], optional): The coordinate reference system to use for the output data. If un-set, will be the CRS of the first tile found. Defaults to None.

None
predictions_save_path Optional[PATH_TYPE]

Path to a geofile to save the prediction outputs.

None
view_predictions_plot bool

Set to True if visualization of the detected regions is needed. Defaults to False.

False
run_nms bool

(bool, optional): Set to True if non-max suppresion needs to be run on predictions from multiple regions.

True
iou_threshold float

What intersection over union value to consider an overlapping detection. Defaults to 0.5.

0.3
min_confidence float

Prediction score threshold for detections to be included.

0.3
batch_size int

Number of images to load in a batch. Defaults to 1.

1
sam2_checkpoint_path PATH_TYPE

Path to the SAM2 checkpoint file. Defaults to checkpoints/sam2.1_hiera_large.pt.

Path(CHECKPOINTS_FOLDER, 'sam2.1_hiera_large.pt')
sam3_checkpoint_path PATH_TYPE

Path to a local SAM3 checkpoint file. Defaults to checkpoints/sam3.pt.

Path(CHECKPOINTS_FOLDER, 'sam3.pt')
sam3_huggingface_token str

HuggingFace token for downloading SAM3 weights. Only used when sam3_checkpoint_path is None.

None
detector_kwargs dict

Optional keyword arguments to be unpacked for the detector constructor.

{}
Source code in tree_detection_framework/entrypoints/generate_predictions.py
def generate_predictions(
    raster_folder_path: PATH_TYPE,
    chip_size: float,
    tree_detection_model: str,
    chip_stride: Optional[float] = None,
    chip_overlap_percentage: float = None,
    use_units_meters: bool = False,
    region_of_interest: Optional[BOUNDARY_TYPE] = None,
    resolution: Optional[float] = None,
    output_CRS: Optional[pyproj.CRS] = None,
    predictions_save_path: Optional[PATH_TYPE] = None,
    view_predictions_plot: bool = False,
    run_nms: bool = True,
    iou_threshold: Optional[float] = 0.3,
    min_confidence: Optional[float] = 0.3,
    batch_size: int = 1,
    detectree2_weights_path: PATH_TYPE = Path(
        CHECKPOINTS_FOLDER, "230103_randresize_full.pth"
    ),
    sam2_checkpoint_path: PATH_TYPE = Path(CHECKPOINTS_FOLDER, "sam2.1_hiera_large.pt"),
    sam3_checkpoint_path: PATH_TYPE = Path(CHECKPOINTS_FOLDER, "sam3.pt"),
    sam3_huggingface_token: Optional[str] = None,
    detector_kwargs: dict = {},
):
    """
    Entrypoint script to generate tree detections for a raster dataset input. Supports visualizing and saving predictions.

    Args:
        raster_folder_path (PATH_TYPE): Path to the folder or raster files.
        chip_size (float):
            Dimension of the chip. May be pixels or meters, based on `use_units_meters`.
        chip_stride (Optional[float], optional):
            Stride of the chip. May be pixels or meters, based on `use_units_meters`. If used,
            `chip_overlap_percentage` should not be set. Defaults to None.
        tree_detection_model (str):
            Selected model for detecting trees. One of "deepforest", "detectree2", "sam2", "sam3", or "tcd".
        chip_overlap_percentage (Optional[float], optional):
            Percent overlap of the chip from 0-100. If used, `chip_stride` should not be set.
            Defaults to None.
        use_units_meters (bool, optional):
            Use units of meters rather than pixels when interpreting the `chip_size` and `chip_stride`.
            Defaults to False.
        region_of_interest (Optional[BOUNDARY_TYPE], optional):
            Only data from this spatial region will be included in the dataloader. Defaults to None.
        resolution (Optional[float], optional):
            Spatial resolution the data in meters/pixel. If un-set, will be the resolution of the
            first raster data that is read. Defaults to None.
        output_CRS: (Optional[pyproj.CRS], optional):
            The coordinate reference system to use for the output data. If un-set, will be the CRS
            of the first tile found. Defaults to None.
        predictions_save_path (Optional[PATH_TYPE], optional):
            Path to a geofile to save the prediction outputs.
        view_predictions_plot (bool, optional):
            Set to True if visualization of the detected regions is needed. Defaults to False.
        run_nms: (bool, optional):
            Set to True if non-max suppresion needs to be run on predictions from multiple regions.
        iou_threshold (float, optional):
            What intersection over union value to consider an overlapping detection. Defaults to 0.5.
        min_confidence (float, optional):
            Prediction score threshold for detections to be included.
        batch_size (int, optional):
            Number of images to load in a batch. Defaults to 1.
        sam2_checkpoint_path (PATH_TYPE, optional):
            Path to the SAM2 checkpoint file. Defaults to checkpoints/sam2.1_hiera_large.pt.
        sam3_checkpoint_path (PATH_TYPE, optional):
            Path to a local SAM3 checkpoint file. Defaults to checkpoints/sam3.pt.
        sam3_huggingface_token (str, optional):
            HuggingFace token for downloading SAM3 weights. Only used when sam3_checkpoint_path is None.
        detector_kwargs (dict, optional):
            Optional keyword arguments to be unpacked for the detector constructor.
    """

    # Create the dataloader by passing folder path to raster data.
    dataloader = create_dataloader(
        raster_folder_path=raster_folder_path,
        chip_size=chip_size,
        chip_stride=chip_stride,
        chip_overlap_percentage=chip_overlap_percentage,
        use_units_meters=use_units_meters,
        region_of_interest=region_of_interest,
        resolution=resolution,
        output_CRS=output_CRS,
        batch_size=batch_size,
    )

    # Setup the specified tree detection model
    if tree_detection_model == "deepforest":

        # Setup the parameters dictionary
        param_dict = {
            "backbone": "retinanet",
            "num_classes": 1,
        }

        df_module = DeepForestModule(param_dict)
        # Move the module to the GPU if available
        df_module.to(
            torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
        )
        detector = DeepForestDetector(df_module, **detector_kwargs)

    elif tree_detection_model == "detectree2":

        param_dict = {"update_model": detectree2_weights_path}

        dtree2_module = Detectree2Module(param_dict)
        detector = MaskRCNNDetector(dtree2_module, **detector_kwargs)

    elif tree_detection_model == "sam2":
        detector = SAMV2Detector(
            sam2_checkpoint=sam2_checkpoint_path,
            **detector_kwargs,
        )

    elif tree_detection_model == "sam3":
        detector = SAM3Detector(
            checkpoint_path=sam3_checkpoint_path,
            huggingface_token=sam3_huggingface_token,
            confidence_threshold=min_confidence,
            **detector_kwargs,
        )

    elif tree_detection_model == "tcd":
        tcd_module = TCDModule()
        detector = MaskRCNNDetector(tcd_module, **detector_kwargs)

    else:
        raise ValueError(
            """Please enter a valid tree detection model. Currently supported models are:
                1. deepforest
                2. detectree2
                3. sam2
                4. sam3
                5. tcd
                """
        )

    # Get predictions by invoking the tree_detection_model
    logging.info("Getting tree detections")
    outputs = detector.predict(dataloader)

    if run_nms is True:
        logging.info("Running non-max suppression")
        # Run non-max suppression on the detected regions
        outputs = multi_region_NMS(
            outputs, threshold=iou_threshold, min_confidence=min_confidence
        )
        if predictions_save_path:
            # Save predictions to disk
            outputs.save(predictions_save_path)
    else:
        if predictions_save_path:
            # Save without merging tiles
            outputs.save_tiled(predictions_save_path)

    if view_predictions_plot is True:
        logging.info("View plot. Kill the plot window to exit.")
        # Plot the detections and the bounds of the region
        outputs.plot()

detect_trees_two_stage(CHM_file, tree_tops_save_path, tree_crowns_save_path=None, chip_size=CHIP_SIZE, chip_stride=CHIP_STRIDE, resolution=RESOLUTION, raster_blur_sigma=None, edge_suppression_meters=None, tree_top_detector_kwargs={}, crown_segmentation_kwargs={})

Detect trees geometrically and save the detected tree tops and tree crowns.

Parameters:

Name Type Description Default
CHM_file Path

Path to a CHM file to detect trees from

required
tree_tops_save_path Path

Where to save the detected tree tops.

required
tree_crowns_save_path Path

Where to save the detected tree crowns. If not provided, no crowns will be detected. Defaults to None.

None
chip_size int

The size of the chip in pixels. Defaults to CHIP_SIZE.

CHIP_SIZE
chip_stride int

The stride of the sliding chip window in pixels. Defaults to CHIP_STRIDE.

CHIP_STRIDE
resolution float

The spatial resolution that the CHM is resampled to. Defaults to OUTPUT_RESOLUTION.

RESOLUTION
raster_blur_sigma float

The standard deviation of the 2D gaussian kernel, in meters. Defaults to None, no smoothing.

None
tree_top_detector_kwargs dict

Keyword arguments to pass to the tree top detector. Defaults to {}.

{}
crown_segmentation_kwargs dict

Keyword arguments to pass to the crown segmentation approach. Defaults to {}.

{}
Source code in tree_detection_framework/entrypoints/detect_geometric_two_stage.py
def detect_trees_two_stage(
    CHM_file: Path,
    tree_tops_save_path: Path,
    tree_crowns_save_path: Optional[Path] = None,
    chip_size: int = CHIP_SIZE,
    chip_stride: int = CHIP_STRIDE,
    resolution: float = RESOLUTION,
    raster_blur_sigma: Optional[float] = None,
    edge_suppression_meters: Optional[float] = None,
    tree_top_detector_kwargs: dict = {},
    crown_segmentation_kwargs: dict = {},
):
    """Detect trees geometrically and save the detected tree tops and tree crowns.

    Args:
        CHM_file (Path):
            Path to a CHM file to detect trees from
        tree_tops_save_path (Path):
            Where to save the detected tree tops.
        tree_crowns_save_path (Path, optional):
            Where to save the detected tree crowns. If not provided, no crowns will be detected. Defaults to None.
        chip_size (int, optional):
            The size of the chip in pixels. Defaults to CHIP_SIZE.
        chip_stride (int, optional):
            The stride of the sliding chip window in pixels. Defaults to CHIP_STRIDE.
        resolution (float, optional):
            The spatial resolution that the CHM is resampled to. Defaults to OUTPUT_RESOLUTION.
        raster_blur_sigma (float, optional):
            The standard deviation of the 2D gaussian kernel, in meters. Defaults to None, no smoothing.
        tree_top_detector_kwargs (dict, optional):
            Keyword arguments to pass to the tree top detector. Defaults to {}.
        crown_segmentation_kwargs (dict, optional):
            Keyword arguments to pass to the crown segmentation approach. Defaults to {}.
    """

    # Stage 1: Create a dataloader for the raster data and detect the tree-tops
    # TODO, consider a larger window for tree detection to reduce boundary artificts, while still
    # keeping the watershed step fast. Counterpoint: in large rasters, the NMS step becomes extremely
    # expensive and the bottleneck, so additional duplicated regions may further slow that step.
    dataloader = create_dataloader(
        raster_folder_path=CHM_file,
        chip_size=chip_size,
        chip_stride=chip_stride,
        resolution=resolution,
    )

    # Create the detector for variable window maximum detection
    treetop_detector = GeometricTreeTopDetector(
        confidence_feature="distance",
        blur_sigma=raster_blur_sigma,
        **tree_top_detector_kwargs,
    )

    # Generate tree top predictions
    treetop_detections = treetop_detector.predict(dataloader)

    # Remove the tree tops that were generated in the edges of tiles. This is an alternative to NMS.
    treetop_detections = remove_detections_from_tile_overlap(treetop_detections)

    # Detect tree crowns if requested
    if tree_crowns_save_path is not None:
        # Stage 2: Combine raster and vector data (from the tree-top detector) to create a new dataloader
        raster_vector_dataloader = create_intersection_dataloader(
            raster_data=CHM_file,
            vector_data=treetop_detections,
            chip_size=chip_size,
            chip_stride=chip_stride,
            resolution=resolution,
        )

        # Create the crown detector, which is seeded by the tree top points detected in the last step
        # The score metric is how far from the edge the detection is, which prioritizes central detections
        treecrown_detector = GeometricTreeCrownDetector(
            confidence_feature="distance",
            **crown_segmentation_kwargs,
        )

        # Predict the crowns
        treecrown_detections = treecrown_detector.predict(raster_vector_dataloader)
        # Convert to a data frame
        treecrown_detections_gdf = treecrown_detections.get_data_frame(merge=True)
        # Rank the detections so the ones which have a tree top closer to the center of their
        # respective tile are first
        treecrown_detections_gdf = treecrown_detections_gdf.sort_values(
            "score", ascending=False
        )
        # The CRS is dropped by the next step
        crown_CRS = treecrown_detections_gdf.crs

        # Take one crown per tree top, prefering the crowns where the corresponding tree top was
        # closer to the center.
        treecrown_detections_gdf = treecrown_detections_gdf.groupby(
            "treetop_unique_ID", as_index=False
        ).first()
        # Reset the CRS
        treecrown_detections_gdf.set_crs(crown_CRS, inplace=True)

    # Convert to the geodataframe representation
    treetop_detections_gdf = treetop_detections.get_data_frame(merge=True)

    # If requested, suppress the trees detected at the boundary of the raster. These can often be
    # low-quality and represent the "shoulder" of trees which are partly outside the raster.
    if edge_suppression_meters is not None and edge_suppression_meters != 0:
        # Determine the extent of the valid raster
        valid_raster_region = get_valid_raster_region(CHM_file)
        # Construct the new valid region eroded from the edge by the requested amount
        valid_raster_region.geometry = valid_raster_region.buffer(
            -edge_suppression_meters
        )
        # Make sure the CRS matches
        valid_raster_region.to_crs(treetop_detections_gdf.crs, inplace=True)

        # Subset tree tops to this valid region
        treetop_detections_gdf = treetop_detections_gdf[
            treetop_detections_gdf.within(valid_raster_region.geometry.values[0])
        ]

    # Save the tree tops
    tree_tops_save_path.parent.mkdir(parents=True, exist_ok=True)
    treetop_detections_gdf.to_file(tree_tops_save_path)

    if tree_crowns_save_path is not None:
        # Drop the crowns corresponding to trees detected at the edge by ensuring crowns correspond
        # to a tree top which was kept
        if edge_suppression_meters is not None and edge_suppression_meters != 0:
            treecrown_detections_gdf = treecrown_detections_gdf[
                treecrown_detections_gdf.treetop_unique_ID.isin(
                    treetop_detections_gdf.unique_ID
                )
            ]
        # Save the crowns
        tree_crowns_save_path.parent.mkdir(parents=True, exist_ok=True)
        treecrown_detections_gdf.to_file(tree_crowns_save_path)

main(image_dir, out_dir, model_key, detectree_checkpoints, chip_size, chip_stride, batch_size)

Detect trees in raw images using a specified model and save detection results.

Parameters:

Name Type Description Default
image_dir Path

Directory containing raw images, or nested subdirectories with raw images in them. Will read all images recusively from this directory.

required
out_dir Path

Directory to save detection results. Will be created if it does not already exist. The subdirectory structure of image_dir will be mirrored, if there is any.

required
model_key str

Model to use for detection.

required
detectree_checkpoints Path or None

Path to detectree2 checkpoints. Required if model_key is 'detectree2'.

required
chip_size int

Chip size for tiling images.

required
chip_stride int

Chip stride for tiling images.

required
batch_size int

Batch size for inference.

required

Raises:

Type Description
ValueError

If an unknown model_key is provided.

Source code in tree_detection_framework/entrypoints/detect_in_raw_images.py
def main(
    image_dir: Path,
    out_dir: Path,
    model_key: str,
    detectree_checkpoints: Path,
    chip_size: int,
    chip_stride: int,
    batch_size: int,
) -> None:
    """
    Detect trees in raw images using a specified model and save detection results.

    Args:
        image_dir (Path): Directory containing raw images, or nested subdirectories with raw
            images in them. Will read all images recusively from this directory.
        out_dir (Path): Directory to save detection results. Will be created if it does
            not already exist. The subdirectory structure of image_dir will be mirrored,
            if there is any.
        model_key (str): Model to use for detection.
        detectree_checkpoints (Path or None): Path to detectree2 checkpoints. Required if model_key is 'detectree2'.
        chip_size (int): Chip size for tiling images.
        chip_stride (int): Chip stride for tiling images.
        batch_size (int): Batch size for inference.

    Raises:
        ValueError: If an unknown model_key is provided.
    """
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(f"Detecting on device: {device}")

    # Create dataloader for raw images
    dataloader = create_image_dataloader(
        images_dir=image_dir,
        chip_size=chip_size,
        chip_stride=chip_stride,
        batch_size=batch_size,
    )
    print("Dataloader created")

    # Instantiate the model
    if model_key == "deepforest":
        model_params = {"backbone": "retinanet", "num_classes": 1}
        module = DeepForestModule(model_params)
        module.to(device)
        detector = DeepForestDetector(module)
    elif model_key == "detectree2":
        model_params = {"update_model": str(detectree_checkpoints)}
        module = Detectree2Module(model_params)
        detector = Detectree2Detector(module)
    elif model_key == "sam2":
        detector = SAMV2Detector(device=device)
        warnings.filterwarnings("ignore", message="cannot import name '_C' from 'sam2'")
        logging.getLogger().setLevel(logging.WARNING)
    else:
        raise ValueError(f"Unknown model key: {model_key}")
    print("Model loaded")

    # Use predict_raw_drone_images to get per-image results
    detection_sets, files, bounds = detector.predict_raw_drone_images(dataloader)

    # For each image, remove detections that extend past the image bounds
    filtered_sets = remove_out_of_bounds_detections(detection_sets, bounds)
    # For each image, suppress overlapping detections
    filtered_sets = [multi_region_NMS(rds) for rds in filtered_sets]

    # Save results for each image. We want to replicate the nested subdirectory
    # structure of the input images
    for rds, file in zip(filtered_sets, files):
        # Get the potentially nested path from the root image directory
        relative_image_path = Path(file).relative_to(image_dir)
        # Rebuild that same subdirectory structure in the output folder
        out_path = out_dir / relative_image_path.with_suffix(".gpkg")
        # Make sure the subdirectory exists and save
        out_path.parent.mkdir(exist_ok=True, parents=True)
        rds.save(out_path)
    print(f"Detection regions saved to {out_dir}")

tile_data(raster_folder_path, chip_size, chip_stride=None, chip_overlap_percentage=None, use_units_meters=False, region_of_interest=None, resolution=None, output_CRS=None, vector_label_folder_path=None, vector_label_attribute=None, visualize_n_tiles=None, save_folder=None, save_n_tiles=None, random_sample=False, batch_size=1)

Entrypoint script for testing preprocessing functions. It enables creating a dataloader, visualizing sample tiles from the dataloader, and saving the contents of the dataloader to disk.

Parameters:

Name Type Description Default
raster_folder_path PATH_TYPE

Path to the folder or raster files.

required
chip_size float

Dimension of the chip. May be pixels or meters, based on use_units_meters.

required
chip_stride Optional[float]

Stride of the chip. May be pixels or meters, based on use_units_meters. If used, chip_overlap_percentage should not be set. Defaults to None.

None
chip_overlap_percentage Optional[float]

Percent overlap of the chip from 0-100. If used, chip_stride should not be set. Defaults to None.

None
use_units_meters bool

Use units of meters rather than pixels when interpreting the chip_size and chip_stride. Defaults to False.

False
region_of_interest Optional[BOUNDARY_TYPE]

Only data from this spatial region will be included in the dataloader. Defaults to None.

None
resolution Optional[float]

Spatial resolution the data in meters/pixel. If un-set, will be the resolution of the first raster data that is read. Defaults to None.

None
output_CRS Optional[CRS]

(Optional[pyproj.CRS], optional): The coordinate reference system to use for the output data. If un-set, will be the CRS of the first tile found. Defaults to None.

None
vector_label_folder_path Optional[PATH_TYPE]

A folder of geospatial vector files that will be used for the label. If un-set, the dataloader will not be labeled. Defaults to None.

None
vector_label_attribute Optional[str]

Attribute to read from the vector data, such as the class or instance ID. Defaults to None.

None
visualize_n_tiles int

The number of randomly-sampled tiles to display.

None
save_folder Optional[PATH_TYPE]

Folder to save data to. Will be created if it doesn't exist.

None
save_n_tiles Optional[int]

Number of tiles to save. Whether they are the first tiles or random is controlled by random_sample. If unset, all tiles will be saved. Defaults to None.

None
random_sample bool

If save_n_tiles is set, should the tiles be randomly sampled rather than taken from the beginning of the dataloader. Defaults to False.

False
batch_size int

Number of images to load in a batch. Defaults to 1.

1
Source code in tree_detection_framework/entrypoints/tile_data.py
def tile_data(
    raster_folder_path: PATH_TYPE,
    chip_size: float,
    chip_stride: Optional[float] = None,
    chip_overlap_percentage: float = None,
    use_units_meters: bool = False,
    region_of_interest: Optional[BOUNDARY_TYPE] = None,
    resolution: Optional[float] = None,
    output_CRS: Optional[pyproj.CRS] = None,
    vector_label_folder_path: Optional[PATH_TYPE] = None,
    vector_label_attribute: Optional[str] = None,
    visualize_n_tiles: Optional[int] = None,
    save_folder: Optional[PATH_TYPE] = None,
    save_n_tiles: Optional[int] = None,
    random_sample: bool = False,
    batch_size: int = 1,
):
    """
    Entrypoint script for testing preprocessing functions.
    It enables creating a dataloader, visualizing sample tiles from the dataloader, and saving the contents of the dataloader to disk.

    Args:
        raster_folder_path (PATH_TYPE): Path to the folder or raster files.
        chip_size (float):
            Dimension of the chip. May be pixels or meters, based on `use_units_meters`.
        chip_stride (Optional[float], optional):
            Stride of the chip. May be pixels or meters, based on `use_units_meters`. If used,
            `chip_overlap_percentage` should not be set. Defaults to None.
        chip_overlap_percentage (Optional[float], optional):
            Percent overlap of the chip from 0-100. If used, `chip_stride` should not be set.
            Defaults to None.
        use_units_meters (bool, optional):
            Use units of meters rather than pixels when interpreting the `chip_size` and `chip_stride`.
            Defaults to False.
        region_of_interest (Optional[BOUNDARY_TYPE], optional):
            Only data from this spatial region will be included in the dataloader. Defaults to None.
        resolution (Optional[float], optional):
            Spatial resolution the data in meters/pixel. If un-set, will be the resolution of the
            first raster data that is read. Defaults to None.
        output_CRS: (Optional[pyproj.CRS], optional):
            The coordinate reference system to use for the output data. If un-set, will be the CRS
            of the first tile found. Defaults to None.
        vector_label_folder_path (Optional[PATH_TYPE], optional):
            A folder of geospatial vector files that will be used for the label. If un-set, the
            dataloader will not be labeled. Defaults to None.
        vector_label_attribute (Optional[str], optional):
            Attribute to read from the vector data, such as the class or instance ID. Defaults to None.
        visualize_n_tiles (int):
            The number of randomly-sampled tiles to display.
        save_folder (Optional[PATH_TYPE], optional):
            Folder to save data to. Will be created if it doesn't exist.
        save_n_tiles (Optional[int], optional):
            Number of tiles to save. Whether they are the first tiles or random is controlled by
            `random_sample`. If unset, all tiles will be saved. Defaults to None.
        random_sample (bool, optional):
            If `save_n_tiles` is set, should the tiles be randomly sampled rather than taken from the
            beginning of the dataloader. Defaults to False.
        batch_size (int, optional):
            Number of images to load in a batch. Defaults to 1.
    """
    # Create the dataloader by passing folder path to raster data and optionally a path to the vector data folder.
    dataloader = create_dataloader(
        raster_folder_path=raster_folder_path,
        chip_size=chip_size,
        chip_stride=chip_stride,
        chip_overlap_percentage=chip_overlap_percentage,
        use_units_meters=use_units_meters,
        region_of_interest=region_of_interest,
        resolution=resolution,
        output_CRS=output_CRS,
        vector_label_folder_path=vector_label_folder_path,
        vector_label_attribute=vector_label_attribute,
        batch_size=batch_size,
    )

    # If `visualize_n_tiles` is specified, display those many number of tiles.
    if visualize_n_tiles is not None:
        visualize_dataloader(dataloader=dataloader, n_tiles=visualize_n_tiles)

    # If path to save tiles is given, save all the tiles (or `n_tiles`) from the dataloader to disk. Tiles can be randomly sampled or ordered.
    if save_folder is not None:
        save_dataloader_contents(
            dataloader=dataloader,
            save_folder=save_folder,
            n_tiles=save_n_tiles,
            random_sample=random_sample,
        )

visualize_detection_sets(image_dir, detection_dir, out_dir, n_images, step, show_centroid, image_ext)

Visualize detection results by pairing images and detection files, sampling according to n_images and step.

Parameters:

Name Type Description Default
image_dir Path

Directory containing raw images.

required
detection_dir Path

Directory containing detection .gpkg files.

required
out_dir Path

Directory to save visualizations.

required
n_images int

Number of images to visualize.

required
step int

Step size between visualized images.

required
show_centroid bool

Whether to show centroid in visualization.

required
image_ext str

Extension of image files (e.g., 'JPG', 'tif').

required

Returns: None. Saves files in the output directory.

Source code in tree_detection_framework/entrypoints/visualize_detections.py
def visualize_detection_sets(
    image_dir: Path,
    detection_dir: Path,
    out_dir: Path,
    n_images: int,
    step: int,
    show_centroid: bool,
    image_ext: str,
) -> None:
    """
    Visualize detection results by pairing images and detection files, sampling
    according to n_images and step.

    Args:
        image_dir: Directory containing raw images.
        detection_dir: Directory containing detection .gpkg files.
        out_dir: Directory to save visualizations.
        n_images: Number of images to visualize.
        step: Step size between visualized images.
        show_centroid: Whether to show centroid in visualization.
        image_ext: Extension of image files (e.g., 'JPG', 'tif').
    Returns: None. Saves files in the output directory.
    """
    # Get sorted lists of image and gpkg files
    gpkg_paths = sorted(detection_dir.glob("*.gpkg"))

    # Sample indices according to n_images and step
    indices = list(range(0, min(len(gpkg_paths), step * n_images), step))

    for idx in tqdm(indices, f"Saving images to {out_dir}"):
        gpkg_path = gpkg_paths[idx]
        image_path = image_dir / f"{gpkg_path.stem}.{image_ext}"

        detections = RegionDetections(
            detection_geometries="geometry",
            data=gpd.read_file(gpkg_path),
        )

        axis = detections.plot(
            plt_show=False,
            raster_file=image_path,
            show_centroid=show_centroid,
            raster_vis_downsample=1,
        )

        figure = axis.get_figure()
        figure.savefig(
            out_dir / (gpkg_path.stem + ".png"), dpi=300, bbox_inches="tight"
        )

detect_trees(detector, detection_params, detector_dir, preprocessed_local_files)

Detect trees using the specified detector and save raw detections.

Parameters:

Name Type Description Default
detector str

Detector name: geometric, deepforest, detectree2, sam2, sam3, or tcd.

required
detection_params dict

Detection parameters (chip_size, chip_stride, resolution, batch_size, detectree2_weights_path, sam3_checkpoint_path, etc.).

required
detector_dir Path

Directory for detector outputs. raw_detections.gpkg is written here.

required
preprocessed_local_files dict

Preprocessed local file paths (should have keys "ortho" and "chm").

required
Source code in tree_detection_framework/entrypoints/detect_trees.py
def detect_trees(
    detector: str,
    detection_params: dict,
    detector_dir: Path,
    preprocessed_local_files: dict,
):
    """
    Detect trees using the specified detector and save raw detections.

    Args:
        detector: Detector name: geometric, deepforest, detectree2, sam2, sam3, or tcd.
        detection_params: Detection parameters (chip_size, chip_stride, resolution, batch_size, detectree2_weights_path, sam3_checkpoint_path, etc.).
        detector_dir: Directory for detector outputs. raw_detections.gpkg is written here.
        preprocessed_local_files: Preprocessed local file paths (should have keys "ortho" and "chm").
    """
    ortho_path = preprocessed_local_files["ortho"]
    chm_path = preprocessed_local_files["chm"]
    output_path = detector_dir / "raw_detections.gpkg"

    output_path.parent.mkdir(parents=True, exist_ok=True)

    if detector == "geometric":
        detect_trees_two_stage(
            CHM_file=Path(chm_path),
            tree_tops_save_path=output_path,  # add tree_crowns_save_path if you want to save tree crowns too
            chip_size=int(detection_params.get("chip_size", CHIP_SIZE)),
            chip_stride=int(detection_params.get("chip_stride", CHIP_STRIDE)),
            resolution=float(detection_params.get("resolution", RESOLUTION)),
            raster_blur_sigma=(
                float(detection_params["raster_blur_sigma"])
                if detection_params.get("raster_blur_sigma")
                else None
            ),
            edge_suppression_meters=(
                float(detection_params["edge_suppression_meters"])
                if detection_params.get("edge_suppression_meters")
                else None
            ),
        )

    else:
        # CV-based detectors: deepforest, detectree2, sam2, sam3, tcd
        detector_kwargs = {
            k: _coerce_numeric(v)
            for k, v in detection_params.items()
            if k not in _STANDARD_PARAMS
        }
        generate_predictions(
            raster_folder_path=ortho_path,
            chip_size=float(detection_params["chip_size"]),
            chip_stride=float(detection_params["chip_stride"]),
            tree_detection_model=detector,
            resolution=(
                float(detection_params["resolution"])
                if detection_params.get("resolution")
                else None
            ),
            predictions_save_path=str(output_path),
            run_nms=False,
            batch_size=(
                int(detection_params["batch_size"])
                if detection_params.get("batch_size")
                else None
            ),
            detectree2_weights_path=detection_params.get(
                "detectree2_weights_path",
                Path(CHECKPOINTS_FOLDER, "230103_randresize_full.pth"),
            ),
            sam3_checkpoint_path=detection_params.get(
                "sam3_checkpoint_path",
                Path(CHECKPOINTS_FOLDER, "sam3.pt"),
            ),
            detector_kwargs=detector_kwargs,
        )

    print(f"[detect] Detections saved to {output_path}")

postprocess(postprocessing_id, chm_path, min_tree_height, postproc_config_path, detector_dir=None, raw_detections_path=None, output_path=None)

Apply postprocessing chain to raw tree detections and save final detections as detections.gpkg in detector_dir. This script is meant to be run after detect_trees.py.

Parameters:

Name Type Description Default
postprocessing_id str

Postprocessing config ID that corresponds to a chain of postprocessing steps defined in postproc_config_path. An empty string for geometric detector would apply only height filtering.

required
chm_path str

Path to the CHM raster (used for height filtering and polygon-to-point conversion).

required
min_tree_height float

Minimum tree height (meters) used to filter detections.

required
postproc_config_path Path

Path to postprocessing_config.yaml.

required
detector_dir Path | None

Directory containing raw_detections.gpkg. detections.gpkg is written here. Used to derive raw_detections_path and output_path if they are not provided.

None
raw_detections_path Path | None

Path to raw_detections.gpkg. If not provided, derived from detector_dir.

None
output_path Path | None

Path to write detections.gpkg. If not provided, derived from detector_dir.

None
Source code in tree_detection_framework/entrypoints/postprocess.py
def postprocess(
    postprocessing_id: str,
    chm_path: str,
    min_tree_height: float,
    postproc_config_path: Path,
    detector_dir: Path | None = None,
    raw_detections_path: Path | None = None,
    output_path: Path | None = None,
):
    """
    Apply postprocessing chain to raw tree detections and save final detections as detections.gpkg
    in detector_dir. This script is meant to be run after detect_trees.py.

    Args:
        postprocessing_id: Postprocessing config ID that corresponds to a chain of postprocessing steps
            defined in `postproc_config_path`. An empty string for geometric detector would apply only height filtering.
        chm_path: Path to the CHM raster (used for height filtering and polygon-to-point conversion).
        min_tree_height: Minimum tree height (meters) used to filter detections.
        postproc_config_path: Path to postprocessing_config.yaml.
        detector_dir: Directory containing raw_detections.gpkg. detections.gpkg is written here.
            Used to derive raw_detections_path and output_path if they are not provided.
        raw_detections_path: Path to raw_detections.gpkg. If not provided, derived from detector_dir.
        output_path: Path to write detections.gpkg. If not provided, derived from detector_dir.
    """
    if raw_detections_path is None or output_path is None:
        if detector_dir is None:
            raise ValueError(
                "detector_dir must be provided if raw_detections_path or output_path are not specified."
            )
    # detect_trees.py writes raw detections to detector_dir / "raw_detections.gpkg", so default to that if not provided
    raw_detections_path = raw_detections_path or detector_dir / "raw_detections.gpkg"
    output_path = output_path or detector_dir / "detections.gpkg"

    # Geometric: skip chain, just height filter
    if not postprocessing_id:
        detections = gpd.read_file(raw_detections_path)
        print(f"[postprocess] Loaded {len(detections)} raw detections.")

    # CV detectors: full postprocessor chain
    else:
        result = RegionDetectionsSet.from_tiled_file(raw_detections_path)
        print(f"[postprocess] Loaded {len(result.get_data_frame())} raw detections.")

        with open(postproc_config_path) as f:
            full_config = yaml.safe_load(f)

        if postprocessing_id not in full_config:
            raise ValueError(
                f"postprocessing_id '{postprocessing_id}' not found in {postproc_config_path}. "
                f"Available keys: {list(full_config.keys())}"
            )

        steps = full_config[postprocessing_id]

        for step in steps:
            fn_name = step["name"]
            kwargs = step.get("args", {})

            fn = getattr(postprocessors, fn_name, None)
            if fn is None:
                raise ValueError(
                    f"Unknown postprocessor '{fn_name}'. "
                    f"Not found in tree_detection_framework.postprocessing.postprocessing."
                )

            # filter_by_chm needs chm_path which is not in the yaml
            if fn_name == "filter_by_chm":
                kwargs["chm_path"] = chm_path

            print(f"[postprocess] Applying {fn_name} with args {kwargs}")
            result = fn(result, **kwargs)

        # Convert polygons to points, sampling CHM max within each crown
        print(f"[postprocess] Converting polygons to points via chm_max")
        result = polygons_to_points(result, method="chm_max", chm_path=chm_path)
        detections = result.drop(columns="crown_geometry")

    # Apply minimum height filter and save final detections
    n_before = len(detections)
    detections = detections[detections["height"] >= min_tree_height]
    print(f"[postprocess] Height filter: {n_before} -> {len(detections)} trees")
    output_path.parent.mkdir(parents=True, exist_ok=True)
    detections.to_file(output_path)
    print(f"[postprocess] Saved {len(detections)} detections to {output_path}")