하루에 5분씩만 투자해서 함께 공부해요~ 댓글은 개별 포스트 클릭하면 작성가능합니다~

Yolo를 이용해서 여러명의 얼굴을 모자이크 처리하기

나 빼고 다 모자이크 처리해야 하는데…

  • 지난 시간에 배경제거 AI 모델을 이용해서 단체사진 속에 저의 사진을 합성해 봤는데요
  • 결과물을 블로그에 올리려다 보니, 사진에 나오신 분들의 얼굴을 모자이크 처리해야 겠더라구요
  • 처음에는 포토피아로 하나하나 블럭 지정해서 할까 하다가 그냥 코파일럿으로 코드 하나 만들어서 돌리는게 더 편하겠더군요
    • 결과물
  • 그런데 또 제 사진은 굳이 모자이크 처리하지 않고 싶은거에요…
광고를 클릭해주시면 블로그운영에 큰 힘이 됩니다.

모자이크 처리 할 얼굴을 골라서 해보자

  • 얼굴인식 모델은 yolo를 이용했구요
  • 프로그램을 돌리면 디텍팅한 얼굴을 하나씩 파일로 저장하게 했습니다.
    • 개별얼굴사진
  • 그럼 이제 여기서 제 사진의 번호만 빼고 selection 옵션에 입력해서 모자이크 처리하게 할건데요
  • 위 사진을 보시면 아시겠지만, 정확도가 100%는 아니라서 얼굴이 아닌 것도 디텍팅 합니다.
  • 굳이 이런것도 모자이크 처리하는데 넣을 필요는 없겟죠.
import os
import cv2
import numpy as np
from PIL import Image
from ultralytics import YOLO

def ensure_dir(path):
    if not os.path.exists(path):
        os.makedirs(path)

def detect_faces_yolov8(image_np, model_path='yolov8x-face-lindevs.pt', conf=0.15):
    # YOLOv8-face 모델 로드 및 추론
    model = YOLO(model_path)
    results = model(image_np, conf=conf)
    faces = []
    for r in results:
        for box in r.boxes.xyxy.cpu().numpy():
            x1, y1, x2, y2 = box[:4]
            x, y, w, h = int(x1), int(y1), int(x2 - x1), int(y2 - y1)
            faces.append((x, y, w, h))
    return faces

def save_faces(image_np, faces, output_dir, expand_ratio=0.8):
    ensure_dir(output_dir)
    h_img, w_img = image_np.shape[:2]
    for idx, (x, y, w, h) in enumerate(faces):
        # 영역 확대
        x_exp = max(0, int(x - w * expand_ratio / 2))
        y_exp = max(0, int(y - h * expand_ratio / 2))
        w_exp = min(w_img - x_exp, int(w * (1 + expand_ratio)))
        h_exp = min(h_img - y_exp, int(h * (1 + expand_ratio)))
        face_img = image_np[y_exp:y_exp+h_exp, x_exp:x_exp+w_exp]
        face_pil = Image.fromarray(face_img)
        face_pil.save(os.path.join(output_dir, f"face_{idx}.png"))

def mosaic_faces(image_np, faces, selection, expand_ratio=0.8):
    out_img = image_np.copy()
    h_img, w_img = image_np.shape[:2]
    # selection이 'all'이면 모든 얼굴 인덱스 선택
    if selection == 'all':
        selected_indices = set(range(len(faces)))
    else:
        selected_indices = set(selection)
    for idx, (x, y, w, h) in enumerate(faces):
        if idx in selected_indices:
            # 영역 확대
            x_exp = max(0, int(x - w * expand_ratio / 2))
            y_exp = max(0, int(y - h * expand_ratio / 2))
            w_exp = min(w_img - x_exp, int(w * (1 + expand_ratio)))
            h_exp = min(h_img - y_exp, int(h * (1 + expand_ratio)))
            face = out_img[y_exp:y_exp+h_exp, x_exp:x_exp+w_exp]
            small = cv2.resize(face, (max(1, w_exp//15), max(1, h_exp//15)), interpolation=cv2.INTER_LINEAR)
            mosaic = cv2.resize(small, (w_exp, h_exp), interpolation=cv2.INTER_NEAREST)
            out_img[y_exp:y_exp+h_exp, x_exp:x_exp+w_exp] = mosaic
    return out_img

def main(input_path, output_dir, selection_str=None, model_path='yolov8x-face-lindevs.pt'):
    # PIL로 이미지 로드 (jfif 등 지원)
    pil_img = Image.open(input_path).convert('RGB')
    image_np = np.array(pil_img)

    faces = detect_faces_yolov8(image_np, model_path)
    print(f"얼굴 검출 수: {len(faces)}")

    # 1. 얼굴 부분만 따로 저장
    save_faces(image_np, faces, output_dir)

    # 2. 선택된 얼굴만 모자이크 처리
    if selection_str:
        if selection_str.strip().lower() == 'all':
            selection = 'all'
        else:
            selection = [int(s) for s in selection_str.split(',') if s.strip().isdigit()]
        mosaic_img = mosaic_faces(image_np, faces, selection, expand_ratio=0.8)
        mosaic_pil = Image.fromarray(mosaic_img)
        mosaic_path = os.path.join(output_dir, "mosaic.png")
        mosaic_pil.save(mosaic_path)
        print(f"모자이크 이미지 저장: {mosaic_path}")

if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--input", type=str, required=True, help="입력 이미지 파일 경로 (jpg, png, jfif 등)")
    parser.add_argument("--output", type=str, required=True, help="출력 폴더 경로")
    parser.add_argument("--selection", type=str, default="", help="모자이크 처리할 얼굴 번호(쉼표로 구분, 예: 0,2,3, 또는 all)")
    parser.add_argument("--model", type=str, default="yolov8x-face-lindevs.pt", help="YOLOv8-face 모델 경로")
    args = parser.parse_args()
    main(args.input, args.output, args.selection, args.model)

U2Net VS Detectron2, 어떤 모델이 배경을 더 잘 지워줄까?

배경을 제거해야 하는 이유

  • 얼마전에 저희 사회인야구팀이 우승을 했는데요. 처갓집에 가족들을 데리로 가야 할 시간이 다되어서 우승 사진을 못 찍고 그냥 왔어요.
  • 사진 속에 제가 없는게 너무 허전했는데, 때마침 저말고 또 다른 한 친구가 가려져서 얼굴이 안보인다고 하더군요.
  • 그래서 AI를 이용해서 합성을 해보려고 하는데, 하고싶은대로 잘 안되더라구요.
    • 괴물이나타났다
  • 어쩔수 없이 제가 직접 편집을 해서 만들어야 했는데, 문제는 배경을 지우는 것 이었어요.

U2Net을 이용해서 지워봤습니다

  • 먼저 제 사진을 하나 찾아서 배경을 지워봤는데요. 처음에는 아래 라이브러리를 이용했습니다.
  • 나름 나쁘지 않은 것 같아서, 이번에는 단체 사진에서 배경을 제거해 봤습니다.
    • 단체사진1
    • U-2-Net의 약점이 발견되었습니다. 혼자 있는 사람은 잘 처리하지만, 단체사진일 경우에 디텍팅하는 정확도가 떨어집니다.

Detectron2로 바꿔봤습니다

  • 아무래도 Detectron2가 여러명을 찾아내는 능력은 더 뛰어날 것 같아서 라이브러리를 바꿔서 구현해 봤습니다.
    • Detectron2
    • 단체사진2
    • 그렇습니다. 현수막 부분까지 아주 정확하게 제외하고 사람들만 딱딱 알아서 찾아줍니다.
  • 반면 1명만 있는 사진은 다소 부자연스러운 결과가 나타났는데요.
    • 독사진2
    • 배경 부분이 약간 남아있는걸 확인할 수 있습니다.

결론

  • U2Net은 한 사람을 정확하게 찾아내는데 강하고, Detectron2는 여러명을 찾아내는데 강하다.
  • 사용한 코드는 아래와 같습니다. 각각의 라이브러리는 git으로 직접 설치해서 임포트 했구요.
  • U2Net의 경우 모델 파일은 직접 다운로드 해야 합니다.
import os
from PIL import Image, ImageFilter
import torch
import torch.nn.functional as F
from torchvision import transforms
import numpy as np
import cv2
import sys
sys.path.append(r'detectron2')

from detectron2.engine import DefaultPredictor
from detectron2.config import get_cfg
from detectron2 import model_zoo

sys.path.append(r'U-2-Net/model')
# U^2-Net 모델 코드 가져오기
from u2net import U2NET  # u2net.py 파일을 같은 폴더에 두세요

def u2net_remove_bg(input_path, output_path, model_path='u2net.pth'):
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    net = U2NET(3, 1)
    net.load_state_dict(torch.load(model_path, map_location=device))
    net.to(device)
    net.eval()

    # 이미지 로드 및 전처리
    image = Image.open(input_path).convert('RGB')
    orig_size = image.size
    image = image.resize((320, 320), Image.BICUBIC)
    img_np = np.array(image).astype(np.float32) / 255.0
    img_np = img_np.transpose((2, 0, 1))
    img_tensor = torch.from_numpy(img_np).unsqueeze(0).to(device)

    # 마스크 예측
    with torch.no_grad():
        d1, *_ = net(img_tensor)
        pred = d1[:, 0, :, :]
        pred = F.interpolate(pred.unsqueeze(1), size=orig_size[::-1], mode='bilinear', align_corners=True)
        mask = pred.squeeze().cpu().numpy()
        mask = (mask - mask.min()) / (mask.max() - mask.min() + 1e-8)

    # 경계 feathering (OpenCV)
    mask_uint8 = (mask * 255).astype(np.uint8)
    mask_blur = cv2.GaussianBlur(mask_uint8, (15, 15), 0)
    mask_blur = mask_blur.astype(np.float32) / 255.0

    # RGBA 이미지 생성
    orig_img = Image.open(input_path).convert('RGB')
    orig_np = np.array(orig_img)
    result = np.zeros((orig_np.shape[0], orig_np.shape[1], 4), dtype=np.uint8)
    result[..., :3] = orig_np
    result[..., 3] = (mask_blur * 255).astype(np.uint8)

    out_img = Image.fromarray(result)
    if not output_path.lower().endswith('.png'):
        output_path = os.path.splitext(output_path)[0] + '.png'
    out_img.save(output_path)
    print(f"Saved: {output_path}")


def detectron2_remove_people(input_path, output_path):
    # Detectron2 config 및 모델 로드 (COCO Instance Segmentation)
    cfg = get_cfg()
    cfg.merge_from_file(model_zoo.get_config_file("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml"))
    cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.5
    cfg.MODEL.ROI_HEADS.NUM_CLASSES = 80
    cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml")
    cfg.MODEL.DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
    predictor = DefaultPredictor(cfg)

    # PIL로 이미지 로드 후 numpy 변환
    pil_img = Image.open(input_path).convert('RGB')
    image = np.array(pil_img)
    image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)

    outputs = predictor(image_bgr)
    masks = outputs["instances"].pred_masks.cpu().numpy()
    classes = outputs["instances"].pred_classes.cpu().numpy()

    # COCO에서 사람 클래스는 0
    person_masks = [m for m, c in zip(masks, classes) if c == 0]
    if not person_masks:
        print("사람 객체가 없습니다.")
        return

    # 여러 사람 마스크 합치기
    mask = np.zeros(image.shape[:2], dtype=np.uint8)
    for m in person_masks:
        mask = np.maximum(mask, m.astype(np.uint8))

    # 경계 feathering
    mask_blur = cv2.GaussianBlur(mask * 255, (15, 15), 0).astype(np.float32) / 255.0

    # RGBA 이미지 생성
    result = np.zeros((image.shape[0], image.shape[1], 4), dtype=np.uint8)
    result[..., :3] = image
    result[..., 3] = (mask_blur * 255).astype(np.uint8)

    out_img = Image.fromarray(result)
    if not output_path.lower().endswith('.png'):
        output_path = output_path.rsplit('.', 1)[0] + '.png'
    out_img.save(output_path)
    print(f"Saved: {output_path}")


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--input", type=str, required=True, help="입력 이미지 파일 경로 (jpg, jpeg, png, jfif 등)")
    parser.add_argument("--output", type=str, required=True, help="출력 이미지 파일 경로")
    parser.add_argument("--u2net", type=str, default="u2net.pth", help="U^2-Net 모델 경로")
    args = parser.parse_args()
    detectron2_remove_people(args.input, args.output)

OpenVPN 이용시 VPN 서버가 아니라 내 PC에 연결된 인터넷 회선을 사용하는지 체크하기

OpenVPN 서버가 클라우드 서비스에 설치되어 있다면?

  • 만약에 여러분이 OpenVPN을 이용해서 가상 사설망을 구축했고
  • OpenVPN 서버가 AWS EC2와 같은 클라우드 서비스의 인스턴스에 설치되어 있다고 가정해봅시다.
  • OpenVPN 설정으로 인해서 내가 요청하는 인터넷 접속 트래픽이 VPN 서버를 통해서 외부로 나가게 된다면?
  • EC2 인스턴스가 인터넷을 사용하는 꼴이 됩니다. 자칫하면 요금이 많이 나오겠죠?

외부 인터넷 트래픽 따로 내부 트래픽 따로인지 확인하는 법

  • 보통 위와 같은 일을 막기 위해서 외부 인터넷은 원래 쓰는 인터넷 망을 이용해서 접속하게 설정하는데요.
  • 혹시라도 그렇게 하고 있는지 확인하려면 아래와 같은 방법을 사용하세요.
    • 먼저 원래 사용하던 인터넷 망의 IP를 확인하세요.
    • 그리고 myip.com 과 같이 IP 확인하는 사이트에 접속해 보세요.
    • 그랬을 때 IP가 앞서 확인해 둔 IP와 같다면, 원하는 방식으로 접속하는 거구요. 아니면 요금폭탄이 터질 수 있으니 설정을 바꾸셔야 합니다.

IP 체크 사이트에 접속하지 않고 확인하는 법

  • 터미널에서 아래와 같이 입력해 보세요(맥, 리눅스)
  • curl ifconfig.me
  • 사실 뭐.. 이것도 인터넷 사이트에 접속하는거긴 하지만…
  • 저래서 나온 IP가 AWS와 같은 클라우드 서비스의 IP이면 당장 설정을 바꾸셔야 합니다!