/* * Copyright 2010-2013 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.roi3d; import icy.canvas.IcyCanvas; import icy.common.EventHierarchicalChecker; import icy.painter.VtkPainter; import icy.roi.BooleanMask2D; import icy.roi.BooleanMask3D; import icy.roi.ROI; import icy.roi.ROI2D; import icy.roi.ROIEvent; import icy.sequence.Sequence; import icy.system.thread.ThreadUtil; import icy.type.point.Point3D; import icy.type.point.Point5D; import icy.type.rectangle.Rectangle3D; import icy.vtk.VtkUtil; import java.awt.Graphics2D; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Map.Entry; import plugins.kernel.canvas.VtkCanvas; import plugins.kernel.roi.roi2d.ROI2DArea; import vtk.vtkActor; import vtk.vtkPolyDataMapper; import vtk.vtkProp; import vtk.vtkStructuredGrid; import vtk.vtkStructuredGridOutlineFilter; /** * 3D Area ROI. * * @author Stephane */ public class ROI3DArea extends ROI3DStack<ROI2DArea> { public class ROI3DAreaPainter extends ROI3DStackPainter implements VtkPainter, Runnable { // VTK 3D objects, we use Object to prevent UnsatisfiedLinkError Object grid; Object gridOutline; // final Object polyData; Object polyMapper; Object actor; // 3D internal boolean needRebuild; double scaling[]; VtkCanvas canvas3d; public ROI3DAreaPainter() { // polyData = null; grid = null; gridOutline = null; polyMapper = null; actor = null; scaling = new double[3]; Arrays.fill(scaling, 1d); needRebuild = true; canvas3d = null; } protected void initVtkObjects() { // init 3D painters stuff // polyData = new vtkPolyData(); grid = new vtkStructuredGrid(); gridOutline = new vtkStructuredGridOutlineFilter(); ((vtkStructuredGridOutlineFilter) gridOutline).SetInputData((vtkStructuredGrid) grid); polyMapper = new vtkPolyDataMapper(); // ((vtkPolyDataMapper) polyMapper).SetInput((vtkPolyData) polyData); ((vtkPolyDataMapper) polyMapper).SetInputConnection(((vtkStructuredGridOutlineFilter) gridOutline) .GetOutputPort()); actor = new vtkActor(); ((vtkActor) actor).SetMapper((vtkPolyDataMapper) polyMapper); } /** * rebuild VTK objects (called only when VTK canvas is selected). */ protected void rebuildVtkObjects() { final VtkCanvas canvas = canvas3d; // nothing to update if (canvas == null) return; final Sequence seq = canvas.getSequence(); // nothing to update if (seq == null) return; int width = seq.getSizeX(); int height = seq.getSizeY(); final ArrayList<double[]> verticesArray = new ArrayList<double[]>(); for (ROI2DArea area : ROI3DArea.this) { int k = area.getZ(); verticesArray.ensureCapacity(verticesArray.size() + width * height); boolean[] mask = area.getBooleanMask(0, 0, width, height, true); int offset = 0; for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++, offset++) { if (mask[offset]) verticesArray.add(new double[] {i, j, k}); } } } double[][] points = new double[verticesArray.size()][3]; verticesArray.toArray(points); // actor can be accessed in canvas3d for rendering so we need to synchronize access canvas3d.lock(); try { ((vtkStructuredGrid) grid).SetDimensions(width, height, getSizeZ()); ((vtkStructuredGrid) grid).SetPoints(VtkUtil.getPoints(points)); ((vtkStructuredGridOutlineFilter) gridOutline).Update(); ((vtkPolyDataMapper) polyMapper).Update(); } finally { canvas3d.unlock(); } // no more pending request if (!ThreadUtil.hasWaitingSingleTask(this)) canvas3d = null; } @Override public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas) { if (isActiveFor(canvas)) { if (canvas instanceof VtkCanvas) { // 3D canvas final VtkCanvas cnv = (VtkCanvas) canvas; // 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) { // initialize VTK objects if not yet done if (actor == null) initVtkObjects(); // request rebuild 3D objects canvas3d = cnv; ThreadUtil.runSingle(this); needRebuild = false; } } else super.paint(g, sequence, canvas); } } @Override public vtkProp[] getProps() { return new vtkActor[] {(vtkActor) actor}; } @Override public void run() { rebuildVtkObjects(); } } public ROI3DArea() { super(ROI2DArea.class); setName("3D area"); } public ROI3DArea(Point3D pt) { this(); addBrush(pt.toPoint2D(), (int) pt.getZ()); } public ROI3DArea(Point5D pt) { this(pt.toPoint3D()); } /** * Create a 3D Area ROI type from the specified {@link BooleanMask3D}. */ public ROI3DArea(BooleanMask3D mask) { this(); setAsBooleanMask(mask); } /** * Create a copy of the specified 3D Area ROI. */ public ROI3DArea(ROI3DArea area) { this(); // copy the source 3D area ROI for (Entry<Integer, ROI2DArea> entry : area.slices.entrySet()) slices.put(entry.getKey(), new ROI2DArea(entry.getValue())); } @Override protected ROIPainter createPainter() { return new ROI3DAreaPainter(); } /** * Adds the specified point to this ROI */ public void addPoint(int x, int y, int z) { setPoint(x, y, z, 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(int x, int y, int z) { setPoint(x, y, z, false); } /** * Set the value for the specified point in the mask. * Don't forget to call optimizeBounds() after consecutive remove point operation * to refresh the mask bounds. */ public void setPoint(int x, int y, int z, boolean value) { getSlice(z, true).setPoint(x, y, value); } /** * Add brush point at specified position and for specified Z slice. */ public void addBrush(Point2D pos, int z) { getSlice(z, true).addBrush(pos); } /** * Remove brush point from the mask at specified position and for specified Z slice.<br> * Don't forget to call optimizeBounds() after consecutive remove operation * to refresh the mask bounds. */ public void removeBrush(Point2D pos, int z) { getSlice(z, true).removeBrush(pos); } /** * Sets the ROI slice at given Z position to this 3D ROI * * @param z * the position where the slice must be set * @param roiSlice * the 2D ROI to set * @param merge * <code>true</code> if the given slice should be merged with the existing slice, or * <code>false</code> to replace the existing slice. */ public void setSlice(int z, ROI2D roiSlice, boolean merge) { if (roiSlice == null) throw new IllegalArgumentException("Cannot add an empty slice in a 3D ROI"); final ROI2DArea currentSlice = getSlice(z); final ROI newSlice; // merge both slice if ((currentSlice != null) && merge) { // we need to modify the Z, T and C position so we do the merge correctly roiSlice.setZ(z); roiSlice.setT(getT()); roiSlice.setC(getC()); // do ROI union newSlice = currentSlice.getUnion(roiSlice); } else newSlice = roiSlice; if (newSlice instanceof ROI2DArea) setSlice(z, (ROI2DArea) newSlice); else if (newSlice instanceof ROI2D) setSlice(z, new ROI2DArea(((ROI2D) newSlice).getBooleanMask(true))); else throw new IllegalArgumentException("Can't add the result of the merge operation on 2D slice " + z + ": " + newSlice.getClassName()); } /** * Returns true if the ROI is empty (the mask does not contains any point). */ @Override public boolean isEmpty() { for (ROI2DArea area : slices.values()) if (!area.isEmpty()) return false; return true; } /** * @deprecated Use {@link #getBooleanMask(boolean)} and {@link BooleanMask3D#getContourPoints()} * instead. */ @Deprecated public Point3D[] getEdgePoints() { return getBooleanMask(true).getContourPoints(); } /** * @deprecated Use {@link #getBooleanMask(boolean)} and {@link BooleanMask3D#getPoints()} * instead. */ @Deprecated public Point3D[] getPoints() { return getBooleanMask(true).getPoints(); } /** * @deprecated Use {@link #translate(double, double, double)} instead. */ @Deprecated public void translate(double dx, double dy) { beginUpdate(); try { for (ROI2DArea slice : slices.values()) slice.translate(dx, dy); } finally { endUpdate(); } } /** * Set all 2D slices ROI to same position.<br> */ public void setPosition2D(Point2D newPosition) { beginUpdate(); try { for (ROI2DArea slice : slices.values()) slice.setPosition2D(newPosition); } finally { endUpdate(); } } /** * Set the mask from a BooleanMask3D object */ public void setAsBooleanMask(BooleanMask3D mask) { if (mask != null) { final Collection<BooleanMask2D> values = mask.mask.values(); setAsBooleanMask(mask.bounds, values.toArray(new BooleanMask2D[values.size()])); } } /** * Set the 3D mask from a 2D boolean mask array * * @param rect * the 3D region defined by 2D boolean mask array * @param mask * the 3D mask data (array length should be equals to rect.sizeZ) */ public void setAsBooleanMask(Rectangle3D.Integer rect, BooleanMask2D[] mask) { if (rect.isInfiniteZ()) throw new IllegalArgumentException("Cannot set infinite Z dimension on the 3D Area ROI."); beginUpdate(); try { clear(); for (int z = 0; z < rect.sizeZ; z++) setSlice(z + rect.z, new ROI2DArea(mask[z])); optimizeBounds(); } finally { endUpdate(); } } /** * Optimize the bounds size to the minimum surface which still include all mask<br> * You should call it after consecutive remove operations. */ public void optimizeBounds() { final Rectangle3D.Integer bounds = getBounds(); beginUpdate(); try { for (int z = bounds.z; z < bounds.z + bounds.sizeZ; z++) { final ROI2DArea roi = getSlice(z); if (roi.isEmpty()) removeSlice(z); else roi.optimizeBounds(); } } finally { endUpdate(); } } /** * roi changed */ @Override public void onChanged(EventHierarchicalChecker object) { final ROIEvent event = (ROIEvent) object; // do here global process on ROI change switch (event.getType()) { case ROI_CHANGED: // the painter need to be rebuild ((ROI3DAreaPainter) painter).needRebuild = true; break; } super.onChanged(object); } }