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

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)