/* Copyright (C) 2014,2015 Philipp Lenk, Jan Müller
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>
*/
package de.hu_berlin.informatik.spws2014.mapever.entzerrung;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;
public class CornerDetector
{
/**
* Guesses the most likly corners of a distorted map within an image.
* Expects OpenCV to be initialized.
* The results are already pretty good but could propably be improved
* via tweaking the parameters or adding some additional line filtering
* criteria(like them being kind of parallel for instance...)
*
* @param gray_img A grayscale image in OpenCVs Mat format.
* @return An array of propable corner points in the following form: {x0,y0,x1,y1,x2,y2,x3,y3} or null on error.
**/
public static Point[] guess_corners(Mat gray_img)
{
Mat lines = new Mat();
Imgproc.Canny(gray_img, gray_img, THRESHOLD0, THRESHOLD1, APERTURE_SIZE, false);
Imgproc.HoughLinesP(gray_img, lines, RHO, THETA, HOUGH_THRESHOLD,
Math.min(gray_img.cols(), gray_img.rows()) / MIN_LINE_LENGTH_FRACTION, MAX_LINE_GAP);
double[][] edge_lines = filter_lines(lines, gray_img.size());
Point[] ret_val = new Point[4];
ret_val[0] = find_intercept_point(edge_lines[0], edge_lines[2]);
ret_val[1] = find_intercept_point(edge_lines[0], edge_lines[3]);
ret_val[2] = find_intercept_point(edge_lines[1], edge_lines[3]);
ret_val[3] = find_intercept_point(edge_lines[1], edge_lines[2]);
// do sanity checks and return null on invalid coordinates
for (int i = 0; i < 4; i++) {
// check if coordinates are outside image boundaries
if (ret_val[i].x < 0 || ret_val[i].y < 0 || ret_val[i].x > gray_img.width() || ret_val[i].y > gray_img.height()) {
return null;
}
// check if point equal to other point
for (int j = i + 1; j < 4; j++) {
if (ret_val[j].x == ret_val[i].x && ret_val[j].y == ret_val[i].y) {
return null;
}
}
}
return ret_val;
}
/**
* Finds the intersection point of 2 given lines. Uses a very ugly formula stolen from Wikipedia, feel free to
* improve the code below ;-)
* Doesnt do proper error handling and returns {-1,-1} with parallel lines.
* Yes, I know its aweful, i was tired and not necessarily extremly motivated ;-)
*
* @param l0 The first line, that is two points on it, saved like this: {x0,y0,x1,y1}
* @param l1 The second line, that is two points on it, saved like this: {x0,y0,x1,y1}
* @return The intersection point between those lines as an OpenCV Point
**/
private static Point find_intercept_point(double[] l0, double[] l1)
{
double denominator = (l0[0] - l0[2]) * (l1[1] - l1[3]) - (l0[1] - l0[3]) * (l1[0] - l1[2]);
if (denominator == 0)
return new Point(-1, -1);
double l0_factor = l0[0] * l0[3] - l0[1] * l0[2];
double l1_factor = l1[0] * l1[3] - l1[1] * l1[2];
double x = (l0_factor * (l1[0] - l1[2]) - l1_factor * (l0[0] - l0[2])) / denominator;
double y = (l0_factor * (l1[1] - l1[3]) - l1_factor * (l0[1] - l0[3])) / denominator; // ugly as hell ;_;
return new Point(x, y);
}
/**
* Calculates the slope of a given line.
*
* @param line The line, that is two points on it, saved like this: {x0,y0,x1,y1}
* @return The lines slope
**/
private static double get_slope(double[] line)
{
if (Math.abs(line[2] - line[0]) < 0.0000001)
return Double.MAX_VALUE;
return (line[3] - line[1]) / (line[2] - line[0]);
}
/**
* Due to lots of false positives close to the images border, this defines what is
* "too close", so that find_lines can reasonably discard those
*
* @param line The line to be tested
* @param dimensions The images size
* @return true if the line is deemed "too close" to the images border, false otherwise
**/
private static boolean too_close(double[] line, Size dimensions)
{
return line[0] <= dimensions.width * TOO_CLOSE_FRACTION ||
line[1] <= dimensions.height * TOO_CLOSE_FRACTION ||
line[2] <= dimensions.width * TOO_CLOSE_FRACTION ||
line[3] <= dimensions.height * TOO_CLOSE_FRACTION ||
line[0] >= dimensions.width * (1.0 - TOO_CLOSE_FRACTION) ||
line[1] >= dimensions.height * (1.0 - TOO_CLOSE_FRACTION) ||
line[2] >= dimensions.width * (1.0 - TOO_CLOSE_FRACTION) ||
line[3] >= dimensions.height * (1.0 - TOO_CLOSE_FRACTION);
}
/**
* Finds the lines closest to the images border within a reasonable range
* of slopes.
*
* Turns out this is sufficient to give acceptable results.
*
* @param lines The lines within the original image, in OpenCVs Mat format(as returned by HoughLines or HoughLinesP)
* @param image_dimensions The original image size
* @return 4 lines in the by now well known format of 4 doubles per line: {x0,y0,x1,y1}{x0,y0,x1,y1}...
**/
private static double[][] filter_lines(Mat lines, Size image_dimensions)
{
double[][] ret_lines = new double[4][4];
double min_x = Double.MAX_VALUE, max_x = Double.MIN_VALUE, min_y = Double.MAX_VALUE, max_y = Double.MIN_VALUE;
for (int l = 0; l < lines.cols(); ++l)
{
double current_line[] = lines.get(0, l);
if (too_close(current_line, image_dimensions))
continue;
double slope = get_slope(current_line);
if (Math.abs(slope) <= MAX_SLOPE)
{
double cl_min_y = Math.min(current_line[1], current_line[3]);
double cl_max_y = Math.max(current_line[1], current_line[3]);
if (cl_min_y < min_y)
{
ret_lines[0] = current_line;
min_y = cl_min_y;
}
if (cl_max_y > max_y)
{
ret_lines[1] = current_line;
max_y = cl_max_y;
}
}
else if (Math.abs(1.0 / slope) <= MAX_SLOPE)
{
double cl_min_x = Math.min(current_line[0], current_line[2]);
double cl_max_x = Math.max(current_line[0], current_line[2]);
if (cl_min_x < min_x)
{
ret_lines[2] = current_line;
min_x = cl_min_x;
}
if (cl_max_x > max_x)
{
ret_lines[3] = current_line;
max_x = cl_max_x;
}
}
}
return ret_lines;
}
private static final double MAX_SLOPE = 0.3;
private static final double THRESHOLD0 = 60,
THRESHOLD1 = 200;
private static final int APERTURE_SIZE = 3;
private static final int RHO = 1;
private static final double THETA = Math.PI / 180;
private static final int HOUGH_THRESHOLD = 60,
MIN_LINE_LENGTH_FRACTION = 4,
MAX_LINE_GAP = 10;
private static double TOO_CLOSE_FRACTION = 0.0001;
}