0


OpenCV数字图像处理基于C++:图像分割

OpenCV数字图像处理基于C++:图像分割

1、基于阈值的分割

图像阈值化分割是一种常用的、传统的图像分割技术,因其实现简单、计算量小、性能比较稳定而成为图像分割中基本和应用广泛的分割技术。特别适合于目标和背景占据不同灰度级范围的图像。不仅可以极大地压缩数据量,而且大大简化了分析和处理的步骤,是进行图像分析、特征提取与模式识别之前所必要的图像预处理步骤。

图像阈值化的目的是要按照灰度级,对像素集合进行一个划分,得到的每个子集形成一个与现实景物相对应的区域,各个区域内部具有一致的属性,而相邻区域不具有这种一致属性。这样的划分可以通过从灰度级出发选取一个或多个阈值来实现。
基本原理:通过设定不同的特征阈值,把图像象素点分为若干类。

1.1 固定阈值分割

给定一个全局的固定的阈值,整张图片的每个像素的像素值都与该值进行比较,若小于该阈值则将像素值改为一个固定的值(常用0),若大于该阈值则将像素值改为另一个固定的值(常用255),则可以将图像进行二值分割,得到一张二值图。

1.1.1 手工实现固定阈值分割

Mat mythreshold(Mat src, double T) {
    Mat dst(src.size(),src.type());
    //获取原图像行列信息
    int nRows = src.rows;
    int nCols = src.cols;

    for (int i = 0; i < nRows; i++) {
        for (int j = 0; j < nCols; j++) {
            //二值化,小于阈值赋0,大于阈值赋255
            if (src.at<uchar>(i, j) < T) {
                dst.at<uchar>(i, j) = 0;
            }
            else {
                dst.at<uchar>(i, j) = 255;
            }
        }
    }
    return dst;
}

int main()
{
    //------------【1】读取源图像并检查图像是否读取成功------------    
    Mat srcImage = imread("E:\\4.jpg");
    if (!srcImage.data)
    {
        puts("读取图片错误,请重新输入正确路径!");
        system("pause");
        return -1;
    }
    imshow("【源图像】", srcImage);
    //------------【2】灰度转换------------    
    Mat srcGray;
    cvtColor(srcImage, srcGray, CV_RGB2GRAY);
    imshow("【灰度图】", srcGray);
    //------------【3】固定阈值分割---------------  
    //1 在这里使用图像的平均值作为阈值T
    Scalar T = mean(srcGray);
    Mat dst;
    dst = mythreshold(srcGray,T[0]);
    imshow("【固定阈值分割图】", dst);
    waitKey(0);
    return 0;
}

image-20221010092646735

1.1.2 函数实现固定阈值分割

threshold(srcGray, dst, T[0], 255,0);
imshow("【函数固定阈值分割图】", dst);

image-20221010093312418

cv::threshold( 
    InputArray src, 
    OutputArray dst, 
    double thresh, 
    double maxval,
    type 
);
//参数1:输入的灰度图像
//参数2:输出图像
//参数3:进行阈值操作时阈值的大小
//参数4:设定的最大灰度值(该参数运用在二进制与反二进制阈值操作中)
//参数5:阈值的类型。从下面提到的5种中选择出的结果
cv::THRESH_BINARY=0: 二进制阈值
cv::THRESH_BINARY_INV=1: 反二进制阈值
cv::THRESH_TRUNC=2: 截断阈值
cv::THRESH_TOZERO=3: 0阈值
cv::THRESH_TOZERO_INV=4: 反0阈值
cv::THRESH_OTSU=8   自适应閾值
 

(1)正向二值化,THRESH_BINARY

正向二值化,如果当前的像素值大于设置的阈值(thresh),则将该点的像素值设置为maxval;否则,将该点的像素值设置为0;

image-20221010094856722

(2)反向二值化,THRESH_BINARY_INV

反向二值化,如果当前的像素值大于设置的阈值(thresh),则将该点的像素值设置为0;否则,将该点的像素值设置为maxval

image-20221010094923816

(3)THRESH_TRUNC

如果当前的像素值大于设置的阈值(thresh),则将该点的像素值设置为threshold;否则,将该点的像素值不变

image-20221010094956622

(4)THRESH_TOZERO

如果当前的像素值大于设置的阈值(thresh),则将该点的像素值不变;否则,将该点的像素值设置为0

img

THRESH_TOZERO_INV

如果当前的像素值大于设置的阈值(thresh),则将该点的像素值设置为0;否则,将该点的像素值不变

img

1.2 自适应阈值分割

在不均匀照明或者灰度值分布不均的情况下,如果使用全局阈值分割,那么得到的分割效果往往会很不理想,这个时候就要用到自适应阈值算法了。

1.2.1 手工实现自适应阈值分割

enum adaptiveMethod { meanFilter, gaaussianFilter, medianFilter };

void myAdaptiveThreshold(Mat& src, Mat& dst, double Maxval, int Subsize, double c, adaptiveMethod method = meanFilter) {

    if (src.channels() > 1)
        cvtColor(src, src, CV_RGB2GRAY);

    Mat smooth;
    switch (method)
    {
    case  meanFilter:
        blur(src, smooth, Size(Subsize, Subsize));  //均值滤波
        break;
    case gaaussianFilter:
        GaussianBlur(src, smooth, Size(Subsize, Subsize), 0, 0); //高斯滤波
        break;
    case medianFilter:
        medianBlur(src, smooth, Subsize);   //中值滤波
        break;
    default:
        break;
    }

    smooth = smooth - c;

    //阈值处理
    src.copyTo(dst);
    for (int r = 0; r < src.rows; ++r) {
        const uchar* srcptr = src.ptr<uchar>(r);
        const uchar* smoothptr = smooth.ptr<uchar>(r);
        uchar* dstptr = dst.ptr<uchar>(r);
        for (int c = 0; c < src.cols; ++c) {
            if (srcptr[c] > smoothptr[c]) {
                dstptr[c] = Maxval;
            }
            else
                dstptr[c] = 0;
        }
    }

}

int main() {
    Mat src = imread("E:\\test.jpg");
    if (src.empty()) {
        return -1;
    }
    if (src.channels() > 1)
        cvtColor(src, src, CV_RGB2GRAY);

    Mat dst, dst2;
    double t2 = (double)getTickCount();
    myAdaptiveThreshold(src, dst, 255, 21, 10, meanFilter);  //
    t2 = (double)getTickCount() - t2;
    double time2 = (t2 * 1000.) / ((double)getTickFrequency());
    std::cout << "my_process=" << time2 << " ms. " << std::endl << std::endl;

    adaptiveThreshold(src, dst2, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 21, 10);

    
    imshow("原图", src);
    imshow("手工自适应", dst);
    imshow("函数自适应", dst2);
    waitKey(0);
}

image-20221010100946963

1.2.2 函数实现自适应阈值分割

int main()
{
    //------------【1】读取源图像并检查图像是否读取成功------------    
    Mat srcImage = imread("E:\\4.jpg");
    if (!srcImage.data)
    {
        puts("读取图片错误,请重新输入正确路径!");
        system("pause");
        return -1;
    }
    imshow("【源图像】", srcImage);
    //------------【2】灰度转换------------    
    Mat srcGray;
    cvtColor(srcImage, srcGray, CV_RGB2GRAY);
    imshow("【灰度图】", srcGray);
    //------------【3】初始化相关变量---------------  
    Mat dstImage;        //初始化自适应阈值参数
    const int maxVal = 255;
    int blockSize = 3;    //取值3、5、7....等
    int constValue = 10;
    int adaptiveMethod = 0;
    int thresholdType = 1;
    //---------------【4】图像自适应阈值操作-------------------------
    adaptiveThreshold(srcGray, dstImage, maxVal, adaptiveMethod, thresholdType, blockSize, constValue);
    
    imshow("【自适应阈值】", dstImage);
    waitKey(0);
    return 0;
}

image-20221010094037721

void adaptiveThreshold(
    InputArray src,
    OutputArray dst,
    double maxValue,
    int adaptiveMethod,
    int thresholdType,
    int blockSize,
    double C
);
第一个参数,InputArray src,原图,即输入图像,是一个8位单通道的图像;
第二个参数,OutputArray dst,目标图像,与原图像具有同样的尺寸与类型;
第三个参数,double maxValue,分配给满足条件的像素的非零值;
第四个参数,int adaptiveMethod,自适应阈值的方法,通常有以下几种方法;
    ADAPTIVE_THRESH_MEAN_C,阈值T(x,y)是(x,y)减去C的Blocksize×Blocksize邻域的平均值。
    ADAPTIVE_THRESH_GAUSSIAN_C ,阈值T(x,y)是(x,y)减去C的Blocksize×Blocksize邻域的加权和(与高斯相关),默认sigma(标准差)用于指定的Blocksize;具体的情况可以参见getGaussianKernel函数;
第五个参数,int thresholdType,阈值的类型必须是以下两种类型,
    THRESH_BINARY,正向二值化
    THRESH_BINARY_INV ,反向二值化
第六个参数,int blockSize,计算blocksize x blocksize大小的领域内的阈值,必须为奇数,例如,3,5,7等等,一般二值化使用21,31,41;
第七个参数,double C,从平均数或加权平均数减去常量。通常,它是正的,但也可能是零或负数。,二值化时使用的7。
补充
函数cvAdaptiveThreshold的确可以将灰度图像二值化,但它的主要功能应该是边缘提取,关键是里面的block_size参数,该参数是决定局部阈值的block的大小
1)当block很小时,如block_size=3 or 5 or 7时,“自适应”的程度很高,即容易出现block里面的像素值都差不多,这样便无法二值化,而只能在边缘等梯度大的地方实现二值化,结果显得它是边缘提取函数;
2)当把block_size设为比较大的值时,如block_size=21 or 31 or 41时,cvAdaptiveThreshold便是二值化函数了;
3)src与dst 这两个都要是单通道的图像。

1.3 对比固定阈值和自适应阈值分割

int main()
{
    Mat img = imread("E:\\test.jpg");
    Mat dst1;
    Mat dst2;
    Mat dst3;
    cv::cvtColor(img, img, COLOR_RGB2GRAY);//进行,灰度处理
    medianBlur(img, img, 5);//中值滤波
    threshold(img, dst1, 127, 255, THRESH_BINARY);//阈值分割
    adaptiveThreshold(img, dst2, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 11, 2);//自动阈值分割,邻域均值
    adaptiveThreshold(img, dst3, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 11, 2);//自动阈值分割,高斯邻域
    imshow("dst1", dst1);
    imshow("dst2", dst2);
    imshow("dst3", dst3);
    imshow("img", img);
    waitKey(0);
}

image-20221010100039879

1.4 迭代法阈值分割

(1)求出图像的最大灰度值和最小灰度值,分别记为Zmax和Zmin,另初始阈值为T0 = (Zmax + Zmin) / 2。
(2)根据阈值Tk将图像分割为前景和背景,分别求出两者的平均灰度值Zo和Zb。
(3)求出新的阈值Tk+1 = (Zo + Zb) / 2。
(4)若Tk == Tk+1,则即为所求的阈值,否则转到步骤2继续迭代。
(5)使用计算后的阈值进行阈值分割。
其实迭代法就是将固定阈值分割里手动给定阈值改为了迭代计算阈值,可以适用的范围更多一些,但是本质还是固定阈值变换。
//声明全局阈值分割函数,输入图像,输出图像,T0,初始阈值T
void global_threshold_segmentation(Mat& input_image, Mat& output_image, int T0, int T);

int main()
{
    Mat image, image_gray, image_bw;
    image = imread("Cameraman.bmp");
    if (image.empty())
    {
        cout << "读取图像出错" << endl;
        return -1;
    }

    cvtColor(image, image_gray, 6);
    namedWindow("image_gray", 0);
    imshow("image_gray", image_gray);

    //1 在这里使用图像的平均值作为初始值T, T0=5
    Scalar image_meam = cv::mean(image_gray); //使用opencv的mean函数求平均值
    int T = (int)image_meam[0];//图像的平均值作为初始值T
    global_threshold_segmentation(image_gray, image_bw, 5, T);
    namedWindow("image_bw", 0);
    imshow("image_bw", image_bw);

    //2 使用任意值作为初始阈值
    Mat image_bw2;
    int T1 = 5; //任意值
    global_threshold_segmentation(image_gray, image_bw2, 5, T1);
    namedWindow("image_bw2", 0);
    imshow("image_bw2", image_bw2);

    waitKey();

    return 0;
}

