Echoを使った左室容積の計算方法には幾つかあるのですが、4腔断面・2腔断面の両方が得られる場合に、良く使われるのが Modified Simpson法です。
日本医師会雑誌の「心エコーのABC」から計算部分を抜粋。
一見、簡単なプログラムで構築できそうですが、交点算出が割と面倒です。
ここでは、javascriptでの実装を目指します。
トレースラインを Q0, Q1, Q2, ...で表し、その長径をMPで表す。
MPをm分割し、分割点をP0, P1, P2, ... とする。
等間隔で分割することにして、k番目の分割線の垂線が、トレース座標のQi, Qi+1線分を横切ることを考える。
直線MPと 直線(Pk)-(Qi)のベクトル内積は負になるが、MPと (Pk)-(Qi+1)のベクトル内積は正になる。
内積符号が異なった場合に、各トレース座標間の直線(Q)と分割点(Pk)からのMPの直交ベクトルとの交点が、候補になる。
トレースラインが捻じれる場合もあるので、分割点(Pk)から最も遠い候補を解とする。
また、左右それぞれに解を求める必要があるので、MPと分割点(Pk)から解までのベクトルとで、
外積を取り、左右どちらの解であるかを確認する。
複雑そうに思えるかもしれないが、ループ回数は、トレースポイント数 x 分割数 でしかなく、
四則演算だけでコードが書ける。
この解説部分に該当する関数名は、calc() で、 85行しかない。
デモプログラムでは、実運用に近づけるため、B-DUAL画像を模して左右に、4腔断面と2腔断面を配置し、
下段に、Modified Simponによる体積を表示する。
左右画像上に表示されるSimpson値は、単画像による等分割で、回転体により体積を求めた場合の数値で、
最初に載せた文献によれば、これを計測値とすることは少ないようです。
ここでは、アルゴリズムの紹介であるので、[pix]単位の計算結果を表示している。
生体長換算値 [cm/pix]があれば、生体長換算値を3回掛ければ [mL] での結果が求まる。
Canvasは、左右と下段の3枚を使っている。
スライス算出部は、2つnewして、左右それぞれに振り、
結果が求まった時点で、callbackにより、下段のスクリプトを呼び出す仕組みにした。
実行サンプルはこちら
Chrome, FF, IE11 で動作確認しています。
以下はソースコードです。
html と js の各1本です。
[html]
Modified Simpson
Modified Simpson's Rule Sample
[js]
// (c)Hundredsoft Corporation. 2015 All right reserved.
// NAME
// DSM.js
//
// FUNCTION
// Modified Simpson's rule.
//
// REVISION
// 1.00 T.Furumoto Jan-30-2015 Original
//
// UTF-8で保存して下さい
//
////////////////////////////////////////////////////////////
new function() {
///////////////////////////////////
// Private Scope
///////////////////////////////////
var m_linesz = 1;
var m_Slice = 20;
var m_Chain = 2;
var mdown = function(p, mouseX, mouseY){
if (p.m_mode != 1){
clear(p);
}
};
var mup = function(p, mouseX, mouseY){
var ret;
if (p.m_mode == 0){
p.m_trace = traceChain(p.m_trace);
p.m_P = autoL(p.m_trace);
ret = calc(p.m_trace, p.m_P);
if (ret){
paint(p, ret);
p.m_mode = 1;
}else{
errorDisp(p);
}
}else if (p.m_mode == 1){
p.m_mode = 0;
ret = calc(p.m_trace, p.m_P);
if (ret){
paint(p, ret);
}else{
errorDisp(p);
}
}
};
var mmove = function(p, mouseX, mouseY){
if (p.m_mode == 0){
var n = p.m_trace.x.length;
p.m_trace.x[n] = mouseX;
p.m_trace.y[n] = mouseY;
drawROI(p);
}else{
p.m_P = manualL(p.m_trace, mouseX, mouseY);
var ret = calc(p.m_trace, p.m_P);
if (ret){
paint(p, ret);
}else{
errorDisp(p);
}
}
};
var intersection = function(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y){
var a = p1y - p2y;
var b = -p1x + p2x;
var c = q1y - q2y;
var d = -q1x + q2x;
var det = a*d - b*c;
if( det * det < 0.01 ) // 平行
return false;
var e = a * p1x + b * p1y;
var f = c * q1x + d * q1y;
return {"x" : (d*e - b*f) / det, "y" : (-c*e + a*f) / det};
};
var autoL = function(trace){
var n = trace.x.length;
var mx = (trace.x[n-1] + trace.x[0]) / 2;
var my = (trace.y[n-1] + trace.y[0]) / 2;
var len2 = 0;
var px = false;
var py = false;
for (var i=0; i len2){
len2 = w;
px = trace.x[i];
py = trace.y[i];
}
}
return {"x": px, "y": py};
};
var manualL = function(trace, mouseX, mouseY){
var n = trace.x.length;
// Pointから最も近い点(p)
var px = trace.x[0];
var py = trace.y[0];
var lmin = (px - mouseX) * (px - mouseX) + (py - mouseY) * (py - mouseY);
for( var i=1; i m_Chain * m_Chain * m_Chain * m_Chain){
var m = ~~(Math.sqrt(len) / m_Chain);
var ex = dx / m;
var ey = dy / m;
var x = o_trace.x[i-1] + ex;
var y = o_trace.y[i-1] + ey;
for (var j=1; j= -(qoy - py) * ly
&& (qnx - px) * lx < -(qny - py) * ly )
|| ( (qox - px) * lx < -(qoy - py) * ly
&& (qnx - px) * lx >= -(qny - py) * ly ) )
{
// 交点座標
var t = intersection(qox, qoy, qnx, qny, px, py, px+vx, py+vy);
if (!t){
var d1x = qnx - px;
var d1y = qny - py;
var d2x = qox - px;
var d2y = qoy - py;
if( (d1x * d1x + d1y * d1y) > (d2x * d2x + d2y * d2y) ){
t = {"x" : qnx, "y" : qny};
}else{
t = {"x" : qox, "y" : qoy};
}
}
var qtlen = (t.x-px) * (t.x-px) + (t.y-py) * (t.y-py);
// 方向確認
if( (t.x - px) * vx + (t.y - py) * vy > 0 ){
qtlen *= -1.;
}
// 最遠方点なら更新
if( qtlen < llen[j] ){
out_plx[j*2] = t.x;
out_ply[j*2] = t.y;
llen[j] = qtlen;
}
if( qtlen > rlen[j] ){
out_plx[j*2+1] = t.x;
out_ply[j*2+1] = t.y;
rlen[j] = qtlen;
}
}
}
qox = qnx;
qoy = qny;
}
return {"x": out_plx, "y": out_ply};
};
var clear = function(p){
m_Slice = ModifiedSimpson.getSlice();
p.m_trace.x = [];
p.m_trace.y = [];
p.m_ctx.font = "16px 'MS Gothic'";
p.m_mode = 0;
p.m_ctx.clearRect(0, 0, p.m_width, p.m_height);
p.callback({"H":0, "D":false});
};
var drawROI = function(p){
var ctx = p.m_ctx;
ctx.clearRect(0, 0, p.m_width, p.m_height);
ctx.beginPath();
ctx.arc(p.m_trace.x[0], p.m_trace.y[0], m_linesz, 0, Math.PI*2, false);
ctx.fill();
ctx.closePath();
var n = p.m_trace.x.length;
for (var i=1; i0 ||
navigator.userAgent.indexOf('iPod')>0 ||
navigator.userAgent.indexOf('iPad')>0 ||
navigator.userAgent.indexOf('Android')>0) {
canvas.addEventListener('touchstart',
function(e) {
e.preventDefault();
var n = e.touches.length;
if (n > 0) {
e = e || window.event;
mouseDown = true;
var rect=e.target.getBoundingClientRect();
mouseX = e.touches[n-1].pageX-rect.left;
mouseY = e.touches[n-1].pageY-rect.top;
mdown(iam, mouseX, mouseY);
}
}, false);
canvas.addEventListener('touchend',
function(e) {
mouseDown = false;
mup(iam, mouseX, mouseY);
}, false);
canvas.addEventListener('touchmove',
function (e) {
e.preventDefault();
var rect=e.target.getBoundingClientRect();
var n = e.touches.length;
if (n > 0){
mouseX = e.touches[n-1].pageX-rect.left;
mouseY = e.touches[n-1].pageY-rect.top;
if (mouseDown) {
mmove(iam, mouseX, mouseY);
}
}
}, false
);
}else{
canvas.addEventListener('mousedown',
function(e) {
e.preventDefault();
mouseDown = true;
e = e || window.event;
var rect=e.target.getBoundingClientRect();
mouseX = e.clientX-rect.left;
mouseY = e.clientY-rect.top;
mdown(iam, mouseX, mouseY);
}, false);
canvas.addEventListener('mouseup',
function(e) {
mouseDown = false;
mup(iam, mouseX, mouseY);
}, false);
canvas.addEventListener('mousemove',
function (e) {
e.preventDefault();
var rect=e.target.getBoundingClientRect();
mouseX = e.clientX-rect.left;
mouseY = e.clientY-rect.top;
if (mouseDown) {
mmove(iam, mouseX, mouseY);
}
}, false
);
}
this.m_ctx = canvas.getContext('2d');
this.m_width = canvas.width;
this.m_height = canvas.height;
clear(this);
}
} // Public Scope
}; // Constructor
///////////////////////////////////
//
// Measurement Display Area
//
///////////////////////////////////
new function() {
///////////////////////////////////
// Private Scope
///////////////////////////////////
var m_ctx = null;
var m_width = 100;
var m_height = 100;
var m_m1 = {"H": 0, "D": false};
var m_m2 = {"H": 0, "D": false};
var m_Slice = 20;
var report = function(){
if (!m_ctx){
return;
}
m_ctx.clearRect(0, 0, m_width, m_height);
if (m_m1.D && m_m2.D){
var h = (m_m1.H + m_m2.H) / 2;
var v = 0;
var hpi = Math.PI * h / 4.0;
for (var i=0; i
Tags:
プログラムメモ