10-band Batch Processing Example¶

In this example, we use the micasense.imageset class to load a set of directories of images into a list of micasense.capture objects, and we iterate over that list saving out each image as an aligned stack of images as separate bands in a single tiff file each. Next, we use the metadata from the original captures to write out a log file of the captures and their locations. Finally, we use exiftool from the command line to inject that metadata into the processed images, allowing us to stitch those images using commercial software such as Pix4D or Agisoft.

For an example dataset, download and unzip either sample dataset at https://www.micasense.com/dual-camera-sample-data into your Downloads folder, and ensure the paths below point to the correct location.

In [1]:
%load_ext autoreload
%autoreload 2

Load Images into ImageSet¶

In [2]:
from ipywidgets import FloatProgress, Layout
from IPython.display import display
import micasense.capture as capture
import os
import glob

panelNames = None
useDLS = True

imagePath = os.path.expanduser(os.path.join(".", "data", "REDEDGE-MX-DUAL"))
panelNames = glob.glob(os.path.join(imagePath, "IMG_0000_*.tif"))

outputPath = os.path.join(imagePath, "..", "stacks")
thumbnailPath = os.path.join(outputPath, "..", "thumbnails")


overwrite = False  # Set to False to continue interrupted processing
generateThumbnails = True

# Allow this code to align both radiance and reflectance images; bu excluding
# a definition for panelNames above, radiance images will be used
# For panel images, efforts will be made to automatically extract the panel information
# but if the panel/firmware is before Altum 1.3.5, RedEdge 5.1.7 the panel reflectance
# will need to be set in the panel_reflectance_by_band variable.
# Note: radiance images will not be used to properly create NDVI/NDRE images below.
if panelNames is not None:
    panelCap = capture.Capture.from_filelist(panelNames)
else:
    panelCap = None

if panelCap is not None:
    if panelCap.panel_albedo() is not None:
        panel_reflectance_by_band = panelCap.panel_albedo()
    else:
        panel_reflectance_by_band = [0.65] * len(panelCap.images)  # inexact, but quick
    panel_irradiance = panelCap.panel_irradiance(panel_reflectance_by_band)
    img_type = "reflectance"
else:
    if useDLS:
        img_type = "reflectance"
    else:
        img_type = "radiance"
In [3]:
import micasense.imageset as imageset

## This progress widget is used for display of the long-running process
f = FloatProgress(min=0, max=1, layout=Layout(width="100%"), description="Loading")
display(f)


def update_f(val):
    if (
        val - f.value
    ) > 0.005 or val == 1:  # reduces cpu usage from updating the progressbar by 10x
        f.value = val


imgset = imageset.ImageSet.from_directory(imagePath, progress_callback=update_f)
%time imgset = imageset.ImageSet.from_directory(imagePath, progress_callback=update_f)
update_f(1.0)
CPU times: user 23.4 ms, sys: 8.06 ms, total: 31.4 ms
Wall time: 285 ms

Define which warp method to use¶

For newer data sets with RigRelatives tags (images captured with RedEdge version 3.4.0 or greater with a valid calibration load, see https://support.micasense.com/hc/en-us/articles/360005428953-Updating-RedEdge-for-Pix4Dfields), we can use the RigRelatives for a simple alignment.

For sets without those tags, or sets that require a RigRelatives optimization, we can go through the Alignment.ipynb notebook and get a set of warp_matrices that we can use here to align.

In [4]:
from numpy import array
from numpy import float32

# Use the warp_matrices derived from the Alignment Tutorial for this RedEdge set without RigRelatives
warp_matrices = [
    array(
        [
            [9.7928989e-01, 1.3615261e-04, 1.9574374e-02],
            [-3.7023663e-03, 9.9245304e-01, 2.8355631e01],
            [-1.0651237e-05, 2.6095395e-06, 1.0000000e00],
        ],
        dtype=float32,
    ),
    array(
        [
            [9.9525303e-01, -7.0169556e-04, -6.3409243e00],
            [-7.1914408e-05, 9.9811482e-01, -6.2239196e-02],
            [-3.6460631e-06, 1.8568957e-06, 1.0000000e00],
        ],
        dtype=float32,
    ),
    array(
        [
            [9.9596852e-01, -2.5820474e-03, -1.2950540e01],
            [2.8751660e-03, 9.9919146e-01, 2.6645420e01],
            [-1.8280600e-06, 2.3902323e-06, 1.0000000e00],
        ],
        dtype=float32,
    ),
    array(
        [
            [9.9592507e-01, 4.5777867e-03, 1.4982515e01],
            [-3.4031910e-03, 9.9773425e-01, 2.2611160e01],
            [-9.0995320e-07, 2.3299665e-06, 1.0000000e00],
        ],
        dtype=float32,
    ),
    array(
        [
            [1.0000000e00, 2.8202797e-20, 8.9091027e-15],
            [-5.6938062e-19, 1.0000000e00, 6.4872178e-15],
            [-1.2599002e-21, 1.3424955e-23, 1.0000000e00],
        ],
        dtype=float32,
    ),
    array(
        [
            [9.9348122e-01, -1.8129036e-02, 1.1003100e00],
            [1.2132729e-02, 9.9091125e-01, 2.8742294e01],
            [-2.9285566e-06, -6.2816152e-06, 1.0000000e00],
        ],
        dtype=float32,
    ),
    array(
        [
            [9.8954433e-01, -1.8810771e-02, 1.1892141e01],
            [1.3862181e-02, 9.8975760e-01, 2.0044521e01],
            [-4.0683108e-06, -4.2664565e-06, 1.0000000e00],
        ],
        dtype=float32,
    ),
    array(
        [
            [9.8794365e-01, -1.2153405e-02, 1.8298512e01],
            [8.4343022e-03, 9.8829275e-01, 2.8633160e01],
            [-2.5430195e-06, -3.1491891e-06, 1.0000000e00],
        ],
        dtype=float32,
    ),
    array(
        [
            [9.9505156e-01, -1.7393423e-02, 3.6898847e00],
            [1.3367594e-02, 9.9287426e-01, 1.8703876e01],
            [-1.3000300e-06, -4.7085673e-06, 1.0000000e00],
        ],
        dtype=float32,
    ),
    array(
        [
            [9.9182397e-01, -1.2042225e-02, 1.2686370e01],
            [7.6934961e-03, 9.8897797e-01, 2.7970253e01],
            [-9.2655773e-07, -6.0620891e-06, 1.0000000e00],
        ],
        dtype=float32,
    ),
]

Align images and save each capture to a layered tiff file¶

In [5]:
import datetime

use_multi_process = True  # set to False for single-process saving
overwrite_existing = False  # skip existing files, set to True to overwrite

## This progress widget is used for display of the long-running process
f2 = FloatProgress(min=0, max=1, layout=Layout(width="100%"), description="Saving")
display(f2)


def update_f2(val):
    f2.value = val


if not os.path.exists(outputPath):
    os.makedirs(outputPath)
if generateThumbnails and not os.path.exists(thumbnailPath):
    os.makedirs(thumbnailPath)


# If we didn't provide a panel above, irradiance set to None will cause DLS data to be used
try:
    irradiance = panel_irradiance + [0]
except NameError:
    irradiance = None

start_time = datetime.datetime.now()

# Save all captures in the imageset as aligned stacks
imgset.save_stacks(
    warp_matrices,
    outputPath,
    thumbnailPath,
    irradiance=irradiance,
    multiprocess=use_multi_process,
    overwrite=overwrite_existing,
    progress_callback=update_f2,
)

end_time = datetime.datetime.now()
update_f2(1.0)

print("Saving time: {}".format(end_time - start_time))
print(
    "Alignment+Saving rate: {:.2f} captures per second".format(
        float(len(imgset.captures)) / float((end_time - start_time).total_seconds())
    )
)
Saving time: 0:00:04.299224
Alignment+Saving rate: 0.47 captures per second

Extract Metadata from Captures list and save to log.csv¶

In [6]:
def decdeg2dms(dd):
    is_positive = dd >= 0
    dd = abs(dd)
    minutes, seconds = divmod(dd * 3600, 60)
    degrees, minutes = divmod(minutes, 60)
    degrees = degrees if is_positive else -degrees
    return (degrees, minutes, seconds)


header = "SourceFile,\
GPSDateStamp,GPSTimeStamp,\
GPSLatitude,GpsLatitudeRef,\
GPSLongitude,GPSLongitudeRef,\
GPSAltitude,GPSAltitudeRef,\
FocalLength,\
XResolution,YResolution,ResolutionUnits\n"

lines = [header]
for capture in imgset.captures:
    # get lat,lon,alt,time
    outputFilename = capture.uuid + ".tif"
    fullOutputPath = os.path.join(outputPath, outputFilename)
    lat, lon, alt = capture.location()
    # write to csv in format:
    # IMG_0199_1.tif,"33 deg 32' 9.73"" N","111 deg 51' 1.41"" W",526 m Above Sea Level
    latdeg, latmin, latsec = decdeg2dms(lat)
    londeg, lonmin, lonsec = decdeg2dms(lon)
    latdir = "North"
    if latdeg < 0:
        latdeg = -latdeg
        latdir = "South"
    londir = "East"
    if londeg < 0:
        londeg = -londeg
        londir = "West"
    resolution = capture.images[0].focal_plane_resolution_px_per_mm

    linestr = '"{}",'.format(fullOutputPath)
    linestr += capture.utc_time().strftime("%Y:%m:%d,%H:%M:%S,")
    linestr += '"{:d} deg {:d}\' {:.2f}"" {}",{},'.format(
        int(latdeg), int(latmin), latsec, latdir[0], latdir
    )
    linestr += '"{:d} deg {:d}\' {:.2f}"" {}",{},{:.1f} m Above Sea Level,Above Sea Level,'.format(
        int(londeg), int(lonmin), lonsec, londir[0], londir, alt
    )
    linestr += "{}".format(capture.images[0].focal_length)
    linestr += "{},{},mm".format(resolution, resolution)
    linestr += (
        "\n"  # when writing in text mode, the write command will convert to os.linesep
    )
    lines.append(linestr)

fullCsvPath = os.path.join(outputPath, "log.csv")
with open(fullCsvPath, "w") as csvfile:  # create CSV
    csvfile.writelines(lines)

Use Exiftool from the command line to write metadata to images¶

In [7]:
import subprocess

if os.environ.get("exiftoolpath") is not None:
    exiftool_cmd = os.path.normpath(os.environ.get("exiftoolpath"))
else:
    exiftool_cmd = "exiftool"

cmd = '{} -csv="{}" -overwrite_original {}'.format(
    exiftool_cmd, fullCsvPath, outputPath
)
print(cmd)
if subprocess.check_call(cmd.split(" ")) == 0:
    print("Successfully updated stack metadata")
exiftool -csv="./data/REDEDGE-MX-DUAL/../stacks/log.csv" -overwrite_original ./data/REDEDGE-MX-DUAL/../stacks
Successfully updated stack metadata
Error opening CSV file '"./data/REDEDGE-MX-DUAL/../stacks/log.csv"'
No SourceFile './data/REDEDGE-MX-DUAL/../stacks/0000_Rb0pibHa08uHJwrTjf8Y.tif' in imported CSV database
(full path: '/home/runner/work/micasense_imageprocessing/micasense_imageprocessing/data/stacks/0000_Rb0pibHa08uHJwrTjf8Y.tif')
No SourceFile './data/REDEDGE-MX-DUAL/../stacks/0001_w5Sawomm8omEMHM0TiFc.tif' in imported CSV database
(full path: '/home/runner/work/micasense_imageprocessing/micasense_imageprocessing/data/stacks/0001_w5Sawomm8omEMHM0TiFc.tif')
No SourceFile './data/REDEDGE-MX-DUAL/../stacks/rsIOD01Q0g7yXxWup7Jr.tif' in imported CSV database
(full path: '/home/runner/work/micasense_imageprocessing/micasense_imageprocessing/data/stacks/rsIOD01Q0g7yXxWup7Jr.tif')
No SourceFile './data/REDEDGE-MX-DUAL/../stacks/rTNoYKJi117Kn6Z46Lhs.tif' in imported CSV database
(full path: '/home/runner/work/micasense_imageprocessing/micasense_imageprocessing/data/stacks/rTNoYKJi117Kn6Z46Lhs.tif')
    1 directories scanned
    0 image files read
In [ ]: