2021-07-10暑期专题培训(opencv1-6)_阳杨羊的博客-程序员宝宝

技术标签: opencv  

opencv 学习

第一章:

  1. 载入图片,保存,旋转。

第二章:

2.1目标:
一,学会如何遍历一张图像并处理其像素
二,高效的处理方法
灰度图:像素由8位无符号数来表示;
彩色图:由三个这样的8位无符号数来表示三个颜色通道,0黑色;255白色;
opencv允许创建不同像素类型的矩阵或图像:如整形(CV_8U),浮点型(CV_32F)。
它们在一些图像处理过程中,用来保存中间值这样的内容很有用。大多数矩阵可以用于任意类型的矩阵,但有些运算对数据类型或矩阵的通道数有要求。

2.2.存取像素值:高效的遍历数组。椒盐噪点,即随机设置为黑白,黑白1通道,彩色3通道。Mat类,at方法。
需要在代码中指定元素所在的行(row)和列(col),函数会返回相应的元素。单通道返回单个数值;多通道返回一组向量(Vector)。

//2021.6.27 图像椒盐,加噪点。
#include<opencv2/opencv.hpp>
#include<iostream>

using namespace cv;
void salt(Mat& image, int n) {
    
//第一个参数是一张输入图像,使用传引用的参数传递方式(值,指针,传引用三种方式);第二个参数是要该改变的噪点个数
	for (int k = 0; k < n; k++)
	{
    
		int i = rand() % image.cols;//随机获取行和列        //col,row;是了类cv::Mat的公有成员变量,能给出图像的宽高;
		int j = rand() % image.rows;
		if (image.channels() == 1) {
    //灰度为1
			image.at<uchar>(j, i) = 255;//255白色,0为黑色   //成员函数at(int y,int x)用来存取图像的元素;但是必须要知道图像的数据类型。cv::Mat可以存放任意类型的元素。
		}
		else if (image.channels() == 3)//彩色为3
		{
    
			image.at<Vec3b>(j, i)[0] = 255;               //分清类型
			image.at<Vec3b>(j, i)[1] = 255;               // 确保指定的数据类型和矩阵的数据类型
			image.at<Vec3b>(j, i)[2] = 255;               //相符,at方法本身不会经行数据类型转换
		}
	}
}

int main() {
    
	Mat src;
	src = imread("D:/opencvcode/img/13.png");
	if (!src.data) {
    
		printf("not img!");
		return -1;
	}
	salt(src, 3000);
	namedWindow("inputimg");
	imshow("inputimg", src);
	waitKey(0);
	return 0;
}

拓展阅读:cv::Mat_是cv::Mat的一个模板子类。

2.3 用指针遍历:rows,cows。
将要遍历图像的所有像素时,像素个数很多,所以高效的遍历很重要。以下两种方法,第一种是指针算术;
每个像素的每个通道,将其值除以N,整除。再乘以N。

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace cv;
//颜色缩减的其中一种方法,(函数)
void colorreduce(Mat& image, int div = 64) {
    
	int n1 = image.rows;
	int nc = image.cols * image.channels();        //获取每行像素值
	for (int j = 0; j < n1; j++) {
    
		uchar* data = image.ptr<uchar>(j);//ptr函数得到图像任意首地址,ptr返回第J行的首地址
		for (int i = 0; i < nc; i++) {
    
			data[i] = data[i] / div * div + div / 2;//使用指针从一列移到下一列(颜色缩减函数公式1)
		//*data++ = *data / div * div + div/2;//颜色缩减函数公式2
		//data[i] = data[i] - data[i] % div + div / 2; //颜色缩减函数公式3,这个方法比较慢

		用位运算(非常高效),限制缩减因子为2^n,(运用掩模???)
		//uchar mask = OxFF << n;//e.g.for div=16,mask=OxFo;
		//data[i] = (data[i] & mask) + div / 2;

		}
	}
}

result.creatr(image.rows, image.cows, image.tye());    //创建一个与输入图像的尺寸和类型相同的矩阵,create 函数创建的内存是连续的,不会对图像进行填补。内存大小为total()*elemSize();

//	for (int j = 0; j < n1; j++) {
    
//		const uchar* data_in = image.ptr<uchar>(j);
//		uchar* data_out = result.ptr<uchar>(j);
//		for (int i = 0; i < nc; i++) {
    
//			data_out[i] = data_in[i] / div * div + div / 2;
//		}
//	}

int main() {
    
	Mat img;
	img = imread("D:/opencvcode/img/13.png");

	colorreduce(img);//调用颜色缩减函数
	namedWindow("input");
	imshow("input", img);
                                          // 额外的复制操作,法1
	Mat imageClone = img.clone();//clone函数的使用//处理克隆的图像
	colorreduce(imageClone);//原始图像不变
                                         //额外的复制操作,法2
	void colorReduce(const Mat & image,//输入图像,常量引用传递,不能被函数修改
		Mat & resul,                  //输出图像
		int div = 64);
	//colorreduce(img,img);//in-place处理方式时,可以将输入输出指定为同一个变量。不能用,mat->int

	//另一个实列,不然就要提供另外一个cv::Mat的实列
	//cv::Mat result;
	//colorreduce(img,result);//必须检查输入输出图像的大小,元素类型是否一致。cv::Mat的create成员函数有内置检查操作。

	void colorreduce(Mat & img, int div = 64) {
        //利用图像的连续性,把整个处理过程用一个循环完成,颜色缩减函数重写为:
		int n1 = img.rows;
		int nc = img.cols * img.channels();
		if (img.isContinuous())//函数
		{
    
			nc = nc * n1;
			n1 = 1;
		}
		for (int j = 0; j < n1; j++) {
    
			uchar* data = img.ptr<uchar>(j);
			data[i] = data[i] / div * div + div / 2;
		}
	}
//也可以用reshape方法来重写这段函数:
	if (img.isContinuous())
	{
    
		img.reshape(1,img.cols * img.rows);//reshpape不用内存拷贝或重新分配就能改变矩阵维度。两个参数为新的通道数和新的行数。矩阵的列数可根据通道数和行数来自适应。
	}
	int n1 = img.rows;
	int nc = img.cols * img.channels();

	/*namedWindow("output");
	imshow("output", imageClone);*/


	namedWindow("input");
	imshow("input", img);
	waitKey(0);
	return 0;
}

cols代表宽度,rows代表高度,step代表以字节为单位的图像有效宽度。elemSize函数能得到像素大小,channels方法得到图像通道数,total函数返回矩阵像素个数。
in-place变换:直接在输入的图像上进行操作,不然就是新创建一个图像 ,“深拷贝”的方式是调用clone函数。

3.高效的遍历连续图像
若不对图像行尾经行填补的时候,图像视为一个长WxH的一位数组。通过cv::Mat的一个成员函数isContinuous方法来判断是否经行了填补,真,没有填补。

4.底层指针运算
容易出错,还不能定义感兴趣区域,不好用。

2.4 迭代器遍历图像
面向对象中常用迭代器编历数据集合,迭代器是一种特殊的类。不管数据类型是什么,都可以用相似的方式遍历集合,STL标准模板库为每一个容器类提供了迭代器,cv::Mat提供与STL迭代器兼容的迭代器。

#include<opencv2/opencv.hpp>
#include<iostream>
using namespace cv;
//Mat Iterator_<Vec3b>it;//创建一个迭代器特化版本1
//Mat_<Vec3b>::iterator it;//创建一个迭代器特化版本2
//第二种实现方式:
void colorreduce(Mat& img, int div = 64) {
    
//迭代器初始化完成(法1),再开始循环遍历
	Mat_<Vec3b>::iterator it = img.begin<Vec3b>();//处理彩色图像3b,用begin方法获得初始位置,得到左上角位置的迭代器
	//img.begin<Vec3b>() + img.rows;//从图像的第二行开始
	Mat_<Vec3b>::iterator itend = img.end<Vec3b>();//终止迭代的运算
	//img.end<Vec3b>() - img.rows;//希望在迭代过程的图像最后一行之前停止
	//循环方法1
	for (; it != itend; ++it) {
    
	//处理每一个像素
		(*it)[0] = (*it)[0] / div * div + div / 2;
		(*it)[1] = (*it)[1] / div * div + div / 2;
		(*it)[2] = (*it)[2] / div * div + div / 2;
		//处理像素完毕
	}
	//循环方法2
	/*
	while (it!=item){
	//处理每一个像素
	……
	//处理完成
	++it;//移动迭代器。可随意改步长,it+=10;
    }
   */

}
int main() {
    
	Mat img = imread("D:/opencvcode/img/13.png");
	if (img.empty()) {
    
		printf("not img!");
		return -1;
	}
	colorreduce(img);//函数调用
	namedWindow("input");
	imshow("input", img);

	waitKey(0);
	return 0;
}


在循环体内部,使用解引用操作符*来读写当前元素。读,element=*it,写,*it=element。
若操作对象是const cv::Mat或者强调当前循环不会对cv::Mat的实列进行修改,要创建常量迭代器。
创建方法1:cv::Mat ConstIterator_cv::Vec3bit;
方法2:cv::Mat_cv::Vec3b::const_iterator it;

迭代器初始化(法2)

cv::Mat_<cv::Vec3b>cimage=image;
cv::Mat_<cv::Vec3b>::iterator it =cimage.begin();
cv::Mat_<cv::Vec3b>::iteratoe itend=cimage.end();

面向对象的迭代器概念,阅读STL中迭代器相关的入门文章。关键字:STL Iterator

2.5. 高效的图像遍历循环

 分析效率,提高运行效率。

opencv中, cv:: getTickCount()函数用来检测一段代码的运行时间,计算从上次开机算起的时钟周期数。我们需要计算某个代码段的运行毫秒数,用cv::getTickFrequency(),此函数返回每秒内的时钟周期数。

//统计函数耗费时间的方法
double  duration;
duration =static_cast<double>(cv::getTickCount());
colorreduce(image);//被测试的函数
duration =static_cast<double>(cv::getTickCount())-duration;
duration/=cv::getTickFrequency();//运行时间,以ms为单位

at方法实现:

//at方法实现
for(int j=0;j<nl;j++){
    
	for(int i=0;i<nc;i++){
    
	//process ench pixel------
	image.at<cv::Vec3b>(j,i)[0]=image.at<cv::Vec3b>(j,i)[0]/div*div+div/2;
	image.at<cv::Vec3b>(j,i)[1]=image.at<cv::Vec3b>(j,i)[1]/div*div+div/2;
	image.at<cv::Vec3b>(j,i)[2]=image.at<cv::Vec3b>(j,i)[2]/div*div+div/2;
	//end of pixel processing------
	}//end of line
}

几种函数时间效率的分析:
得到

3-channel loop实现方法,速度最快的版本:

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace cv;
/*
//颜色缩减的其中一种方法,(函数)
void colorreduce(Mat& image, int div = 64) {
	int n1 = image.rows;
	int nc = image.cols * image.channels();//获取每行像素值
	//for (int j = 0; j < n1; j++) {
	//	uchar* data = image.ptr<uchar>(j);//ptr函数得到图像任意首地址,ptr返回第J行的首地址
	//	//for (int i = 0; i < nc; i++) {//可提前计算的变量,提高运算速度
	//	for (int i = 0; i < image.cols * image.channels(); i++) {
	//		data[i] = data[i] / div * div + div / 2;//使用指针从一列移到下一列(颜色缩减函数公式1)
	//	//*data++ = *data / div * div + div/2;//颜色缩减函数公式2
	//	//data[i] = data[i] - data[i] % div + div / 2; //颜色缩减函数公式3,这个方法比较慢

	//	用位运算(非常高效),限制缩减因子为2^n,(运用掩模???)
	//	//uchar mask = OxFF << n;//e.g.for div=16,mask=OxFo;
	//	//data[i] = (data[i] & mask) + div / 2;

	//	}
	//}
	for (int j = 0; j < n1; j++) {
		for (int i = 0; i < nc; i++) {
			image.at<Vec3b>(j, i)[0] =
				image.at<Vec3b>(j, i)[0] / div * div + div / 2;
			image.at<Vec3b>(j, i)[1] =
				image.at<Vec3b>(j, i)[1] / div * div + div / 2;
			image.at<Vec3b>(j, i)[2] =
				image.at<Vec3b>(j, i)[2] / div * div + div / 2;
		}
	}
}
*/
void colorreduce(Mat& image, int div = 64) {
       //3-chnnel-loop
	int n1 = image.rows;//行数
	int nc = image.cols;//列数
			//图像是连续存储的吗?

	if (image.isContinuous()) {
    
		//没有对行进行填补
		nc = nc * n1;
		n1 = 1;//一维数组
	}
	int n = static_cast<int>(
		log(static_cast<double>(div)) / log(2.0));
		//用来对像素值进行取整的二进制掩膜?????
	uchar mask = 0xFF << n;
	//for all pixels
	for (int j = 0; j < nc; j++) {
    
		//第j列的地主
		uchar* data = image.ptr<uchar>(j);
		for (int i = 0; i < nc; i++) {
    
			//处理每一个像素
			*data++ = *data & mask + div / 2;
			*data++ = *data & mask + div / 2;
			*data++ = *data & mask + div / 2;
			//像素处理结束
		}//行处理结束
	}
}
int main() {
    
	Mat img;
	img = imread("D:/opencvcode/img/13.png");

	//colorreduce(img);//调用颜色缩减函数

	double duration;//count the run time
	duration = static_cast<double>(cv::getTickCount());
	colorreduce(img);
	duration = static_cast<double>(cv::getTickCount()) - duration;
	duration /= cv::getTickFrequency();




	namedWindow("input");
	imshow("input", img);
	waitKey(0);

	return 0;
}

//统计函数耗费时间的方法
double  duration;
duration =static_cast<double>(cv::getTickCount());
colorreduce(image);//被测试的函数
duration =static_cast<double>(cv::getTickCount())-duration;
duration/=cv::getTickFrequency();//运行时间,以ms为单位

//at方法实现
for(int j=0;j<nl;j++){
    
	for(int i=0;i<nc;i++){
    
	//process ench pixel------
	image.at<cv::Vec3b>(j,i)[0]=image.at<cv::Vec3b>(j,i)[0]/div*div+div/2;
	image.at<cv::Vec3b>(j,i)[1]=image.at<cv::Vec3b>(j,i)[1]/div*div+div/2;
	image.at<cv::Vec3b>(j,i)[2]=image.at<cv::Vec3b>(j,i)[2]/div*div+div/2;
	//end of pixel processing------
	}//end of line
}

2.6. 遍历图像与邻域操作
由当前位置的相邻像素计算新的像素值。
对图像进行锐化,基于拉普拉斯算子。处理过后图像的边缘部分将得到放大,细节更加锐利。

sharpened_pixel=5*current-left-right-up-down;   left代表在当前像素左边紧挨着它的像素……

实现方式:3个指针,指向当前行,上一行,下一行。因为每一个像素值都要计算上下左右4个相邻像素,所以第一行,最后一行, 第一列,最后一列不能计算到。

//锐化//基于拉普拉斯算子//公式sharpened_pixel=5*current-left-right-up-down;
//三个指针,当前行,上一行,下一行
//用黑白图像效果更明显,彩色:cv::Scalar(a,b,c)
#include<opencv2/opencv.hpp>
#include<iostream>

using namespace cv;//method1
/*
void sharpen(const Mat& image, Mat& result) {//法一
//如有必要则分配图像
	result.create(image.size(), image.type());
	for (int j = 1; j < image.rows - 1; j++) {//处理除了第一行和最后一行之外的所有行
		const uchar* previous =
			image.ptr<const uchar>(j - 1);//上一行
		const uchar* current =
			image.ptr<const uchar>(j);      //当前行
		const uchar* next =
			image.ptr<const uchar>(j + 1);   //下一行
		uchar* output = result.ptr<uchar>(j);//输出行
		for (int i = 0; i < image.cols - 1; i++) {
			*output++ = saturate_cast<uchar>(
				5 * current[i] - current[i - 1]
				- current[i + 1] - previous[i] - next[i]);
		}
	}
	//将未处理的像素设置为0
	result.row(0).setTo(cv::Scalar(0));
	result.row(result.rows - 1).setTo(cv::Scalar(0));
	result.col(0).setTo(cv::Scalar(0));
	result.col(result.cols - 1).setTo(cv::Scalar(0));
}*/
void sharpen2D(const cv::Mat& image, cv::Mat& result) {
    //法二
//构造核(所有项都是初始化为0)
	cv::Mat kernel(3, 3, CV_32F, cv::Scalar(0));
	//对核元素进行赋值
	kernel.at<float>(1, 1) = 5.0;
	kernel.at<float>(0, 1) = -1.0;
	kernel.at<float>(2, 1) = -1.0;
	kernel.at<float>(1, 0) = -1.0;
	kernel.at<float>(1, 2) = -1.0;
	//对图像进行滤波
	cv::filter2D(image, result, image.depth(), kernel);
}
int main() {
    
	Mat image = imread("D:/opencvcode/img/13.png");
	namedWindow("input1");
	imshow("input1", image);
	//sharpen(image, image);
	sharpen2D(image, image);

	namedWindow("input2");
	imshow("input2", image);

	waitKey(0);
	return 0;
}

3个指针同时进行,计算输出像素值时,模板函数cv::saturate_cast用来截断计算结果。

边缘处理:
边缘像素不好处理,在程序中用两个函数设为0。方法2是,使用setTo 函数来设置矩阵的值,这个函数会将矩阵的所有元素都设为指定的值。

result.row(0).setTo(cv::Scalar(0));将结果第一行的所有像素设置为0。
三通道的彩色图,cv::Scalar(a,b,c)来指定像素的三个通道目标值。

2.7. 简单的图像计算
图像是矩阵,可进行加减乘除运算。
1.图像相加cv::add,完整的cv::addWeighted
//c[i]=a[i]+b[i];
cv::add(imageA,imageB,resultC);

cv::addWeighted(image1,0.7,image2,0.9,0.,result);==
用c++,重载图像操作符:result=0.7*image1+0.9*image2;

//把一个图像加到蓝色通道上

//创建一个图像向量
std::vectorcv::Matplanes;
//将一个三通道图像分离为三个单通道图像
cv::split(image1,planes);
//将新图层叠加到蓝色通道
planes[0]+=image2;
//将三个单通道图像重新合并为一个三通道图像
cv::merge(planes,result);

//cv::merge是cv::split的对偶运算,它将三个单通道图像合并为一个彩色三通道图像

//图像的运算
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace cv;
void sharpen2D(const cv::Mat& image, cv::Mat& result) {
    
	cv::Mat kernel(3, 3, CV_32F, cv::Scalar(0));
	kernel.at<float>(1, 1) = 5.0;
	kernel.at<float>(0, 1) = -1.0;
	kernel.at<float>(2, 1) = -1.0;
	kernel.at<float>(1, 0) = -1.0;
	kernel.at<float>(1, 2) = -1.0;
	cv::filter2D(image, result, image.depth(), kernel);
	}

int main() {
    
	Mat image = imread("D:/opencvcode/img/13.png");
	Mat image2 = imread("D:/opencvcode/img/13-1png");
	cv::addWeighted(image, 0.7, image2, 0.9, 0., result);
	sharpen2D(image, image);
	namedWindow("input");
	imshow("input", image);
	waitKey(0);
	return 0;
}

2.8. 定义感兴趣区域
合并两个不同大小的图像,cv::add合并两个相同尺寸的图像,所以不能用。在使用之前定义感兴趣区域(ROI),感兴趣区域的大小与图像大小相同时,cv::add才能工作。ROI的位置决定了插入图像的位置。

源码中创建ROI方式和原始图像共享数据缓冲区,对ROI的任何操作都会影响到原始图像的对应区域。因为创建ROI时不会拷贝数据。

以下创建包含原始图像特定行的ROI

cv::Mat imageROI=image.rowRange(start,end);//行
cv::Mat imageROI=image.colRange(start,end);//列
 

源代码:


#include<opencv2/opencv.hpp>
#include<iostream>

using namespace cv;
int main() {
    
	Mat image = imread("D:/opencvcode/img/13.png");
	Mat logo = imread("D:/opencvcode/img/13-2.png");
	
	
	//插入logo的操作
	cv::Mat imageROI;//定义图像ROI
	imageROI = image(cv::Rect(385, 270,logo.cols.logo.rows));//1.矩形定义方法,左上角坐标,长宽。
	/*
	//2.Range方法,从起始索引到中止索引(不包括中止索引)
	cv::Mat imageROI=image(cv::Range(270,270+logo.rows),cv::Range(385,385+logo.cols))
	*/
	
	//插入logo
	cv::addWeighted(imageROI, 1.0, logo, 0.3, 0., imageROI);
	
	/*
	//运用掩膜来完成。视觉效果更好一点。
    //定义ROI
	imageROI-image(cv::Rect(385,270,logo.cols,log.rows));
	//加载掩膜(必须是灰度图)
	cv::Mat mask=cv::imread("logo.bmp",0);
	//通过掩膜拷贝ROI
	logo.copyTo(imageROI,mask);
*/

	namedWindow("input1");
	imshow("input1", image);

	//sharpen(image, image);
	//sharpen2D(image, image);

	waitKey(0);
	return 0;
}

第三章 基于类的图像处理
目标:
在算法设计中使用策略(Strategy)模式
使用控制器(Controller)实现模块间通信
使用单件(Singleton)设计模式
使用模型-视图-控制器(Model-View-Controller)架构设计引用程序
颜色空间转换

  1. 3.1设计模式,软件工程中的概念,程序员要熟悉现有的模式。操作图像中的颜色,检测拥有特定颜色的像素。
  2. 3.2 策略设计模式
    将算法封装在类中,替换一个现有的算法。
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;

/*
int main() {
	Mat result;
	Mat image = imread("D:/opencvcode/img/13.png");
	if (image.empty()) {
		cout << "not img" << endl;
		return -1;
	}
	*/
	class ColorDetector {
    
	private:
		//最小可接受距离
		int minDist;
		//目标色
		cv::Vec3b target;
		//结果图像
		cv::Mat result;
	};
	/*
	//空构造函数
	ColorDetector(): minDist(100) {
		//初始化默认参数
		target[0] = target[1] = target[2] = 0;
	}

	cv::Mat ColorDetector::process(const cv::Mat& image) {
		//按需重新分配二值图像
		//与输入图像的尺寸相同,但是只有一个通道
		result.create(image.rows, image.cols, CV_8U);


		//得到迭代器
		Mat_<Vec3b>::const_iterator it = image.begin<Vec3b>();
		Mat_<Vec3b>::const_iterator itend = image.end<Vec3b>();
		Mat_<uchar>::iterator itout = result.begin<uchar>();
		//对于每个像素
		for (; it != itend; ++it, ++itout) {
			//处理每个像素------
			//计算离目标颜色的距离
			if (getDistance(*it) < minDist) {
				*itout = 255;
			}
			else {
				*itout = 0;
			}
			//处理像素结束------
		}

		return result;

	}

	*/
int main()
{
    
	//1.创建图像处理对象
	ColorDetector cdetect;
	//2.读取输入图像
	Mat image = imread("D:/opencvcode/img/13.png ");
	if (!image.data)
		return 0;
	//3.设置输入参数
	cdetect.setTargetColor(130, 190, 230);//蓝天的颜色
	namedWindow("result", WINDOW_AUTOSIZE);
	//4.处理并显示结果
	imshow("result",cdetect.process(image));

	waitKey(0);
	return 0;
	}
	
	/*
//计算与目标颜色的距离
int getDistance(const Vec3b& color)const {
	return abs(color[0] - target[0]) + abs(color[1] - target[1]) + abs(color[2] - target[2]);
}

//设置色彩距离阈值。阈值必须是正的,否则设为0
void setColorDistanceThreshold(int distance) {
	if (distance < 0)
		distance = 0;
	minDist = distance;
}
//获取色彩距离阈值
int getColorDistanceThreshold() const {
	return minDist;
}
*/

/*

 //设置需检测的颜色
void setTargetColor(unsigned char red, unsigned char green, unsigned char blue) {
	//bgr顺序
	target[2] = red;
	target[1] = green;
	target[0] = blue;
}

//设置需检测的颜色
void setTargetColor(cv::Vec3b color) {
	target = color;
}
//获取需检测的颜色
cv::Vec3b getTargetColor() const {
	return target;
}
*/

