Skip to content

Sentinel-2 Normalized Burn Ratio (NBR)

The goal is to produce a Sentinel-2 NBR geotif using CWl to orchestrate gdal and OTB.

The processing steps are:

  • use gdal_translate to clip the COG over the area of interest expressed as a bounding box for the NIR, SWIR22 and Scene Classification
  • use OTB's otbcli_BandMathX to do the band arithmetic and generate the NBR

Data used

The data used are Sentinel-2 products that were converted to COG and STAC and published on publicly accessible URLs.

Inputs

The inputs are:

  • A Sentinel-2 STAC Item URL
  • An area of interest expressed as a bounding box

Steps

STAC asset href resolution

NBR requires the NIR, SWIR Sentinel-2 assets and we'll use the Scene Classification to process the normalized burn ratio only over valid pixels (e.g. avoid clouds, water, etc.).

The first workflow step is getting the B8A (NIR), B12 (SWIR22) and the Scene Classification hrefs.

This is done with the CWL CommandLineTool available in the asset.cwl file:

class: CommandLineTool

requirements:
  DockerRequirement: 
    dockerPull: terradue/jq
  ShellCommandRequirement: {}
  InlineJavascriptRequirement: {}

baseCommand: curl
arguments:
- -s
- valueFrom: ${ return inputs.stac_item; }
- "|"
- jq
- valueFrom: ${ return ".assets." + inputs.asset + ".href"; }
- "|"
- tr 
- -d
- '\"' #\""

stdout: message

inputs:
  stac_item:
    type: string
  asset:
    type: string

outputs:

  asset_href: 
    type: string
    outputBinding:
      glob: message
      loadContents: true
      outputEval: $( self[0].contents.split("\n").join("") )

cwlVersion: v1.0

Extract the data over the area of interest

gdal_translate can take VSI URLs as input and extract an area of interest.

This step takes the resolved STAC assets href and produces clips of the original Sentinel-2 acquisition.

This is achieved with the CWL CommandLineTool available in the translate.cwl file:

class: CommandLineTool

requirements:
  InlineJavascriptRequirement: {}
  DockerRequirement: 
    dockerPull: osgeo/gdal

baseCommand: gdal_translate
arguments:
- -projwin 
- valueFrom: ${ return inputs.bbox.split(",")[0]; }
- valueFrom: ${ return inputs.bbox.split(",")[3]; }
- valueFrom: ${ return inputs.bbox.split(",")[2]; }
- valueFrom: ${ return inputs.bbox.split(",")[1]; }
- -projwin_srs
- valueFrom: ${ return inputs.epsg; }
- valueFrom: |
    ${ if (inputs.asset.startsWith("http")) {

         return "/vsicurl/" + inputs.asset; 

       } else { 

         return inputs.asset;

       } 
    }
- valueFrom: ${ return inputs.asset.split("/").slice(-1)[0]; }

inputs: 
  asset: 
    type: string
  bbox: 
    type: string
  epsg:
    type: string
    default: "EPSG:4326" 

outputs:
  tifs:
    outputBinding:
      glob: '*.tif'
    type: File

cwlVersion: v1.0

Normalized difference using the clipped tifs.

The final step uses OTB's otbcli_BandMathX to do the band arithmetic and generate the NBR.

The CWl CommandLineTool to accomplish that is available in the band_math.cwl file:

class: CommandLineTool

requirements:
  InlineJavascriptRequirement: {}
  DockerRequirement: 
    dockerPull: terradue/otb-7.2.0

baseCommand: otbcli_BandMathX
arguments: 
- -out
- valueFrom: ${ return inputs.stac_item.split("/").slice(-1)[0] + ".tif"; }
- -exp
- '(im3b1 == 8 or im3b1 == 9 or im3b1 == 0 or im3b1 == 1 or im3b1 == 2 or im3b1 == 10 or im3b1 == 11) ? -2 : (im1b1 - im2b1) / (im1b1 + im2b1)'

inputs:

  tifs:
    type: File[]
    inputBinding:
      position: 5
      prefix: -il
      separate: true

  stac_item:
    type: string

