Node.jsでもOpenCVしてみる

Node.jsでもOpenCVしてみる

少し前のエントリではPythonを使用してOpenCVで顔認識をしていたんですが、モノによってはnode.jsでも同様なことができるかなと思いますので 実験をやってみました。

node.jsでもopencvのモジュールがあります。ただ、OpenCVは2系であることに注意です。

www.npmjs.com

github.com

以前のエントリでRaspberryPiにインストールしたのは2系ですので問題なく動作します。(version 2.4.9)

uepon.hatenadiary.com

opencvモジュールのインストール

node.jsのインストールに関しては

uepon.hatenadiary.com

こちらで取り上げましたのでRaspbianのオリジナルのNode.jsではないことに注意です。npmopencvをインストールすればOKです。

$ npm install opencv
-
> opencv@6.0.0 install /home/pi/node_opencv/node_modules/opencv
> node-pre-gyp install --fallback-to-build

make: Entering directory '/home/pi/node_opencv/node_modules/opencv/build'
  CXX(target) Release/obj.target/opencv/src/init.o
  CXX(target) Release/obj.target/opencv/src/Matrix.o
  CXX(target) Release/obj.target/opencv/src/OpenCV.o
  CXX(target) Release/obj.target/opencv/src/CascadeClassifierWrap.o
  CXX(target) Release/obj.target/opencv/src/Contours.o
  CXX(target) Release/obj.target/opencv/src/Point.o
  CXX(target) Release/obj.target/opencv/src/VideoCaptureWrap.o
  CXX(target) Release/obj.target/opencv/src/CamShift.o
  CXX(target) Release/obj.target/opencv/src/HighGUI.o
  CXX(target) Release/obj.target/opencv/src/FaceRecognizer.o
  CXX(target) Release/obj.target/opencv/src/Features2d.o
  CXX(target) Release/obj.target/opencv/src/BackgroundSubtractor.o
  CXX(target) Release/obj.target/opencv/src/Constants.o
  CXX(target) Release/obj.target/opencv/src/Calib3D.o
  CXX(target) Release/obj.target/opencv/src/ImgProc.o
  CXX(target) Release/obj.target/opencv/src/Stereo.o
  CXX(target) Release/obj.target/opencv/src/LDAWrap.o
  SOLINK_MODULE(target) Release/obj.target/opencv.node
  COPY Release/opencv.node
  COPY /home/pi/node_opencv/node_modules/opencv/build/opencv/v6.0.0/Release/node-v46-linux-arm/opencv.node
  TOUCH Release/obj.target/action_after_build.stamp
  CXX(target) Release/obj.target/test_nativemat/test/nativemat.o
  SOLINK_MODULE(target) Release/obj.target/test_nativemat.node
  COPY Release/test_nativemat.node
make: Leaving directory '/home/pi/node_opencv/node_modules/opencv/build'
opencv@6.0.0 node_modules/opencv
tqq buffers@0.1.1
tqq nan@2.6.1
mqq istanbul@0.4.5 (abbrev@1.0.9, async@1.5.2, wordwrap@1.0.0, nopt@3.0.6, esprima@2.7.3, once@1.4.0, supports-color@3.2.3, which@1.2.14, mkdirp@0.5.1, resolve@1.1.7, glob@5.0.15, js-yaml@3.8.3, escodegen@1.8.1, handlebars@4.0.6)

比較的長い時間がかかります。(RaspberryPi2では10分ちょっとかかっています) これが終わるといよいよOpenCVの世界に入れます。

静止画像の顔認識

まずは静止画に含まれる顔認識を行ってみます。 認識をする画像はinput.jpg解析に成功した場合には、 顔と思われる部分に赤色の四角形を描画し、output.jpgとして画像を出力します。

若干書きぶりが異なりますが、なんとなくは同じ様です。oencvパッケージに含まれるサンプルを参考にしているので分類機の読み込みが異なるようです。

CascadeClassifierdetectMultiScaleでの呼び出しではなく、detectObjectで引数での呼び出しとなっています。

OpenCVサンプルフォルダ】 github.com

【face-detect.js】

var cv = require('opencv');

var RED = [0, 0, 255];
var THICKNESS = 2;

cv.readImage("./input.jpg", function(err, im){
  if (err) throw err;
  if (im.width() < 1 || im.height() < 1) throw new Error('error:画像が不正の様です。');

  im.detectObject("./haarcascade_frontalface_alt.xml", {}, function(err, faces){
    for (var i=0;i<faces.length; i++){
      var face = faces[i]
      im.rectangle([face.x, face.y], [face.width, face.height], RED, THICKNESS);
    }
    console.log('イメージを保存しました');
    im.save('./output.jpg');
  });
})

実行のためには、あらかじめプログラムと同じディレクトリにhaarcascade_frontalface_alt.xmlなど分類機設定のファイルがないとエラーになりますので注意してください。

実行させると…

$ node.js face-detect.js

【input画像】 f:id:ueponx:20170410232450j:plain ライセンス: CC0 Public DomainCreative Commons — CC0 1.0 Universal

【output画像】 f:id:ueponx:20170410232511j:plain

Webカメラの画像の取り込み

カメラ画像の取り込みに関しては基本的にpythonと違いはありません。 OpenCVGUIを使えますが、node.js経由でも同様にGUIを使用することができます。

【camera.js】

var cv = require('opencv');

