Skip to content

Entrypoints

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

render_labels(mesh_file, cameras_file, image_folder, texture, render_savefolder, transform_file=None, subset_images_savefolder=None, texture_column_name=None, DTM_file=None, ground_height_threshold=None, render_ground_class=False, textured_mesh_savefile=None, ROI=None, mesh_ROI_buffer_radius_meters=50, cameras_ROI_buffer_radius_meters=150, IDs_to_labels=None, render_image_scale=1, mesh_downsample=1, n_render_clusters=None, vis=False, mesh_vis_file=None, labels_vis_folder=None)

Renders image-based labels using geospatial ground truth data

Parameters:

Name Type Description Default
mesh_file PATH_TYPE

Path to the Metashape-exported mesh file

required
cameras_file PATH_TYPE

Path to the MetaShape-exported .xml cameras file

required
image_folder PATH_TYPE

Path to the folder of images used to create the mesh

required
texture Union[PATH_TYPE, ndarray, None]

See TexturedPhotogrammetryMesh.load_texture

required
render_savefolder PATH_TYPE

Where to save the rendered labels

required
transform_file Union[PATH_TYPE, None]

File containing the transform from local coordinates to EPSG:4978. Defaults to None.

None
subset_images_savefolder Union[PATH_TYPE, None]

Where to save the subset of images for which labels are generated. Defaults to None.

None
texture_column_name Union[str, None]

Column to use in vector file for texture information". Defaults to None.

None
DTM_file Union[PATH_TYPE, None]

Path to a DTM file to use for ground thresholding. Defaults to None.

None
ground_height_threshold Union[float, None]

Set points under this height to ground. Only applicable if DTM_file is provided. Defaults to None.

None
render_ground_class bool

Should the ground class be included in the renders or deleted.. Defaults to False.

False
textured_mesh_savefile Union[PATH_TYPE, None]

Where to save the textured and subsetted mesh, if needed in the future. Defaults to None.

None
ROI Union[PATH_TYPE, GeoDataFrame, MultiPolygon, None]

The region of interest to render labels for. Defaults to None.

None
mesh_ROI_buffer_radius_meters float

The distance in meters to include around the ROI for the mesh. Defaults to 50.

50
cameras_ROI_buffer_radius_meters float

The distance in meters to include around the ROI for the cameras. Defaults to 150.

150
IDs_to_labels Union[None, dict]

Mapping between the integer labels and string values for the classes. Defaults to None.

None
render_image_scale float

Downsample the images to this fraction of the size for increased performance but lower quality. Defaults to 1.

1
mesh_downsample float

Downsample the mesh to this fraction of vertices for increased performance but lower quality. Defaults to 1.

1
n_render_clusters Union[int, None]

If set, break the camera set and mesh into this many clusters before rendering. This is useful for large meshes that are otherwise very slow. Defaults to None.

None
Source code in geograypher/entrypoints/render_labels.py
def render_labels(
    mesh_file: PATH_TYPE,
    cameras_file: PATH_TYPE,
    image_folder: PATH_TYPE,
    texture: typing.Union[PATH_TYPE, np.ndarray, None],
    render_savefolder: PATH_TYPE,
    transform_file: typing.Union[PATH_TYPE, None] = None,
    subset_images_savefolder: typing.Union[PATH_TYPE, None] = None,
    texture_column_name: typing.Union[str, None] = None,
    DTM_file: typing.Union[PATH_TYPE, None] = None,
    ground_height_threshold: typing.Union[float, None] = None,
    render_ground_class: bool = False,
    textured_mesh_savefile: typing.Union[PATH_TYPE, None] = None,
    ROI: typing.Union[PATH_TYPE, gpd.GeoDataFrame, shapely.MultiPolygon, None] = None,
    mesh_ROI_buffer_radius_meters: float = 50,
    cameras_ROI_buffer_radius_meters: float = 150,
    IDs_to_labels: typing.Union[dict, None] = None,
    render_image_scale: float = 1,
    mesh_downsample: float = 1,
    n_render_clusters: typing.Union[int, None] = None,
    vis: bool = False,
    mesh_vis_file: typing.Union[PATH_TYPE, None] = None,
    labels_vis_folder: typing.Union[PATH_TYPE, None] = None,
):
    """Renders image-based labels using geospatial ground truth data

    Args:
        mesh_file (PATH_TYPE):
            Path to the Metashape-exported mesh file
        cameras_file (PATH_TYPE):
            Path to the MetaShape-exported .xml cameras file
        image_folder (PATH_TYPE):
            Path to the folder of images used to create the mesh
        texture (typing.Union[PATH_TYPE, np.ndarray, None]):
            See TexturedPhotogrammetryMesh.load_texture
        render_savefolder (PATH_TYPE):
            Where to save the rendered labels
        transform_file (typing.Union[PATH_TYPE, None], optional):
            File containing the transform from local coordinates to EPSG:4978. Defaults to None.
        subset_images_savefolder (typing.Union[PATH_TYPE, None], optional):
            Where to save the subset of images for which labels are generated. Defaults to None.
        texture_column_name (typing.Union[str, None], optional):
            Column to use in vector file for texture information". Defaults to None.
        DTM_file (typing.Union[PATH_TYPE, None], optional):
            Path to a DTM file to use for ground thresholding. Defaults to None.
        ground_height_threshold (typing.Union[float, None], optional):
            Set points under this height to ground. Only applicable if DTM_file is provided. Defaults to None.
        render_ground_class (bool, optional):
            Should the ground class be included in the renders or deleted.. Defaults to False.
        textured_mesh_savefile (typing.Union[PATH_TYPE, None], optional):
            Where to save the textured and subsetted mesh, if needed in the future. Defaults to None.
        ROI (typing.Union[PATH_TYPE, gpd.GeoDataFrame, shapely.MultiPolygon, None], optional):
            The region of interest to render labels for. Defaults to None.
        mesh_ROI_buffer_radius_meters (float, optional):
            The distance in meters to include around the ROI for the mesh. Defaults to 50.
        cameras_ROI_buffer_radius_meters (float, optional):
            The distance in meters to include around the ROI for the cameras. Defaults to 150.
        IDs_to_labels (typing.Union[None, dict], optional):
            Mapping between the integer labels and string values for the classes. Defaults to None.
        render_image_scale (float, optional):
            Downsample the images to this fraction of the size for increased performance but lower quality. Defaults to 1.
        mesh_downsample (float, optional):
            Downsample the mesh to this fraction of vertices for increased performance but lower quality. Defaults to 1.
        n_render_clusters (typing.Union[int, None]):
            If set, break the camera set and mesh into this many clusters before rendering. This is
            useful for large meshes that are otherwise very slow. Defaults to None.
        mesh_vis (typing.Union[PATH_TYPE, None])
            Path to save the visualized mesh instead of showing it interactively. Only applicable if vis=True. Defaults to None.
        labels_vis (typing.Union[PATH_TYPE, None])
            Defaults to None.
    """
    ## Determine the ROI
    # If the ROI is unset and the texture is a geodataframe, set the ROI to that
    if ROI is None and isinstance(texture, gpd.GeoDataFrame):
        ROI = texture
    elif ROI is None and isinstance(texture, (str, Path)):
        try:
            ROI = gpd.read_file(texture)
        except fiona.errors.DriverError:
            pass

    # If the transform filename is None, use the cameras filename instead
    # since this contains the transform information
    if transform_file is None:
        transform_file = cameras_file

    ## Create the camera set
    # This is done first because it's often faster than mesh operations which
    # makes it a good place to check for failures
    camera_set = MetashapeCameraSet(cameras_file, image_folder)

    if ROI is not None:
        # Extract cameras near the training data
        camera_set = camera_set.get_subset_ROI(
            ROI=ROI, buffer_radius=cameras_ROI_buffer_radius_meters, is_geospatial=True
        )
    # If requested, save out the images corresponding to this subset of cameras.
    # This is useful for model training.
    if subset_images_savefolder is not None:
        camera_set.save_images(subset_images_savefolder)

    # Select whether to use a class that renders by chunks or not
    MeshClass = (
        TexturedPhotogrammetryMesh
        if n_render_clusters is None
        else TexturedPhotogrammetryMeshChunked
    )

    ## Create the textured mesh
    mesh = MeshClass(
        mesh_file,
        downsample_target=mesh_downsample,
        texture=texture,
        texture_column_name=texture_column_name,
        transform_filename=transform_file,
        ROI=ROI,
        ROI_buffer_meters=mesh_ROI_buffer_radius_meters,
        IDs_to_labels=IDs_to_labels,
    )

    ## Set the ground class if applicable
    if DTM_file is not None and ground_height_threshold is not None:
        # The ground ID will be set to the next value if None, or np.nan if np.nan
        ground_ID = None if render_ground_class else np.nan
        mesh.label_ground_class(
            DTM_file=DTM_file,
            height_above_ground_threshold=ground_height_threshold,
            only_label_existing_labels=True,
            ground_class_name="GROUND",
            ground_ID=ground_ID,
            set_mesh_texture=True,
        )

    # Save the textured and subsetted mesh, if applicable
    if textured_mesh_savefile is not None:
        mesh.save_mesh(textured_mesh_savefile)

    # Show the cameras and mesh if requested
    if vis or mesh_vis_file is not None:
        mesh.vis(camera_set=camera_set, screenshot_filename=mesh_vis_file)

    # Include n_render_clusters as an optional keyword argument, if provided. This is only applicable
    # if this mesh is a TexturedPhotogrammetryMeshChunked object
    render_kwargs = (
        {} if n_render_clusters is None else {"n_clusters": n_render_clusters}
    )
    # Render the labels and save them. This is the slow step.
    mesh.save_renders(
        camera_set=camera_set,
        render_image_scale=render_image_scale,
        save_native_resolution=True,
        output_folder=render_savefolder,
        make_composites=False,
        **render_kwargs,
    )

    if vis or labels_vis_folder is not None:
        # Show some examples of the rendered labels side-by-side with the real images
        show_segmentation_labels(
            label_folder=render_savefolder,
            image_folder=image_folder,
            savefolder=labels_vis_folder,
            num_show=10,
        )

aggregate_images(mesh_file, cameras_file, image_folder, label_folder, subset_images_folder=None, take_every_nth_camera=100, mesh_transform_file=None, DTM_file=None, height_above_ground_threshold=2.0, ROI=None, ROI_buffer_radius_meters=50, IDs_to_labels=None, mesh_downsample=1.0, n_aggregation_clusters=None, aggregate_image_scale=1.0, aggregated_face_values_savefile=None, predicted_face_classes_savefile=None, top_down_vector_projection_savefile=None, vis=False)

Aggregate labels from multiple viewpoints onto the surface of the mesh

Parameters:

Name Type Description Default
mesh_file PATH_TYPE

Path to the Metashape-exported mesh file

required
cameras_file PATH_TYPE

Path to the MetaShape-exported .xml cameras file

required
image_folder PATH_TYPE

Path to the folder of images used to create the mesh

required
label_folder PATH_TYPE

Path to the folder of labels to be aggregated onto the mesh. Must be in the same structure as the images

required
subset_images_folder Union[PATH_TYPE, None]

Use only images from this subset. Defaults to None.

None
take_every_nth_camera Union[int, None]

Downsample the camera set to only every nth camera if set. Defaults to None.

100
mesh_transform_file Union[PATH_TYPE, None]

Transform from the mesh coordinates to the earth-centered, earth-fixed frame. Can be a 4x4 matrix represented as a .csv, or a Metashape cameras file containing the information. Defaults to None.

None
DTM_file Union[PATH_TYPE, None]

Path to a digital terrain model file to remove ground points. Defaults to None.

None
height_above_ground_threshold float

Height in meters above the DTM to consider ground. Only used if DTM_file is set. Defaults to 2.0.

2.0
ROI Union[PATH_TYPE, None]

Geofile region of interest to crop the mesh to. Defaults to None.

None
ROI_buffer_radius_meters float

Keep points within this distance of the provided ROI object, if unset, everything will be kept. Defaults to 50.

50
IDs_to_labels Union[dict, None]

Maps from integer IDs to human-readable class name labels. Defaults to None.

None
mesh_downsample float

Downsample the mesh to this fraction of vertices for increased performance but lower quality. Defaults to 1.0.

1.0
n_aggregation_clusters Union[int, None]

If set, aggregate with this many clusters. Defaults to None.

None
aggregate_image_scale float

Downsample the labels before aggregation for faster runtime but lower quality. Defaults to 1.0.

1.0
aggregated_face_values_savefile Union[PATH_TYPE, None]

Where to save the aggregated image values as a numpy array. Defaults to None.

None
predicted_face_classes_savefile Union[PATH_TYPE, None]

Where to save the most common label per face texture as a numpy array. Defaults to None.

None
top_down_vector_projection_savefile Union[PATH_TYPE, None]

Where to export the predicted map. Defaults to None.

None
vis bool

Show the mesh model and predicted results. Defaults to False.

