Examples

Setup

In all the following examples, it is assumed that the user’s license is authenticated, either with an e-mail, password and license_key combination using pix4dengine.login_seat():

from pix4dengine import login_seat

login_seat("<youremail>", "<yourpassword>", "<licensekey>")

or with an access token previously generated with pix4dengine.get_auth_token() and a license key:

from pix4dengine import login_with_token

login_with_token("<token string>", "<licensekey>")

Note that the license key is optional but recommended. If it is not specified and there is more than one Engine SDK enabled license, the login procedure will fail.

Furthermore, to actually use these examples, one would need to provide meaningful values to the variable used, namely project_name, image_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 image_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", image_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_directory)

Basic pipeline usage

Pipelines are the objects used to define the processing steps to execute and their configuration.

The basic pipeline usage currently requires that a Project have been created as shown above. The project on which we are operating is passed when defining a pix4dengine.pipeline.Pipeline. Optionally, one can define which tasks the pipeline should run.

from pix4dengine.pipeline 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()

Pipeline tasks have default configurations, which can be changed by either applying a configuration template to the pipeline or setting each task’s configuration options.

Applying a pipeline template

Pipeline templates can be used to apply pre-defined configurations on one or more Pipeline algos. Templates are defined and documented in pix4dengine.pipeline.templates. One or more templates can be applied, to combine different settings, using apply_template():

from pix4dengine.pipeline import Pipeline, apply_template
from pix4dengine.pipeline.templates import (
    RapidModel3D,  # Quick generation of a 3D model
    MeshLowRes,  # Reduce the mesh resolution
)

# Define a pipeline and choose which tasks to run
pipeline = Pipeline(project=project, algos=["CALIB", "DENSE"])

# Apply the templates
apply_template(pipeline, RapidModel3D, MeshLowRes)

# Run the pipeline
pipeline.run()

Note that the templates only define the algorithm configuration, they do not define which algorithms are to be run.

Defining a custom pipeline template

Custom templates can be useful for storing specific processing options. This enables, e.g., reusing some options that are useful for a specific kind of project or camera, or storing the configuration of a user-defined pipeline algo. The template is a Python enum.Enum that associates a pipeline algo name to a dictionary of options. The dictionary of options can be a plain Python dict. Alternatively, pix4dengine.pipeline.templates.TemplateOptions enables defining a set of options on top of some base options, modifying some of them or adding new ones. In the example, a custom template is defined to configure the “CALIB” algo. The template applies all the “CALIB” options of the pre-defined pix4dengine.pipeline.templates.RapidModel3D template, but modifies the value set for one of the options:

from enum import Enum
from pix4dengine.pipeline.templates import TemplateOptions
from pix4dengine.options import AlgoOption


# Define the custom template by subclassing Enum
class MyTemplate(Enum):
    """This template only applies to CALIB algo."""

    CALIB = TemplateOptions(
        base=RapidModel3D.CALIB,
        # Modify the matching distance wrt RapidModel3D.CALIB
        options={AlgoOption.CameraCalibration.MATCH_RELATIVE_DISTANCE_IMAGES: 4.0},
    )


# Define a pipeline and choose which tasks to run
pipeline = Pipeline(project=project, algos=["CALIB"])

# Apply the the custom template
apply_template(pipeline, MyTemplate)

# Run the pipeline
pipeline.run()

Creating pipeline from template

A pipeline can be created directly from a template. In that case tasks that are configured in the template will be added automatically to the pipeline. This allows to use a template to define which algorithms should be run by a pipeline, as well as the configuration of each algorithm.

from pix4dengine.pipeline import pipeline_from_template

pipeline = pipeline_from_template(project, RapidModel3D)

# You can use your own templates as well.
pipeline = pipeline_from_template(project, MyTemplate)

# Or multiple templates, on top of each other.
pipeline = pipeline_from_template(project, RapidModel3D, MyTemplate)

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 either the set_config() method or the update_config() method. set_config() sets a new configuration discarding the previously existing one. Note that overwriting the default configuration or a configuration applied through a predefined template and not specifying the required options will result in those options taking unspecified values. update_config() updates the configuration by adding new options or replacing the existing ones.

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.KEYPT_SEL_METHOD: "CustomNumberOfKeypoints",
        AlgoOption.CameraCalibration.IMAGE_SCALE: "2",
        AlgoOption.CameraCalibration.MATCH_TIME_NB_NEIGHBOURS: 2,
        AlgoOption.CameraCalibration.MATCH_USE_TRIANGULATION: True,
        AlgoOption.CameraCalibration.MATCH_RELATIVE_DISTANCE_IMAGES: 0.0,
        AlgoOption.CameraCalibration.MATCH_IMAGE_SIMILARITY_MAX_PAIRS: 1,
        AlgoOption.CameraCalibration.MATCH_MTP_MAX_IMAGE_PAIR: 3,
        AlgoOption.CameraCalibration.MATCH_TIME_MULTI_CAMERA: False,
        AlgoOption.CameraCalibration.KEYPT_NUMBER: 10000,
        AlgoOption.CameraCalibration.MATCH_GEOMETRICALLY_VERIFIED: True,
        AlgoOption.CameraCalibration.CALIBRATION_METHOD: "Alternative",
        AlgoOption.CameraCalibration.CALIBRATION_INT_PARAM_OPT: "All",
        AlgoOption.CameraCalibration.CALIBRATION_EXT_PARAM_OPT: "All",
        AlgoOption.CameraCalibration.REMATCH_STRATEGY: "Custom",
        AlgoOption.CameraCalibration.REMATCH: True,
        AlgoOption.CameraCalibration.ORTHOMOSAIC_IN_REPORT: True,
        ExportOption.CameraCalibration.UNDISTORTED_IMAGES: False,
    }
)

