上一个教程 : Hit-or-Miss
下一个教程 : 图像金字塔
原作者 | Theodore Tsesmelis |
---|---|
兼容性 | OpenCV >= 3.0 |
目标
在本教程中,你将学习如何
-
通过创建自定义内核,应用两种非常常见的形态学运算符(即扩张和侵蚀),以提取水平轴和垂直轴上的直线。为此,您将使用以下 OpenCV 函数:
在一个例子中,您的目标是从乐谱中提取音符。
理论
形态学操作
形态学是一套图像处理操作,根据预定义的结构元素(也称为内核)处理图像。输出图像中每个像素的值是基于输入图像中相应像素与其邻近像素的比较。通过选择核的大小和形状,可以构建出对输入图像的特定形状敏感的形态学运算。
两种最基本的形态学操作是扩张和侵蚀。扩张是在图像中的物体边界上增加像素,而侵蚀则恰恰相反。添加或删除的像素数量分别取决于处理图像时所用结构元素的大小和形状。一般来说,这两种操作所遵循的规则如下:
- 膨胀: 输出像素的值是结构元素大小和形状范围内所有像素的最大值。例如,在二进制图像中,如果输入图像中位于内核范围内的任何一个像素被设置为 1,那么输出图像中相应的像素也将被设置为 1。后者适用于任何类型的图像(如灰度图像、二值图像等)。
- 腐蚀: 侵蚀操作也是如此。输出像素的值是结构元素大小和形状范围内所有像素的最小值。请看下面的示例图:
结构元素
如上图所示,一般来说,在任何形态学操作中,用于探测输入图像的结构元素都是最重要的部分。
结构元素是一个仅由 0 和 1 组成的矩阵,可以有任意的形状和大小。通常情况下,结构元素要比被处理的图像小得多,而值为 1 的像素则定义了邻域。结构元素的中心像素称为原点,它标识了感兴趣的像素,也就是正在处理的像素。
例如,下图展示了一个 7x7 大小的菱形结构元素。
C++
本教程代码如下所示。
您也可以从此处下载。
#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)
{
CommandLineParser parser(argc, argv, "{@input | notes.png | input image}");
Mat src = imread( samples::findFile( parser.get<String>("@input") ), IMREAD_COLOR);
if (src.empty())
{
cout << "Could not open or find the image! << endl;
cout << "Usage: " << argv[0] << " <输入图像>" << endl;
return -1;
}
// 显示源图像
imshow("src", src);
// 如果源图像尚未变为灰色,则将其变为灰色
Mat Gray;
if (src.channels() == 3)
{
cvtColor(src, gray, COLOR_BGR2GRAY);
}
else
{
gray = src;
}
// 显示灰色图像
show_wait_destroy("gray", gray);
// 将自适应阈值(adaptiveThreshold)应用于 gray 的 bitwise_not,注意 ~ 符号
Mat bw;
adaptiveThreshold(~gray, bw, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);
// 显示二进制图像
show_wait_destroy("binary", bw);
// 创建用于提取水平线和垂直线的图像
Mat horizontal = bw.clone();
Mat vertical = bw.clone();
// 指定水平轴的尺寸
int horizontal_size = horizontal.cols / 30;
// 创建结构元素,用于通过形态学操作提取水平线条
Mat horizontalStructure = getStructuringElement(MORPH_RECT, Size(horizontal_size, 1));
// 应用形态操作
erode(horizontal, horizontal, horizontalStructure, Point(-1, -1));
dilate(horizontal, horizontal, horizontalStructure, Point(-1, -1));
// 显示提取的水平线条
show_wait_destroy("horizontal", horizontal);
// 指定纵轴尺寸
int vertical_size = vertical.rows / 30;
// 创建结构元素,用于通过形态学操作提取垂直线条
Mat verticalStructure = getStructuringElement(MORPH_RECT, Size(1, vertical_size));
// 应用形态操作
erode(vertical, vertical, verticalStructure, Point(-1, -1));
dilate(vertical, vertical, verticalStructure, Point(-1, -1));
// 显示提取的垂直线条
show_wait_destroy("vertical", vertical);
// 反转垂直图像
bitwise_not(vertical, vertical);
show_wait_destroy("vertical_bit", vertical);
// 根据逻辑提取边缘并平滑图像
// 1.
// 2.
3. src.copyTo(smooth) // 4.
// 4. 模糊平滑图像
// 5. smooth.copyTo(src, edges)
// 第 1 步
Mat edges;
adaptiveThreshold(vertical, edges, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, -2);
show_wait_destroy("edges", edges);
// 第 2 步
Mat kernel = Mat::ones(2, 2, CV_8UC1);
dilate(edges, edges, kernel);
show_wait_destroy("dilate", edges);
// 第三步
Mat smooth;
vertical.copyTo(smooth);
// 第四步
blur(smooth, smooth, Size(2, 2));
// 第五步
smooth.copyTo(vertical, edges);
// 显示最终结果
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);
}
Java
本教程代码如下所示。
您也可以从此处下载。
import org.opencv.core.*;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
class Morphology_3Run {
public void run(String[] args) { // Check the number of arguments.
// 检查参数数
if (args.length == 0){
System.out.println("Not enough parameters!");
System.out.println("Program Arguments: [image_path]");
System.exit(-1);
}
// 加载图像
Mat src = Imgcodecs.imread(args[0]);
// 检查图片加载是否正常
if( src.empty() ) {
System.out.println("Error opening image: " + args[0]);
System.exit(-1);
}
// 显示源图像
HighGui.imshow("src", src);
// 将源图像转换为灰色(如果尚未转换的话
Mat gray = new Mat();
if (src.channels() == 3)
{
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
}
else
{
gray = src;
}
// 显示灰色图像
showWaitDestroy("gray" , gray);
// 在灰色的比特阈值上应用自适应阈值
Mat bw = new Mat();
将自适应阈值应用于灰色图像的 bitwise_not(gray, gray);
Imgproc.adaptiveThreshold(gray, bw, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 15, -2);
// 显示二进制图像
showWaitDestroy("binary" , bw);
// 创建用于提取水平线和垂直线的图像
Mat horizontal = bw.clone();
Mat vertical = bw.clone();
// 指定水平轴的尺寸
int horizontal_size = horizontal.cols() / 30;
// 创建结构元素,用于通过形态学操作提取水平线条
Mat horizontalStructure = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(horizontal_size,1));
// 应用形态操作
Imgproc.erode(horizontal, horizontal, horizontalStructure);
Imgproc.dilate(horizontal, horizontal, horizontalStructure);
// 显示提取的水平线条
showWaitDestroy("horizontal" , horizontal);
// 指定纵轴尺寸
int vertical_size = vertical.rows() / 30;
// 创建结构元素,用于通过形态学操作提取垂直线条
Mat verticalStructure = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size( 1,vertical_size));
// 应用形态操作
Imgproc.erode(vertical, vertical, verticalStructure);
Imgproc.dilate(vertical, vertical, verticalStructure);
// 显示提取的垂直线条
showWaitDestroy("vertical", vertical);
// 反转垂直图像
Core.bitwise_not(vertical, vertical);
showWaitDestroy("vertical_bit" , vertical);
// 根据逻辑提取边缘并平滑图像
// 1.
// 2.
src.copyTo(smooth) // 4.
// 4. 模糊平滑图像
// 5. smooth.copyTo(src, edges)
// 第 1 步
Mat edges = new Mat();
Imgproc.adaptiveThreshold(vertical, edges, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 3, -2);
showWaitDestroy("edges", edges);
// 第 2 步
Mat kernel = Mat.ones(2, 2, CvType.CV_8UC1);
Imgproc.dilate(edges, edges, kernel);
showWaitDestroy("dilate", edges);
// 第三步
Mat smooth = new Mat();
vertical.copyTo(smooth);
// 第四步
Imgproc.blur(smooth, smooth, new Size(2, 2));
// 第 5 步
smooth.copyTo(vertical, edges);
// 显示最终结果
ShowWaitDestroy("smooth - final", vertical);
System.exit(0);
}
private void showWaitDestroy(String winname, Mat img) {
HighGui.imshow(winname, img);
HighGui.moveWindow(winname, 500, 0);
HighGui.waitKey(0);
HighGui.destroyWindow(winname);
}
}
public class Morphology_3 {
public static void main(String[] args) {
// 加载本地库。
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
new Morphology_3Run().run(args);
}
}
Python
本教程代码如下所示。
您也可以从此处下载。
"""
@file morph_lines_detection.py
@brief 使用形态变换提取水平线和垂直线 示例代码
"""
import numpy 为 np
import sys
import cv2 as cv
def show_wait_destroy(winname, img):
cv.imshow(winname, img)
cv.moveWindow(winname, 500, 0)
cv.waitKey(0)
cv.destroyWindow(winname)
def main(argv):
# [load_image]
# 检查参数数量
if len(argv) < 1:
print ('Not enough parameters')
print ('Usage:\nmorph_lines_detection.py < path_to_image >')
return -1
# 加载图像
src = cv.imread(argv[0], cv.IMREAD_COLOR)
# 检查图片加载是否正常
if src is None:
print ('Error opening image: ' + argv[0])
return -1
# 显示源图像
cv.imshow("src", src)
# [load_image]
# [gray]
# 如果源图像尚未变为灰色,则将其变为灰色
if len(src.shape) != 2:
gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
else
gray = src
# 显示灰色图像
show_wait_destroy("gray", gray)
# [gray]
# [bin]
# 在灰色的 bitwise_not 处应用自适应阈值,注意 ~ 符号
gray = cv.bitwise_not(gray)
bw = cv.adaptiveThreshold(gray, 255, cv.ADAPTIVE_THRESH_MEAN_C, \
cv.THRESH_BINARY, 15, -2)
# 显示二进制图像
show_wait_destroy("binary", bw)
# [bin]
# [init]
# 创建用于提取水平线和垂直线的图像
horizontal = np.copy(bw)
vertical = np.copy(bw)
# [init]
# [horiz]
# 指定水平轴的尺寸
cols = horizontal.shape[1]
horizontal_size = cols // 30
# 创建结构元素,用于通过形态学操作提取水平线条
horizontalStructure = cv.getStructuringElement(cv.MORPH_RECT, (horizontal_size, 1))
# 应用形态学操作
horizontal = cv.erode(horizontal, horizontalStructure)
horizontal = cv.dilate(horizontal, horizontalStructure)
# 显示提取的水平线条
show_wait_destroy("horizontal", horizontal)
# [horiz]
# [vert]
# 指定纵轴尺寸
rows = vertical.shape[0]
verticalsize = rows // 30
# 创建结构元素,用于通过形态学操作提取垂直线条
verticalStructure = cv.getStructuringElement(cv.MORPH_RECT, (1, verticalsize))
# 应用形态学操作
vertical = cv.erode(vertical, verticalStructure)
vertical = cv.dilate(vertical, verticalStructure)
# 显示提取的垂直线
show_wait_destroy("vertical", vertical)
# [vert]
# [smooth]
# 反垂直图像
vertical = cv.bitwise_not(vertical)
show_wait_destroy("vertical_bit", vertical)
'''
根据逻辑提取边缘并平滑图像
1. 提取边缘
2. dilate(edges)
3. src.copyTo(smooth)
4. 模糊平滑图像
5. smooth.copyTo(src, edges)
'''
# 第 1 步
edges = cv.adaptiveThreshold(vertical, 255, cv.ADAPTIVE_THRESH_MEAN_C,\)
cv.THRESH_BINARY, 3, -2)
show_wait_destroy("edges", edges)
# 第 2 步
kernel = np.ones((2, 2), np.uint8)
edges = cv.dilate(edges, kernel)
show_wait_destroy("dilate", edges)
# 第三步
smooth = np.copy(vertical)
# 第四步
smooth = cv.blur(smooth, (2, 2))
# 第五步
(rows, cols) = np.where(edges != 0)
vertical[rows, cols] = smooth[rows, cols] # 显示最终结果
# 显示最终结果
show_wait_destroy("smooth - final", vertical)
# [smooth]
return 0
if __name__ == "__main__":
main(sys.argv[1:])
说明/结果
从此处获取图像。
加载图片
C++
CommandLineParser parser(argc, argv, "{@input | notes.png | input image}");
Mat src = imread( samples::findFile( parser.get<String>("@input") ), IMREAD_COLOR);
if (src.empty())
{
cout << "Could not open or find the image! << endl;
cout << "Usage: " << argv[0] << " <Input Image>" << endl;
return -1;
}
// 显示源图像
imshow("src", src);
Java
// 检查参数数量
if (args.length == 0){
System.out.println("Not enough parameters!");
System.out.println("Program Arguments: [image_path]");
System.exit(-1);
}
// 加载图像
Mat src = Imgcodecs.imread(args[0]);
// 检查图片加载是否正常
if( src.empty() ) {
System.out.println("Error opening image: " + args[0]);
System.exit(-1);
}
// 显示源图像
HighGui.imshow("src", src);
Python
# 检查参数数量
如果 len(argv) < 1:
print ('Not enough parameters')
print ('Usage:\nmorph_lines_detection.py < path_to_image >')
return -1
# 加载图像
src = cv.imread(argv[0], cv.IMREAD_COLOR)
# 检查图片加载是否正常
if src 为 None:
print ('Error opening image: ' + argv[0])
return -1
# 显示源图像
cv.imshow("src", src)
灰度
C++
// 如果源图像尚未变为灰色,则将其变为灰色
Mat Gray;
if (src.channels() == 3)
{
cvtColor(src, gray, COLOR_BGR2GRAY);
}
else
{
gray = src;
}
// 显示灰色图像
show_wait_destroy("gray", gray);
Java
// 如果源图像尚未变为灰色,则将其变为灰色
Mat gray = new Mat();
if (src.channels() == 3)
{
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
}
else
{
gray = src;
}
// 显示灰色图像
showWaitDestroy("gray" , gray);
Python
# 如果源图像尚未转换为灰色,则将其转换为灰色
if len(src.shape) != 2:
gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
else
gray = src
# 显示灰色图像
show_wait_destroy("gray", gray)
将灰度图像转换为二进制图像
C++
// 将自适应阈值应用于比特灰度,注意 ~ 符号
Mat bw;
adaptiveThreshold(~gray, bw, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);
// 显示二进制图像
show_wait_destroy("binary", bw);
Java
// 在比特灰度级应用自适应阈值
Mat bw = new Mat();
Core.bitwise_not(gray, gray);
Imgproc.adaptiveThreshold(gray, bw, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 15, -2);
// 显示二进制图像
showWaitDestroy("binary" , bw);
Python
# 在灰色的 bitwise_not 处应用自适应阈值,注意 ~ 符号
gray = cv.bitwise_not(gray)
bw = cv.adaptiveThreshold(gray, 255, cv.ADAPTIVE_THRESH_MEAN_C, \
cv.THRESH_BINARY, 15, -2)
# 显示二进制图像
show_wait_destroy("binary", bw)
输出图像
现在,我们可以应用形态学操作来提取水平线和垂直线,从而将音符从乐谱中分离出来,但首先要初始化输出图像:
C++
// 创建用于提取水平线和垂直线的图像
Mat horizontal = bw.clone();
Mat vertical = bw.clone();
Java
// 创建用于提取水平线和垂直线的图像
Mat horizontal = bw.clone();
Mat vertical = bw.clone();
Python
结构元素
正如我们在理论中所述,为了提取我们想要的对象,我们需要创建相应的结构元素。由于我们要提取水平线条,因此相应的结构元素将具有如下形状:
在源代码中,这可以用下面的代码片段来表示:
C++
// 指定水平轴的大小
int horizontal_size = horizontal.cols / 30;
// 创建结构元素,用于通过形态学操作提取水平线条
Mat horizontalStructure = getStructuringElement(MORPH_RECT, Size(horizontal_size, 1));
// 应用形态操作
erode(horizontal, horizontal, horizontalStructure, Point(-1, -1));
dilate(horizontal, horizontal, horizontalStructure, Point(-1, -1));
// 显示提取的水平线条
show_wait_destroy("horizontal", horizontal);
Java
// 指定横轴尺寸
int horizontal_size = horizontal.cols() / 30;
// 创建结构元素,用于通过形态学操作提取水平线条
Mat horizontalStructure = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(horizontal_size,1));
// 应用形态操作
Imgproc.erode(horizontal, horizontal, horizontalStructure);
Imgproc.dilate(horizontal, horizontal, horizontalStructure);
// 显示提取的水平线条
showWaitDestroy("horizontal" , horizontal);
Python
# 指定水平轴的尺寸
cols = horizontal.shape[1]
horizontal_size = cols // 30
# 创建结构元素,用于通过形态学操作提取水平线条
horizontalStructure = cv.getStructuringElement(cv.MORPH_RECT, (horizontal_size, 1))
# 应用形态学操作
水平 = cv.erode(horizontal, horizontalStructure)
水平 = cv.dilate(horizontal, horizontalStructure)
# 显示提取的水平线条
show_wait_destroy("horizontal", horizontal)
同样的方法也适用于垂直线条,并使用相应的结构元素:
同样表示如下:
C++
// 指定纵轴尺寸
int vertical_size = vertical.rows / 30;
// 创建结构元素,用于通过形态学操作提取垂直线条
Mat verticalStructure = getStructuringElement(MORPH_RECT, Size(1, vertical_size));
// 应用形态操作
erode(vertical, vertical, verticalStructure, Point(-1, -1));
dilate(vertical, vertical, verticalStructure, Point(-1, -1));
// 显示提取的垂直线条
show_wait_destroy("vertical", vertical);
Java
// 指定纵轴尺寸
int vertical_size = vertical.rows() / 30;
// 创建结构元素,用于通过形态学操作提取垂直线条
Mat verticalStructure = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size( 1,vertical_size));
// 应用形态操作
Imgproc.erode(vertical, vertical, verticalStructure);
Imgproc.dilate(vertical, vertical, verticalStructure);
// 显示提取的垂直线条
showWaitDestroy("vertical", vertical);
Python
# 指定纵轴尺寸
行 = vertical.shape[0]
verticalsize = rows // 30
# 创建结构元素,用于通过形态学操作提取垂直线条
verticalStructure = cv.getStructuringElement(cv.MORPH_RECT, (1, verticalsize))
# 应用形态学操作
vertical = cv.erode(vertical, verticalStructure)
vertical = cv.dilate(vertical, verticalStructure)
# 显示提取的垂直线
show_wait_destroy("vertical", vertical)
细化边缘 / 结果
正如你所看到的,我们就快成功了。不过,这时你会发现音符的边缘有点粗糙。因此,我们需要细化边缘,以获得更平滑的效果:
C++
// 反垂直图像
bitwise_not(vertical, vertical);
show_wait_destroy("vertical_bit", vertical);
// 根据逻辑提取边缘并平滑图像
// 1.
// 2.
3. src.copyTo(smooth) // 4.
// 4. 模糊平滑图像
// 5. smooth.copyTo(src, edges)
// 第 1 步
Mat edges;
adaptiveThreshold(vertical, edges, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, -2);
show_wait_destroy("edges", edges);
// 第 2 步
Mat kernel = Mat::ones(2, 2, CV_8UC1);
dilate(edges, edges, kernel);
show_wait_destroy("dilate", edges);
// 第三步
Mat smooth;
vertical.copyTo(smooth);
// 第四步
blur(smooth, smooth, Size(2, 2));
// 第五步
smooth.copyTo(vertical, edges);
// 显示最终结果
show_wait_destroy("smooth - final", vertical);
Java
// 反垂直图像
Core.bitwise_not(vertical, vertical);
showWaitDestroy("vertical_bit" , vertical);
// 根据逻辑提取边缘并平滑图像
// 1.
// 2.
src.copyTo(smooth) // 4.
// 4. 模糊平滑图像
// 5. smooth.copyTo(src, edges)
// 第 1 步
Mat edges = new Mat();
Imgproc.adaptiveThreshold(vertical, edges, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 3, -2);
showWaitDestroy("edges", edges);
// 第 2 步
Mat kernel = Mat.ones(2, 2, CvType.CV_8UC1);
Imgproc.dilate(edges, edges, kernel);
showWaitDestroy("dilate", edges);
// 第三步
Mat smooth = new Mat();
vertical.copyTo(smooth);
// 第四步
Imgproc.blur(smooth, smooth, new Size(2, 2));
// 第 5 步
smooth.copyTo(vertical, edges);
// 显示最终结果
showWaitDestroy("smooth - final", vertical);
Python
# 反垂直图像
vertical = cv.bitwise_not(vertical)
show_wait_destroy("vertical_bit", vertical)
'''
根据逻辑提取边缘并平滑图像
1. 提取边缘
2. dilate(edges)
3. src.copyTo(smooth)
4. 模糊平滑图像
5. smooth.copyTo(src, edges)
'''
# 第 1 步
edges = cv.adaptiveThreshold(vertical, 255, cv.ADAPTIVE_THRESH_MEAN_C,\)
cv.THRESH_BINARY, 3, -2)
show_wait_destroy("edges", edges)
# 第 2 步
kernel = np.ones((2, 2), np.uint8)
edges = cv.dilate(edges, kernel)
show_wait_destroy("dilate", edges)
# 第三步
smooth = np.copy(vertical)
# 第四步
smooth = cv.blur(smooth, (2, 2))
# 第五步
(rows, cols) = np.where(edges != 0)
vertical[rows, cols] = smooth[rows, cols] # 显示最终结果
# 显示最终结果
show_wait_destroy("smooth - final", vertical)