一、概述

1、图像识别的一个核心问题是图像的特征提取,简单描述即为用一组简单的数据(数据描述量)来描述整个图像,这组数据月简单越有代表性越好。良好的特征不受光线、噪点、几何形变的干扰,图像识别技术的发展中,不断有新的描述图像特征提出,而图像不变矩就是其中一个。

2、从图像中计算出来的矩通常描述了图像不同种类的几何特征如:大小、灰度、方向、形状等,图像矩广泛应用于模式识别、目标分类、目标识别与防伪估计、图像编码与重构等领域。.

严格来讲矩是概率与统计中的一个概念,是随机变量的一种数字特征。设 x 为随机变量,C为常数,则量E[(x−c)^k]称为X关于C点的k阶矩。比较重要的两种情况如下:

opencv求质心 opencv 矩_二维


一阶原点矩就是期望,一阶中心矩μ_1=0,二阶中心矩μ_2就是X的方差Var(X)。在统计学上,高于4阶的矩极少使用,μ_3可以去衡量分布是否有偏,μ_4可以衡量分布(密度)在均值拘谨的陡峭程度。3、针对一幅图像,我们把像素的坐标看成是一个二维随机变量(X, Y),那么一副灰度图可以用二维灰度图密度函数来表示,因此可以用矩来描述灰度图像的特征。 图像可以看成是一个平板的物体,其一阶矩和零阶矩就可以拿来计算某个形状的重心,而二阶矩就可以拿来计算形状的方向。

opencv求质心 opencv 矩_二维_02


其中M00即零阶矩,M20和M02为二阶矩,接下来计算物体形状的方向

opencv求质心 opencv 矩_OpenCV_03


opencv求质心 opencv 矩_二维_04

4、不变矩(Invariant Moments)是一种高度浓缩的图像特征,具有平移、灰度、尺度、旋转不变性,由M.K.Hu在1961年首先提出,1979年M.R.Teague根据正交多项式理论提出了Zernike矩。不变矩的物理含义:

**如果把图像看成是一块质量密度不均匀的薄板,其图像的灰度分布函数f(x,y)就是薄板的密度分布函数,则其各阶矩有着不同的含义,如零阶矩表示它的总质量;一阶矩表示它的质心;二阶矩又叫惯性矩,表示图像的大小和方向。**事实上,如果仅考虑阶次为2的矩集,则原始图像等同于一个具有确定的大小、方向和离心率,以图像质心为中心且具有恒定辐射率的椭圆。由三阶矩以下矩构成的七个矩不变量具有平移、旋转和尺度不变性等等。当密度分布函数发生改变时,图像的实质没有改变,仍然可以看做一个薄板,只是密度分布有所改变。虽然此时各阶矩的值可能发生变化,但由各阶矩计算出的不变矩仍具有平移、旋转和尺度不变性。通过这个思想,可对图像进行简化处理,保留最能反映目标特性的信息,再用简化后的图像计算不变矩特征,可减少计算量。

(研究表明,只有基于二阶矩的不变矩对二维物体的描述才是真正的与旋转、平移和尺度无关的。较高阶的矩对于成像过程中的误差,微小的变形等因素非常敏感,所以相应的不变矩基本上不能用于有效的物体识别。即使是基于二阶矩的不变矩也只能用来识别外形相差特别大的物理,否则他们的不变矩会因为很相似而不能识别。)

在OpenCV中,还可以很方便的得到Hu不变距,Hu不变矩在图像旋转、缩放、平移等操作后,仍能保持矩的不变性,所以有时候用Hu不变距更能识别图像的特征。

不变矩的应用过程一般包括:
1)选择合适的不变矩类型;
2)选择分类器(如神经网络、最短距离等);
3)如果是神经网络分类器,则需要计算学习样例的不变矩去训练神经网络;
4)计算待识别对象的不变矩,输入神经网络就可得到待识别对象的类型,或者计算待识别对象不变矩与类别对象不变矩之间的距离,选择最短距离的类别作为待识别对象的类别。

可以看出,不变矩作用主要目的是描述事物(图像)的特征。人眼识别图像的特征往往又表现为“求和”的形式,因此不变矩是对图像元素进行了积分操作。不变矩能够描述图像整体特征就是因为它具有平移不变形、比例不变性和旋转不变性等性质。然而,另一方面图像的各阶不变矩究竟代表的什么特征很难进行直观的物理解释。

二、相关算子

1、moments()函数

特征矩的知识在概率论和数理统计中有介绍,空间矩的方法在图像应用中比较广泛,包括零阶矩求面积、一阶矩确定重心、二阶矩确定主方向、二阶矩和三阶矩可以推导出七个不变矩-Hu不变矩,不变矩具有旋转,平移、缩放等不变性,因此在工业应用和模式识别中得到广泛的应用。

