您的位置:首页 > Web前端 > HTML5

HTML5灰度图像处理练习2:窗宽窗位调节

2011-05-03 21:32 281 查看
大多数医学图像的灰度级别都远远高于256,但一般的计算机屏幕只支持256级,加上人眼灵敏度的天然限制(大多只能到16级),导致人类只能通过一个小小的窗口去观察那个广阔的灰度世界。因此,正如天文学家离不开望远镜,现代放射学家也离不开窗宽窗位。

一些富有想像力的读者会问,未来的PACS影像工作站可以用HTML5来做吗?至今为止,应该没有人能够回答。真正意义上的PACS影像工作站还有很多复杂的机制,比如大规模图像的加载、个性化的布局、便捷的浏览和导航工具,让用户在大序列中快速定位到想看的图像,或者在大幅图像中找到要仔细研究的部位,以及这一切背后复杂的内存管理和网络通信。尽管利用HTML5可以在200行代码之内搞定窗宽窗位,在大多数工作站上这也是最常用的功能,但这仅仅是迈向无需插件的纯粹Web平台工作站的很微小的一步,关于Canvas跨域访问图像的安全问题,以及HTML5的File API, WebSocket API,WebStorage API的未来发展,还需要进一步关注。当然,另外的一种选择,就是把所有这些哪怕是最简单的空间和灰度变换之类的呈现逻辑都放在服务器端。

至少,在几年以前,用Javascript来调节窗宽窗位是难以想象的。首先,大家根本找不到Javascript能够调用的像素处理API;其次,大多数浏览器的Javascript引擎都慢如蜗牛。HTML5的出现,也许提供了某种可能性。不过我也有点担心,HTML5 API最后真的要把所有的桌面应用都替换掉吗?如果是这样,HTML5会不会变得过于复杂?是否有一个合适的技术架构和工作流程来控制这个复杂性?在这个狂热的时代,HTML5的技术决策者们确实应该认真考虑清楚该做什么和不做什么,因为在IT界,遍地都是因为一个微小的功能差异,导致结果截然不同的案例。

下面是一个简单的窗宽窗位调节案例,可以直接复制到本地一个html文件中,然后用IE9/Opera10/etc.打开。原理是先初始化一个512*512大小矩阵,里面填充12位灰度值(即从0到4096取值)。为了便于观察窗宽窗位调节的效果,这个矩阵中的灰度分布按照类似SMPTE的方式来设计。然后通过html中type为range的input控件(即拖动条)来调节窗宽窗位,遗憾的是IE9不支持这个控件,所以干脆把鼠标拖动的支持给加上:横拖改变窗宽,纵拖改变窗位。在一步步编程实现的过程中,为了粗略验证每一步的正确性,我还在页面上增加了一个Canvas专门用来分别显示原始数据和PV(Presentation Value)的直方图,但在最后公布的代码中我把它去掉了,对生成直方图感兴趣的读者可以参考上一篇博客。

做好了以后,发现IE9跟Opera10相比,确实还是有不小的差距。除了前面提到的对range类型的input控件支持,以及Javascript脚本引擎的性能以外,我还注意到了一个更加可怕的缺陷。我不记得用计算机图形学的术语应该怎么说了,如果用放射学的术语,可能可以用线对这种质量指标来衡量。当我在自己的test pattern中增加了横纵两组相隔1个像素的黑白线条之后,用相同的显示器,发现IE9显示出来的线条区域失真非常严重(人眼观察有很明显的疏密差异),但Opera10的显示基本上没有失真(而且奇怪的是,截图上传到CSDN之后,被自动转换成GIF,在Opera10上观察这个图片还是正常的,在IE9上观察就失真了)。真希望有人告诉我Opera10是怎么做到的。

关于用鼠标调节窗宽窗位,我试过在Intel i3 M350机器上用Opera10运行,速度还是勉强可以接受的(跟桌面版本的工作站相比),但我用的算法并没有经过优化。感兴趣的读者可以根据“DICOM 医学图像窗口变换的加速算法”这篇论文做一些改进,相信改进过后,鼠标拖动调节的性能跟桌面工作站相比应该没有多大差别。当然,也还是需要在更大幅的普放图像上做进一步的试验(尽管有人坚持认为普放图像调窗可能意义不大,或者希望用一些曲线来进行调节)。

最后,还是希望IE在对HTML5的支持方面能够进步得快一些,毕竟在微软平台上IE普及率还是很高的。IE10已经提出了HTML5本地化的概念,看看最后他们能贯彻到什么程度。微软虽然很大,但大多数还是很灵活的,他不仅不会像之前的柯达那样抱着老古董不放而错失良机,有时还会活生生地宰杀已经拥有多年的技术,比如90年代的VB/COM,和00年代的Winform,尽管他们曾经而且依旧十分的优秀。真不知道微软拥抱HTML5之后,Sliverlight会不会被宰杀。





]<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Grayscale Windowing Test</title>
<style type="text/css">
table {margin:auto;}
</style>
</head>
<body>
<table>
<tr align="center"><td><h3>Grayscale Image Test</h3><hr height="1px"/></td>
</tr>
<tr>
<td align="right">
WW(<span id="spanWW"></span>): <input id="rangeWW" type="range" min="0" max="4096" onchange="setWindow();"/>
WC(<span id="spanWC"></span>): <input id="rangeWC" type="range" min="0" max="4096" onchange="setWindow();"/>
<button id="resetImage" onclick="loadImage();return true;">Reset</button>
</td>
</tr>
<tr align="center">
<td><canvas id="imageviewer" width="512px" height="512px" style="border: 1px solid;" mce_style="border: 1px solid;" /></td>
</tr>
<tr align="center"><td>© May, 2011, lifegame@263.net</td>
</tr>
</table>
</body>
<script>
var patternData = [];
var patternWidth = 512;
var patternHeight = 512;
var patternMaxGray = 0x0FFF;

function getPatternData(x,y){
return patternData[y*patternWidth+x];
}

function setPatternData(x,y,v){
patternData[y*patternWidth+x]=v;
}

function initPatternData(){
function fillRect(x,y,w,h,v){
var maxX = x+w;
var maxY = y+h;
for(i=x;i<maxX;i++){
for(j=y;j<maxY;j++){
setPatternData(i,j,v);
}
}
}
function fillGradientRect(x,y,w,h,f,t){
var maxX = x+w;
var maxY = y+h;
for(i=x;i<maxX;i++){
var v = f+Math.round((t-f)*(i-x)/w);
for(j=y;j<maxY;j++){
setPatternData(i,j,v);
}
}
}
function fillGrid(c,r,v){
var x = c*128;
var y = r*128;
fillRect(x,y,128,128,v);
}
function fillGradientGrid(c,r,f,t){
var x = c*128;
var y = r*128;
fillGradientRect(x,y,128,128,f,t);
}
function drawLines(x,y,w,h,d){
var v;
var maxX = x+w;
var maxY = y+h;
if(d==1){
for(i=x;i<maxX;i++){
v = ((i%2)==0)? 0 : patternMaxGray;
for(j=y;j<maxY;j++){
setPatternData(i,j,v);
}
}
}
else{
for(i=y;i<maxY;i++){
v = ((i%2)==0)? 0 : patternMaxGray;
for(j=x;j<maxX;j++){
setPatternData(j,i,v);
}
}
}
}
function fillLineGrid(c,r){
var x = c*128;
var y = r*128;
drawLines(x,y,64,128,1);
drawLines(x+64,y,64,128,0);
}
fillGrid(0,0,0);
fillRect(32,32,64,64,patternMaxGray*0.05);
fillGrid(1,0,patternMaxGray*0.1);
fillGrid(2,0,patternMaxGray*0.2);
fillGrid(3,0,patternMaxGray*0.3);
fillGrid(0,1,patternMaxGray*0.4);
fillGrid(1,1,patternMaxGray*0.5);
fillGrid(2,1,patternMaxGray*0.6);
fillGrid(3,1,patternMaxGray*0.7);
fillGrid(0,2,patternMaxGray*0.8);
fillGrid(1,2,patternMaxGray*0.9);
fillGrid(2,2,patternMaxGray);
fillRect(256+32,256+32,64,64,patternMaxGray*0.95);
fillLineGrid(3,2);
fillGradientGrid(0,3,0,patternMaxGray/4);
fillGradientGrid(1,3,patternMaxGray/4,patternMaxGray*2/4);
fillGradientGrid(2,3,patternMaxGray*2/4,patternMaxGray*3/4);
fillGradientGrid(3,3,patternMaxGray*3/4,patternMaxGray);
}
</script>
<script>
var icanvas = document.getElementById('imageviewer');
var icanvasWidth = parseInt(icanvas.getAttribute("width"));
var icanvasHeight = parseInt(icanvas.getAttribute("height"));
var icontext = icanvas.getContext('2d');
var isWindowing = 0;
var beginPointX = 0;
var beginPointY = 0;
var wwStep = 5;
var wcStep = 5;

function loadImage(){
icanvas.onmousedown = function(e){
isWindowing = 1;
if(e.offsetX) {
beginPointX = e.offsetX;
beginPointY = e.offsetY;
}
else if(e.layerX) {
beginPointX = e.layerX;
beginPointY = e.layerY;
}
}
icanvas.onmouseup = function(e){
isWindowing = 0;
if(e.offsetX) {
x = e.offsetX;
y = e.offsetY;
}
else if(e.layerX) {
x = e.layerX;
y = e.layerY;
}
var ww = parseInt(rangeWW.value);
var wc = parseInt(rangeWC.value);
ww = ww+(x-beginPointX)*wwStep;
wc = wc+(y-beginPointY)*wcStep;
updateWindow(ww,wc);
}
function updateWindow(ww,wc){
loadPattern(ww,wc);
rangeWW.value = ww;
rangeWC.value = wc;
spanWW.innerHTML = ww;
spanWC.innerHTML = wc;
}
initPatternData();
updateWindow(4096,2048);
}

function loadPattern(ww,wc){
function setPoint(img,x,y,pv){
var i = (y*img.width+x)*4;
img.data[i]=img.data[i+1]=img.data[i+2]=pv;
img.data[i+3]=255;
}
function getPresentationValue(v,w,c){
var minv = c-Math.round(w/2);
if(v<minv) return 0;
var maxv = c+Math.round(w/2);
if(v>maxv) return 255;
var pv = Math.round(255*(v-minv)/w);
if(pv<0) return 0;
if(pv>255) return 255;
return pv;
}
var imgData = icontext.getImageData(0,0,icanvasWidth,icanvasHeight);
for(y=0;y<patternHeight;y++){
if(y>=imgData.height) break;
for(x=0;x<patternWidth;x++){
if(x>=imgData.width) break;
var v = getPatternData(x,y);
var pv = getPresentationValue(v,ww,wc);
setPoint(imgData,x,y,pv);
}
}
icontext.putImageData(imgData,0,0);
}

function setWindow(){
var ww = parseInt(rangeWW.value);
var wc = parseInt(rangeWC.value);
loadPattern(ww,wc);
spanWW.innerHTML = ww;
spanWC.innerHTML = wc;
}

window.addEventListener("load", loadImage(), true);
</script>
<html>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: