##タグごとの情報を読む
完全な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です.
該当部位のバイナリは図のとおりです.
String.trimingCharacters(in: .whitespacesAndNewlines)
ではNULL文字が消せなかったので,
String.trimmingCharacters(in:
CharacterSet.whitespacesAndNewlines.union(CharacterSet(["\0"])))
を使用します.バイナリの時点で0x00
や0x20
を消してしまっても良いです.
整数値と浮動小数点値のデコード
整数値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に各タグの意味合いが記載されています.
これを見ると,例えば(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とデータ内容の対応
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 に置いておきます.
####データの読み出し
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データ