ライブの興奮をそのまま伝える自動撮影システムのカメラ制御に求められるものとは?

ライブ会場をPTZカメラで撮影している様子

みなさまこんにちは。おさかなと申します。 私はソニー・ミュージックエンタテインメントに所属するプログラマーです。現在は業務でカメラを制御して音楽ライブを自動で撮影するシステムを開発しています。そこで今回は私が開発しているカメラ制御について紹介します。

そもそもカメラ制御とは?

そもそもカメラ制御とは何でしょうか? 開発内容について紹介する前に、まずはカメラを制御する上で重要となるパラメータについて紹介します。

私が現在制御しているカメラは「PTZカメラ」というカメラなのですが、PTZカメラの制御には大きく3つの要素「Pan (パン)」「Tilt (チルト)」「Zoom (ズーム)」があります。Panとはカメラの水平(横)方向の動き、Tiltとはカメラの垂直(縦)方向の動き、Zoomとは拡大・縮小を意味しています。

PTZカメラの制御には大きく3つの要素「Pan (パン)」「Tilt (チルト)」「Zoom (ズーム)」があることを説明する図版

一般的にカメラを設置するときには下から「三脚」、その上にカメラを固定するための「雲台」、そしてその上に「カメラ」が置かれます。この「雲台」でPan/Tiltを操作し、カメラのズームリングを回すことでZoomを変えることができます。対してPTZカメラでは、内部にモーターが内蔵されておりモーターを制御することでPan・Tilt・Zoomを動かします(下図参照)。

一般的なカメラとPTZカメラの違いを説明する図版

場合によっては背景のぼかしなどのために「Focus (フォーカス)」を制御することもありますが、今回は割愛します。このように、Pan・Tilt・Zoomを制御すると任意の画角の映像を撮影することができます。 後述しますが、一部のPTZカメラはPan・Tilt・Zoomをコンピュータ経由で制御することができる「VISCAプロトコル」に対応しています。これを使用してカメラに制御コマンドをコンピュータから流すことで、人がカメラを動かさずとも自動で移動して好きな画角の映像を撮影することが可能となります。

さて、以降では私が実装する中で工夫・苦労したカメラ制御のポイントについて、例を交えて紹介します。

なお、現在私が業務で使っているカメラ(冒頭の写真に写っているカメラです)は、ソニー製のPTZカメラでありながらレンズ交換も可能な、デジタル一眼カメラのような特性も備えた特殊な製品です。ご興味のある方はこちらをご覧ください。

カメラ制御の工夫ポイント①~複数のカメラで動作させる~

私が工夫した一点目は複数のカメラやレンズに対応させるためにコードに柔軟性を持たせた点です。ライブ撮影では、それぞれのカメラが担う役割によってレンズを使い分ける必要があります。柵前に置くカメラやステージ全体を撮影するカメラは広角レンズで、客席側の奥からアーティストをアップショットで撮るカメラは望遠レンズで、といった具合です(レンズを交換できないカメラの場合はカメラ自体を使い分けることになりますね)。分かりやすい例を挙げると、大きな会場で焦点距離の小さなレンズを使用するとアーティストの顔をアップで映すことができないため焦点距離の大きなレンズを選択する必要があります。

レンズによってアングルが変わることを説明する図版

このように、会場によって使用するレンズを変更する場合、複数のレンズに対応したコードを書く必要があります。しかし、使用するレンズが増える度に一つずつ処理を追加していると似たようなコードが現れて冗長になる上、制御コードの変更の際にその影響範囲が広くなり拡張性も損なってしまいます。そこでこの取り組みでは「Strategyパターン」と呼ばれるオブジェクト指向のデザインパターンの考え方を導入することで重複のない柔軟性のあるコードを実現しました。

Strategyパターンとは場面に応じて切り換えられる複数の「戦略」を事前に別クラスとして定義しておくことで、さまざまなシチュエーションに対応できるようにする手法です。言葉だけでは分かりにくいので、カメラの場合を例に挙げて紹介したいと思います。

別々の仕様を持つALensとBLensのズームを制御する場合を考えます。まずはStrategyパターンを使わずにレンズクラスを実装した場合です。