try {
  var camera = new cv.VideoCapture(0);
  var window = new cv.NamedWindow('Video', 0)

  setInterval(function() {
    camera.read(function(err, im) {
      if (err) throw err;
      console.log(im.size())
      if (im.size()[0] > 0 && im.size()[1] > 0){
        window.show(im);
      }
      window.blockingWaitKey(0, 50);
    });
  }, 20);
  
} catch (e){
  console.log("Couldn't start camera:", e)
}

実行させると…

$ node.js camera.js

【動作画像】 f:id:ueponx:20170410232605j:plain

多少のアラームは発生するのですが問題なく動作しました。使用したWebカメラMicrosoftのLifeCamになります。(以前の動作までに少し時間がかかる挙動に関係するのだと思います)あとpythonからの呼び出しとは異なってデフォルト解像度は320*240ぽいですね。

Webカメラで顔認識を行う

あとはこれら2つを組み合わせていきます。

【face-detect-cam.js】

var cv = require('opencv');

var RED = [0, 0, 255];
var THICKNESS = 2;

try {
  var camera = new cv.VideoCapture(0);
  var window = new cv.NamedWindow('Video', 0);

  camera.setWidth(480);
  camera.setHeight(320);

  setInterval(function() {
    camera.read(function(err, im) {
      if (err) throw err;

      if (im.size()[0] > 0 && im.size()[1] > 0){
        im.detectObject("./haarcascade_frontalface_alt.xml", {}, function(err, faces){
          for (var i=0;i<faces.length; i++){
            var face = faces[i]
            im.rectangle([face.x, face.y], [face.width, face.height], RED, THICKNESS);
          }
          window.show(im);
        });
        window.blockingWaitKey(0, 50);
      }
    });
  }, 500);

} catch (e){
  console.log("Couldn't start camera:", e)
}

実行させると…

$ node.js face-detect-cam.js

【動作画像】

f:id:ueponx:20170411094557j:plain

このコードでは動作に関してはかなり遅いと思います。遅延が3秒ほどありそうでした。

今回はsetInterval()を使って周期処理を行っているのですが、直感的には whileでループを回せばいいのかなと思ったんですが、うまくいきません。 javascriptsleep()とかwait()が無いことに起因してるかなと思います。

あと、setInterval()の周期のパラメータを小さくすると結構な確率でSegmentation faultが発生するので500msec以上にしたほうがいいかなと思います。

終わりに

一応、node.jsでOpenCVを使ったプログラムを組んでみました。今後そういうこともあるかなとは思います。RaspberryPi2では少しパワー不足という印象がありますが、ホストが一般的なPCならもっといいパフォーマンスをだせるのかなと思います。

これが限界なのかなと色々おもっていたのですが、Xを使ってリモートで接続していることに気が付きました。

ダイレクトにモニタに接続するとsetInterval()の周期のパラメータを250程度まで小さくしてもSegmentation faultもほとんど発生せず、遅延も2秒程度まで追い込めました。

ちなみにファイルに一度保存したほうが早いのではないかと思って、以下のコードを書いてみましたが、全然早くはならず、逆に処理が追いつかずSegmentation faultの確率が上がったように感じます。こちらはディスプレイの直接接続でもSegmentation faultが発生するようでした。

var cv = require('opencv');

var RED = [0, 0, 255];
var THICKNESS = 2;

try {
  var camera = new cv.VideoCapture(0);
  var window = new cv.NamedWindow('Video', 0);

  camera.setWidth(480);
  camera.setHeight(320);

  setInterval(function() {
    camera.read(function(err, im) {
      if (err) throw err;

      if (im.size()[0] > 0 && im.size()[1] > 0){
        im.detectObject("./haarcascade_frontalface_alt.xml", {}, function(err, faces){
          for (var i=0;i<faces.length; i++){
            var face = faces[i]
            im.rectangle([face.x, face.y], [face.width, face.height], RED, THICKNESS);
          }
          im.save('./tmp.jpg');
        });
        cv.readImage("./tmp.jpg", function(err2, im2){
          if (err2) throw err2;
          
          if (im2.size()[0] > 0 && im2.size()[1] > 0){
            window.show(im2);
          }
        });
        window.blockingWaitKey(0, 50);
      }
    });
  }, 500);

} catch (e){
  console.log("Couldn't start camera:", e)
}

RaspberryPiでL-02Cを使ってSORACOM Airに接続する

RaspberryPiでL-02Cを使ってSORACOM Airに接続する

仕事が落ち着いたのでいろいろやりたいことをぼちぼちと行っています。

昨年、会社の移転に伴い、廃棄物品の中にL-02Cがあったのでいただくことにしました。(一応、ご自由にどうぞということだったので)

docomo L-02C レッド(R) データ通信専用機種

L-02Cはdocomoさんの商品ですが、実際はLGさんの商品になります。

f:id:ueponx:20170408115404j:plain

今回はSORACOM AirのSIMを使って接続を行っていこうと思います。SIMは2016年のSoftwareDesign5月号の付録についていたものをアクティベートしていたのでそれを使う予定でした。ただ、SIMサイズが合わなかったので以下のアダプタで対応しました。

安!

f:id:ueponx:20170408115336j:plain

こんな感じで装着します。

今回は以下のRaspberryPiの環境で行っています。

$ uname -a
Linux raspberrypi 4.4.34-v7+ #930 SMP Wed Nov 23 15:20:41 GMT 2016 armv7l GNU/Linux

また、以下の記事を参考にしています。

