Pythonスクリプトの記述

基本事項

Choreonoid上で実行されるPythonスクリプトは、Choreonoid内蔵のPythonインタプリタで実行されます。そこでは動作中のChoreonoid上のアイテムやビューといったオブジェクトに直接アクセスすることができます。

スクリプトではまず、使用する機能に対応するcnoidパッケージのモジュールをインポートします。

from cnoid.Util import *
from cnoid.Base import *
from cnoid.Body import *
from cnoid.BodyPlugin import *

各モジュールの概要については Pythonスクリプト機能の概要 の「Pythonバインディング」を参照してください。Choreonoid上で実行する場合、cnoidパッケージのパスはあらかじめモジュール検索パスに設定されているため、そのままインポートすることができます。

PythonバインディングのクラスやメソッドはC++ライブラリのものにほぼ対応しており、基本的にC++と同じ名前で利用できます。またC++で getX / setX となっているアクセサの一部は、Pythonではプロパティとして参照・代入できるようになっています(例: body.numJointsitem.name )。

アイテムの操作

Choreonoidの操作を自動化する上で基本となるのは、アイテムの操作です。

アイテムツリーの最上位にあるルートアイテムは以下で取得できます。

from cnoid.Base import *

rootItem = RootItem.instance

新たなアイテムを生成してツリーに配置するには、アイテムクラスのオブジェクトを生成して addChildItem で親アイテムに追加します。

from cnoid.BodyPlugin import *

worldItem = WorldItem()
RootItem.instance.addChildItem(worldItem)

既存のアイテムを取得するには findItem を使用します。アイテムの名前(階層をたどる場合はパス)で検索する方法と、アイテムの型で検索する方法があります。

# 名前・パスによる検索
robotItem = RootItem.instance.findItem("World/SR1")

# 型による検索(最初に見つかったアイテムを返します)
simulatorItem = RootItem.instance.findItem(SimulatorItem)

アイテムの選択状態やチェック状態も以下のように操作できます。

simulatorItem.setSelected(True)
robotItem.setChecked(True)

ファイルに対応するアイテムでは load でファイルを読み込むことができます。ファイルパスには ${SHARE} (Choreonoidのshareディレクトリ)、 ${HOME} (ユーザのホームディレクトリ)、 ${PROJECT_DIR} (プロジェクトファイルのディレクトリ)といったパス変数を使用できます。

robotItem = BodyItem()
robotItem.load("${SHARE}/model/SR1/SR1.body")

プロジェクト構築の例

以下は、SR1モデルの歩行シミュレーションのプロジェクトをスクリプトで構築して、シミュレーションを開始する例です。Choreonoidのサンプルに含まれるスクリプト sample/python/SR1Walk.py の内容となります。

from cnoid.Util import *
from cnoid.Base import *
from cnoid.Body import *
from cnoid.BodyPlugin import *
import math

worldItem = WorldItem()
RootItem.instance.addChildItem(worldItem)

robotItem = BodyItem()
robotItem.load("${SHARE}/model/SR1/SR1.body")

robot = robotItem.body
robot.rootLink.setTranslation([0.0, 0.0, 0.7135])

q = [  0.0, -2.1, 0.0,   4.5, -2.4, 0.0,
      10.0, -0.2, 0.0, -90.0,  0.0, 0.0, 0.0,
       0.0, -2.1, 0.0,   4.5, -2.4, 0.0,
      10.0, -0.2, 0.0, -90.0,  0.0, 0.0, 0.0,
       0.0,  0.0, 0.0  ]

for i in range(robot.numJoints):
    robot.joint(i).q = math.radians(q[i])

robot.calcForwardKinematics()
robotItem.storeInitialState()

controllerItem = SimpleControllerItem()
controllerItem.setController("SR1WalkPatternController")
robotItem.addChildItem(controllerItem)
robotItem.setChecked(True)
worldItem.addChildItem(robotItem)

floorItem = BodyItem()
floorItem.load("${SHARE}/model/misc/floor.body")
worldItem.addChildItem(floorItem)

simulatorItem = AISTSimulatorItem()
simulatorItem.setTimeStep(0.002)
simulatorItem.setActiveControlTimeRangeMode(True)
worldItem.addChildItem(simulatorItem)
simulatorItem.setSelected(True)

simulatorItem.startSimulation()

このスクリプトでは以下の処理を行っています。

  1. ワールドアイテムを生成してルートアイテムの下に配置する

  2. ボディアイテムを生成してSR1のモデルファイルを読み込む

  3. ボディアイテムからボディモデル(Bodyオブジェクト)を取得し、ルートリンクの位置と各関節の関節角を設定して、順運動学計算( calcForwardKinematics )で全身の姿勢を更新する

  4. storeInitialState で現在の姿勢をシミュレーションの初期状態として記憶させる

  5. 歩行パターンを再生するシンプルコントローラを設定したコントローラアイテムをロボットの子アイテムとして配置する

  6. 床モデルとAISTシミュレータアイテムを配置する

  7. startSimulation でシミュレーションを開始する

このように、GUI上で行うプロジェクト構築の操作は、おおよそスクリプトでも記述することが可能です。

シミュレーションとの連携

読み込み済みのプロジェクトに対してシミュレーションの実行を制御することもできます。以下は、シミュレーションを開始し、シミュレーションが終了したらその結果の概要を出力するスクリプトです。

from cnoid.Base import *
from cnoid.BodyPlugin import *

def onSimulationFinished(isForced):
    print("The simulation finished at %f [s]." % simulatorItem.currentTime)

simulatorItem = RootItem.instance.findItem(SimulatorItem)
simulatorItem.sigSimulationFinished.connect(onSimulationFinished)
simulatorItem.setRealtimeSyncMode(SimulatorItem.NonRealtimeSync)
simulatorItem.setSelected()
simulatorItem.startSimulation()

ここではシミュレータアイテムの持つシグナル sigSimulationFinished に関数を接続することで、シミュレーション終了時の処理を記述しています。このように、C++のAPIと同様に、シグナルに対して関数を接続してイベント駆動の処理を記述することができます。

シミュレーションのように時間のかかる処理の完了を待つ場合は、必ずこのようにシグナルで受け取るようにしてください。ループで完了を待つような書き方をすると、Choreonoidのイベント処理が進まなくなり、シミュレーション自体が正しく処理されなくなります。これについては バッチ実行と終了の制御 も参照してください。

このスクリプトを後述の コマンドラインからのスクリプト実行--batch オプションと組み合わせることで、シミュレーションをバッチ的に実行することが可能となります。この場合、シミュレーションが終わればChoreonoidも自動的に終了するため、スクリプトに終了のための記述は必要ありません。また、シミュレーションの開始・終了と連動してスクリプトを実行したい場合は、 Pythonシミュレーションスクリプト も利用できます。

メッセージの出力

スクリプトの実行状況やエラーをユーザに伝えるための出力方法について説明します。

printの出力先

Pythonの print による出力は、メッセージビューに出力されます 。Pythonプラグインがインタプリタの初期化時に sys.stdoutsys.stderr をメッセージビュー向けに差し替えているためで、端末に直接出力されるわけではありません。 sys.stderr も同じ扱いなので、例外のトレースバックもメッセージビューに出ます。

そのうえで、非対話モードが有効な場合は、メッセージビューの内容が標準出力にも出力されます。したがって最終的な出力先は以下のようになります。

実行形態

printの出力先

通常のGUI実行

メッセージビューのみ。端末には出力されません

--non-interactive 指定時

メッセージビューと標準出力

--batch 指定時

同上( --non-interactive を含むため)

--headless 指定時

同上。ウィンドウが無いので実質的に標準出力のみとなります

これらのオプションについては コマンドラインからのスクリプト実行 を参照してください。

なお、 Pythonコンソール に入力したコードの出力については扱いが異なります。詳しくは同ページを参照してください。

