-
Notifications
You must be signed in to change notification settings - Fork 5
/
Deskewer.java
172 lines (154 loc) · 6.64 KB
/
Deskewer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package org.genericsystem.cv.classifier;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import org.genericsystem.cv.Img;
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.Rect;
import org.opencv.core.RotatedRect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;
/**
* This class contains static methods that can be used to deskew an {@link Img}.
*
* @author Nicolas Feybesse
* @author Pierrik Lassalas
*/
public class Deskewer {
private static final double closedImgSizeFactor = 2E-6;
private static final double minAreaFactor = 3E-5;
/**
* Deskew an image.
*
* @param imgPath - the path to the image
* @return a new {@link Img}
*/
public static Img deskew(final Path imgPath) {
Img img = new Img(imgPath.toString());
Img deskewed = _deskew(img);
img.close();
return deskewed;
}
/**
* Draw the Rotated rectangles used to calculate the deskew angle.
*
* @param img - the source image
* @param scalar - the color used to draw the rectangles
* @param thickness - the thickness
* @return - an annotated Img
*/
public static Img getRotatedRectanglesDrawn(final Img img, Scalar scalar, int thickness) {
Img imgCopy = new Img(img.getSrc(), true);
Img closed = getClosedImg(imgCopy);
List<RotatedRect> rectangles = getRotatedRects(closed.getSrc());
rectangles.stream().forEach(rect -> drawSingleRotatedRectangle(imgCopy.getSrc(), rect, scalar, thickness));
List<RotatedRect> filteredRectangles = getInliers(rectangles, 1.0);
filteredRectangles.stream().forEach(rect -> drawSingleRotatedRectangle(imgCopy.getSrc(), rect, new Scalar(0, 255, 0), thickness));
closed.close();
return imgCopy;
}
/**
* Get the binary image used to compute the deskew angle.
*
* @param img - the source image
* @return a binary image
*/
public static Img getBinary(final Img img) {
return getClosedImg(img);
}
// This function modifies the Mat mat
private static void drawSingleRotatedRectangle(Mat mat, final RotatedRect rect, final Scalar scalar, final int thickness) {
Point points[] = new Point[4];
rect.points(points);
for (int i = 0; i < 4; ++i) {
Imgproc.line(mat, points[i], points[(i + 1) % 4], scalar, thickness);
}
}
private static Img _deskew(final Img img) {
final Img closed = getClosedImg(img);
final double angle = contoursDetection(closed.getSrc());
System.out.println("angle: " + angle);
final Point center = new Point(img.width() / 2, img.height() / 2);
// Rotation matrix
Mat rotationMatrix = Imgproc.getRotationMatrix2D(center, angle, 1);
// Get the bounding rectangle
Rect bbox = new RotatedRect(center, img.size(), angle).boundingRect();
// Adjust the transformation matrix to prevent image cropping
double[] array = rotationMatrix.get(0, 2);
array[0] += bbox.width / 2 - center.x;
rotationMatrix.put(0, 2, array);
array = rotationMatrix.get(1, 2);
array[0] += bbox.height / 2 - center.y;
rotationMatrix.put(1, 2, array);
// Rotated Mat and empty Mat to apply the mask
Mat rotated = new Mat(bbox.size(), CvType.CV_8UC3, Scalar.all(255));
Mat rotatedMasked = new Mat();
// New mask
Mat mask = new Mat(img.size(), CvType.CV_8UC1, new Scalar(255));
Mat warpedMask = new Mat();
// Compute the rotation for the mask and the image
Imgproc.warpAffine(mask, warpedMask, rotationMatrix, bbox.size());
Imgproc.warpAffine(img.getSrc(), rotatedMasked, rotationMatrix, bbox.size(), Imgproc.INTER_LINEAR, Core.BORDER_REPLICATE, Scalar.all(255));
// Apply the mask to the rotated Mat
rotatedMasked.copyTo(rotated, warpedMask);
// Release the matrices before return
rotatedMasked.release();
mask.release();
warpedMask.release();
rotationMatrix.release();
closed.close();
return new Img(rotated, false);
}
private static Img getClosedImg(final Img img) {
double size = (closedImgSizeFactor * img.size().area());
// Round the size factor to the nearest odd int
size = 2 * (Math.floor(size / 2)) + 1;
// return img.bilateralFilter(20, 80, 80).adaptativeGaussianInvThreshold(17, 9).morphologyEx(Imgproc.MORPH_CLOSE, Imgproc.MORPH_ELLIPSE, new Size(closedImgSizeFactor, closedImgSizeFactor));
return img.bilateralFilter(20, 80, 80).bgr2Gray().grad(2.0d, 2.0d).thresHold(0, 255, Imgproc.THRESH_BINARY_INV + Imgproc.THRESH_OTSU).bitwise_not().morphologyEx(Imgproc.MORPH_CLOSE, Imgproc.MORPH_ELLIPSE, new Size(size, size));
}
private static double contoursDetection(final Mat dilated) {
List<RotatedRect> rotatedRects = getRotatedRects(dilated);
for (RotatedRect rotatedRect : rotatedRects) {
if (rotatedRect.angle <= -45.0) {
rotatedRect.angle += 90.0;
double tmp = rotatedRect.size.width;
rotatedRect.size.width = rotatedRect.size.height;
rotatedRect.size.height = tmp;
}
}
return getInliers(rotatedRects, 1.0).stream().mapToDouble(i -> i.angle).average().getAsDouble();
}
private static List<RotatedRect> getInliers(final List<RotatedRect> data, final double confidence) {
if (null == data)
return null;
double average = data.stream().mapToDouble(rect -> rect.angle).average().getAsDouble();
double sd = Math.sqrt(data.stream().mapToDouble(rect -> Math.pow(rect.angle - average, 2)).average().getAsDouble());
Collections.sort(data, (r1, r2) -> Double.compare(r1.angle, r2.angle));
int middle = data.size() / 2;
double median;
if (middle % 2 == 1)
median = data.get(middle).angle;
else
median = DoubleStream.of(data.get(middle).angle, data.get(middle - 1).angle).average().getAsDouble();
List<RotatedRect> result = data.stream().filter(rect -> Math.abs(rect.angle - median) < confidence * sd).collect(Collectors.toList());
// List<RotatedRect> result = data.stream().filter(rect -> Math.abs(rect.angle - average) < confidence * sd).collect(Collectors.toList());
// List<RotatedRect> result = data.stream().filter(rect -> Math.abs(rect.angle - average) < 5).collect(Collectors.toList());
return result;
}
private static List<RotatedRect> getRotatedRects(final Mat dilated) {
List<MatOfPoint> contours = new ArrayList<>();
Imgproc.findContours(dilated, contours, new Mat(), Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
double minArea = minAreaFactor * dilated.size().area();
List<RotatedRect> rotatedRects = contours.stream().filter(contour -> Imgproc.contourArea(contour) > minArea).map(contour -> Imgproc.minAreaRect(new MatOfPoint2f(contour.toArray()))).collect(Collectors.toList());
return rotatedRects;
}
}