org.openpnp.machine.reference.camera.TableScannerCamera.java Source code

Java tutorial

Introduction

Here is the source code for org.openpnp.machine.reference.camera.TableScannerCamera.java

Source

/*
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.machine.reference.camera;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeSupport;
import java.io.BufferedReader;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

import javax.imageio.ImageIO;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.openpnp.CameraListener;
import org.openpnp.gui.support.Wizard;
import org.openpnp.machine.reference.ReferenceCamera;
import org.openpnp.machine.reference.camera.wizards.TableScannerCameraConfigurationWizard;
import org.openpnp.model.Configuration;
import org.openpnp.model.LengthUnit;
import org.openpnp.model.Location;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.core.Commit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An implementation of Camera that renders a viewport into a large map
 * of tiles. Tiles are PNG files stored with filenames that denote
 * their position in real space. Ex: 23.456,45.641.png.
 * 
 * TODO: Allow specifying height which will create a larger tile and then
 * scale it down.
 */
public class TableScannerCamera extends ReferenceCamera implements Runnable {
    private final static Logger logger = LoggerFactory.getLogger(TableScannerCamera.class);

    private PropertyChangeSupport pcs = new PropertyChangeSupport(this);

    @Element
    private String sourceUri;

    @Attribute(required = false)
    private int fps = 24;

    private int tilesWide = 3;
    private int tilesHigh = 3;

    /**
     * The last X and Y position that we rendered for. Used to optimize the
     * renderer and not generate duplicate frames.
     */
    private double lastX = Double.MIN_VALUE, lastY = Double.MIN_VALUE;

    /**
     * Two dimensional array representing the layout of the entire set of
     * tiles.
     */
    private Tile[][] tiles;

    /**
     * List of all of the tiles. Used when searching for closest matches.
     */
    private List<Tile> tileList;

    /**
     * Buffered used to render the tiles local to the center point. This buffer
     * is tilesWide * imageWidth by tilesHigh * imageHeight in pixels. By
     * buffering this data we are often able to render multiple frames during
     * small movements.
     */
    private BufferedImage buffer;

    /**
     * The last tile that was used to compute the local tile array. Used to
     * avoid re-rendering when the head has moved less than a tile since the
     * last update.
     */
    private Tile lastCenterTile;

    private int width, height;

    private Thread thread;
    private URL sourceUrl;
    private File cacheDirectory;

    public TableScannerCamera() {
        unitsPerPixel = new Location(LengthUnit.Inches, 0.031, 0.031, 0, 0);
        sourceUri = "http://openpnp.org/downloads/tablescan/1/";
    }

    @SuppressWarnings("unused")
    @Commit
    private void commit() throws Exception {
        setSourceUri(sourceUri);
    }

    @Override
    public synchronized void startContinuousCapture(CameraListener listener, int maximumFps) {
        start();
        super.startContinuousCapture(listener, maximumFps);
    }

    @Override
    public synchronized void stopContinuousCapture(CameraListener listener) {
        super.stopContinuousCapture(listener);
        if (listeners.size() == 0) {
            stop();
        }
    }

    private synchronized void stop() {
        if (thread != null && thread.isAlive()) {
            thread.interrupt();
            try {
                thread.join();
            } catch (Exception e) {

            }
            thread = null;
        }
    }

    private synchronized void start() {
        if (thread == null) {
            thread = new Thread(this);
            thread.start();
        }
    }

    public String getSourceUri() {
        return sourceUri;
    }

    public void setSourceUri(String sourceUri) throws Exception {
        String oldValue = this.sourceUri;
        this.sourceUri = sourceUri;
        pcs.firePropertyChange("sourceUri", oldValue, sourceUri);
        // TODO: Move to start() so simply setting a property doesn't sometimes
        // blow up.
        initialize();
    }

    public String getCacheSizeDescription() {
        try {
            return FileUtils.byteCountToDisplaySize(FileUtils.sizeOf(cacheDirectory));
        } catch (Exception e) {
            return "Not Initialized";
        }
    }

    public synchronized void clearCache() throws IOException {
        FileUtils.cleanDirectory(cacheDirectory);
        pcs.firePropertyChange("cacheSizeDescription", null, getCacheSizeDescription());
    }

    @Override
    public BufferedImage capture() {
        return renderFrame();
    }

    public void run() {
        while (!Thread.interrupted()) {
            BufferedImage frame = renderFrame();
            broadcastCapture(frame);
            try {
                Thread.sleep(1000 / fps);
            } catch (InterruptedException e) {
                return;
            }
        }
    }

