By the end of this lesson you will be able to:
gdal (source: Amatulli)
| Data type | Minimum | Maximum | Size1 | Factor |
|---|---|---|---|---|
| Byte | 0 | 255 | 39M | 1x |
| UInt16 | 0 | 65,535 | 78M | 2x |
| Int16, CInt16 | -32,768 | 32,767 | 78M | 2x |
| UInt32 | 0 | 4,294,967,295 | 155M | ~4x |
| Int32, CInt32 | -2,147,483,648 | 2,147,483,647 | 155M | ~4x |
| Float32, CFloat32 | -3.4E38 | 3.4E38 | 155M | ~4x |
| Float64, CFloat64 | -1.79E308 | 1.79E308 | 309M | ~8x |
ByteMin-max normalization maps any range \([\min(x), \max(x)]\) to a target range \([a, b]\):
\[x' = a + \frac{(x-\min(x))\times(b - a)}{\max(x)-\min(x)}\]
For NDVI ∈ [−1, 1] rescaled to [0, 255]:
\[\begin{align} x' &= 0 + \frac{(x+1)\times 255}{2} \\ x' &= \frac{255x+255}{2} \\ x' &= 127.5x+127.5 \\ \end{align}\]
scale_minmax() applies the formula to a numeric vector:scale and offset: \[x = \frac{x' - 127.5}{127.5}\] writeRaster())\[\begin{align} \text{scale} &= \frac{b - a}{\max(x)-\min(x)} \\ \\ \text{offset} &= \frac{a \times \max(x) - b \times \min(x)}{\max(x)-\min(x)} \end{align}\]
Note:
scale offset
127.5 127.5
scale offset
127.5 127.5
scoff_params() works for vectors, but needs two adjustments for rasters:
min()/max() return per-cell values → use minmax() for global min/maxwriteRaster() divides by scale and subtracts offset → both must be invertedLet’s import a raster, calculate the scale and offset values and use these values to write to disk
scale and offset in the file metadataINT1U bytes are invisible — you always get back the original rangeINT1U + DEFLATE can be 8× smaller than uncompressed FLT8STo download Sentinel data:
.zip)IMG_DATA/ — band images at 10m, 20m, 60m resolutionGRANULE/ — scene-level data and annotationsmanifest.safe — general product infoThe SAFE folder above includes the following content:
levelName
1 data-large
2 °--S2B_MSIL2A_20240915T102559_N0511_R108_T32TMS_20240915T131207.SAFE
3 ¦--DATASTRIP
4 ¦ °--DS_2BPS_20240915T131207_S20240915T102803
5 ¦ ¦--MTD_DS.xml
6 ¦ °--QI_DATA
7 ¦ ¦--FORMAT_CORRECTNESS.xml
8 ¦ ¦--GENERAL_QUALITY.xml
9 ¦ ¦--GEOMETRIC_QUALITY.xml
10 ¦ ¦--RADIOMETRIC_QUALITY.xml
11 ¦ °--SENSOR_QUALITY.xml
12 ¦--GRANULE
13 ¦ °--L2A_T32TMS_A039316_20240915T102803
14 ¦ ¦--AUX_DATA
15 ¦ ¦ ¦--AUX_CAMSFO
16 ¦ ¦ °--AUX_ECMWFT
17 ¦ ¦--IMG_DATA
18 ¦ ¦ ¦--R10m
19 ¦ ¦ ¦ ¦--T32TMS_20240915T102559_AOT_10m.jp2
20 ¦ ¦ ¦ ¦--T32TMS_20240915T102559_B02_10m.jp2
21 ¦ ¦ ¦ °--... (etc)
22 ¦ ¦ ¦--R20m
23 ¦ ¦ ¦ ¦--T32TMS_20240915T102559_AOT_20m.jp2
24 ¦ ¦ ¦ ¦--T32TMS_20240915T102559_B01_20m.jp2
25 ¦ ¦ ¦ °--... (etc)
26 ¦ ¦ °--R60m
27 ¦ ¦ ¦--T32TMS_20240915T102559_AOT_60m.jp2
28 ¦ ¦ ¦--T32TMS_20240915T102559_B01_60m.jp2
29 ¦ ¦ °--... (etc)
30 ¦ ¦--MTD_TL.xml
31 ¦ °--QI_DATA
32 ¦ ¦--FORMAT_CORRECTNESS.xml
33 ¦ ¦--GENERAL_QUALITY.xml
34 ¦ ¦--GEOMETRIC_QUALITY.xml
35 ¦ ¦--L2A_QUALITY.xml
36 ¦ ¦--MSK_CLASSI_B00.jp2
37 ¦ ¦--MSK_CLDPRB_20m.jp2
38 ¦ ¦--MSK_CLDPRB_60m.jp2
39 ¦ ¦--MSK_DETFOO_B01.jp2
40 ¦ ¦--MSK_DETFOO_B02.jp2
41 ¦ ¦--... (etc)
42 ¦ ¦--MSK_QUALIT_B01.jp2
43 ¦ ¦--MSK_QUALIT_B02.jp2
44 ¦ ¦--MSK_SNWPRB_20m.jp2
45 ¦ ¦--MSK_SNWPRB_60m.jp2
46 ¦ ¦--SENSOR_QUALITY.xml
47 ¦ °--T32TMS_20240915T102559_PVI.jp2
48 ¦--HTML
49 ¦ ¦--banner_1.png
50 ¦ ¦--banner_2.png
51 ¦ ¦--banner_3.png
52 ¦ ¦--star_bg.jpg
53 ¦ ¦--UserProduct_index.html
54 ¦ °--UserProduct_index.xsl
55 ¦--INSPIRE.xml
56 ¦--manifest.safe
57 ¦--MTD_MSIL2A.xml
58 ¦--rep_info
59 ¦ ¦--S2_PDI_Level-2A_Datastrip_Metadata.xsd
60 ¦ ¦--S2_PDI_Level-2A_Tile_Metadata.xsd
61 ¦ °--S2_User_Product_Level-2A_Metadata.xsd
62 °--S2B_MSIL2A_20240915T102559_N0511_R108_T32TMS_20240915T131207-ql.jpg
The bands are in IMG_DATA/ — one JP2 per band, at 10m, 20m, and 60m.
B01–B12)str_split_fixed() [1] "B01" "B02" "B03" "B04" "B05" "B06" "B07" "B09" "B11" "B12"
MTD_MSIL2A.xml:\[DN = \rho \times Q - O\]
where \(Q\) = quantification value and \(O\) = radiometric add offset — let’s extract them:
Searching for QUANTIFICATION_VALUE in SAFE/MTD_MSIL2A.xml:
<QUANTIFICATION_VALUES_LIST>
<BOA_QUANTIFICATION_VALUE unit="none">10000</BOA_QUANTIFICATION_VALUE>
<AOT_QUANTIFICATION_VALUE unit="none">1000.0</AOT_QUANTIFICATION_VALUE>
<WVP_QUANTIFICATION_VALUE unit="cm">1000.0</WVP_QUANTIFICATION_VALUE>
</QUANTIFICATION_VALUES_LIST>
Searching for ADD_OFFSET in SAFE/MTD_MSIL2A.xml:
<BOA_ADD_OFFSET_VALUES_LIST>
<BOA_ADD_OFFSET band_id="0">-1000</BOA_ADD_OFFSET>
<BOA_ADD_OFFSET band_id="1">-1000</BOA_ADD_OFFSET>
<BOA_ADD_OFFSET band_id="2">-1000</BOA_ADD_OFFSET>
<BOA_ADD_OFFSET band_id="3">-1000</BOA_ADD_OFFSET>
<BOA_ADD_OFFSET band_id="4">-1000</BOA_ADD_OFFSET>
<BOA_ADD_OFFSET band_id="5">-1000</BOA_ADD_OFFSET>
<BOA_ADD_OFFSET band_id="6">-1000</BOA_ADD_OFFSET>
<BOA_ADD_OFFSET band_id="7">-1000</BOA_ADD_OFFSET>
<BOA_ADD_OFFSET band_id="8">-1000</BOA_ADD_OFFSET>
<BOA_ADD_OFFSET band_id="9">-1000</BOA_ADD_OFFSET>
<BOA_ADD_OFFSET band_id="10">-1000</BOA_ADD_OFFSET>
<BOA_ADD_OFFSET band_id="11">-1000</BOA_ADD_OFFSET>
<BOA_ADD_OFFSET band_id="12">-1000</BOA_ADD_OFFSET>
</BOA_ADD_OFFSET_VALUES_LIST>
To calculate NDVI, we can create our custom function and apply it to the s2b object using app:
| Value | Class | Keep? |
|---|---|---|
| 0 | No Data | ✗ |
| 1 | Saturated / Defective | ✗ |
| 2 | Dark Area Pixels | ✗ |
| 3 | Cloud Shadows | ✗ |
| 4 | Vegetation | ✓ |
| 5 | Bare Soils | ✓ |
| 6 | Water | ✓ |
| 7 | Unclassified | ~ |
| 8 | Cloud, Medium Probability | ✗ |
| 9 | Cloud, High Probability | ✗ |
| 10 | Thin Cirrus | ✗ |
| 11 | Snow or Ice | ✓ |
R20m resolution folder (not R10m)# define which SCL classes are valid (cloud-free surface)
valid_classes <- c(4, 5, 6, 11)
# create a binary mask: TRUE where pixel is valid
cloud_mask <- scl %in% valid_classes
# apply to the reflectance stack
s2_masked <- mask(s2b, cloud_mask, maskvalues = FALSE)
# clip residual out-of-range values after masking
force_minmax <- \(x, min = 0, max = 1){ x[x < min] <- min; x[x > max] <- max; x }
s2_masked <- app(s2_masked, force_minmax)%in% tests membership cell-by-cell and returns a Boolean rastermask(..., maskvalues = FALSE) sets cells to NA wherever the mask is FALSE (i.e. cloudy)

Valid pixels: 74.6%