各種影像處理類庫的比較及選擇(The Comparison of Image Processing Libraries)

weixin_34391854發表於2010-01-26

作者:王先榮

前言

近期需要做一些影像處理方面的學習和研究,首要任務就是選擇一套合適的影像處理類庫。目前較知名且功能完善的影像處理類庫有OpenCvEmguCvAForge.net等等。本文將從許可協議、下載、安裝、文件資料、易用性、效能等方面對這些類庫進行比較,然後給出選擇建議,當然也包括我自己的選擇。

 

許可協議

類庫 許可協議 許可協議網址 大致介紹
OpenCv BSD www.opensource.org/licenses/bsd-license.html 在保留原來BSD協議宣告的前提下,隨便怎麼用都行
EmguCv GPL v3 http://www.gnu.org/licenses/gpl-3.0.txt 你的產品必須也使用GPL協議,開源且免費
商業授權 http://www.emgu.com/wiki/files/CommercialLicense.txt 給錢之後可以用於閉源的商業產品
AForge.net LGPL v3 http://www.gnu.org/licenses/lgpl.html 如果不修改類庫原始碼,引用該類庫的產品可以閉源和(或)收費

以上三種類庫都可以用於開發商業產品,但是EmguCv需要付費;因為我只是用來學習和研究,所以這些許可協議對我無所謂。不過鑑於我們身在中國,如果臉皮厚點,去他丫的許可協議。

 

下載

可以很方便的下載到這些類庫,下載地址分別為:

類庫

下載地址

OpenCv

http://sourceforge.net/projects/opencvlibrary/files/

EmguCv

http://www.emgu.com/wiki/index.php/Download_And_Installation

AForge.net

http://www.aforgenet.com/framework/downloads.html

 

安裝

這些類庫的安裝都比較簡單,直接執行安裝程式,並點“下一步”即可完成。但是OpenCv在安裝完之後還需要一些額外的處理才能在VS2008裡面使用,在http://www.opencv.org.cn有一篇名為《VC2008 Express下安裝OpenCv 2.0》的文章專門介紹瞭如何安裝OpenCv

類庫

安裝難易度

備註

OpenCv

比較容易

VC下使用需要重新編譯

EmguCv

容易

 

AForge.net

容易

 

相信看這篇文章的人都不會被安裝困擾。

 

文件資料 

類庫

總體評價

書籍

網站

文件

示例

社群

備註

OpenCv

中等

中英文

中英文

中英文

較多

中文論壇

有中文資料但不完整

EmguCv

英文

英文

英文論壇

論壇人氣很差

AForge.net

英文

英文

英文論壇

論壇人氣很差

OpenCv有一些中文資料,另外兩種的資料全是英文的;不過EmguCv建立在OpenCv的基礎上,大部分OpenCv的資料可以用於EmguCv;而AForge.net是原生的.net類庫,對GDI+有很多擴充套件,一些MSDN的資料可以借鑑。如果在查詞典的基礎上還看不懂英文文件,基本上可以放棄使用這些類庫了。

 

易用性

易用性這玩意,主觀意志和個人能力對它影響很大,下面是我的看法:

類庫

易用性

備註

OpenCv

比較差

OpenCv大多數功能都以C風格函式形式提供,少部分功能以C++類提供。注意:2.0版將更多的功能封裝成類了。

EmguCv

比較好

OpenCv的絕大部分功能都包裝成了.net類、結構或者列舉。不過文件不全,還是得對照OpenCv的文件去看才行。

AForge.net

.net類庫,用起來很方便。

最近幾年一直用的是C# ,把CC++忘記得差不多了,況且本來C/C++我就不太熟,所以對OpenCv的看法恐怕有偏見。

 

效能

這些類庫能做的事情很多,我選了最基礎的部分來進行效能測試,那就是將一幅彩色影像轉換成灰度圖,然後再將灰度圖轉換成二值影像。因為影像處理大部分時間都用於記憶體讀寫及運算(特別是矩陣運算),所以這兩種操作有一定的代表性。

我分別用以下方式實現了影像的灰度化及二值化:(1C語言呼叫OpenCv庫;(2C#呼叫AForge.net庫;(3C#呼叫EmguCv庫;(4C#中用P/INVOKE的形式呼叫OpenCv函式;(5C#呼叫自己寫的灰度和二值化方法。

ContractedBlock.gifExpandedBlockStart.gifC語言呼叫OpenCv
#include "stdafx.h"
#include 
"cv.h"
#include 
"cxcore.h"
#include 
"highgui.h"
#include 
"winbase.h"

int _tmain(int argc, _TCHAR* argv[])
{
    
//初始化影像
    IplImage * pIplSource=cvLoadImage("E:\\xrwang\\ImageProcessLearn\\Debug\\wky_tms_2272x1704.jpg");
    IplImage 
* pIplGrayscale=cvCreateImage(cvSize(pIplSource->width,pIplSource->height),IPL_DEPTH_8U,1);
    IplImage 
* pIplThreshold=cvCreateImage(cvSize(pIplSource->width,pIplSource->height),IPL_DEPTH_8U,1);
    
//執行灰度化和二值化,並輸出所用時間
    LARGE_INTEGER frequency,count1,count2,count3;
    
double time1,time2;
    QueryPerformanceFrequency(
&frequency);
    
for(int i=0;i<10;i++)
    {
        QueryPerformanceCounter(
&count1);
        cvCvtColor(pIplSource,pIplGrayscale,CV_BGR2GRAY);
        QueryPerformanceCounter(
&count2);
        cvThreshold(pIplGrayscale,pIplThreshold,
128,255,CV_THRESH_BINARY);
        QueryPerformanceCounter(
&count3);
        time1
=(double)1000.0*(count2.QuadPart-count1.QuadPart)/frequency.QuadPart;
        time2
=(double)1000.0*(count3.QuadPart-count2.QuadPart)/frequency.QuadPart;
        printf(
"灰度:%g毫秒,二值化:%g毫秒\r\n",time1,time2);
    }
    
//顯示影像
    cvNamedWindow("grayscale",0);
    cvNamedWindow(
"threshold",0);
    cvResizeWindow(
"grayscale",600,480);
    cvResizeWindow(
"threshold",600,480);
    cvShowImage(
"grayscale",pIplGrayscale);
    cvShowImage(
"threshold",pIplThreshold);
    cvWaitKey(
0);
    
//銷燬物件
    cvDestroyAllWindows();
    cvReleaseImage(
&pIplThreshold);
    cvReleaseImage(
&pIplGrayscale);
    cvReleaseImage(
&pIplSource);
    
return 0;
}

 

ContractedBlock.gifExpandedBlockStart.gifC#呼叫各種類庫處理影像
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.Runtime.InteropServices;
using AForge.Imaging.Filters;
using Emgu.CV;
using Emgu.CV.Structure;
using Emgu.CV.CvEnum;

namespace ImageProcessLearn
{
    
public partial class FormMain : Form
    {
        
public FormMain()
        {
            InitializeComponent();
        }

        
//窗體載入時
        private void FormMain_Load(object sender, EventArgs e)
        {
            
//顯示原始影像
            pbSource.Image = Image.FromFile("wky_tms_2272x1704.jpg");
        }

        
//使用選定的類庫處理影像
        private void btnProcess_Click(object sender, EventArgs e)
        {
            
if (rbAForge.Checked)
            {
                ProcessImageWithAforge();
            }
            
else if (rbEmgucv.Checked)
            {
                ProcessImageWithEmgucv();
            }
            
else if (rbOpencv.Checked)
            {
                ProcessImageWithOpencv();
            }
            
else if (rbOwnMethod.Checked)
                ProcessImageWithOwnMethod();
        }

        
/// <summary>
        
/// 使用AForge.net處理影像
        
/// </summary>
        private void ProcessImageWithAforge()
        {
            Stopwatch sw 
= new Stopwatch(); //計時器
            
//灰度
            sw.Start();
            Grayscale grayscaleFilter 
= new Grayscale(0.2990.5870.114);
            Bitmap bitmapGrayscale 
= grayscaleFilter.Apply((Bitmap)pbSource.Image);
            sw.Stop();
            
double timeGrayscale = sw.Elapsed.TotalMilliseconds;
            
if (pbGrayscale.Image != null)
            {
                pbGrayscale.Image.Dispose();
                pbGrayscale.Image 
= null;
            }
            pbGrayscale.Image 
= bitmapGrayscale;
            
//二值化
            sw.Reset();
            sw.Start();
            Threshold thresholdFilter 
= new Threshold(128);
            Bitmap bitmapThreshold 
= thresholdFilter.Apply(bitmapGrayscale);
            sw.Stop();
            
double timeThreshold = sw.Elapsed.TotalMilliseconds;
            
if (pbThreshold.Image != null)
            {
                pbThreshold.Image.Dispose();
                pbThreshold.Image 
= null;
            }
            pbThreshold.Image 
= bitmapThreshold;
            
//輸出所用時間
            txtResult.Text += string.Format("類庫:AForge.net,灰度:{0:F05}毫秒,二值化:{1:F05}毫秒\r\n", timeGrayscale, timeThreshold);
        }

        
/// <summary>
        
/// 使用EmguCv處理影像
        
/// </summary>
        private void ProcessImageWithEmgucv()
        {
            Stopwatch sw 
= new Stopwatch(); //計時器
            
//灰度
            Image<Bgr, Byte> imageSource = new Image<Bgr, byte>((Bitmap)pbSource.Image);
            sw.Start();
            Image
<Gray, Byte> imageGrayscale = imageSource.Convert<Gray, Byte>();
            sw.Stop();
            
double timeGrayscale = sw.Elapsed.TotalMilliseconds;
            
if (pbGrayscale.Image != null)
            {
                pbGrayscale.Image.Dispose();
                pbGrayscale.Image 
= null;
            }
            pbGrayscale.Image 
= imageGrayscale.ToBitmap();
            
//二值化
            sw.Reset();
            sw.Start();
            Image
<Gray, Byte> imageThreshold = imageGrayscale.ThresholdBinary(new Gray(128), new Gray(255));
            sw.Stop();
            
double timeThreshold = sw.Elapsed.TotalMilliseconds;
            
if (pbThreshold.Image != null)
            {
                pbThreshold.Image.Dispose();
                pbThreshold.Image 
= null;
            }
            pbThreshold.Image 
= imageThreshold.ToBitmap();
            
//輸出所用時間
            txtResult.Text += string.Format("類庫:EmguCv,灰度:{0:F05}毫秒,二值化:{1:F05}毫秒\r\n", timeGrayscale, timeThreshold);
        }

        
/// <summary>
        
/// 使用Open Cv P/Invoke處理影像
        
/// </summary>
        unsafe private void ProcessImageWithOpencv()
        {
            Stopwatch sw 
= new Stopwatch(); //計時器
            
//灰度
            Image<Bgr, Byte> imageSource = new Image<Bgr, byte>((Bitmap)pbSource.Image);
            IntPtr ptrSource 
= Marshal.AllocHGlobal(Marshal.SizeOf(typeof(MIplImage)));
            Marshal.StructureToPtr(imageSource.MIplImage, ptrSource, 
true);
            sw.Start();
            IntPtr ptrGrayscale 
= CvInvoke.cvCreateImage(imageSource.Size, IPL_DEPTH.IPL_DEPTH_8U, 1);
            CvInvoke.cvCvtColor(ptrSource, ptrGrayscale, COLOR_CONVERSION.CV_BGR2GRAY);
            sw.Stop();
            
double timeGrayscale = sw.Elapsed.TotalMilliseconds;
            
if (pbGrayscale.Image != null)
            {
                pbGrayscale.Image.Dispose();
                pbGrayscale.Image 
= null;
            }
            pbGrayscale.Image 
= ImageConverter.IplImagePointerToBitmap(ptrGrayscale);
            
//二值化
            sw.Reset();
            sw.Start();
            IntPtr ptrThreshold 
= CvInvoke.cvCreateImage(imageSource.Size, IPL_DEPTH.IPL_DEPTH_8U, 1);
            CvInvoke.cvThreshold(ptrGrayscale, ptrThreshold, 128d, 255d, THRESH.CV_THRESH_BINARY);
            sw.Stop();
            
double timeThreshold = sw.Elapsed.TotalMilliseconds;
            
if (pbThreshold.Image != null)
            {
                pbThreshold.Image.Dispose();
                pbThreshold.Image 
= null;
            }
            pbThreshold.Image 
= ImageConverter.IplImagePointerToBitmap(ptrThreshold);
            
//釋放資源
            
//CvInvoke.cvReleaseImage(ref ptrThreshold);
            
//CvInvoke.cvReleaseImage(ref ptrGrayscale);
            Marshal.FreeHGlobal(ptrSource);
            
//輸出所用時間
            txtResult.Text += string.Format("類庫:OpenCv P/Invoke,灰度:{0:F05}毫秒,二值化:{1:F05}毫秒\r\n", timeGrayscale, timeThreshold);
        }

        
/// <summary>
        
/// 使用自定義的方法處理影像
        
/// </summary>
        private void ProcessImageWithOwnMethod()
        {
            Stopwatch sw 
= new Stopwatch(); //計時器
            
//灰度
            sw.Start();
            Bitmap bitmapGrayscale 
= Grayscale((Bitmap)pbSource.Image);
            sw.Stop();
            
double timeGrayscale = sw.Elapsed.TotalMilliseconds;
            
if (pbGrayscale.Image != null)
            {
                pbGrayscale.Image.Dispose();
                pbGrayscale.Image 
= null;
            }
            pbGrayscale.Image 
= bitmapGrayscale;
            
//二值化
            sw.Reset();
            sw.Start();
            Bitmap bitmapThreshold 
= Threshold(bitmapGrayscale, 128);
            sw.Stop();
            
double timeThreshold = sw.Elapsed.TotalMilliseconds;
            
if (pbThreshold.Image != null)
            {
                pbThreshold.Image.Dispose();
                pbThreshold.Image 
= null;
            }
            pbThreshold.Image 
= bitmapThreshold;
            
//輸出所用時間
            txtResult.Text += string.Format("類庫:自定義方法,灰度:{0:F05}毫秒,二值化:{1:F05}毫秒\r\n", timeGrayscale, timeThreshold);
        }

        
/// <summary>
        
/// 將指定影像轉換成灰度圖
        
/// </summary>
        
/// <param name="bitmapSource">源影像支援3通道或者4通道影像,支援Format24bppRgb、Format32bppRgb和Format32bppArgb這3種畫素格式</param>
        
/// <returns>返回灰度圖,如果轉化失敗,返回null。</returns>
        private Bitmap Grayscale(Bitmap bitmapSource)
        {
            Bitmap bitmapGrayscale 
= null;
            
if (bitmapSource != null && (bitmapSource.PixelFormat == PixelFormat.Format24bppRgb || bitmapSource.PixelFormat == PixelFormat.Format32bppArgb || bitmapSource.PixelFormat == PixelFormat.Format32bppRgb))
            {
                
int width = bitmapSource.Width;
                
int height = bitmapSource.Height;
                Rectangle rect 
= new Rectangle(00, width, height);
                bitmapGrayscale 
= new Bitmap(width, height, PixelFormat.Format8bppIndexed);
                
//設定調色盤
                ColorPalette palette = bitmapGrayscale.Palette;
                
for (int i = 0; i < palette.Entries.Length; i++)
                    palette.Entries[i] 
= Color.FromArgb(255, i, i, i);
                bitmapGrayscale.Palette 
= palette;
                BitmapData dataSource 
= bitmapSource.LockBits(rect, ImageLockMode.ReadOnly, bitmapSource.PixelFormat);
                BitmapData dataGrayscale 
= bitmapGrayscale.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
                
byte b, g, r;
                
int strideSource = dataSource.Stride;
                
int strideGrayscale = dataGrayscale.Stride;
                
unsafe
                {
                    
byte* ptrSource = (byte*)dataSource.Scan0.ToPointer();
                    
byte* ptr1;
                    
byte* ptrGrayscale = (byte*)dataGrayscale.Scan0.ToPointer();
                    
byte* ptr2;
                    
if (bitmapSource.PixelFormat == PixelFormat.Format24bppRgb)
                    {
                        
for (int row = 0; row < height; row++)
                        {
                            ptr1 
= ptrSource + strideSource * row;
                            ptr2 
= ptrGrayscale + strideGrayscale * row;
                            
for (int col = 0; col < width; col++)
                            {
                                b 
= *ptr1;
                                ptr1
++;
                                g 
= *ptr1;
                                ptr1
++;
                                r 
= *ptr1;
                                ptr1
++;
                                
*ptr2 = (byte)(0.114 * b + 0.587 * g + 0.299 * r);
                                ptr2
++;
                            }
                        }
                    }
                    
else    //bitmapSource.PixelFormat == PixelFormat.Format32bppArgb || bitmapSource.PixelFormat == PixelFormat.Format32bppRgb
                    {
                        
for (int row = 0; row < height; row++)
                        {
                            ptr1 
= ptrSource + strideGrayscale * row;
                            ptr2 
= ptrGrayscale + strideGrayscale * row;
                            
for (int col = 0; col < width; col++)
                            {
                                b 
= *ptr1;
                                ptr1
++;
                                g 
= *ptr1;
                                ptr1
++;
                                r 
= *ptr1;
                                ptr1 
+= 2;
                                
*ptr2 = (byte)(0.114 * b + 0.587 * g + 0.299 * r);
                                ptr2
++;
                            }
                        }
                    }
                }
                bitmapGrayscale.UnlockBits(dataGrayscale);
                bitmapSource.UnlockBits(dataSource);
            }
            
return bitmapGrayscale;
        }

        
/// <summary>
        
/// 將指定的灰度影像轉換成二值影像。如果某個畫素的值大於等於閥值,該畫素置為白色;否則置為黑色。
        
/// 目前支援8bpp和16bpp兩種灰度影像的轉換,對於8bpp,閥值介於0~255之間;對於16bpp,閥值介於0~65535之間。
        
/// </summary>
        
/// <param name="bitmapGrayscale">灰度影像</param>
        
/// <param name="thresholdValue">閥值</param>
        
/// <returns>返回轉換之後的二值影像;如果轉換失敗,返回null。</returns>
        private Bitmap Threshold(Bitmap bitmapGrayscale,int thresholdValue)
        {
            Bitmap bitmapThreshold 
= null;
            
if (bitmapGrayscale != null)
            {
                
int width = bitmapGrayscale.Width;
                
int height = bitmapGrayscale.Height;
                Rectangle rect 
= new Rectangle(00, width, height);
                PixelFormat pixelFormat 
= bitmapGrayscale.PixelFormat;
                
if (pixelFormat == PixelFormat.Format8bppIndexed)
                {
                    
if (thresholdValue >= 0 && thresholdValue <= 255)
                    {
                        bitmapThreshold 
= (Bitmap)bitmapGrayscale.Clone();
                        
byte white = 255;
                        
byte black = 0;
                        BitmapData data 
= bitmapThreshold.LockBits(rect, ImageLockMode.ReadWrite, pixelFormat);
                        
unsafe
                        {
                            
byte* ptrStart = (byte*)data.Scan0.ToPointer();
                            
byte* ptr1;
                            
for (int row = 0; row < height; row++)
                            {
                                ptr1 
= ptrStart + data.Stride * row;
                                
for (int col = 0; col < width; col++)
                                {
                                    
*ptr1 = (*ptr1 < thresholdValue) ? black : white;
                                    ptr1
++;
                                }
                            }
                        }
                        bitmapThreshold.UnlockBits(data);
                    }
                }
                
else if (pixelFormat == PixelFormat.Format16bppGrayScale)
                {
                    bitmapThreshold 
= (Bitmap)bitmapGrayscale.Clone();
                    UInt16 white 
= 65535;
                    UInt16 black 
= 0;
                    BitmapData data 
= bitmapThreshold.LockBits(rect, ImageLockMode.ReadWrite, pixelFormat);
                    
unsafe
                    {
                        
byte* ptrStart = (byte*)data.Scan0.ToPointer();
                        UInt16
* ptr1;
                        
for (int row = 0; row < height; row++)
                        {
                            ptr1 
= (UInt16*)(ptrStart + data.Stride * row);
                            
for (int col = 0; col < width; col++)
                            {
                                
*ptr1 = (*ptr1 < thresholdValue) ? black : white;
                                ptr1
++;
                            }
                        }
                    }
                    bitmapThreshold.UnlockBits(data);
                }
            }
            
return bitmapThreshold;
        }
    }
}

 

     分別用上述5種形式處理10次,記錄下執行時間,去掉每種的最大和最小資料,然後計算平均值。結果如下所示(單位是毫秒):

語言

類庫

灰度化

二值化

效能排名

C

OpenCv

16.89721

7.807766

1

C#

Aforge.net

48.9403

25.32473

5

C#

EmguCv

18.86898

13.74628

3

C#

OpenCv(P/Invoke)

18.68938

10.0149

2

C#

自定義處理方法

48.33593

21.46168

4

測試環境如下:CPU-奔騰4 2.4G,記憶體-512M,作業系統-Windows XP SP2,顯示卡-nVidia GForce4 64M,程式數-49,執行緒數-611,控制程式碼數-13004,可用記憶體101M。

chart.JPG

毫無疑問,用C語言呼叫OpenCv的效能最好,兩種純.net的方式效能最差。

 C語言呼叫OpenCv的處理效果如下所示:

OpencvInC.JPG 

C#的處理效果如下:

.net.JPG 

結論

將上面的內容彙總結果如下表所示:

類庫

OpenCv

EmguCv

AForge.net

許可協議

BSD

GPL v3或商業授權

LGPL v3

下載

方便

方便

方便

安裝

比較容易

容易

容易

文件資料

中等

易用性

比較差

比較好

效能

很好

比較好

不好

綜上所述,我的選擇是使用EmguCv作為我的影像處理類庫,在必要的時候用P/Invoke的形式呼叫沒有被封裝的OpenCv函式。你呢?

 

感謝您耐心看完本文,希望對您有所幫助。

 

部落格園的文字編輯器太操蛋了,辛苦打了一個多小時的字,突然彈出一個錯誤提示無法繼續了。提醒大家注意:如果部落格內容較長,一定要用別的工具(例如WORD)編寫好,然後再複製到部落格園的編輯器。

相關文章