zoukankan      html  css  js  c++  java
  • 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的结构如下:

    显示VR:VR为OB OW OF UT SQ UN的元素结构

    组号

    元素号

    VR

    预留

    值长度

    数据元素值

    2

    2

    2

    2(0x00,0x00)

    4

    由数据长度决定

    显示VR:VR为普通类型时元素结构(少了预留那一行)

    组号

    元素号

    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"://显示little
     4         isLitteEndian = true;
     5         isExplicitVR = true;
     6         break;
     7     case "1.2.840.10008.1.2.2"://显示big
     8         isLitteEndian = false;
     9         isExplicitVR = true;
    10         break;
    11     case "1.2.840.10008.1.2"://隐式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() + "
    ";
     44                     textBox1.Text += enor.Value.ToString().Replace('', ' ');
     45                 }
     46                 else
     47                     textBox1.Text += enor.Key.ToString() + enor.Value.ToString().Replace('', ' ') + "
    ";
     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"://显示little
    248                             isLitteEndian = true;
    249                             isExplicitVR = true;
    250                             break;
    251                         case "1.2.840.10008.1.2.2"://显示big
    252                             isLitteEndian = false;
    253                             isExplicitVR = true;
    254                             break;
    255                         case "1.2.840.10008.1.2"://隐式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年新年快乐。

    这篇文章发布很久了 感谢朋友们的关注,分析讲解跟代码有点混乱 感觉有点敷衍了事纯粹赚人气的感觉 对不住大家了。另外本文的调窗代码是有问题的 升级版本请看《医学影像调窗技术》一文中的改进代码。

  • 相关阅读:
    POJ 1003 解题报告
    POJ 1004 解题报告
    POJ-1002 解题报告
    vi--文本编辑常用快捷键之光标移动
    常用图表工具
    September 05th 2017 Week 36th Tuesday
    September 04th 2017 Week 36th Monday
    September 03rd 2017 Week 36th Sunday
    September 02nd 2017 Week 35th Saturday
    September 01st 2017 Week 35th Friday
  • 原文地址:https://www.cnblogs.com/fx2008/p/5508620.html
Copyright © 2011-2022 走看看