Mercurial > hg > orthanc-java
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 } |