Showing posts with label android. Show all posts
Showing posts with label android. Show all posts

Monday, June 9, 2014

3D effect without glasses

The following shows how to give the phone or tablet user the sensation of looking at an object in 3D by just using the front facing camera. I believe that this is a similar technique to what the Amazon phone is using to display 3D (see this link, just guessing as phone not out till June 18th). See more background at the end of the post. In the mean time, let's just jump into the technique.

Basically with the phone front facing camera we recognize where the viewer's head is respect to the screen and present the objects on the screen from the viewers perspective. With this I create a 3D effect without glasses (no stereo vision, though, just the angle, which is powerful enough).

The code is relatively simple but unfortunately I do not know how to access some low level stuff, so, work comes into getting things running fast with some hacks... I tried my best but still one can see some lagging... Check out this video. Disclaimer: this is just an experiment. I had only ~20 pictures for the full angle of view, I didn't really adjust the angle of those pics to the angle perceived from the front camera (I eye balled that...) and it was kind of tough to record with the other phone while moving... a camera attached to my head would have been nice for this, but anyhow, gives the idea... :).

The top level structure is:

Load in memory all potential pictures, taken a priori from the potential viewer's perspective, so that they can be presented real time as fast as possible (limited by my knowledge :P)

Remember to place your pictures in the root of the SDCard + "/DCIM/3D" or modify that part of the code.

Notice that we save in memory the encoded pictures. This is a trade-off between storing the full raw data (see my first attempt on this topic here), which would be faster as it wouldn't need real time decoding, but would require much larger memory; and not storing anything, which saves all the memory but it is much slower (read from flash + decode).

Capture the camera image

Capturing the image is something pretty trivial in Android. Nevertheless, in our case we want to capture an image but present something completely unrelated to that image. Somehow, Android doesn't seem to support that in a well documented way. I have a post on that here.

I took the same approach as I did here. The general real time image capture framework is done with OpenCV. From OpenCV tutorial: "Implementation of CvCameraViewListener interface allows you to add processing steps after frame grabbing from camera and before its rendering on screen. The most important function is onCameraFrame. It is callback function and it is called on retrieving frame from camera."

Search for the face

This phase, together with displaying the image, are the ones limiting the rendering speed. To speed it up, ideally I wanted to use the "embedded" method that comes with the phone. I.e., the one that is showing a square around the faces when you are using the camera app that comes with my phone (an HTC One). It seems to be fast and reliable. Unfortunately I do not know how to access it.

The next method down (and the one I used) is the one that comes with the Android SDK (see code below and more details here).

The last method in our tool set is the OpenCV approach. See details here.

Compute the viewer's angle respect to the display. This is pretty straightforward, so, just check the code... Ideally you got to adjust this well to the angle of the pictures you have taken but I didn't really do the effort.

Present that image Based on the angle, pick the right picture from memory to be shown, decode it and present it. As explained on the first phase, this is not trivial to do with minimum lag.

Without more delay, let's go into the code:

_3DActivity.java
 /*  
  * Working demo of face detection (remember to put the camera/phone in horizontal)  
  * using OpenCV as framework, with Android Face recognition.  
  * AS GOOD AS IT GETS. Still not that smooth. Probably will do better when we  
  * present a graph with OpenGL.  
  */  
 package com.cell0907.TDpic;  
 import java.io.File;  
 import java.util.Arrays;  
 import org.opencv.android.BaseLoaderCallback;  
 import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame;  
 import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2;  
 import org.opencv.android.LoaderCallbackInterface;  
 import org.opencv.android.OpenCVLoader;  
 import org.opencv.android.Utils;  
 import org.opencv.core.Core;  
 import org.opencv.core.CvException;  
 import org.opencv.core.CvType;  
 import org.opencv.core.Mat;  
 import org.opencv.core.MatOfByte;  
 import org.opencv.core.MatOfInt;  
 import org.opencv.core.Scalar;  
 import org.opencv.core.Size;  
 import org.opencv.highgui.Highgui;  
 import org.opencv.imgproc.Imgproc;  
 import org.opencv.core.Point;  
 import android.app.Activity;  
 import android.graphics.Bitmap;  
 import android.graphics.BitmapFactory;  
 import android.graphics.PointF;  
 import android.media.FaceDetector;  
 import android.media.FaceDetector.Face;  
 import android.os.Bundle;  
 import android.os.Environment;  
 import android.util.Log;  
 import android.view.Menu;  
 import android.view.MenuItem;  
 import android.view.SurfaceView;  
 import android.view.WindowManager;  
 public class _3DActivity extends Activity implements CvCameraViewListener2 {  
   private static final int         VIEW_MODE_CAMERA  = 0;  
   private static final int         VIEW_MODE_GREY   = 1;  
   private static final int         VIEW_MODE_FACES  = 2;  
   private static final int         VIEW_MODE_3D    = 3;  
   private MenuItem             mItemPreviewRGBA;  
   private MenuItem             mItemPreviewGrey;  
   private MenuItem             mItemPreviewFaces;  
   private MenuItem             mItemPreview3D;  
   private int               mViewMode;  
   private Mat               mRgba;  
   private Mat               mGrey;  
   private int                              screen_w, screen_h;  
   private Tutorial3View            mOpenCvCameraView;   
   //private Bitmap[]      mImageCache; // A place to store our pics       
   private MatOfByte[]     mImageCache; // A place to store our pics in jpg format  
   private int           numberofitems;  
   private int           index;  
   private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {  
     @Override  
     public void onManagerConnected(int status) {  
       switch (status) {  
         case LoaderCallbackInterface.SUCCESS:  
         {  
           // Load native library after(!) OpenCV initialization  
           mOpenCvCameraView.enableView();        
         } break;  
         default:  
         {  
           super.onManagerConnected(status);  
         } break;  
       }  
     }  
   };  
   public _3DActivity() {  
   }  
   /** Called when the activity is first created. */  
   @Override  
   public void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);  
     setContentView(R.layout.tutorial2_surface_view);  
     mOpenCvCameraView = (Tutorial3View) findViewById(R.id.tutorial2_activity_surface_view);  
     mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);  
     mOpenCvCameraView.setCvCameraViewListener(this);  
     index=0;  
   }  
   @Override  
   public void onPause()  
   {  
     super.onPause();  
     if (mOpenCvCameraView != null)  
       mOpenCvCameraView.disableView();  
   }  
   @Override  
   public void onResume()  
   {  
     super.onResume();  
     OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_3, this, mLoaderCallback);  
   }  
   public void onDestroy() {  
     super.onDestroy();  
     if (mOpenCvCameraView != null)  
       mOpenCvCameraView.disableView();  
   }  
   public void onCameraViewStarted(int width, int height) {  
        screen_w=width;  
        screen_h=height;  
     mRgba = new Mat(screen_w, screen_h, CvType.CV_8UC4);  
     mGrey = new Mat(screen_w, screen_h, CvType.CV_8UC1);  
     load_images();  
     Log.v("MyActivity","Height: "+height+" Width: "+width);  
   }  
   public void onCameraViewStopped() {  
     mRgba.release();  
     mGrey.release();  
   }  
   public Mat onCameraFrame(CvCameraViewFrame inputFrame) {  
        long startTime = System.nanoTime();  
        long endTime;  
        boolean show=true;  
        mRgba=inputFrame.rgba();  
        if (mViewMode==VIEW_MODE_CAMERA) {  
             endTime = System.nanoTime();  
          if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
             return mRgba;  
        }  
        if (mViewMode==VIEW_MODE_GREY){             
             Imgproc.cvtColor( mRgba, mGrey, Imgproc.COLOR_BGR2GRAY);   
             endTime = System.nanoTime();  
          if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
             return mGrey;  
        }  
        // REDUCE THE RESOLUTION TO EXPEDITE THINGS  
        Mat low_res = new Mat(screen_w, screen_h, CvType.CV_8UC4);  
        Imgproc.resize(mRgba,low_res,new Size(),0.25,0.25,Imgproc.INTER_LINEAR);  
        Bitmap bmp = null;  
        try {  
          bmp = Bitmap.createBitmap(low_res.width(), low_res.height(), Bitmap.Config.RGB_565);  
          Utils.matToBitmap(low_res, bmp);  
        }  
        catch (CvException e){Log.v("MyActivity",e.getMessage());}  
           int maxNumFaces = 1; // Set this to whatever you want  
           FaceDetector fd = new FaceDetector((int)(screen_w/4),(int)(screen_h/4),  
                  maxNumFaces);  
           Face[] faces = new Face[maxNumFaces];  
           int numFacesFound=0;  
           try {  
                  numFacesFound = fd.findFaces(bmp, faces);  
             } catch (IllegalArgumentException e) {  
                  // From Docs:  
                  // if the Bitmap dimensions don't match the dimensions defined at initialization   
                  // or the given array is not sized equal to the maxFaces value defined at   
                  // initialization  
                  Log.v("MyActivity","Argument dimensions wrong");  
             }  
           if (mViewMode==VIEW_MODE_FACES) {  
                if (numFacesFound<maxNumFaces) maxNumFaces=numFacesFound;  
                for (int i = 0; i < maxNumFaces; ++i) {  
                     Face face = faces[i];  
                     PointF MidPoint = new PointF();  
                     face.getMidPoint(MidPoint);  
                     /* Log.v("MyActivity","Face " + i + " found with " + face.confidence() + " confidence!");  
                       Log.v("MyActivity","Face " + i + " eye distance " + face.eyesDistance());  
                       Log.v("MyActivity","Face " + i + " midpoint (between eyes) " + MidPoint);*/  
                     Point center= new Point(4*MidPoint.x, 4*MidPoint.y);  
                     Core.ellipse( mRgba, new Point(center.x,center.y), new Size(8*face.eyesDistance(), 8*face.eyesDistance()), 0, 0, 360, new Scalar( 255, 0, 255 ), 4, 8, 0 );  
                }  
                endTime = System.nanoTime();  
                if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
                return mRgba;  
                //return low_res;  
        }          
           // 3D  
           if (numFacesFound>0){  
                Face face = faces[0];  
                PointF MidPoint = new PointF();  
                face.getMidPoint(MidPoint);  
                int face_x=4*(int)MidPoint.x;  
                // The face can show up from x0=k.screen_w to x1=(1-k)screen_w  
                // index=A.face_x+B  
                // 0 = A.k.screen_w + B  
                // N = A.(1-k).screen_w + B where N=numberofitems-1  
                // Therefore:  
                // A=N/((1-2k).screen_w)  
                // B=-A.k.screen_w=-N.k/(1-2k)  
                int N=numberofitems-1;  
                double k=0.1;  
                double A=N/((1-2*k)*screen_w);  
                double B=-N*k/(1-2*k);  
                index=(int)Math.floor(A*face_x+B);                 
                index=numberofitems-index-1;  
                //Log.v("MyActivity","x: "+face_x+" index: "+index);  
                if (index<0) index=0;  
                if (index>numberofitems-1) index=numberofitems-1;  
           }  
           //mImageCache[index] is a array of bytes containing the jpg  
           mRgba=Highgui.imdecode(mImageCache[index],Highgui.CV_LOAD_IMAGE_COLOR);  
           endTime = System.nanoTime();  
           if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
           //Log.v("MyActivity","Index: "+index);  
           return mRgba;  
    }  
   @Override  
   public boolean onCreateOptionsMenu(Menu menu) {  
     mItemPreviewRGBA = menu.add("RGBA");  
     mItemPreviewGrey = menu.add("Grey");  
     mItemPreviewFaces = menu.add("Faces");  
     mItemPreview3D = menu.add("3D");  
     return true;  
   }  
   public boolean onOptionsItemSelected(MenuItem item) {  
     if (item == mItemPreviewRGBA) {  
       mViewMode = VIEW_MODE_CAMERA;  
     } else if (item == mItemPreviewGrey) {  
       mViewMode = VIEW_MODE_GREY;  
     } else if (item == mItemPreviewFaces) {  
       mViewMode = VIEW_MODE_FACES;  
     } else if (item == mItemPreview3D) {  
       mViewMode = VIEW_MODE_3D;  
     }  
     return true;  
   }    
   //LOAD IMAGES  
   void load_images(){  
     //android.hardware.Camera.Size r=mOpenCvCameraView.getResolution();  
        String root = Environment.getExternalStorageDirectory().toString();  
           File myDir = new File(root + "/DCIM/3D");   
        File[] file_list = myDir.listFiles();   
        Arrays.sort(file_list);          // Otherwise file order is unpredictable  
        numberofitems=file_list.length;  
        //mImageCache=new Bitmap[numberofitems];  
        mImageCache=new MatOfByte[numberofitems];  
        Mat temp3 = new Mat(screen_w, screen_h, CvType.CV_8UC4);  
        MatOfInt compression_params=new MatOfInt(Highgui.CV_IMWRITE_JPEG_QUALITY,50);  
        Log.v("MyActivity","NOI: "+numberofitems);  
        for (int i=0;i<numberofitems;i++){  
             try{  
                  mImageCache[i]=new MatOfByte();  
                  Log.v("MyActivity","i: "+i);  
                  Bitmap temp1=BitmapFactory.decodeFile(file_list[i].getPath());  
                  Bitmap temp2=Bitmap.createScaledBitmap(temp1, screen_w , screen_h, true);  
                  Utils.bitmapToMat(temp2,temp3);  
                  //Log.v("MyActivity","w: "+temp3.width()+" l: "+temp3.height());  
                  Highgui.imencode(".jpg", temp3, mImageCache[i],compression_params);  
                  Log.v("MyActivity","Length: "+mImageCache[i].total());  
             } catch (Exception e) {  
                e.printStackTrace();  
                   Log.v("MyActivity", "L: Error loading");  
              }  
        }  
   }  
 }  

