import java.awt.event.KeyEvent; import org.bytedeco.javacpp.*; import org.bytedeco.javacv.*; import static org.bytedeco.javacpp.opencv_core.*; import static org.bytedeco.javacpp.opencv_imgproc.*; import static org.bytedeco.javacpp.opencv_imgcodecs.*; /** * I was unable to find the OpenCV squares.c translated into JavaCV, so this * is a line-by-line translation of the C source. * The squares.c source used, found here: * https://code.ros.org/trac/opencv/browser/trunk/opencv/samples/c/squares.c?rev=1429 * * This is a demo class for finding squares/rectangles in an image, using JavaCV. * * All individual imports are kept as is; if you are like me, * you probably dislike the catch all .* import when trying to understand stuff. * * The major headache of the C code was figuring out the * "drawLines" method, and what parameters "cvPolyLine" is supposed to use. * * @author geir.ruud@digitalinferno.com */ public class Square { int thresh = 50; IplImage img = null; IplImage img0 = null; CvMemStorage storage = null; String wndname = "Square Detection Demo"; // Java spesific CanvasFrame canvas = null; OpenCVFrameConverter.ToIplImage converter = new OpenCVFrameConverter.ToIplImage(); // helper function: // finds a cosine of angle between vectors // from pt0->pt1 and from pt0->pt2 double angle(CvPoint pt1, CvPoint pt2, CvPoint pt0) { double dx1 = pt1.x() - pt0.x(); double dy1 = pt1.y() - pt0.y(); double dx2 = pt2.x() - pt0.x(); double dy2 = pt2.y() - pt0.y(); return (dx1*dx2 + dy1*dy2) / Math.sqrt((dx1*dx1 + dy1*dy1) * (dx2*dx2 + dy2*dy2) + 1e-10); } // returns sequence of squares detected on the image. // the sequence is stored in the specified memory storage CvSeq findSquares4(IplImage img, CvMemStorage storage) { // Java translation: moved into loop // CvSeq contours = new CvSeq(); int i, c, l, N = 11; CvSize sz = cvSize(img.width() & -2, img.height() & -2); IplImage timg = cvCloneImage(img); // make a copy of input image IplImage gray = cvCreateImage(sz, 8, 1); IplImage pyr = cvCreateImage(cvSize(sz.width()/2, sz.height()/2), 8, 3); IplImage tgray = null; // Java translation: moved into loop // CvSeq result = null; // double s = 0.0, t = 0.0; // create empty sequence that will contain points - // 4 points per square (the square's vertices) CvSeq squares = cvCreateSeq(0, Loader.sizeof(CvSeq.class), Loader.sizeof(CvPoint.class), storage); // select the maximum ROI in the image // with the width and height divisible by 2 cvSetImageROI(timg, cvRect(0, 0, sz.width(), sz.height())); // down-scale and upscale the image to filter out the noise cvPyrDown(timg, pyr, 7); cvPyrUp(pyr, timg, 7); tgray = cvCreateImage(sz, 8, 1); // find squares in every color plane of the image for (c = 0; c < 3; c++) { // extract the c-th color plane cvSetImageCOI(timg, c+1); cvCopy(timg, tgray); // try several threshold levels for (l = 0; l < N; l++) { // hack: use Canny instead of zero threshold level. // Canny helps to catch squares with gradient shading if (l == 0) { // apply Canny. Take the upper threshold from slider // and set the lower to 0 (which forces edges merging) cvCanny(tgray, gray, 0, thresh, 5); // dilate canny output to remove potential // holes between edge segments cvDilate(gray, gray, null, 1); } else { // apply threshold if l!=0: // tgray(x,y) = gray(x,y) < (l+1)*255/N ? 255 : 0 cvThreshold(tgray, gray, (l+1)*255/N, 255, CV_THRESH_BINARY); } // find contours and store them all as a list // Java translation: moved into the loop CvSeq contours = new CvSeq(); cvFindContours(gray, storage, contours, Loader.sizeof(CvContour.class), CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE, cvPoint(0,0)); // test each contour while (contours != null && !contours.isNull()) { // approximate contour with accuracy proportional // to the contour perimeter // Java translation: moved into the loop CvSeq result = cvApproxPoly(contours, Loader.sizeof(CvContour.class), storage, CV_POLY_APPROX_DP, cvContourPerimeter(contours)*0.02, 0); // square contours should have 4 vertices after approximation // relatively large area (to filter out noisy contours) // and be convex. // Note: absolute value of an area is used because // area may be positive or negative - in accordance with the // contour orientation if(result.total() == 4 && Math.abs(cvContourArea(result, CV_WHOLE_SEQ, 0)) > 1000 && cvCheckContourConvexity(result) != 0) { // Java translation: moved into loop double s = 0.0, t = 0.0; for( i = 0; i < 5; i++ ) { // find minimum angle between joint // edges (maximum of cosine) if( i >= 2 ) { // Java translation: // Comment from the HoughLines.java sample code: // " Based on JavaCPP, the equivalent of the C code: // CvPoint* line = (CvPoint*)cvGetSeqElem(lines,i); // CvPoint first=line[0]; // CvPoint second=line[1]; // is: // Pointer line = cvGetSeqElem(lines, i); // CvPoint first = new CvPoint(line).position(0); // CvPoint second = new CvPoint(line).position(1); // " // ... so after some trial and error this seem to work // t = fabs(angle( // (CvPoint*)cvGetSeqElem( result, i ), // (CvPoint*)cvGetSeqElem( result, i-2 ), // (CvPoint*)cvGetSeqElem( result, i-1 ))); t = Math.abs(angle(new CvPoint(cvGetSeqElem(result, i)), new CvPoint(cvGetSeqElem(result, i-2)), new CvPoint(cvGetSeqElem(result, i-1)))); s = s > t ? s : t; } } // if cosines of all angles are small // (all angles are ~90 degree) then write quandrange // vertices to resultant sequence if (s < 0.3) for( i = 0; i < 4; i++ ) { cvSeqPush(squares, cvGetSeqElem(result, i)); } } // take the next contour contours = contours.h_next(); } } } // release all the temporary images cvReleaseImage(gray); cvReleaseImage(pyr); cvReleaseImage(tgray); cvReleaseImage(timg); return squares; } // the function draws all the squares in the image void drawSquares(IplImage img, CvSeq squares) { // Java translation: Here the code is somewhat different from the C version. // I was unable to get straight forward CvPoint[] arrays // working with "reader" and the "CV_READ_SEQ_ELEM". // CvSeqReader reader = new CvSeqReader(); IplImage cpy = cvCloneImage(img); int i = 0; // Used by attempt 3 // Create a "super"-slice, consisting of the entire sequence of squares CvSlice slice = new CvSlice(squares); // initialize reader of the sequence // cvStartReadSeq(squares, reader, 0); // read 4 sequence elements at a time (all vertices of a square) for(i = 0; i < squares.total(); i += 4) { // // Attempt 1: // // This does not work, uses the "reader" // CvPoint pt[] = new CvPoint[]{new CvPoint(1), new CvPoint(1), new CvPoint(1), new CvPoint(1)}; // PointerPointer rect = new PointerPointer(pt); // int count[] = new int[]{4}; // // CV_READ_SEQ_ELEM(pt[0], reader); // CV_READ_SEQ_ELEM(pt[1], reader); // CV_READ_SEQ_ELEM(pt[2], reader); // CV_READ_SEQ_ELEM(pt[3], reader); // // Attempt 2: // // This works, somewhat similar to the C code, somewhat messy, does not use the "reader" // CvPoint pt[] = new CvPoint[]{ // new CvPoint(cvGetSeqElem(squares, i)), // new CvPoint(cvGetSeqElem(squares, i + 1)), // new CvPoint(cvGetSeqElem(squares, i + 2)), // new CvPoint(cvGetSeqElem(squares, i + 3))}; // PointerPointer rect = new PointerPointer(pt); // int count[] = new int[]{4}; // Attempt 3: // This works, may be the "cleanest" solution, does not use the "reader" CvPoint rect = new CvPoint(4); IntPointer count = new IntPointer(1).put(4); // get the 4 corner slice from the "super"-slice cvCvtSeqToArray(squares, rect, slice.start_index(i).end_index(i + 4)); // // Attempt 4: // // This works, and look the most like the original C code, uses the "reader" // CvPoint rect = new CvPoint(4); // int count[] = new int[]{4}; // // // read 4 vertices // CV_READ_SEQ_ELEM(rect.position(0), reader); // CV_READ_SEQ_ELEM(rect.position(1), reader); // CV_READ_SEQ_ELEM(rect.position(2), reader); // CV_READ_SEQ_ELEM(rect.position(3), reader); // draw the square as a closed polyline // Java translation: gotcha (re-)setting the opening "position" of the CvPoint sequence thing cvPolyLine(cpy, rect.position(0), count, 1, 1, CV_RGB(0,255,0), 3, CV_AA, 0); } // show the resultant image // cvShowImage(wndname, cpy); canvas.showImage(converter.convert(cpy)); cvReleaseImage(cpy); } String names[] = new String[]{ "pic1.png", "pic2.png", "pic3.png", "pic4.png", "pic5.png", "pic6.png" }; public static void main(String args[]) throws Exception { new Square().main(); } public void main() throws InterruptedException { // Java translation: c not used int i; // , c; // create memory storage that will contain all the dynamic data storage = cvCreateMemStorage(0); for(i = 0; i < names.length; i++) { // load i-th image // Java translation String filePathAndName = Square.class.getClassLoader().getResource(names[i]).getPath(); filePathAndName = filePathAndName == null || filePathAndName.isEmpty() ? names[i] : filePathAndName; // img0 = cvLoadImage(names[i], 1); img0 = cvLoadImage(filePathAndName, 1); if (img0 == null) { System.err.println("Couldn't load " + names[i]); continue; } img = cvCloneImage(img0); // create window and a trackbar (slider) with parent "image" and set callback // (the slider regulates upper threshold, passed to Canny edge detector) // Java translation canvas = new CanvasFrame(wndname, 1); canvas.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE); // cvNamedWindow( wndname, 1 ); // find and draw the squares drawSquares(img, findSquares4(img, storage)); // wait for key. // Also the function cvWaitKey takes care of event processing // Java translation KeyEvent key = canvas.waitKey(0); // c = cvWaitKey(0); // release both images cvReleaseImage(img); cvReleaseImage(img0); // clear memory storage - reset free space position cvClearMemStorage(storage); if (key.getKeyCode() == 27) { break; } } // cvDestroyWindow( wndname ); if (canvas != null) { canvas.dispose(); } } }