Compare commits

...

2 Commits

Author SHA1 Message Date
911143dfc5 updated strategi 2025-07-10 13:50:44 +03:00
29fd68f732 updated to generate trip 2025-07-10 13:46:05 +03:00
35 changed files with 2402 additions and 24735 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
# Ignore the virtual environment folder
track/
track/
resurces/projects/

View File

@@ -1,45 +0,0 @@
# Project Cleanup Summary
## What Was Cleaned Up
### Moved to `junk_files/`
- Documentation files (*.md) that were cluttering the root directory
- `3D_VIDEO_DOCUMENTATION.md`
- `PAUSE_EDIT_IMPROVEMENTS.md`
- `PROJECT_MODERNIZATION_SUMMARY.md`
- `TEST_MODE_DOCUMENTATION.md`
### Removed
- All `__pycache__` directories and compiled Python bytecode files
- Duplicate and test files that were no longer needed
### Fixed
- Fixed typo in requirements.txt (`reqirements.txt` was corrected to `requirements.txt`)
- Ensured proper import structure (app uses `py_scripts.video_3d_generator` correctly)
## Current Clean Structure
```
traccar_animation/
├── .git/ # Git repository files
├── .gitignore # Git ignore rules
├── config.py # Application configuration
├── main.py # Main application entry point
├── traccar.kv # Kivy UI layout file
├── requirements.txt # Python dependencies (fixed)
├── py_scripts/ # Python modules
│ ├── __init__.py
│ ├── utils.py
│ ├── video_3d_generator.py
│ └── webview.py
├── screens/ # Kivy screen modules
├── resources/ # Application resources
├── track/ # Virtual environment
└── junk_files/ # Non-essential files moved here
```
## Verification
- ✅ Utils module imports correctly
- ✅ Video 3D generator module imports correctly
- ✅ No duplicate files remain
- ✅ All dependencies properly listed in requirements.txt
- ✅ Clean project structure maintained

View File

@@ -1,105 +0,0 @@
# Traccar Animation App - Modernization Complete
## Project Overview
The Traccar Animation App has been successfully modernized with enhanced 3D video animation capabilities, improved code structure, and streamlined codebase.
## Completed Modernization Tasks
### 1. Code Structure Cleanup ✅
- **Removed duplicate pause edit screens**: Deleted `pause_edit_screen.py` and `pause_edit_screen_legacy.py`
- **Single source of truth**: Only `pause_edit_screen_improved.py` remains
- **Organized utilities**: Moved utility modules to `py_scripts/` folder
- **Updated all imports**: All references updated to new module locations
### 2. Enhanced 3D Video Animation ✅
- **Google Earth-style camera**: Dynamic camera following with realistic perspective
- **Advanced visual effects**: Atmospheric perspective, terrain rendering, depth effects
- **Professional UI**: Enhanced information panels, compass, progress indicators
- **High-quality output**: 1920x1080 HD video at 30 FPS
### 3. Project Structure Improvements ✅
```
traccar_animation/
├── main.py # Main application entry
├── config.py # Configuration management
├── traccar.kv # UI layout definitions
├── reqirements.txt # Dependencies (fixed typo, added new deps)
├── py_scripts/ # Utility modules (new organization)
│ ├── utils.py # Core utilities
│ ├── video_3d_generator.py # Enhanced 3D video engine
│ ├── webview.py # Web integration
│ └── 3D_VIDEO_DOCUMENTATION.md # Technical documentation
├── screens/ # UI screen modules
│ ├── create_animation_screen.py
│ ├── get_trip_from_server.py
│ ├── home_screen.py
│ ├── login_screen.py
│ ├── pause_edit_screen_improved.py # Single pause edit implementation
│ └── settings_screen.py
└── resources/ # Static resources and data
├── images/
├── projects/
└── trip_archive/
```
### 4. Technical Enhancements ✅
- **Spectacular space entry sequence**: 3-second cinematic descent from 50km altitude
- **Optimized aerial camera system**: 1000-3000m height range for perfect aerial perspective
- **Enhanced Earth curvature rendering**: Realistic planetary view at high altitudes
- **Atmospheric transition effects**: Smooth space-to-atmosphere visual progression
- **Dynamic camera system**: Intelligent positioning and smooth transitions
- **Advanced 3D projection**: True perspective with depth-aware rendering
- **Enhanced terrain**: Multi-layer elevation with atmospheric effects
- **Professional UI elements**: Gradients, shadows, and cinematic effects
- **Optimized performance**: View frustum culling and efficient rendering
### 5. Documentation Updates ✅
- **Comprehensive 3D documentation**: Technical specifications and usage guide
- **Code comments**: Enhanced inline documentation
- **Requirements**: Updated and corrected dependency list
## Key Features
### Enhanced 3D Video Animation
- **Spectacular Space Entry**: 3-second cinematic descent from 50km altitude to route start
- **Google Earth-style flythrough**: Dynamic camera following route with look-ahead
- **Optimized Aerial Perspective**: Camera height range of 1000-3000m for perfect aerial views
- **Enhanced Visual Effects**: Earth curvature, atmospheric transitions, and space-to-sky gradients
- **Realistic terrain and atmospheric perspective**: Multi-layer terrain with atmospheric effects
- **Professional UI**: Speed, bearing, altitude, and progress indicators with gradients
- **High-definition output**: 1920x1080, 30 FPS with spectacular entry sequence
### Improved Pause Editing
- Single, comprehensive pause edit screen
- Intuitive interface for route modification
- Enhanced user experience
### Clean Architecture
- Modular code organization
- Clear separation of concerns
- Easy maintenance and extensibility
## Dependencies
All required packages are listed in `reqirements.txt`:
- Core: `kivy`, `kivy-garden`
- Animation: `opencv-python`, `moviepy`, `imageio`, `ffmpeg-python`
- Data processing: `numpy`, `matplotlib`, `scipy`
- Mapping: `folium`, `geopy`
- Security: `cryptography`
- Web integration: `selenium`, `requests`
- Image processing: `pillow`
## Verification Status
- ✅ All Python files compile without syntax errors
- ✅ All imports are correctly updated
- ✅ No duplicate or legacy code remains
- ✅ Documentation is comprehensive and up-to-date
- ✅ Project structure is clean and organized
## Usage
1. Install dependencies: `pip install -r reqirements.txt`
2. Run the application: `python main.py`
3. Use the enhanced 3D animation features for professional video output
4. Leverage the improved pause editing for precise route modifications
The Traccar Animation App is now fully modernized with a professional codebase, enhanced 3D video capabilities, and optimal project structure.

View File

