updated versions

This commit is contained in:
2025-07-09 16:39:51 +03:00
parent 507f526433
commit 35d3bb8442
21 changed files with 23620 additions and 136873 deletions

41
complete_video.py Normal file
View 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()

View 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")

View 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)

View File

@@ -13,3 +13,9 @@ matplotlib
scipy
imageio
ffmpeg-python
pydeck
plotly
dash
pandas
geopandas
bpy

View File

@@ -1 +1 @@
gAAAAABobfofcr10BIPwspryfc740kIyIDl3sH0B0Jb598Zc9boEPMP01OyKqPXI1Dcfrqu6KGUI0useWSTQanKWBjCLNY-jQZmGKvbRRWL03bVhFl0i_5qUwgmLNHMSSXZi5U9oXFo7
gAAAAABobmx0PnGbcR3Hxn93Z2r3z0dqZpHYGfWhJC7ko6QSMHLY_qoGsEZLrlLjjGrdjVOqSNVfwCP6_pAQ5QWbDRs6RoyZFPIA-vLFYpU9tUVC6pHCSSxvQimS_Thdj5WMIBlpTOWa

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

View 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
}
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,15 @@ from kivy.uix.screenmanager import Screen
import os
import json
import math
from datetime import datetime
from kivy.clock import Clock
from kivy.properties import StringProperty, NumericProperty, AliasProperty
from py_scripts.utils import (
process_preview_util, optimize_route_entries_util
)
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.button import Button
from kivy.uix.label import Label
@@ -238,89 +241,128 @@ class CreateAnimationScreen(Screen):
)
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.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
layout = BoxLayout(orientation='vertical', spacing=15, padding=15)
layout = BoxLayout(orientation='vertical', spacing=12, padding=15)
# Title
title_label = Label(
text="Choose Video Generation Mode",
font_size=18,
text="Choose Animation Style & Quality",
font_size=20,
size_hint_y=None,
height=40,
color=(1, 1, 1, 1)
)
layout.add_widget(title_label)
# Test mode description
test_layout = BoxLayout(orientation='vertical', spacing=5)
test_title = Label(
text="🏃‍♂️ 720p Test Mode (Fast)",
# Classic 3D Mode
classic_layout = BoxLayout(orientation='vertical', spacing=5)
classic_title = Label(
text="🏃‍♂️ Classic 3D (Original Pipeline)",
font_size=16,
size_hint_y=None,
height=30,
color=(0.2, 0.8, 0.2, 1)
)
test_desc = Label(
text="Resolution: 1280x720\n• Frame rate: 30 FPS\n~3x faster generation\nPerfect for quick previews",
font_size=12,
classic_desc = Label(
text="Traditional OpenCV/PIL approach\nFast generation\nGood for simple tracks\n• Test (720p) or Production (2K)",
font_size=11,
size_hint_y=None,
height=80,
height=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
test_desc.text_size = (None, None)
test_layout.add_widget(test_title)
test_layout.add_widget(test_desc)
layout.add_widget(test_layout)
classic_desc.text_size = (None, None)
classic_layout.add_widget(classic_title)
classic_layout.add_widget(classic_desc)
layout.add_widget(classic_layout)
# Test mode button
test_btn = Button(
text="Generate 720p Test Video",
# Classic buttons
classic_btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=45)
classic_test_btn = Button(
text="Classic 720p",
background_color=(0.2, 0.8, 0.2, 1),
size_hint_y=None,
height=50,
font_size=14
font_size=12
)
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
prod_layout = BoxLayout(orientation='vertical', spacing=5)
prod_title = Label(
text="🎯 2K Production Mode (High Quality)",
# Advanced Pydeck/Plotly Mode
advanced_layout = BoxLayout(orientation='vertical', spacing=5)
advanced_title = Label(
text="🚀 Advanced 3D (Pydeck + Plotly)",
font_size=16,
size_hint_y=None,
height=30,
color=(0.8, 0.2, 0.2, 1)
color=(0.2, 0.6, 0.9, 1)
)
prod_desc = Label(
text="Resolution: 2560x1440\n• Frame rate: 60 FPS\n• Cinema-quality results\n• Ultra-detailed visuals",
font_size=12,
advanced_desc = Label(
text="Professional geospatial visualization\n• Interactive 3D terrain\n• Advanced camera movements\n• High-quality animations",
font_size=11,
size_hint_y=None,
height=80,
height=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
prod_desc.text_size = (None, None)
prod_layout.add_widget(prod_title)
prod_layout.add_widget(prod_desc)
layout.add_widget(prod_layout)
advanced_desc.text_size = (None, None)
advanced_layout.add_widget(advanced_title)
advanced_layout.add_widget(advanced_desc)
layout.add_widget(advanced_layout)
# Production mode button
prod_btn = Button(
text="Generate 2K Production Video",
background_color=(0.8, 0.2, 0.2, 1),
# Advanced button
advanced_btn = Button(
text="Generate Advanced 3D Animation",
background_color=(0.2, 0.6, 0.9, 1),
size_hint_y=None,
height=50,
font_size=14
height=45,
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_btn = Button(
@@ -333,23 +375,279 @@ class CreateAnimationScreen(Screen):
layout.add_widget(cancel_btn)
popup = Popup(
title="Select Video Generation Mode",
title="Select Animation Style",
content=layout,
size_hint=(0.9, 0.8),
size_hint=(0.95, 0.9),
auto_dismiss=False
)
def start_test_mode(instance):
def start_classic_test(instance):
popup.dismiss()
self.generate_3d_video_test_mode()
def start_production_mode(instance):
def start_classic_production(instance):
popup.dismiss()
self.generate_3d_video_production_mode()
test_btn.bind(on_press=start_test_mode)
prod_btn.bind(on_press=start_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()
classic_test_btn.bind(on_press=start_classic_test)
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())
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
View 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()