2018年1月24日水曜日

clojureでopencv javaを利用して、カメラで取得した画像に対して顔検出を行う方法


背景

clojureとは、jvm上で動くlisp系の言語です。
opencvとは、画像処理をするためのライブラリです。

前回は、clojureでopencv javaを利用して画像を表示しました。
clojureでopencv3.2を呼び出し、javax.swingで画像を表示する方法

今回は、カメラの画像を取得して顔認識する方法を説明します。

全体像

  1. 使ったもの
  2. プログラムの説明
  3. 動作確認
  4. まとめ

使ったもの

leiningenが使える環境

liningenとは、clojureのライブラリ管理プログラムの一つです。
ubuntu17.10で、下記のコマンドを実行してインストールしました。
wget https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein
sudo mv lein /usr/local/bin/
sudo chmod a+x ~/usr/bin/lein

opencv javaを使えるようにしたclojureのプロジェクト

まず、下記のコマンドでopencvをビルドしました。
今回はopencv3.4.0を利用しました。
sudo apt install build-essential cmake openjdk-8-jdk ant
mkdir gitprojects
cd gitprojects
# clone version 3.4.0 source
git clone --branch 3.4.0 --depth 1 git@github.com:opencv/opencv.git opencv_source
cd opencv_source
mkdir build
cd build
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/
cmake -D BUILD_SHARED_LIBS=OFF ..
make -j4

次にleinコマンドでプロジェクトを作り、必要なopencvのファイルをコピーしました。
ubuntuはlinuxなので.soファイルをコピーしましたが、macの場合はdylibファイル、windowsの場合はdllファイルをコピーする必要があると思います。(macとwindowsは未検証です。)
lein new clj-opencv-videocapture-example
cd clj-opencv-videocapture-example
mkdir lib
cp ~/gitprojects/opencv_source/build/bin/opencv-340.jar lib/
cp ~/gitprojects/opencv_source/build/lib/libopencv_java340.so lib/

そして、project.cljにopencvを読み込むための記述を追加すると、opencv_javaを使えるようになります。
project.clj
(defproject
  ..
  :jvm-opts ["-Djava.library.path=lib"]
  ..)

opencvの顔認識データ

下記のコマンドで、プロジェクトのresourcesディレクトリに配置しました。
cp [this project]
mkdir resources
cp gitprojects/opencv_source/data/lbpcascades/lbpcascade_frontalface.xml resources/

プログラムの説明

このプログラムで顔認識を行いました。
ざっくりと説明します。
src/clj_opencv_videocapture_example/core.clj
(ns clj-opencv-videocapture-example.core
  (:import [java.awt.image BufferedImage]
           [javax.swing JFrame JLabel ImageIcon WindowConstants SwingUtilities]
           [org.opencv.core Core Mat CvType MatOfRect Point Scalar]
           [org.opencv.imgcodecs Imgcodecs]
           [org.opencv.imgproc Imgproc]
           [org.opencv.objdetect CascadeClassifier]
           [org.opencv.videoio VideoCapture])
  (:require [clojure.core.async :as async]
            [clj-time.core :as t]
            [clj-time.local :as l]))

(def j-frame (new JFrame))
(def mat-frame (new Mat))
(def cv-camera (new VideoCapture 0))
(def face-detector (new CascadeClassifier "resources/lbpcascade_frontalface.xml"))
(def face-detections (new MatOfRect))

(defn create-buffered-image [mat]
  (let [gray? (= (.channels mat) 1)
        type (if gray?
               BufferedImage/TYPE_BYTE_GRAY
               BufferedImage/TYPE_3BYTE_BGR)]
    (new BufferedImage (.width mat) (.height mat) type)))

(defn update-buffered-image [buffered-image mat]
  (.get mat 0 0 (-> buffered-image
                    (.getRaster)
                    (.getDataBuffer)
                    (.getData))))

(defn config-j-frame [j-frame & {:keys [title set-visible close-operation width height]}]
  (when title
    (.setTitle j-frame title))
  (when-not (nil? set-visible)
    (.setVisible j-frame set-visible))
  (when close-operation
    (.setDefaultCloseOperation j-frame close-operation))
  (when (and width height)
    (.setSize j-frame width height)))

(defn check-faces [mat-frame]
  (.detectMultiScale face-detector mat-frame face-detections)
  #_(prn :face-count (.size (.toList face-detections)))
  (doall
   (for [rect (.toArray face-detections)]
     (Imgproc/rectangle mat-frame
                        (new Point (.-x rect) (.-y rect))
                        (new Point (+ (.-x rect) (.-width rect)) (+ (.-y rect) (.-height rect)))
                        (new Scalar 0, 255, 0)))))

(defn show-fps [mat-frame fps]
  (Imgproc/putText mat-frame
                   (str fps "fps")
                   (new Point 10 40)
                   Core/FONT_HERSHEY_SIMPLEX
                   1                  ; Scale
                   (new Scalar 0 0 0) ; Color
                   4))                ; Thickness

(defn one-line-log [fps]
  (print "\r" (format "%3dfps" fps))
  (flush))

(defn -main []
  (prn :start-main)
  (if (.isOpened cv-camera)
    (do
      (.read cv-camera mat-frame)
      (prn :width (.width mat-frame) :height (.height mat-frame))
      #_(Imgcodecs/imwrite "capture.jpg" mat-frame)
      (config-j-frame j-frame
                      :title "captured camera image on opencv in clojure"
                      :set-visible true
                      :close-operation WindowConstants/EXIT_ON_CLOSE
                      :width (.width mat-frame) :height (.height mat-frame))
      (let [buffered-image (create-buffered-image mat-frame)]
        (.add (.getContentPane j-frame) (->> buffered-image
                                             (new ImageIcon)
                                             (new JLabel)))
        (.revalidate j-frame)
        (loop [times-in-sec []]
          (let [one-sec-ago (t/minus (l/local-now) (t/seconds 1))
                times-in-sec (->> (conj times-in-sec (l/local-now))
                                  (filter #(t/before? one-sec-ago %)))
                fps (count times-in-sec)]
            (one-line-log fps)
            (.read cv-camera mat-frame)
            (check-faces mat-frame)
            (show-fps mat-frame fps)
            (update-buffered-image buffered-image mat-frame)
            (.repaint j-frame)
            (recur times-in-sec)))))
    (prn :cannot-open-camera)))

カメラ画像の取得

videoioのvideoCaptureを利用することで、webカメラの画像を取得できました。
(def cv-camera (new VideoCapture 0))
(if (.isOpened cv-camera)
  (.read cv-camera mat-frame))

顔認識

opencvのリポジトリに含まれる学習済みデータを利用して、顔認識を行いました。
顔と認識した部分を、緑色の四角で囲っています。
(def face-detector (new CascadeClassifier "resources/lbpcascade_frontalface.xml"))
(defn check-faces [mat-frame]
  (.detectMultiScale face-detector mat-frame face-detections)
  (doall
   (for [rect (.toArray face-detections)]
     (Imgproc/rectangle mat-frame
                        (new Point (.-x rect) (.-y rect))
                        (new Point (+ (.-x rect) (.-width rect)) (+ (.-y rect) (.-height rect)))
                        (new Scalar 0, 255, 0)))))

fpsを表示

処理速度を見たかったので、ログと画像にfpsを表示しました。
(defn show-fps [mat-frame fps]
  (Imgproc/putText mat-frame
                   (str fps "fps")
                   (new Point 10 40)
                   Core/FONT_HERSHEY_SIMPLEX
                   1                  ; Scale
                   (new Scalar 0 0 0) ; Color
                   4))                ; Thickness

(defn one-line-log [fps]
  (print "\r" (format "%3dfps" fps))
  (flush))

(loop [times-in-sec []]
  (let [one-sec-ago (t/minus (l/local-now) (t/seconds 1))
        times-in-sec (->> (conj times-in-sec (l/local-now))
                          (filter #(t/before? one-sec-ago %)))
        fps (count times-in-sec)]
    (one-line-log fps)
    (show-fps mat-frame fps)
    ;; do something
    ))

表示する画像の更新

loopの前にBufferedImageなどのバッファを作成してJFrameに紐付け、loopの中でBufferedImageの更新とJFrameの再描画を行うことで、表示する画像を更新しています。
BufferedImageのnewをloopの中で繰り返すと、「java.lang.OutOfMemoryError」が発生するので、長時間動かしたいプログラムの場合は、loopの外でbufferを作るのが良いと思います。
(let [buffered-image (create-buffered-image mat-frame)]
  (.add (.getContentPane j-frame) (->> buffered-image
                                       (new ImageIcon)
                                       (new JLabel)))
  (.revalidate j-frame)
  (loop []
    ..
    (update-buffered-image buffered-image mat-frame)
    (.repaint j-frame)
    (recur)))

動作確認

640x480pxの映像を20fps前後で顔認識できました。


まとめ

clojureからopencv javaを利用して、カメラ画像に対する顔認識を実施できました。
BufferedImageをloop毎にnewするとOutOfMemoryErrorになってしまうので、長時間稼働させたい場合はバッファを使い回す必要がありました。

今回動かしたプログラムは、こちらのリポジトリで公開しています。
asukiaaa/clj-opencv-videocapture-example

何かの参考になれば嬉しいです。

参考

opencv javaのドキュメントです。
3.4がリリースされたのに、3.2以後のドキュメントが無いのが謎です。
java向けには新しい機能が提供されてなかったりするのかもしれません。
OpenCV Java 3.1.0

前回取り組んだ内容です。
clojureでopencv3.2を呼び出し、javax.swingで画像を表示する方法

更新履歴

2018.1.25
videocaptureはopencv2から提供されている機能のようなので、「opencv3から追加された」という間違った認識の説明を削除しました。

0 件のコメント :