package org.squidy.nodes.reactivision.remote; import static java.lang.Math.PI; import static java.lang.Math.ceil; import static java.lang.Math.cos; import static java.lang.Math.sin; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.Stroke; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.Scrollable; import javax.swing.event.MouseInputAdapter; import org.squidy.nodes.reactivision.remote.control.ControlServer; import org.squidy.nodes.reactivision.remote.image.ImageServer; public final class CalibrationArea extends JPanel implements Scrollable { private enum CalibrationMode { STANDARD_CALIBRATION, QUICK_CALIBRATION, DISABLED; } private static final long serialVersionUID = 8461995937722552323L; private ControlServer controlServer; private ImageServer imageServer; private static final int manualCalibrationBorderWidth = 196; private static final int autoCalibrationBorderWidth = 32; private JPopupMenu popUpMenu; private CalibrationMode mode = CalibrationMode.STANDARD_CALIBRATION; /** * Contains GridPoints in absolute pixel coordinates */ private GridPoint[] gridPoints = new GridPoint[63];//9*7 /** * Index of the next GridPoint to be placed during quick calibration. */ private int calibrationGridPointIndex; /** * in ReacTIVision coordinates (default position = 0, individual deviation * from that point in 1/8 camera image width increments). * Use during receiving and sending the grid from the ReacTIVision client. */ private float[] grid; /** * The horizontal grid "lines". */ private CatmullRomSpline[] horizontalSplines; /** * The vertical grid "lines". */ private CatmullRomSpline[] verticalSplines; /** * The pixel distance between two adjacent default GridPoint positions - 1. */ private int boxWidth; /** * Reference to the currently grabbed GridPoint. Is <code>null</code> if no * GridPoint is grabbed. */ private GridPoint grabbedGridPoint; /** * If <code>true</code>, the location of the next MouseClickedEvent will be used * as a pivot to rotate all GridPoints. */ private boolean rotateGridPoints = false; //Strokes final static float dash1[] = {5.0f}; final static BasicStroke dashed = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f); final Stroke fullStroke = new BasicStroke(/*(float)1.5*/); private JMenuItem rotateClockwise; private JMenuItem invertGridPoints; private JMenuItem applyChanges; public CalibrationArea(ControlServer controlServer, ImageServer imageServer) { this.controlServer = controlServer; this.imageServer = imageServer; imageServer.setCalibrationArea(this); setOpaque( false ); setFocusable(true); BufferedImage firstImage = imageServer.getImage(); setPreferredSize( new Dimension(firstImage.getWidth() + 2 * getBorderWidth(), firstImage.getHeight() + 2 * getBorderWidth())); grid = controlServer.getGrid(); controlServer.startCameraFeed(); if (grid == null) { //makes no sense to continue mode = CalibrationMode.DISABLED; return; } this.setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR)); boxWidth = firstImage.getWidth() / 8; initGridPoints(); initGridLines(); addPopUpMenu(); addEventListeners(); } private void addEventListeners() { final MouseInputAdapter mouseInputAdapter = new MouseInputAdapter() { @Override public void mouseClicked(MouseEvent m) { if (m.getButton() == MouseEvent.BUTTON3) { rotateGridPoints = false; //show applyChanges only if all GridPoints are set if (gridPoints[gridPoints.length-1].isSet) { popUpMenu.add(rotateClockwise); popUpMenu.add(invertGridPoints); popUpMenu.add(applyChanges); } else { popUpMenu.remove(rotateClockwise); popUpMenu.remove(invertGridPoints); popUpMenu.remove(applyChanges); } popUpMenu.show(m.getComponent(), m.getX(), m.getY()); } else if (m.getButton() == MouseEvent.BUTTON1) { if (rotateGridPoints) { final Point pivot = m.getPoint(); for (int i = 0; i < gridPoints.length; ++i) gridPoints[i].rotateClockwise(pivot); rotateGridPoints = false; repaint(); } else if (mode == CalibrationMode.QUICK_CALIBRATION) { final Point p = new Point(m.getX(), m.getY()); if (!pointCollision(p)) { gridPoints[calibrationGridPointIndex].setPosition(p); gridPoints[calibrationGridPointIndex].isSet = true; if (++calibrationGridPointIndex >= gridPoints.length) mode = CalibrationMode.STANDARD_CALIBRATION; repaint(); } } } } @Override public void mouseDragged(MouseEvent m) { if (grabbedGridPoint != null) { grabbedGridPoint.setPosition(m.getPoint()); repaint(); } } @Override public void mousePressed(MouseEvent m) { //check if point has been grabbed for (int i = 0; i < gridPoints.length; ++i) { if (!gridPoints[i].isSet) continue; if (gridPoints[i].grabbed(m.getX(), m.getY())) { grabbedGridPoint = gridPoints[i]; break; } } } @Override public void mouseReleased(MouseEvent m) { if (grabbedGridPoint != null) { grabbedGridPoint = null; repaint(); } } }; this.addMouseListener(mouseInputAdapter); this.addMouseMotionListener(mouseInputAdapter); final KeyAdapter keyListener = new KeyAdapter() { private boolean control = false; @Override public void keyPressed(KeyEvent k) { if (k.getKeyCode() == KeyEvent.VK_CONTROL) control = true; if (k.getKeyCode() == KeyEvent.VK_Z && control && mode == CalibrationMode.QUICK_CALIBRATION) { --calibrationGridPointIndex; if (calibrationGridPointIndex < 0) calibrationGridPointIndex = 0; gridPoints[calibrationGridPointIndex].isSet = false; repaint(); } if (k.getKeyCode() == KeyEvent.VK_SPACE) { GridPoint.toggleID(); repaint(); } } @Override public void keyReleased(KeyEvent k) { if (k.getKeyCode() == KeyEvent.VK_CONTROL) control = false; } }; this.addKeyListener(keyListener); } private void addPopUpMenu() { popUpMenu = new JPopupMenu(); JMenuItem quickCalibration = new JMenuItem("Start quick calibration"); quickCalibration.addActionListener(new ActionListener() { public void actionPerformed (ActionEvent e) { mode = CalibrationMode.QUICK_CALIBRATION; for (int i = 0; i < gridPoints.length; ++i) gridPoints[i].isSet = false; calibrationGridPointIndex = 0; repaint(); } }); popUpMenu.add( quickCalibration ); JMenuItem resetGrid = new JMenuItem("Reset Grid"); resetGrid.addActionListener(new ActionListener() { public void actionPerformed (ActionEvent e) { mode = CalibrationMode.STANDARD_CALIBRATION; calibrationGridPointIndex = 0; for (int i = 0; i < gridPoints.length; ++i) { gridPoints[i].setPosition(getDefaultGridPointPosition(i)); gridPoints[i].isSet = true; } repaint(); } }); popUpMenu.add( resetGrid ); rotateClockwise = new JMenuItem("Rotate clockwise"); rotateClockwise.addActionListener(new ActionListener() { public void actionPerformed (ActionEvent e) { rotateGridPoints = true; } }); //do not add the above JMenuItem here invertGridPoints = new JMenuItem("Invert grid points"); invertGridPoints.addActionListener(new ActionListener() { public void actionPerformed (ActionEvent e) { for (int i = 0; i < gridPoints.length / 2; ++i) { final float tempX = gridPoints[i].x; final float tempY = gridPoints[i].y; gridPoints[i].x = gridPoints[gridPoints.length - 1 - i].x; gridPoints[i].y = gridPoints[gridPoints.length - 1 - i].y; gridPoints[gridPoints.length - 1 - i].x = tempX; gridPoints[gridPoints.length - 1 - i].y = tempY; } repaint(); } }); //do not add the above JMenuItem here applyChanges = new JMenuItem("Apply Changes"); applyChanges.addActionListener(new ActionListener() { public void actionPerformed (ActionEvent e) { //transform GridPoint coordinates to ReacTIVision coordinates //and store in grid for (int i = 0; i < gridPoints.length; ++i) { final Point defaultPosition = getDefaultGridPointPosition(i); grid[2*i] = ((float)(gridPoints[i].x - defaultPosition.x)) / boxWidth; grid[2*i+1] = ((float)(gridPoints[i].y - defaultPosition.y)) / boxWidth; } if (!controlServer.setGrid(grid)) { // ReacTIVision.showErrorPopUp("Could not apply grid changes."); } } }); //do not add the above JMenuItem here } /** * Returns <code>true</code>, if the the four GridPoints surrounding the * passed coordinates are all set. This method works correctly only on * undistorted relative grid coordinates, i.e. it has to be used before interpolation. * * @param x range: 0 to 8 * @param y range: 0 to 6 * @return see description */ private boolean boxIsSet(double x, double y) { if (x < 0 || x > 8 || y < 0 || y > 6) return false; int ceilX = (int)ceil(x); int ceilY = (int)ceil(y); if (!gridPoints[9 * (ceilY - 1) + ceilX - 1].isSet) return false; if (!gridPoints[9 * (ceilY - 1) + ceilX].isSet) return false; if (!gridPoints[9 * ceilY + ceilX - 1].isSet) return false; if (!gridPoints[9 * ceilY+ ceilX].isSet) return false; return true; } private double catmullRomSpline(double x,double v1,double v2,double v3,double v4) { double c1,c2,c3,c4; c1 = 1.0*v2; c2 = -0.5*v1 + 0.5*v3; c3 = 1.0*v1 + -2.5*v2 + 2.0*v3 + -0.5*v4; c4 = -0.5*v1 + 1.5*v2 + -1.5*v3 + 0.5*v4; return (((c4*x + c3)*x +c2)*x + c1); } private int getBorderWidth() { return manualCalibrationBorderWidth + autoCalibrationBorderWidth; } /** * Returns the default absolute pixel coordinates of the specified GridPoint. * @param gridPointID the position of a GridPoint in gridPoints, which corresponds * to the points default order when traversing the grid from left to right and top * to bottom. * @return a <code>Point</code> with the requested coordinates */ private Point getDefaultGridPointPosition(int gridPointID) { int yBoxes = (62 - gridPointID) / 9; int xBoxes = gridPointID % 9; return new Point(boxWidth * xBoxes + getBorderWidth(), boxWidth * (6 - yBoxes) + getBorderWidth()); } private FloatPoint getInterpolated(float x, float y) { FloatPoint point = new FloatPoint(); x = 8 - x; y = 6 - y; if( x>=9 ) return getInterpolatedY(9,y); if( y>=7 ) return getInterpolatedX(x,7); // x int x_floor = (int)x; float x_offset = x - x_floor; FloatPoint x1 = getInterpolatedY(x_floor,y); FloatPoint x2 = getInterpolatedY(x_floor+1,y); point.x = (x1.x * (1-x_offset)) + (x2.x * x_offset); // y int y_floor = (int)y; float y_offset = y - y_floor; FloatPoint y1 = getInterpolatedX(x,y_floor); FloatPoint y2 = getInterpolatedX(x,y_floor+1); point.y = (y1.y * (1-y_offset)) + (y2.y * y_offset); return point; } private FloatPoint getInterpolatedX( float x, int y ) { int x_floor = (int)x; float x_offset = x - x_floor; FloatPoint v1; try { if( x_floor<=0 ) v1 = new FloatPoint(gridPoints[ (y*9) + x_floor ]); else v1 = new FloatPoint(gridPoints[ (y*9) + x_floor-1 ]); } catch (IndexOutOfBoundsException e) { v1 = new FloatPoint(0,0); } FloatPoint v2; try { v2 = new FloatPoint(gridPoints[ (y*9) + x_floor ]); } catch (IndexOutOfBoundsException e) { v2 = new FloatPoint(0,0); } FloatPoint v3; try { v3 = new FloatPoint(gridPoints[ (y*9) + x_floor+1 ]); } catch (IndexOutOfBoundsException e) { v3 = new FloatPoint(0,0); } FloatPoint v4; try { if( x_floor>=7 ) v4 = new FloatPoint(gridPoints[ (y*9) + x_floor+1 ]); else v4 = new FloatPoint(gridPoints[ (y*9) + x_floor+2 ]); } catch (IndexOutOfBoundsException e) { v4 = new FloatPoint(0,0); } FloatPoint point = new FloatPoint( (float)catmullRomSpline( x_offset, v1.x, v2.x, v3.x, v4.x ), (float)catmullRomSpline( x_offset, v1.y, v2.y, v3.y, v4.y )); return point; } private FloatPoint getInterpolatedY( int x, float y ) { int y_floor = (int)y; float y_offset = y - y_floor; FloatPoint v1; if( y_floor<=0 ) v1 = new FloatPoint(gridPoints[ (y_floor*9) + x ]); else v1 = new FloatPoint(gridPoints[ ((y_floor-1) * 9) + x ]); FloatPoint v2 = new FloatPoint(gridPoints[ (y_floor * 9) + x ]); FloatPoint v3; try { v3 = new FloatPoint(gridPoints[ ((y_floor+1) * 9) + x ]); } catch (IndexOutOfBoundsException e) { v3 = new FloatPoint(0,0); } FloatPoint v4; try { if( y_floor>=5 ) v4 = new FloatPoint(gridPoints[ ((y_floor+1) * 9) + x ]); else v4 = new FloatPoint(gridPoints[ ((y_floor+2) * 9) + x ]); } catch (IndexOutOfBoundsException e) { v4 = new FloatPoint(0,0); } FloatPoint point = new FloatPoint( (float)catmullRomSpline( y_offset, v1.x, v2.x, v3.x, v4.x ), (float)catmullRomSpline( y_offset, v1.y, v2.y, v3.y, v4.y )); return point; } //############### Listeners and subcomponents ############### public Dimension getPreferredScrollableViewportSize() { return new Dimension(imageServer.getImage().getWidth(), imageServer.getImage().getHeight()); } public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { return 1; } public boolean getScrollableTracksViewportHeight() { return false; } public boolean getScrollableTracksViewportWidth() { return false; } public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { return 1; } //############### implement Scrollable interface ################# //################## calibration methods #################### /** * Initializes the grid's horizontal and vertical splines. * Must not be called prior to <code>initGridPoints()</code>. */ private void initGridLines() { horizontalSplines = new CatmullRomSpline[7]; for (int i = 0; i < 7; ++i) { horizontalSplines[i] = new CatmullRomSpline(); for (int j = 0; j < 9; ++j) horizontalSplines[i].addControlPoint(gridPoints[9*i + j]); } verticalSplines = new CatmullRomSpline[9]; for (int i = 0; i < 9; ++i) { verticalSplines[i] = new CatmullRomSpline(); for (int j = 0; j < 7; ++j) verticalSplines[i].addControlPoint(gridPoints[i + 9*j]); } } /** * Uses the information in <code>grid</code> to create corresponding GridPoints * and store them in <code>gridPoints</code>. */ private void initGridPoints() { for (int i = 0; i < gridPoints.length; ++i) { gridPoints[i] = new GridPoint( (int)(getDefaultGridPointPosition(i).x + grid[2*i] * boxWidth), (int)(getDefaultGridPointPosition(i).y + grid[2*i+1] * boxWidth)); gridPoints[i].setID(i); } } private void translatePosition(FloatPoint p) { //translate position to relative coordinates p.x -= (manualCalibrationBorderWidth + autoCalibrationBorderWidth); p.x /= boxWidth; p.y -= (manualCalibrationBorderWidth + autoCalibrationBorderWidth); p.y /= boxWidth; //interpolate p.set(getInterpolated(p.x, p.y)); } public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; //draw area borders g2.setStroke(dashed); g2.draw(new Rectangle2D.Double(0, 0, getPreferredSize().width - 1, getPreferredSize().height - 1)); g2.draw(new Rectangle2D.Double( autoCalibrationBorderWidth, autoCalibrationBorderWidth, getPreferredSize().width - 2 * autoCalibrationBorderWidth - 1, getPreferredSize().height - 2 * autoCalibrationBorderWidth - 1)); //draw camera image (if possible) g.drawImage(imageServer.getImage(), getBorderWidth(), getBorderWidth(), null); if (mode != CalibrationMode.DISABLED) { //draw extrapolated position of next point if (mode == CalibrationMode.QUICK_CALIBRATION && calibrationGridPointIndex > 1 && calibrationGridPointIndex < 63) { g2.setColor(Color.GREEN); g2.setStroke(fullStroke); final GridPoint[] leftPoints = leftPoints(calibrationGridPointIndex); if (leftPoints.length > 1) { FloatPoint f = FloatPointExtrapolator.extrapolate( leftPoints[leftPoints.length - 2], leftPoints[leftPoints.length - 1]); g2.drawLine((int)(f.x - 10), (int)f.y, (int)(f.x + 10), (int)f.y); g2.drawLine((int)f.x, (int)(f.y - 10), (int)f.x, (int)(f.y + 10)); }//TODO g2.setColor(Color.RED); final GridPoint[] abovePoints = abovePoints(calibrationGridPointIndex); if (abovePoints.length > 1) { FloatPoint f = FloatPointExtrapolator.extrapolate( abovePoints[abovePoints.length - 2], abovePoints[abovePoints.length - 1]); g2.drawLine((int)(f.x - 10), (int)f.y, (int)(f.x + 10), (int)f.y); g2.drawLine((int)f.x, (int)(f.y - 10), (int)f.x, (int)(f.y + 10)); }//TODO } //draw grid lines g2.setColor(Color.YELLOW); g2.setStroke(fullStroke); for (int i = 0; i < 7; ++i) horizontalSplines[i].draw(g2); for (int i = 0; i < 9; ++i) verticalSplines[i].draw(g2); //draw grid points for (int i = 0; i < gridPoints.length; ++i) if (gridPoints[i].isSet) gridPoints[i].draw(g2); else break; //*** draw ellipsoids *** final Point center = new Point( getBorderWidth() + 4 * boxWidth, getBorderWidth() + 3 * boxWidth); int steps = 30; int[] x = new int[steps + 1]; int[] y = new int[steps + 1]; boolean[] boxIsSet = new boolean[steps + 1]; final FloatPoint p = new FloatPoint(); //inner circle for (int i = 0; i <= steps; ++i) { final double angle = i*2*PI/steps; p.x = (float)(center.x - boxWidth * cos(angle)); p.y = (float)(center.y + boxWidth * sin(angle)); translatePosition(p); x[i] = (int)p.x; y[i] = (int)p.y; boxIsSet[i] = boxIsSet(4 + cos(angle) * 0.99, 3 - sin(angle) * 0.99); } for (int i = 0; i < steps; ++i) if (boxIsSet[i] && boxIsSet[i+1]) g2.drawLine(x[i], y[i], x[i + 1], y[i + 1]); //middle circle steps = 60; x = new int[steps + 1]; y = new int[steps + 1]; boxIsSet = new boolean[steps + 1]; for (int i = 0; i <= steps; ++i) { final double angle = i*2*PI/steps; p.x = (float)(center.x - 2 * boxWidth * cos(angle)); p.y = (float)(center.y + 2 * boxWidth * sin(angle)); translatePosition(p); x[i] = (int)p.x; y[i] = (int)p.y; boxIsSet[i] = boxIsSet(4 + cos(angle) * 1.99, 3 - sin(angle) * 1.99); } for (int i = 0; i < steps; ++i) if (boxIsSet[i] && boxIsSet[i+1]) g2.drawLine(x[i], y[i], x[i + 1], y[i + 1]); //outer circle steps = 120; x = new int[steps + 1]; y = new int[steps + 1]; boxIsSet = new boolean[steps + 1]; for (int i = 0; i <= steps; ++i) { final double angle = i*2*PI/steps; p.x = (float)(center.x - 3 * boxWidth * cos(angle)); p.y = (float)(center.y + 3 * boxWidth * sin(angle)); translatePosition(p); x[i] = (int)p.x; y[i] = (int)p.y; boxIsSet[i] = boxIsSet(4 + cos(angle) * 2.99, 3 - sin(angle) * 2.99); } for (int i = 0; i < steps; ++i) if (boxIsSet[i] && boxIsSet[i+1]) g2.drawLine(x[i], y[i], x[i + 1], y[i + 1]); //ellipse steps = 150; x = new int[steps + 1]; y = new int[steps + 1]; boxIsSet = new boolean[steps + 1]; for (int i = 0; i <= steps; ++i) { final double angle = i*2*PI/steps; p.x = (float)(center.x - 4 * boxWidth * cos(angle)); p.y = (float)(center.y + 3 * boxWidth * sin(angle)); translatePosition(p); x[i] = (int)p.x; y[i] = (int)p.y; boxIsSet[i] = boxIsSet(4 + cos(angle) * 3.99, 3 - sin(angle) * 2.99); } for (int i = 0; i < steps; ++i) if (boxIsSet[i] && boxIsSet[i+1]) g2.drawLine(x[i], y[i], x[i + 1], y[i + 1]); } } /** * Returns an array containing all GridPoints which are part of the same horizontal * spline and which are located to the left of the point identified by its location * in {@link #gridPoints}. * <p> * If no such GridPoints exist, an array of lenth zero is returned. */ private GridPoint[] leftPoints(int gridPointIndex) { final GridPoint[] points = new GridPoint[gridPointIndex % 9]; for (int i = 0; i < points.length; ++i) points[i] = gridPoints[gridPointIndex - points.length + i]; return points; } /** * This method works similar to {@link #leftPoints(int)}, just the GridPoints * above the spedified GridPoint in the same vertical spline are returned. */ private GridPoint[] abovePoints(int gridPointIndex) { final GridPoint[] points = new GridPoint[gridPointIndex / 9]; for (int i = 0; i < points.length; ++i) points[i] = gridPoints[(gridPointIndex % 9) + i * 9]; return points; } /** * Checks whether a GridPoint at location p would be too close to any existing * GridPoint. * @param p the point to check * @return <code>true</code>, if the distance to all GridPoints in the * CalibrationArea is large enough, else <code>false</code> */ private boolean pointCollision(Point p) { final int minDistance = 8; for (int i = 0; i <= gridPoints.length; ++i) { if (!gridPoints[i].isSet) return false; if (gridPoints[i].distanceFrom(p.x, p.y) < minDistance) return true; } return false; } }