Videopose3Dを眺め始めてから3カ月がたちましたが理解ができてないことに気づいたのでそれぞれの関数を調べて理解してみたいと思います。
(ぜんぜんPythonのライブラリについて知らない)
かなり理解不足なのでメモレベルから書き出してます、認識・理解間違いがあるなと思った方はぜひご指摘していただけるとありがたいです。
#まずVideopose3Dとは
facebookresearchが公開している動画の3D推定が行えるコードです。
Caffe2とDetectronを使用しています。
3D human pose estimation in video with temporal convolutions and semi-supervised training
くわしくはこちらから
ちなみに有志の実装を利用してコードを実行しています。
それはこちらをチェックしてみてください。
#コードを理解する1
##infer_simple.py
/detectron_tools/infer_simple.py
このコードでは動画を切り出した写真から、Detectronを用いて2D推定を行ってVideopose3Dの入力になるような結果を出力しています。
この記事では、各命令が呼び出されたときにそれを定義しているコードに遷移してその内容を読み取り理解していきます。
そのため、少し読みずらいところも多いかと思いますが、許してください。
infer_simple.pyの入力:
・動画を切り出した数フレーム(ファイル)
・COCOで検出したキーポイントを出力するための重みファイル
・e2e_keypoint_rcnn_R-101-FPN_s1x.yaml
infer_simple.pyの出力:
・2Dposeが書き込まれた画像集(ファイル)
・.npzファイル
このコード実行時の私のコマンド上での入力から理解のヒントにしていきましょう。
python infer_simple.py
--cfg e2e_keypoint_rcnn_R-101-FPN_s1x.yaml
--output-dir file0703/cheer1(出力するファイル)
--image-ext jpg
--wts model_final.pkl
cut0703(動画を分割した画像がたくさん入ってるファイル)
ちなみにyamlがわからなかったので、頭から失礼しますがすこしyamlについて。
###yamlって何?
構造化されたデータを表現するのに便利なファイルの書き方ルールの一つの名前
引用:yamlとは
yaml型の例として、今回の入力のe2e_keypoint_rcnn_R-101-FPN_s1x.yamlの一部を紹介します。
MODEL:
TYPE: generalized_rcnn
CONV_BODY: FPN.add_fpn_ResNet101_conv5_body
NUM_CLASSES: 2
FASTER_RCNN: True
KEYPOINTS_ON: True
NUM_GPUS: 8
SOLVER:
WEIGHT_DECAY: 0.0001
LR_POLICY: steps_with_decay
BASE_LR: 0.02
GAMMA: 0.1
MAX_ITER: 130000
STEPS: [0, 100000, 120000]
FPN:
FPN_ON: True
MULTILEVEL_ROIS: True
MULTILEVEL_RPN: True
FAST_RCNN:
ROI_BOX_HEAD: head_builder.add_roi_2mlp_head
ROI_XFORM_METHOD: RoIAlign
ROI_XFORM_RESOLUTION: 7
ROI_XFORM_SAMPLING_RATIO: 2
いろんなデータが書き込まれていますね。
それでは本題に戻ってコードを読んでいきたいと思います。
まずc2.py内のimport_detectron_ops命令が呼び出されています。
import detectron.utils.c2 as c2_utils
c2_utils.import_detectron_ops()
遷移してみましょう。
def import_detectron_ops():
"""Import Detectron ops."""
#detectronのopsライブラリを取り入れます
#見つけたらprint('Found Detectron ops lib: {}'.format(ops_path))
detectron_ops_lib = envu.get_detectron_ops_lib()
#caffe2にcustom operatersを含む動的ライブラリをロードする
dyndep.InitOpsLibrary(detectron_ops_lib)
結果としては、caffe2にdetectronのopsライブラリをロードしてcaffe2内でdetectronを使えるようにしているようです!
(いろいろパッケージ入れてるところは省きました)
本コードに戻ります。
# OpenCL may be enabled by default in OpenCV3; disable it because it's not
# thread safe and causes unwanted GPU memory allocations.
cv2.ocl.setUseOpenCL(False)
Opencv2でGPUを使えないようにしています。
使えるようにしても速度はあまり変わらなかった(先輩情報)ので、GPUが無駄にアクセスしないようにするためにやっていると考えられます。
次に最初の関数parse_argsの定義に入ります。
(関数内でも区切って紹介します。読みづらくてすみません!)
def parse_args():
parser = argparse.ArgumentParser(description='End-to-end inference')
parser知らないので調べました。
###parserって何?
ArgumentParserの使い方を簡単にまとめた
こちらを参考に理解してみます。
- Pythonの実行時にコマンドライン引数を取りたいときに有効
- 様々な形式で引数を指定できる
今回のinfer_simple.pyの引数多いんですが、これのおかげでそれを実現できてたのかと理解できました。
ちなみに・・・
- dest:サブコマンド名を格納する属性の名前です。デフォルトはNoneで値は格納されません。
- help:ヘルプ出力に表示されるサブパーサーグループのヘルプです。フォルトはNoneです。
- type:型指定
- default:初期値設定
それでは戻ります。
parser.add_argument(
'--cfg',
dest='cfg',
help='cfg model file (/path/to/model_config.yaml)',
default=None,
type=str
)
parser.add_argument(
'--wts',
dest='weights',
help='weights model file (/path/to/model_weights.pkl)',
default=None,
type=str
)
parser.add_argument(
'--output-dir',
dest='output_dir',
help='directory for visualization pdfs (default: /tmp/infer_simple)',
default='/tmp/infer_simple',
type=str
)
parser.add_argument(
'--image-ext',
dest='image_ext',
help='image file name extension (default: jpg)',
default='jpg',
type=str
)
parser.add_argument(
'--always-out',
dest='out_when_no_box',
help='output image even when no object is found',
action='store_true'
)
parser.add_argument(
'--output-ext',
dest='output_ext',
help='output image file format (default: pdf)',
default='pdf',
type=str
)
parser.add_argument(
'--thresh',
dest='thresh',
help='Threshold for visualizing detections',
default=0.7,
type=float
)
parser.add_argument(
'--kp-thresh',
dest='kp_thresh',
help='Threshold for visualizing keypoints',
default=2.0,
type=float
)
parser.add_argument(
'im_or_folder', help='image or folder of images', default=None
)
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
return parser.parse_args()
一つ目の関数parse_args()では入力をそれぞれのdestという属性に格納していることがわかりました。
次はmain()をよく見ていきます。
def main(args):
glob_keypoints = []
logger = logging.getLogger(__name__)
1行目でglob_keypointsという配列を用意しています。
loggingはコード実行中にログを書くためのモジュールのようです。
Pythonでお手軽にかっこよくloggingを参考にしました。
logはこのコードの内容としては重要そうじゃないので軽い理解で済ませて次に!
merge_cfg_from_file(args.cfg)
cfg.NUM_GPUS = 1
使うGPUの数を1に指定します。
ここではmerge_cfg_from_file関数が呼び出されています。
ここでの入力はargs.cfgつまり、e2e_keypoint_rcnn_R-101-FPN_s1x.yamlです。
detectron/core/config.py内で定義されています。
"""
Most tools in the tools directory take a --cfg option to specify an override
file and an optional list of override (key, value) pairs:
- See tools/{train,test}_net.py for example code that uses merge_cfg_from_file
- See configs/*/*.yaml for example config files
Detectron supports a lot of different model types, each of which has a lot of
different options. The result is a HUGE set of configuration options.
"""
def merge_cfg_from_cfg(cfg_other):
"""Merge `cfg_other` into the global config."""
_merge_a_into_b(cfg_other, __C)
ここでcfgファイルとは何なのか気になったので調べてみました。
###cfg fileって何?
- config fileのことで、設定ファイルのこと
- 変更するかもしれない値(設定値)が書いてあるファイルのこと
それではmerge_cfg_from_cfg関数に戻ると、_merge_a_into_bという関数が二つの引数cfg_other, __Cを取って呼び出されています。
ここのcfg_otherの入力はargs.cfgつまり、e2e_keypoint_rcnn_R-101-FPN_s1x.yamlです。_Cはこの関数を定義しているconfig.py上で定義されています。
__C = AttrDict()
# Consumers can get config by:
# from detectron.core.config import cfg
cfg = __C
上のconfig.pyでCはAttrDictクラスだとわかります。
AttrDictクラスの初期設定(__C)では、継承する仕組みをもっており、"_dict__"がFalseになっています。詳しくはcollections.pyを読んでみましょう。
また、その後cfgに__Cを代入しています。
class AttrDict(dict):
IMMUTABLE = '__immutable__'
def __init__(self, *args, **kwargs):
super(AttrDict, self).__init__(*args, **kwargs)
self.__dict__[AttrDict.IMMUTABLE] = False
def __getattr__(self, name):
if name in self.__dict__:
return self.__dict__[name]
elif name in self:
return self[name]
else:
raise AttributeError(name)
def __setattr__(self, name, value):
if not self.__dict__[AttrDict.IMMUTABLE]:
if name in self.__dict__:
self.__dict__[name] = value
else:
self[name] = value
else:
raise AttributeError(
'Attempted to set "{}" to "{}", but AttrDict is immutable'.
format(name, value)
)
def immutable(self, is_immutable):
"""Set immutability to is_immutable and recursively apply the setting
to all nested AttrDicts.
"""
self.__dict__[AttrDict.IMMUTABLE] = is_immutable
# Recursively set immutable state
for v in self.__dict__.values():
if isinstance(v, AttrDict):
v.immutable(is_immutable)
for v in self.values():
if isinstance(v, AttrDict):
v.immutable(is_immutable)
def is_immutable(self):
return self.__dict__[AttrDict.IMMUTABLE]
それではinfer_simple.pyに戻ります。
args.weights = cache_url(args.weights, cfg.DOWNLOAD_CACHE)
cache_urlの引数はこのとき
⇒第一引数:urlかfile, 第二引数:キャッシュディレクトリ(?)
なお、このときargs.weightはmodel_final.pklなので第一引数はcoco keypointsの重みファイルです。なのでファイルを返します。
def cache_url(url_or_file, cache_dir):
"""Download the file specified by the URL to the cache_dir and return the
path to the cached file. If the argument is not a URL, simply return it as
is.
"""
is_url = re.match(
r'^(?:http)s?://', url_or_file, re.IGNORECASE
) is not None
★if not is_url:
return url_or_file
url = url_or_file
assert url.startswith(_DETECTRON_S3_BASE_URL), \
('Detectron only automatically caches URLs in the Detectron S3 '
'bucket: {}').format(_DETECTRON_S3_BASE_URL)
cache_file_path = url.replace(_DETECTRON_S3_BASE_URL, cache_dir)
if os.path.exists(cache_file_path):
assert_cache_file_is_ok(url, cache_file_path)
return cache_file_path
cache_file_dir = os.path.dirname(cache_file_path)
if not os.path.exists(cache_file_dir):
os.makedirs(cache_file_dir)
logger.info('Downloading remote file {} to {}'.format(url, cache_file_path))
download_url(url, cache_file_path)
assert_cache_file_is_ok(url, cache_file_path)
return cache_file_path
結果的にURLではないのでそのままファイルを返します。
元のコードに戻ります。
assert_and_infer_cfg(cache_urls=False)
これはdetectron/core/config.py内で宣言されている関数でした。
config.pyを見てみたいと思います。
def assert_and_infer_cfg(cache_urls=True, make_immutable=True):
"""Call this function in your script after you have finished setting all cfg
values that are necessary (e.g., merging a config from a file, merging
command line config options, etc.). By default, this function will also
mark the global cfg as immutable to prevent changing the global cfg settings
during script execution (which can lead to hard to debug errors or code
that's harder to understand than is necessary).
"""
if __C.MODEL.RPN_ONLY or __C.MODEL.FASTER_RCNN:
__C.RPN.RPN_ON = True
if __C.RPN.RPN_ON or __C.RETINANET.RETINANET_ON:
__C.TEST.PRECOMPUTED_PROPOSALS = False
if cache_urls:
cache_cfg_urls()
if make_immutable:
cfg.immutable(True)
よって、cache_cfg_urls()とcfg.immutable(True)が呼び出されます
def cache_cfg_urls():
"""Download URLs in the config, cache them locally, and rewrite cfg to make
use of the locally cached file.
"""
__C.TRAIN.WEIGHTS = cache_url(__C.TRAIN.WEIGHTS, __C.DOWNLOAD_CACHE)
__C.TEST.WEIGHTS = cache_url(__C.TEST.WEIGHTS, __C.DOWNLOAD_CACHE)
__C.TRAIN.PROPOSAL_FILES = tuple(
cache_url(f, __C.DOWNLOAD_CACHE) for f in __C.TRAIN.PROPOSAL_FILES
)
__C.TEST.PROPOSAL_FILES = tuple(
cache_url(f, __C.DOWNLOAD_CACHE) for f in __C.TEST.PROPOSAL_FILES
)
global cfgをIMMUTABLE(不変)にしています。
簡単に変えられちゃうと都合が悪いんですね!
assert not cfg.MODEL.RPN_ONLY, \
'RPN models are not supported'
assert not cfg.TEST.PRECOMPUTED_PROPOSALS, \
'Models that require precomputed proposals are not supported'
model = infer_engine.initialize_model_from_cfg(args.weights)
dummy_coco_dataset = dummy_datasets.get_coco_dataset()
最初の4行でモデルの形等を絞って例外を投げます。
modelにキャッシュファイルのパスを引数にとったinitinalize_model_from_cfg関数の戻り値を代入しています!
detectron/core/test_engine.py内の関数initialize_model_from_cfgを使っているので、test_engine.pyを見てみましょう。
少し長くなってしまったのでDetectronの中身を一部理解してみるにまとめました。
データ並列性についても少し勉強していて、かなり長かったです。
結果としては、initialize_model_from_cfg関数によってmodelをDetectionModelHelperクラスに変更して、様々な値やフラグを初期化しているのだとわかりました。
それでは戻ります。
次の行では、dummy_coco_datasetにdummy_coco_datasetを代入します。
if os.path.isdir(args.im_or_folder):
im_list = glob.iglob(args.im_or_folder + '/*' + '.png')
else:
im_list = [args.im_or_folder]
im_list = sorted(im_list)
os.path.isdir()関数は、パス文字列を引数にとり、ディレクトリ(フォルダ)が存在するか否かをBoolean型で返します。
参考:Pythonでファイル・ディレクトリの存在確認
ここでim_or_folderは動画を切り出した画像のフォルダになっているのでif文を実行します。
if文内ではglobモジュールの関数が使われています。
###globモジュールって何?
ワイルドカード"*"などの特殊文字を使って条件を満たすファイル名・ディレクトリ名などパスの一覧をリストやイテレータで取得できるモジュール
glob()の基本的な使い方は、第一引数にパスの文字列を指定し、条件を満たすパスの文字列を要素とするリスト型が取得できるというもの。
引用:Pythonで条件を満たすパスの一覧を再帰的に取得するglobの使い方
その中でも今回はイテレータで一覧を取得できるiglob()を使っています。
これまでの例のようにglob()はパスのリストを生成する。
ファイルやディレクトリが少なければ特に気にする必要はないが、抽出したパスをfor文などで処理する場合は、リストではなくイテレータを使ったほうがメモリ使用量が抑えられる。
iglob()を使うとイテレータが返される。引数はglob()と同じでrecursiveも使える。
よってメモリ使用量を抑えるためにiglobを使っているが、基本的にはglob()と同様に条件を満たすパスの文字列を要素とするリスト型を取得することが目的です。
入力ファイルのパスを得ているということですね!
ここからfor文です。
複数の写真をそれぞれDetectronを用いて人や物体を検出してそのキーポイントやその物体の名前(例えば人や車)などを入力していくと予想できます。
それではfor文の中身に進みます。いざ!
for i, im_name in enumerate(im_list):
out_name = os.path.join(
args.output_dir, '{}'.format(os.path.basename(im_name) + '.' + args.output_ext)
)
logger.info('Processing {} -> {}'.format(im_name, out_name))
im = cv2.imread(im_name)
timers = defaultdict(Timer)
imにはcv2を用いて対象の画像を読み取って帰ってきた値を代入しています。このときGPUは使っていないため、最初にcv2でGPUを使用しないように設定しました。
ここでTimer型のクラスをdefaultdictに代入しています。
Timer型はtimer.pyで宣言されています。
まずはtimer型を見ていきましょう。
class Timer(object):
"""A simple timer."""
def __init__(self):
self.reset()
def tic(self):
# using time.time instead of time.clock because time time.clock
# does not normalize for multithreading
self.start_time = time.time()
def toc(self, average=True):
self.diff = time.time() - self.start_time
self.total_time += self.diff
self.calls += 1
self.average_time = self.total_time / self.calls
if average:
return self.average_time
else:
return self.diff
def reset(self):
self.total_time = 0.
self.calls = 0
self.start_time = 0.
self.diff = 0.
self.average_time = 0.
次にdefaultdictの使い方について勉強します。
Python defaultdictの使い方を読んで理解します。
defaultdictの引数は関数で、複雑な関数をlambda: Timer()のように実行することができ、その値を返すことができます。(辞書の形に変形している)
それでは元のコードに戻ります。
#今の時間
t = time.time()
with c2_utils.NamedCudaScope(0):
cls_boxes, cls_segms, cls_keyps = infer_engine.im_detect_all(
model, im, None, timers=timers
)
infer_engine.im_detect_allという命令が呼び出されており、cls_boxes, cls_segms, cls_keypsに代入されています。
長くなりそうなのでまたまた子記事作成しました。
長くなりませんように!!
Detectronの中身を一部理解してみる2
タイトルがわかりにくくてすみません!
戻ります。
logger.info('Inference time: {:.3f}s'.format(time.time() - t))
for k, v in timers.items():
logger.info(' | {}: {:.
3f}s'.format(k, v.average_time))
if i == 0:
logger.info(
' \ Note: inference on the first image will be slower than the '
'rest (caches and auto-tuning need to warm up)'
)
vis_utils.vis_one_image(
im[:, :, ::-1], # BGR -> RGB for visualization
im_name,
args.output_dir,
cls_boxes,
cls_segms,
cls_keyps,
dataset=dummy_coco_dataset,
box_alpha=0.3,
show_class=True,
thresh=args.thresh,
kp_thresh=args.kp_thresh,
ext=args.output_ext,
out_when_no_box=args.out_when_no_box
)
vis_one_imageはとても入力が多いですね。
そのため、関数の定義もとても長いです。vis.py内で宣言されています。
しかし関数名通り、写真にdetectronを通して検出した3つの内容を張り付ける関数です。
戻ります。次が重要です。
cls_boxes_np = np.asarray(cls_boxes)
#確率だけを格納している
cls_boxes_prob = cls_boxes_np[1][:,4]
idx_max_prob = np.argmax(cls_boxes_prob)
cls_keyps_max_prob = cls_keyps[1][idx_max_prob]
pose_x_y_prob_after_softmax = cls_keyps_max_prob[[0,1,3]]
glob_keypoints.append(np.transpose(pose_x_y_prob_after_softmax))
この場**idx_max_prob = np.argmax(cls_boxes_prob)**のコードから毎回cls_boxesのなかのindexが4の値が比較され、その中で一番大きい値をとるものが選択されているとわかります。
ここで私が実行したときのcls_boxesの値を毎回出力してみます。(一部)
----
INFO infer_simple.py: 164: | misc_bbox: 0.000s
[[], array([[5.7046942e+02, 1.8761989e+02, 7.7123297e+02, 6.3100037e+02,
9.9947828e-01],
[5.4458844e+02, 6.5797253e+02, 6.2466327e+02, 7.1828662e+02,
9.0132654e-01],
[9.4544727e+02, 5.9454633e+02, 1.0471920e+03, 7.1900000e+02,
9.8554206e-01],
[1.0529901e+03, 7.0750732e+02, 1.0926044e+03, 7.1900000e+02,
9.7724348e-02],
[6.3210559e+02, 1.6499963e+02, 6.8398071e+02, 2.4233069e+02,
6.2936760e-02]], dtype=float32)]
INFO infer_simple.py: 154: Processing test_iou_old_cheer_0709_1/image_421.png -> cheer/test_iou_0709/image_421.png.pdf
INFO infer_simple.py: 162: Inference time: 0.182s
INFO infer_simple.py: 164: | im_detect_bbox: 0.120s
INFO infer_simple.py: 164: | im_detect_keypoints: 0.004s
INFO infer_simple.py: 164: | misc_keypoints: 0.058s
INFO infer_simple.py: 164: | misc_bbox: 0.000s
[[], array([[5.5821161e+02, 1.9941788e+02, 7.8241473e+02, 6.3950916e+02,
9.9955076e-01],
[5.3427997e+02, 6.6528180e+02, 6.2090668e+02, 7.1759113e+02,
7.5113767e-01],
[9.3339331e+02, 5.9492816e+02, 1.0447667e+03, 7.1900000e+02,
9.8391336e-01]], dtype=float32)]
ここから、検知したクラスを2段ずつ使って表して縦方向に接続していることがわかります。
各1段目はx0, y0, x1, y1の値で、2段目は各リストの最後の値のみ入っており確率を表しています。
##infer_simple.pyをIOUを用いる形に修正する
1文1文読むのを終えてプログラムの修正ができる段階まで来ました!
ちなみにIOUとは、、
参考:具体的に学ぶ数学
上記のように評価指数の1種です。
私がVideopose3Dを動かしていて直面した問題は何人か動画に写ったときに、間違えて3D推定する対象が違う人に時々移ってしまうということでした。
(きっとしっかりPython勉強されてきた方はすぐにこの問題解決しているんだろうな、、、)(やっと勉強してます!)
ということで、idx_max_prob = np.argmax(cls_boxes_prob)このインデックスの取り方を変更したいと思います。