スキップしてメイン コンテンツに移動

dcm4cheを使ってDICOM画像を作成

前提

DICOM標準規格の読み方が分かる方が対象です。まだDICOM標準規格の読み方がわからない人は、そのうち技術ブログにアップするので、チェックして下さい。
本来、DICOM画像は、医療機器から出力されます。 今回のように、一度作成された画像を後処理などで改変したり、全く別のDICOM画像として作成することは、研究目的以外では、一般的ではありません。このような二次的に作成されるDICOM画像は、セカンダリキャプチャと呼ばれています。
使い勝手のよい何でもやさんで、ときにはドーズレポートや、スクリーンキャプチャなどもこの形式で保存されます。
今回は、セカンダリキャプチャとしてのDICOM画像を出力します。
EclipseのMavenプロジェクトで、DCM4CHE5.18.x、ImageJ1.52辺りを使います(?の方は、前のブログ内容を参考にして下さい)。

全体像

  1. 適当な一般画像フォーマットを用意する(画像のタイプを調べておく)
  2. 画像を読み込む
  3. DICOM属性のDICOMデータセットを作成する
  4. 画像からピクセルデータを取得して、データセットにセットする
  5. 出力する

適当な一般画像フォーマットを用意する(画像の種類を調べておく)

今回は次のような画像を、画像のタイプ別に用意しました。
すべてImageJサンプル画像です。
画像のタイプは、Image>Typeから変更できます。

8-bit Grayscale 

16-bit Grayscale 

32-bit Grayscale 

RGB

ImageJからこれらの画像を作成するときは、すべてTiffフォーマットで作成します。
ImageJではその他の一般的なフォーマットでグレースケールを保存するとすべて8bitになってしまうため、注意が必要です。

補足

Overlayがある場合がありますが、今回は対象外です。いつになるかわかりませんが、KeyObjectなどと一緒に扱う方法を示せたらと思います。

画像を読み込む

今回は、上記の画像をMavenプロジェクトのsrc/main/resourcesに事前にコピーしています。
DICOMデータの保存先(dest)は、プロジェクトフォルダとし、ファイル名は適当に決めます。
コード内の**は、8, 16, 32, RGBに読み替えて下さい。

