在机器视觉处理中,我们经常要对检测到的物体的方位特征进行评估。比如说,我们要 OCR 识别一个字符串。那么这个字符串与x轴的夹角就很重要,我们需要这个信息把这个字符串转正,然后才方便识别。 条形码识别也类似,尤其是当我们条形码不是很清晰时,首先将条形码转正,然后用各向异性的滤波器处理一下,可以让条形码变得更清晰易于读取。 这里给出一种基于统计参数的特征提取方法。这个方法已经有几十年历史了,算是个老方法,但是效果很不错,所以值得写篇文章来介绍介绍。 区域的矩 一片区域 R 的矩定义为: 当p 和q 都取 0 时,得到的就是这片区域的面积。也就是: 矩还可以归一化,也就是用上面的定义再除以面积 a。 表示的是这片区域的重心。可以用它来描述区域的位置。 归一化的矩回随区域在图像中的位置不同而变化,要去除这个影响,可以用中心矩,中心矩只反映区域本身的特征。 具体的方法是将这个区域当作一个椭圆区域,那么用上面5个参量就可以计算出椭圆的长短轴和旋转角度。具体公式如下: 椭圆的这几个参数的图形解释如下图: 利用这几个参数就可以确定区域的方位和尺寸了。 比如我们有下面的一幅测试图像。 用上面方法计算出的椭圆如下: 可以看出结果非常的好。尤其是旋转角度,计算的非常准确。 下面是我的测试代码,供参考。用到了些 Qt 的功能。 #include #include #include #include #include "picturebox.h" #include QImage threshold(const QImage image, quint8 th) { int height = image.height(); int width = image.width(); QImage ret(width, height, QImage::Format_Indexed8); ret.setColorCount(256); for(int i = 0; i < 256; i++) { ret.setColor(i, qRgb(i, i, i)); } for(int i = 0; i < height; i ++) { const uchar *psrc= (uchar *)image.constScanLine(i); uchar *pDest = (uchar *)ret.scanLine(i); for( int j = 0; j < width; j ++) { pDest[j] = (pSrc[j] > th)? 255: 0; } } return ret; } QImage toGray( const QImage image ) { int height = image.height(); int width = image.width(); QImage ret(width, height, QImage::Format_Indexed8); ret.setColorCount(256); for(int i = 0; i < 256; i++) { ret.setColor(i, qRgb(i, i, i)); } qDebug () << image.format(); switch(image.format()) { case QImage::Format_Indexed8: case QImage::Format_Grayscale8: for(int i = 0; i < height; i ++) { const uchar *psrc= (uchar *)image.constScanLine(i); uchar *pDest = (uchar *)ret.scanLine(i); memcpy(pDest, pSrc, width); } break; case QImage::Format_RGB32: case QImage::Format_ARGB32: case QImage::Format_ARGB32_PremulTIplied: for(int i = 0; i < height; i ++) { const QRgb *psrc= (QRgb *)image.constScanLine(i); uchar *pDest = (uchar *)ret.scanLine(i); for( int j = 0; j < width; j ++) { pDest[j] = qGray(pSrc[j]); } } break; } return ret; } QPointF center(const QImage image, int value) { if(image.isNull() image.format() != QImage::Format_Indexed8) { return QPointF(-1, -1); } int width = image.width(); int height = image.height(); int x_mean = 0; int y_mean = 0; int count = 0; for(int j = 0; j < height; j ++) { const uchar * p = image.constScanLine(j); for(int i = 0; i < width; i++) { if( p[i] == value ) { x_mean += i; y_mean += j; count++; } } } return QPointF((double)x_mean / count, (double)y_mean / count); } struct ELLIPSE_PARA { double x_mean; //椭圆的中心坐标 x double y_mean; //椭圆的中心坐标 y double r1; //椭圆的长轴半径 double r2; //椭圆的短轴半径 double theta; //椭圆的长轴与 x 轴的夹角(逆时针) }; /** * @brief ellipseFit 将一片区域当作椭圆来估计五个几何参数 * @param image * @param value * @param para */ bool ellipseFit(const QImage image, int value, ELLIPSE_PARA * para) { if(image.isNull() image.format() != QImage::Format_Indexed8) { return false; } QPointF c = center(image, value); int width = image.width(); int height = image.height(); double n01 = c.x(); double n10 = c.y(); double mu20 = 0.0; double mu02 = 0.0; double mu11 = 0.0; int count = 0; for(int row = 0; row < height; row ++) { const uchar * p = image.constScanLine(row); for(int col = 0; col < width; col++) { if( p[col] == value ) { mu02 += (col - n01) * (col - n01); mu20 += (row - n10) * (row - n10); mu11 += (col - n01) * (row - n10); count ++; } } } if(count == 0) { return false; } mu20 /= count; mu02 /= count; mu11 /= count; double t1 = mu20 + mu02; double t2 = mu20 - mu02; double t3 = sqrt(t2 * t2 + 4 * mu11 * mu11); double r1 = sqrt(2 * ( t1 + t3) ); double r2 = sqrt(2 * ( t1 - t3) ); double theta = - atan2(2 * mu11, mu02 - mu20) / 2.0; para->r1 = r1; para->r2 = r2; para->theta = theta; para->x_mean = n01; para->y_mean = n10; return true; } int main(int argc, char *argv[]) { QApplicaTIon a(argc, argv); QImage image("D:/test55.png"); QImage imageGray = toGray(image); //imageGray = threshold(imageGray, 128); ELLIPSE_PARA para; ellipseFit(imageGray, 0, ¶); qDebug() << para.r1; qDebug() << para.r2; qDebug() << para.theta * 180 / 3.14159; QPointF c(para.x_mean, para.y_mean); qDebug() << c; QPainter painter(image); painter.setPen(Qt::red); painter.translate(c); painter.rotate(-para.theta * 180 / 3.14159); painter.drawEllipse(QPointF(0, 0), para.r1, para.r2 ); PictureBox box; box.seTImage(image); box.show(); return a.exec(); }