From 68a8a697fe1041b0fa84f50854774cbe1ed0acb2 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Fri, 7 Jan 2022 00:18:47 +0000 Subject: [PATCH 01/53] Implemented skeleton of MLProvider Interface and Servlets --- .../ua/dicoogle/server/web/DicoogleWeb.java | 9 ++- .../mlprovider/CreateDatasetServlet.java | 21 +++++ .../mlprovider/MakeBulkPredictionServlet.java | 20 +++++ .../mlprovider/MakePredictionServlet.java | 20 +++++ .../java/pt/ua/dicoogle/sdk/PluginBase.java | 7 ++ .../java/pt/ua/dicoogle/sdk/PluginSet.java | 11 +++ .../dicoogle/sdk/mlprovider/MLAnnotation.java | 8 ++ .../dicoogle/sdk/mlprovider/MLCSVDataset.java | 9 +++ .../ua/dicoogle/sdk/mlprovider/MLDataset.java | 7 ++ .../dicoogle/sdk/mlprovider/MLEndpoint.java | 7 ++ .../sdk/mlprovider/MLImageDataset.java | 8 ++ .../ua/dicoogle/sdk/mlprovider/MLModel.java | 16 ++++ .../dicoogle/sdk/mlprovider/MLPrediction.java | 4 + .../dicoogle/sdk/mlprovider/ML_DATA_TYPE.java | 6 ++ .../mlprovider/MachineLearningProvider.java | 79 +++++++++++++++++++ 15 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLAnnotation.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MachineLearningProvider.java diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index 87dff12a7..efbc3042b 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java @@ -52,6 +52,7 @@ import pt.ua.dicoogle.server.web.servlets.management.ServicesServlet; import pt.ua.dicoogle.server.web.servlets.management.TransferOptionsServlet; +import pt.ua.dicoogle.server.web.servlets.mlprovider.*; import java.io.File; import java.net.URL; @@ -222,7 +223,13 @@ public DicoogleWeb(int port) throws Exception { createServletHandler(new RunningTasksServlet(), "/index/task"), createServletHandler(new ExportServlet(ExportType.EXPORT_CVS), "/export/cvs"), createServletHandler(new ExportServlet(ExportType.LIST), "/export/list"), - createServletHandler(new ServerStorageServlet(), "/management/settings/storage/dicom"), webpages}; + createServletHandler(new ServerStorageServlet(), "/management/settings/storage/dicom"), + + //ml provider servlets + createServletHandler(new CreateDatasetServlet(), "/mlprovider/createDataset"), + createServletHandler(new MakePredictionServlet(), "/mlprovider/makePrediction"), + createServletHandler(new MakeBulkPredictionServlet(), "/mlprovider/makeBulkPrediction"), + webpages}; // setup the server server = new Server(port); diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java new file mode 100644 index 000000000..e8d9f9774 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java @@ -0,0 +1,21 @@ +package pt.ua.dicoogle.server.web.servlets.mlprovider; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class CreateDatasetServlet extends HttpServlet { + + private static final Logger log = LoggerFactory.getLogger(CreateDatasetServlet.class); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + } +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java new file mode 100644 index 000000000..8f2615897 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java @@ -0,0 +1,20 @@ +package pt.ua.dicoogle.server.web.servlets.mlprovider; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class MakeBulkPredictionServlet extends HttpServlet { + + private static final Logger log = LoggerFactory.getLogger(MakeBulkPredictionServlet.class); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + } +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java new file mode 100644 index 000000000..844165220 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -0,0 +1,20 @@ +package pt.ua.dicoogle.server.web.servlets.mlprovider; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class MakePredictionServlet extends HttpServlet { + + private static final Logger log = LoggerFactory.getLogger(MakePredictionServlet.class); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java index a3132387e..84e3329e4 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java @@ -18,6 +18,7 @@ */ package pt.ua.dicoogle.sdk; +import pt.ua.dicoogle.sdk.mlprovider.MachineLearningProvider; import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; import java.util.ArrayList; @@ -39,6 +40,7 @@ public abstract class PluginBase implements PluginSet, PlatformCommunicatorInter protected List queryPlugins = new ArrayList<>(); protected List jettyPlugins = new ArrayList<>(); protected List storagePlugins = new ArrayList<>(); + protected List mlPlugins = new ArrayList<>(); protected List services = new ArrayList<>(); protected ConfigurationHolder settings = null; @@ -82,6 +84,11 @@ public Collection getStoragePlugins() { return storagePlugins; } + @Override + public Collection getMLPlugins() { + return mlPlugins; + } + @Override public void setPlatformProxy(DicooglePlatformInterface core) { this.platform = core; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java index d58f268dd..cd888c118 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java @@ -25,6 +25,7 @@ import org.restlet.resource.ServerResource; +import pt.ua.dicoogle.sdk.mlprovider.MachineLearningProvider; import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; /** @@ -97,6 +98,16 @@ public default Collection getJettyPlugins() { return Collections.EMPTY_LIST; } + /** + * Obtains a collection of MachineLearningProvider plugins, so as to integrate Machine Learning providers in Dicoogle. + * This collection must be immutable. + * @return a collection of MachineLearningProvider plugins to the core application + * @see MachineLearningProvider + */ + public default Collection getMLPlugins() { + return Collections.EMPTY_LIST; + } + /** * Defines the plugin's settings. This method will be called once after the plugin set was instantiated * with plugin-scoped settings. Dicoogle users can modify these settings by accessing the XML file with diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLAnnotation.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLAnnotation.java new file mode 100644 index 000000000..185538872 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLAnnotation.java @@ -0,0 +1,8 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +public class MLAnnotation { + + private String label; + private double confidence; + +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java new file mode 100644 index 000000000..3ffc0168d --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java @@ -0,0 +1,9 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +import java.io.InputStream; + +public class MLCSVDataset extends MLDataset{ + + private InputStream csvFile; + +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java new file mode 100644 index 000000000..82c4f8d57 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java @@ -0,0 +1,7 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +public abstract class MLDataset { + + protected String name; + +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java new file mode 100644 index 000000000..a1b0db515 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java @@ -0,0 +1,7 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +public class MLEndpoint { + + + +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java new file mode 100644 index 000000000..8bb053e6d --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java @@ -0,0 +1,8 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +import java.io.InputStream; + +public class MLImageDataset extends MLDataset { + Iterable images; + Iterable labels; +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java new file mode 100644 index 000000000..1d5508fab --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java @@ -0,0 +1,16 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +import java.util.Date; + +public class MLModel { + + private String name; + + private String id; + + private String description; + + private Date creationDate; + + private ML_DATA_TYPE dataType; +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java new file mode 100644 index 000000000..d6ae50d6e --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java @@ -0,0 +1,4 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +public class MLPrediction { +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java new file mode 100644 index 000000000..80e92a48f --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java @@ -0,0 +1,6 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +public enum ML_DATA_TYPE { + CSV, + IMAGE +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MachineLearningProvider.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MachineLearningProvider.java new file mode 100644 index 000000000..b0e6e1638 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MachineLearningProvider.java @@ -0,0 +1,79 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +import pt.ua.dicoogle.sdk.DicooglePlugin; + +import java.util.List; +import java.util.Set; + +/** + * Interface to define Machine Learning providers. + * A machine learning provider can be a remote service, hosted on the cloud, or a simple remote/local server + * that has installed machine learning algorithms for problem solving. + * Machine learning providers can work with either image or csv datasets. + * The purpose of these providers is to provide a way to develop plugins to integrate with services such as Google's Vertex API or Amazon's SageMaker API. + * + * @author Rui Jesus + */ +public abstract class MachineLearningProvider implements DicooglePlugin { + + protected Set acceptedDataTypes; + + /** + * This method creates and uploads a dataset to the machine learning provider. + * A dataset is defined as a set of labelled images or a labelled CSV file with one column used to label the entries. + */ + public abstract void createDataset(); + + /** + * This method creates a model using a specific dataset + */ + public abstract MLModel createModel(); + + /** + * This method creates a model using a specific dataset + */ + public abstract void createEndpoint(); + + /** + * This method creates a model using a specific dataset + */ + public abstract List listEndpoints(); + + /** + * This method creates a model using a specific dataset + */ + public abstract void deleteEndpoint(); + + /** + * This method deploys a model + */ + public abstract void deployModel(); + + /** + * This method lists the models created on this provider. + */ + public abstract List listModels(); + + /** + * This method deletes a model + */ + public abstract void deleteModel(); + + /** + * This method makes a prediction about an item using the selected model + */ + public abstract MLPrediction makePrediction(); + + /** + * This method makes a bulk prediction using the selected model + */ + public abstract void makeBulkPrediction(); + + public Set getAcceptedDataTypes() { + return acceptedDataTypes; + } + + public void setAcceptedDataTypes(Set acceptedDataTypes) { + this.acceptedDataTypes = acceptedDataTypes; + } +} From 7a0317c0a61ebdffe834f33c641ad103d5fd8fd0 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Mon, 14 Feb 2022 16:07:01 +0000 Subject: [PATCH 02/53] Started implementation of build dataset endpoint --- dicoogle/pom.xml | 13 ++ .../core/mlprovider/CreateDatasetRequest.java | 40 +++++ .../core/mlprovider/PrepareDatasetTask.java | 154 ++++++++++++++++++ .../ua/dicoogle/plugins/PluginController.java | 60 +++++++ .../mlprovider/CreateDatasetServlet.java | 22 ++- .../mlprovider/MakeBulkPredictionServlet.java | 4 +- .../mlprovider/MakePredictionServlet.java | 4 +- .../sdk/datastructs/dim/BulkAnnotation.java | 25 +++ .../dicoogle/sdk/datastructs/dim/Point2D.java | 38 +++++ .../dicoogle/sdk/mlprovider/MLAnnotation.java | 8 - .../dicoogle/sdk/mlprovider/MLCSVDataset.java | 2 +- .../dicoogle/sdk/mlprovider/MLEndpoint.java | 2 +- .../sdk/mlprovider/MLImageDataset.java | 26 ++- .../dicoogle/sdk/mlprovider/MLPrediction.java | 3 +- ...Provider.java => MLProviderInterface.java} | 4 +- .../dicoogle/sdk/mlprovider/ML_DATA_TYPE.java | 3 +- .../ua/dicoogle/sdk/mlprovider/MLlabel.java | 33 ++++ .../sdk/utils/DicomImageReaderUtil.java | 2 + 18 files changed, 416 insertions(+), 27 deletions(-) create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/CreateDatasetRequest.java create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java delete mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLAnnotation.java rename sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/{MachineLearningProvider.java => MLProviderInterface.java} (91%) create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java diff --git a/dicoogle/pom.xml b/dicoogle/pom.xml index c25a6ba15..cd7139492 100644 --- a/dicoogle/pom.xml +++ b/dicoogle/pom.xml @@ -382,6 +382,19 @@ ${dcm4che.version} + + dcm4che-core + org.dcm4che + 3.3.7 + + + + + org.dcm4che + dcm4che-imageio + 5.25.1 + + org.jdom jdom2 diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/CreateDatasetRequest.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/CreateDatasetRequest.java new file mode 100644 index 000000000..5f989f266 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/CreateDatasetRequest.java @@ -0,0 +1,40 @@ +package pt.ua.dicoogle.core.mlprovider; + +import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; + +import java.util.HashMap; +import java.util.List; + +/** + * Java object to represent create dataset requests + * @author Rui Jesus + */ +public class CreateDatasetRequest { + + /** + * The name of the ML Provider plugin to update the dataset to. + */ + private String providerName; + + /** + * The dataset to upload. + * Each key should be a SOPInstanceUID and optionally the value should be a list of annotations. + */ + private HashMap> dataset; + + public String getProviderName() { + return providerName; + } + + public void setProviderName(String providerName) { + this.providerName = providerName; + } + + public HashMap> getDataset() { + return dataset; + } + + public void setDataset(HashMap> dataset) { + this.dataset = dataset; + } +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java new file mode 100644 index 000000000..6966af6c6 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java @@ -0,0 +1,154 @@ +package pt.ua.dicoogle.core.mlprovider; + +import org.dcm4che3.data.Attributes; +import org.dcm4che3.data.Tag; +import org.dcm4che3.imageio.plugins.dcm.DicomImageReadParam; +import org.dcm4che3.imageio.plugins.dcm.DicomImageReader; +import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; +import org.dcm4che3.io.DicomInputStream; +import pt.ua.dicoogle.plugins.PluginController; +import pt.ua.dicoogle.sdk.datastructs.SearchResult; +import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; +import pt.ua.dicoogle.sdk.mlprovider.MLDataset; +import pt.ua.dicoogle.sdk.mlprovider.MLImageDataset; +import pt.ua.dicoogle.sdk.mlprovider.MLlabel; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +public class PrepareDatasetTask implements Callable { + + private final CreateDatasetRequest request; + private final PluginController controller; + private String dataset; + + public PrepareDatasetTask(PluginController controller, CreateDatasetRequest request) { + this.controller = controller; + this.request = request; + this.dataset = UUID.randomUUID().toString(); + } + + @Override + public MLDataset call() throws Exception { + + MLImageDataset mlDataset = new MLImageDataset(); + ImageReader imageReader = getImageReader(); + + + this.request.getDataset().entrySet().forEach((entry -> { + try { + + Attributes dataset; + DicomMetaData dicomMetaData; + DicomImageReadParam param; + + // Query this image using the first available query provider + Iterable results = controller + .query(controller.getQueryProvidersName(true).get(0), "SOPInstanceUID:" + entry.getKey()).get(); + + for (SearchResult image : results) { + DicomInputStream dis = new DicomInputStream(new File(image.getURI())); + + Attributes fmi = dis.getFileMetaInformation(); + dataset = dis.readDataset(-1, -1); + dicomMetaData = new DicomMetaData(fmi, dataset); + + int tile_height = fmi.getInt(Tag.Rows, -1); + int tile_width = fmi.getInt(Tag.Columns, -1); + + imageReader.setInput(dicomMetaData); + param = (DicomImageReadParam) imageReader.getDefaultReadParam(); + + List labels; + + for (BulkAnnotation annotation : entry.getValue()) { + labels = new ArrayList<>(); + List> frameMatrix = getFrameMatrixFromAnnotation(fmi, annotation); + BufferedImage combined = new BufferedImage(frameMatrix.size() * tile_width, + frameMatrix.get(0).size() * tile_height, BufferedImage.TYPE_INT_RGB); + Graphics g = combined.getGraphics(); + int c = -1; + int d = 0; + for (List row : frameMatrix) { + c++; + for (Integer frame : row) { + BufferedImage bb = imageReader.read(frame, param); + g.drawImage(bb, c * tile_width, d++ * tile_height, null); + } + } + g.dispose(); + // Save as new image + File f = new File(dataset + File.separator + entry.getKey() + ".jpg"); + ImageIO.write(combined, "jpg", f); + + MLlabel label = new MLlabel(annotation.getLabel(), 1); + labels.add(label); + mlDataset.getDataset().put(f.toURI(), labels); + } + + } + } catch (InterruptedException | ExecutionException | IOException e) { + e.printStackTrace(); // Ignore + } + })); + + return mlDataset; + } + + private static ImageReader getImageReader() { + Iterator iter = ImageIO.getImageReadersByFormatName("DICOM"); + while (iter.hasNext()) { + ImageReader reader = iter.next(); + if (reader instanceof DicomImageReader) + return reader; + } + return null; + } + + private List> getFrameMatrixFromAnnotation(Attributes attrs, BulkAnnotation annotation) { + int image_width = attrs.getInt(Tag.TotalPixelMatrixColumns, -1); + + int tile_height = attrs.getInt(Tag.Rows, -1); + int tile_width = attrs.getInt(Tag.Columns, -1); + + int nx_tiles = (int) Math.ceil(image_width / tile_width); + + List> matrix = new ArrayList<>(); + + switch (annotation.getAnnotationType()) { + case RECTANGLE: + // Get dimmensions of the annotation + double width = annotation.getPoints().get(0).distance(annotation.getPoints().get(1)); + double height = annotation.getPoints().get(0).distance(annotation.getPoints().get(3)); + + // Estimate the number of frames that the annotation covers in height and width + int xframes = (int) Math.ceil(width / tile_width); + int yframes = (int) Math.ceil(height / tile_height); + + // Calculate the starting position of the annotation in frame coordinates + int x_c = (int) Math.floor(annotation.getPoints().get(0).getX() / tile_width); + int y_c = (int) Math.floor(annotation.getPoints().get(0).getX() / tile_width); + + for (int i = y_c; i < (yframes + y_c); i++) { + matrix.add(new ArrayList<>()); + for (int j = x_c; j < (xframes + x_c); j++) { + matrix.get(i - y_c).add(i * nx_tiles + j); + } + } + break; + } + + return matrix; + } + +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java index 117ad5c1c..c9d36367f 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java @@ -23,6 +23,7 @@ import org.restlet.resource.ServerResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import pt.ua.dicoogle.core.mlprovider.PrepareDatasetTask; import pt.ua.dicoogle.core.settings.ServerSettingsManager; import pt.ua.dicoogle.plugins.webui.WebUIPlugin; import pt.ua.dicoogle.plugins.webui.WebUIPluginManager; @@ -31,12 +32,15 @@ import pt.ua.dicoogle.sdk.datastructs.UnindexReport; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; +import pt.ua.dicoogle.sdk.mlprovider.MLDataset; +import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; import pt.ua.dicoogle.sdk.task.JointQueryTask; import pt.ua.dicoogle.sdk.task.Task; import pt.ua.dicoogle.server.ControlServices; import pt.ua.dicoogle.server.PluginRestletApplication; import pt.ua.dicoogle.server.web.DicoogleWeb; +import pt.ua.dicoogle.core.mlprovider.CreateDatasetRequest; import pt.ua.dicoogle.taskManager.RunningIndexTasks; import pt.ua.dicoogle.taskManager.TaskManager; @@ -91,6 +95,9 @@ public synchronized static PluginController getInstance() { private TaskManager taskManagerQueries = new TaskManager(Integer.parseInt(System.getProperty("dicoogle.taskManager.nQueryThreads", "4"))); + private final TaskManager taskManagerML = + new TaskManager(Integer.parseInt(System.getProperty("dicoogle.taskManager.nMLThreads", "1"))); + /** Whether to shut down Dicoogle when a plugin is marked as dead */ private static boolean DEAD_PLUGIN_KILL_SWITCH = System.getProperty("dicoogle.deadPluginKillSwitch", "false").equalsIgnoreCase("true"); @@ -361,6 +368,19 @@ public Collection getServletPlugins() { return this.getServletPlugins(true); } + public Collection getMLPlugins(boolean onlyEnabled) { + List plugins = new ArrayList<>(); + for (PluginSet pSet : pluginSets) { + for (MLProviderInterface ml : pSet.getMLPlugins()) { + if (!ml.isEnabled() && onlyEnabled) { + continue; + } + plugins.add(ml); + } + } + return plugins; + } + public Collection getPluginSetNames() { Collection l = new ArrayList<>(); for (PluginSet s : this.pluginSets) { @@ -524,6 +544,18 @@ public StorageInterface getStorageByName(String name, boolean onlyEnabled) { return null; } + public MLProviderInterface getMachineLearningProviderByName(String name, boolean onlyEnabled) { + Collection plugins = getMLPlugins(onlyEnabled); + for (MLProviderInterface p : plugins) { + if (p.getName().equalsIgnoreCase(name)) { + // logger.info("Retrived Query Provider: "+name); + return p; + } + } + logger.debug("No machine learning provider matching name {} for onlyEnabled = {}", name, onlyEnabled); + return null; + } + public JointQueryTask queryAll(JointQueryTask holder, final String query, final Object... parameters) { List providers = this.getQueryProvidersName(true); @@ -843,6 +875,34 @@ public List indexBlocking(URI path) { return reports; } + /** + * This method creates a {@link PrepareDatasetTask}. + * The task is responsible for creating a directory where the processed dataset will be placed. + * After the task is finished, the chosen mlProvider will be invoked to upload the dataset. + * @param datasetRequest the dataset to upload. + * @return the created task + */ + public Task prepareMLDataset(final CreateDatasetRequest datasetRequest) { + String uuid = UUID.randomUUID().toString(); + Task prepareTask = + new Task<>("MLPrepareDatasetTask" + uuid, new PrepareDatasetTask(this, datasetRequest)); + prepareTask.onCompletion(() -> { + MLProviderInterface mlInterface = getMachineLearningProviderByName(datasetRequest.getProviderName(), true); + if (mlInterface == null) { + logger.error("MLProvider with name {} not found", prepareTask.getName()); + } else { + try { + mlInterface.createDataset(prepareTask.get()); + } catch (InterruptedException | ExecutionException e) { + logger.error("Task {} failed execution", prepareTask.getName(), e); + } + } + }); + logger.debug("Fired prepare dataset task with uuid {}", uuid); + taskManagerML.dispatch(prepareTask); + return prepareTask; + } + // Methods for Web UI /** Retrieve all web UI plugin descriptors for the given slot id. diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java index e8d9f9774..7956816d5 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java @@ -14,8 +14,26 @@ public class CreateDatasetServlet extends HttpServlet { private static final Logger log = LoggerFactory.getLogger(CreateDatasetServlet.class); @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) - throws ServletException, IOException { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String dataString = IOUtils.toString(req.getReader()); + if (dataString == null) { + resp.sendError(404, "Empty POST body"); + return; + } + + ObjectMapper mapper = new ObjectMapper(); + CreateDatasetRequest datasetRequest; + try { + datasetRequest = mapper.readValue(dataString, CreateDatasetRequest.class); + /* + if(PluginController.getInstance().getMachineLearningProviderByName(datasetRequest.getProviderName(), true) == null){ + resp.sendError(404, "The requested provider does not exist"); + }*/ + PluginController.getInstance().prepareMLDataset(datasetRequest); + } catch (Exception e) { + log.error("Error parsing json string", e); + resp.sendError(404, "Malformed request"); + } } } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java index 8f2615897..1156437bc 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java @@ -14,7 +14,5 @@ public class MakeBulkPredictionServlet extends HttpServlet { private static final Logger log = LoggerFactory.getLogger(MakeBulkPredictionServlet.class); @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) - throws ServletException, IOException { - } + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {} } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index 844165220..d759ef8f2 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -14,7 +14,5 @@ public class MakePredictionServlet extends HttpServlet { private static final Logger log = LoggerFactory.getLogger(MakePredictionServlet.class); @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) - throws ServletException, IOException { - } + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {} } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java new file mode 100644 index 000000000..acb77d296 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java @@ -0,0 +1,25 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +public class MLAnnotation { + + public enum PixelOrigin { + FRAME, //Coordinates of this annotation are related to the frame (image section) + VOLUME //Coordinates of this annotation are related to the Frame Matrix (whole image) + } + + public enum AnnotationType { + RECTANGLE, + ELLIPSE, + POLYGON, + POLYLINE, + POINT + } + + private PixelOrigin pixelOrigin; + + private AnnotationType annotationType; + + private String label; + private double confidence; + +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java new file mode 100644 index 000000000..2deaf6d5f --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java @@ -0,0 +1,38 @@ +package pt.ua.dicoogle.sdk.datastructs.dim; + +public class Point2D { + + private int x; + + private int y; + + public Point2D() { + x = 0; + y = 0; + } + + public Point2D(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + + public int getY() { + return y; + } + + public void setY(int y) { + this.y = y; + } + + public double distance(Point2D otherPoint) { + return Math.sqrt(Math.pow(this.x - otherPoint.getX(), 2) + Math.pow(this.y - otherPoint.getY(), 2)); + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLAnnotation.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLAnnotation.java deleted file mode 100644 index 185538872..000000000 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLAnnotation.java +++ /dev/null @@ -1,8 +0,0 @@ -package pt.ua.dicoogle.sdk.mlprovider; - -public class MLAnnotation { - - private String label; - private double confidence; - -} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java index 3ffc0168d..30c839a75 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java @@ -2,7 +2,7 @@ import java.io.InputStream; -public class MLCSVDataset extends MLDataset{ +public class MLCSVDataset extends MLDataset { private InputStream csvFile; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java index a1b0db515..f2b93bce2 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java @@ -2,6 +2,6 @@ public class MLEndpoint { - + } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java index 8bb053e6d..c19d7777e 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java @@ -1,8 +1,28 @@ package pt.ua.dicoogle.sdk.mlprovider; -import java.io.InputStream; +import java.net.URI; +import java.util.HashMap; +import java.util.List; public class MLImageDataset extends MLDataset { - Iterable images; - Iterable labels; + + private HashMap> dataset; + + private boolean multiClass; + + public HashMap> getDataset() { + return dataset; + } + + public void setDataset(HashMap> dataset) { + this.dataset = dataset; + } + + public boolean isMultiClass() { + return multiClass; + } + + public void setMultiClass(boolean multiClass) { + this.multiClass = multiClass; + } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java index d6ae50d6e..045a8ab61 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java @@ -1,4 +1,3 @@ package pt.ua.dicoogle.sdk.mlprovider; -public class MLPrediction { -} +public class MLPrediction {} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MachineLearningProvider.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java similarity index 91% rename from sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MachineLearningProvider.java rename to sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java index b0e6e1638..ff85180c2 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MachineLearningProvider.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java @@ -14,7 +14,7 @@ * * @author Rui Jesus */ -public abstract class MachineLearningProvider implements DicooglePlugin { +public abstract class MLProviderInterface implements DicooglePlugin { protected Set acceptedDataTypes; @@ -22,7 +22,7 @@ public abstract class MachineLearningProvider implements DicooglePlugin { * This method creates and uploads a dataset to the machine learning provider. * A dataset is defined as a set of labelled images or a labelled CSV file with one column used to label the entries. */ - public abstract void createDataset(); + public abstract void createDataset(MLDataset dataset); /** * This method creates a model using a specific dataset diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java index 80e92a48f..e2aae5293 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java @@ -1,6 +1,5 @@ package pt.ua.dicoogle.sdk.mlprovider; public enum ML_DATA_TYPE { - CSV, - IMAGE + CSV, IMAGE } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java new file mode 100644 index 000000000..df097de3e --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java @@ -0,0 +1,33 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +public class MLlabel { + + private String label; + private double confidence; + + public MLlabel(String label) { + this.label = label; + this.confidence = 1; + } + + public MLlabel(String label, double confidence) { + this.label = label; + this.confidence = confidence; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public double getConfidence() { + return confidence; + } + + public void setConfidence(double confidence) { + this.confidence = confidence; + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java new file mode 100644 index 000000000..96da7f8ce --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java @@ -0,0 +1,2 @@ +package pt.ua.dicoogle.sdk.utils;public class DicomImageReaderUtil { +} From 27146b6dce0882189dea1baf6ecd9dbec9f21484 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Mon, 14 Feb 2022 16:15:12 +0000 Subject: [PATCH 03/53] Added missing contributes --- .../mlprovider/CreateDatasetServlet.java | 4 + .../java/pt/ua/dicoogle/sdk/PluginBase.java | 192 ++++++------- .../java/pt/ua/dicoogle/sdk/PluginSet.java | 266 +++++++++--------- .../sdk/datastructs/dim/BulkAnnotation.java | 90 ++++-- .../sdk/utils/DicomImageReaderUtil.java | 13 +- 5 files changed, 309 insertions(+), 256 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java index 7956816d5..7afdefefc 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java @@ -1,7 +1,11 @@ package pt.ua.dicoogle.server.web.servlets.mlprovider; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import pt.ua.dicoogle.core.mlprovider.CreateDatasetRequest; +import pt.ua.dicoogle.plugins.PluginController; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java index 84e3329e4..7673ccc1a 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java @@ -1,96 +1,96 @@ -/** - * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ - * - * This file is part of Dicoogle/dicoogle-sdk. - * - * Dicoogle/dicoogle-sdk 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. - * - * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . - */ -package pt.ua.dicoogle.sdk; - -import pt.ua.dicoogle.sdk.mlprovider.MachineLearningProvider; -import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import org.restlet.resource.ServerResource; - -import pt.ua.dicoogle.sdk.core.DicooglePlatformInterface; -import pt.ua.dicoogle.sdk.core.PlatformCommunicatorInterface; - -/** - * @author Luís A. Bastião Silva - * @author Luís S. Ribeiro - */ -public abstract class PluginBase implements PluginSet, PlatformCommunicatorInterface { - - protected List indexPlugins = new ArrayList<>(); - protected List queryPlugins = new ArrayList<>(); - protected List jettyPlugins = new ArrayList<>(); - protected List storagePlugins = new ArrayList<>(); - protected List mlPlugins = new ArrayList<>(); - protected List services = new ArrayList<>(); - protected ConfigurationHolder settings = null; - - protected DicooglePlatformInterface platform; - - @Override - public List getIndexPlugins() { - return indexPlugins; - } - - @Override - public List getQueryPlugins() { - return queryPlugins; - } - - @Override - public List getRestPlugins() { - return services; - } - - @Override - public List getJettyPlugins() { - return jettyPlugins; - } - - @Override - public abstract String getName(); - - @Override - public ConfigurationHolder getSettings() { - return settings; - } - - @Override - public void setSettings(ConfigurationHolder xmlSettings) { - settings = xmlSettings; - } - - @Override - public Collection getStoragePlugins() { - return storagePlugins; - } - - @Override - public Collection getMLPlugins() { - return mlPlugins; - } - - @Override - public void setPlatformProxy(DicooglePlatformInterface core) { - this.platform = core; - } -} +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ +package pt.ua.dicoogle.sdk; + +import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; +import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.restlet.resource.ServerResource; + +import pt.ua.dicoogle.sdk.core.DicooglePlatformInterface; +import pt.ua.dicoogle.sdk.core.PlatformCommunicatorInterface; + +/** + * @author Luís A. Bastião Silva + * @author Luís S. Ribeiro + */ +public abstract class PluginBase implements PluginSet, PlatformCommunicatorInterface { + + protected List indexPlugins = new ArrayList<>(); + protected List queryPlugins = new ArrayList<>(); + protected List jettyPlugins = new ArrayList<>(); + protected List storagePlugins = new ArrayList<>(); + protected List mlPlugins = new ArrayList<>(); + protected List services = new ArrayList<>(); + protected ConfigurationHolder settings = null; + + protected DicooglePlatformInterface platform; + + @Override + public List getIndexPlugins() { + return indexPlugins; + } + + @Override + public List getQueryPlugins() { + return queryPlugins; + } + + @Override + public List getRestPlugins() { + return services; + } + + @Override + public List getJettyPlugins() { + return jettyPlugins; + } + + @Override + public abstract String getName(); + + @Override + public ConfigurationHolder getSettings() { + return settings; + } + + @Override + public void setSettings(ConfigurationHolder xmlSettings) { + settings = xmlSettings; + } + + @Override + public Collection getStoragePlugins() { + return storagePlugins; + } + + @Override + public Collection getMLPlugins() { + return mlPlugins; + } + + @Override + public void setPlatformProxy(DicooglePlatformInterface core) { + this.platform = core; + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java index cd888c118..5e3a7c3a1 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java @@ -1,133 +1,133 @@ -/** - * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ - * - * This file is part of Dicoogle/dicoogle-sdk. - * - * Dicoogle/dicoogle-sdk 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. - * - * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . - */ -package pt.ua.dicoogle.sdk; - -import java.util.Collection; -import java.util.Collections; - -import net.xeoh.plugins.base.Plugin; - -import org.restlet.resource.ServerResource; - -import pt.ua.dicoogle.sdk.mlprovider.MachineLearningProvider; -import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; - -/** - * This is the class responsible for creating a Dicoolge plugin. - * The developer may use this interface in order to manage and expose the implemented plugins. - * One instance of each installed plugin set is created by injecting it as a - * {@link net.xeoh.plugins.base.annotations.PluginImplementation}. All instances are expected - * to be thread safe. It is highly recommended that provided collections are immutable, and - * that no modifications are performed in getter methods. - * - * @author psytek - * @author Luís A. Bastião Silva - * @author Eduardo Pinho - */ -public interface PluginSet extends Plugin { - /** - * Gets the plugin's name. This name will be used for identifying index/query/storage providers, - * and should be unique among the total plugin sets installed. - * @return the name of the plugin, never changes - */ - public String getName(); - - /** - * Gets the indexer plugins enclosed in this plugin set. - * This collection must be immutable. - * @return IndexPluginInterface returns a list of active index plugins - * @see IndexerInterface - */ - public default Collection getIndexPlugins() { - return Collections.EMPTY_LIST; - } - - /** - * Gets the query plugins enclosed in this plugin set. - * This collection must be immutable. - * @return a collection of query plugins - * @see QueryInterface - */ - public default Collection getQueryPlugins() { - return Collections.EMPTY_LIST; - } - - /** - * Gets the storage plugins enclosed in this plugin set. - * This collection must be immutable. - * @return Collection holding the StoragePlugins of this PluginSet - */ - public default Collection getStoragePlugins() { - return Collections.EMPTY_LIST; - } - - /** - * Obtains a collection of access to the RESTful resources. These plugins will be installed to - * the web service hierarchy according to a name defined by the object's {@code toString()} method. - * This collection must be immutable. - * @return a collection of Restlet-based server resources, implementing {@code toString()} - * to provide the resource name - */ - public default Collection getRestPlugins() { - return Collections.EMPTY_LIST; - } - - /** - * Obtains a collection of Jetty plugins, so as to implement web services via Dicoogle. - * This collection must be immutable. - * @return a collection of Jetty plugins to the core application - * @see JettyPluginInterface - */ - public default Collection getJettyPlugins() { - return Collections.EMPTY_LIST; - } - - /** - * Obtains a collection of MachineLearningProvider plugins, so as to integrate Machine Learning providers in Dicoogle. - * This collection must be immutable. - * @return a collection of MachineLearningProvider plugins to the core application - * @see MachineLearningProvider - */ - public default Collection getMLPlugins() { - return Collections.EMPTY_LIST; - } - - /** - * Defines the plugin's settings. This method will be called once after the plugin set was instantiated - * with plugin-scoped settings. Dicoogle users can modify these settings by accessing the XML file with - * the same name in the "Settings" folder. Developers may define such settings programmatically from the - * plugin itself. - * @param xmlSettings an XML-based configuration holder - */ - public void setSettings(ConfigurationHolder xmlSettings); - - /** - * Retrieves the plugin's settings. - * @return an XML-based configuration holder - */ - public ConfigurationHolder getSettings(); - - /** - * Signals a plugin to stop. Upon an invocation of this method, the plugin may clean allocated resources - * and save state if required. - */ - public default void shutdown() { - // do nothing - } -} +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ +package pt.ua.dicoogle.sdk; + +import java.util.Collection; +import java.util.Collections; + +import net.xeoh.plugins.base.Plugin; + +import org.restlet.resource.ServerResource; + +import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; +import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; + +/** + * This is the class responsible for creating a Dicoolge plugin. + * The developer may use this interface in order to manage and expose the implemented plugins. + * One instance of each installed plugin set is created by injecting it as a + * {@link net.xeoh.plugins.base.annotations.PluginImplementation}. All instances are expected + * to be thread safe. It is highly recommended that provided collections are immutable, and + * that no modifications are performed in getter methods. + * + * @author psytek + * @author Luís A. Bastião Silva + * @author Eduardo Pinho + */ +public interface PluginSet extends Plugin { + /** + * Gets the plugin's name. This name will be used for identifying index/query/storage providers, + * and should be unique among the total plugin sets installed. + * @return the name of the plugin, never changes + */ + public String getName(); + + /** + * Gets the indexer plugins enclosed in this plugin set. + * This collection must be immutable. + * @return IndexPluginInterface returns a list of active index plugins + * @see IndexerInterface + */ + public default Collection getIndexPlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Gets the query plugins enclosed in this plugin set. + * This collection must be immutable. + * @return a collection of query plugins + * @see QueryInterface + */ + public default Collection getQueryPlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Gets the storage plugins enclosed in this plugin set. + * This collection must be immutable. + * @return Collection holding the StoragePlugins of this PluginSet + */ + public default Collection getStoragePlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Obtains a collection of access to the RESTful resources. These plugins will be installed to + * the web service hierarchy according to a name defined by the object's {@code toString()} method. + * This collection must be immutable. + * @return a collection of Restlet-based server resources, implementing {@code toString()} + * to provide the resource name + */ + public default Collection getRestPlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Obtains a collection of Jetty plugins, so as to implement web services via Dicoogle. + * This collection must be immutable. + * @return a collection of Jetty plugins to the core application + * @see JettyPluginInterface + */ + public default Collection getJettyPlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Obtains a collection of MachineLearningProvider plugins, so as to integrate Machine Learning providers in Dicoogle. + * This collection must be immutable. + * @return a collection of MachineLearningProvider plugins to the core application + * @see MLProviderInterface + */ + public default Collection getMLPlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Defines the plugin's settings. This method will be called once after the plugin set was instantiated + * with plugin-scoped settings. Dicoogle users can modify these settings by accessing the XML file with + * the same name in the "Settings" folder. Developers may define such settings programmatically from the + * plugin itself. + * @param xmlSettings an XML-based configuration holder + */ + public void setSettings(ConfigurationHolder xmlSettings); + + /** + * Retrieves the plugin's settings. + * @return an XML-based configuration holder + */ + public ConfigurationHolder getSettings(); + + /** + * Signals a plugin to stop. Upon an invocation of this method, the plugin may clean allocated resources + * and save state if required. + */ + public default void shutdown() { + // do nothing + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java index acb77d296..06823363a 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java @@ -1,25 +1,65 @@ -package pt.ua.dicoogle.sdk.mlprovider; - -public class MLAnnotation { - - public enum PixelOrigin { - FRAME, //Coordinates of this annotation are related to the frame (image section) - VOLUME //Coordinates of this annotation are related to the Frame Matrix (whole image) - } - - public enum AnnotationType { - RECTANGLE, - ELLIPSE, - POLYGON, - POLYLINE, - POINT - } - - private PixelOrigin pixelOrigin; - - private AnnotationType annotationType; - - private String label; - private double confidence; - -} +package pt.ua.dicoogle.sdk.datastructs.dim; + +import java.util.List; + +public class BulkAnnotation { + + public enum PixelOrigin { + FRAME, // Coordinates of this annotation are related to the frame (image section) + VOLUME // Coordinates of this annotation are related to the Frame Matrix (whole image) + } + + public enum AnnotationType { + RECTANGLE, ELLIPSE, POLYGON, POLYLINE, POINT + } + + private PixelOrigin pixelOrigin; + + private AnnotationType annotationType; + + private String label; + + private List points; + + private double confidence; + + public PixelOrigin getPixelOrigin() { + return pixelOrigin; + } + + public void setPixelOrigin(PixelOrigin pixelOrigin) { + this.pixelOrigin = pixelOrigin; + } + + public AnnotationType getAnnotationType() { + return annotationType; + } + + public void setAnnotationType(AnnotationType annotationType) { + this.annotationType = annotationType; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public double getConfidence() { + return confidence; + } + + public void setConfidence(double confidence) { + this.confidence = confidence; + } + + public List getPoints() { + return points; + } + + public void setPoints(List points) { + this.points = points; + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java index 96da7f8ce..463321207 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java @@ -1,2 +1,11 @@ -package pt.ua.dicoogle.sdk.utils;public class DicomImageReaderUtil { -} +package pt.ua.dicoogle.sdk.utils; + +/** + * A set of utils to read pixel data from Dicom files + * @author Rui Jesus + */ +public class DicomImageReaderUtil { + + + +} From c8ee2a8d3dc478c85bf814bd7a9c624e366792a5 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Sun, 20 Feb 2022 07:35:45 +0000 Subject: [PATCH 04/53] Added new ImageWorkerInterface --- .../core/mlprovider/CreateDatasetRequest.java | 80 ++--- .../core/mlprovider/PrepareDatasetTask.java | 308 +++++++++--------- .../ua/dicoogle/server/web/DicoogleWeb.java | 9 +- .../mlprovider/CreateDatasetServlet.java | 86 ++--- .../mlprovider/MakeBulkPredictionServlet.java | 36 +- .../mlprovider/MakePredictionServlet.java | 36 +- .../java/pt/ua/dicoogle/sdk/PluginBase.java | 199 +++++------ .../java/pt/ua/dicoogle/sdk/PluginSet.java | 277 ++++++++-------- .../sdk/datastructs/dim/BulkAnnotation.java | 130 ++++---- .../dicoogle/sdk/datastructs/dim/Point2D.java | 76 ++--- .../ua/dicoogle/sdk/imageworker/ImageROI.java | 13 + .../sdk/imageworker/ImageWorkerInterface.java | 11 + .../dicoogle/sdk/mlprovider/MLCSVDataset.java | 18 +- .../ua/dicoogle/sdk/mlprovider/MLDataset.java | 14 +- .../dicoogle/sdk/mlprovider/MLEndpoint.java | 14 +- .../sdk/mlprovider/MLImageDataset.java | 56 ++-- .../ua/dicoogle/sdk/mlprovider/MLModel.java | 32 +- .../dicoogle/sdk/mlprovider/MLPrediction.java | 6 +- .../sdk/mlprovider/MLProviderInterface.java | 158 ++++----- .../dicoogle/sdk/mlprovider/ML_DATA_TYPE.java | 10 +- .../ua/dicoogle/sdk/mlprovider/MLlabel.java | 66 ++-- .../sdk/utils/DicomImageReaderUtil.java | 22 +- 22 files changed, 849 insertions(+), 808 deletions(-) create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/CreateDatasetRequest.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/CreateDatasetRequest.java index 5f989f266..9ab24fe6c 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/CreateDatasetRequest.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/CreateDatasetRequest.java @@ -1,40 +1,40 @@ -package pt.ua.dicoogle.core.mlprovider; - -import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; - -import java.util.HashMap; -import java.util.List; - -/** - * Java object to represent create dataset requests - * @author Rui Jesus - */ -public class CreateDatasetRequest { - - /** - * The name of the ML Provider plugin to update the dataset to. - */ - private String providerName; - - /** - * The dataset to upload. - * Each key should be a SOPInstanceUID and optionally the value should be a list of annotations. - */ - private HashMap> dataset; - - public String getProviderName() { - return providerName; - } - - public void setProviderName(String providerName) { - this.providerName = providerName; - } - - public HashMap> getDataset() { - return dataset; - } - - public void setDataset(HashMap> dataset) { - this.dataset = dataset; - } -} +package pt.ua.dicoogle.core.mlprovider; + +import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; + +import java.util.HashMap; +import java.util.List; + +/** + * Java object to represent create dataset requests + * @author Rui Jesus + */ +public class CreateDatasetRequest { + + /** + * The name of the ML Provider plugin to update the dataset to. + */ + private String providerName; + + /** + * The dataset to upload. + * Each key should be a SOPInstanceUID and optionally the value should be a list of annotations. + */ + private HashMap> dataset; + + public String getProviderName() { + return providerName; + } + + public void setProviderName(String providerName) { + this.providerName = providerName; + } + + public HashMap> getDataset() { + return dataset; + } + + public void setDataset(HashMap> dataset) { + this.dataset = dataset; + } +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java index 6966af6c6..24af8db57 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java @@ -1,154 +1,154 @@ -package pt.ua.dicoogle.core.mlprovider; - -import org.dcm4che3.data.Attributes; -import org.dcm4che3.data.Tag; -import org.dcm4che3.imageio.plugins.dcm.DicomImageReadParam; -import org.dcm4che3.imageio.plugins.dcm.DicomImageReader; -import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; -import org.dcm4che3.io.DicomInputStream; -import pt.ua.dicoogle.plugins.PluginController; -import pt.ua.dicoogle.sdk.datastructs.SearchResult; -import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; -import pt.ua.dicoogle.sdk.mlprovider.MLDataset; -import pt.ua.dicoogle.sdk.mlprovider.MLImageDataset; -import pt.ua.dicoogle.sdk.mlprovider.MLlabel; - -import javax.imageio.ImageIO; -import javax.imageio.ImageReader; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; - -public class PrepareDatasetTask implements Callable { - - private final CreateDatasetRequest request; - private final PluginController controller; - private String dataset; - - public PrepareDatasetTask(PluginController controller, CreateDatasetRequest request) { - this.controller = controller; - this.request = request; - this.dataset = UUID.randomUUID().toString(); - } - - @Override - public MLDataset call() throws Exception { - - MLImageDataset mlDataset = new MLImageDataset(); - ImageReader imageReader = getImageReader(); - - - this.request.getDataset().entrySet().forEach((entry -> { - try { - - Attributes dataset; - DicomMetaData dicomMetaData; - DicomImageReadParam param; - - // Query this image using the first available query provider - Iterable results = controller - .query(controller.getQueryProvidersName(true).get(0), "SOPInstanceUID:" + entry.getKey()).get(); - - for (SearchResult image : results) { - DicomInputStream dis = new DicomInputStream(new File(image.getURI())); - - Attributes fmi = dis.getFileMetaInformation(); - dataset = dis.readDataset(-1, -1); - dicomMetaData = new DicomMetaData(fmi, dataset); - - int tile_height = fmi.getInt(Tag.Rows, -1); - int tile_width = fmi.getInt(Tag.Columns, -1); - - imageReader.setInput(dicomMetaData); - param = (DicomImageReadParam) imageReader.getDefaultReadParam(); - - List labels; - - for (BulkAnnotation annotation : entry.getValue()) { - labels = new ArrayList<>(); - List> frameMatrix = getFrameMatrixFromAnnotation(fmi, annotation); - BufferedImage combined = new BufferedImage(frameMatrix.size() * tile_width, - frameMatrix.get(0).size() * tile_height, BufferedImage.TYPE_INT_RGB); - Graphics g = combined.getGraphics(); - int c = -1; - int d = 0; - for (List row : frameMatrix) { - c++; - for (Integer frame : row) { - BufferedImage bb = imageReader.read(frame, param); - g.drawImage(bb, c * tile_width, d++ * tile_height, null); - } - } - g.dispose(); - // Save as new image - File f = new File(dataset + File.separator + entry.getKey() + ".jpg"); - ImageIO.write(combined, "jpg", f); - - MLlabel label = new MLlabel(annotation.getLabel(), 1); - labels.add(label); - mlDataset.getDataset().put(f.toURI(), labels); - } - - } - } catch (InterruptedException | ExecutionException | IOException e) { - e.printStackTrace(); // Ignore - } - })); - - return mlDataset; - } - - private static ImageReader getImageReader() { - Iterator iter = ImageIO.getImageReadersByFormatName("DICOM"); - while (iter.hasNext()) { - ImageReader reader = iter.next(); - if (reader instanceof DicomImageReader) - return reader; - } - return null; - } - - private List> getFrameMatrixFromAnnotation(Attributes attrs, BulkAnnotation annotation) { - int image_width = attrs.getInt(Tag.TotalPixelMatrixColumns, -1); - - int tile_height = attrs.getInt(Tag.Rows, -1); - int tile_width = attrs.getInt(Tag.Columns, -1); - - int nx_tiles = (int) Math.ceil(image_width / tile_width); - - List> matrix = new ArrayList<>(); - - switch (annotation.getAnnotationType()) { - case RECTANGLE: - // Get dimmensions of the annotation - double width = annotation.getPoints().get(0).distance(annotation.getPoints().get(1)); - double height = annotation.getPoints().get(0).distance(annotation.getPoints().get(3)); - - // Estimate the number of frames that the annotation covers in height and width - int xframes = (int) Math.ceil(width / tile_width); - int yframes = (int) Math.ceil(height / tile_height); - - // Calculate the starting position of the annotation in frame coordinates - int x_c = (int) Math.floor(annotation.getPoints().get(0).getX() / tile_width); - int y_c = (int) Math.floor(annotation.getPoints().get(0).getX() / tile_width); - - for (int i = y_c; i < (yframes + y_c); i++) { - matrix.add(new ArrayList<>()); - for (int j = x_c; j < (xframes + x_c); j++) { - matrix.get(i - y_c).add(i * nx_tiles + j); - } - } - break; - } - - return matrix; - } - -} +package pt.ua.dicoogle.core.mlprovider; + +import org.dcm4che3.data.Attributes; +import org.dcm4che3.data.Tag; +import org.dcm4che3.imageio.plugins.dcm.DicomImageReadParam; +import org.dcm4che3.imageio.plugins.dcm.DicomImageReader; +import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; +import org.dcm4che3.io.DicomInputStream; +import pt.ua.dicoogle.plugins.PluginController; +import pt.ua.dicoogle.sdk.datastructs.SearchResult; +import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; +import pt.ua.dicoogle.sdk.mlprovider.MLDataset; +import pt.ua.dicoogle.sdk.mlprovider.MLImageDataset; +import pt.ua.dicoogle.sdk.mlprovider.MLlabel; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +public class PrepareDatasetTask implements Callable { + + private final CreateDatasetRequest request; + private final PluginController controller; + private String dataset; + + public PrepareDatasetTask(PluginController controller, CreateDatasetRequest request) { + this.controller = controller; + this.request = request; + this.dataset = UUID.randomUUID().toString(); + } + + @Override + public MLDataset call() throws Exception { + + MLImageDataset mlDataset = new MLImageDataset(); + ImageReader imageReader = getImageReader(); + + + this.request.getDataset().entrySet().forEach((entry -> { + try { + + Attributes dataset; + DicomMetaData dicomMetaData; + DicomImageReadParam param; + + // Query this image using the first available query provider + Iterable results = controller + .query(controller.getQueryProvidersName(true).get(0), "SOPInstanceUID:" + entry.getKey()).get(); + + for (SearchResult image : results) { + DicomInputStream dis = new DicomInputStream(new File(image.getURI())); + + Attributes fmi = dis.getFileMetaInformation(); + dataset = dis.readDataset(-1, -1); + dicomMetaData = new DicomMetaData(fmi, dataset); + + int tile_height = fmi.getInt(Tag.Rows, -1); + int tile_width = fmi.getInt(Tag.Columns, -1); + + imageReader.setInput(dicomMetaData); + param = (DicomImageReadParam) imageReader.getDefaultReadParam(); + + List labels; + + for (BulkAnnotation annotation : entry.getValue()) { + labels = new ArrayList<>(); + List> frameMatrix = getFrameMatrixFromAnnotation(fmi, annotation); + BufferedImage combined = new BufferedImage(frameMatrix.size() * tile_width, + frameMatrix.get(0).size() * tile_height, BufferedImage.TYPE_INT_RGB); + Graphics g = combined.getGraphics(); + int c = -1; + int d = 0; + for (List row : frameMatrix) { + c++; + for (Integer frame : row) { + BufferedImage bb = imageReader.read(frame, param); + g.drawImage(bb, c * tile_width, d++ * tile_height, null); + } + } + g.dispose(); + // Save as new image + File f = new File(dataset + File.separator + entry.getKey() + ".jpg"); + ImageIO.write(combined, "jpg", f); + + MLlabel label = new MLlabel(annotation.getLabel(), 1); + labels.add(label); + mlDataset.getDataset().put(f.toURI(), labels); + } + + } + } catch (InterruptedException | ExecutionException | IOException e) { + e.printStackTrace(); // Ignore + } + })); + + return mlDataset; + } + + private static ImageReader getImageReader() { + Iterator iter = ImageIO.getImageReadersByFormatName("DICOM"); + while (iter.hasNext()) { + ImageReader reader = iter.next(); + if (reader instanceof DicomImageReader) + return reader; + } + return null; + } + + private List> getFrameMatrixFromAnnotation(Attributes attrs, BulkAnnotation annotation) { + int image_width = attrs.getInt(Tag.TotalPixelMatrixColumns, -1); + + int tile_height = attrs.getInt(Tag.Rows, -1); + int tile_width = attrs.getInt(Tag.Columns, -1); + + int nx_tiles = (int) Math.ceil(image_width / tile_width); + + List> matrix = new ArrayList<>(); + + switch (annotation.getAnnotationType()) { + case RECTANGLE: + // Get dimmensions of the annotation + double width = annotation.getPoints().get(0).distance(annotation.getPoints().get(1)); + double height = annotation.getPoints().get(0).distance(annotation.getPoints().get(3)); + + // Estimate the number of frames that the annotation covers in height and width + int xframes = (int) Math.ceil(width / tile_width); + int yframes = (int) Math.ceil(height / tile_height); + + // Calculate the starting position of the annotation in frame coordinates + int x_c = (int) Math.floor(annotation.getPoints().get(0).getX() / tile_width); + int y_c = (int) Math.floor(annotation.getPoints().get(0).getX() / tile_width); + + for (int i = y_c; i < (yframes + y_c); i++) { + matrix.add(new ArrayList<>()); + for (int j = x_c; j < (xframes + x_c); j++) { + matrix.get(i - y_c).add(i * nx_tiles + j); + } + } + break; + } + + return matrix; + } + +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index efbc3042b..f225f936c 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java @@ -225,11 +225,10 @@ public DicoogleWeb(int port) throws Exception { createServletHandler(new ExportServlet(ExportType.LIST), "/export/list"), createServletHandler(new ServerStorageServlet(), "/management/settings/storage/dicom"), - //ml provider servlets - createServletHandler(new CreateDatasetServlet(), "/mlprovider/createDataset"), - createServletHandler(new MakePredictionServlet(), "/mlprovider/makePrediction"), - createServletHandler(new MakeBulkPredictionServlet(), "/mlprovider/makeBulkPrediction"), - webpages}; + // ml provider servlets + createServletHandler(new CreateDatasetServlet(), "/ml/createDataset"), + createServletHandler(new MakePredictionServlet(), "/ml/makePrediction"), + createServletHandler(new MakeBulkPredictionServlet(), "/ml/makeBulkPrediction"), webpages}; // setup the server server = new Server(port); diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java index 7afdefefc..1b2e6a706 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java @@ -1,43 +1,43 @@ -package pt.ua.dicoogle.server.web.servlets.mlprovider; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import pt.ua.dicoogle.core.mlprovider.CreateDatasetRequest; -import pt.ua.dicoogle.plugins.PluginController; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -public class CreateDatasetServlet extends HttpServlet { - - private static final Logger log = LoggerFactory.getLogger(CreateDatasetServlet.class); - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - - String dataString = IOUtils.toString(req.getReader()); - if (dataString == null) { - resp.sendError(404, "Empty POST body"); - return; - } - - ObjectMapper mapper = new ObjectMapper(); - CreateDatasetRequest datasetRequest; - try { - datasetRequest = mapper.readValue(dataString, CreateDatasetRequest.class); - /* - if(PluginController.getInstance().getMachineLearningProviderByName(datasetRequest.getProviderName(), true) == null){ - resp.sendError(404, "The requested provider does not exist"); - }*/ - PluginController.getInstance().prepareMLDataset(datasetRequest); - } catch (Exception e) { - log.error("Error parsing json string", e); - resp.sendError(404, "Malformed request"); - } - } -} +package pt.ua.dicoogle.server.web.servlets.mlprovider; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pt.ua.dicoogle.core.mlprovider.CreateDatasetRequest; +import pt.ua.dicoogle.plugins.PluginController; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class CreateDatasetServlet extends HttpServlet { + + private static final Logger log = LoggerFactory.getLogger(CreateDatasetServlet.class); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String dataString = IOUtils.toString(req.getReader()); + if (dataString == null) { + resp.sendError(404, "Empty POST body"); + return; + } + + ObjectMapper mapper = new ObjectMapper(); + CreateDatasetRequest datasetRequest; + try { + datasetRequest = mapper.readValue(dataString, CreateDatasetRequest.class); + /* + if(PluginController.getInstance().getMachineLearningProviderByName(datasetRequest.getProviderName(), true) == null){ + resp.sendError(404, "The requested provider does not exist"); + }*/ + PluginController.getInstance().prepareMLDataset(datasetRequest); + } catch (Exception e) { + log.error("Error parsing json string", e); + resp.sendError(404, "Malformed request"); + } + } +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java index 1156437bc..83d331b78 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java @@ -1,18 +1,18 @@ -package pt.ua.dicoogle.server.web.servlets.mlprovider; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -public class MakeBulkPredictionServlet extends HttpServlet { - - private static final Logger log = LoggerFactory.getLogger(MakeBulkPredictionServlet.class); - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {} -} +package pt.ua.dicoogle.server.web.servlets.mlprovider; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class MakeBulkPredictionServlet extends HttpServlet { + + private static final Logger log = LoggerFactory.getLogger(MakeBulkPredictionServlet.class); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {} +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index d759ef8f2..19702a205 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -1,18 +1,18 @@ -package pt.ua.dicoogle.server.web.servlets.mlprovider; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -public class MakePredictionServlet extends HttpServlet { - - private static final Logger log = LoggerFactory.getLogger(MakePredictionServlet.class); - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {} -} +package pt.ua.dicoogle.server.web.servlets.mlprovider; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class MakePredictionServlet extends HttpServlet { + + private static final Logger log = LoggerFactory.getLogger(MakePredictionServlet.class); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {} +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java index 7673ccc1a..6e362eacf 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java @@ -1,96 +1,103 @@ -/** - * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ - * - * This file is part of Dicoogle/dicoogle-sdk. - * - * Dicoogle/dicoogle-sdk 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. - * - * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . - */ -package pt.ua.dicoogle.sdk; - -import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; -import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import org.restlet.resource.ServerResource; - -import pt.ua.dicoogle.sdk.core.DicooglePlatformInterface; -import pt.ua.dicoogle.sdk.core.PlatformCommunicatorInterface; - -/** - * @author Luís A. Bastião Silva - * @author Luís S. Ribeiro - */ -public abstract class PluginBase implements PluginSet, PlatformCommunicatorInterface { - - protected List indexPlugins = new ArrayList<>(); - protected List queryPlugins = new ArrayList<>(); - protected List jettyPlugins = new ArrayList<>(); - protected List storagePlugins = new ArrayList<>(); - protected List mlPlugins = new ArrayList<>(); - protected List services = new ArrayList<>(); - protected ConfigurationHolder settings = null; - - protected DicooglePlatformInterface platform; - - @Override - public List getIndexPlugins() { - return indexPlugins; - } - - @Override - public List getQueryPlugins() { - return queryPlugins; - } - - @Override - public List getRestPlugins() { - return services; - } - - @Override - public List getJettyPlugins() { - return jettyPlugins; - } - - @Override - public abstract String getName(); - - @Override - public ConfigurationHolder getSettings() { - return settings; - } - - @Override - public void setSettings(ConfigurationHolder xmlSettings) { - settings = xmlSettings; - } - - @Override - public Collection getStoragePlugins() { - return storagePlugins; - } - - @Override - public Collection getMLPlugins() { - return mlPlugins; - } - - @Override - public void setPlatformProxy(DicooglePlatformInterface core) { - this.platform = core; - } -} +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ +package pt.ua.dicoogle.sdk; + +import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; +import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; +import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.restlet.resource.ServerResource; + +import pt.ua.dicoogle.sdk.core.DicooglePlatformInterface; +import pt.ua.dicoogle.sdk.core.PlatformCommunicatorInterface; + +/** + * @author Luís A. Bastião Silva + * @author Luís S. Ribeiro + */ +public abstract class PluginBase implements PluginSet, PlatformCommunicatorInterface { + + protected List indexPlugins = new ArrayList<>(); + protected List queryPlugins = new ArrayList<>(); + protected List jettyPlugins = new ArrayList<>(); + protected List storagePlugins = new ArrayList<>(); + protected List mlPlugins = new ArrayList<>(); + protected List imageWorkerPlugins = new ArrayList<>(); + protected List services = new ArrayList<>(); + protected ConfigurationHolder settings = null; + + protected DicooglePlatformInterface platform; + + @Override + public List getIndexPlugins() { + return indexPlugins; + } + + @Override + public List getQueryPlugins() { + return queryPlugins; + } + + @Override + public List getRestPlugins() { + return services; + } + + @Override + public List getJettyPlugins() { + return jettyPlugins; + } + + @Override + public abstract String getName(); + + @Override + public ConfigurationHolder getSettings() { + return settings; + } + + @Override + public void setSettings(ConfigurationHolder xmlSettings) { + settings = xmlSettings; + } + + @Override + public Collection getStoragePlugins() { + return storagePlugins; + } + + @Override + public Collection getMLPlugins() { + return mlPlugins; + } + + @Override + public Collection getImageWorkerPlugins() { + return imageWorkerPlugins; + } + + @Override + public void setPlatformProxy(DicooglePlatformInterface core) { + this.platform = core; + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java index 5e3a7c3a1..1d74886b9 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java @@ -1,133 +1,144 @@ -/** - * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ - * - * This file is part of Dicoogle/dicoogle-sdk. - * - * Dicoogle/dicoogle-sdk 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. - * - * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . - */ -package pt.ua.dicoogle.sdk; - -import java.util.Collection; -import java.util.Collections; - -import net.xeoh.plugins.base.Plugin; - -import org.restlet.resource.ServerResource; - -import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; -import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; - -/** - * This is the class responsible for creating a Dicoolge plugin. - * The developer may use this interface in order to manage and expose the implemented plugins. - * One instance of each installed plugin set is created by injecting it as a - * {@link net.xeoh.plugins.base.annotations.PluginImplementation}. All instances are expected - * to be thread safe. It is highly recommended that provided collections are immutable, and - * that no modifications are performed in getter methods. - * - * @author psytek - * @author Luís A. Bastião Silva - * @author Eduardo Pinho - */ -public interface PluginSet extends Plugin { - /** - * Gets the plugin's name. This name will be used for identifying index/query/storage providers, - * and should be unique among the total plugin sets installed. - * @return the name of the plugin, never changes - */ - public String getName(); - - /** - * Gets the indexer plugins enclosed in this plugin set. - * This collection must be immutable. - * @return IndexPluginInterface returns a list of active index plugins - * @see IndexerInterface - */ - public default Collection getIndexPlugins() { - return Collections.EMPTY_LIST; - } - - /** - * Gets the query plugins enclosed in this plugin set. - * This collection must be immutable. - * @return a collection of query plugins - * @see QueryInterface - */ - public default Collection getQueryPlugins() { - return Collections.EMPTY_LIST; - } - - /** - * Gets the storage plugins enclosed in this plugin set. - * This collection must be immutable. - * @return Collection holding the StoragePlugins of this PluginSet - */ - public default Collection getStoragePlugins() { - return Collections.EMPTY_LIST; - } - - /** - * Obtains a collection of access to the RESTful resources. These plugins will be installed to - * the web service hierarchy according to a name defined by the object's {@code toString()} method. - * This collection must be immutable. - * @return a collection of Restlet-based server resources, implementing {@code toString()} - * to provide the resource name - */ - public default Collection getRestPlugins() { - return Collections.EMPTY_LIST; - } - - /** - * Obtains a collection of Jetty plugins, so as to implement web services via Dicoogle. - * This collection must be immutable. - * @return a collection of Jetty plugins to the core application - * @see JettyPluginInterface - */ - public default Collection getJettyPlugins() { - return Collections.EMPTY_LIST; - } - - /** - * Obtains a collection of MachineLearningProvider plugins, so as to integrate Machine Learning providers in Dicoogle. - * This collection must be immutable. - * @return a collection of MachineLearningProvider plugins to the core application - * @see MLProviderInterface - */ - public default Collection getMLPlugins() { - return Collections.EMPTY_LIST; - } - - /** - * Defines the plugin's settings. This method will be called once after the plugin set was instantiated - * with plugin-scoped settings. Dicoogle users can modify these settings by accessing the XML file with - * the same name in the "Settings" folder. Developers may define such settings programmatically from the - * plugin itself. - * @param xmlSettings an XML-based configuration holder - */ - public void setSettings(ConfigurationHolder xmlSettings); - - /** - * Retrieves the plugin's settings. - * @return an XML-based configuration holder - */ - public ConfigurationHolder getSettings(); - - /** - * Signals a plugin to stop. Upon an invocation of this method, the plugin may clean allocated resources - * and save state if required. - */ - public default void shutdown() { - // do nothing - } -} +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ +package pt.ua.dicoogle.sdk; + +import java.util.Collection; +import java.util.Collections; + +import net.xeoh.plugins.base.Plugin; + +import org.restlet.resource.ServerResource; + +import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; +import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; +import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; + +/** + * This is the class responsible for creating a Dicoolge plugin. + * The developer may use this interface in order to manage and expose the implemented plugins. + * One instance of each installed plugin set is created by injecting it as a + * {@link net.xeoh.plugins.base.annotations.PluginImplementation}. All instances are expected + * to be thread safe. It is highly recommended that provided collections are immutable, and + * that no modifications are performed in getter methods. + * + * @author psytek + * @author Luís A. Bastião Silva + * @author Eduardo Pinho + */ +public interface PluginSet extends Plugin { + /** + * Gets the plugin's name. This name will be used for identifying index/query/storage providers, + * and should be unique among the total plugin sets installed. + * @return the name of the plugin, never changes + */ + public String getName(); + + /** + * Gets the indexer plugins enclosed in this plugin set. + * This collection must be immutable. + * @return IndexPluginInterface returns a list of active index plugins + * @see IndexerInterface + */ + public default Collection getIndexPlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Gets the query plugins enclosed in this plugin set. + * This collection must be immutable. + * @return a collection of query plugins + * @see QueryInterface + */ + public default Collection getQueryPlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Gets the storage plugins enclosed in this plugin set. + * This collection must be immutable. + * @return Collection holding the StoragePlugins of this PluginSet + */ + public default Collection getStoragePlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Obtains a collection of access to the RESTful resources. These plugins will be installed to + * the web service hierarchy according to a name defined by the object's {@code toString()} method. + * This collection must be immutable. + * @return a collection of Restlet-based server resources, implementing {@code toString()} + * to provide the resource name + */ + public default Collection getRestPlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Obtains a collection of Jetty plugins, so as to implement web services via Dicoogle. + * This collection must be immutable. + * @return a collection of Jetty plugins to the core application + * @see JettyPluginInterface + */ + public default Collection getJettyPlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Obtains a collection of MachineLearningProvider plugins, so as to integrate Machine Learning providers in Dicoogle. + * This collection must be immutable. + * @return a collection of MachineLearningProvider plugins to the core application + * @see MLProviderInterface + */ + public default Collection getMLPlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Obtains a collection of ImageWorker plugins, so as to integrate Image Worker plugins such as ROI extraction plugins in Dicoogle. + * This collection must be immutable. + * @return a collection of ImageWorker plugins to the core application + * @see ImageWorkerInterface + */ + public default Collection getImageWorkerPlugins() { + return Collections.EMPTY_LIST; + } + + /** + * Defines the plugin's settings. This method will be called once after the plugin set was instantiated + * with plugin-scoped settings. Dicoogle users can modify these settings by accessing the XML file with + * the same name in the "Settings" folder. Developers may define such settings programmatically from the + * plugin itself. + * @param xmlSettings an XML-based configuration holder + */ + public void setSettings(ConfigurationHolder xmlSettings); + + /** + * Retrieves the plugin's settings. + * @return an XML-based configuration holder + */ + public ConfigurationHolder getSettings(); + + /** + * Signals a plugin to stop. Upon an invocation of this method, the plugin may clean allocated resources + * and save state if required. + */ + public default void shutdown() { + // do nothing + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java index 06823363a..af7b15934 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java @@ -1,65 +1,65 @@ -package pt.ua.dicoogle.sdk.datastructs.dim; - -import java.util.List; - -public class BulkAnnotation { - - public enum PixelOrigin { - FRAME, // Coordinates of this annotation are related to the frame (image section) - VOLUME // Coordinates of this annotation are related to the Frame Matrix (whole image) - } - - public enum AnnotationType { - RECTANGLE, ELLIPSE, POLYGON, POLYLINE, POINT - } - - private PixelOrigin pixelOrigin; - - private AnnotationType annotationType; - - private String label; - - private List points; - - private double confidence; - - public PixelOrigin getPixelOrigin() { - return pixelOrigin; - } - - public void setPixelOrigin(PixelOrigin pixelOrigin) { - this.pixelOrigin = pixelOrigin; - } - - public AnnotationType getAnnotationType() { - return annotationType; - } - - public void setAnnotationType(AnnotationType annotationType) { - this.annotationType = annotationType; - } - - public String getLabel() { - return label; - } - - public void setLabel(String label) { - this.label = label; - } - - public double getConfidence() { - return confidence; - } - - public void setConfidence(double confidence) { - this.confidence = confidence; - } - - public List getPoints() { - return points; - } - - public void setPoints(List points) { - this.points = points; - } -} +package pt.ua.dicoogle.sdk.datastructs.dim; + +import java.util.List; + +public class BulkAnnotation { + + public enum PixelOrigin { + FRAME, // Coordinates of this annotation are related to the frame (image section) + VOLUME // Coordinates of this annotation are related to the Frame Matrix (whole image) + } + + public enum AnnotationType { + RECTANGLE, ELLIPSE, POLYGON, POLYLINE, POINT + } + + private PixelOrigin pixelOrigin; + + private AnnotationType annotationType; + + private String label; + + private List points; + + private double confidence; + + public PixelOrigin getPixelOrigin() { + return pixelOrigin; + } + + public void setPixelOrigin(PixelOrigin pixelOrigin) { + this.pixelOrigin = pixelOrigin; + } + + public AnnotationType getAnnotationType() { + return annotationType; + } + + public void setAnnotationType(AnnotationType annotationType) { + this.annotationType = annotationType; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public double getConfidence() { + return confidence; + } + + public void setConfidence(double confidence) { + this.confidence = confidence; + } + + public List getPoints() { + return points; + } + + public void setPoints(List points) { + this.points = points; + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java index 2deaf6d5f..63ff33017 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java @@ -1,38 +1,38 @@ -package pt.ua.dicoogle.sdk.datastructs.dim; - -public class Point2D { - - private int x; - - private int y; - - public Point2D() { - x = 0; - y = 0; - } - - public Point2D(int x, int y) { - this.x = x; - this.y = y; - } - - public int getX() { - return x; - } - - public void setX(int x) { - this.x = x; - } - - public int getY() { - return y; - } - - public void setY(int y) { - this.y = y; - } - - public double distance(Point2D otherPoint) { - return Math.sqrt(Math.pow(this.x - otherPoint.getX(), 2) + Math.pow(this.y - otherPoint.getY(), 2)); - } -} +package pt.ua.dicoogle.sdk.datastructs.dim; + +public class Point2D { + + private int x; + + private int y; + + public Point2D() { + x = 0; + y = 0; + } + + public Point2D(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + + public int getY() { + return y; + } + + public void setY(int y) { + this.y = y; + } + + public double distance(Point2D otherPoint) { + return Math.sqrt(Math.pow(this.x - otherPoint.getX(), 2) + Math.pow(this.y - otherPoint.getY(), 2)); + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java new file mode 100644 index 000000000..8f5222d3f --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java @@ -0,0 +1,13 @@ +package pt.ua.dicoogle.sdk.imageworker; + +import java.net.URI; + +public class ImageROI { + + private int width; + + private int height; + + private URI roi; + +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java new file mode 100644 index 000000000..c25c46832 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java @@ -0,0 +1,11 @@ +package pt.ua.dicoogle.sdk.imageworker; + +import org.dcm4che2.io.DicomInputStream; +import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; + +public abstract class ImageWorkerInterface { + + public abstract ImageROI extractROI(DicomInputStream is, BulkAnnotation annotation); + + public abstract Iterable extractROIs(DicomInputStream is, Iterable annotations); +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java index 30c839a75..81f251f44 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java @@ -1,9 +1,9 @@ -package pt.ua.dicoogle.sdk.mlprovider; - -import java.io.InputStream; - -public class MLCSVDataset extends MLDataset { - - private InputStream csvFile; - -} +package pt.ua.dicoogle.sdk.mlprovider; + +import java.io.InputStream; + +public class MLCSVDataset extends MLDataset { + + private InputStream csvFile; + +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java index 82c4f8d57..0259933a9 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java @@ -1,7 +1,7 @@ -package pt.ua.dicoogle.sdk.mlprovider; - -public abstract class MLDataset { - - protected String name; - -} +package pt.ua.dicoogle.sdk.mlprovider; + +public abstract class MLDataset { + + protected String name; + +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java index f2b93bce2..18de95b2a 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java @@ -1,7 +1,7 @@ -package pt.ua.dicoogle.sdk.mlprovider; - -public class MLEndpoint { - - - -} +package pt.ua.dicoogle.sdk.mlprovider; + +public class MLEndpoint { + + + +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java index c19d7777e..dac1ec510 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java @@ -1,28 +1,28 @@ -package pt.ua.dicoogle.sdk.mlprovider; - -import java.net.URI; -import java.util.HashMap; -import java.util.List; - -public class MLImageDataset extends MLDataset { - - private HashMap> dataset; - - private boolean multiClass; - - public HashMap> getDataset() { - return dataset; - } - - public void setDataset(HashMap> dataset) { - this.dataset = dataset; - } - - public boolean isMultiClass() { - return multiClass; - } - - public void setMultiClass(boolean multiClass) { - this.multiClass = multiClass; - } -} +package pt.ua.dicoogle.sdk.mlprovider; + +import java.net.URI; +import java.util.HashMap; +import java.util.List; + +public class MLImageDataset extends MLDataset { + + private HashMap> dataset; + + private boolean multiClass; + + public HashMap> getDataset() { + return dataset; + } + + public void setDataset(HashMap> dataset) { + this.dataset = dataset; + } + + public boolean isMultiClass() { + return multiClass; + } + + public void setMultiClass(boolean multiClass) { + this.multiClass = multiClass; + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java index 1d5508fab..938faef09 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java @@ -1,16 +1,16 @@ -package pt.ua.dicoogle.sdk.mlprovider; - -import java.util.Date; - -public class MLModel { - - private String name; - - private String id; - - private String description; - - private Date creationDate; - - private ML_DATA_TYPE dataType; -} +package pt.ua.dicoogle.sdk.mlprovider; + +import java.util.Date; + +public class MLModel { + + private String name; + + private String id; + + private String description; + + private Date creationDate; + + private ML_DATA_TYPE dataType; +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java index 045a8ab61..0ad8adb50 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java @@ -1,3 +1,3 @@ -package pt.ua.dicoogle.sdk.mlprovider; - -public class MLPrediction {} +package pt.ua.dicoogle.sdk.mlprovider; + +public class MLPrediction {} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java index ff85180c2..66c76963e 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java @@ -1,79 +1,79 @@ -package pt.ua.dicoogle.sdk.mlprovider; - -import pt.ua.dicoogle.sdk.DicooglePlugin; - -import java.util.List; -import java.util.Set; - -/** - * Interface to define Machine Learning providers. - * A machine learning provider can be a remote service, hosted on the cloud, or a simple remote/local server - * that has installed machine learning algorithms for problem solving. - * Machine learning providers can work with either image or csv datasets. - * The purpose of these providers is to provide a way to develop plugins to integrate with services such as Google's Vertex API or Amazon's SageMaker API. - * - * @author Rui Jesus - */ -public abstract class MLProviderInterface implements DicooglePlugin { - - protected Set acceptedDataTypes; - - /** - * This method creates and uploads a dataset to the machine learning provider. - * A dataset is defined as a set of labelled images or a labelled CSV file with one column used to label the entries. - */ - public abstract void createDataset(MLDataset dataset); - - /** - * This method creates a model using a specific dataset - */ - public abstract MLModel createModel(); - - /** - * This method creates a model using a specific dataset - */ - public abstract void createEndpoint(); - - /** - * This method creates a model using a specific dataset - */ - public abstract List listEndpoints(); - - /** - * This method creates a model using a specific dataset - */ - public abstract void deleteEndpoint(); - - /** - * This method deploys a model - */ - public abstract void deployModel(); - - /** - * This method lists the models created on this provider. - */ - public abstract List listModels(); - - /** - * This method deletes a model - */ - public abstract void deleteModel(); - - /** - * This method makes a prediction about an item using the selected model - */ - public abstract MLPrediction makePrediction(); - - /** - * This method makes a bulk prediction using the selected model - */ - public abstract void makeBulkPrediction(); - - public Set getAcceptedDataTypes() { - return acceptedDataTypes; - } - - public void setAcceptedDataTypes(Set acceptedDataTypes) { - this.acceptedDataTypes = acceptedDataTypes; - } -} +package pt.ua.dicoogle.sdk.mlprovider; + +import pt.ua.dicoogle.sdk.DicooglePlugin; + +import java.util.List; +import java.util.Set; + +/** + * Interface to define Machine Learning providers. + * A machine learning provider can be a remote service, hosted on the cloud, or a simple remote/local server + * that has installed machine learning algorithms for problem solving. + * Machine learning providers can work with either image or csv datasets. + * The purpose of these providers is to provide a way to develop plugins to integrate with services such as Google's Vertex API or Amazon's SageMaker API. + * + * @author Rui Jesus + */ +public abstract class MLProviderInterface implements DicooglePlugin { + + protected Set acceptedDataTypes; + + /** + * This method creates and uploads a dataset to the machine learning provider. + * A dataset is defined as a set of labelled images or a labelled CSV file with one column used to label the entries. + */ + public abstract void createDataset(MLDataset dataset); + + /** + * This method creates a model using a specific dataset + */ + public abstract MLModel createModel(); + + /** + * This method creates a model using a specific dataset + */ + public abstract void createEndpoint(); + + /** + * This method creates a model using a specific dataset + */ + public abstract List listEndpoints(); + + /** + * This method creates a model using a specific dataset + */ + public abstract void deleteEndpoint(); + + /** + * This method deploys a model + */ + public abstract void deployModel(); + + /** + * This method lists the models created on this provider. + */ + public abstract List listModels(); + + /** + * This method deletes a model + */ + public abstract void deleteModel(); + + /** + * This method makes a prediction about an item using the selected model + */ + public abstract MLPrediction makePrediction(); + + /** + * This method makes a bulk prediction using the selected model + */ + public abstract void makeBulkPrediction(); + + public Set getAcceptedDataTypes() { + return acceptedDataTypes; + } + + public void setAcceptedDataTypes(Set acceptedDataTypes) { + this.acceptedDataTypes = acceptedDataTypes; + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java index e2aae5293..52e6e2cf8 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java @@ -1,5 +1,5 @@ -package pt.ua.dicoogle.sdk.mlprovider; - -public enum ML_DATA_TYPE { - CSV, IMAGE -} +package pt.ua.dicoogle.sdk.mlprovider; + +public enum ML_DATA_TYPE { + CSV, IMAGE +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java index df097de3e..0eb34fa78 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java @@ -1,33 +1,33 @@ -package pt.ua.dicoogle.sdk.mlprovider; - -public class MLlabel { - - private String label; - private double confidence; - - public MLlabel(String label) { - this.label = label; - this.confidence = 1; - } - - public MLlabel(String label, double confidence) { - this.label = label; - this.confidence = confidence; - } - - public String getLabel() { - return label; - } - - public void setLabel(String label) { - this.label = label; - } - - public double getConfidence() { - return confidence; - } - - public void setConfidence(double confidence) { - this.confidence = confidence; - } -} +package pt.ua.dicoogle.sdk.mlprovider; + +public class MLlabel { + + private String label; + private double confidence; + + public MLlabel(String label) { + this.label = label; + this.confidence = 1; + } + + public MLlabel(String label, double confidence) { + this.label = label; + this.confidence = confidence; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public double getConfidence() { + return confidence; + } + + public void setConfidence(double confidence) { + this.confidence = confidence; + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java index 463321207..740cadaa0 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java @@ -1,11 +1,11 @@ -package pt.ua.dicoogle.sdk.utils; - -/** - * A set of utils to read pixel data from Dicom files - * @author Rui Jesus - */ -public class DicomImageReaderUtil { - - - -} +package pt.ua.dicoogle.sdk.utils; + +/** + * A set of utils to read pixel data from Dicom files + * @author Rui Jesus + */ +public class DicomImageReaderUtil { + + + +} From 091aee5cb7e59f7bff8d8239d93263ff3d5ca6a5 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Sun, 1 May 2022 16:02:51 +0100 Subject: [PATCH 05/53] Implemented new ImageWorker interface --- dicoogle/pom.xml | 13 -- .../core/mlprovider/PrepareDatasetTask.java | 126 +++--------------- .../ua/dicoogle/plugins/PluginController.java | 25 ++++ sdk/pom.xml | 6 + .../ua/dicoogle/sdk/imageworker/ImageROI.java | 29 ++++ .../sdk/imageworker/ImageWorkerInterface.java | 10 +- 6 files changed, 84 insertions(+), 125 deletions(-) diff --git a/dicoogle/pom.xml b/dicoogle/pom.xml index cd7139492..da1818dea 100644 --- a/dicoogle/pom.xml +++ b/dicoogle/pom.xml @@ -376,25 +376,12 @@ 2.9.5 - - dcm4che - dcm4che-imageio - ${dcm4che.version} - - dcm4che-core org.dcm4che 3.3.7 - - - org.dcm4che - dcm4che-imageio - 5.25.1 - - org.jdom jdom2 diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java index 24af8db57..974b35b1f 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java @@ -1,26 +1,15 @@ package pt.ua.dicoogle.core.mlprovider; -import org.dcm4che3.data.Attributes; -import org.dcm4che3.data.Tag; -import org.dcm4che3.imageio.plugins.dcm.DicomImageReadParam; -import org.dcm4che3.imageio.plugins.dcm.DicomImageReader; -import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; import org.dcm4che3.io.DicomInputStream; import pt.ua.dicoogle.plugins.PluginController; import pt.ua.dicoogle.sdk.datastructs.SearchResult; -import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; +import pt.ua.dicoogle.sdk.imageworker.ImageROI; +import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; import pt.ua.dicoogle.sdk.mlprovider.MLDataset; import pt.ua.dicoogle.sdk.mlprovider.MLImageDataset; -import pt.ua.dicoogle.sdk.mlprovider.MLlabel; - -import javax.imageio.ImageIO; -import javax.imageio.ImageReader; -import java.awt.*; -import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; +import java.util.HashMap; import java.util.List; import java.util.UUID; import java.util.concurrent.Callable; @@ -41,114 +30,37 @@ public PrepareDatasetTask(PluginController controller, CreateDatasetRequest requ @Override public MLDataset call() throws Exception { + HashMap extraFields = new HashMap(); + + extraFields.put("SOPInstanceUID", "SOPInstanceUID"); + extraFields.put("SharedFunctionalGroupsSequence_PixelMeasuresSequence_PixelSpacing", "SharedFunctionalGroupsSequence_PixelMeasuresSequence_PixelSpacing"); + extraFields.put("Rows", "Rows"); + extraFields.put("Columns", "Columns"); + extraFields.put("NumberOfFrames", "NumberOfFrames"); + extraFields.put("TotalPixelMatrixColumns", "TotalPixelMatrixColumns"); + extraFields.put("TotalPixelMatrixRows", "TotalPixelMatrixRows"); + extraFields.put("ImageType", "ImageType"); + MLImageDataset mlDataset = new MLImageDataset(); - ImageReader imageReader = getImageReader(); + ImageWorkerInterface worker = controller.getImageWorkerInterfaceByName("roiExtractor", true); this.request.getDataset().entrySet().forEach((entry -> { try { - Attributes dataset; - DicomMetaData dicomMetaData; - DicomImageReadParam param; - // Query this image using the first available query provider Iterable results = controller - .query(controller.getQueryProvidersName(true).get(0), "SOPInstanceUID:" + entry.getKey()).get(); + .query(controller.getQueryProvidersName(true).get(0), "SOPInstanceUID:" + entry.getKey(), extraFields).get(); for (SearchResult image : results) { - DicomInputStream dis = new DicomInputStream(new File(image.getURI())); - - Attributes fmi = dis.getFileMetaInformation(); - dataset = dis.readDataset(-1, -1); - dicomMetaData = new DicomMetaData(fmi, dataset); - - int tile_height = fmi.getInt(Tag.Rows, -1); - int tile_width = fmi.getInt(Tag.Columns, -1); - - imageReader.setInput(dicomMetaData); - param = (DicomImageReadParam) imageReader.getDefaultReadParam(); - - List labels; - - for (BulkAnnotation annotation : entry.getValue()) { - labels = new ArrayList<>(); - List> frameMatrix = getFrameMatrixFromAnnotation(fmi, annotation); - BufferedImage combined = new BufferedImage(frameMatrix.size() * tile_width, - frameMatrix.get(0).size() * tile_height, BufferedImage.TYPE_INT_RGB); - Graphics g = combined.getGraphics(); - int c = -1; - int d = 0; - for (List row : frameMatrix) { - c++; - for (Integer frame : row) { - BufferedImage bb = imageReader.read(frame, param); - g.drawImage(bb, c * tile_width, d++ * tile_height, null); - } - } - g.dispose(); - // Save as new image - File f = new File(dataset + File.separator + entry.getKey() + ".jpg"); - ImageIO.write(combined, "jpg", f); - - MLlabel label = new MLlabel(annotation.getLabel(), 1); - labels.add(label); - mlDataset.getDataset().put(f.toURI(), labels); - } - + List rois = (List) worker.extractROIs(image, entry.getValue()); } - } catch (InterruptedException | ExecutionException | IOException e) { + + } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); // Ignore } })); return mlDataset; } - - private static ImageReader getImageReader() { - Iterator iter = ImageIO.getImageReadersByFormatName("DICOM"); - while (iter.hasNext()) { - ImageReader reader = iter.next(); - if (reader instanceof DicomImageReader) - return reader; - } - return null; - } - - private List> getFrameMatrixFromAnnotation(Attributes attrs, BulkAnnotation annotation) { - int image_width = attrs.getInt(Tag.TotalPixelMatrixColumns, -1); - - int tile_height = attrs.getInt(Tag.Rows, -1); - int tile_width = attrs.getInt(Tag.Columns, -1); - - int nx_tiles = (int) Math.ceil(image_width / tile_width); - - List> matrix = new ArrayList<>(); - - switch (annotation.getAnnotationType()) { - case RECTANGLE: - // Get dimmensions of the annotation - double width = annotation.getPoints().get(0).distance(annotation.getPoints().get(1)); - double height = annotation.getPoints().get(0).distance(annotation.getPoints().get(3)); - - // Estimate the number of frames that the annotation covers in height and width - int xframes = (int) Math.ceil(width / tile_width); - int yframes = (int) Math.ceil(height / tile_height); - - // Calculate the starting position of the annotation in frame coordinates - int x_c = (int) Math.floor(annotation.getPoints().get(0).getX() / tile_width); - int y_c = (int) Math.floor(annotation.getPoints().get(0).getX() / tile_width); - - for (int i = y_c; i < (yframes + y_c); i++) { - matrix.add(new ArrayList<>()); - for (int j = x_c; j < (xframes + x_c); j++) { - matrix.get(i - y_c).add(i * nx_tiles + j); - } - } - break; - } - - return matrix; - } - } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java index c9d36367f..05ac30830 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java @@ -32,6 +32,7 @@ import pt.ua.dicoogle.sdk.datastructs.UnindexReport; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; +import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; import pt.ua.dicoogle.sdk.mlprovider.MLDataset; import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; @@ -381,6 +382,19 @@ public Collection getMLPlugins(boolean onlyEnabled) { return plugins; } + public Collection getImageWorkerPlugins(boolean onlyEnabled) { + List plugins = new ArrayList<>(); + for (PluginSet pSet : pluginSets) { + for (ImageWorkerInterface i : pSet.getImageWorkerPlugins()) { + if (!i.isEnabled() && onlyEnabled) { + continue; + } + plugins.add(i); + } + } + return plugins; + } + public Collection getPluginSetNames() { Collection l = new ArrayList<>(); for (PluginSet s : this.pluginSets) { @@ -556,6 +570,17 @@ public MLProviderInterface getMachineLearningProviderByName(String name, boolean return null; } + public ImageWorkerInterface getImageWorkerInterfaceByName(String name, boolean onlyEnabled) { + Collection plugins = getImageWorkerPlugins(onlyEnabled); + for (ImageWorkerInterface p : plugins) { + if (p.getName().equalsIgnoreCase(name)) { + // logger.info("Retrived Query Provider: "+name); + return p; + } + } + logger.debug("No image worker matching name {} for onlyEnabled = {}", name, onlyEnabled); + return null; + } public JointQueryTask queryAll(JointQueryTask holder, final String query, final Object... parameters) { List providers = this.getQueryProvidersName(true); diff --git a/sdk/pom.xml b/sdk/pom.xml index b234fb6f2..d0d065247 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -188,6 +188,12 @@ ${dcm4che.version} + + dcm4che-core + org.dcm4che + 3.3.7 + + dcm4che dcm4che-net diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java index 8f5222d3f..4ac85de70 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java @@ -10,4 +10,33 @@ public class ImageROI { private URI roi; + public ImageROI(int width, int height, URI roi) { + this.width = width; + this.height = height; + this.roi = roi; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public URI getRoi() { + return roi; + } + + public void setRoi(URI roi) { + this.roi = roi; + } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java index c25c46832..b0e4b62eb 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java @@ -1,11 +1,11 @@ package pt.ua.dicoogle.sdk.imageworker; -import org.dcm4che2.io.DicomInputStream; +import pt.ua.dicoogle.sdk.DicooglePlugin; +import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; -public abstract class ImageWorkerInterface { +public interface ImageWorkerInterface extends DicooglePlugin { + public abstract ImageROI extractROI(SearchResult sr, BulkAnnotation annotation); - public abstract ImageROI extractROI(DicomInputStream is, BulkAnnotation annotation); - - public abstract Iterable extractROIs(DicomInputStream is, Iterable annotations); + public abstract Iterable extractROIs(SearchResult sr, Iterable annotations); } From 2a2eebc458a72741492c306eeae2474c37290acb Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Wed, 30 Nov 2022 07:58:30 +0000 Subject: [PATCH 06/53] Removed image worker interface. --- .../core/mlprovider/PrepareDatasetTask.java | 4 --- .../ua/dicoogle/plugins/PluginController.java | 26 ------------------- .../java/pt/ua/dicoogle/sdk/PluginBase.java | 7 ----- .../java/pt/ua/dicoogle/sdk/PluginSet.java | 11 -------- .../sdk/imageworker/ImageWorkerInterface.java | 11 -------- 5 files changed, 59 deletions(-) delete mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java index 974b35b1f..c65a28860 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java @@ -1,14 +1,10 @@ package pt.ua.dicoogle.core.mlprovider; -import org.dcm4che3.io.DicomInputStream; import pt.ua.dicoogle.plugins.PluginController; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.imageworker.ImageROI; -import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; import pt.ua.dicoogle.sdk.mlprovider.MLDataset; import pt.ua.dicoogle.sdk.mlprovider.MLImageDataset; -import java.io.File; -import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.UUID; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java index 05ac30830..ad05e0a1c 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java @@ -32,7 +32,6 @@ import pt.ua.dicoogle.sdk.datastructs.UnindexReport; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; -import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; import pt.ua.dicoogle.sdk.mlprovider.MLDataset; import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; @@ -382,19 +381,6 @@ public Collection getMLPlugins(boolean onlyEnabled) { return plugins; } - public Collection getImageWorkerPlugins(boolean onlyEnabled) { - List plugins = new ArrayList<>(); - for (PluginSet pSet : pluginSets) { - for (ImageWorkerInterface i : pSet.getImageWorkerPlugins()) { - if (!i.isEnabled() && onlyEnabled) { - continue; - } - plugins.add(i); - } - } - return plugins; - } - public Collection getPluginSetNames() { Collection l = new ArrayList<>(); for (PluginSet s : this.pluginSets) { @@ -570,18 +556,6 @@ public MLProviderInterface getMachineLearningProviderByName(String name, boolean return null; } - public ImageWorkerInterface getImageWorkerInterfaceByName(String name, boolean onlyEnabled) { - Collection plugins = getImageWorkerPlugins(onlyEnabled); - for (ImageWorkerInterface p : plugins) { - if (p.getName().equalsIgnoreCase(name)) { - // logger.info("Retrived Query Provider: "+name); - return p; - } - } - logger.debug("No image worker matching name {} for onlyEnabled = {}", name, onlyEnabled); - return null; - } - public JointQueryTask queryAll(JointQueryTask holder, final String query, final Object... parameters) { List providers = this.getQueryProvidersName(true); return query(holder, providers, query, parameters); diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java index 6e362eacf..62ce1ad8f 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginBase.java @@ -18,7 +18,6 @@ */ package pt.ua.dicoogle.sdk; -import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; @@ -42,7 +41,6 @@ public abstract class PluginBase implements PluginSet, PlatformCommunicatorInter protected List jettyPlugins = new ArrayList<>(); protected List storagePlugins = new ArrayList<>(); protected List mlPlugins = new ArrayList<>(); - protected List imageWorkerPlugins = new ArrayList<>(); protected List services = new ArrayList<>(); protected ConfigurationHolder settings = null; @@ -91,11 +89,6 @@ public Collection getMLPlugins() { return mlPlugins; } - @Override - public Collection getImageWorkerPlugins() { - return imageWorkerPlugins; - } - @Override public void setPlatformProxy(DicooglePlatformInterface core) { this.platform = core; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java index 1d74886b9..3db10404b 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/PluginSet.java @@ -25,7 +25,6 @@ import org.restlet.resource.ServerResource; -import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; @@ -109,16 +108,6 @@ public default Collection getMLPlugins() { return Collections.EMPTY_LIST; } - /** - * Obtains a collection of ImageWorker plugins, so as to integrate Image Worker plugins such as ROI extraction plugins in Dicoogle. - * This collection must be immutable. - * @return a collection of ImageWorker plugins to the core application - * @see ImageWorkerInterface - */ - public default Collection getImageWorkerPlugins() { - return Collections.EMPTY_LIST; - } - /** * Defines the plugin's settings. This method will be called once after the plugin set was instantiated * with plugin-scoped settings. Dicoogle users can modify these settings by accessing the XML file with diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java deleted file mode 100644 index b0e4b62eb..000000000 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java +++ /dev/null @@ -1,11 +0,0 @@ -package pt.ua.dicoogle.sdk.imageworker; - -import pt.ua.dicoogle.sdk.DicooglePlugin; -import pt.ua.dicoogle.sdk.datastructs.SearchResult; -import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; - -public interface ImageWorkerInterface extends DicooglePlugin { - public abstract ImageROI extractROI(SearchResult sr, BulkAnnotation annotation); - - public abstract Iterable extractROIs(SearchResult sr, Iterable annotations); -} From 8ca7bf8f72963684ea9131b82217ae85980fac4f Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Sun, 20 Feb 2022 07:35:45 +0000 Subject: [PATCH 07/53] Added new ImageWorkerInterface --- .../sdk/imageworker/ImageWorkerInterface.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java new file mode 100644 index 000000000..c25c46832 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java @@ -0,0 +1,11 @@ +package pt.ua.dicoogle.sdk.imageworker; + +import org.dcm4che2.io.DicomInputStream; +import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; + +public abstract class ImageWorkerInterface { + + public abstract ImageROI extractROI(DicomInputStream is, BulkAnnotation annotation); + + public abstract Iterable extractROIs(DicomInputStream is, Iterable annotations); +} From 8c391d44315ebc828901ee94596e5d0be1dd74fb Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Sun, 1 May 2022 16:02:51 +0100 Subject: [PATCH 08/53] Implemented new ImageWorker interface --- .../dicoogle/sdk/imageworker/ImageWorkerInterface.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java index c25c46832..b0e4b62eb 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java @@ -1,11 +1,11 @@ package pt.ua.dicoogle.sdk.imageworker; -import org.dcm4che2.io.DicomInputStream; +import pt.ua.dicoogle.sdk.DicooglePlugin; +import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; -public abstract class ImageWorkerInterface { +public interface ImageWorkerInterface extends DicooglePlugin { + public abstract ImageROI extractROI(SearchResult sr, BulkAnnotation annotation); - public abstract ImageROI extractROI(DicomInputStream is, BulkAnnotation annotation); - - public abstract Iterable extractROIs(DicomInputStream is, Iterable annotations); + public abstract Iterable extractROIs(SearchResult sr, Iterable annotations); } From b43506759eba88860470e9859d5458bf8f6e0906 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Mon, 20 Jun 2022 14:39:06 +0100 Subject: [PATCH 09/53] Added ImageWorkerInterface to PlatformInterface --- .../dicoogle/plugins/DicooglePlatformProxy.java | 11 +++++++++++ .../core/plugins/PlatformInterfaceMock.java | 11 +++++++++++ .../sdk/core/DicooglePlatformInterface.java | 16 ++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/DicooglePlatformProxy.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/DicooglePlatformProxy.java index 6878fa77b..255edd047 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/DicooglePlatformProxy.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/DicooglePlatformProxy.java @@ -32,6 +32,7 @@ import pt.ua.dicoogle.sdk.datastructs.Report; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; +import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; import pt.ua.dicoogle.sdk.settings.server.ServerSettingsReader; import pt.ua.dicoogle.sdk.task.JointQueryTask; import pt.ua.dicoogle.sdk.task.Task; @@ -162,6 +163,16 @@ public List indexBlocking(URI path) { return pluginController.indexBlocking(path); } + @Override + public ImageWorkerInterface getImageWorkerByName(String name, boolean onlyEnabled) { + return pluginController.getImageWorkerInterfaceByName(name, onlyEnabled); + } + + @Override + public Collection getImageWorkers(boolean onlyEnabled) { + return pluginController.getImageWorkerPlugins(onlyEnabled); + } + @Override public ServerSettingsReader getSettings() { return ServerSettingsManager.getSettings(); diff --git a/dicoogle/src/test/java/pt/ua/dicoogle/core/plugins/PlatformInterfaceMock.java b/dicoogle/src/test/java/pt/ua/dicoogle/core/plugins/PlatformInterfaceMock.java index 5b2974dcf..efd0a9480 100644 --- a/dicoogle/src/test/java/pt/ua/dicoogle/core/plugins/PlatformInterfaceMock.java +++ b/dicoogle/src/test/java/pt/ua/dicoogle/core/plugins/PlatformInterfaceMock.java @@ -26,6 +26,7 @@ import pt.ua.dicoogle.sdk.datastructs.Report; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; +import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; import pt.ua.dicoogle.sdk.settings.server.ServerSettingsReader; import pt.ua.dicoogle.sdk.task.JointQueryTask; import pt.ua.dicoogle.sdk.task.Task; @@ -125,6 +126,16 @@ public List indexBlocking(URI path) { return null; } + @Override + public ImageWorkerInterface getImageWorkerByName(String name, boolean onlyEnabled) { + return null; + } + + @Override + public Collection getImageWorkers(boolean onlyEnabled) { + return null; + } + @Override public ServerSettingsReader getSettings() { return null; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/core/DicooglePlatformInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/core/DicooglePlatformInterface.java index ac3fac299..a253e74d4 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/core/DicooglePlatformInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/core/DicooglePlatformInterface.java @@ -29,6 +29,7 @@ import pt.ua.dicoogle.sdk.datastructs.Report; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; +import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; import pt.ua.dicoogle.sdk.settings.server.ServerSettingsReader; import pt.ua.dicoogle.sdk.task.JointQueryTask; import pt.ua.dicoogle.sdk.task.Task; @@ -213,6 +214,21 @@ public JointQueryTask query(JointQueryTask holder, List querySources, Di */ public List indexBlocking(URI path); + /** Gets the image worker with the given name. + * + * @param name the unique name of the worker + * @param onlyEnabled whether only enabled plugins should be retrieved (all are retrieved if {@code false}) + * @return the image worker with the given name or null if it doesn't exist + */ + public ImageWorkerInterface getImageWorkerByName(String name, boolean onlyEnabled); + + /** Gets all the image workers available. + * + * @param onlyEnabled whether only enabled plugins should be retrieved (all are retrieved if {@code false}) + * @return a collection with all image workers available + */ + public Collection getImageWorkers(boolean onlyEnabled); + /** Obtain access to the server's settings. * @return an object for read-only access to the settings */ From 4dec4ec17bb8faa4e251cda545193c081c2f28f0 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Fri, 1 Jul 2022 18:17:52 +0100 Subject: [PATCH 10/53] Changes to ImageROI and ImageWorker interfaces --- .../ua/dicoogle/sdk/imageworker/ImageROI.java | 113 ++++++++++++++++-- .../sdk/imageworker/ImageWorkerInterface.java | 19 ++- 2 files changed, 124 insertions(+), 8 deletions(-) diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java index 4ac85de70..337953fbf 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java @@ -1,19 +1,97 @@ package pt.ua.dicoogle.sdk.imageworker; +import java.io.File; import java.net.URI; +import java.util.Objects; +/** + * This object defines an Image ROI. + * It has a physical location defined by an URI. + * It is possible to write the image contents to a file defined by the URI. + * The ROI has an x and y position that identify its origin in the source image. + * The SOPInstanceUID defines where this ROI was extracted from. + */ public class ImageROI { + public enum FileType { + JPEG (".jpg", "image/jpeg"), + PNG (".png", "image/png"); + + private final String extension; + private final String mimeType; + + private FileType(String s, String mimeType) { + this.extension = s; + this.mimeType = mimeType; + } + + public String getExtension() { return extension; } + + public String getMimeType() {return mimeType; } + } + + private double x; + + private double y; + private int width; private int height; - private URI roi; + private String sopInstanceUID; + + private URI uriROI; - public ImageROI(int width, int height, URI roi) { + private FileType fileType; + + private ImageROI(URI uriROI){ + File f = new File(uriROI); + if(!f.exists()) + throw new IllegalArgumentException(String.format("URI %s does not exist", uriROI.getPath())); + this.x = 0; + this.y = 0; + } + + public ImageROI(String sopInstanceUID, int width, int height, URI uriROI, FileType fileType) { + this(uriROI); this.width = width; this.height = height; - this.roi = roi; + this.uriROI = uriROI; + this.fileType = fileType; + } + + public ImageROI(String sopInstanceUID, int x, int y, int width, int height, URI uriROI, FileType fileType) { + this(uriROI); + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.uriROI = uriROI; + this.fileType = fileType; + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public String getSopInstanceUID() { + return sopInstanceUID; + } + + public void setSopInstanceUID(String sopInstanceUID) { + this.sopInstanceUID = sopInstanceUID; } public int getWidth() { @@ -32,11 +110,32 @@ public void setHeight(int height) { this.height = height; } - public URI getRoi() { - return roi; + public URI getUriROI() { + return uriROI; + } + + public void setUriROI(URI uriROI) { + this.uriROI = uriROI; + } + + public FileType getFileType() { + return fileType; + } + + public void setFileType(FileType fileType) { + this.fileType = fileType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ImageROI imageROI = (ImageROI) o; + return Double.compare(imageROI.x, x) == 0 && Double.compare(imageROI.y, y) == 0 && width == imageROI.width && height == imageROI.height && Objects.equals(sopInstanceUID, imageROI.sopInstanceUID) && Objects.equals(uriROI, imageROI.uriROI); } - public void setRoi(URI roi) { - this.roi = roi; + @Override + public int hashCode() { + return Objects.hash(x, y, width, height, sopInstanceUID, uriROI); } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java index b0e4b62eb..5b8d6a8fc 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java @@ -4,8 +4,25 @@ import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; +import java.awt.image.BufferedImage; + public interface ImageWorkerInterface extends DicooglePlugin { - public abstract ImageROI extractROI(SearchResult sr, BulkAnnotation annotation); + /** + * Given a search result and one annotation, extract its ROI as a BufferedImage. + * This method does not write the ROI to disk. + * @param sr + * @param annotation + * @return + */ + public abstract BufferedImage extractROI(SearchResult sr, BulkAnnotation annotation); + + /** + * Given a search result and a list of annotations, extract the ROIs the annotations define on the image. + * This method will automatically write all ROIs to disk. + * @param sr + * @param annotations + * @return a list of image ROIs defined by the list of annotations + */ public abstract Iterable extractROIs(SearchResult sr, Iterable annotations); } From 52d16432e0c12bd8b3248bbec7401c9170b8854e Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Sat, 2 Jul 2022 12:35:33 +0100 Subject: [PATCH 11/53] Removed unecessary dependency --- sdk/pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sdk/pom.xml b/sdk/pom.xml index d0d065247..b234fb6f2 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -188,12 +188,6 @@ ${dcm4che.version} - - dcm4che-core - org.dcm4che - 3.3.7 - - dcm4che dcm4che-net From bb21bf42f6ded2e1c207c7073660a278b1191006 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Fri, 18 Nov 2022 07:25:42 +0000 Subject: [PATCH 12/53] Removed Image worker interface. Introduced instead new module to extract WSI ROIs. --- dicoogle/pom.xml | 6 + .../server/web/dicom/ROIExtractor.java | 2 + .../dim}/ImageROI.java | 282 +++++++++--------- .../sdk/datastructs/wsi/WSIFrame.java | 2 + .../sdk/datastructs/wsi/WSISopDescriptor.java | 2 + .../sdk/imageworker/ImageWorkerInterface.java | 28 -- 6 files changed, 153 insertions(+), 169 deletions(-) create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java rename sdk/src/main/java/pt/ua/dicoogle/sdk/{imageworker => datastructs/dim}/ImageROI.java (96%) create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSISopDescriptor.java delete mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java diff --git a/dicoogle/pom.xml b/dicoogle/pom.xml index da1818dea..9fa7bc31e 100644 --- a/dicoogle/pom.xml +++ b/dicoogle/pom.xml @@ -465,5 +465,11 @@ raven-log4j2 5.0.2 + + org.dcm4che + dcm4che-imageio + 3.3.7 + compile + diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java new file mode 100644 index 000000000..c9f53dc53 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -0,0 +1,2 @@ +package pt.ua.dicoogle.server.web.dicom;public class ROIExtractor { +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java similarity index 96% rename from sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java rename to sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java index 337953fbf..be42a8bd3 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageROI.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java @@ -1,141 +1,141 @@ -package pt.ua.dicoogle.sdk.imageworker; - -import java.io.File; -import java.net.URI; -import java.util.Objects; - -/** - * This object defines an Image ROI. - * It has a physical location defined by an URI. - * It is possible to write the image contents to a file defined by the URI. - * The ROI has an x and y position that identify its origin in the source image. - * The SOPInstanceUID defines where this ROI was extracted from. - */ -public class ImageROI { - - public enum FileType { - JPEG (".jpg", "image/jpeg"), - PNG (".png", "image/png"); - - private final String extension; - private final String mimeType; - - private FileType(String s, String mimeType) { - this.extension = s; - this.mimeType = mimeType; - } - - public String getExtension() { return extension; } - - public String getMimeType() {return mimeType; } - } - - private double x; - - private double y; - - private int width; - - private int height; - - private String sopInstanceUID; - - private URI uriROI; - - private FileType fileType; - - private ImageROI(URI uriROI){ - File f = new File(uriROI); - if(!f.exists()) - throw new IllegalArgumentException(String.format("URI %s does not exist", uriROI.getPath())); - this.x = 0; - this.y = 0; - } - - public ImageROI(String sopInstanceUID, int width, int height, URI uriROI, FileType fileType) { - this(uriROI); - this.width = width; - this.height = height; - this.uriROI = uriROI; - this.fileType = fileType; - } - - public ImageROI(String sopInstanceUID, int x, int y, int width, int height, URI uriROI, FileType fileType) { - this(uriROI); - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.uriROI = uriROI; - this.fileType = fileType; - } - - public double getX() { - return x; - } - - public void setX(double x) { - this.x = x; - } - - public double getY() { - return y; - } - - public void setY(double y) { - this.y = y; - } - - public String getSopInstanceUID() { - return sopInstanceUID; - } - - public void setSopInstanceUID(String sopInstanceUID) { - this.sopInstanceUID = sopInstanceUID; - } - - public int getWidth() { - return width; - } - - public void setWidth(int width) { - this.width = width; - } - - public int getHeight() { - return height; - } - - public void setHeight(int height) { - this.height = height; - } - - public URI getUriROI() { - return uriROI; - } - - public void setUriROI(URI uriROI) { - this.uriROI = uriROI; - } - - public FileType getFileType() { - return fileType; - } - - public void setFileType(FileType fileType) { - this.fileType = fileType; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ImageROI imageROI = (ImageROI) o; - return Double.compare(imageROI.x, x) == 0 && Double.compare(imageROI.y, y) == 0 && width == imageROI.width && height == imageROI.height && Objects.equals(sopInstanceUID, imageROI.sopInstanceUID) && Objects.equals(uriROI, imageROI.uriROI); - } - - @Override - public int hashCode() { - return Objects.hash(x, y, width, height, sopInstanceUID, uriROI); - } -} +package pt.ua.dicoogle.sdk.imageworker; + +import java.io.File; +import java.net.URI; +import java.util.Objects; + +/** + * This object defines an Image ROI. + * It has a physical location defined by an URI. + * It is possible to write the image contents to a file defined by the URI. + * The ROI has an x and y position that identify its origin in the source image. + * The SOPInstanceUID defines where this ROI was extracted from. + */ +public class ImageROI { + + public enum FileType { + JPEG (".jpg", "image/jpeg"), + PNG (".png", "image/png"); + + private final String extension; + private final String mimeType; + + private FileType(String s, String mimeType) { + this.extension = s; + this.mimeType = mimeType; + } + + public String getExtension() { return extension; } + + public String getMimeType() {return mimeType; } + } + + private double x; + + private double y; + + private int width; + + private int height; + + private String sopInstanceUID; + + private URI uriROI; + + private FileType fileType; + + private ImageROI(URI uriROI){ + File f = new File(uriROI); + if(!f.exists()) + throw new IllegalArgumentException(String.format("URI %s does not exist", uriROI.getPath())); + this.x = 0; + this.y = 0; + } + + public ImageROI(String sopInstanceUID, int width, int height, URI uriROI, FileType fileType) { + this(uriROI); + this.width = width; + this.height = height; + this.uriROI = uriROI; + this.fileType = fileType; + } + + public ImageROI(String sopInstanceUID, int x, int y, int width, int height, URI uriROI, FileType fileType) { + this(uriROI); + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.uriROI = uriROI; + this.fileType = fileType; + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public String getSopInstanceUID() { + return sopInstanceUID; + } + + public void setSopInstanceUID(String sopInstanceUID) { + this.sopInstanceUID = sopInstanceUID; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public URI getUriROI() { + return uriROI; + } + + public void setUriROI(URI uriROI) { + this.uriROI = uriROI; + } + + public FileType getFileType() { + return fileType; + } + + public void setFileType(FileType fileType) { + this.fileType = fileType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ImageROI imageROI = (ImageROI) o; + return Double.compare(imageROI.x, x) == 0 && Double.compare(imageROI.y, y) == 0 && width == imageROI.width && height == imageROI.height && Objects.equals(sopInstanceUID, imageROI.sopInstanceUID) && Objects.equals(uriROI, imageROI.uriROI); + } + + @Override + public int hashCode() { + return Objects.hash(x, y, width, height, sopInstanceUID, uriROI); + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java new file mode 100644 index 000000000..24b36e27c --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java @@ -0,0 +1,2 @@ +package pt.ua.dicoogle.sdk.datastructs.wsi;public class WSIFrame { +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSISopDescriptor.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSISopDescriptor.java new file mode 100644 index 000000000..65848f12c --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSISopDescriptor.java @@ -0,0 +1,2 @@ +package pt.ua.dicoogle.sdk.datastructs.wsi;public class WSISopDescriptor { +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java deleted file mode 100644 index 5b8d6a8fc..000000000 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/imageworker/ImageWorkerInterface.java +++ /dev/null @@ -1,28 +0,0 @@ -package pt.ua.dicoogle.sdk.imageworker; - -import pt.ua.dicoogle.sdk.DicooglePlugin; -import pt.ua.dicoogle.sdk.datastructs.SearchResult; -import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; - -import java.awt.image.BufferedImage; - -public interface ImageWorkerInterface extends DicooglePlugin { - - /** - * Given a search result and one annotation, extract its ROI as a BufferedImage. - * This method does not write the ROI to disk. - * @param sr - * @param annotation - * @return - */ - public abstract BufferedImage extractROI(SearchResult sr, BulkAnnotation annotation); - - /** - * Given a search result and a list of annotations, extract the ROIs the annotations define on the image. - * This method will automatically write all ROIs to disk. - * @param sr - * @param annotations - * @return a list of image ROIs defined by the list of annotations - */ - public abstract Iterable extractROIs(SearchResult sr, Iterable annotations); -} From 0d87c2cb5de9891117a94adf7293b078f6787363 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Fri, 18 Nov 2022 10:35:07 +0000 Subject: [PATCH 13/53] Baseline ROI extractor servlet. Fully removed ImageWorkerInterface. --- dicoogle/pom.xml | 1 - .../ua/dicoogle/server/web/DicoogleWeb.java | 14 +- .../server/web/dicom/ROIExtractor.java | 235 +++++++++++++++++- .../server/web/servlets/ROIServlet.java | 134 ++++++++++ sdk/pom.xml | 6 + .../sdk/datastructs/wsi/WSIFrame.java | 71 +++++- .../sdk/datastructs/wsi/WSISopDescriptor.java | 74 +++++- 7 files changed, 522 insertions(+), 13 deletions(-) create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java diff --git a/dicoogle/pom.xml b/dicoogle/pom.xml index 9fa7bc31e..1f8aa2959 100644 --- a/dicoogle/pom.xml +++ b/dicoogle/pom.xml @@ -469,7 +469,6 @@ org.dcm4che dcm4che-imageio 3.3.7 - compile diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index f225f936c..bcba1fc8f 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java @@ -23,14 +23,7 @@ import pt.ua.dicoogle.plugins.webui.WebUIPlugin; import pt.ua.dicoogle.sdk.utils.TagsStruct; import pt.ua.dicoogle.server.web.rest.VersionResource; -import pt.ua.dicoogle.server.web.servlets.RestletHttpServlet; -import pt.ua.dicoogle.server.web.servlets.ExportToCSVServlet; -import pt.ua.dicoogle.server.web.servlets.SettingsServlet; -import pt.ua.dicoogle.server.web.servlets.TagsServlet; -import pt.ua.dicoogle.server.web.servlets.ExportCSVToFILEServlet; -import pt.ua.dicoogle.server.web.servlets.SearchHolderServlet; -import pt.ua.dicoogle.server.web.servlets.IndexerServlet; -import pt.ua.dicoogle.server.web.servlets.ImageServlet; +import pt.ua.dicoogle.server.web.servlets.*; import pt.ua.dicoogle.server.web.servlets.plugins.PluginsServlet; import pt.ua.dicoogle.server.web.servlets.management.*; import pt.ua.dicoogle.server.web.servlets.search.*; @@ -143,6 +136,9 @@ public DicoogleWeb(int port) throws Exception { final ServletContextHandler dic2png = createServletHandler(new ImageServlet(cache), "/dic2png"); cache.start(); // start the caching system + // setup the ROI extractor + final ServletContextHandler roiExtractor = createServletHandler(new ROIServlet(cache), "/roi"); + // setup the DICOM to PNG image servlet final ServletContextHandler dictags = createServletHandler(new TagsServlet(), "/dictags"); @@ -181,7 +177,7 @@ public DicoogleWeb(int port) throws Exception { PluginRestletApplication.attachRestPlugin(new VersionResource()); // list the all the handlers mounted above - Handler[] handlers = new Handler[] {pluginHandler, legacyHandler, dic2png, dictags, + Handler[] handlers = new Handler[] {pluginHandler, legacyHandler, dic2png, roiExtractor, dictags, createServletHandler(new IndexerServlet(), "/indexer"), // DEPRECATED createServletHandler(new SettingsServlet(), "/settings"), csvServletHolder, createServletHandler(new LoginServlet(), "/login"), diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java index c9f53dc53..5a73082e2 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -1,2 +1,235 @@ -package pt.ua.dicoogle.server.web.dicom;public class ROIExtractor { +package pt.ua.dicoogle.server.web.dicom; + +import org.dcm4che3.data.Attributes; +import org.dcm4che3.imageio.plugins.dcm.DicomImageReadParam; +import org.dcm4che3.imageio.plugins.dcm.DicomImageReader; +import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; +import org.dcm4che3.io.BulkDataDescriptor; +import org.dcm4che3.io.DicomInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pt.ua.dicoogle.sdk.StorageInputStream; +import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; +import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; +import pt.ua.dicoogle.sdk.datastructs.wsi.WSIFrame; +import pt.ua.dicoogle.sdk.datastructs.wsi.WSISopDescriptor; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.zip.GZIPInputStream; + +public class ROIExtractor { + + private static final Logger logger = LoggerFactory.getLogger(ROIExtractor.class); + private static final String EXTENSION_GZIP = ".gz"; + private static final int BUFFER_SIZE = 8192; + + public BufferedImage extractROI(StorageInputStream sis, BulkAnnotation bulkAnnotation) { + + ImageReader imageReader = getImageReader(); + + DicomMetaData dicomMetaData; + try { + dicomMetaData = getDicomMetadata(sis); + } catch (IOException e) { + logger.error("Error reading DICOM file", e); + return null; + } + + DicomImageReadParam param; + try{ + imageReader.setInput(dicomMetaData); + param = (DicomImageReadParam) imageReader.getDefaultReadParam(); + } catch (Exception e){ + logger.error("Error setting image reader", e); + return null; + } + + WSISopDescriptor descriptor = new WSISopDescriptor(); + descriptor.extractData(dicomMetaData.getAttributes()); + + try { + return getROIFromAnnotation(bulkAnnotation, descriptor, imageReader, param); + } catch (IllegalArgumentException e) { + logger.error("Error writing ROI", e); + } + + return null; + } + + + + private static ImageReader getImageReader() { + Iterator iter = ImageIO.getImageReadersByFormatName("DICOM"); + while (iter.hasNext()) { + ImageReader reader = iter.next(); + if (reader instanceof DicomImageReader) + return reader; + } + return null; + } + + /** + * Given an annotation and the details of the image, this method returns the frames that intersect with the annotation. + * This method is meant for WSI images that are split into frames. + * @param descriptor the WSI descriptor + * @param annotation The annotation to intersect + * @return a 2D matrix of frames that intersect this annotation. + */ + private List> getFrameMatrixFromAnnotation(WSISopDescriptor descriptor, BulkAnnotation annotation) { + + //Number of tiles along the x direction, number of columns in the frame matrix + int nx_tiles = (int) Math.ceil(descriptor.getTotalPixelMatrixColumns() / descriptor.getTileWidth()); + + //Number of tiles along the y direction, number of rows in the frame matrix + int ny_tiles = (int) Math.ceil(descriptor.getTotalPixelMatrixRows() / descriptor.getTileHeight()); + + List> matrix = new ArrayList<>(); + + switch (annotation.getAnnotationType()) { + case RECTANGLE: + // Calculate the starting position of the annotation in frame coordinates + int x_c = (int) Math.floor(annotation.getPoints().get(0).getX() / descriptor.getTileWidth()); + int y_c = (int) Math.floor(annotation.getPoints().get(0).getY() / descriptor.getTileHeight()); + + //Annotation is completely out of bounds, no intersection possible + if(x_c > nx_tiles || y_c > ny_tiles) + return matrix; + + // Calculate the ending position of the annotation in frame coordinates + int x_e = (int) Math.floor(annotation.getPoints().get(3).getX() / descriptor.getTileWidth()); + int y_e = (int) Math.floor(annotation.getPoints().get(3).getY() / descriptor.getTileHeight()); + + //Annotation might be out of bonds, adjust that + if(x_e > (nx_tiles - 1)) + x_e = nx_tiles - 1; + + if(y_e > (ny_tiles - 1)) + y_e = ny_tiles - 1; + + for (int i = y_c; i <= y_e ; i++) { + matrix.add(new ArrayList<>()); + for (int j = x_c; j <= x_e; j++) { + WSIFrame frame = new WSIFrame(descriptor.getTileWidth(), descriptor.getTileHeight(), j, i, i * nx_tiles + j); + matrix.get(i).add(frame); + } + } + break; + } + + return matrix; + } + + /** + * Given an annotation and an image, return the section of the image the annotation intersects. + * @param annotation + * @param descriptor + * @param imageReader + * @param param + * @return + * @throws IllegalArgumentException + */ + private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDescriptor descriptor, ImageReader imageReader, DicomImageReadParam param) throws IllegalArgumentException { + if(annotation.getAnnotationType() != BulkAnnotation.AnnotationType.RECTANGLE){ + throw new IllegalArgumentException("Trying to build a ROI without a rectangle annotation"); + } + + Point2D annotationPoint1 = annotation.getPoints().get(0); + Point2D annotationPoint2 = annotation.getPoints().get(3); + + int clipX = Math.max(annotationPoint2.getX() - descriptor.getTotalPixelMatrixColumns(), 0); + int clipY = Math.max(annotationPoint2.getY() - descriptor.getTotalPixelMatrixRows(), 0); + + int annotationWidth = (int) annotation.getPoints().get(0).distance(annotation.getPoints().get(1)) - clipX; + int annotationHeight = (int) annotation.getPoints().get(0).distance(annotation.getPoints().get(2)) - clipY; + + List> frameMatrix = getFrameMatrixFromAnnotation(descriptor, annotation); + BufferedImage combined = new BufferedImage(annotationWidth, annotationHeight, BufferedImage.TYPE_INT_RGB); + Graphics g = combined.getGraphics(); + + for (List matrix : frameMatrix) { + for (WSIFrame frame : matrix) { + BufferedImage bb; + try { + bb = imageReader.read(frame.getFrameIndex(), param); + } catch (IOException e) { + logger.error("Error building ROI, skipping entry", e); + continue; + } + + //Calculate intersections between ROI and frame + Point2D framePoint1 = new Point2D(frame.getX() * descriptor.getTileWidth(), frame.getY() * descriptor.getTileHeight()); + Point2D framePoint2 = new Point2D(framePoint1.getX() + bb.getWidth(), framePoint1.getY() + bb.getHeight()); + Point2D intersectionPoint1 = new Point2D(Math.max(annotationPoint1.getX(), framePoint1.getX()), Math.max(annotationPoint1.getY(), framePoint1.getY())); + Point2D intersectionPoint2 = new Point2D(Math.min(annotationPoint2.getX(), framePoint2.getX()), Math.min(annotationPoint2.getY(), framePoint2.getY())); + + int startX = intersectionPoint1.getX() - annotationPoint1.getX(); + int startY = intersectionPoint1.getY() - annotationPoint1.getY(); + + int endX = intersectionPoint2.getX() - annotationPoint1.getX(); + int endY = intersectionPoint2.getY() - annotationPoint1.getY(); + + int frameStartX = intersectionPoint1.getX() - framePoint1.getX(); + int frameStartY = intersectionPoint1.getY() - framePoint1.getY(); + + int frameEndX = intersectionPoint2.getX() - framePoint1.getX(); + int frameEndY = intersectionPoint2.getY() - framePoint1.getY(); + + int deltaX = frameEndX - frameStartX; + int deltaY = frameEndY - frameStartY; + + //This means that the frame is smaller than the intersection area + //It can happen when we are on the edge of the image and the tiles do not have the dimensions stated in the DICOM file + if (deltaX > bb.getWidth()) { + endX = frameEndX - bb.getWidth(); + frameEndX = bb.getWidth(); + } + + if (deltaY > bb.getHeight()) { + endY = frameEndY - bb.getHeight(); + frameEndY = bb.getHeight(); + } + + g.drawImage(bb, startX, startY, + endX, endY, frameStartX, frameStartY, frameEndX, frameEndY, null); + + } + } + + g.dispose(); + return combined; + } + + private DicomMetaData getDicomMetadata(StorageInputStream sis) throws IOException{ + Attributes fmi; + Attributes dataset; + DicomInputStream dis; + + if(sis == null){ + logger.info("Storage == null"); + throw new InvalidParameterException("Could not find the desired URI"); + } + + String filePath = sis.getURI().getPath(); + if (filePath.endsWith(EXTENSION_GZIP)){ + InputStream inStream = new GZIPInputStream(new BufferedInputStream(new FileInputStream(filePath), BUFFER_SIZE)); + dis = new DicomInputStream(inStream); + } + else { + dis = new DicomInputStream(new File(filePath)); + } + dis.setIncludeBulkData(DicomInputStream.IncludeBulkData.URI); + dis.setBulkDataDescriptor(BulkDataDescriptor.PIXELDATA); + fmi = dis.readFileMetaInformation(); + dataset = dis.readDataset(-1, -1); + return new DicomMetaData(fmi, dataset); + } + } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java new file mode 100644 index 000000000..a24ab09e3 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java @@ -0,0 +1,134 @@ +package pt.ua.dicoogle.server.web.servlets; + +import org.restlet.data.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pt.ua.dicoogle.core.settings.ServerSettingsManager; +import pt.ua.dicoogle.plugins.PluginController; +import pt.ua.dicoogle.sdk.QueryInterface; +import pt.ua.dicoogle.sdk.StorageInputStream; +import pt.ua.dicoogle.sdk.datastructs.SearchResult; +import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; +import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; +import pt.ua.dicoogle.sdk.utils.QueryException; +import pt.ua.dicoogle.server.web.dicom.ROIExtractor; +import pt.ua.dicoogle.server.web.utils.LocalImageCache; + +import javax.imageio.ImageIO; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.*; + +public class ROIServlet extends HttpServlet { + + + private static final Logger logger = LoggerFactory.getLogger(ROIServlet.class); + private static final long serialVersionUID = 1L; + + private final LocalImageCache cache; + private final String queryProvider; + private final ROIExtractor roiExtractor; + private HashMap extraFields; + + /** + * Creates an image servlet. + * + * @param cache the local image caching system, can be null and if so no caching mechanism will be used. + */ + public ROIServlet(LocalImageCache cache) { + this.cache = cache; + this.roiExtractor = new ROIExtractor(); + List dicomProviders = ServerSettingsManager.getSettings().getArchiveSettings().getDIMProviders(); + queryProvider = dicomProviders.iterator().next(); + + extraFields = new HashMap<>(); + extraFields.put("SOPInstanceUID", "SOPInstanceUID"); + extraFields.put("SharedFunctionalGroupsSequence_PixelMeasuresSequence_PixelSpacing", "SharedFunctionalGroupsSequence_PixelMeasuresSequence_PixelSpacing"); + extraFields.put("Rows", "Rows"); + extraFields.put("Columns", "Columns"); + extraFields.put("NumberOfFrames", "NumberOfFrames"); + extraFields.put("TotalPixelMatrixColumns", "TotalPixelMatrixColumns"); + extraFields.put("TotalPixelMatrixRows", "TotalPixelMatrixRows"); + extraFields.put("ImageType", "ImageType"); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String sopInstanceUID = request.getParameter("uid"); + String x = request.getParameter("x"); + String y = request.getParameter("y"); + String width = request.getParameter("width"); + String height = request.getParameter("height"); + + if(sopInstanceUID == null || sopInstanceUID.isEmpty()){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "SOPInstanceUID provided was invalid"); + return; + } + + if(x == null || x.isEmpty() || y == null || y.isEmpty() || width == null || width.isEmpty() || height == null || height.isEmpty()){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "ROI provided was invalid"); + return; + } + + BulkAnnotation annotation; + try{ + int nX = Integer.parseInt(x); + int nY = Integer.parseInt(y); + int nWidth = Integer.parseInt(width); + int nHeight = Integer.parseInt(height); + annotation = new BulkAnnotation(); + Point2D tl = new Point2D(nX, nY); + Point2D tr = new Point2D(nX + nWidth, nY); + Point2D bl = new Point2D(nX, nY + nHeight); + Point2D br = new Point2D(nX + nWidth, nY + nHeight); + List points = new ArrayList<>(); + points.add(tl); points.add(tr); points.add(bl); points.add(br); + annotation.setPoints(points); + annotation.setAnnotationType(BulkAnnotation.AnnotationType.RECTANGLE); + } catch (NumberFormatException e){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "ROI provided was invalid"); + return; + } + + QueryInterface queryInterface = PluginController.getInstance().getQueryProviderByName(queryProvider, false); + + String query = String.format("SOPInstanceUID:%s", sopInstanceUID); + Iterable results; + try { + results = queryInterface.query(query, extraFields); + } catch (QueryException e) { + logger.error("Error requesting ROI for image {}", sopInstanceUID, e); + response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Error building response"); + return; + } + + if(!results.iterator().hasNext()){ + response.sendError(Status.CLIENT_ERROR_NOT_FOUND.getCode(), String.format("No instances exist with SOPInstanceUID: %s", sopInstanceUID)); + return; + } + + //We're only expecting one result + SearchResult result = results.iterator().next(); + + StorageInputStream sis = PluginController.getInstance().resolveURI(result.getURI()).iterator().next(); + + BufferedImage bi = roiExtractor.extractROI(sis, annotation); + + if(bi != null){ + response.setContentType("image/jpeg"); + OutputStream out = response.getOutputStream(); + ImageIO.write(bi, "jpg", out); + out.close(); + return; + } + + response.sendError(Status.CLIENT_ERROR_NOT_FOUND.getCode(), "Could not build ROI with the provided UID"); + } + +} diff --git a/sdk/pom.xml b/sdk/pom.xml index b234fb6f2..d89b23cf9 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -242,6 +242,12 @@ slf4j-api ${slf4j.version} + + org.dcm4che + dcm4che-core + 3.3.7 + compile + diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java index 24b36e27c..af17b6721 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java @@ -1,2 +1,71 @@ -package pt.ua.dicoogle.sdk.datastructs.wsi;public class WSIFrame { +package pt.ua.dicoogle.sdk.datastructs.wsi; + +public class WSIFrame { + + private int width; + private int height; + private int x; + private int y; + private int frameIndex; + + /** + * Construct a WSI frame. + * @param width the width of the frame + * @param height the height of the frame + * @param x the x coordinate of this frame in the frame matrix + * @param y the y coordinate of this frame in the frame matrix + * @param frameIndex the index of the frame that uniquely identifies this frame in the WSI + */ + public WSIFrame(int width, int height, int x, int y, int frameIndex) { + this.width = width; + this.height = height; + this.x = x; + this.y = y; + this.frameIndex = frameIndex; + } + + public WSIFrame(int width, int height) { + this.width = width; + this.height = height; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + + public int getY() { + return y; + } + + public void setY(int y) { + this.y = y; + } + + public int getFrameIndex() { + return frameIndex; + } + + public void setFrameIndex(int frameIndex) { + this.frameIndex = frameIndex; + } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSISopDescriptor.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSISopDescriptor.java index 65848f12c..c39464551 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSISopDescriptor.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSISopDescriptor.java @@ -1,2 +1,74 @@ -package pt.ua.dicoogle.sdk.datastructs.wsi;public class WSISopDescriptor { +package pt.ua.dicoogle.sdk.datastructs.wsi; + +import org.dcm4che2.data.Tag; +import org.dcm4che3.data.Attributes; +/** + * Utility class to describe a specific resolution level of a WSI pyramid + */ +public class WSISopDescriptor { + + private int tileWidth; + private int tileHeight; + private int totalPixelMatrixColumns; + private int totalPixelMatrixRows; + + public WSISopDescriptor(int tileWidth, int tileHeight, int totalPixelMatrixColumns, int totalPixelMatrixRows) { + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + this.totalPixelMatrixColumns = totalPixelMatrixColumns; + this.totalPixelMatrixRows = totalPixelMatrixRows; + } + + public WSISopDescriptor() { + } + + public int getTileWidth() { + return tileWidth; + } + + public void setTileWidth(int tileWidth) { + this.tileWidth = tileWidth; + } + + public int getTileHeight() { + return tileHeight; + } + + public void setTileHeight(int tileHeight) { + this.tileHeight = tileHeight; + } + + public int getTotalPixelMatrixColumns() { + return totalPixelMatrixColumns; + } + + public void setTotalPixelMatrixColumns(int totalPixelMatrixColumns) { + this.totalPixelMatrixColumns = totalPixelMatrixColumns; + } + + public int getTotalPixelMatrixRows() { + return totalPixelMatrixRows; + } + + public void setTotalPixelMatrixRows(int totalPixelMatrixRows) { + this.totalPixelMatrixRows = totalPixelMatrixRows; + } + + /** + * Given a search result, extract if possible the information that describes the resolution level contained within. + * @param attrs + */ + public void extractData(Attributes attrs){ + String strRows = attrs.getString(Tag.Rows); + String strColumns = attrs.getString(Tag.Columns); + String strTotalPixelMatrixColumns = attrs.getString(Tag.TotalPixelMatrixColumns); + String strTotalPixelMatrixRows = attrs.getString(Tag.TotalPixelMatrixRows); + + this.totalPixelMatrixColumns = Integer.parseInt(strTotalPixelMatrixColumns); + this.totalPixelMatrixRows = Integer.parseInt(strTotalPixelMatrixRows); + this.tileHeight = Integer.parseInt(strRows.split("\\.")[0]); + this.tileWidth = Integer.parseInt(strColumns.split("\\.")[0]); + } + } + From 268d7d3ac21077b030f22bf7bdd22d260670f860 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Wed, 23 Nov 2022 04:50:05 +0000 Subject: [PATCH 14/53] Fixed compilation issues. --- .../dicoogle/plugins/DicooglePlatformProxy.java | 11 ----------- .../pt/ua/dicoogle/server/web/DicoogleWeb.java | 2 +- .../dicoogle/server/web/servlets/ROIServlet.java | 7 ++----- .../core/plugins/PlatformInterfaceMock.java | 11 ----------- .../sdk/core/DicooglePlatformInterface.java | 16 ---------------- 5 files changed, 3 insertions(+), 44 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/DicooglePlatformProxy.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/DicooglePlatformProxy.java index 255edd047..6878fa77b 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/DicooglePlatformProxy.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/DicooglePlatformProxy.java @@ -32,7 +32,6 @@ import pt.ua.dicoogle.sdk.datastructs.Report; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; -import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; import pt.ua.dicoogle.sdk.settings.server.ServerSettingsReader; import pt.ua.dicoogle.sdk.task.JointQueryTask; import pt.ua.dicoogle.sdk.task.Task; @@ -163,16 +162,6 @@ public List indexBlocking(URI path) { return pluginController.indexBlocking(path); } - @Override - public ImageWorkerInterface getImageWorkerByName(String name, boolean onlyEnabled) { - return pluginController.getImageWorkerInterfaceByName(name, onlyEnabled); - } - - @Override - public Collection getImageWorkers(boolean onlyEnabled) { - return pluginController.getImageWorkerPlugins(onlyEnabled); - } - @Override public ServerSettingsReader getSettings() { return ServerSettingsManager.getSettings(); diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index bcba1fc8f..919ec8e97 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java @@ -137,7 +137,7 @@ public DicoogleWeb(int port) throws Exception { cache.start(); // start the caching system // setup the ROI extractor - final ServletContextHandler roiExtractor = createServletHandler(new ROIServlet(cache), "/roi"); + final ServletContextHandler roiExtractor = createServletHandler(new ROIServlet(), "/roi"); // setup the DICOM to PNG image servlet final ServletContextHandler dictags = createServletHandler(new TagsServlet(), "/dictags"); diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java index a24ab09e3..78d25ad72 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java @@ -30,18 +30,15 @@ public class ROIServlet extends HttpServlet { private static final Logger logger = LoggerFactory.getLogger(ROIServlet.class); private static final long serialVersionUID = 1L; - private final LocalImageCache cache; private final String queryProvider; private final ROIExtractor roiExtractor; private HashMap extraFields; /** - * Creates an image servlet. + * Creates ROI servlet servlet. * - * @param cache the local image caching system, can be null and if so no caching mechanism will be used. */ - public ROIServlet(LocalImageCache cache) { - this.cache = cache; + public ROIServlet() { this.roiExtractor = new ROIExtractor(); List dicomProviders = ServerSettingsManager.getSettings().getArchiveSettings().getDIMProviders(); queryProvider = dicomProviders.iterator().next(); diff --git a/dicoogle/src/test/java/pt/ua/dicoogle/core/plugins/PlatformInterfaceMock.java b/dicoogle/src/test/java/pt/ua/dicoogle/core/plugins/PlatformInterfaceMock.java index efd0a9480..5b2974dcf 100644 --- a/dicoogle/src/test/java/pt/ua/dicoogle/core/plugins/PlatformInterfaceMock.java +++ b/dicoogle/src/test/java/pt/ua/dicoogle/core/plugins/PlatformInterfaceMock.java @@ -26,7 +26,6 @@ import pt.ua.dicoogle.sdk.datastructs.Report; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; -import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; import pt.ua.dicoogle.sdk.settings.server.ServerSettingsReader; import pt.ua.dicoogle.sdk.task.JointQueryTask; import pt.ua.dicoogle.sdk.task.Task; @@ -126,16 +125,6 @@ public List indexBlocking(URI path) { return null; } - @Override - public ImageWorkerInterface getImageWorkerByName(String name, boolean onlyEnabled) { - return null; - } - - @Override - public Collection getImageWorkers(boolean onlyEnabled) { - return null; - } - @Override public ServerSettingsReader getSettings() { return null; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/core/DicooglePlatformInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/core/DicooglePlatformInterface.java index a253e74d4..ac3fac299 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/core/DicooglePlatformInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/core/DicooglePlatformInterface.java @@ -29,7 +29,6 @@ import pt.ua.dicoogle.sdk.datastructs.Report; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; -import pt.ua.dicoogle.sdk.imageworker.ImageWorkerInterface; import pt.ua.dicoogle.sdk.settings.server.ServerSettingsReader; import pt.ua.dicoogle.sdk.task.JointQueryTask; import pt.ua.dicoogle.sdk.task.Task; @@ -214,21 +213,6 @@ public JointQueryTask query(JointQueryTask holder, List querySources, Di */ public List indexBlocking(URI path); - /** Gets the image worker with the given name. - * - * @param name the unique name of the worker - * @param onlyEnabled whether only enabled plugins should be retrieved (all are retrieved if {@code false}) - * @return the image worker with the given name or null if it doesn't exist - */ - public ImageWorkerInterface getImageWorkerByName(String name, boolean onlyEnabled); - - /** Gets all the image workers available. - * - * @param onlyEnabled whether only enabled plugins should be retrieved (all are retrieved if {@code false}) - * @return a collection with all image workers available - */ - public Collection getImageWorkers(boolean onlyEnabled); - /** Obtain access to the server's settings. * @return an object for read-only access to the settings */ From 6633ada10e8824b0392d517f3a53f67d4b6ee52e Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Sat, 26 Nov 2022 00:17:57 +0000 Subject: [PATCH 15/53] Finished implementation of ROI extractor. Added cache to make it faster. --- dicoogle/pom.xml | 5 + .../src/main/java/pt/ua/dicoogle/Main.java | 8 ++ .../server/web/dicom/ROIExtractor.java | 46 +------ .../server/web/servlets/ROIServlet.java | 69 ++++------ .../server/web/utils/cache/MemoryCache.java | 46 +++++++ .../server/web/utils/cache/WSICache.java | 125 ++++++++++++++++++ 6 files changed, 211 insertions(+), 88 deletions(-) create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/MemoryCache.java create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java diff --git a/dicoogle/pom.xml b/dicoogle/pom.xml index 1f8aa2959..e7fdee162 100644 --- a/dicoogle/pom.xml +++ b/dicoogle/pom.xml @@ -470,5 +470,10 @@ dcm4che-imageio 3.3.7 + + org.dcm4che + dcm4che-core + 3.3.7 + diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/Main.java b/dicoogle/src/main/java/pt/ua/dicoogle/Main.java index 699a67bb1..e0c9491ac 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/Main.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/Main.java @@ -19,6 +19,7 @@ package pt.ua.dicoogle; import org.dcm4che2.data.TransferSyntax; +import org.dcm4che3.imageio.plugins.dcm.DicomImageReaderSpi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; @@ -31,6 +32,8 @@ import pt.ua.dicoogle.sdk.settings.server.ServerSettings; import pt.ua.dicoogle.server.web.auth.Authentication; +import javax.imageio.ImageIO; +import javax.imageio.spi.IIORegistry; import javax.swing.*; import java.awt.*; import java.io.File; @@ -172,6 +175,11 @@ private static void LaunchDicoogle() { // Start the initial Services of Dicoogle pt.ua.dicoogle.server.ControlServices.getInstance(); + // Register Image Reader for DICOM Objects + IIORegistry.getDefaultInstance().registerServiceProvider(new DicomImageReaderSpi()); + ImageIO.setUseCache(false); + System.setProperty("dcm4che.useImageIOServiceRegistry", "true"); + // Launch Async Index // It monitors a folder, and when a file is touched an event // triggers and index is updated. diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java index 5a73082e2..c18d7c334 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -1,47 +1,35 @@ package pt.ua.dicoogle.server.web.dicom; -import org.dcm4che3.data.Attributes; import org.dcm4che3.imageio.plugins.dcm.DicomImageReadParam; import org.dcm4che3.imageio.plugins.dcm.DicomImageReader; import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; -import org.dcm4che3.io.BulkDataDescriptor; -import org.dcm4che3.io.DicomInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import pt.ua.dicoogle.sdk.StorageInputStream; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; import pt.ua.dicoogle.sdk.datastructs.wsi.WSIFrame; import pt.ua.dicoogle.sdk.datastructs.wsi.WSISopDescriptor; +import pt.ua.dicoogle.server.web.utils.cache.WSICache; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; -import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import java.util.zip.GZIPInputStream; public class ROIExtractor { private static final Logger logger = LoggerFactory.getLogger(ROIExtractor.class); - private static final String EXTENSION_GZIP = ".gz"; - private static final int BUFFER_SIZE = 8192; - public BufferedImage extractROI(StorageInputStream sis, BulkAnnotation bulkAnnotation) { + public BufferedImage extractROI(DicomMetaData dicomMetaData, BulkAnnotation bulkAnnotation) { ImageReader imageReader = getImageReader(); - DicomMetaData dicomMetaData; - try { - dicomMetaData = getDicomMetadata(sis); - } catch (IOException e) { - logger.error("Error reading DICOM file", e); + if(imageReader == null) return null; - } DicomImageReadParam param; try{ @@ -64,8 +52,6 @@ public BufferedImage extractROI(StorageInputStream sis, BulkAnnotation bulkAnnot return null; } - - private static ImageReader getImageReader() { Iterator iter = ImageIO.getImageReadersByFormatName("DICOM"); while (iter.hasNext()) { @@ -206,30 +192,4 @@ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDesc g.dispose(); return combined; } - - private DicomMetaData getDicomMetadata(StorageInputStream sis) throws IOException{ - Attributes fmi; - Attributes dataset; - DicomInputStream dis; - - if(sis == null){ - logger.info("Storage == null"); - throw new InvalidParameterException("Could not find the desired URI"); - } - - String filePath = sis.getURI().getPath(); - if (filePath.endsWith(EXTENSION_GZIP)){ - InputStream inStream = new GZIPInputStream(new BufferedInputStream(new FileInputStream(filePath), BUFFER_SIZE)); - dis = new DicomInputStream(inStream); - } - else { - dis = new DicomInputStream(new File(filePath)); - } - dis.setIncludeBulkData(DicomInputStream.IncludeBulkData.URI); - dis.setBulkDataDescriptor(BulkDataDescriptor.PIXELDATA); - fmi = dis.readFileMetaInformation(); - dataset = dis.readDataset(-1, -1); - return new DicomMetaData(fmi, dataset); - } - } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java index 78d25ad72..8c38f2c73 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java @@ -1,18 +1,13 @@ package pt.ua.dicoogle.server.web.servlets; +import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; import org.restlet.data.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import pt.ua.dicoogle.core.settings.ServerSettingsManager; -import pt.ua.dicoogle.plugins.PluginController; -import pt.ua.dicoogle.sdk.QueryInterface; -import pt.ua.dicoogle.sdk.StorageInputStream; -import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; -import pt.ua.dicoogle.sdk.utils.QueryException; import pt.ua.dicoogle.server.web.dicom.ROIExtractor; -import pt.ua.dicoogle.server.web.utils.LocalImageCache; +import pt.ua.dicoogle.server.web.utils.cache.WSICache; import javax.imageio.ImageIO; import javax.servlet.ServletException; @@ -26,13 +21,11 @@ public class ROIServlet extends HttpServlet { - private static final Logger logger = LoggerFactory.getLogger(ROIServlet.class); private static final long serialVersionUID = 1L; - private final String queryProvider; + private final WSICache wsiCache; private final ROIExtractor roiExtractor; - private HashMap extraFields; /** * Creates ROI servlet servlet. @@ -40,23 +33,13 @@ public class ROIServlet extends HttpServlet { */ public ROIServlet() { this.roiExtractor = new ROIExtractor(); - List dicomProviders = ServerSettingsManager.getSettings().getArchiveSettings().getDIMProviders(); - queryProvider = dicomProviders.iterator().next(); - - extraFields = new HashMap<>(); - extraFields.put("SOPInstanceUID", "SOPInstanceUID"); - extraFields.put("SharedFunctionalGroupsSequence_PixelMeasuresSequence_PixelSpacing", "SharedFunctionalGroupsSequence_PixelMeasuresSequence_PixelSpacing"); - extraFields.put("Rows", "Rows"); - extraFields.put("Columns", "Columns"); - extraFields.put("NumberOfFrames", "NumberOfFrames"); - extraFields.put("TotalPixelMatrixColumns", "TotalPixelMatrixColumns"); - extraFields.put("TotalPixelMatrixRows", "TotalPixelMatrixRows"); - extraFields.put("ImageType", "ImageType"); + this.wsiCache = WSICache.getInstance(); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String sopInstanceUID = request.getParameter("uid"); String x = request.getParameter("x"); String y = request.getParameter("y"); @@ -73,6 +56,20 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) return; } + DicomMetaData dicomMetaData; + try { + dicomMetaData = getDicomMetadata(sopInstanceUID); + } catch (IOException e) { + logger.error("Error reading DICOM file", e); + response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "There was an error reading the file"); + return; + } + + if(dicomMetaData == null){ + response.sendError(Status.CLIENT_ERROR_NOT_FOUND.getCode(), String.format("No instances exist with SOPInstanceUID: %s", sopInstanceUID)); + return; + } + BulkAnnotation annotation; try{ int nX = Integer.parseInt(x); @@ -93,29 +90,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) return; } - QueryInterface queryInterface = PluginController.getInstance().getQueryProviderByName(queryProvider, false); - - String query = String.format("SOPInstanceUID:%s", sopInstanceUID); - Iterable results; - try { - results = queryInterface.query(query, extraFields); - } catch (QueryException e) { - logger.error("Error requesting ROI for image {}", sopInstanceUID, e); - response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Error building response"); - return; - } - - if(!results.iterator().hasNext()){ - response.sendError(Status.CLIENT_ERROR_NOT_FOUND.getCode(), String.format("No instances exist with SOPInstanceUID: %s", sopInstanceUID)); - return; - } - - //We're only expecting one result - SearchResult result = results.iterator().next(); - - StorageInputStream sis = PluginController.getInstance().resolveURI(result.getURI()).iterator().next(); - - BufferedImage bi = roiExtractor.extractROI(sis, annotation); + BufferedImage bi = roiExtractor.extractROI(dicomMetaData, annotation); if(bi != null){ response.setContentType("image/jpeg"); @@ -128,4 +103,8 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) response.sendError(Status.CLIENT_ERROR_NOT_FOUND.getCode(), "Could not build ROI with the provided UID"); } + private DicomMetaData getDicomMetadata(String sop) throws IOException{ + return wsiCache.get(sop); + } + } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/MemoryCache.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/MemoryCache.java new file mode 100644 index 000000000..dd7311f87 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/MemoryCache.java @@ -0,0 +1,46 @@ +package pt.ua.dicoogle.server.web.utils.cache; + +import com.google.common.cache.LoadingCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ExecutionException; + +/** + * General purpose memory cache to be used by plugins + * @author Rui Jesus + * @param + */ +public abstract class MemoryCache { + + protected LoadingCache memoryCache; + protected Logger logger = LoggerFactory.getLogger(this.getClass()); + + protected int hoursToKeep = 12; + protected int maximumSize = 1000; + + protected MemoryCache() { + } + + protected MemoryCache(int hoursToKeep, int maximumSize) { + this.hoursToKeep = hoursToKeep; + this.maximumSize = maximumSize; + } + + public T get(String key){ + try { + return memoryCache.get(key); + } catch (ExecutionException e) { + logger.error("Error retrieving key {} from cache", key, e); + return null; + } + } + + public int getHoursToKeep() { + return hoursToKeep; + } + + public int getMaximumSize() { + return maximumSize; + } +} \ No newline at end of file diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java new file mode 100644 index 000000000..d882af317 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java @@ -0,0 +1,125 @@ +package pt.ua.dicoogle.server.web.utils.cache; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import org.dcm4che3.data.Attributes; +import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; +import org.dcm4che3.io.BulkDataDescriptor; +import org.dcm4che3.io.DicomInputStream; +import pt.ua.dicoogle.core.settings.ServerSettingsManager; +import pt.ua.dicoogle.plugins.PluginController; +import pt.ua.dicoogle.sdk.QueryInterface; +import pt.ua.dicoogle.sdk.StorageInputStream; +import pt.ua.dicoogle.sdk.datastructs.SearchResult; +import pt.ua.dicoogle.sdk.utils.QueryException; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URI; +import java.security.InvalidParameterException; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; + +/** + * Cache used to store DicomMetadata objects temporarily, as they are quite heavy to build on-demand + */ +public class WSICache extends MemoryCache{ + + private static WSICache instance = null; + private static final String EXTENSION_GZIP = ".gz"; + private static final int BUFFER_SIZE = 8192; + + private QueryInterface queryInterface; + private final String queryProvider; + + private WSICache(){ + super(); + memoryCache = CacheBuilder.newBuilder() + .maximumSize(maximumSize) + .expireAfterAccess(hoursToKeep, TimeUnit.HOURS) + .build(new WsiDcmLoader()); + + List dicomProviders = ServerSettingsManager.getSettings().getArchiveSettings().getDIMProviders(); + queryProvider = dicomProviders.iterator().next(); + } + + public static synchronized WSICache getInstance(){ + if (instance==null){ + instance = new WSICache(); + } + return instance; + } + + private class WsiDcmLoader extends CacheLoader { + + @Override + public DicomMetaData load(String key) throws Exception { + + queryInterface = PluginController.getInstance().getQueryProviderByName(queryProvider, false); + + URI uri = retrieveURI(key); + if(uri == null){ + logger.info("URI == null"); + throw new InvalidParameterException("Could not find the desired URI"); + } + + Attributes fmi; + Attributes dataset; + DicomInputStream dis; + StorageInputStream sis = retrieveInputStream(uri); + + if(sis == null){ + throw new InvalidParameterException("Could not find the desired URI"); + } + + String filePath = sis.getURI().getPath(); + if (filePath.endsWith(EXTENSION_GZIP)){ + InputStream inStream = new GZIPInputStream(new BufferedInputStream(new FileInputStream(filePath), BUFFER_SIZE)); + dis = new DicomInputStream(inStream); + } + else { + dis = new DicomInputStream(new File(filePath)); + } + + dis.setIncludeBulkData(DicomInputStream.IncludeBulkData.URI); + dis.setBulkDataDescriptor(BulkDataDescriptor.PIXELDATA); + fmi = dis.readFileMetaInformation(); + dataset = dis.readDataset(-1, -1); + return new DicomMetaData(fmi, dataset); + } + + } + + /** + * Helper method to retrieve the URI from SOPInstanceUID in lucene. + * + * @param sop SopInstanceUID + * @return uri of the SopInstance + */ + private URI retrieveURI(String sop){ + String query = "SOPInstanceUID:" + sop; + + Iterable results; + try { + results = queryInterface.query(query); + } catch (QueryException e) { + logger.error("Could not complete query:", e); + return null; + } + + for (SearchResult first : results) { + if (first != null) { + return first.getURI(); + } + } + return null; + } + + private StorageInputStream retrieveInputStream(URI uri){ + return PluginController.getInstance().resolveURI(uri).iterator().next(); + } + +} From 863dbd8237c503f705182e8375a73f95c11d5702 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Sun, 27 Nov 2022 00:06:09 +0000 Subject: [PATCH 16/53] Moved some code into roi extractor. --- .../server/web/dicom/ROIExtractor.java | 44 ++++++++++++++----- .../server/web/servlets/ROIServlet.java | 23 +--------- .../server/web/utils/cache/WSICache.java | 5 ++- .../dicoogle/sdk/datastructs/dim/Point2D.java | 3 ++ 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java index c18d7c334..f59b36a9a 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -23,14 +23,31 @@ public class ROIExtractor { private static final Logger logger = LoggerFactory.getLogger(ROIExtractor.class); + private final WSICache wsiCache; - public BufferedImage extractROI(DicomMetaData dicomMetaData, BulkAnnotation bulkAnnotation) { + public ROIExtractor() { + this.wsiCache = WSICache.getInstance(); + } + + public BufferedImage extractROI(String sopInstanceUID, BulkAnnotation bulkAnnotation) { ImageReader imageReader = getImageReader(); if(imageReader == null) return null; + DicomMetaData dicomMetaData; + try { + dicomMetaData = getDicomMetadata(sopInstanceUID); + } catch (IOException e) { + logger.error("Error reading DICOM file", e); + return null; + } + + if(dicomMetaData == null){ + return null; + } + DicomImageReadParam param; try{ imageReader.setInput(dicomMetaData); @@ -72,26 +89,26 @@ private static ImageReader getImageReader() { private List> getFrameMatrixFromAnnotation(WSISopDescriptor descriptor, BulkAnnotation annotation) { //Number of tiles along the x direction, number of columns in the frame matrix - int nx_tiles = (int) Math.ceil(descriptor.getTotalPixelMatrixColumns() / descriptor.getTileWidth()); + int nx_tiles = (int) Math.ceil((descriptor.getTotalPixelMatrixColumns() * 1.0) / descriptor.getTileWidth()); //Number of tiles along the y direction, number of rows in the frame matrix - int ny_tiles = (int) Math.ceil(descriptor.getTotalPixelMatrixRows() / descriptor.getTileHeight()); + int ny_tiles = (int) Math.ceil((descriptor.getTotalPixelMatrixRows() * 1.0) / descriptor.getTileHeight()); List> matrix = new ArrayList<>(); switch (annotation.getAnnotationType()) { case RECTANGLE: // Calculate the starting position of the annotation in frame coordinates - int x_c = (int) Math.floor(annotation.getPoints().get(0).getX() / descriptor.getTileWidth()); - int y_c = (int) Math.floor(annotation.getPoints().get(0).getY() / descriptor.getTileHeight()); + int x_c = annotation.getPoints().get(0).getX() / descriptor.getTileWidth(); + int y_c = annotation.getPoints().get(0).getY() / descriptor.getTileHeight(); //Annotation is completely out of bounds, no intersection possible if(x_c > nx_tiles || y_c > ny_tiles) return matrix; // Calculate the ending position of the annotation in frame coordinates - int x_e = (int) Math.floor(annotation.getPoints().get(3).getX() / descriptor.getTileWidth()); - int y_e = (int) Math.floor(annotation.getPoints().get(3).getY() / descriptor.getTileHeight()); + int x_e = annotation.getPoints().get(3).getX() / descriptor.getTileWidth(); + int y_e = annotation.getPoints().get(3).getY() / descriptor.getTileHeight(); //Annotation might be out of bonds, adjust that if(x_e > (nx_tiles - 1)) @@ -115,12 +132,13 @@ private List> getFrameMatrixFromAnnotation(WSISopDescriptor descr /** * Given an annotation and an image, return the section of the image the annotation intersects. - * @param annotation - * @param descriptor + * It only works with rectangle type annotations. + * @param annotation the annotation to intersect + * @param descriptor descriptor of the WSI pyramid, contains information about the dimmensions of the image. * @param imageReader * @param param - * @return - * @throws IllegalArgumentException + * @return the intersection of the annotation on the image. + * @throws IllegalArgumentException when the annotation is not one of the supported types. */ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDescriptor descriptor, ImageReader imageReader, DicomImageReadParam param) throws IllegalArgumentException { if(annotation.getAnnotationType() != BulkAnnotation.AnnotationType.RECTANGLE){ @@ -192,4 +210,8 @@ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDesc g.dispose(); return combined; } + + private DicomMetaData getDicomMetadata(String sop) throws IOException{ + return wsiCache.get(sop); + } } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java index 8c38f2c73..ad4f6ac4a 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java @@ -1,6 +1,5 @@ package pt.ua.dicoogle.server.web.servlets; -import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; import org.restlet.data.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,7 +23,6 @@ public class ROIServlet extends HttpServlet { private static final Logger logger = LoggerFactory.getLogger(ROIServlet.class); private static final long serialVersionUID = 1L; - private final WSICache wsiCache; private final ROIExtractor roiExtractor; /** @@ -33,7 +31,6 @@ public class ROIServlet extends HttpServlet { */ public ROIServlet() { this.roiExtractor = new ROIExtractor(); - this.wsiCache = WSICache.getInstance(); } @Override @@ -56,20 +53,6 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) return; } - DicomMetaData dicomMetaData; - try { - dicomMetaData = getDicomMetadata(sopInstanceUID); - } catch (IOException e) { - logger.error("Error reading DICOM file", e); - response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "There was an error reading the file"); - return; - } - - if(dicomMetaData == null){ - response.sendError(Status.CLIENT_ERROR_NOT_FOUND.getCode(), String.format("No instances exist with SOPInstanceUID: %s", sopInstanceUID)); - return; - } - BulkAnnotation annotation; try{ int nX = Integer.parseInt(x); @@ -90,7 +73,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) return; } - BufferedImage bi = roiExtractor.extractROI(dicomMetaData, annotation); + BufferedImage bi = roiExtractor.extractROI(sopInstanceUID, annotation); if(bi != null){ response.setContentType("image/jpeg"); @@ -103,8 +86,4 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) response.sendError(Status.CLIENT_ERROR_NOT_FOUND.getCode(), "Could not build ROI with the provided UID"); } - private DicomMetaData getDicomMetadata(String sop) throws IOException{ - return wsiCache.get(sop); - } - } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java index d882af317..8b4a7dea6 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java @@ -25,6 +25,7 @@ /** * Cache used to store DicomMetadata objects temporarily, as they are quite heavy to build on-demand + * @author Rui Jesus */ public class WSICache extends MemoryCache{ @@ -56,11 +57,11 @@ public static synchronized WSICache getInstance(){ private class WsiDcmLoader extends CacheLoader { @Override - public DicomMetaData load(String key) throws Exception { + public DicomMetaData load(String sopInstanceUID) throws Exception { queryInterface = PluginController.getInstance().getQueryProviderByName(queryProvider, false); - URI uri = retrieveURI(key); + URI uri = retrieveURI(sopInstanceUID); if(uri == null){ logger.info("URI == null"); throw new InvalidParameterException("Could not find the desired URI"); diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java index 63ff33017..e5d2294a7 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java @@ -1,5 +1,8 @@ package pt.ua.dicoogle.sdk.datastructs.dim; +/** + * A point in pixel coordinates, to be used to identify coordinates in pixel space. + */ public class Point2D { private int x; From 969beb0a1579053fa929138948635a877d5af5b2 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Wed, 30 Nov 2022 07:50:15 +0000 Subject: [PATCH 17/53] Removed unused import. --- .../java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java index ad4f6ac4a..638882bdd 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java @@ -6,7 +6,6 @@ import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; import pt.ua.dicoogle.server.web.dicom.ROIExtractor; -import pt.ua.dicoogle.server.web.utils.cache.WSICache; import javax.imageio.ImageIO; import javax.servlet.ServletException; @@ -86,4 +85,4 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) response.sendError(Status.CLIENT_ERROR_NOT_FOUND.getCode(), "Could not build ROI with the provided UID"); } -} +} \ No newline at end of file From 9d09bd6cd39304fc4f8fda43e2c6c3a2eb6210f9 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Wed, 30 Nov 2022 12:34:46 +0000 Subject: [PATCH 18/53] Started implementation of makePrediction endpoint. --- .../ua/dicoogle/plugins/PluginController.java | 23 ++++- .../mlprovider/MakePredictionServlet.java | 97 ++++++++++++++++++- .../sdk/datastructs/dim/ImageROI.java | 27 +----- .../sdk/mlprovider/MLProviderInterface.java | 17 ++-- 4 files changed, 128 insertions(+), 36 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java index ad05e0a1c..9593465d8 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java @@ -32,7 +32,9 @@ import pt.ua.dicoogle.sdk.datastructs.UnindexReport; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; +import pt.ua.dicoogle.sdk.datastructs.dim.ImageROI; import pt.ua.dicoogle.sdk.mlprovider.MLDataset; +import pt.ua.dicoogle.sdk.mlprovider.MLPrediction; import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; import pt.ua.dicoogle.sdk.task.JointQueryTask; @@ -44,6 +46,8 @@ import pt.ua.dicoogle.taskManager.RunningIndexTasks; import pt.ua.dicoogle.taskManager.TaskManager; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.net.URI; @@ -574,7 +578,6 @@ public Task> query(String querySource, final String query } - public Task> query(String querySource, final String query, final DimLevel level, final Object... parameters) { Task> t = getTaskForQueryDim(querySource, query, level, parameters); @@ -875,12 +878,22 @@ public List indexBlocking(URI path) { } /** - * This method creates a {@link PrepareDatasetTask}. - * The task is responsible for creating a directory where the processed dataset will be placed. - * After the task is finished, the chosen mlProvider will be invoked to upload the dataset. - * @param datasetRequest the dataset to upload. + * This method orders a prediction on the selected image, using the selected provider. + * @param bos image to classify. + * @param provider provider to use. * @return the created task */ + public Task makePredictionOverImage(final ByteArrayOutputStream bos, final String provider) { + MLProviderInterface providerInterface = this.getMachineLearningProviderByName(provider, true); + if(providerInterface == null) + return null; + + String taskName = "MLPredictionTask" + UUID.randomUUID(); + Task result = providerInterface.makePredictionOverImage(bos); + result.setName(taskName); + return result; + } + public Task prepareMLDataset(final CreateDatasetRequest datasetRequest) { String uuid = UUID.randomUUID().toString(); Task prepareTask = diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index 19702a205..b65bb14fe 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -1,18 +1,113 @@ package pt.ua.dicoogle.server.web.servlets.mlprovider; +import org.restlet.data.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import pt.ua.dicoogle.plugins.PluginController; +import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; +import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; +import pt.ua.dicoogle.sdk.mlprovider.MLPrediction; +import pt.ua.dicoogle.sdk.task.Task; +import pt.ua.dicoogle.server.web.dicom.ROIExtractor; +import javax.imageio.ImageIO; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; public class MakePredictionServlet extends HttpServlet { private static final Logger log = LoggerFactory.getLogger(MakePredictionServlet.class); + private final ROIExtractor roiExtractor; + + /** + * Creates ROI servlet servlet. + * + */ + public MakePredictionServlet() { + this.roiExtractor = new ROIExtractor(); + } + @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {} + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + String sopInstanceUID = request.getParameter("uid"); + String x = request.getParameter("x"); + String y = request.getParameter("y"); + String width = request.getParameter("width"); + String height = request.getParameter("height"); + String provider = request.getParameter("provider"); + + if(sopInstanceUID == null || sopInstanceUID.isEmpty()){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "SOPInstanceUID provided was invalid"); + return; + } + + if(provider == null || provider.isEmpty()){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Provider provided was invalid"); + return; + } + + if(x == null || x.isEmpty() || y == null || y.isEmpty() || width == null || width.isEmpty() || height == null || height.isEmpty()){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "ROI provided was invalid"); + return; + } + + BulkAnnotation annotation; + try{ + int nX = Integer.parseInt(x); + int nY = Integer.parseInt(y); + int nWidth = Integer.parseInt(width); + int nHeight = Integer.parseInt(height); + annotation = new BulkAnnotation(); + Point2D tl = new Point2D(nX, nY); + Point2D tr = new Point2D(nX + nWidth, nY); + Point2D bl = new Point2D(nX, nY + nHeight); + Point2D br = new Point2D(nX + nWidth, nY + nHeight); + List points = new ArrayList<>(); + points.add(tl); points.add(tr); points.add(bl); points.add(br); + annotation.setPoints(points); + annotation.setAnnotationType(BulkAnnotation.AnnotationType.RECTANGLE); + } catch (NumberFormatException e){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "ROI provided was invalid"); + return; + } + + BufferedImage bi = roiExtractor.extractROI(sopInstanceUID, annotation); + + // mount the resulting memory stream + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + ImageIO.write(bi, "jpg", bos); + + Task task = PluginController.getInstance().makePredictionOverImage(bos, provider); + task.onCompletion(() -> { + try { + MLPrediction prediction = task.get(); + response.setContentType("text/html"); + PrintWriter out = response.getWriter(); + out.append("This is a test"); + out.close(); + } catch (InterruptedException | ExecutionException e) { + log.error("Could not make prediction", e); + try { + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Could not make prediction"); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java index be42a8bd3..43af7391b 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java @@ -1,18 +1,16 @@ -package pt.ua.dicoogle.sdk.imageworker; +package pt.ua.dicoogle.sdk.datastructs.dim; -import java.io.File; import java.net.URI; import java.util.Objects; /** * This object defines an Image ROI. - * It has a physical location defined by an URI. - * It is possible to write the image contents to a file defined by the URI. * The ROI has an x and y position that identify its origin in the source image. * The SOPInstanceUID defines where this ROI was extracted from. */ public class ImageROI { + /** Not in use **/ public enum FileType { JPEG (".jpg", "image/jpeg"), PNG (".png", "image/png"); @@ -44,30 +42,11 @@ private FileType(String s, String mimeType) { private FileType fileType; - private ImageROI(URI uriROI){ - File f = new File(uriROI); - if(!f.exists()) - throw new IllegalArgumentException(String.format("URI %s does not exist", uriROI.getPath())); - this.x = 0; - this.y = 0; - } - - public ImageROI(String sopInstanceUID, int width, int height, URI uriROI, FileType fileType) { - this(uriROI); - this.width = width; - this.height = height; - this.uriROI = uriROI; - this.fileType = fileType; - } - - public ImageROI(String sopInstanceUID, int x, int y, int width, int height, URI uriROI, FileType fileType) { - this(uriROI); + public ImageROI(String sopInstanceUID, int x, int y, int width, int height) { this.x = x; this.y = y; this.width = width; this.height = height; - this.uriROI = uriROI; - this.fileType = fileType; } public double getX() { diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java index 66c76963e..f9a80a971 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java @@ -1,7 +1,11 @@ package pt.ua.dicoogle.sdk.mlprovider; import pt.ua.dicoogle.sdk.DicooglePlugin; +import pt.ua.dicoogle.sdk.datastructs.dim.ImageROI; +import pt.ua.dicoogle.sdk.task.Task; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.util.List; import java.util.Set; @@ -10,7 +14,7 @@ * A machine learning provider can be a remote service, hosted on the cloud, or a simple remote/local server * that has installed machine learning algorithms for problem solving. * Machine learning providers can work with either image or csv datasets. - * The purpose of these providers is to provide a way to develop plugins to integrate with services such as Google's Vertex API or Amazon's SageMaker API. + * The purpose of this interface is to provide a way to develop plugins to integrate with services such as Google's Vertex API or Amazon's SageMaker API. * * @author Rui Jesus */ @@ -30,17 +34,17 @@ public abstract class MLProviderInterface implements DicooglePlugin { public abstract MLModel createModel(); /** - * This method creates a model using a specific dataset + * This method creates a endpoint that exposes a service */ public abstract void createEndpoint(); /** - * This method creates a model using a specific dataset + * This method lists all available endpoints */ public abstract List listEndpoints(); /** - * This method creates a model using a specific dataset + * This method deletes a endpoint */ public abstract void deleteEndpoint(); @@ -60,9 +64,10 @@ public abstract class MLProviderInterface implements DicooglePlugin { public abstract void deleteModel(); /** - * This method makes a prediction about an item using the selected model + * Order a image prediction + * @param bos the image to classify */ - public abstract MLPrediction makePrediction(); + public abstract Task makePredictionOverImage(ByteArrayOutputStream bos); /** * This method makes a bulk prediction using the selected model From 5afed6a36d70f91e73ff6c4646292a056001b334 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Fri, 2 Dec 2022 03:51:26 +0000 Subject: [PATCH 19/53] Finished baseline makePrediction endpoint. --- .../ua/dicoogle/plugins/PluginController.java | 1 - .../ua/dicoogle/server/web/DicoogleWeb.java | 5 ++- .../mlprovider/MakePredictionServlet.java | 13 ++++++-- .../dicoogle/sdk/mlprovider/MLPrediction.java | 33 ++++++++++++++++++- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java index 9593465d8..5a9a073c8 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java @@ -552,7 +552,6 @@ public MLProviderInterface getMachineLearningProviderByName(String name, boolean Collection plugins = getMLPlugins(onlyEnabled); for (MLProviderInterface p : plugins) { if (p.getName().equalsIgnoreCase(name)) { - // logger.info("Retrived Query Provider: "+name); return p; } } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index 919ec8e97..0be10a6f5 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java @@ -139,6 +139,9 @@ public DicoogleWeb(int port) throws Exception { // setup the ROI extractor final ServletContextHandler roiExtractor = createServletHandler(new ROIServlet(), "/roi"); + // setup the ml endpoints + final ServletContextHandler makePrediction = createServletHandler(new MakePredictionServlet(), "/makePrediction"); + // setup the DICOM to PNG image servlet final ServletContextHandler dictags = createServletHandler(new TagsServlet(), "/dictags"); @@ -177,7 +180,7 @@ public DicoogleWeb(int port) throws Exception { PluginRestletApplication.attachRestPlugin(new VersionResource()); // list the all the handlers mounted above - Handler[] handlers = new Handler[] {pluginHandler, legacyHandler, dic2png, roiExtractor, dictags, + Handler[] handlers = new Handler[] {pluginHandler, legacyHandler, dic2png, roiExtractor, makePrediction, dictags, createServletHandler(new IndexerServlet(), "/indexer"), // DEPRECATED createServletHandler(new SettingsServlet(), "/settings"), csvServletHolder, createServletHandler(new LoginServlet(), "/login"), diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index b65bb14fe..fc6f23d23 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -1,5 +1,6 @@ package pt.ua.dicoogle.server.web.servlets.mlprovider; +import com.fasterxml.jackson.databind.ObjectMapper; import org.restlet.data.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -90,13 +91,20 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t ImageIO.write(bi, "jpg", bos); Task task = PluginController.getInstance().makePredictionOverImage(bos, provider); + if(task == null){ + response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Could not create prediction task"); + return; + } + task.onCompletion(() -> { try { MLPrediction prediction = task.get(); - response.setContentType("text/html"); + ObjectMapper mapper = new ObjectMapper(); + response.setContentType("application/json"); PrintWriter out = response.getWriter(); - out.append("This is a test"); + mapper.writeValue(out, prediction); out.close(); + out.flush(); } catch (InterruptedException | ExecutionException e) { log.error("Could not make prediction", e); try { @@ -109,5 +117,6 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } }); + task.run(); } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java index 0ad8adb50..2870bbcd1 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java @@ -1,3 +1,34 @@ package pt.ua.dicoogle.sdk.mlprovider; -public class MLPrediction {} +import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; + +import java.util.HashMap; +import java.util.List; + +/** + * This object maps predictions done by the AI algorithms. + * It can contain a set of metrics and a list of annotations. + */ +public class MLPrediction { + + private HashMap metrics; + private String version; + + private List annotations; + + public HashMap getMetrics() { + return metrics; + } + + public void setMetrics(HashMap metrics) { + this.metrics = metrics; + } + + public List getAnnotations() { + return annotations; + } + + public void setAnnotations(List annotations) { + this.annotations = annotations; + } +} From eaf0de6359d07bdedd7093e5b71cb861b0a7e5de Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Fri, 2 Dec 2022 11:35:56 +0000 Subject: [PATCH 20/53] Adjustments to makePrediction endpoint. --- .../server/web/dicom/ROIExtractor.java | 47 ++++++----- .../server/web/servlets/ROIServlet.java | 12 ++- .../mlprovider/MakePredictionServlet.java | 83 +++++++++++++++---- .../server/web/utils/cache/WSICache.java | 3 +- .../sdk/datastructs/dim/BulkAnnotation.java | 15 ++++ .../dicoogle/sdk/datastructs/dim/Point2D.java | 18 ++-- .../dicoogle/sdk/mlprovider/MLPrediction.java | 8 ++ 7 files changed, 140 insertions(+), 46 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java index f59b36a9a..47cc3bbb1 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -30,20 +30,23 @@ public ROIExtractor() { } public BufferedImage extractROI(String sopInstanceUID, BulkAnnotation bulkAnnotation) { + DicomMetaData metaData; + try { + metaData = getDicomMetadata(sopInstanceUID); + } catch (IOException e) { + logger.error("Could not extract metadata", e); + return null; + } + return extractROI(metaData, bulkAnnotation); + } + + public BufferedImage extractROI(DicomMetaData dicomMetaData, BulkAnnotation bulkAnnotation) { ImageReader imageReader = getImageReader(); if(imageReader == null) return null; - DicomMetaData dicomMetaData; - try { - dicomMetaData = getDicomMetadata(sopInstanceUID); - } catch (IOException e) { - logger.error("Error reading DICOM file", e); - return null; - } - if(dicomMetaData == null){ return null; } @@ -99,16 +102,16 @@ private List> getFrameMatrixFromAnnotation(WSISopDescriptor descr switch (annotation.getAnnotationType()) { case RECTANGLE: // Calculate the starting position of the annotation in frame coordinates - int x_c = annotation.getPoints().get(0).getX() / descriptor.getTileWidth(); - int y_c = annotation.getPoints().get(0).getY() / descriptor.getTileHeight(); + int x_c = (int) (annotation.getPoints().get(0).getX() / descriptor.getTileWidth()); + int y_c = (int) (annotation.getPoints().get(0).getY() / descriptor.getTileHeight()); //Annotation is completely out of bounds, no intersection possible if(x_c > nx_tiles || y_c > ny_tiles) return matrix; // Calculate the ending position of the annotation in frame coordinates - int x_e = annotation.getPoints().get(3).getX() / descriptor.getTileWidth(); - int y_e = annotation.getPoints().get(3).getY() / descriptor.getTileHeight(); + int x_e = (int) (annotation.getPoints().get(3).getX() / descriptor.getTileWidth()); + int y_e = (int) (annotation.getPoints().get(3).getY() / descriptor.getTileHeight()); //Annotation might be out of bonds, adjust that if(x_e > (nx_tiles - 1)) @@ -148,8 +151,8 @@ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDesc Point2D annotationPoint1 = annotation.getPoints().get(0); Point2D annotationPoint2 = annotation.getPoints().get(3); - int clipX = Math.max(annotationPoint2.getX() - descriptor.getTotalPixelMatrixColumns(), 0); - int clipY = Math.max(annotationPoint2.getY() - descriptor.getTotalPixelMatrixRows(), 0); + int clipX = (int) Math.max(annotationPoint2.getX() - descriptor.getTotalPixelMatrixColumns(), 0); + int clipY = (int) Math.max(annotationPoint2.getY() - descriptor.getTotalPixelMatrixRows(), 0); int annotationWidth = (int) annotation.getPoints().get(0).distance(annotation.getPoints().get(1)) - clipX; int annotationHeight = (int) annotation.getPoints().get(0).distance(annotation.getPoints().get(2)) - clipY; @@ -174,17 +177,17 @@ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDesc Point2D intersectionPoint1 = new Point2D(Math.max(annotationPoint1.getX(), framePoint1.getX()), Math.max(annotationPoint1.getY(), framePoint1.getY())); Point2D intersectionPoint2 = new Point2D(Math.min(annotationPoint2.getX(), framePoint2.getX()), Math.min(annotationPoint2.getY(), framePoint2.getY())); - int startX = intersectionPoint1.getX() - annotationPoint1.getX(); - int startY = intersectionPoint1.getY() - annotationPoint1.getY(); + int startX = (int) (intersectionPoint1.getX() - annotationPoint1.getX()); + int startY = (int) (intersectionPoint1.getY() - annotationPoint1.getY()); - int endX = intersectionPoint2.getX() - annotationPoint1.getX(); - int endY = intersectionPoint2.getY() - annotationPoint1.getY(); + int endX = (int) (intersectionPoint2.getX() - annotationPoint1.getX()); + int endY = (int) (intersectionPoint2.getY() - annotationPoint1.getY()); - int frameStartX = intersectionPoint1.getX() - framePoint1.getX(); - int frameStartY = intersectionPoint1.getY() - framePoint1.getY(); + int frameStartX = (int) (intersectionPoint1.getX() - framePoint1.getX()); + int frameStartY = (int) (intersectionPoint1.getY() - framePoint1.getY()); - int frameEndX = intersectionPoint2.getX() - framePoint1.getX(); - int frameEndY = intersectionPoint2.getY() - framePoint1.getY(); + int frameEndX = (int) (intersectionPoint2.getX() - framePoint1.getX()); + int frameEndY = (int) (intersectionPoint2.getY() - framePoint1.getY()); int deltaX = frameEndX - frameStartX; int deltaY = frameEndY - frameStartY; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java index 638882bdd..8710e3d9f 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java @@ -1,11 +1,13 @@ package pt.ua.dicoogle.server.web.servlets; +import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; import org.restlet.data.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; import pt.ua.dicoogle.server.web.dicom.ROIExtractor; +import pt.ua.dicoogle.server.web.utils.cache.WSICache; import javax.imageio.ImageIO; import javax.servlet.ServletException; @@ -23,6 +25,7 @@ public class ROIServlet extends HttpServlet { private static final long serialVersionUID = 1L; private final ROIExtractor roiExtractor; + private final WSICache wsiCache; /** * Creates ROI servlet servlet. @@ -30,6 +33,7 @@ public class ROIServlet extends HttpServlet { */ public ROIServlet() { this.roiExtractor = new ROIExtractor(); + this.wsiCache = WSICache.getInstance(); } @Override @@ -72,7 +76,9 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) return; } - BufferedImage bi = roiExtractor.extractROI(sopInstanceUID, annotation); + DicomMetaData metaData = getDicomMetadata(sopInstanceUID); + + BufferedImage bi = roiExtractor.extractROI(metaData, annotation); if(bi != null){ response.setContentType("image/jpeg"); @@ -85,4 +91,8 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) response.sendError(Status.CLIENT_ERROR_NOT_FOUND.getCode(), "Could not build ROI with the provided UID"); } + private DicomMetaData getDicomMetadata(String sop) throws IOException{ + return wsiCache.get(sop); + } + } \ No newline at end of file diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index fc6f23d23..fd1efeabf 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -1,15 +1,18 @@ package pt.ua.dicoogle.server.web.servlets.mlprovider; import com.fasterxml.jackson.databind.ObjectMapper; +import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; import org.restlet.data.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pt.ua.dicoogle.plugins.PluginController; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; +import pt.ua.dicoogle.sdk.datastructs.wsi.WSISopDescriptor; import pt.ua.dicoogle.sdk.mlprovider.MLPrediction; import pt.ua.dicoogle.sdk.task.Task; import pt.ua.dicoogle.server.web.dicom.ROIExtractor; +import pt.ua.dicoogle.server.web.utils.cache.WSICache; import javax.imageio.ImageIO; import javax.servlet.ServletException; @@ -30,11 +33,14 @@ public class MakePredictionServlet extends HttpServlet { private final ROIExtractor roiExtractor; + private final WSICache wsiCache; + /** * Creates ROI servlet servlet. * */ public MakePredictionServlet() { + this.wsiCache = WSICache.getInstance(); this.roiExtractor = new ROIExtractor(); } @@ -47,6 +53,8 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t String width = request.getParameter("width"); String height = request.getParameter("height"); String provider = request.getParameter("provider"); + String wsi = request.getParameter("wsi"); + final String baseSopInstanceUID = request.getParameter("baseUID"); if(sopInstanceUID == null || sopInstanceUID.isEmpty()){ response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "SOPInstanceUID provided was invalid"); @@ -63,27 +71,38 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t return; } - BulkAnnotation annotation; + if(wsi != null && wsi.equals("true")){ + if(baseSopInstanceUID == null || baseSopInstanceUID.isEmpty()){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "WSI was selected but a valid base SopInstanceUID was not provided"); + return; + } + } + + int nX, nY, nWidth, nHeight; + try{ - int nX = Integer.parseInt(x); - int nY = Integer.parseInt(y); - int nWidth = Integer.parseInt(width); - int nHeight = Integer.parseInt(height); - annotation = new BulkAnnotation(); - Point2D tl = new Point2D(nX, nY); - Point2D tr = new Point2D(nX + nWidth, nY); - Point2D bl = new Point2D(nX, nY + nHeight); - Point2D br = new Point2D(nX + nWidth, nY + nHeight); - List points = new ArrayList<>(); - points.add(tl); points.add(tr); points.add(bl); points.add(br); - annotation.setPoints(points); - annotation.setAnnotationType(BulkAnnotation.AnnotationType.RECTANGLE); + nX = Integer.parseInt(x); + nY = Integer.parseInt(y); + nWidth = Integer.parseInt(width); + nHeight = Integer.parseInt(height); } catch (NumberFormatException e){ response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "ROI provided was invalid"); return; } - BufferedImage bi = roiExtractor.extractROI(sopInstanceUID, annotation); + BulkAnnotation annotation = new BulkAnnotation(); + Point2D tl = new Point2D(nX, nY); + Point2D tr = new Point2D(nX + nWidth, nY); + Point2D bl = new Point2D(nX, nY + nHeight); + Point2D br = new Point2D(nX + nWidth, nY + nHeight); + List points = new ArrayList<>(); + points.add(tl); points.add(tr); points.add(bl); points.add(br); + annotation.setPoints(points); + annotation.setAnnotationType(BulkAnnotation.AnnotationType.RECTANGLE); + + DicomMetaData dicomMetaData = this.getDicomMetadata(sopInstanceUID); + + BufferedImage bi = roiExtractor.extractROI(dicomMetaData, annotation); // mount the resulting memory stream ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -99,6 +118,20 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t task.onCompletion(() -> { try { MLPrediction prediction = task.get(); + + // Coordinates need to be converted if we're working with WSI + if(wsi.equals("true")){ + WSISopDescriptor descriptor = new WSISopDescriptor(); + descriptor.extractData(dicomMetaData.getAttributes()); + DicomMetaData base = this.getDicomMetadata(baseSopInstanceUID); + WSISopDescriptor baseDescriptor = new WSISopDescriptor(); + baseDescriptor.extractData(base.getAttributes()); + double scale = (descriptor.getTotalPixelMatrixRows() * 1.0) / baseDescriptor.getTotalPixelMatrixRows(); + if(scale != 1){ // scale will be 1 if the levels match, no conversion needed + convertCoordinates(prediction, nX, nY, scale); + } + } + ObjectMapper mapper = new ObjectMapper(); response.setContentType("application/json"); PrintWriter out = response.getWriter(); @@ -119,4 +152,24 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t task.run(); } + + private DicomMetaData getDicomMetadata(String sop) throws IOException{ + return wsiCache.get(sop); + } + + /** + * When working with WSI, it is convenient to have coordinates relative to the base of the pyramid. + * This method takes care of that. + * @param prediction + * @param scale to transform coordinates + * @return the ml prediction with the converted coordinates. + */ + private void convertCoordinates(MLPrediction prediction, int x, int y, double scale){ + for(BulkAnnotation ann : prediction.getAnnotations()){ + for(Point2D p : ann.getPoints()){ + p.setX((p.getX() + x)/scale); + p.setY((p.getY() + y)/scale); + } + } + } } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java index 8b4a7dea6..583b92610 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java @@ -24,7 +24,8 @@ import java.util.zip.GZIPInputStream; /** - * Cache used to store DicomMetadata objects temporarily, as they are quite heavy to build on-demand + * Cache used to store DicomMetadata objects temporarily, as they are quite heavy to build on-demand. + * Used only for WSI instances. * @author Rui Jesus */ public class WSICache extends MemoryCache{ diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java index af7b15934..8b2779bab 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java @@ -13,8 +13,15 @@ public enum AnnotationType { RECTANGLE, ELLIPSE, POLYGON, POLYLINE, POINT } + public enum CoordinateType { + TWO_DIMENSIONAL, //for image relative coordinates + THREE_DIMENSIONAL //for coordinates in a Cartesian system defined by a frame of reference + } + private PixelOrigin pixelOrigin; + private CoordinateType coordinateType; + private AnnotationType annotationType; private String label; @@ -31,6 +38,14 @@ public void setPixelOrigin(PixelOrigin pixelOrigin) { this.pixelOrigin = pixelOrigin; } + public CoordinateType getCoordinateType() { + return coordinateType; + } + + public void setCoordinateType(CoordinateType coordinateType) { + this.coordinateType = coordinateType; + } + public AnnotationType getAnnotationType() { return annotationType; } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java index e5d2294a7..bd25437e0 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java @@ -5,9 +5,8 @@ */ public class Point2D { - private int x; - - private int y; + private double x; + private double y; public Point2D() { x = 0; @@ -19,19 +18,24 @@ public Point2D(int x, int y) { this.y = y; } - public int getX() { + public Point2D(double x, double y) { + this.x = x; + this.y = y; + } + + public double getX() { return x; } - public void setX(int x) { + public void setX(double x) { this.x = x; } - public int getY() { + public double getY() { return y; } - public void setY(int y) { + public void setY(double y) { this.y = y; } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java index 2870bbcd1..e770305e8 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java @@ -31,4 +31,12 @@ public List getAnnotations() { public void setAnnotations(List annotations) { this.annotations = annotations; } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } } From bf4b4958c51d8dae46093dcc9370c6a7b2e777b9 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Fri, 2 Dec 2022 17:11:22 +0000 Subject: [PATCH 21/53] Fixed a bug in roi extractor. --- .../ua/dicoogle/server/web/dicom/ROIExtractor.java | 2 +- .../servlets/mlprovider/MakePredictionServlet.java | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java index 47cc3bbb1..b1f665132 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -124,7 +124,7 @@ private List> getFrameMatrixFromAnnotation(WSISopDescriptor descr matrix.add(new ArrayList<>()); for (int j = x_c; j <= x_e; j++) { WSIFrame frame = new WSIFrame(descriptor.getTileWidth(), descriptor.getTileHeight(), j, i, i * nx_tiles + j); - matrix.get(i).add(frame); + matrix.get(i-y_c).add(frame); } } break; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index fd1efeabf..f9c26a008 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -78,13 +78,13 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } } - int nX, nY, nWidth, nHeight; + double nX, nY, nWidth, nHeight; try{ - nX = Integer.parseInt(x); - nY = Integer.parseInt(y); - nWidth = Integer.parseInt(width); - nHeight = Integer.parseInt(height); + nX = Double.parseDouble(x); + nY = Double.parseDouble(y); + nWidth = Double.parseDouble(width); + nHeight = Double.parseDouble(height); } catch (NumberFormatException e){ response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "ROI provided was invalid"); return; @@ -164,7 +164,7 @@ private DicomMetaData getDicomMetadata(String sop) throws IOException{ * @param scale to transform coordinates * @return the ml prediction with the converted coordinates. */ - private void convertCoordinates(MLPrediction prediction, int x, int y, double scale){ + private void convertCoordinates(MLPrediction prediction, double x, double y, double scale){ for(BulkAnnotation ann : prediction.getAnnotations()){ for(Point2D p : ann.getPoints()){ p.setX((p.getX() + x)/scale); From 62f523ae3a4bb436d93a26e61f2f50ce3e4660f3 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Sun, 11 Dec 2022 21:14:07 +0000 Subject: [PATCH 22/53] Added missing dependency. Removed duplicated dependencies. --- dicoogle/pom.xml | 11 +++++------ sdk/pom.xml | 6 ------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/dicoogle/pom.xml b/dicoogle/pom.xml index e7fdee162..77c1f9624 100644 --- a/dicoogle/pom.xml +++ b/dicoogle/pom.xml @@ -376,12 +376,6 @@ 2.9.5 - - dcm4che-core - org.dcm4che - 3.3.7 - - org.jdom jdom2 @@ -465,6 +459,11 @@ raven-log4j2 5.0.2 + + dcm4che + dcm4che-imageio + ${dcm4che.version} + org.dcm4che dcm4che-imageio diff --git a/sdk/pom.xml b/sdk/pom.xml index d89b23cf9..b234fb6f2 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -242,12 +242,6 @@ slf4j-api ${slf4j.version} - - org.dcm4che - dcm4che-core - 3.3.7 - compile - From d707dc1d3b42780065a5133b1f61b6ce4c77752e Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Tue, 13 Dec 2022 22:00:24 +0000 Subject: [PATCH 23/53] Fixed potential issue with dicoogle not initializing when a dicom provider was not set. --- .../java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java index 583b92610..c5441b04c 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java @@ -45,7 +45,10 @@ private WSICache(){ .build(new WsiDcmLoader()); List dicomProviders = ServerSettingsManager.getSettings().getArchiveSettings().getDIMProviders(); - queryProvider = dicomProviders.iterator().next(); + if(!dicomProviders.isEmpty()) + queryProvider = dicomProviders.iterator().next(); + else + queryProvider = ""; } public static synchronized WSICache getInstance(){ From bba57d26d13e28008dcb2e0b5d275b6e0c260ebd Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Wed, 14 Dec 2022 00:01:09 +0000 Subject: [PATCH 24/53] Fixed bug in annotation coordinate rescaling --- .../server/web/servlets/mlprovider/MakePredictionServlet.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index f9c26a008..f63f9e391 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -127,9 +127,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t WSISopDescriptor baseDescriptor = new WSISopDescriptor(); baseDescriptor.extractData(base.getAttributes()); double scale = (descriptor.getTotalPixelMatrixRows() * 1.0) / baseDescriptor.getTotalPixelMatrixRows(); - if(scale != 1){ // scale will be 1 if the levels match, no conversion needed - convertCoordinates(prediction, nX, nY, scale); - } + convertCoordinates(prediction, nX, nY, scale); } ObjectMapper mapper = new ObjectMapper(); From 292e6795bd9e7854cd4d7f1e17e048dfec440dde Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Thu, 29 Dec 2022 18:35:33 +0000 Subject: [PATCH 25/53] ROI extractor now supports other shapes such as polygons and ellipses --- .../server/web/dicom/ROIExtractor.java | 250 ++++++++++++------ .../server/web/servlets/ROIServlet.java | 46 ++++ .../mlprovider/MakePredictionServlet.java | 67 ++--- 3 files changed, 238 insertions(+), 125 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java index b1f665132..d76d50b76 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -14,10 +14,10 @@ import javax.imageio.ImageIO; import javax.imageio.ImageReader; import java.awt.*; +import java.awt.geom.Ellipse2D; import java.awt.image.BufferedImage; import java.io.*; -import java.util.ArrayList; -import java.util.Iterator; +import java.util.*; import java.util.List; public class ROIExtractor { @@ -25,6 +25,9 @@ public class ROIExtractor { private static final Logger logger = LoggerFactory.getLogger(ROIExtractor.class); private final WSICache wsiCache; + private static final HashSet notSupportedTypes = + new HashSet<>(Collections.singletonList(BulkAnnotation.AnnotationType.POINT)); + public ROIExtractor() { this.wsiCache = WSICache.getInstance(); } @@ -82,85 +85,43 @@ private static ImageReader getImageReader() { return null; } - /** - * Given an annotation and the details of the image, this method returns the frames that intersect with the annotation. - * This method is meant for WSI images that are split into frames. - * @param descriptor the WSI descriptor - * @param annotation The annotation to intersect - * @return a 2D matrix of frames that intersect this annotation. - */ - private List> getFrameMatrixFromAnnotation(WSISopDescriptor descriptor, BulkAnnotation annotation) { - - //Number of tiles along the x direction, number of columns in the frame matrix - int nx_tiles = (int) Math.ceil((descriptor.getTotalPixelMatrixColumns() * 1.0) / descriptor.getTileWidth()); - - //Number of tiles along the y direction, number of rows in the frame matrix - int ny_tiles = (int) Math.ceil((descriptor.getTotalPixelMatrixRows() * 1.0) / descriptor.getTileHeight()); - - List> matrix = new ArrayList<>(); - - switch (annotation.getAnnotationType()) { - case RECTANGLE: - // Calculate the starting position of the annotation in frame coordinates - int x_c = (int) (annotation.getPoints().get(0).getX() / descriptor.getTileWidth()); - int y_c = (int) (annotation.getPoints().get(0).getY() / descriptor.getTileHeight()); - - //Annotation is completely out of bounds, no intersection possible - if(x_c > nx_tiles || y_c > ny_tiles) - return matrix; - - // Calculate the ending position of the annotation in frame coordinates - int x_e = (int) (annotation.getPoints().get(3).getX() / descriptor.getTileWidth()); - int y_e = (int) (annotation.getPoints().get(3).getY() / descriptor.getTileHeight()); - - //Annotation might be out of bonds, adjust that - if(x_e > (nx_tiles - 1)) - x_e = nx_tiles - 1; - - if(y_e > (ny_tiles - 1)) - y_e = ny_tiles - 1; - - for (int i = y_c; i <= y_e ; i++) { - matrix.add(new ArrayList<>()); - for (int j = x_c; j <= x_e; j++) { - WSIFrame frame = new WSIFrame(descriptor.getTileWidth(), descriptor.getTileHeight(), j, i, i * nx_tiles + j); - matrix.get(i-y_c).add(frame); - } - } - break; - } - - return matrix; - } - /** * Given an annotation and an image, return the section of the image the annotation intersects. * It only works with rectangle type annotations. * @param annotation the annotation to intersect - * @param descriptor descriptor of the WSI pyramid, contains information about the dimmensions of the image. + * @param descriptor descriptor of the WSI pyramid, contains information about the dimensions of the image. * @param imageReader * @param param * @return the intersection of the annotation on the image. * @throws IllegalArgumentException when the annotation is not one of the supported types. */ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDescriptor descriptor, ImageReader imageReader, DicomImageReadParam param) throws IllegalArgumentException { - if(annotation.getAnnotationType() != BulkAnnotation.AnnotationType.RECTANGLE){ - throw new IllegalArgumentException("Trying to build a ROI without a rectangle annotation"); + if(notSupportedTypes.contains(annotation.getAnnotationType())){ + throw new IllegalArgumentException("Trying to build a ROI with an unsupported annotation type"); } - Point2D annotationPoint1 = annotation.getPoints().get(0); - Point2D annotationPoint2 = annotation.getPoints().get(3); + List constructionPoints = getOuterRectangle(annotation); //Points that will be used to construct the ROI. + + Point2D annotationPoint1 = constructionPoints.get(0); + Point2D annotationPoint2 = constructionPoints.get(3); int clipX = (int) Math.max(annotationPoint2.getX() - descriptor.getTotalPixelMatrixColumns(), 0); int clipY = (int) Math.max(annotationPoint2.getY() - descriptor.getTotalPixelMatrixRows(), 0); - int annotationWidth = (int) annotation.getPoints().get(0).distance(annotation.getPoints().get(1)) - clipX; - int annotationHeight = (int) annotation.getPoints().get(0).distance(annotation.getPoints().get(2)) - clipY; + int annotationWidth = (int) constructionPoints.get(0).distance(constructionPoints.get(1)) - clipX; + int annotationHeight = (int) constructionPoints.get(0).distance(constructionPoints.get(2)) - clipY; - List> frameMatrix = getFrameMatrixFromAnnotation(descriptor, annotation); + List> frameMatrix = getFrameMatrixFromAnnotation(descriptor, constructionPoints); BufferedImage combined = new BufferedImage(annotationWidth, annotationHeight, BufferedImage.TYPE_INT_RGB); Graphics g = combined.getGraphics(); + // We need to perform clipping + Shape clippingShape = null; + if(annotation.getAnnotationType() != BulkAnnotation.AnnotationType.RECTANGLE){ + clippingShape = getClippingShape(annotation, constructionPoints.get(0)); + if (clippingShape != null) g.setClip(clippingShape); + } + for (List matrix : frameMatrix) { for (WSIFrame frame : matrix) { BufferedImage bb; @@ -180,33 +141,36 @@ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDesc int startX = (int) (intersectionPoint1.getX() - annotationPoint1.getX()); int startY = (int) (intersectionPoint1.getY() - annotationPoint1.getY()); - int endX = (int) (intersectionPoint2.getX() - annotationPoint1.getX()); - int endY = (int) (intersectionPoint2.getY() - annotationPoint1.getY()); + if(clippingShape == null){ + int endX = (int) (intersectionPoint2.getX() - annotationPoint1.getX()); + int endY = (int) (intersectionPoint2.getY() - annotationPoint1.getY()); - int frameStartX = (int) (intersectionPoint1.getX() - framePoint1.getX()); - int frameStartY = (int) (intersectionPoint1.getY() - framePoint1.getY()); + int frameStartX = (int) (intersectionPoint1.getX() - framePoint1.getX()); + int frameStartY = (int) (intersectionPoint1.getY() - framePoint1.getY()); - int frameEndX = (int) (intersectionPoint2.getX() - framePoint1.getX()); - int frameEndY = (int) (intersectionPoint2.getY() - framePoint1.getY()); + int frameEndX = (int) (intersectionPoint2.getX() - framePoint1.getX()); + int frameEndY = (int) (intersectionPoint2.getY() - framePoint1.getY()); - int deltaX = frameEndX - frameStartX; - int deltaY = frameEndY - frameStartY; + int deltaX = frameEndX - frameStartX; + int deltaY = frameEndY - frameStartY; - //This means that the frame is smaller than the intersection area - //It can happen when we are on the edge of the image and the tiles do not have the dimensions stated in the DICOM file - if (deltaX > bb.getWidth()) { - endX = frameEndX - bb.getWidth(); - frameEndX = bb.getWidth(); - } + //This means that the frame is smaller than the intersection area + //It can happen when we are on the edge of the image and the tiles do not have the dimensions stated in the DICOM file + if (deltaX > bb.getWidth()) { + endX = frameEndX - bb.getWidth(); + frameEndX = bb.getWidth(); + } - if (deltaY > bb.getHeight()) { - endY = frameEndY - bb.getHeight(); - frameEndY = bb.getHeight(); + if (deltaY > bb.getHeight()) { + endY = frameEndY - bb.getHeight(); + frameEndY = bb.getHeight(); + } + g.drawImage(bb, startX, startY, + endX, endY, frameStartX, frameStartY, frameEndX, frameEndY, null); + } else { + g.drawImage(bb, startX, startY,null); } - g.drawImage(bb, startX, startY, - endX, endY, frameStartX, frameStartY, frameEndX, frameEndY, null); - } } @@ -214,6 +178,130 @@ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDesc return combined; } + /** + * From a bulk annotation, find its outer rectangle. Only applicable to shape annotations. + * @param annotation + * @return a list of 4 points, representing a rectangle that contains the provided annotation. + */ + private List getOuterRectangle(BulkAnnotation annotation){ + + double minX = Double.MAX_VALUE; + double minY = Double.MAX_VALUE; + double maxX = Double.MIN_VALUE; + double maxY = Double.MIN_VALUE; + + switch (annotation.getAnnotationType()){ + case RECTANGLE: + return annotation.getPoints(); + case POLYGON: + case POLYLINE: + for(Point2D p : annotation.getPoints()){ + if(p.getX() > maxX) + maxX = p.getX(); + if(p.getX() < minX) + minX = p.getX(); + + if(p.getY() > maxY) + maxY = p.getY(); + if(p.getY() < minY) + minY = p.getY(); + } + break; + case ELLIPSE: + minX = annotation.getPoints().get(0).getX(); + maxX = annotation.getPoints().get(1).getX(); + minY = annotation.getPoints().get(2).getY(); + maxY = annotation.getPoints().get(3).getY(); + break; + } + + Point2D tl = new Point2D(minX, minY); + Point2D tr = new Point2D(maxX, minY); + Point2D bl = new Point2D(minX, maxY); + Point2D br = new Point2D(maxX, maxY); + + return Arrays.asList(tl, tr, bl, br); + } + + /** + * Given an annotation, get it as a Shape to apply as a clipping shape for the ROIs. + * The points of this shape are normalized according to the starting point. + * This is only needed when dealing with non-rectangle annotations. + * @param annotation + * @param startingPoint starting point of the rectangle that contains the annotation + * @return a shape to use to clip the ROI + */ + private Shape getClippingShape(BulkAnnotation annotation, Point2D startingPoint){ + switch (annotation.getAnnotationType()){ + case POLYLINE: + case POLYGON: + Polygon polygon = new Polygon(); + for(Point2D p : annotation.getPoints()){ + polygon.addPoint((int) (p.getX() - startingPoint.getX()), (int) (p.getY() - startingPoint.getY())); + } + return polygon; + case ELLIPSE: + double minX = annotation.getPoints().get(0).getX(); + double maxX = annotation.getPoints().get(1).getX(); + + double minY = annotation.getPoints().get(2).getY(); + double maxY = annotation.getPoints().get(3).getY(); + + return new Ellipse2D.Double(minX, minY, Math.abs(maxX - minX), Math.abs(maxY - minY)); + + default: + return null; + } + } + + /** + * Given an annotation and the details of the image, this method returns the frames that intersect with the annotation. + * This method is meant for WSI images that are split into frames. + * It only accepts rectangle annotations + * @param descriptor the WSI descriptor + * @param points A list of points describing a rectangle + * @return a 2D matrix of frames that intersect this annotation. + */ + private List> getFrameMatrixFromAnnotation(WSISopDescriptor descriptor, List points) { + + //Number of tiles along the x direction, number of columns in the frame matrix + int nx_tiles = (int) Math.ceil((descriptor.getTotalPixelMatrixColumns() * 1.0) / descriptor.getTileWidth()); + + //Number of tiles along the y direction, number of rows in the frame matrix + int ny_tiles = (int) Math.ceil((descriptor.getTotalPixelMatrixRows() * 1.0) / descriptor.getTileHeight()); + + List> matrix = new ArrayList<>(); + + // Calculate the starting position of the annotation in frame coordinates + int x_c = (int) (points.get(0).getX() / descriptor.getTileWidth()); + int y_c = (int) (points.get(0).getY() / descriptor.getTileHeight()); + + //Annotation is completely out of bounds, no intersection possible + if(x_c > nx_tiles || y_c > ny_tiles) + return matrix; + + // Calculate the ending position of the annotation in frame coordinates + int x_e = (int) (points.get(3).getX() / descriptor.getTileWidth()); + int y_e = (int) (points.get(3).getY() / descriptor.getTileHeight()); + + //Annotation might be out of bonds, adjust that + if(x_e > (nx_tiles - 1)) + x_e = nx_tiles - 1; + + if(y_e > (ny_tiles - 1)) + y_e = ny_tiles - 1; + + for (int i = y_c; i <= y_e ; i++) { + matrix.add(new ArrayList<>()); + for (int j = x_c; j <= x_e; j++) { + WSIFrame frame = new WSIFrame(descriptor.getTileWidth(), descriptor.getTileHeight(), j, i, i * nx_tiles + j); + matrix.get(i-y_c).add(frame); + } + } + + return matrix; + } + private DicomMetaData getDicomMetadata(String sop) throws IOException{ return wsiCache.get(sop); } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java index 8710e3d9f..26d4696dd 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java @@ -1,5 +1,9 @@ package pt.ua.dicoogle.server.web.servlets; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; import org.restlet.data.Status; import org.slf4j.Logger; @@ -91,6 +95,48 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) response.sendError(Status.CLIENT_ERROR_NOT_FOUND.getCode(), "Could not build ROI with the provided UID"); } + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + String jsonString = IOUtils.toString(request.getReader()); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode body = mapper.readTree(jsonString); + + if(!body.has("uid")){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "SOPInstanceUID provided was invalid"); + return; + } + + if(!body.has("type")){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Annotation type must be provided"); + return; + } + + String sopInstanceUID = body.get("uid").asText(); + String type = body.get("type").asText(); + List points = mapper.readValue(body.get("points").toString(), new TypeReference>(){}); + + BulkAnnotation annotation = new BulkAnnotation(); + annotation.setPoints(points); + annotation.setAnnotationType(BulkAnnotation.AnnotationType.valueOf(type)); + + DicomMetaData dicomMetaData = this.getDicomMetadata(sopInstanceUID); + + BufferedImage bi = roiExtractor.extractROI(dicomMetaData, annotation); + + if(bi != null){ + response.setContentType("image/jpeg"); + OutputStream out = response.getOutputStream(); + ImageIO.write(bi, "jpg", out); + out.close(); + return; + } + + response.sendError(Status.CLIENT_ERROR_NOT_FOUND.getCode(), "Could not build ROI with the provided UID"); + } + private DicomMetaData getDicomMetadata(String sop) throws IOException{ return wsiCache.get(sop); } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index f63f9e391..85075b0ea 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -1,6 +1,9 @@ package pt.ua.dicoogle.server.web.servlets.mlprovider; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; import org.restlet.data.Status; import org.slf4j.Logger; @@ -23,7 +26,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; @@ -45,60 +47,38 @@ public MakePredictionServlet() { } @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - - String sopInstanceUID = request.getParameter("uid"); - String x = request.getParameter("x"); - String y = request.getParameter("y"); - String width = request.getParameter("width"); - String height = request.getParameter("height"); - String provider = request.getParameter("provider"); - String wsi = request.getParameter("wsi"); - final String baseSopInstanceUID = request.getParameter("baseUID"); - - if(sopInstanceUID == null || sopInstanceUID.isEmpty()){ + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + String jsonString = IOUtils.toString(request.getReader()); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode body = mapper.readTree(jsonString); + + if(!body.has("uid")){ response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "SOPInstanceUID provided was invalid"); return; } - if(provider == null || provider.isEmpty()){ + if(!body.has("provider")){ response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Provider provided was invalid"); return; } - if(x == null || x.isEmpty() || y == null || y.isEmpty() || width == null || width.isEmpty() || height == null || height.isEmpty()){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "ROI provided was invalid"); + if(!body.has("type")){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Annotation type must be provided"); return; } - if(wsi != null && wsi.equals("true")){ - if(baseSopInstanceUID == null || baseSopInstanceUID.isEmpty()){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "WSI was selected but a valid base SopInstanceUID was not provided"); - return; - } - } - - double nX, nY, nWidth, nHeight; - - try{ - nX = Double.parseDouble(x); - nY = Double.parseDouble(y); - nWidth = Double.parseDouble(width); - nHeight = Double.parseDouble(height); - } catch (NumberFormatException e){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "ROI provided was invalid"); - return; - } + String sopInstanceUID = body.get("uid").asText(); + String baseSopInstanceUID = body.get("baseUID").asText(); + String provider = body.get("provider").asText(); + String type = body.get("type").asText(); + boolean wsi = body.get("wsi").asBoolean(); + List points = mapper.readValue(body.get("points").toString(), new TypeReference>(){}); BulkAnnotation annotation = new BulkAnnotation(); - Point2D tl = new Point2D(nX, nY); - Point2D tr = new Point2D(nX + nWidth, nY); - Point2D bl = new Point2D(nX, nY + nHeight); - Point2D br = new Point2D(nX + nWidth, nY + nHeight); - List points = new ArrayList<>(); - points.add(tl); points.add(tr); points.add(bl); points.add(br); annotation.setPoints(points); - annotation.setAnnotationType(BulkAnnotation.AnnotationType.RECTANGLE); + annotation.setAnnotationType(BulkAnnotation.AnnotationType.valueOf(type)); DicomMetaData dicomMetaData = this.getDicomMetadata(sopInstanceUID); @@ -120,17 +100,16 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t MLPrediction prediction = task.get(); // Coordinates need to be converted if we're working with WSI - if(wsi.equals("true")){ + if(wsi){ WSISopDescriptor descriptor = new WSISopDescriptor(); descriptor.extractData(dicomMetaData.getAttributes()); DicomMetaData base = this.getDicomMetadata(baseSopInstanceUID); WSISopDescriptor baseDescriptor = new WSISopDescriptor(); baseDescriptor.extractData(base.getAttributes()); double scale = (descriptor.getTotalPixelMatrixRows() * 1.0) / baseDescriptor.getTotalPixelMatrixRows(); - convertCoordinates(prediction, nX, nY, scale); + convertCoordinates(prediction, points.get(0).getX(), points.get(0).getY(), scale); } - ObjectMapper mapper = new ObjectMapper(); response.setContentType("application/json"); PrintWriter out = response.getWriter(); mapper.writeValue(out, prediction); From b11cfcbbc3e3f28c8ba0faae795acd4620829560 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Fri, 6 Jan 2023 10:43:39 +0000 Subject: [PATCH 26/53] Fixed issues with ellipse ROIs. Some code improvements. --- .../server/web/dicom/ROIExtractor.java | 52 ++----------------- .../mlprovider/MakePredictionServlet.java | 18 +++++-- .../sdk/datastructs/dim/BulkAnnotation.java | 47 +++++++++++++++++ 3 files changed, 64 insertions(+), 53 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java index d76d50b76..5f9660f95 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -1,5 +1,6 @@ package pt.ua.dicoogle.server.web.dicom; +import javafx.scene.shape.Ellipse; import org.dcm4che3.imageio.plugins.dcm.DicomImageReadParam; import org.dcm4che3.imageio.plugins.dcm.DicomImageReader; import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; @@ -100,7 +101,7 @@ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDesc throw new IllegalArgumentException("Trying to build a ROI with an unsupported annotation type"); } - List constructionPoints = getOuterRectangle(annotation); //Points that will be used to construct the ROI. + List constructionPoints = annotation.getBoundingBox(); //Points that will be used to construct the ROI. Point2D annotationPoint1 = constructionPoints.get(0); Point2D annotationPoint2 = constructionPoints.get(3); @@ -115,7 +116,7 @@ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDesc BufferedImage combined = new BufferedImage(annotationWidth, annotationHeight, BufferedImage.TYPE_INT_RGB); Graphics g = combined.getGraphics(); - // We need to perform clipping + // We need to perform clipping if annotation is not a rectangle Shape clippingShape = null; if(annotation.getAnnotationType() != BulkAnnotation.AnnotationType.RECTANGLE){ clippingShape = getClippingShape(annotation, constructionPoints.get(0)); @@ -178,51 +179,6 @@ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDesc return combined; } - /** - * From a bulk annotation, find its outer rectangle. Only applicable to shape annotations. - * @param annotation - * @return a list of 4 points, representing a rectangle that contains the provided annotation. - */ - private List getOuterRectangle(BulkAnnotation annotation){ - - double minX = Double.MAX_VALUE; - double minY = Double.MAX_VALUE; - double maxX = Double.MIN_VALUE; - double maxY = Double.MIN_VALUE; - - switch (annotation.getAnnotationType()){ - case RECTANGLE: - return annotation.getPoints(); - case POLYGON: - case POLYLINE: - for(Point2D p : annotation.getPoints()){ - if(p.getX() > maxX) - maxX = p.getX(); - if(p.getX() < minX) - minX = p.getX(); - - if(p.getY() > maxY) - maxY = p.getY(); - if(p.getY() < minY) - minY = p.getY(); - } - break; - case ELLIPSE: - minX = annotation.getPoints().get(0).getX(); - maxX = annotation.getPoints().get(1).getX(); - minY = annotation.getPoints().get(2).getY(); - maxY = annotation.getPoints().get(3).getY(); - break; - } - - Point2D tl = new Point2D(minX, minY); - Point2D tr = new Point2D(maxX, minY); - Point2D bl = new Point2D(minX, maxY); - Point2D br = new Point2D(maxX, maxY); - - return Arrays.asList(tl, tr, bl, br); - } - /** * Given an annotation, get it as a Shape to apply as a clipping shape for the ROIs. * The points of this shape are normalized according to the starting point. @@ -247,7 +203,7 @@ private Shape getClippingShape(BulkAnnotation annotation, Point2D startingPoint) double minY = annotation.getPoints().get(2).getY(); double maxY = annotation.getPoints().get(3).getY(); - return new Ellipse2D.Double(minX, minY, Math.abs(maxX - minX), Math.abs(maxY - minY)); + return new Ellipse2D.Double(minX - startingPoint.getX(), minY - startingPoint.getY(), Math.abs(maxX - minX), Math.abs(maxY - minY)); default: return null; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index 85075b0ea..a9294cd52 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -99,15 +99,22 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) try { MLPrediction prediction = task.get(); + if(prediction == null){ + log.error("Provider returned null prediction"); + response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Could not make prediction"); + return; + } + // Coordinates need to be converted if we're working with WSI - if(wsi){ + if(wsi && !prediction.getAnnotations().isEmpty()){ WSISopDescriptor descriptor = new WSISopDescriptor(); descriptor.extractData(dicomMetaData.getAttributes()); DicomMetaData base = this.getDicomMetadata(baseSopInstanceUID); WSISopDescriptor baseDescriptor = new WSISopDescriptor(); baseDescriptor.extractData(base.getAttributes()); double scale = (descriptor.getTotalPixelMatrixRows() * 1.0) / baseDescriptor.getTotalPixelMatrixRows(); - convertCoordinates(prediction, points.get(0).getX(), points.get(0).getY(), scale); + Point2D tl = annotation.getBoundingBox().get(0); + convertCoordinates(prediction, tl, scale); } response.setContentType("application/json"); @@ -138,14 +145,15 @@ private DicomMetaData getDicomMetadata(String sop) throws IOException{ * When working with WSI, it is convenient to have coordinates relative to the base of the pyramid. * This method takes care of that. * @param prediction + * @param tl top left corner of the bounding box of the annotation * @param scale to transform coordinates * @return the ml prediction with the converted coordinates. */ - private void convertCoordinates(MLPrediction prediction, double x, double y, double scale){ + private void convertCoordinates(MLPrediction prediction, Point2D tl, double scale){ for(BulkAnnotation ann : prediction.getAnnotations()){ for(Point2D p : ann.getPoints()){ - p.setX((p.getX() + x)/scale); - p.setY((p.getY() + y)/scale); + p.setX((p.getX() + tl.getX())/scale); + p.setY((p.getY() + tl.getY())/scale); } } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java index 8b2779bab..950949152 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java @@ -1,5 +1,6 @@ package pt.ua.dicoogle.sdk.datastructs.dim; +import java.util.Arrays; import java.util.List; public class BulkAnnotation { @@ -77,4 +78,50 @@ public List getPoints() { public void setPoints(List points) { this.points = points; } + + + /** + * Calculate the bounding box of this annotation. + * @return a list of 4 points, representing a rectangle that contains the provided annotation. + */ + public List getBoundingBox(){ + + double minX = Double.MAX_VALUE; + double minY = Double.MAX_VALUE; + double maxX = Double.MIN_VALUE; + double maxY = Double.MIN_VALUE; + + switch (annotationType){ + case RECTANGLE: + return this.getPoints(); + case POLYGON: + case POLYLINE: + for(Point2D p : this.getPoints()){ + if(p.getX() > maxX) + maxX = p.getX(); + if(p.getX() < minX) + minX = p.getX(); + + if(p.getY() > maxY) + maxY = p.getY(); + if(p.getY() < minY) + minY = p.getY(); + } + break; + case ELLIPSE: + minX = this.getPoints().get(0).getX(); + maxX = this.getPoints().get(1).getX(); + minY = this.getPoints().get(2).getY(); + maxY = this.getPoints().get(3).getY(); + break; + } + + Point2D tl = new Point2D(minX, minY); + Point2D tr = new Point2D(maxX, minY); + Point2D bl = new Point2D(minX, maxY); + Point2D br = new Point2D(maxX, maxY); + + return Arrays.asList(tl, tr, bl, br); + } + } From e9ae2d66f37d526b7262e43f8f3788d1c8aba5b1 Mon Sep 17 00:00:00 2001 From: Rui Jesus Date: Fri, 6 Jan 2023 12:08:15 +0000 Subject: [PATCH 27/53] Removed problematic import --- .../main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java index 5f9660f95..d98706a6f 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -1,6 +1,5 @@ package pt.ua.dicoogle.server.web.dicom; -import javafx.scene.shape.Ellipse; import org.dcm4che3.imageio.plugins.dcm.DicomImageReadParam; import org.dcm4che3.imageio.plugins.dcm.DicomImageReader; import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; From 902fcb0d7e85a90576a2fa9a796751251d1689f9 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Tue, 9 May 2023 15:46:47 +0100 Subject: [PATCH 28/53] Fixed some compilation issues --- .../core/mlprovider/PrepareDatasetTask.java | 16 ++++++++++++---- sdk/pom.xml | 6 ++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java index c65a28860..a3dd5423a 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java @@ -2,9 +2,12 @@ import pt.ua.dicoogle.plugins.PluginController; import pt.ua.dicoogle.sdk.datastructs.SearchResult; -import pt.ua.dicoogle.sdk.imageworker.ImageROI; +import pt.ua.dicoogle.sdk.datastructs.dim.ImageROI; import pt.ua.dicoogle.sdk.mlprovider.MLDataset; import pt.ua.dicoogle.sdk.mlprovider.MLImageDataset; +import pt.ua.dicoogle.server.web.dicom.ROIExtractor; +import pt.ua.dicoogle.server.web.utils.cache.WSICache; + import java.util.HashMap; import java.util.List; import java.util.UUID; @@ -17,10 +20,17 @@ public class PrepareDatasetTask implements Callable { private final PluginController controller; private String dataset; + private final ROIExtractor roiExtractor; + + private final WSICache wsiCache; + public PrepareDatasetTask(PluginController controller, CreateDatasetRequest request) { this.controller = controller; this.request = request; this.dataset = UUID.randomUUID().toString(); + + this.wsiCache = WSICache.getInstance(); + this.roiExtractor = new ROIExtractor(); } @Override @@ -39,8 +49,6 @@ public MLDataset call() throws Exception { MLImageDataset mlDataset = new MLImageDataset(); - ImageWorkerInterface worker = controller.getImageWorkerInterfaceByName("roiExtractor", true); - this.request.getDataset().entrySet().forEach((entry -> { try { @@ -49,7 +57,7 @@ public MLDataset call() throws Exception { .query(controller.getQueryProvidersName(true).get(0), "SOPInstanceUID:" + entry.getKey(), extraFields).get(); for (SearchResult image : results) { - List rois = (List) worker.extractROIs(image, entry.getValue()); + //List rois = (List) roiExtractor.extractROI(); } } catch (InterruptedException | ExecutionException e) { diff --git a/sdk/pom.xml b/sdk/pom.xml index b234fb6f2..d89b23cf9 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -242,6 +242,12 @@ slf4j-api ${slf4j.version} + + org.dcm4che + dcm4che-core + 3.3.7 + compile + From 7d431e7191196e3c237e070cedf0d0313454bb3f Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Tue, 30 May 2023 16:28:45 +0100 Subject: [PATCH 29/53] Implemented list models endpoint. Changed make prediction endpoint to receive a model id. --- .../ua/dicoogle/plugins/PluginController.java | 5 ++- .../ua/dicoogle/server/web/DicoogleWeb.java | 4 +- .../mlprovider/ListAllModelsServlet.java | 40 +++++++++++++++++ .../mlprovider/MakePredictionServlet.java | 8 +++- .../ua/dicoogle/sdk/mlprovider/MLModel.java | 45 +++++++++++++++++++ .../dicoogle/sdk/mlprovider/MLProvider.java | 44 ++++++++++++++++++ .../sdk/mlprovider/MLProviderInterface.java | 7 ++- 7 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProvider.java diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java index 5a9a073c8..464cd3ca0 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java @@ -880,15 +880,16 @@ public List indexBlocking(URI path) { * This method orders a prediction on the selected image, using the selected provider. * @param bos image to classify. * @param provider provider to use. + * @param modelID the model identifier of the provider to use. * @return the created task */ - public Task makePredictionOverImage(final ByteArrayOutputStream bos, final String provider) { + public Task makePredictionOverImage(final ByteArrayOutputStream bos, final String provider, final String modelID) { MLProviderInterface providerInterface = this.getMachineLearningProviderByName(provider, true); if(providerInterface == null) return null; String taskName = "MLPredictionTask" + UUID.randomUUID(); - Task result = providerInterface.makePredictionOverImage(bos); + Task result = providerInterface.makePredictionOverImage(bos, modelID); result.setName(taskName); return result; } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index 0be10a6f5..5e45bcc04 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java @@ -141,6 +141,7 @@ public DicoogleWeb(int port) throws Exception { // setup the ml endpoints final ServletContextHandler makePrediction = createServletHandler(new MakePredictionServlet(), "/makePrediction"); + final ServletContextHandler listAllModels = createServletHandler(new ListAllModelsServlet(), "/listAllModels"); // setup the DICOM to PNG image servlet final ServletContextHandler dictags = createServletHandler(new TagsServlet(), "/dictags"); @@ -180,7 +181,7 @@ public DicoogleWeb(int port) throws Exception { PluginRestletApplication.attachRestPlugin(new VersionResource()); // list the all the handlers mounted above - Handler[] handlers = new Handler[] {pluginHandler, legacyHandler, dic2png, roiExtractor, makePrediction, dictags, + Handler[] handlers = new Handler[] {pluginHandler, legacyHandler, dic2png, roiExtractor, makePrediction, dictags, listAllModels, createServletHandler(new IndexerServlet(), "/indexer"), // DEPRECATED createServletHandler(new SettingsServlet(), "/settings"), csvServletHolder, createServletHandler(new LoginServlet(), "/login"), @@ -227,6 +228,7 @@ public DicoogleWeb(int port) throws Exception { // ml provider servlets createServletHandler(new CreateDatasetServlet(), "/ml/createDataset"), createServletHandler(new MakePredictionServlet(), "/ml/makePrediction"), + createServletHandler(new ListAllModelsServlet(), "/ml/listAllModels"), createServletHandler(new MakeBulkPredictionServlet(), "/ml/makeBulkPrediction"), webpages}; // setup the server diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java new file mode 100644 index 000000000..d471fb701 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java @@ -0,0 +1,40 @@ +package pt.ua.dicoogle.server.web.servlets.mlprovider; + +import com.fasterxml.jackson.databind.ObjectMapper; +import pt.ua.dicoogle.plugins.PluginController; +import pt.ua.dicoogle.sdk.mlprovider.MLProvider; +import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +public class ListAllModelsServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + ObjectMapper mapper = new ObjectMapper(); + + Iterable providers = PluginController.getInstance().getMLPlugins(true); + + List providersResponse = new ArrayList<>(); + + providers.forEach((mlPlugin) -> { + MLProvider provider = new MLProvider(mlPlugin.getName()); + provider.setModels(mlPlugin.listModels()); + providersResponse.add(provider); + }); + + response.setContentType("application/json"); + PrintWriter out = response.getWriter(); + mapper.writeValue(out, providersResponse); + out.close(); + out.flush(); + } + +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index a9294cd52..e96f5e3a0 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -64,6 +64,11 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) return; } + if(!body.has("modelID")){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Model identifier was invalid"); + return; + } + if(!body.has("type")){ response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Annotation type must be provided"); return; @@ -72,6 +77,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) String sopInstanceUID = body.get("uid").asText(); String baseSopInstanceUID = body.get("baseUID").asText(); String provider = body.get("provider").asText(); + String modelID = body.get("modelID").asText(); String type = body.get("type").asText(); boolean wsi = body.get("wsi").asBoolean(); List points = mapper.readValue(body.get("points").toString(), new TypeReference>(){}); @@ -89,7 +95,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) ImageIO.write(bi, "jpg", bos); - Task task = PluginController.getInstance().makePredictionOverImage(bos, provider); + Task task = PluginController.getInstance().makePredictionOverImage(bos, provider, modelID); if(task == null){ response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Could not create prediction task"); return; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java index 938faef09..e51de0a06 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java @@ -13,4 +13,49 @@ public class MLModel { private Date creationDate; private ML_DATA_TYPE dataType; + + public MLModel(String name, String id) { + this.name = name; + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + public ML_DATA_TYPE getDataType() { + return dataType; + } + + public void setDataType(ML_DATA_TYPE dataType) { + this.dataType = dataType; + } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProvider.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProvider.java new file mode 100644 index 000000000..46e01d359 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProvider.java @@ -0,0 +1,44 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +import java.util.ArrayList; +import java.util.List; + +/** + * Data object to model MLPlugin instances. + */ +public class MLProvider { + + public MLProvider(String name) { + this.name = name; + this.models = new ArrayList<>(); + this.datasets = new ArrayList<>(); + } + + private List models; + private List datasets; + private String name; + + public List getModels() { + return models; + } + + public void setModels(List models) { + this.models = models; + } + + public List getDatasets() { + return datasets; + } + + public void setDatasets(List datasets) { + this.datasets = datasets; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java index f9a80a971..3803da374 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java @@ -1,10 +1,8 @@ package pt.ua.dicoogle.sdk.mlprovider; import pt.ua.dicoogle.sdk.DicooglePlugin; -import pt.ua.dicoogle.sdk.datastructs.dim.ImageROI; import pt.ua.dicoogle.sdk.task.Task; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.List; import java.util.Set; @@ -65,9 +63,10 @@ public abstract class MLProviderInterface implements DicooglePlugin { /** * Order a image prediction - * @param bos the image to classify + * @param bos the image to classify. + * @param modelID A model identifier to be used to make the prediction. */ - public abstract Task makePredictionOverImage(ByteArrayOutputStream bos); + public abstract Task makePredictionOverImage(ByteArrayOutputStream bos, String modelID); /** * This method makes a bulk prediction using the selected model From 3a7b451d1666fa70963eb9cb4767441f3afbd15b Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Fri, 2 Jun 2023 16:10:16 +0100 Subject: [PATCH 30/53] Added new train model signature do provider interface. Renamed createDataset signature to dataStore. Added new server setting to enable WSI operation. Some code adjustments. --- .../src/main/java/pt/ua/dicoogle/Main.java | 8 +- ...asetRequest.java => DatastoreRequest.java} | 4 +- .../core/mlprovider/PrepareDatasetTask.java | 6 +- .../core/settings/LegacyServerSettings.java | 10 + .../core/settings/part/ArchiveImpl.java | 14 +- .../ua/dicoogle/plugins/PluginController.java | 17 +- .../ua/dicoogle/server/web/DicoogleWeb.java | 2 +- ...asetServlet.java => DatastoreServlet.java} | 10 +- .../mlprovider/ListAllModelsServlet.java | 25 ++- .../mlprovider/MakePredictionServlet.java | 179 ++++++++++++------ .../sdk/mlprovider/MLDicomDataset.java | 41 ++++ .../sdk/mlprovider/MLImageDataset.java | 4 + .../ua/dicoogle/sdk/mlprovider/MLModel.java | 32 +++- .../sdk/mlprovider/MLPredictionRequest.java | 69 +++++++ .../sdk/mlprovider/MLProviderInterface.java | 26 ++- .../dicoogle/sdk/mlprovider/ML_DATA_TYPE.java | 2 +- .../ua/dicoogle/sdk/mlprovider/MLlabel.java | 43 +++-- .../sdk/settings/server/ServerSettings.java | 2 + .../settings/server/ServerSettingsReader.java | 3 + 19 files changed, 386 insertions(+), 111 deletions(-) rename dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/{CreateDatasetRequest.java => DatastoreRequest.java} (91%) rename dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/{CreateDatasetServlet.java => DatastoreServlet.java} (79%) create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPredictionRequest.java diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/Main.java b/dicoogle/src/main/java/pt/ua/dicoogle/Main.java index e0c9491ac..608875215 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/Main.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/Main.java @@ -176,9 +176,11 @@ private static void LaunchDicoogle() { pt.ua.dicoogle.server.ControlServices.getInstance(); // Register Image Reader for DICOM Objects - IIORegistry.getDefaultInstance().registerServiceProvider(new DicomImageReaderSpi()); - ImageIO.setUseCache(false); - System.setProperty("dcm4che.useImageIOServiceRegistry", "true"); + if(settings.getArchiveSettings().isSupportWSI()){ + IIORegistry.getDefaultInstance().registerServiceProvider(new DicomImageReaderSpi()); + ImageIO.setUseCache(false); + System.setProperty("dcm4che.useImageIOServiceRegistry", "true"); + } // Launch Async Index // It monitors a folder, and when a file is touched an event diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/CreateDatasetRequest.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java similarity index 91% rename from dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/CreateDatasetRequest.java rename to dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java index 9ab24fe6c..f5e5ac05e 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/CreateDatasetRequest.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java @@ -6,10 +6,10 @@ import java.util.List; /** - * Java object to represent create dataset requests + * Java object to represent datastore requests * @author Rui Jesus */ -public class CreateDatasetRequest { +public class DatastoreRequest { /** * The name of the ML Provider plugin to update the dataset to. diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java index a3dd5423a..3e458b78a 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java @@ -2,21 +2,19 @@ import pt.ua.dicoogle.plugins.PluginController; import pt.ua.dicoogle.sdk.datastructs.SearchResult; -import pt.ua.dicoogle.sdk.datastructs.dim.ImageROI; import pt.ua.dicoogle.sdk.mlprovider.MLDataset; import pt.ua.dicoogle.sdk.mlprovider.MLImageDataset; import pt.ua.dicoogle.server.web.dicom.ROIExtractor; import pt.ua.dicoogle.server.web.utils.cache.WSICache; import java.util.HashMap; -import java.util.List; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; public class PrepareDatasetTask implements Callable { - private final CreateDatasetRequest request; + private final DatastoreRequest request; private final PluginController controller; private String dataset; @@ -24,7 +22,7 @@ public class PrepareDatasetTask implements Callable { private final WSICache wsiCache; - public PrepareDatasetTask(PluginController controller, CreateDatasetRequest request) { + public PrepareDatasetTask(PluginController controller, DatastoreRequest request) { this.controller = controller; this.request = request; this.dataset = UUID.randomUUID().toString(); diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/LegacyServerSettings.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/LegacyServerSettings.java index ba3a2c708..dfcff1999 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/LegacyServerSettings.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/LegacyServerSettings.java @@ -1206,6 +1206,10 @@ public void setWatchDirectory(String dir) { LegacyServerSettings.this.setWatchDirectory(dir); } + @Override + public void setSupportWSI(boolean supportWSI) { + } + @Override public void setDirectoryWatcherEnabled(boolean watch) { LegacyServerSettings.this.setDirectoryWatcherEnabled(watch); @@ -1267,6 +1271,12 @@ public String getWatchDirectory() { return LegacyServerSettings.this.getWatchDirectory(); } + @JsonGetter("support-wsi") + @Override + public boolean isSupportWSI() { + return false; + } + @JsonGetter("dim-provider") @Override public List getDIMProviders() { diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/part/ArchiveImpl.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/part/ArchiveImpl.java index 29c45cfd8..55f6c831c 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/part/ArchiveImpl.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/part/ArchiveImpl.java @@ -43,6 +43,7 @@ public static ArchiveImpl createDefault() { a.indexerEffort = 100; a.dirWatcherEnabled = false; a.watchDirectory = ""; + a.supportWSI = false; // Note: make it `true` in Dicoogle 4 a.callShutdown = false; @@ -76,6 +77,9 @@ public static ArchiveImpl createDefault() { @JsonProperty("watch-directory") private String watchDirectory; + @JsonProperty("support-wsi") + private boolean supportWSI; + @JsonProperty(value = "encrypt-users-file", defaultValue = "false") private boolean encryptUsersFile; @@ -154,6 +158,14 @@ public void setWatchDirectory(String watchDirectory) { this.watchDirectory = watchDirectory; } + public boolean isSupportWSI() { + return supportWSI; + } + + public void setSupportWSI(boolean supportWSI) { + this.supportWSI = supportWSI; + } + @Override public String getNodeName() { return nodeName; @@ -189,7 +201,7 @@ public String toString() { return "ArchiveImpl{" + "saveThumbnails=" + saveThumbnails + ", thumbnailSize=" + thumbnailSize + ", indexerEffort=" + indexerEffort + ", dimProviders=" + dimProviders + ", defaultStorage=" + defaultStorage + ", dirWatcherEnabled=" + dirWatcherEnabled + ", watchDirectory='" + watchDirectory - + '\'' + ", mainDirectory='" + mainDirectory + '\'' + ", nodeName='" + nodeName + '\'' + + ", supportWSI='" + supportWSI + '\'' + ", mainDirectory='" + mainDirectory + '\'' + ", nodeName='" + nodeName + '\'' + ", callShutdown=" + callShutdown + ", encryptUsersFile=" + encryptUsersFile + '}'; } } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java index 464cd3ca0..2ee6c199d 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java @@ -23,6 +23,7 @@ import org.restlet.resource.ServerResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import pt.ua.dicoogle.core.mlprovider.DatastoreRequest; import pt.ua.dicoogle.core.mlprovider.PrepareDatasetTask; import pt.ua.dicoogle.core.settings.ServerSettingsManager; import pt.ua.dicoogle.plugins.webui.WebUIPlugin; @@ -32,9 +33,9 @@ import pt.ua.dicoogle.sdk.datastructs.UnindexReport; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; -import pt.ua.dicoogle.sdk.datastructs.dim.ImageROI; import pt.ua.dicoogle.sdk.mlprovider.MLDataset; import pt.ua.dicoogle.sdk.mlprovider.MLPrediction; +import pt.ua.dicoogle.sdk.mlprovider.MLPredictionRequest; import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; import pt.ua.dicoogle.sdk.task.JointQueryTask; @@ -42,12 +43,9 @@ import pt.ua.dicoogle.server.ControlServices; import pt.ua.dicoogle.server.PluginRestletApplication; import pt.ua.dicoogle.server.web.DicoogleWeb; -import pt.ua.dicoogle.core.mlprovider.CreateDatasetRequest; import pt.ua.dicoogle.taskManager.RunningIndexTasks; import pt.ua.dicoogle.taskManager.TaskManager; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.net.URI; @@ -878,23 +876,22 @@ public List indexBlocking(URI path) { /** * This method orders a prediction on the selected image, using the selected provider. - * @param bos image to classify. * @param provider provider to use. - * @param modelID the model identifier of the provider to use. + * @param predictionRequest * @return the created task */ - public Task makePredictionOverImage(final ByteArrayOutputStream bos, final String provider, final String modelID) { + public Task makePredictionOverImage(final String provider, final MLPredictionRequest predictionRequest) { MLProviderInterface providerInterface = this.getMachineLearningProviderByName(provider, true); if(providerInterface == null) return null; String taskName = "MLPredictionTask" + UUID.randomUUID(); - Task result = providerInterface.makePredictionOverImage(bos, modelID); + Task result = providerInterface.makePrediction(predictionRequest); result.setName(taskName); return result; } - public Task prepareMLDataset(final CreateDatasetRequest datasetRequest) { + public Task prepareMLDataset(final DatastoreRequest datasetRequest) { String uuid = UUID.randomUUID().toString(); Task prepareTask = new Task<>("MLPrepareDatasetTask" + uuid, new PrepareDatasetTask(this, datasetRequest)); @@ -904,7 +901,7 @@ public Task prepareMLDataset(final CreateDatasetRequest datasetReques logger.error("MLProvider with name {} not found", prepareTask.getName()); } else { try { - mlInterface.createDataset(prepareTask.get()); + mlInterface.dataStore(prepareTask.get()); } catch (InterruptedException | ExecutionException e) { logger.error("Task {} failed execution", prepareTask.getName(), e); } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index 5e45bcc04..227e397c5 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java @@ -226,7 +226,7 @@ public DicoogleWeb(int port) throws Exception { createServletHandler(new ServerStorageServlet(), "/management/settings/storage/dicom"), // ml provider servlets - createServletHandler(new CreateDatasetServlet(), "/ml/createDataset"), + createServletHandler(new DatastoreServlet(), "/ml/createDataset"), createServletHandler(new MakePredictionServlet(), "/ml/makePrediction"), createServletHandler(new ListAllModelsServlet(), "/ml/listAllModels"), createServletHandler(new MakeBulkPredictionServlet(), "/ml/makeBulkPrediction"), webpages}; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java similarity index 79% rename from dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java rename to dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java index 1b2e6a706..26b0542f1 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CreateDatasetServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java @@ -4,7 +4,7 @@ import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import pt.ua.dicoogle.core.mlprovider.CreateDatasetRequest; +import pt.ua.dicoogle.core.mlprovider.DatastoreRequest; import pt.ua.dicoogle.plugins.PluginController; import javax.servlet.ServletException; @@ -13,9 +13,9 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; -public class CreateDatasetServlet extends HttpServlet { +public class DatastoreServlet extends HttpServlet { - private static final Logger log = LoggerFactory.getLogger(CreateDatasetServlet.class); + private static final Logger log = LoggerFactory.getLogger(DatastoreServlet.class); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { @@ -27,9 +27,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S } ObjectMapper mapper = new ObjectMapper(); - CreateDatasetRequest datasetRequest; + DatastoreRequest datasetRequest; try { - datasetRequest = mapper.readValue(dataString, CreateDatasetRequest.class); + datasetRequest = mapper.readValue(dataString, DatastoreRequest.class); /* if(PluginController.getInstance().getMachineLearningProviderByName(datasetRequest.getProviderName(), true) == null){ resp.sendError(404, "The requested provider does not exist"); diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java index d471fb701..c2e63a842 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java @@ -14,21 +14,32 @@ import java.util.ArrayList; import java.util.List; +/** + * This servlet lists all models from all providers. + * Optionally, if a provider is specified, it will only list models from that provider. + */ public class ListAllModelsServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ObjectMapper mapper = new ObjectMapper(); - Iterable providers = PluginController.getInstance().getMLPlugins(true); - + String provider = request.getParameter("provider"); List providersResponse = new ArrayList<>(); - providers.forEach((mlPlugin) -> { - MLProvider provider = new MLProvider(mlPlugin.getName()); - provider.setModels(mlPlugin.listModels()); - providersResponse.add(provider); - }); + if(provider != null && !provider.isEmpty()){ + MLProviderInterface mlPlugin = PluginController.getInstance().getMachineLearningProviderByName(provider, true); + MLProvider p = new MLProvider(mlPlugin.getName()); + p.setModels(mlPlugin.listModels()); + providersResponse.add(p); + } else { + Iterable providers = PluginController.getInstance().getMLPlugins(true); + providers.forEach((mlPlugin) -> { + MLProvider p = new MLProvider(mlPlugin.getName()); + p.setModels(mlPlugin.listModels()); + providersResponse.add(p); + }); + } response.setContentType("application/json"); PrintWriter out = response.getWriter(); diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index e96f5e3a0..3497fdb4a 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -10,20 +10,20 @@ import org.slf4j.LoggerFactory; import pt.ua.dicoogle.plugins.PluginController; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; +import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; import pt.ua.dicoogle.sdk.datastructs.wsi.WSISopDescriptor; import pt.ua.dicoogle.sdk.mlprovider.MLPrediction; +import pt.ua.dicoogle.sdk.mlprovider.MLPredictionRequest; import pt.ua.dicoogle.sdk.task.Task; import pt.ua.dicoogle.server.web.dicom.ROIExtractor; import pt.ua.dicoogle.server.web.utils.cache.WSICache; -import javax.imageio.ImageIO; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.List; @@ -54,8 +54,15 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) ObjectMapper mapper = new ObjectMapper(); JsonNode body = mapper.readTree(jsonString); + // Validate the common attributes between WSI and non-WSI requests + + if(!body.has("level")){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "DIM level provided was invalid"); + return; + } + if(!body.has("uid")){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "SOPInstanceUID provided was invalid"); + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "DIM UID provided was invalid"); return; } @@ -69,78 +76,138 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) return; } - if(!body.has("type")){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Annotation type must be provided"); - return; - } - - String sopInstanceUID = body.get("uid").asText(); - String baseSopInstanceUID = body.get("baseUID").asText(); String provider = body.get("provider").asText(); String modelID = body.get("modelID").asText(); - String type = body.get("type").asText(); boolean wsi = body.get("wsi").asBoolean(); - List points = mapper.readValue(body.get("points").toString(), new TypeReference>(){}); + DimLevel level = DimLevel.valueOf(body.get("level").asText().toUpperCase()); + String dimUID = body.get("uid").asText(); - BulkAnnotation annotation = new BulkAnnotation(); - annotation.setPoints(points); - annotation.setAnnotationType(BulkAnnotation.AnnotationType.valueOf(type)); + Task task; + + if(wsi){ + + if(!body.has("points") || !body.has("type") || !body.has("baseUID")){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Insufficient data to build request"); + return; + } - DicomMetaData dicomMetaData = this.getDicomMetadata(sopInstanceUID); + if(level != DimLevel.INSTANCE){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Only Instance level is supported with WSI"); + return; + } - BufferedImage bi = roiExtractor.extractROI(dicomMetaData, annotation); + String baseSopInstanceUID = body.get("baseUID").asText(); - // mount the resulting memory stream - ByteArrayOutputStream bos = new ByteArrayOutputStream(); + BulkAnnotation.AnnotationType type = BulkAnnotation.AnnotationType.valueOf(body.get("type").asText()); + List points = mapper.readValue(body.get("points").toString(), new TypeReference>(){}); + task = sendWSIRequest(provider, modelID, baseSopInstanceUID, dimUID, type, points, response); - ImageIO.write(bi, "jpg", bos); + } else { + task = sendRequest(provider, modelID, level, dimUID, response); + } - Task task = PluginController.getInstance().makePredictionOverImage(bos, provider, modelID); if(task == null){ - response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Could not create prediction task"); + response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Could not build prediction request"); return; } - task.onCompletion(() -> { - try { - MLPrediction prediction = task.get(); + task.run(); + } - if(prediction == null){ - log.error("Provider returned null prediction"); - response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Could not make prediction"); - return; - } + private Task sendWSIRequest(String provider, String modelID, String baseSopInstanceUID, String uid, + BulkAnnotation.AnnotationType roiType, List roi, HttpServletResponse response){ - // Coordinates need to be converted if we're working with WSI - if(wsi && !prediction.getAnnotations().isEmpty()){ - WSISopDescriptor descriptor = new WSISopDescriptor(); - descriptor.extractData(dicomMetaData.getAttributes()); - DicomMetaData base = this.getDicomMetadata(baseSopInstanceUID); - WSISopDescriptor baseDescriptor = new WSISopDescriptor(); - baseDescriptor.extractData(base.getAttributes()); - double scale = (descriptor.getTotalPixelMatrixRows() * 1.0) / baseDescriptor.getTotalPixelMatrixRows(); - Point2D tl = annotation.getBoundingBox().get(0); - convertCoordinates(prediction, tl, scale); - } + ObjectMapper mapper = new ObjectMapper(); + MLPredictionRequest predictionRequest = new MLPredictionRequest(true, DimLevel.INSTANCE, uid, modelID); + BulkAnnotation annotation = new BulkAnnotation(); + annotation.setPoints(roi); + annotation.setAnnotationType(roiType); + + try { + DicomMetaData dicomMetaData = this.getDicomMetadata(uid); + BufferedImage bi = roiExtractor.extractROI(dicomMetaData, annotation); + predictionRequest.setRoi(bi); + Task task = PluginController.getInstance().makePredictionOverImage(provider, predictionRequest); + if(task != null){ + task.onCompletion(() -> { + try { + MLPrediction prediction = task.get(); + + if(prediction == null){ + log.error("Provider returned null prediction"); + response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Could not make prediction"); + return; + } + + // Coordinates need to be converted if we're working with WSI + if(!prediction.getAnnotations().isEmpty()){ + WSISopDescriptor descriptor = new WSISopDescriptor(); + descriptor.extractData(dicomMetaData.getAttributes()); + DicomMetaData base = this.getDicomMetadata(baseSopInstanceUID); + WSISopDescriptor baseDescriptor = new WSISopDescriptor(); + baseDescriptor.extractData(base.getAttributes()); + double scale = (descriptor.getTotalPixelMatrixRows() * 1.0) / baseDescriptor.getTotalPixelMatrixRows(); + Point2D tl = annotation.getBoundingBox().get(0); + convertCoordinates(prediction, tl, scale); + } + + response.setContentType("application/json"); + PrintWriter out = response.getWriter(); + mapper.writeValue(out, prediction); + out.close(); + out.flush(); + } catch (InterruptedException | ExecutionException e) { + log.error("Could not make prediction", e); + try { + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Could not make prediction"); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + return task; + } catch (IOException e) { + return null; + } + } - response.setContentType("application/json"); - PrintWriter out = response.getWriter(); - mapper.writeValue(out, prediction); - out.close(); - out.flush(); - } catch (InterruptedException | ExecutionException e) { - log.error("Could not make prediction", e); + private Task sendRequest(String provider, String modelID, DimLevel level, String dimUID, HttpServletResponse response){ + ObjectMapper mapper = new ObjectMapper(); + MLPredictionRequest predictionRequest = new MLPredictionRequest(true, level, dimUID, modelID); + Task task = PluginController.getInstance().makePredictionOverImage(provider, predictionRequest); + if(task != null){ + task.onCompletion(() -> { try { - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Could not make prediction"); - } catch (IOException ex) { - throw new RuntimeException(ex); + MLPrediction prediction = task.get(); + + if(prediction == null){ + log.error("Provider returned null prediction"); + response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Could not make prediction"); + return; + } + + response.setContentType("application/json"); + PrintWriter out = response.getWriter(); + mapper.writeValue(out, prediction); + out.close(); + out.flush(); + } catch (InterruptedException | ExecutionException e) { + log.error("Could not make prediction", e); + try { + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Could not make prediction"); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } catch (IOException e) { + throw new RuntimeException(e); } - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + }); + } - task.run(); + return task; } private DicomMetaData getDicomMetadata(String sop) throws IOException{ diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java new file mode 100644 index 000000000..9242f8958 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java @@ -0,0 +1,41 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; + +import java.util.ArrayList; +import java.util.List; + +/** + * An ML dataset of DICOM objects. + */ +public class MLDicomDataset extends MLDataset { + + private DimLevel level; + private List dimUIDs; + + public MLDicomDataset(DimLevel level){ + this.level = level; + dimUIDs = new ArrayList<>(); + } + + public MLDicomDataset(DimLevel level, List dimUIDs){ + this.level = level; + this.dimUIDs = dimUIDs; + } + + public DimLevel getLevel() { + return level; + } + + public void setLevel(DimLevel level) { + this.level = level; + } + + public List getDimUIDs() { + return dimUIDs; + } + + public void setDimUIDs(List dimUIDs) { + this.dimUIDs = dimUIDs; + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java index dac1ec510..00d4851bd 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java @@ -4,6 +4,10 @@ import java.util.HashMap; import java.util.List; +/** + * An ML dataset of image objects. + * Optionally an array of labels can be given to create a labelled dataset. + */ public class MLImageDataset extends MLDataset { private HashMap> dataset; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java index e51de0a06..ded1768f1 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java @@ -1,6 +1,6 @@ package pt.ua.dicoogle.sdk.mlprovider; -import java.util.Date; +import java.util.*; public class MLModel { @@ -8,15 +8,21 @@ public class MLModel { private String id; + private String type; + private String description; private Date creationDate; private ML_DATA_TYPE dataType; + private Set labels; + public MLModel(String name, String id) { this.name = name; this.id = id; + this.type = ""; + labels = new TreeSet<>(); } public String getName() { @@ -35,6 +41,14 @@ public void setId(String id) { this.id = id; } + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + public String getDescription() { return description; } @@ -58,4 +72,20 @@ public ML_DATA_TYPE getDataType() { public void setDataType(ML_DATA_TYPE dataType) { this.dataType = dataType; } + + public Set getLabels() { + return labels; + } + + public void setLabels(Set labels) { + this.labels = labels; + } + + public void removeLabel(MLlabel label){ + this.labels.remove(label); + } + + public void addLabel(MLlabel label){ + this.labels.add(label); + } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPredictionRequest.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPredictionRequest.java new file mode 100644 index 000000000..2c9f7849f --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPredictionRequest.java @@ -0,0 +1,69 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; + +import java.awt.image.BufferedImage; + +public class MLPredictionRequest { + + private boolean isWsi; + + private DimLevel level; + + private String dimID; + + private BufferedImage roi; + + private String modelID; + + public MLPredictionRequest(boolean isWsi, DimLevel level, String dimID, String modelID) { + this.isWsi = isWsi; + this.level = level; + this.dimID = dimID; + this.modelID = modelID; + } + + public boolean isWsi() { + return isWsi; + } + + public void setWsi(boolean wsi) { + isWsi = wsi; + } + + public DimLevel getLevel() { + return level; + } + + public void setLevel(DimLevel level) { + this.level = level; + } + + public String getDimID() { + return dimID; + } + + public void setDimID(String dimID) { + this.dimID = dimID; + } + + public BufferedImage getRoi() { + return roi; + } + + public void setRoi(BufferedImage roi) { + this.roi = roi; + } + + public boolean hasROI(){ + return this.roi != null; + } + + public String getModelID() { + return modelID; + } + + public void setModelID(String modelID) { + this.modelID = modelID; + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java index 3803da374..8b29edb22 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java @@ -3,7 +3,6 @@ import pt.ua.dicoogle.sdk.DicooglePlugin; import pt.ua.dicoogle.sdk.task.Task; -import java.io.ByteArrayOutputStream; import java.util.List; import java.util.Set; @@ -21,16 +20,26 @@ public abstract class MLProviderInterface implements DicooglePlugin { protected Set acceptedDataTypes; /** - * This method creates and uploads a dataset to the machine learning provider. - * A dataset is defined as a set of labelled images or a labelled CSV file with one column used to label the entries. + * This method uploads data to the provider. + * The API assumes three kinds of data: + * - CSV files, identified by a URI + * - Image files, identified by a URI + * - DICOM files, identified by their respective UIDs. + * This method can be used to upload labelled or un-labelled datasets to the provider. */ - public abstract void createDataset(MLDataset dataset); + public abstract void dataStore(MLDataset dataset); /** * This method creates a model using a specific dataset */ public abstract MLModel createModel(); + /** + * This method orders the training of a model. + * @param modelID the unique model identifier within the provider. + */ + public abstract boolean trainModel(String modelID); + /** * This method creates a endpoint that exposes a service */ @@ -62,11 +71,12 @@ public abstract class MLProviderInterface implements DicooglePlugin { public abstract void deleteModel(); /** - * Order a image prediction - * @param bos the image to classify. - * @param modelID A model identifier to be used to make the prediction. + * Order a prediction over a single object. + * The object can be a series instance, a sop instance or a 2D/3D ROI. + * + * @param predictionRequest object that defines this prediction request */ - public abstract Task makePredictionOverImage(ByteArrayOutputStream bos, String modelID); + public abstract Task makePrediction(MLPredictionRequest predictionRequest); /** * This method makes a bulk prediction using the selected model diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java index 52e6e2cf8..0fc12fbd5 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java @@ -1,5 +1,5 @@ package pt.ua.dicoogle.sdk.mlprovider; public enum ML_DATA_TYPE { - CSV, IMAGE + CSV, IMAGE, DICOm } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java index 0eb34fa78..72eacfe01 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java @@ -1,18 +1,19 @@ package pt.ua.dicoogle.sdk.mlprovider; -public class MLlabel { +import java.util.Objects; + +/** + * A label object that belongs to a model. + * Labels must be unique within a model. + */ +public class MLlabel implements Comparable{ private String label; - private double confidence; + private String description; public MLlabel(String label) { this.label = label; - this.confidence = 1; - } - - public MLlabel(String label, double confidence) { - this.label = label; - this.confidence = confidence; + this.description = ""; } public String getLabel() { @@ -23,11 +24,29 @@ public void setLabel(String label) { this.label = label; } - public double getConfidence() { - return confidence; + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MLlabel mLlabel = (MLlabel) o; + return label.equals(mLlabel.label); + } + + @Override + public int hashCode() { + return Objects.hash(label); } - public void setConfidence(double confidence) { - this.confidence = confidence; + @Override + public int compareTo(MLlabel o) { + return o.getLabel().compareTo(this.getLabel()); } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/settings/server/ServerSettings.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/settings/server/ServerSettings.java index b0c20d71a..6e7cbfbf7 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/settings/server/ServerSettings.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/settings/server/ServerSettings.java @@ -65,6 +65,8 @@ interface Archive extends ServerSettingsReader.Archive { void setDirectoryWatcherEnabled(boolean watch); + void setSupportWSI(boolean supportWSI); + void setEncryptUsersFile(boolean encrypt); void setDIMProviders(List providers); diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/settings/server/ServerSettingsReader.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/settings/server/ServerSettingsReader.java index 02a44ed0b..f3c589be1 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/settings/server/ServerSettingsReader.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/settings/server/ServerSettingsReader.java @@ -79,6 +79,9 @@ interface Archive { @JsonGetter("watch-directory") String getWatchDirectory(); + @JsonGetter("support-wsi") + boolean isSupportWSI(); + @JsonGetter("dim-provider") @JacksonXmlElementWrapper(localName = "dim-providers") List getDIMProviders(); From 94ed3d94094394b01725c9de5b8b24988bb82c32 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Wed, 7 Jun 2023 17:24:26 +0100 Subject: [PATCH 31/53] Improvements to datastore endpoint. New Train endpoint. --- .../core/mlprovider/DatastoreRequest.java | 65 +++++++++++++-- .../core/mlprovider/PrepareDatasetTask.java | 68 --------------- .../core/mlprovider/PrepareDatastoreTask.java | 82 +++++++++++++++++++ .../core/mlprovider/TrainRequest.java | 27 ++++++ .../ua/dicoogle/plugins/PluginController.java | 8 +- .../ua/dicoogle/server/web/DicoogleWeb.java | 9 +- .../servlets/mlprovider/DatastoreServlet.java | 4 +- .../web/servlets/mlprovider/TrainServlet.java | 47 +++++++++++ .../ua/dicoogle/sdk/mlprovider/MLModel.java | 2 + .../dicoogle/sdk/mlprovider/ML_DATA_TYPE.java | 2 +- 10 files changed, 228 insertions(+), 86 deletions(-) delete mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java index f5e5ac05e..a583833ef 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java @@ -1,12 +1,17 @@ package pt.ua.dicoogle.core.mlprovider; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; +import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; +import pt.ua.dicoogle.sdk.mlprovider.ML_DATA_TYPE; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** - * Java object to represent datastore requests + * Java object to represent datastore requests. + * Datastore requests can be for image ROIs or DICOM objects. + * In case of DICOM objects, the DIM level and DIM UID must be provided. * @author Rui Jesus */ public class DatastoreRequest { @@ -14,7 +19,16 @@ public class DatastoreRequest { /** * The name of the ML Provider plugin to update the dataset to. */ - private String providerName; + private String provider; + + /** + * The type of dataset to upload + */ + private ML_DATA_TYPE dataType; + + private DimLevel dimLevel; + + private ArrayList uids; /** * The dataset to upload. @@ -22,12 +36,36 @@ public class DatastoreRequest { */ private HashMap> dataset; - public String getProviderName() { - return providerName; + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public ML_DATA_TYPE getDataType() { + return dataType; + } + + public void setDataType(ML_DATA_TYPE dataType) { + this.dataType = dataType; + } + + public DimLevel getDimLevel() { + return dimLevel; + } + + public void setDimLevel(DimLevel dimLevel) { + this.dimLevel = dimLevel; } - public void setProviderName(String providerName) { - this.providerName = providerName; + public ArrayList getUids() { + return uids; + } + + public void setUids(ArrayList uids) { + this.uids = uids; } public HashMap> getDataset() { @@ -37,4 +75,19 @@ public HashMap> getDataset() { public void setDataset(HashMap> dataset) { this.dataset = dataset; } + + public boolean validate(){ + if(provider == null || provider.isEmpty()) + return false; + + switch (dataType){ + case DICOM: + if(this.dimLevel == null || this.uids == null || this.uids.isEmpty()) + return false; + case IMAGE: + return this.dataset == null; + default: + return false; + } + } } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java deleted file mode 100644 index 3e458b78a..000000000 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatasetTask.java +++ /dev/null @@ -1,68 +0,0 @@ -package pt.ua.dicoogle.core.mlprovider; - -import pt.ua.dicoogle.plugins.PluginController; -import pt.ua.dicoogle.sdk.datastructs.SearchResult; -import pt.ua.dicoogle.sdk.mlprovider.MLDataset; -import pt.ua.dicoogle.sdk.mlprovider.MLImageDataset; -import pt.ua.dicoogle.server.web.dicom.ROIExtractor; -import pt.ua.dicoogle.server.web.utils.cache.WSICache; - -import java.util.HashMap; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; - -public class PrepareDatasetTask implements Callable { - - private final DatastoreRequest request; - private final PluginController controller; - private String dataset; - - private final ROIExtractor roiExtractor; - - private final WSICache wsiCache; - - public PrepareDatasetTask(PluginController controller, DatastoreRequest request) { - this.controller = controller; - this.request = request; - this.dataset = UUID.randomUUID().toString(); - - this.wsiCache = WSICache.getInstance(); - this.roiExtractor = new ROIExtractor(); - } - - @Override - public MLDataset call() throws Exception { - - HashMap extraFields = new HashMap(); - - extraFields.put("SOPInstanceUID", "SOPInstanceUID"); - extraFields.put("SharedFunctionalGroupsSequence_PixelMeasuresSequence_PixelSpacing", "SharedFunctionalGroupsSequence_PixelMeasuresSequence_PixelSpacing"); - extraFields.put("Rows", "Rows"); - extraFields.put("Columns", "Columns"); - extraFields.put("NumberOfFrames", "NumberOfFrames"); - extraFields.put("TotalPixelMatrixColumns", "TotalPixelMatrixColumns"); - extraFields.put("TotalPixelMatrixRows", "TotalPixelMatrixRows"); - extraFields.put("ImageType", "ImageType"); - - MLImageDataset mlDataset = new MLImageDataset(); - - this.request.getDataset().entrySet().forEach((entry -> { - try { - - // Query this image using the first available query provider - Iterable results = controller - .query(controller.getQueryProvidersName(true).get(0), "SOPInstanceUID:" + entry.getKey(), extraFields).get(); - - for (SearchResult image : results) { - //List rois = (List) roiExtractor.extractROI(); - } - - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); // Ignore - } - })); - - return mlDataset; - } -} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java new file mode 100644 index 000000000..6ec863570 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java @@ -0,0 +1,82 @@ +package pt.ua.dicoogle.core.mlprovider; + +import pt.ua.dicoogle.plugins.PluginController; +import pt.ua.dicoogle.sdk.datastructs.SearchResult; +import pt.ua.dicoogle.sdk.mlprovider.MLDataset; +import pt.ua.dicoogle.sdk.mlprovider.MLDicomDataset; +import pt.ua.dicoogle.sdk.mlprovider.MLImageDataset; +import pt.ua.dicoogle.server.web.dicom.ROIExtractor; +import pt.ua.dicoogle.server.web.utils.cache.WSICache; + +import java.util.HashMap; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +/** + * This task processes Datastore Requests, and builds the appropriate ML Dataset. + */ +public class PrepareDatastoreTask implements Callable { + + private final DatastoreRequest request; + private final PluginController controller; + private String dataset; + + private final ROIExtractor roiExtractor; + + private final WSICache wsiCache; + + public PrepareDatastoreTask(PluginController controller, DatastoreRequest request) { + this.controller = controller; + this.request = request; + this.dataset = UUID.randomUUID().toString(); + + this.wsiCache = WSICache.getInstance(); + this.roiExtractor = new ROIExtractor(); + } + + @Override + public MLDataset call() throws Exception { + + if(!request.validate()) + return null; + + switch (request.getDataType()){ + case DICOM: + return new MLDicomDataset(request.getDimLevel(), request.getUids()); + case IMAGE: + HashMap extraFields = new HashMap(); + + extraFields.put("SOPInstanceUID", "SOPInstanceUID"); + extraFields.put("SharedFunctionalGroupsSequence_PixelMeasuresSequence_PixelSpacing", "SharedFunctionalGroupsSequence_PixelMeasuresSequence_PixelSpacing"); + extraFields.put("Rows", "Rows"); + extraFields.put("Columns", "Columns"); + extraFields.put("NumberOfFrames", "NumberOfFrames"); + extraFields.put("TotalPixelMatrixColumns", "TotalPixelMatrixColumns"); + extraFields.put("TotalPixelMatrixRows", "TotalPixelMatrixRows"); + extraFields.put("ImageType", "ImageType"); + + MLImageDataset mlDataset = new MLImageDataset(); + + this.request.getDataset().entrySet().forEach((entry -> { + try { + + // Query this image using the first available query provider + Iterable results = controller + .query(controller.getQueryProvidersName(true).get(0), "SOPInstanceUID:" + entry.getKey(), extraFields).get(); + + for (SearchResult image : results) { + //List rois = (List) roiExtractor.extractROI(); + } + + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); // Ignore + } + })); + + return mlDataset; + default: + return null; + } + } +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java new file mode 100644 index 000000000..170b95e2e --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java @@ -0,0 +1,27 @@ +package pt.ua.dicoogle.core.mlprovider; + +/** + * Object to map train requests. + * A train request intends to send a train or re-train request to a model at a provider. + */ +public class TrainRequest { + + private String provider; + private String modelID; + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public String getModelID() { + return modelID; + } + + public void setModelID(String modelID) { + this.modelID = modelID; + } +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java index 2ee6c199d..a690f50b5 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java @@ -24,7 +24,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pt.ua.dicoogle.core.mlprovider.DatastoreRequest; -import pt.ua.dicoogle.core.mlprovider.PrepareDatasetTask; +import pt.ua.dicoogle.core.mlprovider.PrepareDatastoreTask; import pt.ua.dicoogle.core.settings.ServerSettingsManager; import pt.ua.dicoogle.plugins.webui.WebUIPlugin; import pt.ua.dicoogle.plugins.webui.WebUIPluginManager; @@ -891,12 +891,12 @@ public Task makePredictionOverImage(final String provider, final M return result; } - public Task prepareMLDataset(final DatastoreRequest datasetRequest) { + public Task datastore(final DatastoreRequest datasetRequest) { String uuid = UUID.randomUUID().toString(); Task prepareTask = - new Task<>("MLPrepareDatasetTask" + uuid, new PrepareDatasetTask(this, datasetRequest)); + new Task<>("MLPrepareDatastoreTask" + uuid, new PrepareDatastoreTask(this, datasetRequest)); prepareTask.onCompletion(() -> { - MLProviderInterface mlInterface = getMachineLearningProviderByName(datasetRequest.getProviderName(), true); + MLProviderInterface mlInterface = getMachineLearningProviderByName(datasetRequest.getProvider(), true); if (mlInterface == null) { logger.error("MLProvider with name {} not found", prepareTask.getName()); } else { diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index 227e397c5..68be4eeef 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java @@ -139,10 +139,6 @@ public DicoogleWeb(int port) throws Exception { // setup the ROI extractor final ServletContextHandler roiExtractor = createServletHandler(new ROIServlet(), "/roi"); - // setup the ml endpoints - final ServletContextHandler makePrediction = createServletHandler(new MakePredictionServlet(), "/makePrediction"); - final ServletContextHandler listAllModels = createServletHandler(new ListAllModelsServlet(), "/listAllModels"); - // setup the DICOM to PNG image servlet final ServletContextHandler dictags = createServletHandler(new TagsServlet(), "/dictags"); @@ -181,7 +177,7 @@ public DicoogleWeb(int port) throws Exception { PluginRestletApplication.attachRestPlugin(new VersionResource()); // list the all the handlers mounted above - Handler[] handlers = new Handler[] {pluginHandler, legacyHandler, dic2png, roiExtractor, makePrediction, dictags, listAllModels, + Handler[] handlers = new Handler[] {pluginHandler, legacyHandler, dic2png, roiExtractor, dictags, createServletHandler(new IndexerServlet(), "/indexer"), // DEPRECATED createServletHandler(new SettingsServlet(), "/settings"), csvServletHolder, createServletHandler(new LoginServlet(), "/login"), @@ -226,8 +222,9 @@ public DicoogleWeb(int port) throws Exception { createServletHandler(new ServerStorageServlet(), "/management/settings/storage/dicom"), // ml provider servlets - createServletHandler(new DatastoreServlet(), "/ml/createDataset"), + createServletHandler(new DatastoreServlet(), "/ml/datastore"), createServletHandler(new MakePredictionServlet(), "/ml/makePrediction"), + createServletHandler(new TrainServlet(), "/ml/trainModel"), createServletHandler(new ListAllModelsServlet(), "/ml/listAllModels"), createServletHandler(new MakeBulkPredictionServlet(), "/ml/makeBulkPrediction"), webpages}; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java index 26b0542f1..b90b204a7 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java @@ -1,5 +1,6 @@ package pt.ua.dicoogle.server.web.servlets.mlprovider; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; @@ -27,6 +28,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S } ObjectMapper mapper = new ObjectMapper(); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); DatastoreRequest datasetRequest; try { datasetRequest = mapper.readValue(dataString, DatastoreRequest.class); @@ -34,7 +36,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S if(PluginController.getInstance().getMachineLearningProviderByName(datasetRequest.getProviderName(), true) == null){ resp.sendError(404, "The requested provider does not exist"); }*/ - PluginController.getInstance().prepareMLDataset(datasetRequest); + PluginController.getInstance().datastore(datasetRequest); } catch (Exception e) { log.error("Error parsing json string", e); resp.sendError(404, "Malformed request"); diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java new file mode 100644 index 000000000..ad80c8063 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java @@ -0,0 +1,47 @@ +package pt.ua.dicoogle.server.web.servlets.mlprovider; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pt.ua.dicoogle.core.mlprovider.TrainRequest; +import pt.ua.dicoogle.plugins.PluginController; +import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class TrainServlet extends HttpServlet { + + private static final Logger log = LoggerFactory.getLogger(TrainServlet.class); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String dataString = IOUtils.toString(req.getReader()); + if (dataString == null) { + resp.sendError(404, "Empty POST body"); + return; + } + + ObjectMapper mapper = new ObjectMapper(); + TrainRequest trainRequest; + try { + trainRequest = mapper.readValue(dataString, TrainRequest.class); + MLProviderInterface mlplugin = PluginController.getInstance().getMachineLearningProviderByName(trainRequest.getProvider(), true); + if(mlplugin == null){ + log.error("A provider with the provided name does not exist"); + resp.sendError(404, "Malformed request"); + } else { + mlplugin.trainModel(trainRequest.getModelID()); + } + } catch (Exception e) { + log.error("Error parsing json string", e); + resp.sendError(404, "Malformed request"); + } + } + +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java index ded1768f1..47b1494ef 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java @@ -23,6 +23,8 @@ public MLModel(String name, String id) { this.id = id; this.type = ""; labels = new TreeSet<>(); + dataType = ML_DATA_TYPE.IMAGE; + creationDate = new Date(); } public String getName() { diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java index 0fc12fbd5..3b15f10ad 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java @@ -1,5 +1,5 @@ package pt.ua.dicoogle.sdk.mlprovider; public enum ML_DATA_TYPE { - CSV, IMAGE, DICOm + CSV, IMAGE, DICOM } From 14d167c727d37561489675c3eb687eeb86000da7 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Wed, 14 Jun 2023 18:05:28 +0100 Subject: [PATCH 32/53] New endpoints implemented. Code improvements. --- .../ua/dicoogle/server/web/DicoogleWeb.java | 11 ++- .../servlets/mlprovider/ModelinfoServlet.java | 50 +++++++++++ .../web/servlets/mlprovider/TrainServlet.java | 51 ++++++++++- .../sdk/mlprovider/MLModelTrainInfo.java | 85 +++++++++++++++++++ .../sdk/mlprovider/MLProviderInterface.java | 35 +++++--- .../dicoogle/sdk/mlprovider/MLTrainTask.java | 42 +++++++++ 6 files changed, 258 insertions(+), 16 deletions(-) create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelTrainInfo.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index 68be4eeef..f29be9141 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java @@ -223,10 +223,13 @@ public DicoogleWeb(int port) throws Exception { // ml provider servlets createServletHandler(new DatastoreServlet(), "/ml/datastore"), - createServletHandler(new MakePredictionServlet(), "/ml/makePrediction"), - createServletHandler(new TrainServlet(), "/ml/trainModel"), - createServletHandler(new ListAllModelsServlet(), "/ml/listAllModels"), - createServletHandler(new MakeBulkPredictionServlet(), "/ml/makeBulkPrediction"), webpages}; + createServletHandler(new MakePredictionServlet(), "/ml/infer/single"), + createServletHandler(new TrainServlet(), "/ml/train"), + createServletHandler(new ListAllModelsServlet(), "/ml/model/list"), + createServletHandler(new ModelinfoServlet(), "/ml/model/info"), + createServletHandler(new ListAllModelsServlet(), "/ml/info"), + createServletHandler(new MakeBulkPredictionServlet(), "/ml/infer/batch"), + webpages}; // setup the server server = new Server(port); diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java new file mode 100644 index 000000000..26abc3cd6 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java @@ -0,0 +1,50 @@ +package pt.ua.dicoogle.server.web.servlets.mlprovider; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.restlet.data.Status; +import pt.ua.dicoogle.plugins.PluginController; +import pt.ua.dicoogle.sdk.mlprovider.MLModelTrainInfo; +import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +public class ModelinfoServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + ObjectMapper mapper = new ObjectMapper(); + + String provider = request.getParameter("provider"); + String modelID = request.getParameter("modelID"); + + if(provider == null || provider.isEmpty()){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Provider was not specified"); + return; + } + + if(modelID == null || modelID.isEmpty()){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Model was not specified"); + return; + } + + MLProviderInterface mlPlugin = PluginController.getInstance().getMachineLearningProviderByName(provider, true); + if(mlPlugin == null){ + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Provider not found"); + return; + } + + MLModelTrainInfo info = mlPlugin.modelInfo(modelID); + + response.setContentType("application/json"); + PrintWriter out = response.getWriter(); + mapper.writeValue(out, info); + out.close(); + out.flush(); + } + +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java index ad80c8063..c64f72c79 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java @@ -7,12 +7,14 @@ import pt.ua.dicoogle.core.mlprovider.TrainRequest; import pt.ua.dicoogle.plugins.PluginController; import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; +import pt.ua.dicoogle.sdk.mlprovider.MLTrainTask; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.PrintWriter; public class TrainServlet extends HttpServlet { @@ -36,7 +38,54 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S log.error("A provider with the provided name does not exist"); resp.sendError(404, "Malformed request"); } else { - mlplugin.trainModel(trainRequest.getModelID()); + MLTrainTask trainTask = mlplugin.trainModel(trainRequest.getModelID()); + switch (trainTask.getStatus()){ + case BUSY: + log.error("Could not create training task, service is busy"); + resp.sendError(405, "Could not create training task, service is busy"); + break; + case REJECTED: + log.error("Could not create training task, request is malformed"); + resp.sendError(404, "Could not create training task, service is busy"); + break; + default: + resp.setContentType("application/json"); + PrintWriter out = resp.getWriter(); + mapper.writeValue(out, trainTask); + out.close(); + out.flush(); + } + } + } catch (Exception e) { + log.error("Error parsing json string", e); + resp.sendError(404, "Malformed request"); + } + } + + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String dataString = IOUtils.toString(req.getReader()); + if (dataString == null) { + resp.sendError(404, "Empty POST body"); + return; + } + + ObjectMapper mapper = new ObjectMapper(); + TrainRequest trainRequest; + try { + trainRequest = mapper.readValue(dataString, TrainRequest.class); + MLProviderInterface mlplugin = PluginController.getInstance().getMachineLearningProviderByName(trainRequest.getProvider(), true); + if(mlplugin == null){ + log.error("A provider with the provided name does not exist"); + resp.sendError(404, "Malformed request"); + } else { + boolean stopped = mlplugin.stopTraining(""); + + if(!stopped){ + log.error("Could not stop training task"); + resp.sendError(404, "Could not stop training task"); + } } } catch (Exception e) { log.error("Error parsing json string", e); diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelTrainInfo.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelTrainInfo.java new file mode 100644 index 000000000..67a0649ca --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelTrainInfo.java @@ -0,0 +1,85 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +import java.util.Date; +import java.util.Map; + +/** + * This object stores model information + */ +public class MLModelTrainInfo { + + private Date modifiedTime; + + private Date startTime; + + private int currentEpoch; + + private int totalEpochs; + + private double bestMetric; + + private String bestMetricKey; + + private Map metrics; + + public MLModelTrainInfo(Date modifiedTime, Date startTime) { + this.modifiedTime = modifiedTime; + this.startTime = startTime; + } + + public Date getModifiedTime() { + return modifiedTime; + } + + public void setModifiedTime(Date modifiedTime) { + this.modifiedTime = modifiedTime; + } + + public Date getStartTime() { + return startTime; + } + + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + public int getCurrentEpoch() { + return currentEpoch; + } + + public void setCurrentEpoch(int currentEpoch) { + this.currentEpoch = currentEpoch; + } + + public int getTotalEpochs() { + return totalEpochs; + } + + public void setTotalEpochs(int totalEpochs) { + this.totalEpochs = totalEpochs; + } + + public double getBestMetric() { + return bestMetric; + } + + public void setBestMetric(double bestMetric) { + this.bestMetric = bestMetric; + } + + public String getBestMetricKey() { + return bestMetricKey; + } + + public void setBestMetricKey(String bestMetricKey) { + this.bestMetricKey = bestMetricKey; + } + + public Map getMetrics() { + return metrics; + } + + public void setMetrics(Map metrics) { + this.metrics = metrics; + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java index 8b29edb22..17e3b9963 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java @@ -34,11 +34,34 @@ public abstract class MLProviderInterface implements DicooglePlugin { */ public abstract MLModel createModel(); + + /** + * This method deploys a model + */ + public abstract void deployModel(); + + /** + * This method lists the models created on this provider. + */ + public abstract List listModels(); + + /** + * This method lists the models created on this provider. + * @param modelID model identifier + */ + public abstract MLModelTrainInfo modelInfo(String modelID); + /** * This method orders the training of a model. * @param modelID the unique model identifier within the provider. */ - public abstract boolean trainModel(String modelID); + public abstract MLTrainTask trainModel(String modelID); + + /** + * This method orders the training of a model. + * @param trainingTaskID the unique training task identifier. + */ + public abstract boolean stopTraining(String trainingTaskID); /** * This method creates a endpoint that exposes a service @@ -55,16 +78,6 @@ public abstract class MLProviderInterface implements DicooglePlugin { */ public abstract void deleteEndpoint(); - /** - * This method deploys a model - */ - public abstract void deployModel(); - - /** - * This method lists the models created on this provider. - */ - public abstract List listModels(); - /** * This method deletes a model */ diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java new file mode 100644 index 000000000..cf804eb96 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java @@ -0,0 +1,42 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +/** + * Object to map training tasks. + * Training tasks have an associated life cycle and unique identifier. + * Additional information such as current epoch and iteration, as well as training details can be mapped in this object too. + */ +public class MLTrainTask { + + public enum TRAINING_STATUS { + SUBMITTED, + RUNNING, + CANCELLED, + BUSY, + REJECTED + } + + public MLTrainTask(String taskID, TRAINING_STATUS status) { + this.taskID = taskID; + this.status = status; + } + + private String taskID; + + private TRAINING_STATUS status; + + public String getTaskID() { + return taskID; + } + + public void setTaskID(String taskID) { + this.taskID = taskID; + } + + public TRAINING_STATUS getStatus() { + return status; + } + + public void setStatus(TRAINING_STATUS status) { + this.status = status; + } +} From 90b2631c7baf32bfb43c20fcc98919fe5bdeba02 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Thu, 15 Jun 2023 15:53:22 +0100 Subject: [PATCH 33/53] Re-organization of code to remove dcm4che3 dependency from sdk. --- .../java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java | 1 - .../pt/ua/dicoogle/server/web/dicom}/WSISopDescriptor.java | 2 +- .../web/servlets/mlprovider/MakePredictionServlet.java | 2 +- sdk/pom.xml | 6 ------ 4 files changed, 2 insertions(+), 9 deletions(-) rename {sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi => dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom}/WSISopDescriptor.java (98%) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java index d98706a6f..78d1fdafe 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -8,7 +8,6 @@ import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; import pt.ua.dicoogle.sdk.datastructs.wsi.WSIFrame; -import pt.ua.dicoogle.sdk.datastructs.wsi.WSISopDescriptor; import pt.ua.dicoogle.server.web.utils.cache.WSICache; import javax.imageio.ImageIO; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSISopDescriptor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/WSISopDescriptor.java similarity index 98% rename from sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSISopDescriptor.java rename to dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/WSISopDescriptor.java index c39464551..a29f5abbc 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSISopDescriptor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/WSISopDescriptor.java @@ -1,4 +1,4 @@ -package pt.ua.dicoogle.sdk.datastructs.wsi; +package pt.ua.dicoogle.server.web.dicom; import org.dcm4che2.data.Tag; import org.dcm4che3.data.Attributes; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index 3497fdb4a..d1dbe9bf6 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -12,7 +12,7 @@ import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; -import pt.ua.dicoogle.sdk.datastructs.wsi.WSISopDescriptor; +import pt.ua.dicoogle.server.web.dicom.WSISopDescriptor; import pt.ua.dicoogle.sdk.mlprovider.MLPrediction; import pt.ua.dicoogle.sdk.mlprovider.MLPredictionRequest; import pt.ua.dicoogle.sdk.task.Task; diff --git a/sdk/pom.xml b/sdk/pom.xml index d89b23cf9..b234fb6f2 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -242,12 +242,6 @@ slf4j-api ${slf4j.version} - - org.dcm4che - dcm4che-core - 3.3.7 - compile - From 2f9c9291e1a3933dfd933b6b8a740dd6aac423df Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Tue, 20 Jun 2023 11:21:14 +0100 Subject: [PATCH 34/53] Removed unecessary interfaces from MLProvider. Infer endpoint can now return DICOM Seg files. --- .../ua/dicoogle/plugins/PluginController.java | 8 +- .../mlprovider/MakePredictionServlet.java | 81 ++++++++++++++----- .../dicoogle/sdk/mlprovider/MLEndpoint.java | 7 -- .../{MLPrediction.java => MLInference.java} | 29 ++++++- ...onRequest.java => MLInferenceRequest.java} | 4 +- .../sdk/mlprovider/MLProviderInterface.java | 29 +------ 6 files changed, 99 insertions(+), 59 deletions(-) delete mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java rename sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/{MLPrediction.java => MLInference.java} (56%) rename sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/{MLPredictionRequest.java => MLInferenceRequest.java} (89%) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java index a690f50b5..688f110a2 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java @@ -34,8 +34,8 @@ import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; import pt.ua.dicoogle.sdk.mlprovider.MLDataset; -import pt.ua.dicoogle.sdk.mlprovider.MLPrediction; -import pt.ua.dicoogle.sdk.mlprovider.MLPredictionRequest; +import pt.ua.dicoogle.sdk.mlprovider.MLInference; +import pt.ua.dicoogle.sdk.mlprovider.MLInferenceRequest; import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; import pt.ua.dicoogle.sdk.task.JointQueryTask; @@ -880,13 +880,13 @@ public List indexBlocking(URI path) { * @param predictionRequest * @return the created task */ - public Task makePredictionOverImage(final String provider, final MLPredictionRequest predictionRequest) { + public Task infer(final String provider, final MLInferenceRequest predictionRequest) { MLProviderInterface providerInterface = this.getMachineLearningProviderByName(provider, true); if(providerInterface == null) return null; String taskName = "MLPredictionTask" + UUID.randomUUID(); - Task result = providerInterface.makePrediction(predictionRequest); + Task result = providerInterface.infer(predictionRequest); result.setName(taskName); return result; } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index d1dbe9bf6..177e1d3da 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -3,7 +3,9 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; import org.restlet.data.Status; import org.slf4j.Logger; @@ -13,20 +15,23 @@ import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; import pt.ua.dicoogle.server.web.dicom.WSISopDescriptor; -import pt.ua.dicoogle.sdk.mlprovider.MLPrediction; -import pt.ua.dicoogle.sdk.mlprovider.MLPredictionRequest; +import pt.ua.dicoogle.sdk.mlprovider.MLInference; +import pt.ua.dicoogle.sdk.mlprovider.MLInferenceRequest; import pt.ua.dicoogle.sdk.task.Task; import pt.ua.dicoogle.server.web.dicom.ROIExtractor; import pt.ua.dicoogle.server.web.utils.cache.WSICache; import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; +import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.List; +import java.util.UUID; import java.util.concurrent.ExecutionException; public class MakePredictionServlet extends HttpServlet { @@ -78,11 +83,11 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) String provider = body.get("provider").asText(); String modelID = body.get("modelID").asText(); - boolean wsi = body.get("wsi").asBoolean(); + boolean wsi = body.has("wsi") && body.get("wsi").asBoolean(); DimLevel level = DimLevel.valueOf(body.get("level").asText().toUpperCase()); String dimUID = body.get("uid").asText(); - Task task; + Task task; if(wsi){ @@ -114,11 +119,11 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) task.run(); } - private Task sendWSIRequest(String provider, String modelID, String baseSopInstanceUID, String uid, - BulkAnnotation.AnnotationType roiType, List roi, HttpServletResponse response){ + private Task sendWSIRequest(String provider, String modelID, String baseSopInstanceUID, String uid, + BulkAnnotation.AnnotationType roiType, List roi, HttpServletResponse response){ ObjectMapper mapper = new ObjectMapper(); - MLPredictionRequest predictionRequest = new MLPredictionRequest(true, DimLevel.INSTANCE, uid, modelID); + MLInferenceRequest predictionRequest = new MLInferenceRequest(true, DimLevel.INSTANCE, uid, modelID); BulkAnnotation annotation = new BulkAnnotation(); annotation.setPoints(roi); annotation.setAnnotationType(roiType); @@ -127,11 +132,11 @@ private Task sendWSIRequest(String provider, String modelID, Strin DicomMetaData dicomMetaData = this.getDicomMetadata(uid); BufferedImage bi = roiExtractor.extractROI(dicomMetaData, annotation); predictionRequest.setRoi(bi); - Task task = PluginController.getInstance().makePredictionOverImage(provider, predictionRequest); + Task task = PluginController.getInstance().infer(provider, predictionRequest); if(task != null){ task.onCompletion(() -> { try { - MLPrediction prediction = task.get(); + MLInference prediction = task.get(); if(prediction == null){ log.error("Provider returned null prediction"); @@ -174,14 +179,14 @@ private Task sendWSIRequest(String provider, String modelID, Strin } } - private Task sendRequest(String provider, String modelID, DimLevel level, String dimUID, HttpServletResponse response){ + private Task sendRequest(String provider, String modelID, DimLevel level, String dimUID, HttpServletResponse response){ ObjectMapper mapper = new ObjectMapper(); - MLPredictionRequest predictionRequest = new MLPredictionRequest(true, level, dimUID, modelID); - Task task = PluginController.getInstance().makePredictionOverImage(provider, predictionRequest); + MLInferenceRequest predictionRequest = new MLInferenceRequest(false, level, dimUID, modelID); + Task task = PluginController.getInstance().infer(provider, predictionRequest); if(task != null){ task.onCompletion(() -> { try { - MLPrediction prediction = task.get(); + MLInference prediction = task.get(); if(prediction == null){ log.error("Provider returned null prediction"); @@ -189,11 +194,49 @@ private Task sendRequest(String provider, String modelID, DimLevel return; } - response.setContentType("application/json"); - PrintWriter out = response.getWriter(); - mapper.writeValue(out, prediction); - out.close(); - out.flush(); + if(prediction.getDicomSEG() != null){ + // We have a file to send, got to build a multi part response + String boundary = UUID.randomUUID().toString(); + response.setContentType("multipart/form-data; boundary=" + boundary); + + ServletOutputStream out = response.getOutputStream(); + + out.print("--" + boundary); + out.println(); + out.print("Content-Disposition: form-data; name=\"params\""); + out.println(); + out.print("Content-Type: application/json"); + out.println(); out.println(); + out.print(mapper.writeValueAsString(prediction)); + out.println(); + out.print("--" + boundary); + out.println(); + out.print("Content-Disposition: form-data; name=\"dicomseg\"; filename=\"dicomseg.dcm\""); + out.println(); + out.print("Content-Type: application/octet-stream"); + out.println(); out.println(); + + byte[] targetArray = new byte[prediction.getDicomSEG().available()]; + prediction.getDicomSEG().read(targetArray); + out.write(targetArray); + out.flush(); + out.close(); + } else { + response.setContentType("application/json"); + PrintWriter out = response.getWriter(); + mapper.writeValue(out, prediction); + out.close(); + out.flush(); + } + + try{ + if(!StringUtils.isBlank(prediction.getResourcesFolder())){ + FileUtils.deleteDirectory(new File(prediction.getResourcesFolder())); + } + } catch (IOException e){ + log.warn("Could not delete temporary file", e); + } + } catch (InterruptedException | ExecutionException e) { log.error("Could not make prediction", e); try { @@ -222,7 +265,7 @@ private DicomMetaData getDicomMetadata(String sop) throws IOException{ * @param scale to transform coordinates * @return the ml prediction with the converted coordinates. */ - private void convertCoordinates(MLPrediction prediction, Point2D tl, double scale){ + private void convertCoordinates(MLInference prediction, Point2D tl, double scale){ for(BulkAnnotation ann : prediction.getAnnotations()){ for(Point2D p : ann.getPoints()){ p.setX((p.getX() + tl.getX())/scale); diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java deleted file mode 100644 index 18de95b2a..000000000 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLEndpoint.java +++ /dev/null @@ -1,7 +0,0 @@ -package pt.ua.dicoogle.sdk.mlprovider; - -public class MLEndpoint { - - - -} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java similarity index 56% rename from sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java rename to sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java index e770305e8..5af1b5483 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPrediction.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java @@ -1,5 +1,7 @@ package pt.ua.dicoogle.sdk.mlprovider; +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.dcm4che2.io.DicomInputStream; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; import java.util.HashMap; @@ -7,15 +9,22 @@ /** * This object maps predictions done by the AI algorithms. - * It can contain a set of metrics and a list of annotations. + * It can contain a set of metrics, annotations and a DICOM SEG file. */ -public class MLPrediction { +public class MLInference { private HashMap metrics; + private String version; private List annotations; + @JsonIgnore + private String resourcesFolder; + + @JsonIgnore + private DicomInputStream dicomSEG; + public HashMap getMetrics() { return metrics; } @@ -32,6 +41,22 @@ public void setAnnotations(List annotations) { this.annotations = annotations; } + public String getResourcesFolder() { + return resourcesFolder; + } + + public void setResourcesFolder(String resourcesFolder) { + this.resourcesFolder = resourcesFolder; + } + + public DicomInputStream getDicomSEG() { + return dicomSEG; + } + + public void setDicomSEG(DicomInputStream dicomSEG) { + this.dicomSEG = dicomSEG; + } + public String getVersion() { return version; } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPredictionRequest.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInferenceRequest.java similarity index 89% rename from sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPredictionRequest.java rename to sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInferenceRequest.java index 2c9f7849f..d7c9bbaba 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLPredictionRequest.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInferenceRequest.java @@ -4,7 +4,7 @@ import java.awt.image.BufferedImage; -public class MLPredictionRequest { +public class MLInferenceRequest { private boolean isWsi; @@ -16,7 +16,7 @@ public class MLPredictionRequest { private String modelID; - public MLPredictionRequest(boolean isWsi, DimLevel level, String dimID, String modelID) { + public MLInferenceRequest(boolean isWsi, DimLevel level, String dimID, String modelID) { this.isWsi = isWsi; this.level = level; this.dimID = dimID; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java index 17e3b9963..f7c5e8566 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java @@ -34,12 +34,6 @@ public abstract class MLProviderInterface implements DicooglePlugin { */ public abstract MLModel createModel(); - - /** - * This method deploys a model - */ - public abstract void deployModel(); - /** * This method lists the models created on this provider. */ @@ -63,21 +57,6 @@ public abstract class MLProviderInterface implements DicooglePlugin { */ public abstract boolean stopTraining(String trainingTaskID); - /** - * This method creates a endpoint that exposes a service - */ - public abstract void createEndpoint(); - - /** - * This method lists all available endpoints - */ - public abstract List listEndpoints(); - - /** - * This method deletes a endpoint - */ - public abstract void deleteEndpoint(); - /** * This method deletes a model */ @@ -87,14 +66,14 @@ public abstract class MLProviderInterface implements DicooglePlugin { * Order a prediction over a single object. * The object can be a series instance, a sop instance or a 2D/3D ROI. * - * @param predictionRequest object that defines this prediction request + * @param inferRequest object that defines this inference request */ - public abstract Task makePrediction(MLPredictionRequest predictionRequest); + public abstract Task infer(MLInferenceRequest inferRequest); /** - * This method makes a bulk prediction using the selected model + * This method makes a bulk inference request using the selected model */ - public abstract void makeBulkPrediction(); + public abstract void batchInfer(); public Set getAcceptedDataTypes() { return acceptedDataTypes; From a8671dc78898fa4678da1cc9b376ce3e1e70b0df Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Wed, 28 Jun 2023 16:58:07 +0100 Subject: [PATCH 35/53] Changed type of DICOM SEG. Improved make prediction servlet --- .../mlprovider/MakePredictionServlet.java | 27 +++++++++++++------ .../dicoogle/sdk/mlprovider/MLInference.java | 11 +++++--- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java index 177e1d3da..7f5c3c48a 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java @@ -27,9 +27,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.io.PrintWriter; +import java.io.*; +import java.nio.file.Files; import java.util.List; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -194,7 +193,14 @@ private Task sendRequest(String provider, String modelID, DimLevel return; } - if(prediction.getDicomSEG() != null){ + if((prediction.getDicomSEG() != null) && !prediction.hasResults()){ + response.setContentType("application/dicom"); + ServletOutputStream out = response.getOutputStream(); + try (InputStream fi = Files.newInputStream(prediction.getDicomSEG())) { + IOUtils.copy(fi, out); + out.flush(); + } + } else if((prediction.getDicomSEG() != null) && prediction.hasResults()){ // We have a file to send, got to build a multi part response String boundary = UUID.randomUUID().toString(); response.setContentType("multipart/form-data; boundary=" + boundary); @@ -213,14 +219,19 @@ private Task sendRequest(String provider, String modelID, DimLevel out.println(); out.print("Content-Disposition: form-data; name=\"dicomseg\"; filename=\"dicomseg.dcm\""); out.println(); - out.print("Content-Type: application/octet-stream"); + out.print("Content-Type: application/dicom"); out.println(); out.println(); - byte[] targetArray = new byte[prediction.getDicomSEG().available()]; - prediction.getDicomSEG().read(targetArray); - out.write(targetArray); + try (InputStream fi = Files.newInputStream(prediction.getDicomSEG())) { + IOUtils.copy(fi, out); + out.flush(); + } + + out.println(); + out.print("--" + boundary + "--"); out.flush(); out.close(); + } else { response.setContentType("application/json"); PrintWriter out = response.getWriter(); diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java index 5af1b5483..d00508b43 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java @@ -4,6 +4,7 @@ import org.dcm4che2.io.DicomInputStream; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; +import java.nio.file.Path; import java.util.HashMap; import java.util.List; @@ -23,7 +24,7 @@ public class MLInference { private String resourcesFolder; @JsonIgnore - private DicomInputStream dicomSEG; + private Path dicomSEG; public HashMap getMetrics() { return metrics; @@ -49,11 +50,11 @@ public void setResourcesFolder(String resourcesFolder) { this.resourcesFolder = resourcesFolder; } - public DicomInputStream getDicomSEG() { + public Path getDicomSEG() { return dicomSEG; } - public void setDicomSEG(DicomInputStream dicomSEG) { + public void setDicomSEG(Path dicomSEG) { this.dicomSEG = dicomSEG; } @@ -64,4 +65,8 @@ public String getVersion() { public void setVersion(String version) { this.version = version; } + + public boolean hasResults(){ + return metrics != null || annotations != null; + } } From 4e300a8267054292e94d4d10655c3eb71ff6656d Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Mon, 10 Jul 2023 17:08:08 +0100 Subject: [PATCH 36/53] Applied code review --- dicoogle/pom.xml | 4 ++-- .../core/mlprovider/DatastoreRequest.java | 15 ++++++++++----- .../core/mlprovider/PrepareDatastoreTask.java | 7 +++++-- .../dicoogle/sdk/utils/DicomImageReaderUtil.java | 11 ----------- 4 files changed, 17 insertions(+), 20 deletions(-) delete mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java diff --git a/dicoogle/pom.xml b/dicoogle/pom.xml index 77c1f9624..e8c0ec939 100644 --- a/dicoogle/pom.xml +++ b/dicoogle/pom.xml @@ -467,12 +467,12 @@ org.dcm4che dcm4che-imageio - 3.3.7 + 3.3.8 org.dcm4che dcm4che-core - 3.3.7 + 3.3.8 diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java index a583833ef..c8d8bd879 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java @@ -5,8 +5,8 @@ import pt.ua.dicoogle.sdk.mlprovider.ML_DATA_TYPE; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Java object to represent datastore requests. @@ -34,7 +34,7 @@ public class DatastoreRequest { * The dataset to upload. * Each key should be a SOPInstanceUID and optionally the value should be a list of annotations. */ - private HashMap> dataset; + private Map> dataset; public String getProvider() { return provider; @@ -68,14 +68,19 @@ public void setUids(ArrayList uids) { this.uids = uids; } - public HashMap> getDataset() { + public Map> getDataset() { return dataset; } - public void setDataset(HashMap> dataset) { + public void setDataset(Map> dataset) { this.dataset = dataset; } + /** + * Check if this request has enough information to be processed. + * For example, if it is of DICOM type, it must specify the dim level and the dim uid. + * @return true if the request can processed, false otherwise. + */ public boolean validate(){ if(provider == null || provider.isEmpty()) return false; @@ -85,7 +90,7 @@ public boolean validate(){ if(this.dimLevel == null || this.uids == null || this.uids.isEmpty()) return false; case IMAGE: - return this.dataset == null; + return this.dataset != null && !this.dataset.isEmpty(); default: return false; } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java index 6ec863570..50b383394 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java @@ -1,5 +1,7 @@ package pt.ua.dicoogle.core.mlprovider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import pt.ua.dicoogle.plugins.PluginController; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.mlprovider.MLDataset; @@ -18,6 +20,7 @@ */ public class PrepareDatastoreTask implements Callable { + private static final Logger logger = LoggerFactory.getLogger(PrepareDatastoreTask.class); private final DatastoreRequest request; private final PluginController controller; private String dataset; @@ -44,7 +47,7 @@ public MLDataset call() throws Exception { switch (request.getDataType()){ case DICOM: return new MLDicomDataset(request.getDimLevel(), request.getUids()); - case IMAGE: + case IMAGE: // Not operational HashMap extraFields = new HashMap(); extraFields.put("SOPInstanceUID", "SOPInstanceUID"); @@ -70,7 +73,7 @@ public MLDataset call() throws Exception { } } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); // Ignore + logger.error("Error preparing datastore task", e); } })); diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java deleted file mode 100644 index 740cadaa0..000000000 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/utils/DicomImageReaderUtil.java +++ /dev/null @@ -1,11 +0,0 @@ -package pt.ua.dicoogle.sdk.utils; - -/** - * A set of utils to read pixel data from Dicom files - * @author Rui Jesus - */ -public class DicomImageReaderUtil { - - - -} From ce01b7cb66686e3f5d60b042ccf02eb16fb8a2b2 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Mon, 17 Jul 2023 17:58:57 +0100 Subject: [PATCH 37/53] New isAvailable method for mlprovider plugins. MLlabel.java extended with DICOM codes. Several code improvements. --- .../ua/dicoogle/plugins/PluginController.java | 2 +- .../mlprovider/ListAllModelsServlet.java | 18 +++-- .../dicoogle/sdk/mlprovider/MLInference.java | 3 +- .../sdk/mlprovider/MLProviderInterface.java | 6 ++ .../ua/dicoogle/sdk/mlprovider/MLlabel.java | 75 ++++++++++++++++++- 5 files changed, 94 insertions(+), 10 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java index 688f110a2..e08bbd41e 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java @@ -226,7 +226,7 @@ private void initializePlugins(Collection plugins) { private void applySettings(PluginSet set, ConfigurationHolder holder) { // provide platform to each plugin interface final Collection> all = Arrays.asList(set.getStoragePlugins(), - set.getIndexPlugins(), set.getQueryPlugins(), set.getJettyPlugins()); + set.getIndexPlugins(), set.getQueryPlugins(), set.getJettyPlugins(), set.getMLPlugins()); for (Collection interfaces : all) { if (interfaces == null) continue; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java index c2e63a842..286616cd0 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java @@ -17,6 +17,8 @@ /** * This servlet lists all models from all providers. * Optionally, if a provider is specified, it will only list models from that provider. + * It checks the availability of each provider before adding it to the response. + * So it is possible that this method returns an empty list, if no provider is available, even though the plugins are installed. */ public class ListAllModelsServlet extends HttpServlet { @@ -29,15 +31,19 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t if(provider != null && !provider.isEmpty()){ MLProviderInterface mlPlugin = PluginController.getInstance().getMachineLearningProviderByName(provider, true); - MLProvider p = new MLProvider(mlPlugin.getName()); - p.setModels(mlPlugin.listModels()); - providersResponse.add(p); - } else { - Iterable providers = PluginController.getInstance().getMLPlugins(true); - providers.forEach((mlPlugin) -> { + if(mlPlugin.isAvailable()){ MLProvider p = new MLProvider(mlPlugin.getName()); p.setModels(mlPlugin.listModels()); providersResponse.add(p); + } + } else { + Iterable providers = PluginController.getInstance().getMLPlugins(true); + providers.forEach((mlPlugin) -> { + if(mlPlugin.isAvailable()){ + MLProvider p = new MLProvider(mlPlugin.getName()); + p.setModels(mlPlugin.listModels()); + providersResponse.add(p); + } }); } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java index d00508b43..461ec27aa 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java @@ -1,7 +1,6 @@ package pt.ua.dicoogle.sdk.mlprovider; import com.fasterxml.jackson.annotation.JsonIgnore; -import org.dcm4che2.io.DicomInputStream; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; import java.nio.file.Path; @@ -9,7 +8,7 @@ import java.util.List; /** - * This object maps predictions done by the AI algorithms. + * This object maps inferences done by the AI algorithms. * It can contain a set of metrics, annotations and a DICOM SEG file. */ public class MLInference { diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java index f7c5e8566..ad8f1b954 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java @@ -75,6 +75,12 @@ public abstract class MLProviderInterface implements DicooglePlugin { */ public abstract void batchInfer(); + /** + * This method indicates if the service is available. + * @return true if the provider is ready to be used, false otherwise. + */ + public abstract boolean isAvailable(); + public Set getAcceptedDataTypes() { return acceptedDataTypes; } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java index 72eacfe01..0ebce7f13 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java @@ -5,15 +5,56 @@ /** * A label object that belongs to a model. * Labels must be unique within a model. + * The label definition proposed here follows the DICOM standard guidelines for segmentation objects. + * @see C.8.20.2 Segmentation Image Module for more information. */ public class MLlabel implements Comparable{ + public enum CodingSchemeDesignator{ + DCM, // DICOM scheme code designator + SRT, // SNOMED scheme code designator + LN // LOINC scheme code designator + } + + /** + * DICOM Segment Label (0062, 0005) is a user defined label. + */ private String label; + + /** + * DICOM Segment Description (0062, 0007) is a user defined description. + */ private String description; + /** + * A hex color string that specifies the color this label should have. + */ + private String color; + + /** + * DICOM Code Value (0008,0100) is an identifier that is unambiguous within the Coding Scheme denoted by Coding Scheme Designator (0008,0102) and Coding Scheme Version (0008,0103). + */ + private String codeValue; + + /** + * DICOM Code Meaning (0008,0104), a human-readable description of the label,
+ * given by the combination of Code Value and Coding Scheme Designator. + */ + private String codeMeaning; + + /** + * DICOM attribute Coding Scheme Designator (0008,0102) defines the coding scheme in which the code for a term is defined. + * Typical values: "DCM" for DICOM defined codes, "SRT" for SNOMED and "LN" for LOINC + */ + private CodingSchemeDesignator codingSchemeDesignator; + public MLlabel(String label) { this.label = label; - this.description = ""; + this.description = "unknown"; + this.codingSchemeDesignator = CodingSchemeDesignator.DCM; + this.codeValue = "333333"; + this.codeMeaning = "unknown"; + this.color = "#000000"; } public String getLabel() { @@ -32,6 +73,38 @@ public void setDescription(String description) { this.description = description; } + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } + + public String getCodeValue() { + return codeValue; + } + + public void setCodeValue(String codeValue) { + this.codeValue = codeValue; + } + + public String getCodeMeaning() { + return codeMeaning; + } + + public void setCodeMeaning(String codeMeaning) { + this.codeMeaning = codeMeaning; + } + + public CodingSchemeDesignator getCodingSchemeDesignator() { + return codingSchemeDesignator; + } + + public void setCodingSchemeDesignator(CodingSchemeDesignator codingSchemeDesignator) { + this.codingSchemeDesignator = codingSchemeDesignator; + } + @Override public boolean equals(Object o) { if (this == o) return true; From eaa34c3d9d0b225820cf85c7708122b0a198fc7b Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Wed, 19 Jul 2023 14:28:57 +0100 Subject: [PATCH 38/53] Code cleanup. More documentation added. --- .../{MakeBulkPredictionServlet.java => BulkInferServlet.java} | 0 .../mlprovider/{MakePredictionServlet.java => InferServlet.java} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/{MakeBulkPredictionServlet.java => BulkInferServlet.java} (100%) rename dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/{MakePredictionServlet.java => InferServlet.java} (100%) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java similarity index 100% rename from dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakeBulkPredictionServlet.java rename to dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java similarity index 100% rename from dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/MakePredictionServlet.java rename to dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java From ad5f9700a04adca7b35ea55c39b8522c3626a92c Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Wed, 19 Jul 2023 14:29:44 +0100 Subject: [PATCH 39/53] Code cleanup. More documentation added. --- .../core/mlprovider/PrepareDatastoreTask.java | 4 +++- .../java/pt/ua/dicoogle/server/web/DicoogleWeb.java | 4 ++-- .../web/servlets/mlprovider/BulkInferServlet.java | 11 ++++++++--- .../web/servlets/mlprovider/DatastoreServlet.java | 4 ---- .../server/web/servlets/mlprovider/InferServlet.java | 6 +++--- .../web/servlets/mlprovider/ModelinfoServlet.java | 5 +++++ .../server/web/servlets/mlprovider/TrainServlet.java | 4 ++++ 7 files changed, 25 insertions(+), 13 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java index 50b383394..e33c09ded 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java @@ -48,6 +48,8 @@ public MLDataset call() throws Exception { case DICOM: return new MLDicomDataset(request.getDimLevel(), request.getUids()); case IMAGE: // Not operational + throw new UnsupportedOperationException("Datastore requests for image objects is not supported"); + /* HashMap extraFields = new HashMap(); extraFields.put("SOPInstanceUID", "SOPInstanceUID"); @@ -76,8 +78,8 @@ public MLDataset call() throws Exception { logger.error("Error preparing datastore task", e); } })); - return mlDataset; + */ default: return null; } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index f29be9141..4348db02d 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java @@ -223,12 +223,12 @@ public DicoogleWeb(int port) throws Exception { // ml provider servlets createServletHandler(new DatastoreServlet(), "/ml/datastore"), - createServletHandler(new MakePredictionServlet(), "/ml/infer/single"), + createServletHandler(new InferServlet(), "/ml/infer/single"), + createServletHandler(new BulkInferServlet(), "/ml/infer/batch"), createServletHandler(new TrainServlet(), "/ml/train"), createServletHandler(new ListAllModelsServlet(), "/ml/model/list"), createServletHandler(new ModelinfoServlet(), "/ml/model/info"), createServletHandler(new ListAllModelsServlet(), "/ml/info"), - createServletHandler(new MakeBulkPredictionServlet(), "/ml/infer/batch"), webpages}; // setup the server diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java index 83d331b78..a33569499 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java @@ -1,5 +1,6 @@ package pt.ua.dicoogle.server.web.servlets.mlprovider; +import org.restlet.data.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -9,10 +10,14 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; -public class MakeBulkPredictionServlet extends HttpServlet { +public class BulkInferServlet extends HttpServlet { - private static final Logger log = LoggerFactory.getLogger(MakeBulkPredictionServlet.class); + private static final Logger log = LoggerFactory.getLogger(BulkInferServlet.class); @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {} + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + resp.sendError(Status.SERVER_ERROR_NOT_IMPLEMENTED.getCode(), "Endpoint not implemented"); + + } } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java index b90b204a7..c45cdf461 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java @@ -32,10 +32,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S DatastoreRequest datasetRequest; try { datasetRequest = mapper.readValue(dataString, DatastoreRequest.class); - /* - if(PluginController.getInstance().getMachineLearningProviderByName(datasetRequest.getProviderName(), true) == null){ - resp.sendError(404, "The requested provider does not exist"); - }*/ PluginController.getInstance().datastore(datasetRequest); } catch (Exception e) { log.error("Error parsing json string", e); diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java index 7f5c3c48a..2cd8fe2fc 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java @@ -33,9 +33,9 @@ import java.util.UUID; import java.util.concurrent.ExecutionException; -public class MakePredictionServlet extends HttpServlet { +public class InferServlet extends HttpServlet { - private static final Logger log = LoggerFactory.getLogger(MakePredictionServlet.class); + private static final Logger log = LoggerFactory.getLogger(InferServlet.class); private final ROIExtractor roiExtractor; @@ -45,7 +45,7 @@ public class MakePredictionServlet extends HttpServlet { * Creates ROI servlet servlet. * */ - public MakePredictionServlet() { + public InferServlet() { this.wsiCache = WSICache.getInstance(); this.roiExtractor = new ROIExtractor(); } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java index 26abc3cd6..1120c930d 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java @@ -13,6 +13,11 @@ import java.io.IOException; import java.io.PrintWriter; +/** + * This servlet returns a detailed list of metrics described in @see pt.ua.dicoogle.sdk.mlprovider.MLModelTrainInfo. + * Usually it should only be available when a model has already been trained at least once. + * Model is identified by the provider and model ID. + */ public class ModelinfoServlet extends HttpServlet { @Override diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java index c64f72c79..4b4d6c1d7 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java @@ -16,6 +16,10 @@ import java.io.IOException; import java.io.PrintWriter; +/** + * This servlet implements the request training and cancel training endpoints. + * To train a model, labels/annotations must first be uploaded to the provider. + */ public class TrainServlet extends HttpServlet { private static final Logger log = LoggerFactory.getLogger(TrainServlet.class); From 22cd7fb94ecd464b5dd086a60eb68722d8a289d0 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Wed, 19 Jul 2023 17:18:05 +0100 Subject: [PATCH 40/53] Updated API docs with new endpoints --- .../ua/dicoogle/server/web/DicoogleWeb.java | 3 +- dicoogle_web_api.yaml | 352 +++++++++++++++++- 2 files changed, 352 insertions(+), 3 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index 4348db02d..b46b81826 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java @@ -224,11 +224,10 @@ public DicoogleWeb(int port) throws Exception { // ml provider servlets createServletHandler(new DatastoreServlet(), "/ml/datastore"), createServletHandler(new InferServlet(), "/ml/infer/single"), - createServletHandler(new BulkInferServlet(), "/ml/infer/batch"), + createServletHandler(new BulkInferServlet(), "/ml/infer/bulk"), createServletHandler(new TrainServlet(), "/ml/train"), createServletHandler(new ListAllModelsServlet(), "/ml/model/list"), createServletHandler(new ModelinfoServlet(), "/ml/model/info"), - createServletHandler(new ListAllModelsServlet(), "/ml/info"), webpages}; // setup the server diff --git a/dicoogle_web_api.yaml b/dicoogle_web_api.yaml index 97c795d9d..e77f24ba5 100644 --- a/dicoogle_web_api.yaml +++ b/dicoogle_web_api.yaml @@ -19,7 +19,7 @@ info:

- Dicoogle Javadoc

- Dicoogle Javascript Client


- version: 3.1.0 + version: 3.3.1 title: Dicoogle contact: name: Support @@ -47,6 +47,8 @@ tags: description: Management related services - name: Misc description: Misc related services + - name: Machine Learning + description: Machine learning services paths: /login: post: @@ -1031,6 +1033,274 @@ paths: $ref: "#/components/schemas/Version" "400": description: Invalid supplied parameters + /roi: + get: + tags: + - Misc + summary: Retrieve a rectangular region of a WSI + operationId: getImageROI + parameters: + - in: query + name: uid + description: SOPInstanceUID + required: true + schema: + type: string + - in: query + name: x + description: left coordinate of the top left corner of the ROI in relation to the SOPInstanceUID provided + required: true + schema: + type: number + - in: query + name: y + description: top coordinate of the top left corner of the ROI in relation to the SOPInstanceUID provided + required: true + schema: + type: number + - in: query + name: width + description: Width of the ROI in pixels + required: true + schema: + type: number + - in: query + name: height + description: Height of the ROI in pixels + required: true + schema: + type: number + responses: + "200": + description: Successful operation + content: + image/jpeg: + schema: + type: string + format: binary + "400": + description: Invalid supplied parameters + "404": + description: SOPInstanceUID provided does not exist + post: + tags: + - Misc + summary: Retrieve an arbitrary region of a WSI + operationId: postImageROI + requestBody: + description: Describes the region to extract + required: true + content: + application/json: + schema: + type: object + properties: + uid: + type: string + description: SOPInstanceUID + type: + type: string + enum: [RECTANGLE, ELLIPSE, POLYGON, POLYLINE, POINT] + points: + type: array + items: + type: object + description: A list of points that defines the shape to extract + properties: + x: + type: number + y: + type: number + responses: + "200": + description: Successful operation + content: + image/jpeg: + schema: + type: string + format: binary + "400": + description: Invalid supplied parameters + "404": + description: SOPInstanceUID provided does not exist + /ml/datastore: + post: + tags: + - Machine Learning + summary: Datastore endpoint for machine learning providers, used to upload images and labels to services + operationId: mlDatastore + requestBody: + description: Describes the data to upload to the provider + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DatastoreRequest' + responses: + '200': + description: The datastore request was successully processed and sent to the provider + '400': + description: Malformed request + /ml/infer/single: + post: + tags: + - Machine Learning + summary: Infer endpoint for a single object, image or DICOM object (series or sop instance). It has split functionality for WSI images. + operationId: mlInferSingle + parameters: + - in: query + name: provider + description: The name of the provider, e.g "MONAI" + required: true + schema: + type: string + - in: query + name: modelID + description: Model identifier that will perform the inference + required: true + schema: + type: string + - in: query + name: wsi + description: Flag to identify WSI requests. Defaults to false. + required: false + schema: + type: boolean + default: false + - in: query + name: level + description: + The DIM level. Required if wsi is false + required: false + schema: + type: string + enum: [PATIENT, STUDY, SERIES, INSTANCE] + - in: query + name: uid + description: The unique identifier of the DIM object. Required if wsi is false + required: false + schema: + type: string + responses: + '200': + description: The datastore request was successully processed and sent to the provider + content: + application/dicom: + schema: + type: string + format: binary + description: A DICOMSeg file + multipart/form-data: + schema: + type: object + properties: + params: + type: object + description: Extra information the inference might return + dicomseg: + type: string + format: binary + description: DICOMSeg file containing the segmentations returned by the inference + application/json: + schema: + $ref: '#/components/schemas/MLInference' + '400': + description: Malformed request + /ml/infer/bulk: + post: + tags: + - Machine Learning + summary: Infer endpoint for a bulk of objects, images or DICOM objects (series or sop instance) + operationId: mlInferBulk + responses: + '501': + description: Not implemented + /ml/train: + post: + tags: + - Machine Learning + summary: Request a training task + operationId: mlTrain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TrainRequest' + responses: + '200': + description: The training request was successfully processed and sent to the provider + '400': + description: Malformed request + delete: + tags: + - Machine Learning + summary: Cancel a training + operationId: mlCancelTrain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TrainRequest' + responses: + '200': + description: The training request was successfully cancelled + '400': + description: Malformed request + /ml/model/list: + get: + tags: + - Machine Learning + summary: Get a list of models from all providers or a specific one + operationId: mlModelList + parameters: + - in: query + name: provider + description: The name of the provider to request the models from. If specified, only models from this provider are returned. + required: false + schema: + type: string + responses: + '200': + description: The list of providers + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MLProvider' + '400': + description: Malformed request + /ml/model/info: + get: + tags: + - Machine Learning + summary: Get training info a model + operationId: mlModelInfo + parameters: + - in: query + name: provider + description: The name of the provider, e.g "MONAI" + required: true + schema: + type: string + - in: query + name: modelID + description: Model identifier + required: true + schema: + type: string + responses: + '200': + description: The training information of the specified model + content: + application/json: + schema: + $ref: '#/components/schemas/MLModelTrainInfo' + '400': + description: Malformed request + components: securitySchemes: dicoogle_auth: @@ -1434,3 +1704,83 @@ components: type: array items: type: string + DatastoreRequest: + type: object + properties: + provider: + type: string + description: Provider name + example: MONAI + dataType: + type: string + enum: [CSV, IMAGE, DICOM] + dimLevel: + type: string + enum: [PATIENT, STUDY, SERIES, INSTANCE] + uids: + type: array + items: + type: string + dataset: + type: object + description: A map where keys represent SOPInstances and the values are annotations to upload + TrainRequest: + type: object + properties: + provider: + type: string + description: Provider name + example: MONAI + modelID: + type: string + description: Model identifier + example: spleen_model + MLInference: + type: object + properties: + metrics: + type: object + description: A key-value dictionary of extra metrics the inference might return + version: + type: string + description: Version of the model that generated this inference + annotations: + type: array + description: A list of annotations + items: + type: object + MLProvider: + type: object + properties: + name: + type: string + description: The name of this provider + models: + type: array + items: + type: object + description: A list of models that belongs to this provider + datasets: + type: array + items: + type: object + description: A list of datasets that belongs to this provider + MLModelTrainInfo: + type: object + properties: + modifiedTime: + type: string + startTime: + type: string + currentEpoch: + type: number + totalEpochs: + type: number + bestMetric: + type: number + description: Numerical value of the best metric identifier by the best metric key. + bestMetricKey: + type: string + metrics: + type: object + description: A key-value containing the training metrics of the model, such as accuracy, precision, f1-score, etc. \ No newline at end of file From 6648cb0d7e85f89c08d8d1edec88dbe101c8e090 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Thu, 20 Jul 2023 14:51:19 +0100 Subject: [PATCH 41/53] Applied code review suggestions --- .../core/mlprovider/DatastoreRequest.java | 8 ++--- .../core/mlprovider/TrainRequest.java | 10 +++++++ .../core/settings/part/ArchiveImpl.java | 2 +- .../servlets/mlprovider/BulkInferServlet.java | 5 ++-- .../servlets/mlprovider/DatastoreServlet.java | 5 +++- .../web/servlets/mlprovider/InferServlet.java | 23 ++++++++------- .../servlets/mlprovider/ModelinfoServlet.java | 7 +++-- .../web/servlets/mlprovider/TrainServlet.java | 29 ++++++++++--------- .../{ML_DATA_TYPE.java => MLDataType.java} | 2 +- .../ua/dicoogle/sdk/mlprovider/MLModel.java | 11 +++---- .../sdk/mlprovider/MLProviderInterface.java | 13 ++++----- .../ua/dicoogle/sdk/mlprovider/MLlabel.java | 3 +- 12 files changed, 67 insertions(+), 51 deletions(-) rename sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/{ML_DATA_TYPE.java => MLDataType.java} (70%) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java index c8d8bd879..4c5a60d5d 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java @@ -2,7 +2,7 @@ import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; -import pt.ua.dicoogle.sdk.mlprovider.ML_DATA_TYPE; +import pt.ua.dicoogle.sdk.mlprovider.MLDataType; import java.util.ArrayList; import java.util.List; @@ -24,7 +24,7 @@ public class DatastoreRequest { /** * The type of dataset to upload */ - private ML_DATA_TYPE dataType; + private MLDataType dataType; private DimLevel dimLevel; @@ -44,11 +44,11 @@ public void setProvider(String provider) { this.provider = provider; } - public ML_DATA_TYPE getDataType() { + public MLDataType getDataType() { return dataType; } - public void setDataType(ML_DATA_TYPE dataType) { + public void setDataType(MLDataType dataType) { this.dataType = dataType; } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java index 170b95e2e..41debd344 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java @@ -3,11 +3,13 @@ /** * Object to map train requests. * A train request intends to send a train or re-train request to a model at a provider. + * This model is also used to cancel train requests. */ public class TrainRequest { private String provider; private String modelID; + private String trainingTaskID; public String getProvider() { return provider; @@ -24,4 +26,12 @@ public String getModelID() { public void setModelID(String modelID) { this.modelID = modelID; } + + public String getTrainingTaskID() { + return trainingTaskID; + } + + public void setTrainingTaskID(String trainingTaskID) { + this.trainingTaskID = trainingTaskID; + } } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/part/ArchiveImpl.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/part/ArchiveImpl.java index 55f6c831c..0c79e0714 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/part/ArchiveImpl.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/part/ArchiveImpl.java @@ -77,7 +77,7 @@ public static ArchiveImpl createDefault() { @JsonProperty("watch-directory") private String watchDirectory; - @JsonProperty("support-wsi") + @JsonProperty(value = "support-wsi", defaultValue = "false") private boolean supportWSI; @JsonProperty(value = "encrypt-users-file", defaultValue = "false") diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java index a33569499..4263c13ff 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java @@ -3,6 +3,7 @@ import org.restlet.data.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import pt.ua.dicoogle.server.web.utils.ResponseUtil; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -16,8 +17,6 @@ public class BulkInferServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - - resp.sendError(Status.SERVER_ERROR_NOT_IMPLEMENTED.getCode(), "Endpoint not implemented"); - + ResponseUtil.sendError(resp, Status.SERVER_ERROR_NOT_IMPLEMENTED.getCode(), "Endpoint not implemented"); } } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java index c45cdf461..c0ed4275a 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java @@ -3,10 +3,13 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.IOUtils; +import org.json.JSONException; +import org.restlet.data.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pt.ua.dicoogle.core.mlprovider.DatastoreRequest; import pt.ua.dicoogle.plugins.PluginController; +import pt.ua.dicoogle.server.web.utils.ResponseUtil; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -35,7 +38,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S PluginController.getInstance().datastore(datasetRequest); } catch (Exception e) { log.error("Error parsing json string", e); - resp.sendError(404, "Malformed request"); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Malformed request"); } } } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java index 2cd8fe2fc..3823b4be4 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java @@ -19,6 +19,7 @@ import pt.ua.dicoogle.sdk.mlprovider.MLInferenceRequest; import pt.ua.dicoogle.sdk.task.Task; import pt.ua.dicoogle.server.web.dicom.ROIExtractor; +import pt.ua.dicoogle.server.web.utils.ResponseUtil; import pt.ua.dicoogle.server.web.utils.cache.WSICache; import javax.servlet.ServletException; @@ -61,22 +62,22 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) // Validate the common attributes between WSI and non-WSI requests if(!body.has("level")){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "DIM level provided was invalid"); + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "DIM level provided was invalid"); return; } if(!body.has("uid")){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "DIM UID provided was invalid"); + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "DIM UID provided was invalid"); return; } if(!body.has("provider")){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Provider provided was invalid"); + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Provider provided was invalid"); return; } if(!body.has("modelID")){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Model identifier was invalid"); + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Model identifier was invalid"); return; } @@ -91,12 +92,12 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) if(wsi){ if(!body.has("points") || !body.has("type") || !body.has("baseUID")){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Insufficient data to build request"); + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Insufficient data to build request"); return; } if(level != DimLevel.INSTANCE){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Only Instance level is supported with WSI"); + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Only Instance level is supported with WSI"); return; } @@ -111,7 +112,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) } if(task == null){ - response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Could not build prediction request"); + ResponseUtil.sendError(response, Status.SERVER_ERROR_INTERNAL.getCode(), "Could not build prediction request"); return; } @@ -139,7 +140,7 @@ private Task sendWSIRequest(String provider, String modelID, String if(prediction == null){ log.error("Provider returned null prediction"); - response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Could not make prediction"); + ResponseUtil.sendError(response, Status.SERVER_ERROR_INTERNAL.getCode(), "Could not make prediction"); return; } @@ -163,7 +164,7 @@ private Task sendWSIRequest(String provider, String modelID, String } catch (InterruptedException | ExecutionException e) { log.error("Could not make prediction", e); try { - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Could not make prediction"); + ResponseUtil.sendError(response, Status.SERVER_ERROR_INTERNAL.getCode(), "Could not make prediction"); } catch (IOException ex) { throw new RuntimeException(ex); } @@ -189,7 +190,7 @@ private Task sendRequest(String provider, String modelID, DimLevel if(prediction == null){ log.error("Provider returned null prediction"); - response.sendError(Status.SERVER_ERROR_INTERNAL.getCode(), "Could not make prediction"); + ResponseUtil.sendError(response, Status.SERVER_ERROR_INTERNAL.getCode(), "Could not make prediction"); return; } @@ -251,7 +252,7 @@ private Task sendRequest(String provider, String modelID, DimLevel } catch (InterruptedException | ExecutionException e) { log.error("Could not make prediction", e); try { - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Could not make prediction"); + ResponseUtil.sendError(response, Status.SERVER_ERROR_INTERNAL.getCode(), "Could not make prediction"); } catch (IOException ex) { throw new RuntimeException(ex); } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java index 1120c930d..a04c58549 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java @@ -5,6 +5,7 @@ import pt.ua.dicoogle.plugins.PluginController; import pt.ua.dicoogle.sdk.mlprovider.MLModelTrainInfo; import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; +import pt.ua.dicoogle.server.web.utils.ResponseUtil; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -28,18 +29,18 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t String modelID = request.getParameter("modelID"); if(provider == null || provider.isEmpty()){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Provider was not specified"); + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Provider was not specified"); return; } if(modelID == null || modelID.isEmpty()){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Model was not specified"); + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Model was not specified"); return; } MLProviderInterface mlPlugin = PluginController.getInstance().getMachineLearningProviderByName(provider, true); if(mlPlugin == null){ - response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Provider not found"); + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Provider not found"); return; } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java index 4b4d6c1d7..76dae756c 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java @@ -1,13 +1,16 @@ package pt.ua.dicoogle.server.web.servlets.mlprovider; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.IOUtils; +import org.restlet.data.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pt.ua.dicoogle.core.mlprovider.TrainRequest; import pt.ua.dicoogle.plugins.PluginController; import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; import pt.ua.dicoogle.sdk.mlprovider.MLTrainTask; +import pt.ua.dicoogle.server.web.utils.ResponseUtil; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -29,7 +32,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S String dataString = IOUtils.toString(req.getReader()); if (dataString == null) { - resp.sendError(404, "Empty POST body"); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Empty POST body"); return; } @@ -40,17 +43,17 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S MLProviderInterface mlplugin = PluginController.getInstance().getMachineLearningProviderByName(trainRequest.getProvider(), true); if(mlplugin == null){ log.error("A provider with the provided name does not exist"); - resp.sendError(404, "Malformed request"); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Malformed request"); } else { MLTrainTask trainTask = mlplugin.trainModel(trainRequest.getModelID()); switch (trainTask.getStatus()){ case BUSY: log.error("Could not create training task, service is busy"); - resp.sendError(405, "Could not create training task, service is busy"); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_METHOD_NOT_ALLOWED.getCode(), "Could not create training task, service is busy"); break; case REJECTED: log.error("Could not create training task, request is malformed"); - resp.sendError(404, "Could not create training task, service is busy"); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Could not create training task, service is busy"); break; default: resp.setContentType("application/json"); @@ -60,18 +63,18 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S out.flush(); } } - } catch (Exception e) { + } catch (JsonProcessingException e) { log.error("Error parsing json string", e); - resp.sendError(404, "Malformed request"); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Malformed request"); } } @Override - protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { String dataString = IOUtils.toString(req.getReader()); if (dataString == null) { - resp.sendError(404, "Empty POST body"); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Empty POST body"); return; } @@ -82,18 +85,18 @@ protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws MLProviderInterface mlplugin = PluginController.getInstance().getMachineLearningProviderByName(trainRequest.getProvider(), true); if(mlplugin == null){ log.error("A provider with the provided name does not exist"); - resp.sendError(404, "Malformed request"); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Malformed request"); } else { - boolean stopped = mlplugin.stopTraining(""); + boolean stopped = mlplugin.stopTraining(trainRequest.getTrainingTaskID()); if(!stopped){ log.error("Could not stop training task"); - resp.sendError(404, "Could not stop training task"); + ResponseUtil.sendError(resp, Status.SERVER_ERROR_SERVICE_UNAVAILABLE.getCode(), "Could not stop training task"); } } - } catch (Exception e) { + } catch (JsonProcessingException e) { log.error("Error parsing json string", e); - resp.sendError(404, "Malformed request"); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Malformed request"); } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java similarity index 70% rename from sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java rename to sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java index 3b15f10ad..7be5db972 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ML_DATA_TYPE.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java @@ -1,5 +1,5 @@ package pt.ua.dicoogle.sdk.mlprovider; -public enum ML_DATA_TYPE { +public enum MLDataType { CSV, IMAGE, DICOM } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java index 47b1494ef..21dc9c84f 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java @@ -1,8 +1,9 @@ package pt.ua.dicoogle.sdk.mlprovider; +import java.io.Serializable; import java.util.*; -public class MLModel { +public class MLModel implements Serializable { private String name; @@ -14,7 +15,7 @@ public class MLModel { private Date creationDate; - private ML_DATA_TYPE dataType; + private MLDataType dataType; private Set labels; @@ -23,7 +24,7 @@ public MLModel(String name, String id) { this.id = id; this.type = ""; labels = new TreeSet<>(); - dataType = ML_DATA_TYPE.IMAGE; + dataType = MLDataType.IMAGE; creationDate = new Date(); } @@ -67,11 +68,11 @@ public void setCreationDate(Date creationDate) { this.creationDate = creationDate; } - public ML_DATA_TYPE getDataType() { + public MLDataType getDataType() { return dataType; } - public void setDataType(ML_DATA_TYPE dataType) { + public void setDataType(MLDataType dataType) { this.dataType = dataType; } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java index ad8f1b954..1ff54d524 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java @@ -9,15 +9,16 @@ /** * Interface to define Machine Learning providers. * A machine learning provider can be a remote service, hosted on the cloud, or a simple remote/local server - * that has installed machine learning algorithms for problem solving. + * that has machine learning algorithms installed for problem solving. * Machine learning providers can work with either image or csv datasets. - * The purpose of this interface is to provide a way to develop plugins to integrate with services such as Google's Vertex API or Amazon's SageMaker API. + * The purpose of this interface is to provide a way + * to integrate with services such as Google's Vertex API or Amazon's SageMaker API. * * @author Rui Jesus */ public abstract class MLProviderInterface implements DicooglePlugin { - protected Set acceptedDataTypes; + protected Set acceptedDataTypes; /** * This method uploads data to the provider. @@ -81,11 +82,7 @@ public abstract class MLProviderInterface implements DicooglePlugin { */ public abstract boolean isAvailable(); - public Set getAcceptedDataTypes() { + public Set getAcceptedDataTypes() { return acceptedDataTypes; } - - public void setAcceptedDataTypes(Set acceptedDataTypes) { - this.acceptedDataTypes = acceptedDataTypes; - } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java index 0ebce7f13..8d68e69cc 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java @@ -1,5 +1,6 @@ package pt.ua.dicoogle.sdk.mlprovider; +import java.io.Serializable; import java.util.Objects; /** @@ -8,7 +9,7 @@ * The label definition proposed here follows the DICOM standard guidelines for segmentation objects. * @see C.8.20.2 Segmentation Image Module for more information. */ -public class MLlabel implements Comparable{ +public class MLlabel implements Comparable, Serializable { public enum CodingSchemeDesignator{ DCM, // DICOM scheme code designator From da553753a9719990d1572e2b23e7efd0d4d6d15e Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Tue, 25 Jul 2023 12:15:07 +0100 Subject: [PATCH 42/53] Added license headers --- .../core/mlprovider/DatastoreRequest.java | 18 ++++++++++++++++++ .../core/mlprovider/PrepareDatastoreTask.java | 18 ++++++++++++++++++ .../dicoogle/core/mlprovider/TrainRequest.java | 18 ++++++++++++++++++ .../server/web/dicom/ROIExtractor.java | 18 ++++++++++++++++++ .../server/web/dicom/WSISopDescriptor.java | 18 ++++++++++++++++++ .../server/web/servlets/ROIServlet.java | 18 ++++++++++++++++++ .../servlets/mlprovider/BulkInferServlet.java | 18 ++++++++++++++++++ .../servlets/mlprovider/DatastoreServlet.java | 18 ++++++++++++++++++ .../web/servlets/mlprovider/InferServlet.java | 18 ++++++++++++++++++ .../mlprovider/ListAllModelsServlet.java | 18 ++++++++++++++++++ .../servlets/mlprovider/ModelinfoServlet.java | 18 ++++++++++++++++++ .../web/servlets/mlprovider/TrainServlet.java | 18 ++++++++++++++++++ .../server/web/utils/cache/MemoryCache.java | 18 ++++++++++++++++++ .../server/web/utils/cache/WSICache.java | 18 ++++++++++++++++++ .../sdk/datastructs/dim/BulkAnnotation.java | 18 ++++++++++++++++++ .../dicoogle/sdk/datastructs/dim/ImageROI.java | 18 ++++++++++++++++++ .../dicoogle/sdk/datastructs/dim/Point2D.java | 18 ++++++++++++++++++ .../dicoogle/sdk/datastructs/wsi/WSIFrame.java | 18 ++++++++++++++++++ .../dicoogle/sdk/mlprovider/MLCSVDataset.java | 18 ++++++++++++++++++ .../ua/dicoogle/sdk/mlprovider/MLDataType.java | 18 ++++++++++++++++++ .../ua/dicoogle/sdk/mlprovider/MLDataset.java | 18 ++++++++++++++++++ .../sdk/mlprovider/MLDicomDataset.java | 18 ++++++++++++++++++ .../sdk/mlprovider/MLImageDataset.java | 18 ++++++++++++++++++ .../dicoogle/sdk/mlprovider/MLInference.java | 18 ++++++++++++++++++ .../sdk/mlprovider/MLInferenceRequest.java | 18 ++++++++++++++++++ .../pt/ua/dicoogle/sdk/mlprovider/MLModel.java | 18 ++++++++++++++++++ .../sdk/mlprovider/MLModelTrainInfo.java | 18 ++++++++++++++++++ .../ua/dicoogle/sdk/mlprovider/MLProvider.java | 18 ++++++++++++++++++ .../sdk/mlprovider/MLProviderInterface.java | 18 ++++++++++++++++++ .../dicoogle/sdk/mlprovider/MLTrainTask.java | 18 ++++++++++++++++++ .../pt/ua/dicoogle/sdk/mlprovider/MLlabel.java | 18 ++++++++++++++++++ 31 files changed, 558 insertions(+) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java index 4c5a60d5d..01d3e2dab 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.core.mlprovider; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java index e33c09ded..ce570e8c5 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.core.mlprovider; import org.slf4j.Logger; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java index 41debd344..2e4de9762 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.core.mlprovider; /** diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java index 78d1fdafe..fd4275b61 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.dicom; import org.dcm4che3.imageio.plugins.dcm.DicomImageReadParam; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/WSISopDescriptor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/WSISopDescriptor.java index a29f5abbc..ad9e6e257 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/WSISopDescriptor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/WSISopDescriptor.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.dicom; import org.dcm4che2.data.Tag; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java index 26d4696dd..1fa54eb67 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.servlets; import com.fasterxml.jackson.core.type.TypeReference; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java index 4263c13ff..aa6138126 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.servlets.mlprovider; import org.restlet.data.Status; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java index c0ed4275a..207c2abae 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.servlets.mlprovider; import com.fasterxml.jackson.databind.DeserializationFeature; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java index 3823b4be4..2026269c7 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.servlets.mlprovider; import com.fasterxml.jackson.core.type.TypeReference; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java index 286616cd0..cbbfd2f6f 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.servlets.mlprovider; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java index a04c58549..088e4baca 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.servlets.mlprovider; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java index 76dae756c..7953b700b 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.servlets.mlprovider; import com.fasterxml.jackson.core.JsonProcessingException; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/MemoryCache.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/MemoryCache.java index dd7311f87..964fda0fa 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/MemoryCache.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/MemoryCache.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.utils.cache; import com.google.common.cache.LoadingCache; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java index c5441b04c..ffb835c4f 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.utils.cache; import com.google.common.cache.CacheBuilder; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java index 950949152..7dfc8bc80 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.datastructs.dim; import java.util.Arrays; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java index 43af7391b..f70883720 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.datastructs.dim; import java.net.URI; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java index bd25437e0..6871bf65e 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.datastructs.dim; /** diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java index af17b6721..a9b1c681b 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.datastructs.wsi; public class WSIFrame { diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java index 81f251f44..634dcf71a 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; import java.io.InputStream; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java index 7be5db972..3b42a4985 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; public enum MLDataType { diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java index 0259933a9..53104b9e7 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; public abstract class MLDataset { diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java index 9242f8958..3b511957e 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java index 00d4851bd..555182b38 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; import java.net.URI; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java index 461ec27aa..0c8b640a4 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInferenceRequest.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInferenceRequest.java index d7c9bbaba..8e8418144 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInferenceRequest.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInferenceRequest.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java index 21dc9c84f..bf0ced06c 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; import java.io.Serializable; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelTrainInfo.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelTrainInfo.java index 67a0649ca..a2052c817 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelTrainInfo.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelTrainInfo.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; import java.util.Date; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProvider.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProvider.java index 46e01d359..b415f5eb2 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProvider.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProvider.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; import java.util.ArrayList; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java index 1ff54d524..e15c2eb1e 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; import pt.ua.dicoogle.sdk.DicooglePlugin; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java index cf804eb96..d19e0bd85 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; /** diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java index 8d68e69cc..1ba80ea6f 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; import java.io.Serializable; From 44c63f976e2ffdbe29d4354442f98c6e771aded6 Mon Sep 17 00:00:00 2001 From: Rui Jesus <32493599+Rui-Jesus@users.noreply.github.com> Date: Mon, 31 Jul 2023 09:56:25 +0100 Subject: [PATCH 43/53] Apply suggestions from code review Co-authored-by: Eduardo Pinho --- .../web/servlets/mlprovider/TrainServlet.java | 11 +++++------ .../server/web/utils/cache/WSICache.java | 18 +++++++----------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java index 7953b700b..b641fdc39 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java @@ -60,13 +60,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S trainRequest = mapper.readValue(dataString, TrainRequest.class); MLProviderInterface mlplugin = PluginController.getInstance().getMachineLearningProviderByName(trainRequest.getProvider(), true); if(mlplugin == null){ - log.error("A provider with the provided name does not exist"); + log.warn("Request for non-existent ML provider `{}`", trainRequest.getProvider()); ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Malformed request"); } else { MLTrainTask trainTask = mlplugin.trainModel(trainRequest.getModelID()); switch (trainTask.getStatus()){ case BUSY: - log.error("Could not create training task, service is busy"); + log.warn("Could not create training task, service is busy"); ResponseUtil.sendError(resp, Status.CLIENT_ERROR_METHOD_NOT_ALLOWED.getCode(), "Could not create training task, service is busy"); break; case REJECTED: @@ -75,10 +75,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S break; default: resp.setContentType("application/json"); - PrintWriter out = resp.getWriter(); - mapper.writeValue(out, trainTask); - out.close(); - out.flush(); + try (PrintWriter out = resp.getWriter()) { + mapper.writeValue(out, trainTask); + } } } } catch (JsonProcessingException e) { diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java index ffb835c4f..8277a571a 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java @@ -85,7 +85,6 @@ public DicomMetaData load(String sopInstanceUID) throws Exception { URI uri = retrieveURI(sopInstanceUID); if(uri == null){ - logger.info("URI == null"); throw new InvalidParameterException("Could not find the desired URI"); } @@ -98,14 +97,7 @@ public DicomMetaData load(String sopInstanceUID) throws Exception { throw new InvalidParameterException("Could not find the desired URI"); } - String filePath = sis.getURI().getPath(); - if (filePath.endsWith(EXTENSION_GZIP)){ - InputStream inStream = new GZIPInputStream(new BufferedInputStream(new FileInputStream(filePath), BUFFER_SIZE)); - dis = new DicomInputStream(inStream); - } - else { - dis = new DicomInputStream(new File(filePath)); - } + dis = new BufferedInputStream(sis.getInputStream()); dis.setIncludeBulkData(DicomInputStream.IncludeBulkData.URI); dis.setBulkDataDescriptor(BulkDataDescriptor.PIXELDATA); @@ -117,13 +109,17 @@ public DicomMetaData load(String sopInstanceUID) throws Exception { } /** - * Helper method to retrieve the URI from SOPInstanceUID in lucene. + * Helper method to retrieve the URI to + * the file with the given SOP Instance UID + * from the archive's DIM provider. + * * @param sop SopInstanceUID * @return uri of the SopInstance */ private URI retrieveURI(String sop){ - String query = "SOPInstanceUID:" + sop; + String query = "SOPInstanceUID:\"" + sop + '"'; + Iterable results; try { From 667b971a47607b93a62ecb8dff21560c44d19b15 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Mon, 31 Jul 2023 13:02:00 +0100 Subject: [PATCH 44/53] Added documentation. Fixed issue with previous commit --- .../server/web/utils/cache/WSICache.java | 4 +-- .../sdk/datastructs/dim/BulkAnnotation.java | 34 ++++++++++++++++--- .../dicoogle/sdk/mlprovider/MLTrainTask.java | 30 +++++++++++++--- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java index 8277a571a..97ab49060 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java @@ -85,7 +85,7 @@ public DicomMetaData load(String sopInstanceUID) throws Exception { URI uri = retrieveURI(sopInstanceUID); if(uri == null){ - throw new InvalidParameterException("Could not find the desired URI"); + throw new IllegalArgumentException("Could not find the desired URI"); } Attributes fmi; @@ -97,7 +97,7 @@ public DicomMetaData load(String sopInstanceUID) throws Exception { throw new InvalidParameterException("Could not find the desired URI"); } - dis = new BufferedInputStream(sis.getInputStream()); + dis = new DicomInputStream(sis.getInputStream()); dis.setIncludeBulkData(DicomInputStream.IncludeBulkData.URI); dis.setBulkDataDescriptor(BulkDataDescriptor.PIXELDATA); diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java index 7dfc8bc80..b6f108ff0 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java @@ -21,20 +21,46 @@ import java.util.Arrays; import java.util.List; +/** + * A bulk annotation object denotes a group of annotations from a DICOM file generated by third-party services like AI algorithms. + * It follows the supplement 222 of the DICOM standard. + * Annotations in a bulk annotation object share common attributes such as shape type, label, pixel origin, etc. + */ public class BulkAnnotation { + /** + * Denotes the pixel origin of the annotations contained in this bulk. + */ public enum PixelOrigin { - FRAME, // Coordinates of this annotation are related to the frame (image section) - VOLUME // Coordinates of this annotation are related to the Frame Matrix (whole image) + /** + * Coordinates of this annotation are related to the frame (image section) + */ + FRAME, + /** + * Coordinates of this annotation are related to the Frame Matrix (whole image) + */ + VOLUME } + /** + * Denotes the type of annotations contained in this bulk. + */ public enum AnnotationType { RECTANGLE, ELLIPSE, POLYGON, POLYLINE, POINT } + /** + * Denotes the type of coordinates contained in this bulk + */ public enum CoordinateType { - TWO_DIMENSIONAL, //for image relative coordinates - THREE_DIMENSIONAL //for coordinates in a Cartesian system defined by a frame of reference + /** + * For image relative coordinates + */ + TWO_DIMENSIONAL, + /** + * For coordinates in a Cartesian system defined by a frame of reference + */ + THREE_DIMENSIONAL } private PixelOrigin pixelOrigin; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java index d19e0bd85..4495811aa 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java @@ -25,22 +25,42 @@ */ public class MLTrainTask { - public enum TRAINING_STATUS { + /** + * Status of a training task request/job. + * It is used to identify the status of training tasks in execution but also training requests. + */ + public enum TrainingStatus { + /** + * A training task was successfully submitted. + * A corresponding identifier should be generated to track the progress. + */ SUBMITTED, + /** + * A training job identified by a ID is currently running + */ RUNNING, + /** + * A training job identified by an ID was cancelled. + */ CANCELLED, + /** + * A training request was not processed because the service cannot process more training requests. + */ BUSY, + /** + * A training request was not processed because the request was malformed or some other error occured. + */ REJECTED } - public MLTrainTask(String taskID, TRAINING_STATUS status) { + public MLTrainTask(String taskID, TrainingStatus status) { this.taskID = taskID; this.status = status; } private String taskID; - private TRAINING_STATUS status; + private TrainingStatus status; public String getTaskID() { return taskID; @@ -50,11 +70,11 @@ public void setTaskID(String taskID) { this.taskID = taskID; } - public TRAINING_STATUS getStatus() { + public TrainingStatus getStatus() { return status; } - public void setStatus(TRAINING_STATUS status) { + public void setStatus(TrainingStatus status) { this.status = status; } } From 8a217cb05b2a64191d0d9237257a2539680bceb9 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Mon, 31 Jul 2023 13:08:45 +0100 Subject: [PATCH 45/53] Updated version --- dicoogle/pom.xml | 2 +- pom.xml | 2 +- sdk/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dicoogle/pom.xml b/dicoogle/pom.xml index e8c0ec939..5f2596a2a 100644 --- a/dicoogle/pom.xml +++ b/dicoogle/pom.xml @@ -10,7 +10,7 @@ pt.ua.ieeta dicoogle-all - 3.4.1-SNAPSHOT + 3.5.0-1-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 12eeed323..621f657c9 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 pt.ua.ieeta dicoogle-all - 3.4.1-SNAPSHOT + 3.5.0-1-SNAPSHOT pom dicoogle-all diff --git a/sdk/pom.xml b/sdk/pom.xml index b234fb6f2..828bab10f 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -10,7 +10,7 @@ pt.ua.ieeta dicoogle-all - 3.4.1-SNAPSHOT + 3.5.0-1-SNAPSHOT ../pom.xml From d5bf8c6d8fe76a7cc6de9a9fa4dcd2297098ee21 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Thu, 23 Nov 2023 17:21:13 +0000 Subject: [PATCH 46/53] Some corrections to MLInference.java --- .../pt/ua/dicoogle/sdk/mlprovider/MLInference.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java index 0c8b640a4..87ba3b8df 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java @@ -22,8 +22,10 @@ import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * This object maps inferences done by the AI algorithms. @@ -31,7 +33,7 @@ */ public class MLInference { - private HashMap metrics; + private Map metrics; private String version; @@ -43,11 +45,16 @@ public class MLInference { @JsonIgnore private Path dicomSEG; - public HashMap getMetrics() { + public MLInference(){ + this.metrics = new HashMap<>(); + this.annotations = new ArrayList<>(); + } + + public Map getMetrics() { return metrics; } - public void setMetrics(HashMap metrics) { + public void setMetrics(Map metrics) { this.metrics = metrics; } From 0cd1d9b3a1f6a59c6b206ebdbcfcd9dfcabb0fac Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Mon, 11 Dec 2023 10:13:46 +0000 Subject: [PATCH 47/53] Added capacity to define model parameters. --- .../web/servlets/mlprovider/InferServlet.java | 22 ++- .../dicoogle/sdk/mlprovider/MLException.java | 39 +++++ .../sdk/mlprovider/MLInferenceRequest.java | 17 +++ .../ua/dicoogle/sdk/mlprovider/MLModel.java | 16 ++ .../sdk/mlprovider/MLModelParameter.java | 139 ++++++++++++++++++ 5 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLException.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelParameter.java diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java index 2026269c7..db0886337 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java @@ -48,7 +48,9 @@ import java.awt.image.BufferedImage; import java.io.*; import java.nio.file.Files; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -73,12 +75,10 @@ public InferServlet() { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String jsonString = IOUtils.toString(request.getReader()); - ObjectMapper mapper = new ObjectMapper(); JsonNode body = mapper.readTree(jsonString); // Validate the common attributes between WSI and non-WSI requests - if(!body.has("level")){ ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "DIM level provided was invalid"); return; @@ -105,6 +105,12 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) DimLevel level = DimLevel.valueOf(body.get("level").asText().toUpperCase()); String dimUID = body.get("uid").asText(); + Map parameters; + if(body.has("parameters")) + parameters = mapper.convertValue(body.get("parameters"), Map.class); + else + parameters = new HashMap<>(); + Task task; if(wsi){ @@ -123,10 +129,10 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) BulkAnnotation.AnnotationType type = BulkAnnotation.AnnotationType.valueOf(body.get("type").asText()); List points = mapper.readValue(body.get("points").toString(), new TypeReference>(){}); - task = sendWSIRequest(provider, modelID, baseSopInstanceUID, dimUID, type, points, response); + task = sendWSIRequest(provider, modelID, baseSopInstanceUID, dimUID, type, points, parameters, response); } else { - task = sendRequest(provider, modelID, level, dimUID, response); + task = sendRequest(provider, modelID, level, dimUID, parameters, response); } if(task == null){ @@ -138,10 +144,12 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) } private Task sendWSIRequest(String provider, String modelID, String baseSopInstanceUID, String uid, - BulkAnnotation.AnnotationType roiType, List roi, HttpServletResponse response){ + BulkAnnotation.AnnotationType roiType, List roi, + Map parameters, HttpServletResponse response){ ObjectMapper mapper = new ObjectMapper(); MLInferenceRequest predictionRequest = new MLInferenceRequest(true, DimLevel.INSTANCE, uid, modelID); + predictionRequest.setParameters(parameters); BulkAnnotation annotation = new BulkAnnotation(); annotation.setPoints(roi); annotation.setAnnotationType(roiType); @@ -197,9 +205,11 @@ private Task sendWSIRequest(String provider, String modelID, String } } - private Task sendRequest(String provider, String modelID, DimLevel level, String dimUID, HttpServletResponse response){ + private Task sendRequest(String provider, String modelID, DimLevel level, String dimUID, Map parameters, HttpServletResponse response){ ObjectMapper mapper = new ObjectMapper(); MLInferenceRequest predictionRequest = new MLInferenceRequest(false, level, dimUID, modelID); + predictionRequest.setParameters(parameters); Task task = PluginController.getInstance().infer(provider, predictionRequest); if(task != null){ task.onCompletion(() -> { diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLException.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLException.java new file mode 100644 index 000000000..a9f66178d --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLException.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ +package pt.ua.dicoogle.sdk.mlprovider; + +public class MLException extends RuntimeException{ + + public MLException() { + super(); + } + + public MLException(String message) { + super(message); + } + + public MLException(Exception cause) { + super(cause); + } + + public MLException(String message, Exception cause) { + super(message, cause); + } + +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInferenceRequest.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInferenceRequest.java index 8e8418144..b19b3cd6f 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInferenceRequest.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInferenceRequest.java @@ -21,7 +21,12 @@ import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; +/** + * This object is used in the infer servlet to serialize the incoming JSON objects. + */ public class MLInferenceRequest { private boolean isWsi; @@ -34,11 +39,14 @@ public class MLInferenceRequest { private String modelID; + private Map parameters; + public MLInferenceRequest(boolean isWsi, DimLevel level, String dimID, String modelID) { this.isWsi = isWsi; this.level = level; this.dimID = dimID; this.modelID = modelID; + parameters = new HashMap<>(); } public boolean isWsi() { @@ -84,4 +92,13 @@ public String getModelID() { public void setModelID(String modelID) { this.modelID = modelID; } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java index bf0ced06c..10dd825ba 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java @@ -37,15 +37,23 @@ public class MLModel implements Serializable { private Set labels; + private List parameters; + public MLModel(String name, String id) { this.name = name; this.id = id; this.type = ""; labels = new TreeSet<>(); + parameters = new ArrayList<>(); dataType = MLDataType.IMAGE; creationDate = new Date(); } + public MLModel(String name, String id, List parameters){ + this(name, id); + this.parameters = parameters; + } + public String getName() { return name; } @@ -102,6 +110,14 @@ public void setLabels(Set labels) { this.labels = labels; } + public List getParameters() { + return parameters; + } + + public void setParameters(List parameters) { + this.parameters = parameters; + } + public void removeLabel(MLlabel label){ this.labels.remove(label); } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelParameter.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelParameter.java new file mode 100644 index 000000000..9728501b6 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelParameter.java @@ -0,0 +1,139 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ +package pt.ua.dicoogle.sdk.mlprovider; + +import java.util.List; + +/** + * This object is used to map MLModel parameters. + * These parameters are used to fine tune a inference request to a model. + */ +public class MLModelParameter { + + private MLModelParameterType type; + private String name; + private Object defaultValue; + private String description; + private List choices; + + public enum MLModelParameterType{ + TEXT, + NUMBER, + ENUM + } + + public static class Choice { + + private String name; + private Object value; + + public Choice(String name, Object value) { + this.name = name; + this.value = value; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + private MLModelParameter(MLModelParameterType type, String name, Object defaultValue, String description) { + this.type = type; + this.name = name; + this.defaultValue = defaultValue; + this.description = description; + } + + private MLModelParameter(String name, List choices, Object defaultValue, String description){ + this(MLModelParameterType.ENUM, name, defaultValue, description); + this.choices = choices; + } + + public static MLModelParameter buildNumberParam(String name, double defaultValue, String description){ + if(name == null || name.isEmpty()) + throw new IllegalArgumentException(); + return new MLModelParameter(MLModelParameterType.NUMBER, name, defaultValue, description); + } + + public static MLModelParameter buildTextParam(String name, String defaultValue, String description){ + if(name == null || name.isEmpty()) + throw new IllegalArgumentException(); + return new MLModelParameter(MLModelParameterType.TEXT, name, defaultValue, description); + } + + public static MLModelParameter buildEnumParam(String name, List choices, Object defaultValue, String description){ + if(name == null || name.isEmpty()) + throw new IllegalArgumentException(); + if(choices == null || choices.isEmpty()) + throw new IllegalArgumentException(); + return new MLModelParameter(name, choices, defaultValue, description); + } + + public MLModelParameterType getType() { + return type; + } + + public void setType(MLModelParameterType type) { + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Object getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getChoices() { + return choices; + } + + public void setChoices(List choices) { + this.choices = choices; + } +} From dcb16671e66b8a21b43a60d55ceac57bcbc329b9 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Fri, 23 Feb 2024 10:28:09 +0000 Subject: [PATCH 48/53] Started implementation of labeled datastore endpoint. Improvements to data models. --- .../core/mlprovider/PrepareDatastoreTask.java | 79 ++++++++++++++++--- .../ua/dicoogle/plugins/PluginController.java | 20 ++--- .../sdk/datastructs/dim/BulkAnnotation.java | 8 +- .../sdk/datastructs/dim/ImageROI.java | 16 +++- .../dicoogle/sdk/mlprovider/ImageEntry.java | 68 ++++++++++++++++ .../dicoogle/sdk/mlprovider/MLCSVDataset.java | 3 + .../ua/dicoogle/sdk/mlprovider/MLDataset.java | 22 ++++++ .../sdk/mlprovider/MLDicomDataset.java | 2 + .../sdk/mlprovider/MLImageDataset.java | 17 ++-- .../ua/dicoogle/sdk/mlprovider/MLlabel.java | 24 +++--- 10 files changed, 216 insertions(+), 43 deletions(-) create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ImageEntry.java diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java index ce570e8c5..b6ee0e81d 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java @@ -18,18 +18,28 @@ */ package pt.ua.dicoogle.core.mlprovider; +import org.dcm4che2.data.BasicDicomObject; +import org.dcm4che2.data.Tag; +import org.dcm4che2.data.TransferSyntax; +import org.dcm4che2.data.VR; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pt.ua.dicoogle.plugins.PluginController; import pt.ua.dicoogle.sdk.datastructs.SearchResult; -import pt.ua.dicoogle.sdk.mlprovider.MLDataset; -import pt.ua.dicoogle.sdk.mlprovider.MLDicomDataset; -import pt.ua.dicoogle.sdk.mlprovider.MLImageDataset; +import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; +import pt.ua.dicoogle.sdk.mlprovider.*; import pt.ua.dicoogle.server.web.dicom.ROIExtractor; import pt.ua.dicoogle.server.web.utils.cache.WSICache; -import java.util.HashMap; -import java.util.UUID; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -65,10 +75,12 @@ public MLDataset call() throws Exception { switch (request.getDataType()){ case DICOM: return new MLDicomDataset(request.getDimLevel(), request.getUids()); - case IMAGE: // Not operational - throw new UnsupportedOperationException("Datastore requests for image objects is not supported"); - /* - HashMap extraFields = new HashMap(); + case IMAGE: + //throw new UnsupportedOperationException("Datastore requests for image objects is not supported"); + + HashMap extraFields = new HashMap<>(); + + String path = this.ensureAndCreatePath(); extraFields.put("SOPInstanceUID", "SOPInstanceUID"); extraFields.put("SharedFunctionalGroupsSequence_PixelMeasuresSequence_PixelSpacing", "SharedFunctionalGroupsSequence_PixelMeasuresSequence_PixelSpacing"); @@ -80,6 +92,8 @@ public MLDataset call() throws Exception { extraFields.put("ImageType", "ImageType"); MLImageDataset mlDataset = new MLImageDataset(); + HashMap dataset = new HashMap<>(); + Set classes = new HashSet<>(); this.request.getDataset().entrySet().forEach((entry -> { try { @@ -88,18 +102,59 @@ public MLDataset call() throws Exception { Iterable results = controller .query(controller.getQueryProvidersName(true).get(0), "SOPInstanceUID:" + entry.getKey(), extraFields).get(); + int c = 0; for (SearchResult image : results) { - //List rois = (List) roiExtractor.extractROI(); + + BasicDicomObject dcm = new BasicDicomObject(); + dcm.putString(Tag.TransferSyntaxUID, VR.CS, "1.2.840.10008.1.2.4.50"); + + for(BulkAnnotation annotation: entry.getValue()){ + BufferedImage roi = roiExtractor.extractROI(image.get("SOPInstanceUID").toString(), annotation); + String roiFileName = annotation.getLabel().getName() + c++; + classes.add(annotation.getLabel().getName()); + File output = new File(path + File.separator + roiFileName + ".jpeg"); + ImageIO.write(roi, "jpeg", output); + dataset.put(new ImageEntry(dcm, output.toURI()), annotation.getLabel()); + } } - } catch (InterruptedException | ExecutionException e) { + } catch (IOException | InterruptedException | ExecutionException e) { logger.error("Error preparing datastore task", e); } })); + + mlDataset.setMultiClass(classes.size() > 2); + mlDataset.setDataset(dataset); + return mlDataset; - */ default: return null; } } + + private String ensureAndCreatePath(){ + Path datasetsFolder = Paths.get("datasets"); + // Check if the folder exists + if (!Files.exists(datasetsFolder)) { + try { + Files.createDirectories(datasetsFolder); + System.out.println("Datasets folder didn't exist, creating one."); + } catch (Exception e) { + System.err.println("Failed to create datasets folder: " + e.getMessage()); + } + } + + Path datasetFolder = Paths.get("datasets" + File.separator + System.currentTimeMillis()); + + if (!Files.exists(datasetFolder)) { + try { + Files.createDirectories(datasetFolder); + } catch (Exception e) { + System.err.println("Failed to create dataset folder: " + e.getMessage()); + } + } + + return datasetFolder.toString(); + } + } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java index e08bbd41e..c121c5afa 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java @@ -895,16 +895,18 @@ public Task datastore(final DatastoreRequest datasetRequest) { String uuid = UUID.randomUUID().toString(); Task prepareTask = new Task<>("MLPrepareDatastoreTask" + uuid, new PrepareDatastoreTask(this, datasetRequest)); + MLProviderInterface mlInterface = getMachineLearningProviderByName(datasetRequest.getProvider(), true); + if (mlInterface == null) { + logger.error("MLProvider with name {} not found", datasetRequest.getProvider()); + prepareTask.cancel(true); + return prepareTask; + } + prepareTask.onCompletion(() -> { - MLProviderInterface mlInterface = getMachineLearningProviderByName(datasetRequest.getProvider(), true); - if (mlInterface == null) { - logger.error("MLProvider with name {} not found", prepareTask.getName()); - } else { - try { - mlInterface.dataStore(prepareTask.get()); - } catch (InterruptedException | ExecutionException e) { - logger.error("Task {} failed execution", prepareTask.getName(), e); - } + try { + mlInterface.dataStore(prepareTask.get()); + } catch (InterruptedException | ExecutionException e) { + logger.error("Task {} failed execution", prepareTask.getName(), e); } }); logger.debug("Fired prepare dataset task with uuid {}", uuid); diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java index b6f108ff0..3ab7792f2 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java @@ -18,6 +18,8 @@ */ package pt.ua.dicoogle.sdk.datastructs.dim; +import pt.ua.dicoogle.sdk.mlprovider.MLlabel; + import java.util.Arrays; import java.util.List; @@ -69,7 +71,7 @@ public enum CoordinateType { private AnnotationType annotationType; - private String label; + private MLlabel label; private List points; @@ -99,11 +101,11 @@ public void setAnnotationType(AnnotationType annotationType) { this.annotationType = annotationType; } - public String getLabel() { + public MLlabel getLabel() { return label; } - public void setLabel(String label) { + public void setLabel(MLlabel label) { this.label = label; } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java index f70883720..daab4948a 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java @@ -50,9 +50,9 @@ private FileType(String s, String mimeType) { private double y; - private int width; + private double width; - private int height; + private double height; private String sopInstanceUID; @@ -67,6 +67,14 @@ public ImageROI(String sopInstanceUID, int x, int y, int width, int height) { this.height = height; } + public ImageROI(String sopInstanceUID, double x, double y, double width, double height, URI roi) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.uriROI = roi; + } + public double getX() { return x; } @@ -91,7 +99,7 @@ public void setSopInstanceUID(String sopInstanceUID) { this.sopInstanceUID = sopInstanceUID; } - public int getWidth() { + public double getWidth() { return width; } @@ -99,7 +107,7 @@ public void setWidth(int width) { this.width = width; } - public int getHeight() { + public double getHeight() { return height; } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ImageEntry.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ImageEntry.java new file mode 100644 index 000000000..816aa5637 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ImageEntry.java @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ +package pt.ua.dicoogle.sdk.mlprovider; + +import org.dcm4che2.data.DicomObject; + +import java.net.URI; +import java.util.Objects; + +/** + * Used to map the metadata of regions of interest. + */ +public class ImageEntry { + + private DicomObject object; + + private URI file; + + public ImageEntry(DicomObject object, URI file) { + this.object = object; + this.file = file; + } + + public DicomObject getObject() { + return object; + } + + public void setObject(DicomObject object) { + this.object = object; + } + + public URI getFile() { + return file; + } + + public void setFile(URI file) { + this.file = file; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ImageEntry that = (ImageEntry) o; + return object.equals(that.object) && file.equals(that.file); + } + + @Override + public int hashCode() { + return Objects.hash(file); + } +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java index 634dcf71a..50b657542 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java @@ -24,4 +24,7 @@ public class MLCSVDataset extends MLDataset { private InputStream csvFile; + public MLCSVDataset(){ + super("", MLDataType.CSV); + } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java index 53104b9e7..3958f476f 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java @@ -22,4 +22,26 @@ public abstract class MLDataset { protected String name; + protected MLDataType dataType; + + public MLDataset(String name, MLDataType dataType) { + this.name = name; + this.dataType = dataType; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public MLDataType getDataType() { + return dataType; + } + + public void setDataType(MLDataType dataType) { + this.dataType = dataType; + } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java index 3b511957e..8b7d69b88 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java @@ -32,11 +32,13 @@ public class MLDicomDataset extends MLDataset { private List dimUIDs; public MLDicomDataset(DimLevel level){ + super("", MLDataType.DICOM); this.level = level; dimUIDs = new ArrayList<>(); } public MLDicomDataset(DimLevel level, List dimUIDs){ + super("", MLDataType.DICOM); this.level = level; this.dimUIDs = dimUIDs; } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java index 555182b38..272f61561 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java @@ -18,9 +18,7 @@ */ package pt.ua.dicoogle.sdk.mlprovider; -import java.net.URI; import java.util.HashMap; -import java.util.List; /** * An ML dataset of image objects. @@ -28,15 +26,24 @@ */ public class MLImageDataset extends MLDataset { - private HashMap> dataset; + private HashMap dataset; private boolean multiClass; - public HashMap> getDataset() { + public MLImageDataset() { + super("name", MLDataType.IMAGE); + } + + public MLImageDataset(HashMap dataset) { + super("", MLDataType.IMAGE); + this.dataset = dataset; + } + + public HashMap getDataset() { return dataset; } - public void setDataset(HashMap> dataset) { + public void setDataset(HashMap dataset) { this.dataset = dataset; } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java index 1ba80ea6f..a20afa4dc 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java @@ -38,7 +38,7 @@ public enum CodingSchemeDesignator{ /** * DICOM Segment Label (0062, 0005) is a user defined label. */ - private String label; + private String name; /** * DICOM Segment Description (0062, 0007) is a user defined description. @@ -67,8 +67,7 @@ public enum CodingSchemeDesignator{ */ private CodingSchemeDesignator codingSchemeDesignator; - public MLlabel(String label) { - this.label = label; + public MLlabel(){ this.description = "unknown"; this.codingSchemeDesignator = CodingSchemeDesignator.DCM; this.codeValue = "333333"; @@ -76,12 +75,17 @@ public MLlabel(String label) { this.color = "#000000"; } - public String getLabel() { - return label; + public MLlabel(String name) { + this(); + this.name = name; } - public void setLabel(String label) { - this.label = label; + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; } public String getDescription() { @@ -129,16 +133,16 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MLlabel mLlabel = (MLlabel) o; - return label.equals(mLlabel.label); + return name.equals(mLlabel.name); } @Override public int hashCode() { - return Objects.hash(label); + return Objects.hash(name); } @Override public int compareTo(MLlabel o) { - return o.getLabel().compareTo(this.getLabel()); + return o.getName().compareTo(this.getName()); } } From 56a8fd21802efaebdcbaa16e8ccc53078f26832b Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Mon, 26 Feb 2024 09:53:28 +0000 Subject: [PATCH 49/53] Added new model parameter to allow the specification of a preferred magnification level. --- .../web/servlets/mlprovider/InferServlet.java | 42 +++++++++++++++---- .../server/web/utils/cache/WSICache.java | 6 --- .../sdk/datastructs/dim/BulkAnnotation.java | 7 ++++ .../ua/dicoogle/sdk/mlprovider/MLModel.java | 15 +++++++ 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java index db0886337..43a616fef 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java @@ -156,6 +156,25 @@ private Task sendWSIRequest(String provider, String modelID, String try { DicomMetaData dicomMetaData = this.getDicomMetadata(uid); + DicomMetaData baseDicomMetaData = this.getDicomMetadata(baseSopInstanceUID); + + WSISopDescriptor descriptor = new WSISopDescriptor(); + descriptor.extractData(dicomMetaData.getAttributes()); + + WSISopDescriptor baseDescriptor = new WSISopDescriptor(); + baseDescriptor.extractData(baseDicomMetaData.getAttributes()); + + double scale = (descriptor.getTotalPixelMatrixRows() * 1.0) / baseDescriptor.getTotalPixelMatrixRows(); + + if(!uid.equals(baseSopInstanceUID)){ + scaleAnnotation(annotation, scale); + } + + // Verify dimensions of annotation, reject if too big. In the future, adopt a sliding window strategy to process large processing windows. + double area = annotation.getArea(); + if(area > 16000000) // This equates to a maximum of 4000x4000 which represents in RGB an image of 48MB + return null; + BufferedImage bi = roiExtractor.extractROI(dicomMetaData, annotation); predictionRequest.setRoi(bi); Task task = PluginController.getInstance().infer(provider, predictionRequest); @@ -172,12 +191,6 @@ private Task sendWSIRequest(String provider, String modelID, String // Coordinates need to be converted if we're working with WSI if(!prediction.getAnnotations().isEmpty()){ - WSISopDescriptor descriptor = new WSISopDescriptor(); - descriptor.extractData(dicomMetaData.getAttributes()); - DicomMetaData base = this.getDicomMetadata(baseSopInstanceUID); - WSISopDescriptor baseDescriptor = new WSISopDescriptor(); - baseDescriptor.extractData(base.getAttributes()); - double scale = (descriptor.getTotalPixelMatrixRows() * 1.0) / baseDescriptor.getTotalPixelMatrixRows(); Point2D tl = annotation.getBoundingBox().get(0); convertCoordinates(prediction, tl, scale); } @@ -308,9 +321,22 @@ private DicomMetaData getDicomMetadata(String sop) throws IOException{ private void convertCoordinates(MLInference prediction, Point2D tl, double scale){ for(BulkAnnotation ann : prediction.getAnnotations()){ for(Point2D p : ann.getPoints()){ - p.setX((p.getX() + tl.getX())/scale); - p.setY((p.getY() + tl.getY())/scale); + p.setX((p.getX() + tl.getX()) * scale); + p.setY((p.getY() + tl.getY()) * scale); } } } + + /** + * When working with WSI, it is convenient to have coordinates relative to the base of the pyramid. + * This method takes care of that. + * @param scale to transform coordinates + * @return the ml prediction with the converted coordinates. + */ + private void scaleAnnotation(BulkAnnotation annotation, double scale){ + for(Point2D p : annotation.getPoints()){ + p.setX((p.getX()) * scale); + p.setY((p.getY()) * scale); + } + } } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java index 97ab49060..7d9bfa041 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java @@ -31,20 +31,14 @@ import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.utils.QueryException; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; import java.net.URI; import java.security.InvalidParameterException; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.zip.GZIPInputStream; /** * Cache used to store DicomMetadata objects temporarily, as they are quite heavy to build on-demand. * Used only for WSI instances. - * @author Rui Jesus */ public class WSICache extends MemoryCache{ diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java index 3ab7792f2..4997252e5 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java @@ -170,4 +170,11 @@ public List getBoundingBox(){ return Arrays.asList(tl, tr, bl, br); } + public double getArea(){ + List bbox = this.getBoundingBox(); + double width = bbox.get(1).getX() - bbox.get(0).getX(); + double height = bbox.get(0).getY() - bbox.get(2).getY(); + return Math.abs(width * height); + } + } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java index 10dd825ba..40c9f345c 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java @@ -39,6 +39,12 @@ public class MLModel implements Serializable { private List parameters; + /** + * A number between 1 and n that specifies the magnification level of the images this model supports. + * It is only useful in pathology where algorithms might be trained on lower resolution levels of the pyramid. + */ + private int processMagnification; + public MLModel(String name, String id) { this.name = name; this.id = id; @@ -47,6 +53,7 @@ public MLModel(String name, String id) { parameters = new ArrayList<>(); dataType = MLDataType.IMAGE; creationDate = new Date(); + processMagnification = 0; } public MLModel(String name, String id, List parameters){ @@ -125,4 +132,12 @@ public void removeLabel(MLlabel label){ public void addLabel(MLlabel label){ this.labels.add(label); } + + public int getProcessMagnification() { + return processMagnification; + } + + public void setProcessMagnification(int processMagnification) { + this.processMagnification = processMagnification; + } } From ee801272bf002e2484d174a53cc5f96674c34c6b Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Sat, 2 Mar 2024 18:59:11 +0000 Subject: [PATCH 50/53] Reworked bulk annotation to be compliant with the standard. Applied code suggestions. --- .../core/mlprovider/PrepareDatastoreTask.java | 15 ++-- .../server/web/dicom/ROIExtractor.java | 41 +++++----- .../server/web/servlets/ROIServlet.java | 17 ++-- .../web/servlets/mlprovider/InferServlet.java | 81 +++++++++---------- .../sdk/datastructs/dim/BulkAnnotation.java | 62 +++++++++----- .../dicoogle/sdk/mlprovider/ImageEntry.java | 4 +- .../dicoogle/sdk/mlprovider/MLCSVDataset.java | 30 ------- .../dicoogle/sdk/mlprovider/MLDataType.java | 19 ++++- .../ua/dicoogle/sdk/mlprovider/MLDataset.java | 6 +- .../sdk/mlprovider/MLImageDataset.java | 2 +- 10 files changed, 143 insertions(+), 134 deletions(-) delete mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java index b6ee0e81d..ca00d90ca 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java @@ -27,6 +27,7 @@ import pt.ua.dicoogle.plugins.PluginController; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation; +import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; import pt.ua.dicoogle.sdk.mlprovider.*; import pt.ua.dicoogle.server.web.dicom.ROIExtractor; import pt.ua.dicoogle.server.web.utils.cache.WSICache; @@ -109,12 +110,14 @@ public MLDataset call() throws Exception { dcm.putString(Tag.TransferSyntaxUID, VR.CS, "1.2.840.10008.1.2.4.50"); for(BulkAnnotation annotation: entry.getValue()){ - BufferedImage roi = roiExtractor.extractROI(image.get("SOPInstanceUID").toString(), annotation); - String roiFileName = annotation.getLabel().getName() + c++; - classes.add(annotation.getLabel().getName()); - File output = new File(path + File.separator + roiFileName + ".jpeg"); - ImageIO.write(roi, "jpeg", output); - dataset.put(new ImageEntry(dcm, output.toURI()), annotation.getLabel()); + for(List points: annotation.getAnnotations()){ + BufferedImage roi = roiExtractor.extractROI(image.get("SOPInstanceUID").toString(), annotation.getAnnotationType(), points); + String roiFileName = annotation.getLabel().getName() + c++; + classes.add(annotation.getLabel().getName()); + File output = new File(path + File.separator + roiFileName + ".jpeg"); + ImageIO.write(roi, "jpeg", output); + dataset.put(new ImageEntry(dcm, output.toURI()), annotation.getLabel()); + } } } diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java index fd4275b61..e9cf8ab10 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -27,6 +27,7 @@ import pt.ua.dicoogle.sdk.datastructs.dim.Point2D; import pt.ua.dicoogle.sdk.datastructs.wsi.WSIFrame; import pt.ua.dicoogle.server.web.utils.cache.WSICache; +import sun.reflect.annotation.AnnotationType; import javax.imageio.ImageIO; import javax.imageio.ImageReader; @@ -37,6 +38,8 @@ import java.util.*; import java.util.List; +import static pt.ua.dicoogle.sdk.datastructs.dim.BulkAnnotation.AnnotationType.*; + public class ROIExtractor { private static final Logger logger = LoggerFactory.getLogger(ROIExtractor.class); @@ -49,7 +52,7 @@ public ROIExtractor() { this.wsiCache = WSICache.getInstance(); } - public BufferedImage extractROI(String sopInstanceUID, BulkAnnotation bulkAnnotation) { + public BufferedImage extractROI(String sopInstanceUID, BulkAnnotation.AnnotationType type, List annotation) { DicomMetaData metaData; try { metaData = getDicomMetadata(sopInstanceUID); @@ -57,10 +60,10 @@ public BufferedImage extractROI(String sopInstanceUID, BulkAnnotation bulkAnnota logger.error("Could not extract metadata", e); return null; } - return extractROI(metaData, bulkAnnotation); + return extractROI(metaData, type, annotation); } - public BufferedImage extractROI(DicomMetaData dicomMetaData, BulkAnnotation bulkAnnotation) { + public BufferedImage extractROI(DicomMetaData dicomMetaData, BulkAnnotation.AnnotationType type, List annotation) { ImageReader imageReader = getImageReader(); @@ -84,7 +87,7 @@ public BufferedImage extractROI(DicomMetaData dicomMetaData, BulkAnnotation bulk descriptor.extractData(dicomMetaData.getAttributes()); try { - return getROIFromAnnotation(bulkAnnotation, descriptor, imageReader, param); + return getROIFromAnnotation(type, annotation, descriptor, imageReader, param); } catch (IllegalArgumentException e) { logger.error("Error writing ROI", e); } @@ -105,19 +108,20 @@ private static ImageReader getImageReader() { /** * Given an annotation and an image, return the section of the image the annotation intersects. * It only works with rectangle type annotations. - * @param annotation the annotation to intersect + * @param type the annotation type + * @param annotation a list of points defining the annotation to extract * @param descriptor descriptor of the WSI pyramid, contains information about the dimensions of the image. * @param imageReader * @param param * @return the intersection of the annotation on the image. * @throws IllegalArgumentException when the annotation is not one of the supported types. */ - private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDescriptor descriptor, ImageReader imageReader, DicomImageReadParam param) throws IllegalArgumentException { - if(notSupportedTypes.contains(annotation.getAnnotationType())){ + private BufferedImage getROIFromAnnotation(BulkAnnotation.AnnotationType type, List annotation, WSISopDescriptor descriptor, ImageReader imageReader, DicomImageReadParam param) throws IllegalArgumentException { + if(notSupportedTypes.contains(type)){ throw new IllegalArgumentException("Trying to build a ROI with an unsupported annotation type"); } - List constructionPoints = annotation.getBoundingBox(); //Points that will be used to construct the ROI. + List constructionPoints = BulkAnnotation.getBoundingBox(type, annotation); //Points that will be used to construct the ROI. Point2D annotationPoint1 = constructionPoints.get(0); Point2D annotationPoint2 = constructionPoints.get(3); @@ -134,8 +138,8 @@ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDesc // We need to perform clipping if annotation is not a rectangle Shape clippingShape = null; - if(annotation.getAnnotationType() != BulkAnnotation.AnnotationType.RECTANGLE){ - clippingShape = getClippingShape(annotation, constructionPoints.get(0)); + if(type != BulkAnnotation.AnnotationType.RECTANGLE){ + clippingShape = getClippingShape(type, annotation, constructionPoints.get(0)); if (clippingShape != null) g.setClip(clippingShape); } @@ -199,25 +203,26 @@ private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDesc * Given an annotation, get it as a Shape to apply as a clipping shape for the ROIs. * The points of this shape are normalized according to the starting point. * This is only needed when dealing with non-rectangle annotations. - * @param annotation + * @param type the type of annotation + * @param annotation the list of points of the annotation * @param startingPoint starting point of the rectangle that contains the annotation * @return a shape to use to clip the ROI */ - private Shape getClippingShape(BulkAnnotation annotation, Point2D startingPoint){ - switch (annotation.getAnnotationType()){ + private Shape getClippingShape(BulkAnnotation.AnnotationType type, List annotation, Point2D startingPoint){ + switch (type){ case POLYLINE: case POLYGON: Polygon polygon = new Polygon(); - for(Point2D p : annotation.getPoints()){ + for(Point2D p : annotation){ polygon.addPoint((int) (p.getX() - startingPoint.getX()), (int) (p.getY() - startingPoint.getY())); } return polygon; case ELLIPSE: - double minX = annotation.getPoints().get(0).getX(); - double maxX = annotation.getPoints().get(1).getX(); + double minX = annotation.get(0).getX(); + double maxX = annotation.get(1).getX(); - double minY = annotation.getPoints().get(2).getY(); - double maxY = annotation.getPoints().get(3).getY(); + double minY = annotation.get(2).getY(); + double maxY = annotation.get(3).getY(); return new Ellipse2D.Double(minX - startingPoint.getX(), minY - startingPoint.getY(), Math.abs(maxX - minX), Math.abs(maxY - minY)); diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java index 1fa54eb67..f2621f5c6 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java @@ -78,21 +78,18 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) return; } - BulkAnnotation annotation; + List annotation; + BulkAnnotation.AnnotationType annotationType = BulkAnnotation.AnnotationType.RECTANGLE; try{ int nX = Integer.parseInt(x); int nY = Integer.parseInt(y); int nWidth = Integer.parseInt(width); int nHeight = Integer.parseInt(height); - annotation = new BulkAnnotation(); Point2D tl = new Point2D(nX, nY); Point2D tr = new Point2D(nX + nWidth, nY); Point2D bl = new Point2D(nX, nY + nHeight); Point2D br = new Point2D(nX + nWidth, nY + nHeight); - List points = new ArrayList<>(); - points.add(tl); points.add(tr); points.add(bl); points.add(br); - annotation.setPoints(points); - annotation.setAnnotationType(BulkAnnotation.AnnotationType.RECTANGLE); + annotation = Arrays.asList(tl, tr, bl, br); } catch (NumberFormatException e){ response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "ROI provided was invalid"); return; @@ -100,7 +97,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) DicomMetaData metaData = getDicomMetadata(sopInstanceUID); - BufferedImage bi = roiExtractor.extractROI(metaData, annotation); + BufferedImage bi = roiExtractor.extractROI(metaData, annotationType, annotation); if(bi != null){ response.setContentType("image/jpeg"); @@ -136,13 +133,9 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) String type = body.get("type").asText(); List points = mapper.readValue(body.get("points").toString(), new TypeReference>(){}); - BulkAnnotation annotation = new BulkAnnotation(); - annotation.setPoints(points); - annotation.setAnnotationType(BulkAnnotation.AnnotationType.valueOf(type)); - DicomMetaData dicomMetaData = this.getDicomMetadata(sopInstanceUID); - BufferedImage bi = roiExtractor.extractROI(dicomMetaData, annotation); + BufferedImage bi = roiExtractor.extractROI(dicomMetaData, BulkAnnotation.AnnotationType.valueOf(type), points); if(bi != null){ response.setContentType("image/jpeg"); diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java index 43a616fef..5d9f58c73 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java @@ -48,10 +48,7 @@ import java.awt.image.BufferedImage; import java.io.*; import java.nio.file.Files; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ExecutionException; public class InferServlet extends HttpServlet { @@ -150,8 +147,8 @@ private Task sendWSIRequest(String provider, String modelID, String ObjectMapper mapper = new ObjectMapper(); MLInferenceRequest predictionRequest = new MLInferenceRequest(true, DimLevel.INSTANCE, uid, modelID); predictionRequest.setParameters(parameters); - BulkAnnotation annotation = new BulkAnnotation(); - annotation.setPoints(roi); + BulkAnnotation annotation = new BulkAnnotation(roiType, BulkAnnotation.PixelOrigin.VOLUME); + annotation.setAnnotations(Collections.singletonList(roi)); annotation.setAnnotationType(roiType); try { @@ -171,11 +168,11 @@ private Task sendWSIRequest(String provider, String modelID, String } // Verify dimensions of annotation, reject if too big. In the future, adopt a sliding window strategy to process large processing windows. - double area = annotation.getArea(); + double area = annotation.getArea(annotation.getAnnotations().get(0)); if(area > 16000000) // This equates to a maximum of 4000x4000 which represents in RGB an image of 48MB return null; - BufferedImage bi = roiExtractor.extractROI(dicomMetaData, annotation); + BufferedImage bi = roiExtractor.extractROI(dicomMetaData, annotation.getAnnotationType(), annotation.getAnnotations().get(0)); predictionRequest.setRoi(bi); Task task = PluginController.getInstance().infer(provider, predictionRequest); if(task != null){ @@ -191,7 +188,7 @@ private Task sendWSIRequest(String provider, String modelID, String // Coordinates need to be converted if we're working with WSI if(!prediction.getAnnotations().isEmpty()){ - Point2D tl = annotation.getBoundingBox().get(0); + Point2D tl = annotation.getBoundingBox(annotation.getAnnotations().get(0)).get(0); convertCoordinates(prediction, tl, scale); } @@ -199,7 +196,6 @@ private Task sendWSIRequest(String provider, String modelID, String PrintWriter out = response.getWriter(); mapper.writeValue(out, prediction); out.close(); - out.flush(); } catch (InterruptedException | ExecutionException e) { log.error("Could not make prediction", e); try { @@ -247,39 +243,36 @@ private Task sendRequest(String provider, String modelID, DimLevel String boundary = UUID.randomUUID().toString(); response.setContentType("multipart/form-data; boundary=" + boundary); - ServletOutputStream out = response.getOutputStream(); - - out.print("--" + boundary); - out.println(); - out.print("Content-Disposition: form-data; name=\"params\""); - out.println(); - out.print("Content-Type: application/json"); - out.println(); out.println(); - out.print(mapper.writeValueAsString(prediction)); - out.println(); - out.print("--" + boundary); - out.println(); - out.print("Content-Disposition: form-data; name=\"dicomseg\"; filename=\"dicomseg.dcm\""); - out.println(); - out.print("Content-Type: application/dicom"); - out.println(); out.println(); - - try (InputStream fi = Files.newInputStream(prediction.getDicomSEG())) { - IOUtils.copy(fi, out); - out.flush(); + try (ServletOutputStream out = response.getOutputStream()){ + out.print("--" + boundary); + out.println(); + out.print("Content-Disposition: form-data; name=\"params\""); + out.println(); + out.print("Content-Type: application/json"); + out.println(); out.println(); + out.print(mapper.writeValueAsString(prediction)); + out.println(); + out.print("--" + boundary); + out.println(); + out.print("Content-Disposition: form-data; name=\"dicomseg\"; filename=\"dicomseg.dcm\""); + out.println(); + out.print("Content-Type: application/dicom"); + out.println(); out.println(); + + try (InputStream fi = Files.newInputStream(prediction.getDicomSEG())) { + IOUtils.copy(fi, out); + out.flush(); + } + + out.println(); + out.print("--" + boundary + "--"); } - out.println(); - out.print("--" + boundary + "--"); - out.flush(); - out.close(); - } else { response.setContentType("application/json"); PrintWriter out = response.getWriter(); mapper.writeValue(out, prediction); out.close(); - out.flush(); } try{ @@ -320,9 +313,11 @@ private DicomMetaData getDicomMetadata(String sop) throws IOException{ */ private void convertCoordinates(MLInference prediction, Point2D tl, double scale){ for(BulkAnnotation ann : prediction.getAnnotations()){ - for(Point2D p : ann.getPoints()){ - p.setX((p.getX() + tl.getX()) * scale); - p.setY((p.getY() + tl.getY()) * scale); + for(List points : ann.getAnnotations()){ + for(Point2D p : points){ + p.setX((p.getX() + tl.getX()) * scale); + p.setY((p.getY() + tl.getY()) * scale); + } } } } @@ -334,9 +329,11 @@ private void convertCoordinates(MLInference prediction, Point2D tl, double scale * @return the ml prediction with the converted coordinates. */ private void scaleAnnotation(BulkAnnotation annotation, double scale){ - for(Point2D p : annotation.getPoints()){ - p.setX((p.getX()) * scale); - p.setY((p.getY()) * scale); + for(List ann : annotation.getAnnotations()){ + for(Point2D p : ann){ + p.setX((p.getX()) * scale); + p.setY((p.getY()) * scale); + } } } } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java index 4997252e5..abb0d5313 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java @@ -20,6 +20,7 @@ import pt.ua.dicoogle.sdk.mlprovider.MLlabel; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -27,6 +28,10 @@ * A bulk annotation object denotes a group of annotations from a DICOM file generated by third-party services like AI algorithms. * It follows the supplement 222 of the DICOM standard. * Annotations in a bulk annotation object share common attributes such as shape type, label, pixel origin, etc. + * Check the module C.37.1.2 Microscopy Bulk Simple Annotations for more information on annotation bulks. + * This object only maps certain parts of the standard, not the whole of it, as it is quite extensive. + * For ease of use, this object maps annotations as a list of lists. + * The standard stores all the annotations on a single list and uses a secondary list of indices to delimit the annotations. */ public class BulkAnnotation { @@ -73,9 +78,18 @@ public enum CoordinateType { private MLlabel label; - private List points; + private List> annotations; - private double confidence; + public BulkAnnotation(AnnotationType type, PixelOrigin origin){ + this.annotationType = type; + this.pixelOrigin = origin; + this.annotations = new ArrayList<>(); + } + + public BulkAnnotation(AnnotationType type, PixelOrigin origin, List> annotations){ + this(type, origin); + this.annotations = annotations; + } public PixelOrigin getPixelOrigin() { return pixelOrigin; @@ -109,40 +123,39 @@ public void setLabel(MLlabel label) { this.label = label; } - public double getConfidence() { - return confidence; + public List> getAnnotations() { + return annotations; } - public void setConfidence(double confidence) { - this.confidence = confidence; + public void setAnnotations(List> annotations) { + this.annotations = annotations; } - public List getPoints() { - return points; + public void addAnnotation(List annotation){ + this.annotations.add(annotation); } - public void setPoints(List points) { - this.points = points; + public List getBoundingBox(List points){ + return BulkAnnotation.getBoundingBox(this.annotationType, points); } - /** - * Calculate the bounding box of this annotation. + * Calculate the bounding box of an annotation from this bulk. * @return a list of 4 points, representing a rectangle that contains the provided annotation. */ - public List getBoundingBox(){ + public static List getBoundingBox(AnnotationType type, List points){ double minX = Double.MAX_VALUE; double minY = Double.MAX_VALUE; double maxX = Double.MIN_VALUE; double maxY = Double.MIN_VALUE; - switch (annotationType){ + switch (type){ case RECTANGLE: - return this.getPoints(); + return points; // In case of rectangles, annotations are already coded as the corners of the rectangle case POLYGON: case POLYLINE: - for(Point2D p : this.getPoints()){ + for(Point2D p : points){ if(p.getX() > maxX) maxX = p.getX(); if(p.getX() < minX) @@ -155,10 +168,10 @@ public List getBoundingBox(){ } break; case ELLIPSE: - minX = this.getPoints().get(0).getX(); - maxX = this.getPoints().get(1).getX(); - minY = this.getPoints().get(2).getY(); - maxY = this.getPoints().get(3).getY(); + minX = points.get(0).getX(); + maxX = points.get(1).getX(); + minY = points.get(2).getY(); + maxY = points.get(3).getY(); break; } @@ -170,8 +183,13 @@ public List getBoundingBox(){ return Arrays.asList(tl, tr, bl, br); } - public double getArea(){ - List bbox = this.getBoundingBox(); + /** + * Given a list of points from this bulk, calculate its area. + * @param points + * @return + */ + public double getArea(List points){ + List bbox = this.getBoundingBox(points); double width = bbox.get(1).getX() - bbox.get(0).getX(); double height = bbox.get(0).getY() - bbox.get(2).getY(); return Math.abs(width * height); diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ImageEntry.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ImageEntry.java index 816aa5637..c0a618268 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ImageEntry.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ImageEntry.java @@ -24,7 +24,9 @@ import java.util.Objects; /** - * Used to map the metadata of regions of interest. + * This object is used in {@see MLImageDataset} to map the image objects. + * Each entry is defined by a physical file and a set of DICOM metadata containing information such as + * the transfer syntax of the image, and other relevant information to process this image. */ public class ImageEntry { diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java deleted file mode 100644 index 50b657542..000000000 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLCSVDataset.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ - * - * This file is part of Dicoogle/dicoogle-sdk. - * - * Dicoogle/dicoogle-sdk 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. - * - * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . - */ -package pt.ua.dicoogle.sdk.mlprovider; - -import java.io.InputStream; - -public class MLCSVDataset extends MLDataset { - - private InputStream csvFile; - - public MLCSVDataset(){ - super("", MLDataType.CSV); - } -} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java index 3b42a4985..7862d3130 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java @@ -18,6 +18,23 @@ */ package pt.ua.dicoogle.sdk.mlprovider; +/** + * This enum maps the supported data types used in the MLProviderInterface. + * Data in this context always refers to data objects, labelled or unlabelled, used throughout the ML pipeline, + * for example in training or inference jobs. + * The data types listed here are not exhaustive, meaning future releases and iterations might add or remove new data types. + */ public enum MLDataType { - CSV, IMAGE, DICOM + /** + * CSV data objects refers to data that can be mapped in a tabular format. + */ + CSV, + /** + * IMAGE data objects refer explicitly to pixel data objects. + */ + IMAGE, + /** + * DICOM data objects refer to one: Study, Series or Instance. + */ + DICOM } diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java index 3958f476f..69faf0aae 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java @@ -18,10 +18,14 @@ */ package pt.ua.dicoogle.sdk.mlprovider; +/** + * ML dataset objects map a collection of labelled data, to be used in the training and generation of models. + * ML datasets have a type, defined by {@see MLDataType} and an identifier, to be used by the providers to internally manage this dataset. + * This data object is used in datastore requests to construct annotated datasets. + */ public abstract class MLDataset { protected String name; - protected MLDataType dataType; public MLDataset(String name, MLDataType dataType) { diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java index 272f61561..14ef9740e 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java @@ -22,7 +22,7 @@ /** * An ML dataset of image objects. - * Optionally an array of labels can be given to create a labelled dataset. + * Images are defined by {@see ImageEntry} objects and must have a label associated. */ public class MLImageDataset extends MLDataset { From 9ade42ea9de5ce02007b4e37e172d3c9e27dd3b0 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Sat, 25 May 2024 19:05:25 +0100 Subject: [PATCH 51/53] Implemented new getAvailableMethods and cache endpoints. --- .../ua/dicoogle/plugins/PluginController.java | 18 +++++-- .../web/servlets/mlprovider/CacheServlet.java | 51 +++++++++++++++++++ .../mlprovider/ImplementedMethodsServlet.java | 39 ++++++++++++++ .../ua/dicoogle/sdk/mlprovider/MLMethod.java | 19 +++++++ .../sdk/mlprovider/MLProviderInterface.java | 16 ++++++ 5 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CacheServlet.java create mode 100644 dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ImplementedMethodsServlet.java create mode 100644 sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLMethod.java diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java index c121c5afa..7440cfecd 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/plugins/PluginController.java @@ -33,10 +33,7 @@ import pt.ua.dicoogle.sdk.datastructs.UnindexReport; import pt.ua.dicoogle.sdk.datastructs.SearchResult; import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; -import pt.ua.dicoogle.sdk.mlprovider.MLDataset; -import pt.ua.dicoogle.sdk.mlprovider.MLInference; -import pt.ua.dicoogle.sdk.mlprovider.MLInferenceRequest; -import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; +import pt.ua.dicoogle.sdk.mlprovider.*; import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; import pt.ua.dicoogle.sdk.task.JointQueryTask; import pt.ua.dicoogle.sdk.task.Task; @@ -914,6 +911,19 @@ public Task datastore(final DatastoreRequest datasetRequest) { return prepareTask; } + public Task cache(String provider, final MLDicomDataset dataset) { + String taskName = "MLPredictionTask" + UUID.randomUUID(); + MLProviderInterface mlInterface = getMachineLearningProviderByName(provider, true); + if (mlInterface == null) { + logger.error("MLProvider with name {} not found", provider); + return null; + } + + Task task = mlInterface.cache(dataset); + task.setName(taskName); + return task; + } + // Methods for Web UI /** Retrieve all web UI plugin descriptors for the given slot id. diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CacheServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CacheServlet.java new file mode 100644 index 000000000..ef1a98b18 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CacheServlet.java @@ -0,0 +1,51 @@ +package pt.ua.dicoogle.server.web.servlets.mlprovider; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; +import org.restlet.data.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pt.ua.dicoogle.plugins.PluginController; +import pt.ua.dicoogle.sdk.mlprovider.MLDicomDataset; +import pt.ua.dicoogle.server.web.utils.ResponseUtil; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class CacheServlet extends HttpServlet { + + private static final Logger log = LoggerFactory.getLogger(DatastoreServlet.class); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String dataString = IOUtils.toString(req.getReader()); + if (dataString == null) { + resp.sendError(404, "Empty POST body"); + return; + } + + String provider = req.getParameter("provider"); + + if(provider == null || provider.isEmpty()){ + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Provider provided was invalid"); + return; + } + + ObjectMapper mapper = new ObjectMapper(); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + MLDicomDataset dataset; + try { + dataset = mapper.readValue(dataString, MLDicomDataset.class); + PluginController.getInstance().cache(provider, dataset); + } catch (Exception e) { + log.error("Error parsing json string", e); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Malformed request"); + } + } + +} diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ImplementedMethodsServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ImplementedMethodsServlet.java new file mode 100644 index 000000000..4a24192f1 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ImplementedMethodsServlet.java @@ -0,0 +1,39 @@ +package pt.ua.dicoogle.server.web.servlets.mlprovider; + +import com.fasterxml.jackson.databind.ObjectMapper; +import pt.ua.dicoogle.plugins.PluginController; +import pt.ua.dicoogle.sdk.mlprovider.MLMethod; +import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Set; + +public class ImplementedMethodsServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + ObjectMapper mapper = new ObjectMapper(); + + String provider = request.getParameter("provider"); + + if(provider == null || provider.isEmpty()){ + response.sendError(404, "Provider provided was invalid"); + return; + + } + + MLProviderInterface mlPlugin = PluginController.getInstance().getMachineLearningProviderByName(provider, true); + Set implementedMethods = mlPlugin.getImplementedMethods(); + response.setContentType("application/json"); + PrintWriter out = response.getWriter(); + mapper.writeValue(out, implementedMethods); + out.close(); + out.flush(); + } + +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLMethod.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLMethod.java new file mode 100644 index 000000000..c92299ed1 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLMethod.java @@ -0,0 +1,19 @@ +package pt.ua.dicoogle.sdk.mlprovider; + +/** + * This enum lists the available methods of the MLProvider interface. + * This is used when requesting the available methods of a provider. + * It is a ENUM instead for example of a String to restrict the possible values. + */ +public enum MLMethod { + INFER, + BULK_INFER, + DATASTORE, + CACHE, + LIST_MODELS, + CREATE_MODEL, + MODEL_INFO, + TRAIN_MODEL, + STOP_TRAINING, + DELETE_MODEL, +} diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java index e15c2eb1e..ff2887daa 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java @@ -48,6 +48,16 @@ public abstract class MLProviderInterface implements DicooglePlugin { */ public abstract void dataStore(MLDataset dataset); + /** + * This method is similar to dataStore in that is also used to upload data to a provider. + * The main difference is that this method should be used to cache DICOM objects on the provider, + * so that the provider can for example run the inference tasks locally. + * Only DICOM objects can be cached. + * @param dataset a DICOM dataset to cache. + * @return a task to the cache operation. Returns true if the dataset was cached. + */ + public abstract Task cache(MLDicomDataset dataset); + /** * This method creates a model using a specific dataset */ @@ -100,6 +110,12 @@ public abstract class MLProviderInterface implements DicooglePlugin { */ public abstract boolean isAvailable(); + /** + * This method can be used to determine which of the methods of this interface are implemented. + * @return a list of the methods implemented. + */ + public abstract Set getImplementedMethods(); + public Set getAcceptedDataTypes() { return acceptedDataTypes; } From 2115a69ab7d613ea5b8b8fc93e3f8f8e6f24f92b Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Sat, 25 May 2024 19:21:39 +0100 Subject: [PATCH 52/53] Added missing licenses. --- .../web/servlets/mlprovider/CacheServlet.java | 18 ++++++++++++++++++ .../mlprovider/ImplementedMethodsServlet.java | 18 ++++++++++++++++++ .../ua/dicoogle/sdk/mlprovider/MLMethod.java | 18 ++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CacheServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CacheServlet.java index ef1a98b18..b29fa62c5 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CacheServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CacheServlet.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.servlets.mlprovider; import com.fasterxml.jackson.databind.DeserializationFeature; diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ImplementedMethodsServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ImplementedMethodsServlet.java index 4a24192f1..67dec1960 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ImplementedMethodsServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ImplementedMethodsServlet.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle. + * + * Dicoogle/dicoogle 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. + * + * Dicoogle/dicoogle 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.server.web.servlets.mlprovider; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLMethod.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLMethod.java index c92299ed1..516a1b8f5 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLMethod.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLMethod.java @@ -1,3 +1,21 @@ +/** + * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ + * + * This file is part of Dicoogle/dicoogle-sdk. + * + * Dicoogle/dicoogle-sdk 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. + * + * Dicoogle/dicoogle-sdk 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 Dicoogle. If not, see . + */ package pt.ua.dicoogle.sdk.mlprovider; /** From 14470f6681c04b0830ad398dd3b9066492f20155 Mon Sep 17 00:00:00 2001 From: Rui-Jesus Date: Fri, 7 Jun 2024 17:34:53 +0100 Subject: [PATCH 53/53] Added missing servlets and API documentation. --- .../ua/dicoogle/server/web/DicoogleWeb.java | 2 + dicoogle_web_api.yaml | 61 ++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index b46b81826..3200676fb 100755 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java @@ -228,6 +228,8 @@ public DicoogleWeb(int port) throws Exception { createServletHandler(new TrainServlet(), "/ml/train"), createServletHandler(new ListAllModelsServlet(), "/ml/model/list"), createServletHandler(new ModelinfoServlet(), "/ml/model/info"), + createServletHandler(new CacheServlet(), "/ml/cache"), + createServletHandler(new ImplementedMethodsServlet(), "/ml/provider/methods"), webpages}; // setup the server diff --git a/dicoogle_web_api.yaml b/dicoogle_web_api.yaml index e77f24ba5..cbfaaafe1 100644 --- a/dicoogle_web_api.yaml +++ b/dicoogle_web_api.yaml @@ -1138,7 +1138,7 @@ paths: $ref: '#/components/schemas/DatastoreRequest' responses: '200': - description: The datastore request was successully processed and sent to the provider + description: The datastore request was successfully processed and sent to the provider '400': description: Malformed request /ml/infer/single: @@ -1300,6 +1300,55 @@ paths: $ref: '#/components/schemas/MLModelTrainInfo' '400': description: Malformed request + /ml/cache: + post: + tags: + - Machine Learning + summary: Cache DICOM objects on a provider + operationId: mlCache + parameters: + - in: query + name: provider + description: The name of the provider, e.g "MONAI" + required: true + schema: + type: string + requestBody: + description: Contains the DICOM files to cache + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DICOMDataset' + responses: + '200': + description: The cache request was successfully processed and sent to the provider + '400': + description: Malformed request + /ml/provider/methods: + get: + tags: + - Machine Learning + summary: Retrieves the implemented methods of the machine learning interface from a provider + operationId: mlCache + parameters: + - in: query + name: provider + description: The name of the provider, e.g "MONAI" + required: true + schema: + type: string + responses: + '200': + description: A list of the implemented methods + content: + application/json: + schema: + type: array + items: + type: string + '400': + description: Malformed request components: securitySchemes: @@ -1724,6 +1773,16 @@ components: dataset: type: object description: A map where keys represent SOPInstances and the values are annotations to upload + DICOMDataset: + type: object + properties: + level: + type: string + enum: [PATIENT, STUDY, SERIES, INSTANCE] + dimUIDs: + type: array + items: + type: string TrainRequest: type: object properties: