MySafeCode commited on
Commit
e0a1fd2
·
verified ·
1 Parent(s): 3325de6

Upload 6 files

Browse files
Files changed (6) hide show
  1. .gitignore +50 -0
  2. Dockerfile +22 -0
  3. README.md +38 -10
  4. app.py +86 -0
  5. processor.py +121 -0
  6. requirements.txt +8 -0
.gitignore ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .DS_Store
4
+ .vscode/
5
+ *.mp4
6
+ *.avi
7
+ *.mov
8
+ temp/
9
+ /tmp/
10
+ *.log
11
+ .env
12
+ venv/
13
+ env/
14
+ .venv/
15
+ *.sock
16
+ *.pid
17
+ *.seed
18
+ *.pid.lock
19
+ __pycache__/
20
+ *.py[cod]
21
+ *$py.class
22
+ *.so
23
+ .Python
24
+ build/
25
+ develop-eggs/
26
+ dist/
27
+ downloads/
28
+ eggs/
29
+ .eggs/
30
+ lib/
31
+ lib64/
32
+ parts/
33
+ sdist/
34
+ var/
35
+ *.egg-info/
36
+ .installed.cfg
37
+ *.egg
38
+ *.manifest
39
+ *.spec
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+ .tox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .pytest_cache/
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ libgl1-mesa-glx \
8
+ libglib2.0-0 \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ COPY . .
15
+
16
+ # Create temp directory
17
+ RUN mkdir -p /tmp/video-bg-remover
18
+
19
+ # For Hugging Face Spaces
20
+ ENV PORT=7860
21
+
22
+ CMD uvicorn app:app --host 0.0.0.0 --port $PORT
README.md CHANGED
@@ -1,10 +1,38 @@
1
- ---
2
- title: Depthmap
3
- emoji: 👀
4
- colorFrom: purple
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Depth Video Background Remover
3
+ emoji: 🎥
4
+ colorFrom: purple
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # Depth Video Background Remover
11
+
12
+ Remove backgrounds from videos using AI depth estimation - no green screen needed!
13
+
14
+ ## How it works
15
+ 1. Upload a video
16
+ 2. Adjust depth threshold (lower = more background removed)
17
+ 3. Pick a background color
18
+ 4. Click process and download!
19
+
20
+ ## Technical details
21
+ - Uses MiDaS (small) for depth estimation
22
+ - Runs on CPU/GPU (T4 on Spaces)
23
+ - Processes frame-by-frame with PyTorch
24
+
25
+ ## Features
26
+ - ✨ AI-powered depth estimation
27
+ - 🎨 Customizable background color
28
+ - 📁 Drag & drop upload
29
+ - ⚡ Fast processing with PyTorch
30
+ - 📥 One-click download
31
+
32
+ ## Requirements
33
+ - Python 3.8+
34
+ - PyTorch 2.0+
35
+ - CUDA-capable GPU (optional)
36
+
37
+ ## License
38
+ Apache 2.0
app.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, Form, HTTPException
2
+ from fastapi.responses import StreamingResponse, HTMLResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ import uvicorn
5
+ import os
6
+ import shutil
7
+ from typing import Optional
8
+ import uuid
9
+ from pathlib import Path
10
+
11
+ from processor import VideoProcessor
12
+
13
+ # Create FastAPI app
14
+ app = FastAPI()
15
+
16
+ # Mount static files
17
+ app.mount("/static", StaticFiles(directory="static"), name="static")
18
+
19
+ # Create temp directory
20
+ TEMP_DIR = Path("/tmp") / "video-bg-remover"
21
+ TEMP_DIR.mkdir(exist_ok=True)
22
+
23
+ # Initialize processor
24
+ print("Loading MiDaS model...")
25
+ processor = VideoProcessor()
26
+ print("Model loaded!")
27
+
28
+ @app.get("/", response_class=HTMLResponse)
29
+ async def root():
30
+ """Serve the frontend"""
31
+ with open("static/index.html", "r") as f:
32
+ return f.read()
33
+
34
+ @app.post("/process")
35
+ async def process_video(
36
+ file: UploadFile = File(...),
37
+ threshold: float = Form(0.3),
38
+ bg_color: str = Form("#00FF00")
39
+ ):
40
+ """
41
+ Process video: remove background using depth estimation
42
+ """
43
+ # Validate file
44
+ if not file.content_type.startswith('video/'):
45
+ raise HTTPException(400, "File must be a video")
46
+
47
+ # Generate unique ID
48
+ session_id = str(uuid.uuid4())
49
+
50
+ # Save uploaded file
51
+ input_path = TEMP_DIR / f"{session_id}_input.mp4"
52
+ with open(input_path, "wb") as buffer:
53
+ shutil.copyfileobj(file.file, buffer)
54
+
55
+ try:
56
+ # Process video
57
+ output_path = await processor.process_video(
58
+ input_path=str(input_path),
59
+ threshold=threshold,
60
+ bg_color=bg_color,
61
+ session_id=session_id
62
+ )
63
+
64
+ # Stream result back
65
+ def iterfile():
66
+ with open(output_path, "rb") as f:
67
+ yield from f
68
+ # Cleanup
69
+ os.unlink(str(input_path))
70
+ os.unlink(output_path)
71
+
72
+ return StreamingResponse(
73
+ iterfile(),
74
+ media_type="video/mp4",
75
+ headers={"Content-Disposition": f"attachment; filename=processed.mp4"}
76
+ )
77
+
78
+ except Exception as e:
79
+ # Cleanup on error
80
+ if input_path.exists():
81
+ input_path.unlink()
82
+ raise HTTPException(500, str(e))
83
+
84
+ # For Hugging Face Spaces
85
+ if __name__ == "__main__":
86
+ uvicorn.run(app, host="0.0.0.0", port=7860)
processor.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn.functional as F
3
+ import cv2
4
+ import numpy as np
5
+ from PIL import Image
6
+ from pathlib import Path
7
+ import asyncio
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ import gc
10
+
11
+ class VideoProcessor:
12
+ def __init__(self):
13
+ # Use CPU if no GPU
14
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
15
+ print(f"Using device: {self.device}")
16
+
17
+ # Load MiDaS (small model for speed)
18
+ self.model = torch.hub.load("intel-isl/MiDaS", "MiDaS_small")
19
+ self.model.to(self.device)
20
+ self.model.eval()
21
+
22
+ # Load transforms
23
+ midas_transforms = torch.hub.load("intel-isl/MiDaS", "transforms")
24
+ self.transform = midas_transforms.small_transform
25
+
26
+ self.executor = ThreadPoolExecutor(max_workers=1)
27
+
28
+ def hex_to_rgb(self, hex_color: str):
29
+ """Convert hex to RGB"""
30
+ hex_color = hex_color.lstrip('#')
31
+ return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
32
+
33
+ async def process_video(self, input_path: str, threshold: float,
34
+ bg_color: str, session_id: str) -> str:
35
+ """Process video asynchronously"""
36
+ loop = asyncio.get_event_loop()
37
+ output_path = str(Path("/tmp") / f"{session_id}_output.mp4")
38
+
39
+ # Run in thread pool
40
+ await loop.run_in_executor(
41
+ self.executor,
42
+ self._process_video_sync,
43
+ input_path, output_path, threshold, bg_color
44
+ )
45
+
46
+ return output_path
47
+
48
+ def _process_video_sync(self, input_path: str, output_path: str,
49
+ threshold: float, bg_color: str):
50
+ """Synchronous video processing"""
51
+ cap = cv2.VideoCapture(input_path)
52
+ fps = int(cap.get(cv2.CAP_PROP_FPS))
53
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
54
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
55
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
56
+
57
+ # Output video
58
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
59
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
60
+
61
+ bg_rgb = self.hex_to_rgb(bg_color)
62
+ frame_count = 0
63
+
64
+ while cap.isOpened():
65
+ ret, frame = cap.read()
66
+ if not ret:
67
+ break
68
+
69
+ # Process frame
70
+ processed = self.process_frame(frame, threshold, bg_rgb)
71
+ out.write(processed)
72
+
73
+ frame_count += 1
74
+ if frame_count % 30 == 0:
75
+ print(f"Progress: {frame_count}/{total_frames}")
76
+
77
+ # Clear cache occasionally
78
+ if frame_count % 100 == 0:
79
+ gc.collect()
80
+ if torch.cuda.is_available():
81
+ torch.cuda.empty_cache()
82
+
83
+ cap.release()
84
+ out.release()
85
+
86
+ def process_frame(self, frame: np.ndarray, threshold: float,
87
+ bg_color: tuple) -> np.ndarray:
88
+ """Process a single frame"""
89
+ # Resize for speed
90
+ h, w = frame.shape[:2]
91
+ new_h, new_w = 256, int(256 * w / h)
92
+
93
+ frame_small = cv2.resize(frame, (new_w, new_h))
94
+ frame_rgb = cv2.cvtColor(frame_small, cv2.COLOR_BGR2RGB)
95
+
96
+ # Get depth map
97
+ img = Image.fromarray(frame_rgb)
98
+ input_batch = self.transform(img).to(self.device)
99
+
100
+ with torch.no_grad():
101
+ depth = self.model(input_batch)
102
+ depth = F.interpolate(
103
+ depth.unsqueeze(1),
104
+ size=(new_h, new_w),
105
+ mode="bicubic",
106
+ align_corners=False
107
+ ).squeeze().cpu().numpy()
108
+
109
+ # Normalize depth
110
+ depth_norm = (depth - depth.min()) / (depth.max() - depth.min() + 1e-8)
111
+
112
+ # Create mask and resize to original
113
+ mask = (depth_norm > threshold).astype(np.uint8) * 255
114
+ mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_LINEAR)
115
+ mask = mask.astype(bool)
116
+
117
+ # Apply background
118
+ result = frame.copy()
119
+ result[~mask] = bg_color
120
+
121
+ return result
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn==0.24.0
3
+ torch==2.1.0
4
+ torchvision==0.16.0
5
+ opencv-python-headless==4.8.1.78
6
+ numpy==1.24.3
7
+ Pillow==10.1.0
8
+ python-multipart==0.0.6