comparison 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
comparison
equal deleted inserted replaced
27:4a750ca9461e 28:43923934e934
1 /**
2 * SPDX-FileCopyrightText: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium
3 * SPDX-License-Identifier: GPL-3.0-or-later
4 **/
5
6 /**
7 * Java plugin for Orthanc
8 * Copyright (C) 2023-2024 Sebastien Jodogne, UCLouvain, Belgium
9 *
10 * This program is free software: you can redistribute it and/or
11 * modify it under the terms of the GNU General Public License as
12 * published by the Free Software Foundation, either version 3 of the
13 * License, or (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful, but
16 * WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 * General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 **/
23
24
25 import org.json.JSONArray;
26 import org.json.JSONObject;
27
28 import java.io.IOException;
29 import java.util.List;
30
31 public class DicomToolbox {
32 private static final String MAPPING_RESOURCE = "0008,0105";
33 private static final String REFERENCED_SOP_CLASS_UID = "0008,1150";
34 private static final String REFERENCED_SOP_INSTANCE_UID = "0008,1155";
35 private static final String REFERENCED_SOP_SEQUENCE = "0008,1199";
36
37 private static final String CONTENT_TEMPLATE_SEQUENCE = "0040,a504";
38 private static final String TEMPLATE_IDENTIFIER = "0040,db00";
39 private static final String RELATIONSHIP_TYPE = "0040,a010";
40 private static final String CONCEPT_NAME_CODE_SEQUENCE = "0040,a043";
41 private static final String VALUE_TYPE = "0040,a040";
42 private static final String CONCEPT_CODE_SEQUENCE = "0040,a168";
43 private static final String CONTINUITY_OF_CONTENT = "0040,a050";
44 private static final String CONTENT_SEQUENCE = "0040,a730";
45 private static final String TEXT_VALUE_ATTRIBUTE = "0040,a160";
46 private static final String UID_ATTRIBUTE = "0040,a124";
47 private static final String MEASURED_VALUE_SEQUENCE = "0040,a300";
48 private static final String MEASUREMENT_UNITS_CODE_SEQUENCE = "0040,08ea";
49 private static final String FLOATING_POINT_VALUE = "0040,a161";
50 private static final String NUMERIC_VALUE = "0040,a30a";
51 private static final String GRAPHIC_DATA = "0070,0022";
52 private static final String GRAPHIC_TYPE = "0070,0023";
53
54 static JSONObject createDicomSR(OrthancConnection client,
55 String instance,
56 List<Detection> detections,
57 double aspectRatio) throws IOException, InterruptedException {
58 final JSONObject instanceTags = client.doGetAsJsonObject("/instances/" + instance + "/tags?short");
59 final String trackingUid = client.doGetAsString("/tools/generate-uid?level=instance");
60
61 String instanceStudy = client.doGetAsJsonObject("/instances/" + instance + "/study").getString("ID");
62
63 // https://dicom.nema.org/medical/dicom/2024a/output/chtml/part16/chapter_A.html#sect_TID_1500
64 // Sample TID 1500: https://dicom.nema.org/medical/dicom/current/output/chtml/part21/sect_a.7.2.html
65
66
67 JSONObject json = new JSONObject();
68 json.put("Parent", instanceStudy);
69
70 JSONObject tags = new JSONObject();
71 tags.put("0008,0016", "1.2.840.10008.5.1.4.1.1.88.33"); // SOP Class UID
72 tags.put("0008,0060", "SR"); // Modality
73 tags.put("0008,0070", ""); // Manufacturer
74 tags.put("0008,1111", new JSONArray()); // Referenced Performed Procedure Step Sequence is type 2 (required but can be empty)
75 tags.put("0020,0011", "1"); // Series number
76 tags.put("0020,0013", "1"); // Instance number
77 tags.put("0040,a040", "CONTAINER"); // Value type for SR: https://dicom.nema.org/medical/dicom/2024a/output/chtml/part03/sect_C.17.3.html
78
79 // CID 7021 Measurement Report Document Title: https://dicom.nema.org/medical/dicom/2024a/output/chtml/part16/sect_CID_7021.html
80 tags.append("0040,a043", new DicomCode("126000", "DCM", "Imaging Measurement Report").toJson());
81
82 tags.put("0040,a050", "CONTINUOUS"); // Continuity of content
83 tags.put("0040,a372", new JSONArray()); // Performed procedure code sequence
84
85 // Current Requested Procedure Evidence: References to the StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, and SOPClassUID
86 tags.append("0040,a375", new JSONObject().
87 put("0020,000d", instanceTags.getString("0020,000d")).
88 append("0008,1115", new JSONObject().
89 put("0020,000e", instanceTags.getString("0020,000e")).
90 append("0008,1199", new JSONObject().
91 put("0008,1150", instanceTags.getString("0008,0016")).
92 put("0008,1155", instanceTags.getString("0008,0018")))));
93
94 tags.put("0040,a491", "PARTIAL"); // Completion flag
95 tags.put("0040,a493", "UNVERIFIED"); // Verification flag
96 tags.put("0040,a496", "PRELIMINARY"); // Preliminary flag
97
98 // Content template sequence, which indicates TID 1500
99 setTemplateId(tags, "1500");
100
101 JSONArray contentSequence = new JSONArray();
102 contentSequence.put(createCodeRelationship(
103 new DicomCode("121049", "DCM", "Language of Content Item and Descendants"),
104 new DicomCode("en-US", "RFC5646", "English (United States)")));
105
106 // This is "procedure_reported" in "highdicom.sr.MeasurementReport()"
107 contentSequence.put(createCodeRelationship(
108 new DicomCode("121058", "DCM", "Procedure reported"),
109 new DicomCode("43468-8", "LN", "XR unspecified body region")));
110
111 JSONArray measurementGroups = new JSONArray();
112
113 for (Detection detection : detections) {
114 JSONArray measurements = new JSONArray();
115 measurements.put(createTextContext(
116 new DicomCode("112039", "DCM", "Tracking Identifier"),
117 "Orthanc Deep Learning for Mammography"));
118
119 measurements.put(createUidReference(
120 new DicomCode("112040", "DCM", "Tracking Unique Identifier"),
121 trackingUid));
122
123 measurements.put(createNumericValue(
124 new DicomCode("111047", "DCM", "Probability of cancer"),
125 new DicomCode("%", "UCUM", "Percent"),
126 detection.getScore() * 100.0));
127
128 measurements.put(createRectangle(
129 new DicomCode("111030", "DCM", "Image Region"),
130 instanceTags.getString("0008,0016"), instanceTags.getString("0008,0018"),
131 detection.getRectangle(), aspectRatio));
132
133 JSONObject measurementGroup = createContainer(new DicomCode("125007", "DCM", "Measurement Group"), measurements);
134 setTemplateId(measurementGroup, "1410");
135
136 measurementGroups.put(measurementGroup);
137 }
138
139 JSONObject imagingMeasurements = createContainer(new DicomCode("126010", "DCM", "Imaging Measurements"), measurementGroups);
140 contentSequence.put(imagingMeasurements);
141
142 tags.put("0040,a730", contentSequence);
143
144 json.put("Tags", tags);
145
146 return client.doPostAsJsonObject("/tools/create-dicom", json.toString());
147 }
148
149 static public class Point2D {
150 private double x;
151 private double y;
152
153 public Point2D(double x,
154 double y) {
155 this.x = x;
156 this.y = y;
157 }
158
159 public double getX() {
160 return x;
161 }
162
163 public double getY() {
164 return y;
165 }
166 }
167
168 public static JSONObject setTemplateId(JSONObject target,
169 String templateId) {
170 if (target.has(CONTENT_TEMPLATE_SEQUENCE)) {
171 throw new IllegalStateException();
172 } else {
173 target.append(CONTENT_TEMPLATE_SEQUENCE, new JSONObject().
174 put(MAPPING_RESOURCE, "DCMR").
175 put(TEMPLATE_IDENTIFIER, templateId));
176 return target;
177 }
178 }
179
180 public static JSONObject createCodeRelationship(DicomCode concept,
181 DicomCode referencedCode) {
182 JSONObject result = new JSONObject();
183 result.put(RELATIONSHIP_TYPE, "HAS CONCEPT MOD");
184 result.append(CONCEPT_NAME_CODE_SEQUENCE, concept.toJson());
185 result.put(VALUE_TYPE, "CODE");
186 result.append(CONCEPT_CODE_SEQUENCE, referencedCode.toJson());
187 return result;
188 }
189
190 public static JSONObject createContainer(DicomCode concept,
191 JSONArray content) {
192 JSONObject result = new JSONObject();
193 result.put(RELATIONSHIP_TYPE, "CONTAINS");
194 result.put(VALUE_TYPE, "CONTAINER");
195 result.append(CONCEPT_NAME_CODE_SEQUENCE, concept.toJson());
196 result.put(CONTINUITY_OF_CONTENT, "CONTINUOUS");
197 result.put(CONTENT_SEQUENCE, content);
198 return result;
199 }
200
201 public static JSONObject createTextContext(DicomCode concept,
202 String value) {
203 JSONObject result = new JSONObject();
204 result.put(RELATIONSHIP_TYPE, "HAS OBS CONTEXT");
205 result.put(VALUE_TYPE, "TEXT");
206 result.append(CONCEPT_NAME_CODE_SEQUENCE, concept.toJson());
207 result.put(TEXT_VALUE_ATTRIBUTE, value);
208 return result;
209 }
210
211 public static JSONObject createUidReference(DicomCode concept,
212 String uid) {
213 JSONObject result = new JSONObject();
214 result.put(RELATIONSHIP_TYPE, "HAS OBS CONTEXT");
215 result.put(VALUE_TYPE, "UIDREF");
216 result.append(CONCEPT_NAME_CODE_SEQUENCE, concept.toJson());
217 result.put(UID_ATTRIBUTE, uid);
218 return result;
219 }
220
221 public static JSONObject createNumericValue(DicomCode concept,
222 DicomCode unit,
223 double value) {
224 String ds = String.valueOf(value);
225 if (ds.length() > 16) {
226 ds = ds.substring(0, 16); // The DS VR must have less than 16 characters
227 }
228
229 JSONObject result = new JSONObject();
230 result.put(RELATIONSHIP_TYPE, "CONTAINS");
231 result.put(VALUE_TYPE, "NUM");
232 result.append(CONCEPT_NAME_CODE_SEQUENCE, concept.toJson());
233 result.append(MEASURED_VALUE_SEQUENCE, new JSONObject().
234 append(MEASUREMENT_UNITS_CODE_SEQUENCE, unit.toJson()).
235 put(FLOATING_POINT_VALUE, String.valueOf(value)).
236 put(NUMERIC_VALUE, ds));
237 return result;
238 }
239
240 public static JSONObject createSelectedFromImage(String sopClassUid,
241 String sopInstanceUid) {
242 JSONObject result = new JSONObject();
243 result.put(RELATIONSHIP_TYPE, "SELECTED FROM");
244 result.put(VALUE_TYPE, "IMAGE");
245 result.append(REFERENCED_SOP_SEQUENCE, new JSONObject().
246 put(REFERENCED_SOP_CLASS_UID, sopClassUid).
247 put(REFERENCED_SOP_INSTANCE_UID, sopInstanceUid));
248 result.append(CONCEPT_NAME_CODE_SEQUENCE, new DicomCode("111040", "DCM", "Original Source").toJson());
249 return result;
250 }
251
252 public static JSONObject createPolyline2D(DicomCode concept,
253 String sopClassUid,
254 String sopInstanceUid,
255 Point2D vertices[]) {
256 String polyline = new String();
257 for (int i = 0; i < vertices.length; i++) {
258 if (!polyline.isEmpty()) {
259 polyline = polyline + "\\";
260 }
261 polyline += String.valueOf(vertices[i].getX()) + "\\" + String.valueOf(vertices[i].getY());
262 }
263
264 JSONObject result = new JSONObject();
265 result.put(RELATIONSHIP_TYPE, "CONTAINS");
266 result.put(VALUE_TYPE, "SCOORD");
267 result.append(CONCEPT_NAME_CODE_SEQUENCE, concept.toJson());
268 result.append(CONTENT_SEQUENCE, createSelectedFromImage(sopClassUid, sopInstanceUid));
269 result.put(GRAPHIC_DATA, polyline); // WARNING: This necessitates Orthanc > 1.12.4: https://orthanc.uclouvain.be/hg/orthanc/rev/dedbf019a707
270 result.put(GRAPHIC_TYPE, "POLYLINE");
271 return result;
272 }
273
274 public static JSONObject createRectangle(DicomCode concept,
275 String sopClassUid,
276 String sopInstanceUid,
277 Rectangle rectangle,
278 double aspectRatio) {
279 Point2D vertices[] = new Point2D[5];
280 vertices[0] = new Point2D(rectangle.getX1() * aspectRatio, rectangle.getY1() * aspectRatio);
281 vertices[1] = new Point2D(rectangle.getX2() * aspectRatio, rectangle.getY1() * aspectRatio);
282 vertices[2] = new Point2D(rectangle.getX2() * aspectRatio, rectangle.getY2() * aspectRatio);
283 vertices[3] = new Point2D(rectangle.getX1() * aspectRatio, rectangle.getY2() * aspectRatio);
284 vertices[4] = new Point2D(rectangle.getX1() * aspectRatio, rectangle.getY1() * aspectRatio);
285 return createPolyline2D(concept, sopClassUid, sopInstanceUid, vertices);
286 }
287 }