org.ar.rubik.ImageRecognizer.java Source code

Java tutorial

Introduction

Here is the source code for org.ar.rubik.ImageRecognizer.java

Source

/**
 * Augmented Reality Rubik Cube Wizard
 * 
 * Author: Steven P. Punte (aka Android Steve : android.steve@cl-sw.com)
 * Date:   April 25th 2015
 * 
 * Project Description:
 *   Android application developed on a commercial Smart Phone which, when run on a pair 
 *   of Smart Glasses, guides a user through the process of solving a Rubik Cube.
 *   
 * File Description:
 *   Function onFrameListener() really comprises the heart of the application "Controller".
 *   That is, every Camera Frame is like a event possibly resulting in a change in the
 *   application "State" ( or "Model" in the MVC vernacular).
 *   
 *   This class implements the OpenCV Frame Listener. 
 * 
 * License:
 * 
 *  GPL
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.ar.rubik;

import java.util.LinkedList;
import java.util.List;

import org.ar.rubik.Constants.ColorTileEnum;
import org.ar.rubik.Constants.ImageProcessModeEnum;
import org.ar.rubik.Constants.ImageSourceModeEnum;
import org.ar.rubik.RubikFace.FaceRecognitionStatusEnum;
import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame;
import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2;
import org.opencv.core.Core;
import org.opencv.core.CvException;
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.Size;
import org.opencv.imgproc.Imgproc;

import android.util.Log;

/**
 * @author android.steve@cl-sw.com
 *
 */
public class ImageRecognizer implements CvCameraViewListener2 {

    private AppStateMachine appStateMachine;
    private StateModel stateModel;
    private Annotation annotation;

    // Once an exception or error is encountered, display message from thence forth.
    // We cannot use Toast; it must be used on the UI thread and we are executing on the Frame thread.
    private Mat errorImage = null;

    // Time stamp for internal frame-per-second measuring and reporting.
    private long framesPerSecondTimeStamp;

    /**
     * Image Recognizer Constructor
     * 
     * @param appStateMachine
     * @param stateModel
     */
    public ImageRecognizer(AppStateMachine appStateMachine, StateModel stateModel) {
        this.appStateMachine = appStateMachine;
        this.stateModel = stateModel;
        this.annotation = new Annotation(this.stateModel, this.appStateMachine);
    }

    /**
     * On Camer View Started
     * 
     * =+= is this called when screen rotates?
     * 
     *  (non-Javadoc)
     * @see org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2#onCameraViewStarted(int, int)
     */
    @Override
    public void onCameraViewStarted(int width, int height) {
        stateModel.openCVSize = new Size(width, height);
    }

    /**
     * On Camera View Stopped
     * 
     *  (non-Javadoc)
     * @see org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2#onCameraViewStopped()
     */
    @Override
    public void onCameraViewStopped() {
    }

    /**
     * On Camera Frame
     * 
     * Process frame image through Rubik Face recognition possibly resulting in a state change.
     * 
     *  (non-Javadoc)
     * @see org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2#onCameraFrame(org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame)
     */
    @Override
    public Mat onCameraFrame(CvCameraViewFrame inputFrame) {

        //      Log.e(Constants.TAG, "CV Thread ID = " + Thread.currentThread().getId());

        // Just display error message if it is non-null.
        if (errorImage != null)
            return errorImage;

        Mat image = inputFrame.rgba();
        Size imageSize = image.size();
        Log.v(Constants.TAG_CAL, "Input Frame width=" + imageSize.width + " height=" + imageSize.height);
        if (imageSize.width != stateModel.openCVSize.width || imageSize.height != stateModel.openCVSize.height)
            Log.e(Constants.TAG_CAL, "State Model openCVSize does not agree with input frame!");

        // Save or Recall image as requested
        switch (MenuAndParams.imageSourceMode) {
        case NORMAL:
            break;
        case SAVE_NEXT:
            Util.saveImage(image);
            MenuAndParams.imageSourceMode = ImageSourceModeEnum.NORMAL;
            break;
        case PLAYBACK:
            image = Util.recallImage();
        default:
            break;
        }

        // Calculate and display Frames Per Second
        long newTimeStamp = System.currentTimeMillis();
        if (framesPerSecondTimeStamp > 0) {
            long frameTime = newTimeStamp - framesPerSecondTimeStamp;
            double framesPerSecond = 1000.0 / frameTime;
            String string = String.format("%4.1f FPS", framesPerSecond);
            Core.putText(image, string, new Point(50, 700), Constants.FontFace, 2, ColorTileEnum.WHITE.cvColor, 2);
        }
        framesPerSecondTimeStamp = newTimeStamp;

        try {

            // Initialize
            RubikFace rubikFace = new RubikFace();
            rubikFace.profiler.markTime(Profiler.Event.START);
            Log.i(Constants.TAG, "============================================================================");

            /* **********************************************************************
             * **********************************************************************
             * Return Original Image
             */
            if (MenuAndParams.imageProcessMode == ImageProcessModeEnum.DIRECT) {
                stateModel.activeRubikFace = rubikFace;
                rubikFace.profiler.markTime(Profiler.Event.TOTAL);
                return annotation.drawAnnotation(image);
            }

            /* **********************************************************************
             * **********************************************************************
             * Process to Grey Scale
             * 
             * This algorithm finds highlights areas that are all of nearly
             * the same hue.  In particular, cube faces should be highlighted.
             */
            Mat greyscale_image = new Mat();
            Imgproc.cvtColor(image, greyscale_image, Imgproc.COLOR_BGR2GRAY);
            rubikFace.profiler.markTime(Profiler.Event.GREYSCALE);
            if (MenuAndParams.imageProcessMode == ImageProcessModeEnum.GREYSCALE) {
                stateModel.activeRubikFace = rubikFace;
                rubikFace.profiler.markTime(Profiler.Event.TOTAL);
                image.release();
                return annotation.drawAnnotation(greyscale_image);
            }

            /* **********************************************************************
             * **********************************************************************
             * Gaussian Filter Blur prevents getting a lot of false hits 
             */
            Mat blur_image = new Mat();

            int kernelSize = (int) MenuAndParams.gaussianBlurKernelSizeParam.value;
            kernelSize = kernelSize % 2 == 0 ? kernelSize + 1 : kernelSize; // make odd
            Imgproc.GaussianBlur(greyscale_image, blur_image, new Size(kernelSize, kernelSize), -1, -1);
            rubikFace.profiler.markTime(Profiler.Event.GAUSSIAN);
            greyscale_image.release();
            if (MenuAndParams.imageProcessMode == ImageProcessModeEnum.GAUSSIAN) {
                stateModel.activeRubikFace = rubikFace;
                rubikFace.profiler.markTime(Profiler.Event.TOTAL);
                image.release();
                return annotation.drawAnnotation(blur_image);
            }

            /* **********************************************************************
             * **********************************************************************
             * Canny Edge Detection
             */
            Mat canny_image = new Mat();
            Imgproc.Canny(blur_image, canny_image, MenuAndParams.cannyLowerThresholdParam.value,
                    MenuAndParams.cannyUpperThresholdParam.value, 3, // Sobel Aperture size.  This seems to be typically value used in the literature: i.e., a 3x3 Sobel Matrix.
                    false); // use cheap gradient calculation: norm =|dI/dx|+|dI/dy|
            rubikFace.profiler.markTime(Profiler.Event.EDGE);
            blur_image.release();
            if (MenuAndParams.imageProcessMode == ImageProcessModeEnum.CANNY) {
                stateModel.activeRubikFace = rubikFace;
                rubikFace.profiler.markTime(Profiler.Event.TOTAL);
                image.release();
                return annotation.drawAnnotation(canny_image);
            }

            /* **********************************************************************
             * **********************************************************************
             * Dilation Image Process
             */
            Mat dilate_image = new Mat();
            Imgproc.dilate(canny_image, dilate_image, Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(
                    MenuAndParams.dilationKernelSizeParam.value, MenuAndParams.dilationKernelSizeParam.value)));
            rubikFace.profiler.markTime(Profiler.Event.DILATION);
            canny_image.release();
            if (MenuAndParams.imageProcessMode == ImageProcessModeEnum.DILATION) {
                stateModel.activeRubikFace = rubikFace;
                rubikFace.profiler.markTime(Profiler.Event.TOTAL);
                image.release();
                return annotation.drawAnnotation(dilate_image);
            }

            /* **********************************************************************
             * **********************************************************************
             * Contour Generation 
             */
            List<MatOfPoint> contours = new LinkedList<MatOfPoint>();
            Mat heirarchy = new Mat();
            Imgproc.findContours(dilate_image, contours, heirarchy, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE); // Note: tried other TC89 options, but no significant change or improvement on cpu time.
            rubikFace.profiler.markTime(Profiler.Event.CONTOUR);
            dilate_image.release();

            // Create gray scale image but in RGB format, and then added yellow colored contours on top.
            if (MenuAndParams.imageProcessMode == ImageProcessModeEnum.CONTOUR) {
                stateModel.activeRubikFace = rubikFace;
                rubikFace.profiler.markTime(Profiler.Event.TOTAL);
                Mat gray_image = new Mat(imageSize, CvType.CV_8UC4);
                Mat rgba_gray_image = new Mat(imageSize, CvType.CV_8UC4);
                Imgproc.cvtColor(image, gray_image, Imgproc.COLOR_RGB2GRAY);
                Imgproc.cvtColor(gray_image, rgba_gray_image, Imgproc.COLOR_GRAY2BGRA, 3);
                Imgproc.drawContours(rgba_gray_image, contours, -1, ColorTileEnum.YELLOW.cvColor, 3);
                Core.putText(rgba_gray_image, "Num Contours: " + contours.size(), new Point(500, 50),
                        Constants.FontFace, 4, ColorTileEnum.RED.cvColor, 4);
                gray_image.release();
                image.release();
                return annotation.drawAnnotation(rgba_gray_image);
            }

            /* **********************************************************************
             * **********************************************************************
             * Polygon Detection
             */
            List<Rhombus> polygonList = new LinkedList<Rhombus>();
            for (MatOfPoint contour : contours) {

                // Keep only counter clockwise contours.  A clockwise contour is reported as a negative number.
                double contourArea = Imgproc.contourArea(contour, true);
                if (contourArea < 0.0)
                    continue;

                // Keep only reasonable area contours
                if (contourArea < MenuAndParams.minimumContourAreaParam.value)
                    continue;

                // Floating, instead of Double, for some reason required for approximate polygon detection algorithm.
                MatOfPoint2f contour2f = new MatOfPoint2f();
                MatOfPoint2f polygone2f = new MatOfPoint2f();
                MatOfPoint polygon = new MatOfPoint();

                // Make a Polygon out of a contour with provide Epsilon accuracy parameter.
                // It uses the Douglas-Peucker algorithm http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
                contour.convertTo(contour2f, CvType.CV_32FC2);
                Imgproc.approxPolyDP(contour2f, polygone2f, MenuAndParams.polygonEpsilonParam.value, // The maximum distance between the original curve and its approximation.
                        true); // Resulting polygon representation is "closed:" its first and last vertices are connected.
                polygone2f.convertTo(polygon, CvType.CV_32S);

                polygonList.add(new Rhombus(polygon));
            }

            rubikFace.profiler.markTime(Profiler.Event.POLYGON);

            // Create gray scale image but in RGB format, and then add yellow colored polygons on top.
            if (MenuAndParams.imageProcessMode == ImageProcessModeEnum.POLYGON) {
                stateModel.activeRubikFace = rubikFace;
                rubikFace.profiler.markTime(Profiler.Event.TOTAL);
                Mat gray_image = new Mat(imageSize, CvType.CV_8UC4);
                Mat rgba_gray_image = new Mat(imageSize, CvType.CV_8UC4);
                Imgproc.cvtColor(image, gray_image, Imgproc.COLOR_RGB2GRAY);
                Imgproc.cvtColor(gray_image, rgba_gray_image, Imgproc.COLOR_GRAY2BGRA, 4);
                for (Rhombus polygon : polygonList)
                    polygon.draw(rgba_gray_image, ColorTileEnum.YELLOW.cvColor);
                Core.putText(rgba_gray_image, "Num Polygons: " + polygonList.size(), new Point(500, 50),
                        Constants.FontFace, 3, ColorTileEnum.RED.cvColor, 4);
                return annotation.drawAnnotation(rgba_gray_image);
            }

            /* **********************************************************************
             * **********************************************************************
             * Rhombus Tile Recognition
             * 
             * From polygon list, produces a list of suitable Parallelograms (Rhombi).
             */
            Log.i(Constants.TAG, String.format("Rhombus:   X    Y   Area   a-a  b-a a-l b-l gamma"));
            List<Rhombus> rhombusList = new LinkedList<Rhombus>();
            // Get only valid Rhombus(es) : actually parallelograms.
            for (Rhombus rhombus : polygonList) {
                rhombus.qualify();
                if (rhombus.status == Rhombus.StatusEnum.VALID)
                    rhombusList.add(rhombus);
            }

            // Filtering w.r.t. Rhmobus set characteristics
            Rhombus.removedOutlierRhombi(rhombusList);

            rubikFace.profiler.markTime(Profiler.Event.RHOMBUS);

            // Create gray scale image but in RGB format, and then add yellow colored Rhombi (parallelograms) on top.
            if (MenuAndParams.imageProcessMode == ImageProcessModeEnum.RHOMBUS) {
                stateModel.activeRubikFace = rubikFace;
                rubikFace.profiler.markTime(Profiler.Event.TOTAL);
                Mat gray_image = new Mat(imageSize, CvType.CV_8UC4);
                Mat rgba_gray_image = new Mat(imageSize, CvType.CV_8UC4);
                Imgproc.cvtColor(image, gray_image, Imgproc.COLOR_RGB2GRAY);
                Imgproc.cvtColor(gray_image, rgba_gray_image, Imgproc.COLOR_GRAY2BGRA, 4);
                for (Rhombus rhombus : rhombusList)
                    rhombus.draw(rgba_gray_image, ColorTileEnum.YELLOW.cvColor);
                Core.putText(rgba_gray_image, "Num Rhombus: " + rhombusList.size(), new Point(500, 50),
                        Constants.FontFace, 4, ColorTileEnum.RED.cvColor, 4);
                gray_image.release();
                image.release();
                return annotation.drawAnnotation(rgba_gray_image);
            }

            /* **********************************************************************
             * **********************************************************************
             * Face Recognition
             * 
             * Takes a collection of Rhombus objects and determines if a valid
             * Rubik Face can be determined from them, and then also determines 
             * initial color for all nine tiles. 
             */
            rubikFace.processRhombuses(rhombusList, image);
            Log.i(Constants.TAG, "Face Solution = " + rubikFace.faceRecognitionStatus);
            rubikFace.profiler.markTime(Profiler.Event.FACE);
            if (MenuAndParams.imageProcessMode == ImageProcessModeEnum.FACE_DETECT) {
                stateModel.activeRubikFace = rubikFace;
                rubikFace.profiler.markTime(Profiler.Event.TOTAL);
                return annotation.drawAnnotation(image);
            }

            /* **********************************************************************
             * **********************************************************************
             * Cube Pose Estimation
             * 
             * Reconstruct the Rubik Cube 3D location and orientation in GL space coordinates.
             */
            if (rubikFace.faceRecognitionStatus == FaceRecognitionStatusEnum.SOLVED) {

                // Obtain Cube Pose from Face Grid information.
                stateModel.cubePose = CubePoseEstimator.poseEstimation(rubikFace, image, stateModel);

                // Process measurement update on Kalman Filter (if it exists).
                KalmanFilter kalmanFilter = stateModel.kalmanFilter;
                if (kalmanFilter != null)
                    kalmanFilter.measurementUpdate(stateModel.cubePose, System.currentTimeMillis());

                // Process measurement update on Kalman Filter ALSM (if it exists).
                KalmanFilterALSM kalmanFilterALSM = stateModel.kalmanFilterALSM;
                if (kalmanFilter != null)
                    kalmanFilterALSM.measurementUpdate(stateModel.cubePose, System.currentTimeMillis());
            } else {
                stateModel.cubePose = null;
            }
            rubikFace.profiler.markTime(Profiler.Event.POSE);

            /* **********************************************************************
             * **********************************************************************
             * Application State Machine
             * 
             * Will provide user instructions.
             * Will determine when we are on-face and off-face
             * Will determine when we are on-new-face
             * Will change state 
             */
            appStateMachine.onFaceEvent(rubikFace);
            rubikFace.profiler.markTime(Profiler.Event.CONTROLLER);
            rubikFace.profiler.markTime(Profiler.Event.TOTAL);

            // Normal return point.
            stateModel.activeRubikFace = rubikFace;
            return annotation.drawAnnotation(image);

            // =+= Issue: how to get stdio to print as error and not warning in logcat?
        } catch (CvException e) {
            Log.e(Constants.TAG, "CvException: " + e.getMessage());
            e.printStackTrace();
            errorImage = new Mat(imageSize, CvType.CV_8UC4);
            Core.putText(errorImage, "CvException: " + e.getMessage(), new Point(50, 50), Constants.FontFace, 2,
                    ColorTileEnum.WHITE.cvColor, 2);
            int i = 1;
            for (StackTraceElement element : e.getStackTrace())
                Core.putText(errorImage, element.toString(), new Point(50, 50 + 50 * i++), Constants.FontFace, 2,
                        ColorTileEnum.WHITE.cvColor, 2);
        } catch (Exception e) {
            Log.e(Constants.TAG, "Exception: " + e.getMessage());
            e.printStackTrace();
            errorImage = new Mat(imageSize, CvType.CV_8UC4);
            Core.putText(errorImage, "Exception: " + e.getMessage(), new Point(50, 50), Constants.FontFace, 2,
                    ColorTileEnum.WHITE.cvColor, 2);
            int i = 1;
            for (StackTraceElement element : e.getStackTrace())
                Core.putText(errorImage, element.toString(), new Point(50, 50 + 50 * i++), Constants.FontFace, 2,
                        ColorTileEnum.WHITE.cvColor, 2);
        } catch (Error e) {
            Log.e(Constants.TAG, "Error: " + e.getMessage());
            e.printStackTrace();
            errorImage = new Mat(imageSize, CvType.CV_8UC4);
            Core.putText(errorImage, "Error: " + e.getMessage(), new Point(50, 50), Constants.FontFace, 2,
                    ColorTileEnum.WHITE.cvColor, 2);
            int i = 1;
            for (StackTraceElement element : e.getStackTrace())
                Core.putText(errorImage, element.toString(), new Point(50, 50 + 50 * i++), Constants.FontFace, 2,
                        ColorTileEnum.WHITE.cvColor, 2);
        }

        return annotation.drawAnnotation(image);
    }

}