dest = new File(".").getAbsolutePath()+File.separator+"testdcm**bit";
try {
URI uri = getClass().getClassLoader().getResource("**-bit-image.tif").toURI();
File f = new File(uri);
target = f.getAbsolutePath();
} catch (URISyntaxException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
ImagePlus imp = new ImagePlus(target);

DICOM属性のDICOMデータセットを作成する

セカンダリキャプチャに必要なメタデータを準備します。
メタデータは、DICOM IODを参照して、にらめっこして、選択します。これが大変な作業です。

Attributes dataset = new Attributes();//これがDICOMファイルの元
// type 1 attributes.
dataset.setString(Tag.SOPClassUID, VR.UI, UID.SecondaryCaptureImageStorage);
dataset.setString(Tag.StudyInstanceUID, VR.UI, UIDUtils.createUID());
dataset.setString(Tag.SeriesInstanceUID, VR.UI, UIDUtils.createUID());
dataset.setString(Tag.SOPInstanceUID, VR.UI, UIDUtils.createUID());
// type 2 attributes
dataset.setString(Tag.PatientID, VR.LO, "1234567890");
dataset.setString(Tag.PatientName, VR.LO, "DCM4CHE^TUTORIAL");
dataset.setString(Tag.PatientBirthDate, VR.DA, "20190907");
dataset.setString(Tag.PatientSex, VR.CS, "M");
dataset.setString(Tag.StudyDate, VR.DA, "20190907");
dataset.setString(Tag.StudyTime, VR.TM, "170648");
dataset.setString(Tag.AccessionNumber, VR.SH, "");
dataset.setString(Tag.ReferringPhysicianName, VR.PN, "Black^Jack");
dataset.setString(Tag.StudyID, VR.SH, "1");
dataset.setString(Tag.SeriesNumber, VR.IS, "1");
dataset.setString(Tag.ModalitiesInStudy, VR.CS, "");
dataset.setString(Tag.Modality, VR.CS, "OT");// OT:Other
dataset.setString(Tag.NumberOfStudyRelatedInstances, VR.IS, "1");
dataset.setString(Tag.NumberOfStudyRelatedSeries, VR.IS, "1");
dataset.setString(Tag.NumberOfSeriesRelatedInstances, VR.IS, "1");

補足

今回は一から構築していますが、実践では、すでにDICOMファイルがあることが多いので、そのような場合は、dcm2xmlツールを使ってDICOMをXMLに変換した後(このときに、ピクセルをよけておく:-B,--no-bulkdataオプションを使う) 、xml2dcmツールのコードを参照して、このXMLからAttributesオブジェクトを再構築した後に、 必要な部分を変更してからピクセルデータをセットすることも出来ます。

画像からピクセルデータを取得して、データセットにセットする

ここでは、画像のタイプ別にピクセルの処理方法を合わせます。
行っていることは、ImagePlusオブジェクトから、ピクセル配列(キャリブレーションされていない生のピクセル値)を取得して、ByteBufferに落としてから、datasetにセットしています。
その他、画像に関わる部分もここでDICOM属性が決まるので、一緒に設定します。

int type = imp.getBitDepth();
System.out.println(type);

if (type == 8) {
byte[] pixelDataByte = (byte[])imp.getProcessor().getPixelsCopy();
ByteBuffer byteBuffer = ByteBuffer.wrap(pixelDataByte);
dataset.setBytes(Tag.PixelData, VR.OB, byteBuffer.array());
dataset.setInt(Tag.BitsAllocated, VR.US, 8);
dataset.setInt(Tag.BitsStored, VR.US, 8);
dataset.setInt(Tag.HighBit, VR.US, 7);
else if (type == 16) {//unsigned
short [] pixelDataShort = (short[])imp.getProcessor().getPixelsCopy();
ByteBuffer byteBuffer = ByteBuffer.allocate(pixelDataShort.length * 2);
//先にByteOerderを整列させること
byteBuffer.order(ByteOrder.nativeOrder()).asShortBuffer().put(pixelDataShort);
dataset.setBytes(Tag.PixelData, VR.OW, byteBuffer.array());
dataset.setInt(Tag.BitsAllocated, VR.US, 16);
dataset.setInt(Tag.BitsStored, VR.US, 12);
dataset.setInt(Tag.HighBit, VR.US, 11);
}
else if (type == 32) {
float[] pixelDataFloat = (float[])imp.getProcessor().getPixelsCopy();
ByteBuffer byteBuffer = ByteBuffer.allocate(pixelDataFloat.length * 4);
byteBuffer.order(ByteOrder.nativeOrder()).asFloatBuffer().put(pixelDataFloat);
dataset.setBytes(Tag.FloatPixelData, VR.OF, byteBuffer.array());
dataset.setInt(Tag.BitsAllocated, VR.US, 32);
dataset.setInt(Tag.BitsStored, VR.US, 32);//unconfident
dataset.setInt(Tag.HighBit, VR.US, 31);//unconfident
}
// 64 bit gray, if IJ treats 64 bits.
// else if (dataBuffer instanceof DataBufferDouble) {
//     double[] pixelDataDouble =  (double[])imp.getProcessor().getPixelsCopy();
//     ByteBuffer byteBuffer = ByteBuffer.allocate(pixelDataFloat.length * 8);
//     byteBuffer.order(ByteOrder.nativeOrder()).asDoubleBuffer().put(pixelDataDouble);
//     dataset.setBytes(Tag.DoubleFloatPixelData, VR.OD, byteBuffer.array());
//         dataset.setInt(Tag.BitsAllocated, VR.US, 64);
//         dataset.setInt(Tag.BitsStored, VR.US, 64);//unconfident
//         dataset.setInt(Tag.HighBit, VR.US, 63);//unconfident
// }
else if (type == 24) {
pixelDataInt = (int[])imp.getProcessor().getPixels();
ByteBuffer byteBuffer = ByteBuffer.allocate(pixelDataInt.length * 3);
// byteBuffer.order(ByteOrder.nativeOrder()).asIntBuffer().put(IntBuffer.wrap(pixelDataInt));//why are you can not run ??
byte[] bytes = new byte[pixelDataInt.length * 3];
for (int i = 0; i < pixelDataInt.length; i++) {
// bytes[i * 4 + 3] = (byte) ((pixelDataInt[i] & 0xFF000000) >> 24);//a
bytes[i * 3] = (byte) ((pixelDataInt[i] & 0xFF0000) >> 16);//r
bytes[i * 3 + 1] = (byte) ((pixelDataInt[i] & 0xFF00) >> 8);//g
bytes[i * 3 + 2] = (byte) (pixelDataInt[i] & 0xFF);//b
}
byteBuffer = byteBuffer.order(ByteOrder.nativeOrder()).put(bytes);
dataset.setBytes(Tag.PixelData, VR.OB, byteBuffer.array());
RGBTest = byteBuffer.array();
dataset.setInt(Tag.BitsAllocated, VR.US, 8);
dataset.setInt(Tag.BitsStored, VR.US, 8);
dataset.setInt(Tag.BitsAllocated, VR.US, 8);
dataset.setInt(Tag.HighBit, VR.US, 7);
else {
throw new IllegalArgumentException("Not implemented for data buffer type: " + type);
}

上記の方法ではなく、BufferedImageオブジェクトを使う方法もあります。
注意しなければならないのは、「imp.getBufferedImage();」メソッドは、8-bitのBufferedImageしか返さないことです。

// if (dataBuffer instanceof DataBufferByte) {
//    BufferedImage bi = imp.getBufferedImage();
//    DataBuffer dataBuffer = bi.getRaster().getDataBuffer();
// byte[] pixelDataByte = ((DataBufferByte) dataBuffer).getData();
// byteBuffer = ByteBuffer.wrap(pixelDataByte);
// dataset.setBytes(Tag.PixelData, VR.OB, byteBuffer.array());
// dataset.setInt(Tag.BitsAllocated, VR.US, 8);
// dataset.setInt(Tag.BitsStored, VR.US, 8);
// dataset.setInt(Tag.BitsAllocated, VR.US, 8);
// dataset.setInt(Tag.HighBit, VR.US, 7);
// } else if ...

さらに、画像に関する情報を追加します。
ここでは2種類しか扱わない前提なので以下のみです。

if (type == 24) {
dataset.setString(Tag.PhotometricInterpretation, VR.CS, "RGB");
dataset.setInt(Tag.SamplesPerPixel, VR.US, 3);
dataset.setInt(Tag.PlanarConfiguration, VR.US, 0);
} else {
dataset.setString(Tag.PhotometricInterpretation, VR.CS, "MONOCHROME2");
dataset.setInt(Tag.SamplesPerPixel, VR.US, 1);
}
dataset.setInt(Tag.Rows, VR.US, imp.getHeight());
dataset.setInt(Tag.Columns, VR.US, imp.getWidth());
dataset.setInt(Tag.PixelRepresentation, VR.US, 0);// # 1 signed, 0 unsigned

出力する

DICOMデータとして出力します。

DicomOutputStream dos = null;
try {
dos = new DicomOutputStream(new File(dest));
Attributes fmi = dataset.createFileMetaInformation(UID.ExplicitVRLittleEndian);
dos.writeFileMetaInformation(fmi);
dataset.writeTo(dos);
} catch (IOException e) {
    e.printStackTrace();
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
dos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

DICOMデータが正しく作成できているかどうかは、DICOMバリデーションツールを使うか、実際にDICOMビューワで開いて確認すると良いと思います。
32-bit, 64-bit grayscale画像は、病理などの専門的な領域で活躍していますが、一般の医療では、16-bit画像が主流です(32-bit, 64-bit grayscale画像に対応しているソフトウェアはなかなか見つからないと思います)。

16-bit grayscale の例 Weasis

Visionary Imaging Services, Inc
2019/9/8

コメント

このブログの人気の投稿

DockerでDICOMサーバを使いまわす

DCM4CHEEがだいぶ前からDocker対応しているので、その手順をメモします。 基本的には、このページに記載の通りです。 https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker 一番シンプルな構築方法でいきます。 https://github.com/dcm4che/dcm4chee-arc-light/wiki/Run-minimum-set-of-archive-services-on-a-single-host あくまでもテストケースですが、今回は仮想OSを立てて、その中にDockerを入れて、サーバーのホストにします。以下のようにしてみました。 大元のPCでの作業 VirtualBox(またはHyper-V)を用意する Ubuntu19.04を最小構成で仮想化する(メモリは4GB, 割り当て仮想保存容量127GBなど)。 (インストール時にイメージを再起動できない場合はPC本体を再起動するとよいことがあります) Ubuntu19.04を起動する。 Ubuntuで行う作業 Ubuntuをインストールしたら行うお決まりのコマンドを打つ sudo -i apt-get update apt-get upgrade vimを入れておく  apt install vim UbuntuにJavaを入れておく。(JDK9以上) apt install openjdk-12-jre-headless Dockerをインストールする apt install docker.io DCM4CHEE関連のイメージを取得する(一行ずつ実行) docker pull dcm4che/slapd-dcm4chee:2.4.44-19.0 docker pull dcm4che/postgres-dcm4chee:12.0-19 docker pull dcm4che/dcm4chee-arc-psql:5.19.0 docker pull dcm4che/keycloak:7.0.0 docker pull dcm4che/keycloak-gatekeeper:7.0.0 docker pull

dcm4cheをローカル環境にインストールする

DCM4CHEのインストール ここからダウンロードします。 執筆時点では(2019/11/5)、最新バージョンは5.19.0です。 https://sourceforge.net/projects/dcm4che/files/dcm4che3/ (リンクではdcm4che3となっていますが、バージョンでは5で、何か意図があるのかなと、思っています。) 環境変数パス ダウンロードができたら、解凍して、binフォルダまでの環境変数パスを通します。 Windows ならシステムの詳細設定からだったと思います。 MacやLinuxなら、.bashrcあたりにパスを追加します。 例えば、ubuntuで、localフォルダにコピーした場合ならば。 vim ~/.bashrc から、「i」でインサートモードにして、以下を追記して、 #dcm4che export PATH="$PATH:/usr/local/dcm4che/dcm4che-5.15.1/bin" 「esc」押して、「:」おして、wq!でエンター。 追記できたら、 $source ~/.bashrc パスが通せたか、dcmdumpコマンドで確認します。 > dcmdump dcmdump: missing file operand Try `dcmdump --help' for more information. このように表示されれば、インストールは完了です。 注意 DCMTKをインストールしている場合、コマンドが被ることがあります。 DICOMツールは何個もいらないので、DCM4CHEを使うときはDCM4CHEのみにパスを、DCMTKを使うときはDCMTKにパスを通すと競合しません。 Visionary Imaging Services, Inc.

はじめに

所信表明 VISブログ担当の小林です。 私の専門は医用画像情報です。 医用画像情報を扱う際に、DICOMデータの取り扱いは避けて通れません。 しかし、DICOMについて調べると、リソースが無くて困ることが多いです。 そこで、個人的に好きなツールである、dcm4cheを使ったDICOMのハウツーを備忘録としてこの技術ブログに残していくことにしました。 これまでにいろいろなオープンソースのDICOMツールを使ってきました。 DCMTK、GDCM、pixelmed.jarやpydicomなどです。 このブログではdcm4cheを使います。dcm4cheはこれらのツールに匹敵する強力なツールです。そして特筆すべき点として、堅牢なDICOMサーバ(dcm4chee)もこのツールを使ってオープンソースで提供されています。 dcm4che 2000年頃、Gunter Zeilinger氏は、商用のJava DICOM Toolkit(JDT)を使ってJDicomユーティリティを作成しました。 この後、彼は自身のDICOMツールキットを書くべきであると決めたそうです。 そして、dcm4che(d-c-m-for-chayと発音)が生まれました。 Gunter氏は、オープンソースの革命的な側面にインスパイアされて、有名な革命家にちなんで、ツールキットに名前を付けることにしたそうです。 当時、このツールキットのオリジナル版は、JavaベースのDICOM API用のJSR(Java Specification Request)としてSun(現在はOracle)に提出することを目的として設計されました。 そのことを念頭に置いて、ツールキットはインターフェース層と実装層に分けられました。 JSRには至りませんでしたが、プロジェクトはJava DICOM開発コミュニティの間で人気が高まり始めました。 そして、dcm4cheをDICOMバックエンドにしたdcm4jbossアーカイブが誕生したそうです。 このDICOMサーバアプリケーションを開発するサブプロジェクトは、IHE Image ManagerとImage Archiveアクターを実装しました。 dcm4jbossは、医用画像の研究および画像診断レポートを管理するための堅牢で安定したプラッ