non vorrei lavorare

ブログ名の通りです。javascript three.js mruby rust OCaml golang julialang blender

RustでWebカメラの映像をコマンドプロンプトに出すコマンドを作った

おはようございます。以前は、兄より、さっさとご飯を食べていた次男でしたが、最近は、だらだら食べて、最後には食べさせろと言ってくる始末。kjunichiです。

背景

昨年末にWindowsな自作PCをリニューアルして以来、あれこれ、Windowsでやっている。だいぶ、いい感じにやりたいことが 多少出来るようになってきた気がしている。

そんな日々を過ごす中で、WindowsでもRustでFFIのやり方が分かった。

今年はRustの勉強会にも参加したし、なにか自作したかった。

Webカメラの画像をコマンドプロンプトに表示する

構成

作って学んだこと

WindowsでのFFI

MSVC版はなんとなく、libファイルが必要な雰囲気がしていたので、これを用意して、かつ、環境変数LIBにこのlibファイルのパスを追加して試したら、 あっさり動いた!この辺32ビットだと呼び出し規約の指定が必要かもしれない。

extern "system" {
}

とすれば、良さそう。(32ビットな2-wayタブレットPCが奥さんに占領されてて、試せてない)

また、動かす際は必要なDLLがパス上に存在している必要がある。

この辺は、別の処理系でFFIしていれば、体験していることかとも思う。

FFIなクレートの構成

glfw-rsを参考に見よう見まねでやってみた。

CのAPIを叩く部分はffiというフォルダを作成し、この中に以下のファイルを作成しているようだったので、 これをまねた。

  • mod.rs
    • .soや.dllから使う関数を記述
  • link.rs
    • OS毎のリンク方法

C的なポインタ操作が分からなかったので#removeで実装した。。

OpenCVのC APIのcvImageEncode関数でCvMat構造体経由でPPM(P6)形式のデータが取得できるが、そのPPMヘッダ部の解析を 慣れないRustでやってたっぷりとコンパイルエラー時のメッセージでコードの書き方を勉強させられた。が、 ビルドを通せても意図した動きにならなかった。これは、ポインタ操作の実装をRustでは実際に要素をremoveするなど、実装自体を大きく変更させていた 為、バグが混入してしてまった。。

まぁ、なんとか、バグを修正して、意図した通り、PPMヘッダの解析を行うことが出来た。

C言語なら、

tmpbuf = &tmpbuf[index];

というように、index分ずらしたアドレスを参照できるのだが、ちょっとRustでのやり方が 分からず、実際のポインタの実体の要素を削除するという実装で実現した。

for i in 0..index {
(*tmpbuf).remove(0);
}

動いた!

例によって、デスクトップを撮影して、そのキャプチャを動作確認記録として貼り付けてみた。

youtu.be

成果物

github.com

まとめ

Rustは当初はMinGW版だったが、その後、MSVC版がデフォルトになっている。 Goからは直接DLLを生成できないが、ひと手間かけて、DLL化することで、前回はmrubyから、そして 今回はRustから呼び出せた。

Webカメラから画像を取得する処理は非推奨のC APIを使ってやってしまっているが、cv-rsなるRust向けのライブラリが あり、こちらはC++ APIをつかっている。ただし、build.rsでgccクレートを使っているので、たぶんMSVC版のRustでは 動かせない。cl.exeで付属のC++ラッパーをビルドする処理を書けば理屈の上では動かせるハズ。 (どうやら、gccクレートはMSVC環境ではcl.exeをCコンパイラとして使う模様)

今回は#removeして実際に要素を減らしてポインタ操作の代用をしてしまったが、近いうちにこの辺りのRustでのポインタ操作を覚えたい。

f:id:kjw_junichi:20170921064458p:plain

関連記事

Rustでコマンドラインアプリでキーが押されたかの判定しつつ、別の処理も進めるには

おはようございます。先日長男は、保育園のキャンプに参加して、初めての保育園外での泊りをしてきました。ここ数年、天候が微妙だった園のキャンプでしたが、今年は天気に恵まれ、楽しんで帰ってきました。kjunichiです。

背景

RustでWebカメラの画像をコマンドラインに表示するコマンドを作っている。 このコマンドをESCキーが押されたら、終了するようにしたい。

アプリ側で、映像を表示させつつ、キー入力を監視して、 特定のキーが押されたら、終了したい。

(シグナルを使って、SIGINT時の処理にこの終了処理を加えても対応できるが、Windowsの 事を考えると、キー操作の統一感が無い気がして、今回は見送った。)

Windowsのcmd.exe上で動かす場合、Win32APIのキー入力API(GetAsyncKeyState)でキーが押されたかの判定を ブロックせずにできた。

pub fn term_get_esc_key() -> bool {
    let r: i16 = unsafe { ffi::GetAsyncKeyState(ffi::VK_ESCAPE) } as i16;
    if r != 0 {
        return true;
    }
    return false;
}

同じようなことを、macOSLinux等のUNIX系の環境で行いたい。

これらの環境では、標準入力に文字が入力されることで判定する方法がある。 しかし、何も考えず標準入力からの入力を受け取ろうとするとブロックされてしまう。 それなら、スレッドでとやったら、

といったツイートの様に単にスレッドで入力待ちを行っても多くの環境でプロセス単位にブロックされ、 入力待ち以外の処理が行えないことを経験した。

分かった事

readシステムコールでのブロックを防ぐ

標準入力はプロセス実行時に既にオープンされており、このようなファイルデスクリプタはfcntlで以下の様に ノンブロックの指定をすることで、以降のreadでブロックされなくなる。

fcntl(0, F_SETFL, O_NONBLOCK);

エンターキーが押されるのを待たない

readをノンブロックにしても、まだ駄目で、エンターキーが入力されるまで、入力を検知できない。これは 通常使用している端末がそういった動きになるように設定しているからで、端末の属性をプログラム実行時に 変更する必要があった。

幸い以下のページを見つけて、端末の属性を変更する方法が分かった。

プログラム開始後に端末の設定を変えており、プログラム終了後にこの状態を元に戻す必要がある。 戻さないと、コマンド入力がエンターまで待ってくれず、まともに出来なくなることがあるらしい。

macOSの場合、そういった状況に陥ったらreset+エンターで割と復活することが多い。

現在の端末属性を保持する

tcgetattr(0, &saved_term);

エンターまで入力を待たずに受け取る

term.c_lflag &= ~(ICANON | ECHO);
tcsetattr(0, TCSANOW, &term);

プログラム終了時に端末属性を元に戻す

tcsetattr(0, TCSANOW, &saved_term);

以上のことをRustでやった

libcクレートにこれらのAPIが定義されていた。 定数も皆今回必要なモノは定義済みで助かった。

extern crate libc;

use std::{thread, time};
use std::io::Write;
use std::time::SystemTime;

fn main() {
    println!("Hello, world!");
    let mut saved_termattr = libc::termios {
        c_iflag: 0,
        c_oflag: 0,
        c_cflag: 0,
        c_lflag: 0,
        c_cc: [0u8; 20],
        c_ispeed: 0,
        c_ospeed: 0,
    };
    unsafe {
        let mut ptr = &mut saved_termattr;
        libc::tcgetattr(0, ptr);
    }
    let mut termattr = saved_termattr;
    termattr.c_lflag = termattr.c_lflag & !(libc::ICANON | libc::ECHO);
    termattr.c_cc[libc::VMIN] = 1;
    termattr.c_cc[libc::VTIME] = 0;
    unsafe {
        libc::tcsetattr(0, libc::TCSANOW, &termattr);
    }
    unsafe {
        libc::fcntl(0, libc::F_SETFL, libc::O_NONBLOCK);
    }
    let mut buf: [libc::c_char; 1] = [0; 1];
    let ptr = &mut buf;

    loop {
        let r = unsafe { libc::read(0, ptr.as_ptr() as *mut libc::c_void, 1) };
        if r > 0 {
            println!("input !{:?}", *ptr);
            break;
        }
        thread::sleep(time::Duration::from_millis(300));
        let code: [char; 3] = [0x1b as char, '[', 'K'];
        print!("\r");
        for i in 0..3 {
            print!("{}", code[i]);
        }
        print!("{:?}", SystemTime::now());
        std::io::stdout().flush().unwrap();

    }
    unsafe {
        libc::tcsetattr(0, libc::TCSANOW, &saved_termattr);
    }
}

出来た!

youtu.be

まとめ

cursesやncurses使うと、対応できることも薄々分かっていたが、今回termbox-goを使う前提だったので、 これらのライブラリを混ぜるな危険との認識で避けていた。

その、結果、上記のページに出会って、Cでの素朴なやり方を知ることが出来、Rustでも実装出来た。

実は、今回のコマンドプロンプトや、コンソールに画像を表示する為に内部的に使用しているtermbox-goという ライブラリでは、端末の属性変更しており、fcntlシステムコールを発行して標準入力をノンブロックにするだけで良かった。

関連記事

10年前の記事

9年前の記事

7年前の記事

5年前の記事

mruby-webcamにWebカメラで取得する画像サイズを指定できるメソッドを追加した

おはようございます。このところ、すっかり朝晩が涼しく、リビングから寝室に戻り、長男用の簡易ベッドをベッドわきに作って家族で寝室で寝るようになりました。このおかげで、ベッドの自分のスペースがだいぶ広くなりましたkjunichiです。

背景

前回のコマンドプロンプトにWebカメラを表示させた際の表示速度と比べてsixelでの表示が遅いので、 Webカメラから取得する画像サイズを減らして、表示速度の改善を図った。

対応

  • Webcamクラスに@width,@heightを追加
attr_accessor :width, :height
mrb_value v = mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@width"));
if (mrb_fixnum_p(v)) {
  width = mrb_fixnum(v);
}
v = mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@height"));
if (mrb_fixnum_p(v)) {
  height = mrb_fixnum(v);
}
if (width > 0) {
  cap.set(CV_CAP_PROP_FRAME_WIDTH, width);
}
if (height > 0) {
  cap.set(CV_CAP_PROP_FRAME_HEIGHT, height);
}

結果

github.com

まとめ

当初、@width,@heightの値の取得を誤って、fixnum型の判定の関数を読んでいて、いくらサイズを変えても、 ターミナルの端っこに小さな画像しか表示されず、半日以上解決にかかってしまった。

関連記事