AI

이미지를 자연스러운 동영상으로 변환하는 서비스를 위한 가이드와 소스코드

오아름 샘 2025. 6. 26. 14:57
반응형

(G:ing)이미지-비디오 변환 서비스 환경설정 가이드

📋 시스템 요구사항

  • OS: Windows 11
  • GPU: NVIDIA GPU (CUDA 12.7 지원)
  • Python: 3.10
  • RAM: 최소 16GB (권장 32GB)
  • Storage: 최소 50GB 여유공간

🚀 환경설정 단계별 가이드

1. 기본 환경 준비

1.1 Python 3.10 설치

# Python 3.10.11 다운로드 및 설치
# https://www.python.org/downloads/release/python-31011/
# 설치 시 "Add Python to PATH" 체크 필수

1.2 CUDA 12.7 설치

# NVIDIA CUDA Toolkit 12.7 다운로드
# https://developer.nvidia.com/cuda-12-7-0-download-archive
# 설치 후 환경변수 확인
nvcc --version

1.3 Git 설치

# Git for Windows 다운로드 및 설치
# https://git-scm.com/download/win

2. 프로젝트 디렉토리 생성 및 가상환경 설정

# 작업 디렉토리 생성
mkdir image_to_video_service
cd image_to_video_service

# Python 가상환경 생성
python -m venv venv

# 가상환경 활성화 (Windows)
venv\Scripts\activate

3. 핵심 라이브러리 설치

3.1 PyTorch 설치 (CUDA 12.1 버전)

# CUDA 12.1과 호환되는 PyTorch 설치
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

3.2 기본 패키지 설치

pip install -r requirements.txt

4. 오픈소스 모델 및 도구 설치

4.1 Stable Video Diffusion (SVD) 설치

# Diffusers 라이브러리를 통한 SVD 설치
pip install diffusers[torch] transformers accelerate
pip install opencv-python pillow numpy

4.2 Real-ESRGAN (이미지 품질 향상)

pip install realesrgan

4.3 추가 도구들

# 얼굴 인식 및 처리
pip install face-recognition mediapipe

# 비디오 처리
pip install moviepy imageio imageio-ffmpeg

# 웹 인터페이스
pip install gradio flask

# 유틸리티
pip install tqdm accelerate xformers

5. 모델 다운로드 스크립트

5.1 필요한 모델들 자동 다운로드

# download_models.py 실행하여 모델 다운로드
python download_models.py

6. 환경 변수 설정

6.1 .env 파일 생성

# .env 파일에 다음 내용 추가
CUDA_VISIBLE_DEVICES=0
TORCH_HOME=./models
HF_HOME=./models/huggingface

7. 설치 검증

7.1 CUDA 및 PyTorch 동작 확인

# test_environment.py 실행
python test_environment.py

7.2 GPU 메모리 확인

# GPU 메모리 사용량 체크
python check_gpu.py

📁 프로젝트 구조

image_to_video_service/
├── venv/                          # Python 가상환경
├── models/                        # 다운로드된 모델들
│   ├── svd/                      # Stable Video Diffusion
│   ├── realesrgan/               # Real-ESRGAN
│   └── mediapipe/                # MediaPipe 모델들
├── src/                          # 소스코드
│   ├── core/                     # 핵심 로직
│   ├── api/                      # API 서버
│   ├── utils/                    # 유틸리티
│   └── web/                      # 웹 인터페이스
├── uploads/                      # 업로드된 이미지
├── outputs/                      # 생성된 동영상
├── static/                       # 정적 파일
├── templates/                    # HTML 템플릿
├── requirements.txt              # 패키지 목록
├── .env                         # 환경변수
└── main.py                      # 메인 실행 파일

🔧 성능 최적화 설정

GPU 메모리 최적화

# GPU 메모리 분할 할당 설정
import torch
torch.cuda.empty_cache()
torch.backends.cudnn.benchmark = True

배치 처리 최적화

# 배치 크기 조정 (GPU 메모리에 따라)
BATCH_SIZE = 1  # 16GB GPU 기준
MAX_FRAMES = 25  # 생성할 프레임 수

🚨 트러블슈팅

자주 발생하는 문제들

1. CUDA 버전 불일치

# CUDA 버전 확인
nvidia-smi
nvcc --version

# PyTorch CUDA 버전 확인
python -c "import torch; print(torch.version.cuda)"

2. GPU 메모리 부족

# 메모리 사용량 모니터링
nvidia-smi -l 1

# Python에서 GPU 메모리 정리
torch.cuda.empty_cache()

3. 모델 다운로드 실패

# Hugging Face 캐시 정리
rm -rf ~/.cache/huggingface/

# 수동 모델 다운로드
python download_models.py --force

📊 성능 벤치마크

예상 처리 시간 (RTX 4090 기준)

  • 512x512 이미지 → 25프레임 비디오: 약 30-60초
  • 1024x1024 이미지 → 25프레임 비디오: 약 2-4분
  • 고품질 업스케일링 포함: 추가 1-2분

메모리 사용량

  • 최소 요구사항: 12GB VRAM
  • 권장 사양: 16GB+ VRAM
  • RAM: 16GB+ 시스템 메모리

🎯 다음 단계

  1. 환경설정 완료 후 실행: python main.py
  2. 웹 인터페이스 접속: http://localhost:7860
  3. API 서버 실행: python src/api/server.py
  4. 테스트 이미지로 동작 확인

📞 지원 및 문의

설치 과정에서 문제가 발생하면:

  1. GPU 드라이버 최신 버전 확인
  2. CUDA 버전 호환성 확인
  3. Python 패키지 버전 충돌 해결
  4. 로그 파일 확인: logs/installation.log
# requirements.txt
torch>=2.1.0
torchvision>=0.16.0
torchaudio>=2.1.0
diffusers>=0.24.0
transformers>=4.35.0
accelerate>=0.24.0
opencv-python>=4.8.0
pillow>=10.0.0
numpy>=1.24.0
gradio>=4.0.0
flask>=2.3.0
moviepy>=1.0.3
imageio>=2.31.0
imageio-ffmpeg>=0.4.9
face-recognition>=1.3.0
mediapipe>=0.10.0
realesrgan>=0.3.0
tqdm>=4.66.0
xformers>=0.0.22
python-dotenv>=1.0.0
requests>=2.31.0

# ============================================================================
# main.py - 메인 실행 파일
# ============================================================================

import os
import sys
import argparse
from pathlib import Path

# 프로젝트 루트 디렉토리를 Python 경로에 추가
sys.path.append(str(Path(__file__).parent))

from src.web.gradio_app import launch_gradio_app
from src.api.server import launch_api_server
from src.utils.setup import setup_environment, check_requirements

def main():
    parser = argparse.ArgumentParser(description='이미지-비디오 변환 서비스')
    parser.add_argument('--mode', choices=['web', 'api', 'both'], default='web',
                      help='실행 모드 선택 (web: Gradio 웹앱, api: REST API, both: 둘 다)')
    parser.add_argument('--port', type=int, default=7860, help='포트 번호')
    parser.add_argument('--host', default='127.0.0.1', help='호스트 주소')
    parser.add_argument('--share', action='store_true', help='Gradio 공유 링크 생성')
    
    args = parser.parse_args()
    
    # 환경 설정 및 요구사항 확인
    print("🚀 이미지-비디오 변환 서비스 시작")
    print("=" * 50)
    
    setup_environment()
    
    if not check_requirements():
        print("❌ 요구사항 확인 실패. 설치 가이드를 참조하세요.")
        return
    
    print("✅ 환경 설정 완료")
    
    # 실행 모드에 따라 서비스 시작
    if args.mode == 'web':
        print(f"🌐 Gradio 웹 인터페이스 시작: http://{args.host}:{args.port}")
        launch_gradio_app(port=args.port, host=args.host, share=args.share)
    
    elif args.mode == 'api':
        print(f"🔗 REST API 서버 시작: http://{args.host}:{args.port}")
        launch_api_server(port=args.port, host=args.host)
    
    elif args.mode == 'both':
        print("🔄 웹 인터페이스와 API 서버 모두 시작")
        # 별도 프로세스로 API 서버 실행
        import multiprocessing as mp
        api_process = mp.Process(target=launch_api_server, 
                                args=(args.port + 1, args.host))
        api_process.start()
        
        print(f"🌐 Gradio 웹 인터페이스: http://{args.host}:{args.port}")
        print(f"🔗 REST API 서버: http://{args.host}:{args.port + 1}")
        
        try:
            launch_gradio_app(port=args.port, host=args.host, share=args.share)
        finally:
            api_process.terminate()

if __name__ == "__main__":
    main()

# ============================================================================
# src/utils/setup.py - 환경 설정 유틸리티
# ============================================================================

import os
import torch
import subprocess
from pathlib import Path
from dotenv import load_dotenv

def setup_environment():
    """환경 변수 및 디렉토리 설정"""
    load_dotenv()
    
    # 필요한 디렉토리 생성
    directories = [
        'models', 'uploads', 'outputs', 'logs', 
        'models/svd', 'models/realesrgan', 'models/mediapipe'
    ]
    
    for dir_name in directories:
        Path(dir_name).mkdir(exist_ok=True)
    
    # 환경 변수 설정
    os.environ.setdefault('TORCH_HOME', './models')
    os.environ.setdefault('HF_HOME', './models/huggingface')

def check_requirements():
    """시스템 요구사항 확인"""
    checks = []
    
    # CUDA 확인
    if torch.cuda.is_available():
        gpu_count = torch.cuda.device_count()
        gpu_name = torch.cuda.get_device_name(0)
        vram = torch.cuda.get_device_properties(0).total_memory / 1024**3
        
        print(f"✅ CUDA 사용 가능: {gpu_count}개 GPU")
        print(f"📱 GPU: {gpu_name}")
        print(f"💾 VRAM: {vram:.1f}GB")
        
        if vram < 10:
            print("⚠️  경고: VRAM이 10GB 미만입니다. 성능에 영향을 줄 수 있습니다.")
        
        checks.append(True)
    else:
        print("❌ CUDA를 사용할 수 없습니다.")
        checks.append(False)
    
    # Python 버전 확인
    import sys
    python_version = sys.version_info
    if python_version.major == 3 and python_version.minor == 10:
        print(f"✅ Python 버전: {python_version.major}.{python_version.minor}")
        checks.append(True)
    else:
        print(f"⚠️  Python 버전: {python_version.major}.{python_version.minor} (권장: 3.10)")
        checks.append(True)  # 다른 버전도 허용
    
    return all(checks)

# ============================================================================
# src/core/video_generator.py - 비디오 생성 핵심 로직
# ============================================================================

import torch
import numpy as np
from PIL import Image
from diffusers import StableVideoDiffusionPipeline
from diffusers.utils import load_image, export_to_video
import cv2
from typing import Optional, Tuple
import logging

class VideoGenerator:
    def __init__(self, model_id: str = "stabilityai/stable-video-diffusion-img2vid-xt"):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.pipe = None
        self.model_id = model_id
        self.logger = logging.getLogger(__name__)
        
    def load_model(self):
        """모델 로드"""
        try:
            self.logger.info(f"모델 로딩 시작: {self.model_id}")
            
            self.pipe = StableVideoDiffusionPipeline.from_pretrained(
                self.model_id,
                torch_dtype=torch.float16,
                variant="fp16"
            )
            
            self.pipe = self.pipe.to(self.device)
            
            # 메모리 최적화
            self.pipe.enable_model_cpu_offload()
            self.pipe.enable_vae_slicing()
            
            self.logger.info("모델 로딩 완료")
            
        except Exception as e:
            self.logger.error(f"모델 로딩 실패: {e}")
            raise
    
    def preprocess_image(self, image: Image.Image, 
                        target_size: Tuple[int, int] = (1024, 576)) -> Image.Image:
        """이미지 전처리"""
        # 종횡비 유지하면서 리사이즈
        img_ratio = image.width / image.height
        target_ratio = target_size[0] / target_size[1]
        
        if img_ratio > target_ratio:
            # 이미지가 더 넓음
            new_width = target_size[0]
            new_height = int(target_size[0] / img_ratio)
        else:
            # 이미지가 더 높음
            new_height = target_size[1]
            new_width = int(target_size[1] * img_ratio)
        
        # 8의 배수로 조정 (모델 요구사항)
        new_width = (new_width // 8) * 8
        new_height = (new_height // 8) * 8
        
        image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
        
        # 중앙 크롭으로 타겟 사이즈에 맞춤
        if new_width != target_size[0] or new_height != target_size[1]:
            left = (new_width - target_size[0]) // 2
            top = (new_height - target_size[1]) // 2
            right = left + target_size[0]
            bottom = top + target_size[1]
            
            # 패딩이 필요한 경우
            if left < 0 or top < 0:
                padded = Image.new('RGB', target_size, (0, 0, 0))
                paste_x = max(0, -left)
                paste_y = max(0, -top)
                padded.paste(image, (paste_x, paste_y))
                image = padded
            else:
                image = image.crop((left, top, right, bottom))
        
        return image
    
    def generate_video(self, 
                      image: Image.Image,
                      num_frames: int = 25,
                      fps: int = 7,
                      motion_bucket_id: int = 127,
                      noise_aug_strength: float = 0.02,
                      decode_chunk_size: int = 8,
                      seed: Optional[int] = None) -> str:
        """비디오 생성"""
        
        if self.pipe is None:
            self.load_model()
        
        # 시드 설정
        if seed is not None:
            torch.manual_seed(seed)
            np.random.seed(seed)
        
        # 이미지 전처리
        processed_image = self.preprocess_image(image)
        
        self.logger.info(f"비디오 생성 시작: {num_frames}프레임, {fps}fps")
        
        try:
            # 비디오 생성
            frames = self.pipe(
                processed_image,
                decode_chunk_size=decode_chunk_size,
                generator=torch.manual_seed(seed) if seed else None,
                motion_bucket_id=motion_bucket_id,
                noise_aug_strength=noise_aug_strength,
                num_frames=num_frames,
            ).frames[0]
            
            # 비디오 파일로 저장
            output_path = f"outputs/generated_video_{torch.randint(0, 10000, (1,)).item()}.mp4"
            export_to_video(frames, output_path, fps=fps)
            
            self.logger.info(f"비디오 생성 완료: {output_path}")
            return output_path
            
        except Exception as e:
            self.logger.error(f"비디오 생성 실패: {e}")
            raise
        finally:
            # GPU 메모리 정리
            torch.cuda.empty_cache()

# ============================================================================
# src/core/face_animator.py - 얼굴 표정 애니메이션
# ============================================================================

import cv2
import mediapipe as mp
import numpy as np
from PIL import Image
from typing import List, Tuple

class FaceAnimator:
    def __init__(self):
        self.mp_face_mesh = mp.solutions.face_mesh
        self.mp_drawing = mp.solutions.drawing_utils
        self.face_mesh = self.mp_face_mesh.FaceMesh(
            static_image_mode=False,
            max_num_faces=1,
            refine_landmarks=True,
            min_detection_confidence=0.5
        )
    
    def detect_face_landmarks(self, image: np.ndarray) -> List[Tuple[int, int]]:
        """얼굴 랜드마크 검출"""
        rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        results = self.face_mesh.process(rgb_image)
        
        landmarks = []
        if results.multi_face_landmarks:
            for face_landmarks in results.multi_face_landmarks:
                h, w = image.shape[:2]
                for landmark in face_landmarks.landmark:
                    x = int(landmark.x * w)
                    y = int(landmark.y * h)
                    landmarks.append((x, y))
        
        return landmarks
    
    def animate_expression(self, frames: List[np.ndarray], 
                          expression_type: str = "smile") -> List[np.ndarray]:
        """표정 애니메이션 적용"""
        animated_frames = []
        
        for i, frame in enumerate(frames):
            landmarks = self.detect_face_landmarks(frame)
            
            if landmarks:
                # 표정에 따른 랜드마크 조정
                modified_frame = self.apply_expression_morph(
                    frame, landmarks, expression_type, i / len(frames)
                )
                animated_frames.append(modified_frame)
            else:
                animated_frames.append(frame)
        
        return animated_frames
    
    def apply_expression_morph(self, frame: np.ndarray, landmarks: List[Tuple[int, int]], 
                              expression: str, progress: float) -> np.ndarray:
        """표정 변형 적용"""
        # 표정별 랜드마크 조정 매핑
        expression_adjustments = {
            "smile": self.smile_adjustment,
            "surprise": self.surprise_adjustment,
            "blink": self.blink_adjustment
        }
        
        if expression in expression_adjustments:
            return expression_adjustments[expression](frame, landmarks, progress)
        
        return frame
    
    def smile_adjustment(self, frame: np.ndarray, landmarks: List[Tuple[int, int]], 
                        progress: float) -> np.ndarray:
        """미소 표정 조정"""
        # 입꼬리 올리기 (랜드마크 61, 291)
        mouth_corners = [61, 291]
        smile_intensity = np.sin(progress * np.pi) * 5  # 부드러운 미소 애니메이션
        
        # 간단한 워핑 적용 (실제로는 더 정교한 알고리즘 필요)
        return frame
    
    def surprise_adjustment(self, frame: np.ndarray, landmarks: List[Tuple[int, int]], 
                           progress: float) -> np.ndarray:
        """놀람 표정 조정"""
        # 눈썹 올리기, 눈 크게 뜨기
        return frame
    
    def blink_adjustment(self, frame: np.ndarray, landmarks: List[Tuple[int, int]], 
                        progress: float) -> np.ndarray:
        """눈 깜빡임 조정"""
        # 눈꺼풀 움직임
        return frame

# ============================================================================
# src/core/motion_enhancer.py - 자연스러운 움직임 추가
# ============================================================================

import cv2
import numpy as np
from typing import List
from scipy.ndimage import gaussian_filter

class MotionEnhancer:
    def __init__(self):
        self.optical_flow = cv2.optflow.createOptFlow_DeepFlow()
    
    def add_hair_motion(self, frames: List[np.ndarray], 
                       wind_strength: float = 0.5) -> List[np.ndarray]:
        """머리카락 움직임 추가"""
        enhanced_frames = []
        
        for i, frame in enumerate(frames):
            if i == 0:
                enhanced_frames.append(frame)
                continue
            
            # 머리카락 영역 감지 (간단한 색상 기반)
            hair_mask = self.detect_hair_region(frame)
            
            # 바람 효과 시뮬레이션
            wind_vector = self.generate_wind_vector(i, wind_strength)
            
            # 광학 흐름 적용
            enhanced_frame = self.apply_motion_to_region(
                frames[i-1], frame, hair_mask, wind_vector
            )
            
            enhanced_frames.append(enhanced_frame)
        
        return enhanced_frames
    
    def detect_hair_region(self, frame: np.ndarray) -> np.ndarray:
        """머리카락 영역 감지"""
        # HSV 색공간으로 변환
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        
        # 머리카락 색상 범위 (어두운 색상)
        lower_hair = np.array([0, 0, 0])
        upper_hair = np.array([180, 255, 100])
        
        mask = cv2.inRange(hsv, lower_hair, upper_hair)
        
        # 모폴로지 연산으로 노이즈 제거
        kernel = np.ones((5, 5), np.uint8)
        mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
        mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
        
        return mask
    
    def generate_wind_vector(self, frame_idx: int, strength: float) -> np.ndarray:
        """바람 벡터 생성"""
        # 사인파 기반 자연스러운 움직임
        time_factor = frame_idx * 0.1
        
        wind_x = np.sin(time_factor) * strength
        wind_y = np.cos(time_factor * 0.7) * strength * 0.5
        
        return np.array([wind_x, wind_y])
    
    def apply_motion_to_region(self, prev_frame: np.ndarray, curr_frame: np.ndarray,
                              mask: np.ndarray, motion_vector: np.ndarray) -> np.ndarray:
        """특정 영역에 움직임 적용"""
        # 광학 흐름 계산
        prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
        curr_gray = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2GRAY)
        
        # Lucas-Kanade 옵티컬 플로우
        flow = cv2.calcOpticalFlowPyrLK(prev_gray, curr_gray, None, None)
        
        # 움직임 벡터 적용
        h, w = curr_frame.shape[:2]
        flow_map = np.zeros((h, w, 2), dtype=np.float32)
        
        # 마스크 영역에만 움직임 적용
        mask_3d = np.stack([mask] * 2, axis=2) / 255.0
        flow_map[:, :] = motion_vector
        flow_map = flow_map * mask_3d
        
        # 워핑 적용
        warped = cv2.remap(curr_frame, 
                          flow_map[:, :, 0].astype(np.float32),
                          flow_map[:, :, 1].astype(np.float32),
                          cv2.INTER_LINEAR)
        
        # 마스크 영역만 적용
        mask_3d_bgr = np.stack([mask] * 3, axis=2) / 255.0
        result = curr_frame * (1 - mask_3d_bgr) + warped * mask_3d_bgr
        
        return result.astype(np.uint8)
    
    def add_cloth_motion(self, frames: List[np.ndarray]) -> List[np.ndarray]:
        """옷 움직임 추가"""
        # 옷 영역 감지 및 부드러운 움직임 적용
        enhanced_frames = []
        
        for i, frame in enumerate(frames):
            if i == 0:
                enhanced_frames.append(frame)
                continue
            
            # 옷 영역 감지 (색상 및 질감 기반)
            cloth_mask = self.detect_cloth_region(frame)
            
            # 미세한 흔들림 효과
            motion_vector = np.array([
                np.sin(i * 0.05) * 0.3,
                np
반응형