updated versions
This commit is contained in:
41
complete_video.py
Normal file
41
complete_video.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Complete video generation from existing frames
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
from moviepy import ImageSequenceClip
|
||||||
|
|
||||||
|
def create_video_from_frames():
|
||||||
|
frames_folder = "/home/pi/Desktop/traccar_animation/resources/projects/day 2/frames"
|
||||||
|
output_path = "/home/pi/Desktop/traccar_animation/resources/projects/day 2/advanced_3d_animation.mp4"
|
||||||
|
|
||||||
|
# Get all frame files
|
||||||
|
frame_files = glob.glob(os.path.join(frames_folder, "frame_*.png"))
|
||||||
|
frame_files.sort() # Ensure correct order
|
||||||
|
|
||||||
|
if not frame_files:
|
||||||
|
print("No frames found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {len(frame_files)} frames")
|
||||||
|
print("Creating video...")
|
||||||
|
|
||||||
|
# Create video clip
|
||||||
|
clip = ImageSequenceClip(frame_files, fps=30)
|
||||||
|
|
||||||
|
# Write video file
|
||||||
|
clip.write_videofile(
|
||||||
|
output_path,
|
||||||
|
codec='libx264',
|
||||||
|
bitrate='8000k',
|
||||||
|
audio=False,
|
||||||
|
temp_audiofile=None,
|
||||||
|
remove_temp=True
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Video created successfully: {output_path}")
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_video_from_frames()
|
||||||
BIN
py_scripts/__pycache__/advanced_3d_generator.cpython-311.pyc
Normal file
BIN
py_scripts/__pycache__/advanced_3d_generator.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py_scripts/__pycache__/blender_animator.cpython-311.pyc
Normal file
BIN
py_scripts/__pycache__/blender_animator.cpython-311.pyc
Normal file
Binary file not shown.
695
py_scripts/advanced_3d_generator.py
Normal file
695
py_scripts/advanced_3d_generator.py
Normal file
@@ -0,0 +1,695 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import time
|
||||||
|
from PIL import Image
|
||||||
|
import cv2
|
||||||
|
from geopy.distance import geodesic
|
||||||
|
import math
|
||||||
|
|
||||||
|
# Optional advanced dependencies with fallback handling
|
||||||
|
try:
|
||||||
|
import pandas as pd
|
||||||
|
import geopandas as gpd
|
||||||
|
PANDAS_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
PANDAS_AVAILABLE = False
|
||||||
|
print("Warning: pandas/geopandas not available. Install with: pip install pandas geopandas")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
import plotly.express as px
|
||||||
|
from plotly.subplots import make_subplots
|
||||||
|
PLOTLY_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
PLOTLY_AVAILABLE = False
|
||||||
|
print("Warning: plotly not available. Install with: pip install plotly")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pydeck as pdk
|
||||||
|
PYDECK_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
PYDECK_AVAILABLE = False
|
||||||
|
print("Warning: pydeck not available. Install with: pip install pydeck")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from moviepy import VideoFileClip, ImageSequenceClip
|
||||||
|
MOVIEPY_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
MOVIEPY_AVAILABLE = False
|
||||||
|
print("Warning: moviepy not available. Install with: pip install moviepy")
|
||||||
|
|
||||||
|
class Advanced3DGenerator:
|
||||||
|
"""
|
||||||
|
Advanced 3D animation generator using Pydeck + Plotly + Blender pipeline
|
||||||
|
for high-quality GPS track visualizations
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, output_folder):
|
||||||
|
self.output_folder = output_folder
|
||||||
|
self.frames_folder = os.path.join(output_folder, "frames")
|
||||||
|
self.temp_folder = os.path.join(output_folder, "temp")
|
||||||
|
|
||||||
|
# Create necessary folders
|
||||||
|
os.makedirs(self.frames_folder, exist_ok=True)
|
||||||
|
os.makedirs(self.temp_folder, exist_ok=True)
|
||||||
|
|
||||||
|
# Animation settings
|
||||||
|
self.fps = 30
|
||||||
|
self.duration_per_point = 0.5 # seconds per GPS point
|
||||||
|
self.camera_height = 1000 # meters
|
||||||
|
self.trail_length = 50 # number of previous points to show
|
||||||
|
|
||||||
|
def check_dependencies(self):
|
||||||
|
"""Check if all required dependencies are available"""
|
||||||
|
missing = []
|
||||||
|
if not PANDAS_AVAILABLE:
|
||||||
|
missing.append("pandas, geopandas")
|
||||||
|
if not PLOTLY_AVAILABLE:
|
||||||
|
missing.append("plotly")
|
||||||
|
if not PYDECK_AVAILABLE:
|
||||||
|
missing.append("pydeck")
|
||||||
|
if not MOVIEPY_AVAILABLE:
|
||||||
|
missing.append("moviepy")
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
raise ImportError(f"Missing required dependencies: {', '.join(missing)}. Please install them with: pip install {' '.join(missing)}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def load_gps_data(self, positions_file):
|
||||||
|
"""Load and preprocess GPS data"""
|
||||||
|
with open(positions_file, 'r') as f:
|
||||||
|
positions = json.load(f)
|
||||||
|
|
||||||
|
# Convert to DataFrame
|
||||||
|
df = pd.DataFrame(positions)
|
||||||
|
|
||||||
|
# Parse timestamps
|
||||||
|
df['timestamp'] = pd.to_datetime(df['fixTime'])
|
||||||
|
df = df.sort_values('timestamp')
|
||||||
|
|
||||||
|
# Calculate speed and bearing
|
||||||
|
df['speed_kmh'] = df['speed'] * 1.852 # Convert knots to km/h
|
||||||
|
df['elevation'] = df.get('altitude', 0)
|
||||||
|
|
||||||
|
# Calculate distance between points
|
||||||
|
distances = []
|
||||||
|
bearings = []
|
||||||
|
|
||||||
|
for i in range(len(df)):
|
||||||
|
if i == 0:
|
||||||
|
distances.append(0)
|
||||||
|
bearings.append(0)
|
||||||
|
else:
|
||||||
|
prev_point = (df.iloc[i-1]['latitude'], df.iloc[i-1]['longitude'])
|
||||||
|
curr_point = (df.iloc[i]['latitude'], df.iloc[i]['longitude'])
|
||||||
|
|
||||||
|
# Calculate distance
|
||||||
|
dist = geodesic(prev_point, curr_point).meters
|
||||||
|
distances.append(dist)
|
||||||
|
|
||||||
|
# Calculate bearing
|
||||||
|
lat1, lon1 = math.radians(prev_point[0]), math.radians(prev_point[1])
|
||||||
|
lat2, lon2 = math.radians(curr_point[0]), math.radians(curr_point[1])
|
||||||
|
|
||||||
|
dlon = lon2 - lon1
|
||||||
|
bearing = math.atan2(
|
||||||
|
math.sin(dlon) * math.cos(lat2),
|
||||||
|
math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon)
|
||||||
|
)
|
||||||
|
bearings.append(math.degrees(bearing))
|
||||||
|
|
||||||
|
df['distance'] = distances
|
||||||
|
df['bearing'] = bearings
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def create_pydeck_frame(self, df, current_index, frame_num):
|
||||||
|
"""Create a single frame using Pydeck"""
|
||||||
|
# Get current position
|
||||||
|
current_row = df.iloc[current_index]
|
||||||
|
|
||||||
|
# Get trail data (previous points)
|
||||||
|
start_idx = max(0, current_index - self.trail_length)
|
||||||
|
trail_data = df.iloc[start_idx:current_index + 1].copy()
|
||||||
|
|
||||||
|
# Create color gradient for trail (fade effect)
|
||||||
|
trail_colors = []
|
||||||
|
for i, _ in enumerate(trail_data.iterrows()):
|
||||||
|
alpha = (i + 1) / len(trail_data) * 255
|
||||||
|
trail_colors.append([255, 100, 100, int(alpha)])
|
||||||
|
|
||||||
|
trail_data['color'] = trail_colors
|
||||||
|
|
||||||
|
# Create the deck
|
||||||
|
view_state = pdk.ViewState(
|
||||||
|
longitude=current_row['longitude'],
|
||||||
|
latitude=current_row['latitude'],
|
||||||
|
zoom=16,
|
||||||
|
pitch=60,
|
||||||
|
bearing=current_row['bearing']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Path layer (trail)
|
||||||
|
path_layer = pdk.Layer(
|
||||||
|
"PathLayer",
|
||||||
|
trail_data,
|
||||||
|
get_path=lambda x: [[x['longitude'], x['latitude'], x['elevation']]],
|
||||||
|
get_color=[255, 100, 100, 200],
|
||||||
|
width_min_pixels=3,
|
||||||
|
width_max_pixels=8,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Scatterplot layer (current position)
|
||||||
|
current_point = pd.DataFrame([{
|
||||||
|
'longitude': current_row['longitude'],
|
||||||
|
'latitude': current_row['latitude'],
|
||||||
|
'elevation': current_row['elevation'] + 10,
|
||||||
|
'speed': current_row['speed_kmh']
|
||||||
|
}])
|
||||||
|
|
||||||
|
scatter_layer = pdk.Layer(
|
||||||
|
"ScatterplotLayer",
|
||||||
|
current_point,
|
||||||
|
get_position=['longitude', 'latitude', 'elevation'],
|
||||||
|
get_radius=15,
|
||||||
|
get_color=[255, 255, 0, 255],
|
||||||
|
pickable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create deck
|
||||||
|
deck = pdk.Deck(
|
||||||
|
layers=[path_layer, scatter_layer],
|
||||||
|
initial_view_state=view_state,
|
||||||
|
map_style='mapbox://styles/mapbox/satellite-v9'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save frame
|
||||||
|
frame_path = os.path.join(self.frames_folder, f"frame_{frame_num:06d}.png")
|
||||||
|
deck.to_html(frame_path.replace('.png', '.html'))
|
||||||
|
|
||||||
|
return frame_path
|
||||||
|
|
||||||
|
def create_plotly_frame(self, df, current_index, frame_num):
|
||||||
|
"""Create a single frame using Plotly for 3D visualization"""
|
||||||
|
# Get current position
|
||||||
|
current_row = df.iloc[current_index]
|
||||||
|
|
||||||
|
# Get trail data
|
||||||
|
start_idx = max(0, current_index - self.trail_length)
|
||||||
|
trail_data = df.iloc[start_idx:current_index + 1]
|
||||||
|
|
||||||
|
# Create 3D scatter plot
|
||||||
|
fig = go.Figure()
|
||||||
|
|
||||||
|
# Add trail
|
||||||
|
fig.add_trace(go.Scatter3d(
|
||||||
|
x=trail_data['longitude'],
|
||||||
|
y=trail_data['latitude'],
|
||||||
|
z=trail_data['elevation'],
|
||||||
|
mode='lines+markers',
|
||||||
|
line=dict(
|
||||||
|
color=trail_data.index,
|
||||||
|
colorscale='Plasma',
|
||||||
|
width=8
|
||||||
|
),
|
||||||
|
marker=dict(
|
||||||
|
size=3,
|
||||||
|
opacity=0.8
|
||||||
|
),
|
||||||
|
name='Trail'
|
||||||
|
))
|
||||||
|
|
||||||
|
# Add current position
|
||||||
|
fig.add_trace(go.Scatter3d(
|
||||||
|
x=[current_row['longitude']],
|
||||||
|
y=[current_row['latitude']],
|
||||||
|
z=[current_row['elevation'] + 50],
|
||||||
|
mode='markers',
|
||||||
|
marker=dict(
|
||||||
|
size=15,
|
||||||
|
color='red',
|
||||||
|
symbol='diamond'
|
||||||
|
),
|
||||||
|
name='Current Position'
|
||||||
|
))
|
||||||
|
|
||||||
|
# Add speed information as text
|
||||||
|
speed_text = f"Speed: {current_row['speed_kmh']:.1f} km/h<br>"
|
||||||
|
speed_text += f"Time: {current_row['timestamp'].strftime('%H:%M:%S')}<br>"
|
||||||
|
speed_text += f"Altitude: {current_row['elevation']:.0f} m"
|
||||||
|
|
||||||
|
# Update layout
|
||||||
|
fig.update_layout(
|
||||||
|
title=f"3D GPS Track Animation - Frame {frame_num}",
|
||||||
|
scene=dict(
|
||||||
|
xaxis_title='Longitude',
|
||||||
|
yaxis_title='Latitude',
|
||||||
|
zaxis_title='Elevation (m)',
|
||||||
|
camera=dict(
|
||||||
|
eye=dict(x=1.5, y=1.5, z=1.5)
|
||||||
|
),
|
||||||
|
aspectmode='cube'
|
||||||
|
),
|
||||||
|
annotations=[
|
||||||
|
dict(
|
||||||
|
text=speed_text,
|
||||||
|
x=0.02,
|
||||||
|
y=0.98,
|
||||||
|
xref='paper',
|
||||||
|
yref='paper',
|
||||||
|
showarrow=False,
|
||||||
|
bgcolor='rgba(255,255,255,0.8)',
|
||||||
|
bordercolor='black',
|
||||||
|
borderwidth=1
|
||||||
|
)
|
||||||
|
],
|
||||||
|
width=1920,
|
||||||
|
height=1080
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save frame
|
||||||
|
frame_path = os.path.join(self.frames_folder, f"frame_{frame_num:06d}.png")
|
||||||
|
fig.write_image(frame_path, engine="kaleido")
|
||||||
|
|
||||||
|
return frame_path
|
||||||
|
|
||||||
|
def create_advanced_plotly_frame(self, df, current_index, frame_num):
|
||||||
|
"""Create Relive-style progressive animation frame"""
|
||||||
|
# Progressive track: show all points from start to current position
|
||||||
|
current_track = df.iloc[:current_index + 1]
|
||||||
|
current_row = df.iloc[current_index]
|
||||||
|
|
||||||
|
# Create subplot with multiple views
|
||||||
|
fig = make_subplots(
|
||||||
|
rows=2, cols=2,
|
||||||
|
subplot_titles=[
|
||||||
|
f'3D Track Progress - Frame {frame_num + 1}',
|
||||||
|
'Route Overview',
|
||||||
|
'Speed Over Time',
|
||||||
|
'Elevation Profile'
|
||||||
|
],
|
||||||
|
specs=[[{'type': 'scene'}, {'type': 'scatter'}],
|
||||||
|
[{'type': 'scatter'}, {'type': 'scatter'}]],
|
||||||
|
vertical_spacing=0.1,
|
||||||
|
horizontal_spacing=0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1. 3D Progressive Track View
|
||||||
|
if len(current_track) > 1:
|
||||||
|
# Show completed track in blue
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter3d(
|
||||||
|
x=current_track['longitude'],
|
||||||
|
y=current_track['latitude'],
|
||||||
|
z=current_track['elevation'],
|
||||||
|
mode='lines+markers',
|
||||||
|
line=dict(color='blue', width=5),
|
||||||
|
marker=dict(
|
||||||
|
size=3,
|
||||||
|
color=current_track['speed_kmh'],
|
||||||
|
colorscale='Viridis',
|
||||||
|
showscale=True,
|
||||||
|
colorbar=dict(title="Speed (km/h)", x=0.45)
|
||||||
|
),
|
||||||
|
name='Completed Track',
|
||||||
|
showlegend=False
|
||||||
|
),
|
||||||
|
row=1, col=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Current vehicle position (moving marker)
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter3d(
|
||||||
|
x=[current_row['longitude']],
|
||||||
|
y=[current_row['latitude']],
|
||||||
|
z=[current_row['elevation'] + 15],
|
||||||
|
mode='markers',
|
||||||
|
marker=dict(
|
||||||
|
size=15,
|
||||||
|
color='red',
|
||||||
|
symbol='diamond',
|
||||||
|
line=dict(color='white', width=2)
|
||||||
|
),
|
||||||
|
name=f'Vehicle - {current_row["timestamp"].strftime("%H:%M:%S")}',
|
||||||
|
showlegend=False
|
||||||
|
),
|
||||||
|
row=1, col=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Top View - Route Overview with full track
|
||||||
|
# Show full route in light gray
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=df['longitude'],
|
||||||
|
y=df['latitude'],
|
||||||
|
mode='lines',
|
||||||
|
line=dict(color='lightgray', width=2, dash='dot'),
|
||||||
|
name='Full Route',
|
||||||
|
showlegend=False
|
||||||
|
),
|
||||||
|
row=1, col=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show completed track in color
|
||||||
|
if len(current_track) > 1:
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=current_track['longitude'],
|
||||||
|
y=current_track['latitude'],
|
||||||
|
mode='lines+markers',
|
||||||
|
line=dict(color='blue', width=4),
|
||||||
|
marker=dict(
|
||||||
|
size=4,
|
||||||
|
color=current_track['speed_kmh'],
|
||||||
|
colorscale='Viridis'
|
||||||
|
),
|
||||||
|
name='Progress',
|
||||||
|
showlegend=False
|
||||||
|
),
|
||||||
|
row=1, col=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Current position on map
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=[current_row['longitude']],
|
||||||
|
y=[current_row['latitude']],
|
||||||
|
mode='markers',
|
||||||
|
marker=dict(
|
||||||
|
size=12,
|
||||||
|
color='red',
|
||||||
|
symbol='circle',
|
||||||
|
line=dict(color='white', width=3)
|
||||||
|
),
|
||||||
|
name='Current Position',
|
||||||
|
showlegend=False
|
||||||
|
),
|
||||||
|
row=1, col=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Speed Profile Over Time
|
||||||
|
if len(current_track) > 1:
|
||||||
|
time_points = list(range(len(current_track)))
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=time_points,
|
||||||
|
y=current_track['speed_kmh'],
|
||||||
|
mode='lines+markers',
|
||||||
|
line=dict(color='green', width=3),
|
||||||
|
marker=dict(size=4),
|
||||||
|
fill='tonexty',
|
||||||
|
name='Speed',
|
||||||
|
showlegend=False
|
||||||
|
),
|
||||||
|
row=2, col=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Current speed marker
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=[current_index],
|
||||||
|
y=[current_row['speed_kmh']],
|
||||||
|
mode='markers',
|
||||||
|
marker=dict(size=10, color='red'),
|
||||||
|
name='Current Speed',
|
||||||
|
showlegend=False
|
||||||
|
),
|
||||||
|
row=2, col=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Elevation Profile Over Time
|
||||||
|
if len(current_track) > 1:
|
||||||
|
time_points = list(range(len(current_track)))
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=time_points,
|
||||||
|
y=current_track['elevation'],
|
||||||
|
mode='lines+markers',
|
||||||
|
line=dict(color='brown', width=3),
|
||||||
|
marker=dict(size=4),
|
||||||
|
fill='tonexty',
|
||||||
|
name='Elevation',
|
||||||
|
showlegend=False
|
||||||
|
),
|
||||||
|
row=2, col=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Current elevation marker
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=[current_index],
|
||||||
|
y=[current_row['elevation']],
|
||||||
|
mode='markers',
|
||||||
|
marker=dict(size=10, color='red'),
|
||||||
|
name='Current Elevation',
|
||||||
|
showlegend=False
|
||||||
|
),
|
||||||
|
row=2, col=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enhanced layout with better styling
|
||||||
|
fig.update_layout(
|
||||||
|
title=dict(
|
||||||
|
text=f'GPS Track Animation - {current_row["timestamp"].strftime("%Y-%m-%d %H:%M:%S")}<br>' +
|
||||||
|
f'Progress: {current_index + 1}/{len(df)} points ({((current_index + 1)/len(df)*100):.1f}%)',
|
||||||
|
x=0.5,
|
||||||
|
font=dict(size=16)
|
||||||
|
),
|
||||||
|
showlegend=False,
|
||||||
|
width=1920,
|
||||||
|
height=1080,
|
||||||
|
paper_bgcolor='white',
|
||||||
|
plot_bgcolor='white'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update 3D scene
|
||||||
|
fig.update_scenes(
|
||||||
|
camera=dict(
|
||||||
|
eye=dict(x=1.5, y=1.5, z=1.2),
|
||||||
|
center=dict(x=0, y=0, z=0),
|
||||||
|
up=dict(x=0, y=0, z=1)
|
||||||
|
),
|
||||||
|
aspectmode='cube'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update axes labels
|
||||||
|
fig.update_xaxes(title_text="Longitude", row=1, col=2)
|
||||||
|
fig.update_yaxes(title_text="Latitude", row=1, col=2)
|
||||||
|
fig.update_xaxes(title_text="Time Points", row=2, col=1)
|
||||||
|
fig.update_yaxes(title_text="Speed (km/h)", row=2, col=1)
|
||||||
|
fig.update_xaxes(title_text="Time Points", row=2, col=2)
|
||||||
|
fig.update_yaxes(title_text="Elevation (m)", row=2, col=2)
|
||||||
|
|
||||||
|
# Save frame
|
||||||
|
frame_path = os.path.join(self.frames_folder, f"advanced_3d_frame_{frame_num:04d}.png")
|
||||||
|
|
||||||
|
try:
|
||||||
|
fig.write_image(frame_path, engine="kaleido", width=1920, height=1080)
|
||||||
|
return frame_path
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating frame {frame_num}: {e}")
|
||||||
|
return None
|
||||||
|
def generate_frames(self, positions_file, style='advanced', progress_callback=None):
|
||||||
|
"""Generate Relive-style progressive animation frames"""
|
||||||
|
print("Loading GPS data...")
|
||||||
|
df = self.load_gps_data(positions_file)
|
||||||
|
|
||||||
|
if len(df) < 2:
|
||||||
|
print("Not enough GPS points for animation")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Animation settings for smooth progression
|
||||||
|
min_frames = 60 # Minimum frames for very short trips
|
||||||
|
max_frames = 300 # Maximum frames to keep file size reasonable
|
||||||
|
|
||||||
|
# Calculate optimal frame count based on trip length
|
||||||
|
total_frames = min(max_frames, max(min_frames, len(df) * 2))
|
||||||
|
|
||||||
|
# Calculate step size for smooth progression
|
||||||
|
step_size = len(df) / total_frames
|
||||||
|
|
||||||
|
frame_paths = []
|
||||||
|
|
||||||
|
print(f"Generating {total_frames} frames for Relive-style animation...")
|
||||||
|
print(f"Processing {len(df)} GPS points with step size {step_size:.2f}")
|
||||||
|
|
||||||
|
for frame_num in range(total_frames):
|
||||||
|
# Calculate which GPS point to show up to
|
||||||
|
current_index = min(int(frame_num * step_size), len(df) - 1)
|
||||||
|
|
||||||
|
# Ensure we always progress forward
|
||||||
|
if current_index == 0:
|
||||||
|
current_index = 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
if style == 'pydeck':
|
||||||
|
frame_path = self.create_pydeck_frame(df, current_index, frame_num)
|
||||||
|
elif style == 'plotly':
|
||||||
|
frame_path = self.create_plotly_frame(df, current_index, frame_num)
|
||||||
|
else: # advanced
|
||||||
|
frame_path = self.create_advanced_plotly_frame(df, current_index, frame_num)
|
||||||
|
|
||||||
|
if frame_path:
|
||||||
|
frame_paths.append(frame_path)
|
||||||
|
|
||||||
|
# Update progress
|
||||||
|
if progress_callback:
|
||||||
|
progress = ((frame_num + 1) / total_frames) * 100
|
||||||
|
progress_callback(
|
||||||
|
progress,
|
||||||
|
f"Creating animation frame {frame_num + 1}/{total_frames} (GPS point {current_index + 1}/{len(df)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Progress feedback
|
||||||
|
if (frame_num + 1) % 20 == 0:
|
||||||
|
print(f"Generated {frame_num + 1}/{total_frames} frames ({progress:.1f}%)")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating frame {frame_num}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"Successfully generated {len(frame_paths)} animation frames")
|
||||||
|
return frame_paths
|
||||||
|
|
||||||
|
def create_video(self, frame_paths, output_video_path, progress_callback=None):
|
||||||
|
"""Create video from frames using MoviePy with optimized settings"""
|
||||||
|
print("Creating Relive-style animation video...")
|
||||||
|
|
||||||
|
if not frame_paths:
|
||||||
|
print("No frames to create video from")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Filter out None paths
|
||||||
|
valid_frames = [f for f in frame_paths if f and os.path.exists(f)]
|
||||||
|
|
||||||
|
if not valid_frames:
|
||||||
|
print("No valid frames found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"Creating video from {len(valid_frames)} frames at {self.fps} FPS...")
|
||||||
|
|
||||||
|
# Create video clip from images with optimal settings
|
||||||
|
clip = ImageSequenceClip(valid_frames, fps=self.fps)
|
||||||
|
|
||||||
|
# Add smooth fade effects for professional look
|
||||||
|
clip = clip.fadein(0.5).fadeout(0.5)
|
||||||
|
|
||||||
|
# Update progress
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(50, "Encoding video with optimized settings...")
|
||||||
|
|
||||||
|
# Write video file with high quality settings
|
||||||
|
clip.write_videofile(
|
||||||
|
output_video_path,
|
||||||
|
codec='libx264',
|
||||||
|
audio=False,
|
||||||
|
temp_audiofile=None,
|
||||||
|
remove_temp=True,
|
||||||
|
verbose=False,
|
||||||
|
logger=None,
|
||||||
|
bitrate="8000k", # High quality bitrate
|
||||||
|
ffmpeg_params=[
|
||||||
|
"-preset", "medium", # Balance between speed and compression
|
||||||
|
"-crf", "18", # High quality (lower = better quality)
|
||||||
|
"-pix_fmt", "yuv420p" # Better compatibility
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(100, f"Video successfully created: {os.path.basename(output_video_path)}")
|
||||||
|
|
||||||
|
print(f"✅ Relive-style animation video saved to: {output_video_path}")
|
||||||
|
print(f"📊 Video info: {len(valid_frames)} frames, {self.fps} FPS, {clip.duration:.1f}s duration")
|
||||||
|
|
||||||
|
# Clean up clip
|
||||||
|
clip.close()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating video: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def cleanup_frames(self):
|
||||||
|
"""Clean up temporary frame files"""
|
||||||
|
import shutil
|
||||||
|
if os.path.exists(self.frames_folder):
|
||||||
|
shutil.rmtree(self.frames_folder)
|
||||||
|
os.makedirs(self.frames_folder, exist_ok=True)
|
||||||
|
|
||||||
|
def generate_3d_animation(self, positions_file, output_video_path,
|
||||||
|
style='advanced', cleanup=True, progress_callback=None):
|
||||||
|
"""
|
||||||
|
Main method to generate 3D animation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
positions_file: Path to JSON file with GPS positions
|
||||||
|
output_video_path: Path for output video
|
||||||
|
style: 'pydeck', 'plotly', or 'advanced'
|
||||||
|
cleanup: Whether to clean up temporary files
|
||||||
|
progress_callback: Callback function for progress updates
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Generate frames
|
||||||
|
frame_paths = self.generate_frames(positions_file, style, progress_callback)
|
||||||
|
|
||||||
|
if not frame_paths:
|
||||||
|
raise Exception("No frames generated")
|
||||||
|
|
||||||
|
# Create video
|
||||||
|
success = self.create_video(frame_paths, output_video_path, progress_callback)
|
||||||
|
|
||||||
|
if cleanup:
|
||||||
|
self.cleanup_frames()
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating 3D animation: {e}")
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(-1, f"Error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def generate_advanced_3d_video(positions_file, output_folder, filename_prefix="advanced_3d",
|
||||||
|
style='advanced', progress_callback=None):
|
||||||
|
"""
|
||||||
|
Convenience function to generate advanced 3D video
|
||||||
|
"""
|
||||||
|
generator = Advanced3DGenerator(output_folder)
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
output_video_path = os.path.join(output_folder, f"{filename_prefix}_{timestamp}.mp4")
|
||||||
|
|
||||||
|
success = generator.generate_3d_animation(
|
||||||
|
positions_file,
|
||||||
|
output_video_path,
|
||||||
|
style=style,
|
||||||
|
progress_callback=progress_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
return output_video_path if success else None
|
||||||
|
|
||||||
|
# Test function
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Test the advanced 3D generator
|
||||||
|
test_positions = "test_positions.json"
|
||||||
|
output_dir = "test_output"
|
||||||
|
|
||||||
|
def test_progress(progress, message):
|
||||||
|
print(f"Progress: {progress:.1f}% - {message}")
|
||||||
|
|
||||||
|
video_path = generate_advanced_3d_video(
|
||||||
|
test_positions,
|
||||||
|
output_dir,
|
||||||
|
style='advanced',
|
||||||
|
progress_callback=test_progress
|
||||||
|
)
|
||||||
|
|
||||||
|
if video_path:
|
||||||
|
print(f"Test video created: {video_path}")
|
||||||
|
else:
|
||||||
|
print("Test failed")
|
||||||
332
py_scripts/blender_animator.py
Normal file
332
py_scripts/blender_animator.py
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import math
|
||||||
|
|
||||||
|
# Blender dependencies with fallback handling
|
||||||
|
try:
|
||||||
|
import bpy
|
||||||
|
import bmesh
|
||||||
|
from mathutils import Vector, Euler
|
||||||
|
BLENDER_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
BLENDER_AVAILABLE = False
|
||||||
|
print("Warning: Blender (bpy) not available. This module requires Blender to be installed with Python API access.")
|
||||||
|
|
||||||
|
class BlenderGPSAnimator:
|
||||||
|
"""
|
||||||
|
Advanced GPS track animation using Blender for high-quality 3D rendering
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, output_folder):
|
||||||
|
self.output_folder = output_folder
|
||||||
|
if BLENDER_AVAILABLE:
|
||||||
|
self.setup_blender_scene()
|
||||||
|
else:
|
||||||
|
raise ImportError("Blender (bpy) is not available. Please install Blender with Python API access.")
|
||||||
|
|
||||||
|
def check_dependencies(self):
|
||||||
|
"""Check if Blender dependencies are available"""
|
||||||
|
if not BLENDER_AVAILABLE:
|
||||||
|
raise ImportError("Blender (bpy) is not available. Please install Blender with Python API access.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def setup_blender_scene(self):
|
||||||
|
"""Setup Blender scene for GPS animation"""
|
||||||
|
# Clear existing mesh objects
|
||||||
|
bpy.ops.object.select_all(action='SELECT')
|
||||||
|
bpy.ops.object.delete(use_global=False)
|
||||||
|
|
||||||
|
# Add camera
|
||||||
|
bpy.ops.object.camera_add(location=(0, 0, 10))
|
||||||
|
self.camera = bpy.context.object
|
||||||
|
|
||||||
|
# Add sun light
|
||||||
|
bpy.ops.object.light_add(type='SUN', location=(0, 0, 20))
|
||||||
|
light = bpy.context.object
|
||||||
|
light.data.energy = 5
|
||||||
|
|
||||||
|
# Setup world environment
|
||||||
|
world = bpy.context.scene.world
|
||||||
|
world.use_nodes = True
|
||||||
|
env_texture = world.node_tree.nodes.new('ShaderNodeTexEnvironment')
|
||||||
|
world.node_tree.links.new(env_texture.outputs[0], world.node_tree.nodes['Background'].inputs[0])
|
||||||
|
|
||||||
|
# Set render settings
|
||||||
|
scene = bpy.context.scene
|
||||||
|
scene.render.engine = 'CYCLES'
|
||||||
|
scene.render.resolution_x = 1920
|
||||||
|
scene.render.resolution_y = 1080
|
||||||
|
scene.render.fps = 30
|
||||||
|
scene.cycles.samples = 64
|
||||||
|
|
||||||
|
def load_gps_data(self, positions_file):
|
||||||
|
"""Load GPS data from JSON file"""
|
||||||
|
with open(positions_file, 'r') as f:
|
||||||
|
positions = json.load(f)
|
||||||
|
|
||||||
|
# Convert to numpy array for easier processing
|
||||||
|
coords = []
|
||||||
|
times = []
|
||||||
|
speeds = []
|
||||||
|
|
||||||
|
for pos in positions:
|
||||||
|
coords.append([pos['longitude'], pos['latitude'], pos.get('altitude', 0)])
|
||||||
|
times.append(pos['fixTime'])
|
||||||
|
speeds.append(pos.get('speed', 0) * 1.852) # Convert to km/h
|
||||||
|
|
||||||
|
return np.array(coords), times, speeds
|
||||||
|
|
||||||
|
def create_terrain_mesh(self, coords):
|
||||||
|
"""Create a simple terrain mesh based on GPS bounds"""
|
||||||
|
# Calculate bounds
|
||||||
|
min_lon, min_lat = coords[:, :2].min(axis=0)
|
||||||
|
max_lon, max_lat = coords[:, :2].max(axis=0)
|
||||||
|
|
||||||
|
# Expand bounds slightly
|
||||||
|
padding = 0.001
|
||||||
|
min_lon -= padding
|
||||||
|
min_lat -= padding
|
||||||
|
max_lon += padding
|
||||||
|
max_lat += padding
|
||||||
|
|
||||||
|
# Create terrain mesh
|
||||||
|
bpy.ops.mesh.primitive_plane_add(size=2, location=(0, 0, 0))
|
||||||
|
terrain = bpy.context.object
|
||||||
|
terrain.name = "Terrain"
|
||||||
|
|
||||||
|
# Scale terrain to match GPS bounds
|
||||||
|
lon_range = max_lon - min_lon
|
||||||
|
lat_range = max_lat - min_lat
|
||||||
|
scale_factor = max(lon_range, lat_range) * 100000 # Convert to reasonable scale
|
||||||
|
|
||||||
|
terrain.scale = (scale_factor, scale_factor, 1)
|
||||||
|
|
||||||
|
# Apply material
|
||||||
|
mat = bpy.data.materials.new(name="TerrainMaterial")
|
||||||
|
mat.use_nodes = True
|
||||||
|
mat.node_tree.nodes.clear()
|
||||||
|
|
||||||
|
# Add principled BSDF
|
||||||
|
bsdf = mat.node_tree.nodes.new(type='ShaderNodeBsdfPrincipled')
|
||||||
|
bsdf.inputs['Base Color'].default_value = (0.2, 0.5, 0.2, 1.0) # Green
|
||||||
|
bsdf.inputs['Roughness'].default_value = 0.8
|
||||||
|
|
||||||
|
material_output = mat.node_tree.nodes.new(type='ShaderNodeOutputMaterial')
|
||||||
|
mat.node_tree.links.new(bsdf.outputs['BSDF'], material_output.inputs['Surface'])
|
||||||
|
|
||||||
|
terrain.data.materials.append(mat)
|
||||||
|
|
||||||
|
return terrain
|
||||||
|
|
||||||
|
def create_gps_track_mesh(self, coords):
|
||||||
|
"""Create a 3D mesh for the GPS track"""
|
||||||
|
# Normalize coordinates to Blender scale
|
||||||
|
coords_normalized = self.normalize_coordinates(coords)
|
||||||
|
|
||||||
|
# Create curve from GPS points
|
||||||
|
curve_data = bpy.data.curves.new('GPSTrack', type='CURVE')
|
||||||
|
curve_data.dimensions = '3D'
|
||||||
|
curve_data.bevel_depth = 0.02
|
||||||
|
curve_data.bevel_resolution = 4
|
||||||
|
|
||||||
|
# Create spline
|
||||||
|
spline = curve_data.splines.new('BEZIER')
|
||||||
|
spline.bezier_points.add(len(coords_normalized) - 1)
|
||||||
|
|
||||||
|
for i, coord in enumerate(coords_normalized):
|
||||||
|
point = spline.bezier_points[i]
|
||||||
|
point.co = coord
|
||||||
|
point.handle_left_type = 'AUTO'
|
||||||
|
point.handle_right_type = 'AUTO'
|
||||||
|
|
||||||
|
# Create object from curve
|
||||||
|
track_obj = bpy.data.objects.new('GPSTrack', curve_data)
|
||||||
|
bpy.context.collection.objects.link(track_obj)
|
||||||
|
|
||||||
|
# Apply material
|
||||||
|
mat = bpy.data.materials.new(name="TrackMaterial")
|
||||||
|
mat.use_nodes = True
|
||||||
|
bsdf = mat.node_tree.nodes["Principled BSDF"]
|
||||||
|
bsdf.inputs['Base Color'].default_value = (1.0, 0.0, 0.0, 1.0) # Red
|
||||||
|
bsdf.inputs['Emission'].default_value = (1.0, 0.2, 0.2, 1.0)
|
||||||
|
bsdf.inputs['Emission Strength'].default_value = 2.0
|
||||||
|
|
||||||
|
track_obj.data.materials.append(mat)
|
||||||
|
|
||||||
|
return track_obj
|
||||||
|
|
||||||
|
def create_vehicle_model(self):
|
||||||
|
"""Create a simple vehicle model"""
|
||||||
|
# Create a simple car shape using cubes
|
||||||
|
bpy.ops.mesh.primitive_cube_add(size=0.1, location=(0, 0, 0.05))
|
||||||
|
vehicle = bpy.context.object
|
||||||
|
vehicle.name = "Vehicle"
|
||||||
|
vehicle.scale = (2, 1, 0.5)
|
||||||
|
|
||||||
|
# Apply material
|
||||||
|
mat = bpy.data.materials.new(name="VehicleMaterial")
|
||||||
|
mat.use_nodes = True
|
||||||
|
bsdf = mat.node_tree.nodes["Principled BSDF"]
|
||||||
|
bsdf.inputs['Base Color'].default_value = (0.0, 0.0, 1.0, 1.0) # Blue
|
||||||
|
bsdf.inputs['Metallic'].default_value = 0.5
|
||||||
|
bsdf.inputs['Roughness'].default_value = 0.2
|
||||||
|
|
||||||
|
vehicle.data.materials.append(mat)
|
||||||
|
|
||||||
|
return vehicle
|
||||||
|
|
||||||
|
def normalize_coordinates(self, coords):
|
||||||
|
"""Normalize GPS coordinates to Blender scale"""
|
||||||
|
# Center coordinates
|
||||||
|
center = coords.mean(axis=0)
|
||||||
|
coords_centered = coords - center
|
||||||
|
|
||||||
|
# Scale to reasonable size for Blender
|
||||||
|
scale_factor = 100
|
||||||
|
coords_scaled = coords_centered * scale_factor
|
||||||
|
|
||||||
|
# Convert to Vector objects
|
||||||
|
return [Vector((x, y, z)) for x, y, z in coords_scaled]
|
||||||
|
|
||||||
|
def animate_vehicle(self, vehicle, coords, times, speeds):
|
||||||
|
"""Create animation keyframes for vehicle movement"""
|
||||||
|
coords_normalized = self.normalize_coordinates(coords)
|
||||||
|
|
||||||
|
scene = bpy.context.scene
|
||||||
|
scene.frame_start = 1
|
||||||
|
scene.frame_end = len(coords_normalized) * 2 # 2 frames per GPS point
|
||||||
|
|
||||||
|
for i, (coord, speed) in enumerate(zip(coords_normalized, speeds)):
|
||||||
|
frame = i * 2 + 1
|
||||||
|
|
||||||
|
# Set location
|
||||||
|
vehicle.location = coord
|
||||||
|
vehicle.keyframe_insert(data_path="location", frame=frame)
|
||||||
|
|
||||||
|
# Calculate rotation based on direction
|
||||||
|
if i < len(coords_normalized) - 1:
|
||||||
|
next_coord = coords_normalized[i + 1]
|
||||||
|
direction = next_coord - coord
|
||||||
|
if direction.length > 0:
|
||||||
|
direction.normalize()
|
||||||
|
# Calculate rotation angle
|
||||||
|
angle = math.atan2(direction.y, direction.x)
|
||||||
|
vehicle.rotation_euler = Euler((0, 0, angle), 'XYZ')
|
||||||
|
vehicle.keyframe_insert(data_path="rotation_euler", frame=frame)
|
||||||
|
|
||||||
|
# Set interpolation mode
|
||||||
|
if vehicle.animation_data:
|
||||||
|
for fcurve in vehicle.animation_data.action.fcurves:
|
||||||
|
for keyframe in fcurve.keyframe_points:
|
||||||
|
keyframe.interpolation = 'BEZIER'
|
||||||
|
|
||||||
|
def animate_camera(self, coords):
|
||||||
|
"""Create smooth camera animation following the vehicle"""
|
||||||
|
coords_normalized = self.normalize_coordinates(coords)
|
||||||
|
|
||||||
|
# Create camera path
|
||||||
|
for i, coord in enumerate(coords_normalized):
|
||||||
|
frame = i * 2 + 1
|
||||||
|
|
||||||
|
# Position camera above and behind the vehicle
|
||||||
|
offset = Vector((0, -2, 3))
|
||||||
|
cam_location = coord + offset
|
||||||
|
|
||||||
|
self.camera.location = cam_location
|
||||||
|
self.camera.keyframe_insert(data_path="location", frame=frame)
|
||||||
|
|
||||||
|
# Look at the vehicle
|
||||||
|
direction = coord - cam_location
|
||||||
|
if direction.length > 0:
|
||||||
|
rot_quat = direction.to_track_quat('-Z', 'Y')
|
||||||
|
self.camera.rotation_euler = rot_quat.to_euler()
|
||||||
|
self.camera.keyframe_insert(data_path="rotation_euler", frame=frame)
|
||||||
|
|
||||||
|
def add_particles_effects(self, vehicle):
|
||||||
|
"""Add particle effects for enhanced visuals"""
|
||||||
|
# Add dust particles
|
||||||
|
bpy.context.view_layer.objects.active = vehicle
|
||||||
|
bpy.ops.object.modifier_add(type='PARTICLE_SYSTEM')
|
||||||
|
|
||||||
|
particles = vehicle.modifiers["ParticleSystem"].particle_system
|
||||||
|
particles.settings.count = 100
|
||||||
|
particles.settings.lifetime = 30
|
||||||
|
particles.settings.emit_from = 'FACE'
|
||||||
|
particles.settings.physics_type = 'NEWTON'
|
||||||
|
particles.settings.effector_weights.gravity = 0.1
|
||||||
|
|
||||||
|
# Set material for particles
|
||||||
|
particles.settings.material = 1
|
||||||
|
|
||||||
|
def render_animation(self, output_path, progress_callback=None):
|
||||||
|
"""Render the animation to video"""
|
||||||
|
scene = bpy.context.scene
|
||||||
|
|
||||||
|
# Set output settings
|
||||||
|
scene.render.filepath = output_path
|
||||||
|
scene.render.image_settings.file_format = 'FFMPEG'
|
||||||
|
scene.render.ffmpeg.format = 'MPEG4'
|
||||||
|
scene.render.ffmpeg.codec = 'H264'
|
||||||
|
|
||||||
|
# Render animation
|
||||||
|
total_frames = scene.frame_end - scene.frame_start + 1
|
||||||
|
|
||||||
|
for frame in range(scene.frame_start, scene.frame_end + 1):
|
||||||
|
scene.frame_set(frame)
|
||||||
|
|
||||||
|
# Render frame
|
||||||
|
frame_path = f"{output_path}_{frame:04d}.png"
|
||||||
|
scene.render.filepath = frame_path
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
|
||||||
|
# Update progress
|
||||||
|
if progress_callback:
|
||||||
|
progress = ((frame - scene.frame_start) / total_frames) * 100
|
||||||
|
progress_callback(progress, f"Rendering frame {frame}/{scene.frame_end}")
|
||||||
|
|
||||||
|
def create_gps_animation(self, positions_file, output_path, progress_callback=None):
|
||||||
|
"""Main method to create GPS animation in Blender"""
|
||||||
|
try:
|
||||||
|
# Load GPS data
|
||||||
|
coords, times, speeds = self.load_gps_data(positions_file)
|
||||||
|
|
||||||
|
# Create scene elements
|
||||||
|
terrain = self.create_terrain_mesh(coords)
|
||||||
|
track = self.create_gps_track_mesh(coords)
|
||||||
|
vehicle = self.create_vehicle_model()
|
||||||
|
|
||||||
|
# Create animations
|
||||||
|
self.animate_vehicle(vehicle, coords, times, speeds)
|
||||||
|
self.animate_camera(coords)
|
||||||
|
|
||||||
|
# Add effects
|
||||||
|
self.add_particles_effects(vehicle)
|
||||||
|
|
||||||
|
# Render animation
|
||||||
|
self.render_animation(output_path, progress_callback)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating Blender animation: {e}")
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(-1, f"Error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def generate_blender_animation(positions_file, output_folder, progress_callback=None):
|
||||||
|
"""
|
||||||
|
Convenience function to generate Blender animation
|
||||||
|
"""
|
||||||
|
animator = BlenderGPSAnimator(output_folder)
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
output_path = os.path.join(output_folder, f"blender_animation_{timestamp}")
|
||||||
|
|
||||||
|
success = animator.create_gps_animation(positions_file, output_path, progress_callback)
|
||||||
|
|
||||||
|
return f"{output_path}.mp4" if success else None
|
||||||
|
|
||||||
|
# Note: This script should be run from within Blender's Python environment
|
||||||
|
# or with Blender as a Python module (bpy)
|
||||||
@@ -12,4 +12,10 @@ numpy
|
|||||||
matplotlib
|
matplotlib
|
||||||
scipy
|
scipy
|
||||||
imageio
|
imageio
|
||||||
ffmpeg-python
|
ffmpeg-python
|
||||||
|
pydeck
|
||||||
|
plotly
|
||||||
|
dash
|
||||||
|
pandas
|
||||||
|
geopandas
|
||||||
|
bpy
|
||||||
@@ -1 +1 @@
|
|||||||
gAAAAABobfofcr10BIPwspryfc740kIyIDl3sH0B0Jb598Zc9boEPMP01OyKqPXI1Dcfrqu6KGUI0useWSTQanKWBjCLNY-jQZmGKvbRRWL03bVhFl0i_5qUwgmLNHMSSXZi5U9oXFo7
|
gAAAAABobmx0PnGbcR3Hxn93Z2r3z0dqZpHYGfWhJC7ko6QSMHLY_qoGsEZLrlLjjGrdjVOqSNVfwCP6_pAQ5QWbDRs6RoyZFPIA-vLFYpU9tUVC6pHCSSxvQimS_Thdj5WMIBlpTOWa
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
BIN
resources/projects/save1/frames/advanced_3d_frame_0000.png
Normal file
BIN
resources/projects/save1/frames/advanced_3d_frame_0000.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
BIN
resources/projects/save1/frames/advanced_3d_frame_0001.png
Normal file
BIN
resources/projects/save1/frames/advanced_3d_frame_0001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
BIN
resources/projects/save1/frames/advanced_3d_frame_0002.png
Normal file
BIN
resources/projects/save1/frames/advanced_3d_frame_0002.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
BIN
resources/projects/save1/frames/advanced_3d_frame_0003.png
Normal file
BIN
resources/projects/save1/frames/advanced_3d_frame_0003.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
BIN
resources/projects/save1/frames/advanced_3d_frame_0004.png
Normal file
BIN
resources/projects/save1/frames/advanced_3d_frame_0004.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
20
resources/projects/save1/pauses.json
Normal file
20
resources/projects/save1/pauses.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"start_time": "2025-07-08T04:51:13.000+00:00",
|
||||||
|
"end_time": "2025-07-08T13:20:50.000+00:00",
|
||||||
|
"duration_seconds": 30578,
|
||||||
|
"location": {
|
||||||
|
"latitude": 45.79908722222223,
|
||||||
|
"longitude": 24.085938333333335
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start_time": "2025-07-08T13:33:15.000+00:00",
|
||||||
|
"end_time": "2025-07-08T13:35:59.000+00:00",
|
||||||
|
"duration_seconds": 164,
|
||||||
|
"location": {
|
||||||
|
"latitude": 45.794045000000004,
|
||||||
|
"longitude": 24.13890055555556
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
22096
resources/projects/save1/positions.json
Normal file
22096
resources/projects/save1/positions.json
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -3,12 +3,15 @@ from kivy.uix.screenmanager import Screen
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
|
from datetime import datetime
|
||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
from kivy.properties import StringProperty, NumericProperty, AliasProperty
|
from kivy.properties import StringProperty, NumericProperty, AliasProperty
|
||||||
from py_scripts.utils import (
|
from py_scripts.utils import (
|
||||||
process_preview_util, optimize_route_entries_util
|
process_preview_util, optimize_route_entries_util
|
||||||
)
|
)
|
||||||
from py_scripts.video_3d_generator import generate_3d_video_animation
|
from py_scripts.video_3d_generator import generate_3d_video_animation
|
||||||
|
from py_scripts.advanced_3d_generator import Advanced3DGenerator
|
||||||
|
from py_scripts.blender_animator import BlenderGPSAnimator
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.button import Button
|
from kivy.uix.button import Button
|
||||||
from kivy.uix.label import Label
|
from kivy.uix.label import Label
|
||||||
@@ -238,89 +241,128 @@ class CreateAnimationScreen(Screen):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def show_video_generation_options(self):
|
def show_video_generation_options(self):
|
||||||
"""Show popup with video generation mode options"""
|
"""Show popup with video generation mode options including new advanced animations"""
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
from kivy.uix.button import Button
|
from kivy.uix.button import Button
|
||||||
from kivy.uix.label import Label
|
from kivy.uix.label import Label
|
||||||
|
|
||||||
layout = BoxLayout(orientation='vertical', spacing=15, padding=15)
|
layout = BoxLayout(orientation='vertical', spacing=12, padding=15)
|
||||||
|
|
||||||
# Title
|
# Title
|
||||||
title_label = Label(
|
title_label = Label(
|
||||||
text="Choose Video Generation Mode",
|
text="Choose Animation Style & Quality",
|
||||||
font_size=18,
|
font_size=20,
|
||||||
size_hint_y=None,
|
size_hint_y=None,
|
||||||
height=40,
|
height=40,
|
||||||
color=(1, 1, 1, 1)
|
color=(1, 1, 1, 1)
|
||||||
)
|
)
|
||||||
layout.add_widget(title_label)
|
layout.add_widget(title_label)
|
||||||
|
|
||||||
# Test mode description
|
# Classic 3D Mode
|
||||||
test_layout = BoxLayout(orientation='vertical', spacing=5)
|
classic_layout = BoxLayout(orientation='vertical', spacing=5)
|
||||||
test_title = Label(
|
classic_title = Label(
|
||||||
text="🏃♂️ 720p Test Mode (Fast)",
|
text="🏃♂️ Classic 3D (Original Pipeline)",
|
||||||
font_size=16,
|
font_size=16,
|
||||||
size_hint_y=None,
|
size_hint_y=None,
|
||||||
height=30,
|
height=30,
|
||||||
color=(0.2, 0.8, 0.2, 1)
|
color=(0.2, 0.8, 0.2, 1)
|
||||||
)
|
)
|
||||||
test_desc = Label(
|
classic_desc = Label(
|
||||||
text="• Resolution: 1280x720\n• Frame rate: 30 FPS\n• ~3x faster generation\n• Perfect for quick previews",
|
text="• Traditional OpenCV/PIL approach\n• Fast generation\n• Good for simple tracks\n• Test (720p) or Production (2K)",
|
||||||
font_size=12,
|
font_size=11,
|
||||||
size_hint_y=None,
|
size_hint_y=None,
|
||||||
height=80,
|
height=70,
|
||||||
color=(0.9, 0.9, 0.9, 1),
|
color=(0.9, 0.9, 0.9, 1),
|
||||||
halign="left",
|
halign="left",
|
||||||
valign="middle"
|
valign="middle"
|
||||||
)
|
)
|
||||||
test_desc.text_size = (None, None)
|
classic_desc.text_size = (None, None)
|
||||||
test_layout.add_widget(test_title)
|
classic_layout.add_widget(classic_title)
|
||||||
test_layout.add_widget(test_desc)
|
classic_layout.add_widget(classic_desc)
|
||||||
layout.add_widget(test_layout)
|
layout.add_widget(classic_layout)
|
||||||
|
|
||||||
# Test mode button
|
# Classic buttons
|
||||||
test_btn = Button(
|
classic_btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=45)
|
||||||
text="Generate 720p Test Video",
|
classic_test_btn = Button(
|
||||||
|
text="Classic 720p",
|
||||||
background_color=(0.2, 0.8, 0.2, 1),
|
background_color=(0.2, 0.8, 0.2, 1),
|
||||||
size_hint_y=None,
|
font_size=12
|
||||||
height=50,
|
|
||||||
font_size=14
|
|
||||||
)
|
)
|
||||||
layout.add_widget(test_btn)
|
classic_prod_btn = Button(
|
||||||
|
text="Classic 2K",
|
||||||
|
background_color=(0.3, 0.6, 0.3, 1),
|
||||||
|
font_size=12
|
||||||
|
)
|
||||||
|
classic_btn_layout.add_widget(classic_test_btn)
|
||||||
|
classic_btn_layout.add_widget(classic_prod_btn)
|
||||||
|
layout.add_widget(classic_btn_layout)
|
||||||
|
|
||||||
# Production mode description
|
# Advanced Pydeck/Plotly Mode
|
||||||
prod_layout = BoxLayout(orientation='vertical', spacing=5)
|
advanced_layout = BoxLayout(orientation='vertical', spacing=5)
|
||||||
prod_title = Label(
|
advanced_title = Label(
|
||||||
text="🎯 2K Production Mode (High Quality)",
|
text="🚀 Advanced 3D (Pydeck + Plotly)",
|
||||||
font_size=16,
|
font_size=16,
|
||||||
size_hint_y=None,
|
size_hint_y=None,
|
||||||
height=30,
|
height=30,
|
||||||
color=(0.8, 0.2, 0.2, 1)
|
color=(0.2, 0.6, 0.9, 1)
|
||||||
)
|
)
|
||||||
prod_desc = Label(
|
advanced_desc = Label(
|
||||||
text="• Resolution: 2560x1440\n• Frame rate: 60 FPS\n• Cinema-quality results\n• Ultra-detailed visuals",
|
text="• Professional geospatial visualization\n• Interactive 3D terrain\n• Advanced camera movements\n• High-quality animations",
|
||||||
font_size=12,
|
font_size=11,
|
||||||
size_hint_y=None,
|
size_hint_y=None,
|
||||||
height=80,
|
height=70,
|
||||||
color=(0.9, 0.9, 0.9, 1),
|
color=(0.9, 0.9, 0.9, 1),
|
||||||
halign="left",
|
halign="left",
|
||||||
valign="middle"
|
valign="middle"
|
||||||
)
|
)
|
||||||
prod_desc.text_size = (None, None)
|
advanced_desc.text_size = (None, None)
|
||||||
prod_layout.add_widget(prod_title)
|
advanced_layout.add_widget(advanced_title)
|
||||||
prod_layout.add_widget(prod_desc)
|
advanced_layout.add_widget(advanced_desc)
|
||||||
layout.add_widget(prod_layout)
|
layout.add_widget(advanced_layout)
|
||||||
|
|
||||||
# Production mode button
|
# Advanced button
|
||||||
prod_btn = Button(
|
advanced_btn = Button(
|
||||||
text="Generate 2K Production Video",
|
text="Generate Advanced 3D Animation",
|
||||||
background_color=(0.8, 0.2, 0.2, 1),
|
background_color=(0.2, 0.6, 0.9, 1),
|
||||||
size_hint_y=None,
|
size_hint_y=None,
|
||||||
height=50,
|
height=45,
|
||||||
font_size=14
|
font_size=13
|
||||||
)
|
)
|
||||||
layout.add_widget(prod_btn)
|
layout.add_widget(advanced_btn)
|
||||||
|
|
||||||
|
# Blender Cinema Mode
|
||||||
|
blender_layout = BoxLayout(orientation='vertical', spacing=5)
|
||||||
|
blender_title = Label(
|
||||||
|
text="<EFBFBD> Cinema Quality (Blender)",
|
||||||
|
font_size=16,
|
||||||
|
size_hint_y=None,
|
||||||
|
height=30,
|
||||||
|
color=(0.9, 0.6, 0.2, 1)
|
||||||
|
)
|
||||||
|
blender_desc = Label(
|
||||||
|
text="• Professional 3D rendering\n• Photorealistic visuals\n• Cinema-grade quality\n• Longer processing time",
|
||||||
|
font_size=11,
|
||||||
|
size_hint_y=None,
|
||||||
|
height=70,
|
||||||
|
color=(0.9, 0.9, 0.9, 1),
|
||||||
|
halign="left",
|
||||||
|
valign="middle"
|
||||||
|
)
|
||||||
|
blender_desc.text_size = (None, None)
|
||||||
|
blender_layout.add_widget(blender_title)
|
||||||
|
blender_layout.add_widget(blender_desc)
|
||||||
|
layout.add_widget(blender_layout)
|
||||||
|
|
||||||
|
# Blender button
|
||||||
|
blender_btn = Button(
|
||||||
|
text="Generate Blender Cinema Animation",
|
||||||
|
background_color=(0.9, 0.6, 0.2, 1),
|
||||||
|
size_hint_y=None,
|
||||||
|
height=45,
|
||||||
|
font_size=13
|
||||||
|
)
|
||||||
|
layout.add_widget(blender_btn)
|
||||||
|
|
||||||
# Cancel button
|
# Cancel button
|
||||||
cancel_btn = Button(
|
cancel_btn = Button(
|
||||||
@@ -333,23 +375,279 @@ class CreateAnimationScreen(Screen):
|
|||||||
layout.add_widget(cancel_btn)
|
layout.add_widget(cancel_btn)
|
||||||
|
|
||||||
popup = Popup(
|
popup = Popup(
|
||||||
title="Select Video Generation Mode",
|
title="Select Animation Style",
|
||||||
content=layout,
|
content=layout,
|
||||||
size_hint=(0.9, 0.8),
|
size_hint=(0.95, 0.9),
|
||||||
auto_dismiss=False
|
auto_dismiss=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def start_test_mode(instance):
|
def start_classic_test(instance):
|
||||||
popup.dismiss()
|
popup.dismiss()
|
||||||
self.generate_3d_video_test_mode()
|
self.generate_3d_video_test_mode()
|
||||||
|
|
||||||
def start_production_mode(instance):
|
def start_classic_production(instance):
|
||||||
popup.dismiss()
|
popup.dismiss()
|
||||||
self.generate_3d_video_production_mode()
|
self.generate_3d_video_production_mode()
|
||||||
|
|
||||||
|
def start_advanced_3d(instance):
|
||||||
|
popup.dismiss()
|
||||||
|
self.generate_advanced_3d_animation()
|
||||||
|
|
||||||
|
def start_blender_animation(instance):
|
||||||
|
popup.dismiss()
|
||||||
|
self.generate_blender_animation()
|
||||||
|
|
||||||
test_btn.bind(on_press=start_test_mode)
|
classic_test_btn.bind(on_press=start_classic_test)
|
||||||
prod_btn.bind(on_press=start_production_mode)
|
classic_prod_btn.bind(on_press=start_classic_production)
|
||||||
|
advanced_btn.bind(on_press=start_advanced_3d)
|
||||||
|
blender_btn.bind(on_press=start_blender_animation)
|
||||||
cancel_btn.bind(on_press=lambda x: popup.dismiss())
|
cancel_btn.bind(on_press=lambda x: popup.dismiss())
|
||||||
|
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
|
def generate_advanced_3d_animation(self):
|
||||||
|
"""Generate advanced 3D animation using Pydeck and Plotly"""
|
||||||
|
# Show processing popup
|
||||||
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
|
label = Label(text="Initializing advanced 3D animation...")
|
||||||
|
progress = ProgressBar(max=100, value=0)
|
||||||
|
layout.add_widget(label)
|
||||||
|
layout.add_widget(progress)
|
||||||
|
popup = Popup(
|
||||||
|
title="Generating Advanced 3D Animation",
|
||||||
|
content=layout,
|
||||||
|
size_hint=(0.9, None),
|
||||||
|
size=(0, 200),
|
||||||
|
auto_dismiss=False
|
||||||
|
)
|
||||||
|
popup.open()
|
||||||
|
|
||||||
|
def run_advanced_animation():
|
||||||
|
try:
|
||||||
|
# Update status
|
||||||
|
def update_status(progress_val, status_text):
|
||||||
|
def _update(dt):
|
||||||
|
progress.value = progress_val
|
||||||
|
label.text = status_text
|
||||||
|
Clock.schedule_once(_update, 0)
|
||||||
|
|
||||||
|
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
|
||||||
|
positions_path = os.path.join(project_folder, "positions.json")
|
||||||
|
|
||||||
|
if not os.path.exists(positions_path):
|
||||||
|
update_status(0, "Error: No GPS data found")
|
||||||
|
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||||
|
return
|
||||||
|
|
||||||
|
update_status(10, "Loading GPS data...")
|
||||||
|
|
||||||
|
# Check dependencies first
|
||||||
|
generator = Advanced3DGenerator(project_folder)
|
||||||
|
generator.check_dependencies()
|
||||||
|
|
||||||
|
update_status(20, "Processing GPS coordinates...")
|
||||||
|
df = generator.load_gps_data(positions_path)
|
||||||
|
|
||||||
|
update_status(40, "Creating 3D visualization frames...")
|
||||||
|
output_video_path = os.path.join(project_folder, f"{self.project_name}_advanced_3d_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||||
|
|
||||||
|
# Progress callback for the generator
|
||||||
|
def generator_progress(progress, message):
|
||||||
|
update_status(40 + (progress * 0.4), message) # Map 0-100% to 40-80%
|
||||||
|
|
||||||
|
update_status(80, "Rendering video...")
|
||||||
|
success = generator.generate_3d_animation(
|
||||||
|
positions_path,
|
||||||
|
output_video_path,
|
||||||
|
style='advanced',
|
||||||
|
progress_callback=generator_progress
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
update_status(100, "Advanced 3D animation complete!")
|
||||||
|
output_path = output_video_path
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to generate video")
|
||||||
|
|
||||||
|
def show_success(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
self.show_success_popup(
|
||||||
|
"Advanced 3D Animation Complete!",
|
||||||
|
f"Your high-quality 3D animation has been saved to:\n{output_path}",
|
||||||
|
output_path
|
||||||
|
)
|
||||||
|
|
||||||
|
Clock.schedule_once(show_success, 1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = str(e)
|
||||||
|
def show_error(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
self.show_error_popup("Advanced Animation Error", error_message)
|
||||||
|
|
||||||
|
Clock.schedule_once(show_error, 0)
|
||||||
|
|
||||||
|
# Schedule the animation generation
|
||||||
|
Clock.schedule_once(lambda dt: run_advanced_animation(), 0.5)
|
||||||
|
|
||||||
|
def generate_blender_animation(self):
|
||||||
|
"""Generate cinema-quality animation using Blender"""
|
||||||
|
# Show processing popup
|
||||||
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
|
label = Label(text="Initializing Blender rendering pipeline...")
|
||||||
|
progress = ProgressBar(max=100, value=0)
|
||||||
|
layout.add_widget(label)
|
||||||
|
layout.add_widget(progress)
|
||||||
|
popup = Popup(
|
||||||
|
title="Generating Blender Cinema Animation",
|
||||||
|
content=layout,
|
||||||
|
size_hint=(0.9, None),
|
||||||
|
size=(0, 200),
|
||||||
|
auto_dismiss=False
|
||||||
|
)
|
||||||
|
popup.open()
|
||||||
|
|
||||||
|
def run_blender_animation():
|
||||||
|
try:
|
||||||
|
# Update status
|
||||||
|
def update_status(progress_val, status_text):
|
||||||
|
def _update(dt):
|
||||||
|
progress.value = progress_val
|
||||||
|
label.text = status_text
|
||||||
|
Clock.schedule_once(_update, 0)
|
||||||
|
|
||||||
|
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
|
||||||
|
positions_path = os.path.join(project_folder, "positions.json")
|
||||||
|
|
||||||
|
if not os.path.exists(positions_path):
|
||||||
|
update_status(0, "Error: No GPS data found")
|
||||||
|
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||||
|
return
|
||||||
|
|
||||||
|
update_status(10, "Loading GPS data into Blender...")
|
||||||
|
|
||||||
|
# Check dependencies first
|
||||||
|
animator = BlenderGPSAnimator(project_folder)
|
||||||
|
animator.check_dependencies()
|
||||||
|
|
||||||
|
update_status(25, "Processing GPS coordinates...")
|
||||||
|
gps_data = animator.load_gps_data(positions_path)
|
||||||
|
|
||||||
|
output_video_path = os.path.join(project_folder, f"{self.project_name}_blender_cinema_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||||
|
|
||||||
|
# Progress callback for the animator
|
||||||
|
def animator_progress(progress, message):
|
||||||
|
update_status(25 + (progress * 0.6), message) # Map 0-100% to 25-85%
|
||||||
|
|
||||||
|
update_status(85, "Rendering cinema-quality video...")
|
||||||
|
success = animator.create_gps_animation(
|
||||||
|
positions_path,
|
||||||
|
output_video_path,
|
||||||
|
progress_callback=animator_progress
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
update_status(100, "Blender cinema animation complete!")
|
||||||
|
output_path = output_video_path
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to render Blender animation")
|
||||||
|
|
||||||
|
def show_success(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
self.show_success_popup(
|
||||||
|
"Blender Cinema Animation Complete!",
|
||||||
|
f"Your cinema-quality animation has been rendered to:\n{output_path}",
|
||||||
|
output_path
|
||||||
|
)
|
||||||
|
|
||||||
|
Clock.schedule_once(show_success, 1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = str(e)
|
||||||
|
def show_error(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
self.show_error_popup("Blender Animation Error", error_message)
|
||||||
|
|
||||||
|
Clock.schedule_once(show_error, 0)
|
||||||
|
|
||||||
|
# Schedule the animation generation
|
||||||
|
Clock.schedule_once(lambda dt: run_blender_animation(), 0.5)
|
||||||
|
|
||||||
|
def show_success_popup(self, title, message, file_path=None):
|
||||||
|
"""Show success popup with option to open file location"""
|
||||||
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
|
|
||||||
|
success_label = Label(
|
||||||
|
text=message,
|
||||||
|
text_size=(400, None),
|
||||||
|
halign="center",
|
||||||
|
valign="middle"
|
||||||
|
)
|
||||||
|
layout.add_widget(success_label)
|
||||||
|
|
||||||
|
button_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=50)
|
||||||
|
|
||||||
|
if file_path:
|
||||||
|
open_btn = Button(text="Open Folder", background_color=(0.2, 0.8, 0.2, 1))
|
||||||
|
open_btn.bind(on_press=lambda x: self.open_file_location(file_path))
|
||||||
|
button_layout.add_widget(open_btn)
|
||||||
|
|
||||||
|
ok_btn = Button(text="OK", background_color=(0.2, 0.6, 0.9, 1))
|
||||||
|
button_layout.add_widget(ok_btn)
|
||||||
|
layout.add_widget(button_layout)
|
||||||
|
|
||||||
|
popup = Popup(
|
||||||
|
title=title,
|
||||||
|
content=layout,
|
||||||
|
size_hint=(0.8, None),
|
||||||
|
size=(0, 250),
|
||||||
|
auto_dismiss=False
|
||||||
|
)
|
||||||
|
|
||||||
|
ok_btn.bind(on_press=lambda x: popup.dismiss())
|
||||||
|
popup.open()
|
||||||
|
|
||||||
|
def show_error_popup(self, title, message):
|
||||||
|
"""Show error popup"""
|
||||||
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
|
|
||||||
|
error_label = Label(
|
||||||
|
text=f"Error: {message}",
|
||||||
|
text_size=(400, None),
|
||||||
|
halign="center",
|
||||||
|
valign="middle",
|
||||||
|
color=(1, 0.3, 0.3, 1)
|
||||||
|
)
|
||||||
|
layout.add_widget(error_label)
|
||||||
|
|
||||||
|
ok_btn = Button(text="OK", background_color=(0.8, 0.2, 0.2, 1), size_hint_y=None, height=50)
|
||||||
|
layout.add_widget(ok_btn)
|
||||||
|
|
||||||
|
popup = Popup(
|
||||||
|
title=title,
|
||||||
|
content=layout,
|
||||||
|
size_hint=(0.8, None),
|
||||||
|
size=(0, 200),
|
||||||
|
auto_dismiss=False
|
||||||
|
)
|
||||||
|
|
||||||
|
ok_btn.bind(on_press=lambda x: popup.dismiss())
|
||||||
|
popup.open()
|
||||||
|
|
||||||
|
def open_file_location(self, file_path):
|
||||||
|
"""Open file location in system file manager"""
|
||||||
|
import subprocess
|
||||||
|
import platform
|
||||||
|
|
||||||
|
folder_path = os.path.dirname(file_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if platform.system() == "Linux":
|
||||||
|
subprocess.run(["xdg-open", folder_path])
|
||||||
|
elif platform.system() == "Darwin": # macOS
|
||||||
|
subprocess.run(["open", folder_path])
|
||||||
|
elif platform.system() == "Windows":
|
||||||
|
subprocess.run(["explorer", folder_path])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Could not open folder: {e}")
|
||||||
|
|
||||||
|
|||||||
81
test_relive_animation.py
Normal file
81
test_relive_animation.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for the improved Relive-style GPS animation
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Add the project directory to the path
|
||||||
|
sys.path.append('/home/pi/Desktop/traccar_animation')
|
||||||
|
|
||||||
|
from py_scripts.advanced_3d_generator import Advanced3DGenerator
|
||||||
|
|
||||||
|
def test_relive_animation():
|
||||||
|
"""Test the new Relive-style animation"""
|
||||||
|
|
||||||
|
# Find a project with GPS data
|
||||||
|
resources_folder = "/home/pi/Desktop/traccar_animation/resources"
|
||||||
|
projects_folder = os.path.join(resources_folder, "projects")
|
||||||
|
|
||||||
|
if not os.path.exists(projects_folder):
|
||||||
|
print("No projects folder found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Look for projects with positions.json
|
||||||
|
for project_name in os.listdir(projects_folder):
|
||||||
|
project_path = os.path.join(projects_folder, project_name)
|
||||||
|
positions_file = os.path.join(project_path, "positions.json")
|
||||||
|
|
||||||
|
if os.path.exists(positions_file):
|
||||||
|
print(f"🎬 Testing Relive-style animation with project: {project_name}")
|
||||||
|
|
||||||
|
# Check if positions file has data
|
||||||
|
try:
|
||||||
|
with open(positions_file, 'r') as f:
|
||||||
|
positions = json.load(f)
|
||||||
|
|
||||||
|
if len(positions) < 5:
|
||||||
|
print(f"❌ Project {project_name} has only {len(positions)} GPS points - skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"📍 Found {len(positions)} GPS points")
|
||||||
|
|
||||||
|
# Create generator
|
||||||
|
generator = Advanced3DGenerator(project_path)
|
||||||
|
|
||||||
|
# Progress callback
|
||||||
|
def progress_callback(progress, message):
|
||||||
|
print(f"Progress: {progress:.1f}% - {message}")
|
||||||
|
|
||||||
|
# Generate animation
|
||||||
|
output_video = os.path.join(project_path, f"relive_animation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||||
|
|
||||||
|
print(f"🚀 Starting Relive-style animation generation...")
|
||||||
|
success = generator.generate_3d_animation(
|
||||||
|
positions_file,
|
||||||
|
output_video,
|
||||||
|
style='advanced',
|
||||||
|
progress_callback=progress_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"✅ SUCCESS! Relive-style animation created: {output_video}")
|
||||||
|
print(f"📁 You can find your video at: {output_video}")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to generate animation")
|
||||||
|
|
||||||
|
return # Exit after first successful project
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error testing project {project_name}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("❌ No suitable projects found for testing")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("🎬 Testing Improved Relive-Style GPS Animation")
|
||||||
|
print("=" * 50)
|
||||||
|
test_relive_animation()
|
||||||
Reference in New Issue
Block a user