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


背景:

        如前一篇專欄博文所述,借助於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.exe

use 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。


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

        隨后單擊TCP協議。選擇當中數據量大的會話。單擊窗體下方的Follow Stream。能夠打開CGI腳本傳輸圖像到服務端的真實數據流。

        同一時候使用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三種網絡連接模式的實際測試


作者:zssure@163.com

時間:2014-10-27


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM