/**
 * OpenCV for Processing
 * Computer vision with OpenCV.
 * https://github.com/atduskgreg/opencv-processing
 *
 * Copyright (c) 2013 Greg Borenstein http://gregborenstein.com
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA  02111-1307  USA
 * 
 * @author      Greg Borenstein http://gregborenstein.com
 * @modified    05/21/2017
 * @version     0.5.4 (17)
 */



package gab.opencv;

import gab.opencv.Contour;
import gab.opencv.ContourComparator;
import gab.opencv.Histogram;
import gab.opencv.Line;
import gab.opencv.Flow;

import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;

import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfRect;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.MatOfInt;
import org.opencv.core.MatOfFloat;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.core.Point;
import org.opencv.calib3d.Calib3d;
import org.opencv.core.CvException;
import org.opencv.core.Core.MinMaxLocResult;
import org.opencv.video.BackgroundSubtractorMOG;
import org.opencv.objdetect.CascadeClassifier;
import org.opencv.imgproc.Imgproc;

import processing.core.*;

/**
 * OpenCV is the main class for using OpenCV for Processing. Most of the documentation is found here.
 * 
 * OpenCV for Processing is a computer vision library for the Processing creative coding toolkit.
 * It's based on OpenCV, which is widely used throughout industry and academic research. OpenCV for
 * Processing provides friendly, Processing-style functions for doing all of the most common tasks
 * in computer vision: loading images, filtering them, detecting faces, finding contours, background
 * subtraction, optical flow, calculating histograms etc. OpenCV also provides access to all native
 * OpenCV data types and functions. So advanced users can do anything described in the OpenCV java
 * documentation: http://docs.opencv.org/java/ 
 * 
 * A text is also underway to provide a narrative introduction to computer vision for beginners using
 * OpenCV for Processing: https://github.com/atduskgreg/opencv-processing-book/blob/master/book/toc.md
 * 
 */

public class OpenCV {
	
	PApplet parent;
	
	public int width;
	public int height;
	
	private int roiWidth;
	private int roiHeight;
		
	public Mat matBGRA;
	public Mat matR, matG, matB, matA;
	public Mat matHSV;
	public Mat matH, matS, matV;
	public Mat matGray;
	public Mat matROI;
	public Mat nonROImat; // so that releaseROI() can return to color/gray as appropriate
	
	private boolean useColor;
	private boolean useROI;
	public int colorSpace;
	
	private PImage outputImage;
	private PImage inputImage;
	
	private boolean nativeLoaded;
	private boolean isArm = false;
	
	public CascadeClassifier classifier;
	BackgroundSubtractorMOG backgroundSubtractor;
	public Flow flow;

	public final static String VERSION = "0.5.4";
	public final static String CASCADE_FRONTALFACE = "haarcascade_frontalface_alt.xml";
	public final static String CASCADE_PEDESTRIANS = "hogcascade_pedestrians.xml";
	public final static String CASCADE_EYE = "haarcascade_eye.xml";
	public final static String CASCADE_CLOCK = "haarcascade_clock.xml";
	public final static String CASCADE_NOSE = "haarcascade_mcs_nose.xml";
	public final static String CASCADE_MOUTH = "haarcascade_mcs_mouth.xml";
	public final static String CASCADE_UPPERBODY = "haarcascade_upperbody.xml";
	public final static String CASCADE_LOWERBODY = "haarcascade_lowerbody.xml";
	public final static String CASCADE_FULLBODY = "haarcascade_fullbody.xml";
	public final static String CASCADE_PEDESTRIAN = "hogcascade_pedestrians.xml";

	public final static String CASCADE_RIGHT_EAR = "haarcascade_mcs_rightear.xml";
	public final static String CASCADE_PROFILEFACE = "haarcascade_profileface.xml";
	
	// used for both Scharr edge detection orientation
	// and flip(). Values are set for flip, arbitrary from POV of Scharr
	public final static int HORIZONTAL = 1;
	public final static int VERTICAL = 0;
	public final static int BOTH = -1;
	

    
    /**
     * Initialize OpenCV with the path to an image.
     * The image will be loaded and prepared for processing.
     * 
     * @param theParent - A PApplet representing the user sketch, i.e "this"
     * @param pathToImg - A String with a path to the image to be loaded
     */
    public OpenCV(PApplet theParent, String pathToImg){
    	initNative();
    	useColor = false;
    	loadFromString(theParent, pathToImg);
    }
    
    /**
     * Initialize OpenCV with the path to an image.
     * The image will be loaded and prepared for processing.
     * 
     * @param theParent - A PApplet representing the user sketch, i.e "this"
     * @param pathToImg - A String with a path to the image to be loaded
     * @param useColor - (Optional) Set to true if you want to use the color version of the image for processing.
     */
    public OpenCV(PApplet theParent, String pathToImg, boolean useColor){
    	initNative();
    	this.useColor = useColor;
    	if(useColor){
    		useColor(); // have to set the color space.
    	}
    	loadFromString(theParent, pathToImg);
    }
    
    private void loadFromString(PApplet theParent, String pathToImg){
    	parent = theParent;
    	PImage imageToLoad = parent.loadImage(pathToImg);
    	init(imageToLoad.width, imageToLoad.height);
    	loadImage(imageToLoad);	
    }
    
    /**
     * Initialize OpenCV with an image.
     * The image's pixels will be copied and prepared for processing.
     * 
     * @param theParent
     * 			A PApplet representing the user sketch, i.e "this"
     * @param img
     * 			A PImage to be loaded
     */
    public OpenCV(PApplet theParent, PImage img){
    	initNative();
    	useColor = false;
    	loadFromPImage(theParent, img);
    }
    
    /**
     * Initialize OpenCV with an image.
     * The image's pixels will be copiedd and prepared for processing.
     * 
     * @param theParent
     * 			A PApplet representing the user sketch, i.e "this"
     * @param img
     * 			A PImage to be loaded
     * @param useColor
     * 			(Optional) Set to true if you want to use the color version of the image for processing.
     */
    public OpenCV(PApplet theParent, PImage img, boolean useColor){
    	initNative();
    	this.useColor = useColor;
    	if(useColor){
    		useColor();  
    	}
    	loadFromPImage(theParent, img);
    }
    
    private void loadFromPImage(PApplet theParent, PImage img){
    	parent = theParent;
    	init(img.width, img.height);
    	loadImage(img);
    }
    
    /**
     * 
     * Apply subsequent image processing to
     * the color version of the loaded image.
     * 
     * Note: Many OpenCV functions require a grayscale
     *       image. Those functions will raise an exception
     *       if attempted on a color image. 
     * 
     */
    public void useColor(){
    	useColor(PApplet.RGB);
    }
    
    /**
     * 
     * Get the colorSpace of the current color image. Will be either RGB or HSB.
     * 
     * @return
     * 		
     * The color space of the color mats. Either PApplet.RGB or PApplet.HSB
     */
    public int getColorSpace(){
    	return colorSpace;
    }
    
    /**
     * 
     * Set the main working image to be the color version of the imported image.
     * Subsequent image-processing functions will be applied to the color version
     * of the image. Image is assumed to be HSB or RGB based on the argument
     * 
     * 
     * @param colorSpace
     * 		The color space of the image to be processed. Either RGB or HSB.
     */
    public void useColor(int colorSpace){
    	useColor = true;
    	if(colorSpace != PApplet.RGB && colorSpace != PApplet.HSB){
    		PApplet.println("ERROR: color space must be either RGB or HSB");
    	} else {
    		this.colorSpace = colorSpace;
    	}
    	
    	if(this.colorSpace == PApplet.HSB){
    		populateHSV();
    	}
    }
    
    private void populateHSV(){
    	matHSV = imitate(matBGRA);
    	Imgproc.cvtColor(matBGRA, matHSV, Imgproc.COLOR_BGR2HSV);
    	ArrayList<Mat> channels = new ArrayList<Mat>();
    	Core.split(matHSV, channels);
    	    	
    	matH = channels.get(0);
    	matS = channels.get(1);
    	matV = channels.get(2);
    }
    
    private void populateBGRA(){
    	ArrayList<Mat> channels = new ArrayList<Mat>();
    	Core.split(matBGRA, channels);
		matB = channels.get(0);
		matG = channels.get(1);
		matR = channels.get(2);
		matA = channels.get(3);	
    }
    
    /**
     * 
     * Set OpenCV to do image processing on the grayscale version
     * of the loaded image.
     * 
     */
    public void useGray(){
    	useColor = false;
    }
    
    /**
     * 
     * Checks whether OpenCV is currently using the color version of the image
     * or the grayscale version.
     * 
     * @return
     * 		True if OpenCV is currently using the color version of the image.
     */
    public boolean getUseColor(){
    	return useColor;
    }
    
    private Mat getCurrentMat(){
    	if(useROI){
    		return matROI;
    		
    	} else{
    	
    		if(useColor){
    			return matBGRA;
    		} else{
    			return matGray;
    		}
    	}
    }
    
    /**
     * Initialize OpenCV with a width and height.
     * You will need to load an image in before processing.
     * See copy(PImage img).
     * 
     * @param theParent
     * 			A PApplet representing the user sketch, i.e "this"
     * @param width
     * 			int
     * @param height
     * 			int
     */
	public OpenCV(PApplet theParent, int width, int height) {
		initNative();
		parent = theParent;
		init(width, height);
	}
    
    private void init(int w, int h){
    	width = w;
    	height = h;
		welcome();
		setupWorkingImages();
		setupFlow();
		
		matR = new Mat(height, width, CvType.CV_8UC1);
		matG = new Mat(height, width, CvType.CV_8UC1);
		matB = new Mat(height, width, CvType.CV_8UC1);
		matA = new Mat(height, width, CvType.CV_8UC1);
		matGray = new Mat(height, width, CvType.CV_8UC1);
		
		matBGRA = new Mat(height, width, CvType.CV_8UC4);
    }
    
    private void setupFlow(){
    	flow = new Flow(parent);
    }
    
    private void setupWorkingImages(){
		outputImage = parent.createImage(width,height, PConstants.ARGB);
    }
    
    private String getLibPath() {
        URL url = this.getClass().getResource("OpenCV.class");
        if (url != null) {
          // Convert URL to string, taking care of spaces represented by the "%20"
          // string.
          String path = url.toString().replace("%20", " ");
          int n0 = path.indexOf('/');

          int n1 = -1;
            

          n1 = path.indexOf("opencv_processing.jar");
          if (PApplet.platform == PConstants.WINDOWS) { //platform Windows
            // In Windows, path string starts with "jar file/C:/..."
            // so the substring up to the first / is removed.
            n0++;
          }


          if ((-1 < n0) && (-1 < n1)) {
            return path.substring(n0, n1);
          } else {
            return "";
          }
        }
        return "";
      }
    
