作者:王先榮
前言
近期需要做一些影像處理方面的學習和研究,首要任務就是選擇一套合適的影像處理類庫。目前較知名且功能完善的影像處理類庫有OpenCv、EmguCv、AForge.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# ,把C和C++忘記得差不多了,況且本來C/C++我就不太熟,所以對OpenCv的看法恐怕有偏見。
效能
這些類庫能做的事情很多,我選了最基礎的部分來進行效能測試,那就是將一幅彩色影像轉換成灰度圖,然後再將灰度圖轉換成二值影像。因為影像處理大部分時間都用於記憶體讀寫及運算(特別是矩陣運算),所以這兩種操作有一定的代表性。
我分別用以下方式實現了影像的灰度化及二值化:(1)C語言呼叫OpenCv庫;(2)C#呼叫AForge.net庫;(3)C#呼叫EmguCv庫;(4)C#中用P/INVOKE的形式呼叫OpenCv函式;(5)C#呼叫自己寫的灰度和二值化方法。
#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;
}
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.299, 0.587, 0.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(0, 0, 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(0, 0, 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。
毫無疑問,用C語言呼叫OpenCv的效能最好,兩種純.net的方式效能最差。
C語言呼叫OpenCv的處理效果如下所示:
C#的處理效果如下:
結論
將上面的內容彙總結果如下表所示:
類庫 |
OpenCv |
EmguCv |
AForge.net |
許可協議 |
BSD |
GPL v3或商業授權 |
LGPL v3 |
下載 |
方便 |
方便 |
方便 |
安裝 |
比較容易 |
容易 |
容易 |
文件資料 |
中等 |
少 |
少 |
易用性 |
比較差 |
比較好 |
好 |
效能 |
很好 |
比較好 |
不好 |
綜上所述,我的選擇是使用EmguCv作為我的影像處理類庫,在必要的時候用P/Invoke的形式呼叫沒有被封裝的OpenCv函式。你呢?
感謝您耐心看完本文,希望對您有所幫助。
部落格園的文字編輯器太操蛋了,辛苦打了一個多小時的字,突然彈出一個錯誤提示無法繼續了。提醒大家注意:如果部落格內容較長,一定要用別的工具(例如WORD)編寫好,然後再複製到部落格園的編輯器。