DICOM醫學影像處理:WEB PACS初談二,映像的傳輸

來源:互聯網
上載者:User

標籤:排除   標記   val   class   結束   資訊   test   目的   eof   

背景:

        如前一篇專欄博文所述,藉助於CGI或FastCGI技術轉寄瀏覽器發送過來的使用者請求,啟動本地的DCMTK和CxImage庫響應。然後將處理結果轉換成常規映像返回到瀏覽器來實現Web PACS。本博文通過實際的代碼測試來驗證這一模式的可行性,同一時候對C語言編寫CGI指令碼提出了一些問題。

難題:

        計劃參照DCMTK內建工具dcm2pnm.exe的原始碼。通過DicomImage將DCM檔案轉換成BMP檔案,然後利用CGI技術返回到瀏覽器。實現一次簡單的WEB PACS的影像傳輸類比。詳細的代碼例如以下,

// dcmtk-save-test.cpp : 定義控制台應用程式的進入點。//#include "dcmtk/config/osconfig.h"#include "dcmtk/dcmdata/dctk.h"#include "dcmtk/dcmdata/dcpxitem.h"#include "dcmtk/dcmjpeg/djdecode.h"#include "dcmtk/dcmjpeg/djencode.h"#include "dcmtk/dcmjpeg/djcodece.h"#include "dcmtk/dcmjpeg/djrplol.h"#include "dcmtk/dcmimgle/diutils.h"#include "dcmtk/dcmimgle/dcmimage.h"void SendImageDcmtk(char* filename){DcmFileFormat mDcm;mDcm.loadFile(filename);E_TransferSyntax xfer = mDcm.getDataset()->getOriginalXfer();unsigned long mode = CIF_MayDetachPixelData | CIF_TakeOverExternalDataset;DicomImage *di = new DicomImage(&mDcm,xfer,mode,0,0);if(di == NULL){Print2Web("Can not open DCM file by DicomImage!");}printf("Content-Type:image/bmp\n\n");di->writeBMP(stdout,24,0);}int main(int argc ,char* argv[]){char* filename="c:\\test.dcm";SendImageDcmtk(filename);return 0;}

        編譯產生dcm2bmp.exe的CGI程式,將其複製到網站的CGI檔案夾(我本機地址為c:\wamp\www\c-cgi)中。通過在瀏覽器中輸入http://localhost/c-cgi/dcm2bmp.exe啟動服務端的CGI程式。

儘管程式啟動順利,可是並未獲得我們想要的結果——輸出了一幅奇怪的映像,例如以下所看到的:左圖是在PACS看圖端中看到的真實DCM映像,右圖是我傳輸到瀏覽器的失敗的映像。


驗證測試:

        獲得了錯誤的結果,起初並未想到非常好的排除錯誤的方法。遂決定首先確認問題出現的大致範圍。由於介紹CGI技術的書籍大多都採用Perl或者PHP來實現。因此仿照書籍中的執行個體。利用Perl和PHP來實現一次正常的傳輸映像到瀏覽器的功能,驗證一下該機制是否可行。

以下是實際的測試過程,

(1)Perl版本號碼的CGI

#!c:/Perl64/bin/perl.exeuse warnings;use strict;binmode STDOUT;print "Content-type:image/bmp\n\n";open FILE,'<','c:\test.bmp' or die "Can't open file";while (my $buf = <FILE> ){    print $buf;}close(FILE);

        經過測試。能夠輸出正確的映像。

(2)PHP版本號碼的CGI

<?

php$filename="c:/test.bmp";$size=getimagesize($filename);$fp=fopen($filename,"r");#echo $size['mime'];if($size && $fp){header("Content-type:image/bmp\n\n");fpassthru($fp);exit;}?>

       經過測試。也能夠輸出正確的映像。

結果分析:

        通過上面的兩次測試,足以說明WAMP+CGI/FastCGI的環境搭建沒有問題。因此能夠斷定問題出如今C語言編寫的CGI指令碼程式中。由於CGI指令碼是服務端的控制台程式。能夠再命令列中直接調試,可是我們是利用DicomImage的writeBMP函數將轉換後的bmp映像輸出到了stdout中,實際調試中會輸出一堆亂碼,由於stdout預設是ASCII格式的。所以在命令列中調試CGI指令碼的思路行不通。所以決定從最底層入手,利用RawCap.exe工具。抓取瀏覽器與server端的CGI程式之間的資料包。通過分析資料包期望找到問題出現的地方。

1)RawCap+Wireshark本地抓包+分析

        RawCap的操作在早前的博文中介紹過了,這裡不做具體介紹。在命令列輸入RawCap.exe後選擇[2]介面。即本地迴路127.0.0.1的資料包。就可以開始抓取本地迴路資料包。相同依照博文前面測試CGI的方法,分別調用用C語言編寫的輸出結果錯誤的CGI程式和用PHP編寫的輸出結果正確的CGI程式,抓取的資料包分別為wrongimage.pcap和rightimage.pcap。想結束抓取能夠輸入CTRL+C。

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenNzdXJlcWg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" >

        抓取完畢後,在Wireshark中開啟wrongimage.pcap和rightimage.pcap。此處由於僅僅關心映像傳輸的資料包問題,所以直接使用Wireshark中的統計分析工具。詳細操作例如以下,單擊菜單條中的“Statistic”,選擇會話——Conversations,開啟會話表單:

        隨後單擊TCP協議。選擇當中資料量大的會話。單擊表單下方的Follow Stream。能夠開啟CGI指令碼傳輸映像到服務端的真實資料流。

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenNzdXJlcWg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" >

        同一時候使用Follow Stream跟蹤wrongimage.pcap和rightimage.pcap中的資料流,對照結果例如以下:


        從中能夠看到真實的映像資料轉送流,對照左(正確映像)和右(錯誤映像),能夠看出兩個資料流中都表明自己是BMP檔案,具有0X42 4D的類型標記符。

依據BMP檔案結構可知,隨後是顏色表。如中大的紅色矩形框所看到的。可是細緻觀察能夠看到錯誤映像中的0a 0a 0a 00顏色表項變成了0d 0a 0d 0a 0d 0a 00。通過搜尋錯誤資料流發現,凡是原資料流中出現0a的地方都被替換成了0d 0a。

因此斷定這就應該是映像傳輸失敗的原因。

       為了非常好的理解上述錯誤出現的原因。以下補充一些基礎知識,詳情可參見博文後的網址。

2)知識點補充:(1)文字檔 VS 二進位檔案

        眾所周知。電腦非常二,僅僅認識0和1。不論什麼內容在電腦內都是以0和1的方式儲存的,既然如此為何還區分文字檔和二進位檔案?我是這麼理解的。儘管電腦的底層都是由二進位格式來儲存的,可是我們能夠定製不同的解讀標準。相同的0和1序列。解讀方式不同。表達的含義就不同。事實上這樣的應用不同標準來解讀相同序列的現象在電腦領域是非經常見的。在32位機器中,相同的四位元組01序列。可能表示不帶正負號的整數或者有符號整數(在C/C++語言中),也可能表示一個IP地址(在socket編程中)。也可能表示標籤或分隔字元(在DICOM協議中的對象的標籤都是採用四位元組格式,如0x0002 0010代表的是TransferSyntax UID)。豐富多彩、變幻無窮的資訊世界源於不同的解讀標準或解讀規則。所以學習過程中要瞭解標準,瞭解實際的應用情境

        文字檔和二進位檔案能夠理解為應用不同標準儲存的01序列,文字檔指的是全部資訊都以ASCII格式儲存。每一個位元組都相應到一個ASCII字元——ASCII是人們可直接讀出來的(當然這個我們能夠識別的文字也是在電腦內部經過了多次轉換而得來的。能夠簡單地理解為針對不同的01序列,電腦向螢幕繪製相應的圖形——圖形的產生能夠簡單的理解為多個相鄰的晶體發光來實現的);而二進位檔案指的是將實際的01序列原封不動的儲存,而不加不論什麼處理(這也算一種解讀方式吧)。所以之所以要區分文字檔和二進位檔案就是一種聲明,一種告知01序列被解讀方式的聲明。打個不恰當的比喻,01序列就像是敵方發送的電報,而“文字檔”和“二進位檔案”分別表示兩本password本,相同的電報用不同的password本翻譯。出來的結果和意思自然就不同(當然通常情況下有一種解讀方式是失敗的,無法提供給我們有效資訊)。

(2)CRLF

        在程式設計語言中,文字檔和二進位檔案代表的就是不同的操作方式,或者簡單的能夠理解為使用不同的函數。通過上述的解說,能夠覺得不同的函數內部就是依照不同的標準(文字檔標準和二進位檔案標準)對01序列進行操作,比如讀取、寫入等等。

——有些時候不是必需糾結於一個函數的結果為什麼會是這樣子,僅僅要記住這是函數背後定義的標準所致就可以。至於標準的制定就不是必需深究了,總之是一波牛人定的。

        上面出現錯誤的兩個位元組——0x0d 0x0a——是電腦中非常特殊的兩個位元組,他們分別代表斷行符號(CR=Carriage Return)換行(LF=Line Feed)

不同的系統對CRNL的解釋不同。最早的UNIX系統中僅僅用換行(即\n)來表示資料的另起一行;Windows系統使用斷行符號+換行來表示;而Mac系統卻僅僅使用斷行符號。即\r。

        同一個檔案從磁碟讀取檔案到記憶體(程式資料區或者緩衝區)時,在文本和二進位方式下,記憶體中的內容一般不同樣,這就是兩種開啟檔案的實質性區別。

由於CRLF的不同。在windows下,它會做一個處理。就是寫檔案時,分行符號會被轉換成斷行符號+分行符號存在磁碟檔案上,而讀磁碟上的檔案時,它又會進行逆處理。就是把檔案裡連續的斷行符號+分行符號轉換成分行符號。因此,在讀取一個磁碟檔案時,文本方式讀取到檔案內容非常有可能會比二進位檔案短,由於文本方式讀取要把斷行符號和換行兩個字元變成一個字元,相當於截短了檔案。可是為什麼不過可能呢?由於可能文中中不存在連著的0x0d,0x0a這兩個位元組(0X0A是CR斷行符號的ASCII碼。0X0D是分行符號CL的ASCII碼),也就不存在“截短”操作了,因此讀到的內容是一樣的。詳細的來說,檔案檔案(以文本方式寫的),最好以文本方式讀。二進位檔案(以二進位方式寫的)。最好以二進位方式讀。

(3)stdin、stdout

        從(2)知識點就能夠大致推斷出,windows系統在向stdout寫入BMP資料流時。將遇到的0x0a都替換成了0x0d 0x0a,他覺得這裡改換行了。那麼為什麼在向stdout寫入資料流時會將0x0a轉換成0x0d 0x0a呢?有沒有不轉換的方法?這裡簡單的介紹一下C語言中的標準輸入輸出資料流。我們都知道stdin預設綁定到鍵盤;stdout預設綁定到顯示器。事實上stdin和stdout跟我們操作檔案經常使用的FILE*是同樣的類型。能夠簡單的覺得是程式與鍵盤和顯示屏資訊互動的緩衝區。比較特殊的是在CGI架構中,stdin和stdout擔負著瀏覽器與服務端的資訊互動。

        既然stdin和stdout與普通的FILE*沒有差別,依據我們對文本格式和二進位格式的理解,能否夠控制寫入stdout的方式來限制系統將0xa轉換成0x0d 0x0a呢?由於顯示屏預設是字元類型的輸出,不方便調試。我們用一個檔案FILE*來取代stdout,然後通過不同的寫入方式來驗證一下我們剛才的猜想。

測試的輸入檔案(即我們首先讀入到記憶體的資料)是利用dcm2pnm.exe工具轉換而來的bmp映像。我們在讀取檔案的時候選擇了"rb”二進位模式,目的就是為了限制windows系統對CRLF的轉換。測試代碼例如以下:


        如所看到的,二進位方式寫入時能夠得到正確的映像。文本格式寫入時恰恰得到的就是我們前面遇到的錯誤結果。

因此能夠說明在向stdout寫入資料的過程中DicomImage使用的是文本格式。應該使用二進位方式寫入stdout,想必能夠得到正確的結果。

3)嘗試改動C語言版本號碼的CGI程式:

        既然找到了問題的根源。那麼我們就又一次改動C語言的CGI程式。已知stdout與FILE*同樣。那麼直接利用常見的C語言檔案操作函數。用二進位方式來向stdout輸出資料。驗證一下我們的想法。

測試代碼例如以下:

