雑食エンジニアの気まぐれレシピ

日ごろ身に着けた技術や見知った知識などの備忘録的なまとめ.主にRaspberry Piやマイコンを使った電子工作について綴っていく予定.機械学習についても書けるといいな.

RaspberryPiCamera+OpenCVでカメラ画像のUDP送信

「GoogleCardboardとRaspberryPiを使って視界を共有できるロボットを作る」
第二回はラズパイ側のUDP通信についてです.

C言語でのUDP通信については以下の内容が非常に参考になりました.
http://www.sbcr.jp/books/img/Linuxnet_03.pdf

今回のロボットにおけるラズパイのUDP通信部分は
・カメラ画像の送信
Androidの傾き情報の受信
をそれぞれ行います.なお,簡単のため送信と受信でポートを分けて実装しました.

画像の送信については
OpenCVでWebカメラのストリーミング - kivantium活動日記
様のコードをベースに使わせていただきました.

RasPiカメラモジュールの画像をOpenCVで取得する方法については過去記事参照
shikky-lab.hatenablog.com

#include <iostream>
#include <vector>

#include <sys/socket.h>
#include <netdb.h>//gethostbyname
#include <arpa/inet.h>//inet_ntop

#include <raspicam/raspicam_cv.h>

//ipアドレスの確認用
void print_host_info(struct hostent *host, struct sockaddr_in &send_addr) {
    unsigned int **addrptr;
    addrptr = (unsigned int **) host->h_addr_list;
    send_addr.sin_addr.s_addr = *(*addrptr); //送信先IPアドレス
    /*ipアドレスの確認*/
    char sender_str[256] = {0};
    inet_ntop(AF_INET, &send_addr.sin_addr, sender_str, sizeof (sender_str));
    std::cout << "sendto:" << sender_str << ",Port=" << ntohs(send_addr.sin_port) << std::endl;
}

int main(int argc, char *argv[]) {
    //送信ソケットの設定
    int send_sock;
    struct sockaddr_in send_addr;
    send_sock = socket(AF_INET, SOCK_DGRAM, 0);

    send_addr.sin_family = AF_INET;
    send_addr.sin_port = htons(8765); //送信用のポート番号.好きな値を設定.
    struct hostent *host;
    host = gethostbyname("192.168.43.1");//各自の環境に合わせて.bonjourに対応していれば"ホスト名.local"でも可
    if (host == NULL) {
        return 1;
    }
    print_host_info(host, send_addr);//IPアドレスの確認

    //カメラの設定
    raspicam::RaspiCam_Cv Camera;
    //set camera params
    Camera.set(CV_CAP_PROP_FORMAT, CV_8UC3);
    Camera.set(CV_CAP_PROP_FRAME_WIDTH, 640);//画像サイズの設定.
    Camera.set(CV_CAP_PROP_FRAME_HEIGHT, 720);//後々Android上で分割表示するため,スマホ画面サイズの半分に設定
    std::cout << "Opening Camera..." << std::endl;
    if (!Camera.open()) {
        std::cerr << "Error opening the camera" << std::endl;
        return -1;
    }

    static const int sendSize = 65000; //UDPの仕様により上限は65kB

    cv::Mat image;
    std::vector<unsigned char> ibuff;
    std::vector<int> param = std::vector<int>(2);//jpeg変換時のパラメータ設定
    param[0] = CV_IMWRITE_JPEG_QUALITY;
    param[1] = 80;

    while (cvWaitKey(10) == -1) {//sleepの代用.基本的にはsleepなしでも構わないが,joystickをつなぐと不安定になったりするっぽい?
        Camera.grab();//カメラ画像の取得
        Camera.retrieve(image);
        imencode(".jpg", image, ibuff, param);
        if (ibuff.size() < sendSize) {//上限オーバーの場合は送らない
            sendto(send_sock, ibuff.data(), ibuff.size(), 0, (struct sockaddr *) &send_addr, sizeof (send_addr));
        }
        ibuff.clear();
    }
}

コンパイルの際にはリンカに-lraspicam -lraspicam_cv -lopencv_highgui -lopencv_core -lopencv_imgcodecs のオプションが必要です.

UDPでは1パケットの最大サイズがヘッダ込みで65535byteとなるため送信できる画像は最大で約65kBとなります.ラズパイのカメラモジュールは無駄に?解像度がすごいので注意が必要です.今回は画像の幅と高さをスマホ画面サイズの半分にしたうえで,jpegのクオリティを変えることで調整しています.リアルタイム性を追求するならjpegクオリティを更に落として送信サイズを小さくしたほうがいいです(正確にはAndroid側の受信サイズを小さく).
※補足
jpeg画像は内部でハフマン符号化を行っているため画像サイズは毎回変わります(カラフルな画像ほど大きくなる).そのため効率的な通信のためには送受信のサイズを可変にする必要があります.送信サイズを可変にするのは簡単ですが,遅延に直接影響するのは受信サイズのようなのであまり意味ないようです(少なくとも実感はありませんでした).結局最終的な実装では各画像を10kBごとに分割して送信し,受信側で再構成するという処理を行いました.
※補足終わり

受信についてはマルチスレッドやマルチプロセスで並列処理する方法が理想的なのだと思いますが,いまいち扱いづらかったので
ノンブロッキングソケット:Geekなぺーじ
様の記事を参考にノンブロッキング処理によって実装しました.