diff Samples/MammographyDeepLearning/src/main/java/DicomToolbox.java @ 28:43923934e934

added sample: deep learning for mammography
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 12 Jun 2024 13:58:29 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/MammographyDeepLearning/src/main/java/DicomToolbox.java	Wed Jun 12 13:58:29 2024 +0200
@@ -0,0 +1,287 @@
+/**
+ * SPDX-FileCopyrightText: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ **/
+
+/**
+ * Java plugin for Orthanc
+ * Copyright (C) 2023-2024 Sebastien Jodogne, UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.List;
+
+public class DicomToolbox {
+    private static final String MAPPING_RESOURCE = "0008,0105";
+    private static final String REFERENCED_SOP_CLASS_UID = "0008,1150";
+    private static final String REFERENCED_SOP_INSTANCE_UID = "0008,1155";
+    private static final String REFERENCED_SOP_SEQUENCE = "0008,1199";
+
+    private static final String CONTENT_TEMPLATE_SEQUENCE = "0040,a504";
+    private static final String TEMPLATE_IDENTIFIER = "0040,db00";
+    private static final String RELATIONSHIP_TYPE = "0040,a010";
+    private static final String CONCEPT_NAME_CODE_SEQUENCE = "0040,a043";
+    private static final String VALUE_TYPE = "0040,a040";
+    private static final String CONCEPT_CODE_SEQUENCE = "0040,a168";
+    private static final String CONTINUITY_OF_CONTENT = "0040,a050";
+    private static final String CONTENT_SEQUENCE = "0040,a730";
+    private static final String TEXT_VALUE_ATTRIBUTE = "0040,a160";
+    private static final String UID_ATTRIBUTE = "0040,a124";
+    private static final String MEASURED_VALUE_SEQUENCE = "0040,a300";
+    private static final String MEASUREMENT_UNITS_CODE_SEQUENCE = "0040,08ea";
+    private static final String FLOATING_POINT_VALUE = "0040,a161";
+    private static final String NUMERIC_VALUE = "0040,a30a";
+    private static final String GRAPHIC_DATA = "0070,0022";
+    private static final String GRAPHIC_TYPE = "0070,0023";
+
+    static JSONObject createDicomSR(OrthancConnection client,
+                                    String instance,
+                                    List<Detection> detections,
+                                    double aspectRatio) throws IOException, InterruptedException {
+        final JSONObject instanceTags = client.doGetAsJsonObject("/instances/" + instance + "/tags?short");
+        final String trackingUid = client.doGetAsString("/tools/generate-uid?level=instance");
+
+        String instanceStudy = client.doGetAsJsonObject("/instances/" + instance + "/study").getString("ID");
+
+        // https://dicom.nema.org/medical/dicom/2024a/output/chtml/part16/chapter_A.html#sect_TID_1500
+        // Sample TID 1500: https://dicom.nema.org/medical/dicom/current/output/chtml/part21/sect_a.7.2.html
+
+
+        JSONObject json = new JSONObject();
+        json.put("Parent", instanceStudy);
+
+        JSONObject tags = new JSONObject();
+        tags.put("0008,0016", "1.2.840.10008.5.1.4.1.1.88.33");  // SOP Class UID
+        tags.put("0008,0060", "SR"); // Modality
+        tags.put("0008,0070", "");   // Manufacturer
+        tags.put("0008,1111", new JSONArray());  // Referenced Performed Procedure Step Sequence is type 2 (required but can be empty)
+        tags.put("0020,0011", "1");  // Series number
+        tags.put("0020,0013", "1");  // Instance number
+        tags.put("0040,a040", "CONTAINER");  // Value type for SR: https://dicom.nema.org/medical/dicom/2024a/output/chtml/part03/sect_C.17.3.html
+
+        // CID 7021 Measurement Report Document Title: https://dicom.nema.org/medical/dicom/2024a/output/chtml/part16/sect_CID_7021.html
+        tags.append("0040,a043", new DicomCode("126000", "DCM", "Imaging Measurement Report").toJson());
+
+        tags.put("0040,a050", "CONTINUOUS");  // Continuity of content
+        tags.put("0040,a372", new JSONArray());  // Performed procedure code sequence
+
+        // Current Requested Procedure Evidence: References to the StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, and SOPClassUID
+        tags.append("0040,a375", new JSONObject().
+                put("0020,000d", instanceTags.getString("0020,000d")).
+                append("0008,1115", new JSONObject().
+                        put("0020,000e", instanceTags.getString("0020,000e")).
+                        append("0008,1199", new JSONObject().
+                                put("0008,1150", instanceTags.getString("0008,0016")).
+                                put("0008,1155", instanceTags.getString("0008,0018")))));
+
+        tags.put("0040,a491", "PARTIAL");      // Completion flag
+        tags.put("0040,a493", "UNVERIFIED");   // Verification flag
+        tags.put("0040,a496", "PRELIMINARY");  // Preliminary flag
+
+        // Content template sequence, which indicates TID 1500
+        setTemplateId(tags, "1500");
+
+        JSONArray contentSequence = new JSONArray();
+        contentSequence.put(createCodeRelationship(
+                new DicomCode("121049", "DCM", "Language of Content Item and Descendants"),
+                new DicomCode("en-US", "RFC5646", "English (United States)")));
+
+        // This is "procedure_reported" in "highdicom.sr.MeasurementReport()"
+        contentSequence.put(createCodeRelationship(
+                new DicomCode("121058", "DCM", "Procedure reported"),
+                new DicomCode("43468-8", "LN", "XR unspecified body region")));
+
+        JSONArray measurementGroups = new JSONArray();
+
+        for (Detection detection : detections) {
+            JSONArray measurements = new JSONArray();
+            measurements.put(createTextContext(
+                    new DicomCode("112039", "DCM", "Tracking Identifier"),
+                    "Orthanc Deep Learning for Mammography"));
+
+            measurements.put(createUidReference(
+                    new DicomCode("112040", "DCM", "Tracking Unique Identifier"),
+                    trackingUid));
+
+            measurements.put(createNumericValue(
+                    new DicomCode("111047", "DCM", "Probability of cancer"),
+                    new DicomCode("%", "UCUM", "Percent"),
+                    detection.getScore() * 100.0));
+
+            measurements.put(createRectangle(
+                    new DicomCode("111030", "DCM", "Image Region"),
+                    instanceTags.getString("0008,0016"), instanceTags.getString("0008,0018"),
+                    detection.getRectangle(), aspectRatio));
+
+            JSONObject measurementGroup = createContainer(new DicomCode("125007", "DCM", "Measurement Group"), measurements);
+            setTemplateId(measurementGroup, "1410");
+
+            measurementGroups.put(measurementGroup);
+        }
+
+        JSONObject imagingMeasurements = createContainer(new DicomCode("126010", "DCM", "Imaging Measurements"), measurementGroups);
+        contentSequence.put(imagingMeasurements);
+
+        tags.put("0040,a730", contentSequence);
+
+        json.put("Tags", tags);
+
+        return client.doPostAsJsonObject("/tools/create-dicom", json.toString());
+    }
+
+    static public class Point2D {
+        private double x;
+        private double y;
+
+        public Point2D(double x,
+                       double y) {
+            this.x = x;
+            this.y = y;
+        }
+
+        public double getX() {
+            return x;
+        }
+
+        public double getY() {
+            return y;
+        }
+    }
+
+    public static JSONObject setTemplateId(JSONObject target,
+                                           String templateId) {
+        if (target.has(CONTENT_TEMPLATE_SEQUENCE)) {
+            throw new IllegalStateException();
+        } else {
+            target.append(CONTENT_TEMPLATE_SEQUENCE, new JSONObject().
+                    put(MAPPING_RESOURCE, "DCMR").
+                    put(TEMPLATE_IDENTIFIER, templateId));
+            return target;
+        }
+    }
+
+    public static JSONObject createCodeRelationship(DicomCode concept,
+                                                    DicomCode referencedCode) {
+        JSONObject result = new JSONObject();
+        result.put(RELATIONSHIP_TYPE, "HAS CONCEPT MOD");
+        result.append(CONCEPT_NAME_CODE_SEQUENCE, concept.toJson());
+        result.put(VALUE_TYPE, "CODE");
+        result.append(CONCEPT_CODE_SEQUENCE, referencedCode.toJson());
+        return result;
+    }
+
+    public static JSONObject createContainer(DicomCode concept,
+                                             JSONArray content) {
+        JSONObject result = new JSONObject();
+        result.put(RELATIONSHIP_TYPE, "CONTAINS");
+        result.put(VALUE_TYPE, "CONTAINER");
+        result.append(CONCEPT_NAME_CODE_SEQUENCE, concept.toJson());
+        result.put(CONTINUITY_OF_CONTENT, "CONTINUOUS");
+        result.put(CONTENT_SEQUENCE, content);
+        return result;
+    }
+
+    public static JSONObject createTextContext(DicomCode concept,
+                                               String value) {
+        JSONObject result = new JSONObject();
+        result.put(RELATIONSHIP_TYPE, "HAS OBS CONTEXT");
+        result.put(VALUE_TYPE, "TEXT");
+        result.append(CONCEPT_NAME_CODE_SEQUENCE, concept.toJson());
+        result.put(TEXT_VALUE_ATTRIBUTE, value);
+        return result;
+    }
+
+    public static JSONObject createUidReference(DicomCode concept,
+                                                String uid) {
+        JSONObject result = new JSONObject();
+        result.put(RELATIONSHIP_TYPE, "HAS OBS CONTEXT");
+        result.put(VALUE_TYPE, "UIDREF");
+        result.append(CONCEPT_NAME_CODE_SEQUENCE, concept.toJson());
+        result.put(UID_ATTRIBUTE, uid);
+        return result;
+    }
+
+    public static JSONObject createNumericValue(DicomCode concept,
+                                                DicomCode unit,
+                                                double value) {
+        String ds = String.valueOf(value);
+        if (ds.length() > 16) {
+            ds = ds.substring(0, 16);  // The DS VR must have less than 16 characters
+        }
+
+        JSONObject result = new JSONObject();
+        result.put(RELATIONSHIP_TYPE, "CONTAINS");
+        result.put(VALUE_TYPE, "NUM");
+        result.append(CONCEPT_NAME_CODE_SEQUENCE, concept.toJson());
+        result.append(MEASURED_VALUE_SEQUENCE, new JSONObject().
+                append(MEASUREMENT_UNITS_CODE_SEQUENCE, unit.toJson()).
+                put(FLOATING_POINT_VALUE, String.valueOf(value)).
+                put(NUMERIC_VALUE, ds));
+        return result;
+    }
+
+    public static JSONObject createSelectedFromImage(String sopClassUid,
+                                                     String sopInstanceUid) {
+        JSONObject result = new JSONObject();
+        result.put(RELATIONSHIP_TYPE, "SELECTED FROM");
+        result.put(VALUE_TYPE, "IMAGE");
+        result.append(REFERENCED_SOP_SEQUENCE, new JSONObject().
+                put(REFERENCED_SOP_CLASS_UID, sopClassUid).
+                put(REFERENCED_SOP_INSTANCE_UID, sopInstanceUid));
+        result.append(CONCEPT_NAME_CODE_SEQUENCE, new DicomCode("111040", "DCM", "Original Source").toJson());
+        return result;
+    }
+
+    public static JSONObject createPolyline2D(DicomCode concept,
+                                              String sopClassUid,
+                                              String sopInstanceUid,
+                                              Point2D vertices[]) {
+        String polyline = new String();
+        for (int i = 0; i < vertices.length; i++) {
+            if (!polyline.isEmpty()) {
+                polyline = polyline + "\\";
+            }
+            polyline += String.valueOf(vertices[i].getX()) + "\\" + String.valueOf(vertices[i].getY());
+        }
+
+        JSONObject result = new JSONObject();
+        result.put(RELATIONSHIP_TYPE, "CONTAINS");
+        result.put(VALUE_TYPE, "SCOORD");
+        result.append(CONCEPT_NAME_CODE_SEQUENCE, concept.toJson());
+        result.append(CONTENT_SEQUENCE, createSelectedFromImage(sopClassUid, sopInstanceUid));
+        result.put(GRAPHIC_DATA, polyline);  // WARNING: This necessitates Orthanc > 1.12.4: https://orthanc.uclouvain.be/hg/orthanc/rev/dedbf019a707
+        result.put(GRAPHIC_TYPE, "POLYLINE");
+        return result;
+    }
+
+    public static JSONObject createRectangle(DicomCode concept,
+                                             String sopClassUid,
+                                             String sopInstanceUid,
+                                             Rectangle rectangle,
+                                             double aspectRatio) {
+        Point2D vertices[] = new Point2D[5];
+        vertices[0] = new Point2D(rectangle.getX1() * aspectRatio, rectangle.getY1() * aspectRatio);
+        vertices[1] = new Point2D(rectangle.getX2() * aspectRatio, rectangle.getY1() * aspectRatio);
+        vertices[2] = new Point2D(rectangle.getX2() * aspectRatio, rectangle.getY2() * aspectRatio);
+        vertices[3] = new Point2D(rectangle.getX1() * aspectRatio, rectangle.getY2() * aspectRatio);
+        vertices[4] = new Point2D(rectangle.getX1() * aspectRatio, rectangle.getY1() * aspectRatio);
+        return createPolyline2D(concept, sopClassUid, sopInstanceUid, vertices);
+    }
+}