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
とかあります。