DICOM:DICOM三大開源庫對比分析之“數據加載”


背景:

上一篇博文DICOM:DICOM萬能編輯工具之Sante DICOM Editor介紹了DICOM萬能編輯工具,在日常使用過程中發現,“只要Sante DICOM Editor打不開的數據,基本可以判定此DICOM文件格式錯誤(准確率達99.9999%^_^)”。在感嘆Sante DICOM Editor神器牛掰的同時,想了解一下其底層是如何實現的。通過日常使用以及閱讀軟件幫助手冊推斷其底層依賴庫很可能是dcmtk,就如同本人使用dcmtk、fo-dicom、dcm4che3等諸多DICOM開源庫遇到的兼容性問題類似,——dcmtk兼容性最強,fo-dicom次之,dcm4che3最差

問題:

本篇通過對比dcmtk3.6與dcm4che3.x解析同一特殊dicom文件包含非標准VR的元素)分析dcmtk、dcm4che以及fo-dicom數據加載的兼容性問題。
特殊的dicom文件內容如下:
28 00 20 01 20 20 02 00 30 F8,具體描述如下: 
 
使用dcmtk與fo-dicom加載數據時都未出現錯誤,例如dcmtk加載數據時的提示如下: 
 
由此可以看出dcmtk已經順利識別出了非標准VR的元素(0028,0120),並成功加載。 
雖然使用fo-dicom加載數據沒有出現錯誤,但是對於上述非標准VR的元素(0028,0120)后的元素未順利加載,如下圖所示: 
 
而dcm4che3加載過程中直接彈出了錯誤,如下所示: 

問題分析:

出現該問題的原因是dcm4che3和fo-dicom在解析0028,0120元素時,對於20 20的非標准VR無法識別。下文中將通過分析dcm4che3與dcmtk的源碼來定位問題的具體位置並給出解決方案(此處暫時只對比分析了dcm4che3.3.8最新版與dmctk3.6的源碼,對於fo-dicom的源碼分析待后續整理完成后再補充)

1. dcmtk3.6源碼:

使用dcmtk編寫本次數據加載測試工程,簡單的示例代碼如下:

