-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
372 lines (296 loc) · 13.6 KB
/
Copy pathapp.py
File metadata and controls
372 lines (296 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
import streamlit as st
import cv2
import numpy as np
from PIL import Image
import tempfile
import os
import torch
import torchvision.transforms as transforms
from transformers import DPTImageProcessor, DPTForDepthEstimation
import google.generativeai as genai
from moviepy.editor import VideoFileClip, ImageSequenceClip
import io
import time
# Configure the page
st.set_page_config(
page_title="VR 180 Converter",
page_icon="🥽",
layout="wide"
)
# Initialize session state
if 'processing_status' not in st.session_state:
st.session_state.processing_status = ""
if 'processed_video_path' not in st.session_state:
st.session_state.processed_video_path = None
# Configure Gemini API
def setup_gemini():
if 'gemini_api_key' not in st.session_state:
st.session_state.gemini_api_key = None
api_key = st.sidebar.text_input(
"Enter Google Gemini API Key:",
type="password",
value=st.session_state.gemini_api_key or "",
help="This key will be used for AI-powered depth analysis and optimization"
)
if api_key:
st.session_state.gemini_api_key = api_key
genai.configure(api_key=api_key)
return True
return False
# Load depth estimation model
@st.cache_resource
def load_depth_model():
try:
processor = DPTImageProcessor.from_pretrained("Intel/dpt-large")
model = DPTForDepthEstimation.from_pretrained("Intel/dpt-large")
return processor, model
except Exception as e:
st.error(f"Error loading depth model: {str(e)}")
return None, None
# Extract frames from video
def extract_frames(video_path, max_frames=200):
"""Extract frames from video for processing"""
cap = cv2.VideoCapture(video_path)
frames = []
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
# Calculate frame skip to limit processing
frame_skip = max(1, total_frames // max_frames)
frame_count = 0
while True:
ret, frame = cap.read()
if not ret:
break
if frame_count % frame_skip == 0:
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frames.append(frame_rgb)
frame_count += 1
cap.release()
return frames, fps
# Generate depth map for a single frame
def generate_depth_map(frame, processor, model):
"""Generate depth map using DPT model"""
try:
# Convert frame to PIL Image
pil_image = Image.fromarray(frame)
# Process image
inputs = processor(images=pil_image, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
predicted_depth = outputs.predicted_depth
# Convert to numpy and normalize
depth = predicted_depth.squeeze().cpu().numpy()
depth_normalized = (depth - depth.min()) / (depth.max() - depth.min())
return depth_normalized
except Exception as e:
st.error(f"Error generating depth map: {str(e)}")
return None
# Create stereoscopic frame from depth
def create_stereoscopic_frame(frame, depth_map, baseline=0.1):
"""Create left and right eye views for VR 180"""
height, width = frame.shape[:2]
# --- THE FIX ---
# The original depth_map has a fixed size (e.g., 384x384) from the AI model.
# We MUST resize it to match the video frame's dimensions (e.g., 640x360).
depth_map_resized = cv2.resize(depth_map, (width, height), interpolation=cv2.INTER_LINEAR)
# Now, use the CORRECTED `depth_map_resized` variable for all calculations.
max_disparity = int(width * baseline)
disparity = (depth_map_resized * max_disparity).astype(np.int32) # <<< Ensure you are using depth_map_resized here!
# The rest of the function remains the same, but it's now using correctly-sized data.
left_img = frame.copy()
right_img = np.zeros_like(frame)
for y in range(height):
for x in range(width):
shift = disparity[y, x]
new_x = min(width - 1, max(0, x - shift))
right_img[y, new_x] = frame[y, x]
mask = np.all(right_img == 0, axis=2)
right_img[mask] = left_img[mask]
stereo_frame = np.hstack([left_img, right_img])
return stereo_frame
# Process video with Gemini insights
def get_gemini_insights(frame_sample):
"""Get AI insights about the video content for better depth processing"""
if st.session_state.gemini_api_key:
try:
model = genai.GenerativeModel('gemini-2.5-flash')
# Convert frame to PIL Image for Gemini
pil_image = Image.fromarray(frame_sample)
prompt = """
Analyze this video frame for VR 180 conversion. Identify:
1. Main subjects and their depth layers (foreground, middle, background)
2. Camera movement type (static, pan, zoom, etc.)
3. Scene complexity (simple, moderate, complex)
4. Recommended depth baseline (0.05-0.2 range)
Provide a brief analysis in 2-3 sentences focusing on depth optimization.
"""
response = model.generate_content([prompt, pil_image])
return response.text
except Exception as e:
return f"Gemini analysis unavailable: {str(e)}"
return "Gemini API key not provided. Using default depth processing."
# Main processing function
def process_video_to_vr180(video_file):
"""Main function to convert 2D video to VR 180"""
# Create temporary file
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
tmp_file.write(video_file.read())
temp_video_path = tmp_file.name
try:
st.session_state.processing_status = "Extracting frames from video..."
# Extract frames
frames, fps = extract_frames(temp_video_path)
if not frames:
st.error("Could not extract frames from video")
return None
st.session_state.processing_status = f"Extracted {len(frames)} frames. Loading AI models..."
# Load depth estimation model
processor, model = load_depth_model()
if processor is None or model is None:
st.error("Could not load depth estimation model")
return None
# Get Gemini insights from first frame
st.session_state.processing_status = "Getting AI insights for optimal depth processing..."
gemini_insights = get_gemini_insights(frames[0])
st.info(f"AI Analysis: {gemini_insights}")
# Process frames
st.session_state.processing_status = "Processing frames for VR 180 conversion..."
stereo_frames = []
progress_bar = st.progress(0)
status_placeholder = st.empty()
for i, frame in enumerate(frames):
try:
status_placeholder.text(f"Processing frame {i+1}/{len(frames)}...")
# Generate depth map
depth_map = generate_depth_map(frame, processor, model)
if depth_map is not None:
# Create stereoscopic frame
stereo_frame = create_stereoscopic_frame(frame, depth_map)
if stereo_frame is not None:
stereo_frames.append(stereo_frame)
status_placeholder.text(f"✅ Frame {i+1}/{len(frames)} processed successfully")
else:
status_placeholder.text(f"⚠️ Failed to create stereo frame {i+1}")
else:
status_placeholder.text(f"⚠️ Failed to generate depth map for frame {i+1}")
except Exception as e:
status_placeholder.text(f"❌ Error processing frame {i+1}: {str(e)}")
# Continue with next frame instead of stopping
continue
# Update progress
progress_bar.progress((i + 1) / len(frames))
# Small delay to show progress
time.sleep(0.1)
if not stereo_frames:
st.error("No frames could be processed")
return None
st.session_state.processing_status = "Creating VR 180 video..."
# Create video from processed frames
output_path = tempfile.mktemp(suffix='_vr180.mp4')
# Use moviepy to create video
clip = ImageSequenceClip(stereo_frames, fps=fps/2) # Reduce fps due to processing
clip.write_videofile(output_path, codec='libx264', verbose=False)
st.session_state.processing_status = "VR 180 video created successfully!"
return output_path
except Exception as e:
st.error(f"Error processing video: {str(e)}")
return None
finally:
# Clean up temporary file
if os.path.exists(temp_video_path):
os.unlink(temp_video_path)
# Main UI
def main():
st.title("🥽 VR 180 Video Converter")
st.markdown("Convert your 2D videos into immersive VR 180 experiences using AI-powered depth estimation")
# Setup Gemini API
has_gemini = setup_gemini()
# Main interface
col1, col2 = st.columns([2, 1])
with col1:
st.header("Upload Your Video")
uploaded_file = st.file_uploader(
"Choose a video file",
type=['mp4', 'avi', 'mov', 'mkv'],
help="Upload your 2D video to convert to VR 180 format"
)
# --- FIX STARTS HERE ---
if uploaded_file is not None:
# When a new file is uploaded, reset the state
if st.session_state.get('processed_video_path') and uploaded_file.name != st.session_state.get('last_file_name'):
st.session_state.processed_video_path = None
st.session_state.processing_status = ""
st.video(uploaded_file)
# Conversion Settings
st.subheader("Conversion Settings")
col_a, col_b = st.columns(2)
with col_a:
depth_strength = st.slider("Depth Effect Strength", 0.05, 0.3, 0.1, 0.01)
with col_b:
quality_setting = st.selectbox("Processing Quality", ["Fast (Low Quality)", "Balanced", "High Quality (Slow)"], index=1)
# --- LOGIC CHANGE PART 1: The Convert Button ---
# This button's only job is to start the process and set the session state.
if st.button("🚀 Convert to VR 180", type="primary"):
if not has_gemini:
st.warning("For best results, please provide a Gemini API key in the sidebar")
with st.spinner("Processing video..."):
result_path = process_video_to_vr180(uploaded_file)
if result_path:
st.session_state.processed_video_path = result_path
st.session_state.last_file_name = uploaded_file.name # Remember which file was processed
# --- LOGIC CHANGE PART 2: The Download Button ---
# This block is now OUTSIDE the 'if st.button' block.
# It runs if a video has been successfully processed in the current session.
if st.session_state.get('processed_video_path'):
st.success("✅ Conversion completed!")
try:
with open(st.session_state.processed_video_path, 'rb') as f:
st.download_button(
"📥 Download VR 180 Video",
data=f.read(),
file_name="converted_vr180.mp4",
mime="video/mp4"
)
st.info("Your VR 180 video is ready! You can now view it in any VR headset.")
except FileNotFoundError:
st.error("The processed video file was not found. This can happen if the app restarts. Please convert the video again.")
st.session_state.processed_video_path = None # Reset state
# --- FIX ENDS HERE ---
with col2:
st.header("How It Works")
with st.expander("🔍 AI Depth Analysis"):
st.write("""
Our platform uses advanced AI models to:
- Analyze each frame for depth information
- Identify foreground and background elements
- Generate accurate depth maps
""")
with st.expander("🎬 VR 180 Creation"):
st.write("""
The conversion process:
1. Extracts frames from your video
2. Generates depth maps using AI
3. Creates left/right eye views
4. Combines into side-by-side VR format
""")
with st.expander("📱 VR Compatibility"):
st.write("""
Compatible with:
- Meta Quest/Quest 2/Quest 3
- PICO VR headsets
- Google Cardboard
- Any VR player supporting SBS 180°
""")
if st.session_state.processing_status:
st.info(st.session_state.processing_status)
# Footer
st.markdown("---")
st.markdown("""
**Tips for best results:**
- Use videos with clear depth layers (foreground/background)
- Avoid videos with extreme camera movements
- Shorter clips (30-60 seconds) process faster
- Ensure good lighting in your source video
""")
if __name__ == "__main__":
main()