False
Source code in geograypher/entrypoints/aggregate_images.py
def aggregate_images(
    mesh_file: PATH_TYPE,
    cameras_file: PATH_TYPE,
    image_folder: PATH_TYPE,
    label_folder: PATH_TYPE,
    subset_images_folder: typing.Union[PATH_TYPE, None] = None,
    take_every_nth_camera: typing.Union[int, None] = 100,
    mesh_transform_file: typing.Union[PATH_TYPE, None] = None,
    DTM_file: typing.Union[PATH_TYPE, None] = None,
    height_above_ground_threshold: float = 2.0,
    ROI: typing.Union[PATH_TYPE, None] = None,
    ROI_buffer_radius_meters: float = 50,
    IDs_to_labels: typing.Union[dict, None] = None,
    mesh_downsample: float = 1.0,
    n_aggregation_clusters: typing.Union[int, None] = None,
    aggregate_image_scale: float = 1.0,
    aggregated_face_values_savefile: typing.Union[PATH_TYPE, None] = None,
    predicted_face_classes_savefile: typing.Union[PATH_TYPE, None] = None,
    top_down_vector_projection_savefile: typing.Union[PATH_TYPE, None] = None,
    vis: bool = False,
):
    """Aggregate labels from multiple viewpoints onto the surface of the mesh

    Args:
        mesh_file (PATH_TYPE):
            Path to the Metashape-exported mesh file
        cameras_file (PATH_TYPE):
            Path to the MetaShape-exported .xml cameras file
        image_folder (PATH_TYPE):
            Path to the folder of images used to create the mesh
        label_folder (PATH_TYPE):
            Path to the folder of labels to be aggregated onto the mesh. Must be in the same
            structure as the images
        subset_images_folder (typing.Union[PATH_TYPE, None], optional):
            Use only images from this subset. Defaults to None.
        take_every_nth_camera (typing.Union[int, None], optional):
            Downsample the camera set to only every nth camera if set. Defaults to None.
        mesh_transform_file (typing.Union[PATH_TYPE, None], optional):
            Transform from the mesh coordinates to the earth-centered, earth-fixed frame. Can be a
            4x4 matrix represented as a .csv, or a Metashape cameras file containing the
            information. Defaults to None.
        DTM_file (typing.Union[PATH_TYPE, None], optional):
            Path to a digital terrain model file to remove ground points. Defaults to None.
        height_above_ground_threshold (float, optional):
            Height in meters above the DTM to consider ground. Only used if DTM_file is set.
            Defaults to 2.0.
        ROI (typing.Union[PATH_TYPE, None], optional):
            Geofile region of interest to crop the mesh to. Defaults to None.
        ROI_buffer_radius_meters (float, optional):
            Keep points within this distance of the provided ROI object, if unset, everything will
            be kept. Defaults to 50.
        IDs_to_labels (typing.Union[dict, None], optional):
            Maps from integer IDs to human-readable class name labels. Defaults to None.
        mesh_downsample (float, optional):
            Downsample the mesh to this fraction of vertices for increased performance but lower
            quality. Defaults to 1.0.
        n_aggregation_clusters (typing.Union[int, None]):
            If set, aggregate with this many clusters. Defaults to None.
        aggregate_image_scale (float, optional):
            Downsample the labels before aggregation for faster runtime but lower quality. Defaults
            to 1.0.
        aggregated_face_values_savefile (typing.Union[PATH_TYPE, None], optional):
            Where to save the aggregated image values as a numpy array. Defaults to None.
        predicted_face_classes_savefile (typing.Union[PATH_TYPE, None], optional):
            Where to save the most common label per face texture as a numpy array. Defaults to None.
        top_down_vector_projection_savefile (typing.Union[PATH_TYPE, None], optional):
            Where to export the predicted map. Defaults to None.
        vis (bool, optional):
            Show the mesh model and predicted results. Defaults to False.
    """
    ## Create the camera set
    # Do the camera operations first because they are fast and good initial error checking
    camera_set = MetashapeCameraSet(cameras_file, image_folder, validate_images=True)

    # If the ROI is not None, subset to cameras within a buffer distance of the ROI
    # TODO let get_subset_ROI accept a None ROI and return the full camera set
    if subset_images_folder is not None:
        camera_set = camera_set.get_cameras_in_folder(subset_images_folder)

    # If you only want to take every nth camera, helpful for initial testing
    if take_every_nth_camera is not None:
        camera_set = camera_set.get_subset_cameras(
            range(0, len(camera_set), take_every_nth_camera)
        )

    if ROI is not None and ROI_buffer_radius_meters is not None:
        # Extract cameras near the training data
        camera_set = camera_set.get_subset_ROI(
            ROI=ROI, buffer_radius=ROI_buffer_radius_meters
        )

    if mesh_transform_file is None:
        mesh_transform_file = cameras_file

    # Choose whether to use a mesh class that aggregates by clusters of cameras and chunks of the mesh
    MeshClass = (
        TexturedPhotogrammetryMesh
        if n_aggregation_clusters is None
        else TexturedPhotogrammetryMeshChunked
    )
    ## Create the mesh
    mesh = MeshClass(
        mesh_file,
        transform_filename=mesh_transform_file,
        ROI=ROI,
        ROI_buffer_meters=ROI_buffer_radius_meters,
        IDs_to_labels=IDs_to_labels,
        downsample_target=mesh_downsample,
    )

    # Show the mesh if requested
    if vis:
        mesh.vis(camera_set=camera_set)

    # Create a segmentor object to load in the predictions
    segmentor = LookUpSegmentor(
        base_folder=image_folder,
        lookup_folder=label_folder,
        num_classes=np.max(list(mesh.get_IDs_to_labels().keys())) + 1,
    )
    # Create a camera set that returns the segmented images instead of the original ones
    segmentor_camera_set = SegmentorPhotogrammetryCameraSet(
        camera_set, segmentor=segmentor
    )

    # Create the potentially-empty dict of kwargs to match what this class expects
    n_clusters_kwargs = (
        {} if n_aggregation_clusters is None else {"n_clusters": n_aggregation_clusters}
    )

    ## Perform aggregation, this is the slow step
    aggregated_face_labels, _ = mesh.aggregate_projected_images(
        segmentor_camera_set,
        aggregate_img_scale=aggregate_image_scale,
        **n_clusters_kwargs
    )

    # If requested, save this data
    if aggregated_face_values_savefile is not None:
        ensure_containing_folder(aggregated_face_values_savefile)
        np.save(aggregated_face_values_savefile, aggregated_face_labels)

    # Find the index of the most common class per face, with faces with no predictions set to nan
    predicted_face_classes = find_argmax_nonzero_value(
        aggregated_face_labels, keepdims=True
    )

    # If requested, label the ground faces
    if DTM_file is not None and height_above_ground_threshold is not None:
        predicted_face_classes = mesh.label_ground_class(
            labels=predicted_face_classes,
            height_above_ground_threshold=height_above_ground_threshold,
            DTM_file=DTM_file,
            ground_ID=np.nan,
            set_mesh_texture=False,
        )

    if predicted_face_classes_savefile is not None:
        ensure_containing_folder(predicted_face_classes_savefile)
        np.save(predicted_face_classes_savefile, predicted_face_classes)

    if vis:
        # Show the mesh with predicted classes
        mesh.vis(vis_scalars=predicted_face_classes)

    # TODO this should be updated to take IDs_to_labels
    mesh.export_face_labels_vector(
        face_labels=np.squeeze(predicted_face_classes),
        export_file=top_down_vector_projection_savefile,
        vis=vis,
    )

project_detections(mesh_filename, cameras_filename, project_to_mesh=False, convert_to_geospatial=False, image_folder=None, detections_folder=None, projections_to_mesh_filename=None, projections_to_geospatial_savefilename=None, default_focal_length=None, image_shape=None, segmentor_kwargs={}, vis_mesh=False, vis_geodata=False)

Project per-image detections to geospatial coordinates

Parameters:

Name Type Description Default
mesh_filename PATH_TYPE

Path to mesh file, in local coordinates from Metashape

required
cameras_filename PATH_TYPE

Path to cameras file. This also contains local-to-global coordinate transform to convert the mesh to geospatial units.

required
project_to_mesh bool

Execute the projection to mesh step. Defaults to False.

False
convert_to_geospatial bool

Execute the conversion to geospatial step. Defaults to False.

False
image_folder PATH_TYPE

Path to the folder of images used to generate the detections. TODO, see if this can be removed since none of this information is actually used. Defaults to None.

None
detections_folder PATH_TYPE

Folder of detections in the DeepForest format, one per image. Defaults to None.

None
projections_to_mesh_filename PATH_TYPE

Where to save and/or load from the data for the detections projected to the mesh faces. Defaults to None.

None
projections_to_geospatial_savefilename PATH_TYPE

Where to export the geospatial detections. Defaults to None.

None
default_focal_length float

Since the focal length is not provided in many cameras files, it can be specified. The units are in pixels. TODO, figure out where this information can be reliably obtained from. Defaults to None.

None
segmentor_kwargs dict

Dict of keyword arguments to pass to the segmentor. Defaults to {}.

{}
vis_mesh bool

Show the mesh with detections projected onto it. Defaults to False.

False
vis_geodata bool

Show the geospatial projection. Defaults to False.

False

Raises:

Type Description
ValueError

If convert_to_geospatial but no projections to mesh are available

FileNotFoundError

If the projections_to_mesh_filename is set and needed but not present

Source code in geograypher/entrypoints/project_detections.py
def project_detections(
    mesh_filename: PATH_TYPE,
    cameras_filename: PATH_TYPE,
    project_to_mesh: bool = False,
    convert_to_geospatial: bool = False,
    image_folder: PATH_TYPE = None,
    detections_folder: PATH_TYPE = None,
    projections_to_mesh_filename: PATH_TYPE = None,
    projections_to_geospatial_savefilename: PATH_TYPE = None,
    default_focal_length: float = None,
    image_shape: tuple = None,
    segmentor_kwargs: dict = {},
    vis_mesh: bool = False,
    vis_geodata: bool = False,
):
    """Project per-image detections to geospatial coordinates

    Args:
        mesh_filename (PATH_TYPE):
            Path to mesh file, in local coordinates from Metashape
        cameras_filename (PATH_TYPE):
            Path to cameras file. This also contains local-to-global coordinate transform to convert
            the mesh to geospatial units.
        project_to_mesh (bool, optional):
            Execute the projection to mesh step. Defaults to False.
        convert_to_geospatial (bool, optional):
            Execute the conversion to geospatial step. Defaults to False.
        image_folder (PATH_TYPE, optional):
            Path to the folder of images used to generate the detections. TODO, see if this can be
            removed since none of this information is actually used. Defaults to None.
        detections_folder (PATH_TYPE, optional):
            Folder of detections in the DeepForest format, one per image. Defaults to None.
        projections_to_mesh_filename (PATH_TYPE, optional):
            Where to save and/or load from the data for the detections projected to the mesh faces.
            Defaults to None.
        projections_to_geospatial_savefilename (PATH_TYPE, optional):
            Where to export the geospatial detections. Defaults to None.
        default_focal_length (float, optional):
            Since the focal length is not provided in many cameras files, it can be specified.
            The units are in pixels. TODO, figure out where this information can be reliably obtained
            from. Defaults to None.
        segmentor_kwargs (dict, optional):
            Dict of keyword arguments to pass to the segmentor. Defaults to {}.
        vis_mesh (bool, optional):
            Show the mesh with detections projected onto it. Defaults to False.
        vis_geodata (bool, optional):
            Show the geospatial projection. Defaults to False.

    Raises:
        ValueError: If convert_to_geospatial but no projections to mesh are available
        FileNotFoundError: If the projections_to_mesh_filename is set and needed but not present
    """
    # Create the mesh object, which will be used for either workflow
    mesh = TexturedPhotogrammetryMeshIndexPredictions(
        mesh_filename, transform_filename=cameras_filename
    )

    # Project per-image detections to the mesh
    if project_to_mesh:
        # Create a camera set associated with the images that have detections
        camera_set = MetashapeCameraSet(
            cameras_filename,
            image_folder,
            default_sensor_params={"f": default_focal_length, "cx": 0, "cy": 0},
        )
        # Infer the image shape from the first image in the folder
        if image_shape is None:
            image_filename_list = sorted(list(Path(image_folder).glob("*.*")))
            if len(image_filename_list) > 0:
                first_file = image_filename_list[0]
                logging.info(f"loading image shape from {first_file}")
                first_image = imread(first_file)
                image_shape = first_image.shape[:2]
            else:
                raise ValueError(
                    f"No image_shape provided and folder of images {image_folder} was empty"
                )
        # Create an object that looks up the detections from a folder of CSVs or one individual one.
        # Using this, it can generate "predictions" for a given image.
        detections_predictor = TabularRectangleSegmentor(
            detection_file_or_folder=detections_folder,
            image_folder=image_folder,
            image_shape=image_shape,
            **segmentor_kwargs,
        )

        # If a file is provided for the projections, save the detection info alongside it
        if projections_to_mesh_filename is not None:
            # Export the per-image detection information as one standardized file
            detection_info_file = Path(
                projections_to_mesh_filename.parent,
                projections_to_mesh_filename.stem + "_detection_info.csv",
            )
            logging.info(f"Saving detection info to {detection_info_file}")
            detections_predictor.save_detection_data(detection_info_file)

        # Wrap the camera set so that it returns the detections rather than the original images
        detections_camera_set = SegmentorPhotogrammetryCameraSet(
            camera_set, segmentor=detections_predictor
        )
        # Project the detections to the mesh
        aggregated_prejected_images_returns = mesh.aggregate_projected_images(
            cameras=detections_camera_set, n_classes=detections_predictor.num_classes
        )
        # Get the summed (not averaged) projections
        aggregated_projections = aggregated_prejected_images_returns[1][
            "summed_projections"
        ]

        if projections_to_mesh_filename is not None:
            # Export the per-face texture to an npz file, since it's a sparse array
            ensure_containing_folder(projections_to_mesh_filename)
            save_npz(projections_to_mesh_filename, aggregated_projections)

        if vis_mesh:
            # Determine which detection is predicted for each face, if any. In cases where multiple
            # detections project to the same face, the one with the lower index will be reported
            detection_ID_per_face = np.argmax(aggregated_projections, axis=1).astype(
                float
            )
            # Mask out locations for which there are no predictions
            detection_ID_per_face[np.sum(aggregated_projections, axis=1) == 0] = np.nan
            # Show the mesh
            mesh.vis(vis_scalars=detection_ID_per_face)

    # Convert per-face projections to geospatial ones
    if convert_to_geospatial:
        # Determine if the mesh texture was computed in the last step or otherwise if it can be loaded
        if not project_to_mesh:
            if projections_to_mesh_filename is None:
                raise ValueError("No projections_to_mesh_savefilename provided")
            elif os.path.isfile(projections_to_mesh_filename):
                aggregated_projections = load_npz(projections_to_mesh_filename)
                detection_info_file = Path(
                    projections_to_mesh_filename.parent,
                    projections_to_mesh_filename.stem + "_detection_info.csv",
                )
                detection_info = pd.read_csv(detection_info_file)
            else:
                raise FileNotFoundError(
                    f"projections_to_mesh_filename {projections_to_mesh_filename} not found"
                )
        else:
            detection_info = detections_predictor.get_all_detections()

        # Convert the per-face labels to geospatial coordinates. Optionally vis and/or export
        mesh.export_face_labels_vector(
            face_labels=aggregated_projections,
            export_file=projections_to_geospatial_savefilename,
            vis=vis_geodata,
        )

        projected_geo_data = gpd.read_file(projections_to_geospatial_savefilename)
        # Merge the two dataframes so the left df's "class_ID" field aligns with the right df's
        # "instance_ID". This will add back the original data assocaited with each per-image detection
        # to the projected data.
        # Add the "_right" suffix to any of the original fields that share a name with the ones in the
        # projected data
        merged = projected_geo_data.merge(
            detection_info,
            left_on=CLASS_ID_KEY,
            right_on=INSTANCE_ID_KEY,
            suffixes=(None, "_right"),
        )
        # Drop the columns that are just an integer ID, except for "instance_ID"
        # TODO determine why "Unnamed: 0" appears
        merged.drop(columns=[CLASS_ID_KEY, "Unnamed: 0"], inplace=True)

        # Save the data back out with the updated information
        merged.to_file(projections_to_geospatial_savefilename)

