この記事はHamee Advent Calendar 2018の5日目の記事です。
皆さん最近星見てますか。
「忙しくて星なんか見ている暇ない」、「外に行くのが寒い」、「星なんか見るよりSoftwareDesign読んだり、開発していたほうが楽しい」という方必見ですわ。
全部叶えてあげる。
今月のSoftwareDesignにお家でプラネタリウムできるソフト、「stellarium」が紹介されていた。 記事を見ると中々にそそられたしめっちゃ簡単そうだったのでやってみた。 stellariumは下のように実際の星空をバーチャルで見ることができる。
構成
今回やりたいことはiPhoneで撮った写真に本当の星空を合成するというもの。
Docker上にstellariumを構築して、iPhoneからstellarium起動バッチを叩くみたいなイメージ。
Dockerでのstellariumの環境構築は別記事でやったが、出力するディスプレイがないと言われるところで終了した。
$ ./src/stellarium QXcbConnection: Could not connect to display Aborted
まずはこの問題を解決し、headlessでstellariumを起動しスクリーンショットを撮れるようにする。
stellariumの動き
stellariumの起動してからの動きはざっくり言うとこう。
まず初めに言っておきたいのは、そもそもstellariumはheadlessモードが存在しない。
./stellarium --headless
みたいな感じで起動できるんかなと思ってたらそんなものオプションは存在しなかった。いやまあグラフィックを楽しむものだから当然といえばそりゃそうなんだが。
ということで上記イメージの動きの中のどこかでheadlessな状態を仕込むしかない。
- 案1:C++でOpenGLをheadlessで起動できるようにハイパープログラミングする
- 案2:出力するディスプレイを仮想化してheadlessを実現する
・・・
いやC++わからんがな。そもそもOpenGLを全然知らんがな。却下。
出力するディスプレイを仮想化する
これだ、これしかない、一番楽そうだ・・・!(つーか これが限界)
というわけで早速いこう。
# GUIをheadlessで起動させるための仮想環境を立ち上げる $ Xvfb :10 -ac -screen 0 1024x768x24 & # スクリプトとして実行した場合、環境変数が読み込めないので実行時に環境変数を設定 $ export DISPLAY=":10" # stellarium実行 $ ./src/stellarium
フィニッシュです。楽すぎワロタ。以下のような感じの出力されたらOK。
Xvfbってなんや
今回DISPLAY出力を仮想化させるのに用いたXvfb
。どうやら仮想ディスプレイを作るのによく使われる手段らしい。
FireFoxなどのブラウザをheadlessで起動しようぜ!などによく使われるとかなんとか。
Xvfbのインストールはapt-getでいける。
$ sudo apt-get install xvfb
一応コマンドの意味を記載。
オプション | 意味 |
---|---|
:10 | 仮想ディスプレイの番号を10番に設定(0番はXサーバーが使用済みなのでそれ以外だったら何番でも良い) |
-ac | disable access control restrictions。外部からの起動を許可。 |
-screen 0 | スクリーンをOFF。headlessモード。 |
1024x768x24 | 画面サイズを1024x768に設定。色数を24bit(フルカラー)。 |
画面にアクセスしたら星空の画像を表示させる
無事GUIがない環境でstellariumをheadless起動することに成功したので、次は実際に外部からアクセスしたときにstellariumでスクリーンショットを撮ってみる。
スクリーンショットを撮るスクリプトを作成
stellariumは起動時のオプション--startup-script $SCRIPT_PATH
でstellarium用のスクリプトを実行することができる。
なのでまずはスクリーンショットを撮るスクリプトの作成から。スクリプトはstellarium/scripts
にssc
という拡張子で配置します。(配置場所はbuilds/unix/scriptじゃないので注意)
screencapture.ssc
StelMovementMgr.zoomTo(180) ; // 緯度・経度を指定 core.setObserverLocation(35.685, 139.751, 0); StelMovementMgr.lookEast(); core.wait(0.0); // 'hosizora.png'という名前でスクリーンショットが保存される core.screenshot('hosizora', false, '', true); core.quitStellarium();
あとスクリーンショットを保存するディレクトリをstellariumディレクトリ直下に作成しておきましょう。
$ mkdir stellarium/screenshots
これで実行時にスクリーンショットの保存先ディレクトリとスクリプトのscreencapture.sscを指定して起動する。
./builds/unix/src/stellarium --screenshot-dir screenshots --startup-script screencapture.ssc
確認すると
$ ls -l screenshots total 752 -rw-r--r-- 1 root root 382765 12 3 22:50 hosizora.png
きっちり撮れてますね。
画面からアクセスしてスクリーンショットを表示する
PHPで行きます。配置は以下。
docker-stellarium |_docker-compose.yml |_web |_Dockerfile |_index.php |_stellarium |_exec.sh # stellariumの起動スクリプト |_screenshots |_scripts |_builds/unix |_unix |_src |_stellarium # stellarium本体 |_...省略
あとDocker内でstellariumをビルドしたときに/root/.stellarium
が作られる。ここにはconfig.iniなどstellariumの起動時に必要なファイルが置かれている。
画面からアクセスするとwww-dataがユーザーになるので、
$ cp -rf /root/.stellarium /home/www-data $ chown -R www-data:www-data /home/www-data
で必要なファイルをコピーしておく。 それに伴いstellariumの起動スクリプトの方も少し変更しておく。
exec.sh
#!/bin/sh # GUIをheadlessで起動させるための仮想環境 Xvfb :10 -ac -screen 0 1024x768x24 & # スクリプトとして実行した場合、環境変数が読み込めないので実行時に環境変数を設定 export DISPLAY=":10" export HOME=/home/www-data # stellariumの--screenshot-dirや--startup-scriptオプションはフルパスじゃないと読み込めない export EXEC_STELPATH=/var/www/html/stellarium ./stellarium/builds/unix/src/stellarium --screenshot-dir $EXEC_STELPATH/screenshots --startup-script $EXEC_STELPATH/scripts/screencapture.ssc --full-screen no
続いて画面からアクセスしたときに実行するindex.php。 phpもhtmlもshellも夢のコラボレーションしてるけど気にしないで。駆け抜けたいんや。
index.php
<?php // 緯度・経度を受け取る $lon = $_GET['longitude']; $lat = $_GET['latitude']; $script_path = './stellarium/scripts/screencapture.ssc'; $screen_shot_name = md5(uniqid(rand(), true)); $screen_shot_path = "./stellarium/screenshots/{$screen_shot_name}.png"; // stellariumで使用するスクリプトを動的に作成 $script = " StelMovementMgr.zoomTo(180) ; core.setObserverLocation({$lon}, {$lat}, 0); StelMovementMgr.lookEast(); core.wait(0.0); core.screenshot('{$screen_shot_name}', false, '', true); core.quitStellarium(); "; $fp = fopen($script_path, 'w'); fwrite($fp, $script); fclose($fp); // PHPでスクリプトを実行する際、stdoutとstderrを逃しておかないとプロセスがshellに取られっぱなしになり戻ってこない exec('sh ./stellarium/exec.sh > /tmp/stellarium-stdout 2>/tmp/stellarium-stderr &'); while(!file_exists($screen_shot_path)) { sleep(1); } // ファイルが見つかった瞬間に描画すると生成途中の画像が表示され、画像が見切れてしまうため1秒待つ sleep(1); // stellariumは一度起動したら起動しっぱなしになるのでスクリーンショットが保存でき次第KILLする exec("ps -ef | grep stellarium |grep -v grep | awk '{print $2}' | xargs kill"); ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>星</title> </head> <body> <img src="<?= $screen_shot_path ?>" alt="hosizora"> </body> </html>
画面からアクセスしてみる
では実際にhttp://localhost:8080/index.php?longitude=139.1562&latitude=35.2676
でアクセスしてみましょう。
やったぜ、、、!!家で星が見える、、、!
本題
これでやっとやりたいことが実現できる環境が整った。ここからはiOSの話です。
とは言ってもアプリ側でやることは非常に少ないです。
- 現在の位置情報(緯度・経度)を取得する
- 写真を撮る
- GET通信でstellariumサーバー(Docker)のURLを叩いて星空の写真を取得
- 2で撮った写真と3で取得した星空の写真を合成する
では駆け抜けていきましょう。
アプリ実装
現在位置の取得とカメラ機能の実装コードは特に特筆すべきことはないので割愛します。
GET通信でstellariumサーバー(Docker)のURLを叩いて星空の写真を取得
普通にGETするだけですが、取得した画像をそのまま乗っけると暗かったり色彩が美しく映らないので、諸々調整した画像をUIImageViewに乗せます。
注意するのはGET通信の際のURL指定でしょうか。実機のiPhoneからhttp://localhost:8080..
と叩くと失敗するのでMacのIPアドレスを指定するようにしましょう。
import UIKit import CoreLocation import AVFoundation class ViewController: UIViewController, CLLocationManagerDelegate, AVCapturePhotoCaptureDelegate { override func viewDidLoad() { super.viewDidLoad() // 撮影ボタン let takeBtn = UIButton() takeBtn.frame = CGRect(x:0, y:0, width:self.view.frame.width*0.5, height:self.view.frame.height*0.07) takeBtn.center = CGPoint(x:self.view.frame.width/2, y:self.view.frame.height*0.95); takeBtn.setTitle("shot", for: .normal) takeBtn.backgroundColor = .blue takeBtn.addTarget(self, action: #selector(takePhoto(_:)), for: UIControlEvents.touchUpInside) self.view.addSubview(takeBtn) } /* * 撮影ボタンを押したらstellariumサーバーにGET通信 */ @objc func takePhoto(_ sender: UIButton) { getStellariumCapture() } /* * Dockerから星空の画像を取得 */ func getStellariumCapture() { // 実機のiPhoneからローカル(Docker)のURLを叩くときはlocalhostではなくMacのIPを指定する let url = "http://192.168.100.165:8080/index.php?longitude=\(self.nowLon)&latitude=\(self.nowLat)" let request = NSMutableURLRequest(url: NSURL(string: url)! as URL) request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: { data, response, error in if (error == nil) { let result = String(data: data!, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue))?.trimmingCharacters(in: .whitespacesAndNewlines) let imgURL = URL(string:result!) let imgData :Data = try! Data(contentsOf: imgURL!) DispatchQueue.main.async { // 明るさ・コンストラクタを調整した画像をセット self.stellarImgView.image = self.createBrightImg(orgImg:UIImage(data: imgData)!) } } else { print(error) } }) task.resume() } /* * 撮影した写真の明るさ等を調整する */ func createBrightImg(orgImg : UIImage) -> UIImage { let filter = CIFilter(name: "CIColorControls") let ciImage = CIImage(image: orgImg) filter?.setValue(ciImage, forKey: kCIInputImageKey) // 彩度(デフォルト1) filter?.setValue(2, forKey: "inputSaturation") // 明るさ(0~1, デフォルトは0) filter?.setValue(0.6, forKey: "inputBrightness") // コントラスト(デフォルト1) filter?.setValue(2, forKey: "inputContrast") if let filteredImage = filter?.outputImage { return UIImage(ciImage: filteredImage) } return orgImg } }
撮影した写真と星空写真の合成
ここもそんなに難しく考えないで、シンプルに同じ位置にUIImageViewを2枚重ねて合成させる。 星空を乗せるUIImageViewは透過しておきます。
var stellarImgView = UIImageView() var photoImgView = UIImageView() override func viewDidLoad() { super.viewDidLoad() // 撮影した写真を表示するView self.photoImgView.frame = CGRect(x:0, y:0, width:self.view.frame.width, height:self.view.frame.height*0.9) self.photoImgView.center.x = CGFloat(self.view.frame.width/2) view.addSubview(self.photoImgView) // 上から重ねる星空画像のView self.stellarImgView.frame = CGRect(x:0, y:0, width:self.view.frame.width, height:self.view.frame.height*0.9) self.stellarImgView.center.x = CGFloat(self.view.frame.width/2) self.stellarImgView.backgroundColor = .clear self.stellarImgView.alpha = 0.5 self.view.addSubview(self.stellarImgView) }
いざ実行
おおお...!!
とりあえず自分の机で撮ってみたが意外にイケてる...!(若干暗いけど...)
実際に夜空を撮ってみたら下のような感じになりました。結構星っぽくね。
終わり
結構満足してる。
ただ元々SoftwareDesignの記事が超簡単そうだったのでやってみたけど結構時間かかりました。。。
特にseleniumをheadlessにするところやPHPでスクリプトを起動するとプロセス取られっぱなしになったりとかstellariumがどんな環境変数使っているのかとか。実際にやらなかったら「ほーんなるほど簡単そうやな」で終わってた。
でもホントは記事の再現じゃなくてARカメラでリアルタイムに星空を表示したかったんですよね。
ただこれには現状の「毎回stellariumを起動するので画像の取得が超遅い」問題を解決しないといけなかったので断念。
まあとりあえずこれでリリースしてみてもいいかなって思タヨ。
星は結構ネタを盛り込めそうなのでリリースするなら色々実験したいことがあって、
- 太陽をタップしたと思ったら広告だった
- 広告を間違えてタップしたと思ったら課金アイテムもらえた
- 星をズームしたと思ったら孫正義の顔だった
- 人の顔のパーツをつないで勝手に何座か決める
などなど楽しみがいっぱいある。
星なんて全く興味なかったけど、間に「開発」を挟むと結構何でもおもしろくなることに気づいた。
良い開発ライフだった。