Created
April 8, 2017 17:15
-
-
Save sminogue/7bd865e9e46a9a2ab079c22af08d1c0d to your computer and use it in GitHub Desktop.
Document scanner written in java using boofcv
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package imagery; | |
import java.awt.image.BufferedImage; | |
import java.util.ArrayList; | |
import java.util.Collections; | |
import java.util.Comparator; | |
import java.util.List; | |
import org.ddogleg.struct.FastQueue; | |
import boofcv.alg.distort.RemovePerspectiveDistortion; | |
import boofcv.alg.filter.basic.GrayImageOps; | |
import boofcv.alg.filter.binary.GThresholdImageOps; | |
import boofcv.alg.filter.binary.ThresholdImageOps; | |
import boofcv.alg.filter.blur.GBlurImageOps; | |
import boofcv.alg.shapes.polygon.BinaryPolygonDetector; | |
import boofcv.factory.shape.ConfigPolygonDetector; | |
import boofcv.factory.shape.FactoryShapeDetector; | |
import boofcv.gui.ListDisplayPanel; | |
import boofcv.gui.binary.VisualizeBinaryData; | |
import boofcv.io.image.ConvertBufferedImage; | |
import boofcv.io.image.UtilImageIO; | |
import boofcv.struct.image.GrayF32; | |
import boofcv.struct.image.GrayU8; | |
import boofcv.struct.image.ImageType; | |
import boofcv.struct.image.Planar; | |
import georegression.struct.point.Point2D_F64; | |
import georegression.struct.shapes.Polygon2D_F64; | |
public class Scan { | |
/** | |
* Take an image of a printed document fix its orientation and clean it up to be just pure text. | |
* The picture should be taken on a dark background (matte black would be best). The page should be | |
* oriented with the top of the page in the top of the picture. Some angle of the photo and rotation | |
* of the page in the image will be corrected. | |
*/ | |
public static void main( String[] args ) throws Exception { | |
//Load test image from disk | |
BufferedImage image = UtilImageIO.loadImage("test.jpg"); | |
//Convert to gray scale image | |
GrayU8 gray = ConvertBufferedImage.convertFromSingle(image, null, GrayU8.class); | |
//Brighten the image to wash out light colors in the background | |
GrayU8 brighter = GrayImageOps.brighten(gray, 100, 255, null); | |
//Reverse the black and white to make the paper page solid black for detection | |
GrayU8 invert = GrayImageOps.invert(brighter, 0, null); | |
//Detect 4 sided black polygon | |
ConfigPolygonDetector config = new ConfigPolygonDetector(4, 4); | |
BinaryPolygonDetector<GrayU8> detector = FactoryShapeDetector.polygon(config, GrayU8.class); | |
int threshold = GThresholdImageOps.computeOtsu(invert, 0, 255); | |
GrayU8 binary = new GrayU8(invert.width, invert.height); | |
ThresholdImageOps.threshold(invert, binary, threshold, true); | |
detector.process(invert, binary); | |
FastQueue<Polygon2D_F64> found = detector.getFoundPolygons(); | |
//Go through the found polygons and take the polygon with the largest area. | |
//This SHOULD be the page since this should be a picture of a page you are scanning | |
Polygon2D_F64 polygon = null; | |
double polygonSize = 0; | |
for (int i = 0; i < found.size; i++) { | |
Polygon2D_F64 poly = found.get(i); | |
double size = poly.areaSimple(); | |
if (size > polygonSize) { | |
polygonSize = size; | |
polygon = poly; | |
} | |
} | |
//If unable to identify page... Bail. | |
if (polygon == null) { | |
throw new Exception("Unable to identify page in image"); | |
} | |
//Get the four corners and add them to a list fot sorting. | |
List<Point2D_F64> points = new ArrayList<>(); | |
points.add(polygon.get(2)); | |
points.add(polygon.get(1)); | |
points.add(polygon.get(0)); | |
points.add(polygon.get(3)); | |
//Sort the points so that the array is in TL, TR, BR, BL order | |
points = sortPoints(points); | |
//Pull the four points out into variables for ease of use | |
Point2D_F64 topLeft = points.get(0); | |
Point2D_F64 topRight = points.get(1); | |
Point2D_F64 bottomRight = points.get(2); | |
Point2D_F64 bottomLeft = points.get(3); | |
//Convert the ORIGINAL image | |
Planar<GrayF32> input = ConvertBufferedImage.convertFromMulti(image, null, true, GrayF32.class); | |
//Based on the four corners get the width and height of the page | |
Double width = topRight.x - topLeft.x; | |
Double height = bottomLeft.y - topLeft.y; | |
//Setup to make the page top down | |
RemovePerspectiveDistortion<Planar<GrayF32>> removePerspective = new RemovePerspectiveDistortion<>(width.intValue(), height.intValue(), ImageType.pl(3, GrayF32.class)); | |
// Specify the corners in the input image of the region. | |
// Order matters! top-left, top-right, bottom-right, bottom-left | |
if (!removePerspective.apply(input, topLeft, topRight, bottomRight, bottomLeft)) { | |
throw new RuntimeException("Failed!?!?"); | |
} | |
//Get the re-oriented image | |
Planar<GrayF32> output = removePerspective.getOutput(); | |
//Turn it into a BufferedImage. This should be the page straight on and nothind else. | |
BufferedImage flat = ConvertBufferedImage.convertTo_F32(output, null, true); | |
//Turn page into a grayscale image | |
GrayF32 f32 = ConvertBufferedImage.convertFromSingle(flat, null, GrayF32.class); | |
//Apply filter to give us that pure black and white paper look. This should also | |
//Remove any artifacts like shadows or discolorations in the paper. | |
GrayU8 bw = new GrayU8(f32.width, f32.height); | |
GThresholdImageOps.localSauvola(f32, bw, 15, 0.2f, true); | |
//Turn the pure B&W image into a bufferedimage. Also invert the colors as the filter | |
//Turns the page black with white text. | |
BufferedImage finalInversed = VisualizeBinaryData.renderBinary(bw, true, null); | |
//Save the final image to disk | |
UtilImageIO.saveImage(finalInversed, "test2.jpg"); | |
} | |
/** | |
* Method which will sort four points into TL, TR, BR, BL order. | |
* Assumptions: The page is roughly in the proper orientation. If the page is turned | |
* 90 degrees it wont turn out well... But basically if the page is in orientation | |
* where the top of the page is in the top of the document this should work. | |
*/ | |
public static List<Point2D_F64> sortPoints( List<Point2D_F64> pts ) { | |
//Copy points into a working list | |
List<Point2D_F64> points = new ArrayList<Point2D_F64>(); | |
points.addAll(pts); | |
//Create list of points to be returned. | |
List<Point2D_F64> returns = new ArrayList<Point2D_F64>(); | |
//Sort the points by Y value | |
Collections.sort(points, new Comparator<Point2D_F64>() { | |
@Override | |
public int compare( Point2D_F64 a, Point2D_F64 b ) { | |
if (a.y < b.y) { | |
return -1; | |
} else if (a.y == b.y) { | |
return 0; | |
} else { | |
return 1; | |
} | |
} | |
}); | |
// First 2 elements in the list are the top of the page. The one with | |
// the lower X is TL the other is TR | |
Point2D_F64 a, b; | |
a = points.get(0); | |
b = points.get(1); | |
if (a.x < b.x) { | |
returns.add(a); | |
returns.add(b); | |
} else { | |
returns.add(b); | |
returns.add(a); | |
} | |
// Second 2 elements are the bottom points | |
a = points.get(2); | |
b = points.get(3); | |
if (a.x < b.x) { | |
returns.add(b); | |
returns.add(a); | |
} else { | |
returns.add(a); | |
returns.add(b); | |
} | |
return returns; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment