Mercurial > hg > orthanc-java
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Samples/MammographyDeepLearning/src/main/java/OrthancConnection.java Wed Jun 12 13:58:29 2024 +0200 @@ -0,0 +1,277 @@ +/** + * 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.JSONObject; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; + +public abstract class OrthancConnection { + private static final String PIXEL_REPRESENTATION = "0028,0103"; + private static final String BITS_STORED = "0028,0101"; + private static final String SAMPLES_PER_PIXEL = "0028,0002"; + + public static OrthancConnection createHttpClient(ExecutorService executor, + String baseUrl) { + return new OrthancConnection() { + private HttpClient client = HttpClient.newBuilder().executor(executor).build(); + + @Override + public byte[] doGetAsByteArray(String uri) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + uri)) + .build(); + HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); + if (response.statusCode() != 200) { + throw new RuntimeException(); + } else { + return response.body(); + } + } + + @Override + public byte[] doPostAsByteArray(String uri, byte[] body) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + uri)) + .POST(HttpRequest.BodyPublishers.ofByteArray(body)) + .build(); + HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); + if (response.statusCode() != 200) { + throw new RuntimeException(); + } else { + return response.body(); + } + } + }; + } + + public static OrthancConnection createForPlugin() { + return new OrthancConnection() { + @Override + public byte[] doGetAsByteArray(String uri) throws IOException, InterruptedException { + return be.uclouvain.orthanc.Functions.restApiGet(uri); + } + + @Override + public byte[] doPostAsByteArray(String uri, byte[] body) throws IOException, InterruptedException { + return be.uclouvain.orthanc.Functions.restApiPost(uri, body); + } + }; + } + + public abstract byte[] doGetAsByteArray(String uri) throws IOException, InterruptedException; + + public abstract byte[] doPostAsByteArray(String uri, + byte[] body) throws IOException, InterruptedException; + + public String doGetAsString(String uri) throws IOException, InterruptedException { + return new String(doGetAsByteArray(uri), StandardCharsets.UTF_8); + } + + public JSONObject doGetAsJsonObject(String uri) throws IOException, InterruptedException { + return new JSONObject(doGetAsString(uri)); + } + + public String doPostAsString(String uri, + String body) throws IOException, InterruptedException { + byte[] answer = doPostAsByteArray(uri, body.getBytes(StandardCharsets.UTF_8)); + return new String(answer, StandardCharsets.UTF_8); + } + + public JSONObject doPostAsJsonObject(String uri, + String body) throws IOException, InterruptedException { + return new JSONObject(doPostAsString(uri, body)); + } + + public BufferedImage getGrayscaleFrame(String instance, + int frame) throws IOException, InterruptedException { + if (frame < 0) { + throw new IllegalArgumentException(); + } + + JSONObject tags = doGetAsJsonObject("/instances/" + instance + "/tags?short"); + + if (!tags.has(PIXEL_REPRESENTATION) || + tags.getInt(PIXEL_REPRESENTATION) == 1) { + throw new IllegalArgumentException("Negative pixels not supported"); + } + + if (!tags.has(BITS_STORED) || + (tags.getInt(BITS_STORED) != 8 && + tags.getInt(BITS_STORED) != 16)) { + throw new IllegalArgumentException("Pixel depth not supported"); + } + + if (!tags.has(SAMPLES_PER_PIXEL) || + tags.getInt(SAMPLES_PER_PIXEL) != 1) { + throw new IllegalArgumentException("Color images not implemented"); + } + + byte[] png = doGetAsByteArray("/instances/" + instance + "/frames/" + frame + "/image-uint16"); + try (ByteArrayInputStream stream = new ByteArrayInputStream(png)) { + return ImageIO.read(stream); + } + } + + boolean isOrthancVersionAbove(int major, + int minor, + int revision) throws IOException, InterruptedException { + JSONObject system = doGetAsJsonObject("/system"); + String version = system.getString("Version"); + if (version == null) { + throw new RuntimeException("Not an Orthanc server"); + } + + if (version.equals("mainline")) { + return true; + } else { + String[] items = version.split("\\."); + if (items.length != 3) { + throw new RuntimeException("Cannot parse Orthanc version: " + version); + } + + int thisMajor = Integer.valueOf(items[0]); + int thisMinor = Integer.valueOf(items[1]); + int thisRevision = Integer.valueOf(items[2]); + + return (thisMajor > major || + (thisMajor == major && thisMinor > minor) || + (thisMajor == major && thisMinor == minor && thisRevision >= revision)); + } + } + + + public static void download(String targetPath, + ExecutorService executor, + String url, + long expectedSize, + String expectedMd5) throws IOException, NoSuchAlgorithmException, InterruptedException { + class FileDownloader implements HttpResponse.BodyHandler<Void>, Consumer<Optional<byte[]>> { + private FileOutputStream target; + private String url; + private boolean success; + private long contentLength; + private long size; + + public FileDownloader(final FileOutputStream target, + final String url) throws FileNotFoundException { + this.target = target; + this.success = false; + } + + @Override + public HttpResponse.BodySubscriber<Void> apply(final HttpResponse.ResponseInfo responseInfo) { + if (responseInfo.statusCode() != 200) { + throw new RuntimeException("URL does not exist: " + url); + } + + final OptionalLong contentLength = responseInfo.headers().firstValueAsLong("Content-Length"); + if (contentLength.isEmpty()) { + throw new RuntimeException("Server does not provide a content length: " + url); + } + this.contentLength = contentLength.getAsLong(); + + return HttpResponse.BodySubscribers.ofByteArrayConsumer(this); + } + + @Override + public void accept(final Optional<byte[]> bytes) { + if (bytes.isEmpty()) { + System.out.println(); + System.out.flush(); + if (this.success) { + throw new IllegalStateException("File already closed"); + } + if (this.size != this.contentLength) { + throw new RuntimeException("Server has not answered with the proper content length"); + } + this.success = true; + } else { + try { + this.target.write(bytes.get()); + } catch (IOException e) { + System.out.println(); + throw new RuntimeException("Cannot write to file"); + } + this.size += bytes.get().length; + final int BAR_WIDTH = 30; + final int a = Math.min(30, Math.round(this.size / (float) this.contentLength * 30.0f)); + System.out.print("\r Progress: [" + "=".repeat(a) + " ".repeat(30 - a) + "]"); + System.out.flush(); + } + } + + boolean isSuccess() { + return this.success; + } + } + + + System.out.println("Downloading: " + url); + + if (Files.exists(Paths.get(targetPath))) { + System.out.println(" File already downloaded"); + } else { + FileOutputStream target = new FileOutputStream(targetPath); + HttpClient client = HttpClient.newBuilder().executor(executor).followRedirects(HttpClient.Redirect.NORMAL).build(); + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build(); + + FileDownloader consumer = new FileDownloader(target, url); + HttpResponse<Void> response = client.send(request, (HttpResponse.BodyHandler<Void>) consumer); + target.close(); + + if (!consumer.isSuccess()) { + throw new IOException("Could not download: " + url); + } + } + + byte[] content = Files.readAllBytes(Paths.get(targetPath)); + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(content); + + final String actualMd5 = new BigInteger(1, md.digest()).toString(16); + if (content.length != expectedSize || + !actualMd5.equals(expectedMd5)) { + throw new IOException("Incorrect content in a download file, please remove it and retry: " + targetPath); + } + } +}