updated to generate trip

This commit is contained in:
2025-07-10 13:46:05 +03:00
parent 4fa7ed2a48
commit 29fd68f732
338 changed files with 12229 additions and 24735 deletions

View File

@@ -34,44 +34,45 @@ except ImportError:
print("Warning: pydeck not available. Install with: pip install pydeck")
try:
from moviepy import VideoFileClip, ImageSequenceClip
MOVIEPY_AVAILABLE = True
import cv2
OPENCV_AVAILABLE = True
except ImportError:
MOVIEPY_AVAILABLE = False
print("Warning: moviepy not available. Install with: pip install moviepy")
OPENCV_AVAILABLE = False
print("Warning: opencv-python not available. Install with: pip install opencv-python")
class Advanced3DGenerator:
class NavigationAnimationGenerator:
"""
Advanced 3D animation generator using Pydeck + Plotly + Blender pipeline
for high-quality GPS track visualizations
Professional navigation animation generator with satellite view and 3D camera following
Creates Google Earth-style entry scene and detailed terrain navigation
"""
def __init__(self, output_folder):
self.output_folder = output_folder
self.frames_folder = os.path.join(output_folder, "frames")
self.frames_folder = os.path.join(output_folder, "nav_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
# Navigation 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
self.entry_duration = 4 # seconds for Google Earth entry
self.camera_height_min = 1000 # meters
self.camera_height_max = 2000 # meters
self.follow_distance = 500 # meters behind navigation point
def check_dependencies(self):
"""Check if all required dependencies are available"""
missing = []
if not PANDAS_AVAILABLE:
missing.append("pandas, geopandas")
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 not OPENCV_AVAILABLE:
missing.append("opencv-python")
if missing:
raise ImportError(f"Missing required dependencies: {', '.join(missing)}. Please install them with: pip install {' '.join(missing)}")
@@ -79,7 +80,7 @@ class Advanced3DGenerator:
return True
def load_gps_data(self, positions_file):
"""Load and preprocess GPS data"""
"""Load and preprocess GPS data for navigation"""
with open(positions_file, 'r') as f:
positions = json.load(f)
@@ -92,11 +93,360 @@ class Advanced3DGenerator:
# Calculate speed and bearing
df['speed_kmh'] = df['speed'] * 1.852 # Convert knots to km/h
df['elevation'] = df.get('altitude', 0)
df['elevation'] = df.get('altitude', 100)
# Calculate distance between points
distances = []
# Calculate bearings and distances
bearings = []
distances = []
for i in range(len(df)):
if i == 0:
bearings.append(0)
distances.append(0)
else:
# Calculate bearing to next point
lat1, lon1 = df.iloc[i-1]['latitude'], df.iloc[i-1]['longitude']
lat2, lon2 = df.iloc[i]['latitude'], df.iloc[i]['longitude']
bearing = self.calculate_bearing(lat1, lon1, lat2, lon2)
distance = geodesic((lat1, lon1), (lat2, lon2)).meters
bearings.append(bearing)
distances.append(distance)
df['bearing'] = bearings
df['distance'] = distances
return df
def calculate_bearing(self, lat1, lon1, lat2, lon2):
"""Calculate bearing between two GPS points"""
lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2])
dlon = lon2 - lon1
y = math.sin(dlon) * math.cos(lat2)
x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon)
bearing = math.atan2(y, x)
bearing = math.degrees(bearing)
bearing = (bearing + 360) % 360
return bearing
def create_google_earth_entry_scene(self, df, frame_num):
"""Create Google Earth-style entry scene (zooming in from space)"""
if not PLOTLY_AVAILABLE:
raise ImportError("Plotly is required for entry scene")
# Get route bounds
center_lat = df['latitude'].mean()
center_lon = df['longitude'].mean()
# Entry animation: start from very high altitude and zoom in
total_entry_frames = self.entry_duration * self.fps
zoom_progress = frame_num / total_entry_frames
# Camera altitude decreases exponentially
start_altitude = 50000 # Start from 50km
end_altitude = 3000 # End at 3km
current_altitude = start_altitude * (1 - zoom_progress) + end_altitude * zoom_progress
# Create figure
fig = go.Figure()
# Add Earth-like surface with satellite imagery simulation
terrain_size = 0.5 - (0.4 * zoom_progress) # Zoom in effect
resolution = int(30 + (50 * zoom_progress)) # More detail as we zoom
lat_range = np.linspace(center_lat - terrain_size, center_lat + terrain_size, resolution)
lon_range = np.linspace(center_lon - terrain_size, center_lon + terrain_size, resolution)
lat_mesh, lon_mesh = np.meshgrid(lat_range, lon_range)
# Generate satellite-like terrain
terrain_heights = self.generate_satellite_terrain(lat_mesh, lon_mesh, center_lat, center_lon)
# Add terrain surface
fig.add_trace(
go.Surface(
x=lon_mesh,
y=lat_mesh,
z=terrain_heights,
colorscale=[
[0.0, 'rgb(0,100,0)'], # Deep green (forests)
[0.2, 'rgb(34,139,34)'], # Forest green
[0.4, 'rgb(255,215,0)'], # Gold (fields)
[0.6, 'rgb(139,69,19)'], # Brown (earth)
[0.8, 'rgb(105,105,105)'], # Gray (rock)
[1.0, 'rgb(255,255,255)'] # White (snow)
],
opacity=0.95,
showscale=False,
lighting=dict(
ambient=0.4,
diffuse=0.8,
specular=0.2
)
)
)
# Show partial route (fading in)
route_alpha = min(1.0, zoom_progress * 2)
if route_alpha > 0:
fig.add_trace(
go.Scatter3d(
x=df['longitude'],
y=df['latitude'],
z=df['elevation'] + 100,
mode='lines',
line=dict(
color='red',
width=8,
),
opacity=route_alpha,
name='Route'
)
)
# Camera position for entry effect
camera_distance = current_altitude / 10000
fig.update_layout(
title=dict(
text=f'Navigation Overview - Approaching Destination',
x=0.5,
font=dict(size=28, color='white', family="Arial Black")
),
scene=dict(
camera=dict(
eye=dict(x=0, y=-camera_distance, z=camera_distance),
center=dict(x=0, y=0, z=0),
up=dict(x=0, y=0, z=1)
),
xaxis=dict(visible=False),
yaxis=dict(visible=False),
zaxis=dict(visible=False),
aspectmode='cube',
bgcolor='rgb(0,0,50)', # Space-like background
),
paper_bgcolor='black',
showlegend=False,
width=1920,
height=1080,
margin=dict(l=0, r=0, t=60, b=0)
)
# Save frame
frame_path = os.path.join(self.frames_folder, f"NavEntry_{frame_num:04d}.png")
fig.write_image(frame_path, engine="kaleido")
return frame_path
def create_navigation_frame(self, df, current_index, frame_num):
"""Create detailed navigation frame with 3D following camera"""
if not PLOTLY_AVAILABLE:
raise ImportError("Plotly is required for navigation frames")
current_row = df.iloc[current_index]
current_lat = current_row['latitude']
current_lon = current_row['longitude']
current_alt = current_row['elevation']
current_speed = current_row['speed_kmh']
current_bearing = current_row['bearing']
# Get route progress
completed_route = df.iloc[:current_index + 1]
remaining_route = df.iloc[current_index:]
# Create detailed terrain around current position
terrain_radius = 0.01 # degrees around current position
resolution = 60
lat_range = np.linspace(current_lat - terrain_radius, current_lat + terrain_radius, resolution)
lon_range = np.linspace(current_lon - terrain_radius, current_lon + terrain_radius, resolution)
lat_mesh, lon_mesh = np.meshgrid(lat_range, lon_range)
# Generate high-detail satellite terrain
terrain_heights = self.generate_detailed_terrain(lat_mesh, lon_mesh, current_lat, current_lon)
# Create navigation display figure
fig = go.Figure()
# Add detailed terrain
fig.add_trace(
go.Surface(
x=lon_mesh,
y=lat_mesh,
z=terrain_heights,
colorscale=[
[0.0, 'rgb(34,139,34)'], # Forest green
[0.2, 'rgb(107,142,35)'], # Olive drab
[0.4, 'rgb(255,215,0)'], # Gold (fields)
[0.5, 'rgb(210,180,140)'], # Tan (roads/clearings)
[0.7, 'rgb(139,69,19)'], # Brown (earth)
[0.9, 'rgb(105,105,105)'], # Gray (rock)
[1.0, 'rgb(255,255,255)'] # White (peaks)
],
opacity=0.9,
showscale=False,
lighting=dict(
ambient=0.3,
diffuse=0.9,
specular=0.1
)
)
)
# Add completed route (green)
if len(completed_route) > 1:
fig.add_trace(
go.Scatter3d(
x=completed_route['longitude'],
y=completed_route['latitude'],
z=completed_route['elevation'] + 50,
mode='lines',
line=dict(color='lime', width=12),
name='Completed'
)
)
# Add remaining route (blue, semi-transparent)
if len(remaining_route) > 1:
fig.add_trace(
go.Scatter3d(
x=remaining_route['longitude'],
y=remaining_route['latitude'],
z=remaining_route['elevation'] + 50,
mode='lines',
line=dict(color='cyan', width=8),
opacity=0.6,
name='Remaining'
)
)
# Add navigation point (current vehicle position)
fig.add_trace(
go.Scatter3d(
x=[current_lon],
y=[current_lat],
z=[current_alt + 100],
mode='markers',
marker=dict(
color='red',
size=25,
symbol='diamond',
line=dict(color='white', width=4)
),
name='Vehicle'
)
)
# Add direction indicator
bearing_rad = math.radians(current_bearing)
arrow_length = 0.002
arrow_end_lat = current_lat + arrow_length * math.cos(bearing_rad)
arrow_end_lon = current_lon + arrow_length * math.sin(bearing_rad)
fig.add_trace(
go.Scatter3d(
x=[current_lon, arrow_end_lon],
y=[current_lat, arrow_end_lat],
z=[current_alt + 120, current_alt + 120],
mode='lines',
line=dict(color='yellow', width=15),
name='Direction'
)
)
# Calculate dynamic camera position for 3D following
# Camera follows behind and above at specified height
camera_height = self.camera_height_min + (self.camera_height_max - self.camera_height_min) * (current_speed / 100)
follow_distance_deg = 0.005 # degrees behind vehicle
# Position camera behind vehicle based on bearing
camera_bearing = (current_bearing + 180) % 360 # Opposite direction
camera_bearing_rad = math.radians(camera_bearing)
camera_lat = current_lat + follow_distance_deg * math.cos(camera_bearing_rad)
camera_lon = current_lon + follow_distance_deg * math.sin(camera_bearing_rad)
# Calculate relative camera position
camera_eye_x = (camera_lon - current_lon) * 100
camera_eye_y = (camera_lat - current_lat) * 100
camera_eye_z = camera_height / 1000
# Navigation info overlay
total_distance = sum(df['distance'])
completed_distance = sum(completed_route['distance'])
progress_percent = (completed_distance / total_distance) * 100 if total_distance > 0 else 0
fig.update_layout(
title=dict(
text=f'Navigation • Speed: {current_speed:.1f} km/h • Progress: {progress_percent:.1f}% • {current_row["timestamp"].strftime("%H:%M:%S")}',
x=0.5,
font=dict(size=20, color='white', family="Arial Black")
),
scene=dict(
camera=dict(
eye=dict(x=camera_eye_x, y=camera_eye_y, z=camera_eye_z),
center=dict(x=0, y=0, z=0),
up=dict(x=0, y=0, z=1)
),
xaxis=dict(visible=False),
yaxis=dict(visible=False),
zaxis=dict(visible=False),
aspectmode='manual',
aspectratio=dict(x=1, y=1, z=0.3),
bgcolor='rgb(135,206,235)' # Sky blue
),
paper_bgcolor='black',
showlegend=False,
width=1920,
height=1080,
margin=dict(l=0, r=0, t=50, b=0)
)
# Save frame
frame_path = os.path.join(self.frames_folder, f"Navigation_{frame_num:04d}.png")
fig.write_image(frame_path, engine="kaleido")
return frame_path
def generate_satellite_terrain(self, lat_mesh, lon_mesh, center_lat, center_lon):
"""Generate satellite-view realistic terrain for entry scene"""
# Convert to local coordinates
lat_m = (lat_mesh - center_lat) * 111000
lon_m = (lon_mesh - center_lon) * 111000 * np.cos(np.radians(center_lat))
# Base elevation with realistic variation
base_height = 200 + 50 * np.sin(lat_m / 5000) * np.cos(lon_m / 3000)
# Mountain ranges
mountains = 800 * np.exp(-((lat_m - 2000)**2 + (lon_m - 1000)**2) / (3000**2))
mountains += 600 * np.exp(-((lat_m + 1500)**2 + (lon_m + 2000)**2) / (2500**2))
# Hills and valleys
hills = 200 * np.sin(lat_m / 1000) * np.cos(lon_m / 1200)
valleys = -100 * np.exp(-((lat_m)**2 + (lon_m)**2) / (2000**2))
terrain = base_height + mountains + hills + valleys
return np.maximum(terrain, 50)
def generate_detailed_terrain(self, lat_mesh, lon_mesh, center_lat, center_lon):
"""Generate high-detail terrain for navigation view"""
# Convert to local coordinates
lat_m = (lat_mesh - center_lat) * 111000
lon_m = (lon_mesh - center_lon) * 111000 * np.cos(np.radians(center_lat))
# Base terrain
base = 150 + 30 * np.sin(lat_m / 500) * np.cos(lon_m / 400)
# Local features
hills = 100 * np.exp(-((lat_m - 300)**2 + (lon_m - 200)**2) / (200**2))
ridges = 80 * np.exp(-((lat_m + 200)**2 + (lon_m - 400)**2) / (300**2))
# Fine detail
detail = 20 * np.sin(lat_m / 50) * np.cos(lon_m / 60)
terrain = base + hills + ridges + detail
return np.maximum(terrain, 30)
for i in range(len(df)):
if i == 0:
@@ -557,14 +907,16 @@ class Advanced3DGenerator:
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...")
"""Create video from frames using OpenCV for better compatibility"""
print("Creating navigation animation video...")
if not frame_paths:
print("No frames to create video from")
return False
try:
import cv2
# Filter out None paths
valid_frames = [f for f in frame_paths if f and os.path.exists(f)]
@@ -574,46 +926,67 @@ class Advanced3DGenerator:
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)
# Update progress
if progress_callback:
progress_callback(30, "Reading frame dimensions...")
# Add smooth fade effects for professional look
clip = clip.fadein(0.5).fadeout(0.5)
# Read first frame to get dimensions
first_frame = cv2.imread(valid_frames[0])
if first_frame is None:
print("Error reading first frame")
return False
height, width, layers = first_frame.shape
# Update progress
if progress_callback:
progress_callback(50, "Encoding video with optimized settings...")
progress_callback(40, "Setting up video encoder...")
# 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
]
)
# Create video writer with OpenCV
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # You can also try 'XVID'
video_writer = cv2.VideoWriter(output_video_path, fourcc, self.fps, (width, height))
if not video_writer.isOpened():
print("Error: Could not open video writer")
return False
# Update progress
if progress_callback:
progress_callback(50, "Writing frames to video...")
# Write frames to video
total_frames = len(valid_frames)
for i, frame_path in enumerate(valid_frames):
frame = cv2.imread(frame_path)
if frame is not None:
video_writer.write(frame)
# Update progress periodically
if progress_callback and i % 10 == 0:
progress_percent = 50 + (i / total_frames) * 40 # 50-90%
progress_callback(progress_percent, f"Writing frame {i+1}/{total_frames}...")
# Clean up
video_writer.release()
cv2.destroyAllWindows()
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
# Verify the video was created
if os.path.exists(output_video_path) and os.path.getsize(output_video_path) > 0:
print(f"✅ Navigation animation video saved to: {output_video_path}")
file_size = os.path.getsize(output_video_path) / (1024 * 1024) # MB
print(f"📊 Video info: {len(valid_frames)} frames, {self.fps} FPS, {file_size:.1f} MB")
return True
else:
print("Error: Video file was not created properly")
return False
except Exception as e:
print(f"Error creating video: {e}")
if progress_callback:
progress_callback(-1, f"Error: {e}")
return False
def cleanup_frames(self):
@@ -623,38 +996,7 @@ class Advanced3DGenerator:
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 create_google_earth_frame(self, df, current_index, frame_num):
"""
@@ -887,42 +1229,4 @@ class Advanced3DGenerator:
return terrain
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")

File diff suppressed because it is too large Load Diff