6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Vitis-AI v1.4 on Ultra96v2

Last updated at Posted at 2021-11-05

この記事ではDNNDK on Vitis AI on Ultra96v2をベースに,Vitis-AI v1.4/Vitis 2020.2を用いてUltra96v2向けのプラットフォームを作成し、YOLOv3-tinyを動作させます。手順書のようであまり解説はありません。

Setup

  • Host: Ubuntu 18.04
  • Vitis 2020.2 Unified Software Platform
  • petalinux 2020.2
  • Vitis-AI v1.4
    Xilinx統合環境インストーラをダウンロード
    Petalinuxはこのインストーラを使用してダウンロードしたインストーラを起動して、インストールできた。
# Vitisを選択して/tools/Xilinxにインストール、再度起動してPetalinuxダウンロード
sudo ./Xilinx_Unified_2020.2_1118_1232_Lin64.bin
# Petalinuxインストール
sudo chmod +x /tools/Xilinx/PetaLinux/2020.2/bin/petalinux-v2020.2-final-installer.run 
mkdir ~/petalinux_2020.2/
/tools/Xilinx/PetaLinux/2020.2/bin/petalinux-v2020.2-final-installer.run --dir ~/petalinux_2020.2/
# 2020.2有効化
source /tools/Xilinx/Vivado/2020.2/settings64.sh
source /tools/Xilinx/Vitis/2020.2/settings64.sh
source ~/petalinux_2020.2/settings.sh

便宜上、作業を行うディレクトリをBUILD_HOMEと名前を付けておきます。
必要なものをダウンロードします。

mkdir ultra96_vai1_4 #適当な名前
export BUILD_HOME="$PWD"
#Vitis-AI
git clone https://github.com/Xilinx/Vitis-AI
cd Vitis-AI
git checkout refs/tags/v1.4
export VITIS_AI_HOME="$PWD"

Avnet Vitis platforms

Avnetが用意してくれているハードウェアプラットフォームです。

cd $BUILD_HOME
mkdir Avnet
cd Avnet
git clone https://github.com/Avnet/bdf
git clone -b 2020p2_u96v2_sbc_base_20210426_105325 https://github.com/Avnet/hdl
git clone -b 2020p2_u96v2_sbc_base_20210426_105325 https://github.com/Avnet/petalinux
git clone -b 2020p2_u96v2_sbc_base_20210426_105325 https://github.com/Avnet/vitis

ビルド済みのものをAvnetのコミュニティからダウンロードすることもできます。
Ultra96のページからVitis PetaLinux Platform -> Ultra96-V2 – Vitis Platform 2020+ (Sharepoint site) -> 2020.2/Vitis_Platform/u96v2_sbc_vitis_2020_2.tar.gzをダウンロードできます。
今回は1からやってみたかったので自分でビルドしました。ビルド前にAvnet/petalinuxに以下の修正を適用する必要がありました。Avnet/以下のgitのtagチェックの修正と、petalinuxでFailed to add user layersになってしまう問題の回避をしています。(なぜconfigure_petalinux_project()でキャッシュを無効にしないとだめなのかはよくわかっていません)

diff --git a/scripts/common.sh b/scripts/common.sh
index 72d2256..cca75da 100755
--- a/scripts/common.sh
+++ b/scripts/common.sh
@@ -124,11 +124,11 @@ check_git_tag()
     # Verify the hdl repository is checked out with the correct ${TAG_STRING} tag
     cd ${HDL_FOLDER}
     echo -e "\nVerifying the hdl repository is checked out with the correct ${TAG_STRING} tag.\n"
-    if [ ${TAG_STRING} = $(git status head | head -n1 | cut -d ' ' -f4) ]
+    if [ ${TAG_STRING} = $(git status head | head -n1 | cut -d ' ' -f2) ]
     then
       echo -e "\nReported hdl tag matches ${TAG_STRING}.  Check petalinux tag next...\n"
     else
-      echo -e "\nReported hdl tag is $(git status head | head -n1 | cut -d ' ' -f4).\n"
+      echo -e "\nReported hdl tag is $(git status head | head -n1 | cut -d ' ' -f2).\n"
       echo -e "\nThis does not match requested ${TAG_STRING}.  Exiting now.\n"
       exit
     fi
