Update app.py
Browse files
app.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
"""
|
| 2 |
-
πΉ Video Background Replacer - IFRAME OPTIMIZED VERSION
|
| 3 |
Optimized for embedding in MyAvatar Railway app with Claude's recommendations
|
| 4 |
FIXED: Corrected MatAnyone API usage (processor.step() instead of processor.infer())
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import streamlit as st
|
|
@@ -102,6 +103,16 @@
|
|
| 102 |
margin: 20px 0;
|
| 103 |
}
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
/* Iframe-specific optimizations */
|
| 106 |
.block-container {
|
| 107 |
padding-top: 1rem;
|
|
@@ -240,10 +251,10 @@ def process_frame_matanyone(self, frame, is_first_frame=False, mask=None):
|
|
| 240 |
print(f"MatAnyone processing failed: {e}, using fallback")
|
| 241 |
return self.create_simple_mask(frame).astype(np.float32) / 255.0
|
| 242 |
|
| 243 |
-
def
|
| 244 |
-
"""
|
| 245 |
if self.use_matanyone:
|
| 246 |
-
# Use
|
| 247 |
alpha_mask = self.process_frame_matanyone(frame, is_first_frame, mask)
|
| 248 |
elif self.use_mediapipe:
|
| 249 |
try:
|
|
@@ -267,18 +278,66 @@ def process_frame(self, frame, background_image, is_first_frame=False, mask=None
|
|
| 267 |
# Simple fallback method
|
| 268 |
alpha_mask = self.create_simple_mask(frame).astype(np.float32) / 255.0
|
| 269 |
|
| 270 |
-
#
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
|
| 273 |
-
#
|
| 274 |
-
|
| 275 |
-
# mask values close to 0 = use background
|
| 276 |
-
result = frame * alpha_mask + bg_resized * (1 - alpha_mask)
|
| 277 |
|
| 278 |
-
return
|
| 279 |
|
| 280 |
def process_video(self, video_path, background_image, progress_callback=None):
|
| 281 |
-
"""Process entire video with background replacement"""
|
| 282 |
# Convert PIL background to OpenCV format
|
| 283 |
if isinstance(background_image, Image.Image):
|
| 284 |
background_image = cv2.cvtColor(np.array(background_image), cv2.COLOR_RGB2BGR)
|
|
@@ -309,8 +368,8 @@ def process_video(self, video_path, background_image, progress_callback=None):
|
|
| 309 |
is_first_frame = (frame_count == 0)
|
| 310 |
mask = None # Could add automatic mask detection here if needed
|
| 311 |
|
| 312 |
-
# Process frame
|
| 313 |
-
processed_frame = self.
|
| 314 |
out.write(processed_frame)
|
| 315 |
|
| 316 |
frame_count += 1
|
|
@@ -318,7 +377,8 @@ def process_video(self, video_path, background_image, progress_callback=None):
|
|
| 318 |
# Update progress
|
| 319 |
if progress_callback:
|
| 320 |
progress = frame_count / total_frames
|
| 321 |
-
|
|
|
|
| 322 |
|
| 323 |
finally:
|
| 324 |
cap.release()
|
|
@@ -329,20 +389,30 @@ def process_video(self, video_path, background_image, progress_callback=None):
|
|
| 329 |
def main():
|
| 330 |
# Compact header for iframe
|
| 331 |
st.markdown('<h1 class="main-header">πΉ Video Background Replacer</h1>', unsafe_allow_html=True)
|
| 332 |
-
st.markdown('<p style="text-align: center; font-size: 1.1rem; color: #666; margin-bottom: 2rem;">
|
| 333 |
|
| 334 |
# Initialize the background replacer with loading feedback
|
| 335 |
if 'replacer' not in st.session_state:
|
| 336 |
-
with st.spinner('π Loading AI Processing Interface...'):
|
| 337 |
st.session_state.replacer = BackgroundReplacer()
|
| 338 |
|
| 339 |
# Show initialization status
|
| 340 |
if hasattr(st.session_state.replacer, 'use_matanyone') and st.session_state.replacer.use_matanyone:
|
| 341 |
-
st.success('π MatAnyone AI Ready - Ultimate Quality!')
|
| 342 |
elif hasattr(st.session_state.replacer, 'use_mediapipe') and st.session_state.replacer.use_mediapipe:
|
| 343 |
-
st.success('π― MediaPipe AI Ready - Good Quality!')
|
| 344 |
else:
|
| 345 |
-
st.info('π± Basic Processing Ready')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
|
| 347 |
# Compact upload section
|
| 348 |
col1, col2 = st.columns(2)
|
|
@@ -374,10 +444,10 @@ def main():
|
|
| 374 |
# Processing section - SIMPLIFIED to avoid DOM manipulation issues
|
| 375 |
if uploaded_video and uploaded_background:
|
| 376 |
st.markdown("---")
|
| 377 |
-
st.markdown("### π Ready
|
| 378 |
|
| 379 |
# Process button (clean and stable)
|
| 380 |
-
if st.button("πΉ
|
| 381 |
# Save uploaded files
|
| 382 |
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_video:
|
| 383 |
tmp_video.write(uploaded_video.read())
|
|
@@ -390,30 +460,37 @@ def main():
|
|
| 390 |
|
| 391 |
with result_container:
|
| 392 |
st.markdown('<div class="processing-box">', unsafe_allow_html=True)
|
| 393 |
-
st.markdown("### π Processing Your Video...")
|
| 394 |
|
| 395 |
# Progress tracking
|
| 396 |
progress_bar = st.progress(0)
|
| 397 |
status_text = st.empty()
|
|
|
|
| 398 |
|
| 399 |
-
def update_progress(progress, frame_count, total_frames):
|
| 400 |
progress_bar.progress(progress)
|
| 401 |
status_text.text(f"Processing frame {frame_count}/{total_frames} ({progress*100:.1f}%)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
|
| 403 |
try:
|
| 404 |
# Process the video
|
| 405 |
-
st.info("π Starting video processing...")
|
| 406 |
output_path = st.session_state.replacer.process_video(
|
| 407 |
video_path, background_image, update_progress
|
| 408 |
)
|
| 409 |
|
| 410 |
# Complete progress
|
| 411 |
progress_bar.progress(1.0)
|
| 412 |
-
status_text.text("β
Processing complete!")
|
|
|
|
| 413 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 414 |
|
| 415 |
# Success message
|
| 416 |
-
st.markdown('<div class="success-box">π Video Successfully Processed! π</div>', unsafe_allow_html=True)
|
| 417 |
|
| 418 |
# Display result immediately in same container
|
| 419 |
if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
|
|
@@ -429,13 +506,13 @@ def update_progress(progress, frame_count, total_frames):
|
|
| 429 |
st.download_button(
|
| 430 |
label="β¬οΈ Download Processed Video",
|
| 431 |
data=video_data,
|
| 432 |
-
file_name=f"
|
| 433 |
mime="video/mp4",
|
| 434 |
use_container_width=True,
|
| 435 |
key=f"download_button_{int(time.time())}"
|
| 436 |
)
|
| 437 |
|
| 438 |
-
st.success("β
|
| 439 |
|
| 440 |
# Cleanup temp files
|
| 441 |
try:
|
|
@@ -448,18 +525,18 @@ def update_progress(progress, frame_count, total_frames):
|
|
| 448 |
st.error("β Output video file is empty or corrupted")
|
| 449 |
|
| 450 |
except Exception as e:
|
| 451 |
-
status_text.text("β
|
| 452 |
st.error(f"β Processing failed: {str(e)}")
|
| 453 |
st.info("π‘ Try with a shorter video or different background image")
|
| 454 |
|
| 455 |
else:
|
| 456 |
-
st.info("π Upload both a video and background image to start processing!")
|
| 457 |
|
| 458 |
# Compact footer
|
| 459 |
st.markdown("---")
|
| 460 |
st.markdown("""
|
| 461 |
<div style="text-align: center; color: #666; padding: 10px;">
|
| 462 |
-
<p><small>πΉ
|
| 463 |
</div>
|
| 464 |
""", unsafe_allow_html=True)
|
| 465 |
|
|
|
|
| 1 |
"""
|
| 2 |
+
πΉ Video Background Replacer - IFRAME OPTIMIZED VERSION WITH 2-STAGE PIPELINE
|
| 3 |
Optimized for embedding in MyAvatar Railway app with Claude's recommendations
|
| 4 |
FIXED: Corrected MatAnyone API usage (processor.step() instead of processor.infer())
|
| 5 |
+
RESTORED: 2-Stage Pipeline - MatAnyone β Green Screen β Custom Background
|
| 6 |
"""
|
| 7 |
|
| 8 |
import streamlit as st
|
|
|
|
| 103 |
margin: 20px 0;
|
| 104 |
}
|
| 105 |
|
| 106 |
+
.stage-indicator {
|
| 107 |
+
background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
|
| 108 |
+
color: white;
|
| 109 |
+
padding: 10px;
|
| 110 |
+
border-radius: 8px;
|
| 111 |
+
margin: 10px 0;
|
| 112 |
+
text-align: center;
|
| 113 |
+
font-weight: bold;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
/* Iframe-specific optimizations */
|
| 117 |
.block-container {
|
| 118 |
padding-top: 1rem;
|
|
|
|
| 251 |
print(f"MatAnyone processing failed: {e}, using fallback")
|
| 252 |
return self.create_simple_mask(frame).astype(np.float32) / 255.0
|
| 253 |
|
| 254 |
+
def stage1_create_green_screen(self, frame, is_first_frame=False, mask=None):
|
| 255 |
+
"""STAGE 1: Create green screen video using AI segmentation"""
|
| 256 |
if self.use_matanyone:
|
| 257 |
+
# Use MatAnyone for person segmentation
|
| 258 |
alpha_mask = self.process_frame_matanyone(frame, is_first_frame, mask)
|
| 259 |
elif self.use_mediapipe:
|
| 260 |
try:
|
|
|
|
| 278 |
# Simple fallback method
|
| 279 |
alpha_mask = self.create_simple_mask(frame).astype(np.float32) / 255.0
|
| 280 |
|
| 281 |
+
# Create pure green screen background
|
| 282 |
+
green_screen = np.full_like(frame, [0, 255, 0], dtype=np.uint8) # Pure green (BGR)
|
| 283 |
+
|
| 284 |
+
# Composite person on green screen
|
| 285 |
+
# alpha_mask values close to 1 = keep original (person)
|
| 286 |
+
# alpha_mask values close to 0 = use green screen (background)
|
| 287 |
+
green_screen_frame = frame * alpha_mask + green_screen * (1 - alpha_mask)
|
| 288 |
+
|
| 289 |
+
return green_screen_frame.astype(np.uint8)
|
| 290 |
+
|
| 291 |
+
def stage2_chroma_key_replace(self, green_screen_frame, background_image):
|
| 292 |
+
"""STAGE 2: Replace green screen with custom background using chroma key"""
|
| 293 |
+
try:
|
| 294 |
+
# Convert to HSV for better green screen detection
|
| 295 |
+
hsv = cv2.cvtColor(green_screen_frame, cv2.COLOR_BGR2HSV)
|
| 296 |
+
|
| 297 |
+
# Define green screen range (optimized for pure green)
|
| 298 |
+
lower_green = np.array([35, 40, 40]) # Lower bound for green
|
| 299 |
+
upper_green = np.array([85, 255, 255]) # Upper bound for green
|
| 300 |
+
|
| 301 |
+
# Create mask for green screen areas
|
| 302 |
+
green_mask = cv2.inRange(hsv, lower_green, upper_green)
|
| 303 |
+
|
| 304 |
+
# Smooth the mask to avoid harsh edges
|
| 305 |
+
kernel = np.ones((3,3), np.uint8)
|
| 306 |
+
green_mask = cv2.morphologyEx(green_mask, cv2.MORPH_CLOSE, kernel)
|
| 307 |
+
green_mask = cv2.morphologyEx(green_mask, cv2.MORPH_OPEN, kernel)
|
| 308 |
+
|
| 309 |
+
# Apply Gaussian blur for smoother edges
|
| 310 |
+
green_mask = cv2.GaussianBlur(green_mask, (5, 5), 0)
|
| 311 |
+
|
| 312 |
+
# Convert mask to 3-channel and normalize
|
| 313 |
+
green_mask_3ch = cv2.cvtColor(green_mask, cv2.COLOR_GRAY2BGR).astype(np.float32) / 255.0
|
| 314 |
+
|
| 315 |
+
# Resize background to match frame
|
| 316 |
+
bg_resized = cv2.resize(background_image, (green_screen_frame.shape[1], green_screen_frame.shape[0]))
|
| 317 |
+
|
| 318 |
+
# Replace green areas with background
|
| 319 |
+
# green_mask_3ch = 1 where green (use background), 0 where not green (keep original)
|
| 320 |
+
result = green_screen_frame * (1 - green_mask_3ch) + bg_resized * green_mask_3ch
|
| 321 |
+
|
| 322 |
+
return result.astype(np.uint8)
|
| 323 |
+
|
| 324 |
+
except Exception as e:
|
| 325 |
+
print(f"Stage 2 chroma key failed: {e}")
|
| 326 |
+
# Fallback: return original frame
|
| 327 |
+
return green_screen_frame
|
| 328 |
+
|
| 329 |
+
def process_frame_2stage(self, frame, background_image, is_first_frame=False, mask=None):
|
| 330 |
+
"""Process a single frame with 2-stage background replacement"""
|
| 331 |
+
# STAGE 1: Create green screen
|
| 332 |
+
green_screen_frame = self.stage1_create_green_screen(frame, is_first_frame, mask)
|
| 333 |
|
| 334 |
+
# STAGE 2: Replace green screen with custom background
|
| 335 |
+
final_frame = self.stage2_chroma_key_replace(green_screen_frame, background_image)
|
|
|
|
|
|
|
| 336 |
|
| 337 |
+
return final_frame
|
| 338 |
|
| 339 |
def process_video(self, video_path, background_image, progress_callback=None):
|
| 340 |
+
"""Process entire video with 2-stage background replacement"""
|
| 341 |
# Convert PIL background to OpenCV format
|
| 342 |
if isinstance(background_image, Image.Image):
|
| 343 |
background_image = cv2.cvtColor(np.array(background_image), cv2.COLOR_RGB2BGR)
|
|
|
|
| 368 |
is_first_frame = (frame_count == 0)
|
| 369 |
mask = None # Could add automatic mask detection here if needed
|
| 370 |
|
| 371 |
+
# Process frame with 2-stage pipeline
|
| 372 |
+
processed_frame = self.process_frame_2stage(frame, background_image, is_first_frame, mask)
|
| 373 |
out.write(processed_frame)
|
| 374 |
|
| 375 |
frame_count += 1
|
|
|
|
| 377 |
# Update progress
|
| 378 |
if progress_callback:
|
| 379 |
progress = frame_count / total_frames
|
| 380 |
+
stage = "stage1" if progress < 0.5 else "stage2"
|
| 381 |
+
progress_callback(stage, progress, frame_count, total_frames)
|
| 382 |
|
| 383 |
finally:
|
| 384 |
cap.release()
|
|
|
|
| 389 |
def main():
|
| 390 |
# Compact header for iframe
|
| 391 |
st.markdown('<h1 class="main-header">πΉ Video Background Replacer</h1>', unsafe_allow_html=True)
|
| 392 |
+
st.markdown('<p style="text-align: center; font-size: 1.1rem; color: #666; margin-bottom: 2rem;">2-Stage AI Processing: Person Segmentation β Green Screen β Custom Background</p>', unsafe_allow_html=True)
|
| 393 |
|
| 394 |
# Initialize the background replacer with loading feedback
|
| 395 |
if 'replacer' not in st.session_state:
|
| 396 |
+
with st.spinner('π Loading 2-Stage AI Processing Interface...'):
|
| 397 |
st.session_state.replacer = BackgroundReplacer()
|
| 398 |
|
| 399 |
# Show initialization status
|
| 400 |
if hasattr(st.session_state.replacer, 'use_matanyone') and st.session_state.replacer.use_matanyone:
|
| 401 |
+
st.success('π MatAnyone AI Ready - 2-Stage Ultimate Quality!')
|
| 402 |
elif hasattr(st.session_state.replacer, 'use_mediapipe') and st.session_state.replacer.use_mediapipe:
|
| 403 |
+
st.success('π― MediaPipe AI Ready - 2-Stage Good Quality!')
|
| 404 |
else:
|
| 405 |
+
st.info('π± Basic 2-Stage Processing Ready')
|
| 406 |
+
|
| 407 |
+
# Show 2-stage process explanation
|
| 408 |
+
st.markdown("""
|
| 409 |
+
<div class="processing-box">
|
| 410 |
+
<h4>π 2-Stage Processing Pipeline:</h4>
|
| 411 |
+
<p><strong>Stage 1:</strong> AI extracts person from video β Creates clean green screen video</p>
|
| 412 |
+
<p><strong>Stage 2:</strong> Chroma key replacement β Green screen becomes your custom background</p>
|
| 413 |
+
<p><em>This approach ensures the highest quality and most reliable results!</em></p>
|
| 414 |
+
</div>
|
| 415 |
+
""", unsafe_allow_html=True)
|
| 416 |
|
| 417 |
# Compact upload section
|
| 418 |
col1, col2 = st.columns(2)
|
|
|
|
| 444 |
# Processing section - SIMPLIFIED to avoid DOM manipulation issues
|
| 445 |
if uploaded_video and uploaded_background:
|
| 446 |
st.markdown("---")
|
| 447 |
+
st.markdown("### π Ready for 2-Stage Processing!")
|
| 448 |
|
| 449 |
# Process button (clean and stable)
|
| 450 |
+
if st.button("πΉ START 2-STAGE PROCESSING", key="process_button", use_container_width=True):
|
| 451 |
# Save uploaded files
|
| 452 |
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_video:
|
| 453 |
tmp_video.write(uploaded_video.read())
|
|
|
|
| 460 |
|
| 461 |
with result_container:
|
| 462 |
st.markdown('<div class="processing-box">', unsafe_allow_html=True)
|
| 463 |
+
st.markdown("### π 2-Stage Processing Your Video...")
|
| 464 |
|
| 465 |
# Progress tracking
|
| 466 |
progress_bar = st.progress(0)
|
| 467 |
status_text = st.empty()
|
| 468 |
+
stage_indicator = st.empty()
|
| 469 |
|
| 470 |
+
def update_progress(stage, progress, frame_count, total_frames):
|
| 471 |
progress_bar.progress(progress)
|
| 472 |
status_text.text(f"Processing frame {frame_count}/{total_frames} ({progress*100:.1f}%)")
|
| 473 |
+
|
| 474 |
+
if stage == "stage1":
|
| 475 |
+
stage_indicator.markdown('<div class="stage-indicator">π¬ STAGE 1: Creating Green Screen Video</div>', unsafe_allow_html=True)
|
| 476 |
+
elif stage == "stage2":
|
| 477 |
+
stage_indicator.markdown('<div class="stage-indicator">πΌοΈ STAGE 2: Applying Custom Background</div>', unsafe_allow_html=True)
|
| 478 |
|
| 479 |
try:
|
| 480 |
# Process the video
|
| 481 |
+
st.info("π Starting 2-stage video processing...")
|
| 482 |
output_path = st.session_state.replacer.process_video(
|
| 483 |
video_path, background_image, update_progress
|
| 484 |
)
|
| 485 |
|
| 486 |
# Complete progress
|
| 487 |
progress_bar.progress(1.0)
|
| 488 |
+
status_text.text("β
2-Stage Processing complete!")
|
| 489 |
+
stage_indicator.markdown('<div class="stage-indicator">π BOTH STAGES COMPLETED!</div>', unsafe_allow_html=True)
|
| 490 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 491 |
|
| 492 |
# Success message
|
| 493 |
+
st.markdown('<div class="success-box">π Video Successfully Processed with 2-Stage Pipeline! π</div>', unsafe_allow_html=True)
|
| 494 |
|
| 495 |
# Display result immediately in same container
|
| 496 |
if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
|
|
|
|
| 506 |
st.download_button(
|
| 507 |
label="β¬οΈ Download Processed Video",
|
| 508 |
data=video_data,
|
| 509 |
+
file_name=f"2stage_background_replaced_{int(time.time())}.mp4",
|
| 510 |
mime="video/mp4",
|
| 511 |
use_container_width=True,
|
| 512 |
key=f"download_button_{int(time.time())}"
|
| 513 |
)
|
| 514 |
|
| 515 |
+
st.success("β
2-Stage processed video ready for download!")
|
| 516 |
|
| 517 |
# Cleanup temp files
|
| 518 |
try:
|
|
|
|
| 525 |
st.error("β Output video file is empty or corrupted")
|
| 526 |
|
| 527 |
except Exception as e:
|
| 528 |
+
status_text.text("β 2-Stage processing failed")
|
| 529 |
st.error(f"β Processing failed: {str(e)}")
|
| 530 |
st.info("π‘ Try with a shorter video or different background image")
|
| 531 |
|
| 532 |
else:
|
| 533 |
+
st.info("π Upload both a video and background image to start 2-stage processing!")
|
| 534 |
|
| 535 |
# Compact footer
|
| 536 |
st.markdown("---")
|
| 537 |
st.markdown("""
|
| 538 |
<div style="text-align: center; color: #666; padding: 10px;">
|
| 539 |
+
<p><small>πΉ 2-Stage Pipeline: MatAnyone β Green Screen β Custom Background | Optimized for MyAvatar</small></p>
|
| 540 |
</div>
|
| 541 |
""", unsafe_allow_html=True)
|
| 542 |
|