|
|
|
|
|
""" |
|
|
cv_processing.py Β· slim orchestrator layer |
|
|
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
Keeps the public API (segment_person_hq, refine_mask_hq, replace_background_hq, |
|
|
create_professional_background, validate_video_file) exactly the same so that |
|
|
existing callers do **not** need to change their imports. |
|
|
|
|
|
All heavy-lifting implementations live in: |
|
|
utils.segmentation |
|
|
utils.refinement |
|
|
utils.compositing |
|
|
utils.background_factory |
|
|
utils.background_presets |
|
|
""" |
|
|
|
|
|
from __future__ import annotations |
|
|
|
|
|
|
|
|
import os, logging, cv2, numpy as np |
|
|
from pathlib import Path |
|
|
from typing import Tuple, Dict, Any, Optional |
|
|
|
|
|
|
|
|
from utils.segmentation import ( |
|
|
segment_person_hq, |
|
|
segment_person_hq_original, |
|
|
SegmentationError, |
|
|
) |
|
|
from utils.refinement import ( |
|
|
refine_mask_hq, MaskRefinementError, |
|
|
) |
|
|
from utils.compositing import ( |
|
|
replace_background_hq, BackgroundReplacementError, |
|
|
) |
|
|
from utils.background_factory import create_professional_background |
|
|
from utils.background_presets import PROFESSIONAL_BACKGROUNDS |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
USE_AUTO_TEMPORAL_CONSISTENCY = True |
|
|
|
|
|
|
|
|
MIN_AREA_RATIO = 0.015 |
|
|
MAX_AREA_RATIO = 0.97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = [ |
|
|
"segment_person_hq", |
|
|
"segment_person_hq_original", |
|
|
"refine_mask_hq", |
|
|
"replace_background_hq", |
|
|
"create_professional_background", |
|
|
"validate_video_file", |
|
|
"SegmentationError", |
|
|
"MaskRefinementError", |
|
|
"BackgroundReplacementError", |
|
|
"PROFESSIONAL_BACKGROUNDS", |
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def validate_video_file(video_path: str) -> Tuple[bool, str]: |
|
|
""" |
|
|
Quick sanity-check before passing a file to OpenCV / FFmpeg. |
|
|
Returns (ok, human_readable_reason) |
|
|
""" |
|
|
if not video_path or not Path(video_path).exists(): |
|
|
return False, "Video file not found" |
|
|
|
|
|
try: |
|
|
size = Path(video_path).stat().st_size |
|
|
if size == 0: |
|
|
return False, "File is empty" |
|
|
if size > 2 * 1024 * 1024 * 1024: |
|
|
return False, "File > 2 GB β too large for the Space quota" |
|
|
|
|
|
cap = cv2.VideoCapture(video_path) |
|
|
if not cap.isOpened(): |
|
|
return False, "OpenCV cannot read the file" |
|
|
|
|
|
n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) |
|
|
fps = cap.get(cv2.CAP_PROP_FPS) |
|
|
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) |
|
|
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) |
|
|
cap.release() |
|
|
|
|
|
if n_frames == 0: |
|
|
return False, "No frames detected" |
|
|
if fps <= 0 or fps > 120: |
|
|
return False, f"Suspicious FPS: {fps}" |
|
|
if w <= 0 or h <= 0: |
|
|
return False, "Zero resolution" |
|
|
if w > 4096 or h > 4096: |
|
|
return False, f"Resolution {w}Γ{h} too high (max 4 096Β²)" |
|
|
if (n_frames / fps) > 300: |
|
|
return False, "Video longer than 5 minutes" |
|
|
|
|
|
return True, f"OK β {w}Γ{h}, {fps:.1f} fps, {n_frames/fps:.1f} s" |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"validate_video_file: {e}") |
|
|
return False, f"Validation error: {e}" |
|
|
|