2018年1月31日水曜日

rscamとrelmを利用して、カメラの画像を表示するアプリをrustで作る方法


背景

rustとは、安全性、並列性などを重視して作られたプログラミング言語です。
そのrustを利用して、webカメラから画像を取得し、その情報を表示する方法が気になったので、試してみました。

全体像

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

使ったもの

LinuxをインストールしたPC

CPU: Intel® Core™ i7-4600U CPU @ 2.10GHz × 4
メモリ: 8GB
OS: ubuntu17.10

webカメラ

PCに内蔵されているカメラを使いました。
USB接続するタイプのカメラでも、今回のプログラムは多分動くと思います。

rust

プログラミング言語です。
下記のページに従って。PCにインストールしました。

リンク: Rustのインストール
利用したバージョン: 1.22.1

rscam

v4lというLinuxでカメラを扱うためのプログラムをrustで使えるようにしてくれるcrate(ライブラリ)です。
このcrateを使っているため、今回のプログラムはv4lを使える環境でしか動きません。

リンク: githubドキュメント
利用したバージョン: 0.5.3

v4lを利用するため、rustのプログラムを動かす前に下記のコマンドで関連ファイルをインストールします。
sudo apt install libv4l-dev

gtk-rs

gtkというguiプログラムをrustで使えるようにしてくれるcrateです。
これでwindow、ボタン、画像を表示します。

gtkを利用するため、rustのプログラムを動かす前に下記のコマンドで関連ファイルをインストールします。
sudo apt install libgtk-3-dev

リンク: ドキュメント
利用したバージョン: 0.3.0

relm

上記のgtk-rsをラップして、機能を呼び出しやすくしてくれるcrateです。

リンク: github、 ドキュメント
利用したバージョン: 0.11.0

プログラムの部分的な説明

プログラムは100行以上あるので、ここでは部分的に説明します。
全体像は公開しているリポジトリを見てください。

カメラ画像の取得

rscamのreadmeとほぼ同じです。
違いは、画像のサイズを640x360にしているところです。
let mut camera = Camera::new("/dev/video0").unwrap();
camera.start(&Config {
    interval: (1, 30), // 30 fps.
    resolution: (640, 360),
    format: b"MJPG",
    ..Default::default()
let frame = camera.capture().unwrap();

カメラ画像をgtkのPixbufに変換

PixbufLoaderを利用るすと、rscamのframeが持つjpegのデータを、rgbaで構成されるPixbuf形式に変換できます。
fn jpeg_vec_to_pixbuf(jpeg_vec: &[u8]) -> Pixbuf {
    let loader = PixbufLoader::new();
    loader.loader_write(jpeg_vec).unwrap();
    loader.close().unwrap();
    loader.get_pixbuf().unwrap()
}

let frame = camera.capture().unwrap();
let pixbuf = jpeg_vec_to_pixbuf(&frame[..]);

PixbufでImageを更新

WinでImageを保持し、updateで更新しています。
struct Win {
    image: Image,
}

impl Update for Win {
    fn update(&mut self, event: Msg) {
        let image = &self.image;

        let frame = camera.capture().unwrap();
        let pixbuf = jpeg_vec_to_pixbuf(&frame[..]);
        image.set_from_pixbuf(&pixbuf);
    }
}

impl Widget for Win {
    fn view(relm: &Relm, model: Self::Model) -> Self {
        let image = Image::new();
        vbox.add(&image);

        Win {
            image: image,
        }
    }
}

カメラの状態を変更

ModelでCameraをOption型として保持し、ボタンのクリックによって、開いたり閉じたりします。
Winのimplに好きな関数を定義できるので、open_cameraとclose_cameraを定義してstarted_cameraの状況に応じてイベント内で読んでいます。
struct Model {
    started_camera: Option,
}

enum Msg {
    ToggleCamera,
}

impl Update for Win {
    fn update(&mut self, event: Msg) {
        match event {
            Msg::ToggleCamera => {
                if self.model.started_camera.is_some() {
                    self.close_camera();
                } else {
                    self.open_camera();
                }
            },
        }
    }
}

impl Widget for Win {
    fn view(relm: &Relm, model: Self::Model) -> Self {
        let toggle_camera_button = Button::new_with_label("toggle camera");
        vbox.add(&toggle_camera_button);
    }
}

impl Win {
    fn open_camera(&mut self) {
        let label = &self.state_label;
        let mut camera = Camera::new("/dev/video0").unwrap();
        camera.start(&Config {
            interval: (1, 30), // 30 fps.
            resolution: (640, 360),
            format: b"MJPG",
            ..Default::default()
        }).unwrap();
        self.model.started_camera = Some(camera);
        label.set_text("opened camera");
    }

    fn close_camera(&mut self) {
        self.model.started_camera = None;
        let label = &self.state_label;
        label.set_text("closed camera");
    }
}

画像更新の繰り返し

relmをmodelの変数として保持し、relm.connect_exec_ignore_errにstreamとコールバックとして定義したイベントを渡すと、タイムアウトやインターバルを設定できます。
カメラを開いた後と画像を更新した後に画像を更新するイベントを10msのタイムアウトで呼ぶことで、画像を更新し続ける仕組みです。

更新頻度が短いと期待通りに画像が表示されないことがあるので、「gtk::events_pending()」と「gtk::main_iteration_do(true)」を組み合わせて、画像表示の関数内でviewのアップデートを呼び出しています。
struct Model {
    relm: Relm,
}

impl Update for Win {
    fn model(relm: &Relm, _: ()) -> Model {
        Model {
            relm: relm.clone(),
        }
    }

    fn update(&mut self, event: Msg) {
        match event {
            Msg::ToggleCamera => {
                // After open camera
                self.set_msg_timeout(10, Msg::UpdateCameraImage);
            },
            Msg::UpdateCameraImage(()) => {
                if self.model.started_camera.is_some() {
                    self.update_camera_image();
                    self.set_msg_timeout(10, Msg::UpdateCameraImage);
                }
            },
        }
    }
}

impl Win {
    fn set_msg_timeout(
        &mut self,
        millis: u64,
        callback: CALLBACK,
    )
        where CALLBACK: Fn(()) -> Msg + 'static,
    {
        let stream = Timeout::new(Duration::from_millis(millis));
        self.model.relm.connect_exec_ignore_err(stream, callback);
    }

    fn update_camera_image(&mut self) {
        let camera = self.model.started_camera.as_mut().unwrap();
        let image = &self.image;
        let frame = camera.capture().unwrap();
        let pixbuf = jpeg_vec_to_pixbuf(&frame[..]);
        image.set_from_pixbuf(&pixbuf);
        while gtk::events_pending() {
            gtk::main_iteration_do(true);
        }
    }
}

動作確認

プロジェクトのディレクトリに入り、下記のコマンドを実行するとプログラムが動きます。
cargo run

このような動作をする想定です。

「Toggle Camera」クリック時の動作
カメラが動いてない時: カメラを開いて画像の更新を開始
カメラが動いている時: カメラを閉じて画像の更新を停止


期待通りに動きました。

まとめ

rustでカメラの情報を扱うプログラムを書けました。

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

参考

今回の記事のために作成したプロジェクトです。
asukiaaa/rust_relm_and_camera_practice

情報収集に利用したサイト達です。
Are there any video capture libraries out there?
Struct gtk::Image
GTKとRustでLinuxデスクトップアプリ入門
Gdk pixbuf load image from memory
How do I update/redraw a GTK Widget (GTKLabel) internally without a key press event using python?
relm/examples/async/src/main.rs

0 件のコメント :