Java解析多帧DICOM

1,351次阅读
2条评论

共计 7202 个字符,预计需要花费 19 分钟才能阅读完成。

1.前言

近日得到反馈dicom解析的图片不全,结果发现都是多帧的dicom,原来的只能解析单帧,继续原来的解析方式还要先转多帧dicom到多个单帧dicom,再继续解析,很是麻烦。网上几乎没有可用的示例,笔者特在此提供示例 Java解析多帧DICOM

2.源码

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.UID;
import org.dcm4che3.data.VR;
import org.dcm4che3.emf.MultiframeExtractor;
import org.dcm4che3.image.ICCProfile;
import org.dcm4che3.imageio.plugins.dcm.DicomImageReadParam;
import org.dcm4che3.io.DicomInputStream;
import org.dcm4che3.io.DicomOutputStream;
import org.dcm4che3.util.SafeClose;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.StreamSupport;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * @author mysteriousman
 */
public class DicomParserUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(DicomParserUtil.class);
    // 目前只支持zip压缩文件
    private static final String compressExtension = "zip";
    private static final String dicomExtension = "dcm";
    private static final MultiframeExtractor extractor = new MultiframeExtractor();
    private static final ICCProfile.Option iccProfile = ICCProfile.Option.none;
    private static final ImageReader imageReader = ImageIO.getImageReadersByFormatName("DICOM").next();
    private static final ImageWriter imageWriter;
    private static final ImageWriteParam imageWriteParam;

    static {
        Iterator<ImageWriter> imageWriters =
                ImageIO.getImageWritersByFormatName("JPEG");
        if (!imageWriters.hasNext()) {
            throw new RuntimeException();
        }
        Iterable<ImageWriter> iterable = () -> imageWriters;
        imageWriter = StreamSupport.stream(iterable.spliterator(), false)
                .filter(matchClassName("com.sun.imageio.plugins.*"))
                .findFirst()
                .orElseThrow(IllegalArgumentException::new);
        imageWriteParam = imageWriter.getDefaultWriteParam();
    }

    /**
     * 解析上传的文件
     *
     * @param multipartFiles 上传文件
     */
    public static Map<String, List<DicomAttribute>> parse(List<MultipartFile> multipartFiles) {
        for (MultipartFile multipartFile : multipartFiles) {
            String extension = FilenameUtils.getExtension(multipartFile.getOriginalFilename());
            if (!compressExtension.equalsIgnoreCase(extension)) continue;
            InputStream inputStream = null;
            try {
                inputStream = multipartFile.getInputStream();
                return parse(inputStream);
            } catch (Exception ignored) {
            } finally {
                try {
                    Objects.requireNonNull(inputStream).close();
                } catch (Exception ignored) {
                }
            }
        }
        return Collections.emptyMap();
    }

    /**
     * 解析输入流
     *
     * @param inputStream 输入流
     * @return DICOM文件信息
     */
    public static Map<String, List<DicomAttribute>> parse(InputStream inputStream) throws IOException {
        Map<String, List<DicomAttribute>> result = new HashMap<>();
        parse(inputStream, result);
        return result;
    }

    /**
     * 解析输入流
     *
     * @param inputStream 输入流
     * @param result      DICOM文件信息容器
     * @return DICOM文件信息
     */
    public static Map<String, List<DicomAttribute>> parse(InputStream inputStream, Map<String, List<DicomAttribute>> result) throws IOException {
        ByteArrayInputStream originalInputStream = new ByteArrayInputStream(IOUtils.toByteArray(inputStream));
        List<ZipEntry> zipEntries = unwrapZipEntries(originalInputStream);
        for (ZipEntry zipEntry : zipEntries) {
            if (!zipEntry.isDirectory()) {
                String extension = FilenameUtils.getExtension(zipEntry.getName());
                InputStream zipEntryInputStream = getInputStream(originalInputStream, zipEntry.getName());
                // zip压缩文件
                if (compressExtension.equalsIgnoreCase(extension)) {
                    parse(zipEntryInputStream, result);
                }
                // 可能是dicom文件
                if (dicomExtension.equalsIgnoreCase(extension) || StringUtils.EMPTY.equals(extension)) {
                    try {
                        // dicom文件流
                        DicomInputStream dicomInputStream = new DicomInputStream(zipEntryInputStream);
                        Attributes attributes = dicomInputStream.readDataset();
                        // 当前dicom帧数
                        int frames = attributes.getInt(Tag.NumberOfFrames, 1);
                        Attributes fmi;
                        for (int frame = 1; frame <= frames; frame++) {
                            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                            Attributes sf;
                            if (frames > 1) {
                                fmi = dicomInputStream.getFileMetaInformation();
                                sf = extractor.extract(attributes, frame - 1);
                            } else {
                                fmi = null;
                                sf = attributes;
                            }
                            convert(getInputStream(originalInputStream, zipEntry.getName()), byteArrayOutputStream, fmi, sf, frame);
                            String imageUrl = "";
                            //String imageUrl = OssUtil.uploadFileSimply(OssUtil.OssBucket.JPG_BUCKET, byteArrayOutputStream);
                            List<DicomAttribute> dicomAttributes = new ArrayList<>();
                            for (int tag : sf.tags()) {
                                DicomAttribute dicomAttribute = new DicomAttribute();
                                dicomAttribute.name = tag;
                                dicomAttribute.value = sf.getString(tag);
                                dicomAttribute.vr = sf.getVR(tag);
                                dicomAttributes.add(dicomAttribute);
                            }
                            result.put(imageUrl, dicomAttributes);
                            LOGGER.info("DICOM解析完毕 图片地址 -> " + imageUrl);
                            //FileUtils.writeByteArrayToFile(new File("/home/mysteriousman/Downloads/abc" + frame + ".jpeg"), byteArrayOutputStream.toByteArray());
                        }
                    } catch (Exception e) {
                        LOGGER.warn("DICOM解析失败", e);
                    }
                }
            }
        }
        return result;
    }

    private static void convert(InputStream src, OutputStream target, Attributes fmi, Attributes sf, int frame) throws IOException {
        writeImage(target, iccProfile.adjust(readImage(src, frame)));
        DicomOutputStream dicomOutputStream = new DicomOutputStream(target, UID.ExplicitVRLittleEndian);
        try {
            dicomOutputStream.writeDataset(fmi != null
                    ? sf.createFileMetaInformation(
                    fmi.getString(Tag.TransferSyntaxUID))
                    : null, sf);
        } finally {
            SafeClose.close(dicomOutputStream);
        }
    }

    private static void writeImage(OutputStream target, BufferedImage bi) throws IOException {
        imageWriter.setOutput(ImageIO.createImageOutputStream(target));
        imageWriter.write(null, new IIOImage(bi, null, null), imageWriteParam);
    }

    private static BufferedImage readImage(InputStream inputStream, int frame) throws IOException {
        try (DicomInputStream dicomInputStream = new DicomInputStream(inputStream)) {
            imageReader.setInput(dicomInputStream);
            return imageReader.read(frame - 1, readParam());
        }
    }

    private static ImageReadParam readParam() {
        DicomImageReadParam param = (DicomImageReadParam) imageReader.getDefaultReadParam();
        param.setAutoWindowing(true);
        param.setPreferWindow(true);
        param.setOverlayActivationMask(0xffff);
        param.setOverlayGrayscaleValue(0xffff);
        param.setOverlayRGBValue(0xffffff);
        return param;
    }

    private static List<ZipEntry> unwrapZipEntries(InputStream inputStream) {
        List<ZipEntry> zipEntries = new ArrayList<>();
        try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
            ZipEntry ze;
            while ((ze = zipInputStream.getNextEntry()) != null) {
                LOGGER.debug("Unzipping " + ze.getName());
                zipEntries.add(ze);
            }
        } catch (Exception e) {
            LOGGER.warn("Unwrap file failed", e);
        }
        return zipEntries;
    }

    private static InputStream getInputStream(InputStream inputStream, String entry) throws IOException {
        inputStream.reset();
        ZipInputStream zin = new ZipInputStream(inputStream);
        for (ZipEntry e; (e = zin.getNextEntry()) != null; ) {
            if (e.getName().equals(entry)) {
                byte[] bytes = IOUtils.toByteArray(zin);
                ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
                inputStream.reset();
                return byteArrayInputStream;
            }
        }
        throw new EOFException("Cannot find " + entry);
    }

    private static Predicate<Object> matchClassName(String clazz) {
        Predicate<String> predicate = clazz.endsWith("*")
                ? startsWith(clazz.substring(0, clazz.length() - 1))
                : clazz::equals;
        return w -> predicate.test(w.getClass().getName());
    }

    private static Predicate<String> startsWith(String prefix) {
        return s -> s.startsWith(prefix);
    }

    public static class DicomAttribute {
        public int name;
        public Object value;
        public VR vr;
    }
}

3.说明

使用前自行引入相应的依赖包,注意当前只支持zip压缩文件。可以处理基于spring的文件上传parse(List<multipartFiles> multipartFiles),也可直接使用parse(InputStream inputStream)处理zip输入流;方法最终返回图片地址(需要自己处理)及相应的dicom属性。

正文完
 1
mysteriousman
版权声明:本站原创文章,由 mysteriousman 2023-03-07发表,共计7202字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(2条评论)
你帆哥 评论达人 LV.1
2023-03-13 16:42:30 回复

学到了

 Macintosh  Chrome  中国安徽省合肥市电信
mysteriousman 博主
2023-11-08 10:57:35 回复

JDK的ZipEntry在某些情况下存在解析问题,可以使用Apache的ArchiveEntry改写

 Linux  Chrome  美国加利福尼亚