/* * Copyright (C) 2011 Jason von Nieda <jason@vonnieda.org> * * This file is part of OpenPnP. * * OpenPnP 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. * * OpenPnP 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 OpenPnP. If not, see * <http://www.gnu.org/licenses/>. * * For more information about OpenPnP visit http://openpnp.org */ package org.openpnp.gui.components; 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.Insets; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionAdapter; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.awt.font.TextLayout; import java.awt.image.BufferedImage; import java.io.File; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.prefs.Preferences; import javax.imageio.ImageIO; import javax.swing.JComponent; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import org.openpnp.CameraListener; import org.openpnp.gui.MainFrame; import org.openpnp.gui.components.reticle.Reticle; import org.openpnp.model.Configuration; import org.openpnp.model.Location; import org.openpnp.spi.Camera; import org.openpnp.spi.Nozzle; import org.openpnp.util.MovableUtils; import org.openpnp.util.UiUtils; import org.openpnp.util.XmlSerialize; @SuppressWarnings("serial") public class CameraView extends JComponent implements CameraListener { private static final String PREF_RETICLE = "CamerView.reticle"; private static final String DEFAULT_RETICLE_KEY = "DEFAULT_RETICLE_KEY"; private final static int HANDLE_DIAMETER = 8; private enum HandlePosition { NW, N, NE, E, SE, S, SW, W } private enum SelectionMode { Resizing, Moving, Creating } /** * The Camera we are viewing. */ private Camera camera; /** * The last frame received, reported by the Camera. */ private BufferedImage lastFrame; /** * The maximum frames per second that we'll display. */ private int maximumFps; private LinkedHashMap<Object, Reticle> reticles = new LinkedHashMap<>(); private JPopupMenu popupMenu; /** * The last width and height of the component that we painted for. If the width or height is * different from these values at the start of paint we'll recalculate all the scaling data. */ private double lastWidth, lastHeight; /** * The last width and height of the image that we painted for. If the width or height is * different from these values at the start of paint we'll recalculate all the scaling data. */ private double lastSourceWidth, lastSourceHeight; private Location lastUnitsPerPixel; /** * The width and height of the image after it has been scaled to fit the bounds of the * component. */ private int scaledWidth, scaledHeight; /** * The ratio of scaled width and height to unscaled width and height. scaledWidth * scaleRatioX * = sourceWidth. scaleRatioX = sourceWidth / scaledWidth */ private double scaleRatioX, scaleRatioY; /** * The Camera's units per pixel scaled at the same ratio as the image. That is, each pixel in * the scaled image is scaledUnitsPerPixelX wide and scaledUnitsPerPixelY high. */ private double scaledUnitsPerPixelX, scaledUnitsPerPixelY; /** * The top left position within the component at which the scaled image can be drawn for it to * be centered. */ private int imageX, imageY; private boolean selectionEnabled; /** * Rectangle describing the bounds of the selection in image coordinates. */ private Rectangle selection; /** * The scaled version of the selection Rectangle. Rescaled any time the component's size is * changed. */ private Rectangle selectionScaled; private SelectionMode selectionMode; private HandlePosition selectionActiveHandle; private int selectionStartX, selectionStartY; private float selectionFlashOpacity; private float selectionDashPhase; private static float[] selectionDashProfile = new float[] {6f, 6f}; // 11 is the sum of the dash lengths minus 1. private static float selectionDashPhaseStart = 11f; private CameraViewSelectionTextDelegate selectionTextDelegate; private ScheduledExecutorService scheduledExecutor; private Preferences prefs = Preferences.userNodeForPackage(CameraView.class); private String text; private boolean showImageInfo; private List<CameraViewActionListener> actionListeners = new ArrayList<>(); private CameraViewFilter cameraViewFilter; private long flashStartTimeMs; private long flashLengthMs = 250; private boolean showName = false; private double zoom = 1d; private boolean dragJogging = false; private MouseEvent dragJoggingTarget = null; public CameraView() { setBackground(Color.black); setOpaque(true); String reticlePref = prefs.get(PREF_RETICLE, null); try { Reticle reticle = (Reticle) XmlSerialize.deserialize(reticlePref); setDefaultReticle(reticle); } catch (Exception e) { // Logger.warn("Warning: Unable to load Reticle preference"); } popupMenu = new CameraViewPopupMenu(this); addMouseListener(mouseListener); addMouseMotionListener(mouseMotionListener); addComponentListener(componentListener); addMouseWheelListener(mouseWheelListener); scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); // TODO: Cancel this when it's not being used instead of spinning, // or maybe create a real thread and wait(). scheduledExecutor.scheduleAtFixedRate(new Runnable() { public void run() { if (selectionEnabled && selection != null) { // Adjust the dash phase so the line marches on the next // paint selectionDashPhase -= 1f; if (selectionDashPhase < 0) { selectionDashPhase = selectionDashPhaseStart; } repaint(); } } }, 0, 50, TimeUnit.MILLISECONDS); } public CameraView(int maximumFps) { this(); setMaximumFps(maximumFps); } public void addActionListener(CameraViewActionListener listener) { if (!actionListeners.contains(listener)) { actionListeners.add(listener); } } public boolean removeActionListener(CameraViewActionListener listener) { return actionListeners.remove(listener); } public void setMaximumFps(int maximumFps) { this.maximumFps = maximumFps; // turn off capture for the camera we are replacing, if any if (this.camera != null) { this.camera.stopContinuousCapture(this); } // turn on capture for the new camera if (this.camera != null) { this.camera.startContinuousCapture(this, maximumFps); } } public int getMaximumFps() { return maximumFps; } public void setCamera(Camera camera) { // turn off capture for the camera we are replacing, if any if (this.camera != null) { this.camera.stopContinuousCapture(this); } this.camera = camera; // turn on capture for the new camera if (this.camera != null) { this.camera.startContinuousCapture(this, maximumFps); } } public Camera getCamera() { return camera; } public void setShowName(boolean showName) { this.showName = showName; } public boolean isShowName() { return this.showName; } public void setDefaultReticle(Reticle reticle) { setReticle(DEFAULT_RETICLE_KEY, reticle); prefs.put(PREF_RETICLE, XmlSerialize.serialize(reticle)); try { prefs.flush(); } catch (Exception e) { } } public Reticle getDefaultReticle() { return reticles.get(DEFAULT_RETICLE_KEY); } public void setReticle(Object key, Reticle reticle) { if (reticle == null) { removeReticle(key); } else { reticles.put(key, reticle); } } public Reticle getReticle(Object key) { return reticles.get(key); } public Reticle removeReticle(Object key) { return reticles.remove(key); } public CameraViewSelectionTextDelegate getSelectionTextDelegate() { return selectionTextDelegate; } public void setSelectionTextDelegate(CameraViewSelectionTextDelegate selectionTextDelegate) { this.selectionTextDelegate = selectionTextDelegate; } public String getText() { return text; } public void setText(String text) { this.text = text; } /** * Causes a short flash in the CameraView to get the user's attention. */ public void flash() { flashStartTimeMs = System.currentTimeMillis(); scheduledExecutor.scheduleAtFixedRate(new Runnable() { public void run() { if (System.currentTimeMillis() - flashStartTimeMs < flashLengthMs) { repaint(); } else { flashStartTimeMs = 0; throw new RuntimeException(); } } }, 0, 30, TimeUnit.MILLISECONDS); } public void setCameraViewFilter(CameraViewFilter cameraViewFilter) { this.cameraViewFilter = cameraViewFilter; } public void showFilteredImage(final BufferedImage filteredImage, final long milliseconds) { showFilteredImage(filteredImage, null, milliseconds); } /** * Show image instead of the camera image for milliseconds. After milliseconds elapses the view * goes back to showing the camera image. The image should be the same width and height as the * camera image otherwise the behavior is undefined. This function is intended to be used to * briefly show the result of image processing. This is a shortcut to * setCameraViewFilter(CameraViewFilter) which simply removes itself after the specified time. * * In addition to showing the given image, if the text parameters is not null the text will be * shown during the timeout using setText(). * * @param image * @param text * @param millseconds */ public void showFilteredImage(BufferedImage filteredImage, String text, long milliseconds) { if (text != null) { setText(text); } setCameraViewFilter(new CameraViewFilter() { long t = System.currentTimeMillis(); @Override public BufferedImage filterCameraImage(Camera camera, BufferedImage image) { if ((System.currentTimeMillis() - t) < milliseconds) { return filteredImage; } else { if (text != null) { setText(null); } setCameraViewFilter(null); return image; } } }); } public BufferedImage captureSelectionImage() { if (selection == null || lastFrame == null) { return null; } selectionFlashOpacity = 1.0f; ScheduledFuture future = scheduledExecutor.scheduleAtFixedRate(new Runnable() { public void run() { if (selectionFlashOpacity > 0) { selectionFlashOpacity -= 0.07; selectionFlashOpacity = Math.max(0, selectionFlashOpacity); repaint(); } else { throw new RuntimeException(); } } }, 0, 30, TimeUnit.MILLISECONDS); int sx = selection.x; int sy = selection.y; int sw = selection.width; int sh = selection.height; BufferedImage image = new BufferedImage(sw, sh, BufferedImage.TYPE_INT_ARGB); Graphics g = image.getGraphics(); g.drawImage(lastFrame, 0, 0, sw, sh, sx, sy, sx + sw, sy + sh, null); g.dispose(); while (!future.isDone()); return image; } public Rectangle getSelection() { return selection; } @Override public void frameReceived(BufferedImage img) { if (cameraViewFilter != null) { img = cameraViewFilter.filterCameraImage(camera, img); } if (img == null) { return; } BufferedImage oldFrame = lastFrame; lastFrame = img; if (oldFrame == null || (oldFrame.getWidth() != img.getWidth() || oldFrame.getHeight() != img.getHeight() || camera.getUnitsPerPixel() != lastUnitsPerPixel)) { calculateScalingData(); } repaint(); } /** * Calculates a bunch of scaling data that we cache to speed up painting. This is recalculated * when the size of the component or the size of the source changes. This method is * synchronized, along with paintComponent() so that the updates to the cached data are atomic. */ private synchronized void calculateScalingData() { BufferedImage image = lastFrame; if (image == null) { return; } Insets ins = getInsets(); int width = getWidth() - ins.left - ins.right; int height = getHeight() - ins.top - ins.bottom; double destWidth = width, destHeight = height; lastWidth = width; lastHeight = height; lastSourceWidth = image.getWidth(); lastSourceHeight = image.getHeight(); double heightRatio = lastSourceHeight / destHeight; double widthRatio = lastSourceWidth / destWidth; if (heightRatio > widthRatio) { double aspectRatio = lastSourceWidth / lastSourceHeight; scaledHeight = (int) destHeight; scaledWidth = (int) (scaledHeight * aspectRatio); } else { double aspectRatio = lastSourceHeight / lastSourceWidth; scaledWidth = (int) destWidth; scaledHeight = (int) (scaledWidth * aspectRatio); } scaledWidth *= zoom; scaledHeight *= zoom; imageX = ins.left + (width / 2) - (scaledWidth / 2); imageY = ins.top + (height / 2) - (scaledHeight / 2); scaleRatioX = lastSourceWidth / (double) scaledWidth; scaleRatioY = lastSourceHeight / (double) scaledHeight; lastUnitsPerPixel = camera.getUnitsPerPixel(); scaledUnitsPerPixelX = lastUnitsPerPixel.getX() * scaleRatioX; scaledUnitsPerPixelY = lastUnitsPerPixel.getY() * scaleRatioY; if (selectionEnabled && selection != null) { // setSelection() handles updating the scaled rectangle setSelection(selection); } } @Override protected synchronized void paintComponent(Graphics g) { super.paintComponent(g); BufferedImage image = lastFrame; Insets ins = getInsets(); int width = getWidth() - ins.left - ins.right; int height = getHeight() - ins.top - ins.bottom; Graphics2D g2d = (Graphics2D) g; g.setColor(getBackground()); g2d.fillRect(ins.left, ins.top, width, height); if (image != null) { // Only render if there is a valid image. g2d.drawImage(lastFrame, imageX, imageY, scaledWidth, scaledHeight, null); double c = MainFrame.get().getMachineControls().getSelectedTool().getLocation() .getRotation(); for (Reticle reticle : reticles.values()) { reticle.draw(g2d, camera.getUnitsPerPixel().getUnits(), scaledUnitsPerPixelX, scaledUnitsPerPixelY, ins.left + (width / 2), ins.top + (height / 2), scaledWidth, scaledHeight, c); } if (text != null) { drawTextOverlay(g2d, 10, 10, text); } if (showName) { Dimension dim = measureTextOverlay(g2d, camera.getName()); drawTextOverlay(g2d, 10, height - dim.height - 10, camera.getName()); } if (showImageInfo && text == null) { drawImageInfo(g2d, 10, 10, image); } if (selectionEnabled && selection != null) { paintSelection(g2d); } paintDragJogging(g2d); } else { g.setColor(Color.red); g.drawLine(ins.left, ins.top, ins.right, ins.bottom); g.drawLine(ins.right, ins.top, ins.left, ins.bottom); } if (flashStartTimeMs > 0) { long timeLeft = flashLengthMs - (System.currentTimeMillis() - flashStartTimeMs); float alpha = (1f / flashLengthMs) * timeLeft; alpha = Math.min(alpha, 1); alpha = Math.max(alpha, 0); g2d.setColor(new Color(1f, 1f, 1f, alpha)); g2d.fillRect(0, 0, getWidth(), getHeight()); } } private void paintDragJogging(Graphics2D g2d) { if (!isDragJogging() || dragJoggingTarget == null) { return; } Insets ins = getInsets(); int width = getWidth() - ins.left - ins.right; int height = getHeight() - ins.top - ins.bottom; g2d.setColor(Color.white); g2d.drawLine(width / 2, height / 2, dragJoggingTarget.getX(), dragJoggingTarget.getY()); } private void paintSelection(Graphics2D g2d) { int rx = selectionScaled.x; int ry = selectionScaled.y; int rw = selectionScaled.width; int rh = selectionScaled.height; int rx2 = rx + rw; int ry2 = ry + rh; int rxc = rx + rw / 2; int ryc = ry + rh / 2; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Draw the dashed rectangle, black background, white dashes g2d.setColor(Color.black); g2d.setStroke(new BasicStroke(1f)); g2d.drawRect(rx, ry, rw, rh); g2d.setColor(Color.white); g2d.setStroke(new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, selectionDashProfile, selectionDashPhase)); g2d.drawRect(rx, ry, rw, rh); if (selectionMode != SelectionMode.Creating) { // If we're not drawing a whole new rectangle, draw the // handles for the existing one. drawHandle(g2d, rx, ry); drawHandle(g2d, rx2, ry); drawHandle(g2d, rx, ry2); drawHandle(g2d, rx2, ry2); drawHandle(g2d, rxc, ry); drawHandle(g2d, rx2, ryc); drawHandle(g2d, rxc, ry2); drawHandle(g2d, rx, ryc); } if (selectionTextDelegate != null) { String text = selectionTextDelegate.getSelectionText(this); if (text != null) { // TODO: Be awesome like Apple and move the overlay inside // the rect if it goes past the edge of the window drawTextOverlay(g2d, (int) (rx + rw + 6), (int) (ry + rh + 6), text); } } if (selectionFlashOpacity > 0) { g2d.setColor(new Color(1.0f, 1.0f, 1.0f, selectionFlashOpacity)); g2d.fillRect(rx, ry, rw, rh); } } /** * Draws a standard handle centered on the given x and y position. * * @param g2d * @param x * @param y */ private static void drawHandle(Graphics2D g2d, int x, int y) { g2d.setStroke(new BasicStroke(1f)); g2d.setColor(new Color(153, 153, 187)); g2d.fillArc(x - HANDLE_DIAMETER / 2, y - HANDLE_DIAMETER / 2, HANDLE_DIAMETER, HANDLE_DIAMETER, 0, 360); g2d.setColor(Color.white); g2d.drawArc(x - HANDLE_DIAMETER / 2, y - HANDLE_DIAMETER / 2, HANDLE_DIAMETER, HANDLE_DIAMETER, 0, 360); } /** * Gets the HandlePosition, if any, at the given x and y. Returns null if there is not a handle * at that position. * * @param x * @param y * @return */ private HandlePosition getSelectionHandleAtPosition(int x, int y) { if (selection == null) { return null; } int rx = selectionScaled.x; int ry = selectionScaled.y; int rw = selectionScaled.width; int rh = selectionScaled.height; int rx2 = rx + rw; int ry2 = ry + rh; int rxc = rx + rw / 2; int ryc = ry + rh / 2; if (isWithinHandle(x, y, rx, ry)) { return HandlePosition.NW; } else if (isWithinHandle(x, y, rx2, ry)) { return HandlePosition.NE; } else if (isWithinHandle(x, y, rx, ry2)) { return HandlePosition.SW; } else if (isWithinHandle(x, y, rx2, ry2)) { return HandlePosition.SE; } else if (isWithinHandle(x, y, rxc, ry)) { return HandlePosition.N; } else if (isWithinHandle(x, y, rx2, ryc)) { return HandlePosition.E; } else if (isWithinHandle(x, y, rxc, ry2)) { return HandlePosition.S; } else if (isWithinHandle(x, y, rx, ryc)) { return HandlePosition.W; } return null; } /** * A specialization of isWithin() that uses uses the bounding box of a handle. * * @param x * @param y * @param handleX * @param handleY * @return */ private static boolean isWithinHandle(int x, int y, int handleX, int handleY) { return isWithin(x, y, handleX - 4, handleY - 4, 8, 8); } private static boolean isWithin(int pointX, int pointY, int boundsX, int boundsY, int boundsWidth, int boundsHeight) { return pointX >= boundsX && pointX <= (boundsX + boundsWidth) && pointY >= boundsY && pointY <= (boundsY + boundsHeight); } private static Rectangle normalizeRectangle(Rectangle r) { return normalizeRectangle(r.x, r.y, r.width, r.height); } /** * Builds a rectangle with the given parameters. If the width or height is negative the * corresponding x or y value is modified and the width or height is made positive. * * @param x * @param y * @param width * @param height * @return */ private static Rectangle normalizeRectangle(int x, int y, int width, int height) { if (width < 0) { width *= -1; x -= width; } if (height < 0) { height *= -1; y -= height; } return new Rectangle(x, y, width, height); } /** * Draws text in a nice bubble at the given position. Newline characters in the text cause line * breaks. * * @param g2d * @param topLeftX * @param topLeftY * @param text */ private static void drawTextOverlay(Graphics2D g2d, int topLeftX, int topLeftY, String text) { Insets insets = new Insets(10, 10, 10, 10); int interLineSpacing = 4; int cornerRadius = 8; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setStroke(new BasicStroke(1.0f)); g2d.setFont(g2d.getFont().deriveFont(12.0f)); String[] lines = text.split("\n"); List<TextLayout> textLayouts = new ArrayList<>(); int textWidth = 0, textHeight = 0; for (String line : lines) { TextLayout textLayout = new TextLayout(line, g2d.getFont(), g2d.getFontRenderContext()); textWidth = (int) Math.max(textWidth, textLayout.getBounds().getWidth()); textHeight += (int) textLayout.getBounds().getHeight() + interLineSpacing; textLayouts.add(textLayout); } textHeight -= interLineSpacing; g2d.setColor(new Color(0, 0, 0, 0.75f)); g2d.fillRoundRect(topLeftX, topLeftY, textWidth + insets.left + insets.right, textHeight + insets.top + insets.bottom, cornerRadius, cornerRadius); g2d.setColor(Color.white); g2d.drawRoundRect(topLeftX, topLeftY, textWidth + insets.left + insets.right, textHeight + insets.top + insets.bottom, cornerRadius, cornerRadius); int yPen = topLeftY + insets.top; for (TextLayout textLayout : textLayouts) { yPen += textLayout.getBounds().getHeight(); textLayout.draw(g2d, topLeftX + insets.left, yPen); yPen += interLineSpacing; } } private static Dimension measureTextOverlay(Graphics2D g2d, String text) { Insets insets = new Insets(10, 10, 10, 10); int interLineSpacing = 4; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setStroke(new BasicStroke(1.0f)); g2d.setFont(g2d.getFont().deriveFont(12.0f)); String[] lines = text.split("\n"); List<TextLayout> textLayouts = new ArrayList<>(); int textWidth = 0, textHeight = 0; for (String line : lines) { TextLayout textLayout = new TextLayout(line, g2d.getFont(), g2d.getFontRenderContext()); textWidth = (int) Math.max(textWidth, textLayout.getBounds().getWidth()); textHeight += (int) textLayout.getBounds().getHeight() + interLineSpacing; textLayouts.add(textLayout); } textHeight -= interLineSpacing; return new Dimension(textWidth + insets.left + insets.right, textHeight + insets.top + insets.bottom); } private void drawImageInfo(Graphics2D g2d, int topLeftX, int topLeftY, BufferedImage image) { if (image == null) { return; } String text = String.format("Resolution: %d x %d\nZoom: %d%%\nHistogram:", image.getWidth(), image.getHeight(), (int) (zoom * 100)); Insets insets = new Insets(10, 10, 10, 10); int interLineSpacing = 4; int cornerRadius = 8; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setStroke(new BasicStroke(1.0f)); g2d.setFont(g2d.getFont().deriveFont(12.0f)); String[] lines = text.split("\n"); List<TextLayout> textLayouts = new ArrayList<>(); int textWidth = 0, textHeight = 0; for (String line : lines) { TextLayout textLayout = new TextLayout(line, g2d.getFont(), g2d.getFontRenderContext()); textWidth = (int) Math.max(textWidth, textLayout.getBounds().getWidth()); textHeight += (int) textLayout.getBounds().getHeight() + interLineSpacing; textLayouts.add(textLayout); } textHeight -= interLineSpacing; int histogramHeight = 50 + 2; int histogramWidth = 255 + 2; int width = Math.max(textWidth, histogramWidth); int height = textHeight + histogramHeight; g2d.setColor(new Color(0, 0, 0, 0.75f)); g2d.fillRoundRect(topLeftX, topLeftY, width + insets.left + insets.right, height + insets.top + insets.bottom, cornerRadius, cornerRadius); g2d.setColor(Color.white); g2d.drawRoundRect(topLeftX, topLeftY, width + insets.left + insets.right, height + insets.top + insets.bottom, cornerRadius, cornerRadius); int yPen = topLeftY + insets.top; for (TextLayout textLayout : textLayouts) { yPen += textLayout.getBounds().getHeight(); textLayout.draw(g2d, topLeftX + insets.left, yPen); yPen += interLineSpacing; } g2d.setColor(new Color(1, 1, 1, 0.20f)); g2d.fillRect(topLeftX + insets.left, yPen, histogramWidth, histogramHeight); // Calculate the histogram long[][] histogram = new long[3][256]; for (int y = 0; y < image.getHeight(); y++) { for (int x = 0; x < image.getWidth(); x++) { int rgb = image.getRGB(x, y); int r = (rgb >> 16) & 0xff; int g = (rgb >> 8) & 0xff; int b = (rgb >> 0) & 0xff; histogram[0][r]++; histogram[1][g]++; histogram[2][b]++; } } // find the highest value in the histogram long maxVal = 0; for (int channel = 0; channel < 3; channel++) { for (int bucket = 0; bucket < 256; bucket++) { maxVal = Math.max(maxVal, histogram[channel][bucket]); } } // and scale it to 50 pixels tall double scale = 50.0 / maxVal; Color[] colors = new Color[] {Color.red, Color.green, Color.blue}; for (int channel = 0; channel < 3; channel++) { g2d.setColor(colors[channel]); for (int bucket = 0; bucket < 256; bucket++) { int value = (int) (histogram[channel][bucket] * scale); g2d.drawLine(topLeftX + insets.left + 1 + bucket, yPen + 1 + 50 - value, topLeftX + insets.left + 1 + bucket, yPen + 1 + 50 - value); } } } /** * Changes the HandlePosition to it's inverse if the given rectangle has a negative width, * height or both. * * @param r */ private static HandlePosition getOpposingHandle(Rectangle r, HandlePosition handlePosition) { if (r.getWidth() < 0 && r.getHeight() < 0) { if (handlePosition == HandlePosition.NW) { return HandlePosition.SE; } else if (handlePosition == HandlePosition.NE) { return HandlePosition.SW; } else if (handlePosition == HandlePosition.SE) { return HandlePosition.NW; } else if (handlePosition == HandlePosition.SW) { return HandlePosition.NE; } } else if (r.getWidth() < 0) { if (handlePosition == HandlePosition.NW) { return HandlePosition.NE; } else if (handlePosition == HandlePosition.NE) { return HandlePosition.NW; } else if (handlePosition == HandlePosition.SE) { return HandlePosition.SW; } else if (handlePosition == HandlePosition.SW) { return HandlePosition.SE; } else if (handlePosition == HandlePosition.E) { return HandlePosition.W; } else if (handlePosition == HandlePosition.W) { return HandlePosition.E; } } else if (r.getHeight() < 0) { if (handlePosition == HandlePosition.SW) { return HandlePosition.NW; } else if (handlePosition == HandlePosition.SE) { return HandlePosition.NE; } else if (handlePosition == HandlePosition.NW) { return HandlePosition.SW; } else if (handlePosition == HandlePosition.NE) { return HandlePosition.SE; } else if (handlePosition == HandlePosition.S) { return HandlePosition.N; } else if (handlePosition == HandlePosition.N) { return HandlePosition.S; } } return handlePosition; } /** * Set the selection rectangle in image coordinates. * * @param x * @param y * @param width * @param height */ public void setSelection(int x, int y, int width, int height) { setSelection(new Rectangle(x, y, width, height)); } /** * Set the selection rectangle in image coordinates. * * @param r */ public void setSelection(Rectangle r) { if (r == null) { selection = null; selectionMode = null; } else { selectionActiveHandle = getOpposingHandle(r, selectionActiveHandle); selection = normalizeRectangle(r); int rx = (int) (imageX + selection.x / scaleRatioX); int ry = (int) (imageY + selection.y / scaleRatioY); int rw = (int) (selection.width / scaleRatioX); int rh = (int) (selection.height / scaleRatioY); selectionScaled = new Rectangle(rx, ry, rw, rh); } } /** * Set the selection rectangle in component coordinates. Updates the selection property with the * properly scaled coordinates. * * @param x * @param y * @param width * @param height */ private void setScaledSelection(int x, int y, int width, int height) { selectionScaled = new Rectangle(x, y, width, height); selectionActiveHandle = getOpposingHandle(selectionScaled, selectionActiveHandle); selectionScaled = normalizeRectangle(selectionScaled); int rx = (int) ((x - imageX) * scaleRatioX); int ry = (int) ((y - imageY) * scaleRatioY); int rw = (int) (width * scaleRatioX); int rh = (int) (height * scaleRatioY); selection = new Rectangle(rx, ry, rw, rh); } public boolean isSelectionEnabled() { return selectionEnabled; } public void setSelectionEnabled(boolean selectionEnabled) { this.selectionEnabled = selectionEnabled; } public boolean isShowImageInfo() { return showImageInfo; } public void setShowImageInfo(boolean showImageInfo) { this.showImageInfo = showImageInfo; } public static Cursor getCursorForHandlePosition(HandlePosition handlePosition) { switch (handlePosition) { case NW: return Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR); case N: return Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR); case NE: return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR); case E: return Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR); case SE: return Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR); case S: return Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR); case SW: return Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR); case W: return Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR); } return null; } /** * Updates the Cursor to reflect the current state of the component. */ private void updateCursor() { if (selectionEnabled) { if (selectionMode == SelectionMode.Moving) { setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); } else if (selectionMode == SelectionMode.Resizing) { setCursor(getCursorForHandlePosition(selectionActiveHandle)); } else if (selectionMode == null && selection != null) { int x = getMousePosition().x; int y = getMousePosition().y; HandlePosition handlePosition = getSelectionHandleAtPosition(x, y); if (handlePosition != null) { setCursor(getCursorForHandlePosition(handlePosition)); } else if (selectionScaled.contains(x, y)) { setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); } else { setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); } } else { setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); } } else { setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); } } /** * Capture the current image (unscaled, unmodified) and write it to disk. */ private void captureSnapshot() { try { flash(); File dir = new File(Configuration.get().getConfigurationDirectory(), "snapshots"); dir.mkdirs(); DateFormat df = new SimpleDateFormat("YYYY-MM-dd_HH.mm.ss.SSS"); File file = new File(dir, camera.getName() + "_" + df.format(new Date()) + ".png"); ImageIO.write(lastFrame, "png", file); } catch (Exception e1) { e1.printStackTrace(); } } private void fireActionEvent(MouseEvent e) { if (actionListeners.isEmpty()) { return; } int x = e.getX(); int y = e.getY(); // Find the difference in X and Y from the center of the image // to the mouse click. double offsetX = (scaledWidth / 2.0D) - (x - imageX); double offsetY = (scaledHeight / 2.0D) - (y - imageY); // Invert the X so that the offsets represent a bottom left to // top right coordinate system. offsetX = -offsetX; // Scale the offsets by the units per pixel for the camera. offsetX *= scaledUnitsPerPixelX; offsetY *= scaledUnitsPerPixelY; // The offsets now represent the distance to move the camera // in the Camera's units per pixel's units. // Create a location in the Camera's units per pixel's units // and with the values of the offsets. Location offsets = camera.getUnitsPerPixel().derive(offsetX, offsetY, 0.0, 0.0); // Add the offsets to the Camera's position. Location location = camera.getLocation().add(offsets); CameraViewActionEvent action = new CameraViewActionEvent(CameraView.this, e.getX(), e.getY(), e.getX() * scaledUnitsPerPixelX, e.getY() * scaledUnitsPerPixelY, location); for (CameraViewActionListener listener : new ArrayList<>(actionListeners)) { listener.actionPerformed(action); } } private void moveToClick(MouseEvent e) { int x = e.getX(); int y = e.getY(); // Find the difference in X and Y from the center of the image // to the mouse click. double offsetX = (scaledWidth / 2.0D) - (x - imageX); double offsetY = (scaledHeight / 2.0D) - (y - imageY) + 1; // Invert the X so that the offsets represent a bottom left to // top right coordinate system. offsetX = -offsetX; // Scale the offsets by the units per pixel for the camera. offsetX *= scaledUnitsPerPixelX; offsetY *= scaledUnitsPerPixelY; // The offsets now represent the distance to move the camera // in the Camera's units per pixel's units. // Create a location in the Camera's units per pixel's units // and with the values of the offsets. Location offsets = camera.getUnitsPerPixel().derive(offsetX, offsetY, 0.0, 0.0); // Add the offsets to the Camera's position. Location location = camera.getLocation().add(offsets); // And move there. UiUtils.submitUiMachineTask(() -> { if (camera.getHead() == null) { // move the nozzle to the camera Nozzle nozzle = MainFrame.get().getMachineControls().getSelectedNozzle(); MovableUtils.moveToLocationAtSafeZ(nozzle, location); } else { // move the camera to the location MovableUtils.moveToLocationAtSafeZ(camera, location); } }); } private void beginSelection(MouseEvent e) { // If we're not doing anything currently, we can start // a new operation. if (selectionMode == null) { int x = e.getX(); int y = e.getY(); // See if there is a handle under the cursor. HandlePosition handlePosition = getSelectionHandleAtPosition(x, y); if (handlePosition != null) { selectionMode = SelectionMode.Resizing; selectionActiveHandle = handlePosition; } // If not, perhaps they want to move the rectangle else if (selection != null && selectionScaled.contains(x, y)) { selectionMode = SelectionMode.Moving; // Store the distance between the rectangle's origin and // where they started moving it from. selectionStartX = x - selectionScaled.x; selectionStartY = y - selectionScaled.y; } // If not those, it's time to create a rectangle else { selectionMode = SelectionMode.Creating; selectionStartX = x; selectionStartY = y; } } } private void continueSelection(MouseEvent e) { int x = e.getX(); int y = e.getY(); if (selectionMode == SelectionMode.Resizing) { int rx = selectionScaled.x; int ry = selectionScaled.y; int rw = selectionScaled.width; int rh = selectionScaled.height; if (selectionActiveHandle == HandlePosition.NW) { setScaledSelection(x, y, (rw - (x - rx)), (rh - (y - ry))); } else if (selectionActiveHandle == HandlePosition.NE) { setScaledSelection(rx, y, x - rx, (rh - (y - ry))); } else if (selectionActiveHandle == HandlePosition.N) { setScaledSelection(rx, y, rw, (rh - (y - ry))); } else if (selectionActiveHandle == HandlePosition.E) { setScaledSelection(rx, ry, rw + (x - (rx + rw)), rh); } else if (selectionActiveHandle == HandlePosition.SE) { setScaledSelection(rx, ry, rw + (x - (rx + rw)), rh + (y - (ry + rh))); } else if (selectionActiveHandle == HandlePosition.S) { setScaledSelection(rx, ry, rw, rh + (y - (ry + rh))); } else if (selectionActiveHandle == HandlePosition.SW) { setScaledSelection(x, ry, (rw - (x - rx)), rh + (y - (ry + rh))); } else if (selectionActiveHandle == HandlePosition.W) { setScaledSelection(x, ry, (rw - (x - rx)), rh); } } else if (selectionMode == SelectionMode.Moving) { setScaledSelection(x - selectionStartX, y - selectionStartY, selectionScaled.width, selectionScaled.height); } else if (selectionMode == SelectionMode.Creating) { int sx = selectionStartX; int sy = selectionStartY; int w = x - sx; int h = y - sy; setScaledSelection(sx, sy, w, h); } updateCursor(); repaint(); } private void endSelection() { selectionMode = null; selectionActiveHandle = null; } private void dragJoggingBegin(MouseEvent e) { this.dragJogging = true; this.dragJoggingTarget = e; repaint(); } private void dragJoggingContinue(MouseEvent e) { this.dragJoggingTarget = e; repaint(); } private void dragJoggingEnd(MouseEvent e) { this.dragJogging = false; this.dragJoggingTarget = null; repaint(); moveToClick(e); } private boolean isDragJogging() { return this.dragJogging; } private MouseListener mouseListener = new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.isPopupTrigger() || e.isShiftDown() || SwingUtilities.isRightMouseButton(e)) { return; } // double click captures an image from the camera and writes it to disk. if (e.getClickCount() == 2) { captureSnapshot(); } else { fireActionEvent(e); } } @Override public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) { popupMenu.show(e.getComponent(), e.getX(), e.getY()); return; } else if (e.isShiftDown()) { moveToClick(e); } else if (selectionEnabled) { beginSelection(e); } } @Override public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) { popupMenu.show(e.getComponent(), e.getX(), e.getY()); return; } else if (isDragJogging()) { dragJoggingEnd(e); } else { endSelection(); } } }; private MouseMotionListener mouseMotionListener = new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { updateCursor(); } @Override public void mouseDragged(MouseEvent e) { if (selectionEnabled) { continueSelection(e); } else if (!isDragJogging()) { dragJoggingBegin(e); } else if (isDragJogging()) { dragJoggingContinue(e); } } }; private ComponentListener componentListener = new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { calculateScalingData(); } }; private MouseWheelListener mouseWheelListener = new MouseWheelListener() { @Override public void mouseWheelMoved(MouseWheelEvent e) { zoom -= e.getPreciseWheelRotation() * 0.01d; zoom = Math.max(zoom, 1.0d); zoom = Math.min(zoom, 100d); calculateScalingData(); repaint(); } }; public CameraViewSelectionTextDelegate pixelsAndUnitsTextSelectionDelegate = new CameraViewSelectionTextDelegate() { @Override public String getSelectionText(CameraView cameraView) { double widthInUnits = selection.width * camera.getUnitsPerPixel().getX(); double heightInUnits = selection.height * camera.getUnitsPerPixel().getY(); String text = String.format(Locale.US, "%dpx, %dpx\n%2.3f%s, %2.3f%s", (int) selection.getWidth(), (int) selection.getHeight(), widthInUnits, camera.getUnitsPerPixel().getUnits().getShortName(), heightInUnits, camera.getUnitsPerPixel().getUnits().getShortName()); return text; } }; }