• Unity 读取DICOM文件,并支持移动端


    本文参考网上大神的源码,在此基础上修改,适配Unity,并支持移动端,本文代码支持安卓端,ios端请各位自行适配,原理不变,只是在数据读取的方法上可以有点区别。

    大神原贴:医学影像调窗技术 - assassinx - 博客园

    Dicom格式文件解析器 - assassinx - 博客园

    下面直接上源码。

      public RawImage testimg;
        public TextMeshProUGUI detext;

        string fileName = "";
        Dictionary tags = new Dictionary();//dicom文件中的标签
        BinaryReader dicomFile = null;//dicom文件流
       
        //文件元信息
        //public Bitmap gdiImg;//转换后的gdi图像
        private Texture2D dicomimg = null;
        UInt32 fileHeadLen;//文件头长度
        long fileHeadOffset;//文件数据开始位置
        UInt32 pixDatalen;//像素数据长度
        long pixDataOffset = 0;//像素数据开始位置
        bool isLitteEndian = true;//是否小字节序(小端在前 、大端在前)
        bool isExplicitVR = true;//有无VR
        MemoryStream mstream;
        //像素信息
        int colors;//颜色数 RGB为3 黑白为1
        //public int windowWith = 2048, windowCenter = 2048 / 2;//窗宽窗位
        private int windowWith = 0, windowCenter = 0;//窗宽窗位
        int rows, cols;

     void Start()
        {
    #if UNITY_EDITOR
            fileName = Application.dataPath + "/StreamingAssets/DicomImage" + "/test.dcm";
    #elif UNITY_IPHONE
                fileName = Application.dataPath +"/Raw/DicomImage"+"/test.dcm";
    #elif UNITY_ANDROID
                //fileName = Application.persistentDataPath + "/DicomImage"+"/test.dcm";
                fileName = "jar:file://" + Application.dataPath + "!/assets/DicomImage"+"/test.dcm";
    #endif
            detext.text += fileName + "\n";
            mstream = new MemoryStream();
            UnityWebRequest request = UnityWebRequest.Get(fileName);
            request.SendWebRequest();//读取数据
            while (true)
            {
                if (request.downloadHandler.isDone)//是否读取完数据
                {
                    mstream.Write(request.downloadHandler.data, 0, request.downloadHandler.data.Length);
                    break;
                }
            }
            readAndShow();
        }

    public void readAndShow()
        {
            if (fileName == string.Empty)
                return;
            dicomFile = new BinaryReader(mstream);
            if(dicomFile == null)
                detext.text += "dicomFile == null" + "\n";
            //跳过128字节导言部分
            dicomFile.BaseStream.Seek(128, SeekOrigin.Begin);

            if (new string(dicomFile.ReadChars(4)) != "DICM")
            {
                //MessageBox.Show("没有dicom标识头,文件格式错误");
                Debug.Log("没有dicom标识头,文件格式错误");
                detext.text += "没有dicom标识头,文件格式错误" + "\n";
                return;
            }
            else
            {
                detext.text += "DICM" + "\n";
            }

     
            tagRead();
            getImg();
            IDictionaryEnumerator enor = tags.GetEnumerator();
           
            dicomFile.Close();
        }

      void tagRead()//不断读取所有tag 及其值 直到碰到图像数据 (7fe0 0010 )
        {
            detext.text += "tagRead" + "\n";
            bool enDir = false;
            int leve = 0;
            StringBuilder folderData = new StringBuilder();//该死的文件夹标签
            string folderTag = "";
            while (dicomFile.BaseStream.Position + 6 < dicomFile.BaseStream.Length)
            {
                //读取tag
                string tag = dicomFile.ReadUInt16().ToString("x4") + "," +
                dicomFile.ReadUInt16().ToString("x4");
                string VR = string.Empty;

                UInt32 Len = 0;
                //读取VR跟Len
                //对OB OW SQ 要做特殊处理 先置两个字节0 然后4字节值长度
                //------------------------------------------------------这些都是在读取VR一步被阻断的情况
                if (tag.Substring(0, 4) == "0002")//文件头 特殊情况
                {
                    //VR = new string(dicomFile.ReadChars(2));
                    VR = Encoding.Default.GetString(dicomFile.ReadBytes(2));
                    if (VR == "OB" || VR == "OW" || VR == "SQ" || VR == "OF" || VR == "UT" || VR == "UN")
                        {
                            dicomFile.BaseStream.Seek(2, SeekOrigin.Current);
                            Len = dicomFile.ReadUInt32();
                        }
                    else
                            Len = dicomFile.ReadUInt16();
                }
                else if (tag == "fffe,e000" || tag == "fffe,e00d" || tag == "fffe,e0dd")//文件夹标签
                    {
                        VR = "**";
                        Len = dicomFile.ReadUInt32();
                    }
                else if (isExplicitVR == true)//有无VR的情况
                    {
                        VR = new string(dicomFile.ReadChars(2));
                
                        if (VR == "OB" || VR == "OW" || VR == "SQ" || VR == "OF" || VR == "UT" || VR == "UN")
                            {
                                dicomFile.BaseStream.Seek(2, SeekOrigin.Current);
                                Len = dicomFile.ReadUInt32();
                            }
                        else
                                Len = dicomFile.ReadUInt16();
                    }
                else if (isExplicitVR == false)
                    {
                        VR = getVR(tag);//无显示VR时根据tag一个一个去找 真烦啊。
                        Len = dicomFile.ReadUInt32();
                    }
                //判断是否应该读取VF 以何种方式读取VF
                //-------------------------------------------------------这些都是在读取VF一步被阻断的情况
                byte[] VF = { 0x00 };
            
                    if (tag == "7fe0,0010")//图像数据开始了
                        {
                            pixDatalen = Len;
                            pixDataOffset = dicomFile.BaseStream.Position;
                            dicomFile.BaseStream.Seek(Len, SeekOrigin.Current);
                            VR = "UL";
                            VF = BitConverter.GetBytes(Len);
                        }
                    else if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue))//靠 遇到文件夹开始标签了
                        {
                            if (enDir == false)
                                {
                                    enDir = true;
                                    folderData.Remove(0, folderData.Length);
                                    folderTag = tag;
                                }
                            else
                                {
                                    leve++;//VF不赋值
                                }
                        }
                    else if ((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue))//文件夹结束标签
                        {
                            if (enDir == true)
                                {
                                    enDir = false;
                                }
                            else
                                {
                                    leve--;
                                }
                        }
                    else
                            VF = dicomFile.ReadBytes((int)Len);
            
                    string VFStr;
            
                    VFStr = getVF(VR, VF);
                //Debug.Log(tag + "   " + VFStr);
                if (tag == "0028,1052")
                {
                    //Debug.Log(VFStr);
                    //windowWith = int.Parse(VFStr);
                }
                if (tag == "0028,1053")
                {
                    //Debug.Log(VFStr);
                    //windowCenter = int.Parse(VFStr);
                }
                //----------------------------------------------------------------针对特殊的tag的值的处理
                //特别针对文件头信息处理
                if (tag == "0002,0000")
                    {
                        fileHeadLen = Len;
                        fileHeadOffset = dicomFile.BaseStream.Position;
                    }
                else if (tag == "0002,0010")//传输语法 关系到后面的数据读取
                {
                    switch (VFStr)
                    {
                        case "1.2.840.10008.1.2.1\0"://显示little
                        isLitteEndian = true;
                                    isExplicitVR = true;
                                    break;
                                case "1.2.840.10008.1.2.2\0"://显示big
                        isLitteEndian = false;
                                    isExplicitVR = true;
                                    break;
                                case "1.2.840.10008.1.2\0"://隐式little
                        isLitteEndian = true;
                                    isExplicitVR = false;
                                    break;
                                default:
                        break;
                    }
                }
                for (int i = 1; i <= leve; i++)
                        tag = "--" + tag;
                //------------------------------------数据搜集代码
                if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue) || leve > 0)//文件夹标签代码
                    {
                        folderData.AppendLine(tag + "(" + VR + "):" + VFStr);
                    }
                else if (((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue)) && leve == 0)//文件夹结束标签
                    {
                        folderData.AppendLine(tag + "(" + VR + "):" + VFStr);
                        tags.Add(folderTag + "SQ", folderData.ToString());
                    }
                else
                        tags.Add(tag, "(" + VR + "):" + VFStr); 
            }
        }

    string getVR(string tag)
    {
        switch (tag)
        {
            case "0002,0000"://文件元信息长度
                return "UL";
                break;
            case "0002,0010"://传输语法
                return "UI";
                break;
            case "0002,0013"://文件生成程序的标题
                return "SH";
                break;
            case "0008,0005"://文本编码
                return "CS";
                break;
            case "0008,0008":
                return "CS";
                break;
            case "0008,1032"://成像时间
                return "SQ";
                break;
            case "0008,1111":
                return "SQ";
                break;
            case "0008,0020"://检查日期
                return "DA";
                break;
            case "0008,0060"://成像仪器
                return "CS";
                break;
            case "0008,0070"://成像仪厂商
                return "LO";
                break;
            case "0008,0080":
                return "LO";
                break;
            case "0010,0010"://病人姓名
                return "PN";
                break;
            case "0010,0020"://病人id
                return "LO";
                break;
            case "0010,0030"://病人生日
                return "DA";
                break;
            case "0018,0060"://电压
                return "DS";
                break;
            case "0018,1030"://协议名
                return "LO";
                break;
            case "0018,1151":
                return "IS";
                break;
            case "0020,0010"://检查ID
                return "SH";
                break;
            case "0020,0011"://序列
                return "IS";
                break;
            case "0020,0012"://成像编号
                return "IS";
                break;
            case "0020,0013"://影像编号
                return "IS";
                break;
            case "0028,0002"://像素采样1为灰度3为彩色
                return "US";
                break;
            case "0028,0004"://图像模式MONOCHROME2为灰度
                return "CS";
                break;
            case "0028,0010"://row高
                return "US";
                break;
            case "0028,0011"://col宽
                return "US";
                break;
            case "0028,0100"://单个采样数据长度
                return "US";
                break;
            case "0028,0101"://实际长度
                return "US";
                break;
            case "0028,0102"://采样最大值
                return "US";
                break;
            case "0028,1050"://窗位
                return "DS";
                break;
            case "0028,1051"://窗宽
                return "DS";
                break;
            case "0028,1052":
                return "DS";
                break;
            case "0028,1053":
                return "DS";
                break;
            case "0040,0008"://文件夹标签
                return "SQ";
                break;
            case "0040,0260"://文件夹标签
                return "SQ";
                break;
            case "0040,0275"://文件夹标签
                return "SQ";
                break;
            case "7fe0,0010"://像素数据开始处
                return "OW";
                break;
            default:
                return "UN";
                break;
        }
    }

    string getVF(string VR, byte[] VF)
    {
    string VFStr = string.Empty;
    switch (VR)
    {
        case "SS":
            VFStr = BitConverter.ToInt16(VF, 0).ToString();
            break;
        case "US":
            VFStr = BitConverter.ToUInt16(VF, 0).ToString();
     
            break;
        case "SL":
            VFStr = BitConverter.ToInt32(VF, 0).ToString();
     
            break;
        case "UL":
            VFStr = BitConverter.ToUInt32(VF, 0).ToString();
     
            break;
        case "AT":
            VFStr = BitConverter.ToUInt16(VF, 0).ToString();
     
            break;
        case "FL":
            VFStr = BitConverter.ToSingle(VF, 0).ToString();
     
            break;
        case "FD":
            VFStr = BitConverter.ToDouble(VF, 0).ToString();
     
            break;
        case "OB":
            VFStr = BitConverter.ToString(VF, 0);
            break;
        case "OW":
            VFStr = BitConverter.ToString(VF, 0);
            break;
        case "SQ":
            VFStr = BitConverter.ToString(VF, 0);
            break;
        case "OF":
            VFStr = BitConverter.ToString(VF, 0);
            break;
        case "UT":
            VFStr = BitConverter.ToString(VF, 0);
            break;
        case "UN":
            VFStr = Encoding.Default.GetString(VF);
            break;
        default:
            VFStr = Encoding.Default.GetString(VF);
            break;
    }
    return VFStr;
    }

     public unsafe void convertTo8(BinaryReader streamdata, int colors, bool littleEdition, bool signed, short nHighBit,
                 int dataLen, float rescaleSlope, float rescaleIntercept, float windowCenter, float windowWidth, int width, int height)
        {
            if (dicomimg != null)
                Destroy(dicomimg);
            dicomimg = new Texture2D(width, height);
            detext.text += "convertTo8" + "\n";
            if (colors == 3)//color Img
            {
                for (int i = 0; i < height; i++)
                {
                    for (int j = 0; j < width; j++)
                    {
                        Color32 c = new Color32(streamdata.ReadByte(), streamdata.ReadByte(), streamdata.ReadByte(), (byte)255);
                        dicomimg.SetPixel(j, i, c);
                    }
                }
            }
            else if (colors == 1)//grayscale Img
            {
                int nMin = ~(0xffff << (nHighBit + 1)), nMax = 0;
                for (int i = 0; i < height; i++)
                {
                    for (int j = 0; j < width; j++)
                    {
                        short nMask; nMask = (short)(0xffff << (nHighBit + 1));
                        short nSignBit;

                        byte[] pixData = null;
                        short pixValue = 0;

                        pixData = streamdata.ReadBytes(dataLen / 8 * colors);
                        if (nHighBit <= 15 && nHighBit > 7)
                        {
                            if (littleEdition == false)
                                Array.Reverse(pixData, 0, 2);

                            // 1. Clip the high bits.
                            if (signed == false)// Unsigned integer
                            {
                                pixValue = (short)((~nMask) & (BitConverter.ToInt16(pixData, 0)));
                            }
                            else
                            {
                                nSignBit = (short)(1 << nHighBit);
                                if (((BitConverter.ToInt16(pixData, 0)) & nSignBit) != 0)
                                    pixValue = (short)(BitConverter.ToInt16(pixData, 0) | nMask);
                                else
                                    pixValue = (short)((~nMask) & (BitConverter.ToInt16(pixData, 0)));
                            }
                        }
                        else if (nHighBit <= 7)
                        {
                            if (signed == false)// Unsigned integer
                            {
                                nMask = (short)(0xffff << (nHighBit + 1));
                                pixValue = (short)((~nMask) & (pixData[0]));
                            }
                            else
                            {
                                nMask = (short)(0xffff << (nHighBit + 1));
                                nSignBit = (short)(1 << nHighBit);
                                if (((pixData[0]) & nSignBit) != 0)
                                    pixValue = (short)((short)pixData[0] | nMask);
                                else
                                    pixValue = (short)((~nMask) & (pixData[0]));
                            }

                        }

                        // 2. Rescale if needed (especially for CT)
                        if ((rescaleSlope != 1.0f) || (rescaleIntercept != 0.0f))
                        {
                            float fValue = pixValue * rescaleSlope + rescaleIntercept;
                            pixValue = (short)fValue;
                        }

                        // 3. Window-level or rescale to 8-bit
                        if ((windowCenter != 0) || (windowWidth != 0))
                        {
                            float fSlope;
                            float fShift;
                            float fValue;

                            fShift = windowCenter - windowWidth / 2.0f;
                            fSlope = 255.0f / windowWidth;

                            fValue = ((pixValue) - fShift) * fSlope;
                            if (fValue < 0)
                                fValue = 0;
                            else if (fValue > 255)
                                fValue = 255;

                            Color32 c = new Color32((byte)fValue, (byte)fValue, (byte)fValue, (byte)255);
                            dicomimg.SetPixel(j, i, c);
                        }
                        else
                        {
                            // We will map the whole dynamic range.
                            float fSlope;
                            float fValue;

                            // First compute the min and max.
                            if (i == 0)
                                nMin = nMax = pixValue;
                            else
                            {
                                if (pixValue < nMin)
                                    nMin = pixValue;

                                if (pixValue > nMask)
                                    nMask = pixValue;
                            }

                            // Calculate the scaling factor.
                            if (nMax != nMin)
                                fSlope = 255.0f / (nMax - nMin);
                            else
                                fSlope = 1.0f;

                            fValue = ((pixValue) - nMin) * fSlope;
                            if (fValue < 0)
                                fValue = 0;
                            else if (fValue > 255)
                                fValue = 255;

                            Color32 c = new Color32((byte)fValue, (byte)fValue, (byte)fValue, (byte)255);
                            dicomimg.SetPixel(j, i, c);
                        }
                    }
                }
            }
            dicomimg.Apply();
            testimg.texture = dicomimg;
        }

        public bool getImg()//获取图像 在图像数据偏移量已经确定的情况下
        {
            if (fileName == string.Empty)
                return false;
            detext.text += "getImg" + "\n";
            int dataLen, validLen, hibit;//数据长度 有效位
            int imgNum;//帧数

            rows = int.Parse(tags["0028,0010"].Substring(5));
            cols = int.Parse(tags["0028,0011"].Substring(5));

            colors = int.Parse(tags["0028,0002"].Substring(5));
            dataLen = int.Parse(tags["0028,0100"].Substring(5));//bits allocated
            validLen = int.Parse(tags["0028,0101"].Substring(5));
            bool signed = int.Parse(tags["0028,0103"].Substring(5)) == 0 ? false : true;
            hibit = int.Parse(tags["0028,0102"].Substring(5));
            float rescaleSlop = 1, rescaleinter = 0;
            if (tags.ContainsKey("0028,1052") && tags.ContainsKey("0028,1053"))
            {
                rescaleSlop = float.Parse(tags["0028,1053"].Substring(5));
                rescaleinter = float.Parse(tags["0028,1052"].Substring(5));
            }
            //读取预设窗宽窗位
            //预设窗值读取代码......
            #region//读取预设窗宽窗位
            if (windowWith == 0 && windowCenter == 0)
            {
                Regex r = new Regex(@"([0-9]+)+");
                if (tags.ContainsKey("0028,1051"))
                {
                    Match m = r.Match(tags["0028,1051"].Substring(5));

                    if (m.Success)
                        windowWith = int.Parse(m.Value);
                    else
                        windowWith = 1 << validLen;
                }
                else
                {
                    windowWith = 1 << validLen;
                }

                if (tags.ContainsKey("0028,1050"))
                {
                    Match m = r.Match(tags["0028,1050"].Substring(5));
                    if (m.Success)
                        windowCenter = int.Parse(m.Value);//窗位
                    else
                        windowCenter = windowWith / 2;
                }
                else
                {
                    windowCenter = windowWith / 2;
                }
            }
            BinaryReader dicomFile = new BinaryReader(mstream);

            dicomFile.BaseStream.Seek(pixDataOffset, SeekOrigin.Begin);
            convertTo8(dicomFile, colors, isLitteEndian, signed, (short)hibit, dataLen, rescaleSlop, rescaleinter, windowCenter, windowWith, 512, 512);
            return true;
            #endregion
            }

  • 相关阅读:
    CSS入门
    杰理之内置短按再长按,不管长按多长时间都是短按【篇】
    商业智能BI,助力企业数据文化建设
    cnn感受野计算方法
    代码随想录算法训练营第四十三天 | ● 1049. 最后一块石头的重量 II ● 494. 目标和 ● 474.一和零
    sql -- 聚合函数和GroupBy和Having的爱恨情仇
    Hive DDL常见操作
    【C++ 学习 ㉙】- 详解 C++11 的 constexpr 和 decltype 关键字
    简单的项目部署脚本(转载)
    Unity SKFramework框架(二十)、VFX Lab 特效库
  • 原文地址:https://blog.csdn.net/lwsas1/article/details/125887970