    private BufferedImage renderFrame() {
        if (buffer == null) {
            return null;
        }
        if (head == null) {
            return null;
        }
        synchronized (buffer) {
            // Grab these values only once since the head may continue to move
            // while we are rendering.
            Location l = getLocation().convertToUnits(LengthUnit.Millimeters);
            double headX = l.getX();
            double headY = l.getY();
            if (lastX != headX || lastY != headY) {
                // Find the closest tile to the head's current position.
                Tile closestTile = getClosestTile(headX, headY);
                logger.debug("closestTile {}", closestTile);

                // If it has changed we need to render the entire buffer.
                if (closestTile != lastCenterTile) {
                    lastCenterTile = closestTile;
                    renderBuffer();
                }

                // And remember the last position we rendered.
                lastX = headX;
                lastY = headY;
            }

            /*
             * Get the distance from the center tile to the point we need to render.
             * TODO: Had to invert these from experimentation. Need to figure out
             * why and maybe make it configurable. I was too tired to figure it out.
             */
            double unitsDeltaX = headX - lastCenterTile.getX();
            double unitsDeltaY = lastCenterTile.getY() - headY;

            /*
             * Get the distance in pixels from the center tile to the head.
             */
            Location unitsPerPixel = getUnitsPerPixel().convertToUnits(LengthUnit.Millimeters);
            double deltaX = unitsDeltaX / unitsPerPixel.getX();
            double deltaY = unitsDeltaY / unitsPerPixel.getY();

            /*
             * Get the position within the buffer of the top left pixel of the
             * frame sized chunk we'll grab.
             */
            double bufferStartX = (buffer.getWidth() / 2) - (width / 2);
            double bufferStartY = (buffer.getHeight() / 2) - (height / 2);

            BufferedImage frame = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

            /*
             * Render the frame sized chunk from the center of the buffer offset
             * by the distance of the head from the center tile to the frame
             * buffer for final output.
             */
            Graphics2D g = (Graphics2D) frame.getGraphics();
            g.drawImage(buffer, 0, 0, frame.getWidth(), frame.getHeight(), (int) (bufferStartX + deltaX),
                    (int) (bufferStartY + deltaY), (int) (bufferStartX + frame.getWidth() + deltaX),
                    (int) (bufferStartY + frame.getHeight() + deltaY), null);
            g.dispose();

            return frame;
        }
    }

    private void renderBuffer() {
        // determine where in the map the center tile is
        int centerTileX = lastCenterTile.getTileX();
        int centerTileY = lastCenterTile.getTileY();

        Graphics2D g = (Graphics2D) buffer.getGraphics();
        g.setColor(Color.black);
        g.clearRect(0, 0, buffer.getWidth(), buffer.getHeight());
        g.setColor(Color.white);

        /*
         * Render the tiles into the buffer. Our goal is to render an area that
         * is tilesWide x tilesHigh with the center tile in the middle. Any
         * locations that fall outside of the tiles array are not rendered and
         * as such are left black.
         */
        for (int x = 0; x < tilesWide; x++) {
            for (int y = 0; y < tilesHigh; y++) {
                /*
                 * We multiply everything by two here because the TableScanner
                 * takes two images per width of the camera. By doing this
                 * we increase the effective resolution of this image because
                 * decrease the distance between tiles.
                 * TODO: This should be made configurable.
                 */
                int tileX = centerTileX - (2 * (tilesWide / 2)) + (2 * x);
                int tileY = centerTileY - (2 * (tilesHigh / 2)) + (2 * y);
                // If the position is within the array's bounds we'll render it.
                if (tileX >= 0 && tileX < tiles.length && tileY >= 0 && tileY < tiles[tileX].length
                        && tiles[tileX][tileY] != null) {
                    Tile tile = tiles[tileX][tileY];
                    BufferedImage image = tile.getImage();

                    /*
                     * The source images are flipped in both dimensions, and
                     * we're rendering the local array from top to bottom
                     * instead of bottom to top, so we have to flip the images
                     * and then render right to left, bottom to top.
                     */
                    int dx1 = image.getWidth() * x;
                    int dy1 = image.getHeight() * (tilesHigh - y) - image.getHeight();
                    int dx2 = image.getWidth() * x + image.getWidth();
                    int dy2 = image.getHeight() * (tilesHigh - y);

                    int sx1 = image.getWidth();
                    int sy1 = image.getHeight();
                    int sx2 = 0;
                    int sy2 = 0;

                    g.drawImage(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null);
                }
            }
        }

        g.dispose();
    }

