AppleデバイスのLiDARスキャナとHoudini

 最近のiPad ProやiPhone 12 Pro / Maxに搭載されているLiDARスキャナで取得した点群をHoudiniに取り込んでみるテストをしたのでメモ。

使用機材

iPad Pro 2020
Houdini 18.5.351
pronoPointsScan

PCにファイルを転送する

  • pronoPointsScanで点群をスキャンし、ファイルとして保存します
  • iOSのファイルアプリで、「このiPad内 → pronoPointScan」を開きます
  • 先程保存した目的のファイルを「コピー」
  • PCから参照しやすい場所に「ペースト」
    • 今回は、OneDrive経由でPCにファイルを転送しました

Houdiniでファイルを開く

Table Import SOPを作り、先程PCに転送したtxtファイルを開きます。

pronoPointsScanから出力されるデータは非常にシンプルで、以下のようなフォーマットになっています。

各行が一つ一つのポイント情報に相当し、各行にはそれぞれ以下のように数字が羅列されています。

@P.x,@P.y,@P.z,@Cd.r,@Cd.g,@Cd.b


@Cdを修正

pronoPointsScanで生成した点群データのカラー値は0-255なので、Wrangleを書いて0-1に修正します。

v@Cd = fit(v@Cd, {0,0,0}, {255,255,255}, {0,0,0}, {1,1,1});

方向を修正

Bound SOPで点群のOBBと、OBBをまっすぐに戻すためのxformアトリビュートを作成


点群ジオメトリにxformアトリビュートをコピー

Attribute Copy SOPで、点群ジオメトリにOBBのxformアトリビュートをコピーする


点群の軸を修正する

Match Size SOPで、Bound SOPで生成されたxformアトリビュートを使い、点群の軸を修正する

今回は点群の撮影場所が室内だったこともあり、たまたまいい方向に向いてくれましたが、点群の形状によっては全く見当違いな方向に向く可能性があるので、その場合は適宜、いい感じに工夫する必要があります。


問題点

  • Table Import SOPが非常に遅い
    • Pythonでやるなら先にtxtを解釈し、行数分のポイントを作成した後、アトリビュート配列を組み立ててから、hou.Geometry.setPointFloatAttribValues(name, values)を使って、一括でセットするなどしてタイムロスを最小限に抑える必要がありそう。
  • ポイントにはそもそも法線情報がないので、Poinit Cloud Iso SOPなどで面を貼るためには下準備が必要

以上、手軽にLiDARスキャナで遊べる小ネタでした。

Ricoh THETA S でIBL画像を作成するためのスクリプトを書きました

簡易的なIBL用のHDRIを作成するため、素材になる天球マップ画像をRicoh THETA SをiPhoneで遠隔操作して手軽にオートブラケット撮影できるようにしてみました。

撮影方法は、ISOを固定しEVを変化させて行う露出ブラケット撮影になります。
今の所、使用する撮影モードではシャッタースピードを同時に操作できなそうなので、自動設定にしてあります。

THETA SはRAWデータでの撮影ができず、また撮影時に厳密なホワイトバランス設定などもできないので精度は落ちますが、簡易的に使用するにはそこそこ使えるHDRIが得られると思います。

iOSデバイスに限らず、THETA Sとネットワーク接続できるデバイスで、なおかつ標準的なPythonを実行できるなら同じように使用できるはずです。

というわけで、需要がありそうなのでソースコードを公開します。

時間の関係で良い作例の用意ができていませんが、そのうち貼りたいと思います。

・使い方

1:デバイスとTHETA Sをネットワーク接続接続します

2:下記スクリプトをデバイス内のPython実行環境で実行します
当方は、iPhone6S+上で iOS用のPython 2.7 というアプリを使用しています。

3:撮影されたJPEG画像をHDR ShopLuminance HDRなどを使ってHDRIにします

#******************************************************************************
'''
Tool Name     : MYAM_ThetaAutoBracket.py
Description   : 
Copyright     : (c) Yamabe Michiyoshi
Author Name   : Yamabe Michiyoshi
'''
#******************************************************************************

import httplib
import json
import time

# 
class ThetaSettings(object):
    # Full
    # evs = [-2.0, -1.7, -1.3, -1.0, -0.7, -0.3, 0.0, 0.3, 0.7, 1.0, 1.3, 1.7, 2.0]

    # Simple
    evs = [-2.0, -1.0, 0.0, 1.0, 2.0]

    exposureProgram = 9
    iso = 100
    whiteBalance = "auto"
    _shutterVolume = 50

    shutterInterval = 1

#
class Theta(object):
    def __init__(self):
        self.headers = {"Content-Type":"application/json", "Accept":"application/json"}

    # Auto Bracket
    def takePicturesByAutoBracket(self):

        print "--------------------"
        print "START AUTO BRACKET"
        print "--------------------"

        # Connect to THETA 
        connection = self.connect()

        # Start Session
        self.startSession(connection)

        # Get Session ID
        responseData = self.getResponseData(connection)
        sessionId = responseData["results"]["sessionId"]

        # Take Pictures
        for i in range(len(ThetaSettings.evs)):
            
            # Set options
            ev = ThetaSettings.evs[i]
            
            print "----------"
            print "%i / %i" % ((i + 1), len(ThetaSettings.evs))
            print "iso : %i" % ThetaSettings.iso
            print "EV  : %d" % ev

            picOpt = {"exposureProgram":ThetaSettings.exposureProgram,
                      "exposureCompensation":ev,
                      "iso":ThetaSettings.iso,
                      "whiteBalance":ThetaSettings.whiteBalance,
                      "_shutterVolume":ThetaSettings._shutterVolume}

            self.setOptions(connection, sessionId, optionParams=picOpt)
            responseData = self.getResponseData(connection)

            # Take Pictute
            self.takePicture(connection, sessionId)
            
            # Wait to finish idle time
            self.waitFinishCurrentCommand(connection, interval=ThetaSettings.shutterInterval)

        # Close Session
        self.closeSession(connection, sessionId)

        # Disconnect from THETA
        self.disconnect(connection)

        print "--------------------"
        print "FINISH AUTO BRACKET"
        print "--------------------"

    # HTTPConnection Control
    def connect(self):
        connection = httplib.HTTPConnection("192.168.1.1",80)
        return connection

    def disconnect(self,connection):
        connection.close()

    # Common Commands
    def postExecCommand(self,connection,params):
        connection.request("POST", "/osc/commands/execute", params, self.headers)

    def postStatusCommand(self, connection, commandId):
        params = json.dumps({ "id":commandId })
        connection.request("POST", "/osc/commands/status", params, self.headers)

    def waitFinishCurrentCommand(self, connection, interval=1):
        responseData = self.getResponseData(connection)
        commandId = responseData["id"]

        while True:
            self.postStatusCommand(connection, commandId)
            responseData = self.getResponseData(connection)
            commandStatus = responseData["state"]

            if "inProgress" == commandStatus:
                print "commandId [%s] : Processing..." % ( commandId )
                time.sleep(interval)
            else:
                print "commandId [%s] : Finish!" % ( commandId )
                break

    # Command Control
    def startSession(self, connection):
        params = json.dumps({ "name":"camera.startSession", "parameters":{} })
        self.postExecCommand(connection, params)

    def updateSession(self,connection,sessionId):
        params = json.dumps({ "name":"camera.updateSession", "parameters":{"sessionId":sessionId} })
        self.postExecCommand(connection, params)

    def closeSession(self, connection, sessionId):
        params = json.dumps({ "name":"camera.closeSession", "parameters":{"sessionId":sessionId} })
        self.postExecCommand(connection, params)

    def takePicture(self,connection , sessionId):
        params = json.dumps({ "name":"camera.takePicture", "parameters":{"sessionId":sessionId} })
        self.postExecCommand(connection, params)

    def setOptions(self, connection, sessionId, optionParams):
        params = { "name":"camera.setOptions" , "parameters":{"sessionId":sessionId, "options":{} } }

        for key , value in optionParams.items():
            params["parameters"]["options"][key] = value

        jsonParams = json.dumps(params)
        self.postExecCommand(connection, jsonParams )

    def getOptions(self, connection, sessionId, optionNames):
        params = { "name":"camera.setOptions" , "parameters":{"sessionId":sessionId, "optionNames":{} } }

        for key , value in optionParams.items():
            params["parameters"]["options"][key] = value

        jsonParams = json.dumps(params)
        self.postExecCommand(connection, jsonParams )

        self.postExecCommand(connection, params)

    # Support Functions
    def getResponseString(self,connection):
        response = connection.getresponse()
        responseString = response.read()
        return responseString

    def getResponseData(self,connection):
        responseString = self.getResponseString(connection)
        responseData = self.convertJSONToPythonData(responseString)
        return responseData

    def convertJSONToPythonData(self,jsonDataString):
        jsonDecoder = json.JSONDecoder()
        pythonData = jsonDecoder.decode(jsonDataString)
        return pythonData

    def convertPythonDataToJSON(self,pythonData):
        jsonEncoder = json.JSONEncoder()
        jsonDataString = jsonEncoder.encode(pythonData)
        return jsonDataString

def main():
    theta = Theta()
    theta.takePicturesByAutoBracket()

if __name__ == "__main__":
    main()

・設定

設定の変更を行うにはコードを直接書き換える必要があります。
撮影枚数は、ThetaSettingsの evs で指定されているEV値の数で指定されます。
撮影ごとにリスト内のEV値がそれぞれ使用されます。

・EV値と撮影枚数の設定

class ThetaSettings(object):
    # Full
    # evs = [-2.0, -1.7, -1.3, -1.0, -0.7, -0.3, 0.0, 0.3, 0.7, 1.0, 1.3, 1.7, 2.0]

    # Simple
    evs = [-2.0, -1.0, 0.0, 1.0, 2.0]

指定できるEV値は、上記の Full で指定されている13個の値になります。
THETA Sは一枚撮影するごとに内部処理に約8秒ほどかかるようなので、撮影枚数が多くなると時間がかかります。
必要に応じ、不要な値を削除するなどして調整してください。

・各種オプションの設定

    exposureProgram = 9
    iso = 100
    whiteBalance = "auto"
    _shutterVolume = 50

現在は、上記を指定できます。
必要に応じ、RICOH THETA API v2 Referenceを参照して値を設定してください。
といいつつおそらく上記の中では iso と _shutterVolume くらいしかいじらないと思います。

・_shutterVolume
0−100の間で指定します。0が無音です。

・iso
以下の値が使えます
100, 125, 160, 200, 250, 320, 400, 500, 640, 800, 1000, 1250, 1600

ExpoDisc 2.0 でお手軽ホワイトバランス生活

 ・マニュアルホワイトバランスの重要性

写真を正しい色味で仕上げるためにはRAWデータでの撮影とホワイトバランス(WB)調整が不可欠です。

誤ったWB設定のまま、何十枚、何百枚と撮影された画像を、一枚ずつ調整するのは非常に手間がかかるため、できるだけ撮影の時点で正しいWB設定を使用しておきたいものです。

また、RAWではなくJPGで撮影する場合は、後処理で厳密なWB調整ができないため、なおのこと撮影時点で正確なWB設定を行うことが重要です。

仕上げの味付けのため、RAW現像処理でWBを大幅に変えることがありますが、この時、基準となる正しいWB設定が分かっていると、調整のやり直しや、ニュートラルな色味を中心に効果的な調整がしやすくなります。

このように正しいWBで撮影することには多くのメリットがあり、時間の節約にもなるのでとても重要です。


 

・オートホワイトバランス(AWB)の罠

オートホワイトバランスモード(AWB)は、おおよそ正しい色が再現されるようにカメラがWBを自動設定しますが、ほとんどの場合カメラ性能の限界なども手伝い正確な色の再現には至りません。(後半の作例を参照)


 

・WBの設定方法 – ホワイトバランスカードを使う場合

正しいWBで撮影したい場合、一般的には市販のカラーチャートに付属しているホワイトバランスカードやグレースケールカードを使用して設定を行い、マニュアルホワイトバランスモード(MWB)を使用して撮影を行います。

MWBでは、周囲の色味を測るためのMWB画像を撮影し、正確なホワイトバランスを判断するために使用します。

この方法では、被写体の近くにカードを配置して撮影し、MWB画像として使用する必要があるため、被写体のある場所に踏み入れられない場合は使用できません。

※ホワイトバランスカードを使う場合の設定方法(Canon公式サイトより)
http://cweb.canon.jp/camera/cms/c4_1.html


 

・WBの設定方法 ー ExpoDiscを使用する場合

ExposDiscを使用する場合は、MWB画像の作り方が違います。
ここでは、Canon EOS M3での設定方法を例に解説したいと思います。

1・カメラをマニュアルホワイトバランスモード(MWB)に設定する

IMG_0288

MWBモードでは、周囲の光の色味を測定するための画像データ(MWB画像)を解析し、正しいホワイトバランスの計算に使用します。

2・MWB画像を撮影する

フォーカスモードをマニュアルフォーカスモード(MF)にします。
ちなみにMFにするのは、ピントが合っていない状態でシャッターを切るためです。
ExpoDiscをレンズに密着させ、カメラを光源に向けシャッターを切ります。

スタジオ撮影のように被写体が小さく光源が明確な場合は、カメラを被写体の位置から光源方向へ向けることで、より正確なMWB画像が撮影できます。

被写体の色が強く、被写体の表面から跳ね返る光に不要な色が乗ってしまう場合は、純粋な環境光を取り込む事ができず、WBが狂います。
このような場合、光源に向けてシャッターを切るのがいいと思います。

風景写真などの場合は空全体が光源と考えて差し支えないので、純粋に撮りたい方向へカメラを向けてMWB画像を撮影することができます。

シャッタースピードや絞りが極端な値の場合、MWB画像の明るさも極端なものになり、正しいMWB画像として使用できません。撮影時のEVレベルが0になる状態で撮影するのが望ましいです。

IMG_0287

※実際にExpoDiscを使用している様子を自分一人で撮影するのは難しかったので、上の画像は機材一式をテーブルの上に置いた状態で撮影しています。実際は、両手でカメラとExpoDiscをそれぞれ持ってMWB画像を撮影します。

IMG_1972_REF

このように、MWB画像はほぼ単色の画像になります。

3・MWB画像を指定します

設定メニューから「MWB画像選択」を選び、先程撮影した画像を選択します。

IMG_0290

これで、WB設定が完了します。

4・撮影します

あとは撮るだけ。
時間が経過して日差しが変わったり、撮影場所を移動することで光源が変わった場合は、再び同じ手順でWBを調整します。


 

・作例 ー 環境光が暖色の例

AWBでの撮影

IMG_1971_AWB

環境光が暖色の状態で撮影。
環境光の影響で画面全体が若干赤みがかっている事がわかります。

MWB画像

IMG_1972_REF

調整後

IMG_1977_MWB

赤みが取り除かれる事で白がより白くなり、画面全体が自然な色合いになりました。


 

・作例 – 環境光が寒色の例

AWBで撮影

IMG_1982_AWB

環境光が寒色の状態で撮影。
環境光の影響で画面全体が青みがかっている事がわかります。

MWB画像

IMG_1983_REF

調整後

IMG_1985_MWB

画面全体から青みが取り除かれ、自然な色合いになりました。
若干赤みが強くなってしまった感はありますが、色味がAWBに比べ遥かに改善しており、十分に許容範囲と思います。


 

作例を見ても分かる通り、多少のブレはあるものの、ExpoDiscを使用することで、かなり手軽に、おおよそ許容範囲のWBを設定できます。

手頃な価格で現像の手間がかなり軽減できるこの製品、WB設定でお悩みの方は導入してみてはいかがでしょうか。