opencv的点点滴滴
Ptr
源码分析Ptr
是一个智能指针,用于动态分配objects。这是一个template pointer-wrapping class,用于存储引用计算(reference counter)以及object pointer。它类似于C++中的智能指针,但它更加简易。可以这样使用:Ptr<MyObjectType> ptr
。
template<typename _Tp> class CV_EXPORTS Ptr
{
public:
//! empty constructor
Ptr();
//! take ownership of the pointer. The associated reference counter is allocated and set to 1
Ptr(_Tp* _obj);
//! calls release()
~Ptr();
//! copy constructor. Copies the members and calls addref()
Ptr(const Ptr& ptr);
//! copy operator. Calls ptr.addref() and release() before copying the members
Ptr& operator = (const Ptr& ptr);
//! increments the reference counter
void addref();
//! decrements the reference counter. If it reaches 0, delete_obj() is called
void release();
//! deletes the object. Override if needed
void delete_obj();
//! returns true iff obj==NULL
bool empty() const;
//! helper operators making "Ptr<T> ptr" use very similar to "T* ptr".
_Tp* operator -> ();
const _Tp* operator -> () const;
operator _Tp* ();
operator const _Tp*() const;
protected:
_Tp* obj; //< the object pointer.
int* refcount; //< the associated reference counter
};
所谓的智能指针,其实就是模板参数可以是任意的C++ clas,但考虑到对象在使用完毕的时候,需要进行析构,因此需要特化delete_obj()
函数。
Ptr
的构造函数:
// obj是指向对象的指针,refcount是引用计数,默认ctor只是构造了一个空对象
// 带参数的ctor则使用了动态分配内存的技术。其中,fastMalloc这个函数使用了pointer alignment技术
template<typename _Tp> inline Ptr<_Tp>::Ptr() : obj(0), refcount(0) {}
template<typename _Tp> inline Ptr<_Tp>::Ptr(_Tp* _obj) : obj(_obj)
{
if(obj)
{
refcount = (int*)fastMalloc(sizeof(*refcount));
*refcount = 1;
}
else
refcount = 0;
}
// 对于拷贝ctor,参数对应的对象不需要进行释放,直接进行拷贝即可,所以引用计数加1。
// 赋值operator则需要先释放掉参数对应的对象,因为将当前对象赋值给另一个对象之后,
// 当前指针就不再指向任何对象
template<typename _Tp> inline Ptr<_Tp>::Ptr(const Ptr<_Tp>& ptr) {
obj = ptr.obj;
refcount = ptr.refcount;
addref();
}
template<typename _Tp> inline Ptr<_Tp>& Ptr<_Tp>::operator = (const Ptr<_Tp>& ptr) {
int* _refcount = ptr.refcount;
if( _refcount )
CV_XADD(_refcount, 1);
release();
obj = ptr.obj;
refcount = _refcount;
return *this;
}
fastMalloc()
的实现:
void* fastMalloc( size_t size )
{
// 在此处,内存多开辟了20字节的空间。其中,前4个字节用以存储分配的这块空间的首地址。
// 在进行释放空间时,可以很快地进行释放。另外的16字节用于调整地址,是地址到达16的倍数
// 其中,CV_MALLOC_ALIGN的值为16
uchar* udata = (uchar*)malloc(size + sizeof(void*) + CV_MALLOC_ALIGN);
if(!udata)
return OutOfMemoryError(size);
// alignPtr用于将地址空间的大小向上调整为16的倍数
// udata+1 ===> 其实是 udata + sizeof(uchar**) ,即4,这4个字节便是用于存储udata这个地址空间的起始地址
// 要将udata强制转换为uchar**的原因是:因为要访问者4个字节之中的内容,而这个内容中存储的又是首地址,于是就是二级指针
uchar** adata = alignPtr((uchar**)udata + 1, CV_MALLOC_ALIGN);
// adata[-1] ===> -1就是向前移动4个字节,既是udata这个地址空间的起始地址
adata[-1] = udata;
return adata;
}
void fastFree(void* ptr)
{
if(ptr) {
// 取分配地址的首地址
uchar* udata = ((uchar**)ptr)[-1];
CV_DbgAssert(udata < (uchar*)ptr &&
((uchar*)ptr - udata) <= (ptrdiff_t)(sizeof(void*)+CV_MALLOC_ALIGN));
// 直接删除首地址即可删除整个分配的空间
free(udata);
}
}
alignPtr()
的实现:
template<typename _Tp> static inline _Tp* alignPtr(_Tp* ptr, int n=(int)sizeof(_Tp))
{
return (_Tp*)(((size_t)ptr + n-1) & -n);
}
此处使用了位操作的黑魔法,
这个函数用来将ptr
指向的地址对齐到n
边界,使得到的地址是n
的倍数,由于此处n=CV_MALLOC_ALIGN=16
。
现假设ptr
为十六进制形式,ptr=0xYY YY YY YY
,则ptr+n-1
的计算为:
当ptr
的低4位不全为0
时,ptr+n-1
的结果将导致进位,加1
。再与-n
相&
,而-n==0xF0
,所以最终返回的地址形式为0x YY YY YY Y0
,即为CV_MALLOC_ALIGN==16
的倍数。
增加引用计数时,使用了无锁(lock-free)编程技术:
template<typename _Tp> inline void Ptr<_Tp>::addref() {
if( refcount )
CV_XADD(refcount, 1);
}
CV_XADD
该操作类似于post increment的运算,使用了无锁化编程,其可以用于多线程编程,用户不需要自己维护锁。
接下来是释放内存的操作:
template<typename _Tp> inline void Ptr<_Tp>::release()
{
if( refcount && CV_XADD(refcount, -1) == 1 )
{
delete_obj();
fastFree(refcount);
}
refcount = 0;
obj = 0;
}
template<typename _Tp> inline void Ptr<_Tp>::delete_obj()
{
if( obj ) delete obj;
}
template<typename _Tp> inline Ptr<_Tp>::~Ptr() { release(); }
其他操作:
template<typename _Tp> inline _Tp* Ptr<_Tp>::operator -> () { return obj; }
template<typename _Tp> inline const _Tp* Ptr<_Tp>::operator -> () const { return obj; }
template<typename _Tp> inline Ptr<_Tp>::operator _Tp* () { return obj; }
template<typename _Tp> inline Ptr<_Tp>::operator const _Tp*() const { return obj; }
什么是对齐(alignment)?
在现代的计算机中,内存空间都是按照byte进行划分的,从理论上讲,对任何类型的变量的访问,都可以从任何地址开始,但实际情况是:在访问特定变量的时候,经常需要在特定的内存地址进行访问,这就是对齐。
需要对齐的原因:
- 某些硬件平台只能在某些地址取某些特定类型的数据,否则抛出硬件异常。
- 其余的硬件平台虽然可以在任何地址处,取得任何类型的数据。但如果变量没有对齐的情况下,取这个数据可能会存在效率上的损失。
许多硬件相关的东西在对齐上存在限制。在某些系统中,某种数据类型只能存储在偶数边界的地址处。
例如,在经典的SPARC架构(以及经典的ARM)上,便不能从奇数地址读取一个超过1
字节的整型数据。尝试这么做将会立即终止程序,并伴随着总线(bus)错误。而在X86架构上,CPU硬件处理了这个问题,只是这么做将会花费更多时间,而RISC架构通常是不会为用户做这些。
mixChannels()
将输入数组的指定通道复制到输出数组的指定通道。
void mixChannels(
const Mat* src, //输入数组或向量矩阵,所有矩阵的大小和深度必须相同。
size_t nsrcs, //矩阵的数量
Mat* dst, //输出数组或矩阵向量,大小和深度必须与src[0]相同
size_t ndsts, //矩阵的数量
const int* fromTo,//指定被复制通道与要复制到的位置组成的索引对
size_t npairs //fromTo中索引对的数目
);
例如,将一个4
通道BGRA图像分割成一个3
通道BGR和一个单独的alpha通道图像:
#include<opencv2/opencv.hpp>
using namespace cv;
int main()
{
Mat bgra( 500, 500, CV_8UC4, Scalar(255,255,0,255) );
Mat bgr( bgra.rows, bgra.cols, CV_8UC3 );
Mat alpha( bgra.rows, bgra.cols, CV_8UC1 );
Mat out[] = { bgr, alpha };
int from_to[] = { 0, 2, 1, 1, 2, 0, 3, 3 };
mixChannels( &bgra, 1, out, 2, from_to, 4 );
imshow("bgra", bgra);
imshow("bgr", bgr);
waitKey(0);
return 0;
}
其中,索引对from_to[] = { 0, 2, 1, 1, 2, 0, 3, 3 }
的含义为:
- bgra的
0
通道复制到out[]
的2
通道,即bgr的0
通道 - bgra的
1
通道复制到out[]
的1
通道,即bgr的1
通道 - bgra的
2
通道复制到out[]
的0
通道,即bgr2
通道 - bgra的
3
通道复制到out[]
的3
通道,即alpha通道
因此,原图的青色(255,255,0)
通过指定通道复制到输出图像中变成了黄色(0,255,255)
。
calcHist
void calcHist(
const Mat* images, //输入的图像的指针,可以是多幅图像,所有的图像必须有同样的深度(CV_8U or CV_32F)。
//同时一副图像可以有多个channels。
int nimages, // 输入图像的数量
const int* channels, //用来计算直方图的channels的数组。
//比如输入是两副图像,第一副图像有0,1,2共三个channel
//第二幅图像只有一个channel:0,那么输入就一共有4个channels
//如果int channels[3] = {3, 2, 0},那么就表示是使用第二副图像的第一个通道和第一副图像的第2和第0个通道来计算直方图。
InputArray mask,
OutputArray hist, //计算出来的直方图
int dims, //计算出来的直方图的维数
const int* histSize, //在每一维上直方图的个数。如果简单地把直方图看作一个一个的竖条的话,就是每一维上竖条的个数
const float** ranges,
//用来进行统计的范围。比如
// float rang1[] = {0, 20};
// float rang2[] = {30, 40};
// const float *rangs[] = {rang1, rang2};
//那么就是对0—20和30—40范围的值进行统计。
bool uniform=true, // 一个竖条的宽度是否相等
bool accumulate=false
)
findContours()
其用于寻找图像中物体的轮廓(一般结合drawContours()
函数将找到的轮廓绘制出)。
void cv::findContours (
// 输入图像,图像必须为8-bit单通道图像,图像中的非零像素将被视为1,0像素保留其像素值,
//故加载图像后会自动转换为二值图像。同样可以使用cv::compare,cv::inRange,cv::threshold,
//cv::adaptiveThreshold,cv::Canny等函数来创建二值图像。如果第四个参数为cv::RETR_CCOMP或
//cv::RETR_FLOODFILL,输入图像可以是32-bit整型图像(CV_32SC1)
InputOutputArray image,
OutputArrayOfArrays contours,
OutputArray hier
int mode,
int method,
Point offset = Point()
)
- 第一个参数:
image
,单通道(8-bit)图像矩阵,可以是灰度图,但更常用的是二值图像,一般是经过Canny
、Laplcian
等边缘检测算子处理过的二值图像 - 第二个参数:
contours
,定义为vector<vector<Point>> contours
,是一个双重向量。向量内每个元素保存了一组由连续的Point
点构成的点的集合的向量,每一组Point
点集就是一个轮廓。有多少轮廓,向量contours
就有多少元素。 - 第三个参数:
hierarchy
,定义为vector<Vec4i> hierarchy
。向量hiararchy
内的元素和轮廓向量contours
内的元素是一一对应的,向量的容量相同。hierarchy
向量内每一个元素的4
个int
型变量—hierarchy[i][0] ~ hierarchy[i][3]
,分别表示第i
个轮廓的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。如果当前轮廓没有对应的后一个轮廓、前一个轮廓、父轮廓或内嵌轮廓的话,则hierarchy[i][0] ~ hierarchy[i][3]
的相应位被设置为默认值-1
。 - 第四个参数:
mode
,定义轮廓的检索模式:CV_RETR_EXTERNAL
:只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略CV_RETR_LIST
:检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓,所以hierarchy
向量内所有元素的第3
、第4
个分量都会被置为-1
CV_RETR_CCOMP
:检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层CV_RETR_TREE
:检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。
- 第五个参数:
method
,定义轮廓的近似方法:CV_CHAIN_APPROX_NONE
:获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过1CV_CHAIN_APPROX_SIMPLE
:仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours
向量内,拐点与拐点之间直线段上的信息点不予保留CV_CHAIN_APPROX_TC89_L1
,CV_CHAIN_APPROX_TC89_KCOS
使用teh-Chinl chain
近似算法
- 第六个参数:
offset
,所有的轮廓信息相对于原始图像对应点的偏移量,相当于在每一个检测出的轮廓点上加上该偏移量
drawContours
void cv::drawContours (
InputOutputArray image,
InputArrayOfArrays contours, // 使用findContours检测到的轮廓数据,每个轮廓以点向量的形式存储,Point类型的vector
int contourIdx,
const Scalar & color, //轮廓的颜色
int thickness = 1, //绘制轮廓所用线条粗细度,如果值为负值,则在轮廓内部绘制
int lineType = LINE_8,//线条类型,默认值为LINE_8
InputArray hierarchy = noArray(),//可选层次结构信息,可传入vector<Vec4i> hierarchy;
int maxLevel = INT_MAX,
Point offset = Point()
)
createTrackbar()
在显示图像的窗口中快速创建一个滑动控件,用于手动调节阈值。
CV_EXPORTS int createTrackbar(
const string& trackbarname, //滑动控件的名称
const string& winname, // 滑动控件所在的图像窗口的名称
int* value, // 初始化的阈值
int count,//滑动控件的刻度范围
TrackbarCallback onChange = 0,
//TrackbarCallback是回调函数,一般将回调函数定义为:void thresh_callback(int, void*)
void* userdata = 0
);
其中,TrackbarCallback
的定义为:
typedef void (CV_CDECL *TrackbarCallback)(int pos, void* userdata);
approxPolyDP()
它的主要功能是:把一个连续光滑曲线折线化,对图像轮廓点进行多边形拟合。
函数原型为:
void approxPolyDP(
InputArray curve, //一般是由图像的轮廓点组成的点集
OutputArray approxCurve, //表示输出的多边形点集
double epsilon, //要表示输出的精度,就是另个轮廓点之间最大距离数,5,6,7,8...
bool closed //表示输出的多边形是否封闭
)
原理图(对比之前黑点连线,之后蓝色连线):
算法描述如下:
起始曲线是有序的一组点或线,距离维度ε>0
,该算法递归地划分线。
- 最初,给出了第一点和最后一点之间的所有点,它会自动标记要保存的第一个和最后一个点。
- 然后,找到距离第一点和最后一点组成的线段的最远的点作为终点; 这一点在距离终点之间的近似线段的曲线上显然最远。 如果该点比线段更接近于
ε
,那么当前未被标记的任何点将被保存,而没有简化的曲线比ε
更差的可以丢弃。
如果离线段最远的点距离近似值大于ε
,则必须保留该点。该算法以第一个点和最远点递归地调用自身,然后以最远点和最后一个点(包括最远点被标记为保留)递归调用自身。
当递归完成时,可以生成一个新的输出曲线,其中包括所有且仅标记为保留的点。
伪代码:
function DouglasPeucker(PointList[], epsilon)
// Find the point with the maximum distance
dmax = 0
index = 0
end = length(PointList)
for i = 2 to ( end - 1) {
d = perpendicularDistance(PointList[i], Line(PointList[1], PointList[end]))
if ( d > dmax ) {
index = i
dmax = d
}
}
// If max distance is greater than epsilon, recursively simplify
if ( dmax > epsilon ) {
// Recursive call
recResults1[] = DouglasPeucker(PointList[1...index], epsilon)
recResults2[] = DouglasPeucker(PointList[index...end], epsilon)
// Build the result list
ResultList[] = {recResults1[1...length(recResults1)-1], recResults2[1...length(recResults2)]}
} else {
ResultList[] = {PointList[1], PointList[end]}
}
// Return the result
return ResultList[]
end
boundingRect()
当得到对象轮廓后,可用
Rect boundingRect(InputArray points)
得到包覆此轮廓的最小正矩形RotatedRect minAreaRect(InputArray points)
得到包覆轮廓的最小斜矩形void minEnclosingCircle(InputArray points, Point2f& center, float& radius)
得到包覆此轮廓的最小圆形。其中,points
为输入的二维点集,center
为输出的圆形的中心坐标,radius
为输出的最小圆的半径。
这些函数可以填补空隙,或者作进一步的对象辨识。boundingRect()
返回正矩形,所以如果对象有倾斜的情形,返回的可能不是想要的结果。
图像通道的分离与合并
在图像处理中,尤其是处理多通道图像时,有时需要对各个通道进行分离,分别处理;有时还需要对分离处理后的各个通道进行合并,重新合并成一个多通道的图像。
cv::split()
void cv::split(
const cv::Mat& mtx, //输入图像
vector<Mat>& mv // 输出的多通道序列(n个单通道序列)
);
cv::merge()
void merge(
const vector<cv::Mat>& mv, // 输入的多通道序列(n个单通道序列)
cv::OutputArray dst // 输出图像,包含mv
);
图像翻转
void cv::flip(
cv::InputArray src, // 输入图像
cv::OutputArray dst, // 输出
int flipCode = 0 // >0: 沿y-轴翻转, 0: 沿x-轴翻转, <0: x、y轴同时翻转
);
Mat matRotateClockWise90(Mat src)
{
if (src.empty())
{
qDebug()<<"RorateMat src is empty!";
}
// 矩阵转置
transpose(src, src);
//0: 沿X轴翻转; >0: 沿Y轴翻转; <0: 沿X轴和Y轴翻转
flip(src, src, 1);// 翻转模式,flipCode == 0垂直翻转(沿X轴翻转),flipCode>0水平翻转(沿Y轴翻转),flipCode<0水平垂直翻转(先沿X轴翻转,再沿Y轴翻转,等价于旋转180°)
return src;
}
Mat matRotateClockWise180(Mat src)//顺时针180
{
if (src.empty())
{
qDebug() << "RorateMat src is empty!";
}
//0: 沿X轴翻转; >0: 沿Y轴翻转; <0: 沿X轴和Y轴翻转
flip(src, src, 0);// 翻转模式,flipCode == 0垂直翻转(沿X轴翻转),flipCode>0水平翻转(沿Y轴翻转),flipCode<0水平垂直翻转(先沿X轴翻转,再沿Y轴翻转,等价于旋转180°)
flip(src, src, 1);
return src;
//transpose(src, src);// 矩阵转置
}
Mat matRotateClockWise270(Mat src)//顺时针270
{
if (src.empty())
{
qDebug() << "RorateMat src is empty!";
}
// 矩阵转置
//transpose(src, src);
//0: 沿X轴翻转; >0: 沿Y轴翻转; <0: 沿X轴和Y轴翻转
transpose(src, src);// 翻转模式,flipCode == 0垂直翻转(沿X轴翻转),flipCode>0水平翻转(沿Y轴翻转),flipCode<0水平垂直翻转(先沿X轴翻转,再沿Y轴翻转,等价于旋转180°)
flip(src, src, 0);
return src;
}
Mat myRotateAntiClockWise90(Mat src)//逆时针90°
{
if (src.empty())
{
qDebug()<<"mat is empty!";
}
transpose(src, src);
flip(src, src, 0);
Mat
的<<
操作
// 创建 3 x 3 双精度恒等矩阵
Mat M = (Mat_ <double> (3,3) <<1,0,0,0,1,0,0,0,1);
使用此方法,首先调用具有适当的参数的 Mat_
类构造函数,然后只需要把 <<
运算符后面的值用逗号分隔,这些值可以是常量、变量、 表达式,等等。注意:所需的额外的圆括号 ((Mat_<double>(3,3)<<1,0,0,0,1,0,0,0,1))
以免出现编译错误。
CNN 模块
1. 读取 .protxt
文件和 .caffemodel
文件
cv::dnn::Net net = cv::dnn::readNetFromCaffe(modelTxt, modelBin);
2. 检查网络是否读取成功
if (net.empty()) {
std::cerr << "Can't load network by using the following files: " << std::endl;
std::cerr << "prototxt: " << modelTxt << std::endl;
std::cerr << "caffemodel: " << modelBin << std::endl;
std::cerr << "bvlc_googlenet.caffemodel can be downloaded here:" << std::endl;
std::cerr << "http://dl.caffe.berkeleyvision.org/bvlc_googlenet.caffemodel" << std::endl;
exit(-1);
}
3. blobFromImage
Mat cv::dnn::blobFromImage(
InputArray image,
double scalefactor = 1.0,
const Size & size = Size(),
const Scalar & mean = Scalar(),
bool swapRB = false,
bool crop = false,
int ddepth = CV_32F
)
从图像创建一个 4
维 blob。 (可选)从中心调整大小并裁剪 image
,减去 mean
值,通过 scalefactor
缩放值,交换蓝色和红色通道。构造 blob,为传入网络做准备,图片不能直接进入网络。
参数:
image
:input image (with 1-, 3- or 4-channels)scalefactor
:multiplier for image values(缩放因子)size
:spatial size for output image(输出图像的尺寸)mean
:scalar with mean values which are subtracted from channels. Values are intended to be in (mean-R, mean-G, mean-B) order if image has BGR ordering andswapRB
is trueswapRB
:flag which indicates that swap first and last channels in 3-channel image is necessarycrop
:flag which indicates whether image will be cropped after resize or notddepth
:Depth of output blob. ChooseCV_32F
orCV_8U
4. setInput
void cv::dnn::Net::setInput (
InputArray blob,
const String & name = "",
double scalefactor = 1.0,
const Scalar & mean = Scalar()
)
设置网络的新输入值。即将构建的 blob 传入网络 data 层
参数:
blob
:A new blob. Should haveCV_32F
orCV_8U
depth.name
:A name of input layer.scalefactor
:An optional normalization scale.mean
:An optional mean subtraction values.
5. forward
Mat cv::dnn::Net::forward (
const String & outputName = String()
)
进行前向传递,计算名称为 outputName
的图层输出(Runs forward pass to compute output of layer with name outputName,即前向预测)。
返回值:blob for first output of specified layer.