I'm trying to do a perspective transformation of a set of points in order to achieve a deskewing effect:
http://nuigroup.com/?ACT=28&fid=27&aid=1892_H6eNAaign4Mrnn30Au8d
I'm using the image below for tests, and the green rectangle display the area of interest.
I was wondering if it's possible to achieve the effect I'm hoping for using a simple combination of cv::getPerspectiveTransform
and cv::warpPerspective
. I'm sharing the source code I've written so far, but it doesn't work. This is the resulting image:
So there is a vector<cv::Point>
that defines the region of interest, but the points are not stored in any particular order inside the vector, and that's something I can't change in the detection procedure. Anyway, later, the points in the vector are used to define a RotatedRect
, which in turn is used to assemble cv::Point2f src_vertices[4];
, one of the variables required by cv::getPerspectiveTransform()
.
My understanding about vertices and how they are organized might be one of the issues. I also think that using a RotatedRect
is not the best idea to store the original points of the ROI, since the coordinates will change a little bit to fit into the rotated rectangle, and that's not very cool.
#include <cv.h>
#include <highgui.h>
#include <iostream>
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
cv::Mat src = cv::imread(argv[1], 1);
// After some magical procedure, these are points detect that represent
// the corners of the paper in the picture:
// [408, 69] [72, 2186] [1584, 2426] [1912, 291]
vector<Point> not_a_rect_shape;
not_a_rect_shape.push_back(Point(408, 69));
not_a_rect_shape.push_back(Point(72, 2186));
not_a_rect_shape.push_back(Point(1584, 2426));
not_a_rect_shape.push_back(Point(1912, 291));
// For debugging purposes, draw green lines connecting those points
// and save it on disk
const Point* point = ¬_a_rect_shape[0];
int n = (int)not_a_rect_shape.size();
Mat draw = src.clone();
polylines(draw, &point, &n, 1, true, Scalar(0, 255, 0), 3, CV_AA);
imwrite("draw.jpg", draw);
// Assemble a rotated rectangle out of that info
RotatedRect box = minAreaRect(cv::Mat(not_a_rect_shape));
std::cout << "Rotated box set to (" << box.boundingRect().x << "," << box.boundingRect().y << ") " << box.size.width << "x" << box.size.height << std::endl;
// Does the order of the points matter? I assume they do NOT.
// But if it does, is there an easy way to identify and order
// them as topLeft, topRight, bottomRight, bottomLeft?
cv::Point2f src_vertices[4];
src_vertices[0] = not_a_rect_shape[0];
src_vertices[1] = not_a_rect_shape[1];
src_vertices[2] = not_a_rect_shape[2];
src_vertices[3] = not_a_rect_shape[3];
Point2f dst_vertices[4];
dst_vertices[0] = Point(0, 0);
dst_vertices[1] = Point(0, box.boundingRect().width-1);
dst_vertices[2] = Point(0, box.boundingRect().height-1);
dst_vertices[3] = Point(box.boundingRect().width-1, box.boundingRect().height-1);
Mat warpMatrix = getPerspectiveTransform(src_vertices, dst_vertices);
cv::Mat rotated;
warpPerspective(src, rotated, warpMatrix, rotated.size(), INTER_LINEAR, BORDER_CONSTANT);
imwrite("rotated.jpg", rotated);
return 0;
}
Can someone help me fix this problem?
The problem was the order in which the points were declared inside the vector, and then there was also another issue related to this on the definition of
dst_vertices
.The order of the points matter to
getPerspectiveTransform()
and must be specified in the following order:Therefore, the points of origin needed to be re-ordered to this:
and the destination:
After this, some cropping need to be done because the resulting image is not just the area within the green rectangle as I thought it would be:
I don't know if this is a bug of OpenCV or if I'm missing something, but the main issue has been solved.
I got the same kind of issue and fixed it using OpenCV's homography extraction function.
You can see how I did in this question: Transforming a rectangle image into a quadrilateral using a CATransform3D
When working with a quadrangle, OpenCV isn't really your friend.
RotatedRect
will give you incorrect results. Also you will need a perspective projection instead of a affine projection like others mentioned here..Basicly what must been done is:
I implemented a class
Quadrangle
which takes care of contour to quadrangle conversion and will also transform it over the right perspective.See a working implementation here: Java OpenCV deskewing a contour
UPDATE: RESOLVED
I almost have this working. So close to being usable. It deskews properly but I seem to have a scale or translate issue. I have set the anchor point to zero and also experimented with changing the scale mode (aspectFill, scale to fit, etc...).
Setup the deskew points (red makes them hard to see):
Apply the transform calculated:
Now it deskews. This looks pretty good except that its not centered on the screen. By adding a pan gesture to the image view I can drag it over and verify that it lines up:
This is not as simple as translate by -0.5, -0.5 because the original image become a polygon that stretches out very very far (potentially), so it's bounding rect is much bigger than the screen frame.
Does anyone see what I can do to get this wrapped up? I'd like to get it committed and share it here. This is a popular topic but I haven't found a solution that's as simple as copy/paste.
Full source code is here:
git clone https://github.com/zakkhoyt/Quadrilateral.git
git checkout demo
However, I'll paste the relevant parts here. This first method is mine and is where I get the deskew points.
Update: I got it working properly. The coordinates needed to be origin in the center, not the upperleft. I applied xOffset and yOffset and viola. Demo code at the location mentioned above ("demo" branch)
So, first problem is corner order. They must be in the same order in both vectors. So, if in the first vector your order is:(top-left, bottom-left, bottom-right, top-right) , they MUST be in the same order in the other vector.
Second, to have the resulting image contain only the object of interest, you must set its width and height to be the same as resulting rectangle width and height. Do not worry, the src and dst images in warpPerspective can be different sizes.
Third, a performance concern. While your method is absolutely accurate, because you are doing only affine transforms (rotate, resize, deskew), mathematically, you can use the affine corespondent of your functions. They are much faster.
getAffineTransform()
warpAffine().
Important note: getAffine transform needs and expects ONLY 3 points, and the result matrix is 2-by-3, instead of 3-by-3.
How to make the result image have a different size than the input:
use
So here you are, and your programming assignment is over.
Very much inspired by @VaporwareWolf's answer, implemented in C# using Xamarin MonoTouch for iOS. The main difference is that I'm using GetPerspectiveTransform instead of FindHomography and TopLeft rather than ScaleToFit for content mode: