Skip to content

L1 Conversion

Utilities for converting Sentinel-2 L1C Digital Numbers (DN) to TOA (Top-of-Atmosphere) reflectance, extracting viewing geometry, and resampling bands to a uniform resolution.

Overview

The gee_acolite.utils.l1_convert module:

  • Converts DN values to TOA reflectance (DN / 10000)
  • Extracts solar and sensor viewing angles from image metadata
  • Computes the relative azimuth angle (RAA)
  • Resamples all bands to a common target resolution

Conversion Flow

flowchart TD A["S2 L1C Image
DN values"] --> B[DN_to_rrs] B --> C["Divide by 10000
TOA reflectance 0-1"] C --> D[Extract Angles] D --> D1[Solar Zenith SZA] D --> D2[Solar Azimuth SAA] D --> D3["View Zenith VZA
mean over 13 bands"] D --> D4["View Azimuth VAA
mean over 13 bands"] D1 --> E["Compute RAA
abs(SAA - VAA), clamped 0-180"] D2 --> E D4 --> E E --> F["Add geometry bands
sza, saa, vza, vaa, raa"] F --> G["resample
bilinear to target scale"] G --> H[TOA Image + Geometry] style A fill:#e1f5ff style H fill:#e1ffe1 style E fill:#fef3c7

Functions

l1_to_rrs

l1_to_rrs(images: ImageCollection, scale: int) -> ImageCollection

Convert L1C image collection from DN to TOA reflectance.

Applies DN to reflectance conversion and resamples all bands to the specified spatial resolution.

Parameters:

Name Type Description Default
images ImageCollection

Sentinel-2 L1C image collection with DN values.

required
scale int

Target spatial resolution in meters (10, 20, or 60).

required

Returns:

Type Description
ImageCollection

Collection with TOA reflectance and geometry metadata.

Source code in gee_acolite/utils/l1_convert.py
def l1_to_rrs(images : ee.ImageCollection, scale: int) -> ee.ImageCollection:
    """
    Convert L1C image collection from DN to TOA reflectance.

    Applies DN to reflectance conversion and resamples all bands to
    the specified spatial resolution.

    Parameters
    ----------
    images : ee.ImageCollection
        Sentinel-2 L1C image collection with DN values.
    scale : int
        Target spatial resolution in meters (10, 20, or 60).

    Returns
    -------
    ee.ImageCollection
        Collection with TOA reflectance and geometry metadata.
    """
    resample_scale = partial(resample, band = BAND_BY_SCALE.get(scale, 'B2'))
    return images.select(SENTINEL2_BANDS).map(DN_to_rrs).map(resample_scale)

DN_to_rrs

DN_to_rrs(image: Image) -> Image

Convert L1C image from Digital Numbers to TOA reflectance.

Applies radiometric calibration (DN / 10000) and extracts viewing geometry from metadata (solar and viewing zenith/azimuth angles). Also computes relative azimuth angle.

Parameters:

Name Type Description Default
image Image

Sentinel-2 L1C image with DN values and angle metadata.

required

Returns:

Type Description
Image

Image with TOA reflectance (0-1) and geometry properties: 'sza', 'saa', 'vza', 'vaa', 'raa'.

Source code in gee_acolite/utils/l1_convert.py
def DN_to_rrs(image : ee.Image) -> ee.Image:
    """
    Convert L1C image from Digital Numbers to TOA reflectance.

    Applies radiometric calibration (DN / 10000) and extracts viewing
    geometry from metadata (solar and viewing zenith/azimuth angles).
    Also computes relative azimuth angle.

    Parameters
    ----------
    image : ee.Image
        Sentinel-2 L1C image with DN values and angle metadata.

    Returns
    -------
    ee.Image
        Image with TOA reflectance (0-1) and geometry properties:
        'sza', 'saa', 'vza', 'vaa', 'raa'.
    """
    rrs = image.divide(10_000)

    rrs = rrs.set('sza', image.get('MEAN_SOLAR_ZENITH_ANGLE'))
    rrs = rrs.set('saa', image.get('MEAN_SOLAR_AZIMUTH_ANGLE'))
    rrs = rrs.set('vza', get_mean_band_angle(image, 'ZENITH'))
    rrs = rrs.set('vaa', get_mean_band_angle(image, 'AZIMUTH'))

    raa = ee.Number(rrs.get('saa')).subtract(rrs.get('vaa')).abs()

    raa = ee.Algorithms.If(raa.gt(180), raa.subtract(360).abs(), raa)
    rrs = rrs.set('raa', raa)
    rrs = rrs.set('PRODUCT_ID', ee.String(ee.String(image.get('PRODUCT_ID')).split('L1C').get(0)))

    rrs = rrs.set('system:time_start', image.get('system:time_start'))

    rrs = rrs.copyProperties(image)
    rrs = rrs.set('system:time_start', image.get('system:time_start'))

    return rrs

resample

resample(image: Image, band: str) -> Image

Resample all bands to match a reference band's projection.

Applies bilinear resampling to align all bands to the spatial resolution and projection of the specified reference band.

Parameters:

Name Type Description Default
image Image

Image with multiple bands at different resolutions.

required
band str

Reference band name (e.g., 'B2' for 10m, 'B5' for 20m).

required

Returns:

Type Description
Image

Resampled image with all bands matching reference projection.

Source code in gee_acolite/utils/l1_convert.py
def resample(image : ee.Image, band: str) -> ee.Image:
    """
    Resample all bands to match a reference band's projection.

    Applies bilinear resampling to align all bands to the spatial
    resolution and projection of the specified reference band.

    Parameters
    ----------
    image : ee.Image
        Image with multiple bands at different resolutions.
    band : str
        Reference band name (e.g., 'B2' for 10m, 'B5' for 20m).

    Returns
    -------
    ee.Image
        Resampled image with all bands matching reference projection.
    """
    return image.resample('bilinear').reproject(image.select(band).projection())

Viewing Geometry

The geometry angles are critical inputs to the ACOLITE LUT interpolation:

graph TD A[Viewing Geometry] --> B[Solar Angles] A --> C[Sensor Angles] A --> D[Relative Angle] B --> B1[Solar Zenith SZA] B --> B2[Solar Azimuth SAA] C --> C1[View Zenith VZA] C --> C2[View Azimuth VAA] D --> D1["RAA = |SAA - VAA|, clamped to [0°, 180°]"] style D1 fill:#fef3c7

Extracted Angles

Angle Output band Source metadata property Range
Solar Zenith (SZA) sza MEAN_SOLAR_ZENITH_ANGLE 0°–90°
Solar Azimuth (SAA) saa MEAN_SOLAR_AZIMUTH_ANGLE 0°–360°
View Zenith (VZA) vza Mean of MEAN_INCIDENCE_ZENITH_ANGLE_B* (13 bands) 0°–90°
View Azimuth (VAA) vaa Mean of MEAN_INCIDENCE_AZIMUTH_ANGLE_B* (13 bands) 0°–360°
Relative Azimuth (RAA) raa |SAA - VAA|, clamped to [0°, 180°] 0°–180°

Spatial Resolutions

Sentinel-2 MSI has three native resolutions. All bands are resampled to a single target resolution using the reference band for that scale:

graph LR A[S2 Bands] --> B[10m] A --> C[20m] A --> D[60m] B --> B1["B2, B3, B4, B8\nReference: B2"] C --> C1["B5, B6, B7, B8A, B11, B12\nReference: B5"] D --> D1["B1, B9, B10\nReference: B1"] style B fill:#dcfce7 style C fill:#fef3c7 style D fill:#dbeafe

Full Band Table

Band Name Central λ (nm) Width (nm) Native res. Use
B1 Coastal Aerosol 443 21 60m Aerosol correction
B2 Blue 492 66 10m Ocean, bathymetry
B3 Green 560 36 10m Water quality
B4 Red 665 31 10m SPM, turbidity
B5 Red Edge 1 705 15 20m Chlorophyll
B6 Red Edge 2 740 15 20m Vegetation
B7 Red Edge 3 783 20 20m Vegetation
B8 NIR 842 106 10m Water mask, shadows
B8A Narrow NIR 865 21 20m Water vapour
B9 Water Vapour 945 20 60m Atmospheric correction
B10 Cirrus 1375 31 60m Cirrus detection
B11 SWIR 1 1610 91 20m Water/land mask, glint
B12 SWIR 2 2190 175 20m Glint reference

Usage Examples

Full Collection Conversion

import ee
from gee_acolite.utils.search import search
from gee_acolite.utils.l1_convert import l1_to_rrs

ee.Initialize(project='your-project-id')

roi = ee.Geometry.Rectangle([-0.5, 39.3, -0.1, 39.7])
images_l1c = search(roi, '2023-06-15', '2023-06-16', tile='30SYJ')

# Convert to TOA reflectance at 10m
images_toa = l1_to_rrs(images_l1c, scale=10)

# Inspect output bands
band_names = images_toa.first().bandNames().getInfo()
print(band_names)
# ['B1', 'B2', ..., 'B12', 'sza', 'saa', 'vza', 'vaa', 'raa']

Single Image Conversion

from gee_acolite.utils.l1_convert import DN_to_rrs

image_toa = DN_to_rrs(images_l1c.first())

# Access geometry
sza_mean = image_toa.select('sza').reduceRegion(
    ee.Reducer.mean(), roi, 1000
).getInfo()
print(f'Mean SZA: {sza_mean}')

DN to Reflectance Formula

The conversion for Sentinel-2 L1C is:

\[ \rho_{\text{TOA}} = \frac{DN}{10000} \]

This normalises all band values to the range [0, 1], which is required by the ACOLITE LUT interpolation.

RAA Computation

The relative azimuth angle is computed from solar and sensor azimuths and clamped to [0°, 180°] to match the LUT indexing:

\[ \text{RAA} = |\phi_s - \phi_v|, \quad \text{if RAA} > 180° \Rightarrow \text{RAA} = 360° - \text{RAA} \]

Integration with ACOLITE

sequenceDiagram participant GEE participant L1Convert participant ACOLITE_LUT as ACOLITE LUTs GEE->>L1Convert: ImageCollection L1C (DN) L1Convert->>L1Convert: DN / 10000 L1Convert->>L1Convert: Extract angles (SZA, SAA, VZA, VAA, RAA) L1Convert->>L1Convert: Bilinear resample to target scale L1Convert->>ACOLITE_LUT: TOA + geometry → LUT lookup (SZA, VZA, RAA, tau) ACOLITE_LUT->>ACOLITE_LUT: Interpolate atmospheric parameters ACOLITE_LUT->>GEE: romix, dutott, astot, tg per band

References