org.openpnp.vision.FluentCv.java Source code

Java tutorial

Introduction

Here is the source code for org.openpnp.vision.FluentCv.java

Source

/*
 * Copyright (C) 2015 Jason von Nieda <jason@vonnieda.org>
 * 
 * This file is part of OpenPnP.
 * 
 * You may use this file under either the GPLv3 License or the MIT License at your preference.
 * Functions in OpenPnP that this file rely on are also available under these terms. See the two
 * licenses below.
 * 
 * GPLv3 License Terms -------------------
 * 
 * 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
 * 
 * 
 * MIT License Terms -----------------
 * 
 * The MIT License (MIT)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package org.openpnp.vision;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;

import javax.imageio.ImageIO;

import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.RotatedRect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;
import org.opencv.utils.Converters;
import org.openpnp.model.Length;
import org.openpnp.model.Location;
import org.openpnp.spi.Camera;
import org.openpnp.util.HslColor;
import org.openpnp.util.VisionUtils;

/**
 * A fluent API for some of the most commonly used OpenCV primitives. Successive operations modify a
 * running Mat. By specifying a tag on an operation the result of the operation will be stored and
 * can be recalled back into the current Mat.
 * 
 * Heavily influenced by FireSight by Karl Lew https://github.com/firepick1/FireSight
 * 
 * In the spirit of FireSight, this code is licensed differently from the rest of OpenPnP. Please
 * see the license header above.
 * 
 * WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING This API is still under heavy
 * development and is likely to change significantly in the near future.
 * 
 * TODO: Rethink operations that return or process data points versus images. Perhaps these should
 * require a tag to work with and leave the image unchanged.
 * 
 * There is a bit of a divergence right now between how things like contours and rotated rects are
 * handled versus circles. Circles already have a Mat representation that we can kind of coerce
 * along the pipeline where contours do not (List<MatOfPoint>). We need to pick one of the methods
 * and stick with it, doing translation where needed.
 * 
 * Keeping things in Mat does give the benefit of not moving too much memory between OpenCv and
 * Java.
 */
public class FluentCv {
    static {
        nu.pattern.OpenCV.loadShared();
        System.loadLibrary(org.opencv.core.Core.NATIVE_LIBRARY_NAME);
    }

    public enum ColorCode {
        Bgr2Gray(Imgproc.COLOR_BGR2GRAY), Rgb2Gray(Imgproc.COLOR_RGB2GRAY), Gray2Bgr(
                Imgproc.COLOR_GRAY2BGR), Gray2Rgb(Imgproc.COLOR_GRAY2RGB), Bgr2Hls(Imgproc.COLOR_BGR2HLS), Hls2Bgr(
                        Imgproc.COLOR_HLS2BGR), Bgr2HlsFull(
                                Imgproc.COLOR_BGR2HLS_FULL), Hls2BgrFull(Imgproc.COLOR_HLS2BGR_FULL), Bgr2Hsv(
                                        Imgproc.COLOR_BGR2HSV), Hsv2Bgr(Imgproc.COLOR_HSV2BGR), Bgr2HsvFull(
                                                Imgproc.COLOR_BGR2HSV_FULL), Hsv2BgrFull(
                                                        Imgproc.COLOR_HSV2BGR_FULL);

        private int code;

        ColorCode(int code) {
            this.code = code;
        }

        public int getCode() {
            return code;
        }
    }

    private LinkedHashMap<String, Mat> stored = new LinkedHashMap<>();
    private Mat mat = new Mat();
    private Camera camera;

    public FluentCv toMat(BufferedImage img, String... tag) {
        Integer type = null;
        if (img.getType() == BufferedImage.TYPE_BYTE_GRAY) {
            type = CvType.CV_8UC1;
        } else if (img.getType() == BufferedImage.TYPE_3BYTE_BGR) {
            type = CvType.CV_8UC3;
        } else {
            img = convertBufferedImage(img, BufferedImage.TYPE_3BYTE_BGR);
            type = CvType.CV_8UC3;
        }
        Mat mat = new Mat(img.getHeight(), img.getWidth(), type);
        mat.put(0, 0, ((DataBufferByte) img.getRaster().getDataBuffer()).getData());
        return store(mat, tag);
    }

    public FluentCv toGray(String... tag) {
        return convertColor(ColorCode.Bgr2Gray, tag);
    }

    public FluentCv toColor(String... tag) {
        return convertColor(ColorCode.Gray2Bgr, tag);
    }

    public FluentCv convertColor(ColorCode code, String... tag) {
        return convertColor(code.getCode(), tag);
    }

    public FluentCv convertColor(int code, String... tag) {
        Imgproc.cvtColor(mat, mat, code);
        return store(mat, tag);
    }

    /**
     * Apply a threshold to the Mat. If the threshold value is 0 then the Otsu flag will be added
     * and the threshold value ignored. Otsu performs automatic determination of the threshold value
     * by sampling the image.
     * 
     * @param threshold
     * @param tag
     * @return
     */
    public FluentCv threshold(double threshold, String... tag) {
        return threshold(threshold, false, tag);
    }

    public FluentCv threshold(double threshold, boolean invert, String... tag) {
        int type = invert ? Imgproc.THRESH_BINARY_INV : Imgproc.THRESH_BINARY;
        if (threshold == 0) {
            type |= Imgproc.THRESH_OTSU;
        }
        Imgproc.threshold(mat, mat, threshold, 255, type);
        return store(mat, tag);
    }

    public FluentCv thresholdAdaptive(String... tag) {
        return thresholdAdaptive(false, tag);
    }

    public FluentCv thresholdAdaptive(boolean invert, String... tag) {
        Imgproc.adaptiveThreshold(mat, mat, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C,
                invert ? Imgproc.THRESH_BINARY_INV : Imgproc.THRESH_BINARY, 3, 5);
        return store(mat, tag);
    }

    public FluentCv blurGaussian(int kernelSize, String... tag) {
        Imgproc.GaussianBlur(mat, mat, new Size(kernelSize, kernelSize), 0);
        return store(mat, tag);
    }

    public FluentCv blurMedian(int kernelSize, String... tag) {
        Imgproc.medianBlur(mat, mat, kernelSize);
        return store(mat, tag);
    }

    public FluentCv findCirclesHough(Length minDiameter, Length maxDiameter, Length minDistance, String... tag) {
        checkCamera();
        return findCirclesHough((int) VisionUtils.toPixels(minDiameter, camera),
                (int) VisionUtils.toPixels(maxDiameter, camera), (int) VisionUtils.toPixels(minDistance, camera),
                tag);
    }

    public FluentCv findCirclesHough(int minDiameter, int maxDiameter, int minDistance, String... tag) {
        Mat circles = new Mat();
        Imgproc.HoughCircles(mat, circles, Imgproc.CV_HOUGH_GRADIENT, 1, minDistance, 80, 10, minDiameter / 2,
                maxDiameter / 2);
        store(circles, tag);
        return this;
    }

    public FluentCv convertCirclesToPoints(List<Point> points) {
        for (int i = 0; i < mat.cols(); i++) {
            double[] circle = mat.get(0, i);
            double x = circle[0];
            double y = circle[1];
            points.add(new Point(x, y));
        }
        return this;
    }

    public FluentCv convertCirclesToLocations(List<Location> locations) {
        checkCamera();
        Location unitsPerPixel = camera.getUnitsPerPixel().convertToUnits(camera.getLocation().getUnits());
        double avgUnitsPerPixel = (unitsPerPixel.getX() + unitsPerPixel.getY()) / 2;

        for (int i = 0; i < mat.cols(); i++) {
            double[] circle = mat.get(0, i);
            double x = circle[0];
            double y = circle[1];
            double radius = circle[2];
            Location location = VisionUtils.getPixelLocation(camera, x, y);
            location = location.derive(null, null, null, radius * 2 * avgUnitsPerPixel);
            locations.add(location);
        }

        VisionUtils.sortLocationsByDistance(camera.getLocation(), locations);
        return this;
    }

    /**
     * Draw circles from the current Mat contained onto the Mat specified in baseTag using the
     * specified color, optionally storing the results in tag. The current Mat is replaced with the
     * Mat from baseTag with the circles drawn on top of it.
     * 
     * @param baseTag
     * @param color
     * @param tag
     * @return
     */
    public FluentCv drawCircles(String baseTag, Color color, String... tag) {
        Color centerColor = new HslColor(color).getComplementary();
        Mat mat = get(baseTag);
        if (mat == null) {
            mat = new Mat();
        }
        for (int i = 0; i < this.mat.cols(); i++) {
            double[] circle = this.mat.get(0, i);
            double x = circle[0];
            double y = circle[1];
            double radius = circle[2];
            Core.circle(mat, new Point(x, y), (int) radius, colorToScalar(color), 2);
            Core.circle(mat, new Point(x, y), 1, colorToScalar(centerColor), 2);
        }
        return store(mat, tag);
    }

    /**
     * Draw circles from the current Mat contained onto the Mat specified in baseTag using the color
     * red, optionally storing the results in tag. The current Mat is replaced with the Mat from
     * baseTag with the circles drawn on top of it.
     * 
     * @param baseTag
     * @param tag
     * @return
     */
    public FluentCv drawCircles(String baseTag, String... tag) {
        return drawCircles(baseTag, Color.red, tag);
    }

    public FluentCv recall(String tag) {
        mat = get(tag);
        return this;
    }

    public FluentCv store(String tag) {
        return store(mat, tag);
    }

    public List<String> getStoredTags() {
        return new ArrayList<>(stored.keySet());
    }

    public FluentCv write(File file) throws Exception {
        ImageIO.write(toBufferedImage(), "PNG", file);
        return this;
    }

    public FluentCv read(File file, String... tag) throws Exception {
        return toMat(ImageIO.read(file), tag);
    }

    public BufferedImage toBufferedImage() {
        Integer type = null;
        if (mat.type() == CvType.CV_8UC1) {
            type = BufferedImage.TYPE_BYTE_GRAY;
        } else if (mat.type() == CvType.CV_8UC3) {
            type = BufferedImage.TYPE_3BYTE_BGR;
        } else if (mat.type() == CvType.CV_32F) {
            type = BufferedImage.TYPE_BYTE_GRAY;
            Mat tmp = new Mat();
            mat.convertTo(tmp, CvType.CV_8UC1, 255);
            mat = tmp;
        }
        if (type == null) {
            throw new Error(String.format("Unsupported Mat: type %d, channels %d, depth %d", mat.type(),
                    mat.channels(), mat.depth()));
        }
        BufferedImage image = new BufferedImage(mat.cols(), mat.rows(), type);
        mat.get(0, 0, ((DataBufferByte) image.getRaster().getDataBuffer()).getData());
        return image;
    }

    public FluentCv settleAndCapture(String... tag) {
        checkCamera();
        return toMat(camera.settleAndCapture(), tag);
    }

    /**
     * Set a Camera that can be used for calculations that require a Camera Location or units per
     * pixel.
     * 
     * @param camera
     * @return
     */
    public FluentCv setCamera(Camera camera) {
        this.camera = camera;
        return this;
    }

    public FluentCv filterCirclesByDistance(Length minDistance, Length maxDistance, String... tag) {

        double minDistancePx = VisionUtils.toPixels(minDistance, camera);
        double maxDistancePx = VisionUtils.toPixels(maxDistance, camera);
        return filterCirclesByDistance(camera.getWidth() / 2, camera.getHeight() / 2, minDistancePx, maxDistancePx,
                tag);
    }

    public FluentCv filterCirclesByDistance(double originX, double originY, double minDistance, double maxDistance,
            String... tag) {
        List<float[]> results = new ArrayList<>();
        for (int i = 0; i < this.mat.cols(); i++) {
            float[] circle = new float[3];
            this.mat.get(0, i, circle);
            float x = circle[0];
            float y = circle[1];
            float radius = circle[2];
            double distance = Math.sqrt(Math.pow(x - originX, 2) + Math.pow(y - originY, 2));
            if (distance >= minDistance && distance <= maxDistance) {
                results.add(new float[] { x, y, radius });
            }
        }
        // It really seems like there must be a better way to do this, but after hours
        // and hours of trying I can't find one. How the hell do you append an element
        // of 3 channels to a Mat?!
        Mat r = new Mat(1, results.size(), CvType.CV_32FC3);
        for (int i = 0; i < results.size(); i++) {
            r.put(0, i, results.get(i));
        }
        return store(r, tag);
    }

    public FluentCv filterCirclesToLine(Length maxDistance, String... tag) {
        return filterCirclesToLine(VisionUtils.toPixels(maxDistance, camera), tag);
    }

    /**
     * Filter circles as returned from e.g. houghCircles to only those that are within maxDistance
     * of the best fitting line.
     * 
     * @param tag
     * @return
     */
    public FluentCv filterCirclesToLine(double maxDistance, String... tag) {
        if (this.mat.cols() < 2) {
            return store(this.mat, tag);
        }

        List<Point> points = new ArrayList<>();
        // collect the circles into a list of points
        for (int i = 0; i < this.mat.cols(); i++) {
            float[] circle = new float[3];
            this.mat.get(0, i, circle);
            float x = circle[0];
            float y = circle[1];
            points.add(new Point(x, y));
        }

        Point[] line = Ransac.ransac(points, 100, maxDistance);
        Point a = line[0];
        Point b = line[1];

        // filter the points by distance from the resulting line
        List<float[]> results = new ArrayList<>();
        for (int i = 0; i < this.mat.cols(); i++) {
            float[] circle = new float[3];
            this.mat.get(0, i, circle);
            Point p = new Point(circle[0], circle[1]);
            if (pointToLineDistance(a, b, p) <= maxDistance) {
                results.add(circle);
            }
        }

        // It really seems like there must be a better way to do this, but after hours
        // and hours of trying I can't find one. How the hell do you append an element
        // of 3 channels to a Mat?!
        Mat r = new Mat(1, results.size(), CvType.CV_32FC3);
        for (int i = 0; i < results.size(); i++) {
            r.put(0, i, results.get(i));
        }
        return store(r, tag);
    }

    public Mat mat() {
        return mat.clone();
    }

    public FluentCv mat(Mat mat, String... tag) {
        return store(mat, tag);
    }

    /**
     * Calculate the absolute difference between the previously stored Mat called source1 and the
     * current Mat.
     * 
     * @param source1
     * @param tag
     */
    public FluentCv absDiff(String source1, String... tag) {
        Core.absdiff(get(source1), mat, mat);
        return store(mat, tag);
    }

    public FluentCv findEdgesCanny(double threshold1, double threshold2, String... tag) {
        Imgproc.Canny(mat, mat, threshold1, threshold2);
        return store(mat, tag);
    }

    public FluentCv findEdgesRobertsCross(String... tag) {
        // Java interpretation of
        // https://www.scss.tcd.ie/publications/book-supplements/A-Practical-Introduction-to-Computer-Vision-with-OpenCV/Code/Edges.cpp
        // Note: Java API does not have abs. This appears to be doing the
        // same thing effectively, but I am not sure it's 100% the same
        // as Cri's version.
        Mat kernel = Mat.eye(new Size(2, 2), CvType.CV_32FC1);
        kernel.put(0, 0, 0, 1, -1, 0);
        Mat roberts1 = new Mat();
        Imgproc.filter2D(mat, roberts1, CvType.CV_32FC1, kernel);
        Core.convertScaleAbs(roberts1, roberts1);

        kernel.put(0, 0, 1, 0, 0, -1);
        Mat roberts2 = new Mat();
        Imgproc.filter2D(mat, roberts2, CvType.CV_32FC1, kernel);
        Core.convertScaleAbs(roberts2, roberts2);

        Mat roberts = new Mat();
        Core.add(roberts1, roberts2, roberts);

        return store(roberts, tag);

        // // Java interpretation of Cri S's C version.
        // // This is very slow, my fault, not his. Probably due to all the
        // // array accesses.
        // int ptr1[] = { 0, 0, 0, 0 };
        // int indexx[] = { 0, 1, 1, 0 };
        // int indexy[] = { 0, 0, 1, 1 };
        // for (int y = 0; y < mat.rows() - 1; y++) {
        // for (int x = 0; x < mat.cols() - 1; x++) {
        // int temp = 0, temp1 = 0;
        // for (int i = 0; i < 4; i++) {
        // ptr1[i] = (int) mat.get(y + indexy[i], x + indexx[i])[0]; // // ptr1[i] = *(ptr + (y +
        // indexy[i]) * gray->widthStep + x + indexx[i]);
        // }
        // temp = Math.abs(ptr1[0] - ptr1[2]);
        // temp1 = Math.abs(ptr1[1] - ptr1[3]);
        // temp = (temp > temp1 ? temp : temp1);
        // temp = (int) Math.sqrt((float) (temp * temp) + (float) (temp1 * temp1));
        // mat.put(y, x, temp); // *(ptr + y * gray->widthStep + x) = temp;
        // }
        // }
        // return store(mat, tag);
    }

    public FluentCv findContours(List<MatOfPoint> contours, String... tag) {
        Mat hierarchy = new Mat();
        Imgproc.findContours(mat, contours, hierarchy, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_NONE);
        return store(mat, tag);
    }

    public FluentCv drawContours(List<MatOfPoint> contours, Color color, int thickness, String... tag) {
        if (color == null) {
            for (int i = 0; i < contours.size(); i++) {
                Imgproc.drawContours(mat, contours, i, colorToScalar(indexedColor(i)), thickness);
            }
        } else {
            Imgproc.drawContours(mat, contours, -1, colorToScalar(color), thickness);
        }
        return store(mat, tag);
    }

    public FluentCv filterContoursByArea(List<MatOfPoint> contours, double areaMin, double areaMax) {
        for (Iterator<MatOfPoint> i = contours.iterator(); i.hasNext();) {
            MatOfPoint contour = i.next();
            double area = Imgproc.contourArea(contour);
            if (area < areaMin || area > areaMax) {
                i.remove();
            }
        }
        return this;
    }

    public FluentCv drawRects(List<RotatedRect> rects, Color color, int thickness, String... tag) {
        for (int i = 0; i < rects.size(); i++) {
            RotatedRect rect = rects.get(i);
            if (color == null) {
                drawRotatedRect(mat, rect, indexedColor(i), thickness);
            } else {
                drawRotatedRect(mat, rect, color, thickness);
            }
        }
        return store(mat, tag);
    }

    public FluentCv getContourRects(List<MatOfPoint> contours, List<RotatedRect> rects) {
        for (int i = 0; i < contours.size(); i++) {
            MatOfPoint2f contour_ = new MatOfPoint2f();
            contours.get(i).convertTo(contour_, CvType.CV_32FC2);
            if (contour_.empty()) {
                continue;
            }
            RotatedRect rect = Imgproc.minAreaRect(contour_);
            rects.add(rect);
        }
        return this;
    }

    public FluentCv getContourMaxRects(List<MatOfPoint> contours, List<RotatedRect> rect) {
        List<Point> contoursCombined = new ArrayList<>();
        for (MatOfPoint mp : contours) {
            List<Point> points = new ArrayList<>();
            Converters.Mat_to_vector_Point(mp, points);
            for (Point point : points) {
                contoursCombined.add(point);
            }
        }
        contours.clear();
        MatOfPoint points = new MatOfPoint();
        points.fromList(contoursCombined);

        return getContourRects(Collections.singletonList(points), rect);
    }

    public FluentCv filterRectsByArea(List<RotatedRect> rects, double areaMin, double areaMax) {
        for (Iterator<RotatedRect> i = rects.iterator(); i.hasNext();) {
            RotatedRect rect = i.next();
            double area = rect.size.width * rect.size.height;
            if (area < areaMin || area > areaMax) {
                i.remove();
            }
        }
        return this;
    }

    private void checkCamera() {
        if (camera == null) {
            throw new Error("Call setCamera(Camera) before calling methods that rely on units per pixel.");
        }
    }

    private FluentCv store(Mat mat, String... tag) {
        this.mat = mat;
        if (tag != null && tag.length > 0) {
            // Clone so that future writes to the pipeline Mat
            // don't overwrite our stored one.
            mat = stored.get(tag[0]);
            if (mat != null) {
                mat.release();
            }
            stored.put(tag[0], this.mat.clone());
        }
        return this;
    }

    public FluentCv floodFill(Point seedPoint, Color color, String... tag) {
        Mat mask = new Mat();
        Imgproc.floodFill(mat, mask, seedPoint, colorToScalar(color));
        return store(mat, tag);
    }

    private Mat get(String tag) {
        Mat mat = stored.get(tag);
        if (mat == null) {
            return null;
        }
        // Clone so that future writes to the pipeline Mat
        // don't overwrite our stored one.
        return mat.clone();
    }

    public static Scalar colorToScalar(Color color) {
        return new Scalar(color.getBlue(), color.getGreen(), color.getRed(), 255);
    }

    /**
     * Return a Color from an imaginary list of colors starting at index 0 and extending on to
     * Integer.MAX_VALUE. Can be used to pick a different color for each object in a list. Colors
     * are not guaranteed to be unique but successive colors will be significantly different from
     * each other.
     * 
     * @param i
     * @return
     */
    public static Color indexedColor(int i) {
        float h = (i * 59) % 360;
        float s = Math.max((i * i) % 100, 80);
        float l = Math.max((i * i) % 100, 50);
        Color color = new HslColor(h, s, l).getRGB();
        return color;
    }

    public static BufferedImage convertBufferedImage(BufferedImage src, int type) {
        if (src.getType() == type) {
            return src;
        }
        BufferedImage img = new BufferedImage(src.getWidth(), src.getHeight(), type);
        Graphics2D g2d = img.createGraphics();
        g2d.drawImage(src, 0, 0, null);
        g2d.dispose();
        return img;
    }

    // From http://www.ahristov.com/tutorial/geometry-games/point-line-distance.html
    public static double pointToLineDistance(Point A, Point B, Point P) {
        double normalLength = Math.sqrt((B.x - A.x) * (B.x - A.x) + (B.y - A.y) * (B.y - A.y));
        return Math.abs((P.x - A.x) * (B.y - A.y) - (P.y - A.y) * (B.x - A.x)) / normalLength;
    }

    /**
     * Draw the infinite line defined by the two points to the extents of the image instead of just
     * between the two points. From:
     * http://stackoverflow.com/questions/13160722/how-to-draw-line-not-line-segment-opencv-2-4-2
     * 
     * @param img
     * @param p1
     * @param p2
     * @param color
     */
    public static void infiniteLine(Mat img, Point p1, Point p2, Color color) {
        Point p = new Point(), q = new Point();
        // Check if the line is a vertical line because vertical lines don't
        // have slope
        if (p1.x != p2.x) {
            p.x = 0;
            q.x = img.cols();
            // Slope equation (y1 - y2) / (x1 - x2)
            float m = (float) ((p1.y - p2.y) / (p1.x - p2.x));
            // Line equation: y = mx + b
            float b = (float) (p1.y - (m * p1.x));
            p.y = m * p.x + b;
            q.y = m * q.x + b;
        } else {
            p.x = q.x = p2.x;
            p.y = 0;
            q.y = img.rows();
        }
        Core.line(img, p, q, colorToScalar(color));
    }

    // From: http://stackoverflow.com/questions/23327502/opencv-how-to-draw-minarearect-in-java
    public static void drawRotatedRect(Mat mat, RotatedRect rect, Color color, int thickness) {
        Point points[] = new Point[4];
        rect.points(points);
        Scalar color_ = colorToScalar(color);
        for (int j = 0; j < 4; ++j) {
            Core.line(mat, points[j], points[(j + 1) % 4], color_, thickness);
        }
    }

    // From:
    // http://docs.opencv.org/doc/tutorials/highgui/video-input-psnr-ssim/video-input-psnr-ssim.html#image-similarity-psnr-and-ssim
    public static double calculatePsnr(Mat I1, Mat I2) {
        Mat s1 = new Mat();
        Core.absdiff(I1, I2, s1); // |I1 - I2|
        s1.convertTo(s1, CvType.CV_32F); // cannot make a square on 8 bits
        s1 = s1.mul(s1); // |I1 - I2|^2

        Scalar s = Core.sumElems(s1); // sum elements per channel

        double sse = s.val[0] + s.val[1] + s.val[2]; // sum channels

        if (sse <= 1e-10) // for small values return zero
            return 0;
        else {
            double mse = sse / (double) (I1.channels() * I1.total());
            double psnr = 10.0 * Math.log10((255 * 255) / mse);
            return psnr;
        }
    }

    /**
     * From FireSight: https://github.com/firepick1/FireSight/wiki/op-Sharpness
     * 
     * @param image
     * @return
     */
    public static double calculateSharpnessGRAS(Mat image) {
        int sum = 0;
        Mat matGray = new Mat();

        if (image.channels() == 1) {
            matGray = image;
        } else {
            Imgproc.cvtColor(image, matGray, Imgproc.COLOR_BGR2GRAY);
        }

        byte[] b1 = new byte[1];
        byte[] b2 = new byte[1];
        for (int r = 0; r < matGray.rows(); r++) {
            for (int c = 0; c < matGray.cols() - 1; c++) {
                matGray.get(r, c, b1);
                matGray.get(r, c + 1, b2);
                int df = (int) b1[0] - (int) b2[0];
                sum += df * df;
            }
        }

        return ((double) sum / matGray.rows() / (matGray.cols() - 1));
    }
}