    private void initNative(){
    	if(!nativeLoaded){
    		int bitsJVM = PApplet.parseInt(System.getProperty("sun.arch.data.model"));
    		
    		String osArch = System.getProperty("os.arch");
    		
	    	String nativeLibPath = getLibPath();
	    	
	    	String path = null;

	    	// determine the path to the platform-specific opencv libs
	    	if (PApplet.platform == PConstants.WINDOWS) { //platform Windows
	    		path = nativeLibPath + "windows" + bitsJVM;
	    	}
	    	if (PApplet.platform == PConstants.MACOSX) { //platform Mac
	    		path = nativeLibPath + "macosx" + bitsJVM;
	    	}
	    	if (PApplet.platform == PConstants.LINUX) { //platform Linux
	    		// attempt to detect arm architecture - is it fair to assume linux for ARM devices?
	    		isArm = osArch.contains("arm");
			// armv6hf as found on the Raspberry Pi is the lowest architecture supported by Processing
			// in the future we'll have runtime-detection of armv7 systems, and use the optimized library on those
    			path = isArm ? nativeLibPath + "linux-armv6hf" : nativeLibPath + "linux" + bitsJVM;
	    	}
	    	
	    	// ensure the determined path exists
	    	try {
	    		File libDir = new File(path);
	    		if (libDir.exists()) {
	    			nativeLibPath = path; 
	    		}
	    	} catch (NullPointerException e) {
	    		// platform couldn't be determined
	    		System.err.println("Cannot load local version of opencv_java245  : Linux 32/64, arm7, Windows 32 bits or Mac Os 64 bits are only avaible");
	    		e.printStackTrace();
	    	}
	    	
	    	// this check might be redundant now...
	    	if((PApplet.platform == PConstants.MACOSX && bitsJVM == 64) || (PApplet.platform == PConstants.WINDOWS) || (PApplet.platform == PConstants.LINUX)){
		    	try {
					addLibraryPath(nativeLibPath);
				} catch (Exception e) {
					e.printStackTrace();
				}
		    	System.loadLibrary("opencv_java245");
	    	}
	    	else{
	    		 System.err.println("Cannot load local version of opencv_java245  : Linux 32/64, Windows 32 bits or Mac Os 64 bits are only avaible");
	    	}
	    	
	    	nativeLoaded = true;
    	}
    }
    

    private void addLibraryPath(String path) throws Exception {
        String originalPath = System.getProperty("java.library.path");
        
        // If this is an arm device running linux, Processing seems to include the linux32 dirs in the path,
        // which conflict with the arm-specific libs. To fix this, we remove the linux32 segments from the path.
        //
        // Alternatively, we could do one of the following:
        // 		A) prepend to the path instead of append, forcing our libs to be used
        // 		B) rename the libopencv_java245 in the arm7 dir and add logic to load it instead above in System.loadLibrary(...)
        
        if (isArm) {
        	if (originalPath.indexOf("linux32") != -1) {
        		originalPath = originalPath.replaceAll(":[^:]*?linux32", "");
        	}
        }
        
    	System.setProperty("java.library.path", originalPath +System.getProperty("path.separator")+ path);
     
        //set sys_paths to null
        final Field sysPathsField = ClassLoader.class.getDeclaredField("sys_paths");
        sysPathsField.setAccessible(true);
        sysPathsField.set(null, null);
    }

	/**
	 * Load a cascade file for face or object detection.
	 * Expects one of:
	 * 
	 * <pre>
	 * OpenCV.CASCADE_FRONTALFACE
	 * OpenCV.CASCADE_PEDESTRIANS
	 * OpenCV.CASCADE_EYE			
	 * OpenCV.CASCADE_CLOCK		
	 * OpenCV.CASCADE_NOSE 		
	 * OpenCV.CASCADE_MOUTH		
	 * OpenCV.CASCADE_UPPERBODY 	
	 * OpenCV.CASCADE_LOWERBODY 	
	 * OpenCV.CASCADE_FULLBODY 	
	 * OpenCV.CASCADE_PEDESTRIANS
	 * OpenCV.CASCADE_RIGHT_EAR 	
	 * OpenCV.CASCADE_PROFILEFACE
	 * </pre>
	 * 
	 * To pass your own cascade file, provide an absolute path and a second
	 * argument of true, thusly:
	 * 
	 * <pre>
	 * opencv.loadCascade("/path/to/my/custom/cascade.xml", true)
	 * </pre>
	 * 
	 * (NB: ant build scripts copy the data folder outside of the
	 * jar so that this will work.)
	 * 
	 * @param cascadeFileName
	 * 		The name of the cascade file to be loaded form within OpenCV for Processing.
	 * 		Must be one of the constants provided by this library 
	*/
	public void loadCascade(String cascadeFileName){

		// localize path to cascade file to point at the library's data folder
		String relativePath = "cascade-files/" + cascadeFileName;
		String cascadePath = getLibPath();
		cascadePath += relativePath;
				
		PApplet.println("Load cascade from: " + cascadePath);

		classifier = new CascadeClassifier(cascadePath);   
        
        if(classifier.empty()){
        	PApplet.println("Cascade failed to load"); // raise exception here?
        } else {
        	PApplet.println("Cascade loaded: " + cascadeFileName);
        }
	}

	/**
	 * Load a cascade file for face or object detection.
	 * If absolute is true, cascadeFilePath must be an
	 * absolute path to a cascade xml file. If it is false
	 * then cascadeFilePath must be one of the options provided
	 * by OpenCV for Processing as in the single-argument
	 * version of this function.
	 * 
	 * @param cascadeFilePath
	 * 		A string. Either an absolute path to a cascade XML file or 
	 * 		one of the constants provided by this library.
	 * @param absolute
	 * 		Whether or not the cascadeFilePath is an absolute path to an XML file.		
	 */
	public void loadCascade(String cascadeFilePath, boolean absolute){
		if(absolute){
			classifier = new CascadeClassifier(cascadeFilePath);   
	        
	        if(classifier.empty()){
	        	PApplet.println("Cascade failed to load"); // raise exception here?
	        } else {
	        	PApplet.println("Cascade loaded from absolute path: " + cascadeFilePath);
	        }
		} else {
			loadCascade(cascadeFilePath);
		}
	}
	
	/**
	 * Convert an array of OpenCV Rect objects into
	 * an array of java.awt.Rectangle rectangles.
	 * Especially useful when working with
	 * classifier.detectMultiScale().
	 *
	 * @param Rect[] rects
	 * 
	 * @return 
	 *  A Rectangle[] of java.awt.Rectangle
	 */
	public static Rectangle[] toProcessing(Rect[] rects){
		Rectangle[] results = new Rectangle[rects.length];
		for(int i = 0; i < rects.length; i++){
			results[i] = new Rectangle(rects[i].x, rects[i].y, rects[i].width, rects[i].height);
		}
		return results;
	}
	
	/**
	 * Detect objects using the cascade classifier. loadCascade() must already
	 * have been called to setup the classifier. See the OpenCV documentation
	 * for details on the arguments: http://docs.opencv.org/java/org/opencv/objdetect/CascadeClassifier.html#detectMultiScale(org.opencv.core.Mat, org.opencv.core.MatOfRect, double, int, int, org.opencv.core.Size, org.opencv.core.Size)
	 * 
	 * A simpler version of detect() that doesn't need these arguments is also available.
	 * 
	 * @param scaleFactor
	 * @param minNeighbors
	 * @param flags
	 * @param minSize
	 * @param maxSize
	 * @return
	 * 		An array of java.awt.Rectangle objects with the location, width, and height of each detected object.
	 */
	public Rectangle[] detect(double scaleFactor , int minNeighbors , int flags, int minSize , int maxSize){
		Size minS = new Size(minSize, minSize);
		Size maxS = new Size(maxSize, maxSize);
		
		MatOfRect detections = new MatOfRect();
		classifier.detectMultiScale(getCurrentMat(), detections, scaleFactor, minNeighbors, flags, minS, maxS );

		return OpenCV.toProcessing(detections.toArray());
	}
	
	/**
	 * Detect objects using the cascade classifier. loadCascade() must already
	 * have been called to setup the classifier.
	 * 
	 * @return
	 * 		An array of java.awt.Rectnangle objects with the location, width, and height of each detected object.
	 */
	public Rectangle[] detect(){
		MatOfRect detections = new MatOfRect();
		classifier.detectMultiScale(getCurrentMat(), detections);
		
		return OpenCV.toProcessing(detections.toArray());
	}
	
	/**
	 * Setup background subtraction. After calling this function,
	 * updateBackground() must be called with each new frame
	 * you want to add to the running background subtraction calculation.
	 * 
	 * For details on the arguments, see:
	 * http://docs.opencv.org/java/org/opencv/video/BackgroundSubtractorMOG.html#BackgroundSubtractorMOG(int, int, double)
	 * 
	 * @param history
	 * @param nMixtures
	 * @param backgroundRatio
	 */
	public void startBackgroundSubtraction(int history, int nMixtures, double backgroundRatio){
		backgroundSubtractor = new BackgroundSubtractorMOG(history, nMixtures, backgroundRatio);
	}
	
	/**
	 * Update the running background for background subtraction based on
	 * the current image loaded into OpenCV. startBackgroundSubtraction()
	 * must have been called before this to setup the background subtractor.
	 * 
	 */
	public void updateBackground(){
		Mat foreground = imitate(getCurrentMat());
		backgroundSubtractor.apply(getCurrentMat(), foreground, 0.05);
		setGray(foreground);
	}	
	
	/**
	 * Calculate the optical flow of the current image relative
	 * to a running series of images (typically frames from video).
	 * Optical flow is useful for detecting what parts of the image
	 * are moving and in what direction.
	 * 
	 */
	public void calculateOpticalFlow(){
		flow.calculateOpticalFlow(getCurrentMat());
	}
	
	/*
	 * Get the total optical flow within a region of the image.
	 * Be sure to call calculateOpticalFlow() first.
	 * 
	 */
	public PVector getTotalFlowInRegion(int x, int y, int w, int h) {
		return flow.getTotalFlowInRegion(x, y, w, h);
	}
	
	/*
	 * Get the average optical flow within a region of the image.
	 * Be sure to call calculateOpticalFlow() first.
	 * 
	 */
	public PVector getAverageFlowInRegion(int x, int y, int w, int h) {
		return flow.getAverageFlowInRegion(x,y,w,h);
	}
	
	/*
	 * Get the total optical flow for the entire image.
	 * Be sure to call calculateOpticalFlow() first. 
	 */
	public PVector getTotalFlow() {
		return flow.getTotalFlow();
	}
	
	/*
	 * Get the average optical flow for the entire image.
	 * Be sure to call calculateOpticalFlow() first.
	 */
	public PVector getAverageFlow() {
		return flow.getAverageFlow();
	}
	
	/*
	 * Get the optical flow at a single point in the image.
	 * Be sure to call calcuateOpticalFlow() first.
	 */
	public PVector getFlowAt(int x, int y){
		return flow.getFlowAt(x,y);
	}
	
	/*
	 * Draw the optical flow.
	 * Be sure to call calcuateOpticalFlow() first.
	 */
	public void drawOpticalFlow(){
		flow.draw();
	}
	
	/**
	 * Flip the current image.
	 * 
	 * @param direction
	 * 		One of: OpenCV.HORIZONTAL, OpenCV.VERTICAL, or OpenCV.BOTH
	 */
	public void flip(int direction){
		Core.flip(getCurrentMat(), getCurrentMat(), direction);
	}
	
	/**
	 * 
	 * Adjust the contrast of the image. Works on color or black and white images.
	 * 
	 * @param amt
	 * 			Amount of contrast to apply. 0-1.0 reduces contrast. Above 1.0 increases contrast.
	 * 
	 **/
	public void contrast(float amt){
		Scalar modifier;
		if(useColor){
			modifier = new Scalar(amt,amt,amt,1);

		} else{
			modifier = new Scalar(amt);
		}
		
		Core.multiply(getCurrentMat(), modifier, getCurrentMat());
	}
	
	/**
	 * Get the x-y location of the maximum value in the current image.
	 * 
	 * @return 
	 * 		A PVector with the location of the maximum value.
	 */
	public PVector max(){
		MinMaxLocResult r = Core.minMaxLoc(getCurrentMat());
		return OpenCV.pointToPVector(r.maxLoc);
	}
	
	/**
	 * Get the x-y location of the minimum value in the current image.
	 * 
	 * @return 
	 * 		A PVector with the location of the minimum value.
	 */
	public PVector min(){
		MinMaxLocResult r = Core.minMaxLoc(getCurrentMat());
		return OpenCV.pointToPVector(r.minLoc);
	}
	
	/**
	 * Helper function to convert an OpenCV Point into a Processing PVector
	 * 
	 * @param p
	 * 		A Point
	 * @return
	 * 		A PVector
	 */
	public static PVector pointToPVector(Point p){
		return new PVector((float)p.x, (float)p.y);
	}
	
	
	/**
	 * Adjust the brightness of the image. Works on color or black and white images.
	 * 
	 * @param amt
	 * 			The amount to brighten the image. Ranges -255 to 255. 
	 * 
	 **/	
	public void brightness(int amt){
		Scalar modifier;
		if(useColor){
			modifier = new Scalar(amt,amt,amt, 1);

		} else{
			modifier = new Scalar(amt);
		}
		
		Core.add(getCurrentMat(), modifier, getCurrentMat());
	}
	
	/**
	 * Helper to create a new OpenCV Mat whose channels and
	 * bit-depth mask an existing Mat.
	 * 
	 * @param m
	 * 		The Mat to match
	 * @return
	 * 		A new Mat
	 */
	public static Mat imitate(Mat m){
		return new Mat(m.height(), m.width(), m.type());
	}
	
	/**
	 * Calculate the difference between the current image
	 * loaded into OpenCV and a second image. The result is stored
	 * in the loaded image in OpenCV. Works on both color and grayscale
	 * images.
	 * 
	 * @param img
	 * 		A PImage to diff against.
	 */
	public void diff(PImage img){
		Mat imgMat = imitate(getColor());
		toCv(img, imgMat);

		Mat dst = imitate(getCurrentMat());

		if(useColor){
			ARGBtoBGRA(imgMat, imgMat);
			Core.absdiff(getCurrentMat(), imgMat, dst);
		} else {
			Core.absdiff(getCurrentMat(), OpenCV.gray(imgMat), dst);
		}
		
		dst.assignTo(getCurrentMat());
	}
	
	/**
	 * A helper function that diffs two Mats using absdiff.
	 * Places the result back into mat1 
	 * 
	 * @param mat1
	 * 		The destination Mat
	 * @param mat2
	 * 		The Mat to diff against
	 */
	public static void diff(Mat mat1, Mat mat2){
		Mat dst = imitate(mat1);
		Core.absdiff(mat1, mat2, dst);
		dst.assignTo(mat1);
	}
	
	/**
	 * Apply a global threshold to an image. Produces a binary image
	 * with white pixels where the original image was above the threshold
	 * and black where it was below.
	 * 
	 * @param threshold
	 * 		An int from 0-255.
	 */
	public void threshold(int threshold){
		Imgproc.threshold(getCurrentMat(), getCurrentMat(), threshold, 255, Imgproc.THRESH_BINARY); 
	}
	
	/**
	 * Apply a global threshold to the image. The threshold is determined by Otsu's method, which
	 * attempts to divide the image at a threshold which minimizes the variance of pixels in the black
	 * and white regions.
	 *
	 * See: https://en.wikipedia.org/wiki/Otsu's_method
	 */
	public void threshold() {
		Imgproc.threshold(getCurrentMat(), getCurrentMat(), 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU); 
	}
	
	/**
	 * Apply an adaptive threshold to an image. Produces a binary image
	 * with white pixels where the original image was above the threshold
	 * and black where it was below.
	 * 
	 * See:
	 * http://docs.opencv.org/java/org/opencv/imgproc/Imgproc.html#adaptiveThreshold(org.opencv.core.Mat, org.opencv.core.Mat, double, int, int, int, double)
	 * 
	 * @param blockSize
	 * 		The size of the pixel neighborhood to use.
	 * @param c
	 * 		A constant subtracted from the mean of each neighborhood.
	 */
	public void adaptiveThreshold(int blockSize, int c){
		try{
			Imgproc.adaptiveThreshold(getCurrentMat(), getCurrentMat(), 255, Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, Imgproc.THRESH_BINARY, blockSize, c);
		} catch(CvException e){
			PApplet.println("ERROR: adaptiveThreshold function only works on gray images.");
		}
	}
	
	/**
	 * Normalize the histogram of the image. This will spread the image's color
	 * spectrum over the full 0-255 range. Only works on grayscale images. 
	 * 
	 * 
	 * See: http://docs.opencv.org/java/org/opencv/imgproc/Imgproc.html#equalizeHist(org.opencv.core.Mat, org.opencv.core.Mat)
	 * 
	 */
	public void equalizeHistogram(){
		try{
			Imgproc.equalizeHist(getCurrentMat(), getCurrentMat());
		} catch(CvException e){
			PApplet.println("ERROR: equalizeHistogram only works on a gray image.");
		}
	}
	
