现在的位置: 首页 > 综合 > 正文

图像特征检测(Image Feature Detection)

2011年01月11日 ⁄ 综合 ⁄ 共 9709字 ⁄ 字号 评论关闭

作者:王先荣
前言
    图像特征提取是计算机视觉和图像处理中的一个概念。它指的是使用计算机提取图像信息,决定每个图像的点是否属于一个图像特征。本文主要探讨如何提取图像中的“角点”这一特征,及其相关的内容。而诸如直方图、边缘、区域等内容在前文中有所提及,请查看相关文章。OpenCv(EmguCv)中实现了多种角点特征的提取方法,包括:Harris角点、ShiTomasi角点、亚像素级角点、SURF角点、Star关键点、FAST关键点、Lepetit关键点等等,本文将逐一介绍如何检测这些角点。在此之前将会先介绍跟角点检测密切相关的一些变换,包括Sobel算子、拉普拉斯算子、Canny算子、霍夫变换。另外,还会介绍一种广泛使用而OpenCv中并未实现的SIFT角点检测,以及最近在OpenCv中实现的MSER区域检测。所要讲述的内容会很多,我这里尽量写一些需要注意的地方及实现代码,而参考手册及书本中有的内容将一笔带过或者不会提及。

Sobel算子
    Sobel算子用多项式计算来拟合导数计算,可以用OpenCv中的cvSobel函数或者EmguCv中的Image<TColor,TDepth>.Sobel方法来进行计算。需要注意的是,xorder和yorder中必须且只能有一个为非零值,即只能计算x方向或者y反向的导数;如果将方形滤波器的宽度设置为特殊值CV_SCHARR(-1),将使用Scharr滤波器代替Sobel滤波器。
    使用Sobel滤波器的示例代码如下:

Sobel算子

//Sobel算子
private string SobelFeatureDetect()
{
//获取参数
int xOrder = int.Parse((string)cmbSobelXOrder.SelectedItem);
int yOrder = int.Parse((string)cmbSobelYOrder.SelectedItem);
int apertureSize = int.Parse((string)cmbSobelApertureSize.SelectedItem);
if ((xOrder == 0 && yOrder == 0) || (xOrder != 0 && yOrder != 0))
return "Sobel算子,参数错误:xOrder和yOrder中必须且只能有一个非零。\r\n";
//计算
Stopwatch sw = new Stopwatch();
sw.Start();
Image
<Gray, Single> imageDest = imageSourceGrayscale.Sobel(xOrder, yOrder, apertureSize);
sw.Stop();
//显示
pbResult.Image = imageDest.Bitmap;
//释放资源
imageDest.Dispose();
//返回
return string.Format("·Sobel算子,用时{0:F05}毫秒,参数(x方向求导阶数:{1},y方向求导阶数:{2},方形滤波器宽度:{3})\r\n", sw.Elapsed.TotalMilliseconds, xOrder, yOrder, apertureSize);
}

 
拉普拉斯算子
    拉普拉斯算子可以用作边缘检测;可以用OpenCv中的cvLaplace函数或者EmguCv中的Image<TColor,TDepth>.Laplace方法来进行拉普拉斯变换。需要注意的是:OpenCv的文档有点小错误,apertureSize参数值不能为CV_SCHARR(-1)。
    使用拉普拉斯变换的示例代码如下:

拉普拉斯算子

//拉普拉斯变换
private string LaplaceFeatureDetect()
{
//获取参数
int apertureSize = int.Parse((string)cmbLaplaceApertureSize.SelectedItem);
//计算
Stopwatch sw = new Stopwatch();
sw.Start();
Image
<Gray, Single> imageDest = imageSourceGrayscale.Laplace(apertureSize);
sw.Stop();
//显示
pbResult.Image = imageDest.Bitmap;
//释放资源
imageDest.Dispose();
//返回
return string.Format("·拉普拉斯变换,用时{0:F05}毫秒,参数(方形滤波器宽度:{1})\r\n", sw.Elapsed.TotalMilliseconds, apertureSize);
}

 
Canny算子
    Canny算子也可以用作边缘检测;可以用OpenCv中的cvCanny函数或者EmguCv中的Image<TColor,TDepth>.Canny方法来进行Canny边缘检测。所不同的是,Image<TColor,TDepth>.Canny方法可以用于检测彩色图像的边缘,但是它只能使用apertureSize参数的默认值3;
而cvCanny只能处理灰度图像,不过可以自定义apertureSize。cvCanny和Canny的方法参数名有点点不同,下面是参数对照表。
Image<TColor,TDepth>.Canny    CvInvoke.cvCanny
thresh                                         lowThresh
threshLinking                               highThresh
3                                                apertureSize
值得注意的是,apertureSize只能取3,5或者7,这可以在cvcanny.cpp第87行看到:

aperture_size &= INT_MAX;
if( (aperture_size & 1) == 0 || aperture_size < 3 || aperture_size > 7 )
CV_ERROR( CV_StsBadFlag,
"" );

使用Canny算子的示例代码如下:

Canny算子

//Canny算子
private string CannyFeatureDetect()
{
//获取参数
double lowThresh = double.Parse(txtCannyLowThresh.Text);
double highThresh = double.Parse(txtCannyHighThresh.Text);
int apertureSize = int.Parse((string)cmbCannyApertureSize.SelectedItem);
//计算
Stopwatch sw = new Stopwatch();
sw.Start();
Image
<Gray, Byte> imageDest = null;
Image
<Bgr, Byte> imageDest2 = null;
if (rbCannyUseCvCanny.Checked)
{
imageDest
= new Image<Gray, byte>(imageSourceGrayscale.Size);
CvInvoke.cvCanny(imageSourceGrayscale.Ptr, imageDest.Ptr, lowThresh, highThresh, apertureSize);
}
else
imageDest2
= imageSource.Canny(new Bgr(lowThresh, lowThresh, lowThresh), new Bgr(highThresh, highThresh, highThresh));
sw.Stop();
//显示
pbResult.Image = rbCannyUseCvCanny.Checked ? imageDest.Bitmap : imageDest2.Bitmap;
//释放资源
if (imageDest != null)
imageDest.Dispose();
if (imageDest2 != null)
imageDest2.Dispose();
//返回
return string.Format("·Canny算子,用时{0:F05}毫秒,参数(方式:{1},阀值下限:{2},阀值上限:{3},方形滤波器宽度:{4})\r\n", sw.Elapsed.TotalMilliseconds, rbCannyUseCvCanny.Checked ? "cvCanny" : "Image<TColor, TDepth>.Canny", lowThresh, highThresh, apertureSize);
}

 

另外,在http://www.china-vision.net/blog/user2/15975/archives/2007/804.html有一种自动获取Canny算子高低阀值的方法,作者提供了用C语言实现的代码。我将其改写成了C#版本,代码如下:

计算图像的自适应Canny算子阀值

/// <summary>
/// 计算图像的自适应Canny算子阀值
/// </summary>
/// <param name="imageSrc">源图像,只能是256级灰度图像</param>
/// <param name="apertureSize">方形滤波器的宽度</param>
/// <param name="lowThresh">阀值下限</param>
/// <param name="highThresh">阀值上限</param>
unsafe void AdaptiveFindCannyThreshold(Image<Gray, Byte> imageSrc, int apertureSize, out double lowThresh, out double highThresh)
{
//计算源图像x方向和y方向的1阶Sobel算子
Size size = imageSrc.Size;
Image
<Gray, Int16> imageDx = new Image<Gray, short>(size);
Image
<Gray, Int16> imageDy = new Image<Gray, short>(size);
CvInvoke.cvSobel(imageSrc.Ptr, imageDx.Ptr,
1, 0, apertureSize);
CvInvoke.cvSobel(imageSrc.Ptr, imageDy.Ptr,
0, 1, apertureSize);
Image
<Gray, Single> image = new Image<Gray, float>(size);
int i, j;
DenseHistogram hist
= null;
int hist_size = 255;
float[] range_0 = new float[] { 0, 256 };
double PercentOfPixelsNotEdges = 0.7;
//计算边缘的强度,并保存于图像中
float maxv = 0;
float temp;
byte* imageDataDx = (byte*)imageDx.MIplImage.imageData.ToPointer();
byte* imageDataDy = (byte*)imageDy.MIplImage.imageData.ToPointer();
byte* imageData = (byte*)image.MIplImage.imageData.ToPointer();
int widthStepDx = imageDx.MIplImage.widthStep;
int widthStepDy = widthStepDx;
int widthStep = image.MIplImage.widthStep;
for (i = 0; i < size.Height; i++)
{
short* _dx = (short*)(imageDataDx + widthStepDx * i);
short* _dy = (short*)(imageDataDy + widthStepDy * i);
float* _image = (float*)(imageData + widthStep * i);
for (j = 0; j < size.Width; j++)
{
temp
= (float)(Math.Abs(*(_dx + j)) + Math.Abs(*(_dy + j)));
*(_image + j) = temp;
if (maxv < temp)
maxv
= temp;
}
}
//计算直方图
range_0[1] = maxv;
hist_size
= hist_size > maxv ? (int)maxv : hist_size;
hist
= new DenseHistogram(hist_size, new RangeF(range_0[0], range_0[1]));
hist.Calculate
<Single>(new Image<Gray, Single>[] { image }, false, null);
int total = (int)(size.Height * size.Width * PercentOfPixelsNotEdges);
double sum = 0;
int icount = hist.BinDimension[0].Size;
for (i = 0; i < icount; i++)
{
sum
+= hist[i];
if (sum > total)
break;
}
//计算阀值
highThresh = (i + 1) * maxv / hist_size;
lowThresh
= highThresh * 0.4;
//释放资源
imageDx.Dispose();
imageDy.Dispose(); image.Dispose();
hist.Dispose();
}

 
霍夫变换
    霍夫变换是一种在图像中寻找直线、圆及其他简单形状的方法,在OpenCv中实现了霍夫线变换和霍夫圆变换。值得注意的地方有以下几点:(1)HoughLines2需要先计算Canny边缘,然后再检测直线;(2)HoughLines2计算结果的获取随获取方式的不同而不同;(3)HoughCircles检测结果似乎不正确。
    使用霍夫变换的示例代码如下所示:

霍夫变换

//霍夫线变换
private string HoughLinesFeatureDetect()
{
//获取参数
HOUGH_TYPE method = rbHoughLinesSHT.Checked ? HOUGH_TYPE.CV_HOUGH_STANDARD : (rbHoughLinesPPHT.Checked ? HOUGH_TYPE.CV_HOUGH_PROBABILISTIC : HOUGH_TYPE.CV_HOUGH_MULTI_SCALE);
double rho = double.Parse(txtHoughLinesRho.Text);
double theta = double.Parse(txtHoughLinesTheta.Text);
int threshold = int.Parse(txtHoughLinesThreshold.Text);
double param1 = double.Parse(txtHoughLinesParam1.Text);
double param2 = double.Parse(txtHoughLinesParam2.Text);
MemStorage storage
= new MemStorage();
int linesCount = 0;
StringBuilder sbResult
= new StringBuilder();
//计算,先运行Canny边缘检测(参数来自Canny算子属性页),然后再用计算霍夫线变换
double lowThresh = double.Parse(txtCannyLowThresh.Text);
double highThresh = double.Parse(txtCannyHighThresh.Text);
int apertureSize = int.Parse((string)cmbCannyApertureSize.SelectedItem);
Image
<Gray, Byte> imageCanny = new Image<Gray, byte>(imageSourceGrayscale.Size);
CvInvoke.cvCanny(imageSourceGrayscale.Ptr, imageCanny.Ptr, lowThresh, highThresh, apertureSize);
Stopwatch sw
= new Stopwatch();
sw.Start();
IntPtr ptrLines
= CvInvoke.cvHoughLines2(imageCanny.Ptr, storage.Ptr, method, rho, theta, threshold, param1, param2);
Seq
<LineSegment2D> linesSeq = null;
Seq
<PointF> linesSeq2 = null;
if (method == HOUGH_TYPE.CV_HOUGH_PROBABILISTIC)
linesSeq
= new Seq<LineSegment2D>(ptrLines, storage);
else
linesSeq2
= new Seq<PointF>(ptrLines, storage);
sw.Stop();
//显示
Image<Bgr, Byte> imageResult = imageSourceGrayscale.Convert<Bgr, Byte>();
if (linesSeq != null)
{
linesCount
= linesSeq.Total;
foreach (LineSegment2D line in linesSeq)
{
imageResult.Draw(line,
new Bgr(255d, 0d, 0d), 4);
sbResult.AppendFormat(
"{0}-{1},", line.P1, line.P2);
}
}
else
{
linesCount
= linesSeq2.Total;
foreach (PointF line in linesSeq2)
{
float r = line.X;
float t = line.Y;
double a = Math.Cos(t), b = Math.Sin(t);
double x0 = a * r, y0 = b * r;
int x1 = (int)(x0 + 1000 * (-b));
int y1 = (int)(y0 + 1000 * (a));
int x2 = (int)(x0 - 1000 * (-b));
int y2 = (int)(y0 - 1000 * (a));
Point pt1
= new Point(x1, y1);
Point pt2
= new Point(x2, y2);
imageResult.Draw(
new LineSegment2D(pt1, pt2), new Bgr(255d, 0d, 0d), 4);
sbResult.AppendFormat(
"{0}-{1},", pt1, pt2);
}
}
pbResult.Image
= imageResult.Bitmap;
//释放资源
imageCanny.Dispose();
imageResult.Dispose();
storage.Dispose();
//返回
return string.Format("·霍夫线变换,用时{0:F05}毫秒,参数(变换方式:{1},距离精度:{2},弧度精度:{3},阀值:{4},参数1:{5},参数2:{6}),找到{7}条直线\r\n{8}",
sw.Elapsed.TotalMilliseconds, method.ToString(
"G"), rho, theta, threshold, param1, param2, linesCount, linesCount != 0 ? (sbResult.ToString() + "\r\n") : "");
}

//霍夫圆变换
private string HoughCirclesFeatureDetect()
{
//获取参数
double dp = double.Parse(txtHoughCirclesDp.Text);
double minDist = double.Parse(txtHoughCirclesMinDist.Text);
double param1 = double.Parse(txtHoughCirclesParam1.Text);
double param2 = double.Parse(txtHoughCirclesParam2.Text);
int minRadius = int.Parse(txtHoughCirclesMinRadius.Text);
int maxRadius = int.Parse(txtHoughCirclesMaxRadius.Text);
StringBuilder sbResult
= new StringBuilder();
//计算
Stopwatch sw = new Stopwatch();
sw.Start();
CircleF[][] circles
= imageSourceGrayscale.HoughCircles(new Gray(param1), new Gray(param2), dp, minDist, minRadius, maxRadius);
sw.Stop();
//显示
Image<Bgr, Byte> imageResult = imageSourceGrayscale.Convert<Bgr, Byte>();
int circlesCount = 0;
foreach (CircleF[] cs in circles)
{
foreach (CircleF circle in cs)
{
imageResult.Draw(circle,
new Bgr(255d, 0d, 0d), 4);
sbResult.AppendFormat(
"圆心{0}半径{1},", circle.Center, circle.Radius);
circlesCount
++;
}
}
pbResult.Image
= imageResult.Bitmap;
//释放资源
imageResult.Dispose();
//返回
return string.Format("·霍夫圆变换,用时{0:F05}毫秒,参数(累加器图像的最小分辨率:{1},不同圆之间的最小距离:{2},边缘阀值:{3},累加器阀值:{4},最小圆半径:{5},最大圆半径:{6}),找到{7}个圆\r\n{8}",
sw.Elapsed.TotalMilliseconds, dp, minDist, param1, param2, minRadius, maxRadius, circlesCount, sbResult.Length
> 0 ? (sbResult.ToString() + "\r\n") : "");
}

 

Harris角点
    cvCornerHarris函数检测的结果实际上是一幅包含Harris角点的浮点型单通道图像,可以使用类似下面的代码来计算包含Harris角点的图像:

Harris角点

//Harris角点
private string CornerHarrisFeatureDetect()
{
//获取参数
int blockSize = int.Parse(txtCornerHarrisBlockSize.Text);
int apertureSize = int.Parse(txtCornerHarrisApertureSize.Text);
double k = double.Parse(txtCornerHarrisK.Text);
//计算
Image<Gray, Single> imageDest = new Image<Gray, float>(imageSourceGrayscale.Size);
Stopwatch sw

抱歉!评论已关闭.