JetsonでD言語をやる

## JetsonでD言語

JetsonはNVIDIAが出している組み込み向けのシングルボードコンピュータです。ARMプロセッサに加え、GPUやDLA、PVAといったアクセラレータが搭載されており、AIや画像処理といったアプリケーションを高速に処理することができます。以下の図はJetson Orinの [Technical Brief](https://www.nvidia.com/content/dam/en-zz/Solutions/gtcf21/jetson-orin/nvidia-jetson-agx-orin-technical-brief.pdf)からの引用です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73600/8ef45bfa-59d8-5cc7-16bf-9e5624e6b70b.png)

Ubuntuが動作するので、D言語は[公式のインストールスクリプト](https://dlang.org/install.html)で入れることができます。

```shell
curl -fsS https://dlang.org/install.sh | bash -s ldc
source ~/dlang/ldc-1.35.0/activate
```

![Screenshot from 2023-12-31 14-32-35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73600/e4b63929-aa9a-8f69-bace-6e4ac2c60355.png)



簡単ですね。

前述の通り、Jetsonには様々なアクセレラータが搭載されていていて、VPIを使うことでアクセスすることができます。そこでD言語でVPIを使ってみることにします。


## D言語でVPI

VPI(Vision Programming Interface。https://docs.nvidia.com/vpi/)とはNVIDIA GPU搭載のコンピュータやJetson向けに画像処理機能を提供するライブラリです。日本語では[こちらの記事](https://qiita.com/dandelion1124/items/cd618e653f7a5b5e1ab4)などが参考になると思います。

VPIはC APIとPython APIが提供されています。C APIが提供されていることは、理屈上C APIを参照することができる任意の言語で利用可能なはず。[D言語コンパイラはC言語をコンパイルすることができる](https://qiita.com/mono_shoo/items/b30056818dbc7953bbda)ので、VPIをD言語から利用することは難しくないと思います。というわけで、VPIをD言語から呼び出すサンプルプロジェクトを作りました。

https://github.com/nonanonno/vpi_d

`dub`コマンド1つでビルドと実行が可能です。画像を入力として、エッジ画像を作成します。使用するアクセラレータを3種類から選択できます。入力画像と、実行結果の出力画像を並べたものがこちらです。

![Screenshot from 2023-12-16 03-24-36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73600/cd388c47-e13c-1d73-8678-52861a433bf6.png)

### ビルドの流れ

dub.sdlに記述していますが、ビルドは以下の流れで行っています。

1. VPIのC APIをincludeするヘッダーファイル(`include/vpi.h`)をプリプロセッサにかけ、マクロ展開後のファイル(`include/vpi.i`ファイル)を生成する。これはdub.sdlの`preBuildCommands`に記述した内容です。
1. それとDソースと一緒にビルドする。

D言語コンパイラはCコード(`*.c`ファイル)をビルドできますが、ヘッダーのみを使いたい場合はプリプロセッサにかける処理の追加が必要なようです。また、この仮定でマクロが展開されてしまうので、マクロで定義した定数は`const`定数で置き換えてあげる必要がありました。このあたりは、[こちらの動画](https://www.youtube.com/watch?v=1BrvRkZdGOA)を参考にしています。

### 処理の流れ

プログラムは`/opt/nvidia/vpi2/samples/01-convolve_2d`にあるC++のサンプルを模擬しています。3x3の畳み込みを行うプログラムで、前述の通りバックエンドを3つ選ぶことができます。選択肢はCPUとCUDA、それからPVAです。

畳み込みを行う部分はただVPIの呼ぶだけなので、元々のサンプルから大きく変わることはないと思います。

```d
void main(string[] args)
{
	VPIImage image;
	VPIImage imageRGB;
	VPIImage gradient;
	VPIStream stream;
	VPIBackend backend;

	try
	{
		if (args.length != 3)
		{
			throw new Exception(format!"Usage: %s <cpu|pva|cuda> <input image>"(args[0]));
		}

		auto strBackend = args[1];
		auto strInputFilename = args[2];

		Image inputImage;
		inputImage.loadFromFile(strInputFilename, LAYOUT_GAPLESS | LAYOUT_VERT_STRAIGHT | LOAD_NO_ALPHA);
		if (inputImage.isError())
		{
			throw new Exception(inputImage.errorMessage.to!string);
		}

		switch (strBackend)
		{
		case "cpu":
			backend = VPI_BACKEND_CPU;
			break;
		case "cuda":
			backend = VPI_BACKEND_CUDA;
			break;
		case "pva":
			backend = VPI_BACKEND_PVA;
			break;
		default:
			new Exception(
				"Backend '" ~ strBackend ~ "' not recognized, it must be either cpu, cuda or pva.");
		}

		checkStatus(vpiStreamCreate(0, &stream));
		scope (exit)
			vpiStreamDestroy(stream);

		checkStatus(imageCreateWrapperGamut(inputImage, 0, &imageRGB));
		scope (exit)
			vpiImageDestroy(imageRGB);

		checkStatus(vpiImageCreate(inputImage.width, inputImage.height, VpiImageFormatU8, 0, &image));
		scope (exit)
			vpiImageDestroy(image);

		checkStatus(vpiSubmitConvertImageFormat(stream, VPI_BACKEND_CUDA, imageRGB, image, null));

		checkStatus(vpiImageCreate(inputImage.width, inputImage.height, VpiImageFormatU8, 0, &gradient));

		float[3 * 3] kernel = [1, 0, -1, 0, 0, 0, -1, 0, 1];

		checkStatus(vpiSubmitConvolution(stream, backend, image, gradient, kernel.ptr, 3, 3, VPI_BORDER_ZERO));

		checkStatus(vpiStreamSync(stream));

		{
			VPIImageData outData;
			checkStatus(vpiImageLockData(gradient, VPI_LOCK_READ, VpiImageBufferHostPitchLinear, &outData));
			scope (exit)
				checkStatus(vpiImageUnlock(gradient));

			assert(outData.bufferType == VpiImageBufferHostPitchLinear);

			Image outputImage;
			outputImage.createViewFromData(outData.buffer.pitch.planes[0].data, outData.buffer.pitch.planes[0].width, outData
					.buffer.pitch.planes[0].height, PixelType.l8, outData
					.buffer.pitch.planes[0].pitchBytes);

			outputImage.saveToFile("edges_" ~ strBackend ~ ".png");
		}

	}
	catch (Exception e)
	{
		writeln(e.msg);
		return;
	}
}
```


問題は画像の入出力です。
C++のサンプルではOpenCVを使って画像の読み込みを行っていました。D言語にはないので、別のライブラリを使用します。なんでも良かったのですが、「image」で検索して一番上に出てきた[gamut](https://code.dlang.org/packages/gamut)を使ってみました。

作る必要のある構造体は最終的に`VPIImage`になります。main関数の`imageRGB`に格納されるものです。これを、`/opt/nvidia/vpi2/include/vpi/detail/OpenCVUtils.hpp`を見ながら何となく作ってみたのがこちら。

```d
VPIStatus imageCreateWrapperGamut(ref Image gam, ulong flags, VPIImage* img)
{
	VPIImageData imgData;

	VPIStatus status = fillImageData(gam, &imgData);
	if (status != VPI_SUCCESS)
	{
		return status;
	}

	return vpiImageCreateWrapper(&imgData, null, flags, img);
}

VPIStatus fillImageData(ref Image gam, VPIImageData* imgData)
{
	assert(imgData);
	if (!gam.hasData)
	{
		return VpiErrorInvalidArgument;
	}
	VPIImageFormat fmt = ToImageFormatFromGamut(gam.type);
	if (fmt == VpiImageFormatInvalid)
	{
		return VpiErrorInvalidArgument;
	}

	imgData.bufferType = VpiImageBufferHostPitchLinear;

	imgData.buffer.pitch.format = fmt;
	imgData.buffer.pitch.numPlanes = 1;
	imgData.buffer.pitch.planes[0].width = gam.width;
	imgData.buffer.pitch.planes[0].height = gam.height;
	imgData.buffer.pitch.planes[0].pitchBytes = gam.pitchInBytes;
	imgData.buffer.pitch.planes[0].data = cast(void*) gam.allPixelsAtOnce().ptr;
	imgData.buffer.pitch.planes[0].pixelType = vpiImageFormatGetPlanePixelType(fmt, 0);

	return VPI_SUCCESS;
}

VPIImageFormat ToImageFormatFromGamut(PixelType type)
{
	switch (type)
	{
	case PixelType.rgb8:
		return VpiImageFormatBGR8;
	default:
		return VpiImageFormatInvalid;
	}
}
```

最低限しか作っていないですが、以下のような流れです。

1. `VPIImageData`を作る。これは画像の大きさなどのプロパティと、画像データが格納されているポインタを保持する(`fillImageData`)。
1. `vpiImageCreateWrapper`関数で`VPIImageData`を元に`VPIImage`を作る。(`imageCreateWrapperGamut`。ポインタは外部から与えられることもあるからWrapperを作るのかなと想像)

これで画像の入出力を含む、VPIの呼び出しができるようになりました。D言語でVPIを触りたい時に活用できると思います。

（ちなみに、`format`に `VpiImageFormatRGB8` を入れたら何か動かなかったです。何故だろう)