//定义全局阈值分割函数
void global_threshold_segmentation(Mat& input_image, Mat& output_image, int T0, int T)
{
    //使用初始值T进行分组并求每组的平均值m1和m2,并计算新的阈值T2
    int width = input_image.cols; //图像列数
    int height = input_image.rows; //图像行数
    uchar* Img = input_image.data; //图像指针
    int G1_mean, G2_mean; //定义每组像素的均值
    int G1_num = 1, G2_num = 1; //定义每组像素的数量,初始值设为1,以免后面出现除以0的问题
    int G1_sum = 0, G2_sum = 0; //定义每组灰度值之和

    for (int i = 0; i < height; i++)
    {
        uchar* Img = input_image.ptr(i); //图像每行数据的指针
        for (int j = 0; j < width; j++)
        {
            if (Img[j] < T)
            {
                G1_sum += Img[j];
                G1_num += 1;
            }
            else
            {
                G2_sum += Img[j];
                G2_num += 1;
            }
        }
    }
    G1_mean = G1_sum / G1_num;
    G2_mean = G2_sum / G2_num;
    int T2 = (G1_mean + G2_mean) * 0.5; //新阈值

    //迭代计算T
    if (abs(T2 - T) > T0)
    {
        global_threshold_segmentation(input_image, output_image, T0, T2);
    }
    else
    {
        threshold(input_image, output_image, T2, 255, 1);
    }

}

image-20221010101840081

2、基于边缘分割

基于边缘的分割代表了一大类基于图像边缘信息的方法。基于边缘的分割依赖于由边缘检测算子找到的图像边缘,这些边缘表示除了图像在灰度、彩色、纹理等方面不连续的位置。在分割处理中可获得的先验信息越多,能达到的分割效果越好。常见的边缘检测方法有Roberts算子,Laplance算子,Sobel算子,LoG算子以及Canny算子等。可参考:(53条消息) OpenCV数字图像处理基于C++:边缘检测_qq_43784519的博客-CSDN博客和(53条消息) OpenCV数字图像处理基于C++:Canny边缘检测_qq_43784519的博客-CSDN博客

2.1 轮廓函数

轮廓函数

OpenCV 中,可在图像的边缘检测之后,使用 findContours() 寻找到轮廓,该函数参数如下: image 一般为二值化图像,可由 compare, inRange, threshold , adaptiveThreshold, Canny 等函数获得。

Mat src, src_gray;
int thresh = 100;
int max_thresh = 255;
RNG rng(12345);

void thresh_callback(int, void*);

int main()
{
    // 读图
    src = imread("E:\\la.jpg", IMREAD_COLOR);
    if (src.empty())
        return -1;

    // 转化为灰度图
    cvtColor(src, src_gray, COLOR_BGR2GRAY);
    //均值滤波
    blur(src_gray, src_gray, Size(3, 3));

    // 显示
    namedWindow("Source", WINDOW_AUTOSIZE);
    imshow("Source", src);

    // 滑动条
    createTrackbar("Canny thresh:", "Source", &thresh, max_thresh, thresh_callback);

    // 回调函数
    thresh_callback(0, 0);

    waitKey(0);
}

// 回调函数
void thresh_callback(int, void*)
{
    Mat canny_output;
    vector<vector<Point> > contours;
    vector<Vec4i> hierarchy;

    // canny 边缘检测
    Canny(src_gray, canny_output, thresh, thresh * 2, 3);

    // 寻找轮廓
    findContours(canny_output, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));

    Mat drawing = Mat::zeros(canny_output.size(), CV_8UC3);

    // 画出轮廓
    for (size_t i = 0; i < contours.size(); i++) {
        Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
        drawContours(drawing, contours, (int)i, color, 2, 8, hierarchy, 0, Point());
    }

    namedWindow("Contours", WINDOW_AUTOSIZE);
    imshow("Contours", drawing);
}

image-20221010123354843

image-20221010123332422

void findContours (
    InputOutputArray      image,       // 输入图像
    OutputArrayOfArrays   contours,    // 检测到的轮廓
    OutputArray           hierarchy,   // 可选的输出向量
    int       mode,            // 轮廓获取模式 (RETR_EXTERNAL, RETR_LIST, RETR_CCOMP,RETR_TREE, RETR_FLOODFILL)
    int       method,          // 轮廓近似算法 (CHAIN_APPROX_NONE, CHAIN_APPROX_SIMPLE, CHAIN_APPROX_TC89_L1, CHAIN_APPROX_TC89_KCOS)
    Point     offset = Point() // 轮廓偏移量
)

hierarchy 为可选的参数,如果不选择该参数,则可得到 findContours 函数的第二种形式
void findContours (
  InputOutputArray   image,
  OutputArrayOfArrays contours,
  int    mode,
  int    method,
  Point   offset = Point()
)

drawContours() 函数如下: 
void drawContours (
    InputOutputArray     image,         // 目标图像
    InputArrayOfArrays   contours,      // 所有的输入轮廓
    int               contourIdx,      //
    const Scalar &     color,           //  轮廓颜色
    int          thickness = 1,         //  轮廓线厚度
    int          lineType = LINE_8,     //
    InputArray   hierarchy = noArray(), //
    int          maxLevel = INT_MAX,    //
    Point        offset = Point()       //    
)

3、基于区域分割

3.1 分水岭算法

分水岭算法的基本原理为:将任意的灰度图像视为地形图表面,其中灰度值高的部分表示山峰和丘陵,而灰度值低的地方表示山谷。用不同颜色的水(标签)填充每个独立的山谷(局部最小值);随着水平面的上升,来自不同山谷(具有不同颜色)的水将开始合并。为了避免出现这种情况,需要在水汇合的位置建造水坝;持续填充水和建造水坝,直到所有的山峰和丘陵都在水下。整个过程中建造的水坝将作为图像分割的依据。

