Solaris Multimodal Preprocessing Library

Tutorial Part 3: SAR

[1]:
import matplotlib.pyplot as plt
import numpy as np
import os

import solaris.preproc.pipesegment as pipesegment
import solaris.preproc.image as image
import solaris.preproc.sar as sar
import solaris.preproc.optical as optical
import solaris.preproc.label as label

plt.rcParams['figure.figsize'] = [4, 4]
datadir = '../../../solaris/data/preproc_tutorial'
/home/sol/conda/envs/solaris/lib/python3.7/site-packages/tensorflow/python/framework/dtypes.py:526: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
  _np_qint8 = np.dtype([("qint8", np.int8, 1)])
/home/sol/conda/envs/solaris/lib/python3.7/site-packages/tensorflow/python/framework/dtypes.py:527: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
  _np_quint8 = np.dtype([("quint8", np.uint8, 1)])
/home/sol/conda/envs/solaris/lib/python3.7/site-packages/tensorflow/python/framework/dtypes.py:528: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
  _np_qint16 = np.dtype([("qint16", np.int16, 1)])
/home/sol/conda/envs/solaris/lib/python3.7/site-packages/tensorflow/python/framework/dtypes.py:529: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
  _np_quint16 = np.dtype([("quint16", np.uint16, 1)])
/home/sol/conda/envs/solaris/lib/python3.7/site-packages/tensorflow/python/framework/dtypes.py:530: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
  _np_qint32 = np.dtype([("qint32", np.int32, 1)])
/home/sol/conda/envs/solaris/lib/python3.7/site-packages/tensorflow/python/framework/dtypes.py:535: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
  np_resource = np.dtype([("resource", np.ubyte, 1)])

For the third example of the preprocessing library, we’ll look at synthetic aperture radar (SAR) data. SAR requires a number of specialized processing steps to go from complex data to the most widely-used finished products. A typical workflow might be:

91e6f5e3ebcd49fb890b39adfc32e8e0

And here’s a class to do that, with ShowImage objects added to illustrate some of the steps:

[2]:
class SARClass(pipesegment.PipeSegment):
    def __init__(self):
        super().__init__()
        self.feeder = (
            image.LoadImage(os.path.join(datadir, 'sar_hh.tif'))
            * sar.CapellaScaleFactor()
            * sar.Intensity() * image.ShowImage(caption='Intensity')
            * sar.Multilook(2) * image.ShowImage(caption='Multilook (Boxcar Filter)')
            * sar.Decibels() * image.ShowImage(caption='Conversion to Decibels')
            * sar.Orthorectify(projection = 32631, row_res=3, col_res=3) * image.ShowImage(caption='Orthorectification')
            * image.SaveImage(os.path.join(datadir, 'output3a.tif'))
        )

sar_processing = SARClass()
sar_processing()
Intensity
../../_images/tutorials_notebooks_preprocessing_sar_5_1.png
Multilook (Boxcar Filter)
../../_images/tutorials_notebooks_preprocessing_sar_5_3.png
Conversion to Decibels
../../_images/tutorials_notebooks_preprocessing_sar_5_5.png
Orthorectification
../../_images/tutorials_notebooks_preprocessing_sar_5_7.png
[2]:
<solaris.preproc.image.Image at 0x7f3c77bc6690>

The API reference lists possible arguments for these various classes. Things like the kernel size of the Multilook filter and how the Decibel class handles zeros can be adjusted through arguments.

Also in preproc.sar are classes for several polarimetric decompositions, including Freeman-Durden (for quad pol) and H-Alpha (for dual pol). A Pauli decomposition is shown here:

[3]:
class SARDecomposition(pipesegment.PipeSegment):
    def __init__(self):
        super().__init__()
        loadlist = [image.LoadImage(os.path.join(datadir, file))
                    * sar.CapellaScaleFactor() for file in
                    ['sar_hh.tif', 'sar_hv.tif', 'sar_vh.tif', 'sar_vv.tif']]
        stack = np.sum(loadlist) * image.MergeToStack()
        self.feeder = (
            stack
            * sar.DecompositionPauli(hh_band=0, vv_band=3, xx_band=1)
            * sar.Multilook(2)
            * sar.Decibels()
            * sar.Orthorectify(projection = 32631, row_res=3, col_res=3)
            * image.SaveImage(os.path.join(datadir, 'output3b.tif'))
            * image.ShowImage(bands=[1,2,0], vmin=-20, vmax=0)
        )

