nakas commited on
Commit
77f33ba
·
verified ·
1 Parent(s): efe5371

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +224 -30
app.py CHANGED
@@ -1,9 +1,18 @@
1
  import gradio as gr
2
- from PIL import Image
 
3
  import os
 
 
 
 
4
  from datetime import datetime
5
  import struct
6
 
 
 
 
 
7
  def create_xmp_block(width, height):
8
  """Create XMP metadata block following ExifTool's exact format."""
9
  xmp = (
@@ -59,43 +68,228 @@ def write_xmp_to_jpg(input_path, output_path, width, height):
59
  with open(output_path, 'wb') as f:
60
  f.write(output)
61
 
62
- def add_360_metadata(input_image):
63
- """Add 360 photo metadata to an image file."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  try:
65
- # Open and verify the image
66
- img = Image.open(input_image)
67
- if img.width != 2 * img.height:
68
- raise gr.Error("Image must have 2:1 aspect ratio for equirectangular projection")
69
-
70
- # Create output filename
71
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
72
- output_filename = f"360_photo_{timestamp}.jpg"
73
- output_path = os.path.join("/tmp", output_filename)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- # First save as high-quality JPEG
76
- img.save(output_path, "JPEG", quality=95)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- # Then inject XMP metadata directly into JPEG file
79
- write_xmp_to_jpg(output_path, output_path, img.width, img.height)
80
 
81
- return output_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  except Exception as e:
84
- raise gr.Error(f"Error processing image: {str(e)}")
 
85
 
86
  # Create Gradio interface
87
  iface = gr.Interface(
88
- fn=add_360_metadata,
89
- inputs=gr.Image(type="filepath", label="Upload 360° Photo"),
90
- outputs=gr.Image(type="filepath", label="360° Photo with Metadata"),
91
- title="360° Photo Metadata Adder",
92
- description=(
93
- "Upload an equirectangular 360° photo to add metadata for Google Photos and other 360° viewers.\n"
94
- "Important: Image must have 2:1 aspect ratio (width = 2 × height)."
95
- ),
96
- examples=[],
97
- cache_examples=False
 
 
 
 
 
 
 
 
 
 
 
98
  )
99
 
100
- # Launch the interface
101
- iface.launch()
 
 
 
 
 
1
  import gradio as gr
2
+ import cv2
3
+ import numpy as np
4
  import os
5
+ import gc
6
+ from tqdm import tqdm
7
+ import logging
8
+ from PIL import Image
9
  from datetime import datetime
10
  import struct
11
 
12
+ # Set up logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
  def create_xmp_block(width, height):
17
  """Create XMP metadata block following ExifTool's exact format."""
18
  xmp = (
 
68
  with open(output_path, 'wb') as f:
69
  f.write(output)
70
 
71
+ def preprocess_frame(frame):
72
+ """Preprocess frame with improved feature detection"""
73
+ target_height = 1080
74
+ aspect_ratio = frame.shape[1] / frame.shape[0]
75
+ target_width = int(target_height * aspect_ratio)
76
+ frame = cv2.resize(frame, (target_width, target_height))
77
+
78
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
79
+ lab = cv2.cvtColor(frame, cv2.COLOR_BGR2LAB)
80
+ l, a, b = cv2.split(lab)
81
+ cl = clahe.apply(l)
82
+ enhanced = cv2.merge((cl,a,b))
83
+ enhanced = cv2.cvtColor(enhanced, cv2.COLOR_LAB2BGR)
84
+
85
+ return enhanced
86
+
87
+ def extract_frames(video_path, num_frames=24):
88
+ """Extract frames with progress tracking"""
89
  try:
90
+ logger.info(f"Opening video: {video_path}")
91
+ cap = cv2.VideoCapture(video_path)
92
+ if not cap.isOpened():
93
+ raise Exception("Could not open video file")
94
+
95
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
96
+ frame_indices = np.linspace(0, total_frames-1, num_frames, dtype=int)
97
+ frames = []
98
+
99
+ for idx in frame_indices:
100
+ cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
101
+ ret, frame = cap.read()
102
+ if ret:
103
+ processed = preprocess_frame(frame)
104
+ frames.append(processed)
105
+ gc.collect()
106
+
107
+ cap.release()
108
+ logger.info(f"Extracted {len(frames)} frames")
109
+ return frames
110
+
111
+ except Exception as e:
112
+ if 'cap' in locals():
113
+ cap.release()
114
+ raise Exception(f"Frame extraction failed: {str(e)}")
115
+
116
+ def create_360_panorama(frames):
117
+ """Create an equirectangular panorama with better stitching and wide-angle adjustment"""
118
+ try:
119
+ if len(frames) < 2:
120
+ raise Exception("Need at least 2 frames")
121
+
122
+ # iPhone wide angle is typically around 120 degrees vertical FOV
123
+ # We'll adjust the output size to account for this
124
+ vertical_fov = 120 # degrees
125
+ total_vertical_fov = 180 # full equirectangular height
126
+
127
+ # Calculate padding needed
128
+ padding_ratio = (total_vertical_fov - vertical_fov) / (2 * total_vertical_fov)
129
 
130
+ # Create stitcher with custom settings
131
+ stitcher = cv2.Stitcher.create(cv2.Stitcher_PANORAMA)
132
+ stitcher.setPanoConfidenceThresh(0.8)
133
+
134
+ logger.info("Starting panorama stitching...")
135
+ status, panorama = stitcher.stitch(frames)
136
+
137
+ if status != cv2.Stitcher_OK:
138
+ raise Exception(f"Stitching failed with status {status}")
139
+
140
+ # Calculate target dimensions
141
+ target_height = 1080
142
+ target_width = target_height * 2 # 2:1 aspect ratio for equirectangular
143
+
144
+ # Resize stitched panorama
145
+ panorama = cv2.resize(panorama, (target_width, int(target_height * (1 - 2*padding_ratio))))
146
+
147
+ # Create final image with padding
148
+ final_panorama = np.zeros((target_height, target_width, 3), dtype=np.uint8)
149
 
150
+ # Calculate padding pixels
151
+ pad_pixels = int(target_height * padding_ratio)
152
 
153
+ # Place the panorama in the middle
154
+ final_panorama[pad_pixels:target_height-pad_pixels, :] = panorama
155
+
156
+ # Apply slight feathering at the edges to avoid hard transitions
157
+ feather_size = int(pad_pixels * 0.3)
158
+ for i in range(feather_size):
159
+ alpha = i / feather_size
160
+ # Feather top
161
+ final_panorama[pad_pixels-feather_size+i, :] = \
162
+ (panorama[0, :] * alpha).astype(np.uint8)
163
+ # Feather bottom
164
+ final_panorama[target_height-pad_pixels+i, :] = \
165
+ (panorama[-1, :] * (1-alpha)).astype(np.uint8)
166
+
167
+ logger.info(f"Created panorama of size {final_panorama.shape} with vertical FOV adjustment")
168
+ return final_panorama
169
+
170
+ except Exception as e:
171
+ raise Exception(f"360° panorama creation failed: {str(e)}")
172
+
173
+ def equirect_to_cubemap(equirect):
174
+ """Convert equirectangular image to cubemap"""
175
+ face_size = equirect.shape[0] // 2
176
+ cubemap = np.zeros((face_size * 3, face_size * 4, 3), dtype=np.uint8)
177
 
178
+ rotations = [
179
+ (0, 0, 0), # front
180
+ (0, 90, 0), # right
181
+ (0, 180, 0), # back
182
+ (0, 270, 0), # left
183
+ (-90, 0, 0), # top
184
+ (90, 0, 0) # bottom
185
+ ]
186
+
187
+ for i, rotation in enumerate(rotations):
188
+ x = (i % 4) * face_size
189
+ y = (i // 4) * face_size
190
+
191
+ R = cv2.Rodrigues(np.array([rotation[0] * np.pi / 180,
192
+ rotation[1] * np.pi / 180,
193
+ rotation[2] * np.pi / 180]))[0]
194
+
195
+ for u in range(face_size):
196
+ for v in range(face_size):
197
+ x_3d = (2 * u / face_size - 1)
198
+ y_3d = (2 * v / face_size - 1)
199
+ z_3d = 1.0
200
+
201
+ point = R.dot(np.array([x_3d, y_3d, z_3d]))
202
+ theta = np.arctan2(point[0], point[2])
203
+ phi = np.arctan2(point[1], np.sqrt(point[0]**2 + point[2]**2))
204
+
205
+ u_equi = int((theta + np.pi) * equirect.shape[1] / (2 * np.pi))
206
+ v_equi = int((phi + np.pi/2) * equirect.shape[0] / np.pi)
207
+
208
+ if 0 <= u_equi < equirect.shape[1] and 0 <= v_equi < equirect.shape[0]:
209
+ cubemap[y+v, x+u] = equirect[v_equi, u_equi]
210
+
211
+ return cubemap
212
+
213
+ def process_video(video):
214
+ """Main processing function for Gradio interface"""
215
+ try:
216
+ if video is None:
217
+ return None, None, "Please upload a video file."
218
+
219
+ video_path = video
220
+ if not os.path.exists(video_path):
221
+ return None, None, "Error: Video file not found."
222
+
223
+ # Log the working directory and file permission
224
+ logger.info(f"Working directory: {os.getcwd()}")
225
+ logger.info(f"Video path exists: {os.path.exists(video_path)}")
226
+ logger.info(f"Video path permissions: {oct(os.stat(video_path).st_mode)[-3:]}")
227
+
228
+ # Extract frames
229
+ frames = extract_frames(video_path, num_frames=24)
230
+ if not frames:
231
+ return None, None, "Error: No frames could be extracted from the video."
232
+
233
+ # Create panorama
234
+ equirect = create_360_panorama(frames)
235
+ logger.info("Created equirectangular panorama")
236
+
237
+ # Create cubemap
238
+ cubemap = equirect_to_cubemap(equirect)
239
+ logger.info("Created cubemap")
240
+
241
+ # Save paths
242
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
243
+ equirect_path = f"360_photo_{timestamp}.jpg"
244
+ cubemap_path = f"cubemap_{timestamp}.jpg"
245
+
246
+ # Save equirectangular image
247
+ logger.info("Saving equirectangular image...")
248
+ cv2.imwrite(equirect_path, equirect)
249
+
250
+ # Add metadata to equirectangular image
251
+ height, width = equirect.shape[:2]
252
+ write_xmp_to_jpg(equirect_path, equirect_path, width, height)
253
+ logger.info("Added 360 metadata to equirectangular image")
254
+
255
+ # Save cubemap
256
+ logger.info("Saving cubemap...")
257
+ cv2.imwrite(cubemap_path, cubemap)
258
+
259
+ return equirect_path, cubemap_path, "Processing completed successfully!"
260
+
261
  except Exception as e:
262
+ logger.error(f"Error in process_video: {str(e)}")
263
+ return None, None, f"Error during processing: {str(e)}"
264
 
265
  # Create Gradio interface
266
  iface = gr.Interface(
267
+ fn=process_video,
268
+ inputs=gr.Video(label="Upload 360° Video"),
269
+ outputs=[
270
+ gr.Image(label="360° Photo (with metadata)"),
271
+ gr.Image(label="Cubemap View"),
272
+ gr.Textbox(label="Status")
273
+ ],
274
+ title="360° Video to Photo Converter",
275
+ description="""
276
+ Upload a 360° panoramic video (shot with iPhone wide-angle lens) to convert it into:
277
+ 1. 360° Photo with proper metadata (can be viewed in Google Photos, Facebook, etc.)
278
+ 2. Cubemap view
279
+
280
+ Tips for best results:
281
+ - Keep video length under 30 seconds
282
+ - Ensure steady camera motion
283
+ - Video should complete a full 360° rotation
284
+ - Maintain consistent camera height
285
+ - Good lighting conditions help with stitching
286
+ """,
287
+ flagging_mode="never"
288
  )
289
 
290
+ # Launch with queue
291
+ if __name__ == "__main__":
292
+ iface.queue().launch(
293
+ server_name="0.0.0.0",
294
+ server_port=7860
295
+ )