ITK 中進行圖像讀寫時使用的接口是完全一樣的,針對不同的圖像文件格式,輸入輸出系統自動選擇使用對應的子系統進行處理。而且,用戶還可以自定義圖像格式,擴展 ITK 的圖像輸入輸出處理庫。
ITK 中的圖像輸入輸出系統框架有如下特點(VTK 中是一樣的):
1.可移植:ITK 的圖像輸入輸出庫能夠在不同的計算機體系結構上工作,使用 C++ 編寫,沒有調用任何操作系統相關的函數,平臺獨立。
2.模塊性:該圖像輸入輸出庫是“自包含”的,與 ITK 的其它子系統相互獨立,並且可以在其它系統中重用(VTK 中的圖像輸入輸出系統應該是同一個系統,但我並不懂 VTK,只是猜測)。爲了達到這個目的,圖像以兩種形式存在:1)在磁盤中以文件存在;2.在內存中以數據結構存在。該 IO 庫只負責圖像的加載和保存,沒有任何與特定圖像應用相關的功能。這要求一個內部類能夠對內存中的圖像數據格式與存儲與磁盤上的圖像文件進行轉換。這個功能由下圖的 FilterIoToImage 類完成。
3.可擴展性:該 IO 庫支持新的圖像文件格式,而不需要修改任何已存在的代碼:"Open to extension, but closed to modification”。這個目標是通過一個組合的設計模式達到的,即 Pluggable object factory(可插入式對象工廠模式),它通過定義一個具有標準接口的抽象超類達到這一目的。Pluggable object factory 允許在運行時增加新的子類,或稱爲 "plugged”。
4.透明性:理想情況下,用戶只需要傳遞一個文件名便可操作圖像的數據,而不必擔心圖像文件格式的實現細節。這一目標通過兩個機制實現:1)所有圖像文件格式的共同點被抽取出來,作爲一個超類。具體的圖像文件格式從此類進行繼承。這樣,用戶可以通過一個共同的接口操作 IO 庫所支持的所有圖像文件。2)pluggable object factory 能夠自動實例化處理給定類型圖像文件的子類對象。
有兩點必須熟記於心:1)圖像在磁盤中與內存中是以不同形式存在的,這們是兩個完全不再的概念。應用程序處理在內存中處理圖像,我們使用一個內部類 FilterIoToImage 進行轉換。2)我們使用了 pluggable object factory 模式,動態地實例化處理特定格式圖像文件的子類對象。當我們需要支持一種新的文件格式時,需要從 ImageIO 繼承一個新的子類處理此種圖像格式的細節。另外,能夠創建該新子類對象實例的工廠也被實例化,並且在主字典(a master dictionary)中註冊。該主字典被用來在運行時、動態地選擇合適的 ImageIO 子類進行實例化,以處理特定的圖像格式。
類結構圖如下所示:
類層次結構,其中,斜體表示抽象類;箭頭表示繼承關係,即 "is-a” 關係;三角表示聚合關係,即 "has-a” 關係。
主要類代碼如下:
1. ImageIO:IO 庫的核心,抽象類。用於處理儲存於磁盤上的圖像文件,並且定義程序員與 IO 庫交互的接口。提供加載和保存圖像文件的方法,以及讀取任何相關的頭文件信息(如,維數、顏色深度等)。另外,我們這裏定義了一個特化的方法,以允許我們將 2D 圖像文件裝配成一個 3D 體文件,並且從 3D 圖像中抽取單一的 2D 切片。
1:
2: //ImageIO
3: class ImageIO
4: {
5: public:
6: virtual void Load() = 0;
7: virtual void Save() = 0;
8: virtual void Save2DSlice(int sliceNumber) = 0;
9: virtual void Assemble3DVolume() = 0;
10:
11: //返回該類可以處理的文件類型的擴展名
12: virtual string GetSupportedFileExtensions() const = 0;
13:
14: private:
15: void* imageData;
16: };
17:
18: class ImageIOJpeg
19: {
20: public:
21: string GetSupportedFileExtensions() const
22: {
23: return ".jpg";
24: }
25: };
其中,數據成員 imageData 被定義爲指向 void 的指針,這樣它便可以儲存任何類型的圖像數據,Load 和 Save 方法將該指針轉換(typecast)成合適的數據類型。 1:
2: //Factory
3: template <class Key, class Object>
4: class Factory
5: {
6: public:
7: static Object* Create (const Key k)
8: {
9: Factory* f = (dictionary->find(k))->second;
10: if (!f)
11: return NULL
12: else
13: return f->MakeObject();
14: }
15: protected:
16: Factory(const Key k)
17: {
18: static bool dictionaryInitialized = false;
19: if (!dictionaryInitialized)
20: {
21: dictionary = new Dictionary;
22: dictionaryInitialized = true;
23: }
24: dictionary->insert(std::make_pair(k, this);
25: }
26:
27: virtual Object* MakeObject() const
28: {
29: /* implemented by subclasses. return instance of object that subclass is responsible for */
30: }
31: private:
32:
33: typedef Factory
34: typedef std::map Dictionary;
35: static Dictionary* dictionary;
36: };
37:
公有成員方法 Create() 是程序員唯一可見的方法,它唯一的參數是一個在內部字典中用來進行索引的 key。如果在字典中找到該 key,則使用與其相關聯的對象工廠實例化一個對象。字典是一個 static 數據成員。因此,所有的對象工廠間共享一個字典實例。
Abstract Facotory 構造函數的參數是一個 key,該 key 被插入字典中,字典保持 key 與具體的(concrete)工廠子類之間的映射關係(子類的構造函數通過 insert(std::make_pair(k, this); 中的 this 實現該功能,因爲 this 表示指向自身的指針)。現在來看一下具體的子類工廠註冊自己的過程,注意在構造函數中使用了靜態布爾變量 dictionaryInitialized,該變量用於確保字典只被實例化一次,且必須在第一個具體的子類工廠註冊自己之前。因爲不同的平臺及編譯器在編譯多個源文件時,靜態變量的初始化順序並不相同,所以這裏有必要進行檢查以確保字典在具體的工廠註冊自身時已經初始化。
3. FactoryIO:繼承自 Factory 類。因爲每一種格式的圖像文件都有不同的擴展名(如,bmp, jpg, png 等等),所以可以使用圖像格式的擴展名作爲 key。這樣,便可從 FactoryIO 類繼承得到處理不同格式圖像的具體工廠,每一種工廠僅僅負責實例化與之相關聯的 ImageIO 類。如下,類 FactoryIOJpeg 用來處理 JPEG 格式的圖像文件:
1:
2:
3: //FactoryIOJpeg
4: class FactoryIOJpeg : public FactoryIO
5: {
6: public:
7: FactoryIOJpeg() : FactoryIO(myObject.GetSupportedFileExtensions()) {}
8:
9: //每個具體的工廠類都包含一個自身的靜態實例
10: static const ImageIOJpeg myObject;
11: ImageIO* MakeObject() const
12: {
13: return new ImageIOJpeg;
14: }
15: static const FactoryIOJpeg registerMyself;
16: };
17:
18:
具體工廠類的實現有兩點需要注意:1.每個具體的工廠類包含一個自身的靜態實例,它唯一的目的便是在字典中註冊自己。2.每個具體的工廠類都有一個靜態的 ImageIO Object 對象實例,負責克隆,它的目的是用來在註冊過程中決定 key 值。
4. Image:該類保存在內存中,這裏不關心它的設計與實現。我們只需要知道它的參數是應用程序中處理的圖像的像素類型即可。PixelType 沒有必要與磁盤中圖像文件的數據類型相同。
5. FilterIOToImage:最後一個重要的類,因爲我們將圖像文件在磁盤中與內存中的形式區分成兩種不同的類型,而且我們還想要這兩個類不知道對方的任何信息。因此我們必須使用一個內部過濾器類,即 FilterIOToImage 在這兩種類型之間進行轉換。
1:
2:
3: //FilterIOToImage
4: class FilterIOToImage
5: {
6: public:
7: FilterIOToImage(const char* fileName, Image* image);
8: FilterIOToImage(Image* image, const char* fileName);
9: virtual void CopyPixelsToIO();
10: virtual void CopyPixelsToImage;
11: protected:
12: ImageIO* myIO;
13: Image* myImage;
14: static const Factory myIOFactory;
15: };
16:
17:
類 FilterIOToImage 包含一個 ImageIO 對象以及 Image Object 對象的引用。因爲 FilterIOToImage 類和 Image 類的參數都是像素類型,所以 FilterIOToImage 知道關於 Image 的模板參數。正如其名字暗示的一樣,CopyPixelsToIO() 和 CopyPixelsToImage() 將圖像數據從一種表示轉換成另一種。通過繼承 FilterIOToImage,該轉換可以是任意複雜的形式。通過其構造函數可知,FilterIOToImage 通過分析文件名的擴展名實例化合適的 ImageIO Object,然後使用該對象加載或者保存圖像數據。
使用時:
1:
2: //test
3: void main ()
4: {
5: FilterIOToImage<float>* loadFilter, saveFilter;
6: Image<float>* image = NULL;
7: loadFilter = new FilterIOToImage<float>("input.jpg", image);
8: // Start processing image
9: //
10: ...
11: // Done processing image. Save result
12: saveFilter = new FilterIOToImage<float>(image, "output.bmp");
13:
14: }
15:
上面演示瞭如何從磁盤中加載一幀 jpg 格式的圖像文件,以及如何將一幅圖像以 bmp 格式保存到磁盤中,可以看到,其使用是非常簡潔的。
常用的使用格式如下所示:
1:
2: //test
3: void main ()
4: {
5: FilterIOToImage<float>* loadFilter, saveFilter;
6: Image<float>* image;
7: const FactoryIO ioFactory;
8: ImageIO* loadIO, saveIO;
9: char inputFileName[] = "inputSlice*.jpg";
10: char outputFileName[] = "outputSlice.bmp";
11:
12: loadIO = ioFactory.Create(ParseFileExtension(inputFileName));
13: loadIO->SetFileName(inputFileName);
14: loadIO->Assemble3DVolume();
15: loadFilter = new FilterIOToImage<float>(loadIO, image);
16: loadFilter->CopyPixelsToImage();
17: // Start processing image
18: //...
19:
20: // Done processing image. Save result
21: saveIO = ioFactory.Create(ParseFileExtension(outputFileName));
22: saveIO->SetFileName(outputFileName);
23: saveFilter = new FilterIOToImage<float>(saveIO, image);
24: saveFilter->CopyPixelsToIO();
25: saveIO->Save2DSlice(3);
26: }
27: