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工具,抓取浏览器与服务器端的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序列就像是敌方发送的电报,而“文本文件”和“二进制文件”分别表示两本密码本,同样的电报用不同的密码本翻译,出来的结果和意思自然就不同(当然通常情况下有一种解读方式是失败的,无法提供给我们有效的信息)。

(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搭建WML服务器

利用oracle直接操作DICOM数据

C#的异步编程模式在fo-dicom中的应用

VMWare三种网络连接模式的实际测试


作者:[email protected]

时间:2014-10-27

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。