NeuromagのMRI断面・脳表画像との位置合わせ

Neuromagでは以下の4つの座標系が使われています。 (1).脳磁図センサー(device)座標系、 (2).Polhemusで取得した左耳介、鼻根点、右耳介の3つ点を基準点とする頭(head)座標系、 (3).構造画像であるDICOMデータを計測する際に使われた(MRI)座標系、 (4).画像上(slice)の座標系。

センサー・頭座標間の変換行列は、脳磁場の計測直前にHead Position Indicator コイルに電流を流し、計測用ワークステーションが自動で4つの等価電流双極子推定を行うことで確立されます。 頭・MRI座標間の変換行列は、解析者がMRI Labを起動して、MRI画像上で上記3つの基準点を選ぶことで確立されます。 MRI・画像間の変換行列は、CT・MRIの元画像の場合は、DICOMファイルに書き込まれていますが、 脳表画像の場合はViewBrainのrendererツールが自動で確立していきます。

Contents

FIFFファイルの読み込み

ここでは各種FIFFファイルを読み込み、とりあえずDICOM ACCESSで作成されたsets以下のFIFファイルと、 それをもとにSEG LABで作成した3D.FIFファイルをhelperVolumeRegistration関数で比較してみました。 尚、前者はint16、後者はuint8で、両者とも位置合わせ後です。 前者ではDICOMファイルの並び方によっては、画像の順番が狂っていることが有るので注意が必要です

for nn=1:4
    switch nn
        case 1;filename='hasizume_akira_coregistered_with_RSEF.fif';% DICOM Access後MriLabで位置合わせ
        case 2;filename='hasizume_akira_coregistered_with_RSEF_3D.fif';% SegLabで3D化
        case 3;filename='bhasizume_akira_coregistered_with_RSEF_3D.scenes.fif';% 脳表画像
        case 4;filename='SEF_median_ave.fif';%脳磁図データ
    end;
    F=hns_loadfiff4(filename);

    if isfield(F,'dcmfiles')
        nf=size(F.dcmfiles,2);
        slice2MR=zeros(3,8,nf);
        for n=1:nf
            [~,name,ext]=fileparts(F.dcmfiles{n});
            loadfile=[name,ext];
            info=dicominfo(loadfile);

            e0=info.ImagePositionPatient/1000;% mmからmに
            ex=info.ImageOrientationPatient(1:3)';
            ey=info.ImageOrientationPatient(4:6)';
            e0(1)=-e0(1);ex=-ex;% 右手系と左手系の反転
            ez=cross(ex,ey);
            M=[[[ex;ey;ez],e0];0 0 0 1];
            M=[inv(M),M];
            slice2MR(:,:,n)=M(1:3,:);
            F.Volume3D(:,:,n)=int16(dicomread(loadfile));
        end;
        row=double(info.Rows);
        column=double(info.Columns);
        F.scale=[row,column,[row,column].*info.PixelSpacing'/1000,nf];
    end;
    eval(sprintf('F%d=F;',nn));
end;
clear F;

helperVolumeRegistration(F1.Volume3D,F2.Volume3D);
view([-80,10]);
警告: WAVPLAY は将来のリリースで削除される予定です。代わりに AUDIOPLAYER を使用してください。 
警告: WAVPLAY は将来のリリースで削除される予定です。代わりに AUDIOPLAYER を使用してください。 
警告: WAVPLAY は将来のリリースで削除される予定です。代わりに AUDIOPLAYER を使用してください。 
警告: WAVPLAY は将来のリリースで削除される予定です。代わりに AUDIOPLAYER を使用してください。 

Symbolic Math Toolboxを使って両耳・鼻根点から頭座標系を構築

左耳がP1[p1x,p1y,p1z],右耳がP2[p2x,p2y,p2z],鼻根点がP3[p3x,p3y,p3z], 頭座標の原点がP0[p0x,p0y,p0z]とします。 P0はP1とP2を結ぶ線にP3からおろした垂線の足です。 頭座標系を作成した後、頭座標系とMRI座標系間の変換行列を求め、MRI LABの結果と比較しました。 微妙な誤差がありますが、singleとdoubleの計算結果の違いだと思います。

syms p0x p0y p0z p1x p1y p1z p2x p2y p2z p3x p3y p3z real;
[sol_p0x,sol_p0y,sol_p0z]=solve(...
    (p2x-p1x)/(p2x-p0x)==(p2y-p1y)/(p2y-p0y),...
    (p2x-p1x)/(p2x-p0x)==(p2z-p1z)/(p2z-p0z),...
    (p3x-p0x)*(p2x-p1x)+(p3y-p0y)*(p2y-p1y)+(p3z-p0z)*(p2z-p1z)==0....
    );
sol_p0x
sol_p0y
sol_p0z

P=F2.DigitPts;
P0=[...
double(subs(sol_p0x,[p1x,p1y,p1z,p3x,p3y,p3z,p2x,p2y,p2z],reshape(F2.DigitPts',[1,9])));...
double(subs(sol_p0y,[p1x,p1y,p1z,p3x,p3y,p3z,p2x,p2y,p2z],reshape(F2.DigitPts',[1,9])));...
double(subs(sol_p0z,[p1x,p1y,p1z,p3x,p3y,p3z,p2x,p2y,p2z],reshape(F2.DigitPts',[1,9])))];
P0=P0';

figure('color',[1,1,1]);
h=plot3(P(:,1),P(:,2),P(:,3),'bo');hold on;
text(P(1,1),P(1,2),P(1,3),'LPA');
text(P(2,1),P(2,2),P(2,3),'Nasion');
text(P(3,1),P(3,2),P(3,3),'RPA');
h=[h;plot3(P0(1),P0(2),P0(3),'bo')];
set(h,'Marker','pentagram','MarkerSize',12,'MarkerFaceColor',[0,1,1]);
xlabel('x');ylabel('y');zlabel('z');grid on;
daspect([1,1,1]);
title('MRI座標系上の両耳・鼻根点から求めた頭座標系の原点');
% 頭座標系のx方向 y方向 z方向の単位ベクトルを求める
vec=P-ones(3,1)*P0;
vec=vec./(sum(vec.^2,2).^0.5*ones(1,3));
vec(1,:)=vec(3,:);% 右耳介方向がX軸の正の方向
vec(4,:)=-vec(1,:);% 作図用
vec(5,:)=-vec(2,:);% 作図用
vec(3,:)=cross(vec(1,:),vec(2,:));% HEAD2MRIの回転部分 拡大縮小・平行移動はまだ
scale=0.1;% 適当に縮小して表示
for n=1:5
    h=quiver3(P0(1),P0(2),P0(3),vec(n,1)*scale,vec(n,2)*scale,vec(n,3)*scale,'b');
    if n>3;set(h,'linestyle',':');end;
end;

% 基準点をまとめる
P0=[P(1,:);P(3,:);P0;P(2,:)]';
P0(4,:)=1;
vec(4:5,:)=[];
% digitized points
P=F4.DigitPts';
P(4,:)=1;
MTX=F2.HEAD2MRI(:,1:4);
MTX(4,:)=[0,0,0,1];
P=MTX*P;
plot3(P(1,:),P(2,:),P(3,:),'r^','MarkerSize',5,'MarkerFaceColor',[1,0,1]);

axis tight;
view([-100,35]);
rotate3d on;
% 検算
HEAD2MRI=[inv(vec),P0(1:3,3)];% 順番をずらした。
k=F2.HEAD2MRI(:,1:4)-HEAD2MRI;
max(abs(k(:)))
 
sol_p0x =
 
(p3x*p1x^2 - 2*p3x*p1x*p2x - p1x*p1y*p2y + p3y*p1x*p1y + p1x*p2y^2 - p3y*p1x*p2y - p1x*p1z*p2z + p3z*p1x*p1z + p1x*p2z^2 - p3z*p1x*p2z + p3x*p2x^2 + p2x*p1y^2 - p2x*p1y*p2y - p3y*p2x*p1y + p3y*p2x*p2y + p2x*p1z^2 - p2x*p1z*p2z - p3z*p2x*p1z + p3z*p2x*p2z)/(p1x^2 - 2*p1x*p2x + p2x^2 + p1y^2 - 2*p1y*p2y + p2y^2 + p1z^2 - 2*p1z*p2z + p2z^2)
 
 
sol_p0y =
 
(p1x^2*p2y - p1x*p2x*p1y - p1x*p2x*p2y + p3x*p1x*p1y - p3x*p1x*p2y + p2x^2*p1y - p3x*p2x*p1y + p3x*p2x*p2y + p3y*p1y^2 - 2*p3y*p1y*p2y - p1y*p1z*p2z + p3z*p1y*p1z + p1y*p2z^2 - p3z*p1y*p2z + p3y*p2y^2 + p2y*p1z^2 - p2y*p1z*p2z - p3z*p2y*p1z + p3z*p2y*p2z)/(p1x^2 - 2*p1x*p2x + p2x^2 + p1y^2 - 2*p1y*p2y + p2y^2 + p1z^2 - 2*p1z*p2z + p2z^2)
 
 
sol_p0z =
 
(p1x^2*p2z - p1x*p2x*p1z - p1x*p2x*p2z + p3x*p1x*p1z - p3x*p1x*p2z + p2x^2*p1z - p3x*p2x*p1z + p3x*p2x*p2z + p1y^2*p2z - p1y*p2y*p1z - p1y*p2y*p2z + p3y*p1y*p1z - p3y*p1y*p2z + p2y^2*p1z - p3y*p2y*p1z + p3y*p2y*p2z + p3z*p1z^2 - 2*p3z*p1z*p2z + p3z*p2z^2)/(p1x^2 - 2*p1x*p2x + p2x^2 + p1y^2 - 2*p1y*p2y + p2y^2 + p1z^2 - 2*p1z*p2z + p2z^2)
 

ans =

  7.4457e-08

基準点・頭皮プロット点・センサーを脳断面画像に重畳

MRI座標上のモノを実際の脳断面画像上に表示してみます。 尚、MATLABの表示の問題でY軸方向にマイナスをかけています。

SliceNo=round(linspace(1,F2.scale(5),5));
SliceNo([1,end])=[];
scale=diag([F2.scale(1:2)./F2.scale(3:4),1000,1]);% zubbfer方向はmをmmに
np=size(P,2);
lim=2.5;% 厚さ±2.5mmを表示
lim2=15;%  厚さ±15mmを表示
S0=F4.ChInform(1:3,1:3:306);% センサー座標系のセンサー位置
Sx=F4.ChInform(4:6,1:3:306);% センサー座標系の接平面方向
Sy=F4.ChInform(7:9,1:3:306);% センサー座標系の接平面方向
Sz=F4.ChInform(10:12,1:3:306);% センサー座標系の法線方向
S0(4,:)=1;
MTX4=[F4.MEG2HEAD(:,1:4);0 0 0 1];% センサー座標・頭座標間の変換行列
MTX2=[F3.HEAD2MRI(:,1:4);0 0 0 1]*MTX4;% センサー座標・MRI座標感の変換行列
% MTX3=[F3.HEAD2MRI(:,1:4);0 0 0 1]*MtX4;% 同上(異なるFIFFファイル)
S0=MTX2*S0;
Sx=MTX2(1:3,1:3)*Sx;
Sy=MTX2(1:3,1:3)*Sy;
Sz=MTX2(1:3,1:3)*Sz;
wx=0.014;wz=0.0003;
S=[S0(1:3,:)+wz*Sz;...% MRI座標系上のセンサーの中心点の位置
   S0(1:3,:)+wx*Sx+wx*Sy+wz*Sz;...
   S0(1:3,:)+wx*Sx-wx*Sy+wz*Sz;...
   S0(1:3,:)-wx*Sx-wx*Sy+wz*Sz;...
   S0(1:3,:)-wx*Sx+wx*Sy+wz*Sz];
S=reshape(S,[3,5*102]);
S(4,:)=1;

for n=1:length(SliceNo)
    MTX=[squeeze(F2.slice2MR(:,5:8,SliceNo(n)));0 0 0 1];
    p0=scale*MTX*P0;
    p=scale*MTX*P;
    figure('color',[1,1,1]);
    plot(1:np,p(3,:));hold on;
    x=find(p(3,:)<lim & p(3,:)>-lim);
    plot(x,p(3,x),'r^','MarkerFaceColor',[1,0,1]);
    plot([1;np]*ones(1,2),lim*ones(2,1)*[-1,1],'r');
    ylabel('Z buffer [mm]');
    axis tight;
    title(sprintf('頭皮プロット点 %d/%d %d枚目',length(x),np,SliceNo(n)));
    s=scale*MTX*S;
    s=reshape(s(1:3,:),[3*5,102]);
    z=find(s(3,:)<lim2 & s(3,:)>-lim2);

    figure('color',[1,1,1]*0,'InvertHardCopy','off');
    imagesc(squeeze(F2.Volume3D(:,:,SliceNo(n))));
    colormap(gray);daspect([1,1,1]);hold on;
    h=plot(p0(1,:),-p0(2,:),'b','Marker','pentagram');
    set(h,'MarkerSize',12,'MarkerFaceColor',[0,1,1]);
    plot(p(1,x),-p(2,x),'r^','MarkerFaceColor',[1,0,1]);
    if isempty(z);continue;end;
    h=fill(s(4:3:15,z),-s(5:3:15,z),'g','FaceColor',[0.5,1,0]);
    set(gca,'position',[0,0,1,1]);
    axis off;axis tight;
end;

基準点・頭皮プロット点・センサーを脳表画像に重畳

頭・MRI座標感の変換行列が同一として、脳断面画像に重畳した際のでMRI座標上のデータはそのまま使います。 奥行き方向であるZ buffer情報はありません。そこでセンサーの法線方向を計算し、適当に間引いています。 ViewBrainもこれくらいできたらいいんだけど・・・。

SliceNo=round(linspace(1,F3.scale(5),5));
SliceNo([1,end])=[];
scale=diag([F3.scale(1:2)./F3.scale(3:4),1000,1]);% zubbfer方向はmをmmに
Sz(4,:)=1;
for n=1:length(SliceNo)
    MTX=[squeeze(F3.slice2MR(:,5:8,SliceNo(n)));0 0 0 1];
    p0=scale*MTX*P0;
    p=scale*MTX*P;
    s=scale*MTX*S;
    s=reshape(s(1:3,:),[3*5,102]);
    sz=MTX*Sz;
    z=find(sz(3,:)>-0.5);

    figure('color',[1,1,1]*0,'InvertHardCopy','off');
    imagesc(squeeze(F3.VolImage(:,:,SliceNo(n))));
    colormap(gray);daspect([1,1,1]);hold on;
    h=plot(p0(1,:),-p0(2,:),'b','Marker','pentagram');
    set(h,'MarkerSize',12,'MarkerFaceColor',[0,1,1]);
    plot(p(1,:),-p(2,:),'r^','MarkerFaceColor',[1,0,1]);
    if isempty(z);continue;end;
    h=fill(s(4:3:15,z),-s(5:3:15,z),'g','FaceColor',[0.5,1,0],'FaceAlpha',0.2);
    set(h,'LineWidth',1,'EdgeColor',[0.5,1,0]);
    set(gca,'position',[0,0,1,1]);
    axis off;axis tight;
end;

基準点・頭皮プロット点・センサーをボリュームデータ上に重畳

MRI座標系と画像座標系間の変換行列は有るのですが、MRI座標系とデータボリューム間での変換行列はありません。 なので、作ります、といってもscaleのところをmmから枚に返るだけです。 slice2MR=squeeze(F2.slice2MR(:,1:3,1)); scale=[F2.scale(1:2)./F2.scale(3:4),F2.scale(5),1]; T=squeeze(F2.slice2MR(:,4,:));

zscale=F2.slice2MR(:,4,end)-F2.slice2MR(:,4,1);
zscale=sqrt(sum(squeeze(zscale).^2));
scale=[F2.scale(1:2)./F2.scale(3:4),(F2.scale(5)-1)/zscale];
MTX=diag([scale,1])*[squeeze(F2.slice2MR(:,5:8,1));0 0 0 1];
MTX=diag([1,-1,1,1])*MTX;
p0=MTX*P0;
p=MTX*P;np=size(p,2);
s=MTX*S;
s=reshape(s(1:3,:),[3*5,102]);
lim=3;% 頭皮プロット転用スライス厚±3枚
lim2=20;% センサー用スライス厚±20枚

for ax=1:3
    SliceNo=round(linspace(1,size(F2.Volume3D,ax),5));
    SliceNo([1,end])=[];
    for n=1:length(SliceNo);
        switch ax
            case 2;V=F2.Volume3D(SliceNo(n),:,:);V=squeeze(V)';ax1=1;ax2=3;
            case 1;V=F2.Volume3D(:,SliceNo(n),:);V=squeeze(V)';ax1=2;ax2=3;
            case 3;V=F2.Volume3D(:,:,SliceNo(n));V=squeeze(V); ax1=1;ax2=2;
        end;
        x=find(p(ax,:)>=SliceNo(n)-lim & p(ax,:)<=SliceNo(n)+lim);
        figure('color',[1,1,1]);plot(1:np,p(ax,:));hold on;
        plot(x,p(ax,x),'r^','MarkerFaceColor',[1,0,1]);
        plot([1;np]*ones(1,2),SliceNo(n)+lim*ones(2,1)*[-1,1],'r');
        z=find(s(ax,:)>=SliceNo(n)-lim2 & s(ax,:)<=SliceNo(n)+lim2);

        figure('color',[1,1,1]*0,'InvertHardCopy','off');
        imagesc(V);hold on;
        colormap(gray);
        h=plot(p0(ax1,:),p0(ax2,:),'b','Marker','pentagram','MarkerFaceColor',[0,1,1]);
        plot(p(ax1,x),p(ax2,x),'r^','MarKerFaceColor',[1,0,1]);
        daspect([scale(ax1),scale(ax2),scale(ax)]);
        if isempty(z);continue;end;
        h=fill(s((3+ax1):3:15,z),s((3+ax2):3:15,z),'g','FaceColor',[0.5,1,0]);
        axis tight;axis off;set(gca,'position',[0,0,1,1]);
    end;
end;