Dicom格式文件解析器

Dicom格式文件解析器

Dicom全稱是醫學數字圖像與通訊,這裏講的暫不涉及通訊那方面的問題 只講*.dcm 也就是diocm格式文件的讀取,讀取本身是沒啥難度的 無非就是字節碼數據流處理。只不過確實比較繁瑣。
好了 正題

分析


整體結構先是128字節所謂的導言部分,說俗點就是沒啥意義的破數據 跳過就是了,然後是dataElement依次排列的方式 就是一個dataElement接一個dataElement的方式排到文件結尾 通俗的講dataElement就是指tag 就是破Dicom標準裏定義的數據字典。tag是4個字節表示的 前兩字節是組號後兩字節是偏移號 比如0008,0018。所有dataElement在文件中都是按tag排序的 比如0002,0001  0002,0002  0003,0011
文件整體結構如下:

又把論文裏的這圖貼上來 總結的很好。單個dataElement的結構如下:

顯示VRVROB OW OF UT SQ UN的元素結構

組號

元素號

VR

預留

值長度

數據元素值

2

2

2

20x00,0x00

4

由數據長度決定

顯示VRVR爲普通類型時元素結構(少了預留那一行)

組號

元素號

VR

值長度

數據元素值

2

2

2

4

由數據長度決定

隱式VR 時元素結構

組號

元素號

值長度

數據元素值

2

2

4

由數據長度決定

 


要問VR是啥東東 ,值表示法 啥叫值表示法啊 俺不懂 int string short ushort 懂不 就是這個意思,Dicom標準真坑爹 非要整個怪怪的概念。
VR總共27個 跟c#值類型對應關係我都寫好了:

複製代碼
 1 string getVF(string VR, byte[] VF)
 2 {
 3     string VFStr = string.Empty;
 4     switch (VR)
 5     {
 6         case "SS":
 7             VFStr = BitConverter.ToInt16(VF, 0).ToString();
 8             break;
 9         case "US":
10             VFStr = BitConverter.ToUInt16(VF, 0).ToString();
11 
12             break;
13         case "SL":
14             VFStr = BitConverter.ToInt32(VF, 0).ToString();
15 
16             break;
17         case "UL":
18             VFStr = BitConverter.ToUInt32(VF, 0).ToString();
19 
20             break;
21         case "AT":
22             VFStr = BitConverter.ToUInt16(VF, 0).ToString();
23 
24             break;
25         case "FL":
26             VFStr = BitConverter.ToSingle(VF, 0).ToString();
27 
28             break;
29         case "FD":
30             VFStr = BitConverter.ToDouble(VF, 0).ToString();
31 
32             break;
33         case "OB":
34             VFStr = BitConverter.ToString(VF, 0);
35             break;
36         case "OW":
37             VFStr = BitConverter.ToString(VF, 0);
38             break;
39         case "SQ":
40             VFStr = BitConverter.ToString(VF, 0);
41             break;
42         case "OF":
43             VFStr = BitConverter.ToString(VF, 0);
44             break;
45         case "UT":
46             VFStr = BitConverter.ToString(VF, 0);
47             break;
48         case "UN":
49             VFStr = Encoding.Default.GetString(VF);
50             break;
51         default:
52             VFStr = Encoding.Default.GetString(VF);
53             break;
54     }
55     return VFStr;
56 }
複製代碼

找個dicom文件在十六進制編輯器下瞧瞧 給你整明白:

所有dataElement從前到後按tag又可簡單分段:

文件元dataElement 不受傳輸語法影響 總是以顯示VR方式表示  因爲它裏面就定義了傳輸語法
普通dataElement 受傳輸語法影響 顯示VR表示方式還是隱式VR表示方式
像素數據dataElement 最重要也是最大的一個數據項 其實存儲的就是圖像數據



幾個特殊的tag很重要 前面說過了tag就是dicom裏定義的字典。文件元dataElement 和跟像素數據相關的dataElement 都很重要,其他的很多 如果全部照顧完的話估計得寫上千行switch語句吧,所以沒有必要一般我們一般只抓取關鍵的tag。並且在隱式語法下要確定VR也必須根據字典來確定
關鍵的tag如下:

複製代碼
  1 string getVR(string tag)
  2 {
  3     switch (tag)
  4     {
  5         case "0002,0000"://文件元信息長度
  6             return "UL";
  7             break;
  8         case "0002,0010"://傳輸語法
  9             return "UI";
 10             break;
 11         case "0002,0013"://文件生成程序的標題
 12             return "SH";
 13             break;
 14         case "0008,0005"://文本編碼
 15             return "CS";
 16             break;
 17         case "0008,0008":
 18             return "CS";
 19             break;
 20         case "0008,1032"://成像時間
 21             return "SQ";
 22             break;
 23         case "0008,1111":
 24             return "SQ";
 25             break;
 26         case "0008,0020"://檢查日期
 27             return "DA";
 28             break;
 29         case "0008,0060"://成像儀器
 30             return "CS";
 31             break;
 32         case "0008,0070"://成像儀廠商
 33             return "LO";
 34             break;
 35         case "0008,0080":
 36             return "LO";
 37             break;
 38         case "0010,0010"://病人姓名
 39             return "PN";
 40             break;
 41         case "0010,0020"://病人id
 42             return "LO";
 43             break;
 44         case "0010,0030"://病人生日
 45             return "DA";
 46             break;
 47         case "0018,0060"://電壓
 48             return "DS";
 49             break;
 50         case "0018,1030"://協議名
 51             return "LO";
 52             break;
 53         case "0018,1151":
 54             return "IS";
 55             break;
 56         case "0020,0010"://檢查ID
 57             return "SH";
 58             break;
 59         case "0020,0011"://序列
 60             return "IS";
 61             break;
 62         case "0020,0012"://成像編號
 63             return "IS";
 64             break;
 65         case "0020,0013"://影像編號
 66             return "IS";
 67             break;
 68         case "0028,0002"://像素採樣1爲灰度3爲彩色
 69             return "US";
 70             break;
 71         case "0028,0004"://圖像模式MONOCHROME2爲灰度
 72             return "CS";
 73             break;
 74         case "0028,0010"://row高
 75             return "US";
 76             break;
 77         case "0028,0011"://col寬
 78             return "US";
 79             break;
 80         case "0028,0100"://單個採樣數據長度
 81             return "US";
 82             break;
 83         case "0028,0101"://實際長度
 84             return "US";
 85             break;
 86         case "0028,0102"://採樣最大值
 87             return "US";
 88             break;
 89         case "0028,1050"://窗位
 90             return "DS";
 91             break;
 92         case "0028,1051"://窗寬
 93             return "DS";
 94             break;
 95         case "0028,1052":
 96             return "DS";
 97             break;
 98         case "0028,1053":
 99             return "DS";
100             break;
101         case "0040,0008"://文件夾標籤
102             return "SQ";
103             break;
104         case "0040,0260"://文件夾標籤
105             return "SQ";
106             break;
107         case "0040,0275"://文件夾標籤
108             return "SQ";
109             break;
110         case "7fe0,0010"://像素數據開始處
111             return "OW";
112             break;
113         default:
114             return "UN";
115             break;
116     }
117 }
複製代碼

 

最關鍵的兩個tag:
0002,0010
普通tag的讀取方式 little字節序還是big字節序  隱式VR還是顯示VR。由它的值決定

複製代碼
 1 switch (VFStr)
 2 {
 3     case "1.2.840.10008.1.2.1\0"://顯示little
 4         isLitteEndian = true;
 5         isExplicitVR = true;
 6         break;
 7     case "1.2.840.10008.1.2.2\0"://顯示big
 8         isLitteEndian = false;
 9         isExplicitVR = true;
10         break;
11     case "1.2.840.10008.1.2\0"://隱式little
12         isLitteEndian = true;
13         isExplicitVR = false;
14         break;
15     default:
16         break;
17 }
複製代碼

7fe0,0010
像素數據開始處

整理

根據以上的分析相信解析一個dicom格式文件的過程已經很清晰了吧
第一步:跳過128字節導言部分,並讀取"DICM"4個字符 以確認是dicom格式文件
第二步:讀取第一部分 也就是非常重要的文件元dataElement 。讀取所有0002開頭的tag 並根據0002,0010的值確定傳輸語法。文件元tag部分的數據元素都是以顯示VR的方式表示的 讀取它的值 也就是字節碼處理 別告訴我說你不會字節碼處理哈。傳輸語法 說得那麼官方,你就忽悠吧 其實就確定兩個東西而已 
1字節序 這個基本上都是little字節序。舉個例子吧十進制數 35280 用十六進制表示是0xff00 但是存儲到文件中你用十六進制編輯器打開你看到的是這個樣子00ff 這就是little字節序。平常我們用的x86PC在windows下都是little字節序 包括AMD的CPU。別太較真 較真的話這個問題又可以寫篇博客了。
2確定從0002以後的dataElement的VR是顯示還是隱式。說來說去0002,0010的值就 那麼固定幾個 並且只能是那麼幾個 這些都在那個北美放射學會定義的dicom標準的第六章 有說明 :

1.2.840.10008.1.2 Implicit VR Little Endian: Default Transfer Syntax for DICOM Transfer Syntax
1.2.840.10008.1.2.1 Explicit VR Little Endian Transfer Syntax
1.2.840.10008.1.2.2 Explicit VR Big Endian Transfer Syntax

上面的那段代碼其實就是這個表格的實現,講到這裏你會覺得多麼的坑爹啊 是的dicom面向對象的破概念非常煩的。
第三步:讀取普通tag 直到搜尋到7fe0,0010 這個最巨體的存儲圖像數據的 dataElement 它一個頂別人幾十個 上百個。我們在前一步已經把VR是顯示還是隱式確定 通過前面的圖 ,也就是字節碼處理而已無任何壓力。顯示情況下根據VR 和Len 確定數據類型 跟數據長度直接讀取就可以了。隱式情況下這破玩藝兒有點煩,只能根據tag 字典確定它是什麼VR再才能讀取。關於這個字典也在dicom標準的第六章。上面倒數第二段代碼已經把重要的字典都列了出來。
第四步:讀取灰度像素數據並調窗 以GDI的方式顯示出來。 說實話開始我還以爲dicom這種號稱醫學什麼影像的專家制定出來的標準 讀取像素數據應該有難度吧 結果沒想到這麼的傻瓜。直接按像素從左到右從上到下 一行行依次掃描。兩個字節表示1個像素普通Dicom格式存儲的是16位的灰度圖像,其實有效數據只有12位,除去0 所以最高值是2047。比如CT值 從-1000到+1000,空氣的密度爲-1000 水的密度爲0 金屬的密度爲+1000 總共的值爲2000


調窗技術:
即把12級灰度的數據 通過調節窗寬窗位並讓他在RGB模式下顯示出來。還技術呢 說實話這個也是沒什麼技術含量的所謂的技術,兩句代碼給你整明白。
調節窗寬窗位到底什麼意思,12位的數據那麼它總共有2047個等級的灰度 沒有顯示設備可以體現兩千多級的明暗度 就算有我們肉眼也無法分辨更無法診斷。我們要診斷是要提取關鍵密度值的數據 在醫院放射科呆久了你一定經常聽醫生講什麼骨窗 肺窗 之類的詞兒,這就是指的這個“窗”。比如有病人骨折了打了鋼板我們想看金屬部分來診斷 那麼我們應該抓取CT值從800到1000 密度的像素 也就是灰度值 然後把它放到RGB模式下顯示,低於800的不論值大小都顯示黑色 高於1000的不論值大小都顯示白色。
通過以上例子那麼這個範圍1000-800=200 這個200表示窗寬,800+(200/2)這個表示窗位
一句話,從2047個等級的灰度裏選取一個範圍放到0~255的灰度環境裏顯示。

怎樣把12位灰度影射到8位灰度顯示出來呢,還怎麼顯示 上面方法都給說明了基本上算半成品了。聯想到角度制弧度制,設要求的8位灰度值爲x 已知的12位灰度值爲y那麼:x/255=y/2047 那麼x=255y/2047 原理不多講 等比中項十字相乘法 這個是初中的知識哈。初中沒讀過的童鞋飄過。。。

原理過程講完了

代碼走起

複製代碼
  1 class DicomHandler
  2     {
  3         string fileName = "";
  4         Dictionary<string, string> tags = new Dictionary<string, string>();//dicom文件中的標籤
  5         BinaryReader dicomFile;//dicom文件流
  6 
  7         //文件元信息
  8         public Bitmap gdiImg;//轉換後的gdi圖像
  9         UInt32 fileHeadLen;//文件頭長度
 10         long fileHeadOffset;//文件數據開始位置
 11         UInt32 pixDatalen;//像素數據長度
 12         long pixDataOffset = 0;//像素數據開始位置
 13         bool isLitteEndian = true;//是否小字節序(小端在前 、大端在前)
 14         bool isExplicitVR = true;//有無VR
 15 
 16         //像素信息
 17         int colors;//顏色數 RGB爲3 黑白爲1
 18         public int windowWith = 2048, windowCenter = 2048 / 2;//窗寬窗位
 19         int rows, cols;
 20         public void readAndShow(TextBox textBox1)
 21         {
 22             if (fileName == string.Empty)
 23                 return;
 24             dicomFile = new BinaryReader(File.OpenRead(fileName));
 25 
 26             //跳過128字節導言部分
 27             dicomFile.BaseStream.Seek(128, SeekOrigin.Begin);
 28 
 29             if (new string(dicomFile.ReadChars(4)) != "DICM")
 30             {
 31                 MessageBox.Show("沒有dicom標識頭,文件格式錯誤");
 32                 return;
 33             }
 34 
 35 
 36             tagRead();
 37 
 38             IDictionaryEnumerator enor = tags.GetEnumerator();
 39             while (enor.MoveNext())
 40             {
 41                 if (enor.Key.ToString().Length > 9)
 42                 {
 43                     textBox1.Text += enor.Key.ToString() + "\r\n";
 44                     textBox1.Text += enor.Value.ToString().Replace('\0', ' ');
 45                 }
 46                 else
 47                     textBox1.Text += enor.Key.ToString() + enor.Value.ToString().Replace('\0', ' ') + "\r\n";
 48             }
 49             dicomFile.Close();
 50         }
 51         public  DicomHandler(string _filename)
 52         {
 53             fileName = _filename;
 54         }
 55 
 56         public void saveAs(string filename)
 57         {
 58             switch (filename.Substring(filename.LastIndexOf('.')))
 59             {
 60                 case ".jpg":
 61                     gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
 62                     break;
 63                 case ".bmp":
 64                     gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Bmp);
 65                     break;
 66                 case ".png":
 67                     gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Png);
 68                     break;
 69                 default:
 70                     break;
 71             }
 72         }
 73         public bool getImg( )//獲取圖像 在圖像數據偏移量已經確定的情況下
 74         {
 75             if (fileName == string.Empty)
 76                 return false;
 77             
 78             int dataLen, validLen;//數據長度 有效位
 79             int imgNum;//幀數
 80 
 81             rows = int.Parse(tags["0028,0010"].Substring(5));
 82             cols = int.Parse(tags["0028,0011"].Substring(5));
 83 
 84             colors = int.Parse(tags["0028,0002"].Substring(5));
 85             dataLen = int.Parse(tags["0028,0100"].Substring(5));
 86             validLen = int.Parse(tags["0028,0101"].Substring(5));
 87 
 88             gdiImg = new Bitmap(cols, rows);
 89 
 90             BinaryReader dicomFile = new BinaryReader(File.OpenRead(fileName));
 91 
 92             dicomFile.BaseStream.Seek(pixDataOffset, SeekOrigin.Begin);
 93 
 94             long reads = 0;
 95             for (int i = 0; i < gdiImg.Height; i++)
 96             {
 97                 for (int j = 0; j < gdiImg.Width; j++)
 98                 {
 99                     if (reads >= pixDatalen)
100                         break;
101                     byte[] pixData = dicomFile.ReadBytes(dataLen / 8 * colors);
102                     reads += pixData.Length;
103 
104                     Color c = Color.Empty;
105                     if (colors == 1)
106                     {
107                         int grayGDI;
108 
109                         double gray = BitConverter.ToUInt16(pixData, 0);
110                         //調窗代碼,就這麼幾句而已 
111                         //1先確定窗口範圍 2映射到8位灰度
112                         int grayStart = (windowCenter - windowWith / 2);
113                         int grayEnd = (windowCenter + windowWith / 2);
114 
115                         if (gray < grayStart)
116                             grayGDI = 0;
117                         else if (gray > grayEnd)
118                             grayGDI = 255;
119                         else
120                         {
121                             grayGDI = (int)((gray - grayStart) * 255 / windowWith);
122                         }
123 
124                         if (grayGDI > 255)
125                             grayGDI = 255;
126                         else if (grayGDI < 0)
127                             grayGDI = 0;
128                         c = Color.FromArgb(grayGDI, grayGDI, grayGDI);
129                     }
130                     else if (colors == 3)
131                     {
132                         c = Color.FromArgb(pixData[0], pixData[1], pixData[2]);
133                     }
134 
135                     gdiImg.SetPixel(j, i, c);
136                 }
137             }
138 
139             dicomFile.Close();
140             return true;
141         }
142         void tagRead()//不斷讀取所有tag 及其值 直到碰到圖像數據 (7fe0 0010 )
143         {
144             bool enDir = false;
145             int leve = 0;
146             StringBuilder folderData = new StringBuilder();//該死的文件夾標籤
147             string folderTag = "";
148             while (dicomFile.BaseStream.Position + 6 < dicomFile.BaseStream.Length)
149             {
150                 //讀取tag
151                 string tag = dicomFile.ReadUInt16().ToString("x4") + "," +
152                 dicomFile.ReadUInt16().ToString("x4");
153 
154                 string VR = string.Empty;
155                 UInt32 Len = 0;
156                 //讀取VR跟Len
157                 //對OB OW SQ 要做特殊處理 先置兩個字節0 然後4字節值長度
158                 //------------------------------------------------------這些都是在讀取VR一步被阻斷的情況
159                 if (tag.Substring(0, 4) == "0002")//文件頭 特殊情況
160                 {
161                     VR = new string(dicomFile.ReadChars(2));
162 
163                     if (VR == "OB" || VR == "OW" || VR == "SQ" || VR == "OF" || VR == "UT" || VR == "UN")
164                     {
165                         dicomFile.BaseStream.Seek(2, SeekOrigin.Current);
166                         Len = dicomFile.ReadUInt32();
167                     }
168                     else
169                         Len = dicomFile.ReadUInt16();
170                 }
171                 else if (tag == "fffe,e000" || tag == "fffe,e00d" || tag == "fffe,e0dd")//文件夾標籤
172                 {
173                     VR = "**";
174                     Len = dicomFile.ReadUInt32();
175                 }
176                 else if (isExplicitVR == true)//有無VR的情況
177                 {
178                     VR = new string(dicomFile.ReadChars(2));
179 
180                     if (VR == "OB" || VR == "OW" || VR == "SQ" || VR == "OF" || VR == "UT" || VR == "UN")
181                     {
182                         dicomFile.BaseStream.Seek(2, SeekOrigin.Current);
183                         Len = dicomFile.ReadUInt32();
184                     }
185                     else
186                         Len = dicomFile.ReadUInt16();
187                 }
188                 else if (isExplicitVR == false)
189                 {
190                     VR = getVR(tag);//無顯示VR時根據tag一個一個去找 真煩啊。
191                     Len = dicomFile.ReadUInt32();
192                 }
193                 //判斷是否應該讀取VF 以何種方式讀取VF
194                 //-------------------------------------------------------這些都是在讀取VF一步被阻斷的情況
195                 byte[] VF = { 0x00 };
196 
197                 if (tag == "7fe0,0010")//圖像數據開始了
198                 {
199                     pixDatalen = Len;
200                     pixDataOffset = dicomFile.BaseStream.Position;
201                     dicomFile.BaseStream.Seek(Len, SeekOrigin.Current);
202                     VR = "UL";
203                     VF = BitConverter.GetBytes(Len);
204                 }
205                 else if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue))//靠 遇到文件夾開始標籤了
206                 {
207                     if (enDir == false)
208                     {
209                         enDir = true;
210                         folderData.Remove(0, folderData.Length);
211                         folderTag = tag;
212                     }
213                     else
214                     {
215                         leve++;//VF不賦值
216                     }
217                 }
218                 else if ((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue))//文件夾結束標籤
219                 {
220                     if (enDir == true)
221                     {
222                         enDir = false;
223                     }
224                     else
225                     {
226                         leve--;
227                     }
228                 }
229                 else
230                     VF = dicomFile.ReadBytes((int)Len);
231 
232                 string VFStr;
233 
234                 VFStr = getVF(VR, VF);
235 
236                 //----------------------------------------------------------------針對特殊的tag的值的處理
237                 //特別針對文件頭信息處理
238                 if (tag == "0002,0000")
239                 {
240                     fileHeadLen = Len;
241                     fileHeadOffset = dicomFile.BaseStream.Position;
242                 }
243                 else if (tag == "0002,0010")//傳輸語法 關係到後面的數據讀取
244                 {
245                     switch (VFStr)
246                     {
247                         case "1.2.840.10008.1.2.1\0"://顯示little
248                             isLitteEndian = true;
249                             isExplicitVR = true;
250                             break;
251                         case "1.2.840.10008.1.2.2\0"://顯示big
252                             isLitteEndian = false;
253                             isExplicitVR = true;
254                             break;
255                         case "1.2.840.10008.1.2\0"://隱式little
256                             isLitteEndian = true;
257                             isExplicitVR = false;
258                             break;
259                         default:
260                             break;
261                     }
262                 }
263                 for (int i = 1; i <= leve; i++)
264                     tag = "--" + tag;
265                 //------------------------------------數據蒐集代碼
266                 if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue) || leve > 0)//文件夾標籤代碼
267                 {
268                     folderData.AppendLine(tag + "(" + VR + "):" + VFStr);
269                 }
270                 else if (((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue)) && leve == 0)//文件夾結束標籤
271                 {
272                     folderData.AppendLine(tag + "(" + VR + "):" + VFStr);
273                     tags.Add(folderTag + "SQ", folderData.ToString());
274                 }
275                 else
276                     tags.Add(tag, "(" + VR + "):" + VFStr);
277             }
278         }
279 }
複製代碼

