基于YOLOV8图像处理——人脸检测及遮挡

需求

原本想给视频中的人物批量打码,百度之后发现大部分教程都是使用pr之类的工具进行遮罩处理,由于许久没碰pr,故想着做个自动化处理人脸的工具,编写代码的过程中使用了lindevs大佬训练好的模型,lindevs大佬的项目地址如下:

GitHub - lindevs/yolov8-face: Pre-trained YOLOv8-Face models.

对应的预训练模型链接:https://download.csdn.net/download/Huang_X_H/89813266

以下是该工具的主要代码。

代码实现

代码结构

project
----block_face
    ----block_images
        ----block_image.png
    ----input_files
        ----test_image.jpg
        ----test.mp4
    ----models
        ----yolov8s-face-lindevs.pt
    ----output_files
----block_face.py

block_face.py

导入对应库

主要使用ultralytics(即YOLOV8)来识别人脸,使用moviepy来剪辑视频

import os
import time
from abc import ABCMeta, abstractmethod
from datetime import datetime
import cv2
import numpy as np
from moviepy.audio.io.AudioFileClip import AudioFileClip
from moviepy.video.io.VideoFileClip import VideoFileClip
from ultralytics import YOLO

抽象工具类

此处有抽象类是因为原本想着可以改为有多种具体的人脸检测方法,可以自行合并为一个类

class BlockFaceUtil(metaclass=ABCMeta):
    input_file_path = ""
    output_file_path = None
    flag_mosaic = True
    block_image_path = ""
    input_file_type = "unknown"
    input_file_extension = None
    mosaic_block_size = 10
    block_image = None
    block_image_opacity = 1
    flag_stretch = False
    flag_over_full = True
    flag_show_box = False
    flag_initialed = False

    def get_file_type(self, file_path):
        """
            获取文件类型,图片还是视频
        :param file_path:
        :return:
        """
        if not os.path.exists(file_path):
            return "unknown", None

        image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff']
        video_extensions = ['.mp4', '.avi', '.mkv', '.mov', '.flv', '.wmv']

        _, file_extension = os.path.splitext(file_path)
        if file_extension.lower() in image_extensions:
            return "image", file_extension
        elif file_extension.lower() in video_extensions:
            return "video", file_extension
        else:
            return "unknown", None

    def initial_params(self, input_file_path, output_file_path=None, flag_mosaic=True, block_image_path="",
                       mosaic_block_size=10, block_image_opacity=1, flag_stretch=False, flag_over_full=True,
                       flag_show_box=False):
        """
            初始化目录等基本信息
        :param input_file_path:
        :param output_file_path:
        :param flag_mosaic:
        :param block_image_path:
        :param mosaic_block_size:
        :param block_image_opacity:
        :param flag_stretch:
        :param flag_over_full:
        :param flag_show_box:
        :return:
        """
        self.input_file_path = input_file_path
        self.output_file_path = output_file_path
        self.flag_mosaic = flag_mosaic
        self.block_image_path = block_image_path
        self.mosaic_block_size = mosaic_block_size
        self.block_image_opacity = block_image_opacity
        self.flag_stretch = flag_stretch
        self.flag_over_full = flag_over_full
        self.flag_show_box = flag_show_box
        if not self.flag_mosaic and (not os.path.exists(self.block_image_path) or self.get_file_type(self.block_image_path)[0] != "image"):
            print(f"invalid block_image_path: {self.block_image_path}")
        else:
            self.input_file_type, self.input_file_extension = self.get_file_type(self.input_file_path)
            if self.input_file_type == "image" or self.input_file_type == "video":
                if self.output_file_path is None or self.output_file_path == "" or not os.path.exists(os.path.dirname(self.output_file_path)) or os.path.exists(self.output_file_path):
                    if not os.path.exists("block_face"):
                        os.mkdir("block_face")
                    if not os.path.exists(os.path.join("block_face", "output_files")):
                        os.mkdir(os.path.join("block_face", "output_files"))
                    now = datetime.now()
                    self.output_file_path = os.path.join("block_face", "output_files", f"output_{now.strftime('%Y%m%d%H%M%S')}{self.input_file_extension}")

                # 加载遮挡图片
                self.block_image = cv2.imread(block_image_path, cv2.IMREAD_UNCHANGED) if not self.flag_mosaic else None

                self.flag_initialed = True

            else:
                print("Unsupported file type")

    @abstractmethod
    def block_face(self):
        """
            遮挡人脸
        :return:
        """
        pass

    @abstractmethod
    def handle_frame(self, input_image):
        """
            处理单个图像
        :param input_image:
        :return:
        """
        pass

    @abstractmethod
    def block_face_in_image(self):
        """
            遮挡图片中的人脸
        :return:
        """
        pass

    @abstractmethod
    def block_face_in_video(self):
        """
            遮挡视频中的人脸
        :return:
        """
        pass

    @abstractmethod
    def detect_face(self, original_image):
        """
            检测人脸
        :param original_image:
        :return:
        """
        pass

    @abstractmethod
    def block_image_area_with_mosaic(self, original_image, area):
        """
            使用马赛克对指定区域进行遮挡
        :param original_image:
        :param area:
        :return:
        """
        pass

    @abstractmethod
    def block_image_area_with_image(self, original_image, block_image, block_area):
        """
            使用图片对指定区域进行遮挡
        :param original_image:
        :param block_image:
        :param block_area:
        :return:
        """
        pass

具体工具类

class YoloV8FaceBlockFaceUtil(BlockFaceUtil):
    def __init__(self):
        super().__init__()
        self.model_path = ""
        self.yolo_model = None
        self.flag_yolo_initialed = False
        self.conf = 0.6

    def initial_yolo(self, model_path, conf=0.6):
        """
            除了默认的参数,还需要设置一下yolov8相关的参数
        :param model_path:
        :param conf:
        :return:
        """
        self.model_path = model_path
        self.yolo_model = YOLO(self.model_path)
        self.conf = conf
        self.flag_yolo_initialed = True

    def block_face(self):
        if self.flag_initialed:
            if self.flag_yolo_initialed:
                if self.input_file_type == "image":
                    self.block_face_in_image()
                elif self.input_file_type == "video":
                    self.block_face_in_video()
            else:
                print("未初始化YOLO参数")
        else:
            print("未初始化参数")

    def handle_frame(self, input_image):
        face_locations = self.detect_face(input_image)

        if self.flag_mosaic:
            for face_location in face_locations:
                x1, y1, x2, y2 = face_location
                block_area = (int(x1), int(y1), int(x2), int(y2))  # x1, y1, x2, y2 左上角坐标和右下角坐标
                # 应用马赛克
                input_image = self.block_image_area_with_mosaic(input_image, block_area)
        else:
            for face_location in face_locations:
                x1, y1, x2, y2 = face_location
                block_area = (int(x1), int(y1), int(x2), int(y2))  # x1, y1, x2, y2 左上角坐标和右下角坐标
                # 应用遮挡
                input_image = self.block_image_area_with_image(
                    original_image=input_image,
                    block_image=self.block_image,
                    block_area=block_area)

        if self.flag_show_box:
            # 对于每张脸,画一个矩形框
            for face_location in face_locations:
                x1, y1, x2, y2 = face_location
                cv2.rectangle(input_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)

    def block_face_in_image(self):
        input_image = cv2.imread(self.input_file_path)

        self.handle_frame(input_image)

        # 保存结果
        cv2.imwrite(self.output_file_path, input_image)
        print(f"已输出文件:{self.output_file_path}")

    def block_face_in_video(self):
        output_file_name, file_extension = os.path.splitext(os.path.basename(self.output_file_path))
        output_file_temp_path = os.path.join(os.path.dirname(self.output_file_path),
                                             f"{output_file_name}-temp{file_extension}")
        # 读取视频
        cap = cv2.VideoCapture(self.input_file_path)

        # 获取视频的基本信息
        frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        fps = cap.get(cv2.CAP_PROP_FPS)

        # 创建视频写入器
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        out = cv2.VideoWriter(output_file_temp_path, fourcc, fps, (frame_width, frame_height))

        print("开始生成画面")

        while cap.isOpened():
            ret, frame = cap.read()
            if not ret:
                break

            self.handle_frame(frame)

            # 写入帧
            out.write(frame)

        # 释放资源
        cap.release()
        out.release()

        print("已创建画面,正在合并音频")

        # 加载没有声音的新视频和原视频的声音
        video_without_audio = VideoFileClip(output_file_temp_path)
        audio = AudioFileClip(self.input_file_path)

        # 将音频添加到新视频中
        final_video = video_without_audio.set_audio(audio)

        # 写入最终结果
        final_video.write_videofile(self.output_file_path, codec='libx264')
        print(f"已输出文件:{self.output_file_path}")

    def detect_face(self, original_image):
        # 使用YOLOv8进行人脸检测,conf 参数用于设置置信度阈值,verbose 参数用于在控制台显示预测信息
        results = self.yolo_model.predict(source=original_image, conf=self.conf, verbose=False)  # conf 参数用于设置置信度阈值

        face_locations = []
        # 提取检测结果
        for result in results:
            boxes = result.boxes.cpu().numpy()
            for box in boxes:
                x1, y1, x2, y2 = box.xyxy[0].astype(int)
                face_locations.append((x1, y1, x2, y2))

        return face_locations

    def block_image_area_with_mosaic(self, original_image, area):
        # 获取图片的宽度和高度
        height = original_image.shape[0]
        width = original_image.shape[1]

        # 检查坐标是否超出范围
        x1 = max(0, min(area[0], width))
        y1 = max(0, min(area[1], height))
        x2 = max(x1, min(area[2], width))
        y2 = max(y1, min(area[3], height))

        # 提取指定区域
        region = original_image[y1:y2, x1:x2]

        # 计算马赛克块的大小
        h, w, _ = region.shape
        nh = h // self.mosaic_block_size
        nw = w // self.mosaic_block_size

        # 创建一个空的数组用于存储马赛克后的区域
        mosaic_region = np.zeros_like(region)

        # 对每个马赛克块求平均值并填充
        for i in range(nh):
            for j in range(nw):
                y_start, y_end = i * self.mosaic_block_size, (i + 1) * self.mosaic_block_size
                x_start, x_end = j * self.mosaic_block_size, (j + 1) * self.mosaic_block_size

                # 计算当前块的平均颜色
                avg_color = np.mean(region[y_start:y_end, x_start:x_end], axis=(0, 1)).astype(np.uint8)

                # 将当前块填充为平均颜色
                mosaic_region[y_start:y_end, x_start:x_end] = avg_color

        # 处理最后一行
        if h % self.mosaic_block_size != 0:
            y_start = nh * self.mosaic_block_size
            for j in range(nw):
                x_start, x_end = j * self.mosaic_block_size, (j + 1) * self.mosaic_block_size
                # 计算当前块的平均颜色
                avg_color = np.mean(region[y_start:h, x_start:x_end], axis=(0, 1)).astype(np.uint8)
                # 填充剩余部分
                mosaic_region[y_start:h, x_start:x_end] = avg_color

        # 处理最后一列
        if w % self.mosaic_block_size != 0:
            x_start = nw * self.mosaic_block_size
            for i in range(nh):
                y_start, y_end = i * self.mosaic_block_size, (i + 1) * self.mosaic_block_size
                # 计算当前块的平均颜色
                avg_color = np.mean(region[y_start:y_end, x_start:w], axis=(0, 1)).astype(np.uint8)
                # 填充剩余部分
                mosaic_region[y_start:y_end, x_start:w] = avg_color

        # 如果最后一行和最后一列都有剩余,则单独处理最后一个色块
        if h % self.mosaic_block_size != 0 and w % self.mosaic_block_size != 0:
            y_start = nh * self.mosaic_block_size
            x_start = nw * self.mosaic_block_size
            # 计算当前块的平均颜色
            avg_color = np.mean(region[y_start:h, x_start:w], axis=(0, 1)).astype(np.uint8)
            # 填充剩余部分
            mosaic_region[y_start:h, x_start:w] = avg_color

        # 将马赛克后的区域放回原图
        original_image[y1:y2, x1:x2] = mosaic_region

        return original_image

    def block_image_area_with_image(self, original_image, block_image, block_area):
        # 获取背景图片的尺寸
        bg_height, bg_width = original_image.shape[:2]

        # 获取前景图片的尺寸
        fg_height, fg_width = block_image.shape[:2]

        # 计算指定区域的中心
        region_center = ((block_area[0] + block_area[2]) // 2,
                         (block_area[1] + block_area[3]) // 2)

        # 计算前景图片的中心
        fg_center = (fg_width // 2, fg_height // 2)

        # 获取指定区域的尺寸
        region_width = block_area[2] - block_area[0]
        region_height = block_area[3] - block_area[1]

        if self.flag_stretch:
            new_height = int(region_height)
            new_width = int(region_width)
        else:
            # 调整前景图片大小以完全覆盖指定区域
            aspect_ratio = fg_width / fg_height
            if self.flag_over_full:
                if region_width / region_height < aspect_ratio:
                    new_height = int(region_height)
                    new_width = int(new_height * aspect_ratio)
                else:
                    new_width = int(region_width)
                    new_height = int(new_width / aspect_ratio)
            else:
                if region_width / region_height > aspect_ratio:
                    new_height = int(region_height)
                    new_width = int(new_height * aspect_ratio)
                else:
                    new_width = int(region_width)
                    new_height = int(new_width / aspect_ratio)

        # 保持长宽比,调整前景图片大小
        resized_foreground = cv2.resize(block_image, (new_width, new_height))

        # 计算调整后前景图片的中心
        resized_fg_center = (new_width // 2, new_height // 2)

        # 计算调整后前景图片在背景图片中的左上角坐标
        bg_x_start = region_center[0] - resized_fg_center[0]
        bg_y_start = region_center[1] - resized_fg_center[1]

        # 计算调整后前景图片在背景图片中的右下角坐标
        bg_x_end = bg_x_start + new_width
        bg_y_end = bg_y_start + new_height

        # 获取背景图片的重叠区域
        bg_overlap = original_image[max(0, bg_y_start):min(bg_y_end, bg_height), max(0, bg_x_start):min(bg_x_end, bg_width)]

        # 获取前景图片的重叠区域
        fg_overlap = resized_foreground[max(0, -bg_y_start):min(new_height, bg_y_end - bg_y_start),
                     max(0, -bg_x_start):min(new_width, bg_x_end - bg_x_start)]

        # 处理超出区域的部分
        if fg_overlap.shape[2] == 4:
            # 前景图片有Alpha通道
            alpha_channel = fg_overlap[:, :, 3] / 255.0

            # 对超出区域的部分设置透明度
            alpha_channel[alpha_channel > 0] = self.block_image_opacity

            # 确保 fg_overlap 和 bg_overlap 的形状匹配
            if fg_overlap.shape[:2] != bg_overlap.shape[:2]:
                # 裁剪或填充 fg_overlap 以匹配 bg_overlap
                fg_overlap = fg_overlap[:bg_overlap.shape[0], :bg_overlap.shape[1]]
                alpha_channel = alpha_channel[:bg_overlap.shape[0], :bg_overlap.shape[1]]

            # 应用Alpha混合
            blended = (alpha_channel[..., np.newaxis] * fg_overlap[:, :, :3] + (
                        1 - alpha_channel[..., np.newaxis]) * bg_overlap).astype(np.uint8)

            # 替换背景图片的重叠区域
            original_image[max(0, bg_y_start):min(bg_y_end, bg_height),
            max(0, bg_x_start):min(bg_x_end, bg_width)] = blended
        else:
            # 前景图片没有Alpha通道
            # 应用透明度
            blended = (self.block_image_opacity * fg_overlap + (1 - self.block_image_opacity) * bg_overlap).astype(np.uint8)

            # 替换背景图片的重叠区域
            original_image[max(0, bg_y_start):min(bg_y_end, bg_height),
            max(0, bg_x_start):min(bg_x_end, bg_width)] = blended

        return original_image

程序入口

def main():
    start_time = time.time()
    print("程序开始运行")
    block_face_util = YoloV8FaceBlockFaceUtil()
    # 遮挡图片中的人脸,只设置了必要的参数
    block_face_util.initial_params(input_file_path="block_face/input_files/test_image.png",
                                   output_file_path="block_face/output_files/test_image_yolov8_face.png",
                                   flag_mosaic=False,
                                   block_image_path="block_face/block_images/block_image.png",)
    # 遮挡视频中的人脸
    block_face_util.initial_params(input_file_path="block_face/input_files/test.mp4",
                                   output_file_path="block_face/output_files/output_yolo_face.mp4",
                                   flag_mosaic=False,
                                   block_image_path="block_face/block_images/block_image.png",
                                   mosaic_block_size=10,
                                   block_image_opacity=1,
                                   flag_stretch=False,
                                   flag_over_full=True,
                                   flag_show_box=False)
    block_face_util.initial_yolo(model_path="block_face/models/yolov8s-face-lindevs.pt", conf=0.6)
    block_face_util.block_face()

    print(f"耗时:{time.time() - start_time}秒")

if __name__ == '__main__':
    main()

效果展示

原图

程序处理之后,人脸露出较多的都能检测到了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值