outputs:

  nbr:
    outputBinding:
      glob: "*.tif"
    type: File

cwlVersion: v1.0

Putting the pieces together

The single CWL documents encapsulating the different steps of the final workflow are orchestrated as CWL Workflow.

class: Workflow
label: NBR - produce the normalized difference between NIR and SWIR 22 
doc: NBR - produce the normalized difference between NIR and SWIR 22 
id: main

requirements:
- class: ScatterFeatureRequirement

inputs:
  stac_item: 
    doc: Sentinel-2 item
    type: string
  aoi: 
    doc: area of interest as a bounding box
    type: string
  bands: 
    type: string[]
    default: ["B8A", "B12", "SCL"]

outputs:
  nbr:
    outputSource:
    - node_nbr/nbr
    type: File

steps:

  node_stac:

    run: asset.cwl

    in:
      stac_item: stac_item
      asset: bands

    out:
      - asset_href

    scatter: asset
    scatterMethod: dotproduct 

  node_subset:

    run: translate.cwl  

    in: 
      asset: 
        source: node_stac/asset_href
      bbox: aoi

    out:
    - tifs

    scatter: asset
    scatterMethod: dotproduct

  node_nbr:

    run: band_math.cwl

    in:
      stac_item: stac_item
      tifs: 
        source: [node_subset/tifs]

    out:
    - nbr

cwlVersion: v1.0

The workflow uses CWL's ScatterFeatureRequirement to run some of the steps in parallel:

  • Each asset is resolved as an individual process
  • Each gdal_translate is invoked individually

The final node aggregates the inputs into a the NBR product.

This CWL Workflow can be invoked with the parameters:

stac_item: "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2B_53HPA_20210723_0_L2A"
aoi: "136.659,-35.96,136.923,-35.791"

And using cwltool:

cwltool --parallel nbr.cwl nbr.yml

This will output:

INFO /srv/conda/bin/cwltool 3.0.20210319143721
INFO Resolved 'nbr.cwl' to 'file:///home/fbrito/work/sentinel-2-nbr/nbr.cwl'
INFO [workflow ] starting step node_stac
INFO [step node_stac] start
INFO [workflow ] start
INFO [step node_stac] start
INFO [step node_stac] start
INFO [job node_stac] /tmp/6z7ooxq4$ docker \
    run \
    -i \
    --mount=type=bind,source=/tmp/6z7ooxq4,target=/AHORGP \
    --mount=type=bind,source=/tmp/fxfeqivt,target=/tmp \
    --workdir=/AHORGP \
    --read-only=true \
    --log-driver=none \
    --user=1000:1000 \
    --rm \
    --env=TMPDIR=/tmp \
    --env=HOME=/AHORGP \
    --cidfile=/tmp/36mv9qqs/20210803131208-777472.cid \
    terradue/jq \
    /bin/sh \
    -c \
    'curl' '-s' 'https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2B_53HPA_20210723_0_L2A' | 'jq' '.assets.B8A.href' | 'tr' '-d' \" > /tmp/6z7ooxq4/message
INFO [job node_stac_2] /tmp/lukn7xa_$ docker \
    run \
    -i \
    --mount=type=bind,source=/tmp/lukn7xa_,target=/AHORGP \
    --mount=type=bind,source=/tmp/j3ojpzbw,target=/tmp \
    --workdir=/AHORGP \
    --read-only=true \
    --log-driver=none \
    --user=1000:1000 \
    --rm \
    --env=TMPDIR=/tmp \
    --env=HOME=/AHORGP \
    --cidfile=/tmp/2pbnjsek/20210803131208-803455.cid \
    terradue/jq \
    /bin/sh \
    -c \
    'curl' '-s' 'https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2B_53HPA_20210723_0_L2A' | 'jq' '.assets.B12.href' | 'tr' '-d' \" > /tmp/lukn7xa_/message