使用分水岭算法执行图像分割操作时通常包含下列步骤:
(1) 将原图转换为灰度图像
(2) 应用形态变换中的开运算和膨胀操作,去除图像噪声,获得图像边缘信息,确定图像背景
(3) 进行距离转换,再进行阈值处理,确定图像前景
(4) 确定图像的未知区域(用图像的背景减去前景剩余的部分)
(5) 标记背景图像
(6) 执行分水岭算法分割图像
Vec3b RandomColor(int value);  //生成随机颜色函数

int main(int argc, char* argv[])
{
    Mat image = imread("test2.jpg");    //载入RGB彩色图像
    imshow("Source Image", image);

    //灰度化,滤波,Canny边缘检测
    Mat imageGray;
    cvtColor(image, imageGray, CV_RGB2GRAY);//灰度转换
    GaussianBlur(imageGray, imageGray, Size(5, 5), 2);   //高斯滤波
    imshow("Gray Image", imageGray);
    Canny(imageGray, imageGray, 80, 150);
    imshow("Canny Image", imageGray);

    //查找轮廓
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(imageGray, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
    Mat imageContours = Mat::zeros(image.size(), CV_8UC1);  //轮廓    
    Mat marks(image.size(), CV_32S);   //Opencv分水岭第二个矩阵参数
    marks = Scalar::all(0);
    int index = 0;
    int compCount = 0;
    for (; index >= 0; index = hierarchy[index][0], compCount++)
    {
        //对marks进行标记,对不同区域的轮廓进行编号,相当于设置注水点,有多少轮廓,就有多少注水点
        drawContours(marks, contours, index, Scalar::all(compCount + 1), 1, 8, hierarchy);
        drawContours(imageContours, contours, index, Scalar(255), 1, 8, hierarchy);
    }

    //我们来看一下传入的矩阵marks里是什么东西
    Mat marksShows;
    convertScaleAbs(marks, marksShows);
    imshow("marksShow", marksShows);
    imshow("轮廓", imageContours);
    watershed(image, marks);

    //我们再来看一下分水岭算法之后的矩阵marks里是什么东西
    Mat afterWatershed;
    convertScaleAbs(marks, afterWatershed);
    imshow("After Watershed", afterWatershed);

    //对每一个区域进行颜色填充
    Mat PerspectiveImage = Mat::zeros(image.size(), CV_8UC3);
    for (int i = 0; i < marks.rows; i++)
    {
        for (int j = 0; j < marks.cols; j++)
        {
            int index = marks.at<int>(i, j);
            if (marks.at<int>(i, j) == -1)
            {
                PerspectiveImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
            }
            else
            {
                PerspectiveImage.at<Vec3b>(i, j) = RandomColor(index);
            }
        }
    }
    imshow("After ColorFill", PerspectiveImage);

    //分割并填充颜色的结果跟原始图像融合
    Mat wshed;
    addWeighted(image, 0.4, PerspectiveImage, 0.6, 0, wshed);
    imshow("AddWeighted Image", wshed);

    waitKey();
}

Vec3b RandomColor(int value)  //生成随机颜色函数 
{
    value = value % 255;  //生成0~255的随机数
    RNG rng;
    int aa = rng.uniform(0, value);
    int bb = rng.uniform(0, value);
    int cc = rng.uniform(0, value);
    return Vec3b(aa, bb, cc);
}

image-20221010112336890

法二:

实现步骤:
    1.输入图像
    2.灰度化
    3.二值化
    4.执行距离变换
    5.归一化
    6.二值化
    7.生成marker:通过findContours+drawContours来创建一个marker
    8.将7生成的marker放入分水岭函数:watershed
    9.给marker着色
    10.输出着色后的图像
此算法关键点在于生成marker。生成marker之后其实已经完成了算法,后面的着色只是为了让输出更加好看。
int main() {

    Mat src = imread("E:\\la.jpg");//输入原图
    if (src.empty()) {
        //
        cout<< "图像为空";
        return 0;
    }
    //图像灰度化
    Mat gray;
    cvtColor(src, gray, COLOR_BGR2GRAY);
    //图像二值化
    Mat binary;
    threshold(gray, binary, 0, 255, THRESH_BINARY | cv::THRESH_OTSU);
    imshow("binary", binary);
    cout<< "binary....";
    //执行距离变换
    Mat dist;
    distanceTransform(binary, dist, DistanceTypes::DIST_L2, 3, CV_32F);
    cout << "distanceTransform....";
    normalize(dist, dist, 0.0, 1.0, NORM_MINMAX);//归一化0~1之间
    cout << "normalize....";
    //重新二值化预值
    threshold(dist, dist, 0.1, 1.0, THRESH_BINARY);
    cout << "threshold....";
    normalize(dist, dist, 0, 255, NORM_MINMAX);
    cout << "normalize....";
    dist.convertTo(dist, CV_8UC1);//
    cout << "convertTo....";

    //开始生成marker并绘制出来
    vector<vector<Point>> contours;
    vector<Vec4i> heri;
    findContours(dist, contours, RETR_CCOMP, CHAIN_APPROX_SIMPLE);
    cout << "findContours....";

    Mat marker = Mat::zeros(dist.size(), CV_32S);
    for (size_t i = 0; i < contours.size(); i++) {
        drawContours(marker, contours, i, Scalar(i + 1), -1, 8, heri, INT_MAX);
    }
    cout << "drawContours...." << contours.size();
    circle(marker, Point(5, 5), 3, Scalar(255), -1);
    watershed(src, marker);
    cout << "watershed....";
    //    marker.convertTo(marker,CV_8UC1);//ps:此处需要注意(到这里实际上已经完成了算法)。一旦不转换就不能用imshow,一旦转换了后面的marker着色就会出现异常
    //    imshow("marker",marker);
    cout << "imshow(marker,marker);....";
    //生成颜色数组
    vector<Vec3b> colors;
    for (size_t i = 0; i < contours.size(); i++) {
        int r = theRNG().uniform(0, 255);
        int g = theRNG().uniform(0, 255);
        int b = theRNG().uniform(0, 255);
        colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
    }

    //给marker着色
    Mat finalResult = Mat::zeros(dist.size(), CV_8UC3);//三通道彩色图像
    int index = 0;
    for (int row = 0; row < marker.rows; row++) {
        for (int col = 0; col < marker.cols; col++) {
            index = marker.at<int>(row, col);
            if (index > 0 && index <= contours.size()) {
                finalResult.at<Vec3b>(row, col) = colors[index - 1];
            }
            else {
                finalResult.at<Vec3b>(row, col) = Vec3b(255, 255, 255);
            }
        }
    }
    imshow("finalResult", finalResult);
    waitKey(0);
    return 0;
}

image-20221010133045829

distanceTransform()距离变换的定义是计算一个图像中非零像素点到最近的零像素点的距离,也就是到零像素点的最短距离。即距离变换的定义是计算一个图像中非零像素点到最近的零像素点的距离,也就是到零像素点的最短距离。
通常处理的是一个二值化的图,所以求距离可以归一化,距离(像素距离)单位为1。
void distanceTransform(
    InputArray src, 
    OutputArray dst, 
    int distanceType, 
    int maskSize, 
    int dstType=CV_32F )
void distanceTransform(
    InputArray src, 
    OutputArray dst, 
    OutputArray labels, 
    int distanceType, 
    int maskSize, 
    int labelType=DIST_LABEL_CCOMP )
src:源矩阵
dst:目标矩阵
distanceType:距离类型。可以的类型是CV_DIST_L1、CV_DIST_L2、CV_DIST_C,具体各类型的意义,请查阅相关算法文档。
maskSize:距离变换运算时的掩码大小。值可以是3、5或CV_DIST_MASK_PRECISE(5或CV_DIST_MASK_PRECISE只能用在第一个原型中)。当distanceType=CV_DIST_L1 或 CV_DIST_C时,maskSize只能为3。
dstType:输出图像(矩阵)的类型,可以是CV_8U 或 CV_32F。CV_8U只能用在第一个原型中,而且distanceType只能为CV_DIST_L1。
labels:输出二维阵列标签。
labelType:标签数组类型。可选值为DIST_LABEL_CCOMP和DIST_LABEL_PIXEL

3.2 grab算法分割图像

Graphcut是一种基于图论的分割方法,在计算机视觉领域中应用于前景分割、医学处理、纹理分割及立体视觉等方面,类似于PS中的抠图功能。基本图论的分割技术是图像分割领域中新的研究热点,该方法基于能量优化算法,将图像分割问题转换为图的最小割优化问题。
Grabcut是Graphcut算法的改进,Graphcut是一种直接基于图切算法的图像分割技术,仅仅需要确认前景与背景输入,该算法就可以完成背景与前景相似督导赋权图,并通过最优切割来实现图像分割。Grabcut算法可以不需要用户交互,仅仅需要输入包含目标前景的区域就可以完成前景与前景的分离。
Graphcut的目标和背景模型是灰度直方图,Grabcut采用的是RGB三通道混合高斯模型;Graphcut的能量最小化分割是能通过一次计算实现的,而Grabcut是根据分割模型参数更新完成学的学习过程;Graphcut需要用户输入前景与背景区域点集,而Grabcut只需要提供含有背景的区域像素集就可以完成分割。

算法流程:
(1)在图片中定义含有(一个或多个)物体的矩形;
(2)矩形外的区域被自动认为是背景;
(3)对于用户定义的矩形区域,可用背景中数据来区分是前景还是背景;
(4)用高斯混合模型(GMM)来对被禁和前景见面,并将未定义的像素标记为可能的前景或背景;
(5)图像中的每一个像素都被看作通过通过虚拟变与周围像素连接,而每条边都有一个属于前景或背景的概率这基于它和周围像素颜色上的相似性;
(6)每一个像素(即算法中的节点)会与前一各前景或背景节点连接;
(7)在节点连接完成后,用图论中最大流最小割的方法来分割。
int main()
{
    Mat src = imread("E:\\la.jpg");
    Rect rect(24, 24, 206, 218);//左上坐标(X,Y)和长宽
    Mat result, bg, fg;
    imshow("src", src);
    grabCut(src, result, rect, bg, fg, 1, GC_INIT_WITH_RECT);
    imshow("grab", result);
    /*threshold(result, result, 2, 255, CV_THRESH_BINARY);
    imshow("threshold", result);*/

    compare(result, GC_PR_FGD, result, CMP_EQ);//result和GC_PR_FGD对应像素相等时,目标图像该像素值置为255
    imshow("result", result);
    Mat foreground(src.size(), CV_8UC3, Scalar(255, 255, 255));
    src.copyTo(foreground, result);//copyTo有两种形式,此形式表示result为mask
    imshow("foreground", foreground);
    waitKey(0);
    return 0;
}

image-20221010104701641

grabCut(img, rect, mask,
            bgdModel, fgdModel, 
            iterCount, mode = GC_EVAL)
            
void grabCut( InputArray img,           
//输入图像,必须是8位3通道图像,在处理过程中不会被修改
              InputOutputArray mask,    
//掩码图像,用来确定哪些区域是背景,前景,可能是背景,可能是前景等,mask既可以作为输入也可以作为输出。作为输入时,mode要 选择GC_INIT_WITH_MASK (=1);GCD_BGD (=0), 背景;GCD_FGD (=1),前景;GCD_PR_BGD (=2),可能是背景;GCD_PR_FGD(=3),可能是前景
    Rect rect,                
//包含前景的矩形,格式为(x, y, w, h)
     InputOutputArray bgdModel,
//算法内部使用的数组,只需要创建大小为(1,65), 数据类型为np.float64的数组
     InputOutputArray fgdModel,
 //同上
     int iterCount,            
//算法迭代的次数
     int mode = GC_EVAL        
//用来指示grabCut函数进行什么操作
// GC_INIT_WITH_RECT (=0),用矩形窗初始化GrabCut;
// GC_INIT_WITH_MASK (=1),用掩码图像初始化GrabCut
            );

法二:


//grabcut算法
bool setMouse = false;    //判断鼠标左键的状态(up / down)
bool init;
Point pt;
Rect rect;
Mat srcImg, mask, bgModel, fgModel;
int numRun = 0;
void onMouse(int, int, int, int, void*);
void runGrabCut();
void showImage();
int main()
{
    srcImg = imread("E:\\4.jpg");
    if (srcImg.empty())
    {
        printf("could not load image...\n");
        return -1;
    }

    imshow("源图像", srcImg);

    mask.create(srcImg.size(), CV_8U);
    setMouseCallback("源图像", onMouse, 0);

    while (1)
    {
        char c = (char)waitKey(0);
        if (c == ' ') {//选中矩形框后,按空格键执行grabcut分割
            runGrabCut();
            numRun++;
            showImage();
            printf("current iteative times : %d\n", numRun);
        }
        if ((int)c == 27) {
            break;
        }

    }
    return 0;
}

void showImage()
{
    Mat result, binmask;
    binmask = mask & 1;                //进一步掩膜
    if (init)                        //进一步抠出无效区域。鼠标按下,init变为false
    {
        srcImg.copyTo(result, binmask);
    }
    else
    {
        result = srcImg.clone();
    }
    rectangle(result, rect, Scalar(0, 0, 255), 2, 8);
    imshow("源图像", result);
}

void onMouse(int events, int x, int y, int flag, void*)
{
    if (x < 0 || y < 0 || x > srcImg.cols || y > srcImg.rows)    //无效区域
        return;

    if (events == EVENT_LBUTTONDOWN)
    {
        setMouse = true;
        pt.x = x;
        pt.y = y;
        init = false;
    }
    else if (events == EVENT_MOUSEMOVE)//鼠标只要动,就执行一次
    {
        if (setMouse == true)            //鼠标左键按住,滑动
        {
            Point pt1;
            pt1.x = x;
            pt1.y = y;
            rect = Rect(pt, pt1);//定义矩形区域
            showImage();
            mask.setTo(Scalar::all(GC_BGD));//背景
            mask(rect).setTo(Scalar(GC_PR_FGD));//前景                //对rect内部设置为可能的前景,外部设置为背景
        }
    }
    else if (events == EVENT_LBUTTONUP)
        setMouse = false;                //鼠标左键抬起
}

void runGrabCut()
{
    if (init)//鼠标按下,init变为false
        grabCut(srcImg, mask, rect, bgModel, fgModel, 1);//第二次迭代,用mask初始化grabcut
    else
    {
        grabCut(srcImg, mask, rect, bgModel, fgModel, 1, GC_INIT_WITH_RECT);//用矩形窗初始化GrabCut
        init = true;
    }
}

image-20221010105904367

image-20221010105922606

int event:
 
#define CV_EVENT_MOUSEMOVE 0             //滑动
#define CV_EVENT_LBUTTONDOWN 1           //左键点击
#define CV_EVENT_RBUTTONDOWN 2           //右键点击
#define CV_EVENT_MBUTTONDOWN 3           //中键点击
#define CV_EVENT_LBUTTONUP 4             //左键放开
#define CV_EVENT_RBUTTONUP 5             //右键放开
#define CV_EVENT_MBUTTONUP 6             //中键放开
#define CV_EVENT_LBUTTONDBLCLK 7         //左键双击
#define CV_EVENT_RBUTTONDBLCLK 8         //右键双击
#define CV_EVENT_MBUTTONDBLCLK 9         //中键双击

int flags:
 
#define CV_EVENT_FLAG_LBUTTON 1       //左鍵拖曳
#define CV_EVENT_FLAG_RBUTTON 2       //右鍵拖曳
#define CV_EVENT_FLAG_MBUTTON 4       //中鍵拖曳
#define CV_EVENT_FLAG_CTRLKEY 8       //(8~15)按Ctrl不放事件
#define CV_EVENT_FLAG_SHIFTKEY 16     //(16~31)按Shift不放事件
#define CV_EVENT_FLAG_ALTKEY 32       //(32~39)按Alt不放事件

4、floodFill漫水填充分割

漫水填充法是一种用特定的颜色填充联通区域,通过设置可连同像素的上下限以及联通方式来达到不同的填充效果。

int main()
{
    Mat src = imread("E:\\la.jpg");
    imshow("【原始图】", src);
    Rect ccomp;
    floodFill(src, Point(80, 200), Scalar(0, 0, 0), &ccomp, Scalar(20, 20, 20), Scalar(20, 20, 20));
    imshow("【效果图】", src);
    waitKey(0);
    return 0;
}

image-20221010110616145

第二版本(功能更加强大)

//全局变量声明部分
Mat g_srcImage, g_dstImage, g_grayImage, g_maskImage;//定义原始图、目标图、灰度图、掩膜图
int g_nFillMode = 1;//漫水填充的模式
int g_nLowDifference = 20, g_nUpDifference = 20;//负差最大值,正差最大值
int g_nConnectivity = 4;//表示floodFill函数标识符低八位的连通值
bool g_bIsColor = true;//是否为彩色图的标识符布尔值
bool g_bUseMask = false;//是否显示掩膜窗口的布尔值
int g_nNewMaskVal = 255;//新的重新绘制的像素值

//===============【onMouse()函数】=======================
static void onMouse(int event, int x, int y, int, void *) {
    //若鼠标左键没有按下,便返回
    if (event != EVENT_LBUTTONDOWN)
        return;

    //-----------------【<1>调用floodFill函数之前的参数准备部分】-------------
    Point seed = Point(x, y);
    int LowDifference = g_nFillMode == 0 ? 0 : g_nLowDifference;
    int UpDifference = g_nFillMode == 0 ? 0 : g_nUpDifference;

    //标识符的0~7位为g_nConnectivity,8~15位为g_nNewMaskVal左移8位的值,16~23位为CV_FLOODFILL_FIXED_RANGE或者0
    int flags = g_nConnectivity + (g_nNewMaskVal << 8) + (g_nFillMode == 1 ? FLOODFILL_FIXED_RANGE : 0);

    //随机生成BGR值
    int b = (unsigned)theRNG() & 255;//随机返回一个0~255之间的值
    int g = (unsigned)theRNG() & 255;
    int r = (unsigned)theRNG() & 255;
    Rect ccomp;//定义重绘区域的最小边界矩阵区域

    Scalar newVal = g_bIsColor ? Scalar(b, g, r) : Scalar(r*0.299 + g * 0.587 + b * 0.114);

    Mat dst = g_bIsColor ? g_dstImage : g_grayImage;//目标图的赋值
    int area;

    //---------------------【<2>正式调用floodFill函数】------------------
    if (g_bUseMask) {
        threshold(g_maskImage, g_maskImage, 1, 128, THRESH_BINARY);

        area = floodFill(dst, g_maskImage, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference), Scalar(UpDifference, UpDifference, UpDifference), flags);

        imshow("mask", g_maskImage);
    }
    else {
        area = floodFill(dst, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference), Scalar(UpDifference, UpDifference, UpDifference), flags);
    }
    imshow("效果图", dst);
    cout << area << " 个像素被重新绘制\n";
}