@@ -1,108 +0,0 @@
# 3D Video Generation Test Mode
## Overview
The 3D video generation now supports two modes to balance quality and generation speed:
### 🏃‍♂️ 720p Test Mode (Fast)
- **Resolution**: 1280x720 pixels
- **Frame Rate**: 30 FPS
- **Entry Sequence**: 60 frames (2 seconds)
- **Route Frames**: 2x per GPS point
- **Generation Speed**: ~3x faster than production mode
- **File Size**: ~1/4 of production mode
- **Best For**: Quick previews, debugging routes, testing changes
### 🎯 2K Production Mode (High Quality)
- **Resolution**: 2560x1440 pixels (2K)
- **Frame Rate**: 60 FPS
- **Entry Sequence**: 120 frames (4 seconds)
- **Route Frames**: 3x per GPS point
- **Generation Speed**: Full quality processing
- **File Size**: Full size for maximum quality
- **Best For**: Final videos, presentations, high-quality output
## How to Use
### In the App UI
1. Click "Generate 3D Video" button
2. Choose from the popup:
- **"Generate 720p Test Video"** for fast testing
- **"Generate 2K Production Video"** for final quality
### In Code
```python
# Test mode (720p, faster)
generate_3d_video_animation(project_name, resources_folder, label, progress, popup, clock, test_mode=True)
# Production mode (2K, high quality)
generate_3d_video_animation(project_name, resources_folder, label, progress, popup, clock, test_mode=False)
# Or use convenience functions
generate_3d_video_animation_test_mode(...)
generate_3d_video_animation_production_mode(...)
```
## Performance Comparison
| Aspect | 720p Test Mode | 2K Production Mode |
|--------|----------------|-------------------|
| Resolution | 1280x720 | 2560x1440 |
| Total Pixels | ~0.9 megapixels | ~3.7 megapixels |
| Frame Rate | 30 FPS | 60 FPS |
| Space Entry | 2 seconds | 4 seconds |
| Processing Time | ~3x faster | Full quality |
| File Size | ~1/4 size | Full size |
| Quality | Good for preview | Cinema quality |
## When to Use Each Mode
### Use 720p Test Mode When:
- ✅ Testing route visualization
- ✅ Debugging GPS data issues
- ✅ Iterating on video parameters
- ✅ Quick previews for clients
- ✅ Development and testing
- ✅ Limited storage space
- ✅ Faster upload/sharing needed
### Use 2K Production Mode When:
- ✅ Creating final deliverable videos
- ✅ Professional presentations
- ✅ High-quality demos
- ✅ Maximum visual impact needed
- ✅ Detailed route analysis required
- ✅ Large screen display planned
## File Naming Convention
Generated videos will include the mode in the filename:
- Test mode: `project_720p_test_20250708_142815.mp4`
- Production mode: `project_2K_production_20250708_142815.mp4`
## Technical Details
### Test Mode Optimizations:
- Reduced frame generation (60 vs 120 for entry)
- Lower resolution reduces processing per frame
- Fewer intermediate frames per GPS point
- 30 FPS reduces total frame count
- Optimized rendering pipeline
### Production Mode Features:
- Ultra-high resolution Earth rendering
- Extended space entry sequence
- Maximum detail in atmospheric effects
- Professional-grade visual effects
- Cinema-quality color grading
- Smooth 60 FPS motion
## Tips for Best Results
1. **Start with Test Mode**: Always preview your route in 720p test mode first
2. **Iterate Quickly**: Use test mode to adjust route parameters
3. **Final Production**: Once satisfied, generate the 2K production version
4. **Storage Planning**: Test mode files are ~25% the size of production files
5. **Time Management**: Test mode generates ~3x faster than production mode
This dual-mode approach allows for rapid iteration during development while maintaining the ability to produce ultra-high-quality final videos.

View File