Tutorial3View.java
 package com.cell0907.TDpic;  
 import java.io.FileOutputStream;  
 import java.util.List;  
 import org.opencv.android.JavaCameraView;  
 import android.content.Context;  
 import android.hardware.Camera;  
 import android.hardware.Camera.PictureCallback;  
 import android.hardware.Camera.Size;  
 import android.util.AttributeSet;  
 import android.util.Log;  
 public class Tutorial3View extends JavaCameraView implements PictureCallback {  
   private static final String TAG = "MyActivity";  
   private String mPictureFileName;  
   public Tutorial3View(Context context, AttributeSet attrs) {  
     super(context, attrs);  
   }  
   public List<String> getEffectList() {  
     return mCamera.getParameters().getSupportedColorEffects();  
   }  
   public boolean isEffectSupported() {  
     return (mCamera.getParameters().getColorEffect() != null);  
   }  
   public String getEffect() {  
     return mCamera.getParameters().getColorEffect();  
   }  
   public void setEffect(String effect) {  
     Camera.Parameters params = mCamera.getParameters();  
     params.setColorEffect(effect);  
     mCamera.setParameters(params);  
   }  
   public List<Size> getResolutionList() {  
     return mCamera.getParameters().getSupportedPreviewSizes();  
   }  
   public void setResolution(Size resolution) {  
     disconnectCamera();  
     mMaxHeight = resolution.height;  
     mMaxWidth = resolution.width;  
     connectCamera(getWidth(), getHeight());  
   }  
   public Size getResolution() {  
     return mCamera.getParameters().getPreviewSize();  
   }  
   public void takePicture(final String fileName) {  
     Log.i(TAG, "Taking picture");  
     this.mPictureFileName = fileName;  
     // Postview and jpeg are sent in the same buffers if the queue is not empty when performing a capture.  
     // Clear up buffers to avoid mCamera.takePicture to be stuck because of a memory issue  
     mCamera.setPreviewCallback(null);  
     // PictureCallback is implemented by the current class  
     mCamera.takePicture(null, null, this);  
   }  
   @Override  
   public void onPictureTaken(byte[] data, Camera camera) {  
     Log.i(TAG, "Saving a bitmap to file");  
     // The camera preview was automatically stopped. Start it again.  
     mCamera.startPreview();  
     mCamera.setPreviewCallback(this);  
     // Write the image in a file (in jpeg format)  
     try {  
       FileOutputStream fos = new FileOutputStream(mPictureFileName);  
       fos.write(data);  
       fos.close();  
     } catch (java.io.IOException e) {  
       Log.e("PictureDemo", "Exception in photoCallback", e);  
     }  
   }  
 }  

AndroidManifest.xml
 <?xml version="1.0" encoding="utf-8"?>  
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
      package="com.cell0907.TDpic"  
      android:versionCode="21"  
      android:versionName="2.1">  
       <supports-screens android:resizeable="true"  
            android:smallScreens="true"  
            android:normalScreens="true"  
            android:largeScreens="true"  
            android:anyDensity="true" />  
   <uses-sdk android:minSdkVersion="8"   
               android:targetSdkVersion="10" />  
   <uses-permission android:name="android.permission.CAMERA"/>  
   <uses-feature android:name="android.hardware.camera" android:required="false"/>  
   <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>  
   <uses-feature android:name="android.hardware.camera.front" android:required="false"/>  
   <uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false"/>  
   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>  
   <application  
     android:label="@string/app_name"  
     android:icon="@drawable/icon"  
     android:theme="@android:style/Theme.NoTitleBar.Fullscreen"  
     android:allowBackup="false">  
     <activity android:name="_3DActivity"  
          android:label="@string/app_name"  
          android:screenOrientation="landscape"  
          android:configChanges="keyboardHidden|orientation">  
       <intent-filter>  
         <action android:name="android.intent.action.MAIN" />  
         <category android:name="android.intent.category.LAUNCHER" />  
       </intent-filter>  
     </activity>  
   </application>  
 </manifest>  

Notice the manifest android.permission.READ_EXTERNAL_STORAGE and android.permission.CAMERA

And tutorial2_surface_view.xml
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   xmlns:tools="http://schemas.android.com/tools"  
   xmlns:opencv="http://schemas.android.com/apk/res-auto"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent" >  
   <com.cell0907.TDpic.Tutorial3View  
     android:id="@+id/tutorial2_activity_surface_view"  
     android:layout_width="match_parent"  
     android:layout_height="match_parent"  
     opencv:camera_id="1"  
     opencv:show_fps="false" />  
 </LinearLayout>  

Finally, as promissed, some background on this project.

A friend that I had shown this app back in January just sent me this link. It is about the Amazon phone which we believe that it is using the same trick as I do here... Of course, not saying that I was the first one to come up with this idea. Probably somebody had the same thought before. Other folks have used similar tricks (like using a Kinect or a hack of the Wiimote) to sense where the viewer's head is respect to the display and present the right image.

Using those approaches they also have a better/real/full-3D location of the eyes/face/head respect to the display which allows for even a better effect. With one camera you can find only the angle of the face respect to the display, but not the distance (although somebody could argue that you can use the size of the face to estimate that...). Amazon probably solves that with the use of few cameras/triangulation.

Another aspect for improvement is that if there are several viewers in the field of view (FOV) of the camera, then it can get confused respect who to show the image. You could still present respect to one as long as you track the same face, which is one level above what I do.

I also don't do vertical tracking, only horizontal, to the sides... No biggy... Just didn't have enough pics. To simplify the picture taking part, I was thinking to use OpenGL/virtual world, instead of real life pictures, but never finished that... That will certainly be faster to render to.

Finally, I am sure the final effect in Amazon's phone will be a production ready thing, a better effect than I got, I hope, lol! (disclaimer :) ).

Anyhow, just posting this to claim my bragging rights, no matter how small those may be :P

Cheers!

PS.: Please check the following links for a full index of OpenCV and Android posts with other super duper examples :P

Saturday, January 25, 2014

Detect faces with Android SDK, no OpenCV

By now, we have tackled this in many ways :). Usually with OpenCV. See a list of posts here...
Now we are going to basically take this post, where we detected the faces with OpenCV in Android, and replace the face detection portion (CascadeClassifier + detectMultiScale) and replace it for FaceDetector.findFaces method in Android SDK. So, everything else is the same as that post, only thing is that we need to:
  1. Convert Mat to Bitmap on the right format for the face detection method (RGB_565).
  2. Do the face detection with the Android SDK.
We also reduce the original image resolution so that the detection happens much faster, as we did in the original post. So, although I copy here all the code so you don't have to go back and forth, the part changing is what goes after VIEW_MODE_GRAY inside the "public Mat onCameraFrame(CvCameraViewFrame inputFrame)" method. Notice that we still use all the framework from OpenCV to capture and display the image, and not the Android SDK approach.

Note: it seems that the FaceDetector used here is not the one my built-in camera app is using. I know this from simple performance evaluation. For instance, if I rotate the camera, the camera app still detects my face but FaceDetector loses it. It seems that there is one more way in the SDK to detect faces starting from Android 4.0 which I have not tried (so, don't now its performance). Still, probably that is not what is used in the camera app. This post here points to the same and has no answer, in case anybody wants to get some StackOverflow points :). I agree though that using the built-in camera app would likely narrow my software down to my phone.

_3DActivity.java
 /*  
  * Working demo of face detection (remember to put the camera in horizontal)  
  * using OpenCV/CascadeClassifier.  
  * Posted in http://cell0907.blogspot.com/2014/01/detecting-faces-in-android-with-opencv.html  
  */  
 package com.cell0907.td1;  
 import org.opencv.android.BaseLoaderCallback;  
 import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame;  
 import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2;  
 import org.opencv.android.LoaderCallbackInterface;  
 import org.opencv.android.OpenCVLoader;  
 import org.opencv.android.Utils;  
 import org.opencv.core.Core;  
 import org.opencv.core.CvException;  
 import org.opencv.core.CvType;  
 import org.opencv.core.Mat;  
 import org.opencv.core.Scalar;  
 import org.opencv.core.Size;  
 import org.opencv.imgproc.Imgproc;  
 import org.opencv.core.Point;  
 import android.app.Activity;  
 import android.graphics.Bitmap;  
 import android.graphics.PointF;  
 import android.media.FaceDetector;  
 import android.media.FaceDetector.Face;  
 import android.os.Bundle;  
 import android.util.Log;  
 import android.view.Menu;  
 import android.view.MenuItem;  
 import android.view.SurfaceView;  
 import android.view.WindowManager;  
 public class _3DActivity extends Activity implements CvCameraViewListener2 {  
   private static final int         VIEW_MODE_CAMERA  = 0;  
   private static final int         VIEW_MODE_GRAY   = 1;  
   private static final int         VIEW_MODE_FACES  = 2;  
   private MenuItem             mItemPreviewRGBA;  
   private MenuItem             mItemPreviewGrey;  
   private MenuItem             mItemPreviewFaces;  
   private int               mViewMode;  
   private Mat               mRgba;  
   private Mat               mGrey;  
   private int                              screen_w, screen_h;  
   private Tutorial3View            mOpenCvCameraView;   
   private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {  
     @Override  
     public void onManagerConnected(int status) {  
       switch (status) {  
         case LoaderCallbackInterface.SUCCESS:  
         {  
           // Load native library after(!) OpenCV initialization  
           mOpenCvCameraView.enableView();        
         } break;  
         default:  
         {  
           super.onManagerConnected(status);  
         } break;  
       }  
     }  
   };  
   public _3DActivity() {  
   }  
   /** Called when the activity is first created. */  
   @Override  
   public void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);  
     setContentView(R.layout.tutorial2_surface_view);  
     mOpenCvCameraView = (Tutorial3View) findViewById(R.id.tutorial2_activity_surface_view);  
     mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);  
     mOpenCvCameraView.setCvCameraViewListener(this);  
   }  
   @Override  
   public void onPause()  
   {  
     super.onPause();  
     if (mOpenCvCameraView != null)  
       mOpenCvCameraView.disableView();  
   }  
   @Override  
   public void onResume()  
   {  
     super.onResume();  
     OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_3, this, mLoaderCallback);  
   }  
   public void onDestroy() {  
     super.onDestroy();  
     if (mOpenCvCameraView != null)  
       mOpenCvCameraView.disableView();  
   }  
   public void onCameraViewStarted(int width, int height) {  
        screen_w=width;  
        screen_h=height;  
     mRgba = new Mat(screen_w, screen_h, CvType.CV_8UC4);  
     mGrey = new Mat(screen_w, screen_h, CvType.CV_8UC1);  
     Log.v("MyActivity","Height: "+height+" Width: "+width);  
   }  
   public void onCameraViewStopped() {  
     mRgba.release();  
     mGrey.release();  
   }  
   public Mat onCameraFrame(CvCameraViewFrame inputFrame) {  
        long startTime = System.nanoTime();  
        long endTime;  
        boolean show=true;  
        mRgba=inputFrame.rgba();  
        if (mViewMode==VIEW_MODE_CAMERA) {  
             endTime = System.nanoTime();  
          if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
             return mRgba;  
        }     
        if (mViewMode==VIEW_MODE_GRAY){  
          Imgproc.cvtColor( mRgba, mGrey, Imgproc.COLOR_BGR2GRAY);           
          endTime = System.nanoTime();  
          if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
             return mGrey;  
        }  
        // REDUCE THE RESOLUTION TO EXPEDITE THINGS  
        Mat low_res = new Mat(screen_w, screen_h, CvType.CV_8UC4);  
        Imgproc.resize(mRgba,low_res,new Size(),0.25,0.25,Imgproc.INTER_LINEAR);  
        Bitmap bmp = null;  
        try {  
          bmp = Bitmap.createBitmap(low_res.width(), low_res.height(), Bitmap.Config.RGB_565);  
          Utils.matToBitmap(low_res, bmp);  
        }  
        catch (CvException e){Log.v("MyActivity",e.getMessage());}  
           int maxNumFaces = 1; // Set this to whatever you want  
           FaceDetector fd = new FaceDetector((int)(screen_w/4),(int)(screen_h/4),  
                  maxNumFaces);  
           Face[] faces = new Face[maxNumFaces];  
           try {  
                  int numFacesFound = fd.findFaces(bmp, faces);  
                  if (numFacesFound<maxNumFaces) maxNumFaces=numFacesFound;  
                  for (int i = 0; i < maxNumFaces; ++i) {  
                       Face face = faces[i];  
                       PointF MidPoint = new PointF();  
                 face.getMidPoint(MidPoint);  
 /*                      Log.v("MyActivity","Face " + i + " found with " + face.confidence() + " confidence!");  
                       Log.v("MyActivity","Face " + i + " eye distance " + face.eyesDistance());  
                       Log.v("MyActivity","Face " + i + " midpoint (between eyes) " + MidPoint);*/  
                       Point center= new Point(4*MidPoint.x, 4*MidPoint.y);  
                       Core.ellipse( mRgba, new Point(center.x,center.y), new Size(8*face.eyesDistance(), 8*face.eyesDistance()), 0, 0, 360, new Scalar( 255, 0, 255 ), 4, 8, 0 );  
                  }  
             } catch (IllegalArgumentException e) {  
                  // From Docs:  
                  // if the Bitmap dimensions don't match the dimensions defined at initialization   
                  // or the given array is not sized equal to the maxFaces value defined at   
                  // initialization  
                  Log.v("MyActivity","Argument dimensions wrong");  
             }  
           if (mViewMode==VIEW_MODE_FACES) {  
                endTime = System.nanoTime();  
                if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
             return mRgba;  
                //return low_res;  
        }          
           return mRgba;  
    }  
   @Override  
   public boolean onCreateOptionsMenu(Menu menu) {  
     mItemPreviewRGBA = menu.add("RGBA");  
     mItemPreviewGrey = menu.add("Grey");  
     mItemPreviewFaces = menu.add("Faces");  
     return true;  
   }  
   public boolean onOptionsItemSelected(MenuItem item) {  
     if (item == mItemPreviewRGBA) {  
       mViewMode = VIEW_MODE_CAMERA;  
     } else if (item == mItemPreviewGrey) {  
       mViewMode = VIEW_MODE_GRAY;  
     } else if (item == mItemPreviewFaces) {  
       mViewMode = VIEW_MODE_FACES;  
     }  
     return true;  
   }    
 }  

For the rest of the files, please check the original post.

Just a final note... Somebody may wonder why I am mixing OpenCV with face detection from Android SDK. I am using OpenCV because it allows me to display something completely unrelated to what the camera is capturing (I need that for my final app). Although I found a method to do that without OpenCV I think it doesn't work with all the devices out there. And I use the face detection from Android SDK because I think it is more robust and still works, for instance, when turning the head... The weird thing is that it doesn't seem to work as good as the one I get on my camera app (the one that comes from factory with my phone, and HTC One). I also thought it would be faster, but actually looks slower...

PS.: Reference I used...

Monday, January 20, 2014

Detecting faces in Android with OpenCV

Here we are going to detect the faces in images captured by the camera of an Android phone. Notice that Android has built in support for this specific function (see my other post on that) but this example can be applied to anything else where we want to use CascadeClassifier. Also, you can see this with an example for PC using Java/OpenCV. 

Few things to highlight from this post:

1. We wanted to load the .xml cascade classifier from the raw resources, not from a file somewhere. As the casacadeclassifier::load needs a  filepath and raw resources do not have one, the only way I found around this is to save the xml in a file (at the beginning of execution) and load it from there. Explained here. Other stuff I tried is documented in the end, but didn't work...

2. Also, notice that unlike Android camera, in general, and as we have seen in another example, what you capture and what you display, do not have to be the same. Basically, the onCameraFrame  method is called automatically (callback) when a frame from camera is ready. This method returns the Mat that is to be presented in the display (automatically). So, inside the method we can take what the camera captured (object of CvCameraViewFrame class that represents frame from camera) and return whatever we want, related or not to the capture, that will then be displayed in the screen. Note: If you just want to "cut and paste" something that gets your Android camera going and capturing to process in OpenCV, you can use the explanation here.

3. Finally, notice how we choose a smaller size of the capture (setting the camera to lower resolution) in order to expedite the analysis. Using full resolution the processing was taken 750ms in my HTC One! Obviously, we can't work with that... The issue on this case is that we display that same image, so, the display quality is not good. We could, of course, choose to capture full resolution, scale down, detect the faces, highlight them in the original capture and display it (at full resolution). The first code below uses the first method, while the 2nd code below uses the 2nd method.

_3DActivity.java
 /*  
  * Working demo of face detection (remember to put the camera in horizontal)  
  * using OpenCV/CascadeClassifier. Capture at lower resolution  
  */  
 package com.cell0907.td1;  
 import java.io.File;  
 import java.io.FileOutputStream;  
 import java.io.IOException;  
 import java.io.InputStream;  
 import java.util.Arrays;  
 import java.util.List;  
 import org.opencv.android.BaseLoaderCallback;  
 import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame;  
 import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2;  
 import org.opencv.android.LoaderCallbackInterface;  
 import org.opencv.android.OpenCVLoader;  
 import org.opencv.android.Utils;  
 import org.opencv.core.Core;  
 import org.opencv.core.CvType;  
 import org.opencv.core.Mat;  
 import org.opencv.core.MatOfRect;  
 import org.opencv.core.Scalar;  
 import org.opencv.core.Size;  
 import org.opencv.imgproc.Imgproc;  
 import org.opencv.objdetect.CascadeClassifier;  
 import org.opencv.core.Point;  
 import org.opencv.core.Rect;  
 import android.app.Activity;  
 import android.content.Context;  
 import android.graphics.Bitmap;  
 import android.graphics.BitmapFactory;  
 import android.os.Bundle;  
 import android.os.Environment;  
 import android.util.Log;  
 import android.view.Menu;  
 import android.view.MenuItem;  
 import android.view.SurfaceView;  
 import android.view.WindowManager;  
 public class _3DActivity extends Activity implements CvCameraViewListener2 {  
   private static final int         VIEW_MODE_CAMERA  = 0;  
   private static final int         VIEW_MODE_GRAY   = 1;  
   private static final int         VIEW_MODE_FACES  = 2;  
   private MenuItem             mItemPreviewRGBA;  
   private MenuItem             mItemPreviewGrey;  
   private MenuItem             mItemPreviewFaces;  
   private int               mViewMode;  
   private Mat               mRgba;  
   private Mat               mGrey;  
   private int                              screen_w, screen_h;  
   private CascadeClassifier           face_cascade;  
   private Tutorial3View            mOpenCvCameraView;   
   private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {  
     @Override  
     public void onManagerConnected(int status) {  
       switch (status) {  
         case LoaderCallbackInterface.SUCCESS:  
         {  
           // Load native library after(!) OpenCV initialization  
           mOpenCvCameraView.enableView();        
           load_cascade();  
         } break;  
         default:  
         {  
           super.onManagerConnected(status);  
         } break;  
       }  
     }  
   };  
   public _3DActivity() {  
   }  
   /** Called when the activity is first created. */  
   @Override  
   public void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);  
     setContentView(R.layout.tutorial2_surface_view);  
     mOpenCvCameraView = (Tutorial3View) findViewById(R.id.tutorial2_activity_surface_view);  
     mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);  
     mOpenCvCameraView.setCvCameraViewListener(this);  
   }  
   @Override  
   public void onPause()  
   {  
     super.onPause();  
     if (mOpenCvCameraView != null)  
       mOpenCvCameraView.disableView();  
   }  
   @Override  
   public void onResume()  
   {  
     super.onResume();  
     OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_3, this, mLoaderCallback);  
   }  
   public void onDestroy() {  
     super.onDestroy();  
     if (mOpenCvCameraView != null)  
       mOpenCvCameraView.disableView();  
   }  
   android.hardware.Camera.Size set_resolution(){  
        List<android.hardware.Camera.Size> mResolutionList = mOpenCvCameraView.getResolutionList();  
     int lowest= mResolutionList.size()-1;  
     android.hardware.Camera.Size resolution = mResolutionList.get(lowest);  
     // This has worked fine with my phone, but not sure the resolutions are sorted  
     mOpenCvCameraView.setResolution(resolution);  
     return resolution;   
   }  
   public void onCameraViewStarted(int width, int height) {  
        android.hardware.Camera.Size r=set_resolution();   
        //Do we know if the two of them (view and camera) match  
        screen_w=r.width;  
        screen_h=r.height;  
     mRgba = new Mat(screen_w, screen_h, CvType.CV_8UC4);  
     mGrey = new Mat(screen_w, screen_h, CvType.CV_8UC1);  
     Log.v("MyActivity","Height: "+r.height+" Width: "+r.width);  
   }  
   public void onCameraViewStopped() {  
     mRgba.release();  
     mGrey.release();  
   }  
   public Mat onCameraFrame(CvCameraViewFrame inputFrame) {  
        long startTime = System.nanoTime();  
        long endTime;  
        boolean show=true;  
        MatOfRect faces = new MatOfRect();  
        mRgba=inputFrame.rgba();  
        if (mViewMode==VIEW_MODE_CAMERA) {  
             endTime = System.nanoTime();  
          if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
             return mRgba;  
        }  
        Imgproc.cvtColor( mRgba, mGrey, Imgproc.COLOR_BGR2GRAY);   
        if (mViewMode==VIEW_MODE_GRAY){             
             endTime = System.nanoTime();  
          if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
             return mGrey;  
        }  
        Imgproc.equalizeHist( mGrey, mGrey );   
           face_cascade.detectMultiScale(mGrey, faces);  
           if (show==true) Log.v("MyActivity","Detected "+faces.toArray().length+" faces");  
           for(Rect rect:faces.toArray())  
           {  
                Point center= new Point(rect.x + rect.width*0.5, rect.y + rect.height*0.5 );  
                Core.ellipse( mRgba, center, new Size( rect.width*0.5, rect.height*0.5), 0, 0, 360, new Scalar( 255, 0, 255 ), 4, 8, 0 );  
           }  
           if (mViewMode==VIEW_MODE_FACES) {  
                endTime = System.nanoTime();  
                if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
             return mRgba;  
        }  
           return mRgba;  
    }  
   @Override  
   public boolean onCreateOptionsMenu(Menu menu) {  
     mItemPreviewRGBA = menu.add("RGBA");  
     mItemPreviewGrey = menu.add("Grey");  
     mItemPreviewFaces = menu.add("Faces");  
     return true;  
   }  
   public boolean onOptionsItemSelected(MenuItem item) {  
     if (item == mItemPreviewRGBA) {  
       mViewMode = VIEW_MODE_CAMERA;  
     } else if (item == mItemPreviewGrey) {  
       mViewMode = VIEW_MODE_GRAY;  
     } else if (item == mItemPreviewFaces) {  
       mViewMode = VIEW_MODE_FACES;  
     }  
     return true;  
   }    
   private void load_cascade(){  
        try {  
             // LOAD FROM ASSET  
             InputStream is = getResources().openRawResource(R.raw.lbpcascade_frontalface);  
             //InputStream is = getResources().openRawResource(R.raw.haarcascade_frontalface_alt);  
             File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);  
             File mCascadeFile = new File(cascadeDir, "lbpcascade_frontalface.xml");  
             FileOutputStream os = new FileOutputStream(mCascadeFile);  
             byte[] buffer = new byte[4096];  
             int bytesRead;  
             while ((bytesRead = is.read(buffer)) != -1) {  
                  os.write(buffer, 0, bytesRead);  
             }  
             is.close();  
             os.close();  
             face_cascade = new CascadeClassifier(mCascadeFile.getAbsolutePath());  
             if(face_cascade.empty())  
             {  
                  Log.v("MyActivity","--(!)Error loading A\n");  
                  return;  
             }  
             else  
             {  
                  Log.v("MyActivity",  
                            "Loaded cascade classifier from " + mCascadeFile.getAbsolutePath());  
             }  
        } catch (IOException e) {  
             e.printStackTrace();  
             Log.v("MyActivity", "Failed to load cascade. Exception thrown: " + e);  
        }  
   }  
 }  

I've read somewhere that picking the lowest resolution this way is not guaranteed to work, as the list may not be in order. Have not worked further on this and it works for me, but just a warning...

Using the second method, basically we remove the stuff that checks/set the resolution of the camera and use Imgproc.resize to get a lower resolution Mat to be processed, instead of the full resolution. Then we put the results back in the original resolution Mat, to present it nicely:
 /*  
  * Working demo of face detection (remember to put the camera in horizontal)  
  * using OpenCV/CascadeClassifier.  
  */  
 package com.cell0907.td1;  
 import java.io.File;  
 import java.io.FileOutputStream;  
 import java.io.IOException;  
 import java.io.InputStream;  
 import java.util.Arrays;  
 import java.util.List;  
 import org.opencv.android.BaseLoaderCallback;  
 import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame;  
 import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2;  
 import org.opencv.android.LoaderCallbackInterface;  
 import org.opencv.android.OpenCVLoader;  
 import org.opencv.android.Utils;  
 import org.opencv.core.Core;  
 import org.opencv.core.CvType;  
 import org.opencv.core.Mat;  
 import org.opencv.core.MatOfRect;  
 import org.opencv.core.Scalar;  
 import org.opencv.core.Size;  
 import org.opencv.imgproc.Imgproc;  
 import org.opencv.objdetect.CascadeClassifier;  
 import org.opencv.core.Point;  
 import org.opencv.core.Rect;  
 import android.app.Activity;  
 import android.content.Context;  
 import android.graphics.Bitmap;  
 import android.graphics.BitmapFactory;  
 import android.os.Bundle;  
 import android.os.Environment;  
 import android.util.Log;  
 import android.view.Menu;  
 import android.view.MenuItem;  
 import android.view.SurfaceView;  
 import android.view.WindowManager;  
 public class _3DActivity extends Activity implements CvCameraViewListener2 {  
   private static final int         VIEW_MODE_CAMERA  = 0;  
   private static final int         VIEW_MODE_GRAY   = 1;  
   private static final int         VIEW_MODE_FACES  = 2;  
   private MenuItem             mItemPreviewRGBA;  
   private MenuItem             mItemPreviewGrey;  
   private MenuItem             mItemPreviewFaces;  
   private int               mViewMode;  
   private Mat               mRgba;  
   private Mat               mGrey;  
   private int                              screen_w, screen_h;  
   private CascadeClassifier           face_cascade;  
   private Tutorial3View            mOpenCvCameraView;   
   private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {  
     @Override  
     public void onManagerConnected(int status) {  
       switch (status) {  
         case LoaderCallbackInterface.SUCCESS:  
         {  
           // Load native library after(!) OpenCV initialization  
           mOpenCvCameraView.enableView();        
           load_cascade();  
         } break;  
         default:  
         {  
           super.onManagerConnected(status);  
         } break;  
       }  
     }  
   };  
   public _3DActivity() {  
   }  
   /** Called when the activity is first created. */  
   @Override  
   public void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);  
     setContentView(R.layout.tutorial2_surface_view);  
     mOpenCvCameraView = (Tutorial3View) findViewById(R.id.tutorial2_activity_surface_view);  
     mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);  
     mOpenCvCameraView.setCvCameraViewListener(this);  
   }  
   @Override  
   public void onPause()  
   {  
     super.onPause();  
     if (mOpenCvCameraView != null)  
       mOpenCvCameraView.disableView();  
   }  
   @Override  
   public void onResume()  
   {  
     super.onResume();  
     OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_3, this, mLoaderCallback);  
   }  
   public void onDestroy() {  
     super.onDestroy();  
     if (mOpenCvCameraView != null)  
       mOpenCvCameraView.disableView();  
   }  
   public void onCameraViewStarted(int width, int height) {  
        screen_w=width;  
        screen_h=height;  
     mRgba = new Mat(screen_w, screen_h, CvType.CV_8UC4);  
     mGrey = new Mat(screen_w, screen_h, CvType.CV_8UC1);  
     Log.v("MyActivity","Height: "+height+" Width: "+width);  
   }  
   public void onCameraViewStopped() {  
     mRgba.release();  
     mGrey.release();  
   }  
   public Mat onCameraFrame(CvCameraViewFrame inputFrame) {  
        long startTime = System.nanoTime();  
        long endTime;  
        boolean show=true;  
        MatOfRect faces = new MatOfRect();  
        mRgba=inputFrame.rgba();  
        if (mViewMode==VIEW_MODE_CAMERA) {  
             endTime = System.nanoTime();  
          if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
             return mRgba;  
        }  
        Imgproc.cvtColor( mRgba, mGrey, Imgproc.COLOR_BGR2GRAY);   
        if (mViewMode==VIEW_MODE_GRAY){             
             endTime = System.nanoTime();  
          if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
             return mGrey;  
        }  
        Mat low_res = new Mat(screen_w, screen_h, CvType.CV_8UC1);  
        // 1280 x 720  
        Log.v("MyActivity","width: "+screen_w+" height: "+screen_h);  
        Imgproc.resize(mGrey,low_res,new Size(),0.25,0.25,Imgproc.INTER_LINEAR);  
        Imgproc.equalizeHist( low_res, low_res );   
           face_cascade.detectMultiScale(low_res, faces);  
           if (show==true) Log.v("MyActivity","Detected "+faces.toArray().length+" faces");  
           for(Rect rect:faces.toArray())  
           {  
                Point center= new Point(4*rect.x + 4*rect.width*0.5, 4*rect.y + 4*rect.height*0.5 );  
                Core.ellipse( mRgba, new Point(center.x,center.y), new Size( rect.width*2, rect.height*2), 0, 0, 360, new Scalar( 255, 0, 255 ), 4, 8, 0 );  
           }  
           if (mViewMode==VIEW_MODE_FACES) {  
                endTime = System.nanoTime();  
                if (show==true) Log.v("MyActivity","Elapsed time: "+ (float)(endTime - startTime)/1000000+"ms");  
             return mRgba;  
                //return low_res;  
        }  
           return mRgba;  
    }  
   @Override  
   public boolean onCreateOptionsMenu(Menu menu) {  
     mItemPreviewRGBA = menu.add("RGBA");  
     mItemPreviewGrey = menu.add("Grey");  
     mItemPreviewFaces = menu.add("Faces");  
     return true;  
   }  
   public boolean onOptionsItemSelected(MenuItem item) {  
     if (item == mItemPreviewRGBA) {  
       mViewMode = VIEW_MODE_CAMERA;  
     } else if (item == mItemPreviewGrey) {  
       mViewMode = VIEW_MODE_GRAY;  
     } else if (item == mItemPreviewFaces) {  
       mViewMode = VIEW_MODE_FACES;  
     }  
     return true;  
   }    
   private void load_cascade(){  
        try {  
             // LOAD FROM ASSET  
             InputStream is = getResources().openRawResource(R.raw.lbpcascade_frontalface);  
             //InputStream is = getResources().openRawResource(R.raw.haarcascade_frontalface_alt);  
             File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);  
             File mCascadeFile = new File(cascadeDir, "lbpcascade_frontalface.xml");  
             FileOutputStream os = new FileOutputStream(mCascadeFile);  
             byte[] buffer = new byte[4096];  
             int bytesRead;  
             while ((bytesRead = is.read(buffer)) != -1) {  
                  os.write(buffer, 0, bytesRead);  
             }  
             is.close();  
             os.close();  
             face_cascade = new CascadeClassifier(mCascadeFile.getAbsolutePath());  
             if(face_cascade.empty())  
             {  
                  Log.v("MyActivity","--(!)Error loading A\n");  
                  return;  
             }  
             else  
             {  
                  Log.v("MyActivity",  
                            "Loaded cascade classifier from " + mCascadeFile.getAbsolutePath());  
             }  
        } catch (IOException e) {  
             e.printStackTrace();  
             Log.v("MyActivity", "Failed to load cascade. Exception thrown: " + e);  
        }  
   }  
 }  

For any of the two, the following files are the same. Tutorial3View.java:
 package com.cell0907.td1;  
 import java.io.FileOutputStream;  
 import java.util.List;  
 import org.opencv.android.JavaCameraView;  
 import android.content.Context;  
 import android.hardware.Camera;  
 import android.hardware.Camera.PictureCallback;  
 import android.hardware.Camera.Size;  
 import android.util.AttributeSet;  
 import android.util.Log;  
 public class Tutorial3View extends JavaCameraView implements PictureCallback {  
   private static final String TAG = "MyActivity";  
   private String mPictureFileName;  
   public Tutorial3View(Context context, AttributeSet attrs) {  
     super(context, attrs);  
   }  
   public List<String> getEffectList() {  
     return mCamera.getParameters().getSupportedColorEffects();  
   }  
   public boolean isEffectSupported() {  
     return (mCamera.getParameters().getColorEffect() != null);  
   }  
   public String getEffect() {  
     return mCamera.getParameters().getColorEffect();  
   }  
   public void setEffect(String effect) {  
     Camera.Parameters params = mCamera.getParameters();  
     params.setColorEffect(effect);  
     mCamera.setParameters(params);  
   }  
   public List<Size> getResolutionList() {  
     return mCamera.getParameters().getSupportedPreviewSizes();  
   }  
   public void setResolution(Size resolution) {  
     disconnectCamera();  
     mMaxHeight = resolution.height;  
     mMaxWidth = resolution.width;  
     connectCamera(getWidth(), getHeight());  
   }  
   public Size getResolution() {  
     return mCamera.getParameters().getPreviewSize();  
   }  
   public void takePicture(final String fileName) {  
     Log.i(TAG, "Taking picture");  
     this.mPictureFileName = fileName;  
     // Postview and jpeg are sent in the same buffers if the queue is not empty when performing a capture.  
     // Clear up buffers to avoid mCamera.takePicture to be stuck because of a memory issue  
     mCamera.setPreviewCallback(null);  
     // PictureCallback is implemented by the current class  
     mCamera.takePicture(null, null, this);  
   }  
   @Override  
   public void onPictureTaken(byte[] data, Camera camera) {  
     Log.i(TAG, "Saving a bitmap to file");  
     // The camera preview was automatically stopped. Start it again.  
     mCamera.startPreview();  
     mCamera.setPreviewCallback(this);  
     // Write the image in a file (in jpeg format)  
     try {  
       FileOutputStream fos = new FileOutputStream(mPictureFileName);  
       fos.write(data);  
       fos.close();  
     } catch (java.io.IOException e) {  
       Log.e("PictureDemo", "Exception in photoCallback", e);  
     }  
   }  
 }  

The layout: Tutorial2_surface_view.xml:
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   xmlns:tools="http://schemas.android.com/tools"  
   xmlns:opencv="http://schemas.android.com/apk/res-auto"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent" >  
   <com.cell0907.td1.Tutorial3View  
     android:id="@+id/tutorial2_activity_surface_view"  
     android:layout_width="match_parent"  
     android:layout_height="match_parent"  
     opencv:camera_id="1"  
     opencv:show_fps="false" />  
 </LinearLayout>  

AndroidManifest.xml:
 <?xml version="1.0" encoding="utf-8"?>  
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
      package="com.cell0907.td1"  
      android:versionCode="21"  
      android:versionName="2.1">  
       <supports-screens android:resizeable="true"  
            android:smallScreens="true"  
            android:normalScreens="true"  
            android:largeScreens="true"  
            android:anyDensity="true" />  
   <uses-sdk android:minSdkVersion="8"   
               android:targetSdkVersion="10" />  
   <uses-permission android:name="android.permission.CAMERA"/>  
   <uses-feature android:name="android.hardware.camera" android:required="false"/>  
   <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>  
   <uses-feature android:name="android.hardware.camera.front" android:required="false"/>  
   <uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false"/>  
   <application  
     android:label="@string/app_name"  
     android:icon="@drawable/icon"  
     android:theme="@android:style/Theme.NoTitleBar.Fullscreen"  
     android:allowBackup="false">  
     <activity android:name="_3DActivity"  
          android:label="@string/app_name"  
          android:screenOrientation="landscape"  
          android:configChanges="keyboardHidden|orientation">  
       <intent-filter>  
         <action android:name="android.intent.action.MAIN" />  
         <category android:name="android.intent.category.LAUNCHER" />  
       </intent-filter>  
     </activity>  
   </application>  
 </manifest>  

Cheers!

PS1.: Click here to see the index of these series of posts on OpenCV
PS2.: This is other stuff I found in some places and "randomly" tried after reading that it was not possible to get a file path to a resource, and sure enough, didn't work... But just for documentation sake:

    Uri uri=Uri.parse("android.resource://com.example.td1/raw/lbpcascade_frontalface");
    File myFile = new File(uri.toString());
    face_cascade=new CascadeClassifier(myFile.getAbsolutePath());

Or this:                                       

    face_cascade=new CascadeClassifier("file:///android_asset/lbpcascade_frontalface.xml");
   
Or this:

    face_cascade=new CascadeClassifier("android.resource://com.example.td1/raw/lbpcascade_frontalface.xml");

Thursday, January 16, 2014

Android Camera capture without display/user interface preview

Here I am going to capture the scene with the Android camera without showing in the screen what the camera is seeing but something completely unrelated to the captured image itself. This has several uses. In my case, I plan to do image processing on the pics but actually display something completely unrelated.

If you want to show what the camera is seeing, it is something standard and you can find more documentation out there. I provide some links all the way down.

Avoiding to show it, which somebody would think it should be straightforward, seems to be unsupported in Android. Some say that it may be because of privacy concerns (you want to see that the camera is on by seeing the display on). I kind of doubt it, because, as I'll show it can actually be done easilly. Just that is not documented.