www.mana-cat.com

SORACOM AirのSIM設定を事前にしておく。

今回はアクティベート済みのSIMなのですが、念のためSORACOM Airのユーザーコンソールで確認しておきます。

soracom.jp

ユーザーコンソールへ

f:id:ueponx:20170408111602j:plain

ユーザーコンソールでSIMの状況を確認

f:id:ueponx:20170408111653j:plain

大丈夫そうですね。

L-02CをRaspberryPiに接続する

L-02Cが意外と大きいのでその他のUSB機器とコネクタの干渉をしてしまうので延長ケーブルを経由したほうが無難です。

USBに指すとdmesgの結果が以下のように変化しました。ネットで出ている情報ではL-02CはCDROMドライブとして認識されるということだったのですが、今回はCDROMにはなっていませんでした。なぜ?

L-02Cはドライバーなどを本体に持っている機器のため、そのまま接続するとCDROMドライブとして見え、ドライバインストール後はモデムとしてみえるタイプのデバイスの様です。

$ dmesg |tail
[ 6026.677568] option 1-1.4:1.0: GSM modem (1-port) converter detected
[ 6026.677996] usb 1-1.4: GSM modem (1-port) converter now attached to ttyUSB0
[ 6026.678756] option 1-1.4:1.1: GSM modem (1-port) converter detected
[ 6026.679132] usb 1-1.4: GSM modem (1-port) converter now attached to ttyUSB1
[ 6026.679870] option 1-1.4:1.2: GSM modem (1-port) converter detected
[ 6026.680318] usb 1-1.4: GSM modem (1-port) converter now attached to ttyUSB2
[ 6026.681146] option 1-1.4:1.3: GSM modem (1-port) converter detected
[ 6026.681537] usb 1-1.4: GSM modem (1-port) converter now attached to ttyUSB3
[ 6027.411662] w1_master_driver w1_bus_master1: Family 0 for 00.3e0000000000.a1 is not registered.
[ 6086.671765] w1_master_driver w1_bus_master1: Family 0 for 00.be0000000000.2d is not registered.

たまたま何でしょうか?あるいはOSのバージョンアップなどの要因でしょうか?会社で使っているときの設定でなにかあったのでしょうか。

正解はUSBシリアルとして認識されることなので問題はないのですが…。 念のためlsusbコマンドでも確認します。

【接続前】

$ lsusb
Bus 001 Device 005: ID 045e:075d Microsoft Corp. LifeCam Cinema
Bus 001 Device 004: ID 0411:01ee BUFFALO INC. (formerly MelCo., Inc.) WLI-UC-GNM2 Wireless LAN Adapter [Ralink RT3070]
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

【接続後】

$ lsusb
Bus 001 Device 006: ID 1004:61dd LG Electronics, Inc.
Bus 001 Device 005: ID 045e:075d Microsoft Corp. LifeCam Cinema
Bus 001 Device 004: ID 0411:01ee BUFFALO INC. (formerly MelCo., Inc.) WLI-UC-GNM2 Wireless LAN Adapter [Ralink RT3070]
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

Bus 001 Device 006: ID 1004:61dd LG Electronics, Inc.のエントリが追加されているので正常に認識されているようです。

wvdialejectをパッケージをインストール

あとはUSBモデムのダイヤラーであるwvdialパッケージをインストールします。 今回はCDROMとして認識されていないので必要ないのですがejectパッケージは念のためインストールします。

$ sudo apt-get update
$ sudo apt-get install wvdial eject
()
Success!  You can run "wvdial" to connect to the internet.
  (You can also change your configuration by editing /etc/wvdial.conf)
()

CDROMとして認識されているようであれば sudo eject sr0を実行してください。

$ sudo modprobe usbserial vendor=0x1004 product=0x618f
$ sudo eject sr0
$ sudo chmod 666 /dev/ttyUSB*

これでインストールは完了しました。

wvdialの接続設定を行う

インストール時にも表示されましたが

Success! You can run "wvdial" to connect to the internet. (You can also change your configuration by editing /etc/wvdial.conf)

とのことですので/etc/wvdial.confの設定を行います。

$ sudo vim /etc/wvdial.conf

【インストール後のオリジナル設定ファイル】

[Dialer Defaults]
Init1 = ATZ
Init2 = ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
Modem Type = Analog Modem
Baud = 9600
New PPPD = yes
Modem = /dev/ttyUSB2
ISDN = 0
; Phone = <Target Phone Number>
; Password = <Your Password>
; Username = <Your Login Name>

【変更後】

[Dialer Defaults]
Init1 = ATZ
Init2 = ATH
Init3 = AT+CGDCONT=1,"IP","soracom.io"
Init4 = ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
Dial Attempts = 3
Stupid Mode = 1
Modem Type = Analog Modem
Dial Command = ATD
New PPPD = yes
APN = soracom.io
Modem = /dev/ttyUSB2
Baud = 460800
ISDN = 0
Phone = *99***1#
Username = sora
Password = sora
Carrier Check = no

内容は初期化するためのATコマンドやダイアル先、スピード、認証、APNなどになります。SORACOM Airはほぼ同じ設定になるかと思います。

SORACOM Airで接続

すべての設定が終わったので後は接続テストになります。以下で実行することになりますがコンソールが占領されるので&をつけるか、もう一つコンソールを起動してください。自分はコンソールもう一つ起動しています。

