Raspberry Piのpicamera2のプレビューモードの"size"って何がどうなってるんだ?(Python)

 picamera2の機能の一つプレビューモード。シンプルにカメラの映像をウィンドウに投影してくれるありがたい機能です。ただシンプルがゆえに「?」と思う所も。その一つが"size"指定。表示解像度を決めるパラメータですが、例えばカメラ自体の縦横比が16:9のワイドだとして、sizeに(640,480)と4:3の縦横比を指定した時、表示される映像はいったいどうなるのか?カメラが捉える映像の横に合わせている?それと縦?それとも画面中央を切り取っている??この辺りをちゃんと把握しないとカメラの機能を持て余してしまうかもしれないんです。高解像度のカメラモジュールを買ったのに低解像度な表示しかしていなかったとなったら悲しいですよね。

 シンプルがゆえに実は曖昧に捉えがちな"size"パラメータ。今回はpicamera2のプレビューモードで設定できる"size"を検証してみましょう。

検証1:デフォルトはどうなってる?

 一番簡単なプレビュー表示のコードはこんな感じです:

from picamera2 import Picamera2, Preview
import time

pc2 = Picamera2()
pc2.start_preview( True )
pc2.start()

# カメラの情報を取得&出力
conf = pc2.create_preview_configuration()
print( conf[ "main" ][ "size" ] )

time.sleep( 2 )

pc2.capture_file( "image.png" )

プレビューのconfigurationをすべてデフォルトにしてstartメソッドを呼んでいます。もちろんプレビューが表示されますが、この時の表示解像度であるmainストリームのサイズはどうなっているのでしょうか?

 上コードのprint部分でそのサイズを出力しています。これによると、

(640, 480)のようです。縦横比が4:3ですね。これはおそらくPicamera2ライブラリが決めている初期値だと思います。プレビュー画面と保存した画像はそれぞれ以下の通りになりました:

プレビュー画面(640x480, 4:3)
保存画像(640x480, 4:3)

双方一致していますね。これを基準にサイズを色々変えてpicamera2の仕様を掴んでみましょう。

検証2:縦横比は同じでサイズを小さくしたら?

 では次にこの4:3の縦横比(アスペクト比)は同じでサイズを1/5 (128x96)に設定してみます:

from picamera2 import Picamera2, Preview
import time

prevW = 640 // 5
prevH = 480 // 5

pc2 = Picamera2()
conf = pc2.create_preview_configuration( main = { "size" : ( prevW, prevH ) } )
pc2.configure( conf )

pc2.start_preview( True )
pc2.start()

# カメラの情報を出力
print( conf[ "main" ][ "size" ] )

time.sleep( 10 )

pc2.capture_file( "image.png" )
プレビュー画面(128x96, 4:3)
保存画像(128x96, 4:3)

 プレビューウィンドウ自体のサイズは同じですが、表示解像度が1/5になったため、出力映像が拡大されプレビュー表示がジャギってます。これは予想通りです。保存画像も指定のサイズに縮小しました。スクリーンショットは100%表示サイズなので確かに1/5になっていますね。

 注目は取れている絵の画角・これデフォルトと同じみたいですね。縦横比が一緒なので「まぁそうだろうなぁ」という気もします。でもです、次その予想が外れます!

検証3:縦横比は同じでサイズを大きくしたら?

 今度は先程の逆で、縦横比は変えずサイズを大きく指定したらどうなるかやってみましょう。プレビューの横幅限度は4096ピクセルなので、その最大サイズ(4096, 3072)にしてみます:

from picamera2 import Picamera2, Preview
import time

prevW = 4096
prevH = 3072

pc2 = Picamera2()
conf = pc2.create_preview_configuration( main = { "size" : ( prevW, prevH ) } )
pc2.configure( conf )

pc2.start_preview( True )
pc2.start()

# カメラの情報を出力
print( conf[ "main" ][ "size" ] )

time.sleep( 10 )

pc2.capture_file( "image.png" )
プレビュー画面(4096x3072, 4:3)
保存画像(4096x3072, 4:3)

高解像なのでプレビュー画面、保存画像共に目立ったジャギも見られず綺麗です。ただ、ただです!デフォルト時より画角が広いんです!先程は無かった乾電池の左右の空間が入っていますよね。もちろんカメラを引いたりはしていません。カメラにズーム機能も無いです。全く同じカメラポジションで縦横比も同じなのに、これ…え、なんで?ってなります。

検証4:ScalerCropを見てみる

 検証1のデフォルトと検証3の高解像度時で縦横比が同じなのに画角が異なる事がわかりました。そこで双方の「ScalerCrop」を見てみましょう。

 ScalerCropというのはセンサーが捉えた映像を切り取る範囲の事です。これはカメラ起動後にcapture_metadataメソッドを呼び出すと得られるmetadataというデータ構造体から得る事が出来ます。デフォルト時と高解像時それぞれで範囲を取得してみます:

from picamera2 import Picamera2, Preview
import time

pc2 = Picamera2()

# 解像度テスト
def resoTest( width, height ):
    # プレビュー起動
    conf = pc2.create_preview_configuration( main = { "size" : ( width, height ) } )
    pc2.configure( conf )
    pc2.start_preview( True )
    pc2.start()

    # カメラの情報を出力
    fullReso = pc2.camera_properties['PixelArraySize']
    scalerCropLowReso = pc2.capture_metadata()['ScalerCrop']

    print( conf["main"]["size"] )
    print( fullReso )
    print( scalerCropLowReso )

    time.sleep( 1 )
    pc2.stop_preview()
    pc2.stop()
    time.sleep( 1 )


# 低解像度でプレビュー
resoTest( 640, 480 )

# 高解像度でプレビュー
resoTest( 4096, 3072 )

 metadataは辞書形式で"ScalerCrop”というキーで切り取り範囲をタプル型で取得出来ます。上ではもう一つPicamera2.camera_propertiesメソッドで取れるカメラプロパティの"PixelArraySize"からカメラセンサーの最大縦横ピクセル数を取得しています。

 (640,480)の低解像度と(4096,3072)の高解像度とで範囲情報を出力すると以下のようになりました:

(640, 480)
(4608, 2592)
(1152, 432, 2304, 1728)
 
(4096, 3072)
(4608, 2592)
(576, 0, 3456, 2592)

上が低解像度、下が高解像度で、一塊は上からmainのsize(表示解像度)、カメラセンサーの最大縦横幅(フル解像度)、そしてScalerCrop(切り取り範囲)です。

 ScalerCropの4成分のタプルはセンサー内の長方形の範囲を表していて、それぞれ(x, y, width, height)に対応しています。以上の情報を図示してみるとこうなります:

 上段の白い範囲がセンサーの最大領域です。緑色の所がScalerCropが表すセンサー内で切り取られる映像範囲です。そして下段の青い矩形がmainの"size"で指定した表示解像度です。

 まず注目ですが、左側の低解像度も右の高解像度も「mainの"size"で指定した解像度とScalerCrop領域の解像度は等しくない」という事がわかります。低解像度の方はどこから出てきたか謎な(2304, 1728)というScalerCropで画角が切り取られ、それが表示や保存時に(640, 480)に縮小されています。一方高解像度の方はmainで指定した表示サイズがセンサーの解像度を超えているため、ScalerCrop領域がセンサーの縦幅いっぱいになっています。それに見合う横幅で領域が切り取られ、それが(4096, 3072)に引き伸ばされています。

 そうなんです。センサーが捉えたフル映像がmainの"size"の指定によって意図的に限定して切り取られているんです。検証3で見られた画角の違いはここから来ています。実際上の高解像度側の緑の領域に高解像度で撮影した画像をはめ込んで、低解像度の範囲を重ねて実際の保存画像と見比べてみると、

ほら、ピッタリ合っていますよね。

検証5:mainの"size"は低解像度指定だけどカメラのフル解像度の画角を表示保存して欲しい

 ここまでのお話をまとめます。プレビューのmianの"size"で表示解像度を決めるとその縦横比な画像がプレビュー表示され、画像も指定の解像度で保存されました。縦横比が同じでも低解像度と高解像度とではセンサーの切り取り領域(ScalerCrop領域)が異なり、これにより画角の違いが生じる事もわかりました。

 実はpicamera2ではmainに指定した"size"の大きさからセンサーモード(sensor format)が自動的に選択されます。そのセンサーモードからさらにScalerCrop領域が決められます。sensor formatはカメラモジュール毎に固定値のパターンがいくつか決まっていまして、その中から選ばないといけないんです。

 検証2で"size"を縮小したのに画角はデフォルトと変わらなかったですよね。あれは検証1と2とで同じsensor format(1536x864)が使われたためだったんです。検証3で高解像度にした時このsensor formatが4608x2592に変更されました。画角が広くなった直接理由はこれです。

 mainの"size"に合わせてPicamera2が良さそうな範囲を選択してくれると言うと聞こえが良いのですが、逆に言えばこれは勝手なデジタルズームが入るようなもので、それはそれで困る場合もあります。広範囲な画角を持つカメラを使っているのであれば、中央の狭い範囲に絞るのではなくてその範囲の映像をちゃんと表示して欲しいわけです。でも解像度が高いままだとつらいので、表示解像度は落としたい。ストリーミング配信などはこうしないとネットワーク負荷が爆裂してしまいますからね。

 mainの"size"で低解像度指定しても画角は保つ。これどうするか?当初僕もやり方がわからなくてあれこれ色々テストしてみましたが全然うまく行かず…。で、調べて見つけたのがこちらのpicamera2のissuesフォーラム:

結論から言うとconfigurationのrawストリームにカメラのフル解像度を指定します。

from picamera2 import Picamera2, Preview
import time

# プレビュー起動
pc2 = Picamera2()
prevW = 640
prevH = 480
fullReso = pc2.camera_properties['PixelArraySize']
conf = pc2.create_preview_configuration( main = { "size" : ( prevW, prevH ) }, raw={ "size" : fullReso } )
pc2.configure( conf )
pc2.start_preview( True )
pc2.start()

time.sleep( 5 )

pc2.capture_file( "image_rescale.png" )

 まずPicamera2.camera_propertiesに"PixelArraySize"を指定しセンサーのフル解像度を取得します。次にcreate_preview_configurationのrawストリーム設定の"size"にそのフル解像度を指定。こうすると…、

ターミナルの出力に「4608x2592」という今搭載しているカメラの限界最大サイズのsensor formatを使用する旨が出力されました。これrawを未指定のままにすると「1536x864」と低くて中央寄りの範囲が使用される表示だったので、rawの"size"指定が有効に利いています!

 実際プレビューと保存画像も、

プレビュー画面(640x480, 4:3)
保存画像(640x480, 4:3)

このように高解像度と同様の広さを画角に収めつつ、保存サイズは640x480の低解像度になりました(カメラを触ってしまって画角がちょっとずれちゃった(^-^;;;)

 rawの"size"を指定する事でセンサーが持つ広い画角を使いつつ低解像度で表示保存する事ができました。めでたしめでたしなのですが一つ注意としては、センサーの限界解像度をフルで使うという事は、その分だけ処理速度がかかる事になります。プレビューサイズが640x480だとしても、センサーが捉えた最も広い映像を膨大なメモリに収める処理が入るのは変わりないので、「640x480にしたからFPSは高速のまま」というわけではありません。実際上のでカメラを動かしてみると、カクカクというわけではありませんが画面が歪む現象(ローリングシャッター現象)が顕著に感じられました:

 sensor formatをより低い解像度に下げれば負荷が下がり、ローリングシャッタ現象も抑えられます。ただ画角は狭くなるトレードオフは避けられません。

まとめと終わりに

 今回はプレビューで設定する"size"の実態が何なのかについて色々実験して確かめてみました。実験で分かった事を列挙すると、

  • mainの"size"は純粋に表示や保存の解像度になる

  • mainの"size"によりsensor formatとScalerCropが異なる。これにより画角の違いが生じる

  • 縦横比が同じで解像度が異なっても画角を同じにしたい場合はrawの"size"を指定して同じsensor format及びScalerCropが選択されるようにする。

  • mainの"size"によらず高解像度なsensor formatは高負荷なので注意

こういう事のようです。

 カメラモジュールのスペックには画角が角度表示されています。これはレンズが捉えられる最大の画角で、センサーの範囲をフルに利用して初めて得られます。でも今回の実験で見てきたように、デフォルト表示ではその画角の一部しか使用していない可能性があります。ちゃんとセンサーの縦横比を調べ、その比率をmainの"size"に指定、さらにrawの"size"にもフル解像度を指定する事で初めてカメラの最大解像度を扱えるようになります。

 次回は今回得た知見を踏まえつつセンサーの一部分を切り取る事で画角を拡大する「デジタルズーム」にチャレンジしてましょう。

ではまた(^-^)/

いいなと思ったら応援しよう!