Skip to content

DTACSNet: onboard cloud detection and atmospheric correction with end-to-end deep learning emulators

This repo contains an open implementation to run inference with DTACSNet models for atmospheric correction and also for cloud detection. These two models are independent (you could run one or the other or both). The trained models provided here are customized to the band configuration that will be available in Phi-Sat-II (excluding PAN band). Trained models are released under a Creative Commons non-commercial licence licence.

Phi-Sat-II simulated image run

In this notebook we run the DTACSNet model over a simulated Phi-Sat-II image. To run this tutorial we downloaded this image from here and save it in the tutorials/examples folder.

# download the image
import os
if not os.path.exists("examples/phisat2-l1c.tiff"):
    !wget -P examples/ https://cloud.sinergise.com/s/RSgHGwWWCxYp5yq/download/phisat2-l1c.tiff 
import sys
sys.path.append("..")

Load data

Load the simulated Phi-Sat-II image. We multiply the values by 10,000 to follow the same convention as in Sentinel-2.

import rasterio
import rasterio.windows
import os


phisat2_bands = ["B2","B3","B4","B5","B6","B7","B8"]

PHISAT2_PAN_BANDS = ["B2", "B3", "B4", "PAN", "B8", "B5", "B6", "B7"]

folder_examples = "examples"

with rasterio.open(os.path.join(folder_examples,"phisat2-l1c.tiff")) as rst:
    indexes_read = [PHISAT2_PAN_BANDS.index(b) + 1 for b in phisat2_bands]
    data = rst.read(indexes_read).astype("float32") * 10_000 # Convert to the same units as S2 TOA images
    transform_phisat2 = rst.transform
data.shape
(7, 4096, 4096)
# Data are TOA reflectance values. We multiply them by 10_000 to use the same convention as in Sentinel-2
data
array([[[1322.5994 , 1320.0856 , 1320.9397 , ..., 1042.8582 ,
         1037.9543 , 1032.7795 ],
        [1317.1211 , 1313.7217 , 1314.116  , ..., 1028.6802 ,
         1026.053  , 1023.8384 ],
        [1315.6958 , 1308.3627 , 1306.545  , ..., 1015.23615,
         1009.0241 , 1009.10614],
        ...,
        [1430.4143 , 1428.8866 , 1416.1835 , ...,  934.9696 ,
          944.6366 ,  950.5102 ],
        [1429.8018 , 1424.3774 , 1407.938  , ...,  930.64264,
          938.4229 ,  944.8581 ],
        [1426.832  , 1418.0771 , 1400.1425 , ...,  929.38654,
          931.2642 ,  934.7899 ]],

       [[1360.229  , 1338.267  , 1292.3629 , ..., 1023.04486,
         1010.3757 , 1008.6486 ],
        [1342.062  , 1317.6985 , 1277.8824 , ..., 1020.6479 ,
         1010.9121 , 1009.59503],
        [1310.8638 , 1293.7273 , 1272.6669 , ..., 1016.228  ,
         1011.0665 , 1011.6896 ],
        ...,
        [1514.7881 , 1492.5245 , 1452.1195 , ...,  897.7126 ,
          913.43396,  912.3569 ],
        [1509.8177 , 1485.543  , 1442.6339 , ...,  883.9485 ,
          905.8627 ,  911.69574],
        [1509.6626 , 1485.7672 , 1442.7484 , ...,  874.86005,
          901.2488 ,  911.895  ]],

       [[1033.3932 , 1028.7035 , 1018.2965 , ..., 1227.7335 ,
         1235.5188 , 1255.2526 ],
        [1027.109  , 1022.1214 , 1013.8055 , ..., 1185.7994 ,
         1200.9028 , 1223.2795 ],
        [1011.16583, 1005.5013 , 1001.6236 , ..., 1106.2363 ,
         1116.8206 , 1140.863  ],
        ...,
        [1600.7773 , 1567.0034 , 1523.0471 , ...,  699.65967,
          693.79926,  693.8701 ],
        [1607.5692 , 1567.5837 , 1500.4966 , ...,  686.4775 ,
          708.05176,  718.30365],
        [1608.4591 , 1571.6168 , 1502.3864 , ...,  668.43634,
          709.58386,  731.99615]],

       ...,

       [[2865.501  , 2877.168  , 2896.6174 , ..., 2397.5806 ,
         2408.7175 , 2411.5754 ],
        [2910.6616 , 2917.7883 , 2925.9536 , ..., 2381.6914 ,
         2383.2058 , 2386.7314 ],
        [2997.9626 , 3000.0742 , 2994.6182 , ..., 2353.371  ,
         2357.1824 , 2363.8162 ],
        ...,
        [2795.5955 , 2786.0886 , 2780.7942 , ..., 2003.6438 ,
         1992.5857 , 1997.2983 ],
        [2804.8813 , 2780.6316 , 2752.8333 , ..., 2009.3411 ,
         2005.3824 , 2006.0068 ],
        [2808.557  , 2778.3118 , 2741.2388 , ..., 2016.016  ,
         2008.3715 , 2005.9791 ]],

       [[3600.7786 , 3600.3042 , 3614.4646 , ..., 2733.0044 ,
         2729.215  , 2717.5703 ],
        [3598.449  , 3604.9832 , 3630.5503 , ..., 2722.8826 ,
         2713.4092 , 2701.8696 ],
        [3590.9646 , 3616.0625 , 3667.7424 , ..., 2717.4773 ,
         2701.5737 , 2690.5364 ],
        ...,
        [3108.8032 , 3106.8699 , 3092.6897 , ..., 2463.1504 ,
         2452.1025 , 2444.5637 ],
        [3095.445  , 3088.4348 , 3079.641  , ..., 2460.2698 ,
         2433.2341 , 2427.4868 ],
        [3095.9392 , 3087.4583 , 3077.9648 , ..., 2463.0059 ,
         2429.3193 , 2424.994  ]],

       [[3584.3557 , 3568.8684 , 3505.664  , ..., 2548.6487 ,
         2534.796  , 2524.942  ],
        [3550.0635 , 3541.381  , 3492.8608 , ..., 2564.5261 ,
         2550.476  , 2540.0837 ],
        [3487.404  , 3492.49   , 3475.8538 , ..., 2591.5137 ,
         2576.6697 , 2572.516  ],
        ...,
        [3025.2507 , 3023.0496 , 2995.3074 , ..., 2291.15   ,
         2298.512  , 2302.8228 ],
        [3026.4873 , 3015.5457 , 2987.4424 , ..., 2319.7998 ,
         2353.5754 , 2362.2373 ],
        [3017.9026 , 3004.0955 , 2981.346  , ..., 2350.9143 ,
         2395.339  , 2406.5127 ]]], dtype=float32)

Atmospheric correction model

Load model

from dtacs.model_wrapper import ACModel
import torch

model_atmospheric_correction = ACModel(model_name="CNN_corrector_phisat2")
model_atmospheric_correction.load_weights()
/home/gonzalo/miniconda3/envs/starcop/lib/python3.9/site-packages/tqdm/auto.py:22: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Run inference

%%time
ac_output = model_atmospheric_correction.predict(data)
ac_output
CPU times: user 5.37 s, sys: 3.78 s, total: 9.15 s
Wall time: 2.73 s

array([[[ 917,  909,  897, ...,  472,  463,  455],
        [ 905,  897,  885, ...,  456,  450,  446],
        [ 896,  883,  872, ...,  442,  433,  432],
        ...,
        [1029, 1024, 1002, ...,  330,  346,  354],
        [1028, 1018,  991, ...,  327,  343,  353],
        [1025, 1010,  982, ...,  327,  338,  344]],

       [[1288, 1261, 1201, ...,  796,  779,  776],
        [1262, 1232, 1181, ...,  790,  778,  775],
        [1216, 1194, 1168, ...,  781,  773,  773],
        ...,
        [1457, 1429, 1377, ...,  621,  642,  642],
        [1450, 1420, 1364, ...,  606,  636,  645],
        [1450, 1420, 1365, ...,  597,  632,  647]],

       [[ 990,  982,  966, ..., 1127, 1133, 1153],
        [ 975,  968,  956, ..., 1082, 1096, 1120],
        [ 945,  939,  935, ...,  998, 1007, 1033],
        ...,
        [1618, 1582, 1530, ...,  543,  539,  540],
        [1625, 1582, 1505, ...,  527,  554,  567],
        [1623, 1583, 1506, ...,  507,  555,  581]],

       ...,

       [[3185, 3197, 3223, ..., 2563, 2573, 2572],
        [3231, 3240, 3255, ..., 2543, 2542, 2543],
        [3320, 3325, 3329, ..., 2512, 2512, 2515],
        ...,
        [3027, 3019, 3013, ..., 2148, 2137, 2141],
        [3034, 3010, 2984, ..., 2154, 2144, 2143],
        [3038, 3008, 2972, ..., 2161, 2144, 2139]],

       [[3825, 3825, 3845, ..., 2825, 2820, 2804],
        [3823, 3831, 3863, ..., 2813, 2802, 2787],
        [3818, 3845, 3904, ..., 2810, 2790, 2776],
        ...,
        [3257, 3258, 3245, ..., 2558, 2547, 2540],
        [3242, 3237, 3232, ..., 2555, 2523, 2517],
        [3242, 3235, 3229, ..., 2559, 2516, 2510]],

       [[4073, 4052, 3978, ..., 2771, 2751, 2735],
        [4021, 4011, 3958, ..., 2789, 2770, 2753],
        [3928, 3936, 3926, ..., 2823, 2801, 2791],
        ...,
        [3349, 3349, 3316, ..., 2517, 2529, 2533],
        [3346, 3338, 3310, ..., 2550, 2586, 2596],
        [3335, 3324, 3305, ..., 2586, 2633, 2645]]], dtype=uint16)

Plot

import matplotlib.pyplot as plt
from dtacs import plot
import rasterio.plot as rstplt

fig, ax = plt.subplots(2,2,figsize=(12,12),tight_layout=True)

rstplt.show(data[2::-1,...]/4_000,ax=ax[0,0])
ax[0,0].set_title("RGB PhiSat-II L1C")
rstplt.show(ac_output[2::-1,...]/4_000,ax=ax[0,1])
ax[0,1].set_title("RGB PhiSat-II L2A (DTACSNet corrected)")

nirredgreen = [-1, 2, 1]
rstplt.show(data[nirredgreen,...]/10_000,ax=ax[1,0])
ax[1,0].set_title("NIRRG PhiSat-II L1C")
rstplt.show(ac_output[nirredgreen,...]/10_000,ax=ax[1,1])
ax[1,1].set_title("NIRRG PhiSat-II L2A (DTACSNet corrected)")
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).

Text(0.5, 1.0, 'NIRRG PhiSat-II L2A (DTACSNet corrected)')
No description has been provided for this image

Compare L2A Phi-Sat-II image with L2A Sentinel-2 image

The simulated Phi-Sat-II image that we have shown below has been created from the Sentinel-2 tile: S2A_MSIL1C_20191111T132241_N0208_R038_T23LLK_20191111T145626. We have copied a 500x500 subset of this tile in the tutorials/examples folder. We have also downloaded the corresponding L2A Sentinel-2 file to compare the atmospheric correction output of the two products.

# Load Sentinel-2 L2A 
with rasterio.open(os.path.join(folder_examples,"S2L2A.tif")) as rst:
    indexes_read = [rst.descriptions.index(b) + 1 for b in phisat2_bands]
    data_l2a = rst.read(indexes_read)

# Load Sentinel-2 L1C 
with rasterio.open(os.path.join(folder_examples,"S2L1C.tif")) as rst:
    indexes_read = [rst.descriptions.index(b) + 1 for b in phisat2_bands]
    data_l1c = rst.read(indexes_read)
    bounds = rst.bounds

window_in = rasterio.windows.from_bounds(*bounds, transform=transform_phisat2)
slice_read = (slice(None),) + window_in.toslices()
fig, ax = plt.subplots(2,4,figsize=(24,12),tight_layout=True)

rstplt.show(data[2::-1,...][slice_read]/4_000,ax=ax[0,0])
ax[0,0].set_title("RGB PhiSat-II L1C")
rstplt.show(ac_output[2::-1,...][slice_read]/4_000,ax=ax[0,1])
ax[0,1].set_title("RGB PhiSat-II L2A (DTACSNet corrected)")

rstplt.show(data_l1c[2::-1,...]/4_000,ax=ax[0,2])
ax[0,2].set_title("RGB Sentinel-2 L1C")
rstplt.show(data_l2a[2::-1,...]/4_000,ax=ax[0,3])
ax[0,3].set_title("RGB Sentinel-2 L2A")

nirredgreen = [-1, 2, 1]
rstplt.show(data[nirredgreen,...][slice_read]/10_000,ax=ax[1,0])
ax[1,0].set_title("NIRRG PhiSat-II L1C")
rstplt.show(ac_output[nirredgreen,...][slice_read]/10_000,ax=ax[1,1])
ax[1,1].set_title("NIRRG PhiSat-II L2A (DTACSNet corrected)")

rstplt.show(data_l1c[nirredgreen,...]/10_000,ax=ax[1,2])
ax[1,2].set_title("NIRRG PhiSat-II L1C")
rstplt.show(data_l2a[nirredgreen,...]/10_000,ax=ax[1,3])
ax[1,3].set_title("NIRRG Sentinel-2 L2A")
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).

Text(0.5, 1.0, 'NIRRG Sentinel-2 L2A')
No description has been provided for this image