/* * Copyright 2010-2015 Institut Pasteur. * * This file is part of Icy. * * Icy 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. * * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>. */ package plugins.kernel.roi.roi2d; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.Ellipse2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.awt.image.IndexColorModel; import java.lang.ref.WeakReference; import java.util.Arrays; import org.w3c.dom.Node; import icy.canvas.IcyCanvas; import icy.canvas.IcyCanvas2D; import icy.common.CollapsibleEvent; import icy.gui.inspector.RoisPanel; import icy.image.ImageUtil; import icy.main.Icy; import icy.painter.VtkPainter; import icy.resource.ResourceUtil; import icy.roi.BooleanMask2D; import icy.roi.ROI; import icy.roi.ROI2D; import icy.roi.ROIEvent; import icy.roi.edit.Area2DChangeROIEdit; import icy.sequence.Sequence; import icy.system.thread.ThreadUtil; import icy.type.point.Point5D; import icy.type.point.Point5D.Double; import icy.type.rectangle.Rectangle3D; import icy.util.EventUtil; import icy.util.GraphicsUtil; import icy.util.ShapeUtil; import icy.util.StringUtil; import icy.util.XMLUtil; import icy.vtk.IcyVtkPanel; import icy.vtk.VtkUtil; import plugins.kernel.canvas.VtkCanvas; import vtk.vtkActor; import vtk.vtkImageData; import vtk.vtkInformation; import vtk.vtkPolyData; import vtk.vtkPolyDataMapper; import vtk.vtkProp; /** * ROI Area type.<br> * Use a bitmap mask internally for fast boolean mask operation.<br> * * @author Stephane */ public class ROI2DArea extends ROI2D { protected static final float DEFAULT_CURSOR_SIZE = 15f; // we want to keep a static brush protected static final Ellipse2D brush = new Ellipse2D.Double(); // protected static final Point2D.Double cursorPosition = new Point2D.Double(); protected static Color brushColor = Color.red; protected static float brushSize = DEFAULT_CURSOR_SIZE; public class ROI2DAreaPainter extends ROI2DPainter implements VtkPainter, Runnable { /** * @deprecated Use {@link #getOpacity()} instead. */ @Deprecated public static final float CONTENT_ALPHA = 0.3f; private static final float MIN_CURSOR_SIZE = 0.3f; private static final float MAX_CURSOR_SIZE = 500f; // VTK 3D objects protected vtkPolyData outline; protected vtkPolyDataMapper outlineMapper; protected vtkActor outlineActor; protected vtkInformation vtkInfo; protected vtkPolyData polyData; protected vtkPolyDataMapper polyMapper; protected vtkActor surfaceActor; // 3D internal protected boolean needRebuild; protected double scaling[]; protected WeakReference<VtkCanvas> canvas3d; protected int lastBuildPosZ; // internal protected final Point2D brushPosition; public ROI2DAreaPainter() { super(); brushPosition = new Point2D.Double(); outline = null; outlineMapper = null; outlineActor = null; vtkInfo = null; polyData = null; polyMapper = null; surfaceActor = null; scaling = new double[3]; Arrays.fill(scaling, 1d); needRebuild = true; canvas3d = new WeakReference<VtkCanvas>(null); lastBuildPosZ = getZ(); } @Override protected void finalize() throws Throwable { super.finalize(); // release allocated VTK resources if (surfaceActor != null) surfaceActor.Delete(); if (polyMapper != null) polyMapper.Delete(); if (polyData != null) { polyData.GetPointData().GetScalars().Delete(); polyData.GetPointData().Delete(); polyData.Delete(); } if (outlineActor != null) { outlineActor.SetPropertyKeys(null); outlineActor.Delete(); } if (vtkInfo != null) { vtkInfo.Remove(VtkCanvas.visibilityKey); vtkInfo.Delete(); } if (outlineMapper != null) outlineMapper.Delete(); if (outline != null) { outline.GetPointData().GetScalars().Delete(); outline.GetPointData().Delete(); outline.Delete(); } }; protected void initVtkObjects() { outline = VtkUtil.getOutline(0d, 1d, 0d, 1d, 0d, 1d); outlineMapper = new vtkPolyDataMapper(); outlineMapper.SetInputData(outline); outlineActor = new vtkActor(); outlineActor.SetMapper(outlineMapper); // disable picking on the outline outlineActor.SetPickable(0); // and set it to wireframe representation outlineActor.GetProperty().SetRepresentationToWireframe(); // use vtkInformations to store outline visibility state (hacky) vtkInfo = new vtkInformation(); vtkInfo.Set(VtkCanvas.visibilityKey, 0); // VtkCanvas use this to restore correctly outline visibility flag outlineActor.SetPropertyKeys(vtkInfo); polyMapper = new vtkPolyDataMapper(); surfaceActor = new vtkActor(); surfaceActor.SetMapper(polyMapper); final Color col = getColor(); final double r = col.getRed() / 255d; final double g = col.getGreen() / 255d; final double b = col.getBlue() / 255d; // set actors color outlineActor.GetProperty().SetColor(r, g, b); surfaceActor.GetProperty().SetColor(r, g, b); } /** * rebuild VTK objects (called only when VTK canvas is selected). */ protected void rebuildVtkObjects() { final VtkCanvas canvas = canvas3d.get(); // canvas was closed if (canvas == null) return; final IcyVtkPanel vtkPanel = canvas.getVtkPanel(); // canvas was closed if (vtkPanel == null) return; final Sequence seq = canvas.getSequence(); // nothing to update if (seq == null) return; // get previous polydata object final vtkPolyData previousPolyData = polyData; // get VTK binary image from ROI mask final vtkImageData imageData = VtkUtil.getBinaryImageData(ROI2DArea.this, seq.getSizeZ(), canvas.getPositionT()); // adjust spacing imageData.SetSpacing(scaling[0], scaling[1], scaling[2]); // get VTK polygon data representing the surface of the binary image polyData = VtkUtil.getSurfaceFromImage(imageData, 0.5d); // get bounds final Rectangle3D bounds = getBounds5D().toRectangle3D(); // apply scaling on bounds bounds.setX(bounds.getX() * scaling[0]); bounds.setSizeX(bounds.getSizeX() * scaling[0]); bounds.setY(bounds.getY() * scaling[1]); bounds.setSizeY(bounds.getSizeY() * scaling[1]); if (bounds.isInfiniteZ()) { bounds.setZ(0); bounds.setSizeZ(seq.getSizeZ() * scaling[2]); lastBuildPosZ = -1; } else { lastBuildPosZ = getZ(); bounds.setZ(bounds.getZ() * scaling[2]); bounds.setSizeZ(1d * scaling[2]); } // actor can be accessed in canvas3d for rendering so we need to synchronize access vtkPanel.lock(); try { // update outline data VtkUtil.setOutlineBounds(outline, bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(), bounds.getMaxY(), bounds.getMinZ(), bounds.getMaxZ(), canvas); outlineMapper.Update(); // update polygon data from image polyMapper.SetInputData(polyData); polyMapper.Update(); // update actor position surfaceActor.SetPosition(bounds.getX(), bounds.getY(), bounds.getZ()); // release image data imageData.GetPointData().GetScalars().Delete(); imageData.GetPointData().Delete(); imageData.Delete(); // release previous polydata if (previousPolyData != null) { previousPolyData.GetPointData().GetScalars().Delete(); previousPolyData.GetPointData().Delete(); previousPolyData.Delete(); } } finally { vtkPanel.unlock(); } // update color and others properties updateVtkDisplayProperties(); } protected void updateVtkDisplayProperties() { if (surfaceActor == null) return; final VtkCanvas cnv = canvas3d.get(); final Color col = getDisplayColor(); final double r = col.getRed() / 255d; final double g = col.getGreen() / 255d; final double b = col.getBlue() / 255d; // final double strk = getStroke(); // final float opacity = getOpacity(); final IcyVtkPanel vtkPanel = (cnv != null) ? cnv.getVtkPanel() : null; // we need to lock canvas as actor can be accessed during rendering if (vtkPanel != null) vtkPanel.lock(); try { // set actors color outlineActor.GetProperty().SetColor(r, g, b); if (isSelected()) { outlineActor.GetProperty().SetRepresentationToWireframe(); outlineActor.SetVisibility(1); vtkInfo.Set(VtkCanvas.visibilityKey, 1); } else { outlineActor.GetProperty().SetRepresentationToPoints(); outlineActor.SetVisibility(0); vtkInfo.Set(VtkCanvas.visibilityKey, 0); } surfaceActor.GetProperty().SetColor(r, g, b); // opacity here is about ROI content, global opacity is handled by Layer // surfaceActor.GetProperty().SetOpacity(opacity); setVtkObjectsColor(col); } finally { if (vtkPanel != null) vtkPanel.unlock(); } // need to repaint painterChanged(); } protected void setVtkObjectsColor(Color color) { if (outline != null) VtkUtil.setPolyDataColor(outline, color, canvas3d.get()); if (polyData != null) VtkUtil.setPolyDataColor(polyData, color, canvas3d.get()); } protected void updateVtkObjectsBounds() { final VtkCanvas canvas = canvas3d.get(); // canvas was closed if (canvas == null) return; final IcyVtkPanel vtkPanel = canvas.getVtkPanel(); // canvas was closed if (vtkPanel == null) return; final Sequence seq = canvas.getSequence(); // nothing to update if (seq == null) return; final Rectangle3D bounds = getBounds5D().toRectangle3D(); // apply scaling on bounds bounds.setX(bounds.getX() * scaling[0]); bounds.setSizeX(bounds.getSizeX() * scaling[0]); bounds.setY(bounds.getY() * scaling[1]); bounds.setSizeY(bounds.getSizeY() * scaling[1]); if (bounds.isInfiniteZ()) { bounds.setZ(0); bounds.setSizeZ(seq.getSizeZ() * scaling[2]); } else { bounds.setZ(bounds.getZ() * scaling[2]); bounds.setSizeZ(1d * scaling[2]); } // actor can be accessed in canvas3d for rendering so we need to synchronize access vtkPanel.lock(); try { // update outline position VtkUtil.setOutlineBounds(outline, bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(), bounds.getMaxY(), bounds.getMinZ(), bounds.getMaxZ(), canvas); outlineMapper.Update(); // update actor position surfaceActor.SetPosition(bounds.getX(), bounds.getY(), bounds.getZ()); } finally { vtkPanel.unlock(); } } void updateCursor() { final double x = brushPosition.getX(); final double y = brushPosition.getY(); brush.setFrameFromDiagonal(x - brushSize, y - brushSize, x + brushSize, y + brushSize); // if roi selected (cursor displayed) --> painter changed if (isSelected()) painterChanged(); } /** * Returns the brush position. */ public Point2D getBrushPosition() { return (Point) brushPosition.clone(); } /** * Set the brush position. */ public void setBrushPosition(Point2D position) { if (!brushPosition.equals(position)) { brushPosition.setLocation(position); updateCursor(); } } /** * @deprecated Use {@link #getBrushPosition()} instead. */ @Deprecated public Point2D getCursorPosition() { return getBrushPosition(); } /** * @deprecated Use {@link #setBrushPosition(Point2D)} instead. */ @Deprecated public void setCursorPosition(Point2D position) { setBrushPosition(position); } /** * Returns the brush size. */ public float getBrushSize() { return brushSize; } /** * Sets the brush size. */ public void setBrushSize(float value) { final float adjValue = Math.max(Math.min(value, MAX_CURSOR_SIZE), MIN_CURSOR_SIZE); if (brushSize != adjValue) { brushSize = adjValue; updateCursor(); } } /** * @deprecated Use {@link #getBrushSize()} instead */ @Deprecated public float getCursorSize() { return getBrushSize(); } /** * @deprecated Use {@link #setBrushSize(float)} instead */ @Deprecated public void setCursorSize(float value) { setBrushSize(value); } /** * Returns the brush color */ public Color getBrushColor() { return brushColor; } /** * Sets the brush color */ public void setBrushColor(Color value) { if (!brushColor.equals(value)) { brushColor = value; painterChanged(); } } /** * @deprecated Use {@link #getBrushColor()} instead */ @Deprecated public Color getCursorColor() { return getBrushColor(); } /** * @deprecated Use {@link #setBrushColor(Color)} instead */ @Deprecated public void setCursorColor(Color value) { setBrushColor(value); } public void addToMask(Point2D pos) { setBrushPosition(pos); updateMask(brush, false); } public void removeFromMask(Point2D pos) { setBrushPosition(pos); updateMask(brush, true); } @Override public void painterChanged() { updateMaskColor(true); super.painterChanged(); } @Override protected boolean updateFocus(InputEvent e, Point5D imagePoint, IcyCanvas canvas) { // specific VTK canvas processing if (canvas instanceof VtkCanvas) { // mouse is over the ROI actor ? --> focus the ROI final boolean focused = (surfaceActor != null) && (surfaceActor == ((VtkCanvas) canvas).getPickedObject()); setFocused(focused); return focused; } return super.updateFocus(e, imagePoint, canvas); } @Override public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.keyPressed(e, imagePoint, canvas); // edition not supported on VtkCanvas if (canvas instanceof VtkCanvas) return; // not yet consumed and ROI editable... if (!e.isConsumed() && !isReadOnly()) { // then process it here if (isActiveFor(canvas)) { ROI2DArea.this.beginUpdate(); try { switch (e.getKeyChar()) { case '+': if (isSelected()) { setBrushSize(getBrushSize() * 1.1f); e.consume(); } break; case '-': if (isSelected()) { setBrushSize(getBrushSize() * 0.9f); e.consume(); } break; } } finally { ROI2DArea.this.endUpdate(); } } } } @Override public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.mousePressed(e, imagePoint, canvas); // edition not supported on VtkCanvas if (canvas instanceof VtkCanvas) return; // we need it if (imagePoint == null) return; // not yet consumed, ROI editable, selected and not focused... if (!e.isConsumed() && !isReadOnly() && isSelected() && !isFocused()) { // then process it here if (isActiveFor(canvas)) { // keep trace of roi changes from user mouse action roiModifiedByMouse = false; // save current ROI undoSave = getBooleanMask(true); ROI2DArea.this.beginUpdate(); try { // left button action if (EventUtil.isLeftMouseButton(e)) { // add point first addToMask(imagePoint.toPoint2D()); roiModifiedByMouse = true; e.consume(); } // right button action else if (EventUtil.isRightMouseButton(e)) { // remove point removeFromMask(imagePoint.toPoint2D()); roiModifiedByMouse = true; e.consume(); } } finally { ROI2DArea.this.endUpdate(); } } } } @Override public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.mouseReleased(e, imagePoint, canvas); // update only on release as it can be long if (!isReadOnly()) { if (roiModifiedByMouse) { if (boundsNeedUpdate) { if (optimizeBounds()) { roiChanged(true); // empty ? delete ROI if (bounds.isEmpty()) { ROI2DArea.this.remove(); // nothing more to do return; } } } final Sequence sequence = canvas.getSequence(); // add undo operation try { if ((sequence != null) && (undoSave != null)) sequence.addUndoableEdit(new Area2DChangeROIEdit(ROI2DArea.this, undoSave)); } catch (OutOfMemoryError err) { // can't create undo operation, show message and clear undo manager System.out.println("Warning: not enough memory to create undo point for ROI area change"); sequence.clearUndoManager(); } // release save undoSave = null; roiModifiedByMouse = false; } } } @Override public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // provide backward compatibility if (imagePoint != null) mouseClick(e, imagePoint.toPoint2D(), canvas); else mouseClick(e, (Point2D) null, canvas); // not yet consumed... if (!e.isConsumed()) { // and process ROI stuff now if (isActiveFor(canvas)) { final int clickCount = e.getClickCount(); // double click if (clickCount == 2) { // focused ? if (isFocused()) { // show in ROI panel final RoisPanel roiPanel = Icy.getMainInterface().getRoisPanel(); if (roiPanel != null) { roiPanel.scrollTo(ROI2DArea.this); // consume event e.consume(); } } } } } } @Override public void mouseMove(MouseEvent e, Double imagePoint, IcyCanvas canvas) { // send event to parent first super.mouseMove(e, imagePoint, canvas); // edition not supported on VtkCanvas if (canvas instanceof VtkCanvas) return; // we need it if (imagePoint == null) return; // not yet consumed, ROI editable and selected... if (!e.isConsumed() && !isReadOnly() && isSelected()) { // then process it here if (isActiveFor(canvas)) { setBrushPosition(imagePoint.toPoint2D()); } } } @Override public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.mouseDrag(e, imagePoint, canvas); // edition not supported on VtkCanvas if (canvas instanceof VtkCanvas) return; // we need it if (imagePoint == null) return; // not yet consumed, ROI editable and selected... if (!e.isConsumed() && !isReadOnly() && isSelected()) { // then process it here if (isActiveFor(canvas)) { ROI2DArea.this.beginUpdate(); try { // left button action if (EventUtil.isLeftMouseButton(e)) { // add point first addToMask(imagePoint.toPoint2D()); roiModifiedByMouse = true; e.consume(); } // right button action else if (EventUtil.isRightMouseButton(e)) { // remove point removeFromMask(imagePoint.toPoint2D()); roiModifiedByMouse = true; e.consume(); } } finally { ROI2DArea.this.endUpdate(); } } } } @Override public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas) { super.paint(g, sequence, canvas); if (isActiveFor(canvas)) { // ROI selected ? draw cursor if (isSelected() && !isFocused() && !isReadOnly()) drawCursor(g, sequence, canvas); } } /** * Draw the ROI itself */ @Override public void drawROI(Graphics2D g, Sequence sequence, IcyCanvas canvas) { if (canvas instanceof IcyCanvas2D) { // not supported if (g == null) return; final Rectangle bounds = getBounds(); // trivial paint optimization final boolean shapeVisible = GraphicsUtil.isVisible(g, bounds); if (shapeVisible) { final Graphics2D g2 = (Graphics2D) g.create(); final boolean small; // disable LOD when creating the ROI if (isCreating()) small = false; else { final double scale = Math.max(Math.abs(canvas.getScaleX()), Math.abs(canvas.getScaleY())); small = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight()) < LOD_SMALL; } // simplified draw if (small) { g2.setColor(getDisplayColor()); g2.drawImage(imageMask, null, bounds.x, bounds.y); } // normal draw else { final AlphaComposite prevAlpha = (AlphaComposite) g2.getComposite(); float newAlpha = prevAlpha.getAlpha() * getOpacity(); newAlpha = Math.min(1f, newAlpha); newAlpha = Math.max(0f, newAlpha); // show content with an alpha factor g2.setComposite(prevAlpha.derive(newAlpha)); // draw mask g2.drawImage(imageMask, null, bounds.x, bounds.y); // restore alpha g2.setComposite(prevAlpha); // draw border if (isSelected()) { g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d))); g2.setColor(getDisplayColor()); g2.draw(bounds); } else { // outside border g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d))); g2.setColor(Color.black); g2.draw(bounds); // internal border g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke))); g2.setColor(getDisplayColor()); g2.draw(bounds); } } g2.dispose(); } // for (Point2D pt : getBooleanMask().getEdgePoints()) // g2.drawRect((int) pt.getX(), (int) pt.getY(), 1, 1); } if (canvas instanceof VtkCanvas) { // 3D canvas final VtkCanvas cnv = (VtkCanvas) canvas; // update reference if needed if (canvas3d.get() != cnv) canvas3d = new WeakReference<VtkCanvas>(cnv); // initialize VTK objects if not yet done if (surfaceActor == null) initVtkObjects(); // FIXME : need a better implementation final double[] s = cnv.getVolumeScale(); // scaling changed ? if (!Arrays.equals(scaling, s)) { // update scaling scaling = s; // need rebuild needRebuild = true; } // need to rebuild 3D data structures ? if (needRebuild) { // request rebuild 3D objects ThreadUtil.runSingle(this); needRebuild = false; } } } /** * draw the ROI cursor */ protected void drawCursor(Graphics2D g, Sequence sequence, IcyCanvas canvas) { if (canvas instanceof IcyCanvas2D) { // not supported if (g == null) return; final Rectangle bounds = brush.getBounds(); // trivial paint optimization final boolean shapeVisible = GraphicsUtil.isVisible(g, bounds); if (shapeVisible) { final Graphics2D g2 = (Graphics2D) g.create(); final boolean tiny; // disable LOD when creating the ROI if (isCreating()) tiny = false; else { final double scale = Math.max(canvas.getScaleX(), canvas.getScaleY()); tiny = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight()) < LOD_TINY; } // simplified draw if (tiny) { // cursor color g2.setColor(brushColor); // draw cursor g2.fill(brush); } // normal draw else { final AlphaComposite prevAlpha = (AlphaComposite) g2.getComposite(); float newAlpha = prevAlpha.getAlpha() * getOpacity() * 2f; newAlpha = Math.min(1f, newAlpha); newAlpha = Math.max(0f, newAlpha); // show cursor with an alpha factor g2.setComposite(prevAlpha.derive(newAlpha)); // draw cursor border g2.setColor(Color.black); g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke))); g2.draw(brush); // draw cursor g2.setColor(brushColor); g2.fill(brush); } g2.dispose(); } } } @Override public vtkProp[] getProps() { // initialize VTK objects if not yet done if (surfaceActor == null) initVtkObjects(); return new vtkActor[] {surfaceActor, outlineActor}; } @Override public void run() { rebuildVtkObjects(); } } public static final String ID_BOUNDS_X = "boundsX"; public static final String ID_BOUNDS_Y = "boundsY"; public static final String ID_BOUNDS_W = "boundsW"; public static final String ID_BOUNDS_H = "boundsH"; // protected static final String ID_BOOLMASK_LEN = "boolMaskLen"; public static final String ID_BOOLMASK_DATA = "boolMaskData"; /** * image containing the mask */ protected BufferedImage imageMask; /** * rectangle bounds */ protected Rectangle bounds; /** * internals */ protected final byte[] red; protected final byte[] green; protected final byte[] blue; protected IndexColorModel colorModel; protected byte[] maskData; // 0 = false, 1 = true protected double translateX, translateY; protected Color previousColor; protected boolean boundsNeedUpdate; protected boolean roiModifiedByMouse; protected BooleanMask2D undoSave; /** * Create a ROI2D Area type from the specified {@link BooleanMask2D}. */ public ROI2DArea() { super(); bounds = new Rectangle(); boundsNeedUpdate = false; roiModifiedByMouse = false; undoSave = null; translateX = 0d; translateY = 0d; // prepare indexed image red = new byte[256]; green = new byte[256]; blue = new byte[256]; // keep trace of previous color previousColor = getDisplayColor(); // set colormap red[1] = (byte) previousColor.getRed(); green[1] = (byte) previousColor.getGreen(); blue[1] = (byte) previousColor.getBlue(); // classic 8 bits indexed with one transparent color (index = 0) colorModel = new IndexColorModel(8, 256, red, green, blue, 0); // create default image imageMask = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, colorModel); // get data pointer maskData = ((DataBufferByte) imageMask.getRaster().getDataBuffer()).getData(); // set icon (default name is defined by getDefaultName()) setIcon(ResourceUtil.ICON_ROI_AREA); } /** * @deprecated Use {@link #ROI2DArea(Point5D)} instead */ @Deprecated public ROI2DArea(Point2D position, boolean cm) { this(position); } /** * Create a ROI2D Area type with a single point. */ public ROI2DArea(Point2D position) { this(); // add current point to mask addBrush(position); } /** * Generic constructor for interactive mode. */ public ROI2DArea(Point5D position) { this(position.toPoint2D()); } /** * Create a ROI2D Area type from the specified {@link BooleanMask2D}. */ public ROI2DArea(BooleanMask2D mask) { this(); setAsBooleanMask(mask); } /** * Create a copy of the specified 2D Area ROI */ public ROI2DArea(ROI2DArea area) { super(); bounds = new Rectangle(); boundsNeedUpdate = false; roiModifiedByMouse = false; undoSave = null; translateX = 0d; translateY = 0d; // prepare indexed image red = new byte[256]; green = new byte[256]; blue = new byte[256]; // keep trace of previous color previousColor = getDisplayColor(); // set colormap red[1] = (byte) previousColor.getRed(); green[1] = (byte) previousColor.getGreen(); blue[1] = (byte) previousColor.getBlue(); // classic 8 bits indexed with one transparent color (index = 0) colorModel = new IndexColorModel(8, 256, red, green, blue, 0); imageMask = new BufferedImage(area.bounds.width, area.bounds.height, BufferedImage.TYPE_BYTE_INDEXED, colorModel); maskData = ((DataBufferByte) imageMask.getRaster().getDataBuffer()).getData(); System.arraycopy(area.maskData, 0, maskData, 0, maskData.length); bounds.setBounds(area.bounds); // set icon (default name is defined by getDefaultName()) setIcon(ResourceUtil.ICON_ROI_AREA); } @Override public String getDefaultName() { return "Area2D"; } void addToBounds(Rectangle bnd) { final Rectangle newBounds; if (bounds.isEmpty()) newBounds = new Rectangle(bnd); else { newBounds = new Rectangle(bounds); newBounds.add(bnd); } try { // update image to the new bounds updateImage(newBounds); } catch (Error E) { // perhaps a "out of memory" error, restore back old bounds System.err.println("can't enlarge ROI, no enough memory !"); } } /** * @deprecated Use {@link #optimizeBounds()} instead. */ @Deprecated public void optimizeBounds(boolean removeIfEmpty) { optimizeBounds(); if (removeIfEmpty && bounds.isEmpty()) remove(); } /** * Returns true if the ROI is empty (the mask does not contains any point). */ @Override public boolean isEmpty() { if (bounds.isEmpty()) return true; final byte[] data = maskData; for (byte b : data) if (b != 0) return false; return true; } /** * Optimize the bounds size to the minimum surface which still include all mask<br> * You should call it after consecutive remove operations. */ public boolean optimizeBounds() { // bounds are being updated boundsNeedUpdate = false; final byte[] data; final Rectangle bnds; // recompute bound from the mask data synchronized (this) { data = maskData; bnds = bounds; } final int sizeX = bnds.width; final int sizeY = bnds.height; int minX, minY, maxX, maxY; minX = maxX = minY = maxY = 0; boolean empty = true; int offset = 0; for (int y = 0; y < sizeY; y++) { for (int x = 0; x < sizeX; x++) { if (data[offset++] != 0) { if (empty) { minX = maxX = x; minY = maxY = y; empty = false; } else { if (x < minX) minX = x; else if (x > maxX) maxX = x; if (y < minY) minY = y; else if (y > maxY) maxY = y; } } } } if (!empty) // update image to the new bounds return updateImage(new Rectangle(bnds.x + minX, bnds.y + minY, (maxX - minX) + 1, (maxY - minY) + 1)); // update to empty bounds return updateImage(new Rectangle(bnds.x, bnds.y, 0, 0)); } /** * @deprecated Use {@link #getDisplayColor()} instead. */ @Deprecated public Color getMaskColor() { return getOverlay().getDisplayColor(); } void updateMaskColor(boolean rebuildImage) { final Color color = getOverlay().getDisplayColor(); // roi color changed ? if (!previousColor.equals(color)) { // update colormap red[1] = (byte) color.getRed(); green[1] = (byte) color.getGreen(); blue[1] = (byte) color.getBlue(); colorModel = new IndexColorModel(8, 256, red, green, blue, 0); // recreate image (so the new colormodel takes effect) if (rebuildImage) imageMask = ImageUtil.createIndexedImage(imageMask.getWidth(), imageMask.getHeight(), colorModel, maskData); // set to new color previousColor = color; } } /** * Returns the internal image mask. */ public BufferedImage getImageMask() { return imageMask; } boolean updateImage(Rectangle newBnd) { final byte[] data; final Rectangle bnds; synchronized (this) { data = maskData; bnds = bounds; } // copy rectangle final Rectangle oldBounds = new Rectangle(bnds); final Rectangle newBounds = new Rectangle(newBnd); // replace to oldBounds origin oldBounds.translate(-bnds.x, -bnds.y); newBounds.translate(-bnds.x, -bnds.y); // dimension changed ? if ((oldBounds.width != newBounds.width) || (oldBounds.height != newBounds.height)) { final BufferedImage newImageMask; final byte[] newMaskData; if (!newBounds.isEmpty()) { // new bounds not empty newImageMask = new BufferedImage(newBounds.width, newBounds.height, BufferedImage.TYPE_BYTE_INDEXED, colorModel); newMaskData = ((DataBufferByte) newImageMask.getRaster().getDataBuffer()).getData(); final Rectangle intersect = newBounds.intersection(oldBounds); if (!intersect.isEmpty()) { int offSrc = 0; int offDst = 0; // adjust offset in source mask if (intersect.x > 0) offSrc += intersect.x; if (intersect.y > 0) offSrc += intersect.y * oldBounds.width; // adjust offset in destination mask if (newBounds.x < 0) offDst += -newBounds.x; if (newBounds.y < 0) offDst += -newBounds.y * newBounds.width; // preserve data for (int j = 0; j < intersect.height; j++) { System.arraycopy(data, offSrc, newMaskData, offDst, intersect.width); offSrc += oldBounds.width; offDst += newBounds.width; } } } else { // new bounds empty --> use single pixel image to avoid NPE newImageMask = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, colorModel); newMaskData = ((DataBufferByte) newImageMask.getRaster().getDataBuffer()).getData(); } synchronized (this) { // set new image and maskData imageMask = newImageMask; maskData = newMaskData; bounds = newBnd; } return true; } return false; } /** * Set the value of the specified point.<br> * Don't forget to call optimizeBounds() after consecutive remove operation to refresh the mask * bounds. */ public void setPoint(int x, int y, boolean value) { final byte[] data; final Rectangle bnds; if (value) { // set point in mask addToBounds(new Rectangle(x, y, 1, 1)); synchronized (this) { data = maskData; bnds = bounds; } // set color depending remove or adding to mask data[(x - bnds.x) + ((y - bnds.y) * bnds.width)] = 1; // notify roi changed roiChanged(true); } else { synchronized (this) { data = maskData; bnds = bounds; } if (bnds.contains(x, y)) { // remove point from mask data[(x - bnds.x) + ((y - bnds.y) * bnds.width)] = 0; // mark that bounds need to be updated boundsNeedUpdate = true; // notify roi changed roiChanged(true); } } } /** * @deprecated Use {@link #setPoint(int, int, boolean)} instead. */ @Deprecated public void updateMask(int x, int y, boolean remove) { setPoint(x, y, !remove); } /** * Add the specified {@link ROI2DArea} content to this ROI2DArea */ public void add(ROI2DArea roi) { final Rectangle boundsToAdd = roi.getBounds(); final byte[] maskToAdd = roi.maskData; // update bounds (this update the image dimension if needed) addToBounds(boundsToAdd); int offDst, offSrc; final byte[] data; final Rectangle bnds; synchronized (this) { data = maskData; bnds = bounds; } // calculate offset offDst = ((boundsToAdd.y - bnds.y) * bnds.width) + (boundsToAdd.x - bnds.x); offSrc = 0; for (int y = 0; y < boundsToAdd.height; y++) { for (int x = 0; x < boundsToAdd.width; x++) if (maskToAdd[offSrc++] != 0) data[offDst + x] = 1; offDst += bnds.width; } // notify roi changed roiChanged(true); } /** * Add the specified {@link BooleanMask2D} content to this ROI2DArea */ public void add(BooleanMask2D mask) { final Rectangle boundsToAdd = mask.bounds; final boolean[] maskToAdd = mask.mask; // update bounds (this update the image dimension if needed) addToBounds(boundsToAdd); int offDst, offSrc; final byte[] data; final Rectangle bnds; synchronized (this) { data = maskData; bnds = bounds; } // calculate offset offDst = ((boundsToAdd.y - bnds.y) * bnds.width) + (boundsToAdd.x - bnds.x); offSrc = 0; for (int y = 0; y < boundsToAdd.height; y++) { for (int x = 0; x < boundsToAdd.width; x++) if (maskToAdd[offSrc++]) data[offDst + x] = 1; offDst += bnds.width; } // notify roi changed roiChanged(true); } /** * Exclusively add the specified {@link ROI2DArea} content to this ROI2DArea: * * <pre> * mask1 xor mask2 = result * * ################ ################ * ############## ############## ## ## * ############ ############ #### #### * ########## ########## ###### ###### * ######## ######## ################ * ###### ###### ###### ###### * #### #### #### #### * ## ## ## ## * </pre> */ public void exclusiveAdd(ROI2DArea roi) { final Rectangle boundsToXAdd = roi.getBounds(); final byte[] maskToXAdd = roi.maskData; // update bounds (this update the image dimension if needed) addToBounds(boundsToXAdd); int offDst, offSrc; final byte[] data; final Rectangle bnds; synchronized (this) { data = maskData; bnds = bounds; } // calculate offset offDst = ((boundsToXAdd.y - bnds.y) * bnds.width) + (boundsToXAdd.x - bnds.x); offSrc = 0; for (int y = 0; y < boundsToXAdd.height; y++) { for (int x = 0; x < boundsToXAdd.width; x++) if (maskToXAdd[offSrc++] != 0) data[offDst + x] ^= 1; offDst += bnds.width; } // optimize bounds if (isUpdating()) boundsNeedUpdate = true; else optimizeBounds(); // notify roi changed roiChanged(true); } /** * Exclusively add the specified {@link BooleanMask2D} content to this ROI2DArea: * * <pre> * mask1 xor mask2 = result * * ################ ################ * ############## ############## ## ## * ############ ############ #### #### * ########## ########## ###### ###### * ######## ######## ################ * ###### ###### ###### ###### * #### #### #### #### * ## ## ## ## * </pre> */ public void exclusiveAdd(BooleanMask2D mask) { final Rectangle boundsToXAdd = mask.bounds; final boolean[] maskToXAdd = mask.mask; // update bounds (this update the image dimension if needed) addToBounds(boundsToXAdd); int offDst, offSrc; final byte[] data; final Rectangle bnds; synchronized (this) { data = maskData; bnds = bounds; } // calculate offset offDst = ((boundsToXAdd.y - bnds.y) * bnds.width) + (boundsToXAdd.x - bnds.x); offSrc = 0; for (int y = 0; y < boundsToXAdd.height; y++) { for (int x = 0; x < boundsToXAdd.width; x++) if (maskToXAdd[offSrc++]) data[offDst + x] ^= 1; offDst += bnds.width; } // optimize bounds if (isUpdating()) boundsNeedUpdate = true; else optimizeBounds(); // notify roi changed roiChanged(true); } /** * Subtract the specified {@link ROI2DArea} from this ROI2DArea */ public void subtract(ROI2DArea roi) { final Rectangle boundsToRemove = roi.getBounds(); final byte[] maskToRemove = roi.maskData; final byte[] data; final Rectangle bnds; synchronized (this) { data = maskData; bnds = bounds; } // compute intersection final Rectangle intersection = bnds.intersection(boundsToRemove); // nothing to remove so nothing to do... if (intersection.isEmpty()) return; // calculate offset int offDst = ((intersection.y - bnds.y) * bnds.width) + (intersection.x - bnds.x); int offSrc = ((intersection.y - boundsToRemove.y) * boundsToRemove.width) + (intersection.x - boundsToRemove.x); for (int y = 0; y < intersection.height; y++) { for (int x = 0; x < intersection.width; x++) if (maskToRemove[offSrc + x] != 0) data[offDst + x] = 0; offDst += bnds.width; offSrc += boundsToRemove.width; } // optimize bounds if (isUpdating()) boundsNeedUpdate = true; else optimizeBounds(); // notify roi changed roiChanged(true); } /** * Subtract the specified {@link BooleanMask2D} from this ROI2DArea */ public void subtract(BooleanMask2D mask) { final Rectangle boundsToRemove = mask.bounds; final boolean[] maskToRemove = mask.mask; final byte[] data; final Rectangle bnds; synchronized (this) { data = maskData; bnds = bounds; } // compute intersection final Rectangle intersection = bnds.intersection(boundsToRemove); // nothing to remove so nothing to do... if (intersection.isEmpty()) return; // calculate offset int offDst = ((intersection.y - bnds.y) * bnds.width) + (intersection.x - bnds.x); int offSrc = ((intersection.y - boundsToRemove.y) * boundsToRemove.width) + (intersection.x - boundsToRemove.x); for (int y = 0; y < intersection.height; y++) { for (int x = 0; x < intersection.width; x++) if (maskToRemove[offSrc + x]) data[offDst + x] = 0; offDst += bnds.width; offSrc += boundsToRemove.width; } // optimize bounds if (isUpdating()) boundsNeedUpdate = true; else optimizeBounds(); // notify roi changed roiChanged(true); } /** * @deprecated Use {@link #subtract(ROI2DArea)} instead */ @Deprecated public void remove(ROI2DArea roi) { subtract(roi); } /** * @deprecated Use {@link #subtract(BooleanMask2D)} instead */ @Deprecated public void remove(BooleanMask2D mask) { subtract(mask); } /** * Update mask by adding/removing the specified shape to/from it. * * @param shape * the shape to add in or remove from the mask * @param remove * if set to <code>true</code> the shape will be removed from the mask * @param inclusive * if we should also consider the edge of the shape to update the mask * @param accurate * if set to <code>true</code> the operation will be done to be as pixel accurate as * possible * @param immediateUpdate * if set to <code>true</code> the bounds of the mask will be immediately recomputed * (only meaningful for a * remove operation) */ public void updateMask(Shape shape, boolean remove, boolean inclusive, boolean accurate, boolean immediateUpdate) { if (remove) { // outside bounds ? --> nothing to remove so nothing to do... if (!bounds.intersects(shape.getBounds2D())) return; // mark that bounds need to be updated if (isUpdating() || !immediateUpdate) boundsNeedUpdate = true; } else // update bounds (this update the image dimension if needed) addToBounds(shape.getBounds()); // get image graphics object final Graphics2D g = imageMask.createGraphics(); // we don't need anti aliasing here g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); // force accurate stroke rendering if (accurate) g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); g.setComposite(AlphaComposite.Src); // set color depending remove or adding to mask if (remove) g.setColor(new Color(colorModel.getRGB(0), true)); else g.setColor(new Color(colorModel.getRGB(1), true)); // translate to origin of image and pixel center g.translate(-(bounds.x + 0.5d), -(bounds.y + 0.5d)); // draw shape into the mask g.fill(ShapeUtil.getClosedPath(shape)); // we want edge as well if (inclusive) g.draw(shape); g.dispose(); // need to optimize bounds if (remove && !isUpdating() && immediateUpdate) optimizeBounds(); // notify roi changed roiChanged(true); } /** * Update mask from specified shape */ public void updateMask(Shape shape, boolean remove) { updateMask(shape, remove, true, false, false); } @Deprecated @Override public ROI2DAreaPainter getPainter() { return getOverlay(); } @Override public ROI2DAreaPainter getOverlay() { return (ROI2DAreaPainter) super.painter; } @Override protected ROI2DAreaPainter createPainter() { return new ROI2DAreaPainter(); } @Override public boolean hasSelectedPoint() { return false; } /** * @deprecated useless method. */ @Deprecated public boolean canAddPoint() { return true; } /** * @deprecated useless method. */ @Deprecated public boolean canRemovePoint() { return true; } /** * @deprecated Use {@link #addBrush(Point2D)} instead. */ @Deprecated public boolean addPointAt(Point2D pos, boolean ctrl) { addBrush(pos); return true; } /** * @deprecated Use {@link #removeBrush(Point2D)} instead. */ @Deprecated public boolean removePointAt(IcyCanvas canvas, Point2D pos) { removeBrush(pos); return true; } /** * @deprecated Useless method. */ @Deprecated protected boolean removeSelectedPoint(IcyCanvas canvas, Point2D imagePoint) { // no selected point for this ROI return false; } /** * Add brush point at specified position. */ public void addBrush(Point2D pos) { getOverlay().addToMask(pos); } /** * Remove brush point from the mask at specified position.<br> * Don't forget to call optimizeBounds() after consecutive remove operation * to refresh the mask bounds. */ public void removeBrush(Point2D pos) { getOverlay().removeFromMask(pos); } /** * Add a point to the mask */ public void addPoint(Point pos) { addPoint(pos.x, pos.y); } /** * Add a point to the mask */ public void addPoint(int x, int y) { setPoint(x, y, true); } /** * Remove a point from the mask.<br> * Don't forget to call optimizeBounds() after consecutive remove operation * to refresh the mask bounds. */ public void removePoint(Point pos) { removePoint(pos.x, pos.y); } /** * Remove a point to the mask.<br> * Don't forget to call optimizeBounds() after consecutive remove operation * to refresh the mask bounds. */ public void removePoint(int x, int y) { setPoint(x, y, false); } /** * Add a rectangle to the mask */ public void addRect(Rectangle r) { updateMask(r, false, false, true, true); } /** * Add a rectangle to the mask */ public void addRect(int x, int y, int w, int h) { addRect(new Rectangle(x, y, w, h)); } /** * Remove a rectangle from the mask.<br> * Don't forget to call optimizeBounds() after consecutive remove operation<br> * to refresh the mask bounds. */ public void removeRect(Rectangle r) { updateMask(r, true, false, true, true); } /** * Remove a rectangle from the mask.<br> * Don't forget to call optimizeBounds() after consecutive remove operation<br> * to refresh the mask bounds. */ public void removeRect(int x, int y, int w, int h) { removeRect(new Rectangle(x, y, w, h)); } /** * Add a shape to the mask */ public void addShape(Shape s) { updateMask(s, false, false, true, true); } /** * Remove a shape to the mask.<br> * Don't forget to call optimizeBounds() after consecutive remove operation<br> * to refresh the mask bounds. */ public void removeShape(Shape s) { updateMask(s, true, false, true, true); } @Override public ROI add(ROI roi, boolean allowCreate) throws UnsupportedOperationException { if (roi instanceof ROI2D) { final ROI2D roi2d = (ROI2D) roi; // only if on same position if ((getZ() == roi2d.getZ()) && (getT() == roi2d.getT()) && (getC() == roi2d.getC())) { if (roi2d instanceof ROI2DArea) add((ROI2DArea) roi2d); else if (roi2d instanceof ROI2DShape) updateMask(((ROI2DShape) roi2d).getShape(), false, true, true, true); else add(roi2d.getBooleanMask(true)); return this; } } return super.add(roi, allowCreate); } @Override public ROI intersect(ROI roi, boolean allowCreate) throws UnsupportedOperationException { if (roi instanceof ROI2D) { final ROI2D roi2d = (ROI2D) roi; // only if on same position if ((getZ() == roi2d.getZ()) && (getT() == roi2d.getT()) && (getC() == roi2d.getC())) { final Rectangle intersection = getBounds().intersection(roi2d.getBounds()); final BooleanMask2D mask = new BooleanMask2D(intersection, getBooleanMask(intersection, true)); final BooleanMask2D roiMask = new BooleanMask2D(intersection, roi2d.getBooleanMask(intersection, true)); setAsBooleanMask(BooleanMask2D.getIntersection(mask, roiMask)); return this; } } return super.intersect(roi, allowCreate); } @Override public ROI exclusiveAdd(ROI roi, boolean allowCreate) throws UnsupportedOperationException { if (roi instanceof ROI2D) { final ROI2D roi2d = (ROI2D) roi; // only if on same position if ((getZ() == roi2d.getZ()) && (getT() == roi2d.getT()) && (getC() == roi2d.getC())) { if (roi2d instanceof ROI2DArea) exclusiveAdd((ROI2DArea) roi2d); else exclusiveAdd(roi2d.getBooleanMask(true)); return this; } } return super.exclusiveAdd(roi, allowCreate); } @Override public ROI subtract(ROI roi, boolean allowCreate) throws UnsupportedOperationException { if (roi instanceof ROI2D) { final ROI2D roi2d = (ROI2D) roi; // only if on same position if ((getZ() == roi2d.getZ()) && (getT() == roi2d.getT()) && (getC() == roi2d.getC())) { if (roi2d instanceof ROI2DArea) subtract((ROI2DArea) roi2d); else if (roi2d instanceof ROI2DShape) updateMask(((ROI2DShape) roi2d).getShape(), true, true, true, true); else subtract(roi2d.getBooleanMask(true)); return this; } } return super.subtract(roi, allowCreate); } /** * Return true if bounds need to be updated by calling optimizeBounds() method. */ public boolean getBoundsNeedUpdate() { return boundsNeedUpdate; } /** * Clear the mask */ public void clear() { // reset image with new rectangle updateImage(new Rectangle()); } @Override public boolean isOverEdge(IcyCanvas canvas, double x, double y) { // use bigger stroke for isOverEdge test for easier intersection final double strk = getAdjustedStroke(canvas) * 3; final Rectangle2D rect = new Rectangle2D.Double(x - (strk * 0.5), y - (strk * 0.5), strk, strk); // fast intersect test to start with if (getBounds2D().intersects(rect)) // use flatten path, intersects on curved shape return incorrect result return ShapeUtil.pathIntersects(bounds.getPathIterator(null, 0.1), rect); return false; } @Override public boolean contains(double x, double y) { final byte[] data; final Rectangle bnds; synchronized (this) { data = maskData; bnds = bounds; } // fast discard if (!bnds.contains(x, y)) return false; // replace to origin final int xi = (int) x - bnds.x; final int yi = (int) y - bnds.y; return (data[(yi * bnds.width) + xi] != 0); } @Override public boolean contains(double x, double y, double w, double h) { final byte[] data; final Rectangle bnds; synchronized (this) { data = maskData; bnds = bounds; } // fast discard if (!bnds.contains(x, y, w, h)) return false; // replace to origin final int xi = (int) x - bnds.x; final int yi = (int) y - bnds.y; final int wi = (int) (x + w) - (int) x; final int hi = (int) (y + h) - (int) y; // scan all pixels, can take sometime if mask is large int offset = (yi * bnds.width) + xi; for (int j = 0; j < hi; j++) { for (int i = 0; i < wi; i++) if (data[offset++] == 0) return false; offset += bnds.width - wi; } return true; } /* * already calculated */ @Override public Rectangle2D computeBounds2D() { return bounds; } /* * We can override directly this method as we use our own bounds calculation method here */ @Override public Rectangle2D getBounds2D() { return bounds; } @Override public boolean intersects(double x, double y, double w, double h) { final byte[] data; final Rectangle bnds; synchronized (this) { data = maskData; bnds = bounds; } // fast discard if (!bnds.intersects(x, y, w, h)) return false; // replace to origin int xi = (int) x - bnds.x; int yi = (int) y - bnds.y; int wi = (int) (x + w) - (int) x; int hi = (int) (y + h) - (int) y; // adjust box to mask size if (xi < 0) { wi += xi; xi = 0; } if (yi < 0) { hi += yi; yi = 0; } if ((xi + wi) > bnds.width) wi -= (xi + wi) - bnds.width; if ((yi + hi) > bnds.height) hi -= (yi + hi) - bnds.height; // scan all pixels, can take sometime if mask is large int offset = (yi * bnds.width) + xi; for (int j = 0; j < hi; j++) { for (int i = 0; i < wi; i++) if (data[offset++] != 0) return true; offset += bnds.width - wi; } return false; } @Override public boolean[] getBooleanMask(int x, int y, int w, int h, boolean inclusive) { final boolean[] result = new boolean[Math.max(0, w) * Math.max(0, h)]; final byte[] data; final Rectangle bnds; synchronized (this) { data = maskData; bnds = bounds; } // calculate intersection final Rectangle intersect = bnds.intersection(new Rectangle(x, y, w, h)); // no intersection between mask and specified rectangle if (intersect.isEmpty()) return result; // this ROI doesn't take care of inclusive parameter as intersect = contains int offSrc = 0; int offDst = 0; // adjust offset in source mask if (intersect.x > bnds.x) offSrc += (intersect.x - bnds.x); if (intersect.y > bnds.y) offSrc += (intersect.y - bnds.y) * bnds.width; // adjust offset in destination mask if (bnds.x > x) offDst += (bnds.x - x); if (bnds.y > y) offDst += (bnds.y - y) * w; for (int j = 0; j < intersect.height; j++) { for (int i = 0; i < intersect.width; i++) result[offDst++] = (data[offSrc++] != 0); offSrc += bnds.width - intersect.width; offDst += w - intersect.width; } return result; } @Override public double computeNumberOfPoints() { // just count the number of point contained in the mask double result = 0d; final byte[] data = maskData; for (int i = 0; i < data.length; i++) if (data[i] != 0) result += 1d; return result; } @Override public boolean canTranslate() { return true; } @Override public void translate(double dx, double dy) { translateX += dx; translateY += dy; // convert to integer final int dxi = (int) translateX; final int dyi = (int) translateY; // keep trace of not used floating part translateX -= dxi; translateY -= dyi; if ((dxi != 0) || (dyi != 0)) { bounds.translate(dxi, dyi); roiChanged(false); } } @Override public boolean canSetPosition() { return true; } @Override public void setPosition2D(Point2D newPosition) { bounds = new Rectangle((int) newPosition.getX(), (int) newPosition.getY(), bounds.width, bounds.height); roiChanged(false); } /** * Set the mask from a BooleanMask2D object.<br> * If specified mask is <i>null</i> then ROI is cleared. */ public void setAsBooleanMask(BooleanMask2D mask) { // mask empty ? --> just clear the ROI if ((mask == null) || mask.isEmpty()) clear(); // don't need bounds optimization as BooleanMask2D should be already optimized else setAsBooleanMask(mask.bounds, mask.mask, false); } /** * Set the mask from a boolean array.<br> * r represents the region defined by the boolean array. * * @param r * @param booleanMask */ protected void setAsByteMask(Rectangle r, byte[] mask, boolean doBoundsOptimization) { // reset image with new rectangle updateImage(r); System.arraycopy(mask, 0, maskData, 0, r.width * r.height); if (doBoundsOptimization) { // optimize bounds if (isUpdating()) boundsNeedUpdate = true; else optimizeBounds(); } // notify roi changed roiChanged(true); } /** * Set the mask from a boolean array.<br> * r represents the region defined by the boolean array. * * @param r * @param booleanMask */ protected void setAsBooleanMask(Rectangle r, boolean[] booleanMask, boolean doBoundsOptimization) { // reset image with new rectangle updateImage(r); final byte[] data = maskData; for (int i = 0; i < data.length; i++) data[i] = (byte) (booleanMask[i] ? 1 : 0); if (doBoundsOptimization) { // optimize bounds if (isUpdating()) boundsNeedUpdate = true; else optimizeBounds(); } // notify roi changed roiChanged(true); } /** * Set the mask from a boolean array.<br> * r represents the region defined by the boolean array. * * @param r * @param booleanMask */ public void setAsBooleanMask(Rectangle r, boolean[] booleanMask) { setAsBooleanMask(r, booleanMask, true); } public void setAsBooleanMask(int x, int y, int w, int h, boolean[] booleanMask) { setAsBooleanMask(new Rectangle(x, y, w, h), booleanMask); } @Override public void onChanged(CollapsibleEvent object) { final ROIEvent event = (ROIEvent) object; // do here global process on ROI change switch (event.getType()) { case ROI_CHANGED: // update bounds if needed if (boundsNeedUpdate && !roiModifiedByMouse) { if (optimizeBounds()) // need to send a new change event ! roiChanged(true); } // we need to rebuild shape if (StringUtil.equals(event.getPropertyName(), ROI_CHANGED_ALL)) getOverlay().needRebuild = true; else { final ROI2DAreaPainter overlay = getOverlay(); // z position change ? --> need total rebuild if (overlay.lastBuildPosZ != getZ()) overlay.needRebuild = true; // just need to change position else overlay.updateVtkObjectsBounds(); } break; case FOCUS_CHANGED: case SELECTION_CHANGED: getOverlay().updateVtkDisplayProperties(); break; case PROPERTY_CHANGED: final String property = event.getPropertyName(); if (StringUtil.equals(property, PROPERTY_STROKE) || StringUtil.equals(property, PROPERTY_COLOR) || StringUtil.equals(property, PROPERTY_OPACITY)) getOverlay().updateVtkDisplayProperties(); break; default: break; } super.onChanged(object); } @Override public boolean loadFromXML(Node node) { beginUpdate(); try { if (!super.loadFromXML(node)) return false; final Rectangle rect = new Rectangle(); // retrieve mask bounds rect.x = XMLUtil.getElementIntValue(node, ID_BOUNDS_X, 0); rect.y = XMLUtil.getElementIntValue(node, ID_BOUNDS_Y, 0); rect.width = XMLUtil.getElementIntValue(node, ID_BOUNDS_W, 0); rect.height = XMLUtil.getElementIntValue(node, ID_BOUNDS_H, 0); // retrieve mask data final byte[] data = XMLUtil.getElementBytesValue(node, ID_BOOLMASK_DATA, new byte[0]); // an error occurred while retrieved XML data if (data == null) return false; // set the ROI from the unpacked boolean mask setAsByteMask(rect, data, false); } finally { endUpdate(); } return true; } @Override public boolean saveToXML(Node node) { if (!super.saveToXML(node)) return false; final byte[] data; final Rectangle bnds; synchronized (maskData) { // need to duplicate to avoid array change during XML saving (ZIP packing don't like that) data = maskData.clone(); bnds = bounds; } final int len = bnds.width * bnds.height; // invalid --> return false if ((len > 0) && (len != data.length)) return false; // retrieve mask bounds XMLUtil.setElementIntValue(node, ID_BOUNDS_X, bnds.x); XMLUtil.setElementIntValue(node, ID_BOUNDS_Y, bnds.y); XMLUtil.setElementIntValue(node, ID_BOUNDS_W, bnds.width); XMLUtil.setElementIntValue(node, ID_BOUNDS_H, bnds.height); // set mask data as byte array if (len > 0) XMLUtil.setElementBytesValue(node, ID_BOOLMASK_DATA, data); return true; } }