DICOM画像を開いてみる準備
Eclipseを使って、Mavenプロジェクトを作ります。
ImageJもdependencyに追加しておいてください。
JPEGなどで圧縮されている場合は、ページ一番下の読み込みを。
(pom.xmlの例)
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.visionary</groupId>
<artifactId>dcm4che_tutorial</artifactId>
<version>0.0.1-SNAPSHOT</version>
<repositories>
<repository>
<id>www.dcm4che.org</id>
<name>dcm4che Repository</name>
<url>https://www.dcm4che.org/maven2/</url>
</repository>
</repositories>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.dcm4che/dcm4che-core -->
<dependency>
<groupId>org.dcm4che</groupId>
<artifactId>dcm4che-core</artifactId>
<version>5.18.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/net.imagej/ij -->
<dependency>
<groupId>net.imagej</groupId>
<artifactId>ij</artifactId>
<version>1.52p</version>
</dependency>
</dependencies>
</project>
DICOM画像を開く
前提です。
DICOM画像であること。つまり、ピクセルが含まれていること。構造化レポートはピクセルがないので、以下のコードでは動きません。(ピクセルを持たないDICOMファイルの読み込みヒントはコードに。)
今回はRGB形式の画像読み込みです。この読み込みの仕組みを理解できれば、ShortでもFloatでもDoubleでも、それがグレースケールでもRGBでも、マルチチャンネルでも、読めるようになるのではないか、という淡い期待から。
DICOM画像を開く
ImageJを使って開いてみますが、Javaのオリジナルで開く方法と、ImageJで開く方法を紹介する。ImageIO.read(File dicom);を使うと早いが、この方法では、画像が圧縮されていた場合に気づかずにそのまま処理してしまうので、DICOM属性を確認して、DICOM画像の性格を見越して、処理したほうがいい。
ピクセルをByteBufferという最小単位に分解してから、ピクセルを構築する。
まず、DICOMファイルを読み込みます。
String img_path = "C:\\Users\\RT-Mii\\Desktop\\test-dcm";
File f = new File(img_path);
if (f == null) {
return;
}
DicomInputStream dis = null;
Attributes dcm = null;
try {
dis = new DicomInputStream(f);
dcm = dis.readDataset(-1, -1);
//if you want to read dcm file which not included pixeldata,
//dcm = dis.readDataset(-1, Tag.PixelData);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
SafeClose.close(dis);
}
画像の種類と画像フォーマットを調べます。(番号が取得できるので、詳細はDICOM標準規格から検索して下さい。)
(ここで、思うところがあれば、先に非圧縮したりなどの前処理をします。)
System.out.println(dcm.getString(Tag.SOPClassUID));//種類
System.out.println(dcm.getString(Tag.TransferSyntaxUID));//形式
ピクセルデータを取り出します。
byte[] pixels = null;
try {
pixels = dcm.getBytes(Tag.PixelData);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
byteはCPUによって並び順が上下する(BigEndian/LittleEndian)ので、今回はIntel向けにリトルエンディアンにして、念のため整列させておきます。(おそらく、ビッグエンディアンでも大丈夫なはず。。)
ByteBuffer buffer = ByteBuffer.wrap(pixels).order(ByteOrder.LITTLE_ENDIAN);
pixels = buffer.array();
後から使うので、画像の高さと幅を取得しておきます。
int w = dcm.getInt(Tag.Columns, 0);//0は、タグに値がなかった場合の用意
int h= dcm.getInt(Tag.Rows, 0);
ピクセル当たりのデータ量を確認しておきます。BitsAllocatedは1ピクセルあたりに割り振られる容量です(aRGBのカラーでも、この値はカラーチャンネルごとのピクセルという意味で8-bitになります)。
SamplesPerPixelはピクセルのもつチャンネル数です。例えば、RGBの場合、3などですね。
System.out.println(dcm.getString(Tag.BitsAllocated));
System.out.println(dcm.getString(Tag.SamplesPerPixel));//ここでは3
今回はRGBを対象としているので、SamplesPerPixelは3です。では、pixelsとSamplePerPixelの整合性を確認します。
pixelsの中には、1ピクセル当たり3つのbyte(r,g,b)が整列しているはずです。
よって、pixels/SamplePerPixel == x*hとなるはずです。
System.out.println(pixels.length/3);
System.out.println(w*h);
ここまで確認出来たら、あとはピクセルを再構築して、画像を表示します。
2つ、やり方を紹介します。先に、byteピクセルをintにまとめるために、ピクセルの受け皿を用意します。
int[] rgb = new int[pixels.length/3];
1.ImageJで楽ちんする
javaの場合、通常、intは32bitですから、aRGBの4要素に、それぞれ8ビットずつ割り当ててやります。ビット演算でこのようにできます。RGBの部分はもとのpixelsから3つずつ順に取得して、アルファ(透明)の設定は今回はないので、すべて透明になるようにそのまま値を入れます。
for (int i = 0; i < pixels.length / 3; i++) {
rgb[i] = 0xff << 24 // Alpha (all opaque)
| ((pixels[i * 3] & 0xff) << 16)
| ((pixels[i * 3 + 1] & 0xff) << 8)
| ((pixels[i * 3 + 2] & 0xff));
}
(補足)- あるピクセルのRED成分:(pixels[i * 3] & 0xff)
- あるピクセルのGREEN成分:(pixels[i * 3 + 1] & 0xff)
- あるピクセルのBLUE成分:(pixels[i * 3 + 2] & 0xff)
ColorProcessor cp = new ColorProcessor(w, h, rgb);
画像を確認します。
ImagePlus imp = new ImagePlus("",cp);
imp.show();
2.Javaのクラスをゴリゴリ使います。
今回、pixelsはbankというピクセル位置ごとにRGBが整列しているbyte[]になっています。これを使って画像を作ることもできます。実際には、カラーの表示方法や、透明度の設定、ピクセルの性質(byte or short or float or double)など、画像にするまでに詳細な設定があります。
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
ColorModel cm = new ComponentColorModel(cs, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
SampleModel sm = cm.createCompatibleSampleModel(w, h);
ここで、バンクデータであるpixelsを使います。
DataBufferByte db = new DataBufferByte(pixels, w*h);
WritableRaster raster = Raster.createWritableRaster(sm, db, null);
BufferedImage result = new BufferedImage(cm, raster, false, null);
画像を確認します。
ImagePlus imp = new ImagePlus("",result);
imp.show();
個人的には、1.の方法が分かりやすかったです。グレースケールの読み込みも似たような方法でできます。DICOM画像はほとんど16bit-グレースケール(つまりピクセルごとの値はshortで、ピクセルごとのサンプル数は1)なので、ByteをShortにすることを意識して、上のコードを改変すればよいはずです。
aRGBは4つの配列を意識しますが、グレースケールは1つの配列なので、もっと楽でしょう。
(追記)16ビットグレースケール画像を開く
void show16bit(byte[] pixels,int w,int h) {
short[] shortArray = new short[pixels.length/2];
ByteBuffer byteBuf = ByteBuffer.wrap(pixels).order(ByteOrder.LITTLE_ENDIAN);
byteBuf.asShortBuffer().get(shortArray);
System.out.println(shortArray.length);
//https://forum.image.sc/t/how-to-initialize-the-shortprocessor/23689/2
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
ColorModel cm = new ComponentColorModel(cs, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);//allways byte for colormodel!
ShortProcessor sp = new ShortProcessor(w, h, shortArray, cm);
ImagePlus imp = new ImagePlus("",sp);
imp.show();
}
(追記) 圧縮形式のDICOM画像の場合
BufferedImage bi = null;
File f = new File(path);
// ImageIO.scanForPlugins();
// Iterator<ImageReader> iter = ImageIO.getImageReadersByFormatName("DICOM");
// ImageReader reader = (ImageReader) iter.next();
try {
DicomImageReader reader = (DicomImageReader) new DicomImageReaderSpi().createReaderInstance();
DicomImageReadParam param = (DicomImageReadParam) reader.getDefaultReadParam();
FileImageInputStream fiis = new FileImageInputStream(f);
reader.setInput(fiis,false);
int numFrame = reader.getNumImages(true);
if(numFrame > 1) {
System.out.println("CanonTotalReportExtractor: this file is frame data, return.");
return new Object[] {null, null};
}
/*
* なぜだかEclipse上では動作するが、
* リリースすると動かない。
* bi = ImageIO.read(f);
*/
// bi = ImageIO.read(f);//対応していない種類の画像だったときは例外は発生せず、nullが返る。
bi = reader.read(0, param); //frameNoOfImage and Param.this case always index is 0.
fiis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Visionary Imaging Services, Inc.2019/9/5
コメント
コメントを投稿