Vertdure commited on
Commit
a3c85cd
·
verified ·
1 Parent(s): 5aabf60

Create Swisslapse_ortho.py

Browse files
Files changed (1) hide show
  1. pages/Swisslapse_ortho.py +262 -0
pages/Swisslapse_ortho.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import geopandas as gpd
3
+ import folium
4
+ from folium import plugins
5
+ import requests
6
+ from PIL import Image, ImageDraw, ImageFont
7
+ from io import BytesIO
8
+ import imageio
9
+ import tempfile
10
+ import os
11
+ import zipfile
12
+ from streamlit_folium import folium_static
13
+ import base64
14
+ import asyncio
15
+ import aiohttp
16
+ from functools import lru_cache
17
+ import logging
18
+ import numpy as np
19
+
20
+ # Configuration du logging
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Configuration de la page Streamlit
25
+ st.set_page_config(layout="wide")
26
+
27
+ # Liste mise à jour des dates disponibles pour SWISSIMAGE Voyage dans le temps
28
+ AVAILABLE_DATES = [
29
+ 1946, 1959,
30
+ *range(1965, 1968), # 1965-1967
31
+ *range(1970, 1975), # 1970-1974
32
+ *range(1976, 1983), # 1976-1982
33
+ *range(1983, 1989), # 1983-1988
34
+ *range(1989, 1993), # 1989-1992
35
+ *range(1993, 1998), # 1993-1997
36
+ *range(1998, 2003), # 1998-2002
37
+ *range(2003, 2008), # 2003-2007
38
+ *range(2008, 2012), # 2008-2011
39
+ *range(2012, 2016), # 2012-2015
40
+ *range(2016, 2020), # 2016-2019
41
+ *range(2020, 2024) # 2020-2023
42
+ ]
43
+
44
+ WMS_BASE_URL = "https://wms.geo.admin.ch/"
45
+
46
+ @st.cache_data
47
+ def uploaded_file_to_gdf(data):
48
+ import tempfile
49
+ import os
50
+ import uuid
51
+
52
+ _, file_extension = os.path.splitext(data.name)
53
+ file_id = str(uuid.uuid4())
54
+ file_path = os.path.join(tempfile.gettempdir(), f"{file_id}{file_extension}")
55
+
56
+ with open(file_path, "wb") as file:
57
+ file.write(data.getbuffer())
58
+
59
+ if file_path.lower().endswith(".kml"):
60
+ gdf = gpd.read_file(file_path, driver="KML")
61
+ else:
62
+ gdf = gpd.read_file(file_path)
63
+
64
+ return gdf
65
+
66
+ @lru_cache(maxsize=128)
67
+ def get_wms_url(bbox, width, height, time):
68
+ params = {
69
+ "SERVICE": "WMS",
70
+ "VERSION": "1.3.0",
71
+ "REQUEST": "GetMap",
72
+ "LAYERS": "ch.swisstopo.swissimage-product",
73
+ "FORMAT": "image/jpeg",
74
+ "CRS": "EPSG:2056",
75
+ "BBOX": ",".join(map(str, bbox)),
76
+ "WIDTH": str(width),
77
+ "HEIGHT": str(height),
78
+ "TIME": str(time)
79
+ }
80
+ return WMS_BASE_URL + "?" + "&".join(f"{k}={v}" for k, v in params.items())
81
+
82
+ def add_date_to_image(image, date):
83
+ draw = ImageDraw.Draw(image)
84
+ font = ImageFont.load_default()
85
+ text = str(date)
86
+
87
+ bbox = draw.textbbox((0, 0), text, font=font)
88
+ textwidth = bbox[2] - bbox[0]
89
+ textheight = bbox[3] - bbox[1]
90
+
91
+ margin = 10
92
+ x = image.width - textwidth - margin
93
+ y = image.height - textheight - margin
94
+ draw.rectangle((x-5, y-5, x+textwidth+5, y+textheight+5), fill="black")
95
+ draw.text((x, y), text, font=font, fill="white")
96
+ return image
97
+
98
+ async def fetch_image(session, url, date, semaphore):
99
+ async with semaphore:
100
+ try:
101
+ async with session.get(url) as response:
102
+ if response.status == 200:
103
+ data = await response.read()
104
+ img = Image.open(BytesIO(data))
105
+ return add_date_to_image(img, date)
106
+ except Exception as e:
107
+ logger.error(f"Erreur lors de la récupération de l'image pour la date {date}: {str(e)}")
108
+ return None
109
+
110
+ async def download_images(bbox, width, height, available_years):
111
+ semaphore = asyncio.Semaphore(20)
112
+ async with aiohttp.ClientSession() as session:
113
+ tasks = [fetch_image(session, get_wms_url(bbox, width, height, year), year, semaphore) for year in available_years]
114
+ return await asyncio.gather(*tasks)
115
+
116
+ def process_images_stream(images, format_option, speed, temp_dir, batch_size=20):
117
+ results = {}
118
+
119
+ if "GIF" in format_option:
120
+ gif_path = os.path.join(temp_dir, "timelapse.gif")
121
+ with imageio.get_writer(gif_path, mode='I', fps=speed, loop=0) as writer:
122
+ for img in images:
123
+ if img is not None:
124
+ writer.append_data(np.array(img))
125
+ results["GIF"] = gif_path
126
+
127
+ if "MP4" in format_option:
128
+ mp4_path = os.path.join(temp_dir, "timelapse.mp4")
129
+ with imageio.get_writer(mp4_path, fps=speed, quality=9) as writer:
130
+ for img in images:
131
+ if img is not None:
132
+ writer.append_data(np.array(img))
133
+ results["MP4"] = mp4_path
134
+
135
+ if "Images individuelles (ZIP)" in format_option:
136
+ zip_paths = []
137
+ num_batches = (len(images) + batch_size - 1) // batch_size
138
+ for batch_index in range(num_batches):
139
+ batch_zip_path = os.path.join(temp_dir, f"images_batch_{batch_index + 1}.zip")
140
+ with zipfile.ZipFile(batch_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
141
+ batch_images = images[batch_index * batch_size:(batch_index + 1) * batch_size]
142
+ for i, img in enumerate(batch_images):
143
+ if img is not None:
144
+ img_path = os.path.join(temp_dir, f"image_{batch_index}_{i}.png")
145
+ img.save(img_path)
146
+ zipf.write(img_path, os.path.basename(img_path))
147
+ os.remove(img_path)
148
+ zip_paths.append(batch_zip_path)
149
+ results["ZIP"] = zip_paths
150
+
151
+ return results
152
+
153
+ def get_binary_file_downloader_html(bin_file, file_label='File'):
154
+ with open(bin_file, 'rb') as f:
155
+ data = f.read()
156
+ bin_str = base64.b64encode(data).decode()
157
+ href = f'<a href="data:application/octet-stream;base64,{bin_str}" download="{os.path.basename(bin_file)}">Télécharger {file_label}</a>'
158
+ return href
159
+
160
+ def app():
161
+ st.title("Générateur de Timelapse SWISSIMAGE Voyage dans le temps (WMS)")
162
+
163
+ st.markdown(
164
+ """
165
+ Une application web interactive pour créer des timelapses historiques de la Suisse en utilisant SWISSIMAGE Voyage dans le temps via WMS.
166
+ """
167
+ )
168
+
169
+ row1_col1, row1_col2 = st.columns([2, 1])
170
+
171
+ with row1_col1:
172
+ m = folium.Map(location=[46.8182, 8.2275], zoom_start=8)
173
+ folium.TileLayer(
174
+ tiles="https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage/default/current/3857/{z}/{x}/{y}.jpeg",
175
+ attr="© swisstopo",
176
+ name="SWISSIMAGE",
177
+ overlay=False,
178
+ control=True
179
+ ).add_to(m)
180
+
181
+ draw = plugins.Draw(export=True)
182
+ draw.add_to(m)
183
+ folium.LayerControl().add_to(m)
184
+
185
+ folium_static(m, height=400)
186
+
187
+ with row1_col2:
188
+ data = st.file_uploader(
189
+ "Téléchargez un fichier GeoJSON à utiliser comme ROI. Personnalisez les paramètres du timelapse puis cliquez sur le bouton Soumettre 😇👇",
190
+ type=["geojson", "kml", "zip"],
191
+ )
192
+
193
+ with st.form("submit_form"):
194
+ start_year = st.selectbox("Sélectionnez l'année de début:", AVAILABLE_DATES)
195
+ end_year = st.selectbox("Sélectionnez l'année de fin:", AVAILABLE_DATES, index=len(AVAILABLE_DATES)-1)
196
+
197
+ size_options = {
198
+ "HD (720p)": (1280, 720),
199
+ "Full HD (1080p)": (1920, 1080),
200
+ "2K": (2560, 1440),
201
+ "4K": (3840, 2160),
202
+ "Personnalisé": None
203
+ }
204
+
205
+ size_choice = st.selectbox("Choisissez la taille de l'image:", list(size_options.keys()))
206
+
207
+ if size_choice == "Personnalisé":
208
+ col1, col2 = st.columns(2)
209
+ with col1:
210
+ width = st.number_input("Largeur:", min_value=100, max_value=4000, value=800)
211
+ with col2:
212
+ height = st.number_input("Hauteur:", min_value=100, max_value=4000, value=600)
213
+ else:
214
+ width, height = size_options[size_choice]
215
+
216
+ if width * height > 4000 * 4000:
217
+ st.warning("Attention: La taille de l'image dépasse le maximum autorisé par swisstopo (4000x4000 pixels). Veuillez réduire la largeur ou la hauteur.")
218
+
219
+ speed = st.slider("Images par seconde:", 1, 30, 5)
220
+ format_option = st.multiselect("Choisissez le(s) format(s) de sortie:", ["GIF", "MP4", "Images individuelles (ZIP)"], default=["GIF", "MP4", "Images individuelles (ZIP)"])
221
+
222
+ submitted = st.form_submit_button("Générer le Timelapse")
223
+
224
+ if submitted:
225
+ if data is None:
226
+ st.warning("Veuillez télécharger un fichier GeoJSON.")
227
+ elif width * height > 4000 * 4000:
228
+ st.error("La taille de l'image dépasse le maximum autorisé par swisstopo (4000x4000 pixels). Veuillez réduire la largeur ou la hauteur.")
229
+ else:
230
+ gdf = uploaded_file_to_gdf(data)
231
+ gdf_2056 = gdf.to_crs(epsg=2056)
232
+ bbox = tuple(gdf_2056.total_bounds)
233
+
234
+ available_years = [year for year in AVAILABLE_DATES if start_year <= year <= end_year]
235
+
236
+ progress_bar = st.progress(0)
237
+
238
+ images = asyncio.run(download_images(bbox, width, height, available_years))
239
+
240
+ progress_bar.progress(100)
241
+
242
+ if images:
243
+ logger.info(f"Récupération réussie de {len(images)} images")
244
+ with tempfile.TemporaryDirectory() as temp_dir:
245
+ with st.spinner('Traitement des images en cours... Cela peut prendre un certain temps pour les grandes images.'):
246
+ results = process_images_stream(images, format_option, speed, temp_dir)
247
+
248
+ for format, paths in results.items():
249
+ if isinstance(paths, list): # Pour les lots ZIP
250
+ for path in paths:
251
+ batch_number = path.split("_")[-1].split(".")[0]
252
+ st.success(f"Images individuelles (ZIP) - Lot {batch_number} créé avec succès!")
253
+ st.markdown(get_binary_file_downloader_html(path, f'Images individuelles (ZIP) - Lot {batch_number}'), unsafe_allow_html=True)
254
+ else:
255
+ st.success(f"Timelapse {format} créé avec succès!")
256
+ st.markdown(get_binary_file_downloader_html(paths, f'Timelapse {format}'), unsafe_allow_html=True)
257
+ else:
258
+ logger.error("Aucune image n'a été récupérée")
259
+ st.error("Échec de la création du timelapse. Aucune image n'a été générée.")
260
+
261
+ if __name__ == "__main__":
262
+ app()