Java tutorial
/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package oct.analysis.application.dat; import ij.process.ByteProcessor; import ij.process.FloatProcessor; import java.awt.Component; import java.awt.Container; import java.awt.Point; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.IntStream; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.ProgressMonitor; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import oct.analysis.application.OCTImagePanel; import oct.analysis.application.OCTLine; import oct.analysis.application.OCTSelection; import oct.util.Segmentation; import oct.util.Util; import oct.util.ip.ImageOperation; import oct.util.ip.SharpenOperation; import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.differentiation.DerivativeStructure; import org.apache.commons.math3.analysis.differentiation.FiniteDifferencesDifferentiator; import org.apache.commons.math3.analysis.differentiation.UnivariateDifferentiableFunction; import org.apache.commons.math3.analysis.interpolation.LoessInterpolator; import org.apache.commons.math3.analysis.interpolation.UnivariateInterpolator; /** * * @author Brandon */ public class OCTAnalysisManager { /* property change support */ public static final String PROP_FOVEA_CENTER_X_POSITION = "foveaCenterXPosition"; private transient final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); private double xscale; private double yscale; private int micronsBetweenSelections = 0; private OCT oct = null; private OCTMode displayMode = OCTMode.LOG; //default display mode of image is assumed to be a Log OCT image private int foveaCenterXPosition = -1; private AnalysisMode analysisMode = null; private OCTImagePanel imgPanel; private OCTAnalysisManager() { } public static OCTAnalysisManager getInstance() { return OCTAnalysisManagerHolder.INSTANCE; } private static class OCTAnalysisManagerHolder { private static final OCTAnalysisManager INSTANCE = new OCTAnalysisManager(); } public double getYscale() { return yscale; } public void setYscale(double yscale) { this.yscale = yscale; } public void setImjPanel(OCTImagePanel imjPanel) { this.imgPanel = imjPanel; } public OCTImagePanel getImgPanel() { return imgPanel; } public AnalysisMode getAnalysisMode() { return analysisMode; } public void setAnalysisMode(AnalysisMode analysisMode) { this.analysisMode = analysisMode; } /** * Obtain the X coordinate (relative to the OCT image) of the center of the * fovea. * * @return the X coordinate of the fovea relative to the OCT image supplied */ public int getFoveaCenterXPosition() { return foveaCenterXPosition; } /** * Get the distance from the supplied X position on the OCT to the fovea (in * microns). * * @param xPos X position on OCT * @return distance between xPos and center of fovea in microns, returns -1 * if the center of fovea hasn't be identified yet. */ public double getDistanceFromFovea(int xPos) { if (foveaCenterXPosition < 0) { return -1D; } else { return (double) Math.abs(xPos - foveaCenterXPosition) * xscale; } } /** * Define where the center of the fovea is. * * @param foveaCenterXPosition */ public void setFoveaCenterXPosition(int foveaCenterXPosition) { int oldval = this.foveaCenterXPosition; this.foveaCenterXPosition = foveaCenterXPosition; propertyChangeSupport.firePropertyChange(PROP_FOVEA_CENTER_X_POSITION, oldval, foveaCenterXPosition); } /** * This method will take care of interacting with the user in determining * where the fovea is within the OCT. It first lets the user inspect the * automatically identified locations where the fovea may be and then choose * the selection that is at the fovea. If none of the automated findings are * at the fovea the user has the option to manual specify it's location. * Finally, the chosen X coordinate (within the OCT) of the fovea is set in * the manager and can be obtained via the getFoveaCenterXPosition getter. * * @param fullAutoMode find the fovea automatically without user input when * true, otherwise find the fovea in semi-automatic mode involving user * interaction */ public void findCenterOfFovea(boolean fullAutoMode) throws InterruptedException, ExecutionException { //disable clicking other components while processing by enabling glass pane JFrame topFrame = (JFrame) SwingUtilities.getWindowAncestor(imgPanel); Component glassPane = topFrame.getGlassPane(); glassPane.setVisible(true); //monitor progress of finding the fovea ProgressMonitor pm = new ProgressMonitor(imgPanel, "Analyzing OCT for fovea...", "", 0, 100); pm.setMillisToDecideToPopup(0); pm.setMillisToPopup(100); pm.setProgress(0); FoveaFindingTask fvtask = new FoveaFindingTask(!fullAutoMode, glassPane); fvtask.addPropertyChangeListener((PropertyChangeEvent evt) -> { if ("progress".equals(evt.getPropertyName())) { int progress1 = (Integer) evt.getNewValue(); pm.setProgress(progress1); } }); fvtask.execute(); } /** * Using the previously supplied values for the number of microns between * selections calculate the distance from the center of the fovea based on * the selection order distance away from the fovea. That is, the distance * returned by this method will indicate how far from the center of the * fovea a selection should be placed based on the number of selections * between it and the fovea. * * <p> * For example, a multiplier of 1 indicates the first selection set a * distance away from the fovea. A multiplier of 2 will be the second * selection encountered when counting selections outward from the fovea. * This continues for any multiplier, irrespective of direction. * </p> * * @param multiplier * @return */ public int getNumPixelFromFovea(int multiplier) { return (oct == null) ? -1 : (int) Math.round((double) (micronsBetweenSelections * multiplier) * (1D / xscale)); } public int getMicronsBetweenSelections() { return micronsBetweenSelections; } public void setMicronsBetweenSelections(int micronsBetweenSelections) { this.micronsBetweenSelections = micronsBetweenSelections; } public OCT getOct() { return oct; } public void setOct(OCT oct) { this.oct = oct; } public void setScale(double axialLength, double nominalScanWidth, int octWidth) { double scanLength = (nominalScanWidth * axialLength) / 24D; setXscale(((scanLength * 1000D) / (double) octWidth)); } public void setXscale(double xscale) { this.xscale = xscale; } /** * Changes the mode with which the OCT should be rendered. Calling this * method will cause the panel to redraw the OCT and any analysis artifacts * using the new mode setting. * * @param mode th mode to change the display of the OCT image */ public void setOCTMode(OCTMode mode) { this.displayMode = mode; } public double getXscale() { return xscale; } public OCTMode getDisplayMode() { return displayMode; } /** * This method returns the OCT image according to the currently set OCT mode * and image operations. * * @return */ public BufferedImage getOctImage() { BufferedImage modeOCT = (displayMode == OCTMode.LOG) ? oct.getLogOctImage() : oct.getLinearOctImage(); FloatProcessor fp = new ByteProcessor(modeOCT).convertToFloatProcessor(); fp.snapshot(); ImageOperationManager.getInstance().getActiveOperationList().forEach(imop -> { imop.performOperation(fp); }); return fp.getBufferedImage(); } /** * Segment the four major layers of the retina from the Log OCT image. An * optional {@code ImageOperation} can be applied to the image before the * segmentation is performed, to help improve segmentation performance. * * @param optionalOp an ImageOperation to apply before segmenting an image, * or null (indicating tat no operations should be performed before * segmenting the OCT) * @return segmentation of the OCT */ public Segmentation getSegmentation(ImageOperation optionalOp) { //segmentation and image operations can only be done on 8-bit gray xscale images, using the OCT we ensure //the image is in useable format which handles this upon creation BufferedImage segImg; if (optionalOp != null) { //apply supplied operation before segmenting OCT FloatProcessor tmpFp = new ByteProcessor(oct.getLogOctImage()).convertToFloatProcessor(); tmpFp.snapshot();//need to create a snapshot before any operations can be performed on image optionalOp.performOperation(tmpFp); segImg = tmpFp.getBufferedImage(); } else { segImg = oct.getLogOctImage(); } return new Segmentation(segImg, 1); } /** * This method grabs the current OCT and sharpens it using a radius (sigma) * of 15 and a weight factor of the supplied value. The sharpened image is * then returned. * * @return sharpened image */ public BufferedImage getSharpenedOctImage(double sigma, float weight) { FloatProcessor tmpFp = new ByteProcessor(oct.getLogOctImage()).convertToFloatProcessor(); tmpFp.snapshot();//need to create a snapshot before any operations can be performed on image new SharpenOperation(sigma, weight).performOperation(tmpFp); return tmpFp.getBufferedImage(); } public List<LinePoint> findAbsoluteDiff(UnivariateFunction fa, UnivariateFunction fb, int minX, int maxX) { return IntStream.rangeClosed(minX, maxX) .mapToObj(x -> new LinePoint(x, Math.abs(fa.value(x) - fb.value(x)))).collect(Collectors.toList()); } public List<LinePoint> findAbsoluteDiff(List<LinePoint> fa, List<LinePoint> fb) { ListIterator<LinePoint> faIter, fbIter; if (fa.get(0).getX() == fb.get(0).getX()) { faIter = fa.listIterator(); fbIter = fb.listIterator(); } else if (fa.get(0).getX() > fb.get(0).getX()) { faIter = fa.listIterator(); fbIter = fb.listIterator(fa.get(0).getX() - fb.get(0).getX()); } else { faIter = fa.listIterator(fb.get(0).getX() - fa.get(0).getX()); fbIter = fb.listIterator(); } LinkedList<LinePoint> retLine = new LinkedList<>(); while (faIter.hasNext() && fbIter.hasNext()) { LinePoint pointA = faIter.next(); LinePoint pointB = fbIter.next(); retLine.add(new LinePoint(pointA.getX(), Math.abs(pointA.getY() - pointB.getY()))); } return retLine; } private class FoveaFindingTask extends SwingWorker<List<Integer>, Integer> { private final boolean interactiveMode; private final Component glassPane; public FoveaFindingTask(boolean interactiveMode, Component glassWindow) { this.interactiveMode = interactiveMode; this.glassPane = glassWindow; } @Override protected List<Integer> doInBackground() throws Exception { return findPotentialFoveaSites(); } private List<Integer> findPotentialFoveaSites() { //find the fovea since it hasn't been found/defined yet UnivariateInterpolator interpolator = new LoessInterpolator(0.1, 0); setProgress(5); Segmentation octSeg = getSegmentation(new SharpenOperation(15, 0.6F)); setProgress(50); double[][] ilmSeg = Util .getXYArraysFromPoints(new ArrayList<>(octSeg.getSegment(Segmentation.ILM_SEGMENT))); UnivariateFunction ilmInterp = interpolator.interpolate(ilmSeg[0], ilmSeg[1]); LinkedList<LinePoint> ilmLine = new LinkedList<>(); IntStream.range(0, oct.getImageWidth() - 1).forEach((int i) -> { ilmLine.add(new LinePoint(i, ilmInterp.value(i))); }); double[][] brmSeg = Util .getXYArraysFromPoints(new ArrayList<>(octSeg.getSegment(Segmentation.BrM_SEGMENT))); UnivariateFunction brmInterp = interpolator.interpolate(brmSeg[0], brmSeg[1]); LinkedList<LinePoint> brmLine = new LinkedList<>(); IntStream.range(0, oct.getImageWidth() - 1).forEach((int i) -> { brmLine.add(new LinePoint(i, brmInterp.value(i))); }); imgPanel.addDrawnLine(ilmLine, brmLine); double[][] diffLine = Util.getXYArraysFromLinePoints( findAbsoluteDiff(brmInterp, ilmInterp, 0, oct.getLinearOctImage().getWidth() - 1)); UnivariateFunction diffInerp = interpolator.interpolate(diffLine[0], diffLine[1]); FiniteDifferencesDifferentiator differ = new FiniteDifferencesDifferentiator(4, 0.25); UnivariateDifferentiableFunction difFunc = differ.differentiate(diffInerp); setProgress(80); /* * collect the first derivative at each pixel in the image */ int numFreeVariablesInFunction = 1; int order = 1; DerivativeStructure xd; DerivativeStructure yd; ArrayList<LinePoint> firstDeriv = new ArrayList<>(oct.getLinearOctImage().getWidth() - 1); IntStream.range(0, oct.getLinearOctImage().getWidth() - 1).forEach((int i) -> { firstDeriv.add(new LinePoint(0, 0)); }); for (int xRealValue = 1; xRealValue <= oct.getLinearOctImage().getWidth() - 2; xRealValue++) { xd = new DerivativeStructure(numFreeVariablesInFunction, order, 0, xRealValue); yd = difFunc.value(xd); firstDeriv.set(xRealValue, new LinePoint(xRealValue, yd.getPartialDerivative(1))); } setProgress(90); List<LinePoint> peaks = Util.findMaxAndMins(firstDeriv); LinePoint prevPeak = null; LinkedList<Diff> diffs = new LinkedList<>(); for (LinePoint curPeak : peaks) { if (prevPeak != null) { diffs.add(new Diff(prevPeak, curPeak)); } prevPeak = curPeak; } Diff maxDiff = diffs.stream().max(Comparator.comparingDouble((Diff diff) -> diff.getYDiff())).get(); double sign = Math.signum(maxDiff.getLinePoint1().getY()); int signChangeXPos = maxDiff.getLinePoint1().getX() + 1; while (sign == Math.signum(firstDeriv.get(signChangeXPos).getY())) { signChangeXPos++; } //add the most likely fovea position to list first int foveaCenterXPosition = (Math.abs(firstDeriv.get(signChangeXPos).getY()) < Math .abs(firstDeriv.get(signChangeXPos - 1).getY())) ? signChangeXPos : signChangeXPos - 1; LinkedList<Integer> positionList = new LinkedList<>(); positionList.add(foveaCenterXPosition); //find other zero crossings for (LinePoint curPeak : peaks) { if (prevPeak != null && !prevPeak.equals(maxDiff.getLinePoint1())) { sign = Math.signum(prevPeak.getY()); signChangeXPos = prevPeak.getX() + 1; try { while (sign == Math.signum(firstDeriv.get(signChangeXPos).getY())) { signChangeXPos++; } //add other possible fovea site to list foveaCenterXPosition = (Math.abs(firstDeriv.get(signChangeXPos).getY()) < Math .abs(firstDeriv.get(signChangeXPos - 1).getY())) ? signChangeXPos : signChangeXPos - 1; positionList.add(foveaCenterXPosition); } catch (IndexOutOfBoundsException ie) { //caused because the first derivative line is shorter than the original //fail sillently } } prevPeak = curPeak; } setProgress(100); return positionList; } @Override protected void done() { SelectionLRPManager selMngr = SelectionLRPManager.getInstance(); //grab findings List<Integer> sites = null; try { sites = get(); } catch (InterruptedException | ExecutionException ex) { Logger.getLogger(OCTAnalysisManager.class.getName()).log(Level.SEVERE, null, ex); } //process based of of fovea finding user interaction mode if (interactiveMode) { //since the glass pane is blocking user interation with the UI add //a listener that will only pass through clicks over the image panel JFrame topFrame = (JFrame) SwingUtilities.getWindowAncestor(imgPanel); glassPane.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { /* With the glasspane up we have to listen for click events and forward them if they fit with this process. In this case we will only forward clicks to the JPanel that displays the OCT. */ Point glassPanePoint = e.getPoint(); Container container = topFrame.getContentPane(); Point containerPoint = SwingUtilities.convertPoint(glassPane, glassPanePoint, container); if (containerPoint.y > 0) { //The mouse event is probably over the content pane. //Find out exactly which component it's over. Component component = SwingUtilities.getDeepestComponentAt(container, containerPoint.x, containerPoint.y); if ((component != null) && (component.equals(imgPanel))) { //process where user clicked over the JPanel that displays the OCT to the panel to process. Point componentPoint = SwingUtilities.convertPoint(glassPane, glassPanePoint, component); //determine if click was over the OCT image if (imgPanel.coordinateOverlapsOCT(componentPoint)) { //determine if click was over one of the possible fovea selections OCTSelection selection = selMngr.getSelection( imgPanel.translatePanelPointToOctPoint(componentPoint), false); if (selection == null) { //user decided that none of the automated selections was correct and made their own selection //check that new selection is what they want //clear all selections from being displayed selMngr.removeSelections(true); //translate component coordinates to OCT coordinates Point p = imgPanel.translatePanelPointToOctPoint(componentPoint); selection = new OCTLine(p.x, 0, oct.getImageHeight(), SelectionType.FOVEAL, "Fovea", false); selMngr.addOrUpdateSelection(selection); imgPanel.repaint(); if (JOptionPane.showConfirmDialog(imgPanel, "Is this the location of the center of the fovea? If not hit 'No' and click on the image where you believe the center of the fovea resides.", "Center of Fovea?", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { glassPane.removeMouseListener(this); glassPane.setVisible(false); setFoveaCenterXPosition(selection.getXPositionOnOct()); selMngr.removeSelections(true); imgPanel.repaint(); } } else { glassPane.removeMouseListener(this); glassPane.setVisible(false); setFoveaCenterXPosition(selection.getXPositionOnOct()); selMngr.removeSelections(true); imgPanel.repaint(); } } } } } }); //draw potential fovea selections to screen for user to choose from if (sites == null) { sites = new LinkedList<>(); } if (sites.isEmpty()) { sites.add(oct.getImageWidth() / 2); } sites.forEach(x -> { selMngr.addOrUpdateSelection(new OCTLine(x, 0, oct.getImageHeight(), SelectionType.FOVEAL, "Potential Fovea @ " + x, false)); }); imgPanel.repaint(); //notify user of how they can select the selection that is the fovea or make a new selection JOptionPane.showMessageDialog(imgPanel, "Please select (by clicking one of the gray boxes at the top of a selection)\n" + " the selection that you believe is the fovea. If none of the\n" + " presented seletions look like the location of the fovea click\n" + " anywhere on the image to assign the location manually.", "Select Fovea", JOptionPane.INFORMATION_MESSAGE); } else { //auto identification process returns most likely result without user interaction if (sites == null || sites.isEmpty()) { setFoveaCenterXPosition(oct.getImageWidth() / 2); } else { setFoveaCenterXPosition(sites.get(0)); } } } } /** * Add PropertyChangeListener. * * @param listener */ public void addPropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); } /** * Remove PropertyChangeListener. * * @param listener */ public void removePropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(listener); } }