Windows GPU 얼굴 블러 + 원본 소리 보존 영상 자동화 매뉴얼

1. 준비물 & 환경

  • Python 3.10
    다운로드
    설치 중 “Add Python to PATH” 꼭 체크
  • CUDA Toolkit 11.2
    다운로드
  • cuDNN 8.1.1 (CUDA 11.2용)
    아카이브
    예시: cudnn-11.2-windows-x64-v8.1.1.33.zip
    압축 풀고 bin, include, lib 폴더 전체를
    C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2에 드래그&드롭(덮어쓰기/병합)
  • FFmpeg
    다운로드
    압축 해제 후 bin 폴더 경로(예: C:\ffmpeg\bin)를
    시스템 환경 변수(Path)에 추가
    CMD에서 ffmpeg -version으로 정상 출력 확인
  • 환경 변수
    시스템 Path에
    C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2\bin
    반드시 등록

2. RetinaFace 모델(가중치) 파일 설치

  • 자동 다운로드: 코드 첫 실행 때 자동 다운로드 시도
  • 수동(다운 실패/방화벽 등 발생 시):
    retinaface.h5 수동 다운로드
    • Windows:
      C:\Users\내이름\.deepface\weights\retinaface.h5
    • 폴더가 없으면 직접 만듦

3. 파이썬 패키지 설치

Bash
pip install opencv-python
pip install retina-face
pip install tensorflow==2.10.0

4. 얼굴 블러+소리 보존 전체 코드 (face_blur.py로 저장)

4.1. 2025-07-28

Python
import cv2
from retinaface import RetinaFace
from datetime import datetime
import tensorflow as tf
import subprocess
import os
import glob
import numpy as np

def log(msg):
    now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{now}] {msg}")

def get_face_dicts(faces):
    if faces is None:
        return []
    if isinstance(faces, dict):
        if 'facial_area' in faces:
            return [faces]
        return [v for v in faces.values() if isinstance(v, dict) and 'facial_area' in v]
    if isinstance(faces, (list, tuple)):
        result = []
        for item in faces:
            if isinstance(item, dict) and 'facial_area' in item:
                result.append(item)
            elif isinstance(item, (list, tuple)):
                result.extend(get_face_dicts(item))
        return result
    return []

def expand_box(x1, y1, x2, y2, img_w, img_h, scale):
    cx = (x1 + x2) / 2
    cy = (y1 + y2) / 2
    w_box = (x2 - x1) * scale
    h_box = (y2 - y1) * scale
    nx1 = int(max(cx - w_box / 2, 0))
    ny1 = int(max(cy - h_box / 2, 0))
    nx2 = int(min(cx + w_box / 2, img_w))
    ny2 = int(min(cy + h_box / 2, img_h))
    return nx1, ny1, nx2, ny2