$ sudo wvdial
--> WvDial: Internet dialer version 1.61
--> Initializing modem.
--> Sending: ATZ
ATZ
OK
--> Sending: ATH
ATH
OK
--> Sending: AT+CGDCONT=1,"IP","soracom.io"
AT+CGDCONT=1,"IP","soracom.io"
OK
--> Sending: ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
OK
--> Modem initialized.
--> Sending: ATD*99***1#
--> Waiting for carrier.
ATD*99***1#
CONNECT
--> Carrier detected.  Starting PPP immediately.
--> Starting pppd at Sat Apr  8 10:04:58 2017
--> Pid of pppd: 29671
--> Using interface ppp0
--> pppd: ー[01]黐・5
--> pppd: ー[01]黐・5
--> pppd: ー[01]黐・5
--> pppd: ー[01]黐・5
--> pppd: ー[01]黐・5
--> pppd: ー[01]黐・5
--> local  IP address ???.???.???.???
--> pppd: ー[01]黐・5
--> remote IP address ???.???.???.???
--> pppd: ー[01]黐・5
--> primary   DNS address ???.???.???.???
--> pppd: ー[01]黐・5
--> secondary DNS address ???.???.???.???
--> pppd: ー[01]黐・5

接続されるとpppのインターフェースにIPアドレスDNSなどが設定されてネットワーク接続ができるようになります。

接続状況の確認

ipコマンドで確認するとppp0インターフェースが追加されていることがわかります。

【接続前のネットワークインターフェース】

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000
    (略)
       valid_lft forever preferred_lft forever
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    (略)
       valid_lft forever preferred_lft forever

【接続後のネットワークインターフェース】

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000
    (略)
       valid_lft forever preferred_lft forever
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    (略)
       valid_lft forever preferred_lft forever
4: ppp0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 3
    link/ppp
    inet ???.???.???.??? peer ???.???.???.???/?? scope global ppp0
       valid_lft forever preferred_lft forever

4番目のエントリにppp0インターフェースが追加されています。

疎通確認

ローカルIPに対してのPING実行を行う

???.???.???.???の部分は接続後のipコマンドから調べて置き換えてください。

$ ping ???.???.???.???
PING ???.???.???.??? (???.???.???.???) 56(84) bytes of data.
64 bytes from ???.???.???.???: icmp_seq=1 ttl=64 time=0.169 ms
64 bytes from ???.???.???.???: icmp_seq=2 ttl=64 time=0.098 ms
64 bytes from ???.???.???.???: icmp_seq=3 ttl=64 time=0.115 ms
64 bytes from ???.???.???.???: icmp_seq=4 ttl=64 time=0.112 ms
64 bytes from ???.???.???.???: icmp_seq=5 ttl=64 time=0.114 ms
^C
--- ???.???.???.??? ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 3999ms
rtt min/avg/max/mdev = 0.098/0.121/0.169/0.027 ms

ローカル側のpingも成功しています。

ppp0インターフェースを経由してのPING実行

googleDNSpingを飛ばしてみます。

$ ping -I ppp0 8.8.8.8
PING 8.8.8.8 (8.8.8.8) from ???.???.???.??? ppp0: 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=55 time=68.2 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=55 time=70.8 ms
^C
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 68.234/69.549/70.865/1.341 ms

無事に接続できました。

終わりに

折角アクティベートしていたSORACOM AirのSIMがやっと活きました。作品のデモを行う展示会などではWiFiやBulethoothが入らない事があるので通信がうまくいかないことも多いのですが、USBモデムとSORACOM Airがあることで回避策を一つ追加して保険がかけられるようになるのかなと思って期待しています。

RaspberryPiでOpenCVを使って顔認識を行ってみる

RaspberryPiでOpenCVを使って顔認識を行ってみる

ようやくやりたかったOpenCVを使った顔認識を行ってみます。MicrosoftさんやIBMさんのサービスなどのWebサービスで顔認識ができるのでそれほど必須というわけでは無いのですが、ネットワークサービスが使えない場合や、回数制限などがあって頻繁にWebAPIを叩けない場合にはローカルの処理でもそこそこ似たような動作ができると助かる局面はあると思います。

例えば、顔認識はローカルのOpenCVで行って、顔が検知できたらWebAPIにアクセスすることで表情の解析を行うというような場合です。以前作成した「ヒミツのくまちゃん」 では、周期的にMicrosoftEmotionAPIを使用しているので、APIの回数制限のため長期間は動作できないことになります。そこで顔を見つけるまではローカル処理でそれなりに対応することで、WebAPIへのアクセス回数を減らすことができるようになります。

【参考】 uepon.hatenadiary.com

以前の記事でOpenCVをインストールしていることを前提にします。

uepon.hatenadiary.com

顔認識を行う。

かなり行われていることなのでネットの情報もかなり充実しています。自分も他のかたのブログなどを参考させていただきます。

自分はこちらを参考にさせていただきました。その為、今回のエントリーでは自分がやったことのメモという扱いになります。

famirror.hateblo.jp

顔認識の流れ

顔認識では大まかに以下のような処理を行います。

  1. 分類器の読み込み
  2. 画像読み込み
  3. BGR2GRAY変換(グレースケールへの変換)
  4. 顔検出処理

ここで分類器という言葉がでています。OpenCVを使った顔認識では「カスケード分類器」というものを使用して顔と非顔の分別を行う様です。

分類器とは?

カスケード型分類器 — opencv 2.2 (r4295) documentation

う~。

自分なりの解釈としては

正しいオブジェクトと間違ったオブジェクトから特徴量を学習して作った分類器から 拡大や回転などの変換をかけて一致するかしないかを検出する方法

分類器を作成するのは荷が重いのでOpenCVにデフォルトでついている分類器を使用することにします。ただし、パッケージのインストールをつかっているので分類器は別途ソースコードについているサンプルから取ってくる必要があります。

OpenCVソースコードGitHubにありますが、分類器は以下のパスにあります。

github.com

今回はhaarcascade_frontalface_default.xmlを使うことにしました。これをプログラムを置くパスにダウンロードしておきます。分類器は20あまりもあるので、ほかのものを選んでもいいのかも。

画像ファイルに含まれる顔を認識する

では、画像ファイルから顔認識をしてみます。見つかった場合には顔として認識されたエリアを赤い四角で囲みファイルとして出力をします。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cv2

faceCascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')

img = cv2.imread('image.jpg', cv2.IMREAD_COLOR)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
face = faceCascade.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=2, minSize=(10, 10))

if len(face) > 0:
    for rect in face:
        cv2.rectangle(img, tuple(rect[0:2]), tuple(rect[0:2]+rect[2:4]), (0, 0,255), thickness=2)
else:
    print "no face"

cv2.imwrite('detected.jpg', img)

サンプルとしてはネットを探してでてきものを使ってみます。オリジナルは以下になります。

f:id:ueponx:20170406160132j:plain

ライセンス: CC0 Public DomainCreative Commons — CC0 1.0 Universal

これをプログラムに処理させると以下のような画像が出力されます。

f:id:ueponx:20170406160603j:plain

顔認識の精度は微妙ではありますが認識は行われたようです。デフォルトなんでこんなもんでしょう。ちなみに別の分類器を使えばこんな感じにもなります。(haarcascade_frontalface_alt.xml

f:id:ueponx:20170406160841j:plain

コードをみる

分類器の読み込み

faceCascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')

OpenCVのCascadeClassifier()メソッドを使用して分類器の読み込みを行います。引数に与えるXMLファイルが分類器に相当します。

画像読み込み

img = cv2.imread('image.jpg', cv2.IMREAD_COLOR)

OpenCVのimread()メソッドを使用して画像の読み込みを行います。 特定のファイルではなくプログラムの引数で与えるのであれば

args = sys.argv
img = cv2.imread(args[1], cv2.IMREAD_COLOR)

としてもいいかと思います。

BGR2GRAY変換(グレースケール変換)

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

OpenCVのcvtColor()メソッドを使用して画像のグレースケール変換を行います。

顔検出処理

face = faceCascade.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=2, minSize=(10, 10))

ここまできてやっと顔検出のを行います。 第2引数の1.2がscaleFactorであり、分類器の精度と処理スピードに影響するパラメータとなります。以下のリンクに解説がありました。

answers.opencv.org

数値を小さくすると検出するが上がるのですが、誤検知も増え、処理時間も増えると考えていいみたいです。画像の解像度が高いと処理時間が数秒になるのでWebCameraを使うときには調整が必要そうです。(WebCameraの解像度を下げればいいのですけど。)

今回のサンプルでは処理が終わるのに5秒ぐらいかかっていました。もう少しゆるい制限にしたほうが現実的だと思います。

WebCameraの映像に含まれる顔を認識する

今度はこれをWebCameraで行ってみたいと思います。

前回エントリではcapture = cv.CaptureFromCAM(0)って感じでキャプチャデバイスの設定していましたが今回はcapture = cv2.VideoCapture(0)って感じでキャプチャデバイスを取得しています。後者のほうがより一般的なキャプチャデバイスへの対応ができるのかなと思います。(スピードはあんまり変わりません)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cv2
import time

faceCascade = cv2.CascadeClassifier('haarcascade_frontalface_alt.xml')

capture = cv2.VideoCapture(0) # カメラセット
# 画像サイズの指定
ret = capture.set(3, 480)
ret = capture.set(4, 320)

i = 0
while True:
    start = time.clock() # 開始時刻
    ret, image = capture.read() # 画像を取得する作業
    gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    face = faceCascade.detectMultiScale(gray_image, scaleFactor=1.3, minNeighbors=2, minSize=(30, 30))

    if len(face) > 0:
        for rect in face:
            cv2.rectangle(image, tuple(rect[0:2]), tuple(rect[0:2]+rect[2:4]), (0, 0,255), thickness=2)

    get_image_time = int((time.clock()-start)*1000) # 処理時間計測
    # 1フレーム取得するのにかかった時間を表示
    cv2.putText( image, str(get_image_time)+"ms", (10,10), 1, 1, (0,255,0))

    cv2.imshow("Camera Test",image)
    # キーが押されたら保存・終了
    if cv2.waitKey(10) == 32: # 32:[Space]
        cv2.imwrite(str(i)+".jpg",image)
        i+=1
        print("Save Image..."+str(i)+".jpg")
    elif cv2.waitKey(10) == 27: # 27:Esc
        capture.release()
        cv2.destroyAllWindows()
        break

処理時間の測定をしているのはパラメータの変更の効果をみるためになります。 また、スペースを押すとその時点での認識状況の保存ができるようになっています。

パラメータに関しては

scaleFactor=1.3, minNeighbors=2, minSize=(30, 30)

