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 }