Générer des séries chronologiques animées à l’aide de « xr_animation »
Produits utilisés : s2_l2a
Mots clés données utilisées ; sentinel-2, méthodes de données ; animation, index de bande ; NDWI
Aperçu
Les animations peuvent être une méthode puissante pour visualiser l’évolution du paysage au fil du temps à l’aide d’images satellite. Les données satellite de Digital Earth Africa sont un sujet idéal pour les animations car elles ont été géoréférencées, traitées pour obtenir une réflectance de surface prête à être analysée et empilées dans un « cube de données » spatio-temporel, ce qui permet d’extraire et de visualiser les conditions du paysage de manière cohérente au fil du temps.
En utilisant les fonctions DE Africa dans « Scripts/deafrica_plotting », qui sont basées sur « matplotlib.animation » et « xarray », nous pouvons prendre une série chronologique d’images satellite Digital Earth Africa et exporter une animation de série chronologique visuellement attrayante qui montre comment n’importe quel endroit en Afrique a changé.
Description
Ce cahier montre comment :
Importer une série chronologique d’images satellite sans nuages provenant de plusieurs satellites (par exemple Sentinel-2A et -2B) sous forme d’un ensemble de données xarray
Tracez les données sous forme d’animation de séries chronologiques à trois bandes
Tracer les données sous forme d’animation de série chronologique à une bande
Exportez les animations résultantes sous forme de fichier GIF ou MP4
Ajouter des superpositions vectorielles personnalisées
Appliquer des fonctions de traitement d’image personnalisées à chaque image d’animation
Commencer
Pour exécuter cette analyse, exécutez toutes les cellules du bloc-notes, en commençant par la cellule « Charger les packages ».
Charger des paquets
[1]:
%matplotlib inline
# Force GeoPandas to use Shapely instead of PyGEOS
# In a future release, GeoPandas will switch to using Shapely by default.
import os
os.environ['USE_PYGEOS'] = '0'
import datacube
import skimage.exposure
import geopandas as gpd
import matplotlib.pyplot as plt
from IPython.display import Image
from datacube.utils.geometry import Geometry
from deafrica_tools.plotting import xr_animation, rgb
from deafrica_tools.datahandling import load_ard
from deafrica_tools.areaofinterest import define_area
Se connecter au datacube
[2]:
dc = datacube.Datacube(app='Animated_timeseries')
Sélectionnez l’emplacement
Pour définir la zone d’intérêt, deux méthodes sont disponibles :
En spécifiant la latitude, la longitude et la zone tampon. Cette méthode nécessite que vous saisissiez la latitude centrale, la longitude centrale et la valeur de la zone tampon en degrés carrés autour du point central que vous souhaitez analyser. Par exemple, « lat = 10,338 », « lon = -1,055 » et « buffer = 0,1 » sélectionneront une zone avec un rayon de 0,1 degré carré autour du point avec les coordonnées (10,338, -1,055).
By uploading a polygon as a
GeoJSON or Esri Shapefile
. If you choose this option, you will need to upload the geojson or ESRI shapefile into the Sandbox using Upload Files button in the top left corner of the Jupyter Notebook interface. ESRI shapefiles must be uploaded with all the related files(.cpg, .dbf, .shp, .shx)
. Once uploaded, you can use the shapefile or geojson to define the area of interest. Remember to update the code to call the file you have uploaded.
Pour utiliser l’une de ces méthodes, vous pouvez décommenter la ligne de code concernée et commenter l’autre. Pour commenter une ligne, ajoutez le symbole "#"
avant le code que vous souhaitez commenter. Par défaut, la première option qui définit l’emplacement à l’aide de la latitude, de la longitude et du tampon est utilisée.
Si vous exécutez le bloc-notes pour la première fois, conservez les paramètres par défaut ci-dessous. Cela permettra de démontrer le fonctionnement de l’analyse et de fournir des résultats significatifs.
[3]:
# Method 1: Specify the latitude, longitude, and buffer
aoi = define_area(lat=8.57, lon=-2.4, buffer=0.05)
# Method 2: Use a polygon as a GeoJSON or Esri Shapefile.
# aoi = define_area(vector_path='aoi.shp')
#Create a geopolygon and geodataframe of the area of interest
geopolygon = Geometry(aoi["features"][0]["geometry"], crs="epsg:4326")
geopolygon_gdf = gpd.GeoDataFrame(geometry=[geopolygon], crs=geopolygon.crs)
# Get the latitude and longitude range of the geopolygon
lat_range = (geopolygon_gdf.total_bounds[1], geopolygon_gdf.total_bounds[3])
lon_range = (geopolygon_gdf.total_bounds[0], geopolygon_gdf.total_bounds[2])
Charger des données satellite à partir de Datacube
Nous pouvons utiliser la fonction load_ard pour charger des données à partir de plusieurs satellites (c’est-à-dire Sentinel-2A et -2B) et renvoyer un seul xarray.Dataset
contenant uniquement des observations avec un pourcentage minimum de pixels de bonne qualité. Cela nous permettra de créer une animation de séries chronologiques visuellement attrayante d’observations qui ne sont pas affectées par les nuages.
Dans l’exemple ci-dessous, nous demandons que la fonction renvoie uniquement les observations qui sont à 95 % exemptes de nuages et d’autres pixels de mauvaise qualité en spécifiant « min_gooddata=0.95 ».
[4]:
# Create a reusable query
query = {
'x': lon_range,
'y': lat_range,
'time': ('2017-10-10', '2018-01-10'),
'resolution': (-20, 20)
}
[5]:
# Load available data
ds = load_ard(dc=dc,
products=['s2_l2a'],
measurements=['red', 'green', 'blue', 'nir_1', 'swir_1', 'swir_2'],
group_by='solar_day',
output_crs='epsg:32630',
min_gooddata=0.95,
mask_pixel_quality=True,
**query)
# Print output data
print(ds)
Using pixel quality parameters for Sentinel 2
Finding datasets
s2_l2a
Counting good quality pixels for each time step
Filtering to 13 out of 19 time steps with at least 95.0% good quality pixels
Applying pixel quality/cloud mask
Loading 13 time steps
<xarray.Dataset>
Dimensions: (time: 13, y: 555, x: 552)
Coordinates:
* time (time) datetime64[ns] 2017-10-17T10:40:55 ... 2018-01-10T10:...
* y (y) float64 9.529e+05 9.529e+05 ... 9.418e+05 9.418e+05
* x (x) float64 5.605e+05 5.605e+05 ... 5.715e+05 5.715e+05
spatial_ref int32 32630
Data variables:
red (time, y, x) float32 440.0 447.0 465.0 ... 526.0 537.0 557.0
green (time, y, x) float32 677.0 682.0 690.0 ... 683.0 679.0 668.0
blue (time, y, x) float32 250.0 274.0 285.0 ... 547.0 548.0 540.0
nir_1 (time, y, x) float32 3.098e+03 3.123e+03 ... 446.0 626.0
swir_1 (time, y, x) float32 1.671e+03 1.696e+03 ... 399.0 442.0
swir_2 (time, y, x) float32 783.0 775.0 773.0 ... 307.0 311.0 340.0
Attributes:
crs: epsg:32630
grid_mapping: spatial_ref
Pour avoir une idée rapide de l’apparence des données, nous pouvons tracer l’ensemble de données en vraies couleurs à l’aide de la fonction « rgb » :
[6]:
# Plot images from the dataset
rgb(ds, index=list(range(0, 8)))
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
Tracer une série chronologique sous forme de GIF animé RVB/trois bandes
La fonction « xr_animation() » est basée sur la fonctionnalité de « matplotlib.animation ». Elle prend un « xarray.Dataset » et exporte une animation GIF ou MP4 à une ou trois bandes (par exemple, couleur vraie ou fausse) montrant les changements du paysage au fil du temps.
[7]:
# Produce time series animation of red, green and blue bands
xr_animation(ds=ds,
bands=['red', 'green', 'blue'],
output_path='animated_timeseries.gif',
interval=200,
width_pixels=300)
# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')
Exporting animation to animated_timeseries.gif
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
[7]:
<IPython.core.display.Image object>
Nous pouvons également utiliser différentes combinaisons de bandes (par exemple, de fausses couleurs) en utilisant « bands », ajouter du texte supplémentaire en utilisant « show_text » et modifier la taille de la police en utilisant « annotation_kwargs », qui transmet un dictionnaire de valeurs à la fonction matplotlib « plt.annotate » (voir « matplotlib.pyplot.annotate <https://matplotlib.org/api/_as_gen/matplotlib.pyplot.annotate.html> »__ pour les options).
La fonction sélectionnera automatiquement un étirement de couleur approprié en coupant les données pour supprimer les valeurs aberrantes/extrêmes inférieures ou supérieures aux 2e et 98e percentiles (par exemple, de manière similaire au paramètre « robust=True » de xarray). Cela peut être contrôlé plus en détail avec le paramètre « percentile_stretch ». Par exemple, le réglage « percentile_stretch=(0.01, 0.99) » appliquera un étirement de couleur avec moins de contraste :
[8]:
# Produce time series animation of red, green and blue bands
xr_animation(ds=ds,
bands=['swir_2', 'nir_1', 'green'],
output_path='animated_timeseries.gif',
width_pixels=300,
show_text='Time-series animation',
percentile_stretch=(0.01, 0.99),
annotation_kwargs={'fontsize': 25})
# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')
Exporting animation to animated_timeseries.gif
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
[8]:
<IPython.core.display.Image object>
Traçage d’animations à bande unique
Il est également possible de tracer une image à bande unique au lieu d’une image à trois bandes. Par exemple, nous pourrions tracer un indice comme l’indice d’eau par différence normalisée (NDWI), qui présente des valeurs élevées là où un pixel est susceptible d’être en eau libre (par exemple, NDWI > 0).
Par défaut, les limites de la barre de couleur sont définies sur la base de « percentile_stretch » qui éliminera les valeurs aberrantes/extrêmes pour optimiser l’étirement des couleurs (définissez « percentile_stretch=(0.0, 1.00) » pour afficher la plage complète de valeurs du min au max).
[9]:
# Compute NDWI using the formula (green - nir) / (green + nir).
# This will calculate NDWI for every time-step in the dataset:
ds['NDWI'] = ((ds.green - ds.nir_1) /
(ds.green + ds.nir_1))
# Produce time series animation of NDWI:
xr_animation(ds=ds,
output_path='animated_timeseries.gif',
bands=['NDWI'],
show_text='NDWI',
width_pixels=300)
# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')
Exporting animation to animated_timeseries.gif
[9]:
<IPython.core.display.Image object>
Nous pouvons personnaliser les animations basées sur une seule bande comme NDWI en spécifiant des paramètres à l’aide de imshow_kwargs
, qui est passé à la fonction matplotlib plt.imshow (voir le lien pour les options). Par exemple, nous pouvons utiliser un jeu de couleurs bleu plus approprié avec 'cmap': 'Blues'
, et définir 'vmin': 0.0, 'vmax': 0.5
pour remplacer les limites de la barre de couleurs par défaut par des valeurs spécifiées manuellement :
[10]:
# Produce time series animation using a custom colour scheme and limits:
xr_animation(ds=ds,
bands='NDWI',
output_path='animated_timeseries.gif',
width_pixels=300,
show_text='NDWI',
imshow_kwargs={'cmap': 'Blues', 'vmin': 0.0, 'vmax': 0.5},
colorbar_kwargs={'colors': 'black'})
# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')
Exporting animation to animated_timeseries.gif
[10]:
<IPython.core.display.Image object>
Les animations à une bande affichent une barre de couleur par défaut, mais cela peut être désactivé à l’aide de « show_colorbar » :
[11]:
# Produce time series animation using a custom colour scheme and limits:
xr_animation(ds=ds,
bands='NDWI',
output_path='animated_timeseries.gif',
width_pixels=300,
show_text='NDWI',
show_colorbar=False,
imshow_kwargs={'cmap': 'Blues', 'vmin': 0.0, 'vmax': 0.5})
# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')
Exporting animation to animated_timeseries.gif
[11]:
<IPython.core.display.Image object>
Les animations à une bande affichent une barre de couleur par défaut, mais cela peut être désactivé :
Formats de sortie disponibles
Les exemples ci-dessus se sont concentrés sur l’exportation de GIF animés, mais des fichiers MP4 peuvent également être générés. Les deux formats ont leurs propres avantages et inconvénients :
« .mp4 » : rapide à générer, taille de fichier la plus petite et qualité la plus élevée ; adapté à Twitter/aux médias sociaux et aux versions récentes de Powerpoint
« .gif » : lent à générer, fichiers volumineux, faible qualité de rendu ; convient à toutes les versions de Powerpoint et de Twitter/réseaux sociaux
Remarque : pour prévisualiser un fichier « .mp4 » depuis JupyterLab, recherchez le fichier (par exemple, « animated_timeseries.mp4 ») dans le navigateur de fichiers sur la gauche, faites un clic droit et sélectionnez « Ouvrir dans un nouvel onglet du navigateur ».
[12]:
# Animate datasets as a MP4 file
xr_animation(ds=ds,
bands=['red', 'green', 'blue'],
output_path='animated_timeseries.mp4')
Exporting animation to animated_timeseries.mp4
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
Ajout de superpositions vectorielles
Le code d’animation prend en charge le traçage de fichiers vectoriels (par exemple, ESRI Shapefiles ou GeoJSON) sur des images satellite. Pour ce faire, nous chargeons d’abord le fichier à l’aide de geopandas, puis le transmettons à « xr_animation » à l’aide du paramètre « show_gdf » :
[13]:
# Get shapefile path
poly_gdf = gpd.read_file('../Supplementary_data/Animated_timeseries/example_polygon.shp')
# Produce time series animation
xr_animation(ds=ds,
bands=['red', 'green', 'blue'],
output_path='animated_timeseries.gif',
width_pixels=300,
show_gdf=poly_gdf)
# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')
Exporting animation to animated_timeseries.gif
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
[13]:
<IPython.core.display.Image object>
Vous pouvez personnaliser le style des superpositions vectorielles en incluant une colonne appelée « couleur » dans l’objet « geopandas.GeoDataFrame » :
[14]:
# Assign a colour to the GeoDataFrame
poly_gdf['color'] = 'red'
# Produce time series animation
xr_animation(ds=ds,
bands=['red', 'green', 'blue'],
output_path='animated_timeseries.gif',
width_pixels=300,
show_gdf=poly_gdf)
# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')
Exporting animation to animated_timeseries.gif
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
[14]:
<IPython.core.display.Image object>
Tracé de vecteurs en fonction du temps
Il peut être utile de tracer des entités vectorielles au-dessus des images à des moments précis d’une animation. Par exemple, dans cet exemple, nous pouvons vouloir tracer différents vecteurs en fonction du niveau de remplissage de notre plan d’eau pendant notre période d’analyse.
Pour ce faire, nous pouvons créer de nouvelles colonnes « start_time » et « end_time » dans notre « geopandas.GeoDataFrame » qui indiquent à « xr_animation » combien de temps tracer chaque entité au-dessus de notre imagerie :
Remarque : les dates peuvent être fournies dans n’importe quel format de chaîne pouvant être converti à l’aide de la fonction pandas.to_datetime(). Par exemple, « 2009 », « 2009-11 », « 2009-11-01 », etc.
[15]:
# Assign start and end times to each feature
poly_gdf['start_time'] = ['2017-09', '2017-11', '2017-12', '2018-01']
poly_gdf['end_time'] = ['2017-11', '2017-12', '2018-01', '2018-02']
# Preview the updated geopandas.GeoDataFrame
poly_gdf
[15]:
FID | geometry | color | start_time | end_time | |
---|---|---|---|---|---|
0 | 0 | MULTIPOLYGON (((565920.000 946110.000, 565950.... | red | 2017-09 | 2017-11 |
1 | 1 | MULTIPOLYGON (((567300.000 945180.000, 567300.... | red | 2017-11 | 2017-12 |
2 | 2 | MULTIPOLYGON (((568830.000 944130.000, 568800.... | red | 2017-12 | 2018-01 |
3 | 3 | MULTIPOLYGON (((568830.000 944130.000, 568800.... | red | 2018-01 | 2018-02 |
Nous pouvons maintenant passer le fichier « geopandas.GeoDataFram » mis à jour à « xr_animation ». Nous allons également changer les couleurs de nos vecteurs en un bleu semi-transparent et leur donner un contour bleu en utilisant le paramètre « gdf_kwargs » :
[16]:
# Set vector features to a semi-transperant blue to represent water
poly_gdf['color'] = '#0066ff90'
# Produce time series animation
xr_animation(ds=ds,
bands=['red', 'green', 'blue'],
output_path='animated_timeseries.gif',
width_pixels=300,
show_gdf=poly_gdf,
gdf_kwargs={'edgecolor': 'blue'})
# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')
Exporting animation to animated_timeseries.gif
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
/usr/local/lib/python3.10/dist-packages/matplotlib/cm.py:478: RuntimeWarning: invalid value encountered in cast
xx = (xx * 255).astype(np.uint8)
[16]:
<IPython.core.display.Image object>
Fonctions de traitement d’image personnalisées
Le paramètre image_proc_funcs vous permet de transmettre des fonctions de traitement d’image personnalisées qui seront appliquées à chaque image de votre animation au fur et à mesure de leur rendu. Cela peut être un moyen puissant de produire des animations visuellement attrayantes. Voici quelques exemples d’applications :
Améliorer la luminosité, la saturation ou le contraste
Améliorer la netteté de vos images
Correspondance ou égalisation d’histogramme
Pour démontrer cela, nous allons appliquer deux fonctions du module skimage.exposure qui contient de nombreux algorithmes de traitement d’image puissants :
« skimage.exposure.rescale_intensity » mettra d’abord à l’échelle nos données entre 0,0 et 1,0 (requis pour l’étape 2)
skimage.exposure.equalize_adapthist
prendra ces données redimensionnées et appliquera un algorithme qui améliorera et contrastera les détails locaux de l’image
Remarque : les fonctions fournies à « image_proc_funcs » sont appliquées l’une après l’autre à chaque pas de temps dans « ds » (par exemple, chaque image de l’animation). Toute fonction personnalisée peut être fournie à « image_proc_funcs », à condition qu’elle accepte et génère un « numpy.ndarray » de forme « (y, x, bands) ».
[17]:
# Aniamte data after applying custom image processing functions to each frame
xr_animation(ds=ds.fillna(0),
bands=['red', 'green', 'blue'],
output_path='animated_timeseries.gif',
width_pixels=300,
image_proc_funcs=[skimage.exposure.rescale_intensity,
skimage.exposure.equalize_adapthist])
# Plot animated gifj
plt.close()
Image(filename='animated_timeseries.gif')
Applying custom image processing functions
Exporting animation to animated_timeseries.gif
[17]:
<IPython.core.display.Image object>
Informations Complémentaires
Licence : Le code de ce carnet est sous licence Apache, version 2.0 <https://www.apache.org/licenses/LICENSE-2.0>. Les données de Digital Earth Africa sont sous licence Creative Commons par attribution 4.0 <https://creativecommons.org/licenses/by/4.0/>.
Contact : Si vous avez besoin d’aide, veuillez poster une question sur le canal Slack Open Data Cube <http://slack.opendatacube.org/>`__ ou sur le GIS Stack Exchange en utilisant la balise open-data-cube
(vous pouvez consulter les questions posées précédemment ici). Si vous souhaitez signaler un problème avec ce bloc-notes, vous pouvez en déposer un sur Github.
Version de Datacube compatible :
[18]:
print(datacube.__version__)
1.8.15
Dernier test :
[19]:
from datetime import datetime
datetime.today().strftime('%Y-%m-%d')
[19]:
'2023-08-11'