モーションセンサー連動ライブカメラ

提供: ディーズガレージ wiki
2017年1月16日 (月) 01:47時点におけるShogooda (トーク | 投稿記録)による版

(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)
移動先: 案内検索

汎用性の高い形で検証してみようと思います。

iOS10のインラインvideoのthree.js取り込みができるようなので検証してみました。
ビデオVR
成功してるようなので、この記事全体の書き換えの必要性を感じます。

検証の方向性

一人称視点VR

FPV-VR(First Person View Virtual Reality) ※造語です
FPV-VR資料.jpg
操作デバイスからのモーション値を映像送信サーバ側に送りカメラを操作し映像がデバイスに戻ってくるため、双方向の低遅延環境が必要になると思います。もしくは、モーションの遅延に酔う可能性が大きくタッチイベントの操作に向いてるような気がします。環境さえ用意できれば映像はHD画質程度で最低限満足できるので開発は容易と思います。デメリットに操作対象者1名でそれ以外の方は閲覧モードに制限することになると思います。

多人称視点VR

MPV-VR(Multi Person View Virtual Reality) ※造語です
Dmln53000001xioi.jpg
参照: 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

モニター、マウス、キーボードを取り外して電源入れ直し

RealVNC® Enterprise Edition の場合

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年前半の情報では開発中の発表があったようなので暫定的に他の手法を使ってみます。

この辺りの記事を参考にさせていただいて進めてみます。

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の場合

PC GoogleChrome
PC android x86

ハードウェア

ソースコード

このソースだとパソコンの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の場合

PC GoogleChrome
PC GoogleChrome

ハードウェア

ソースコード

このソースだとパソコンの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

参考

NASA公開、遅延の様子

変更履歴

  • 2016.07.14 公開