sar_decomposition = SARDecomposition()
sar_decomposition()
../../_images/tutorials_notebooks_preprocessing_sar_8_0.png
[3]:
<solaris.preproc.image.Image at 0x7f3c77da4b10>

In the code above, note how np.sum() respects the special meaning of + for PipeSegment subclasses.

Example 3 Follow-Up: Masking and Multimodal Datasets

Multimodal datasets contain multiple types of data covering the same area. A common task when preparing such datasets is taking a mask from one data source and applying it to another.

Suppose we have two equal-sized images covering the same area: a SAR image which is masked, and an optical image which isn’t. To apply the mask from the former to the latter:

[4]:
class TransferMask(pipesegment.PipeSegment):
    def __init__(self, masked_path, unmasked_path, output_path):
        super().__init__()
        load_masked = image.LoadImage(masked_path) * image.ShowImage(bands=[0])
        load_unmasked = image.LoadImage(unmasked_path) * image.ShowImage()
        get_mask = image.GetMask()
        set_mask = image.SetMask(0)
        save_output = image.SaveImage(output_path) * image.ShowImage()
        self.feeder = (load_unmasked + load_masked * get_mask) * set_mask * save_output

masked_path = os.path.join(datadir, 'sar_masked.tif')
unmasked_path = os.path.join(datadir, 'rgb_unmasked.tif')
output_path = os.path.join(datadir, 'output3c.tif')
transfer_mask = TransferMask(masked_path, unmasked_path, output_path)
transfer_mask()
../../_images/tutorials_notebooks_preprocessing_sar_12_0.png
../../_images/tutorials_notebooks_preprocessing_sar_12_1.png
../../_images/tutorials_notebooks_preprocessing_sar_12_2.png
[4]:
<solaris.preproc.image.Image at 0x7f3c7624e050>

The SetMask class expects two inputs: the first is the image to be masked, and the second is the mask to use. An optional argument to SetMask allows for reversing the order of the arguments. By default, preproc uses NaN (the float value that’s “not a number”) as the mask value, but this too can be modified with arguments to GetMask and SetMask.

For imagery labels and for data types beyond imagery, the preproc.labels module contains classes for working with vector labels and other data in GeoPandas.

Epilogue

The classes in the preprocessing library can be used in different ways. They can be used one-at-a-time within a traditional (“imperative”) program, like this:

[5]:
#Read in an image, and store it in the variable 'myimage'
myimage = image.LoadImage(os.path.join(datadir, 'ms1.tif'))()

#Now resize the image, and store the result in a different variable
mythumbnail = (myimage * image.Resize(50,50))()

(Note the empty parentheses at the end of each line – these lines instantiate an object and call it immediately.)

But these classes are more powerful when they are attached to each other to build up more complicated workflows. That can be done by defining a one-time-use object:

[6]:
thumbnail_pipeline = image.LoadImage(os.path.join(datadir, 'ms1.tif')) \
    * image.Resize(50,50)
mythumbnail = thumbnail_pipeline()

Or by defining a reusable class:

[7]:
class Thumbnail(pipesegment.PipeSegment):
    def __init__(self):
        super().__init__()
        load = image.LoadImage(os.path.join(datadir, 'ms1.tif'))
        size = image.Resize(50,50)
        self.feeder = load * size
thumbnail_pipeline = Thumbnail()
mythumbnail = thumbnail_pipeline()

By defining a workflow, the program takes care of details like figuring out the order in which to do the different calculations and remembering where each intermediate result is stored. To see what the program is actually doing when the instance is called, replace mythumbnail = thumbnail_pipeline() with mythumbnail = thumbnail_pipeline(verbose=1). Or set the verbose argument to 2 or 3 for additional information. This is useful for debugging.

Finally, this tutorial has focused on creating new capabilities by combining and using the library’s existing classes. For capabilities that can’t be built out of the existing preproc classes, one can create a new class directly from the PipeSegment base class. The new class should be given a transform method that takes some input, does a calculation, and returns some output. The whole preproc source code is mostly classes of this type and is a ready source of examples.

For completeness, here’s a short example. This class takes a string as input and returns the string in all capital letters.

[8]:
class Uppercase(pipesegment.PipeSegment):
    def transform(self, pin):
        return pin.upper()

(pipesegment.LoadSegment('Thanks for reading!') * Uppercase())()
[8]:
'THANKS FOR READING!'