Remote Sensing - Water Quality
Remote sensing water quality analysis
This notebook walks through the steps required to create an interactive map widget displaying simple band-ratio algorithms for water quality analysis using cloud-optimized GeoTIFFs. This example will show how to use Normalized Difference Water Index to differentiate water bodies from land.
Fetching OpenLayers packages
The OpenLayers map widget will allow us to fetch multi-spectral satellite imagery in GeoTIFF format and manipulate the data with WebGL.
First fetch the packages from CDNs:
(kind/hiccup:div
[:link {:rel "stylesheet"
[:href "https://cdn.jsdelivr.net/npm/ol@v10.6.0/ol.css"}]
:script {:src "https://cdn.jsdelivr.net/npm/geotiff"}]
[:script {:src "https://cdn.jsdelivr.net/npm/ol@v10.6.0/dist/ol.js"}]]) [
Handling the GeoTIFF files
We will create an OpenLayers WebGL layer that uses a GeoTIFF source. Here we are using a Sentinel-2 10m resolution GeoTIFF image that contains blue, green, red and near infra-red bands.
(kind/scittledef geotiff-sources
'(
(js/ol.source.GeoTIFF.
(clj->js:sources [{:min 0
{:nodata 0
:max 10000
:bands [1 ;; B02 blue (490nm) -> band 1
2 ;; B03 green (560nm) -> band 2
3 ;; B04 red (665nm) -> band 3
4 ;; B08 NIR (841nm) -> band 4
]:url "https://s2downloads.eox.at/demo/Sentinel-2/3857/R10m.tif"}]}))))
OpenLayers WebGL DSL helpers
Openlayers has it’s own lisp-oid DSL that gets converted to GLSL. Here we set up a few helper functions to allow us to write it more clearly.
(kind/scittle;; helper fns for writing in OpenLayers GL DSL
do
'(defn gl-var [kw]
("var" kw])
[defn gl-band [band-key]
("band" (band-key {:blue 1
[:green 2
:red 3
:NIR 4})])))
Normalized Difference Water Index
\[\text{NDWI} = \frac{\text{Green} - \text{NIR}}{\text{Green} + \text{NIR}}\]
NDWI is a band ratio Index that allows us to detect the presence of bodies of water. Water bodies absorb most NIR and reflect plenty of green light, so they result in a positive NDWI. Vegetation and soil strongly reflect NIR and and moderately reflect green light so they result in a negative NDWI.
(kind/scittledef NDWI
'("/"
["-" (gl-band :green) (gl-band :NIR)]
["+" (gl-band :green) (gl-band :NIR)]])) [
WebGL instructions
Here we compose the WebGL instructions that will get converted into a GLSL fragment shader. We can access variables that will be passed in a separate variables map, and also the values from the bands selected above.
In this example we show the NDWI value in a gradient from blue to green. NDWI values below the land_threshold
are displayed in RGB with a brightness variable so we can dim the land pixels dynamically.
(kind/scittledef color
'(let [opaque-blue [1 0 0 255]
(0 1 0 255]]
opaque-green ["case"
["<" NDWI (gl-var :land_threshold)]
["color"
["*" (gl-var :land_brightness) (gl-band :red)]
["*" (gl-var :land_brightness) (gl-band :green)]
["*" (gl-var :land_brightness) (gl-band :blue)]
[225]
"interpolate"
["linear"]
[
NDWI:ndwi_upper) opaque-blue ; (lower bound)
(gl-var :ndwi_lower) opaque-green ; (upper bound)
(gl-var ]])))
(kind/scittle;; default variable values for the map vis
def param-defaults {:land_threshold 0.0
'(:land_brightness 3.0
:ndwi_upper 1.0
:ndwi_lower 0.0}))
Openlayers map widget setup
Below we create a map widget with a WebGL tile and a view based on the bounds of our GeoTIFF file.
(kind/scittledef webgl-tile-layer
'(
(js/ol.layer.WebGLTile.:style {:variables param-defaults
(clj->js {:color color}
:source geotiff-sources}))))
(kind/scittlelet [osm-layer (->> {:source (js/ol.source.OSM.)}
'(
clj->js
js/ol.layer.Tile.)
->> {:center [0 0]
view (:zoom 2}
clj->js
js/ol.View.)]->> {:target "map"
(:layers [webgl-tile-layer]
:view (.getView geotiff-sources)}
clj->js js/ol.Map.)))
Dynamic controls
Using reagent we can create some sliders to control the WebGL variables, triggering a redraw on change.
(kind/reagentfn []
['(let [*map-params (reagent.core/atom param-defaults)
(
sliderfn [label param minv maxv]
(let [value (param @*map-params)]
(:div
[:label {:for param} label] " : "
[:input
[:id param
{:type "range"
:min minv
:max maxv
:value value
:step 0.01
:on-change (fn [e]
let [newv (js/parseFloat (.. e -target -value))]
(swap! *map-params #(assoc % param newv))
(
(.updateStyleVariables
webgl-tile-layer@*map-params))))}]]))]
(clj->js
fn []
(:div {:style {:text-align "right"}}
["Land Threshold" :land_threshold 0.0 1.0]
[slider "Land Brightness" :land_brightness 0.0 10.0]
[slider "NDWI Upper" :ndwi_upper 0.0 1.0]
[slider "NDWI Lower" :ndwi_lower 0.0 1.0]])))]) [slider
Results
We can see the river clearly outlined by the NDWI index. The sliders allow us to dim the land to see the water outline more clearly, and adjust the thresholds to control how the water is displayed.