2012年8月12日 星期日

EmguCV Image Process: Manipulating the pixels part1

Taipei 101 with grass HDR1

EmguCV 2.3 Application Programming : Manipulating the pixels

EmguCV的安裝教學請看這篇

參考OpenCV 2 Computer Vision Application Programming Cookbook第二章

來做一些EmguCV上的簡單示範,主題如下:

Create Image Class

Accessing Pixel Values

Scanning an Image with Points

Writing Efficient Image Scanning Loops

Create Image Class
這次的範例專案需要參考下列兩個dll:Emgu.CV.dll、Emgu.Util.dll

跟一個.NET的元件:System.Drawing

並且在執行目錄下放置:Emgu.CV.dll、Emgu.Util.dll、opencv_core231.dll、opencv_highgui231.dll

然後將上圖抓下來,存放到E:\101.jpg,便可透過下面程式載入此圖:

using Emgu.CV;
using Emgu.Util;
using Emgu.CV.Structure;
...
Image<Bgr, Byte> image = new Image<Bgr, Byte>(@"E:\101.jpg");

Image這個類別可讀取多種的影像格式

這裡的Bgr代表每個pixel的struct是由blue、green、red三個顏色來控制

如果是用Image<Gray, Byte>讀取影像

每個pixel的灰階值就是由Gray這個struct來控制

下列是Image所支援的色域結構:
Gray
Bgr (Blue Green Red)
Bgra (Blue Green Red Alpha)
Hsv (Hue Saturation Value)
Hls (Hue Lightness Saturation)
Lab (CIE L*a*b*)
Luv (CIE L*u*v*)
Xyz (CIE XYZ.Rec 709 with D65 white point)
Ycc (YCrCb JPEG)

而Byte代表著每一個色域的顏色深度(Image Depth)採用8bits的容量來存放

也就是0~255的值域,而一個pixel有bgr三色就需要24bits

一般的使用下都是以Byte來實作!

下列是Image所支援的影像深度:
Byte
SByte
Single (float)
Double
UInt16
Int16
Int32 (int)

想要有更了解Image可以到EmguCV的官網參考原文

Accessing Pixel Values
影像的左上角是[0, 0],右下角是[431, 640]

而最簡單對影像的像素值控制的方法如下:

for (int height = 0; height < image.Height; height++)
{
    for (int width = 0; width < image.Width; width++)
    {
        image[height, width] = new Bgr(blue, green, red);
    }
}
blue、green、red分別就是對應到藍色、綠色、紅色的值

利用兩個for迴圈來對每個pixel做存取

pixel的對應很直覺地採用二維陣列

第一欄的值為垂直方向:height

第二欄的值為水平方向:width

由於這邊是載入彩色的影像Image<Bgr, Byte>

所以每個pixel的顏色值是透過Bgr這個struct來做設定

這樣便可對整張影像的所有pixel值來做存取或設定!

Scanning an Image with Points
那要比較快的方法

就得透過pointer方式來存取

原本的存取方式是不斷透過封裝過的Image類別來做存取

不斷的呼叫Image的方法來對pixel做控制

以筆者電腦CPU:Intel I5-560M 2.66GHz,記憶體:DDR3 2GB

這張640*431的影像跑完一輪需要67ms

若是改成pointer的方法會差多少呢??


這時必須在專案的屬性中,把容許unsafe程式碼打勾!!


MIplImage MIpImg = (MIplImage)System.Runtime.InteropServices.Marshal.PtrToStructure(image.Ptr, typeof(MIplImage));
unsafe
{
    byte* npixel = (byte*)MIpImg.imageData;

    for (int height = 0; height < image.Height; height++)
    {
        for (int width = 0; width < image.Width; width++)
        {
            npixel[0] = 255;  //blue
            npixel++;
            npixel[0] = 255;  //green
            npixel++;
            npixel[0] = 255;  //red
            npixel++;
        }
    }
}

這裡關鍵的步驟就是在於使用了byte的指標:byte*

透過指標,一個pixel一個pixel的去移動並設定值

這對C#來說是unsafe的語法,所以必須用unsafe包起來

而在每個迴圈中,都對blue、green、red三種顏色做設定(這裡都設定成255)

這裡透過一個MIplImage類別的轉換

影像所有的pixel值會存放在一個byte陣列裡,並且連續分布在記憶體中

所以就不斷地將指標指向下一個位置,就可以輪完所有的pixel

這樣的方式比原先的速度快上很多,大約只要15ms的時間!


在這邊是一個pixel一個pixel來設定

那如果要一次跳一行來設定呢?

例如原本是設定image[0, 0],直接跳到image[1, 0]

這在原本的二維陣列架構中很容易,但在指標裡卻是一維陣列

你可以直接

npixel+=MIpImg.width*3;

但如果現在處理的是灰值影像,乘以3就不對了!

npixel+=MIpImg.width*MIpImg.nChannels;

在MIplImage類別中有一個nChannels的屬性

他會告訴你這個image有幾個色域,Gray就是一,BGR就是三

或者更直接的方法就是

npixel+=MIpImg.widthStep;

就可以直接跳下一行了!!

Writing Efficient Image Scanning Loops
但在這過程中還是有些地方可以改進

尤其在for loop的設計上

原先的做法會不斷的使用image.Height跟image.Width這兩個屬性

這是封裝在Image類別中所實作的CvArray抽象類別裡頭

public int Height { get; }
public int Width { get; }

所以在for loop就會不斷的get!!

所以這裡需要做一些修正


在迴圈裡頭,原先的作法是每設定一個色域的值就將指標+1

所以在一個迴圈裡頭就會做三次運算!!

這裡也要做一些修正

MIplImage MIpImg = (MIplImage)System.Runtime.InteropServices.Marshal.PtrToStructure(image.Ptr, typeof(MIplImage));
int imageHeight = MIpImg.height;
int imageWidth = MIpImg.width;
unsafe
{
    byte* npixel = (byte*)MIpImg.imageData;
    for (int height = 0; height < imageHeight; height++)
    {
        for (int width = 0; width < imageWidth; width++)
        {
            npixel[0] = 255;  //blue
            npixel[1] = 255;  //green
            npixel[2] = 255;  //red
            npixel+=3;
        }
    }
}

先將影像的高、寬用兩個integer的變數(imageHeight、imageWidth)來儲存

所以在for loop中就是直接採用這兩個integer的變數

而迴圈內則直接設定三個色域的值

在一次將指標移動三個位址,到下一個pixel

這樣在一次迴圈內只會有一次運算

這樣的修正的運算速度會降低到1ms左右!

若是對影像的存取速度有興趣的,可看這篇C# 影像處理的速度極限!


下一篇就會開始介紹簡單的影像處理

應該會有趣一點~XD


沒有留言:

張貼留言