# Get densification task and configure it
PCL_densification = pipeline.get_task("DENSE")
PCL_densification.update_config(
    {AlgoOption.Densification.PCL_DENSITY: "Low", ExportOption.Mesh.PLY: True}
)

Adding indices

Index maps are generated in the "ORTHO" step of a pipeline. Indices are a part of this step’s configuration. Therefore, adding indices means configuring the "ORTHO" step. This is done by setting the value of the AlgoOption.Index.INDICES configuration parameter to a list of pix4dengine.constants.index.Index objects. The SDK predefines several commonly used indices in the pix4dengine.constants.index.Indices enumeration. Custom indices can be created as instances of the pix4dengine.constants.index.Index class.

from pix4dengine.constants.index import Index, Indices

# Define a pipeline with the default configuration
pipeline = Pipeline(project=project, algos=("CALIB", "DENSE", "ORTHO"))

# Get the ortho task and add indices to its configuration
ortho = pipeline.get_task("ORTHO")
indices = [Indices.RED.value, Indices.GREEN.value, Indices.BLUE.value]
ortho.update_config({AlgoOption.Index.INDICES: indices})

# Extend the indices
indices.extend([Indices.GRAYSCALE.value])
ortho.update_config({AlgoOption.Index.INDICES: indices})

# Replace the indices with a new list containing a custom index
indices = [
    Indices.RED.value,
    Indices.GREEN.value,
    Indices.BLUE.value,
    Index(name="pink", formula="red + 0.6 * green + 0.8 * blue", enabled=True),
]
ortho.update_config({AlgoOption.Index.INDICES: indices})

Using externally supplied geotags to georeference a project

If your project images do not contain GPS data, it is possible to use external geotags to geolocate the images when creating the project. This is achieved using a sequence of Geotag. This data structure can be also used to provide information about camera orientation angles.

from pix4dengine.geotag import Geotag

geotags = [
    Geotag(
        image="IMG_4082.JPG",
        longitude=8.1,
        latitude=46.4,
        altitude=814.0,
        hor_accuracy=5.1,
        ver_accuracy=10.0,
        omega=1.5,
        phi=4.2,
        kappa=90.7,
    ),
    Geotag(
        image="IMG_4083.JPG",
        longitude=8.3,
        latitude=46.6,
        altitude=814.7,
        hor_accuracy=5.2,
        ver_accuracy=10.1,
        omega=1.6,
        phi=4.3,
        kappa=90.8,
    ),
    Geotag(
        image="IMG_4087.JPG",
        longitude=8.5,
        latitude=46.8,
        altitude=814.9,
        hor_accuracy=5.3,
        ver_accuracy=10.2,
        omega=1.7,
        phi=4.4,
        kappa=90.9,
    ),
]

project = p4d.create_project(
    "test", image_directory, work_dir=project_directory, external_geoloc=geotags
)

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.geotag import ExternalGeolocation, ExternalGeolocationFormat

project = p4d.create_project(
    "test",
    image_directory,
    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.

Setting callbacks

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

pipeline = Pipeline(project=project)
apply_template(pipeline, RapidMaps3D)  # fast processing

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

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

pipeline.get_task("ORTHO").set_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, on_start=lambda: None, on_success=lambda: None
):
    """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, on_start=on_start, on_success=on_success)


# 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=424_921.35, y=5_132_471.58),
        PointXY(x=424_954.51, y=5_132_519.64),
        PointXY(x=424_985.60, y=5_132_488.43),
        PointXY(x=424_961.98, y=5_132_453.91),
    ],
    height_interval=MinMaxRange(min=749.91, max=779.53),
    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, on_start=lambda: None, on_success=lambda: None):
    """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, on_start=on_start, on_success=on_success)


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=os.path.join(image_directory, "R0079100.JPG"), x=2165.942, y=146.395),
            Mark(photo=os.path.join(image_directory, "R0079101.JPG"), x=1726.303, y=1147.080),
            Mark(photo=os.path.join(image_directory, "R0079102.JPG"), x=1289.577, y=2028.925),
        ),
    ),
    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=os.path.join(image_directory, "R0079100.JPG"), x=2144.649, y=224.019),
            Mark(photo=os.path.join(image_directory, "R0079101.JPG"), x=1730.395, y=1179.870),
            Mark(photo=os.path.join(image_directory, "R0079102.JPG"), x=1320.590, y=2009.453),
        ),
    ),
    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=os.path.join(image_directory, "R0079100.JPG"), x=2311.575, y=652.467),
            Mark(photo=os.path.join(image_directory, "R0079101.JPG"), x=1903.187, y=1639.156),
            Mark(photo=os.path.join(image_directory, "R0079102.JPG"), x=1482.204, y=2502.646),
        ),
    ),
]

# User-defined GCP task
ADD_GCPS = user_gcp_task(
    project=project,
    gcp_infos=GCP_INFOS,
    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()

Finding the output files

The path to the output files produced by running a processing pipeline can be obtained from a project using the function pix4dengine.exports.get_output().

To demonstrate this functionality, we first create a processing pipeline, activate only two exports in a project, and run it:

from pix4dengine.exports import get_output
from pix4dengine.options import ExportOption
from pix4dengine.pipeline.templates import RapidMaps3D
from pix4dengine.pipeline import apply_template

# instantiate a pipeline
pipeline = Pipeline(project, algos=("CALIB", "DENSE", "ORTHO"))
apply_template(pipeline, RapidMaps3D)  # fast processing

# Define specific exports to produce
pipeline.get_task("DENSE").update_config({ExportOption.Densification.PCL_LAS: True})
# Note: Orthomosaic requires DSM as input. The relevant options
# ExportOption.Ortho.DSM_TIFF and ExportOption.Ortho.DSM_TIFF_MERGED
# are set in the template.
pipeline.get_task("ORTHO").update_config({ExportOption.Ortho.MOSAIC_TIFF: True})

pipeline.run()

The output locations of one of the exports can be obtained as follows:

file_paths = get_output(project, ExportOption.Ortho.MOSAIC_TIFF)

file_paths is a list of paths to the export files, e.g.:

file_paths = ['/path/to/project/3_dsm_ortho/2_mosaic/tiles/proj_name_transparent_mosaic_group1_1.tif']

Note that we used Unix-style paths in this example, but Windows-style ones would be returned on that platform.

This function can raise UnsetExportException if requesting the output path for an ExportOption that is set to False.

If, for any reason, expected output files are not found on disk (e.g., before processing starts, because of a processing error) pix4dengine.exports.get_output() raises OutputFilesNotFound.

Accessing the processing quality reports

An HTML 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_html_report = get_output(project, StandardExport.Report.HTML)

If a report in PDF format is required, we recommend using wkhtmltopdf to transform the HTML report. wkhtmltopdf is available on Ubuntu 18.04 in the official software repository.

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)

print(quality_report.calibration_quality_status())
print(quality_report.absolute_geolocation_rms())
print(quality_report.image_dataset_info())

print(
    "Calibration quality status = %s." % quality_report.calibration_quality_status().dataset.value
)

print(
    "Number of 3D GCPs used to georeference the project = %d." % quality_report.number_of_3d_gcps()
)
print("Camera optimization relative difference = %d." % quality_report.camera_opt_rel_diff())

The code above will print examples of data that can be accessed in an instance of the Report class:

Calibration quality status:
images              = success
dataset             = success
camera_optimization = warning
matching            = success
georeferencing      = warning
GeolocationRMS(x=0.2068001209, y=0.403338568, z=0.24444913)
Image dataset information:
total                 = 3
enabled               = 3
calibrated            = 3
calibrated_enabled    = 3
calibrated_percentage = 100.000000
disabled              = 0
Calibration quality status = success.
Number of 3D GCPs used to georeference the project = 0.
Camera optimization relative difference = 11.

Accessing or changing the project coordinate systems

A 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 CS names 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_name(ProjectCS.OUTPUT))
print("GCPs CS = %s" % project.coord_sys.get_cs_name(ProjectCS.GCPS))
print("GCPs CS as WKT\n\n%s" % fill(project.coord_sys.get_cs_wkt(ProjectCS.GCPS), 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_from_name(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_from_name(ProjectCS.GCPS, "Tokyo / Japan Plane Rectangular CS XVIII")
project.coord_sys.set_vert_cs(ProjectCS.GCPS, 3.0)  # 3. meters above WGS 84

# Modify the output (project) CS using a WKT
project.coord_sys.set_cs_from_wkt(ProjectCS.OUTPUT, wkt)
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.

Coordinate systems can use three different units to measure distances: the meter, the foot (0.3048 m) and the US survey foot (1200/3937 m). The generic UTM coordinate systems are defined in meters. The projected coordinate systems can be defined in other units. For example, the State Plane CS of Montana is available in two units: “NAD83(2011) / Montana” is in metre while “NAD83(2011) / Montana (ft)” is in feet.

We provide functions to identify the length units of a coordinate system:

from pix4dengine.coordsys import unit_from_cs_name
from pix4dengine.constants.coordsys import ProjectCS, LengthUnit

# These three statements are true, they will not cause assertion errors
assert project.coord_sys.get_length_unit(ProjectCS.OUTPUT) == LengthUnit.METER
assert unit_from_cs_name("NAD83(2011) / Montana") == LengthUnit.METER
assert unit_from_cs_name("NAD83(2011) / Montana (ft)") == LengthUnit.FOOT