Mercurial > hg > orthanc-java
comparison Samples/MammographyDeepLearning/src/main/java/OrthancConnection.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.JSONObject; | |
26 | |
27 import javax.imageio.ImageIO; | |
28 import java.awt.image.BufferedImage; | |
29 import java.io.ByteArrayInputStream; | |
30 import java.io.FileNotFoundException; | |
31 import java.io.FileOutputStream; | |
32 import java.io.IOException; | |
33 import java.math.BigInteger; | |
34 import java.net.URI; | |
35 import java.net.http.HttpClient; | |
36 import java.net.http.HttpRequest; | |
37 import java.net.http.HttpResponse; | |
38 import java.nio.charset.StandardCharsets; | |
39 import java.nio.file.Files; | |
40 import java.nio.file.Paths; | |
41 import java.security.MessageDigest; | |
42 import java.security.NoSuchAlgorithmException; | |
43 import java.util.Optional; | |
44 import java.util.OptionalLong; | |
45 import java.util.concurrent.ExecutorService; | |
46 import java.util.function.Consumer; | |
47 | |
48 public abstract class OrthancConnection { | |
49 private static final String PIXEL_REPRESENTATION = "0028,0103"; | |
50 private static final String BITS_STORED = "0028,0101"; | |
51 private static final String SAMPLES_PER_PIXEL = "0028,0002"; | |
52 | |
53 public static OrthancConnection createHttpClient(ExecutorService executor, | |
54 String baseUrl) { | |
55 return new OrthancConnection() { | |
56 private HttpClient client = HttpClient.newBuilder().executor(executor).build(); | |
57 | |
58 @Override | |
59 public byte[] doGetAsByteArray(String uri) throws IOException, InterruptedException { | |
60 HttpRequest request = HttpRequest.newBuilder() | |
61 .uri(URI.create(baseUrl + uri)) | |
62 .build(); | |
63 HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); | |
64 if (response.statusCode() != 200) { | |
65 throw new RuntimeException(); | |
66 } else { | |
67 return response.body(); | |
68 } | |
69 } | |
70 | |
71 @Override | |
72 public byte[] doPostAsByteArray(String uri, byte[] body) throws IOException, InterruptedException { | |
73 HttpRequest request = HttpRequest.newBuilder() | |
74 .uri(URI.create(baseUrl + uri)) | |
75 .POST(HttpRequest.BodyPublishers.ofByteArray(body)) | |
76 .build(); | |
77 HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); | |
78 if (response.statusCode() != 200) { | |
79 throw new RuntimeException(); | |
80 } else { | |
81 return response.body(); | |
82 } | |
83 } | |
84 }; | |
85 } | |
86 | |
87 public static OrthancConnection createForPlugin() { | |
88 return new OrthancConnection() { | |
89 @Override | |
90 public byte[] doGetAsByteArray(String uri) throws IOException, InterruptedException { | |
91 return be.uclouvain.orthanc.Functions.restApiGet(uri); | |
92 } | |
93 | |
94 @Override | |
95 public byte[] doPostAsByteArray(String uri, byte[] body) throws IOException, InterruptedException { | |
96 return be.uclouvain.orthanc.Functions.restApiPost(uri, body); | |
97 } | |
98 }; | |
99 } | |
100 | |
101 public abstract byte[] doGetAsByteArray(String uri) throws IOException, InterruptedException; | |
102 | |
103 public abstract byte[] doPostAsByteArray(String uri, | |
104 byte[] body) throws IOException, InterruptedException; | |
105 | |
106 public String doGetAsString(String uri) throws IOException, InterruptedException { | |
107 return new String(doGetAsByteArray(uri), StandardCharsets.UTF_8); | |
108 } | |
109 | |
110 public JSONObject doGetAsJsonObject(String uri) throws IOException, InterruptedException { | |
111 return new JSONObject(doGetAsString(uri)); | |
112 } | |
113 | |
114 public String doPostAsString(String uri, | |
115 String body) throws IOException, InterruptedException { | |
116 byte[] answer = doPostAsByteArray(uri, body.getBytes(StandardCharsets.UTF_8)); | |
117 return new String(answer, StandardCharsets.UTF_8); | |
118 } | |
119 | |
120 public JSONObject doPostAsJsonObject(String uri, | |
121 String body) throws IOException, InterruptedException { | |
122 return new JSONObject(doPostAsString(uri, body)); | |
123 } | |
124 | |
125 public BufferedImage getGrayscaleFrame(String instance, | |
126 int frame) throws IOException, InterruptedException { | |
127 if (frame < 0) { | |
128 throw new IllegalArgumentException(); | |
129 } | |
130 | |
131 JSONObject tags = doGetAsJsonObject("/instances/" + instance + "/tags?short"); | |
132 | |
133 if (!tags.has(PIXEL_REPRESENTATION) || | |
134 tags.getInt(PIXEL_REPRESENTATION) == 1) { | |
135 throw new IllegalArgumentException("Negative pixels not supported"); | |
136 } | |
137 | |
138 if (!tags.has(BITS_STORED) || | |
139 (tags.getInt(BITS_STORED) != 8 && | |
140 tags.getInt(BITS_STORED) != 16)) { | |
141 throw new IllegalArgumentException("Pixel depth not supported"); | |
142 } | |
143 | |
144 if (!tags.has(SAMPLES_PER_PIXEL) || | |
145 tags.getInt(SAMPLES_PER_PIXEL) != 1) { | |
146 throw new IllegalArgumentException("Color images not implemented"); | |
147 } | |
148 | |
149 byte[] png = doGetAsByteArray("/instances/" + instance + "/frames/" + frame + "/image-uint16"); | |
150 try (ByteArrayInputStream stream = new ByteArrayInputStream(png)) { | |
151 return ImageIO.read(stream); | |
152 } | |
153 } | |
154 | |
155 boolean isOrthancVersionAbove(int major, | |
156 int minor, | |
157 int revision) throws IOException, InterruptedException { | |
158 JSONObject system = doGetAsJsonObject("/system"); | |
159 String version = system.getString("Version"); | |
160 if (version == null) { | |
161 throw new RuntimeException("Not an Orthanc server"); | |
162 } | |
163 | |
164 if (version.equals("mainline")) { | |
165 return true; | |
166 } else { | |
167 String[] items = version.split("\\."); | |
168 if (items.length != 3) { | |
169 throw new RuntimeException("Cannot parse Orthanc version: " + version); | |
170 } | |
171 | |
172 int thisMajor = Integer.valueOf(items[0]); | |
173 int thisMinor = Integer.valueOf(items[1]); | |
174 int thisRevision = Integer.valueOf(items[2]); | |
175 | |
176 return (thisMajor > major || | |
177 (thisMajor == major && thisMinor > minor) || | |
178 (thisMajor == major && thisMinor == minor && thisRevision >= revision)); | |
179 } | |
180 } | |
181 | |
182 | |
183 public static void download(String targetPath, | |
184 ExecutorService executor, | |
185 String url, | |
186 long expectedSize, | |
187 String expectedMd5) throws IOException, NoSuchAlgorithmException, InterruptedException { | |
188 class FileDownloader implements HttpResponse.BodyHandler<Void>, Consumer<Optional<byte[]>> { | |
189 private FileOutputStream target; | |
190 private String url; | |
191 private boolean success; | |
192 private long contentLength; | |
193 private long size; | |
194 | |
195 public FileDownloader(final FileOutputStream target, | |
196 final String url) throws FileNotFoundException { | |
197 this.target = target; | |
198 this.success = false; | |
199 } | |
200 | |
201 @Override | |
202 public HttpResponse.BodySubscriber<Void> apply(final HttpResponse.ResponseInfo responseInfo) { | |
203 if (responseInfo.statusCode() != 200) { | |
204 throw new RuntimeException("URL does not exist: " + url); | |
205 } | |
206 | |
207 final OptionalLong contentLength = responseInfo.headers().firstValueAsLong("Content-Length"); | |
208 if (contentLength.isEmpty()) { | |
209 throw new RuntimeException("Server does not provide a content length: " + url); | |
210 } | |
211 this.contentLength = contentLength.getAsLong(); | |
212 | |
213 return HttpResponse.BodySubscribers.ofByteArrayConsumer(this); | |
214 } | |
215 | |
216 @Override | |
217 public void accept(final Optional<byte[]> bytes) { | |
218 if (bytes.isEmpty()) { | |
219 System.out.println(); | |
220 System.out.flush(); | |
221 if (this.success) { | |
222 throw new IllegalStateException("File already closed"); | |
223 } | |
224 if (this.size != this.contentLength) { | |
225 throw new RuntimeException("Server has not answered with the proper content length"); | |
226 } | |
227 this.success = true; | |
228 } else { | |
229 try { | |
230 this.target.write(bytes.get()); | |
231 } catch (IOException e) { | |
232 System.out.println(); | |
233 throw new RuntimeException("Cannot write to file"); | |
234 } | |
235 this.size += bytes.get().length; | |
236 final int BAR_WIDTH = 30; | |
237 final int a = Math.min(30, Math.round(this.size / (float) this.contentLength * 30.0f)); | |
238 System.out.print("\r Progress: [" + "=".repeat(a) + " ".repeat(30 - a) + "]"); | |
239 System.out.flush(); | |
240 } | |
241 } | |
242 | |
243 boolean isSuccess() { | |
244 return this.success; | |
245 } | |
246 } | |
247 | |
248 | |
249 System.out.println("Downloading: " + url); | |
250 | |
251 if (Files.exists(Paths.get(targetPath))) { | |
252 System.out.println(" File already downloaded"); | |
253 } else { | |
254 FileOutputStream target = new FileOutputStream(targetPath); | |
255 HttpClient client = HttpClient.newBuilder().executor(executor).followRedirects(HttpClient.Redirect.NORMAL).build(); | |
256 HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build(); | |
257 | |
258 FileDownloader consumer = new FileDownloader(target, url); | |
259 HttpResponse<Void> response = client.send(request, (HttpResponse.BodyHandler<Void>) consumer); | |
260 target.close(); | |
261 | |
262 if (!consumer.isSuccess()) { | |
263 throw new IOException("Could not download: " + url); | |
264 } | |
265 } | |
266 | |
267 byte[] content = Files.readAllBytes(Paths.get(targetPath)); | |
268 MessageDigest md = MessageDigest.getInstance("MD5"); | |
269 md.update(content); | |
270 | |
271 final String actualMd5 = new BigInteger(1, md.digest()).toString(16); | |
272 if (content.length != expectedSize || | |
273 !actualMd5.equals(expectedMd5)) { | |
274 throw new IOException("Incorrect content in a download file, please remove it and retry: " + targetPath); | |
275 } | |
276 } | |
277 } |