やってみた!

やってみた!

試したことを中心に、書評や興味のあること、思ったこととか

Jetson NanoでSingleShotPoseを動かす

 SingleShotPoseはMicrosoftが開発した対象物の姿勢を画像から推定するネットワークです。ネットワークの構造はYOLOをヒントに開発されたとあって良く似た構造です。極端に大きなネットワークでは無いのでJetson Nanoで試しに動かしてみます。

*本記事はSDカードの容量をかなり消費します。64GBのカードでも今の残り容量によっては足りなくなることもあります。

github.com

 もともとpython2.7で実装されてましたが、2019年10月にpython3.6へのアップデート版が公開されました。

 ちなみにNVIDIAでもDeep Object Poseというのがあり、実際にロボットのデモ動画もあります。以前、Jetson Nanoで動かしてみたのですが、ネットワークが大きく、1回の演算に2~3秒くらい(もっとかも?)かかりました・・・。

github.com

1.JETPACK4.2.2の準備

 本記事ではDocker Containerを使います。Containerを使うにはJETPACK4.2.2以降が必要です。JETPACK4.2.2のセットアップは次を参考にしてください。

akifukka.hatenablog.com

2.DeepStream-l4t Containerの作成、起動

 singleshotposeはContainerで動かすことにします。まずはl4t-base Container(r32.2.1)を作成、起動します。

sudo xhost +si:localuser:root
sudo docker run --runtime nvidia --network host -it -e DISPLAY=$DISPLAY -v /tmp/.X11-unix/:/tmp/.X11-unix --device /dev/video0:/dev/video0:mwr nvcr.io/nvidia/l4t-base:r32.2.1

3.SingleShotPose関連ファイルのダウンロード

 SingleShotPoseのgitホームページの手順に従いファイルをダウンロードします。コマンドはContainerの端末で実行します。

apt-get insatll git
git clone https://github.com/microsoft/singleshotpose
cd singleshotpose
wget -O LINEMOD.tar --no-check-certificate "https://onedrive.live.com/download?cid=05750EBEE1537631&resid=5750EBEE1537631%21135&authkey=AJRHFmZbcjXxTmI"
wget -O backup.tar --no-check-certificate "https://onedrive.live.com/download?cid=0C78B7DE6C569D7B&resid=C78B7DE6C569D7B%21191&authkey=AP183o4PlczZR78"
tar xf LINEMOD.tar LINEMOD/ape
tar xf backup.tar backup/ape
cd ..

  singleshotposeのサイトでは他に次のファイルについてもダウンロードして解凍するように書かれていますが、学習用のデータなので今回は必要ありません。あと、上ではape(猿の模型)のデータしか解凍しないようにしてあります。それでもContainerは11GBくらい使用します。

ーーーー 今回は学習しないので必要なし -------

wget -O backup_multi.tar --no-check-certificate "https://onedrive.live.com/download?cid=05750EBEE1537631&resid=5750EBEE1537631%21136&authkey=AFQv01OSbvhGnoM"
wget https://pjreddie.com/media/files/VOCtrainval_11-May-2012.tar wget https://pjreddie.com/media/files/darknet19_448.conv.23 -P cfg/
tar xf backup_multi.tar -C multi_obj_pose_estimation/
tar xf VOCtrainval_11-May-2012.tar

---------------------------

 なお、backup_multi.tarはディレクトリ名が書かれているせいでエラーになってダウンロードできません(今回は使いませんが)。上記は修正してあります。

 4.環境構築

 Containerの端末で次を実行します。

  • コンパイラ
    apt-get update
    apt-get install -y --no-install-recommends make g++
  • python3,matplot
    apt install -y python3-pip python3-pil python3-smbus python3-matplotlib cmake
    地域を聞かれるのでAsia-Tokyoを選択します。
  • pytorch1.3
    wget https://nvidia.box.com/shared/static/phqe92v26cbhqjohwtvxorrwnmrnfx1o.whl -O torch-1.3.0-cp36-cp36m-linux_aarch64.whlpip3 install numpy torch-1.3.0-cp36-cp36m-linux_aarch64.whl
  • scipy
    apt-get install python3-scipy
  • opencv
    apt-get install python3-opencv
  • torch vision 0.5
    apt-get install libjpeg-dev zlib1g-dev
    git clone --branch v0.5.0 https://github.com/pytorch/vision torchvision
    cd torchvision
    python3 setup.py install
    cd ..

 5.動かしてみる

  次のコマンドで動かすことができます。jpeg1枚あたりの処理時間は約1秒でした。

cd singleshotpose
python valid.py --datacfg cfg/ape.data --modelcfg cfg/yolo-pose.cfg --weightfile backup/ape/model_backup.weights

 

f:id:akifukka:20191027133700j:plain

 このままでは画面は表示されません。singleshotposeプロジェクト内にあるジュピターファイルvalid.ipynbは画面表示するので、これを参考にvalid.pyを修正します。

import os
import time
import torch
import argparse
import scipy.io
# insert
import matplotlib.pyplot as plt
import numpy
from PIL import Image # end insert
import warnings from torch.autograd import Variable from torchvision import datasets, transforms import dataset from darknet import Darknet from utils import * from MeshPly import MeshPly def valid(datacfg, modelcfg, weightfile): def truths_length(truths, max_num_gt=50): for i in range(max_num_gt): if truths[i][1] == 0: return i # Parse configuration files data_options = read_data_cfg(datacfg) valid_images = data_options['valid'] meshname = data_options['mesh'] backupdir = data_options['backup'] name = data_options['name'] gpus = data_options['gpus'] fx = float(data_options['fx']) fy = float(data_options['fy']) u0 = float(data_options['u0']) v0 = float(data_options['v0']) im_width = int(data_options['width']) im_height = int(data_options['height']) if not os.path.exists(backupdir): makedirs(backupdir) # Parameters seed = int(time.time()) os.environ['CUDA_VISIBLE_DEVICES'] = gpus torch.cuda.manual_seed(seed) save = False # insert visualize = True # end insert testtime = True num_classes = 1 testing_samples = 0.0 # insert edges_corners = [[0, 1], [0, 2], [0, 4], [1, 3], [1, 5], [2, 3], [2, 6], [3, 7], [4, 5], [4, 6], [5, 7], [6, 7]] # end insert if save: makedirs(backupdir + '/test') makedirs(backupdir + '/test/gt') makedirs(backupdir + '/test/pr') # To save testing_error_trans = 0.0 testing_error_angle = 0.0 testing_error_pixel = 0.0 errs_2d = [] errs_3d = [] errs_trans = [] errs_angle = [] errs_corner2D = [] preds_trans = [] preds_rot = [] preds_corners2D = [] gts_trans = [] gts_rot = [] gts_corners2D = [] # Read object model information, get 3D bounding box corners mesh = MeshPly(meshname) vertices = np.c_[np.array(mesh.vertices), np.ones((len(mesh.vertices), 1))].transpose() corners3D = get_3D_corners(vertices) try: diam = float(options['diam']) except: diam = calc_pts_diameter(np.array(mesh.vertices)) # Read intrinsic camera parameters intrinsic_calibration = get_camera_intrinsic(u0, v0, fx, fy) # Get validation file names with open(valid_images) as fp: tmp_files = fp.readlines() valid_files = [item.rstrip() for item in tmp_files] # Specicy model, load pretrained weights, pass to GPU and set the module in evaluation mode model = Darknet(modelcfg) model.print_network() model.load_weights(weightfile) model.cuda() model.eval() test_width = model.test_width test_height = model.test_height num_keypoints = model.num_keypoints num_labels = num_keypoints * 2 + 3 # +2 for width, height, +1 for class label # Get the parser for the test dataset valid_dataset = dataset.listDataset(valid_images, shape=(test_width, test_height), shuffle=False, transform=transforms.Compose([transforms.ToTensor(),])) # Specify the number of workers for multiple processing, get the dataloader for the test dataset kwargs = {'num_workers': 4, 'pin_memory': True} test_loader = torch.utils.data.DataLoader(valid_dataset, batch_size=1, shuffle=False, **kwargs) logging(" Testing {}...".format(name)) logging(" Number of test samples: %d" % len(test_loader.dataset)) # Iterate through test batches (Batch size for test data is 1) count = 0 for batch_idx, (data, target) in enumerate(test_loader): # insert # Images img = data[0, :, :, :] img = img.numpy().squeeze() img = np.transpose(img, (1, 2, 0)) #end insert t1 = time.time() # Pass data to GPU data = data.cuda() target = target.cuda() # Wrap tensors in Variable class, set volatile=True for inference mode and to use minimal memory during inference data = Variable(data, volatile=True) t2 = time.time() # Forward pass output = model(data).data t3 = time.time() # Using confidence threshold, eliminate low-confidence predictions all_boxes = get_region_boxes(output, num_classes, num_keypoints) t4 = time.time() # Evaluation # Iterate through all batch elements for box_pr, target in zip([all_boxes], [target[0]]): # For each image, get all the targets (for multiple object pose estimation, there might be more than 1 target per image) truths = target.view(-1, num_labels) # Get how many objects are present in the scene num_gts = truths_length(truths) # Iterate through each ground-truth object for k in range(num_gts): box_gt = list() for j in range(1, 2*num_keypoints+1): box_gt.append(truths[k][j]) box_gt.extend([1.0, 1.0]) box_gt.append(truths[k][0]) # Denormalize the corner predictions corners2D_gt = np.array(np.reshape(box_gt[:18], [-1, 2]), dtype='float32') corners2D_pr = np.array(np.reshape(box_pr[:18], [-1, 2]), dtype='float32') corners2D_gt[:, 0] = corners2D_gt[:, 0] * im_width corners2D_gt[:, 1] = corners2D_gt[:, 1] * im_height corners2D_pr[:, 0] = corners2D_pr[:, 0] * im_width corners2D_pr[:, 1] = corners2D_pr[:, 1] * im_height preds_corners2D.append(corners2D_pr) gts_corners2D.append(corners2D_gt) # Compute corner prediction error corner_norm = np.linalg.norm(corners2D_gt - corners2D_pr, axis=1) corner_dist = np.mean(corner_norm) errs_corner2D.append(corner_dist) # Compute [R|t] by pnp R_gt, t_gt = pnp(np.array(np.transpose(np.concatenate((np.zeros((3, 1)), corners3D[:3, :]), axis=1)), dtype='float32'), corners2D_gt, np.array(intrinsic_calibration, dtype='float32')) R_pr, t_pr = pnp(np.array(np.transpose(np.concatenate((np.zeros((3, 1)), corners3D[:3, :]), axis=1)), dtype='float32'), corners2D_pr, np.array(intrinsic_calibration, dtype='float32')) # Compute translation error trans_dist = np.sqrt(np.sum(np.square(t_gt - t_pr))) errs_trans.append(trans_dist) # Compute angle error angle_dist = calcAngularDistance(R_gt, R_pr) errs_angle.append(angle_dist) # Compute pixel error Rt_gt = np.concatenate((R_gt, t_gt), axis=1) Rt_pr = np.concatenate((R_pr, t_pr), axis=1) proj_2d_gt = compute_projection(vertices, Rt_gt, intrinsic_calibration) proj_2d_pred = compute_projection(vertices, Rt_pr, intrinsic_calibration) norm = np.linalg.norm(proj_2d_gt - proj_2d_pred, axis=0) # insert proj_corners_gt = np.transpose(compute_projection(corners3D, Rt_gt, intrinsic_calibration)) proj_corners_pr = np.transpose(compute_projection(corners3D, Rt_pr, intrinsic_calibration)) # end insert pixel_dist = np.mean(norm) errs_2d.append(pixel_dist) # insert if visualize: # Visualize plt.xlim((0, im_width)) plt.ylim((0, im_height)) #plt.imshow(scipy.misc.imresize(img, (im_height, im_width)))
plt.imshow(numpy.array(Image.fromarray(numpy.uint8(img*255)).resize((im_width,im_height)))) # Projections for edge in edges_corners: plt.plot(proj_corners_gt[edge, 0], proj_corners_gt[edge, 1], color='g', linewidth=3.0) plt.plot(proj_corners_pr[edge, 0], proj_corners_pr[edge, 1], color='b', linewidth=3.0) plt.gca().invert_yaxis() plt.show() # end insert
# Compute 3D distances transform_3d_gt = compute_transformation(vertices, Rt_gt) transform_3d_pred = compute_transformation(vertices, Rt_pr) norm3d = np.linalg.norm(transform_3d_gt - transform_3d_pred, axis=0) vertex_dist = np.mean(norm3d) errs_3d.append(vertex_dist) # Sum errors testing_error_trans += trans_dist testing_error_angle += angle_dist testing_error_pixel += pixel_dist testing_samples += 1 count = count + 1 if save: preds_trans.append(t_pr) gts_trans.append(t_gt) preds_rot.append(R_pr) gts_rot.append(R_gt) np.savetxt(backupdir + '/test/gt/R_' + valid_files[count][-8:-3] + 'txt', np.array(R_gt, dtype='float32')) np.savetxt(backupdir + '/test/gt/t_' + valid_files[count][-8:-3] + 'txt', np.array(t_gt, dtype='float32')) np.savetxt(backupdir + '/test/pr/R_' + valid_files[count][-8:-3] + 'txt', np.array(R_pr, dtype='float32')) np.savetxt(backupdir + '/test/pr/t_' + valid_files[count][-8:-3] + 'txt', np.array(t_pr, dtype='float32')) np.savetxt(backupdir + '/test/gt/corners_' + valid_files[count][-8:-3] + 'txt', np.array(corners2D_gt, dtype='float32')) np.savetxt(backupdir + '/test/pr/corners_' + valid_files[count][-8:-3] + 'txt', np.array(corners2D_pr, dtype='float32')) t5 = time.time() # Compute 2D projection error, 6D pose error, 5cm5degree error px_threshold = 5 # 5 pixel threshold for 2D reprojection error is standard in recent sota 6D object pose estimation works eps = 1e-5 acc = len(np.where(np.array(errs_2d) <= px_threshold)[0]) * 100. / (len(errs_2d)+eps) acc5cm5deg = len(np.where((np.array(errs_trans) <= 0.05) & (np.array(errs_angle) <= 5))[0]) * 100. / (len(errs_trans)+eps) acc3d10 = len(np.where(np.array(errs_3d) <= diam * 0.1)[0]) * 100. / (len(errs_3d)+eps) acc5cm5deg = len(np.where((np.array(errs_trans) <= 0.05) & (np.array(errs_angle) <= 5))[0]) * 100. / (len(errs_trans)+eps) corner_acc = len(np.where(np.array(errs_corner2D) <= px_threshold)[0]) * 100. / (len(errs_corner2D)+eps) mean_err_2d = np.mean(errs_2d) mean_corner_err_2d = np.mean(errs_corner2D) nts = float(testing_samples) if testtime: print('-----------------------------------') print(' tensor to cuda : %f' % (t2 - t1)) print(' forward pass : %f' % (t3 - t2)) print('get_region_boxes : %f' % (t4 - t3)) print(' prediction time : %f' % (t4 - t1)) print(' eval : %f' % (t5 - t4)) print('-----------------------------------') # Print test statistics logging('Results of {}'.format(name)) logging(' Acc using {} px 2D Projection = {:.2f}%'.format(px_threshold, acc)) logging(' Acc using 10% threshold - {} vx 3D Transformation = {:.2f}%'.format(diam * 0.1, acc3d10)) logging(' Acc using 5 cm 5 degree metric = {:.2f}%'.format(acc5cm5deg)) logging(" Mean 2D pixel error is %f, Mean vertex error is %f, mean corner error is %f" % (mean_err_2d, np.mean(errs_3d), mean_corner_err_2d)) logging(' Translation error: %f m, angle error: %f degree, pixel error: % f pix' % (testing_error_trans/nts, testing_error_angle/nts, testing_error_pixel/nts) ) if save: predfile = backupdir + '/predictions_linemod_' + name + '.mat' scipy.io.savemat(predfile, {'R_gts': gts_rot, 't_gts':gts_trans, 'corner_gts': gts_corners2D, 'R_prs': preds_rot, 't_prs':preds_trans, 'corner_prs': preds_corners2D}) if __name__ == '__main__': # Parse configuration files parser = argparse.ArgumentParser(description='SingleShotPose') parser.add_argument('--datacfg', type=str, default='cfg/ape.data') # data config parser.add_argument('--modelcfg', type=str, default='cfg/yolo-pose.cfg') # network config parser.add_argument('--weightfile', type=str, default='backup/ape/model_backup.weights') # imagenet initialized weights args = parser.parse_args() datacfg = args.datacfg modelcfg = args.modelcfg weightfile = args.weightfile valid(datacfg, modelcfg, weightfile)

 先ほどのコマンドで再度動かしてみます。次は表示例です。実際には1枚づつ表示され、画像を消すと次の画像が順番に表示されます。