public class Lens
{
    //レンズごとのズーム処理
    public void DriveZoom(LensType lensType, double targetFocalLength, int velocity)
    {
        switch(lensType)
        {
             case: ALens
                //ALensのvelocity制御コード(velocity)
                break;

             case: BLens
                //BLensのvelocity制御コード(velocity)
                break;

             default:
                break;
        }

        //////////
        //レンズ共通の処理1
        ~~~
        //////////

        switch(lensType)
        {
             case: ALens
                //ALensのZoom制御コード(targetFocalLength)
                break;

             case: BLens
                //BLensのZoom制御コード(targetFocalLength)
                break;

             default:
                break;
        }

        //////////
        //レンズ共通の処理2
        ~~~
        //////////

        switch(lensType)
        {
             case: ALens
                // ALensの終了処理コード
                break;

             case: BLens
                //BLensの終了処理コード
                break;

             default:
                break;
        }

    }
}

上記のクラスの場合ではlensTypeでどのレンズが使用されているかを判断し、レンズに応じたズーム処理を行っています。動作に問題はありませんが、この実装だと新規に対応するレンズが追加になるたびに条件分岐が増えていきます。また、今回はZoom処理しか行っていませんが、他の処理を行う場合も同様に条件分岐させる必要があります。

それではStrategyパターンを使用した場合のコードを紹介します。

Interface ILensStrategy
{
    void SetZoomVelocity(double velocity);
    void Zoom(double targetFocalLength);
    void Reset();
}

public class ALensStrategy: ILensStrategy
{
    public void SetVelocity(int velocity)
    {
        //velocityの設定処理
    }

    public void Zoom(double targetFocalLength)
    {
        //ALensのZoom制御コード(targetFocalLength)
    }

    public void Reset()
    {
        //Zoom後の終了処理
    }
}

public class BLensStrategy: ILensStrategy
{
    public void SetVelocity(int velocity)
    {
        //velocityの設定処理
    }

    public void Zoom(double focalLength)
    {
        //BLensのZoom制御コード(targetFocalLength)
    }

    public void Reset()
    {
        //Zoom後の終了処理
    }
}

まずインターフェースで元となるILensStrategyを作成し、それを継承してレンズクラスALensStrategyとBLensStrategyを作成しています。このクラス内でレンズ固有の制御を定義することで、固有の処理をそのクラス内だけで完結させることができます。もし使用するレンズの追加(CLens)があった場合もレンズ用のクラス(CLensStrategy)を新規で作るだけで済みます。

また、レンズのZoom制御を行うControllerクラスを実装する際も、レンズの違いを意識せずに固有の制御の呼び出しが可能です。

public class Controller
{
    ILensStrategy lens;

    public Controller(LensType lensType)
    {
        switch(lensType)
        {
            case ALens:
                lens = new ALens();
                break;

            case BLens:
                lens = new BLens();
                break;

            default:
                break;
        }
    }

    public void DriveZoom(double targetFocalLength, int velocity)
    {
        //搭載レンズが何かを意識しなくても良くなる
        lens.SetVelocity(velocity);
        
        //////////
        //レンズ共通の処理1
        ~~~
        //////////

        lens.Zoom(targetFocalLength);

        //////////
        //レンズ共通の処理2
        ~~~
        //////////

        lens.Reset();
    }
}

コードを解説する図版

カメラ制御の工夫ポイント②~カメラ制御のコア 現在値の取得・通知~

二つ目のカメラ制御における工夫ポイントは現在値の通知方法です。リモート環境でカメラを制御する場合、Pan・Tilt・Zoomの現在値をリアルタイムに取得して多くの関連クラスに通知して情報を更新する必要があります。Pan・Tilt・Zoomのモーターを制御するクラスが最新の値を保持していますが、現在値の更新があったかどうか各クラスが問い合わせ(ポーリング)すると、関連クラスの数が増えるにつれて処理負荷が増えたりタイムラグが発生します。そこで、私はオブジェクト指向デザインパターンの一つであるObserverパターンを使用して現在値の通知を行っています。

Observerパターンとは観察される側(Subject)と観察する側(Observer)が存在し、Subjectに変化があったときにすべてのObserverに通知をするという考え方です。SubjectとObserverは一対多の関係になっており、あらかじめSubjectが通知するObserverを複数登録しておくことで同時に通知することが可能になります。一見普通なようにも思えますが、Subjectから通知を行うことでObserverは常にSubjectを監視することから解放されます。