MessageOutによる出力

print は手軽ですが、出力の種別を指定することができません。種別を使い分けたい場合は、Choreonoid本体でも使用している MessageOut クラスを使用します。 MessageOut.master で標準の出力先が得られます。

from cnoid.Base import *
from cnoid.Util import *

mout = MessageOut.master

mout.putln("The processing has started.")
mout.putHighlightedln("An important notice")
mout.putWarningln("The setting is not valid, so the default value is used.")
mout.putErrorln("The file could not be read.")

各関数の使い分けは以下のとおりです。

関数

用途

putln

通常のメッセージです。処理の進行状況などを伝えます。

putHighlightedln

通常のメッセージのうち、特に目立たせたいものに使用します。

putWarningln

警告です。処理は続行できたものの、注意すべき点があることを伝えます。「警告:」という接頭辞が付きます。

putErrorln

エラーです。処理が失敗し、目的が達成されなかったことを伝えます。「エラー:」という接頭辞が付きます。

メッセージビュー上では、通常のメッセージ以外はすべて赤字で表示されます。 つまり、ハイライト・警告・エラーのいずれも赤字になり、警告とエラーにはさらに接頭辞が付きます。

出力の種別を引数で指定することもできます。

mout.putln("An error message", MessageOut.MessageType.Error)

注釈

エラーを出力すると、Choreonoidの終了コードが1になります。 警告やハイライトは終了コードに影響しません。バッチ実行の成否をシェルスクリプトから判定する場合、この違いが意味を持ちます。詳しくは バッチ実行と終了の制御 を参照してください。

メッセージの反映(フラッシュ)

出力したメッセージは、必ずしもその時点で画面に表示されるわけではありません。溜まっているメッセージを実際にメッセージビューへ反映させる操作を「フラッシュ」と呼びます。

上で紹介した末尾に "ln" の付く関数は、改行を出力するとともにフラッシュも行います 。そのため通常はこれらを使っておけば、メッセージはその都度表示されます。

一方、末尾に "ln" の付かない putputHighlightedputWarningputError もあり、こちらは**改行もフラッシュも行いません** 。1行のメッセージを複数回に分けて組み立てながら出力する場合に使用します。この場合、行の出力を終えたところで flush を呼んでください。

mout.put("Processing")
mout.put(" ... ")
mout.put("done.")
mout.flush()

これが特に問題になるのは、 時間のかかる処理を行っている最中に途中経過を表示したい場合 です。スクリプトが処理を続けている間、Choreonoidの画面は更新されません。 putln を使うか、明示的に flush を呼ぶことで、その時点までのメッセージが表示されるようになります。

注釈

フラッシュは画面の更新処理を伴うため、非常に細かい間隔で繰り返し呼ぶと処理速度が低下します。

複数行のメッセージを出力する場合、 putln を行数分繰り返すとその回数だけフラッシュが行われます。これを避けるには、メッセージ中に改行文字を含めて put で出力し、最後に一度だけフラッシュします。

mout.put("Line 1\n")
mout.put("Line 2\n")
mout.put("Line 3\n")
mout.flush()

出力するメッセージがあらかじめ分かっている場合は、ひとつの文字列にまとめてしまうのが最も簡潔です。

mout.putln("Line 1\nLine 2\nLine 3")

ループの中で毎回出力するような場合も、同様に出力をまとめるか、出力の頻度を落とすことを検討してください。

なお、 print による出力は毎回自動的にフラッシュされるので、この点を意識する必要はありません。

メッセージに限らず、スクリプトの処理中はGUI全体が更新されません。これについては次の スクリプト処理中のGUIの更新 を参照してください。

スクリプト処理中のGUIの更新

ChoreonoidのGUIは、Choreonoid本体が制御を持っている間に更新されます。 スクリプトが処理を続けている間、GUIの表示は更新されません。

例えば以下のスクリプトは、ロボットの全関節の角度を変更して、その変更をアイテムに通知しています。

