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デコーダーを創る (2)

Posted at

##本日はtag, VR, lengthを読み取ってみる
需要の程は不明ですが,知識の整理も兼ねて今日も作っていきます.

今日はtag, VR, 値長さを読み取るところまでやってみます.
前回はViewController.swiftにまとめていましたが,本格的に読み取り始めることにするので,新たにDicomDataクラスを作成します.

DicomData.swift
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関数

です.

DicomData.swift
    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を関して実装していこうと思います.

ではまた.

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?