週刊 DICOMデコーダーを創る (4) データ解読

##タグごとの情報を読む
完全なDICOMデコーダーを目指す場合は全タグの情報を正しく処理する必要がありますが，表示であれば，各モダリティ毎に必須となっているtagがあるので，必要な情報を読む事でとりあえず表示は可能です．

今回作成するデコーダーでは，とりあえず表示できることを目指しますが，練習ですから一応前データを読むだけ読んでから，必要な情報を収集することにします．

####代表的なタグの例
| tag  | 情報種類  |   
|:-:|:-:|
| 0008, xxxx  | 画像情報  |   
| 0010, xxxx  | 患者情報  |   
| 0018, xxxx  | 画像収集情報  |   
| 0020, xxxx  | 画像付帯情報  |   
| 0028, xxxx  | 画像表示情報  |   
| 7FE0, 0010  | 画素データ  |   

| tag  | 情報種類  |  VR |
|:-:|:-:|:-:|
| 0010, 0010  | 患者氏名  | PN  |
| 0010, 0020  | 患者ID  | LO  |
| 0010, 0030  | 生年月日  | DA  |

#### 文字列のデコード
DICOMでは，**データ長は偶数バイトである**という制約があります．
そのため，奇数文字や一部のデータは，指定した偶数のデータ長を埋めるようにゼロパディングやNULLパディングが行われます．

これまでにasciiから文字列へ変換する`readChar`関数を実装していますが，実際のデータの読み取りでは，図に示したような前方後方の空白文字`0x20`やNULL文字`0x00`をトリムする必要があります．
例えば，`0x880(2176) tag: (0010, 0010)`は`Patient's Name`で，データ長さは12bytesです．
該当部位のバイナリは図のとおりです．
<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1465632/62a638d2-2c2f-45cc-1696-621231080427.png" alt="image.png">

`String.trimingCharacters(in: .whitespacesAndNewlines)`
ではNULL文字が消せなかったので，

```swift
String.trimmingCharacters(in: 
    CharacterSet.whitespacesAndNewlines.union(CharacterSet(["\0"])))
```
を使用します．バイナリの時点で`0x00`や`0x20`を消してしまっても良いです．

#### 整数値と浮動小数点値のデコード
整数値UInt32やUInt16は，前回までにビットシフトを利用してデコードしました．
今回は32bit浮動小数点値 Float32をデコードする必要が生じました．
ちょっと統一性がありませんが，ここはポインタを使用します．

```swift
let data = dicomData[currentPosition ... currentPosition+3].map{$0}
let value = data.withUnsafeBytes{$0.load(as: Float32.self)}
```

## タグの内容
#### タグからデータの意味合いを解釈する
DICOM企画書 PS 3.6に各タグの意味合いが記載されています．
<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1465632/cb82bffb-1b0b-d4fa-efa7-1752e16023a3.png" alt="image.png">

これを見ると，例えば`(0028, 0008)`は`Number of Frames`を示していることがわかります．
本連載で取りあげているサンプルのDICOMデータでは33という値になっているので，このデータには33枚の画像データが入っていることがわかります．
そこで，どのタイミングで次の画像を示すのか，は`(0028, 0009)`で`Frame Increment Pointer`に記載されています．
ここの値をバイナリで確認すると，`18 00 63 10`です．よって，`(0018, 1063)`のタグにフレーム増分タイミングの記載があることになります．
`(0018, 1063)`は`Frame Time`で，`33`というデータになっています．
よって，このDICOMデータでは，33枚の画像を，フレームレート33で表示すればいいことになります．

各タグには意味があるので，このように辞書と照らし合わせて解読します．
なお，必ずしもフレームレートが記載されているわけではなく，各画像毎に何秒表示するか，の配列が指定されたりしていることもあります．

#### タグとVR
先程の辞書を見ると，各タグにVRが指定されていることに気がつくと思います．
実は，各タグに対応する値には意味がありますから，自ずとVRは定まるわけです．
ですので，DICOMではVRの記載は必須ではありません．
しかし，現状では多くのDICOMデータはVRを記載してくれています．
企画書によれば，デコーダーはVRあり・VRなし，どちらのデータでも対応できるようにすべき，と記載されています．

煩雑になるので，本デコーダーではVRが記載されているデータを前提に進めていきます．

