dicom格式文件 界定標識符的處理

dicom格式文件 界定標識符的處理

說到底無非幾個事情 :
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數據對象的處理 。我這講講當然很簡單 看上去很容易的樣子 ,因爲我已經親手一行代碼一行代碼的實現了。 代碼很多請同學們 仔細閱讀每一個細節 他們之間的調用關係及邏輯。很多地方我沒講到 爲了限制篇幅其實有很多與重點部分無關的代碼貼出來的時候我已經刪除了,但是源碼文件裏有。

源碼及測試數據下載猛擊此處
做一個好的程序員是要有縝密的思維跟耐心的

發佈了24 篇原創文章 · 獲贊 12 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章