avatarDan Carlson

Summary

The provided content outlines a method for using Python scripts to automate the iterative gradual selection process in Agisoft Metashape, enhancing the accuracy of sparse point clouds by removing low-accuracy tie points based on USGS guidelines.

Abstract

The article delves into the use of Python programming to streamline the Agisoft Metashape workflow, focusing on the iterative gradual selection technique to refine sparse point clouds. It explains the importance of removing low-accuracy tie points using accuracy measures such as reconstruction uncertainty, projection accuracy, and reprojection error. The author references USGS guidelines to establish threshold values for these measures and provides detailed Python functions to automate the process. The code iteratively adjusts thresholds to select and remove points without exceeding a 50% deletion rate, aiming to improve the overall accuracy of the point cloud. The article also includes before-and-after visualizations of the point cloud, demonstrating the effectiveness of the gradual selection process. By integrating these Python functions into the Metashape workflow, users can achieve more precise camera parameter optimization and higher quality 3D models.

Opinions

  • The author emphasizes the importance of adhering to USGS processing guidelines for gradual selection to ensure the quality of the point cloud.
  • Iterative gradual selection is presented as a critical step in refining the accuracy of tie points within Agisoft Metashape, which can significantly impact the quality of the final 3D model.
  • The USGS recommended threshold for reconstruction uncertainty is considered a starting point, with the iterative process allowing for dynamic adjustment based on the specific dataset.
  • The article suggests that high-quality cameras may allow for stricter projection accuracy thresholds, while lower quality images may require more lenient thresholds.
  • The author advocates for an iterative approach to gradual selection that avoids deleting more than 50% of the points with each change in the threshold.
  • The provided Python code is designed to be repetitive in parts to ensure thoroughness in the gradual selection process, with the suggestion that these parts could be combined into a single function for efficiency.
  • The effectiveness of the gradual selection process is visually demonstrated through the reduction of points in the point cloud while maintaining its integrity.
  • The article concludes by encouraging readers to automate gradual selection in Agisoft Metashape using Python and hints at future discussions on more complex processes such as checking camera parameters for convergence.

How to Use Python to Streamline Your Agisoft Metashape Workflow: Part II

Clean the sparse point cloud using iterative gradual selection

A sparse point cloud in Agisoft Metashape with low-accuracy points highlighted in pink.

The first post in this series provided instructions and python code to automate image alignment in Agisoft Metashape. This post will show you how to use python to perform iterative gradual selection to remove low-accuracy tie points.

Iterative Gradual Selection

Gradual selection is the process by which low-accuracy tie points can be identified and removed. Gradual selection options can be accessed in the Metashape GUI by navigating to Model → Gradual Selection

A Reconstruction Uncertainty threshold of 10.859 is used to select tie points in this example. Screenshot of the Gradual Selection Window in Agisoft Metashape by the author on 2023–09–05

Thresholds for each measure can be set, allowing tie points that exceed those thresholds to be removed from the point cloud. Removing tie points with low accuracy will enhance the accuracy interior and exterior camera parameters. Therefore, the camera optimization should be repeated after removing tie points. Iterative gradual selection is the process by which a threshold is adjusted to select low-accuracy tie points while not exceeding a specific percentage of the total number of tie points.

Gradual Selection Explained

The USGS has defined ideal threshold ranges for each parameter in a comprehensive Open File Report by Over et al. (2021).

The three accuracy measures used here include:

  • Reconstruction uncertainty
  • Projection accuracy
  • Reprojection error

To understand these accuracy measures it is helpful to think of the sparse point cloud as a 3D jigsaw puzzle.

Reconstruction Uncertainty

The reconstruction uncertainty measures how sure you are that a given puzzle piece is in the right spot. If you’re really sure then the piece fits perfectly. If you’re not sure then it might not fit at all.

Projection Accuracy

The projection accuracy measures how accurately you can point to the location of each puzzle piece. Your projection accuracy is very good if you’re viewing the puzzle from across the room and you can point to a single puzzle piece with a laser pointer. On the other hand, your projection accuracy is very bad if you can only gesture broadly in the general direction of where a puzzle piece.

Reprojection Error

The reprojection error measures how close a puzzle piece is to where it should be. If a puzzle piece is in exactly the right spot then the reprojection error is low.

Coding Gradual Selection in Python

The USGS recommends using a reconstruction uncertainty threshold of 10, without selecting more than 50% of the points.

Reconstruction Uncertainty

With these goals in mind, define a function called grad_selection_RU.