f:id:akifukka:20191027132214j:plain
 最後(1050枚)まではとてもつきあえないと思うので、途中でctrl+cで止めてください。以降はContainerの終了、次回以降の再開の準備です。

  • Container終了
    exit

  • 何のContainerか後でわからなくなるので、名前(singleshotpose)を付けます。以降、各コマンドでCONTAINER_IDの代わりに名前をつかえるようになります。
    sudo docker ps -a -s
    ->最後に名前(NAMES)が表示されます。-aは全て、-sは容量も表示の意味です。
    例:hopeful_franklin
    名前をhopefule_franklinからsingleshotposeに変更します。
    sudo rename hopeful_franklin singleshotpose
  • Container起動
    最初の行はJETSON起動後に実行済みなら再度実行する必要はありません。
    sudo xhost +si:localuser:root
    sudo docker start  -i singleshotpose
  • attach
    Container起動で-iオプション(Containerの入出力に端末を接続)をつけ忘れた時は以下のコマンドでContainerに接続できます。
    sudo docker attach singleshotpose
  • Container削除
    もしもうContainerを起動することが無ければ、以下のコマンドでContainerを削除してSDカードの空き領域を増やせます。
    sudo docker rm singleshotpose

6.まとめ

 JETSON NANOでも動くことは確認できましたが、1枚あたり約1秒だとちょっとつらいです・・・。ネットワークはほぼYOLOv2なのでtinyベースに書き換えると精度と引き換えにもっと早く動かせるかも。

 

とりあえず、おしまい 

akifukka.hatenablog.com

Jetson Nano カテゴリーの記事一覧へ