图像特征检测(Harris角点、ShiTomasi角点、亚像素级角点、SURF角点、Star关键点、FAST关键点、Lepetit关键点)

前言

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

Sobel算子

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

使用拉普拉斯变换的示例代码如下:

        //拉普拉斯变换
        private string LaplaceFeatureDetect()
        {
            //获取参数
            int apertureSize = int.Parse((string)cmbLaplaceApertureSize.SelectedItem);
            //计算
            Stopwatch sw = new Stopwatch();
            sw.Start();
            Image 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.Canny方法来进行Canny边缘检测。所不同的是,Image.Canny方法可以用于检测彩色图像的边缘,但是它只能使用apertureSize参数的默认值3;而cvCanny只能处理灰度图像,不过可以自定义apertureSize。cvCanny和Canny的方法参数名有点点不同,下面是参数对照表。
Image.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算子
        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 imageDest = null;
            Image imageDest2 = null;
            if (rbCannyUseCvCanny.Checked)
            {
                imageDest = new Image(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.Canny", lowThresh, highThresh, apertureSize);
        }

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

       /// 
        /// 计算图像的自适应Canny算子阀值
        /// 
        /// 源图像,只能是256级灰度图像
        /// 方形滤波器的宽度
        /// 阀值下限
        /// 阀值上限
        unsafe void AdaptiveFindCannyThreshold(Image imageSrc, int apertureSize, out double lowThresh, out double highThresh)
        {
            //计算源图像x方向和y方向的1阶Sobel算子
            Size size = imageSrc.Size;
            Image imageDx = new Image(size);
            Image imageDy = new Image(size);
            CvInvoke.cvSobel(imageSrc.Ptr, imageDx.Ptr, 1, 0, apertureSize);
            CvInvoke.cvSobel(imageSrc.Ptr, imageDy.Ptr, 0, 1, apertureSize);
            Image image = new Image(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(new Image[] { 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 imageCanny = new Image(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 linesSeq = null;
            Seq linesSeq2 = null;
            if (method == HOUGH_TYPE.CV_HOUGH_PROBABILISTIC)
                linesSeq = new Seq(ptrLines, storage);
            else
                linesSeq2 = new Seq(ptrLines, storage);
            sw.Stop();
            //显示
            Image imageResult = imageSourceGrayscale.Convert();
            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 imageResult = imageSourceGrayscale.Convert();
            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角点
        private string CornerHarrisFeatureDetect()
        {
            //获取参数
            int blockSize = int.Parse(txtCornerHarrisBlockSize.Text);
            int apertureSize = int.Parse(txtCornerHarrisApertureSize.Text);
            double k = double.Parse(txtCornerHarrisK.Text);
            //计算
            Image imageDest = new Image(imageSourceGrayscale.Size);
            Stopwatch sw = new Stopwatch();
            sw.Start();
            CvInvoke.cvCornerHarris(imageSourceGrayscale.Ptr, imageDest.Ptr, blockSize, apertureSize, k);
            sw.Stop();
            //显示
            pbResult.Image = imageDest.Bitmap;
            //释放资源
            imageDest.Dispose();
            //返回
            return string.Format("·Harris角点,用时{0:F05}毫秒,参数(邻域大小:{1},方形滤波器宽度:{2},权重系数:{3})\r\n", sw.Elapsed.TotalMilliseconds, blockSize, apertureSize, k);
        }

 如果要计算Harris角点列表,需要使用cvGoodFeatureToTrack函数,并传递适当的参数。

 

ShiTomasi角点

在默认情况下,cvGoodFeatureToTrack函数计算ShiTomasi角点;不过如果将参数use_harris设置为非0值,那么它会计算harris角点。
      使用cvGoodFeatureToTrack函数的示例代码如下:

        //ShiTomasi角点
        private string CornerShiTomasiFeatureDetect()
        {
            //获取参数
            int cornerCount = int.Parse(txtGoodFeaturesCornerCount.Text);
            double qualityLevel = double.Parse(txtGoodFeaturesQualityLevel.Text);
            double minDistance = double.Parse(txtGoodFeaturesMinDistance.Text);
            int blockSize = int.Parse(txtGoodFeaturesBlockSize.Text);
            bool useHarris = cbGoodFeaturesUseHarris.Checked;
            double k = double.Parse(txtGoodFeaturesK.Text);
            //计算
            Stopwatch sw = new Stopwatch();
            sw.Start();
            PointF[][] corners = imageSourceGrayscale.GoodFeaturesToTrack(cornerCount, qualityLevel, minDistance, blockSize, useHarris, k);
            sw.Stop();
            //显示
            Image imageResult = imageSourceGrayscale.Convert();
            int cornerCount2 = 0;
            StringBuilder sbResult = new StringBuilder();
            int radius = (int)(minDistance / 2) + 1;
            int thickness = (int)(minDistance / 4) + 1;
            foreach (PointF[] cs in corners)
            {
                foreach (PointF p in cs)
                {
                    imageResult.Draw(new CircleF(p, radius), new Bgr(255d, 0d, 0d), thickness);
                    cornerCount2++;
                    sbResult.AppendFormat("{0},", p);
                }
            }
            pbResult.Image = imageResult.Bitmap;
            //释放资源
            imageResult.Dispose();
            //返回
            return string.Format("·ShiTomasi角点,用时{0:F05}毫秒,参数(最大角点数目:{1},最小特征值:{2},角点间的最小距离:{3},邻域大小:{4},角点类型:{5},权重系数:{6}),检测到{7}个角点\r\n{8}",
                sw.Elapsed.TotalMilliseconds, cornerCount, qualityLevel, minDistance, blockSize, useHarris ? "Harris" : "ShiTomasi", k, cornerCount2, cornerCount2 > 0 ? (sbResult.ToString() + "\r\n") : "");
        }

亚像素级角点

在检测亚像素级角点前,需要提供角点的初始为止,这些初始位置可以用本文给出的其他的角点检测方式来获取,不过使用GoodFeaturesToTrack得到的结果最方便直接使用。
    亚像素级角点检测的示例代码如下:

       //亚像素级角点
        private string CornerSubPixFeatureDetect()
        {
            //获取参数
            int winWidth = int.Parse(txtCornerSubPixWinWidth.Text);
            int winHeight = int.Parse(txtCornerSubPixWinHeight.Text);
            Size win = new Size(winWidth, winHeight);
            int zeroZoneWidth = int.Parse(txtCornerSubPixZeroZoneWidth.Text);
            int zeroZoneHeight = int.Parse(txtCornerSubPixZeroZoneHeight.Text);
            Size zeroZone = new Size(zeroZoneWidth, zeroZoneHeight);
            int maxIter=int.Parse(txtCornerSubPixMaxIter.Text);
            double epsilon=double.Parse(txtCornerSubPixEpsilon.Text);
            MCvTermCriteria criteria = new MCvTermCriteria(maxIter, epsilon);
            //先计算得到易于跟踪的点(ShiTomasi角点)
            int cornerCount = int.Parse(txtGoodFeaturesCornerCount.Text);
            double qualityLevel = double.Parse(txtGoodFeaturesQualityLevel.Text);
            double minDistance = double.Parse(txtGoodFeaturesMinDistance.Text);
            int blockSize = int.Parse(txtGoodFeaturesBlockSize.Text);
            bool useHarris = cbGoodFeaturesUseHarris.Checked;
            double k = double.Parse(txtGoodFeaturesK.Text);
            PointF[][] corners = imageSourceGrayscale.GoodFeaturesToTrack(cornerCount, qualityLevel, minDistance, blockSize, useHarris, k);
            //计算
            Stopwatch sw = new Stopwatch();
            sw.Start();
            imageSourceGrayscale.FindCornerSubPix(corners, win, zeroZone, criteria);
            sw.Stop();
            //显示
            Image imageResult = imageSourceGrayscale.Convert();
            int cornerCount2 = 0;
            StringBuilder sbResult = new StringBuilder();
            int radius = (int)(minDistance / 2) + 1;
            int thickness = (int)(minDistance / 4) + 1;
            foreach (PointF[] cs in corners)
            {
                foreach (PointF p in cs)
                {
                    imageResult.Draw(new CircleF(p, radius), new Bgr(255d, 0d, 0d), thickness);
                    cornerCount2++;
                    sbResult.AppendFormat("{0},", p);
                }
            }
            pbResult.Image = imageResult.Bitmap;
            //释放资源
            imageResult.Dispose();
            //返回
            return string.Format("·亚像素级角点,用时{0:F05}毫秒,参数(搜索窗口:{1},死区:{2},最大迭代次数:{3},亚像素值的精度:{4}),检测到{5}个角点\r\n{6}",
                sw.Elapsed.TotalMilliseconds, win, zeroZone, maxIter, epsilon, cornerCount2, cornerCount2 > 0 ? (sbResult.ToString() + "\r\n") : "");
        }

SURF角点

OpenCv中的cvExtractSURF函数和EmguCv中的Image.ExtractSURF方法用于检测SURF角点。
    SURF角点检测的示例代码如下:

        //SURF角点
        private string SurfFeatureDetect()
        {
            //获取参数
            bool getDescriptors = cbSurfGetDescriptors.Checked;
            MCvSURFParams surfParam = new MCvSURFParams();
            surfParam.extended=rbSurfBasicDescriptor.Checked ? 0 : 1;
            surfParam.hessianThreshold=double.Parse(txtSurfHessianThreshold.Text);
            surfParam.nOctaves=int.Parse(txtSurfNumberOfOctaves.Text);
            surfParam.nOctaveLayers=int.Parse(txtSurfNumberOfOctaveLayers.Text);
            //计算
            SURFFeature[] features = null;
            MKeyPoint[] keyPoints = null;
            Stopwatch sw = new Stopwatch();
            sw.Start();
            if (getDescriptors)
                features = imageSourceGrayscale.ExtractSURF(ref surfParam);
            else
                keyPoints = surfParam.DetectKeyPoints(imageSourceGrayscale, null);
            sw.Stop();
            //显示
            bool showDetail = cbSurfShowDetail.Checked;
            Image imageResult = imageSourceGrayscale.Convert();
            StringBuilder sbResult = new StringBuilder();
            int idx = 0;
            if (getDescriptors)
            {
                foreach (SURFFeature feature in features)
                {
                    imageResult.Draw(new CircleF(feature.Point.pt, 5), new Bgr(255d, 0d, 0d), 2);
                    if (showDetail)
                    {
                        sbResult.AppendFormat("第{0}点(坐标:{1},尺寸:{2},方向:{3}°,hessian值:{4},拉普拉斯标志:{5},描述:[",
                            idx, feature.Point.pt, feature.Point.size, feature.Point.dir, feature.Point.hessian, feature.Point.laplacian);
                        foreach (float d in feature.Descriptor)
                            sbResult.AppendFormat("{0},", d);
                        sbResult.Append("]),");
                    }
                    idx++;
                }
            }
            else
            {
                foreach (MKeyPoint keypoint in keyPoints)
                {
                    imageResult.Draw(new CircleF(keypoint.Point, 5), new Bgr(255d, 0d, 0d), 2);
                    if (showDetail)
                        sbResult.AppendFormat("第{0}点(坐标:{1},尺寸:{2},方向:{3}°,响应:{4},octave:{5}),",
                            idx, keypoint.Point, keypoint.Size, keypoint.Angle, keypoint.Response, keypoint.Octave);
                    idx++;
                }
            }
            pbResult.Image = imageResult.Bitmap;
            //释放资源
            imageResult.Dispose();
            //返回
            return string.Format("·SURF角点,用时{0:F05}毫秒,参数(描述:{1},hessian阀值:{2},octave数目:{3},每个octave的层数:{4},检测到{5}个角点\r\n{6}",
                sw.Elapsed.TotalMilliseconds, getDescriptors ? (surfParam.extended == 0 ? "获取基本描述" : "获取扩展描述") : "不获取描述", surfParam.hessianThreshold,
                surfParam.nOctaves, surfParam.nOctaveLayers, getDescriptors ? features.Length : keyPoints.Length, showDetail ? sbResult.ToString() + "\r\n" : "");
        }

Star关键点

OpenCv中的cvGetStarKeypoints函数和EmguCv中的Image.GetStarKeypoints方法用于检测“星型”附近的点。
    Star关键点检测的示例代码如下:

        //Star关键点
        private string StarKeyPointFeatureDetect()
        {
            //获取参数
            StarDetector starParam = new StarDetector();
            starParam.MaxSize = int.Parse((string)cmbStarMaxSize.SelectedItem);
            starParam.ResponseThreshold = int.Parse(txtStarResponseThreshold.Text);
            starParam.LineThresholdProjected = int.Parse(txtStarLineThresholdProjected.Text);
            starParam.LineThresholdBinarized = int.Parse(txtStarLineThresholdBinarized.Text);
            starParam.SuppressNonmaxSize = int.Parse(txtStarSuppressNonmaxSize.Text);
            //计算
            Stopwatch sw = new Stopwatch();
            sw.Start();
            MCvStarKeypoint[] keyPoints = imageSourceGrayscale.GetStarKeypoints(ref starParam);
            sw.Stop();
            //显示
            Image imageResult = imageSourceGrayscale.Convert();
            StringBuilder sbResult = new StringBuilder();
            int idx = 0;
            foreach (MCvStarKeypoint keypoint in keyPoints)
            {
                imageResult.Draw(new CircleF(new PointF(keypoint.pt.X, keypoint.pt.Y), keypoint.size / 2), new Bgr(255d, 0d, 0d), keypoint.size / 4);
                sbResult.AppendFormat("第{0}点(坐标:{1},尺寸:{2},强度:{3}),", idx, keypoint.pt, keypoint.size, keypoint.response);
                idx++;
            }
            pbResult.Image = imageResult.Bitmap;
            //释放资源
            imageResult.Dispose();
            //返回
            return string.Format("·Star关键点,用时{0:F05}毫秒,参数(MaxSize:{1},ResponseThreshold:{2},LineThresholdProjected:{3},LineThresholdBinarized:{4},SuppressNonmaxSize:{5}),检测到{6}个关键点\r\n{7}",
                sw.Elapsed.TotalMilliseconds, starParam.MaxSize, starParam.ResponseThreshold, starParam.LineThresholdProjected, starParam.LineThresholdBinarized, starParam.SuppressNonmaxSize, keyPoints.Length, keyPoints.Length > 0 ? (sbResult.ToString() + "\r\n") : "");
        }

FAST角点检测

FAST角点由E. Rosten教授提出,相比其他检测手段,这种方法的速度正如其名,相当的快。值得关注的是他所研究的理论都是属于实用类的,都很快。Rosten教授实现了FAST角点检测,并将其提供给了OpenCv。

使用FAST角点检测的示例代码如下:

        //FAST关键点
        private string FASTKeyPointFeatureDetect()
        {
            //获取参数
            int threshold = int.Parse(txtFASTThreshold.Text);
            bool nonmaxSuppression = cbFASTNonmaxSuppression.Checked;
            bool showDetail = cbFASTShowDetail.Checked;
            //计算
            Stopwatch sw = new Stopwatch();
            sw.Start();
            MKeyPoint[] keyPoints = imageSourceGrayscale.GetFASTKeypoints(threshold, nonmaxSuppression);
            sw.Stop();
            //显示
            Image imageResult = imageSourceGrayscale.Convert();
            StringBuilder sbResult = new StringBuilder();
            int idx = 0;
            foreach (MKeyPoint keypoint in keyPoints)
            {
                imageResult.Draw(new CircleF(keypoint.Point, (int)(keypoint.Size / 2)), new Bgr(255d, 0d, 0d), (int)(keypoint.Size / 4));
                if (showDetail)
                    sbResult.AppendFormat("第{0}点(坐标:{1},尺寸:{2},方向:{3}°,响应:{4},octave:{5}),",
                                idx, keypoint.Point, keypoint.Size, keypoint.Angle, keypoint.Response, keypoint.Octave);
                idx++;
            }
            pbResult.Image = imageResult.Bitmap;
            //释放资源
            imageResult.Dispose();
            //返回
            return string.Format("·FAST关键点,用时{0:F05}毫秒,参数(阀值:{1},nonmaxSupression:{2}),检测到{3}个关键点\r\n{4}",
                sw.Elapsed.TotalMilliseconds, threshold, nonmaxSuppression, keyPoints.Length, showDetail ? (sbResult.ToString() + "\r\n") : "");
        }

Lepetit关键点

Lepetit关键点由Vincent Lepetit提出,可以在他的网站(http://cvlab.epfl.ch/~vlepetit/)上看到相关的论文等资料。EmguCv中的类LDetector实现了Lepetit关键点的检测。
    使用Lepetit关键点检测的示例代码如下:

        //Lepetit关键点
        private string LepetitKeyPointFeatureDetect()
        {
            //获取参数
            LDetector lepetitDetector = new LDetector();
            lepetitDetector.BaseFeatureSize = double.Parse(txtLepetitBaseFeatureSize.Text);
            lepetitDetector.ClusteringDistance = double.Parse(txtLepetitClasteringDistance.Text);
            lepetitDetector.NOctaves = int.Parse(txtLepetitNumberOfOctaves.Text);
            lepetitDetector.NViews = int.Parse(txtLepetitNumberOfViews.Text);
            lepetitDetector.Radius = int.Parse(txtLepetitRadius.Text);
            lepetitDetector.Threshold = int.Parse(txtLepetitThreshold.Text);
            lepetitDetector.Verbose = cbLepetitVerbose.Checked;
            int maxCount = int.Parse(txtLepetitMaxCount.Text);
            bool scaleCoords = cbLepetitScaleCoords.Checked;
            bool showDetail = cbLepetitShowDetail.Checked;
            //计算
            Stopwatch sw = new Stopwatch();
            sw.Start();
            MKeyPoint[] keyPoints = lepetitDetector.DetectKeyPoints(imageSourceGrayscale, maxCount, scaleCoords);
            sw.Stop();
            //显示
            Image imageResult = imageSourceGrayscale.Convert();
            StringBuilder sbResult = new StringBuilder();
            int idx = 0;
            foreach (MKeyPoint keypoint in keyPoints)
            {
                //imageResult.Draw(new CircleF(keypoint.Point, (int)(keypoint.Size / 2)), new Bgr(255d, 0d, 0d), (int)(keypoint.Size / 4));
                imageResult.Draw(new CircleF(keypoint.Point, 4), new Bgr(255d, 0d, 0d), 2);
                if (showDetail)
                    sbResult.AppendFormat("第{0}点(坐标:{1},尺寸:{2},方向:{3}°,响应:{4},octave:{5}),",
                                idx, keypoint.Point, keypoint.Size, keypoint.Angle, keypoint.Response, keypoint.Octave);
                idx++;
            }
            pbResult.Image = imageResult.Bitmap;
            //释放资源
            imageResult.Dispose();
            //返回
            return string.Format("·Lepetit关键点,用时{0:F05}毫秒,参数(基础特征尺寸:{1},集群距离:{2},阶数:{3},视图数:{4},半径:{5},阀值:{6},计算详细结果:{7},最大关键点数目:{8},缩放坐标:{9}),检测到{10}个关键点\r\n{11}",
                sw.Elapsed.TotalMilliseconds, lepetitDetector.BaseFeatureSize, lepetitDetector.ClusteringDistance, lepetitDetector.NOctaves, lepetitDetector.NViews,
                lepetitDetector.Radius, lepetitDetector.Threshold, lepetitDetector.Verbose, maxCount, scaleCoords, keyPoints.Length, showDetail ? (sbResult.ToString() + "\r\n") : "");
        }

SIFT角点

SIFT角点是一种广泛使用的图像特征,可用于物体跟踪、图像匹配、图像拼接等领域,然而奇怪的是它并未被OpenCv实现。提出SIFT角点的David Lowe教授已经用C和matlab实现了SIFT角点的检测,并开放了源代码,不过他的实现不方便直接使用。您可以在Keypoint detector看到SIFT的介绍、相关论文及David Lowe教授的实现代码。下面我要介绍由Andrea Vedaldi和Brian Fulkerson先生创建的vlfeat开源图像处理库,vlfeat库有C和matlab两种实现,其中包含了SIFT检测。您可以在VLFeat - Home下载到vlfeat库的代码、文档及可执行文件。
    使用vlfeat检测SIFT角点需要以下步骤:
    (1)用函数vl_sift_new()初始化SIFT过滤器对象,该过滤器对象可以反复用于多幅尺寸相同的图像;
    (2)用函数vl_sift_first_octave()及vl_sift_process_next()遍历缩放空间的每一阶,直到返回VL_ERR_EOF为止;
    (3)对于缩放空间的每一阶,用函数vl_sift_detect()来获取关键点;
    (4)对每个关键点,用函数vl_sift_calc_keypoint_orientations()来获取该点的方向;
    (5)对关键点的每个方向,用函数vl_sift_calc_keypoint_descriptor()来获取该方向的描述;
    (6)使用完之后,用函数vl_sift_delete()来释放资源;
    (7)如果要计算某个自定义关键点的描述,可以使用函数vl_sift_calc_raw_descriptor()。
    直接使用vlfeat中的SIFT角点检测示例代码如下:

        //通过P/Invoke调用vlfeat函数来进行SIFT检测
        unsafe private string SiftFeatureDetectByPinvoke(int noctaves, int nlevels, int o_min, bool showDetail)
        {
            StringBuilder sbResult = new StringBuilder();
            //初始化
            IntPtr ptrSiftFilt = VlFeatInvoke.vl_sift_new(imageSource.Width, imageSource.Height, noctaves, nlevels, o_min);
            if (ptrSiftFilt == IntPtr.Zero)
                return "Sift特征检测:初始化失败。";
            //处理
            Image imageSourceSingle = imageSourceGrayscale.ConvertScale(1d, 0d);
            Image imageResult = imageSourceGrayscale.Convert();
            int pointCount = 0;
            int idx = 0;
            //依次遍历每一组
            if (VlFeatInvoke.vl_sift_process_first_octave(ptrSiftFilt, imageSourceSingle.MIplImage.imageData) != VlFeatInvoke.VL_ERR_EOF)
            {
                while (true)
                {
                    //计算每组中的关键点
                    VlFeatInvoke.vl_sift_detect(ptrSiftFilt);
                    //遍历并绘制每个点
                    VlSiftFilt siftFilt = (VlSiftFilt)Marshal.PtrToStructure(ptrSiftFilt, typeof(VlSiftFilt));
                    pointCount += siftFilt.nkeys;
                    VlSiftKeypoint* pKeyPoints = (VlSiftKeypoint*)siftFilt.keys.ToPointer();
                    for (int i = 0; i < siftFilt.nkeys; i++)
                    {
                        VlSiftKeypoint keyPoint = *pKeyPoints;
                        pKeyPoints++;
                        imageResult.Draw(new CircleF(new PointF(keyPoint.x, keyPoint.y), keyPoint.sigma / 2), new Bgr(255d, 0d, 0d), 2);
                        if (showDetail)
                            sbResult.AppendFormat("第{0}点,坐标:({1},{2}),阶:{3},缩放:{4},s:{5},", idx, keyPoint.x, keyPoint.y, keyPoint.o, keyPoint.sigma, keyPoint.s);
                        idx++;
                        //计算并遍历每个点的方向
                        double[] angles = new double[4];
                        int angleCount = VlFeatInvoke.vl_sift_calc_keypoint_orientations(ptrSiftFilt, angles, ref keyPoint);
                        if (showDetail)
                            sbResult.AppendFormat("共{0}个方向,", angleCount);
                        for (int j = 0; j < angleCount; j++)
                        {
                            double angle = angles[j];
                            if (showDetail)
                                sbResult.AppendFormat("【方向:{0},描述:", angle);
                            //计算每个方向的描述
                            IntPtr ptrDescriptors = Marshal.AllocHGlobal(128 * sizeof(float));
                            VlFeatInvoke.vl_sift_calc_keypoint_descriptor(ptrSiftFilt, ptrDescriptors, ref keyPoint, angle);
                            float* pDescriptors = (float*)ptrDescriptors.ToPointer();
                            for (int k = 0; k < 128; k++)
                            {
                                float descriptor = *pDescriptors;
                                pDescriptors++;
                                if (showDetail)
                                    sbResult.AppendFormat("{0},", descriptor);
                            }
                            sbResult.Append("】,");
                            Marshal.FreeHGlobal(ptrDescriptors);
                        }
                    }
                    //下一阶
                    if (VlFeatInvoke.vl_sift_process_next_octave(ptrSiftFilt) == VlFeatInvoke.VL_ERR_EOF)
                        break;
                }
            }
            //显示
            pbResult.Image = imageResult.Bitmap;
            //释放资源
            VlFeatInvoke.vl_sift_delete(ptrSiftFilt);
            imageSourceSingle.Dispose();
            imageResult.Dispose();
            //返回
            return string.Format("·SIFT特征检测(P/Invoke),用时:未统计,参数(阶数:{0},每阶层数:{1},最小阶索引:{2}),{3}个关键点\r\n{4}",
                noctaves, nlevels, o_min, pointCount, showDetail ? (sbResult.ToString() + "\r\n") : "");
        }

 要在.net中使用vlfeat还是不够方便,为此我对vlfeat中的SIFT角点检测部分进行了封装,将相关操作放到了类SiftDetector中。
    使用SiftDetector需要两至三步:
    (1)用构造函数初始化SiftDetector对象;
    (2)用Process方法计算特征;
    (3)视需要调用Dispose方法释放资源,或者等待垃圾回收器来自动释放资源。
    使用SiftDetector的示例代码如下:

        //通过dotnet封装的SiftDetector类来进行SIFT检测
        private string SiftFeatureDetectByDotNet(int noctaves, int nlevels, int o_min, bool showDetail)
        {
            //初始化对象
            SiftDetector siftDetector = new SiftDetector(imageSource.Size, noctaves, nlevels, o_min);
            //计算
            Image imageSourceSingle = imageSourceGrayscale.Convert();
            Stopwatch sw = new Stopwatch();
            sw.Start();
            List features = siftDetector.Process(imageSourceSingle, showDetail ? SiftDetectorResultType.Extended : SiftDetectorResultType.Basic);
            sw.Stop();
            //显示结果
            Image imageResult = imageSourceGrayscale.Convert();
            StringBuilder sbResult = new StringBuilder();
            int idx=0;
            foreach (SiftFeature feature in features)
            {
                imageResult.Draw(new CircleF(new PointF(feature.keypoint.x, feature.keypoint.y), feature.keypoint.sigma / 2), new Bgr(255d, 0d, 0d), 2);
                if (showDetail)
                {
                    sbResult.AppendFormat("第{0}点,坐标:({1},{2}),阶:{3},缩放:{4},s:{5},",
                        idx, feature.keypoint.x, feature.keypoint.y, feature.keypoint.o, feature.keypoint.sigma, feature.keypoint.s);
                    sbResult.AppendFormat("共{0}个方向,", feature.keypointOrientations != null ? feature.keypointOrientations.Length : 0);
                    if (feature.keypointOrientations != null)
                    {
                        foreach (SiftKeyPointOrientation orientation in feature.keypointOrientations)
                        {
                            if (orientation.descriptors != null)
                            {
                                sbResult.AppendFormat("【方向:{0},描述:", orientation.angle);
                                foreach (float descriptor in orientation.descriptors)
                                    sbResult.AppendFormat("{0},", descriptor);
                            }
                            else
                                sbResult.AppendFormat("【方向:{0},", orientation.angle);
                            sbResult.Append("】,");
                        }
                    }
                }
            }
            pbResult.Image = imageResult.Bitmap;
            //释放资源
            siftDetector.Dispose();
            imageSourceSingle.Dispose();
            imageResult.Dispose();
            //返回
            return string.Format("·SIFT特征检测(.net),用时:{0:F05}毫秒,参数(阶数:{1},每阶层数:{2},最小阶索引:{3}),{4}个关键点\r\n{5}",
                sw.Elapsed.TotalMilliseconds, noctaves, nlevels, o_min, features.Count, showDetail ? (sbResult.ToString() + "\r\n") : "");
        }

   对vlfeat库中的SIFT部分封装代码如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace ImageProcessLearn
{
    [StructLayoutAttribute(LayoutKind.Sequential)]
    public struct VlSiftKeypoint
    {

        /// int
        public int o;

        /// int
        public int ix;

        /// int
        public int iy;

        /// int
        public int @is;

        /// float
        public float x;

        /// float
        public float y;

        /// float
        public float s;

        /// float
        public float sigma;
    }

    [StructLayoutAttribute(LayoutKind.Sequential)]
    public struct VlSiftFilt
    {

        /// double
        public double sigman;

        /// double
        public double sigma0;

        /// double
        public double sigmak;

        /// double
        public double dsigma0;

        /// int
        public int width;

        /// int
        public int height;

        /// int
        public int O;

        /// int
        public int S;

        /// int
        public int o_min;

        /// int
        public int s_min;

        /// int
        public int s_max;

        /// int
        public int o_cur;

        /// vl_sift_pix*
        public System.IntPtr temp;

        /// vl_sift_pix*
        public System.IntPtr octave;

        /// vl_sift_pix*
        public System.IntPtr dog;

        /// int
        public int octave_width;

        /// int
        public int octave_height;

        /// VlSiftKeypoint*
        public System.IntPtr keys;

        /// int
        public int nkeys;

        /// int
        public int keys_res;

        /// double
        public double peak_thresh;

        /// double
        public double edge_thresh;

        /// double
        public double norm_thresh;

        /// double
        public double magnif;

        /// double
        public double windowSize;

        /// vl_sift_pix*
        public System.IntPtr grad;

        /// int
        public int grad_o;

        /// 
        /// 获取SiftFilt指针;
        /// 注意在使用完指针之后,需要用Marshal.FreeHGlobal释放内存。
        /// 
        /// 
        unsafe public IntPtr GetPtrOfVlSiftFilt()
        {
            IntPtr ptrSiftFilt = Marshal.AllocHGlobal(sizeof(VlSiftFilt));
            Marshal.StructureToPtr(this, ptrSiftFilt, true);
            return ptrSiftFilt;
        }
    }

    public class VlFeatInvoke
    {
        /// VL_ERR_MSG_LEN -> 1024
        public const int VL_ERR_MSG_LEN = 1024;

        /// VL_ERR_OK -> 0
        public const int VL_ERR_OK = 0;

        /// VL_ERR_OVERFLOW -> 1
        public const int VL_ERR_OVERFLOW = 1;

        /// VL_ERR_ALLOC -> 2
        public const int VL_ERR_ALLOC = 2;

        /// VL_ERR_BAD_ARG -> 3
        public const int VL_ERR_BAD_ARG = 3;

        /// VL_ERR_IO -> 4
        public const int VL_ERR_IO = 4;

        /// VL_ERR_EOF -> 5
        public const int VL_ERR_EOF = 5;

        /// VL_ERR_NO_MORE -> 5
        public const int VL_ERR_NO_MORE = 5;

        /// Return Type: VlSiftFilt*
        ///width: int
        ///height: int
        ///noctaves: int
        ///nlevels: int
        ///o_min: int
        [DllImportAttribute("vl.dll", EntryPoint = "vl_sift_new")]
        public static extern System.IntPtr vl_sift_new(int width, int height, int noctaves, int nlevels, int o_min);


        /// Return Type: void
        ///f: VlSiftFilt*
        [DllImportAttribute("vl.dll", EntryPoint = "vl_sift_delete")]
        public static extern void vl_sift_delete(IntPtr f);


        /// Return Type: int
        ///f: VlSiftFilt*
        ///im: vl_sift_pix*
        [DllImportAttribute("vl.dll", EntryPoint = "vl_sift_process_first_octave")]
        public static extern int vl_sift_process_first_octave(IntPtr f, IntPtr im);


        /// Return Type: int
        ///f: VlSiftFilt*
        [DllImportAttribute("vl.dll", EntryPoint = "vl_sift_process_next_octave")]
        public static extern int vl_sift_process_next_octave(IntPtr f);


        /// Return Type: void
        ///f: VlSiftFilt*
        [DllImportAttribute("vl.dll", EntryPoint = "vl_sift_detect")]
        public static extern void vl_sift_detect(IntPtr f);


        /// Return Type: int
        ///f: VlSiftFilt*
        ///angles: double*
        ///k: VlSiftKeypoint*
        [DllImportAttribute("vl.dll", EntryPoint = "vl_sift_calc_keypoint_orientations")]
        public static extern int vl_sift_calc_keypoint_orientations(IntPtr f, double[] angles, ref VlSiftKeypoint k);


        /// Return Type: void
        ///f: VlSiftFilt*
        ///descr: vl_sift_pix*
        ///k: VlSiftKeypoint*
        ///angle: double
        [DllImportAttribute("vl.dll", EntryPoint = "vl_sift_calc_keypoint_descriptor")]
        public static extern void vl_sift_calc_keypoint_descriptor(IntPtr f, IntPtr descr, ref VlSiftKeypoint k, double angle);


        /// Return Type: void
        ///f: VlSiftFilt*
        ///image: vl_sift_pix*
        ///descr: vl_sift_pix*
        ///widht: int
        ///height: int
        ///x: double
        ///y: double
        ///s: double
        ///angle0: double
        [DllImportAttribute("vl.dll", EntryPoint = "vl_sift_calc_raw_descriptor")]
        public static extern void vl_sift_calc_raw_descriptor(IntPtr f, IntPtr image, IntPtr descr, int widht, int height, double x, double y, double s, double angle0);


        /// Return Type: void
        ///f: VlSiftFilt*
        ///k: VlSiftKeypoint*
        ///x: double
        ///y: double
        ///sigma: double
        [DllImportAttribute("vl.dll", EntryPoint = "vl_sift_keypoint_init")]
        public static extern void vl_sift_keypoint_init(IntPtr f, ref VlSiftKeypoint k, double x, double y, double sigma);
    }
}
    SiftDetector类的实现代码如下所示:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Runtime.InteropServices;
using Emgu.CV;
using Emgu.CV.Structure;

namespace ImageProcessLearn
{
    /// 
    /// SIFT检测器
    /// 
    public class SiftDetector : IDisposable
    {
        //成员变量
        private IntPtr ptrSiftFilt;

        //属性
        /// 
        /// SiftFilt指针
        /// 
        public IntPtr PtrSiftFilt
        {
            get
            {
                return ptrSiftFilt;
            }
        }

        /// 
        /// 获取SIFT检测器中的SiftFilt
        /// 
        public VlSiftFilt SiftFilt
        {
            get
            {
                return (VlSiftFilt)Marshal.PtrToStructure(ptrSiftFilt, typeof(VlSiftFilt));
            }
        }

        /// 
        /// 构造函数
        /// 
        /// 图像的宽度
        /// 图像的高度
        /// 阶数
        /// 每一阶的层数
        /// 最小阶的索引
        public SiftDetector(int width, int height, int noctaves, int nlevels, int o_min)
        {
            ptrSiftFilt = VlFeatInvoke.vl_sift_new(width, height, noctaves, nlevels, o_min);
        }
        public SiftDetector(int width, int height)
            : this(width, height, 4, 2, 0)
        { }
        public SiftDetector(Size size, int noctaves, int nlevels, int o_min)
            : this(size.Width, size.Height, noctaves, nlevels, o_min)
        { }
        public SiftDetector(Size size)
            : this(size.Width, size.Height, 4, 2, 0)
        { }

        /// 
        /// 进行SIFT检测,并返回检测的结果
        /// 
        /// 单通道浮点型图像数据,图像数据不必归一化到区间[0,1]
        /// SIFT检测的结果类型
        /// 返回SIFT检测结果——SIFT特征列表;如果检测失败,返回null。
        unsafe public List Process(IntPtr im, SiftDetectorResultType resultType)
        {
            //定义变量
            List features = null;      //检测结果:SIFT特征列表
            VlSiftFilt siftFilt;                    //
            VlSiftKeypoint* pKeyPoints;             //指向关键点的指针
            VlSiftKeypoint keyPoint;                //关键点
            SiftKeyPointOrientation[] orientations; //关键点对应的方向及描述
            double[] angles = new double[4];        //关键点对应的方向(角度)
            int angleCount;                         //某个关键点的方向数目
            double angle;                           //方向
            float[] descriptors;                    //关键点某个方向的描述
            IntPtr ptrDescriptors = Marshal.AllocHGlobal(128 * sizeof(float));  //指向描述的缓冲区指针
            //依次遍历每一阶
            if (VlFeatInvoke.vl_sift_process_first_octave(ptrSiftFilt, im) != VlFeatInvoke.VL_ERR_EOF)
            {
                features = new List(100);
                while (true)
                {
                    //计算每组中的关键点
                    VlFeatInvoke.vl_sift_detect(ptrSiftFilt);
                    //遍历每个点
                    siftFilt = (VlSiftFilt)Marshal.PtrToStructure(ptrSiftFilt, typeof(VlSiftFilt));
                    pKeyPoints = (VlSiftKeypoint*)siftFilt.keys.ToPointer();
                    for (int i = 0; i < siftFilt.nkeys; i++)
                    {
                        keyPoint = *pKeyPoints;
                        pKeyPoints++;
                        orientations = null;
                        if (resultType == SiftDetectorResultType.Normal || resultType == SiftDetectorResultType.Extended)
                        {
                            //计算并遍历每个点的方向
                            angleCount = VlFeatInvoke.vl_sift_calc_keypoint_orientations(ptrSiftFilt, angles, ref keyPoint);
                            orientations = new SiftKeyPointOrientation[angleCount];
                            for (int j = 0; j < angleCount; j++)
                            {
                                angle = angles[j];
                                descriptors = null;
                                if (resultType == SiftDetectorResultType.Extended)
                                {
                                    //计算每个方向的描述
                                    VlFeatInvoke.vl_sift_calc_keypoint_descriptor(ptrSiftFilt, ptrDescriptors, ref keyPoint, angle);
                                    descriptors = new float[128];
                                    Marshal.Copy(ptrDescriptors, descriptors, 0, 128);
                                }
                                orientations[j] = new SiftKeyPointOrientation(angle, descriptors);  //保存关键点方向和描述
                            }
                        }
                        features.Add(new SiftFeature(keyPoint, orientations));  //将得到的特征添加到列表中
                    }
                    //下一阶
                    if (VlFeatInvoke.vl_sift_process_next_octave(ptrSiftFilt) == VlFeatInvoke.VL_ERR_EOF)
                        break;
                }
            }
            //释放资源
            Marshal.FreeHGlobal(ptrDescriptors);
            //返回
            return features;
        }

        /// 
        /// 进行基本的SIFT检测,并返回关键点列表
        /// 
        /// 单通道浮点型图像数据,图像数据不必归一化到区间[0,1]
        /// 返回关键点列表;如果获取失败,返回null。
        public List Process(IntPtr im)
        {
            return Process(im, SiftDetectorResultType.Basic);
        }

        /// 
        /// 进行SIFT检测,并返回检测的结果
        /// 
        /// 图像
        /// SIFT检测的结果类型
        /// 返回SIFT检测结果——SIFT特征列表;如果检测失败,返回null。
        public List Process(Image image, SiftDetectorResultType resultType)
        {
            if (image.Width != SiftFilt.width || image.Height != SiftFilt.height)
                throw new ArgumentException("图像的尺寸和构造函数中指定的尺寸不一致。", "image");
            return Process(image.MIplImage.imageData, resultType);
        }

        /// 
        /// 进行基本的SIFT检测,并返回检测的结果
        /// 
        /// 图像
        /// 返回SIFT检测结果——SIFT特征列表;如果检测失败,返回null。
        public List Process(Image image)
        {
            return Process(image, SiftDetectorResultType.Basic);
        }

        /// 
        /// 释放资源
        /// 
        public void Dispose()
        {
            if (ptrSiftFilt != IntPtr.Zero)
                VlFeatInvoke.vl_sift_delete(ptrSiftFilt);
        }
    }

    /// 
    /// SIFT特征
    /// 
    public struct SiftFeature
    {
        public VlSiftKeypoint keypoint;                            //关键点
        public SiftKeyPointOrientation[] keypointOrientations;     //关键点的方向及方向对应的描述

        public SiftFeature(VlSiftKeypoint keypoint)
            : this(keypoint, null)
        {
        }

        public SiftFeature(VlSiftKeypoint keypoint, SiftKeyPointOrientation[] keypointOrientations)
        {
            this.keypoint = keypoint;
            this.keypointOrientations = keypointOrientations;
        }
    }

    /// 
    /// Sift关键点的方向及描述
    /// 
    public struct SiftKeyPointOrientation
    {
        public double angle;            //方向
        public float[] descriptors;     //描述

        public SiftKeyPointOrientation(double angle)
            : this(angle, null)
        {
        }

        public SiftKeyPointOrientation(double angle, float[] descriptors)
        {
            this.angle = angle;
            this.descriptors = descriptors;
        }
    }

    /// 
    /// SIFT检测的结果
    /// 
    public enum SiftDetectorResultType
    {
        Basic,      //基本:仅包含关键点
        Normal,     //正常:包含关键点、方向
        Extended    //扩展:包含关键点、方向以及描述
    }
}

MSER区域

OpenCv中的函数cvExtractMSER以及EmguCv中的Image.ExtractMSER方法实现了MSER区域的检测。由于OpenCv的文档中目前还没有cvExtractMSER这一部分,大家如果要看文档的话,可以先去看EmguCv的文档。
    需要注意的是MSER区域的检测结果是区域中所有的点序列。例如检测到3个区域,其中一个区域是从(0,0)到(2,1)的矩形,那么结果点序列为:(0,0),(1,0),(2,0),(2,1),(1,1),(0,1)。
    MSER区域检测的示例代码如下:

        //MSER(区域)特征检测
        private string MserFeatureDetect()
        {
            //获取参数
            MCvMSERParams mserParam = new MCvMSERParams();
            mserParam.delta = int.Parse(txtMserDelta.Text);
            mserParam.maxArea = int.Parse(txtMserMaxArea.Text);
            mserParam.minArea = int.Parse(txtMserMinArea.Text);
            mserParam.maxVariation = float.Parse(txtMserMaxVariation.Text);
            mserParam.minDiversity = float.Parse(txtMserMinDiversity.Text);
            mserParam.maxEvolution = int.Parse(txtMserMaxEvolution.Text);
            mserParam.areaThreshold = double.Parse(txtMserAreaThreshold.Text);
            mserParam.minMargin = double.Parse(txtMserMinMargin.Text);
            mserParam.edgeBlurSize = int.Parse(txtMserEdgeBlurSize.Text);
            bool showDetail = cbMserShowDetail.Checked;
            //计算
            Stopwatch sw = new Stopwatch();
            sw.Start();
            MemStorage storage = new MemStorage();
            Seq[] regions = imageSource.ExtractMSER(null, ref mserParam, storage);
            sw.Stop();
            //显示
            Image imageResult = imageSourceGrayscale.Convert();
            StringBuilder sbResult = new StringBuilder();
            int idx = 0;
            foreach (Seq region in regions)
            {
                imageResult.DrawPolyline(region.ToArray(), true, new Bgr(255d, 0d, 0d), 2);
                if (showDetail)
                {
                    sbResult.AppendFormat("第{0}区域,包含{1}个顶点(", idx, region.Total);
                    foreach (Point pt in region)
                        sbResult.AppendFormat("{0},", pt);
                    sbResult.Append(")\r\n");
                }
                idx++;
            }
            pbResult.Image = imageResult.Bitmap;
            //释放资源
            imageResult.Dispose();
            storage.Dispose();
            //返回
            return string.Format("·MSER区域,用时{0:F05}毫秒,参数(delta:{1},maxArea:{2},minArea:{3},maxVariation:{4},minDiversity:{5},maxEvolution:{6},areaThreshold:{7},minMargin:{8},edgeBlurSize:{9}),检测到{10}个区域\r\n{11}",
                sw.Elapsed.TotalMilliseconds, mserParam.delta, mserParam.maxArea, mserParam.minArea, mserParam.maxVariation, mserParam.minDiversity,
                mserParam.maxEvolution, mserParam.areaThreshold, mserParam.minMargin, mserParam.edgeBlurSize, regions.Length, showDetail ? sbResult.ToString() : "");
        }

各种特征检测方法性能对比

上面介绍了这么多的特征检测方法,那么它们的性能到底如何呢?因为它们的参数设置对处理时间及结果的影响很大,我们在这里基本都使用默认参数处理同一幅图像。在我机器上的处理结果见下表:

特征 用时(毫秒) 特征数目
Sobel算子 5.99420 n/a
拉普拉斯算子 3.13440 n/a
Canny算子 3.41160 n/a
霍夫线变换 13.70790 10
霍夫圆变换 78.07720 0
Harris角点 9.41750 n/a
ShiTomasi角点 16.98390 18
亚像素级角点 3.63360 18
SURF角点 266.27000 151
Star关键点 14.82800 56
FAST角点 31.29670 159
SIFT角点 287.52310 54
MSER区域 40.62970 2

如果需要本文的源代码,请点击这里下载。

转载

图像特征检测(Image Feature Detection) - Wuya - 博客园 (cnblogs.com)

你可能感兴趣的:(数字图像处理,photoshop,ui)