	/**
	 * Invert the image.
	 * See: http://docs.opencv.org/java/org/opencv/core/Core.html#bitwise_not(org.opencv.core.Mat, org.opencv.core.Mat)
	 * 
	 */
	public void invert(){
		Core.bitwise_not(getCurrentMat(),getCurrentMat());
	}
	
	/**
	 * Dilate the image. Dilation is a morphological operation (i.e. it affects the shape) often used to
	 * close holes in contours. It expands white areas of the image.
	 * 
	 * See:
	 * http://docs.opencv.org/java/org/opencv/imgproc/Imgproc.html#dilate(org.opencv.core.Mat, org.opencv.core.Mat, org.opencv.core.Mat)
	 * 
	 */
	public void dilate(){
		Imgproc.dilate(getCurrentMat(), getCurrentMat(), new Mat());
	}
	
	/**
	 * Erode the image. Erosion  is a morphological operation (i.e. it affects the shape) often used to
	 * close holes in contours. It contracts white areas of the image.
	 * 
	 * See:
	 * http://docs.opencv.org/java/org/opencv/imgproc/Imgproc.html#erode(org.opencv.core.Mat, org.opencv.core.Mat, org.opencv.core.Mat)
	 * 
	 */
	public void erode(){
		Imgproc.erode(getCurrentMat(), getCurrentMat(), new Mat());
	}
	
  /**
   * Apply a morphological operation (e.g., opening, closing) to the image with a given kernel element.
   *
   * See:
   * http://docs.opencv.org/doc/tutorials/imgproc/opening_closing_hats/opening_closing_hats.html
   * 
   * @param operation
   *    The morphological operation to apply: Imgproc.MORPH_CLOSE, MORPH_OPEN,
   *    MORPH_TOPHAT, MORPH_BLACKHAT, MORPH_GRADIENT.
   * @param kernelElement
   *    The shape to apply the operation with: Imgproc.MORPH_RECT, MORPH_CROSS, or MORPH_ELLIPSE.
   * @param width
   *    Width of the shape.
   * @param height
   *    Height of the shape.
   */
  public void morphX(int operation, int kernelElement, int width, int height) {
    Mat kernel = Imgproc.getStructuringElement(kernelElement, new Size(width, height));
    Imgproc.morphologyEx(getCurrentMat(), getCurrentMat(), operation, kernel);
  }
  