View File

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""
Test script for Google Earth-style flythrough animation
"""
import os
import sys
sys.path.append('/home/pi/Desktop/traccar_animation')
from py_scripts.advanced_3d_generator import Advanced3DGenerator
from datetime import datetime
def test_google_earth_animation():
"""Test the new Google Earth flythrough animation"""
# Find a project with GPS data
projects_folder = "/home/pi/Desktop/traccar_animation/resources/projects"
if not os.path.exists(projects_folder):
print("Projects folder not found!")
return
# Look for projects
projects = [d for d in os.listdir(projects_folder) if os.path.isdir(os.path.join(projects_folder, d))]
if not projects:
print("No projects found!")
return
# Use the first project found
project_name = projects[0]
project_folder = os.path.join(projects_folder, project_name)
positions_file = os.path.join(project_folder, "positions.json")
if not os.path.exists(positions_file):
print(f"No positions.json found in project {project_name}")
return
print(f"Testing Google Earth animation with project: {project_name}")
# Create generator
generator = Advanced3DGenerator(project_folder)
# Check dependencies
try:
generator.check_dependencies()
print("✅ All dependencies available")
except Exception as e:
print(f"❌ Dependency error: {e}")
return
# Generate Google Earth-style animation
output_video = os.path.join(project_folder, f"{project_name}_google_earth_test_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
def progress_callback(progress, message):
print(f"Progress: {progress:.1f}% - {message}")
try:
print("Starting Google Earth flythrough generation...")
success = generator.generate_3d_animation(
positions_file,
output_video,
style='google_earth',
progress_callback=progress_callback
)
if success and os.path.exists(output_video):
print(f"✅ SUCCESS! Google Earth flythrough created: {output_video}")
# Get file size
file_size = os.path.getsize(output_video) / (1024 * 1024) # MB
print(f"📹 Video size: {file_size:.1f} MB")
else:
print("❌ Failed to create video")
except Exception as e:
print(f"❌ Error during generation: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
test_google_earth_animation()

View File

View File

View File

View File

View File

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

View File

@@ -6,7 +6,6 @@ selenium
pillow
geopy
opencv-python
moviepy
requests
numpy
matplotlib

View File

@@ -1 +1 @@
gAAAAABobmx0PnGbcR3Hxn93Z2r3z0dqZpHYGfWhJC7ko6QSMHLY_qoGsEZLrlLjjGrdjVOqSNVfwCP6_pAQ5QWbDRs6RoyZFPIA-vLFYpU9tUVC6pHCSSxvQimS_Thdj5WMIBlpTOWa
gAAAAABob4-usKjps0vEVupB8FIJ3tKqoOeedzOUpt16NICbpi1ejKoSwqDvH7eIAPaZCOkfbPC6gjJGTo2yxt4BOPg1yzg_-Xanpl5iL1Y2mIRxWag-5cWhDNiqZo3bEZMqZ3M875O-

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

View File

@@ -1,20 +0,0 @@
[
{
"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

@@ -9,8 +9,7 @@ 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.advanced_3d_generator import NavigationAnimationGenerator
from py_scripts.blender_animator import BlenderGPSAnimator
from kivy.uix.popup import Popup
from kivy.uix.button import Button
@@ -51,8 +50,6 @@ class CreateAnimationScreen(Screen):
count = 0
self.ids.route_entries_label.text = f"Your route has {count} entries,"
def open_rename_popup(self):
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
@@ -131,7 +128,6 @@ class CreateAnimationScreen(Screen):
on_save=lambda: self.on_pre_enter()
)
def preview_route(self):
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
@@ -168,291 +164,16 @@ class CreateAnimationScreen(Screen):
0.5
)
def open_pauses_popup(self):
"""Navigate to the pause edit screen"""
pause_edit_screen = self.manager.get_screen("pause_edit")
pause_edit_screen.set_project_and_callback(self.project_name, self.on_pre_enter)
self.manager.current = "pause_edit"
def generate_3d_video(self):
"""Show video generation mode selection popup"""
self.show_video_generation_options()
def generate_3d_video_test_mode(self):
"""Generate a 3D video animation in 720p test mode for faster processing"""
# Show processing popup with test mode indication
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Preparing 720p test video generation...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Generating 3D Video Animation (720p Test Mode)",
content=layout,
size_hint=(0.9, None),
size=(0, 200),
auto_dismiss=False
)
popup.open()
# Schedule the 3D video generation in test mode
Clock.schedule_once(
lambda dt: generate_3d_video_animation(
self.project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
Clock,
test_mode=True # Enable test mode
),
0.5
)
def generate_3d_video_production_mode(self):
"""Generate a 3D video animation in 2K production mode for high quality"""
# Show processing popup with production mode indication
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Preparing 2K production video generation...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Generating 3D Video Animation (2K Production Mode)",
content=layout,
size_hint=(0.9, None),
size=(0, 200),
auto_dismiss=False
)
popup.open()
# Schedule the 3D video generation in production mode
Clock.schedule_once(
lambda dt: generate_3d_video_animation(
self.project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
Clock,
test_mode=False # Disable test mode for production
),
0.5
)
def show_video_generation_options(self):
"""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=12, padding=15)
# Title
title_label = Label(
text="Choose Animation Style & Quality",
font_size=20,
size_hint_y=None,
height=40,
color=(1, 1, 1, 1)
)
layout.add_widget(title_label)
# 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)
)
classic_desc = Label(
text="• Traditional OpenCV/PIL approach\n• Fast generation\n• Good for simple tracks\n• Test (720p) or Production (2K)",
font_size=11,
size_hint_y=None,
height=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
classic_desc.text_size = (None, None)
classic_layout.add_widget(classic_title)
classic_layout.add_widget(classic_desc)
layout.add_widget(classic_layout)
# 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),
font_size=12
)
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)
# 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.2, 0.6, 0.9, 1)
)
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=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
advanced_desc.text_size = (None, None)
advanced_layout.add_widget(advanced_title)
advanced_layout.add_widget(advanced_desc)
layout.add_widget(advanced_layout)
# Advanced button
advanced_btn = Button(
text="Generate Advanced 3D Animation",
background_color=(0.2, 0.6, 0.9, 1),
size_hint_y=None,
height=45,
font_size=13
)
layout.add_widget(advanced_btn)
# Google Earth Flythrough Mode
google_earth_layout = BoxLayout(orientation='vertical', spacing=5)
google_earth_title = Label(
text="🌍 Google Earth Flythrough",
font_size=16,
size_hint_y=None,
height=30,
color=(0.1, 0.8, 0.1, 1)
)
google_earth_desc = Label(
text="• Realistic 3D terrain with mountains\n• Cinematic camera following at 1000-2000m\n• Google Earth-style flythrough\n• Professional geographic animation",
font_size=11,
size_hint_y=None,
height=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
google_earth_desc.text_size = (None, None)
google_earth_layout.add_widget(google_earth_title)
google_earth_layout.add_widget(google_earth_desc)
layout.add_widget(google_earth_layout)
# Google Earth button
google_earth_btn = Button(
text="Generate Google Earth Flythrough",
background_color=(0.1, 0.8, 0.1, 1),
size_hint_y=None,
height=45,
font_size=13
)
layout.add_widget(google_earth_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(
text="Cancel",
background_color=(0.5, 0.5, 0.5, 1),
size_hint_y=None,
height=40,
font_size=12
)
layout.add_widget(cancel_btn)
popup = Popup(
title="Select Animation Style",
content=layout,
size_hint=(0.95, 0.9),
auto_dismiss=False
)
def start_classic_test(instance):
popup.dismiss()
self.generate_3d_video_test_mode()
def start_classic_production(instance):
popup.dismiss()
self.generate_3d_video_production_mode()
def start_advanced_3d(instance):
popup.dismiss()
self.generate_advanced_3d_animation()
def start_google_earth(instance):
popup.dismiss()
self.generate_google_earth_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)
google_earth_btn.bind(on_press=start_google_earth)
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"""
def generate_google_earth_animation(self):
"""Generate Google Earth-style flythrough animation using NavigationAnimationGenerator"""
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Initializing advanced 3D animation...")
label = Label(text="Initializing Google Earth flythrough...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Generating Advanced 3D Animation",
title="Generating Google Earth Flythrough",
content=layout,
size_hint=(0.9, None),
size=(0, 200),
@@ -460,7 +181,7 @@ class CreateAnimationScreen(Screen):
)
popup.open()
def run_advanced_animation():
def run_google_earth_animation():
try:
# Update status
def update_status(progress_val, status_text):
@@ -480,38 +201,38 @@ class CreateAnimationScreen(Screen):
update_status(10, "Loading GPS data...")
# Check dependencies first
generator = Advanced3DGenerator(project_folder)
generator = NavigationAnimationGenerator(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")
update_status(40, "Creating Google Earth flythrough...")
output_video_path = os.path.join(project_folder, f"{self.project_name}_google_earth_{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(40 + (progress * 0.5), message) # Map 0-100% to 40-90%
update_status(80, "Rendering video...")
success = generator.generate_3d_animation(
positions_path,
output_video_path,
style='advanced',
progress_callback=generator_progress
)
update_status(90, "Creating flythrough video...")
success = generator.generate_frames(positions_path, style='google_earth', progress_callback=generator_progress)
if success:
update_status(100, "Advanced 3D animation complete!")
output_path = output_video_path
if success and len(success) > 0:
update_status(95, "Rendering final video...")
video_success = generator.create_video(success, output_video_path, generator_progress)
if video_success:
update_status(100, "Google Earth flythrough complete!")
output_path = output_video_path
else:
raise Exception("Failed to create video from frames")
else:
raise Exception("Failed to generate video")
raise Exception("Failed to generate frames")
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}",
"Google Earth Flythrough Complete!",
f"Your Google Earth-style flythrough has been saved to:\n{output_path}",
output_path
)
@@ -521,13 +242,13 @@ class CreateAnimationScreen(Screen):
error_message = str(e)
def show_error(dt):
popup.dismiss()
self.show_error_popup("Advanced Animation Error", error_message)
self.show_error_popup("Google Earth Animation Error", error_message)
Clock.schedule_once(show_error, 0)
# Schedule the animation generation
Clock.schedule_once(lambda dt: run_advanced_animation(), 0.5)
Clock.schedule_once(lambda dt: run_google_earth_animation(), 0.5)
def generate_blender_animation(self):
"""Generate cinema-quality animation using Blender"""
# Show processing popup
@@ -588,13 +309,13 @@ class CreateAnimationScreen(Screen):
update_status(100, "Blender cinema animation complete!")
output_path = output_video_path
else:
raise Exception("Failed to render Blender animation")
raise Exception("Failed to generate 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}",
f"Your cinema-quality animation has been saved to:\n{output_path}",
output_path
)
@@ -610,26 +331,34 @@ class CreateAnimationScreen(Screen):
# Schedule the animation generation
Clock.schedule_once(lambda dt: run_blender_animation(), 0.5)
def generate_google_earth_animation(self):
"""Generate Google Earth-style flythrough animation with terrain"""
def generate_progressive_3d_animation(self):
"""Generate a progressive 3D animation that builds the trip point by point"""
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Initializing Google Earth flythrough...")
label = Label(text="Initializing progressive 3D animation...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Generating Google Earth Flythrough",
title="Generating Progressive 3D Animation",
content=layout,
size_hint=(0.9, None),
size=(0, 200),
auto_dismiss=False
)
popup.open()
def run_google_earth_animation():
def run_progressive_animation():
try:
# Import here to avoid startup delays
import matplotlib
matplotlib.use('Agg') # Use non-interactive backend
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
import cv2 # Use OpenCV instead of MoviePy
# Update status
def update_status(progress_val, status_text):
def _update(dt):
@@ -645,132 +374,360 @@ class CreateAnimationScreen(Screen):
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
return
update_status(10, "Checking dependencies...")
update_status(10, "Loading GPS data...")
# Check dependencies first
generator = Advanced3DGenerator(project_folder)
generator.check_dependencies()
# Load GPS data
with open(positions_path, 'r') as f:
positions = json.load(f)
update_status(20, "Loading GPS data...")
df = generator.load_gps_data(positions_path)
if len(positions) < 2:
update_status(0, "Error: Need at least 2 GPS points")
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
return
update_status(30, "Generating terrain and camera flythrough...")
output_video_path = os.path.join(project_folder, f"{self.project_name}_google_earth_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
update_status(20, "Processing GPS coordinates...")
# Progress callback for the generator
def generator_progress(progress, message):
update_status(30 + (progress * 0.5), message) # Map 0-100% to 30-80%
# Extract coordinates and timestamps
lats = [pos['latitude'] for pos in positions]
lons = [pos['longitude'] for pos in positions]
alts = [pos.get('altitude', 0) for pos in positions]
timestamps = [pos.get('fixTime', '') for pos in positions]
update_status(80, "Creating flythrough video...")
success = generator.generate_3d_animation(
positions_path,
output_video_path,
style='google_earth',
progress_callback=generator_progress
)
# Convert to numpy arrays for easier manipulation
lats = np.array(lats)
lons = np.array(lons)
alts = np.array(alts)
if success:
update_status(100, "Google Earth flythrough complete!")
output_path = output_video_path
# Normalize coordinates for better visualization
lat_center = np.mean(lats)
lon_center = np.mean(lons)
alt_min = np.min(alts)
# Convert to relative coordinates (in meters approximately)
x = (lons - lon_center) * 111320 * np.cos(np.radians(lat_center)) # longitude to meters
y = (lats - lat_center) * 110540 # latitude to meters
z = alts - alt_min # relative altitude
update_status(30, "Creating animation frames...")
# Create frames folder
frames_folder = os.path.join(project_folder, "progressive_frames")
os.makedirs(frames_folder, exist_ok=True)
# Animation settings
fps = 10 # frames per second
points_per_frame = max(1, len(positions) // 100) # Show multiple points per frame for long routes
total_frames = len(positions) // points_per_frame
frame_files = []
# Generate frames
for frame_idx in range(total_frames):
current_progress = 30 + (frame_idx / total_frames) * 50
update_status(current_progress, f"Creating frame {frame_idx + 1}/{total_frames}...")
# Points to show in this frame
end_point = min((frame_idx + 1) * points_per_frame, len(positions))
# Create 3D plot
fig = plt.figure(figsize=(12, 9), dpi=100)
ax = fig.add_subplot(111, projection='3d')
# Plot the route progressively
if end_point > 1:
# Plot completed route in blue
ax.plot(x[:end_point], y[:end_point], z[:end_point],
'b-', linewidth=2, alpha=0.7, label='Route')
# Plot points as small dots
ax.scatter(x[:end_point], y[:end_point], z[:end_point],
c='blue', s=20, alpha=0.6)
# Highlight current position in red
if end_point > 0:
current_idx = end_point - 1
ax.scatter(x[current_idx], y[current_idx], z[current_idx],
c='red', s=100, marker='o', label='Current Position')
# Add a small trail behind current position
trail_start = max(0, current_idx - 5)
if current_idx > trail_start:
ax.plot(x[trail_start:current_idx+1],
y[trail_start:current_idx+1],
z[trail_start:current_idx+1],
'r-', linewidth=4, alpha=0.8)
# Plot remaining route in light gray (preview)
if end_point < len(positions):
ax.plot(x[end_point:], y[end_point:], z[end_point:],
'lightgray', linewidth=1, alpha=0.3, label='Remaining Route')
# Set labels and title
ax.set_xlabel('East-West (meters)')
ax.set_ylabel('North-South (meters)')
ax.set_zlabel('Elevation (meters)')
# Add progress info
progress_percent = (end_point / len(positions)) * 100
timestamp_str = timestamps[end_point-1] if end_point > 0 else "Start"
ax.set_title(f'GPS Trip Animation\nProgress: {progress_percent:.1f}% - Point {end_point}/{len(positions)}\nTime: {timestamp_str}',
fontsize=14, pad=20)
# Set consistent view limits for all frames
margin = max(np.ptp(x), np.ptp(y)) * 0.1
ax.set_xlim(np.min(x) - margin, np.max(x) + margin)
ax.set_ylim(np.min(y) - margin, np.max(y) + margin)
ax.set_zlim(np.min(z) - 10, np.max(z) + 10)
# Set viewing angle for better 3D perspective
ax.view_init(elev=20, azim=45)
# Add legend
ax.legend(loc='upper right')
# Add grid
ax.grid(True, alpha=0.3)
# Save frame with comprehensive error handling
frame_path = os.path.join(frames_folder, f"frame_{frame_idx:04d}.png")
try:
plt.savefig(frame_path, dpi=100, bbox_inches='tight',
facecolor='white', edgecolor='none',
format='png', optimize=False)
plt.close(fig)
# Verify frame was saved properly and is readable by OpenCV
if os.path.exists(frame_path) and os.path.getsize(frame_path) > 1024:
# Test if OpenCV can read the frame
test_frame = cv2.imread(frame_path)
if test_frame is not None:
frame_files.append(frame_path)
if frame_idx == 0: # Log first frame details
h, w, c = test_frame.shape
update_status(current_progress, f"First frame: {w}x{h}, size: {os.path.getsize(frame_path)} bytes")
else:
update_status(current_progress, f"Warning: Frame {frame_idx} not readable by OpenCV")
try:
os.remove(frame_path)
except:
pass
else:
update_status(current_progress, f"Warning: Frame {frame_idx} too small or missing")
except Exception as e:
update_status(current_progress, f"Error saving frame {frame_idx}: {str(e)}")
try:
plt.close(fig)
except:
pass
continue
# Validate frames before creating video
if not frame_files:
raise Exception("No valid frames were generated")
update_status(80, f"Creating video from {len(frame_files)} frames...")
# Create video using OpenCV with better error handling
output_video_path = os.path.join(project_folder,
f"{self.project_name}_progressive_3d_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
if frame_files:
try:
# Read first frame to get dimensions
first_frame = cv2.imread(frame_files[0])
if first_frame is None:
raise Exception(f"Could not read first frame: {frame_files[0]}")
height, width, layers = first_frame.shape
update_status(82, f"Video dimensions: {width}x{height}")
# Try different codecs for better compatibility
codecs_to_try = [
('mp4v', '.mp4'),
('XVID', '.avi'),
('MJPG', '.avi')
]
video_created = False
for codec, ext in codecs_to_try:
try:
# Update output path for different codecs
if ext != '.mp4':
test_output_path = output_video_path.replace('.mp4', ext)
else:
test_output_path = output_video_path
update_status(84, f"Trying codec {codec}...")
# Create video writer
fourcc = cv2.VideoWriter_fourcc(*codec)
video_writer = cv2.VideoWriter(test_output_path, fourcc, fps, (width, height))
if not video_writer.isOpened():
update_status(85, f"Failed to open video writer with {codec}")
continue
# Add frames to video
frames_written = 0
for i, frame_file in enumerate(frame_files):
frame = cv2.imread(frame_file)
if frame is not None:
# Ensure frame dimensions match
if frame.shape[:2] != (height, width):
frame = cv2.resize(frame, (width, height))
video_writer.write(frame)
frames_written += 1
if i % 10 == 0: # Update progress every 10 frames
progress = 85 + (i / len(frame_files)) * 3
update_status(progress, f"Writing frame {i+1}/{len(frame_files)} with {codec}")
video_writer.release()
# Check if video file was created and has reasonable size
if os.path.exists(test_output_path) and os.path.getsize(test_output_path) > 1024:
output_video_path = test_output_path
video_created = True
update_status(88, f"Video created successfully with {codec} ({frames_written} frames)")
break
else:
update_status(86, f"Video file not created or too small with {codec}")
except Exception as codec_error:
update_status(87, f"Error with {codec}: {str(codec_error)}")
continue
if not video_created:
raise Exception("Failed to create video with any codec")
except Exception as video_error:
raise Exception(f"Video creation failed: {str(video_error)}")
update_status(90, "Cleaning up temporary files...")
# Clean up frame files
for frame_file in frame_files:
try:
os.remove(frame_file)
except:
pass
try:
os.rmdir(frames_folder)
except:
pass
update_status(100, "Progressive 3D animation complete!")
def show_success(dt):
popup.dismiss()
self.show_success_popup(
"Progressive 3D Animation Complete!",
f"Your progressive 3D animation has been saved to:\n{output_video_path}\n\nThe animation shows {len(positions)} GPS points building the route progressively from start to finish.",
output_video_path
)
Clock.schedule_once(show_success, 1)
else:
raise Exception("Failed to generate flythrough video")
def show_success(dt):
popup.dismiss()
self.show_success_popup(
"Google Earth Flythrough Complete!",
f"Your cinematic flythrough has been created:\n{output_path}",
output_path
)
Clock.schedule_once(show_success, 1)
raise Exception("No frames were generated")
except Exception as e:
error_message = str(e)
print(f"DEBUG: Progressive animation error: {error_message}")
import traceback
traceback.print_exc()
def show_error(dt):
popup.dismiss()
self.show_error_popup("Google Earth Animation Error", error_message)
self.show_error_popup("Progressive Animation Error", error_message)
Clock.schedule_once(show_error, 0)
# Schedule the animation generation
Clock.schedule_once(lambda dt: run_google_earth_animation(), 0.5)
def show_success_popup(self, title, message, file_path=None):
Clock.schedule_once(lambda dt: run_progressive_animation(), 0.5)
def open_pauses_popup(self):
"""Navigate to the pause edit screen"""
pause_edit_screen = self.manager.get_screen("pause_edit")
pause_edit_screen.set_project_and_callback(self.project_name, self.on_pre_enter)
self.manager.current = "pause_edit"
def show_success_popup(self, title, message, file_path):
"""Show success popup with option to open file location"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
layout = BoxLayout(orientation='vertical', spacing=10, padding=15)
# Success message
success_label = Label(
text=message,
text_size=(400, None),
text_size=(None, None),
halign="center",
valign="middle"
)
layout.add_widget(success_label)
button_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=50)
# Buttons
btn_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)
open_folder_btn = Button(
text="Open Folder",
background_color=(0.2, 0.6, 0.9, 1)
)
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)
ok_btn = Button(
text="OK",
background_color=(0.3, 0.7, 0.3, 1)
)
btn_layout.add_widget(open_folder_btn)
btn_layout.add_widget(ok_btn)
layout.add_widget(btn_layout)
popup = Popup(
title=title,
content=layout,
size_hint=(0.8, None),
size=(0, 250),
size_hint=(0.9, 0.6),
auto_dismiss=False
)
ok_btn.bind(on_press=lambda x: popup.dismiss())
def open_folder(instance):
folder_path = os.path.dirname(file_path)
os.system(f'xdg-open "{folder_path}"') # Linux
popup.dismiss()
def close_popup(instance):
popup.dismiss()
open_folder_btn.bind(on_press=open_folder)
ok_btn.bind(on_press=close_popup)
popup.open()
def show_error_popup(self, title, message):
"""Show error popup"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
layout = BoxLayout(orientation='vertical', spacing=10, padding=15)
error_label = Label(
text=f"Error: {message}",
text_size=(400, None),
text_size=(None, None),
halign="center",
valign="middle",
color=(1, 0.3, 0.3, 1)
valign="middle"
)
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)
ok_btn = Button(
text="OK",
background_color=(0.8, 0.3, 0.3, 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),
size_hint=(0.8, 0.4),
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}")

View File

@@ -0,0 +1,633 @@
import kivy
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.advanced_3d_generator import NavigationAnimationGenerator
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
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.progressbar import ProgressBar
from kivy.uix.textinput import TextInput
from config import RESOURCES_FOLDER
class CreateAnimationScreen(Screen):
project_name = StringProperty("")
preview_html_path = StringProperty("") # Path to the HTML file for preview
preview_image_path = StringProperty("") # Add this line
preview_image_version = NumericProperty(0) # Add this line
def get_preview_image_source(self):
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
img_path = os.path.join(project_folder, "preview.png")
if os.path.exists(img_path):
return img_path
return "resources/images/track.png"
preview_image_source = AliasProperty(
get_preview_image_source, None, bind=['project_name', 'preview_image_version']
)
def on_pre_enter(self):
# Update the route entries label with the actual number of entries
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.json")
count = 0
if os.path.exists(positions_path):
with open(positions_path, "r") as f:
try:
positions = json.load(f)
count = len(positions)
except Exception:
count = 0
self.ids.route_entries_label.text = f"Your route has {count} entries,"
def open_rename_popup(self):
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.label import Label
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Enter new project name:")
input_field = TextInput(text=self.project_name, multiline=False)
btn_save = Button(text="Save", background_color=(0.008, 0.525, 0.290, 1))
btn_cancel = Button(text="Cancel")
layout.add_widget(label)
layout.add_widget(input_field)
layout.add_widget(btn_save)
layout.add_widget(btn_cancel)
popup = Popup(
title="Rename Project",
content=layout,
size_hint=(0.92, None),
size=(0, 260),
auto_dismiss=False
)
def do_rename(instance):
new_name = input_field.text.strip()
if new_name and new_name != self.project_name:
if self.rename_project_folder(self.project_name, new_name):
self.project_name = new_name
popup.dismiss()
self.on_pre_enter() # Refresh label
else:
label.text = "Rename failed (name exists?)"
else:
label.text = "Please enter a new name."
btn_save.bind(on_press=do_rename)
btn_cancel.bind(on_press=lambda x: popup.dismiss())
popup.open()
def rename_project_folder(self, old_name, new_name):
import os
old_path = os.path.join(RESOURCES_FOLDER, "projects", old_name)
new_path = os.path.join(RESOURCES_FOLDER, "projects", new_name)
if os.path.exists(old_path) and not os.path.exists(new_path):
os.rename(old_path, new_path)
return True
return False
def optimize_route_entries(self):
# Create the popup and UI elements
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Processing route entries...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Optimizing Route",
content=layout,
size_hint=(0.92, None),
size=(0, 260),
auto_dismiss=False
)
popup.open()
# Now call the utility function with these objects
optimize_route_entries_util(
self.project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
Clock,
on_save=lambda: self.on_pre_enter()
)
def preview_route(self):
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Processing route preview...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Previewing Route",
content=layout,
size_hint=(0.8, None),
size=(0, 180),
auto_dismiss=False
)
popup.open()
def set_preview_image_path(path):
self.preview_image_path = path
self.preview_image_version += 1 # Force AliasProperty to update
self.property('preview_image_source').dispatch(self)
self.ids.preview_image.reload()
# Schedule the processing function
Clock.schedule_once(
lambda dt: process_preview_util(
self.project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
self.ids.preview_image,
set_preview_image_path,
Clock
),
0.5
)
def open_pauses_popup(self):
"""Navigate to the pause edit screen"""
pause_edit_screen = self.manager.get_screen("pause_edit")
pause_edit_screen.set_project_and_callback(self.project_name, self.on_pre_enter)
self.manager.current = "pause_edit"
def show_video_generation_options(self):
"""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=12, padding=15)
# Title
title_label = Label(
text="Choose Animation Style & Quality",
font_size=20,
size_hint_y=None,
height=40,
color=(1, 1, 1, 1)
)
layout.add_widget(title_label)
# 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)
)
classic_desc = Label(
text="• Traditional OpenCV/PIL approach\n• Fast generation\n• Good for simple tracks\n• Test (720p) or Production (2K)",
font_size=11,
size_hint_y=None,
height=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
classic_desc.text_size = (None, None)
classic_layout.add_widget(classic_title)
classic_layout.add_widget(classic_desc)
layout.add_widget(classic_layout)
# 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),
font_size=12
)
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)
# Advanced Navigation Mode
advanced_layout = BoxLayout(orientation='vertical', spacing=5)
advanced_title = Label(
text="🧭 Navigation Animation",
font_size=16,
size_hint_y=None,
height=30,
color=(0.2, 0.6, 0.9, 1)
)
advanced_desc = Label(
text="• Satellite terrain details\n• 3D camera following at 1000-2000m\n• Google Earth entry scene\n• Professional navigation view",
font_size=11,
size_hint_y=None,
height=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
advanced_desc.text_size = (None, None)
advanced_layout.add_widget(advanced_title)
advanced_layout.add_widget(advanced_desc)
layout.add_widget(advanced_layout)
# Advanced button
advanced_btn = Button(
text="Generate Navigation Animation",
background_color=(0.2, 0.6, 0.9, 1),
size_hint_y=None,
height=45,
font_size=13
)
layout.add_widget(advanced_btn)
# Google Earth Flythrough Mode
google_earth_layout = BoxLayout(orientation='vertical', spacing=5)
google_earth_title = Label(
text="🌍 Google Earth Flythrough",
font_size=16,
size_hint_y=None,
height=30,
color=(0.1, 0.8, 0.1, 1)
)
google_earth_desc = Label(
text="• Realistic 3D terrain with mountains\n• Cinematic camera following at 1000-2000m\n• Google Earth-style flythrough\n• Professional geographic animation",
font_size=11,
size_hint_y=None,
height=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
google_earth_desc.text_size = (None, None)
google_earth_layout.add_widget(google_earth_title)
google_earth_layout.add_widget(google_earth_desc)
layout.add_widget(google_earth_layout)
# Google Earth button
google_earth_btn = Button(
text="Generate Google Earth Flythrough",
background_color=(0.1, 0.8, 0.1, 1),
size_hint_y=None,
height=45,
font_size=13
)
layout.add_widget(google_earth_btn)
# Blender Cinema Mode
blender_layout = BoxLayout(orientation='vertical', spacing=5)
blender_title = Label(
text="<22> 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(
text="Cancel",
background_color=(0.5, 0.5, 0.5, 1),
size_hint_y=None,
height=40,
font_size=12
)
layout.add_widget(cancel_btn)
popup = Popup(
title="Select Animation Style",
content=layout,
size_hint=(0.95, 0.9),
auto_dismiss=False
)
def start_classic_test(instance):
popup.dismiss()
self.generate_3d_video_test_mode()
def start_classic_production(instance):
popup.dismiss()
self.generate_3d_video_production_mode()
def start_advanced_3d(instance):
popup.dismiss()
self.generate_advanced_3d_animation()
def start_google_earth(instance):
popup.dismiss()
self.generate_google_earth_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)
google_earth_btn.bind(on_press=start_google_earth)
blender_btn.bind(on_press=start_blender_animation)
cancel_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
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 generate_google_earth_animation(self):
"""Generate Google Earth-style flythrough animation with terrain"""
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Initializing Google Earth flythrough...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Generating Google Earth Flythrough",
content=layout,
size_hint=(0.9, None),
size=(0, 200),
auto_dismiss=False
)
popup.open()
def run_google_earth_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, "Checking dependencies...")
# Check dependencies first
generator = NavigationAnimationGenerator(project_folder)
generator.check_dependencies()
update_status(20, "Loading GPS data...")
df = generator.load_gps_data(positions_path)
update_status(30, "Generating navigation flythrough...")
output_video_path = os.path.join(project_folder, f"{self.project_name}_navigation_flythrough_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
# Progress callback for the generator
def generator_progress(progress, message):
update_status(30 + (progress * 0.5), message) # Map 0-100% to 30-80%
update_status(80, "Creating navigation flythrough...")
success = generator.generate_3d_animation(
positions_path,
output_video_path,
style='advanced',
progress_callback=generator_progress
)
if success:
update_status(100, "Navigation flythrough complete!")
output_path = output_video_path
else:
raise Exception("Failed to generate navigation flythrough")
def show_success(dt):
popup.dismiss()
self.show_success_popup(
"Google Earth Flythrough Complete!",
f"Your cinematic flythrough has been created:\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("Google Earth Animation Error", error_message)
Clock.schedule_once(show_error, 0)
# Schedule the animation generation
Clock.schedule_once(lambda dt: run_google_earth_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}")

View File

@@ -0,0 +1,419 @@
import kivy
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.advanced_3d_generator import NavigationAnimationGenerator
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
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.progressbar import ProgressBar
from kivy.uix.textinput import TextInput
from config import RESOURCES_FOLDER
class CreateAnimationScreen(Screen):
project_name = StringProperty("")
preview_html_path = StringProperty("") # Path to the HTML file for preview
preview_image_path = StringProperty("") # Add this line
preview_image_version = NumericProperty(0) # Add this line
def get_preview_image_source(self):
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
img_path = os.path.join(project_folder, "preview.png")
if os.path.exists(img_path):
return img_path
return "resources/images/track.png"
preview_image_source = AliasProperty(
get_preview_image_source, None, bind=['project_name', 'preview_image_version']
)
def on_pre_enter(self):
# Update the route entries label with the actual number of entries
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.json")
count = 0
if os.path.exists(positions_path):
with open(positions_path, "r") as f:
try:
positions = json.load(f)
count = len(positions)
except Exception:
count = 0
self.ids.route_entries_label.text = f"Your route has {count} entries,"
def open_rename_popup(self):
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.label import Label
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Enter new project name:")
input_field = TextInput(text=self.project_name, multiline=False)
btn_save = Button(text="Save", background_color=(0.008, 0.525, 0.290, 1))
btn_cancel = Button(text="Cancel")
layout.add_widget(label)
layout.add_widget(input_field)
layout.add_widget(btn_save)
layout.add_widget(btn_cancel)
popup = Popup(
title="Rename Project",
content=layout,
size_hint=(0.92, None),
size=(0, 260),
auto_dismiss=False
)
def do_rename(instance):
new_name = input_field.text.strip()
if new_name and new_name != self.project_name:
if self.rename_project_folder(self.project_name, new_name):
self.project_name = new_name
popup.dismiss()
self.on_pre_enter() # Refresh label
else:
label.text = "Rename failed (name exists?)"
else:
label.text = "Please enter a new name."
btn_save.bind(on_press=do_rename)
btn_cancel.bind(on_press=lambda x: popup.dismiss())
popup.open()
def rename_project_folder(self, old_name, new_name):
import os
old_path = os.path.join(RESOURCES_FOLDER, "projects", old_name)
new_path = os.path.join(RESOURCES_FOLDER, "projects", new_name)
if os.path.exists(old_path) and not os.path.exists(new_path):
os.rename(old_path, new_path)
return True
return False
def optimize_route_entries(self):
# Create the popup and UI elements
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Processing route entries...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Optimizing Route",
content=layout,
size_hint=(0.92, None),
size=(0, 260),
auto_dismiss=False
)
popup.open()
# Now call the utility function with these objects
optimize_route_entries_util(
self.project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
Clock,
on_save=lambda: self.on_pre_enter()
)
def preview_route(self):
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Processing route preview...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Previewing Route",
content=layout,
size_hint=(0.8, None),
size=(0, 180),
auto_dismiss=False
)
popup.open()
def set_preview_image_path(path):
self.preview_image_path = path
self.preview_image_version += 1 # Force AliasProperty to update
self.property('preview_image_source').dispatch(self)
self.ids.preview_image.reload()
# Schedule the processing function
Clock.schedule_once(
lambda dt: process_preview_util(
self.project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
self.ids.preview_image,
set_preview_image_path,
Clock
),
0.5
)
def open_pauses_popup(self):
"""Navigate to the pause edit screen"""
pause_edit_screen = self.manager.get_screen("pause_edit")
pause_edit_screen.set_project_and_callback(self.project_name, self.on_pre_enter)
self.manager.current = "pause_edit"
def generate_google_earth_animation(self):
"""Generate Google Earth-style flythrough animation using NavigationAnimationGenerator"""
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Initializing Google Earth flythrough...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Generating Google Earth Flythrough",
content=layout,
size_hint=(0.9, None),
size=(0, 200),
auto_dismiss=False
)
popup.open()
def run_google_earth_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 = NavigationAnimationGenerator(project_folder)
generator.check_dependencies()
update_status(20, "Processing GPS coordinates...")
df = generator.load_gps_data(positions_path)
update_status(40, "Creating Google Earth flythrough...")
output_video_path = os.path.join(project_folder, f"{self.project_name}_google_earth_{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.5), message) # Map 0-100% to 40-90%
update_status(90, "Creating flythrough video...")
success = generator.generate_frames(positions_path, style='google_earth', progress_callback=generator_progress)
if success and len(success) > 0:
update_status(95, "Rendering final video...")
video_success = generator.create_video(success, output_video_path, generator_progress)
if video_success:
update_status(100, "Google Earth flythrough complete!")
output_path = output_video_path
else:
raise Exception("Failed to create video from frames")
else:
raise Exception("Failed to generate frames")
def show_success(dt):
popup.dismiss()
self.show_success_popup(
"Google Earth Flythrough Complete!",
f"Your Google Earth-style flythrough 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("Google Earth Animation Error", error_message)
Clock.schedule_once(show_error, 0)
# Schedule the animation generation
Clock.schedule_once(lambda dt: run_google_earth_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 generate Blender animation")
def show_success(dt):
popup.dismiss()
self.show_success_popup(
"Blender Cinema Animation Complete!",
f"Your cinema-quality 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("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):
"""Show success popup with option to open file location"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=15)
# Success message
success_label = Label(
text=message,
text_size=(None, None),
halign="center",
valign="middle"
)
layout.add_widget(success_label)
# Buttons
btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=50)
open_folder_btn = Button(
text="Open Folder",
background_color=(0.2, 0.6, 0.9, 1)
)
ok_btn = Button(
text="OK",
background_color=(0.3, 0.7, 0.3, 1)
)
btn_layout.add_widget(open_folder_btn)
btn_layout.add_widget(ok_btn)
layout.add_widget(btn_layout)
popup = Popup(
title=title,
content=layout,
size_hint=(0.9, 0.6),
auto_dismiss=False
)
def open_folder(instance):
folder_path = os.path.dirname(file_path)
os.system(f'xdg-open "{folder_path}"') # Linux
popup.dismiss()
def close_popup(instance):
popup.dismiss()
open_folder_btn.bind(on_press=open_folder)
ok_btn.bind(on_press=close_popup)
popup.open()
def show_error_popup(self, title, message):
"""Show error popup"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=15)
error_label = Label(
text=f"Error: {message}",
text_size=(None, None),
halign="center",
valign="middle"
)
layout.add_widget(error_label)
ok_btn = Button(
text="OK",
background_color=(0.8, 0.3, 0.3, 1),
size_hint_y=None,
height=50
)
layout.add_widget(ok_btn)
popup = Popup(
title=title,
content=layout,
size_hint=(0.8, 0.4),
auto_dismiss=False
)
ok_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()

View File

@@ -7,7 +7,7 @@ import os
import sys
sys.path.append('/home/pi/Desktop/traccar_animation')
from py_scripts.advanced_3d_generator import Advanced3DGenerator
from py_scripts.advanced_3d_generator import NavigationAnimationGenerator
from datetime import datetime
def test_google_earth_animation():
@@ -39,7 +39,7 @@ def test_google_earth_animation():
print(f"Testing Google Earth animation with project: {project_name}")
# Create generator
generator = Advanced3DGenerator(project_folder)
generator = NavigationAnimationGenerator(project_folder)
# Check dependencies
try:
@@ -57,22 +57,36 @@ def test_google_earth_animation():
try:
print("Starting Google Earth flythrough generation...")
success = generator.generate_3d_animation(
# Generate frames
frame_paths = generator.generate_frames(
positions_file,
output_video,
style='google_earth',
progress_callback=progress_callback
)
if success and os.path.exists(output_video):
print(f"SUCCESS! Google Earth flythrough created: {output_video}")
if frame_paths and len(frame_paths) > 0:
print(f"Generated {len(frame_paths)} frames")
# Get file size
file_size = os.path.getsize(output_video) / (1024 * 1024) # MB
print(f"📹 Video size: {file_size:.1f} MB")
# Create video
success = generator.create_video(
frame_paths,
output_video,
progress_callback=progress_callback
)
if success and os.path.exists(output_video):
print(f"✅ SUCCESS! Google Earth flythrough created: {output_video}")
# Get file size
file_size = os.path.getsize(output_video) / (1024 * 1024) # MB
print(f"📹 Video size: {file_size:.1f} MB")
# Clean up frames
generator.cleanup_frames()
else:
print("❌ Failed to create video from frames")
else:
print("❌ Failed to create video")
print("❌ Failed to generate frames")
except Exception as e:
print(f"❌ Error during generation: {e}")

69
test_progressive_3d.py Normal file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
Test script for progressive 3D animation function
"""
import os
import sys
sys.path.append('/home/pi/Desktop/traccar_animation')
def test_progressive_3d_animation():
"""Test the progressive 3D animation dependencies"""
print("Testing progressive 3D animation dependencies...")
try:
# Test matplotlib with 3D
import matplotlib
matplotlib.use('Agg') # Non-interactive backend
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
print("✅ Matplotlib with 3D support available")
# Test OpenCV (instead of MoviePy)
import cv2
print("✅ OpenCV available for video creation")
# Test numpy
import numpy as np
print("✅ NumPy available")
# Test basic 3D plot creation
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')
# Create simple test data
x = np.array([0, 1, 2, 3, 4])
y = np.array([0, 1, 0, 1, 0])
z = np.array([0, 0, 1, 1, 2])
ax.plot(x, y, z, 'b-', linewidth=2)
ax.scatter(x, y, z, c='red', s=50)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.set_title('Test 3D Plot')
# Save test plot
test_path = '/tmp/test_3d_plot.png'
plt.savefig(test_path, dpi=100, bbox_inches='tight')
plt.close(fig)
if os.path.exists(test_path):
print("✅ 3D plot creation and saving works")
os.remove(test_path)
else:
print("❌ Failed to create 3D plot")
except ImportError as e:
print(f"❌ Import error: {e}")
return False
except Exception as e:
print(f"❌ Error: {e}")
return False
print("🎉 All dependencies for progressive 3D animation are working!")
return True
if __name__ == "__main__":
test_progressive_3d_animation()

202
test_progressive_debug.py Normal file
View File

@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""
Test script for progressive 3D animation with debugging
"""
import os
import sys
sys.path.append('/home/pi/Desktop/traccar_animation')
def test_progressive_animation_debug():
"""Test the progressive animation with a simple GPS dataset"""
print("Testing progressive 3D animation with debug output...")
# Find a project with GPS data
projects_folder = "/home/pi/Desktop/traccar_animation/resources/projects"
if not os.path.exists(projects_folder):
print("❌ Projects folder not found!")
return
# Look for projects
projects = [d for d in os.listdir(projects_folder) if os.path.isdir(os.path.join(projects_folder, d))]
if not projects:
print("❌ No projects found!")
return
# Use the first project found
project_name = projects[0]
project_folder = os.path.join(projects_folder, project_name)
positions_file = os.path.join(project_folder, "positions.json")
if not os.path.exists(positions_file):
print(f"❌ No positions.json found in project {project_name}")
return
print(f"✅ Testing with project: {project_name}")
try:
# Import the animation generation code
import json
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
import cv2
from datetime import datetime
# Load GPS data
with open(positions_file, 'r') as f:
positions = json.load(f)
print(f"✅ Loaded {len(positions)} GPS points")
if len(positions) < 2:
print("❌ Need at least 2 GPS points")
return
# Test creating just 3 frames
test_frames_folder = os.path.join(project_folder, "test_frames")
os.makedirs(test_frames_folder, exist_ok=True)
# Extract coordinates
lats = np.array([pos['latitude'] for pos in positions[:10]]) # Just first 10 points
lons = np.array([pos['longitude'] for pos in positions[:10]])
alts = np.array([pos.get('altitude', 0) for pos in positions[:10]])
# Convert to relative coordinates
lat_center = np.mean(lats)
lon_center = np.mean(lons)
alt_min = np.min(alts)
x = (lons - lon_center) * 111320 * np.cos(np.radians(lat_center))
y = (lats - lat_center) * 110540
z = alts - alt_min
print(f"✅ Processed coordinates: x={x.min():.1f} to {x.max():.1f}, y={y.min():.1f} to {y.max():.1f}")
frame_files = []
# Create 3 test frames
for frame_idx in range(3):
end_point = (frame_idx + 1) * 3
end_point = min(end_point, len(x))
print(f"Creating frame {frame_idx + 1}, showing {end_point} points...")
# Create 3D plot
fig = plt.figure(figsize=(10, 8), dpi=100)
ax = fig.add_subplot(111, projection='3d')
# Plot route up to current point
if end_point > 1:
ax.plot(x[:end_point], y[:end_point], z[:end_point], 'b-', linewidth=2)
ax.scatter(x[:end_point], y[:end_point], z[:end_point], c='blue', s=20)
# Current position
if end_point > 0:
current_idx = end_point - 1
ax.scatter(x[current_idx], y[current_idx], z[current_idx],
c='red', s=100, marker='o')
# Remaining route
if end_point < len(x):
ax.plot(x[end_point:], y[end_point:], z[end_point:],
'lightgray', linewidth=1, alpha=0.3)
ax.set_xlabel('East-West (m)')
ax.set_ylabel('North-South (m)')
ax.set_zlabel('Elevation (m)')
ax.set_title(f'Test Frame {frame_idx + 1} - Point {end_point}/{len(x)}')
# Set view
margin = max(np.ptp(x), np.ptp(y)) * 0.1 if np.ptp(x) > 0 else 100
ax.set_xlim(np.min(x) - margin, np.max(x) + margin)
ax.set_ylim(np.min(y) - margin, np.max(y) + margin)
ax.set_zlim(np.min(z) - 10, np.max(z) + 10)
ax.view_init(elev=20, azim=45)
ax.grid(True)
# Save frame
frame_path = os.path.join(test_frames_folder, f"test_frame_{frame_idx:03d}.png")
try:
plt.savefig(frame_path, dpi=100, bbox_inches='tight',
facecolor='white', edgecolor='none', format='png')
plt.close(fig)
# Check frame
if os.path.exists(frame_path):
file_size = os.path.getsize(frame_path)
print(f"✅ Frame {frame_idx + 1} saved: {file_size} bytes")
# Test OpenCV reading
test_img = cv2.imread(frame_path)
if test_img is not None:
h, w, c = test_img.shape
print(f"✅ Frame {frame_idx + 1} readable by OpenCV: {w}x{h}")
frame_files.append(frame_path)
else:
print(f"❌ Frame {frame_idx + 1} not readable by OpenCV")
else:
print(f"❌ Frame {frame_idx + 1} not created")
except Exception as e:
print(f"❌ Error creating frame {frame_idx + 1}: {e}")
plt.close(fig)
print(f"Created {len(frame_files)} valid frames")
# Test video creation
if frame_files:
output_video = os.path.join(project_folder, f"test_progressive_{datetime.now().strftime('%H%M%S')}.mp4")
# Read first frame for dimensions
first_frame = cv2.imread(frame_files[0])
height, width, layers = first_frame.shape
print(f"Video dimensions: {width}x{height}")
# Create video
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
video_writer = cv2.VideoWriter(output_video, fourcc, 2.0, (width, height))
if video_writer.isOpened():
for i, frame_file in enumerate(frame_files):
frame = cv2.imread(frame_file)
if frame is not None:
video_writer.write(frame)
print(f"✅ Added frame {i+1} to video")
else:
print(f"❌ Could not read frame {i+1}")
video_writer.release()
if os.path.exists(output_video):
file_size = os.path.getsize(output_video)
print(f"✅ Video created: {output_video} ({file_size} bytes)")
else:
print("❌ Video file not created")
else:
print("❌ Could not open video writer")
# Clean up
for frame_file in frame_files:
try:
os.remove(frame_file)
except:
pass
try:
os.rmdir(test_frames_folder)
except:
pass
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
test_progressive_animation_debug()

126
test_video_creation.py Normal file
View File

@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""
Test script for video creation functionality
"""
import os
import sys
sys.path.append('/home/pi/Desktop/traccar_animation')
import cv2
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
def test_video_creation():
"""Test video creation with sample frames"""
print("Testing video creation functionality...")
# Create test directory
test_dir = "/tmp/video_test"
os.makedirs(test_dir, exist_ok=True)
try:
# Create sample frames
frame_files = []
for i in range(10):
# Create a simple test plot
fig, ax = plt.subplots(figsize=(8, 6))
# Simple animation - moving dot
x = np.linspace(0, 10, 100)
y = np.sin(x + i * 0.5)
ax.plot(x, y, 'b-', linewidth=2)
ax.scatter([i], [np.sin(i * 0.5)], c='red', s=100)
ax.set_xlim(0, 10)
ax.set_ylim(-2, 2)
ax.set_title(f'Test Frame {i+1}/10')
ax.grid(True)
# Save frame
frame_path = os.path.join(test_dir, f"frame_{i:03d}.png")
plt.savefig(frame_path, dpi=100, bbox_inches='tight')
plt.close(fig)
frame_files.append(frame_path)
print(f"Created frame {i+1}/10")
print(f"Created {len(frame_files)} test frames")
# Test video creation with different codecs
codecs_to_test = [
('mp4v', '.mp4'),
('XVID', '.avi'),
('MJPG', '.avi')
]
for codec, ext in codecs_to_test:
try:
output_path = os.path.join(test_dir, f"test_video_{codec}{ext}")
# Read first frame for dimensions
first_frame = cv2.imread(frame_files[0])
if first_frame is None:
print(f"❌ Could not read first frame")
continue
height, width, layers = first_frame.shape
print(f"Frame dimensions: {width}x{height}")
# Create video writer
fourcc = cv2.VideoWriter_fourcc(*codec)
video_writer = cv2.VideoWriter(output_path, fourcc, 5.0, (width, height))
if not video_writer.isOpened():
print(f"❌ Could not open video writer with {codec}")
continue
# Write frames
frames_written = 0
for frame_file in frame_files:
frame = cv2.imread(frame_file)
if frame is not None:
video_writer.write(frame)
frames_written += 1
video_writer.release()
# Check result
if os.path.exists(output_path):
file_size = os.path.getsize(output_path)
if file_size > 1024: # At least 1KB
print(f"{codec} video created: {output_path} ({file_size} bytes, {frames_written} frames)")
else:
print(f"{codec} video too small: {file_size} bytes")
else:
print(f"{codec} video not created")
except Exception as e:
print(f"❌ Error with {codec}: {e}")
# Check OpenCV version and capabilities
print(f"\nOpenCV version: {cv2.__version__}")
print(f"OpenCV build info available: {hasattr(cv2, 'getBuildInformation')}")
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
finally:
# Clean up
for frame_file in frame_files:
try:
os.remove(frame_file)
except:
pass
try:
os.rmdir(test_dir)
except:
pass
if __name__ == "__main__":
test_video_creation()

View File

@@ -722,7 +722,7 @@
height: 202
size_hint_x: 1
# 3D Video Animation frame
# Progressive 3D Animation frame
BoxLayout:
orientation: "horizontal"
size_hint_y: None
@@ -737,7 +737,7 @@
size: self.size
Label:
text: "Generate 3D video animation\nsimilar to Relive style"
text: "Generate progressive 3D animation\nBuilds trip point by point"
font_size: 16
color: 1, 1, 1, 1
size_hint_x: 0.7
@@ -746,15 +746,84 @@
text_size: self.size
Button:
text: "Generate\n3D Video"
text: "Generate\n3D Trip"
size_hint_x: 0.3
font_size: 14
background_color: 0.8, 0.2, 0.4, 1
background_color: 0.2, 0.8, 0.4, 1
color: 1, 1, 1, 1
halign: "center"
valign: "middle"
text_size: self.size
on_press: root.generate_3d_video()
on_press: root.generate_progressive_3d_animation()
# Google Earth Animation frame
BoxLayout:
orientation: "horizontal"
size_hint_y: None
height: 60
padding: [10, 10, 10, 10]
spacing: 10
canvas.before:
Color:
rgba: 0.15, 0.15, 0.18, 1
Rectangle:
pos: self.pos
size: self.size
Label:
text: "Generate Google Earth flythrough\nCinematic aerial view"
font_size: 16
color: 1, 1, 1, 1
size_hint_x: 0.7
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "Generate\nFlythrough"
size_hint_x: 0.3
font_size: 14
background_color: 0.1, 0.8, 0.1, 1
color: 1, 1, 1, 1
halign: "center"
valign: "middle"
text_size: self.size
on_press: root.generate_google_earth_animation()
# Blender Animation frame
BoxLayout:
orientation: "horizontal"
size_hint_y: None
height: 60
padding: [10, 10, 10, 10]
spacing: 10
canvas.before:
Color:
rgba: 0.15, 0.15, 0.18, 1
Rectangle:
pos: self.pos
size: self.size
Label:
text: "Generate cinema-quality animation\nProfessional 3D rendering"
font_size: 16
color: 1, 1, 1, 1
size_hint_x: 0.7
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "Generate\nCinema"
size_hint_x: 0.3
font_size: 14
background_color: 0.9, 0.6, 0.2, 1
color: 1, 1, 1, 1
halign: "center"
valign: "middle"
text_size: self.size
on_press: root.generate_blender_animation()
Widget: