Showing posts with label OpenCV. Show all posts
Showing posts with label OpenCV. Show all posts

Saturday, July 9, 2016

Head tracking off-the-shelf solutions

So had a quick look at how to track the head position, just to see what is out there. Some quick findings:

Through face tracking:
Besides the theta/phi that helps present the image from the right angle, we can probably find out the R from the size of the face. Not trivial, will have to see how the effect works out.
  1. Of course, we can use OpenCV and all the stuff presented in this blog (only for the position, not so much for rotation).
  2. FaceTracknoIR Open source face tracking for Windows. It tracks rotation too...
With markers in the head: Either passive (reflective) or active (LED as light source):
  • Tutorials:
    1. This guy talks too much too but the slides he shows makes everything funnier...
    2. I feel this guy talks too much.
  • Solutions: 
    1. FreeTrack (software)
    2. OpenTrack (software)
    3. LinuxTrack (software)
    4. TrackIR (both software and hardware, ~$150)
    5. Sony Playstation Move (I include here as it is cheaper than the Vive or Occulus systems...) and the Playstation Camera. Probably these would give you anything beyond position.
    6. TrackHat buy the hardware, use the free software (spin from OpenTrack).
    7. DIY hardware Mostly about the mechanical part... The electronics is basic if you know anything about it (the LEDs are just DC, not pulsed or anything fancy...)
    8. These guys did something very similar to what I had in mind (kind of a virtual window). It was probably inspired in both cases on the great and open job of Johnny Chung Lee
    Comparison of both methods (not a good one...)

    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");

    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...

    Friday, November 8, 2013

    OpenCV: How to get started and index of posts

    So, you want to do some cool image processing... so, here is a very basic explanation on how to get started with link/index to the stuff I posted so far.

    Where to start

    Basically OpenCV is a set of nice libraries that will highly expedite a lot of your work. See more detail here. You will need to call them from your program, say in C/C++ or Java.

    To develop the program you will need an IDE (integrated development environment) which takes care of editing (more than a text editor, with nice support features), and allows you to "plug in", compilers, debuggers, etc... so that you can run everything from there. In my case, I am using Eclipse. Notice that you could use a different IDE. During configuration of the IDE, you tell it what compiler to use, etc... So that when you press the compile button, Eclipse calls it to generate the object file... I think you get the point...

    So, install Eclipse if you don't have it. Of course, you could choose a different IDE, but sorry, no experience on that, and honestly, I have no complains about Eclipse. Great tool. In my case, I did the installation when I was planning to use it with Qt (a set of widgets for something that I was doing completely unrelated). Still, the initial portion of the installation is the same. You can see my installation experience here.

    So, what do I choose, C/C++ or Java?

    Not an expert on this but I believe that a lot of the initial stuff on OpenCV was done for C development. Nevertheless, I think that almost everything in OpenCV has a port to Java already. I.e., there are packages in Java equivalent to libraries in C. So, probably depends more on what language you feel comfortable with.

    One thing I was thinking (and I was wrong) is that if I want to use OpenCV in Android, I better get a handle of the Java version, as that is the language to develop in that platform (see this overview on Android programming environment). Nevertheless, that it's not that truth. In Android development, there is something called the NDK which allows you to call pieces of C code (native code) from the java code (see more info here on the NDK). The funny part is that in the end, as OpenCV is pretty processing intensive, you may want to use the NDK for the OpenCV routines (native code runs much faster than java code, if you do things properly...). Bottom line, I went down the path of working with OpenCV in Java, but probably I (and you, if you prefer) could have saved part of that work, as I was more familiar with C. But as I went down that path, I end-up with Eclipse supporting both languages (that is done with the "views"). So, to install each of the languages to work with OpenCV in a PC CPU, follow:
    1. Installation to work with C/C++
    2. Installation to work with Java
    To develop for an Android machine, follow this.

    Note: The 3 links above are in the order of what I historically did on my case. I.e., the 3 steps may not be completely independent of each other. For instance, something got installed in step 2 that is needed for Android, but I never realized and if I had done only the third, it would not have worked. Please let me know if you got any trouble...

    Coding examples

    So, now that you got it installed, let's do some coding:
    1. As part of the installation of OpenCV in Java (the same link as above) we did a first tutorial where we detect faces in an image file in a PC.
    2. Creating windows in Java and drawing in them. We strip everything to get familiar with the java drawing/workings. There is no OpenCV here. I read an image file from disk and display it. No detection yet.
    3. Next, we create windows and capture webcam, i.e., not from a file, like in #1. Although we do not do any image processing, we do use OpenCV structures to be ready for the next step (processing). 
    4. One of the keys on that post is how to take a Mat structure from OpenCV and pass it to BufferedImage, for display in Java. I created a post just for that.
    5. And into image processing, we now detect the faces on the webcam stream. Notice that this is very similar (a port to Java) of the original C tutorial
    6. Then we move into detecting a ball in the image. Part of the detection is based on color. So, you got to understand the color space. See this.
    7. And then we track the ball with the PC camera/CPU.
    8. Then we start moving into Android with a first app using OpenCV.
    9. Here we track faces with OpenCV in Android, using the smart phone camera. And here I use the Android SDK for the same thing.
    10. Using the face tracking I created a 3D display, which seems to be a similar technique to what Amazon is going to be using on their phone.
    11. And finally, we port the tracking of the ball to Android, i.e., we use the smart phone camera/CPU.
    12. The last final thing that I had in plan but haven't done yet is to actually port it to Android but have the OpenCV CPU intensive routines done in a C library, with the NDK.
    For the next posts I am going to be concentrating in Android development, not related to OpenCV, but I'll be back :)

    Cheers!!

    Sunday, August 4, 2013

    Tracking a ball in Android, with OpenCV

    I am going to port to Android the example we did in PC/Java with OpenCV, about tracking a ball.
    As that app was displaying different levels of processing of the incoming camera stream, it looks like a good idea to start from the second tutorial that comes with OpenCV. I follow these steps in order to make a copy of that project and start editing it.
      Note: we do not want to use jni at this time, but the tutorial 2 actually was doing so. I feel like leaving it in there without calling any of it... (so we can grow it later).

      The following is the code running OpenCV ball detection, all in java, no native, and (this is not good, so, will work on it later), running on the main/UI thread. I.e., if your phone is not powerful enough, it may just hang... Also, obviously, I want to port a lot of that processing into native, so, that I expedite things. There are also some other issues with the detection, but will work on a clean up version later.

      This is my Ball3Activity.java
       package com.cell0907.ball3;  
       import java.util.ArrayList;  
       import java.util.List;  
       import org.opencv.android.BaseLoaderCallback;  
       import org.opencv.android.CameraBridgeViewBase;  
       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.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.imgproc.Imgproc;  
       import org.opencv.core.Point;  
       import android.app.Activity;  
       import android.os.Bundle;  
       import android.util.Log;  
       import android.view.Menu;  
       import android.view.MenuItem;  
       import android.view.WindowManager;  
       public class Ball3Activity extends Activity implements CvCameraViewListener2 {  
         private static final String  TAG = "OCVSample::Activity";  
         private static final int    VIEW_MODE_RGBA   = 0;  
         private static final int    VIEW_MODE_GRAY   = 1;  
         private static final int    VIEW_MODE_CANNY  = 2;  
         private static final int    VIEW_MODE_FEATURES = 5;  
         private int          mViewMode;  
         private Mat          mRgba;  
         private Mat          mIntermediateMat;  
         private Mat          mGray;  
         private Mat                           mHSV;  
         private Mat                           mThresholded;  
         private Mat                           mThresholded2;  
            private Mat                       array255;  
            private Mat                       distance;  
         private MenuItem        mItemPreviewRGBA;  
         private MenuItem        mItemPreviewGray;  
         private MenuItem        mItemPreviewCanny;  
         private MenuItem        mItemPreviewFeatures;  
         private CameraBridgeViewBase  mOpenCvCameraView;  
         private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {  
           @Override  
           public void onManagerConnected(int status) {  
             switch (status) {  
               case LoaderCallbackInterface.SUCCESS:  
               {  
                 Log.i(TAG, "OpenCV loaded successfully");  
                 // Load native library after(!) OpenCV initialization  
                 //System.loadLibrary("mixed_sample");  
                 mOpenCvCameraView.enableView();  
               } break;  
               default:  
               {  
                 super.onManagerConnected(status);  
               } break;  
             }  
           }  
         };  
         public Ball3Activity() {  
           Log.i(TAG, "Instantiated new " + this.getClass());  
         }  
         /** Called when the activity is first created. */  
         @Override  
         public void onCreate(Bundle savedInstanceState) {  
           Log.i(TAG, "called onCreate");  
           super.onCreate(savedInstanceState);  
           getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);  
           setContentView(R.layout.tutorial2_surface_view);  
           mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.tutorial2_activity_surface_view);  
           mOpenCvCameraView.setCvCameraViewListener(this);  
         }  
         @Override  
         public boolean onCreateOptionsMenu(Menu menu) {  
           Log.i(TAG, "called onCreateOptionsMenu");  
           mItemPreviewRGBA = menu.add("RGBA");  
           mItemPreviewGray = menu.add("HSV");  
           mItemPreviewCanny = menu.add("Thresholded");  
           mItemPreviewFeatures = menu.add("Ball");  
           return true;  
         }  
         @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) {  
           mRgba = new Mat(height, width, CvType.CV_8UC4);  
           mHSV = new Mat(height, width, CvType.CV_8UC4);  
           mIntermediateMat = new Mat(height, width, CvType.CV_8UC4);  
           mGray = new Mat(height, width, CvType.CV_8UC1);  
           array255=new Mat(height,width,CvType.CV_8UC1);  
           distance=new Mat(height,width,CvType.CV_8UC1);  
           mThresholded=new Mat(height,width,CvType.CV_8UC1);  
           mThresholded2=new Mat(height,width,CvType.CV_8UC1);  
         }  
         public void onCameraViewStopped() {  
           mRgba.release();  
           mGray.release();  
           mIntermediateMat.release();  
         }  
         public Mat onCameraFrame(CvCameraViewFrame inputFrame) {  
              final int viewMode = mViewMode;  
              mRgba = inputFrame.rgba();  
              if (viewMode==VIEW_MODE_RGBA) return mRgba;  
                 List<Mat> lhsv = new ArrayList<Mat>(3);             
                 Mat circles = new Mat(); // No need (and don't know how) to initialize it.  
                                                // The function later will do it... (to a 1*N*CV_32FC3)  
                 array255.setTo(new Scalar(255));  
                 Scalar hsv_min = new Scalar(0, 50, 50, 0);  
                 Scalar hsv_max = new Scalar(6, 255, 255, 0);  
                 Scalar hsv_min2 = new Scalar(175, 50, 50, 0);  
                 Scalar hsv_max2 = new Scalar(179, 255, 255, 0);  
                 //double[] data=new double[3];  
              // One way to select a range of colors by Hue  
              Imgproc.cvtColor(mRgba, mHSV, Imgproc.COLOR_RGB2HSV,4);  
              if (viewMode==VIEW_MODE_GRAY) return mHSV;  
              Core.inRange(mHSV, hsv_min, hsv_max, mThresholded);                     
              Core.inRange(mHSV, hsv_min2, hsv_max2, mThresholded2);   
              Core.bitwise_or(mThresholded, mThresholded2, mThresholded);  
              /*Core.line(mRgba, new Point(150,50), new Point(202,200), new Scalar(100,10,10)CV_BGR(100,10,10), 3);  
                   Core.circle(mRgba, new Point(210,210), 10, new Scalar(100,10,10),3);  
                   data=mRgba.get(210, 210);  
                   Core.putText(mRgba,String.format("("+String.valueOf(data[0])+","+String.valueOf(data[1])+","+String.valueOf(data[2])+")"),new Point(30, 30) , 3 //FONT_HERSHEY_SCRIPT_SIMPLEX  
                         ,1.0,new Scalar(100,10,10,255),3);*/  
              // Notice that the thresholds don't really work as a "distance"  
              // Ideally we would like to cut the image by hue and then pick just  
              // the area where S combined V are largest.   
              // Strictly speaking, this would be something like sqrt((255-S)^2+(255-V)^2)>Range  
              // But if we want to be "faster" we can do just (255-S)+(255-V)>Range  
              // Or otherwise 510-S-V>Range  
              // Anyhow, we do the following... Will see how fast it goes...  
              Core.split(mHSV, lhsv); // We get 3 2D one channel Mats  
              Mat S = lhsv.get(1);  
              Mat V = lhsv.get(2);  
              Core.subtract(array255, S, S);  
              Core.subtract(array255, V, V);  
              S.convertTo(S, CvType.CV_32F);  
              V.convertTo(V, CvType.CV_32F);  
              Core.magnitude(S, V, distance);  
              Core.inRange(distance,new Scalar(0.0), new Scalar(200.0), mThresholded2);  
              Core.bitwise_and(mThresholded, mThresholded2, mThresholded);  
       /*       if (viewMode==VIEW_MODE_CANNY){  
                   Imgproc.cvtColor(mThresholded, mRgba, Imgproc.COLOR_GRAY2RGB, 4);  
                   return mRgba;   
              }*/  
              // Apply the Hough Transform to find the circles  
              Imgproc.GaussianBlur(mThresholded, mThresholded, new Size(9,9),0,0);  
              Imgproc.HoughCircles(mThresholded, circles, Imgproc.CV_HOUGH_GRADIENT, 2, mThresholded.height()/4, 500, 50, 0, 0);       
              if (viewMode==VIEW_MODE_CANNY){  
                   Imgproc.Canny(mThresholded, mThresholded, 500, 250); // This is not needed.  
                                                                                   // It is just for display  
                   Imgproc.cvtColor(mThresholded, mRgba, Imgproc.COLOR_GRAY2RGB, 4);  
                   return mRgba;   
              }   
              //int cols = circles.cols();  
               int rows = circles.rows();  
               int elemSize = (int)circles.elemSize(); // Returns 12 (3 * 4bytes in a float)  
               float[] data2 = new float[rows * elemSize/4];  
               if (data2.length>0){  
                    circles.get(0, 0, data2); // Points to the first element and reads the whole thing  
                                                    // into data2  
                    for(int i=0; i<data2.length; i=i+3) {  
                         Point center= new Point(data2[i], data2[i+1]);  
                         Core.ellipse( mRgba, center, new Size((double)data2[i+2], (double)data2[i+2]), 0, 0, 360, new Scalar( 255, 0, 255 ), 4, 8, 0 );  
                  }  
                }  
              return mRgba;  
         }  
         public boolean onOptionsItemSelected(MenuItem item) {  
           Log.i(TAG, "called onOptionsItemSelected; selected item: " + item);  
           if (item == mItemPreviewRGBA) {  
             mViewMode = VIEW_MODE_RGBA;  
           } else if (item == mItemPreviewGray) {  
             mViewMode = VIEW_MODE_GRAY;  
           } else if (item == mItemPreviewCanny) {  
             mViewMode = VIEW_MODE_CANNY;  
           } else if (item == mItemPreviewFeatures) {  
             mViewMode = VIEW_MODE_FEATURES;  
           }  
           return true;  
         }  
         //public native void FindFeatures(long matAddrGr, long matAddrRgba);  
       }  
      

      And this is my 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" >  
         <org.opencv.android.JavaCameraView  
           android:layout_width="fill_parent"  
           android:layout_height="fill_parent"  
           android:id="@+id/tutorial2_activity_surface_view"  
           opencv:show_fps="true"  
           opencv:camera_id="any" />  
       </LinearLayout>  
      

      Notice the use of opencv:show_fps="true" to see the fps on screen. To include this line, you will need to also add on top xmlns:opencv="http://schemas.android.com/apk/res-auto". On my HTC One, the fps will go from 10fps when just capturing the images down to barely 2 when is doing all the processing...

      PS.: Click here to see the index of these series of posts on OpenCV

      Monday, July 22, 2013

      OpenCV in Android - First app

      This should be short. The tutorial here is straightforward and worked fine for me. Only thing is that it doesn't explain some minor details that then I found in another sample code and pasted over:
      1. The activity should implement CvViewFrameListener2: "public class HelloOpenCvActivity extends Activity implements CvCameraViewListener2 {"...
      2. They do not show some of the creation/initialization of variables in the class: "    private static final String TAG = "OCVSample::Activity";    private CameraBridgeViewBase mOpenCvCameraView;"
      That should do... We get about 8fps... Wonder if that is all I can get...  
      Notice that no use of the NDK was necessary...

      Another sample/tutorial here

      PS.: Click here to see the index of these series of posts on OpenCV

      Sunday, July 21, 2013

      OpenCV in Android - Quick install guide

      I found a good explanation of OpenCV in Android and the inner workings of calling this library... I feel it is a must read before you start. Will save you work/time.

      Then we go into the tutorial. Top level, check this first. Then you need to download/add the OpenCV for Android library. Then create a new workspace, this library should be one of the projects on the workspace, and any OpenCV Android project will refer to that project. When you run it, your apk gets loaded and a Manager (that gets downloaded the first time, if you don't have it in your phone), loads the library apk.

      Still trying to wrap my brain around this, but here are the details of how I did till I got to run one of the sample demos (puzzle15). Follow the tutorial and if that doesn't work, read below to see my experience...

      We have stuff installed from the past (like Eclipse, Basic4Android, java...). So, we need to figure out how to make it work without reinstalling or creating a parallel copy of all this (maybe only alternative).

       Sun JDK 6 or higher:
      1. I seem to have 7 (jdk1.7.0_03). JDK stands for Java Development Kit. Check in Program Files/Java...
      2. I also have JRE6 and JRE7. JRE stands for Java Runtime Environment, which contains  everything required to run Java applications on your system. Do I really need both?

      Android SDK:

      This is nicely documented by the Android folks. Just look around, as I guess pages may change, but I found this and this.
      1. Got an old installation (about a year ago) that I have been leveraging with Basic4Android. So, I launched my Android SDK Manager and selected the updates. The screen shows Android 4.2.2 (API17) empty when I had Android 4.1 (API16), indicating possible updates. I clicked the updates (not a new install) and the Manager suggested other dependencies I needed to get...
      2. Nevertheless, I think I still should install the new API17 (the updates just update the old API16). This is what the SDK Manager looks like:
      3. To develop in Eclipse, I need the ADT plug-in. Follow the instructions there, but remember that if you want to develop in Android with C++ (which is likely for any kind of intensive processing), you should get also the NDK. This is how my Eclipse - Help - About Eclipse looks like...
      4. Get the NDK. Follow links... Installation is just to unzip anywhere...
      5. At this moment, things seem to look very complicated (maybe because of the NDK), so, we go build a simple first app in Android, nothing to do with OpenCV. Just to make sure all the above installation is working... If you do not need this, jump to #8.
      6. While doing this, Eclipse didn't detect the HTC One at launch. Also, if you run the adb (Android Debug Bridge, go to command prompt and type adb devices) nothing shows up. In the Windows Device Manager the driver shows nicely, but it is NOT the one we need. Found a solution here. You should see at the TOP of the device manager "Android USB devices". If not, keep uninstalling driver and installing the right one till it does show up like that. What is the right one it is tough to say. Initially it worked when I got the USB Composite Device. But lately works with "My HTC". Key seems to be to have an "Android USB devices" at the top of the list. Now the adb  will see it... Restart Eclipse and now the launch should show the phone! Note, during this whole thing, I uninstalled HTC sync (but I don't think it had nothing to do with it). Other things that I am not sure they can work are here and here. A good summary of things to try is actually on the original tutorial we are following.
      7. Even after knowing the app launches, you may want to finish the tutorial. It is very useful and a nicely explained to quickly get a background on Android app development, if you didn't have one... Just as a quick note, it uses a intent to create the screen with the result of an app, so, that when you click the "back" key (which I think that is what he calls "up") it can go back to the app.
      8. Then we take this further by trying to use the NDK. We will go for the Hello-jni example which works right away. Note: JNI stands for Java Native Interface and it is a programming framework that enables Java code running in a Java Virtual Machine (JVM) to call, and to be called by, native applications (programs specific to a hardware and operating system platform) and libraries written in other languages such as C, C++ and assembly. To understand what is all this NDK about, look  in the NDK folder for a "docs" folder, and open the file OVERVIEW.html. Nice explanation.
      9. We do also the 2nd example, a fully native app. I.e., no java involved, only the .C with the NDK. Notice how the src file is empty.
         Notice that the original sample source only had the C stuff in jni folder and the res folder.
        Most of the final stuff (in "gen", etc...) is generated by Eclipse when creating the project from an existing source. Before running NDK... The ndk-build will create the binary of our native libraries and place them in the right places for Eclipse to use them...
      10. Anyhow, back to our OpenCV installation tutorial!! The above NDK tutorial shows how to include native code in your app. But you can build the native part of your application from within Eclipse. That is where this tutorial continues...
      11. It is a bit confusing, though, in some way... Maybe I missed it, but so far, we have not downloaded the OpenCV for Android SDK! Follow these instructions... Note: there are Android OpenCV samples on the original OpenCV package (not the specific one for Android but the one I was using in my previous examples), under "samples\android", but the tutorial can't assume that...
      12. The tutorial then goes into importing those samples. Either of the two tutorials (Introduction to Android Development or OpenCV Android SDK) does this, but it is not clear if we need to add the C Nature to the samples that don't even have a jni. I am not 100% it will even allow you to do it... but seems to ask for that.
      13. So, simply do the import to the point we see the projects in the workspace but nothing else. And we try to load puzzle15. Lots of errors on the package from OpenCV. Left click on the error and one of the solutions (all the way to the bottom) sounds like "fix project setup" or something like that. It will suggest to add OpenCV library - 2.4.6 to the build path. Notice that it is on the same workspace. These errors go away:
      14. Nevertheless, I was still having lots of errors, some of them on the OpenCV library project. As the other projects link to this one, they didn't run. So, after running the SDK manager and updating everything (which was just some minor stuff as I recently installed everything), I deleted the projects (not from the drive) and run again the import. Now everything looks clean... Don't ask me why...
      15. Still, when I launch the project, I get an error: "Could not find OpenCV-2.3.1.apk!". This was solved following this.
      16. So, now, when you launch it, the installer realizes that is missing a package and gives you the following notice in the phone "Package not found. Do you want to install it?" Answer yes and that launches Google play and looks for OpenCV Manager. Install it as any other app... The Manager will get the right openCV apk for your hardware.
      17. Now your app should run!!
      In the next post we will create our own app...

      PS.: Click here to see the index of these series of posts on OpenCV