としました。RaspberryPi2では秒間2-3回程度の認識ができるようにしています。 パラメータで一番効果があったのはscaleFactor=1.3でした。1.2あたりでは処理時間が倍くらいかかってしまいました。また、キャプチャの解像度を下げるのも効果があります。ただし、キャプチャの解像度を下げると認識率がわるくなるのである程度にしておいたほうが無難です。デフォルトの640*480のサイズで動かすと認識に1.6秒ほどかかりました。

以下のような認識ができました。

f:id:ueponx:20170406210903j:plain

f:id:ueponx:20170406211019j:plain

おわりに

かなり昔から度々やろうと思っていたことがやっとできました。

最近ではWebAPIを使えば比較的簡単に同じことはできますが、通信制限がある場合には、OpenCVとWebAPIを組み合わせるなどのことも考えていければいいかなと思っています。

あと、RaspberryPiではやはりパワー不足は否めませんので、Xの機能をいっその事削除したり、PCを使って動かしたほうがいいかなと思います。別の処理系であってもpythonなのでそのままコードが使用できるのはいいですね。

RaspberryPiにPhantomJSをインストールする

RaspberryPiにPhantomJSをインストールする

昔作ったPhantomJSのプログラムをRaspberryPiでも実行したいと思ったのですが、RaspberryPiのパッケージがなく、 ビルドにも数時間かかるということだったので諦めていました。そろそろ時間もできたのでインストールすることにしました。

PhantomJS | PhantomJS

ネットを探すと既にビルドしているものを公開している方がいらっしゃったので、そちらを使うことにしました。 自分でビルドするつもりでしたが、さすがに5時間とかかかると言われると厳しいです。

以下のサイトを参考にさせていただきました。ビルドしたバイナリも公開していただいているので大変ありがたいです。(バージョンは2.1.1なので新しいです!)

https://mecrazy.net/ja/2016/06/06/raspberry-pi%E7%94%A8%E3%81%ABphantomjs-2-1-1%E3%82%92%E3%83%93%E3%83%AB%E3%83%89%E3%81%97%E3%81%A6%E3%81%BF%E3%81%BE%E3%81%97%E3%81%9F/

他にも

www.miyakawa.link

github.com

こちらのサイトもあるんですが、少しバージョンが古かったので前者にしています。

インストールする

ビルドされたバイナリは以下にありますのでgitを使って取得します。

github.com

ビルド時に必要なパッケージも事前にインストールしていますが念のためということで。

$ sudo apt-get install build-essential g++ flex bison gperf ruby perl libsqlite3-dev libfontconfig1-dev libicu-dev libfreetype6 libssl-dev libpng-dev libjpeg-dev
$ git clone https://github.com/mecrazy/phantomjs-binaries.git
$ chmod 755 phantomjs-2.1.1-linux-armhf
$ sudo apt-get install fonts-ipafont
$ sudo ln -sf `pwd`/bin/phantomjs-2.1.1-linux-armhf /usr/local/bin/phantomjs

cloneしたディレクトリにあるbinディレクトリにバイナリがあります。phantomjsもあります(多分x86なバイナリのリンク?)が、今回使用するのはarm版のphantomjs-2.1.1-linux-armhfになります。これを/usr/local/bin/シンボリックリンクを作成します。これで準備完了です。

以下のようにコマンドを実行するとバージョン出ます。これでOKです。

$ phantomjs --version
2.1.1

サンプルっぽいものを実行してみる

PhantomJSはWebkitベースのHeadlessブラウザなので、googleのトップページにアクセスしてスクリーンキャプチャをしてみます。

//sample.js
var page = require('webpage').create();
page.open('https://google.com', function(status) {
  console.log("Status: " + status);
  if(status === "success") {
    //screen capture
    page.render('google.png');
  }
  phantom.exit();
});

実行すると、画面上にURLアクセスしたステータスを表示して終了します。 実行ディレクトリにgoogle.pngができていれば成功です。

$ phantomjs sample.js
Status: success
$ ls
google.png  sample.js

google.pngは以下のような画像になります。(フォントをインストールしてないと文字化けしますので注意です。)

f:id:ueponx:20170316133617p:plain

簡単な説明

ソースの一行目でブラウザオブジェクトを作成し

var page = require('webpage').create(); // オブジェクト生成

