14
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

OculusとTHETAでリアルタイムの旅をしよう〜その2

Last updated at Posted at 2015-04-14

その1はこちら

サービス概要

遠くの場所を、リアルタイムに360度体験できるサービスをなるべく簡単に作ります。

ゴールイメージ

  • 作成したアプリの入ったiPhoneとTHETAを持って旅にでる
  • 家で待っている人が、旅先の風景をOculusでリアルタイムに360°体験できる

使ったもの

Hardware

  • THETA m15
  • Oculus Rift DK2
  • iPhone 6
  • MacBook Air (13-inch, Mid 2012)

Software

  • Ruby 2.1.5
  • Rails 4.1.8
  • Unity
  • Xcode 6.1

サービス全体像

サービス全体像.png

iOS App 編

  • 今回は (1)iOS App 部分をつくります。

前提条件

  • その1ができてること
  • XCode 6.1がインストールされている、Macがあること
    • iPhone実機で実行できる環境がないと寂しいです
  • RICOH THETA があること

RICOH THETA SDKの準備

https://developers.theta360.com/ja/docs/sdk/download.html から RICOH_THETA_SDK_for_iOS.0.2.1.zip をダウンロードして任意の場所に解凍します。(開発者登録が必要です。)

ricoh-theta-sample-for-ios.xcodeproj を開いて、Xcodeから実機で実行してみましょう。

THETAと接続して、接続、一覧の取得・表示、写真の引取等が動作することを確認して下さい。

作成するアプリの説明

画面はSDK付属のサンプルアプリをそのまま利用します。
変更するのは ViewController.m のみです。

実現するシーケンスは下記の通りです。

  1. Connectをタップし、接続する。この時、一覧の取得は行わないようにします。
  2. Captureをタップし、インターバル撮影を開始する。
  3. 写真が撮影されたら、画像の引取を自動で行う
  4. 引取が完了したら、そのままAPIへ画像の投稿を行う
  5. 3,4を撮影が停止するまで繰り返す

1.接続処理

Connectタップ後の処理では、一覧の表示をなくします。

- (void)connect
{
  ...

  // Start enum objects.
  // [self enumObjects]; <- コメントアウトする

2.インターバル撮影の開始

Captureタップ後の処理では、インターバル撮影の開始をします。
撮影枚数の指定はなしにして、THETAの空きとバッテリーがある限り撮影させます。
撮影間隔は15秒に1枚にします。iPhoneの回線状態が良ければ10秒でも良いかもしれません。

- (IBAction)onCaptureClicked:(id)sender
{
  ...

  // This block is running at PtpConnection#gcd thread.
  // BOOL rtn = [session initiateCapture]; <- コメントアウト

  // 追加ここから
  [session setUint16PropValue:PTPDP_TIMELAPSE_NUMBER value:0]; // 撮影枚数の指定なし(撮影し続ける)
  [session setUint32PropValue:PTPDP_TIMELAPSE_INTERVAL value:15000]; // 15秒に1枚撮影を指定
  [session setUint16PropValue:PTPDP_STILL_CAPTURE_MODE value:PTPIP_STILL_CAPTURE_MODE_TIMELAPSE]; //インターバル撮影モードを指定
  BOOL rtn = [session initiateOpenCapture]; // インターバル撮影開始
  // 追加ここまで

3.撮影完了後に画像の引取をおこなう

撮影完了のイベントを受信したら、loadObject: session: を呼び出すように変更します。

-(void)ptpip_eventReceived:(int)code :(uint32_t)param1 :(uint32_t)param2 :(uint32_t)param3
{
  ...

  [_ptpConnection operateSession:^(PtpIpSession *session) {
    // コメントアウトここから
    // [_objects addObject:[self loadObject:param1 session:session]];
    // NSIndexPath* pos = [NSIndexPath indexPathForRow:_objects.count-1 inSection:1];
    // dispatch_async_main(^{
    //   [_contentsView beginUpdates];
    //   [_contentsView insertRowsAtIndexPaths:@[pos]
    //                        withRowAnimation:UITableViewRowAnimationRight];
    //   [_contentsView endUpdates];
    // });
    // コメントアウトここまで

    // 追加ここから
    [self loadObject:param1 session:session];
    // 追加ここまで
  }];

loadObject は戻り値が不要になるので、メソッドごと書き換えます。
画像のデータを取り出し、postEqui: を呼び出します。

// - (PtpObject*)loadObject:(uint32_t)objectHandle session:(PtpIpSession*)session { ... }
- (void)loadObject:(uint32_t)objectHandle session:(PtpIpSession*)session
{
    PtpIpObjectInfo* objectInfo = [session getObjectInfo:objectHandle];
    if (!objectInfo) return;
    
    if (objectInfo.object_format==PTPIP_FORMAT_JPEG) {
        NSMutableData* imageData = [NSMutableData data];
        BOOL result = [session getResizedImageObject:objectHandle width:2048 height:1024
                            onStartData:^(NSUInteger totalLength) {
                                NSLog(@"getResizedImageObject(0x%08x) will received %zd bytes.", objectHandle, totalLength);
                            } onChunkReceived:^BOOL(NSData *data) {
                                [imageData appendData:data];
                                return YES;
                            }];
        if (result)  [self postEqui:imageData];
    }
}

4.画像の引取後、APIへ画像の投稿を行う

postEqui: ではメソッド名の通り、Equirectangular画像のpostを行います。
[app name] には前回 作成したアプリの名前が入ります。

- (void)postEqui:(NSData*)equi
{
    NSURL* url = [NSURL URLWithString:@"https://[app name].herokuapp.com/equi"];
    const NSString* boundaryConstant = @"----------V2ymHFg03ehbqgZCaKO6jy";
    const NSString* fileParamConstant = @"img";
    
    
    NSURLSession* session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    
    NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url];
    [request setHTTPMethod:@"POST"];
    
    NSString* contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundaryConstant];
    [request setValue:contentType forHTTPHeaderField:@"Content-Type"];
    
    NSMutableData* body = [NSMutableData data];
    [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundaryConstant] dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fileParamConstant, @"img"] dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[[NSString stringWithFormat:@"Content-Type: image/jpeg\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:equi];
    [body appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
    [body appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundaryConstant] dataUsingEncoding:NSUTF8StringEncoding]];
    NSString *postLength = [NSString stringWithFormat:@"%zu", (unsigned long)[body length]];
    [request setValue:postLength forHTTPHeaderField:@"Content-Length"];

    NSURLSessionUploadTask * uploadTask = [session uploadTaskWithRequest:request fromData:body
                                                       completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                                           dispatch_async_main(^{
                                                               if (!error) {
                                                                   [self appendLog:[NSString stringWithFormat:@"POST failed(%@)", error]];
                                                               }
                                                               else {
                                                                   [self appendLog:@"POST success"];
                                                               }
                                                           });
                                                       }];
    [uploadTask resume];
}

5. 3, 4を撮影が停止するまで繰り返す

ptpip_eventReceived は画像の撮影完了時に都度呼ばれるので、特に追加で行うことはありません。

iOSアプリを動かして、アップロード後に、下記のコマンドをshellから実行して、画像がアップロードされているか確認してみましょう。ここでも [app name] は前回作成したものです。

$ curl -o theta.jpg https://[app name].herokuapp.com/equi/dl && open theta.jpg

つづき

だいぶ時間が空いてしまいましたが、最終回はUnityでAPIから画像をダウンロードしてきて、リアルタイムにOculusで画像を見るところをつくります。

その3: http://qiita.com/sandinist/items/6e2b9856c9273d580fa2

14
16
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?