OpenCV学习笔记(二)

HSV 通道的二值化

很简单,调一个inrange函数即可,函数原型:

void inRange(InputArray src, InputArray lowerb, InputArray upperb, OutputArray dst)

用法类似threshold(),中间两个是阈值,该函数单通道和多通道都可以使用,三通道图像可以用Scalar(h,s,v)来表示阈值

需要注意OpenCV中的HSV空间度量和标准的HSV有所不同,需要换算一下

#include 
#include
using namespace cv;
using namespace std;


int main()
{
	Mat srcMat = imread("C:/Users/bluef/Pictures/2.1.jpg");
	Mat detectMat;
	inRange(srcMat, Scalar(0, 43, 100), Scalar(180, 255, 255), detectMat);
	imshow("src", srcMat);
	imshow("detect", detectMat);
	waitKey(0);
	return 0;
}

上面的代码作用是简单的提取人物的皮肤

(上面代码有点问题,图像要转换成hsv通道的再二值化,inrange参数也要调整下)

灰度图的二值化

OpenCV中有两个灰度图的二值化的函数,一个就是刚刚提到过的threshold


double cv::threshold(
	cv::InputArray src, // 输入图像
	cv::OutputArray dst, // 输出图像
	double thresh, // 阈值
	double maxValue, // 向上最大值
	int thresholdType // 阈值化操作的类型 
);

阈值化类型有下面几种,加INV的表示原有结果取反,前面几种是普通的二值化,后面几种是自适应的二值化,比如OSTU大津法

第二个函数就是自适应阈值化函数 adaptiveThreshold,自适应就是程序自动选择合适的阈值,该函数原型为


void cv::adaptiveThreshold(
	cv::InputArray src, // 输入图像
	cv::OutputArray dst, // 输出图像
	double maxValue, // 向上最大值
	int adaptiveMethod, // 自适应方法,平均或高斯
	int thresholdType // 阈值化类型
	int blockSize, // 块大小
	double C // 常量
);
#include 
#include
using namespace cv;
using namespace std;


int main()
{
	Mat srcMat = imread("C:/Users/bluef/Pictures/2.1.jpg");
	Mat dstMat,thsMat,adtMat;
	cvtColor(srcMat, dstMat, CV_RGB2GRAY);//转为灰度图
	threshold(dstMat, thsMat, 100, 255, THRESH_BINARY);
	adaptiveThreshold(dstMat, adtMat, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 15, 10);
	imshow("src", srcMat);
	imshow("dst", dstMat);
	imshow("threshold", thsMat);
	imshow("adaptive", adtMat);
	waitKey(0);
	return 0;
}

