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 - 폴더가 없으면 직접 만듦
- Windows:
3. 파이썬 패키지 설치
Bash
pip install opencv-python
pip install retina-face
pip install tensorflow==2.10.04. 얼굴 블러+소리 보존 전체 코드 (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, 코드 순서대로 재검토하면 안정적으로 해결됩니다.
- 위 문서를 그대로 따라하면 초보자도 실전 자동화 환경을 구축해 동영상을 안전하게 만들 수 있습니다.