制作 3D 阵列的 2D 横截面切片 [英] Making 2D cross-sectional slices of a 3D array
问题描述
我正在尝试根据体积 CT 数据制作实时模拟超声图像.诀窍在于用户控制探头的位置,它定义了他们看到的平面.
到目前为止,我所做的是将来自所有 dicom 图像的像素数据读取到单个 3D 像素阵列中,现在我需要做的是在不同角度重新切割该 3D 阵列.对不起,如果下面的描述有点草率,但想象一个 3D 矩形框(比如 100 像素宽和深 [x,z],和 500 长 [y])和一个 2D观察平面"(比如 50 x 50 像素).假设观察平面的起始位置(原点定义为平面近边缘的中点 - [0,25])的原点为 [50,250,0](顶面的死点,向下看),从左到右定向并笔直向下刺穿矩形.因此,视图平面具有三个可以更改的参数 - 原点的位置、围绕垂直线的旋转(从原点到平面相对边缘上的相应点的线)和倾斜"(平面围绕与盒子相交的线旋转).因此用户可以更改这三个参数,输出是由视图平面接触"的像素构建的图像.
再次,如果描述草率,我很抱歉,但我是一名没有强大数学背景的医学生.任何帮助将不胜感激.
我会写一条线的二维方程,求解 x 的每个值,并将结果 y 变量四舍五入到最接近的整数——昨天 Edje09
暂时坚持二维情况,您建议的方法有两个主要问题
- 如果线条比 1 的梯度更陡,则可能会遗漏一些像素.
- 舍入可以选择比您想要选择的像素高一个像素.
这显示了可能存在的问题和a>oln2D 案例的解决方案,然后可以为 3D 案例构建.
编辑经过进一步思考,我可能已经制作了一个 写的大纲3D 案例的解决方案 可以转化为算法,从而转化为代码.就我所知,我没有做任何检查,也不能保证它的正确性,但希望能让你更上一层楼.
添加编辑代码以下 Javascript 代码似乎可以满足您的要求.它很慢,因此您需要在单击 SET 后等待.此外,窗格"不会在视图之间清除,因此在重新填充窗格"之前您无法判断正在发生任何事情.我仅通过使用 2 个图像在 z 方向表示 100 个像素进行了测试.函数 getPixels 中的第一行代码处理这个限制,删除 z 方向上的完整图像集.我进行的测试相当肤浅,但似乎通过了.最好有全套图片.
我将 3D 阵列想象为后面的一系列 D 图像 image(0),沿 z 方向运行到前面的 image(D-1).每个图像在 x 方向上具有宽度 W,在 y 方向上具有高度 H.感谢您的挑战,我喜欢它.
指向所用图像的压缩文件夹的链接位于代码末尾.
<头><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><!--版权所有 (c) 2013 约翰·金特此授予任何人免费获得本软件和相关文档文件(软件")副本的许可,不受限制地处理本软件,包括但不限于使用、复制、修改、合并的权利、发布、分发、再许可和/或销售本软件的副本,并允许向其提供本软件的人员这样做,但须符合以下条件:上述版权声明和本许可声明应包含在本软件的所有副本或重要部分中.该软件按原样"提供,不提供任何形式的明示或暗示的保证,包括但不限于适销性、特定用途的适用性和不侵权的保证.在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任承担责任,无论是在合同诉讼、侵权行为或其他方面,由软件或软件的使用或使用或其他原因引起的或与之相关的软件.--><title>3D 切片器</title><style type="text/css">div, 画布, img {位置:绝对;}图片{顶部:0像素;左:0像素;可见性:隐藏;}输入 {文本对齐:右;}.看到{可见性:可见;}#canvas3D {左:10px;顶部:10px;可见性:隐藏;}#canvas2D {左:10px;顶部:50px;边框:1px纯黑色;}#框架 {左:650像素;顶部:10px;边框:1px纯黑色;背景颜色:#DDDDDD;宽度:600px;高度:600px;}#framehead {左:0像素;顶部:0像素;高度:25px;宽度:100%;边框底部:1px纯黑色;背景色:#999999;}#用户数据 {顶部:10px;左:10px;}#originins {顶部:10px;左:10px;宽度:260px;}#origintext {顶部:200px;左:10px;宽度:260px;}#origininput {顶部:225px;左:10px;宽度:260px;}#originlimits {顶部:250px;左:10px;宽度:260px;}#thetaimg {顶部:10px;左:225px;}#thetatext {顶部:200px;左:225px;宽度:260px;}#thetainput {顶部:225px;左:225px;宽度:260px;}#thetalimits {顶部:250px;左:225px;宽度:260px;}#psiimg {顶部:10px;左:440px;}#psitext {顶部:200px;左:440px;宽度:260px;}#psiinput {顶部:220px;左:440px;宽度:260px;}#psilimits {顶部:250px;左:440px;宽度:260px;}#setButton {顶部:310px;左:10px;宽度:260px;}#轴{顶部:350px;左:10px;}</风格><script type="text/javascript">//如果不存在,则向字符串添加修剪函数 - 从字符串的开头和结尾去除空格if(typeof String.prototype.trim !== 'function') {String.prototype.trim = function() {return this.replace(/^s+|s+$/g, '');}}//getElementById 的缩写函数函数 $(id) {返回 document.getElementById(id);}//代码中设置的3D像素数组的参数无功W=100;//数组x方向的宽度,必须是偶数无功D=100;//z方向的数组深度,必须是偶数无功H=500;//数组y方向的高度//矩形平面 PQRS 的参数,它将通过切片 3D 阵列来选择 2D 阵列的像素//PQRS 以这样一种方式移动,即 PQ 保持平行于 xz 平面,PS 保持平行于 yz 平面//这些参数在代码中设置无功L=50;//矩形PQ的长度无功B=50;//矩形PS的宽度//初始化可由用户更改的参数.var O=new Point(W/2,0,D/2);//O是PQ的中间var theta=0;//平面绕垂直轴旋转后旋转角度PQ通过O,必须在-PI/2和PI/2之间无功psi=0;//平面以PQ为轴旋转后旋转角度PS,必须在-PI/2和PI/2之间//画布变量var c3D, c2D;/*getPixel 从由一堆 D(用于深度)2D 图像形成的 3D 像素阵列中获取单个像素* 从 0 到 D-1 编号,0 是后面的图像.* 每个图像具有宽度 W 和高度 H 像素.* 0<=x<W,0<=y<H,0<=z<D* 每个图像都在画布 canvas3D 上** 在本次测试中,img0.jpg 将用于 img0.jpg 到 img49.jpg,而 img50.jpg 将用于 img50 到 img99*/函数 getPixel(x,y,z) {//只需要下面一行,因为只有两个图像 img0.jpg 和 img50.jpg 用于测试z=数学地板(z/50)*50;//如果在z方向使用了完整系列的图像,则删除上面的行this.ctx.drawImage($("i"+z),0,0);var imdata=this.ctx.getImageData(0,0,this.width,this.height);var col=4*(y*this.width+x);var pix=new Pixel();pix.red=imdata.data[col++];pix.green=imdata.data[col++];pix.blue=imdata.data[col++];pix.alpha=imdata.data[col];返回像素;}//像素对象函数像素(){this.red;this.green;this.blue;这个.alpha;}//点对象函数点(x,y,z){这个.x=x;this.y=y;这个.z=z;}函数 Point2D(a,d) {这.a=a;这.d=d;}函数 setValues() {c2D.ctx.clearRect(0,0,c2D.width,c2D.height);var Oobj=Ochecked($("Oin").value);如果(!Oobj.OK){$("Oin").style.backgroundColor="#F1B7B7";返回}$("Oin").style.backgroundColor="#FFFFFF";O = Oobj.point;var th=parseInt($("thetain").value.trim());如果(isNaN(th)){$("thetain").style.backgroundColor="#F1B7B7";返回}if(th<=-90 || th>90) {$("thetain").style.backgroundColor="#F1B7B7";返回}$("thetain").style.backgroundColor="#FFFFFF";theta=th*Math.PI/180;var si=parseInt($("psiin").value.trim());如果(isNaN(si)){$("psiin").style.backgroundColor="#F1B7B7";返回}if(si<=-90 || si>90) {$("psiin").style.backgroundColor="#F1B7B7";返回}$("psiin").style.backgroundColor="#FFFFFF";psi=si*Math.PI/180;打印窗格();}函数 Ochecked(Ovalue) {Ovalue=Ovalue.trim();var V=Ovalue.split(",");if(V.length!=3) {return {OK:false}};var x=parseInt(V[0].trim());var y=parseInt(V[1].trim());var z=parseInt(V[2].trim());if(isNaN(x) || isNaN(y) || isNaN(z)) {return {OK:false}};if(x<0 || x>=W) {return {OK:false}};if(y<0 || y>=H) {return {OK:false}};if(z<0 || z>=D) {return {OK:false}};p=新点(x,y,z);返回 {OK:true,point:p};}函数打印窗格(){var p = new Point(O.x-Math.round((L/2)*Math.cos(theta)),O.y,O.z - Math.round((L/2)*Math.sin(theta)));var q = new Point(O.x+Math.round((L/2)*Math.cos(theta)),Oy,Oz + Math.round((L/2)*Math.sin(theta)));var s = new Point(p.x,p.y+Math.round((B)*Math.cos(psi)),p.z + Math.round((B)*Math.sin(psi)));var n = new Point2D(q.x-p.x,q.z-p.z);var PQincVec=getIncVec(n.a,n.d);n = new Point2D(s.y-p.y,s.z-p.z);var PSincVec=getIncVec(n.a,n.d);var 像素,col;var PSpoint =new Point(p.x,p.y,p.z);//沿 PS 初始化的点从 P 开始var PQpoint;//沿平行于 PQ 的线的点的变量var imdata=c2D.ctx.getImageData(0,0,c2D.width,c2D.height);for(var ps=0;ps<PSincVec.length;ps++){//沿线递增PSPSpoint.y+=PSincVec[ps].a;PSpoint.z+=PSincVec[ps].d;PQpoint = new Point(PSpoint.x,PSpoint.y,PSpoint.z);//沿平行于 PQ 的线的点初始化为 PS 上的当前点for(var pq=0;pqMath.abs(d)) {var incVec=getIncs(a,Math.abs(d));}别的 {var incVec=getIncs(Math.abs(d),a);for(var i=0;i<incVec.length;i++) {r=incVec[i];t=r.a;r.a=r.d;r.d=t;}}如果(d<0){for(var i=0;i<incVec.length;i++) {incVec[i].d*=-1;}}返回 incVec;}函数 getIncs(a,d) {var p=new Point2D(0,0);var vec=[];vec.push(p);for(var i=0;i<a;i++){p=new Point2D(1,Math.floor((i+1)*d/a) - Math.floor(i*d/a));vec.push(p);}返回 vec;}函数主(){//为用户输入设置限制和值.$("Oin").value=O.x+","+O.y+","+O.z;$("thetain").value=theta;$("psiin").value=psi;$("originlimits").innerHTML="0<= x <"+W+"<br>0>0<= y <"+H+"<br>0<= z <"+D;//设置canvas3D,使像素可读c3D=$("canvas3D");c3D.width=W;c3D.height=H;c3D.ctx=c3D.getContext('2d');c3D.getPixel=getPixel;//设置canvas2D以便像素可设置c2D=$("canvas2D");c2D.width=L;c2D.高度=B;c2D.ctx=c2D.getContext('2d');c2D.initialise=初始化;$("隐藏").style.width=L+"px";$("隐藏").style.height=B+"px";}头部><body onload="main()"><!-- 3D 阵列的图像列表--><img id="i0" src="images/img0.jpg"><img id="i50" src="images/img50.jpg"><!-- 3D 阵列的图像列表结束--><canvas id="canvas3D"></canvas><div id="frame"><div id="framehead"> 切片窗格的视图</div><canvas id="canvas2D"></canvas>
<div id="用户数据"><div id="originins">以 x,y,z 格式输入 </br>例如40、27、83/div<div id="origintext">Origin O 的位置</div><div id="origininput"><input id="Oin"></div><div id="originlimits">limits</div><img class="seen" id="thetaimg" src="images/theta.png"><div id="thetatext">Theta 度数</div><div id="thetainput"><input id="thetain"></div><div id="thetalimits">-90 <θ<=90</div><img class="seen" id="psiimg" src="images/psi.jpg"><div id="psitext">以度为单位的 Psi</div><div id="psiinput"><input id="psiin"></div><div id="psilimits">-90 <psi <=90 </div><div id="setButton"><input type="button" value="SET" onclick="setValues()"></div><img class="seen" id="axes" src="images/axes.jpg">
<div id="msg"></div>