在此之前,此係統是結合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讓客戶端直接繪製