Examples

Processing scenarios using high-level functions

Setup

In all the following examples, it is assumed that the user created a pix4dengine.enginewrapper.EngineWrapper instance using

from pix4dengine.settings import set_exe_path
from pix4dengine.enginewrapper import EngineWrapper

set_exe_path("/opt/pix4dmapper/bin/pix4dmapper")

engine_wrapper = EngineWrapper()
if not engine_wrapper.is_logged_in():
    engine_wrapper.login("<youremail>", "<yourpassword>")

Furthermore, to actually use these examples, one would need to provide meaningful values to the variable used, namely project_name, images_directory and project_directory.

Note

The examples hereinafter are meant as a sequence of code samples. Imports, variable definitions, etc. are thus not repeated in each sample. In the examples, it is also assumed that images_directory and project_directory are variables containing paths to these resources.

Creating or opening a project

A project can be created using the pix4dengine.create_project() function:

import pix4dengine as p4d

project = p4d.create_project("test_project",
                             engine_wrapper=engine_wrapper,
                             images_dir=images_directory,
                             work_dir=project_directory)

A previously created project can be opened using the pix4dengine.open_project() method:

existing_project = p4d.open_project("test_project", project_directory)

A minimal processing workflow

In the most basic workflow, a previously created project can simply be processed using all the default settings, using pix4dengine.process_project(). It is necessary to explicitly define which processing steps should be run, by passing one or a sequence of ProcessingStep objects to process_project():

from pix4dengine.constants.processing import ProcessingStep

p4d.process_project(project, engine_wrapper,
                    steps=(ProcessingStep.CALIB,
                           ProcessingStep.DENSE))

Processing flow control

Individual processing steps can be run separately, checking for successful completion of each one before proceeding to the following one. See Project for how to use a quality validator.

from pix4dengine.utils.errors import ProjectProcessingError, FailedValidation


def _check_report(report):
    from pix4dengine.utils.report import Quality
    return report.calibration_quality_status().georeferencing == Quality.SUCCESS


try:
    p4d.process_project(project, engine_wrapper,
                        steps=(ProcessingStep.CALIB,
                               ProcessingStep.DENSE,
                               ProcessingStep.ORTHO),
                        validator=_check_report)

except FailedValidation:
    print("Warning: georefering information available.")

else:
    p4d.process_project(project, engine_wrapper,
                        steps=ProcessingStep.DENSE)

Create a project using a processing template

Pre-defined templates can be used to create a project. They can be obtained from the ProcessingTemplate object.

from pix4dengine.constants.processing import ProcessingTemplate

project_from_template = p4d.create_project("test_project", images_directory, engine_wrapper,
                                           work_dir=project_directory,
                                           template=ProcessingTemplate.MAPS_3D_RAPID)

p4d.process_project(project_from_template, engine_wrapper, steps=ProcessingStep.CALIB)

It is also possible to use a tmpl file generated by Pix4Dmapper, passing it to the template option, e.g., as template="/path/to/file.tmpl" on Linux or template=r"C:\path\to\file.tmpl" on Windows.

Any option modified using pix4dengine.project.Project.set_options() after project creation will overwrite the template settings.

Setting algorithmic or export options

The algorithmic options for configuring a project are collected in AlgoOption. The toggles for enabling or disabling specific kinds of output can be found in ExportOption. To change any of these options, one can use the set_options() method of a Project instance.

from pix4dengine.options import AlgoOption, ExportOption

# set one algorithmic option
project.set_options((AlgoOption.CameraCalibration.MATCH_STRATEGY, "AutoAerial"))

# set one export toggle
project.set_options((ExportOption.Densification.PCL_LAS, True))

# set the two options in one shot
project.set_options((AlgoOption.CameraCalibration.MATCH_STRATEGY, "AutoAerial"),
                    (ExportOption.Densification.PCL_LAS, True))

Using a CSV file to geolocate the project images

If your project images do not contain GPS data, it is possible to use a CSV file to geolocate the images when creating the project. This is achieved by using the ExternalGeolocation and ExternalGeolocationFormat objects:

from pix4dengine.constants.processing import ExternalGeolocation, ExternalGeolocationFormat

project = p4d.create_project("test_geoloc_project",
                             images_directory,
                             engine_wrapper,
                             work_dir=project_directory,
                             external_geoloc=ExternalGeolocation(
                                 file_path=path_to_geoloc_file,
                                 file_format=ExternalGeolocationFormat.LONG_LAT))

This support page provides more information on such files.

Defining custom callbacks

Custom callback functions allow user-defined actions before or after some task is performed, or when an error occurs during processing. The example below shows how a callback can be defined and used to print a message when a processing step fails. First, a toy callback receiver class is defined, and an instance of it is linked to the on_error event of the Callbacks callback configuration.

from pix4dengine.utils.enginewrapper import Callbacks


class CustomCallback:
    """Set up a callback using a message template."""
    def __init__(self, message_template):
        self.num_calls = 0
        self.message_template = message_template

    def __call__(self, error_description):
        """Trigger the callback and print a message."""
        self.num_calls += 1
        print(self.message_template % (error_description.message,
                                       error_description.pix4d_error_code))


# Callback receiver instance
on_error_receiver = CustomCallback("Processing failed: %s, error code=%s.")

# Callback configuration
callback_setup = Callbacks(on_start=lambda: print("Start processing."),
                           on_error=on_error_receiver,
                           on_success=lambda: print("Finished processing."))

A new project is created. To generate a processing failure, the point-cloud densification step is run before calibration. This raises a ProjectProcessingError that is caught, printing available information on the error that occurred:

# Create a new project
project = p4d.create_project("test_project",
                             engine_wrapper=engine_wrapper,
                             images_dir=images_directory,
                             work_dir=project_directory)

# This fails, as no calibration is available
try:
    p4d.process_project(project, engine_wrapper,
                        steps=ProcessingStep.DENSE,
                        callbacks=callback_setup)  # <-- set the callbacks

except ProjectProcessingError:
    print("The error callback was called %d times." % on_error_receiver.num_calls)

This produces the following output:

Start processing.
Processing failed: densification failed, was calibration run?, error code=None.
The error callback was called 1 times.

Note that the on_error callback is executed despite the failure of the processing, but not the on_success callback, which would run only upon successful completion of the task.

The callbacks can be any valid python function. The callback triggered on failure can optionally accept a CallbackMessage instance, that provides information on the error and its possible cause. See the documentation of Callbacks for further details.

Using a processing area

It is sometimes useful to manually define a processing area, instead of processing the entire area covered by calibrated images. This can be done using the set_processing_area() method of Project().

from pix4dengine.utils.project import PointXY, MinMaxRange

vertices = [
    PointXY(x=424945., y=5132553.),
    PointXY(x=425025., y=5132545.),
    PointXY(x=425129., y=5132478.),
    PointXY(x=425055., y=5132329.),
    PointXY(x=424878., y=5132447.)
]

height_interval = MinMaxRange(min=750., max=780.)

project.set_processing_area(vertices, height_interval)

# Only running the initial calibration
p4d.process_project(project, engine_wrapper,
                    steps=ProcessingStep.CALIB)

At least three points are needed to define the processing area horizontally, otherwise a ValueError is raised. Setting a processing area overwrites any previous definition. To define a processing area, use the map coordinates.

The default processing pipeline, available in pix4dengine.enginewrapper.Pipeline, automatically sets a processing area, based on the calibrated camera positions. This can substantially reduce the processing time for orthomosaic generation and the number of artefacts in the outputs produced.

Adding Ground Control Points (GCPs) with image marks

Adding a GCP (GCP3D) with image marks (Mark) to a project is done using add_3d_gcp_with_marks().

GCPs and marks are defined independently, and only associated when added to a project:

from pix4dengine.utils.gcp import GCP3D, Mark

# The GCP definition
gcp1 = GCP3D(label="gcp1", id=1,
             alt=51.444, lat=51.21884, lon=5.98994,
             xy_accuracy=0.02, z_accuracy=0.02)

# Marks on images
gcp1_marks = (
    Mark(photo="R0079100.JPG", x=2165.942, y=146.395, scale=1.34585),
    Mark(photo="R0079101.JPG", x=1726.303, y=1147.080, scale=1.34585),
    Mark(photo="R0079102.JPG", x=1289.577, y=2028.925, scale=1.24913),
)

# Associate marks to the GCP
project.add_3d_gcp_with_marks(gcp1, gcp1_marks)

Multiple GCPs are defined in most cases. After adding them to a project, they are used for processing the calibration step:

gcp2 = GCP3D(label="gcp2", id=2,
             alt=45.689, lat=51.21881, lon=5.98991,
             xy_accuracy=0.02, z_accuracy=0.02)
gcp2_marks = (
    Mark(photo="R0079100.JPG", x=2144.649, y=224.019, scale=1.16656),
    Mark(photo="R0079101.JPG", x=1730.395, y=1179.870, scale=1.16656),
    Mark(photo="R0079102.JPG", x=1320.590, y=2009.453, scale=1.11227),
)

gcp3 = GCP3D(label="gcp3", id=3,
             alt=271.076, lat=51.21914, lon=5.99044,
             xy_accuracy=0.02, z_accuracy=0.02)
gcp3_marks = (
    Mark(photo="R0079100.JPG", x=2311.575, y=652.467, scale=1.11054),
    Mark(photo="R0079101.JPG", x=1903.187, y=1639.156, scale=1.87584),
    Mark(photo="R0079102.JPG", x=1482.204, y=2502.646, scale=1.70531),
)

project.add_3d_gcp_with_marks(gcp2, gcp2_marks)
project.add_3d_gcp_with_marks(gcp3, gcp3_marks)

# Calibrate using the GCPs defined
p4d.process_project(project, engine_wrapper, steps=ProcessingStep.CALIB)

If GCPs are defined using map coordinates, you need to make sure you have set the proper projected coordinate systems. See the coordinate system section for more information.

Finding the output files

Expected output location

The expected location of the output files produced by processing a project can be obtained from a project using the function pix4dengine.exports.get_expected_output(). To check the actual availability of these files after processing, the function pix4dengine.exports.get_available_output() can be used.

To demonstrate this functionality, we first activate only two exports in a project:

from pix4dengine.exports import get_expected_output, get_available_output

# Define which exports to produce
project.set_options((ExportOption.Densification.PCL_LAS, True),  # From ProcessingStep.DENSE
                    (ExportOption.Ortho.MOSAIC_TIFF, True))  # From ProcessingStep.ORTHO

The expected output locations of these two files can already be obtained, even before processing:

file_paths = get_expected_output(project,
                                 ExportOption.Ortho.MOSAIC_TIFF, ExportOption.Densification.PCL_LAS)

file_paths is a dictionary which maps an export option to the expected file, e.g.:

file_paths = {
    ExportOption.Ortho.MOSAIC_TIFF:
        '/path/to/project/3_dsm_ortho/2_mosaic/tiles/proj_name_transparent_mosaic_group1_*.tif',
    ExportOption.Densification.PCL_LAS:
        '/path/to/project/2_densification/point_cloud/proj_name_group1_densified_point_cloud.las'
}

Note that we used Unix-style paths in this example, but Windows-style ones would obviously be returned on that platform. Also note that this function can raise UnsetExportException if requesting the expected output location for an ExportOption that is set to False. To know all the expected output files from a given project, taking into account the ExportOption toggles, the same method with the special argument "all" can be used:

file_paths = get_expected_output(project, "all")

When using the special "all" argument, unset ExportOption are automatically ignored.

Available output files

To check for available output, get_available_output() can be used in a way similar to get_expected_output(). This function additionally checks which files have actually been produced, and only those that were found on disk at the expected location are returned. If no file is found for an ExportOption, an empty list is associated to it in the returned dictionary. If the ExportOption was set to False, it will be ignored when using the "all" argument:

