在此之前,此系統是結合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調窗寬窗位的意見,是不是還要用到別的算法啥的?。先謝謝了。