//main()函数
int main(int argc, char** argv) {
    //载入原图
    g_srcImage = imread("test.jpg", 1);
    if (!g_srcImage.data) {
        printf("读取g_srcImage错误!\n");
        return false;
    }

    g_srcImage.copyTo(g_dstImage);//复制原图到目标图
    cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);//转为灰度图到g_grayImage

    g_maskImage.create(g_srcImage.rows + 2, g_srcImage.cols + 2, CV_8UC1);//用原图尺寸初始化掩膜mask

    namedWindow("效果图", WINDOW_AUTOSIZE);

    //创建Trackbar
    createTrackbar("负差最大值", "效果图", &g_nLowDifference, 255, 0);
    createTrackbar("正差最大值", "效果图", &g_nUpDifference, 255, 0);

    //鼠标回调函数
    setMouseCallback("效果图", onMouse, 0);

    //循环轮询按键
    while (1) {
        //先显示效果图
        imshow("效果图", g_bIsColor ? g_dstImage : g_grayImage);

        //获取按键键盘
        int c = waitKey(0);
        //判断ESC是否按下,按下退出
        if (c == 27) {
            cout << "程序退出........、\n";
            break;
        }

        //根据按键不同进行不同的操作
        switch ((char)c) {
            //如果键盘1被按下,效果图在灰度图和彩色图之间转换
        case '1':
            if (g_bIsColor) {//若原来为彩色图,转换为灰度图,并将掩膜mask所有元素设置为0
                cout << "键盘‘1’按下,切换彩色/灰度模式,当前操作将【彩色模式】切换为【灰度模式】" << endl;
                cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
                g_maskImage = Scalar::all(0);//将mask所有元素设置为0
                g_bIsColor = false;
            }
            else {
                cout << "键盘‘1’按下,切换彩色/灰度模式,当前操作将【灰度模式】切换为【彩色模式】" << endl;
                g_srcImage.copyTo(g_dstImage);
                g_maskImage = Scalar::all(0);
                g_bIsColor = true;
            }
        case '2':
            if (g_bUseMask) {
                destroyWindow("mask");
                g_bUseMask = false;
            }
            else {
                namedWindow("mask", 0);
                g_maskImage = Scalar::all(0);
                imshow("mask", g_maskImage);
                g_bUseMask = true;
            }
            break;
        case '3'://如果键盘3被按下,恢复原始图像
            cout << "按下键盘‘3’,恢复原始图像\n";
            g_srcImage.copyTo(g_dstImage);
            cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
            g_maskImage = Scalar::all(0);
            break;
        case '4':
            cout << "键盘‘4’被按下,使用空范围的漫水填充\n";
            g_nFillMode = 0;
            break;
        case '5':
            cout << "键盘‘5’被按下,使用渐变、固定范围的漫水填充\n";
            g_nFillMode = 1;
            break;
        case '6':
            cout << "键盘‘6’被按下,使用渐变、浮动范围的漫水填充\n";
            g_nFillMode = 2;
            break;
        case '7':
            cout << "键盘‘7’被按下,操作标识符的低八位使用4位的连接模式\n";
            g_nConnectivity = 4;
            break;
        case '8':
            cout << "键盘‘8’被按下,操作标识符的低八位使用8为的连接模式\n";
            g_nConnectivity = 8;
            break;
        }

    }

    return 0;

}
键盘‘1’按下,切换彩色/灰度模式,当前操作将【彩色模式】切换为【灰度模式】
键盘‘1’按下,切换彩色/灰度模式,当前操作将【灰度模式】切换为【彩色模式】
按下键盘‘3’,恢复原始图像
键盘‘4’被按下,使用空范围的漫水填充
键盘‘5’被按下,使用渐变、固定范围的漫水填充
键盘‘6’被按下,使用渐变、浮动范围的漫水填充
键盘‘7’被按下,操作标识符的低八位使用4位的连接模式
键盘‘8’被按下,操作标识符的低八位使用8为的连接模式

