Skip to content

Instantly share code, notes, and snippets.

@sminogue
Created April 8, 2017 17:15
Show Gist options
  • Save sminogue/7bd865e9e46a9a2ab079c22af08d1c0d to your computer and use it in GitHub Desktop.
Save sminogue/7bd865e9e46a9a2ab079c22af08d1c0d to your computer and use it in GitHub Desktop.
Document scanner written in java using boofcv
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