首页 > 编程语言 >24/11/11 算法笔记<视觉> 换脸,人脸特征点检测

24/11/11 算法笔记<视觉> 换脸,人脸特征点检测

时间:2024-11-11 21:17:25浏览次数:6  
标签:11 24 return image mask 凸包 人脸 face 换脸

先介绍一下换脸的简单步骤

1、提取两张图片的脸部特征点

2、为两张图片创建mask

3、进行映射变换使得人脸对齐

4、使用opencv的泊松融合将两张图片合成

我们直接上代码

1.导入代码包

import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
import cv2
import numpy as np

mediapipe 是一个由Google开发的多平台框架,用于构建应用中的多媒体内容处理管道。它特别适用于构建和部署跨平台的计算机视觉应用,如视频分析、图像处理等。

  1. mediapipe

    • mediapipe库提供了一系列的解决方案(solutions),用于实现特定的计算机视觉任务,如人脸检测、手势识别、姿态识别等。
    • 它具有实时性能,可以用于实时应用程序和流媒体处理,各种模型基本上可以做到实时运行且速度较快。
    • 支持跨平台和多语言,可以在Android、iOS、Windows和Linux等多个平台上运行,支持C++、Python、JavaScript、Coral等主流编程语言。
    • 提供了一系列经过训练的模型,可以直接用于各种计算机视觉和音频处理任务。
  2. mediapipe.tasks

    • mediapipe.tasks模块是mediapipe的一部分,它提供了任务库,用于处理特定的任务,如图像识别、视频分析等。
    • mediapipe.tasks.pythonmediapipe.tasks的一个子模块,专门用于Python语言的任务处理。
  3. mediapipe.solutions

    • mediapipe.solutionsmediapipe中用于访问预训练模型和解决方案的模块,例如手势识别(hands)、姿态识别(pose)等。

2.获取眼部标志位置

#新的获取眼部标志位置代码
def ladmask(img):
    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img)#创建一个图像对象
    #创建一个基础选项对象,用于配置面部标志检测模型的路径。
    base_options = python.BaseOptions(model_asset_path='face_landmarker.task')

    #创建一个面部标志检测选项对象,配置输出面部混合形状和面部变换矩阵,并设置检测的面部数量为1。
    options = vision.FaceLandmarkerOptions(base_options=base_options,
                                           output_face_blendshapes=True,
                                           output_facial_transformation_matrixes=True,
                                           num_faces=1)
    #根据配置的选项创建一个面部标志检测器。
    detector = vision.FaceLandmarker.create_from_options(options)
    #使用检测器对 mp_image 图像进行检测,获取检测结果。
    detection_result = detector.detect(mp_image)
    x=[]
    y=[]
    points=[]
    for i in range(0,len(detection_result.face_landmarks[0])):
        points.append([int(detection_result.face_landmarks[0][i].x*img.shape[1]),int(detection_result.face_landmarks[0][i].y*img.shape[0])])
    points=np.array(points)
    return points

这边重要的有一个面部标志检测器,用于面部特征点检测,比如面部轮廓、眼睛、鼻子、嘴巴等标志点的检测。我们来看一下他的运行方式

2.1

源代码有些繁琐,我让gpt生成了简易的代码,让我们来看下

class FaceLandmarkerOptions:
    def __init__(self, model_path, output_blendshapes, output_transformation_matrices, num_faces):
        self.model_path = model_path
        self.output_blendshapes = output_blendshapes
        #是否输出面部变换矩阵,这些矩阵可以用来对面部特征进行变换。
        self.output_transformation_matrices = output_transformation_matrices
        self.num_faces = num_faces

class FaceLandmarker:
    def __init__(self, options):
        self.options = options
        # 加载人脸检测分类器Haar
        self.face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
        # 初始化识别的方法,这里以LBPH方法为例
        self.recog = cv2.face.LBPHFaceRecognizer_create()
        # 加载训练好的模型
        self.recog.read(self.options.model_path)
        print(f"Initializing Face Landmarker with model from {options.model_path}")

    def detect(self, image):
        # 这里模拟面部标志点检测过程
        print("Detecting face landmarks...")
        # 假设检测到的面部标志点数据
        landmarks = [[(100, 100), (150, 150), (200, 200)]]  # 示例数据
        return landmarks

def create_face_landmarker_from_options(options):
    # 根据提供的选项创建面部标志检测器实例
    return FaceLandmarker(options)

# 使用示例
if __name__ == "__main__":
    # 创建配置选项
    options = FaceLandmarkerOptions(
        model_path='path/to/model',
        output_blendshapes=True,
        output_transformation_matrices=True,
        num_faces=1
    )
    
    # 根据选项创建面部标志检测器
    face_landmarker = create_face_landmarker_from_options(options)
    
    # 模拟一个图像
    image = "path/to/image.jpg"
    
    # 检测面部标志点
    landmarks = face_landmarker.detect(image)
    print("Detected landmarks:", landmarks)

人脸检测算法中的特征点是通过以下步骤找到的:

  1. 定义特征点:首先,算法定义了一张脸上的具体特征点数量,常见的有人脸的68个特征点(landmarks)。

  2. 标记面部特征点:需要手动标记图像上的面部特征点,以获得带标记的训练数据。这些标签指定围绕每个面部结构的区域。

  3. 训练回归树的集合:给定训练数据,算法训练一个回归树的集合,直接从像素强度本身估计面部界标位置,得到模型。

  4. 人脸检测:使用人脸检测器,如Dlib的get_frontal_face_detector(),来检测图像中的人脸,并提取人脸外部矩形框。

  5. 特征点预测:利用训练好的人脸特征点检测器,如Dlib的shape_predictor,进行人脸面部轮廓特征提取。这个模型能够识别人脸上的68个关键点,包括眼睛、鼻子、嘴巴等部位的位置。

  6. 级联的残差回归树(GBDT):算法使用的是“回归树”的概念,通过建立一个级联的残差回归树来使人脸形状从初始形状逐步回归到真实形状。每个GBDT的叶子节点上都存储着一个残差回归量,当输入落到一个节点上时,就将残差加到该输入上,起到回归的目的。

  7. 训练集成的回归树模型:在训练时,使用标记有人脸特征点的图像来训练一个集成的回归树模型,这些回归树共同预测人脸特征点的位置。

  8. 实时人脸特征点检测:无论给出怎样一幅图像,都通过这个模型来预测人脸特征点的位置,该算法能够用于实时的人脸特征点检测,并且预测质量高,是一个高效且鲁棒的特征点定位方法。

我们来看下特征点检测器Dlib的shape_predictor

Dlib的get_frontal_face_detector()函数是Dlib库中用于检测图像中人脸的一个功能。其检测人脸的原理主要基于以下几个步骤:

  1. HOG特征提取get_frontal_face_detector()函数使用Histogram of Oriented Gradients(HOG)特征检测技术。HOG是一种描述图像局部特征的方法,通过计算图像中每个小区域的梯度方向和幅值来提取特征。这些特征能够捕捉到图像中的纹理、形状等信息,非常适合用于人脸检测。

  2. 级联分类器:Dlib的人脸检测算法使用了级联分类器,这是一种由多个弱分类器组成的强分类器。在人脸检测中,级联分类器可以逐层过滤掉非人脸区域,从而提高检测的效率和准确性。级联分类器的每一层都是一个简单的分类器,它们会根据HOG特征向量进行人脸和非人脸的分类。

  3. 滑动窗口检测:在检测过程中,算法会在图像上滑动一个小窗口,检查窗口内的所有区域是否包含人脸。这个过程会遍历图像的每个像素,但由于级联分类器的逐层筛选,大部分非人脸区域可以快速被排除,从而减少计算量。

  4. 检测流程:Dlib人脸检测的流程包括加载图像、初始化分类器、扫描图像、提取特征、分类器判断、输出结果和可视化显示等步骤。通过这些步骤,算法能够有效地在图像中定位人脸,并输出人脸的位置和大小信息。

get_frontal_face_detector()函数通常接受两个参数:一个是灰度图像,另一个是可选的上采样次数(默认为0),上采样可以提高检测的精度,但也会增加计算量。

总结来说,get_frontal_face_detector()通过结合HOG特征提取和级联分类器技术,实现了高效且准确的正面人脸检测。

2.1.1

发现这里还有个核心的LBPHFaceRecognizer_create()函数,我们来来看一下它的简单源码

#include <opencv2/opencv.hpp>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/objdetect.hpp>
#include <opencv2/face.hpp>

class LBPHFaceRecognizer : public cv::face::BasicFaceRecognizer {
public:
    LBPHFaceRecognizer(int _radius, int _neighbors, int _grid_x, int _grid_y, double _threshold)
        : radius(_radius), neighbors(_neighbors), grid_x(_grid_x), grid_y(_grid_y), threshold(_threshold) {}

    virtual void train(InputArrayOfArrays src, InputArray labels) override {
        // 训练代码
    }

    virtual int predict(InputArray src) override {
        // 预测代码
        return 0;
    }

    virtual void read(const FileNode& fn) override {
        // 读取模型代码
    }

    virtual void write(FileStorage& fs) const override {
        // 保存模型代码
    }

    virtual int getRadius() const { return radius; }
    virtual int getNeighbors() const { return neighbors; }
    virtual int getGridX() const { return grid_x; }
    virtual int getGridY() const { return grid_y; }
    virtual double getThreshold() const { return threshold; }

private:
    int radius, neighbors;
    int grid_x, grid_y;
    double threshold;
};

// 创建函数
cv::Ptr<cv::face::LBPHFaceRecognizer> LBPHFaceRecognizer_create(int radius=1, int neighbors=8,
    int grid_x=8, int grid_y=8, double threshold=-1.0) {
    return cv::makePtr<LBPHFaceRecognizer>(radius, neighbors, grid_x, grid_y, threshold);
}

分段解读代码

2.1.2类定义和成员变量

class LBPHFaceRecognizer : public cv::face::BasicFaceRecognizer {
public:
    LBPHFaceRecognizer(int _radius, int _neighbors, int _grid_x, int _grid_y, double _threshold)
        : radius(_radius), neighbors(_neighbors), grid_x(_grid_x), grid_y(_grid_y), threshold(_threshold) {}
    ...
private:
    int radius, neighbors;
    int grid_x, grid_y;
    double threshold;
};

     

2.1.3公共成员函数

virtual void train(InputArrayOfArrays src, InputArray labels) override {
        // 将输入数据转换为适合处理的格式
        std::vector<cv::Mat> images = src.getMatVector();
        std::vector<int> labelsVec = labels.getMat().row(0) * labels.getMat().row(0).t();
        
        // 存储训练数据
        for (size_t i = 0; i < images.size(); i++) {
            cv::Mat hist;
            computeLBP(images[i], hist);
            // 存储直方图和标签
            histograms.push_back(hist.clone());
            labelsHist.push_back(labelsVec[i]);
        }
    }
