Advanced usage

Project validation during processing

The Pix4Dengine enables performing checks on the quality report after each processing step is completed. This is done by constructing a Pipeline with a validators argument.

Define a validator

The validator is a callable which must accept as argument an instance of Report. Here we show how this can be used for checking calibration quality and stopping processing if the calibration is judged insufficient based on some user-defined criterion.

First, we define a callable that returns False if the calibration quality in the project is too poor, and True otherwise. The callable also defines a message, providing a description of why the validation does not succeed.



class CalibrationChecker:
    def __init__(self):
        self.message = "calibration quality is insufficient"

    def __call__(self, report):
        from pix4dengine.utils.report import Quality

        return all(q == Quality.SUCCESS for q in report.calibration_quality_status())


Use a validator with Pipeline

Similar behaviour can be accomplished with pipelines. Pipeline may contain multiple tasks, therefore one has to define which task will be validated. This can be done by constructing a Pipeline with the validators argument. This argument is a dictionary where keys are task names and values are previously defined validators.


from pix4dengine.pipeline import Pipeline, apply_template
from pix4dengine.utils.errors import FailedValidation
from pix4dengine.pipeline.templates import RapidMaps3D

pipeline = Pipeline(
    project=project, algos=["CALIB", "DENSE", "ORTHO"], validators={"CALIB": CalibrationChecker()}
)
apply_template(pipeline, RapidMaps3D)  # fast processing

try:
    pipeline.run()
except FailedValidation as stop:
    print("Stopping processing, %s." % str(stop))

Note that CalibrationChecker will be called only after finishing the "CALIB" task.

Filtering outliers from a densified point cloud

The standard densification task can be configured to flag outliers (noise) in the generated point cloud. The outlier points are flagged and can later be filtered out by the meshing algorithm thus producing a better mesh.

In order to produce a point cloud where the outlier points are flagged, we need to do the following. As before, we create a pipeline for processing a project. This pipeline will run two tasks, the calibration task and the point cloud densification task. The latter will also run the meshing algorithm. Next, we update the configuration of the densification task to make it flag the outliers in the generated point cloud. Finally, we run the pipeline. If after densifying the point cloud any outliers are detected, they will be filtered out by the meshing algorithm.

# Define a pipeline that includes a densification task
pipeline = Pipeline(project=project, algos=("CALIB", "DENSE"))

# Configure the "DENSE" task to flag outliers in the generated point cloud
dense_with_pointcloud_outliers_task = pipeline.get_task("DENSE")
dense_with_pointcloud_outliers_task.update_config(
    {AlgoOption.Densification.PCL_FLAG_OUTLIERS: True}
)

# Run the pipeline
pipeline.run()

The outlier detection algorithm runs a statistical analysis of a point’s neighborhood and flags the points that don’t meet a given criterion as outliers. The statistical analysis computes the average distance from each point to its neighbors and flags a point as an outlier if that average distance is larger than the overall mean of all average distances plus some margin. The hypothesis is that the density of a point cloud around outlier points is much lower as fewer measurements agree together, thus the distance between an outlier point and its neighbors is bigger than the distance between a valid point and its neighbors.

The algorithm takes two input parameters, in addition to the point cloud itself, which define the neighborhood of a point, and the distance threshold. These parameters have to be tuned for each particular case depending on the nature of the noise in a point cloud. The first parameter is the number of neighbors of a point to consider when computing the average distance from that point to its neighbors. The second parameter controls the threshold on that distance, and is defined as the mean of all average distances plus a certain number of times the standard deviation of all average distances, i.e. t = x + SIGMA_COEF * σ.

The two parameters have reasonable default values for the case when the noise in the point cloud is random, i.e. there are single randomly distributed noisy points that don’t follow a pattern. These default parameters are 6 for the number of neighbors, and 1.0 for SIGMA_COEF. When the nature of the noise is such that the noisy points form clusters, the neighborhood has to be large enough to identify the clusters as noise. When the amount of noise is important, the overall mean of average distances between all points and their respective neighbors is going to increase. In order to filter out noisy points in that case, the threshold should be made smaller. In general, a smaller threshold means a more aggressive filtering.

The tuning of the algorithm with different values of the neighborhood and the threshold can be done by giving the size of the neighborhood and SIGMA_COEF to the standard densification task as part of its configuration.

# Tune the outlier flagging algorithm for:
# (1) the number of neighbors of a point to consider when computing a local average distance from a point to its neighbours
dense_with_pointcloud_outliers_task.update_config(
    {AlgoOption.Densification.PCL_FLAG_OUTLIERS_NEIGHBOR_COUNT: 50}
)
# (2) the threshold on the standard deviation of the distribution of average local distances to neighbours
dense_with_pointcloud_outliers_task.update_config(
    {AlgoOption.Densification.PCL_FLAG_OUTLIERS_SIGMA_COEF: 0.1}
)

# Run the pipeline
pipeline.run()

Processing multispectral projects

Pix4Dengine supports multispectral cameras and can thus be used for algriculture projects. The following code sample shows how to generate an NDVI map using multispectral images:

