2013年12月23日月曜日

opencvで処理した画像(動画)をandroid端末に送信して表示するあたりのメモ

以前、SimpleMjpegViewというライブラリを利用してwebcamの映像をandroid上に表示したが、今回は別の手法で(もっと原始的な形で)端末上に動画を表示する。

前回:

 webcam → (mjpegストリーム) → android端末

今回:
 webcam → PC(c++(Opencv)) → (jpeg) → android端末

 今回の方でやれば重たい画像処理を母艦PCやraspiで行ってその処理結果をandroidに送信できる。

とりあえず結果↓

 いいwebカメラ(ロジクールの1万のやつ)なのに若干カクつく。サイズを小さくしたり、jpg品質落とすことでましにできると思われる



opencvを使ったりしているのでMITライセンスになります。
強制終了前提のコードなのであくまでご参考、自己責任でお願いします。

↓MITLicenceに基づく告知

Some codes in OpenCV are utilized.
Therefore, before downloading, you have to agree to the following license.
//////////////////////////////////////////////////////////////////////////////////
IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
By downloading, copying, installing or using the software you agree to this license.
If you do not agree to this license, do not download, install,
copy or use the software.

                    Intel License Agreement

            For Open Source Computer Vision Library
Copyright (C) 2000, Intel Corporation, all rights reserved.
Third party copyrights are property of their respective owners.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
  • Redistribution's of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.
  • Redistribution's in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.
  • The name of Intel Corporation may not be used to endorse or promote products
    derived from this software without specific prior written permission.
This software is provided by the copyright holders and contributors "as is" and
any express or implied warranties, including, but not limited to, the implied
warranties of merchantability and fitness for a particular purpose are disclaimed.
In no event shall the Intel Corporation or contributors be liable for any direct,
indirect, incidental, special, exemplary, or consequential damages
(including, but not limited to, procurement of substitute goods or services;
loss of use, data, or profits; or business interruption) however caused
and on any theory of liability, whether in contract, strict liability,
or tort (including negligence or otherwise) arising in any way out of
the use of this software, even if advised of the possibility of such damage.

 ライセンス告知ここまで


まずC++側、環境はwin7 64bit Gcc(MINGW) opencvが必要(今回は2.4.7使用)


概要 : 
0、ソケット構築、android側からのアクセスを待ち受け

1、opencvで動画や画像フレームを取ってきて好きなように処理して最終的にjpgに圧縮 jpgのデータはvector<uchar>で保存される

2、jpgのデータサイズ(何バイトか)をバイナリ(intだと4byte)にする、地味なビット演算処理が必要(もっとスマートにできそうだが)。要は送信先でintとしてデータサイズを取れるようにする。これがわからないとあちらで正確なバッファが作れない

3、 jpgデータサイズとjpgデータをandroidに送る。
   vector<uchar>をソケットで送信できるconst char*に変換する部分の調査に時間がかかった(初見だとここで詰まる可能性あり)

4、おわり。上記処理をループさせている


(参考ソース)

#include <iostream>
#include <windows.h>
#include <winsock2.h>

#include "opencv2\highgui\highgui.hpp"
#include <vector>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <sstream>

using namespace cv;
using std::stringstream;
int main()
{
~~~~~~~~~~~~~



ここにソケットの初期化(bindまで)の処理が入るが諸事情(参考文献の丸写しのため)によりカット
参考文献 : http://blog.pusheax.com/2013/07/windows-api-winsock-create-your-first.html

windowsソケットプログラミング(C++)の基本説明



~~~~~~~~~~~~~



    //////////////// start(original from here)

    int lis=listen(u_sock,3);
    /////////////// waiting client data   
   
    //opencvによる動画の取得
    VideoCapture cap(0);
   
    //メインループ
    while(1){
        stringstream ss;
        //クライアントからの受信用バッファ、何らかのコマンド受け取りを想定
        int receive_buf_size = 20;
        char vect[receive_buf_size];

        std::cout <<"start waiting client command" <<std::endl;
        //client adress define
        struct sockaddr_in client;
        int len = sizeof(client);

        //client connect waiting・・・クライアント定義、待ちうけはループ外でもいいかもしれない
        SOCKET sockCli = accept(u_sock, (struct sockaddr *)&client, &len);   
        //クライアントから20byte何かしら受け取る、必要に応じてパース
        int get=recv(sockCli,vect,receive_buf_size,0);
        if(get==SOCKET_ERROR){
            std::cout<<"Error in Receiving: "<<WSAGetLastError()<<std::endl;
            continue;
        }

        // receivedta(command) process 受け取ったデータの処理、Stringstreamを使って文字列にして表示
        Sleep(10);//data receive waiting クライアントからのデータを待つ
        std::cout <<"end receive"<<std::endl;
        ss.clear();
        String res_string;
        ss << vect;
        ss >> res_string;

        if (get <0){
            std::cout <<"no receive data"<<std::endl;
            //    break;
        }else{
            std::cout <<"receive data : ";
            std::cout <<res_string<<std::endl;
        }

        ss.clear();

        /////////// image data send process 画像の取得と送信   
        Mat frame;

        cap >> frame;

        int framesize = 1000000; ///Max size

        //送信データ用のバッファ
        vector<uchar> bufv;
        //ちゃんと変換されたかの確認用
        bool imok;
        //jpg圧縮用のパラメータ設定
        vector<int> jpgparam;
        jpgparam.push_back(CV_IMWRITE_JPEG_QUALITY);
        jpgparam.push_back(20); /* jpeg quality */
        //ここまでパラメータ設定

        //圧縮した画像をバッファに入れる
        imok=imencode(".jpg",frame,bufv,jpgparam);
       
        //Mat sframe;

        std::cout<<bufv.size()<<std::endl;

        //Send some message to remote host

        ///////////// jpg data sending
        std::cout <<"start sending"<<std::endl;

        std::cout <<"send size"<<bufv.size() <<std::endl;
        std::cout <<"senddata "<<bufv.data()<<std::endl;
        std::cout <<"start sending"<<std::endl;

        // sizeof jpg send to java  画像サイズ(何バイトか)をクライアントに送信する
        // intの数値をバイナリ(4バイト)に変換する。泥臭い処理
        vector<uchar> jpgsize_send(4);
        int jpgsize=bufv.size();
       
        //int(4byte)のバイナリを1バイトずつ送信用の配列に入れる。エンディアン注意   
        for (int i=0;i<4;i++){
        jpgsize_send[3-i]=(jpgsize >>(i * 8) );
        }
        ////////// bynary genelator

        std::cout <<"data size : "<< jpgsize <<"type size"<<sizeof(jpgsize) <<std::endl;
        //std::string siz=std::to_string(jpgsize);
        std::cout <<"type size"<<sizeof(jpgsize_send) <<std::endl;
       
        /////////////////////// send data to android
        //vectorをcharに変換してクライアントに送信、
        //ロベール本によるとreinteroret_castの使用には注意が必要らしい

        //ヘッダ部(今回は画像データバイト数のみ)送信
        int smsg_header=send(sockCli,reinterpret_cast<const char*>(jpgsize_send
                    .data()),jpgsize_send.size(),0);
        //画像データ送信
        int smsg=send(sockCli,reinterpret_cast<const char*>(bufv.data()),bufv.size(),0);

        ///////////////////////


        if(smsg==SOCKET_ERROR){
            std::cout<<"Error: "<<WSAGetLastError()<<std::endl;
            WSACleanup();
        }

        /*
        //get=recv(u_sock,vect,512,0);
        if(get==SOCKET_ERROR){
        std::cout<<"Error in Receiving: "<<WSAGetLastError()<<std::endl;
        }
        //std::cout<<vect<<std::endl;
        */
    }
    closesocket(u_sock);

~~~~~ ここから終了処理を適当に

}

------------------------------------------  C++ここまで



Java(Android側)、eclipse4.3(Kepler Service Release 1) androidosは4.4が入ったnexsus7(2012)


概要:androidのプロジェクトで新しいクラスを作って以下のソースをコピペして、パラメータ(アドレス、ポートなど)設定してからonCreateに他のTextViewみたいに設定するだけで動作する(コピーした時点でレイアウトのxmlのカスタム&~のところに出てくるので普通にレイアウトできる)。はず。
インターネットの許可が必要(忘れてちょっと詰まった)


ゴミコードが混ざっているのはご愛嬌。あくまで個人的なメモなので。
自動生成とかどうでもいいところでないところは太字にしてある



基本的なsurfaceviewのクラスにネットから画像を取ってくるクラスを接続している。サーフェスの更新をマルチスレッドでずっと回してその中でさらにネットから画像を取ってくるスレッドを走らせる。
はっきり言ってバグの温床なのでいろいろ注意、ちなみに強制終了前提




コード

(Class Mysurfaceview.java)


package com.example.jpeg_binaly_client;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.ImageView;
import android.widget.TextView;

