Corredor

ウェブ、プログラミングの勉強メモ。

Chrome ブラウザで Web Bluetooth API を試してみる

Web Bluetooth API というモノがある。Bluetooth が内蔵されたノート PC なんかで、Chrome ブラウザを使って Bluetooth デバイスとの接続ができるらしい。コレを使えば「Cordova-Plugin-BluetoothLE」のような Cordova プラグインを使ってアプリ化せずとも、ウェブアプリとして Bluetooth デバイスを扱えそうだ。

ということで今回は、この Web Bluetooth API の触りだけ試してみた。

Web Bluetooth API を有効にする

本校執筆時点の Chrome ブラウザのデフォルトでは、Web Bluetooth API が無効化されているので、コレを有効にする。

Chrome ブラウザを開いたら次の URL にアクセスする。

  • chrome://flags/#enable-experimental-web-platform-features

すると「Experimental Web Platform features」という設定項目にフォーカスが当たっているので、コレを「Enabled」にしてブラウザを再起動する。

必要な事前準備はコレだけ。

開発者コンソールで試してみる

機能を有効にして Chrome を再起動したら、F12 キーなどで開発者ツールを開く。そして「Console」タブに移動したら、次のように入力する。

navigator.bluetooth

機能が正しく有効にできていれば、Bluetooth オブジェクトが返ってくるはずだ。もし undefined が返ってきた場合は、前述の有効化手順がうまく出来ていない可能性がある。

そしたら、次のようなワンライナーを叩いてみる。

navigator.bluetooth.requestDevice({ acceptAllDevices: true })

コレで、専用の「Chooser UI」、デバイス一覧のダイアログが開くはずだ。PC 周辺の Bluetooth デバイスをスキャンしていて、モノによっては製品名などが見えると思われる。

requestDevice() によって、ペアリングを開始できる。

navigator.bluetooth.requestLEScan({ acceptAllAdvertisements: true })

コチラは、BLE (Bluetooth Low Energy) のアドバタイジングが取得できる様子。MacOS の Chrome では何やら情報が取れたが、ThinkPad X250 にインストールした Ubuntu 上の Chrome だと特に情報が取れなかった。

以上

いずれのメソッドも Promise 形式なので、ペアリング後に続けてコネクションを張ったりして Bluetooth 機器を操作したりできる。

ブラウザオンリーで Bluetooth 操作ができるのは面白そうだ。

iOS×BLE Core Bluetoothプログラミング

iOS×BLE Core Bluetoothプログラミング

[rakuten:vaboo:15996821:detail]

OpenCV.js : JavaScript で実装・ブラウザオンリーで OpenCV を使う

Python や Java で利用することが多い OpenCV。ビルドやインストールが面倒臭いのがタマにキズなのだが、今回 OpenCV.js という JavaScript 版を見つけたので紹介。

公式のガイド

公式のガイドは以下にある。

ハッキリ言って滅茶苦茶分かりづらい…。どうやったら使えるのか、何をどう実装したらいいのかが分かりにくく、また一見したサンプルコードがそのまま動かせなかったりするので、順々に説明していく。

ライブラリファイル

公式的には JavaScript 版もビルドして手元で作れ、ってことみたいで、npm とかで配布されていない。

しかし、公式で「どうしてもビルドできないならコレ使えば?」というビルド済のファイルが公開されているので、コレを使うことにする。

自分は今回、OpenCV v3.4.0 を利用したのだが、以下のように URL 中にバージョン番号があるので、コレを書き換えてやればそれぞれ必要なファイルがダウンロードできる。

OpenCV.js の基本的な操作だけならこのファイルだけで良いのだが、顔認識を行うために特徴分類器の XML ファイルを読み込む場合は、次の utils.js が必要になる。

で、この utils.js を使って読み込む、特徴分類器ファイルも落としておく。今回は人の顔の正面を特定したいので、haarcascade_frontalface_default.xml をダウンロードしておく。

特徴分類器の概要は以下を参照。

ということで、最低限 opencv.js、後はやりたいことに応じて utils.js.xml ファイルを用意しておけば良い。

実装サンプル・デモ

コレから使ってみた感触を紹介するのだが、先に動作デモとサンプルコードの全量をお伝えしておく。

github.com

以下は単純に opencv.js のみを使って、ウェブカメラの映像をキャプチャしてグレースケールに変換し canvas 要素に描画している例。

neos21.github.io

以下は opencv.js に加えて utils.jshaarcascade_frontalface_default.xml を利用して実現した、顔認識のサンプル。

neos21.github.io

顔認識までやってみせると、かなり OpenCV っぽいかなと思う。

動作の感覚だが、グレースケール変換の方は 30fps できちんと動いた。しかし、顔認識の方は結構ガクガク。重たいのねー。

読み込み方・初期処理

まずは基本的な使い方を紹介。

opencv.js は結構ファイルが重たいので、script 要素で読み込む時に onload 属性を付けて読み込み完了を確認しておくと良い。読み込めると、window.cv が使えるようになる。

cv.onRuntimeInitialized に初期化関数を代入するようなサンプルコードを見かけたのだが、WASM 版じゃないと効かないのかな。指定しても無駄だった。そこで、window.onload イベントのタイミングで各種初期化をしてやることにした。

今回のサンプルでは video 要素に描画しているウェブカメラの映像を利用したりするので、DOM 要素が出揃っている状態で OpenCV の初期処理をしてやらないといけない。

これらのタイミングをハンドリングするためには、body 要素の末尾で、次の順番で実装 (読み込み) してやると良い。

<!-- はじめに初期化処理などを実装しておく -->
<script>

// Video Settings
const width  = 640;
const height = 480;
const fps = 30;

// Globals
let videoCapture = null;

// Elements
const videoElem  = document.getElementById('video');
const canvasElem = document.getElementById('canvas');

/** On OpenCV.js Loaded */
function onCvLoaded() {
  cv.onRuntimeInitialized = onReady;  // ← 動かない
}

/** On Ready */
function onReady() {
  // Set Element Size
  videoElem.width  = canvasElem.width  = width;
  videoElem.height = canvasElem.height = height;
  
  // Start Video Capture
  videoCapture = new cv.VideoCapture(videoElem);
};

/** On Window Loaded */
window.addEventListener('load', () => {
  onReady();
});

</script>

<!-- それから OpenCV.js を読み込み onload 属性で読み込み完了を確認する -->
<script src="./opencv.js" onload="onCvLoaded();"></script>

</body>
</html>

videoElemcanvasElem など、グローバルで DOM 要素の参照を取得しておくので、body 要素の末尾でやるのが良い。

window.onload の実体は onReady() 関数で、ココで要素のサイズを指定しつつ、VideoCapture を準備している。

ウェブカメラのキャプチャ開始

ウェブカメラのキャプチャを開始するためのスタートボタンを用意し、次のような関数を実行してやる。

// Globals
let stream = null;
let isStreaming = false;
let matSrc = null;
let matDst = null;

/** On Start */
function onStart() {
  navigator.mediaDevices.getUserMedia({
    video: true,
    audio: false
  })
    .then((_stream) => {
      stream = videoElem.srcObject = _stream;
      videoElem.play();
      
      matSrc = new cv.Mat(height, width, cv.CV_8UC4);  // For Video Capture
      matDst = new cv.Mat(height, width, cv.CV_8UC1);  // For Canvas Preview
      
      // Start Process Video
      setTimeout(processVideo, 0);
      
      isStreaming = true;
    })
    .catch((error) => {
      console.error('On Start : Error', error);
    });
}

ウェブカメラのキャプチャは navigator.mediaDevices.getUserMedia() を使う。

取得した _stream は、キャプチャ停止処理のためにグローバル変数 stream に退避させておく他、video 要素の srcObject に割り当てる。コレにより、video 要素がプレビュー表示されるようになる。

matSrc はキャプチャ映像をそのまま取得するための Mat、matDst はグレースケールに変換した後のイメージを保持しておくための Mat だ。

setTimeout()processVideo() 関数を実行しているが、コレが「映像をキャプチャしてグレースケール変換して表示する」といった一連の動作を行うための関数である。内容は後述。

キャプチャ停止ボタン

キャプチャを停止する場合は、次のような関数を用意してやれば良いだろう。それぞれ null チェックをしておくと安心かも。

/** On Stop */
function onStop() {
  videoElem.pause();
  videoElem.srcObject = null;
  
  stream.getVideoTracks()[0].stop();
  
  isStreaming = false;
}

映像のキャプチャ処理

映像をキャプチャしてグレースケール変換する部分。

/** Process Video */
function processVideo() {
  // キャプチャ中でない場合は Mat を破棄して終了する
  if(!isStreaming) {
    matSrc.delete();
    matDst.delete();
    return;
  }
  
  const begin = Date.now();
  videoCapture.read(matSrc);  // Capture Video Image To Mat Src
  cv.cvtColor(matSrc, matDst, cv.COLOR_RGBA2GRAY);  // Convert Colour To Grey
  cv.imshow('canvas', matDst);  // Set Element ID
  
  // Loop
  const delay = 1000 / fps - (Date.now() - begin);
  setTimeout(processVideo, delay);
}

VideoCapture#read(mat) を使い、引数に渡した mat にウェブカメラのコマ画像を取得させる。

コレをよしなに変換し、cv.imshow() 関数で描画している。第1引数の文字列は getElementById() で指定する要素の ID になるので、

<canvas id="canvas"></canvas>

というように、グレースケールに変換した画像を出力する canvas 要素に、id 属性を付与しておくこと。

このキャプチャ処理を 30fps 感覚で動かすために、関数の末尾で setTimeout() を使って、自分自身を再度遅延実行している。ほとんど setInterval() 的な動作になるワケだが、OpenCV.js の処理が終わらない間に再度 processVideo() が呼ばれるようなことがないように制御しているワケ。

顔認識を行う場合は

顔認識を行う場合は、XML ファイルを読み込む必要があるのだが、そのまますんなりとは読み込めないので、utils.js を使用する。

<div id="error-message">エラーがある場合はココに書き込まれる</div>

<script>
  // 自前の処理…
</script>


<!-- utils.js を読み込む -->
<script src="./utils.js"></script>

<!-- 最後に OpenCV.js を読み込む -->
<script src="./opencv.js" onload="onCvLoaded();"></script>

utils.js を使って XML ファイルを読み込むには、次のように実装する。

// Globals
const faceCascadeFile = './haarcascade_frontalface_default.xml';

/** On Ready */
function onReady() {
  // Video Capture などなど…
  videoCapture = new cv.VideoCapture(videoElem);
  
  // XML ファイルを XHR で読み込んでおく
  const utils = new Utils('error-message');
  utils.createFileFromUrl(faceCascadeFile, faceCascadeFile, () => {
    console.log('Face Cascade File Loaded');
  });
};

まずは new Utils()utils.js をインスタンス化するのだが、第1引数に ID 名の文字列を渡しておく。エラーが発生した場合は、この ID の要素にエラーメッセージが書き込まれる。内部的には getElementById()innerHTML を使っているので、存在しない DOM 要素だとエラーメッセージ出力時にエラーが発生してしまうことに注意。

utils をインスタンス化したら、createFileFromUrl() で XML ファイルを読み込んでおく。第1・第2引数には、同じ読み込みたい XML ファイルのパスを指定すれば良い。内部的には XMLHttpRequest で読み込んでいるので、ローカルだと CORS 制限に引っかかって Ajax が動作しないと思う。そのため、

$ npx http-server

なんかで簡易サーバを立てて実行してやると良い。

第3引数のコールバック関数で、XML ファイルを Ajax 読み込みできたことを確認できるようになっている。

utils.jscreateFileFromUrl() 関数の中身を見ると、cv.FS_createDataFile() 関数を使って XML ファイルの中身をキャッシュしている様子。

ファイルを読み込んだら、カメラの起動時なんかに、「XML を読み込む」という処理が動作するようになる。

// Globals
let videoCapture = null;
let stream = null;
let isStreaming = false;
let matSrc  = null;
let matDst  = null;
let matGrey = null;
let faces = null;
let classifier = null;

/** On Start */
function onStart() {
  navigator.mediaDevices.getUserMedia({
    video: true,
    audio: false
  })
    .then((_stream) => {
      stream = videoElem.srcObject = _stream;
      videoElem.play();
      
      matSrc  = new cv.Mat(height, width, cv.CV_8UC4);  // For Video Capture
      matDst  = new cv.Mat(height, width, cv.CV_8UC4);  // For Canvas Preview
      matGrey = new cv.Mat();
      faces = new cv.RectVector();
      classifier = new cv.CascadeClassifier();
      // Load Pre-Trained Classifiers
      classifier.load(faceCascadeFile);
      
      // Start Process Video
      setTimeout(processVideo, 0);
      
      isStreaming = true;
    })
    .catch((error) => {
      console.error('On Start : Error', error);
    });
}

classifier.load() 部分がそれ。utils.createFileFromUrl() をやらずに classifier.load() を実行しても、うまく動作しない。

あとは普通の OpenCV とほぼ同じ API なので、以下のような感じで顔認識した結果を四角い枠で囲んでやると良い。

/** Process Video */
function processVideo() {
  if(!isStreaming) {
    matSrc.delete();
    matDst.delete();
    matGrey.delete();
    faces.delete();
    classifier.delete();
    return;
  }
  
  const begin = Date.now();
  
  videoCapture.read(matSrc);  // Capture Video Image To Mat Src
  matSrc.copyTo(matDst);  // Copy Src To Dst
  cv.cvtColor(matDst, matGrey, cv.COLOR_RGBA2GRAY, 0);  // Get Grey Image
  classifier.detectMultiScale(matGrey, faces, 1.1, 3, 0);  // Detect Faces
  
  // Draw Faces Rectangle
  for(let i = 0; i < faces.size(); ++i) {
    const face = faces.get(i);
    const point1 = new cv.Point(face.x, face.y);
    const point2 = new cv.Point(face.x + face.width, face.y + face.height);
    cv.rectangle(matDst, point1, point2, [255, 0, 0, 255]);
  }
  
  cv.imshow('canvas', matDst);  // Set Element ID
  
  // Loop
  const delay = 1000 / fps - (Date.now() - begin);
  setTimeout(processVideo, delay);
}

VideoCapture#read()matSrc にコマ画像を取得し、copyTo() を使って matDst に同じ内容を貼り付ける。matDst にはこのあと、認識した顔を示す四角枠を描画していく。

顔認識はグレースケールの画像を利用するので、グレースケールに変換した画像を matGrey に置き、classifier.detectMultiScale() で顔認識処理を行う。

結果は引数の faces に格納されるので、コレを取り出して cv.rectangle() で四角枠を描き込んでいく流れ。

今回はココまで

…ということで、今回はココまで。

良いところ

  • JavaScript オンリーで OpenCV が使えて、ブラウザ上で動作するので環境構築・利用が楽チン
  • JS 版特有のちょっとした違いはあるが、ほとんどの API がネイティブの OpenCV と同様に使えるので、他の言語で OpenCV 経験があれば JS 版も実装しやすいかと

良くないところ

  • 動作速度が遅めか
  • XML ファイルを読み込む際に XHR を使う必要があり、完全なローカルでの動作は CORS エラーになるため困難
  • JS 版特有の仕様に関するドキュメント・文献が少ない

環境構築がほぼ要らないので手軽さはダントツだが、JS 版特有の調整が必要で、文献も少なめだ。

上手く活用できる場面に遭遇したら、すぐに使えるよう、キャッチアップだけしておくとしよう。

参考文献

ラズパイ4をバッテリー駆動させるためのモバイルバッテリーを買った

ラズパイ4をモバイルバッテリーで駆動させるため、モバイルバッテリーを買ってみた。

以前、Anker Astro E5 第2世代という古いモバブーを使って、ラズパイ4と Elecrow 5インチ液晶をバッテリー駆動させることができた。

この製品は 16,000mAh の大容量なのだが、重量が 296g あり、2014年発売の製品と若干古いので、ラズパイ専用のモバブーとして何か安いのを買ってみることにした。

ラズパイ4は 5V・3A で電源供給できれば動作するそうなので、モバイルバッテリーを選ぶ際は出力のスペックが 5V・3A のモノを選んだ。安いモノだと 5V・2.4A 出力しかなかったりして、コレだとラズパイ3系までにしか対応できず、ラズパイ4では正常に動作するか怪しいので避けること。

色々探して自分が選んだのは、Power Add Aries I という 10,000mAh のモノ。値段が安くて、1,599円だった。

↑ コレね。

f:id:neos21:20200607171536j:plain

付属品はモバブーの充電に使用できる Micro USB-B ⇔ USB-A ケーブルと、USB-C ⇔ USB-C ケーブル。

f:id:neos21:20200607171541j:plain

USB-A ポートは 5V・3.1A 出力に対応していて、USB-C ポートは 5V・3A 出力とのこと。

見た目は角ばっていて格好良い。本体側面のスイッチを押すと、電池残量がパーセント値で確認できる。

f:id:neos21:20200607171546j:plain

Anker Astro E5 と比べてみたが、本体サイズは Aries I の方が大きい。重量は 399g ほどあるらしく、コンパクトさに欠ける。コレは完全に失敗した。安いだけで小さく・軽くなかった。

f:id:neos21:20200607171551j:plain

Aries I に、電源スイッチ付き USB-A → USB-C ケーブルを挿して、ラズパイ4に接続してみた。ラズパイ4の USB-A から、Elecrow の5インチ液晶の Micro USB-B ポートに接続し、液晶にも給電している。

この状態で、動作は問題なく行えた。Astro E5 より大きく・重たくて残念だが、ラズパイ4の駆動には使えたのが良かった。


安さで選ぶならこの商品は悪くない。10,000mAh という容量も十分だろう。これ以上容量を減らすと、5V・3A 出力に対応していない製品が多いので、製品選定の際は注意が必要。

