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。

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

时间: 2024-08-23 23:35:13

DICOM医学图像处理:WEB PACS初谈二,图像的传输的相关文章

DICOM医学图像处理:WEB PACS初谈

背景: 周末看到了一篇原公司同事的文章,讲的是关于新的互联网形势下的PACS系统.正好上一篇专栏文章也提到了有想搭建一个worklist服务器的冲动,所以就翻箱倒柜将原本学生时代做课题时搭建的简易Web PACS找了出来,借着再次搭建的机会学习一下Web PACS相关的技术,例如WADO标准.CGI或者FastCGI等技术. WEB PACS技术浅谈: WEB PACS是一种利用互联网技术,跨越了医院和地域限制的,可随意查询和获取DICOM对象的PACS系统.目前常见的方式有两种:第一种是通过W

DICOM医学图像处理:WEB PACS初谈三,PHP扩展骨架

背景: 最近两篇专栏博文讲解的都是有关WEB PACS环境的搭建,如果搭建的平台后端不进行DICOM的相关操作,其实跟PACS压根就一点关系也没有,所以最近几篇看似有些跑题,不过大家不要着急,开发环境的搭建本身就是一项巨大而且艰难的工程,等调试好环境后续的PACS相关开发就会如单机版一样得心应手,再忍耐一会,近期马上会开始介绍在平台上进行WEB PACS的研发. C/C++编写PHP扩展的环境搭建: 上两篇博文只是对该环境的一个取巧的尝试,第一篇博文直接利用APACHE服务自带的CGI,直接调用

DICOM医学图像处理:WEB PACS初谈四,PHP DICOM Class

背景: 预告了好久的几篇专栏博文一直没有整理好,主要原因是早前希望搭建的WML服务器计划遇到了问题.起初以为参照DCMTK的官方文档wwwapp.txt结合前两天搭建的WAMP服务器可以顺利的实现WML服务,借此就可以同时完成WEB PACS系列以及搭建Dicom WML服务器的两篇博文.可是在实际部署过程中发现了几个严重的问题,一时无法解决.但是在搜索解决方案的时候,偶然间找到了在DCMTK论坛上贴出来的用PHP对DCMTK工具包封装的文章.因此此篇博文在记录搭建WML遇到的问题的同时,主要想

DICOM医学图像处理:DCMTK的wiki资料学习之PACS调试

背景: 前段时间着重从dcmtk和fo-dicom(mDCM)源码角度进行剖析,期望加深对DICOM协议的理解.知其然,知其所以然.如果"所以然"很不好懂,那我们还是先多多"知其然"吧.搞清楚原理的目的不也是为了更好的运用于实践么?所以理论和实践应该彼此交错进行,理论搞不动了就搞搞应用,应用久了就钻研钻研理论. 以前上DCMTK官网仅仅是浏览关于开源库中各个类的设计模式.依赖关系.最近在打开DCMTK官网的wiki时,才发现OFFIS对DCMTK的介绍是如此的详细.

DICOM医学图像处理:DICOM网络传输

背景: 专栏取名为DICOM医学图像处理原因是:博主是从医学图像处理算法研究时开始接触DICOM协议的.当初认识有局限性,认为DICOM只是一个简单的文件格式约定,简而言之,我当时认为DICOM协议就是扩展名为DCM文件的格式说明.其实不然,随着对医疗行业的深入,对DICOM协议也有了更全面的认识.而今才发现DCM文件只是DICOM协议一部分中的一小节,仅仅是整个协议中的一个数据结构,而DICOM协议更多的是关于医疗行业各种服务及相关流程的约定,因此其实DICOM协议中最主要的是信息流,是对医院

DICOM医学图像处理:Dcmtk与fo-dicom保存文件的不同设计模式之“同步VS异步”+“单线程VS多线程”

一.背景: 最近一直在做DCM相关的编程工作,以前项目使用C++居多,所以使用DCMTK开源库,而目前团队使用C#居多,所以需要转向使用fo-dicom库,由于前一篇专栏文章DICOM医学图像处理:利用fo-dicom发送C-Find查询Worklist在调试过程中需要对DIMSE信息进行手动保存,偶然间发现了dcmtk开源库与fo-dicom开源库在保存dcm文件时使用的方式差异很大,因此决定研究一下,期望通过对比分析来看一下孰优孰劣. 二.dcmtk与fo-dicom保存文件函数的源码剖析:

DICOM医学图像处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求

背景: 上一篇专栏博文中针对PACS终端(或设备终端,如CT设备)与RIS系统之间worklist查询进行了介绍,并着重对比分析了DICOM3.0中各部分对DICOM网络通讯服务的定义.此次通过结合早些时间的博文DICOM医学图像处理:基于DCMTK工具包学习和分析worklist,对DCMTK开源库中提供的storescp.exe和storescu.exe工具的源码进行剖析,从底层深入了解C-STORE服务的触发及响应. 分析思路: storescp.exe和storescu.exe分别充当着

DICOM医学图像处理:DICOM存储操作之“多幅BMP图像数据存入DCM文件”

背景: 本专栏"DICOM医学图像处理"受众较窄,起初只想作为自己学习积累和工作经验的简单整理.前几天无聊浏览了一下,发现阅读量两极化严重,主要集中在"关于BMP(JPG)与DCM格式转换"和"DICOM 通讯协议",尤其是许久前的第一篇博文DCMTK开源库的学习笔记1:将DCM文件保存成BMP文件或数据流(即数组).因此在2014年底前打算写几篇关于DCM格式转换的文章,此次主要聚焦"如何将BMP.JPG等常规图像保存成DCM文件&q

DICOM医学图像处理:DCMTK在VS2012中的配置

背景: 最近由于项目需要,将原本的开发IDE环境由VS2008升级到了VS2012.本以为编译完成后的DCMTK开源库可以直接从VS2008移植到VS2012.但是通过项目属性添加完包含目录和依赖库后,编译会出现大量的链接错误(大多是跟dcmdata.lib.oflog.lib有关). 解决方法: 重新按照原本的博客前辈柳北风儿(大神目前已经博客转移到网易:http://blog.163.com/[email protected]/),利用CMake工具,选择VS2012本地编译器对DCMTK3