I found so far two approaches, which basically should yield a third one that I still need to research:
  1. Directly, using the Android camera API. I found the solution here which pointed also here (the same).
  2. Using OpenCV (I'll show that on a different post). 
  3. One would think that if OpenCV in Android can do it, there may be an Android API way using the same trick that OpenCV is using. Got to research that...
So, anyhow, let's look at the first method, directly, using Android camera API:

The top level steps are:
  1. Check if there is a camera
  2. If there is, find the id of the one you want (front or back...). Check the findFrontFacingCamera routine in the code below.
  3. Open it (camera.open API call). See safeCameraOpen.
  4. Create a fake SurfaceView and set it for the camera. Usually this is used for the preview of the camera (again, see all the way below for the typical use), but here we just trick the camera as we never actually display it. This, with #5, are the key differences respect to displaying the preview...
  5. When we want to take a picture, triggered on any way (somebody presses a button or touches the screen or, in my case, with a timer), call camera.takePicture. The arguments of the method are callback functions that happen along the process of taking the picture. From the documentation:
Triggers an asynchronous image capture. The camera service will initiate a series of callbacks to the application as the image capture progresses. The shutter callback occurs after the image is captured. This can be used to trigger a sound to let the user know that image has been captured. The raw callback occurs when the raw image data is available (NOTE: the data will be null if there is no raw image callback buffer available or the raw image callback buffer is not large enough to hold the raw image). The postview callback occurs when a scaled, fully processed postview image is available (NOTE: not all hardware supports this). The jpeg callback occurs when the compressed image is available. If the application does not need a particular callback, a null can be passed instead of a callback method.
This method is only valid when preview is active (after startPreview()). Preview will be stopped after the image is taken; callers must call startPreview() again if they want to re-start preview or take more pictures. This should not be called between start() and stop().
After calling this method, you must not call startPreview() or take another picture until the JPEG callback has returned.
  1. The shutter callback is mostly used to trigger a shutter sound. The raw callback would be the ideal thing to use in my case, for image processing. Nevertheless, many posts warn about the poor documentation and manufacturer specific issues, so, I go, for the time being, for the safe/next one, which uses  jpeg encoding, unfortunately wasting precious processor bandwidth to encode/decode the image and loss of quality (although I don't think the second is so critical to me).
  2. Parallel to that, we have an ImageView that we use to display whatever we want related or not to the camera.
  3. So, that would be it. Notice that in my code I trigger the capture with a timer done with an AsyncTask. After a certain time, the task sends a message that will make the handle call takePicture, setting the callback to get a jpeg from the camera. When done, sends another message back and then the handle presents that picture in the display, re-stating the whole process. Notice that:
    1. I present the picture because I want (just to show is working), but I didn't have to. I can choose to present whatever I want in ImageView. That was the whole point...
    2. In my case, actually I want to take pictures as fast as possible, so, the timer is actually irrelevant. I could just delete it and call takePicture directly, but I leave it there to explain how I would do it with delay in the middle...
MainActivity.java
 package com.example.camera1;  
 import java.io.IOException;  
 import android.os.Bundle;  
 import android.os.Handler;  
 import android.os.Message;  
 import android.app.Activity;  
 import android.content.pm.PackageManager;  
 import android.graphics.Bitmap;  
 import android.graphics.BitmapFactory;  
 import android.hardware.Camera;  
 import android.hardware.Camera.CameraInfo;  
 import android.util.Log;  
 import android.view.SurfaceView;  
 import android.widget.ImageView;  
 import android.widget.Toast;  
 public class MainActivity extends Activity {  
      public static final int DONE=1;  
      public static final int NEXT=2;  
      public static final int PERIOD=1;   
      private Camera camera;  
      private int cameraId = 0;  
      private ImageView display;  
      private Timer timer;  
      @Override  
      public void onCreate(Bundle savedInstanceState) {  
           super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        display=(ImageView)findViewById(R.id.imageView1);  
        // do we have a camera?  
        if (!getPackageManager()  
          .hasSystemFeature(PackageManager.FEATURE_CAMERA)) {  
         Toast.makeText(this, "No camera on this device", Toast.LENGTH_LONG)  
           .show();  
        } else {  
         cameraId = findFrontFacingCamera();  
         if (cameraId < 0) {  
          Toast.makeText(this, "No front facing camera found.",  
            Toast.LENGTH_LONG).show();  
         } else {  
              safeCameraOpen(cameraId);   
         }  
        }         
        // THIS IS JUST A FAKE SURFACE TO TRICK THE CAMERA PREVIEW  
        // http://stackoverflow.com/questions/17859777/how-to-take-pictures-in-android-  
        // application-without-the-user-interface  
        SurfaceView view = new SurfaceView(this);  
        try {  
                camera.setPreviewDisplay(view.getHolder());  
           } catch (IOException e) {  
                // TODO Auto-generated catch block  
                e.printStackTrace();  
           }  
        camera.startPreview();  
        Camera.Parameters params = camera.getParameters();  
        params.setJpegQuality(100);  
        camera.setParameters(params);  
        // We need something to trigger periodically the capture of a  
        // picture to be processed  
        timer=new Timer(getApplicationContext(),threadHandler);  
        timer.execute();  
        }  
      ////////////////////////////////////thread Handler///////////////////////////////////////  
      private Handler threadHandler = new Handler() {  
           public void handleMessage(android.os.Message msg) {       
                 switch(msg.what){  
                 case DONE:  
                     // Trigger camera callback to take pic  
                      camera.takePicture(null, null, mCall);  
                      break;  
                 case NEXT:  
                      timer=new Timer(getApplicationContext(),threadHandler);  
                      timer.execute();  
                      break;  
                 }  
                 }  
            };  
       Camera.PictureCallback mCall = new Camera.PictureCallback() {  
            public void onPictureTaken(byte[] data, Camera camera) {  
               //decode the data obtained by the camera into a Bitmap  
                  //display.setImageBitmap(photo);  
                  Bitmap bitmapPicture  
                  = BitmapFactory.decodeByteArray(data, 0, data.length);  
                  display.setImageBitmap(bitmapPicture);  
                  Message.obtain(threadHandler, MainActivity.NEXT, "").sendToTarget();   
                  //Log.v("MyActivity","Length: "+data.length);  
             }        
       };  
      private int findFrontFacingCamera() {  
           int cameraId = -1;  
           // Search for the front facing camera  
           int numberOfCameras = Camera.getNumberOfCameras();  
           for (int i = 0; i < numberOfCameras; i++) {  
                CameraInfo info = new CameraInfo();  
                Camera.getCameraInfo(i, info);  
                if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {  
                     Log.v("MyActivity", "Camera found");  
               cameraId = i;  
               break;  
              }  
             }  
             return cameraId;  
            }  
      @Override  
      protected void onPause() {  
           if (timer!=null){  
                timer.cancel(true);  
           }  
        releaseCamera();  
        super.onPause();  
       }       
      // I think Android Documentation recommends doing this in a separate  
      // task to avoid blocking main UI  
      private boolean safeCameraOpen(int id) {  
        boolean qOpened = false;  
        try {  
          releaseCamera();  
          camera = Camera.open(id);  
          qOpened = (camera != null);  
        } catch (Exception e) {  
          Log.e(getString(R.string.app_name), "failed to open Camera");  
          e.printStackTrace();  
        }  
        return qOpened;    
      }  
      private void releaseCamera() {  
        if (camera != null) {  
             camera.stopPreview();  
          camera.release();  
          camera = null;  
        }  
      }  
 }  

Timer.java
 package com.example.camera1;  
 import android.content.Context;  
 import android.os.Handler;  
 import android.os.Message;  
 import android.os.AsyncTask;  
 public class Timer extends AsyncTask<Void, Void, Void> {  
   Context mContext;  
      private Handler threadHandler;  
   public Timer(Context context,Handler threadHandler) {  
     super();  
     this.threadHandler=threadHandler;  
     mContext = context;  
       }  
   @Override  
      protected Void doInBackground(Void...params) {   
        try {  
                Thread.sleep(MainActivity.PERIOD);  
           } catch (InterruptedException e) {  
                // TODO Auto-generated catch block  
                e.printStackTrace();  
           }   
        Message.obtain(threadHandler, MainActivity.DONE, "").sendToTarget();   
         return null;  
   }  
 }  

AndroidManifest.xml
 <?xml version="1.0" encoding="utf-8"?>  
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
   package="com.example.camera1"  
   android:versionCode="1"  
   android:versionName="1.0" >  
   <uses-sdk  
     android:minSdkVersion="9"  
     android:targetSdkVersion="17" />  
      <uses-permission android:name="android.permission.CAMERA"/>  
   <application  
     android:label="@string/app_name"  
     android:icon="@drawable/ic_launcher"  
     android:theme="@android:style/Theme.NoTitleBar.Fullscreen"  
     android:allowBackup="false">  
     <activity android:name="MainActivity"  
          android:label="@string/app_name"  
          android:screenOrientation="landscape"  
          android:configChanges="keyboardHidden|orientation">  
       <intent-filter>  
         <action android:name="android.intent.action.MAIN" />  
         <category android:name="android.intent.category.LAUNCHER" />  
       </intent-filter>  
     </activity>  
   </application>  
 </manifest>  

And the layout activity_main.xml:
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   android:layout_width="match_parent"  
   android:layout_height="wrap_content" >  
   <ImageView  
     android:id="@+id/imageView1"  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content"  
     android:layout_alignParentBottom="true"  
     android:layout_alignParentLeft="true"  
     android:layout_alignParentRight="true"  
     android:layout_alignParentTop="true"  
     android:src="@drawable/ic_launcher" />  
 </RelativeLayout>  

Cheers!

PS.: Check out this video on the Android 3D display I build based on this stuff and the details here.
PS1.: Please, click here to see an index of other posts on Android.
PS2.: Other camera examples:
http://www.vogella.com/articles/AndroidCamera/article.html
http://android-er.blogspot.com.es/2010/12/implement-takepicture-function-of.html
http://android.codota.com/scenarios/518915fdda0a68487f6039c2/android.hardware.Camera?tag=out_2013_05_05_07_19_34&fullSource=1

Wednesday, December 25, 2013

Presenting images in Android fast

The idea here was to be able to display very fast a slideshow of all the images in a directory. So, in principle I didn't want to be loading from flash/disk while displaying, therefore I wanted to save them in memory.

Top level, the app will simply store the images in some kind of memory structure and then, periodically, display one. The periodicity is done with an AsyncTask acting as timer. I.e., we call the task, in the task we have a 50ms wait (Thread.sleep(50);) and then when done we send a message back to the UI, which displays the new image, calls the task again and so on... See code below...

For the display of the image, we simply assign a Bitmap to the ImageView object using ImageView.setImageBitmap

So, finally, the question is how do we store all the images in memory for a quick retrieval when we need them. I show here two methods. Either one seems to work fine. The key is to be aware that we have limited memory resources and that Bitmaps can be quite big. To figure the resources use:
  1. final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
  2. Log.v("MyActivity","maxMemory: "+maxMemory);
which in my case (HTC ONE) returns: 196608 (that is 196MBytes).

If we start loading the pictures that the same phone has taken, each one will take 2688*1520*4 Bytes! We can see this by looking at the LogCat when we run the apps below. We see a message saying "Grow heap to XXX for 16343056-byte allocation". So, bottom line, either one of the two methods below will work only if the amount of images you have x the resolution of each one is kept within some boundaries... You can, of course, save the images in full resolution and resize them within your app or save them, to start with, in lower resolution (that is what I did here, using 640x360).

So, now to the two methods to store the pics. The first one uses simply an array of Bitmaps (duh!). PicActivity.java:
 package com.cell0907.pic;  
 import java.io.File;  
 import java.util.Random;  
 import com.cell0907.pic.R;  
 import android.os.Bundle;  
 import android.os.Environment;  
 import android.os.Handler;  
 import android.app.Activity;  
 import android.graphics.Bitmap;  
 import android.graphics.BitmapFactory;  
 import android.util.Log;  
 import android.widget.ImageView;  
 public class PicActivity extends Activity {  
      private Bitmap[] mMemoryCache; // A place to store our pics       
      private ImageView picture;  
      public static final int DONE=1;  
      private int numberofitems;  
      int i;  
      long startTime,stopTime;  
      @Override  
      protected void onCreate(Bundle savedInstanceState) {  
           super.onCreate(savedInstanceState);  
           setContentView(R.layout.activity_pic);  
           String root = Environment.getExternalStorageDirectory().toString();  
           File myDir = new File(root + "/DCIM/3D");   
     picture=(ImageView)findViewById(R.id.imageView1);  
     // FOR LOOP TO LOAD ALL THE PICS IN CACHE  
        File[] file_list = myDir.listFiles();   
        numberofitems=file_list.length;  
        mMemoryCache=new Bitmap[numberofitems];  
        Log.v("MyActivity","items: "+numberofitems);  
        for (int i=0;i<numberofitems;i++){  
             mMemoryCache[i]=BitmapFactory.decodeFile(file_list[i].getPath());  
        }        
     // RANDOM ACCESS TO PRESENT THE PICS VERY FAST  
        // We do this in a separate task, when finishes sends a message, the handler  
        // presents the image and send the task again...  
        i=0;  
        new Timer(getApplicationContext(),threadHandler).execute();  
      }  
      ////////////////////////////////////thread Handler///////////////////////////////////////  
   private Handler threadHandler = new Handler() {  
        public void handleMessage(android.os.Message msg) {       
             switch(msg.what){  
                     case DONE:  
                          //Random r = new Random();  
                       //int i=r.nextInt(numberofitems);   
                          startTime = System.nanoTime();  
                       picture.setImageBitmap(mMemoryCache[i]);  
                       i++;  
                       if (i==numberofitems) i=0;  
                       //if (i==4) i=0;  
                       long endTime = System.nanoTime();  
                       System.out.println(String.format("Elapsed time: %.2f ms", (float)(endTime - startTime)/1000000));  
                          new Timer(getApplicationContext(),threadHandler).execute();  
                          break;                           
             }  
        }  
   };  
 }  

And for the timer portion we will do (Timer.java):
 package com.cell0907.pic;  
 import android.content.Context;  
 import android.os.Handler;  
 import android.os.Message;  
 import android.util.Log;  
 import android.os.AsyncTask;  
 public class Timer extends AsyncTask<Void, Void, Void> {  
   Context mContext;  
      private Handler threadHandler;  
   public Timer(Context context,Handler threadHandler) {  
     super();  
     this.threadHandler=threadHandler;  
     mContext = context;  
       }  
   @Override  
      protected Void doInBackground(Void...params) {   
        try {  
                Thread.sleep(50);  
           } catch (InterruptedException e) {  
                // TODO Auto-generated catch block  
                e.printStackTrace();  
           }   
        Message.obtain(threadHandler, PicActivity.DONE, "").sendToTarget();   
         return null;  
   }  
 }  

AndroidManifest.xml:
 <?xml version="1.0" encoding="utf-8"?>  
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
   package="com.cell0907.pic"  
   android:versionCode="1"  
   android:versionName="1.0" >  
   <uses-sdk  
     android:minSdkVersion="12"  
     android:targetSdkVersion="17" />  
   <application  
     android:allowBackup="true"  
     android:icon="@drawable/ic_launcher"  
     android:label="@string/app_name"  
     android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >  
     <activity  
       android:name="com.cell0907.pic.PicActivity"  
       android:label="@string/app_name"   
       android:screenOrientation="landscape">  
       <intent-filter>  
         <action android:name="android.intent.action.MAIN" />  
         <category android:name="android.intent.category.LAUNCHER" />  
       </intent-filter>  
     </activity>  
   </application>  
 </manifest>  

And the layout activity_pic.xml:
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   xmlns:tools="http://schemas.android.com/tools"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent"  
   tools:context=".PicActivity" >  
   <ImageView  
     android:id="@+id/imageView1"  
     android:layout_width="match_parent"  
     android:layout_height="match_parent"  
     android:layout_centerHorizontal="true"  
     android:layout_centerVertical="true"  
     android:src="@drawable/ic_launcher" />  
 </RelativeLayout>  

We profile the execution to see if there is any difference between this method and the next:
12-25 14:03:25.469: I/System.out(15336): Elapsed time: 0.70 ms
12-25 14:03:25.519: I/System.out(15336): Elapsed time: 0.24 ms
12-25 14:03:25.579: I/System.out(15336): Elapsed time: 0.61 ms
12-25 14:03:25.629: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:25.679: I/System.out(15336): Elapsed time: 0.21 ms
12-25 14:03:25.729: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:25.779: I/System.out(15336): Elapsed time: 0.15 ms
12-25 14:03:25.839: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:25.889: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:25.940: I/System.out(15336): Elapsed time: 0.15 ms
12-25 14:03:25.990: I/System.out(15336): Elapsed time: 0.15 ms
12-25 14:03:26.040: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:26.090: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:26.140: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:26.190: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:26.240: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:26.300: I/System.out(15336): Elapsed time: 0.21 ms

For the second method, I wanted to use the LruCache class.
 package com.cell0907.pic;  
 import java.io.File;  
 import java.util.Random;  
 import com.cell0907.pic.R;  
 import android.os.Bundle;  
 import android.os.Environment;  
 import android.os.Handler;  
 import android.app.Activity;  
 import android.graphics.Bitmap;  
 import android.graphics.BitmapFactory;  
 import android.support.v4.util.LruCache;  
 import android.util.Log;  
 import android.widget.ImageView;  
 public class PicActivity extends Activity {  
      private LruCache<String, Bitmap> mMemoryCache; // A place to store our pics       
      private ImageView picture;  
      public static final int DONE=1;  
      private int numberofitems;  
      int i;  
      long startTime,endTime;  
      @Override  
      protected void onCreate(Bundle savedInstanceState) {  
           super.onCreate(savedInstanceState);  
           setContentView(R.layout.activity_pic);  
           String root = Environment.getExternalStorageDirectory().toString();  
           File myDir = new File(root + "/DCIM/3D");   
     picture=(ImageView)findViewById(R.id.imageView1);  
     // SETUP THE CACHE  
        // Get max available VM memory, exceeding this amount will throw an  
        // OutOfMemory exception. Stored in kilobytes as LruCache takes an  
        // int in its constructor.  
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  
        Log.v("MyActivity","maxMemory: "+maxMemory);  
        // Use half of the available memory at max for this memory cache.  
        final int cacheSize = maxMemory / 2;  
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {  
          @Override  
          protected int sizeOf(String key, Bitmap bitmap) {  
            // The cache size will be measured in kilobytes rather than  
            // number of items.  
            return bitmap.getByteCount() / 1024;  
          }  
        };  
     // FOR LOOP TO LOAD ALL THE PICS IN CACHE  
        File[] file_list = myDir.listFiles();   
        numberofitems=file_list.length;  
        Log.v("MyActivity","items: "+numberofitems);  
        String imageKey;  
        for (int i=0;i<numberofitems;i++){  
             imageKey = String.valueOf(i);  
             addBitmapToMemoryCache(imageKey,BitmapFactory.decodeFile(file_list[i].getPath()));  
        }        
     // RANDOM ACCESS TO PRESENT THE PICS VERY FAST  
        // We do this in a separate task, when finishes sends a message, the handler  
        // presents the image and send the task again...  
        i=0;  
        new Timer(getApplicationContext(),threadHandler).execute();  
      }  
      public void addBitmapToMemoryCache(String key, Bitmap bitmap) {  
        if (getBitmapFromMemCache(key) == null) {  
          mMemoryCache.put(key, bitmap);  
        }  
      }  
      public Bitmap getBitmapFromMemCache(String key) {  
        return mMemoryCache.get(key);  
      }  
      ////////////////////////////////////thread Handler///////////////////////////////////////  
   private Handler threadHandler = new Handler() {  
        public void handleMessage(android.os.Message msg) {       
             switch(msg.what){  
                     case DONE:  
                       //Random r = new Random();  
                       //int i=r.nextInt(numberofitems);  
                       startTime = System.nanoTime();  
                       picture.setImageBitmap(getBitmapFromMemCache(String.valueOf(i)));  
                       i++;  
                       if (i==numberofitems) i=0;  
                       //if (i==4) i=0;  
                       long endTime = System.nanoTime();  
                       System.out.println(String.format("Elapsed time: %.2f ms", (float)(endTime - startTime)/1000000));  
                       new Timer(getApplicationContext(),threadHandler).execute();  
                          break;  
             }  
        }  
   };  
 }  

For the timer, manifest and layout we used the same as the first case. Notice that in both cases I had provision to display the pics in random access. I left it there commented out, just for reference...

Profiling this 2nd method, it seems that, curiously, this method actually seems to be slower than simple array of Bitmaps!
12-25 13:58:43.408: I/System.out(14553): Elapsed time: 0.37 ms
12-25 13:58:43.458: I/System.out(14553): Elapsed time: 0.37 ms
12-25 13:58:43.508: I/System.out(14553): Elapsed time: 0.34 ms
12-25 13:58:43.558: I/System.out(14553): Elapsed time: 0.92 ms
12-25 13:58:43.608: I/System.out(14553): Elapsed time: 0.34 ms
12-25 13:58:43.668: I/System.out(14553): Elapsed time: 0.43 ms
12-25 13:58:43.718: I/System.out(14553): Elapsed time: 0.37 ms
12-25 13:58:43.768: I/System.out(14553): Elapsed time: 0.34 ms
12-25 13:58:43.819: I/System.out(14553): Elapsed time: 0.46 ms

Oh well, good to know... I just wonder why then somebody would use the cache approach (?)
Cheers!!

PS.: Please, click here to see an index of other posts on Android. 

Monday, December 23, 2013

From Mat to BufferedImage

There has been couple of comments on the posts around the Mat and BufferedImage classes (see here). So, I took a step back to understand them better. These names refer to the array of pixels in the image in OpenCV  (Mat) or in Java (BufferedImage) and the question comes on how to go from one to the other efficiently. The main reason I needed that was because to display the image processed in OpenCV in Java I had to transform it to BufferedImage first (there is no OpenCV imshow available).

Here is all what you wanted to know about Mat... but it doesn't talk much about what you put inside (the images itself), so for an easier ride (hopefully), keep reading: :)

You can read one element with get(x,y), with x/y the position of the element or a group of elements with get(x,y,byte[]) with x,y=0,0 (for the full array; does it indicate the origin?). The result will get stored in the argument array byte[]. You can change one element with put, in reverse way as get... See example here.

On the other side, BufferedImage is a java subclass that describes an image with an accessible buffer of image data. A BufferedImage is comprised of a ColorModel and a Raster of image data. The 2nd is what holds the image pixels. See how to access them here.

To answer how to read/set the pixels on those structures, we will try to answer the title of this post, i.e., how to go from Mat (the openCV resulting image) to BufferedImage (to display), and do it in the most efficient way. Therefore, we proceed to benchmark few methods (see full code here). Hint: if you want to skip all the reading, jump to Method 4 :)

Method 1 Go through a jpg. Although looks the cleanest code (actually suggested by one commenter), another commenter thought that this would take the highest computation effort, and basically trigger this whole post :).