もう少し高い商品なら、5V・3A に対応していて、200g を切るようなモバブーも売っているので、よりコンパクトにしたい場合は調べてみると良いだろう。

Ewin 折りたたみ式 Bluetooth キーボード・トラックパッドを購入した

ラズパイ4を操作するための入力デバイスとして、Ewin というメーカの折りたたみ式キーボードを購入した。

f:id:neos21:20200607170840j:plain

f:id:neos21:20200607170844j:plain

本体と付属品を確認

購入したのは以下のリンクで見られる商品。本稿執筆時点で在庫切れのためリンクが貼れず。

楽天だと以下。

このキーボードはトラックパッドも搭載されており、Bluetooth 接続できるスグレモノ。値段は4,000円程度だった。

f:id:neos21:20200607170849j:plain

付属品はこんな感じ。充電用の Micro USB ⇔ USB ケーブル、ソフトケース、日本語の説明書などが付属。

f:id:neos21:20200607170853j:plain

対応 OS は Windows、Mac、iOS、Android と、ひととおり対応している。「iOS でトラックパッドが使えない」とされていたが、iOS 13 ならトラックパッドにも対応しているので、iOS アップデートすれば OK。

f:id:neos21:20200607170857j:plain

充電中は赤い LED が付き、Bluetooth の接続状況は青い LED で示される。

使用感をレビュー

Windows、MacBook (MacOS)、iPhone 11 Pro Max (iOS)、Raspberry Pi 4 に接続して検証した。

全体的な質感は値段相応か。悪くはないかな。ただ、折りたたみのヒンジ部分はヤワい印象で、危なっかしい。開いた部分含めて平たい所に置いて使わないと、すぐにヒンジが折れそうな気がする。不安。w

トラックパッドの精度は悪くない。ただ、カーソル移動以外の操作が敏感すぎるかな。タップでクリック扱いになったり、2本指スライドでスクロールしたりするのだが、この感度が良すぎるせいで誤ってクリックしてしまったり、スクロールのつもりでピンチ操作とみなされてしまったりする。慣れが必要か。

iPhone (iOS) でトラックパッドを有効にするには、「設定」→「アクセシビリティ」と進み、「AssistiveTouch」を ON にするだけ。不自由なく使えている。

キーボード部分も、ちゃんと入力できる。全体的なピッチが狭い他、ヒンジ部分周辺のキーはより小さくなっているので、コチラも慣れが必要。ファンクションキーは fn キーと同時押しになるが、一応使える。Esc キーが fn 同時押しになるのはちょっと面倒くさいかな。

スペースキー左に Control と Alt、スペースキー右に Command と Alt キーがある。左右に Alt キーがあるので、Windows と Linux では Alt キーの空打ちによる IME 切り替えが操作できる。

MacOS の場合、Cmd キーが右側にしかないので、Karabiner-Elements で実現していた「左右 Cmd キー空打ちによる IME 切替」ができない。Control + Space などで代替するしかない。また、Cmd キーが右側にしかないということで、コピペ操作は「右 Cmd + C」などとなり、不慣れな人が多いかも。

Raspberry Pi 4 (Raspbian OS) にも、設定不要で接続できた。Bluetooth 接続自体は問題なくできるが、接続が切れるのが若干早いかもしれない。bluetoothctl コマンドの trust で信頼設定しておくと良いだろう。

f:id:neos21:20200607170904j:plain

Micro USB ケーブルは充電専用で、パソコンと有線接続したからといって USB 駆動するワケではなかったのが残念。

類似品が多いので購入は注意

今回自分は「Ewin」というメーカの製品を買ったが、Amazon で調べると、ほぼ同一の画像を使用した商品が多数出品されている。

Ewin は4,000円程度で販売されていたが、他のメーカのモノは3,000円代のモノもあった。

Ewin は以下のサイトなど、実際の商品写真をアップしているレビュー記事がいくつか見つかったが、他のメーカの商品はそうした記事が見当たらず、信用できなかったので購入しなかった。

他のメーカの類似製品がまともに使えるのかは分からない。Amazon で売っていた Ewin 製のモノであれば、今回自分が購入したように、一定のクオリティで実用できるので、値段だけ見ると割高に見えるかもしれないが、安心を買っても良いかもしれない。

以上

どんな OS でもサクッと接続でき、マウスとキーボードをセットで使えるようになる。

若干剛性が心配ではあるが、安いのでこんなもんかしら。

↑最小クラスの小ささだとコレかしら。