from cnoid.Base import *
from cnoid.BodyPlugin import *
import math, time

bodyItem = RootItem.instance.findItem(BodyItem)
body = bodyItem.body

for i in range(body.numJoints):
    body.joint(i).q = math.radians(40.0)
body.calcForwardKinematics()
bodyItem.notifyKinematicStateChange()

time.sleep(20)   # 時間のかかる処理の代わり

notifyKinematicStateChange によって姿勢の変更は通知されていますが、その後スクリプトが処理を続けている間、 シーンビュー上のロボットの姿勢は変わりません。 表示が更新されるのは、スクリプトが処理を終えてChoreonoidに制御が戻ってからです。

処理の途中で表示を更新したい場合は、 App.updateGui を呼びます。

from cnoid.Base import *

bodyItem.notifyKinematicStateChange()
App.updateGui()      # ここで表示が更新される

これにより、それまでに行った変更が画面に反映されます。ロボットの姿勢を少しずつ変化させるアニメーションをスクリプトで行いたい場合などは、変更のたびに App.updateGui を呼ぶ必要があります。

前節で述べたメッセージのフラッシュも、この画面更新の一種です。 putlnflush はメッセージビューの表示を更新するもので、シーンビューを含めたGUI全体を更新したい場合は App.updateGui を使用してください。

注釈

App.updateGui はGUIのイベント処理を行うため、その間にユーザの操作やタイマー等の処理が実行される可能性があります。また、頻繁に呼ぶと処理速度が低下します。

注釈

時間のかかる処理を行う場合、本来はスクリプト側で制御を握り続けるのではなく、シグナルやタイマーを利用して処理を分割し、Choreonoidに制御を返しながら進める方が望ましい構成です。 バッチ実行と終了の制御 も参照してください。

Qtのクラスの利用

cnoid.QtCorecnoid.QtGuicnoid.QtWidgets の各モジュールを用いると、Qtの主要なクラスをスクリプトから利用できます。以下はサンプル sample/python/TimerSample.py の内容で、QTimerを用いて1秒ごとに処理を実行する例です。

from cnoid.QtCore import *

class TimerSample:
    def __init__(self):
        self.timer = QTimer()
        self.timer.setInterval(1000)
        self.timer.timeout.connect(self.doSomething)
        self.timer.start()
        self.counter = 0

    def doSomething(self):
        print("do something %d" % self.counter)
        self.counter += 1
        if self.counter == 10:
            self.timer.stop()

timerSample = TimerSample()

このほか、QtWidgetsのウィジェット類を用いて、ボタンやダイアログ等からなる独自のGUIをスクリプトで構築することも可能です。

クラスや関数の調べ方

Pythonスクリプトの記述に使用できるクラスや関数については、Pythonコンソール上で各モジュールをインポートし、Python標準の dir() 関数や help() 関数で確認することができます。

>>> import cnoid.Body
>>> dir(cnoid.Body)
>>> help(cnoid.Body.Body)

Pythonコンソール の入力補完(Tabキー)を用いて、クラスの持つメソッドをその場で確認しながら試すこともできます。

サンプルスクリプト

Choreonoidのソースの sample/python/ ディレクトリに、Pythonスクリプトのサンプルが格納されています。スクリプトを記述する際の参考にしてください。以下はその一部です。

ファイル

内容

SR1Walk.py

SR1モデルの歩行シミュレーションのプロジェクトを構築して実行します。

StartSimulation.py

読み込み済みプロジェクトのシミュレーションを開始します。

StartSimulationAndQuitWhenFinished.py

シミュレーションを開始し、終了したらChoreonoidを終了します。(同じことは コマンドラインからのスクリプト実行--batch オプションでも行えます)

KinematicsTest.py

ボディモデルに対する運動学計算のテストを行います。

BodyShaker.py

選択されたボディモデルを振動させます。独自ツールバーの追加やシグナル、タイマーの利用例にもなっています。

TimerSample.py

QTimerを用いた周期処理の例です。