2015-10-18 09:58:56 iracer 阅读数 46920
  • OpenCV3.2 Java图像处理视频学习教程

    OpenCV3.2 Java图像处理视频培训课程:基于OpenCV新版本3.2.0详细讲述Java OpenCV图像处理部分内容,包括Mat对象使用、图像读写、 基于常用核心API讲述基本原理、使用方法、参数、代码演示、图像处理思路与流程讲授。主要内容包括opencv像素操作、滤波、边缘提取、直线与圆检测、形态学操作与分水岭、图像金子塔融合重建、多尺度模板匹配、opencv人脸检测、OpenCV跟Tomcat使用实现服务器端图像处理服务。

    4245 人正在学习 去看看 贾志刚


OpenCV—图像分割中的分水岭算法原理与应用



图像分割是按照一定的原则,将一幅图像分为若干个互不相交的小局域的过程,它是图像处理中最为基础的研究领域之一。目前有很多图像分割方法,其中分水岭算法是一种基于区域的图像分割算法,分水岭算法因实现方便,已经在医疗图像,模式识别等领域得到了广泛的应用。

1.传统分水岭算法基本原理

分水岭比较经典的计算方法是L.Vincent于1991年在PAMI上提出的[1]。传统的分水岭分割方法,是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作是测地学上的拓扑地貌,图像中每一像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆地,而集水盆地的边界则形成分水岭。分水岭的概念和形成可以通过模拟浸入过程来说明。在每一个局部极小值表面,刺穿一个小孔,然后把整个模型慢慢浸人水中,随着浸入的加深,每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝如下图所示,即形成分水岭。


传统分水岭算法示意图

然而基于梯度图像的直接分水岭算法容易导致图像的过分割,产生这一现象的原因主要是由于输入的图像存在过多的极小区域而产生许多小的集水盆地,从而导致分割后的图像不能将图像中有意义的区域表示出来。所以必须对分割结果的相似区域进行合并。

[1]L.Vincent, P Soille. Watersheds in digital space: An efficientalgorithms based on immersion simulation[J]. IEEE Trans. on Pattern Analysisand Machine Intelligence, 1991, 13(6): 583-598.

2.改进的分水岭算法基本原理

因为传统分水岭算法存在过分割的不足,OpenCV提供了一种改进的分水岭算法,使用一系列预定义标记来引导图像分割的定义方式。使用OpenCV的分水岭算法cv::wathershed,需要输入一个标记图像,图像的像素值为32位有符号正数(CV_32S类型),每个非零像素代表一个标签。它的原理是对图像中部分像素做标记,表明它的所属区域是已知的。分水岭算法可以根据这个初始标签确定其他像素所属的区域。传统的基于梯度的分水岭算法和改进后基于标记的分水岭算法示意图如下图所示。


传统基于梯度的分水岭算法和基于标记的分水岭算法原理图

从上图可以看出,传统基于梯度的分水岭算法由于局部最小值过多造成分割后的分水岭较多。而基于标记的分水岭算法,水淹过程从预先定义好的标记图像(像素)开始,较好的克服了过度分割的不足。本质上讲,基于标记点的改进算法是利用先验知识来帮助分割的一种方法。因此,改进算法的关键在于如何获得准确的标记图像,即如何将前景物体与背景准确的标记出来。

3.基于标记点的分水岭算法应用

基于标记点的分水岭算法应用步骤

●  封装分水岭算法类

●  获取标记图像

            获取前景像素,并用255标记前景

            获取背景像素,并用128标记背景,未知像素,使用0标记

            合成标记图像

●  将原图和标记图像输入分水岭算法

●  显示结果

(1)封装分水岭算法类

将分水岭算法cv::watershed(image,markers)封装进类WatershedSegmenter,并保存为头文件以便于操作。(本段封装代码参考《OpenCV计算机视觉编程攻略(第二版)》)

#if !defined WATERSHS
#define WATERSHS

#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>

class WatershedSegmenter {

  private:

	  cv::Mat markers;

  public:

	  void setMarkers(const cv::Mat& markerImage) {

		// Convert to image of ints
		markerImage.convertTo(markers,CV_32S);
	  }

	  cv::Mat process(const cv::Mat &image) {

		// Apply watershed
		cv::watershed(image,markers);

		return markers;
	  }

	  // Return result in the form of an image
	  cv::Mat getSegmentation() {
		  
		cv::Mat tmp;
		// all segment with label higher than 255
		// will be assigned value 255
		markers.convertTo(tmp,CV_8U);

		return tmp;
	  }

	  // Return watershed in the form of an image以图像的形式返回分水岭
	  cv::Mat getWatersheds() {
	
		cv::Mat tmp;
		//在变换前,把每个像素p转换为255p+255(在conertTo中实现)
		markers.convertTo(tmp,CV_8U,255,255);

		return tmp;
	  }
};
#endif

(2)获取标记图像

标记前景

读取原图

// Read input image
	cv::Mat image1= cv::imread("image.jpg");
	if (!image1.data)
		return 0; 
// Display the color image
	cv::resize(image1, image1, cv::Size(), 0.7, 0.7);
	cv::namedWindow("Original Image1");
	cv::imshow("Original Image1",image1);

原图

以下代码目的是获取前景物体的像素,并用255标记。这里使用阈值分割初步分割前景和背景,接着使用形态学闭运算连接二值图像中前景的各个部分,并平滑边缘。如何更好的获取前景像素,需要根据实际图像的情况灵活处理。

// Identify image pixels with object
	
	Mat binary;
	cv::cvtColor(image1,binary,COLOR_BGRA2GRAY);
	cv::threshold(binary,binary,30,255,THRESH_BINARY_INV);//阈值分割原图的灰度图,获得二值图像
	// Display the binary image
	cv::namedWindow("binary Image1");
	cv::imshow("binary Image1",binary);
	waitKey();
	
	// CLOSE operation
	cv::Mat element5(5,5,CV_8U,cv::Scalar(1));//5*5正方形,8位uchar型,全1结构元素
	cv::Mat fg1;
	cv::morphologyEx(binary, fg1,cv::MORPH_CLOSE,element5,Point(-1,-1),1);// 闭运算填充物体内细小空洞、连接邻近物体

	// Display the foreground image
	cv::namedWindow("Foreground Image");
	cv::imshow("Foreground Image",fg1);
	waitKey();

阈值分割原图像的灰度图


闭运算获取前景

标记背景和未知区域

在上面阈值分割得到的二值图像binary的基础上,通过对白色前景的深度膨胀运算获得一个超过前景实际大小的物体,紧接着用反向阈值将深度膨胀后的图像中的黑色部分转换成128,即完成了对背景像素的标记。实际上,在0~255范围内,任意不为0或255的值均可作为背景的标记。当然如果有其他类型的物体,可以使用另外一个数值作为其标记。也就是说,多个目标可以有多个标记来帮助分水岭算法正确分割图像。

// Identify image pixels without objects
	
	cv::Mat bg1;
	cv::dilate(binary,bg1,cv::Mat(),cv::Point(-1,-1),4);//膨胀4次,锚点为结构元素中心点
	cv::threshold(bg1,bg1,1,128,cv::THRESH_BINARY_INV);//>=1的像素设置为128(即背景)
	// Display the background image
	cv::namedWindow("Background Image");
	cv::imshow("Background Image",bg1);
	waitKey();

将背景设置为128,未知区域设置为0

合成标记图像

将前景、背景及未知区域合成为一个标记图像。则标记图像中通过255标记前景物体,通过128标记背景,通过0标记未知区域。

//Get markers image

	Mat markers1 = fg1 + bg1; //使用Mat类的重载运算符+来合并图像。
	cv::namedWindow("markers Image");
	cv::imshow("markers Image",markers1);
	waitKey();

标记图像

(3)分水岭算法分割图像

将标记图像和原图输入分水岭算法封装的类WatershedSegmenter,执行分水岭算法,并显示算法运行的结果。
// Apply watershed segmentation

	WatershedSegmenter segmenter1;  //实例化一个分水岭分割方法的对象
	segmenter1.setMarkers(markers1);//设置算法的标记图像,使得水淹过程从这组预先定义好的标记像素开始
	segmenter1.process(image1);     //传入待分割原图
	 
	// Display segmentation result
	cv::namedWindow("Segmentation1");
	cv::imshow("Segmentation1",segmenter1.getSegmentation());//将修改后的标记图markers转换为可显示的8位灰度图并返回分割结果(白色为前景,灰色为背景,0为边缘)
	waitKey();
        // Display watersheds
	cv::namedWindow("Watersheds1");
	cv::imshow("Watersheds1",segmenter1.getWatersheds());//以图像的形式返回分水岭(分割线条)
	waitKey();

代码segmenter1.process(image)将修改标记图像markers,每个值为0的像素都会被赋予一个输入标签,而边缘处的像素赋值为-1,得到的标签图像如下图所示。


显示分水岭分割图像

分水岭分割线显示

(4)显示结果图像

本步骤的目的是将前景物体的分割结果在黑/白底色中显示出来。背景颜色由黑转白时使用了Mat矩阵扫描的.ptr方法与指针运算。

// Get the masked image
	Mat maskimage = segmenter1.getSegmentation();
	cv::threshold(maskimage,maskimage,250,1,THRESH_BINARY);
	cv::cvtColor(maskimage,maskimage,COLOR_GRAY2BGR);

	maskimage = image1.mul(maskimage);
	cv::namedWindow("maskimage");
	cv::imshow("maskimage",maskimage);
	waitKey();

	// Turn background (0) to white (255)
	int nl= maskimage.rows; // number of lines
    int nc= maskimage.cols * maskimage.channels(); // total number of elements per line

    for (int j=0; j<nl; j++) {
         uchar* data= maskimage.ptr<uchar>(j);
		for (int i=0; i<nc; i++) 
		{
            // process each pixel ---------------------
			if (*data==0) //将背景由黑色改为白色显示
				*data=255;
			data++;//指针操作:如为uchar型指针则移动1个字节,即移动到下1列
        }
     }
	cv::namedWindow("result");
	cv::imshow("result",maskimage);
	waitKey();

原图的前景分割图(黑色背景)


原图的前景分割图(白色背景)

从上图的分割结果可以看出,基于标记图像的分水岭算法较好的实现了复杂背景下前景目标分割。算法应用的关键步骤为标记图像的获取,目前很多文献提出了各类获取标记图像的方法,如何使用还需要根据所处理的图像来量身确定。

贴出实验原始图像:)

转载请注明:iracer的CSDN博客 http://blog.csdn.net/iracer/article/details/49225823

0

2018-09-11 11:56:40 qq_18234121 阅读数 379
  • OpenCV3.2 Java图像处理视频学习教程

    OpenCV3.2 Java图像处理视频培训课程:基于OpenCV新版本3.2.0详细讲述Java OpenCV图像处理部分内容,包括Mat对象使用、图像读写、 基于常用核心API讲述基本原理、使用方法、参数、代码演示、图像处理思路与流程讲授。主要内容包括opencv像素操作、滤波、边缘提取、直线与圆检测、形态学操作与分水岭、图像金子塔融合重建、多尺度模板匹配、opencv人脸检测、OpenCV跟Tomcat使用实现服务器端图像处理服务。

    4245 人正在学习 去看看 贾志刚

        分水岭算法是一种图像区域分割法,在分割的过程中,它会把跟临近像素间的相似性作为重要的参考依据,从而将在空间位置上相近并且灰度值相近(求梯度)的像素点互相连接起来构成一个封闭的轮廓。分水岭算法常用的操作步骤:彩色图像灰度化,然后再求梯度图,最后在梯度图的基础上进行分水岭算法,求得分段图像的边缘线。

        下面左边的灰度图,可以描述为右边的地形图,地形的高度是由灰度图的灰度值决定,灰度为0对应地形图的地面,灰度值最大的像素对应地形图的最高点。

                                             ima1 (2)ima2

对灰度图的地形学解释,我们我们考虑三类点:

1. 局部最小值点,该点对应一个盆地的最低点,当我们在盆地里滴一滴水的时候,由于重力作用,水最终会汇聚到该点。注意:可能存在一个最小值面,该平面内的都是最小值点。

2. 盆地的其它位置点,该位置滴的水滴会汇聚到局部最小点。

3. 盆地的边缘点,是该盆地和其它盆地交接点,在该点滴一滴水,会等概率的流向任何一个盆地。

                                       image

       假设我们在盆地的最小值点,打一个洞,然后往盆地里面注水,并阻止两个盆地的水汇集,我们会在两个盆地的水汇集的时刻,在交接的边缘线上(也即分水岭线),建一个坝,来阻止两个盆地的水汇集成一片水域。这样图像就被分成2个像素集,一个是注水盆地像素集,一个是分水岭线像素集。

      下面的gif图很好的演示了分水岭算法的效果:

                                          lpe1 (1)ima3 (1)

          在真实图像中,由于噪声点或者其它干扰因素的存在,使用分水岭算法常常存在过度分割的现象,这是因为很多很小的局部极值点的存在,比如下面的图像,这样的分割效果是毫无用处的。

                                          ima7ima7b

      为了解决过度分割的问题,可以使用基于标记(mark)图像的分水岭算法,就是通过先验知识,来指导分水岭算法,以便获得更好的图像分段效果。通常的mark图像,都是在某个区域定义了一些灰度层级,在这个区域的洪水淹没过程中,水平面都是从定义的高度开始的,这样可以避免一些很小的噪声极值区域的分割。

OPENCV中关于这一部分的解决方案:

大意说的是在执行分水岭函数watershed之前,必须对第二个参数markers进行处理,它应该包含不同区域的轮廓,每个轮廓有一个自己唯一的编号,轮廓的定位可以通过Opencv中findContours方法实现,这个是执行分水岭之前的要求。

接下来执行分水岭会发生什么呢?算法会根据markers传入的轮廓作为种子(也就是所谓的注水点),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。

简单概括一下就是说第二个入参markers必须包含了种子点信息。Opencv官方例程中使用鼠标划线标记,其实就是在定义种子,只不过需要手动操作,而使用findContours可以自动标记种子点。而分水岭方法完成之后并不会直接生成分割后的图像,还需要进一步的显示处理,如此看来,只有两个参数的watershed其实并不简单。

 

      下面的gif图很好的演示了基于mark的分水岭算法过程:

ima4lpe2ima5

      上面的过度分段图像,我们通过指定mark区域,可以得到很好的分段效果:

                             ima8ima9

图像原始图:

 

分割效果:

按比例跟原始图像融合:

2017-03-24 22:13:27 qq_35724402 阅读数 408
  • OpenCV3.2 Java图像处理视频学习教程

    OpenCV3.2 Java图像处理视频培训课程:基于OpenCV新版本3.2.0详细讲述Java OpenCV图像处理部分内容,包括Mat对象使用、图像读写、 基于常用核心API讲述基本原理、使用方法、参数、代码演示、图像处理思路与流程讲授。主要内容包括opencv像素操作、滤波、边缘提取、直线与圆检测、形态学操作与分水岭、图像金子塔融合重建、多尺度模板匹配、opencv人脸检测、OpenCV跟Tomcat使用实现服务器端图像处理服务。

    4245 人正在学习 去看看 贾志刚

     图像的分水岭变换是一种流行的图像处理算法,用于快速将图像分割成多个同质区域。

     它基于这样的思想:如果把图像看作一个拓扑地貌,那么同类区域就相当于陡峭的边缘内相对平坦的盆地。

     使用图像的分水岭分割算法,函数为:

      CV_EXPORTS_W void watershed( InputArray image, InputOutputArray markers );

注意:

  image: 输入图像,需为8位的三通道彩色图像

  markers: 参数调用后的结果,输入/输出32位单通道图像标记结果,需和原图一样的大小。


过程如下:

      以下处理中,使用的原图像为:


      首先读取灰度图像,并设置阈值,将其转化为二值图像,如下图所示:


     在形态学中,习惯用高像素值(白色)表示前景物体,用低像素值(黑色)表示背景物体,故对图像做反向处理。

      我们需要从二值图像中识别出属于前景(楼房、道路)以及属于背景(天空)的像素。这里我们把前景像素标记为255,背景像素标记为128(该数字是随意选择的,任何不等于255的数字都可以使用)。其他标签是未知的,标记为0.

      现在对图像做深度腐蚀运算,只保留属于重点物体的像素:

得到的图像如下所示:


       然后,我们通过对原二值图像做一次大幅度的膨胀运算来选中一些背景像素:

//标示不含物体的图像像素
    cv::Mat bg;
    cv::dilate(binary,bg, cv::Mat(),cv::Point(-1,-1),4);
    cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);
    cv::namedWindow("dilate image");
    cv::imshow("dilate image",bg);
    waitKey(0);

得到的黑色像素对应背景像素。因此在膨胀后,要立即通过阈值化运算将它们赋值为128,得到的图像如下图所示:


合并这两个图像得到标记图像,代码如下:

//创建标记图像
    cv::Mat markers(binary.size(),CV_8U,cv::Scalar(0));
    markers=fg+bg;

注意:这里运用重载运算符“+”来合并图像。得到下面的图像,将会被输入分水岭算法:


  在上幅图像中,白色区域属于前景物体,灰色区域属于背景,而黑色区域带有未知标签。

接下来,可用下面的方法来分割图像:

//创建分水岭分割类的对象
    WatershedSegmenter segmenter;
    
    //设置标记图像,然后执行分割过程
    segmenter.setMarkers(markers);
    segmenter.process(image1);
    
    //显示分割结果
    cv::namedWindow("Segmentation");
    cv::imshow("Segmentation",segmenter.getSegmentation());
    
    waitKey(0);

以上代码会修改标记图像,每个值为0的像素都会被赋予一个输入标签,而边缘处的像素赋值为-1。得到的标签图像如下所示:


边缘图像如下:


    原理:

        用分水岭算法分割图像的原理是从高度0开始逐步用洪水淹没图像。当“水”的高度逐步增加时(到1、2、3等),会形成聚水的盆地。随着盆地面积逐步变大,两个不同盆地的水最终会汇合到一起。这时就要创建一个分水岭,用来分割这两个盆地。当水位达到最大高度时,创建的盆地和分水岭就组成了分水岭分割图。

        可以想象,在水淹过程的开始阶段会创建很多细小的独立盆地。当所有盆地汇合时,就会创建很多分水岭线条,导致图像被过度分割。要解决这个问题,就要对这个算法进行修改,使得水淹的过程从一组预先定义好的标记像素开始。每个用标记创建的盆地,都按照初始标记的值加上标签。如果两个标签相同的盆地汇合,就不创建分水岭,以避免过度分割。

       调用cv::watershed函数时就执行了这些过程。输入的标记图像会被修改,用以生成最终的分水岭分割图。输入的标记
图像可以含有任意数值的标签,未知标签的像素值为0。标记图像的类型选用32位有符号整数,以便定义超过255个的标签。另外,可以把分水岭的对应像素设为特殊值-1。这是由cv::watershed函数返回的。

完整代码如下:

#ifndef WatershedSegmenter_h
#define WatershedSegmenter_h

#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>

class WatershedSegmenter {
    
private:
    //用来表示标记(图)
    cv::Mat marker;
    
public:
    
    //设置标记图
    void setMarkers(const cv::Mat& markers)
    {
        //watershed()的输入参数必须为一个32位有符号的标记,所以要先进行转换
        markers.convertTo(marker, CV_32S);
    }
    
    
    //watershed()
    cv::Mat process(const cv::Mat &image)
    {
        
        marker.convertTo(marker, CV_32S);
        cv::watershed(image, marker);
        
        return marker;
    }
    
    
    cv::Mat getSegmentation() {
        
        cv::Mat tmp;
        // 从32S到8U(0-255)会进行饱和运算,所以像素高于255的一律复制为255
        marker.convertTo(tmp,CV_8U);//
        
        return tmp;
    }
     cv::Mat getWatersheds()
        {
            
            cv::Mat tmp;
            //在设置标记图像,即执行setMarkers()后,边缘的像素会被赋值为-1,其他的用正整数表示
            //下面的这个转换可以让边缘像素变为-1*255+255=0,即黑色,其余的溢出,赋值为255,即白色。
            marker.convertTo(tmp, CV_8U,255,255);
            return tmp;
        }
    
    
};





#endif /* WatershedSegmenter_h */

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include "WatershedSegmenter.h"


using namespace cv;
using namespace std;


int main( )
{
    Mat image=imread("/Users/zhangxiaoyu/Desktop/1.png",0);//读取图像
    Mat image1=imread("/Users/zhangxiaoyu/Desktop/1.png");//读取图像
    
    if(image.empty())
    {
        cout<<"Error!cannot be read...../n";
        return -1;
    }
    
    
    cv::Mat binary;
    cv::threshold(image, binary, 105, 255, THRESH_BINARY_INV);
    
    
    //消除噪声和细小物体
    cv::Mat fg;
    cv::erode(binary, fg, cv::Mat(),cv::Point(-1,-1),4);
    
    
    //标示不含物体的图像像素
    cv::Mat bg;
    cv::dilate(binary,bg, cv::Mat(),cv::Point(-1,-1),4);
    cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);
    
    //创建标记图像
    cv::Mat markers(binary.size(),CV_8U,cv::Scalar(0));
    markers=fg+bg;
    
    //创建分水岭分割类的对象
    WatershedSegmenter segmenter;
    
    //设置标记图像,然后执行分割过程
    segmenter.setMarkers(markers);
    segmenter.process(image1);
    
    //显示分割结果
    cv::namedWindow("watersheds image");
    cv::imshow("watersheds image",segmenter.getWatersheds());
    
    waitKey(0);
    
}







     




2018-02-17 10:53:02 jacke121 阅读数 343
  • OpenCV3.2 Java图像处理视频学习教程

    OpenCV3.2 Java图像处理视频培训课程:基于OpenCV新版本3.2.0详细讲述Java OpenCV图像处理部分内容,包括Mat对象使用、图像读写、 基于常用核心API讲述基本原理、使用方法、参数、代码演示、图像处理思路与流程讲授。主要内容包括opencv像素操作、滤波、边缘提取、直线与圆检测、形态学操作与分水岭、图像金子塔融合重建、多尺度模板匹配、opencv人脸检测、OpenCV跟Tomcat使用实现服务器端图像处理服务。

    4245 人正在学习 去看看 贾志刚

python数字图像处理(19):骨架提取与分水岭算法

源码注解:http://blog.csdn.net/u011375993/article/details/46793655

源码:https://github.com/keke2014/Watershed

骨架提取与分水岭算法也属于形态学处理范畴,都放在morphology子模块内。

1、骨架提取

骨架提取,也叫二值图像细化。这种算法能将一个连通区域细化成一个像素的宽度,用于特征提取和目标拓扑表示。

morphology子模块提供了两个函数用于骨架提取,分别是Skeletonize()函数和medial_axis()函数。我们先来看Skeletonize()函数。