INFO [job node_stac_3] /tmp/8susbghr$ docker \
    run \
    -i \
    --mount=type=bind,source=/tmp/8susbghr,target=/AHORGP \
    --mount=type=bind,source=/tmp/w3eo95ll,target=/tmp \
    --workdir=/AHORGP \
    --read-only=true \
    --log-driver=none \
    --user=1000:1000 \
    --rm \
    --env=TMPDIR=/tmp \
    --env=HOME=/AHORGP \
    --cidfile=/tmp/wgl0e8p6/20210803131208-816645.cid \
    terradue/jq \
    /bin/sh \
    -c \
    'curl' '-s' 'https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2B_53HPA_20210723_0_L2A' | 'jq' '.assets.SCL.href' | 'tr' '-d' \" > /tmp/8susbghr/message
INFO [job node_stac] Max memory used: 0MiB
INFO [job node_stac_3] Max memory used: 0MiB
INFO [job node_stac_2] Max memory used: 0MiB
INFO [job node_stac] completed success
INFO [job node_stac_3] completed success
INFO [job node_stac_2] completed success
INFO [step node_stac] completed success
INFO [workflow ] starting step node_subset
INFO [step node_subset] start
INFO [step node_subset] start
INFO [step node_subset] start
INFO [job node_subset] /tmp/354unm3n$ docker \
    run \
    -i \
    --mount=type=bind,source=/tmp/354unm3n,target=/AHORGP \
    --mount=type=bind,source=/tmp/rna17u4f,target=/tmp \
    --workdir=/AHORGP \
    --read-only=true \
    --user=1000:1000 \
    --rm \
    --env=TMPDIR=/tmp \
    --env=HOME=/AHORGP \
    --cidfile=/tmp/bu2jkwzt/20210803131211-498283.cid \
    osgeo/gdal \
    gdal_translate \
    -projwin \
    136.659 \
    -35.791 \
    136.923 \
    -35.96 \
    -projwin_srs \
    EPSG:4326 \
    /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/53/H/PA/2021/7/S2B_53HPA_20210723_0_L2A/B8A.tif \
    B8A.tif
INFO [job node_subset_2] /tmp/4wetamd3$ docker \
    run \
    -i \
    --mount=type=bind,source=/tmp/4wetamd3,target=/AHORGP \
    --mount=type=bind,source=/tmp/lb07hcp3,target=/tmp \
    --workdir=/AHORGP \
    --read-only=true \
    --user=1000:1000 \
    --rm \
    --env=TMPDIR=/tmp \
    --env=HOME=/AHORGP \
    --cidfile=/tmp/op_rhfej/20210803131211-569257.cid \
    osgeo/gdal \
    gdal_translate \
    -projwin \
    136.659 \
    -35.791 \
    136.923 \
    -35.96 \
    -projwin_srs \
    EPSG:4326 \
    /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/53/H/PA/2021/7/S2B_53HPA_20210723_0_L2A/B12.tif \
    B12.tif
INFO [job node_subset_3] /tmp/zd2y8jwn$ docker \
    run \
    -i \
    --mount=type=bind,source=/tmp/zd2y8jwn,target=/AHORGP \
    --mount=type=bind,source=/tmp/vr39h9nm,target=/tmp \
    --workdir=/AHORGP \
    --read-only=true \
    --user=1000:1000 \
    --rm \
    --env=TMPDIR=/tmp \
    --env=HOME=/AHORGP \
    --cidfile=/tmp/3yl71gjr/20210803131211-604042.cid \
    osgeo/gdal \
    gdal_translate \
    -projwin \
    136.659 \
    -35.791 \
    136.923 \
    -35.96 \
    -projwin_srs \
    EPSG:4326 \
    /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/53/H/PA/2021/7/S2B_53HPA_20210723_0_L2A/SCL.tif \
    SCL.tif
Input file size is 5490, 5490
0Input file size is 5490, 5490
0Input file size is 5490, 5490
0...10...20...30...40...50...60...70...80...90...100 - done.
INFO [job node_subset_3] Max memory used: 19MiB
INFO [job node_subset_3] completed success
...10...20...30...40...50...60...70...80...90...100 - done.
INFO [job node_subset] Max memory used: 25MiB
INFO [job node_subset] completed success
...10...20...30...40...50...60...70...80...90...100 - done.
INFO [job node_subset_2] Max memory used: 25MiB
INFO [job node_subset_2] completed success
INFO [step node_subset] completed success
INFO [workflow ] starting step node_nbr
INFO [step node_nbr] start
INFO [job node_nbr] /tmp/ufuo4sxs$ docker \
    run \
    -i \
    --mount=type=bind,source=/tmp/ufuo4sxs,target=/AHORGP \
    --mount=type=bind,source=/tmp/85nzo8d3,target=/tmp \
    --mount=type=bind,source=/tmp/354unm3n/B8A.tif,target=/var/lib/cwl/stg68264295-cc93-4bd9-989f-7c8c5fe1d55b/B8A.tif,readonly \
    --mount=type=bind,source=/tmp/4wetamd3/B12.tif,target=/var/lib/cwl/stg38a3dc1d-5b7e-4ed9-aff5-42564f25ca03/B12.tif,readonly \
    --mount=type=bind,source=/tmp/zd2y8jwn/SCL.tif,target=/var/lib/cwl/stg89b3a6a0-9c1d-4d16-bae9-1dfa1da681a5/SCL.tif,readonly \
    --workdir=/AHORGP \
    --read-only=true \
    --user=1000:1000 \
    --rm \
    --env=TMPDIR=/tmp \
    --env=HOME=/AHORGP \
    --cidfile=/tmp/hzj6lygr/20210803131224-614651.cid \
    terradue/otb-7.2.0 \
    otbcli_BandMathX \
    -out \
    S2B_53HPA_20210723_0_L2A.tif \
    -exp \
    '(im3b1 == 8 or im3b1 == 9 or im3b1 == 0 or im3b1 == 1 or im3b1 == 2 or im3b1 == 10 or im3b1 == 11) ? -2 : (im1b1 - im2b1) / (im1b1 + im2b1)' \
    -il \
    /var/lib/cwl/stg68264295-cc93-4bd9-989f-7c8c5fe1d55b/B8A.tif \
    /var/lib/cwl/stg38a3dc1d-5b7e-4ed9-aff5-42564f25ca03/B12.tif \
    /var/lib/cwl/stg89b3a6a0-9c1d-4d16-bae9-1dfa1da681a5/SCL.tif
2021-08-03 11:12:24 (INFO) BandMathX: Default RAM limit for OTB is 256 MB
2021-08-03 11:12:24 (INFO) BandMathX: GDAL maximum cache size is 3197 MB
2021-08-03 11:12:24 (INFO) BandMathX: OTB will use at most 16 threads
2021-08-03 11:12:24 (INFO) BandMathX: Image #1 has 1 components
2021-08-03 11:12:24 (INFO) BandMathX: Image #2 has 1 components
2021-08-03 11:12:24 (INFO) BandMathX: Image #3 has 1 components
2021-08-03 11:12:24 (INFO) BandMathX: Using expression: (im3b1 == 8 or im3b1 == 9 or im3b1 == 0 or im3b1 == 1 or im3b1 == 2 or im3b1 == 10 or im3b1 == 11) ? -2 : (im1b1 - im2b1) / (im1b1 + im2b1)
2021-08-03 11:12:24 (INFO): Estimated memory for full processing: 21.4543MB (avail.: 256 MB), optimal image partitioning: 1 blocks
2021-08-03 11:12:24 (INFO): File S2B_53HPA_20210723_0_L2A.tif will be written in 1 blocks of 1175x959 pixels
Writing S2B_53HPA_20210723_0_L2A.tif...: 100% [**************************************************] (0s)
INFO [job node_nbr] Max memory used: 0MiB
INFO [job node_nbr] completed success
INFO [step node_nbr] completed success
INFO [workflow ] completed success
{
    "nbr": {
        "location": "file:///home/fbrito/work/sentinel-2-nbr/S2B_53HPA_20210723_0_L2A.tif",
        "basename": "S2B_53HPA_20210723_0_L2A.tif",
        "class": "File",
        "checksum": "sha1$62ca971658766d3a8c0e975fce87ce850ce5f180",
        "size": 4515438,
        "path": "/home/fbrito/work/sentinel-2-nbr/S2B_53HPA_20210723_0_L2A.tif"
    }
}
INFO Final process status is success