DICOM醫學影象處理:DICOM儲存操作之“多幅BMP影象資料存入DCM檔案”

zssure發表於2014-12-24

背景:

        本專欄“DICOM醫學影象處理”受眾較窄,起初只想作為自己學習積累和工作經驗的簡單整理。前幾天無聊瀏覽了一下,發現閱讀量兩極化嚴重,主要集中在“關於BMP(JPG)與DCM格式轉換”和“DICOM 通訊協議”,尤其是許久前的第一篇博文DCMTK開源庫的學習筆記1:將DCM檔案儲存成BMP檔案或資料流(即陣列)。因此在2014年底前打算寫幾篇關於DCM格式轉換的文章,此次主要聚焦“如何將BMP、JPG等常規影象儲存成DCM檔案”,以DCMTK庫為基礎,給出簡單的例項。

        這幾篇博文采用倒敘的方式,先給出可直接執行的原始碼,然後重點講解其中易犯的錯誤,最後是知識點補充。

利用DCMTK實現Multi-BMP存入DCM:

        原始碼以DCMTK為基礎,思路參照DCMTK的img2dcm工具包,依賴庫包含:netapi32.lib; wsock32.lib; ofstd.lib; oflog.lib; dcmimgle.lib; ijg8.lib; ijg12.lib; ijg16.lib; dcmdata.lib; dcmimage.lib; dcmjpeg.lib; dcmnet.lib; zlib.lib;libi2d.lib;(【注】:libi2d.lib庫是用於匯入BMP檔案的)

原始碼如下:

 

// DcmPixelDataTest.cpp : 定義控制檯應用程式的入口點。
//

#include "stdafx.h"
#include "dcmtk/config/osconfig.h"
#include "dcmtk/dcmdata/dctk.h"
#include "dcmtk/dcmdata/dcistrmf.h"
#include "dcmtk/dcmdata/libi2d/i2dbmps.h"
#include "DicomUtils.h"
#include <direct.h>

int _tmain(int argc, _TCHAR* argv[])
{
	OFCondition status;

	DcmFileFormat fileformat;
	DcmDataset* mydatasete=fileformat.getDataset();
	DicomUtils::AddDicomElements((DcmDataset*&)mydatasete);
	Uint16 rows,cols,samplePerPixel,bitsAlloc,bitsStored,highBit,pixelRpr,planConf,pixAspectH,pixAspectV;
	OFString photoMetrInt;
	Uint32 length;
	E_TransferSyntax ts;
	char* mydata=new char[1024*1024*10];
	memset(mydata,0,sizeof(char)*1024*1024*10);
	char* tmpData=mydata;
	char curDir[100];
	getcwd(curDir,100);
	//迴圈新增4張圖片
	for(int i=0;i<4;++i)
	{
		OFString num;
		char numtmp[100];
		memset(numtmp,0,sizeof(char)*100);
		sprintf(numtmp,"%s\\test\\%d.bmp",curDir,i+1);
		OFString filename=OFString(numtmp);
		I2DBmpSource* bmpSource=new I2DBmpSource();
		bmpSource->setImageFile(filename);

		char* pixData=NULL;
		bmpSource->readPixelData(rows,cols,samplePerPixel,photoMetrInt,bitsAlloc,bitsStored,highBit,pixelRpr,planConf,pixAspectH,pixAspectV,pixData,length,ts);
		memcpy(tmpData,pixData,length);
		tmpData+=length;

		delete bmpSource;
	};

	mydatasete->putAndInsertUint16(DCM_SamplesPerPixel,samplePerPixel);
	mydatasete->putAndInsertString(DCM_NumberOfFrames,"4");
	mydatasete->putAndInsertUint16(DCM_Rows,rows);
	mydatasete->putAndInsertUint16(DCM_Columns,cols);
	mydatasete->putAndInsertUint16(DCM_BitsAllocated,bitsAlloc);
	mydatasete->putAndInsertUint16(DCM_BitsStored,bitsStored);
	mydatasete->putAndInsertUint16(DCM_HighBit,highBit);
	mydatasete->putAndInsertUint8Array(DCM_PixelData,(Uint8*)mydata,4*length);
	mydatasete->putAndInsertOFStringArray(DCM_PhotometricInterpretation,photoMetrInt);
	//mydatasete->putAndInsertString(DCM_PlanarConfiguration,"1");
	status=fileformat.saveFile("c:\\Multibmp2dcmtest.dcm",ts);
	if(status.bad())
	{
		std::cout<<"Error:("<<status.text()<<")\n";
	}
	return 0;
}

程式碼中的DicomUtils類是一個方法類,提供了一個靜態方法AddDicomElement構造DICOM基本元素,程式碼如下:

 

