2017年4月24日月曜日

clojureでopencv3.2を呼び出し、javax.swingで画像を表示する方法


背景

opencvにはjava向けのapiが提供されています。
そのapiの設定次第でclojureからopencvを扱えます。

しかし、プロジェクトの設定方法や画像の表示など、調査や試行錯誤に時間がかかることが多かったので、自分の分かった情報を共有します。

全体像

この順に共有していきます。
  1. 使ったもの
  2. opencvのビルド
  3. opencvのためのclojureプロジェクトの設定
  4. プログラムの説明
  5. 実行

使ったもの

  • linux(ubuntu)OSをインストールしたPC
    opencvのビルドとclojureプロジェクトの設定は、linuxを想定して説明します。
  • leiningen 2.7.1
    leinスクリプトなどでインストールしてください。
  • opencv 3.2
    ビルドコマンドを共有します。

opencvのビルド

opencvはjava向けのapiを提供しています。
そのapiを使うのに必要なファイルを、下記のコマンドでビルドします。

sudo apt install cmake ant openjdk-8-jdk git
mkdir gitprojects
cd gitprojects
git clone git@github.com:opencv/opencv.git opencv_source
cd opencv_source
git checkout tags/3.2.0
# git fetch -p # if you cannot find 3.2.0 tag
mkdir build
cd build
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/
cmake -D BUILD_SHARED_LIBS=OFF ..
make -j4

opencvのためのclojureプロジェクトの設定

今回はclj-opencv-exampleというプロジェクトを作り、opencvの操作を試みます。
cd gitprojects
lein new clj-opencv-swing-example

ビルドしたopencvのjarファイルとsoファイルを、clojureのプロジェクトのlibに配置します。
cd ~/gitprojects/clj-opencv-swing-example
mkdir lib
cp ~/gitprojects/opencv_source/build/bin/opencv-320.jar lib/
cp ~/gitprojects/opencv_source/build/lib/libopencv_java320.so lib/

今回はlinuxなのでsoファイルを必要としますが、macの場合はdylib、windowsの場合はdllファイルがそれぞれ必要なようです。


libに配置したopencvのファイルを読み込めるように、project.cljを下記のように記述します。
~/gitprojects/clj-opencv-swing-example/project.clj
(defproject clj-opencv-swing-example "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/core.async "0.3.442"]]
  :jvm-opts ["-Djava.library.path=lib"]
  :resource-paths ["lib/opencv-320.jar"]
  :injections [(clojure.lang.RT/loadLibrary org.opencv.core.Core/NATIVE_LIBRARY_NAME)]
  :main clj-opencv-swing-example.core)

サンプル画像として使いたいので、lenaも配置します。
cd ~/gitprojects/clj-opencv-swing-example
mkdir img
cp ~/gitprojects/opencv_source/samples/data/lena.jpg img/

replが立ち上がることを確認します。
cd ~/gitprojects/clj-opencv-swing-example
lein repl

自分の環境だとleinコマンド実行時に下記のwarningが出ました。
OpenJDK 64-Bit Server VM warning: You have loaded library /home/asuki/gitprojects/clj-opencv-swing-example/lib/libopencv_java320.so which might have disabled stack guard. The VM will try to fix the stack guard now.
It's highly recommended that you fix the library with 'execstack -c <libfile>', or link it with '-z noexecstack'.

warningの指示通り下記のようにexecstackを実行すると、warningは出なくなります。
sudo apt install execstack
cd ~/gitprojects/clj-opencv-swing-example
execstack -c lib/libopencv_java320.so

プロジェクトの設定ができました。

プログラムの説明

core.cljを下記のように記述しました。
定義した関数を説明します。

~/gitprojects/clj-opencv-swing-example/src/clj_opencv_swing_example/
(ns clj-opencv-swing-example.core
  (:import [java.awt.image BufferedImage]
           [javax.swing JFrame JLabel ImageIcon WindowConstants SwingUtilities]
           [org.opencv.core Core Mat CvType]
           [org.opencv.imgcodecs Imgcodecs]
           [org.opencv.imgproc Imgproc])
  (:require [clojure.core.async :as async]))

(def color-lena-mat (Imgcodecs/imread "img/lena.jpg"))
(def j-frame (new JFrame))

(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 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 color->gray [mat]
  (let [gray-mat (new Mat)]
    (Imgproc/cvtColor mat gray-mat Imgproc/COLOR_RGB2GRAY)
    gray-mat))

(defn -main []
  (config-j-frame j-frame
                  :title "opencv mat on swing"
                  :set-visible true
                  :close-operation WindowConstants/EXIT_ON_CLOSE
                  :width (.width color-lena-mat) :height (.height color-lena-mat))
  (let [color-buffered-image (create-buffered-image color-lena-mat)
        _ (update-buffered-image color-buffered-image color-lena-mat)
        color-panel (->> color-buffered-image
                        (new ImageIcon)
                        (new JLabel))
        gray-lena-mat (color->gray color-lena-mat)
        gray-buffered-image (create-buffered-image gray-lena-mat)
        _ (update-buffered-image gray-buffered-image gray-lena-mat)
        gray-panel (->> gray-buffered-image
                            (new ImageIcon)
                            (new JLabel))]
    (async/go-loop [seconds 1]
      (async/<! (async/timeout 1000))
      (-> j-frame
          .getContentPane
          .removeAll)
      (.add j-frame (if (even? seconds) color-panel gray-panel))
      (.revalidate j-frame)
      (.repaint j-frame)
      (recur (inc seconds)))))

JFrameの初期設定

opencvは3.0からjava apiではhighGUIの提供しなくなっています。
そのため、このプログラムではjavaxのswingというライブラリを画像の表示に利用しています。

JFrameとは、swingが提供する画面を呼び出すクラスです。
config-j-frameという関数の中で、タイトル、表示の有無、終了動作、大きさを設定しています。

closeOperatoinにWindowConstants/EXIT_ON_CLOSEを設定すると、windowの閉じるボタンが押されたらプログラムを終了するようになります。

画像表示の流れ

opencvのmatをJFrameで表示するには下記のような処理を行います。

  1. JFrameに表示したいmatと同じサイズのBufferedImageを作る
  2. BufferedImageからImageIconを作る
  3. ImageIconからJLabelを作る
  4. JLabelをJFrameに追加
  5. BufferedImageに表示したいmatの情報をコピー
  6. JFrameを更新

上記の1-3をmainのgo-loopの前で行っています。

JFrameで表示している内容を更新したい場合は、下記の2つのどちらかの方法があります。

画像のサイズやチャンネル数(カラーかグレーか)が違う場合

  1. JFrameとJLabelの紐付けを削除
  2. 別のJLabelをJFrameに追加
  3. JLabelに紐づくBuffferedImageが空なら、表示したいmatの情報をコピー
  4. JFrameを更新


画像サイズとチャンネル数が同じ場合

  1. BufferedImageに表示したいmatの情報をコピー
  2. JFrameを更新


今回はチャンネル数の違う画像(カラーとグレー)を交互に表示したいので、「画像サイズやチャンネル数が違う場合」の処理をmainのgo-loopの中でカラーとグレーに対して交互に実施しています。

実行

下記のコマンドで実行できます。
cd ~/gitprojects/clj-opencv-practice
lein run

lenaの画像が表示されます。


1秒ごとにカラーと白黒が入れ替わります。


今回作成したプロジェクトはこちらです。

https://github.com/asukiaaa/clj-opencv-swing-example

共有する情報は以上です。

参考

ClojureでOpenCV 3.0と戯れる
milq/scripts/bash/install-opencv.sh
Introduction to Java Development -- OpenCV3.0.0-dev
http://docs.opencv.org/java/3.1.0/
二次元絵の顔を検出する
SwingでMat型画像をGUI上に表示
Clojureでjavax.swing.JFrameを使ってみる
Opencv java - Load image to GUI
How to add an image to a JPanel?
ImShow-Java-OpenCV/ImShow_JCV/src/com/atul/JavaOpenCV/Imshow.java
How to remove all components from a JFrame in Java?

変更情報

2017.09.17
color->grayの処理の間違いを修正しました。
「apt-install」を「apt install」に修正しました。

2018.1.23
BufferdImageのnewを繰り返すとOutOfMemoryErrorが発生することが分かりました。
それを防ぐため、loopの前にBufferedImageなどを作成し、それらを使いまわすプログラムに変更しました。
変更前のプログラムはこちらです。

0 件のコメント :