🔆 文章首发于我的个人博客:欢迎大佬们来逛逛
🔆 OpenCV项目地址及源代码:点击这里
文章目录
图形读取与显示
通过 imread
我们可以读取一个图片
其形式如下:
cpp
void imshow(const String& winname, InputArray mat);
而我们需要传递一个InputArray类型的参数,实际上就是 cv::Mat
如果创建cv::Mat呢?
可以通过它的很多的构造函数
先来了解一下图片的颜色通道。
- 颜色通道
- RGB 图像有4 个默认通道:红色、绿色和蓝色各有一个通道,以及一个用于编辑图像复合通道(主通道)
- 彩色深度
- 8位色,每个像素所能显示的彩色数为2的8次方,即256种颜色。
- 16位增强色,16位彩色,每个像素所能显示的彩色数为2的16次方,即65536种颜色。
- 24位真彩色,每个像素所能显示的彩色数为24位,即2的24次方,约1680万种颜色。
- 32位真彩色,即在24位真彩色图像的基础上再增加一个表示图像透明度信息的Alpha通道。
- Alpha通道:一张图片的透明和半透明度
我们使用如下的形式来描述它的通道类型:
cpp
CV_<bit_depth>(S|U|F)C<number_of_channels>
其中:
- bit_depth :位数,就是我们上面讲的彩色深度
- S|U|F:即 signed ,unsigned int,float 来存储
- number_of_channels:通道,有单通道,双通道,三通道,和四通道等等
加载图片
Mat类型的结构如下:
cpp
class Mat
{
public:
/*
flag:
1.数字签名
2.维度
3.通道数
4.连续性
*/
int flags;
int dims; //数据维数
int rows,cols; //数据行列
uchar *data; //存储的数据
const uchar* datastart; //数据开始
const uchar* dataend; //数据结束
const uchar* datalimit; //数据边界
//其他成员
//.....
//其他方法
//.....
public: //构造方式
// 默认构造函数 Mat A;
Mat ()
// 常用构造函数 Mat A(10,10,CV_8UC3);
Mat (int rows, int cols, int type)
//Mat A(300, 400, CV_8UC3,Scalar(255,255,255));
Mat (int ndims, const int *sizes, int type, const Scalar &s)
Mat (Size size, int type)
Mat (int rows, int cols, int type, const Scalar &s)
Mat (Size size, int type, const Scalar &s)
Mat (int ndims, const int *sizes, int type)
Mat (const Mat &m)
Mat (int rows, int cols, int type, void *data, size_t step=AUTO_STEP)
Mat (Size size, int type, void *data, size_t step=AUTO_STEP)
Mat (int ndims, const int *sizes, int type, void *data, const size_t *steps=0)
Mat (const Mat &m, const Range &rowRange, const Range &colRange=Range::all())
//Mat D (A, Rect(10, 10, 100, 100) );
Mat (const Mat &m, const Rect &roi)
Mat (const Mat &m, const Range *ranges)
};
我们使用如下的几种方式来加载图片:
- CV_8UC1:单通道
- Scalar:使用BGR形式
- clone/copyTo:从另一个Mat拷贝
cpp
void testShow() {
cv::Mat m1(200, 200, CV_8UC1);
cv::imshow("1", m1);
//std::cout << m << '\n';
cv::Mat m2(200, 200, CV_8UC3, cv::Scalar(255, 0, 255));
cv::imshow("2", m2);
cv::Mat m3 = m2.clone();
cv::imshow("3", m3);
cv::Mat m4;
m3.copyTo(m4);
cv::imshow("4", m4);
//imread
cv::Mat m5 = cv::imread("images/mm.png", cv::ImreadModes::IMREAD_GRAYSCALE);
cv::imshow("5", m5);
}
显示图片
我们使用函数:imshow
我们已经直到了它的第二个参数是一个类型,用来表示以何种形式显示图片:**cv::ImreadModes**
cpp
Mat imread( const String& filename, int flags = IMREAD_COLOR );
/****************************************************************
* filename: 文件路径
* flags : 显示方式
*****************************************************************/
enum ImreadModes {
IMREAD_UNCHANGED = -1, //按原样返回加载的图像(带有alpha通道,否则会被裁剪)
IMREAD_GRAYSCALE = 0, //单通道灰度图像
IMREAD_COLOR = 1, //3通道BGR彩色图像
IMREAD_ANYDEPTH = 2, //16位/32位图像,其他则转换为8位
IMREAD_ANYCOLOR = 4, //图像以任何可能的颜色格式读取
IMREAD_LOAD_GDAL = 8, //gdal驱动程序加载映像
IMREAD_REDUCED_GRAYSCALE_2 = 16, //单通道灰度图像,并将图像大小减小1/2
IMREAD_REDUCED_COLOR_2 = 17, //3通道BGR彩色图像,使图像大小减小1/2
IMREAD_REDUCED_GRAYSCALE_4 = 32, //单通道灰度图像,并将图像尺寸减小1/4
IMREAD_REDUCED_COLOR_4 = 33, //3通道BGR彩色图像,使图像大小减小1/4
IMREAD_REDUCED_GRAYSCALE_8 = 64, //单通道灰度图像,并将图像尺寸减小1/8
IMREAD_REDUCED_COLOR_8 = 65, //3通道BGR彩色图像,使图像大小减小1/8
IMREAD_IGNORE_ORIENTATION = 128 //不要根据EXIF的方向标志旋转图像
};
打印图片信息
我们可以打印Mat
的信息,因为它实际上就是一个矩阵,我们可以采用多种形式来格式化输出
- C语言形式
- numpy形式
- python形式
....
cpp
void testPrint() {
cv::Mat m(10, 10, CV_8UC1);
std::cout << "Mat: \n"<<m << '\n';
//格式化
std::cout << "C: \n" << cv::format(m, cv::Formatter::FMT_C);
std::cout << "numpy: \n" << cv::format(m, cv::Formatter::FMT_NUMPY);
}
保存图片
使用函数: imwrite
第一个参数为保存的图片的路径,第二个参数为保存的图片。
其中路径我们可以使用 cv::String
来传递,就是个字符串。
cpp
void testSaveFile(cv::String filename) {
auto m = cv::imread("./images/dog.png" ,cv::ImreadModes::IMREAD_GRAYSCALE);
cv::imshow("dog", m);
cv::imwrite(filename, m);
}
色彩模型转换
什么是色彩模型?
颜色模型指的是某个三维颜色空间中的一个可见光子集,它包含某个色彩域的所有色彩。一般而言,任何一个色彩域都只是可见光的子集,任何一个颜色模型都无法包含所有的可见光(通俗一点讲就是表示颜色的一种方式
RGB颜色模型
在计算机体系中,最常见的色彩模型就是**RGB颜色
**模型。
它具有三维坐标的模型形式:
- RGB16 :每个像素用
16个比特位
表示,占2个字节
。- RGB565:RGB分量分别使用5位、6位、5位:
- RGB555:RGB分量分别使用5位、5位、5位:
对于RGB555
,如何获取各个5位上的值,即分别获取 R,G,B所代表的值?
通过位运算即可获取。
cpp
/*
RGB 颜色模型
*/
struct XColor {
unsigned int color : 15;
};
void getRGB555() {
XColor col{};
col.color = 0b110000010111100;
std::cout << "R: " << (col.color >> 10) << '\n';
std::cout << "G: " << ((col.color & 0x3F0) >> 5) << '\n';
std::cout << "B: " << (col.color & 0x1F) << '\n';
}
- RGB24 格式:每个像素用
24比特位
表示,占3个字节
,在内存中RGB各排列顺序为:BGR:
-
RGB32 格式:每个像素用
32比特位
表示,占4个字节
,R,G,B分量分别用8个bit表示,存储顺序为B,G,R,最后8个字节保留- ARGB32:本质就是带
alpha通道
的RGB24,与RGB32的区别在与,保留的8个bit
用来表示透明,也就是alpha
的值
-
对于
**RGB32**
,如何获取各个8位上的值,即分别获取 R,G,B,Alpha所代表的值?- 通过位运算即可获取。
cppvoid getRGB32() { int color = 0x0F0A2B0C; std::cout << "B: " << (color >> 24) << '\n'; std::cout << "G: " << ((color & 0x00FF0000) >> 16) << '\n'; std::cout << "R: " << ((color & 0x0000FF00) >> 8) << '\n'; std::cout << "Alpha: " << (color & 0x000000FF) << '\n'; }
- ARGB32:本质就是带
HSV颜色模型
HSV
(Hue, Saturation,Value)也被称为六角锥体模型 ,即色调,饱和度,明度
将RGB转换为HSV模型:
- **
cvtColor
**函数:将图像从一种颜色模型转换为另一个。 - 起到关键作用的是第三个参数:
cv::ColorConversionCodes
是一个枚举,表示了你想要从谁转换到谁,这里我们让BGR形式转换为HSV形式。
cpp
cv::Mat res;
cv::cvtColor(m1, res, cv::ColorConversionCodes::COLOR_BGR2HSV);
cv::imshow("hsv", res);
HLS模型
HLS模型分别是色调,亮度,饱和度。
上图可以看出,固定一个颜色(H),那么随着饱和度(S,Chroma)的增加,颜色越来越深。
cpp
cv::cvtColor(m1, res, cv::ColorConversionCodes::COLOR_BGR2HLS);
cv::imshow("hls", res);
LAB模型
Lab颜色模型由三个要素组成,一个要素是**亮度**
(L),a 和b是两个颜色通道。
**a**
包括的颜色是从深绿色(低亮度值)到灰色(中亮度值)再到亮粉红色(高亮度值)**b**
是从亮蓝色(低亮度值)到灰色(中亮度值)再到黄色(高亮度值)。- 因此,这种颜色混合后将产生具有明亮效果的色彩。
cpp
cv::cvtColor(m1, res, cv::ColorConversionCodes::COLOR_BGR2Lab);
cv::imshow("Lab", res);
还有很多的颜色模型都可以在 cvtColor
这个函数中找到。
图像像素读写操作
openCV基本类型介绍:
- 基本类型:
cpp
typedef unsigned uint;
typedef signed char schar;
typedef unsigned char uchar;
typedef unsigned short ushort;
- 封装类型:Vec类似于std::vector,只不过可以指定其大小,并且命名规则为 cv:: Vec
大小
+类型
cpp
typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;
typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;
typedef Vec<ushort, 2> Vec2w;
typedef Vec<ushort, 3> Vec3w;
typedef Vec<ushort, 4> Vec4w;
typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<int, 6> Vec6i;
typedef Vec<int, 8> Vec8i;
图像是由像素掉构成的,因此我们可以获取图像的每一个像素,这个像素是由(i,j)确定的,即在图片宽度与高度的范围下,每一个行的每一列都可以是一个像素。
我们使用 Mat 的 at
函数来获取某个位置的像素值。
注意像素具有channels
的区别。
- 如果是单通道: 则直接获取
uchar
表示一个像素(一字节)。 - 如果是三通道: 则需要获取
Vec3b
表示三个像素,可以通过[0],[1],[2]来分别操作。
以下操作对图片转换为负片 ,使用 255 - 当前像素值。
cpp
#include <iostream>
#include <opencv2/opencv.hpp>
class ImgPixel {
public:
ImgPixel(std::string filename):m(cv::imread(filename)){}
void show(std::string title) {
cv::imshow(title, m);
}
void Visit_By_Array() {
auto dims = m.channels(); //获取通道数
for (int i = 0; i < m.rows; i++) {
for (int j = 0; j < m.cols; j++) {
if (dims == 1) {
//如果是一通道,则直接操作
uchar rets = m.at<uchar>(i, j);
rets = 255 - rets;
}
if (dims == 3) {
cv::Vec3b rets = m.at<cv::Vec3b>(i, j);
#if 0
m.at<cv::Vec3b>(i, j)[0] = 255 - rets[0]; //转换为负片
m.at<cv::Vec3b>(i, j)[1] = 255 - rets[1];
m.at<cv::Vec3b>(i, j)[2] = 255 - rets[2];
#else
//安全类型转换: 超过255的赋值为 255 小于0的赋值为 0
m.at<cv::Vec3b>(i, j)[0] = cv::saturate_cast<uchar>(rets[0] + 100);
m.at<cv::Vec3b>(i, j)[1] = cv::saturate_cast<uchar>(rets[1] + 100);
m.at<cv::Vec3b>(i, j)[2] = cv::saturate_cast<uchar>(rets[2] + 100);
#endif
}
}
}
}
private:
cv::Mat m;
};
int main(){
ImgPixel img("dog.png");
img.show("origin");
img.Visit_By_Array();
img.show("convert1");
cv::waitKey(0);
return 0;
}
安全类型转换:saturate_cast<T>
是openCV的一种安全转换函数,当我们对像素执行加减乘除的时候,有可能会超出 [ 0 , 255 ] [0,255] [0,255]
的范围,因此使用此函数来保证不会越界。
实际上这个函数就是:
- 超过255:转为255
- 小于0:转为0
cpp
template<> inline uchar saturate_cast<uchar>(int v) { return (uchar)((unsigned)v <= UCHAR_MAX ? v : v > 0 ? UCHAR_MAX : 0); }
像素算数运算
图像可以进行像素之间的算数运算,跟我们上节的对像素的简单减法是一样的:
我们有专门的函数:
- add:像素加
- subtract:像素减
- multiply:像素乘
- divide:像素除
- addWeighted :对图片执行: α ⋅ i m g 1 + β ⋅ i m g 2 = r e s \alpha\cdot img_1 + \beta\cdot img_2 = res α⋅img1+β⋅img2=res 的操作,其中的 α \alpha α和 β \beta β就是透明度。
除了基本的运算,还有二进制运算:
- and:像素按位与
- or:按位或
- **not:**按位取反
- xor:按位异或
测试如下:
需要两个原图片执行下面的这些操作,我使用的图片是:(注意尺寸要一样)
然后自行执行下面代码。
cpp
#include <iostream>
#include <opencv2/opencv.hpp>
class ImgOperation {
public:
ImgOperation() :m1(cv::imread("dog.png")), m2(cv::imread("text.jpg")) {}
void testAdd(std::string title="add") {
cv::add(m1, m2, res);
cv::imshow(title, res);
cv::waitKey();
}
void testSub(std::string title = "subtract") {
cv::subtract(m1, m2, res);
cv::imshow(title, res);
cv::waitKey();
}
void testMul(std::string title = "multiply") {
cv::multiply(m1, m2, res);
cv::imshow(title, res);
cv::waitKey();
}
void testDivide(std::string title = "divide") {
cv::divide(m1, m2, res);
cv::imshow(title, res);
cv::waitKey();
}
void testAddWeighted(std::string title="AddWeighted") {
cv::addWeighted(m1, 0.1, m2, 0.9, 0, res);
cv::imshow(title, res);
cv::waitKey();
}
//BitWise
void testBitWise() {
cv::Mat _and, _or, _xor, _not;
cv::bitwise_and(m1, m2, _and);
cv::bitwise_or(m1, m2, _or);
cv::bitwise_xor(m1, m2, _xor);
cv::bitwise_not(m1, _not);
cv::imshow("and", _and);
cv::imshow("or", _or);
cv::imshow("xor", _xor);
cv::imshow("not", _not);
}
private:
cv::Mat m1;
cv::Mat m2;
cv::Mat res;
};
int main()
{
ImgOperation opt;
//opt.testAdd();
//opt.testSub();
//opt.testMul();
//opt.testDivide();
//opt.testAddWeighted();
opt.testBitWise();
cv::waitKey();
return 0;
}
图形文字绘制
绘制线
使用**line
**函数,原型如下:
cpp
void line(InputOutputArray img, Point pt1, Point pt2, const Scalar& color,int thickness = 1, int lineType = LINE_8, int shift = 0);
//线的样式
enum LineTypes {
FILLED = -1,
LINE_4 = 4, //!< 4-connected line
LINE_8 = 8, //!< 8-connected line
LINE_AA = 16 //!< antialiased line
};
/*******************************************************************
* img: 绘制在那个图像上
* pt1: 起点
* pt2: 终点
* color: 颜色
* thickness: 厚度(宽度)
* lineType: 线的样式
* FILLED: 线填充的
* LINE_4: 4邻接连接线
* LINE_8: 8邻接连接线
* LINE_AA:反锯齿连接线(高斯滤波)
* shift: 坐标点小数点位数(可忽略不写)
*********************************************************************/
在执行 imshow的之前,在图片上画一条线:
cpp
void testDrawLine() {
//绘制线
cv::line(m, cv::Point(0, 0), cv::Point(300, 300), cv::Scalar(0,255,0), 3, cv::LineTypes::LINE_AA);
}
绘制圆
使用**circle
函数来绘制 圆或者填充圆**
cpp
void circle(InputOutputArray img, Point center, int radius,const Scalar& color, int thickness = 1,int lineType = LINE_8, int shift = 0);
//线的样式
enum LineTypes {
FILLED = -1,
LINE_4 = 4, //!< 4-connected line
LINE_8 = 8, //!< 8-connected line
LINE_AA = 16 //!< antialiased line
};
/*******************************************************************
* img: 绘制在那个图像上
* center: 圆心坐标
* radius: 半径
* color: 颜色
* thickness: 厚度(宽度)
* -1: 填充圆
* 其他值: 空心
* lineType: 线的样式
* FILLED: 线填充的
* LINE_4: 4邻接连接线
* LINE_8: 8邻接连接线
* LINE_AA:反锯齿连接线(高斯滤波)
* shift: 坐标点小数点位数(可忽略不写)
*********************************************************************/
效果如下:
- 填充效果需要把
thickness
置为-1。
cpp
void testDrawCircle() {
cv::circle(m, cv::Point(150, 150), 50, cv::Scalar(0, 255, 0),-1); //填充圆
cv::circle(m, cv::Point(150, 150), 52, cv::Scalar(0, 0, 255), 2,
cv::LineTypes::LINE_AA); //空心圆
}
绘制矩形
使用rectangle
函数来绘制矩形。
cpp
void rectangle(InputOutputArray img, Rect rec,const Scalar& color, int thickness = 1,int lineType = LINE_8, int shift = 0);
/*******************************************************************
* img: 绘制在那个图像上
* rec: 矩形大小 Rect(x,y,w,h);
* x,y: 起始坐标
* w,h: 宽度和高度
* color: 颜色
* thickness: 厚度(宽度)
* -1: 填充矩形
* 其他值: 空心矩形
* lineType: 线的样式
* FILLED: 线填充的
* LINE_4: 4邻接连接线
* LINE_8: 8邻接连接线
* LINE_AA:反锯齿连接线(高斯滤波)
* shift: 坐标点小数点位数(可忽略不写)
*********************************************************************/
效果如下:
cpp
void testDrawRectangle() {
cv::rectangle(m, cv::Rect(0, 0, 50, 50), cv::Scalar(100, 45, 20),-1);
}
绘制椭圆
使用ellipse
来绘制椭圆:
cpp
void ellipse(InputOutputArray img, Point center, Size axes,double angle, double startAngle, double endAngle,
const Scalar& color, int thickness = 1,int lineType = LINE_8, int shift = 0);
/*******************************************************************
* img: 绘制在那个图像上
* center: 椭圆圆心
* axes: 矩形内置椭圆
* angle: 倾斜角
* startAngle: 扩展的弧度 0
* endAngle: 扩展的弧度 360
* color: 颜色
* thickness: 线宽度
* -1: 填充矩形
* 其他值: 空心矩形
* lineType: 线的样式
* FILLED: 线填充的
* LINE_4: 4邻接连接线
* LINE_8: 8邻接连接线
* LINE_AA:反锯齿连接线(高斯滤波)
* shift: 坐标点小数点位数(可忽略不写)
*********************************************************************/
效果如下:
cpp
void testDrawEllipse() {
cv::ellipse(m, cv::Point(100, 100),cv::Size(20,80),
180,0,360, cv::Scalar(54, 54, 43));
cv::ellipse(m, cv::Point(100, 100), cv::Size(20, 80),
90, 0, 360, cv::Scalar(54, 54, 43),-1);
}
绘制文字
使用**putText
来绘制文字,注意不支持中文!!!!!!!!!!!!!**
cpp
void putText( InputOutputArray img, const String& text, Point org,int fontFace, double fontScale, Scalar color,int thickness = 1, int lineType = LINE_8,bool bottomLeftOrigin = false );
/*******************************************************************
* img: 绘制在那个图像上
* text: 绘制文字
* org: 文本框左下角
* fontFace: 字体
* fontScale: 缩放
* color: 颜色
* thickness 线宽度
* lineType: 线的样式
* FILLED: 线填充的
* LINE_4: 4邻接连接线
* LINE_8: 8邻接连接线
* LINE_AA:反锯齿连接线(高斯滤波)
* bottomLeftOrigin: 起点位置
* true: 左上角 反转倒立显示
* false: 左下角 正常显示
*********************************************************************/
//opencv 不识别汉字
//fontFace: 字体
enum HersheyFonts {
FONT_HERSHEY_SIMPLEX = 0, //!< normal size sans-serif font //灯芯体
FONT_HERSHEY_PLAIN = 1, //!< small size sans-serif font
FONT_HERSHEY_DUPLEX = 2, //!< normal size sans-serif font (more complex than FONT_HERSHEY_SIMPLEX)
FONT_HERSHEY_COMPLEX = 3, //!< normal size serif font
FONT_HERSHEY_TRIPLEX = 4, //!< normal size serif font (more complex than FONT_HERSHEY_COMPLEX)
FONT_HERSHEY_COMPLEX_SMALL = 5, //!< smaller version of FONT_HERSHEY_COMPLEX
FONT_HERSHEY_SCRIPT_SIMPLEX = 6, //!< hand-writing style font
FONT_HERSHEY_SCRIPT_COMPLEX = 7, //!< more complex variant of FONT_HERSHEY_SCRIPT_SIMPLEX
FONT_ITALIC = 16 //!< flag for italic font
};
测试如下:
cpp
void testDrawText(const std::string& name) {
cv::putText(m,
name,
cv::Point(10, 200),
cv::HersheyFonts::FONT_HERSHEY_COMPLEX,
1.0,
cv::Scalar(50,50,255),
2,
cv::LineTypes::LINE_AA,
false); //翻转
}
其他绘制函数
绘制多边形线:
- polylines
绘制填充多边形:
- fillPoly
文本参考