3.3使用控制器实现模块间通信

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace std;
using namespace cv;
int main() {
    
	class ColorDetectController {
    
	private:
		//算法类
		ColorDetector * cdetect;
		Mat image;//被处理的图像
		Mat result;//结果
	public:
		ColorDetectController() {
    
			//初始化工作
			cdetect = new ColorDetector();
		}

		//设置色彩距离阈值
		void setColorDistanceThreshold(int distance) {
    
			cdetect->setColorDistanceThreshold(distance);
		}
		//获取色彩距离阈值
		int getColorDistanceThreshold() const {
    
			return cdetect->getColorDistanceThreshold();
		}

		//设置要检测的色彩
		void setTargetColor(unsigned char red, unsigned char green, unsigned char blue) {
    
			cdetect->setTargetColor(red, green, blue);
		}

		//获取需检测的色彩
		void setTargetColor(unsigned char &red, unsigned char &green, unsigned char &blue)const {
    
			Vec3b color = cdetect->getTargetColor();
			red = color[2];
			green = color[1];
			blue = color[0];
		}
		//设置输入图像,通过文件读取
		bool setInputImage(std::string filename) {
    
			image = cv::imread(filename);
			if (!image.data)
				return false;
			else
				return true;
		}
		//返回当前的输入图像
		const cv::Mat getInputImage()const {
    
			return image;
		}

		//开始图像处理
		void process() {
    
			result = cdetect->process(image);
		}

		//获取最近一次处理的结果
		const cv::Mat getLastResult() const {
    
			return result;
		}

		//删除当前控制器创建的处理对象
		~ColorDetectController() {
    
			delete cdetect;
		}

		//Open按钮的回调函数
		void onOpen() {
    
			//用于选择bmp或jpg文件的MFC控件
			CFileDialog dlg(TRUE, T("*.bmp"), NULL,OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_HIDEREADONLY,_T("image files(*.bmp;*,jpg)| *.bmp; *.jpg | All Files(*.*) | *.* || "),NULL);
			dlg.m_ofn.lpstrTitle = _T("Open Image");
			//如果选中了一个文件
			if (dlg.DoModal() == IDOK) {
    
				//获取选中文件的路径
				std::string filename = dlg.GetPathName();
				//设置并显示该图像
				colordetect.setInputImage(filename);
				cv::imshow("input Image", colordetect.getInputImage());
			}
		}

		//Process 按钮的回调函数
		void OnProcess() {
    
			//此处目标色为硬编码
			colordetect.setTargetColor(130, 190, 230);
			//处理输入图像,并显示结果
			colordetect.process();
			cv::imshow("output Result", colordetect.getLastResult());
		}

//imshow("output", src);
	waitKey(0);
	return 0;
}
	};


	

3.4使用单件设计模式
单件(Singleton)模式

3.5使用模式-视图-控制器(Model-View-Controller)构架设计应用程序
MVC,基于Qt的GUI

3.6颜色空间转换
使用cv::cvtColor可以轻易在不同颜色空间经行转换,

第4章 使用直方图统计像素
计算图像的直方图
通过查找表修改图像外观
直方图均衡化
反投影直方图以检测特定的图像内容
使用均值漂移(Mean Shift)计算查找物体
通过比较直方图检索相似图片

4.2计算图像的直方图
归一化
cv::calcHist函数,可计算任意类型的多通道图像

4.3使用查找表修改图像外观

4.4直方图均衡化
均衡,

4.5反投影直方图以检测特定的图像内容
检测特定的内容,首先选取感兴趣区域(ROI),
一个概率映射

4.6使用均值漂移(Mean Shift)计算查找物体
知道物体的近似位置,用迭代移动,找到精确的位置。
HSV颜色空间,

4.7通过比较直方图检索相似图片
cv::compareHist函数

第5章 基于形态学运算的图像变换
使用形态学滤波对图像进行腐蚀、膨胀运算
使用形态学滤波对图像进行开闭运算
使用形态学滤波对图像进行边缘及角点检测
使用分水岭算法对图像进行分割
使用GrabCut算法提取前景物体

5.2使用形态学滤波对图像进行腐蚀、膨胀运算
腐蚀和膨胀函数:cv::erode,cv::dilate

#include<opencv2/opencv.hpp>
#include<iostream>
 
using namespace cv;
using namespace std;
int main() {
    
	//读取输入图像
	Mat image = imread("D:/opencvcode/img/my1.png");
	imshow("input ", image);
	//腐蚀图像
	Mat eroded;//目标图像

	//方法1	
	//erode(image, eroded, Mat());

	//方法2
	//Mat element(7, 7, CV_8U, Scalar(1));
	//erode(image, eroded, element);

	//方法3,效果和方法2一样
	//腐蚀图像三次
	//erode(image, eroded, Mat(), Point(-1, -1), 3);
	
	//方法4
	erode(image, image, Mat());
	imshow("Eroded Image", image);

	//imshow("Eroded Image", eroded);

	//膨胀图像
	Mat dilated;//目标图像
	dilate(image, dilated,Mat());
	//显示膨胀后的图像
	namedWindow("Dilated Image");
	imshow("Dilated Image", dilated);

	waitKey(0);
	return 0;
}

5.3使用形态学滤波对图像进行开闭运算
cv::morphologyEx函数
闭运算
开运算

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace cv;
using namespace std;
int main() {
    
	//读取输入图像
	Mat image = imread("D:/opencvcode/img/my1.png");
	imshow("input ", image);
	/*

	//腐蚀图像
	Mat eroded;//目标图像

	//方法1	
	//erode(image, eroded, Mat());

	//方法2
	//Mat element(7, 7, CV_8U, Scalar(1));
	//erode(image, eroded, element);

	//方法3,效果和方法2一样
	//腐蚀图像三次
	//erode(image, eroded, Mat(), Point(-1, -1), 3);

	//方法4
	//erode(image, image, Mat());
	//imshow("Eroded Image", image);

	//5.3闭运算
	
	Mat element5(5, 5,CV_8U, Scalar(1));
	/*
	Mat closed;
	morphologyEx(image, closed, MORPH_CLOSE, element5);
	imshow("closed Image", image);
	*/
/*
	//5.3开运算
	Mat opened;
	morphologyEx(image, opened, MORPH_OPEN, element5);
	imshow("opened Image", image);

	//imshow("Eroded Image", eroded);//方法1-3通用

*/

	//膨胀图像
	Mat dilated;//目标图像
	dilate(image, dilated, Mat());

	//5.3先膨胀后进行原地腐蚀
	Mat result;
	dilate(image, result, Mat());
	erode(result, result, Mat());
	imshow("opened Image", result);

	//显示膨胀后的图像
	namedWindow("Dilated Image");
	imshow("Dilated Image", dilated);

	waitKey(0);
	return 0;
}