public class Mysurfaceview extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    //ビューの常時更新には普通のマルチスレッドを使う
    Thread thread;


    SurfaceHolder holder;

    //更新される画像
    public Bitmap tcpbmp;
    //ネットワークスレッドの更新可能シグナル
    public int imggetflag;

    public Mysurfaceview(Context context, AttributeSet attrs ) {//太字の部分がないとxml関連のエラーになる
        super(context,attrs);
        // TODO 自動生成されたコンストラクター・スタブ
        getHolder().addCallback(this);
        this.holder = getHolder();

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
        // TODO 自動生成されたメソッド・スタブ

    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // TODO 自動生成されたメソッド・スタブ
        thread = new Thread(this);
        thread.start();


    }

    @Override
    public void run() {
        while(true){//無限ループ注意

            doDraw(getHolder());

            try {
                Thread.sleep(100);
//通信も絡むので若干待つ、が、ここで待たなくてもいいかも
             } catch (InterruptedException e) {
                // TODO 自動生成された catch ブロック
                e.printStackTrace();
            }
        }

    }


    //描画用関数
    public void doDraw(SurfaceHolder holder) {
       
        tcpbmp =null;
        //画像取得スレッドを走らせる
        tcp_getimage3 jpgg= new tcp_getimage3();
        jpgg.execute("nl");
//特にコマンドなんかが無ければ何でもいい

        int cnt=0;
        int timeout =10;
        int waiting=10;
   //画像取得が終わるまで待つ
        while (imggetflag ==0){
            System.out.println("waiting get...");

            //一定時間待つ
            try {
                Thread.sleep(waiting);
            } catch (InterruptedException e1) {
                // TODO 自動生成された catch ブロック
                e1.printStackTrace();
            }
            cnt += waiting;
            //一定時間待っても更新シグナルがこない場合はタイムアウトとする
            if(cnt>timeout){
                System.out.println("waiting get...timeout");
                break;}
        }

        //更新可能(フラグが出ており、bmpがカラでないとき)なら画像を更新
        if(imggetflag==1 && tcpbmp !=null){
             Canvas canvas = holder.lockCanvas();

             canvas.drawBitmap(tcpbmp, 0, 0, null);
          
            holder.unlockCanvasAndPost(canvas);

        }
        //フラグを0にする
        imggetflag=0;

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // TODO 自動生成されたメソッド・スタブ

    }

    //ネットワークからの画像取得クラス
    public class tcp_getimage3 extends AsyncTask<String,Integer,byte[]>{


        public String ipaddr="(IPアドレス)"; 

        int port_bg = (ポート番号);
       
        Socket socket_bg = null;

        DataOutputStream out=null;
        BufferedReader inp=null;

        DataOutputStream out_bg=null;
        DataInputStream inp_bg=null;
       

        //エラー出力用のカラのバイト列
        public byte[] reterr =null;


        public int cnt =0;
        public int serverflag_bg=0;  //1だとサーバ無しとみなしている。

        public tcp_getimage3(){
            super();
        }



        @Override
        protected byte[] doInBackground(String... val) {
            // TODO 自動生成されたメソッド・スタブ
           
            if (serverflag_bg==0){

                try{

                    try{
                        InetSocketAddress addr = new InetSocketAddress(ipaddr, port_bg);
                        socket_bg =new Socket();
                        socket_bg.connect(addr,50);//タイムアウトの時間は重要、繋がらなかったらさっさと切り上げて再接続したほうが早いので、とりあえず短いほうがいい。でも短すぎると電波環境によっては通信できない可能性もある。

                    }catch(SocketException e){
                        e.printStackTrace();
                        Log.i("network", "socket not connect");
                        return reterr;  //タイムアウトしたらさっさとスレッドを閉じにいく

                    }catch(Exception e){  ///調べたがこの例外じゃないと死ぬ。重要
                        e.printStackTrace();
                        Log.i("network", "その他のネットワーク系エラー");

                        return reterr;

                    }


                    Log.i("network", "sendstart ");
         ///////////////////////////////////////ここからが実質的な処理
                    DataOutputStream out_bg = new DataOutputStream(socket_bg.getOutputStream());

                    ////// データの送信、コマンドなどが無いなら何でもいい
                    out_bg.writeBytes("jpgg");

                    byte[] buf =new byte[1000000]; //最大データサイズ(byte)c++と合わせる

                    int readflag;

                    //データ読み込み↓
                    InputStream is =socket_bg.getInputStream();
                    DataInputStream inp_bg = new DataInputStream(is);

                    Thread.sleep(50);//4バイト受信できるくらい待つ必要がある


                    int receive_size=inp_bg.readInt(); //C++からヘッダ(画像のバイト数)情報を受け取る

                    //画像容量分のバッファを定義する、ここら辺が正確じゃないとBMPがちゃんと生成されない

                    byte[] buffull =new byte[receive_size];

                    //画像データをすべて受信するまで待つ
                    if (inp_bg.available() != receive_size){

                        Thread.sleep(10);
                    }


                    //受信データをバッファに入れる、ループしなくていいかも
                    while(inp_bg.available()>=0){
                        System.out.print("datasize is ");
                        System.out.println(receive_size);

                        //受信データをバッファに入れる。何らかの方法で全部のバッファを受け取ったか確認する必要がある
                        inp_bg.read(buffull,0,receive_size);
                        if(inp_bg.available()==0){break;}

                    };
                   
                    out_bg.close();
                    inp_bg.close();
                    socket_bg.close();


                    //バイナリをビットマップに変換する。jpgも勝手に解析してBMPにしてくれる
                    Bitmap bmplocal=null;
                    bmplocal=BitmapFactory.decodeByteArray(buffull,0,buffull.length);
                    System.out.println(buffull.length);←データサイズと同じか一応比較

                    //グローバルのbmpに変換したBMPを入れる
                    tcpbmp = bmplocal;


                    ///////////////////////////////////////ここまでが実質的な処理
                    //バイナリを出力する。onPostExecuteでの処理はこっちに持ってきても問題ない
                    return buffull;

                } catch (IOException e) {
                    e.printStackTrace();
                    System.out.println("io err"+e);

                    serverflag_bg=1;
                    return reterr;
                }catch (Exception e) {
                    e.printStackTrace();
                    Log.i("network", "Iilligal ");
                    System.out.println("io err"+e);

                    serverflag_bg=1;
                    return reterr;

                }

            }else{
                Log.v("tcp","noserver");
                return reterr;
            }
        }

        protected void onPostExecute(byte[] result) {

            //呼び出し元のスレッドに描画更新OKのシグナルを出す
            if (result !=null){
                imggetflag=1;}
            else{
                //何らかのエラーでバイナリが無い場合はシグナルを出さない
                imggetflag=0;
            }

            System.out.println("end tcp thread");


        }

    }
}











2013年12月7日土曜日

自分用Vim設定メモ

Vimの設定に関するメモ。主にc++(時々pythonも)でプログラム書くのをイメージ
GVimを(http://www.kaoriya.net/)入手

■ウィンドウズのキーバインドを変えてしまう
無変換キーとか変換キーなんて使わないのでそれぞれEscキーと半角/全角キーに割り振ってしまう。vimではEscキーを使いまくるのでこの変更は重要
↓これで簡単に変更できる(レジストリ変えるので再起動が必要)
http://www.forest.impress.co.jp/library/software/changekey/

■まず以下を vimrc(編集→起動時の設定 で開くファイル)にコピー

nnoremap - $
nnoremap <F11> :make run <Enter>
nnoremap <F5> :!python % <Enter>
set number
set incsearch
set autochdir
set wildmenu wildmode=list:full
copen

ここからメモ
・ノーマルモードで「-」を押すとカーソルが行末に行くこれで0が行頭 -が行末移動になる(隣り合っててわかりやすい)

・F11でメイク→実行 ちゃんとrunができるようにmakefileを書くこと
・ F5でpython実行

・左に行数を表示

・インクリメンタルサーチをオン(いい感じに検索できる)

・set autochdir : 開いているファイルのある場所にカレントディレクトリを移動する。基本的に必須

・ set wildmenu wildmode=list:full : コマンドモード(「:」入力以降)で補間する

・ copen デバッグウィンドウ的なものを開く。便利

■ctags(Exuberant ctags)を使う
主要参考ページ
http://nanasi.jp/articles/others/ctags.html

・ここ(http://hp.vector.co.jp/authors/VA025040/ctags/)でctagsをダウンロード
・exeを環境変数Pathにあるフォルダに入れる
・autochdir がオンになっていれば編集したいファイル開いてGvimのタグ作成ボタンなどでタグを作れる
・宣言と同時に値を入力している変数はタグをつけてもらえない模様

Taglistを入手(参考:http://nanasi.jp/articles/vim/taglist_vim.html)

(GVimフォルダ)\runtime\plugin の中に.vimを \runtime\doc の中に.docをコピーするだけ

・「:Taglist」で画面左もプロジェクトエクスプローラー的なものが出る(eclipseのほうが高性能だが)