最終更新: a few seconds agoRemove Netlify-related code from main (grafted, HEAD)

2D モードの ImageSource の作成

2 次元画像を単に表示する ImageSource を実装し、CIRCUS DB に上手く組み込む。

概要

これまでの CIRCUS DB では CT や MRI などの 3 次元画像をボリュームで表示することに特化していたが、2 次元画像の表示とアノテーションにも対応する。

ここで、「2 次元画像(2D 画像)」とは、以下のようなものを指すことにする。

  • 胸部単純写真 (CXR): モダリティが CRDR となっている
  • マンモグラフィー: モダリティが MG となっている
  • MRI の再構成済み画像(プリレンダされている VR 画像):モダリティは MR だがカラー画像。
  • 内視鏡画像(本質的に単なるカラーのデジカメ写真)

これらのようなものを表示するための TwoDimensionalImageSource を実装し、CIRCUS システム内で統合的に動作するようにする。

2D 画像とはいえそれが複数枚あってシリーズを構成しているという点は同じ。例えば同じシリーズ UID のシリーズに「正面像と側面像の 2 枚の 2D 画像が入っている」「カラー写真が 20 枚入っている」ということは普通にある。単に 3 次元ボリュームとして MPR や VR を計算する意味がないということである。

含まれる作業

RawData のカラー画像への対応

RawData, PixelFormat を拡張してカラー画像 (RGBA 32bit/voxel) に対応する。

  • PixelFormat に rgba8 を追加する。
  • RawData は単なる画素置き場として使用できれば十分なので、getPixelAt などが動けばよい。バイリニア補間や MPR など、R/G/B 成分に分けて正しく画素を補間するようなコードは不要。そのような 2 次元の補間は canvas でやった方が断然速いので。エラーチェックをするだけで遅くなりそうなので、チェックすらせず単に間違った値を返すだけで構わないが、一応コメントに「カラー画像の時は正しい値を返さない」みたいな記述はしておく。(遅くならないようなら throw するようにしてもいいかも)
  • アルファチャンネルは当面不要なのだが、Canvas 内部の画像形式が RGBA なので、メモリ効率より実行時パフォーマンスを優先してそちらの仕様に合わせることにする。
  • ウィンドウについても RGB 成分を分割して正しく計算するようなコードは不要。間違った値を返すだけで構わない(ソースコードにカラー非対応というコメントだけは付ける)。

カラー画像や CXR 画像の読み取り

circus-lib 内の DicomPixelExtractor を拡張して、CR, DR, MG やカラーの画像を読み込めるようにする。

  • CR などのモノクロ画像の場合は基本 int16 などを使う。
  • カラー画像の場合 rgba8 な ArrayBuffer を吐き出す。

CIRCUS RS の metadatavolume エンドポイントの改善

現在の metadata は与えられたサブシリーズ(seriesUid と partialVolumeDescriptor の組み合わせにより定義されるもの。以下同様)がモノクロ 3D 画像であるというのを前提に動き、情報を返している。この部分で、カラー画像や 2D 画像であるということを正しく判定して、十分な情報を返すようにする。

シリーズが 2D なのか 3D なのかの判定は、本当に厳密にやろうとすると全 DICOM 画像を開いて走査しおわるまで行えない(100 枚の画像のうち 90 枚目だけが他とフォーマットが違う、ということは理論上あり得るため)。しかし metadata はフロント側でのメモリ確保にも使われるため高速に動作する必要がある。

このため、metadata の部分では、これまで同様にサブシリーズの最初の 2 枚の画像だけを見て「3D っぽい 」かどうかを判定することにする。現状では「3D っぽい」の判定を以下のようにする(この部分は将来的に改善する可能性が高いので綺麗に分離した isLike3D() のような関数として定義すること)。

「3D っぽい画像」とは、サブシリーズの最初の 2 枚が以下のすべてを満たすものとする:

  • モダリティが CT, MR または PT である
  • ピクセルフォーマットがモノクロである
  • DICOM タグに書かれている画像の向きが 1 枚目と 2 枚目で同一である
  • DICOM タグに再構成画像であるというフラグがない

上記以外のサブシリーズはすべて 2D モードで扱うべき(MPR や VR が意味を成さない)シリーズということになる。

VolumeProvider は今のところエラー処理が非常に甘いが、以下のようなことがあった場合はエラーを吐くようにする。

  • 「3D っぽい」と思って画像を読み込んでいたが途中で上記の条件に合わない DICOM 画像が現れた場合
  • 2D か 3D かに関わらず、途中で画像のサイズやピクセルフォーマットが変わった場合

TwoDimensionalImageSource とそれに対応する ViewState の作成

以下のような ViewState に対応する新しい ImageSource を作成する。

interface TwoDimensionalViewState {
  readonly type: '2d';
  readonly imageNumber: number;
  readonly origin: Vector2D;
  readonly xAxis: Vector2D;
  readonly yLength: number;
  readonly window?: ViewWindow;
  readonly interpolationMode?: 'none' | 'bilinear';
}

基本は 3D の MprViewState を 2 次元に落としたものと考えればよい。例えば横 3000 × 縦 4000 ピクセルの画像を 512×512 のビューアに表示する場合、以下のような形になる。

2D View State

const viewState = {
  type: '2d',
  imageNumber, 10,
  origin: [-70, 0],
  yAxis: [230, 0],
  yLength: 400,
  window: { width: 300, level: 150 }
};

ViewState がこのような形になっているのは将来的に 2D 回転にも対応するためだが、今回はひとまずそのツールは実装しないでよい。

TwoDimensionalImageSource は VolumeLoader から画素を読み取って画像を描画するという点は同一だが、MPR 計算を一切行わずに、普通の CanvasRenderingContext2D.drawImage() を使って画像を描画する。

TwoDimensionalImageSource.draw() は、内部の VolumeLoader が持っている RawData にアクセスし、必要に応じてウィンドウ計算を行いながら ImageData を取り出し、普通の drawImage() を使って画面に描画する。

  • パフォーマンスのため、このウィンドウ処理済み ImageData (そのままキャンバスに drawImage できるもの)については TwoDimensionalImageSource 内でメモ化を行うこと。z-index と window の組み合わせで ImageData はメモ化できるので最後の 10 枚くらいは持っておけばよさそう。
  • カラー画像についてはそもそもウィンドウ処理がないので、RawData 内の ArrayBuffer をそのまま描画できる。

CIRCUS DB での画像表示

metadata に従って 2D か 3D かのモードを切り替えつつ画像を表示するようにする。

ツールの調整

CIRCUS RS / DB で、以下の通り、ツールがうまく動作するようにする。

有効になるツール

  • Pager: 1 枚ずつ画像をめくる。
  • Zoom
  • Hand
  • Window (ただしカラーの場合は対応しない)

マウスホイールも Pager 相当の動作が行えるように対応する。

無効になるツール

  • CelestialRotate, Brush/Eraser/Bucket/Wand も含む上記以外のすべて

VoxelCloud 系は将来的には対応する可能性もあるが現時点で需要がないので今回の範囲外とする。voxels 型のラベルがアクティブな状態で 2D ビューア上でブラシツールがドラッグされた場合は可能ならエラーを表示する(難しければただ何も反応しないのでもよい)。

アノテーションの調整

以下の 2D 系のアノテーションは、2D モードでも上手く動くようにする。これまで通り x/y 軸方向の単位はミリメートルとするが、z 座標については「partialVolumeDescript 基準でのスライスのインデックス」に対応する 0 以上の整数とする。例えば partialVolumeDescriptor: { start: 10, end: 2, delta: -1 } の場合、z = 1 とはシリーズ番号 9 の画像を指す。

  • Ellipsis / Rectangle / Point
  • Ruler
  • PolyLine
  • ScrollBar