Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New/image worker interface #590

Closed
10 changes: 10 additions & 0 deletions dicoogle/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -466,5 +466,15 @@
<artifactId>raven-log4j2</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>org.dcm4che</groupId>
<artifactId>dcm4che-imageio</artifactId>
<version>3.3.7</version>
</dependency>
<dependency>
<groupId>org.dcm4che</groupId>
<artifactId>dcm4che-core</artifactId>
<version>3.3.7</version>
</dependency>
</dependencies>
</project>
8 changes: 8 additions & 0 deletions dicoogle/src/main/java/pt/ua/dicoogle/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -172,6 +175,11 @@ private static void LaunchDicoogle() {
// Start the initial Services of Dicoogle
pt.ua.dicoogle.server.ControlServices.getInstance();

// Register Image Reader for DICOM Objects
IIORegistry.getDefaultInstance().registerServiceProvider(new DicomImageReaderSpi());
ImageIO.setUseCache(false);
System.setProperty("dcm4che.useImageIOServiceRegistry", "true");

// Launch Async Index
// It monitors a folder, and when a file is touched an event
// triggers and index is updated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,6 @@ public StorageInterface getStorageByName(String name, boolean onlyEnabled) {
return null;
}


public JointQueryTask queryAll(JointQueryTask holder, final String query, final Object... parameters) {
List<String> providers = this.getQueryProvidersName(true);
return query(holder, providers, query, parameters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,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.*;
Expand Down Expand Up @@ -140,6 +133,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");

Expand Down Expand Up @@ -178,7 +174,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"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
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.sdk.datastructs.wsi.WSISopDescriptor;
import pt.ua.dicoogle.server.web.utils.cache.WSICache;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ROIExtractor {

private static final Logger logger = LoggerFactory.getLogger(ROIExtractor.class);
private final WSICache wsiCache;

public ROIExtractor() {
this.wsiCache = WSICache.getInstance();
}

public BufferedImage extractROI(String sopInstanceUID, BulkAnnotation bulkAnnotation) {

ImageReader imageReader = getImageReader();

if(imageReader == null)
return null;

DicomMetaData dicomMetaData;
try {
dicomMetaData = getDicomMetadata(sopInstanceUID);
} catch (IOException e) {
logger.error("Error reading DICOM file", e);
return null;
}

if(dicomMetaData == null){
return null;
}

DicomImageReadParam param;
try{
imageReader.setInput(dicomMetaData);
param = (DicomImageReadParam) imageReader.getDefaultReadParam();
} catch (Exception e){
logger.error("Error setting image reader", e);
return null;
}

WSISopDescriptor descriptor = new WSISopDescriptor();
descriptor.extractData(dicomMetaData.getAttributes());

try {
return getROIFromAnnotation(bulkAnnotation, descriptor, imageReader, param);
} catch (IllegalArgumentException e) {
logger.error("Error writing ROI", e);
}

return null;
}

private static ImageReader getImageReader() {
Iterator<ImageReader> iter = ImageIO.getImageReadersByFormatName("DICOM");
while (iter.hasNext()) {
ImageReader reader = iter.next();
if (reader instanceof DicomImageReader)
return reader;
}
return null;
}

/**
* Given an annotation and the details of the image, this method returns the frames that intersect with the annotation.
* This method is meant for WSI images that are split into frames.
* @param descriptor the WSI descriptor
* @param annotation The annotation to intersect
* @return a 2D matrix of frames that intersect this annotation.
*/
private List<List<WSIFrame>> getFrameMatrixFromAnnotation(WSISopDescriptor descriptor, BulkAnnotation annotation) {

//Number of tiles along the x direction, number of columns in the frame matrix
int nx_tiles = (int) Math.ceil((descriptor.getTotalPixelMatrixColumns() * 1.0) / descriptor.getTileWidth());

//Number of tiles along the y direction, number of rows in the frame matrix
int ny_tiles = (int) Math.ceil((descriptor.getTotalPixelMatrixRows() * 1.0) / descriptor.getTileHeight());

List<List<WSIFrame>> matrix = new ArrayList<>();

switch (annotation.getAnnotationType()) {
case RECTANGLE:
// Calculate the starting position of the annotation in frame coordinates
int x_c = annotation.getPoints().get(0).getX() / descriptor.getTileWidth();
int y_c = annotation.getPoints().get(0).getY() / descriptor.getTileHeight();

//Annotation is completely out of bounds, no intersection possible
if(x_c > nx_tiles || y_c > ny_tiles)
return matrix;

// Calculate the ending position of the annotation in frame coordinates
int x_e = annotation.getPoints().get(3).getX() / descriptor.getTileWidth();
int y_e = annotation.getPoints().get(3).getY() / descriptor.getTileHeight();

//Annotation might be out of bonds, adjust that
if(x_e > (nx_tiles - 1))
x_e = nx_tiles - 1;

if(y_e > (ny_tiles - 1))
y_e = ny_tiles - 1;

for (int i = y_c; i <= y_e ; i++) {
matrix.add(new ArrayList<>());
for (int j = x_c; j <= x_e; j++) {
WSIFrame frame = new WSIFrame(descriptor.getTileWidth(), descriptor.getTileHeight(), j, i, i * nx_tiles + j);
matrix.get(i).add(frame);
}
}
break;
}

return matrix;
}

/**
* Given an annotation and an image, return the section of the image the annotation intersects.
* It only works with rectangle type annotations.
* @param annotation the annotation to intersect
* @param descriptor descriptor of the WSI pyramid, contains information about the dimmensions of the image.
* @param imageReader
* @param param
* @return the intersection of the annotation on the image.
* @throws IllegalArgumentException when the annotation is not one of the supported types.
*/
private BufferedImage getROIFromAnnotation(BulkAnnotation annotation, WSISopDescriptor descriptor, ImageReader imageReader, DicomImageReadParam param) throws IllegalArgumentException {
if(annotation.getAnnotationType() != BulkAnnotation.AnnotationType.RECTANGLE){
throw new IllegalArgumentException("Trying to build a ROI without a rectangle annotation");
}

Point2D annotationPoint1 = annotation.getPoints().get(0);
Point2D annotationPoint2 = annotation.getPoints().get(3);

int clipX = Math.max(annotationPoint2.getX() - descriptor.getTotalPixelMatrixColumns(), 0);
int clipY = Math.max(annotationPoint2.getY() - descriptor.getTotalPixelMatrixRows(), 0);

int annotationWidth = (int) annotation.getPoints().get(0).distance(annotation.getPoints().get(1)) - clipX;
int annotationHeight = (int) annotation.getPoints().get(0).distance(annotation.getPoints().get(2)) - clipY;

List<List<WSIFrame>> frameMatrix = getFrameMatrixFromAnnotation(descriptor, annotation);
BufferedImage combined = new BufferedImage(annotationWidth, annotationHeight, BufferedImage.TYPE_INT_RGB);
Graphics g = combined.getGraphics();

for (List<WSIFrame> matrix : frameMatrix) {
for (WSIFrame frame : matrix) {
BufferedImage bb;
try {
bb = imageReader.read(frame.getFrameIndex(), param);
} catch (IOException e) {
logger.error("Error building ROI, skipping entry", e);
continue;
}

//Calculate intersections between ROI and frame
Point2D framePoint1 = new Point2D(frame.getX() * descriptor.getTileWidth(), frame.getY() * descriptor.getTileHeight());
Point2D framePoint2 = new Point2D(framePoint1.getX() + bb.getWidth(), framePoint1.getY() + bb.getHeight());
Point2D intersectionPoint1 = new Point2D(Math.max(annotationPoint1.getX(), framePoint1.getX()), Math.max(annotationPoint1.getY(), framePoint1.getY()));
Point2D intersectionPoint2 = new Point2D(Math.min(annotationPoint2.getX(), framePoint2.getX()), Math.min(annotationPoint2.getY(), framePoint2.getY()));

int startX = intersectionPoint1.getX() - annotationPoint1.getX();
int startY = intersectionPoint1.getY() - annotationPoint1.getY();

int endX = intersectionPoint2.getX() - annotationPoint1.getX();
int endY = intersectionPoint2.getY() - annotationPoint1.getY();

int frameStartX = intersectionPoint1.getX() - framePoint1.getX();
int frameStartY = intersectionPoint1.getY() - framePoint1.getY();

int frameEndX = intersectionPoint2.getX() - framePoint1.getX();
int frameEndY = intersectionPoint2.getY() - framePoint1.getY();

int deltaX = frameEndX - frameStartX;
int deltaY = frameEndY - frameStartY;

//This means that the frame is smaller than the intersection area
//It can happen when we are on the edge of the image and the tiles do not have the dimensions stated in the DICOM file
if (deltaX > bb.getWidth()) {
endX = frameEndX - bb.getWidth();
frameEndX = bb.getWidth();
}

if (deltaY > bb.getHeight()) {
endY = frameEndY - bb.getHeight();
frameEndY = bb.getHeight();
}

g.drawImage(bb, startX, startY,
endX, endY, frameStartX, frameStartY, frameEndX, frameEndY, null);

}
}

g.dispose();
return combined;
}

private DicomMetaData getDicomMetadata(String sop) throws IOException{
return wsiCache.get(sop);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package pt.ua.dicoogle.server.web.servlets;

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 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;

/**
* Creates ROI servlet servlet.
*
*/
public ROIServlet() {
this.roiExtractor = new ROIExtractor();
}

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

String sopInstanceUID = request.getParameter("uid");
String x = request.getParameter("x");
String y = request.getParameter("y");
String width = request.getParameter("width");
String height = request.getParameter("height");

if(sopInstanceUID == null || sopInstanceUID.isEmpty()){
response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "SOPInstanceUID provided was invalid");
return;
}

if(x == null || x.isEmpty() || y == null || y.isEmpty() || width == null || width.isEmpty() || height == null || height.isEmpty()){
response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "ROI provided was invalid");
return;
}

BulkAnnotation annotation;
try{
int nX = Integer.parseInt(x);
int nY = Integer.parseInt(y);
int nWidth = Integer.parseInt(width);
int nHeight = Integer.parseInt(height);
annotation = new BulkAnnotation();
Point2D tl = new Point2D(nX, nY);
Point2D tr = new Point2D(nX + nWidth, nY);
Point2D bl = new Point2D(nX, nY + nHeight);
Point2D br = new Point2D(nX + nWidth, nY + nHeight);
List<Point2D> points = new ArrayList<>();
points.add(tl); points.add(tr); points.add(bl); points.add(br);
annotation.setPoints(points);
annotation.setAnnotationType(BulkAnnotation.AnnotationType.RECTANGLE);
} catch (NumberFormatException e){
response.sendError(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), "ROI provided was invalid");
return;
}

BufferedImage bi = roiExtractor.extractROI(sopInstanceUID, annotation);

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");
}

}
Loading