5.4使用形态学滤波对图像进行边缘及角点检测
形态学滤波用于检测图像中的特征,检测灰度图中的直线和角点

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace std;
using namespace cv;

int main(){
     

	class MorphoFeatures 
{
    
	private:
		//用于生成二值图像的阈值
		int threshold;
		//角点检测中用到的结构元素
		Mat cross;
		Mat diamond;
		Mat square;
		Mat x;


		//使用morphologyEx函数配上合适的滤波器可以轻松实现直线的检测
		cv::Mat getEdges(const Mat& image) 
		{
    
			//得到梯度图
			Mat result;
			morphologyEx(image, result, MORPH_GRADIENT, Mat());
			//阈值化得到二值图像
			applyThreshold(result);
			return result;
	    }

		void applyThreshold(Mat& result) {
    
			//使用阈值化
			if (threshold > 0)
				cv:: threshold(result, result, threshold, 255, THRESH_BINARY);

		}
	};


	//使用形态学检测角点
	MorphoFeatures() : threshold(-1),
		cross(5, 5, CV_8U, cv::Scalar(0)),
		diamond(5, 5, CV_8U, Scalar(1)),
		square(5, 5, CV_8U, Scalar(1)), x(5, 5, CV_8U, Scalar(0)) {
    
		//创建十字形元素
		for (int i = 0; i < 5; i++) {
    
			cross.at<uchar>(2, i) = 1;
			cross.at<uchar>(i, 2) = 1;
		}
		//创建菱形元素
		diamond.at<uchar>(0, 0) = 0;
		diamond.at<uchar>(0, 1) = 0;
		diamond.at<uchar>(1, 0) = 0;
		diamond.at<uchar>(4, 4) = 0;
		diamond.at<uchar>(3, 4) = 0;
		diamond.at<uchar>(4, 3) = 0;

		diamond.at<uchar>(4, 0) = 0;
		diamond.at<uchar>(4, 1) = 0;
		diamond.at<uchar>(3, 0) = 0;
		diamond.at<uchar>(0, 4) = 0;
		diamond.at<uchar>(0, 3) = 0;
		diamond.at<uchar>(1, 4) = 0;
		//创建x形元素
		for (int i = 0; i < 5; i++) {
    
			x.at<uchar>(i, i) = 1;
			x.at<uchat>(4 - i, i) = 1;
		}

	}

	cv::Mat getCorners(const cv::Mat &image) {
    
		Mat result;
		//十字形膨胀
		dilate(image, result,cross);
		//菱形腐蚀
		cv::erode(result, result, diamond);
		cv::Mat result2;
		//x形膨胀
		cv::dilate(image, result2, x);
		//方形腐蚀
		cv::erode(result2, result2, square);
		//阈值化以得到二值图像
		applyThreshold(result);
		return result;
	}


	//在每个检测点上绘制一个园
	void drawOnImage(const cv::Mat& binary,
		cv::Mat& image) {
    
		cv::Mat_<uchar>::const_iterator it = binary.begin<uchar>();
		cv::Mat_<uchar>::const_iterator itend = binary.end<uchar>();
		//遍历每一个像素
		for (int i = 0; it != itend; ++it, ++i) {
    
			if (!*it)
				cv::circle(image,
					cv::circle(image,
						cv::Point(i % image.step, i / image.step),
						5, cv::Scalar(255, 0, 0));
		}
	}



	Mat image = imread("D:/opencvcode/img/gry.png");

	MorphoFeatures morpho;
	morpho.setThreshold(40);
	//获取边缘
	Mat edges;
	edges = morpho.getEdges(image);



	//得到角点
	Mat corners;
	corners = morpho.getCorners(image);
	//在图像中显示角点
	morpho.drawOnImage(corners, image);
	cv::nameWindow("Corners on Image");
	cv::imshow("Corners on Image", image);



	waitKey(0);
	return 0;
}

5.5使用分水岭算法对图像进行分割
快速分割图像为同类区域
分割结果通过cv::watershed函数获得,

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace cv;
using namespace std;
int main() {
    
	//读取输入图像
	Mat image = imread("D:/opencvcode/img/my1.png");
	

	class WatershedSegment {
    
	private:
		cv::Mat markers;
	public:
		void setMarkers(const cv::Mat& markerImage) {
    
			//转换为整数图像
			markerImage.convertTo(markers, CV_32S);
		}
		cv::Mat process(const Mat & image) {
    
			//使用算法
			watershed(image, markers);
			return markers;
		}
		//移除噪点与微小物体
		cv::Mat fg;
		cv::erode(binary, fg, Mat(), Point(-1, -1), 6);

		//识别不包括物体的像素
		Mat bg;
		dilate(binary, bg, Mat(), Point(-1, -1), 6);
		threshold(bg, bg, 1, 128, THRESH_BINARY_INV);


		//创建标记图像
		Mat markers(binary.size(), CV_8U, Scalar(0));
		markers = fg + bg;

		//创建分水岭分割对象
		WatershedSegment segmenter;
		//设置标记,并进行处理
		segmenter.setMarkers(markers);
		segmenter.process(image);

		//以图像的形式返回结果
		Mat getSetmentation() {
    
			Mat tmp;
			//标签高于255的分割一律赋值为255
			markers.convertTo(tmp, CV_8U);
			return tmp;
		}

		//以图像的形式返回分水岭
		Mat getWatersheds() {
    
			Mat tmp;
			//转换前每一个像素都为255p+255
			markers.convertTo(tmp, CV_8U, 255, 255);
			return tmp;
		}

	};

	namedWindow("Dilated Image");
	imshow("Dilated Image", image);

	waitKey(0);
	return 0;
}

5.6使用GrabCut算法提取前景物体
与分水岭算法有相似之处,但更加复杂,产生的结果精确很多。
从静态图像中提前前景物体的应用,如,将一幅图中的物体剪贴到另一幅图中。是最佳算法。

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace std;
using namespace cv;

int main() {
    
	Mat image;
	//打开图像
	image = imread("D:/opencvcode/img/13.png");
	//定义前景物体的包围盒
	Rect rectangle(10, 100, 380, 180);
	Mat result;//分割(四种可能的值)
	Mat bgModel, fgmodel;//模型(供内部使用)
	//GrabCut分割
	grabCut(image,//输入图像
		result,//分割结果
		rectangle,//包括前景物体的矩形
		bgModel, fgmodel,//模型
		5,//迭代次数
		GC_INIT_WITH_RECT);//使用矩形经行初始化

	//得到可能为前景的像素
	compare(result, GC_PR_FGD,result, CMP_EQ);
	//生成输出图像

	//使用按位与(bitwise-and)核对第1个位
	result = result & 1;

	Mat foreground(image.size(), CV_8UC3,
		Scalar(255, 255, 255));
	image.copyTo(foreground,//不复制背景像素
		result);
	 	
	//namedWindow("output");
	imshow("output", image);
	waitKey(0);

	return 0;

}

第6章 图像滤波
使用低通滤波器
使用中值滤波器
使用方向滤波器检测边缘
计算图像的拉普拉斯变换

6.1滤波操作,增强 部分频段,限制其他频段。低通滤波器去除了图像中的高频成分,高通滤波器去除了低频成分。

6.2使用低通滤波器
降低图像变化的幅度,将每个像素替换为相邻像素的平均值。使用cv::blur函数

cv::blur(image,result,cv::Size(5,5));

高斯函数,cv::GaussianBlur(image,result,cv::Size(5,5),1.5);

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace std;
using namespace cv;
int main() {
    
	Mat image = imread("D:/opencvcode/img/13.png");
	imshow("intput", image);

	Mat result;
	//cv::blur(image, result, cv::Size(5, 5));
	cv::GaussianBlur(image, result, cv::Size(5, 5), 1.5);//高斯 函数
	
	Mat reducedImage;//包含缩小后的图像
	pyrDown(image, reducedImage);//将图像尺寸减半

	Mat resizedImage;//包含改变尺寸后的图像
	resize(image, resizedImage,Size(image.cols/3,image.rows/3));//改变为1/3
	
	imshow("output", result);
	waitKey(0);
	return 0;
}

6.3使用中值滤波器
上一节线性滤波器,这一节中值滤波器就是一种非线性滤波器。
中值滤波器对去除噪点很有用,

//2021.6.27 图像椒盐,加噪点。
#include<opencv2/opencv.hpp>
#include<iostream>

using namespace cv;
void salt(Mat& image, int n) {
    
	for (int k = 0; k < n; k++)
	{
    
		int i = rand() % image.cols;//随机获取行和列
		int j = rand() % image.rows;
		if (image.channels() == 1) {
    //灰度为1
			image.at<uchar>(j, i) = 255;//255白色,0为黑色
		}
		else if (image.channels() == 3)//彩色为3
		{
    
			image.at<Vec3b>(j, i)[0] = 255;
			image.at<Vec3b>(j, i)[1] = 255;
			image.at<Vec3b>(j, i)[2] = 255;
		}
	}
}

int main() {
    
	Mat src,result;
	src = imread("D:/opencvcode/img/13.png");
	if (!src.data) {
    
		printf("not img!");
		return -1;
	}
	salt(src, 3000);

	namedWindow("inputimg");
	imshow("inputimg", src);

	medianBlur(src, result,5);//中值滤波器去除噪点
	namedWindow("outputimg");
	imshow("outputimg", result);


	waitKey(0);
	return 0;
}

6.4使用方向滤波器检测边缘
强调图像中的高频分量,Sobel滤波器。具有方向性

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace cv;

int main() {
    
	Mat src, sobelX, sobelY;
	src = imread("D:/opencvcode/img/13.png");
	if (!src.data) {
    
		printf("not img!");
		return -1;
	}

	namedWindow("inputimg");
	imshow("inputimg", src);

	cv::Sobel(src,sobelX,CV_8U, 1, 0, 3, 0.4, 128);//水平滤波器
	Sobel(src,sobelY, CV_8U, 0, 1, 3, 0.4, 128);//垂直滤波器

	//计算Sobel范式
	Sobel(src, sobelX, CV_16S, 1, 0);
	Sobel(src,sobelY, CV_16S,0,1);
	Mat sobel;
	//计算L1范式
	sobel = abs(sobelX) + abs(sobelY);

	//搜寻Sobel极大值
	double sobmin, sobmax;
	minMaxLoc(sobel, &sobmin, &sobmax);
	//变换为8位图像
	//sobelImage=-alpha*sobel+255;
	Mat sobelImage;
	sobel.convertTo(sobelImage, CV_8U, -255. / sobmax, 255);
/*
	threshold(sobelImage,sobelThresholded,
		threshold, 255, THRESH_BINARY);
*/
	namedWindow("outputimg");
	imshow("outputimg", src);
	waitKey(0);
	return 0;
}

6.5计算图像的拉普拉斯变换
一种基于图像导数的高通线性滤波器。
函数Laplacion与Sobel函数很类似。

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace std;
using namespace cv;
int main() {
    
	class LaplacianZC {
    
	private:
		//原图
		Mat img;
		//包含Laplacion的32位浮点图像
		Mat laplace;
		//laplacian卷积核的大小
		int aperture;
	public:
		LaplacianZC() :aperture(3) {
    }
		//设置卷积核的大小
		void setAperture(int a) {
    
			aperture = a;
		}
		//计算浮点数Lapladion
		Mat computeLaplacian(const Mat& image) {
    
			//计算Laplacian
			Laplacian(image, laplace, CV_32F, aperture);
			//保留图像的局部备份(用于零点交叉)
			img = image.clone();
			return laplace;
		}
	};

	//返回8位图像存储的Laplacian结果
	//零点交叉于灰度值128
	//如果没有指定scale参数,那么最大值将缩至强度255
	//你必须在调用它之前调用computeLaplacian
	Mat getLaplacianImage(double scale = -1.0) {
    
		if (scale < 0)
		{
    
			double lapmin, lapmax;
			minMaxLoc(laplace, &lapmin, &lapmax);
			scale = 127 / std::max(-lapmin, lapmax);
		}
		Mat laplaceImage;
		laplace.convertTo(laplaceImage, CVV_8U, scale, 128);
		return laplaceImage;

	}

	//使用LaplacianZC类进行计算
	LaplacianZC laplacian;
	laplacian.setAperture(7);
	Mat flap = laplacian.computeLacian(image);
	laplace = laplacian.getLaplacianImage();

	//得到零点交叉的二值图像
	//如果相邻像素的乘积小于thteshold
	//那么零点交叉将忽略
	Mat getZeroCrossings(float threshold = 1.0)
	{
    
		//创建迭代器
		Mat_<float>::const_iterator it =
			Laplace.begin<float>() + lapace.stepl();
		Mat_<float>::const_iterator itend =
			laplace.end<float>();
		Mat_<float>::const_iterator itup =
			laplace.begin<float>();
		//初始化为白色的二值图像
		Mat binary(laplace.size(), CV_8U, Scalar(255));
		Mat_<uchar>::iterator itout =
			binary.begin<uchar>() + binary.stepl();
		//对输入阈值取反
		threshold *= -1.0;
		for (; it! = itend; ++it, ++itup, ++itout)
			//如果相邻像素的乘积为负数,那么符号发生改变
			if (*it * *(it - 1) < threshold)
				*itout = 0;//水平方向零点交叉
			else if (*it * *itup < threshold)
				*itout = 0;//垂直方向零点交叉
	}
	return binary;


	Mat image= imread("D:/opencvcode/img/13.png");
	//class LaplacianZC;

	namedWindow("inputimg");
	imshow("inputimg", image);
	waitKey(0);
	return 0;
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_47506273/article/details/118642866

智能推荐

Gitlab + Jenkins + k8s 实现企业 CI/CD 落地_比特币气氛组的博客-程序员宝宝

gitlab-pipelineGitlab + Jenkins + k8s 实现企业 CI/CD 落地1、启动 docker、kubernetes(docker-desktop Mac本地环境)2、使用K8s集群启动 jenkinsapiVersion: v1kind: ServiceAccountmetadata: name: jenkins---kind: RoleapiVersion: rbac.authorization.k8s.io/v1metadata: name:

前端基础学习之h5-dom操作、新增api(全屏操作、video多媒体、history、web存储等)_Endless Daydream233的博客-程序员宝宝

1.获取dom的方式H5 api有效选择器:在css当中能使用的都是有效选择器document.getElementsByClassName (‘class’);通过类名获取元素,以伪数组形式存在。document.querySelector(‘selector’);通过CSS选择器获取元素,符合匹配条件的第1个元素。document.querySelectorAll(‘selector’);通过CSS选择器获取元素,以伪数组形式存在。2.类操作H5 api 基于对象的classLis

(转载)企业级加密文件系统 eCryptfs 详解_NewbieRock的博客-程序员宝宝_ecryptfs tpm 分区

企业级加密文件系统 eCryptfs 详解近年来,保护个人敏感数据成为人们关注的热点问题,使用加密技术成为一种比较成功的保护方法。eCryptfs 是一个功能强大的企业级加密文件系统,通过堆叠在其它文件系统之上(如 Ext2, Ext3, ReiserFS, JFS 等),为应用程序提供透明、动态、高效和安全的加密功能。本文先介绍加密文件系统的背景,然后介绍 eCryptfs 的使用方法,最后阐述

Java8 EnumMap 源码分析_留兰香丶的博客-程序员宝宝

一、EnumMap 概述EnumMap 是一个用于存储 key 为枚举类型的 map,底层使用数组实现(K,V 双数组)。下面是其继承结构:public class EnumMap&lt;K extends Enum&lt;K&gt;, V&gt; extends AbstractMap&lt;K, V&gt; implements java.io.Serializable, Clon...

ubuntu安装串口助手cutecom_莫马达的博客-程序员宝宝_ubuntu安装串口助手

Serialport Assistantsudo apt-get install cutecomsudo usermod -a -G dialout user_name

Dubbox&&zookeeper基础_荒天帝灬的博客-程序员宝宝_dubbox zookeeper

目录Dubbox的原理Dubbo相关的jar包zookeeper的使用zookeeper的安装zookeeper的启动和结束调用服务例子写一个接口服务提供方配置服务调用方的配置远程注入属性扫描外部的bean管理中心分布式dubbo配置端口配置前言Dubbox是一个分布式服务框架,相等于Dubbox的升级版主要功能是进行远程的服务调...

随便推点

陈年老洞CVE-2017-11882漏洞分析_FFE5的博客-程序员宝宝

目录漏洞简介影响版本Office基础漏洞分析获取POC漏洞演示漏洞定位漏洞分析漏洞简介2017年11月14日,微软发布了11月份的安全补丁更新,其中比较引人关注的莫过于悄然修复了长达17年的Office远程代码执行(CVE-2017-11882)陈年老洞。该漏洞属于缓冲区溢出类型漏洞,影响目前流行的所有Office版本,产生漏洞原因于EQNEDT32.EXE...

NAT网络地址转换小实验_半世情`半世醉¹³¹⁴的博客-程序员宝宝

NAT网络地址转换小实验模拟实验情景 12开头外网,192开头时私网静态NAT小实验上述端口配置【路由器】interface GigabitEthernet0/0/0【路由器】ip address 192.168.1.254 255.255.255.0【路由器】interface GigabitEthernet0/0/1【路由器】ip address 12.0.0.254 255.255.255.0【路由器】nat static global 1.1.1.1 inside 192.16

LockSupport学习_小健健健的博客-程序员宝宝

文章目录blockerThread.interrupt()Object.wait()/notify()常用方法public static void park(Object blocker); // 暂停当前线程public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制public static void parkUntil(Object blocker, long deadline); // 暂停当前线

Eclipse 中使用Hibernate 反向工程_adalu1986的博客-程序员宝宝

1.生成Hibernate配置文件(.cfg.xml) 2.生成反向工程文件(.reveng.xml) 3.生成持久化类和映射文件(.java,.xml) 参考文章:http://blog.csdn.net/b671900/article/details/39156065