MySafeCode commited on
Commit
48496fe
Β·
verified Β·
1 Parent(s): df164b1

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +429 -207
app.py CHANGED
@@ -1,207 +1,429 @@
1
- import faulthandler
2
- faulthandler.enable()
3
-
4
-
5
- import gradio as gr
6
- import cv2
7
- import numpy as np
8
- import trimesh
9
- import tempfile
10
- import os
11
-
12
- # -------------------------
13
- # GLOBAL (checkerboard persistence)
14
- # -------------------------
15
- _checkerboard_colors = None
16
-
17
- # -------------------------
18
- # VIDEO LOADING (BGR β†’ RGB FIXED βœ…)
19
- # -------------------------
20
- def read_video_frames(video_path, start=0, end=None, frame_step=1):
21
- cap = cv2.VideoCapture(video_path)
22
- frames = []
23
-
24
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
25
- if end is None:
26
- end = total_frames
27
-
28
- count = 0
29
-
30
- while True:
31
- ret, frame = cap.read()
32
- if not ret or count >= end:
33
- break
34
-
35
- if count >= start and (count - start) % frame_step == 0:
36
- # FIX COLOR ORDER HERE
37
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
38
- frames.append(frame)
39
-
40
- count += 1
41
-
42
- cap.release()
43
- return np.array(frames)
44
-
45
-
46
- # -------------------------
47
- # DOWNSAMPLING
48
- # -------------------------
49
- def downsample_frames(frames, block_size=1, method='stride'):
50
- if block_size == 1:
51
- return frames
52
-
53
- z, h, w, c = frames.shape
54
-
55
- if method == 'stride':
56
- return frames[:, ::block_size, ::block_size]
57
-
58
- elif method == 'mean':
59
- new_h = h // block_size
60
- new_w = w // block_size
61
- out = np.zeros((z, new_h, new_w, c), dtype=np.uint8)
62
-
63
- for zi in range(z):
64
- for i in range(new_h):
65
- for j in range(new_w):
66
- block = frames[
67
- zi,
68
- i*block_size:(i+1)*block_size,
69
- j*block_size:(j+1)*block_size
70
- ]
71
- out[zi, i, j] = block.mean(axis=(0,1))
72
- return out
73
-
74
-
75
- # -------------------------
76
- # VOXEL MASK
77
- # -------------------------
78
- def frames_to_voxels(frames, threshold=10):
79
- return (np.sum(frames, axis=3) > threshold)
80
-
81
-
82
- # -------------------------
83
- # VOXEL β†’ MESH (FIXED COLORS βœ…)
84
- # -------------------------
85
- def voxels_to_mesh(frames, voxels, voxel_size=1.0):
86
- meshes = []
87
- z_len, h, w = voxels.shape
88
-
89
- for z in range(z_len):
90
- for y in range(h):
91
- for x in range(w):
92
- if voxels[z, y, x]:
93
- color = frames[z, frames.shape[1] - 1 - y, x].astype(np.uint8)
94
-
95
- cube = trimesh.creation.box(extents=[voxel_size]*3)
96
- cube.apply_translation([x, y, z])
97
-
98
- # Apply colors correctly (RGBA uint8)
99
- rgba = np.append(color, 255)
100
- cube.visual.face_colors = np.tile(rgba, (12,1))
101
-
102
- meshes.append(cube)
103
-
104
- if meshes:
105
- return trimesh.util.concatenate(meshes)
106
- return trimesh.Scene()
107
-
108
-
109
- # -------------------------
110
- # RANDOM CHECKERBOARD (ONE-TIME COLORS βœ…)
111
- # -------------------------
112
- def default_checkerboard():
113
- global _checkerboard_colors
114
-
115
- h, w, z_len = 10, 10, 2
116
- frames = np.zeros((z_len, h, w, 3), dtype=np.uint8)
117
-
118
- if _checkerboard_colors is None:
119
- _checkerboard_colors = np.random.randint(
120
- 0, 256, size=(z_len, h, w, 3), dtype=np.uint8
121
- )
122
-
123
- for z in range(z_len):
124
- for y in range(h):
125
- for x in range(w):
126
- if (x + y + z) % 2 == 0:
127
- frames[z, y, x] = [0, 0, 0]
128
- else:
129
- frames[z, y, x] = _checkerboard_colors[z, y, x]
130
-
131
- voxels = frames_to_voxels(frames, threshold=1)
132
- mesh = voxels_to_mesh(frames, voxels, voxel_size=2)
133
-
134
- tmp = tempfile.gettempdir()
135
- obj = os.path.join(tmp, "checkerboard.obj")
136
- glb = os.path.join(tmp, "checkerboard.glb")
137
-
138
- mesh.export(obj)
139
- mesh.export(glb)
140
-
141
- return obj, glb, glb
142
-
143
-
144
- # -------------------------
145
- # MAIN GENERATOR
146
- # -------------------------
147
- def generate_voxel_files(
148
- video_file,
149
- start_frame,
150
- end_frame,
151
- frame_step,
152
- block_size,
153
- downsample_method
154
- ):
155
- if video_file is None:
156
- return default_checkerboard()
157
-
158
- frames = read_video_frames(
159
- video_file.name,
160
- start=start_frame,
161
- end=end_frame,
162
- frame_step=frame_step
163
- )
164
-
165
- frames = downsample_frames(
166
- frames,
167
- block_size=block_size,
168
- method=downsample_method
169
- )
170
-
171
- voxels = frames_to_voxels(frames)
172
- mesh = voxels_to_mesh(frames, voxels)
173
-
174
- tmp = tempfile.gettempdir()
175
- obj = os.path.join(tmp, "output.obj")
176
- glb = os.path.join(tmp, "output.glb")
177
-
178
- mesh.export(obj)
179
- mesh.export(glb)
180
-
181
- return obj, glb, glb
182
-
183
-
184
- # -------------------------
185
- # GRADIO UI
186
- # -------------------------
187
- iface = gr.Interface(
188
- fn=generate_voxel_files,
189
- inputs=[
190
- gr.File(label="Upload MP4 (or leave empty for checkerboard)"),
191
- gr.Slider(0, 500, value=0, step=1, label="Start Frame"),
192
- gr.Slider(0, 500, value=50, step=1, label="End Frame"),
193
- gr.Slider(1, 10, value=1, step=1, label="Frame Step"),
194
- gr.Slider(1, 32, value=1, step=1, label="Pixel Block Size"),
195
- gr.Radio(["stride", "mean"], value="stride", label="Downsample Method"),
196
- ],
197
- outputs=[
198
- gr.File(label="OBJ"),
199
- gr.File(label="GLB"),
200
- gr.Model3D(label="3D Preview"),
201
- ],
202
- title="MP4 β†’ Voxels β†’ 3D",
203
- description="If no file is uploaded, a random-color checkerboard appears."
204
- )
205
-
206
- if __name__ == "__main__":
207
- iface.launch(server_name="0.0.0.0", server_port=7860, debug=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import faulthandler
2
+ faulthandler.enable()
3
+
4
+ import gradio as gr
5
+ import cv2
6
+ import numpy as np
7
+ import trimesh
8
+ import tempfile
9
+ import os
10
+ import logging
11
+
12
+ # Set up logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Fix OpenGL issues for headless environments
17
+ os.environ['PYOPENGL_PLATFORM'] = 'osmesa'
18
+
19
+ # -------------------------
20
+ # GLOBAL (checkerboard persistence)
21
+ # -------------------------
22
+ _checkerboard_colors = None
23
+
24
+ # -------------------------
25
+ # VIDEO LOADING (BGR β†’ RGB FIXED βœ…)
26
+ # -------------------------
27
+ def read_video_frames(video_path, start=0, end=None, frame_step=1):
28
+ """Read video frames with proper error handling"""
29
+ try:
30
+ cap = cv2.VideoCapture(video_path)
31
+ if not cap.isOpened():
32
+ raise ValueError(f"Cannot open video file: {video_path}")
33
+
34
+ frames = []
35
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
36
+
37
+ if total_frames == 0:
38
+ raise ValueError("Video file appears to be empty or corrupted")
39
+
40
+ if end is None or end > total_frames:
41
+ end = total_frames
42
+
43
+ count = 0
44
+ frames_read = 0
45
+
46
+ while True:
47
+ ret, frame = cap.read()
48
+ if not ret or count >= end:
49
+ break
50
+
51
+ if count >= start and (count - start) % frame_step == 0:
52
+ # FIX COLOR ORDER HERE
53
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
54
+ frames.append(frame)
55
+ frames_read += 1
56
+
57
+ count += 1
58
+
59
+ cap.release()
60
+
61
+ if not frames:
62
+ raise ValueError("No frames could be read from the video")
63
+
64
+ logger.info(f"Successfully read {frames_read} frames")
65
+ return np.array(frames)
66
+
67
+ except Exception as e:
68
+ logger.error(f"Error reading video: {str(e)}")
69
+ raise
70
+
71
+ # -------------------------
72
+ # DOWNSAMPLING
73
+ # -------------------------
74
+ def downsample_frames(frames, block_size=1, method='stride'):
75
+ """Downsample frames with better error handling"""
76
+ if block_size == 1 or frames.size == 0:
77
+ return frames
78
+
79
+ z, h, w, c = frames.shape
80
+
81
+ if method == 'stride':
82
+ return frames[:, ::block_size, ::block_size]
83
+
84
+ elif method == 'mean':
85
+ new_h = h // block_size
86
+ new_w = w // block_size
87
+ out = np.zeros((z, new_h, new_w, c), dtype=np.uint8)
88
+
89
+ for zi in range(z):
90
+ for i in range(new_h):
91
+ for j in range(new_w):
92
+ block = frames[
93
+ zi,
94
+ i*block_size:(i+1)*block_size,
95
+ j*block_size:(j+1)*block_size
96
+ ]
97
+ if block.size > 0:
98
+ out[zi, i, j] = block.mean(axis=(0,1)).astype(np.uint8)
99
+ return out
100
+
101
+ return frames
102
+
103
+ # -------------------------
104
+ # VOXEL MASK
105
+ # -------------------------
106
+ def frames_to_voxels(frames, threshold=10):
107
+ """Convert frames to voxel representation"""
108
+ if frames.size == 0:
109
+ return np.array([])
110
+
111
+ # Ensure we're working with the right dimensions
112
+ if len(frames.shape) == 4:
113
+ return (np.sum(frames, axis=3) > threshold)
114
+ else:
115
+ raise ValueError("Frames must be 4D array (z, h, w, c)")
116
+
117
+ # -------------------------
118
+ # VOXEL β†’ MESH (FIXED COLORS βœ…)
119
+ # -------------------------
120
+ def voxels_to_mesh(frames, voxels, voxel_size=1.0):
121
+ """Convert voxels to mesh with proper color handling"""
122
+ if voxels.size == 0 or frames.size == 0:
123
+ return trimesh.Scene()
124
+
125
+ meshes = []
126
+ z_len, h, w = voxels.shape
127
+
128
+ for z in range(z_len):
129
+ for y in range(h):
130
+ for x in range(w):
131
+ if voxels[z, y, x]:
132
+ # Ensure we have valid frame dimensions
133
+ if z < frames.shape[0] and y < frames.shape[1] and x < frames.shape[2]:
134
+ color = frames[z, frames.shape[1] - 1 - y, x].astype(np.uint8)
135
+
136
+ try:
137
+ cube = trimesh.creation.box(extents=[voxel_size]*3)
138
+ cube.apply_translation([x, y, z])
139
+
140
+ # Apply colors correctly (RGBA uint8)
141
+ rgba = np.append(color, 255)
142
+ cube.visual.face_colors = np.tile(rgba, (12,1))
143
+
144
+ meshes.append(cube)
145
+ except Exception as e:
146
+ logger.warning(f"Could not create cube at position ({x}, {y}, {z}): {str(e)}")
147
+
148
+ if meshes:
149
+ try:
150
+ return trimesh.util.concatenate(meshes)
151
+ except Exception as e:
152
+ logger.warning(f"Could not concatenate meshes: {str(e)}")
153
+ return meshes[0] if meshes else trimesh.Scene()
154
+
155
+ return trimesh.Scene()
156
+
157
+ # -------------------------
158
+ # RANDOM CHECKERBOARD (ONE-TIME COLORS βœ…)
159
+ # -------------------------
160
+ def default_checkerboard():
161
+ """Generate a default checkerboard pattern"""
162
+ global _checkerboard_colors
163
+
164
+ h, w, z_len = 10, 10, 2
165
+ frames = np.zeros((z_len, h, w, 3), dtype=np.uint8)
166
+
167
+ if _checkerboard_colors is None:
168
+ _checkerboard_colors = np.random.randint(
169
+ 0, 256, size=(z_len, h, w, 3), dtype=np.uint8
170
+ )
171
+
172
+ for z in range(z_len):
173
+ for y in range(h):
174
+ for x in range(w):
175
+ if (x + y + z) % 2 == 0:
176
+ frames[z, y, x] = [0, 0, 0]
177
+ else:
178
+ frames[z, y, x] = _checkerboard_colors[z, y, x]
179
+
180
+ try:
181
+ voxels = frames_to_voxels(frames, threshold=1)
182
+ mesh = voxels_to_mesh(frames, voxels, voxel_size=2)
183
+
184
+ tmp = tempfile.gettempdir()
185
+ obj = os.path.join(tmp, "checkerboard.obj")
186
+ glb = os.path.join(tmp, "checkerboard.glb")
187
+
188
+ mesh.export(obj)
189
+ mesh.export(glb)
190
+
191
+ return obj, glb, glb
192
+ except Exception as e:
193
+ logger.error(f"Error creating checkerboard: {str(e)}")
194
+ raise
195
+
196
+ # -------------------------
197
+ # MAIN GENERATOR
198
+ # -------------------------
199
+ def generate_voxel_files(
200
+ video_file,
201
+ start_frame,
202
+ end_frame,
203
+ frame_step,
204
+ block_size,
205
+ downsample_method
206
+ ):
207
+ """Main function to generate voxel files from video"""
208
+ try:
209
+ if video_file is None:
210
+ logger.info("No video file provided, generating checkerboard")
211
+ return default_checkerboard()
212
+
213
+ # Ensure video_file has a valid name attribute
214
+ video_path = getattr(video_file, 'name', video_file)
215
+ if not video_path or not os.path.exists(video_path):
216
+ raise ValueError("Invalid video file path")
217
+
218
+ logger.info(f"Processing video: {video_path}")
219
+
220
+ frames = read_video_frames(
221
+ video_path,
222
+ start=start_frame,
223
+ end=end_frame,
224
+ frame_step=frame_step
225
+ )
226
+
227
+ if frames.size == 0:
228
+ raise ValueError("No frames could be processed")
229
+
230
+ frames = downsample_frames(
231
+ frames,
232
+ block_size=block_size,
233
+ method=downsample_method
234
+ )
235
+
236
+ voxels = frames_to_voxels(frames)
237
+ mesh = voxels_to_mesh(frames, voxels)
238
+
239
+ tmp = tempfile.gettempdir()
240
+ obj = os.path.join(tmp, "output.obj")
241
+ glb = os.path.join(tmp, "output.glb")
242
+
243
+ mesh.export(obj)
244
+ mesh.export(glb)
245
+
246
+ logger.info("Successfully generated voxel files")
247
+ return obj, glb, glb
248
+
249
+ except Exception as e:
250
+ logger.error(f"Error in generate_voxel_files: {str(e)}")
251
+ # Return checkerboard as fallback
252
+ return default_checkerboard()
253
+
254
+ # -------------------------
255
+ # GRADIO 6.0+ UI WITH BLOCKS
256
+ # -------------------------
257
+ def create_interface():
258
+ """Create Gradio 6.0+ compatible interface using Blocks"""
259
+
260
+ with gr.Blocks(
261
+ title="MP4 β†’ Voxels β†’ 3D",
262
+ theme=gr.themes.Soft(),
263
+ css="""
264
+ .gradio-container {max-width: 1200px !important; margin: auto !important;}
265
+ .output-file {margin: 10px 0;}
266
+ """
267
+ ) as interface:
268
+
269
+ gr.Markdown("# πŸ“Ή MP4 β†’ Voxels β†’ 3D")
270
+ gr.Markdown("Convert video files into voxelized 3D meshes. If no file is uploaded, a random-color checkerboard appears.")
271
+
272
+ with gr.Row():
273
+ with gr.Column(scale=1):
274
+ video_input = gr.File(
275
+ label="Upload MP4 Video",
276
+ file_types=["video"],
277
+ file_count="single"
278
+ )
279
+
280
+ gr.Markdown("### Frame Settings")
281
+ start_frame = gr.Slider(
282
+ minimum=0,
283
+ maximum=500,
284
+ value=0,
285
+ step=1,
286
+ label="Start Frame"
287
+ )
288
+
289
+ end_frame = gr.Slider(
290
+ minimum=0,
291
+ maximum=500,
292
+ value=50,
293
+ step=1,
294
+ label="End Frame"
295
+ )
296
+
297
+ frame_step = gr.Slider(
298
+ minimum=1,
299
+ maximum=10,
300
+ value=1,
301
+ step=1,
302
+ label="Frame Step"
303
+ )
304
+
305
+ gr.Markdown("### Processing Settings")
306
+ block_size = gr.Slider(
307
+ minimum=1,
308
+ maximum=32,
309
+ value=1,
310
+ step=1,
311
+ label="Pixel Block Size"
312
+ )
313
+
314
+ downsample_method = gr.Radio(
315
+ choices=["stride", "mean"],
316
+ value="stride",
317
+ label="Downsample Method"
318
+ )
319
+
320
+ process_btn = gr.Button("πŸ”„ Convert to Voxels", variant="primary")
321
+
322
+ with gr.Column(scale=2):
323
+ with gr.Row():
324
+ obj_output = gr.File(label="OBJ File", file_types=[".obj"])
325
+ glb_output = gr.File(label="GLB File", file_types=[".glb"])
326
+
327
+ model_3d = gr.Model3D(
328
+ label="3D Preview",
329
+ height=600,
330
+ camera_position=[0, 0, 0]
331
+ )
332
+
333
+ status = gr.Textbox(
334
+ label="Status",
335
+ value="Ready to process...",
336
+ interactive=False
337
+ )
338
+
339
+ # Event handlers
340
+ def update_status(message):
341
+ return gr.update(value=message)
342
+
343
+ def process_with_status(video_file, start, end, step, block, method):
344
+ status_update = gr.update(value="Processing video...")
345
+ yield [status_update, None, None, None]
346
+
347
+ try:
348
+ result = generate_voxel_files(video_file, start, end, step, block, method)
349
+ if result and len(result) == 3:
350
+ obj_path, glb_path, glb_preview = result
351
+ status_update = gr.update(value="βœ… Processing complete!")
352
+ yield [status_update, obj_path, glb_path, glb_preview]
353
+ else:
354
+ status_update = gr.update(value="❌ Processing failed")
355
+ yield [status_update, None, None, None]
356
+ except Exception as e:
357
+ logger.error(f"Processing error: {str(e)}")
358
+ status_update = gr.update(value=f"❌ Error: {str(e)}")
359
+ yield [status_update, None, None, None]
360
+
361
+ # Connect the button click event
362
+ process_btn.click(
363
+ fn=process_with_status,
364
+ inputs=[
365
+ video_input,
366
+ start_frame,
367
+ end_frame,
368
+ frame_step,
369
+ block_size,
370
+ downsample_method
371
+ ],
372
+ outputs=[
373
+ status,
374
+ obj_output,
375
+ glb_output,
376
+ model_3d
377
+ ]
378
+ )
379
+
380
+ # Auto-process when video is uploaded
381
+ video_input.upload(
382
+ fn=process_with_status,
383
+ inputs=[
384
+ video_input,
385
+ start_frame,
386
+ end_frame,
387
+ frame_step,
388
+ block_size,
389
+ downsample_method
390
+ ],
391
+ outputs=[
392
+ status,
393
+ obj_output,
394
+ glb_output,
395
+ model_3d
396
+ ]
397
+ )
398
+
399
+ # Examples
400
+ gr.Examples(
401
+ examples=[
402
+ [None, 0, 50, 1, 1, "stride"]
403
+ ],
404
+ inputs=[
405
+ video_input,
406
+ start_frame,
407
+ end_frame,
408
+ frame_step,
409
+ block_size,
410
+ downsample_method
411
+ ],
412
+ label="Example Configurations"
413
+ )
414
+
415
+ return interface
416
+
417
+ if __name__ == "__main__":
418
+ try:
419
+ interface = create_interface()
420
+ interface.launch(
421
+ server_name="0.0.0.0",
422
+ server_port=7860,
423
+ debug=True,
424
+ share=False,
425
+ show_error=True
426
+ )
427
+ except Exception as e:
428
+ logger.error(f"Failed to launch Gradio interface: {str(e)}")
429
+ raise