p4d.process_project(project, engine_wrapper,
                    steps=[ProcessingStep.CALIB,
                           ProcessingStep.DENSE])

file_paths = get_available_output(project, "all")

The dictionary returned by the function will look like this:

file_paths == {
    ExportOption.Densification.PCL_LAS:
        ['/path/to/project/2_densification/point_cloud/proj_name_group1_densified_point_cloud.las'],
    ExportOption.Ortho.MOSAIC_TIFF:
        [],
    StandardExport.Report.PDF:
        ['/path/to/project/1_initial/report/proj_name_report.pdf']}

The two requested outputs are included, as well as StandardExport.REPORTS.PDF which is always produced. Since the ORTHO step was not run, ExportOption.MOSAIC_TIFF was not produced, and the list of available files for this option is thus empty.

Accessing the processing quality reports

A pdf quality report is always produced, and can be accessed using StandardExport as for the ExportOption (with the difference that the former is always set to True):

from pix4dengine.options import StandardExport
from pix4dengine.exports import get_report

path_to_pdf = get_expected_output(project, StandardExport.Report.PDF)

The quality report can also be accessed using the get_report() function, returning a Report instance that lets you access the processing quality diagnostics.

# Get the quality report
quality_report = get_report(project)

The code above will print the relative difference between initial and optimized internal camera parameters, as well as the quality check status:

Relative difference = 1.3%

Calibration quality status:
images              = success
dataset             = success
camera_optimization = warning
matching            = success
georeferencing      = warning

Accessing or changing the project coordinate systems

A Pix4Dmapper project contains three coordinate systems (CS): the output CS, the images CS and the GCPs CS. Each CS is made of a horizontal and a vertical CS. The CoordSys class provides methods for knowing which CSs are in use and to modify them. An instance of this class can be obtained with the coord_sys() property of the Project class. The CoordSys class allows to obtain and modify the horizontal coordinate systems using the Well-Known Text format or using projected CSs such as “JGD2011 / Japan Plane Rectangular CS VIII”. For what concerns the vertical CS, they can be arbitrary, they can be set to be a certain number of meters above the WGS 84 ellipsoid or they can be set using a geoid listed in Geoid.

from textwrap import fill
from pix4dengine.constants.coordsys import ProjectCS
print("Output CS = %s " % project.coord_sys.get_cs(ProjectCS.OUTPUT))
print("GCPs CS = %s" % project.coord_sys.get_cs(ProjectCS.GCPS))
print("GCPs CS as WKT\n\n%s" % fill(project.coord_sys.get_cs(ProjectCS.GCPS, as_wkt=True), 80))

This code should print something like the following:

Project CS = WGS 84 / UTM zone 32N
GCPs CS = WGS 84 / UTM zone 32N
GCPs CS as WKT

PROJCS["WGS 84 / UTM zone 32N",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84
",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIM
EM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTH
ORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"
],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",9],PARAMETER["s
cale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing
",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northin
g",NORTH],AUTHORITY["EPSG","32632"]]

A list of recognised CSs or a selection can be obtained using list_cs():

from pix4dengine.coordsys import list_cs
list_cs("albania")

This code should return the following:

['Albanian 1987',
 'Albanian 1987 / Gauss Kruger zone 4 (deprecated)',
 'Albanian 1987 / Gauss-Kruger zone 4',
 'ETRS89 / Albania LCC 2010',
 'ETRS89 / Albania TM 2010']

Finally, it is possible to change either a single CS or all CSs as follows:

from pix4dengine.constants.coordsys import ProjectCS, Geoid
# Modify the output CS
project.coord_sys.set_cs(ProjectCS.OUTPUT, "Tokyo / Japan Plane Rectangular CS XVIII")
project.coord_sys.set_vert_cs(ProjectCS.OUTPUT, Geoid.GEOID96)

# Modify only the GCPs CS
project.coord_sys.set_cs(ProjectCS.GCPS, "Tokyo / Japan Plane Rectangular CS XVIII")
project.coord_sys.set_vert_cs(ProjectCS.GCPS, 3.)  # 3. meters above WGS 84

# Modify the output (project) CS using a WKT
project.coord_sys.set_cs(ProjectCS.OUTPUT, wkt, as_wkt=True)
project.coord_sys.set_vert_cs(ProjectCS.OUTPUT, None)  # use arbitrary vert CS

On the last line, we change the output CS using a WKT, wkt is a str variable containing the WKT. It could have been obtained for example using GDAL or at epsg.io.

Processing using pipelines

Pipelines provide similar functionality to the functions presented above, but allow for simpler and more flexible implementations.

Basic pipeline usage

The basic pipeline usage currently implies that a Project was created. The project on which we are operating is passed when defining a pix4dengine.enginewrapper.Pipeline. Optionally, one can define which tasks the pipeline should run.

from pix4dengine.enginewrapper import Pipeline

# Define a pipeline and choose which tasks to run
pipeline = Pipeline(project=project,
                    algos=["CALIB"])  # <-- Note: algos must be a tuple or a list

pipeline.run()

In the example, we assumed as before to have a Project instance, named project.

Configuring pipeline tasks

Tasks within a pipeline can be configured individually. This can be done by first accessing a task from the pipeline, and then configuring it using the set_config() method:

pipeline = Pipeline(project=project, algos=("CALIB", "DENSE"))

# Get calibration task and configure it
cameras_calibration = pipeline.get_task("CALIB")
cameras_calibration.set_config({
    AlgoOption.CameraCalibration.MATCH_GEOMETRICALLY_VERIFIED: False,
    AlgoOption.CameraCalibration.MATCH_MTP_MAX_IMAGE_PAIR: 3,
    AlgoOption.CameraCalibration.MATCH_RELATIVE_DISTANCE_IMAGES: 0.0,
    AlgoOption.CameraCalibration.MATCH_IMAGE_SIMILARITY_MAX_PAIRS: 1
})

# Get densification task and configure it
PCL_densification = pipeline.get_task("DENSE")
PCL_densification.set_config({
    AlgoOption.Densification.PCL_DENSITY: "Low"
})

pipeline.run()

Setting callbacks

Similarly, callbacks can be configured by first accessing a task, and then using the set_callbacks() method:

pipeline = Pipeline(project=project)

pipeline.get_task("CALIB").set_callbacks(
    Callbacks(on_start=lambda: print("Starting camera calibration"),
              on_success=lambda: print("Camera calibration successful!"))
)

pipeline.get_task("DENSE").set_callbacks(
    Callbacks(
        on_start=lambda: print("Starting point cloud densification"),
        on_success=lambda: print("Point cloud densification successful!")
    )
)

pipeline.get_task("ORTHO").set_callbacks(
    Callbacks(
        on_start=lambda: print("Starting orthomosaic generation"),
        on_success=lambda: print("Orthomosaic generation successful!")
    )
)

pipeline.run()

Adding a user-defined processing area

We demonstrate how to create new tasks by adding a user-defined processing area to a pipeline. We first define a pipeline without the "DEF_PROC_AREA" task. The "DEF_PROC_AREA" task would set a default processing area automatically (see pix4dengine.utils.processingarea for details on the default processing area definition).

pipeline = Pipeline(project=project,
                    algos=("CALIB", "DENSE", "ORTHO"))

Next, we define a Task which operates on the project to be processed, and sets a user-defined processing area.

from pix4dengine.task import Task
from pix4dengine.utils.project import PointXY, MinMaxRange


# Define a function to set a custom processing area
def user_proc_area_task(project, points, height_interval, callbacks=Callbacks()):
    """Callable that adds a processing area to a project."""
    from functools import partial

    work = partial(project.set_processing_area,
                   points=points,
                   height_interval=height_interval)
    return Task(name="USER_PROC_AREA", work=work, callbacks=callbacks)


# User-defined processing area task
proc_area_task = user_proc_area_task(
    project=project,  # <-- operates on the same project of the pipeline
    points=[
        PointXY(x=424921.35, y=5132471.58),
        PointXY(x=424954.51, y=5132519.64),
        PointXY(x=424985.60, y=5132488.43),
        PointXY(x=424961.98, y=5132453.91)
    ],
    height_interval=MinMaxRange(min=749.91, max=779.53),
    callbacks=Callbacks(
        on_start=lambda: print("Starting proc_area_task"),
        on_success=lambda: print("proc_area_task successful!")
    )
)

Finally, the new task is inserted into the pipeline. Since the processing area definition is only used after the camera positions are known, the "CALIB" task of the pipeline is added as a dependency: the processing area setting task must run after it. To ensure that "DENSE" and "ORTHO" steps take into account the processing area, a second dependency, before="DENSE", is added.

pipeline.add_task(proc_area_task, after="CALIB", before="DENSE")

pipeline.run()

Adding GCPs with image marks

In order to add GCPs with marks in a pipeline workflow, one can again define a custom task. We assume to have available a pipeline for processing a project, similarly to the previous example. We start by defining a function which adds a series of GCPs with their marks to a project:

# Data structure for holding a GCP with its marks
GcpInfo = namedtuple("GcpInfo", ["gcp", "marks"])


# Define a function to set GCPs with marks in a project
def user_gcp_task(project, gcp_infos, callbacks=Callbacks()):
    """Callable that adds a set of 3D GCPs with associated image marks."""
    def _add_gcps():
        for info in gcp_infos:
            project.add_3d_gcp_with_marks(info.gcp, info.marks)
    return Task(name="USER_ADD_GCPS", work=_add_gcps, callbacks=callbacks)

We define, in this case manually, the locations of GCPs and marks, and create a Task to add these locations to the project we are processing:

# Definitions of GCPs and marks
GCP_INFOS = [
    GcpInfo(
        gcp=GCP3D(label="gcp1", id=1, alt=51.444, lat=51.21884, lon=5.98994,
                  xy_accuracy=0.02, z_accuracy=0.02),
        marks=(Mark(photo="R0079100.JPG", x=2165.942, y=146.395, scale=1.34585),
               Mark(photo="R0079101.JPG", x=1726.303, y=1147.080, scale=1.34585),
               Mark(photo="R0079102.JPG", x=1289.577, y=2028.925, scale=1.24913))
    ),
    GcpInfo(
        gcp=GCP3D(label="gcp2", id=2, alt=45.689, lat=51.21881, lon=5.98991,
                  xy_accuracy=0.02, z_accuracy=0.02),
        marks=(Mark(photo="R0079100.JPG", x=2144.649, y=224.019, scale=1.16656),
               Mark(photo="R0079101.JPG", x=1730.395, y=1179.870, scale=1.16656),
               Mark(photo="R0079102.JPG", x=1320.590, y=2009.453, scale=1.11227))
    ),
    GcpInfo(
        gcp=GCP3D(label="gcp3", id=3, alt=271.076, lat=51.21914, lon=5.99044,
                  xy_accuracy=0.02, z_accuracy=0.02),
        marks=(Mark(photo="R0079100.JPG", x=2311.575, y=652.467, scale=1.11054),
               Mark(photo="R0079101.JPG", x=1903.187, y=1639.156, scale=1.87584),
               Mark(photo="R0079102.JPG", x=1482.204, y=2502.646, scale=1.70531))
    )
]

# User-defined GCP task
ADD_GCPS = user_gcp_task(
    project=project,
    gcp_infos=GCP_INFOS,
    callbacks=Callbacks(
        on_start=lambda: print("Starting USER_ADD_GCPS"),
        on_success=lambda: print("USER_ADD_GCPS SUCCESS!")
    )
)

Finally, the new task is inserted into the pipeline. However, the dependency of the new task is different from the one in the previous example. Since GCPs are needed during calibration, the new task is added before the "CALIB" task.

pipeline.add_task(ADD_GCPS, before="CALIB")

pipeline.run()