diff --git a/dicoogle/pom.xml b/dicoogle/pom.xml index c25a6ba15..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 @@ -376,12 +376,6 @@ 2.9.5 - - dcm4che - dcm4che-imageio - ${dcm4che.version} - - org.jdom jdom2 @@ -465,5 +459,20 @@ raven-log4j2 5.0.2 + + dcm4che + dcm4che-imageio + ${dcm4che.version} + + + org.dcm4che + dcm4che-imageio + 3.3.8 + + + org.dcm4che + dcm4che-core + 3.3.8 + diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/Main.java b/dicoogle/src/main/java/pt/ua/dicoogle/Main.java index 699a67bb1..8c54a6a02 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,13 @@ private static void LaunchDicoogle() { // Start the initial Services of Dicoogle pt.ua.dicoogle.server.ControlServices.getInstance(); + // Register Image Reader for DICOM Objects + 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 // triggers and index is updated. 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 new file mode 100644 index 000000000..44f7fdff7 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/DatastoreRequest.java @@ -0,0 +1,116 @@ +/** + * 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; +import pt.ua.dicoogle.sdk.datastructs.dim.DimLevel; +import pt.ua.dicoogle.sdk.mlprovider.MLDataType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 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 { + + /** + * The name of the ML Provider plugin to update the dataset to. + */ + private String provider; + + /** + * The type of dataset to upload + */ + private MLDataType dataType; + + private DimLevel dimLevel; + + private ArrayList uids; + + /** + * The dataset to upload. + * Each key should be a SOPInstanceUID and optionally the value should be a list of annotations. + */ + private Map> dataset; + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public MLDataType getDataType() { + return dataType; + } + + public void setDataType(MLDataType dataType) { + this.dataType = dataType; + } + + public DimLevel getDimLevel() { + return dimLevel; + } + + public void setDimLevel(DimLevel dimLevel) { + this.dimLevel = dimLevel; + } + + public ArrayList getUids() { + return uids; + } + + public void setUids(ArrayList uids) { + this.uids = uids; + } + + public Map> getDataset() { + return 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; + + switch (dataType) { + case DICOM: + if (this.dimLevel == null || this.uids == null || this.uids.isEmpty()) + return false; + case IMAGE: + 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 new file mode 100644 index 000000000..e11ee5d13 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/PrepareDatastoreTask.java @@ -0,0 +1,165 @@ +/** + * 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.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.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; + +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; + +/** + * This task processes Datastore Requests, and builds the appropriate ML Dataset. + */ +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; + + 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: + // 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"); + 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(); + HashMap dataset = new HashMap<>(); + Set classes = new HashSet<>(); + + 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(); + + int c = 0; + for (SearchResult image : results) { + + BasicDicomObject dcm = new BasicDicomObject(); + dcm.putString(Tag.TransferSyntaxUID, VR.CS, "1.2.840.10008.1.2.4.50"); + + for (BulkAnnotation annotation : entry.getValue()) { + 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()); + } + } + } + + } 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/core/mlprovider/TrainRequest.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java new file mode 100644 index 000000000..2e4de9762 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/core/mlprovider/TrainRequest.java @@ -0,0 +1,55 @@ +/** + * 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; + +/** + * 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; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public String getModelID() { + return modelID; + } + + 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/LegacyServerSettings.java b/dicoogle/src/main/java/pt/ua/dicoogle/core/settings/LegacyServerSettings.java index ba3a2c708..c81441c99 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,9 @@ 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 +1270,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..9f5c934c7 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(value = "support-wsi", defaultValue = "false") + 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 + '\'' - + ", callShutdown=" + callShutdown + ", encryptUsersFile=" + encryptUsersFile + '}'; + + ", 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 117ad5c1c..5f66ff267 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,8 @@ 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.PrepareDatastoreTask; import pt.ua.dicoogle.core.settings.ServerSettingsManager; import pt.ua.dicoogle.plugins.webui.WebUIPlugin; import pt.ua.dicoogle.plugins.webui.WebUIPluginManager; @@ -31,6 +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.*; import pt.ua.dicoogle.sdk.settings.ConfigurationHolder; import pt.ua.dicoogle.sdk.task.JointQueryTask; import pt.ua.dicoogle.sdk.task.Task; @@ -91,6 +94,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"); @@ -217,7 +223,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; @@ -361,6 +367,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 +543,16 @@ 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)) { + 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); @@ -543,7 +572,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); @@ -771,7 +799,8 @@ public void unindex(URI path, Collection indexProviders) { * and whether some of them were not found in the database * @throws IOException */ - public Task unindex(String indexProvider, Collection items, Consumer> progressCallback) throws IOException { + public Task unindex(String indexProvider, Collection items, + Consumer> progressCallback) throws IOException { logger.info("Starting unindexing procedure for {} items", items.size()); IndexerInterface indexer = null; @@ -843,6 +872,59 @@ public List indexBlocking(URI path) { return reports; } + /** + * This method orders a prediction on the selected image, using the selected provider. + * @param provider provider to use. + * @param predictionRequest + * @return the created task + */ + 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.infer(predictionRequest); + result.setName(taskName); + return result; + } + + 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(() -> { + 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); + taskManagerML.dispatch(prepareTask); + 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/DicoogleWeb.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/DicoogleWeb.java index 87dff12a7..8de35922e 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.*; @@ -52,6 +45,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; @@ -142,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(), "/roi"); + // setup the DICOM to PNG image servlet final ServletContextHandler dictags = createServletHandler(new TagsServlet(), "/dictags"); @@ -180,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"), @@ -222,7 +219,17 @@ 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 DatastoreServlet(), "/ml/datastore"), + createServletHandler(new InferServlet(), "/ml/infer/single"), + 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 CacheServlet(), "/ml/cache"), + createServletHandler(new ImplementedMethodsServlet(), "/ml/provider/methods"), webpages}; // setup the server server = new Server(port); 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..41fa3c2cb --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/ROIExtractor.java @@ -0,0 +1,296 @@ +/** + * 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; +import org.dcm4che3.imageio.plugins.dcm.DicomImageReader; +import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; +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.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; +import java.awt.*; +import java.awt.geom.Ellipse2D; +import java.awt.image.BufferedImage; +import java.io.*; +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); + private final WSICache wsiCache; + + private static final HashSet notSupportedTypes = + new HashSet<>(Collections.singletonList(BulkAnnotation.AnnotationType.POINT)); + + public ROIExtractor() { + this.wsiCache = WSICache.getInstance(); + } + + public BufferedImage extractROI(String sopInstanceUID, BulkAnnotation.AnnotationType type, + List annotation) { + DicomMetaData metaData; + try { + metaData = getDicomMetadata(sopInstanceUID); + } catch (IOException e) { + logger.error("Could not extract metadata", e); + return null; + } + return extractROI(metaData, type, annotation); + } + + public BufferedImage extractROI(DicomMetaData dicomMetaData, BulkAnnotation.AnnotationType type, + List annotation) { + + ImageReader imageReader = getImageReader(); + + if (imageReader == null) + return null; + + if (dicomMetaData == null) { + 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(type, annotation, 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 an image, return the section of the image the annotation intersects. + * It only works with rectangle type annotations. + * @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.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 = BulkAnnotation.getBoundingBox(type, 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) constructionPoints.get(0).distance(constructionPoints.get(1)) - clipX; + int annotationHeight = (int) constructionPoints.get(0).distance(constructionPoints.get(2)) - clipY; + + List> frameMatrix = getFrameMatrixFromAnnotation(descriptor, constructionPoints); + BufferedImage combined = new BufferedImage(annotationWidth, annotationHeight, BufferedImage.TYPE_INT_RGB); + Graphics g = combined.getGraphics(); + + // We need to perform clipping if annotation is not a rectangle + Shape clippingShape = null; + if (type != BulkAnnotation.AnnotationType.RECTANGLE) { + clippingShape = getClippingShape(type, annotation, constructionPoints.get(0)); + if (clippingShape != null) + g.setClip(clippingShape); + } + + 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 = (int) (intersectionPoint1.getX() - annotationPoint1.getX()); + int startY = (int) (intersectionPoint1.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 frameEndX = (int) (intersectionPoint2.getX() - framePoint1.getX()); + int frameEndY = (int) (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); + } else { + g.drawImage(bb, startX, startY, null); + } + + } + } + + g.dispose(); + return combined; + } + + /** + * 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 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.AnnotationType type, List annotation, + Point2D startingPoint) { + switch (type) { + case POLYLINE: + case POLYGON: + Polygon polygon = new Polygon(); + for (Point2D p : annotation) { + polygon.addPoint((int) (p.getX() - startingPoint.getX()), (int) (p.getY() - startingPoint.getY())); + } + return polygon; + case ELLIPSE: + double minX = annotation.get(0).getX(); + double maxX = annotation.get(1).getX(); + + 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)); + + 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/dicom/WSISopDescriptor.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/WSISopDescriptor.java new file mode 100644 index 000000000..879c073c1 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/dicom/WSISopDescriptor.java @@ -0,0 +1,92 @@ +/** + * 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; +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]); + } + +} + diff --git a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ImageServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ImageServlet.java index d65f224a3..18386b31d 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ImageServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ImageServlet.java @@ -239,8 +239,8 @@ public void onReceive(Task> e) {} providers = pc.getQueryProvidersName(true); } } - Iterator it = pc - .query(qt, providers, "SOPInstanceUID:\"" + sopInstanceUID + '"').get().iterator(); + Iterator it = + pc.query(qt, providers, "SOPInstanceUID:\"" + sopInstanceUID + '"').get().iterator(); if (!it.hasNext()) { throw new IOException("No such image of SOPInstanceUID " + sopInstanceUID); } 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..ebd0643ce --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/ROIServlet.java @@ -0,0 +1,156 @@ +/** + * 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; +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; +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; +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 ROIExtractor roiExtractor; + private final WSICache wsiCache; + + /** + * Creates ROI servlet servlet. + * + */ + public ROIServlet() { + this.roiExtractor = new ROIExtractor(); + 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"); + 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; + } + + 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); + 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); + annotation = Arrays.asList(tl, tr, bl, br); + } catch (NumberFormatException e) { + response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "ROI provided was invalid"); + return; + } + + DicomMetaData metaData = getDicomMetadata(sopInstanceUID); + + BufferedImage bi = roiExtractor.extractROI(metaData, annotationType, 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"); + } + + @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>() {}); + + DicomMetaData dicomMetaData = this.getDicomMetadata(sopInstanceUID); + + BufferedImage bi = roiExtractor.extractROI(dicomMetaData, BulkAnnotation.AnnotationType.valueOf(type), points); + + 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/management/UnindexServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/management/UnindexServlet.java index 02e14f824..ef615edbb 100644 --- a/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/management/UnindexServlet.java +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/management/UnindexServlet.java @@ -83,7 +83,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S "No arguments provided; must include either one of `uri`, `SOPInstanceUID`, `SeriesInstanceUID` or `StudyInstanceUID`"); return; } - + PluginController pc = PluginController.getInstance(); long indexed = 0; @@ -109,16 +109,14 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S List> tasks = new ArrayList<>(); if (providers == null) { - providers = pc.getIndexingPlugins(true).stream() - .map(p -> p.getName()) - .collect(Collectors.toList()); + providers = pc.getIndexingPlugins(true).stream().map(p -> p.getName()).collect(Collectors.toList()); } - for (String indexProvider: providers) { + for (String indexProvider : providers) { tasks.add(pc.unindex(indexProvider, uris, null)); } int i = 0; - for (Task task: tasks) { + for (Task task : tasks) { try { UnindexReport report = task.get(); indexed = uris.size() - report.notUnindexedFileCount(); @@ -144,9 +142,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S private static Collection resolveURIs(String[] paramUri, String[] paramSop, String[] paramSeries, String[] paramStudy) { if (paramUri != null) { - return Stream.of(paramUri) - .map(URI::create) - .collect(Collectors.toList()); + return Stream.of(paramUri).map(URI::create).collect(Collectors.toList()); } String attribute = null; if (paramSop != null) { 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 new file mode 100644 index 000000000..aa6138126 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/BulkInferServlet.java @@ -0,0 +1,40 @@ +/** + * 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; +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; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class BulkInferServlet extends HttpServlet { + + private static final Logger log = LoggerFactory.getLogger(BulkInferServlet.class); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + 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/CacheServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CacheServlet.java new file mode 100644 index 000000000..d6b517f40 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/CacheServlet.java @@ -0,0 +1,69 @@ +/** + * 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; +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/DatastoreServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java new file mode 100644 index 000000000..207c2abae --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/DatastoreServlet.java @@ -0,0 +1,62 @@ +/** + * 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; +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; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class DatastoreServlet 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; + } + + ObjectMapper mapper = new ObjectMapper(); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + DatastoreRequest datasetRequest; + try { + datasetRequest = mapper.readValue(dataString, DatastoreRequest.class); + PluginController.getInstance().datastore(datasetRequest); + } 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..fc5121dc3 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ImplementedMethodsServlet.java @@ -0,0 +1,58 @@ +/** + * 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; +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/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 new file mode 100644 index 000000000..4310d7117 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/InferServlet.java @@ -0,0 +1,353 @@ +/** + * 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; +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; +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.server.web.dicom.WSISopDescriptor; +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.ResponseUtil; +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.*; +import java.nio.file.Files; +import java.util.*; +import java.util.concurrent.ExecutionException; + +public class InferServlet extends HttpServlet { + + private static final Logger log = LoggerFactory.getLogger(InferServlet.class); + + private final ROIExtractor roiExtractor; + + private final WSICache wsiCache; + + /** + * Creates ROI servlet servlet. + * + */ + public InferServlet() { + this.wsiCache = WSICache.getInstance(); + this.roiExtractor = new ROIExtractor(); + } + + @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); + + // 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; + } + + if (!body.has("uid")) { + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "DIM UID provided was invalid"); + return; + } + + if (!body.has("provider")) { + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), + "Provider provided was invalid"); + return; + } + + if (!body.has("modelID")) { + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Model identifier was invalid"); + return; + } + + String provider = body.get("provider").asText(); + String modelID = body.get("modelID").asText(); + boolean wsi = body.has("wsi") && body.get("wsi").asBoolean(); + 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) { + + if (!body.has("points") || !body.has("type") || !body.has("baseUID")) { + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), + "Insufficient data to build request"); + return; + } + + if (level != DimLevel.INSTANCE) { + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), + "Only Instance level is supported with WSI"); + return; + } + + String baseSopInstanceUID = body.get("baseUID").asText(); + + 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, parameters, response); + + } else { + task = sendRequest(provider, modelID, level, dimUID, parameters, response); + } + + if (task == null) { + ResponseUtil.sendError(response, Status.SERVER_ERROR_INTERNAL.getCode(), + "Could not build prediction request"); + return; + } + + task.run(); + } + + private Task sendWSIRequest(String provider, String modelID, String baseSopInstanceUID, String uid, + 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(roiType, BulkAnnotation.PixelOrigin.VOLUME); + annotation.setAnnotations(Collections.singletonList(roi)); + annotation.setAnnotationType(roiType); + + 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(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.getAnnotationType(), + annotation.getAnnotations().get(0)); + predictionRequest.setRoi(bi); + Task task = PluginController.getInstance().infer(provider, predictionRequest); + if (task != null) { + task.onCompletion(() -> { + try { + MLInference prediction = task.get(); + + if (prediction == null) { + log.error("Provider returned null prediction"); + ResponseUtil.sendError(response, 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()) { + Point2D tl = annotation.getBoundingBox(annotation.getAnnotations().get(0)).get(0); + convertCoordinates(prediction, tl, scale); + } + + response.setContentType("application/json"); + PrintWriter out = response.getWriter(); + mapper.writeValue(out, prediction); + out.close(); + } catch (InterruptedException | ExecutionException e) { + log.error("Could not make prediction", e); + try { + ResponseUtil.sendError(response, Status.SERVER_ERROR_INTERNAL.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; + } + } + + 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(() -> { + try { + MLInference prediction = task.get(); + + if (prediction == null) { + log.error("Provider returned null prediction"); + ResponseUtil.sendError(response, Status.SERVER_ERROR_INTERNAL.getCode(), + "Could not make prediction"); + return; + } + + 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); + + 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 + "--"); + } + + } else { + response.setContentType("application/json"); + PrintWriter out = response.getWriter(); + mapper.writeValue(out, prediction); + out.close(); + } + + 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 { + ResponseUtil.sendError(response, Status.SERVER_ERROR_INTERNAL.getCode(), + "Could not make prediction"); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + return task; + } + + 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 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(MLInference prediction, Point2D tl, double scale) { + for (BulkAnnotation ann : prediction.getAnnotations()) { + for (List points : ann.getAnnotations()) { + for (Point2D p : points) { + 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 (List ann : annotation.getAnnotations()) { + for (Point2D p : ann) { + p.setX((p.getX()) * scale); + p.setY((p.getY()) * scale); + } + } + } +} 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..16b7c9cfa --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ListAllModelsServlet.java @@ -0,0 +1,77 @@ +/** + * 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; +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; + +/** + * 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 { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + ObjectMapper mapper = new ObjectMapper(); + + String provider = request.getParameter("provider"); + List providersResponse = new ArrayList<>(); + + if (provider != null && !provider.isEmpty()) { + MLProviderInterface mlPlugin = + PluginController.getInstance().getMachineLearningProviderByName(provider, true); + 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); + } + }); + } + + 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/ModelinfoServlet.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java new file mode 100644 index 000000000..5c5cf2e13 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/ModelinfoServlet.java @@ -0,0 +1,75 @@ +/** + * 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; +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 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; +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 + 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()) { + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Provider was not specified"); + return; + } + + if (modelID == null || modelID.isEmpty()) { + ResponseUtil.sendError(response, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Model was not specified"); + return; + } + + MLProviderInterface mlPlugin = PluginController.getInstance().getMachineLearningProviderByName(provider, true); + if (mlPlugin == null) { + ResponseUtil.sendError(response, 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 new file mode 100644 index 000000000..a4a911e2e --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/servlets/mlprovider/TrainServlet.java @@ -0,0 +1,125 @@ +/** + * 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; +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; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +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); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String dataString = IOUtils.toString(req.getReader()); + if (dataString == null) { + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "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.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.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: + log.error("Could not create training task, request is malformed"); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), + "Could not create training task, service is busy"); + break; + default: + resp.setContentType("application/json"); + try (PrintWriter out = resp.getWriter()) { + mapper.writeValue(out, trainTask); + } + } + } + } catch (JsonProcessingException e) { + log.error("Error parsing json string", e); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Malformed request"); + } + } + + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + String dataString = IOUtils.toString(req.getReader()); + if (dataString == null) { + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "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"); + ResponseUtil.sendError(resp, Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "Malformed request"); + } else { + boolean stopped = mlplugin.stopTraining(trainRequest.getTrainingTaskID()); + + if (!stopped) { + log.error("Could not stop training task"); + ResponseUtil.sendError(resp, Status.SERVER_ERROR_SERVICE_UNAVAILABLE.getCode(), + "Could not stop training task"); + } + } + } catch (JsonProcessingException 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/utils/cache/MemoryCache.java b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/MemoryCache.java new file mode 100644 index 000000000..3af9ee790 --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/MemoryCache.java @@ -0,0 +1,63 @@ +/** + * 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; +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; + } +} 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..999c15b2e --- /dev/null +++ b/dicoogle/src/main/java/pt/ua/dicoogle/server/web/utils/cache/WSICache.java @@ -0,0 +1,136 @@ +/** + * 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; +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.net.URI; +import java.security.InvalidParameterException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Cache used to store DicomMetadata objects temporarily, as they are quite heavy to build on-demand. + * Used only for WSI instances. + */ +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(); + if (!dicomProviders.isEmpty()) + queryProvider = dicomProviders.iterator().next(); + else + queryProvider = ""; + } + + public static synchronized WSICache getInstance() { + if (instance == null) { + instance = new WSICache(); + } + return instance; + } + + private class WsiDcmLoader extends CacheLoader { + + @Override + public DicomMetaData load(String sopInstanceUID) throws Exception { + + queryInterface = PluginController.getInstance().getQueryProviderByName(queryProvider, false); + + URI uri = retrieveURI(sopInstanceUID); + if (uri == null) { + throw new IllegalArgumentException("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"); + } + + dis = new DicomInputStream(sis.getInputStream()); + + 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 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 + '"'; + + + 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(); + } + +} diff --git a/dicoogle_web_api.yaml b/dicoogle_web_api.yaml index 97c795d9d..cbfaaafe1 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,323 @@ 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 successfully 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 + /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: dicoogle_auth: @@ -1434,3 +1753,93 @@ 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 + DICOMDataset: + type: object + properties: + level: + type: string + enum: [PATIENT, STUDY, SERIES, INSTANCE] + dimUIDs: + type: array + items: + type: string + 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 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 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..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,6 +18,7 @@ */ package pt.ua.dicoogle.sdk; +import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; 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..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,6 +25,7 @@ import org.restlet.resource.ServerResource; +import pt.ua.dicoogle.sdk.mlprovider.MLProviderInterface; 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 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 diff --git a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/UnindexReport.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/UnindexReport.java index 34ebcc952..a928efd5b 100644 --- a/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/UnindexReport.java +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/UnindexReport.java @@ -152,8 +152,6 @@ public long notUnindexedFileCount() { * for reasons other than the files not being found. */ public long failedFileCount() { - return this.failures.stream() - .mapToLong(f -> f.urisAffected.size()) - .sum(); + return this.failures.stream().mapToLong(f -> f.urisAffected.size()).sum(); } } 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..510dc60b7 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/BulkAnnotation.java @@ -0,0 +1,198 @@ +/** + * 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 pt.ua.dicoogle.sdk.mlprovider.MLlabel; + +import java.util.ArrayList; +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. + * 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 { + + /** + * Denotes the pixel origin of the annotations contained in this bulk. + */ + public enum PixelOrigin { + /** + * 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 { + /** + * For image relative coordinates + */ + TWO_DIMENSIONAL, + /** + * For coordinates in a Cartesian system defined by a frame of reference + */ + THREE_DIMENSIONAL + } + + private PixelOrigin pixelOrigin; + + private CoordinateType coordinateType; + + private AnnotationType annotationType; + + private MLlabel label; + + private List> annotations; + + 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; + } + + 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; + } + + public void setAnnotationType(AnnotationType annotationType) { + this.annotationType = annotationType; + } + + public MLlabel getLabel() { + return label; + } + + public void setLabel(MLlabel label) { + this.label = label; + } + + public List> getAnnotations() { + return annotations; + } + + public void setAnnotations(List> annotations) { + this.annotations = annotations; + } + + public void addAnnotation(List annotation) { + this.annotations.add(annotation); + } + + public List getBoundingBox(List points) { + return BulkAnnotation.getBoundingBox(this.annotationType, points); + } + + /** + * 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 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 (type) { + case RECTANGLE: + return points; // In case of rectangles, annotations are already coded as the corners of the rectangle + case POLYGON: + case POLYLINE: + for (Point2D p : points) { + 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 = points.get(0).getX(); + maxX = points.get(1).getX(); + minY = points.get(2).getY(); + maxY = points.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 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/datastructs/dim/ImageROI.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java new file mode 100644 index 000000000..c8a860ad3 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/ImageROI.java @@ -0,0 +1,153 @@ +/** + * 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; +import java.util.Objects; + +/** + * This object defines an Image ROI. + * 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"); + + 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 double width; + + private double height; + + private String sopInstanceUID; + + private URI uriROI; + + private FileType fileType; + + public ImageROI(String sopInstanceUID, int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + 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; + } + + 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 double getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public double 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/dim/Point2D.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java new file mode 100644 index 000000000..6871bf65e --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/dim/Point2D.java @@ -0,0 +1,63 @@ +/** + * 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; + +/** + * A point in pixel coordinates, to be used to identify coordinates in pixel space. + */ +public class Point2D { + + private double x; + private double y; + + public Point2D() { + x = 0; + y = 0; + } + + public Point2D(int x, int y) { + this.x = x; + this.y = y; + } + + public Point2D(double x, double y) { + this.x = x; + this.y = y; + } + + 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 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/datastructs/wsi/WSIFrame.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java new file mode 100644 index 000000000..a9b1c681b --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/datastructs/wsi/WSIFrame.java @@ -0,0 +1,89 @@ +/** + * 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 { + + 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/mlprovider/ImageEntry.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ImageEntry.java new file mode 100644 index 000000000..2b53ec5d2 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/ImageEntry.java @@ -0,0 +1,72 @@ +/** + * 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; + +/** + * 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 { + + 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/MLDataType.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java new file mode 100644 index 000000000..7862d3130 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataType.java @@ -0,0 +1,40 @@ +/** + * 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; + +/** + * 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 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 new file mode 100644 index 000000000..69faf0aae --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDataset.java @@ -0,0 +1,51 @@ +/** + * 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; + +/** + * 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) { + 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 new file mode 100644 index 000000000..006e298cc --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLDicomDataset.java @@ -0,0 +1,61 @@ +/** + * 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; + +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) { + super("", MLDataType.DICOM); + this.level = level; + dimUIDs = new ArrayList<>(); + } + + public MLDicomDataset(DimLevel level, List dimUIDs) { + super("", MLDataType.DICOM); + 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/MLException.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLException.java new file mode 100644 index 000000000..d74fa4c54 --- /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/MLImageDataset.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java new file mode 100644 index 000000000..14ef9740e --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLImageDataset.java @@ -0,0 +1,57 @@ +/** + * 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.HashMap; + +/** + * An ML dataset of image objects. + * Images are defined by {@see ImageEntry} objects and must have a label associated. + */ +public class MLImageDataset extends MLDataset { + + private HashMap dataset; + + private boolean multiClass; + + 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) { + 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/MLInference.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java new file mode 100644 index 000000000..dc0d7fa26 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInference.java @@ -0,0 +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.mlprovider; + +import com.fasterxml.jackson.annotation.JsonIgnore; +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. + * It can contain a set of metrics, annotations and a DICOM SEG file. + */ +public class MLInference { + + private Map metrics; + + private String version; + + private List annotations; + + @JsonIgnore + private String resourcesFolder; + + @JsonIgnore + private Path dicomSEG; + + public MLInference() { + this.metrics = new HashMap<>(); + this.annotations = new ArrayList<>(); + } + + public Map getMetrics() { + return metrics; + } + + public void setMetrics(Map metrics) { + this.metrics = metrics; + } + + public List getAnnotations() { + return annotations; + } + + public void setAnnotations(List annotations) { + this.annotations = annotations; + } + + public String getResourcesFolder() { + return resourcesFolder; + } + + public void setResourcesFolder(String resourcesFolder) { + this.resourcesFolder = resourcesFolder; + } + + public Path getDicomSEG() { + return dicomSEG; + } + + public void setDicomSEG(Path dicomSEG) { + this.dicomSEG = dicomSEG; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public boolean hasResults() { + return metrics != null || annotations != null; + } +} 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 new file mode 100644 index 000000000..ac6e7b8cb --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLInferenceRequest.java @@ -0,0 +1,104 @@ +/** + * 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; + +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; + + private DimLevel level; + + private String dimID; + + private BufferedImage roi; + + 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() { + 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; + } + + 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/MLMethod.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLMethod.java new file mode 100644 index 000000000..3d5bf36e6 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLMethod.java @@ -0,0 +1,28 @@ +/** + * 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; + +/** + * 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/MLModel.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java new file mode 100644 index 000000000..9d0fb4c14 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModel.java @@ -0,0 +1,143 @@ +/** + * 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; +import java.util.*; + +public class MLModel implements Serializable { + + private String name; + + private String id; + + private String type; + + private String description; + + private Date creationDate; + + private MLDataType dataType; + + private Set labels; + + 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; + this.type = ""; + labels = new TreeSet<>(); + parameters = new ArrayList<>(); + dataType = MLDataType.IMAGE; + creationDate = new Date(); + processMagnification = 0; + } + + public MLModel(String name, String id, List parameters) { + this(name, id); + this.parameters = parameters; + } + + 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 getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + 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 MLDataType getDataType() { + return dataType; + } + + public void setDataType(MLDataType dataType) { + this.dataType = dataType; + } + + public Set getLabels() { + return labels; + } + + 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); + } + + public void addLabel(MLlabel label) { + this.labels.add(label); + } + + public int getProcessMagnification() { + return processMagnification; + } + + public void setProcessMagnification(int processMagnification) { + this.processMagnification = processMagnification; + } +} 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..5a027809d --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelParameter.java @@ -0,0 +1,138 @@ +/** + * 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; + } +} 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..a2052c817 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLModelTrainInfo.java @@ -0,0 +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.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/MLProvider.java b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProvider.java new file mode 100644 index 000000000..b415f5eb2 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProvider.java @@ -0,0 +1,62 @@ +/** + * 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; +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 new file mode 100644 index 000000000..88b96c1d2 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLProviderInterface.java @@ -0,0 +1,122 @@ +/** + * 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; +import pt.ua.dicoogle.sdk.task.Task; + +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 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 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 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 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 + */ + public abstract MLModel createModel(); + + /** + * 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 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 deletes a model + */ + public abstract void deleteModel(); + + /** + * Order a prediction over a single object. + * The object can be a series instance, a sop instance or a 2D/3D ROI. + * + * @param inferRequest object that defines this inference request + */ + public abstract Task infer(MLInferenceRequest inferRequest); + + /** + * This method makes a bulk inference request using the selected model + */ + 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(); + + /** + * 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; + } +} 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..3c51a372f --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLTrainTask.java @@ -0,0 +1,80 @@ +/** + * 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; + +/** + * 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 { + + /** + * 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, TrainingStatus status) { + this.taskID = taskID; + this.status = status; + } + + private String taskID; + + private TrainingStatus status; + + public String getTaskID() { + return taskID; + } + + public void setTaskID(String taskID) { + this.taskID = taskID; + } + + public TrainingStatus getStatus() { + return status; + } + + public void setStatus(TrainingStatus status) { + this.status = status; + } +} 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..64fb2efd4 --- /dev/null +++ b/sdk/src/main/java/pt/ua/dicoogle/sdk/mlprovider/MLlabel.java @@ -0,0 +1,150 @@ +/** + * 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; +import java.util.Objects; + +/** + * 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, Serializable { + + 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 name; + + /** + * 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() { + this.description = "unknown"; + this.codingSchemeDesignator = CodingSchemeDesignator.DCM; + this.codeValue = "333333"; + this.codeMeaning = "unknown"; + this.color = "#000000"; + } + + public MLlabel(String name) { + this(); + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + 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; + if (o == null || getClass() != o.getClass()) + return false; + MLlabel mLlabel = (MLlabel) o; + return name.equals(mLlabel.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public int compareTo(MLlabel o) { + return o.getName().compareTo(this.getName()); + } +} 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();