背景:
? ? ? ? 本專欄“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文件的)
源碼如下:
?
[csharp] ?view plaincopy print?
?? ?? ? #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);?? ?????? ????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);?? ?????? ????status=fileformat.saveFile("c:\\Multibmp2dcmtest.dcm" ,ts);?? ????if (status.bad())?? ????{?? ????????std::cout<<"Error:(" <<status.text()<< ")\n" ;?? ????}?? ????return ?0;?? }??
代碼中的DicomUtils類是一個方法類,提供了一個靜態方法AddDicomElement構造DICOM基本元素,代碼如下:
?
[csharp] ?view plaincopy print?
#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" );?? ?? ?????? ????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" );?? ?? ?? ?????? ????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);?? ?????? ????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軟件中打開彈出如下錯誤:
? ? ? ? 利用DCMTK自帶的dcmdump.exe工具分析結果如下:
? ? ? ? 由此可以看出像素數據讀取失敗。
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字段規定了實際像素信息的存儲方式,具體如下:
?
最終結果:
? ? ? ? 博文中實例代碼最終在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
?
總結
以上是生活随笔 為你收集整理的DICOM医学图像处理:DICOM存储操作之“多幅BMP图像数据存入DCM文件” 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。