指定されたURL(https://google.com)にアクセスし、アクセスに成功したら画面イメージをキャプチャします。

page.open('https://google.com', function(status) { // URLへアクセス
  console.log("Status: " + status); // 実行のステータスをコンソール上に表示
  if(status === "success") { //アクセスに成功したら
    //screen capture
    page.render('google.png'); //スクリーンキャプチャする
  }
  phantom.exit(); //実行終了
});

説明になってないような気もしますが、サンプルなので…

おわりに

公開されたバイナリがあって圧倒的感謝・・・・!

http://cdn-ak.f.st-hatena.com/images/fotolife/g/gotocc/20101123/20101123074729.jpg

RaspberryPiでOpenCVを使ってみる【つまづき編】

RaspberryPiでOpenCVを使ってみる【つまづき編】

f:id:ueponx:20170226143214j:plain

やっと、OpenCVに手をだす感じになってきたのですが、即挫折させられるという運の無さ。 急いでいる人は以下の手順でOKです。

$ sudo apt-get install libopencv-dev
$ sudo apt-get install python-opencv
$ sudo apt-get install libgl1-mesa-dri
$ x11vnc -storepasswd
$ sudo mkdir /home/pi/.config/autostart/
$ sudo vim /home/pi/.config/autostart/x11vnc.desktop (設定ファイルの中身は下を参照)
$ sudo vi /boot/config.txt(設定ファイルの中身は下を参照)
$ sudo raspi-config(設定後再起動)

インストール作業

以前のエントリでも、opnecvの開発パッケージ、pythonopencvモジュールの2つはインストールしていますので、あえてやらなくてもいいのかなとは思いますがバージョンアップもあるかなということで行いました。

$ sudo apt-get install libopencv-dev
$ sudo apt-get install python-opencv

一ヶ月ほど前に入れたにも関わらず、バージョンアップが行われていたので時間がかかりました。

インストールを行ったOpenCVのバージョンの確認方法

インストールするとバージョンがあとからわからなくなることがあるのですが以下のファイルにバージョンが記載されています。

$ less /usr/include/opencv2/core/version.hpp
(略)

/*
  definition of the current version of OpenCV
  Usefull to test in user programs
*/

#ifndef __OPENCV_VERSION_HPP__
#define __OPENCV_VERSION_HPP__

#define CV_VERSION_EPOCH    2
#define CV_VERSION_MAJOR    4

#define CV_VERSION_EPOCH    2
#define CV_VERSION_MAJOR    4
#define CV_VERSION_MINOR    9
#define CV_VERSION_REVISION 1

(略)

/* old  style version constants*/
#define CV_MAJOR_VERSION    CV_VERSION_EPOCH
#define CV_MINOR_VERSION    CV_VERSION_MAJOR
#define CV_SUBMINOR_VERSION CV_VERSION_MINOR

#endif

この時点(2017/02/25)でのインストールではversionは2.4.9になっていたようです。

インストールでは難なく完了するので後はサンプルを動かすだけです。以前のエントリーでも画面表示は行っていませんでしたがUSBカメラからのキャプチャは行っていましたので、どちらかというとGUIっぽい表示にすることがメインのチェックとなります。

OpenCVのサイトに行けばサンプルも結構あるのでそれを編集しています。

カメラ画像をX上で表示する使用するサンプルcamera.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cv2.cv as cv
import time

cv.NamedWindow("camera", 1)

capture = cv.CaptureFromCAM(0)

# 画像サイズの指定
cv.SetCaptureProperty(capture,cv.CV_CAP_PROP_FRAME_WIDTH,320)
cv.SetCaptureProperty(capture,cv.CV_CAP_PROP_FRAME_HEIGHT,240)

while True:
    img = cv.QueryFrame(capture)
    cv.ShowImage("camera", img)
    if cv.WaitKey(10) > 0:
        break
cv.DestroyAllWindows()

「カチャカチャカチャ・・・ッターンッ!!」これで出来上がったぜいぇーいとなるはずだったんです。

実行時エラーがでました。

$ python camera.py
Xlib:  extension "RANDR" missing on display ":11.0".

(camera:31765): GdkGLExt-WARNING **: Window system doesn't support OpenGL.

お前の環境だとOpenGLに対応してね~から実行は無理というものでした。オゥフ。 これまでは画像のキャプチャのみを行っていたので表示系まではやっていなかったので気がついてイなかったみたいです。というかXを使っていなかったというのが正しいw。

今回はコンソール(Teraterm)の代わりにWindowsにデフォルトでインストールされている【リモートデスクトップ接続】を使っていました。これが原因なんだろうなあと思って、気が重くなりました。個人的には経験からLinux系の設定のツラミはほぼほとんどがX関係っていうことが多くキツイ目にあっていたので。

といっても、GUIの無い画像処理はありえないと思うので解決を模索します。

早速エラーメッセージ関係でググってみると以下の記事にあたりました。

Raspberry Pi • View topic - Enable OpenGL on Raspbian Jessie for OpenCV

内容としては「OpenGL系の対応ライブラリが入っていないのでインストールせよ」というものでした。 では早速インストールしてみます。

$ sudo apt-get install libgl1-mesa-dri

動いてくれ頼む!

$ python camera.py
Xlib:  extension "RANDR" missing on display ":11.0".

(camera:31765): GdkGLExt-WARNING **: Window system doesn't support OpenGL.

変わっていねえ…(一部の数値とか変わるみたいですが、キャプチャ忘れました)

作戦変更へ

どうも以前入れたVNCサーバであるTightVNCserverがX接続すると別のディスプレイ接続環境を生成して、接続を行うという特徴があるというものだったので、純粋にハード的なXではなくソフトウエア上で動いている(表現がおかしいが)Xということだったようです。

X環境は基本設定をしないと共有されるはずですが、それがTightVNCserverにはなかったので若干不思議ではあったのですが、なんとなく理解できました。

そこでRaspberryPiのVNCサーバを

tightvncserver から x11vncに変更することにします。

X11vnc - ArchWiki

一応、ぐぐってみるとと実績があるようでした。

機能がバッティングしていることも考えてアンインストールしてからインストールを行っています。

$ sudo apt-get remove tightvncserver
$ sudo apt-get install x11vnc

インストールは全くトラブルなく終了しました。

x11vncの設定

以下のサイトが詳しく乗っていたので参考にしました。

www.1ft-seabass.jp

流れとしては

  • vnc接続用のパスワード設定
  • vncserverの起動
  • vncviewerからの接続

となります。

それが完了したら

*自動起動を設定

を行います。

vnc接続用のパスワード設定

vncではユーザという考え方以外にもvnc用のパスワードが存在しています。そのパスワードを作成することになります。

$ x11vnc -storepasswd
Enter VNC password:
Verify password:
Write password to /home/pi/.vnc/passwd?  [y]/n y
Password written to: /home/pi/.vnc/passwd

このコマンドで/home/pi/.vnc/passwdにパスワード情報が生成されます。このパスがパスワードファイルのデフォルト場所のようです。

vncserverの起動

後はVNCサーバを起動しますがテストであれば

$ x11vnc -usepw

の実行でいいかなと思います。オプションの-usepwは最初に設定したパスワードを使用するという宣言になります。

起動が正常にできて、このままずーっとVNCサーバを起動状態にする場合には以下の様に実行します。

$ x11vnc -usepw -forever

今回こそはとおもったんですが、失敗しました。

エラーメッセージの内容としては「VNCからXのディスプレイ:0に接続できないです」(意訳)というものでした。

ここからが苦痛でした。

エラーの内容からするとXがどうも立ち上がっていない様子。 しかし、HDMIを接続するとXは立ち上がりログインもできる。

そこで実験としてraspi-configコマンドでCLI環境とし、HDMIを接続しない状態でターミナルを2つ立ち上げて、一つでstartxを起動すると無事に起動しました。(WindowManegerはない状態ではあります。)そのあとにVNCServerを起動するとエラーは発生せず5900ポートで接続してほしい旨のメッセージがでました。

ここで仮説を立ててみました。 HDMIモニターを繋いでいるときにはXが起動している、RaspberryPiはHDMIを繋いでいないとコンポジット側に接続を替えるという仕様だったようなきがするので、強制的にHDMI出力をするモードに させてしまえばXは勝手に起動してくれる?と思い、/boot/config.txtを編集することを試してみました。

$ sudo vim /boot/config.txt

ファイル中ほどにある以下の部分のhdmi_force_hotplug=1のコメント化を解除します。

/boot/config.txtの変更前

(略)
# uncomment if hdmi display is not detected and composite is being output
# hdmi_force_hotplug=1
(略)

/boot/config.txtの変更後

(略)
# uncomment if hdmi display is not detected and composite is being output
hdmi_force_hotplug=1
(略)

ようやくこれでXは自動で起動してくれる様になりました。(これでほぼ1日潰れました) 後は前述のコマンドでVNCServerを起動します。

$ x11vnc -usepw
26/02/2017 13:29:15 -usepw: found /home/pi/.vnc/passwd
26/02/2017 13:29:15 x11vnc version: 0.9.13 lastmod: 2011-08-10  pid: 5984
26/02/2017 13:29:15 XOpenDisplay("") failed.
26/02/2017 13:29:15 Trying again with XAUTHLOCALHOSTNAME=localhost ...
26/02/2017 13:29:15

(略)

The VNC desktop is:      raspberrypi:0
PORT=5900

(略)

そしてリモートデスクトップ接続からRaspberryPiに接続、xrdp経由でVNCに接続となります。 注意点としては

  • 接続後のモジュール選択を【vnc-any】
  • IPには接続する【RaspberryPiのIPアドレス
  • portにはVNCServerの起動時に指定された【ポート番号(初期値は5900)】

を設定することになります。

ログイン画面のダイアログは以下の様になります。

f:id:ueponx:20170226132624j:plain

ここまできたら後は、目的のサンプルプログラムを実行してみます。

f:id:ueponx:20170226133643j:plain

うまく実行でき、やっと第一歩が踏み出せました。

後は自動起動の設定となります。

自動起動設定

まず、/home/pi/.config/autostart/というディレクトリを作成し、そのフォルダの中にx11vnc.desktopというファイルを作成します。

$ sudo mkdir /home/pi/.config/autostart/
$ sudo vim /home/pi/.config/autostart/x11vnc.desktop

x11vnc.desktop

[Desktop Entry]
Encoding=UTF-8
Type=Application
Name=X11VNC
Comment=
Exec=x11vnc -forever -display :0 -rfbauth /home/pi/.vnc/passwd
StartupNotify=false
Terminal=false
Hidden=false

続いてはraspi-configの設定をします。前述の設定ではログインしないと自動設定が行われないので自動でログインする設定にします。セキュリティとしては微妙ですが今回は目を瞑ります。

raspi-configを起動して【3. Boot Options】を選択

f:id:ueponx:20170226140556j:plain

画面が切り替わったら【B1. Desktop / CLI】を選択

f:id:ueponx:20170226140610j:plain

画面が切り替わったら【B4. Desktop Autologin Desktop GUI. automatically logged in as ‘pi’ user】を選択

f:id:ueponx:20170226140627j:plain

設定が終わったら再起動すると、リモートデスクトップから即ログインができるようになっています。

おまけ

リモートデスクトップ接続の画面が狭くなりますが、先程編集した/boot/config.txtに解像度のパラメータもありますので、これを変更することで広くすることができます。

sudo vi /boot/config.txt

/boot/config.txtの変更前

(略)
# framebuffer_width=1280
# framebuffer_height=720
(略)

/boot/config.txtの変更後

(略)
framebuffer_width=1280
framebuffer_height=720
(略)

これで1280*720の解像度で起動が行われます。

f:id:ueponx:20170226143214j:plain

おわりに

思った以上につまずいてしまって泣けましたが、これでもう障壁はないかなと思う(と思いたい)ので、引き続きOpenCVもやっていきたいと思います。

/* -----codeの行番号----- */