12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

picameraのsensor_modeと画角について

Posted at

sensor_modeについて

ラズパイのカメラモジュールを使っていると,特定の解像度で撮影すると何故か画角が狭くなったり,画質が変化したり,といった現象に遭遇することがあります.これは,sensor_modeの違いによって起きる現象です.

sensor_modeはイメージセンサの使い方を指定するパラメータです.例えば,センサーの一部の画素のみを使用したり,4画素を1画素として扱ったり,というようなものです.各モードの動作はカメラモジュールごとに対応表として用意されています.

対応表

ここでは表の見方の例として,v2 moduleの表を引用します.

v2_table.png

  • 左端の#(sensor_mode)
    センサーモードです.v2 moduleは1~7まで指定できます.2と3は全く同じですが,これはv1 moduleとの互換性を考慮した結果だそうです.

  • Resolution
    撮影される画像の解像度です.ここに書かれている値以外を指定すると,リサイズが発生します.

  • Aspect Ratio
    画像の縦横比です.

  • Framerates
    設定可能なフレームレートです.

  • Video1
    ビデオポートを使った際に設定できるセンサーモードを表しています(表のx印は✓の意味です2).1~7まで対応しています.ビデオポートは動画撮影時,または静止画撮影時にuse_video_port=Trueとすることで使用できます.スチルポートより高速ですが,ざらついた画像になる傾向があるそうです.

  • Image3
    スチル(静止画)ポートを使った際に設定できるセンサーモードです.スチルポートでは最大解像度で撮影され,Resolutionを指定した際はそのサイズにリサイズされます.強力なノイズ除去が使用できますが,遅くなります.

  • FoV (Field of View)
    センサーのどの領域を使用するかです.Fullならセンサーの全領域,Partialなら一部を使用します.よってPartialとなっているものは画角(FoV)が狭くなります.

  • Binning
    ピクセルビニングを行うかどうかです.Noneはピクセル等倍(ドットバイドット),2x2は4画素を1画素として扱います.ピクセルビニングは解像度を落とすことに加え,ノイズの低減効果もあります.

以下にsensor_modeと撮影範囲についての図を引用します.上の表と下の図より,センサー全体を使って最大画角で撮影するにはsensor_mode=2 or 3 or 4を使えばよいことが分かります.また,それ以外のsensor_modeまたは解像度(例えば1920x1080)に設定すると,画角が狭くなることもわかります.

後述しますが,sensor_modeを指定しなければResolutionとFramerateによって自動的にsensor_modeが選択されます.

v2_fov.png

ここに掲載した表と画像は以下のpicameraドキュメントから引用しています.
https://picamera.readthedocs.io/en/release-1.13/fov.html

ピクセルビニング

binning.png

2x2のビニングの流れです.
①:センサーの色配列(ベイヤー配列)
②:色ごとに4画素の平均を取る(g1, g2は区別する)
③:平均を取った後

このように画素平均を取るため,ノイズ低減効果があります.また,画像処理(デモザイク,ホワイトバランス調整など)を行う前に画素数を減らすことができるので撮影を高速にする効果もあります.

センサーに関して詳しく知りたい方はデータシートを探してください.IMX219 (v2 module) はデータシートのp.53にビニングのことが書いてあります.
IMX219 (v2 module)のデータシート:https://www.raspberrypi.org/forums/viewtopic.php?t=177308

各モジュールの表

picameraは現在(2020/10),2年以上更新されていません.そのためか,Raspberry Pi OS (raspbian) のサイトと表記が異なる部分があります.よって,ここではpicameraのドキュメントの表とRaspberry Pi OSの表の2つを引用します.

引用元:
picamera document:https://picamera.readthedocs.io/en/release-1.13/fov.html
Raspberry Pi OS document: https://www.raspberrypi.org/documentation/raspbian/applications/camera.md

v1 (OV5647)

v1_table.png
picamera document

v1_table(app).png
Raspberry Pi OS document

binningの部分を除いてほぼ同じですね.4x4binningと2x2+skipでは意味が異なるように思うのですが,どうなのでしょうか.

v2 (IMX219)

v2_table.png
picamera document

v2_table(app).png
Raspberry Pi OS document

注目すべきはsensor_mode=7の項目です.picameraでは最大90fpsですが,Pi OS documentの方では200fpsとなっています.200fpsは,

1For frame rates over 120fps, it is necessary to turn off automatic exposure and gain control using -ex off. Doing so should achieve the higher frame rates, but exposure time and gains will need to be set to fixed values supplied by the user.

と書いてあるように,自動露出とゲインコントロールをオフにする必要がありますが,そうでなくとも120fpsは対応しているみたいです.

しかし,picameraドキュメントの方には

The maximum framerate of the camera depends on several factors. With overclocking, 120fps has been achieved on a V2 module but 90fps is the maximum supported framerate.

と,オーバークロックなどの要因で120fps出ることはあるが,サポートされるフレームレートの最大は90fps,と書いてあります.

実際にpicameraを使ってsensor_mode=7, Resolution=(320,240), framerate=120の設定を試してみると,120fps出ることが確認できました.ちなみに,同じsensor_modeでもResolutionを小さくした方が,GPUによるリサイズと転送量の減少により速くなります.

HQ (IMX477)

hq_table(app).png
Raspberry Pi OS document

HQのデータはpicameraのドキュメントには無いので,Raspberry Pi OSのドキュメントを参照する必要があります.

検証

実際にsensor_mode,Resolution,use_video_portを変更して静止画撮影を行いました.
少し長いので,スキップしたい方はこちらをクリックしてください.

## 環境

  • Raspberry Pi 4 (4GB)
  • Camera module V2
  • Python 3.7.3
  • picamera 1.13

撮影には以下のようなテストチャートをA4用紙に印刷して使用しました.背景の色は,上に示した各sensor_modeの撮影範囲に対応しています.また,画角の変化をわかりやすくするためにいらすとやのイラストを使用しています.

chart.png

撮影

sensor_mode, Resolution, use_video_portを総当り

sensor_mode, Resolution, use_video_portを総当りして撮影した画像を以下に示します.Resolutionはsensor_modeの表に登場する解像度です.掲載画像はサムネイルのスクリーンショットです.

横方向(→)はsensor_mode(1~7)の違い,縦方向(↓)はResolutionとuse_video_portの有無です.画像下に表示されているファイル名は,

{Resolution}_{use_video_port}_{sensor_mode}.jpg

となっています.

→ sensor_mode (1~7)
↓ Resolution, use_video_port(False,True交互)
rsvすべて変化(連結).png

全体を見た感じ,sensor_modeが同じときはほぼ同じ範囲が写っています.このことから,指定したsensor_modeの画角で撮影されたあと,Resolutionのサイズにクロップ,リサイズされていることがわかります.

use_video_portの違いについては,全く変化がありません.sensor_modeを指定すればuse_video_portの値は無視されるのでしょうか.use_video_portの違いによるノイズ量の違いについては,撮影時の設定が適当なので比較できないとは思いますが,見た感じは同じでした.

sensor_modeの違いを見てみると,撮影範囲に差があることがわかります.これがsensor_modeによる画角の差になります.下に解像度とsensor_modeを一致させた時の画像を掲載します.画像から,テストチャートの背景色と撮影範囲が合致していることがわかります.

(1920, 1080)_False_1.jpg
(1920,1080)_False_1.jpg (sensor_mode=1)

(3280, 2464)_False_2.jpg
(3280, 2464)_False_2.jpg (sensor_mode=2)

(3280, 2464)_False_3.jpg
(3280, 2464)_False_3.jpg (sensor_mode=3)

(1640, 1232)_False_4.jpg
(1640, 1232)_False_4.jpg (sensor_mode=4)

(1640, 922)_False_5.jpg
(1640, 922)_False_5.jpg (sensor_mode=5)

(1280, 720)_False_6.jpg
(1280, 720)_False_6.jpg (sensor_mode=6)

(640, 480)_False_7.jpg
(640, 480)_False_7.jpg (sensor_mode=7)

sensor_modeを指定しなかった場合

次に,sensor_modeを指定せずにResolutionとsensor_modeを変化させた結果を以下に示します.ファイル名は先程と同じで,

{Resolution}_{use_video_port}_{sensor_mode}.jpg

となっています(sensor_modeは指定していないので0です).横方向(→)がuse_video_portの有無,縦方向(↓)がResolutionの違いです.

rvのみ変化.png

use_video_port=Falseのとき(左列)の画像は "AIの本のイラスト" が写っていることから,すべて最大撮影範囲で撮影されていることがわかります.

次に,use_video_port=Trueのとき(右列)を見てみると,(1920,1080)以外は左と同じ画像になっています.しかし,感覚的にはuse_video_port=Trueとした場合,最もResolutionの近いsensor_modeに設定される(例えば,(640, 480)は画角が狭くなる)ほうが自然です.

この挙動はpicameraのsensor_modeの決定方法によるもので,picameraでは,Resolutionとframerateを考慮してsensor_modeを決定する仕組みになっているそうです.
(参考)https://picamera.readthedocs.io/en/release-1.13/fov.html#sensor-modes

上の結果はframerate=30(default)の結果でした.次に,framerate=60とした結果を掲載します.

rvのみ変化(60fps).png

framerate=60の結果からわかるように,use_video_port=Trueのときの撮影範囲が変化しました.先程,黄色の領域しか写っていなかった(1920, 1080)は青色の範囲まで写るようになっています.これは,sensor_mode=1が60fpsに対応していないため,sensor_mode=6に変更されたことを表しています.

他の画像についても青色の範囲まで写っているので,sensor_mode=6で撮影されたことがわかります.また,(640, 480)はResolutionも考慮されてsensor_mode=7となっていることがわかります.

use_video_portを指定しなかった場合

最後に,use_video_portを指定せずに,Resolution, sensor_modeを変更した結果です.

rsのみ変化(30fps).png
framerate=30のとき

rsのみ変化(60fps).png
framerate=60のとき

結果より,1つ目の検証と変わりないことがわかります.framerateを変更しても変わらないので,設定の上下関係は sensor_mode > framerate となっているようです.

binningの影響

1640x922, use_video_port=Falseの画像を用いて,sensor_mode=4(2x2 binning)とsensor_mode=2(binning無し最大解像度をリサイズしたもの)を比較しました.左がsensor_mode=4,右がsensor_mode=2です.掲載画像は2枚の画像をビューワーソフトに表示して並べたもののスクリーンショットです.

1640x922_False_(4vs2).png
左:sensor_mode=4 (binning), 右:sensor_mode=2

1640x922_False_(4vs2)-2.png
左:sensor_mode=4 (binning), 右:sensor_mode=2

binning有りのほうは無しと比べて解像度は低く感じます.おそらく,binning後にデモザイク処理が行われるために解像度が落ちていると思われます.解像度優先で撮影するなら,後でリサイズする場合でもbinning無しの最大解像度で撮影したほうが良さそうです.

今回は検証していませんが,暗い環境で撮影する場合はbinningのノイズ低減効果が表れるかもしれません.また,今回は露出やホワイトバランスに関するパラメータを設定していないので,ノイズ感については上の画像で比較しないほうが良いかもしれません.

一応,この二枚の撮影時に書き出した設定を書いておきます.シャッタースピードが若干違うのと,ホワイトバランスの値が異なりますが,解像感の比較については影響ないと思います.

parameters sensor_mode=4の画像 sensor_mode=2の画像
resolution 1640x922 1640x922
exposure_speed 25605 (us) 25832 (us)
sensor_mode 4 2
analog_gain 1 1
digital_gain 1 1
framerate 30 30
awb_gains (Fraction(205, 128), Fraction(235, 128)) (Fraction(205, 128), Fraction(471, 256))

画像取得までの流れ

以上の結果からわかる,撮影してから画像が得られるまでに行われる,画像サイズに関わる処理の流れを3種類の例をもとに示します.(この項に示す処理には私の予想も含まれるので,正確性は保証できません.)

1 .sensor_mode=1, resolution=(3280, 2464)としたとき

表:v2 moduleのsensor_mode対応表(一部)

sensor_mode Resolution FoV Binning
1 1920x1080 Partial None

img1.png

上の表は先程掲載したv2 moduleの表の一部です.sensor_mode=1は1920x1080のモードです.撮影すると,FoVがPartialなのでセンサー(オレンジ部)の一部を使って撮影されます.

その後,指定したResolution(ここでは3280x2464)のアスペクト比(4:3)に切り取り(crop)され,リサイズされます.

※クロップとリサイズがこのような順序で行われているかは不明です.
※正確な4:3は3280x2460なので,クロップ後のサイズは若干異なるかもしれません.短辺の4画素がどういう扱いになるのかも不明です.

2.sensor_mode=2, Resolution=(800, 600)としたとき

表:v2 moduleのsensor_mode対応表(一部)

sensor_mode Resolution FoV Binning
2 3280x2464 Full None

img2.png

これはsensor_modeを指定せずに静止画ポートを使った場合と同じ処理です.静止画ポートでは最高画質が使われるので,たとえ800x600でもこのような処理が発生するので遅いです.

3.sensor_mode=7, Resolution=(800, 600)としたとき

表:v2 moduleのsensor_mode対応表(一部)

sensor_mode Resolution FoV Binning
7 640x480 Partial 2x2

img3.png

sensor_mode=7ではピクセルビニングが行われます.よって,センサーのうち1280x960の範囲が使用され,ビニングによって640x480となります.その後,指定された解像度にリサイズされます.

sensor_modeを指定しなかった場合

A resolution of 800x600 and a framerate of 60fps will select the 640x480 60fps mode, even though it requires upscaling because the algorithm considers the framerate to take precedence in this case.

引用:https://picamera.readthedocs.io/en/release-1.13/fov.html#sensor-modes

picameraのドキュメントによると,設定したフレームレートによっては,リサイズは必ずしも縮小になるとは限らないそうです.

今回の検証では,framerateの制限によるアップスケーリングの発生を確認しました.アップスケーリングを避け,高画質で撮影したい場合は手動でsensor_modeを設定すべきです.

ソースコード

今回の検証に使用したソースコードです.このコードでうまくsensor_modeの違いが表れたので問題ないとは思います.気になる点としては,

  • sensor_mode変更のたびにPiCameraインスタンスを作成する必要があるのか
  • sensor_modeはコンストラクタで指定したほうが良いのか4

といったところがありますが,確認はしていません.また,撮影前のsleepが1秒ですが,実際に何か撮影を行う場合は露出やホワイトバランスなども関係するので,2秒程度取ったほうが良いでしょう.今回は画角の検証がメインなので適当です.

write_config()はカメラの設定値(シャッタースピードなど)を書き出す関数です.sensor_modeの検証とは直接関係有りません.

import picamera
import numpy as np
import cv2
from PIL import Image

import io
import os, sys
import datetime
import time

