0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-05-17

##タグごとの情報を読む
完全な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です.
該当部位のバイナリは図のとおりです.
image.png

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

String.trimmingCharacters(in: 
    CharacterSet.whitespacesAndNewlines.union(CharacterSet(["\0"])))

を使用します.バイナリの時点で0x000x20を消してしまっても良いです.

整数値と浮動小数点値のデコード

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

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

タグの内容

タグからデータの意味合いを解釈する

DICOM企画書 PS 3.6に各タグの意味合いが記載されています.
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 に置いておきます.

####データの読み出し

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とそのデータの解読が終わりました.
現時点ではただログに出力しているだけですが,この中から必要な情報を読み取って表示します.
次回は画像の表示まで実装しようと思います.

ではまた.

###参考
日本画像医療システム工業会
RuboMedical 本連載で題材にさせてもらっているDICOMデータ

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?