forked from ytsutano/bookscan
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
9,016 additions
and
1 deletion.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,3 +10,9 @@ | |
*.lai | ||
*.la | ||
*.a | ||
|
||
# Ignore SCons related files | ||
.sconsign.dblite | ||
|
||
# Ignore OS X related files | ||
.DS_Store |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,36 @@ | ||
bookscan | ||
======== | ||
|
||
Single camera solution for book scanner. | ||
Single camera solution for book scanner. | ||
|
||
## WARNING: EXPERIMENTAL PROJECT. | ||
|
||
This program was written in ad-hoc manner without thinking about | ||
publishing the source code. It was just a test of concept. | ||
|
||
I've got many requests to publish the program after I posted YouTube videos: | ||
* [Book Scanner: First Prototype](http://www.youtube.com/watch?v=rjzxlA9RWio) | ||
* [Book Scanner: Marker Test](http://www.youtube.com/watch?v=YXANjnry6CU) | ||
* [Book Scanner: Image Processing Test #1](http://www.youtube.com/watch?v=lHHPFBH2EkA) | ||
|
||
So I published it. But it was a project that was done in a day years ago, and | ||
I have no time to verify it still works today. | ||
|
||
## Compilation | ||
|
||
I have installed OpenCV using MacPorts: | ||
sudo port install opencv | ||
|
||
Compile using SCons: | ||
scons | ||
|
||
## Usage | ||
|
||
The program takes three arguments: | ||
./extpage test_input.jpg output_left.jpg output_right.jpg | ||
where test_input.jpg is the input file name, and the following two are the | ||
output file names. | ||
|
||
## Contributor | ||
|
||
[Yutaka Tsutano](http://yutaka.tsutano.com) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import os.path | ||
|
||
sources = ['main.cpp', 'marker.cpp', 'page.cpp'] | ||
target = 'extpage' | ||
objs = [] | ||
|
||
for s in sources: | ||
objs.append(s.replace('.cpp', '.o')) | ||
|
||
env = Environment(ENV = {'PATH': os.environ['PATH']}) | ||
|
||
env.AppendUnique(CCFLAGS = ['-O2']) | ||
env.AppendUnique(CCFLAGS = ['-Wall']) | ||
|
||
env.AppendUnique(CCFLAGS = ['-I/opt/local/include/']) | ||
env.AppendUnique(CCFLAGS = ['-I/opt/local/include/opencv']) | ||
env.AppendUnique(CCFLAGS = ['-I/opt/local/include/opencv2']) | ||
|
||
env.AppendUnique(LINKFLAGS = ['-L/opt/local/lib']) | ||
env.AppendUnique(LINKFLAGS = ['-lopencv_core']) | ||
env.AppendUnique(LINKFLAGS = ['-lopencv_highgui']) | ||
env.AppendUnique(LINKFLAGS = ['-lopencv_imgproc']) | ||
env.AppendUnique(LINKFLAGS = ['-lopencv_calib3d']) | ||
|
||
env.Object(source = sources) | ||
env.Program(target = target, source = objs) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
#include <opencv2/objdetect/objdetect.hpp> | ||
#include <opencv2/features2d/features2d.hpp> | ||
#include <opencv2/highgui/highgui.hpp> | ||
#include <opencv2/calib3d/calib3d.hpp> | ||
#include <opencv2/imgproc/imgproc_c.h> | ||
|
||
#include <iostream> | ||
#include <vector> | ||
#include <string> | ||
|
||
#include "marker.h" | ||
#include "page.h" | ||
|
||
void process_image(IplImage *src_img, | ||
std::map<int, CvPoint2D32f> &left_dst_markers, | ||
LayoutInfo left_layout, | ||
std::map<int, CvPoint2D32f> &right_dst_markers, | ||
LayoutInfo right_layout) | ||
{ | ||
BookImage book_image(src_img); | ||
|
||
{ | ||
IplImage *dst_img | ||
= book_image.create_page_image(left_dst_markers, left_layout); | ||
if (dst_img != NULL) { | ||
cvShowImage("Left", dst_img); | ||
cvReleaseImage(&dst_img); | ||
} | ||
} | ||
|
||
{ | ||
IplImage *dst_img | ||
= book_image.create_page_image(right_dst_markers, right_layout); | ||
if (dst_img != NULL) { | ||
cvShowImage("Right", dst_img); | ||
cvReleaseImage(&dst_img); | ||
} | ||
} | ||
} | ||
|
||
int main(int argc, char **argv) | ||
{ | ||
// Configure left page. | ||
std::map<int, CvPoint2D32f> left_dst_markers; | ||
left_dst_markers[0] = cvPoint2D32f(0.00, 0.00); | ||
left_dst_markers[1] = cvPoint2D32f(6.00, 0.00); | ||
left_dst_markers[2] = cvPoint2D32f(6.00, 9.50); | ||
left_dst_markers[3] = cvPoint2D32f(0.00, 9.50); | ||
LayoutInfo left_layout; | ||
left_layout.page_left = 0.50; | ||
left_layout.page_top = 0.25; | ||
left_layout.page_right = 6.30; | ||
left_layout.page_bottom = 9.20; | ||
left_layout.dpi = 600.0; | ||
|
||
// Configure right page. | ||
std::map<int, CvPoint2D32f> right_dst_markers; | ||
right_dst_markers[4] = cvPoint2D32f(0.00, 0.00); | ||
right_dst_markers[5] = cvPoint2D32f(6.00, 0.00); | ||
right_dst_markers[6] = cvPoint2D32f(6.00, 9.50); | ||
right_dst_markers[7] = cvPoint2D32f(0.00, 9.50); | ||
LayoutInfo right_layout; | ||
right_layout.page_left = -0.30; | ||
right_layout.page_top = 0.25; | ||
right_layout.page_right = 5.50; | ||
right_layout.page_bottom = 9.20; | ||
right_layout.dpi = 600.0; | ||
|
||
// Process if an input image is supplied; otherwise, open a webcam for | ||
// debugging. | ||
if (argc > 3) { | ||
IplImage *src_img = cvLoadImage(argv[1]); | ||
if (src_img == NULL) { | ||
std::cerr << "Failed to load the source image specified.\n"; | ||
return 1; | ||
} | ||
|
||
BookImage book_img(src_img); | ||
|
||
IplImage *left_img | ||
= book_img.create_page_image(left_dst_markers, left_layout); | ||
if (left_img != NULL) { | ||
cvSaveImage(argv[2], left_img); | ||
cvReleaseImage(&left_img); | ||
} | ||
|
||
IplImage *right_img | ||
= book_img.create_page_image(right_dst_markers, right_layout); | ||
if (right_img != NULL) { | ||
cvSaveImage(argv[3], right_img); | ||
cvReleaseImage(&right_img); | ||
} | ||
|
||
cvReleaseImage(&src_img); | ||
} else { | ||
// Create windows. | ||
cvNamedWindow("Source", 0); | ||
cvResizeWindow("Source", 480, 640); | ||
|
||
left_layout.dpi = 100; | ||
right_layout.dpi = 100; | ||
|
||
// Open webcam. | ||
CvCapture* capture = cvCreateCameraCapture(0); | ||
if (!capture) { | ||
std::cerr << "Failed to load the camera device.\n"; | ||
return 1; | ||
} | ||
const double scale = 1.0; | ||
cvSetCaptureProperty(capture, CV_CAP_PROP_FRAME_WIDTH, 1600 * scale); | ||
cvSetCaptureProperty(capture, CV_CAP_PROP_FRAME_HEIGHT, 1200 * scale); | ||
|
||
while (cvWaitKey(10) < 0) { | ||
IplImage *src_img = cvQueryFrame(capture); | ||
cvShowImage("Source", src_img); | ||
process_image(src_img, | ||
left_dst_markers, left_layout, | ||
right_dst_markers, right_layout); | ||
} | ||
} | ||
|
||
return 0; | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
#include "marker.h" | ||
|
||
int decode_marker(CvMat *mark_mat, marker_rotation_t &rotation) | ||
{ | ||
// Make sure that the outermost cells are black. | ||
for (int i = 0; i < 6; i++) { | ||
if (cvmGet(mark_mat, i, 0) > 0.5 | ||
|| cvmGet(mark_mat, i, 5) > 0.5 | ||
|| cvmGet(mark_mat, 0, i) > 0.5 | ||
|| cvmGet(mark_mat, 5, i) > 0.5) { | ||
return -1; | ||
} | ||
} | ||
|
||
// Make sure that the next cells of the outermost cells are white. | ||
if (cvmGet(mark_mat, 2, 1) < 0.5 | ||
|| cvmGet(mark_mat, 3, 1) < 0.5 | ||
|| cvmGet(mark_mat, 1, 2) < 0.5 | ||
|| cvmGet(mark_mat, 1, 3) < 0.5 | ||
|| cvmGet(mark_mat, 2, 4) < 0.5 | ||
|| cvmGet(mark_mat, 3, 4) < 0.5 | ||
|| cvmGet(mark_mat, 4, 2) < 0.5 | ||
|| cvmGet(mark_mat, 4, 3) < 0.5) { | ||
return -1; | ||
} | ||
|
||
// Make sure that the number of corner marker is exactly one. Also | ||
// detect the orientation. | ||
int numberOfCornerMarkers = 0; | ||
if (cvmGet(mark_mat, 1, 1) < 0.5) { | ||
numberOfCornerMarkers++; | ||
rotation = MARKER_ROT_0_DEG; | ||
} | ||
if (cvmGet(mark_mat, 1, 4) < 0.5) { | ||
numberOfCornerMarkers++; | ||
rotation = MARKER_ROT_90_DEG; | ||
} | ||
if (cvmGet(mark_mat, 4, 4) < 0.5) { | ||
numberOfCornerMarkers++; | ||
rotation = MARKER_ROT_180_DEG; | ||
} | ||
if (cvmGet(mark_mat, 4, 1) < 0.5) { | ||
numberOfCornerMarkers++; | ||
rotation = MARKER_ROT_270_DEG; | ||
} | ||
if (numberOfCornerMarkers != 1) { | ||
return -1; | ||
} | ||
|
||
int id = 0; | ||
switch (rotation) { | ||
case MARKER_ROT_0_DEG: | ||
id = ((cvmGet(mark_mat, 2, 2) < 0.5) << 3) | ||
| ((cvmGet(mark_mat, 2, 3) < 0.5) << 2) | ||
| ((cvmGet(mark_mat, 3, 2) < 0.5) << 1) | ||
| ((cvmGet(mark_mat, 3, 3) < 0.5) << 0); | ||
break; | ||
case MARKER_ROT_90_DEG: | ||
id = ((cvmGet(mark_mat, 2, 2) < 0.5) << 1) | ||
| ((cvmGet(mark_mat, 2, 3) < 0.5) << 3) | ||
| ((cvmGet(mark_mat, 3, 2) < 0.5) << 0) | ||
| ((cvmGet(mark_mat, 3, 3) < 0.5) << 2); | ||
break; | ||
case MARKER_ROT_180_DEG: | ||
id = ((cvmGet(mark_mat, 2, 2) < 0.5) << 0) | ||
| ((cvmGet(mark_mat, 2, 3) < 0.5) << 1) | ||
| ((cvmGet(mark_mat, 3, 2) < 0.5) << 2) | ||
| ((cvmGet(mark_mat, 3, 3) < 0.5) << 3); | ||
break; | ||
case MARKER_ROT_270_DEG: | ||
id = ((cvmGet(mark_mat, 2, 2) < 0.5) << 2) | ||
| ((cvmGet(mark_mat, 2, 3) < 0.5) << 0) | ||
| ((cvmGet(mark_mat, 3, 2) < 0.5) << 3) | ||
| ((cvmGet(mark_mat, 3, 3) < 0.5) << 1); | ||
break; | ||
} | ||
|
||
// Now determine the id using a table. | ||
static const unsigned char id_table[] = { | ||
8, 2, 4, 15, 6, 13, 11, 1, | ||
0, 10, 12, 7, 14, 5, 3, 9 | ||
}; | ||
id = id_table[id]; | ||
|
||
return id; | ||
} | ||
|
||
int analyze_marker(const IplImage *src_img, CvSeq *poly, CvPoint2D32f *points) | ||
{ | ||
// Make sure the shape is square and convex. | ||
if (poly->total != 4 | ||
|| !cvCheckContourConvexity(poly) | ||
|| cvContourArea(poly) < 360.0) { | ||
return -1; | ||
} | ||
|
||
for (int i = 0; i < 4; i++) { | ||
points[i] = cvPointTo32f(*CV_GET_SEQ_ELEM(CvPoint, poly, i)); | ||
} | ||
|
||
// Refine to sub-pixel accuracy. | ||
cvFindCornerSubPix(src_img, points, 4, cvSize(3, 3), cvSize(-1, -1), | ||
cvTermCriteria (CV_TERMCRIT_ITER | CV_TERMCRIT_EPS, 20, 0.03)); | ||
|
||
double a[] = { | ||
points[0].x, points[0].y, | ||
points[1].x, points[1].y, | ||
points[2].x, points[2].y, | ||
points[3].x, points[3].y | ||
}; | ||
CvMat src_points = cvMat(poly->total, 2, CV_64FC1, a); | ||
|
||
static const int MARK_WIDTH = 6*3; | ||
static const int MARK_HEIGHT = 6*3; | ||
static const int MARK_TEMP_WIDTH = MARK_WIDTH * 10; | ||
static const int MARK_TEMP_HEIGHT = MARK_HEIGHT * 10; | ||
IplImage *mark_temp_img = cvCreateImage( | ||
cvSize(MARK_TEMP_WIDTH, MARK_TEMP_HEIGHT), IPL_DEPTH_8U, 1); | ||
double b[] = { | ||
0, 0, | ||
0, MARK_TEMP_HEIGHT, | ||
MARK_TEMP_WIDTH, MARK_TEMP_HEIGHT, | ||
MARK_TEMP_WIDTH, 0, | ||
}; | ||
CvMat mark_points = cvMat(poly->total, 2, CV_64FC1, b); | ||
|
||
// Compute homography matrix. | ||
CvMat *h = cvCreateMat(3, 3, CV_64FC1); | ||
cvFindHomography(&src_points, &mark_points, h); | ||
|
||
// Transform perspective. | ||
cvWarpPerspective(src_img, mark_temp_img, h); | ||
|
||
IplImage *mark_img = cvCreateImage( | ||
cvSize(MARK_WIDTH, MARK_HEIGHT), IPL_DEPTH_8U, 1); | ||
cvResize(mark_temp_img, mark_img, CV_INTER_AREA); | ||
int threshold = cvAvg(mark_img).val[0]; | ||
cvThreshold(mark_img, mark_img, threshold, 255, CV_THRESH_BINARY); | ||
for (int i = 0; i < MARK_WIDTH; i++) { | ||
for (int j = 0; j < MARK_HEIGHT; j++) { | ||
if ((i % 3) != 1 || (j % 3) != 1) { | ||
cvSetReal2D(mark_img, i, j, threshold); | ||
} | ||
} | ||
} | ||
|
||
// Create marker matrix. | ||
CvMat *mark_mat = cvCreateMat(6, 6, CV_64FC1); | ||
for (int i = 0; i < 6; i++) { | ||
for (int j = 0; j < 6; j++) { | ||
cvmSet(mark_mat, i, j, (cvGetReal2D(mark_img, i*3+1, j*3+1) > 128)); | ||
} | ||
} | ||
|
||
// Decode the marker ID. | ||
marker_rotation_t rotation; | ||
int marker_id = decode_marker(mark_mat, rotation); | ||
|
||
// Based on rotation, correct the points array so that it starts from | ||
// the corner with the rotation dot. | ||
CvPoint2D32f temp[4]; | ||
for (int i = 0; i < 4; i++) { | ||
temp[i] = points[i]; | ||
} | ||
for (int i = 0; i < 4; i++) { | ||
points[i] = temp[(i + rotation) % 4]; | ||
} | ||
|
||
// Show decoded marker image for debugging. | ||
/* | ||
if (marker_id != -1) { | ||
cvResize(mark_img, mark_temp_img, CV_INTER_AREA); | ||
cvShowImage("Window", mark_temp_img); | ||
} | ||
//*/ | ||
|
||
// Clean up. | ||
cvReleaseMat(&h); | ||
cvReleaseMat(&mark_mat); | ||
cvReleaseImage(&mark_img); | ||
cvReleaseImage(&mark_temp_img); | ||
|
||
return marker_id; | ||
} |
Oops, something went wrong.