說到底無非幾個事情 :
1傳輸語法確定
2數據元素讀取
3 7fe0,0010元素 也就是圖像數據處理。
關於這整個過程已經不想多說了 在我的上上一篇博客里已經基本實現了。 當然還很有問題比如圖像調窗就有bug 這個以后再說吧。眾所周知dicom格式文件是由一個接一個連續的“數據元素”組成的。
這次我們只講怎樣去處理文件里一種特殊的數據元素:那就是VR為SQ類型的元素 還有delimited 也就是界定標識付 我們暫且把它歸為一類。 為什么特殊呢?因為其他元素都很簡單 ,根據傳輸語法:-> tag_有無顯式VR_len_VF 這樣的結構。 len是數據長度, VF是固定字節數的字節流數據。 而遇到SQ類型的數據元素就麻煩了 首先他的len是FFFFFFFF 就是無長度。然后是VF 他是一種“文件夾”結構 ,就是里面嵌套其他數據元素,可能嵌套一層 可能嵌套幾層 處理是很棘手的問題。
說了這么多我們先來直觀的看一下 找到我們上一篇文章《Dicom格式文件解析器》打開測試數據IM-0001-0002.dcm 看到tag數據里有很多類似這樣的:
0040,0275SQ
0040,0275(SQ):00
--fffe,e000(**):
--0040,0007(LO):CT2 t阾e, face, sinus
----0040,0008(SQ):00
------fffe,e000(**):
------0008,0100(SH):CTTETE
------0008,0102(SH):XPLORE
------0008,0104(LO):CT2 T蔜E, FACE, SINUS
------fffe,e00d(**):
----fffe,e0dd(**):
----0040,0009(SH):A10011234815
----0040,1001(SH):A10011234814
--fffe,e00d(**):
fffe,e0dd(**):
事先已經知道VR是顯式方式的explicit VR 字節序是little edition,這里只是測試 那么我們直接就在處理代碼class的變量里寫死了:
1 class Reader 2 { 3 4 WarpedStream.byteOrder bytOrder = WarpedStream.byteOrder.littleEdition; 5 bool ExplicitVR = true; 6 7 }
然后我們把組號=0002開頭的元素都剔除了 然后把7fe0,0010 也就是圖像數據去掉了。這里我們已經有根據上述方式從IM-0001-0002.dcm分離出來的數據元素內容IM-0001-0002.bin 我們來看下IM-0001-0002.bin的二進制流數據組織方式:
對應元素0040,0275看
通過觀察有如下規律,tag的VR類型如果等於SQ len=ffffffff 那么它必定以fffe,e0dd len=00000000結尾。 如果tag=fffe,e000 len=ffffffff 必定以fffe,e00d len=00000000 結尾 但是tag=fffe,e000 並不能稱之為為節點下的元素。通過dicom標識我們知道 元素同一節點下只能出現一次,而tag=fffe,e000 可以出現多次。這稱之為界定標識符 即Delimiter,並且他們是成對的, 有DelimiterStart 就有DelimiterEnd 就是通過這種一個包一個的嵌套方式實現了一個樹狀目錄結構 。 簡而言之我們要做的就是解析他。 但是在《Dicom格式文件解析器》里不是已經實現了么,他的關鍵代碼 是這樣做的:
1 if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue))// 遇到文件夾開始標簽了 2 { 3 if (enDir == false) 4 { 5 enDir = true; 6 folderData.Remove(0, folderData.Length); 7 folderTag = tag; 8 } 9 else 10 { 11 leve++;//VF不賦值 12 } 13 } 14 else if ((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue))//文件夾結束標簽 15 { 16 if (enDir == true) 17 { 18 enDir = false; 19 } 20 else 21 { 22 leve--; 23 } 24 } 25 else 26 VF = dicomFile.ReadBytes((int)Len); 27 28 string VFStr; 29 30 VFStr = getVF(VR, VF); 31 32 for (int i = 1; i <= leve; i++) 33 tag = "--" + tag; 34 //------------------------------------數據搜集代碼 35 if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue) || leve > 0)//文件夾標簽代碼 36 { 37 folderData.AppendLine(tag + "(" + VR + "):" + VFStr); 38 } 39 else if (((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue)) && leve == 0)//文件夾結束標簽 40 { 41 folderData.AppendLine(tag + "(" + VR + "):" + VFStr); 42 tags.Add(folderTag + "SQ", folderData.ToString()); 43 } 44 else 45 tags.Add(tag, "(" + VR + "):" + VFStr);
看得出基本邏輯就是 遇見DelimiterStart 則level++,遇見DelimiterEnd 則level-- 直至根節點VR=SQ的元素結束,把所有同一節點下的數據全都append到一個stringBuilder下。看得出來當時只是為了實現功能 代碼比較簡單 並沒有用到遞歸 ,並且dataelement的“數據模型”也沒有實現。
現在我們就用遞歸算法來重新實現這個解析過程:
首先應當把數據元素封裝成一個結構體,為了他能夠實現層級目錄結構 ,通過觀察windows文件系統的結構 那么它應該是這樣的:一個目錄下有很多項 有的是文件夾 有的是文件,如果是文件夾那么它下面可能又包括有文件,就是說如果是文件夾則遞歸 否則結束。
dataelement的struct代碼:
1 struct DataElement 2 { 3 public uint _tag; 4 public WarpedStream.byteOrder bytOrder; 5 public bool explicitVR;//是顯式VR的 否則隱式VR 6 public uint tag 7 { 8 get { return _tag; } 9 set 10 { 11 _tag = value; 12 VR = VRs.GetVR(value); 13 uint _len = VRs.getLen(VR); 14 if (_len != 0) 15 len = _len; 16 } 17 } 18 public ushort VR; 19 //雖然長度為uint 但要看情況隱式時都是4字節 顯式時除ow那幾個外都是2字節 20 //如果為ow 顯示不但長度為4 在之前還要跳過2字節,除ow那幾個之外不用跳過 21 public uint len; 22 public byte[] value; 23 public IList<DataElement> items; 24 public bool haveItems; 25 26 public string showValue() 27 { 28 if (haveItems) 29 return null; 30 31 if (value != null) 32 return Tags.VFdecoding(VR, value, bytOrder); 33 else 34 return null; 35 } 36 public void setValue(string valStr) 37 { 38 if (haveItems) 39 return; 40 41 if (len != 0) 42 value = Tags.VFencoding(VR, valStr, bytOrder, len); 43 else 44 { 45 value = Tags.VFencoding(VR, valStr, bytOrder); 46 if (VRs.IsStringValue(VR)) 47 len = (uint)value.Length; 48 } 49 } 50 51 }
haveItems 代表是否是Delimiter ,items則代表它里面的項。母項 跟子項之間用haveItems 和Delimiter 來產生聯系 。為了實現解析我們先得做些前期處理 以供方便調用。就是itemHeader的讀取 ,itemHeader指的是一個數據元素除VF部分的所有 詳細請看《Dicom格式文件解析器》。上面第一段那句話 普通dataelement跟 “文件夾”dataelement的區別 ,前面部分一樣的 主要區別於VF部分 所以我們才寫了這個itemHeader讀取的函數(潛意識的是說通過header去確定VF部分是否包含子元素)。
貼出代碼:
1 public DataElement readItemHeader(WarpedStream pdv_stream) 2 { 3 //bool ExplicitVR = true; 4 //WarpedStream.byteOrder bytOrder = WarpedStream.byteOrder.littleEdition; 5 6 DataElement item_tmp = new DataElement(); 7 item_tmp.bytOrder = bytOrder; 8 9 #region tag 和len 處理部分 10 item_tmp.tag = pdv_stream.readTag(); 11 12 if (item_tmp.tag == 0xfffee000)//針對界定標識符的處理 delimited 13 { 14 item_tmp.len = 0xffffffff; 15 pdv_stream.skip(4); 16 } 17 else if (item_tmp.tag == 0xfffee00d || item_tmp.tag == 0xfffee0dd) 18 { 19 item_tmp.len = 0x00000000; 20 pdv_stream.skip(4); 21 } 22 else if (ExplicitVR)//顯示VR 23 { 24 byte[] vrData = pdv_stream.readBytes(2); 25 Array.Reverse(vrData); 26 item_tmp.VR = BitConverter.ToUInt16(vrData, 0); 27 //ow情況 length=4字節 5個不是 OB OW OF UT SQ UN 外加NONE 28 if (item_tmp.VR == VRs.OB || item_tmp.VR == VRs.OW || item_tmp.VR == VRs.OF || 29 item_tmp.VR == VRs.UT || item_tmp.VR == VRs.SQ || item_tmp.VR == VRs.UN) 30 { 31 pdv_stream.skip(2); 32 item_tmp.len = pdv_stream.readUint(); 33 } 34 else 35 { //非ow情況 length=2字節 36 item_tmp.len = pdv_stream.readUshort(); 37 } 38 } 39 else//隱式VR 自己通過tag找 40 { 41 item_tmp.VR = VRs.GetVR(item_tmp.tag);//調用根據tag找尋vr的函數 42 if (item_tmp.tag == 0xfffee000) 43 item_tmp.len = 0xffffffff; 44 else if (item_tmp.tag == 0xfffee00d || item_tmp.tag == 0xfffee0dd) 45 item_tmp.len = 0x00000000; 46 else 47 item_tmp.len = pdv_stream.readUint(); 48 } 49 #endregion 50 return item_tmp; 51 }
沒啥特殊的 就是讀取數據 只要遵循dicom標准就行了 。看代碼的時候留意下。如果不熟悉請看《Dicom格式文件解析器》一章。解析的遞歸算法代碼實現, 說着挺簡單的實際上還是比較復雜的 但是中心思想還是跟上面一樣遇見DelimiterStart 則level++,遇見DelimiterEnd 則level--。
看上面那句話“主要區別與VF部分 ”,為什么呢 因為VF部分涉及到遞歸調用 把header跟VF部分區分開 ,如果VF類型是文件夾 則遞歸否則結束。遇見DelimiterStart 則陷入遞歸調用,遇見DelimiterEnd 則從遞歸調用中退出一級。
貼出代碼:
1 public void readItem(ref DataElement item, WarpedStream pdv_stream) 2 { 3 //bool ExplicitVR = true; 4 //WarpedStream.byteOrder bytOrder = WarpedStream.byteOrder.littleEdition; 5 6 #region value 處理部分 7 //文件夾標簽情況 8 if ((item.VR == VRs.SQ && item.len == UInt32.MaxValue) || (item.tag == 0xfffee000 && item.len == UInt32.MaxValue)) 9 { 10 item.haveItems = true; 11 item.items = new List<DataElement>(); 12 13 while (true) //讀取所有item 直到根據文件夾結尾標識 不斷的退出所有的遞歸循環; 14 { 15 16 DataElement item_tmp = readItemHeader(pdv_stream);//讀取tag的頭部 即 tag VR Len 17 if (item_tmp.tag == 0xfffee00d || item_tmp.tag == 0xfffee0dd) 18 { 19 //檢查是否文件夾結尾標識的代碼 如果遇到文件夾結尾標識 立即break 別忘了把讀到的tag 字節偏移退回去; 20 //即從已經陷入的遞歸循環里退一級 21 pdv_stream.seek(-item_tmp.getHeaderLen(), SeekOrigin.Current); 22 break; 23 } 24 else if ((item_tmp.VR == VRs.SQ && item_tmp.len == UInt32.MaxValue) || (item_tmp.tag == 0xfffee000 && item_tmp.len == UInt32.MaxValue)) 25 { 26 //pdv_stream.seek(-item_tmp.getHeaderLen(), SeekOrigin.Current);//字節偏移退回去;貌似不用偏移 27 //文件夾標簽起始標識 遞歸 28 //即往遞歸循環里陷入一級 29 readItem(ref item_tmp, pdv_stream); 30 item.items.Add(item_tmp);//items.add代碼(文件夾元素) 31 } 32 else 33 { 34 //普通tag及數據讀取代碼 35 item_tmp.value = pdv_stream.readBytes((int)item_tmp.len); 36 item.items.Add(item_tmp);//items.add代碼(普通元素) 37 } 38 } 39 40 //針對文件夾結束標簽的處理 //讀取文件夾結尾標簽 以跟開始標簽相呼應 41 if (item.VR == VRs.SQ && item.len == UInt32.MaxValue) 42 { 43 //0xfffee0dd len=00000000//(item_tmp.tag == 0xfffee0dd && item_tmp.len == UInt32.MinValue) 44 pdv_stream.skip(4 + 4); 45 } 46 else if (item.tag == 0xfffee000 && item.len == UInt32.MaxValue) 47 { 48 //0xfffee00d len=00000000 49 pdv_stream.skip(4 + 4); 50 } 51 }//普通元素情況 52 else 53 { 54 item.value = pdv_stream.readBytes((int)item.len); 55 } 56 #endregion 57 58 }
代碼沒什么好解釋的 看就是了 有注釋。 最后說下源文件里 VRs.cs 跟Tags.cs 是根據dicom標准編寫的 。里面實現的是幾千個tag跟VR的對應關系。 這當然不是我寫的。 用的別人的成果,幾千個啊你想想不把我整瘋么。
大功告成 我們來調用試下結果:
1 public IDictionary<uint, DataElement> pdvDecoding() 2 { 3 4 //pdvBuffer.Seek(0, SeekOrigin.Begin);//把讀取偏移點設置到開始處 5 6 FileStream fs = new FileStream("IM-0001-0002.bin", FileMode.Open); 7 WarpedStream ws = new WarpedStream(fs, bytOrder); 8 9 IDictionary<uint, DataElement> ds = new Dictionary<uint, DataElement>(); 10 //int indx = 0; 11 while (ws.getPostion() < fs.Length) 12 { 13 DataElement item = readItemHeader(ws); 14 //Console.WriteLine(Tags.ToHexString(item.tag)); 15 readItem(ref item, ws); 16 ds.Add(item.tag, item); 17 //indx++; 18 //if (indx >= 22) 19 // break; 20 21 showItem(item); 22 } 23 24 ws.close(); 25 return ds; 26 } 27 28 int level = 0; 29 public void showItem(DataElement element) 30 { 31 for (int i = 0; i < level; i++) 32 { 33 Console.Write("-"); 34 } 35 if (element.haveItems) 36 { 37 level++; 38 39 Console.WriteLine(Tags.ToHexString(element.tag)); 40 foreach (DataElement item in element.items) 41 { 42 showItem(item); 43 } 44 level--; 45 } 46 else 47 { 48 Console.WriteLine(Tags.ToHexString(element.tag)); 49 } 50 }
這種解析跟數據組織方式 更方便了dicom數據對象的處理 。我這講講當然很簡單 看上去很容易的樣子 ,因為我已經親手一行代碼一行代碼的實現了。 代碼很多請同學們 仔細閱讀每一個細節 他們之間的調用關系及邏輯。很多地方我沒講到 為了限制篇幅其實有很多與重點部分無關的代碼貼出來的時候我已經刪除了,但是源碼文件里有。
源碼及測試數據下載猛擊此處
做一個好的程序員是要有縝密的思維跟耐心的