JetsonでD言語
JetsonはNVIDIAが出している組み込み向けのシングルボードコンピュータです。ARMプロセッサに加え、GPUやDLA、PVAといったアクセラレータが搭載されており、AIや画像処理といったアプリケーションを高速に処理することができます。以下の図はJetson Orinの Technical Briefからの引用です。
Ubuntuが動作するので、D言語は公式のインストールスクリプトで入れることができます。
curl -fsS https://dlang.org/install.sh | bash -s ldc
source ~/dlang/ldc-1.35.0/activate
簡単ですね。
前述の通り、Jetsonには様々なアクセレラータが搭載されていていて、VPIを使うことでアクセスすることができます。そこでD言語でVPIを使ってみることにします。
D言語でVPI
VPI(Vision Programming Interface。https://docs.nvidia.com/vpi/)とはNVIDIA GPU搭載のコンピュータやJetson向けに画像処理機能を提供するライブラリです。日本語ではこちらの記事などが参考になると思います。
VPIはC APIとPython APIが提供されています。C APIが提供されていることは、理屈上C APIを参照することができる任意の言語で利用可能なはず。D言語コンパイラはC言語をコンパイルすることができるので、VPIをD言語から利用することは難しくないと思います。というわけで、VPIをD言語から呼び出すサンプルプロジェクトを作りました。
dub
コマンド1つでビルドと実行が可能です。画像を入力として、エッジ画像を作成します。使用するアクセラレータを3種類から選択できます。入力画像と、実行結果の出力画像を並べたものがこちらです。
ビルドの流れ
dub.sdlに記述していますが、ビルドは以下の流れで行っています。
- VPIのC APIをincludeするヘッダーファイル(
include/vpi.h
)をプリプロセッサにかけ、マクロ展開後のファイル(include/vpi.i
ファイル)を生成する。これはdub.sdlのpreBuildCommands
に記述した内容です。 - それとDソースと一緒にビルドする。
D言語コンパイラはCコード(*.c
ファイル)をビルドできますが、ヘッダーのみを使いたい場合はプリプロセッサにかける処理の追加が必要なようです。また、この仮定でマクロが展開されてしまうので、マクロで定義した定数はconst
定数で置き換えてあげる必要がありました。このあたりは、こちらの動画を参考にしています。
処理の流れ
プログラムは/opt/nvidia/vpi2/samples/01-convolve_2d
にあるC++のサンプルを模擬しています。3x3の畳み込みを行うプログラムで、前述の通りバックエンドを3つ選ぶことができます。選択肢はCPUとCUDA、それからPVAです。
畳み込みを行う部分はただVPIの呼ぶだけなので、元々のサンプルから大きく変わることはないと思います。
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を使ってみました。
作る必要のある構造体は最終的にVPIImage
になります。main関数のimageRGB
に格納されるものです。これを、/opt/nvidia/vpi2/include/vpi/detail/OpenCVUtils.hpp
を見ながら何となく作ってみたのがこちら。
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;
}
}
最低限しか作っていないですが、以下のような流れです。
-
VPIImageData
を作る。これは画像の大きさなどのプロパティと、画像データが格納されているポインタを保持する(fillImageData
)。 -
vpiImageCreateWrapper
関数でVPIImageData
を元にVPIImage
を作る。(imageCreateWrapperGamut
。ポインタは外部から与えられることもあるからWrapperを作るのかなと想像)
これで画像の入出力を含む、VPIの呼び出しができるようになりました。D言語でVPIを触りたい時に活用できると思います。
(ちなみに、format
に VpiImageFormatRGB8
を入れたら何か動かなかったです。何故だろう)