Vertdure commited on
Commit
f7acec6
·
verified ·
1 Parent(s): 6b9a28f

Update pages/photo2timelapse.py

Browse files
Files changed (1) hide show
  1. pages/photo2timelapse.py +235 -101
pages/photo2timelapse.py CHANGED
@@ -1,121 +1,255 @@
1
  import streamlit as st
2
- import cv2
3
- import numpy as np
 
 
 
 
 
4
  import tempfile
5
  import os
6
- from PIL import Image
7
- import io
 
8
  import base64
9
- from concurrent.futures import ThreadPoolExecutor, as_completed
10
- from moviepy.editor import ImageSequenceClip
 
 
11
  import logging
12
- from tqdm import tqdm
13
 
14
  # Configuration du logging
15
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
16
-
17
- # Fonction pour compresser une image avec différentes méthodes
18
- def compress_image(image_path, quality=85, method='pillow'):
19
- if method == 'pillow':
20
- with Image.open(image_path) as img:
21
- img_byte_arr = io.BytesIO()
22
- img.save(img_byte_arr, format='JPEG', quality=quality, optimize=True)
23
- return Image.open(img_byte_arr)
24
- elif method == 'cv2':
25
- img = cv2.imread(image_path)
26
- encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
27
- _, encimg = cv2.imencode('.jpg', img, encode_param)
28
- return cv2.imdecode(encimg, 1)
29
-
30
- # Fonction pour créer un GIF optimisé
31
- def create_optimized_gif(image_paths, output_path, fps=10, max_size_mb=8):
32
- frames = []
33
- for img_path in tqdm(image_paths, desc="Traitement des images pour GIF"):
34
- with Image.open(img_path) as img:
35
- frames.append(img.copy())
36
-
37
- durations = [1000//fps] * len(frames)
38
-
39
- for quality in range(100, 0, -10):
40
- with io.BytesIO() as buffer:
41
- frames[0].save(buffer, format="GIF", save_all=True, append_images=frames[1:],
42
- optimize=True, duration=durations, loop=0, quality=quality)
43
- if buffer.tell() <= max_size_mb * 1024 * 1024:
44
- with open(output_path, "wb") as f:
45
- f.write(buffer.getvalue())
46
- logging.info(f"GIF créé avec succès. Qualité: {quality}")
47
- return
48
-
49
- logging.warning("Impossible de créer un GIF sous la taille maximale spécifiée.")
50
 
51
- # Fonction pour créer une vidéo MP4 optimisée
52
- def create_optimized_video(image_paths, output_path, fps=30, quality='high'):
53
- clip = ImageSequenceClip(image_paths, fps=fps)
54
-
55
- if quality == 'high':
56
- clip.write_videofile(output_path, codec='libx264', audio=False, fps=fps, bitrate="8000k")
57
- elif quality == 'medium':
58
- clip.write_videofile(output_path, codec='libx264', audio=False, fps=fps, bitrate="4000k")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  else:
60
- clip.write_videofile(output_path, codec='libx264', audio=False, fps=fps, bitrate="2000k")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
- logging.info(f"Vidéo MP4 créée avec succès. Qualité: {quality}")
63
-
64
- # Fonction pour traiter une image en parallèle
65
- def process_image(args):
66
- img_path, temp_dir, compress = args
67
- if compress:
68
- img = compress_image(img_path, quality=85, method='pillow')
69
- output_path = os.path.join(temp_dir, os.path.basename(img_path))
70
- img.save(output_path, "JPEG", quality=85)
71
- else:
72
- output_path = img_path
73
- return output_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- # Fonction pour générer un lien de téléchargement
76
  def get_binary_file_downloader_html(bin_file, file_label='File'):
77
  with open(bin_file, 'rb') as f:
78
  data = f.read()
79
  bin_str = base64.b64encode(data).decode()
80
- href = f'<a href="data:application/octet-stream;base64,{bin_str}" download="{os.path.basename(bin_file)}">{file_label}</a>'
81
  return href
82
 
83
- # Application Streamlit principale
84
- def main():
85
- st.title("Générateur de Timelapse Suprême")
86
 
87
- uploaded_files = st.file_uploader("Uploadez vos images", type=["png", "jpg", "jpeg"], accept_multiple_files=True)
88
-
89
- if uploaded_files:
90
- output_format = st.radio("Choisissez le format de sortie", ["GIF optimisé", "Vidéo MP4"])
91
-
92
- if output_format == "Vidéo MP4":
93
- quality = st.select_slider("Qualité vidéo", options=['low', 'medium', 'high'], value='medium')
94
-
95
- fps = st.slider("Images par seconde", min_value=1, max_value=60, value=30)
96
- compress_images = st.checkbox("Compresser les images avant traitement", value=True)
97
-
98
- if st.button("Générer Timelapse"):
99
- with st.spinner("Génération du timelapse en cours..."):
100
- with tempfile.TemporaryDirectory() as temp_dir:
101
- # Traitement parallèle des images
102
- with ThreadPoolExecutor() as executor:
103
- futures = [executor.submit(process_image, (file, temp_dir, compress_images)) for file in uploaded_files]
104
- processed_images = [future.result() for future in as_completed(futures)]
105
-
106
- if output_format == "GIF optimisé":
107
- output_path = os.path.join(temp_dir, "optimized_timelapse.gif")
108
- create_optimized_gif(processed_images, output_path, fps=fps)
109
- st.success("GIF optimisé généré!")
110
- st.image(output_path)
111
- st.markdown(get_binary_file_downloader_html(output_path, 'Télécharger le GIF'), unsafe_allow_html=True)
112
-
113
- elif output_format == "Vidéo MP4":
114
- output_path = os.path.join(temp_dir, "timelapse.mp4")
115
- create_optimized_video(processed_images, output_path, fps=fps, quality=quality)
116
- st.success("Vidéo MP4 générée!")
117
- st.video(output_path)
118
- st.markdown(get_binary_file_downloader_html(output_path, 'Télécharger la vidéo MP4'), unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
  if __name__ == "__main__":
121
- main()
 
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 datetime import datetime
13
+ from streamlit_folium import folium_static
14
  import base64
15
+ import asyncio
16
+ import aiohttp
17
+ from concurrent.futures import ThreadPoolExecutor
18
+ from functools import lru_cache
19
  import logging
20
+ import numpy as np
21
 
22
  # Configuration du logging
23
+ logging.basicConfig(level=logging.INFO)
24
+ logger = logging.getLogger(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
+ # Configuration de la page Streamlit
27
+ st.set_page_config(layout="wide")
28
+
29
+ # Liste des dates disponibles pour SWISSIMAGE Voyage dans le temps
30
+ AVAILABLE_DATES = [
31
+ 1946, 1959, 1965, 1980, 1985, 1990, 1995, 2000, 2005, 2010, 2015, 2018, 2021
32
+ ]
33
+
34
+ @st.cache_data
35
+ def uploaded_file_to_gdf(data):
36
+ import tempfile
37
+ import os
38
+ import uuid
39
+
40
+ _, file_extension = os.path.splitext(data.name)
41
+ file_id = str(uuid.uuid4())
42
+ file_path = os.path.join(tempfile.gettempdir(), f"{file_id}{file_extension}")
43
+
44
+ with open(file_path, "wb") as file:
45
+ file.write(data.getbuffer())
46
+
47
+ if file_path.lower().endswith(".kml"):
48
+ gdf = gpd.read_file(file_path, driver="KML")
49
  else:
50
+ gdf = gpd.read_file(file_path)
51
+
52
+ return gdf
53
+
54
+ @lru_cache(maxsize=128)
55
+ def get_wms_url(bbox, width, height, time):
56
+ url = "https://wms.geo.admin.ch/"
57
+ params = {
58
+ "SERVICE": "WMS",
59
+ "REQUEST": "GetMap",
60
+ "VERSION": "1.3.0",
61
+ "LAYERS": "ch.swisstopo.swissimage-product_timeseries",
62
+ "STYLES": "",
63
+ "CRS": "EPSG:2056",
64
+ "BBOX": ",".join(map(str, bbox)),
65
+ "WIDTH": str(width),
66
+ "HEIGHT": str(height),
67
+ "FORMAT": "image/png",
68
+ "TIME": str(time),
69
+ "TILED": "true"
70
+ }
71
+ return url + "?" + "&".join(f"{k}={v}" for k, v in params.items())
72
+
73
+ def add_date_to_image(image, date):
74
+ draw = ImageDraw.Draw(image)
75
+ font = ImageFont.load_default()
76
+ text = str(date)
77
 
78
+ bbox = draw.textbbox((0, 0), text, font=font)
79
+ textwidth = bbox[2] - bbox[0]
80
+ textheight = bbox[3] - bbox[1]
81
+
82
+ margin = 10
83
+ x = image.width - textwidth - margin
84
+ y = image.height - textheight - margin
85
+ draw.rectangle((x-5, y-5, x+textwidth+5, y+textheight+5), fill="black")
86
+ draw.text((x, y), text, font=font, fill="white")
87
+ return image
88
+
89
+ async def fetch_image(session, url, date, semaphore):
90
+ async with semaphore:
91
+ try:
92
+ async with session.get(url) as response:
93
+ if response.status == 200:
94
+ data = await response.read()
95
+ img = Image.open(BytesIO(data))
96
+ return add_date_to_image(img, date)
97
+ except Exception as e:
98
+ logger.error(f"Erreur lors de la récupération de l'image pour la date {date}: {str(e)}")
99
+ return None
100
+
101
+ async def download_images(bbox, width, height, available_years):
102
+ semaphore = asyncio.Semaphore(20) # Limité à 20 requêtes simultanées
103
+ async with aiohttp.ClientSession() as session:
104
+ tasks = [fetch_image(session, get_wms_url(bbox, width, height, date), date, semaphore) for date in available_years]
105
+ return await asyncio.gather(*tasks)
106
+
107
+ def process_images_stream(images, format_option, speed, temp_dir):
108
+ results = {}
109
+
110
+ if "GIF" in format_option:
111
+ gif_path = os.path.join(temp_dir, "timelapse.gif")
112
+ with imageio.get_writer(gif_path, mode='I', fps=speed, loop=0) as writer:
113
+ for img in images:
114
+ if img is not None:
115
+ writer.append_data(np.array(img))
116
+ results["GIF"] = gif_path
117
+
118
+ if "MP4" in format_option:
119
+ mp4_path = os.path.join(temp_dir, "timelapse.mp4")
120
+ with imageio.get_writer(mp4_path, fps=speed, quality=9) as writer:
121
+ for img in images:
122
+ if img is not None:
123
+ writer.append_data(np.array(img))
124
+ results["MP4"] = mp4_path
125
+
126
+ if "Images individuelles (ZIP)" in format_option:
127
+ zip_path = os.path.join(temp_dir, "images.zip")
128
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
129
+ for i, img in enumerate(images):
130
+ if img is not None:
131
+ img_path = os.path.join(temp_dir, f"image_{i}.png")
132
+ img.save(img_path)
133
+ zipf.write(img_path, os.path.basename(img_path))
134
+ os.remove(img_path)
135
+ results["ZIP"] = zip_path
136
+
137
+ return results
138
 
 
139
  def get_binary_file_downloader_html(bin_file, file_label='File'):
140
  with open(bin_file, 'rb') as f:
141
  data = f.read()
142
  bin_str = base64.b64encode(data).decode()
143
+ href = f'<a href="data:application/octet-stream;base64,{bin_str}" download="{os.path.basename(bin_file)}">Télécharger {file_label}</a>'
144
  return href
145
 
146
+ def app():
147
+ st.title("Générateur de Timelapse SWISSIMAGE Voyage dans le temps")
 
148
 
149
+ st.markdown(
150
+ """
151
+ Une application web interactive pour créer des timelapses historiques de la Suisse en utilisant SWISSIMAGE Voyage dans le temps.
152
+ """
153
+ )
154
+
155
+ row1_col1, row1_col2 = st.columns([2, 1])
156
+
157
+ with row1_col1:
158
+ m = folium.Map(location=[46.8182, 8.2275], zoom_start=8)
159
+ folium.TileLayer(
160
+ tiles="https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage/default/current/3857/{z}/{x}/{y}.jpeg",
161
+ attr="© swisstopo",
162
+ name="SWISSIMAGE",
163
+ overlay=False,
164
+ control=True
165
+ ).add_to(m)
166
+
167
+ draw = plugins.Draw(export=True)
168
+ draw.add_to(m)
169
+
170
+ folium.LayerControl().add_to(m)
171
+
172
+ folium_static(m, height=400)
173
+
174
+ with row1_col2:
175
+ data = st.file_uploader(
176
+ "Téléchargez un fichier GeoJSON à utiliser comme ROI. Personnalisez les paramètres du timelapse puis cliquez sur le bouton Soumettre 😇👇",
177
+ type=["geojson", "kml", "zip"],
178
+ )
179
+
180
+ with st.form("submit_form"):
181
+ start_year = st.selectbox("Sélectionnez l'année de début:", AVAILABLE_DATES)
182
+ end_year = st.selectbox("Sélectionnez l'année de fin:", AVAILABLE_DATES, index=len(AVAILABLE_DATES)-1)
183
+
184
+ size_options = {
185
+ "HD (720p)": (1280, 720),
186
+ "Full HD (1080p)": (1920, 1080),
187
+ "2K": (2560, 1440),
188
+ "4K": (3840, 2160),
189
+ "Personnalisé": None
190
+ }
191
+
192
+ size_choice = st.selectbox("Choisissez la taille de l'image:", list(size_options.keys()))
193
+
194
+ if size_choice == "Personnalisé":
195
+ col1, col2 = st.columns(2)
196
+ with col1:
197
+ width = st.number_input("Largeur:", min_value=100, max_value=4000, value=800)
198
+ with col2:
199
+ height = st.number_input("Hauteur:", min_value=100, max_value=4000, value=600)
200
+ else:
201
+ width, height = size_options[size_choice]
202
+
203
+ if width * height > 4000 * 4000:
204
+ 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.")
205
+
206
+ speed = st.slider("Images par seconde:", 1, 30, 5)
207
+
208
+ format_option = st.multiselect("Choisissez le(s) format(s) de sortie:", ["GIF", "MP4", "Images individuelles (ZIP)"], default=["GIF", "MP4", "Images individuelles (ZIP)"])
209
+
210
+ submitted = st.form_submit_button("Générer le Timelapse")
211
+
212
+ if submitted:
213
+ if data is None:
214
+ st.warning("Veuillez télécharger un fichier GeoJSON.")
215
+ elif width * height > 4000 * 4000:
216
+ st.error("La taille de l'image dépasse le maximum autorisé par swisstopo (4000x4000 pixels). Veuillez réduire la largeur ou la hauteur.")
217
+ else:
218
+ gdf = uploaded_file_to_gdf(data)
219
+ gdf_2056 = gdf.to_crs(epsg=2056)
220
+ bbox = tuple(gdf_2056.total_bounds)
221
+
222
+ available_years = [year for year in AVAILABLE_DATES if start_year <= year <= end_year]
223
+
224
+ total_requests = len(available_years)
225
+
226
+ if total_requests > 500:
227
+ st.warning(f"Vous demandez {total_requests} images. Cela dépasse la limite de 500 requêtes par seconde fixée par swisstopo. Le processus peut prendre plus de temps que prévu.")
228
+
229
+ progress_bar = st.progress(0)
230
+
231
+ images = asyncio.run(download_images(bbox, width, height, available_years))
232
+
233
+ progress_bar.progress(100)
234
+
235
+ if images:
236
+ logger.info(f"Récupération réussie de {len(images)} images")
237
+ with tempfile.TemporaryDirectory() as temp_dir:
238
+ with st.spinner('Traitement des images en cours... Cela peut prendre un certain temps pour les grandes images.'):
239
+ results = process_images_stream(images, format_option, speed, temp_dir)
240
+
241
+ for format, path in results.items():
242
+ if os.path.exists(path):
243
+ if format == "ZIP":
244
+ st.success("Images individuelles (ZIP) créées avec succès!")
245
+ else:
246
+ st.success(f"Timelapse {format} créé avec succès!")
247
+ st.markdown(get_binary_file_downloader_html(path, f'Timelapse {format if format != "ZIP" else "Images individuelles (ZIP)"}'), unsafe_allow_html=True)
248
+ else:
249
+ st.error(f"Le fichier {format} n'a pas été créé avec succès.")
250
+ else:
251
+ logger.error("Aucune image n'a été récupérée")
252
+ st.error("Échec de la création du timelapse. Aucune image n'a été générée.")
253
 
254
  if __name__ == "__main__":
255
+ app()