#include "DicomUtils.h"


DicomUtils::DicomUtils(void)
{
}


DicomUtils::~DicomUtils(void)
{
}
void DicomUtils::AddDicomElements(DcmDataset*& dataset)
{
	//構建測試資料

	/*	新增患者資訊	*/
	dataset->putAndInsertUint16(DCM_AccessionNumber,0);
	dataset->putAndInsertString(DCM_PatientName,"zssure",true);
	dataset->putAndInsertString(DCM_PatientID,"2234");
	dataset->putAndInsertString(DCM_PatientBirthDate,"20141221");
	dataset->putAndInsertString(DCM_PatientSex,"M");

	/*	新增Study資訊	*/
	dataset->putAndInsertString(DCM_StudyDate,"20141221");
	dataset->putAndInsertString(DCM_StudyTime,"195411");
	char uid[100];
	dcmGenerateUniqueIdentifier(uid,SITE_STUDY_UID_ROOT);
	dataset->putAndInsertString(DCM_StudyInstanceUID,uid);
	dataset->putAndInsertString(DCM_StudyID,"1111");


	/*	新增Series資訊	*/
	dataset->putAndInsertString(DCM_SeriesDate,"20141221");
	dataset->putAndInsertString(DCM_SeriesTime,"195411");
	memset(uid,0,sizeof(char)*100);
	dcmGenerateUniqueIdentifier(uid,SITE_SERIES_UID_ROOT);
	dataset->putAndInsertString(DCM_SeriesInstanceUID,uid);
	/*	新增Image資訊	*/
	dataset->putAndInsertString(DCM_ImageType,"ORIGINAL\\PRIMARY\\AXIAL");
	dataset->putAndInsertString(DCM_ContentDate,"20141221");
	dataset->putAndInsertString(DCM_ContentTime,"200700");
	dataset->putAndInsertString(DCM_InstanceNumber,"1");
	dataset->putAndInsertString(DCM_SamplesPerPixel,"1");
	dataset->putAndInsertString(DCM_PhotometricInterpretation,"MONOCHROME2");
	dataset->putAndInsertString(DCM_PixelSpacing,"0.3\\0.3");
	dataset->putAndInsertString(DCM_BitsAllocated,"16");
	dataset->putAndInsertString(DCM_BitsStored,"16");
	dataset->putAndInsertString(DCM_HighBit,"15");
	dataset->putAndInsertString(DCM_WindowCenter,"600");
	dataset->putAndInsertString(DCM_WindowWidth,"800");
	dataset->putAndInsertString(DCM_RescaleIntercept,"0");
	dataset->putAndInsertString(DCM_RescaleSlope,"1");


}

問題分析:

1)檔案格式錯誤:

        我在方法類DicomUtils中預設新增的SamplePerPixel標籤值為1,如果最終讀取完畫素資料(即readPixelData函式呼叫完)未重新寫入BMP相應的SamplePerPixel欄位,會引發檔案格式錯誤,在SanteSoft DICOM Editor軟體中開啟彈出如下錯誤:

image

        利用DCMTK自帶的dcmdump.exe工具分析結果如下:

image

        由此可以看出畫素資料讀取失敗。

2)影象資訊顯示錯誤:



        上圖是正確影象,下圖是由於Photometric Interpretation欄位寫入錯誤導致的,靜態類DicomUtils中預設的Photometric Interpretation值為MONOCHROME2,修改為I2DBmpSource中readPixelData函式返回的photoMetrInt引數後影象資料顯示正確,如下圖所示:

 

3)影象色彩顯示錯誤:

        在靜態類DicomUtils中並未新增Planar Configuration欄位,因此DCMTK自動填充該欄位為0,如果我們修改為1,會出現影象色彩錯誤,如下圖:


BMP格式:

        關於BMP格式介紹的博文很多,可參考http://blog.csdn.net/zhandoushi1982/article/details/5196017或者http://blog.csdn.net/gwwgle/article/details/4775396。BMP檔案資料主要有以下幾部分組成:1)檔案頭,即結構BITMAPFILEHEADER, *PBITMAPFILEHEADER,類似於DCM中的DcmMetaInfo;2)影象描述資訊塊,該部分記錄了影象資訊塊的大小、影象的寬度、高度、影象通道數(即Plane)、畫素位數(即後面DICOM標準中的SamplesPerPixel)、影象壓縮方式、影象資料區大小等等;3)顏色表,即調色盤。該部分與DICOM標準中的COLOR PALETTE,隨著畫素位數不同顏色表大小也不同,當畫素位數為24或更大,即SamplesPerPixel=3時,畫素資料本身就可以代表顏色,因此不需要顏色表;4)影象資料區,即檔案中儲存的真正的畫素資訊。【注】:這裡有一個座標轉換,標準的BMP檔案畫素儲存順序是由左到右、由下到上,即座標原點為影象左下角;而DICOM標準儲存順序為從左到右,從上到下,座標原點為影象左上角,因此在自己讀取時需要進行反轉。

獲取BMP影象資訊方法:

1)直接讀取二進位制

        瞭解了BMP檔案的具體格式,可以利用常用的二進位制操作方式,直接從檔案中提取畫素資料。這種程式碼網上也很多,可參考:http://www.jb51.net/article/56274.htm

2)DCMTK庫

        DCMTK庫中的I2DBmpSource類是專門用來解析BMP檔案的,並且提供了BMP到DICOM資料格式的轉換。具體的使用可參照我上面的例項,也可參考DCMTK給出的img2dcm工具包原始碼。 

3)CxImage第三方庫

        CxImage是一款免費的、優秀的影象操作類庫,可以快捷的存取、顯示、轉換各種影象,例如BMP、GIF、ICO、TGA、JPEG、PCX、PNG、TIFF、MNG、RAS等等;CxImage使用簡單,文件詳細,只有一個API介面檔案;支援Windows、Linux和Unix等多平臺,支援32位和64位。

        關於CxImage的複雜使用,可參見CodeProject中大神的博文:http://www.codeproject.com/Articles/1300/CxImage

        另外我在博文DCMTK開源庫的學習筆記1:將DCM檔案儲存成BMP檔案或資料流(即陣列)中給出的原始碼是結合了CxImage和DCMTK兩種開源庫,這也是常見的一種組合方式,具體細節可參考我的GitHub上的原始碼。

DICOM檔案格式:

1)Samples Per Pixel:

        標籤為(0028,0002),具體的介紹在DICOM3.0標準第3部分的附錄C7.6.3.1。含義表示【the number of separate planes in this image】,就像PhotoShop中的通道,每個通道表示一種顏色(除了RGB三個通道以外,也會存在第四個通透性通道)。對於灰度影象(monochrome或gray)和顏色表影象(palette,就是BMP格式中介紹的有調色盤的BMP檔案),該標籤值為1,RGB影象或其他色彩模式影象,該標籤值為3。本例項中使用的BMP影象是RGB格式的,因此SamplePerPixel=3,起初的檔案格式錯誤就是由於該欄位設定為1所致。

2)Photometric Interpretation:

        標籤為(0028,0004),具體介紹在DICOM3.0標準第3部分的附錄C7.6.3.1.2。該欄位常見的值有MONOCHROME1、MONOCHROME2、PALETTE COLOR、RGB,其中MONOCHROME1和MONOCHROME2表示單通道灰度影象,只是兩者對黑色和白色的對映相反而已;PALETTE COLOR就是BMP中提到的調色盤影象,此時需要SamplesPerPixel欄位為1,;RGB是常見的R(紅)、G(綠)、B(藍)三通道彩色影象,此時SamplesPerPixel欄位值為3,這就是我們例項中使用的影象。除此以外DICOM3.0標準中還給出了YBR_FULL、HSV、ARGB、CMYK等方式,此處就不詳細介紹了。

3)Planar Configuration:

        標籤為(0028,0006),具體介紹在DICOM3.0標準第3部分附錄C7.6.3.1.3。當Samples Per Pixel欄位的值大於1時,Planar Configuration欄位規定了實際畫素資訊的儲存方式,具體如下:

image

 

最終結果:

        博文中例項程式碼最終在C盤根目錄生成Multibmp2dcmtest.dcm檔案,利用Sante DICOM Editor開啟可以順利看到檔案中包含了我們插入的四張bmp影象,如下圖:


 

        至此將多幅BMP影象寫入DCM檔案的任務順利完成了,其實將多幅影象寫入DCM檔案與寫入單幅BMP影象是完全相同的,只需要將多張BMP影象(此時要求每張BMP影象的寬度和高度相同)畫素資料首尾相接的寫入DCM中的PixelData標籤下,即(7FE0,0010);此時將NumberofFrames標籤賦值為影象張數,DCM檔案編輯器就可自動識別提取各張影象。

原始碼:

百度網盤:http://pan.baidu.com/s/1dDrhHlR

GitHub:https://github.com/zssure-thu/CSDN/tree/master

 

後續博文介紹:

多幅JPEG影象資料存入DCM檔案

fo-dicom搭建簡單的PACS Server服務端

 

 

作者:zssure@163.com

時間:2014-12-24

相關文章