数学形态学是一门20c60s发展起来的理论,用于分析和处理离散图像。它定义了一系列运算,用预先定义的形状元素探测图像,从而实现图像的转换。这个结构元素与像素领域的相交方式决定了运算的结果。
*形态学滤波器服饰和膨胀图像
腐蚀和膨胀是最基本的形态学运算。数学形态学中最基本的概念是结构元素。结构元素可以简单地定义为像素的组合,在对应的像素上定义了一个原点。形态学滤波器的应用过程就包含了用这个结构元素探测图像中每个像素的操作过程。(结构元素原则上可以是任何形状,但通常它是一个简单形状,如正方形、圆形或菱形,并且把中心作为原点)
实现过程:
首先我们从百度上下载一张图:
然后通过阈值化创建二值图像
(在形态学中,习惯用高像素值白色表示前景物体,低像素值表示背景物体)
得到如下图像:
接下来就是实现腐蚀和膨胀了,在OpenCV中,实现两功能的函数分别为cv::erode和cv::dilate
先看结果:
代码:
int main()
{
cv::Mat image = cv::imread("cow.jpg");
cv::imshow("原图", image);
cv::cvtColor(image, image, CV_BGR2GRAY);
cv::threshold(image, image, 75, 255, cv::THRESH_BINARY_INV);
cv::imshow("二值图像", image);
cv::Mat eroded, dilated;
cv::erode(image, eroded, cv::Mat());
cv::dilate(image, dilated, cv::Mat());
cv::imshow("腐蚀图", eroded);
cv::imshow("膨胀图", dilated);
cvWaitKey();
}
腐蚀就是把当前像素替换成所定义像素集合中的最小像素值,腐蚀时,如果结构元素放到某个像素位置时碰到了背景,那么这个像素就会变成背景。
膨胀是浮士德反运算,它把当前像素替换成所定义像素集合中的最大像素值,膨胀时,如果结构元素放到某个背景像素位置时碰到了前景物体,那么这个像素就会标为白色。
可见,腐蚀和膨胀的程度与结构元素的大小有关,OpenCV默认的是3*3正方形结构元素。
若是需要更改结构元素的大小,可以按照下面的代码:
cv::Mat element(7, 7, CV_8U, cv::Scalar(1));
cv::erode(image, eroded, element);
当然多次腐蚀/膨胀可以达到同样的效果
*用形态学滤波器开启和闭合图像
为了应用较高级别的形态学滤波器,需要用cv::morphologyEx函数
开启图像:
闭合图像:
代码:
cv::Mat opened, closed;
cv::Mat element5(5, 5, CV_8U, cv::Scalar(1));
cv::morphologyEx(image, opened, cv::MORPH_OPEN, element5);
cv::imshow("开启图像", opened);
cv::morphologyEx(image, closed, cv::MORPH_CLOSE, element5);
cv::imshow("闭合图像",closed);
继续向后阅览,也是验证了我的想法。
闭合的定义是对图像先膨胀后腐蚀,开启的定义是对图像先腐蚀后膨胀。
用途:
闭合滤波器->可用于把错误分裂成小碎片的物体连接起来
开启滤波器->可用于移除因图像噪声产生的斑点
一般使用顺序为先闭合滤波器,后开启滤波器(个人总结,因为先使用开启滤波器会消除掉部分物体碎片)
*用形态学滤波器检测边缘和角点
这里需要用到一张建筑图片,于是我请出了在一卡通上跟随我多年的老图书馆照(第一个大创项目的登陆界面也用的这个),这样:
边缘检测结果:
为了用形态学检测角点,定义了MorphoFeatures类。
角点检测结果:
可以看到,大部分角点能检测出来,不过也有误差(这种效果用来特征匹配用于视觉里程计应该是没问题了)
代码:
MorphoFeatures类:
class MorphoFeatures
{
private:
int threshold;
cv::Mat_<uchar> cross;
cv::Mat_<uchar> diamond;
cv::Mat_<uchar> square;
cv::Mat_<uchar> x;
public:
MorphoFeatures() : threshold(-1), cross(5, 5), diamond(5, 5), square(5, 5), x(5, 5)
{
//创建十字形结构元素
cross <<
0, 0, 1, 0, 0,
0, 0, 1, 0, 0,
1, 1, 1, 1, 1,
0, 0, 1, 0, 0,
0, 0, 1, 0, 0;
diamond <<
0, 0, 1, 0, 0,
0, 1, 0, 1, 0,
1, 0, 0, 0, 1,
0, 1, 0, 1, 0,
0, 0, 1, 0, 0;
square <<
1, 1, 1, 1, 1,
1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 1, 1, 1, 1;
x <<
1, 0, 0, 0, 1,
0, 1, 0, 1, 0,
0, 0, 1, 0, 0,
0, 1, 0, 1, 0,
1, 0, 0, 0, 1;
}
cv::Mat getCorners(const cv::Mat& image, int threshold)
{
cv::Mat result;
//用十字元素膨胀
cv::dilate(image, result, cross);
//用菱形元素腐蚀
cv::erode(result, result, diamond);
cv::Mat result2;
//用X元素膨胀
cv::dilate(image, result2, x);
//用正方形元素腐蚀
cv::dilate(result2, result2, square);
//比较两个经过闭合运算的图像,得到角点
cv::absdiff(result2, result, result);
//获得二值图像
cv::threshold(result, result, threshold, 255, cv::THRESH_BINARY_INV);
return result;
}
};
int main()
{
cv::Mat image = cv::imread("old_library.jpg");
cv::imshow("原图", image);
cv::cvtColor(image, image, CV_BGR2GRAY);
cv::Mat result;
cv::morphologyEx(image, result, cv::MORPH_GRADIENT, cv::Mat());
cv::threshold(result, result, 90, 255, cv::THRESH_BINARY_INV);
cv::imshow("边缘", result);
MorphoFeatures mpf;
cv::Mat corners = mpf.getCorners(result,20);
cv::Mat image2 = cv::imread("old_library.jpg");
image2.copyTo(result, corners);
cv::imshow("角点检测", result);
cvWaitKey();
}
*分水岭算法实现图像分割
分水岭算法是一种流行的图像处理算法,用于快速将图像分割为多个同质区域。(久仰大名)
OpenCV给出的是分水岭算法的改进版(原始版本会过度分割图像),调用cv::watershed函数。该函数的输入对象是一个标记图像,图像的像素值为32位有符号整数,每个非零像素代表一个标签。
这里使用牛吃草的图,其二值化图像:
现在二值化图像包含了太多属于图像不同部分的白色像素,因此要对图像做深度腐蚀运算,只保留属于重点物体的像素,效果如下:
类似地,通过对原二值图像做大幅度膨胀运算来选中一些背景像素:
灰色的为背景天空
合并两个图像得到标记图像:
将标记图像作为分水岭函数的输入参数,得到标签图像:
也可用分水岭线条表示:
代码:
WatershedSegmenter类:
class WatershedSegmenter
{
private:
cv::Mat marker;
public:
void setMarkers(const cv::Mat& markerImage)
{
markerImage.convertTo(marker,CV_32S);
}
void showMarkers()
{
cv::imshow("markers", marker);
}
void process(const cv::Mat& image)
{
cv::watershed(image, marker);
}
cv::Mat getsegmentation()
{
cv::Mat tmp;
marker.convertTo(tmp, CV_8U);
return tmp;
}
cv::Mat getWatersheds()
{
cv::Mat tmp;
marker.convertTo(tmp, CV_8U, 255, 255);
return tmp;
}
};
main函数:
int main()
{
cv::Mat image = cv::imread("cow.jpg");
cv::cvtColor(image, image, CV_BGR2GRAY);
cv::threshold(image, image, 90, 255, cv::THRESH_BINARY_INV);
cv::imshow("二值化图像", image);
cv::Mat eroded;
cv::erode(image, eroded, cv::Mat(), cv::Point(-1, -1), 4);
cv::imshow("深度腐蚀图像", eroded);
cv::Mat dilated;
cv::dilate(image, dilated, cv::Mat(), cv::Point(-1, -1), 4);
cv::threshold(dilated, dilated, 1, 128, cv::THRESH_BINARY_INV);
cv::imshow("背景图像",dilated);
cv::Mat markers(image.size(), CV_8U, cv::Scalar(0));
markers = eroded + dilated;
cv::imshow("标记图像", markers);
WatershedSegmenter segmenter;
segmenter.setMarkers(markers);
segmenter.showMarkers();
segmenter.process(image);
cv::imshow("分水岭图像", segmenter.getsegmentation());
cv::imshow("分水岭线条", segmenter.getWatersheds());
cvWaitKey();
}
错误记录:实际程序运行到cv::watershed(image,marker)这一句时编译器会报错,但是跳过执行之后仍能得到正确的结果,比较令人困惑。
*用MSER算法提取特征区域
最大稳定极限区域(MSER)算法也用相同的水淹类比,以便从图像中提取有意义的区域,创建这些区域时也使用逐步提高水位的方法,但是MSER关注的是在水淹过程中的某个时间段内保持相对稳定的盆地,这些区域对应图像中某些物体的独特部分。
检测结果:
可以看出,这里能大致检测出窗户这些特殊部分,并用随机颜色标注。
版本更改说明:
OpenCV2的MSER基础类是cv::MSER,可直接用其实例化对象,在OpenCV3.3中报错提示MSER类含纯虚函数,即MSER为抽象类,不能实例化,后查阅OpenCV官方文档(传送门),发现其与之前版本有较大的更改
参数含义:
最新版本中,使用cv::MSER::create()方法返回一个MSER指针,而检测MSER特征则是使用的如下函数:
参数含义:
该函数同样是一个纯虚函数,所以需要用上个函数返回的指针变量来使用该纯虚函数:mser->detectRegions(image,points,bboxes);在这里,bboxes的含义我不是很清楚,但是仅就定义一下不去理会似乎也能得到想要的结果。
代码:
int main()
{
cv::Mat image = cv::imread("old_library.jpg");
cv::Ptr<cv::MSER> mser = cv::MSER::create(5,//检测极值区域时使用的增量
200,//允许的最小面积
1500);//允许的最大面积
std::vector<std::vector<cv::Point>> points;
std::vector<cv::Rect> bboxes;
mser->detectRegions(image, points, bboxes);
cv::Mat output(image.size(), CV_8UC3);
output = cv::Scalar(255, 255, 255);
//随机数生成器
cv::RNG rng;
for (std::vector<std::vector<cv::Point>>::iterator it = points.begin(); it != points.end(); ++it)
{
//生成随机颜色
cv::Vec3b color(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
for (std::vector<cv::Point>::iterator itPts = it->begin(); itPts != it->end(); itPts++)
{
if (output.at<cv::Vec3b>(*itPts)[0] == 255)
{
output.at<cv::Vec3b>(*itPts) = color;
}
}
}
cv::imshow("detected result", output);
cvWaitKey();
}
MSER检测的结果是一个包含点集的容器。由于我们通常更关心区域的整体而不是单个像素的位置,因此普遍采用含有位置和大小信息的单一的几何图形来表示MSER,常用的形状是带边缘的椭圆,画椭圆的过程封装在MSERFeatures类中。
检测结果:
代码:
MSERFeatures类:
class MSERFeatures
{
private:
cv::Ptr<cv::MSER> mser = cv::MSER::create();
double minAreaRatio;
public:
MSERFeatures(
int minArea = 60, int maxArea = 14400,
double minAreaRatio = 0.5,
int delta = 5,
double maxVariation = 0.25,
double minDiversity = 0.2) :
minAreaRatio(minAreaRatio)
{
mser = cv::MSER::create(delta, minArea, maxArea, maxVariation, minDiversity);
}
//得到对应每个MSER特征的旋转带边框的矩形
//如果(MSER面积/矩形面积) < areaRatio,就清除这个特征
void getBoundingRects(const cv::Mat& image, std::vector<cv::RotatedRect>& rects)
{
//检测MSER特征
std::vector<std::vector<cv::Point>> points;
std::vector<cv::Rect> bboxes;
mser->detectRegions(image, points, bboxes);
//针对某个检测到的特征
for (std::vector<std::vector<cv::Point>>::iterator it = points.begin(); it != points.end(); it++)
{
cv::RotatedRect rr = cv::minAreaRect(*it);
//检查面积比例
if (it->size() > minAreaRatio*rr.size.area())
{
rects.push_back(rr);
}
}
}
cv::Mat getImageOfEllipses(const cv::Mat& image, std::vector<cv::RotatedRect>& rects, cv::Scalar color = 255)
{
cv::Mat output = image.clone();
getBoundingRects(image, rects);
for (std::vector<cv::RotatedRect>::iterator it = rects.begin(); it != rects.end(); it++)
{
cv::ellipse(output, *it, color);
}
return output;
}
};
main函数:
int main()
{
cv::Mat image = cv::imread("old_library.jpg");
cv::imshow("原图", image);
MSERFeatures mserf(200, 1500, 0.5);
std::vector<cv::RotatedRect> rects;
cv::Mat result = mserf.getImageOfEllipses(image, rects,cv::Scalar(255,255,255));
cv::imshow("识别区椭圆", result);
cvWaitKey();
}
*GrabCut算法提取前景物体
如果要从静态图像中提取前景物体(例如从一个图像剪切物体黏贴到另一个图像),采用GrubCut算法是最好的选择。
cv::grabCut函数的用法非常简单。只需要输入一个图像,并对一些像素做上属于背景或属于前景的标记。算法会根据这个局部的标记,计算出整个图像中前景/背景的分割线
前景检测结果:
代码:
int main()
{
cv::Mat image = cv::imread("cow.jpg");
cv::imshow("原图", image);
cv::Rect rectangle(131, 135, 216, 140);
cv::rectangle(image, rectangle, cv::Scalar(255, 255, 255));
cv::imshow("image with rectangle", image);
cv::Mat result;
cv::Mat bgModel, fgModel;
image = cv::imread("cow.jpg");
cv::grabCut(image,
result,
rectangle,//包含前景的矩形
bgModel, fgModel, //模型
5,//迭代次数
cv::GC_INIT_WITH_RECT);//使用矩形
cv::compare(result, cv::GC_PR_FGD, result, cv::CMP_EQ);
//生成输出图像
cv::Mat foreground(image.size(), CV_8UC3, cv::Scalar(255, 255, 255));
image.copyTo(foreground, result);
cv::imshow("result", foreground);
cvWaitKey();
}
注意点:
调用cv::grabCut时,除了需要输入图像和分割后图像,还需要定义两个矩阵,用于存放和构建模型。
输出的分割图像可以是一下的四个值之一:
cv::GC_BGD:这个值表示明确属于背景的像素
cv::GC_FGD:这个值表示明确属于前景的像素
cv::GC_PR_BGD:这个值表示可能属于背景的像素
cv::GC_PR_BGD:这个值表示可能属于前景的像素