背景:

如前一篇专栏博文所述,借助于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" alt="">

抓取完毕后,在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" alt="">

同一时候使用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

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

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

    背景: 周末看到了一篇原公司同事的文章,讲的是关于新的互联网形势下的PACS系统.正好上一篇专栏文章也提到了有想搭建一个worklist服务器的冲动,所以就翻箱倒柜将原本学生时代做课题时搭建的简易We ...

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

    背景: 预告了好久的几篇专栏博文一直没有整理好,主要原因是早前希望搭建的WML服务器计划遇到了问题.起初以为参照DCMTK的官方文档wwwapp.txt结合前两天搭建的WAMP服务器可以顺利的实现WM ...

  3. DICOM医学图像处理:DIMSE消息发送与接收“大同小异”之DCMTK fo-dicom mDCM

    背景: 从DICOM网络传输一文开始,相继介绍了C-ECHO.C-FIND.C-STORE.C-MOVE等DIMSE-C服务的简单实现,博文中的代码给出的实例都是基于fo-dicom库来实现的,原因只 ...

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

    转载:http://blog.csdn.net/zssureqh/article/details/39213817 背景: 上一篇专栏博文中针对PACS终端(或设备终端,如CT设备)与RIS系统之间w ...

  5. [转]DICOM医学图像处理:Deconstructed PACS之Orthanc

    转载:http://blog.csdn.net/zssureqh/article/details/41424027 背景: 此篇博文介绍一个开源的.基于WEB的DICOM Server软件.该开源软件 ...

  6. DICOM医学图像处理:Deconstructed PACS之Orthanc

    背景: 此篇博文介绍一个开源的.基于WEB的DICOM Server软件.该开源软件完全使用C++编写,不依赖于第三方数据库(内置了SQLite数据库)或其他框架,支持RESTful API设计模式. ...

  7. DICOM医学图像处理:开源库mDCM与DCMTK的比較分析(一),JPEG无损压缩DCM图像

    背景介绍: 近期项目需求,须要使用C#进行最新的UI和相关DICOM3.0医学图像模块的开发.在C++语言下,我使用的是应用最广泛的DCMTK开源库,在本专栏的起初阶段的大多数博文都是对DCMTK开源 ...

  8. DICOM医学图像处理:fo-dicom网络传输之 C-Echo and C-Store

    背景: 上一篇博文对DICOM中的网络传输进行了介绍.主要參照DCMTK Wiki中的英文原文.通过对照DCMTK与fo-dicom两个开源库对DICOM标准的详细实现,对理解DICOM标准有一个更直 ...

  9. DICOM医学图像处理:Deconstructed PACS之Orthanc,Modification & Anonymization

    背景: 上篇博文为引子,介绍了一款神奇的开源PACS系统——Orthanc.本篇开始解读官方Cookbook中的相关内容,对于简单的浏览.访问和上传请阅读前篇博文.在常规的PACS系统中还未出现对于D ...

随机推荐

  1. python每日一类(3):os和sys

    os与sys模块的官方解释如下: os: This module provides a portable way of using operating system dependent functio ...

  2. NEUQOJ 1999: 三角形or四边形?【搜索联通块/模拟】

    http://newoj.acmclub.cn/problems/1999 1999: 三角形or四边形? 描述 题目描述: JiangYu很无聊,所以他拿钉子在板子上戳出了一个由.#组成的10*10 ...

  3. Ngnix 安装常见错误的处理

    错误:   解决方案:(联网下) 出现上面的问题是由于没有c++编译器造成 # yum -y install gcc-c++   使用上面的命令即可安装c++解决问题 如果确实c编译器,使用如下命令解 ...

  4. String&&StringBuilder&&StringBuffer

    在java中提供三个操作字符串的类:String,StringBuilder,StringBuffer (1)什么是字符串:多个字符的集合 (2)String 是内容不可变的字符串.(底层使用了一个不 ...

  5. iOS中正则表达式的基本使用方法

    一.第三方框架RegexKitLite的使用 在ios项目中可以借用第三方框架RegexKitLite来简化对正则表达式的使用,使用方法如下 1.去RegexKitLite下载类库,解压出来会有一个例 ...

  6. Android activity中单击返回键或home键彻底退出应用

    @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if(keyCode == KeyEvent.KEYCODE_BAC ...

  7. [给自己扫盲]Node.js 究竟是什么?

    Node.js 究竟是什么? 一个 “编码就绪” 服务器 Node 是一个服务器端 JavaScript 解释器,它将改变服务器应该如何工作的概念.它的目标是帮助程序员构建高度可伸缩的应用程序,编写能 ...

  8. linux-单引号、双引号、反引号的区别。

    一.单引号和双引号 单引号和双引号,都是为了解决中间有空格的问题. 因为空格在Linux中时作为一个很典型的分隔符,比如string1=this is astring,这样执行就会报错.为了避免这个问 ...

  9. sqlserver 下载地址(SQL Server 2008 R2 中英文 开发版/企业版/标准版 下载)

    转自:http://blog.sina.com.cn/s/blog_624b1f950100pioh.html   注:企业版无法安装在xp和win7,开发版才可以! 一. 简体中文 1. SQL S ...

  10. linux之网络配置相关

    ubuntu的网络配置文件在 /etc/network/intrfaces; suse的网络配置在          /etc/sysconfig/network/下面,每个网卡一个配置文件. int ...