Raspberry Pi 4のカメラのマニュアルフォーカスをGUIからランタイムで動かす(Python、picamera2、Tkinter)

 以前Tktinterを用いてPythonでGUIを表示してみました:

GUIを導入した目的はラズパイに取り付けた外部機器をGUI上で操作しようと思ったからです。具体的には前々回オートフォーカスを説明したカメラモジュールをマニュアル操作したいって事です:

で、前回picamera2のマニュアルフォーカスの設定方法とその注意点をピックアップしてみました。

LensPositionに設定するディオプトリ(焦点距離の逆数)は概算値であり全然信用できないよ~という類の話をしました。

 この辺りを総括して、GUIでマニュアルフォーカスする簡易ツールを作るのが今回の目的です。早速取り掛かりましょう。

焦点距離から設定距離へ

 先の記事でも注意したように、Picamera2のset_controls( "LensPosition", dioptre )に与えるディオプトリ値(焦点距離の逆数値)は概算値であり、ラズパイに付けているカメラモジュールの特性や仕様などに物凄く左右されます。正直何も信用できません(^-^;。そこで実際に想定する焦点距離とピントが合う対象距離の関係を測定し、グラフ化してみます。

 僕のラズパイに取り付けてあるArduCam製の64MP Hawk-eyeの場合は、ざっくりこんな感じでした:

 このグラフのプロットをおおよそ再現する関数を作れば、指定の焦点距離にマッチする設定距離(逆数にしたらディオプトリ値)をプログラム上で計算して割り出せます。

パラメトリックなフィット関数

 グラフの形状を見る限り、対数関数かなとも思ったのですが、解析的に解けない形になりそうなので、次のような反比例の関数を考えてみました:

パラメータがa,b,cの3つあるので、通って欲しい点を3つ選びます。

 僕のカメラの場合、カメラスペックの限界近焦点距離である0.08mの時は設定距離も0.08mでドンピシャでした。つまりこの点は外したくない。この最近距離をN(Near)とすると(N, N)という点がまず候補に上がります。

 次に外したくない点は…僕のカメラだとx=0.2mの時のy=0.12。そしてx=1mの時のy=0.16あたりかなぁと。この2点は通って欲しい。ちょっと一般化してこの2点をP=(Px,Py)、Q=(Qx,Qy)としておきましょう。

 この3点で出来る以下の連立方程式を解きます:

 これは解析的に解く事が出来まして、どりゃーっとやりますと各パラメータを以下の式で計算できます:

実際この式に先の通って欲しい3点を入れて、a,b,cを求めグラフに重ねると、

ほら、いい感じにフィットしました(^-^)。これで指定の焦点距離にピントを合わせる事が出来るようになりました。

 さてではGUIを…と行きたいのですが、実はカメラのマニュアルフォーカスについてはもう一つ考慮しなければならない事があります。それはGUI側のスライダー等の「調整幅」についてです。

GUIの調整幅は逆数軸じゃないと辛い

 この後GUIの所でスライダーを使って焦点距離を設定しようと思っています。スライダーは最小値、最大値、そして調整幅を指定すると定まります。でもこの調整幅は一定なんです。カメラのマニュアルフォーカスの場合、これは致命的です。

 例えば最小焦点距離が0.1mで、最大が10.0mだとしましょう。スライダーのステップ数が100だとすると、1ステップの焦点距離幅は約10cmです。すると最小焦点距離辺りでは1ステップごとに10cm、20cm、30cmという大股刻みになってしまいます。手元にある物などの近距離撮影をしようとする時、これでは全く使い物にならないんですね。逆に焦点距離が5m辺りだともう5.0mのを5.1mに変えても刻み幅のスケールが小さ過ぎてピントは殆ど変わらなくなります。

 つまりカメラのマニュアルフォーカスをする場合は「近距離程細かく、遠距離は大胆に」調整出来ないといけないんです。

 そこでスライダーの最小値を0.0、最大値を1.0と標準化して、それに対応した焦点距離を逆数として求める事にしましょう。グラフでイメージするとこんな感じです:

 横軸Xがスライダーの値で0~1の間です。対応する縦軸Yは焦点距離Dの逆数(1/D)です。x=0の時が最近焦点距離で上の場合だと1/0.08=12.25になっています。注目は0.5の所で、縦軸が2なので焦点距離Dは0.5mです。つまりスライダーの左端から真ん中までの間で8cm~50cmの焦点距離を調整できる事になります。これなら分解能として十分ですよね。最後x=1.0の最遠焦点距離は上の例では5m、つまり1/5=0.2にしています。スライダーの真ん中から右端までで0.5m~5.0mの間を大きな幅で調整している事になります。

 上のようなスライダーを焦点距離の逆数に変換する反比例の式はこうなります:

「あれ?さっき見たぞ。」はい、そうなんです。全く同じ式なんですね。ただし今回はNear=(0.0, 1/N)、Middle=(0.5, 1/M)、Far=(1.0, 1/F)という3点を通るようパラメータa,b,cを求めます。N,M,Fはそれぞれ最近焦点距離、中間焦点距離、最遠焦点距離です。これも連立方程式をたてておりゃーっと計算すると、以下のようになります:

 これでスライダーが人が調整しやすい逆数軸になり、[スライダー値]→[焦点距離の逆数(1/D)]→[焦点距離]と焦点距離を求める事もできるようになります。

 ではようやくGUIを作る事にしましょう。

Tkinterで焦点距離を調整する逆数軸スライダーを作る

 GUIはTkinterで作ります。カメラの焦点距離を手動で変化させるスライダー(Scale)があるウィンドウを作りましょう。

スライダーを作成

 Tkinterの初期設定やスライダー(Scale)の設置については前回説明しておりますので、そこは省略します:

 スライダーの最小値は0.0、最大値は1です。ステップ幅は0.01にしておきましょうか。TkinterのScaleはデフォルトでスライダーの値が表示されますが、今回はこの値に意味はあまりないため非表示にします。替わりに変換後の焦点距離を表示するテキスト(Label)を追加します。

 焦点距離の再計算はスライダーの値が変更された時に行います。これはcommandパラメータに登録する関数内で行えばOKですよね。勿論求めた焦点距離からさらにpicamera2のLensPositionに与える設定距離も計算します。

 以上諸々を考慮してスライダーのサンプルコードを作るとこんな感じになるでしょうか:

from picamera2 import Picamera2
from libcamera import controls
import tkinter
import math

pc2  = Picamera2() # カメラ

# 焦点距離調整パラメータ
#  値はお好きにどうぞ
N    = 0.08        # 最近焦点距離(m)
M    = 0.25        # 中間焦点距離(m)
F    = 1.0         # 最遠焦点距離(m)
IN   = 1.0 / N
IM   = 1.0 / M
IF   = 1.0 / F

# スライダーの値を焦点距離に変換
def convSliderToFocusLen( t ):
   a = ( IM - IF ) / ( 2.0 * IM - IN - IF )
   b = a * ( 1.0 - 2.0 * a ) * ( IM - IN )
   c = IN + b / a
   ID = b / ( t - a ) + c   # 1/Dなので注意
   return 1.0 / ID


# 焦点距離を設定距離に変換
def convFocusLenToSetLen( focusLen ) :
   Px = 0.2   # 基準となる焦点距離Px
   Py = 0.12  # 基準となる焦点距離Py
   Qx = 1.0   # 基準となる焦点距離Qx
   Qy = 0.16  # 基準となる設定距離Qy
   bP = ( Py - N ) / ( Px - N )
   bQ = ( Qy - N ) / ( Qx - N )
   a = ( bQ * Qx - bP * Px ) / ( bQ - bP )
   b = ( Px - a ) * ( N - a ) * ( Py - N ) / ( N - Px )
   c = N - b / ( N - a )
   return b / ( focusLen - a ) + c


# マニュアルフォーカス距離をカメラに設定
def setFocusLen( focusLen ):
   # 焦点距離を設定距離に変換
   setLen = convFocusLenToSetLen( focusLen )

   # カメラに設定距離を指定
   pc2.set_controls( {"LensPosition" : 1.0 / setLen} )

   # 焦点距離表示
   label["text"] = "{:.4f}m".format( focusLen )
   

# スライダー変更のコールバック
def onChangeSlider( event ):
   focusLen = convSliderToFocusLen( slider.get() )
   setFocusLen( focusLen )
      

# メインウィンドウ
root = tkinter.Tk()
root.title( "Camera manual focus test" )

# 焦点距離調整スライダーを追加
sliderLen = 250
sliderWidth = 15
slider = tkinter.Scale(
   root,
   orient = tkinter.HORIZONTAL,    # 水平スライダー
   from_ = 0.0, to = 1.0,          # 0〜1の間
   resolution = 0.01,              # 0.01ステップ幅
   length = sliderLen,             # スライダーの長さ
   width = sliderWidth,            # スライダーの幅
   showvalue = 0,                  # 値を非表示
   command = onChangeSlider        # 値が変わった時に呼ばれる関数を登録
)

initFocusPos = 0.5
slider.set( initFocusPos );        # 初期値
slider.place( x = 30, y = 20 )

# 焦点距離表示ラベル
label = tkinter.Label( root )
label.place( x = 34 + sliderLen, y = 5 + sliderWidth )

# カメラ初期化
# プレビューを表示しオートフォーカスモードをManualに設定
pc2.start_preview( True )
pc2.start()
pc2.set_controls( {"AfMode" : controls.AfModeEnum.Manual} )

# ループスタート
root.mainloop()

 実際に動かしてみると…

お~~~、割といい感じに距離感が合ってます!

終わりに

 今回はラズパイでPicamera2を通したカメラのマニュアルフォーカスをGUI上で行うサンプルを作ってみました。設定自体はPicamera2.set_controls()のLensPositionキーにディオプトリ(1/D)を与えるだけのはずなんですが、実際はカメラモジュールの特性やスペックなどで反応が全然違い、設定値が理屈通りにならないため、それを補正する関数を作るなどが必要で中々に大変でした。でも結果としては上の動画のように指定の距離にほぼピントが合うようにキャリブレーションできました。

 さてそうなりますと、次は映像を保存したくなりますね。という事で次回からは「保存」についてピックアップしていきたいと思います。

ではまた(^-^)/

<次回>


この記事が気に入ったらサポートをしてみませんか?