Agisoft Metashape

Agisoft Metashape => Python and Java API => Topic started by: farmflightadam on May 20, 2021, 11:45:52 AM

Title: Building and Exporting Orthomosaic using Network Processing
Post by: farmflightadam on May 20, 2021, 11:45:52 AM
Hello and please forgive me as I am new to Python and Photogrammetry. I am a Systems/DevOps Engineer and a PHP/Javascript developer. My background is building production environments for everything from websites to enterprise applications as well as mobile applications. For the last few months, I have been building a tile processing pipeline that we feed imagery into that is stitched using Pix4d. The tile processing pipeline takes the images fed into it and creates the map tiles so that we can load the imagery into our web application. Moving forward, we want to replace Pix4d with an in-house built and managed stitching pipeline. We have looked at other software and now we're testing Metashape.

Currently, I have three AWS EC2 instances running (a master controller and 2 processing nodes) that all share and AWS EFS mount for shared storage. I have some imagery from a Sony camera on the shared filesystem and I have a Python script that successfully creates a Metashape project (though I do know if it is complete and accurate). You can see that code here: https://pb.recoilnetworks.com/?118554ebbf7cd5c9#5hfzwilEuY1Tt0hi35moHKWhA6kkOApNPlDvi1XmLRQ=

Now, I am trying to write another script that simply exports a single bigtiff Orthomosaic. Here's the current code:

Code: [Select]
import Metashape
import sys


def do_create_export(project, dir_root, dir_save, ipaddr):
    tasks = list()

    doc = Metashape.Document()
    doc.open(project, read_only=False, ignore_lock=True)

    chunk = doc.chunk

    # Match Photos
    task = Metashape.Tasks.MatchPhotos()
    task.keypoint_limit = 40000
    task.tiepoint_limit = 4000

    net_task = Metashape.NetworkTask()
    net_task.name = task.name
    net_task.params = task.encode()
    net_task.frames.append((chunk.key, 0))
    tasks.append(net_task)

    # Align Cameras
    task = Metashape.Tasks.AlignCameras()
    task.adaptive_fitting = False

    net_task = Metashape.NetworkTask()
    net_task.name = task.name
    net_task.params = task.encode()
    net_task.frames.append((chunk.key, 0))
    tasks.append(net_task)

    # Depth Maps
    task = Metashape.Tasks.BuildDepthMaps()
    task.filter_mode = Metashape.FilterMode.MildFiltering

    net_task = Metashape.NetworkTask()
    net_task.name = task.name
    net_task.params = task.encode()
    net_task.frames.append((chunk.key, 0))
    tasks.append(net_task)

    # Dense Cloud
    task = Metashape.Tasks.BuildDenseCloud()
    task.point_colors = True

    net_task = Metashape.NetworkTask()
    net_task.name = task.name
    net_task.params = task.encode()
    net_task.frames.append((chunk.key, 0))
    tasks.append(net_task)

    # DEM
    task = Metashape.Tasks.BuildDem()
    task.source_data = Metashape.DataSource.DenseCloudData
    task.interpolation = Metashape.Interpolation.Extrapolated
    task.projection = chunk.crs

    net_task = Metashape.NetworkTask()
    net_task.name = task.name
    net_task.params = task.encode()
    net_task.frames.append((chunk.key, 0))
    tasks.append(net_task)

    # Build Orthomosaic
    task = Metashape.Tasks.BuildOrthomosaic()
    task.resolution = 0.05
    task.projection = chunk.crs
    task.blending_mode = Metashape.BlendingMode.MosaicBlending

    net_task = Metashape.NetworkTask()
    net_task.name = task.name
    net_task.params = task.encode()
    net_task.frames.append((chunk.key, 0))
    tasks.append(net_task)

    # Export Orthomosaic
    compression = Metashape.ImageCompression()
    compression.tiff_compression = Metashape.ImageCompression.TiffCompressionJPEG
    compression.jpeg_quality = 100
    compression.tiff_big = True

    task = Metashape.Tasks.ExportRaster()
    task.path = dir_save + 'orthomosaic.tif'
    task.image_compression = compression
    task.image_format = Metashape.ImageFormatTIFF
    task.projection = chunk.crs
    task.save_world = False
    task.save_alpha = False
    task.white_background = True
    task.save_kml = False
    task.source_data = Metashape.ElevationData

    net_task = Metashape.NetworkTask()
    net_task.name = task.name
    net_task.params = task.encode()
    net_task.frames.append((chunk.key, 0))
    tasks.append(net_task)

    client = Metashape.NetworkClient()
    client.connect(ipaddr)
    batch_id = client.createBatch(project[len(dir_root):], tasks)
    client.resumeBatch(batch_id)