// dcmtk-save-test.cpp : 定義控制台應用程式的進入點。//#include "dcmtk/config/osconfig.h"#include "dcmtk/dcmdata/dctk.h"#include "dcmtk/dcmdata/dcpxitem.h"#include "dcmtk/dcmjpeg/djdecode.h"#include "dcmtk/dcmjpeg/djencode.h"#include "dcmtk/dcmjpeg/djcodece.h"#include "dcmtk/dcmjpeg/djrplol.h"#include "dcmtk/dcmimgle/diutils.h"#include "dcmtk/dcmimgle/dcmimage.h"#include <stdio.h>#include <iostream>#include <iomanip>#include <bitset>#include <windows.h>using std::cout;using std::bitset;using std::hex;void Print2Web(char* msg){printf("Content-Type:text/html\n\n");printf("<HTML>\n");printf("<HEAD>\n<TITLE >DCM to BMP Test</TITLE>\n</HEAD>\n");printf("<BODY>\n");printf("<div style=\"font-size:12px\">\n");printf("<div style=\"COLOR:RED\">%s</div>\n",msg);printf("</div>\n");printf("</BODY>\n");printf("</HTML>\n");}void SendImageDcmtk(char* filename){DcmFileFormat mDcm;mDcm.loadFile(filename);E_TransferSyntax xfer = mDcm.getDataset()->getOriginalXfer();unsigned long mode = CIF_MayDetachPixelData | CIF_TakeOverExternalDataset;DicomImage *di = new DicomImage(&mDcm,xfer,mode,0,0);if(di == NULL){Print2Web("Can not open DCM file by DicomImage!");}printf("Content-Type:image/bmp\n\n");di->writeBMP(stdout,8,0);;}void SendImage(char* filename){FILE* fp=fopen(filename,"rb");printf("Content-Type:image/bmp\n\n");fclose(stdout);freopen("CON","wb",stdout);int r=getc(fp);while(!feof(fp)){putc(r,stdout);r=getc(fp);}fclose(fp);}void SendImage2(char* filename){FILE* fp=fopen(filename,"rb");fseek(fp,0,SEEK_END);int length=ftell(fp);printf("Content-Length:%d\n",length);printf("Content-Type:image/bmp\n\n");fseek(fp,0,SEEK_SET);char buf[1024];memset(buf,0,sizeof(buf));if(length>1024){while(length>1024){fread(buf,sizeof(buf),1,fp);fwrite(buf,sizeof(buf),1,stdout);memset(buf,0,sizeof(buf));length-=1024;}fread(buf,length*sizeof(char),1,fp);fwrite(buf,length*sizeof(char),1,stdout);}else{fread(buf,length*sizeof(char),1,fp);fwrite(buf,length*sizeof(char),1,stdout);}fclose(fp);}void SendImage3(char* filename){FILE* fp=fopen(filename ,"rb");printf("Content-Type:image/bmp\n\n");char buf[1024];memset(buf,0,sizeof(char)*1024);int size=0;fclose(stdout);freopen("CON","wb",stdout);while(size = fread(buf,sizeof(char),1024,fp)){fwrite(buf,sizeof(char),size,stdout);fflush(stdout);}fflush(stdout);fclose(fp);}void SendImage4(char* filename){printf("Content-Type:image/bmp\n\n");HANDLE hStdout=GetStdHandle(STD_OUTPUT_HANDLE);HANDLE hFile=CreateFile(filename,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_FLAG_SEQUENTIAL_SCAN,NULL);if(hFile == INVALID_HANDLE_VALUE)return;DWORD dwHighSize;unsigned long size=GetFileSize(hFile,&dwHighSize);char *data=new char[size];unsigned long readsize=0;ReadFile(hFile,data,size,&readsize,NULL);if(size!=readsize){delete data;return;}unsigned long writesize=0;while(writesize<size){unsigned long wsize=0;if(size-writesize>1024)WriteFile(hStdout,data+writesize,1024,&wsize,NULL);else{WriteFile(hStdout,data+writesize,size-writesize,&wsize,NULL);}fflush(stdout);writesize+=wsize;}fflush(stdout);CloseHandle(hStdout);}int main(int argc ,char* argv[]){char* filename="c:\\test.bmp";SendImage(filename);return 0;}

        經過了上述多種嘗試後。探索資料依舊是有問題。因此推測C語言的檔案操作函數內部可能對stdout的寫入有特殊的操作,無法實現二進位格式寫入。至此該問題在C語言環境下還是未解決。假設哪位朋友知道原因。還請指教。興許我也會繼續進行分析,希望儘快找到原因。


未完待續……


參考資料:

[1]http://blog.csdn.net/silyvin/article/details/7275037

[2]http://blog.csdn.net/lanbing510/article/details/8183343

[3]http://www.jb51.net/article/31458.htm

[4]http://blog.csdn.net/babygjx/article/details/5832235

興許專欄博文介紹:

利用DCMTK搭建WMLserver

利用oracle直接操作DICOM資料

C#的非同步編程模式在fo-dicom中的應用

VMWare三種網路連接模式的實際測試


[email protected]

時間:2014-10-27

DICOM醫學影像處理:WEB PACS初談二,映像的傳輸

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.