""" 3D Video Animation Generator Creates Relive-style 3D video animations from GPS route data """ import json import os import math import requests import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont import tempfile import shutil from datetime import datetime def generate_3d_video_animation(project_name, resources_folder, label_widget, progress_widget, popup_widget, clock_module): """ Generate a 3D video animation similar to Relive Args: project_name: Name of the project resources_folder: Path to resources folder label_widget: Kivy label for status updates progress_widget: Kivy progress bar popup_widget: Kivy popup to dismiss when done clock_module: Kivy Clock module for scheduling """ def update_progress(progress_val, status_text): """Update UI from background thread""" def _update(dt): progress_widget.value = progress_val label_widget.text = status_text clock_module.schedule_once(_update, 0) def finish_generation(success, message, output_path=None): """Finish the generation process""" def _finish(dt): if popup_widget: popup_widget.dismiss() # Show result popup from kivy.uix.popup import Popup from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.label import Label result_layout = BoxLayout(orientation='vertical', spacing=10, padding=10) if success: result_label = Label( text=f"3D Video Generated Successfully!\n\nSaved to:\n{output_path}", color=(0, 1, 0, 1), halign="center" ) open_btn = Button( text="Open Video Folder", size_hint_y=None, height=40, background_color=(0.2, 0.7, 0.2, 1) ) open_btn.bind(on_press=lambda x: (os.system(f"xdg-open '{os.path.dirname(output_path)}'"), result_popup.dismiss())) result_layout.add_widget(result_label) result_layout.add_widget(open_btn) else: result_label = Label( text=f"Generation Failed:\n{message}", color=(1, 0, 0, 1), halign="center" ) result_layout.add_widget(result_label) close_btn = Button( text="Close", size_hint_y=None, height=40, background_color=(0.3, 0.3, 0.3, 1) ) result_layout.add_widget(close_btn) result_popup = Popup( title="3D Video Generation Result", content=result_layout, size_hint=(0.9, 0.6), auto_dismiss=False ) close_btn.bind(on_press=lambda x: result_popup.dismiss()) result_popup.open() clock_module.schedule_once(_finish, 0) def run_generation(): """Main generation function""" try: # Step 1: Load route data update_progress(10, "Loading route data...") project_folder = os.path.join(resources_folder, "projects", project_name) positions_path = os.path.join(project_folder, "positions.json") if not os.path.exists(positions_path): finish_generation(False, "No route data found!") return with open(positions_path, "r") as f: positions = json.load(f) if len(positions) < 10: finish_generation(False, "Route too short for 3D animation (minimum 10 points)") return # Step 2: Calculate route bounds and center update_progress(20, "Calculating route boundaries...") lats = [pos['latitude'] for pos in positions] lons = [pos['longitude'] for pos in positions] center_lat = sum(lats) / len(lats) center_lon = sum(lons) / len(lons) min_lat, max_lat = min(lats), max(lats) min_lon, max_lon = min(lons), max(lons) # Step 3: Generate frames update_progress(30, "Generating 3D frames...") # Create temporary directory for frames temp_dir = tempfile.mkdtemp() frames_dir = os.path.join(temp_dir, "frames") os.makedirs(frames_dir) # Video settings width, height = 1920, 1080 fps = 30 total_frames = len(positions) * 2 # 2 frames per position for smooth animation # Generate frames for i, pos in enumerate(positions): progress = 30 + (i / len(positions)) * 40 update_progress(progress, f"Generating frame {i+1}/{len(positions)}...") frame = create_3d_frame( pos, positions, i, center_lat, center_lon, min_lat, max_lat, min_lon, max_lon, width, height ) # Save frame frame_path = os.path.join(frames_dir, f"frame_{i:06d}.png") cv2.imwrite(frame_path, frame) # Step 4: Create video update_progress(75, "Compiling video...") # Output path output_filename = f"{project_name}_3d_animation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4" output_path = os.path.join(project_folder, output_filename) # Create video writer fourcc = cv2.VideoWriter_fourcc(*'mp4v') video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) # Add frames to video frame_files = sorted([f for f in os.listdir(frames_dir) if f.endswith('.png')]) for frame_file in frame_files: frame_path = os.path.join(frames_dir, frame_file) frame = cv2.imread(frame_path) video_writer.write(frame) video_writer.release() # Step 5: Add audio (optional) update_progress(90, "Adding finishing touches...") # Clean up shutil.rmtree(temp_dir) update_progress(100, "3D Video generated successfully!") finish_generation(True, "Success!", output_path) except Exception as e: finish_generation(False, str(e)) # Start generation in background import threading thread = threading.Thread(target=run_generation) thread.daemon = True thread.start() def create_3d_frame(current_pos, all_positions, frame_index, center_lat, center_lon, min_lat, max_lat, min_lon, max_lon, width, height): """ Create a single 3D-style frame """ # Create canvas frame = np.zeros((height, width, 3), dtype=np.uint8) # Background gradient (sky effect) for y in range(height): color_intensity = int(255 * (1 - y / height)) sky_color = (min(255, color_intensity + 50), min(255, color_intensity + 100), 255) frame[y, :] = sky_color # Calculate perspective transformation # Simple isometric-style projection scale_x = width * 0.6 / (max_lon - min_lon) if max_lon != min_lon else 1000 scale_y = height * 0.6 / (max_lat - min_lat) if max_lat != min_lat else 1000 # Draw route path with 3D effect route_points = [] for i, pos in enumerate(all_positions[:frame_index + 1]): # Convert GPS to screen coordinates x = int((pos['longitude'] - min_lon) * scale_x + width * 0.2) y = int(height * 0.8 - (pos['latitude'] - min_lat) * scale_y) # Add 3D effect (elevation simulation) elevation_offset = int(20 * math.sin(i * 0.1)) # Simulated elevation y -= elevation_offset route_points.append((x, y)) # Draw route trail with gradient if len(route_points) > 1: for i in range(1, len(route_points)): # Color gradient from blue to red progress = i / len(route_points) color_r = int(255 * progress) color_b = int(255 * (1 - progress)) color = (color_b, 100, color_r) # Draw thick line with 3D shadow effect pt1, pt2 = route_points[i-1], route_points[i] # Shadow cv2.line(frame, (pt1[0]+2, pt1[1]+2), (pt2[0]+2, pt2[1]+2), (50, 50, 50), 8) # Main line cv2.line(frame, pt1, pt2, color, 6) # Draw current position marker if route_points: current_point = route_points[-1] # Pulsing effect pulse_size = int(15 + 10 * math.sin(frame_index * 0.3)) # Shadow cv2.circle(frame, (current_point[0]+3, current_point[1]+3), pulse_size, (0, 0, 0), -1) # Main marker cv2.circle(frame, current_point, pulse_size, (0, 255, 255), -1) cv2.circle(frame, current_point, pulse_size-3, (255, 255, 255), 2) # Add grid effect for 3D feel grid_spacing = 50 for x in range(0, width, grid_spacing): cv2.line(frame, (x, 0), (x, height), (100, 100, 100), 1) for y in range(0, height, grid_spacing): cv2.line(frame, (0, y), (width, y), (100, 100, 100), 1) # Add text overlay try: # Position info speed = current_pos.get('speed', 0) if current_pos else 0 timestamp = current_pos.get('deviceTime', '') if current_pos else '' text_y = 50 cv2.putText(frame, f"Speed: {speed:.1f} km/h", (50, text_y), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) text_y += 40 if timestamp: cv2.putText(frame, f"Time: {timestamp[:16]}", (50, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2) text_y += 40 cv2.putText(frame, f"Point: {frame_index + 1}/{len(all_positions)}", (50, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2) except Exception: pass # Skip text if font issues return frame def get_elevation_data(lat, lon): """ Get elevation data for a coordinate (optional enhancement) """ try: # Using a free elevation API url = f"https://api.open-elevation.com/api/v1/lookup?locations={lat},{lon}" response = requests.get(url, timeout=5) if response.status_code == 200: data = response.json() return data['results'][0]['elevation'] except Exception: pass return 0 # Default elevation