label_polygons(mesh_file, mesh_transform_file, aggregated_face_values_file, geospatial_polygons_to_label, geospatial_polygons_labeled_savefile, mesh_downsample=1.0, DTM_file=None, height_above_ground_threshold=2.0, ground_voting_weight=0.01, ROI=None, ROI_buffer_radius_meters=50, n_polygons_per_cluster=1000, IDs_to_labels=None, vis_mesh=False)

Label each polygon with the most commonly predicted class as computed by the weighted sum of 3D face areas

Parameters:

Name Type Description Default
mesh_file PATH_TYPE

Path to the Metashape-exported mesh file

required
mesh_transform_file PATH_TYPE

Transform from the mesh coordinates to the earth-centered, earth-fixed frame. Can be a 4x4 matrix represented as a .csv, or a Metashape cameras file containing the information.

required
aggregated_face_values_file PATH_TYPE

Path to a (n_faces, n_classes) numpy array containing the frequency of each class prediction for each face

required
geospatial_polygons_to_label Union[PATH_TYPE, None]

Each polygon/multipolygon will be labeled independently. Defaults to None.

required
geospatial_polygons_labeled_savefile Union[PATH_TYPE, None]

Where to save the labeled results.

required
mesh_downsample float

Fraction to downsample mesh. Should match what was used to generate the aggregated_face_values_file. Defaults to 1.0.

1.0
DTM_file Union[PATH_TYPE, None]

Path to a digital terrain model file to remove ground points. Defaults to None.

None
height_above_ground_threshold float

Height in meters above the DTM to consider ground. Only used if DTM_file is set. Defaults to 2.0.

2.0
ground_voting_weight float

Faces identified as ground are given this weight during voting. Defaults to 0.01.

0.01
ROI Union[PATH_TYPE, None]

Geofile region of interest to crop the mesh to. Should match what was used to generate aggregated_face_values_file. Defaults to None.

None
ROI_buffer_radius_meters float

Keep points within this distance of the provided ROI object, if unset, everything will be kept. Should match what was used to generate aggregated_face_values_file. Defaults to 50.

50
n_polygons_per_cluster int

The number of polygons to use in each cluster, when computing labeling by chunks. Defaults to 1000.

1000
IDs_to_labels Union[dict, None]

Mapping from integer IDs to human readable labels. Defaults to None.