好了收工。
測試下成果

複製代碼
 1 if (openFileDialog1.ShowDialog() != DialogResult.OK)
 2     return;
 3 
 4 string fileName = openFileDialog1.FileName;
 5 
 6 handler = new DicomHandler(fileName);
 7 
 8 handler.readAndShow(textBox1);
 9 
10 this.Text = "DicomViewer-" + openFileDialog1.FileName;
11 
12 
13 backgroundWorker1.RunWorkerAsync();
複製代碼

這裏處理gdi位圖的時候直接用的setPix 處理速度比較慢所以用了backgroundWorker,實際應用中請使用內存緩衝跟指針的方式
否則效率低了是得不到客戶的認可的哦,gdi位圖操作可使用lockBits加指針的方式 ,12位的灰度像素數據可以第一次讀取後緩存到內存中 以方便後面調窗的快速讀取
優化這點代碼也不難哈 對指針什麼的熟點就行了,前幾章都有。

這是ezDicom 經過公認測試的軟件 我們來跟他對比一下,打開 
調窗測試,我們注意到兩個東西 在沒有窗寬窗位時 默認窗寬是2047+1即2048  窗位是2048/2即1024
直觀的感受是調窗寬像在調圖像對比度 ,調窗位像在調圖像亮度。
窗寬爲255的時候圖像是最瑞麗的 因爲255其實就是8位圖像的默認窗寬。
注意窗位那裏有小小區別,ez窗位顯示的是根據1024那裏爲0開始偏移 而我的程序是根據窗寬中間值沒有偏移
沒有偏移的情況稍微符合邏輯點吧。
但是可以看到原理是一樣的 結果是一樣的。


源碼下載測試dcm文件: 猛擊此處

最近也沒有以前寫的文章那麼歡樂了 不知道爲什麼,長大了 沒有以前開心了 呵呵 。
筒子們2013年新年快樂。

這篇文章發佈很久了 感謝朋友們的關注,分析講解跟代碼有點混亂 感覺有點敷衍了事純粹賺人氣的感覺 對不住大家了。另外本文的調窗代碼是有問題的 升級版本請看《醫學影像調窗技術》一文中的改進代碼。

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