形态学在图像处理中的应用
腐蚀和膨胀
理论基础
形态学操作
- 简而言之:一系列操作基于形状来操作图像,形态学操作通过在图像上应用结构元素来产生输出图像。
- 最基础的形态学操作包括腐蚀和扩张。它包含广泛的应用:
- 移除噪声
- 孤立一些单独的元素和聚合一些分散的元素
- 找到图像中的局部块状或者孔
- 我们将使用下面的图像简要的介绍膨胀和腐蚀
膨胀
- 这一操作包含使用卷积核B对图片A进行卷积运算,这个卷积核可以有任意的形状和大小,通常是一个方形或者圆形。
- 卷积核B通常有个锚点,通常位于卷积核的中央位置。
- 随着卷积核扫描这个图像,我们计算叠加区域的最大像素值,并将锚点的位置用最大值替换。这样你可以推断最大化操作导致图片中亮的区域增长(所以这里面叫做膨胀)。举个例子,应用膨胀我们可以得到:
我们看到背景亮的区域膨胀变大。
为了掌握这一思想,避免可能的混淆,我们在另外一个例子中反转了原来的图像,现在白色代表原先的字。我们使用3*3矩形元素的元素进行膨胀操作两次。
左侧是反转之后的图像-右侧是膨胀之后的图像
膨胀使得对象的白色区域变大。
腐蚀
- 腐蚀与膨胀类似。它是计算卷积核里面的最小元素。
- 随着卷积核B扫描图片,它会计算B叠加区域的最小像素值,并使用这个像素值替换锚点的值。
- 与膨胀相似,对原始的图像应用腐蚀操作。你可以看到背景亮的区域变小,而黑的区域变得很大。
同样对原始图像进行反转,进行腐蚀操作,得到:
左侧为原始图像反转的图像-右侧为腐蚀的结果
腐蚀使得对象白色变小。
代码
示例代码如下,你可以从这里下载代码
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
using namespace cv;
Mat src, erosion_dst, dilation_dst;
int erosion_elem = 0;
int erosion_size = 0;
int dilation_elem = 0;
int dilation_size = 0;
int const max_elem = 2;
int const max_kernel_size = 21;
void Erosion( int, void* );
void Dilation( int, void* );
int main( int, char** argv )
{
src = imread( argv[1], IMREAD_COLOR );
if( src.empty() )
{ return -1; }
namedWindow( "Erosion Demo", WINDOW_AUTOSIZE );
namedWindow( "Dilation Demo", WINDOW_AUTOSIZE );
moveWindow( "Dilation Demo", src.cols, 0 );
createTrackbar( "Element:\n 0: Rect \n 1: Cross \n 2: Ellipse", "Erosion Demo",
&erosion_elem, max_elem,
Erosion );
createTrackbar( "Kernel size:\n 2n +1", "Erosion Demo",
&erosion_size, max_kernel_size,
Erosion );
createTrackbar( "Element:\n 0: Rect \n 1: Cross \n 2: Ellipse", "Dilation Demo",
&dilation_elem, max_elem,
Dilation );
createTrackbar( "Kernel size:\n 2n +1", "Dilation Demo",
&dilation_size, max_kernel_size,
Dilation );
Erosion( 0, 0 );
Dilation( 0, 0 );
waitKey(0);
return 0;
}
void Erosion( int, void* )
{
int erosion_type = 0;
if( erosion_elem == 0 ){ erosion_type = MORPH_RECT; }
else if( erosion_elem == 1 ){ erosion_type = MORPH_CROSS; }
else if( erosion_elem == 2) { erosion_type = MORPH_ELLIPSE; }
Mat element = getStructuringElement( erosion_type,
Size( 2*erosion_size + 1, 2*erosion_size+1 ),
Point( erosion_size, erosion_size ) );
erode( src, erosion_dst, element );
imshow( "Erosion Demo", erosion_dst );
}
void Dilation( int, void* )
{
int dilation_type = 0;
if( dilation_elem == 0 ){ dilation_type = MORPH_RECT; }
else if( dilation_elem == 1 ){ dilation_type = MORPH_CROSS; }
else if( dilation_elem == 2) { dilation_type = MORPH_ELLIPSE; }
Mat element = getStructuringElement( dilation_type,
Size( 2*dilation_size + 1, 2*dilation_size+1 ),
Point( dilation_size, dilation_size ) );
dilate( src, dilation_dst, element );
imshow( "Dilation Demo", dilation_dst );
}
代码说明
- 所有的这些东西如果你不清楚,可以参照前面的介绍。我们先看一看程序的结构:
- 加载图像(可以是BGR也可以是灰度图像)
- 创建两个窗口(一个是膨胀输出,一个是腐蚀输出)
- 对于每个操作常见一组滑动条
- 第一个元素返回元素类型erosion_elem 或者dilation_elem
- 第二个元素返回卷积核的大小
- 当我们移动滑动条的时候,用户的函数Erosion 和Dilation 会被调用,它会根据滑动条上的值更新输出图像
- 腐蚀
void Erosion( int, void* )
{
int erosion_type = 0;
if( erosion_elem == 0 ){ erosion_type = MORPH_RECT; }
else if( erosion_elem == 1 ){ erosion_type = MORPH_CROSS; }
else if( erosion_elem == 2) { erosion_type = MORPH_ELLIPSE; }
Mat element = getStructuringElement( erosion_type,
Size( 2*erosion_size + 1, 2*erosion_size+1 ),
Point( erosion_size, erosion_size ) );
erode( src, erosion_dst, element );
imshow( "Erosion Demo", erosion_dst );
}
该函数使用cv::erode进行腐蚀操作,它接收三个参数:
- src:源图像
- erosion_dst:目标输出图像
- element:我们将使用的卷积核。如果不指定,将使用3*3的矩阵。我们可以指定形状。为了指定形状,我们可以使用cv::getStructuringElement 函数:
Mat element = getStructuringElement( erosion_type, Size( 2*erosion_size + 1, 2*erosion_size+1 ), Point( erosion_size, erosion_size ) );
我们可以为卷积核选择以下形状:
- 矩形框:MORPH_RECT
- 十字框:MORPH_CROSS
- 椭圆框:MORPH_ELLIPSE
然后我们只需指定核的大小和锚点。如果没有指定,假定在中心。- 指定这些值之后,我们就可以进行图像的腐蚀操作了。
3.膨胀
膨胀的算法与腐蚀的算法类似。
void Dilation( int, void* )
{
int dilation_type = 0;
if( dilation_elem == 0 ){ dilation_type = MORPH_RECT; }
else if( dilation_elem == 1 ){ dilation_type = MORPH_CROSS; }
else if( dilation_elem == 2) { dilation_type = MORPH_ELLIPSE; }
Mat element = getStructuringElement( dilation_type,
Size( 2*dilation_size + 1, 2*dilation_size+1 ),
Point( dilation_size, dilation_size ) );
dilate( src, dilation_dst, element );
imshow( "Dilation Demo", dilation_dst );
}
结果
编译代码,运行时使用以下图片:
调整滑动条的参数值,得到类似下面结果:
更多形态学转换
理论基础
在上面我们已经了解到形态学两种基本操作:
- 膨胀
- 腐蚀
在这两种基础操作的基础之上,我们探究图像的更为复杂的转换。这里我们讨论OpenCV提供的5中操作。
开操作
- 它通过先进性腐蚀操作,再进行膨胀操作得到
- 在移除小的对象时候很有用(假设物品是亮色,前景色是黑色)
- 如下所示。左侧是原始图像右侧是应用开操作之后的图像。我们可以看到左侧图像的小的空间消失
为了更清晰,对原先对象进行反转,然后再进行开操作,结果如下:
左侧为原始图像反转-右侧为开操作之后的图像
闭操作
比操作是先进行膨胀然后进行腐蚀操作
dst = close( src, element ) = erode( dilate( src, element ) )有利于移除小的洞(黑色区域)
便于说明问题,对反转的图像进行闭操作
左侧反转图像-右侧为进行闭操作之后的图像
梯度操作
- 是膨胀操作与腐蚀操作的差
dst=morphgrad(src,element)=dilate(src,element)−erode(src,element) - 对于寻找对象的轮廓很有用,如下:
顶帽操作
- 是原图像与开操作对象的差
dst=tophat(src,element)=src−open(src,element)
黑帽操作
是闭操作与原始图像的差值
dst=blackhat(src,element)=close(src,element)−src
示例代码
教程代码如下,你可以从这里下载
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
using namespace cv;
Mat src, dst;
int morph_elem = 0;
int morph_size = 0;
int morph_operator = 0;
int const max_operator = 4;
int const max_elem = 2;
int const max_kernel_size = 21;
const char* window_name = "Morphology Transformations Demo";
void Morphology_Operations( int, void* );
int main( int, char** argv )
{
src = imread( argv[1], IMREAD_COLOR ); // Load an image
if( src.empty() )
{ return -1; }
namedWindow( window_name, WINDOW_AUTOSIZE ); // Create window
createTrackbar("Operator:\n 0: Opening - 1: Closing \n 2: Gradient - 3: Top Hat \n 4: Black Hat", window_name, &morph_operator, max_operator, Morphology_Operations );
createTrackbar( "Element:\n 0: Rect - 1: Cross - 2: Ellipse", window_name,
&morph_elem, max_elem,
Morphology_Operations );
createTrackbar( "Kernel size:\n 2n +1", window_name,
&morph_size, max_kernel_size,
Morphology_Operations );
Morphology_Operations( 0, 0 );
waitKey(0);
return 0;
}
void Morphology_Operations( int, void* )
{
// Since MORPH_X : 2,3,4,5 and 6
int operation = morph_operator + 2;
Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) );
morphologyEx( src, dst, operation, element );
imshow( window_name, dst );
}
代码阐述
我们来看一下程序的结构:
- 加载图像
- 创建一个窗口展示形态学操作结果
创建三个滑动按钮来输入参数
- 第一个滑动按钮返回形态学操作类型,使用morph_operator
createTrackbar("Operator:\n 0: Opening - 1: Closing \n 2: Gradient - 3: Top Hat \n 4: Black Hat", window_name, &morph_operator, max_operator, Morphology_Operations );
- 第二个滑动条参数返回morph_elem,它表示卷积核的类型
createTrackbar( "Element:\n 0: Rect - 1: Cross - 2: Ellipse", window_name,
&morph_elem, max_elem,
Morphology_Operations ); - 最后一个参数表示morph_size表示卷积核的大小
createTrackbar( "Kernel size:\n 2n +1", window_name,
&morph_size, max_kernel_size,
Morphology_Operations );
- 第一个滑动按钮返回形态学操作类型,使用morph_operator
每次你移动滑动条的时候,Morphology_Operations 函数会被调用,使得新的形态学操作有效,它会根据当前滑动按钮的值输出图像
void Morphology_Operations( int, void* )
{
// Since MORPH_X : 2,3,4,5 and 6
int operation = morph_operator + 2;
Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) );
morphologyEx( src, dst, operation, element );
imshow( window_name, dst );
}
我们可以看到执行形态学转换的例子的关键函数是cv::morphologyEx,在这里我们指定了四个参数(剩余的使用默认值):- src,源输入图像
- dst,输出文件
- operation,执行的形态学转换。注意我们有5个可选项:
- 开操作: MORPH_OPEN : 2
- 闭操作: MORPH_CLOSE: 3
- 梯度操作: MORPH_GRADIENT: 4
- 顶帽操作: MORPH_TOPHAT: 5
- 黑帽操作: MORPH_BLACKHAT: 6
这里的值从2到6,这就是前面函数中加2的原因。
int operation = morph_operator + 2;
- element,卷积核,我们使用cv::getStructuringElement来定义元素结构
结果
在编译完代码之后,我们使用以下图片作为输入参数:
- 这里我们显示了两个窗口截图第一幅图展示了使用十字形卷积核进行开操作的输出结果,第二幅展示了使用椭圆形卷积核进行黑帽操作得出的结果。
- 这里我们显示了两个窗口截图第一幅图展示了使用十字形卷积核进行开操作的输出结果,第二幅展示了使用椭圆形卷积核进行黑帽操作得出的结果。
利用形态学操作抽取水平和竖直线
理论
形态学操作
形态学是一系列的图像处理操作,这些图像处理基于预先定义好的结构核。输出图像中的每个像素的值由输入图像的响应像素和周边像素决定。通过选择核的尺寸和大小,你可以构造对于输入图像中特定形状敏感的形态学操作。
形态学中,最基础的两个操作是膨胀和腐蚀。膨胀向图像中的对象周边添加像素,腐蚀恰恰相反,添加或者移除 的多少依赖于构造的核的形状和大小,一般来说,这两项操作遵循以下规则:
- 膨胀:输出的像素值是落在核内的最大值。例如一个二值图像,如果有一个落在核内的像素值是1,那么相应的输出图像的像素值为1.
二值图像上的膨胀
灰度图像上的膨胀
- 腐蚀:腐蚀与膨胀恰恰相反,取最小值
二值图像的腐蚀
灰度图像的腐蚀
- 构造核
通过以上可以看出,一般的形态学操作都使用构造的核来探测输出的图像,它是最重要的。这个核通常只包含0和1两个元素,可以拥有任意的形状和大小。通常来说,要比输入的 图像小的多,值为1的元素定义邻居。核的中心元素,也叫原始元素决定要处理的元素。
例如下面是一个7*7大小的菱形核。
菱形核和它的起始点
构造的核元素可以有许多形状,例如线形,菱形,周期性线性,磁盘形,圆形等。通常形状和大小和输入图像中要处理的对象类似。例如,要发现图像中的线性,需要构造出线性的核,你在后面将会看到。
示例代码
示例代码如下,你可以在这里下载。
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int, char** argv)
{
// Load the image
Mat src = imread(argv[1]);
// Check if image is loaded fine
if(!src.data)
cerr << "Problem loading image!!!" << endl;
// Show source image
imshow("src", src);
// Transform source image to gray if it is not
Mat gray;
if (src.channels() == 3)
{
cvtColor(src, gray, CV_BGR2GRAY);
}
else
{
gray = src;
}
// Show gray image
imshow("gray", gray);
// Apply adaptiveThreshold at the bitwise_not of gray, notice the ~ symbol
Mat bw;
adaptiveThreshold(~gray, bw, 255, CV_ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);
// Show binary image
imshow("binary", bw);
// Create the images that will use to extract the horizontal and vertical lines
Mat horizontal = bw.clone();
Mat vertical = bw.clone();
// Specify size on horizontal axis
int horizontalsize = horizontal.cols / 30;
// Create structure element for extracting horizontal lines through morphology operations
Mat horizontalStructure = getStructuringElement(MORPH_RECT, Size(horizontalsize,1));
// Apply morphology operations
erode(horizontal, horizontal, horizontalStructure, Point(-1, -1));
dilate(horizontal, horizontal, horizontalStructure, Point(-1, -1));
// Show extracted horizontal lines
imshow("horizontal", horizontal);
// Specify size on vertical axis
int verticalsize = vertical.rows / 30;
// Create structure element for extracting vertical lines through morphology operations
Mat verticalStructure = getStructuringElement(MORPH_RECT, Size( 1,verticalsize));
// Apply morphology operations
erode(vertical, vertical, verticalStructure, Point(-1, -1));
dilate(vertical, vertical, verticalStructure, Point(-1, -1));
// Show extracted vertical lines
imshow("vertical", vertical);
// Inverse vertical image
bitwise_not(vertical, vertical);
imshow("vertical_bit", vertical);
// Extract edges and smooth image according to the logic
// 1. extract edges
// 2. dilate(edges)
// 3. src.copyTo(smooth)
// 4. blur smooth img
// 5. smooth.copyTo(src, edges)
// Step 1
Mat edges;
adaptiveThreshold(vertical, edges, 255, CV_ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, -2);
imshow("edges", edges);
// Step 2
Mat kernel = Mat::ones(2, 2, CV_8UC1);
dilate(edges, edges, kernel);
imshow("dilate", edges);
// Step 3
Mat smooth;
vertical.copyTo(smooth);
// Step 4
blur(smooth, smooth, Size(2, 2));
// Step 5
smooth.copyTo(vertical, edges);
// Show final result
imshow("smooth", vertical);
waitKey(0);
return 0;
}
阐述和结果
- 载入原始图像,并检查它是否加载成功,紧接着进行展示:
// Load the image
Mat src = imread(argv[1]);
// Check if image is loaded fine
if(!src.data)
cerr << "Problem loading image!!!" << endl;
// Show source image
imshow("src", src);
2. 如果图像没有转换,将图像转为灰度图像
// Transform source image to gray if it is not
Mat gray;
if (src.channels() == 3)
{
cvtColor(src, gray, CV_BGR2GRAY);
}
else
{
gray = src;
}
// Show gray image
imshow("gray", gray);
3. 之后将灰度图像转为二值图像。注意到符号~表示取反:
// Apply adaptiveThreshold at the bitwise_not of gray, notice the ~ symbol
Mat bw;
adaptiveThreshold(~gray, bw, 255, CV_ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);
// Show binary image
imshow("binary", bw);
4. 现在我们已经准备好了提取水平线和竖直线,同时紧接着会将音符从音符本中提取出来,但是首先初始化输出的图像
// Create the images that will use to extract the horizontal and vertical lines
Mat horizontal = bw.clone();
Mat vertical = bw.clone();
5. 前面已经介绍为了提取想要的对象,我们必须构造相应的核,为了提取水平线我们构造下面形状的核:
在源代码中通过以下代码段展示:
// Specify size on horizontal axis
int horizontalsize = horizontal.cols / 30;
// Create structure element for extracting horizontal lines through morphology operations
Mat horizontalStructure = getStructuringElement(MORPH_RECT, Size(horizontalsize,1));
// Apply morphology operations
erode(horizontal, horizontal, horizontalStructure, Point(-1, -1));
dilate(horizontal, horizontal, horizontalStructure, Point(-1, -1));
// Show extracted horizontal lines
imshow("horizontal", horizontal);
6. 竖直线处理类似:
// Specify size on vertical axis
int verticalsize = vertical.rows / 30;
// Create structure element for extracting vertical lines through morphology operations
Mat verticalStructure = getStructuringElement(MORPH_RECT, Size( 1,verticalsize));
// Apply morphology operations
erode(vertical, vertical, verticalStructure, Point(-1, -1));
dilate(vertical, vertical, verticalStructure, Point(-1, -1));
// Show extracted vertical lines
imshow("vertical", vertical);
7. 如你所看到的那样,音符的边缘很粗糙。你需要细化边缘来获得平滑的结果:
// Inverse vertical image
bitwise_not(vertical, vertical);
imshow("vertical_bit", vertical);
// Extract edges and smooth image according to the logic
// 1. extract edges
// 2. dilate(edges)
// 3. src.copyTo(smooth)
// 4. blur smooth img
// 5. smooth.copyTo(src, edges)
// Step 1
Mat edges;
adaptiveThreshold(vertical, edges, 255, CV_ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, -2);
imshow("edges", edges);
// Step 2
Mat kernel = Mat::ones(2, 2, CV_8UC1);
dilate(edges, edges, kernel);
imshow("dilate", edges);
// Step 3
Mat smooth;
vertical.copyTo(smooth);
// Step 4
blur(smooth, smooth, Size(2, 2));
// Step 5
smooth.copyTo(vertical, edges);
// Show final result
imshow("smooth", vertical);