    private synchronized void initialize() throws Exception {
        stop();
        sourceUrl = new URL(sourceUri);
        cacheDirectory = new File(Configuration.get().getResourceDirectory(getClass()),
                DigestUtils.shaHex(sourceUri));
        if (!cacheDirectory.exists()) {
            cacheDirectory.mkdirs();
        }
        File[] files = null;
        // Attempt to get the list of files from the source.
        try {
            files = loadSourceFiles();
        } catch (Exception e) {
            logger.warn("Unable to load file list from {}", sourceUri);
            logger.warn("Reason", e);
        }

        if (files == null) {
            files = loadCachedFiles();
        }

        if (files.length == 0) {
            throw new Exception("No source or cached files found.");
        }
        // Load the first image we found and use it's properties as a template
        // for the rest of the images.
        BufferedImage templateImage = new Tile(0, 0, files[0]).getImage();

        width = templateImage.getWidth();
        height = templateImage.getHeight();

        tileList = new ArrayList<Tile>();
        lastX = Double.MIN_VALUE;
        lastY = Double.MIN_VALUE;
        lastCenterTile = null;

        // We build a set of unique X and Y positions that we see so we can
        // later build a two dimensional array of the riles
        TreeSet<Double> uniqueX = new TreeSet<Double>();
        TreeSet<Double> uniqueY = new TreeSet<Double>();
        // Create a map of the tiles so that we can quickly find them when we
        // build the array.
        Map<Tile, Tile> tileMap = new HashMap<Tile, Tile>();
        // Parse the filenames of the all the files and add their coordinates
        // to the sets and map.
        for (File file : files) {
            String filename = file.getName();
            filename = filename.substring(0, filename.indexOf(".png"));
            String[] xy = filename.split(",");
            double x = Double.parseDouble(xy[0]);
            double y = Double.parseDouble(xy[1]);
            Tile tile = new Tile(x, y, file);
            uniqueX.add(x);
            uniqueY.add(y);
            tileMap.put(tile, tile);
            tileList.add(tile);
        }
        // Create a two dimensional array to store all the of the tiles
        tiles = new Tile[uniqueX.size()][uniqueY.size()];

        // Iterate through all the unique X and Y positions that were found
        // and add each file to the two dimensional array in the position
        // where it belongs
        int x = 0, y = 0;
        for (Double xPos : uniqueX) {
            y = 0;
            for (Double yPos : uniqueY) {
                Tile tile = tileMap.get(new Tile(xPos, yPos, null));
                tiles[x][y] = tile;
                tile.setTileX(x);
                tile.setTileY(y);
                y++;
            }
            x++;
        }

        /*
         * Create a buffer that we will render the center tile and it's
         * surrounding tiles to. 
         */
        buffer = new BufferedImage(templateImage.getWidth() * tilesWide, templateImage.getHeight() * tilesHigh,
                BufferedImage.TYPE_INT_ARGB);

        if (listeners.size() > 0) {
            start();
        }
    }

    private File[] loadSourceFiles() throws Exception {
        // Load the list of the files from the website
        URL filesUrl = new URL(sourceUrl, "files.txt");
        BufferedReader reader = new BufferedReader(new InputStreamReader(filesUrl.openStream()));
        ArrayList<File> files = new ArrayList<File>();
        String line;
        while ((line = reader.readLine()) != null) {
            line = line.trim();
            if (line.length() == 0) {
                continue;
            }
            File file = new File(cacheDirectory, line);
            files.add(file);
        }
        if (files.size() == 0) {
            throw new Exception("No files found.");
        }
        logger.debug("Loaded {} filenames from {}", files.size(), sourceUri);
        return files.toArray(new File[] {});
    }

    private File[] loadCachedFiles() throws Exception {
        // Load all png files from the directory that look like they match what
        // we are expecting.
        File[] files = cacheDirectory.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File arg0, String arg1) {
                return arg1.contains(".") && arg1.contains(",") && arg1.endsWith(".png");
            }
        });

        return files;
    }

    private Tile getClosestTile(double x, double y) {
        Tile closestTile = tileList.get(0);
        double closestDistance = Math
                .sqrt(Math.pow(x - closestTile.getX(), 2) + Math.pow(y - closestTile.getY(), 2));
        for (Tile tile : tileList) {
            double distance = Math.sqrt(Math.pow(x - tile.getX(), 2) + Math.pow(y - tile.getY(), 2));
            if (distance <= closestDistance) {
                closestTile = tile;
                closestDistance = distance;
            }
        }
        return closestTile;
    }

    @Override
    public Wizard getConfigurationWizard() {
        return new TableScannerCameraConfigurationWizard(this);
    }

    public class Tile {
        private File file;
        private double x, y;
        private int tileX, tileY;
        private SoftReference<BufferedImage> image;

        public Tile(double x, double y, File file) {
            this.x = x;
            this.y = y;
            this.file = file;
        }

        public synchronized BufferedImage getImage() {
            if (image == null || image.get() == null) {
                if (!file.exists() && sourceUrl != null) {
                    // If the file doesn't exist, see if we can downlaod it
                    // from the Intertron.
                    try {
                        URL imageUrl = new URL(sourceUrl, file.getName());
                        logger.debug("Attempting to download {}", imageUrl.toString());
                        FileUtils.copyURLToFile(imageUrl, file);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                try {
                    image = new SoftReference<BufferedImage>(ImageIO.read(file));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return image.get();
        }

        public double getX() {
            return x;
        }

        public void setX(double x) {
            this.x = x;
        }

        public double getY() {
            return y;
        }

        public void setY(double y) {
            this.y = y;
        }

        public File getFile() {
            return file;
        }

        public void setFile(File file) {
            this.file = file;
        }

        public int getTileX() {
            return tileX;
        }

        public void setTileX(int tileX) {
            this.tileX = tileX;
        }

        public int getTileY() {
            return tileY;
        }

        public void setTileY(int tileY) {
            this.tileY = tileY;
        }

        @Override
        public String toString() {
            return String.format("[%2.3f, %2.3f (%d, %d)]", x, y, tileX, tileY);
        }

        @Override
        public boolean equals(Object obj) {
            Tile other = (Tile) obj;
            return other.x == x && other.y == y;
        }

        @Override
        public int hashCode() {
            return ("" + x + "," + y).hashCode();
        }
    }
}