在此之前,此系统是结合 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 很大的时候损失可能会很大)
- //线性的window-leveling算法
- min = (2*windowCenter - windowWidth)/2.0 - 0.5;
- max = (2*windowCenter + windowWidth)/2.0 - 0.5;
- for (var i = 0; i != nNumPixels; i++){
- showPixelValue = (pixelHuValue[i] - min)*255.0/(double)(max - min);
- }
- //非线性的gamma算法
- min = (2*windowCenter - windowWidth)/2.0 - 0.5;
- max = (2*windowCenter + windowWidth)/2.0 - 0.5;
- for (var i = 0; i != nNumPixels; i++){
- showPixelValue = 255.0 * Math.pow(pixelHuValue/(max-min), 1.0/gamma);
- }
如下代码展示 JS 端如何用后台获取到的像素数据生成图像。其中用到了查找表的概念。
- /**
- * @author http://www.cnblogs.com/poxiao
- * pixelBuffer代表是从后台获取到的像素信息数组,代码只列出了单色灰度图像的情况,
- * 如果是三色的RGB图像自己稍微改动下代码即可。篇幅有限不在叙述。
- **/
- var pixelBuffer;
- //width 代表图像的宽度,即DICOM中的标签(0028,0011)Columns
- var width;
- //height 代表图像的高度,即DICOM中的标签(0028,0010)Rows
- var height;
- /**
- * @windowCenter 代表当前要显示的窗位
- * @windowWidth 代表当前要显示的窗宽
- * @bitsStored (0028,0101) 根据每个像素的存储位数生成查找表大小
- * @rescaleSlope (0028,1053)用于计算HU值
- * @rescaleIntercept (0028,1052)用于计算HU值
- * **/
- function createImageCanvas(windowCenter,windowWidth,bitsStored,rescaleSlope,rescaleIntercept){
- var lookupObject=new LookupTable();
- lookupObject.setData(windowCenter,windowWidth,bitsStored,rescaleSlope,rescaleIntercept);
- lookupObject.calculateHULookup();
- lookupObject.calculateLookup();
- var imageCanvas=document.createElement("canvas");
- imageCanvas.width = width;
- imageCanvas.height =height;
- imageCanvas.style.width = width;
- imageCanvas.style.height = height;
- var tmpCxt = imageCanvas.getContext("2d");
- var imageData = tmpCxt.getImageData(0,0,width,height);
- var n=0;
- for(var yPix=0; yPix<height; yPix++)
- {
- for(var xPix=0; xPix<width;xPix++)
- {
- var offset = (yPix * width + xPix) * 4;
- var pixelValue=lookupObject.lookup[pixelBuffer[n]];
- imageData.data[offset]= pixelValue;
- imageData.data[offset+1]=pixelValue;
- imageData.data[offset+2]=pixelValue;
- imageData.data[offset+3]=255;
- n++;
- }
- }
- tmpCxt.putImageData(imageData, 0,0);
- return imageCanvas;
- };
- /**
- * 像素查找表,主要要先根据rescaleSlope和rescaleIntercept进行Hounsfield值的转换
- * HU[i] = pixel_val[i]*rescaleSlope+ rescaleIntercept
- */
- function LookupTable()
- {
- this.bitsStored;
- this.rescaleSlope;
- this.rescaleIntercept;
- this.windowCenter;
- this.windowWidth;
- this.huLookup;
- this.lookup;
- }
- LookupTable.prototype.setData=function(wc,ww,bs,rs,ri)
- {
- this.windowCenter=wc;
- this.windowWidth=ww;
- this.bitsStored=bs;
- this.rescaleSlope=rs;
- this.rescaleIntercept=ri;
- };
- LookupTable.prototype.setWindowingdata=function(wc,ww)
- {
- this.windowCenter=wc;
- this.windowWidth=ww;
- };
- LookupTable.prototype.calculateHULookup=function()
- {
- var size=1<<this.bitsStored;
- this.huLookup = new Array(size);
- for(var inputValue=0;inputValue<size;inputValue++)
- {
- if(this.rescaleSlope == undefined && this.rescaleIntercept == undefined) {
- this.huLookup[inputValue] = inputValue;
- } else {
- this.huLookup[inputValue] = inputValue * this.rescaleSlope + this.rescaleIntercept;
- }
- }
- };
- /**
- * 窗宽窗位的调整线性的Window-leveling算法
- * 非线性的gamma算法,稍微修改下:
- * var y=255.0 * Math.pow(this.huLookup[inputValue]/this.windowWidth, 1.0/gamma);
- * **/
- LookupTable.prototype.calculateLookup=function()
- {
- var size=1<<this.bitsStored;
- var min=this.windowCenter-0.5-(this.windowWidth-1)/2;
- var max=this.windowCenter-0.5+(this.windowWidth-1)/2;
- this.lookup=new Array(size);
- for(var inputValue=0;inputValue<size;inputValue++)
- {
- if(this.huLookup[inputValue]<=min){
- this.lookup[inputValue]=0 ;
- }else if (this.huLookup[inputValue]>max){
- this.lookup[inputValue]=255;
- }else{
- var y=((this.huLookup[inputValue]-(this.windowCenter-0.5))/(this.windowWidth-1)+0.5)*255;
- this.lookup[inputValue]= parseInt(y);
- }
- }
- };
鼠标调整窗宽窗位的时候 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 调窗宽窗位的意见,是不是还要用到别的算法啥的?。先谢谢了。
来源: