本文作者:小嗷
微信公众号:aoxiaoji
吹比QQ群:736854977
简书链接:https://www.jianshu.com/u/45da1fbce7d0
在本篇中,您将学习:
用两个非常常见的形态学算子(即扩张和侵蚀),创建自定义内核,以便在水平和垂直轴上提取直线。为此,您将使用以下OpenCV函数:
- erode()
- dilate()
- getStructuringElement()
在一个例子中,你的目标是从乐谱中提取音乐音符。
(在第25篇写道)
本文你会找到以下问题的答案:
- 用形态学方法提取水平和垂直的线条
- CommandLineParser()
- adaptiveThreshold()
- DestroyWindow()
2.1 理论
形态学操作
形态学是一组图像处理操作,它们基于预定义的结构元素(也称为内核)来处理图像。输出图像中每个像素的值都是基于与相邻的输入图像中对应像素的比较。通过选择内核的大小和形状,您可以构造一个对输入映像的特定形状敏感的形态学操作。
两种最基本的形态学操作是扩张和侵蚀。扩张将像素添加到图像中对象的边界,而侵蚀则恰恰相反。添加或删除像素的数量分别取决于用于处理图像的结构元素的大小和形状。一般来说,这两项行动所遵循的规则如下:
膨胀:输出像素的值是在结构元素的大小和形状范围内的所有像素的最大值。例如,在二进制图像中,如果将内核范围内的输入图像的任何像素设置为1,那么输出图像的相应像素也将设置为1。后者适用于任何类型的图像(例如灰度、bgr等)。
二值图像的膨胀(看不懂就查看第25篇)
灰度图像的膨胀
(其实,就是核心点取3个数中的最大值。看不懂就证明你没好好学)
侵蚀:反之亦然适用于侵蚀操作。输出像素的值是位于结构元素的大小和形状中的所有像素的最小值。请看下图:
二值图像的腐蚀(看不懂就查看第25篇)
灰度图像的侵蚀
结构化元素
如上所述,在任何形态学操作中,用于探测输入图像的结构元素都是最重要的部分。
结构元素是一个只有0和1的矩阵,可以有任意的形状和大小。通常比被处理的图像小得多,而值为1的像素定义邻域。结构元素的中心像素,称为原点,标识感兴趣的像素——被处理的像素。
例如,下面演示了一个7x7大小的菱形结构元素。
菱形结构元素
结构元素可以有许多常见的形状,如线、钻石、磁盘、周期线、圆和大小。通常,您选择的结构元素的大小和形状与您希望在输入映像中处理/提取的对象相同。例如,要在图像中找到线条,请创建一个线性结构元素,稍后您将看到。
3.1 CommandLineParser()
初始化命令行解析器对象
cv::CommandLineParser::CommandLineParser (
int argc,
const char *const argv[],
const String & keys
)
参数解析:
- argc :命令行参数的数量(来自main()))
- argv[]:命令行参数的数组(来自main()))
- keys :描述可接受的命令行参数的字符串(语法参见类描述)
详细说明:
用于命令行解析。
下面的示例演示如何使用CommandLineParser:
//初始化命令行解析器对象
CommandLineParser parser(argc, argv, keys);
//设置有关消息
parser.about("Application name v1.0.0");
//检查命令行中是否提供了字段,“help”。
if (parser.has("help"))
{
//打印帮助信息。
parser.printMessage();
//退出
return 0;
}
//通过名字“N”访问参数。
//返回转换为选定类型的参数。如果参数未知或不能转换为所选类型,则设置错误标志(可以用check检查)。
int N = parser.get<int>("N");
double fps = parser.get<double>("fps");
String path = parser.get<String>("path");
//检查命令行中是否提供了字段,“timestamp”。
use_time_stamp = parser.has("timestamp");
//通过索引“0”访问位置参数。
//返回转换为选定类型的参数。索引从0开始计数。
String img1 = parser.get<String>(0);
String img2 = parser.get<String>(1);
int repeat = parser.get<int>(2);
//检查解析错误。
//当访问参数时发生错误(错误转换、丢失的参数等),返回false。调用printErrors打印错误消息列表。
if (!parser.check())
{
//打印错误列表
parser.printErrors();
return 0;
}
上面代码Keys的语法
keys参数是一个包含多个块的字符串,每个块都包含在花括号中,并描述一个参数。每个参数包含由|符号分隔的三个部分:
- 参数名称是一个空格分隔的选项同义词列表(将参数标记为位置,在其前面加上@符号)
- 如果没有提供参数,将使用默认值(可以为空)
- 帮助消息(可以为空)
例如:
const String keys =
"{help h usage ? | | print this message }"
"{@image1 | | image1 for compare }"
"{@image2 |<none>| image2 for compare }"
"{@repeat |1 | number }"
"{path |. | path to file }"
"{fps | -1.0 | fps for output video }"
"{N count |100 | count of objects }"
"{ts timestamp | | use time stamp }"
;
}
注意:没有帮助和时间戳的默认值,因此我们可以使用has()方法检查它们的存在。带默认值的参数被认为总是存在的。在这些情况下使用get()方法来检查它们的实际值。
字符串键如get(“@image1”)默认返回空字符串,即使默认值为空。使用特殊的默认值强制返回的字符串不能为空。(就像在<字符串>(“@image2”))
用法
keys的描述
# Good call (3 positional parameters: image1, image2 and repeat; N is 200, ts is true)
$ ./app -N=200 1.png 2.jpg 19 -ts
# Bad call
$ ./app -fps=aaa
ERRORS:
Parameter 'fps': can not convert: [aaa] to [double]
好像在opencv的c版本中,应该是opencv1.0以前,还没有出现CommandLineParser这个类,最近看到opencv2.3后面的版本里自带的samples,很多都用到了CommandLineParser这个类,那么这个类到底有什么作用呢,从命名大概可以猜出这是个命令行解析类。因为我们知道opencv是一个开源库,所以其很少有图形操作方面的api,基本上还是基于命令行执行的。那么这个类的出现主要是方便用户在命令行使用过程中减少工作量,可以在程序文件中直接指定命令行中的参数指令,方便了调试。
我简单写了下面这个例子:
#include "opencv2/video/tracking.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include <ctype.h>
#include <string>
using namespace cv;
using namespace std;
const char* keys =
{
"{ c | camera | 0 | use camera or not}"
"{ fn | filename |xxxx.avi | movie file}"
"{ t | test | test string | good day!}"
};
int main(int argc, const char** argv )
{
CommandLineParser parser(argc, argv, keys);
bool useCamera = parser.get<bool>("c");//括号里写成“camera”也可以
string file = parser.get<string>("fn");
string third = parser.get<string>("t");
//打印输出
cout<<useCamera<<endl;
cout<<file<<endl;
cout<<third<<endl;
cout<<endl;
parser.printParams();//CommandLineParser的成员函数,打印全部参数,还有其他成员函数,如:has(),getString()等
return 0;
}
运行结果:
第一行就是这个类的构造函数,前2个参数是命令行传过来的,第3个就是刚刚定义的keys了,keys的结构有一定规律,比如说”{ c | camera | 0 | use camera or not}”都是用大括号和双引号引起来,然后中间的内容分成4断,用”|”分隔开,分别表示简称,文件来源,文件值和帮助语句。第二行和第三行表示打开摄像头和打开文件,文件的文件名等都在keys指针中了。
大概可以看出来用这个类的好处就是很方便,因为以前版本没这个类时,如果要运行带参数的.exe,必须在命令行中输入文件路径以及各种参数,并且输入的参数格式要与代码中的if语句判断内容格式一样,一不小心就输错了,很不方便。另外如果想要更改输入格式的话在主函数文件中要相应更改很多地方。现在有了这个类,只需要改keys里面的内容就可以了,并且可以直接运行,不需要cmd命令行带参运行。最后这个类封装了很多函数,可以直接用,只不过这个本来就是类结构的优点。
3.2 adaptiveThreshold()
对一个数组应用一个自适应阈值。
void cv::adaptiveThreshold ( InputArray src,
OutputArray dst,
double maxValue,
int adaptiveMethod,
int thresholdType,
int blockSize,
double C
)
该函数根据公式将灰度图像转换为二进制图像:
T(x,y)是为每个像素单独计算的阈值(参见adaptiveMethod参数)。
这个函数可以在适当的位置处理图像。
这个函数可以在适当的位置处理图像。
参数解析:
- src :8位单通道图像来源。(输入图像)
- dst:与src相同大小的目标图像。(输出图像)
- maxValue :描述可接受的命令行参数的字符串(语法参见类描述)【向上最大值】
- adaptiveMethod:使用自适应阈值算法,请参阅适应性阈值类型。边界复制的边界被用来处理边界。(自适应方法,平均或高斯)
- thresholdType:阈值类型必须是THRESH_BINARY或THRESH_BINARY_INV,请参阅阈值类型。(参考第5篇阈值)
- blockSize:用于计算阈值大小的一个像素的邻域尺寸,取值为3、5、7等等(块大小)
- C:常量减去平均值或加权平均值(参见下面的详细信息)。通常情况下,它是正的,但也可能是零或负的。
cv::adaptiveThreshold()支持两种自适应方法,即cv::ADAPTIVE_THRESH_MEAN_C(平均)和cv::ADAPTIVE_THRESH_GAUSSIAN_C(高斯)。在两种情况下,自适应阈值T(x, y)。通过计算每个像素周围bxb大小像素块的加权均值并减去常量C得到。其中,b由blockSize给出,大小必须为奇数;如果使用平均的方法,则所有像素周围的权值相同;如果使用高斯的方法,则(x,y)周围的像素的权值则根据其到中心点的距离通过高斯方程得到。(平均和高斯参考第14篇)
测试代码如下:
void test_adaptive_threshold()
{
cv::Mat src = cv::imread("chessboard.png", cv::IMREAD_GRAYSCALE);
cv::Mat dst;
int maxVal = 255;
int blockSize = 41;
double C = 0;
cv::adaptiveThreshold(src, dst, maxVal, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, blockSize, C);
cv::imshow("threshold", dst);
cv::waitKey(0);
return;
}
我们分别使用了平均和高斯两种自适应方法,结果如下:
3.3 DestroyWindow
函数功能:销毁指定的窗口。这个函数通过发送WM_DESTROY 消息和 WM_NCDESTROY 消息使窗口无效并移除其键盘焦点。这个函数还销毁窗口的菜单,清空线程的消息队列,销毁与窗口过程相关的定时器,解除窗口对剪贴板的拥有权,打断剪贴板器的查看链。
BOOL DestroyWindow(
HWND hWnd //"对话框名字"
);
归纳起来,主要有三层意思:
1、该函数销毁一个指定的窗口。
2、如果指定的窗口是一个父窗口,则该函数自动销毁与之管理的子窗口。
3、该函数也用于销毁用CreateDialog 函数创建的非模式对话框。
相关网址:
https://blog.csdn.net/liuy_yy/article/details/7095286
任务:
用形态学方法提取水平和垂直的线条
代码如下:
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>
void show_wait_destroy(const char* winname, cv::Mat img);
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
//用命令行解析String的内容
CommandLineParser parser(argc, argv, "{@input | ../data/notes.png | input image}");
//通过parser获取图片地址
Mat src = imread(parser.get<String>("@input"), IMREAD_COLOR);
if (src.empty())
{
cout << "Could not open or find the image!\n" << endl;
cout << "Usage: " << argv[0] << " <Input image>" << endl;
//暂停一下
getchar();
return -1;
}
// Show source image(展示源图像)
imshow("src", src);
// Transform source image to gray if it is not already
Mat gray;
//如果三通道就换成灰度图
if (src.channels() == 3)
{
cvtColor(src, gray, COLOR_BGR2GRAY);
}
else
{
//等于灰度图
gray = src;
}
// Show gray image展示灰度图,并等待销毁主窗口,执行下一步
show_wait_destroy("gray", gray);
// Apply adaptiveThreshold at the bitwise_not of gray, notice the ~ symbol
Mat bw;
//自定义阈值,转成二值图。
adaptiveThreshold(~gray, bw, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);
// Show binary image展示二值图,并等待销毁主窗口,执行下一步
show_wait_destroy("binary", bw);
// Create the images that will use to extract the horizontal and vertical lines
//horizontal、horizontal复制bw
Mat horizontal = bw.clone();
Mat vertical = bw.clone();
// 指定尺寸在水平轴等于二值图的列数除30
int horizontal_size = horizontal.cols / 30;
// Create structure element for extracting horizontal lines through morphology operations
//创建一个getStructuringElement对象:矩形,核为纵为二值图的列数除30,列为1,
Mat horizontalStructure = getStructuringElement(MORPH_RECT, Size(horizontal_size, 1));
// Apply morphology operations
//应用形态学操作,先腐蚀后膨胀(开运算:先腐蚀再膨胀,可以去掉目标外的孤立点。)
//简单来说就是直线以外的其他元素,通通给小嗷去掉
erode(horizontal, horizontal, horizontalStructure, Point(-1, -1));
dilate(horizontal, horizontal, horizontalStructure, Point(-1, -1));
// Show extracted horizontal lines
//展示提取的直线
show_wait_destroy("horizontal", horizontal);
// Specify size on vertical axis
//指定尺寸在水平轴等于二值图的纵数除30
int vertical_size = vertical.rows / 30;
// Create structure element for extracting vertical lines through morphology operations
Mat verticalStructure = getStructuringElement(MORPH_RECT, Size(1, vertical_size));
// Apply morphology operations
erode(vertical, vertical, verticalStructure, Point(-1, -1));
dilate(vertical, vertical, verticalStructure, Point(-1, -1));
// Show extracted vertical lines
// 同理没意思,小嗷不写。
show_wait_destroy("vertical", vertical);
// Inverse vertical image
// 看不懂参考第6篇
bitwise_not(vertical, vertical);
// 得到相减后的图像(只有)
show_wait_destroy("vertical_bit", vertical);
// Extract edges and smooth image according to the logic
//根据逻辑提取边缘和平滑图像
// 1. extract edges提取边缘
// 2. dilate(edges)膨胀
// 3. src.copyTo(smooth)复制smooth
// 4. blur smooth img(参考第18篇均值)
// 5. smooth.copyTo(src, edges)(再复制到vertical里面)
// Step 1
Mat edges;
adaptiveThreshold(vertical, edges, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, -2);
show_wait_destroy("edges", edges);
// Step 2
Mat kernel = Mat::ones(2, 2, CV_8UC1);
dilate(edges, edges, kernel);
show_wait_destroy("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
show_wait_destroy("smooth - final", vertical);
return 0;
}
//展示输入图像
void show_wait_destroy(const char* winname, cv::Mat img) {
//展示输入图像
imshow(winname, img);
//移动窗口
moveWindow(winname, 500, 0);
waitKey(0);
//销毁窗口
destroyWindow(winname);
}
原图:
下载图片代码:
CommandLineParser parser(argc, argv, "{@input | ../data/notes.png | input image}");
Mat src = imread(parser.get<String>("@input"), IMREAD_COLOR);
if (src.empty())
{
cout << "Could not open or find the image!\n" << endl;
cout << "Usage: " << argv[0] << " <Input image>" << endl;
return -1;
}
// Show source image
imshow("src", src);
下载图片:
灰度图代码:
// Transform source image to gray if it is not already
Mat gray;
if (src.channels() == 3)
{
cvtColor(src, gray, COLOR_BGR2GRAY);
}
else
{
gray = src;
}
// Show gray image
show_wait_destroy("gray", gray);
灰度图:
二值图代码:
// Apply adaptiveThreshold at the bitwise_not of gray, notice the ~ symbol
Mat bw;
adaptiveThreshold(~gray, bw, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);
// Show binary image
show_wait_destroy("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 horizontal_size = horizontal.cols / 30;
// Create structure element for extracting horizontal lines through morphology operations
Mat horizontalStructure = getStructuringElement(MORPH_RECT, Size(horizontal_size, 1));
// Apply morphology operations
erode(horizontal, horizontal, horizontalStructure, Point(-1, -1));
dilate(horizontal, horizontal, horizontalStructure, Point(-1, -1));
// Show extracted horizontal lines
show_wait_destroy("horizontal", horizontal);
同样的道理也适用于垂直的线,有相应的结构元素:
这又一次表示如下:
// Specify size on vertical axis
int vertical_size = vertical.rows / 30;
// Create structure element for extracting vertical lines through morphology operations
Mat verticalStructure = getStructuringElement(MORPH_RECT, Size(1, vertical_size));
// Apply morphology operations
erode(vertical, vertical, verticalStructure, Point(-1, -1));
dilate(vertical, vertical, verticalStructure, Point(-1, -1));
// Show extracted vertical lines
show_wait_destroy("vertical", vertical);
效果图:
细化边缘/结果
正如你所看到的,我们快到了。然而,在这一点上,你会注意到音符的边缘有点粗糙。出于这个原因,我们需要对边缘进行优化,以获得更平滑的结果:
// Inverse vertical image
bitwise_not(vertical, vertical);
show_wait_destroy("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, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, -2);
show_wait_destroy("edges", edges);
// Step 2
Mat kernel = Mat::ones(2, 2, CV_8UC1);
dilate(edges, edges, kernel);
show_wait_destroy("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
show_wait_destroy("smooth - final", vertical);
这个是小实战处理图像(提取直线。当然,具体项目具体看。下期见,感觉好的小伙伴,可以推荐给身边的小伙伴添加公众号,谢谢大家支持。)
- 本人是抱着玩一玩的心态,学习opencv(其实深度学习没有外界说的这么高深,小嗷是白板,而且有工作在身并且于代码无关)
- 大家可以把我的数学水平想象成初中水平,毕竟小嗷既不是代码靠吃饭又不是靠数学吃饭,毕业N年
- 写文章主要是为了后人少走点弯路,多交点朋友,一起学习
- 如果有好的图像识别群拉我进去QQ:631821577
- 就我一个白板,最后还是成的,你们别怕,慢慢来把
分享可以无数次,转载成自己文章QQ邮箱通知一下,未经授权请勿转载。
- 邮箱:631821577@qq.com
- QQ群:736854977
- 有什么疑问公众号提问,下班或者周六日回答,ths
推荐文章:
8.更正曝光不足的图像(图像的对比度和亮度及轨迹条) — OpenCV从零开始到图像(人脸 + 物体)识别系列 【没有排版好】
(公众号底下的文章分类 -> 编程 -> 查看第四篇文章)【已经排版好,建议PC电脑看】
25.消除不相关的细节/裂缝桥接(形态学 –膨胀与腐蚀详解 )— OpenCV从零开始到图像(人脸 + 物体)识别系列
27.形态学图像运算(形态学梯度计算/开运算/闭运算/顶帽运算/黑帽)– OpenCV从零开始到图像(人脸 + 物体)识别系列
28.击中击不中变换(二值图像/结构元素/集合/convertTo/saturate_cast/moveWindow)
感言
很多知识点都是串起来,怎么说呢?可以理解之前二十几篇文章的知识布局,才理解这篇或者做到这篇文章。
实在有点不好意思,小嗷因为个人原因(工资低老想着跳槽,哈哈哈)中断了一期。一周5篇实在吃不消。
还有1开始几篇文章布局什么真难看,小嗷会重新群发(微信规矩:不群发,上不了链接地址)
这张图提取的直线:
当然,运用场景还是有的。例如提取道路的前方的白色长方形矩形,识别是否再自己道路上等等。
这期写了19118个字,呵呵呵