RICOH THETAで撮った写真をPTP-IPで取得する

前回シャッター操作をしたので、撮った写真の取得もしたくなることでしょう。
今回もRuby-PTP-IPをベースにして写真の取得シーケンスとコードを解説します。

THETAでの画像取得について


PTP対応機種では写真を撮ると、画像、サムネイル、メタデータが作成されます。
それらはまとめて一つの32bit整数が割り当てられます。
取得、変更、削除はその値を指定してアクセスすることになります。
これをオブジェクトハンドルといいます。一意の値です。


同一のオブジェクトハンドルに対して

  • 画像を取得する時はGetObject
  • サムネイルはGetThumb
  • メタデータはGetObjectInfo

と、オペレーションコードを変えてアクセスすることでそれぞれのデータを得る形です。


セッション内で取得したオブジェクトハンドルはそのセッション中でのみ一意性が保証されます。
まあ通常、セッションまたいでも同一ですが。


オブジェクトハンドルの取得についてですが、
前回のシャッター操作のようにInitiateCapture後はObjectAddedイベントが来ます。
そして、そのイベントの一つ目のパラメータに新しいオブジェクトハンドルが格納されています。
・・・が、THETAでは常に100が入っていて、そのオブジェクトハンドルでアクセスしてもエラーとして
レスポンスコード:PTP_RC_InvalidObjectHandle
が帰ってきます。(2014/08/24 : こちらのバグでした)


よって、今回のサンプルではTHETA内のオブジェクトハンドルを全て取得しています。
そしてTHETAではその末尾が最も新しいオブジェクトハンドルなのでその値を使用しています。

シーケンス


シーケンス図では長くなるので画像とサムネイルにおけるデータフェイズの記述を省略しています。







今回はイベントコネクションを使ったセッション中の通信は無いです。
THETAではイベントコネクションを開いていなくてもオペレーションリクエストを受付るらしいので、画像取得だけなら省略してしまってもいいでしょう。

コード


接続やセッションの部分は前回(修正版)と同じです。
今回はセッション中にサムネイルと画像を受信して保存しているだけです。
簡単ですね。

#!/bin/ruby

require 'socket'

require './ruby_ptp_ip/ptp.rb'
require './ruby_ptp_ip/ptp_ip.rb'

# THETAのIPアドレスとPTP-IPのポート番号
ADDR = "192.168.1.1"
PORT = 15740

# THETAに送るレスポンダ情報
NAME = "Ruby_THETA_GetObject"
GUID = "ab653fb8-4add-44f0-980e-939b5f6ea266"
PROTOCOL_VERSION = 65536

# GUID文字列を整数の配列にする関数
def str2guid(str)
    hexes = str.scan /([a-fA-F0-9]{2})-*/
    hexes.flatten!
    raise "Invalid GUID" if hexes.length != 16
    hexes.map do |s|
        s.hex
    end
end

# パケットを書き込む関数
def write_packet(sock, pkt)
    sock.send(pkt.to_data.pack("C*"), 0)
end

# パケットを読み込む関数
def read_packet(sock)
    data = []
    data += sock.read(PTPIP_packet::MIN_PACKET_SIZE).unpack("C*")
    len = PTPIP_packet.parse_length(data)
    data += sock.read(len-PTPIP_packet::MIN_PACKET_SIZE).unpack("C*")
    PTPIP_packet.new(data)
end

# データフェイズの読み込み関数
def recv_data(sock, transaction_id)
    recv_pkt = read_packet(sock)
    raise "Invalid Packet : #{recv_pkt.to_s}" if recv_pkt.type != PTPIP_PT_StartDataPacket
    raise "Invalid Transaction ID" if recv_pkt.payload.transaction_id != transaction_id
    data_len = recv_pkt.payload.total_data_length_low
    data = []
    while recv_pkt.type != PTPIP_PT_EndDataPacket
        recv_pkt = read_packet(sock)
        raise "Invalid Packet : #{recv_pkt.to_s}" if recv_pkt.type != PTPIP_PT_DataPacket && recv_pkt.type != PTPIP_PT_EndDataPacket
        raise "Invalid Transaction ID" if recv_pkt.payload.transaction_id != transaction_id
        data += recv_pkt.payload.data_payload
    end
    raise "Invalid Data Size" unless data_len == data.length
    return data
end

# コマンドコネクション初期化関数
def init_command(sock)
    init_command_payload = PTPIP_payload_INIT_CMD_PKT.new()
    init_command_payload.guid = str2guid(GUID)
    init_command_payload.friendly_name = NAME
    init_command_payload.protocol_version = PROTOCOL_VERSION 
    init_command = PTPIP_packet.create(init_command_payload)

    write_packet(sock, init_command)
    read_packet(sock)#ACK
end

# イベントコネクション初期化関数
def init_event(sock, conn_number)
    init_event_payload = PTPIP_payload_INIT_EVENT_REQ_PKT.new()
    init_event_payload.conn_number = @conn_number
    init_event = PTPIP_packet.create(init_event_payload);
    
    write_packet(sock, init_event)
    read_packet(sock)
end