  /**
   * Close the image with a circle of a given size.
   *
   * See:
   * http://docs.opencv.org/doc/tutorials/imgproc/opening_closing_hats/opening_closing_hats.html#closing
   *
   * @param size
   *    Radius of the circle to close with.
   */
  public void close(int size) {
    Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE, new Size(size, size));
    Imgproc.morphologyEx(getCurrentMat(), getCurrentMat(), Imgproc.MORPH_CLOSE, kernel);
  }
	
  /**
   * Open the image with a circle of a given size.
   *
   * See:
   * http://docs.opencv.org/doc/tutorials/imgproc/opening_closing_hats/opening_closing_hats.html#opening
   *
   * @param size
   *    Radius of the circle to open with.
   */
  public void open(int size) {
    Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE, new Size(size, size));
    Imgproc.morphologyEx(getCurrentMat(), getCurrentMat(), Imgproc.MORPH_OPEN, kernel);
  }
  
	/**
	 * Blur an image symetrically by a given number of pixels.
	 * 
	 * @param blurSize
	 * 		int - the amount to blur by in x- and y-directions.
	 */
	public void blur(int blurSize){
		Imgproc.blur(getCurrentMat(), getCurrentMat(), new Size(blurSize, blurSize)); 
	}
	
	/**
	 * Blur an image assymetrically by a different number of pixels in x- and y-directions.
	 * 
	 * @param blurW
	 * 		amount to blur in the x-direction
	 * @param blurH
	 * 		amount to blur in the y-direction
	 */
	public void blur(int blurW, int blurH){
		Imgproc.blur(getCurrentMat(), getCurrentMat(), new Size(blurW, blurH)); 
	}
	
	/**
	 * Find edges in the image using Canny edge detection.
	 * 
	 * @param lowThreshold
	 * @param highThreshold
	 */
	public void findCannyEdges(int lowThreshold, int highThreshold){
		Imgproc.Canny(getCurrentMat(), getCurrentMat(), lowThreshold, highThreshold);
	}
	
	public void findSobelEdges(int dx, int dy){
		Mat sobeled = new Mat(getCurrentMat().height(), getCurrentMat().width(), CvType.CV_32F);
		Imgproc.Sobel(getCurrentMat(), sobeled, CvType.CV_32F, dx, dy);
		sobeled.convertTo(getCurrentMat(), getCurrentMat().type());
	}
	
	public void findScharrEdges(int direction){
		if(direction == HORIZONTAL){
			Imgproc.Scharr(getCurrentMat(), getCurrentMat(), -1, 1, 0 );
		}
		
		if(direction == VERTICAL){
			Imgproc.Scharr(getCurrentMat(), getCurrentMat(), -1, 0, 1 );
		}
		
		if(direction == BOTH){
			Mat hMat = imitate(getCurrentMat());
			Mat vMat = imitate(getCurrentMat());
			Imgproc.Scharr(getCurrentMat(), hMat, -1, 1, 0 );
			Imgproc.Scharr(getCurrentMat(), vMat, -1, 0, 1 );
			Core.add(vMat,hMat, getCurrentMat());
		}
	}
	
	public ArrayList<Contour> findContours(){
		return findContours(true, false);
	}
	
	public ArrayList<Contour> findContours(boolean findHoles, boolean sort){
		ArrayList<Contour> result = new ArrayList<Contour>();
		
		ArrayList<MatOfPoint> contourMat = new ArrayList<MatOfPoint>();
		try{
			int contourFindingMode = (findHoles ? Imgproc.RETR_LIST : Imgproc.RETR_EXTERNAL);
			
			Imgproc.findContours(getCurrentMat(), contourMat, new Mat(), contourFindingMode, Imgproc.CHAIN_APPROX_NONE);
		} catch(CvException e){
			PApplet.println("ERROR: findContours only works with a gray image.");
		}
		  for (MatOfPoint c : contourMat) {
		    result.add(new Contour(parent, c));
		  }
		
		if(sort){
			Collections.sort(result, new ContourComparator());
		}
		  
		return result;
	}
	
	public ArrayList<Line> findLines(int threshold, double minLineLength, double maxLineGap){
		ArrayList<Line> result = new ArrayList<Line>();
		
		Mat lineMat = new Mat();
		Imgproc.HoughLinesP(getCurrentMat(), lineMat, 1, PConstants.PI/180.0, threshold, minLineLength, maxLineGap);
		for (int i = 0; i < lineMat.width(); i++) {
			double[] coords = lineMat.get(0, i);
		    result.add(new Line(coords[0], coords[1], coords[2], coords[3]));
		}
		
		return result;
	}
	
	public ArrayList<PVector> findChessboardCorners(int patternWidth, int patternHeight){
		MatOfPoint2f corners = new MatOfPoint2f();
		Calib3d.findChessboardCorners(getCurrentMat(), new Size(patternWidth,patternHeight), corners);
		return matToPVectors(corners);
	}
	
	/**
	 * 
	 * @param mat 
	 * 		The mat from which to calculate the histogram. Get this from getGray(), getR(), getG(), getB(), etc..
	 * 		By default this will normalize the histogram (scale the values to 0.0-1.0). Pass false as the third argument to keep values unormalized.
	 * @param numBins 
	 * 		The number of bins into which divide the histogram should be divided.
	 * @param normalize (optional)
	 * 		Whether or not to normalize the histogram (scale the values to 0.0-1.0). Defaults to true.
	 * @return
	 * 		A Histogram object that you can call draw() on.
	 */
	public Histogram findHistogram(Mat mat, int numBins){
		return findHistogram(mat, numBins, true);
	}
	
	
	public Histogram findHistogram(Mat mat, int numBins, boolean normalize){
		
		MatOfInt channels = new MatOfInt(0);
		MatOfInt histSize = new MatOfInt(numBins);
		float[] r = {0f, 256f};
		MatOfFloat ranges = new MatOfFloat(r);
		Mat hist = new Mat();
		
		ArrayList<Mat> images = new ArrayList<Mat>();
		images.add(mat);

		Imgproc.calcHist( images, channels, new Mat(), hist, histSize, ranges);
		
		if(normalize){
			Core.normalize(hist, hist);
		}
		
		return new Histogram(parent, hist);
	}
	
	/**
	 * 
	 * Filter the image for values between a lower and upper bound.
	 * Converts the current image into a binary image with white where pixel
	 * values were within bounds and black elsewhere.
	 * 
	 * @param lowerBound
	 * @param upperBound
	 */
	public void inRange(int lowerBound, int upperBound){
		Core.inRange(getCurrentMat(), new Scalar(lowerBound), new Scalar(upperBound), getCurrentMat());
	}
	
	/**
	 * 
	 * @param src
	 * 		A Mat of type 8UC4 with channels arranged as BGRA.
	 * @return
	 * 		A Mat of type 8UC1 in grayscale.
	 */
	public static Mat gray(Mat src){
		Mat result = new Mat(src.height(), src.width(), CvType.CV_8UC1);
		Imgproc.cvtColor(src, result, Imgproc.COLOR_BGRA2GRAY);
			
		return result;
	}
	
	public void gray(){
		matGray = gray(matBGRA);
		useGray(); //???
	}
	
	/**
	 * Set a Region of Interest within the image. Subsequent image processing
	 * functions will apply to this ROI rather than the full image.
	 * Full image will display be included in output.
	 * 
	 * @return
	 * 		False if requested ROI exceed the bounds of the working image.
	 * 		True if ROI was successfully set.
	 */
	public boolean setROI(int x, int y, int w, int h){
		if(x < 0 ||
		   x + w > width ||
		   y < 0 ||
		   y + h > height){
			return false;
		} else{
			roiWidth = w;
			roiHeight = h;
		
			if(useColor){
				nonROImat = matBGRA;
				matROI = new Mat(matBGRA, new Rect(x, y, w, h));
			} else {
				nonROImat = matGray;
				matROI = new Mat(matGray, new Rect(x, y, w, h));
			}
			useROI = true;
			
			return true;
		}
	}
	
	public void releaseROI(){
		useROI = false;
	}

	/**
	 * Load an image from a path.
	 * 
	 * @param imgPath
	 * 			String with the path to the image
	 */
	public void loadImage(String imgPath){
		loadImage(parent.loadImage(imgPath));
	}
	
	// NOTE: We're not handling the signed/unsigned
	// 		 conversion. Is that any issue?
	public void loadImage(PImage img){				
		// FIXME: is there a better way to hold onto
		// 			this?
		inputImage = img;
		
		toCv(img, matBGRA);
		ARGBtoBGRA(matBGRA,matBGRA);
		populateBGRA();
		
		if(useColor){
			useColor(this.colorSpace);
		} else {
			gray();
		}
		
	}
	
	public static void ARGBtoBGRA(Mat rgba, Mat bgra){
		ArrayList<Mat> channels = new ArrayList<Mat>();
		Core.split(rgba, channels);

		ArrayList<Mat> reordered = new ArrayList<Mat>();
		// Starts as ARGB. 
		// Make into BGRA.
		
		reordered.add(channels.get(3));
		reordered.add(channels.get(2));
		reordered.add(channels.get(1));
		reordered.add(channels.get(0));
		  
		Core.merge(reordered, bgra);	
	}
	
	
	public int getSize(){
		return width * height;
	}
    
    /**
     * 
     * Convert a 4 channel OpenCV Mat object into 
     * pixels to be shoved into a 4 channel ARGB PImage's
     * pixel array.
     * 
     * @param m
     * 		An RGBA Mat we want converted 
     * @return
     * 		An int[] formatted to be the pixels of a PImage
     */
    public int[] matToARGBPixels(Mat m){
    	 int pImageChannels = 4;
         int numPixels = m.width()*m.height();
         int[] intPixels = new int[numPixels];
         byte[] matPixels = new byte[numPixels*pImageChannels];
   
         m.get(0,0, matPixels);
         ByteBuffer.wrap(matPixels).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get(intPixels);
         return intPixels;
    }

 
	/**
	 * Convert an OpenCV Mat object into a PImage
	 * to be used in other Processing code.
	 * Copies the Mat's pixel data into the PImage's pixel array.
	 * Iterates over each pixel in the Mat, i.e. expensive.
	 * 
	 * (Mainly used internally by OpenCV. Inspired by toCv()
	 * from KyleMcDonald's ofxCv.)
	 * 
	 * @param m
	 * 			A Mat you want converted
	 * @param img
	 * 			The PImage you want the Mat converted into.
	 */
	public void toPImage(Mat m, PImage img){	
		  img.loadPixels();

		  if(m.channels() == 3){
			  Mat m2 = new Mat();
			  Imgproc.cvtColor(m, m2, Imgproc.COLOR_RGB2RGBA);
              img.pixels = matToARGBPixels(m2);
		  } else if(m.channels() == 1){
			  Mat m2 = new Mat();
			  Imgproc.cvtColor(m, m2, Imgproc.COLOR_GRAY2RGBA);
              img.pixels = matToARGBPixels(m2);
		  } else if(m.channels() == 4){
              img.pixels = matToARGBPixels(m);
		  }
		  
		  img.updatePixels();
	}
	
	/**
	 * Convert a Processing PImage to an OpenCV Mat.
	 * (Inspired by Kyle McDonald's ofxCv's toOf())
	 * 
	 * @param img
	 * 		The PImage to convert.
	 * @param m
	 * 		The Mat to receive the image data.
	 */	
	public static void toCv(PImage img, Mat m){
		BufferedImage image = (BufferedImage)img.getNative();
		int[] matPixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
		
		ByteBuffer bb = ByteBuffer.allocate(matPixels.length * 4);
		IntBuffer ib = bb.asIntBuffer();
		ib.put(matPixels);
		
		byte[] bvals = bb.array();

		m.put(0,0, bvals);
	}
	
	public static ArrayList<PVector> matToPVectors(MatOfPoint mat){
		ArrayList<PVector> result = new ArrayList<PVector>();
		Point[] points =  mat.toArray();
		for(int i = 0; i < points.length; i++){
			result.add(new PVector((float)points[i].x, (float)points[i].y));
		}
		  
		return result;
	}

	public static ArrayList<PVector> matToPVectors(MatOfPoint2f mat){
		ArrayList<PVector> result = new ArrayList<PVector>();
		Point[] points =  mat.toArray();
		for(int i = 0; i < points.length; i++){
			result.add(new PVector((float)points[i].x, (float)points[i].y));
		}
		  
		return result;
	}
	
	public String matToS(Mat mat){
		return CvType.typeToString(mat.type());
	}
			
	public PImage getInput(){
		return inputImage;
	}
	
	public PImage getOutput(){
		if(useColor){
			toPImage(matBGRA, outputImage);
		} else {
			toPImage(matGray, outputImage);
		}
		
		return outputImage;	
	}
	
	public PImage getSnapshot(){
		PImage result;
		
		if(useROI){
			result = getSnapshot(matROI);
		} else {
			if(useColor){
				if(colorSpace == PApplet.HSB){
					result = getSnapshot(matHSV);
				} else {
					result = getSnapshot(matBGRA);
				}
			} else {
				result = getSnapshot(matGray);
			}
		}
		return result;
	}
	
	public PImage getSnapshot(Mat m){
		PImage result = parent.createImage(m.width(), m.height(), PApplet.ARGB);
		toPImage(m, result);
		return result;
	}

	public Mat getR(){
		return matR;
	}
	
	public Mat getG(){
		return matG;
	}
	
	public Mat getB(){
		return matB;
	}
	
	public Mat getA(){
		return matA;
	}
	
	public Mat getH(){
		return matH;
	}
	
	public Mat getS(){
		return matS;
	}
	
	public Mat getV(){
		return matV;
	}
	
	public Mat getGray(){
		return matGray;
	}
	
	public void setGray(Mat m){
		matGray = m;
		useColor = false;
	}
	
	public void setColor(Mat m){
		matBGRA = m;
		useColor = true;
	}
	
	public Mat getColor(){
		return matBGRA;
	}
	
	public Mat getROI(){
		return matROI;
	}

	private void welcome() {
		System.out.println("OpenCV for Processing 0.5.4 by Greg Borenstein http://gregborenstein.com");
		System.out.println("Using Java OpenCV " + Core.VERSION);
	}
	
	/**
	 * return the version of the library.
	 * 
	 * @return String
	 */
	public static String version() {
		return VERSION;
	}
}

