TOP 顔認識 機械学習 Eigenfaces

OpenCV for Android

OpenCVの前にカメラの使い方

カメラを使ってみるため、https://developer.android.com/samples/からCamera2Basicをダウンロード。

Android Studioに"Import Project"で読み込む。

サンプルにはGradle Wrapperが含まれていて、指定のSDK build tool versionがインストールされていない場合、ダウンロードしてインストールするようになっている。 Gradle Toolのバージョンも指定があり、最新版に更新するようメッセージが出るが、どちらも指定通りのバージョンのままでビルドする。

カメラアプリとして動作することが確認できた。シャッター音がせず、無音カメラになる。

マニフェストファイルAndroidManifest.xmlから抜粋。

    <uses-permission android:name="android.permission.CAMERA" />

    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />

レイアウトファイルfragment_camera2_basic.xmlはオリジナルのままだとNGで、横向きにするとボタンが見えなくなる。

横向き修正版。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.android.camera2basic.AutoFitTextureView
        android:id="@+id/texture"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true" />

    <FrameLayout
        android:id="@+id/control"
        android:layout_width="112dp"
        android:layout_height="match_parent"
        android:layout_alignParentBottom="true"
        android:layout_alignParentEnd="true"
        android:background="@color/control_background"
        android:orientation="horizontal">

        <Button
            android:id="@+id/picture"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="@string/picture" />

        <ImageButton
            android:id="@+id/info"
            style="@android:style/Widget.Material.Light.Button.Borderless"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal|bottom"
            android:contentDescription="@string/description_info"
            android:padding="20dp"
            android:src="@drawable/ic_action_info" />


    </FrameLayout>

</RelativeLayout>

縦向き修正版。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.android.camera2basic.AutoFitTextureView
        android:id="@+id/texture"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true" />

    <FrameLayout
        android:id="@+id/control"
        android:layout_width="match_parent"
        android:layout_height="112dp"
        android:layout_alignParentBottom="true"
        android:layout_alignParentStart="true"
        android:background="@color/control_background">

        <Button
            android:id="@+id/picture"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="@string/picture" />

        <ImageButton
            android:id="@+id/info"
            android:contentDescription="@string/description_info"
            style="@android:style/Widget.Material.Light.Button.Borderless"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical|end"
            android:padding="20dp"
            android:src="@drawable/ic_action_info" />

    </FrameLayout>

</RelativeLayout>

Camera2BasicFragment

CameraActivity.javaはフラグメントを置き換えるだけなので、Camera2BasicFragment.javaのフラグメントを見る。

startBackgroundThread()でバックグラウンドスレッドを開始している。

    @Override
    public void onResume() {
        super.onResume();
        startBackgroundThread();
        if (mTextureView.isAvailable()) {
            openCamera(mTextureView.getWidth(), mTextureView.getHeight());
        } else {
            mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
        }
    }

スレッド

スレッド関連の構造を理解するため、ThreadLooperHandlerなど、詳しく追ってみる。

    private void startBackgroundThread() {
        mBackgroundThread = new HandlerThread("CameraBackground");
        mBackgroundThread.start();
        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
    }

まずHandlerThreadクラスのインスタンスを作っている。コンストラクタは

    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }

superクラスはThreadなので、

public Thread(String name) {
        init(null, null, name, 0);
    }

initメソッドは

private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
        Thread parent = currentThread();
        if (g == null) {
            g = parent.getThreadGroup();
        }

        g.addUnstarted();
        this.group = g;

        this.target = target;
        this.priority = parent.getPriority();
        this.daemon = parent.isDaemon();
        setName(name);

        init2(parent);

        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
        tid = nextThreadID();
    }

ここでは主に、

を行っている。

次に、

mBackgroundThread.start();

でスレッドを開始している。具体的には、

    public synchronized void start() {
        // Android-changed: throw if 'started' is true
        if (threadStatus != 0 || started)
            throw new IllegalThreadStateException();

        group.add(this);

        started = false;
        try {
            nativeCreate(this, stackSize, daemon);
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

nativeCreate()はAndroid Runtime (ART)のネイティブメソッドで、C++で記述されている。

static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size,
                                jboolean daemon) {
  // There are sections in the zygote that forbid thread creation.
  Runtime* runtime = Runtime::Current();
  if (runtime->IsZygote() && runtime->IsZygoteNoThreadSection()) {
    jclass internal_error = env->FindClass("java/lang/InternalError");
    CHECK(internal_error != nullptr);
    env->ThrowNew(internal_error, "Cannot create threads in zygote");
    return;
  }
  Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
}

上記はJNIで、本体のCreateNativeThreadクラスは以下。

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
  CHECK(java_peer != nullptr);
  Thread* self = static_cast<JNIEnvExt*>(env)->GetSelf();
  if (VLOG_IS_ON(threads)) {
    ScopedObjectAccess soa(env);
    ArtField* f = jni::DecodeArtField(WellKnownClasses::java_lang_Thread_name);
    ObjPtr<mirror::String> java_name =
        f->GetObject(soa.Decode<mirror::Object>(java_peer))->AsString();
    std::string thread_name;
    if (java_name != nullptr) {
      thread_name = java_name->ToModifiedUtf8();
    } else {
      thread_name = "(Unnamed)";
    }
    VLOG(threads) << "Creating native thread for " << thread_name;
    self->Dump(LOG_STREAM(INFO));
  }
  Runtime* runtime = Runtime::Current();
  // Atomically start the birth of the thread ensuring the runtime isn't shutting down.
  bool thread_start_during_shutdown = false;
  {
    MutexLock mu(self, *Locks::runtime_shutdown_lock_);
    if (runtime->IsShuttingDownLocked()) {
      thread_start_during_shutdown = true;
    } else {
      runtime->StartThreadBirth();
    }
  }
  if (thread_start_during_shutdown) {
    ScopedLocalRef<jclass> error_class(env, env->FindClass("java/lang/InternalError"));
    env->ThrowNew(error_class.get(), "Thread starting during runtime shutdown");
    return;
  }
  Thread* child_thread = new Thread(is_daemon);
  // Use global JNI ref to hold peer live while child thread starts.
  child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer);
  stack_size = FixStackSize(stack_size);
  // Thread.start is synchronized, so we know that nativePeer is 0, and know that we're not racing
  // to assign it.
  env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer,
                    reinterpret_cast<jlong>(child_thread));
  // Try to allocate a JNIEnvExt for the thread. We do this here as we might be out of memory and
  // do not have a good way to report this on the child's side.
  std::string error_msg;
  std::unique_ptr<JNIEnvExt> child_jni_env_ext(
      JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM(), &error_msg));
  int pthread_create_result = 0;
  if (child_jni_env_ext.get() != nullptr) {
    pthread_t new_pthread;
    pthread_attr_t attr;
    child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();
    CHECK_PTHREAD_CALL(pthread_attr_init, (&attr), "new thread");
    CHECK_PTHREAD_CALL(pthread_attr_setdetachstate, (&attr, PTHREAD_CREATE_DETACHED),
                       "PTHREAD_CREATE_DETACHED");
    CHECK_PTHREAD_CALL(pthread_attr_setstacksize, (&attr, stack_size), stack_size);
    pthread_create_result = pthread_create(&new_pthread,
                                           &attr,
                                           Thread::CreateCallback,
                                           child_thread);
    CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread");
    if (pthread_create_result == 0) {
      // pthread_create started the new thread. The child is now responsible for managing the
      // JNIEnvExt we created.
      // Note: we can't check for tmp_jni_env == nullptr, as that would require synchronization
      //       between the threads.
      child_jni_env_ext.release();
      return;
    }
  }
  // Either JNIEnvExt::Create or pthread_create(3) failed, so clean up.
  {
    MutexLock mu(self, *Locks::runtime_shutdown_lock_);
    runtime->EndThreadBirth();
  }
  // Manually delete the global reference since Thread::Init will not have been run.
  env->DeleteGlobalRef(child_thread->tlsPtr_.jpeer);
  child_thread->tlsPtr_.jpeer = nullptr;
  delete child_thread;
  child_thread = nullptr;
  // TODO: remove from thread group?
  env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0);
  {
    std::string msg(child_jni_env_ext.get() == nullptr ?
        StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) :
        StringPrintf("pthread_create (%s stack) failed: %s",
                                 PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
    ScopedObjectAccess soa(env);
    soa.Self()->ThrowOutOfMemoryError(msg.c_str());
  }
}

Thread::CreateCallbackから、Java側のThread.run()が呼び出される。

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }

Looper.prepare()メソッドは以下。Looperはクラス外部からprivateコンストラクタにアクセスできず、代わりにLooper.prepare()が用意されている。

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

sThreadLocalThreadLocalクラスで、同一スレッド内だけからアクセス可能な、スレッド内で1つだけ存在する変数を作成できる。

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

prepare()メソッド内でnew Looper()を生成し、set()メソッドでThreadLocal変数として保持する。

Looperコンストラクタは、

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

Looper.prepare()の後、synchronized文でmBackgroundThreadをロックし、Looper.prepare()メソッドで生成したLooperインスタンスmLooperLooper.myLooper()で取得する。

    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

Looper.loop()を呼び出す。

    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (slowDispatchThresholdMs > 0) {
                final long time = end - start;
                if (time > slowDispatchThresholdMs) {
                    Slog.w(TAG, "Dispatch took " + time + "ms on "
                            + Thread.currentThread().getName() + ", h=" +
                            msg.target + " cb=" + msg.callback + " msg=" + msg.what);
                }
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }

主な部分だけ取り出すと、

        for (;;) {
            Message msg = queue.next(); // might block
            try {
                msg.target.dispatchMessage(msg);
            }
        }

このメッセージ処理ループがバックグラウンドスレッドで実行される。

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

メッセージオブジェクトは、いわゆるメッセージだけでなく、Runnableを保持するmsg.callbackを持つ

スレッドをstartした後は、ハンドラーの生成。

mBackgroundHandler = new Handler(mBackgroundThread.getLooper());

HandlerのコンストラクタにはLooperを入れている。

    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }

メインスレッドからバックグラウンドスレッドで生成されるLooperオブジェクトを取得するために、スレッド間の同期を取っている。

ひとまず、ここまででバックグラウンドスレッドが開始された状態になる。

TextureView.SurfaceTextureListener

mTextureViewのリスナーをトリガーにしてカメラの設定などを行うので、SurfaceTextureListenerを実装する。

    private final TextureView.SurfaceTextureListener mSurfaceTextureListener
            = new TextureView.SurfaceTextureListener() {
        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
            openCamera(width, height);
        }
        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {
            configureTransform(width, height);
        }
        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
            return true;
        }
        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture texture) {
        }
    }

このリスナーの実装により、以下の流れでopenCamera()が実行される。

openCamera

    private void openCamera(int width, int height) {
        if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            requestCameraPermission();
            return;
        }
        setUpCameraOutputs(width, height);
        configureTransform(width, height);
        Activity activity = getActivity();
        CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
        try {
            if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
                throw new RuntimeException("Time out waiting to lock camera opening.");
            }
            manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
        }
    }

カメラのパーミッションをチェックしたあと、setUpCameraOutputs()を呼び出す。

    private void setUpCameraOutputs(int width, int height) {
        Activity activity = getActivity();
        CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);

CameraManagerのインスタンスを生成。

接続されているカメラのcameraIdリストをgetCameraIdList()で取得し、それぞれのカメラごとにfor文loopさせる。

        try {
            for (String cameraId : manager.getCameraIdList()) {

CameraManagerからCameraCharacteristicsを取得し、CameraCharacteristics.LENS_FACINGで背面カメラ、前面カメラを識別し、前面カメラの場合はcontinueでスキップする。

                CameraCharacteristics characteristics
                        = manager.getCameraCharacteristics(cameraId);
                Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
                if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
                    continue;
                }
                StreamConfigurationMap map = characteristics.get(
                        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                if (map == null) {
                    continue;
                }
                Size largest = Collections.max(
                        Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
                        new CompareSizesByArea());

最大サイズでImageReaderのインスタンスを生成し、画像が使用可能となったときのリスナーと、バックグラウンドスレッドのハンドラーを設定する。

                mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
                        ImageFormat.JPEG, /*maxImages*/2);
                mImageReader.setOnImageAvailableListener(
                        mOnImageAvailableListener, mBackgroundHandler);

リスナーmOnImageAvailableListeneronImageAvailable()には、RunnableインタフェースのImageSaverインスタンスを設定。このクラスはImageReaderのキューから取得した画像をファイルに保存する。

    private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
            = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), mFile));
        }
    };

ハンドラーのpost()メソッドは以下。

    public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

MessageクラスにはRunnableを格納するcallbackがあり、そこにImageSaverインスタンスを設定することで、RunnableMessage化できる。

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

リスナーの設定の後は画面の回転の処理。ディスプレイの回転状態を取得する。

                int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();

プレビュー画像のオフセット回転角度を取得する。

                mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);

それぞれを考慮して、縦横サイズの交換の必要有無フラグを設定する。

                boolean swappedDimensions = false;
                switch (displayRotation) {
                    case Surface.ROTATION_0:
                    case Surface.ROTATION_180:
                        if (mSensorOrientation == 90 || mSensorOrientation == 270) {
                            swappedDimensions = true;
                        }
                        break;
                    case Surface.ROTATION_90:
                    case Surface.ROTATION_270:
                        if (mSensorOrientation == 0 || mSensorOrientation == 180) {
                            swappedDimensions = true;
                        }
                        break;
                    default:
                        Log.e(TAG, "Display rotation is invalid: " + displayRotation);
                }

縦横サイズの交換が必要あれば、SurfaceTextureサイズの縦横を入れ替えたものをプレビュー画面サイズとする。ディスプレイサイズをMaxサイズとして取得し、これも縦横交換処理をする。

                Point displaySize = new Point();
                activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
                int rotatedPreviewWidth = width;
                int rotatedPreviewHeight = height;
                int maxPreviewWidth = displaySize.x;
                int maxPreviewHeight = displaySize.y;

                if (swappedDimensions) {
                    rotatedPreviewWidth = height;
                    rotatedPreviewHeight = width;
                    maxPreviewWidth = displaySize.y;
                    maxPreviewHeight = displaySize.x;
                }

                if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
                    maxPreviewWidth = MAX_PREVIEW_WIDTH;
                }

                if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
                    maxPreviewHeight = MAX_PREVIEW_HEIGHT;
                }

カメラのSurfaceTextureの出力サイズリストの中から、プレビュー画像のサイズを選択する。chooseOptimalSize()は以下の優先度でサイズを選択している。

                mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
                        rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth,
                        maxPreviewHeight, largest);

その後、setAspectRatio()の中で、mTextureViewのサイズを選択したサイズのアスペクト比に変更する。

                int orientation = getResources().getConfiguration().orientation;
                if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
                    mTextureView.setAspectRatio(
                            mPreviewSize.getWidth(), mPreviewSize.getHeight());
                } else {
                    mTextureView.setAspectRatio(
                            mPreviewSize.getHeight(), mPreviewSize.getWidth());
                }

ここまでがsetUpCameraOutputs()

サイズの例として、SO-02Jの場合、以下のようになる。

Displayサイズ

WidthHeightアスペクト比
720118445:74 縦にしたとき
118472074:45 横にしたとき

TextureViewサイズ

WidthHeightアスペクト比
720118445:74 縦にしたとき
118472074:45 横にしたとき

JPEGサイズ

WidthHeightアスペクト比
3840216016:9
326424484:3
204815364:3
1920108016:9
128072016:9
7204803:2
6404804:3
4803203:2
35228811:9
3202404:3
17614411:9

SurfaceTextureクラスのサイズ。Displayサイズのアスペクト比が標準的でないために条件に合致するサイズはなく、先頭が選択される。

WidthHeightアスペクト比
3840216016:9
326424484:3
204815364:3
1920108016:9
160012004:3
144010804:3
128072016:9
9607204:3
8644809:5
7204803:2
6404804:3
4803203:2
35228811:9
3202404:3
17614411:9

画面の回転処理についてSO-02Jで確認する。

まず、通常の縦持ちにしたとき

TextureView初期状態プレビュー元画像プレビューオフセット回転画像TextureViewアスペクト比変更後結果
720x11843840x21602160x3840666x1184666x1184

ディスプレイサイズを720x1280にするため、View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATIONstyles.xmlの設定でナビゲーションバーを透過に変更する。

public class CameraActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera);
        if (null == savedInstanceState) {
            getSupportFragmentManager().beginTransaction()
                    .replace(R.id.container, Camera2BasicFragment.newInstance())
                    .commit();
        }
    }
    @Override
    protected void onResume(){
        Log.i("Camera2BasicFragment","onResume Activity");
        super.onResume();
        findViewById(R.id.container).setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
    }
    
}
<resources>
    <style name="MaterialTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowFullscreen">true</item>
        <item name="android:navigationBarColor">@android:color/transparent</item>
    </style>
</resources>

ただし、ナビゲーションバーを隠してもDisplayサイズはナビゲーションバーを除いた領域になってしまうため、chooseOptimalSize()のMAXサイズパラメータを変更する。(元々、プレビューサイズリストと比較するパラメータなので、回転角度によってWidth/Heightを入れ替えてしまうmaxPreviewWidthmaxPreviewHeihtを使うと意図通りの選択ができない。)

                mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
                        rotatedPreviewWidth, rotatedPreviewHeight, MAX_PREVIEW_WIDTH/*maxPreviewWidth*/,
                        MAX_PREVIEW_HEIGHT/*maxPreviewHeight*/, largest);

ナビゲーションバーを隠すと、通常の縦持ちにしたとき

TextureView初期状態プレビュー元画像プレビューオフセット回転画像TextureViewアスペクト比変更後結果
720x12801280x720720x1280720x1280720x1280

スマホを左90°回転すると、

TextureView初期状態プレビュー元画像プレビューオフセット回転画像結果
1280x7201280x720720x12801280x720

このため、回転時には取得したプレビュー画像を回転させる必要がある。MatrixクラスをsetTransform()で設定することで回転させる。

    private void configureTransform(int viewWidth, int viewHeight) {
        Activity activity = getActivity();
        if (null == mTextureView || null == mPreviewSize || null == activity) {
            return;
        }
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        Matrix matrix = new Matrix();
        RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
        RectF bufferRect = new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth());
        float centerX = viewRect.centerX();
        float centerY = viewRect.centerY();
        if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
            bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());
            matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
            float scale = Math.max(
                    (float) viewHeight / mPreviewSize.getHeight(),
                    (float) viewWidth / mPreviewSize.getWidth());
            matrix.postScale(scale, scale, centerX, centerY);
            matrix.postRotate(90 * (rotation - 2), centerX, centerY);
        } else if (Surface.ROTATION_180 == rotation) {
            matrix.postRotate(180, centerX, centerY);
        }
        mTextureView.setTransform(matrix);
    }

このままではまだ問題があり、実際には90°から180°への回転ではディスプレイは回転しない。さらに180°から270°へ回転させたときは、ディスプレイは回転するにもかかわらずActivityの再生成は行われず、onResume()が呼ばれない。そのためconfigureTransform()も実行されず、オリジナルのサンプルアプリでは表示画像が上下反転状態になってしまう。

回転操作
左90°回転

右90°回転

右180°回転

右270°回転
displayRotationROTATION_90ROTATION_0ROTATION_270ROTATION_270ROTATION_90
mSensorOrientation90°90°90°90°90°
ディスプレイ回転有り有り無し有り
Activity再生成有り有り無し無し

onDisplayChanged()で対策する。onDisplayChanged()Activity再生成の前に呼ばれるので、再生成されるときは改めてonResume()から設定が必要で、openCamera()内のconfigureTransform()は省略できない。

DisplayListenerリスナーを追加し、270°回転でも上下反転しないよう変更する。

public class Camera2BasicFragment extends Fragment
        implements View.OnClickListener, ActivityCompat.OnRequestPermissionsResultCallback, DisplayManager.DisplayListener {
    private DisplayManager displayManager;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Activity activity = getActivity();
        displayManager = (DisplayManager) activity.getSystemService(Context.DISPLAY_SERVICE);
        displayManager.registerDisplayListener(this, null);
    }
    @Override
    public void onDisplayAdded(int displayId) {
    }
    @Override
    public void onDisplayChanged(int displayId) {
        configureTransform(mTextureView.getWidth(), mTextureView.getHeight());
    }
    @Override
    public void onDisplayRemoved(int displayId) {
    }

まだopenCamera()の中身の続き。次にCameraManagerクラスを生成。

        CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);

カメラへの接続をopenする。セマフォを使っているのはopenとcloseを排他的に処理するためで、ここではopenが確実に完了するまで他の場所からopen/close処理が出来ないように、mCameraOpenCloseLockのパーミッションをtryAcquire()で取得する。

        try {
            if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
                throw new RuntimeException("Time out waiting to lock camera opening.");
            }
            manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();

        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
        }

CameraManager.openCamera()で設定されるmStateCallbackは以下。

    private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            // This method is called when the camera is opened.  We start camera preview here.
            mCameraOpenCloseLock.release();
            mCameraDevice = cameraDevice;
            createCameraPreviewSession();
        }
        @Override
        public void onDisconnected(@NonNull CameraDevice cameraDevice) {
            mCameraOpenCloseLock.release();
            cameraDevice.close();
            mCameraDevice = null;
        }
        @Override
        public void onError(@NonNull CameraDevice cameraDevice, int error) {
            mCameraOpenCloseLock.release();
            cameraDevice.close();
            mCameraDevice = null;
            Activity activity = getActivity();
            if (null != activity) {
                activity.finish();
            }
        }
    };

open時には、onOpened()により、

createCameraPreviewSession()の内容を順に見ていく。

SurfaceTextureクラスを取得。SurfaceTextureは、カメラプレビューの映像からフレームをキャプチャするためのクラス。

    private void createCameraPreviewSession() {
        try {
            SurfaceTexture texture = mTextureView.getSurfaceTexture();

SurfaceTextureのサイズを設定。

            texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());

SurfaceクラスをSurfaceTextureから生成する。Surfaceは、カメラデバイスからの出力を受け取り、ディスプレイへ表示するためのバッファをハンドルするクラス。

            Surface surface = new Surface(texture);

キャプチャをリクエストするため、CaptureRequest.Builderを生成し、surfaceをターゲットに追加する。

            mPreviewRequestBuilder
                    = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            mPreviewRequestBuilder.addTarget(surface);

CameraCaptureSessionを生成する。1つ目のパラメータには、キャプチャ画像の出力先Surfaceのリストを入力する。

            mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
                    new CameraCaptureSession.StateCallback() {

2つ目のパラメータにCallbackを入力し、CameraCaptureSession生成時に呼ばれるonConfiguredを定義する。

                        @Override
                        public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                            // The camera is already closed
                            if (null == mCameraDevice) {
                                return;
                            }

CaptureRequest.Builderにオートフォーカスモードの設定とフラッシュの設定を行う。

                            // When the session is ready, we start displaying the preview.
                            mCaptureSession = cameraCaptureSession;
                            try {
                                // Auto focus should be continuous for camera preview.
                                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                                // Flash is automatically enabled when necessary.
                                setAutoFlash(mPreviewRequestBuilder);

setRepeatingRequestにより、繰り返しキャプチャをリクエストする。キャプチャが繰り返されるごとに、mBackgroundHandlerがハンドルするバックグラウンドスレッドで、mCaptureCallbackが呼び出される。

                                // Finally, we start displaying the camera preview.
                                mPreviewRequest = mPreviewRequestBuilder.build();
                                mCaptureSession.setRepeatingRequest(mPreviewRequest,
                                        mCaptureCallback, mBackgroundHandler);
                            } catch (CameraAccessException e) {
                                e.printStackTrace();
                            }
                        }

                        @Override
                        public void onConfigureFailed(
                                @NonNull CameraCaptureSession cameraCaptureSession) {
                            showToast("Failed");
                        }
                    }, null
            );

mCaptureCallbackでは、独自に定義したmStateによって、ステータス処理をしている。プレビュー中は特に処理は不要。写真撮影時に、オートフォーカスと自動露出の状況に合わせて、captureStillPicture()を呼び出し、繰り返しキャプチャをStopして、ターゲットをTextureViewではなくImageReaderSurfaceに切り替えて、1回のキャプチャを実行する。撮影すると、ImageReaderonImageAvailableがリスナーとして呼び出され、画像が保存される。

    private CameraCaptureSession.CaptureCallback mCaptureCallback
            = new CameraCaptureSession.CaptureCallback() {

        private void process(CaptureResult result) {
            switch (mState) {
                case STATE_PREVIEW: {
                    // We have nothing to do when the camera preview is working normally.
                    break;
                }
                case STATE_WAITING_LOCK: {
                    Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
                    if (afState == null) {
                        captureStillPicture();
                    } else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState ||
                            CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState) {
                        // CONTROL_AE_STATE can be null on some devices
                        Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
                        if (aeState == null ||
                                aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
                            mState = STATE_PICTURE_TAKEN;
                            captureStillPicture();
                        } else {
                            runPrecaptureSequence();
                        }
                    }
                    break;
                }
                case STATE_WAITING_PRECAPTURE: {
                    // CONTROL_AE_STATE can be null on some devices
                    Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
                    if (aeState == null ||
                            aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ||
                            aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) {
                        mState = STATE_WAITING_NON_PRECAPTURE;
                    }
                    break;
                }
                case STATE_WAITING_NON_PRECAPTURE: {
                    // CONTROL_AE_STATE can be null on some devices
                    Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
                    if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
                        mState = STATE_PICTURE_TAKEN;
                        captureStillPicture();
                    }
                    break;
                }
            }
        }

OpenCVの前にJNIのHello World

OpenCVのソースコードはC/C++なので、JavaとC/C++をインタフェースするJNI(Java Native Interface)と呼ばれるnativeメソッドを介して実行可能になる。

JNIのサンプルとして、https://developer.android.com/samples/からHello JNIをダウンロードしてopenする。

nativeコードを含むため、CPUを気にする必要がある。app\build.gradleを確認する。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId 'com.example.hellojni'
        minSdkVersion 23
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }

    flavorDimensions 'cpuArch'
    productFlavors {
        arm7 {
            dimension 'cpuArch'
            ndk {
                abiFilters 'armeabi-v7a'
            }
        }
        arm8 {
            dimension 'cpuArch'
            ndk {
                abiFilters 'arm64-v8a'
            }
        }
        x86 {
            dimension 'cpuArch'
            ndk {
                abiFilters 'x86'
            }
        }
        x86_64 {
            dimension 'cpuArch'
            ndk {
                abiFilters 'x86_64'
            }
        }
        universal {
            dimension 'cpuArch'
            // include all default ABIs. with NDK-r16,  it is:
            //   armeabi-v7a, arm64-v8a, x86, x86_64
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
}

Android Studioでビルドするときに、ターゲットCPUを選択できるようになる。universalを選べばすべてのApplication Binary Interface (ABI)が生成される。

生成されたAPKファイルを解凍してみると、すべてのABI向けにlibhello-jni.soが出来ていることが分かる。

CMakeファイルのadd_libraryでソースファイルhello-jni.cをコンパイルしたライブラリhello-jniを追加する。target_link_libraries文についてはHello Worldでは不要。

cmake_minimum_required(VERSION 3.4.1)

add_library(hello-jni SHARED
            hello-jni.c)

# Include libraries needed for hello-jni lib
target_link_libraries(hello-jni
                      android
                      log)

SO-02Jで実行すると、SO-02JのABIがarm64-v8aと分かる。

OpenCVを入手

https://opencv.org/に行き、RELEASESから、Android Packをダウンロードする。



ダウンロードしたファイルのハッシュの一致を確認。SHA1はコマンドプロンプト上でfcivコマンドにより計算出来る。

Camera2BasicにOpenCVライブラリの追加

まず、OpenCV-android-sdk/sdk/native/libs配下にある、各CPUごとのnativeライブラリファイルlibopencv_java3.soを取り込むため、mainの下にjniLibsディレクトリを作成し、ライブラリファイルをコピーする。

次に、OpenCV-android-sdk/sdk/javaをプロジェクトディレクトリ直下へコピー、ディレクトリ名をopencvに変更する。

opencvディレクトリにあるマニフェストファイルのSDK Versionを削除する。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="org.opencv"
      android:versionCode="3440"
      android:versionName="3.4.4">
</manifest>

新たに以下内容のbuild.gradleopencvディレクトリに置く。

buildscript {
    repositories {
        jcenter()
        google()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
    }
}
apply plugin: 'com.android.library'
repositories {
    jcenter()
    google()
}
android {
    compileSdkVersion 28
    defaultConfig {
        minSdkVersion 23
        targetSdkVersion 28
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            res.srcDirs = ['res']
            aidl.srcDirs = ['src']
        }
    }
}

settings.gradleopencvのincludeを追加。

include 'Application'
include "opencv"

Module Dependencyにopencvを追加する。


Gradleバージョンを最新にする。


Applicationディレクトリのbuild.gradleを最新バージョンで整理する。

buildscript {
    repositories {
        jcenter()
        google()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
    }
}
apply plugin: 'com.android.application'
repositories {
    jcenter()
    google()
}
dependencies {
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation project(':opencv')
}
android {
    compileSdkVersion 28
    defaultConfig {
        minSdkVersion 23
        targetSdkVersion 28
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    sourceSets {
        main {
            java.srcDirs "src/main/java"
            res.srcDirs "src/main/res"
        }
        androidTest.setRoot('tests')
        androidTest.java.srcDirs = ['tests/src']
    }
}

nativeライブラリをロードするコードを追加する。

public class Camera2BasicFragment extends Fragment
        implements View.OnClickListener, ActivityCompat.OnRequestPermissionsResultCallback, DisplayManager.DisplayListener {

    static {
        System.loadLibrary("opencv_java3");
    }

プレビュー画像のMat化

Camera2Basicサンプルは、プレビュー画像をそのままTextureViewへ送っていたが、OpenCVで画像処理するために、Matクラスへ変換する必要がある。OpenCVライブラリではJavaCamera2Viewを使えばカメラ画像をMatクラスで取得できるが、使ってみると色がおかしく、画面サイズや回転の制御も出来ないので使わない。

CameraDeviceのプレビュー画像はYUV_420_888。撮影時のみ使っていたImageReaderを、プレビュー用にJPEGからYUV_420_888に変更する。

                mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
                        ImageFormat.YUV_420_888, /*maxImages*/2);

RGBがRed/Green/Blueの3原色で色を表すのに対して、YUVフォーマットは、輝度Y、青と輝度の差U、赤と輝度の差V、の3つで色を表す。この分け方により、U/Vの情報を減らしても画質の劣化が分かりづらくなり、圧縮しやすくなるというフォーマット。444が圧縮なし、422がU/Vをそれぞれ水平方向のみ2分の1、420がU/Vをそれぞれ水平・垂直両方2分の1に圧縮し、2x2の4ピクセルあたり、輝度Yはピクセル分4つを保持するが、色差情報UとVはそれぞれ1つだけとなる。

YUV420


このフォーマットに加えて、Y、U、V、各プレーンデータのbuffer配置方法にいくつも種類がある。

I420


N12


N21


ただし、Androidの場合、プレーンデータの順序は固定であることが保証されている。


UVPixel Stride = 2Row Stride = 6の場合


が保証される。

実際には


となるので、planes[1]を使うとN12相当になるが、JavaCamera2ViewYUV_420_888planes[1]N21で変換してしまっているため、色差プレーンが逆になり、色がおかしくなる。

以上を踏まえてonImageAvailable()を変更する。これで、Matクラスに変換したうえでカメラプレビューと撮影ができるようになった。

    private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
            = new ImageReader.OnImageAvailableListener() {

        @Override
        public void onImageAvailable(ImageReader reader) {
            try {
                if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
                    throw new RuntimeException("Time out waiting to lock camera opening.");
                }

                Image mImage = reader.acquireLatestImage();
                if(mImage==null){
                    mCameraOpenCloseLock.release();
                    return;
                }

                int planenum = 4;
                int w = mImage.getWidth();
                int h = mImage.getHeight();

                Image.Plane[] planes = mImage.getPlanes();
                ByteBuffer yplane = planes[0].getBuffer();
                ByteBuffer uplane = planes[1].getBuffer();
                ByteBuffer vplane = planes[2].getBuffer();
                int uvps = planes[1].getPixelStride(); // = planes[2].getPixelStride();
                int uvrs = planes[1].getRowStride(); // = planes[2].getRowStride();

                byte[] data = new byte[w * h * planenum];

                int yint = 0;
                int uint = 0;
                int vint = 0;
                int rint = 0;
                int gint = 0;
                int bint = 0;
                if (uvps == 1) { // yplane=YYYYYYYY.. / uplane=UUUU.. / vplane=VVVV..
                    for (int y = 0; y < h; y++) {
                        for (int x = 0; x < w; x++) {
                            if (yplane.limit() > x + y * w) {
                                yint = (0xff & yplane.get(x + y * w));
                            } else {
                                yint = 0;
                            }
                            if ((x & 1) == 0) {
                                if (uplane.limit() > (x / 2) + (y / 2) * uvrs) {
                                    uint = (0xff & uplane.get((x / 2) + (y / 2) * uvrs));
                                    uint = uint - 128;
                                } else {
                                    uint = 0;
                                }
                                if (vplane.limit() > (x / 2) + (y / 2) * uvrs) {
                                    vint = (0xff & vplane.get((x / 2) + (y / 2) * uvrs));
                                    vint = vint - 128;
                                } else {
                                    vint = 0;
                                }
                            }
                            // ITU-R BT. 709
                            rint = yint + (vint * 1575) / 1000;
                            gint = yint - (uint * 187) / 1000 - (vint * 468) / 1000;
                            bint = yint + (uint * 1856) / 1000;
                            if (rint < 0) { rint = 0; } else if (rint > 255) { rint = 255; }
                            if (gint < 0) { gint = 0; } else if (gint > 255) { gint = 255; }
                            if (bint < 0) { bint = 0; } else if (bint > 255) { bint = 255; }
                            data[x * planenum + y * planenum * w] = (byte) rint;
                            data[x * planenum + y * planenum * w + 1] = (byte) gint;
                            data[x * planenum + y * planenum * w + 2] = (byte) bint;
                            data[x * planenum + y * planenum * w + 3] = (byte) 0xff;
                        }
                    }
                } else { // yplane=YYYYYYYY.. / uplane=UVUV.. / vplane=VUVU..
                    for (int y = 0; y < h; y++) {
                        for (int x = 0; x < w; x++) {
                            if (yplane.limit() > x + y * w) {
                                yint = (0xff & yplane.get(x + y * w));
                            } else {
                                yint = 0;
                            }
                            if ((x & 1) == 0) {
                                if (uplane.limit() > x + (y / 2) * uvrs) {
                                    uint = (0xff & uplane.get(x + (y / 2) * uvrs));
                                    uint = uint - 128;
                                } else {
                                    uint = 0;
                                }
                                if (vplane.limit() > x + (y / 2) * uvrs) {
                                    vint = (0xff & vplane.get(x + (y / 2) * uvrs));
                                    vint = vint - 128;
                                } else {
                                    vint = 0;
                                }
                            }
                            // ITU-R BT. 709
                            rint = yint + (vint * 1575) / 1000;
                            gint = yint - (uint * 187) / 1000 - (vint * 468) / 1000;
                            bint = yint + (uint * 1856) / 1000;
                            if (rint < 0) { rint = 0; } else if (rint > 255) { rint = 255; }
                            if (gint < 0) { gint = 0; } else if (gint > 255) { gint = 255; }
                            if (bint < 0) { bint = 0; } else if (bint > 255) { bint = 255; }
                            data[x * planenum + y * planenum * w] = (byte) rint;
                            data[x * planenum + y * planenum * w + 1] = (byte) gint;
                            data[x * planenum + y * planenum * w + 2] = (byte) bint;
                            data[x * planenum + y * planenum * w + 3] = (byte) 0xff;
                        }
                    }
                }
                Mat rgbamat = new Mat(h, w, CvType.CV_8UC4);
                rgbamat.put(0, 0, data);
                if(mSensorOrientation==90 || mSensorOrientation==270) {
                    Core.flip(rgbamat.t(), rgbamat, 1);
                }
                Utils.matToBitmap(rgbamat, mCacheBitmap);
                if(mCacheBitmap!=null) {
                    Canvas canvas = mTextureView.lockCanvas();
                    canvas.drawBitmap(mCacheBitmap, 0, 0, PAINT);
                    mTextureView.unlockCanvasAndPost(canvas);
                    if(ftakepicture) {
                        SavePreview();
                        ftakepicture=false;
                    }
                }
                mImage.close();
            }catch(Exception e) {
                Log.i(TAG, e.toString());
            }
            mCameraOpenCloseLock.release();
        }
    };

顔認識

ようやく、まずは既存のカスケードファイルを使って顔認識をしてみる。カスケードファイルはWindows向けのOpenCVをダウンロードすると入っている。main/res/rawディレクトリにカスケードファイルhaarcascade_frontalface_alt.xmlを入れておき、onCreate()内でCascadeClassifierクラスを生成する。

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Activity activity = getActivity();
        File mCascadeFile;
        try {
            InputStream is = activity.getResources().openRawResource(R.raw.haarcascade_frontalface_alt);
            File cascadeDir = activity.getDir("cascade", Context.MODE_PRIVATE);
            mCascadeFile = new File(cascadeDir, "haarcascade.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();

            mJavaDetector = new CascadeClassifier(mCascadeFile.getAbsolutePath());
            if (mJavaDetector.empty()) {
                Log.e("tag", "Failed to load cascade classifier");
                mJavaDetector = null;
            } else
                Log.i("tag", "Loaded cascade classifier from " + mCascadeFile.getAbsolutePath());

            cascadeDir.delete();

        } catch (IOException e) {
            e.printStackTrace();
            Log.e("tag", "Failed to load cascade. Exception thrown: " + e);
        }
    }

onImageAvailable()内でMatデータをグレーMatに変換し、CascadeClassifier.detectMultiScaleメソッドで顔検出する。

                Mat rgbamat = new Mat(h, w, CvType.CV_8UC4);
                Mat graymat = new Mat(h, w, CvType.CV_8UC1);
                rgbamat.put(0, 0, data);
                Imgproc.cvtColor(rgbamat, graymat, Imgproc.COLOR_RGB2GRAY);
                if(mSensorOrientation==90 || mSensorOrientation==270) {
                    Core.flip(rgbamat.t(), rgbamat, 1);
                    Core.flip(graymat.t(), graymat, 1);
                }

                MatOfRect faces = new MatOfRect();
                float mRelativeFaceSize   = 0.15f;
                float mRelativeFaceMaxSize   = 0.35f;
                if (mAbsoluteFaceSize == 0) {
                    if (w>h) {
                        mAbsoluteFaceSize = Math.round(h * mRelativeFaceSize);
                    }else{
                        mAbsoluteFaceSize = Math.round(w * mRelativeFaceSize);
                    }
                }
                if (mAbsoluteFaceMaxSize == 0){
                    if (w>h) {
                        mAbsoluteFaceMaxSize = Math.round(h * mRelativeFaceMaxSize);
                    }else{
                        mAbsoluteFaceMaxSize = Math.round(w * mRelativeFaceMaxSize);
                    }
                }

                mJavaDetector.detectMultiScale(graymat, faces, 1.1, 3, 2,
                        new org.opencv.core.Size(mAbsoluteFaceSize, mAbsoluteFaceSize), new org.opencv.core.Size(mAbsoluteFaceMaxSize, mAbsoluteFaceMaxSize));
                Rect[] facesArray = faces.toArray();
                for (int i = 0; i < facesArray.length; i++)
                    Imgproc.rectangle(rgbamat, facesArray[i].tl(), facesArray[i].br(), FACE_RECT_COLOR, 3);

上のCascadeClassifier.detectMultiScaleの設定では、カスケードファイルによる特徴検出を、顔サイズを画面幅の35%から15%まで1/1.1ずつ小さくしながら、各サイズごとに画面の端から端まで特徴検査を行う。隣接3か所で検出をして初めて検出判定となる。


TOP 顔認識 機械学習 Eigenfaces