DICOM XAという深遠にして難解なる世界への手がかり
DICOM-XAという動画フォーマットは、医療画像診断分野において1997年頃より全世界の医療現場・研究において用いられてきたものです。このDICOMフォーマットの制定により、全世界で統一されたフォーマットで画像情報を正確に伝達・保存が可能となったのです。しかしながら、その動画フォーマットであるDICOM-XAというものは、難解なデータ構造を持っています。
齋藤 滋は、2010年頃に、C/C++プログラミングの勉強のために自身でその動画再生プログラムを作成することを決意しました。しかし、それは困難と苦痛に満ちた試みでした。DICOM-XAは静止画のフォーマットである JPEGに基づいています。そして JPEGというフォーマットの根源にあるのは、ハフマン符号化というデータ欠損の無い圧縮です。なぜ、DICOM-XAが難解であるか? と問われれば、その多くは、肝腎のHuffmannフォーマットに関する明確な記述が JPEG仕様書にしか存在しないからであり、その JPEG独特の符号化をプログラミング言語で記載したものの解説は世の中にほとんど存在しないからでした。このような荒波の中、DICOM-XAの解読プログラム(=動画再生プログラム)を自分自身でフル・スクラッチで書くことに私は挑戦しまた。ここに、この齋藤 滋自身が歩んできた困難に満ちた長い戦いの記録を残します。
まだ道は遠い
2013年7月30日
ようやく絵が出せるようになった DICOM-XA Viewerですが まだまだバグがありそうです
片っ端から、XA fileを開き、またコマを変えたりして見ているのですが 既にこんな異常が見つかりました
次に掲げてあるのが 001013XAというシーンの XA fileです このシーンは短く5コマしかありませんが、その中の 3コマ目、4コマ目、5コマ目を並べると以下のようになったのですが、これはあくまでも僕の Viewerのバグです 問題はバグがどこにあるのか現時点では皆目見当がつかない、ということです それに どうやってバグ潰しをすればいいのか? それも見当がつかないということです いやもっと大きな問題は自由に思索する時間が無い、ということです 仕事に追われています
やっと解決
2013年8月3日
DICOM XA Viewerが何とか走るようになりそうです 前日解決できなかった、あるフレームでは絵がビット崩れする現象の原因が特定できたのです
XA dataからBit入力して、Huffman codeを用いて暗号解読していく訳ですが、肝腎のHuffman codeが実はフレーム毎に変化していたのです いや、大体は変化せず、最初のフレームのHuffman codeがそのまま用いられるのですが、時に変化するのです だから、DHTからHuffman codeを解読せねばならないのです
その部分を書き直したところ下記のように先日ビット崩れしていたフレームもきちんと解読することに成功しました
あとはタイマー割り込みを実装して時間と共にフレームを書き換えるだけです
それはかつてブロック崩しを作成した時に実装した経験があるので簡単な筈なのですが・・・
実はすっかり忘れてしまい、今はどうやるやら全く判らなくなっているのです でもまあできるでしょう
現在の BitStreamクラス
2013年8月5日
現在までにBitStream入力クラスの一部にバグが見つかり修正しています。ここでは高村先生に随分とお世話になりました。まずは header部分です
#ifndef __CRBitSTREAM_H_ #define __CRBitSTREAM_H_ #define TAG (0xFF) // JPEG Tag Marker #define DHT (0xC4) // DHT: 0xFFC4 -- Define Huffman Table #define SOI (0xD8) // SOI; 0xFFD8 -- Start of Image #define EOI (0xD9) // EOI; 0xFFD9 -- End of Image #define SOF (0xC3) // SOF; 0xFFC3 -- Start of Frame (Lossless) #define SOS (0xDA) // SOS; 0xFFDA -- Start of Scan class CRBitStream { private: struct JPEGTAGTBL{ // JPEG TAGのアドレス格納 BYTE *tagDHT; BYTE *tagSOI; BYTE *tagEOI; BYTE *tagSOF; BYTE *tagSOS; }; struct HUFFTAB { int HuffBit; // ハフマン符号のビット長 int HuffCode; // ハフマン符号 int HuffVal; // ハフマン符号の意味する値 }; BYTE *fileTop; BYTE *fileEnd; long fileSize; BYTE BITS[16]; // DHTのビット数を入れる配列 int totalFrameCount; // 総フレーム数 int huffElementNo; // ハフマン符号の数 int currentFrame; // 現在のフレーム番号 BYTE *frameTop; // フレーム先頭アドレス BYTE *frameEnd; // フレーム終了アドレス long frameSize; // フレームの長さ BYTE *mRead; // 読み出しアドレス BYTE mMask; // bit maskであると同時に現在の読み出しビット位置 (MSB = 7, LSB = 0) bool mReadable; // 1: 読み出し可、 0: 読み出し不可 void IncBuf(void); // 読み出しアドレスのインクリメントとアクセス違反のチェック void SearchJpegTag(); // JPEG tagをサーチしてそのアドレスをjpegTagTbl[]に格納する void MakeHuffTab(void); // ハフマン表を作る public: CRBitStream(void); CRBitStream(CFile *); ~CRBitStream(void); JPEGTAGTBL *jpegTagTbl; // JPEG Tagを格納するテーブル HUFFTAB *huffTab; // ハフマン表 BYTE *GetFileTop(void); int SetFrame(int); // フレーム(何コマ目?)をセットする 成功 = フレーム、失敗 = -1 int GetTotalFrame(void); // 総フレーム数を返す BYTE GetBYTE(void); // 1 BYTE読み出し WORD GetWORD(void); // 1 WORD読み出し int GetBit(void); // 1 bit読みだして返す int GetBits(int); // numOfBits数のビットを読みだして返す BYTE *GetAddressPresent(void); // bufferの現在のアドレスを返す void ResetBuffer(void); // bufferの先頭に戻す bufferそのものは変更しない bool IsReadable(void); // まだ読み出し可能か否か? int DecodeHuffman(void); // ハフマン符号の解読 int DecodeHuffmanValue(int); // ハフマン符号から実際の値に変換する }; #endif
そして実装ファイルです
#include "StdAfx.h" #include "RBitStream.h" #include <iostream> CRBitStream::CRBitStream(void) { } CRBitStream::CRBitStream(CFile *pFile) :fileTop(NULL), fileEnd(NULL), frameTop(NULL), frameEnd(NULL), mRead(NULL), jpegTagTbl(NULL), huffTab(NULL), currentFrame(0) { mReadable = true; fileSize = static_cast<long>(pFile->GetLength()); fileTop = new BYTE[fileSize]; pFile->SeekToBegin(); pFile->Read(fileTop, fileSize); // これでファイル全体ががメモリ(*fileTop)に取り込まれた fileEnd = fileTop + fileSize; SearchJpegTag(); // 既にJPEG Tagが検出され、フレーム毎に分割された } CRBitStream::~CRBitStream(void) { delete[] fileTop; delete[] huffTab; delete[] jpegTagTbl; } void CRBitStream::SearchJpegTag(void) { // JPEG Tagを探索し、そのアドレスをjpegTagTblに格納する BYTE *memPointer = fileTop; BYTE *tempPointer = fileTop; totalFrameCount = 0; while (memPointer < fileEnd-1) { // フレーム数をカウント if (*memPointer == TAG) { memPointer++; if (*memPointer == SOI) { totalFrameCount++; } } memPointer++; } jpegTagTbl = new JPEGTAGTBL[totalFrameCount + 1]; // これでJPEG Tagを格納する動的配列確保 // sentinel用に一つ余分に確保 int frames = 0; memPointer = fileTop; while (memPointer < fileEnd) { if (*memPointer == TAG) { memPointer++; if (*memPointer == SOI) { jpegTagTbl[frames].tagSOI = memPointer; // SOIアドレス格納した frames++; } } memPointer++; } jpegTagTbl[totalFrameCount].tagSOI = fileEnd; // sentinelとしてfileEndを代入した frames = 0; while (frames < totalFrameCount) { memPointer = jpegTagTbl[frames].tagSOI; while (memPointer < jpegTagTbl[frames + 1].tagSOI) { if (*memPointer == TAG) { tempPointer = memPointer + 1; if (*tempPointer == DHT) { jpegTagTbl[frames].tagDHT = tempPointer; // DHT(Define Huffman Table)アドレス格納 } else if (*tempPointer == SOF) { jpegTagTbl[frames].tagSOF = tempPointer; // SOF3(Start of Frame Losless)アドレス格納 } else if (*tempPointer == SOS) { jpegTagTbl[frames].tagSOS = tempPointer; // SOS(Start of Scan)アドレス格納 } else if (*tempPointer == EOI) { jpegTagTbl[frames].tagEOI = tempPointer; // EOI(End of Image)アドレス格納 } } memPointer++; } frames++; } } void CRBitStream::IncBuf(void) { // buffer読み出しを1 BYTE進める if (++mRead >= frameEnd) { mReadable = false; // 次は読み出し不可にする } else { if (*mRead == TAG) { // 0x00をカラ送りすると共に最後のチェック if (++mRead >= frameEnd) mReadable = false; *mRead = TAG; //taka: 次に0xffを返さなければならないので便宜的に書き換える } } } void CRBitStream::MakeHuffTab(void) { // ハフマン表を作る huffElementNo = 0; // ハフマン符号の個数 BYTE *memory = jpegTagTbl[currentFrame].tagDHT; // 現在のフレームのDHTアドレス memory++; int dhtLength = *memory*256 + memory[1]; //taka, 2バイト整数 memory+=2; memory++; // DHTの長さとDC成分バイトを読み飛ばし for (int i=0; i<16; i++) { BITS[i] = *memory; huffElementNo += *memory++; } huffTab = new HUFFTAB[huffElementNo]; for (int i=0; i < dhtLength-3-16; i++) { huffTab[i].HuffVal = *memory++; } // ハフマン符号の生成 int code = 0; // ハフマン符号初期値は0である int huffElement = 0; // 配列のカウンター for (int i=0; i < 16; i++) { // これでBitS[]配列全体を走査する for (int j=0; j < BITS[i]; j++) { huffTab[huffElement].HuffBit = i+1; huffTab[huffElement].HuffCode = code; huffElement++; code+=1; // 次のハフマン符号のために1 bit足す } code <<= 1; // 次のビット数のハフマン符号のために、左に1 bitシフトする } } BYTE *CRBitStream::GetFileTop(void) { return fileTop; } int CRBitStream::SetFrame(int frame) { // フレーム(何コマ目?)をセットする 成功 = フレーム、失敗 = -1 if (frame < 0) frame = 0; if (frame > (totalFrameCount - 1)) frame = totalFrameCount - 1; currentFrame = frame; frameTop = jpegTagTbl[frame].tagSOS + 9; frameEnd = jpegTagTbl[frame].tagEOI; mRead = frameTop; mMask = static_cast<BYTE>(0x80); // setMSB mReadable = true; // アクセスエラー無し MakeHuffTab(); // ハフマン符号表も完成した return frame; } int CRBitStream::GetTotalFrame(void) { // 総フレーム数を返す return totalFrameCount; } BYTE CRBitStream::GetBYTE(void) { // 1 BYTE読み出し if (mReadable) { if (!(mMask & 0x80)) { // BYTE途中は不可 IncBuf(); // 次のBYTEに進める mMask = static_cast<BYTE>(0x80); } BYTE r = *mRead; // 実際に1 BYTE読みだす IncBuf(); return r; } else { return 0; // 読み出し不可ならば作用の無い 0を返す } } WORD CRBitStream::GetWORD(void) { // 1 WORD読み出し if (mReadable) { if (!(mMask & 0x80)) { // BYTE途中は不可 IncBuf(); mMask = static_cast<BYTE>(0x80); } WORD r = (static_cast<WORD>(*mRead))<<8; // 1 BYTE読み出しそれをWORDにcastしてから8bits左シフト IncBuf(); r |= static_cast<WORD>(*mRead); // さらに1 BYTEを下のBYTEに付加する IncBuf(); return r; } else { return 0; // 読み出し不可ならば作用の無い 0を返す } } int CRBitStream::GetBit(void) { // 1 bit読みだす if (mReadable == true) { int r; r = (*mRead & mMask) ? 1:0; mMask >>= 1; if (mMask == 0x00) { mMask = static_cast<BYTE>(0x80); IncBuf(); } return r; } else { return -1; // 読み出し不可ならば -1を返す } } int CRBitStream::GetBits(int numOfBits) { // numOfBits読みだす (0 < n <= 16) if (numOfBits == 0) return 0; if ((numOfBits < 0)||(numOfBits > 16)) { return -1; // エラー 読み出し不可 //taka:-1のほうがよいと思います } if (!mReadable) { return -1; // 読み出し不可ならば 0を返す//taka:-1のほうがよいと思います } int r = 0; // 追記: if (mMask == 0x00)判定を最初にしていたことがエラーであり修正した while (numOfBits) { if (mReadable) { r <<= 1; r |= ((*mRead & mMask) ? 1: 0); mMask >>= 1; numOfBits--; if (mMask == 0x00) { mMask = static_cast<BYTE>(0x80); IncBuf(); } } else { return -1; // 読み出し不可ならば -1を返す } } return r; } BYTE *CRBitStream::GetAddressPresent(void) { return mRead; } void CRBitStream::ResetBuffer(void) { frameTop = jpegTagTbl[currentFrame].tagSOS + 9; frameEnd = jpegTagTbl[currentFrame].tagEOI; mRead = frameTop; mMask = static_cast<BYTE>(0x80); // setMSB mReadable = true; // アクセスエラー無し } bool CRBitStream::IsReadable(void) { return mReadable; } int CRBitStream::DecodeHuffman(void) { int code = 0; // ハフマン符号候補 最大16 bits int length = 0; // ハフマン符号候補のビット数 int next = 0; // 次の1 bit int k = 0; // ハフマン符号表のインデックス while (k < huffElementNo && length < 16) { length++; code <<= 1; next = GetBit(); if (next < 0) return -1; code |= next; while(huffTab[k].HuffBit == length) { if(huffTab[k].HuffCode == code) return huffTab[k].HuffVal; k++; } } return -1; // エラー } int CRBitStream::DecodeHuffmanValue(int huffVal) { int diff = 0; // ピクセルの差分 if (huffVal < 0 ) return -1; // エラー diff = GetBits(huffVal); // 差分を取り出し if ((diff & (1 << (huffVal - 1) )) ==0) { diff -= (1 << huffVal) -1; } return diff; }
さてコードの説明補足をしましょう まず以下の部分ですが・・・
void CRBitStream::IncBuf(void) { // buffer読み出しを1 BYTE進める if (++mRead >= frameEnd) { mReadable = false; // 次は読み出し不可にする } else { if (*mRead == TAG) { // 0x00をカラ送りすると共に最後のチェック if (++mRead >= frameEnd) mReadable = false; *mRead = TAG; //taka: 次に0xffを返さなければならないので便宜的に書き換える } } }
この部分では0xFF00
という2 Bytesのビット列に対して、これはJPEG規約に則り、0xFFという1 Byteビット列とみなすための処理です
int CRBitStream::SetFrame(int frame) { // フレーム(何コマ目?)をセットする 成功 = フレーム、失敗 = -1 if (frame < 0) frame = 0; if (frame > (totalFrameCount - 1)) frame = totalFrameCount - 1; currentFrame = frame; frameTop = jpegTagTbl[frame].tagSOS + 9; frameEnd = jpegTagTbl[frame].tagEOI; mRead = frameTop; mMask = static_cast<BYTE>(0x80); // setMSB mReadable = true; // アクセスエラー無し MakeHuffTab(); // ハフマン符号表も完成した return frame; }
このコードにおいて、
frameTop = jpegTagTbl[frame].tagSOS + 9;と +9をしていますが、これは、
SOSに引き続いての 9 bytesはScan Headerであり画像データそのものでは無く、読み飛ばさねばならないからです
ついに動画出力に成功
2013年8月6日
各コマのDICOM XA画像出力に成功し、動画とすることは目前に迫っていました。動画化することに技術的難点があるとは考えませんでした。実際、MFC single document classを用いて実装し、ファイルからメモリに読み取り、フレームに分割する部分をCDocument classに実装し、一秒間15コマのタイマー割り込みと、画面に書き込む部分をCView classに実装し、エイヤッとコンパイルしたところ、見事に動画として動作しました。動画も軽快であり、画面のチラつきも無く、まだまだコマ数を増やせそうです。いやあ長い旅路でしたが、ようやく DICOM XA出力できるようになりました。
後は、DICOMとしてのタグの処理などを実装します。これは簡単ですが、ややこしい作業です。