##本日はtag, VR, lengthを読み取ってみる
需要の程は不明ですが,知識の整理も兼ねて今日も作っていきます.
今日はtag, VR, 値長さを読み取るところまでやってみます.
前回はViewController.swiftにまとめていましたが,本格的に読み取り始めることにするので,新たにDicomDataクラスを作成します.
class DicomData{
var dicomData : Data!
init(withData data:Data) {
dicomData = data
// 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
}
print("dicom file loaded")
}
}
##データ要素の読み取り
###VR一覧
PS3.5 p23に一覧が記載されています.
各VRには対応するデータ内容がどのような値型で格納されているかを示しています.
DICOM規格ではかなり厳密に区別されていますが,実際には値として入っているのは
数字・文字列・バイト列(バイナリ)だけです.
VR | 内容 | 備考 |
---|---|---|
AE | 文字列 | |
AS | 年齢を表す文字列 | |
AT | tag, elementを表す16bit符号なし整数 | |
CS | 文字列 | |
DA | 日付を表す文字列 | |
DS | 固定小数点か浮動小数点を表す文字列 | |
DT | 日時を示す文字列 | |
FL | 32bit浮動小数点数 | |
FD | 64bit浮動小数点数 | |
IS | 整数列 整数を表す文字列 | |
LO | 文字列 | |
LT | 文字列 | |
OB | byte列 ここにdataが入る | * |
OF | 浮動小数点ワードの列 | * |
OW | その他のワード列 dataが入る | * |
PN | 人名が入る文字列 | |
SH | 文字列 | |
SL | 32bit符号付き整数 | |
SQ | 項目のシーケンス | * |
SS | 16bit符号付き整数 | |
ST | 文字列 | |
TM | 時間を表す文字列 | |
UI | 文字列 | |
UL | 32bit符号なし整数 | |
UN | 内容が不明のバイト列 | * |
US | 16bit符号なし整数 | |
UT | 文字列 | * |
各データ要素は,
| group | element | VR | 値長さ | 値
|:-:|:-:|:-:|:-:|:-:|:-:|
|1byte|1byte|2byte|2byte = UInt16|値長さで指定された分だけ|
で構成されているが,*をつけたOB, OW, OF, SQ, UT, UNは
| group | element | VR | 予約領域(使用しない)| 値長さ | 値
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|1byte|1byte|2byte|2byte|4byte = UInt32|値長さで指定された分だけ|
で構成される点に注意が必要です.
VRに続く2byteに値長さは格納されておらず,2byteあけて4byte分使って値が格納されています.
これはdicomの各tagに対応するデータは日時であったり人名であったり,基本的に小さなデータを格納しているが,例えばOBやOWには16bit高解像度CTの画素値が入ったりするので,その容量を示すためには2byte長で表すことができる最大数(=16bit符号なし整数 max 65,535)となるので,データとして60KB程度のものしか格納できなくなります.
そのため,OBやOWなど画素を収納する部分は4byteでデータ容量を表すことができ,4,294,967,295byte = 4GB程度のデータが格納可能となります.
##関数作成
新たにDicomDataクラスを作ったので,そちらに各読み取り関数を実装していきます.
今回実装するのは,
- 指定した長さ分のバイト列→文字列へasciiテーブルで変換するクラス
- 16bit符号なし整数
- 32bit符号なし整数
- 現在読み取りしているファイルアドレスを移動させるseek関数
です.
var currentPosition : Int = 0
func readUInt16() -> UInt16{
let data = dicomData[currentPosition ... currentPosition+1].map{$0}
currentPosition += 2
return UInt16(data[1]) << 8 + UInt16(data[0])
}
func readUInt32() -> UInt32{
let data = dicomData[currentPosition ... currentPosition+3].map{$0}
currentPosition += 4
return UInt32(UInt16(data[3])) << 24 + UInt32(UInt16(data[2])) << 16 + UInt32(UInt16(data[1])) << 8 + UInt32(UInt16(data[0]))
}
func readChar(length:Int) -> String{
let pos = currentPosition
currentPosition += 2
return dicomData[pos...pos+length-1].map{String(Unicode.Scalar($0))}.joined()
}
func seek(offset : Int){
currentPosition += offset
}
readChar
関数は前回と同様にString(Unicode.Scalar($0))
で実装します.
readUInt16 readUInt32
はWindowsの.NET frameworkには存在するのですが,swiftでは見当たりませんでした(間違っていたらすいません).
bit演算のシフトを用いて実装します.
例えば
16 A2 15 DA
のバイナリが順に並んでいたとき,little endianであれば4byteとったときに
DA 15 A2 16 = 3658850838
と読み取る必要があります
それぞれをbitで表すと
DA(=218) 1101 1011
15(= 21) 0001 0101
A2(=162) 1010 0010
16(= 22) 0001 0110
です.
これを左にそれぞれ24bit, 16bit, 8bitずらすことで
1101 1011 0000 0000 0000 0000 0000 0000
0000 0000 0001 0101 0000 0000 0000 0000
0000 0000 0000 0000 1010 0010 0000 0000
0000 0000 0000 0000 0000 0000 0001 0110
として,これらを足し合わせて(bit演算の四則演算は癖があるので確認してください)
(今回は単純に + でいいですが)
1101 1010 0001 0101 1010 0010 0001 0110 = 0xDA15A216
とすることで得られます.
##読み取り
func analyzeData(){
currentPosition = 128 + 4
while currentPosition <= 1950 {
let position = currentPosition
let group = readUInt16()
let element = readUInt16()
let vr = readChar(length: 2)
if ["OB", "OW", "OF", "SQ", "UT"].contains(vr){
// VRの続きの2byteは意味をなさないので飛ばす
currentPosition += 2
let length = readUInt32()
print("Address: 0x\(String(position, radix: 16))(\(position)), tag: (\(String(format: "%04x", group)), \(String(format: "%04x", element))), VR: \(vr), Length: \(length)")
currentPosition += Int(length)
}else{
let length = readUInt16()
print("Address: 0x\(String(position, radix: 16))(\(position)), tag: (\(String(format: "%04x", group)), \(String(format: "%04x", element))), VR: \(vr), Length: \(length)")
currentPosition += Int( length)
}
}
}
今回は試しに1950バイト目まで読み取ってみます
格納されたデータの内容は読み飛ばして,tag, VR, lengthのみ読んでいます
出力は
Address: 0x84(132), tag: (0002, 0000), VR: UL, Length: 4
Address: 0x90(144), tag: (0002, 0001), VR: OB, Length: 2
Address: 0x9e(158), tag: (0002, 0002), VR: UI, Length: 28
Address: 0xc2(194), tag: (0002, 0003), VR: UI, Length: 46
Address: 0xf8(248), tag: (0002, 0010), VR: UI, Length: 22
Address: 0x116(278), tag: (0002, 0012), VR: UI, Length: 8
Address: 0x126(294), tag: (0008, 0008), VR: CS, Length: 38
Address: 0x154(340), tag: (0008, 0016), VR: UI, Length: 28
Address: 0x178(376), tag: (0008, 0018), VR: UI, Length: 46
Address: 0x1ae(430), tag: (0008, 0020), VR: DA, Length: 8
Address: 0x1be(446), tag: (0008, 0030), VR: TM, Length: 6
Address: 0x1cc(460), tag: (0008, 0050), VR: SH, Length: 0
Address: 0x1d4(468), tag: (0008, 0060), VR: CS, Length: 2
Address: 0x1de(478), tag: (0008, 0070), VR: LO, Length: 0
Address: 0x1e6(486), tag: (0008, 0080), VR: LO, Length: 0
Address: 0x1ee(494), tag: (0008, 0081), VR: ST, Length: 0
Address: 0x1f6(502), tag: (0008, 0090), VR: PN, Length: 0
Address: 0x1fe(510), tag: (0008, 1030), VR: LO, Length: 0
Address: 0x206(518), tag: (0008, 1050), VR: PN, Length: 0
Address: 0x20e(526), tag: (0008, 2110), VR: CS, Length: 2
Address: 0x218(536), tag: (0008, 2112), VR: SQ, Length: 4294967295
です.
先程示したとおり,VRがOB, OW, OF, SQ, UTは挙動が違うので分けています.
現時点ではアドレスとVR,データ長が表示されているだけで,各tagが何を示すのかはわかりませんが,これは対応表があります.
またそのうち実装しますが,今見えている中で大事なのは
(0002, 0010)のUIが入っているデータで,UIなので文字列が入っています.
内容は,1.2.840.10008.1.2.4.50という文字列で,これも対応表がありますが,
JPEG Baseline (Process 1) で圧縮転送していることを示しています.
ここを読み取って,どのような方法で最終の画像データをデコードするのかを決めることになります.
データ | 転送形式 |
---|---|
1.2.840.10008.1.2 | Implicit VR Little Endian Default Transfer Syntax |
1.2.840.10008.1.2.1 | Explicit VR Little Endian Transfer Syntax |
1.2.840.10008.1.2.1.99 | Deflated Explicit VR Little Endian |
1.2.840.10008.1.2.2 | Explicit VR Big Endian |
1.2.840.10008.1.2.4.50 | JPEG Baseline (Process 1) |
1.2.840.10008.1.2.4.70 | JPEG Lossless, Non-Hierarchical, First-Order Prediction (Process 14 [Selection Value 1]) |
1.2.840.10008.1.2.4.80 | JPEG-LS Lossless Image Compression |
1.2.840.10008.1.2.4.81 | JPEG-LS Lossy (Near-Lossless) Image Compression |
1.2.840.10008.1.2.4.90 | JPEG 2000 Image Compression (Lossless Only) |
1.2.840.10008.1.2.4.91 | JPEG 2000 Image Compression |
1.2.840.10008.1.2.4.100 | MPEG2 Main Profile |
1.2.840.10008.1.2.5 | RLE Lossless |
本来はすべての伝送方法に対応してデコードできるようにする必要がありますが,
現時点では上記から一部を実装する予定です.
今回のログですが,最初のほうは順調に読めていますが,最後のVRがSQの部分で変になってしまっています.
このSQが曲者で,正しく実装するのはなかなか難しそうなのですが,データを階層化して入れ子のようにまとめるためのVRです.
今後どのように実装していくかはまだ悩み中ですが,次回はSQを関して実装していこうと思います.
ではまた.