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, <h, 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, <h, mth,onchange, &srcMat);
waitKey(0);
return 0;
}
图像形态学基础操作
图像形态学最基本的操作是膨胀和腐蚀,具体什么是膨胀和腐蚀可以参考这篇《数学形态学运算——腐蚀、膨胀、开运算、闭运算》
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通道的
思路很简单,就是提取皮肤部分进行双边滤波,再把皮肤覆盖回原图