Package org.openpnp.machine.reference.camera

Source Code of org.openpnp.machine.reference.camera.TableScannerCamera

/*
   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 javax.swing.Action;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.openpnp.CameraListener;
import org.openpnp.gui.support.PropertySheetWizardAdapter;
import org.openpnp.gui.support.Wizard;
import org.openpnp.gui.wizards.CameraConfigurationWizard;
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.openpnp.spi.PropertySheetHolder;
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;
      }
    }
  }
 
  /**
   * Renders a single frame for the camera based on the camera's position
   * over the entire array. First renders a tilesWide * tilesHigh hidden
   * buffer and then copies a templateImage.width * templateImage.height
   * chunk from that buffer based on the position of the camera within
   * the buffer.
   * @return
   */
  private BufferedImage renderFrame() {
    if (buffer == null) {
      return null;
    }
    if (head == null) {
        // TODO: Render an error image saying that it must be attached
        // to a head.
      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 the head position has not changed we don't need to re-render.
       * TODO: Doesn't that mean we can skip everything below
       * this block aside from on the first render, too?
       */
      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);
  }
 
    @Override
    public String getPropertySheetHolderTitle() {
        return getClass().getSimpleName() + " " + getId();
    }

    @Override
    public PropertySheetHolder[] getChildPropertySheetHolders() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public PropertySheet[] getPropertySheets() {
        return new PropertySheet[] {
                new PropertySheetWizardAdapter(new CameraConfigurationWizard(this)),
                new PropertySheetWizardAdapter(getConfigurationWizard())
        };
    }
 
    @Override
    public Action[] getPropertySheetHolderActions() {
        // TODO Auto-generated method stub
        return null;
    }

  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();
    }
  }
}
TOP

Related Classes of org.openpnp.machine.reference.camera.TableScannerCamera

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.