6. 证件照换背景

K-均值聚类算法是著名的划分聚类分割方法。划分方法的基本思想是:给定一个有N个元组或者纪录的数据集,分裂法将构造K个分组,每一个分组就代表一个聚类,K<N。而且这K个分组满足下列条件:(1) 每一个分组至少包含一个数据纪录;(2)每一个数据纪录属于且仅属于一个分组;对于给定的K,算法首先给出一个初始的分组方法,以后通过反复迭代的方法改变分组,使得每一次改进之后的分组方案都较前一次好,而所谓好的标准就是:同一分组中的记录越近越好,而不同分组中的纪录越远越好。

K-means算法的工作原理:算法首先随机从数据集中选取 K个点作为初始聚类中心,然后计算各个样本到聚类中心的距离,把样本归到离它最近的那个聚类中心所在的类。计算新形成的每一个聚类的数据对象的平均值来得到新的聚类中心,如果相邻两次的聚类中心没有任何变化,说明样本调整结束,聚类准则函数 已经收敛。本算法的一个特点是在每次迭代中都要考察每个样本的分类是否正确。若不正确,就要调整,在全部样本调整完后,再修改聚类中心,进入下一次迭代。这个过程将不断重复直到满足某个终止条件,终止条件可以是以下任何一个:

(1)没有对象被重新分配给不同的聚类。

(2)聚类中心不再发生变化。

(3)误差平方和局部最小。

K-means聚类算法的一般步骤:

(1)从 n个数据对象任意选择 k 个对象作为初始聚类中心;

(2)循环(3)到(4)直到每个聚类不再发生变化为止;

(3)根据每个聚类对象的均值(中心对象),计算每个对象与这些中心对象的距离;并根据最小距离重新对相应对象进行划分;

(4)重新计算每个(有变化)聚类的均值(中心对象),直到聚类中心不再变化。这种划分使得下式最小

K-均值聚类法的缺点:

(1)在 K-means 算法中 K 是事先给定的,这个 K 值的选定是非常难以估计的。

(2)在 K-means 算法中,首先需要根据初始聚类中心来确定一个初始划分,然后对初始划分进行优化。

(3) K-means算法需要不断地进行样本分类调整不断地计算调整后的新的聚类中心因此当数据量非常大时算法的时间开销是非常大的。

(4)K-means算法对一些离散点和初始k值敏感,不同的距离初始值对同样的数据样本可能得到不同的结果。

kmeans动态演示:https://www.naftaliharris.com/blog/visualizing-k-means-clustering/

kmeans动态演示:http://shabal.in/visuals/kmeans/4.html

示例代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

Scalar colors[]={
        Scalar(255,0,0),
        Scalar(255,255,0),
        Scalar(0,0,255),
        Scalar(255,0,255),
        Scalar(0,255,0),

};

void useKmeans(Mat &src, Mat &distImg);
void changeBackground(Mat&src ,Mat &img,Vec3b color);
int main(int argc, char** argv){
    //string path = "/home/kaijun/Documents/resource/opencv/zhengjianzhao.png";
    //string path = "/home/kaijun/Pictures/zjz.jpeg";
    string path = "/home/kaijun/Pictures/zjz3.jpg";
    // 读取原图
    Mat src = imread(path);
    // 显示原图像
    imshow("src",src);

    // 创建一张空白的图片, 用于接收kmeans聚类结果
    Mat distImg(src.size(),src.type());
    // 使用聚类算法 对图像进行处理
    useKmeans(src, distImg);

    changeBackground(src,distImg,Vec3b(125,125,120));

    waitKey(0);

    return 0;
}

/**
 * 更换背景
 * @param img
 * @param result
 */
void changeBackground(Mat &src, Mat &img,Vec3b color){
    // 1. 提取到背景颜色值
    Vec3b backgroundColor = img.at<Vec3b>(0,0);

    // 2. 遍历背景,创建一个蒙版
    Mat mask(img.size(),CV_8UC1);
    for (int row = 0; row < img.rows; ++row) {
        for (int col = 0; col < img.cols; ++col) {
            Vec3b currentColor = img.at<Vec3b>(row,col);
            if(currentColor == backgroundColor){
                mask.at<uchar>(row,col) = 0;
            }else{
                mask.at<uchar>(row,col) = 255;
            }
        }
    }

    // 3. 形态学变换,去掉一些细小的边缘
    erode(mask,mask,Mat::ones(3,3,CV_8UC1),Point(-1,-1),3);

    // 创建一个蒙版
    imshow("mask",mask);

    // 5. 从原图中抠出人像
    Mat copyImg;
    src.copyTo(copyImg,mask);

    imshow("copy",copyImg);
    // 6. 遍历抠出来的人像
    for (int row = 0; row < copyImg.rows; ++row) {
        for (int col = 0; col < copyImg.cols; ++col) {
            int value = mask.at<uchar>(row,col);
            if(value == 0){
                copyImg.at<Vec3b>(row,col)=Vec3b(125,125,125);
            }
        }
    }
    imshow("result",copyImg);
}


void useKmeans(Mat &src, Mat &distImg) {// 封装数据点
    int sampleCount = src.rows*src.cols;
    // N 行 3列数据
    Mat sampleData(sampleCount,src.channels(),CV_32F);
    // 将图片的像素数据封装到样本数据中
    for(int row = 0; row< src.rows;row++){
        for(int col=0; col<src.cols;col++){
            int index = row*src.cols + col;
            // 从原图从取出颜色信息
            Vec3b bgr = src.at<Vec3b>(row,col);
            // 将数据填到数据列表中
            sampleData.at<Vec3f>(index)=bgr;
        }
    }

    // 调用kmeans函数
    int clusterNum = 4;
    Mat labels;
    TermCriteria termCriteria(TermCriteria::EPS|TermCriteria::COUNT,10,0.1);
    Mat centers;
    kmeans(sampleData,clusterNum,labels,termCriteria,3,KMEANS_PP_CENTERS,centers);

    //取出每一个样本数据,根据它的标签 填入颜色
    for (int row = 0; row < src.rows; ++row) {
        for (int col = 0; col < src.cols; ++col) {
            int index = row*src.cols + col;

            int label = labels.at<int>(index,0);

            distImg.at<Vec3b>(row,col)[0] = colors[label][0];
            distImg.at<Vec3b>(row,col)[1] = colors[label][1];
            distImg.at<Vec3b>(row,col)[2] = colors[label][2];
        }
    }

    imshow("kmeans",distImg);
}