# データフェイズのあるオペレーションコード実行関数
def data_operation(sock, operation_code, transaction_id, parameters = [])
    op_payload = PTPIP_payload_OPERATION_REQ_PKT.new()
    op_payload.data_phase_info = PTPIP_payload_OPERATION_REQ_PKT::NO_DATA_OR_DATA_IN_PHASE
    op_payload.operation_code = operation_code
    op_payload.transaction_id = transaction_id
    op_payload.parameters = parameters
    op_pkt = PTPIP_packet.create(op_payload)
    write_packet(sock, op_pkt)

    data = recv_data(sock, transaction_id)
    recv_pkt = read_packet(sock)
    return recv_pkt, data
end

# シンプルなオペレーションコードの実行関数
def simple_operation(sock, operation_code, transaction_id, parameters = [])
    op_payload = PTPIP_payload_OPERATION_REQ_PKT.new()
    op_payload.data_phase_info = PTPIP_payload_OPERATION_REQ_PKT::NO_DATA_OR_DATA_IN_PHASE
    op_payload.operation_code = operation_code
    op_payload.transaction_id = transaction_id
    op_payload.parameters = parameters
    op_pkt = PTPIP_packet.create(op_payload)
    write_packet(sock, op_pkt)

    read_packet(sock)
end

# 
# ここから処理
# 
TCPSocket.open(ADDR, PORT) do |s|

    # コマンドコネクション
    recv_pkt = init_command(s)
    raise "Initialization Failed (Command) #{recv_pkt.payload.reason}" if recv_pkt.type == PTPIP_PT_InitFailPacket
    p @conn_number = recv_pkt.payload.conn_number
    p @guid = recv_pkt.payload.guid
    p @name = recv_pkt.payload.friendly_name
    p @protocol_version = recv_pkt.payload.protocol_version

    TCPSocket.open(ADDR, PORT) do |es|

        # イベントコネクション
        recv_pkt = init_event(es, @conn_number)
        raise "Initialization Failed (Event) #{recv_pkt.payload.reason}" if recv_pkt.type == PTPIP_PT_InitFailPacket
        print "Command/Event Connections are established.\n"
        
        @transaction_id = 1
        @session_id = 1

        # セッション開始
        recv_pkt = simple_operation(s, PTP_OC_OpenSession, @transaction_id, [@session_id])
        raise "Open Session Failed #{recv_pkt.payload.response_code}" if recv_pkt.payload.response_code != PTP_RC_OK
        @transaction_id += 1
        
        # オブジェクトの列挙
        # 1つ目のパラメータはストレージID. 0xFFFFFFFFのときは全てのストレージから列挙
        # 2つめのパラメータはフォーマットID. 0xFFFFFFFFの時は全てのフォーマット.
        # 3つめのパラメータはオプション.場合によって異なるが、今回の全列挙の場合は0.
        recv_pkt, data = data_operation(s, PTP_OC_GetObjectHandles, @transaction_id, [0xFFFFFFFF, 0xFFFFFFFF, 0])
        raise "GetObjectHandles Failed #{recv_pkt.payload.reason_code}" if recv_pkt.payload.response_code != PTP_RC_OK
        offset = 0
        @object_handles, offset = PTP_parse_long_array(offset, data)
        p @object_handles #オブジェクトハンドルのダンプ
        @transaction_id += 1

        print "GetThumb...\n"
        # 最後に撮ったサムネイルの取得
        # 一番目のパラメータにObjectHandleを格納
        recv_pkt, data = data_operation(s, PTP_OC_GetThumb, @transaction_id, [@object_handles[-1]])
        raise "GetThumb Failed #{recv_pkt.payload.reason_code}" if recv_pkt.payload.response_code != PTP_RC_OK
        File.open("./theta_thumb.jpg", "wb") do |f|
            f.write(data.pack("C*"))
            print "Saved!\n"
        end
        @transaction_id += 1

        print "GetObject...\n"
        # 最後に撮った写真の取得
        # 一番目のパラメータにObjectHandleを格納
        recv_pkt, data = data_operation(s, PTP_OC_GetObject, @transaction_id, [@object_handles[-1]])
        raise "GetObject Failed #{recv_pkt.payload.reason_code}" if recv_pkt.payload.response_code != PTP_RC_OK
        File.open("./theta_pic.jpg", "wb") do |f|
            f.write(data.pack("C*"))
            print "Saved!\n"
        end
        @transaction_id += 1

        # セッション終了
        recv_pkt = simple_operation(s, PTP_OC_CloseSession, @transaction_id) 
        raise "Close Session Failed #{recv_pkt.payload.response_code}" if recv_pkt.payload.response_code != PTP_RC_OK
        @transaction_id += 1

    end

end


プログラム的にサムネイル、画像を取得できるようになれば定期的な撮影とアップロードとかを自動で出来ますね。
RICOHのページにアップする以外にもウェブページに埋め込んだり、アプリに埋め込んだりという選択肢もあるので、以下のプログラムと組み合わせるといいかもです。

ブラウザで見る場合は
Haga氏のビューワ(github)
https://github.com/thaga/IOTA
akokubo氏のビューワ(github)
https://github.com/akokubo/ThetaViewer

Oculus持っている人は
MobileHackerzの中の人のOculus用ビューワ
http://mobilehackerz.jp/contents/Review/RICOH_THETA/Oculus

とかあります。