モーションセンサー連動ライブカメラ
汎用性の高い形で検証してみようと思います。
iOS10のインラインvideoのthree.js取り込みができるようなので検証してみました。
ビデオVR
成功してるようなので、この記事全体の書き換えの必要性を感じます。
検証の方向性
一人称視点VR
FPV-VR(First Person View Virtual Reality) ※造語です
操作デバイスからのモーション値を映像送信サーバ側に送りカメラを操作し映像がデバイスに戻ってくるため、双方向の低遅延環境が必要になると思います。もしくは、モーションの遅延に酔う可能性が大きくタッチイベントの操作に向いてるような気がします。環境さえ用意できれば映像はHD画質程度で最低限満足できるので開発は容易と思います。デメリットに操作対象者1名でそれ以外の方は閲覧モードに制限することになると思います。
多人称視点VR
MPV-VR(Multi Person View Virtual Reality) ※造語です
参照: PICAM360
同時操作対象者を制限しません。映像の遅延が多少あってもモーションの遅延が発生しないのでVRに向いていると思います。課題はより高画質の映像を用意しなければならないことと思います。VRコンテンツと資料で調べてみた範囲では理想で8K映像、妥協して4K程度と思います。
環境
ボード | Raspberry Pi3 ModelB |
OS | 2016-05-27-raspbian-jessie.img |
カメラ | Raspberry Pi カメラモジュール V2 |
マイク | SANWA USBマイク MM-MCUSB16 |
Raspberry Piにモニター、マウス、キーボード、カメラ、マイク、電源、有線LANが接続されている状態から作業をすすめます。
事前準備
Fontの設定
日本語環境にして文字化けするようならFontのインストール
$ sudo apt-get install fonts-vlgothic
再起動
$ sudo reboot
ファームウェア更新
$ sudo rpi-update
再起動
$ sudo reboot
最新状態にアップデート
$ sudo apt-get update
$ sudo apt-get upgrade
再起動
$ sudo reboot
USB電流供給制限解除
気持ち程度設定します。設定しなくても構わないと思います。
USB電流供給を0.6A制限から1.2Aまで増加できるそうです。
$ sudo leafpad /boot/config.txt
末尾に追加
safe_mode_gpio=4
max_usb_current=1
再起動
$ sudo reboot
ウォッチドッグタイマー
Raspberry PiのSoCにはハードウェアとしてWatchDogTimer(WDT)が組み込まれているそうです。
設定してみます。
ネットから拾える情報で設定するとカーネルパニックを起こしてしまいます。
しばらく様子見にします。
有線LANと無線LANのIP固定化
有線LANは映像配信専用、無線LANはVNC接続専用にしてみます。
$ sudo leafpad /etc/dhcpcd.conf
末尾に追記(設定内容は各ネットワーク環境により異なります。「Raspberry Pi Jessie IP固定」などで検索してみてください。)
interface eth0
static ip_address=192.168.1.185/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1
interface wlan0
static ip_address=192.168.1.186/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1
Pi3の無線LANはIEEE 802.11 b/g/n 2.4 GHzだけなのでご注意
<SSID>と<PASSPHRASE>は自身の環境にあわせ設定
$ sudo sh -c 'wpa_passphrase <SSID> <PASSPHRASE> >> /etc/wpa_supplicant/wpa_supplicant.conf'
再起動
$ sudo reboot
遠隔操作
※外部に公開するwebサーバーでは導入しないでください。非常に危険です。
運用にはモニター、マウス、キーボードは必要ないので、この段階で遠隔操作にしてしまいます。
TightVNCなどではroot(管理者権限)が難しい作りになっています。モニターに表示するX-Windowそのままを表示してくれるx11vncを使用します。
x11vncのインストール
$ sudo apt-get install x11vnc
$ x11vnc -storepasswd
パスワードの入力
保存していいか聞かれるので「y」を選択
起動
$ x11vnc -usepw -forever
Ctrl+Cで一旦停止
自動起動設定
$ cd /home/pi/.config
$ mkdir autostart
$ cd autostart
新規作成
$ sudo leafpad x11vnc.desktop
コピーペースト
[Desktop Entry]
Encoding=UTF-8
Type=Application
Name=X11VNC
Comment=
Exec=x11vnc -forever -display :0 -ultrafilexfer
StartupNotify=false
Terminal=false
Hidden=false
スクリーン解像度を変更する場合
$ sudo leafpad /boot/config.txt
コメントを削除して有効化
framebuffer_width=1440
framebuffer_height=900
シャットダウン
$ sudo shutdown
モニター、マウス、キーボードを取り外して電源入れ直し
VNCクライアントから接続してみる
同じネットワーク内のWindows/Mac/Linux/スマホ/タブレットにVNCクライアントソフトをインストールして接続してみてください。
VNC Server: 192.168.1.186:5900
Password: x11vnc設定時のパスワード
成功していれば、以降はVNCクライアントの窓の中で作業できます。
映像配信の準備
検証した中ではMJPG-Streamerがリアルタイム性を要求する低遅延映像送信に向いていると思いました。音声同期ができない問題がありますが、まずはこれを採用してみます。
MJPG-Streamerのインストール
MJPG-Streamerは開発が止まっていますがgithubで多くforkされ改良されてます。
google検索でいちばんに出てくるjacksonliamさんのMJPG-Streamer使ってみます。
参考: jacksonliam/mjpg-streamer
必要なライブラリのインストール
$ sudo apt-get install cmake libjpeg-dev imagemagick
コンパイル
$ git clone https://github.com/jacksonliam/mjpg-streamer.git
$ cd mjpg-streamer/mjpg-streamer-experimental
$ make
$ sudo make install
起動スクリプト
$ cd ~
$ sudo leafpad mjpg-streamer.sh
新規作成 (raspicam以外のカメラで検証してます。ご注意。)
#!/bin/sh
export LD_LIBRARY_PATH=/usr/local/lib
v4l2-ctl --set-ctrl=exposure_auto_priority=0
v4l2-ctl --set-ctrl=exposure_auto=3
v4l2-ctl --set-ctrl=focus_auto=0
mjpg_streamer \
-i "input_uvc.so -d /dev/video0 -r 1280x720 -f 30 -n" \
-o "output_http.so -w /usr/local/share/mjpg-streamer/www -p 80"
権限変更
$ sudo chmod 755 mjpg-streamer.sh
起動
$ sudo ./mjpg-streamer.sh
動作確認
RaspberryPiの場合 http://127.0.0.1 他のPCの場合 http://192.168.1.185
Ctrl+Cキーで終了
音声配信の準備
検証中
WebRTCが本命と思いますがiPhone/iPadが対応してません。2016年前半の情報では開発中の発表があったようなので暫定的に他の手法を使ってみます。
- iOSでWebRTCが使えないからWebSocketとWebAudioで擬似ストリーミングしてみた - console.lealog();
- WebAudio+WebSocketでブラウザへの音声リアルタイムストリーミングを実装する · GitHub
この辺りの記事を参考にさせていただいて進めてみます。
node.jsのインストール
インストール
$ wget https://nodejs.org/dist/v4.4.7/node-v4.4.7-linux-armv7l.tar.gz
$ tar -xvf node-v4.4.7-linux-armv7l.tar.gz
$ cd node-v4.4.7-linux-armv7l
$ sudo cp -R * /usr/local/
再起動
$ sudo reboot
npmアップデート
$ sudo npm update -g npm
バージョン確認
$ node -v
v4.4.7
$ npm -v
3.10.5
作業ディレクトリの準備
FMPV-VRディレクトリを作成して、この中でまとめることにします。
ディレクトリ作成
$ cd ~
$ mkdir FMPV-VR
起動スクリプト移動
$ sudo mv mjpg-streamer.sh /home/pi/FMPV-VR/
起動スクリプト変更
$ cd FMPV-VR
$ sudo leafpad mjpg-streamer.sh
コピーペースト (raspicam以外のカメラで検証してます。ご注意。)
#!/bin/sh
export LD_LIBRARY_PATH=/usr/local/lib
v4l2-ctl --set-ctrl=exposure_auto_priority=0
v4l2-ctl --set-ctrl=exposure_auto=3
v4l2-ctl --set-ctrl=focus_auto=0
mjpg_streamer \
-i "input_uvc.so -d /dev/video0 -r 1280x720 -f 30 -n" \
-o "output_http.so -w /home/pi/FMPV-VR -p 80"
MJPG-Streamerの自動起動設定
$ sudo leafpad /etc/rc.local
末尾exit0の手前に追記
sudo /home/pi/FMPV-VR/mjpg-streamer.sh
再起動
$ sudo reboot
動作確認
RaspberryPiの場合 http://127.0.0.1/?action=stream 他のPCの場合 http://192.168.1.185/?action=stream
一人称視点VRの場合
ハードウェア
ソースコード
このソースだとパソコンのIEとFirefoxが動かない感じ。
センサー連動サーボ制御プログラムは作成中
新規作成
$ cd /home/pi/FMPV-VR
$ sudo leafpad fpv-vr.html
コピーペースト
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>FPV-VR</title>
<style>
*{
margin:0;
padding:0;
}
html {
font-family: 'Lucida Grande', 'Hiragino Kaku Gothic ProN', 'ヒラギノ角ゴ ProN W3', Meiryo, メイリオ, sans-serif;
}
body {
background: #222222 none repeat scroll 0 0;
overflow: hidden;
}
#container{
position: relative;
}
#stats {
position: absolute;
top: 0;
right: 6px;
cursor: default !important;
}
#stats #fps {
background: transparent !important;
text-align: right !important;
}
#stats #fps #fpsText {
color: #fff !important;
}
#stats #fps #fpsGraph {
display: none;
}
#sysinfo {
font-family: Helvetica, Arial, sans-serif;
font-size: 10px;
font-weight: bold;
line-height: 12px;
text-align: right;
color: #fff;
opacity: 0.9;
position: absolute;
top: 14px;
right: 6px;
cursor: default;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/stats.js/r14/build/stats.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/system.js/master/build/system.min.js"></script>
<script>
$(function() {
var cam_stream, ctx;
var stats;
screenSize();
init();
//初期化
function init() {
cam_stream = new Image();
cam_stream.onload = function() {
var canvas = $("#camera")[0];
ctx = canvas.getContext("2d");
animate();
};
cam_stream.onerror = function() {
console.log("error");
};
cam_stream.src = "http://192.168.1.185/?action=stream";
stats = new Stats();
$("#container").append(stats.domElement);
$("#sysinfo").html([
'Resolution: 1280x720',
'Browser: ' + System.browser,
'OS: ' + System.os,
'Canvas: ' + System.support.canvas,
'Local storage: ' + System.support.localStorage,
'File API: ' + System.support.file,
'FileSystem API: ' + System.support.fileSystem,
'RequestAnimationFrame: ' + System.support.requestAnimationFrame,
'Session storage: ' + System.support.sessionStorage,
'SVG: ' + System.support.svg,
'WebGL: ' + System.support.webgl,
'Worker: ' + System.support.worker
].join( '<br />' ));
}
//レンダリング
function animate() {
id = requestAnimationFrame(animate);
ctx.drawImage(cam_stream, 0, 0, 1280, 720);
stats.update();
/*window.onclick = function () {
cancelAnimationFrame(id);
};*/
}
//表示調整
$(window).resize(function() {
screenSize();
});
function screenSize() {
windowWidth = $(window).innerWidth(), windowHeight = $(window).innerHeight();
$("#container").css({"margin-top": (windowHeight - windowWidth / 16 * 9) / 2, "height": windowWidth / 16 * 9});
$("#camera").css({"width": windowWidth, "height": windowWidth / 16 * 9});
}
});
</script>
</head>
<body>
<div id="container">
<canvas id="camera" width="1280" height="720"></canvas>
<div id="sysinfo"></div>
</div>
</body>
</html>
動作確認
RaspberryPiの場合 http://127.0.0.1/fpv-vr.html 他のPCの場合 http://192.168.1.185/fpv-vr.html
多人称視点VRの場合
ハードウェア
ソースコード
このソースだとパソコンのIEとFirefoxが動かない感じ。
画像の用意
$ cd /home/pi/FMPV-VR
$ wget http://dz.plala.jp/wiki_data/fpv-vr_icon.zip
$ unzip fpv-vr_icon.zip
新規作成
$ cd /home/pi/FMPV-VR
$ sudo leafpad mpv-vr.html
コピーペースト
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>MPV-VR</title>
<style>
*{
margin:0;
padding:0;
}
html {
font-family: 'Lucida Grande', 'Hiragino Kaku Gothic ProN', 'ヒラギノ角ゴ ProN W3', Meiryo, メイリオ, sans-serif;
}
body {
background: #222222 none repeat scroll 0 0;
overflow: hidden;
}
#container{
/*position: relative;*/
z-index: 0;
}
#mode {
position: absolute;
cursor: pointer;
z-index: 1;
left: 20px;
bottom: 72px;
width: 32px;
height: 32px;
}
#question {
position: absolute;
cursor: pointer;
z-index: 1;
left: 20px;
bottom: 20px;
width: 32px;
height: 32px;
}
#alert {
position: absolute;
padding: 0;
margin: -88px 0 0 -120px;
left: 50%;
top: 50%;
width: 240px;
height: 176px;
border-radius: 10px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
z-index: -1;
opacity: 0;
}
#support {
position: absolute;
padding: 0;
margin: -151px 0 0 -130px;
left: 50%;
top: 50%;
width: 260px;
height: 303px;
border-radius: 10px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
z-index: -1;
opacity: 0;
}
.grab {
cursor:grab;
cursor:-moz-grab;
cursor:-webkit-grab;
}
.grabbing {
cursor:grabbing;
cursor:-moz-grabbing;
cursor:-webkit-grabbing;
}
#stats {
position: absolute;
top: 0;
right: 6px;
cursor: default !important;
}
#stats #fps {
background: transparent !important;
text-align: right !important;
}
#stats #fps #fpsText {
color: #fff !important;
}
#stats #fps #fpsGraph {
display: none;
}
#sysinfo {
font-family: Helvetica, Arial, sans-serif;
font-size: 10px;
font-weight: bold;
line-height: 12px;
text-align: right;
color: #fff;
opacity: 0.9;
position: absolute;
top: 14px;
right: 6px;
cursor: default;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/r73/build/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/r73/examples/js/controls/DeviceOrientationControls.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/r73/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/stats.js/r14/build/stats.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/system.js/master/build/system.min.js"></script>
<script>
$(function() {
var cam_mode = 1;
var toggle = true;
var stats;
var renderer, scene, mesh, material, texture;
var sensor_cam, orbit_cam, globe_cam;
var sensor_ctl, orbit_ctl, globe_ctl;
var loader = new THREE.TextureLoader();
loader.load("http://192.168.1.185/?action=stream",
function (texture) {
texture.minFilter = THREE.LinearFilter;
material = new THREE.MeshBasicMaterial({map: texture});
init();
}
);
//初期化
function init() {
windowWidth = $(window).innerWidth(), windowHeight = $(window).innerHeight();
renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(windowWidth, windowHeight);
$("#container").append(renderer.domElement);
sensor_cam = new THREE.PerspectiveCamera(70, windowWidth / windowHeight, 1, 1000);
sensor_ctl = new THREE.DeviceOrientationControls(sensor_cam);
orbit_cam = new THREE.PerspectiveCamera(70, windowWidth / windowHeight, 1, 1000);
orbit_cam.position.set(0, 0, 1);
orbit_ctl = new THREE.OrbitControls(orbit_cam, renderer.domElement);
orbit_ctl.userPan = false;
orbit_ctl.userZoom = false;
orbit_ctl.autoRotate = true;
orbit_ctl.autoRotateSpeed = 0.02;
globe_cam = new THREE.PerspectiveCamera(70, windowWidth / windowHeight, 1, 2000);
globe_cam.position.set(0, 0, 1000);
globe_ctl = new THREE.OrbitControls(globe_cam, renderer.domElement);
globe_ctl.userPan = false;
globe_ctl.userZoom = false;
globe_ctl.autoRotate = true;
globe_ctl.autoRotateSpeed = 0.04;
globe_ctl.minDistance = 1000;
globe_ctl.maxDistance = 1000;
scene = new THREE.Scene();
mesh = new THREE.Mesh(new THREE.SphereGeometry(500, 60, 40), material);
mesh.scale.x = -1;
scene.add(mesh);
stats = new Stats();
$("#container").append(stats.domElement);
$("#sysinfo").html([
'Resolution: 1280x720',
'Browser: ' + System.browser,
'OS: ' + System.os,
'Canvas: ' + System.support.canvas,
'Local storage: ' + System.support.localStorage,
'File API: ' + System.support.file,
'FileSystem API: ' + System.support.fileSystem,
'RequestAnimationFrame: ' + System.support.requestAnimationFrame,
'Session storage: ' + System.support.sessionStorage,
'SVG: ' + System.support.svg,
'WebGL: ' + System.support.webgl,
'Worker: ' + System.support.worker
].join( '<br />' ));
animate();
};
//レンダリング
function animate() {
requestAnimationFrame(animate);
mesh.material.map.needsUpdate = true;
switch (cam_mode){
case 1:
sensor_ctl.update();
renderer.render(scene, sensor_cam);
break;
case 2:
orbit_ctl.update();
renderer.render(scene, orbit_cam);
break;
case 3:
globe_ctl.update();
renderer.render(scene, globe_cam);
break;
}
stats.update();
};
//リサイズ
$(window).resize(function() {
windowWidth = $(window).innerWidth(),windowHeight = $(window).innerHeight();
sensor_cam.aspect = windowWidth / windowHeight;
sensor_cam.updateProjectionMatrix();
orbit_cam.aspect = windowWidth / windowHeight;
orbit_cam.updateProjectionMatrix();
globe_cam.aspect = windowWidth / windowHeight;
globe_cam.updateProjectionMatrix();
renderer.setSize(windowWidth, windowHeight);
});
//モードボタン
$("#mode").click(function() {
switch (cam_mode) {
case 1:
$(this).attr("src",$(this).attr("src").replace("_sensor", "_orbit"));
cam_mode = 2;
break;
case 2:
$(this).attr("src",$(this).attr("src").replace("_orbit", "_globe"));
cam_mode = 3;
break;
case 3:
$(this).attr("src",$(this).attr("src").replace("_globe", "_sensor"));
cam_mode = 1;
break;
}
});
//ハテナボタン
$("#question").click(function() {
if (toggle) {
$("#question").css("cursor", "default");
$("#alert").css("z-index", "1");
$("#alert").css("opacity", "1.0");
$("#alert").css("cursor", "pointer");
toggle = false;
}
});
$("#alert").click(function() {
if (!toggle) {
$("#alert").css("z-index", "-1");
$("#alert").css("opacity", "0");
$("#alert").css("cursor", "default");
$("#question").css("cursor", "pointer");
toggle = true;
}
});
//マウスアイコン
$("#container").mousedown(function() {
$("#container").addClass("grabbing");
$("#container").removeClass("grab");
});
$("#container").mouseup(function() {
$("#container").addClass("grab");
$("#container").removeClass("grabbing");
});
});
</script>
</head>
<body>
<div id="container" class="grab">
<img id="support" src="support.png">
<img id="alert" src="alert.png">
<img id="mode" src="mode_sensor.png">
<img id="question" src="question.png">
<div id="sysinfo"></div>
</div>
</body>
</html>
動作確認
RaspberryPiの場合 http://127.0.0.1/mpv-vr.html 他のPCの場合 http://192.168.1.185/mpv-vr.html
機器の具合
Raspberry Pi
CPU使用量 2%程度
操作デバイス
一人称視点VR(FPV-VR) 映像規格 1280x720 | |
---|---|
映像遅延ロス 東京-愛知間 | 約500ms |
iPhone5 | 30FPS |
iPhone6 | 60FPS |
Windows Vista AMD Phenom 9600 Quad-Core 2.30GHz | 45FPS |
Windows 7 Intel(R) Core(TM) i7-2700K 3.5GHz | 60FPS |
多人称視点VR(MPV-VR) ドームマスター形式 1280x720 | |
---|---|
映像遅延ロス 東京-愛知間 | 約500ms |
iPhone5 | 10FPS |
iPhone6 | 27FPS |
Windows Vista AMD Phenom 9600 Quad-Core 2.30GHz | 12FPS |
Windows 7 Intel(R) Core(TM) i7-2700K 3.5GHz | 40FPS |
参考
変更履歴
- 2016.07.14 公開