public boolean MatToBufferedImage(Mat matrix) {  
       long startTime = System.nanoTime();  
       MatOfByte mb=new MatOfByte();  
       Highgui.imencode(".jpg", matrix, mb);  
       try {  
            image = ImageIO.read(new ByteArrayInputStream(mb.toArray()));  
       } catch (IOException e) {  
       // TODO Auto-generated catch block  
            e.printStackTrace();  
            return false; // Error  
       }  
       long endTime = System.nanoTime();  
       System.out.println(String.format("Elapsed time: %.2f ms", (float)(endTime - startTime)/1000000));  
       return true; // Successful  
}  

Detected 2 faces
Elapsed time: 25.94 ms
Detected 1 faces
Elapsed time: 27.56 ms
Detected 2 faces
Elapsed time: 27.37 ms
Detected 1 faces
Elapsed time: 26.96 ms
Detected 1 faces
Elapsed time: 35.70 ms
Detected 1 faces
Elapsed time: 27.32 ms

Method 2 Extract the data from Mat into an array, flip the blue and the red channels/columns and store in BufferedImage.

 public boolean MatToBufferedImage(Mat matrix) {  
        long startTime = System.nanoTime();  
        int cols = matrix.cols();  
        int rows = matrix.rows();  
        int elemSize = (int)matrix.elemSize();  
        byte[] data = new byte[cols * rows * elemSize];  
        int type;  
        matrix.get(0, 0, data);  
        switch (matrix.channels()) {  
          case 1:  
            type = BufferedImage.TYPE_BYTE_GRAY;  
            break;  
          case 3:   
            type = BufferedImage.TYPE_3BYTE_BGR;  
            // bgr to rgb  
            byte b;  
            for(int i=0; i<data.length; i=i+3) {  
              b = data[i];  
              data[i] = data[i+2];  
              data[i+2] = b;  
            }  
            break;  
          default:  
            return false; // Error  
        }  
        image = new BufferedImage(cols, rows, type);  
        image.getRaster().setDataElements(0, 0, cols, rows, data);  
        long endTime = System.nanoTime();  
        System.out.println(String.format("Elapsed time: %.2f ms", (float)(endTime - startTime)/1000000));  
        return true; // Successful  
}  

Detected 2 faces
Elapsed time: 2.27 ms
Detected 2 faces
Elapsed time: 2.81 ms
Detected 2 faces
Elapsed time: 2.25 ms
Detected 2 faces
Elapsed time: 2.75 ms
Detected 2 faces
Elapsed time: 2.22 ms

Substantial (10x!!) improvement, as the anonymous commenter had anticipated...

Method 3 We do the color conversion in OpenCV (see here for color conversions within OpenCV) and then save to BufferedImage:

public boolean MatToBufferedImage(Mat matrix) {  
        long startTime = System.nanoTime();  
        Imgproc.cvtColor(matrix, matrix, Imgproc.COLOR_BGR2RGB);   
        int cols = matrix.cols();  
        int rows = matrix.rows();  
        int elemSize = (int)matrix.elemSize();  
        byte[] data = new byte[cols * rows * elemSize];  
        matrix.get(0, 0, data);  
        image = new BufferedImage(cols, rows, BufferedImage.TYPE_3BYTE_BGR);  
        image.getRaster().setDataElements(0, 0, cols, rows, data);  
        long endTime = System.nanoTime();  
        System.out.println(String.format("Elapsed time: %.2f ms", (float)(endTime - startTime)/1000000));  
        return true; // Successful  
}  

Detected 2 faces
Elapsed time: 2.19 ms
Detected 2 faces
Elapsed time: 12.68 ms
Detected 3 faces
Elapsed time: 2.04 ms
Detected 2 faces
Elapsed time: 2.91 ms
Detected 3 faces
Elapsed time: 2.05 ms
Detected 2 faces
Elapsed time: 2.84 ms

Maybe slightly faster than doing it by hand but... Notice also the long 12ms case above. Nevertheless, I observed that in the other algorithms too, so, I feel that is because the processor gets distracted with some other function...

Method 4 Finally, we get to what is the most efficient method. It basically goes straight from the BGR to the BufferedImage.

public boolean MatToBufferedImage(Mat matBGR){  
      long startTime = System.nanoTime();  
      int width = matBGR.width(), height = matBGR.height(), channels = matBGR.channels() ;  
      byte[] sourcePixels = new byte[width * height * channels];  
      matBGR.get(0, 0, sourcePixels);  
      // create new image and get reference to backing data  
      image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);  
      final byte[] targetPixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();  
      System.arraycopy(sourcePixels, 0, targetPixels, 0, sourcePixels.length);  
      long endTime = System.nanoTime();  
      System.out.println(String.format("Elapsed time: %.2f ms", (float)(endTime - startTime)/1000000));  
      return true;  
}  

In this 4th method suggested by the anonymous commenter we get the best. Even up to 50x improvement respect to the method 1:
Detected 2 faces
Elapsed time: 0.51 ms
Detected 2 faces
Elapsed time: 1.22 ms
Detected 1 faces
Elapsed time: 0.47 ms
Detected 1 faces
Elapsed time: 1.32 ms
Detected 1 faces
Elapsed time: 0.48 ms
Detected 1 faces
Elapsed time: 1.77 ms

I still need to understand why this other method does not require to flip R and B, but hey, it works... So, thank you, sir! :)
Cheers!!

PS.: This other post talks about how to get the picture in BufferedImage into an array and does a nice work benchmarking two methods, either using getRGB or using ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
PS2.: More theory on BufferedImages.
PS3.: By the way, the method may be nicer if it was returning the image instead of accessing it as a variable of the mother class, but anyhow...