##DICOMデコーダーの作成
医療用画像を扱う上でDICOMは避けては通ることができない形式です.
近年はPythonなど様々な言語で使用可能なDICOMプラグインが登場しているし,そもそもDICOM→扱いやすい形式へ変換可能なソフトウェアはフリーでもたくさんある.なので,今更自力でDICOMを表示することに需要はないかもしれない・・・
DICOM規格は経年的にどんどん膨れ上がっており,その全体を理解することや,すべてを実装することは個人の力では到底できない.(少なくとも自分には)(なのでライブラリを使え,と言う話だが・・・)
そんな中でも,**DICOMを実装したい!!と考えている人がいるはず!**と期待し,少しでも誰かの役に立つといいなと思い,DICOMファイルのデコーダーの基礎部分を何回かに分けて実装したいと思います.(最後にいきなりズルしてますが)
##DICOMデータ
DICOMデータは1つのファイルに
- 患者名,患者ID,生年月日,年齢,性別など患者情報
- 検査方法(CT, レントゲン, エコー, カテーテル, MRI, ...),検査日,検査時間,使用した機器など検査に関わる情報
- 画像サイズ,画像Bit数, 動画かどうか,フレームレートなど画像のメタデータ
- 画像の転送形式(≒圧縮形式)
- 画像データの集合
など多くの情報が埋め込まれている.
DICOM構造の定義上は,あらゆるデータが想定されていて,
軍の階級,照会医師の住所など,そこに埋め込む必要あるのか?というようなデータ項目も定義されている.
JIRAにDICOM規格書の日本語訳があり,実装には非常に参考になるので目を通してほしい.
PS 3.6がDICOMで実装可能なデータセットの一覧だが,これをすべて実装するのは到底無理.
DICOM binary dataの中から,適宜扱う予定のデータに必要な部分だけ取り出して実装していけば良い.
DICOM dataの中の,途中の情報はすべてすっ飛ばして,画像形式・大きさ・深度情報だけを読み取って,ファイル最後尾にある,画像情報のbinaryだけを取り出せば画像は表示可能である.
しかし,本刊ではせっかくなので,途中に含まれるデータも階層だけは辿ってデコードしていくことにする.
##題材
DICOMの画像形式はいくつかあるのだが,
- 全くの非圧縮
- 可逆圧縮(Jpeg Lossless)
- Jpeg圧縮
- Jpeg2000圧縮
などがある.この中で,Jpeg2000はwindowsでは標準で表示できないようだが,macは表示可能(iOSは見てないのでわからない).
JPEG LosslessはDICOM以外ではほとんど使用されていないと思う.
ただし,一部メーカーのデジカメRAW形式はかなり近い.
JPEG Losslessのデコードも需要がありそうなら挑戦するが,今回はLossy JPEGと非圧縮を題材にデコードしていくことにする.
(Lossy JPEGは言ってしまえば普通のJPEGなので,DICOMファイルから該当部分のbinaryデータだけをとってきて,拡張子を.jpgにすればそれだけで表示できる)
####今回は下記サイトからDICOMデータを拝借することにする
####まずは,リンク先の一番上 DEMO 0002 1702Kb を使用する.
(これはカテーテル画像の動画になる予定です)
##DICOMデコーダー始動!!
まずはBinaryを開けてみる.
ここではバイナリエディタとして[0xED]を使用した.(WindowsならBzがおすすめ)
上記JIRAにあるDICOM規格書翻訳 PS 3.10 医療におけるデジタル画像と通信 (DICOM)第10部:媒体相互交換のための媒体保存とファイルフォーマットのp20にDICOMファイルの構造は
- 最初の128byteは基本0x00
- 続く4byteはプレフィックスでDICMと記載されている.
2.がDICOMファイルかどうかを決めているようです.
この0002.DCMはサンプルファイルなので,最初の128byteに色々書いてありますが気にしないでおきましょう.
AssetsにDICOMファイルである0002.DCMを登録した.
DICOMファイルはバイナリデータで扱っていくので,Dataで読み込んでおく
let fileName = "0002"
guard let dicomData = NSDataAsset(name: fileName)?.data else {
print("file not found")
return
}
dicomDataの128バイトは無視して,続く4byteにDICMと記載されているかを確認する.
Data型
は単純に取り出してもbyte数を吐いてしまいます.
各byteはUInt8で表現されますが,Data型のmapはデフォルトがUInt8
で処理してくれるので,
dicomData[128...131].map{$0}
とすれば
[68, 73, 67, 77]
となるはずです.
これは,ファイルアドレス 0x80〜0x83にある0x44, 0x49, 0x43, 0x4DがUInt8,つまり符号なし整数(10進法)で表示されたことになります.
これをasciiコードから文字列に直します.
68(0x44) = D
73(0x49) = I
67(0x43) = C
77(0x4D) = M
と出力したいわけです.
print(Unicode.Scalar(68)!)
-> D
のようにUnicode.Scalarで取り出してあげればいいわけです.
この変換を今後,山程使うので,
extension Data{
func getStringWithRange(start:Int, length:Int) -> String{
let aa = self[start...start+length-1]
return aa.map{String(Unicode.Scalar($0))}.joined()
}
}
として指定した場所と長さを指定すると文字が取り出せるよう,Data型を拡張しておきました.これで
print(dicomData.getStringWithRange(start: 128, length: 4))
のようにすれば,DICM
と出力されるようになります.
// dicomデータかチェック
// ヘッダは128バイトのファイルプリアンブル
// つづく4バイトにプレフィックスが続く
if dicomData.count < 132{
print("file size error")
return
}
if dicomData.getStringWithRange(start: 128, length: 4) != "DICM"{
print("file type error")
return
}
##DICOMデータの基本構造
DICOMファイルかどうかのチェックをしたあとのデータには,
- タグ
- (0x0002, 0x0000)のような(Group, Element)構造
- tagは規格書PS 3.6と照らし合わせて,何を示すかを確認しないとわからない
- VR (value representation)
- タグで示した場所に存在するデータの種類を表す
- アルファベット2文字で,この後ろに何が入っているかを表している
- 詳細はリンク参照
- 例えば,ASは年齢を表す文字列.DAは日付を表す文字列.USはunsigned integer 16 bits long(符号なし16ビット整数)が後ろに格納されていると明示している
- 値長さ
- 後ろに格納されているデータのByte数を示す
- 値領域
- 何が入っているかはVR次第だが,直前に示されたbyte数分だけデータが入っている
このように,
128byte〜DICM
132byte〜
Tag, (Group, Element), VR, Length, Data
Tag, (Group, Element), VR, Length, Data
Tag, (Group, Element), VR, Length, Data
...
Tag, (Group, Element), VR, Length, Data
Tag, (Group, Element), VR, Length, Data の繰り返し
です,
Tagは(0x1111, 0x2222)と4byte使って表され,続く2byteでVRを示し,続く2byteが格納されているデータのbyte数を示します.(一部特殊なVRがあり,それはlengthの示し方が異なりますが,今は無視)
先程のバイナリの例では,DICM
に続く02 00 00 00
がTagで(0002, 0000)
を示しています,Little endianで表示することに注意してください!!
続く2byteの55 4C
がasciiコードでUL
を示します.
ULはUInt32で符号なし32bit整数値を示している,と先程の規格書に書いてあるので,後ろには数字が入っています.
続く2byteは04 00
です.これは後ろに入っているデータ長を表すわけですから,後ろには4byteのUInt32が入っていることになります.
04 00
はLittle endianで読むので,0x400 = 1024
ではなく0x0004 = 4
と解釈してください
続く4byteを読んでみます
96 00 00 00
ですから150
です.
tag (0002, 0000)はファイルメタ情報の一部のようですので,デコードにはあまり関係ないことがわかります.
つづく,02 00 01 00
はtag (0002, 0010)を示していますが,これもメタデータで関係ありません.続いて,VR, length, data, tag, VR, length, data, ...と順々に読み取っていくことになります.
あるソフトで読み取ってみると以下の様になっていました.これを再現していきます.
##今日の最後に...
今日はいきなり長くなりました.
DICOMデータは膨大ですし,データ規格が鬼のようにあるので,そのごく一部を再現することになると思います.どんな検査データにも対応できるようなDICOMソフトは実装が非常に大変だと思います.
終わりが見えないのも嫌なので,ちょっと最後にズルをしておきましょう.
DICOMのtag情報から,このDICOMのImageDataはファイルアドレス 0x2B76から始まり,1枚目は17912byte格納されていることがわかりました.
let pix1 = dicomData[0x2B76...0x2B76+17912-1]
let img = NSImage(data: pix1)
imgView.image = img
どうですか?これは,このDICOMがLossy JPEGで圧縮されているので,imageのbinary dataさえ読めればかんたんに画像はデコードできます.これがJpeg losslessだとややこしいのですが・・・
ちょっと先読みでズルしましたが,次回はtag, VR, length, dataを読んでいくところを実装しようとおもいます!
それではまた!