基于HTML5的PACS HTML5图像处理(7)实现客户端JS调整窗宽窗位

基于HTML5的PACS HTML5图像处理(7)实现客户端JS调整窗宽窗位

作者:破曉  发布日期:2014-05-22 20:16:56

  • 要查看此系统更多的图像处理功能请参考:

    区域医疗移动医疗影像解决方案--基于HTML5的PACS--HTML5图像处理

    在此之前,此系统是结合DICOM的WADO标准,在浏览器里通过javascript操作返回的JPG图片。这种服务器端解析,客户端展现的方式,对实现图像的移动、缩放、旋转、测量等图像操作能够实现实时的交互。但这种方式存在着几个弊端:

    1.获取图像上的CT值(钙化值)信息的时候,要频繁的和服务器进行交互。

    2.调整图像的窗宽窗位或者对图像进行反色,也要和服务器进行频繁的交互。

    3.对图像进行测量(长方形测量,椭圆测量等)只能获取到面值和周长的简单的信息,这对于医生的诊断没多大的用处,实际运用中需要知道所测量的区域的最大值、最小值、方差值、均值等测量信息。

     以上的缺点归结为一点:即本地没有处理像素信息的操作。但是HTML5对于像素级处理的能力已经支持得很好,完成可以实现客户端对像素信息的操作。所以为了解决以上问题最近对系统做了一次比较大的升级。即客户端端直接操作DICOM的像素数据进行JS端图像的生成以及JS端实现窗宽窗位的调整。

    获取dicom中的像素数据,可考虑以下两种方式:

    A:服务器端直接以字节流的方式返回DICOM文件,客户端用JS来接收字节流,并负责解析DICOM中的图像数据,这种方式不仅要根据DICOM的传输语法(0002,0010)Transfer Syntax UID,还要根据  (0028,0002)Samples per pixel、(0028,0004)Photometric Interpretation,(0028,0010)Rows,(0028,0011)Columns,(0028,0100)Bits Allocated,(0028,0103)Pixel Representation等标签来确定像素数据的结构,复杂点的可能还会用到查找表来查找((0028,0004)Photometric Interpretation的值等于==PALETTE COLOR)。对于非压缩的显示VR或者是隐形VR,(0028,0004)Photometric Interpretation等于MONOCHROME1或者MONOCHROME2来说JS解析出像素数据确实很方便,但是DICOM文件各式各样,要写出包罗给种传输语法以及各种像素结构的JS文件确实很费劲。还要考虑到多帧动态图像,如果多针图像很大整个文件下载下来解析估计浏览器会彻底奔溃。所以觉得这种方式不太可行。(虽然这过程中实现了显示VR的DICOM文件的JS解析,但是中途考虑到复杂性和难度还是放弃了)。

    B:从服务器端获取DICOM文件的像素数组,既然目前基于C/S模式的PACS已经相当成熟,各式各样的第三方开源的dicom解析工具如DCMTK,DCM4CHE,MDCM,OPENDICOM等也相当的多,用开源的DICOM解析工具获取到像素数据也相当的方便。所以在服务器获取到像素数据返回给JS端,让JS端直接操作像素数据来生成要显示的图像。对于多帧图像也可以按需按帧的从服务器下载像素数据。

    言归正传,目前此系统是基于第二种方式来实现。需要特别注意的是:做窗宽窗位调整的时候要先做Hounsfield 值的转换。 

    HU[i] = pixel_val[i]*rescaleSlope+ rescaleIntercept。窗宽窗位的调整使用了线性的window-leveling算法针对CT/MR等图像,或者是非线性的gamma算法针对DX图像(即当windowWidth比较大的时候要考虑非线性的gamma算法,因为线性算法中每windowWidth/255个原始密度会压缩成一个显示灰度,windowWidth很大的时候损失可能会很大)

    01. 1 //线性的window-leveling算法
    02. 2 min = (2*windowCenter - windowWidth)/2.0 0.5
    03. 3 max = (2*windowCenter + windowWidth)/2.0 0.5;
    04. 4 for (var i = 0; i != nNumPixels; i++){
    05. 5     showPixelValue = (pixelHuValue[i] - min)*255.0/(double)(max - min);
    06. 6 }
    07. 7 //非线性的gamma算法
    08. 8 min = (2*windowCenter - windowWidth)/2.0 0.5
    09. 9 max = (2*windowCenter + windowWidth)/2.0 0.5;
    10. 10 for (var i = 0; i != nNumPixels; i++){
    11. 11     showPixelValue = 255.0 * Math.pow(pixelHuValue/(max-min), 1.0/gamma);
    12. 12 }

    如下代码展示JS端如何用后台获取到的像素数据生成图像。其中用到了查找表的概念。

    001. 1 /**
    002. 2  * @author http://www.cnblogs.com/poxiao
    003. 3  * pixelBuffer代表是从后台获取到的像素信息数组,代码只列出了单色灰度图像的情况,
    004. 4  * 如果是三色的RGB图像自己稍微改动下代码即可。篇幅有限不在叙述。
    005. 5  **/
    006. 6 var pixelBuffer;
    007. 7 //width 代表图像的宽度,即DICOM中的标签(0028,0011)Columns
    008. 8 var width;
    009. 9 //height 代表图像的高度,即DICOM中的标签(0028,0010)Rows
    010. 10 var height;
    011. 11 /**
    012. 12  * @windowCenter 代表当前要显示的窗位
    013. 13  * @windowWidth 代表当前要显示的窗宽
    014. 14  * @bitsStored (0028,0101) 根据每个像素的存储位数生成查找表大小
    015. 15  * @rescaleSlope (0028,1053)用于计算HU值
    016. 16  * @rescaleIntercept  (0028,1052)用于计算HU值
    017. 17  * **/
    018. 18 function createImageCanvas(windowCenter,windowWidth,bitsStored,rescaleSlope,rescaleIntercept){
    019. 19     var lookupObject=new LookupTable();
    020. 20    lookupObject.setData(windowCenter,windowWidth,bitsStored,rescaleSlope,rescaleIntercept);
    021. 21     lookupObject.calculateHULookup();
    022. 22     lookupObject.calculateLookup();
    023. 23    
    024. 24     var imageCanvas=document.createElement('canvas');
    025. 25     imageCanvas.width = width;
    026. 26     imageCanvas.height =height;
    027. 27     imageCanvas.style.width = width;
    028. 28     imageCanvas.style.height = height;
    029. 29     var tmpCxt = imageCanvas.getContext('2d');
    030. 30     var imageData = tmpCxt.getImageData(0,0,width,height);
    031. 31     var n=0;
    032. 32     for(var yPix=0; yPix<height; yPix++)
    033. 33     {
    034. 34         for(var xPix=0; xPix<width;xPix++)
    035. 35         {
    036. 36             var offset = (yPix * width + xPix) * 4;
    037. 37             var pixelValue=lookupObject.lookup[pixelBuffer[n]];
    038. 38             imageData.data[offset]=    pixelValue;
    039. 39             imageData.data[offset+1]=pixelValue;
    040. 40             imageData.data[offset+2]=pixelValue;
    041. 41             imageData.data[offset+3]=255;
    042. 42             n++;
    043. 43         }
    044. 44     }
    045. 45     tmpCxt.putImageData(imageData, 0,0);
    046. 46    
    047. 47     return imageCanvas;
    048. 48 };
    049. 49 /**
    050. 50  * 像素查找表,主要要先根据rescaleSlope和rescaleIntercept进行Hounsfield值的转换
    051. 51  * HU[i] = pixel_val[i]*rescaleSlope+ rescaleIntercept
    052. 52  */
    053. 53 function LookupTable()
    054. 54 {
    055. 55     this.bitsStored;
    056. 56     this.rescaleSlope;
    057. 57     this.rescaleIntercept;
    058. 58     this.windowCenter;
    059. 59     this.windowWidth;
    060. 60    
    061. 61     this.huLookup;
    062. 62     this.lookup;
    063. 63 }
    064. 64
    065. 65 LookupTable.prototype.setData=function(wc,ww,bs,rs,ri)
    066. 66 {   
    067. 67     this.windowCenter=wc;
    068. 68     this.windowWidth=ww;
    069. 69     this.bitsStored=bs;
    070. 70     this.rescaleSlope=rs;
    071. 71     this.rescaleIntercept=ri;
    072. 72 };
    073. 73
    074. 74 LookupTable.prototype.setWindowingdata=function(wc,ww)
    075. 75 {
    076. 76     this.windowCenter=wc;
    077. 77     this.windowWidth=ww;
    078. 78 };
    079. 79
    080. 80 LookupTable.prototype.calculateHULookup=function()
    081. 81 {
    082. 82     var size=1<<this.bitsStored;
    083. 83     this.huLookup = new Array(size);
    084. 84     for(var inputValue=0;inputValue<size;inputValue++)
    085. 85     {
    086. 86         if(this.rescaleSlope == undefined && this.rescaleIntercept == undefined) {
    087. 87             this.huLookup[inputValue] = inputValue;
    088. 88         else {      
    089. 89             this.huLookup[inputValue] = inputValue * this.rescaleSlope +this.rescaleIntercept;
    090. 90         }
    091. 91     }
    092. 92 };
    093. 93 /**
    094. 94  * 窗宽窗位的调整线性的Window-leveling算法
    095. 95  * 非线性的gamma算法,稍微修改下:
    096. 96  *  var y=255.0 * Math.pow(this.huLookup[inputValue]/this.windowWidth, 1.0/gamma);
    097. 97  * **/
    098. 98 LookupTable.prototype.calculateLookup=function()
    099. 99 {   
    100. 100     var size=1<<this.bitsStored;
    101. 101     var min=this.windowCenter-0.5-(this.windowWidth-1)/2;
    102. 102     var max=this.windowCenter-0.5+(this.windowWidth-1)/2;
    103. 103     this.lookup=new Array(size);
    104. 104     for(var inputValue=0;inputValue<size;inputValue++)
    105. 105     {
    106. 106         if(this.huLookup[inputValue]<=min){
    107. 107             this.lookup[inputValue]=0 ;
    108. 108         }else if (this.huLookup[inputValue]>max){
    109. 109             this.lookup[inputValue]=255;
    110. 110         }else{               
    111. 111             var y=((this.huLookup[inputValue]-(this.windowCenter-0.5))/(this.windowWidth-1)+0.5)*255;
    112. 112             this.lookup[inputValue]= parseInt(y);
    113. 113         }
    114. 114     }
    115. 115 };

    鼠标调整窗宽窗位的时候JS端生成图像+绘制图形的速度。

    1.512 X 512大小的CT图像调整窗宽窗位速度

    \

     2.512 X 512大小的彩色CT图像调整窗宽窗位速度

    \

     3.512 x 512大小的MR图像调整窗宽窗位速度

    \

     4.2057 X 1347大小的CR图像调整窗宽窗位速度

    \

    5.有了像素信息后就可以在客户端实时的获取到CT值了。

    \

    6:有了像素信息后测量也可以获取到测量区域的最大值、最小值、方差值、均值等测量信息了

    \

    进测试,调整窗宽窗位时HTML5上绘制图形的时间还是很快的,总的绘制时间在10毫秒的数量级,而且发现绘制时间还可以变少,这绘制时间包括了图像边角上的文字信息,但是HTML5绘制文字的信息效率明显比绘制图像的效率要底,所以不必每次刷新都绘制文本信息,可以加以参数控制在图像切换或者调窗宽窗位的时候也就是文本信息变化的时候才绘制文字信息。关于图像的生成时间,发现图像的生成时间和图像的宽X高成正比,图像越大所需时间越长,对于CT/MR等图像时间大概在几十个毫秒级。对于2057X1347的CR图像时间大概在400毫秒级,对于2000X3000多的DX图像生成图像的时间就有点卡顿了,要1秒-2秒左右。。。这速度还得想办法优化有木有。。。。。还有对于DX图像调整窗宽窗位虽然使用了gamma算法,但是出来的图像,我总感觉得没有用第三方工具比如RadiAnt上看见的光滑,噪声有点大。所以在没得到更好的解决方案前,目前DX的图像只能特殊化即保留原来的方式在服务器端直接生成JPG让客户端直接绘制,希望会DICOM图像算法的大神们看到此文章后能给小弟我一点关于DX调窗宽窗位的意见,是不是还要用到别的算法啥的?。先谢谢了

     

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值