int main() { OFLog::configure(OFLogger::TRACE_LOG_LEVEL); char* ifname = "c:\\1.dcm"; E_FileReadMode readMode = /*ERM_fileOnly*/ERM_autoDetect; E_TransferSyntax xfer = EXS_Unknown; Uint32 maxReadLength = DCM_MaxReadLength; bool loadIntoMemory = true; DcmFileFormat dfile; DcmObject *dset = &dfile; if (readMode == ERM_dataset) dset = dfile.getDataset(); OFCondition cond = dfile.loadFile(ifname, xfer, EGL_noChange, maxReadLength, readMode); if (cond.bad()) { return 1; } return 0; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

單步調試,可以知道dcmtk加載dicom文件的流程如下:

  1. 創建DcmMetaInfo、DcmDataset元素
  2. 分別加載DcmMetaInfo、DcmDataset元素
  3. 使用DcmItem中的readGroupLength、readTagAndLength、readSubElement逐步加載DcmMetaInfo、DcmDataset的各個子元素。

在DcmItem類中對於非標准VR元素有相應的警告提示信息,

/* if the VR which was read is not a standard VR, print a warning */ if (!vr.isStandard()) { OFOStringStream oss; oss << "DcmItem: Non-standard VR '" << ((OFstatic_cast(unsigned char, vrstr[0]) < 32) ? ' ' : vrstr[0]) << ((OFstatic_cast(unsigned char, vrstr[1]) < 32) ? ' ' : vrstr[1]) << "' (" << STD_NAMESPACE hex << STD_NAMESPACE setfill('0') << STD_NAMESPACE setw(2) << OFstatic_cast(unsigned int, vrstr[0] & 0xff) << "\\" << STD_NAMESPACE setw(2) << OFstatic_cast(unsigned int, vrstr[1] & 0xff) << ") encountered while parsing element " << newTag << OFStringStream_ends; OFSTRINGSTREAM_GETSTR(oss, tmpString) /* encoding of this data element might be wrong, try to correct it */ if (dcmAcceptUnexpectedImplicitEncoding.get()) { DCMDATA_WARN(tmpString << ", trying again with Implicit VR Little Endian"); /* put back read bytes to input stream ... */ inStream.putback(); bytesRead = 0; /* ... and retry with Implicit VR Little Endian transfer syntax */ return readTagAndLength(inStream, EXS_LittleEndianImplicit, tag, length, bytesRead); } else { DCMDATA_WARN(tmpString << ", assuming " << (vr.usesExtendedLengthEncoding() ? "4" : "2") << " byte length field"); } OFSTRINGSTREAM_FREESTR(tmpString) } /* set the VR which was read in the above created tag object. */ newTag.setVR(vr); /* increase counter by 2 */ bytesRead += 2;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

在警告后,對於非標准VR元素的處理過程如下:

 /* read the value in the length field. In some cases, it is 4 bytes wide, in other */ /* cases only 2 bytes (see DICOM standard part 5, section 7.1.1) */ if (xferSyn.isImplicitVR() || nxtobj == EVR_na) //note that delimitation items don't have a VR { inStream.read(&valueLength, 4); //length field is 4 bytes wide swapIfNecessary(gLocalByteOrder, byteOrder, &valueLength, 4, 4); bytesRead += 4; } else { //the transfer syntax is explicit VR DcmVR vr(newTag.getEVR()); if (vr.usesExtendedLengthEncoding()) { Uint16 reserved; inStream.read(&reserved, 2); // 2 reserved bytes inStream.read(&valueLength, 4); // length field is 4 bytes wide swapIfNecessary(gLocalByteOrder, byteOrder, &valueLength, 4, 4); bytesRead += 6; } else { Uint16 tmpValueLength; inStream.read(&tmpValueLength, 2); // length field is 2 bytes wide swapIfNecessary(gLocalByteOrder, byteOrder, &tmpValueLength, 2, 2); bytesRead += 2; valueLength = tmpValueLength; } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

由上述代碼可知,0028,0120VR=20,20,被dcmtk解析為 EVR_UNKNOWN2B類型,如同代碼注釋中所描述:

/// used internally for elements with unknown VR with 2-byte length field in explicit VR 
EVR_UNKNOWN2B

DICOM標准PS5的7.1.2有對於非標准VR的相關描述,如下: 

2. dcm4che3.3.8源碼:

再對比dcm4che3.3.8的源碼,單步調試發現,對於0028,0120VR=20,20,被dcmtk直接標記為UN類型,

public static VR valueOf(int code) { try { VR vr = VALUE_OF[indexOf(code)]; if (vr != null) return vr; } catch (IndexOutOfBoundsException e) {} LOG.warn("Unrecogniced VR code: {0}H - treat as UN", Integer.toHexString(code)); return UN; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

並且在dcm4che3中對於UN類型定義為 
 
此處UN類型是參照上述截圖中DICOM3.0標准對於VR=UN(unknown)類型的標簽約束來定義的,即,其VR字段應該是四個字節。然而此處0028,0120VR=20,20后的Value Length只有兩個字節02 00。因此導致dcm4che3在加載0028,0120元素時,將其長度錯誤地解析為4163895298,即十六進制的F8 30 00 02,如下圖所示: 

解決方案:

至此我們找到了dcm4che3錯誤解析0028,0120VR=20,20非標准VR元素的原因。對於這種非標准VR不能統一當做VR.UN類型進行處理,而應該根據其后續的Value Length的具體長度為2或者4來進行分類處理關於該問題后續博文會繼續深入剖析,請注意),需要修改的地方有兩處:

1. 正確解析Non-standard VR:

//VR.java,Line 110 public static VR valueOf(int code) { try { VR vr = VALUE_OF[indexOf(code)]; if (vr != null) return vr; } catch (IndexOutOfBoundsException e) {} LOG.warn("Unrecogniced VR code: {0}H - treat as UN", Integer.toHexString(code)); //return UN; LOG.warn("zssure:to solve non-standard VR,Unrecogniced VR code: {0}H - treat as UN", Integer.toHexString(code)); return null;//zssure:to solve non-standard VR } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

2. 正確讀取Non-standard VR的VL:

//DicomInputStream.java Line 386 public int readHeader() throws IOException { byte[] buf = buffer; tagPos = pos; readFully(buf, 0, 8); switch(tag = ByteUtils.bytesToTag(buf, 0, bigEndian)) { case Tag.Item: case Tag.ItemDelimitationItem: case Tag.SequenceDelimitationItem: vr = null; break; default: if (explicitVR) { vr = VR.valueOf(ByteUtils.bytesToVR(buf, 4)); //zssure:to solve non-standard VR //referred:dcmtk/dcitem.cc/readTagAndLength,Line 970 if(vr == null) { length = ByteUtils.bytesToUShort(buf, 6, bigEndian); return tag; } //zssure:end if (vr.headerLength() == 8) { length = ByteUtils.bytesToUShort(buf, 6, bigEndian); return tag; } readFully(buf, 4, 4); } else { vr = VR.UN; } } length = ByteUtils.bytesToInt(buf, 4, bigEndian); return tag; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

測試文件下載:

本文中使用的測試數據已經上傳到了我Github的CSDN倉庫中,可自行下載,為了保護患者隱私已經進行了匿名化處理。
Download Non-standard VR test dcm file

后續博文介紹:

1. 由dcm4che3.x庫看Java流操作之”流的拷貝”
2. Eclipse自動編譯dcm4che3.x源碼
3. DICOM三大開源庫對比分析之“數據加載”(續)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM