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>")

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

Alternatively, an access token previously generated with pix4dengine.gen_session_token() may be used:

from pix4dengine import login_with_token

login_with_token("<token string>")

Note that, to actually use these examples, one would need to provide meaningful values to the following variables: 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.

When creating a pipeline, one can define the tasks the pipeline should run. In order to run a pipeline, a Project must have been created as shown above and passed to pix4dengine.pipeline.Pipeline.run():

from pix4dengine.pipeline import Pipeline

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

pipeline.run(project)

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(algos=["CALIB", "DENSE"])

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

# Run the pipeline
pipeline.run(project)

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.algo import calib
from pix4dengine.pipeline.templates import TemplateOptions


# 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={calib.AlgoOption.MATCH_RELATIVE_DISTANCE_IMAGES: 4.0},
    )


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

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

# Run the pipeline
pipeline.run(project)

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.

Note that since the project parameter in pix4dengine.pipeline.pipeline_from_template() has been deprecated, it is recommended to call this function with None as the first argument and specify a project when running a pipeline.

from pix4dengine.pipeline import pipeline_from_template

pipeline = pipeline_from_template(None, RapidModel3D)

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

# Or multiple templates, on top of each other.
pipeline = pipeline_from_template(None, RapidModel3D, MyTemplate)
pipeline.run(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 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(algos=("CALIB", "DENSE"))

# Get calibration task and configure it
cameras_calibration = pipeline.get_task("CALIB")
cameras_calibration.set_config(
    {
        calib.AlgoOption.KEYPT_SEL_METHOD: "CustomNumberOfKeypoints",
        calib.AlgoOption.IMAGE_SCALE: "2",
        calib.AlgoOption.MATCH_TIME_NB_NEIGHBOURS: 2,
        calib.AlgoOption.MATCH_USE_TRIANGULATION: True,
        calib.AlgoOption.MATCH_RELATIVE_DISTANCE_IMAGES: 0.0,
        calib.AlgoOption.MATCH_IMAGE_SIMILARITY_MAX_PAIRS: 1,
        calib.AlgoOption.MATCH_MTP_MAX_IMAGE_PAIR: 3,
        calib.AlgoOption.MATCH_TIME_MULTI_CAMERA: False,
        calib.AlgoOption.KEYPT_NUMBER: 10000,
        calib.AlgoOption.MATCH_GEOMETRICALLY_VERIFIED: True,
        calib.AlgoOption.CALIBRATION_METHOD: "Alternative",
        calib.AlgoOption.CALIBRATION_INT_PARAM_OPT: "All",
        calib.AlgoOption.CALIBRATION_EXT_PARAM_OPT: "All",
        calib.AlgoOption.REMATCH: True,
        calib.AlgoOption.ORTHOMOSAIC_IN_REPORT: True,
        calib.ExportOption.UNDISTORTED_IMAGES: False,
    }
)

# Get densification task and configure it
PCL_densification = pipeline.get_task("DENSE")
PCL_densification.update_config({dense.AlgoOption.PCL_DENSITY: "Low", mesh.ExportOption.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(algos=("CALIB", "DENSE", "ORTHO"))

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

# Extend the indices
indices.extend([Indices.GRAYSCALE.value])
ortho_task.update_config({ortho.IndexAlgoOption.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_task.update_config({ortho.IndexAlgoOption.INDICES: indices})

Warning

Band names typically originating from multispectral cameras may have mixed case text and could include conflicting characters (' ', '.', '+', '-', '*', '/'). When defining formulas for calculating custom indices using band names, it is imperative to modify the original band name strings by replacing any conflicting characters with “_” and using lower case only (e.g. “Red Edge” -> “red_edge”, “RED-W” -> “red_w” and so on).

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

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

pipeline = Pipeline()
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(project)

Adding a user-defined processing area

We demonstrate how to create a pipeline with a user-defined processing area.

We first create a callable that sets a user-defined processing area for a project:

from functools import partial
from pix4dengine.algo.callbacks import callbacks
from pix4dengine.utils.project import PointXY, MinMaxRange


# Define a function to set a custom processing area
@callbacks(
    on_start=lambda: print("Starting to set processing area"),
    on_success=lambda: print("Processing area successfully set!"),
)
def _set_proc_area(project, points, height_interval):
    project.set_processing_area(points=points, height_interval=height_interval)


# Create a callable object and set its name property
proc_area_callable = partial(
    _set_proc_area,
    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),
)
proc_area_callable.name = "USER_PROC_AREA"

Next, we define a pipeline with the new processing area callable in the list of its algorithms. It must be placed after the "CALIB" task, since the processing area definition is only used after the camera positions are known, and before the "DENSE" task to ensure that the "DENSE" and "ORTHO" steps take into account the processing area:

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

apply_template(pipeline, RapidMaps3D)
pipeline.run(project)

Adding GCPs and check-points with image marks

Preamble: set up a callable to add GCPs and check-points

We start by defining a function which adds a series of GCPs with their marks to a project:

from functools import partial
from pix4dengine.algo.callbacks import callbacks


# Define a function to set GCPs with marks in a project
@callbacks(
    on_start=lambda: print("Starting USER_ADD_GCPS"),
    on_success=lambda: print("USER_ADD_GCPS SUCCESS!"),
)
def _add_gcps(project, gcp_infos):
    for info in gcp_infos:
        project.add_3d_gcp_with_marks(info.gcp, info.marks)


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

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


# Definitions of GCPs (3D-GCPs and check-points) and marks
GCP_INFOS = [
    GcpInfo(
        gcp=GCP3D(
            label="gcp1",
            id=1,
            gcp_type="3DGCP",
            alt=51.444,
            lat=51.21884,
            lon=5.98994,
            xy_accuracy=0.02,
            z_accuracy=0.045,
        ),
        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,
            gcp_type="checkPoint",
            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,
            gcp_type="checkPoint",
            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),
        ),
    ),
]

# Create a callable that adds a set of 3D GCPs with associated image marks
add_gcps_callable = partial(_add_gcps, gcp_infos=GCP_INFOS)
add_gcps_callable.name = "USER_ADD_GCPS"

Add GCPs before initial calibration

The new callable must be placed before the "CALIB" task in the pipeline if the GCPs are needed during calibration:

pipeline = Pipeline(algos=(add_gcps_callable, "CALIB"))

apply_template(pipeline, RapidMaps3D)
pipeline.run(project)

Add GCPs and re-optimize after initial calibration

It is also possible to add GCPs after initial calibration, and then run a reoptimization instead of a full calibration (for more details on reoptimization, see Re-optimization). In that case, set the pipeline to run a sequence of "CALIB", add_gcps_callable defined above, and "REOPT":

# Define a pipeline with "CALIB", GCPs and "REOPT"
p = Pipeline(algos=("CALIB", add_gcps_callable, "REOPT"))
p.run(project)

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, configure two exports, and run the pipeline:

from pix4dengine.algo import dense, ortho
from pix4dengine.exports import get_output
from pix4dengine.pipeline.templates import RapidMaps3D
from pix4dengine.pipeline import apply_template

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

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

pipeline.run(project)

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

file_paths = get_output(project, ortho.OrthoExportOption.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 from algo 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 ReportFormat:

from pix4dengine.exports import get_report, ReportFormat

path_to_html_report = get_output(project, ReportFormat.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

Creating, configuring and running standard photogrammetry algorithms

In order to run a standard algorithm, that is calibration, point cloud densification, or orthomosaic, it does not have to be a part of a pipeline. Such an algorithm can be created, configured, and run independently for a given project:

from pix4dengine.algo import calib

# Create a configuration for the calibration algorithm
config = {
    calib.AlgoOption.KEYPT_SEL_METHOD: "Automatic",
    calib.AlgoOption.IMAGE_SCALE: "0.25",
    calib.AlgoOption.MATCH_TIME_NB_NEIGHBOURS: 2,
    calib.AlgoOption.MATCH_USE_TRIANGULATION: True,
    calib.AlgoOption.MATCH_RELATIVE_DISTANCE_IMAGES: 0.0,
    calib.AlgoOption.MATCH_IMAGE_SIMILARITY_MAX_PAIRS: 1,
    calib.AlgoOption.MATCH_MTP_MAX_IMAGE_PAIR: 5,
    calib.AlgoOption.MATCH_TIME_MULTI_CAMERA: False,
    calib.AlgoOption.KEYPT_NUMBER: 10000,
    calib.AlgoOption.MATCH_GEOMETRICALLY_VERIFIED: False,
    calib.AlgoOption.CALIBRATION_METHOD: "Standard",
    calib.AlgoOption.CALIBRATION_INT_PARAM_OPT: "All",
    calib.AlgoOption.CALIBRATION_EXT_PARAM_OPT: "All",
    calib.AlgoOption.REMATCH: True,
    calib.AlgoOption.ORTHOMOSAIC_IN_REPORT: True,
    calib.ExportOption.UNDISTORTED_IMAGES: False,
}

# Create a calibration algorithm with the given configuration
calib_algo = calib.make_algo(config=config)

# Run the calibration algorithm
calib_algo(project)

A standard algorithm can be given callbacks to execute on its start, its successful completion, or its failure:

from pix4dengine.algo.callbacks import callbacks

# Run the calibration algorithm with callbacks
calib_algo = callbacks(
    on_start=lambda: print("Calibration started."),
    on_success=lambda: print("Calibration succeeded."),
)(calib_algo)

calib_algo(project)