1 DICOM的详细介绍
DICOM要到这里面看Current Edition,这是标准委员会制定的标准,同时也在与时俱进,不断的进行新的数据格式更新。
2 DICOM的格式
图1 DICOM文件格式的图示
要先说一下数据结构,我们被最多影响的是数据结构和算法的先导知识。什么队列,链表,二叉树,红黑树,无向图等概念,这些概念是一个抽象的逻辑概念。其实数据在内存中是顺序存储的,你看看单片机开发,和汇编语言就回明白了。一个DICOM文件在磁盘上也是顺序存储的,就像一张图片,在电脑硬盘上的存储是拆成一列一列或者是一行一行的存储的,当然了,数据的前面要有一个文件头,说明此图像的基本信息,比如文件格式,颜色通道,数据位,8位还是16位图等,以及长和宽等。当一副图像被读入内存中,也是按照顺序存储的从文件头到文件的结尾。
从图1中,我们可以看到一个DICOM的文件格式也是顺序的,导言部分,按照顺序是128个空字节,然后是‘DICM’四个字节的标识,接着是meta信息,这个多少个字节根据语法决定的。接下来是数据元素,一个接着一个。数据元素的组成如图2所示
图2 数据元素的组成
我们可以看到数据元素是由4个部分组成的,第一部分是Tag,标签,两个字节,使用16进制表示。第二部分是VR值表示,说明后面的数据(Value Field)的类型,比如字符型还是整型数据,这个跟读取方式有关,很重要。第三部分是值的长度,一般都是偶数(为了数据的整齐),表示后面的数据的长度(字节)。
3 实践部分
我们使用纯C++来读取,不使用第三方解析工具,等理解了之后再使用DCMTK来解析。
先介绍几个用到的函数,读取4个字节的函数
void read4Byte(std::fstream &file,char* str){ for(int i=0;i<4;++i){ *str=readByte(file); str++; } }
上面的这个函数其实还关系到一个局部变量的返回问题,传入一个全局数组,可以避免局部数组返回问题。还可以使用局部在堆上new一个数组的方法,然后返回指针。这个以后再说吧。
使用下面的函数读取Tag标签: void readTag(std::fstream& file){ char byte[2]; unsigned short group; unsigned short elem; file.read(byte,2); group=byte[1]<<8|byte[0]; file.read(byte,2); elem= byte[1]<<8|byte[0]; std::cout<<"["<<std::setw(4)<<std::setfill('0')<<std::hex<<group<<","<<std::setw(4)<<std::setfill('0')<<std::hex<<elem<<"]"<<std::dec<<" : "; }
可以看到,每次读取两个字节,因为是16位数据,是无符号整型数据。分成两个部分,group和element。先读区的是group,两个字节。这里的小知识,数据在内存中是怎么存储的。
大家知道,现在的计算机是64位操作系统。计算机位数,简单来说就是计算机一次能处理的二进制数的位数。这个看似简单的概念,却承载着计算机发展史上的诸多变革。
- 4位和8位时代:
- 早期的微处理器,如Intel 4004,只有4位,只能处理非常简单的任务。
- 随着集成电路技术的进步,8位微处理器逐渐成为主流,代表机型有Intel 8080和Zilog Z80。它们被广泛应用于个人电脑和嵌入式系统。
- 16位和32位时代:
- 16位微处理器标志着个人电脑时代的到来,代表机型有Intel 8086和Motorola 68000。
- 32位微处理器的出现,大大提高了计算机的性能,满足了对多任务和大型软件的需求。Intel 80386和80486是这一时代的代表。
- 64位时代:
- 64位微处理器能够直接寻址更大的内存空间,处理更复杂的数据,为现代操作系统和应用程序提供了强大的支持。Intel x86-64架构成为主流。
由于历史的原因,现在一个字节还是8位,64位机器一次可以处理8个字节。我们计算机存储多个字节的数据,比如无符号整型数据,是16位,两个字节。计算机存储多个字节数据时有小端字节序和大端字节序(Little-Endian/Big-Endian),一般都是小端字节序,网络传输时大端字节序。
小端字节序(Little-Endian)是一种在计算机系统中存储多字节数据的顺序。在这种顺序下,最低有效字节(Least Significant Byte,LSB) 存储在最低的内存地址,而最高有效字节(Most Significant Byte,MSB) 存储在最高的内存地址。
为什么会有小端字节序?
-
历史原因: 不同的计算机架构师在设计计算机时,选择了不同的字节序存储方式。
-
硬件设计: 某些硬件架构可能更适合小端字节序,例如早期的x86处理器。
-
软件兼容性: 为了兼容已有的软件和硬件,一些系统选择保留小端字节序。
小端字节序的示例
假设我们要存储一个16位的整数170(十进制),其二进制表示为0000000101010000。在小端字节序系统中,这个整数会在内存中存储为:
地址 值
1000 10100000 (A0)
1001 00000001 (01)
可以看到,最低字节A0存储在地址1000,而最高字节01存储在地址1001。
小端字节序与大端字节序
-
大端字节序(Big-Endian): 与小端字节序相反,大端字节序将MSB存储在最低的内存地址。
-
选择: 不同的计算机系统采用不同的字节序,例如x86架构通常是小端序,而PowerPC和大多数网络协议(如TCP/IP)采用大端序。
所以我们用了下面的方法,将读取到的高8位向左移动8位,然后跟低8位的数据或一下,组成无符号整型数据
group=byte[1]<<8|byte[0];
然后再读取下一个element数据,同样处理。但是显示的时候需要使用16进制,这个是DICOM标准规定的,只要是要用这个标签去查dicom词典,知道标签的含义,比如,姓名,数据的位数,长,宽等。
还有读取字符串的函数:
void readString(std::fstream &file, char *str,int length){
for(int i=0;i<length;++i){
*str=readByte(file);
str++;
}
}
读取整型数据的函数:
int getInt(std::fstream& file){
char byte[4];
file.read(byte,4);
int value=byte[3]<<24|byte[2]<<16|byte[1]<<8|byte[0];
value&=0xff; //这个要有,只取低8位的数据。
//cout<<value<<endl;
return value;
}
还有一些函数在这里
char readByte(std::fstream& file){ char byte[1]; file.read(byte,1); return byte[0]; }
void read2Byte(std::fstream& file){ char byte[2]; file.read(byte,2); std::cout<<" "<<byte[0]<<byte[1]<<" "; }
然后我们看主函数中的内容和输出 string filename="../data/CT000011.dcm"; // 文件名 std::fstream file; //定义一个stream 在文件头#include <fstream> file.open(filename.c_str(),ios::in|ios::binary); // open the dicom file as binary // 以二进制模式打开文件 file.seekg(128,ios::beg); // skip the 128 byte //跳过前面的128个空字节
接下来就开始读区Data Element(数据元素了) char str[128]; // 用来存储读取的字符串,128位应该够用了 read4Byte(file,str); // 读取‘DICM’四个字符 std::cout<<"DICOM marker (4 Bytes):"<<std::string(str)<<std::endl;
****DICOM marker (4 Bytes):DICM
// begin read tag and its value readTag(file); read2Byte(file); 这个会显示UL,表示一个4个字节的整型数据 int length=getShort(file); std::cout<<length<<" # "; //长度 是4个字节 //readString(file,str,length); std::cout<<getInt(file)<<std::endl; // 显示获取的整型数据是214
****[0002,0000] : UL 4 # 214
readTag(file); read2Byte(file); length=getShort(file); std::cout<<length<<" # "; read2Byte(file); read2Byte(file); read2Byte(file); //skip 6 Byte by the transfer syntax definition. // reference:https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html std::cout<<std::endl;
****[0002,0001] : OB 0 # 。//这个OB是一个与传输语法有关的长度,在这里是6个字节,直接使用三次读取2个字节的方法跳过。
readTag(file); read2Byte(file); length=getShort(file); std::cout<<length<<" # "; readString(file,str,length); std::cout<<std::string(str)<<std::endl; std::memset(str,0,sizeof(str)); // cause we use the str varible again and again
****[0002,0002] : UI 26 # 1.2.840.10008.5.1.4.1.1.2
这里最后使用memset方法将str清空,以免影响后面存储字符串。然后就开始循环使用上面的这一段就可以先读取一部分头部文件了。结果如下图
图3 输出结果
跟使用DCMTK的输出基本是一致的,只是没有查询DICOM词典获取标签的含义,但是还是有一些小小的差异。
图4 使用DCMTK解析的Metainfo结果
小伙伴们,还没有解析到图像呢,等待下一集吧。