実際の例

この章では、実際の例を使用して次について説明します。

  • トップダウン フローおよびボトムアップ フローの両方を使用して最適化する方法
    • トップダウン フローは、Lucas-Kanade (LK) オプティカル フロー アルゴリズムを使用して説明します。
    • ボトムアップ フローは、ステレオ ビジョン ブロック マッチング アルゴリズムを使用して説明します。
  • 適用される最適化指示子
  • これらの指示子を使用する理由

トップダウン: オプティカル フローのアルゴリズム

Lucas-Kanade (LK) 法は、オプティカル フローの見積もりや、2 つの関連画像間におけるピクセル移動の見積もりに広く使用される差分手法です。このシステム例では、関連画像は 1 つのビデオ ストリームの現在の画像と前の画像です。LK 法は計算負荷の高いアルゴリズムで、隣接するピクセル領域に適用され、最小二乗の差を使用して一致するピクセルを見つけます。

次のコード例は、このアルゴリズムをインプリメントする方法を示していを示します。2 つの入力ファイルが読み込まれ、fpga_optflow 関数で処理され、結果が出力ファイルに書き出されています。

int main()
{
	FILE *f;
	pix_t *inY1 = (pix_t *)sds_alloc(HEIGHT*WIDTH);
	yuv_t *inCY1 = (yuv_t *)sds_alloc(HEIGHT*WIDTH*2);
	pix_t *inY2 = (pix_t *)sds_alloc(HEIGHT*WIDTH);
	yuv_t *inCY2 = (yuv_t *)sds_alloc(HEIGHT*WIDTH*2);
	yuv_t *outCY = (yuv_t *)sds_alloc(HEIGHT*WIDTH*2);
	printf("allocated buffers\n");

	f = fopen(FILEINAME,"rb");
	if (f == NULL) {
		printf("failed to open file %s\n", FILEINAME);
		return -1;
	}
	printf("opened file %s\n", FILEINAME);

	read_yuv_frame(inY1, WIDTH, WIDTH, HEIGHT, f);
	printf("read 1st %dx%d frame\n", WIDTH, HEIGHT);
	read_yuv_frame(inY2, WIDTH, WIDTH, HEIGHT, f);
	printf("read 2nd %dx%d frame\n", WIDTH, HEIGHT);

	fclose(f);
	printf("closed file %s\n", FILEINAME);

	convert_Y8toCY16(inY1, inCY1, HEIGHT*WIDTH);
	printf("converted 1st frame to 16bit\n");
	convert_Y8toCY16(inY2, inCY2, HEIGHT*WIDTH);
	printf("converted 2nd frame to 16bit\n");

	fpga_optflow(inCY1, inCY2, outCY, HEIGHT, WIDTH, WIDTH, 10.0);
	printf("computed optical flow\n");

	// write optical flow data image to disk
	write_yuv_file(outCY, WIDTH, WIDTH, HEIGHT, ONAME);

	sds_free(inY1);
	sds_free(inCY1);
	sds_free(inY2);
	sds_free(inCY2);
	sds_free(outCY);
	printf("freed buffers\n");

return 0;
}

これは、標準 C/C++ データ型を使用した典型的なトップダウンのデザイン フローです。

次のコード例に示す fpa_optflow 関数には、次のサブ関数が含まれています。

  • readMatRows
  • computeSum
  • computeFlow
  • getOutPix
  • writeMatRows
int fpga_optflow (yuv_t *frame0, yuv_t *frame1, yuv_t *framef, int height, int width, int stride, float clip_flowmag)
{
#ifdef COMPILEFORSW
	  int img_pix_count = height*width;
#else
	  int img_pix_count = 10;
#endif

  if (f0Stream == NULL) f0Stream = (pix_t *) malloc(sizeof(pix_t) * img_pix_count);
  if (f1Stream == NULL) f1Stream = (pix_t *) malloc(sizeof(pix_t) * img_pix_count);
  if (ffStream == NULL) ffStream = (yuv_t *) malloc(sizeof(yuv_t) * img_pix_count);

  if (ixix == NULL) ixix = (int *) malloc(sizeof(int) * img_pix_count);
  if (ixiy == NULL) ixiy = (int *) malloc(sizeof(int) * img_pix_count);
  if (iyiy == NULL) iyiy = (int *) malloc(sizeof(int) * img_pix_count);
  if (dix == NULL) dix = (int *) malloc(sizeof(int) * img_pix_count);
  if (diy == NULL) diy = (int *) malloc(sizeof(int) * img_pix_count);

  if (fx == NULL) fx = (float *) malloc(sizeof(float) * img_pix_count);
  if (fy == NULL) fy = (float *) malloc(sizeof(float) * img_pix_count);

  readMatRows (frame0, f0Stream, height, width, stride);
  readMatRows (frame1, f1Stream, height, width, stride);

  computeSum (f0Stream, f1Stream, ixix, ixiy, iyiy, dix, diy, height, width);
  computeFlow (ixix, ixiy, iyiy, dix, diy, fx, fy, height, width);
  getOutPix (fx, fy, ffStream, height, width, clip_flowmag);

  writeMatRows (ffStream, framef, height, width, stride);

  return 0;
}

この例では、fpga_optflow 内のすべての関数がライブ ビデオ データを処理するので、ハードウェア アクセラレーションを実行し、PS とのデータ転送に DMA を使用すると有益です。5 つの関数すべてをハードウェア関数に指定すると場合、システムのトポロジは次の図のようになります。

図: システム トポロジ

システムは、ハードウェアと、パフォーマンスを詳細に解析するためのイベント トレースにコンパイルできます。

ここでの問題は、終了するまでに時間がかかり過ぎることです (1 フレームに 15 秒)。HD ビデオを処理するには、毎秒 60 フレームまたは 16.7 ms ごとに 1 フレーム処理される必要があります。次に示す最適化指示子を使用すると、システムがターゲット パフォーマンスを満たすようになります。

オプティカル フローのメモリ アクセス最適化

設計手法の最初のタスクはデータ転送の最適化です。この場合、システムはストリーミング ビデオを処理するので (各サンプルを連続処理)、メモリ転送最適化では、すべてのアクセスがシーケンシャルに実行されることが SDSoC™ 環境で認識されるようにします。

これには、関数シグネチャの前に SDSoC プラグマを追加して、すべての関数に適用されるようにします。

#pragma SDS data access_pattern(matB:SEQUENTIAL, pixStream:SEQUENTIAL)
#pragma SDS data mem_attribute(matB:PHYSICAL_CONTIGUOUS)
#pragma SDS data copy(matB[0:stride*height])
void readMatRows (yuv_t *matB, pix_t* pixStream,
		int height, int width, int stride);

#pragma SDS data access_pattern(pixStream:SEQUENTIAL, dst:SEQUENTIAL)
#pragma SDS data mem_attribute(dst:PHYSICAL_CONTIGUOUS)
#pragma SDS data copy(dst[0:stride*height])
void writeMatRows (yuv_t* pixStream, yuv_t *dst,
		int height, int width, int stride);

#pragma SDS data access_pattern(f0Stream:SEQUENTIAL, f1Stream:SEQUENTIAL)
#pragma SDS data access_pattern(ixix_out:SEQUENTIAL, ixiy_out:SEQUENTIAL, iyiy_out:SEQUENTIAL)
#pragma SDS data access_pattern(dix_out:SEQUENTIAL, diy_out:SEQUENTIAL)
void computeSum(pix_t* f0Stream, pix_t* f1Stream,
		int* ixix_out, int* ixiy_out, int* iyiy_out,
		int*  dix_out, int* diy_out,
		int height, int width);

#pragma SDS data access_pattern(ixix:SEQUENTIAL, ixiy:SEQUENTIAL, iyiy:SEQUENTIAL)
#pragma SDS data access_pattern(dix:SEQUENTIAL, diy:SEQUENTIAL)
#pragma SDS data access_pattern(fx_out:SEQUENTIAL, fy_out:SEQUENTIAL)
void computeFlow(int* ixix, int* ixiy, int* iyiy,
		int* dix, int* diy,
		float* fx_out, float* fy_out,
		int height, int width);

#pragma SDS data access_pattern(fx:SEQUENTIAL, fy:SEQUENTIAL, out_pix:SEQUENTIAL)
void getOutPix (float* fx, float* fy, yuv_t* out_pix,
		int height, int width, float clip_flowmag);

プロセッサへのインターフェイスである readMatRows および writeMatRows 関数引数では、メモリ転送は物理的に隣接したメモリからシーケンシャルにアクセスされるように指定され、データは単にアクセラレータからアクセスされるのではなく、ハードウェア関数にコピーおよびハードウェア関数からコピーされます。これにより、データが効率的にコピーされます。次のオプションが使用できます。

Sequential
データは処理されるのと同じ順序でシーケンシャルに転送されます。このタイプの転送では、高速データ処理レートのハードウェア オーバーヘッドは最小限であり、エリア効率の高いデータ ムーバーが使用されます。
Contiguous
データは隣接したメモリからアクセスされるので、データ転送レートにスキャッター ギャザー オーバーヘッドはなく、効率的で高速のハードウェア データ ムーバーが使用されます。この指示子は main() 関数内の関連する scs_alloc ライブラリ呼び出しでサポートされており、これらの引数のデータが隣接するメモリに格納されます。
Copy
データはアクセラレータにコピーおよびアクセラレータからコピーされるので、CPU または DDR メモリへのデータ アクセスは必要ありません。ポインターが使用されるので、コピーするデータのサイズが指定されます。

残りのハードウェア関数では、データ転送はシーケンシャルと指定されるので、プログラマブル ロジック (PL) ファブリックの関数が最も効率的なハードウェアを使用して接続されます。

オプティカル フローのハードウェア関数最適化

最高レベルのパフォーマンスで実行するため、ハードウェア関数にも最適化指示子が必要です。これらは既にデザイン サンプルに含まれています。詳細は、ハードウェア関数の最適化手法を参照してください。このデザイン例のハードウェア関数のほとんどは、getOutPix 関数と同様に主に PIPELINE 指示子を使用して最適化されています。

getOutPix 関数の特徴は次のとおりです。

  • サブ関数に INLINE 最適化が適用され、これらの関数のロジックが上位の関数に統合されます。この統合は小型の関数では自動的に実行されますが、この指示子を使用するとサブ関数が常にインライン展開されるので、サブ関数をパイプライン処理する必要はありません。
  • getOutPix 関数内のループは、データを各ピクセル レベルで処理するループであり、クロックごとに 1 ピクセル処理するように PIPELINE 指示子で最適化されます。
pix_t getLuma (float fx, float fy, float clip_flowmag)
{
#pragma HLS inline
  float rad = sqrtf (fx*fx + fy*fy);

  if (rad > clip_flowmag) rad = clip_flowmag; // clamp to MAX
  rad /= clip_flowmag;			    // convert 0..MAX to 0.0..1.0
  pix_t pix = (pix_t) (255.0f * rad);

  return pix;
}

pix_t getChroma (float f, float clip_flowmag)
{
#pragma HLS inline
  if (f >   clip_flowmag ) f =  clip_flowmag; // clamp big positive f to  MAX
  if (f < (-clip_flowmag)) f = -clip_flowmag; // clamp big negative f to -MAX
  f /= clip_flowmag;				// convert -MAX..MAX to -1.0..1.0
  pix_t pix = (pix_t) (127.0f * f + 128.0f);  // convert -1.0..1.0 to -127..127 to 1..255

  return pix;
}

void getOutPix (float* fx, 
                float* fy, 
                yuv_t* out_pix,
				int height, int width, float clip_flowmag)
{
  int pix_index = 0;
  for (int r = 0; r < height; r++) {
    for (int c = 0; c < width; c++) {
      #pragma HLS PIPELINE
      float fx_ = fx[pix_index];
      float fy_ = fy[pix_index];

      pix_t outLuma = getLuma (fx_, fy_, clip_flowmag);
      pix_t outChroma = (c&1)? getChroma (fy_, clip_flowmag) : getChroma (fx_, clip_flowmag);
      yuv_t yuvpix;

      yuvpix = ((yuv_t)outChroma << 8) | outLuma;

      out_pix[pix_index++] = yuvpix;
    }
  }
}

ARRAY_PARTITION および DEPENDENCE 指示子の例は、computeSum 関数を参照してください。この関数では、img1Win 配列に ARRAY_PARTITION 指示子が使用されています。img1Win は配列なので、次のコードのサマリに示すように、デフォルトで最大 2 つのポートを持つブロック RAM にインプリメントされます。

img1Win
クロック サイクルごとに 1 サンプルを処理するようにパイプライン処理された for ループで使用されます。
img1Win
for ループ内で 8 + (KMEDP1-1) + (KMEDP1-1) 回読み出されます。
img1Win
for ループ内で (KMEDP1-1) + (KMEDP1-1) 回書き込まれます。
void computeSum(pix_t* f0Stream, 
                 pix_t* f1Stream, 
                 int*   ixix_out, 
                 int*   ixiy_out, 
                 int*   iyiy_out, 
                 int*   dix_out, 
                 int*   diy_out)
{

   static pix_t img1Win [2 * KMEDP1], img2Win [1 * KMEDP1];
   #pragma HLS ARRAY_PARTITION variable=img1Win complete dim=0
    ...
   for (int r = 0; r < MAX_HEIGHT; r++) {
     for (int c = 0; c < MAX_WIDTH; c++) {
        #pragma HLS PIPELINE
        ...
        int cIxTopR = (img1Col_ [wrt] - img1Win [wrt*2 + 2-2]) /2 ;
        int cIyTopR = (img1Win [ (wrt+1)*2 + 2-1] - img1Win [ (wrt-1)*2 + 2-1])  /2;
        int delTopR = img1Win [wrt*2 + 2-1] - img2Win [wrt*1 + 1-1];
        ...
        int cIxBotR = (img1Col_ [wrb] - img1Win [wrb*2 + 2-2]) /2 ;
        int cIyBotR = (img1Win [ (wrb+1)*2 + 2-1] - img1Win [ (wrb-1)*2 + 2-1]) /2;
        int delBotR = img1Win [wrb*2 + 2-1] - img2Win [wrb*1 + 1-1];
        ...
        // shift windows
        for (int i = 0; i < KMEDP1; i++) {
          img1Win [i * 2] = img1Win [i * 2 + 1];
        }
        for (int i=0; i < KMEDP1; ++i) {
          img1Win  [i*2 + 1] = img1Col_ [i];
          ...
        }
        ...
      } // for c
   }  // for r
   ...
}

ブロック RAM ではクロック サイクルごとに 2 アクセスまでしかサポートされないので、これらのアクセスすべてを 1 クロックで完了することはできません。前述のように、ARRAY_PARTITION 指示子を使用して配列を小型のブロックに分割します。この例では、complete オプションを使用して個別の要素に分割しています。これにより、配列のすべての要素に同時に並列アクセスでき、for ループでクロック サイクルごとにデータが処理されるようになります。

最後に確認する指示子は、DEPENDENCE です。csIxix 配列に、DEPENDENCE 指示子が適用されています。次のコード例では、この配列が読み出された後別のインデックスを使用して書き込まれ、パイプライン処理されたループ内でこれらの読み出しおよび書き込みが実行されます。

 void computeSum(pix_t* f0Stream, 
                 pix_t* f1Stream, 
                 int*   ixix_out, 
                 int*   ixiy_out, 
                 int*   iyiy_out, 
                 int*   dix_out, 
                 int*   diy_out)
{
  ...
   static int csIxix [MAX_WIDTH], csIxiy [MAX_WIDTH], csIyiy [MAX_WIDTH], csDix [MAX_WIDTH], csDiy [MAX_WIDTH];
   ...
   #pragma HLS DEPENDENCE variable=csIxix inter WAR false
   ...
   int zIdx= - (KMED-2);
   int nIdx = zIdx + KMED-2;

   for (int r = 0; r < MAX_HEIGHT; r++) {
     for (int c = 0; c < MAX_WIDTH; c++) {
        #pragma HLS PIPELINE
        ...
        if (zIdx >= 0) {
          csIxixL = csIxix [zIdx];
          ...
       }
       ...
        csIxix [nIdx] = csIxixR;
        ...
        zIdx++;
        if (zIdx == MAX_WIDTH) zIdx = 0;
        nIdx++;
        if (nIdx == MAX_WIDTH) nIdx = 0;
        ...
      } // for c
   }  // for r
   ...
}

ループがハードウェアでパイプライン処理されると、配列へのアクセスは時間的にオーバーラップします。コンパイラで配列へのすべてのアクセスが解析され、N 回目の書き込みによりN + K 回目のデータが上書きされて値が変わってしまうような条件があると、警告メッセージが表示されます。この警告により、II = 1 のパイプラインはインプリメントされません。

次に、インデックス 0 ~ 9 の配列に対して読み出しと書き込みをループで複数回実行する例を示します。上記のコードと同様に、読み出しと書き込みのアドレス カウンターが異なり、ループのすべての繰り返しが終了する前に 0 に戻ることがあります。これらの演算は、パイプライン処理されたインプリメンテーションと同様、時間的にオーバーラップします。


R4---------W8
  R5---------W9
    R6---------W0
      R7---------W1
        R8–––------W2
          R9--------W3
            R0--------W4
              R1--------W5
                R2--------W6

各繰り返しが次の繰り返しの開始前に終了するシーケンシャル C コードでは、読み出しと書き込みの順序は明確ですが、同時実行されるハードウェア パイプラインではアクセスが重なり、異なる順序で実行されます。上記に示すように、インデックス 8 (R8) からの読み出しが、R8 の前に実行されるはずであったインデックス 8 (W8) への書き込みよりも前に実行される可能性があります。

コンパイラでこの状況に関する警告メッセージが表示され、DEPENDENCE 指示子が false に設定されていて WAR (Write-After-Read) に依存性はないことが示されているので、II=1 で実行されるパイプライン処理ハードウェアが作成されます。

DEPENDENCE 指示子は通常、コードのスタティック解析では認識されない関数外部のアルゴリズム動作と条件をコンパイラに通知するために使用します。DEPENDENCE 指示子が正しく設定されていないと、ハードウェアの結果がソフトウェアでの結果と異なる場合に、ハードウェア エミュレーションで問題が発生します。

オプティカル フローの結果

データ転送とハードウェア関数の両方を最適化したら、ハードウェア関数をコンパイルし直して、イベント トレースを使用してパフォーマンスを解析します。次の図に、イベント トレースの開始を示します。パイプライン処理されたハードウェア関数は前の関数が終了するまで実行されません。各ハードウェア関数はデータが使用可能になるとデータを処理し始めます。

図: トレース結果

次に示すイベント トレースの全体図では、システムのパフォーマンスができるだけ高くなるように、ハードウェア関数とデータ転送のすべてが並列実行されていることが示されています。

図: イベント トレース

レーンの 1 つにカーソルを置くと、アクセラレータのランタイムの期間がポップアップで表示されます。実行時間は 15.5 ms 未満であり、毎秒 60 フレームを達成するための要件である 16.8 ms を満たしています。[AXI State View] ビューのトレースは、次のように色分けされます。

図: [AXI State View] ビューの凡例

Software
Arm® プロセッサ コアで実行。
アクセラレータ
アクセラレータで実行。
Transfer
Arm コアから送信されるデータ。
Receive
Arm プロセッサ コアで受信されるデータ。

ボトムアップ: ステレオ ビジョン アルゴリズム

ステレオ ビジョン アルゴリズムは、水平方向にずらして配置された 2 つのカメラからの画像を使用して、人間の視覚と同様、2 つの視点からの 2 つの映像を表示します。映像からの相対的な深さの情報は、2 つの画像を比較して視差マップを作成することで取得できます。視差マップでは、水平座標のオブジェクトどうしの相対的な位置は値が対応するピクセル位置の映像の深さに反比例するようエンコードされます。

ボトムアップ手法では、Vivado® HLS (高位合成) ツールで既に合成済みの完全に最適化されたデザインから開始し、最適化済みのハードウェア関数を SDSoC 環境でソフトウェアと統合します。

このフローを使用すると、HLS ツールを熟知したハードウェア設計者が高度な HLS 機能を使用してハードウェア関数全体をビルドして最適化した後、それをソフトウェア プログラマが利用できます。

次のセクションでは、ステレオ ビジョンのデザイン例を使用して、HLS ツールで最適化されたハードウェア関数から開始し、SDSoC 環境を使用して、ボードで実行されるハードウェアとソフトウェアを完全なシステムに統合するアプリケーションをビルドする手順を説明します。次の図に、既存のハードウェア関数 stereo_remap_bmSDSoC 環境に組み込んだ最終的なシステムを示します。

図: システムのブロック図



ボトムアップ フローでは、この資料で説明する SDSoC 環境の一般的な最適化手法を逆にします。つまり、最適化済みのハードウェア関数から始めて、それを SDSoC 環境に組み込んだ後、データ転送を最適化します。

ステレオ ビジョンのハードウェア関数の最適化

次のコード例に、既存のハードウェア関数 stereo_remap_bm と最適化プラグマを示します。最適化指示子を確認する前に、この関数の詳細について説明します。

  • readLRinputwriteDispOutwriteDispOut というサブ関数が含まれ、これらも最適化されています。
  • Vivado HLS ツールのビデオ ライブラリ hls_video.h から namespace hls という接頭辞の付いた最適化済みの関数が使用されています。これらのサブ関数では、独自のデータ型 MAT が使用されています。
#include "hls_video.h"
#include "top.h"
#include "transform.h"

void readLRinput (yuv_t *inLR,
	    hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_8UC1>& img_l,
	    hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_8UC1>& img_r,
		int height, int dual_width, int width, int stride)
{

  for (int i=0; i < height; ++i) {
#pragma HLS loop_tripcount min=1080 max=1080 avg=1080
    for (int j=0; j < stride; ++j) {
#pragma HLS loop_tripcount min=1920 max=1920 avg=1920
    #pragma HLS PIPELINE
      yuv_t tmpData = inLR [i*stride + j];	  // from yuv_t array: consume height*stride
      if (j < width)
    	  img_l.write (tmpData & 0x00FF);	// to HLS_8UC1 stream
      else if (j < dual_width)
    	  img_r.write (tmpData & 0x00FF);	// to HLS_8UC1 stream
    }
  }
}

void writeDispOut(hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_8UC1>& img_d,
					yuv_t *dst,
					int height, int width, int stride)
{
  pix_t tmpOut;
  yuv_t outData;

  for (int i=0; i < height; ++i) {
#pragma HLS loop_tripcount min=1080 max=1080 avg=1080
    for (int j=0; j < stride; ++j) {
#pragma HLS loop_tripcount min=960 max=960 avg=960
#pragma HLS PIPELINE
	  if (j < width) {
		tmpOut = img_d.read().val[0];
		outData = ((yuv_t) 0x8000) | ((yuv_t)tmpOut);
		dst [i*stride +j] = outData;
	  }
	  else {
		outData = (yuv_t) 0x8000;
		dst [i*stride +j] = outData;
	  }
    }
  }
}

namespace hls {
void SaveAsGray(
            Mat<IMG_HEIGHT, IMG_WIDTH, HLS_16SC1>& src,
            Mat<IMG_HEIGHT, IMG_WIDTH, HLS_8UC1>& dst)
{
    int height = src.rows;
    int width = src.cols;
    for (int i = 0; i < height; i++) {
#pragma HLS loop_tripcount min=1080 max=1080 avg=1080
        for (int j = 0; j < width; j++) {
#pragma HLS loop_tripcount min=960 max=960 avg=960
#pragma HLS pipeline II=1
            Scalar<1, short> s;
            Scalar<1, unsigned char> d;
            src >> s;

            short uval = (short) (abs ((int)s.val[0]));

            // Scale to avoid overflow.  The right scaling here for a
            // good picture depends on the NDISP parameter during
            // block matching.
            d.val[0] = (unsigned char)(uval >> 1);
            //d.val[0] = (unsigned char)(s.val[0] >> 1);
            dst << d;
        }
    }
}
} // namespace hls

int stereo_remap_bm_new(
        yuv_t *img_data_lr,
        yuv_t *img_data_disp,
        hls::Window<3, 3, param_T > &lcameraMA_l,
        hls::Window<3, 3, param_T > &lcameraMA_r,
        hls::Window<3, 3, param_T > &lirA_l,
        hls::Window<3, 3, param_T > &lirA_r,
        param_T (&ldistC_l)[5],
        param_T (&ldistC_r)[5],
        int height,		 // 1080
        int dual_width,	   // 1920 (two 960x1080 images side by side)
        int stride_in,	    // 1920 (two 960x1080 images side by side)
        int stride_out)	   // 960
{
	int width = dual_width/2; // 960

#pragma HLS DATAFLOW

    hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_8UC1> img_l(height, width);
    hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_8UC1> img_r(height, width);

    hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_8UC1> img_l_remap(height, width);	// remapped left image
    hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_8UC1> img_r_remap(height, width);	// remapped left image
    hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_8UC1> img_d(height, width);

    hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_16SC2> map1_l(height, width);
    hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_16SC2> map1_r(height, width);
    hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_16UC2> map2_l(height, width);
    hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_16UC2> map2_r(height, width);

    hls::Mat<IMG_HEIGHT, IMG_WIDTH, HLS_16SC1> img_disp(height, width);
    hls::StereoBMState<15, 32, 32> state;

// ddr -> kernel streams: extract luma from left and right yuv images
// store it in single channel HLS_8UC1 left and right Mat's
    readLRinput (img_data_lr, img_l, img_r, height, dual_width, width, stride_in);

//////////////////////// remap left and right images, all types are HLS_8UC1 //////////
    hls::InitUndistortRectifyMapInverse(lcameraMA_l, ldistC_l, lirA_l, map1_l, map2_l);
    hls::Remap<8>(img_l, img_l_remap, map1_l, map2_l, HLS_INTER_LINEAR);
    hls::InitUndistortRectifyMapInverse(lcameraMA_r, ldistC_r, lirA_r, map1_r, map2_r);
    hls::Remap<8>(img_r, img_r_remap, map1_r, map2_r, HLS_INTER_LINEAR);

////////// find disparity of remapped images //////////
    hls::FindStereoCorrespondenceBM(img_l_remap, img_r_remap, img_disp, state);
    hls::SaveAsGray(img_disp, img_d);

  // kernel stream -> ddr : output single wide
  writeDispOut (img_d, img_data_disp, height, width, stride_out);

  return 0;
}

int stereo_remap_bm(
        yuv_t *img_data_lr,
        yuv_t *img_data_disp,
        int height,		// 1080
        int dual_width,	// 1920 (two 960x1080 images side by side)
        int stride_in,	// 1920 (two 960x1080 images side by side)
        int stride_out)	// 960
{
//1920*1080
//#pragma HLS interface m_axi port=img_data_lr depth=2073600
//#pragma HLS interface m_axi port=img_data_disp depth=2073600

    hls::Window<3, 3, param_T > lcameraMA_l;
    hls::Window<3, 3, param_T > lcameraMA_r;
    hls::Window<3, 3, param_T > lirA_l;
    hls::Window<3, 3, param_T > lirA_r;
    param_T ldistC_l[5];
    param_T ldistC_r[5];

    for (int i=0; i<3; i++) {
        for (int j=0; j<3; j++) {
            lcameraMA_l.val[i][j]=cameraMA_l[i*3+j];
            lcameraMA_r.val[i][j]=cameraMA_r[i*3+j];
            lirA_l.val[i][j]=irA_l[i*3+j];
            lirA_r.val[i][j]=irA_r[i*3+j];
        }
    }
    for (int i=0; i<5; i++) {
        ldistC_l[i] = distC_l[i];
        ldistC_r[i] = distC_r[i];
    }

    int ret = stereo_remap_bm_new(img_data_lr,
                                img_data_disp,
                                lcameraMA_l,
                                lcameraMA_r,
                                lirA_l,
                                lirA_r,
                                ldistC_l,
                                ldistC_r,
                                height,
                                dual_width,
                                stride_in,
                                stride_out);
    return ret;
}

ハードウェア関数の最適化手法 で説明したように、主に使用される最適化指示子は PIPELINE および DATAFLOW です。このほか、LOOP_TRIPCOUNT 指示子も使用されます。

データ フレームを処理するハードウェア関数の最適化に関する推奨事項に基づいて、PIPELINE 指示子はサンプル レベル (この場合、ピクセル レベル) でデータを処理する for ループすべてに適用されています。これにより、ハードウェア パイプライン処理が使用され、最高パフォーマンスのデザインが達成されます。

LOOP_TRIPCOUNT 指示子が for ループに使用されています。この for ループではループ インデックスの上限が変数で定義されているので、正確な値はコンパイル時には不明です。トリップカウントまたはループ繰り返し数に見積もり値を使用すると、HLS で生成されるレポートに、レイテンシおよび開始間隔 (II) の見積もり値が含まれるようになります。この指示子は、作成されるハードウェアには影響せず、レポートにのみ影響します。

最上位関数 stereo_remap_bm には、最適化されたサブ関数と HLS ビデオ ライブラリ (hls_video.h) からの多くの関数が含まれます。HLS のライブラリ関数の詳細は、『Vivado Design Suite ユーザー ガイド: 高位合成』 (UG902) を参照してください。

HLS ビデオ ライブラリの関数は既に最適化済みであり、できるだけ高パフォーマンスでインプリメントされるようにするためのすべての最適化指示子を含みます。最上位関数には最適化されたサブ関数が含まれているので、DATAFLOW 指示子を使用して、各サブ関数がデータが使用可能になった直後にハードウェアで実行を開始する必要だけがあります。

int stereo_remap_bm(..) {

#pragma HLS DATAFLOW
  readLRinput (img_data_lr, img_l, img_r, height, dual_width, width, stride
  hls::InitUndistortRectifyMapInverse(lcameraMA_l, ldistC_l, lirA_l, map1_l, map2_l);
  hls::Remap<8>(img_l, img_l_remap, map1_l, map2_l, HLS_INTER_LINEAR);
  hls::InitUndistortRectifyMapInverse(lcameraMA_r, ldistC_r, lirA_r, map1_r, map2_r);
  hls::Remap<8>(img_r, img_r_remap, map1_r, map2_r, HLS_INTER_LINEAR);
  hls::Duplicate(img_l_remap, img_l_remap_bm, img_l_remap_pt);
  hls::FindStereoCorrespondenceBM(img_l_remap_bm, img_r_remap, img_disp, state);
  hls::SaveAsGray(img_disp, img_d);
  writeDispOut (img_l_remap_pt, img_d, img_data_disp, height, dual_width, width, stride);

}

SDSoC™ 環境では、データが使用可能になるとすぐに自動的に 1 つのハードウェア関数から次の関数に渡されるので、通常 DATAFLOW 最適化は必要ありません。ただし、この例では stereo_remap_bm 内の関数で HLS のデータ型 hls::stream が使用されています。このデータ型は、Arm® プロセッサでコンパイルできず、SDSoC 環境のハードウェア関数インターフェイスでは使用できません。そのため、最上位ハードウェア関数は stereo_remap_bm である必要があり、サブ関数間で高パフォーマンスの転送が達成できるように DATAFLOW 指示子が使用されています。このようになっていない場合は、DATAFLOW を削除して、stereo_remap_bm 内の各サブ関数をハードウェア関数として指定できます。

この例のハードウェア関数では、HLS データ型 hls::stream に基づいたデータ型 Mat が使用されています。hls::stream データ型は、シーケンシャルにのみアクセスできるので、データは入力されると出力されます。

  • ソフトウェア シミュレーションでは、hls::stream データ型のサイズは無限です。
  • ハードウェアでは、hls::stream データ型が 1 つのレジスタとしてインプリメントされ、ストリーミング データは前のデータが上書きされる前に消費されると想定されるので、一度に 1 つのデータ値しか格納できません。

最上位関数 stereo_remap_bm をハードウェア関数として指定すると、ソフトウェア環境でこれらのハードウェア型の影響を無視できます。ただし、これらの関数を SDSoC 環境に組み込むと、Arm プロセッサでコンパイルできないので、システムはハードウェア エミュレーションおよびターゲット プラットフォーム上での実行によってのみ検証可能です。

重要: HLS ハードウェア データ型を含むハードウェア関数を SDSoC 環境に組み込む場合は、HLS ツール環境内で C コンパイルおよびハードウェア シミュレーションで完全に検証された状態にしてください。
重要: hls::stream データ型は HLS ツール内で使用するよう設計されており、エンベデッド CPU でソフトウェアを実行するのには不向きなので、このデータ型は最上位関数インターフェイスには含めないようにしてください。

ハードウェア関数のいずれかの引数で HLS ツール特有のデータ型が使用されている場合、関数引数リストにネイティブ C/C++ 型のみを含む最上位 C/C++ ラッパー関数にその関数を含める必要があります。

データ モーション ネットワークの最適化

最適化済みのハードウェア関数を SDSoC 環境のプロジェクトにインポートしたら、まずインターフェイス最適化をすべて削除します。PS とハードウェア関数は、ハードウェア関数とデータ アクセスのデータ型に基づいて管理され、自動的に最適化されます。詳細は、データ モーションの最適化を参照してください。

  • ハードウェア関数の INTERFACE 指示子を削除します。
  • ハードウェア関数引数リストの変数を参照する DATA_PACK 指示子を削除します。
  • 関数引数にネイティブ C/C++ データ型のみを使用するラッパーに最上位関数を含めて、Vivado HLS ツールのハードウェア データ型を削除します。

この例では、アクセラレーションされる関数は 1 つの最上位ハードウェア関数 stereo_remap_bm に含まれています。

int main() {

  unsigned char *inY = (unsigned char *)sds_alloc(HEIGHT*DUALWIDTH);
  unsigned short *inCY = (unsigned short *)sds_alloc(HEIGHT*DUALWIDTH*2);
  unsigned short *outCY = (unsigned short *)sds_alloc(HEIGHT*DUALWIDTH*2);
  unsigned char *outY = (unsigned char *)sds_alloc(HEIGHT*DUALWIDTH);

  // read double wide image from disk
  if (read_yuv_file(inY, DUALWIDTH, DUALWIDTH, HEIGHT, FILEINAME) != 0)
    return -1;

  convert_Y8toCY16(inY, inCY, HEIGHT*DUALWIDTH);

  stereo_remap_bm(inCY, outCY, HEIGHT, DUALWIDTH, DUALWIDTH);

  // write single wide image to disk
  convert_CY16toY8(outCY, outY, HEIGHT*DUALWIDTH);
  write_yuv_file(outY, DUALWIDTH, DUALWIDTH, HEIGHT, ONAME);

  // write single wide image to disk
  sds_free(inY);
  sds_free(inCY);
  sds_free(outCY);
  sds_free(outY);
  return 0;
}

ハードウェアへのメモリ アクセスを最適化する際は、ハードウェア関数に渡されるデータ型を確認することが重要です。関数シグネチャを確認すると、最適化する主要な変数は入力データ ストリーム img_data_lr と出力データ ストリーム img_data_disp であることがわかります。

int stereo_remap_bm( 
        yuv_t *img_data_lr,
        yuv_t *img_data_disp,
        int height,
        int dual_width,
        int stride);

データはシーケンシャルに転送されるので、まず両方の引数のアクセス パターンを SEQUENTIAL と定義します。次の最適化では、memory_attributePHYSICAL_CONTIGUOUS|NON_CACHEABLE に指定して、データ転送がスキャッター ギャザー DMA の動作により中断されないようにします。これにはメモリを sds_lib からの sds_alloc を使用して割り当てるようにする必要もあります。

#include "sds_lib.h"
int main() {

  unsigned char *inY = (unsigned char *)sds_alloc(HEIGHT*DUALWIDTH);
  unsigned short *inCY = (unsigned short *)sds_alloc(HEIGHT*DUALWIDTH*2);
  unsigned short *outCY = (unsigned short *)sds_alloc(HEIGHT*DUALWIDTH*2);
  unsigned char *outY = (unsigned char *)sds_alloc(HEIGHT*DUALWIDTH);

}

最後に、copy 指示子でデータがアクセラレータにコピーされるようにし、共有メモリからアクセスされないようにします。

#pragma SDS data access_pattern(img_data_lr:SEQUENTIAL)
#pragma SDS data mem_attribute(img_data_lr:PHYSICAL_CONTIGUOUS|NON_CACHEABLE)
#pragma SDS data copy(img_data_lr[0:stride*height])
#pragma SDS data access_pattern(img_data_disp:SEQUENTIAL)
#pragma SDS data mem_attribute(img_data_disp:PHYSICAL_CONTIGUOUS|NON_CACHEABLE)
#pragma SDS data copy(img_data_disp[0:stride*height])
int stereo_remap_bm( 
        yuv_t *img_data_lr,
        yuv_t *img_data_disp,
        int height,
        int dual_width,
        int stride);

これらの最適化指示子を使用すると、最も効率的な転送になるように PS と PL 間のメモリ アクセスが最適化されます。

ステレオ ビジョンの結果

Vivado HLS ツールで最適化されたハードウェア関数をラップして HLS のハードウェア データ型がハードウェア関数のインターフェイスに露出しないようにし、インターフェイス指示子を削除してデータ転送を最適化し、ハードウェア関数をコンパイルし直したら、イベント トレースを使用してパフォーマンスを解析します。

次に示すイベント トレースの全体図では、システムのパフォーマンスができるだけ高くなるように、ハードウェア関数とデータ転送のすべてが並列実行されていることが示されています。

図: イベント トレース



レーンの 1 つにカーソルを置くと、アクセラレータのランタイムの期間がポップアップで表示されます。実行時間は 15.86 ms であり、ライブ ビデオの毎秒 60 フレームを達成するために必要な条件の 16.8 ms を満たしています。