def write_config(f, camera):
    """
    カメラの書き出せそうな設定値をすべて書き出す関数
    """
    f.writelines(f"timestamp*={camera.timestamp}\n")
    f.writelines(f"revision={camera.revision}\n")
    f.writelines(f"resolution={camera.resolution}\n")
    f.writelines(f"-"*30)
    f.writelines("\n")
    shutter_speed = camera.shutter_speed
    exposure_speed = camera.exposure_speed
    if(shutter_speed!=0):
        f.writelines("shutter_speed={0} (1/{1:.2f}s)\n".format(shutter_speed, 1/(shutter_speed/1000000)))
    else:
        f.writelines("shutter_speed={0}\n".format(shutter_speed))
    f.writelines("exposure_speed*={0} (1/{1:.2f}s)\n".format(exposure_speed, 1/(exposure_speed/1000000)))
    f.writelines(f"exposure_mode={camera.exposure_mode}\n")
    f.writelines(f"exposure_compensation={camera.exposure_compensation}\n")
    f.writelines(f"iso={camera.iso}\n")
    f.writelines(f"sensor_mode={camera.sensor_mode}\n")
    f.writelines(f"analog_gain*={camera.analog_gain}\n")
    f.writelines(f"digital_gain*={camera.digital_gain}\n")
    f.writelines(f"framerate={camera.framerate}\n")
    f.writelines(f"framerate_delta={camera.framerate_delta}\n")
    f.writelines(f"framerate_range={camera.framerate_range}\n")
    f.writelines(f"meter_mode={camera.meter_mode}\n")
    f.writelines(f"drc_strength={camera.drc_strength}\n")
    f.writelines(f"raw_format={camera.raw_format}\n")
    f.writelines("-"*30)
    f.writelines("\n")

    f.writelines(f"image_denoise={camera.image_denoise}\n")
    f.writelines(f"video_denoise={camera.video_denoise}\n")
    f.writelines(f"video_stabilization={camera.video_stabilization}\n")
    f.writelines("-"*30)
    f.writelines("\n")

    f.writelines(f"awb_gains=    {camera.awb_gains}\n")
    f.writelines(f"awb_mode=     {camera.awb_mode}\n")
    f.writelines(f"brightness=   {camera.brightness}\n")
    f.writelines(f"saturation=   {camera.saturation}\n")
    f.writelines(f"contrast=     {camera.contrast}\n")
    f.writelines(f"sharpness=    {camera.sharpness}\n")
    f.writelines(f"flash_mode=   {camera.flash_mode}\n")
    f.writelines(f"rotation=     {camera.rotation}\n")
    f.writelines(f"hflip=        {camera.hflip}\n")
    f.writelines(f"vflip=        {camera.vflip}\n")
    f.writelines(f"zoom=         {camera.zoom}\n")
    f.writelines("-"*30)
    f.writelines("\n")

    f.writelines(f"color_effects={camera.color_effects}\n")
    f.writelines(f"image_effect={camera.image_effect}\n")
    f.writelines(f"image_effect_params={camera.image_effect_params}\n")
    f.writelines(f"still_stats={camera.still_stats}\n")

# ----------- 検証に用いるパラメータ設定 --------------
RESOLUTION_LIST = ((3280, 2464),   # full sensor area #2, #3
                    (1640, 1232),  # full sensor area(binned) #4
                    (1640, 922),   # #5
                    (1280,720),    # #6
                    (1920, 1080),  # #1
                    (640, 480))    # #7
FRAMERATE = (30, 60)
USE_VIDEO_PORT = (True, False)                    
SENSOR_MODES = (1,2,3,4,5,6,7)

nowtime = datetime.datetime.now()
outputdir = nowtime.strftime("%Y%m%d-%H%M%S")
os.mkdir(outputdir)

TEST1 = True
TEST2 = True
TEST3 = True
# -----------------------------------------------------

"""
test1
Resolution, use_video_port, sensor_modeの組み合わせを総当りするテスト
"""
if(TEST1):
    foldername = os.path.join(outputdir, "test1")
    os.mkdir(foldername)

    for resolution in RESOLUTION_LIST:
        for use_vp in USE_VIDEO_PORT:
            for sensor_mode in SENSOR_MODES:
                with picamera.PiCamera() as camera:
                    camera.resolution = resolution
                    camera.sensor_mode = sensor_mode
                    camera.start_preview()
                    time.sleep(1)

                    stream = io.BytesIO()
                    camera.capture(stream, format="jpeg", use_video_port=use_vp, quality=95)
                    camera.stop_preview()
                    stream.seek(0)
                    img = Image.open(stream)
                    img = np.array(img, dtype=np.uint8)
                    img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)

                    filename = "{0}_{1}_{2}.jpg".format(str(resolution), str(use_vp), str(sensor_mode))
                    cv2.imwrite(os.path.join(foldername, filename), img)
                    #cv2.imshow("capture", img)
                    print(filename)
                    print(img.shape)
                    print("")
                    with open(os.path.join(foldername, filename[:-4]+".txt"), "w") as f:
                        write_config(f, camera)

"""
test2 
sensor_modeを指定しなかったときのテスト
"""
if(TEST2):
    for framerate in FRAMERATE:
        foldername = os.path.join(outputdir, "test2_{0}fps".format(framerate))
        os.mkdir(foldername)

        for resolution in RESOLUTION_LIST:
            for use_vp in USE_VIDEO_PORT:
                with picamera.PiCamera() as camera:
                    camera.resolution = resolution
                    camera.framerate = framerate
                    # camera.sensor_mode = sensor_mode  # sensor_modeを指定しない
                    camera.start_preview()
                    time.sleep(1)

                    stream = io.BytesIO()
                    camera.capture(stream, format="jpeg", use_video_port=use_vp, quality=95)
                    camera.stop_preview()
                    stream.seek(0)
                    img = Image.open(stream)
                    img = np.array(img, dtype=np.uint8)
                    img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)

                    filename = "{0}_{1}_{2}.jpg".format(str(resolution), str(use_vp), str(camera.sensor_mode))
                    cv2.imwrite(os.path.join(foldername, filename), img)
                    #cv2.imshow("capture", img)
                    print(filename)
                    print(img.shape)
                    print("")
                    with open(os.path.join(foldername, filename[:-4]+".txt"), "w") as f:
                        write_config(f, camera)   

"""
test3
use_video_portを指定しなかったときのテスト
"""
if(TEST3):
    for framerate in FRAMERATE:
        foldername = os.path.join(outputdir, "test3_{0}fps".format(framerate))
        os.mkdir(foldername)

        for resolution in RESOLUTION_LIST:
            for sensor_mode in SENSOR_MODES:
                with picamera.PiCamera() as camera:
                    camera.resolution = resolution
                    camera.sensor_mode = sensor_mode
                    camera.framerate = framerate
                    camera.start_preview()
                    time.sleep(1)

                    stream = io.BytesIO()
                    camera.capture(stream, format="jpeg", quality=95)  # use_video_portを指定しない
                    camera.stop_preview()
                    stream.seek(0)
                    img = Image.open(stream)
                    img = np.array(img, dtype=np.uint8)
                    img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)

                    filename = "{0}_{1}_{2}.jpg".format(str(resolution), "None", str(sensor_mode))
                    cv2.imwrite(os.path.join(foldername, filename), img)
                    #cv2.imshow("capture", img)
                    print(filename)
                    print(img.shape)
                    print("")
                    with open(os.path.join(foldername, filename[:-4]+".txt"), "w") as f:
                        write_config(f, camera)

おわりに

sensor_modeを意識しなくても撮影は可能ですが,自分に必要な性能(高画質,画角,速度)は何かを考えて設定すべきだということがわかりました.個人的にはピクセルビニングによる解像感の低下が思ったよりも大きいことに驚きました.

この記事の大部分はpicameraのドキュメントから引用しています.picameraドキュメントにはライブラリの使い方に限らず,カメラモジュールの仕組みや撮影の流れなどが書かれており,非常に参考になるので一読することをおすすめします.

参考

https://picamera.readthedocs.io/en/release-1.13/index.html (picaemraドキュメント)
https://www.raspberrypi.org/documentation/raspbian/applications/camera.md (Raspberry Pi camera applicationのドキュメント)

  1. https://picamera.readthedocs.io/en/release-1.13/fov.html#the-video-port

  2. 海外ではx印はチェックの意味で使われます.https://www.sociomedia.co.jp/7304

  3. https://picamera.readthedocs.io/en/release-1.13/fov.html#the-still-port

  4. https://picamera.readthedocs.io/en/release-1.13/api_camera.html#picamera.PiCamera.sensor_mode

12
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?