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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +30 -224
app.py CHANGED
@@ -1,18 +1,9 @@
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,228 +59,43 @@ def write_xmp_to_jpg(input_path, output_path, width, height):
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
- )
 
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
  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()