None
Source code in geograypher/entrypoints/label_polygons.py
def label_polygons(
    mesh_file: PATH_TYPE,
    mesh_transform_file: PATH_TYPE,
    aggregated_face_values_file: PATH_TYPE,
    geospatial_polygons_to_label: typing.Union[PATH_TYPE, None],
    geospatial_polygons_labeled_savefile: typing.Union[PATH_TYPE, None],
    mesh_downsample: float = 1.0,
    DTM_file: typing.Union[PATH_TYPE, None] = None,
    height_above_ground_threshold: float = 2.0,
    ground_voting_weight: float = 0.01,
    ROI: typing.Union[PATH_TYPE, None] = None,
    ROI_buffer_radius_meters: float = 50,
    n_polygons_per_cluster: int = 1000,
    IDs_to_labels: typing.Union[dict, None] = None,
    vis_mesh: bool = False,
):
    """
    Label each polygon with the most commonly predicted class as computed by the weighted sum of 3D
    face areas

    Args:
        mesh_file (PATH_TYPE):
            Path to the Metashape-exported mesh file
        mesh_transform_file (PATH_TYPE):
            Transform from the mesh coordinates to the earth-centered, earth-fixed frame. Can be a
            4x4 matrix represented as a .csv, or a Metashape cameras file containing the information.
        aggregated_face_values_file (PATH_TYPE):
            Path to a (n_faces, n_classes) numpy array containing the frequency of each class
            prediction for each face
        geospatial_polygons_to_label (typing.Union[PATH_TYPE, None], optional):
            Each polygon/multipolygon will be labeled independently. Defaults to None.
        geospatial_polygons_labeled_savefile (typing.Union[PATH_TYPE, None], optional):
            Where to save the labeled results.
        mesh_downsample (float, optional):
            Fraction to downsample mesh. Should match what was used to generate the
            aggregated_face_values_file. Defaults to 1.0.
        DTM_file (typing.Union[PATH_TYPE, None], optional):
            Path to a digital terrain model file to remove ground points. Defaults to None.
        height_above_ground_threshold (float, optional):
            Height in meters above the DTM to consider ground. Only used if DTM_file is set.
            Defaults to 2.0.
        ground_voting_weight (float, optional):
            Faces identified as ground are given this weight during voting. Defaults to 0.01.
        ROI (typing.Union[PATH_TYPE, None], optional):
            Geofile region of interest to crop the mesh to. Should match what was used to generate
            aggregated_face_values_file. Defaults to None.
        ROI_buffer_radius_meters (float, optional):
            Keep points within this distance of the provided ROI object, if unset, everything will
            be kept. Should match what was used to generate aggregated_face_values_file. Defaults to 50.
        n_polygons_per_cluster (int, optional):
            The number of polygons to use in each cluster, when computing labeling by chunks.
            Defaults to 1000.
        IDs_to_labels (typing.Union[dict, None], optional):
            Mapping from integer IDs to human readable labels. Defaults to None.
    """
    # Load this first because it's quick
    aggregated_face_values = np.load(aggregated_face_values_file)
    predicted_face_classes = np.argmax(aggregated_face_values, axis=1).astype(float)
    no_preds_mask = np.all(np.logical_not(np.isfinite(aggregated_face_values)), axis=1)
    predicted_face_classes[no_preds_mask] = np.nan

    ## Create the mesh
    mesh = TexturedPhotogrammetryMeshChunked(
        mesh_file,
        transform_filename=mesh_transform_file,
        ROI=ROI,
        ROI_buffer_meters=ROI_buffer_radius_meters,
        IDs_to_labels=IDs_to_labels,
        downsample_target=mesh_downsample,
    )

    if vis_mesh:
        mesh.vis(vis_scalars=predicted_face_classes)

    # Extract which vertices are labeled as ground
    # TODO check that the types are correct here
    ground_mask_verts = mesh.get_height_above_ground(
        DTM_file=DTM_file,
        threshold=height_above_ground_threshold,
    )
    # Convert that vertex labels into face labels
    ground_mask_faces = mesh.vert_to_face_texture(ground_mask_verts)

    # Ground points get a weighting of ground_voting_weight, others get 1
    ground_weighting = 1 - (
        (1 - ground_voting_weight) * ground_mask_faces.astype(float)
    )
    if vis_mesh:
        ground_masked_predicted_face_classes = predicted_face_classes.copy()
        ground_masked_predicted_face_classes[ground_mask_faces.astype(bool)] = np.nan
        mesh.vis(vis_scalars=ground_masked_predicted_face_classes)

    # Perform per-polygon labeling
    polygon_labels = mesh.label_polygons(
        face_labels=predicted_face_classes,
        polygons=geospatial_polygons_to_label,
        face_weighting=ground_weighting,
        n_polygons_per_cluster=n_polygons_per_cluster,
    )

    # Save out the predicted classes into a copy of the original file
    geospatial_polygons = gpd.read_file(geospatial_polygons_to_label)
    geospatial_polygons[PRED_CLASS_ID_KEY] = polygon_labels
    ensure_containing_folder(geospatial_polygons_labeled_savefile)
    geospatial_polygons.to_file(geospatial_polygons_labeled_savefile)