格式为:skimage.morphology.skeletonize(image)

输入和输出都是一幅二值图像。

例1:

复制代码
from skimage import morphology,draw
import numpy as np
import matplotlib.pyplot as plt

#创建一个二值图像用于测试
image = np.zeros((400, 400))

#生成目标对象1(白色U型)
image[10:-10, 10:100] = 1
image[-100:-10, 10:-10] = 1
image[10:-10, -100:-10] = 1

#生成目标对象2(X型)
rs, cs = draw.line(250, 150, 10, 280)
for i in range(10):
    image[rs + i, cs] = 1
rs, cs = draw.line(10, 150, 250, 280)
for i in range(20):
    image[rs + i, cs] = 1

#生成目标对象3(O型)
ir, ic = np.indices(image.shape)
circle1 = (ic - 135)**2 + (ir - 150)**2 < 30**2
circle2 = (ic - 135)**2 + (ir - 150)**2 < 20**2
image[circle1] = 1
image[circle2] = 0

#实施骨架算法
skeleton =morphology.skeletonize(image)

#显示结果
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(8, 4))

ax1.imshow(image, cmap=plt.cm.gray)
ax1.axis('off')
ax1.set_title('original', fontsize=20)

ax2.imshow(skeleton, cmap=plt.cm.gray)
ax2.axis('off')
ax2.set_title('skeleton', fontsize=20)

fig.tight_layout()
plt.show()
复制代码

生成一幅测试图像,上面有三个目标对象,分别进行骨架提取,结果如下:

例2:利用系统自带的马图片进行骨架提取

复制代码
from skimage import morphology,data,color
import matplotlib.pyplot as plt

image=color.rgb2gray(data.horse())
image=1-image #反相
#实施骨架算法
skeleton =morphology.skeletonize(image)

#显示结果
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(8, 4))

ax1.imshow(image, cmap=plt.cm.gray)
ax1.axis('off')
ax1.set_title('original', fontsize=20)

ax2.imshow(skeleton, cmap=plt.cm.gray)
ax2.axis('off')
ax2.set_title('skeleton', fontsize=20)

fig.tight_layout()
plt.show()
复制代码

medial_axis就是中轴的意思,利用中轴变换方法计算前景(1值)目标对象的宽度,格式为:

skimage.morphology.medial_axis(imagemask=Nonereturn_distance=False)

mask: 掩模。默认为None, 如果给定一个掩模,则在掩模内的像素值才执行骨架算法。

return_distance: bool型值,默认为False. 如果为True, 则除了返回骨架,还将距离变换值也同时返回。这里的距离指的是中轴线上的所有点与背景点的距离。

复制代码
import numpy as np
import scipy.ndimage as ndi
from skimage import morphology
import matplotlib.pyplot as plt

#编写一个函数,生成测试图像
def microstructure(l=256):
    n = 5
    x, y = np.ogrid[0:l, 0:l]
    mask = np.zeros((l, l))
    generator = np.random.RandomState(1)
    points = l * generator.rand(2, n**2)
    mask[(points[0]).astype(np.int), (points[1]).astype(np.int)] = 1
    mask = ndi.gaussian_filter(mask, sigma=l/(4.*n))
    return mask > mask.mean()

data = microstructure(l=64) #生成测试图像

#计算中轴和距离变换值
skel, distance =morphology.medial_axis(data, return_distance=True)

#中轴上的点到背景像素点的距离
dist_on_skel = distance * skel

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4))
ax1.imshow(data, cmap=plt.cm.gray, interpolation='nearest')
#用光谱色显示中轴
ax2.imshow(dist_on_skel, cmap=plt.cm.spectral, interpolation='nearest')
ax2.contour(data, [0.5], colors='w')  #显示轮廓线

fig.tight_layout()
plt.show()
复制代码

2、分水岭算法

分水岭在地理学上就是指一个山脊,水通常会沿着山脊的两边流向不同的“汇水盆”。分水岭算法是一种用于图像分割的经典算法,是基于拓扑理论的数学形态学的分割方法。如果图像中的目标物体是连在一起的,则分割起来会更困难,分水岭算法经常用于处理这类问题,通常会取得比较好的效果。

分水岭算法可以和距离变换结合,寻找“汇水盆地”和“分水岭界限”,从而对图像进行分割。二值图像的距离变换就是每一个像素点到最近非零值像素点的距离,我们可以使用scipy包来计算距离变换。

在下面的例子中,需要将两个重叠的圆分开。我们先计算圆上的这些白色像素点到黑色背景像素点的距离变换,选出距离变换中的最大值作为初始标记点(如果是反色的话,则是取最小值),从这些标记点开始的两个汇水盆越集越大,最后相交于分山岭。从分山岭处断开,我们就得到了两个分离的圆。

例1:基于距离变换的分山岭图像分割

复制代码
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage as ndi
from skimage import morphology,feature

#创建两个带有重叠圆的图像
x, y = np.indices((80, 80))
x1, y1, x2, y2 = 28, 28, 44, 52
r1, r2 = 16, 20
mask_circle1 = (x - x1)**2 + (y - y1)**2 < r1**2
mask_circle2 = (x - x2)**2 + (y - y2)**2 < r2**2
image = np.logical_or(mask_circle1, mask_circle2)

#现在我们用分水岭算法分离两个圆
distance = ndi.distance_transform_edt(image) #距离变换
local_maxi =feature.peak_local_max(distance, indices=False, footprint=np.ones((3, 3)),
                            labels=image)   #寻找峰值
markers = ndi.label(local_maxi)[0] #初始标记点
labels =morphology.watershed(-distance, markers, mask=image) #基于距离变换的分水岭算法

fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8, 8))
axes = axes.ravel()
ax0, ax1, ax2, ax3 = axes

ax0.imshow(image, cmap=plt.cm.gray, interpolation='nearest')
ax0.set_title("Original")
ax1.imshow(-distance, cmap=plt.cm.jet, interpolation='nearest')
ax1.set_title("Distance")
ax2.imshow(markers, cmap=plt.cm.spectral, interpolation='nearest')
ax2.set_title("Markers")
ax3.imshow(labels, cmap=plt.cm.spectral, interpolation='nearest')
ax3.set_title("Segmented")

for ax in axes:
    ax.axis('off')

fig.tight_layout()
plt.show()
复制代码

分水岭算法也可以和梯度相结合,来实现图像分割。一般梯度图像在边缘处有较高的像素值,而在其它地方则有较低的像素值,理想情况 下,分山岭恰好在边缘。因此,我们可以根据梯度来寻找分山岭。

例2:基于梯度的分水岭图像分割

复制代码
import matplotlib.pyplot as plt
from scipy import ndimage as ndi
from skimage import morphology,color,data,filter

image =color.rgb2gray(data.camera())
denoised = filter.rank.median(image, morphology.disk(2)) #过滤噪声

#将梯度值低于10的作为开始标记点
markers = filter.rank.gradient(denoised, morphology.disk(5)) <10
markers = ndi.label(markers)[0]

gradient = filter.rank.gradient(denoised, morphology.disk(2)) #计算梯度
labels =morphology.watershed(gradient, markers, mask=image) #基于梯度的分水岭算法

fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(6, 6))
axes = axes.ravel()
ax0, ax1, ax2, ax3 = axes

ax0.imshow(image, cmap=plt.cm.gray, interpolation='nearest')
ax0.set_title("Original")
ax1.imshow(gradient, cmap=plt.cm.spectral, interpolation='nearest')
ax1.set_title("Gradient")
ax2.imshow(markers, cmap=plt.cm.spectral, interpolation='nearest')
ax2.set_title("Markers")
ax3.imshow(labels, cmap=plt.cm.spectral, interpolation='nearest')
ax3.set_title("Segmented")

for ax in axes:
    ax.axis('off')

fig.tight_layout()
plt.show()
复制代码

2017-11-29 13:04:32 qq_33414271 阅读数 11666
  • OpenCV3.2 Java图像处理视频学习教程

    OpenCV3.2 Java图像处理视频培训课程:基于OpenCV新版本3.2.0详细讲述Java OpenCV图像处理部分内容,包括Mat对象使用、图像读写、 基于常用核心API讲述基本原理、使用方法、参数、代码演示、图像处理思路与流程讲授。主要内容包括opencv像素操作、滤波、边缘提取、直线与圆检测、形态学操作与分水岭、图像金子塔融合重建、多尺度模板匹配、opencv人脸检测、OpenCV跟Tomcat使用实现服务器端图像处理服务。

    4245 人正在学习 去看看 贾志刚

分水岭算法

Watershed Algorithm(分水岭算法),顾名思义,就是根据分水岭的构成来考虑图像的分割。现实中我们可以或者说可以想象有山有湖的景象,那么那一定是水绕 山,山围水的情形。当然在需要的时候,要人工构筑分水岭,以防集水盆之间的互相穿透。而区分高山(plateaus)与水的界线,以及湖与湖之间的间隔或 都是连通的关系,就是我们可爱的分水岭(watershed)。

如果图像中的目标物体是连接在一起的,则分割起来会更困难,分水岭分割算法经常用于处理这类问题,
通常会取得比较好的效果。分水岭分割算法把图像看成一幅“地形图”,
其中亮度比较强的区域像素值较大,而比较暗的区域像素值较小,通过寻找“汇水盆地”和“分水岭界限”,对图像进行分割。

案例

案例参考matlab官网案例,添加了详细注释,做出一定的调整,更容易让读者理解和接受。
实现功能
将明显的梨从一堆梨子中分离出来

最终结果
这里写图片描述

文末将回答一下几个问题
(1) 如果采用最大类间方差阈值分割方法进行分割,效果如何?为什么?
(2) 直接用分水岭分割把“pears.png”分割好么?为什么?
(3) 如何获得前景标记?
(4) Imregionalmax是什么作用,请举例说明。
(5) bwareaopen是什么作用,请举例说明。它是不是用数学形态学算法实现?
(6) 如何获得背景标记?
(7) 最终如何用前景标记和背景标记实现标记分水岭分割?

第一步:读入彩色图像,将其转化成灰度图像

clc; clear ; close ;
rgb = imread('pears.png');
if ndims(rgb) == 3
I = rgb2gray(rgb);
else
I = rgb;
end
figure('units', 'normalized','Name','图像读取:原图及灰度图比较');
subplot(1, 2, 1); imshow(rgb); title('原图');
subplot(1, 2, 2); imshow(I); title('灰度图');

这里写图片描述

第2步:方法1:将梯度幅值作为分割函数

使用Sobel边缘算子对图像进行水平和垂直方向的滤波,然后求取模值, sobel算子滤波后的图像在边界处会显示比较大的值,在没有边界处的值会很小。
第一种方法是直接对梯度幅值图像使用分水岭算法

hy = fspecial('sobel');
hx = hy';
Iy = imfilter(double(I), hy, 'replicate');
Ix = imfilter(double(I), hx, 'replicate');
gradmag = sqrt(Ix.^2 + Iy.^2);
figure('units', 'normalized','Name','直接计算梯度幅值作为分割函数的结果');
subplot(1, 3, 1); imshow(I,[]), title('灰度图像')
subplot(1, 3, 2); imshow(gradmag,[]), title('梯度幅值图像')
L = watershed(gradmag);
Lrgb = label2rgb(L);
subplot(1, 3, 3); imshow(Lrgb); title('梯度幅值做分水岭变换')

这里写图片描述
直接使用梯度模值图像进行分水岭算法得到的结果往往会存在过度分割的现象。因此通常需要分别对前景对象和背景对象进行标记,以获得更好的分割效果。

第3步:使用形态学技术“基于开的重建”和“基于闭的重建”来清理图像。

se = strel('disk', 20);
Io = imopen(I, se);
% 通过腐蚀后重建来做基于开的重建计算。
Ie = imerode(I, se);
Iobr = imreconstruct(Ie, I);
% 开操作后,接着进行闭操作,可以移除较暗的斑点和枝干标记。对比常规的形态学闭操作和基于闭的重建操作。
Ioc = imclose(Io, se);
Ic = imclose(I, se);
% 现在使用imdilate,然后使用imreconstruct。注意必须对输入图像求补,对imreconstruct输出图像求补。
% IM2 = imcomplement(IM)计算图像IM的补集。IM可以是二值图像,或者RGB图像。IM2与IM有着相同的数据类型和大小。
Iobrd = imdilate(Iobr, se);
Iobrcbr = imreconstruct(imcomplement(Iobrd), imcomplement(Iobr));
Iobrcbr = imcomplement(Iobrcbr);

figure('units', 'normalized','Name','比较各种形态学运算后的重建结果');
subplot(2, 3, 1); imshow(I, []); title('灰度图像');
subplot(2, 3, 2); imshow(Io, []); title('开操作图像');
subplot(2, 3, 3); imshow(Ic, []); title('闭操作图像');
subplot(2, 3, 4); imshow(Ioc, []), title('开闭操作');
subplot(2, 3, 5); imshow(Iobr, []); title('基于开的重建图像-腐蚀原图作为Marker');
subplot(2, 3, 6); imshow(Iobrcbr, []), title('基于闭的重建图像-膨胀开重建(取反)作为Marker');
% 通过比较Iobrcbr和loc可以看到,在移除小污点同时不影响对象全局形状的应用下,
% 基于重建的开闭操作要比标准的开闭重建更加有效。计算Iobrcbr的局部极大来得到更好的前景标记。

这里写图片描述
处理结果发现:基于重建的开闭操作要比标准的开闭重建更加有效。所以计算Iobrcbr的局部极大来得到更好的前景标记。

第4步:标记前景对象

有多种方法可以应用在这里来获得前景标记,这些标记必须是前景对象内部的连接斑点像素。 这些操作将会在每个对象内部创建单位极大值,使得可以使用imregionalmax来定位。

fgm = imregionalmax(Iobrcbr);
% 为了帮助理解这个结果,叠加前景标记到原图上。
It1 = rgb(:, :, 1);
It2 = rgb(:, :, 2);
It3 = rgb(:, :, 3);
It1(fgm) = 255; It2(fgm) = 0; It3(fgm) = 0;
I2 = cat(3, It1, It2, It3);

figure('units', 'normalized','Name','前景标记及调整');
subplot(3, 3, 1); imshow(I, []); title('灰度图像');
subplot(3, 3, 2); imshow(Iobrcbr, []); title('基于开闭的重建操作');
subplot(3, 3, 3); imshow(fgm, []); title('局部极大图像');
subplot(3, 3, 4); imshow(rgb, []); title('原图像');
subplot(3, 3, 5); imshow(I2); title('局部极大叠加到原图像');
% 注意到大多闭塞处和阴影对象没有被标记,这就意味着这些对象在结果中将不会得到合理的分割。
% 而且,一些对象的前景标记会一直到对象的边缘。
% 这就意味着应该清理标记斑点的边缘,然后收缩它们。可以通过闭操作和腐蚀操作来完成。
se2 = strel(ones(5,5));
fgm2 = imclose(fgm, se2);
fgm3 = imerode(fgm2, se2);
subplot(3, 3, 6); imshow(fgm2, []); title('闭操作后-局部极大图像');
subplot(3, 3, 7); imshow(fgm3, []); title('腐蚀操作后-局部极大图像');
% 这个过程将会留下一些偏离的孤立像素,应该移除它们。可以使用bwareaopen,用来移除少于特定像素个数的斑点。
% BW2 = bwareaopen(BW,P)从二值图像中移除所以少于P像素值的连通块,得到另外的二值图像BW2。
fgm4 = bwareaopen(fgm3, 20);
It1 = rgb(:, :, 1);
It2 = rgb(:, :, 2);
It3 = rgb(:, :, 3);
It1(fgm4) = 255; It2(fgm4) = 0; It3(fgm4) = 0;
I3 = cat(3, It1, It2, It3);
subplot(3, 3, 8); imshow(fgm4, []); title('进一步去除小斑点操作');
subplot(3, 3, 9); imshow(I3, []); title('修改后局部极大叠加到原图像');

这里写图片描述

第5步:计算背景标记

% 现在,需要标记背景。在清理后的图像Iobrcbr中,暗像素属于背景,所以可以从阈值操作开始。
bw =  imbinarize(Iobrcbr);
% 背景像素在黑色区域,但是理想情形下,不必要求背景标记太接近于要分割的对象边缘。
% 通过计算“骨架影响范围”来“细化”背景,或者SKIZ,bw的前景。这个可以通过计算bw的距离变换的分水岭变换来实现,
% 然后寻找结果的分水岭脊线(DL==0)。D = bwdist(BW)计算二值图像BW的欧几里得矩阵。对BW的每一个像素,
% 距离变换指定像素和最近的BW非零像素的距离。bwdist默认使用欧几里得距离公式。BW可以由任意维数,D与BW有同样的大小。
D = bwdist(bw);
DL = watershed(D);
bgm = DL == 0;
figure('units', 'normalized','Name','背景标记');
subplot(2, 2, 1); imshow(Iobrcbr, []); title('基于开闭的重建操作');
subplot(2, 2, 2); imshow(bw, []); title('阈值分割');
subplot(2, 2, 3); imshow(label2rgb(DL), []); title('分水岭变换示意图');
subplot(2, 2, 4); imshow(bgm, []); title('分水岭变换脊线图');

这里写图片描述

第6步:计算分割函数的分水岭变换

% 函数imimposemin可以用来修改图像,使其只是在特定的要求位置有局部极小。
% 这里可以使用imimposemin来修改梯度幅值图像,使其只在前景和后景标记像素有局部极小。
gradmag2 = imimposemin(gradmag, bgm | fgm4);
figure('units', 'normalized','Name','分割函数的分水岭变换对比');
subplot(2, 2, 1); imshow(bgm, []); title('分水岭变换脊线图-背景');
subplot(2, 2, 2); imshow(fgm4, []); title('前景标记');
subplot(2, 2, 3); imshow(gradmag, []); title('第一次梯度幅值图像');
subplot(2, 2, 4); imshow(gradmag2, []); title('修改后梯度幅值图像');

这里写图片描述

第7步:查看结果

% 一个可视化技术是叠加前景标记、背景标记、分割对象边界到初始图像。可
% 以使用膨胀来实现某些要求,比如对象边界,更加清晰可见。对象边界定位于L==0的位置。
L = watershed(gradmag2);
It1 = rgb(:,:, 1);
It2 = rgb(:, :, 2);
It3 = rgb(:, :, 3);
fgm5 = imdilate(L == 0, ones(3, 3)) | bgm | fgm4;
It1(fgm5) = 255; It2(fgm5) = 0; It3(fgm5) = 0;
I4 = cat(3, It1, It2, It3);
figure('units', 'normalized','Name','分割结果');
subplot(2, 2, 1); imshow(rgb, []); title('原图像');
subplot(2, 2, 2); imshow(I4, []); title('标记和对象边缘叠加到原图像');
% 另外一个有用的可视化技术是将标记矩阵作为彩色图像进行显示。标记矩阵,
% 比如通过watershed和bwlabel得到的,可以使用label2rgb转换到真彩图像来显示。
Lrgb = label2rgb(L, 'jet', 'w', 'shuffle');
subplot(2, 2, 3); imshow(Lrgb); title('彩色分水岭标记矩阵');
% 可以使用透明度来叠加这个伪彩色标记矩阵在原亮度图像上进行显示。
subplot(2, 2, 4); imshow(rgb, []); hold on;
himage = imshow(Lrgb);
set(himage, 'AlphaData', 0.3);
title('标记矩阵叠加到原图像');

这里写图片描述

问题解答:
(1) 如果采用最大类间方差阈值分割方法进行分割,效果如何?为什么?
最大类间方差阈值分割方法实际上是当做双峰分布,分割结果不理想。不适合做背景比较复杂的图像的分割

clc,clear
rgb=imread('pears.png');
I = rgb2gray(rgb);
imshow(I)

T=graythresh(I);%通过graythresh选择阈值
BW=im2bw(I,T);%用Otus阈值对图像进行分割
figure,imshow(BW);

(2) 直接用分水岭分割把“pears.png”分割好么?为什么?
直接分割会出现分割过度
(3) 如何获得前景标记?
1.首先使用形态学技术“基于开的重建”和“基于闭的重建”来清理图像。发现基于开+闭的重建效果最好
2.对重建后的图像在每个对象内部创建单位极大值,使得可以使用imregionalmax来定位
3.这个过程将会留下一些偏离的孤立像素,应该移除它们。可以使用bwareaopen,用来移除少于特定像素个数的斑点。
(4) Imregionalmax是什么作用,请举例说明。
使得可以使用imregionalmax来定位极大值和极小值

A = 10*ones(10,10);
A(2:4,2:4) = 22; 
A(6:8,6:8) = 33; 
A(2,7) = 44;
A(3,8) = 45;
A(4,9) = 44

这里写图片描述

regmax = imregionalmax(A)

这里写图片描述
(5) bwareaopen是什么作用,请举例说明。它是不是用数学形态学算法实现?
BW2 = bwareaopen(BW,P)从二值图像中移除所以少于P像素值的连通块,得到另外的二值图像BW2。
感觉是形态学的方法
官方Example
(6) 如何获得背景标记?

% 现在,需要标记背景。在清理后的图像Iobrcbr中,暗像素属于背景,所以可以从阈值操作开始。
bw = im2bw(Iobrcbr, graythresh(Iobrcbr));
% 背景像素在黑色区域,但是理想情形下,不必要求背景标记太接近于要分割的对象边缘。
% 通过计算“骨架影响范围”来“细化”背景,或者SKIZ,bw的前景。这个可以通过计算bw的距离变换的分水岭变换来实现,
% 然后寻找结果的分水岭脊线(DL==0)。D = bwdist(BW)计算二值图像BW的欧几里得矩阵。对BW的每一个像素,
% 距离变换指定像素和最近的BW非零像素的距离。bwdist默认使用欧几里得距离公式。BW可以由任意维数,D与BW有同样的大小。
D = bwdist(bw);
DL = watershed(D);
bgm = DL == 0;
figure('units', 'normalized','Name','背景标记');
subplot(2, 2, 1); imshow(Iobrcbr, []); title('基于重建的开闭操作');
subplot(2, 2, 2); imshow(bw, []); title('阈值分割');
subplot(2, 2, 3); imshow(label2rgb(DL), []); title('分水岭变换示意图');
subplot(2, 2, 4); imshow(bgm, []); title('分水岭变换脊线图');

(7) 最终如何用前景标记和背景标记实现标记分水岭分割?
函数imimposemin可以用来修改图像,使其只是在特定的要求位置有局部极小。
这里可以使用imimposemin来修改梯度幅值图像,使其只在前景和后景标记像素有局部极小。

gradmag2 = imimposemin(gradmag, bgm | fgm4);
L = watershed(gradmag2);

完整的代码:https://code.csdn.net/snippets/2603174

参考
http://blog.sina.cn/dpool/blog/s/blog_725866260100rz7x.html

没有更多推荐了,返回首页