package com.github.sarxos.webcam; import static java.awt.RenderingHints.KEY_ANTIALIASING; import static java.awt.RenderingHints.KEY_INTERPOLATION; import static java.awt.RenderingHints.KEY_RENDERING; import static java.awt.RenderingHints.VALUE_ANTIALIAS_OFF; import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON; import static java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR; import static java.awt.RenderingHints.VALUE_RENDER_SPEED; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.GraphicsEnvironment; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import javax.swing.JPanel; import javax.swing.SwingUtilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Simply implementation of JPanel allowing users to render pictures taken with * webcam. * * @author Bartosz Firyn (SarXos) */ public class WebcamPanel extends JPanel implements WebcamListener, PropertyChangeListener { /** * This enum is to control of how image will be drawn in the panel bounds. * * @author Sylwia Kauczor */ public enum DrawMode { /** * Do not resize image - paint it as it is. This will make the image to * go off out the bounds if panel is smaller than image size. */ NONE, /** * Will resize image to the panel bounds. This mode does not care of the * image scale, so the final image may be disrupted. */ FILL, /** * Will fir image into the panel bounds. This will resize the image and * keep both x and y scale factor. */ FIT, } /** * Interface of the painter used to draw image in panel. * * @author Bartosz Firyn (SarXos) */ public interface Painter { /** * Paint panel without image. * * @param panel the webcam panel to paint on * @param g2 the graphics 2D object used for drawing */ void paintPanel(WebcamPanel panel, Graphics2D g2); /** * Paint webcam image in panel. * * @param panel the webcam panel to paint on * @param image the image from webcam * @param g2 the graphics 2D object used for drawing */ void paintImage(WebcamPanel panel, BufferedImage image, Graphics2D g2); } /** * Default painter used to draw image in panel. * * @author Bartosz Firyn (SarXos) * @author Sylwia Kauczor */ public class DefaultPainter implements Painter { /** * Webcam device name. */ private String name = null; /** * Lat repaint time, uset for debug purpose. */ private long lastRepaintTime = -1; /** * Buffered image resized to fit into panel drawing area. */ private BufferedImage resizedImage = null; @Override public void paintPanel(WebcamPanel owner, Graphics2D g2) { assert owner != null; assert g2 != null; Object antialiasing = g2.getRenderingHint(KEY_ANTIALIASING); g2.setRenderingHint(KEY_ANTIALIASING, isAntialiasingEnabled() ? VALUE_ANTIALIAS_ON : VALUE_ANTIALIAS_OFF); g2.setBackground(Color.BLACK); g2.fillRect(0, 0, getWidth(), getHeight()); int cx = (getWidth() - 70) / 2; int cy = (getHeight() - 40) / 2; g2.setStroke(new BasicStroke(2)); g2.setColor(Color.LIGHT_GRAY); g2.fillRoundRect(cx, cy, 70, 40, 10, 10); g2.setColor(Color.WHITE); g2.fillOval(cx + 5, cy + 5, 30, 30); g2.setColor(Color.LIGHT_GRAY); g2.fillOval(cx + 10, cy + 10, 20, 20); g2.setColor(Color.WHITE); g2.fillOval(cx + 12, cy + 12, 16, 16); g2.fillRoundRect(cx + 50, cy + 5, 15, 10, 5, 5); g2.fillRect(cx + 63, cy + 25, 7, 2); g2.fillRect(cx + 63, cy + 28, 7, 2); g2.fillRect(cx + 63, cy + 31, 7, 2); g2.setColor(Color.DARK_GRAY); g2.setStroke(new BasicStroke(3)); g2.drawLine(0, 0, getWidth(), getHeight()); g2.drawLine(0, getHeight(), getWidth(), 0); String str; final String strInitDevice = rb.getString("INITIALIZING_DEVICE"); final String strNoImage = rb.getString("NO_IMAGE"); final String strDeviceError = rb.getString("DEVICE_ERROR"); if (!errored) { str = starting ? strInitDevice : strNoImage; } else { str = strDeviceError; } FontMetrics metrics = g2.getFontMetrics(getFont()); int w = metrics.stringWidth(str); int h = metrics.getHeight(); int x = (getWidth() - w) / 2; int y = cy - h; g2.setFont(getFont()); g2.setColor(Color.WHITE); g2.drawString(str, x, y); if (name == null) { name = webcam.getName(); } str = name; w = metrics.stringWidth(str); h = metrics.getHeight(); g2.drawString(str, (getWidth() - w) / 2, cy - 2 * h); g2.setRenderingHint(KEY_ANTIALIASING, antialiasing); } @Override public void paintImage(WebcamPanel owner, BufferedImage image, Graphics2D g2) { assert owner != null; assert image != null; assert g2 != null; int pw = getWidth(); int ph = getHeight(); int iw = image.getWidth(); int ih = image.getHeight(); Object antialiasing = g2.getRenderingHint(KEY_ANTIALIASING); Object rendering = g2.getRenderingHint(KEY_RENDERING); g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF); g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_SPEED); g2.setBackground(Color.BLACK); g2.setColor(Color.BLACK); g2.fillRect(0, 0, pw, ph); // resized image position and size int x = 0; int y = 0; int w = 0; int h = 0; switch (drawMode) { case NONE: w = image.getWidth(); h = image.getHeight(); break; case FILL: w = pw; h = ph; break; case FIT: double s = Math.max((double) iw / pw, (double) ih / ph); double niw = iw / s; double nih = ih / s; double dx = (pw - niw) / 2; double dy = (ph - nih) / 2; w = (int) niw; h = (int) nih; x = (int) dx; y = (int) dy; break; } if (resizedImage != null) { resizedImage.flush(); } if (w == image.getWidth() && h == image.getHeight() && !mirrored) { resizedImage = image; } else { GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsConfiguration gc = genv.getDefaultScreenDevice().getDefaultConfiguration(); Graphics2D gr = null; try { resizedImage = gc.createCompatibleImage(pw, ph); gr = resizedImage.createGraphics(); gr.setComposite(AlphaComposite.Src); for (Map.Entry<RenderingHints.Key, Object> hint : imageRenderingHints.entrySet()) { gr.setRenderingHint(hint.getKey(), hint.getValue()); } gr.setBackground(Color.BLACK); gr.setColor(Color.BLACK); gr.fillRect(0, 0, pw, ph); int sx1, sx2, sy1, sy2; // source rectangle coordinates int dx1, dx2, dy1, dy2; // destination rectangle coordinates dx1 = x; dy1 = y; dx2 = x + w; dy2 = y + h; if (mirrored) { sx1 = iw; sy1 = 0; sx2 = 0; sy2 = ih; } else { sx1 = 0; sy1 = 0; sx2 = iw; sy2 = ih; } gr.drawImage(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null); } finally { if (gr != null) { gr.dispose(); } } } g2.drawImage(resizedImage, 0, 0, null); if (isFPSDisplayed()) { String str = String.format("FPS: %.1f", webcam.getFPS()); int sx = 5; int sy = ph - 5; g2.setFont(getFont()); g2.setColor(Color.BLACK); g2.drawString(str, sx + 1, sy + 1); g2.setColor(Color.WHITE); g2.drawString(str, sx, sy); } if (isImageSizeDisplayed()) { String res = String.format("%d\u2A2F%d px", iw, ih); FontMetrics metrics = g2.getFontMetrics(getFont()); int sw = metrics.stringWidth(res); int sx = pw - sw - 5; int sy = ph - 5; g2.setFont(getFont()); g2.setColor(Color.BLACK); g2.drawString(res, sx + 1, sy + 1); g2.setColor(Color.WHITE); g2.drawString(res, sx, sy); } if (isDisplayDebugInfo()) { if (lastRepaintTime < 0) { lastRepaintTime = System.currentTimeMillis(); } else { long now = System.currentTimeMillis(); String res = String.format("DEBUG: repaints per second: %.1f", (double) 1000 / (now - lastRepaintTime)); lastRepaintTime = now; g2.setFont(getFont()); g2.setColor(Color.BLACK); g2.drawString(res, 6, 16); g2.setColor(Color.WHITE); g2.drawString(res, 5, 15); } } g2.setRenderingHint(KEY_ANTIALIASING, antialiasing); g2.setRenderingHint(KEY_RENDERING, rendering); } } private static final class PanelThreadFactory implements ThreadFactory { private static final AtomicInteger number = new AtomicInteger(0); @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, String.format("webcam-panel-scheduled-executor-%d", number.incrementAndGet())); t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); t.setDaemon(true); return t; } } /** * This runnable will do nothing more than repaint panel. */ private static final class SwingRepainter implements Runnable { private WebcamPanel panel = null; public SwingRepainter(WebcamPanel panel) { this.panel = panel; } @Override public void run() { panel.repaint(); } } /** * S/N used by Java to serialize beans. */ private static final long serialVersionUID = 1L; /** * Logger. */ private static final Logger LOG = LoggerFactory.getLogger(WebcamPanel.class); /** * Minimum FPS frequency. */ public static final double MIN_FREQUENCY = 0.016; // 1 frame per minute /** * Maximum FPS frequency. */ private static final double MAX_FREQUENCY = 50; // 50 frames per second /** * Thread factory used by execution service. */ private static final ThreadFactory THREAD_FACTORY = new PanelThreadFactory(); public static final Map<RenderingHints.Key, Object> DEFAULT_IMAGE_RENDERING_HINTS = new HashMap<RenderingHints.Key, Object>(); static { DEFAULT_IMAGE_RENDERING_HINTS.put(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR); DEFAULT_IMAGE_RENDERING_HINTS.put(KEY_RENDERING, VALUE_RENDER_SPEED); DEFAULT_IMAGE_RENDERING_HINTS.put(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); } /** * This runnable will do nothing more than repaint panel. */ private final Runnable repaint = new SwingRepainter(this); /** * Rendering hints to be used when painting image to be displayed. */ private Map<RenderingHints.Key, Object> imageRenderingHints = new HashMap<RenderingHints.Key, Object>(DEFAULT_IMAGE_RENDERING_HINTS); /** * Scheduled executor acting as timer. */ private ScheduledExecutorService executor = null; /** * Image updater reads images from camera and force panel to be repainted. * * @author Bartosz Firyn (SarXos) */ private class ImageUpdater implements Runnable { /** * Repaint scheduler schedule panel updates. * * @author Bartosz Firyn (sarxos) */ private class RepaintScheduler extends Thread { /** * Repaint scheduler schedule panel updates. */ public RepaintScheduler() { setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); setName(String.format("repaint-scheduler-%s", webcam.getName())); setDaemon(true); } @Override public void run() { // do nothing when not running if (!running.get()) { return; } repaintPanel(); // loop when starting, to wait for images while (starting) { try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } } // schedule update when webcam is open, otherwise schedule // second scheduler execution try { // FPS limit means that panel rendering frequency is // limited to the specific value and panel will not be // rendered more often then specific value if (webcam.isOpen()) { // TODO: rename FPS value in panel to rendering // frequency if (isFPSLimited()) { executor.scheduleAtFixedRate(updater, 0, (long) (1000 / frequency), TimeUnit.MILLISECONDS); } else { executor.scheduleWithFixedDelay(updater, 100, 1, TimeUnit.MILLISECONDS); } } else { executor.schedule(this, 500, TimeUnit.MILLISECONDS); } } catch (RejectedExecutionException e) { // executor has been shut down, which means that someone // stopped panel / webcam device before it was actually // completely started (it was in "starting" timeframe) LOG.warn("Executor rejected paint update"); LOG.trace("Executor rejected paint update because of", e); } } } /** * Update scheduler thread. */ private Thread scheduler = null; /** * Is repainter running? */ private AtomicBoolean running = new AtomicBoolean(false); /** * Start repainter. Can be invoked many times, but only first call will * take effect. */ public void start() { if (running.compareAndSet(false, true)) { executor = Executors.newScheduledThreadPool(1, THREAD_FACTORY); scheduler = new RepaintScheduler(); scheduler.start(); } } /** * Stop repainter. Can be invoked many times, but only first call will * take effect. * * @throws InterruptedException */ public void stop() throws InterruptedException { if (running.compareAndSet(true, false)) { executor.shutdown(); executor.awaitTermination(5000, TimeUnit.MILLISECONDS); scheduler.join(); } } @Override public void run() { try { update(); } catch (Throwable t) { errored = true; WebcamExceptionHandler.handle(t); } } /** * Perform single panel area update (repaint newly obtained image). */ private void update() { // do nothing when updater not running, when webcam is closed, or // panel repainting is paused if (!running.get() || !webcam.isOpen() || paused) { return; } // get new image from webcam BufferedImage tmp = webcam.getImage(); boolean repaint = true; if (tmp != null) { // ignore repaint if image is the same as before if (image == tmp) { repaint = false; } errored = false; image = tmp; } if (repaint) { repaintPanel(); } } } /** * Resource bundle. */ private ResourceBundle rb = null; /** * The mode of how the image will be resized to fit into panel bounds. * Default is {@link DrawMode#FIT} * * @see DrawMode */ private DrawMode drawMode = DrawMode.FIT; /** * Frames requesting frequency. */ private double frequency = 5; // FPS /** * Is frames requesting frequency limited? If true, images will be fetched * in configured time intervals. If false, images will be fetched as fast as * camera can serve them. */ private boolean frequencyLimit = false; /** * Display FPS. */ private boolean frequencyDisplayed = false; /** * Display image size. */ private boolean imageSizeDisplayed = false; /** * Is antialiasing enabled (true by default). */ private boolean antialiasingEnabled = true; /** * Webcam object used to fetch images. */ private final Webcam webcam; /** * Repainter is used to fetch images from camera and force panel repaint * when image is ready. */ private final ImageUpdater updater; /** * Image currently being displayed. */ private BufferedImage image = null; /** * Webcam is currently starting. */ private volatile boolean starting = false; /** * Painting is paused. */ private volatile boolean paused = false; /** * Is there any problem with webcam? */ private volatile boolean errored = false; /** * Webcam has been started. */ private final AtomicBoolean started = new AtomicBoolean(false); /** * Default painter. */ private final Painter defaultPainter = new DefaultPainter(); /** * Painter used to draw image in panel. * * @see #setPainter(Painter) * @see #getPainter() */ private Painter painter = defaultPainter; /** * Preferred panel size. */ private Dimension defaultSize = null; /** * If debug info should be displayed. */ private boolean displayDebugInfo = false; /** * Is image mirrored. */ private boolean mirrored = false; /** * Creates webcam panel and automatically start webcam. * * @param webcam the webcam to be used to fetch images */ public WebcamPanel(Webcam webcam) { this(webcam, true); } /** * Creates new webcam panel which display image from camera in you your * Swing application. * * @param webcam the webcam to be used to fetch images * @param start true if webcam shall be automatically started */ public WebcamPanel(Webcam webcam, boolean start) { this(webcam, null, start); } /** * Creates new webcam panel which display image from camera in you your * Swing application. If panel size argument is null, then image size will * be used. If you would like to fill panel area with image even if its size * is different, then you can use {@link WebcamPanel#setFillArea(boolean)} * method to configure this. * * @param webcam the webcam to be used to fetch images * @param size the size of panel * @param start true if webcam shall be automatically started * @see WebcamPanel#setFillArea(boolean) */ public WebcamPanel(Webcam webcam, Dimension size, boolean start) { if (webcam == null) { throw new IllegalArgumentException(String.format("Webcam argument in %s constructor cannot be null!", getClass().getSimpleName())); } this.defaultSize = size; this.webcam = webcam; this.updater = new ImageUpdater(); this.rb = WebcamUtils.loadRB(WebcamPanel.class, getLocale()); setDoubleBuffered(true); addPropertyChangeListener("locale", this); if (size == null) { Dimension r = webcam.getViewSize(); if (r == null) { r = webcam.getViewSizes()[0]; } setPreferredSize(r); } else { setPreferredSize(size); } if (start) { start(); } } /** * Set new painter. Painter is a class which pains image visible when * * @param painter the painter object to be set */ public void setPainter(Painter painter) { this.painter = painter; } /** * Get painter used to draw image in webcam panel. * * @return Painter object */ public Painter getPainter() { return painter; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (image == null) { painter.paintPanel(this, (Graphics2D) g); } else { painter.paintImage(this, image, (Graphics2D) g); } } /** * Open webcam and start rendering. */ public void start() { if (!started.compareAndSet(false, true)) { return; } webcam.addWebcamListener(this); LOG.debug("Starting panel rendering and trying to open attached webcam"); updater.start(); starting = true; try { if (!webcam.isOpen()) { errored = !webcam.open(); } } catch (WebcamException e) { errored = true; throw e; } finally { starting = false; repaintPanel(); } } /** * Stop rendering and close webcam. */ public void stop() { if (!started.compareAndSet(true, false)) { return; } webcam.removeWebcamListener(this); LOG.debug("Stopping panel rendering and closing attached webcam"); try { updater.stop(); } catch (InterruptedException e) { throw new RuntimeException(e); } image = null; try { if (webcam.isOpen()) { errored = !webcam.close(); } } catch (WebcamException e) { errored = true; throw e; } finally { repaintPanel(); } } /** * Repaint panel in Swing asynchronous manner. */ private void repaintPanel() { SwingUtilities.invokeLater(repaint); } /** * Pause rendering. */ public void pause() { if (paused) { return; } LOG.debug("Pausing panel rendering"); paused = true; } /** * Resume rendering. */ public void resume() { if (!paused) { return; } LOG.debug("Resuming panel rendering"); paused = false; } /** * Is frequency limit enabled? * * @return True or false */ public boolean isFPSLimited() { return frequencyLimit; } /** * Enable or disable frequency limit. Frequency limit should be used for * <b>all IP cameras working in pull mode</b> (to save number of HTTP * requests). If true, images will be fetched in configured time intervals. * If false, images will be fetched as fast as camera can serve them. * * @param frequencyLimit true if limiting the frequency of image requests */ public void setFPSLimited(boolean frequencyLimit) { this.frequencyLimit = frequencyLimit; } /** * Get rendering frequency in FPS (equivalent to Hz). * * @return Rendering frequency */ public double getFPSLimit() { return frequency; } /** * Set rendering frequency (in Hz or FPS). Minimum frequency is 0.016 (1 * frame per minute) and maximum is 25 (25 frames per second). * * @param fps the frequency */ public void setFPSLimit(double fps) { if (fps > MAX_FREQUENCY) { fps = MAX_FREQUENCY; } if (fps < MIN_FREQUENCY) { fps = MIN_FREQUENCY; } this.frequency = fps; } /** * Is displaying of some debug information enabled. * * @return True if debug information are enabled, false otherwise */ public boolean isDisplayDebugInfo() { return displayDebugInfo; } /** * Display some debug information on image surface. * * @param displayDebugInfo the value to control debug information */ public void setDisplayDebugInfo(boolean displayDebugInfo) { this.displayDebugInfo = displayDebugInfo; } /** * This method return true in case if camera FPS is set to be displayed on * panel surface. Default value returned is false. * * @return True if camera FPS is set to be displayed on panel surface * @see #setFPSDisplayed(boolean) */ public boolean isFPSDisplayed() { return frequencyDisplayed; } /** * This method is to control if camera FPS should be displayed on the webcam * panel surface. * * @param displayed the value to control if camera FPS should be displayed */ public void setFPSDisplayed(boolean displayed) { this.frequencyDisplayed = displayed; } /** * This method will return true in case when panel is configured to display * image size. The string will be printed in the right bottom corner of the * panel surface. * * @return True in case if panel is configured to display image size */ public boolean isImageSizeDisplayed() { return imageSizeDisplayed; } /** * Configure panel to display camera image size to be displayed. * * @param imageSizeDisplayed if true the pixel dimensions are displayed over the image. */ public void setImageSizeDisplayed(boolean imageSizeDisplayed) { this.imageSizeDisplayed = imageSizeDisplayed; } /** * Turn on/off antialiasing. * * @param antialiasing the true to enable, false to disable antialiasing */ public void setAntialiasingEnabled(boolean antialiasing) { this.antialiasingEnabled = antialiasing; } /** * @return True is antialiasing is enabled, false otherwise */ public boolean isAntialiasingEnabled() { return antialiasingEnabled; } /** * Is webcam panel repainting starting. * * @return True if panel is starting */ public boolean isStarting() { return starting; } /** * Is webcam panel repainting started. * * @return True if panel repainting has been started */ public boolean isStarted() { return started.get(); } /** * This method returns the current draw mode, mainly used by custom painters * @return the current value of the {@link DrawMode} */ public DrawMode getDrawMode(){ return this.drawMode; } /** * This method sets the drawmode * @param drawMode the desired {@link DrawMode} */ public void setDrawMode(DrawMode drawMode){ this.drawMode = drawMode; } /** * Indicates whether the panel is in an error state * @return true if the panel has an error present */ public boolean isErrored(){ return errored; } /** * Hints for rendering, mainly used for custom painters * @return the stored RenderingHints * @deprecated use {@link #getDrawMode()} instead. */ public Map<RenderingHints.Key, Object> getImageRenderingHints() { return imageRenderingHints; } @Deprecated public boolean isFitArea() { return drawMode == DrawMode.FIT; } /** * This method will change the mode of panel area painting so the image will * be resized and will keep scale factor to fit into drawable panel bounds. * When set to false, the mode will be reset to {@link DrawMode#NONE} so * image will be drawn as it is. * * @param fitArea the fit area mode enabled or disabled * @deprecated use {@link #setDrawMode(DrawMode drawMode)} instead. */ @Deprecated public void setFitArea(boolean fitArea) { this.drawMode = fitArea ? DrawMode.FIT : DrawMode.NONE; } /** * Image will be resized to fill panel area if true. If false then image * will be rendered as it was obtained from webcam instance. * * @param fillArea shall image be resided to fill panel area * @deprecated use {@link #setDrawMode(DrawMode drawMode)} instead. */ @Deprecated public void setFillArea(boolean fillArea) { this.drawMode = fillArea ? DrawMode.FILL : DrawMode.NONE; } /** * Get value of fill area setting. Image will be resized to fill panel area * if true. If false then image will be rendered as it was obtained from * webcam instance. * * @return True if image is being resized, false otherwise * @deprecated use {@link #getDrawMode()} instead. */ @Deprecated public boolean isFillArea() { return drawMode == DrawMode.FILL; } /** * Get default painter used to draw panel. * * @return Default painter */ public Painter getDefaultPainter() { return defaultPainter; } @Override public void propertyChange(PropertyChangeEvent evt) { Locale lc = (Locale) evt.getNewValue(); if (lc != null) { rb = WebcamUtils.loadRB(WebcamPanel.class, lc); } } @Override public void webcamOpen(WebcamEvent we) { // if default size has not been provided, then use the one from webcam // device (this will be current webcam resolution) if (defaultSize == null) { setPreferredSize(webcam.getViewSize()); } } @Override public void webcamClosed(WebcamEvent we) { stop(); } @Override public void webcamDisposed(WebcamEvent we) { stop(); } @Override public void webcamImageObtained(WebcamEvent we) { // do nothing } /** * This method returns true if image mirroring is enabled. The default value * is false. * * @return True if image is mirrored, false otherwise */ public boolean isMirrored() { return mirrored; } /** * Decide whether or not the image from webcam painted on panel surface will * be mirrored. The image from camera itself is not modified. * * @param mirrored the parameter to control if image should be mirrored */ public void setMirrored(boolean mirrored) { this.mirrored = mirrored; } /** * Return {@link Webcam} used by this panel. * * @return {@link Webcam} */ public Webcam getWebcam() { return webcam; } }