๐Ÿฉป dcm4che โ€“ Spring์—์„œ DICOM ํŒŒ์ผ ๋‹ค๋ฃจ๊ธฐ

๊น€๊ณต์˜ยท2024๋…„ 8์›” 19์ผ

how-to

๋ชฉ๋ก ๋ณด๊ธฐ
11/12
post-thumbnail

์ด ๊ธ€์—์„œ๋Š” dcm4che ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•ด Spring์—์„œ DICOM ํŒŒ์ผ์„ ์ฝ๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ๋‹ค๋ฃน๋‹ˆ๋‹ค. ๊ตฌํ˜„ ๋ฐฉ๋ฒ•์€ ๐Ÿงญ ํ™˜๊ฒฝ ์„ค์ • ๋ถ€ํ„ฐ ๋‚˜์™€์žˆ์Šต๋‹ˆ๋‹ค.

์™œ dcm4che๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๊ฐ€

DICOM ํŒŒ์ผ์€ ๋ฌด์—‡์ธ๊ฐ€

DICOM์€ Digital Imaging and COmmunication in Medicine์˜ ์•ฝ์–ด๋กœ, ์˜๋ฃŒ ์˜์ƒ(Medical imaging)์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ด๋ฏธ์ง€ ํ˜•์‹์ด๋‹ค. ์ •์˜๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

์˜๋ฃŒ์šฉ ๋””์ง€ํ„ธ ์˜์ƒ ๋ฐ ํ†ต์‹ (Digital Imaging and Communications in Medicine, DICOM) ํ‘œ์ค€์€ ์˜๋ฃŒ์šฉ ๊ธฐ๊ธฐ์—์„œ ๋””์ง€ํ„ธ ์˜์ƒํ‘œํ˜„๊ณผ ํ†ต์‹ ์— ์‚ฌ์šฉ๋˜๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ํ‘œ์ค€์„ ์ด์นญํ•˜๋Š” ๋ง์ด๋‹ค.

DICOM ํŒŒ์ผ์—๋Š” ์ด๋ฏธ์ง€์™€ ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ(key-value ํ˜•์‹)๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค. ์ด๋ฏธ์ง€์˜ ํฌ๊ธฐ๊ฐ€ ํด ๋ฟ๋”๋Ÿฌ ์ด์— ๋Œ€ํ•œ ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ๋„ ํฌํ•จํ•˜๊ณ  ์žˆ๊ธฐ์— ๊ฐ ํŒŒ์ผ์˜ ์šฉ๋Ÿ‰์ด 20~30MB ์ •๋„๋กœ ํฌ๋‹ค. ๋‚ด๊ฐ€ ๋‹ค๋ค˜๋˜ Mammography ์ด๋ฏธ์ง€๊ฐ€ ํ‰๊ท ์ ์œผ๋กœ ๊ทธ๋žฌ๋‹ค๋Š”๊ฑฐ์ง€, ๋” ํด ์ˆ˜๋„ ์žˆ๋‹ค.

์™œ DICOM ํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜๋Š”๊ฐ€

ํ˜„์žฌ ๊ฐœ๋ฐœ ์ค‘์ธ ๊ฒƒ์€ Mammography ์ด๋ฏธ์ง€๋กœ ์œ ๋ฐฉ์•” ๋ฐœ์ƒ๋ฅ ์„ ์ง„๋‹จํ•˜๋Š” ์ œํ’ˆ์ด๋‹ค. ์ด๋•Œ Mammography ์ด๋ฏธ์ง€๊ฐ€ DICOM ํŒŒ์ผ์ด๋‹ค. ์ด์— ๋Œ€ํ•œ ์ง„๋‹จ ๊ฒฐ๊ณผ(Secondary capture) ๋˜ํ•œ DICOM ํŒŒ์ผ๋กœ ๊ตฌ์„ฑ๋˜์–ด์žˆ๋‹ค. ์ง„๋‹จ ๊ฒฐ๊ณผ๋Š” ์ธํผ๋Ÿฐ์Šค๋ฅผ ํ†ตํ•ด ๋‚˜์˜ค๋Š”๋ฐ, ์ธํผ๋Ÿฐ์Šค๊ฐ€ ๋ถˆ๊ฐ€ํ•œ ์กฐ๊ฑด์„ ํ•„ํ„ฐ๋งํ•ด validํ•œ ์ด๋ฏธ์ง€๋งŒ AI ์„œ๋ฒ„์— ์ „๋‹ฌํ•ด์•ผ ํ•œ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด DICOM ํŒŒ์ผ์„ ์ฝ์–ด ํƒœ๊ทธ๋ฅผ validationํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์ž๋ฉด, ์ด๋ฏธ์ง€์˜ ๋ถ„๋ฅ˜๊ฐ€ Mammography๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ invalidํ•œ ์ด๋ฏธ์ง€๋กœ ๋ถ„๋ฅ˜๋˜๋Š” ์‹์ด๋‹ค.

์™œ ๊ตณ์ด dcm4che๋ฅผ ์“ฐ๋Š”๊ฐ€

DICOM ์ด๋ฏธ์ง€์˜ validation์€ key-value ํ˜•์‹์˜ ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ, ์ฆ‰ tag๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค. tag๋ฅผ ์ฝ๊ธฐ ์œ„ํ•ด์„œ๋Š” DICOM ํŒŒ์ผ์„ ์ฝ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ–ˆ๋‹ค. Java๋กœ DICOM ํŒŒ์ผ์„ ์ฝ๊ธฐ ์œ„ํ•ด ์—ฌ๋Ÿฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ฐพ์•„๋ดค์ง€๋งŒ, dcm4che๊ฐ€ Java ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค ์ค‘ ๊ฐ€์žฅ ์‚ฌ์šฉ ๋นˆ๋„๊ฐ€ ๋†’์•„ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค. ๋˜ํ•œ dcm4che๋Š” ์˜์กด์„ฑ ์„ค์ •๋งŒ์œผ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ธฐ์— ์„ ํƒํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

๐Ÿงญ ํ™˜๊ฒฝ ์„ค์ •

environment

  • Spring Boot v3.3.1
  • Gradle build

์˜์กด์„ฑ ์„ค์ •

ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” maven central์— ์—†๋Š” ์•„ํ‹ฐํŒฉํŠธ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ repository๋ฅผ ๋“ฑ๋กํ•ด์•ผ ํ•œ๋‹ค. repository ๋“ฑ๋ก ์—†์ด ์ง„ํ–‰ํ•˜๋ฉด ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค.
์ด ๊ธ€์—์„œ๋Š” ์•„๋ž˜์˜ ์•„ํ‹ฐํŒฉํŠธ๋งŒ์„ ์‚ฌ์šฉํ–ˆ์ง€๋งŒ, ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” maven repository์—์„œ ๊ฒ€์ƒ‰ ํ›„ ์ฐพ์•„์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•œ๋‹ค.

...
repositories {
    mavenCentral()
    maven{ // repository ๋“ฑ๋ก
        url "https://maven.scijava.org/content/repositories/public/"
    }
}

dependencies {
    ...
    // dcm4che
    implementation 'org.dcm4che:dcm4che-core:5.31.1'
    implementation 'org.dcm4che.tool:dcm4che-tool-common:5.30.0'
    implementation 'org.dcm4che.tool:dcm4che-tool-dcm2json:5.30.0'
	...
}
...

๐Ÿš€ How to use

์•„๋ž˜๋Š” DICOM ํŒŒ์ผ์˜ ํƒœ๊ทธ๋ฅผ ํŒŒ์‹ฑํ•˜๊ณ  ์ด๋ฅผ validationํ•˜๋Š” ์˜ˆ์ œ์ด๋‹ค. ์›ํ•˜๋Š” ํƒœ๊ทธ๋งŒ ์ฝ์–ด์„œ ์ €์žฅํ•ด๋‘˜ ์ˆ˜ ์žˆ๋„๋ก DicomTagValue ํด๋ž˜์Šค๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ์„ ์–ธํ–ˆ๋‹ค.

DicomTagValue.java

package example.api.instance.util;

import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class DicomTagValue {

    private String patientName;
    private String institutionName;
    private String patientId;
    private String studyInstanceUid;
    private String SOPInstanceUid;

    private String SOPClassUid;
    private String viewPosition;
    private String imageLaterality;
    private String imageType;
    private Double estimatedRadiographicMagnificationFactor;
    private String patientSex;
    private String breastImplantPresent;
    private String performedProcedureStepDescription;
    private String seriesDescription;
    private String ProtocolName;
    private String presentationLUTShape;
    private String exposureStatus;

    private String studyDescription;
    private Date studyDate;
}

DicomTagParser.java

parser๊ฐ€ ๊ฐ ํƒœ๊ทธ์— ๋งž๋Š” ์ž๋ฃŒํ˜•์œผ๋กœ ๊ฐ’์„ ์ถ”์ถœํ•˜๊ธธ ๋ฐ”๋žฌ๊ธฐ ๋•Œ๋ฌธ์— ์ œ๋„ค๋ฆญ ํƒ€์ž…์œผ๋กœ ๊ฐ ํƒœ๊ทธ๋ฅผ ์ฝ์–ด์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ญ”๊ฐ€ ๋งˆ์Œ์— ๋“ค์ง€ ์•Š์•„ ์ฐจํ›„ ๋ฆฌํŒฉํ† ๋ง์ด ํ•„์š”ํ•  ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค.

package example.api.instance.util;

import java.io.File;
import java.io.IOException;
import java.util.Date;
import example.global.exception.DicomTagParseException;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Tag;
import org.dcm4che3.io.DicomInputStream;

/**
 * Dicom tag parser
 */
public class DicomTagParser {

    /**
     * Parse dicom tag
     *
     * @param dicomFile dicom file
     * @return dicom tag value
     */
    public static DicomTagValue parseDicomTag(File dicomFile) {

        DicomTagValue dicomTagValue = new DicomTagValue(
            getDicomTagValue(dicomFile, Tag.PatientName, String.class),
            getDicomTagValue(dicomFile, Tag.InstitutionName, String.class),
            getDicomTagValue(dicomFile, Tag.PatientID, String.class),
            getDicomTagValue(dicomFile, Tag.StudyInstanceUID, String.class),
            getDicomTagValue(dicomFile, Tag.SOPInstanceUID, String.class),
            getDicomTagValue(dicomFile, Tag.SOPClassUID, String.class),
            getDicomTagValue(dicomFile, Tag.ViewPosition, String.class),
            getDicomTagValue(dicomFile, Tag.ImageLaterality, String.class),
            getDicomTagValue(dicomFile, Tag.ImageType, String.class),
            getDicomTagValue(dicomFile, Tag.EstimatedRadiographicMagnificationFactor, Double.class),
            getDicomTagValue(dicomFile, Tag.PatientSex, String.class),
            getDicomTagValue(dicomFile, Tag.BreastImplantPresent, String.class),
            getDicomTagValue(dicomFile, Tag.PerformedProcedureStepDescription, String.class),
            getDicomTagValue(dicomFile, Tag.SeriesDescription, String.class),
            getDicomTagValue(dicomFile, Tag.ProtocolName, String.class),
            getDicomTagValue(dicomFile, Tag.PresentationLUTShape, String.class),
            getDicomTagValue(dicomFile, Tag.ExposureStatus, String.class),
            getDicomTagValue(dicomFile, Tag.StudyDescription, String.class),
            getDicomTagValue(dicomFile, Tag.StudyDate, Date.class)
        );

        return dicomTagValue;
    }

    /**
     * Get dicom tag value
     *
     * @param file dicom file
     * @param tag  dicom tag
     * @param type tag value type
     * @return tag value
     */
    private static <T> T getDicomTagValue(File file, int tag, Class<T> type) {

        try (DicomInputStream dis = new DicomInputStream(file)) {

            Attributes dicomAttributes = dis.readDataset(-1, -1);

            if (type.equals(Double.class)) {
                return (T) Double.valueOf(dicomAttributes.getString(tag));
            }

            if (type.equals(java.util.Date.class)) {
                return (T) dicomAttributes.getDate(tag);
            }

            return (T) dicomAttributes.getString(tag);

        } catch (IOException e) {
            throw new DicomTagParseException();
        }
    }
}

DicomTagValidator.java

DICOM ํŒŒ์ผ์˜ ํƒœ๊ทธ์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์˜ˆ์‹œ๋Š” ์•„๋ž˜์˜ ๋ฉ”์„œ๋“œ์™€ ๊ฐ™๋‹ค. ์‹ค์ œ๋กœ๋Š” public ๋ฉ”์„œ๋“œ์—์„œ private ๋ฉ”์„œ๋“œ ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ๋ถˆ๋Ÿฌ์™€ ๊ฒ€์ฆํ•˜๋Š” ์‹์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค. ๋‚˜๋จธ์ง€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋Š” ์กฐ๊ฑด์ด ๋„ˆ๋ฌด ๋งŽ๊ณ  ์ฝ”๋“œ๊ฐ€ ๊ธธ์–ด์ ธ ์ƒ๋žตํ–ˆ๋‹ค.

/**
  * validate Image that does not contain "supplemental", "localization", "3d", "tomo", "combo",
  * "implant"
  *
  * @param performedProcedureStepDescription PerformedProcedureStepDescription
  * @param SeriesDescription                 SeriesDescription
  * @param protocolName                      ProtocolName
  */
private void validateImage(String performedProcedureStepDescription, String SeriesDescription, String protocolName) {

    List<String> temp = List.of(
        performedProcedureStepDescription,
        SeriesDescription,
        protocolName
    );

    temp = temp.stream()
        .map(String::toLowerCase)
        .toList();

    List<String> NotContains = List.of("supplemental", "localization", "3d", "tomo", "combo",
        "implant");

    if (temp.stream().anyMatch(s -> NotContains.stream().anyMatch(s::contains))) {
        throw new InvalidDicomTagValueException(
            "PerformedProcedureStepDescription, SeriesDescription, ProtocolName");
    }
}

