aoirint's note

メモ帳

Yolo v3でObject Detectionする(darknet)

https://github.com/pjreddie/darknet

git clone https://github.com/pjreddie/darknet.git

データセットの作成

mydata.data

classes = CLASS_NUM
train = mydata-train.txt
test = mydata-test.txt
names = mydata.names
backup = backup/mydata/

各ファイルパスはdarknetの実行ディレクトリからの相対パス

mydata.names

category1
category2
category3
...
categoryCLASS_NUM

ラベル=[クラスの数値表現]に対応するクラス名(行番号=ラベル)を記述する。

mydata-train.txtmydata-test.txtにはデータセット(訓練データ、テストデータ)に含まれる画像のパスをそれぞれ1画像=1行で記述する。

データセットの画像と同じ階層に、IMAGE.jpgアノテーション情報としてIMAGE.txtを配置する必要がある(1画像=1アノテーションファイル)。

IMAGE.txt

label center_x center_y width height

カラムはスペース区切りで、BoundingBox1つ=1行で記述する。

画像中のオブジェクトの種類はlabel(0始まりの数値)で表現する。

画像中のオブジェクトのBoundingBoxはcenter_xcenter_ywidthheight(オブジェクト中心X座標、オブジェクト中心Y座標、オブジェクト幅、オブジェクト高さ)の4パラメータで表現する。center_xwidthは画像幅で除算、center_yheightは画像高さで除算し、実数で記述する。

設定ファイルの作成

cfgディレクトリ以下のファイルをベースに使う。

batchsubdivision, width, heightなどをメモリサイズなどに応じて変更する。デフォルトでは画像にランダムリサイズがかかるようになっているので、メモリ使用量が変動することに注意。

Yolo v3

cfg/yolov3.cfgをコピーする(mydata-yolov3.cfg)。

CLASS_NUM=クラス数

# L610, 696, 783
classes=CLASS_NUM

# L603, 689, 776
filters=(CLASS_NUM + 5) * 3

# L610, 696, 783
classes=1

# L603, 689, 776
filters=18

Tiny Yolo v3

cfg/yolov3-tiny.cfgをコピーする(mydata-yolov3-tiny.cfg)。

CLASS_NUM=クラス数

# L135, 177
classes=CLASS_NUM

# L127, 171
filters=(CLASS_NUM + 5) * 3

# L135, 177
classes=1

# L127, 171
filters=18

コマンド

darknetの学習済みモデルをダウンロードする。

wget https://pjreddie.com/media/files/darknet53.conv.74
# 学習
./darknet detector train mydata.data mydata-yolov3.cfg darknet53.conv.74

# 画像ファイルでテスト(対話型)
./darknet detector test mydata.data mydata-yolov3.cfg backup/mydata/mydata-yolov3_ITER.weights

# 画像ファイルでテスト
./darknet detector test mydata.data mydata-yolov3.cfg backup/mydata/mydata-yolov3_ITER.weights IMAGE_FILE

※ ファイルパス・カテゴリ名は(たぶん)ASCIIでないとSegmentation Error吐きます

Python

darknet/python/darknet.pylibdarknet.soをctypesでPythonから呼び出すことのできるスクリプトになっているが、Python 2ベースのようなのでPython 3で使うのに便利なインタフェースを作成した。darknet.py_darknet.pyにリネームした。PIL.Imageの場合にtempfileを使わない改修をしたほうがいいかもしれないが、今回は割愛。

(追記 19/10/27) ※ examples/以下にちゃんとしたサンプルがあるみたいです.

_darknet.pydarknet.soを指定してる箇所(https://github.com/pjreddie/darknet/blob/master/python/darknet.py#L48)を環境変数化したりすると汎用性上がると思う。

darknet/python/Darknet.py

import os
import sys

import tempfile

sys.path.append(os.path.dirname(__file__))

from _darknet import *

# 標準出力・標準エラー出力の抑制
def silent(verbose=False):
    def _silent(func):
        def wrapper(*args, **kwargs):
            if not verbose:
                devnull = open(os.devnull, 'w')
                stdout = os.dup(1)
                stderr = os.dup(2)
                os.dup2(devnull.fileno(), 1)
                os.dup2(devnull.fileno(), 2)

            res = func(*args, **kwargs)

            if not verbose:
                os.dup2(stdout, 1)
                os.dup2(stderr, 2)
                devnull.close()

            return res

        return wrapper
    return _silent

class Darknet:
    def __init__(self, meta_file, cfg_file, weights_file, verbose=False):
        @silent(verbose=verbose)
        def _init():
            self.net = load_net(cfg_file.encode('ascii'), weights_file.encode('ascii'), 0)
            self.meta = load_meta(meta_file.encode('ascii'))

        _init()

    # [ name, conf, (x, y, w, h) ]
    # x, y is the center of the object

    def detect_pil(self, img_pil, format='jpg', verbose=False):
        with tempfile.NamedTemporaryFile(suffix='.%s' % os.path.basename(format)) as fp:
            filename = fp.name
            img_pil.save(fp.name)

            return self.detect_file(filename, verbose)

    def detect_file(self, img_file, verbose=False):
        @silent(verbose=verbose)
        def _detect():
            return detect(self.net, self.meta, img_file.encode('ascii'))

        results = _detect()

        ret = []
        for result in results:
            name, conf, box = result
            ret.append((
                name.decode('utf-8'),
                conf,
                box,
            ))

        return ret

if __name__ == "__main__":
    import argparse
    import time

    parser = argparse.ArgumentParser()
    parser.add_argument('meta', type=str) # *.data
    parser.add_argument('cfg', type=str) # *.cfg
    parser.add_argument('weights', type=str) # *.weights
    parser.add_argument('img', type=str)
    parser.add_argument('-v', '--verbose', action='store_true')

    args = parser.parse_args()

    darknet = Darknet(meta_file=args.meta, cfg_file=args.cfg, weights_file=args.weights, verbose=args.verbose)

    ts = time.time()
    # direct
    results = darknet.detect_file(args.img, verbose=args.verbose)

    # PIL example
    # from PIL import Image
    # img_pil = Image.open(args.img)
    # results = darknet.detect_pil(img_pil, verbose=args.verbose)

    elapsed = time.time() - ts

    print('FPS: %f (%f s)' % (1/elapsed, elapsed))

    print('%d boxes found' % len(results))
    for result in results:
        name, conf, box = result
        print(name, conf, box)

参考

付録

makeでエラーが出るとき

Path to libdevice library not specified環境変数PATHにcuda/binを追加(export PATH=/usr/local/cuda/bin:$PATH

train/test 分割

ImageFolder形式(クラス別ディレクトリ)、1ディレクトリ形式(クラス問わず同ディレクトリ)に対応。アノテーションデータは別途作成する。

SplitImageFolder.py

import os
import random
from tqdm import trange

if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('indir', type=str)
    parser.add_argument('test_rate', type=float)
    parser.add_argument('--flat-input', action='store_true')
    parser.add_argument('--out-train', type=str, default='train.txt')
    parser.add_argument('--out-test', type=str, default='test.txt')
    args = parser.parse_args()

    current_dir = os.path.realpath(args.indir)

    files = []
    if args.flat_input:
        for imgfile in os.listdir(current_dir):
            imgpath = os.path.join(current_dir, imgfile)
            files.append(imgpath)
    else:
        for catfile in os.listdir(current_dir):
            catpath  = os.path.join(current_dir, catfile)
            for imgfile in os.listdir(catpath):
                imgpath = os.path.join(catpath, imgfile)
                files.append(imgpath)

    test_end = int( len(files) * args.test_rate )
    assert len(files) - test_end > 0, args.test_rate

    print('Data num: %d' % len(files))
    print('Train: %d, Test: %d' % (len(files)-test_end, test_end))

    indexes = list(range(len(files)))
    random.shuffle(indexes)

    with open(args.out_train, 'w') as file_train:
        with open(args.out_test, 'w') as file_test:
            for index in trange(len(files)):
                file_idx = indexes[index]

                imgpath = os.path.realpath(files[file_idx])
                imgfile = os.path.basename(imgpath)
                _, imgext = os.path.splitext(imgfile)
                if imgext == '.txt':
                    continue

                if index < test_end:
                    file_test.write(imgpath + "\n")
                else:
                    file_train.write(imgpath + "\n")