@@ -136,11 +136,11 @@ check_git_tag()
     # Verify the petalinux repository is checked out with the correct ${TAG_STRING} tag
     cd ${PETALINUX_FOLDER}
     echo -e "\nVerifying the petalinux repository is checked out with the correct ${TAG_STRING} tag.\n"
-    if [ ${TAG_STRING} = $(git status head | head -n1 | cut -d ' ' -f4) ]
+    if [ ${TAG_STRING} = $(git status head | head -n1 | cut -d ' ' -f2) ]
     then
       echo -e "\nReported petalinux tag matches ${TAG_STRING}.  Build will continue...\n"
     else
-      echo -e "\nReported petalinux tag is $(git status head | head -n1 | cut -d ' ' -f4).\n"
+      echo -e "\nReported petalinux tag is $(git status head | head -n1 | cut -d ' ' -f2).\n"
       echo -e "\nThis does not match ${TAG_STRING}.  Exiting now.\n"
       exit
     fi
@@ -339,10 +339,10 @@ configure_petalinux_project()
     git clone -b ${META_AVNET_BRANCH} ${META_AVNET_URL} project-spec/meta-avnet
   fi

-  if [ "$KEEP_CACHE" = "true" ]
-  then
-    configure_cache_path
-  fi
+  # if [ "$KEEP_CACHE" = "true" ]
+  # then
+  #   configure_cache_path
+  # fi

   if [ "$KEEP_WORK" = "true" ]
   then
diff --git a/scripts/make_u96v2_sbc_base.sh b/scripts/make_u96v2_sbc_base.sh
index 8e50ab5..315ba2a 100755
--- a/scripts/make_u96v2_sbc_base.sh
+++ b/scripts/make_u96v2_sbc_base.sh
@@ -71,7 +71,7 @@ DEBUG="no"
 #NO_BIT_OPTION can be set to 'yes' to generate a BOOT.BIN without bitstream
 NO_BIT_OPTION='yes'

-source ${MAIN_SCRIPT_FOLDER}/common.sh
+. ${MAIN_SCRIPT_FOLDER}/common.sh

適用後は以下のようにしてビルドできます。2時間くらいかかりました。

cd $BUILD_HOME/Avnet/vitis
make u96v2_sbc

ダウンロードまたはビルドが完了すれば、Vitisフローで必要になる環境変数を設定しておきます。

export SDX_PLATFORM=$BUILD_HOME/Avnet/vitis/platform_repo/u96v2_sbc_base/u96v2_sbc_base.xpfm

Build the Hardware Project

Vitis-AIリポジトリにある、DPU-TRDを元に作成します。
参考記事と同様、Vitis-AIリポジトリからコピーして修正し、ビルドを行います。
ここでの手順はVitis-AI/dsa/DPU-TRD/prj/Vitis/README.mdを参考にしています。

Vitis-AI v1.1の時とはDPU-TRDの位置が変更されていました。また、READMEにもあるように、新たに$EDGE_COMMON_SWを設定しておく必要があります。

cd $BUILD_HOME
mkdir ultra96v2_vitis_flow_tutorial_1 #適当な名前
cd ultra96v2_vitis_flow_tutorial_1
cp -r $VITIS_AI_HOME/dsa/DPU-TRD ./DPU-TRD-ULTRA96V2
export TRD_HOME=$BUILD_HOME/ultra96v2_vitis_flow_tutorial_1/DPU-TRD-ULTRA96V2
export EDGE_COMMON_SW=$BUILD_HOME/Avnet/petalinux/projects/u96v2_sbc_base_2020_2/images/linux

Ultra96v2向けに、以下の2つの設定ファイルを編集します。

  • $TRD_HOME/prj/Vitis/dpu_conf.vh: DPUの構成を小さいものに変更
26c26
< `define B4096 
---
> `define B1600
  • $TRD_HOME/prj/Vitis/config_file/prj_config: Vivado(HWプラットフォーム)構成に合わせて修正
< freqHz=300000000:DPUCZDX8G_1.aclk
< freqHz=600000000:DPUCZDX8G_1.ap_clk_2
< freqHz=300000000:DPUCZDX8G_2.aclk
< freqHz=600000000:DPUCZDX8G_2.ap_clk_2
---
> id=0:DPUCZDX8G_1.aclk
> id=1:DPUCZDX8G_1.ap_clk_2
30,32d27
< sp=DPUCZDX8G_2.M_AXI_GP0:HPC0
< sp=DPUCZDX8G_2.M_AXI_HP0:HP2
< sp=DPUCZDX8G_2.M_AXI_HP2:HP3
35c30
< nk=DPUCZDX8G:2
---
> nk=DPUCZDX8G:1

修正が完了したら、ビルドします。

cd $TRD_HOME/prj/Vitis/
make KERNEL=DPU DEVICE=ULTRA96V2

ハードウェアのビルドはそれなりに時間がかかるので、待ちます。
完了すると、以下に必要なものができあがっています。

tree binary_container_1/sd_card
binary_container_1/sd_card
├── BOOT.BIN
├── Image
├── arch.json
├── boot.scr
├── dpu.xclbin
├── image.ub
├── init.sh
├── platform_desc.txt
├── rootfs.tar.gz
└── u96v2_sbc_base.hwh

Compile the Models from the Xilinx Model Zoo

Vitis-AIコンテナをビルドします。CPUしか使わない場合はdocker pull xilinx/vitis-ai:1.4.916でセットアップできます。

cd $VITIS_AI_HOME/setup/docker
./docker_build_gpu.sh

XilinxがAI-Model-Zooで公開しているモデルのうち、いくつかのモデルはUltra96での性能が公開されています:
https://github.com/Xilinx/Vitis-AI/tree/master/models/AI-Model-Zoo#performance-on-ultra96

AI-Model-Zooにはtiny-yolov3モデルが存在するのですが、なぜかUltra96での性能が公開されていません。しかも、このtiny_yolov3_vmssはクラスカテゴリが['KELLOGS,CHOCOLATE,CANDLE,SHAMPOO,BULB,PLIERS,DETERGENT,KOOLAID,LIPSTICK,BOX']となっていて、データセットも見つかりません・・

そこで、今回はVitis-AI v1.1で動作させたことのあるtiny_yolov3によるface mask検出を動作させてみようと思います。
Real-time tiny-YOLOv3 face mask detection on Ultra96v2
tiny-yolov3の学習方法については元記事で紹介しています。学習済みのモデル・重みはGitHubで公開しています。元記事でも記述の通り、オリジナルのyolov3-tiny.cfgから変更を加えています。(darknetからcaffeへの変換の失敗を防ぐためmaxpoolのサイズを1箇所変更)
量子化に必要なデータセットはYOLOv3-face-mask-detectionからダウンロードします。

モデルのコンパイルはVitis-AI dockerコンテナ環境で行います。

cd $VITIS_AI_HOME
mkdir yolov3_tiny_model #ここにモデルとデータセット(mask.zip)を配置
cd yolov3_tiny_model
unzip mask.zip
cd $BUILD_HOME
./Vitis-AI/docker_run.sh xilinx/vitis-ai-gpu:1.4.916 
#以下dockerコンテナ内で作業
conda activate vitis-ai-caffe
cd /workspace/Vitis-AI/yolov3_tiny_model

Darknet2Caffe

DarknetからCaffeに変換するスクリプトがXilinxから提供されています。

python3 ../models/AI-Model-Zoo/caffe-xilinx/scripts/convert.py yolov3-tiny_mask.cfg yolov3-tiny_mask_60000.weights yolov3_tiny_mask.prototxt yolov3_tiny_mask.caffemodel

Quantization

Vitis-AI Quantizerを使用してモデルの量子化を行います。量子化のためのprototxtを作成します。

cp yolov3_tiny_mask.prototxt for_quantize.prototxt

量子化のスケールを最適化するために、実際に推論で使用する画像を使用してキャリブレーションを行います。
for_quantize.prototxtの1〜6行目を削除し、以下のようにキャリブレーション画像へのパスを設定します。

diff yolov3_tiny_mask.prototxt for_quantize.prototxt 
1,6c1,18
< name: "Darkent2Caffe"
< input: "data"
< input_dim: 1
< input_dim: 3
< input_dim: 224
< input_dim: 224
---
> layer {
>   name: "data"
>   type: "ImageData"
>   top: "data"
>   top: "label"
>   include {
>     phase: TRAIN
>   }
>   image_data_param {
>     source: "./quant.txt"
>     batch_size: 16
>   }
>   transform_param {
>     mirror: false
>     yolo_width: 416
>     yolo_height: 416
>   }
> }

画像ファイルリストをquant.txtとして作成します。以下のPythonコードで作成しました。

import os
import glob
calib_images = glob.glob('./mask/*.jpg')
for calib_image in calib_images:
    print(os.path.abspath(calib_image), 0)
python3 gen_quantize_list.py > quant.txt
vai_q_caffe quantize \
    -model ./for_quantize.prototxt \
    -weights ./yolov3_tiny_mask.caffemodel \
    --keep_fixed_neuron \
    -calib_iter 100 \
    -gpu 0

量子化が完了すると、以下のようにファイルが生成されます。

...
I1105 12:47:51.496042  7397 vai_q.cpp:368] Deploy Done!
--------------------------------------------------
Output Quantized Train&Test Model:   "./quantize_results/quantize_train_test.prototxt"
Output Quantized Train&Test Weights: "./quantize_results/quantize_train_test.caffemodel"
Output Deploy Weights: "./quantize_results/deploy.caffemodel"
Output Deploy Model:   "./quantize_results/deploy.prototxt"

Compilation

次に、DPU向けにモデルのコンパイルを行います。
ここで、DPUの構成に応じてコンパイルをする必要があり、Step1で生成されたarch.jsonを使用します。
ちなみに、Vitis-AI v1.2まではDPU情報を表記したhwhファイルからdletを用いてdcfを生成していましたが、dletはv1.3以降docker環境に存在しないようです。

cp /workspace/ultra96v2_vitis_flow_tutorial_1/DPU-TRD-ULTRA96V2/prj/Vitis/binary_container_1/sd_card/arch.json ./
vai_c_caffe \
    -a arch.json \
    -p ./quantize_results/deploy.prototxt \
    -c ./quantize_results/deploy.caffemodel \
    -o ./compiled \
    -n yolov3_tiny_mask
**************************************************
* VITIS_AI Compilation - Xilinx Inc.
**************************************************
[INFO] Namespace(batchsize=1, inputs_shape=None, layout='NCHW', model_files=['./quantize_results/deploy.caffemodel'], model_type='caffe', named_inputs_shape=None, out_filename='/tmp/yolov3_tiny_mask_org.xmodel', proto='./quantize_results/deploy.prototxt')
[INFO] caffe model: /workspace/Vitis-AI/yolov3_tiny_model/quantize_results/deploy.caffemodel
[INFO] caffe model: /workspace/Vitis-AI/yolov3_tiny_model/quantize_results/deploy.prototxt
[INFO] parse raw model     :100%|██████████████████████████████████████████████████████████████████| 48/48 [00:01<00:00, 38.79it/s]                  
[INFO] infer shape (NCHW)  :100%|██████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 2231.09it/s]                
[INFO] infer shape (NHWC)  :100%|██████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 3651.45it/s]                
[INFO] perform level-1 opt :100%|██████████████████████████████████████████████████████████████████| 3/3 [00:00<00:00, 1443.49it/s]                  
[INFO] generate xmodel     :100%|██████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 647.94it/s]                 
[INFO] dump xmodel: /tmp/yolov3_tiny_mask_org.xmodel
[UNILOG][INFO] Compile mode: dpu
[UNILOG][INFO] Debug mode: function
[UNILOG][INFO] Target architecture: DPUCVDX8G_ISA0_B1600_01000020F6014404
[UNILOG][INFO] Graph name: deploy, with op num: 100
[UNILOG][INFO] Begin to compile...
[UNILOG][WARNING] xir::Op{name = layer11-maxpool, type = pool-fix}'s input and output is unchanged, so it will be removed.
[UNILOG][INFO] Total device subgraph number 4, DPU subgraph number 1
[UNILOG][INFO] Compile done.
[UNILOG][INFO] The meta json is saved to "/workspace/Vitis-AI/yolov3_tiny_model/./compiled/meta.json"
[UNILOG][INFO] The compiled xmodel is saved to "/workspace/Vitis-AI/yolov3_tiny_model/./compiled/yolov3_tiny_mask.xmodel"
[UNILOG][INFO] The compiled xmodel's md5sum is 5476c1153bfe24bc2665ca081d449edb, and has been saved to "/workspace/Vitis-AI/yolov3_tiny_model/./compiled/md5sum.txt"

DPU subgraph number 1となっているので理想的にDPUにオフロードされていそうです。
netronを使用してコンパイルしたモデルyolov3_tiny_mask.xmodelを確認できました。

prototxt

今回使用するXilinxの評価アプリケーションはモデルの情報をprototxtファイルから読み込みます。
AI-Model-Zooにあるtiny-yolov3を参考に以下のprototxtファイルを./compiled/tiny-yolov3.prototxtとして保存しました。

model {
  name: "yolov3_tiny_mask"
  kernel {
     name: "yolov3_tiny_mask_0"
     mean: 0.0
     mean: 0.0
     mean: 0.0
     scale: 0.00390625
     scale: 0.00390625
     scale: 0.00390625
  }
  model_type : YOLOv3
  yolo_v3_param {
    num_classes: 3
    anchorCnt: 3
    layer_name: "15"
    layer_name: "22"
    conf_threshold: 0.3
    nms_threshold: 0.45
    biases: 10
    biases: 14
    biases: 23
    biases: 27
    biases: 37
    biases: 58
    biases: 81
    biases: 82
    biases: 135
    biases: 169
    biases: 344
    biases: 319
    test_mAP: false
  }
}

Compile the AI Applications

クロスコンパイル環境を整えてyolov3の評価アプリケーションをホスト上でビルドします。
本手順はVitis-AIドキュメントのstep1-setup-cross-compilerに沿っています。
この作業はdockerコンテナではなくホスト環境で行います。

Vitis2020.2を使用しているので./host_cross_compiler_setup_2020.2.shを実行する

cd $VITIS_AI_HOME/setup/mpsoc
./host_cross_compiler_setup_2020.2.sh
unset LD_LIBRARY_PATH
source /home/`$USER`/petalinux_sdk_2020.2/environment-setup-aarch64-xilinx-linux

クロスコンパイル環境が整ったのでアプリケーションをビルドします。
yolov3含め多数モデル向けの評価アプリケーションが公開されています。

cd $VITIS_AI_HOME/demo/Vitis-AI-Library/samples/yolov3
sh build.sh

test_jpeg_yolov3, test_accuracy_yolov3_mt, test_performance_yolov3, test_video_yolov3の4つの実行ファイルが生成されています。

Create the SD card

これまでに作成したものを集めてきて、Ultra96v2で使用するSDカードを作成します。
Gpartedなどを使用して第一パーティションをFAT, 第二パーティションをext4でフォーマットします。
(それぞれBOOT, rootfsとラベルを付けました。)

  • ブートに必要なもの
cd $TRD_HOME/prj/Vitis/binary_container_1/sd_card
cp dpu.xclbin /media/lp6m/BOOT/
cp BOOT.BIN  /media/lp6m/BOOT/
cp image.ub /media/lp6m/BOOT/
cp boot.scr /media/lp6m/BOOT/
cp Image /media/lp6m/BOOT/
sudo tar xvf rootfs.tar.gz -C /media/lp6m/rootfs/
  • VART(Vitis-AI-Runtime)
cd $VITIS_AI_HOME/setup/mpsoc
sudo cp -r VART /media/lp6m/rootfs/home/root/
  • yolov3 model, application, test image (video)
cd $VITIS_AI_HOME/yolov3_tiny_model
sudo cp -r compiled /media/lp6m/rootfs/home/root/yolov3_tiny_mask
sudo cp -r mask /media/lp6m/rootfs/home/root
cd $VITIS_AI_HOME/demo/Vitis-AI-Library/samples
sudo cp -r yolov3 /media/lp6m/rootfs/home/root/yolov3_app
sync

syncが完了してからSDを取り外します。

  • SDカード
  • Ultra96V2
  • 電源

Execute the AI applications on hardware

SDカード、電源、HDMIアダプタ、キーボード、マウス、Webカメラなどを繋いで本体を起動します。
参考:Ultra96-V2 で開発を行う際に必要なもの & あるといいもの
以降の作業はUltra96上で行います。

Wifiの設定

ターミナルを開いて、/home/root/wpa_supplicant.confを編集してSSIDとパスワードを設定します。

./wifi.sh
ifconfig -a

これでネットワークにつながるので、sshもできるようになります。(パスワードはroot)

VART(Vitis-AI Runtime)の実機へのインストール

VART(Vitis-AI Runtime)の実機へのインストールを参考にします。
無事起動したらVARTを実機側にもインストールします。

cd /home/root/VART
./target_vart_setup_2020.2.sh 

dexplorerでDPU情報確認

dexplorerでDPUのバージョンや有効になっている機能をチェックすることができます。
/usr/lib/にカーネルイメージdpu.xclbinをコピーする必要があります。

cp /mnt/sd-mmcblk0p1/dpu.xclbin /usr/lib/
dexplorer -w
[DPU IP Spec]
IP  Timestamp            : 2021-06-07 19:15:00
DPU Core Count           : 1

[DPU Core Configuration List]
DPU Core                 : #0
DPU Enabled              : Yes
DPU Arch                 : B1600
DPU Target Version       : v1.4.1
DPU Freqency             : 150 MHz
Ram Usage                : Low
DepthwiseConv            : Enabled
DepthwiseConv+Relu6      : Enabled
Conv+Leakyrelu           : Enabled
Conv+Relu6               : Enabled
Channel Augmentation     : Enabled
Average Pool             : Enabled

Test Performance

評価アプリケーションは/usr/share/vitis_ai_library/models内のモデルを対象に実行するので、コンパイルしたモデルをコピーします。

mkdir /usr/share/vitis_ai_library/models
cp -r yolov3_tiny_mask /usr/share/vitis_ai_library/models/

パフォーマンス計測には画像が1枚以上必要なので、画像リストimage.listを作成します。

cd ~/yolov3_app
echo "/home/root/mask/Mask_0.jpg" > image.list
./test_performance_yolov3 yolov3_tiny_mask image.list
WARNING: Logging before InitGoogleLogging() is written to STDERR
I1105 19:42:33.142753  1657 benchmark.hpp:184] writing report to <STDOUT>
I1105 19:42:33.143976  1657 benchmark.hpp:211] waiting for 0/30 seconds, 1 threads running
I1105 19:42:43.144228  1657 benchmark.hpp:211] waiting for 10/30 seconds, 1 threads running
I1105 19:42:53.144506  1657 benchmark.hpp:211] waiting for 20/30 seconds, 1 threads running
I1105 19:43:03.144932  1657 benchmark.hpp:219] waiting for threads terminated
FPS=28.2217
E2E_MEAN=35403.1
DPU_MEAN=32646.8

画像サイズ416x416のyolov3-tinyはUltra96v2上(DPU1600, 150Mhz)で28FPSが達成されることがわかりました。 グラフ全体をDPUにオフロードできたため、End-to-Endの時間のうちほとんどの時間がDPUの時間を占めています。

Image Test

./test_jpeg_yolov3 yolov3_tiny_mask ../mask/Mask_74.jpg
I1105 20:43:09.306178  1530 demo.hpp:1183] batch: 0     image: ../mask/Mask_74.jpg
I1105 20:43:09.306447  1530 process_result.hpp:44] RESULT: 0	1701.7	1393.09	2543.82	2229.84	0.983498
I1105 20:43:09.306952  1530 process_result.hpp:44] RESULT: 0	581.677	742.82	1237.53	1579.57	0.777177
I1105 20:43:09.307421  1530 process_result.hpp:44] RESULT: 2	3082.35	219.546	4175.43	1562.61	0.990156

残念ながら評価アプリケーションはbboxを描画してくれないようなので適当に手元で描画した結果はこのようになりました。

今回は精度評価などは行なっていませんがある程度認識できているように思います。

Video Test

最近のニュース番組を使うと良い感じになります。

export DISPLAY=:0.0
xrandr --output DP-1 --mode 640x480
./test_vido_yolov3 yolov3_tiny_mask youtube.mp4

解像度を変更してから実行したのですが動画の評価を実行中は画面のちらつきがものすごくひどくなってしまいました。

まとめ

とりあえずVitis-AI v1.4 / Vitis 2020.2のフローを追うことができた。

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?