def egg_mask(height, width):
    mask = np.zeros((height, width), dtype=np.uint8)
    center = (width//2, height//2)
    axes = (int(width*0.45), int(height*0.46))
    pts = []
    for angle in np.linspace(0, 2*np.pi, 200):
        r = 1.0
        if np.pi < angle < 2*np.pi:
            r = 1.13
        x = int(center[0] + axes[0]*np.cos(angle)*1.0)
        y = int(center[1] + axes[1]*np.sin(angle)*r)
        pts.append((x,y))
    pts = np.array([pts], np.int32)
    cv2.fillPoly(mask, pts, 255)
    return mask

def detect_and_blur_with_egg_mask(
    input_path, output_path, threshold=0.1, temp_video='temp_blur.mp4',
    face_scale=3.0, motion_thresh=30
):
    start_time = datetime.now()
    log(f"작업 시작! (시작시간: {start_time.strftime('%Y-%m-%d %H:%M:%S')})")

    gpus = tf.config.list_physical_devices('GPU')
    if gpus:
        log(f"사용 가능한 GPU: {[gpu.name for gpu in gpus]}")
    else:
        log("⚠️ GPU를 인식하지 못했습니다. (CPU만 사용)")

    cap = cv2.VideoCapture(input_path)
    if not cap.isOpened():
        log("❌ 동영상 파일을 열 수 없습니다.")
        return

    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    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))
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    out = cv2.VideoWriter(temp_video, fourcc, fps, (w, h))

    log(f"동영상 정보: {w}x{h}px, {fps:.2f}fps, 총 프레임 {total_frames}개")

    prev_gray = None
    prev_faces = []
    processed = 0
    face_found = None
    motion_prev = None

    percent_checkpoints = {}
    for perc in [70,80,90]:
        p_frame = int(total_frames*perc/100)
        if 1 <= p_frame <= total_frames:
            percent_checkpoints[p_frame] = perc
    percent_logged = set()

    while True:
        ret, frame = cap.read()
        if not ret:
            break
        frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

        face_dicts = []
        detect_new = False
        motion_detected = False

        if prev_gray is None or not prev_faces:
            try:
                faces = RetinaFace.detect_faces(frame, threshold=threshold)
            except TypeError:
                faces = RetinaFace.detect_faces(frame)
            face_dicts = get_face_dicts(faces)
            detect_new = True
            motion_detected = True if face_dicts else False
        else:
            for f in prev_faces:
                x1, y1, x2, y2 = f['facial_area']
                bx1, by1, bx2, by2 = expand_box(x1, y1, x2, y2, w, h, scale=face_scale)
                prev_roi = prev_gray[by1:by2, bx1:bx2]
                curr_roi = frame_gray[by1:by2, bx1:bx2]
                if prev_roi.shape == curr_roi.shape and prev_roi.size > 0:
                    diff = cv2.absdiff(prev_roi, curr_roi)
                    mean_diff = np.mean(diff)
                    if mean_diff > motion_thresh:
                        motion_detected = True
                        break
            if motion_detected:
                try:
                    faces = RetinaFace.detect_faces(frame, threshold=threshold)
                except TypeError:
                    faces = RetinaFace.detect_faces(frame)
                face_dicts = get_face_dicts(faces)
            else:
                face_dicts = prev_faces

        has_faces = bool(face_dicts)

        if face_found is None:
            face_found = has_faces
            if has_faces:
                log(f"얼굴 발견 시작! 프레임 {processed + 1}")
            else:
                log(f"얼굴을 발견하지 못했습니다. 프레임 {processed + 1}")
        elif face_found != has_faces:
            if has_faces:
                log(f"얼굴 발견 시작! 프레임 {processed + 1}")
            else:
                log(f"얼굴을 발견하지 못했습니다. 프레임 {processed + 1}")
            face_found = has_faces

        if has_faces:
            if motion_prev is None or motion_prev != motion_detected:
                if motion_detected:
                    log(f"[프레임 {processed + 1}] 얼굴 영역에서 움직임 감지됨")
                else:
                    log(f"[프레임 {processed + 1}] 얼굴 영역에서 움직임 없음")
            motion_prev = motion_detected
        else:
            motion_prev = None

        for face in face_dicts:
            x1, y1, x2, y2 = face['facial_area']
            bx1, by1, bx2, by2 = expand_box(x1, y1, x2, y2, w, h, scale=face_scale)
            roi = frame[by1:by2, bx1:bx2]
            if roi.size > 0:
                blurred_roi = cv2.GaussianBlur(roi, (51, 51), 0)
                mask = egg_mask(roi.shape[0], roi.shape[1])
                mask_3 = cv2.merge([mask]*3)
                roi[:] = np.where(mask_3==255, blurred_roi, roi)
                frame[by1:by2, bx1:bx2] = roi

        out.write(frame)
        prev_gray = frame_gray
        prev_faces = face_dicts
        processed += 1

        if processed in percent_checkpoints and percent_checkpoints[processed] not in percent_logged:
            log(f"진행률 {percent_checkpoints[processed]}% ({processed}/{total_frames}프레임)")
            percent_logged.add(percent_checkpoints[processed])

    cap.release()
    out.release()

    end_time = datetime.now()
    log(f"모든 프레임 처리 완료! (종료시간: {end_time.strftime('%Y-%m-%d %H:%M:%S')})")
    log(f"총 경과시간: {str(end_time - start_time).split('.')[0]}")

    log("FFmpeg로 소리 복사 중...")
    ffmpeg_command = [
        "ffmpeg", "-y",
        "-i", input_path,
        "-i", temp_video,
        "-c:v", "copy", "-c:a", "copy",
        "-map", "1:v:0", "-map", "0:a:0?",
        "-shortest",
        output_path
    ]
    result = subprocess.run(ffmpeg_command, capture_output=True, text=True)
    if result.returncode != 0:
        log("❌ FFmpeg 오류 발생:")
        log(result.stderr)
    else:
        log("오디오가 포함된 최종 파일 저장 완료!")

    if os.path.exists(temp_video):
        os.remove(temp_video)

