X線CT装置の構成要素について

CT装置の種類について

X線CT装置には主に医療用のCT産業用のCTがあります。大きな違いは医療用のCTでは少ない被爆量で撮影を行うために、ヘリカルスキャンという螺旋状に撮影を行う技術が用いられていて撮影も数十秒で行うことができます。一方産業用のCTでは精密な検査を行うことが求められるので、強いX線を発生することができて数十秒から数十分と長時間の撮影も行うことができます。おもにコーンビームのX線で撮影が行われます。

ctsouti

医療用のCTでは患者さんはベッドに寝たままで、装置自体が回転して撮影を行っています。その一方で産業用CTは通常被写体を回転させて撮影を行っています。

CT装置の構成要素について

X線CT装置は、おおまかにX線を発生させるX線管装置とX線量を検出する検出器からできています。X線管装置と検出器の間に被写体を起き、被写体もしくはX線管と検出器を回転させながら撮影を行います。

X線管装置

X線管装置には主に密閉型開放型の二種類があります。開放型のX線管では焦点がX線の出る窓に近くなるのでその分高拡大率で撮影することが可能になります。対陰極としては銅やモリブデン、タングステンなどの金属に加速した電子ビームを当て、原子の1s軌道の電子を弾き飛ばします。空になった1s軌道に外の軌道の電子が遷移してきます。この遷移によって放出される電磁波をX線特に特性X線と呼んでいます。本研究で使用しているX線管は以下のような仕様となっています。

Xray tube pic

検出器

X線検出器にはパルス型積分型の検出器があります。パルス型の検出器にはシンチレーションカウンタのように、X線光子を1個1個計測するタイプであり、検出器が持つノイズはほとんど無視することができ、微弱なX線強度を精度よく検出することができます。一方積分型はX線エネルギーを可視光強度や電流などの物理量に変換して検出しています。X線フィルムやX線CCDがこのタイプです。本研究で使われている検出器はCCD型で二次元的にフォトダイオードが画素として並んだ撮像素子です。

ditector

CTスキャンの仕組み

X線管で発生させたX線を被写体に照射し、検出器で検出器に到達したX線強度を測定します。被写体を通過したX線は吸収され検出されるX線強度は小さくなっています。このデータ1つ1つを透視画像と呼びます。この透視画像を被写体の周囲方向から通常800方向程度のデータを取得し、得られたデータを使い画像を再構成することで物体の断面画像を得ています。

—————————————–参考文献———————————————-

「Wikipedia X線」

「二次元X線検出器の原理と性能,  雨宮,伊藤,  日本結晶学会誌, 45, p163−170, 2003」

X線CTの単純逆投影について

CTの勉強会第2回目は単純逆投影を行いCTの再構成画像をつくりました。CT撮影によって得られた800方向分の透視画像を縦に並べたサイノグラムと呼ばれるデータを使って、再構成画像を作っていきます。単純逆投影は最も簡単な投影方法で誤差が大きく生じる方法です。

1.順投影と逆投影について

まず順投影は下図のように被写体の周囲方向からX線を当てて、被写体のデータを取得する方法です。通常800方向分のデータを取得します。逆投影は得られた周囲方向のデータを使って逆にデータを収めていく方法です。例えば左の{3,12,3}という並びの投影データを収めると「3」が入っていた行はすべて「3」入ります。「12」が入っていた行はすべて「12」となります。これを取得したデータすべてで行い、そのつど加算していきます。最後に撮影方向数でそれぞれを割ると、再構成画像ができます。単純逆投影だとかなり誤差が大きくなり、通常ですとフィルタをかけることでより鮮明な再構成画像を作っています。

project

2.sng形式のサイノグラムを読み込む

前回の勉強会の時にrawデータを読み込んだのと同様にサイノグラムを読み込みます。サイノグラムの大きさは640×800pixelとなっているのでそこだけ気をつけます。画像を読み込むプログラムは第一回勉強会のページを参考にしてください。今回はモーターを撮影したデータを扱います。最終的に透視画像の中心部分の断面画像を作成します。

スクリーンショット 2013-10-14 13.54.24

3.投影データを作成する

sino[]という配列にデータを格納できたら、rawデータのままでは再構成画像を作ることができないので、対数を取って投影データを作成します。投影データは以下の数式で表せれます。但し、$$I_o$$が吸収体がないときの入射強度(65536)で、$$I_y$$が透過強度を表しています。

$$I = ln(\frac{I_0}{I_y})$$

これはX線に吸収体の厚み(L)が大きくなると、指数関数的にX線が透過しにくくなる性質があり、再構成する際には線形に戻す必要があるからです。詳しくは理解できたら追記します。

$$I = I_0e^{\sum_{m=1}^{n}\mu_ml_m}$$

それと投影データを作る際に、スケーリングを行うために同時に投影データの最大値と最小値を求めておきます。

<br />public static void change(){<br /><%%KEEPWHITESPACE%%>  for(int i = 0; i < height; i++){<br /><%%KEEPWHITESPACE%%>    for(int j = 0; j < width; j++){<br /><%%KEEPWHITESPACE%%>      //対数をとる<br /><%%KEEPWHITESPACE%%>      sinoP[i * width + j]= - Math.log(sino[i * width + j]/65536.0);<br /><br /><%%KEEPWHITESPACE%%>      //最大最小を求める<br /><%%KEEPWHITESPACE%%>      sinoPmax=Math.max(sinoP[i * width + j], sinoPmax);<br /><%%KEEPWHITESPACE%%>      sinoPmin=Math.min(sinoP[i * width + j], sinoPmin);<br /><%%KEEPWHITESPACE%%>    }<br /><%%KEEPWHITESPACE%%>  }<br /><%%KEEPWHITESPACE%%>  repaint();<br />}<br />

投影データの描画方法です。前回同様に描画を行うクラスを作成し、その中に記述していきます。理由はよくわかりませんがrawデータを単純に表示するときと違いスケーリングを行う必要があります。rawデータでは65536諧調のデータだったので256で割っていましたが、今回は画素値の最大値から最小値を引いたものに256をかけてからそれぞれの画素値を割っています。こうすることで投影データのサイノグラムを表示することができます。

<br />public void paintComponent(Graphics g) {<br /><%%KEEPWHITESPACE%%>  int width = 640; //幅<br /><br /><%%KEEPWHITESPACE%%>  //背景を白く塗りつぶす<br /><%%KEEPWHITESPACE%%>  g.setColor(Color.white);<br /><%%KEEPWHITESPACE%%>  g.fillRect(0, 0, width, width);<br /><br /><%%KEEPWHITESPACE%%>  for (int i = 0; i < width; i++) {<br /><%%KEEPWHITESPACE%%>    for (int j = 0; j < width; j++) {<br /><%%KEEPWHITESPACE%%>      //スケーリングを行う<br /><%%KEEPWHITESPACE%%>      int c = (int)(sbp[j+i*width]/(sbpmax - sbpmin)*256);<br /><br /><%%KEEPWHITESPACE%%>      g.setColor(new Color(c, c, c));<br /><%%KEEPWHITESPACE%%>      g.drawLine(j, i, j, i);<br /><%%KEEPWHITESPACE%%>    }<br /><%%KEEPWHITESPACE%%>  }<br />}<br />

スクリーンショット 2013-10-14 13.55.05

4.単純逆投影を行う

単純逆投影順投影とは逆にwidth(640px)×height(640px)の領域に800方向からのデータを投影していくという説明がよくされますが、通常はwidth×heightの領域を回転させ、その領域に投影データを入れていくという手法を取る方が実装が簡単なようです。投影データを入れていく際にピクセルをまたいでしまう場合があり、それはバイリニア法という画像処理方法を使って線形に補完して数値を入れていきます。最後にまた再構成画像の最大値と最小値を求めます。プログラムは以下のように記述することができます。

<br />public static void backProjection(){<br /><%%KEEPWHITESPACE%%>  int proj = 800; // プロジェクション数<br /><%%KEEPWHITESPACE%%>  int center = width/2;<br /><%%KEEPWHITESPACE%%>  double step = 360.0/(double)proj; //ステップ角度(度)<br /><%%KEEPWHITESPACE%%>  double sin, cos;<br /><%%KEEPWHITESPACE%%>  int x,y;<br /><%%KEEPWHITESPACE%%>  double bx,by;<br /><br /><%%KEEPWHITESPACE%%>  for(int p = 0; p < height; p++){<br /><%%KEEPWHITESPACE%%>    //現在の角度を計算<br /><%%KEEPWHITESPACE%%>    theta = p * step; //+=stepより安全<br /><br /><%%KEEPWHITESPACE%%>    //あらかじめ算出(角度はラジアン表記)<br /><%%KEEPWHITESPACE%%>    sin = Math.sin((theta+180)*Math.PI/180);<br /><%%KEEPWHITESPACE%%>    cos = Math.cos(theta * Math.PI /180);<br /><br /><%%KEEPWHITESPACE%%>    for(int i = 0; i < width; i++){<br /><%%KEEPWHITESPACE%%>       for(int j = 0; j < width; j++){<br /><br /><%%KEEPWHITESPACE%%>         //再構成画像上のピクセルを回転 各方向から投影していく方法は難しい<br /><%%KEEPWHITESPACE%%>         bx = (int)((i - center) * cos - (j - center) * sin );<br /><%%KEEPWHITESPACE%%>         by = (int)((i - center) * sin + (j - center) * cos );<br /><br /><%%KEEPWHITESPACE%%>         //回転後再構成領域からはみ出していたら次の点に進む<br /><%%KEEPWHITESPACE%%>         if(bx*bx + by*by >= center*center){<br /><%%KEEPWHITESPACE%%>           continue;<br /><%%KEEPWHITESPACE%%>         }<br /><br /><%%KEEPWHITESPACE%%>         bx += center;<br /><%%KEEPWHITESPACE%%>         by += center;<br /><br /><%%KEEPWHITESPACE%%>         x = (int)bx;<br /><%%KEEPWHITESPACE%%>         y = (int)by;<br /><br /><%%KEEPWHITESPACE%%>         double bbx0,bbx1,bby0,bby1,s00,s10,s01,s11;<br /><%%KEEPWHITESPACE%%>         bbx0 = bx - x;<br /><%%KEEPWHITESPACE%%>         bbx1 = 1 - bbx0;<br /><%%KEEPWHITESPACE%%>         bby0 = by - y;<br /><%%KEEPWHITESPACE%%>         bby1 = 1 - bby0;<br /><br /><%%KEEPWHITESPACE%%>         s00 = bbx0 * bby0;<br /><%%KEEPWHITESPACE%%>         s10 = bbx1 * bby0;<br /><%%KEEPWHITESPACE%%>         s01 = bbx0 * bby1;<br /><%%KEEPWHITESPACE%%>         s11 = bbx1 * bby1;<br /><br /><%%KEEPWHITESPACE%%>         if(x >= 0 && x <= width -1 && y >= 0&& y <= width -1){<br /><%%KEEPWHITESPACE%%>           sbp[x + y * width] += sino[i + width * (int)(theta/step)]*s11;<br /><%%KEEPWHITESPACE%%>         }<br /><%%KEEPWHITESPACE%%>         if(x+1 >= 0 && x+1 <= width -1 && y >= 0&& y <= width -1){<br /><%%KEEPWHITESPACE%%>           sbp[x + y * width] += sino[i + width * (int)(theta/step)]*s01;<br /><%%KEEPWHITESPACE%%>         }<br /><%%KEEPWHITESPACE%%>         if(x >= 0 && x <= width -1 && y+1 >= 0&& y+1 <= width -1){<br /><%%KEEPWHITESPACE%%>           sbp[x + y * width] += sino[i + width * (int)(theta/step)]*s10;<br /><%%KEEPWHITESPACE%%>         }<br /><%%KEEPWHITESPACE%%>         if(x +1>= 0 && x+1 <= width -1 && y +1>= 0&& y +1<= width -1){<br /><%%KEEPWHITESPACE%%>           sbp[x + y * width] += sino[i + width * (int)(theta/step)]*s00;<br /><%%KEEPWHITESPACE%%>         }<br /><%%KEEPWHITESPACE%%>       }<br /><%%KEEPWHITESPACE%%>     }<br /><%%KEEPWHITESPACE%%>   }<br /><%%KEEPWHITESPACE%%>  //再構成画像の最大値最小値を求める<br /><%%KEEPWHITESPACE%%>  for(int i = 0; i < width; i++){<br /><%%KEEPWHITESPACE%%>    for(int j = 0; j < width; j++){<br /><%%KEEPWHITESPACE%%>      spbmax = Math.max(sbp[i*width+j], spbmax);<br /><%%KEEPWHITESPACE%%>      spbmin = Math.min(sbp[i*width+j], spbmin);<br /><%%KEEPWHITESPACE%%>    }<br /><%%KEEPWHITESPACE%%>  }<br />}<br />

これでsbp[]の中に再構成画像のデータを格納することができたので、投影データを表示した時と同じ方法を使って、再構成画像を表示します。これで一番簡単な逆投影ができるようになりました。全体的にボケが多い再構成画像になっています。よりはっきりした画像を得るためにはフィルタをかけるなど難しい処理がまだ必要になるようです。

スクリーンショット 2013-10-23 21.20.13

X線CTの撮影実験

X線CTのX線点光源のブレが被写体に与える影響を調べるために,微小な被写体を拡大率25倍で撮影を行った.本実験では直径0.4㎜のドリルの刃を,マイクロフォーカスX線CT(ScanXmate-A130ss940, コムスキャンテクノ株式会社)を使用しで撮影を行った。

CT
X線CT装置

drildril
被写体のドリルの刃

長時間のCT撮影を行うことで点光源のブレが大きくなると考えられるので,以下の3つの撮影条件で撮影を行った.各撮影方向で取得するデータ数である積算回数を増やすことによって撮影時間を増やす.

既存研究について

X線CT装置の焦点が撮影中に変動する現象はいくつかの著書で紹介されています[1]。ですが、焦点ブレを補正する研究がおこなわれている例はほとんどなく、私が知る限りでは日立メディコが行っている、機械的に焦点移動を起こしにくくする研究のみです。その研究の概要を紹介します。

 

その一方で画像データ上で焦点移動を補正する研究は私の知る限りでは存在せず、その有用性は大きいと考えています。

————————————–参考文献————————————–

「X線管装置の焦点移動量低減技術の開発」, MEDIX, VOL.36, pp32-36

クイックソートとその計算量について

【編集中】

数列を昇順または降順に並べるアルゴリズムの一つにクイックソートがあります。直感的に分かりやすいバブルソートと比較してピボットによる分割があったりでめんどくさいイメージがあったのですが、有名なアルゴリズムなので勉強しておきたいと思います。以下計算手順です。テストケースとして次の数列を扱います。

$$A={4,8,6,5,2,1,3,9,7}$$

  1. 基準値を決めます。たとえば、最初と最後の要素の平均にします。
  2. 前方から基準値より大きな数を、後方から基準より小さな数を探します。見つかったら入れ替えます。
  3. 得られた列はぶつかったところを境に、基準値よりも小さいグループと大きいグループに分かれています。そして、この得られた列それぞれに同じ手順を繰り返していきます。

qick

この操作を繰り返すことで数列を昇順に並び替えることができます。それではこの操作をC言語で記述してみます。


#include <stdio.h>
int main(void){

return 0;
}

CT画像の読み込み方書き出し方

第一回の勉強会では大学で使用しているマイクロフォーカスX線CT(ScanXmate-A130ss940, コムスキャンテクノ株式会社)で採用されているRAWデータの画像を読み書きする方法を勉強しました。今回は最終的にrawデータを読み込んで、リアルタイムで閾値処理をするプログラムを作成しました。

rawデータの読み込み方

今回扱うrawデータは640×480(307200)pixelですが、データの大きさは614400byteとピクセルの情報の2倍の大きさとなっています。これは2byteで1pixel分の情報を表していることを意味していてこの画像が16bitのrawデータであることがわかります(1byteが8bitです)。

data16bitの画像を読み込むためには、2byte分の情報から1pixelの情報を知るために特別な処理が必要となります。画像にはビッグエンディアンリトルエンディアンという記録方法の方式があります。例えば{1234ABCD}という4バイトのデータをデータの上位バイトからメモリに{12 34 AB CD}と並べる方式をビッグエンディアン、{CD AB 34 12}とデータの下位バイトから並べる方式をリトルエンディアンと言います。それぞれデータの読み込み方が異なってくるので注意が必要です。

$$d_1=b_1+b_2\times256$$

もしくは

$$d_1=b_1\times256+b_2$$

となります。

public static void readRawData() {
  File f = new File("○○○.raw");
  try {
    DataInputStream dis = new DataInputStream(new FileInputStream(f));
    //データの大きさだけバイト型の配列を用意
    byte[] b = new byte[dis.available()];
    //データを読み込む
    dis.read(b);
    //データの形式に合わせてint型の配列に画素地を入れる(今回は16bit)
    for (int i = 0; i < b.length / 2; i++) {
      rawdata[i] = (0xff & b[i * 2]) + (0xff & b[i * 2 + 1]) * 256;
    }
  } catch (Exception e) {
    e.printStackTrace();
  }
}

上記のように記述することによって16bitのrawデータを配列に格納することができました。配列に数値を格納できれば、閾値処理を行ったり、平滑化フィルタをかけたりなど様々な画像処理を行うことができるようになります。画像は画像の形式によって配列に格納する方法が変わってきます。例えばTIFF形式の画像ファイルの場合、ヘッダやフッタ(データの最初と最後)に画像に関する情報が入っている場合があり、このような画像では単純に配列に格納するだけでは読み込むことができません。

rawデータの表示方法

続けて表示方法です。描画を行うクラスを作成し、メインクラスでインスタンスを生成し、描画を行うます。NetBeansの場合はパッケージを右クリック→新規→JPanelフォームをクリックし、描画を行うパネルを作ります。そうすることでJPanelクラスをスーパークラスとするサブクラスが自動生成されます。自動生成したクラスの中に以下のコードを記述します。

public void paintComponent(Graphics g){
  //背景を白く塗りつぶす
  g.setColor(Color.white);
  g.fillRect(0,0,width,heigth);
  //画像データを描画する
  for(int i = 0; i < height; i++){
    for(int j = 0; j < width; j++){
      //65536諧調になっているので256諧調に変換
      int c = rawdata[i * width + j]/256;
      //色をセット(グレースケールなのでRGBすべてに同じ値を入れる)
      g.setColor(new Color(c,c,c));
      //1pixelずつ色を塗る
      g.drawLine(j, i, j, i);
    }
  }
}

上記のコードを記述し呼び出すことで、rawdata[]に格納していた画像のデータを描画することができます。次に呼び出し方についてです。

initComponentsメソッドが記述されているメソッドに以下のコードを書き足します。ちなみにinitComponentsメソッドはコンポーネントの作成に関するコードが記述されていてプログラムの中で一番最初に呼び出される部分になっています。

public StudyCt() {
  initComponents();
  //インスタンスの生成
  PanelDraw pDraw = new PanelDraw();
  lbl1.setLayout(new BorderLayout());
  //生成したパネルをラベルの上に張り付ける
  lbl1.add(pDraw);
  //外観を成形する
  pack();
}

これでおおまかな部分は完成です。後は閾値を変更した後、ボタンを押した後など随所でrepaintメソッドを呼び出すことで、再描画を行うことができます。

rawデータの書き出し方

16bitのrawデータとして画像を書き出します。読み込み方と方法は似ていて、おまじないの部分が多くなってしまいますが、以下のように記述します。理由は不明ですが書き出すときはsng形式で書き出すようです。

public void writeRaw(){
  File f = new File("○○○.sng");
  int bit = 16;
  try{
    byte b1[] = new byte[width * height * bit / 8];
    for(int i = 0; i < height; i++){
      for(int j = 0; j < width; j++){
        int buf = 0;
        if(rawdata[j][i]>65535)buf = 65535;
        else if(rawdata[j][i] < 0)buf = 0;
        else buf = (int)rawdata[j][i];
        //1pixelのデータを2byte分に分けて配列に格納する
        b1[(i * 640+j)*2] = (byte)(buf%256);
        b1[(i * 640+j)*2+1] = (byte)(buf/256);
      }
    }
    FileOutputStream fos0 = new FileOutputStream(f);
    fos0.write(b1,0,widht*height*2);
    fos0.close();
  }catch(IOException e){
  }
}

上記のように記述することで○○○.sngという画像ファイルを作成することができます。だいたいの部分が読み込むときと逆の操作を行っているのがわかると思います。配列に格納するときは条件分岐させて0以下65536以上の情報を持っているpixelがないようにしています。配列に格納できたら書き出してファイルを閉じます。これで完成です。

今回の勉強会で勉強したことを使って簡単なソフトを作ってみました。CTの画像を読み込んで、閾値処理を行い形状と拡大率がわかっている被写体に対して理想的な再構成画像とどれだけ違いがあるかを調べることができます。勉強会大変勉強になりました。

スクリーンショット 2013-10-12 11.40.51