from pix4dengine.pipeline import Pipeline
from pix4dengine.constants.index import Indices
from pix4dengine.options import AlgoOption, ExportOption

# Define a pipeline where we run only CALIB and ORTHO
pipeline = Pipeline(project=project, algos=("CALIB", "ORTHO"))

# Get the ortho task
ortho = pipeline.get_task("ORTHO")

# Produce only the reflectance maps
ortho.update_config(
    {
        ExportOption.Ortho.MOSAIC_TIFF: False,
        ExportOption.Ortho.MOSAIC_TIFF_MERGED: False,
        ExportOption.Ortho.DSM_TIFF: False,
        ExportOption.Ortho.DSM_TIFF_MERGED: False,
        ExportOption.Index.REFLECTANCE: True,
        ExportOption.Index.REFLECTANCE_MERGED: True,
    }
)

# Add the NDVI index
ortho.update_config({AlgoOption.Index.INDICES: [Indices.NDVI.value]})

# Run the pipeline
pipeline.run()

We could have used the pix4dengine.pipeline.templates.AgriMultispectral template. The main difference with the code above is that the AgriMultispectral template sets non-default calibration options such as CALIBRATION_METHOD and MATCH_GEOMETRICALLY_VERIFIED.

CUDA support

CUDA is NVidia proprietary technology for General-Purpose computing on Graphics Processing Units (GPUs). CUDA is used during calibration and usually brings significant speedups.

pix4dengine requires an NVidia GPU supporting CUDA 9.1, which corresponds to a compute capability version of 3.0, and driver version 390.46 or greater. Further information can be found on Wikipedia or on the NVidia websites.

CUDA is supported in pix4dengine on both Ubuntu 18.04 and Windows and is used automatically if a compatible GPU is available.

The function number_of_cuda_gpus() returns the number of compatible GPUs that can be leveraged. When creating a Pipeline, the argument enable_cuda can be set to False in order not to use CUDA.

Custom cameras

A custom camera configuration can be passed to the create_project() function. Only this camera configuration shall be used, and it will not be saved to the database

pix4dengine uses two databases to store calibrated cameras. We refer to them as “internal camera database” (ICDB) and “user camera database” (UCDB) for cameras calibrated by Pix4D and users, respectively. The UCDB is initially empty. Users may calibrate custom cameras and save them there for later use. The purpose of this section is to illustrate how to do this.

Functions and types required to modify the user camera database are present in camera module.

User camera database

It is possible to access or change the location of the UCDB. The database can be erased.

from pix4dengine.camera import user_database_location, change_user_database_location

path_to_db = user_database_location()
print(f"System user configuration: {path_to_db}")
new_user_db = Path(tempfile.mkdtemp()) / "user_db.xml"
change_user_database_location(str(new_user_db))
new_path_to_db = user_database_location()
print(f"Temporary user db: {new_path_to_db}")
System user configuration: /home/user/.local/share/pix4d/common/19/ucmdb.xml
Temporary user db: /tmp/tmp74ok79t7/user_db.xml

Remember that user camera database location is saved in the system configuration, per system user. To temporarily change the location of the UCDB location user_camera_database() managed context can be used, as follows:

from pix4dengine.camera import user_camera_database, clean_userdb

with user_camera_database(new_user_db):
    print(f"Just created temporary user db: {user_database_location()}")
with user_camera_database() as location:
    print(f"Another temporary db: {location}")
with user_camera_database(path_to_db):
    print(f"Saved before system configuration: {user_database_location()}")
    clean_userdb()
Just created temporary user db: /tmp/tmp74ok79t7/user_db.xml
Another temporary db: /tmp/pix4dengine_db_4vbkb738/user_db.xml
Saved before system configuration: /home/user/.local/share/pix4d/common/19/ucmdb.xml

As can be seen, it is possible to clean up user database using clean_userdb().

Example

Before adding a custom camera to the database let’s have a look at an example where this is crucial: a wrong camera may be the cause of a failing pipeline.

project = create_project("test_project", image_directory, work_dir=tempfile.mkdtemp())
pipeline = Pipeline(project=project, algos=["CALIB"])

try:
    pipeline.run()
except Exception:
    print(f"Failed because cannot find calibrated camera.")
Failed because cannot find calibrated camera.

Retrieve a camera

A camera configuration (CameraConfig) can be retrieved from pix4dengine database by name or with use of an image. It will be the best camera that pix4dengine can use for that image.

from pix4dengine.camera import get_camera_config_from_image

path_to_image = glob(image_directory + "/*.JPG")[0]
camera_config, camera_id = get_camera_config_from_image(path_to_image)

print(f"Camera identifier {camera_id}.")
print(camera_config)
Camera identifier CanonIXUS220HS_4.3_4000x3000.
Camera:
        Serial number=
        Image width= 4000
        Image height= 3000
        Pixel size in micrometers= 1.5494
        Maker name= Canon
        Model name= CanonIXUS220HS
        Bands=
                Spectral band name= Red
                Central wavelength in microns= 660
                Width of the band in microns= 0
                Band weight for grayscale mapping= 0.2126
                Spectral band name= Green
                Central wavelength in microns= 550
                Width of the band in microns= 0
                Band weight for grayscale mapping= 0.7152
                Spectral band name= Blue
                Central wavelength in microns= 470
                Width of the band in microns= 0
                Band weight for grayscale mapping= 0.0722
        Shutter type= Global
        Sensor=
                Perspective Sensor:
                        Focal length= 35
                        Principal point x-cord= 3.12942
                        Principal point y-cord= 2.39692
                        First radial distortion parameter= 18
                        Second radial distortion parameter= 0.0259073
                        Third radial distortion parameter= -0.00608853
                        First tangential distortion parameter= 0.00119999
                        Second tangential distortion parameter= 0.00169852
                        Number of nonzero distortion parameters= 5
        Lens model= None
        Pixel values=
        Vignetting=
        Velocity number= 0
        Rolling shutter readout time= 0

Camera can be retrieved by identifier. A camera identifier has a pattern CameraModel_FocalLength_WidthxHeight or CameraModel_LensModel_FocalLength_WidthxHeight.

from pix4dengine.camera import get_camera_config_from_name

camera_config = get_camera_config_from_name("CanonIXUS220HS_4.3_4000x3000")
print(f"Retrieved camera: {str(camera_config)[:100]}...")
Retrieved camera: Camera:
        Serial number=
        Image width= 4000
        Image height= 3000
        Pixel size in micrometers= 1.5494
        ...

Add a camera to the user database.

A camera can be saved to the user database. Note that it has to be saved under a correct identifier (CameraModel_FocalLength_WidthxHeight or CameraModel_LensModel_FocalLength_WidthxHeight). In the code below it is camera_id from previous paragraph.

from pix4dengine.camera import add_camera_to_userdb

camera_config.sensor.focal_length_in_mm = 4.39974
camera_config.sensor.radial_K1 = -0.042563
add_camera_to_userdb(camera_config, camera_id)

project = create_project("test_project", image_directory, work_dir=tempfile.mkdtemp())
pipeline = Pipeline(project=project, algos=["CALIB"])
apply_template(pipeline, RapidMaps3D)
pipeline.run()

Note

pix4dengine will try to find the best camera while creating a project. Therefore the camera should be added before project creation.

Remove a camera from the user database.

The camera configuration can be removed from the user database. Do do so just use remove_cameras_from_userdb(). Note that there can be more than one camera that is mapped to this identifier.

from pix4dengine.camera import remove_cameras_from_userdb

removed_ids = remove_cameras_from_userdb(camera_id)
print(f"Removed: {removed_ids}.")
Removed: ['CanonIXUS220HS_4.4_4000x3000'].

After this operation our example will fail again because calibration cannot success without calibrated camera.

project = create_project("test_project", image_directory, work_dir=tempfile.mkdtemp())
pipeline = Pipeline(project=project, algos=["CALIB"])
apply_template(pipeline, RapidMaps3D)
try:
    pipeline.run()
except Exception:
    print(f"Failed.")
Failed.

Create a camera configuration manually

To add a new camera we can start by creating one from scratch, by creating an object of CameraConfig. All required structures are documented in camera.

from pix4dengine.camera import CameraConfig, PerspectiveSensorConfig, BandConfig

camera_config = CameraConfig()
camera_config.image_width = 4000
camera_config.image_height = 3000
camera_config.pixel_size_in_um = 1.5494
camera_config.maker_name = "Canon"
camera_config.model_name = "CanonIXUS220HS"
bR = BandConfig(name="Red", central_wavelength=660, width=0, weight=0.2126)
bG = BandConfig(name="Green", central_wavelength=550, width=0, weight=0.7152)
bB = BandConfig(name="Blue", central_wavelength=470, width=0, weight=0.0722)
camera_config.bands = [bR, bG, bB]
camera_config.shutter_type = "Global"
sensor = PerspectiveSensorConfig()
sensor.focal_length_in_mm = 4.39974
sensor.principal_point_x_in_mm = 3.12942
sensor.principal_point_y_in_mm = 2.39692
sensor.radial_K1 = -0.042563
sensor.radial_K2 = 0.0259073
sensor.radial_K3 = -0.00608853
sensor.tangential_T1 = 0.00119999
sensor.tangential_T2 = 0.00169852
camera_config.sensor = sensor
add_camera_to_userdb(camera_config, camera_id)

project = create_project("test_project", image_directory, work_dir=tempfile.mkdtemp())
pipeline = Pipeline(project=project, algos=["CALIB"])
apply_template(pipeline, RapidMaps3D)

pipeline.run()

Use a custom camera configuration

A custom camera configuration can be specified explicitly when creating a project. In this case the camera database will not be used.

project = create_project(
    "test_project", image_directory, work_dir=tempfile.mkdtemp(), camera_config=camera_config
)
pipeline = Pipeline(project=project, algos=["CALIB"])
apply_template(pipeline, RapidMaps3D)