def process_multiple_videos(
    input_folder='video/input', output_folder='video/output',
    threshold=0.1, face_scale=3.0, motion_thresh=30
):
    os.makedirs(output_folder, exist_ok=True)
    if not os.path.exists(input_folder):
        os.makedirs(input_folder)
        log(f"📁 입력 폴더 '{input_folder}'를 생성했습니다. 동영상 파일을 넣어주세요.")
        return

    video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm']
    video_files = []
    for ext in video_extensions:
        pattern = os.path.join(input_folder, f'*{ext}')
        video_files.extend(glob.glob(pattern, recursive=False))
        pattern_upper = os.path.join(input_folder, f'*{ext.upper()}')
        video_files.extend(glob.glob(pattern_upper, recursive=False))
    video_files = list(set(os.path.normcase(v) for v in video_files))
    if not video_files:
        log(f"❌ '{input_folder}' 폴더에 처리할 동영상 파일이 없습니다.")
        log(f"지원 형식: {', '.join(video_extensions)}")
        return

    total_videos = len(video_files)
    log(f"🎬 총 {total_videos}개의 동영상 파일을 발견했습니다.")
    log(f"설정: 얼굴 블러 크기 {face_scale}배, 움직임 임계값={motion_thresh}, threshold={threshold}")

    for i, video_path in enumerate(video_files, 1):
        filename = os.path.basename(video_path)
        name_without_ext = os.path.splitext(filename)[0]
        output_path = os.path.join(output_folder, f"{name_without_ext}_blurred.mp4")
        temp_video = f"temp_{name_without_ext}.mp4"

        log(f"\n🔄 [{i}/{total_videos}] 처리 중: {filename}")

        try:
            detect_and_blur_with_egg_mask(
                video_path, output_path,
                threshold=threshold,
                temp_video=temp_video,
                face_scale=face_scale,
                motion_thresh=motion_thresh
            )
            log(f"✅ 완료: {os.path.basename(output_path)}")
        except Exception as e:
            log(f"❌ 오류 발생 ({filename}): {str(e)}")
            continue

    log(f"\n🎉 모든 동영상 처리 완료! 결과는 '{output_folder}' 폴더에 있습니다.")

4.2. 2025-07-23

Python
import cv2
from retinaface import RetinaFace
from datetime import datetime
import tensorflow as tf
import subprocess
import os
import glob

def log(msg):
    now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{now}] {msg}")

def get_face_dicts(faces):
    if faces is None:
        return []
    if isinstance(faces, dict):
        if 'facial_area' in faces:
            return [faces]
        return [v for v in faces.values() if isinstance(v, dict) and 'facial_area' in v]
    if isinstance(faces, (list, tuple)):
        result = []
        for item in faces:
            if isinstance(item, dict) and 'facial_area' in item:
                result.append(item)
            elif isinstance(item, (list, tuple)):
                result.extend(get_face_dicts(item))
        return result
    return []

