diff --git a/samples/android/15-puzzle-framework/.classpath b/samples/android/15-puzzle-framework/.classpath
new file mode 100644
index 000000000..a4763d1ee
--- /dev/null
+++ b/samples/android/15-puzzle-framework/.classpath
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/samples/android/15-puzzle-framework/.project b/samples/android/15-puzzle-framework/.project
new file mode 100644
index 000000000..d9387703f
--- /dev/null
+++ b/samples/android/15-puzzle-framework/.project
@@ -0,0 +1,33 @@
+
+
+ 15-puzzle
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/samples/android/15-puzzle-framework/AndroidManifest.xml b/samples/android/15-puzzle-framework/AndroidManifest.xml
new file mode 100644
index 000000000..83c8e8a6c
--- /dev/null
+++ b/samples/android/15-puzzle-framework/AndroidManifest.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/android/15-puzzle-framework/ic_launcher-web.png b/samples/android/15-puzzle-framework/ic_launcher-web.png
new file mode 100644
index 000000000..c37372acb
Binary files /dev/null and b/samples/android/15-puzzle-framework/ic_launcher-web.png differ
diff --git a/samples/android/15-puzzle-framework/libs/android-support-v4.jar b/samples/android/15-puzzle-framework/libs/android-support-v4.jar
new file mode 100644
index 000000000..018c1272b
Binary files /dev/null and b/samples/android/15-puzzle-framework/libs/android-support-v4.jar differ
diff --git a/samples/android/15-puzzle-framework/res/drawable-hdpi/ic_action_search.png b/samples/android/15-puzzle-framework/res/drawable-hdpi/ic_action_search.png
new file mode 100644
index 000000000..67de12dec
Binary files /dev/null and b/samples/android/15-puzzle-framework/res/drawable-hdpi/ic_action_search.png differ
diff --git a/samples/android/15-puzzle-framework/res/drawable-hdpi/ic_launcher.png b/samples/android/15-puzzle-framework/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a301d5795
Binary files /dev/null and b/samples/android/15-puzzle-framework/res/drawable-hdpi/ic_launcher.png differ
diff --git a/samples/android/15-puzzle-framework/res/drawable-ldpi/ic_launcher.png b/samples/android/15-puzzle-framework/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 000000000..2c2a58b2f
Binary files /dev/null and b/samples/android/15-puzzle-framework/res/drawable-ldpi/ic_launcher.png differ
diff --git a/samples/android/15-puzzle-framework/res/drawable-mdpi/ic_action_search.png b/samples/android/15-puzzle-framework/res/drawable-mdpi/ic_action_search.png
new file mode 100644
index 000000000..134d5490b
Binary files /dev/null and b/samples/android/15-puzzle-framework/res/drawable-mdpi/ic_action_search.png differ
diff --git a/samples/android/15-puzzle-framework/res/drawable-mdpi/ic_launcher.png b/samples/android/15-puzzle-framework/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 000000000..f91f736fe
Binary files /dev/null and b/samples/android/15-puzzle-framework/res/drawable-mdpi/ic_launcher.png differ
diff --git a/samples/android/15-puzzle-framework/res/drawable-xhdpi/ic_action_search.png b/samples/android/15-puzzle-framework/res/drawable-xhdpi/ic_action_search.png
new file mode 100644
index 000000000..d699c6b37
Binary files /dev/null and b/samples/android/15-puzzle-framework/res/drawable-xhdpi/ic_action_search.png differ
diff --git a/samples/android/15-puzzle-framework/res/drawable-xhdpi/ic_launcher.png b/samples/android/15-puzzle-framework/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..96095ec84
Binary files /dev/null and b/samples/android/15-puzzle-framework/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/samples/android/15-puzzle-framework/res/layout/activity_puzzle15.xml b/samples/android/15-puzzle-framework/res/layout/activity_puzzle15.xml
new file mode 100644
index 000000000..364acf0e2
--- /dev/null
+++ b/samples/android/15-puzzle-framework/res/layout/activity_puzzle15.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/samples/android/15-puzzle-framework/res/menu/activity_puzzle15.xml b/samples/android/15-puzzle-framework/res/menu/activity_puzzle15.xml
new file mode 100644
index 000000000..7810d8196
--- /dev/null
+++ b/samples/android/15-puzzle-framework/res/menu/activity_puzzle15.xml
@@ -0,0 +1,6 @@
+
diff --git a/samples/android/15-puzzle-framework/res/values/strings.xml b/samples/android/15-puzzle-framework/res/values/strings.xml
new file mode 100644
index 000000000..3834e2117
--- /dev/null
+++ b/samples/android/15-puzzle-framework/res/values/strings.xml
@@ -0,0 +1,10 @@
+
+
+ 15-puzzle
+ Hello world!
+ Settings
+ Puzzle15Activity
+ Show/hide tile numbers
+ Start new game
+
+
\ No newline at end of file
diff --git a/samples/android/15-puzzle-framework/res/values/styles.xml b/samples/android/15-puzzle-framework/res/values/styles.xml
new file mode 100644
index 000000000..4dba0d0a4
--- /dev/null
+++ b/samples/android/15-puzzle-framework/res/values/styles.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/android/15-puzzle-framework/src/org/opencv/framework/OpenCvCameraBridgeViewBase.java b/samples/android/15-puzzle-framework/src/org/opencv/framework/OpenCvCameraBridgeViewBase.java
new file mode 100644
index 000000000..9ff6d2b36
--- /dev/null
+++ b/samples/android/15-puzzle-framework/src/org/opencv/framework/OpenCvCameraBridgeViewBase.java
@@ -0,0 +1,320 @@
+package org.opencv.framework;
+
+import java.util.List;
+
+import org.opencv.android.Utils;
+import org.opencv.core.Mat;
+import org.opencv.core.Size;
+import org.opencv.highgui.Highgui;
+import org.opencv.highgui.VideoCapture;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+/**
+ * This is a basic class, implementing the interaction with Camera and OpenCV library.
+ * The main responsibility of it - is to control when camera can be enabled, process the frame,
+ * call external listener to make any adjustments to the frame and then draw the resulting
+ * frame to the screen.
+ * The clients shall implement CvCameraViewListener
+ * TODO: add method to control the format in which the frames will be delivered to CvCameraViewListener
+ */
+public abstract class OpenCvCameraBridgeViewBase extends SurfaceView implements SurfaceHolder.Callback {
+
+ private static final int MAX_UNSPECIFIED = -1;
+
+ protected int mFrameWidth;
+ protected int mFrameHeight;
+
+ protected int mMaxHeight;
+ protected int mMaxWidth;
+
+ private Bitmap mCacheBitmap;
+
+
+ public OpenCvCameraBridgeViewBase(Context context, AttributeSet attrs) {
+ super(context,attrs);
+ getHolder().addCallback(this);
+ mMaxWidth = MAX_UNSPECIFIED;
+ mMaxHeight = MAX_UNSPECIFIED;
+
+ }
+
+ public interface CvCameraViewListener {
+ /**
+ * This method is invoked when camera preview has started. After this method is invoked
+ * the frames will start to be delivered to client via the onCameraFrame() callback.
+ * @param width - the width of the frames that will be delivered
+ * @param height - the height of the frames that will be delivered
+ */
+ public void onCameraViewStarted(int width, int height);
+
+ /**
+ * This method is invoked when camera preview has been stopped for some reason.
+ * No frames will be delivered via onCameraFrame() callback after this method is called.
+ */
+ public void onCameraViewStopped();
+
+ /**
+ * This method is invoked when delivery of the frame needs to be done.
+ * The returned values - is a modified frame which needs to be displayed on the screen.
+ * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc)
+ */
+ public Mat onCameraFrame(Mat inputFrame);
+
+ }
+
+ public class FrameSize {
+ public int width;
+ public int height;
+
+ public FrameSize(int w, int h) {
+ width = w;
+ height = h;
+ }
+ }
+
+
+ private static final int STOPPED = 0;
+ private static final int STARTED = 1;
+ private static final String TAG = "SampleCvBase";
+
+ private CvCameraViewListener mListener;
+ private int mState = STOPPED;
+
+ private boolean mEnabled;
+ private boolean mSurfaceExist;
+
+
+
+ private Object mSyncObject = new Object();
+
+ public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
+ synchronized(mSyncObject) {
+ if (!mSurfaceExist) {
+ mSurfaceExist = true;
+ checkCurrentState();
+ } else {
+ /** Surface changed. We need to stop camera and restart with new parameters */
+ /* Pretend that old surface has been destroyed */
+ mSurfaceExist = false;
+ checkCurrentState();
+ /* Now use new surface. Say we have it now */
+ mSurfaceExist = true;
+ checkCurrentState();
+ }
+ }
+ }
+
+ public void surfaceCreated(SurfaceHolder holder) {
+ /* Do nothing. Wait until surfaceChanged delivered */
+ }
+
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ synchronized(mSyncObject) {
+ mSurfaceExist = false;
+ checkCurrentState();
+ }
+ }
+
+
+ /**
+ * This method is provided for clients, so they can enable the camera connection.
+ * The actuall onCameraViewStarted callback will be delivered only after both this method is called and surface is available
+ */
+ public void enableView() {
+ synchronized(mSyncObject) {
+ mEnabled = true;
+ checkCurrentState();
+ }
+ }
+
+ /**
+ * This method is provided for clients, so they can disable camera connection and stop
+ * the delivery of frames eventhough the surfaceview itself is not destroyed and still stays on the scren
+ */
+ public void disableView() {
+ synchronized(mSyncObject) {
+ mEnabled = false;
+ checkCurrentState();
+ }
+ }
+
+
+ public void setCvCameraViewListener(CvCameraViewListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * This method sets the maximum size that camera frame is allowed to be. When selecting
+ * size - the biggest size which less or equal the size set will be selected.
+ * As an example - we set setMaxFrameSize(200,200) and we have 176x152 and 320x240 sizes. The
+ * preview frame will be selected with 176x152 size.
+ * This method is usefull when need to restrict the size of preview frame for some reason (for example for video recording)
+ * @param maxWidth - the maximum width allowed for camera frame.
+ * @param maxHeight - the maxumum height allowed for camera frame
+ */
+ public void setMaxFrameSize(int maxWidth, int maxHeight) {
+ mMaxWidth = maxWidth;
+ mMaxHeight = maxHeight;
+ }
+
+ /**
+ * Called when mSyncObject lock is held
+ */
+ private void checkCurrentState() {
+ int targetState;
+
+ if (mEnabled && mSurfaceExist) {
+ targetState = STARTED;
+ } else {
+ targetState = STOPPED;
+ }
+
+ if (targetState != mState) {
+ /* The state change detected. Need to exit the current state and enter target state */
+ processExitState(mState);
+ mState = targetState;
+ processEnterState(mState);
+ }
+ }
+
+ private void processEnterState(int state) {
+ switch(state) {
+ case STARTED:
+ onEnterStartedState();
+ if (mListener != null) {
+ mListener.onCameraViewStarted(mFrameWidth, mFrameHeight);
+ }
+ break;
+ case STOPPED:
+ onEnterStoppedState();
+ if (mListener != null) {
+ mListener.onCameraViewStopped();
+ }
+ break;
+ };
+ }
+
+
+ private void processExitState(int state) {
+ switch(state) {
+ case STARTED:
+ onExitStartedState();
+ break;
+ case STOPPED:
+ onExitStoppedState();
+ break;
+ };
+ }
+
+ private void onEnterStoppedState() {
+ /* nothing to do */
+ }
+
+ private void onExitStoppedState() {
+ /* nothing to do */
+ }
+
+ private void onEnterStartedState() {
+
+ connectCamera(getWidth(), getHeight());
+ /* Now create cahe Bitmap */
+ mCacheBitmap = Bitmap.createBitmap(mFrameWidth, mFrameHeight, Bitmap.Config.ARGB_8888);
+
+ }
+
+ private void onExitStartedState() {
+
+ disconnectCamera();
+ if (mCacheBitmap != null) {
+ mCacheBitmap.recycle();
+ }
+ }
+
+
+ /**
+ * This method shall be called by the subclasses when they have valid
+ * object and want it to be delivered to external client (via callback) and
+ * then displayed on the screen.
+ * @param frame - the current frame to be delivered
+ */
+ protected void deliverAndDrawFrame(Mat frame) {
+ Mat modified;
+
+ if (mListener != null) {
+ modified = mListener.onCameraFrame(frame);
+ } else {
+ modified = frame;
+ }
+
+ if (modified != null) {
+ Utils.matToBitmap(modified, mCacheBitmap);
+ }
+
+ if (mCacheBitmap != null) {
+ Canvas canvas = getHolder().lockCanvas();
+ if (canvas != null) {
+ canvas.drawBitmap(mCacheBitmap, (canvas.getWidth() - mCacheBitmap.getWidth()) / 2, (canvas.getHeight() - mCacheBitmap.getHeight()) / 2, null);
+ getHolder().unlockCanvasAndPost(canvas);
+ }
+ }
+ }
+
+ /**
+ * This method is invoked shall perform concrete operation to initialize the camera.
+ * CONTRACT: as a result of this method variables mFrameWidth and mFrameHeight MUST be
+ * initialized with the size of the Camera frames that will be delivered to external processor.
+ * @param width - the width of this SurfaceView
+ * @param height - the height of this SurfaceView
+ */
+ protected abstract void connectCamera(int width, int height);
+
+ /**
+ * Disconnects and release the particular camera object beeing connected to this surface view.
+ * Called when syncObject lock is held
+ */
+ protected abstract void disconnectCamera();
+
+
+ public interface ListItemAccessor {
+ public int getWidth(Object obj);
+ public int getHeight(Object obj);
+ };
+
+
+ /**
+ * This helper method can be called by subclasses to select camera preview size.
+ * It goes over the list of the supported preview sizes and selects the maximum one which
+ * fits both values set via setMaxFrameSize() and surface frame allocated for this view
+ * @param supportedSizes
+ * @param surfaceWidth
+ * @param surfaceHeight
+ * @return
+ */
+ protected FrameSize calculateCameraFrameSize(List> supportedSizes, ListItemAccessor accessor, int surfaceWidth, int surfaceHeight) {
+ int calcWidth = 0;
+ int calcHeight = 0;
+
+ int maxAllowedWidth = (mMaxWidth != MAX_UNSPECIFIED && mMaxWidth < surfaceWidth)? mMaxWidth : surfaceWidth;
+ int maxAllowedHeight = (mMaxHeight != MAX_UNSPECIFIED && mMaxHeight < surfaceHeight)? mMaxHeight : surfaceHeight;
+
+ for (Object size : supportedSizes) {
+ int width = accessor.getWidth(size);
+ int height = accessor.getHeight(size);
+
+ if (width <= maxAllowedWidth && height <= maxAllowedHeight) {
+ if (width >= calcWidth && height >= calcHeight) {
+ calcWidth = (int) width;
+ calcHeight = (int) height;
+ }
+ }
+ }
+ return new FrameSize(calcWidth, calcHeight);
+ }
+}
diff --git a/samples/android/15-puzzle-framework/src/org/opencv/framework/OpenCvJavaCameraView.java b/samples/android/15-puzzle-framework/src/org/opencv/framework/OpenCvJavaCameraView.java
new file mode 100644
index 000000000..c01df669e
--- /dev/null
+++ b/samples/android/15-puzzle-framework/src/org/opencv/framework/OpenCvJavaCameraView.java
@@ -0,0 +1,120 @@
+package org.opencv.framework;
+
+import java.io.IOException;
+import java.util.List;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.hardware.Camera.PreviewCallback;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import org.opencv.core.CvType;
+import org.opencv.core.Mat;
+import org.opencv.core.MatOfByte;
+import org.opencv.imgproc.Imgproc;
+
+/**
+ * This class is an implementation of the Bridge View between OpenCv and JAVA Camera.
+ * This class relays on the functionality available in base class and only implements
+ * required functions:
+ * connectCamera - opens Java camera and sets the PreviewCallback to be delivered.
+ * disconnectCamera - closes the camera and stops preview.
+ * When frame is delivered via callback from Camera - it processed via OpenCV to be
+ * converted to RGBA32 and then passed to the external callback for modifications if required.
+ */
+public class OpenCvJavaCameraView extends OpenCvCameraBridgeViewBase implements PreviewCallback {
+
+ private static final int MAGIC_TEXTURE_ID = 10;
+ private static final String TAG = "OpenCvJavaCameraView";
+
+ private Mat mBaseMat;
+
+ public static class JavaCameraSizeAccessor implements ListItemAccessor {
+
+ @Override
+ public int getWidth(Object obj) {
+ Camera.Size size = (Camera.Size) obj;
+ return size.width;
+ }
+
+ @Override
+ public int getHeight(Object obj) {
+ Camera.Size size = (Camera.Size) obj;
+ return size.height;
+ }
+
+ }
+
+ private Camera mCamera;
+
+ public OpenCvJavaCameraView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+
+
+ @Override
+ protected void connectCamera(int width, int height) {
+ mCamera = Camera.open(0);
+
+ List sizes = mCamera.getParameters().getSupportedPreviewSizes();
+ /* Select the size that fits surface considering maximum size allowed */
+ FrameSize frameSize = calculateCameraFrameSize(sizes, new JavaCameraSizeAccessor(), width, height);
+
+ /* Now set camera parameters */
+ try {
+ Camera.Parameters params = mCamera.getParameters();
+
+ List formats = params.getSupportedPictureFormats();
+
+ params.setPreviewFormat(ImageFormat.NV21);
+ params.setPreviewSize(frameSize.width, frameSize.height);
+
+ mCamera.setPreviewCallback(this);
+ mCamera.setParameters(params);
+ //mCamera.setPreviewTexture(new SurfaceTexture(MAGIC_TEXTURE_ID));
+
+ SurfaceTexture tex = new SurfaceTexture(MAGIC_TEXTURE_ID);
+
+ mCamera.setPreviewTexture(tex);
+
+ mFrameWidth = frameSize.width;
+ mFrameHeight = frameSize.height;
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ mBaseMat = new Mat(mFrameHeight + (mFrameHeight/2), mFrameWidth, CvType.CV_8UC1);
+
+ /* Finally we are ready to start the preview */
+ mCamera.startPreview();
+ }
+
+ @Override
+ protected void disconnectCamera() {
+
+ mCamera.setPreviewCallback(null);
+ mCamera.stopPreview();
+ mCamera.release();
+ }
+
+
+
+ @Override
+ public void onPreviewFrame(byte[] frame, Camera arg1) {
+ Log.i(TAG, "Preview Frame received. Need to create MAT and deliver it to clients");
+
+ Log.i(TAG, "Frame size is " + frame.length);
+
+ mBaseMat.put(0, 0, frame);
+ Mat frameMat = new Mat();
+ Imgproc.cvtColor(mBaseMat, frameMat, Imgproc.COLOR_YUV2RGBA_NV21, 4);
+ deliverAndDrawFrame(frameMat);
+ frameMat.release();
+ }
+
+}
diff --git a/samples/android/15-puzzle-framework/src/org/opencv/framework/OpenCvNativeCameraView.java b/samples/android/15-puzzle-framework/src/org/opencv/framework/OpenCvNativeCameraView.java
new file mode 100644
index 000000000..767bfaf5b
--- /dev/null
+++ b/samples/android/15-puzzle-framework/src/org/opencv/framework/OpenCvNativeCameraView.java
@@ -0,0 +1,142 @@
+package org.opencv.framework;
+
+import org.opencv.android.Utils;
+import org.opencv.core.Mat;
+import org.opencv.core.Size;
+import org.opencv.highgui.Highgui;
+import org.opencv.highgui.VideoCapture;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.util.Log;
+
+/**
+ * This class is an implementation of a bridge between SurfaceView and native OpenCV camera.
+ * Due to the big amount of work done, by the base class this child is only responsible
+ * for creating camera, destroying camera and delivering frames while camera is enabled
+ */
+public class OpenCvNativeCameraView extends OpenCvCameraBridgeViewBase {
+
+ public static final String TAG = "OpenCvNativeCameraView";
+ private boolean mStopThread;
+ private Thread mThread;
+ private VideoCapture mCamera;
+
+
+ public OpenCvNativeCameraView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+
+ @Override
+ protected void connectCamera(int width, int height) {
+
+ /* 1. We need to instantiate camera
+ * 2. We need to start thread which will be getting frames
+ */
+ /* First step - initialize camera connection */
+ initializeCamera(getWidth(), getHeight());
+
+ /* now we can start update thread */
+ mThread = new Thread(new CameraWorker(getWidth(), getHeight()));
+ mThread.start();
+ }
+
+ @Override
+ protected void disconnectCamera() {
+ /* 1. We need to stop thread which updating the frames
+ * 2. Stop camera and release it
+ */
+ try {
+ mStopThread = true;
+ mThread.join();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ mThread = null;
+ mStopThread = false;
+ }
+
+ /* Now release camera */
+ releaseCamera();
+
+ }
+
+ public static class OpenCvSizeAccessor implements ListItemAccessor {
+
+ @Override
+ public int getWidth(Object obj) {
+ Size size = (Size)obj;
+ return (int)size.width;
+ }
+
+ @Override
+ public int getHeight(Object obj) {
+ Size size = (Size)obj;
+ return (int)size.height;
+ }
+
+ }
+
+ private void initializeCamera(int width, int height) {
+ mCamera = new VideoCapture(Highgui.CV_CAP_ANDROID);
+ //TODO: improve error handling
+
+ java.util.List sizes = mCamera.getSupportedPreviewSizes();
+
+ /* Select the size that fits surface considering maximum size allowed */
+ FrameSize frameSize = calculateCameraFrameSize(sizes, new OpenCvSizeAccessor(), width, height);
+
+
+ double frameWidth = frameSize.width;
+ double frameHeight = frameSize.height;
+
+
+ mCamera.set(Highgui.CV_CAP_PROP_FRAME_WIDTH, frameWidth);
+ mCamera.set(Highgui.CV_CAP_PROP_FRAME_HEIGHT, frameHeight);
+
+ mFrameWidth = (int)frameWidth;
+ mFrameHeight = (int)frameHeight;
+
+ Log.i(TAG, "Selected camera frame size = (" + mFrameWidth + ", " + mFrameHeight + ")");
+ }
+
+ private void releaseCamera() {
+ if (mCamera != null) {
+ mCamera.release();
+ }
+ }
+
+ private class CameraWorker implements Runnable {
+
+ private Mat mRgba = new Mat();
+ private int mWidth;
+ private int mHeight;
+
+ CameraWorker(int w, int h) {
+ mWidth = w;
+ mHeight = h;
+ }
+
+ @Override
+ public void run() {
+ Mat modified;
+
+
+ do {
+ if (!mCamera.grab()) {
+ Log.e(TAG, "Camera frame grab failed");
+ break;
+ }
+ mCamera.retrieve(mRgba, Highgui.CV_CAP_ANDROID_COLOR_FRAME_RGBA);
+
+ deliverAndDrawFrame(mRgba);
+
+ } while (!mStopThread);
+
+ }
+ }
+
+}
diff --git a/samples/android/15-puzzle-framework/src/org/opencv/samples/puzzle15/Puzzle15Activity.java b/samples/android/15-puzzle-framework/src/org/opencv/samples/puzzle15/Puzzle15Activity.java
new file mode 100644
index 000000000..632eecb8b
--- /dev/null
+++ b/samples/android/15-puzzle-framework/src/org/opencv/samples/puzzle15/Puzzle15Activity.java
@@ -0,0 +1,127 @@
+package org.opencv.samples.puzzle15;
+
+import org.opencv.android.BaseLoaderCallback;
+import org.opencv.android.LoaderCallbackInterface;
+import org.opencv.android.OpenCVLoader;
+import org.opencv.core.Mat;
+import org.opencv.framework.OpenCvCameraBridgeViewBase.CvCameraViewListener;
+import org.opencv.framework.OpenCvNativeCameraView;
+
+import android.os.Bundle;
+import android.app.Activity;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.Window;
+import android.view.View;
+
+public class Puzzle15Activity extends Activity implements CvCameraViewListener, View.OnTouchListener {
+
+ private static final String TAG = "Sample::Puzzle15::Activity";
+
+ private OpenCvNativeCameraView mOpenCvCameraView;
+ private Puzzle15Processor mPuzzle15;
+
+ private int mGameWidth;
+ private int mGameHeight;
+
+
+ private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
+ @Override
+ public void onManagerConnected(int status) {
+ switch (status) {
+ case LoaderCallbackInterface.SUCCESS:
+ {
+ Log.i(TAG, "OpenCV loaded successfully");
+
+ mPuzzle15 = new Puzzle15Processor();
+ mPuzzle15.prepareNewGame();
+ /* Now enable camera view to start receiving frames */
+ mOpenCvCameraView.setOnTouchListener(Puzzle15Activity.this);
+ mOpenCvCameraView.enableView();
+ } break;
+ default:
+ {
+ super.onManagerConnected(status);
+ } break;
+ }
+ }
+ };
+
+
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ setContentView(R.layout.activity_puzzle15);
+
+ mOpenCvCameraView = (OpenCvNativeCameraView) findViewById(R.id.puzzle_activity_surface_view);
+ mOpenCvCameraView.setCvCameraViewListener(this);
+ OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_2, this, mLoaderCallback);
+ }
+
+ public void onDestroy() {
+ super.onDestroy();
+ mOpenCvCameraView.disableView();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.activity_puzzle15, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ Log.i(TAG, "Menu Item selected " + item);
+ if (item.getItemId() == R.id.menu_start_new_game) {
+ /* We need to start new game */
+ mPuzzle15.prepareNewGame();
+ } else if (item.getItemId() == R.id.menu_toggle_tile_numbers) {
+ /* We need to enable or disable drawing of the tile numbers */
+ mPuzzle15.toggleTileNumbers();
+ }
+ return true;
+ }
+
+
+ @Override
+ public void onCameraViewStarted(int width, int height) {
+ mGameWidth = width;
+ mGameHeight = height;
+ mPuzzle15.prepareGameSize(width, height);
+ }
+
+ @Override
+ public void onCameraViewStopped() {
+ }
+
+ @Override
+ public Mat onCameraFrame(Mat inputFrame) {
+ return mPuzzle15.puzzleFrame(inputFrame);
+ }
+
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ int xpos, ypos;
+
+ xpos = (view.getWidth() - mGameWidth) / 2;
+ xpos = (int)event.getX() - xpos;
+
+ ypos = (view.getHeight() - mGameHeight) / 2;
+ ypos = (int)event.getY() - ypos;
+
+ if (xpos >=0 && xpos <= mGameWidth && ypos >=0 && ypos <= mGameHeight) {
+ /* click is inside the picture. Deliver this event to processor */
+ mPuzzle15.deliverTouchEvent(xpos, ypos);
+ }
+
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+}
diff --git a/samples/android/15-puzzle-framework/src/org/opencv/samples/puzzle15/Puzzle15Processor.java b/samples/android/15-puzzle-framework/src/org/opencv/samples/puzzle15/Puzzle15Processor.java
new file mode 100644
index 000000000..b8ef702ab
--- /dev/null
+++ b/samples/android/15-puzzle-framework/src/org/opencv/samples/puzzle15/Puzzle15Processor.java
@@ -0,0 +1,203 @@
+package org.opencv.samples.puzzle15;
+
+import org.opencv.core.Core;
+import org.opencv.core.CvType;
+import org.opencv.core.Mat;
+import org.opencv.core.Scalar;
+import org.opencv.core.Size;
+import org.opencv.core.Point;
+
+import android.util.Log;
+
+
+/**
+ * This class is a controller for puzzle game.
+ * It converts the image from Camera into the shuffled image
+ */
+public class Puzzle15Processor {
+
+ private static final int GRID_SIZE = 4;
+ private static final int GRID_AREA = GRID_SIZE * GRID_SIZE;
+ private static final int GRID_EMPTY_INDEX = GRID_AREA - 1;
+ private static final String TAG = "Puzzle15Processor";
+
+ private int[] mIndexes;
+ private int[] mTextWidths;
+ private int[] mTextHeights;
+
+ private Mat mRgba;
+ private Mat mRgba15;
+
+ private Mat[] mCells;
+ private Mat[] mCells15;
+ private boolean mShowTileNumbers = true;
+
+ public Puzzle15Processor() {
+ mTextWidths = new int[GRID_AREA];
+ mTextHeights = new int[GRID_AREA];
+
+ mIndexes = new int [GRID_AREA];
+
+ for (int i = 0; i < GRID_AREA; i++) {
+ Size s = Core.getTextSize(Integer.toString(i + 1), 3/* CV_FONT_HERSHEY_COMPLEX */, 1, 2, null);
+ mTextHeights[i] = (int) s.height;
+ mTextWidths[i] = (int) s.width;
+
+ mIndexes[i] = i;
+ }
+ }
+
+ /* this method is intended to make processor prepared for a new game */
+ public synchronized void prepareNewGame() {
+ do {
+ shuffle(mIndexes);
+ } while (!isPuzzleSolvable());
+ }
+
+ /* This method is to make the processor know the size of the frames that
+ * will be delivered via puzzleFrame.
+ * If the frames will be different size - then the result is unpredictable
+ */
+ public synchronized void prepareGameSize(int width, int height) {
+ mRgba15 = new Mat(height, width, CvType.CV_8UC4);
+
+ mCells = new Mat[GRID_AREA];
+ mCells15 = new Mat[GRID_AREA];
+
+ for (int i = 0; i < GRID_SIZE; i++) {
+ for (int j = 0; j < GRID_SIZE; j++) {
+ int k = i * GRID_SIZE + j;
+// mCells[k] = mRgba.submat(i * height / GRID_SIZE, (i + 1) * height / GRID_SIZE, j * width / GRID_SIZE, (j + 1) * width / GRID_SIZE);
+ mCells15[k] = mRgba15.submat(i * height / GRID_SIZE, (i + 1) * height / GRID_SIZE, j * width / GRID_SIZE, (j + 1) * width / GRID_SIZE);
+ }
+ }
+
+
+ }
+
+
+ /* this method to be called from the outside. it processes the frame and shuffles
+ * the tiles as specified by mIndexes array
+ */
+ public synchronized Mat puzzleFrame(Mat inputPicture) {
+ int rows = inputPicture.rows();
+ int cols = inputPicture.cols();
+
+ int type = inputPicture.type();
+
+ rows = rows - rows%4;
+ cols = cols - cols%4;
+
+
+ for (int i = 0; i < GRID_SIZE; i++) {
+ for (int j = 0; j < GRID_SIZE; j++) {
+ int k = i * GRID_SIZE + j;
+ mCells[k] = inputPicture.submat(i * inputPicture.rows() / GRID_SIZE, (i + 1) * inputPicture.rows() / GRID_SIZE, j * inputPicture.cols()/ GRID_SIZE, (j + 1) * inputPicture.cols() / GRID_SIZE);
+ }
+ }
+
+ rows = rows - rows%4;
+ cols = cols - cols%4;
+
+ // copy shuffled tiles
+ for (int i = 0; i < GRID_AREA; i++) {
+ int idx = mIndexes[i];
+ if (idx == GRID_EMPTY_INDEX)
+ mCells15[i].setTo(new Scalar(0x33, 0x33, 0x33, 0xFF));
+ else {
+ mCells[idx].copyTo(mCells15[i]);
+ if (mShowTileNumbers) {
+ Core.putText(mCells15[i], Integer.toString(1 + idx), new Point((cols / GRID_SIZE - mTextWidths[idx]) / 2,
+ (rows / GRID_SIZE + mTextHeights[idx]) / 2), 3/* CV_FONT_HERSHEY_COMPLEX */, 1, new Scalar(255, 0, 0, 255), 2);
+ }
+ }
+ }
+
+ drawGrid(cols, rows, mRgba15);
+
+ return mRgba15;
+ }
+
+
+ public void toggleTileNumbers() {
+ mShowTileNumbers = !mShowTileNumbers;
+ }
+
+ public void deliverTouchEvent(int x, int y) {
+ int rows = mRgba15.rows();
+ int cols = mRgba15.cols();
+
+ int row = (int) Math.floor(y * GRID_SIZE / rows);
+ int col = (int) Math.floor(x * GRID_SIZE / cols);
+
+ if (row < 0 || row >= GRID_SIZE || col < 0 || col >= GRID_SIZE) {
+ Log.e(TAG, "It is not expected to get touch event outside of picture");
+ return ;
+ }
+
+ int idx = row * GRID_SIZE + col;
+ int idxtoswap = -1;
+
+ // left
+ if (idxtoswap < 0 && col > 0)
+ if (mIndexes[idx - 1] == GRID_EMPTY_INDEX)
+ idxtoswap = idx - 1;
+ // right
+ if (idxtoswap < 0 && col < GRID_SIZE - 1)
+ if (mIndexes[idx + 1] == GRID_EMPTY_INDEX)
+ idxtoswap = idx + 1;
+ // top
+ if (idxtoswap < 0 && row > 0)
+ if (mIndexes[idx - GRID_SIZE] == GRID_EMPTY_INDEX)
+ idxtoswap = idx - GRID_SIZE;
+ // bottom
+ if (idxtoswap < 0 && row < GRID_SIZE - 1)
+ if (mIndexes[idx + GRID_SIZE] == GRID_EMPTY_INDEX)
+ idxtoswap = idx + GRID_SIZE;
+
+ // swap
+ if (idxtoswap >= 0) {
+ synchronized (this) {
+ int touched = mIndexes[idx];
+ mIndexes[idx] = mIndexes[idxtoswap];
+ mIndexes[idxtoswap] = touched;
+ }
+ }
+ }
+
+ private void drawGrid(int cols, int rows, Mat drawMat) {
+ for (int i = 1; i < GRID_SIZE; i++) {
+ Core.line(drawMat, new Point(0, i * rows / GRID_SIZE), new Point(cols, i * rows / GRID_SIZE), new Scalar(0, 255, 0, 255), 3);
+ Core.line(drawMat, new Point(i * cols / GRID_SIZE, 0), new Point(i * cols / GRID_SIZE, rows), new Scalar(0, 255, 0, 255), 3);
+ }
+ }
+
+
+ private static void shuffle(int[] array) {
+ for (int i = array.length; i > 1; i--) {
+ int temp = array[i - 1];
+ int randIx = (int) (Math.random() * i);
+ array[i - 1] = array[randIx];
+ array[randIx] = temp;
+ }
+ }
+
+ private boolean isPuzzleSolvable() {
+
+ int sum = 0;
+ for (int i = 0; i < GRID_AREA; i++) {
+ if (mIndexes[i] == GRID_EMPTY_INDEX)
+ sum += (i / GRID_SIZE) + 1;
+ else {
+ int smaller = 0;
+ for (int j = i + 1; j < GRID_AREA; j++) {
+ if (mIndexes[j] < mIndexes[i])
+ smaller++;
+ }
+ sum += smaller;
+ }
+ }
+ return sum % 2 == 0;
+ }
+
+}