標籤:旋轉 如何 調整 升級 store 返回 let function 轉換
在此之前,此系統是結合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)Columnsvar width;//height 代表映像的高度,即DICOM中的標籤(0028,0010)Rowsvar 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調窗寬窗位的意見,是不是還要用到別的演算法啥的?。先謝謝了。
基於HTML5的PACS--HTML5影像處理