Raspberry Piのpicamera2でデジタルズーム(Python)

 picamera2にはセンサーが捉えた映像の一部を切り取る機能があります。これを利用するといわゆるデジタルズームを実現できます。

ScalerCropでセンサー領域を切り取り

 デジタルズームというのは高解像度な元映像の一部を切り取って拡大する疑似的なズーム法です。これを実現するにはPicamera2.set_controlsにScaleCropキーにセンサー内で切り取りたい矩形領域をパラメータとして渡します。

 具体的なコードを見てみましょう:

import time

from picamera2 import Picamera2, Preview

# カメラをプレビューモードで起動
picam2 = Picamera2()
picam2.start_preview(Preview.QTGL)

# 高解像度でスタート
fullReso = picam2.camera_properties['PixelArraySize']  # センサー解像度
aspectRatio = fullReso[ 0 ] / fullReso[ 1 ]            # アスペクト比(W/H)
previewReso = ( int( 800 * aspectRatio ), 800 )        # プレビュー表示解像度

preview_config = picam2.create_preview_configuration( main = {"size" : previewReso}, raw = {"size" : fullReso})
picam2.configure(preview_config)
picam2.start()

time.sleep(3)

# 現在のScalerCrop領域を取得
#  ScalerCrop -> [ left, top, width, height ]から[width, height]へ
scalerCrop = picam2.capture_metadata()['ScalerCrop']
scalerCropWH = scalerCrop[2:]

# 縦横比と中心は一緒で幅高が半分の領域を切り取り
#  = デジタルズーム
halfReso = [ int( s // 2 ) for s in scalerCropWH ]
leftTop = [ ( f - l ) // 2 for f, l in zip( scalerCropWH, halfReso ) ]

print( "Pre scalerCrop", scalerCrop )
print( "Full Reso.", fullReso )
print( "Zoom ScalerCrop", leftTop + halfReso )

picam2.set_controls({"ScalerCrop": leftTop + halfReso})

time.sleep( 5 )

 全体的な流れは、まずカメラセンサーの縦横比に合わせたプレビューを表示しています。表示は低解像度ですがrawストリームはフル解像度にしています。次に今の設定でセンサーで使用している領域(ScalerCrop)を得て、その半分になる領域を計算します。最後にその領域をカメラに設定して2倍デジタルズームに切り替えています。

 途中色々ごにょごにょとやっていますが、最終的に一番下段にある、

picam2.set_controls({"ScalerCrop": leftTop + halfReso})

ここがデジタルズームの指定をしている本丸です。Picamera2.set_controlsメソッドに"ScalerCrop"をキーとしてセンサー内の矩形領域を指定すると、その領域を切り取って使うようになります。ごにょごにょしている大半はその領域を計算しているだけです。

 上コードを実行して撮影した映像がこちら:

元高解像度映像
2倍デジタルズーム

上が元の解像度、下が2倍ズームした映像です。でっかくなっていますw。下の映像を半分サイズにして元の映像に重ねてみると、

見事にぴったりです(^-^)

オフセット位置を割合で指定

 ScalerCropは切り取る幅高と一緒に左上座標も指定します。これにより切り取り位置を変更する事が出来ます。先程のコードにあるleftTopはズーム後も中心点を変えないようにわざわざ計算していたんですが、次に2倍ズームは変えずに切り取り位置をパーセントで指定できるよう先のコードを改造してみましょう。

 切り取り矩形の左上座標が動ける幅を100%として割合指定します。こうすると(50%, 50%)でちゃんと中央になります。

import time

from picamera2 import Picamera2, Preview

# カメラをプレビューモードで起動
picam2 = Picamera2()
picam2.start_preview(Preview.QTGL)
fullReso = picam2.camera_properties['PixelArraySize']  # センサー解像度
aspectRatio = fullReso[ 0 ] / fullReso[ 1 ]            # アスペクト比(W/H)
previewReso = ( int( 800 * aspectRatio ), 800 )        # プレビュー表示解像度
preview_config = picam2.create_preview_configuration( main = {"size" : previewReso}, raw = {"size" : fullReso})
picam2.configure(preview_config)
picam2.start()

time.sleep(3)

# 現在のScalerCrop領域を取得
#  ScalerCrop -> [ left, top, width, height ]から[width, height]へ
scalerCrop = picam2.capture_metadata()['ScalerCrop']
scalerCropWH = scalerCrop[2:]

# 縦横比と中心は一緒で幅高が半分の領域を切り取り
#  = デジタルズーム
halfReso = [ int( s // 2 ) for s in scalerCropWH ]

# 指定の割合位置になるScalerCrop左上座標を算出
leftTopRate = [ 0.65, 0.75 ]
moveLen = [ int( f - h ) for f, h in zip( scalerCropWH, halfReso ) ]
leftTop = [ int( b + m * r ) for b, m, r in zip( scalerCrop, moveLen, leftTopRate ) ]

print( "Pre scalerCrop", scalerCrop )
print( "Full Reso.", fullReso )
print( "Zoom ScalerCrop", leftTop + halfReso )

picam2.set_controls({"ScalerCrop": leftTop + halfReso})

time.sleep( 5 )

 途中までは最初のコードと一緒です。leftTop座標を計算している所辺りが追加している部分です:

# 指定の割合位置になるScalerCrop左上座標を算出
leftTopRate = [ 0.65, 0.55 ]
moveLen = [ int( f - h ) for f, h in zip( scalerCropWH, halfReso ) ]
leftTop = [ int( b + m * r ) for b, m, r in zip( scalerCrop, moveLen, leftTopRate ) ]

 1行でごそっと色々な事をしているので解説を。 
 moveLenが左上座標が動ける範囲です。zip関数は引数の配列を要素ごとにまとめたタプルを作ってくれる便利関数です。zip( scalerCropWH, halfReso )によって( ( scalerCropWH[0], halfReso[0] ), ( scalerCropWH[1], halfReso[1] ) )という塊ができます。それをfor文で回して縦横それぞれの移動可能幅を計算し配列として格納しています。

 leftTop座標も似たようなzipとfor文の合わせ技で計算しています。一つ気を付けたいのが左上座標の0%に当たる点は「ScalerCropの左上座標」になる事です。(0,0)が左上原点ではありません。そのため「b」にScalerCropの左上座標を渡して足し算しています。

 コード内は(65%, 75%)の位置になるようにしています。実際動かして出力した映像を重ねてみると、

そんな感じの位置になっています!

ズーム倍率と位置割合の両方を指定

 では最後にズーム倍率と位置割合の両方を指定できるコードに仕上げてみましょう。再利用性を上げるためScalerCropの計算部分を関数として分離します:

import time

from picamera2 import Picamera2, Preview

# 指定のズーム倍率と位置割合のScalerCrop値を算出する
def calcSropScale( baseScalerCrop, zoom, leftTopRate ):
    scalerCropWH = baseScalerCrop[2:]

    # zoom倍になる解像度を計算
    if ( zoom <= 0.0 ):
        zoom = 0.01
    halfReso = [ int( s // zoom ) for s in scalerCropWH ]

    # 指定の割合位置になるScalerCrop左上座標を算出
    moveLen = [ int( f - h ) for f, h in zip( scalerCropWH, halfReso ) ]
    leftTop = [ int( b + m * r ) for b, m, r in zip( baseScalerCrop, moveLen, leftTopRate ) ]

    return leftTop + halfReso


# カメラをプレビューモードで起動
picam2 = Picamera2()
picam2.start_preview(Preview.QTGL)
fullReso = picam2.camera_properties['PixelArraySize']  # センサー解像度
aspectRatio = fullReso[ 0 ] / fullReso[ 1 ]            # アスペクト比(W/H)
previewReso = ( int( 800 * aspectRatio ), 800 )        # プレビュー表示解像度
preview_config = picam2.create_preview_configuration( main = {"size" : previewReso}, raw = {"size" : fullReso})
picam2.configure(preview_config)
picam2.start()

# 元のScalerCropを格納
baseScalerCrop = picam2.capture_metadata()['ScalerCrop']

time.sleep(3)

# ズーム倍率と左上割合を指定
params = [ (3.0, (0.65, 0.75)), (4.0, (0.4, 0.15)) ]

for p in params:
    scalerCrop = calcSropScale( baseScalerCrop, p[0], p[1] )
    picam2.set_controls({"ScalerCrop": scalerCrop })
    time.sleep( 5 )

 元のScalerCropの範囲に対してzoom倍とleftTopRate率になるScalerCropを算出するcalcScalerCorp関数を追加しました。中でやっている事はこれまでと基本一緒です。

 テストとして「3倍, (65%, 75%)」と「4倍, (40%, 15%)」のデジタルズームを5秒間隔で連続的に切り替えてみています。結果はこんな感じで、

3倍ズーム、位置(65%, 75%)
4倍ズーム、位置(40%, 15%)

楽しい~。zoom倍率の分だけ得られた映像を縮小して該当位置に当てはめると指定の位置にぴったりです。ちゃんとデジタルズーム画像を得られています!

終わりに

 今回はPicamera2のScalerCropを指定してデジタルズームを実現してみました。元のScalerCropにセンサーで使用されている範囲が格納されているので、それを基準に切り取り矩形を計算しました。

 今回は位置を割合として指定しましたが、もちろん絶対座標で指定する事も出来ます。ただセンサーのScalerCropの範囲はカメラモジュールやrawストリームに指定する解像度などで多義に変化するため、絶対座標指定でユーザビリティーを確保するのは割合指定より面倒になります。

 ScalerCropを駆使すれば、現在の中心絶対座標を固定してズーム倍率のみを変化させるとか、タッチスクリーンでズーム拡大した映像を左右に振るとか、そういう事も頑張れば出来そうですね。

ではまた(^-^)/

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