image-20221010111524529

image-20221010111415936

 void cvFloodFill (
     IplImage * img,         // 输入图像
     CvPoint    seedPoint,    // 种子点
     CvScalar newVal,       // 像素点被染色的值
     CvScalar loDiff = cvScalarAll(0),     // 染色边界判定
     CvScalar upDiff = cvScalarAll(0),     // 染色边界判定
     CvConnectedComp * comp = NULL,        // 填充区域统计属性
     int flags = 4,                        // 连通性,相关性等参数设置。
     CvArr * mask = NULL                   // 掩码区域
);
image 【输入/输出】 1或者3通道、 8bit或者浮点图像。仅当参数flags的FLOODFILL_MASK_ONLY标志位被设置时image不会被修改,否则会被修改。
mask 【输入/输出】 操作掩码,必须为单通道、8bit,且比image宽2个像素、高2个像素。使用前必须先初始化。Flood-filling无法跨越mask中的非0像素。例如,一个边缘检测的结果可以作为mask来阻止边缘填充。在输出中,mask中与image中填充像素对应的像素点被设置为1,或者flags标志位中设置的值(详见flags标志位的解释)。此外,该函数还用1填充了mask的边缘来简化内部处理。因此,可以在多个调用中使用同一mask,以确保填充区域不会重叠。
seedPoint 起始像素点
newVal 重绘像素区域的新的填充值(颜色)
rect 可选输出参数,返回重绘区域的最小绑定矩形。
loDiff 当前选定像素与其连通区中相邻像素中的一个像素,或者与加入该连通区的一个seedPoint像素,二者之间的最大下行差异值。
upDiff 当前选定像素与其连通区中相邻像素中的一个像素,或者与加入该连通区的一个seedPoint像素,二者之间的最大上行差异值。
flags flags标志位是一个32bit的int类型数据,其由3部分组成: 0-7bit表示邻接性(4邻接、8邻接);8-15bit表示mask的填充颜色;16-31bit表示填充模式(详见填充模式解释)

5、彩色图像分割

HSV是一种比较直观的颜色模型,HSV 颜色空间的各通道分别表示色调(Hue)、饱和度(Saturation)和明度(Value),可以直观地表达色彩的明暗、色调及鲜艳程度。由于RGB色彩控件是由三个通道来编码颜色的,因此很难根据颜色来分割物体,而HSV中只有Hue一个通道表示颜色。
HSV 颜色空间可以用一个圆锥空间模型来描述。圆锥的顶点处 V=0,H 和 S 无定义,代表黑色;圆锥的顶面中心处V=max,S=0,H 无定义,代表白色。当 S=1, V=1 时,H 所代表的任何颜色被称为纯色;当 S=0 时,饱和度为 0,颜色最浅,最浅被描述为灰色,灰色的亮度由 V 决定,此时 H 无意义;当 V=0 时,颜色最暗,最暗被描述为黑色,此时 H 和 S 均无意义,无论如何取值均为黑色。

image-20221010102911814

image-20221010103329286

//输入图像
Mat img;
//灰度值归一化
Mat bgr;
//HSV图像
Mat hsv;
//色相
int hmin = 0;
int hmin_Max = 360;
int hmax = 180;
int hmax_Max = 180;
//饱和度
int smin = 0;
int smin_Max = 255;
int smax = 255;
int smax_Max = 255;
//亮度
int vmin = 106;
int vmin_Max = 255;
int vmax = 255;
int vmax_Max = 255;
//显示原图的窗口
string windowName = "原图";
//输出图像的显示窗口
string dstName = "dst";
//输出图像
Mat dst;
//回调函数
void callBack(int, void*)
{
    //输出图像分配内存
    dst = Mat::zeros(img.size(), img.type());
    //掩码
    Mat mask;
    inRange(hsv, Scalar(hmin, smin, vmin), Scalar(hmax, smax, vmax), mask);
    //掩模到原图的转换
    for (int r = 0; r < bgr.rows; r++)
    {
        for (int c = 0; c < bgr.cols; c++)
        {
            if (mask.at<uchar>(r, c) == 255)
            {
                dst.at<Vec3b>(r, c) = bgr.at<Vec3b>(r, c);
            }
        }
    }
    //输出图像
    imshow(dstName, dst);
    //保存图像
    //dst.convertTo(dst, CV_8UC3, 255.0, 0);
    
}
int main(int argc, char** argv)
{
    //输入图像
    img = imread("E:\\se.jpg");
    if (!img.data || img.channels() != 3)
        return -1;
    imshow(windowName, img);
    bgr = img.clone();
    //颜色空间转换
    cvtColor(bgr, hsv, CV_BGR2HSV);
    //cout << hsv << endl;
    //定义输出图像的显示窗口
    namedWindow(dstName, WINDOW_GUI_EXPANDED);
    //调节色相 H
    createTrackbar("hmin", dstName, &hmin, hmin_Max, callBack);
    createTrackbar("hmax", dstName, &hmax, hmax_Max, callBack);
    //调节饱和度 S
    createTrackbar("smin", dstName, &smin, smin_Max, callBack);
    createTrackbar("smax", dstName, &smax, smax_Max, callBack);
    //调节亮度 V
    createTrackbar("vmin", dstName, &vmin, vmin_Max, callBack);
    createTrackbar("vmax", dstName, &vmax, vmax_Max, callBack);
    callBack(0, 0);
    waitKey(0);
    return 0;
}

image-20221010103312926

image-20221010103424849

void cv::inRange(    
                    InputArray     src,
                    InputArray     lowerb,
                    InputArray     upperb,
                    OutputArray dst 
                )    
OpenCV中的函数inRange()用于将指定值范围的像素选出来。如果像素的值满足指定的范围,则这个像素点的值被置为255,否则值被置为0。
    src:输入图像,CV2常用Mat类型;
  lowerb:lower boundary下限,scalar类型的像素值,单通道scalar取一个值就行,彩图3通道scalar三个值;
  upperb:上限,类型与lowerb同理;
  dst:输出图像,尺寸与src一致,类型是CV_8U,但没有指定通道数。

(int argc, char** argv)
{
//输入图像
img = imread(“E:\se.jpg”);
if (!img.data || img.channels() != 3)
return -1;
imshow(windowName, img);
bgr = img.clone();
//颜色空间转换
cvtColor(bgr, hsv, CV_BGR2HSV);
//cout << hsv << endl;
//定义输出图像的显示窗口
namedWindow(dstName, WINDOW_GUI_EXPANDED);
//调节色相 H
createTrackbar(“hmin”, dstName, &hmin, hmin_Max, callBack);
createTrackbar(“hmax”, dstName, &hmax, hmax_Max, callBack);
//调节饱和度 S
createTrackbar(“smin”, dstName, &smin, smin_Max, callBack);
createTrackbar(“smax”, dstName, &smax, smax_Max, callBack);
//调节亮度 V
createTrackbar(“vmin”, dstName, &vmin, vmin_Max, callBack);
createTrackbar(“vmax”, dstName, &vmax, vmax_Max, callBack);
callBack(0, 0);
waitKey(0);
return 0;
}


[外链图片转存中...(img-ccEX47mI-1665380014085)]

[外链图片转存中...(img-EOomyhXf-1665380014085)]

```txt
void cv::inRange(    
                    InputArray     src,
                    InputArray     lowerb,
                    InputArray     upperb,
                    OutputArray dst 
                )    
OpenCV中的函数inRange()用于将指定值范围的像素选出来。如果像素的值满足指定的范围,则这个像素点的值被置为255,否则值被置为0。
    src:输入图像,CV2常用Mat类型;
  lowerb:lower boundary下限,scalar类型的像素值,单通道scalar取一个值就行,彩图3通道scalar三个值;
  upperb:上限,类型与lowerb同理;
  dst:输出图像,尺寸与src一致,类型是CV_8U,但没有指定通道数。

部分参考来自OpenCV中Grabcut算法的具体使用_C 语言_脚本之家 (jb51.net)


本文转载自: https://blog.csdn.net/qq_43784519/article/details/127242922
版权归原作者 qq_43784519 所有, 如有侵权,请联系我们删除。

“OpenCV数字图像处理基于C++:图像分割”的评论:

还没有评论