This function reads in our initial threshold, the maximum number of iterations, the percentage of points it can delete per iteration, and the threshold increment. grad_selection_RU begins by adjusting the initial threshold, up or down, until a sufficient number of points have been selected. Then the threshold is adjusted until the number of points selected divided by the total number of points meets or exceeds the limit and these points are removed.

def grad_selection_RU(RU_thrsh, num_tries=4, pct_del=10, thrsh_incr=1):
    n=0
    target_thrsh = 10
    init_thrsh = RU_thrsh
    points = chunk.tie_points.points
    # compute number of starting points
    points_start_num = len(points)
    f = Metashape.TiePoints.Filter()
    f.init(chunk, criterion = Metashape.TiePoints.Filter.ReconstructionUncertainty)
    # select points that exceed initial threshold 
    f.selectPoints(init_thrsh)
    # count number of points selected
    nselected = len([True for point in points if point.valid is True and point.selected is True])
    # check to see if any points were selected
    pct_selected = (nselected / points_start_num)*100
    # decrease the initial threshold if few points were selected
    if pct_selected <= 1:
        while True:
            print(f"Current threshold is {init_thrsh}. Adjusting downward by {thrsh_incr}")
            init_thrsh -= thrsh_incr
            f.selectPoints(init_thrsh)
            nselected = len([True for point in points if point.valid is True and point.selected is True])
            ps = (nselected / points_start_num)*100
            if ps >= 1:
                break
    # increase the initial threshold if too many points were selected
    elif pct_selected > pct_del:
        while True:
            if pct_selected > pct_del:
                init_thrsh += thrsh_incr
                f.selectPoints(init_thrsh)
                nselected = len([True for point in points if point.valid is True and point.selected is True])
                ps = (nselected / points_start_num)*100
                if ps < pct_del:
                    break
    print(f"New adjusted initial threshold is {init_thrsh}. Beginning gradual selection.")
    # begin iterative gradual selection
    n+=1
    print(f"Removing {nselected} points.")
    f.removePoints(init_thrsh)
    init_thrsh -= thrsh_incr
    chunk.optimizeCameras(adaptive_fitting=True,fit_f = True, fit_cx = True, fit_cy = True, fit_b1 = True, fit_b2 = True, fit_k1 = True, fit_k2 = True, fit_k3 = True, fit_k4 = True, fit_p1 = True, fit_p2 = True)
    points = chunk.tie_points.points
    npoints = len(points)
    while True:
        n+=1
        if n>num_tries or init_thrsh<=target_thrsh or (100*((points_start_num-npoints)/points_start_num))>=50:
            break
        else:
            points = chunk.tie_points.points
            npoints = len(points)
            f.selectPoints(init_thrsh)
            nselected = len([True for point in points if point.valid is True and point.selected is True])
            pct_selected = (nselected / npoints)*100
            while True:
                if pct_selected <= pct_del:
                    init_thrsh -= thrsh_incr/5
                    f.selectPoints(init_thrsh)
                    nselected = len([True for point in points if point.valid is True and point.selected is True])
                    pct_selected = (nselected/npoints)*100
                else:
                    break
            f.selectPoints(init_thrsh)
            nselected = len([True for point in points if point.valid is True and point.selected is True])
            if nselected > 0:
                print(f"Removing {nselected} points at a threshold of {init_thrsh}")
                f.removePoints(init_thrsh)
                init_thrsh -= thrsh_incr
                chunk.optimizeCameras(adaptive_fitting=True,fit_f = True, fit_cx = True, fit_cy = True, fit_b1 = True, fit_b2 = True, fit_k1 = True, fit_k2 = True, fit_k3 = True, fit_k4 = True, fit_p1 = True, fit_p2 = True)
                points = chunk.tie_points.points
                npoints = len(points)

Projection Accuracy

Tie points with a projection accuracy = 1 have the highest accuracy. A tie point with a projection accuracy of 2 has double the error, and so on. Images acquired by high-quality cameras may allow a projection accuracy threshold of 3 (perhaps even 2) to be used. Images obtained by lower quality cameras (e.g. action cams) or images that have been compressed should use thresholds > 5. Again, the USGS guidance suggests that threshold values should be approached iteratively without deleting more than 50% the points with each change in the threshold.

Let’s define a function called grad_selection_PA that reads in the threshold value and iteratively removes points.

