<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                > 原文鏈接:[http://www.aosabook.org/en/itk.html](http://www.aosabook.org/en/itk.html) > 作者:Luis Ibanez 和 Brad King > 譯按:原文的二級和三級標題并無章節號,只有字號區別。 ## 9.1 ITK是什么? [ITK](http://www.itk.org/),又名Insight Toolkit,是一種用于圖像分析的庫,它由美國國家醫學圖書館([US National Libraray of Medicine](http://www.nlm.nih.gov/))發起并資助開發的。ITK可以看作是一個方便使用的圖像分析算法百科全書,特別是它包含了圖像濾波、圖像分割和圖像配準。該庫由一個大學和商業公司組成的聯合組織、以及來自世界各地的代碼提交者共同開發。ITK的開發工作始于1999年,在其最近的十周年紀念過后不久,全庫經歷了一次重構過程,這次重構旨在去除代碼庫中的頑固代碼;并對其重塑,以適應下一個十年的發展。 ## 9.2 架構特性 軟件工具箱與他們的社區有一種密切的關系。他們以一個持續迭代的周期塑造彼此。軟件被持續改進,直到它能滿足社區的需要,與此同時,社區基于軟件準許或者約束他們要怎樣做來制約他們自身的行為。因此,為了更好地理解ITK架構的天性,了解ITK社區經常提出何種問題,以及他們如何著手解決這些問題,是非常有用的。 ### 9.2.1 野獸的天性 > *如果你不了解野獸的天性,那這就對理解它們解剖結構的機制毫無用處。* > --?*Dee Hock, One from Many: VISA and the Rise of Chaordic Organization* 一個典型的圖像分析問題中,研究者或者工程師會取一個輸入圖像,通過降噪或是提高對比度來改善圖像的某些特性,然后進行處理以辨別圖像中的某些特征,比如拐角和強邊緣。這種類型的處理很自然地符合一種數據管線架構,如圖9.1所示。 ![enter image description here](http://www.ituring.com.cn/download/01RB2mHD86xx) **圖9.1:圖像處理管線** 為了說明這一點,圖9.2展示了一幅人腦的核磁共振圖像(Magnetic Resonance Imaging,MRI),以及使用中值濾波器對其進行降噪處理的結果、還有利用邊緣檢測濾波器來辨別其中解剖結構的結果。 ![enter image description here](http://www.ituring.com.cn/download/01RB2mLToFOs)![enter image description here](http://www.ituring.com.cn/download/01RB2mMWUoMK)![enter image description here](http://www.ituring.com.cn/download/01RB2mMcAxe9) **圖9.2:MRI腦部圖像,中值濾波器,邊緣檢測濾波器** 對于圖中的每一項任務,圖像處理社區都已經開發出了各種算法,并且繼續開發新的算法。為什么他們繼續做這些?你可能會問,答案就是圖像處理是一種科學、工程、藝術、以及“烹飪”技術的組合。公然宣稱某種算法組合對于一個圖像處理任務來說是“對的”無異于類似宣布正餐上要備“對的”巧克力甜點一樣的誤導。不是追求完美,社區奮力制造出豐富的工具來確保在面對一項挑戰性的圖像處理任務時,不會出現可選項的短缺。當然,事情發展的狀態是要付出代價的。代價就是圖像分析人員有一個困難的任務,就是在從幾十個不同工具中選擇可用的不同組合,而這些組合可以得出類似的結果。 圖像分析社區與研究社區聯系緊密。某一個研究小組與某一個算法群相關聯是尋常現象。“品牌命名”的風俗,以及某種程度的“市場化”,導致了一種這樣的情況:軟件工具箱可以盡可能好地為社區提供一個非常完整的算法實現集,然后將之混合并匹配,以創建一個滿足社區需要的菜譜。 為什么ITK要被設計并實現成一個巨大的某種程度獨立、且有條理的工具——*圖像濾波器*——的原因有很多,多數濾波器用于解決類似的問題。在本文中,某種程度的“冗余”——打個比方,提供高斯濾波器的三種不同實現方法——這不應該被看做是問題,而應該是一種有價值的特性,因為不同的實現可以可交換地使用,以滿足約束并且發掘與圖像尺寸、處理器數量、以及可能與某個給定圖像處理應用程序中的特定高斯核尺度相關的效率潛能。 該工具箱還被創建成一個成長的、不斷更新自身的資源,因為新的算法和更好的實現出現了,取代了現有的;還因為為了應對不斷涌現的新的醫學影像處理技術而開發的新工具。 快速了解了ITK社區中的圖像分析人員的每日例行公事,我們現在開始深入架構的主要特性: * 模塊化 * 數據管線 * 工廠 * IO工廠 * 流 * 可復用性 * 可維護性 ### 9.2.2 模塊化 模塊化是ITK的主要特性之一。這個需求源于圖像分析社區的人們解決問題時的工作方式。大多數圖像分析問題將一幅或多幅輸入圖像放入處理濾波器的組合中,這些濾波器用于增強或是提取圖像中的某些特定的信息片段。因此這中間就沒有大的處理對象,而是許多小的。邏輯上講,這種圖像處理問題的結構性本質特征意味著要實現一個由許多圖像處理濾波器組成的軟件,這些濾波器就可以以不同的方式組合使用了。 將某些特定的處理濾波器聚合為一個家族也是如此,其中的某些實現上的特性可以被分解。這就導致圖像濾波器自然分組為模塊以及模塊群。 至此,模塊化存在于ITK中的三個自然層次上: * 濾波器層次 * 濾波器家族層次 * 濾波器家族群層次 在圖像濾波器層次上,ITK大約擁有700個濾波器。考慮到ITK是以C++實現的,這個層次中的每一個濾波器都是以C++類輔以面向對象的設計模式來實現的。在濾波器家族層次上,ITK根據濾波器進行處理時的方式將其分組。例如,所有與傅里葉變換有關的濾波器將會放入同一個模塊。在C++層次上,模塊映射于源代碼文件樹,并且映射于軟件編譯成二進制形式后的庫文件。ITK擁有120個這種模塊。每個模塊包含: * 屬于該家族的圖像濾波器的源代碼。 * 一些描述該模塊如何構建并列出該模塊與其他模塊之間依賴關系的配置文件。 * 對應于每個濾波器的一組單元測試。 ![enter image description here](http://www.ituring.com.cn/download/01RB85hKM6Sd) **圖9.3:家族群、模塊和類的層次結構** 家族群層次是最為概念性的劃分,它表述軟件的頂層,有助于在源文件樹中定位濾波器。家族群與高層次的概念相關聯,例如:濾波、分割、配準、和IO。這種層次化結構如圖9.3所示。ITK目前擁有124個模塊,這些模塊又聚合成了13個主要家族群。這些模塊大小不一。這種大小的分布,在圖9.4中以字節為單位表示: ![enter image description here](http://www.ituring.com.cn/download/01RB85nXlEgR) **圖9.4:ITK中50個最大的模塊的大小分布,單位:字節** ITK中的模塊化也應用于其中的第三方庫,這些庫并不是工具箱的直接組成部分,但是工具箱依賴它們,因此將這些第三方庫與工具箱中的其余代碼一起發行,以方便使用者。尤其是圖像文件格式庫:HDF5、PNG、TIFF、JPEG、OpenJPEG等。這里強調第三方庫是因為約占ITK總大小的56%。這一點反映了開源應用建立在現有平臺之上的自然特征。第三方庫的大小分布固然不能反映ITK的組織架構,因為我們采用了這些有用的庫,僅僅是由于它們屬于上游開發產物。然而, 第三方庫的代碼與工具箱一并發行、并且將之分割,是模塊化過程的關鍵驅動因素之一。 這里給出模塊大小的分布,因為它是一種代碼合理模塊化的量度。可以把代碼的模塊化看做是一個連續譜,分布于從只有一個巨大的、單體的模塊的一端,到將代碼分割成許多相等大小的模塊的另一端。大小分布是一種工具,它用于顯示模塊化過程的進展,尤其是確保同一個模塊中沒有大塊的代碼,除非有真實的邏輯依賴關系需要這樣的分組。 ITK的模塊化架構使下面的事項成為可能或有助于它們實現: * 減少和澄清交叉依賴關系 * 采用社區貢獻的代碼 * 評估各模塊的質量指標(如:代碼覆蓋率) * 構建工具箱的某個子集 * 將工具箱的某個子集打包用于再發行 * 通過添加新的模塊來維持持續成長 模塊化過程使顯式地辨別并聲明工具箱中不同部分的依賴關系成為可能,當然,這些不同的部分是要放在模塊中的。在許多情況下,這種做法暴露了做作的以及不正確的依賴關系,隨著時間的變化,這些依賴關系被引入工具箱,當大多數代碼被放入一些大的家族群中的時候,這些依賴關系就會被忽視。 評估各模塊的質量指標的用處是雙重的。首先,它使開發者對其維護的模塊負責變得容易。其次,它使得參與由若干開發者集中短期時間來提高某個特定模塊的質量的清理行動成為可能。當集中精力于工具箱的一小部分的時候,它使我們更容易看到我們的努力、并且更容易地保持開發者的參與、受到激勵。 對于重新迭代,我們發現工具箱架構反映了社區的組織,以及在有些情況下,被用于軟件的持續成長和質量控制的過程。 ### 9.2.3 數據管線 多數圖像分析任務具有的階段性特征很自然地導致我們選擇了數據管線架構作為數據處理的基礎設施。數據管線是下列成為可能: * *濾波器串聯*:若干圖像濾波器可以一個接一個的串聯起來,組成一個處理鏈,它可以對輸入圖像進行一系列的操作。 * *參數探測*:一旦處理鏈組合在一起,改變鏈中任何一個濾波器的參數就會很容易,并且可以探測改變參數會對最終的輸出圖像產生什么影響。 * *內存動態載入*:數據量大的圖像可以通過每次只處理該圖像的一部分來管理。利用這種方法,處理數據量大的圖像就成為可能,否則,這種圖像將無法載入內存。 圖9.1和9.2已經從圖像處理的角度展示了一種數據管線的簡化表示。圖像濾波器一般都具有數值型參數,用于調整濾波器的行為。每次有參數發生變更,數據管線就會將其輸出標記為“臟的”,并且知道這個濾波器及其下游使用它的輸出的各濾波器應該重新執行。管線設施的特性減少了探測參數空間的困難,同時為實驗中的各個示例分配最少的處理能力。 更新管線的過程可以通過每次只處理圖像的一部分的方式來驅動。這是一種對于支持動態載入處理功能來說很有必要的機制。實踐中,該過程被一種從一個`RequestedRegion`規范的內部傳遞所控制,這種傳遞過程將規范從下游的濾波器傳向其上游的濾波器。這種通信過程是通過一個內部API來實現的,并且可供應用程序開發者直接調用。 舉一個更具體些的例子,如果一個高斯模糊圖像濾波器以一幅由中值濾波器處理過的100×100像素的圖像作為輸入,那么該模糊濾波器可以向中值濾波器請求只處理原圖像的四分之一,也就是說,一個大小為100×25像素的圖像區域。該請求還會繼續向上游傳播,同時警告沿途各濾波器為了生成請求中規定大小的圖像區域,將不得不向圖像區域的尺寸附加邊界。后面還將講述更多關于數據流的內容。 不論是對給定濾波器的參數做出的改變,還是該濾波器所要處理的特定請求區域所做的改變,都會將管線標記為“臟的”、并提示管線的下游濾波器需要重新執行。 ### 9.2.3.1 過程與數據對象 有兩種主要的對象類型被設計用于存儲管線的基本結構。它們是`DataObject`和`ProcessObject`。`DataObject`是承載數據的類的抽象;例如:圖像和幾何網格。`ProcessObject`為處理上述數據的圖像濾波器和網格濾波器提供抽象。`ProcessObject`以`DataObject`為輸入,并對其進行某種算法變換,例如圖9.2中的那些。 `DataObject`是由`ProcessObject`生成的。這個鏈條通常自從磁盤讀取`DataObject`開始,例如通過使用一種`ProcessObject`類型的`ImageFileReader`。唯一能夠修改某個`DataObject`的就是生成該`DataObject`的`ProcessObject`。輸出的`DataObject`一般連入管線中下游的濾波器作為它們的輸入。 **圖9.5:`ProcessObject`與`DataObject`之間的關系** 這種序列關系如圖9.5所示。同一個`DataObject`可能會傳給多個`ProcessObject`作為它們的輸入,如圖中所示,`DataObject`由管線開端的文件reader生成。在這種特定情況下,文件reader是`ImageFileReader`的實例,而它所生成的、作為其輸出的`DataObject`是`Image`類的一個實例。某些濾波器需要兩個`DataObject`作為輸入也是很平常的現象,比如上圖中右半部出現的相減濾波器就是這樣的例子。 `ProcessObject`和`DataObject`連接起來 構建管線的副作用。從應用程序開發者的角度來看,管線是通過涉及到一連串的調用連接起來的,如: ~~~ writer->SetInput(canny->GetOutput()); canny->SetInput(median->GetOutput()); median->SetInput(reader->GetOutput()); ~~~ 然而在內部,連接在一起的并非以這中一連串的`ProcessObject`,而是下游的`ProcessObject`與其上游`ProcessObject`生成的`DataObject`。 管線內部的鏈條結構通過三種類型的連接維持在一起: * `ProcessObject`保有一系列指向其輸出的`DataObject`的指針。`ProcessObject`擁有并控制著其生成的`DataObject`。 * `ProcessObject`保有一系列指向作為其輸入的`DataObject`的指針。輸入的`DataObject`由上游的`ProcessObject`擁有。 * `DataObject`保有指向生成它的`ProcessObject`的指針。該`ProcessObject`正好還擁有和控制著這個`DataObject`。 這些內部鏈接隨后被用于在管線內部向上游或下游傳遞調用信息。在所有這些互動過程中,`ProcessObject`都保持對其所生成的`DataObject`的控制和所有權。下游的濾波器通過指針的鏈接來獲得對一個給定`DataObject`的信息的訪問權限,這種鏈接是由一連串的對`SetInput()`和`GetOutput()`的調用建立起來的,它甚至無需獲得對輸入數據的控制。出于實踐的目的,濾波器應當將其各自的輸入數據看作是只讀的對象。這一點在API中通過在`SetInput()`方法的變量中使用C++的`const`關鍵字得到了加強。作為一個通用的規則,ITK還是包含了一個const-correct的外部API,盡管從內部來看,這種const-correctness被某些管線操作重載。 ### 9.2.3.2 管線類層次 ![enter image description here](http://www.ituring.com.cn/download/01REyixB6cv1) **圖9.6:`ProcessObject`和`DataObject`的類層次** ITK中的數據管線的最初設計與實現是從VTK衍生而來的,其時,VTK已然是一個成熟的項目,而ITK的開發才剛剛開始(見《開源軟件架構,第I卷》)。 圖9.6展示了ITK管線對象面向對象的類層次。特別注意基礎的`Object`,`ProcessObject`,`DataObject`,以及濾波器家族和數據家族的一些類。在這個抽象層次上,任何被用來做某一濾波器的輸入、或是某一濾波器的輸出的對象,必須派生自`DataObject`。所有產生數據和消耗數據的對象,都應該派生自`ProcessObject`。數據通過管線的流動一部分由`ProcessObject`實現,一部分由`DataObject`實現。 `LightObject`和`Object`位于`ProcessObject`和`DataObject`形成的二叉樹之上。`LightObject`和`Object`提供諸如用于`Events`通信的API的公共功能,以及對多線程的支持。 ### 9.2.3.3 管線的內部工作原理 圖9.7給出了UML時序圖,描述了在一條由`ImageFileReader`,`MedianImageFilter`和`ImageFileWriter`所組成的最小管線中,`ProcessObject`和`DataObject`之間的交互。 完整的交互過程由四個環節組成: * 更新輸出信息(上游的調用時序) * 更新請求的區域(上游的調用時序) * 更新輸出數據(上游的調用時序) * 生成數據(下游的調用時序) ![enter image description here](http://www.ituring.com.cn/download/01RFFotj1cYA) **圖9.7:UML時序圖** 當應用程序調用管線中最后一個濾波器的`Update()`方法時,整個流程即被觸發;在這個具體的例子當中,這個濾波器就是`ImageFileWriter`。`Update()`調用指向上游方向以初始化第一階段。也就是說,從管線中的最后一個濾波器起,朝向管線中的第一個濾波器。 第一個環節的目的是為了查詢這樣的問題,“你能為我生成多少數據?”這個問題轉化為代碼就是`UpdateOutputInformation()`。這個方法中,各個濾波器根據其輸入中的可用數據量來計算可作為輸出的圖像數據量。考慮到必須在該濾波器回應輸出數據量之前獲知輸入數據量,這個問題就得傳導至上游的濾波器,一直傳至某個能夠回應該問題的源濾波器。在這個示例中,源濾波器就是`ImageFileReader`。它能夠通過從其所讀入的圖像文件收集信息,得出其輸出的數據大小。一旦管線中的第一個濾波器對問題做出了回應,該濾波器下游的一系列濾波器就能夠依次計算其各自的輸出數據量,并一直運行至管線中的最末一個濾波器。 第二個環節的處理方向也是向上游方向的,用于告知各濾波器應該輸出的數據量,此數據量是管線運行過程中所需要的。*Requested Region*是支持ITK的流處理能力的基本概念。它使“告知濾波器不要生成整個完整圖像、而只是關注圖像的某個子區域(即:Requested Region)”成為可能。這在手頭的圖像大于系統內存的時候是非常有用的。調用請求從最后一個濾波器傳導至第一個,在途中的每個濾波器,requested region的尺寸都會被修正,這些修正要考慮到該濾波器輸入中可能需要的任何附加的邊界,這樣該濾波器才能生成給定區域尺寸的輸出。在我們的這個示例中,中位數濾波器一般會向其輸入中加入2-像素的邊界。也就是說,如果writer向中位數濾波器請求一個500×500尺寸的區域,那么中位數濾波器就會相應地向reader請求一個502×502尺寸的區域,因為中位數濾波器在缺省情況下計算一個輸出像素,需要一個3×3像素的鄰域。這個環節被寫入`PropagateRequestedRegion()`方法。 第三個環節要觸發Requested Region內的數據的計算。該環節的處理方向也是向上游,它被定義為`UpdateOutputData()`方法。由于各個濾波器在其計算出輸出結果之前都需要輸入數據,本環節的調用請求先向其上游的濾波器傳遞,然后再向上游傳導。然后返回到實際進行數據計算的當前濾波器。 第四個環節(最后一個環節)的處理方向是向下游的,它由每個實際執行運算的濾波器組成。該環節被寫為`GenerateData()`。下游方向并不是一個濾波器向其下游發送調用請求的結果,而是`UpdateOutputData()`的調用以從管線中的第一個濾波器到最后一個的順序執行。也就是說,所發生的下游方向的順序,要歸因于調用的時機,而不要歸因于什么濾波器在驅動這一調用。這個說明是很重要的,因為ITK的管線從本質上講是*Pull Pipeline*,其中的數據是管線的末端所請求的,而且這種邏輯也是由管線的末端來控制的。 ### 9.2.4 工廠 ITK的基礎設計需求之一是提供多平臺支持。這一需求出現于追求使該工具箱的影響最大化,通過使工具箱能夠為社區所廣泛使用,而無需考慮其各自的平臺。ITK采用*工廠*設計模式來應對這樣的挑戰:支持多種不同硬件和軟件平臺、而不犧牲一個解決方案在不同平臺上的實用性。 ITK中的工廠模式使用類的名稱作為向構造函數注冊的鍵值。工廠的注冊在運行時進行,這一過程可以在ITK應用程序啟動時,通過簡單地將動態鏈接庫放入指定路徑來完成。后一種特性提供了一種以干凈、透明的方式實現插件架構的基本機制。其影響是減少可擴展圖像分析應用程序的開發難度,同時滿足了提供持續成長的圖像分析能力的需要。 ### 9.2.5 IO工廠 工廠機制對于IO操作尤為重要。 ### 9.2.5.1 以外觀模式擁抱多樣性 圖像分析社群開發了非常多的文件格式來儲存圖像數據。這些文件格式中的大多數都是為了滿足特定的需要而設計和實現的,因此為支持特定類型的圖像而進行了微調。結果,新的文件格式定期涌現并推廣到這個社群。注意到這一形勢,ITK開發團隊設計了一個IO架構,適于減輕擴展性工作,向這樣的架構中定期添加越來越多的文件格式是簡單的。 ![enter image description here](http://www.ituring.com.cn/download/01RYAa9tv2un.small)[[+]查看原圖](http://www.ituring.com.cn/download/01RYAa9tv2un.big) **圖9.8:IO工廠的依賴關系** 這個IO可擴展架構建立在上一部分所述的工廠機制的基礎上。主要的不同點就是在IO情形中,IO工廠在一個由基類`ImageIOFactory`所管理的特殊的注冊機制中實現注冊,如圖9.8中左上角所示。從圖像文件格式中讀寫數據的實際功能又類族`ImageIO`來實現,見圖9.8中的右側。這些服務類在使用者請求讀入或者寫出一個圖像時被初始化。這些服務類不直接暴露給應用程序代碼。反之,我們希望應用程序與下列外觀類(facade)進行交互: * `ImageFileReader` * `ImageFileWriter` 應用程序可以通過類似下列的代碼來調用這兩個類: ~~~ reader->SetFileName("../image1.png"); reader->Update(); ~~~ 或者, ~~~ writer->SetFileName("../image2.png"); writer->Update(); ~~~ 這兩種情形中,對`Update()`的調用觸發了這些`ProcessObject`所連接的管線上游的執行。reader和writer的行為類似管線中有多了一個濾波器。在reader的特例中,對`Update()`的調用觸發了將相應圖像文件讀取到內存的操作。在writer的情形中,對`Upadate()`的調用觸發了為writer提供輸入的上游管線的執行、并最終將圖像結果以一種特定的文件格式寫到磁盤中。 這些外觀類將應用程序開發人員與各文件格式所固有的、內部的不同隔離開來。這些外觀類甚至把文件格式的存在性本身也隔離了起來。這些外觀以這樣一種方式設計:應用程序開發人員大多數時候無需了解應用程序需要讀入的文件格式。典型的應用會簡單地調用如下代碼: ~~~ std::string filename = this->GetFileNameFromGUI(); writer->SetFileName( filename ); writer->Update(); ~~~ 不管變量`filename`的內容是否是下列串中的任何一個,這些調用都能正常工作: * image1.png * image1.jpeg * image1.tiff * image1.dcm * image1.mha * image1.nii * image1.nii.gz 其中文件擴展名標識各種情形中不同的圖像文件格式。 ### 9.2.5.2 獲知像素類型 盡管有文件reader和writer外觀所提供的支持,我們還是得依賴應用程序開發人員來確定應用程序所需處理的像素的類型。在醫學影像處理工作中,指望應用程序開發人員知道輸入圖像是否含有MRI(核磁共振成像,Magnetic Resonance Imaging)、乳腺透視(mammogram)、還是CT(計算斷層掃描,Computed Tomography)掃描是合理的,因此,為不同成像模態要選擇合適的像素類型和圖像幾何維度都要銘記于心。在使用者想要讀取任意類型圖像的場合中,圖像類型的特異性對于應用程序想的設置就可能帶來不便,這種現象經常在快速樣機開發和教學的環境中出現。然而,在臨床上,部署醫學成像應用程序時,我們希望像素類型和圖像的幾何維度都根據成像模態清楚的定義出來。舉一個具體的例子,對于一個管理三維MRI掃描的應用程序,形如: ~~~ typedef itk::Image< signed short, 3 > MRImageType; typedef itk::ImageFileWriter< MRImageType > MRIWriterType; MRIWriterType::Pointer writer = MRIWriterType::New(); writer->Update(); ~~~ 然而,這里存在一個限度,那就是:能有多少圖像文件格式的特性與應用程序開發人員隔離。例如,當我們從DICOM文件中讀取圖像時,或者讀取RAW圖像時,應用程序開發人員可能插入額外的調用,以進一步指明手頭文件格式的特點。DICOM文件是在臨床環境中最為普遍的,而RAW圖像對于研究領域中交流數據,仍然是“食之無味、棄之可惜”。 ### 9.2.5.3 和而不群 每一個IO工廠和ImageIO服務類的自包含特性也反映在模塊化上。典型地,一個ImageIO類依賴一個特定的庫,這個庫專門管理某個特定的文件格式。例如,PNG,JPEG,TIFF,以及DICOM。在那些情形中,第三方庫以自包含的模塊的形式被管理,而特定的、用于為ITK提供該第三方庫接口的ImageIO代碼也加入到這個模塊中來。這樣,特定的應用程序可能會禁掉一部分許多與其所在領域無關的文件格式,以集中提供那些對該應用程序的既定場合有用的文件格式。 正如標準的工廠一樣,IO工廠可以在運行時從動態鏈接庫中載入。這種靈活性促進了特定場合和內部開發文件格式的使用,而無需將所有這樣的文件格式都直接整合到ITK工具包本身中去。可載入的IO工廠一直是ITK架構設計中最為成功的特性之一。它使不用向代碼添加負擔或者使其實現變得含混、就能便捷地管理一個富有挑戰性的情況成為可能。最近,類似的IO架構已經被采用,以管理那些讀取和寫出含有空間變換的文件的過程,這些空間變換由類族`Transform`所表示。 ### 9.2.6 流 ITK最初的誕生是一組圖像處理工具,為了滿足“可視人計劃”的需要。那時很明確的是,這樣巨大的數據集是不能像醫學影像處理研究社群的典型做法那樣、被載入計算機的RAM中的。這樣的數據集同樣也無法載入到我們今天所使用的臺式機的內存中。于是,開發ITK的需求之一就是能夠使圖像數據在數據管線中流動。更具體點,就是能夠以壓入圖像的子塊通過數據管線的方式處理巨大的圖像,然后將管線輸出端得到的塊再組裝起來。 ![enter image description here](http://www.ituring.com.cn/download/01RYAaC7XMyo.small)[[+]查看原圖](http://www.ituring.com.cn/download/01RYAaC7XMyo.big) **圖9.9:圖像流處理示意圖** 這種對圖像區域的劃分展現在圖9.9中的中位數濾波器的例子。中位數濾波器計算一個輸出像素,并將其作為輸入圖像中該像素某鄰域內像素值的統計中位數。該鄰域的大小是濾波器的一個數值參數。我們將其設定為2像素,意思就是我們將取那個輸出像素周圍以2個像素為半徑的區域作為鄰域。這樣就得到了一個5×5像素的鄰域,該輸出像素就位于這個鄰域的正中,被一個邊長為2像素的的矩形包圍。這個半徑通常被稱為Manhattan半徑。當中位數濾波器收到計算輸出圖像中一個指定的Requested Region的請求時,它就會向其所在管線的上游濾波器發出請求,要其提供一個將Requested Region邊界(在我們這個例子當中,就是2像素)放大一倍的區域。在圖9.9中的情形中,當被要求處理尺寸為100×25像素的Region 2時,中位數濾波器將這個請求傳遞到其上游濾波器,要求對方提供尺寸為100×29像素的區域。這個29像素的縱向尺寸是由25像素加上該尺寸兩端各加2個像素得到。注意到橫向尺寸并沒有被放大,這是由于在該示例中,100像素的橫向尺寸已經是輸入圖像所能提供的最大尺寸了;于是,104像素的橫向尺寸放大請求(100像素加上該尺寸兩端各加2個像素)就被裁減為圖像在該方向的最大尺寸,也就是100像素的橫向尺寸。 在鄰域上計算的ITK濾波器通過三種典型的方法來處理邊界條件:看圖像外部是否存在null值,或者將像素值關于圖像邊界做鏡像,或者重復賦予圖像外部以邊界值。在中位數濾波器的情形中,我們采用了零通量Neumann邊界條件,意思只是區域邊界以外的像素被假設為邊界上最后一個像素的重復。 在圖像處理文獻中,有一個心照不宣的小秘密,那就是圖像濾波器的實現難點通常是與對邊界條件的恰當處理相關的。這是切斷多數教科書中所提供的理論訓練與圖像處理的軟件實踐的一個特殊癥狀。在ITK中,這個問題通過實現一大批圖像迭代器和相關的邊界條件計算器族來解決。這兩組helper類將圖像濾波器與處理N維邊界條件問題的復雜性隔離開來。 流處理過程由濾波器之外的設施驅動,典型的驅動來源就是`ImageFileWriter`和`StreamingImageFilter`。這兩個類實現了將完整尺寸的圖像投入處理、并將其劃分為一系列由應用程序開發人員所要求的分區的流功能。而后,當調用它們的`Update()`的過程中,它們會進入查詢圖像的每一個處于中間階段的分區的迭代循環。在該過程中,它們利用圖9.7所述的`SetRequestedRegion()`API。這樣就將管線上游的計算約束在圖像的一個子區域中。 驅動流處理過程的應用程序代碼如下: ~~~ median->SetInput( reader->GetOutput() ); median->SetNeighborhoodRadius( 2 ); writer->SetInput( median->GetOutput() ); writer->SetFileName( filename ); writer->SetNumberOfStreamDivisions( 4 ); writer->Update(); ~~~ 其中,唯一一個新要素是`SetNumberOfStreamDivisions()`的調用,它定義了為了將圖像進行流處理而進行的分區的數量。為了與圖9.9中的示例對應,我們把要劃分的區域的數量設定為4。這就是說`writer`將要四次觸發`mdian`濾波器開始執行,每次一個不同的Requested Region。 在流處理過程和給定濾波器的并行執行過程之間,有些很有趣的相似之處。兩者都依賴于將圖像處理工作分解為將大塊的圖像數據進行劃分,然后再分別進行處理的可能性。在流處理過程中,大塊的圖像數據依照時間的先后順序,依次進行處理;而在并行處理過程中,大塊的圖像數據被分配給不同的線程,再挨個分配給CPU的各核。最終,這一切將由濾波器的算法特性來決定是否能夠將輸出圖像劃分為成塊的數據、從而得以進行基于對應輸入圖像的塊數據的獨立計算。在ITK中,有API處理流過程、還有獨立的API專門為基于多線程和共享內存的并行計算的實現提供支持,從這個意義上講,流和并行事實上是正交的。 不走運的是,流并不能應用于所有類型的算法。不適用流的特例有: * 迭代算法,為了在每步迭代中都計算一個像素值,作為其輸入像素鄰域的像素值。多數基于PDE求解的算法就是這種情況,比如:各向異性擴散,demons形變配準,以及稠密水平集。 * 需要所有輸入像素的像素值以計算其中一個輸出像素的像素值的算法。Fourier變換和IIR(Infinite Impulse Response,IIR)濾波器,像是遞歸高斯濾波器就是這種類型。 * 區域傳導或者前線傳導算法中對像素的修改也以迭代的方式進行,但是這些算法不能通過可預測的方式將區域或者前線的位置系統地劃分給分塊的數據。區域生長分割,稀疏水平集,一些數學形態學操作的實現,以及某些形式的分水嶺,就是典型的例子。 * 圖像配準算法,注意,它們需要在其優化循環的每步迭代中訪問整個輸入圖像來計算度量值。 所幸的是,另一方面,所有濾波器都創建其自身的輸出,于是它們就不會覆蓋內存中的輸入圖像,ITK的數據管線結構通過利用這一事實帶來的優勢,能夠支持多種變換濾波器的流功能。這是以消耗內存為代價的,因為管線得同時為輸入圖像和輸出圖像分配內存空間。象翻轉,軸排列,以及幾何重采樣均屬此列。在這些情形中,數據管線通過要求每個濾波器都提供一個名為`GenerateInputRequestedRegion()`的方法來管理輸入區域與輸入區域的匹配,該方法以矩形的輸出區域為參數。這個方法計算矩形輸入區域,這是濾波器為計算指定的矩形輸出區域所需要的。 更精確點,我們必須說ITK支持流——但僅僅支持那些具有“可成流”特性的算法。就是說,從積極意義上講,考慮其余的算法的時候,我們應當在這里修飾一下我們的聲明,不能是“不可能在這些算法上用流”,而是“我們處理流的典型方法不適用于這些算法”,同時我們期待將來社群中能夠設計出新的技術來解決這些問題。 ## 9.3 經驗教訓 ### 9.3.1 可復用性 可復用性的原則也可以理解為“避免冗余”。在ITK中,這一原則通過如下三個方面來實現: * 首先,采用面向對象編程,尤其是類層次結構的合理創建,其中的共同功能都被分解組織到各個基類當中。 * 第二,采用泛型編程,這通過C++模板的大量使用來實現,同時將那些以模式來表示的行為分解開來。 * 第三,C++宏的放開使用也使整個工具包中無數地方都需要的標準代碼段的重用成為可能。 許多這些事項聽起來像是老生常談,而且在今天看來是顯然的,但是當1999年ITK的開發工作開始時,其中的某些事項并非如此明顯。尤其是,那時大多數C++編譯器對模板的支持并沒有嚴格遵循一個一貫的標準。即使今天,在社群中做出采用泛型編程并且使用廣泛模板化的實現的決策仍然是充滿爭議的。在社群中,這表現于對通過Python,Tcl,或者Java的包裹層來使用ITK的偏好。 ### 9.3.1.1 泛型編程 泛型編程的采用是ITK定義的實現特點之一。在1999年,這是一個艱難的決定,那時編譯器對C++模板的支持是相當碎片化的,而標準模板庫(STL)看起來仍然有些不容易被接受。 在ITK中,泛型編程的采用,是通過擁抱使用C++模板來實現概念的泛化來實現的,這種方式還能提高代碼的重用。ITK中C++模板參數化的典型是類`Image`,可以通過下面的方式實例化: ~~~ typdef unsigned char PixelType; const unsigned int Dimension = 3; typedef itk::Image< PixelType, Dimension > ImageType; ImageType::Pointer image = ImageType::New(); ~~~ 這種表達式中,應用程序開發人員選擇用于表示圖像像素的數據類型,還有圖像的幾何維度,這個圖像就像是空間中的一個網格。在這個特例中,我們選擇使用`unsigned char`來代表三維圖像中的8位的像素。多虧有為此相關的泛型實現,才可能實例化ITK中的任意像素類型和任意幾何維度的圖像。 為了能夠寫這些表達式,ITK開發人員得非常謹慎地根據對像素類型的假設來實現類`Image`。一旦應用程序開發人員實例化了圖像類型,開發者就能夠創建那種類型的對象,或者繼續實例化圖像濾波器——它們的類型也依賴圖像的類型。例如: ~~~ typedef itk::MedianImageFilter< ImageType, ImageType > FilterType; FilterType::Pointer median = FilterType::New(); ~~~ 不同圖像濾波器的算法特性限制其能夠支持的實際像素類型。例如,某些圖像濾波器預期圖像像素類型為整數標量類型,而另外一些濾波器則期望像素類型是由浮點數組成的矢量。當這些濾波器被不當的像素類型實例化時,它們就會生成編譯錯誤或者得出錯誤的計算結果。為防止不正確的實例化并使編譯錯誤的故障排除變得容易一些,ITK采用了*概念檢查*,它基于對類型的某些預期的特定功能強制進行檢查,以生成早期錯誤、并具有便于人類閱讀的錯誤消息為目的。 在工具包的一些部分中,C++模板也以模板元編程的方式被使用,這樣做是以提高代碼的運行時速度性能、尤其是展開那些控制低維矢量和矩陣的計算的循環為目的的。諷刺的是,我們已經多次發現一些編譯器在判斷何時展開循環方面已經變得更加靈巧,并且在一些案例中,已經不再需要模板元編程表達式的幫助。 ### 9.3.1.2 知道何時停止 在這里,也存在著“好事過頭”的風險,就是說,過度使用模板和過度使用宏的風險。很容易走得太遠,而后以在C++的基礎上生造了一門新的語言收場,這門語言本質上是基于模板和宏的使用而產生的。這是一條恰當的邊界,它要求來自開發團隊持久的關注,以確保語言特性的合理使用而非濫用。 舉一個具體的例子,通過C++的關鍵字`typedef`顯式地命名類型這一做法的廣泛使用,已經證明是尤其重要的。這種實踐起兩種作用:一方面,它提供利于人類閱讀的富有信息的名稱來描述類型的天然屬性及其目的;另一方面,它確保類型在整個工具包中的使用是相容的。例如,在為其4.0版進行重構的過程中,在收集諸如`int`,`unsigned int`,`long`和`unsigned long`等C++整數類型在何處使用、并將其取代上投入了大量的精力,取代這些整數類型的類型名稱,根據與該類型所表示的變量相關聯的概念命名。這是任務中代價最大的部分,為的是確保工具包能夠利用64位數據類型的優勢,在所有平臺上處理大于4G的圖像數據。這個任務對于ITK在顯微鏡和遙感領域的推廣至關重要,在這些領域中,數十G的圖像數據是很普遍的。 ### 9.3.2 可維護性 ITK的架構滿足使維護成本最小化的約束。 * 模塊化(類級別) * 許多小文件 * 代碼復用 * 重復性模式 這些特性以下列方式降低維護成本: * 模塊化(類級別)使強制執行圖像濾波器級別,或ITK的類級別的測試驅動的開發技術成為可能。應用于小規模、模塊化的代碼片段的嚴格的測試規定具有減少bug可能隱藏其中的代碼的優勢,而且具有模塊化所帶來的自然的解耦性,這樣,定位并消除缺陷的工作就變得非常容易了。 * 許多小文件使將某部分代碼布置給某個開發人員變得容易,并且簡化了缺陷的跟蹤,它們都與版本控制系統中的某個特定的commit相關聯。保持小文件的規定也引起了函數和類的金規則的強制執行:Do on thing, and do it right。 * 代碼復用:當代碼被復用(而不是被“復制-粘帖”和再實現)時,代碼本身就從更高層次的審查中獲益,這種審查是由于其(被復用的代碼——譯注)在多種不同的環境被使用所引起的。這使更多的眼睛能夠注視著這些代碼,或者至少注視著這些代碼的效果,因此這些代碼得益于Linus定律:“Given enough eyeballs, all bugs are shallow.” * 重復性模式簡化了維護人員的工作,在現實中,他們負責項目生命期中超過75%的軟件開發成本。使用相互兼容地在代碼中的不同位置重復的編碼模式,使開發人員打開一個文件就能夠快速理解這段代碼在做什么、或者想做什么變得非常容易。 當開發人員牽涉到常規維護活動中時,他們會面臨一些“常見失敗”,尤其是: * 某些濾波器為其輸入和輸出圖像制造相應的特定像素類型的假設,但是這并不是通過類型或者概念檢查所強制進行的,而且也沒有在文檔中指出。 * 代碼的可讀性不強。這是對任何新算法實現的軟件的最普遍挑戰之一,這些新算法源自于研究社群。在那種環境中,寫出“能工作”的代碼,而無視代碼的目的不僅僅在于運行時的執行、更在于其非常便于下一個開發人員閱讀是很正常的。典型的書寫“干凈的代碼”的良好規則——舉個例子,寫較小的做且僅做一件事的函數(單一責任原則和最小意外原則),以及對變量和函數的恰當命名——往往容易被研究人員因看到他們嶄新的算法能夠正常工作而忽視。 * 忽視失敗情形和錯誤管理。關注于數據處理的“nice cases”而未能提供用于管理所有可能出錯的情形的代碼是很普遍的。采用了工具包的人一旦開始開發和部署實際的應用程序,就迅速地扎到這些情形中去。 * 不充分的測試。它需要許多規則來跟進測試驅動的開發實踐,尤其是先寫測試以及只實現那些你要測試的功能的觀點。代碼的bug幾乎總是隱匿在這些在實現測試代碼過程中所略過的情形中。 多虧了開源社群的傳播實踐,這些問題中的多數已經由于通過郵件列表中經常被問及的問題所暴露出來,或者由使用者直接報告bug而終結。在處理了許多這類問題后,開發人員認識到應該編寫“利于維護”的代碼。某些這類特性同時應用于代碼風格和代碼的實際組織。我們的看法是,一個開發人員只能通過花費時間——至少一年——做維護工作并暴露在“所有的東西都有可能出錯”這一事實面前,來達到掌握的程度。 ### 9.3.3 無形的手 軟件應該看起來像是由一個人寫出來的那樣。最好的開發人員是這樣的人,should they be hit by the proverbial bus,他們寫的代碼能夠被他人順利接管。我們逐漸認識到任何“人身接觸”的蛛絲馬跡都是軟件中被引入缺陷的跡象。 為了強制推行代碼風格的統一,下列工具已被證實是非常有效的: * 用于源代碼風格自動檢查的`KWStyle`。這是一種簡化的C++解析器,用于檢查代碼風格并標出任何與該風格沖突的地方。 * 用于常規代碼審查的`Gerrit`。該工具服務于兩個目的:一方面,它防止不成熟的代碼進入代碼庫,這是以抽取其在迭代審查周期過程中的錯誤、缺陷、和不完善的方式來實現的,在這個周期當中,其他開發人員通過貢獻以提高代碼的質量。另一方面,它提供一個虛擬的訓練營,在其中新的開發人員能夠向更有經驗(這里所謂的“有經驗”是指*犯過所有的錯誤、并且知道尸體埋在哪兒……*)的開發人員學習如何提高代碼的質量,和如何避免那些在維護周期中被發現的已知問題。 * Git hooks促使了KWStyle和Gerrit的強制推行,并且還能夠承擔一些本該由它們自己負責的檢查工作。例如,ITK利用Git hooks來防止帶有tab和結尾空格的代碼提交。 * 團隊也已摸索了`Uncurstify`的使用,它作為強制使用一個相容風格工具。 值得強調的是風格的統一性并不簡單地處于審美情趣,它實際上是一種經濟方面的考慮。對軟件項目的*所有者的總成本*(TCO,Total Cost of Ownership)的研究估計,在一個項目的的生命周期中,維護的成本大約是75%的TCO,而考慮到維護成本主要以年度為基準,它一般會超出初始開發成本計劃所給出的成本,這個初始開發成本是一個軟件項目的生命周期中前五年的總成本。(見*"Software Development Cost Estimating Handbook"*,第I卷,Naval Center for Cost Analysis, Air Force Cost Analysis Agency, 2008。)維護估計要占到一個軟件開發人員實際工作的大約80%,而當忙于維護的時候,開發人員的大部分時間都被用于閱讀他人的代碼,試圖看懂這些代碼的意圖(見*Clean Code, A Handbook of Agile Software Craftsmanship*,Robert C. Martin,Prentice Hall,2009)。統一的風格確實想減少開發人員將自己沉浸于一個新近的開源文件、并在對該文件做出任何修改之前理解其中的代碼的工作中所花費的時間。出于同樣的原因,統一的風格降低了開發人員嘗試修復舊有的bug時、由于對代碼的誤解并隨之做出的引入新的bug的修改的概率(*The Art of Readable Code*,Dustin Boswell,Trevor Foucher, O'Reilly,2012)。 使這些工具有效的關鍵在于確保它們: * 能夠為每一個開發人員所使用,因此我們傾向于開源工具。 * 能夠運行于一個正規的基礎上。在ITK中,這些工具已被整合到由CDash管理的每日構建和Continuous Dashboard構建中去。 * 盡可能緊密地運行于代碼所寫的地方,這樣變動就能被立即修復,開發人員就能快速查到哪種做法破壞了風格規則。 ### 9.3.4 重構 ITK始于2000年,并持續發展至2010年。2011年,幸虧融入了聯邦資助基金,開發團隊才有了真正的專門的機會進行重構的努力。該基金由國家醫學圖書館提供,作為美國恢復和再投資法案(ARRA,American Recovery and Reinvestment Act)所發倡議的一部分。這不是一個小小的承諾。想象一下你一直致力于一個軟件超過十年時間,然后你獲得了一個把它清理干凈的機會;你該改動些什么呢? 這個做廣泛重構的機會十分難得。在之前的十年里,我們依賴于每天的努力來進行小規模的、局部的重構,清理那些我們走進的特殊的角落。這個持續的清理和提高過程利用了開源社群的大規模協作的優勢,該過程由CDash驅動的測試基礎設施確保安全,此基礎設施通常進行工具包中84%的代碼的測試。注意,與此相反,軟件工業的平均測試覆蓋率估計只有50%。 在重構的努力過程里被改動的許多事物當中,與架構最為相關的有: * 工具包中引入了模塊化 * 整型被標準化 * typedef被修復,從而能夠在所有平臺上進行大于4GB的圖像數據的處理 * 軟件過程被修正: * 從CVS遷移到Git * 利用Gerrit引入代碼審查 * 根據CDash@home的要求引入測試 * 用于下載單元測試所需數據的改進方法 * 廢棄對過時的編譯器的支持 * 對許多IO圖像文件格式的改進支持,包括: * DICOM * JPEG2000 * TIFF(BigTIFF) * HDF5 * 引入支持GPU計算的框架 * 引入視頻處理的支持 * 加入OpenCV橋 * 加入VXL橋 基于遞增修正的維護——諸如為濾波器添加特性、提高一個給定算法的性能等任務——對于特定的C++類的局部改進很奏效。然而,基礎設施的修改需要大規模的重構,這會影響整個工具包中大量的類,像是上面所講到的那些。舉個例子,這些為支持大于4GB圖像的處理所需的變動有可能是迄今為止給ITK所打的最大的補丁之一。它要求對數以百計的類進行修改,并且無法在不經受巨大的痛苦的情況下完成。模塊化是這個任務中的另一個實例,它并沒有增量地完成。這確實影響了整個工具包的組織,它的測試基礎設施是如何工作的、測試數據是如何被管理的、工具包是如何被打包并發行的、以及新的代碼貢獻將如何被封裝以添加的未來的工具包中。 ### 9.3.5 可再生性 ITK在其早期所接受的教訓之一,就是發表在這個領域的許多論文的實現并不像我們所了解的那么容易。計算領域傾向于過度褒獎算法,而輕視作為“只是實現細節”的這一編寫軟件的實際工作。 那種輕視的態度對這個領域具有相當的破壞性,因為它貶低了通過編寫代碼和恰當的使用它而獲得的第一手經驗的重要性。后果是大多數發表的論文就是不能重現,而且當研究人員和學生想使用這些技術的時候,他們都以花費了大量時間在這一(重現)過程中、并且引入了對原作的變動而結束。在實踐中,要驗證一個實現是否與一篇文章中所描述的內容是否契合,確實是相當困難的。 ITK出于良善的目的,破壞了那種環境,并且在這樣一個領域恢復了一種DIY文化,這個領域已經變得習慣于理論推理、并且已經樹立起輕視實驗工作的風氣。由ITK帶來的新文化是一種實踐的、實用主義的文化,這種文化中,軟件的性能的判定是基于其自身的實踐結果的,而不是基于其自身看起來所具有的復雜性,這種復雜性被許多科學出版物推崇備至。事實證明,在實踐中,最有效的處理方法恰恰是那些看起來太簡單而不能以科學論文的形式被接受的方法。 可重現的文化是測試驅動型開發哲學的一種延續,并且有條不紊地做出更好的軟件;更高的清晰度,可讀性,魯棒性,以及專注的方向。 為了填補缺乏可重現出版物的空白,ITK社群創建了Insight Journal。它是可以公開訪問的、完全在線的出版物,它要求投稿都要包含代碼,數據,參數,和測試,使可重現性的驗證成為可能。文章在提交后的24小時內發表上線。然后社群中的任何成員就能夠對這些文章進行同行評審。讀者能夠獲得隨文章一起的所有材料:源代碼,數據,參數,和測試腳本。這個期刊一直提供一個多產的空間,用于共享新的代碼貢獻,這些代碼貢獻將會在這里走上進入代碼主倉庫的道路。期刊最近收到了它的第500篇投遞文章,還將繼續作為向ITK添加新代碼的正式門戶。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看