该函数计算多边形或栅格化形状(一个矢量形状或光栅形状)的最高达三阶所有矩。结果在结构cv::Moments 中返回

Moments cv::moments(
InputArray array,			// 光栅图像(单通道、8位或浮点二维数组)或二维点(点或点2f)的数组(1×N或N×1)
bool binaryImage = false 	// binaryImage用来指示输出图像是否为一幅二值图像,如果是二值图像,则图像中所有非0像素看作为1进行计算。
)

结构 Moments 成员数据:

cv::Moments::Moments	
(
// 空间矩(10个)
double 	m00,double 	m10,double 	m01,double 	m20,double 	m11,double 	m02,double 	m30,double 	m21,double 	m12,double 	m03  
// 中心矩(7个)
double mu20, double mu11, double mu02, double mu30, double mu21 , double mu12,double mu03
// 中心归一化矩() 
double nu20, double nu11, double nu02, double nu30, double nu21, double nu12,double nu03;
)

opencv求质心 opencv 矩_中心矩_05


1)空间矩 Moments::mji 的计算公式:

opencv求质心 opencv 矩_二维_06


对于01二值化的图像,m00即为轮廓的面积

2)中心距 Moments::muji 计算公式:

opencv求质心 opencv 矩_二维_07


其中:(x¯,y¯)为轮廓质心

opencv求质心 opencv 矩_OpenCV_08


3)归一化的中心矩Moments::nuji计算公式为:

opencv求质心 opencv 矩_二维_09


注意:

mu00=m00, nu00=1nu10=mu10=mu01=mu10=0 , 因此不存储值。

轮廓的矩是用同样的方法定义的,但是使用格林公式计算(参见http://en.wikipedia.org/wiki/Green_theorem)。因此,由于栅格分辨率的限制,计算轮廓的矩与计算相同栅格化轮廓的矩略有不同。

由于轮廓的矩是用格林公式计算的,对于具有自交点的轮廓,你可能会得到看似奇怪的结果,例如蝴蝶形轮廓的面积(m00)。

2、示例:(绘制轮廓及其质心,计算轮廓面积和长度)

使用到的算子:
1)绘制带箭头的直线(arrowedLine) 2)OpenCV—椭圆拟合fitEllipse

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int thresh=50;
RNG rng(12345);