virtual int predict(InputArray src) override {
        cv::Mat img = src.getMat();
        cv::Mat hist;
        computeLBP(img, hist);
        
        // 计算直方图之间的距离
        double bestDist = DBL_MAX;
        int bestLabel = -1;
        for (size_t i = 0; i < histograms.size(); i++) {
            double dist = histNorm(hist, histograms[i]);
            if (dist < bestDist) {
                bestDist = dist;
                bestLabel = labelsHist[i];
            }
        }
        return bestLabel;
    }
virtual void read(const cv::FileNode& fn) override {
        // 读取模型代码(省略)
    }

    virtual void write(cv::FileStorage& fs) const override {
        // 保存模型代码(省略)
    }

2.1.4访问器函数

virtual int getRadius() const { return radius; }
virtual int getNeighbors() const { return neighbors; }
virtual int getGridX() const { return grid_x; }
virtual int getGridY() const { return grid_y; }
virtual double getThreshold() const { return threshold; }
  • 这些函数提供了对私有成员变量的只读访问,允许外部代码获取这些参数的值。

2.1.5computeLBP 方法

这个方法计算给定图像的局部二值模式(LBP)直方图。LBP是一种纹理描述符,用于提取图像中的局部特征。

void computeLBP(const cv::Mat& image, cv::Mat& hist) {
    int histSize = 256; // LBP有256种可能的模式
    hist.create(histSize, 1, CV_32F);
    hist.setTo(0);

    for (int y = 1; y < image.rows - 1; y++) {
        for (int x = 1; x < image.cols - 1; x++) {
            uchar center = image.at<uchar>(y, x);
            int idx = 0;
            for (int i = -1; i <= 1; i++) {
                for (int j = -1; j <= 1; j++) {
                    if (image.at<uchar>(y + i, x + j) > center) idx |= (1 << ((i + 1) * 3 + (j + 1)));
                }
            }
            hist.at<float>(idx) += 1;
        }
    }
}

2.1.6histNorm 方法

double histNorm(const cv::Mat& hist1, const cv::Mat& hist2) {
    cv::Mat diff;
    cv::absdiff(hist1, hist2, diff);
    return cv::sum(diff)[0];
}

2.1.7工厂函数

cv::Ptr<cv::face::LBPHFaceRecognizer> LBPHFaceRecognizer_create(int radius=1, int neighbors=8,
    int grid_x=8, int grid_y=8, double threshold=-1.0) {
    return cv::makePtr<LBPHFaceRecognizer>(radius, neighbors, grid_x, grid_y, threshold);
}
  • LBPHFaceRecognizer_create 是一个工厂函数,它创建并返回一个 LBPHFaceRecognizer 类的实例。这个函数允许用户指定算法参数,如果用户不指定,则使用默认值。
  • cv::makePtr 是 OpenCV 中用于创建智能指针的函数,它可以自动管理内存,确保对象在使用完毕后被正确销毁。

3图像处理和特征点匹配

def transformation_from_points(points1, points2):
    '''0 - 先确定是float数据类型 '''
    points1 = points1.astype(numpy.float64)
    points2 = points2.astype(numpy.float64)

    '''1 - 消除平移的影响 '''
    c1 = numpy.mean(points1, axis=0)
    c2 = numpy.mean(points2, axis=0)
    points1 -= c1
    points2 -= c2

    '''2 - 消除缩放的影响 '''
    s1 = numpy.std(points1)
    s2 = numpy.std(points2)
    points1 /= s1
    points2 /= s2

    '''3 - 计算矩阵M=BA^T;对矩阵M进行SVD分解;计算得到R '''
    # ||RA-B||; M=BA^T
    A = points1.T # 2xN
    B = points2.T # 2xN
    M = np.dot(B, A.T)
    U, S, Vt = numpy.linalg.svd(M)
    R = np.dot(U, Vt)

    '''4 - 构建仿射变换矩阵 '''
    s = s2/s1
    sR = s*R
    c1 = c1.reshape(2,1)
    c2 = c2.reshape(2,1)
    T = c2 - np.dot(sR,c1) # 模板人脸的中心位置减去 需要对齐的中心位置(经过旋转和缩放之后)

    trans_mat = numpy.hstack([sR,T])   # 2x3

    return trans_mat


def get_affine_image(image1, image2, face_landmarks1, face_landmarks2,M):
    """
    获取图片1仿射变换后的图片
    :param image1: 图片1, 要进行仿射变换的图片
    :param image2: 图片2, 只要用来获取图片大小,生成与之大小相同的仿射变换图片
    :param face_landmarks1: 图片1的人脸特征点
    :param face_landmarks2: 图片2的人脸特征点
    :return: 仿射变换后的图片
    """
    three_points_index = [18, 8, 25]
    M = M
    #M=cv2.getAffineTransform(face_landmarks1[three_points_index].astype(np.float32),face_landmarks2[three_points_index].astype(np.float32))
    dsize = (image2.shape[1], image2.shape[0])
    affine_image = cv2.warpAffine(image1, M, dsize)
    return affine_image.astype(np.uint8)

def get_face_mask(img,face_landmarks):
    """
    获取人脸掩模
    :param image_size: 图片大小
    :param face_landmarks: 68个特征点
    :return: image_mask, 掩模图片
    """
    img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
    image_size = (img.shape[0], img.shape[1])
    mask = np.zeros_like(img_gray)