![](https://img.blueflame.org.cn/images/2020/07/opencv2-3.png)

使用前需要先转为灰度图

实现回调函数

我们知道很多函数需要经过反复的调参才能达到预期的效果,所以如何高效的调参是很重要的一件事,opencv提供了一个函数,来实现可视化的快速调参,函数原型如下

CV_EXPORTS int createTrackbar(const string& trackbarname, const string& winname,
                              int* value, int count,
                              TrackbarCallback onChange = 0,
                              void* userdata = 0);
  • trackbarname:滑动空间的名称
  • winname:滑动空间用于依附的图像窗口的名称;
  • value:初始化阈值;
  • count:滑动控件的刻度范围;
  • TrackbarCallback是回调函数,其定义如下:
typedef void (CV_CDECL *TrackbarCallback)(int pos, void* userdata);

下面这个代码是刚刚的二值化函数的回调,需要注意的是这里的图像窗口名需要完全一致,即win_name

#include 
#include
using namespace cv;
using namespace std;

string win_name="dst";
void threshold_Mat(int th, void* data)
{
	Mat src = *(Mat*)(data);

	Mat dst;
	threshold(src, dst, th, 255, THRESH_BINARY);
	imshow(win_name, dst);
}
int main()
{
	Mat srcMat = imread("C:/Users/bluef/Pictures/2.1.jpg");
	Mat dstMat, thsMat, adtMat;
	cvtColor(srcMat, dstMat, CV_RGB2GRAY);
	imshow(win_name, dstMat);

	int lth = 30;
	int mth = 255;
	createTrackbar("threshold", win_name, &lth, mth, threshold_Mat, &dstMat);

	waitKey(0);
	return 0;
}

下面是gamma校正的回调

#include 
#include
using namespace cv;
using namespace std;

string win_name = "dst";

Mat GammaCorrection(float Gamma, Mat src)
{

	uchar lut[256];
	for (int i = 0; i < 256; i++)
		lut[i] = saturate_cast(pow((float)(i / 255.0), Gamma) * 255.0f);
	for (auto it = src.begin(); it != src.end(); it++)
	{
		(*it)[0] = lut[((*it)[0])];
		(*it)[1] = lut[((*it)[1])];
		(*it)[2] = lut[((*it)[2])];
	}
	return src;
}
void onchange(int usedate, void* data)
{
	Mat src = *(Mat*)(data);
	float Gamma = usedate / 100.0f;
	imshow(win_name, GammaCorrection(Gamma, src.clone()));
}
int main()
{
	Mat srcMat = imread("C:/Users/bluef/Pictures/gtest.jpg");
	imshow(win_name, srcMat);

	int lth = 100;
	int mth = 200;
	createTrackbar("threshold", win_name, &lth, mth,onchange, &srcMat);
	waitKey(0);
	return 0;
}

gamma校正的回调

图像形态学基础操作

图像形态学最基本的操作是膨胀和腐蚀,具体什么是膨胀和腐蚀可以参考这篇《数学形态学运算——腐蚀、膨胀、开运算、闭运算》

opencv有dilate膨胀函数和erode腐蚀函数

CV_EXPORTS_W void erode( InputArray src, OutputArray dst, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );
CV_EXPORTS_W void dilate( InputArray src, OutputArray dst, InputArray kernel,
                          Point anchor = Point(-1,-1), int iterations = 1,
                          int borderType = BORDER_CONSTANT,
                          const Scalar& borderValue = morphologyDefaultBorderValue() );

可以看到两者用法基本相同

  • src:输入图,可以多通道,深度可为CV_8U、CV_16U、CV_16S、CV_32F或CV_64F。
  • dst:输出图,和输入图尺寸、形态相同。
  • kernel:结构元素,如果kernel=Mat()则为预设的3×3矩形,越大腐蚀或膨胀的效果越明显。
  • anchor:原点位置(锚点),预设为结构元素的中央。
  • iterations:执行次数,预设为1次,执行越多次腐蚀或膨胀效果越明显。

这里的kernel我们一般通过一个getstructuringElement函数来获得

Mat getStructuringElement(int shape, Size ksize, Point anchor=Point(-1,-1))
  • shape:模板形状,有MORPH_RECT、MORPH_ELLIPSE、MORPH_CROSS三种可选。
  • ksize:模板尺寸。

除了这两个基本操作,图像形态学还有个开运算和闭运算,开运算即先腐蚀再膨胀,闭运算则相反

opencv提供了一个基本形态学函数

void morphologyEx( InputArray src, OutputArray dst,
                                int op, InputArray kernel,
                                Point anchor = Point(-1,-1), int iterations = 1,
                                int borderType = BORDER_CONSTANT,
                                const Scalar& borderValue = morphologyDefaultBorderValue() )

用法和erode和dilate差不多,多了给op表示操作类型,具体参数如下,里面的膨胀和腐蚀与dilate和erode效果完全相同

enum MorphTypes{
    MORPH_ERODE    = 0, //腐蚀
    MORPH_DILATE   = 1, //膨胀
    MORPH_OPEN     = 2, //开操作
    MORPH_CLOSE    = 3, //闭操作
    MORPH_GRADIENT = 4, //梯度操作
    MORPH_TOPHAT   = 5, //顶帽操作
    MORPH_BLACKHAT = 6, //黑帽操作
    MORPH_HITMISS  = 7  
};
#include 
#include
using namespace cv;
using namespace std;


int main()
{
	Mat srcMat = imread("C:/Users/bluef/Pictures/coin.png");
	Mat dilateMat, erodeMat;
	Mat openMat, closeMat;
	Mat dstMat;
	threshold(srcMat, dstMat, 110, 255, THRESH_BINARY_INV);
	Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
	dilate(dstMat, dilateMat, kernel);
	erode(dstMat, erodeMat, kernel);
	morphologyEx(dstMat, openMat, MORPH_OPEN, kernel);
	morphologyEx(dstMat, closeMat, MORPH_CLOSE, kernel);
	imshow("src", srcMat);
	imshow("dst", dstMat);
	imshow("dilate", dilateMat);
	imshow("erode", erodeMat);
	imshow("open", openMat);
	imshow("close", closeMat);
	waitKey(0);
	return 0;
}

为了效果明显点我把前后景反转了一下,可以注意到所有的膨胀和腐蚀是针对白色部分的。

连通域分析是图像处理中常用到的方法,在物体计数时时常用到

opencv提供了两个连通域分析的函数,第二个和第一个的区别就是多了个中心坐标矩阵和状态矩阵

int cv::connectedComponents (
    cv::InputArrayn image, // input 8-bit single-channel (binary)
    cv::OutputArray labels, // output label map
    int connectivity = 8, // 4- or 8-connected components
    int ltype = CV_32S // Output label type (CV_32S or CV_16U)
);
int cv::connectedComponentsWithStats (
    cv::InputArrayn image, // input 8-bit single-channel (binary)
    cv::OutputArray labels, // output label map
    cv::OutputArray stats, // Nx5 matrix (CV_32S) of statistics:
    // [x0, y0, width0, height0, area0;
    // ... ; x(N-1), y(N-1), width(N-1),
    // height(N-1), area(N-1)]
    cv::OutputArray centroids, // Nx2 CV_64F matrix of centroids:
    // [ cx0, cy0; ... ; cx(N-1), cy(N-1)]
    int connectivity = 8, // 4- or 8-connected components
    int ltype = CV_32S // Output label type (CV_32S or CV_16U)
);
  • lable是个标签地图,标记了每个像素属于哪个连通域
  • connectivity选择是4邻域还是8邻域,即只用上下左右是相邻,还是九宫格内都是相邻
  • stats是一个状态矩阵大小是连通域数量*5,每行5个参数分别为该连通域最小外接四边形的x,y,width,height,面积(即像素数量)
  • centroids为一个大小为连通域数量*2的矩阵,每行两个参数为该连通域的中心坐标x和y
#include 
#include
using namespace cv;
using namespace std;


int main()
{
	Mat srcMat = imread("C:/Users/bluef/Pictures/coin.png", 0);
	Mat dstMat,labels,stats,centroids;
	threshold(srcMat, dstMat, 90, 255, THRESH_BINARY);
	int num = connectedComponentsWithStats(dstMat, labels, stats, centroids);
	cout << num - 1 << endl;//减去背景
    vector colors(num);
    colors[0] = Vec3b(0, 0, 0);
    for (int i = 1; i < num; i++)
        colors[i] = Vec3b(rand() % 256, rand() % 256, rand() % 256);
  
    Mat img_color = Mat::zeros(dstMat.size(), CV_8UC3);
    for (int y = 0; y < img_color.rows; y++)
        for (int x = 0; x < img_color.cols; x++)
            img_color.at(y, x) = colors[labels.at(y, x)];
	for (int i = 1; i < num; i++)
		rectangle(img_color, Rect(stats.at(i, 0), stats.at(i, 1), stats.at(i, 2), stats.at(i, 3)), Scalar(255, 255, 255));
	imshow("res", img_color);
	waitKey(0);
	return 0;
}

需要注意连通域分析的对象也是白色的部分,函数的返回值就是连通域的数量,第0个连通域为背景,函数只能对单通道图片使用,使用前需要对图像二值化

#include 
#include
using namespace cv;
using namespace std;


int main()
{
	Mat srcMat = imread("C:/Users/bluef/Pictures/2.5.jpg", 0);
	Mat dstMat, openMat, labels, stats, centroids;
	threshold(srcMat, dstMat, 0, 255, THRESH_OTSU);
	bitwise_not(dstMat, dstMat);
	Mat kernel = getStructuringElement(MORPH_RECT, Size(10, 10));
	morphologyEx(dstMat, openMat, MORPH_ERODE, kernel);
	imshow("src", srcMat);
	imshow("dst", openMat);
	int num = connectedComponentsWithStats(openMat, labels, stats, centroids);
	cout << num - 1 << endl;//减去背景
	waitKey(0);
	return 0;
}

可以看到如果不进行腐蚀运算,那么就无法统计图中的原点个数,我们需要把中间连接部分给腐蚀掉

有的时候图片中会有一些不是你所需要的,但无法通过图像形态学的方法来去除,就需要在连通域分析后,根据特征把不需要的部分给去掉,比如去掉面积小于一定程度的

#include 
#include
using namespace cv;
using namespace std;


int main()
{
	Mat srcMat = imread("C:/Users/bluef/Pictures/clip.png", 0);
	Mat dstMat, openMat, labels, stats, centroids;
	threshold(srcMat, dstMat, 56, 255, THRESH_BINARY_INV);
	imshow("dst", dstMat);
	int num = connectedComponentsWithStats(dstMat, labels, stats, centroids);
	vector colors(num);
	colors[0] = Vec3b(0, 0, 0);
	for (int i = 1; i < num; i++)
	{
		colors[i] = Vec3b(rand() % 256, rand() % 256, rand() % 256);
		if (stats.at(i, cv::CC_STAT_AREA) < 3000)
		{
			colors[i] = Vec3b(0, 0, 0);
			num--;
		}
	}
	Mat img_color = Mat::zeros(dstMat.size(), CV_8UC3);
	for (int y = 0; y < img_color.rows; y++)
		for (int x = 0; x < img_color.cols; x++)
			img_color.at(y, x) = colors[labels.at(y, x)];
	cout << num - 1 << endl;//减去背景
	imshow("res", img_color);
	waitKey(0);
	return 0;
}

下图中可以注意到二值化后的图片左侧有一条细细的白色区域,这个区域显然不是我们想要的回形针,所以要把这部分区域去掉

图像平滑是一种区域增强的算法,平滑算法有邻域平均法、中指滤波、边界保持类滤波等。在图像产生、传输和复制过程中,常常会因为多方面原因而被噪声干扰或出现数据丢失,降低了图像的质量(某一像素,如果它与周围像素点相比有明显的不同,则该点被噪声所感染)。这就需要对图像进行一定的增强处理以减小这些缺陷带来的影响。
       图像平滑 有均值滤波、方框滤波、中值滤波和高斯滤波等。

这几种滤波的区别可以参考这篇《数字图像处理:图像平滑 (均值滤波、中值滤波和高斯滤波)》

均值滤波

void blur( InputArray src, OutputArray dst,
                        Size ksize, Point anchor = Point(-1,-1),
                        int borderType = BORDER_DEFAULT );
  • src: 输入图像,可以是Mat类型 
  • dst: 经滤波后输出图像 
  • ksize: Size类型,内核的大小,一般用Size(w, h)表示,如Size(3, 3)表示kernel窗口大小为3x3 
  • anchor = Point(-1,-1): 进行滤波操作的点,如果是默认值(-1, -1)说明对上述窗口中心点所对应的像素点进行操作 
  • borderType = BORDER_DEFAULT: 用于腿短图像外部像素的某种便捷模式,有默认值BORDER_DEFAULT. 

中值滤波

void medianBlur( InputArray src, OutputArray dst, int ksize );

ksize为孔径的线性尺寸(aperture linear size),注意这个参数必须是大于1的奇数

高斯滤波

void GaussianBlur( InputArray src, OutputArray dst, Size ksize,
                                double sigmaX, double sigmaY = 0,
                                int borderType = BORDER_DEFAULT );
  • 前几个参数和blur差不多
  • sigmaX,表示高斯核函数在X方向的的标准偏差。
  • sigmaY,表示高斯核函数在Y方向的的标准偏差。

下面的代码是对摄像头进行不同的滤波进行对比,参数懒得改,这里效果不太明显,就不放图了

#include 
#include
#include
using namespace cv;
using namespace std;



int main()
{
	VideoCapture cap(0);

	while (1)
	{
		Mat frame,blurMat,median,gaussian;
		cap >> frame;
		blur(frame, blurMat, Size(3, 3));
		medianBlur(frame, median, 5);
		GaussianBlur(frame, gaussian, Size(5, 5), 1);

		imshow("frame", frame);
		imshow("blur", blurMat);
		imshow("median", median);
		imshow("gaussian", gaussian);
		waitKey(30);
	}
	return 0;
}

边缘检测

常用的一个是sobel算子,一般是检测水平边缘或垂直边缘,函数原型如下

void Sobel( InputArray src, OutputArray dst, int ddepth,
                         int dx, int dy, int ksize = 3,
                         double scale = 1, double delta = 0,
                         int borderType = BORDER_DEFAULT );
  • src: 输入的源影像 
  • dst: 输出的目标影像,大小和通道数与源影像相同。深度由ddepth来决定
  • ddepth: 目标影像的深度;当源影像的深度为CV_8U时,一般ddepth选择为CV_16S
  • dx:x方向上的导数因子
  • dy:Y方向上的导数因子
#include 
#include
#include
using namespace cv;
using namespace std;



int main()
{
	VideoCapture cap(0);

	while (1)
	{
		Mat frame, grad_x, grad_y;
		cap >> frame;

		Sobel(frame, grad_x, CV_16SC1, 1, 0, 3);
		Sobel(frame, grad_y, CV_16SC1, 0, 1, 3);
		convertScaleAbs(grad_x, grad_x);//该函数用于图像增强,否则图像不明显
		convertScaleAbs(grad_y, grad_y);
		//imshow("frame", frame);
		imshow("x", grad_x);
		imshow("y", grad_y);
		waitKey(30);
	}
	return 0;
}

一般情况下会将图片转换成灰度图(这里我懒得加了)

为什么我是横着的呢?因为我的笔记本是竖着放的

注意y方向求导是得到水平的边缘,x方向求导是垂直的边缘

另一种常用于边缘检测的是canny算子

canny函数有两个api

void Canny( InputArray image, OutputArray edges,
                         double threshold1, double threshold2,
                         int apertureSize = 3, bool L2gradient = false );
void Canny( InputArray dx, InputArray dy,
                         OutputArray edges,
                         double threshold1, double threshold2,
                         bool L2gradient = false );

两个没什么区别,区别就是把输入图像换成sobel函数得到的dx和dy的图像 apertureSize就是前面sobel函数的ksize,我理解为区别就是你自已用sobel还是它帮你用sobel

#include 
#include
#include
using namespace cv;
using namespace std;




int main()
{
	VideoCapture cap(0);

	while (1)
	{
		Mat frame,dst;
		cap >> frame;
		Canny(frame, dst, 50, 100);
		imshow("frame", frame);
		imshow("dst", dst);
		waitKey(30);
	}
	return 0;
}

canny算子检测的是所有的边缘

磨皮

数字图像处理应用最多的地方除了人工智能就是美颜了吧,其实通过简单的几个函数就可以实现简单的磨皮

初次之外还需要知道一个东西叫mask,可以参考这篇详解掩膜mask

简单来说mask就是一块板子,板子上被挖空了一部分(非0的像素)

当你使用image1.copyto(image2,mask)时,就是在image2上盖上mask这块板,然后把image1涂上去,这时候拿开板子就得到了image2

这里真正实现磨皮的其实是双边滤波,下面是函数原型

void bilateralFilter(InputArray src, OutputArray dst, int d, double sigmaColor, double sigmaSpace, int borderType=BORDER_DEFAULT )
  • InputArray src: 输入图像,可以是Mat类型,图像必须是8位或浮点型单通道、三通道的图像。
  • OutputArray dst: 输出图像,和原图像有相同的尺寸和类型。
  • int d: 表示在过滤过程中每个像素邻域的直径范围。如果这个值是非正数,则函数会从第五个参数sigmaSpace计算该值。
  • double sigmaColor: 颜色空间过滤器的sigma值,这个参数的值月大,表明该像素邻域内有月宽广的颜色会被混合到一起,产生较大的半相等颜色区域。
  • double sigmaSpace: 坐标空间中滤波器的sigma值,如果该值较大,则意味着颜色相近的较远的像素将相互影响,从而使更大的区域中足够相似的颜色获取相同的颜色。当d>0时,d指定了邻域大小且与sigmaSpace五官,否则d正比于sigmaSpace.
  • int borderType=BORDER_DEFAULT: 用于推断图像外部像素的某种边界模式,有默认值BORDER_DEFAULT.

试了下各种滤波,双边滤波的磨皮效果是最好的

下面是完整代码

#include 
#include
using namespace cv;
using namespace std;


int main()
{
	Mat srcMat = imread("C:/Users/bluef/Pictures/skin3.jpg");
	Mat mask;
	imshow("before", srcMat);
	inRange(srcMat, Scalar(0, 43, 100), Scalar(180, 255, 255), mask);
	Mat skin;
	srcMat.copyTo(skin, mask);
	Mat temp;
	bilateralFilter(skin, temp, 15, 37.5, 37.5);
	temp.copyTo(srcMat, mask);
	imshow("after", srcMat);
	waitKey(0);
	return 0;
}

这里代码和最上面的问题一样,我忘了转化成hsv通道的

思路很简单,就是提取皮肤部分进行双边滤波,再把皮肤覆盖回原图