def grad_selection_PA(PA_thrsh, num_tries=4, pct_del=10, thrsh_incr=1):
    n=0
    target_thrsh = 3
    init_thrsh=PA_thrsh
    points = chunk.tie_points.points
    points_start_num = len(points)
    npoints = len(points)
    f = Metashape.TiePoints.Filter()
    f.init(chunk, criterion = Metashape.TiePoints.Filter.ProjectionAccuracy)
    f.selectPoints(init_thrsh)
    # count number of points selected
    nselected = len([True for point in points if point.valid is True and point.selected is True])
    # check to see if any points were selected
    pct_selected = (nselected / points_start_num)*100
    if init_thrsh<=target_thrsh and pct_selected <= 50: # job done
        print(f"Now removing {nselected} points at a threshold of {init_thrsh}")
        f.removePoints(init_thrsh)
        chunk.optimizeCameras(adaptive_fitting=True,fit_f = True, fit_cx = True, fit_cy = True, fit_b1 = True, fit_b2 = True, fit_k1 = True, fit_k2 = True, fit_k3 = True, fit_k4 = True, fit_p1 = True, fit_p2 = True)
    else:
        while True:
            n+=1
            if n>num_tries or init_thrsh<=target_thrsh or (100*((points_start_num-npoints)/points_start_num))>=50:
                break
            else:
                points = chunk.tie_points.points
                npoints = len(points)
                f.selectPoints(init_thrsh)
                nselected = len([True for point in points if point.valid is True and point.selected is True])
                pct_selected = (nselected / npoints)*100
                while True:
                    if pct_selected <= pct_del:
                        init_thrsh -= thrsh_incr/5
                        f.selectPoints(init_thrsh)
                        nselected = len([True for point in points if point.valid is True and point.selected is True])
                        pct_selected = (nselected/npoints)*100
                    else:
                        break
                f.selectPoints(init_thrsh)
                nselected = len([True for point in points if point.valid is True and point.selected is True])
                if nselected > 0:
                    print(f"Removing {nselected} points at a threshold of {init_thrsh}")
                    f.removePoints(init_thrsh)
                    init_thrsh -= thrsh_incr
                    chunk.optimizeCameras(adaptive_fitting=True,fit_f = True, fit_cx = True, fit_cy = True, fit_b1 = True, fit_b2 = True, fit_k1 = True, fit_k2 = True, fit_k3 = True, fit_k4 = True, fit_p1 = True, fit_p2 = True)
                    points = chunk.tie_points.points
                    npoints = len(points)

Reprojection Error

A threshold of 0.25 — 0.3 pixels should be sufficient for most projects. USGS guidance suggests that the reprojection error threshold should selected such that it does not delete more than 10% of the remaining tie points in each iteration.

Let’s define a function called grad_selection_RE to do this.

def grad_selection_RE(RE_thrsh, num_tries=10, pct_del=10, thrsh_incr=0.1):
    n=0
    target_thrsh=0.3
    init_thrsh=RE_thrsh
    points = chunk.tie_points.points
    points_start_num = len(points)
    npoints = len(points)
    f = Metashape.TiePoints.Filter()
    f.init(chunk, criterion = Metashape.TiePoints.Filter.ReprojectionError)
    f.selectPoints(init_thrsh)
    # count number of points selected
    nselected = len([True for point in points if point.valid is True and point.selected is True])
    # check to see if any points were selected
    pct_selected = (nselected / points_start_num)*100
    if init_thrsh <= target_thrsh and pct_selected <= pct_del: # job done
        print(f"Now removing {nselected} points at a threshold of {init_thrsh}")
        f.removePoints(init_thrsh)
        chunk.optimizeCameras(adaptive_fitting=True,fit_f = True, fit_cx = True, fit_cy = True, fit_b1 = True, fit_b2 = True, fit_k1 = True, fit_k2 = True, fit_k3 = True, fit_k4 = True, fit_p1 = True, fit_p2 = True)
    else:
        while True:
            n+=1
            if n>num_tries or init_thrsh<=target_thrsh or (100*((points_start_num-npoints)/points_start_num))>=pct_del:
                break
            else:
                points = chunk.tie_points.points
                npoints = len(points)
                f.selectPoints(init_thrsh)
                nselected = len([True for point in points if point.valid is True and point.selected is True])
                pct_selected = (nselected / npoints)*100
                while True:
                    if pct_selected <= pct_del:
                        init_thrsh -= thrsh_incr
                        f.selectPoints(init_thrsh)
                        nselected = len([True for point in points if point.valid is True and point.selected is True])
                        pct_selected = (nselected/npoints)*100
                    else:
                        break
                f.selectPoints(init_thrsh)
                nselected = len([True for point in points if point.valid is True and point.selected is True])
                if nselected > 0:
                    print(f"Removing {nselected} points at a threshold of {init_thrsh}")
                    f.removePoints(init_thrsh)
                    init_thrsh -= thrsh_incr
                    chunk.optimizeCameras(adaptive_fitting=True,fit_f = True, fit_cx = True, fit_cy = True, fit_b1 = True, fit_b2 = True, fit_k1 = True, fit_k2 = True, fit_k3 = True, fit_k4 = True, fit_p1 = True, fit_p2 = True)
                    points = chunk.tie_points.points
                    npoints = len(points)

Summary

These functions perform basic iterative gradual selection using the reconstruction uncertainty, projection accuracy, and reprojection error and by applying the USGS processing guidelines.

The three functions contain repetitive sections that could be combined into a single function.

To use these functions, copy them into your python/text editor along with the code below.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import Metashape

# Reconstruction Uncertainty (RU) filter threshold
RU_thrsh = 12
PA_thrsh = 6
RE_thrsh = 0.5

# gradual selection functions

def copy_new_chunk(chunk, old_label, new_label):
    new_chunk = chunk.copy()
    clbl_i = str("".join([old_label,new_label]))
    new_chunk.label = clbl_i
    return clbl_i
    
def find_chunk(doc, label):
    if type(doc) != Metashape.Document:
       print("Not a valid MetaShape file!")
       return False
    if type(label) != str:
       print("Please provide a string as input!")
       return False
    for chunk in doc.chunks:
       if chunk.label == label:
          print(f"Chunk {label} was found.")
          return(chunk)
    print(f"Chunk {label} was not found!")
    return None

# Main body of program 

Projs = ["/data2/metashape_course/20230828_mcourse.psx"
]
# chunk(s) to process within a project
clbls = [                                                                      
        "run1-OC", 
        ]
doc = Metashape.app.document
for proj in Projs:
    doc.open(proj)
    proj_name = proj.split("/")[len(proj.split("/"))-1]
    proj_path = "/".join(proj.split("/")[:-1])
    for clbl in clbls:
        for chunk in doc.chunks:   
            if chunk.label != clbl:
                continue
            print("")
            print("Now processing project <", proj_name, ">...")
            # find chunk by name
            chunk = find_chunk(doc, clbl)
            # sets selected chunk as active
            doc.chunk = chunk
            # reconstruction uncertainty
            ru_clbl = copy_new_chunk(chunk,clbl,"-GS-RU") # copy to new chunk before proceeding
            print(f"Now performing gradual selection using reconstruction uncertainty with a threshold of {RU_thrsh}")
            chunk = find_chunk(doc, ru_clbl)
            doc.chunk = chunk
            grad_selection_RU(RU_thrsh)
            doc.save()
            # projection accuracy
            pa_clbl = copy_new_chunk(chunk,  ru_clbl, "-PA")
            print(f"Now performing gradual selection using projection accuracy with a threshold of {PA_thrsh}")
            chunk = find_chunk(doc, pa_clbl)
            doc.chunk = chunk
            grad_selection_PA(PA_thrsh)
            doc.save()
            # reprojection error
            re_clbl = copy_new_chunk(chunk,  pa_clbl, "-RE")
            print(f"Now performing gradual selection using reprojection error with a threshold of {RE_thrsh}")
            chunk = find_chunk(doc, re_clbl)
            doc.chunk = chunk
            grad_selection_RE(RE_thrsh)
            doc.save()

Results

The gradual selection code starts with the chunk containing the sparse point cloud after camera optimization (run1-OC). The results of the gradual selection process are shown in the images below.

The point cloud before gradual selection

Before gradual selection, the point cloud contained 227,186 points.

The point cloud after gradual selection based on reconstruction uncertainty

After calling the reconstruction uncertainty function, the number of points was reduced to 138,311.

The point cloud after gradual selection based on projection accuracy

The number of points was further reduced to 73,125 after calling the projection accuracy function.

The point cloud after gradual selection based on reprojection error

Finally, after applying the reprojection error, the point cloud contained 62,991 points.

Wrapping Up

Now you should be able to automate gradual selection in Agisoft Metashape using python. Of course we can make this process more complex by checking camera parameters for convergence, but that’s a topic for another post!

Drones
Python
Python Programming
Computer Science
Science
Recommended from ReadMedium
avatarAbhishek Kumar Pandey
Monocular Depth Estimation

Easy:

6 min read