##実装
#### tagとデータ内容の対応
これまでのログに加えて，次のようにTagの意味合い(図ではPatient's Name)を表示し，
さらに，データ内容（図ではRubo DEMO）を表示するようにします．
上で述べたように，文字列が入っている場合は，実際に格納されている文字データはデータ長よりも短い場合がありますから，空白やNULL文字を除いて出力し，本当の文字数を記載するようにします．

```
0x880(2176) tag: (0010, 0010) VR: PN, Length: 12, Patient's Name Rubo DEMO(9)
...
0xA18(2584) tag: (0028, 0010) VR: US, Length: 2, Rows 512
0xA22(2594) tag: (0028, 0011) VR: US, Length: 2, Columns 512
```

#### tagとデータ内容の対応
```tag_database.swift
let tagDictionary:[UInt32 : String]=[
    0x00020000 : "Group Length",
    0x00020001 : "File Meta Information Version",
    0x00020002 : "Media Storage SOP Cl",
...(略)...
    0xfffee000 : "Item",
    0xfffee00d : "Item Delimitation Item",
    0xfffee0dd : "Sequence Delimitation Item"]
```
のように，PS3.6の辞書からdictionaryを作成します．
ファイル全体は [tag_database.swift](https://github.com/STrizzle-Naoki/experiments/blob/main/tag_database.swift) に置いておきます．

####データの読み出し
```DicomData.swift
    func readData(hierarchy: Int, position: Int, group: UInt16, element:UInt16, vr:String, length:Int) -> Any?  {
        let group_element:UInt32 =  UInt32(group) << 16 + UInt32(element)
        
        let description: String = tagDictionary[group_element] != nil ? tagDictionary[group_element]! : ""
        
        if length == 0xFFFFFFFF{
            print("\(String(repeating: " ", count: hierarchy))0x\(String(position, radix: 16).uppercased())(\(position))\ttag: (\(String(format: "%04x", group)), \(String(format: "%04x", element)))\tVR: \(vr), Length: ", terminator: "")
        }else{
            print("\(String(repeating: " ", count: hierarchy))0x\(String(position, radix: 16).uppercased())(\(position))\ttag: (\(String(format: "%04x", group)), \(String(format: "%04x", element)))\tVR: \(vr), Length: \(length), \(description) ", terminator: "")
        }
        
        if length == 0{
            print("")
            return nil
        }
        
        switch vr {
        case "CS", "DA", "TM", "UI", "SH", "LO", "ST", "PN", "DS", "IS":
            let value = dicomData[currentPosition...currentPosition+length-1].map{String(Unicode.Scalar($0))}.joined().trimmingCharacters(in: CharacterSet.whitespacesAndNewlines.union(CharacterSet(["\0"])))
            print(value + "(\(value.count))")
            return value
            
        case "UL":
            let data = dicomData[currentPosition ... currentPosition+3].map{$0}
            //            let value = data.withUnsafeBytes{$0.load(as: UInt32.self)}
            let value = UInt32(UInt16(data[3])) << 24 + UInt32(UInt16(data[2])) << 16 + UInt32(UInt16(data[1])) << 8 + UInt32(UInt16(data[0]))
            print(value)
            return value
            
        case "US":
            let data = dicomData[currentPosition ... currentPosition+1].map{$0}
            let value = UInt16(data[1]) << 8 + UInt16(data[0])
            print(value)
            return value
            
        case "FL":
            let data = dicomData[currentPosition ... currentPosition+3].map{$0}
            let value = data.withUnsafeBytes{$0.load(as: Float32.self)}
            print(value)
            return value
            
        case "AT":
            let data = dicomData[currentPosition ... currentPosition+3].map{$0}
            let value = UInt32(UInt16(data[1])) << 24 + UInt32(UInt16(data[0])) << 16 + UInt32(UInt16(data[2])) << 8 + UInt32(UInt16(data[3]))
            print(String(format: "%08x", value))
            return value
            
        case "OB", "OW":
            if length == 0xFFFFFFFF{
                print("")
                return nil
            }else{
                
                let value = dicomData[currentPosition ... currentPosition+length-1].map{$0}
                print(value)
                return value
            }
            
        case "SQ":
            print("")
            return nil
            
        default:
            return nil
        }
    }
```

各VRに対応するデータを読み取ります．
VR毎に意味合いと使い分けが本来はありますが，結局は文字列の集合であったりするので，
まとめられるものは全てまとめて同じ読み取り方法でOKです．

全VRを実装したわけではありませんが，このサンプルDICOMで必要な分は実装しました．

##終わりに
ようやくtagとそのデータの解読が終わりました．
現時点ではただログに出力しているだけですが，この中から必要な情報を読み取って表示します．
次回は画像の表示まで実装しようと思います．

ではまた．


###参考
[日本画像医療システム工業会](https://www.jira-net.or.jp/dicom/dicom_data_02_01.html)
[RuboMedical](http://www.rubomedical.com/dicom_files/) 本連載で題材にさせてもらっているDICOMデータ