Jetsonのカメラドライバを書く[Argus撮影編]

# はじめに

前の記事: [Jetsonのカメラドライバを書く[RAW撮影編]](https://qiita.com/uzuna/items/05c169c91460e60fecea)
次の記事: [Jetsonのカメラドライバを書く[モード追加とGStreamer編]](https://qiita.com/uzuna/items/b0f7eedb1affc5909891)

前の記事ではRAW画像を取得ができました。
しかし目debayer出来る人でもない限り、実用面で絵が撮れているとは言いがたいです。
JetsonのISPを通して人の目に優しい絵を撮ります。

# Argus撮影編

なぜUSBカメラは繋いでからすぐ使えるのでしょう?
USBカメラもイメージセンサーは同じようなものです、ですがカメラの中にdebayerする、更に必要なら圧縮形式に変換するISPを搭載しているのです。
Linuxは通常この機能を提供していませんし、仮に出来たとしても画像データを処理するのはCPUのコストが高いです。
Jetsonはどうしているのでしょうか?

## Argusについて

[L4T 35.3.1のCamera Architecture Stack](https://docs.nvidia.com/jetson/archives/r35.3.1/DeveloperGuide/text/SD/CameraDevelopment/CameraSoftwareDevelopmentSolution.html#camera-architecture-stack)を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92809/e47e65f3-b54d-75a4-44e4-d088569a3bb8.png)

Linuxカーネルはカメラを扱うのにV4L2のフレームワークを用意しています。
青のブロックで構成された`Sensor -> V4L2Driver -> V4L2 MediaController Framework`がLinuxのカーネルがサポートしている一般的な構成で、先のRAW画像の取得もこのルートでデータを得ています。
V4L2というレイヤーが別れていることで、アプリケーションはカメラの詳細を知らなくてもV4L2を通して同等に取り扱いが出来ますし、ドライバ開発者はV4L2のインターフェースに従って実装をすることで、多様なアプリケーションのことを考慮する必要はしなくて良くなります。便利ですね。

ところでこの図には、横から異なるルートで`[VI, ISP], -> Tegra Drivers -> Camera Core -> libargus`のルートが書かれているのがわかります。
これが、Jetsonのカメラ特有で、今回の記事の中心となるところです。

Jetson Orin シリーズにはISPが搭載されており、メインのCPUを使うこと無くdebayerが出来ます。
残念ながらISPの詳細は公開されていないため、一般ユーザーにはパラメータ調整は出来ません。
ですがデフォルト設定でdebayerはしてくれるので今回はこれを使います。

V4L2とは異なるこちらのルートはユーザーから見ると`libargus`を起点としており、ArgusAPIを経由してカーネルやドライバにアクセスできます。
詳細は[Libargus Camera API](https://docs.nvidia.com/jetson/l4t-multimedia/group__LibargusAPI.html)にあり、カメラの制御の他、バッファの操作、カメラからのメタデータ取得などのAPIが用意されています。

直接このAPIを使うところまで触るとカメラドライバの話から外れるのでやりません。
NVIDIAのMultimediaAPIに含まれるサンプルアプリケーションを使います。

### argus_camera

このArgusAPI使うサンプルアプリケーションは`/usr/src/jetson_multimedia_api`にあります。
その中でも`argus_camera`がパラメータ制御などについて見やすいUIを持つのでこれを使います。
[I2C編](https://qiita.com/uzuna/items/693cabb800173f2175f5#dtb%E6%9B%B8%E3%81%8D%E6%8F%9B%E3%81%88)で追加したMakefileにsetupのターゲットがあるので`make setup`を実行して`argus_camera`をビルドします。

## 露光制御の実装

JetsonではISPを通すと露光制御についても行ってくれます。
RaspberryPiではカーネルモジュールにモード設定や変更可能なレンジを細かく記述していましたが、Jetsonの場合はデバイスツリーに記述し`tegra-camera`がその設定からV4L2の定義をしてくれる用で、カーネルモジュール内には最低限のレジスタとモードの対応付けを記述するだけですみます。
tegra-cameraはV4L2のインターフェースを通して露光制御を行うため、以下の4つの機能の実装を必要です。

```
static const u32 ctrl_cid_list[] = {
	TEGRA_CAMERA_CID_GAIN,
	TEGRA_CAMERA_CID_EXPOSURE,
	TEGRA_CAMERA_CID_FRAME_RATE,
	TEGRA_CAMERA_CID_SENSOR_MODE_ID,
};
```

センサーモードIDはモードテーブルを記述すれば問題ありません。
残り3つの関数の内、FrameRateは今回固定しているので0を返す実装で省略できます。
ゲインと露光時間については露出制御には欠かせません。実装をしましょう。

### ゲイン設定

`imx708.c`を見ると`imx708_set_analogue_gain`という関数があり、これがゲインの設定を行っています。
関係するコードを抜粋すると以下です。

```imx708.c
/* Analog gain control */
#define IMX708_REG_ANALOG_GAIN		0x0204
#define IMX708_ANA_GAIN_MIN		112
#define IMX708_ANA_GAIN_MAX		960
#define IMX708_ANA_GAIN_STEP		1
#define IMX708_ANA_GAIN_DEFAULT	   IMX708_ANA_GAIN_MIN

static int imx708_set_analogue_gain(struct imx708 *imx708, unsigned int val)
{
	int ret;

	/*
	 * In HDR mode this will set the gain for the longest exposure,
	 * and by default the sensor uses the same gain for all of them.
	 */
	ret = imx708_write_reg(imx708, IMX708_REG_ANALOG_GAIN,
			       IMX708_REG_VALUE_16BIT, val);

	return ret;
}
```

ここから読み取れるのはレジスタアドレス `0x0204` からおそらく16bitのデータで112-960の範囲で指定できるみたいですね。
単位がわかりませんが、値が大きいほどゲインが増えるようなので気にせず設定します。

#### Jetsonでの設定

設定はデバイスツリーのmode0のgainに関する値を書き換えます

```diff: hardware/nvidia/platform/t23x/p3768/kernel-dts/cvb/tegra234-camera-rbpcv3-imx708.dtsi
	cam_i2cmux {
		i2c@1 {
			imx708_cam1: rbpcv3_imx708_c@1a {
				mode0 { /* imx708_MODE_4608x2592 */

+					gain_factor = "1";
+					min_gain_val = "112";
+					max_gain_val = "960";
+					step_gain_val = "1";
+					default_gain = "112";

```

制御はカーネルモジュールに追加しましょう。

```diff: Linux_for_Tegra/source/public/kernel/nvidia/drivers/media/i2c/nv_imx708.c

// 値が8bit以上で連続するレジスタに書き込む時に使う
+ static void imx708_regtable(imx708_reg table[], u16 address_low, u8 nr_regs,
+ 	u32 value)
+ {
+ 	unsigned int i;
+ 
+ 	for (i = 0; i < nr_regs; i++) {
+ 		(table+i)->addr = address_low + i;
+ 		(table+i)->val = (u8)(value >> ((nr_regs - 1 - i) * 8));
+ 	}
+ }


static int imx708_set_gain(struct tegracam_device *tc_dev, s64 val)
{
+	struct camera_common_data *s_data = tc_dev->s_data;
+	struct imx708 *priv = (struct imx708 *)tc_dev->priv;
+	struct device *dev = s_data->dev;
+	const struct sensor_mode_properties *mode =
+		&s_data->sensor_props.sensor_modes[s_data->mode_prop_idx];
+
+	imx708_reg regs[3];
+
+	if (val < mode->control_properties.min_gain_val)
+		val = mode->control_properties.min_gain_val;
+	else if (val > mode->control_properties.max_gain_val)
+		val = mode->control_properties.max_gain_val;
+
+	dev_dbg(dev, "%s: val: %lld\n", __func__, val);
+	
+	imx708_regtable(regs, IMX708_REG_ANALOG_GAIN, 2, val);
+	dev_dbg(dev, "%s: val: %lld\n", __func__, val);
+	(regs+2)->addr = IMX708_TABLE_END;
+
+	return imx708_write_table(priv, regs);
}
```

### 露光時間

露光時間も同様に見ていきます。
モード設定の時に少し見えましたが、露光時間設定の単位は時間ではなくラインとなっています。

```imx708.c
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
	},
```

IMX219の`5-7 Storage Time (Electronic Shutter) Settings`に解説があります。
$T_{sh} = \verb|exposure lines| * \verb|Time per line| + (\alpha * \verb|pixel clock period|)$です。後者は前者に比べると非常に小さいのでここでは無視をして、**ラインアクセスの周期 * ライン数**で露光時間が決まるようです。

[前回の駆動モードを決める](![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92809/a5b76444-51c1-66e1-8165-39fa482c1b7d.png)
)で見たように1ラインにかかる時間はおよそ26.29usです。時間はこのラインの倍数単位で設定できることになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92809/948dfc35-c305-5cbf-45dc-8943b0d75742.png)
そして上の設定に書いてあるとおり最小の幅は8lineなので**210.32us**が最小露光時間ですね。
設定の実装`imx708_set_exposure`を見てみます。

```imx708.c
/* Exposure control */
#define IMX708_REG_EXPOSURE		0x0202
#define IMX708_EXPOSURE_OFFSET		48
#define IMX708_EXPOSURE_DEFAULT		0x640
#define IMX708_EXPOSURE_STEP		1
#define IMX708_EXPOSURE_MIN		1
#define IMX708_EXPOSURE_MAX		(IMX708_FRAME_LENGTH_MAX - \
					 IMX708_EXPOSURE_OFFSET)

static int imx708_set_exposure(struct imx708 *imx708, unsigned int val)
{
	int ret;

	val = max(val, imx708->mode->exposure_lines_min);
	val -= val % imx708->mode->exposure_lines_step;

	/*
	 * In HDR mode this will set the longest exposure. The sensor
	 * will automatically divide the medium and short ones by 4,16.
	 */
	ret = imx708_write_reg(imx708, IMX708_REG_EXPOSURE,
			       IMX708_REG_VALUE_16BIT,
			       val >> imx708->long_exp_shift);

	return ret;
}

static void imx708_adjust_exposure_range(struct imx708 *imx708,
					 struct v4l2_ctrl *ctrl)
{
	int exposure_max, exposure_def;

	/* Honour the VBLANK limits when setting exposure. */
	exposure_max = imx708->mode->height + imx708->vblank->val -
		IMX708_EXPOSURE_OFFSET;
	exposure_def = min(exposure_max, imx708->exposure->val);
	__v4l2_ctrl_modify_range(imx708->exposure, imx708->exposure->minimum,
				 exposure_max, imx708->exposure->step,
				 exposure_def);
}
```

レジスタは`0x0202`です。
ストリームの周期を維持するために、設定可能な最大値は`フレーム長-offset`以下に制限しています。
また、HSYNCが短すぎると最小設定値が変わるのか `exposure_lines_step` の倍数になるようにしています。
`long_exp_shift`は、フレーム周期を超える長時間露光のためのでしょうか、今は気にしないことにしましょう。

#### Jetsonでの設定

まずは制御ですが、デバイスツリーの設定は秒単位での指定となっています。
これは露出条件をArgusが認識する上での制約があるようなので、単位を時間に変換して設定します。
ArgusAPIは撮影時に設定した露光時間やゲインが取れるので単位系を合わせておくとデータの扱いが楽になります。

|項目|line|nsec|
|---|---|---|
|1line|1|26290|
|min exp|8|210320|
|max exp|2650-48 = 2602|68406580|

出来るなら誤差が小さくなるようにnsecで設定したいのですが、usでなければargusがうまく受け付けてくれなかったのでusで指定します。

```diff: hardware/nvidia/platform/t23x/p3768/kernel-dts/cvb/tegra234-camera-rbpcv3-imx708.dtsi
	cam_i2cmux {
		i2c@1 {
			imx708_cam1: rbpcv3_imx708_c@1a {
				mode0 { /* imx708_MODE_4608x2592 */

+					exposure_factor = "1000000"; /* us */
+					min_exp_time = "210";
+					max_exp_time = "68406";
+					step_exp_time = "26";
+					default_exp_time = "5000";

```

カーネルモジュールでは時間をライン単位に戻して適用します

```diff: Linux_for_Tegra/source/public/kernel/nvidia/drivers/media/i2c/nv_imx708.c
static int imx708_set_exposure(struct tegracam_device *tc_dev, s64 val)
{
+	struct camera_common_data *s_data = tc_dev->s_data;
+	struct imx708 *priv = (struct imx708 *)tc_dev->priv;
+	struct device *dev = s_data->dev;
+	const struct sensor_mode_properties *mode =
+		&s_data->sensor_props.sensor_modes[s_data->mode_prop_idx];
+	
+	u32 exp = 0;
+	imx708_reg regs[3];
+
+	if (val < (s64)mode->control_properties.min_exp_time.val)
+		val = (s64)mode->control_properties.min_exp_time.val;
+	else if (val > (s64)mode->control_properties.max_exp_time.val)
+		val = (s64)mode->control_properties.max_exp_time.val;
+	
+	exp = (u32)(val / (s64)mode->control_properties.step_exp_time.val);
+	dev_dbg(dev, "%s: exp_time: %lld [%u]\n", __func__, val, exp);
+	imx708_regtable(regs, IMX708_REG_EXPOSURE, 2, exp);
+	(regs+2)->addr = IMX708_TABLE_END;
+	return imx708_write_table(priv, regs);
}
```

### Argus向けデバイスツリーの修正

設定を追加しただけではまだArgusでの撮影が出来ません。

カメラ制御関係の設定デバイスツリーで`tegra-camera-platform`のノード設定をしていました。
これも初期の状態ではimx219の設定になっています。

```
$ cat /proc/device-tree/tegra-camera-platform/modules/module0/drivernode0/proc-device-tree
/proc/device-tree/cam_i2cmux/i2c@0/rbpcv2_imx219_a
```

imx708のオーバーレイで、これらをimx708に向けます。

```diff: hardware/nvidia/platform/t23x/p3768/kernel-dts/tegra234-p3767-camera-p3768-imx708.dts
+	fragment@4 {
+		target = <&cam_module1>;
+		__overlay__ {
+			status = "okay";
+			badge = "jakku_rear_RBPCV3";
+			position = "rear";
+			orientation = "0";
+		};
+	};
+	fragment@5 {
+		target = <&cam_module1_drivernode0>;
+		__overlay__ {
+			status = "okay";
+			pcl_id = "v4l2_sensor";
+			devname = "imx708 10-001a";
+			proc-device-tree = "/proc/device-tree/cam_i2cmux/i2c@1/rbpcv3_imx708_c@1a";
+		};
+	};
```

これで必要な実装はできました。
前回同様ビルドしてorin-nanoに転送します。

## 撮影する

### argus_cameraでプレビュー

argus_cameraは実機につないだディスプレイで確認します。
remote sshではEGLをつかもうとして失敗するためです。

```
~/imx708$ ./argus_camrera
```

これ問題なければ以下のような画面が表示されると思います。
絵もでていますし露光時間、ゲイン、フレームレートの設定も期待通りですね :tada: :tada: 
![ss.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92809/d4ab8490-4a84-6f00-3756-24dc1cf867e6.jpeg)

ボケが強いのはIMX708がオートフォーカスのレンズであり、今は機能していないからですね。
色味もいまいちなのはISPのチューニングが出来ていないからです。
今回は仕方ないので諦めます。
Jetson Camera Partner認定の会社にISPチューニングをお願いしたらもっと良い絵が取れるようになるでしょう。

### GStreamerで撮影

撮影できたならGStreamerでファイル保存とか転送をしたいですね。
Orin NanoはNVENCがないのでH264のCPUエンコードで頑張ってもらいます。

```
gst-launch-1.0 nvarguscamerasrc sensor-id=0 sensor-mode=0 num-buffers=60 ! 'video/x-raw(memory:NVMM), width=1920, height=1080,format=NV12' ! nvvideoconvert ! x264enc ! h264parse ! qtmux ! filesink location=test.mp4
Setting pipeline to PAUSED ...
Pipeline is live and does not need PREROLL ...
Setting pipeline to PLAYING ...
New clock: GstSystemClock
Redistribute latency...
GST_ARGUS: Creating output stream
CONSUMER: Waiting until producer is connected...
GST_ARGUS: Available Sensor modes :
GST_ARGUS: 4608 x 2592 FR = 14.350001 fps Duration = 69686408 ; Analog Gain range min 112.000000, max 960.000000; Exposure Range min 210000, max 68406000;

GST_ARGUS: Running with following settings:
   Camera index = 1 
   Camera mode  = 0 
   Output Stream W = 4608 H = 2592 
   seconds to Run    = 0 
   Frame Rate = 14.350001 
GST_ARGUS: Setup Complete, Starting captures for 0 seconds
GST_ARGUS: Starting repeat capture requests.
CONSUMER: Producer has connected; continuing.
ARGUS_ERROR: Error generated. /dvs/git/dirty/git-master_linux/multimedia/nvgstreamer/gst-nvarguscamera/gstnvarguscamerasrc.cpp, execute: 1139 InvalidState.
GST_ARGUS: Cleaning up
ERROR: from element /GstPipeline:pipeline0/GstNvArgusCameraSrc:nvarguscamerasrc0: CANCELLED
Additional debug info:
Argus Error Status
Execution ended after 0:00:00.065694248
Setting pipeline to NULL ...
nvbuf_utils: dmabuf_fd -1 mapped entry NOT found
Error generated. /dvs/git/dirty/git-master_linux/multimedia/nvgstreamer/gst-nvarguscamera/gstnvarguscamerasrc.cpp, threadExecute:694 NvBufSurfaceFromFd Failed.
Error generated. /dvs/git/dirty/git-master_linux/multimedia/nvgstreamer/gst-nvarguscamera/gstnvarguscamerasrc.cpp, threadFunction:247 (propagating)
Freeing pipeline ...
make: *** [Makefile:74: preview] Error 1
```

エラーがでました...。
~~調べた雰囲気だと、今の設定だと画像が大きくてNVBuffferSurfaceに収まらないようです。
プレビューは出るもののGStreamerで保存できないのは片手落ちですね。
困った。~~

#### 追記(2023/10/19)

原因がわかりました。
エラーはnvarguscamerasrcのInvalidStateです。

```
ARGUS_ERROR: Error generated. /dvs/git/dirty/git-master_linux/multimedia/nvgstreamer/gst-nvarguscamera/gstnvarguscamerasrc.cpp, execute: 1139 InvalidState
```

このままでは意味がわかりませんが、ソースコードを追ったところフレームレート設定に失敗しているようです。`nvarguscamerasrc`はデフォルトのフレームレートとして30fpsと指定されています。今回作ったmode0は14.35fpsを最大値としているため、この範囲外ということで`InvalidState`になっていました。
これは`nvarguscamerasrc`直後のCapsに適切なフレームレートを設定することで回避できます。

```diff
- gst-launch-1.0 nvarguscamerasrc sensor-id=0 sensor-mode=0 num-buffers=60 ! 'video/x-raw(memory:NVMM),width=1920,height=1080,format=NV12' ! nvvideoconvert ! x264enc ! h264parse ! qtmux ! filesink location=test.mp4
+ gst-launch-1.0 nvarguscamerasrc sensor-id=0 sensor-mode=0 num-buffers=60 ! 'video/x-raw(memory:NVMM),width=1920,height=1080,format=NV12,framerate=14/1' ! nvvideoconvert ! x264enc ! h264parse ! qtmux ! filesink location=test.mp4
```

# まとめ

今回はISPなどはtegra-cameraが握っており、ArgusAPIを使うことでdebayerされた映像を得ることが出来ました。
また自動露光制御などのためにデバイスツリーにイメージセンサーの動作モードも詳しく記述が必要で、L4Tではそれらをいい感じにArgusが扱ってくれるため、レジスタ設定とV4L2のAPIを実装すれば良いことがわかりました。

次回、binningモードを追加してGStreamerで動画を録ります -> [Jetsonのカメラドライバを書く[モード追加とGStreamer編]](https://qiita.com/uzuna/items/b0f7eedb1affc5909891)

この記事時点のソースコードは[Github](https://github.com/uzuna/jetson-imx708/tree/9d8f4c1e0de159dd62873276190b9c4f445bef01)で公開しています。