Opencv暑期历程--Day2 图像缩放->图像金字塔->Canny边缘提取->Sobel边缘提取->Laplacian边缘提取

图像缩放

在Opencv中,我们经常使用resize函数来对需要缩放大小的图片进行缩放

resize(img1, img2, Size(300, 300));指定大小为300*300的尺寸

img1:原图片

img2:缩放后的图片

Size(300,300)指定大小。

将原图缩放为300*300像素的图片

// opencv_day1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdlib.h>
#include <stdio.h>
using namespace std;
using namespace cv;

int main()
{
	Mat img1;
	cvNamedWindow("原图", WINDOW_AUTOSIZE);
	cvNamedWindow("指定像素缩小", WINDOW_AUTOSIZE);
	img1 = imread("1.png");
	Mat img2;
	resize(img1, img2, Size(300, 300));//指定大小为300*300的尺寸
	imshow("原图", img1);
	imshow("指定像素缩小", img2);
	waitKey(0);
	return 0;
}



当然,缩放除了这个方法,还可以指定比例进行缩放。

resize(img1, img2, Size(),0.5,0.5);缩小大小长宽各为原来的0.5倍

要在第三个参数那里用Size()表示尺寸按原来一样,然后后面的两个参数分别表示横尺寸的比例,纵尺寸缩放的比例

 

 长宽各缩放成原来的0.5倍效果:

 

// opencv_day1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdlib.h>
#include <stdio.h>
using namespace std;
using namespace cv;

int main()
{
	Mat img1;
	cvNamedWindow("原图", WINDOW_AUTOSIZE);
	cvNamedWindow("指定像素缩小", WINDOW_AUTOSIZE);
	img1 = imread("1.png");
	Mat img2;
	resize(img1, img2, Size(), 0.5, 0.5);//长宽各缩小为原来的0.5倍
	imshow("原图", img1);
	imshow("指定像素缩小", img2);
	waitKey(0);
	return 0;
}



图像金字塔

为了方便日后重新复习,以下原理从http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/imgproc/pyramids/pyramids.html#pyramids而来。

图像金字塔

  • 一个图像金字塔是一系列图像的集合 - 所有图像来源于同一张原始图像 - 通过梯次向下采样获得,直到达到某个终止条件才停止采样。
  • 有两种类型的图像金字塔常常出现在文献和应用中:
    • 高斯金字塔(Gaussian pyramid): 用来向下采样
    • 拉普拉斯金字塔(Laplacian pyramid): 用来从金字塔低层图像重建上层未采样图像
  • 在这篇文档中我们将使用 高斯金字塔

高斯金字塔

  • 想想金字塔为一层一层的图像,层级越高,图像越小。

    Pyramid figure
  • 每一层都按从下到上的次序编号, 层级 (i+1) (表示为 G_{i+1} 尺寸小于层级 i (G_{i}))。

  • 为了获取层级为 (i+1) 的金字塔图像,我们采用如下方法:

    • G_{i} 与高斯内核卷积:

      \frac{1}{16} \begin{bmatrix} 1 & 4 & 6 & 4 & 1  \\ 4 & 16 & 24 & 16 & 4  \\ 6 & 24 & 36 & 24 & 6  \\ 4 & 16 & 24 & 16 & 4  \\ 1 & 4 & 6 & 4 & 1 \end{bmatrix}

    • 将所有偶数行和列去除。

  • 显而易见,结果图像只有原图的四分之一。通过对输入图像 G_{0} (原始图像) 不停迭代以上步骤就会得到整个金字塔。

  • 以上过程描述了对图像的向下采样,如果将图像变大呢?:

    • 首先,将图像在每个方向扩大为原来的两倍,新增的行和列以0填充(0)
    • 使用先前同样的内核(乘以4)与放大后的图像卷积,获得 “新增像素” 的近似值。
  • 这两个步骤(向下和向上采样) 分别通过OpenCV函数 pyrUppyrDown 实现

h从原理也可以看到,向下采样的时候去除了偶数的行列,所以啊,图像的很多细节肯定也丢失了,还是resize好用,而且拉普拉斯金字塔是向上构建图片,是放大用的,中间填充的点用邻近的像素点来填充。但是无论用哪种,图像都会模糊。

pyrUp( tmp, dst, Size( tmp.cols*2, tmp.rows*2 ) );

函数 pyrUp 接受了3个参数:(放大用)

  • tmp: 当前图像, 初始化为原图像 src
  • dst: 目的图像( 显示图像,为输入图像的两倍)
  • Size( tmp.cols*2, tmp.rows*2 ) : 目的图像大小, 既然我们是向上采样, pyrUp 期待一个两倍于输入图像( tmp )的大小。
pyrDown( tmp, dst, Size( tmp.cols/2, tmp.rows/2 )
  • 类似于 pyrUp, 函数 pyrDown 也接受了3个参数:(缩小用)

    • tmp: 当前图像, 初始化为原图像 src
    • dst: 目的图像( 显示图像,为输入图像的一半)
    • Size( tmp.cols/2, tmp.rows/2 ) :目的图像大小, 既然我们是向下采样, pyrDown 期待一个一半于输入图像( tmp)的大小。
  • 注意输入图像的大小(在两个方向)必须是2的冥,否则,将会显示错误。

// opencv_day1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdlib.h>
#include <stdio.h>
using namespace std;
using namespace cv;

int main()
{
	Mat img1;
	cvNamedWindow("原图", WINDOW_AUTOSIZE);
	cvNamedWindow("缩小", WINDOW_AUTOSIZE);
	cvNamedWindow("放大", WINDOW_AUTOSIZE);
	img1 = imread("1.png");
	Mat img2;
	Mat img3;
	pyrUp(img1, img2, Size(img1.cols*2,img1.rows*2));//长宽各缩小为原来的0.5倍
	pyrDown(img1, img3, Size(img1.cols/2, img1.rows/2));
	imshow("原图", img1);
	imshow("缩小", img3);
	imshow("放大", img2);
	waitKey(0);
	return 0;
}



 感觉还是用resize好用啊,虽然同样是缩放功能,但是resize就不会变模糊啊。

 

边缘检测

接下来做下边缘检测,边缘检测的算法有以下三种:Canny算子,Sobel导数,Laplacian算子。

其中,Canny 边缘检测算法 是 John F. Canny 于 1986年开发出来的一个多级边缘检测算法,也被很多人认为是边缘检测的 最优算法.

边缘检测一般要经过以下三个步骤:

  1. 滤波——消除噪声
  2. 增强——使边界轮廓更加明显
  3. 检测——选出边缘点

Canny算法

这个用的多,我就先用它看看效果吧。

/// 运行Canny算子
  Canny( detected_edges, detected_edges, lowThreshold, highThreshold, kernel_size );

输入参数:

  • detected_edges: 原灰度图像
  • detected_edges: 输出图像 (支持原地计算,可为输入图像)
  • lowThreshold: 用户通过 trackbar设定的值。
  • highThreshold: 设定为低阈值的3倍 (根据Canny算法的推荐)
  • kernel_size: 设定为 3 (Sobel内核大小,内部使用)

 

首先先用滤波器进行除噪,emmm,什么滤波器都行,为了方便就用均值滤波器了。

最后边缘检测的结果是这样的:(这是阈值设置比较低的时候的结果,可以设置高一些的阈值,用来检测边缘像素颜色差异比较大的时候)

 

 最低阈值设置为3时:

 最低阈值设置为100时:

// opencv_day1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdlib.h>
#include <stdio.h>
using namespace std;
using namespace cv;
int lowThreshold = 3;
int highThreshold = 9;
int kenel_size = 3;
int main()
{
	Mat img1;
	img1 = imread("1.png");
	cvNamedWindow("原图", WINDOW_AUTOSIZE);
	cvNamedWindow("Canny边缘检测", WINDOW_AUTOSIZE);
	Mat dst,img_gray,edge;
	cvtColor(img1, img_gray, COLOR_BGR2GRAY);
	//用3*3的内核降噪
	blur(img_gray, edge, Size(3, 3));
	//运行Canny算子
	Canny(edge, edge, lowThreshold, highThreshold, kenel_size);
	
	imshow("原图", img1);
	imshow("Canny边缘检测", edge);
	waitKey(0);
	return 0;
}



 Sobel导数

Note

以下内容来自于Bradski和Kaehler的大作: Learning OpenCV .

  1. 上面两节我们已经学习了卷积操作。一个最重要的卷积运算就是导数的计算(或者近似计算).

  2. 为什么对图像进行求导是重要的呢? 假设我们需要检测图像中的 边缘 ,如下图:

    How intensity changes in an edge

    你可以看到在 边缘 ,相素值显著的 改变 了。表示这一 改变 的一个方法是使用 导数 。 梯度值的大变预示着图像中内容的显著变化。

  3. 用更加形象的图像来解释,假设我们有一张一维图形。下图中灰度值的”跃升”表示边缘的存在:

    Intensity Plot for an edge
  4. 使用一阶微分求导我们可以更加清晰的看到边缘”跃升”的存在(这里显示为高峰值)

    First derivative of Intensity - Plot for an edge
  5. 从上例中我们可以推论检测边缘可以通过定位梯度值大于邻域的相素的方法找到(或者推广到大于一个阀值).

Sobel算子

  1. Sobel 算子是一个离散微分算子 (discrete differentiation operator)。 它用来计算图像灰度函数的近似梯度。
  2. Sobel 算子结合了高斯平滑和微分求导.

前面原理可以得知,边缘处,由于颜色变化差异大,所以求导后的值是跃升的,那么依此原理我们可以认为在函数图像发生“跃升”的地方,也就是该点导数的有极值的地方,就是边界处。那么我们接下来可以用Sobel算子对X方向上先进行求导,得到X方向上的边缘图;再从Y方向上进行求导,得到Y方向的边缘图。

最后两个边缘图相加就是整幅图的边缘了。

Sobel算子首先先要进行高斯降噪,灰度化,然后再提取边缘。

计算

假设被作用图像为 I:

  1. 在两个方向求导:

    1. 水平变化: 将 I 与一个奇数大小的内核 G_{x} 进行卷积。比如,当内核大小为3时, G_{x} 的计算结果为:

      G_{x} = \begin{bmatrix}&#xA;-1 & 0 & +1  \\&#xA;-2 & 0 & +2  \\&#xA;-1 & 0 & +1&#xA;\end{bmatrix} * I

    2. 垂直变化: 将:math:I 与一个奇数大小的内核 G_{y} 进行卷积。比如,当内核大小为3时, G_{y} 的计算结果为:

      G_{y} = \begin{bmatrix}&#xA;-1 & -2 & -1  \\&#xA;0 & 0 & 0  \\&#xA;+1 & +2 & +1&#xA;\end{bmatrix} * I

  2. 在图像的每一点,结合以上两个结果求出近似 梯度:

    G = \sqrt{ G_{x}^{2} + G_{y}^{2} }

    有时也用下面更简单公式代替:

    G = |G_{x}| + |G_{y}|

int scale = 1;
int delta = 0;
int ddepth = CV_16S;//图像的深度,为了防止溢出
Mat grad_x, grad_y;//X方向和Y方向上的边缘图
Mat grad;//最后终的边缘图
Mat abs_grad_x, abs_grad_y;//X方向和Y方向的绝对值
Sobel(img1, grad_x, ddepth, 1, 0, 3, scale, delta);//计算X方向上的边缘
  • img1: 在本例中为输入图像
  • grad_x/grad_y: 输出图像.
  • ddepth: 输出图像的深度,设定为 CV_16S 避免外溢。
  • x_order: x 方向求导的阶数。
  • y_order: y 方向求导的阶数。
  • 内核大小
  • scale, delta : 使用默认值

注意为了在 x 方向求导我们使用: x_{order}= 1y_{order} = 0. 采用同样方法在 y 方向求导。

求完了水平梯度后再进行取中间变量,转换为绝对值。

//转换中间结果为绝对值
	convertScaleAbs(grad_x, abs_grad_x);
        convertScaleAbs(grad_y, abs_grad_y);

最后将两个方向上的梯度相加取近似梯度

//将两个方向上的梯度相加取近似梯度
	addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad);

 

// opencv_day1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdlib.h>
#include <stdio.h>
using namespace std;
using namespace cv;
int scale = 1;
int delta = 0;
int ddepth = CV_16S;//图像的深度,为了防止溢出
int main()
{
	Mat img1,img1_gray;
	img1 = imread("1.png");
	cvNamedWindow("原图", WINDOW_AUTOSIZE);
	cvNamedWindow("边缘图", WINDOW_AUTOSIZE);
	GaussianBlur(img1, img1, Size(3, 3), 0, 0);//高斯降噪
	cvtColor(img1, img1, COLOR_BGR2GRAY);
	Mat grad_x, grad_y;//X方向和Y方向上的梯度变化
	Mat grad;//最后终的梯度变化
	Mat abs_grad_x, abs_grad_y;//X方向和Y方向的绝对值

	Sobel(img1, grad_x, ddepth, 1, 0, 3, scale, delta);//计算X方向上的梯度变化
	Sobel(img1, grad_y, ddepth, 0, 1, 3, scale, delta);//计算Y方向上的梯度变化

	//转换中间结果为绝对值
	convertScaleAbs(grad_x, abs_grad_x);
	convertScaleAbs(grad_y, abs_grad_y);

	//将两个方向上的梯度相加取近似梯度
	addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad);
	imshow("原图", img1);
	imshow("边缘图",grad);
	waitKey(0);
	return 0;
}



其实我觉得用Sobel导数求得边缘固然不错,但是不是真正我们需要的边缘,尤其日后要用图像分割技术的时候这样的边缘提取就不好用了,用Canny感觉比较好。嗯~还有一个边缘提取算法没有做,来看看最后一个算法吧。

Laplacian算法(以下原理从http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/imgproc/imgtrans/laplace_operator/laplace_operator.html#laplace-operator转载)

  1. 前一节我们学习了 Sobel 算子 ,其基础来自于一个事实,即在边缘部分,像素值出现”跳跃“或者较大的变化。如果在此边缘部分求取一阶导数,你会看到极值的出现。正如下图所示:

    Previous theory
  2. 如果在边缘部分求二阶导数会出现什么情况?

    Second derivative

    你会发现在一阶导数的极值位置,二阶导数为0。所以我们也可以用这个特点来作为检测图像边缘的方法。 但是, 二阶导数的0值不仅仅出现在边缘(它们也可能出现在无意义的位置),但是我们可以过滤掉这些点。

Laplacian 算子

  1. 从以上分析中,我们推论二阶导数可以用来 检测边缘 。 因为图像是 “2维”, 我们需要在两个方向求导。使用Laplacian算子将会使求导过程变得简单。
  2. Laplacian 算子 的定义:

Laplace(f) = \dfrac{\partial^{2} f}{\partial x^{2}} + \dfrac{\partial^{2} f}{\partial y^{2}}

  1. OpenCV函数 Laplacian 实现了Laplacian算子。 实际上,由于 Laplacian使用了图像梯度,它内部调用了 Sobel 算子

 一样需要高斯平滑后再转为灰度图去提取梯度。

//第三个参数:目标图像深度;第四个参数:滤波器孔径尺寸;第五个参数:比例因子;第六个参数:表示结果存入目标图
Laplacian(gray, dst, CV_16S, 3, 1, 0, BORDER_DEFAULT);
 //计算绝对值,并将结果转为8位
convertScaleAbs(dst, abs_dst);
Laplacian( src_gray, dst, ddepth, kernel_size, scale, delta, BORDER_DEFAULT );

函数接受了以下参数:

  • src_gray: 输入图像。
  • dst: 输出图像
  • ddepth: 输出图像的深度。 因为输入图像的深度是 CV_8U ,这里我们必须定义 ddepth = CV_16S 以避免外溢。
  • kernel_size: 内部调用的 Sobel算子的内核大小,此例中设置为3。
  • scale, deltaBORDER_DEFAULT: 使用默认值。scale = 1, delta = 0

 

头一回看到西红柿这么恶心,,,,,,,,,,,感觉还不如Sobel类=勒,还模糊了。嗯~~~那个图像深度还需要再看看,还有那个CV_8U是怎么回事也需要看看。

// opencv_day1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdlib.h>
#include <stdio.h>
using namespace std;
using namespace cv;
int scale = 1;
int delta = 0;
int ddepth = CV_16S;//图像的深度,为了防止溢出
int main()
{
	Mat img1,img1_gray;
	img1 = imread("1.png");
	cvNamedWindow("原图", WINDOW_AUTOSIZE);
	cvNamedWindow("边缘图", WINDOW_AUTOSIZE);
	GaussianBlur(img1, img1, Size(3, 3), 0, 0);//高斯降噪
	cvtColor(img1, img1, COLOR_BGR2GRAY);
	Mat dst;//输出图像
	Mat abs_dst;
	//对灰度图使用Laplacian算子
	Laplacian(img1, dst, ddepth, 3, scale, delta);
	//将输出图像深度转化为CV_8U
	convertScaleAbs(dst, abs_dst);
	

	imshow("原图", img1);
	imshow("边缘图",abs_dst);
	waitKey(0);
	return 0;
}



 

今天就先学习到这里吧。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值