カメラ制御の例ではモーターを制御するクラスがSubjectで、Controllerなどのモーター制御クラスの外側で現在値が必要となるクラスがObserverに当たります。モーター制御クラス(Subject)にあらかじめ通知処理を記述しておき(NotifyObserver)、通知対象を登録(AddObserver)・削除(DeleteObserver)することで任意のクラスに通知することが可能です。

コードを解説する図版

カメラ制御の苦労ポイント③~遠隔制御!VISCAプロトコル~

一方でカメラ制御で苦労したポイントもあります。それは先述したVISCAプロトコルによる遠隔制御です。VISCAプロトコルとは「Video System Control Architecture」の略称で、様々なビデオ機器をコンピュータで制御するためのプロトコルです。中でもLAN経由でVISCA制御できる「VISCA over IP」を使用すると、PCとカメラが同じネットワーク内にあればどこからでも制御が可能であるため、その場にいなくてもカメラを動かすことができます。

PTZカメラの遠隔操作を説明する図版

コマンドさえ正しく送信すれば無人制御はすぐに実現可能かと思われましたが、精度の面で課題がありました。VISCAプロトコルは制御コマンドを送信するとカメラからACKなどのレスポンスが返却されるのですが、次のコマンドの送信はレスポンスが届いてから実行する必要があります。つまり、コマンド一つ一つを順次制御することになります。

コマンド一つ一つを順次制御することを説明する図版

しかし、精度高く制御するには1秒の間に絶えず複数のコマンド (カメラの現在値取得/Pan・Tilt・Zoom制御)を使用する必要があるため、ACK待ちで制御コマンドの送信回数が少なくなると指定した位置に動くまでに予期せぬ動作が起こることがありました。予期せぬ動作の一つとして、「目標値を行き過ぎてから戻る現象」が挙げられます。これは現在値の取得頻度が低い場合に起きる現象で、「目標値に到着したのに現在値が更新されるのが遅いため行き過ぎてしまう」というものです。

目的地に到着したのに現在値が更新されるのが遅いため行き過ぎてしまうことを説明する図版

上図における目的位置までの制御にはPID制御を使用しているのですが、速度計算のパラメータに問題がありそうなのでその最適値を検討しました。まず、プロトコルの仕様から理論的に送受信可能な制御コマンドの回数を割り出しました。次に、ネットワークのプロトコルアナライザーで解析しながら現在値取得/Pan・Tilt・Zoomの制御間隔を変化させることで最適値を実験的に割り出しました。理論だけでなく実験的な検証も含めたアプローチが必要で調整には少し苦労しましたが、なんとか実現することができ、本番でも無事動作したことを確認しています。

最後に

冒頭の自己紹介でもお伝えしましたが、私が実装しているカメラ制御はライブ自動撮影システムのために作っているものです。そのため、システム全体が機能し、撮影した映像も品質的に問題がないかを確認するために定期的にライブ現場に入って撮影を行っています。システムは実際のカメラマンの方々にも触っていただき、気になる点があれば意見をヒアリングしてブラッシュアップに努めています。

エンタメなので、単純に撮影するだけでなく「魅せる」映像の撮影のために必要な機能を意識してシステムに組み込んでいます。例えば、一般的に「手振れ」は無くした方が良いと思われますが、激しいバンドの撮影などでは手振れがあった方が「魅せる」映像になるということで、あえて手振れを付与して撮影する機能なども実装しています。撮影した映像は事務所側に納品することが多いのですが、映像を見て喜んでもらえた時には非常に励みになります。

また、このシステムは2022年末にソニーグループの社内技術交換会イベントSTEF(Sony Technology Exchange Fair)の対外向けブースにてデモ展示を行いました。

国内外問わずさまざまな方にご見学いただき、システムの意義やその有用性を認知していただけたのがうれしかったですね。

今回は私が業務で関わっているカメラ制御に関して紹介しました。長々とカメラ制御に関して書かせていただきましたが、私自身撮影機材に関わるコーディングは初めてだったので手探りの中、学びながら作業することもたくさんありました。今後も撮影現場などで様々なエンタメにおける撮影を吸収しながらシステムのアップデートをしていきたいと考えています。

それでは、また次の記事でお会いしましょう!