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