if len(sys.argv) > 2:
    psx = sys.argv[1]
    rdir = sys.argv[2]
    sdir = sys.argv[3]
    sip = sys.argv[4]
    do_create_export(psx, rdir, sdir, sip)
else:
    print("-- missing arguments - provide project file path, root path, save path, and server ip --")

And here's the output:

Code: [Select]
2021-05-20 08:28:08 [172.16.2.101:50920] finished #1 MatchPhotos
2021-05-20 08:28:10 [172.16.2.101:50920] finished #1 MatchPhotos.initialize (1/1)
2021-05-20 08:28:11 [172.16.2.101:50920] finished #1 MatchPhotos.cleanup (1/1)
2021-05-20 08:28:12 [172.16.2.101:50920] finished #1 AlignCameras
2021-05-20 08:28:13 [172.16.2.101:50920] finished #1 AlignCameras.initialize (1/1)
2021-05-20 08:28:14 [172.16.2.101:50920] finished #1 AlignCameras.update (1/1)
2021-05-20 08:28:15 [172.16.2.101:50920] finished #1 AlignCameras.finalize (1/1)
2021-05-20 08:28:16 [172.16.2.101:50920] finished #1 AlignCameras.cleanup (1/1)
2021-05-20 08:28:17 [172.16.2.101:50920] finished #1 BuildDepthMaps
2021-05-20 08:28:18 [172.16.2.101:50920] finished #1 BuildDepthMaps.initialize (1/1)
2021-05-20 08:28:20 [172.16.2.101:50920] finished #1 BuildDepthMaps.finalize (1/1)
2021-05-20 08:28:21 [172.16.2.101:50920] finished #1 BuildDepthMaps.cleanup (1/1)
2021-05-20 08:28:22 [172.16.2.101:50920] finished #1 BuildDenseCloud
2021-05-20 08:28:23 [172.16.2.101:50920] finished #1 BuildDenseCloud.initialize (1/1): Zero resolution
2021-05-20 08:28:24 [172.16.2.101:50920] finished #1 BuildDenseCloud.cleanup (1/1)
2021-05-20 08:28:25 [172.16.2.101:50920] finished #1 BuildDem
2021-05-20 08:28:26 [172.16.2.101:50920] finished #1 BuildDem.initialize (1/1): Null dense cloud
2021-05-20 08:28:27 [172.16.2.101:50920] finished #1 BuildDem.cleanup (1/1)
2021-05-20 08:28:28 [172.16.2.101:50920] finished #1 BuildOrthomosaic
2021-05-20 08:28:30 [172.16.2.101:50920] finished #1 BuildOrthomosaic.initialize (1/1): Null model
2021-05-20 08:28:31 [172.16.2.101:50920] finished #1 BuildOrthomosaic.cleanup (1/1)
2021-05-20 08:28:32 [172.16.2.101:50920] finished #1 ExportRaster: Null elevation

I am not sure why the DenseCloud says there's a "Zero resolution" and not sure about the other issues the follow. Any help is greatly appreciated!

Also, if you have experience with Metashape and the Python SDK, please reply. We will be looking for a Python developer soon to help us build this pipeline, if we can get this to work.
Title: Re: Building and Exporting Orthomosaic using Network Processing
Post by: Alexey Pasumansky on May 20, 2021, 04:34:43 PM
Hello farmflightadam,

Please try to modify the line for Align Cameras task to the following:
Code: [Select]
net_task.chunks.append(chunk.key)
If it still doesn't help, would it be possible to you to open the partially processed project in Metashape GUI and check, if there are depth maps in the project and that they are not empty (represented by false colors).
Title: Re: Building and Exporting Orthomosaic using Network Processing
Post by: farmflightadam on May 20, 2021, 10:16:58 PM
Okay, change has been made however, I am still seeing the same out. Here's the output from the network monitor:

Code: [Select]
2021-05-20 19:21:41 [172.16.2.101:32992] finished #0 MatchPhotos
2021-05-20 19:21:42 [172.16.2.101:32992] finished #0 MatchPhotos.initialize (1/1)
2021-05-20 19:21:43 [172.16.2.101:32992] finished #0 MatchPhotos.cleanup (1/1)
2021-05-20 19:21:44 [172.16.2.101:32992] finished #0 AlignCameras
2021-05-20 19:21:46 [172.16.2.101:32992] finished #0 AlignCameras.initialize (1/1)
2021-05-20 19:21:47 [172.16.2.101:32992] finished #0 AlignCameras.update (1/1)
2021-05-20 19:21:48 [172.16.2.101:32992] finished #0 AlignCameras.finalize (1/1)
2021-05-20 19:21:49 [172.16.2.101:32992] finished #0 AlignCameras.cleanup (1/1)
2021-05-20 19:21:50 [172.16.2.101:32992] finished #0 BuildDepthMaps
2021-05-20 19:21:51 [172.16.2.101:32992] finished #0 BuildDepthMaps.initialize (1/1)
2021-05-20 19:21:52 [172.16.2.101:32992] finished #0 BuildDepthMaps.finalize (1/1)
2021-05-20 19:21:53 [172.16.2.101:32992] finished #0 BuildDepthMaps.cleanup (1/1)
2021-05-20 19:21:54 [172.16.2.101:32992] finished #0 BuildDenseCloud
2021-05-20 19:21:55 [172.16.2.101:32992] finished #0 BuildDenseCloud.initialize (1/1): Zero resolution
2021-05-20 19:21:57 [172.16.2.101:32992] finished #0 BuildDenseCloud.cleanup (1/1)
2021-05-20 19:21:58 [172.16.2.101:32992] finished #0 BuildDem
2021-05-20 19:21:59 [172.16.2.101:32992] finished #0 BuildDem.initialize (1/1): Null dense cloud
2021-05-20 19:22:00 [172.16.2.101:32992] finished #0 BuildDem.cleanup (1/1)
2021-05-20 19:22:01 [172.16.2.101:32992] finished #0 BuildOrthomosaic
2021-05-20 19:22:02 [172.16.2.101:32992] finished #0 BuildOrthomosaic.initialize (1/1): Null model
2021-05-20 19:22:03 [172.16.2.101:32992] finished #0 BuildOrthomosaic.cleanup (1/1)
2021-05-20 19:22:04 [172.16.2.101:32992] finished #0 ExportRaster: Null elevation

And here's the output from the CLI:

Code: [Select]
2021-05-20 19:21:41 MatchPhotos: accuracy = High, preselection = generic, reference, keypoint limit = 40000, tiepoint limit = 4000, apply masks = 0, filter tie points = 1, filter stationary points = 1
2021-05-20 19:21:41 Matching photos...
2021-05-20 19:21:41 processing finished in 0.272402 sec
2021-05-20 19:21:42 MatchPhotos.initialize (1/1): accuracy = High, preselection = generic, reference, keypoint limit = 40000, tiepoint limit = 4000, apply masks = 0, filter tie points = 1, filter stationary points = 1
2021-05-20 19:21:42 processing finished in 0.221081 sec
2021-05-20 19:21:43 MatchPhotos.cleanup (1/1): accuracy = High, preselection = generic, reference, keypoint limit = 40000, tiepoint limit = 4000, apply masks = 0, filter tie points = 1, filter stationary points = 1
2021-05-20 19:21:43 processing finished in 0.221829 sec
2021-05-20 19:21:44 AlignCameras: adaptive fitting = 0
2021-05-20 19:21:44 Estimating camera locations...
2021-05-20 19:21:44 processing finished in 0.215476 sec
2021-05-20 19:21:45 AlignCameras.initialize (1/1): adaptive fitting = 0
2021-05-20 19:21:45 processing matches... done in 0.003786 sec
2021-05-20 19:21:45 selecting camera groups... done in 0.000938 sec
2021-05-20 19:21:45 scheduled 0 alignment groups
2021-05-20 19:21:45 saved camera partition in 0.013206 sec
2021-05-20 19:21:46 processing finished in 0.216372 sec
2021-05-20 19:21:46 AlignCameras.update (1/1): adaptive fitting = 0
2021-05-20 19:21:46 loaded camera partition in 0.002102 sec
2021-05-20 19:21:47 processing finished in 0.2136 sec
2021-05-20 19:21:47 AlignCameras.finalize (1/1): adaptive fitting = 0
2021-05-20 19:21:48 loaded camera partition in 0.002523 sec
2021-05-20 19:21:48 processing finished in 0.21477 sec
2021-05-20 19:21:49 AlignCameras.cleanup (1/1): adaptive fitting = 0
2021-05-20 19:21:49 processing finished in 0.216865 sec
2021-05-20 19:21:50 BuildDepthMaps: quality = Medium, depth filtering = Mild, PM version
2021-05-20 19:21:50 Generating depth maps...
2021-05-20 19:21:50 processing finished in 0.217966 sec
2021-05-20 19:21:51 BuildDepthMaps.initialize (1/1): quality = Medium, depth filtering = Mild, PM version
2021-05-20 19:21:51 Preparing 0 cameras info...
2021-05-20 19:21:51 cameras data loaded in 0.000764 s
2021-05-20 19:21:51 cameras graph built in 0.000211 s
2021-05-20 19:21:51 No cameras left
2021-05-20 19:21:51 cameras info prepared in 0.002181 s
2021-05-20 19:21:51 saved cameras info in 0.015778
2021-05-20 19:21:51 Partitioning 0 cameras...
2021-05-20 19:21:51 number of mini clusters: 0
2021-05-20 19:21:51 0 groups
2021-05-20 19:21:51 cameras partitioned in 0.000213 s
2021-05-20 19:21:51 saved depth map partition in 0.01144 sec
2021-05-20 19:21:51 processing finished in 0.219158 sec
2021-05-20 19:21:52 BuildDepthMaps.finalize (1/1): quality = Medium, depth filtering = Mild, PM version
2021-05-20 19:21:52 loaded depth map partition in 0.002566 sec
2021-05-20 19:21:52 processing finished in 0.217146 sec
2021-05-20 19:21:53 BuildDepthMaps.cleanup (1/1): quality = Medium, depth filtering = Mild, PM version
2021-05-20 19:21:53 processing finished in 0.21954 sec
2021-05-20 19:21:54 BuildDenseCloud: point colors = 1
2021-05-20 19:21:54 Generating dense point cloud...
2021-05-20 19:21:54 processing finished in 0.219276 sec
2021-05-20 19:21:55 BuildDenseCloud.initialize (1/1): point colors = 1
2021-05-20 19:21:55 Generating dense point cloud...
2021-05-20 19:21:55 initializing...
2021-05-20 19:21:55 selected 0 cameras in 7.4e-05 sec
2021-05-20 19:21:55 Error: Zero resolution
2021-05-20 19:21:55 processing finished in 0.218614 sec
2021-05-20 19:21:56 BuildDenseCloud.cleanup (1/1): point colors = 1
2021-05-20 19:21:57 processing finished in 0.220858 sec
2021-05-20 19:21:57 BuildDem: source data = Dense cloud, interpolation = Extrapolated, resolution = 0
2021-05-20 19:21:57 Generating DEM...
2021-05-20 19:21:58 processing finished in 0.21771 sec
2021-05-20 19:21:59 BuildDem.initialize (1/1): source data = Dense cloud, interpolation = Extrapolated, resolution = 0
2021-05-20 19:21:59 initializing...
2021-05-20 19:21:59 Error: Null dense cloud
2021-05-20 19:21:59 processing finished in 0.220108 sec
2021-05-20 19:22:00 BuildDem.cleanup (1/1): source data = Dense cloud, interpolation = Extrapolated, resolution = 0
2021-05-20 19:22:00 processing finished in 0.223007 sec
2021-05-20 19:22:01 BuildOrthomosaic: surface = Mesh, blending mode = Mosaic, refine seamlines = 0, ghosting filter = 0, resolution = 0.05
2021-05-20 19:22:01 Generating orthomosaic...
2021-05-20 19:22:01 processing finished in 0.214354 sec
2021-05-20 19:22:02 BuildOrthomosaic.initialize (1/1): surface = Mesh, blending mode = Mosaic, refine seamlines = 0, ghosting filter = 0, resolution = 0.05
2021-05-20 19:22:02 initializing...
2021-05-20 19:22:02 Error: Null model
2021-05-20 19:22:02 processing finished in 0.213471 sec
2021-05-20 19:22:03 BuildOrthomosaic.cleanup (1/1): surface = Mesh, blending mode = Mosaic, refine seamlines = 0, ghosting filter = 0, resolution = 0.05
2021-05-20 19:22:03 processing finished in 0.216932 sec
2021-05-20 19:22:04 ExportRaster: image_format = TIFF, path = /data/projects/rusty-johnson-east-of-house/orthomosaic.tif, save_alpha = off, source_data = Elevation
2021-05-20 19:22:04 Error: Null elevation
2021-05-20 19:22:04 processing finished in 0.222748 sec
Title: Re: Building and Exporting Orthomosaic using Network Processing
Post by: farmflightadam on May 21, 2021, 12:30:00 AM
Hey Alexey, we're making progress! We opened the project in the GUI and noticed that the images were not loaded. We went ahead and loaded the images then copied the project files back to our shared storage and we are now stitching. Here's the code that creates the project. How do we get this code to create the project and add the images to the project?

Code: [Select]
import Metashape
import glob
import sys
import os
import time

print(":: create project using sony rgb imagery ::")

time.sleep(1)


def do_create_project(directory, project):
    if not (os.path.isdir(directory)):
        print("-- image directory provided is invalid --")
        return 0

    if not project.lower().endswith(".psx"):
        project += ".psx"

    print(":: collecting imagery from directory ", directory, " ::")

    time.sleep(1)

    images = [photo for photo in glob.iglob(directory + "*.*", recursive=True)
              if os.path.isfile(photo) and os.path.splitext(photo)[1][1:].upper() in ["JPG", "JPEG"]]

    if not len(images):
        print("-- no images (TIF/TIFF) found in directory provided --")
        return 0

    print(":: found ", len(images), " images ::")

    time.sleep(1)

    print(":: creating new metashape project ::")

    doc = Metashape.Document()
    doc.save(project)

    chunk = doc.addChunk()
    chunk.label = os.path.basename(directory)
    chunk.loadReferenceExif()
    chunk.crs = Metashape.CoordinateSystem("EPSG::4326")

    doc.save()

    print(":: project created successfully ::")
    return 1


if len(sys.argv) > 2:
    direc = sys.argv[1]
    psx = sys.argv[2]
    do_create_project(direc, psx)
else:
    print("-- missing arguments - provide image directory path and project file to create --")
Title: Re: Building and Exporting Orthomosaic using Network Processing
Post by: farmflightadam on May 21, 2021, 10:29:28 AM
Hey Alexey,

I have been working on trying to get an NDVI (RedEdge) image to export but I am running into an issue. Here's the code

Code: [Select]
    chunk = doc.chunks[0]
    chunk.raster_transform.formula = ["(B4-B2)/(B4+B2)"]
    chunk.raster_transform.calibrateRange()
    chunk.raster_transform.enabled = True

    task = Metashape.Tasks.ExportRaster()
    task.path = dir_save + 'ndvi.tif'
    task.source_data = Metashape.DataSource.OrthomosaicData
    task.image_compression = compression
    task.image_format = Metashape.ImageFormat.ImageFormatTIFF
    task.raster_transform = Metashape.RasterTransformValue
    task.projection = chunk.crs
    task.save_world = False
    task.save_alpha = False
    task.save_kml = False
    task.white_background = False

    net_task = Metashape.NetworkTask()
    net_task.name = task.name
    net_task.params = task.encode()
    net_task.frames.append((chunk.key, 0))
    tasks.append(net_task)

The error I am getting is something about a missing color transform. If you could help, I would greatly appreciate it.

Thank you,

Adam M.
Title: Re: Building and Exporting Orthomosaic using Network Processing
Post by: Alexey Pasumansky on May 21, 2021, 03:47:57 PM
Hello Adam,

If the source images are expected to be in TIFF format, you should add the expected extension to the file path check:
Code: [Select]
images = [photo for photo in glob.iglob(directory + "*.*", recursive=True)
              if os.path.isfile(photo) and os.path.splitext(photo)[1][1:].upper() in ["TIFF", "TIF"]]

Also after creating a new chunk in the project chunk=doc.addChunk() you should add the line that adds the images to the chunk:
Code: [Select]
chunk.addPhotos(images)If the source data comes from MicaSense multispectral camera, then also use layout=Metashape.MultiplaneLayout argument to create a mult-camera system instances.
Title: Re: Building and Exporting Orthomosaic using Network Processing
Post by: Alexey Pasumansky on May 21, 2021, 03:57:40 PM
As for the raster transformation to be applied for the orthomosaic export, the formula should be defined in the actual project. If it is typical for most of the projects you are working with, you can add the formula definition to the project creation script.
It may be also reasonable to perform the reflectance calibration operation in advance (could be also included to the project creation script).

Additionally please check that the input formula for NDVI corresponds to NIR and Red bands, in Metashape they are sorted based on the Central WaveLength and for MicaSense cameras NVDI usually corresponds to
"(B5 - B3) / (B5 + B3)" formula.
Title: Re: Building and Exporting Orthomosaic using Network Processing
Post by: farmflightadam on May 21, 2021, 09:50:46 PM
As for the raster transformation to be applied for the orthomosaic export, the formula should be defined in the actual project. If it is typical for most of the projects you are working with, you can add the formula definition to the project creation script.
It may be also reasonable to perform the reflectance calibration operation in advance (could be also included to the project creation script).

Additionally please check that the input formula for NDVI corresponds to NIR and Red bands, in Metashape they are sorted based on the Central WaveLength and for MicaSense cameras NVDI usually corresponds to
"(B5 - B3) / (B5 + B3)" formula.

Can you further explain how this works please and provide a code sample? Does this mean that I have to create a new project for every single export that I am doing? Essentially, the Sony RGB images will be stitched and export an Orthomosaic, an NDVI, and an NDRE. Likewise, the Micasense Altum would produce those three as well as a Surface Model, an Elevation Model and an VARI for a total of six. Is there any possible way that we could pay you to help us further this along? We really need to get this working so that we can work on automating the entire process.

Thank you,

Adam
Title: Re: Building and Exporting Orthomosaic using Network Processing
Post by: Alexey Pasumansky on May 24, 2021, 03:17:53 PM
Hello Adam,

Raster transformation parameters are the project property and cannot be set as a part of export task.
If you need to calculate and output different indices for the same project, you'll need to modify the project properties before export. To do that you may need to create a Run Script task that will open the project and apply a proper raster transformation, then export the orthomosaic.
Title: Re: Building and Exporting Orthomosaic using Network Processing
Post by: Alexey Pasumansky on June 11, 2021, 06:24:42 PM
Hello Adam,


I have prepared the script prototype for the processing workflow + export operations, assuming that the project with the added images and coordinate information already exists.
Code: [Select]
import Metashape, sys
SERVER_IP = "192.168.10.10"

root = "//NAS1/datasets" # can be passed as a script argument if required #root = sys.argv[1]
Metashape.app.settings.network_path = "//NAS1/datasets"
project_path = "/processing/temp/project.psx" #relative path to project #project_path = sys.argv[2] #can be defined as script argument
output_folder = root + "/processing/export/" #absolute path to output folder for exports
doc = Metashape.Document()
doc.open(root + project_path) #loading existing project using relative path from the root
chunk = doc.chunk #active chunk of the project

tasks = []
#processing tasks
task = Metashape.Tasks.MatchPhotos()
task.downscale = 1
task.keypoint_limit = 40000
task.tiepoint_limit = 10000
task.generic_preselection = True
task.reference_preselection = True
tasks.append(task)

task = Metashape.Tasks.AlignCameras()
tasks.append(task)

task = Metashape.Tasks.OptimizeCameras()
tasks.append(task)

task = Metashape.Tasks.BuildDepthMaps()
task.downscale = 4
task.filter_mode = Metashape.MildFiltering
tasks.append(task)

task = Metashape.Tasks.BuildDenseCloud()
tasks.append(task)

task = Metashape.Tasks.BuildDem()
task.source_data = Metashape.DenseCloudData
tasks.append(task)

task = Metashape.Tasks.BuildOrthomosaic()
task.surface_data = Metashape.ElevationData
tasks.append(task)

#export tasks
task = Metashape.Tasks.ExportRaster()
task.path = output_folder + '/dem.tif'
task.source_data = Metashape.ElevationData
tasks.append(task)

task = Metashape.Tasks.ExportRaster()
task.path = output_folder + '/orthomosaic_default.tif'
task.source_data = Metashape.OrthomosaicData
task.raster_transform = Metashape.RasterTransformType.RasterTransformNone
task.save_alpha = False
tasks.append(task)

task = Metashape.Tasks.RunScript() #switching raster transformation in the project
task.code = 'import Metashape\ndoc = Metashape.Document()\ndoc.open(Metashape.app.settings.network_path + "' + project_path + '", ignore_lock = True)\nchunk = doc.chunk\nchunk.raster_transform.formula = ["(B4-B2)/(B4+B2)"]\nchunk.raster_transform.enabled = True\ndoc.save()\n'
tasks.append(task)

task = Metashape.Tasks.ExportRaster()
task.path = output_folder + '/orthomosaic_NDVI.tif'
task.source_data = Metashape.OrthomosaicData
task.raster_transform = Metashape.RasterTransformType.RasterTransformValue
task.save_alpha = False
tasks.append(task)


#converting tasks to network tasks
network_tasks = []
for task in tasks:
if task.target == Metashape.Tasks.DocumentTarget:
network_tasks.append(task.toNetworkTask(doc))
else:
network_tasks.append(task.toNetworkTask(chunk))

client = Metashape.NetworkClient()
client.connect(SERVER_IP) #
batch_id = client.createBatch(doc.path, network_tasks)
client.resumeBatch(batch_id)

Upon working on the script we have figured out that it would be helpful, that toNetworkTask and createBatch calls could automatically consider root path, this has been implemented in the version 1.7.4 build 12511 (pre-release), therefore I suggest to use the pre-release version when checking the script:
http://download.agisoft.com/metashape-pro_1_7_4_x64.msi
http://download.agisoft.com/metashape-pro_1_7_4.dmg
http://download.agisoft.com/metashape-pro_1_7_4_amd64.tar.gz
If the script is used with 1.7.3 release version, the script requires modification in the parts related to the paths.