はじめに
前の記事: Jetsonのカメラドライバを書く[I2C編]
次の記事: Jetsonのカメラドライバを書く[Argus撮影編]
前の記事ではカーネルモジュールが読み込まれるまでをやりました。
制御用のI2C通信が出来たのでカメラを制御をして絵をとります。
RAW画像を得る
USBカメラなら繋いだらすぐに映像が得られますし、スマホとかは当たり前にカメラがついて最近は画質も良いです。
Jetsonもコマンドが流せてるし、後は簡単に...とは行きません、本当に大変なのはここからです。
IMX708が出力するのはよく見るYUVでもRGBでもMJPEGでもない、Bayerです。
SSSの技術サイトquad-bayer-codingを引用します。
CVによく使うRGB画像は1画素にRGBの3色の情報を持ち、ディスプレイでは加色法によって任意の色を作ります。イメージセンサーに1画素で3色を分離して撮像する技術は存在しないため、RGB特定の色のみを通すフィルターをつかい1画素=特定色の光の強さをデータにします。
これがBayer形式、あるいはRAW画像などと呼ばれ、1画素=RGBの画像にするには隣接する他の色を適切に混色が必要です。このDebayerのプロセスはたいてい専用のHWが載っていることが多くISP(image signal processor)と呼ばれます。実際にISPを使う場合はMIPI-CSI2のデータに加えて、ISP変換のためのメモリやら処理のスループットも気にしなければならず、原因の特定が困難になります。
まずはRAW画像を得ることから始めましょう。
駆動モードを決める
イメージセンサーの制御をする上ではじめに考える必要があるので駆動モードの設定です。
イメージセンサーは数メガピクセルの画素を持っていますが、それを保持するメモリを持っていないことがほとんどです。写真撮影のように一枚撮って読み出すというものではなくストリームで、撮れたものから即座にMIPIを通じてシステム側に転送します。この転送するストリームを作るのに大きく影響するパラメータはVSYNCとHSYNCといった画素読み出しの周期に関わるもの、画像の大きさや転送レートといった受け側に対応するリソースを用意しなければならないものがあります。これらストリームを開始したら変更できない要素を基準にモードを決めます。
RaspberryPiのimx708.c
を見ると4つのモードが定義されています。
Mode | resolution | description |
---|---|---|
0 | 4608x2592 | Full Resolution |
1 | 2304x1296 | 2x2binning |
2 | 1536x864 | 2x2binning and cropped for 720p |
3 | 2304x1296 | 2x2binning with HDR |
まずは撮影モードを1つに決めてそれを実装します。
今回はmode0の全画素読み出しのものにします。
static const struct imx708_mode supported_modes_10bit_no_hdr[] = {
{
/* Full resolution. */
.width = 4608,
.height = 2592,
.line_length_pix = 0x3d20,
.crop = {
.left = IMX708_PIXEL_ARRAY_LEFT,
.top = IMX708_PIXEL_ARRAY_TOP,
.width = 4608,
.height = 2592,
},
.vblank_min = 58,
.vblank_default = 58,
.reg_list = {
.num_of_regs = ARRAY_SIZE(mode_4608x2592_regs),
.regs = mode_4608x2592_regs,
},
.pixel_rate = 595200000,
.exposure_lines_min = 8,
.exposure_lines_step = 1,
.hdr = false
},
受け側がメモリを確保する必要があるためか、設定はV4L2が知る必要があるためカーネルモジュールにも詳しく定義しています。
これをもとにJetsonに必要な情報を整理します。
item | value |
---|---|
width | 4608 |
height | 2592 |
HSYNC(line_length) | 0x3d20 -> 15648 |
VSYNC | 2592 + 58 = 2650 |
pixel_rate(pix/s) | 595200000 |
fps | 595200000 / (15648 * 2650) = 14.35 |
おさらいで再びMACNICA社のひろみんさんの記事、【徹底解説】MIPI なんて難しくない!新人がゼロから学ぶ MIPI 規格 基礎編からビデオフレーム図を引用します。
今回のモードを雰囲気でスケール合わせると以下のような感じです。line-blankingの感覚が長く、frame-blankingの間隔が短いですね。画素のデータ自体は7.7usで読めますが、転送には20us程度はかかってしまうので十分なline-blankingがなければならないため、この設定になっていると考えられます。
これで流し撮りなんかしたらすごく歪むでしょう。動きモノで使うのは向かないですね。
一方MIPIの伝送レートが低くても良くなるため、長いFFCを使っても撮影できる可能性は高いです。
モードを作る
駆動モードが決まったのでJetsonに移植をします。
先の通りJetsonの場合、駆動モード設定の多くはデバイスツリーに書き、レジスタ設定はカーネルモジュールに書きます。
なので作業内容は以下になります。
- デバイスツリーのmode0の設定をRaspberryPiのものに合わせる
- カーネルモジュールの画面設定とモード設定をデバイスツリーに合わせる
- カーネルモジュールのレジスタ設定をコピーする
デバイスツリーのmode0の設定をRaspberryPiのものに合わせる
まずはtegra234-camera-rbpcv3-imx708.dtsi
を開きます。
デバイスツリーを読むと大体以下のようになっているのがわかります。
- tegra-capture-vi
- ...
- host1x@13e00000
- ...
- cam_i2cmux
- i2c@0
- rbpcv3_imx708_a@1a
- mode0
- mode1
- ports
- i2c@1
- rbpcv3_imx708_c@1a
- mode0
- mode1
- ports
今回変更が必要なモード設定の記述があるのはのはcam_i2cmux
の下のノードです。
まずi2c@0
とi2c@1
で別れており、だいたい同じ内容が書かれています。基本的には片方で書き換えたら、もう一方にもコピーするのが良いです。今回はCAM1なのでi2c@1
で書き換えをします。
mode0
とmode1
がありますが、mode1
は今は使わないので削除しましょう。
mode0の書き換え
デバイスツリーは先程整理した、ストリーム中に変更できない重要なパラメータの指定の他、露出に関する変更可能な値についてはレンジを指定します。
スペースで区切られて3段(+1行)に分かれていると思いますが、これは大雑把に以下のような項目となってます
- センサー及びCAMポート固有の項目。基本的には変更しなくていい
- モード固有の固定値。モードごとに適切な値にしなければならない
- ストリーミング中に変更可能な項目。レンジを指定する。RAW撮影のときは影響しない
画面の大きさやラインの長さは読み取れた設定どおりの設定で良いです。
mclk_multiplier
は非推奨になっているので削除します。
pix_clk_hz
はそのままにしてください。
変更可能な露出関係の設定の内、フレームレートだけは判明しているので設定しておきます。
最後におまじないとしてembedded_metadata_height = "4"
を設定しておきます。
変更すると以下のようになります。
mode0 { /* imx708_MODE_4608x2592 */
mclk_khz = "24000";
num_lanes = "2";
tegra_sinterface = "serial_c";
phy_mode = "DPHY";
discontinuous_clk = "no";
dpcm_enable = "false";
cil_settletime = "0";
lane_polarity = "0";
active_w = "4608";
active_h = "2592";
mode_type = "bayer";
pixel_phase = "rggb";
csi_pixel_bit_depth = "10";
readout_orientation = "0";
line_length = "15648";
inherent_gain = "1";
pix_clk_hz = "300000000";
gain_factor = "16";
framerate_factor = "1000000";
exposure_factor = "1000000";
min_gain_val = "16"; /* 1.00x */
max_gain_val = "356"; /* 22x */
step_gain_val = "1";
default_gain = "16"; /* 1.00x */
min_hdr_ratio = "1";
max_hdr_ratio = "1";
min_framerate = "14350000"; /* 14.35 fps */
max_framerate = "14350000";
step_framerate = "1";
default_framerate = "14350000";
min_exp_time = "13"; /* us */
max_exp_time = "683709"; /* us */
step_exp_time = "1";
default_exp_time = "2495"; /* us */
embedded_metadata_height = "4";
};
RAWを撮影するのに重要なのはストリーミング中に変更不可能な値、すなわちスペースで分けられた2の段が主なので、レンジ設定の項目はまだ適当で大丈夫です。
設定変更できたらこの2,3段の設定をCAM0側にもコピーしてCAM1と同じにします。
ドライバの画面設定とモード設定をデバイスツリーに合わせる
modeを1つにしてモード固有値を設定しました。
カーネルモジュールにもこのモードに対応する設定がimx708_mode_tbls.h
のファイルがあり、enumと配列を使って対応関係を作っています。
これをデバイスツリーに合わせます。
- enumのモードに関する設定を1つだけ残し、適切な名前
IMX708_MODE_4608x2592_14FPS
にする -
mode_table
も不要なenumに該当する行を消す - フレームレートを14とする
-
imx708_frmfmt
に画像の大きさ、とモードenumをmode0に該当するものにする
すると以下のようになると思います。
不要なレジスタ設定定義は消しましょう。
基本的にはimx708_frmfmt
の配列がデバイスツリーのmodeXと対応関係になります。
変更結果を抜粋すると以下のようになっているはずです。
enum {
IMX708_MODE_4608x2592_14FPS,
IMX708_MODE_COMMON,
IMX708_START_STREAM,
IMX708_STOP_STREAM,
};
static const imx708_reg *mode_table[] = {
[IMX708_MODE_4608x2592_14FPS] = imx708_mode_4608x2592_14fps,
[IMX708_MODE_COMMON] = imx708_mode_common,
[IMX708_START_STREAM] = imx708_start,
[IMX708_STOP_STREAM] = imx708_stop,
};
static const int imx708_14_fr[] = {
14,
};
static const struct camera_common_frmfmt imx708_frmfmt[] = {
{{4608, 2592}, imx708_14_fr, 1, 0, IMX708_MODE_4608x2592_14FPS},
};
#endif /* __IMX708_I2C_TABLES__ */
カーネルモジュールのレジスタ設定書き換え
モード固有値をデバイスツリーに設定してきました、この設定値はイメージセンサーでどのように設定するのでしょうか?
これらはすべてI2Cと通してレジスタに書き込むことでセンサーに反映します。
イメージセンサーは電源投入後は基本Standbyモード
で、ピクセルのスキャン動作などは行っておらず設定を待っている状態です。
使うときにはまず、上のモード固有のような値を事前に該当するレジスタに適切に書き込みます。
レジスタ設定が出来たらStanbyモード
から、データを読み出し続けるStreamingモード
に切り替えることでセンサーから画像のデータが得られます。
まずは設定値群であるmode_common
とmode0
のレジスタ設定をコピーしてください。(コピペなのでここに差分は貼りません)
Jetsonはレジスタリストの末尾にセンチネルIMX708_TABLE_END
を置いているので、間違えて消さないように注意しましょう。
次に開始と終了について、IMX477と共通で0x0100
を0x01
にすることでStreamingモード
になるようです。
変えなくても問題ないですが、わかりやすさの点では定数にしたほうがいいかもしれません。
#define IMX708_REG_MODE_SELECT 0x0100
#define IMX708_MODE_STANDBY 0x00
#define IMX708_MODE_STREAMING 0x01
#define imx708_reg struct reg_8
static const imx708_reg imx708_start[] = {
{IMX708_REG_MODE_SELECT, IMX708_MODE_STREAMING},
{IMX708_TABLE_WAIT_MS, IMX708_WAIT_MS*3},
{IMX708_TABLE_END, 0x00}
};
static const imx708_reg imx708_stop[] = {
{IMX708_REG_MODE_SELECT, IMX708_MODE_STANDBY},
{IMX708_TABLE_END, 0x00}
};
これでデバイスツリーとカーネルモジュールが出来ました。
ビルドしてorin-nanoに持ち込み、デバイスツリーを上書きして再起動しましょう。
動作させてみる
再起動したところで再びmake insmod
します。
問題なく設定できていれば、設定したとおりの解像度とfpsになっているはずです。
v4l2-ctl
を使って期待通りになっているか確認しましょう。
以下のようにimx708 10-001a
が見つかり解像度が4608x2592
ならばOKです。
jetson@orin-nano:~/imx708$ v4l2-ctl --list-devices
NVIDIA Tegra Video Input Device (platform:tegra-camrtc-ca):
/dev/media0
vi-output, imx708 10-001a (platform:tegra-capture-vi:2):
/dev/video0
jetson@orin-nano:~/imx708$ v4l2-ctl --list-formats-ext
ioctl: VIDIOC_ENUM_FMT
Type: Video Capture
[0]: 'RG10' (10-bit Bayer RGRG/GBGB)
Size: Discrete 4608x2592
Interval: Discrete 0.071s (14.000 fps)
早速RAW画像を取得しましょう
jetson@orin-nano:~/imx708$ make check0
v4l2-ctl -d /dev/video0 --set-ctrl bypass_mode=0 --stream-mmap --stream-count=1 --stream-to=test.raw
<
<
がでて、エラーが発生したり処理が終わらないということがなければ成功です
test.raw
というバイナリファイルが出来て、おそらく撮像できているというのがわかります。
うまく動かない場合はdmest
でerr_codeにヒントがあります。
NVIDIAのforumで同じ問題の人がいないか検索するのがいいです。
$ hexdump test.raw | head
0000000 1685 2108 16c5 2088 1685 21c8 16c5 2148
0000010 16c5 2188 1685 22c8 1705 2148 1785 21c8
0000020 1705 2108 1785 2108 1745 21c8 1745 20c8
0000030 1785 2108 1806 21c8 1745 2148 1745 2208
0000040 1745 22c8 1705 2288 16c5 2188 1705 2108
0000050 1745 2348 16c5 22c8 1705 2288 1705 2208
0000060 16c5 2308 16c5 2248 1745 2208 1705 22c8
0000070 1785 2248 1705 2308 1685 21c8 1745 21c8
0000080 1785 2288 1745 23c8 1785 2308 1745 2348
0000090 1785 2208 1685 2248 1705 22c8 1745 23c8
さっぱり値の意味はわかりませんが、光が当たらないように覆い隠したり、明るく照らすと値が変わるので、何かを撮影しているっぽいことはわかります。
暗くした結果。
$ hexdump test.raw | head
0000000 1004 1004 1004 1044 1004 1004 1004 1004
*
0000020 1004 1004 1004 1004 1004 1004 1004 1004
0000030 1004 1044 1044 1004 1044 1004 1004 1044
0000040 1004 1004 1004 1044 1004 1004 1004 1004
0000050 1044 1004 1044 1004 1004 1004 1004 1004
0000060 1004 1004 1004 1004 1004 1004 1004 1004
0000070 1044 1004 1044 1004 1044 1004 1004 1044
0000080 1004 1004 1004 1004 1004 1004 1004 1004
0000090 1004 1004 1044 1044 1044 1004 1044 1004
ついでにstream-countを増やしてfps計測もしてみましょう。
jetson@orin-nano:~/imx708$ v4l2-ctl -d /dev/video0 --set-ctrl bypass_mode=0 --stream-mmap --stream-count=60
<<<<<<<<<<<<<<<< 14.36 fps
<<<<<<<<<<<<<< 14.36 fps
<<<<<<<<<<<<<<< 14.36 fps
<<<<<<<<<<<<<< 14.36 fps
わずかに違いますが、だいたい良さそうですね。
これでセンサーが期待通りにデータを送り出していることがしていることがわかりました。
まとめ
MIPI-CSI2のイメージセンサーの出力はたいていBayerで変換が必要という前提を確認しました。
駆動モードという概念が存在し、センサーは事前にI2Cで設定されたレジスタの値に従って連続的にデータを送って来るため、受信側も出力されるデータ形式を設定を知っておく必要があることもわかりました。
ところでBayerから変換しないと絵が見れないですよね、次はJetsonのISPを通して画像を得るところをやります。