Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What are the problem?

週刊 DICOMデコーダーを創る (1) 創刊号

DICOMデコーダーの作成

医療用画像を扱う上でDICOMは避けては通ることができない形式です.
近年はPythonなど様々な言語で使用可能なDICOMプラグインが登場しているし,そもそもDICOM→扱いやすい形式へ変換可能なソフトウェアはフリーでもたくさんある.なので,今更自力でDICOMを表示することに需要はないかもしれない・・・

DICOM規格は経年的にどんどん膨れ上がっており,その全体を理解することや,すべてを実装することは個人の力では到底できない.(少なくとも自分には)(なのでライブラリを使え,と言う話だが・・・)
そんな中でも,DICOMを実装したい!!と考えている人がいるはず!と期待し,少しでも誰かの役に立つといいなと思い,DICOMファイルのデコーダーの基礎部分を何回かに分けて実装したいと思います.(最後にいきなりズルしてますが)

DICOMデータ

DICOMデータは1つのファイルに

  1. 患者名,患者ID,生年月日,年齢,性別など患者情報
  2. 検査方法(CT, レントゲン, エコー, カテーテル, MRI, ...),検査日,検査時間,使用した機器など検査に関わる情報
  3. 画像サイズ,画像Bit数, 動画かどうか,フレームレートなど画像のメタデータ
  4. 画像の転送形式(≒圧縮形式)
  5. 画像データの集合

など多くの情報が埋め込まれている.
DICOM構造の定義上は,あらゆるデータが想定されていて,
軍の階級,照会医師の住所など,そこに埋め込む必要あるのか?というようなデータ項目も定義されている.
JIRAにDICOM規格書の日本語訳があり,実装には非常に参考になるので目を通してほしい.

PS 3.6がDICOMで実装可能なデータセットの一覧だが,これをすべて実装するのは到底無理.
DICOM binary dataの中から,適宜扱う予定のデータに必要な部分だけ取り出して実装していけば良い.

DICOM dataの中の,途中の情報はすべてすっ飛ばして,画像形式・大きさ・深度情報だけを読み取って,ファイル最後尾にある,画像情報のbinaryだけを取り出せば画像は表示可能である.

image.png
雑で申し訳ない・・・・

しかし,本刊ではせっかくなので,途中に含まれるデータも階層だけは辿ってデコードしていくことにする.

題材

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がおすすめ)
スクリーンショット 2021-05-11 2.00.59.png

上記JIRAにあるDICOM規格書翻訳 PS 3.10 医療におけるデジタル画像と通信 (DICOM)第10部:媒体相互交換のための媒体保存とファイルフォーマットのp20にDICOMファイルの構造は

  1. 最初の128byteは基本0x00
  2. 続く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数分だけデータが入っている

image.png

このように,

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, ...と順々に読み取っていくことになります.

あるソフトで読み取ってみると以下の様になっていました.これを再現していきます.
image.png

今日の最後に...

今日はいきなり長くなりました.
DICOMデータは膨大ですし,データ規格が鬼のようにあるので,そのごく一部を再現することになると思います.どんな検査データにも対応できるようなDICOMソフトは実装が非常に大変だと思います.

終わりが見えないのも嫌なので,ちょっと最後にズルをしておきましょう.
DICOMのtag情報から,このDICOMのImageDataはファイルアドレス 0x2B76から始まり,1枚目は17912byte格納されていることがわかりました.

        let pix1 = dicomData[0x2B76...0x2B76+17912-1]
        let img = NSImage(data: pix1)
        imgView.image = img

image.png

どうですか?これは,このDICOMがLossy JPEGで圧縮されているので,imageのbinary dataさえ読めればかんたんに画像はデコードできます.これがJpeg losslessだとややこしいのですが・・・

ちょっと先読みでズルしましたが,次回はtag, VR, length, dataを読んでいくところを実装しようとおもいます!

それではまた!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
2
Help us understand the problem. What are the problem?