#    convexhull = cv2.convexHull(points)
#    cv2.polylines(img,[convexhull],True,(255,0,0), 3)
#    cv2.fillConvexPoly(mask,convexhull,255)
   # mask = np.zeros(image_size, dtype=np.uint8)
    #points = np.concatenate([face_landmarks[0:16], face_landmarks[26:17:-1]])
    #points = np.concatenate([face_landmarks])

    #cv2.fillPoly(img=mask ,pts=[points],color=(255,255,255))

    mask = np.zeros(image_size, dtype=np.uint8)
    points = cv2.convexHull(face_landmarks)  # 凸包
    cv2.fillConvexPoly(mask, points, color=255)
    return mask
def get_mask_union(mask1, mask2):
    """
    获取两个掩模掩盖部分的并集
    :param mask1: mask_image, 掩模1
    :param mask2: mask_image, 掩模2
    :return: 两个掩模掩盖部分的并集
    """
    mask = np.min([mask1, mask2], axis=0)  # 掩盖部分并集
    mask = ((cv2.blur(mask, (3, 3)) == 255) * 255).astype(np.uint8)  # 缩小掩模大小
    mask = cv2.blur(mask, (5, 5))#.astype(np.uint8)  # 模糊掩模
    return mask
def get_mask_center_point(image_mask):
    """
    获取掩模的中心点坐标
    :param image_mask: 掩模图片
    :return: 掩模中心
    """
    image_mask_index = np.argwhere(image_mask > 0)
    miny, minx = np.min(image_mask_index, axis=0)
    maxy, maxx = np.max(image_mask_index, axis=0)
    center_point = ((maxx + minx) // 2, (maxy + miny) // 2)
    return center_point

让我们分析每段代码

3.1计算两个点集之间的仿射变换矩阵

def transformation_from_points(points1, points2):
    '''0 - 先确定是float数据类型 '''
    points1 = points1.astype(numpy.float64)
    points2 = points2.astype(numpy.float64)

    '''1 - 消除平移的影响 '''
    c1 = numpy.mean(points1, axis=0)
    c2 = numpy.mean(points2, axis=0)
    points1 -= c1
    points2 -= c2

    '''2 - 消除缩放的影响 '''
    s1 = numpy.std(points1)
    s2 = numpy.std(points2)
    points1 /= s1
    points2 /= s2

    '''3 - 计算矩阵M=BA^T;对矩阵M进行SVD分解;计算得到R '''
    # ||RA-B||; M=BA^T
    A = points1.T # 2xN
    B = points2.T # 2xN
    M = np.dot(B, A.T)
    U, S, Vt = numpy.linalg.svd(M)
    R = np.dot(U, Vt)

    '''4 - 构建仿射变换矩阵 '''
    s = s2/s1
    sR = s*R
    c1 = c1.reshape(2,1)
    c2 = c2.reshape(2,1)
    T = c2 - np.dot(sR,c1) # 模板人脸的中心位置减去 需要对齐的中心位置(经过旋转和缩放之后)

    trans_mat = numpy.hstack([sR,T])   # 2x3

    return trans_mat

它通过以下步骤实现:

  • 数据类型转换:确保输入的点集points1points2float64类型,以进行精确的数学运算。
  • 消除平移影响:计算每个点集的均值(中心点),并将点集平移至原点。
  • 消除缩放影响:计算每个点集的标准差,并进行归一化处理,使得两个点集具有相同的尺度。
  • 计算仿射变换矩阵:通过奇异值分解(SVD)计算旋转矩阵R
  • 构建仿射变换矩阵:结合旋转、缩放和平移,构建最终的仿射变换矩阵。

3.2对图像image1进行仿射变换,使其与image2中的人脸特征点对齐:

def get_affine_image(image1, image2, face_landmarks1, face_landmarks2,M):
    """
    获取图片1仿射变换后的图片
    :param image1: 图片1, 要进行仿射变换的图片
    :param image2: 图片2, 只要用来获取图片大小,生成与之大小相同的仿射变换图片
    :param face_landmarks1: 图片1的人脸特征点
    :param face_landmarks2: 图片2的人脸特征点
    :return: 仿射变换后的图片
    """
    three_points_index = [18, 8, 25] #这些索引值用于从人脸特征点中选择三个关键点,这些点将用于计算仿射变换矩阵。
    M = M
    #M=cv2.getAffineTransform(face_landmarks1[three_points_index].astype(np.float32),face_landmarks2[three_points_index].astype(np.float32))
    dsize = (image2.shape[1], image2.shape[0]) #这个尺寸将用于指定仿射变换后图片的大小。
    affine_image = cv2.warpAffine(image1, M, dsize)#用OpenCV库中的warpAffine函数来对image1应用仿射变换。M是变换矩阵,dsize是输出图片的尺寸。
    return affine_image.astype(np.uint8)
  • 特征点索引:选择三个特定的特征点(18, 8, 25)用于计算仿射变换矩阵。
  • 仿射变换矩阵:使用OpenCV的getAffineTransform函数或直接使用传入的矩阵M来计算仿射变换矩阵。
  • 应用仿射变换:使用cv2.warpAffine函数将image1应用仿射变换,生成与image2大小相同的新图像。

3.3生成掩模

def get_face_mask(img,face_landmarks):
    """
    获取人脸掩模
    :param image_size: 图片大小
    :param face_landmarks: 68个特征点
    :return: image_mask, 掩模图片
    """
    img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
    image_size = (img.shape[0], img.shape[1])
    mask = np.zeros_like(img_gray) #这行代码创建一个与灰度图像img_gray大小相同、类型相同的零矩阵mask。这个矩阵将用作掩模的初始状态。
#    convexhull = cv2.convexHull(points)
#    cv2.polylines(img,[convexhull],True,(255,0,0), 3)
#    cv2.fillConvexPoly(mask,convexhull,255)
   # mask = np.zeros(image_size, dtype=np.uint8)
    #points = np.concatenate([face_landmarks[0:16], face_landmarks[26:17:-1]])
    #points = np.concatenate([face_landmarks])

    #cv2.fillPoly(img=mask ,pts=[points],color=(255,255,255))

    mask = np.zeros(image_size, dtype=np.uint8)
    points = cv2.convexHull(face_landmarks)  # 凸包 #这行代码计算人脸特征点的凸包。凸包是包含所有点的最小凸多边形,这里用于定义人脸的轮廓。
    cv2.fillConvexPoly(mask, points, color=255) #颜色设置为255(白色),这样人脸区域在掩模中被标记出来。
    return mask

这边有个核心函数convexHull,检测人脸凸包的函数,我们来看下

以下是 cv2.convexHull 函数的工作原理和特点:

  1. 输入和输出:该函数接受一个点集作为输入,并输出这些点的凸包。点集通常是一个二维数组,其中每个元素是一个点的坐标(x, y)。

  2. 算法实现cv2.convexHull 函数内部使用的是 Graham 扫描算法或者 Jarvis 步进(Gift wrapping)算法,这两种算法都是计算凸包的常用方法。Graham 扫描算法首先找到最低的点,然后通过旋转卡壳的方式找到凸包的边界点;而 Jarvis 步进算法则是从任一点开始,每次选择与当前点最远的点作为下一个边界点,直到回到起始点。

  3. 凸包检测cv2.convexHull 函数可以直接应用于图像轮廓,用于检测轮廓的凸包。例如,在图像处理流程中,首先通过 cv2.findContours 函数找到图像的轮廓,然后将每个轮廓传递给 cv2.convexHull 函数来获取凸包。

  4. 缺陷检测:除了计算凸包,cv2.convexHull 还可以与 cv2.convexityDefects 函数结合使用,来检测凸包的缺陷。凸包的缺陷是指在凸包内部的轮廓部分,这些部分通常是由于轮廓的凹陷造成的。

  5. 应用场景cv2.convexHull 函数在计算机视觉中有广泛的应用,比如在物体识别、形状分析和图像分割等领域。

3.3.1

在看检测凸包算法之前我们来看一个查找轮廓算法findContours

这里问gpt写了个简化版的伪代码,它没有考虑轮廓的层次结构、轮廓的结束条件、非极大值抑制、边缘跟踪的优化等。实际的 findContours 函数要复杂得多,并且使用了更高级的图像处理技术。这个伪代码只是为了说明轮廓检测的基本思想。

# 伪代码:简化版的轮廓检测算法

# 函数:简化轮廓检测
# 参数:
#   image - 灰度二值图像
# 返回值:
#   contours - 检测到的轮廓列表

function simple_findContours(image):
    contours = []
    edgePoints = []  # 用于存储边缘点

    # 遍历图像中的每个像素
    for y from 0 to image.height:
        for x from 0 to image.width:
            # 如果当前像素是前景(白色)
            if image.getPixel(x, y) == white:
                # 检查3x3邻域内的像素
                for dy from -1 to 1:
                    for dx from -1 to 1:
                        if (not (dy == 0 and dx == 0)) and image.getPixel(x + dx, y + dy) == black:
                            edgePoints.append((x, y))
                            break

    # 简化版的轮廓跟踪算法
    for point in edgePoints:
        if point not in contours:
            contour = trackContourFromPoint(image, point)
            contours.append(contour)

    return contours

# 函数:从给定点跟踪轮廓
# 参数:
#   image - 灰度二值图像
#   startPoint - 轮廓跟踪的起始点
# 返回值:
#   contour - 从起始点跟踪到的轮廓

function trackContourFromPoint(image, startPoint):
    contour = [startPoint]
    x, y = startPoint
    direction = North  # 初始方向,可以是 North, South, East, West

    while true:
        nextPoint = findNextPointInDirection(image, x, y, direction)
        if nextPoint is None:
            break
        contour.append(nextPoint)
        x, y = nextPoint
        direction = turnClockwise(direction)  # 顺时针旋转方向

    return contour

# 函数:在指定方向上找到下一个点
# 参数:
#   image - 灰度二值图像
#   x, y - 当前点的坐标
#   direction - 搜索方向
# 返回值:
#   nextPoint - 方向上的下一个点,如果没有则返回 None

function findNextPointInDirection(image, x, y, direction):
    # 根据方向定义的偏移量
    offset_x = 0
    offset_y = 0

    if direction == North:
        offset_y = -1
    elif direction == South:
        offset_y = 1
    elif direction == East:
        offset_x = 1
    elif direction == West:
        offset_x = -1

    # 检查邻域点是否是边缘点
    if image.getPixel(x + offset_x, y + offset_y) == white:
        return (x + offset_x, y + offset_y)

    return None

# 函数:顺时针旋转方向
# 参数:
#   direction - 当前方向
# 返回值:
#   newDirection - 顺时针旋转后的方向

function turnClockwise(direction):
    if direction == North:
        return East
    elif direction == East:
        return South
    elif direction == South:
        return West
    elif direction == West:
        return North

让我们分析每段代码

3.3.2二值图像中检测轮廓。

function simple_findContours(image):
    contours = []
    edgePoints = []  # 用于存储边缘点

    # 遍历图像中的每个像素
    for y from 0 to image.height:
        for x from 0 to image.width:
            # 如果当前像素是前景(白色)
            if image.getPixel(x, y) == white:
                # 检查3x3邻域内的像素
                for dy from -1 to 1:
                    for dx from -1 to 1:
                        if (not (dy == 0 and dx == 0)) and image.getPixel(x + dx, y + dy) == black:
                            edgePoints.append((x, y))
                            break

这个函数是从输入的二值图中检测轮廓,

  • contours 列表用于存储检测到的轮廓,每个轮廓是一个点的列表。
  • edgePoints 列表用于存储边缘点,即前景(白色)像素且其周围有背景(黑色)像素的点。
  • 函数遍历图像的每个像素,检查每个像素是否为前景(白色)。
  • 对于每个前景像素,函数检查其3x3邻域内的像素。如果邻域内有任何背景(黑色)像素,当前像素被认为是边缘点,并被添加到 edgePoints 列表中。

3.3.3从给定点跟踪轮廓 

function trackContourFromPoint(image, startPoint):
    contour = [startPoint]
    x, y = startPoint
    direction = North  # 初始方向,可以是 North, South, East, West

    while true:
        nextPoint = findNextPointInDirection(image, x, y, direction)
        if nextPoint is None:
            break
        contour.append(nextPoint)
        x, y = nextPoint
        direction = turnClockwise(direction)  # 顺时针旋转方向

    return contour
  • 这个函数从给定的起始点开始跟踪轮廓。
  • contour 列表用于存储当前跟踪的轮廓。
  • 函数初始化当前点 (x, y) 为起始点,并设置初始搜索方向为北(North)。
  • 函数进入一个无限循环,不断在当前方向上寻找下一个点。
  • 如果在当前方向上找到下一个点,将其添加到轮廓中,并更新当前点和方向。
  • 如果在当前方向上没有找到下一个点,循环结束,返回当前跟踪的轮廓。

3.3.4在指定方向上找到下一个点

function findNextPointInDirection(image, x, y, direction):
    # 根据方向定义的偏移量
    offset_x = 0
    offset_y = 0

    if direction == North:
        offset_y = -1
    elif direction == South:
        offset_y = 1
    elif direction == East:
        offset_x = 1
    elif direction == West:
        offset_x = -1

    # 检查邻域点是否是边缘点
    if image.getPixel(x + offset_x, y + offset_y) == white:
        return (x + offset_x, y + offset_y)

    return None
  • 这个函数在指定方向上寻找轮廓的下一个点。
  • 根据方向,函数计算偏移量 (offset_x, offset_y)
  • 函数检查当前点加上偏移量后的位置是否为前景(白色)像素。
  • 如果是,返回该点作为下一个点;如果不是,返回 None

3.3.5顺时针旋转方向

function turnClockwise(direction):
    if direction == North:
        return East
    elif direction == East:
        return South
    elif direction == South:
        return West
    elif direction == West:
        return North

3.4 我们再回到检测人脸凸包函数cv2.convexHull 里来

看下它的简化源代码

这个算法的基本思想是:

  1. 排序:首先按照 x 坐标对所有点进行排序。
  2. 构建上凸包:从左到右遍历点集,使用栈来构建上凸包。
  3. 构建下凸包:从右到左遍历点集,使用栈来构建下凸包。
  4. 合并:将上凸包和下凸包合并,移除重复的点,得到最终的凸包。

什么是上凸包?

上凸包是由给定点集中的点构成的凸包的上半部分。

为什么要构建上凸包?

  1. 简化问题:通过将问题分解为构建上凸包和下凸包两个子问题,可以简化凸包的计算过程。

  2. 利用单调性:在 Andrew's monotone chain 算法中,上凸包和下凸包的边缘是单调的,即它们的斜率是单调递增或递减的。这个性质可以用来快速判断新点是否应该被添加到凸包中。

  3. 避免重复计算:通过先构建上凸包和下凸包,然后再将它们合并,可以避免对每个点都进行全集中点的比较,从而提高效率。

  4. 处理边界情况:在构建凸包的过程中,可能会遇到一些边界情况,如点在一条直线上或者凸包是一条线段。通过分别构建上凸包和下凸包,可以更容易地处理这些情况。

构建上凸包的步骤:

  1. 排序:首先按照 x 坐标对所有点进行排序。

  2. 初始化栈:使用一个栈(或列表)来维护当前的上凸包的边缘。

  3. 遍历点集:从左到右遍历排序后的点集。

  4. 维护栈:对于每个新点,如果它使得栈中的点形成凹角,则弹出栈顶的点,直到新点可以被添加到栈中而不形成凹角。

  5. 构建上凸包:重复上述步骤,直到所有点都被处理,栈中剩余的点就构成了上凸包的边缘。

import numpy as np
import cv2

def andrew_convex_hull(points):
    """
    计算凸包的简化版源代码(基于 Andrew's monotone chain 算法)。

    参数:
    points -- 一个二维点集,形状为 (n, 2),其中 n 是点的数量。

    返回:
    hull -- 凸包的点集。
    """
    if len(points) <= 3:
        return points

    # 按照 x 坐标对点进行排序
    points = sorted(points, key=lambda x: (x[0], x[1]))

    # 构建上凸包
    upper = []
    for p in points:
        while len(upper) >= 2 and not counter_clockwise(upper[-2], upper[-1], p):
        #counter_clockwise:逆时针顺序
            upper.pop()
        upper.append(p)

    # 构建下凸包
    lower = []
    for p in reversed(points): #这个循环遍历原始点集 points,但是是反向遍历。这意味着我们从点集中的最高点开始,向最低点遍历。
        while len(lower) >= 2 and not counter_clockwise(lower[-2], lower[-1], p):
            lower.pop()
        lower.append(p)

    # 移除上凸包和下凸包之间的重复点
    hull = upper[:-1] + lower[:-1]
    return np.array(hull)

def counter_clockwise(p1, p2, p3):
    """
    判断三个点是否为逆时针方向。

    参数:
    p1, p2, p3 -- 三个点的坐标。

    返回:
    True 如果 p1, p2, p3 为逆时针方向,否则 False。
    """
    return (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p2[1] - p1[1]) * (p3[0] - p1[0]) > 0

# 示例使用
if __name__ == "__main__":
    # 创建一个简单的点集
    points = np.array([[0, 0], [1, 1], [2, 0], [0, 2], [1, 3]], dtype=np.float32)

    # 计算凸包
    hull = andrew_convex_hull(points)

    # 打印凸包的点集
    print("凸包的点集:")
    print(hull)

在人脸识别中,凸包的应用主要体现在以下几个方面:

  1. 特征提取与粗分类

    • 凸包几何特征可以用于快速提取大量人脸数据集的外轮廓特征信息,并进行快速的人脸粗分类。这种方法通过计算人脸特征点的凸包,可以快速识别出人脸的大致轮廓,为后续的精细识别提供初步的分类。
  2. 优化算法性能

    • 在人脸识别中,凸包算法可以提高算法的分类性能。例如,基于稀疏恢复的L1范数凸包分类器在人脸识别中的应用,通过将原始训练数据集进行低秩恢复,利用恢复出的低秩矩阵和误差矩阵构成新训练集字典建立各类训练样本凸包模型,并在范数意义下,计算观测样本与各类凸包模型差值,用所得差值等价观测样本到各类样本凸包的距离,将距离最小的一类视为判别输出类。这种方法可以提高识别效率。
  3. 鲁棒性问题

    • 尽管凸包几何特征在人脸识别中有应用,但在受到光照、遮挡及姿态变化影响时不能很好地保证识别的鲁棒性。因此,凸包算法通常与其他算法结合使用,以提高人脸识别的准确性和鲁棒性。
  4. 人脸融合技术

    • 在人脸融合技术中,凸包算法用于查找人脸特征点的凸包,这是人脸交换过程中的一个重要步骤。通过基于凸包的三角剖分和仿射变换,可以实现人脸特征点的精确对齐,进而进行图像融合。
  5. 分类器设计

    • 在分类器设计中,最近邻凸包分类器(NNCH)将各类训练样本的凸包作为各类样本分布的粗略估计,把测试点到各类凸包的距离作为相似性度量依据,按最近邻原则分类。这种方法在人脸识别中可以作为一种有效的分类手段。

3.5 计算掩膜

def get_mask_union(mask1, mask2):
    """
    获取两个掩模掩盖部分的并集
    :param mask1: mask_image, 掩模1
    :param mask2: mask_image, 掩模2
    :return: 两个掩模掩盖部分的并集
    """
    mask = np.min([mask1, mask2], axis=0)  # 掩盖部分并集
    mask = ((cv2.blur(mask, (3, 3)) == 255) * 255).astype(np.uint8)  # 缩小掩模大小
    mask = cv2.blur(mask, (5, 5))#.astype(np.uint8)  # 模糊掩模
    return mask
  • 这个函数计算两个掩模的并集,即两个掩模都有的部分。
  • 它通过取两个掩模的逐元素最小值来实现,并使用高斯模糊来平滑掩模的边缘。

3.6找到掩模的中心点坐标

def get_mask_center_point(image_mask):
    """
    获取掩模的中心点坐标
    :param image_mask: 掩模图片
    :return: 掩模中心
    """
    image_mask_index = np.argwhere(image_mask > 0)
    miny, minx = np.min(image_mask_index, axis=0)
    maxy, maxx = np.max(image_mask_index, axis=0)
    center_point = ((maxx + minx) // 2, (maxy + miny) // 2)
    return center_point

  • 这个函数找到掩模的中心点坐标。
  • 它通过找到掩模中非零元素的最小和最大坐标,然后计算它们的平均值来确定中心点。

4 处理图像,并从中提取人脸特征点以及生成人脸掩模。

img1=cv2.imread("huge.jpg")#原图像

points1=ladmask(img1) #检测图像中的人脸特征点。

img1_mask=get_face_mask(img1,points1)#生成一个掩模图像

landmarks1=ladmask(img1) #获取人脸特征点

5.完整的人脸识别,图像融合流程

import cv2 as cv
import numpy
"""
读取我们的摄像头的输入以及,读取视频的输入,展示出来

"""

#capture = cv.VideoCapture('./Resources/Videos/kitten.mp4')
capture = cv.VideoCapture(0)
#VideoCapture 是获取视频源当参数为0表示从摄像头获取输入,当参数为一个视频地址表示播放该文件
while True:
    isTrue,frame=capture.read()
    
    if isTrue:
        
       # img2=cv2.imread("222.jpg")#目标图像
        points2=ladmask(frame)
        img2_mask=get_face_mask(frame,points2)
        landmarks2=ladmask(frame)
        M = transformation_from_points(landmarks1,landmarks2)
        #应用仿射变换
        affine_im1 = get_affine_image(img1, frame, landmarks1, landmarks2,M)
        affine_im1_mask = get_affine_image(img1_mask, frame, landmarks1, landmarks2,M)
        union_mask = get_mask_union(img2_mask, affine_im1_mask)  # 掩模合并
        point = get_mask_center_point(union_mask)  # im1(脸图)仿射变换后的图片的人脸掩模的中心点 cv2.MIXED_CLONE cv2.MONOCHROME_TRANSFER
        seamless_im = cv2.seamlessClone(np.uint8(affine_im1), frame, mask=union_mask, p=point, flags=cv2.NORMAL_CLONE)  # 进行泊松融合
        #gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
        #adaptive_thresh = cv.adaptiveThreshold(gray, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY_INV, 11, 9)
        cv.imshow("vedio",seamless_im)
        if cv.waitKey(20) & 0xFF==ord('d'):  
            
            break
    else:
        break
capture.release()
cv.destroyAllWindows()

6.泊松融合

泊松融合可以看另一文章

24/11/11 算法笔记 泊松融合-CSDN博客

输出结果

标签:11,24,return,image,mask,凸包,人脸,face,换脸
From: https://blog.csdn.net/yyyy2711/article/details/143689571

相关文章

  • ICPC2024 杭州
    省流:麻。Day\(-\infin\)今年一共分到两个ICPC名额。第一场ICPC,好耶。有两个神仙队友和一个采集,分别记为\(I,J,K\)。其中\(I\)是队长。期望不高,有个牌子就行。Day-1周五下午出发。出发前有一节体育课。体育老师:这个周日中午的体测啊,你们就.....我:?????(事后发现更......
  • 2024/11/11日工作总结
    完成数据结构pta实验题:6-3链表逆置:本题要求实现一个函数,将给定单向链表逆置,即表头置为表尾,表尾置为表头。链表结点定义如下:structListNode{intdata;structListNode*next;};函数接口定义:structListNode*reverse(structListNode*head);其中head是用户传入的链......
  • 题解:P11262 [COTS 2018] 题日 Zapatak
    https://www.luogu.com.cn/article/i7ajvm8e哈希好题。题意给定一个序列,每次询问给定两个长度相等的区间,问这两个区间是否只有一个数不一样。思路发现我们要求的信息只与数的出现次数有关,自然想到桶。那么如果有两个区间合法,那这两个区间的桶只有两个位置不同且桶内的值均相......
  • CSP-S 2024 游记
    前情提要:初赛\(54.5\),比去年还低\(2.5\),但是过了。考点在七中高新,在红杏酒家吃的午饭,在旁边酒店小睡了一会儿。进考场,机子还是一如既往地牛,但感觉键盘有点难用,是放在抽屉里的,但我强行拉到了桌子上。起初一直打不开代码回收系统,过后不知道怎么回事就打开了。看T1,一眼没读懂,......
  • 『模拟赛』NOIP2024加赛4
    Rank给我唐完了,又名,【MX-S5】梦熊NOIP2024模拟赛1。A.王国边缘好像简单倍增就做完了。由于昨天T5在我脑海中留下了挥之不去的印象,今天一上来看到这题就发现是一个内向基环树森林。然后被硬控硬控硬控,最后一个小点加一点优化就能过没调出来,挂30pts,菜菜菜菜菜。注......
  • 20241111
    HappyBirthdayElysia!T1很有味道的题目\(dp_i\)表示到\(a\)的第\(i\)个数,最多能到\(b\)的哪一个数。向后转移能够给到的是一个值域的后缀,离散化后BIT维护即可。代码#include<iostream>#include<algorithm>#definelowbit(x)((x)&(-(x)))#defineintl......
  • EEEE4116 Design for a 2-Level Inverter
    AdvancedControl(EEEE4116)Coursework1ModellingandAdvancedControllerDesignfora2-LevelGrid-FeedingInverterInthisassignmentyouwillbringtogetheryourskillsofstate-spaceequationdevelopmentandcontrollerdesigntocontrolagrid-tied......
  • C#/.NET/.NET Core技术前沿周刊 | 第 12 期(2024年11.01-11.10)
    前言C#/.NET/.NETCore技术前沿周刊,你的每周技术指南针!记录、追踪C#/.NET/.NETCore领域、生态的每周最新、最实用、最有价值的技术文章、社区动态、优质项目和学习资源等。让你时刻站在技术前沿,助力技术成长与视野拓宽。欢迎投稿、推荐或自荐优质文章、项目、学习资源等。每......
  • 杀怪物(NHOI2011pj4)
    题目为了庆祝自己的生日,小张推出一款游戏。游戏在一个20*20的方格上进行,上面有一些怪物,用#表示,其他是空格,用.表示。怪物有两点体力。体力为0时死亡。你可以进行以下操作:(1)使一个横行上的怪物体力减一(2)使一个竖行上的怪物体力减一对每个横行或竖行只能操作一次,限定n次,问最......
  • 8.100ASK_T113-PRO 应用程序驱动LED灯 (/sys/class/gpio)
    前言1.利用LINUX内核的GPIO子统驱动LED灯.2. 编写应用程序控制LED灯的亮灭.3.不用写驱动程序,只写应用程序.1.原理图使用的是PE12这个IO口,计算一个IO编号: PE=4*32, IO编号=4*32+12=140.注解一下:PAX= 0*32+XPBX= 1*32+XPCX= 2*32+XPD......