我正在尝试用体积CT数据制作实时模拟超声图像。诀窍是用户控制探头的位置,该位置定义了他们正在看到的平面。
到目前为止我所做的是将所有dicom图像中的像素数据读入单个3D像素阵列,现在我需要做的是以不同的角度重新组合3D阵列。很抱歉,如果以下描述有点草率,但想象一个3D矩形框(比如100像素宽和深[x,z],500长[y])和2D“观看平面”(比如说50 x 50像素) )。假设观察平面的起始位置(原点定义为平面近边缘的中间点 - [0,25])原点位于[50,250,0](顶面的死点,向下看) ,从左到右定向,直接刺穿矩形。因此,观察平面有三个可以改变的参数 - 原点的位置,围绕垂直的旋转(从原点到平面的相对边缘上的相应点的线)和“倾斜”(平面围绕与盒子相交的线的旋转)。因此,用户可以更改这三个参数,输出是根据观察平面“触摸”的像素构建的图像。
再次,如果描述很草率,我道歉,但我是一名没有强大数学背景的医学生。任何帮助将不胜感激。
答案 0 :(得分:1)
听起来像一个有趣的问题,我开始考虑它但很快就遇到了一些问题。它并不像你想象的那么简单或直截了当!首先,我将其简化为通过2D阵列获取1D切片的情况。很快就发现,对于某些切片,对于所有像素而言,哪些像素将形成切片的一部分并不明显。我制作了一个pdf来表明我的意思。这是pdf文档Issues 2D的链接。在提出可能的解决方案之前,我或其他人需要更多的思考。对不起,我现在无法提供更多帮助。
答案 1 :(得分:1)
我会为一条线写出二维方程,求解x的每个值, 并将得到的y变量舍入到最接近的整数-Jeje09昨天
目前坚持2D案例你建议的方法有两个主要问题
This pdf显示了2D案例的问题和可能的解决方案,然后可以针对3D案例进行构建。
编辑经过进一步思考后,我可能已经产生了一个written pdf outline solution for the 3D case,可以转化为算法,从而转化为代码。这是我所得到的,我没有做过任何检查,也不能保证其正确性,但希望能让你进一步发展。
添加了编辑代码 以下Javascript代码似乎可以满足您的需求。它很慢,所以你需要在点击SET后等待。此外,“窗格”不会在视图之间清除,因此在重新填充“窗格”之前,您无法说出任何事情。 我只通过使用2个图像来测试z方向上的100个像素。函数getPixels中的第一个代码行处理此限制,删除z方向上的完整图像集。我进行的测试相当肤浅但似乎没有通过。使用全套图像更好。
我把3D阵列想象成一系列D图像(0),在后面运行z方向到前面的图像(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<= x <"+W+"<br>0<= y <"+H+"<br>0<= z <"+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"> 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 < theta <=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 < psi <=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>