๊ทธ๋ž˜์„œ ์ด๊ฑธ ์–ด๋””์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ?

์–ด๋””๋“  ์“ธ๋ชจ๊ฐ€ ์žˆ๊ฒ ์ง€.....

์‚ฌ์‹ค ์˜๋ฃŒ ์˜์ƒ์„ ๋‹ค๋ฃจ์ง€ ์•Š๋Š”๋‹ค๋ฉด ๊ตณ์ด dcm4che๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค. ๊ทธ๋Ÿผ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์ด๋ ‡๊ฒŒ ์ •๋ฆฌํ•ด๋‘๋Š” ๊ฒƒ์€ ๋‚ด๊ฐ€ ์˜๋ฃŒ๊ธฐ๊ธฐ ๊ฐœ๋ฐœ์„ ํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด๋ณด๊ณ ์ž ๊ตฌ๊ธ€๋ง์„ ๋งŽ์ด ํ–ˆ๋Š”๋ฐ, ํ•œ๊ตญ์–ด๋กœ dcm4che ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ •๋ฆฌํ•ด๋‘” ๊ธ€์ด ๊ฑฐ์˜ ์—†์—ˆ๋‹ค. ๋ฌผ๋ก  ๋‚ด๊ฐ€ ๋ชป์ฐพ์€ ๊ฒƒ์ผ์ˆ˜๋„ ์žˆ๋‹ค.

์˜๋ฃŒ ์˜์ƒ ์ฒ˜๋ฆฌ? ์•ผ ๋„ˆ๋‘ ํ•  ์ˆ˜ ์žˆ์–ด

dcm4che ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ ์€ DICOM ํŒŒ์ผ ํ•ธ๋“ค๋ง์ด ์‰ฝ๊ฒŒ ๋А๊ปด์ง€๋„๋ก ๋งŽ์€ ๊ธฐ๋Šฅ์„ ์ง€์›ํ•ด์ค€๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๋‚˜์˜ ๊ฒฝ์šฐ, ํƒœ๊ทธ ํŒŒ์‹ฑ ์ž‘์—…์„ ์œ„ํ•ด ๊ฐ ํƒœ๊ทธ๋ฅผ ์–ด๋–ป๊ฒŒ ์ž…๋ ฅํ•ด์ค˜์•ผ ํ•˜๋Š” ์ง€์— ๋Œ€ํ•ด ๊ถ๊ธˆํ–ˆ์—ˆ๋Š”๋ฐ, DICOM ํ˜•์‹์—์„œ ๊ณต์‹์ ์œผ๋กœ ์ง€์›ํ•˜๋Š” ํƒœ๊ทธ๋“ค์„ Tag ํด๋ž˜์Šค๋กœ ๋ฌถ์–ด๋‘ฌ์„œ ๊ต‰์žฅํžˆ ํŽธ๋ฆฌํ–ˆ๋‹ค.
์•„์‰ฌ์šด ์ ์€ ๋น„์Šทํ•œ ์ด๋ฆ„์˜ ํด๋ž˜์Šค๋“ค์ด ๋งŽ์•„ ์–ด๋–ค ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ์ง€ ๋ชจํ˜ธํ–ˆ๋‹ค๋Š” ์ ์ด๋‹ค. ํ•˜์ง€๋งŒ ์ด๋„ ๊ฐœ๋ฐœ ์ดˆ๊ธฐ์—๋งŒ ๋ถ€๊ฐ๋˜์—ˆ๋˜ ๋‹จ์ ์ด๋ผ ์žฅ์ ์ด ๋” ๋งŽ๊ฒŒ ๋А๊ปด์กŒ๋‹ค.

์•ž์œผ๋กœ๋„ DICOM ํŒŒ์ผ์„ ๋‹ค๋ฃฐ ์ผ์ด ๋งŽ์„ ๊ฒƒ ๊ฐ™์€๋ฐ, dcm4che ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋” ์ž˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐœ๊ฒฌํ•˜๋ฉด ๊ธ€์„ ์ž‘์„ฑํ•ด๋ณด๊ณ ์ž ํ•œ๋‹ค.


Reference : ์˜์ƒ ์ค‘์—์„œ๋„ ์˜๋ฃŒ ์˜์ƒ?, DICOM์ด๋ž€?

profile
๋‚˜๋Š”์•ผ ๋งํ•˜๋Š” ๊ฐœ๋ฐœ(๊ฐ)์ž

0๊ฐœ์˜ ๋Œ“๊ธ€