int main()
{
    // 1、 读取图片
    //Mat src = imread( "F:/C++/2. OPENCV 3.1.0/TEST/b.jpg", 1 );
    Mat src = imread( "F:/C++/2. OPENCV 3.1.0/TEST/arrow.PNG", 1 );
    if(!src.data ) { printf("读取图片错误,请确定目录下是否有imread函数指定图片存在~! \n"); return false; }
    imshow( "image", src );

    // 2、转灰度图
    Mat src_gray;
    cvtColor( src, src_gray, CV_BGR2GRAY );
    blur( src_gray, src_gray, Size(3,3) );

//    // 3、利用canny算法检测边缘
//    Mat canny_output;
//    Canny( src_gray, canny_output, thresh, thresh*2, 3 );
//    imshow( "canny", canny_output );

    // 4、查找轮廓
    vector<vector<Point> > contours;
    vector<Vec4i> hierarchy;
    findContours( src_gray, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, Point(0, 0) );

    // 5、计算每个轮廓所有矩
    vector<Moments> mu(contours.size());    // 创建一个vector,元素个数为contours.size()
    for( int i = 0; i < contours.size(); i++ )
    {
        mu[i] = moments(contours[i], false );   // 获得轮廓的所有最高达三阶所有矩
    }

    // 6、计算轮廓的质心
    vector<Point2f> mc( contours.size() );
    for( int i = 0; i < contours.size(); i++ )
    {
        mc[i] = Point2f(static_cast<float>(mu[i].m10/mu[i].m00), static_cast<float>(mu[i].m01/mu[i].m00));   // 质心的 X,Y 坐标:(m10/m00, m01/m00)
    }

    // 7、输出轮廓面积、轮廓长度、轮廓方向,画轮廓及其质心
    Mat drawing = Mat::zeros( src.size(), CV_8UC3 );   // 背景图
    for( size_t i = 0; i< contours.size(); i++ )
    {
        // 7.1 轮廓面积和轮廓长度,画轮廓及其质心
        Scalar color = Scalar( rng.uniform(0, 255), rng.uniform(0,255), rng.uniform(0,255) );
        drawContours( drawing, contours, i, color, -1, 8, hierarchy, 0, Point() );   // 画轮廓
        circle( drawing, mc[i], 4, color, -1, 8, 0 );                   // 画质心
        double area = mu[i].m00;                                        // 轮廓面积
        double perimeter = arcLength(contours.at(i), true);   // 轮廓周长
        printf(" * 轮廓[%d] :\n  通过轮廓空间距 (M_00)计算出的面积 = %.2f \ncontourArea(OpenCV_API)计算出的面积 = %.2f    长度 = %.2f \n", i, area, contourArea(contours[i]), perimeter );


        // 7.2 绘制轮廓方向
        // 拟合轮廓椭圆,画出
        size_t count = contours[i].size();
        if( count < 6 )	// 点数少于6 跳出循环
            continue;
        Mat pointsf;
        Mat(contours.at(i)).convertTo(pointsf, CV_32F); // 转化为点集
        RotatedRect r= fitEllipse(pointsf);
        // 长短轴比大于30跳出循环
        if( MAX(box.size.width, box.size.height) > MIN(box.size.width, box.size.height)*30 )    
            continue;
        ellipse(drawing, r, Scalar(0,0,255), 1, CV_AA);	// 两种方式画椭圆
        ellipse(cimage, box.center, box.size*0.5f, box.angle, 0, 360, Scalar(0,255,255), 1, LINE_AA);	
        // 画外界矩形        
        Point2f r[4];
        box.points(r);
        for( int j = 0; j < 4; j++ )
            line(cimage, r[j], r[(j+1)%4], Scalar(0,255,0), 1, LINE_AA);
        // 拟合椭圆的长、短轴 大小;旋转角度、直径、离心率、圆滑度
        double majorAxis = r.size.height > r.size.width ? r.size.height : r.size.width; //长轴
        double minorAxis = r.size.height > r.size.width ? r.size.width : r.size.height; //短轴
        double orientation = r.angle-90;                                     // 角度制
        double orientation_rads = orientation*CV_PI/180;                     // 转化为弧度制
      	double diameter = sqrt((4*area)/3.1416);                            // 直径
        double eccentricity = sqrt(1-pow(minorAxis/majorAxis,2));           // 离心率
        double roundness = pow(perimeter, 2)/(2*3.1416*area);               // 圆滑度
        printf(" 偏移角度 = %.2f ,直径 = %.2f,离心率 = %.2, 圆滑度 = %.2f \n\n", orientation,diameter,eccentricity,roundness);

        // 7.3 画出方向箭头
        //line(drawing, Point(mc[i].x, mc[i].y), Point(mc[i].x+30*cos(orientation_rads), mc[i].y+30*sin(orientation_rads)), cvScalar(0,0,255), 3);
        arrowedLine(drawing, Point(mc[i].x, mc[i].y), Point(mc[i].x+30*cos(orientation_rads), mc[i].y+30*sin(orientation_rads)),Scalar(0, 255, 0), 2, LINE_8, 0, 0.5);
        // 7.4 输出角度 坐标值
        char temp_text[100];
        sprintf(temp_text, "%.2f", orientation);
        putText(drawing, temp_text, Point(mc[i].x, mc[i].y), FONT_HERSHEY_SIMPLEX, 0.5, cvScalar(0,0,255),1.5);

    }
    imshow( "Contours", drawing );

    waitKey(0);
    return 0;
}

结果:

opencv求质心 opencv 矩_二维_10


opencv求质心 opencv 矩_opencv求质心_11


opencv求质心 opencv 矩_opencv求质心_12


opencv求质心 opencv 矩_OpenCV_13

2、HuMoments()函数

opencv里对Hu矩的计算有直接的API,它分为了两个函数:moments()函数用于计算中心矩,HuMoments函数用于由中心矩计算Hu矩。

void HuMoments(
const Moments& moments, // moments即为上面一个函数计算得到的moments类型。
double* hu	// hu是一个含有7个数的数组。
)
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main()
{
    Mat image = imread( "F:/C++/2. OPENCV 3.1.0/TEST/test1.png", 1 );
    if(!image.data ) { printf("读取图片错误,请确定目录下是否有imread函数指定图片存在~! \n"); return false; }
    cvtColor(image, image, CV_BGR2GRAY);
    imshow( "image", image );


    Moments mts = moments(image);   //  对整副图像 求所有 矩
    double hu[7];
    HuMoments(mts, hu);                     // 对整副图像 求 Hu几何不变矩
    for (int i=0; i<7; i++)
    {
        cout << log(abs(hu[i])) <<endl; // 取对数 (自然指数e 为底)
    }

    //test
    double x=9,y=10;
    cout<<log(x)<<endl;
    cout<<log(exp(x))<<endl;
    cout<<log10(y)<<endl;

    waitKey(0);
    return 0;
}

上面代码中,最终输出的值为log|Φi|

opencv求质心 opencv 矩_OpenCV_14


opencv求质心 opencv 矩_二维_15