def batch_blur_faces(
    input_path, output_path, batch_size=8,
    threshold=0.1, temp_video='temp_blur.mp4'
):
    gpus = tf.config.list_physical_devices('GPU')
    if gpus:
        log(f"사용 가능한 GPU: {[gpu.name for gpu in gpus]}")
    else:
        log("⚠️ GPU를 인식하지 못했습니다. (CPU만 사용)")

    start_time = datetime.now()
    log(f"작업 시작! (시작시간: {start_time.strftime('%Y-%m-%d %H:%M:%S')})")

    cap = cv2.VideoCapture(input_path)
    if not cap.isOpened():
        log("❌ 동영상 파일을 열 수 없습니다.")
        return

    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    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))
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    out = cv2.VideoWriter(temp_video, fourcc, fps, (w, h))

    log(f"동영상 정보: {w}x{h}px, {fps:.2f}fps, 총 프레임 {total_frames}개")
    percent_checkpoints = [int(total_frames * i / 10) for i in range(1, 11)]
    processed = 0
    face_found = None
    frames_buffer = []
    batch_indices = []

    def expand_box(x1, y1, x2, y2, img_w, img_h, scale=1.5):
        cx = (x1 + x2) / 2
        cy = (y1 + y2) / 2
        w_box = (x2 - x1) * scale
        h_box = (y2 - y1) * scale
        nx1 = int(max(cx - w_box / 2, 0))
        ny1 = int(max(cy - h_box / 2, 0))
        nx2 = int(min(cx + w_box / 2, img_w))
        ny2 = int(min(cy + h_box / 2, img_h))
        return nx1, ny1, nx2, ny2

    while True:
        ret, frame = cap.read()
        if not ret:
            break
        frames_buffer.append(frame)
        batch_indices.append(processed)
        processed += 1

        if len(frames_buffer) == batch_size or processed == total_frames:
            faces_list = []
            for f in frames_buffer:
                try:
                    faces = RetinaFace.detect_faces(f, threshold=threshold)
                except TypeError:
                    faces = RetinaFace.detect_faces(f)
                faces_list.append(faces)

            for idx, (frame, faces) in enumerate(zip(frames_buffer, faces_list)):
                face_dicts = get_face_dicts(faces)
                has_faces = bool(face_dicts)
                if face_found is None:
                    face_found = has_faces
                    if has_faces:
                        log(f"얼굴 발견 시작! 프레임 {batch_indices[idx]+1}")
                    else:
                        log(f"얼굴을 발견하지 못했습니다. 프레임 {batch_indices[idx]+1}")
                elif face_found != has_faces:
                    if has_faces:
                        log(f"얼굴 발견 시작! 프레임 {batch_indices[idx]+1}")
                    else:
                        log(f"얼굴을 발견하지 못했습니다. 프레임 {batch_indices[idx]+1}")
                    face_found = has_faces

                if has_faces:
                    for face in face_dicts:
                        x1, y1, x2, y2 = face['facial_area']
                        bx1, by1, bx2, by2 = expand_box(x1, y1, x2, y2, w, h, scale=1.5)
                        roi = frame[by1:by2, bx1:bx2]
                        if roi.size > 0:
                            blur = cv2.GaussianBlur(roi, (51, 51), 0)
                            frame[by1:by2, bx1:bx2] = blur
                out.write(frame)

                if (batch_indices[idx]+1) in percent_checkpoints:
                    pct = percent_checkpoints.index(batch_indices[idx]+1) + 1
                    now = datetime.now()
                    elapsed = now - start_time
                    log(f"진행률 {pct*10}% ({batch_indices[idx]+1}/{total_frames}프레임) 경과: {str(elapsed).split('.')[0]}")

            frames_buffer.clear()
            batch_indices.clear()

    cap.release()
    out.release()
    end_time = datetime.now()
    log(f"모든 프레임 처리 완료! (종료시간: {end_time.strftime('%Y-%m-%d %H:%M:%S')})")
    log(f"총 경과시간: {str(end_time - start_time).split('.')[0]}")

    log("FFmpeg로 소리 복사 중...")
    ffmpeg_command = [
        "ffmpeg", "-y",
        "-i", input_path,
        "-i", temp_video,
        "-c:v", "copy", "-c:a", "copy",
        "-map", "1:v:0", "-map", "0:a:0?",
        "-shortest",
        output_path
    ]
    result = subprocess.run(ffmpeg_command, capture_output=True, text=True)
    if result.returncode != 0:
        log("❌ FFmpeg 오류 발생:")
        log(result.stderr)
    else:
        log("오디오가 포함된 최종 파일 저장 완료!")

    if os.path.exists(temp_video):
        os.remove(temp_video)

def process_multiple_videos(input_folder='video/input', output_folder='video/output', batch_size=8, threshold=0.1):
    """
    여러 동영상을 일괄 처리하는 함수 (1.5배 블러 크기 적용)
    """
    # 출력 폴더 생성
    os.makedirs(output_folder, exist_ok=True)
    
    # 입력 폴더가 존재하지 않으면 생성
    if not os.path.exists(input_folder):
        os.makedirs(input_folder)
        log(f"📁 입력 폴더 '{input_folder}'를 생성했습니다. 동영상 파일을 넣어주세요.")
        return
    
    # 지원하는 동영상 확장자
    video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm']
    
    # 입력 폴더에서 동영상 파일 찾기
    video_files = []
    for ext in video_extensions:
        pattern = os.path.join(input_folder, f'*{ext}')
        video_files.extend(glob.glob(pattern, recursive=False))
        pattern_upper = os.path.join(input_folder, f'*{ext.upper()}')
        video_files.extend(glob.glob(pattern_upper, recursive=False))
    
    if not video_files:
        log(f"❌ '{input_folder}' 폴더에 처리할 동영상 파일이 없습니다.")
        log(f"지원 형식: {', '.join(video_extensions)}")
        return
    
    total_videos = len(video_files)
    log(f"🎬 총 {total_videos}개의 동영상 파일을 발견했습니다.")
    log(f"설정: 블러 크기 1.5배, threshold={threshold}, batch_size={batch_size}")
    
    # 각 동영상 파일 처리
    for i, video_path in enumerate(video_files, 1):
        filename = os.path.basename(video_path)
        name_without_ext = os.path.splitext(filename)[0]
        output_path = os.path.join(output_folder, f"{name_without_ext}_blurred.mp4")
        temp_video = f"temp_{name_without_ext}.mp4"
        
        log(f"\n🔄 [{i}/{total_videos}] 처리 중: {filename}")
        
        try:
            batch_blur_faces(video_path, output_path, batch_size, threshold, temp_video)
            log(f"✅ 완료: {os.path.basename(output_path)}")
        except Exception as e:
            log(f"❌ 오류 발생 ({filename}): {str(e)}")
            continue
    
    log(f"\n🎉 모든 동영상 처리 완료! 결과는 '{output_folder}' 폴더에 있습니다.")

5. 실행 방법 예시

Python
# 기본 사용법
from face_blur import process_multiple_videos
process_multiple_videos()

# 커스텀 설정
from face_blur import process_multiple_videos
process_multiple_videos(
    input_folder='video/input',
    output_folder='video/output', 
    batch_size=8,
    threshold=0.1
)
  • input.mp4, face_blur.py가 같은 폴더에 있으면 위 한 줄로 얼굴 1.5배 블러 + 소리 OK 영상이 자동 생성됩니다.

6. 폴더 구조 예시

프로젝트 폴더/
├── face_blur.py
├── video/
│   ├── input/           # 여기에 처리할 동영상들 넣기
│   │   ├── video1.mp4
│   │   ├── video2.avi
│   │   └── video3.mov
│   └── output/          # 처리된 결과가 여기에 생성됨
│       ├── video1_blurred.mp4
│       ├── video2_blurred.mp4
│       └── video3_blurred.mp4

7. 문제/에러 대처 Q&A

증상해결 요령
얼굴 블러 안 됨패키지, 모델파일, 환경 변수, cuda, cudnn, ffmpeg, 파일명 재확인
FFmpeg 에러Path에 ffmpeg/bin 포함, CMD 새로 실행, ffmpeg -version으로 정상 확인
retinaface.h5 에러수동 다운로드 후 .deepface/weights 폴더에 복사
GPU 사용 안 됨cuda, cudnn, tf 설치 및 tf.config.list_physical_devices(‘GPU’) 점검
속도 불만족SSD로 작업, 영상 해상도 낮추기, batch_size=8에서 단계 별 조정

전체 다시 정리

  • Python 3.10 + CUDA 11.2 + cuDNN 8.1.1 + FFmpeg + RetinaFace + 환경 변수 + 모델 파일까지, 단계별 안내만 따라하면 얼굴 블러(확대)와 음성이 모두 살아있는 영상을 누구나 만들 수 있습니다.
  • batch_size=8, threshold=0.1, 블러 1.5배 확대 세팅이 기본.
  • 문제가 생기면 환경, 패키지, 모델, FFmpeg, 코드 순서대로 재검토하면 안정적으로 해결됩니다.
  • 위 문서를 그대로 따라하면 초보자도 실전 자동화 환경을 구축해 동영상을 안전하게 만들 수 있습니다.

게시됨

카테고리

작성자

태그: