【问题标题】:Making 2D cross-sectional slices of a 3D array制作 3D 阵列的 2D 横截面切片
【发布时间】:2013-01-30 00:39:54
【问题描述】:

我正在尝试根据体积 CT 数据制作实时模拟超声图像。诀窍是用户控制探针的位置,这定义了他们看到的平面。

到目前为止,我所做的是将所有 dicom 图像中的像素数据读取到单个 3D 像素阵列中,现在我需要做的是以不同角度重新切片该 3D 阵列。抱歉,如果以下描述有点草率,但请想象一个 3D 矩形框(例如 100 像素宽和深 [x,z],以及 500 像素长 [y])和 2D“观察平面”(例如 50 x 50 像素)。假设观察平面的起始位置(原点定义为平面闭合边缘的中点 - [0,25])与原点 [50,250,0] (顶面的死点,向下看) ,从左到右并垂直向下刺穿矩形。因此,观察平面具有三个可以更改的参数 - 原点的位置、围绕垂直方向的旋转(从原点到平面相对边缘上对应点的直线)和“倾斜”(围绕与盒子相交的线旋转平面)。因此用户可以更改这三个参数,输出是由视平面“触摸”的像素构建的图像。

再次,如果描述草率,我深表歉意,但我是一名医学生,没有很强的数学背景。任何帮助将不胜感激。

【问题讨论】:

    标签: javascript arrays 3d


    【解决方案1】:

    我会写出一条直线的二维方程,求解每个 x 的值, 并将生成的 y 变量四舍五入到最接近的整数——昨天 Edje09

    暂时坚持使用 2D 案例,您建议的方法有两个主要问题

    1. 如果线条比梯度 1 更陡,则可能会遗漏一些像素。
    2. 四舍五入可以选择比您要选择的像素高一个像素。

    This pdf 显示了 2D 案例的问题和可能的解决方案,然后可以在 3D 案例的基础上进行构建。

    编辑经过进一步思考,我可能已经生成了一个written pdf outline solution for the 3D case,可以将其转换为算法并因此转换为代码。据我所知,我没有做任何检查,也不能保证它的正确性,但希望能让你更进一步。

    添加了编辑代码 以下 Javascript 代码似乎可以满足您的要求。它很慢,因此您需要在单击 SET 后等待。此外,视图之间的“窗格”也不清楚,因此在重新填充“窗格”之前,您无法判断正在发生任何事情。 我只测试了使用 2 个图像来表示 z 方向上的 100 个像素。函数 getPixels 中的第一行代码处理了这个限制,删除了 z 方向上的完整图像集。我进行的测试相当肤浅,但似乎通过了。最好有全套图片。

    我将 3D 阵列想象为一系列 D 图像 image(0) 在后面沿 z 方向运行到前面的 image(D-1)。每个图像在 x 方向上具有宽度 W,在 y 方向上具有高度 H。感谢我喜欢它的挑战。

    所用图片压缩文件夹的链接位于代码末尾。

    <!DOCTYPE HTML>
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <!--
    Copyright (c)  2013   John King
    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    -->
    <title>3D Slicer</title>
    <style type="text/css">
        div, canvas, img {
            position: absolute;
        }
    
        img {
            top:0px;
            left:0px;
            visibility:hidden;
        }
        input {
            text-align: right;
        }
        .seen {
            visibility: visible;
        }   
        #canvas3D {
            left:10px;
            top:10px;
            visibility:hidden;
        }
        #canvas2D {
            left:10px;
            top:50px;
            border:1px solid black;
        }
        #frame {
            left:650px;
            top:10px;
            border:1px solid black;
            background-color: #DDDDDD;
            width:600px;
            height:600px;
        }
        #framehead {
            left:0px;
            top:0px;
            height:25px;
            width:100%;
            border-bottom: 1px solid black;
            background-color: #999999;
        }
        #userdata {
            top:10px;
            left:10px;
        }
        #originins {
            top:10px;
            left:10px;
            width:260px;
        }
        #origintext {
            top:200px;
            left:10px;
            width:260px;
        }
        #origininput {
            top:225px;
            left:10px;
            width:260px;        
        }
        #originlimits {
            top:250px;
            left:10px;
            width:260px;
        }
        #thetaimg {
            top:10px;
            left:225px;
        }
        #thetatext {
            top:200px;
            left:225px;
            width:260px;
        }
        #thetainput {
            top:225px;
            left:225px;
            width:260px;
        }
        #thetalimits {
            top:250px;
            left:225px;
            width:260px;
        }
        #psiimg {
            top:10px;
            left:440px;
        }
        #psitext {
            top:200px;
            left:440px;
            width:260px;
        }
        #psiinput {
            top:220px;
            left:440px;
            width:260px;
        }
        #psilimits {
            top:250px;
            left:440px;
            width:260px;
        }
        #setButton {
            top:310px;
            left:10px;
            width:260px;
        }
        #axes {
            top:350px;
            left:10px;
        }
    </style>
    <script type="text/javascript">
    
        //add a trim function to string if not present - strips white space from start and end of string
        if(typeof String.prototype.trim !== 'function') {
            String.prototype.trim = function() {
                return this.replace(/^\s+|\s+$/g, ''); 
            }
        }
    
        // abbreviation function for getElementById
        function $(id) {
          return document.getElementById(id);
        }
    
        //parameters for 3D array of pixels set in code
        var W=100; //width of array in x direction, must be even
        var D=100; //depth of array in z direction, must be even
        var H=500; //height of array in y direction
    
        //parameters for the rectangular plane PQRS that will select the pixels for a 2D array by slicing through the 3D array
        //PQRS moves in such a way that PQ remains parallel to xz plane and PS remains parallel to yz plane
        //these parameters set in code
        var L=50; //length of rectangle PQ
        var B=50; //breadth of rectangle PS
    
        //Initialisation of parameters that can be changed by the user.
        var O=new Point(W/2,0,D/2); //O is middle of PQ
        var theta=0; //angle PQ is rotated after plane is rotated about a vertical axis through O, must be between -PI/2 and PI/2
        var psi=0; //angle PS is rotated after plane is rotated about PQ as an axis, must be between -PI/2 and PI/2
    
        //variable for canvases
        var c3D, c2D;
    
    
        /*getPixel gets an individual pixel from the 3D array of pixels formed by a stack of D (for depth) 2D images
         * numbered from 0 to D-1, with 0 being the image at the back.
         * Each image having width W and height H pixels.
         * 0<= x <W, 0<= y <H,  0<= z <D
         * each image is on the canvas canvas3D
         * 
         * for this test img0.jpg will be used for img0.jpg to img49.jpg  and img50.jpg will be used for img50 to img99
         */
        function getPixel(x,y,z) {
            // line below only required because just two images img0.jpg and img50.jpg are used for testing
            z=Math.floor(z/50)*50;          
            //Remove above line if full series of images used in z direction
            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];
            return pix;
        }
    
        //Pixel Object
        function Pixel() {
            this.red;
            this.green;
            this.blue;
            this.alpha;
        }
    
        //Point Object
        function Point(x,y,z) {
            this.x=x;
            this.y=y;
            this.z=z;
        }
    
        function Point2D(a,d) {
            this.a=a;
            this.d=d;
        }
    
        function setValues() {
            c2D.ctx.clearRect(0,0,c2D.width,c2D.height);
            var Oobj=Ochecked($("Oin").value);
            if(!Oobj.OK) {
                $("Oin").style.backgroundColor="#F1B7B7";
                return
            }
            $("Oin").style.backgroundColor="#FFFFFF";
            O=Oobj.point;
            var th=parseInt($("thetain").value.trim());
            if(isNaN(th)) {
                $("thetain").style.backgroundColor="#F1B7B7";
                return
            }
            if(th<=-90 || th>90) {
                $("thetain").style.backgroundColor="#F1B7B7";
                return
            }
            $("thetain").style.backgroundColor="#FFFFFF";
            theta=th*Math.PI/180;
            var si=parseInt($("psiin").value.trim());
            if(isNaN(si)) {
                $("psiin").style.backgroundColor="#F1B7B7";
                return
            }
            if(si<=-90 || si>90) {
                $("psiin").style.backgroundColor="#F1B7B7";
                return
            }
            $("psiin").style.backgroundColor="#FFFFFF";
            psi=si*Math.PI/180;
            printPane();
        }
    
        function 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=new Point(x,y,z);
            return {OK:true,point:p};
        }
    
        function printPane(){
            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)),O.y,O.z + 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 pixel,col;
            var PSpoint =new Point(p.x,p.y,p.z); // points along PS initialised to start at P
            var PQpoint; //variable for points along line parallel to PQ
            var imdata=c2D.ctx.getImageData(0,0,c2D.width,c2D.height);          
            for(var ps=0;ps<PSincVec.length;ps++) {
                //increment along line PS
                PSpoint.y+=PSincVec[ps].a;
                PSpoint.z+=PSincVec[ps].d;
                PQpoint =new Point(PSpoint.x,PSpoint.y,PSpoint.z); // points along line parallel to PQ initialised to current point on PS
                for(var pq=0;pq<PQincVec.length;pq++) {
                    //increment along line PQ
                    PQpoint.x+=PQincVec[pq].a;
                    PQpoint.z+=PQincVec[pq].d;
                    //check that PQpoint is inside 3D array
                    if(0<=PQpoint.x && PQpoint.x<W && 0<=PQpoint.y && PQpoint.y<H && 0<=PQpoint.z && PQpoint.z<D) {
                        pixel=c3D.getPixel(PQpoint.x,PQpoint.y,PQpoint.z);
                        //write pixel from point along line parallel to PQ onto plane
                        col=4*(ps*c2D.width+pq);
                        imdata.data[col++]=pixel.red;
                        imdata.data[col++]=pixel.green;
                        imdata.data[col++]=pixel.blue;
                        imdata.data[col]=pixel.alpha;
                    }               
                }
            }
            c2D.ctx.putImageData(imdata,0,0);
        }
    
        function getIncVec(a,d) {
            var r,t;
            if(a>Math.abs(d)) {
                var incVec=getIncs(a,Math.abs(d));
            }
            else {
                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;
                }
            }
            if(d<0) {
                for(var i=0;i<incVec.length;i++) {
                    incVec[i].d*=-1;
                }
            }
            return incVec;
        }
    
        function 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);
            }
            return vec;
        }
    
        function main() {
            //set limits and values for user input.
            $("Oin").value=O.x+","+O.y+","+O.z;
            $("thetain").value=theta;
            $("psiin").value=psi;
            $("originlimits").innerHTML="0&lt;= x &lt;"+W+"<br>0&lt;= y &lt;"+H+"<br>0&lt;= z &lt;"+D;
            //set canvas3D so that pixels are readable
            c3D=$("canvas3D");
            c3D.width=W;
            c3D.height=H;
            c3D.ctx=c3D.getContext('2d');
            c3D.getPixel=getPixel;
    
            //set canvas2D so that pixels are settable
            c2D=$("canvas2D");
            c2D.width=L;
            c2D.height=B;
            c2D.ctx=c2D.getContext('2d');
            c2D.initialise=initialise;
    
            $("hide").style.width=L+"px";
            $("hide").style.height=B+"px";
        }
    </script>
    </head>
    <body onload="main()">
        <!-- list of images for 3D array -->
        <img id="i0" src="images/img0.jpg">
        <img id="i50" src="images/img50.jpg">
        <!-- end of list of images for 3D array -->
    
        <canvas id="canvas3D"></canvas>
        <div id="frame">
            <div id="framehead">&nbsp;&nbsp;&nbsp;View of Slicing Pane</div>
            <canvas id="canvas2D"></canvas>
        </div>
        <div id="userdata">
            <div id="originins">Enter in form x,y,z </br> eg 40,27,83</div>
            <div id="origintext">Position for 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 in degrees</div>
            <div id="thetainput"><input id="thetain"></div>
            <div id="thetalimits">-90 &lt; theta &lt;=90</div>
            <img class="seen" id="psiimg" src="images/psi.jpg">
            <div id="psitext">Psi in degrees</div>
            <div id="psiinput"><input id="psiin"></div>
            <div id="psilimits">-90 &lt; psi &lt;=90</div>
            <div id="setButton"><input type="button" value="SET" onclick="setValues()"></div>
            <img class="seen" id="axes" src="images/axes.jpg">
        </div>
    <div id="msg"></div>    
    </body>
    </html>
    

    images used in code

    【讨论】:

    • 经过相当肤浅的略读后,太棒了!!非常感谢您花时间写出所有这些内容(并说明!)。我会让你知道结果如何!
    • 在解决方案中计算 P 和 Q 以修正 3D 案例,重新检查 pdf
    • 3D 案例解决方案中 S 的计算已更正,重新检查pdf
    【解决方案2】:

    听起来是个有趣的问题,我开始考虑它,但很快遇到了一些问题。它并不像您最初想象的那么简单或直接!作为开始,我将其简化为通过 2D 数组获取 1D 切片的情况。很快就很清楚,对于某些切片,对于所有像素来说,哪些像素将构成切片的一部分并不明显。我制作了一个pdf来说明我的意思。这是 pdf 文档Issues 2D 的链接。在想出一个可能的解决方案之前,我或其他人需要更多的思考。抱歉,我目前无法提供更多帮助。

    【讨论】:

    • 非常感谢您的回复!这正是我所期待的问题。我最初(相当模糊)的计划是使用 3D 平面方程将查看窗格“放置”在阵列的 3D 空间中,然后循环查看查看平面上每个点对应的位置。对于您提出的问题,我正在考虑四舍五入到最接近由方程式确定的“确切”位置的像素。类似于您的 pdf,在 2D 中,我会为一条线编写 2D 方程,求解 x 的每个值,然后将结果 y 变量四舍五入到最接近的整数。
    • 现在想一想,我可能可以这样做,然后遍历所有 x 和 y 值,然后将结果 z 舍入,但正如我所提到的,我的数学背景不是很强,而且我还没有弄清楚如何将这些方程式转换为代码。想法?
    猜你喜欢
    • 1970-01-01
    • 2020-10-24
    • 1970-01-01
    • 2012-01-31
    • 2018-07-09
    • 1970-01-01
    • 2018-12-08
    • 2016-10-19
    • 2011-05-31
    相关资源
    最近更新 更多