”开篇提要:因为是任务驱动型学习,所以没有一个系统的体系,只有前人的脚步可循。”
row = 1:从第二行开始;rows - 1 :最后一行舍去;
col = offsetx :从3开始,即第二个数,结合row = 1得到初始像素点为(1,1),
01、定义出两个Mat文件,src存输入的图像值,dst储存输出的图像。因为dst文件的大小没定,所以在开始时要进行初始化。定义出跟原图像大小一样但是纯黑的图像
dst = Mat::zeros(src.size(), src.type()); //与matlab语句类似的函数
02、开始时候判断是否输入图像为空有新的API判定:
一个是用!src.data
if (!src.data) {
printf("image can not load...\n");
return -1;
}
另一个是用.empty()
if(src.empty()){
cout << "img cannot load...\n" << endl;
return -1;
}
03、指针遍历
const uchar* previous = src.ptr(row - 1):返回一个指向当前格上一格像素的指针
const uchar* current = src.ptr(row): 返回指向当前格像素的指针
const uchar* next = src.ptr(row + 1):返回一个指向当前格下一格像素的指针
步长为三(channel),每格像素都可被掩膜覆盖到。
for (int row = 1; row < (rows - 1); row++) {
const uchar* previous = src.ptr(row - 1);
const uchar* current = src.ptr(row);
const uchar* next = src.ptr(row + 1);
uchar* output = dst.ptr(row);
for (int col = offsetx; col < cols; col++) {
output[col] = saturate_cast(5 * current[col] - (current[col - offsetx] + current[col + offsetx] + previous[col] + next[col]));
04.1、使用函数进行像素处理:
//saturate_cast(-100)返回0
//saturate_cast(288) 返回255
//saturate_cast(100) 返回100
这个函数的功能是确保RGB值的范围在0~255之间
而uchar类型的范围为0~255,所以使用了这个函数后输出的值也定为255 :)
源码:
#include
#include
using namespace cv;
int main(int argc, char** argv) {
Mat src, dst;
src = imread("D:/搬家/study/maomao.jpg");
if (!src.data) {
printf("image can not load...\n");
}
namedWindow("output window", WINDOW_AUTOSIZE);
imshow("output window", src);
int cols = (src.cols-1) * src.channels();
int offsetx = src.channels();
int rows = src.rows;
dst = Mat::zeros(src.size(), src.type());
for (int row = 1; row < (rows - 1); row++) {
const uchar* previous = src.ptr(row - 1);
const uchar* current = src.ptr(row);
const uchar* next = src.ptr(row + 1);
uchar* output = dst.ptr(row);
for (int col = offsetx; col < cols; col++) {
output[col] = saturate_cast(5 * current[col] - (current[col - offsetx] + current[col + offsetx] + previous[col] + next[col]));
}
}
namedWindow("constant image demo", WINDOW_AUTOSIZE);
imshow("constant image demo",dst);
waitKey(0);
}
调用函数filter2D
小数组:使用cv::Mat::create实现:create函数传入参数的意义:前两个为行跟列, 后一个中8表示8位, U表示无符号 ,char表示Char类型3表示通道数目是3,第四个参数是向量表示初始化每个像素的值是多少,向量长度对应通道数目一致
Mat M;
M.create(4,3,CV_8UC2); //使用create不能进行赋值
M = Scalar(127,127); //使用Scalar(向量)赋值
cout << "M=" << endl << "" << M << endl<< endl;
uchar* firstRow = M.ptr(0);
printf("%d", *firstRow) //输出灰度图像第一个像素值
输出
M=
[127, 127, 127, 127, 127, 127;
127, 127, 127, 127, 127, 127;
127, 127, 127, 127, 127, 127;
127, 127, 127, 127, 127, 127]
127
前三行可以写成:
Mat M(rows, cols, CV_8UC3, Scalar(255, 0 ,0));
源码:
#include
#include
#include
using namespace cv;
using namespace std;
int main(int argc, char** argv) {
Mat src;
namedWindow("output img", WINDOW_AUTOSIZE);
namedWindow("input img", WINDOW_AUTOSIZE);
src = imread("D:/搬家/Study/maomao.jpg");
if (!src.data) {
cout << "img cannot load...\n" << endl;
return -1;
}
Mat csrc;
Mat kernel = (Mat_(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
filter2D(src, csrc, -1, kernel);
imshow("output img", csrc);
imshow("input img", src);
waitKey(0);
Mat m = Mat::eye(3, 3, CV_8UC1);
cout << "m=" << endl << m << endl;
}
要注意:调用API后编译速度会减慢(MOST)
1、对Mat对象规定它跟原来的大小一样,类型一样,用向量函数赋值。
dst = Mat(src.size(), src.type());
dst = Scalar(127, 0, 255);
2.1、克隆出一样的:使用clone
Mat dst = src.clone();
imshow("input img", dst);
2.2、或者用copyTo
Mat dst;
src.copyTo(dst);
2.3、查看输入的通道数:调用channel()函数
int channels = src.channel();
03、像素指针
const uchar *firstRow = dst.ptr(0);
作用是返回一个指向像素值的指针,这里返回dst图像的第一个像素值。老司机们可以通过指针遍历来对图像进行操作。
04.1、Matlab类型的函数 CV::zeros:);
Mat m = Mat::zeros(2, 2, CV_8UC1);
cout << "m=" << endl << m << endl;
生成一个单通道,2 x 2 的Mat图像
输出:全为0即是纯黑
m=
[ 0, 0;
0, 0]
04.2、
Mat m = Mat::eye(3, 3, CV_8UC1);
cout << "m=" << endl << m << endl;
输出:对角线为1,其他为0
m=
[ 1, 0, 0;
0, 1, 0;
0, 0, 1]
imread:指定加载为灰度或者RGB图像
imwrite:保存图像文件,由扩展名决定
单通道:读一个GRAY像素点像素值(CV_8UC1)
Scalar intensity = img.at(y, x);
Scalar intensity = img.at(Point(x, y));
源码:
Mat src, gray_src;
namedWindow("output img", WINDOW_AUTOSIZE);
namedWindow("input img", WINDOW_AUTOSIZE);
src = imread("D:/搬家/Study/maomao.jpg");
if (!src.data) {
cout << "img cannot load...\n" << endl;
return -1;
}
cvtColor(src , gray_src, COLOR_BGR2GRAY);
int height = gray_src.rows;
int width = gray_src.cols;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int gray = gray_src.at(row, col);
gray_src.at(row, col) = 255 - gray; //每个像素值取反,输出图像为gray_scr的反色效果
}
}
imshow("output img", gray_src);
imshow("input img", src);
waitKey(0);
三通道:读一个RGB像素点的像素值:
Vec3f intensity = img.at(y, x);
float blue = intensity.val[0];
float green = intensity.val[1];
float red = intensity.val[2];
源码:输出反色
Mat dst;
dst.create(src.size(), src.type());
int nc = dst.channels();
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
if (nc == 1) {
int gray = gray_src.at(row, col);
gray_src.at(row, col) = 255 - gray;
}
else if (nc == 3) {
int b = src.at(row, col)[0];
int g = src.at(row, col)[1];
int r = src.at(row, col)[2];
dst.at(row, col)[0] = 255 - b;
dst.at(row, col)[1] = 255 - g;
dst.at(row, col)[2] = 255 - r;
}
}
先定义一个和原图像大小类型一样的Mat图像,然后判断channel值,若是单通道,nc为1时,使用第一种取像素值反值;若是多通道,nc为3时,使用后者,而且一定按照 b g r 的顺序写,不可乱(原因在于存储的顺序及就是b g r)。0对应第一个blue通道,1对应第二个green通道,2对应第三个red通道。
Vec3b:对应的是B G R 的uchar类型
Vec3f:对应的是float类型
把CV_8UC1转换到CV32F1实现如下:
src.converyTo(dst, CV_32F);
源码
dst.convertTo(dst, CV_32F);
float b = dst.at(row, col)[0];
float g = dst.at(row, col)[1];
float r = dst.at(row, col)[2];
dst.at(row, col)[0] = 255 - b;
dst.at(row, col)[1] = 255 - g;
dst.at(row, col)[2] = 255 - r;
bitwise_not(src, dst);
输出取反后的图像:反色效果
img = Scalar(0);
使用max,min前需要先调用
max(r,max(b, g));
min(r,min(b, g));
此时对gray_src,有src_gray.at(col, row) = max(r,max(b, g)) 输出较亮的灰度
或src_gray.at(col, row) = min(r,min(b, g)) 输出较暗的灰度
首先读取图像,使用cvtColor改变通道数,使用循环遍历每个像素值进行操作,对像素值的读取分为单通道和多通道两种。对像素值的处理:全为0为黑色,2全为55白色,255-pix值为反色,BGR通道取最大值进行取亮度不同的灰度。imread读出图像,imwrite读写图像,waitKey来保持图像显示。图像处理千万别忘了要中心化!
介绍了三种混合方式:直接相加,权重相加,相乘相加。
相加前提:两图像的大小相同。这里试图用create来使得两个大小,类型相等。所以首先加一个判断
if (src1.cols == src2.cols && src1.rows == src2.rows && src1.type() == src2.type())
⭐权重相加:
addWeighted(src1, alpha, src2, beta, 0.0, dst);
输入参数:输入的图像1,双精度浮点数alpha, 输入的图像2 , 双精度浮点数beta (即alpha - 1), gamma 值(为了使得当前像素点更亮而增设的常量,),输出的图像dst
理论:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3L0erxqa-1627700542335)(C:\Users\FileCrasher\AppData\Roaming\Typora\typora-user-images\image-20201220233648033.png)]
像素线性混合操作:每个像素值混合。一个x对应一个像素。
⭐相加:
add(src1, src2, dst, Mat());
像素值直接相加
⭐相乘:
multiply(src1, src2, dst, 1.0);
#include
#include
#include
using namespace cv;
using namespace std;
int main(int argc, char** argv) {
Mat src, dst;
src = imread("D:/搬家/Study/opencv学习/药瓶缺陷类型/尺寸.png");
char input_win[] = "output window";
namedWindow(input_win, WINDOW_AUTOSIZE);
if (src.empty()) {
printf("img cannot load...\n");
return -1;
}
int width = src.cols;
int height = src.rows;
dst = Mat::zeros(src.size(), src.type());
float alpha = 1.2;
float beta = 100;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++)
if (src.channels() == 3){
float b = src.at(row, col)[0];
float g = src.at(row, col)[1];
float r = src.at(row, col)[2];
dst.at(row, col)[0] = saturate_cast(b*alpha+beta);
dst.at(row, col)[1] = saturate_cast(g*alpha+beta);
dst.at(row, col)[2] = saturate_cast(r*alpha+beta);
}
else if (src.channels() == 1) {
float v = dst.at(row, col);
}
}
char output_title[] = "contrast and brightness change demo";
namedWindow(output_title, WINDOW_AUTOSIZE);
imshow(output_title, dst);
imshow(input_win, src);
waitKey(0);
return 0;
}
saturate_cast 使用以保证图像的范围为0-255之间。当然有时候像素值可能为负值,比如tif类型图像。
b*alpha+beta 每个通道的像素值进行操作,beta值越大,其所输出的图像越大,与周围的对比度更强烈。
使用cv::Point与cv::Scalar绘制线,矩形,圆,椭圆,多边形等基本几何形状。随机生成与绘制文本。代码演示。
Point表示2D平面上的一个点x ,y,是一个类:
Point p;
p.x = 10;
p.y = 8;
or
p = Point(10, 8);
Scalar表示四个元素的向量
Scalar(a, b, c);
a = blue, b=green, c=red; //表示B G R三个通道
0、画线cv::line (LINE_4\LINE_8\LINE_AA )
4和8分别表示4领域和8领域的Bresenham, 而AA代表高斯平滑处理, 这三种都代表反锯齿, 只是算法不同。
传入参数:图像, 第一点, 第二点, 颜色, 线宽(1), 线型
Point p1 = Point(20, 20);
Point p2;
p2.x = 100;
p2.y = 100;
Scalar color = Scalar(0, 255, 255);
line(bg_image, p1, p2, color, 1, LINE_8);
1、画椭圆cv::ellipse
ellipse(bg_image, Point(bg_image.cols/2, bg_image.rows/2), Size(bg_image.cols/4, bg_image.rows/8), 45, 0, 180, color, 2, LINE_AA);
第一个Point为中心点,第二个为大小,调整为与图片长宽成一定比例,后一个45表示45°倾斜, 0,180表示绘制椭圆的范围,颜色,线宽, 线型。
2、画矩形cv:rectangle
Rect rect(100, 100, 300, 300); //前两个参数定义第一个点的x , y后两个定义长宽
Scalar color = Scalar(0, 255, 0); //定义颜色
rectangle(bg_image, rect, color, 2, LINE_8);
3、画圆cv::circle
Point center = Point(bg_image.cols/2, bg_image.rows/2);
circle(bg_image, center, 150 , color, 2, LINE_AA);
图像, 中心点, 半径, 颜色, 线宽, 线性。
4、填充 && 画多边形cv::fillPoly
Point pts[1][5];
pts[0][0] = Point(100, 100);
pts[0][1] = Point(100, 200);
pts[0][2] = Point(200, 200);
pts[0][3] = Point(200, 100);
pts[0][4] = Point(100, 100);
const Point* ppts[] = { pts[0] };
int npt[] = { 5 };
Scalar color = Scalar(222, 33, 125);
fillPoly(bg_image, ppts, npt, 1, color, 8);
因为fillPoly后面有一个指针,所以定义点的时候用指针定义。
这个技术的实用性在于:做目标识别,最后用框框圈出,标注并用文字标出其类别。
5、使用putText打上标注
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lshq9GJm-1627700542338)(C:\Users\FileCrasher\AppData\Roaming\Typora\typora-user-images\image-20201221224258408.png)]
出现问题:文字打不上去,可能因为字体原因。
putText(bg_image, "这是尺寸标注", Point(100,200), FONT_HERSHEY_COMPLEX, 2.0, Scalar(0 ,255, 255),1, 8);
6、随机颜色填充
void RandomLineDemo() {
RNG rng(12345); //先生成种子,一般用时间种子
Point pt1;
Point pt2;
Mat bg = Mat::zeros(bg_image.size(),bg_image.type());
namedWindow("random line demo", WINDOW_AUTOSIZE);
for (int i = 0; i < 10000; i++) {
pt1.x = rng.uniform(0, bg_image.cols);
pt2.x = rng.uniform(0, bg_image.cols);
pt1.y = rng.uniform(0, bg_image.rows);
pt2.y = rng.uniform(0, bg_image.rows);
Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
if (waitKey(50) > 0) {
break;
}
line(bg, pt1, pt2, color, 1, 8);
imshow("random line demo", bg);
}
使用随机生成的数赋值给:两点的坐标,颜色,使用循环实现多次画线,填充出来的效果很棒。
Smooth/Blur是图像处理中最简单和常用的操作之一
使用该操作的原因:给图像预处理的时候降低噪声。
使用Smooth/Blur操作背后就是数学的卷积运算,放在图像上一个一个离散的操作变成了求和,问题简单化。
通常这些卷积算子计算都是线性操作,所以又叫线性滤波。
对于一个6 x 6 的像素点矩阵, 卷积过程从左到右,从上到下移动,黄色的每个像素点的值之和取平均值赋给中间像素作为它卷积后的新像素值。步长为1.
对于边缘处理问题的解决:
3x3的点阵边缘会少一格不被卷积,可以往外row和col各扩充两行,就外面加一圈。
5x5的点阵会少两行,一样的可以加两圈。处理边缘像素后面会有讲。
对于一个大小为a x b的像素点阵:
K = 像素值求和 / a x b(如3 x 3大小的点阵中的九个像素值加起来除以9)
均值模糊:
blur(Mat src, Mat dst, Size(xradius, yradius), Point(-1,-1));
Point表示中心点在哪里,那么3 x 3的矩阵自然-1, -1在中心
其中Size(x, y) ,x, y必须是正数而且是奇数
x, y越大,其图像输出越模糊。
⭐x == y时,整体上模糊; x > y时,横向模糊(左右横跳之感); x < y时,纵向模糊(上下跃动之感)。
图像呈现正态分布型,有个权重。与盒子滤波相比会保留图像一些原有的特征。比如原来的像素值很大,经过高斯滤波后不论周围的值再小它还是比较大,但盒子滤波不行。
高斯模糊:
-GaussianBlur(Mat src, Mat dst, Size(11, 11).sigmax, segmay);
sigmay不输入,就根据sigmax找,再根据autosize找。
基于统计学的一种滤波:统计排序滤波:对胡椒噪声(0 或 255)具有良好的过滤作用
第一种:排序后取中值,赋值给Center Point(-1,-1):那么最大的255和最小的0自然会被滤掉
还可以取最大值滤波 or 最小值滤波
⭐中值模糊:
参数:输入,输出,卷积核大小(一定是奇数1,3,5…)
medianBlur(src, dest, ksize);
主要是高斯双边滤波:克服了边缘像素信息丢失缺陷。均值滤波是基于平均weight = 1,所以无法克服该问题。
高斯模糊部分克服了该缺陷,但是完全没法避免,因为没有考虑像素值的问题。当中心与边缘的像素值之差超过一个范围时可以跳过其处理。双边滤波正是实现了这个处理。越靠近中心其权重值越大。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zx10IQjA-1627700542341)(C:\Users\FileCrasher\AppData\Roaming\Typora\typora-user-images\image-20201222211203133.png)]
空域核:每一个都有权重,由sigma决定是这样比较陡的曲线还是比较大的钟摆。
值域核:像素值在一定范围内去模糊,超过一定范围不去模糊:相差的部分并没有被抹去,则图像的特征被保留。
那么需要两个参数:一个是空域核的窗口大小,另一个是值域核的大小
⭐双边模糊:
bilateralFilter(src, dest, d=15, 150, 3);
-15-计算的半径,半径之内的像数都会被纳入计算,如果提供-1,则根据sigma space参数取值-150- sigma color决定多少差值之内的像素会被计算
-3- sigma space如果d的值大于o则声明无效,否则根据它来计算d值。
中值模糊的ksize大小必须是大于1而且必须是奇数。
与高斯模糊的对比:更具立体感
使用filter2D来达到更好的PS效果
图像形态学操作-基于形状的一系列图像处理操作的合集,主要是基于集合论基础上的形态学数学
形态学有四个基本操作:腐蚀、膨胀、开、闭
膨胀与腐蚀是图像处理中最常用的形态学操作手段
膨胀:跟卷积操作类似,假设有图像A和结构元素BⅠ结构元素B在A上面移动,其中B定义其中心为锚点,计算B覆盖下A的最大像素值用来替换锚点的像素,其中B作为结构体可以是任意形状(圆,线,矩形…)
腐蚀:与膨胀唯一的不同是用最小值代替锚点像素。
getStructuringElement(int shape, Size ksize, Point Anchor);
形状(MORPH_RECT\MORPH_CROSS\MORPH_ELLIPSE)
锚点:默认为(-1, -1)为中心点
dilate(src, dst, kernel);
erode(src, dst, kernel);
对于erode:
erode(src, dst, structureElement);
后面两个参数可以不输,用默认值
应用:对噪声的处理,若背景为白色则可膨胀;黑色则反之。
#include
#include
#include
using namespace cv;
using namespace std;
char OUTPUT_WIN[] = "output img";
int element_size = 3; //设置结构元素的大小
int max_size = 21;
void CallBack_Demo(int, void*);
Mat src, dst;
int main(int argc, char **argv) {
src = imread("D:/搬家/Study/maomao.jpg");
if (!src.data) {
cout << "img can not load...\n" << endl;
return -1;
}
namedWindow(OUTPUT_WIN, WINDOW_AUTOSIZE);
createTrackbar("Element Size", OUTPUT_WIN, &element_size, max_size, CallBack_Demo); //使用它进行Callback循环
CallBack_Demo(0, 0);
imshow(OUTPUT_WIN, src);
waitKey(0);
return 0;
}
void CallBack_Demo(int, void*) {
int s = element_size * 2 + 1;
Mat structureElement = getStructuringElement(MORPH_RECT, Size(s, s), Point(-1, -1));
dilate(src, dst, structureElement, Point(-1, -1), 1);
imshow(OUTPUT_WIN, dst);
return;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qvIMWyd0-1627700542344)(C:\Users\FileCrasher\AppData\Roaming\Typora\typora-user-images\image-20201222223221683.png)]
全局变量的定义放在了name space 前面,报一堆错,被自己坑了.
开操作:OPEN 先腐蚀后膨胀:即将小的噪声去掉,主体膨胀后恢复。
Mat kernel = getStructuringElement(MORPH_RECT, Size(11, 11), Point(-1, -1));
morphologyEx(src, dst, MORPH_OPEN, kernel);
在调用形态学处理API之前,一定先创好结构体,然后作为最后一个参数传入,剩下倒数第二个参数为操作的类型。可以每个API都调用一遍看看效果。效果不明显则调大Size。
闭操作:close 先膨胀后腐蚀
形态学梯度-Morphological Gradient
膨胀减去腐蚀。称为基本梯度(其它还包括-内部梯度、方向梯度)主要对黑白二值化图像进行处理,彩色图像处理可能不太明显
MOPHO_GRADIANT;梯度学操作
MOPHO_TOPHAT: 顶帽,是原图像与开操作之后两者的差值:开操作去掉了什么,顶帽就保留什么
MOPHO_BALCKHAT: 黑帽,是原图像与闭操作之后两者的差值:闭操作去掉了什么,黑帽就保留什么
在这之前还需要进行图像二值化等一系列处理。用黑帽测出去掉得到的值