高位合成でFPGA開発!最短 1日で映像リサイズ機能を実装する

f:id:apt-k-ueno:20200107190949j:plain

aptpodでは複数のカメラをフレーム単位で同期させて映像を取得できるカメラデバイスの開発を行なっています。前日の記事では、このカメラデバイスのエンコードを担当するSoCの話でしたが、aptpod Advent Calendar 2019 13日目の今回は映像のフロントエンドに使用しているFPGAについての話題です。

カメラデバイスを開発する上で、FPGAでイメージセンサから取得した画像データをリサイズする機能を実装する必要が出てきたのですが、RTL設計経験のない私でも流行りの高位合成でサクッと実装できた話をまとめます。

前日に続き塩出が担当します。

話の流れ

  • まずは高位合成の説明
  • 高位合成での実装手順
    • アルゴリズムのC++ソース記述方法
    • C++でのテストベンチ記述方法
    • シミュレーション結果の確認
  • まとめ

高位合成とは?

高位合成の詳しい話は色々記事が出ておりますので、そちらを参照してください。

ざっくり言えば、C/C++ベースのアルゴリズムをRTL(レジスタ転送レベル)に変換してくれるツールになります。HDL(ハードウェア記述言語)であるverilogやVHDLなどで記述するよりも複雑な処理を記述しやすく、うまく使えば実装時間が短縮できると思います。ただし、動作はRTLなのでRTLも少し知っていた方が思い通りの動作をさせやすくなるのかなと思います。

今回使用した環境

  • FPGA :Xilinx社製のFPGA,Artix-7 speed grade 1
  • ツール:Xilinx社製 Vivado HLS 2018.3,Vivado 2018.3,SDK 2018.3

これらのツールはXilinxのダウンロードページからダウンロードできます。

インストールの方法や基本的なツールの使用方法は省略します。

高位合成で実現したいこと

今回のシステムでは、YUV422の画像のデータがAxi Streamとして流れています(xilinxでは画像データは基本的にこのAxi Streamで扱うようです)。このストリームデータを受け取って、フルサイズから1/4サイズにリサイズして、同じくAxi Streamで流したいという要望がありました。また、リサイズするかしないかはAxi Liteのアクセスで変えられるようにしたい,画像のサイズも同じく変えられるようにしたいという要望がありました。

また、BRAMがカツカツだったのでラインバッファを使わない、単純な間引きで実装したいという思いがありました。

まとめますと、以下のような要望になります。

  • YUV422のデータを1/4にリサイズできること
    • リサイズにはラインバッファを使わず,単純な間引きをすること
  • Axi Stream入力,出力できること
  • 画像のサイズ,リサイズ適用制御はAxi Liteから設定可能であること

Axi Streamの入出力、Axi Liteでの制御と考えると、RTLでどうかけばいいのか全く検討がつきませんでしたが、色々調べると高位合成だったらサクッと実装できそうだったので挑戦してみました。

高位合成での実装手順

実装の前にアルゴリズムの説明

実装に入る前に,YUV422について触れて間引きのアルゴリズムを説明します。

YUV422とは?

YUV422の説明はこの記事を参考にしてください。Y(輝度成分)は毎ピクセルごと、u,v(色成分)は毎ピクセル交互に格納されています。2つで1つなので、隣り合うY2つに対してuvが1セットです。その仕組み上、幅サイズは偶数になります。

ピクセル数 1 2 ・・・ n-1 n
データ Y1U1 Y2V1 Yn-1Un/2 YnVn/2
YUV422の間引き方法

2ピクセルで1セットなので、以下の図のように幅方向を間引く時は4ピクセルから2セットを抽出します。なので、4の倍数ピクセル目から1つ、その1つ前のピクセルから1つを採用しあとは間引きます。

高さ方向は単純に偶数行を丸っと間引くようにします。

本当なら2ライン分バッファに入れて、上下左右のピクセルから補間するようにするのが一般的だと思いますが、ラインバッファ(BRAM)削減のためこのような単純間引きをしています。

f:id:aptpod_tech-writer:20191211144503p:plain

余談ですが、実際にXilinx が提供しているライブラリにRisizerがあり、こちらはRGBのみ対応ですがlinerの補間をしてくれるものになっております。

アルゴリズムのC++ソース記述方法

上記のアルゴリズムをC++で記述します。いきなりですが、コードを公開します。 メインの関数はF2Q()です。#pragmaがいっぱい入っていて見にくいですが、高位合成では重要な文言ですので我慢して見てください。

各関数をざっくり説明しますと

  • F2Q()
    • いわゆるメイン関数.関数の引数をpragma宣言によってAxi Streamだったり、Axi Liteだったり、関数そのものの実行制御をAxi-Liteから可能にしています。
    • また、メインの処理順序を記述しています。
    • 処理の流れはAxi Streamからデータを読んでフィルタを通してAxi Streamで出すといった感じです。
  • YUV2Full2Quarter_local()
    • この関数では、flagによってリサイズするかしないかを条件分岐させています。
  • YUV2Full2Quarter()
    • この関数が先ほど説明したリサイズのアルゴリズムです。
  • mat_copy()
    • これはリサイズしない時に入力をそのまま出力する関数です。

自分で型宣言していますので、その説明です。

  • typedef hls::stream<ap_axiu<24,1,1,1> >AXI_STREAM

これがAxi Streamの変数型で今回は将来的にRGBも通せるようにデータ幅24bitにしています。

  • typedef hls::Mat<MAX_HEIGHT, MAX_WIDTH, HLS_8UC3> RGB_IMAGE

これは画像を扱う変数型です。テンプレートでMAXサイズを指定した方がメモリのサイズを抑えられるようです。1pixelあたり8bit、3要素で作成しています。

F2Q.cpp

#include "F2Q.h"
#include <stdint.h>

template<int SRC_T, int ROWS,int COLS,int DROWS,int DCOLS>
void mat_copy(hls::Mat<ROWS, COLS, SRC_T>    & src,
        hls::Mat<DROWS, DCOLS, SRC_T>& dst)
{
    HLS_SIZE_T scols = src.cols;
    HLS_SIZE_T srows = src.rows;

    loop_height:for(HLS_SIZE_T h = 0; h < srows; h++){
#pragma HLS LOOP_TRIPCOUNT min=1 max=1

        loop_width:for(HLS_SIZE_T w = 0; w < scols; w++){
#pragma HLS loop_flatten off
#pragma HLS pipeline II=1
#pragma HLS LOOP_TRIPCOUNT min=1 max=1

            hls::Scalar<HLS_MAT_CN(SRC_T), HLS_TNAME(SRC_T)> s;
            src >> s;
            dst << s;
        }
    }
}

template<int SRC_T, int ROWS,int COLS,int DROWS,int DCOLS>
void YUV2Full2Quarter(hls::Mat<ROWS, COLS, SRC_T>& src,
        hls::Mat<DROWS, DCOLS, SRC_T>& dst)
{
    HLS_SIZE_T scols = src.cols;
    HLS_SIZE_T srows = src.rows;
    HLS_SIZE_T dcols = dst.cols;
    HLS_SIZE_T drows = dst.rows;


    loop_height:for(HLS_SIZE_T h = 0; h < srows; h++){
#pragma HLS LOOP_TRIPCOUNT min=1 max=1
        loop_width:for(HLS_SIZE_T w = 0; w < scols; w++){
#pragma HLS LOOP_FLATTEN off
#pragma HLS PIPELINE II=1
#pragma HLS LOOP_TRIPCOUNT min=1 max=1

            hls::Scalar<HLS_MAT_CN(SRC_T), HLS_TNAME(SRC_T)> s;
            src >> s;
            if( (h%2 == 0) &&
                    ( (w%4 == 0) || ((w+1)%4 == 0) ) ){
                dst << s;
            }
        }
    }

}

template<int SRC_T, int ROWS,int COLS,int DROWS,int DCOLS>
void YUV2Full2Quarter_local(hls::Mat<ROWS, COLS, SRC_T>& src,
        hls::Mat<DROWS, DCOLS, SRC_T>& dst,
        bool flag)
{
    if(flag){
        YUV2Full2Quarter(src, dst);
    }else{
        mat_copy(src, dst);
    }
}

void F2Q(AXI_STREAM& input, AXI_STREAM& output,
        uint16_t height, uint16_t width, uint16_t rheight, uint16_t rwidth, bool flag)
{
#pragma HLS_INTERFACE axis register both port=input
#pragma HLS_INTERFACE axis register both port=output
#pragma HLS_INTERFACE s_axilite port=height
#pragma HLS_INTERFACE s_axilite port=width
#pragma HLS_INTERFACE s_axilite port=rheight
#pragma HLS_INTERFACE s_axilite port=rwidth
#pragma HLS_INTERFACE s_axilite port=flag
#pragma HLS_INTERFACE s_axilite port=return


    RGB_IMAGE in_image(height, width);
    RGB_IMAGE out_image(rheight, rwidth);


#pragma HLS dataflow

    hls::AXIvideo2Mat(input, in_image);
    YUV2Full2Quarter_local(in_image, out_image, flag);
    hls::Mat2AXIvideo(out_image, output);

}

F2Q.h

#ifndef F2Q_H
#define F2Q_H

#define MAX_WIDTH  1920
#define MAX_HEIGHT 1080

#include "hls_stream.h"
#include "ap_int.h"
#include "hls_video.h"



typedef hls::stream<ap_axiu<24,1,1,1> >AXI_STREAM;
typedef hls::Mat<MAX_HEIGHT,  MAX_WIDTH, HLS_8UC3> RGB_IMAGE;

typedef hls::Scalar<3, unsigned char> RGB_PIXEL;

void F2Q(AXI_STREAM& input, AXI_STREAM& output,
        uint16_t height, uint16_t width, uint16_t rheight, uint16_t rwidth, bool flag);

#endif /* F2Q_H */

関数のポイント

Axi Streamについて

通常、Axi Streamはバッファを噛ませない限り要素をさかのぼってアクセスすることはできません。なので処理を書くときも要素をさかのぼるような記述をしてはいけません。

さかのぼるとは、例えばライン1とライン2の比較とかがそれに当たります。したければバッファ(LineBufferWindow)にその要素を入れてやる必要があります。ここら辺は実際のハードを意識した書き方をしなければならないようです。

#pragma HLS dataflow

この宣言によって、関数の終了を待たずに次の関数の処理に移れるようになります。イメージは下図を参照してください。

これはAxiStreamのようなデータフローの場合に最適な処理になります。自分のプログラムでは、streamの入力が来たらすぐにそのデータをフィルタに通し、通し終わったものはすぐにstream出力したいので、このpragmaを宣言しています。

f:id:aptpod_tech-writer:20191211144844p:plain

この宣言には1つ注意が必要で、図から明らかなのですが処理は関数ごとに書かないと構文エラーになります。なので、YUV2Full2Quarter_local()ではflagによって実行する関数を分けるだけの処理をさせています。例えばmainの関数の中に条件分岐は書いてはいけません。(#pragma dataflowの前での関数振り分けもできませんでした。)

詳しい説明はXilixの解説ページを見てください。

#pragma HLS LOOP_FLATTEN off

これはonにすると、入れ子になっているループを平滑化して、内側のループ<->外側のループ間の移動に1クロックかかるのをかからなくしてくれるものなのですが、videoの処理ではoffにするようです。おそらくtlastをアサートするからだと思いますが、詳しくはわかっておりません。 詳しい説明はXilinxのページを見てください。

#pragma HLS pipeline

これはループ内処理の終了を待たずに次の処理を行うようにさせ、レイテンシを改善するための宣言です。

f:id:aptpod_tech-writer:20191211144949p:plain

詳しい説明はXilinxのページを見てください。

テストベンチ

先ほどの関数が思った通りの動作をしているかを確認するためのテストベンチになります。 画像サイズ8x6のデータを4x3にリサイズさせています。

元データのpixelデータは要素1に幅成分、要素2に高さ成分、要素3は要素1と要素2を足した値を格納しています。

return 0で成功したとみなされるため、本当は期待値と比較して正しければ0を返すといったように書く方が望ましいですが、期待値を書くのが面倒だったため、printして期待値と同じかをチェックしています。

F2Q_tb.cpp

#include <stdio.h>
#include "../passthru.h"


#define HEIGHT 6
#define WIDTH 8

#define RESIZE

#ifdef RESIZE
#define RHEIGHT HEIGHT/2
#define RWIDTH WIDTH/2
#define FLAG 1
#else
#define RHEIGHT HEIGHT
#define RWIDTH WIDTH
#define FLAG 0
#endif

int main(int argc, char** argv)
{

    AXI_STREAM input;
    AXI_STREAM output;

    RGB_IMAGE in_image(HEIGHT, WIDTH);
    RGB_IMAGE out_image(RHEIGHT, RWIDTH);

    for(int h = 0; h < HEIGHT; h++)
    {
        for(int w = 0; w < WIDTH; w++){
            RGB_PIXEL pixel;
            pixel.val[0] = w;
            pixel.val[1] = h;
            pixel.val[2] = w+h;
            in_image << pixel;
            printf("input:%d, %d, %d\n", pixel.val[0], pixel.val[1], pixel.val[2]);
        }
    }
    hls::Mat2AXIvideo(in_image, input);
    F2Q(input, output, HEIGHT, WIDTH, RHEIGHT, RWIDTH, FLAG);
    hls::AXIvideo2Mat(output, out_image);
    for(int h = 0; h < RHEIGHT; h++)
    {
        for(int w = 0; w < RWIDTH; w++){
            RGB_PIXEL pixel;
            out_image >> pixel;
            printf("output:%d, %d, %d\n", pixel.val[0], pixel.val[1], pixel.val[2]);
        }
    }
    return 0;
}

こちらを実行した結果になります。幅、高さ共に狙った間引きになっているのが確認できます。

input:0, 0, 0
input:1, 0, 1
input:2, 0, 2
input:3, 0, 3
input:4, 0, 4
input:5, 0, 5
input:6, 0, 6
input:7, 0, 7
input:0, 1, 1
input:1, 1, 2
input:2, 1, 3
input:3, 1, 4
input:4, 1, 5
input:5, 1, 6
input:6, 1, 7
input:7, 1, 8
input:0, 2, 2
input:1, 2, 3
input:2, 2, 4
input:3, 2, 5
input:4, 2, 6
input:5, 2, 7
input:6, 2, 8
input:7, 2, 9
input:0, 3, 3
input:1, 3, 4
input:2, 3, 5
input:3, 3, 6
input:4, 3, 7
input:5, 3, 8
input:6, 3, 9
input:7, 3, 10
input:0, 4, 4
input:1, 4, 5
input:2, 4, 6
input:3, 4, 7
input:4, 4, 8
input:5, 4, 9
input:6, 4, 10
input:7, 4, 11
input:0, 5, 5
input:1, 5, 6
input:2, 5, 7
input:3, 5, 8
input:4, 5, 9
input:5, 5, 10
input:6, 5, 11
input:7, 5, 12
output:0, 0, 0
output:3, 0, 3
output:4, 0, 4
output:7, 0, 7
output:0, 2, 2
output:3, 2, 5
output:4, 2, 6
output:7, 2, 9
output:0, 4, 4
output:3, 4, 7
output:4, 4, 8
output:7, 4, 11

こちらの結果は波形でも確認できます。stream のoutができていることが確認できました。

f:id:aptpod_tech-writer:20191211145606p:plain

まとめ

Axi Streamで流れてくる画像のサイズを変更する機能を高位合成で作成した話をまとめました。今回は省いてしまいましたが、高位合成で作成したIPをMicroblazeから制御して画像サイズの変更やresizeするしないなども制御できます。

RTL設計経験がない私でも、Microblazeから制御するのを含めて実働1日程度で実装することができましたので、RTLで書くよりかは早く実装できたのではないかな?と思います。

今回は単純な間引きだけでしたが、バッファを使ってもう少し高度なこともできるので時間があればそのこともまとめてみたいと思います。