updated to generate trip
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
# Ignore the virtual environment folder
|
||||
track/
|
||||
track/
|
||||
resurces/projects/
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
0
junk_files/reqirements.txt
Normal file
0
junk_files/test_enhanced_video.py
Normal file
83
junk_files/test_google_earth.py
Normal 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()
|
||||
0
junk_files/test_space_entry_fix.py
Normal file
0
junk_files/test_transition.py
Normal file
0
junk_files/test_video_generator.py
Normal file
0
junk_files/test_video_modes.py
Normal file
0
junk_files/video_3d_generator.py
Normal 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")
|
||||
|
||||
@@ -6,7 +6,6 @@ selenium
|
||||
pillow
|
||||
geopy
|
||||
opencv-python
|
||||
moviepy
|
||||
requests
|
||||
numpy
|
||||
matplotlib
|
||||
|
||||
@@ -1 +1 @@
|
||||
gAAAAABobmx0PnGbcR3Hxn93Z2r3z0dqZpHYGfWhJC7ko6QSMHLY_qoGsEZLrlLjjGrdjVOqSNVfwCP6_pAQ5QWbDRs6RoyZFPIA-vLFYpU9tUVC6pHCSSxvQimS_Thdj5WMIBlpTOWa
|
||||
gAAAAABob4-usKjps0vEVupB8FIJ3tKqoOeedzOUpt16NICbpi1ejKoSwqDvH7eIAPaZCOkfbPC6gjJGTo2yxt4BOPg1yzg_-Xanpl5iL1Y2mIRxWag-5cWhDNiqZo3bEZMqZ3M875O-
|
||||
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 169 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 257 KiB |